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:
authorbradwilson <bradwils@microsoft.com>2012-03-11 21:17:56 +0400
committerbradwilson <bradwils@microsoft.com>2012-03-11 21:17:56 +0400
commit0f8c45fe03e71446fd8287115a1774b549a72314 (patch)
treee26a39eb9ce385aa6993677f44a27446d089c2ff
Initial revision.
-rw-r--r--Runtime.msbuild79
-rw-r--r--Runtime.sln1068
-rw-r--r--Runtime.xunit18
-rw-r--r--Settings.StyleCop109
-rw-r--r--build.cmd30
-rw-r--r--packages/repositories.config41
-rw-r--r--src/AptcaCommonAssemblyInfo.cs11
-rw-r--r--src/CodeAnalysisDictionary.xml56
-rw-r--r--src/CommonAssemblyInfo.cs29
-rw-r--r--src/CommonResources.Designer.cs143
-rw-r--r--src/CommonResources.resx144
-rw-r--r--src/DynamicHelper.cs110
-rw-r--r--src/ExceptionHelper.cs14
-rw-r--r--src/GlobalSuppressions.cs3
-rw-r--r--src/HashCodeCombiner.cs51
-rw-r--r--src/IVirtualPathUtility.cs9
-rw-r--r--src/Microsoft.Web.Helpers/Analytics.cshtml61
-rw-r--r--src/Microsoft.Web.Helpers/Analytics.generated.cs247
-rw-r--r--src/Microsoft.Web.Helpers/Bing.cshtml93
-rw-r--r--src/Microsoft.Web.Helpers/Bing.generated.cs257
-rw-r--r--src/Microsoft.Web.Helpers/Facebook.cshtml819
-rw-r--r--src/Microsoft.Web.Helpers/Facebook.generated.cs1582
-rw-r--r--src/Microsoft.Web.Helpers/FileUpload.cshtml111
-rw-r--r--src/Microsoft.Web.Helpers/FileUpload.generated.cs351
-rw-r--r--src/Microsoft.Web.Helpers/GamerCard.cshtml10
-rw-r--r--src/Microsoft.Web.Helpers/GamerCard.generated.cs90
-rw-r--r--src/Microsoft.Web.Helpers/GlobalSuppressions.cs68
-rw-r--r--src/Microsoft.Web.Helpers/Gravatar.cs97
-rw-r--r--src/Microsoft.Web.Helpers/GravatarRating.cs16
-rw-r--r--src/Microsoft.Web.Helpers/LinkShare.cshtml181
-rw-r--r--src/Microsoft.Web.Helpers/LinkShare.generated.cs461
-rw-r--r--src/Microsoft.Web.Helpers/LinkShareSite.cs14
-rw-r--r--src/Microsoft.Web.Helpers/Maps.cshtml385
-rw-r--r--src/Microsoft.Web.Helpers/Maps.generated.cs761
-rw-r--r--src/Microsoft.Web.Helpers/Microsoft.Web.Helpers.csproj230
-rw-r--r--src/Microsoft.Web.Helpers/PreApplicationStartCode.cs28
-rw-r--r--src/Microsoft.Web.Helpers/Properties/AssemblyInfo.cs13
-rw-r--r--src/Microsoft.Web.Helpers/ReCaptcha.cshtml179
-rw-r--r--src/Microsoft.Web.Helpers/ReCaptcha.generated.cs357
-rw-r--r--src/Microsoft.Web.Helpers/Resources/HelpersToolkitResources.Designer.cs162
-rw-r--r--src/Microsoft.Web.Helpers/Resources/HelpersToolkitResources.resx153
-rw-r--r--src/Microsoft.Web.Helpers/Themes.cs283
-rw-r--r--src/Microsoft.Web.Helpers/Twitter.cshtml279
-rw-r--r--src/Microsoft.Web.Helpers/Twitter.generated.cs1193
-rw-r--r--src/Microsoft.Web.Helpers/UrlBuilder.cs190
-rw-r--r--src/Microsoft.Web.Helpers/Video.cs347
-rw-r--r--src/Microsoft.Web.Helpers/VirtualPathUtilityBase.cs9
-rw-r--r--src/Microsoft.Web.Helpers/VirtualPathUtilityWrapper.cs17
-rw-r--r--src/Microsoft.Web.Http.Data.EntityFramework/DbContextExtensions.cs182
-rw-r--r--src/Microsoft.Web.Http.Data.EntityFramework/DbDataController.cs320
-rw-r--r--src/Microsoft.Web.Http.Data.EntityFramework/GlobalSuppressions.cs14
-rw-r--r--src/Microsoft.Web.Http.Data.EntityFramework/LinqToEntitiesDataController.cs310
-rw-r--r--src/Microsoft.Web.Http.Data.EntityFramework/Metadata/AssociationInfo.cs34
-rw-r--r--src/Microsoft.Web.Http.Data.EntityFramework/Metadata/DbMetadataProviderAttribute.cs92
-rw-r--r--src/Microsoft.Web.Http.Data.EntityFramework/Metadata/LinqToEntitiesMetadataProvider.cs73
-rw-r--r--src/Microsoft.Web.Http.Data.EntityFramework/Metadata/LinqToEntitiesMetadataProviderAttribute.cs92
-rw-r--r--src/Microsoft.Web.Http.Data.EntityFramework/Metadata/LinqToEntitiesTypeDescriptionContext.cs148
-rw-r--r--src/Microsoft.Web.Http.Data.EntityFramework/Metadata/LinqToEntitiesTypeDescriptor.cs271
-rw-r--r--src/Microsoft.Web.Http.Data.EntityFramework/Metadata/MetadataPropertyDescriptorWrapper.cs71
-rw-r--r--src/Microsoft.Web.Http.Data.EntityFramework/Metadata/MetadataWorkspaceUtilities.cs208
-rw-r--r--src/Microsoft.Web.Http.Data.EntityFramework/Metadata/TypeDescriptionContextBase.cs31
-rw-r--r--src/Microsoft.Web.Http.Data.EntityFramework/Metadata/TypeDescriptorBase.cs90
-rw-r--r--src/Microsoft.Web.Http.Data.EntityFramework/Microsoft.Web.Http.Data.EntityFramework.csproj126
-rw-r--r--src/Microsoft.Web.Http.Data.EntityFramework/ObjectContextExtensions.cs90
-rw-r--r--src/Microsoft.Web.Http.Data.EntityFramework/ObjectContextUtilities.cs178
-rw-r--r--src/Microsoft.Web.Http.Data.EntityFramework/Properties/AssemblyInfo.cs6
-rw-r--r--src/Microsoft.Web.Http.Data.EntityFramework/Resource.Designer.cs135
-rw-r--r--src/Microsoft.Web.Http.Data.EntityFramework/Resource.resx144
-rw-r--r--src/Microsoft.Web.Http.Data.EntityFramework/Settings.StyleCop12
-rw-r--r--src/Microsoft.Web.Http.Data.EntityFramework/packages.config5
-rw-r--r--src/Microsoft.Web.Http.Data.Helpers/DataControllerMetadataGenerator.cs391
-rw-r--r--src/Microsoft.Web.Http.Data.Helpers/GlobalSuppressions.cs17
-rw-r--r--src/Microsoft.Web.Http.Data.Helpers/MetadataExtensions.cs35
-rw-r--r--src/Microsoft.Web.Http.Data.Helpers/Microsoft.Web.Http.Data.Helpers.csproj108
-rw-r--r--src/Microsoft.Web.Http.Data.Helpers/Properties/AssemblyInfo.cs6
-rw-r--r--src/Microsoft.Web.Http.Data.Helpers/UpshotExtensions.cs318
-rw-r--r--src/Microsoft.Web.Http.Data.Helpers/packages.config4
-rw-r--r--src/Microsoft.Web.Http.Data/ChangeOperation.cs33
-rw-r--r--src/Microsoft.Web.Http.Data/ChangeSet.cs335
-rw-r--r--src/Microsoft.Web.Http.Data/ChangeSetEntry.cs100
-rw-r--r--src/Microsoft.Web.Http.Data/CustomizingActionDescriptor.cs71
-rw-r--r--src/Microsoft.Web.Http.Data/DataController.cs348
-rw-r--r--src/Microsoft.Web.Http.Data/DataControllerActionInvoker.cs17
-rw-r--r--src/Microsoft.Web.Http.Data/DataControllerActionSelector.cs34
-rw-r--r--src/Microsoft.Web.Http.Data/DataControllerActionValueBinder.cs105
-rw-r--r--src/Microsoft.Web.Http.Data/DataControllerDescription.cs439
-rw-r--r--src/Microsoft.Web.Http.Data/DataControllerValidation.cs99
-rw-r--r--src/Microsoft.Web.Http.Data/DeleteAttribute.cs14
-rw-r--r--src/Microsoft.Web.Http.Data/GlobalSuppressions.cs24
-rw-r--r--src/Microsoft.Web.Http.Data/InsertAttribute.cs14
-rw-r--r--src/Microsoft.Web.Http.Data/Metadata/DataControllerTypeDescriptionProvider.cs100
-rw-r--r--src/Microsoft.Web.Http.Data/Metadata/DataControllerTypeDescriptor.cs245
-rw-r--r--src/Microsoft.Web.Http.Data/Metadata/MetadataProvider.cs111
-rw-r--r--src/Microsoft.Web.Http.Data/Metadata/MetadataProviderAttribute.cs78
-rw-r--r--src/Microsoft.Web.Http.Data/Microsoft.Web.Http.Data.csproj138
-rw-r--r--src/Microsoft.Web.Http.Data/Properties/AssemblyInfo.cs6
-rw-r--r--src/Microsoft.Web.Http.Data/QueryFilterAttribute.cs131
-rw-r--r--src/Microsoft.Web.Http.Data/QueryResult.cs32
-rw-r--r--src/Microsoft.Web.Http.Data/Resource.Designer.cs225
-rw-r--r--src/Microsoft.Web.Http.Data/Resource.resx174
-rw-r--r--src/Microsoft.Web.Http.Data/RoundtripOriginalAttribute.cs13
-rw-r--r--src/Microsoft.Web.Http.Data/SubmitActionDescriptor.cs77
-rw-r--r--src/Microsoft.Web.Http.Data/SubmitProxyActionDescriptor.cs79
-rw-r--r--src/Microsoft.Web.Http.Data/TypeDescriptorExtensions.cs87
-rw-r--r--src/Microsoft.Web.Http.Data/TypeUtility.cs156
-rw-r--r--src/Microsoft.Web.Http.Data/UpdateActionDescriptor.cs68
-rw-r--r--src/Microsoft.Web.Http.Data/UpdateAttribute.cs18
-rw-r--r--src/Microsoft.Web.Http.Data/ValidationResultInfo.cs124
-rw-r--r--src/Microsoft.Web.Http.Data/packages.config5
-rw-r--r--src/Microsoft.Web.Mvc/ActionLinkAreaAttribute.cs20
-rw-r--r--src/Microsoft.Web.Mvc/AjaxOnlyAttribute.cs23
-rw-r--r--src/Microsoft.Web.Mvc/AreaHelpers.cs36
-rw-r--r--src/Microsoft.Web.Mvc/AsyncManagerExtensions.cs82
-rw-r--r--src/Microsoft.Web.Mvc/ButtonBuilder.cs87
-rw-r--r--src/Microsoft.Web.Mvc/ButtonsAndLinkExtensions.cs171
-rw-r--r--src/Microsoft.Web.Mvc/CachedExpressionCompiler.cs47
-rw-r--r--src/Microsoft.Web.Mvc/ContentTypeAttribute.cs32
-rw-r--r--src/Microsoft.Web.Mvc/ControllerExtensions.cs32
-rw-r--r--src/Microsoft.Web.Mvc/Controls/ActionLink.cs109
-rw-r--r--src/Microsoft.Web.Mvc/Controls/DropDownList.cs205
-rw-r--r--src/Microsoft.Web.Mvc/Controls/EncodeType.cs9
-rw-r--r--src/Microsoft.Web.Mvc/Controls/Hidden.cs10
-rw-r--r--src/Microsoft.Web.Mvc/Controls/Label.cs111
-rw-r--r--src/Microsoft.Web.Mvc/Controls/MvcControl.cs124
-rw-r--r--src/Microsoft.Web.Mvc/Controls/MvcInputControl.cs143
-rw-r--r--src/Microsoft.Web.Mvc/Controls/Password.cs10
-rw-r--r--src/Microsoft.Web.Mvc/Controls/Repeater.cs77
-rw-r--r--src/Microsoft.Web.Mvc/Controls/RepeaterItem.cs36
-rw-r--r--src/Microsoft.Web.Mvc/Controls/RouteValues.cs56
-rw-r--r--src/Microsoft.Web.Mvc/Controls/TextBox.cs10
-rw-r--r--src/Microsoft.Web.Mvc/CookieTempDataProvider.cs99
-rw-r--r--src/Microsoft.Web.Mvc/CookieValueProviderFactory.cs28
-rw-r--r--src/Microsoft.Web.Mvc/CopyAsyncParametersAttribute.cs28
-rw-r--r--src/Microsoft.Web.Mvc/CreditCardAttribute.cs66
-rw-r--r--src/Microsoft.Web.Mvc/CssExtensions.cs42
-rw-r--r--src/Microsoft.Web.Mvc/DeserializeAttribute.cs60
-rw-r--r--src/Microsoft.Web.Mvc/DynamicReflectionObject.cs66
-rw-r--r--src/Microsoft.Web.Mvc/DynamicViewDataDictionary.cs72
-rw-r--r--src/Microsoft.Web.Mvc/DynamicViewPage.cs17
-rw-r--r--src/Microsoft.Web.Mvc/DynamicViewPage`1.cs12
-rw-r--r--src/Microsoft.Web.Mvc/ElementalValueProvider.cs35
-rw-r--r--src/Microsoft.Web.Mvc/EmailAddressAttribute.cs41
-rw-r--r--src/Microsoft.Web.Mvc/Error.cs80
-rw-r--r--src/Microsoft.Web.Mvc/ExpressionUtil/BinaryExpressionFingerprint.cs42
-rw-r--r--src/Microsoft.Web.Mvc/ExpressionUtil/CachedExpressionCompiler.cs143
-rw-r--r--src/Microsoft.Web.Mvc/ExpressionUtil/ConditionalExpressionFingerprint.cs29
-rw-r--r--src/Microsoft.Web.Mvc/ExpressionUtil/ConstantExpressionFingerprint.cs33
-rw-r--r--src/Microsoft.Web.Mvc/ExpressionUtil/DefaultExpressionFingerprint.cs29
-rw-r--r--src/Microsoft.Web.Mvc/ExpressionUtil/ExpressionFingerprint.cs48
-rw-r--r--src/Microsoft.Web.Mvc/ExpressionUtil/ExpressionFingerprintChain.cs86
-rw-r--r--src/Microsoft.Web.Mvc/ExpressionUtil/FingerprintingExpressionVisitor.cs296
-rw-r--r--src/Microsoft.Web.Mvc/ExpressionUtil/HashCodeCombiner.cs56
-rw-r--r--src/Microsoft.Web.Mvc/ExpressionUtil/Hoisted`2.cs6
-rw-r--r--src/Microsoft.Web.Mvc/ExpressionUtil/HoistingExpressionVisitor.cs36
-rw-r--r--src/Microsoft.Web.Mvc/ExpressionUtil/IndexExpressionFingerprint.cs42
-rw-r--r--src/Microsoft.Web.Mvc/ExpressionUtil/LambdaExpressionFingerprint.cs29
-rw-r--r--src/Microsoft.Web.Mvc/ExpressionUtil/MemberExpressionFingerprint.cs39
-rw-r--r--src/Microsoft.Web.Mvc/ExpressionUtil/MethodCallExpressionFingerprint.cs42
-rw-r--r--src/Microsoft.Web.Mvc/ExpressionUtil/ParameterExpressionFingerprint.cs38
-rw-r--r--src/Microsoft.Web.Mvc/ExpressionUtil/TypeBinaryExpressionFingerprint.cs38
-rw-r--r--src/Microsoft.Web.Mvc/ExpressionUtil/UnaryExpressionFingerprint.cs42
-rw-r--r--src/Microsoft.Web.Mvc/FileExtensionsAttribute.cs98
-rw-r--r--src/Microsoft.Web.Mvc/FormExtensions.cs43
-rw-r--r--src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/DisplayTemplates/Boolean.ascx20
-rw-r--r--src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/DisplayTemplates/Collection.ascx16
-rw-r--r--src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/DisplayTemplates/Decimal.ascx12
-rw-r--r--src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/DisplayTemplates/EmailAddress.ascx2
-rw-r--r--src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/DisplayTemplates/HiddenInput.ascx4
-rw-r--r--src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/DisplayTemplates/Html.ascx2
-rw-r--r--src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/DisplayTemplates/Object.ascx25
-rw-r--r--src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/DisplayTemplates/String.ascx2
-rw-r--r--src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/DisplayTemplates/Url.ascx2
-rw-r--r--src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/EditorTemplates/Boolean.ascx25
-rw-r--r--src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/EditorTemplates/Collection.ascx16
-rw-r--r--src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/EditorTemplates/Decimal.ascx12
-rw-r--r--src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/EditorTemplates/HiddenInput.ascx18
-rw-r--r--src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/EditorTemplates/MultilineText.ascx2
-rw-r--r--src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/EditorTemplates/Object.ascx27
-rw-r--r--src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/EditorTemplates/Password.ascx2
-rw-r--r--src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/EditorTemplates/String.ascx2
-rw-r--r--src/Microsoft.Web.Mvc/FuturesFiles/iismap.vbs128
-rw-r--r--src/Microsoft.Web.Mvc/FuturesFiles/registermvc.wsf20
-rw-r--r--src/Microsoft.Web.Mvc/FuturesFiles/unregistermvc.wsf20
-rw-r--r--src/Microsoft.Web.Mvc/GlobalSuppressions.cs15
-rw-r--r--src/Microsoft.Web.Mvc/Html/HtmlHelperExtensions.cs530
-rw-r--r--src/Microsoft.Web.Mvc/HtmlButtonType.cs9
-rw-r--r--src/Microsoft.Web.Mvc/IMachineKey.cs12
-rw-r--r--src/Microsoft.Web.Mvc/ImageExtensions.cs82
-rw-r--r--src/Microsoft.Web.Mvc/Internal/ExpressionHelper.cs150
-rw-r--r--src/Microsoft.Web.Mvc/LinkBuilder.cs73
-rw-r--r--src/Microsoft.Web.Mvc/LinkExtensions.cs51
-rw-r--r--src/Microsoft.Web.Mvc/MachineKeyWrapper.cs19
-rw-r--r--src/Microsoft.Web.Mvc/MailToExtensions.cs104
-rw-r--r--src/Microsoft.Web.Mvc/Microsoft.Web.Mvc.csproj276
-rw-r--r--src/Microsoft.Web.Mvc/ModelBinding/ArrayModelBinderProvider.cs22
-rw-r--r--src/Microsoft.Web.Mvc/ModelBinding/ArrayModelBinder`1.cs15
-rw-r--r--src/Microsoft.Web.Mvc/ModelBinding/BinaryDataModelBinderProvider.cs80
-rw-r--r--src/Microsoft.Web.Mvc/ModelBinding/BindNeverAttribute.cs13
-rw-r--r--src/Microsoft.Web.Mvc/ModelBinding/BindRequiredAttribute.cs13
-rw-r--r--src/Microsoft.Web.Mvc/ModelBinding/BindingBehavior.cs9
-rw-r--r--src/Microsoft.Web.Mvc/ModelBinding/BindingBehaviorAttribute.cs24
-rw-r--r--src/Microsoft.Web.Mvc/ModelBinding/CollectionModelBinderProvider.cs22
-rw-r--r--src/Microsoft.Web.Mvc/ModelBinding/CollectionModelBinderUtil.cs144
-rw-r--r--src/Microsoft.Web.Mvc/ModelBinding/CollectionModelBinder`1.cs131
-rw-r--r--src/Microsoft.Web.Mvc/ModelBinding/ComplexModelDto.cs38
-rw-r--r--src/Microsoft.Web.Mvc/ModelBinding/ComplexModelDtoModelBinder.cs38
-rw-r--r--src/Microsoft.Web.Mvc/ModelBinding/ComplexModelDtoModelBinderProvider.cs24
-rw-r--r--src/Microsoft.Web.Mvc/ModelBinding/ComplexModelDtoResult.cs22
-rw-r--r--src/Microsoft.Web.Mvc/ModelBinding/DictionaryModelBinderProvider.cs22
-rw-r--r--src/Microsoft.Web.Mvc/ModelBinding/DictionaryModelBinder`2.cs14
-rw-r--r--src/Microsoft.Web.Mvc/ModelBinding/ExtensibleModelBinderAdapter.cs68
-rw-r--r--src/Microsoft.Web.Mvc/ModelBinding/ExtensibleModelBinderAttribute.cs17
-rw-r--r--src/Microsoft.Web.Mvc/ModelBinding/ExtensibleModelBindingContext.cs136
-rw-r--r--src/Microsoft.Web.Mvc/ModelBinding/GenericModelBinderProvider.cs130
-rw-r--r--src/Microsoft.Web.Mvc/ModelBinding/IExtensibleModelBinder.cs9
-rw-r--r--src/Microsoft.Web.Mvc/ModelBinding/KeyValuePairModelBinderProvider.cs26
-rw-r--r--src/Microsoft.Web.Mvc/ModelBinding/KeyValuePairModelBinderUtil.cs31
-rw-r--r--src/Microsoft.Web.Mvc/ModelBinding/KeyValuePairModelBinder`2.cs40
-rw-r--r--src/Microsoft.Web.Mvc/ModelBinding/ModelBinderConfig.cs99
-rw-r--r--src/Microsoft.Web.Mvc/ModelBinding/ModelBinderErrorMessageProvider.cs6
-rw-r--r--src/Microsoft.Web.Mvc/ModelBinding/ModelBinderProvider.cs9
-rw-r--r--src/Microsoft.Web.Mvc/ModelBinding/ModelBinderProviderCollection.cs177
-rw-r--r--src/Microsoft.Web.Mvc/ModelBinding/ModelBinderProviderOptionsAttribute.cs12
-rw-r--r--src/Microsoft.Web.Mvc/ModelBinding/ModelBinderProviders.cs28
-rw-r--r--src/Microsoft.Web.Mvc/ModelBinding/ModelBinderUtil.cs136
-rw-r--r--src/Microsoft.Web.Mvc/ModelBinding/ModelValidatedEventArgs.cs23
-rw-r--r--src/Microsoft.Web.Mvc/ModelBinding/ModelValidatingEventArgs.cs24
-rw-r--r--src/Microsoft.Web.Mvc/ModelBinding/ModelValidationNode.cs193
-rw-r--r--src/Microsoft.Web.Mvc/ModelBinding/MutableObjectModelBinder.cs264
-rw-r--r--src/Microsoft.Web.Mvc/ModelBinding/MutableObjectModelBinderProvider.cs26
-rw-r--r--src/Microsoft.Web.Mvc/ModelBinding/SimpleModelBinderProvider.cs64
-rw-r--r--src/Microsoft.Web.Mvc/ModelBinding/TypeConverterModelBinder.cs62
-rw-r--r--src/Microsoft.Web.Mvc/ModelBinding/TypeConverterModelBinderProvider.cs27
-rw-r--r--src/Microsoft.Web.Mvc/ModelBinding/TypeMatchModelBinder.cs41
-rw-r--r--src/Microsoft.Web.Mvc/ModelBinding/TypeMatchModelBinderProvider.cs16
-rw-r--r--src/Microsoft.Web.Mvc/ModelCopier.cs55
-rw-r--r--src/Microsoft.Web.Mvc/MvcSerializer.cs141
-rw-r--r--src/Microsoft.Web.Mvc/Properties/AssemblyInfo.cs8
-rw-r--r--src/Microsoft.Web.Mvc/Properties/MvcResources.Designer.cs423
-rw-r--r--src/Microsoft.Web.Mvc/Properties/MvcResources.resx240
-rw-r--r--src/Microsoft.Web.Mvc/RadioExtensions.cs115
-rw-r--r--src/Microsoft.Web.Mvc/ReaderWriterCache`2.cs67
-rw-r--r--src/Microsoft.Web.Mvc/Resources/ActionType.cs16
-rw-r--r--src/Microsoft.Web.Mvc/Resources/AjaxHelperExtensions.cs113
-rw-r--r--src/Microsoft.Web.Mvc/Resources/AtomEntryActionResult.cs79
-rw-r--r--src/Microsoft.Web.Mvc/Resources/AtomFeedActionResult.cs79
-rw-r--r--src/Microsoft.Web.Mvc/Resources/AtomServiceDocumentActionResult.cs79
-rw-r--r--src/Microsoft.Web.Mvc/Resources/DataContractJsonActionResult.cs71
-rw-r--r--src/Microsoft.Web.Mvc/Resources/DataContractXmlActionResult.cs71
-rw-r--r--src/Microsoft.Web.Mvc/Resources/DefaultFormatHelper.cs370
-rw-r--r--src/Microsoft.Web.Mvc/Resources/DefaultFormatManager.cs15
-rw-r--r--src/Microsoft.Web.Mvc/Resources/FormatHelper.cs35
-rw-r--r--src/Microsoft.Web.Mvc/Resources/FormatManager.cs139
-rw-r--r--src/Microsoft.Web.Mvc/Resources/HtmlHelperExtensions.cs135
-rw-r--r--src/Microsoft.Web.Mvc/Resources/HttpRequestBaseExtensions.cs73
-rw-r--r--src/Microsoft.Web.Mvc/Resources/IEnumerableExtensions.cs60
-rw-r--r--src/Microsoft.Web.Mvc/Resources/IRequestFormatHandler.cs30
-rw-r--r--src/Microsoft.Web.Mvc/Resources/IResponseFormatHandler.cs43
-rw-r--r--src/Microsoft.Web.Mvc/Resources/JsonFormatHandler.cs53
-rw-r--r--src/Microsoft.Web.Mvc/Resources/MultiFormatActionResult.cs56
-rw-r--r--src/Microsoft.Web.Mvc/Resources/RequestContextExtensions.cs27
-rw-r--r--src/Microsoft.Web.Mvc/Resources/ResourceControllerFactory.cs179
-rw-r--r--src/Microsoft.Web.Mvc/Resources/ResourceErrorActionResult.cs48
-rw-r--r--src/Microsoft.Web.Mvc/Resources/ResourceModelBinder.cs98
-rw-r--r--src/Microsoft.Web.Mvc/Resources/ResourceRedirectToRouteResult.cs31
-rw-r--r--src/Microsoft.Web.Mvc/Resources/RouteCollectionExtensions.cs135
-rw-r--r--src/Microsoft.Web.Mvc/Resources/UriHelperExtensions.cs52
-rw-r--r--src/Microsoft.Web.Mvc/Resources/WebApiEnabledAttribute.cs215
-rw-r--r--src/Microsoft.Web.Mvc/Resources/XmlFormatHandler.cs53
-rw-r--r--src/Microsoft.Web.Mvc/ScriptExtensions.cs51
-rw-r--r--src/Microsoft.Web.Mvc/SerializationExtensions.cs71
-rw-r--r--src/Microsoft.Web.Mvc/SerializationMode.cs8
-rw-r--r--src/Microsoft.Web.Mvc/ServerVariablesValueProviderFactory.cs15
-rw-r--r--src/Microsoft.Web.Mvc/SessionValueProviderFactory.cs33
-rw-r--r--src/Microsoft.Web.Mvc/SkipBindingAttribute.cs24
-rw-r--r--src/Microsoft.Web.Mvc/TempDataValueProviderFactory.cs73
-rw-r--r--src/Microsoft.Web.Mvc/TypeDescriptorHelper.cs32
-rw-r--r--src/Microsoft.Web.Mvc/TypeHelpers.cs40
-rw-r--r--src/Microsoft.Web.Mvc/UrlAttribute.cs41
-rw-r--r--src/Microsoft.Web.Mvc/ValueProviderUtil.cs41
-rw-r--r--src/Microsoft.Web.Mvc/ViewExtensions.cs40
-rw-r--r--src/Microsoft.Web.WebPages.OAuth/AuthenticationClientCollection.cs22
-rw-r--r--src/Microsoft.Web.WebPages.OAuth/BuiltInAuthenticationClient.cs16
-rw-r--r--src/Microsoft.Web.WebPages.OAuth/BuiltInOpenIDClient.cs11
-rw-r--r--src/Microsoft.Web.WebPages.OAuth/Microsoft.Web.WebPages.OAuth.csproj129
-rw-r--r--src/Microsoft.Web.WebPages.OAuth/OAuthAccount.cs47
-rw-r--r--src/Microsoft.Web.WebPages.OAuth/OAuthWebSecurity.cs311
-rw-r--r--src/Microsoft.Web.WebPages.OAuth/PreApplicationStartCode.cs15
-rw-r--r--src/Microsoft.Web.WebPages.OAuth/Properties/AssemblyInfo.cs9
-rw-r--r--src/Microsoft.Web.WebPages.OAuth/Properties/WebResources.Designer.cs108
-rw-r--r--src/Microsoft.Web.WebPages.OAuth/Properties/WebResources.resx135
-rw-r--r--src/Microsoft.Web.WebPages.OAuth/WebPagesOAuthDataProvider.cs33
-rw-r--r--src/Microsoft.Web.WebPages.OAuth/packages.config9
-rw-r--r--src/MimeMapping.cs378
-rw-r--r--src/RS.cs10
-rw-r--r--src/SPA/Properties/AssemblyInfo.cs10
-rw-r--r--src/SPA/SPA.csproj166
-rw-r--r--src/SPA/SPA.targets68
-rw-r--r--src/SPA/nav/nav.coffee222
-rw-r--r--src/SPA/nav/nav.js317
-rw-r--r--src/SPA/nav/nav.transitions.coffee231
-rw-r--r--src/SPA/nav/nav.transitions.js338
-rw-r--r--src/SPA/upshot/AssociatedEntitiesView.js221
-rw-r--r--src/SPA/upshot/Core.js270
-rw-r--r--src/SPA/upshot/DataContext.js508
-rw-r--r--src/SPA/upshot/DataProvider.OData.js172
-rw-r--r--src/SPA/upshot/DataProvider.js173
-rw-r--r--src/SPA/upshot/DataProvider.ria.js251
-rw-r--r--src/SPA/upshot/DataSource.js168
-rw-r--r--src/SPA/upshot/EntitySet.js1491
-rw-r--r--src/SPA/upshot/EntitySource.js232
-rw-r--r--src/SPA/upshot/EntityView.js327
-rw-r--r--src/SPA/upshot/IntelliSense/Dependencies.js5
-rw-r--r--src/SPA/upshot/IntelliSense/References.js21
-rw-r--r--src/SPA/upshot/IntelliSense/jquery-1.5.2-vsdoc.js6651
-rw-r--r--src/SPA/upshot/IntelliSense/knockout-2.0.0.debug.js3223
-rw-r--r--src/SPA/upshot/LocalDataSource.js470
-rw-r--r--src/SPA/upshot/Metadata.js124
-rw-r--r--src/SPA/upshot/Observability.js24
-rw-r--r--src/SPA/upshot/RemoteDataSource.js259
-rw-r--r--src/SPA/upshot/Upshot.Compat.JsViews.js135
-rw-r--r--src/SPA/upshot/Upshot.Compat.Knockout.js400
-rw-r--r--src/SPA/upshot/Upshot.Compat.WinJS.js495
-rw-r--r--src/SPA/upshot/Upshot.Compat.jQueryUI.js175
-rw-r--r--src/SPA/upshot/upshot.dataview.js162
-rw-r--r--src/Settings.StyleCop381
-rw-r--r--src/Strict.ruleset10
-rw-r--r--src/System.Json/Extensions/JsonValueExtensions.cs380
-rw-r--r--src/System.Json/FormUrlEncodedJson.cs589
-rw-r--r--src/System.Json/GlobalSuppressions.cs4
-rw-r--r--src/System.Json/JXmlToJsonValueConverter.cs302
-rw-r--r--src/System.Json/JsonArray.cs387
-rw-r--r--src/System.Json/JsonObject.cs472
-rw-r--r--src/System.Json/JsonPrimitive.cs1128
-rw-r--r--src/System.Json/JsonType.cs48
-rw-r--r--src/System.Json/JsonValue.cs1254
-rw-r--r--src/System.Json/JsonValueChange.cs28
-rw-r--r--src/System.Json/JsonValueChangeEventArgs.cs96
-rw-r--r--src/System.Json/JsonValueDynamicMetaObject.cs381
-rw-r--r--src/System.Json/JsonValueLinqExtensions.cs35
-rw-r--r--src/System.Json/NGenWrapper.cs46
-rw-r--r--src/System.Json/Properties/AssemblyInfo.cs10
-rw-r--r--src/System.Json/Properties/Resources.Designer.cs288
-rw-r--r--src/System.Json/Properties/Resources.resx195
-rw-r--r--src/System.Json/Settings.StyleCop198
-rw-r--r--src/System.Json/System.Json.csproj97
-rw-r--r--src/System.Net.Http.Formatting/CloneableExtensions.cs16
-rw-r--r--src/System.Net.Http.Formatting/ContentDispositionHeaderValueExtensions.cs52
-rw-r--r--src/System.Net.Http.Formatting/Formatting/BufferedMediaTypeFormatter.cs119
-rw-r--r--src/System.Net.Http.Formatting/Formatting/DefaultContentNegotiator.cs182
-rw-r--r--src/System.Net.Http.Formatting/Formatting/DelegatingEnumerable.cs66
-rw-r--r--src/System.Net.Http.Formatting/Formatting/FormDataCollection.cs122
-rw-r--r--src/System.Net.Http.Formatting/Formatting/FormUrlEncodedMediaTypeFormatter.cs215
-rw-r--r--src/System.Net.Http.Formatting/Formatting/IContentNegotiator.cs30
-rw-r--r--src/System.Net.Http.Formatting/Formatting/IFormatterLogger.cs15
-rw-r--r--src/System.Net.Http.Formatting/Formatting/IKeyValueModel.cs31
-rw-r--r--src/System.Net.Http.Formatting/Formatting/IRequiredMemberSelector.cs17
-rw-r--r--src/System.Net.Http.Formatting/Formatting/JsonContractResolver.cs68
-rw-r--r--src/System.Net.Http.Formatting/Formatting/JsonKeyValueModel.cs131
-rw-r--r--src/System.Net.Http.Formatting/Formatting/JsonMediaTypeFormatter.cs447
-rw-r--r--src/System.Net.Http.Formatting/Formatting/JsonValueConverter.cs107
-rw-r--r--src/System.Net.Http.Formatting/Formatting/MediaRangeMapping.cs90
-rw-r--r--src/System.Net.Http.Formatting/Formatting/MediaTypeConstants.cs84
-rw-r--r--src/System.Net.Http.Formatting/Formatting/MediaTypeFormatter.cs493
-rw-r--r--src/System.Net.Http.Formatting/Formatting/MediaTypeFormatterCollection.cs137
-rw-r--r--src/System.Net.Http.Formatting/Formatting/MediaTypeFormatterExtensions.cs217
-rw-r--r--src/System.Net.Http.Formatting/Formatting/MediaTypeHeaderValueComparer.cs91
-rw-r--r--src/System.Net.Http.Formatting/Formatting/MediaTypeHeaderValueEqualityComparer.cs79
-rw-r--r--src/System.Net.Http.Formatting/Formatting/MediaTypeHeaderValueExtensions.cs42
-rw-r--r--src/System.Net.Http.Formatting/Formatting/MediaTypeMapping.cs73
-rw-r--r--src/System.Net.Http.Formatting/Formatting/MediaTypeMatch.cs55
-rw-r--r--src/System.Net.Http.Formatting/Formatting/ParsedMediaTypeHeaderValue.cs99
-rw-r--r--src/System.Net.Http.Formatting/Formatting/Parsers/FormUrlEncodedParser.cs308
-rw-r--r--src/System.Net.Http.Formatting/Formatting/Parsers/HttpRequestHeaderParser.cs137
-rw-r--r--src/System.Net.Http.Formatting/Formatting/Parsers/HttpRequestLineParser.cs343
-rw-r--r--src/System.Net.Http.Formatting/Formatting/Parsers/HttpResponseHeaderParser.cs137
-rw-r--r--src/System.Net.Http.Formatting/Formatting/Parsers/HttpStatusLineParser.cs345
-rw-r--r--src/System.Net.Http.Formatting/Formatting/Parsers/InternetMessageFormatHeaderParser.cs320
-rw-r--r--src/System.Net.Http.Formatting/Formatting/Parsers/MimeMultipartBodyPartParser.cs278
-rw-r--r--src/System.Net.Http.Formatting/Formatting/Parsers/MimeMultipartParser.cs612
-rw-r--r--src/System.Net.Http.Formatting/Formatting/Parsers/ParserState.cs28
-rw-r--r--src/System.Net.Http.Formatting/Formatting/QueryStringMapping.cs114
-rw-r--r--src/System.Net.Http.Formatting/Formatting/RequestHeaderMapping.cs144
-rw-r--r--src/System.Net.Http.Formatting/Formatting/ResponseFormatterSelectionResult.cs40
-rw-r--r--src/System.Net.Http.Formatting/Formatting/ResponseMediaTypeMatch.cs35
-rw-r--r--src/System.Net.Http.Formatting/Formatting/SecureJsonTextReader.cs78
-rw-r--r--src/System.Net.Http.Formatting/Formatting/StringComparisonHelper.cs44
-rw-r--r--src/System.Net.Http.Formatting/Formatting/UriPathExtensionMapping.cs99
-rw-r--r--src/System.Net.Http.Formatting/Formatting/XHRRequestHeaderMapping.cs49
-rw-r--r--src/System.Net.Http.Formatting/Formatting/XmlKeyValueModel.cs63
-rw-r--r--src/System.Net.Http.Formatting/Formatting/XmlMediaTypeFormatter.cs474
-rw-r--r--src/System.Net.Http.Formatting/FormattingUtilities.cs194
-rw-r--r--src/System.Net.Http.Formatting/GlobalSuppressions.cs3
-rw-r--r--src/System.Net.Http.Formatting/HttpClientExtensions.cs327
-rw-r--r--src/System.Net.Http.Formatting/HttpContentCollectionExtensions.cs271
-rw-r--r--src/System.Net.Http.Formatting/HttpContentExtensions.cs158
-rw-r--r--src/System.Net.Http.Formatting/HttpContentMessageExtensions.cs384
-rw-r--r--src/System.Net.Http.Formatting/HttpContentMultipartExtensions.cs390
-rw-r--r--src/System.Net.Http.Formatting/HttpHeaderExtensions.cs42
-rw-r--r--src/System.Net.Http.Formatting/HttpMessageContent.cs457
-rw-r--r--src/System.Net.Http.Formatting/HttpUnsortedHeaders.cs15
-rw-r--r--src/System.Net.Http.Formatting/HttpUnsortedRequest.cs51
-rw-r--r--src/System.Net.Http.Formatting/HttpUnsortedResponse.cs51
-rw-r--r--src/System.Net.Http.Formatting/IMultipartStreamProvider.cs20
-rw-r--r--src/System.Net.Http.Formatting/Internal/AsyncResultWithExtraData.cs36
-rw-r--r--src/System.Net.Http.Formatting/Internal/DelegatingStream.cs127
-rw-r--r--src/System.Net.Http.Formatting/Internal/TaskExtensions.cs547
-rw-r--r--src/System.Net.Http.Formatting/Internal/TaskHelpers.cs384
-rw-r--r--src/System.Net.Http.Formatting/Internal/ThresholdStream.cs87
-rw-r--r--src/System.Net.Http.Formatting/Internal/UriQueryUtility.cs541
-rw-r--r--src/System.Net.Http.Formatting/MimeBodyPart.cs162
-rw-r--r--src/System.Net.Http.Formatting/MultipartFileStreamProvider.cs167
-rw-r--r--src/System.Net.Http.Formatting/MultipartFormDataStreamProvider.cs190
-rw-r--r--src/System.Net.Http.Formatting/MultipartMemoryStreamProvider.cs46
-rw-r--r--src/System.Net.Http.Formatting/ObjectContent.cs159
-rw-r--r--src/System.Net.Http.Formatting/Properties/AssemblyInfo.cs16
-rw-r--r--src/System.Net.Http.Formatting/Properties/Resources.Designer.cs477
-rw-r--r--src/System.Net.Http.Formatting/Properties/Resources.resx259
-rw-r--r--src/System.Net.Http.Formatting/SR.resx261
-rw-r--r--src/System.Net.Http.Formatting/Settings.StyleCop10
-rw-r--r--src/System.Net.Http.Formatting/System.Net.Http.Formatting.csproj174
-rw-r--r--src/System.Net.Http.Formatting/UriExtensions.cs137
-rw-r--r--src/System.Net.Http.Formatting/packages.config5
-rw-r--r--src/System.Web.Helpers/Chart/Chart.cs792
-rw-r--r--src/System.Web.Helpers/Chart/ChartTheme.cs84
-rw-r--r--src/System.Web.Helpers/Common/VirtualPathUtil.cs78
-rw-r--r--src/System.Web.Helpers/ConversionUtil.cs201
-rw-r--r--src/System.Web.Helpers/Crypto.cs179
-rw-r--r--src/System.Web.Helpers/DynamicJavaScriptConverter.cs46
-rw-r--r--src/System.Web.Helpers/DynamicJsonArray.cs78
-rw-r--r--src/System.Web.Helpers/DynamicJsonObject.cs94
-rw-r--r--src/System.Web.Helpers/GlobalSuppressions.cs13
-rw-r--r--src/System.Web.Helpers/HtmlElement.cs122
-rw-r--r--src/System.Web.Helpers/HtmlObjectPrinter.cs411
-rw-r--r--src/System.Web.Helpers/Json.cs70
-rw-r--r--src/System.Web.Helpers/ObjectInfo.cs31
-rw-r--r--src/System.Web.Helpers/ObjectVisitor.cs465
-rw-r--r--src/System.Web.Helpers/Properties/AssemblyInfo.cs10
-rw-r--r--src/System.Web.Helpers/Resources/ChartTemplates.Designer.cs141
-rw-r--r--src/System.Web.Helpers/Resources/ChartTemplates.resx197
-rw-r--r--src/System.Web.Helpers/Resources/HelpersResources.Designer.cs414
-rw-r--r--src/System.Web.Helpers/Resources/HelpersResources.resx237
-rw-r--r--src/System.Web.Helpers/ServerInfo.cs324
-rw-r--r--src/System.Web.Helpers/SortDirection.cs8
-rw-r--r--src/System.Web.Helpers/System.Web.Helpers.csproj133
-rw-r--r--src/System.Web.Helpers/WebCache.cs48
-rw-r--r--src/System.Web.Helpers/WebGrid/IWebGridDataSource.cs11
-rw-r--r--src/System.Web.Helpers/WebGrid/PreComputedGridDataSource.cs36
-rw-r--r--src/System.Web.Helpers/WebGrid/SortInfo.cs31
-rw-r--r--src/System.Web.Helpers/WebGrid/WebGrid.cs947
-rw-r--r--src/System.Web.Helpers/WebGrid/WebGridColumn.cs15
-rw-r--r--src/System.Web.Helpers/WebGrid/WebGridDataSource.cs159
-rw-r--r--src/System.Web.Helpers/WebGrid/WebGridPagerModes.cs11
-rw-r--r--src/System.Web.Helpers/WebGrid/WebGridRow.cs195
-rw-r--r--src/System.Web.Helpers/WebGrid/_WebGridRenderer.cshtml321
-rw-r--r--src/System.Web.Helpers/WebGrid/_WebGridRenderer.generated.csbin0 -> 41462 bytes
-rw-r--r--src/System.Web.Helpers/WebImage.cs1172
-rw-r--r--src/System.Web.Helpers/WebMail.cs364
-rw-r--r--src/System.Web.Http.Common/Error.cs261
-rw-r--r--src/System.Web.Http.Common/GlobalSuppressions.cs5
-rw-r--r--src/System.Web.Http.Common/HttpMethodHelper.cs60
-rw-r--r--src/System.Web.Http.Common/HttpRequestMessageCommonExtensions.cs55
-rw-r--r--src/System.Web.Http.Common/Properties/AssemblyInfo.cs16
-rw-r--r--src/System.Web.Http.Common/Properties/SRResources.Designer.cs117
-rw-r--r--src/System.Web.Http.Common/Properties/SRResources.resx138
-rw-r--r--src/System.Web.Http.Common/RouteParameter.cs34
-rw-r--r--src/System.Web.Http.Common/System.Web.Http.Common.csproj93
-rw-r--r--src/System.Web.Http.Common/TaskHelpers.cs421
-rw-r--r--src/System.Web.Http.Common/TaskHelpersExtensions.cs557
-rw-r--r--src/System.Web.Http.Common/packages.config4
-rw-r--r--src/System.Web.Http.SelfHost/Channels/HttpBinding.cs221
-rw-r--r--src/System.Web.Http.SelfHost/Channels/HttpBindingSecurity.cs54
-rw-r--r--src/System.Web.Http.SelfHost/Channels/HttpBindingSecurityMode.cs24
-rw-r--r--src/System.Web.Http.SelfHost/Channels/HttpBindingSecurityModeHelper.cs40
-rw-r--r--src/System.Web.Http.SelfHost/Channels/HttpMessage.cs228
-rw-r--r--src/System.Web.Http.SelfHost/Channels/HttpMessageEncoderFactory.cs239
-rw-r--r--src/System.Web.Http.SelfHost/Channels/HttpMessageEncodingBindingElement.cs139
-rw-r--r--src/System.Web.Http.SelfHost/Channels/HttpMessageEncodingChannelListener.cs60
-rw-r--r--src/System.Web.Http.SelfHost/Channels/HttpMessageEncodingReplyChannel.cs102
-rw-r--r--src/System.Web.Http.SelfHost/Channels/HttpMessageEncodingRequestContext.cs289
-rw-r--r--src/System.Web.Http.SelfHost/Channels/HttpMessageExtensions.cs188
-rw-r--r--src/System.Web.Http.SelfHost/GlobalSuppressions.cs8
-rw-r--r--src/System.Web.Http.SelfHost/HttpSelfHostConfiguration.cs298
-rw-r--r--src/System.Web.Http.SelfHost/HttpSelfHostServer.cs893
-rw-r--r--src/System.Web.Http.SelfHost/Properties/AssemblyInfo.cs7
-rw-r--r--src/System.Web.Http.SelfHost/Properties/SRResources.Designer.cs378
-rw-r--r--src/System.Web.Http.SelfHost/Properties/SRResources.resx225
-rw-r--r--src/System.Web.Http.SelfHost/ServiceModel/Activation/AspNetEnvironment.cs60
-rw-r--r--src/System.Web.Http.SelfHost/ServiceModel/Channels/AsyncResult.cs360
-rw-r--r--src/System.Web.Http.SelfHost/ServiceModel/Channels/BufferManagerOutputStream.cs83
-rw-r--r--src/System.Web.Http.SelfHost/ServiceModel/Channels/BufferedOutputStream.cs316
-rw-r--r--src/System.Web.Http.SelfHost/ServiceModel/Channels/ChannelAcceptor.cs75
-rw-r--r--src/System.Web.Http.SelfHost/ServiceModel/Channels/ChannelBindingUtility.cs60
-rw-r--r--src/System.Web.Http.SelfHost/ServiceModel/Channels/CompletedAsyncResult.cs45
-rw-r--r--src/System.Web.Http.SelfHost/ServiceModel/Channels/HttpTransportDefaults.cs10
-rw-r--r--src/System.Web.Http.SelfHost/ServiceModel/Channels/IChannelAcceptor.cs21
-rw-r--r--src/System.Web.Http.SelfHost/ServiceModel/Channels/InternalBufferManager.cs502
-rw-r--r--src/System.Web.Http.SelfHost/ServiceModel/Channels/LayeredChannel.cs86
-rw-r--r--src/System.Web.Http.SelfHost/ServiceModel/Channels/LayeredChannelAcceptor.cs65
-rw-r--r--src/System.Web.Http.SelfHost/ServiceModel/Channels/LayeredChannelListener.cs298
-rw-r--r--src/System.Web.Http.SelfHost/ServiceModel/Channels/SynchronizedPool.cs418
-rw-r--r--src/System.Web.Http.SelfHost/ServiceModel/Channels/TransportDefaults.cs10
-rw-r--r--src/System.Web.Http.SelfHost/ServiceModel/HostNameComparisonModeHelper.cs24
-rw-r--r--src/System.Web.Http.SelfHost/ServiceModel/HttpClientCredentialTypeHelper.cs34
-rw-r--r--src/System.Web.Http.SelfHost/ServiceModel/HttpProxyCredentialTypeHelper.cs33
-rw-r--r--src/System.Web.Http.SelfHost/ServiceModel/HttpTransportSecurityExtensionMethods.cs56
-rw-r--r--src/System.Web.Http.SelfHost/ServiceModel/TransferModeHelper.cs34
-rw-r--r--src/System.Web.Http.SelfHost/System.Web.Http.SelfHost.csproj135
-rw-r--r--src/System.Web.Http.SelfHost/packages.config4
-rw-r--r--src/System.Web.Http.WebHost/GlobalConfiguration.cs43
-rw-r--r--src/System.Web.Http.WebHost/GlobalSuppressions.cs7
-rw-r--r--src/System.Web.Http.WebHost/HttpControllerHandler.cs299
-rw-r--r--src/System.Web.Http.WebHost/HttpControllerRouteHandler.cs53
-rw-r--r--src/System.Web.Http.WebHost/Properties/AssemblyInfo.cs6
-rw-r--r--src/System.Web.Http.WebHost/Properties/SRResources.Designer.cs99
-rw-r--r--src/System.Web.Http.WebHost/Properties/SRResources.resx132
-rw-r--r--src/System.Web.Http.WebHost/RouteCollectionExtensions.cs67
-rw-r--r--src/System.Web.Http.WebHost/Routing/HostedHttpRoute.cs99
-rw-r--r--src/System.Web.Http.WebHost/Routing/HostedHttpRouteCollection.cs195
-rw-r--r--src/System.Web.Http.WebHost/Routing/HostedHttpRouteData.cs39
-rw-r--r--src/System.Web.Http.WebHost/Routing/HostedHttpVirtualPathData.cs33
-rw-r--r--src/System.Web.Http.WebHost/Routing/HttpRouteDataExtensions.cs26
-rw-r--r--src/System.Web.Http.WebHost/Routing/HttpRouteExtensions.cs30
-rw-r--r--src/System.Web.Http.WebHost/Routing/HttpWebRoute.cs74
-rw-r--r--src/System.Web.Http.WebHost/System.Web.Http.WebHost.csproj118
-rw-r--r--src/System.Web.Http.WebHost/TaskWrapperAsyncResult.cs70
-rw-r--r--src/System.Web.Http.WebHost/WebHostBuildManager.cs62
-rw-r--r--src/System.Web.Http.WebHost/packages.config4
-rw-r--r--src/System.Web.Http/AcceptVerbsAttribute.cs38
-rw-r--r--src/System.Web.Http/ActionNameAttribute.cs15
-rw-r--r--src/System.Web.Http/AllowAnonymousAttribute.cs10
-rw-r--r--src/System.Web.Http/ApiController.cs307
-rw-r--r--src/System.Web.Http/AuthorizeAttribute.cs178
-rw-r--r--src/System.Web.Http/Controllers/ActionResponseConverter.cs76
-rw-r--r--src/System.Web.Http/Controllers/ApiControllerActionInvoker.cs71
-rw-r--r--src/System.Web.Http/Controllers/ApiControllerActionSelector.cs381
-rw-r--r--src/System.Web.Http/Controllers/HttpActionContext.cs91
-rw-r--r--src/System.Web.Http/Controllers/HttpActionContextExtensions.cs123
-rw-r--r--src/System.Web.Http/Controllers/HttpActionDescriptor.cs164
-rw-r--r--src/System.Web.Http/Controllers/HttpContentMessageConverter.cs31
-rw-r--r--src/System.Web.Http/Controllers/HttpControllerConfigurationAttribute.cs23
-rw-r--r--src/System.Web.Http/Controllers/HttpControllerContext.cs150
-rw-r--r--src/System.Web.Http/Controllers/HttpControllerDescriptor.cs265
-rw-r--r--src/System.Web.Http/Controllers/HttpParameterDescriptor.cs120
-rw-r--r--src/System.Web.Http/Controllers/HttpResponseMessageConverter.cs57
-rw-r--r--src/System.Web.Http/Controllers/IActionHttpMethodProvider.cs11
-rw-r--r--src/System.Web.Http/Controllers/IActionMethodSelector.cs9
-rw-r--r--src/System.Web.Http/Controllers/IActionValueBinder.cs11
-rw-r--r--src/System.Web.Http/Controllers/IHttpActionInvoker.cs11
-rw-r--r--src/System.Web.Http/Controllers/IHttpActionSelector.cs21
-rw-r--r--src/System.Web.Http/Controllers/IHttpController.cs11
-rw-r--r--src/System.Web.Http/Controllers/ReflectedHttpActionDescriptor.cs284
-rw-r--r--src/System.Web.Http/Controllers/ReflectedHttpParameterDescriptor.cs77
-rw-r--r--src/System.Web.Http/Controllers/TaskActionResponseConverter.cs37
-rw-r--r--src/System.Web.Http/Controllers/VoidHttpResponseMessageConverter.cs20
-rw-r--r--src/System.Web.Http/DependencyResolverExtensions.cs160
-rw-r--r--src/System.Web.Http/Description/ApiDescription.cs91
-rw-r--r--src/System.Web.Http/Description/ApiExplorer.cs486
-rw-r--r--src/System.Web.Http/Description/ApiExplorerSettingsAttribute.cs17
-rw-r--r--src/System.Web.Http/Description/ApiParameterDescription.cs42
-rw-r--r--src/System.Web.Http/Description/ApiParameterSource.cs12
-rw-r--r--src/System.Web.Http/Description/IApiExplorer.cs15
-rw-r--r--src/System.Web.Http/Description/IDocumentationProvider.cs24
-rw-r--r--src/System.Web.Http/DictionaryExtensions.cs140
-rw-r--r--src/System.Web.Http/Dispatcher/DefaultBuildManager.cs60
-rw-r--r--src/System.Web.Http/Dispatcher/DefaultHttpControllerActivator.cs94
-rw-r--r--src/System.Web.Http/Dispatcher/DefaultHttpControllerFactory.cs168
-rw-r--r--src/System.Web.Http/Dispatcher/ExceptionSurrogate.cs42
-rw-r--r--src/System.Web.Http/Dispatcher/HttpControllerDispatcher.cs227
-rw-r--r--src/System.Web.Http/Dispatcher/HttpControllerTypeCache.cs83
-rw-r--r--src/System.Web.Http/Dispatcher/HttpControllerTypeCacheSerializer.cs126
-rw-r--r--src/System.Web.Http/Dispatcher/HttpControllerTypeCacheUtil.cs103
-rw-r--r--src/System.Web.Http/Dispatcher/IBuildManager.cs48
-rw-r--r--src/System.Web.Http/Dispatcher/IHttpControllerActivator.cs12
-rw-r--r--src/System.Web.Http/Dispatcher/IHttpControllerFactory.cs35
-rw-r--r--src/System.Web.Http/Filters/ActionDescriptorFilterProvider.cs41
-rw-r--r--src/System.Web.Http/Filters/ActionFilterAttribute.cs93
-rw-r--r--src/System.Web.Http/Filters/AuthorizationFilterAttribute.cs48
-rw-r--r--src/System.Web.Http/Filters/ConfigurationFilterProvider.cs19
-rw-r--r--src/System.Web.Http/Filters/EnumerableEvaluatorFilter.cs125
-rw-r--r--src/System.Web.Http/Filters/EnumerableEvaluatorFilterProvider.cs36
-rw-r--r--src/System.Web.Http/Filters/ExceptionFilterAttribute.cs25
-rw-r--r--src/System.Web.Http/Filters/FilterAttribute.cs26
-rw-r--r--src/System.Web.Http/Filters/FilterInfo.cs22
-rw-r--r--src/System.Web.Http/Filters/FilterInfoComparer.cs35
-rw-r--r--src/System.Web.Http/Filters/FilterScope.cs11
-rw-r--r--src/System.Web.Http/Filters/HttpActionExecutedContext.cs60
-rw-r--r--src/System.Web.Http/Filters/HttpFilterCollection.cs57
-rw-r--r--src/System.Web.Http/Filters/IActionFilter.cs14
-rw-r--r--src/System.Web.Http/Filters/IAuthorizationFilter.cs14
-rw-r--r--src/System.Web.Http/Filters/IExceptionFilter.cs10
-rw-r--r--src/System.Web.Http/Filters/IFilter.cs7
-rw-r--r--src/System.Web.Http/Filters/IFilterProvider.cs10
-rw-r--r--src/System.Web.Http/Filters/QueryCompositionFilterAttribute.cs104
-rw-r--r--src/System.Web.Http/Filters/QueryCompositionFilterProvider.cs35
-rw-r--r--src/System.Web.Http/FromBodyAttribute.cs14
-rw-r--r--src/System.Web.Http/FromUriAttribute.cs17
-rw-r--r--src/System.Web.Http/GlobalSuppressions.cs17
-rw-r--r--src/System.Web.Http/Hosting/HttpPipelineFactory.cs54
-rw-r--r--src/System.Web.Http/Hosting/HttpPropertyKeys.cs45
-rw-r--r--src/System.Web.Http/HttpBindNeverAttribute.cs11
-rw-r--r--src/System.Web.Http/HttpBindRequiredAttribute.cs11
-rw-r--r--src/System.Web.Http/HttpBindingBehavior.cs11
-rw-r--r--src/System.Web.Http/HttpBindingBehaviorAttribute.cs23
-rw-r--r--src/System.Web.Http/HttpConfiguration.cs183
-rw-r--r--src/System.Web.Http/HttpDeleteAttribute.cs21
-rw-r--r--src/System.Web.Http/HttpGetAttribute.cs21
-rw-r--r--src/System.Web.Http/HttpPostAttribute.cs21
-rw-r--r--src/System.Web.Http/HttpPutAttribute.cs21
-rw-r--r--src/System.Web.Http/HttpRequestMessageExtensions.cs242
-rw-r--r--src/System.Web.Http/HttpResponseException.cs104
-rw-r--r--src/System.Web.Http/HttpResponseMessageExtensions.cs58
-rw-r--r--src/System.Web.Http/HttpRouteCollection.cs297
-rw-r--r--src/System.Web.Http/HttpRouteCollectionExtensions.cs59
-rw-r--r--src/System.Web.Http/HttpServer.cs190
-rw-r--r--src/System.Web.Http/IncludeErrorDetailPolicy.cs23
-rw-r--r--src/System.Web.Http/Internal/CollectionModelBinderUtil.cs145
-rw-r--r--src/System.Web.Http/Internal/DataTypeUtil.cs72
-rw-r--r--src/System.Web.Http/Internal/HttpActionContextExtensions.cs39
-rw-r--r--src/System.Web.Http/Internal/MemberInfoExtensions.cs18
-rw-r--r--src/System.Web.Http/Internal/ParameterDescriptorExtensionMethods.cs16
-rw-r--r--src/System.Web.Http/Internal/ParameterInfoExtensions.cs49
-rw-r--r--src/System.Web.Http/Internal/TypeActivator.cs26
-rw-r--r--src/System.Web.Http/Internal/TypeDescriptorHelper.cs17
-rw-r--r--src/System.Web.Http/Internal/TypeHelper.cs366
-rw-r--r--src/System.Web.Http/Internal/UriQueryUtility.cs544
-rw-r--r--src/System.Web.Http/Internal/ValueProviderUtil.cs161
-rw-r--r--src/System.Web.Http/Metadata/IMetadataAware.cs12
-rw-r--r--src/System.Web.Http/Metadata/ModelMetadata.cs264
-rw-r--r--src/System.Web.Http/Metadata/ModelMetadataProvider.cs13
-rw-r--r--src/System.Web.Http/Metadata/Providers/AssociatedMetadataProvider.cs110
-rw-r--r--src/System.Web.Http/Metadata/Providers/CachedAssociatedMetadataProvider.cs94
-rw-r--r--src/System.Web.Http/Metadata/Providers/CachedDataAnnotationsMetadataAttributes.cs92
-rw-r--r--src/System.Web.Http/Metadata/Providers/CachedDataAnnotationsModelMetadata.cs268
-rw-r--r--src/System.Web.Http/Metadata/Providers/CachedDataAnnotationsModelMetadataProvider.cs17
-rw-r--r--src/System.Web.Http/Metadata/Providers/CachedModelMetadata.cs415
-rw-r--r--src/System.Web.Http/Metadata/Providers/EmptyMetadataProvider.cs12
-rw-r--r--src/System.Web.Http/ModelBinding/Binders/ArrayModelBinder.cs15
-rw-r--r--src/System.Web.Http/ModelBinding/Binders/ArrayModelBinderProvider.cs21
-rw-r--r--src/System.Web.Http/ModelBinding/Binders/BinaryDataModelBinderProvider.cs80
-rw-r--r--src/System.Web.Http/ModelBinding/Binders/CollectionModelBinder.cs133
-rw-r--r--src/System.Web.Http/ModelBinding/Binders/CollectionModelBinderProvider.cs23
-rw-r--r--src/System.Web.Http/ModelBinding/Binders/ComplexModelDto.cs39
-rw-r--r--src/System.Web.Http/ModelBinding/Binders/ComplexModelDtoModelBinder.cs39
-rw-r--r--src/System.Web.Http/ModelBinding/Binders/ComplexModelDtoModelBinderProvider.cs24
-rw-r--r--src/System.Web.Http/ModelBinding/Binders/ComplexModelDtoResult.cs23
-rw-r--r--src/System.Web.Http/ModelBinding/Binders/CompositeModelBinder.cs130
-rw-r--r--src/System.Web.Http/ModelBinding/Binders/CompositeModelBinderProvider.cs42
-rw-r--r--src/System.Web.Http/ModelBinding/Binders/DictionaryModelBinder.cs15
-rw-r--r--src/System.Web.Http/ModelBinding/Binders/DictionaryModelBinderProvider.cs23
-rw-r--r--src/System.Web.Http/ModelBinding/Binders/GenericModelBinderProvider.cs137
-rw-r--r--src/System.Web.Http/ModelBinding/Binders/KeyValuePairModelBinder.cs30
-rw-r--r--src/System.Web.Http/ModelBinding/Binders/KeyValuePairModelBinderProvider.cs26
-rw-r--r--src/System.Web.Http/ModelBinding/Binders/MutableObjectModelBinder.cs257
-rw-r--r--src/System.Web.Http/ModelBinding/Binders/MutableObjectModelBinderProvider.cs32
-rw-r--r--src/System.Web.Http/ModelBinding/Binders/SimpleModelBinderProvider.cs64
-rw-r--r--src/System.Web.Http/ModelBinding/Binders/TypeConverterModelBinder.cs62
-rw-r--r--src/System.Web.Http/ModelBinding/Binders/TypeConverterModelBinderProvider.cs28
-rw-r--r--src/System.Web.Http/ModelBinding/Binders/TypeMatchModelBinder.cs43
-rw-r--r--src/System.Web.Http/ModelBinding/Binders/TypeMatchModelBinderProvider.cs15
-rw-r--r--src/System.Web.Http/ModelBinding/CancellationTokenParameterBinding.cs25
-rw-r--r--src/System.Web.Http/ModelBinding/CustomModelBinderAttribute.cs13
-rw-r--r--src/System.Web.Http/ModelBinding/DefaultActionValueBinder.cs149
-rw-r--r--src/System.Web.Http/ModelBinding/ErrorParameterBinding.cs40
-rw-r--r--src/System.Web.Http/ModelBinding/FormDataCollectionExtensions.cs181
-rw-r--r--src/System.Web.Http/ModelBinding/FormatterParameterBinding.cs81
-rw-r--r--src/System.Web.Http/ModelBinding/HttpActionBinding.cs91
-rw-r--r--src/System.Web.Http/ModelBinding/HttpParameterBinding.cs70
-rw-r--r--src/System.Web.Http/ModelBinding/HttpRequestParameterBinding.cs31
-rw-r--r--src/System.Web.Http/ModelBinding/IModelBinder.cs9
-rw-r--r--src/System.Web.Http/ModelBinding/JQueryMVCFormUrlEncodedFormatter.cs53
-rw-r--r--src/System.Web.Http/ModelBinding/ModelBinderAttribute.cs92
-rw-r--r--src/System.Web.Http/ModelBinding/ModelBinderConfig.cs99
-rw-r--r--src/System.Web.Http/ModelBinding/ModelBinderErrorMessageProvider.cs7
-rw-r--r--src/System.Web.Http/ModelBinding/ModelBinderParameterBinding.cs87
-rw-r--r--src/System.Web.Http/ModelBinding/ModelBinderProvider.cs9
-rw-r--r--src/System.Web.Http/ModelBinding/ModelBindingContext.cs125
-rw-r--r--src/System.Web.Http/ModelBinding/ModelBindingHelper.cs193
-rw-r--r--src/System.Web.Http/ModelBinding/ModelError.cs33
-rw-r--r--src/System.Web.Http/ModelBinding/ModelErrorCollection.cs18
-rw-r--r--src/System.Web.Http/ModelBinding/ModelState.cs17
-rw-r--r--src/System.Web.Http/ModelBinding/ModelStateDictionary.cs182
-rw-r--r--src/System.Web.Http/Modelbinding_ClassDiagram.cd148
-rw-r--r--src/System.Web.Http/NonActionAttribute.cs14
-rw-r--r--src/System.Web.Http/Properties/AssemblyInfo.cs9
-rw-r--r--src/System.Web.Http/Properties/SRResources.Designer.cs1190
-rw-r--r--src/System.Web.Http/Properties/SRResources.resx497
-rw-r--r--src/System.Web.Http/Query/DynamicQueryable.cs2214
-rw-r--r--src/System.Web.Http/Query/ODataQueryDeserializer.cs190
-rw-r--r--src/System.Web.Http/Query/QueryComposer.cs71
-rw-r--r--src/System.Web.Http/Query/QueryResolver.cs19
-rw-r--r--src/System.Web.Http/Query/QueryTypeHelper.cs49
-rw-r--r--src/System.Web.Http/Query/QueryValidator.cs71
-rw-r--r--src/System.Web.Http/Query/ServiceQuery.cs21
-rw-r--r--src/System.Web.Http/Query/ServiceQueryPart.cs62
-rw-r--r--src/System.Web.Http/ResultLimitAttribute.cs72
-rw-r--r--src/System.Web.Http/Routing/BoundRouteTemplate.cs12
-rw-r--r--src/System.Web.Http/Routing/HttpMethodConstraint.cs85
-rw-r--r--src/System.Web.Http/Routing/HttpParsedRoute.cs815
-rw-r--r--src/System.Web.Http/Routing/HttpRoute.cs228
-rw-r--r--src/System.Web.Http/Routing/HttpRouteData.cs42
-rw-r--r--src/System.Web.Http/Routing/HttpRouteDirection.cs8
-rw-r--r--src/System.Web.Http/Routing/HttpRouteParser.cs360
-rw-r--r--src/System.Web.Http/Routing/HttpRouteValueDictionary.cs37
-rw-r--r--src/System.Web.Http/Routing/HttpVirtualPathData.cs27
-rw-r--r--src/System.Web.Http/Routing/IHttpRoute.cs49
-rw-r--r--src/System.Web.Http/Routing/IHttpRouteConstraint.cs10
-rw-r--r--src/System.Web.Http/Routing/IHttpRouteData.cs11
-rw-r--r--src/System.Web.Http/Routing/IHttpVirtualPathData.cs9
-rw-r--r--src/System.Web.Http/Routing/PathContentSegment.cs52
-rw-r--r--src/System.Web.Http/Routing/PathLiteralSubsegment.cs28
-rw-r--r--src/System.Web.Http/Routing/PathParameterSubsegment.cs38
-rw-r--r--src/System.Web.Http/Routing/PathSegment.cs13
-rw-r--r--src/System.Web.Http/Routing/PathSeparatorSegment.cs21
-rw-r--r--src/System.Web.Http/Routing/PathSubsegment.cs13
-rw-r--r--src/System.Web.Http/Routing/UrlHelper.cs86
-rw-r--r--src/System.Web.Http/Services/DefaultServiceResolver.cs154
-rw-r--r--src/System.Web.Http/Services/DependencyResolver.cs255
-rw-r--r--src/System.Web.Http/Services/IDependencyResolver.cs22
-rw-r--r--src/System.Web.Http/Settings.StyleCop11
-rw-r--r--src/System.Web.Http/System.Web.Http.csproj374
-rw-r--r--src/System.Web.Http/Tracing/FormattingUtilities.cs147
-rw-r--r--src/System.Web.Http/Tracing/HttpRequestMessageExtensions.cs30
-rw-r--r--src/System.Web.Http/Tracing/IFormatterTracer.cs22
-rw-r--r--src/System.Web.Http/Tracing/ITraceManager.cs21
-rw-r--r--src/System.Web.Http/Tracing/ITraceWriter.cs41
-rw-r--r--src/System.Web.Http/Tracing/ITraceWriterExtensions.cs746
-rw-r--r--src/System.Web.Http/Tracing/TraceCategories.cs25
-rw-r--r--src/System.Web.Http/Tracing/TraceKind.cs23
-rw-r--r--src/System.Web.Http/Tracing/TraceLevel.cs45
-rw-r--r--src/System.Web.Http/Tracing/TraceManager.cs101
-rw-r--r--src/System.Web.Http/Tracing/TraceRecord.cs87
-rw-r--r--src/System.Web.Http/Tracing/Tracers/ActionFilterAttributeTracer.cs113
-rw-r--r--src/System.Web.Http/Tracing/Tracers/ActionFilterTracer.cs48
-rw-r--r--src/System.Web.Http/Tracing/Tracers/ActionValueBinderTracer.cs41
-rw-r--r--src/System.Web.Http/Tracing/Tracers/AuthorizationFilterAttributeTracer.cs58
-rw-r--r--src/System.Web.Http/Tracing/Tracers/AuthorizationFilterTracer.cs48
-rw-r--r--src/System.Web.Http/Tracing/Tracers/BufferedMediaTypeFormatterTracer.cs123
-rw-r--r--src/System.Web.Http/Tracing/Tracers/ContentNegotiatorTracer.cs77
-rw-r--r--src/System.Web.Http/Tracing/Tracers/ExceptionFilterAttributeTracer.cs63
-rw-r--r--src/System.Web.Http/Tracing/Tracers/ExceptionFilterTracer.cs49
-rw-r--r--src/System.Web.Http/Tracing/Tracers/FilterTracer.cs104
-rw-r--r--src/System.Web.Http/Tracing/Tracers/FormUrlEncodedMediaTypeFormatterTracer.cs62
-rw-r--r--src/System.Web.Http/Tracing/Tracers/FormatterParameterBindingTracer.cs77
-rw-r--r--src/System.Web.Http/Tracing/Tracers/HttpActionBindingTracer.cs58
-rw-r--r--src/System.Web.Http/Tracing/Tracers/HttpActionDescriptorTracer.cs123
-rw-r--r--src/System.Web.Http/Tracing/Tracers/HttpActionInvokerTracer.cs62
-rw-r--r--src/System.Web.Http/Tracing/Tracers/HttpActionSelectorTracer.cs55
-rw-r--r--src/System.Web.Http/Tracing/Tracers/HttpControllerActivatorTracer.cs53
-rw-r--r--src/System.Web.Http/Tracing/Tracers/HttpControllerFactoryTracer.cs90
-rw-r--r--src/System.Web.Http/Tracing/Tracers/HttpControllerTracer.cs60
-rw-r--r--src/System.Web.Http/Tracing/Tracers/HttpParameterBindingTracer.cs87
-rw-r--r--src/System.Web.Http/Tracing/Tracers/JsonMediaTypeFormatterTracer.cs62
-rw-r--r--src/System.Web.Http/Tracing/Tracers/MediaTypeFormatterTracer.cs233
-rw-r--r--src/System.Web.Http/Tracing/Tracers/MessageHandlerTracer.cs44
-rw-r--r--src/System.Web.Http/Tracing/Tracers/RequestMessageHandlerTracer.cs71
-rw-r--r--src/System.Web.Http/Tracing/Tracers/XmlMediaTypeFormatterTracer.cs63
-rw-r--r--src/System.Web.Http/Validation/ClientRules/ModelClientValidationRangeRule.cs13
-rw-r--r--src/System.Web.Http/Validation/ClientRules/ModelClientValidationRegexRule.cs12
-rw-r--r--src/System.Web.Http/Validation/ClientRules/ModelClientValidationRequiredRule.cs11
-rw-r--r--src/System.Web.Http/Validation/ClientRules/ModelClientValidationStringLengthRule.cs21
-rw-r--r--src/System.Web.Http/Validation/DefaultBodyModelValidator.cs161
-rw-r--r--src/System.Web.Http/Validation/IBodyModelValidator.cs24
-rw-r--r--src/System.Web.Http/Validation/IClientValidatable.cs21
-rw-r--r--src/System.Web.Http/Validation/ModelClientValidationRule.cs23
-rw-r--r--src/System.Web.Http/Validation/ModelStateFormatterLogger.cs38
-rw-r--r--src/System.Web.Http/Validation/ModelValidatedEventArgs.cs23
-rw-r--r--src/System.Web.Http/Validation/ModelValidatingEventArgs.cs24
-rw-r--r--src/System.Web.Http/Validation/ModelValidationNode.cs206
-rw-r--r--src/System.Web.Http/Validation/ModelValidationRequiredMemberSelector.cs48
-rw-r--r--src/System.Web.Http/Validation/ModelValidationResult.cs20
-rw-r--r--src/System.Web.Http/Validation/ModelValidator.cs90
-rw-r--r--src/System.Web.Http/Validation/ModelValidatorProvider.cs11
-rw-r--r--src/System.Web.Http/Validation/Providers/AssociatedValidatorProvider.cs57
-rw-r--r--src/System.Web.Http/Validation/Providers/ClientDataTypeModelValidatorProvider.cs164
-rw-r--r--src/System.Web.Http/Validation/Providers/DataAnnotationsModelValidatorProvider.cs345
-rw-r--r--src/System.Web.Http/Validation/Providers/DataMemberModelValidatorProvider.cs29
-rw-r--r--src/System.Web.Http/Validation/Validators/DataAnnotationsModelValidator.cs93
-rw-r--r--src/System.Web.Http/Validation/Validators/RangeAttributeAdapter.cs25
-rw-r--r--src/System.Web.Http/Validation/Validators/RegularExpressionAttributeAdapter.cs24
-rw-r--r--src/System.Web.Http/Validation/Validators/RequiredAttributeAdapter.cs24
-rw-r--r--src/System.Web.Http/Validation/Validators/RequiredMemberModelValidator.cs47
-rw-r--r--src/System.Web.Http/Validation/Validators/StringLengthAttributeAdapter.cs24
-rw-r--r--src/System.Web.Http/Validation/Validators/ValidatableObjectAdapter.cs60
-rw-r--r--src/System.Web.Http/ValueProviders/IEnumerableValueProvider.cs9
-rw-r--r--src/System.Web.Http/ValueProviders/IUriValueProviderFactory.cs14
-rw-r--r--src/System.Web.Http/ValueProviders/IValueProvider.cs8
-rw-r--r--src/System.Web.Http/ValueProviders/Providers/CompositeValueProvider.cs66
-rw-r--r--src/System.Web.Http/ValueProviders/Providers/CompositeValueProviderFactory.cs22
-rw-r--r--src/System.Web.Http/ValueProviders/Providers/ElementalValueProvider.cs34
-rw-r--r--src/System.Web.Http/ValueProviders/Providers/KeyValueModelValueProvider.cs98
-rw-r--r--src/System.Web.Http/ValueProviders/Providers/NameValueCollectionValueProvider.cs95
-rw-r--r--src/System.Web.Http/ValueProviders/Providers/QueryStringValueProvider.cs31
-rw-r--r--src/System.Web.Http/ValueProviders/Providers/QueryStringValueProviderFactory.cs13
-rw-r--r--src/System.Web.Http/ValueProviders/Providers/RouteDataValueProvider.cs28
-rw-r--r--src/System.Web.Http/ValueProviders/Providers/RouteDataValueProviderFactory.cs17
-rw-r--r--src/System.Web.Http/ValueProviders/SingleObjectKeyValueModel.cs49
-rw-r--r--src/System.Web.Http/ValueProviders/ValueProviderAttribute.cs56
-rw-r--r--src/System.Web.Http/ValueProviders/ValueProviderFactory.cs9
-rw-r--r--src/System.Web.Http/ValueProviders/ValueProviderResult.cs160
-rw-r--r--src/System.Web.Http/packages.config5
-rw-r--r--src/System.Web.Mvc/AcceptVerbsAttribute.cs64
-rw-r--r--src/System.Web.Mvc/ActionDescriptor.cs196
-rw-r--r--src/System.Web.Mvc/ActionDescriptorHelper.cs46
-rw-r--r--src/System.Web.Mvc/ActionExecutedContext.cs42
-rw-r--r--src/System.Web.Mvc/ActionExecutingContext.cs37
-rw-r--r--src/System.Web.Mvc/ActionFilterAttribute.cs25
-rw-r--r--src/System.Web.Mvc/ActionMethodDispatcher.cs79
-rw-r--r--src/System.Web.Mvc/ActionMethodDispatcherCache.cs16
-rw-r--r--src/System.Web.Mvc/ActionMethodSelector.cs116
-rw-r--r--src/System.Web.Mvc/ActionMethodSelectorAttribute.cs10
-rw-r--r--src/System.Web.Mvc/ActionNameAttribute.cs26
-rw-r--r--src/System.Web.Mvc/ActionNameSelectorAttribute.cs10
-rw-r--r--src/System.Web.Mvc/ActionResult.cs7
-rw-r--r--src/System.Web.Mvc/ActionSelector.cs4
-rw-r--r--src/System.Web.Mvc/AdditionalMetaDataAttribute.cs38
-rw-r--r--src/System.Web.Mvc/Ajax/AjaxExtensions.cs358
-rw-r--r--src/System.Web.Mvc/Ajax/AjaxOptions.cs216
-rw-r--r--src/System.Web.Mvc/Ajax/InsertionMode.cs9
-rw-r--r--src/System.Web.Mvc/AjaxHelper.cs83
-rw-r--r--src/System.Web.Mvc/AjaxHelper`1.cs39
-rw-r--r--src/System.Web.Mvc/AjaxRequestExtensions.cs15
-rw-r--r--src/System.Web.Mvc/AllowAnonymousAttribute.cs11
-rw-r--r--src/System.Web.Mvc/AllowHtmlAttribute.cs19
-rw-r--r--src/System.Web.Mvc/AreaHelpers.cs35
-rw-r--r--src/System.Web.Mvc/AreaRegistration.cs54
-rw-r--r--src/System.Web.Mvc/AreaRegistrationContext.cs93
-rw-r--r--src/System.Web.Mvc/AssociatedMetadataProvider.cs110
-rw-r--r--src/System.Web.Mvc/AssociatedValidatorProvider.cs59
-rw-r--r--src/System.Web.Mvc/Async/ActionDescriptorCreator.cs4
-rw-r--r--src/System.Web.Mvc/Async/AsyncActionDescriptor.cs32
-rw-r--r--src/System.Web.Mvc/Async/AsyncActionMethodSelector.cs227
-rw-r--r--src/System.Web.Mvc/Async/AsyncControllerActionInvoker.cs324
-rw-r--r--src/System.Web.Mvc/Async/AsyncManager.cs80
-rw-r--r--src/System.Web.Mvc/Async/AsyncResultWrapper.cs303
-rw-r--r--src/System.Web.Mvc/Async/AsyncUtil.cs31
-rw-r--r--src/System.Web.Mvc/Async/AsyncVoid.cs7
-rw-r--r--src/System.Web.Mvc/Async/BeginInvokeDelegate.cs4
-rw-r--r--src/System.Web.Mvc/Async/EndInvokeDelegate.cs4
-rw-r--r--src/System.Web.Mvc/Async/EndInvokeDelegate`1.cs4
-rw-r--r--src/System.Web.Mvc/Async/IAsyncActionInvoker.cs8
-rw-r--r--src/System.Web.Mvc/Async/IAsyncController.cs10
-rw-r--r--src/System.Web.Mvc/Async/IAsyncManagerContainer.cs7
-rw-r--r--src/System.Web.Mvc/Async/OperationCounter.cs56
-rw-r--r--src/System.Web.Mvc/Async/ReflectedAsyncActionDescriptor.cs188
-rw-r--r--src/System.Web.Mvc/Async/ReflectedAsyncControllerDescriptor.cs104
-rw-r--r--src/System.Web.Mvc/Async/SimpleAsyncResult.cs53
-rw-r--r--src/System.Web.Mvc/Async/SingleEntryGate.cs20
-rw-r--r--src/System.Web.Mvc/Async/SynchronizationContextUtil.cs52
-rw-r--r--src/System.Web.Mvc/Async/SynchronousOperationException.cs30
-rw-r--r--src/System.Web.Mvc/Async/TaskAsyncActionDescriptor.cs264
-rw-r--r--src/System.Web.Mvc/Async/TaskWrapperAsyncResult.cs43
-rw-r--r--src/System.Web.Mvc/Async/Trigger.cs20
-rw-r--r--src/System.Web.Mvc/Async/TriggerListener.cs65
-rw-r--r--src/System.Web.Mvc/AsyncController.cs10
-rw-r--r--src/System.Web.Mvc/AsyncTimeoutAttribute.cs41
-rw-r--r--src/System.Web.Mvc/AuthorizationContext.cs34
-rw-r--r--src/System.Web.Mvc/AuthorizeAttribute.cs152
-rw-r--r--src/System.Web.Mvc/BindAttribute.cs50
-rw-r--r--src/System.Web.Mvc/BuildManagerCompiledView.cs85
-rw-r--r--src/System.Web.Mvc/BuildManagerViewEngine.cs92
-rw-r--r--src/System.Web.Mvc/BuildManagerWrapper.cs34
-rw-r--r--src/System.Web.Mvc/ByteArrayModelBinder.cs34
-rw-r--r--src/System.Web.Mvc/CachedAssociatedMetadataProvider`1.cs94
-rw-r--r--src/System.Web.Mvc/CachedDataAnnotationsMetadataAttributes.cs54
-rw-r--r--src/System.Web.Mvc/CachedDataAnnotationsModelMetadata.cs216
-rw-r--r--src/System.Web.Mvc/CachedDataAnnotationsModelMetadataProvider.cs17
-rw-r--r--src/System.Web.Mvc/CachedModelMetadata`1.cs415
-rw-r--r--src/System.Web.Mvc/CancellationTokenModelBinder.cs12
-rw-r--r--src/System.Web.Mvc/ChildActionOnlyAttribute.cs19
-rw-r--r--src/System.Web.Mvc/ChildActionValueProvider.cs39
-rw-r--r--src/System.Web.Mvc/ChildActionValueProviderFactory.cs15
-rw-r--r--src/System.Web.Mvc/ClientDataTypeModelValidatorProvider.cs159
-rw-r--r--src/System.Web.Mvc/CompareAttribute.cs74
-rw-r--r--src/System.Web.Mvc/ContentResult.cs36
-rw-r--r--src/System.Web.Mvc/Controller.cs920
-rw-r--r--src/System.Web.Mvc/ControllerActionInvoker.cs372
-rw-r--r--src/System.Web.Mvc/ControllerBase.cs130
-rw-r--r--src/System.Web.Mvc/ControllerBuilder.cs88
-rw-r--r--src/System.Web.Mvc/ControllerContext.cs133
-rw-r--r--src/System.Web.Mvc/ControllerDescriptor.cs78
-rw-r--r--src/System.Web.Mvc/ControllerDescriptorCache.cs14
-rw-r--r--src/System.Web.Mvc/ControllerInstanceFilterProvider.cs16
-rw-r--r--src/System.Web.Mvc/ControllerTypeCache.cs138
-rw-r--r--src/System.Web.Mvc/CustomModelBinderAttribute.cs13
-rw-r--r--src/System.Web.Mvc/DataAnnotationsModelMetadata.cs60
-rw-r--r--src/System.Web.Mvc/DataAnnotationsModelMetadataProvider.cs115
-rw-r--r--src/System.Web.Mvc/DataAnnotationsModelValidator.cs67
-rw-r--r--src/System.Web.Mvc/DataAnnotationsModelValidatorProvider.cs375
-rw-r--r--src/System.Web.Mvc/DataAnnotationsModelValidator`1.cs18
-rw-r--r--src/System.Web.Mvc/DataErrorInfoModelValidatorProvider.cs95
-rw-r--r--src/System.Web.Mvc/DataTypeUtil.cs109
-rw-r--r--src/System.Web.Mvc/DefaultControllerFactory.cs294
-rw-r--r--src/System.Web.Mvc/DefaultModelBinder.cs840
-rw-r--r--src/System.Web.Mvc/DefaultViewLocationCache.cs52
-rw-r--r--src/System.Web.Mvc/DependencyResolver.cs210
-rw-r--r--src/System.Web.Mvc/DependencyResolverExtensions.cs18
-rw-r--r--src/System.Web.Mvc/DescriptorUtil.cs86
-rw-r--r--src/System.Web.Mvc/DictionaryHelpers.cs56
-rw-r--r--src/System.Web.Mvc/DictionaryValueProvider`1.cs65
-rw-r--r--src/System.Web.Mvc/DynamicViewDataDictionary.cs47
-rw-r--r--src/System.Web.Mvc/EmptyModelMetadataProvider.cs12
-rw-r--r--src/System.Web.Mvc/EmptyModelValidatorProvider.cs13
-rw-r--r--src/System.Web.Mvc/EmptyResult.cs17
-rw-r--r--src/System.Web.Mvc/Error.cs76
-rw-r--r--src/System.Web.Mvc/ExceptionContext.cs36
-rw-r--r--src/System.Web.Mvc/ExpressionHelper.cs131
-rw-r--r--src/System.Web.Mvc/ExpressionUtil/BinaryExpressionFingerprint.cs41
-rw-r--r--src/System.Web.Mvc/ExpressionUtil/CachedExpressionCompiler.cs142
-rw-r--r--src/System.Web.Mvc/ExpressionUtil/ConditionalExpressionFingerprint.cs28
-rw-r--r--src/System.Web.Mvc/ExpressionUtil/ConstantExpressionFingerprint.cs32
-rw-r--r--src/System.Web.Mvc/ExpressionUtil/DefaultExpressionFingerprint.cs28
-rw-r--r--src/System.Web.Mvc/ExpressionUtil/ExpressionFingerprint.cs47
-rw-r--r--src/System.Web.Mvc/ExpressionUtil/ExpressionFingerprintChain.cs85
-rw-r--r--src/System.Web.Mvc/ExpressionUtil/FingerprintingExpressionVisitor.cs296
-rw-r--r--src/System.Web.Mvc/ExpressionUtil/HashCodeCombiner.cs56
-rw-r--r--src/System.Web.Mvc/ExpressionUtil/Hoisted`2.cs6
-rw-r--r--src/System.Web.Mvc/ExpressionUtil/HoistingExpressionVisitor.cs35
-rw-r--r--src/System.Web.Mvc/ExpressionUtil/IndexExpressionFingerprint.cs41
-rw-r--r--src/System.Web.Mvc/ExpressionUtil/LambdaExpressionFingerprint.cs28
-rw-r--r--src/System.Web.Mvc/ExpressionUtil/MemberExpressionFingerprint.cs38
-rw-r--r--src/System.Web.Mvc/ExpressionUtil/MethodCallExpressionFingerprint.cs41
-rw-r--r--src/System.Web.Mvc/ExpressionUtil/ParameterExpressionFingerprint.cs37
-rw-r--r--src/System.Web.Mvc/ExpressionUtil/TypeBinaryExpressionFingerprint.cs37
-rw-r--r--src/System.Web.Mvc/ExpressionUtil/UnaryExpressionFingerprint.cs41
-rw-r--r--src/System.Web.Mvc/FieldValidationMetadata.cs26
-rw-r--r--src/System.Web.Mvc/FileContentResult.cs26
-rw-r--r--src/System.Web.Mvc/FilePathResult.cs25
-rw-r--r--src/System.Web.Mvc/FileResult.cs143
-rw-r--r--src/System.Web.Mvc/FileStreamResult.cs45
-rw-r--r--src/System.Web.Mvc/Filter.cs34
-rw-r--r--src/System.Web.Mvc/FilterAttribute.cs41
-rw-r--r--src/System.Web.Mvc/FilterAttributeFilterProvider.cs46
-rw-r--r--src/System.Web.Mvc/FilterInfo.cs48
-rw-r--r--src/System.Web.Mvc/FilterProviderCollection.cs125
-rw-r--r--src/System.Web.Mvc/FilterProviders.cs15
-rw-r--r--src/System.Web.Mvc/FilterScope.cs11
-rw-r--r--src/System.Web.Mvc/FormCollection.cs96
-rw-r--r--src/System.Web.Mvc/FormContext.cs81
-rw-r--r--src/System.Web.Mvc/FormMethod.cs8
-rw-r--r--src/System.Web.Mvc/FormValueProvider.cs19
-rw-r--r--src/System.Web.Mvc/FormValueProviderFactory.cs30
-rw-r--r--src/System.Web.Mvc/GlobalFilterCollection.cs61
-rw-r--r--src/System.Web.Mvc/GlobalFilters.cs12
-rw-r--r--src/System.Web.Mvc/GlobalSuppressions.cs19
-rw-r--r--src/System.Web.Mvc/HandleErrorAttribute.cs107
-rw-r--r--src/System.Web.Mvc/HandleErrorInfo.cs33
-rw-r--r--src/System.Web.Mvc/HiddenInputAttribute.cs13
-rw-r--r--src/System.Web.Mvc/Html/ChildActionExtensions.cs181
-rw-r--r--src/System.Web.Mvc/Html/DefaultDisplayTemplates.cs225
-rw-r--r--src/System.Web.Mvc/Html/DefaultEditorTemplates.cs236
-rw-r--r--src/System.Web.Mvc/Html/DisplayExtensions.cs105
-rw-r--r--src/System.Web.Mvc/Html/DisplayNameExtensions.cs60
-rw-r--r--src/System.Web.Mvc/Html/DisplayTextExtensions.cs24
-rw-r--r--src/System.Web.Mvc/Html/EditorExtensions.cs105
-rw-r--r--src/System.Web.Mvc/Html/FormExtensions.cs180
-rw-r--r--src/System.Web.Mvc/Html/InputExtensions.cs581
-rw-r--r--src/System.Web.Mvc/Html/LabelExtensions.cs159
-rw-r--r--src/System.Web.Mvc/Html/LinkExtensions.cs130
-rw-r--r--src/System.Web.Mvc/Html/MvcForm.cs51
-rw-r--r--src/System.Web.Mvc/Html/NameExtensions.cs43
-rw-r--r--src/System.Web.Mvc/Html/PartialExtensions.cs32
-rw-r--r--src/System.Web.Mvc/Html/RenderPartialExtensions.cs29
-rw-r--r--src/System.Web.Mvc/Html/SelectExtensions.cs317
-rw-r--r--src/System.Web.Mvc/Html/TemplateHelpers.cs333
-rw-r--r--src/System.Web.Mvc/Html/TextAreaExtensions.cs192
-rw-r--r--src/System.Web.Mvc/Html/ValidationExtensions.cs390
-rw-r--r--src/System.Web.Mvc/Html/ValueExtensions.cs80
-rw-r--r--src/System.Web.Mvc/HtmlHelper.cs451
-rw-r--r--src/System.Web.Mvc/HtmlHelper`1.cs39
-rw-r--r--src/System.Web.Mvc/HttpDeleteAttribute.cs15
-rw-r--r--src/System.Web.Mvc/HttpFileCollectionValueProvider.cs44
-rw-r--r--src/System.Web.Mvc/HttpFileCollectionValueProviderFactory.cs15
-rw-r--r--src/System.Web.Mvc/HttpGetAttribute.cs15
-rw-r--r--src/System.Web.Mvc/HttpHandlerUtil.cs91
-rw-r--r--src/System.Web.Mvc/HttpNotFoundResult.cs18
-rw-r--r--src/System.Web.Mvc/HttpPostAttribute.cs15
-rw-r--r--src/System.Web.Mvc/HttpPostedFileBaseModelBinder.cs39
-rw-r--r--src/System.Web.Mvc/HttpPutAttribute.cs15
-rw-r--r--src/System.Web.Mvc/HttpRequestExtensions.cs54
-rw-r--r--src/System.Web.Mvc/HttpStatusCodeResult.cs46
-rw-r--r--src/System.Web.Mvc/HttpUnauthorizedResult.cs21
-rw-r--r--src/System.Web.Mvc/HttpVerbs.cs12
-rw-r--r--src/System.Web.Mvc/IActionFilter.cs8
-rw-r--r--src/System.Web.Mvc/IActionInvoker.cs7
-rw-r--r--src/System.Web.Mvc/IAuthorizationFilter.cs7
-rw-r--r--src/System.Web.Mvc/IBuildManager.cs14
-rw-r--r--src/System.Web.Mvc/IClientValidatable.cs19
-rw-r--r--src/System.Web.Mvc/IController.cs9
-rw-r--r--src/System.Web.Mvc/IControllerActivator.cs9
-rw-r--r--src/System.Web.Mvc/IControllerFactory.cs12
-rw-r--r--src/System.Web.Mvc/IDependencyResolver.cs10
-rw-r--r--src/System.Web.Mvc/IEnumerableValueProvider.cs10
-rw-r--r--src/System.Web.Mvc/IExceptionFilter.cs7
-rw-r--r--src/System.Web.Mvc/IFilterProvider.cs9
-rw-r--r--src/System.Web.Mvc/IMetadataAware.cs12
-rw-r--r--src/System.Web.Mvc/IModelBinder.cs7
-rw-r--r--src/System.Web.Mvc/IModelBinderProvider.cs7
-rw-r--r--src/System.Web.Mvc/IMvcControlBuilder.cs7
-rw-r--r--src/System.Web.Mvc/IMvcFilter.cs8
-rw-r--r--src/System.Web.Mvc/IResolver.cs7
-rw-r--r--src/System.Web.Mvc/IResultFilter.cs8
-rw-r--r--src/System.Web.Mvc/IRouteWithArea.cs7
-rw-r--r--src/System.Web.Mvc/ITempDataProvider.cs10
-rw-r--r--src/System.Web.Mvc/IUniquelyIdentifiable.cs7
-rw-r--r--src/System.Web.Mvc/IUnvalidatedRequestValues.cs13
-rw-r--r--src/System.Web.Mvc/IUnvalidatedValueProvider.cs8
-rw-r--r--src/System.Web.Mvc/IValueProvider.cs8
-rw-r--r--src/System.Web.Mvc/IView.cs9
-rw-r--r--src/System.Web.Mvc/IViewDataContainer.cs10
-rw-r--r--src/System.Web.Mvc/IViewEngine.cs9
-rw-r--r--src/System.Web.Mvc/IViewLocationCache.cs8
-rw-r--r--src/System.Web.Mvc/IViewPageActivator.cs7
-rw-r--r--src/System.Web.Mvc/IViewStartPageChild.cs9
-rw-r--r--src/System.Web.Mvc/InputType.cs11
-rw-r--r--src/System.Web.Mvc/JavaScriptResult.cs23
-rw-r--r--src/System.Web.Mvc/JsonRequestBehavior.cs8
-rw-r--r--src/System.Web.Mvc/JsonResult.cs73
-rw-r--r--src/System.Web.Mvc/JsonValueProviderFactory.cs131
-rw-r--r--src/System.Web.Mvc/LinqBinaryModelBinder.cs18
-rw-r--r--src/System.Web.Mvc/ModelBinderAttribute.cs44
-rw-r--r--src/System.Web.Mvc/ModelBinderDictionary.cs167
-rw-r--r--src/System.Web.Mvc/ModelBinderProviderCollection.cs66
-rw-r--r--src/System.Web.Mvc/ModelBinderProviders.cs14
-rw-r--r--src/System.Web.Mvc/ModelBinders.cs71
-rw-r--r--src/System.Web.Mvc/ModelBindingContext.cs138
-rw-r--r--src/System.Web.Mvc/ModelError.cs31
-rw-r--r--src/System.Web.Mvc/ModelErrorCollection.cs18
-rw-r--r--src/System.Web.Mvc/ModelMetadata.cs407
-rw-r--r--src/System.Web.Mvc/ModelMetadataProvider.cs13
-rw-r--r--src/System.Web.Mvc/ModelMetadataProviders.cs29
-rw-r--r--src/System.Web.Mvc/ModelState.cs15
-rw-r--r--src/System.Web.Mvc/ModelStateDictionary.cs180
-rw-r--r--src/System.Web.Mvc/ModelValidationResult.cs20
-rw-r--r--src/System.Web.Mvc/ModelValidator.cs86
-rw-r--r--src/System.Web.Mvc/ModelValidatorProvider.cs9
-rw-r--r--src/System.Web.Mvc/ModelValidatorProviderCollection.cs56
-rw-r--r--src/System.Web.Mvc/ModelValidatorProviders.cs17
-rw-r--r--src/System.Web.Mvc/MultiSelectList.cs118
-rw-r--r--src/System.Web.Mvc/MultiServiceResolver.cs51
-rw-r--r--src/System.Web.Mvc/MvcFilter.cs19
-rw-r--r--src/System.Web.Mvc/MvcHandler.cs248
-rw-r--r--src/System.Web.Mvc/MvcHtmlString.cs28
-rw-r--r--src/System.Web.Mvc/MvcHttpHandler.cs105
-rw-r--r--src/System.Web.Mvc/MvcRouteHandler.cs47
-rw-r--r--src/System.Web.Mvc/MvcWebRazorHostFactory.cs20
-rw-r--r--src/System.Web.Mvc/NameValueCollectionExtensions.cs33
-rw-r--r--src/System.Web.Mvc/NameValueCollectionValueProvider.cs121
-rw-r--r--src/System.Web.Mvc/NoAsyncTimeoutAttribute.cs13
-rw-r--r--src/System.Web.Mvc/NonActionAttribute.cs13
-rw-r--r--src/System.Web.Mvc/NullViewLocationCache.cs18
-rw-r--r--src/System.Web.Mvc/OutputCacheAttribute.cs355
-rw-r--r--src/System.Web.Mvc/ParameterBindingInfo.cs27
-rw-r--r--src/System.Web.Mvc/ParameterDescriptor.cs54
-rw-r--r--src/System.Web.Mvc/ParameterInfoUtil.cs33
-rw-r--r--src/System.Web.Mvc/PartialViewResult.cs28
-rw-r--r--src/System.Web.Mvc/PathHelpers.cs97
-rw-r--r--src/System.Web.Mvc/PreApplicationStartCode.cs26
-rw-r--r--src/System.Web.Mvc/Properties/AssemblyInfo.cs27
-rw-r--r--src/System.Web.Mvc/Properties/MvcResources.Designer.cs1012
-rw-r--r--src/System.Web.Mvc/Properties/MvcResources.resx439
-rw-r--r--src/System.Web.Mvc/QueryStringValueProvider.cs21
-rw-r--r--src/System.Web.Mvc/QueryStringValueProviderFactory.cs30
-rw-r--r--src/System.Web.Mvc/RangeAttributeAdapter.cs19
-rw-r--r--src/System.Web.Mvc/Razor/MvcCSharpRazorCodeGenerator.cs30
-rw-r--r--src/System.Web.Mvc/Razor/MvcCSharpRazorCodeParser.cs68
-rw-r--r--src/System.Web.Mvc/Razor/MvcVBRazorCodeParser.cs93
-rw-r--r--src/System.Web.Mvc/Razor/MvcWebPageRazorHost.cs64
-rw-r--r--src/System.Web.Mvc/Razor/SetModelTypeCodeGenerator.cs47
-rw-r--r--src/System.Web.Mvc/Razor/StartPageLookupDelegate.cs7
-rw-r--r--src/System.Web.Mvc/RazorView.cs82
-rw-r--r--src/System.Web.Mvc/RazorViewEngine.cs85
-rw-r--r--src/System.Web.Mvc/ReaderWriterCache`2.cs66
-rw-r--r--src/System.Web.Mvc/RedirectResult.cs56
-rw-r--r--src/System.Web.Mvc/RedirectToRouteResult.cs77
-rw-r--r--src/System.Web.Mvc/ReflectedActionDescriptor.cs135
-rw-r--r--src/System.Web.Mvc/ReflectedAttributeCache.cs46
-rw-r--r--src/System.Web.Mvc/ReflectedControllerDescriptor.cs99
-rw-r--r--src/System.Web.Mvc/ReflectedParameterBindingInfo.cs62
-rw-r--r--src/System.Web.Mvc/ReflectedParameterDescriptor.cs79
-rw-r--r--src/System.Web.Mvc/RegularExpressionAttributeAdapter.cs18
-rw-r--r--src/System.Web.Mvc/RemoteAttribute.cs139
-rw-r--r--src/System.Web.Mvc/RequireHttpsAttribute.cs38
-rw-r--r--src/System.Web.Mvc/RequiredAttributeAdapter.cs18
-rw-r--r--src/System.Web.Mvc/ResultExecutedContext.cs34
-rw-r--r--src/System.Web.Mvc/ResultExecutingContext.cs28
-rw-r--r--src/System.Web.Mvc/RouteCollectionExtensions.cs193
-rw-r--r--src/System.Web.Mvc/RouteDataValueProvider.cs14
-rw-r--r--src/System.Web.Mvc/RouteDataValueProviderFactory.cs15
-rw-r--r--src/System.Web.Mvc/RouteValuesHelpers.cs60
-rw-r--r--src/System.Web.Mvc/SecurityUtil.cs79
-rw-r--r--src/System.Web.Mvc/SelectList.cs37
-rw-r--r--src/System.Web.Mvc/SelectListItem.cs11
-rw-r--r--src/System.Web.Mvc/SessionStateAttribute.cs15
-rw-r--r--src/System.Web.Mvc/SessionStateTempDataProvider.cs64
-rw-r--r--src/System.Web.Mvc/SingleServiceResolver.cs65
-rw-r--r--src/System.Web.Mvc/StringLengthAttributeAdapter.cs18
-rw-r--r--src/System.Web.Mvc/System.Web.Mvc.csproj475
-rw-r--r--src/System.Web.Mvc/TagBuilderExtensions.cs13
-rw-r--r--src/System.Web.Mvc/TempDataDictionary.cs208
-rw-r--r--src/System.Web.Mvc/TemplateInfo.cs58
-rw-r--r--src/System.Web.Mvc/TryGetValueDelegate.cs4
-rw-r--r--src/System.Web.Mvc/TypeCacheSerializer.cs122
-rw-r--r--src/System.Web.Mvc/TypeCacheUtil.cs104
-rw-r--r--src/System.Web.Mvc/TypeDescriptorHelper.cs13
-rw-r--r--src/System.Web.Mvc/TypeHelpers.cs146
-rw-r--r--src/System.Web.Mvc/UnvalidatedRequestValuesAccessor.cs4
-rw-r--r--src/System.Web.Mvc/UnvalidatedRequestValuesWrapper.cs32
-rw-r--r--src/System.Web.Mvc/UrlHelper.cs222
-rw-r--r--src/System.Web.Mvc/UrlParameter.cs20
-rw-r--r--src/System.Web.Mvc/UrlRewriterHelper.cs45
-rw-r--r--src/System.Web.Mvc/ValidatableObjectAdapter.cs63
-rw-r--r--src/System.Web.Mvc/ValidateAntiForgeryTokenAttribute.cs40
-rw-r--r--src/System.Web.Mvc/ValidateInputAttribute.cs26
-rw-r--r--src/System.Web.Mvc/ValueProviderCollection.cs78
-rw-r--r--src/System.Web.Mvc/ValueProviderDictionary.cs217
-rw-r--r--src/System.Web.Mvc/ValueProviderFactories.cs20
-rw-r--r--src/System.Web.Mvc/ValueProviderFactory.cs7
-rw-r--r--src/System.Web.Mvc/ValueProviderFactoryCollection.cs56
-rw-r--r--src/System.Web.Mvc/ValueProviderResult.cs163
-rw-r--r--src/System.Web.Mvc/ValueProviderUtil.cs119
-rw-r--r--src/System.Web.Mvc/ViewContext.cs281
-rw-r--r--src/System.Web.Mvc/ViewDataDictionary.cs387
-rw-r--r--src/System.Web.Mvc/ViewDataDictionary`1.cs60
-rw-r--r--src/System.Web.Mvc/ViewDataInfo.cs42
-rw-r--r--src/System.Web.Mvc/ViewEngineCollection.cs133
-rw-r--r--src/System.Web.Mvc/ViewEngineResult.cs38
-rw-r--r--src/System.Web.Mvc/ViewEngines.cs16
-rw-r--r--src/System.Web.Mvc/ViewMasterPage.cs68
-rw-r--r--src/System.Web.Mvc/ViewMasterPageControlBuilder.cs18
-rw-r--r--src/System.Web.Mvc/ViewMasterPage`1.cs50
-rw-r--r--src/System.Web.Mvc/ViewPage.cs427
-rw-r--r--src/System.Web.Mvc/ViewPageControlBuilder.cs18
-rw-r--r--src/System.Web.Mvc/ViewPage`1.cs47
-rw-r--r--src/System.Web.Mvc/ViewResult.cs36
-rw-r--r--src/System.Web.Mvc/ViewResultBase.cs105
-rw-r--r--src/System.Web.Mvc/ViewStartPage.cs43
-rw-r--r--src/System.Web.Mvc/ViewTemplateUserControl.cs6
-rw-r--r--src/System.Web.Mvc/ViewTemplateUserControl`1.cs10
-rw-r--r--src/System.Web.Mvc/ViewType.cs19
-rw-r--r--src/System.Web.Mvc/ViewTypeControlBuilder.cs29
-rw-r--r--src/System.Web.Mvc/ViewTypeParserFilter.cs109
-rw-r--r--src/System.Web.Mvc/ViewUserControl.cs213
-rw-r--r--src/System.Web.Mvc/ViewUserControlControlBuilder.cs18
-rw-r--r--src/System.Web.Mvc/ViewUserControl`1.cs58
-rw-r--r--src/System.Web.Mvc/VirtualPathProviderViewEngine.cs345
-rw-r--r--src/System.Web.Mvc/WebFormView.cs72
-rw-r--r--src/System.Web.Mvc/WebFormViewEngine.cs62
-rw-r--r--src/System.Web.Mvc/WebViewPage.cs115
-rw-r--r--src/System.Web.Mvc/WebViewPage`1.cs47
-rw-r--r--src/System.Web.Mvc/packages.config4
-rw-r--r--src/System.Web.Razor/CSharpRazorCodeLanguage.cs46
-rw-r--r--src/System.Web.Razor/DocumentParseCompleteEventArgs.cs25
-rw-r--r--src/System.Web.Razor/Editor/AutoCompleteEditHandler.cs61
-rw-r--r--src/System.Web.Razor/Editor/BackgroundParseTask.cs110
-rw-r--r--src/System.Web.Razor/Editor/EditResult.cs16
-rw-r--r--src/System.Web.Razor/Editor/EditorHints.cs27
-rw-r--r--src/System.Web.Razor/Editor/ImplicitExpressionEditHandler.cs234
-rw-r--r--src/System.Web.Razor/Editor/SingleLineMarkupEditHandler.cs22
-rw-r--r--src/System.Web.Razor/Editor/SpanEditHandler.cs181
-rw-r--r--src/System.Web.Razor/Generator/AddImportCodeGenerator.cs66
-rw-r--r--src/System.Web.Razor/Generator/AttributeBlockCodeGenerator.cs88
-rw-r--r--src/System.Web.Razor/Generator/BaseCodeWriter.cs74
-rw-r--r--src/System.Web.Razor/Generator/BlockCodeGenerator.cs45
-rw-r--r--src/System.Web.Razor/Generator/CSharpCodeWriter.cs247
-rw-r--r--src/System.Web.Razor/Generator/CSharpRazorCodeGenerator.cs28
-rw-r--r--src/System.Web.Razor/Generator/CodeGenerationCompleteEventArgs.cs27
-rw-r--r--src/System.Web.Razor/Generator/CodeGeneratorBase.cs35
-rw-r--r--src/System.Web.Razor/Generator/CodeGeneratorContext.cs327
-rw-r--r--src/System.Web.Razor/Generator/CodeWriter.cs207
-rw-r--r--src/System.Web.Razor/Generator/CodeWriterExtensions.cs17
-rw-r--r--src/System.Web.Razor/Generator/DynamicAttributeBlockCodeGenerator.cs139
-rw-r--r--src/System.Web.Razor/Generator/ExpressionCodeGenerator.cs110
-rw-r--r--src/System.Web.Razor/Generator/ExpressionRenderingMode.cs26
-rw-r--r--src/System.Web.Razor/Generator/GeneratedClassContext.cs174
-rw-r--r--src/System.Web.Razor/Generator/GeneratedCodeMapping.cs99
-rw-r--r--src/System.Web.Razor/Generator/HelperCodeGenerator.cs112
-rw-r--r--src/System.Web.Razor/Generator/HybridCodeGenerator.cs19
-rw-r--r--src/System.Web.Razor/Generator/IBlockCodeGenerator.cs10
-rw-r--r--src/System.Web.Razor/Generator/ISpanCodeGenerator.cs9
-rw-r--r--src/System.Web.Razor/Generator/LiteralAttributeCodeGenerator.cs111
-rw-r--r--src/System.Web.Razor/Generator/MarkupCodeGenerator.cs61
-rw-r--r--src/System.Web.Razor/Generator/RazorCodeGenerator.cs101
-rw-r--r--src/System.Web.Razor/Generator/RazorCommentCodeGenerator.cs18
-rw-r--r--src/System.Web.Razor/Generator/RazorDirectiveAttributeCodeGenerator.cs52
-rw-r--r--src/System.Web.Razor/Generator/ResolveUrlCodeGenerator.cs90
-rw-r--r--src/System.Web.Razor/Generator/SectionCodeGenerator.cs59
-rw-r--r--src/System.Web.Razor/Generator/SetBaseTypeCodeGenerator.cs62
-rw-r--r--src/System.Web.Razor/Generator/SetLayoutCodeGenerator.cs42
-rw-r--r--src/System.Web.Razor/Generator/SetVBOptionCodeGenerator.cs41
-rw-r--r--src/System.Web.Razor/Generator/SpanCodeGenerator.cs37
-rw-r--r--src/System.Web.Razor/Generator/StatementCodeGenerator.cs52
-rw-r--r--src/System.Web.Razor/Generator/TemplateBlockCodeGenerator.cs42
-rw-r--r--src/System.Web.Razor/Generator/TypeMemberCodeGenerator.cs38
-rw-r--r--src/System.Web.Razor/Generator/VBCodeWriter.cs198
-rw-r--r--src/System.Web.Razor/Generator/VBRazorCodeGenerator.cs15
-rw-r--r--src/System.Web.Razor/GeneratorResults.cs53
-rw-r--r--src/System.Web.Razor/GlobalSuppressions.cs20
-rw-r--r--src/System.Web.Razor/Parser/BalancingModes.cs12
-rw-r--r--src/System.Web.Razor/Parser/CSharpCodeParser.Directives.cs502
-rw-r--r--src/System.Web.Razor/Parser/CSharpCodeParser.Statements.cs672
-rw-r--r--src/System.Web.Razor/Parser/CSharpCodeParser.cs568
-rw-r--r--src/System.Web.Razor/Parser/CSharpLanguageCharacteristics.cs187
-rw-r--r--src/System.Web.Razor/Parser/CallbackVisitor.cs93
-rw-r--r--src/System.Web.Razor/Parser/ConditionalAttributeCollapser.cs49
-rw-r--r--src/System.Web.Razor/Parser/HtmlLanguageCharacteristics.cs129
-rw-r--r--src/System.Web.Razor/Parser/HtmlMarkupParser.Block.cs770
-rw-r--r--src/System.Web.Razor/Parser/HtmlMarkupParser.Document.cs36
-rw-r--r--src/System.Web.Razor/Parser/HtmlMarkupParser.Section.cs202
-rw-r--r--src/System.Web.Razor/Parser/HtmlMarkupParser.cs184
-rw-r--r--src/System.Web.Razor/Parser/ISyntaxTreeRewriter.cs9
-rw-r--r--src/System.Web.Razor/Parser/LanguageCharacteristics.cs110
-rw-r--r--src/System.Web.Razor/Parser/MarkupCollapser.cs35
-rw-r--r--src/System.Web.Razor/Parser/MarkupRewriter.cs102
-rw-r--r--src/System.Web.Razor/Parser/ParserBase.cs48
-rw-r--r--src/System.Web.Razor/Parser/ParserContext.cs309
-rw-r--r--src/System.Web.Razor/Parser/ParserHelpers.cs143
-rw-r--r--src/System.Web.Razor/Parser/ParserVisitor.cs53
-rw-r--r--src/System.Web.Razor/Parser/ParserVisitorExtensions.cs26
-rw-r--r--src/System.Web.Razor/Parser/RazorParser.cs145
-rw-r--r--src/System.Web.Razor/Parser/SyntaxConstants.cs50
-rw-r--r--src/System.Web.Razor/Parser/SyntaxTree/AcceptedCharacters.cs21
-rw-r--r--src/System.Web.Razor/Parser/SyntaxTree/Block.cs198
-rw-r--r--src/System.Web.Razor/Parser/SyntaxTree/BlockBuilder.cs42
-rw-r--r--src/System.Web.Razor/Parser/SyntaxTree/BlockType.cs20
-rw-r--r--src/System.Web.Razor/Parser/SyntaxTree/EquivalenceComparer.cs17
-rw-r--r--src/System.Web.Razor/Parser/SyntaxTree/RazorError.cs56
-rw-r--r--src/System.Web.Razor/Parser/SyntaxTree/Span.cs150
-rw-r--r--src/System.Web.Razor/Parser/SyntaxTree/SpanBuilder.cs82
-rw-r--r--src/System.Web.Razor/Parser/SyntaxTree/SpanKind.cs11
-rw-r--r--src/System.Web.Razor/Parser/SyntaxTree/SyntaxTreeNode.cs39
-rw-r--r--src/System.Web.Razor/Parser/TextReaderExtensions.cs106
-rw-r--r--src/System.Web.Razor/Parser/TokenizerBackedParser.Helpers.cs547
-rw-r--r--src/System.Web.Razor/Parser/TokenizerBackedParser.cs85
-rw-r--r--src/System.Web.Razor/Parser/VBCodeParser.Directives.cs408
-rw-r--r--src/System.Web.Razor/Parser/VBCodeParser.Statements.cs312
-rw-r--r--src/System.Web.Razor/Parser/VBCodeParser.cs598
-rw-r--r--src/System.Web.Razor/Parser/VBLanguageCharacteristics.cs88
-rw-r--r--src/System.Web.Razor/Parser/WhitespaceRewriter.cs74
-rw-r--r--src/System.Web.Razor/ParserResults.cs38
-rw-r--r--src/System.Web.Razor/PartialParseResult.cs57
-rw-r--r--src/System.Web.Razor/Properties/AssemblyInfo.cs12
-rw-r--r--src/System.Web.Razor/RazorCodeLanguage.cs58
-rw-r--r--src/System.Web.Razor/RazorDebugHelpers.cs197
-rw-r--r--src/System.Web.Razor/RazorDirectiveAttribute.cs47
-rw-r--r--src/System.Web.Razor/RazorEditorParser.cs366
-rw-r--r--src/System.Web.Razor/RazorEngineHost.cs213
-rw-r--r--src/System.Web.Razor/RazorTemplateEngine.cs197
-rw-r--r--src/System.Web.Razor/Resources/RazorResources.Designer.cs868
-rw-r--r--src/System.Web.Razor/Resources/RazorResources.resx412
-rw-r--r--src/System.Web.Razor/StateMachine.cs103
-rw-r--r--src/System.Web.Razor/System.Web.Razor.csproj236
-rw-r--r--src/System.Web.Razor/Text/BufferingTextReader.cs198
-rw-r--r--src/System.Web.Razor/Text/ITextBuffer.cs16
-rw-r--r--src/System.Web.Razor/Text/LineTrackingStringBuffer.cs159
-rw-r--r--src/System.Web.Razor/Text/LocationTagged.cs90
-rw-r--r--src/System.Web.Razor/Text/LookaheadTextReader.cs11
-rw-r--r--src/System.Web.Razor/Text/LookaheadToken.cs32
-rw-r--r--src/System.Web.Razor/Text/SeekableTextReader.cs97
-rw-r--r--src/System.Web.Razor/Text/SourceLocation.cs136
-rw-r--r--src/System.Web.Razor/Text/SourceLocationTracker.cs85
-rw-r--r--src/System.Web.Razor/Text/TextBufferReader.cs103
-rw-r--r--src/System.Web.Razor/Text/TextChange.cs208
-rw-r--r--src/System.Web.Razor/Text/TextChangeType.cs8
-rw-r--r--src/System.Web.Razor/Text/TextDocumentReader.cs40
-rw-r--r--src/System.Web.Razor/Text/TextExtensions.cs44
-rw-r--r--src/System.Web.Razor/Tokenizer/CSharpHelpers.cs41
-rw-r--r--src/System.Web.Razor/Tokenizer/CSharpKeywordDetector.cs99
-rw-r--r--src/System.Web.Razor/Tokenizer/CSharpTokenizer.cs425
-rw-r--r--src/System.Web.Razor/Tokenizer/HtmlTokenizer.cs197
-rw-r--r--src/System.Web.Razor/Tokenizer/ITokenizer.cs9
-rw-r--r--src/System.Web.Razor/Tokenizer/Symbols/CSharpKeyword.cs83
-rw-r--r--src/System.Web.Razor/Tokenizer/Symbols/CSharpSymbol.cs45
-rw-r--r--src/System.Web.Razor/Tokenizer/Symbols/CSharpSymbolType.cs72
-rw-r--r--src/System.Web.Razor/Tokenizer/Symbols/HtmlSymbol.cs31
-rw-r--r--src/System.Web.Razor/Tokenizer/Symbols/HtmlSymbolType.cs26
-rw-r--r--src/System.Web.Razor/Tokenizer/Symbols/ISymbol.cs13
-rw-r--r--src/System.Web.Razor/Tokenizer/Symbols/KnownSymbolType.cs15
-rw-r--r--src/System.Web.Razor/Tokenizer/Symbols/SymbolBase.cs69
-rw-r--r--src/System.Web.Razor/Tokenizer/Symbols/SymbolExtensions.cs39
-rw-r--r--src/System.Web.Razor/Tokenizer/Symbols/SymbolTypeSuppressions.cs24
-rw-r--r--src/System.Web.Razor/Tokenizer/Symbols/VBKeyword.cs166
-rw-r--r--src/System.Web.Razor/Tokenizer/Symbols/VBSymbol.cs112
-rw-r--r--src/System.Web.Razor/Tokenizer/Symbols/VBSymbolType.cs46
-rw-r--r--src/System.Web.Razor/Tokenizer/Tokenizer.cs352
-rw-r--r--src/System.Web.Razor/Tokenizer/TokenizerView.cs54
-rw-r--r--src/System.Web.Razor/Tokenizer/VBHelpers.cs20
-rw-r--r--src/System.Web.Razor/Tokenizer/VBKeywordDetector.cs174
-rw-r--r--src/System.Web.Razor/Tokenizer/VBTokenizer.cs381
-rw-r--r--src/System.Web.Razor/Tokenizer/XmlHelpers.cs47
-rw-r--r--src/System.Web.Razor/Utils/CharUtils.cs18
-rw-r--r--src/System.Web.Razor/Utils/DisposableAction.cs31
-rw-r--r--src/System.Web.Razor/Utils/EnumUtil.cs21
-rw-r--r--src/System.Web.Razor/Utils/EnumeratorExtensions.cs13
-rw-r--r--src/System.Web.Razor/VBRazorCodeLanguage.cs46
-rw-r--r--src/System.Web.WebPages.Administration/Default.cshtml39
-rw-r--r--src/System.Web.WebPages.Administration/Default.generated.cs149
-rw-r--r--src/System.Web.WebPages.Administration/EnableInstructions.cshtml21
-rw-r--r--src/System.Web.WebPages.Administration/EnableInstructions.generated.cs86
-rw-r--r--src/System.Web.WebPages.Administration/Framework/AdminSecurity.cs258
-rw-r--r--src/System.Web.WebPages.Administration/Framework/PreApplicationStartCode.cs32
-rw-r--r--src/System.Web.WebPages.Administration/Framework/SiteAdmin.cs230
-rw-r--r--src/System.Web.WebPages.Administration/Framework/packages/IPackagesSourceFile.cs13
-rw-r--r--src/System.Web.WebPages.Administration/Framework/packages/IWebProjectManager.cs74
-rw-r--r--src/System.Web.WebPages.Administration/Framework/packages/PackageExtensions.cs13
-rw-r--r--src/System.Web.WebPages.Administration/Framework/packages/PackageManagerModule.cs165
-rw-r--r--src/System.Web.WebPages.Administration/Framework/packages/PackageSourceFile.cs103
-rw-r--r--src/System.Web.WebPages.Administration/Framework/packages/PageUtils.cs80
-rw-r--r--src/System.Web.WebPages.Administration/Framework/packages/RemoteAssembly.cs198
-rw-r--r--src/System.Web.WebPages.Administration/Framework/packages/WebPackageSource.cs25
-rw-r--r--src/System.Web.WebPages.Administration/Framework/packages/WebProjectManager.cs266
-rw-r--r--src/System.Web.WebPages.Administration/Framework/packages/WebProjectSystem.cs225
-rw-r--r--src/System.Web.WebPages.Administration/Login.cshtml65
-rw-r--r--src/System.Web.WebPages.Administration/Login.generated.cs154
-rw-r--r--src/System.Web.WebPages.Administration/Logout.cshtml9
-rw-r--r--src/System.Web.WebPages.Administration/Logout.generated.cs67
-rw-r--r--src/System.Web.WebPages.Administration/Properties/AssemblyInfo.cs13
-rw-r--r--src/System.Web.WebPages.Administration/Register.cshtml66
-rw-r--r--src/System.Web.WebPages.Administration/Register.generated.cs151
-rw-r--r--src/System.Web.WebPages.Administration/Resources/AdminResources.Designer.cs297
-rw-r--r--src/System.Web.WebPages.Administration/Resources/AdminResources.resx201
-rw-r--r--src/System.Web.WebPages.Administration/Resources/PackageManagerResources.Designer.cs513
-rw-r--r--src/System.Web.WebPages.Administration/Resources/PackageManagerResources.resx277
-rw-r--r--src/System.Web.WebPages.Administration/Site.css589
-rw-r--r--src/System.Web.WebPages.Administration/System.Web.WebPages.Administration.csproj329
-rw-r--r--src/System.Web.WebPages.Administration/_Layout.cshtml73
-rw-r--r--src/System.Web.WebPages.Administration/_Layout.generated.cs232
-rw-r--r--src/System.Web.WebPages.Administration/_pagestart.cshtml30
-rw-r--r--src/System.Web.WebPages.Administration/_pagestart.generated.cs91
-rw-r--r--src/System.Web.WebPages.Administration/images/aspLogo.gifbin0 -> 2465 bytes
-rw-r--r--src/System.Web.WebPages.Administration/images/error.pngbin0 -> 562 bytes
-rw-r--r--src/System.Web.WebPages.Administration/images/ok.pngbin0 -> 2122 bytes
-rw-r--r--src/System.Web.WebPages.Administration/images/package.pngbin0 -> 567 bytes
-rw-r--r--src/System.Web.WebPages.Administration/images/tabon.gifbin0 -> 1154 bytes
-rw-r--r--src/System.Web.WebPages.Administration/packages.config4
-rw-r--r--src/System.Web.WebPages.Administration/packages/Default.cshtml218
-rw-r--r--src/System.Web.WebPages.Administration/packages/Default.generated.cs420
-rw-r--r--src/System.Web.WebPages.Administration/packages/Install.cshtml91
-rw-r--r--src/System.Web.WebPages.Administration/packages/Install.generated.cs259
-rw-r--r--src/System.Web.WebPages.Administration/packages/PackageSources.cshtml111
-rw-r--r--src/System.Web.WebPages.Administration/packages/PackageSources.generated.cs248
-rw-r--r--src/System.Web.WebPages.Administration/packages/Site.css127
-rw-r--r--src/System.Web.WebPages.Administration/packages/SourceFileInstructions.cshtml7
-rw-r--r--src/System.Web.WebPages.Administration/packages/SourceFileInstructions.generated.cs67
-rw-r--r--src/System.Web.WebPages.Administration/packages/Uninstall.cshtml74
-rw-r--r--src/System.Web.WebPages.Administration/packages/Uninstall.generated.cs212
-rw-r--r--src/System.Web.WebPages.Administration/packages/Update.cshtml69
-rw-r--r--src/System.Web.WebPages.Administration/packages/Update.generated.cs189
-rw-r--r--src/System.Web.WebPages.Administration/packages/_Layout.cshtml22
-rw-r--r--src/System.Web.WebPages.Administration/packages/_Layout.generated.cs123
-rw-r--r--src/System.Web.WebPages.Administration/packages/_Package.cshtml16
-rw-r--r--src/System.Web.WebPages.Administration/packages/_Package.generated.cs88
-rw-r--r--src/System.Web.WebPages.Administration/packages/_PackageDetails.cshtml23
-rw-r--r--src/System.Web.WebPages.Administration/packages/_PackageDetails.generated.cs110
-rw-r--r--src/System.Web.WebPages.Administration/packages/_pagestart.cshtml15
-rw-r--r--src/System.Web.WebPages.Administration/packages/_pagestart.generated.cs73
-rw-r--r--src/System.Web.WebPages.Administration/packages/images/error.pngbin0 -> 562 bytes
-rw-r--r--src/System.Web.WebPages.Administration/packages/images/package.pngbin0 -> 1297 bytes
-rw-r--r--src/System.Web.WebPages.Administration/packages/scripts/Default.js30
-rw-r--r--src/System.Web.WebPages.Administration/packages/scripts/PackageAction.js5
-rw-r--r--src/System.Web.WebPages.Deployment/AppDomainHelper.cs61
-rw-r--r--src/System.Web.WebPages.Deployment/AssemblyUtils.cs182
-rw-r--r--src/System.Web.WebPages.Deployment/BuildManagerWrapper.cs26
-rw-r--r--src/System.Web.WebPages.Deployment/Common/IFileSystem.cs16
-rw-r--r--src/System.Web.WebPages.Deployment/Common/PhysicalFileSystem.cs38
-rw-r--r--src/System.Web.WebPages.Deployment/GlobalSuppressions.cs13
-rw-r--r--src/System.Web.WebPages.Deployment/IBuildManager.cs11
-rw-r--r--src/System.Web.WebPages.Deployment/PreApplicationStartCode.cs272
-rw-r--r--src/System.Web.WebPages.Deployment/Properties/AssemblyInfo.cs13
-rw-r--r--src/System.Web.WebPages.Deployment/Resources/ConfigurationResources.Designer.cs108
-rw-r--r--src/System.Web.WebPages.Deployment/Resources/ConfigurationResources.resx135
-rw-r--r--src/System.Web.WebPages.Deployment/System.Web.WebPages.Deployment.csproj111
-rw-r--r--src/System.Web.WebPages.Deployment/WebPagesDeployment.cs384
-rw-r--r--src/System.Web.WebPages.Deployment/packages.config4
-rw-r--r--src/System.Web.WebPages.Razor/AssemblyBuilderWrapper.cs30
-rw-r--r--src/System.Web.WebPages.Razor/CompilingPathEventArgs.cs14
-rw-r--r--src/System.Web.WebPages.Razor/Configuration/HostSection.cs29
-rw-r--r--src/System.Web.WebPages.Razor/Configuration/RazorPagesSection.cs52
-rw-r--r--src/System.Web.WebPages.Razor/Configuration/RazorWebSectionGroup.cs38
-rw-r--r--src/System.Web.WebPages.Razor/GlobalSuppressions.cs9
-rw-r--r--src/System.Web.WebPages.Razor/HostingEnvironmentWrapper.cs12
-rw-r--r--src/System.Web.WebPages.Razor/IAssemblyBuilder.cs11
-rw-r--r--src/System.Web.WebPages.Razor/IHostingEnvironment.cs7
-rw-r--r--src/System.Web.WebPages.Razor/PreApplicationStartCode.cs34
-rw-r--r--src/System.Web.WebPages.Razor/Properties/AssemblyInfo.cs13
-rw-r--r--src/System.Web.WebPages.Razor/RazorBuildProvider.cs252
-rw-r--r--src/System.Web.WebPages.Razor/Resources/RazorWebResources.Designer.cs81
-rw-r--r--src/System.Web.WebPages.Razor/Resources/RazorWebResources.resx126
-rw-r--r--src/System.Web.WebPages.Razor/System.Web.WebPages.Razor.csproj113
-rw-r--r--src/System.Web.WebPages.Razor/WebCodeRazorHost.cs99
-rw-r--r--src/System.Web.WebPages.Razor/WebPageRazorHost.cs351
-rw-r--r--src/System.Web.WebPages.Razor/WebRazorHostFactory.cs175
-rw-r--r--src/System.Web.WebPages/ApplicationPart.cs226
-rw-r--r--src/System.Web.WebPages/ApplicationParts/ApplicationPartRegistry.cs142
-rw-r--r--src/System.Web.WebPages/ApplicationParts/DictionaryBasedVirtualPathFactory.cs29
-rw-r--r--src/System.Web.WebPages/ApplicationParts/IResourceAssembly.cs14
-rw-r--r--src/System.Web.WebPages/ApplicationParts/LazyAction.cs29
-rw-r--r--src/System.Web.WebPages/ApplicationParts/ResourceAssembly.cs65
-rw-r--r--src/System.Web.WebPages/ApplicationParts/ResourceHandler.cs67
-rw-r--r--src/System.Web.WebPages/ApplicationParts/ResourceRouteHandler.cs39
-rw-r--r--src/System.Web.WebPages/ApplicationStartPage.cs173
-rw-r--r--src/System.Web.WebPages/AttributeValue.cs43
-rw-r--r--src/System.Web.WebPages/BrowserHelpers.cs168
-rw-r--r--src/System.Web.WebPages/BrowserOverride.cs11
-rw-r--r--src/System.Web.WebPages/BrowserOverrideStore.cs12
-rw-r--r--src/System.Web.WebPages/BrowserOverrideStores.cs23
-rw-r--r--src/System.Web.WebPages/BuildManagerWrapper.cs202
-rw-r--r--src/System.Web.WebPages/Common/DisposableAction.cs39
-rw-r--r--src/System.Web.WebPages/CookieBrowserOverrideStore.cs95
-rw-r--r--src/System.Web.WebPages/DefaultDisplayMode.cs71
-rw-r--r--src/System.Web.WebPages/DisplayInfo.cs35
-rw-r--r--src/System.Web.WebPages/DisplayModeProvider.cs92
-rw-r--r--src/System.Web.WebPages/DynamicHttpApplicationState.cs78
-rw-r--r--src/System.Web.WebPages/DynamicPageDataDictionary.cs149
-rw-r--r--src/System.Web.WebPages/FileExistenceCache.cs77
-rw-r--r--src/System.Web.WebPages/GlobalSuppressions.cs15
-rw-r--r--src/System.Web.WebPages/HelperPage.cs266
-rw-r--r--src/System.Web.WebPages/HelperResult.cs38
-rw-r--r--src/System.Web.WebPages/Helpers/AntiForgery.cs48
-rw-r--r--src/System.Web.WebPages/Helpers/AntiForgeryData.cs117
-rw-r--r--src/System.Web.WebPages/Helpers/AntiForgeryDataSerializer.cs109
-rw-r--r--src/System.Web.WebPages/Helpers/AntiForgeryWorker.cs117
-rw-r--r--src/System.Web.WebPages/Helpers/UnvalidatedRequestValues.cs62
-rw-r--r--src/System.Web.WebPages/Helpers/Validation.cs39
-rw-r--r--src/System.Web.WebPages/Html/HtmlHelper.Checkbox.cs84
-rw-r--r--src/System.Web.WebPages/Html/HtmlHelper.Input.cs176
-rw-r--r--src/System.Web.WebPages/Html/HtmlHelper.Internal.cs117
-rw-r--r--src/System.Web.WebPages/Html/HtmlHelper.Label.cs49
-rw-r--r--src/System.Web.WebPages/Html/HtmlHelper.Radio.cs93
-rw-r--r--src/System.Web.WebPages/Html/HtmlHelper.Select.cs288
-rw-r--r--src/System.Web.WebPages/Html/HtmlHelper.TextArea.cs119
-rw-r--r--src/System.Web.WebPages/Html/HtmlHelper.Validation.cs182
-rw-r--r--src/System.Web.WebPages/Html/HtmlHelper.cs200
-rw-r--r--src/System.Web.WebPages/Html/ModelState.cs16
-rw-r--r--src/System.Web.WebPages/Html/ModelStateDictionary.cs183
-rw-r--r--src/System.Web.WebPages/Html/SelectListItem.cs22
-rw-r--r--src/System.Web.WebPages/HttpContextExtensions.cs29
-rw-r--r--src/System.Web.WebPages/IDisplayMode.cs16
-rw-r--r--src/System.Web.WebPages/ITemplateFile.cs12
-rw-r--r--src/System.Web.WebPages/IVirtualPathFactory.cs10
-rw-r--r--src/System.Web.WebPages/IWebPageRequestExecutor.cs9
-rw-r--r--src/System.Web.WebPages/Instrumentation/HttpContextAdapter.Availability.cs14
-rw-r--r--src/System.Web.WebPages/Instrumentation/HttpContextAdapter.generated.cs24
-rw-r--r--src/System.Web.WebPages/Instrumentation/HttpContextAdapter.tt21
-rw-r--r--src/System.Web.WebPages/Instrumentation/InstrumentationService.cs81
-rw-r--r--src/System.Web.WebPages/Instrumentation/PageExecutionContextAdapter.generated.cs68
-rw-r--r--src/System.Web.WebPages/Instrumentation/PageExecutionContextAdapter.tt9
-rw-r--r--src/System.Web.WebPages/Instrumentation/PageExecutionListenerAdapter.generated.cs29
-rw-r--r--src/System.Web.WebPages/Instrumentation/PageExecutionListenerAdapter.tt14
-rw-r--r--src/System.Web.WebPages/Instrumentation/PageInstrumentationServiceAdapter.cs95
-rw-r--r--src/System.Web.WebPages/Instrumentation/PositionTagged.cs65
-rw-r--r--src/System.Web.WebPages/Mvc/HttpAntiForgeryException.cs29
-rw-r--r--src/System.Web.WebPages/Mvc/ModelClientValidationEqualToRule.cs15
-rw-r--r--src/System.Web.WebPages/Mvc/ModelClientValidationRangeRule.cs16
-rw-r--r--src/System.Web.WebPages/Mvc/ModelClientValidationRegexRule.cs15
-rw-r--r--src/System.Web.WebPages/Mvc/ModelClientValidationRemoteRule.cs24
-rw-r--r--src/System.Web.WebPages/Mvc/ModelClientValidationRequiredRule.cs14
-rw-r--r--src/System.Web.WebPages/Mvc/ModelClientValidationRule.cs25
-rw-r--r--src/System.Web.WebPages/Mvc/ModelClientValidationStringLengthRule.cs24
-rw-r--r--src/System.Web.WebPages/Mvc/TagBuilder.cs259
-rw-r--r--src/System.Web.WebPages/Mvc/TagRenderMode.cs13
-rw-r--r--src/System.Web.WebPages/Mvc/UnobtrusiveValidationAttributesGenerator.cs96
-rw-r--r--src/System.Web.WebPages/PageDataDictionary.cs332
-rw-r--r--src/System.Web.WebPages/PageVirtualPathAttribute.cs16
-rw-r--r--src/System.Web.WebPages/PreApplicationStartCode.cs43
-rw-r--r--src/System.Web.WebPages/Properties/AssemblyInfo.cs19
-rw-r--r--src/System.Web.WebPages/ReflectionDynamicObject.cs82
-rw-r--r--src/System.Web.WebPages/RequestBrowserOverrideStore.cs18
-rw-r--r--src/System.Web.WebPages/RequestExtensions.cs16
-rw-r--r--src/System.Web.WebPages/RequestResourceTracker.cs68
-rw-r--r--src/System.Web.WebPages/Resources/WebPageResources.Designer.cs441
-rw-r--r--src/System.Web.WebPages/Resources/WebPageResources.resx246
-rw-r--r--src/System.Web.WebPages/ResponseExtensions.cs87
-rw-r--r--src/System.Web.WebPages/Scope/ApplicationScopeStorageDictionary.cs24
-rw-r--r--src/System.Web.WebPages/Scope/AspNetRequestScopeStorageProvider.cs108
-rw-r--r--src/System.Web.WebPages/Scope/IScopeStorageProvider.cs13
-rw-r--r--src/System.Web.WebPages/Scope/ScopeStorage.cs38
-rw-r--r--src/System.Web.WebPages/Scope/ScopeStorageComparer.cs60
-rw-r--r--src/System.Web.WebPages/Scope/ScopeStorageDictionary.cs165
-rw-r--r--src/System.Web.WebPages/Scope/StaticScopeStorageProvider.cs26
-rw-r--r--src/System.Web.WebPages/Scope/WebConfigScopeStorageDictionary.cs115
-rw-r--r--src/System.Web.WebPages/SectionWriter.cs4
-rw-r--r--src/System.Web.WebPages/SecurityUtil.cs80
-rw-r--r--src/System.Web.WebPages/StartPage.cs172
-rw-r--r--src/System.Web.WebPages/StringExtensions.cs161
-rw-r--r--src/System.Web.WebPages/System.Web.WebPages.csproj269
-rw-r--r--src/System.Web.WebPages/TemplateFileInfo.cs21
-rw-r--r--src/System.Web.WebPages/TemplateStack.cs59
-rw-r--r--src/System.Web.WebPages/UrlDataList.cs99
-rw-r--r--src/System.Web.WebPages/Utils/BuildManagerExceptionUtil.cs51
-rw-r--r--src/System.Web.WebPages/Utils/CultureUtil.cs73
-rw-r--r--src/System.Web.WebPages/Utils/PathUtil.cs73
-rw-r--r--src/System.Web.WebPages/Utils/SessionStateUtil.cs81
-rw-r--r--src/System.Web.WebPages/Utils/TypeHelper.cs45
-rw-r--r--src/System.Web.WebPages/Utils/UrlUtil.cs69
-rw-r--r--src/System.Web.WebPages/Validation/CompareValidator.cs30
-rw-r--r--src/System.Web.WebPages/Validation/DataTypeValidator.cs45
-rw-r--r--src/System.Web.WebPages/Validation/IValidator.cs11
-rw-r--r--src/System.Web.WebPages/Validation/RequestFieldValidatorBase.cs72
-rw-r--r--src/System.Web.WebPages/Validation/ValidationAttributeAdapter.cs39
-rw-r--r--src/System.Web.WebPages/Validation/ValidationHelper.cs293
-rw-r--r--src/System.Web.WebPages/Validation/Validator.cs125
-rw-r--r--src/System.Web.WebPages/VirtualPathFactoryExtensions.cs21
-rw-r--r--src/System.Web.WebPages/VirtualPathFactoryManager.cs59
-rw-r--r--src/System.Web.WebPages/WebPage.cs98
-rw-r--r--src/System.Web.WebPages/WebPageBase.cs491
-rw-r--r--src/System.Web.WebPages/WebPageContext.cs155
-rw-r--r--src/System.Web.WebPages/WebPageExecutingBase.cs288
-rw-r--r--src/System.Web.WebPages/WebPageHttpHandler.cs190
-rw-r--r--src/System.Web.WebPages/WebPageHttpModule.cs115
-rw-r--r--src/System.Web.WebPages/WebPageMatch.cs15
-rw-r--r--src/System.Web.WebPages/WebPageRenderingBase.cs207
-rw-r--r--src/System.Web.WebPages/WebPageRoute.cs222
-rw-r--r--src/System.Web.WebPages/packages.config4
-rw-r--r--src/TransparentCommonAssemblyInfo.cs3
-rw-r--r--src/VirtualPathUtilityWrapper.cs17
-rw-r--r--src/WebHelpers.ruleset7
-rw-r--r--src/WebMatrix.Data/ConfigurationManagerWrapper.cs75
-rw-r--r--src/WebMatrix.Data/ConnectionConfiguration.cs25
-rw-r--r--src/WebMatrix.Data/ConnectionEventArgs.cs15
-rw-r--r--src/WebMatrix.Data/Database.cs302
-rw-r--r--src/WebMatrix.Data/DbProviderFactoryWrapper.cs34
-rw-r--r--src/WebMatrix.Data/DynamicRecord.cs197
-rw-r--r--src/WebMatrix.Data/GlobalSuppressions.cs13
-rw-r--r--src/WebMatrix.Data/IConfigurationManager.cs10
-rw-r--r--src/WebMatrix.Data/IConnectionConfiguration.cs8
-rw-r--r--src/WebMatrix.Data/IDbFileHandler.cs7
-rw-r--r--src/WebMatrix.Data/IDbProviderFactory.cs9
-rw-r--r--src/WebMatrix.Data/Properties/AssemblyInfo.cs11
-rw-r--r--src/WebMatrix.Data/Resources/DataResources.Designer.cs99
-rw-r--r--src/WebMatrix.Data/Resources/DataResources.resx132
-rw-r--r--src/WebMatrix.Data/SqlCeDbFileHandler.cs34
-rw-r--r--src/WebMatrix.Data/SqlServerDbFileHandler.cs32
-rw-r--r--src/WebMatrix.Data/WebMatrix.Data.csproj108
-rw-r--r--src/WebMatrix.WebData/ConfigUtil.cs54
-rw-r--r--src/WebMatrix.WebData/DatabaseConnectionInfo.cs53
-rw-r--r--src/WebMatrix.WebData/DatabaseWrapper.cs40
-rw-r--r--src/WebMatrix.WebData/ExtendedMembershipProvider.cs93
-rw-r--r--src/WebMatrix.WebData/FormsAuthenticationSettings.cs19
-rw-r--r--src/WebMatrix.WebData/GlobalSuppressions.cs15
-rw-r--r--src/WebMatrix.WebData/IDatabase.cs16
-rw-r--r--src/WebMatrix.WebData/OAuthAccountData.cs47
-rw-r--r--src/WebMatrix.WebData/PreApplicationStartCode.cs81
-rw-r--r--src/WebMatrix.WebData/Properties/AssemblyInfo.cs14
-rw-r--r--src/WebMatrix.WebData/Resources/WebDataResources.Designer.cs189
-rw-r--r--src/WebMatrix.WebData/Resources/WebDataResources.resx162
-rw-r--r--src/WebMatrix.WebData/SimpleMembershipProvider.cs1135
-rw-r--r--src/WebMatrix.WebData/SimpleRoleProvider.cs419
-rw-r--r--src/WebMatrix.WebData/WebMatrix.WebData.csproj130
-rw-r--r--src/WebMatrix.WebData/WebSecurity.cs423
-rw-r--r--test/Microsoft.TestCommon/AppDomainUtils.cs71
-rw-r--r--test/Microsoft.TestCommon/AssertEx.cs31
-rw-r--r--test/Microsoft.TestCommon/CultureReplacer.cs34
-rw-r--r--test/Microsoft.TestCommon/DefaultTimeoutFactAttribute.cs17
-rw-r--r--test/Microsoft.TestCommon/DefaultTimeoutTheoryAttribute.cs14
-rw-r--r--test/Microsoft.TestCommon/DictionaryEqualityComparer.cs47
-rw-r--r--test/Microsoft.TestCommon/ExceptionAssertions.cs515
-rw-r--r--test/Microsoft.TestCommon/MemberHelper.cs381
-rw-r--r--test/Microsoft.TestCommon/Microsoft.TestCommon.csproj116
-rw-r--r--test/Microsoft.TestCommon/Microsoft/TestCommon/DataSets/CommonUnitTestDataSets.cs38
-rw-r--r--test/Microsoft.TestCommon/Microsoft/TestCommon/DataSets/RefTypeTestData.cs67
-rw-r--r--test/Microsoft.TestCommon/Microsoft/TestCommon/DataSets/TestData.cs444
-rw-r--r--test/Microsoft.TestCommon/Microsoft/TestCommon/DataSets/TestDataVariations.cs95
-rw-r--r--test/Microsoft.TestCommon/Microsoft/TestCommon/DataSets/ValueTypeTestData.cs31
-rw-r--r--test/Microsoft.TestCommon/Microsoft/TestCommon/GenericTypeAssert.cs489
-rw-r--r--test/Microsoft.TestCommon/Microsoft/TestCommon/HttpAssert.cs252
-rw-r--r--test/Microsoft.TestCommon/Microsoft/TestCommon/MediaTypeAssert.cs53
-rw-r--r--test/Microsoft.TestCommon/Microsoft/TestCommon/MediaTypeHeaderValueComparer.cs88
-rw-r--r--test/Microsoft.TestCommon/Microsoft/TestCommon/ParsedMediaTypeHeaderValue.cs109
-rw-r--r--test/Microsoft.TestCommon/Microsoft/TestCommon/RegexReplacement.cs38
-rw-r--r--test/Microsoft.TestCommon/Microsoft/TestCommon/RuntimeEnvironment.cs31
-rw-r--r--test/Microsoft.TestCommon/Microsoft/TestCommon/SerializerAssert.cs150
-rw-r--r--test/Microsoft.TestCommon/Microsoft/TestCommon/StreamAssert.cs94
-rw-r--r--test/Microsoft.TestCommon/Microsoft/TestCommon/TaskAssert.cs99
-rw-r--r--test/Microsoft.TestCommon/Microsoft/TestCommon/TestDataSetAttribute.cs193
-rw-r--r--test/Microsoft.TestCommon/Microsoft/TestCommon/TimeoutConstant.cs20
-rw-r--r--test/Microsoft.TestCommon/Microsoft/TestCommon/TypeAssert.cs162
-rw-r--r--test/Microsoft.TestCommon/Microsoft/TestCommon/Types/FlagsEnum.cs12
-rw-r--r--test/Microsoft.TestCommon/Microsoft/TestCommon/Types/INameAndIdContainer.cs12
-rw-r--r--test/Microsoft.TestCommon/Microsoft/TestCommon/Types/ISerializableType.cs73
-rw-r--r--test/Microsoft.TestCommon/Microsoft/TestCommon/Types/LongEnum.cs9
-rw-r--r--test/Microsoft.TestCommon/Microsoft/TestCommon/Types/SimpleEnum.cs9
-rw-r--r--test/Microsoft.TestCommon/Microsoft/TestCommon/XmlAssert.cs77
-rw-r--r--test/Microsoft.TestCommon/PreAppStartTestHelper.cs25
-rw-r--r--test/Microsoft.TestCommon/PreserveSyncContextAttribute.cs24
-rw-r--r--test/Microsoft.TestCommon/Properties/AssemblyInfo.cs35
-rw-r--r--test/Microsoft.TestCommon/ReflectionAssert.cs115
-rw-r--r--test/Microsoft.TestCommon/TaskExtensions.cs25
-rw-r--r--test/Microsoft.TestCommon/TestFile.cs73
-rw-r--r--test/Microsoft.TestCommon/TestHelper.cs29
-rw-r--r--test/Microsoft.TestCommon/TheoryDataSet.cs86
-rw-r--r--test/Microsoft.TestCommon/ThreadPoolSyncContext.cs31
-rw-r--r--test/Microsoft.TestCommon/WebUtils.cs87
-rw-r--r--test/Microsoft.TestCommon/packages.config6
-rw-r--r--test/Microsoft.Web.Helpers.Test/AnalyticsTest.cs133
-rw-r--r--test/Microsoft.Web.Helpers.Test/BingTest.cs252
-rw-r--r--test/Microsoft.Web.Helpers.Test/FacebookTest.cs270
-rw-r--r--test/Microsoft.Web.Helpers.Test/FileUploadTest.cs199
-rw-r--r--test/Microsoft.Web.Helpers.Test/GamerCardTest.cs59
-rw-r--r--test/Microsoft.Web.Helpers.Test/GravatarTest.cs142
-rw-r--r--test/Microsoft.Web.Helpers.Test/LinkShareTest.cs188
-rw-r--r--test/Microsoft.Web.Helpers.Test/MapsTest.cs55
-rw-r--r--test/Microsoft.Web.Helpers.Test/Microsoft.Web.Helpers.Test.csproj108
-rw-r--r--test/Microsoft.Web.Helpers.Test/PreAppStartCodeTest.cs31
-rw-r--r--test/Microsoft.Web.Helpers.Test/Properties/AssemblyInfo.cs34
-rw-r--r--test/Microsoft.Web.Helpers.Test/ReCaptchaTest.cs251
-rw-r--r--test/Microsoft.Web.Helpers.Test/ThemesTest.cs373
-rw-r--r--test/Microsoft.Web.Helpers.Test/TwitterTest.cs481
-rw-r--r--test/Microsoft.Web.Helpers.Test/UrlBuilderTest.cs413
-rw-r--r--test/Microsoft.Web.Helpers.Test/VideoTest.cs300
-rw-r--r--test/Microsoft.Web.Helpers.Test/packages.config6
-rw-r--r--test/Microsoft.Web.Http.Data.Test/ChangeSetTests.cs164
-rw-r--r--test/Microsoft.Web.Http.Data.Test/Controllers/CatalogController.cs71
-rw-r--r--test/Microsoft.Web.Http.Data.Test/Controllers/CitiesController.cs15
-rw-r--r--test/Microsoft.Web.Http.Data.Test/Controllers/NorthwindEFController.cs45
-rw-r--r--test/Microsoft.Web.Http.Data.Test/DataControllerDescriptionTest.cs192
-rw-r--r--test/Microsoft.Web.Http.Data.Test/DataControllerQueryTests.cs231
-rw-r--r--test/Microsoft.Web.Http.Data.Test/DataControllerSubmitTests.cs372
-rw-r--r--test/Microsoft.Web.Http.Data.Test/MetadataExtensionsTests.cs59
-rw-r--r--test/Microsoft.Web.Http.Data.Test/Microsoft.Web.Http.Data.Test.csproj143
-rw-r--r--test/Microsoft.Web.Http.Data.Test/Models/CatalogEntities.cs169
-rw-r--r--test/Microsoft.Web.Http.Data.Test/Models/Cities.cs302
-rw-r--r--test/Microsoft.Web.Http.Data.Test/Models/Northwind.Designer.cs3411
-rw-r--r--test/Microsoft.Web.Http.Data.Test/Models/Northwind.edmx933
-rw-r--r--test/Microsoft.Web.Http.Data.Test/Properties/AssemblyInfo.cs34
-rw-r--r--test/Microsoft.Web.Http.Data.Test/TestHelpers.cs114
-rw-r--r--test/Microsoft.Web.Http.Data.Test/packages.config9
-rw-r--r--test/Microsoft.Web.Mvc.Test/Controls/Test/DesignModeSite.cs34
-rw-r--r--test/Microsoft.Web.Mvc.Test/Controls/Test/DropDownListTest.cs128
-rw-r--r--test/Microsoft.Web.Mvc.Test/Controls/Test/MvcControlTest.cs80
-rw-r--r--test/Microsoft.Web.Mvc.Test/Controls/Test/MvcTestHelper.cs19
-rw-r--r--test/Microsoft.Web.Mvc.Test/Controls/Test/ViewDataContainer.cs10
-rw-r--r--test/Microsoft.Web.Mvc.Test/Microsoft.Web.Mvc.Test.csproj167
-rw-r--r--test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ArrayModelBinderProviderTest.cs100
-rw-r--r--test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ArrayModelBinderTest.cs48
-rw-r--r--test/Microsoft.Web.Mvc.Test/ModelBinding/Test/BinaryDataModelBinderProviderTest.cs159
-rw-r--r--test/Microsoft.Web.Mvc.Test/ModelBinding/Test/BindingBehaviorAttributeTest.cs51
-rw-r--r--test/Microsoft.Web.Mvc.Test/ModelBinding/Test/CollectionModelBinderProviderTest.cs76
-rw-r--r--test/Microsoft.Web.Mvc.Test/ModelBinding/Test/CollectionModelBinderTest.cs245
-rw-r--r--test/Microsoft.Web.Mvc.Test/ModelBinding/Test/CollectionModelBinderUtilTest.cs391
-rw-r--r--test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ComplexModelDtoModelBinderProviderTest.cs45
-rw-r--r--test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ComplexModelDtoModelBinderTest.cs106
-rw-r--r--test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ComplexModelDtoResultTest.cs38
-rw-r--r--test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ComplexModelDtoTest.cs50
-rw-r--r--test/Microsoft.Web.Mvc.Test/ModelBinding/Test/DictionaryModelBinderProviderTest.cs76
-rw-r--r--test/Microsoft.Web.Mvc.Test/ModelBinding/Test/DictionaryModelBinderTest.cs52
-rw-r--r--test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ExtensibleModelBinderAdapterTest.cs210
-rw-r--r--test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ExtensibleModelBindingContextTest.cs136
-rw-r--r--test/Microsoft.Web.Mvc.Test/ModelBinding/Test/GenericModelBinderProviderTest.cs245
-rw-r--r--test/Microsoft.Web.Mvc.Test/ModelBinding/Test/KeyValuePairModelBinderProviderTest.cs104
-rw-r--r--test/Microsoft.Web.Mvc.Test/ModelBinding/Test/KeyValuePairModelBinderTest.cs116
-rw-r--r--test/Microsoft.Web.Mvc.Test/ModelBinding/Test/KeyValuePairModelBinderUtilTest.cs108
-rw-r--r--test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ModelBinderConfigTest.cs166
-rw-r--r--test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ModelBinderProviderCollectionTest.cs528
-rw-r--r--test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ModelBinderProvidersTest.cs33
-rw-r--r--test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ModelBinderUtilTest.cs324
-rw-r--r--test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ModelValidationNodeTest.cs389
-rw-r--r--test/Microsoft.Web.Mvc.Test/ModelBinding/Test/MutableObjectModelBinderProviderTest.cs76
-rw-r--r--test/Microsoft.Web.Mvc.Test/ModelBinding/Test/MutableObjectModelBinderTest.cs769
-rw-r--r--test/Microsoft.Web.Mvc.Test/ModelBinding/Test/SimpleModelBinderProviderTest.cs152
-rw-r--r--test/Microsoft.Web.Mvc.Test/ModelBinding/Test/TypeConverterModelBinderProviderTest.cs69
-rw-r--r--test/Microsoft.Web.Mvc.Test/ModelBinding/Test/TypeConverterModelBinderTest.cs171
-rw-r--r--test/Microsoft.Web.Mvc.Test/ModelBinding/Test/TypeMatchModelBinderProviderTest.cs73
-rw-r--r--test/Microsoft.Web.Mvc.Test/ModelBinding/Test/TypeMatchModelBinderTest.cs113
-rw-r--r--test/Microsoft.Web.Mvc.Test/Properties/AssemblyInfo.cs38
-rw-r--r--test/Microsoft.Web.Mvc.Test/Test/AjaxOnlyAttributeTest.cs66
-rw-r--r--test/Microsoft.Web.Mvc.Test/Test/AreaHelpersTest.cs100
-rw-r--r--test/Microsoft.Web.Mvc.Test/Test/AsyncManagerExtensionsTest.cs189
-rw-r--r--test/Microsoft.Web.Mvc.Test/Test/ButtonTest.cs66
-rw-r--r--test/Microsoft.Web.Mvc.Test/Test/ContentTypeAttributeTest.cs43
-rw-r--r--test/Microsoft.Web.Mvc.Test/Test/ControllerExtensionsTest.cs67
-rw-r--r--test/Microsoft.Web.Mvc.Test/Test/CookieTempDataProviderTest.cs171
-rw-r--r--test/Microsoft.Web.Mvc.Test/Test/CookieValueProviderFactoryTest.cs38
-rw-r--r--test/Microsoft.Web.Mvc.Test/Test/CopyAsyncParametersAttributeTest.cs78
-rw-r--r--test/Microsoft.Web.Mvc.Test/Test/CreditCardAttributeTest.cs42
-rw-r--r--test/Microsoft.Web.Mvc.Test/Test/CssExtensionsTests.cs138
-rw-r--r--test/Microsoft.Web.Mvc.Test/Test/DeserializeAttributeTest.cs105
-rw-r--r--test/Microsoft.Web.Mvc.Test/Test/DynamicReflectionObjectTest.cs54
-rw-r--r--test/Microsoft.Web.Mvc.Test/Test/DynamicViewDataDictionaryTest.cs115
-rw-r--r--test/Microsoft.Web.Mvc.Test/Test/DynamicViewPageTest.cs53
-rw-r--r--test/Microsoft.Web.Mvc.Test/Test/ElementalValueProviderTest.cs54
-rw-r--r--test/Microsoft.Web.Mvc.Test/Test/EmailAddressAttribueTest.cs42
-rw-r--r--test/Microsoft.Web.Mvc.Test/Test/ExpressionHelperTest.cs302
-rw-r--r--test/Microsoft.Web.Mvc.Test/Test/FileExtensionsAttributeTest.cs53
-rw-r--r--test/Microsoft.Web.Mvc.Test/Test/FormExtensionsTest.cs98
-rw-r--r--test/Microsoft.Web.Mvc.Test/Test/ImageExtensionsTest.cs130
-rw-r--r--test/Microsoft.Web.Mvc.Test/Test/MailToExtensionsTest.cs132
-rw-r--r--test/Microsoft.Web.Mvc.Test/Test/ModelCopierTest.cs223
-rw-r--r--test/Microsoft.Web.Mvc.Test/Test/MvcSerializerTest.cs122
-rw-r--r--test/Microsoft.Web.Mvc.Test/Test/RadioExtensionsTest.cs199
-rw-r--r--test/Microsoft.Web.Mvc.Test/Test/ReaderWriterCacheTest.cs77
-rw-r--r--test/Microsoft.Web.Mvc.Test/Test/RenderActionTest.cs115
-rw-r--r--test/Microsoft.Web.Mvc.Test/Test/ScriptExtensionsTest.cs123
-rw-r--r--test/Microsoft.Web.Mvc.Test/Test/SerializationExtensionsTest.cs78
-rw-r--r--test/Microsoft.Web.Mvc.Test/Test/ServerVariablesValueProviderFactoryTest.cs35
-rw-r--r--test/Microsoft.Web.Mvc.Test/Test/SessionValueProviderFactoryTest.cs60
-rw-r--r--test/Microsoft.Web.Mvc.Test/Test/SkipBindingAttributeTest.cs22
-rw-r--r--test/Microsoft.Web.Mvc.Test/Test/SubmitButtonExtensionsTest.cs82
-rw-r--r--test/Microsoft.Web.Mvc.Test/Test/SubmitImageExtensionsTest.cs66
-rw-r--r--test/Microsoft.Web.Mvc.Test/Test/TempDataValueProviderFactoryTest.cs86
-rw-r--r--test/Microsoft.Web.Mvc.Test/Test/TypeHelpersTest.cs121
-rw-r--r--test/Microsoft.Web.Mvc.Test/Test/UrlAttributeTest.cs44
-rw-r--r--test/Microsoft.Web.Mvc.Test/Test/ValueProviderUtilTest.cs46
-rw-r--r--test/Microsoft.Web.Mvc.Test/packages.config6
-rw-r--r--test/Microsoft.Web.WebPages.OAuth.Test/Microsoft.Web.WebPages.OAuth.Test.csproj104
-rw-r--r--test/Microsoft.Web.WebPages.OAuth.Test/OAuthWebSecurityTest.cs389
-rw-r--r--test/Microsoft.Web.WebPages.OAuth.Test/PreAppStartCodeTest.cs12
-rw-r--r--test/Microsoft.Web.WebPages.OAuth.Test/Properties/AssemblyInfo.cs7
-rw-r--r--test/Microsoft.Web.WebPages.OAuth.Test/packages.config11
-rw-r--r--test/SPA.Test/Index.html47
-rw-r--r--test/SPA.Test/Properties/AssemblyInfo.cs35
-rw-r--r--test/SPA.Test/SPA.Test.csproj133
-rw-r--r--test/SPA.Test/Scripts/IntellisenseFix.js32
-rw-r--r--test/SPA.Test/Scripts/References.js20
-rw-r--r--test/SPA.Test/Scripts/TestSetup.js320
-rw-r--r--test/SPA.Test/Web.Debug.config30
-rw-r--r--test/SPA.Test/Web.Release.config31
-rw-r--r--test/SPA.Test/Web.config13
-rw-r--r--test/SPA.Test/css/qunit.css226
-rw-r--r--test/SPA.Test/css/tests.css15
-rw-r--r--test/SPA.Test/upshot/ChangeTracking.tests.js348
-rw-r--r--test/SPA.Test/upshot/Consistency.tests.js158
-rw-r--r--test/SPA.Test/upshot/Core.tests.js293
-rw-r--r--test/SPA.Test/upshot/DataContext.tests.js7
-rw-r--r--test/SPA.Test/upshot/DataProvider.tests.js156
-rw-r--r--test/SPA.Test/upshot/DataSource.Common.js161
-rw-r--r--test/SPA.Test/upshot/DataSource.Tests.js349
-rw-r--r--test/SPA.Test/upshot/Datasets.js257
-rw-r--r--test/SPA.Test/upshot/Delete.Tests.js753
-rw-r--r--test/SPA.Test/upshot/EntitySet.tests.js1525
-rw-r--r--test/SPA.Test/upshot/Init.js41
-rw-r--r--test/SPA.Test/upshot/Mapping.tests.js441
-rw-r--r--test/SPA.Test/upshot/RecordSet.js67
-rw-r--r--test/SPA.Test/upshot/jQuery.DataView.Tests.js287
-rw-r--r--test/Settings.StyleCop145
-rw-r--r--test/System.Json.Test.Integration/Common/InstanceCreator.cs1585
-rw-r--r--test/System.Json.Test.Integration/Common/JsonValueCreatorSurrogate.cs149
-rw-r--r--test/System.Json.Test.Integration/Common/Log.cs10
-rw-r--r--test/System.Json.Test.Integration/Common/TypeLibrary.cs1943
-rw-r--r--test/System.Json.Test.Integration/Common/Util.cs201
-rw-r--r--test/System.Json.Test.Integration/JObjectFunctionalTest.cs990
-rw-r--r--test/System.Json.Test.Integration/JsonPrimitiveTests.cs1068
-rw-r--r--test/System.Json.Test.Integration/JsonStringRoundTripTests.cs583
-rw-r--r--test/System.Json.Test.Integration/JsonValueAndComplexTypesTests.cs329
-rw-r--r--test/System.Json.Test.Integration/JsonValueDynamicTests.cs1108
-rw-r--r--test/System.Json.Test.Integration/JsonValueEventsTests.cs518
-rw-r--r--test/System.Json.Test.Integration/JsonValueLinqExtensionsIntegrationTest.cs68
-rw-r--r--test/System.Json.Test.Integration/JsonValuePartialTrustTests.cs186
-rw-r--r--test/System.Json.Test.Integration/JsonValueTestHelper.cs609
-rw-r--r--test/System.Json.Test.Integration/JsonValueTests.cs380
-rw-r--r--test/System.Json.Test.Integration/JsonValueUsageTest.cs433
-rw-r--r--test/System.Json.Test.Integration/Properties/AssemblyInfo.cs34
-rw-r--r--test/System.Json.Test.Integration/System.Json.Test.Integration.csproj88
-rw-r--r--test/System.Json.Test.Integration/packages.config5
-rw-r--r--test/System.Json.Test.Unit/Common/AnyInstance.cs220
-rw-r--r--test/System.Json.Test.Unit/Common/ExceptionTestHelper.cs27
-rw-r--r--test/System.Json.Test.Unit/Extensions/JsonValueExtensionsTest.cs476
-rw-r--r--test/System.Json.Test.Unit/FormUrlEncodedJsonTests.cs74
-rw-r--r--test/System.Json.Test.Unit/JsonArrayTest.cs604
-rw-r--r--test/System.Json.Test.Unit/JsonDefaultTest.cs153
-rw-r--r--test/System.Json.Test.Unit/JsonObjectTest.cs787
-rw-r--r--test/System.Json.Test.Unit/JsonPrimitiveTest.cs410
-rw-r--r--test/System.Json.Test.Unit/JsonTypeTest.cs26
-rw-r--r--test/System.Json.Test.Unit/JsonValueDynamicMetaObjectTest.cs534
-rw-r--r--test/System.Json.Test.Unit/JsonValueDynamicTest.cs468
-rw-r--r--test/System.Json.Test.Unit/JsonValueLinqExtensionsTest.cs35
-rw-r--r--test/System.Json.Test.Unit/JsonValueTest.cs565
-rw-r--r--test/System.Json.Test.Unit/Properties/AssemblyInfo.cs37
-rw-r--r--test/System.Json.Test.Unit/System.Json.Test.Unit.csproj85
-rw-r--r--test/System.Json.Test.Unit/packages.config5
-rw-r--r--test/System.Net.Http.Formatting.Test.Integration/FormUrlEncodedFromContentTests.cs527
-rw-r--r--test/System.Net.Http.Formatting.Test.Integration/FormUrlEncodedFromUriQueryTests.cs503
-rw-r--r--test/System.Net.Http.Formatting.Test.Integration/JsonNetSerializationTest.cs536
-rw-r--r--test/System.Net.Http.Formatting.Test.Integration/JsonValueRoundTripComparer.cs83
-rw-r--r--test/System.Net.Http.Formatting.Test.Integration/Properties/AssemblyInfo.cs34
-rw-r--r--test/System.Net.Http.Formatting.Test.Integration/System.Net.Http.Formatting.Test.Integration.csproj96
-rw-r--r--test/System.Net.Http.Formatting.Test.Integration/packages.config7
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/ContentDispositionHeaderValueExtensionsTests.cs48
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/DataSets/HttpUnitTestDataSets.cs103
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/DataContractEnum.cs16
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/DataContractType.cs75
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/DerivedDataContractType.cs59
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/DerivedFormUrlEncodedMediaTypeFormatter.cs7
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/DerivedJsonMediaTypeFormatter.cs7
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/DerivedWcfPocoType.cs38
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/DerivedXmlMediaTypeFormatter.cs7
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/DerivedXmlSerializableType.cs56
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/HttpTestData.cs427
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/INotJsonSerializable.cs9
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/WcfPocoType.cs86
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/XmlSerializableType.cs96
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/Formatting/BufferedMediaTypeFormatterTests.cs95
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/Formatting/DefaultContentNegotiatorTests.cs220
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/Formatting/FormDataCollectionTests.cs107
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/Formatting/FormUrlEncodedMediaTypeFormatterTests.cs148
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/Formatting/JsonKeyValueModelTest.cs39
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/Formatting/JsonMediaTypeFormatterTests.cs244
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/Formatting/MediaRangeMappingTests.cs151
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/Formatting/MediaTypeConstantsTests.cs74
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/Formatting/MediaTypeFormatterCollectionTests.cs222
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/Formatting/MediaTypeFormatterExtensionsTests.cs120
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/Formatting/MediaTypeFormatterTests.cs401
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/Formatting/MediaTypeHeadeValueComparerTests.cs209
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/Formatting/MediaTypeHeadeValueExtensionsTests.cs176
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/Formatting/MediaTypeHeaderValueEqualityComparerTests.cs199
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/Formatting/ParsedMediaTypeHeaderValueTests.cs168
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/Formatting/Parsers/FormUrlEncodedParserTests.cs176
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/Formatting/Parsers/HttpRequestHeaderParserTests.cs273
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/Formatting/Parsers/HttpRequestLineParserTests.cs273
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/Formatting/Parsers/HttpResponseHeaderParserTests.cs272
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/Formatting/Parsers/HttpStatusLineParserTests.cs284
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/Formatting/Parsers/InternetMessageFormatHeaderParserTests.cs664
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/Formatting/Parsers/MimeMultipartParserTests.cs482
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/Formatting/QueryStringMappingTests.cs202
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/Formatting/RequestHeaderMappingTests.cs236
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/Formatting/ThresholdStreamTest.cs114
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/Formatting/UriPathExtensionMappingTests.cs168
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/Formatting/XmlMediaTypeFormatterTests.cs304
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/FormattingUtilitiesTests.cs95
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/HttpClientExtensionsTest.cs256
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/HttpContentCollectionExtensionsTests.cs336
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/HttpContentExtensionsTest.cs127
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/HttpContentMessageExtensionsTests.cs491
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/HttpContentMultipartExtensionsTests.cs512
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/HttpMessageContentTests.cs309
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/Mocks/MockHttpContent.cs90
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/Mocks/MockMediaTypeFormatter.cs30
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/Mocks/TestableHttpMessageHandler.cs18
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/MultipartFileStreamProviderTests.cs117
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/MultipartFormDataStreamProviderTests.cs120
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/MultipartMemoryStreamProviderTests.cs55
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/ObjectContentOfTTests.cs38
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/ObjectContentTests.cs168
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/ParserData.cs192
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/Properties/AssemblyInfo.cs21
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/System.Net.Http.Formatting.Test.Unit.csproj156
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/UriExtensionsTests.cs173
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/UriQueryDataSet.cs31
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/UriQueryUtilityTests.cs160
-rw-r--r--test/System.Net.Http.Formatting.Test.Unit/packages.config8
-rw-r--r--test/System.Web.Helpers.Test/ChartTest.cs653
-rw-r--r--test/System.Web.Helpers.Test/ConversionUtilTest.cs76
-rw-r--r--test/System.Web.Helpers.Test/CryptoTest.cs170
-rw-r--r--test/System.Web.Helpers.Test/DynamicDictionary.cs103
-rw-r--r--test/System.Web.Helpers.Test/DynamicHelperTest.cs38
-rw-r--r--test/System.Web.Helpers.Test/DynamicWrapper.cs80
-rw-r--r--test/System.Web.Helpers.Test/HelperResultTest.cs72
-rw-r--r--test/System.Web.Helpers.Test/JsonTest.cs369
-rw-r--r--test/System.Web.Helpers.Test/ObjectInfoTest.cs728
-rw-r--r--test/System.Web.Helpers.Test/PreComputedGridDataSourceTest.cs41
-rw-r--r--test/System.Web.Helpers.Test/Properties/AssemblyInfo.cs34
-rw-r--r--test/System.Web.Helpers.Test/ServerInfoTest.cs162
-rw-r--r--test/System.Web.Helpers.Test/System.Web.Helpers.Test.csproj106
-rw-r--r--test/System.Web.Helpers.Test/TestFiles/HiRes.jpgbin0 -> 21694 bytes
-rw-r--r--test/System.Web.Helpers.Test/TestFiles/LambdaFinal.jpgbin0 -> 119973 bytes
-rw-r--r--test/System.Web.Helpers.Test/TestFiles/NETLogo.pngbin0 -> 10337 bytes
-rw-r--r--test/System.Web.Helpers.Test/TestFiles/logo.bmpbin0 -> 14310 bytes
-rw-r--r--test/System.Web.Helpers.Test/TestFiles/xhtml11-flat.dtd4513
-rw-r--r--test/System.Web.Helpers.Test/WebCacheTest.cs117
-rw-r--r--test/System.Web.Helpers.Test/WebGridDataSourceTest.cs305
-rw-r--r--test/System.Web.Helpers.Test/WebGridTest.cs2321
-rw-r--r--test/System.Web.Helpers.Test/WebImageTest.cs1162
-rw-r--r--test/System.Web.Helpers.Test/WebMailTest.cs378
-rw-r--r--test/System.Web.Helpers.Test/XhtmlAssert.cs128
-rw-r--r--test/System.Web.Helpers.Test/packages.config6
-rw-r--r--test/System.Web.Http.Common.Test/ErrorTests.cs21
-rw-r--r--test/System.Web.Http.Common.Test/HttpRequestMessageCommonExtensionsTest.cs57
-rw-r--r--test/System.Web.Http.Common.Test/System.Web.Http.Common.Test.csproj82
-rw-r--r--test/System.Web.Http.Common.Test/TaskHelpersExtensionsTest.cs2169
-rw-r--r--test/System.Web.Http.Common.Test/TaskHelpersTest.cs574
-rw-r--r--test/System.Web.Http.Common.Test/packages.config7
-rw-r--r--test/System.Web.Http.Integration.Test/ApiExplorer/ApiExplorerSettingsTest.cs62
-rw-r--r--test/System.Web.Http.Integration.Test/ApiExplorer/Controllers/DocumentationController.cs31
-rw-r--r--test/System.Web.Http.Integration.Test/ApiExplorer/Controllers/HiddenActionController.cs29
-rw-r--r--test/System.Web.Http.Integration.Test/ApiExplorer/Controllers/HiddenController.cs28
-rw-r--r--test/System.Web.Http.Integration.Test/ApiExplorer/Controllers/ItemController.cs32
-rw-r--r--test/System.Web.Http.Integration.Test/ApiExplorer/Controllers/OverloadsController.cs28
-rw-r--r--test/System.Web.Http.Integration.Test/ApiExplorer/Controllers/ParameterSourceController.cs58
-rw-r--r--test/System.Web.Http.Integration.Test/ApiExplorer/DocumentationProviders/AttributeDocumentationProvider.cs56
-rw-r--r--test/System.Web.Http.Integration.Test/ApiExplorer/DocumentationTest.cs65
-rw-r--r--test/System.Web.Http.Integration.Test/ApiExplorer/Formatters/ItemFormatter.cs27
-rw-r--r--test/System.Web.Http.Integration.Test/ApiExplorer/FormattersTest.cs42
-rw-r--r--test/System.Web.Http.Integration.Test/ApiExplorer/ParameterSourceTest.cs57
-rw-r--r--test/System.Web.Http.Integration.Test/ApiExplorer/RouteConstraintsTest.cs116
-rw-r--r--test/System.Web.Http.Integration.Test/ApiExplorer/RoutesTest.cs203
-rw-r--r--test/System.Web.Http.Integration.Test/Authentication/BasicOverHttpTest.cs74
-rw-r--r--test/System.Web.Http.Integration.Test/Authentication/CustomMessageHandler.cs33
-rw-r--r--test/System.Web.Http.Integration.Test/Authentication/CustomUsernamePasswordValidator.cs19
-rw-r--r--test/System.Web.Http.Integration.Test/Authentication/RequireAdminAttribute.cs21
-rw-r--r--test/System.Web.Http.Integration.Test/Authentication/SampleController.cs14
-rw-r--r--test/System.Web.Http.Integration.Test/ContentNegotiation/AcceptHeaderTests.cs30
-rw-r--r--test/System.Web.Http.Integration.Test/ContentNegotiation/ConnegController.cs19
-rw-r--r--test/System.Web.Http.Integration.Test/ContentNegotiation/ConnegItem.cs8
-rw-r--r--test/System.Web.Http.Integration.Test/ContentNegotiation/ContentNegotiationTestBase.cs46
-rw-r--r--test/System.Web.Http.Integration.Test/ContentNegotiation/CustomFormatterTests.cs299
-rw-r--r--test/System.Web.Http.Integration.Test/ContentNegotiation/DefaultContentNegotiatorTests.cs37
-rw-r--r--test/System.Web.Http.Integration.Test/ContentNegotiation/HttpResponseReturnTests.cs132
-rw-r--r--test/System.Web.Http.Integration.Test/Controllers/ActionAttributesTest.cs138
-rw-r--r--test/System.Web.Http.Integration.Test/Controllers/ApiControllerActionSelectorTest.cs191
-rw-r--r--test/System.Web.Http.Integration.Test/Controllers/Apis/ActionAttributeTestController.cs29
-rw-r--r--test/System.Web.Http.Integration.Test/Controllers/Apis/TestController.cs28
-rw-r--r--test/System.Web.Http.Integration.Test/Controllers/Apis/User.cs9
-rw-r--r--test/System.Web.Http.Integration.Test/Controllers/Apis/UserAddress.cs9
-rw-r--r--test/System.Web.Http.Integration.Test/Controllers/CustomControllerFactoryTest.cs70
-rw-r--r--test/System.Web.Http.Integration.Test/Controllers/Helpers/ApiControllerHelper.cs54
-rw-r--r--test/System.Web.Http.Integration.Test/ExceptionHandling/DuplicateControllers.cs23
-rw-r--r--test/System.Web.Http.Integration.Test/ExceptionHandling/ExceptionController.cs102
-rw-r--r--test/System.Web.Http.Integration.Test/ExceptionHandling/ExceptionHandlingTest.cs225
-rw-r--r--test/System.Web.Http.Integration.Test/ExceptionHandling/HttpResponseExceptionTest.cs224
-rw-r--r--test/System.Web.Http.Integration.Test/ExceptionHandling/IncludeErrorDetailTest.cs76
-rw-r--r--test/System.Web.Http.Integration.Test/Filters/IQueryableFilterPipelineTest.cs99
-rw-r--r--test/System.Web.Http.Integration.Test/ModelBinding/BodyBindingTests.cs120
-rw-r--r--test/System.Web.Http.Integration.Test/ModelBinding/CustomBindingTests.cs32
-rw-r--r--test/System.Web.Http.Integration.Test/ModelBinding/DefaultActionValueBinderTest.cs990
-rw-r--r--test/System.Web.Http.Integration.Test/ModelBinding/HttpContentBindingTests.cs92
-rw-r--r--test/System.Web.Http.Integration.Test/ModelBinding/ModelBindingController.cs272
-rw-r--r--test/System.Web.Http.Integration.Test/ModelBinding/ModelBindingTests.cs48
-rw-r--r--test/System.Web.Http.Integration.Test/ModelBinding/QueryStringBindingTests.cs115
-rw-r--r--test/System.Web.Http.Integration.Test/ModelBinding/RouteBindingTests.cs29
-rw-r--r--test/System.Web.Http.Integration.Test/PartialTrust/BasicScenarioTest.cs62
-rw-r--r--test/System.Web.Http.Integration.Test/PartialTrust/PartialTrustRunner.cs132
-rw-r--r--test/System.Web.Http.Integration.Test/Properties/AssemblyInfo.cs3
-rw-r--r--test/System.Web.Http.Integration.Test/System.Web.Http.Integration.Test.csproj155
-rw-r--r--test/System.Web.Http.Integration.Test/Util/ApiExplorerHelper.cs54
-rw-r--r--test/System.Web.Http.Integration.Test/Util/ContextUtil.cs62
-rw-r--r--test/System.Web.Http.Integration.Test/Util/ScenarioHelper.cs41
-rw-r--r--test/System.Web.Http.Integration.Test/packages.config7
-rw-r--r--test/System.Web.Http.Test/AuthorizeAttributeTest.cs280
-rw-r--r--test/System.Web.Http.Test/Controllers/ApiControllerActionInvokerTest.cs49
-rw-r--r--test/System.Web.Http.Test/Controllers/ApiControllerActionSelectorTest.cs47
-rw-r--r--test/System.Web.Http.Test/Controllers/ApiControllerTest.cs702
-rw-r--r--test/System.Web.Http.Test/Controllers/Apis/User.cs9
-rw-r--r--test/System.Web.Http.Test/Controllers/Apis/UsersController.cs40
-rw-r--r--test/System.Web.Http.Test/Controllers/Apis/UsersRpcController.cs57
-rw-r--r--test/System.Web.Http.Test/Controllers/HttpActionContextTest.cs76
-rw-r--r--test/System.Web.Http.Test/Controllers/HttpConfigurationTest.cs48
-rw-r--r--test/System.Web.Http.Test/Controllers/HttpControllerContextTest.cs108
-rw-r--r--test/System.Web.Http.Test/Controllers/HttpControllerDescriptorTest.cs183
-rw-r--r--test/System.Web.Http.Test/Controllers/HttpParameterDescriptorTest.cs73
-rw-r--r--test/System.Web.Http.Test/Controllers/ReflectedHttpActionDescriptorTest.cs299
-rw-r--r--test/System.Web.Http.Test/Controllers/ReflectedHttpParameterDescriptorTest.cs92
-rw-r--r--test/System.Web.Http.Test/DictionaryExtensionsTest.cs127
-rw-r--r--test/System.Web.Http.Test/Dispatcher/DefaultBuildManagerTest.cs73
-rw-r--r--test/System.Web.Http.Test/Filters/ActionDescriptorFilterProviderTest.cs75
-rw-r--r--test/System.Web.Http.Test/Filters/ActionFilterAttributeTest.cs334
-rw-r--r--test/System.Web.Http.Test/Filters/AuthorizationFilterAttributeTest.cs189
-rw-r--r--test/System.Web.Http.Test/Filters/ConfigurationFilterProviderTest.cs34
-rw-r--r--test/System.Web.Http.Test/Filters/EnumerableEvaluatorFilterProviderTest.cs82
-rw-r--r--test/System.Web.Http.Test/Filters/EnumerableEvaluatorFilterTest.cs191
-rw-r--r--test/System.Web.Http.Test/Filters/ExceptionFilterAttributeTest.cs82
-rw-r--r--test/System.Web.Http.Test/Filters/FilterAttributeTest.cs34
-rw-r--r--test/System.Web.Http.Test/Filters/FilterInfoComparerTest.cs37
-rw-r--r--test/System.Web.Http.Test/Filters/FilterInfoTest.cs29
-rw-r--r--test/System.Web.Http.Test/Filters/HttpActionExecutedContextTest.cs88
-rw-r--r--test/System.Web.Http.Test/Filters/HttpFilterCollectionTest.cs94
-rw-r--r--test/System.Web.Http.Test/Filters/QueryCompositionFilterAttributeTest.cs88
-rw-r--r--test/System.Web.Http.Test/Filters/QueryCompositionFilterProviderTest.cs72
-rw-r--r--test/System.Web.Http.Test/Hosting/HttpRouteTest.cs38
-rw-r--r--test/System.Web.Http.Test/HttpBindingBehaviorAttributeTest.cs51
-rw-r--r--test/System.Web.Http.Test/HttpRequestMessageExtensionsTest.cs256
-rw-r--r--test/System.Web.Http.Test/HttpRouteCollectionExtensionsTest.cs67
-rw-r--r--test/System.Web.Http.Test/HttpServerTest.cs163
-rw-r--r--test/System.Web.Http.Test/Internal/CollectionModelBinderUtilTest.cs394
-rw-r--r--test/System.Web.Http.Test/Internal/TypeActivatorTest.cs158
-rw-r--r--test/System.Web.Http.Test/ModelBinding/Binders/ArrayModelBinderProviderTest.cs100
-rw-r--r--test/System.Web.Http.Test/ModelBinding/Binders/ArrayModelBinderTest.cs47
-rw-r--r--test/System.Web.Http.Test/ModelBinding/Binders/BinaryDataModelBinderProviderTest.cs159
-rw-r--r--test/System.Web.Http.Test/ModelBinding/Binders/CollectionModelBinderProviderTest.cs77
-rw-r--r--test/System.Web.Http.Test/ModelBinding/Binders/CollectionModelBinderTest.cs240
-rw-r--r--test/System.Web.Http.Test/ModelBinding/Binders/ComplexModelDtoModelBinderProviderTest.cs44
-rw-r--r--test/System.Web.Http.Test/ModelBinding/Binders/ComplexModelDtoModelBinderTest.cs91
-rw-r--r--test/System.Web.Http.Test/ModelBinding/Binders/ComplexModelDtoResultTest.cs41
-rw-r--r--test/System.Web.Http.Test/ModelBinding/Binders/ComplexModelDtoTest.cs53
-rw-r--r--test/System.Web.Http.Test/ModelBinding/Binders/DictionaryModelBinderProviderTest.cs76
-rw-r--r--test/System.Web.Http.Test/ModelBinding/Binders/DictionaryModelBinderTest.cs51
-rw-r--r--test/System.Web.Http.Test/ModelBinding/Binders/GenericModelBinderProviderTest.cs251
-rw-r--r--test/System.Web.Http.Test/ModelBinding/Binders/KeyValuePairModelBinderProviderTest.cs104
-rw-r--r--test/System.Web.Http.Test/ModelBinding/Binders/KeyValuePairModelBinderTest.cs110
-rw-r--r--test/System.Web.Http.Test/ModelBinding/Binders/KeyValuePairModelBinderUtilTest.cs105
-rw-r--r--test/System.Web.Http.Test/ModelBinding/Binders/MutableObjectModelBinderProviderTest.cs103
-rw-r--r--test/System.Web.Http.Test/ModelBinding/Binders/MutableObjectModelBinderTest.cs737
-rw-r--r--test/System.Web.Http.Test/ModelBinding/Binders/SimpleModelBinderProviderTest.cs155
-rw-r--r--test/System.Web.Http.Test/ModelBinding/Binders/TypeConverterModelBinderProviderTest.cs68
-rw-r--r--test/System.Web.Http.Test/ModelBinding/Binders/TypeConverterModelBinderTest.cs170
-rw-r--r--test/System.Web.Http.Test/ModelBinding/Binders/TypeMatchModelBinderProviderTest.cs61
-rw-r--r--test/System.Web.Http.Test/ModelBinding/Binders/TypeMatchModelBinderTest.cs113
-rw-r--r--test/System.Web.Http.Test/ModelBinding/CompositeModelBinderTest.cs312
-rw-r--r--test/System.Web.Http.Test/ModelBinding/DefaultActionValueBinderTest.cs490
-rw-r--r--test/System.Web.Http.Test/ModelBinding/FormDataCollectionExtensionsTest.cs229
-rw-r--r--test/System.Web.Http.Test/ModelBinding/ModelBinderAttributeTest.cs101
-rw-r--r--test/System.Web.Http.Test/ModelBinding/ModelBinderConfigTest.cs147
-rw-r--r--test/System.Web.Http.Test/ModelBinding/ModelBindingContextTest.cs126
-rw-r--r--test/System.Web.Http.Test/ModelBinding/ModelBindingUtilTest.cs388
-rw-r--r--test/System.Web.Http.Test/Properties/AssemblyInfo.cs3
-rw-r--r--test/System.Web.Http.Test/Query/DataModel.cs43
-rw-r--r--test/System.Web.Http.Test/Query/ODataQueryDeserializerTests.cs623
-rw-r--r--test/System.Web.Http.Test/Query/QueryValidatorTest.cs93
-rw-r--r--test/System.Web.Http.Test/Routing/HttpRouteTest.cs27
-rw-r--r--test/System.Web.Http.Test/Routing/UrlHelperTest.cs149
-rw-r--r--test/System.Web.Http.Test/Services/DependencyResolverTests.cs219
-rw-r--r--test/System.Web.Http.Test/System.Web.Http.Test.csproj247
-rw-r--r--test/System.Web.Http.Test/Tracing/FormattingUtilitiesTest.cs283
-rw-r--r--test/System.Web.Http.Test/Tracing/HttpRequestMessageExtensionsTest.cs24
-rw-r--r--test/System.Web.Http.Test/Tracing/ITraceWriterExtensionsTest.cs1004
-rw-r--r--test/System.Web.Http.Test/Tracing/TestTraceWriter.cs30
-rw-r--r--test/System.Web.Http.Test/Tracing/TraceManagerTest.cs137
-rw-r--r--test/System.Web.Http.Test/Tracing/TraceRecordComparer.cs39
-rw-r--r--test/System.Web.Http.Test/Tracing/Tracers/ActionFilterAttributeTracerTest.cs108
-rw-r--r--test/System.Web.Http.Test/Tracing/Tracers/ActionFilterTracerTest.cs82
-rw-r--r--test/System.Web.Http.Test/Tracing/Tracers/ActionValueBinderTracerTest.cs71
-rw-r--r--test/System.Web.Http.Test/Tracing/Tracers/AuthorizationFilterAttributeTracerTest.cs72
-rw-r--r--test/System.Web.Http.Test/Tracing/Tracers/AuthorizationFilterTracerTest.cs76
-rw-r--r--test/System.Web.Http.Test/Tracing/Tracers/ContentNegotiatorTracerTest.cs227
-rw-r--r--test/System.Web.Http.Test/Tracing/Tracers/ExceptionFilterAttributeTracerTest.cs74
-rw-r--r--test/System.Web.Http.Test/Tracing/Tracers/ExceptionFilterTracerTest.cs71
-rw-r--r--test/System.Web.Http.Test/Tracing/Tracers/FilterTracerTest.cs274
-rw-r--r--test/System.Web.Http.Test/Tracing/Tracers/FormatterParameterBindingTracerTest.cs128
-rw-r--r--test/System.Web.Http.Test/Tracing/Tracers/HttpActionBindingTracerTest.cs102
-rw-r--r--test/System.Web.Http.Test/Tracing/Tracers/HttpActionDescriptorTracerTest.cs136
-rw-r--r--test/System.Web.Http.Test/Tracing/Tracers/HttpActionInvokerTracerTest.cs224
-rw-r--r--test/System.Web.Http.Test/Tracing/Tracers/HttpActionSelectorTracerTest.cs80
-rw-r--r--test/System.Web.Http.Test/Tracing/Tracers/HttpControllerActivatorTracerTest.cs64
-rw-r--r--test/System.Web.Http.Test/Tracing/Tracers/HttpControllerFactoryTracerTest.cs63
-rw-r--r--test/System.Web.Http.Test/Tracing/Tracers/HttpControllerTracerTest.cs121
-rw-r--r--test/System.Web.Http.Test/Tracing/Tracers/HttpParameterBindingTracerTest.cs122
-rw-r--r--test/System.Web.Http.Test/Tracing/Tracers/MediaTypeFormatterTracerTest.cs248
-rw-r--r--test/System.Web.Http.Test/Tracing/Tracers/MessageHandlerTracerTest.cs156
-rw-r--r--test/System.Web.Http.Test/Tracing/Tracers/RequestMessageHandlerTracerTest.cs150
-rw-r--r--test/System.Web.Http.Test/Util/ContextUtil.cs48
-rw-r--r--test/System.Web.Http.Test/Util/SimpleHttpValueProvider.cs70
-rw-r--r--test/System.Web.Http.Test/Validation/DefaultBodyModelValidatorTest.cs160
-rw-r--r--test/System.Web.Http.Test/Validation/ModelValidationNodeTest.cs331
-rw-r--r--test/System.Web.Http.Test/Validation/ModelValidationRequiredMemberSelectorTest.cs43
-rw-r--r--test/System.Web.Http.Test/ValueProviders/Providers/KeyValueModelValueProviderTest.cs217
-rw-r--r--test/System.Web.Http.Test/ValueProviders/Providers/NameValueCollectionValueProviderTest.cs210
-rw-r--r--test/System.Web.Http.Test/ValueProviders/Providers/QueryStringValueProviderTest.cs152
-rw-r--r--test/System.Web.Http.Test/packages.config7
-rw-r--r--test/System.Web.Http.WebHost.Test/HttpControllerHandlerTest.cs85
-rw-r--r--test/System.Web.Http.WebHost.Test/Properties/AssemblyInfo.cs3
-rw-r--r--test/System.Web.Http.WebHost.Test/RouteCollectionExtensionsTest.cs67
-rw-r--r--test/System.Web.Http.WebHost.Test/Routing/HostedUrlHelperTest.cs137
-rw-r--r--test/System.Web.Http.WebHost.Test/System.Web.Http.WebHost.Test.csproj92
-rw-r--r--test/System.Web.Http.WebHost.Test/packages.config7
-rw-r--r--test/System.Web.Mvc.Test/Ajax/Test/AjaxExtensionsTest.cs1975
-rw-r--r--test/System.Web.Mvc.Test/Ajax/Test/AjaxOptionsTest.cs290
-rw-r--r--test/System.Web.Mvc.Test/Async/Test/AsyncActionDescriptorTest.cs51
-rw-r--r--test/System.Web.Mvc.Test/Async/Test/AsyncActionMethodSelectorTest.cs377
-rw-r--r--test/System.Web.Mvc.Test/Async/Test/AsyncControllerActionInvokerTest.cs916
-rw-r--r--test/System.Web.Mvc.Test/Async/Test/AsyncManagerTest.cs113
-rw-r--r--test/System.Web.Mvc.Test/Async/Test/AsyncResultWrapperTest.cs228
-rw-r--r--test/System.Web.Mvc.Test/Async/Test/AsyncUtilTest.cs98
-rw-r--r--test/System.Web.Mvc.Test/Async/Test/MockAsyncResult.cs45
-rw-r--r--test/System.Web.Mvc.Test/Async/Test/OperationCounterTest.cs110
-rw-r--r--test/System.Web.Mvc.Test/Async/Test/ReflectedAsyncActionDescriptorTest.cs314
-rw-r--r--test/System.Web.Mvc.Test/Async/Test/ReflectedAsyncControllerDescriptorTest.cs177
-rw-r--r--test/System.Web.Mvc.Test/Async/Test/SignalContainer.cs22
-rw-r--r--test/System.Web.Mvc.Test/Async/Test/SimpleAsyncResultTest.cs101
-rw-r--r--test/System.Web.Mvc.Test/Async/Test/SingleEntryGateTest.cs24
-rw-r--r--test/System.Web.Mvc.Test/Async/Test/SynchronizationContextUtilTest.cs89
-rw-r--r--test/System.Web.Mvc.Test/Async/Test/SynchronousOperationExceptionTest.cs62
-rw-r--r--test/System.Web.Mvc.Test/Async/Test/TaskAsyncActionDescriptorTest.cs558
-rw-r--r--test/System.Web.Mvc.Test/Async/Test/TaskWrapperAsyncResultTest.cs62
-rw-r--r--test/System.Web.Mvc.Test/Async/Test/TriggerListenerTest.cs30
-rw-r--r--test/System.Web.Mvc.Test/ExpressionUtil/Test/BinaryExpressionFingerprintTest.cs91
-rw-r--r--test/System.Web.Mvc.Test/ExpressionUtil/Test/CachedExpressionCompilerTest.cs141
-rw-r--r--test/System.Web.Mvc.Test/ExpressionUtil/Test/ConditionalExpressionFingerprintTest.cs69
-rw-r--r--test/System.Web.Mvc.Test/ExpressionUtil/Test/ConstantExpressionFingerprintTest.cs69
-rw-r--r--test/System.Web.Mvc.Test/ExpressionUtil/Test/DefaultExpressionFingerprintTest.cs69
-rw-r--r--test/System.Web.Mvc.Test/ExpressionUtil/Test/DummyExpressionFingerprint.cs13
-rw-r--r--test/System.Web.Mvc.Test/ExpressionUtil/Test/ExpressionFingerprintTest.cs42
-rw-r--r--test/System.Web.Mvc.Test/ExpressionUtil/Test/FingerprintingExpressionVisitorTest.cs301
-rw-r--r--test/System.Web.Mvc.Test/ExpressionUtil/Test/HoistingExpressionVisitorTest.cs45
-rw-r--r--test/System.Web.Mvc.Test/ExpressionUtil/Test/IndexExpressionFingerprintTest.cs91
-rw-r--r--test/System.Web.Mvc.Test/ExpressionUtil/Test/LambdaExpressionFingerprintTest.cs69
-rw-r--r--test/System.Web.Mvc.Test/ExpressionUtil/Test/MemberExpressionFingerprintTest.cs91
-rw-r--r--test/System.Web.Mvc.Test/ExpressionUtil/Test/MethodCallExpressionFingerprintTest.cs91
-rw-r--r--test/System.Web.Mvc.Test/ExpressionUtil/Test/ParameterExpressionFingerprintTest.cs90
-rw-r--r--test/System.Web.Mvc.Test/ExpressionUtil/Test/TypeBinaryExpressionFingerprintTest.cs90
-rw-r--r--test/System.Web.Mvc.Test/ExpressionUtil/Test/UnaryExpressionFingerprintTest.cs91
-rw-r--r--test/System.Web.Mvc.Test/Html/Test/ChildActionExtensionsTest.cs256
-rw-r--r--test/System.Web.Mvc.Test/Html/Test/DefaultDisplayTemplatesTest.cs560
-rw-r--r--test/System.Web.Mvc.Test/Html/Test/DefaultEditorTemplatesTest.cs595
-rw-r--r--test/System.Web.Mvc.Test/Html/Test/DisplayNameExtensionsTest.cs255
-rw-r--r--test/System.Web.Mvc.Test/Html/Test/FormExtensionsTest.cs423
-rw-r--r--test/System.Web.Mvc.Test/Html/Test/InputExtensionsTest.cs2376
-rw-r--r--test/System.Web.Mvc.Test/Html/Test/LabelExtensionsTest.cs516
-rw-r--r--test/System.Web.Mvc.Test/Html/Test/LinkExtensionsTest.cs562
-rw-r--r--test/System.Web.Mvc.Test/Html/Test/MvcFormTest.cs97
-rw-r--r--test/System.Web.Mvc.Test/Html/Test/NameExtensionsTest.cs83
-rw-r--r--test/System.Web.Mvc.Test/Html/Test/PartialExtensionsTest.cs84
-rw-r--r--test/System.Web.Mvc.Test/Html/Test/RenderPartialExtensionsTest.cs122
-rw-r--r--test/System.Web.Mvc.Test/Html/Test/SelectExtensionsTest.cs1857
-rw-r--r--test/System.Web.Mvc.Test/Html/Test/TemplateHelpersTest.cs1158
-rw-r--r--test/System.Web.Mvc.Test/Html/Test/TextAreaExtensionsTest.cs629
-rw-r--r--test/System.Web.Mvc.Test/Html/Test/ValidationExtensionsTest.cs1086
-rw-r--r--test/System.Web.Mvc.Test/Html/Test/ValueExtensionsTest.cs164
-rw-r--r--test/System.Web.Mvc.Test/Properties/AssemblyInfo.cs3
-rw-r--r--test/System.Web.Mvc.Test/Razor/Test/MvcCSharpRazorCodeGeneratorTest.cs69
-rw-r--r--test/System.Web.Mvc.Test/Razor/Test/MvcCSharpRazorCodeParserTest.cs284
-rw-r--r--test/System.Web.Mvc.Test/Razor/Test/MvcVBRazorCodeParserTest.cs301
-rw-r--r--test/System.Web.Mvc.Test/Razor/Test/MvcWebPageRazorHostTest.cs107
-rw-r--r--test/System.Web.Mvc.Test/System.Web.Mvc.Test.csproj356
-rw-r--r--test/System.Web.Mvc.Test/Test/AcceptVerbsAttributeTest.cs214
-rw-r--r--test/System.Web.Mvc.Test/Test/ActionDescriptorTest.cs280
-rw-r--r--test/System.Web.Mvc.Test/Test/ActionExecutedContextTest.cs52
-rw-r--r--test/System.Web.Mvc.Test/Test/ActionExecutingContextTest.cs52
-rw-r--r--test/System.Web.Mvc.Test/Test/ActionFilterAttributeTest.cs42
-rw-r--r--test/System.Web.Mvc.Test/Test/ActionMethodDispatcherCacheTest.cs24
-rw-r--r--test/System.Web.Mvc.Test/Test/ActionMethodDispatcherTest.cs126
-rw-r--r--test/System.Web.Mvc.Test/Test/ActionMethodSelectorTest.cs212
-rw-r--r--test/System.Web.Mvc.Test/Test/ActionNameAttributeTest.cs60
-rw-r--r--test/System.Web.Mvc.Test/Test/AdditionalMetadataAttributeTest.cs73
-rw-r--r--test/System.Web.Mvc.Test/Test/AjaxHelperTest.cs236
-rw-r--r--test/System.Web.Mvc.Test/Test/AjaxHelper`1Test.cs41
-rw-r--r--test/System.Web.Mvc.Test/Test/AjaxRequestExtensionsTest.cs70
-rw-r--r--test/System.Web.Mvc.Test/Test/AllowHtmlAttributeTest.cs37
-rw-r--r--test/System.Web.Mvc.Test/Test/AreaHelpersTest.cs97
-rw-r--r--test/System.Web.Mvc.Test/Test/AreaRegistrationContextTest.cs138
-rw-r--r--test/System.Web.Mvc.Test/Test/AreaRegistrationTest.cs99
-rw-r--r--test/System.Web.Mvc.Test/Test/AssociatedMetadataProviderTest.cs346
-rw-r--r--test/System.Web.Mvc.Test/Test/AssociatedValidatorProviderTest.cs124
-rw-r--r--test/System.Web.Mvc.Test/Test/AsyncControllerTest.cs263
-rw-r--r--test/System.Web.Mvc.Test/Test/AsyncTimeoutAttributeTest.cs87
-rw-r--r--test/System.Web.Mvc.Test/Test/AuthorizationContextTest.cs35
-rw-r--r--test/System.Web.Mvc.Test/Test/AuthorizeAttributeTest.cs362
-rw-r--r--test/System.Web.Mvc.Test/Test/BindAttributeTest.cs96
-rw-r--r--test/System.Web.Mvc.Test/Test/BuildManagerCompiledViewTest.cs145
-rw-r--r--test/System.Web.Mvc.Test/Test/BuildManagerViewEngineTest.cs182
-rw-r--r--test/System.Web.Mvc.Test/Test/ByteArrayModelBinderTest.cs121
-rw-r--r--test/System.Web.Mvc.Test/Test/CachedAssociatedMetadataProviderTest.cs275
-rw-r--r--test/System.Web.Mvc.Test/Test/CachedDataAnnotationsModelMetadataProviderTest.cs10
-rw-r--r--test/System.Web.Mvc.Test/Test/CancellationTokenModelBinderTest.cs21
-rw-r--r--test/System.Web.Mvc.Test/Test/ChildActionOnlyAttributeTest.cs52
-rw-r--r--test/System.Web.Mvc.Test/Test/ChildActionValueProviderFactoryTest.cs93
-rw-r--r--test/System.Web.Mvc.Test/Test/ClientDataTypeModelValidatorProviderTest.cs215
-rw-r--r--test/System.Web.Mvc.Test/Test/CompareAttributeTest.cs198
-rw-r--r--test/System.Web.Mvc.Test/Test/ContentResultTest.cs158
-rw-r--r--test/System.Web.Mvc.Test/Test/ControllerActionInvokerTest.cs2427
-rw-r--r--test/System.Web.Mvc.Test/Test/ControllerBaseTest.cs234
-rw-r--r--test/System.Web.Mvc.Test/Test/ControllerBuilderTest.cs274
-rw-r--r--test/System.Web.Mvc.Test/Test/ControllerContextTest.cs296
-rw-r--r--test/System.Web.Mvc.Test/Test/ControllerDescriptorCacheTest.cs23
-rw-r--r--test/System.Web.Mvc.Test/Test/ControllerDescriptorTest.cs129
-rw-r--r--test/System.Web.Mvc.Test/Test/ControllerInstanceFilterProviderTest.cs44
-rw-r--r--test/System.Web.Mvc.Test/Test/ControllerTest.cs2047
-rw-r--r--test/System.Web.Mvc.Test/Test/DataAnnotationsModelMetadataProviderTest.cs10
-rw-r--r--test/System.Web.Mvc.Test/Test/DataAnnotationsModelMetadataProviderTestBase.cs614
-rw-r--r--test/System.Web.Mvc.Test/Test/DataAnnotationsModelValidatorProviderTest.cs725
-rw-r--r--test/System.Web.Mvc.Test/Test/DataAnnotationsModelValidatorTest.cs158
-rw-r--r--test/System.Web.Mvc.Test/Test/DataErrorInfoModelValidatorProviderTest.cs287
-rw-r--r--test/System.Web.Mvc.Test/Test/DataTypeUtilTest.cs75
-rw-r--r--test/System.Web.Mvc.Test/Test/DefaultControllerFactoryTest.cs766
-rw-r--r--test/System.Web.Mvc.Test/Test/DefaultModelBinderTest.cs2904
-rw-r--r--test/System.Web.Mvc.Test/Test/DefaultViewLocationCacheTest.cs73
-rw-r--r--test/System.Web.Mvc.Test/Test/DependencyResolverTest.cs241
-rw-r--r--test/System.Web.Mvc.Test/Test/DescriptorUtilTest.cs55
-rw-r--r--test/System.Web.Mvc.Test/Test/DictionaryHelpersTest.cs89
-rw-r--r--test/System.Web.Mvc.Test/Test/DictionaryValueProviderTest.cs102
-rw-r--r--test/System.Web.Mvc.Test/Test/DynamicViewDataDictionaryTest.cs214
-rw-r--r--test/System.Web.Mvc.Test/Test/EmptyModelValidatorProviderTest.cs21
-rw-r--r--test/System.Web.Mvc.Test/Test/ExceptionContextTest.cs46
-rw-r--r--test/System.Web.Mvc.Test/Test/ExpressionHelperTest.cs120
-rw-r--r--test/System.Web.Mvc.Test/Test/FieldValidationMetadataTest.cs33
-rw-r--r--test/System.Web.Mvc.Test/Test/FileContentResultTest.cs65
-rw-r--r--test/System.Web.Mvc.Test/Test/FilePathResultTest.cs64
-rw-r--r--test/System.Web.Mvc.Test/Test/FileResultTest.cs160
-rw-r--r--test/System.Web.Mvc.Test/Test/FileStreamResultTest.cs77
-rw-r--r--test/System.Web.Mvc.Test/Test/FilterAttributeFilterProviderTest.cs155
-rw-r--r--test/System.Web.Mvc.Test/Test/FilterInfoTest.cs69
-rw-r--r--test/System.Web.Mvc.Test/Test/FilterProviderCollectionTest.cs202
-rw-r--r--test/System.Web.Mvc.Test/Test/FilterProvidersTest.cs17
-rw-r--r--test/System.Web.Mvc.Test/Test/FilterTest.cs66
-rw-r--r--test/System.Web.Mvc.Test/Test/FormCollectionTest.cs180
-rw-r--r--test/System.Web.Mvc.Test/Test/FormContextTest.cs172
-rw-r--r--test/System.Web.Mvc.Test/Test/FormValueProviderFactoryTest.cs77
-rw-r--r--test/System.Web.Mvc.Test/Test/GlobalFilterCollectionTest.cs92
-rw-r--r--test/System.Web.Mvc.Test/Test/HandleErrorAttributeTest.cs251
-rw-r--r--test/System.Web.Mvc.Test/Test/HandleErrorInfoTest.cs76
-rw-r--r--test/System.Web.Mvc.Test/Test/HtmlHelperTest.cs1172
-rw-r--r--test/System.Web.Mvc.Test/Test/HtmlHelper`1Test.cs40
-rw-r--r--test/System.Web.Mvc.Test/Test/HttpDeleteAttributeTest.cs25
-rw-r--r--test/System.Web.Mvc.Test/Test/HttpFileCollectionValueProviderFactoryTest.cs36
-rw-r--r--test/System.Web.Mvc.Test/Test/HttpFileCollectionValueProviderTest.cs147
-rw-r--r--test/System.Web.Mvc.Test/Test/HttpGetAttributeTest.cs25
-rw-r--r--test/System.Web.Mvc.Test/Test/HttpHandlerUtilTest.cs153
-rw-r--r--test/System.Web.Mvc.Test/Test/HttpNotFoundResultTest.cs31
-rw-r--r--test/System.Web.Mvc.Test/Test/HttpPostAttributeTest.cs25
-rw-r--r--test/System.Web.Mvc.Test/Test/HttpPostedFileBaseModelBinderTest.cs90
-rw-r--r--test/System.Web.Mvc.Test/Test/HttpPutAttributeTest.cs25
-rw-r--r--test/System.Web.Mvc.Test/Test/HttpRequestExtensionsTest.cs50
-rw-r--r--test/System.Web.Mvc.Test/Test/HttpStatusCodeResultTest.cs96
-rw-r--r--test/System.Web.Mvc.Test/Test/HttpUnauthorizedResultTest.cs31
-rw-r--r--test/System.Web.Mvc.Test/Test/HttpVerbAttributeHelper.cs46
-rw-r--r--test/System.Web.Mvc.Test/Test/JavaScriptResultTest.cs69
-rw-r--r--test/System.Web.Mvc.Test/Test/JsonResultTest.cs292
-rw-r--r--test/System.Web.Mvc.Test/Test/JsonValueProviderFactoryTest.cs152
-rw-r--r--test/System.Web.Mvc.Test/Test/LinqBinaryModelBinderTest.cs120
-rw-r--r--test/System.Web.Mvc.Test/Test/MockBuildManager.cs80
-rw-r--r--test/System.Web.Mvc.Test/Test/MockHelpers.cs13
-rw-r--r--test/System.Web.Mvc.Test/Test/MockableUnvalidatedRequestValues.cs11
-rw-r--r--test/System.Web.Mvc.Test/Test/ModelBinderAttributeTest.cs85
-rw-r--r--test/System.Web.Mvc.Test/Test/ModelBinderDictionaryTest.cs191
-rw-r--r--test/System.Web.Mvc.Test/Test/ModelBinderProviderCollectionTest.cs113
-rw-r--r--test/System.Web.Mvc.Test/Test/ModelBinderProvidersTest.cs18
-rw-r--r--test/System.Web.Mvc.Test/Test/ModelBindersTest.cs65
-rw-r--r--test/System.Web.Mvc.Test/Test/ModelBindingContextTest.cs122
-rw-r--r--test/System.Web.Mvc.Test/Test/ModelClientValidationRuleTest.cs33
-rw-r--r--test/System.Web.Mvc.Test/Test/ModelErrorCollectionTest.cs37
-rw-r--r--test/System.Web.Mvc.Test/Test/ModelErrorTest.cs65
-rw-r--r--test/System.Web.Mvc.Test/Test/ModelMetadataProvidersTest.cs68
-rw-r--r--test/System.Web.Mvc.Test/Test/ModelMetadataTest.cs892
-rw-r--r--test/System.Web.Mvc.Test/Test/ModelStateDictionaryTest.cs309
-rw-r--r--test/System.Web.Mvc.Test/Test/ModelStateTest.cs17
-rw-r--r--test/System.Web.Mvc.Test/Test/ModelValidationResultTest.cs28
-rw-r--r--test/System.Web.Mvc.Test/Test/ModelValidatorProviderCollectionTest.cs185
-rw-r--r--test/System.Web.Mvc.Test/Test/ModelValidatorProvidersTest.cs26
-rw-r--r--test/System.Web.Mvc.Test/Test/ModelValidatorTest.cs233
-rw-r--r--test/System.Web.Mvc.Test/Test/MultiSelectListTest.cs307
-rw-r--r--test/System.Web.Mvc.Test/Test/MultiServiceResolverTest.cs136
-rw-r--r--test/System.Web.Mvc.Test/Test/MvcHandlerTest.cs360
-rw-r--r--test/System.Web.Mvc.Test/Test/MvcHtmlStringTest.cs62
-rw-r--r--test/System.Web.Mvc.Test/Test/MvcHttpHandlerTest.cs63
-rw-r--r--test/System.Web.Mvc.Test/Test/MvcRouteHandlerTest.cs71
-rw-r--r--test/System.Web.Mvc.Test/Test/MvcTestHelper.cs102
-rw-r--r--test/System.Web.Mvc.Test/Test/MvcWebRazorHostFactoryTest.cs56
-rw-r--r--test/System.Web.Mvc.Test/Test/NameValueCollectionExtensionsTest.cs76
-rw-r--r--test/System.Web.Mvc.Test/Test/NameValueCollectionValueProviderTest.cs143
-rw-r--r--test/System.Web.Mvc.Test/Test/NoAsyncTimeoutAttributeTest.cs18
-rw-r--r--test/System.Web.Mvc.Test/Test/NonActionAttributeTest.cs17
-rw-r--r--test/System.Web.Mvc.Test/Test/OutputCacheAttributeTest.cs279
-rw-r--r--test/System.Web.Mvc.Test/Test/ParameterBindingInfoTest.cs60
-rw-r--r--test/System.Web.Mvc.Test/Test/ParameterDescriptorTest.cs114
-rw-r--r--test/System.Web.Mvc.Test/Test/ParameterInfoUtilTest.cs182
-rw-r--r--test/System.Web.Mvc.Test/Test/PartialViewResultTest.cs197
-rw-r--r--test/System.Web.Mvc.Test/Test/PathHelpersTest.cs247
-rw-r--r--test/System.Web.Mvc.Test/Test/PreApplicationStartCodeTest.cs26
-rw-r--r--test/System.Web.Mvc.Test/Test/QueryStringValueProviderFactoryTest.cs77
-rw-r--r--test/System.Web.Mvc.Test/Test/RangeAttributeAdapterTest.cs32
-rw-r--r--test/System.Web.Mvc.Test/Test/RazorViewEngineTest.cs253
-rw-r--r--test/System.Web.Mvc.Test/Test/RazorViewTest.cs248
-rw-r--r--test/System.Web.Mvc.Test/Test/ReaderWriterCacheTest.cs76
-rw-r--r--test/System.Web.Mvc.Test/Test/RedirectResultTest.cs125
-rw-r--r--test/System.Web.Mvc.Test/Test/RedirectToRouteResultTest.cs209
-rw-r--r--test/System.Web.Mvc.Test/Test/ReflectedActionDescriptorTest.cs491
-rw-r--r--test/System.Web.Mvc.Test/Test/ReflectedControllerDescriptorTest.cs198
-rw-r--r--test/System.Web.Mvc.Test/Test/ReflectedParameterBindingInfoTest.cs172
-rw-r--r--test/System.Web.Mvc.Test/Test/ReflectedParameterDescriptorTest.cs159
-rw-r--r--test/System.Web.Mvc.Test/Test/RegularExpressionAttributeAdapterTest.cs31
-rw-r--r--test/System.Web.Mvc.Test/Test/RemoteAttributeTest.cs172
-rw-r--r--test/System.Web.Mvc.Test/Test/RequireHttpsAttributeTest.cs106
-rw-r--r--test/System.Web.Mvc.Test/Test/RequiredAttributeAdapterTest.cs30
-rw-r--r--test/System.Web.Mvc.Test/Test/ResultExecutedContextTest.cs55
-rw-r--r--test/System.Web.Mvc.Test/Test/ResultExecutingContextTest.cs47
-rw-r--r--test/System.Web.Mvc.Test/Test/RouteCollectionExtensionsTest.cs388
-rw-r--r--test/System.Web.Mvc.Test/Test/RouteDataValueProviderFactoryTest.cs44
-rw-r--r--test/System.Web.Mvc.Test/Test/SelectListTest.cs84
-rw-r--r--test/System.Web.Mvc.Test/Test/SessionStateTempDataProviderTest.cs166
-rw-r--r--test/System.Web.Mvc.Test/Test/SingleServiceResolverTest.cs163
-rw-r--r--test/System.Web.Mvc.Test/Test/StringLengthAttributeAdapterTest.cs32
-rw-r--r--test/System.Web.Mvc.Test/Test/TempDataDictionaryTest.cs340
-rw-r--r--test/System.Web.Mvc.Test/Test/TypeCacheSerializerTest.cs165
-rw-r--r--test/System.Web.Mvc.Test/Test/TypeCacheUtilTest.cs149
-rw-r--r--test/System.Web.Mvc.Test/Test/TypeHelpersTest.cs242
-rw-r--r--test/System.Web.Mvc.Test/Test/UrlHelperTest.cs694
-rw-r--r--test/System.Web.Mvc.Test/Test/UrlParameterTest.cs14
-rw-r--r--test/System.Web.Mvc.Test/Test/UrlRewriterHelperTest.cs83
-rw-r--r--test/System.Web.Mvc.Test/Test/ValidatableObjectAdapterTest.cs172
-rw-r--r--test/System.Web.Mvc.Test/Test/ValidateAntiForgeryTokenAttributeTest.cs68
-rw-r--r--test/System.Web.Mvc.Test/Test/ValidateInputAttributeTest.cs73
-rw-r--r--test/System.Web.Mvc.Test/Test/ValueProviderCollectionTest.cs246
-rw-r--r--test/System.Web.Mvc.Test/Test/ValueProviderDictionaryTest.cs204
-rw-r--r--test/System.Web.Mvc.Test/Test/ValueProviderFactoriesTest.cs29
-rw-r--r--test/System.Web.Mvc.Test/Test/ValueProviderFactoryCollectionTest.cs143
-rw-r--r--test/System.Web.Mvc.Test/Test/ValueProviderResultTest.cs398
-rw-r--r--test/System.Web.Mvc.Test/Test/ValueProviderUtilTest.cs185
-rw-r--r--test/System.Web.Mvc.Test/Test/ViewContextTest.cs182
-rw-r--r--test/System.Web.Mvc.Test/Test/ViewDataDictionaryTest.cs536
-rw-r--r--test/System.Web.Mvc.Test/Test/ViewDataInfoTest.cs64
-rw-r--r--test/System.Web.Mvc.Test/Test/ViewEngineCollectionTest.cs631
-rw-r--r--test/System.Web.Mvc.Test/Test/ViewEngineResultTest.cs80
-rw-r--r--test/System.Web.Mvc.Test/Test/ViewEnginesTest.cs19
-rw-r--r--test/System.Web.Mvc.Test/Test/ViewMasterPageControlBuilderTest.cs39
-rw-r--r--test/System.Web.Mvc.Test/Test/ViewMasterPageTest.cs245
-rw-r--r--test/System.Web.Mvc.Test/Test/ViewPageControlBuilderTest.cs39
-rw-r--r--test/System.Web.Mvc.Test/Test/ViewPageTest.cs262
-rw-r--r--test/System.Web.Mvc.Test/Test/ViewResultBaseTest.cs72
-rw-r--r--test/System.Web.Mvc.Test/Test/ViewResultTest.cs222
-rw-r--r--test/System.Web.Mvc.Test/Test/ViewStartPageTest.cs110
-rw-r--r--test/System.Web.Mvc.Test/Test/ViewTypeParserFilterTest.cs208
-rw-r--r--test/System.Web.Mvc.Test/Test/ViewUserControlControlBuilderTest.cs39
-rw-r--r--test/System.Web.Mvc.Test/Test/ViewUserControlTest.cs538
-rw-r--r--test/System.Web.Mvc.Test/Test/VirtualPathProviderViewEngineTest.cs1412
-rw-r--r--test/System.Web.Mvc.Test/Test/WebFormViewEngineTest.cs244
-rw-r--r--test/System.Web.Mvc.Test/Test/WebFormViewTest.cs164
-rw-r--r--test/System.Web.Mvc.Test/Util/AnonymousObject.cs29
-rw-r--r--test/System.Web.Mvc.Test/Util/DictionaryHelper.cs517
-rw-r--r--test/System.Web.Mvc.Test/Util/HttpContextHelpers.cs15
-rw-r--r--test/System.Web.Mvc.Test/Util/MvcHelper.cs174
-rw-r--r--test/System.Web.Mvc.Test/Util/Resolver.cs7
-rw-r--r--test/System.Web.Mvc.Test/Util/SimpleValueProvider.cs73
-rw-r--r--test/System.Web.Mvc.Test/Util/SimpleViewDataContainer.cs14
-rw-r--r--test/System.Web.Mvc.Test/packages.config6
-rw-r--r--test/System.Web.Razor.Test/CSharpRazorCodeLanguageTest.cs53
-rw-r--r--test/System.Web.Razor.Test/CodeCompileUnitExtensions.cs22
-rw-r--r--test/System.Web.Razor.Test/Editor/RazorEditorParserTest.cs216
-rw-r--r--test/System.Web.Razor.Test/Framework/BlockExtensions.cs27
-rw-r--r--test/System.Web.Razor.Test/Framework/BlockTypes.cs233
-rw-r--r--test/System.Web.Razor.Test/Framework/CodeParserTestBase.cs74
-rw-r--r--test/System.Web.Razor.Test/Framework/CsHtmlCodeParserTestBase.cs28
-rw-r--r--test/System.Web.Razor.Test/Framework/CsHtmlMarkupParserTestBase.cs28
-rw-r--r--test/System.Web.Razor.Test/Framework/ErrorCollector.cs54
-rw-r--r--test/System.Web.Razor.Test/Framework/MarkupParserTestBase.cs59
-rw-r--r--test/System.Web.Razor.Test/Framework/ParserTestBase.cs381
-rw-r--r--test/System.Web.Razor.Test/Framework/RawTextSymbol.cs71
-rw-r--r--test/System.Web.Razor.Test/Framework/TestSpanBuilder.cs405
-rw-r--r--test/System.Web.Razor.Test/Framework/VBHtmlCodeParserTestBase.cs28
-rw-r--r--test/System.Web.Razor.Test/Framework/VBHtmlMarkupParserTestBase.cs28
-rw-r--r--test/System.Web.Razor.Test/Generator/CSharpRazorCodeGeneratorTest.cs290
-rw-r--r--test/System.Web.Razor.Test/Generator/GeneratedCodeMappingTest.cs63
-rw-r--r--test/System.Web.Razor.Test/Generator/RazorCodeGeneratorTest.cs146
-rw-r--r--test/System.Web.Razor.Test/Generator/VBRazorCodeGeneratorTest.cs279
-rw-r--r--test/System.Web.Razor.Test/Parser/BlockTest.cs122
-rw-r--r--test/System.Web.Razor.Test/Parser/CSharp/CSharpAutoCompleteTest.cs172
-rw-r--r--test/System.Web.Razor.Test/Parser/CSharp/CSharpBlockTest.cs731
-rw-r--r--test/System.Web.Razor.Test/Parser/CSharp/CSharpDirectivesTest.cs161
-rw-r--r--test/System.Web.Razor.Test/Parser/CSharp/CSharpErrorTest.cs604
-rw-r--r--test/System.Web.Razor.Test/Parser/CSharp/CSharpExplicitExpressionTest.cs139
-rw-r--r--test/System.Web.Razor.Test/Parser/CSharp/CSharpHelperTest.cs345
-rw-r--r--test/System.Web.Razor.Test/Parser/CSharp/CSharpImplicitExpressionTest.cs201
-rw-r--r--test/System.Web.Razor.Test/Parser/CSharp/CSharpLayoutDirectiveTest.cs84
-rw-r--r--test/System.Web.Razor.Test/Parser/CSharp/CSharpNestedStatementsTest.cs100
-rw-r--r--test/System.Web.Razor.Test/Parser/CSharp/CSharpRazorCommentsTest.cs172
-rw-r--r--test/System.Web.Razor.Test/Parser/CSharp/CSharpReservedWordsTest.cs41
-rw-r--r--test/System.Web.Razor.Test/Parser/CSharp/CSharpSectionTest.cs298
-rw-r--r--test/System.Web.Razor.Test/Parser/CSharp/CSharpSpecialBlockTest.cs195
-rw-r--r--test/System.Web.Razor.Test/Parser/CSharp/CSharpStatementTest.cs208
-rw-r--r--test/System.Web.Razor.Test/Parser/CSharp/CSharpTemplateTest.cs258
-rw-r--r--test/System.Web.Razor.Test/Parser/CSharp/CSharpToMarkupSwitchTest.cs491
-rw-r--r--test/System.Web.Razor.Test/Parser/CSharp/CSharpVerbatimBlockTest.cs118
-rw-r--r--test/System.Web.Razor.Test/Parser/CSharp/CSharpWhitespaceHandlingTest.cs30
-rw-r--r--test/System.Web.Razor.Test/Parser/CSharp/CsHtmlDocumentTest.cs286
-rw-r--r--test/System.Web.Razor.Test/Parser/CallbackParserListenerTest.cs162
-rw-r--r--test/System.Web.Razor.Test/Parser/Html/HtmlAttributeTest.cs268
-rw-r--r--test/System.Web.Razor.Test/Parser/Html/HtmlBlockTest.cs388
-rw-r--r--test/System.Web.Razor.Test/Parser/Html/HtmlDocumentTest.cs214
-rw-r--r--test/System.Web.Razor.Test/Parser/Html/HtmlErrorTest.cs87
-rw-r--r--test/System.Web.Razor.Test/Parser/Html/HtmlParserTestUtils.cs37
-rw-r--r--test/System.Web.Razor.Test/Parser/Html/HtmlTagsTest.cs150
-rw-r--r--test/System.Web.Razor.Test/Parser/Html/HtmlToCodeSwitchTest.cs222
-rw-r--r--test/System.Web.Razor.Test/Parser/Html/HtmlUrlAttributeTest.cs270
-rw-r--r--test/System.Web.Razor.Test/Parser/Old/CsHtmlDocumentTest.cs267
-rw-r--r--test/System.Web.Razor.Test/Parser/ParserContextTest.cs240
-rw-r--r--test/System.Web.Razor.Test/Parser/ParserVisitorExtensionsTest.cs82
-rw-r--r--test/System.Web.Razor.Test/Parser/PartialParsing/CSharpPartialParsingTest.cs400
-rw-r--r--test/System.Web.Razor.Test/Parser/PartialParsing/PartialParsingTestBase.cs111
-rw-r--r--test/System.Web.Razor.Test/Parser/PartialParsing/VBPartialParsingTest.cs372
-rw-r--r--test/System.Web.Razor.Test/Parser/RazorParserTest.cs134
-rw-r--r--test/System.Web.Razor.Test/Parser/VB/VBAutoCompleteTest.cs153
-rw-r--r--test/System.Web.Razor.Test/Parser/VB/VBBlockTest.cs372
-rw-r--r--test/System.Web.Razor.Test/Parser/VB/VBContinueStatementTest.cs56
-rw-r--r--test/System.Web.Razor.Test/Parser/VB/VBDirectiveTest.cs127
-rw-r--r--test/System.Web.Razor.Test/Parser/VB/VBErrorTest.cs172
-rw-r--r--test/System.Web.Razor.Test/Parser/VB/VBExitStatementTest.cs94
-rw-r--r--test/System.Web.Razor.Test/Parser/VB/VBExplicitExpressionTest.cs20
-rw-r--r--test/System.Web.Razor.Test/Parser/VB/VBExpressionTest.cs109
-rw-r--r--test/System.Web.Razor.Test/Parser/VB/VBExpressionsInCodeTest.cs119
-rw-r--r--test/System.Web.Razor.Test/Parser/VB/VBHelperTest.cs322
-rw-r--r--test/System.Web.Razor.Test/Parser/VB/VBHtmlDocumentTest.cs248
-rw-r--r--test/System.Web.Razor.Test/Parser/VB/VBImplicitExpressionTest.cs77
-rw-r--r--test/System.Web.Razor.Test/Parser/VB/VBLayoutDirectiveTest.cs86
-rw-r--r--test/System.Web.Razor.Test/Parser/VB/VBNestedStatementsTest.cs167
-rw-r--r--test/System.Web.Razor.Test/Parser/VB/VBRazorCommentsTest.cs172
-rw-r--r--test/System.Web.Razor.Test/Parser/VB/VBReservedWordsTest.cs28
-rw-r--r--test/System.Web.Razor.Test/Parser/VB/VBSectionTest.cs225
-rw-r--r--test/System.Web.Razor.Test/Parser/VB/VBSpecialKeywordsTest.cs169
-rw-r--r--test/System.Web.Razor.Test/Parser/VB/VBStatementTest.cs218
-rw-r--r--test/System.Web.Razor.Test/Parser/VB/VBTemplateTest.cs211
-rw-r--r--test/System.Web.Razor.Test/Parser/VB/VBToMarkupSwitchTest.cs103
-rw-r--r--test/System.Web.Razor.Test/Parser/WhitespaceRewriterTest.cs50
-rw-r--r--test/System.Web.Razor.Test/Properties/AssemblyInfo.cs34
-rw-r--r--test/System.Web.Razor.Test/RazorCodeLanguageTest.cs47
-rw-r--r--test/System.Web.Razor.Test/RazorDirectiveAttributeTest.cs79
-rw-r--r--test/System.Web.Razor.Test/RazorEngineHostTest.cs186
-rw-r--r--test/System.Web.Razor.Test/RazorTemplateEngineTest.cs197
-rw-r--r--test/System.Web.Razor.Test/StringTextBuffer.cs50
-rw-r--r--test/System.Web.Razor.Test/System.Web.Razor.Test.csproj464
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Blocks.cs194
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/CodeBlock.cs31
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/CodeBlockAtEOF.cs27
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Comments.cs38
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/ConditionalAttributes.cs197
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/DesignTime.cs108
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/EmptyCodeBlock.cs27
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/EmptyExplicitExpression.cs29
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/EmptyImplicitExpression.cs29
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/EmptyImplicitExpressionInCode.cs43
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/ExplicitExpression.cs30
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/ExplicitExpressionAtEOF.cs29
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/ExpressionsInCode.cs89
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/FunctionsBlock.DesignTime.cs46
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/FunctionsBlock.cs49
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Helpers.Instance.cs96
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Helpers.cs96
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/HelpersMissingCloseParen.cs61
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/HelpersMissingName.cs21
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/HelpersMissingOpenBrace.cs67
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/HelpersMissingOpenParen.cs67
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/HiddenSpansInCode.cs35
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/ImplicitExpression.cs45
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/ImplicitExpressionAtEOF.cs29
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Imports.DesignTime.cs53
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Imports.cs58
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Inherits.Designtime.cs40
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Inherits.Runtime.cs30
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/InlineBlocks.cs72
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Instrumented.cs348
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/LayoutDirective.cs22
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/MarkupInCodeBlock.cs49
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/NestedCodeBlocks.cs42
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/NestedHelpers.cs100
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/NoLinePragmas.cs102
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/ParserError.cs31
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/RazorComments.DesignTime.cs72
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/RazorComments.cs83
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/ResolveUrl.cs230
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Sections.cs45
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Templates.cs180
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/UnfinishedExpressionInCode.cs43
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/Blocks.cshtml37
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/CodeBlock.cshtml5
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/CodeBlockAtEOF.cshtml1
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/ConditionalAttributes.cshtml15
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/DesignTime.cshtml21
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/EmptyCodeBlock.cshtml3
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/EmptyExplicitExpression.cshtml3
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/EmptyImplicitExpression.cshtml3
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/EmptyImplicitExpressionInCode.cshtml3
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/ExplicitExpression.cshtml1
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/ExplicitExpressionAtEOF.cshtml3
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/ExpressionsInCode.cshtml16
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/FunctionsBlock.cshtml12
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/Helpers.cshtml11
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/HelpersMissingCloseParen.cshtml7
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/HelpersMissingName.cshtml1
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/HelpersMissingOpenBrace.cshtml7
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/HelpersMissingOpenParen.cshtml7
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/HiddenSpansInCode.cshtml3
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/ImplicitExpression.cshtml3
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/ImplicitExpressionAtEOF.cshtml3
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/Imports.cshtml6
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/Inherits.cshtml3
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/InlineBlocks.cshtml3
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/Instrumented.cshtml42
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/LayoutDirective.cshtml1
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/MarkupInCodeBlock.cshtml5
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/NestedCodeBlocks.cshtml4
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/NestedHelpers.cshtml10
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/NoLinePragmas.cshtml38
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/ParserError.cshtml5
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/RazorComments.cshtml15
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/ResolveUrl.cshtml20
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/Sections.cshtml13
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/Templates.cshtml35
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/UnfinishedExpressionInCode.cshtml3
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Blocks.vb255
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/CodeBlock.vb31
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/CodeBlockAtEOF.vb29
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Comments.vb51
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/ConditionalAttributes.vb146
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/DesignTime.vb99
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/EmptyExplicitExpression.vb31
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/EmptyImplicitExpression.vb31
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/EmptyImplicitExpressionInCode.vb43
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/ExplicitExpression.vb30
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/ExplicitExpressionAtEOF.vb31
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/ExpressionsInCode.vb86
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/FunctionsBlock.DesignTime.vb47
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/FunctionsBlock.vb50
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Helpers.Instance.vb87
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Helpers.vb87
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/HelpersMissingCloseParen.vb60
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/HelpersMissingName.vb24
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/HelpersMissingOpenParen.vb66
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/ImplicitExpression.vb56
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/ImplicitExpressionAtEOF.vb31
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Imports.DesignTime.vb41
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Imports.vb46
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Inherits.Designtime.vb35
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Inherits.Runtime.vb25
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Instrumented.vb497
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/LayoutDirective.vb25
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/MarkupInCodeBlock.vb51
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/NestedCodeBlocks.vb42
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/NestedHelpers.vb90
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/NoLinePragmas.vb143
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Options.vb28
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/ParserError.vb31
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/RazorComments.DesignTime.vb59
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/RazorComments.vb72
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/ResolveUrl.vb183
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Sections.vb47
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Templates.vb169
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/UnfinishedExpressionInCode.vb43
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/Blocks.vbhtml46
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/CodeBlock.vbhtml3
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/CodeBlockAtEOF.vbhtml3
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/ConditionalAttributes.vbhtml12
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/DesignTime.vbhtml21
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/EmptyExplicitExpression.vbhtml3
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/EmptyImplicitExpression.vbhtml3
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/EmptyImplicitExpressionInCode.vbhtml3
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/ExplicitExpression.vbhtml1
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/ExplicitExpressionAtEOF.vbhtml3
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/ExpressionsInCode.vbhtml16
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/FunctionsBlock.vbhtml12
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/Helpers.vbhtml11
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/HelpersMissingCloseParen.vbhtml8
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/HelpersMissingName.vbhtml1
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/HelpersMissingOpenParen.vbhtml8
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/ImplicitExpression.vbhtml5
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/ImplicitExpressionAtEOF.vbhtml3
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/Imports.vbhtml6
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/Inherits.vbhtml1
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/Instrumented.vbhtml53
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/LayoutDirective.vbhtml1
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/MarkupInCodeBlock.vbhtml5
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/NestedCodeBlocks.vbhtml4
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/NestedHelpers.vbhtml10
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/NoLinePragmas.vbhtml45
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/Options.vbhtml4
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/ParserError.vbhtml3
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/RazorComments.vbhtml12
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/ResolveUrl.vbhtml20
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/Sections.vbhtml13
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/Templates.vbhtml36
-rw-r--r--test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/UnfinishedExpressionInCode.vbhtml3
-rw-r--r--test/System.Web.Razor.Test/TestFiles/DesignTime/Simple.cshtml16
-rw-r--r--test/System.Web.Razor.Test/TestFiles/DesignTime/Simple.txt60
-rw-r--r--test/System.Web.Razor.Test/TestFiles/nested-1000.html2002
-rw-r--r--test/System.Web.Razor.Test/Text/BufferingTextReaderTest.cs265
-rw-r--r--test/System.Web.Razor.Test/Text/LineTrackingStringBufferTest.cs25
-rw-r--r--test/System.Web.Razor.Test/Text/LookaheadTextReaderTestBase.cs252
-rw-r--r--test/System.Web.Razor.Test/Text/SourceLocationTest.cs20
-rw-r--r--test/System.Web.Razor.Test/Text/SourceLocationTrackerTest.cs179
-rw-r--r--test/System.Web.Razor.Test/Text/TextBufferReaderTest.cs229
-rw-r--r--test/System.Web.Razor.Test/Text/TextChangeTest.cs249
-rw-r--r--test/System.Web.Razor.Test/Text/TextReaderExtensionsTest.cs184
-rw-r--r--test/System.Web.Razor.Test/Tokenizer/CSharpTokenizerCommentTest.cs87
-rw-r--r--test/System.Web.Razor.Test/Tokenizer/CSharpTokenizerIdentifierTest.cs167
-rw-r--r--test/System.Web.Razor.Test/Tokenizer/CSharpTokenizerLiteralTest.cs222
-rw-r--r--test/System.Web.Razor.Test/Tokenizer/CSharpTokenizerOperatorsTest.cs294
-rw-r--r--test/System.Web.Razor.Test/Tokenizer/CSharpTokenizerTest.cs102
-rw-r--r--test/System.Web.Razor.Test/Tokenizer/CSharpTokenizerTestBase.cs26
-rw-r--r--test/System.Web.Razor.Test/Tokenizer/HtmlTokenizerTest.cs166
-rw-r--r--test/System.Web.Razor.Test/Tokenizer/HtmlTokenizerTestBase.cs26
-rw-r--r--test/System.Web.Razor.Test/Tokenizer/TokenizerLookaheadTest.cs39
-rw-r--r--test/System.Web.Razor.Test/Tokenizer/TokenizerTestBase.cs74
-rw-r--r--test/System.Web.Razor.Test/Tokenizer/VBTokenizerCommentTest.cs91
-rw-r--r--test/System.Web.Razor.Test/Tokenizer/VBTokenizerIdentifierTest.cs265
-rw-r--r--test/System.Web.Razor.Test/Tokenizer/VBTokenizerLiteralTest.cs180
-rw-r--r--test/System.Web.Razor.Test/Tokenizer/VBTokenizerOperatorsTest.cs152
-rw-r--r--test/System.Web.Razor.Test/Tokenizer/VBTokenizerTest.cs94
-rw-r--r--test/System.Web.Razor.Test/Tokenizer/VBTokenizerTestBase.cs26
-rw-r--r--test/System.Web.Razor.Test/Utils/DisposableActionTest.cs29
-rw-r--r--test/System.Web.Razor.Test/Utils/EnumerableUtils.cs17
-rw-r--r--test/System.Web.Razor.Test/Utils/MiscAssert.cs31
-rw-r--r--test/System.Web.Razor.Test/Utils/MiscUtils.cs33
-rw-r--r--test/System.Web.Razor.Test/Utils/SpanAssert.cs56
-rw-r--r--test/System.Web.Razor.Test/VBRazorCodeLanguageTest.cs53
-rw-r--r--test/System.Web.Razor.Test/packages.config6
-rw-r--r--test/System.Web.WebPages.Administration.Test/AdminPackageTest.cs397
-rw-r--r--test/System.Web.WebPages.Administration.Test/PackageManagerModuleTest.cs150
-rw-r--r--test/System.Web.WebPages.Administration.Test/PackagesSourceFileTest.cs146
-rw-r--r--test/System.Web.WebPages.Administration.Test/PageUtilsTest.cs153
-rw-r--r--test/System.Web.WebPages.Administration.Test/PreApplicationStartCodeTest.cs33
-rw-r--r--test/System.Web.WebPages.Administration.Test/RemoteAssemblyTest.cs147
-rw-r--r--test/System.Web.WebPages.Administration.Test/System.Web.WebPages.Administration.Test.csproj102
-rw-r--r--test/System.Web.WebPages.Administration.Test/WebProjectManagerTest.cs160
-rw-r--r--test/System.Web.WebPages.Administration.Test/WebProjectSystemTest.cs253
-rw-r--r--test/System.Web.WebPages.Administration.Test/packages.config7
-rw-r--r--test/System.Web.WebPages.Deployment.Test/App.Config3
-rw-r--r--test/System.Web.WebPages.Deployment.Test/AssemblyUtilsTest.cs266
-rw-r--r--test/System.Web.WebPages.Deployment.Test/DeploymentUtil.cs13
-rw-r--r--test/System.Web.WebPages.Deployment.Test/PreApplicationStartCodeTest.cs510
-rw-r--r--test/System.Web.WebPages.Deployment.Test/Properties/AssemblyInfo.cs34
-rw-r--r--test/System.Web.WebPages.Deployment.Test/System.Web.WebPages.Deployment.Test.csproj97
-rw-r--r--test/System.Web.WebPages.Deployment.Test/TestFileSystem.cs50
-rw-r--r--test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestAssemblies/V2_Signed/System.Web.WebPages.Deployment.dllbin0 -> 3584 bytes
-rw-r--r--test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestAssemblies/V2_Unsigned/System.Web.WebPages.Deployment.dllbin0 -> 3072 bytes
-rw-r--r--test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/CshtmlFileConfigV1/Default.cshtml9
-rw-r--r--test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/CshtmlFileConfigV1/web.config6
-rw-r--r--test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/CshtmlFileNoVersion/Default.cshtml9
-rw-r--r--test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/NoCshtml/Default.htm9
-rw-r--r--test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/NoCshtmlConfigv1/Default.htm9
-rw-r--r--test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/NoCshtmlConfigv1/web.config6
-rw-r--r--test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/NoCshtmlNoConfigSetting/Default.htm9
-rw-r--r--test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/NoCshtmlNoConfigSetting/web.config3
-rw-r--r--test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/NoCshtmlWithEnabledSetting/Default.htm9
-rw-r--r--test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/NoCshtmlWithEnabledSetting/web.config6
-rw-r--r--test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/NoCshtmlWithEnabledSettingFalse/Default.htm9
-rw-r--r--test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/NoCshtmlWithEnabledSettingFalse/web.config6
-rw-r--r--test/System.Web.WebPages.Deployment.Test/WebPagesDeploymentTest.cs303
-rw-r--r--test/System.Web.WebPages.Deployment.Test/packages.config5
-rw-r--r--test/System.Web.WebPages.Razor.Test/PreApplicationStartCodeTest.cs28
-rw-r--r--test/System.Web.WebPages.Razor.Test/Properties/AssemblyInfo.cs34
-rw-r--r--test/System.Web.WebPages.Razor.Test/RazorBuildProviderTest.cs248
-rw-r--r--test/System.Web.WebPages.Razor.Test/System.Web.WebPages.Razor.Test.csproj92
-rw-r--r--test/System.Web.WebPages.Razor.Test/WebCodeRazorEngineHostTest.cs110
-rw-r--r--test/System.Web.WebPages.Razor.Test/WebPageRazorEngineHostTest.cs100
-rw-r--r--test/System.Web.WebPages.Razor.Test/WebRazorHostFactoryTest.cs387
-rw-r--r--test/System.Web.WebPages.Razor.Test/app.config21
-rw-r--r--test/System.Web.WebPages.Razor.Test/packages.config6
-rw-r--r--test/System.Web.WebPages.Test/App.config18
-rw-r--r--test/System.Web.WebPages.Test/ApplicationParts/ApplicationPartRegistryTest.cs150
-rw-r--r--test/System.Web.WebPages.Test/ApplicationParts/ApplicationPartTest.cs198
-rw-r--r--test/System.Web.WebPages.Test/ApplicationParts/MimeMappingTest.cs61
-rw-r--r--test/System.Web.WebPages.Test/ApplicationParts/ResourceHandlerTest.cs62
-rw-r--r--test/System.Web.WebPages.Test/ApplicationParts/TestResourceAssembly.cs17
-rw-r--r--test/System.Web.WebPages.Test/Extensions/HttpContextExtensionsTest.cs83
-rw-r--r--test/System.Web.WebPages.Test/Extensions/HttpRequestExtensionsTest.cs130
-rw-r--r--test/System.Web.WebPages.Test/Extensions/HttpResponseExtensionsTest.cs128
-rw-r--r--test/System.Web.WebPages.Test/Extensions/StringExtensionsTest.cs251
-rw-r--r--test/System.Web.WebPages.Test/Helpers/AntiForgeryDataSerializerTest.cs103
-rw-r--r--test/System.Web.WebPages.Test/Helpers/AntiForgeryDataTest.cs168
-rw-r--r--test/System.Web.WebPages.Test/Helpers/AntiForgeryTest.cs36
-rw-r--r--test/System.Web.WebPages.Test/Helpers/AntiForgeryWorkerTest.cs237
-rw-r--r--test/System.Web.WebPages.Test/Helpers/UnvalidatedRequestValuesTest.cs78
-rw-r--r--test/System.Web.WebPages.Test/Html/CheckBoxTest.cs272
-rw-r--r--test/System.Web.WebPages.Test/Html/HtmlHelperFactory.cs16
-rw-r--r--test/System.Web.WebPages.Test/Html/HtmlHelperTest.cs159
-rw-r--r--test/System.Web.WebPages.Test/Html/InputHelperTest.cs584
-rw-r--r--test/System.Web.WebPages.Test/Html/RadioButtonTest.cs197
-rw-r--r--test/System.Web.WebPages.Test/Html/SelectHelperTest.cs776
-rw-r--r--test/System.Web.WebPages.Test/Html/TextAreaHelperTest.cs209
-rw-r--r--test/System.Web.WebPages.Test/Html/ValidationHelperTest.cs342
-rw-r--r--test/System.Web.WebPages.Test/Instrumentation/InstrumentationServiceTest.cs97
-rw-r--r--test/System.Web.WebPages.Test/Mvc/HttpAntiForgeryExceptionTest.cs64
-rw-r--r--test/System.Web.WebPages.Test/Mvc/TagBuilderTest.cs416
-rw-r--r--test/System.Web.WebPages.Test/PreApplicationStartCodeTest.cs38
-rw-r--r--test/System.Web.WebPages.Test/Properties/AssemblyInfo.cs34
-rw-r--r--test/System.Web.WebPages.Test/ScopeStorage/AspNetRequestScopeStorageProviderTest.cs93
-rw-r--r--test/System.Web.WebPages.Test/ScopeStorage/ScopeStorageDictionaryTest.cs170
-rw-r--r--test/System.Web.WebPages.Test/ScopeStorage/ScopeStorageKeyComparerTest.cs48
-rw-r--r--test/System.Web.WebPages.Test/ScopeStorage/WebConfigScopeStorageTest.cs78
-rw-r--r--test/System.Web.WebPages.Test/System.Web.WebPages.Test.csproj162
-rw-r--r--test/System.Web.WebPages.Test/TestFiles/Deployed/Bar1
-rw-r--r--test/System.Web.WebPages.Test/TestFiles/Deployed/Bar.cshtml1
-rw-r--r--test/System.Web.WebPages.Test/TestFiles/Deployed/Bar.foohtml1
-rw-r--r--test/System.Web.WebPages.Test/Utils/CultureUtilTest.cs256
-rw-r--r--test/System.Web.WebPages.Test/Utils/PathUtilTest.cs177
-rw-r--r--test/System.Web.WebPages.Test/Utils/SessionStateUtilTest.cs183
-rw-r--r--test/System.Web.WebPages.Test/Utils/TestObjectFactory.cs30
-rw-r--r--test/System.Web.WebPages.Test/Utils/TypeHelperTest.cs111
-rw-r--r--test/System.Web.WebPages.Test/Utils/UrlUtilTest.cs207
-rw-r--r--test/System.Web.WebPages.Test/Validation/ValidationHelperTest.cs835
-rw-r--r--test/System.Web.WebPages.Test/Validation/ValidatorTest.cs882
-rw-r--r--test/System.Web.WebPages.Test/WebPage/ApplicationStartPageTest.cs166
-rw-r--r--test/System.Web.WebPages.Test/WebPage/BrowserHelpersTest.cs285
-rw-r--r--test/System.Web.WebPages.Test/WebPage/BrowserOverrideStoresTest.cs42
-rw-r--r--test/System.Web.WebPages.Test/WebPage/BuildManagerExceptionUtilTest.cs90
-rw-r--r--test/System.Web.WebPages.Test/WebPage/BuildManagerWrapperTest.cs277
-rw-r--r--test/System.Web.WebPages.Test/WebPage/CookieBrowserOverrideStoreTest.cs153
-rw-r--r--test/System.Web.WebPages.Test/WebPage/DefaultDisplayModeTest.cs139
-rw-r--r--test/System.Web.WebPages.Test/WebPage/DisplayInfoTest.cs37
-rw-r--r--test/System.Web.WebPages.Test/WebPage/DisplayModeProviderTest.cs229
-rw-r--r--test/System.Web.WebPages.Test/WebPage/DynamicHttpApplicationStateTest.cs75
-rw-r--r--test/System.Web.WebPages.Test/WebPage/DynamicPageDataDictionaryTest.cs243
-rw-r--r--test/System.Web.WebPages.Test/WebPage/FileExistenceCacheTest.cs95
-rw-r--r--test/System.Web.WebPages.Test/WebPage/LayoutTest.cs714
-rw-r--r--test/System.Web.WebPages.Test/WebPage/PageDataDictionaryTest.cs236
-rw-r--r--test/System.Web.WebPages.Test/WebPage/RenderPageTest.cs867
-rw-r--r--test/System.Web.WebPages.Test/WebPage/RequestBrowserOverrideStoreTest.cs35
-rw-r--r--test/System.Web.WebPages.Test/WebPage/RequestResourceTrackerTest.cs38
-rw-r--r--test/System.Web.WebPages.Test/WebPage/SectionControlBuilderTest.cs249
-rw-r--r--test/System.Web.WebPages.Test/WebPage/StartPageTest.cs568
-rw-r--r--test/System.Web.WebPages.Test/WebPage/TemplateStackTest.cs86
-rw-r--r--test/System.Web.WebPages.Test/WebPage/UrlDataTest.cs111
-rw-r--r--test/System.Web.WebPages.Test/WebPage/Utils.cs261
-rw-r--r--test/System.Web.WebPages.Test/WebPage/VirtualPathFactoryExtensionsTest.cs61
-rw-r--r--test/System.Web.WebPages.Test/WebPage/VirtualPathFactoryManagerTest.cs103
-rw-r--r--test/System.Web.WebPages.Test/WebPage/WebPageContextTest.cs57
-rw-r--r--test/System.Web.WebPages.Test/WebPage/WebPageExecutingBaseTest.cs189
-rw-r--r--test/System.Web.WebPages.Test/WebPage/WebPageHttpHandlerTest.cs249
-rw-r--r--test/System.Web.WebPages.Test/WebPage/WebPageHttpModuleTest.cs83
-rw-r--r--test/System.Web.WebPages.Test/WebPage/WebPageRenderingBaseTest.cs65
-rw-r--r--test/System.Web.WebPages.Test/WebPage/WebPageRouteTest.cs307
-rw-r--r--test/System.Web.WebPages.Test/WebPage/WebPageSurrogateControlBuilderTest.cs550
-rw-r--r--test/System.Web.WebPages.Test/WebPage/WebPageTest.cs309
-rw-r--r--test/System.Web.WebPages.Test/packages.config6
-rw-r--r--test/WebMatrix.Data.Test/App.config9
-rw-r--r--test/WebMatrix.Data.Test/ConfigurationManagerWrapperTest.cs90
-rw-r--r--test/WebMatrix.Data.Test/DatabaseTest.cs89
-rw-r--r--test/WebMatrix.Data.Test/DynamicRecordTest.cs150
-rw-r--r--test/WebMatrix.Data.Test/FileHandlerTest.cs52
-rw-r--r--test/WebMatrix.Data.Test/Mocks/MockConfigurationManager.cs28
-rw-r--r--test/WebMatrix.Data.Test/Mocks/MockConnectionConfiguration.cs22
-rw-r--r--test/WebMatrix.Data.Test/Mocks/MockDbFileHandler.cs12
-rw-r--r--test/WebMatrix.Data.Test/Mocks/MockDbProviderFactory.cs10
-rw-r--r--test/WebMatrix.Data.Test/Properties/AssemblyInfo.cs8
-rw-r--r--test/WebMatrix.Data.Test/WebMatrix.Data.Test.csproj90
-rw-r--r--test/WebMatrix.Data.Test/packages.config6
-rw-r--r--test/WebMatrix.WebData.Test/MockDatabase.cs20
-rw-r--r--test/WebMatrix.WebData.Test/PreApplicationStartCodeTest.cs102
-rw-r--r--test/WebMatrix.WebData.Test/Properties/AssemblyInfo.cs8
-rw-r--r--test/WebMatrix.WebData.Test/SimpleMembershipProviderTest.cs199
-rw-r--r--test/WebMatrix.WebData.Test/WebMatrix.WebData.Test.csproj94
-rw-r--r--test/WebMatrix.WebData.Test/WebSecurityTest.cs26
-rw-r--r--test/WebMatrix.WebData.Test/packages.config6
-rw-r--r--tools/35MSSharedLib1024.snkbin0 -> 160 bytes
-rw-r--r--tools/WebStack.NuGet.targets123
-rw-r--r--tools/WebStack.StyleCop.targets126
-rw-r--r--tools/WebStack.settings.targets44
-rw-r--r--tools/WebStack.targets5
-rw-r--r--tools/WebStack.tasks.targets47
-rw-r--r--tools/WebStack.xunit.targets20
2715 files changed, 379589 insertions, 0 deletions
diff --git a/Runtime.msbuild b/Runtime.msbuild
new file mode 100644
index 00000000..e7190dc0
--- /dev/null
+++ b/Runtime.msbuild
@@ -0,0 +1,79 @@
+<Project DefaultTargets="UnitTest" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Release</Configuration>
+
+ <!-- TODO: CodeAnalysis is off by default in VS11 because FxCop cannot load custom rules built against Dev10 -->
+ <CodeAnalysis Condition=" '$(CodeAnalysis)' == '' and '$(VS110COMNTOOLS)' == ''">true</CodeAnalysis>
+
+ <StyleCopEnabled Condition=" '$(StyleCopEnabled)' == '' ">true</StyleCopEnabled>
+ <BuildInParallel Condition=" '$(BuildInParallel)' == '' And $(MSBuildNodeCount) &gt; 1 ">true</BuildInParallel>
+ <BuildInParallel Condition=" '$(BuildInParallel)' == '' ">false</BuildInParallel>
+ <TestResultsDirectory>$(MSBuildThisFileDirectory)bin\$(Configuration)\test\TestResults\</TestResultsDirectory>
+ </PropertyGroup>
+
+ <Target Name="Integration" DependsOnTargets="Clean;Build;UnitTest" />
+
+ <Target Name="Clean">
+ <MSBuild
+ Projects="Runtime.sln"
+ Targets="Clean"
+ Properties="Configuration=$(Configuration)" />
+ <RemoveDir Directories="bin\$(Configuration)" />
+ </Target>
+
+ <Target Name="RestorePackages">
+ <!--
+ This can't build in parallel because of NuGet package restore race conditions.
+ When this is fixed in NuGet, we can remove the CSPROJ part of this target
+ (we will continue to need the NuGet install for StyleCop and FxCop tasks).
+
+ NOTE: These projects are hand selected to be the minimum # of CSPROJ files that
+ ensure we've restored every remote package. If another collision is found,
+ please review the project list as appropriate.
+ -->
+ <ItemGroup>
+ <RestoreCsProjFiles
+ Include="test\Microsoft.Web.Http.Data.Test\*.csproj;
+ src\System.Web.WebPages.Administration\*.csproj;
+ src\System.Web.WebPages.Deployment\*.csproj;
+ src\Microsoft.Web.WebPages.OAuth\*.csproj" />
+ </ItemGroup>
+ <Message Text="Restoring NuGet packages..." Importance="High" />
+ <!-- Download NuGet.exe -->
+ <MSBuild
+ Projects="tools\WebStack.NuGet.targets"
+ Targets="CheckPrerequisites" />
+ <!-- Restore the things the CSPROJ files need -->
+ <MSBuild
+ Projects="@(RestoreCsProjFiles)"
+ BuildInParallel="false"
+ Targets="RestorePackages" />
+ <!-- Hand restore packages with binaries that this MSBuild process needs -->
+ <Exec
+ Command='tools\NuGet.exe install StyleCop -Source "\\webstack-git\packages;https://go.microsoft.com/fwlink/?LinkID=230477" -Version 4.7.10.0 -o packages > NUL'
+ LogStandardErrorAsError="true" />
+ <Exec
+ Command='tools\NuGet.exe install Microsoft.Web.FxCop -Source "\\webstack-git\packages;https://go.microsoft.com/fwlink/?LinkID=230477" -ExcludeVersion -o packages > NUL'
+ LogStandardErrorAsError="true" />
+ </Target>
+
+ <Target Name="Build" DependsOnTargets="RestorePackages">
+ <MakeDir Directories="bin\$(Configuration)" />
+ <MSBuild
+ Projects="Runtime.sln"
+ BuildInParallel="$(BuildInParallel)"
+ Targets="Build"
+ Properties="Configuration=$(Configuration);CodeAnalysis=$(CodeAnalysis);StyleCopEnabled=$(StyleCopEnabled)" />
+ </Target>
+
+ <Target Name="UnitTest" DependsOnTargets="Build">
+ <ItemGroup>
+ <TestDLLsXunit Include="bin\$(Configuration)\test\*.Test.dll;bin\$(Configuration)\test\*.Test.*.dll" Exclude="**\SPA.Test.dll" />
+ <XunitProject Include="tools\WebStack.xunit.targets">
+ <Properties>TestAssembly=%(TestDLLsXunit.FullPath);XmlPath=$(TestResultsDirectory)%(TestDLLsXunit.FileName)-XunitResults.xml</Properties>
+ </XunitProject>
+ </ItemGroup>
+ <MakeDir Directories="$(TestResultsDirectory)" />
+ <MSBuild Projects="@(XunitProject)" BuildInParallel="$(BuildInParallel)" Targets="Xunit" />
+ </Target>
+</Project>
diff --git a/Runtime.sln b/Runtime.sln
new file mode 100644
index 00000000..5a9da54f
--- /dev/null
+++ b/Runtime.sln
@@ -0,0 +1,1068 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 2010
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A9836F9E-6DB3-4D9F-ADCA-CF42D8C8BA93}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{C40883CD-366D-4534-8B58-3EA0D13136DF}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Web.Razor", "src\System.Web.Razor\System.Web.Razor.csproj", "{8F18041B-9410-4C36-A9C5-067813DF5F31}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Web.WebPages.Deployment", "src\System.Web.WebPages.Deployment\System.Web.WebPages.Deployment.csproj", "{22BABB60-8F02-4027-AFFC-ACF069954536}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Web.WebPages", "src\System.Web.WebPages\System.Web.WebPages.csproj", "{76EFA9C5-8D7E-4FDF-B710-E20F8B6B00D2}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Web.Helpers", "src\System.Web.Helpers\System.Web.Helpers.csproj", "{9B7E3740-6161-4548-833C-4BBCA43B970E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Web.WebPages.Razor", "src\System.Web.WebPages.Razor\System.Web.WebPages.Razor.csproj", "{0939B11A-FE4E-4BA1-8AD6-D97741EE314F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebMatrix.Data", "src\WebMatrix.Data\WebMatrix.Data.csproj", "{4D39BAAF-8A96-473E-AB79-C8A341885137}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebMatrix.WebData", "src\WebMatrix.WebData\WebMatrix.WebData.csproj", "{55A15F40-1435-4248-A7F2-2A146BB83586}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Web.Helpers", "src\Microsoft.Web.Helpers\Microsoft.Web.Helpers.csproj", "{0C7CE809-0F72-4C19-8C64-D6573E4D9521}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Web.WebPages.Administration", "src\System.Web.WebPages.Administration\System.Web.WebPages.Administration.csproj", "{C23F02FC-4538-43F5-ABBA-38BA069AEA8F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Web.Mvc", "src\System.Web.Mvc\System.Web.Mvc.csproj", "{3D3FFD8A-624D-4E9B-954B-E1C105507975}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Web.Mvc", "src\Microsoft.Web.Mvc\Microsoft.Web.Mvc.csproj", "{D3CF7430-6DA4-42B0-BD90-CA39D16687B2}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Web.Razor.Test", "test\System.Web.Razor.Test\System.Web.Razor.Test.csproj", "{0BB62A1D-E6B5-49FA-9E3C-6AF679A66DFE}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Web.WebPages.Deployment.Test", "test\System.Web.WebPages.Deployment.Test\System.Web.WebPages.Deployment.Test.csproj", "{268DEE9D-F323-4A00-8ED8-3784388C3E3A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Web.WebPages.Test", "test\System.Web.WebPages.Test\System.Web.WebPages.Test.csproj", "{0F4870DB-A799-4DBA-99DF-0D74BB52FEC2}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Web.Helpers.Test", "test\System.Web.Helpers.Test\System.Web.Helpers.Test.csproj", "{D3313BDF-8071-4AC8-9D98-ABF7F9E88A57}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Web.WebPages.Razor.Test", "test\System.Web.WebPages.Razor.Test\System.Web.WebPages.Razor.Test.csproj", "{66A74F3C-A106-4C1E-BAA0-001908FEA2CA}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebMatrix.Data.Test", "test\WebMatrix.Data.Test\WebMatrix.Data.Test.csproj", "{E2D008A9-4D1D-4F6B-8325-4ED717D6EA0A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebMatrix.WebData.Test", "test\WebMatrix.WebData.Test\WebMatrix.WebData.Test.csproj", "{CD48EB41-92A5-4628-A0F7-6A43DF58827E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Web.Helpers.Test", "test\Microsoft.Web.Helpers.Test\Microsoft.Web.Helpers.Test.csproj", "{2C653A66-8159-4A41-954F-A67915DFDA87}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Web.WebPages.Administration.Test", "test\System.Web.WebPages.Administration.Test\System.Web.WebPages.Administration.Test.csproj", "{21C729D6-ECF8-47EF-A236-7C6A4272EAF0}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Web.Mvc.Test", "test\System.Web.Mvc.Test\System.Web.Mvc.Test.csproj", "{8AC2A2E4-2F11-4D40-A887-62E2583A65E6}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Web.Mvc.Test", "test\Microsoft.Web.Mvc.Test\Microsoft.Web.Mvc.Test.csproj", "{6C28DA70-60F1-4442-967F-591BF3962EC5}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Web.Http", "src\System.Web.Http\System.Web.Http.csproj", "{DDC1CE0C-486E-4E35-BB3B-EAB61F8F9440}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Web.Http.Test", "test\System.Web.Http.Test\System.Web.Http.Test.csproj", "{7F2C796F-43B2-4F8F-ABFF-A154EC8AAFA1}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Json", "src\System.Json\System.Json.csproj", "{F0441BE9-BDC0-4629-BE5A-8765FFAA2481}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Net.Http.Formatting", "src\System.Net.Http.Formatting\System.Net.Http.Formatting.csproj", "{668E9021-CE84-49D9-98FB-DF125A9FCDB0}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.TestCommon", "test\Microsoft.TestCommon\Microsoft.TestCommon.csproj", "{FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Json.Test.Unit", "test\System.Json.Test.Unit\System.Json.Test.Unit.csproj", "{EB09CD33-992B-4A31-AB95-8673BA90F1CD}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Net.Http.Formatting.Test.Unit", "test\System.Net.Http.Formatting.Test.Unit\System.Net.Http.Formatting.Test.Unit.csproj", "{7AF77741-9158-4D5F-8782-8F21FADF025F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Json.Test.Integration", "test\System.Json.Test.Integration\System.Json.Test.Integration.csproj", "{A7B1264E-BCE5-42A8-8B5E-001A5360B128}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Net.Http.Formatting.Test.Integration", "test\System.Net.Http.Formatting.Test.Integration\System.Net.Http.Formatting.Test.Integration.csproj", "{6C18CC83-1E4C-42D2-B93E-55D6C363850C}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Web.Http.Common", "src\System.Web.Http.Common\System.Web.Http.Common.csproj", "{03A5E5F2-2E23-48F2-ABCC-6C41BAC9AC02}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Web.Http.SelfHost", "src\System.Web.Http.SelfHost\System.Web.Http.SelfHost.csproj", "{66492E69-CE4C-4FB1-9B1F-88DEE09D06F1}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Web.Http.Common.Test", "test\System.Web.Http.Common.Test\System.Web.Http.Common.Test.csproj", "{7FB5C0C0-5223-4C79-A8DA-D2A0F264A478}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Web.Http.WebHost", "src\System.Web.Http.WebHost\System.Web.Http.WebHost.csproj", "{A0187BC2-8325-4BB2-8697-7F955CF4173E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Web.Http.Data", "src\Microsoft.Web.Http.Data\Microsoft.Web.Http.Data.csproj", "{ACE91549-D86E-4EB6-8C2A-5FF51386BB68}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Web.Http.Data.EntityFramework", "src\Microsoft.Web.Http.Data.EntityFramework\Microsoft.Web.Http.Data.EntityFramework.csproj", "{653F3946-541C-42D3-BBC1-CE89B392BDA9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Web.Http.Data.Test", "test\Microsoft.Web.Http.Data.Test\Microsoft.Web.Http.Data.Test.csproj", "{81876811-6C36-492A-9609-F0E85990FBC9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Web.Http.Integration.Test", "test\System.Web.Http.Integration.Test\System.Web.Http.Integration.Test.csproj", "{3267DFC6-B34D-4011-BC0F-D3B56AF6F608}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Web.Http.WebHost.Test", "test\System.Web.Http.WebHost.Test\System.Web.Http.WebHost.Test.csproj", "{EA62944F-BD25-4730-9405-9BE8FF5BEACD}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Web.Http.Data.Helpers", "src\Microsoft.Web.Http.Data.Helpers\Microsoft.Web.Http.Data.Helpers.csproj", "{B6895A1B-382F-4A69-99EC-E965E19B0AB3}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SPA", "src\SPA\SPA.csproj", "{1ACEF677-B6A0-4680-A076-7893DE176D6B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SPA.Test", "test\SPA.Test\SPA.Test.csproj", "{7B8601F8-8D1F-4B9C-8C20-772B673A2FA6}"
+ ProjectSection(ProjectDependencies) = postProject
+ {1ACEF677-B6A0-4680-A076-7893DE176D6B} = {1ACEF677-B6A0-4680-A076-7893DE176D6B}
+ EndProjectSection
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Web.WebPages.OAuth", "src\Microsoft.Web.WebPages.OAuth\Microsoft.Web.WebPages.OAuth.csproj", "{4CBFC7D3-1600-4CE5-BC6B-AC7BC2D6F853}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Web.WebPages.OAuth.Test", "test\Microsoft.Web.WebPages.OAuth.Test\Microsoft.Web.WebPages.OAuth.Test.csproj", "{694C6EDF-EA52-438F-B745-82B025ECC0E7}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ CodeCov|Any CPU = CodeCov|Any CPU
+ CodeCov|Mixed Platforms = CodeCov|Mixed Platforms
+ CodeCov|x86 = CodeCov|x86
+ CodeCoverage|Any CPU = CodeCoverage|Any CPU
+ CodeCoverage|Mixed Platforms = CodeCoverage|Mixed Platforms
+ CodeCoverage|x86 = CodeCoverage|x86
+ Debug|Any CPU = Debug|Any CPU
+ Debug|Mixed Platforms = Debug|Mixed Platforms
+ Debug|x86 = Debug|x86
+ Release|Any CPU = Release|Any CPU
+ Release|Mixed Platforms = Release|Mixed Platforms
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {8F18041B-9410-4C36-A9C5-067813DF5F31}.CodeCov|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {8F18041B-9410-4C36-A9C5-067813DF5F31}.CodeCov|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {8F18041B-9410-4C36-A9C5-067813DF5F31}.CodeCov|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {8F18041B-9410-4C36-A9C5-067813DF5F31}.CodeCov|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {8F18041B-9410-4C36-A9C5-067813DF5F31}.CodeCov|x86.ActiveCfg = CodeCoverage|Any CPU
+ {8F18041B-9410-4C36-A9C5-067813DF5F31}.CodeCoverage|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {8F18041B-9410-4C36-A9C5-067813DF5F31}.CodeCoverage|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {8F18041B-9410-4C36-A9C5-067813DF5F31}.CodeCoverage|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {8F18041B-9410-4C36-A9C5-067813DF5F31}.CodeCoverage|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {8F18041B-9410-4C36-A9C5-067813DF5F31}.CodeCoverage|x86.ActiveCfg = CodeCoverage|Any CPU
+ {8F18041B-9410-4C36-A9C5-067813DF5F31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8F18041B-9410-4C36-A9C5-067813DF5F31}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8F18041B-9410-4C36-A9C5-067813DF5F31}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {8F18041B-9410-4C36-A9C5-067813DF5F31}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {8F18041B-9410-4C36-A9C5-067813DF5F31}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {8F18041B-9410-4C36-A9C5-067813DF5F31}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8F18041B-9410-4C36-A9C5-067813DF5F31}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8F18041B-9410-4C36-A9C5-067813DF5F31}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {8F18041B-9410-4C36-A9C5-067813DF5F31}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {8F18041B-9410-4C36-A9C5-067813DF5F31}.Release|x86.ActiveCfg = Release|Any CPU
+ {22BABB60-8F02-4027-AFFC-ACF069954536}.CodeCov|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {22BABB60-8F02-4027-AFFC-ACF069954536}.CodeCov|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {22BABB60-8F02-4027-AFFC-ACF069954536}.CodeCov|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {22BABB60-8F02-4027-AFFC-ACF069954536}.CodeCov|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {22BABB60-8F02-4027-AFFC-ACF069954536}.CodeCov|x86.ActiveCfg = CodeCoverage|Any CPU
+ {22BABB60-8F02-4027-AFFC-ACF069954536}.CodeCoverage|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {22BABB60-8F02-4027-AFFC-ACF069954536}.CodeCoverage|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {22BABB60-8F02-4027-AFFC-ACF069954536}.CodeCoverage|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {22BABB60-8F02-4027-AFFC-ACF069954536}.CodeCoverage|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {22BABB60-8F02-4027-AFFC-ACF069954536}.CodeCoverage|x86.ActiveCfg = CodeCoverage|Any CPU
+ {22BABB60-8F02-4027-AFFC-ACF069954536}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {22BABB60-8F02-4027-AFFC-ACF069954536}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {22BABB60-8F02-4027-AFFC-ACF069954536}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {22BABB60-8F02-4027-AFFC-ACF069954536}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {22BABB60-8F02-4027-AFFC-ACF069954536}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {22BABB60-8F02-4027-AFFC-ACF069954536}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {22BABB60-8F02-4027-AFFC-ACF069954536}.Release|Any CPU.Build.0 = Release|Any CPU
+ {22BABB60-8F02-4027-AFFC-ACF069954536}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {22BABB60-8F02-4027-AFFC-ACF069954536}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {22BABB60-8F02-4027-AFFC-ACF069954536}.Release|x86.ActiveCfg = Release|Any CPU
+ {76EFA9C5-8D7E-4FDF-B710-E20F8B6B00D2}.CodeCov|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {76EFA9C5-8D7E-4FDF-B710-E20F8B6B00D2}.CodeCov|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {76EFA9C5-8D7E-4FDF-B710-E20F8B6B00D2}.CodeCov|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {76EFA9C5-8D7E-4FDF-B710-E20F8B6B00D2}.CodeCov|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {76EFA9C5-8D7E-4FDF-B710-E20F8B6B00D2}.CodeCov|x86.ActiveCfg = CodeCoverage|Any CPU
+ {76EFA9C5-8D7E-4FDF-B710-E20F8B6B00D2}.CodeCoverage|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {76EFA9C5-8D7E-4FDF-B710-E20F8B6B00D2}.CodeCoverage|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {76EFA9C5-8D7E-4FDF-B710-E20F8B6B00D2}.CodeCoverage|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {76EFA9C5-8D7E-4FDF-B710-E20F8B6B00D2}.CodeCoverage|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {76EFA9C5-8D7E-4FDF-B710-E20F8B6B00D2}.CodeCoverage|x86.ActiveCfg = CodeCoverage|Any CPU
+ {76EFA9C5-8D7E-4FDF-B710-E20F8B6B00D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {76EFA9C5-8D7E-4FDF-B710-E20F8B6B00D2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {76EFA9C5-8D7E-4FDF-B710-E20F8B6B00D2}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {76EFA9C5-8D7E-4FDF-B710-E20F8B6B00D2}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {76EFA9C5-8D7E-4FDF-B710-E20F8B6B00D2}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {76EFA9C5-8D7E-4FDF-B710-E20F8B6B00D2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {76EFA9C5-8D7E-4FDF-B710-E20F8B6B00D2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {76EFA9C5-8D7E-4FDF-B710-E20F8B6B00D2}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {76EFA9C5-8D7E-4FDF-B710-E20F8B6B00D2}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {76EFA9C5-8D7E-4FDF-B710-E20F8B6B00D2}.Release|x86.ActiveCfg = Release|Any CPU
+ {9B7E3740-6161-4548-833C-4BBCA43B970E}.CodeCov|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {9B7E3740-6161-4548-833C-4BBCA43B970E}.CodeCov|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {9B7E3740-6161-4548-833C-4BBCA43B970E}.CodeCov|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {9B7E3740-6161-4548-833C-4BBCA43B970E}.CodeCov|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {9B7E3740-6161-4548-833C-4BBCA43B970E}.CodeCov|x86.ActiveCfg = CodeCoverage|Any CPU
+ {9B7E3740-6161-4548-833C-4BBCA43B970E}.CodeCoverage|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {9B7E3740-6161-4548-833C-4BBCA43B970E}.CodeCoverage|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {9B7E3740-6161-4548-833C-4BBCA43B970E}.CodeCoverage|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {9B7E3740-6161-4548-833C-4BBCA43B970E}.CodeCoverage|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {9B7E3740-6161-4548-833C-4BBCA43B970E}.CodeCoverage|x86.ActiveCfg = CodeCoverage|Any CPU
+ {9B7E3740-6161-4548-833C-4BBCA43B970E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9B7E3740-6161-4548-833C-4BBCA43B970E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9B7E3740-6161-4548-833C-4BBCA43B970E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {9B7E3740-6161-4548-833C-4BBCA43B970E}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {9B7E3740-6161-4548-833C-4BBCA43B970E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {9B7E3740-6161-4548-833C-4BBCA43B970E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9B7E3740-6161-4548-833C-4BBCA43B970E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9B7E3740-6161-4548-833C-4BBCA43B970E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {9B7E3740-6161-4548-833C-4BBCA43B970E}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {9B7E3740-6161-4548-833C-4BBCA43B970E}.Release|x86.ActiveCfg = Release|Any CPU
+ {0939B11A-FE4E-4BA1-8AD6-D97741EE314F}.CodeCov|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {0939B11A-FE4E-4BA1-8AD6-D97741EE314F}.CodeCov|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {0939B11A-FE4E-4BA1-8AD6-D97741EE314F}.CodeCov|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {0939B11A-FE4E-4BA1-8AD6-D97741EE314F}.CodeCov|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {0939B11A-FE4E-4BA1-8AD6-D97741EE314F}.CodeCov|x86.ActiveCfg = CodeCoverage|Any CPU
+ {0939B11A-FE4E-4BA1-8AD6-D97741EE314F}.CodeCoverage|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {0939B11A-FE4E-4BA1-8AD6-D97741EE314F}.CodeCoverage|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {0939B11A-FE4E-4BA1-8AD6-D97741EE314F}.CodeCoverage|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {0939B11A-FE4E-4BA1-8AD6-D97741EE314F}.CodeCoverage|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {0939B11A-FE4E-4BA1-8AD6-D97741EE314F}.CodeCoverage|x86.ActiveCfg = CodeCoverage|Any CPU
+ {0939B11A-FE4E-4BA1-8AD6-D97741EE314F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0939B11A-FE4E-4BA1-8AD6-D97741EE314F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0939B11A-FE4E-4BA1-8AD6-D97741EE314F}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {0939B11A-FE4E-4BA1-8AD6-D97741EE314F}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {0939B11A-FE4E-4BA1-8AD6-D97741EE314F}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {0939B11A-FE4E-4BA1-8AD6-D97741EE314F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0939B11A-FE4E-4BA1-8AD6-D97741EE314F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0939B11A-FE4E-4BA1-8AD6-D97741EE314F}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {0939B11A-FE4E-4BA1-8AD6-D97741EE314F}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {0939B11A-FE4E-4BA1-8AD6-D97741EE314F}.Release|x86.ActiveCfg = Release|Any CPU
+ {4D39BAAF-8A96-473E-AB79-C8A341885137}.CodeCov|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {4D39BAAF-8A96-473E-AB79-C8A341885137}.CodeCov|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {4D39BAAF-8A96-473E-AB79-C8A341885137}.CodeCov|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {4D39BAAF-8A96-473E-AB79-C8A341885137}.CodeCov|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {4D39BAAF-8A96-473E-AB79-C8A341885137}.CodeCov|x86.ActiveCfg = CodeCoverage|Any CPU
+ {4D39BAAF-8A96-473E-AB79-C8A341885137}.CodeCoverage|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {4D39BAAF-8A96-473E-AB79-C8A341885137}.CodeCoverage|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {4D39BAAF-8A96-473E-AB79-C8A341885137}.CodeCoverage|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {4D39BAAF-8A96-473E-AB79-C8A341885137}.CodeCoverage|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {4D39BAAF-8A96-473E-AB79-C8A341885137}.CodeCoverage|x86.ActiveCfg = CodeCoverage|Any CPU
+ {4D39BAAF-8A96-473E-AB79-C8A341885137}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4D39BAAF-8A96-473E-AB79-C8A341885137}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4D39BAAF-8A96-473E-AB79-C8A341885137}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {4D39BAAF-8A96-473E-AB79-C8A341885137}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {4D39BAAF-8A96-473E-AB79-C8A341885137}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {4D39BAAF-8A96-473E-AB79-C8A341885137}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4D39BAAF-8A96-473E-AB79-C8A341885137}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4D39BAAF-8A96-473E-AB79-C8A341885137}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {4D39BAAF-8A96-473E-AB79-C8A341885137}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {4D39BAAF-8A96-473E-AB79-C8A341885137}.Release|x86.ActiveCfg = Release|Any CPU
+ {55A15F40-1435-4248-A7F2-2A146BB83586}.CodeCov|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {55A15F40-1435-4248-A7F2-2A146BB83586}.CodeCov|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {55A15F40-1435-4248-A7F2-2A146BB83586}.CodeCov|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {55A15F40-1435-4248-A7F2-2A146BB83586}.CodeCov|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {55A15F40-1435-4248-A7F2-2A146BB83586}.CodeCov|x86.ActiveCfg = CodeCoverage|Any CPU
+ {55A15F40-1435-4248-A7F2-2A146BB83586}.CodeCoverage|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {55A15F40-1435-4248-A7F2-2A146BB83586}.CodeCoverage|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {55A15F40-1435-4248-A7F2-2A146BB83586}.CodeCoverage|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {55A15F40-1435-4248-A7F2-2A146BB83586}.CodeCoverage|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {55A15F40-1435-4248-A7F2-2A146BB83586}.CodeCoverage|x86.ActiveCfg = CodeCoverage|Any CPU
+ {55A15F40-1435-4248-A7F2-2A146BB83586}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {55A15F40-1435-4248-A7F2-2A146BB83586}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {55A15F40-1435-4248-A7F2-2A146BB83586}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {55A15F40-1435-4248-A7F2-2A146BB83586}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {55A15F40-1435-4248-A7F2-2A146BB83586}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {55A15F40-1435-4248-A7F2-2A146BB83586}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {55A15F40-1435-4248-A7F2-2A146BB83586}.Release|Any CPU.Build.0 = Release|Any CPU
+ {55A15F40-1435-4248-A7F2-2A146BB83586}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {55A15F40-1435-4248-A7F2-2A146BB83586}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {55A15F40-1435-4248-A7F2-2A146BB83586}.Release|x86.ActiveCfg = Release|Any CPU
+ {0C7CE809-0F72-4C19-8C64-D6573E4D9521}.CodeCov|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {0C7CE809-0F72-4C19-8C64-D6573E4D9521}.CodeCov|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {0C7CE809-0F72-4C19-8C64-D6573E4D9521}.CodeCov|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {0C7CE809-0F72-4C19-8C64-D6573E4D9521}.CodeCov|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {0C7CE809-0F72-4C19-8C64-D6573E4D9521}.CodeCov|x86.ActiveCfg = CodeCoverage|Any CPU
+ {0C7CE809-0F72-4C19-8C64-D6573E4D9521}.CodeCoverage|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {0C7CE809-0F72-4C19-8C64-D6573E4D9521}.CodeCoverage|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {0C7CE809-0F72-4C19-8C64-D6573E4D9521}.CodeCoverage|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {0C7CE809-0F72-4C19-8C64-D6573E4D9521}.CodeCoverage|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {0C7CE809-0F72-4C19-8C64-D6573E4D9521}.CodeCoverage|x86.ActiveCfg = CodeCoverage|Any CPU
+ {0C7CE809-0F72-4C19-8C64-D6573E4D9521}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0C7CE809-0F72-4C19-8C64-D6573E4D9521}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0C7CE809-0F72-4C19-8C64-D6573E4D9521}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {0C7CE809-0F72-4C19-8C64-D6573E4D9521}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {0C7CE809-0F72-4C19-8C64-D6573E4D9521}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {0C7CE809-0F72-4C19-8C64-D6573E4D9521}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0C7CE809-0F72-4C19-8C64-D6573E4D9521}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0C7CE809-0F72-4C19-8C64-D6573E4D9521}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {0C7CE809-0F72-4C19-8C64-D6573E4D9521}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {0C7CE809-0F72-4C19-8C64-D6573E4D9521}.Release|x86.ActiveCfg = Release|Any CPU
+ {C23F02FC-4538-43F5-ABBA-38BA069AEA8F}.CodeCov|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {C23F02FC-4538-43F5-ABBA-38BA069AEA8F}.CodeCov|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {C23F02FC-4538-43F5-ABBA-38BA069AEA8F}.CodeCov|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {C23F02FC-4538-43F5-ABBA-38BA069AEA8F}.CodeCov|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {C23F02FC-4538-43F5-ABBA-38BA069AEA8F}.CodeCov|x86.ActiveCfg = CodeCoverage|Any CPU
+ {C23F02FC-4538-43F5-ABBA-38BA069AEA8F}.CodeCoverage|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {C23F02FC-4538-43F5-ABBA-38BA069AEA8F}.CodeCoverage|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {C23F02FC-4538-43F5-ABBA-38BA069AEA8F}.CodeCoverage|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {C23F02FC-4538-43F5-ABBA-38BA069AEA8F}.CodeCoverage|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {C23F02FC-4538-43F5-ABBA-38BA069AEA8F}.CodeCoverage|x86.ActiveCfg = CodeCoverage|Any CPU
+ {C23F02FC-4538-43F5-ABBA-38BA069AEA8F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C23F02FC-4538-43F5-ABBA-38BA069AEA8F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C23F02FC-4538-43F5-ABBA-38BA069AEA8F}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {C23F02FC-4538-43F5-ABBA-38BA069AEA8F}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {C23F02FC-4538-43F5-ABBA-38BA069AEA8F}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C23F02FC-4538-43F5-ABBA-38BA069AEA8F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C23F02FC-4538-43F5-ABBA-38BA069AEA8F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C23F02FC-4538-43F5-ABBA-38BA069AEA8F}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {C23F02FC-4538-43F5-ABBA-38BA069AEA8F}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {C23F02FC-4538-43F5-ABBA-38BA069AEA8F}.Release|x86.ActiveCfg = Release|Any CPU
+ {3D3FFD8A-624D-4E9B-954B-E1C105507975}.CodeCov|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {3D3FFD8A-624D-4E9B-954B-E1C105507975}.CodeCov|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {3D3FFD8A-624D-4E9B-954B-E1C105507975}.CodeCov|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {3D3FFD8A-624D-4E9B-954B-E1C105507975}.CodeCov|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {3D3FFD8A-624D-4E9B-954B-E1C105507975}.CodeCov|x86.ActiveCfg = CodeCoverage|Any CPU
+ {3D3FFD8A-624D-4E9B-954B-E1C105507975}.CodeCoverage|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {3D3FFD8A-624D-4E9B-954B-E1C105507975}.CodeCoverage|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {3D3FFD8A-624D-4E9B-954B-E1C105507975}.CodeCoverage|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {3D3FFD8A-624D-4E9B-954B-E1C105507975}.CodeCoverage|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {3D3FFD8A-624D-4E9B-954B-E1C105507975}.CodeCoverage|x86.ActiveCfg = CodeCoverage|Any CPU
+ {3D3FFD8A-624D-4E9B-954B-E1C105507975}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3D3FFD8A-624D-4E9B-954B-E1C105507975}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3D3FFD8A-624D-4E9B-954B-E1C105507975}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {3D3FFD8A-624D-4E9B-954B-E1C105507975}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {3D3FFD8A-624D-4E9B-954B-E1C105507975}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {3D3FFD8A-624D-4E9B-954B-E1C105507975}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3D3FFD8A-624D-4E9B-954B-E1C105507975}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3D3FFD8A-624D-4E9B-954B-E1C105507975}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {3D3FFD8A-624D-4E9B-954B-E1C105507975}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {3D3FFD8A-624D-4E9B-954B-E1C105507975}.Release|x86.ActiveCfg = Release|Any CPU
+ {D3CF7430-6DA4-42B0-BD90-CA39D16687B2}.CodeCov|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {D3CF7430-6DA4-42B0-BD90-CA39D16687B2}.CodeCov|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {D3CF7430-6DA4-42B0-BD90-CA39D16687B2}.CodeCov|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {D3CF7430-6DA4-42B0-BD90-CA39D16687B2}.CodeCov|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {D3CF7430-6DA4-42B0-BD90-CA39D16687B2}.CodeCov|x86.ActiveCfg = CodeCoverage|Any CPU
+ {D3CF7430-6DA4-42B0-BD90-CA39D16687B2}.CodeCoverage|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {D3CF7430-6DA4-42B0-BD90-CA39D16687B2}.CodeCoverage|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {D3CF7430-6DA4-42B0-BD90-CA39D16687B2}.CodeCoverage|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {D3CF7430-6DA4-42B0-BD90-CA39D16687B2}.CodeCoverage|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {D3CF7430-6DA4-42B0-BD90-CA39D16687B2}.CodeCoverage|x86.ActiveCfg = CodeCoverage|Any CPU
+ {D3CF7430-6DA4-42B0-BD90-CA39D16687B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D3CF7430-6DA4-42B0-BD90-CA39D16687B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D3CF7430-6DA4-42B0-BD90-CA39D16687B2}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {D3CF7430-6DA4-42B0-BD90-CA39D16687B2}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {D3CF7430-6DA4-42B0-BD90-CA39D16687B2}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {D3CF7430-6DA4-42B0-BD90-CA39D16687B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D3CF7430-6DA4-42B0-BD90-CA39D16687B2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D3CF7430-6DA4-42B0-BD90-CA39D16687B2}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {D3CF7430-6DA4-42B0-BD90-CA39D16687B2}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {D3CF7430-6DA4-42B0-BD90-CA39D16687B2}.Release|x86.ActiveCfg = Release|Any CPU
+ {0BB62A1D-E6B5-49FA-9E3C-6AF679A66DFE}.CodeCov|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {0BB62A1D-E6B5-49FA-9E3C-6AF679A66DFE}.CodeCov|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {0BB62A1D-E6B5-49FA-9E3C-6AF679A66DFE}.CodeCov|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {0BB62A1D-E6B5-49FA-9E3C-6AF679A66DFE}.CodeCov|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {0BB62A1D-E6B5-49FA-9E3C-6AF679A66DFE}.CodeCov|x86.ActiveCfg = CodeCoverage|Any CPU
+ {0BB62A1D-E6B5-49FA-9E3C-6AF679A66DFE}.CodeCoverage|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {0BB62A1D-E6B5-49FA-9E3C-6AF679A66DFE}.CodeCoverage|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {0BB62A1D-E6B5-49FA-9E3C-6AF679A66DFE}.CodeCoverage|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {0BB62A1D-E6B5-49FA-9E3C-6AF679A66DFE}.CodeCoverage|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {0BB62A1D-E6B5-49FA-9E3C-6AF679A66DFE}.CodeCoverage|x86.ActiveCfg = CodeCoverage|Any CPU
+ {0BB62A1D-E6B5-49FA-9E3C-6AF679A66DFE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0BB62A1D-E6B5-49FA-9E3C-6AF679A66DFE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0BB62A1D-E6B5-49FA-9E3C-6AF679A66DFE}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {0BB62A1D-E6B5-49FA-9E3C-6AF679A66DFE}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {0BB62A1D-E6B5-49FA-9E3C-6AF679A66DFE}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {0BB62A1D-E6B5-49FA-9E3C-6AF679A66DFE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0BB62A1D-E6B5-49FA-9E3C-6AF679A66DFE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0BB62A1D-E6B5-49FA-9E3C-6AF679A66DFE}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {0BB62A1D-E6B5-49FA-9E3C-6AF679A66DFE}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {0BB62A1D-E6B5-49FA-9E3C-6AF679A66DFE}.Release|x86.ActiveCfg = Release|Any CPU
+ {268DEE9D-F323-4A00-8ED8-3784388C3E3A}.CodeCov|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {268DEE9D-F323-4A00-8ED8-3784388C3E3A}.CodeCov|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {268DEE9D-F323-4A00-8ED8-3784388C3E3A}.CodeCov|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {268DEE9D-F323-4A00-8ED8-3784388C3E3A}.CodeCov|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {268DEE9D-F323-4A00-8ED8-3784388C3E3A}.CodeCov|x86.ActiveCfg = CodeCoverage|Any CPU
+ {268DEE9D-F323-4A00-8ED8-3784388C3E3A}.CodeCoverage|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {268DEE9D-F323-4A00-8ED8-3784388C3E3A}.CodeCoverage|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {268DEE9D-F323-4A00-8ED8-3784388C3E3A}.CodeCoverage|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {268DEE9D-F323-4A00-8ED8-3784388C3E3A}.CodeCoverage|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {268DEE9D-F323-4A00-8ED8-3784388C3E3A}.CodeCoverage|x86.ActiveCfg = CodeCoverage|Any CPU
+ {268DEE9D-F323-4A00-8ED8-3784388C3E3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {268DEE9D-F323-4A00-8ED8-3784388C3E3A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {268DEE9D-F323-4A00-8ED8-3784388C3E3A}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {268DEE9D-F323-4A00-8ED8-3784388C3E3A}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {268DEE9D-F323-4A00-8ED8-3784388C3E3A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {268DEE9D-F323-4A00-8ED8-3784388C3E3A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {268DEE9D-F323-4A00-8ED8-3784388C3E3A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {268DEE9D-F323-4A00-8ED8-3784388C3E3A}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {268DEE9D-F323-4A00-8ED8-3784388C3E3A}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {268DEE9D-F323-4A00-8ED8-3784388C3E3A}.Release|x86.ActiveCfg = Release|Any CPU
+ {0F4870DB-A799-4DBA-99DF-0D74BB52FEC2}.CodeCov|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {0F4870DB-A799-4DBA-99DF-0D74BB52FEC2}.CodeCov|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {0F4870DB-A799-4DBA-99DF-0D74BB52FEC2}.CodeCov|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {0F4870DB-A799-4DBA-99DF-0D74BB52FEC2}.CodeCov|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {0F4870DB-A799-4DBA-99DF-0D74BB52FEC2}.CodeCov|x86.ActiveCfg = CodeCoverage|Any CPU
+ {0F4870DB-A799-4DBA-99DF-0D74BB52FEC2}.CodeCoverage|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {0F4870DB-A799-4DBA-99DF-0D74BB52FEC2}.CodeCoverage|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {0F4870DB-A799-4DBA-99DF-0D74BB52FEC2}.CodeCoverage|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {0F4870DB-A799-4DBA-99DF-0D74BB52FEC2}.CodeCoverage|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {0F4870DB-A799-4DBA-99DF-0D74BB52FEC2}.CodeCoverage|x86.ActiveCfg = CodeCoverage|Any CPU
+ {0F4870DB-A799-4DBA-99DF-0D74BB52FEC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0F4870DB-A799-4DBA-99DF-0D74BB52FEC2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0F4870DB-A799-4DBA-99DF-0D74BB52FEC2}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {0F4870DB-A799-4DBA-99DF-0D74BB52FEC2}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {0F4870DB-A799-4DBA-99DF-0D74BB52FEC2}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {0F4870DB-A799-4DBA-99DF-0D74BB52FEC2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0F4870DB-A799-4DBA-99DF-0D74BB52FEC2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0F4870DB-A799-4DBA-99DF-0D74BB52FEC2}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {0F4870DB-A799-4DBA-99DF-0D74BB52FEC2}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {0F4870DB-A799-4DBA-99DF-0D74BB52FEC2}.Release|x86.ActiveCfg = Release|Any CPU
+ {D3313BDF-8071-4AC8-9D98-ABF7F9E88A57}.CodeCov|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {D3313BDF-8071-4AC8-9D98-ABF7F9E88A57}.CodeCov|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {D3313BDF-8071-4AC8-9D98-ABF7F9E88A57}.CodeCov|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {D3313BDF-8071-4AC8-9D98-ABF7F9E88A57}.CodeCov|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {D3313BDF-8071-4AC8-9D98-ABF7F9E88A57}.CodeCov|x86.ActiveCfg = CodeCoverage|Any CPU
+ {D3313BDF-8071-4AC8-9D98-ABF7F9E88A57}.CodeCoverage|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {D3313BDF-8071-4AC8-9D98-ABF7F9E88A57}.CodeCoverage|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {D3313BDF-8071-4AC8-9D98-ABF7F9E88A57}.CodeCoverage|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {D3313BDF-8071-4AC8-9D98-ABF7F9E88A57}.CodeCoverage|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {D3313BDF-8071-4AC8-9D98-ABF7F9E88A57}.CodeCoverage|x86.ActiveCfg = CodeCoverage|Any CPU
+ {D3313BDF-8071-4AC8-9D98-ABF7F9E88A57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D3313BDF-8071-4AC8-9D98-ABF7F9E88A57}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D3313BDF-8071-4AC8-9D98-ABF7F9E88A57}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {D3313BDF-8071-4AC8-9D98-ABF7F9E88A57}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {D3313BDF-8071-4AC8-9D98-ABF7F9E88A57}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {D3313BDF-8071-4AC8-9D98-ABF7F9E88A57}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D3313BDF-8071-4AC8-9D98-ABF7F9E88A57}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D3313BDF-8071-4AC8-9D98-ABF7F9E88A57}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {D3313BDF-8071-4AC8-9D98-ABF7F9E88A57}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {D3313BDF-8071-4AC8-9D98-ABF7F9E88A57}.Release|x86.ActiveCfg = Release|Any CPU
+ {66A74F3C-A106-4C1E-BAA0-001908FEA2CA}.CodeCov|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {66A74F3C-A106-4C1E-BAA0-001908FEA2CA}.CodeCov|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {66A74F3C-A106-4C1E-BAA0-001908FEA2CA}.CodeCov|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {66A74F3C-A106-4C1E-BAA0-001908FEA2CA}.CodeCov|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {66A74F3C-A106-4C1E-BAA0-001908FEA2CA}.CodeCov|x86.ActiveCfg = CodeCoverage|Any CPU
+ {66A74F3C-A106-4C1E-BAA0-001908FEA2CA}.CodeCoverage|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {66A74F3C-A106-4C1E-BAA0-001908FEA2CA}.CodeCoverage|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {66A74F3C-A106-4C1E-BAA0-001908FEA2CA}.CodeCoverage|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {66A74F3C-A106-4C1E-BAA0-001908FEA2CA}.CodeCoverage|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {66A74F3C-A106-4C1E-BAA0-001908FEA2CA}.CodeCoverage|x86.ActiveCfg = CodeCoverage|Any CPU
+ {66A74F3C-A106-4C1E-BAA0-001908FEA2CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {66A74F3C-A106-4C1E-BAA0-001908FEA2CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {66A74F3C-A106-4C1E-BAA0-001908FEA2CA}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {66A74F3C-A106-4C1E-BAA0-001908FEA2CA}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {66A74F3C-A106-4C1E-BAA0-001908FEA2CA}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {66A74F3C-A106-4C1E-BAA0-001908FEA2CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {66A74F3C-A106-4C1E-BAA0-001908FEA2CA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {66A74F3C-A106-4C1E-BAA0-001908FEA2CA}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {66A74F3C-A106-4C1E-BAA0-001908FEA2CA}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {66A74F3C-A106-4C1E-BAA0-001908FEA2CA}.Release|x86.ActiveCfg = Release|Any CPU
+ {E2D008A9-4D1D-4F6B-8325-4ED717D6EA0A}.CodeCov|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {E2D008A9-4D1D-4F6B-8325-4ED717D6EA0A}.CodeCov|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {E2D008A9-4D1D-4F6B-8325-4ED717D6EA0A}.CodeCov|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {E2D008A9-4D1D-4F6B-8325-4ED717D6EA0A}.CodeCov|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {E2D008A9-4D1D-4F6B-8325-4ED717D6EA0A}.CodeCov|x86.ActiveCfg = CodeCoverage|Any CPU
+ {E2D008A9-4D1D-4F6B-8325-4ED717D6EA0A}.CodeCoverage|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {E2D008A9-4D1D-4F6B-8325-4ED717D6EA0A}.CodeCoverage|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {E2D008A9-4D1D-4F6B-8325-4ED717D6EA0A}.CodeCoverage|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {E2D008A9-4D1D-4F6B-8325-4ED717D6EA0A}.CodeCoverage|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {E2D008A9-4D1D-4F6B-8325-4ED717D6EA0A}.CodeCoverage|x86.ActiveCfg = CodeCoverage|Any CPU
+ {E2D008A9-4D1D-4F6B-8325-4ED717D6EA0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E2D008A9-4D1D-4F6B-8325-4ED717D6EA0A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E2D008A9-4D1D-4F6B-8325-4ED717D6EA0A}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {E2D008A9-4D1D-4F6B-8325-4ED717D6EA0A}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {E2D008A9-4D1D-4F6B-8325-4ED717D6EA0A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E2D008A9-4D1D-4F6B-8325-4ED717D6EA0A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E2D008A9-4D1D-4F6B-8325-4ED717D6EA0A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E2D008A9-4D1D-4F6B-8325-4ED717D6EA0A}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {E2D008A9-4D1D-4F6B-8325-4ED717D6EA0A}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {E2D008A9-4D1D-4F6B-8325-4ED717D6EA0A}.Release|x86.ActiveCfg = Release|Any CPU
+ {CD48EB41-92A5-4628-A0F7-6A43DF58827E}.CodeCov|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {CD48EB41-92A5-4628-A0F7-6A43DF58827E}.CodeCov|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {CD48EB41-92A5-4628-A0F7-6A43DF58827E}.CodeCov|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {CD48EB41-92A5-4628-A0F7-6A43DF58827E}.CodeCov|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {CD48EB41-92A5-4628-A0F7-6A43DF58827E}.CodeCov|x86.ActiveCfg = CodeCoverage|Any CPU
+ {CD48EB41-92A5-4628-A0F7-6A43DF58827E}.CodeCoverage|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {CD48EB41-92A5-4628-A0F7-6A43DF58827E}.CodeCoverage|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {CD48EB41-92A5-4628-A0F7-6A43DF58827E}.CodeCoverage|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {CD48EB41-92A5-4628-A0F7-6A43DF58827E}.CodeCoverage|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {CD48EB41-92A5-4628-A0F7-6A43DF58827E}.CodeCoverage|x86.ActiveCfg = CodeCoverage|Any CPU
+ {CD48EB41-92A5-4628-A0F7-6A43DF58827E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CD48EB41-92A5-4628-A0F7-6A43DF58827E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CD48EB41-92A5-4628-A0F7-6A43DF58827E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {CD48EB41-92A5-4628-A0F7-6A43DF58827E}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {CD48EB41-92A5-4628-A0F7-6A43DF58827E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {CD48EB41-92A5-4628-A0F7-6A43DF58827E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CD48EB41-92A5-4628-A0F7-6A43DF58827E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CD48EB41-92A5-4628-A0F7-6A43DF58827E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {CD48EB41-92A5-4628-A0F7-6A43DF58827E}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {CD48EB41-92A5-4628-A0F7-6A43DF58827E}.Release|x86.ActiveCfg = Release|Any CPU
+ {2C653A66-8159-4A41-954F-A67915DFDA87}.CodeCov|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {2C653A66-8159-4A41-954F-A67915DFDA87}.CodeCov|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {2C653A66-8159-4A41-954F-A67915DFDA87}.CodeCov|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {2C653A66-8159-4A41-954F-A67915DFDA87}.CodeCov|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {2C653A66-8159-4A41-954F-A67915DFDA87}.CodeCov|x86.ActiveCfg = CodeCoverage|Any CPU
+ {2C653A66-8159-4A41-954F-A67915DFDA87}.CodeCoverage|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {2C653A66-8159-4A41-954F-A67915DFDA87}.CodeCoverage|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {2C653A66-8159-4A41-954F-A67915DFDA87}.CodeCoverage|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {2C653A66-8159-4A41-954F-A67915DFDA87}.CodeCoverage|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {2C653A66-8159-4A41-954F-A67915DFDA87}.CodeCoverage|x86.ActiveCfg = CodeCoverage|Any CPU
+ {2C653A66-8159-4A41-954F-A67915DFDA87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2C653A66-8159-4A41-954F-A67915DFDA87}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2C653A66-8159-4A41-954F-A67915DFDA87}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {2C653A66-8159-4A41-954F-A67915DFDA87}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {2C653A66-8159-4A41-954F-A67915DFDA87}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {2C653A66-8159-4A41-954F-A67915DFDA87}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2C653A66-8159-4A41-954F-A67915DFDA87}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2C653A66-8159-4A41-954F-A67915DFDA87}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {2C653A66-8159-4A41-954F-A67915DFDA87}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {2C653A66-8159-4A41-954F-A67915DFDA87}.Release|x86.ActiveCfg = Release|Any CPU
+ {21C729D6-ECF8-47EF-A236-7C6A4272EAF0}.CodeCov|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {21C729D6-ECF8-47EF-A236-7C6A4272EAF0}.CodeCov|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {21C729D6-ECF8-47EF-A236-7C6A4272EAF0}.CodeCov|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {21C729D6-ECF8-47EF-A236-7C6A4272EAF0}.CodeCov|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {21C729D6-ECF8-47EF-A236-7C6A4272EAF0}.CodeCov|x86.ActiveCfg = CodeCoverage|Any CPU
+ {21C729D6-ECF8-47EF-A236-7C6A4272EAF0}.CodeCoverage|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {21C729D6-ECF8-47EF-A236-7C6A4272EAF0}.CodeCoverage|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {21C729D6-ECF8-47EF-A236-7C6A4272EAF0}.CodeCoverage|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {21C729D6-ECF8-47EF-A236-7C6A4272EAF0}.CodeCoverage|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {21C729D6-ECF8-47EF-A236-7C6A4272EAF0}.CodeCoverage|x86.ActiveCfg = CodeCoverage|Any CPU
+ {21C729D6-ECF8-47EF-A236-7C6A4272EAF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {21C729D6-ECF8-47EF-A236-7C6A4272EAF0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {21C729D6-ECF8-47EF-A236-7C6A4272EAF0}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {21C729D6-ECF8-47EF-A236-7C6A4272EAF0}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {21C729D6-ECF8-47EF-A236-7C6A4272EAF0}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {21C729D6-ECF8-47EF-A236-7C6A4272EAF0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {21C729D6-ECF8-47EF-A236-7C6A4272EAF0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {21C729D6-ECF8-47EF-A236-7C6A4272EAF0}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {21C729D6-ECF8-47EF-A236-7C6A4272EAF0}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {21C729D6-ECF8-47EF-A236-7C6A4272EAF0}.Release|x86.ActiveCfg = Release|Any CPU
+ {8AC2A2E4-2F11-4D40-A887-62E2583A65E6}.CodeCov|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {8AC2A2E4-2F11-4D40-A887-62E2583A65E6}.CodeCov|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {8AC2A2E4-2F11-4D40-A887-62E2583A65E6}.CodeCov|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {8AC2A2E4-2F11-4D40-A887-62E2583A65E6}.CodeCov|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {8AC2A2E4-2F11-4D40-A887-62E2583A65E6}.CodeCov|x86.ActiveCfg = CodeCoverage|Any CPU
+ {8AC2A2E4-2F11-4D40-A887-62E2583A65E6}.CodeCoverage|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {8AC2A2E4-2F11-4D40-A887-62E2583A65E6}.CodeCoverage|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {8AC2A2E4-2F11-4D40-A887-62E2583A65E6}.CodeCoverage|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {8AC2A2E4-2F11-4D40-A887-62E2583A65E6}.CodeCoverage|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {8AC2A2E4-2F11-4D40-A887-62E2583A65E6}.CodeCoverage|x86.ActiveCfg = CodeCoverage|Any CPU
+ {8AC2A2E4-2F11-4D40-A887-62E2583A65E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8AC2A2E4-2F11-4D40-A887-62E2583A65E6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8AC2A2E4-2F11-4D40-A887-62E2583A65E6}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {8AC2A2E4-2F11-4D40-A887-62E2583A65E6}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {8AC2A2E4-2F11-4D40-A887-62E2583A65E6}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {8AC2A2E4-2F11-4D40-A887-62E2583A65E6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8AC2A2E4-2F11-4D40-A887-62E2583A65E6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8AC2A2E4-2F11-4D40-A887-62E2583A65E6}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {8AC2A2E4-2F11-4D40-A887-62E2583A65E6}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {8AC2A2E4-2F11-4D40-A887-62E2583A65E6}.Release|x86.ActiveCfg = Release|Any CPU
+ {6C28DA70-60F1-4442-967F-591BF3962EC5}.CodeCov|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {6C28DA70-60F1-4442-967F-591BF3962EC5}.CodeCov|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {6C28DA70-60F1-4442-967F-591BF3962EC5}.CodeCov|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {6C28DA70-60F1-4442-967F-591BF3962EC5}.CodeCov|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {6C28DA70-60F1-4442-967F-591BF3962EC5}.CodeCov|x86.ActiveCfg = CodeCoverage|Any CPU
+ {6C28DA70-60F1-4442-967F-591BF3962EC5}.CodeCoverage|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {6C28DA70-60F1-4442-967F-591BF3962EC5}.CodeCoverage|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {6C28DA70-60F1-4442-967F-591BF3962EC5}.CodeCoverage|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {6C28DA70-60F1-4442-967F-591BF3962EC5}.CodeCoverage|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {6C28DA70-60F1-4442-967F-591BF3962EC5}.CodeCoverage|x86.ActiveCfg = CodeCoverage|Any CPU
+ {6C28DA70-60F1-4442-967F-591BF3962EC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6C28DA70-60F1-4442-967F-591BF3962EC5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6C28DA70-60F1-4442-967F-591BF3962EC5}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {6C28DA70-60F1-4442-967F-591BF3962EC5}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {6C28DA70-60F1-4442-967F-591BF3962EC5}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {6C28DA70-60F1-4442-967F-591BF3962EC5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6C28DA70-60F1-4442-967F-591BF3962EC5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6C28DA70-60F1-4442-967F-591BF3962EC5}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {6C28DA70-60F1-4442-967F-591BF3962EC5}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {6C28DA70-60F1-4442-967F-591BF3962EC5}.Release|x86.ActiveCfg = Release|Any CPU
+ {DDC1CE0C-486E-4E35-BB3B-EAB61F8F9440}.CodeCov|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {DDC1CE0C-486E-4E35-BB3B-EAB61F8F9440}.CodeCov|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {DDC1CE0C-486E-4E35-BB3B-EAB61F8F9440}.CodeCov|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {DDC1CE0C-486E-4E35-BB3B-EAB61F8F9440}.CodeCov|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {DDC1CE0C-486E-4E35-BB3B-EAB61F8F9440}.CodeCov|x86.ActiveCfg = CodeCoverage|Any CPU
+ {DDC1CE0C-486E-4E35-BB3B-EAB61F8F9440}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU
+ {DDC1CE0C-486E-4E35-BB3B-EAB61F8F9440}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU
+ {DDC1CE0C-486E-4E35-BB3B-EAB61F8F9440}.CodeCoverage|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {DDC1CE0C-486E-4E35-BB3B-EAB61F8F9440}.CodeCoverage|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {DDC1CE0C-486E-4E35-BB3B-EAB61F8F9440}.CodeCoverage|x86.ActiveCfg = CodeCoverage|Any CPU
+ {DDC1CE0C-486E-4E35-BB3B-EAB61F8F9440}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DDC1CE0C-486E-4E35-BB3B-EAB61F8F9440}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DDC1CE0C-486E-4E35-BB3B-EAB61F8F9440}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {DDC1CE0C-486E-4E35-BB3B-EAB61F8F9440}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {DDC1CE0C-486E-4E35-BB3B-EAB61F8F9440}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {DDC1CE0C-486E-4E35-BB3B-EAB61F8F9440}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DDC1CE0C-486E-4E35-BB3B-EAB61F8F9440}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DDC1CE0C-486E-4E35-BB3B-EAB61F8F9440}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {DDC1CE0C-486E-4E35-BB3B-EAB61F8F9440}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {DDC1CE0C-486E-4E35-BB3B-EAB61F8F9440}.Release|x86.ActiveCfg = Release|Any CPU
+ {7F2C796F-43B2-4F8F-ABFF-A154EC8AAFA1}.CodeCov|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {7F2C796F-43B2-4F8F-ABFF-A154EC8AAFA1}.CodeCov|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {7F2C796F-43B2-4F8F-ABFF-A154EC8AAFA1}.CodeCov|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {7F2C796F-43B2-4F8F-ABFF-A154EC8AAFA1}.CodeCov|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {7F2C796F-43B2-4F8F-ABFF-A154EC8AAFA1}.CodeCov|x86.ActiveCfg = CodeCoverage|Any CPU
+ {7F2C796F-43B2-4F8F-ABFF-A154EC8AAFA1}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU
+ {7F2C796F-43B2-4F8F-ABFF-A154EC8AAFA1}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU
+ {7F2C796F-43B2-4F8F-ABFF-A154EC8AAFA1}.CodeCoverage|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {7F2C796F-43B2-4F8F-ABFF-A154EC8AAFA1}.CodeCoverage|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {7F2C796F-43B2-4F8F-ABFF-A154EC8AAFA1}.CodeCoverage|x86.ActiveCfg = CodeCoverage|Any CPU
+ {7F2C796F-43B2-4F8F-ABFF-A154EC8AAFA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7F2C796F-43B2-4F8F-ABFF-A154EC8AAFA1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7F2C796F-43B2-4F8F-ABFF-A154EC8AAFA1}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {7F2C796F-43B2-4F8F-ABFF-A154EC8AAFA1}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {7F2C796F-43B2-4F8F-ABFF-A154EC8AAFA1}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {7F2C796F-43B2-4F8F-ABFF-A154EC8AAFA1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7F2C796F-43B2-4F8F-ABFF-A154EC8AAFA1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7F2C796F-43B2-4F8F-ABFF-A154EC8AAFA1}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {7F2C796F-43B2-4F8F-ABFF-A154EC8AAFA1}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {7F2C796F-43B2-4F8F-ABFF-A154EC8AAFA1}.Release|x86.ActiveCfg = Release|Any CPU
+ {F0441BE9-BDC0-4629-BE5A-8765FFAA2481}.CodeCov|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {F0441BE9-BDC0-4629-BE5A-8765FFAA2481}.CodeCov|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {F0441BE9-BDC0-4629-BE5A-8765FFAA2481}.CodeCov|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {F0441BE9-BDC0-4629-BE5A-8765FFAA2481}.CodeCov|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {F0441BE9-BDC0-4629-BE5A-8765FFAA2481}.CodeCov|x86.ActiveCfg = CodeCoverage|Any CPU
+ {F0441BE9-BDC0-4629-BE5A-8765FFAA2481}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU
+ {F0441BE9-BDC0-4629-BE5A-8765FFAA2481}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU
+ {F0441BE9-BDC0-4629-BE5A-8765FFAA2481}.CodeCoverage|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {F0441BE9-BDC0-4629-BE5A-8765FFAA2481}.CodeCoverage|Mixed Platforms.Build.0 = Release|Any CPU
+ {F0441BE9-BDC0-4629-BE5A-8765FFAA2481}.CodeCoverage|x86.ActiveCfg = Release|Any CPU
+ {F0441BE9-BDC0-4629-BE5A-8765FFAA2481}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F0441BE9-BDC0-4629-BE5A-8765FFAA2481}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F0441BE9-BDC0-4629-BE5A-8765FFAA2481}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {F0441BE9-BDC0-4629-BE5A-8765FFAA2481}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {F0441BE9-BDC0-4629-BE5A-8765FFAA2481}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {F0441BE9-BDC0-4629-BE5A-8765FFAA2481}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F0441BE9-BDC0-4629-BE5A-8765FFAA2481}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F0441BE9-BDC0-4629-BE5A-8765FFAA2481}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {F0441BE9-BDC0-4629-BE5A-8765FFAA2481}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {F0441BE9-BDC0-4629-BE5A-8765FFAA2481}.Release|x86.ActiveCfg = Release|Any CPU
+ {668E9021-CE84-49D9-98FB-DF125A9FCDB0}.CodeCov|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {668E9021-CE84-49D9-98FB-DF125A9FCDB0}.CodeCov|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {668E9021-CE84-49D9-98FB-DF125A9FCDB0}.CodeCov|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {668E9021-CE84-49D9-98FB-DF125A9FCDB0}.CodeCov|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {668E9021-CE84-49D9-98FB-DF125A9FCDB0}.CodeCov|x86.ActiveCfg = CodeCoverage|Any CPU
+ {668E9021-CE84-49D9-98FB-DF125A9FCDB0}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU
+ {668E9021-CE84-49D9-98FB-DF125A9FCDB0}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU
+ {668E9021-CE84-49D9-98FB-DF125A9FCDB0}.CodeCoverage|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {668E9021-CE84-49D9-98FB-DF125A9FCDB0}.CodeCoverage|Mixed Platforms.Build.0 = Release|Any CPU
+ {668E9021-CE84-49D9-98FB-DF125A9FCDB0}.CodeCoverage|x86.ActiveCfg = Release|Any CPU
+ {668E9021-CE84-49D9-98FB-DF125A9FCDB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {668E9021-CE84-49D9-98FB-DF125A9FCDB0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {668E9021-CE84-49D9-98FB-DF125A9FCDB0}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {668E9021-CE84-49D9-98FB-DF125A9FCDB0}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {668E9021-CE84-49D9-98FB-DF125A9FCDB0}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {668E9021-CE84-49D9-98FB-DF125A9FCDB0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {668E9021-CE84-49D9-98FB-DF125A9FCDB0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {668E9021-CE84-49D9-98FB-DF125A9FCDB0}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {668E9021-CE84-49D9-98FB-DF125A9FCDB0}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {668E9021-CE84-49D9-98FB-DF125A9FCDB0}.Release|x86.ActiveCfg = Release|Any CPU
+ {FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0}.CodeCov|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0}.CodeCov|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0}.CodeCov|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0}.CodeCov|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0}.CodeCov|x86.ActiveCfg = CodeCoverage|Any CPU
+ {FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU
+ {FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU
+ {FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0}.CodeCoverage|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0}.CodeCoverage|Mixed Platforms.Build.0 = Release|Any CPU
+ {FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0}.CodeCoverage|x86.ActiveCfg = Release|Any CPU
+ {FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0}.Release|x86.ActiveCfg = Release|Any CPU
+ {EB09CD33-992B-4A31-AB95-8673BA90F1CD}.CodeCov|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {EB09CD33-992B-4A31-AB95-8673BA90F1CD}.CodeCov|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {EB09CD33-992B-4A31-AB95-8673BA90F1CD}.CodeCov|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {EB09CD33-992B-4A31-AB95-8673BA90F1CD}.CodeCov|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {EB09CD33-992B-4A31-AB95-8673BA90F1CD}.CodeCov|x86.ActiveCfg = CodeCoverage|Any CPU
+ {EB09CD33-992B-4A31-AB95-8673BA90F1CD}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU
+ {EB09CD33-992B-4A31-AB95-8673BA90F1CD}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU
+ {EB09CD33-992B-4A31-AB95-8673BA90F1CD}.CodeCoverage|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {EB09CD33-992B-4A31-AB95-8673BA90F1CD}.CodeCoverage|Mixed Platforms.Build.0 = Release|Any CPU
+ {EB09CD33-992B-4A31-AB95-8673BA90F1CD}.CodeCoverage|x86.ActiveCfg = Release|Any CPU
+ {EB09CD33-992B-4A31-AB95-8673BA90F1CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EB09CD33-992B-4A31-AB95-8673BA90F1CD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EB09CD33-992B-4A31-AB95-8673BA90F1CD}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {EB09CD33-992B-4A31-AB95-8673BA90F1CD}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {EB09CD33-992B-4A31-AB95-8673BA90F1CD}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {EB09CD33-992B-4A31-AB95-8673BA90F1CD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {EB09CD33-992B-4A31-AB95-8673BA90F1CD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {EB09CD33-992B-4A31-AB95-8673BA90F1CD}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {EB09CD33-992B-4A31-AB95-8673BA90F1CD}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {EB09CD33-992B-4A31-AB95-8673BA90F1CD}.Release|x86.ActiveCfg = Release|Any CPU
+ {7AF77741-9158-4D5F-8782-8F21FADF025F}.CodeCov|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {7AF77741-9158-4D5F-8782-8F21FADF025F}.CodeCov|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {7AF77741-9158-4D5F-8782-8F21FADF025F}.CodeCov|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {7AF77741-9158-4D5F-8782-8F21FADF025F}.CodeCov|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {7AF77741-9158-4D5F-8782-8F21FADF025F}.CodeCov|x86.ActiveCfg = CodeCoverage|Any CPU
+ {7AF77741-9158-4D5F-8782-8F21FADF025F}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU
+ {7AF77741-9158-4D5F-8782-8F21FADF025F}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU
+ {7AF77741-9158-4D5F-8782-8F21FADF025F}.CodeCoverage|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {7AF77741-9158-4D5F-8782-8F21FADF025F}.CodeCoverage|Mixed Platforms.Build.0 = Release|Any CPU
+ {7AF77741-9158-4D5F-8782-8F21FADF025F}.CodeCoverage|x86.ActiveCfg = Release|Any CPU
+ {7AF77741-9158-4D5F-8782-8F21FADF025F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7AF77741-9158-4D5F-8782-8F21FADF025F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7AF77741-9158-4D5F-8782-8F21FADF025F}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {7AF77741-9158-4D5F-8782-8F21FADF025F}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {7AF77741-9158-4D5F-8782-8F21FADF025F}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {7AF77741-9158-4D5F-8782-8F21FADF025F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7AF77741-9158-4D5F-8782-8F21FADF025F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7AF77741-9158-4D5F-8782-8F21FADF025F}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {7AF77741-9158-4D5F-8782-8F21FADF025F}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {7AF77741-9158-4D5F-8782-8F21FADF025F}.Release|x86.ActiveCfg = Release|Any CPU
+ {A7B1264E-BCE5-42A8-8B5E-001A5360B128}.CodeCov|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {A7B1264E-BCE5-42A8-8B5E-001A5360B128}.CodeCov|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {A7B1264E-BCE5-42A8-8B5E-001A5360B128}.CodeCov|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {A7B1264E-BCE5-42A8-8B5E-001A5360B128}.CodeCov|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {A7B1264E-BCE5-42A8-8B5E-001A5360B128}.CodeCov|x86.ActiveCfg = CodeCoverage|Any CPU
+ {A7B1264E-BCE5-42A8-8B5E-001A5360B128}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU
+ {A7B1264E-BCE5-42A8-8B5E-001A5360B128}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU
+ {A7B1264E-BCE5-42A8-8B5E-001A5360B128}.CodeCoverage|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {A7B1264E-BCE5-42A8-8B5E-001A5360B128}.CodeCoverage|Mixed Platforms.Build.0 = Release|Any CPU
+ {A7B1264E-BCE5-42A8-8B5E-001A5360B128}.CodeCoverage|x86.ActiveCfg = Release|Any CPU
+ {A7B1264E-BCE5-42A8-8B5E-001A5360B128}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A7B1264E-BCE5-42A8-8B5E-001A5360B128}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A7B1264E-BCE5-42A8-8B5E-001A5360B128}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {A7B1264E-BCE5-42A8-8B5E-001A5360B128}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {A7B1264E-BCE5-42A8-8B5E-001A5360B128}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {A7B1264E-BCE5-42A8-8B5E-001A5360B128}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A7B1264E-BCE5-42A8-8B5E-001A5360B128}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A7B1264E-BCE5-42A8-8B5E-001A5360B128}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {A7B1264E-BCE5-42A8-8B5E-001A5360B128}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {A7B1264E-BCE5-42A8-8B5E-001A5360B128}.Release|x86.ActiveCfg = Release|Any CPU
+ {6C18CC83-1E4C-42D2-B93E-55D6C363850C}.CodeCov|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {6C18CC83-1E4C-42D2-B93E-55D6C363850C}.CodeCov|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {6C18CC83-1E4C-42D2-B93E-55D6C363850C}.CodeCov|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {6C18CC83-1E4C-42D2-B93E-55D6C363850C}.CodeCov|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {6C18CC83-1E4C-42D2-B93E-55D6C363850C}.CodeCov|x86.ActiveCfg = CodeCoverage|Any CPU
+ {6C18CC83-1E4C-42D2-B93E-55D6C363850C}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU
+ {6C18CC83-1E4C-42D2-B93E-55D6C363850C}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU
+ {6C18CC83-1E4C-42D2-B93E-55D6C363850C}.CodeCoverage|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {6C18CC83-1E4C-42D2-B93E-55D6C363850C}.CodeCoverage|Mixed Platforms.Build.0 = Release|Any CPU
+ {6C18CC83-1E4C-42D2-B93E-55D6C363850C}.CodeCoverage|x86.ActiveCfg = Release|Any CPU
+ {6C18CC83-1E4C-42D2-B93E-55D6C363850C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6C18CC83-1E4C-42D2-B93E-55D6C363850C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6C18CC83-1E4C-42D2-B93E-55D6C363850C}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {6C18CC83-1E4C-42D2-B93E-55D6C363850C}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {6C18CC83-1E4C-42D2-B93E-55D6C363850C}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {6C18CC83-1E4C-42D2-B93E-55D6C363850C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6C18CC83-1E4C-42D2-B93E-55D6C363850C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6C18CC83-1E4C-42D2-B93E-55D6C363850C}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {6C18CC83-1E4C-42D2-B93E-55D6C363850C}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {6C18CC83-1E4C-42D2-B93E-55D6C363850C}.Release|x86.ActiveCfg = Release|Any CPU
+ {03A5E5F2-2E23-48F2-ABCC-6C41BAC9AC02}.CodeCov|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {03A5E5F2-2E23-48F2-ABCC-6C41BAC9AC02}.CodeCov|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {03A5E5F2-2E23-48F2-ABCC-6C41BAC9AC02}.CodeCov|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {03A5E5F2-2E23-48F2-ABCC-6C41BAC9AC02}.CodeCov|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {03A5E5F2-2E23-48F2-ABCC-6C41BAC9AC02}.CodeCov|x86.ActiveCfg = CodeCoverage|Any CPU
+ {03A5E5F2-2E23-48F2-ABCC-6C41BAC9AC02}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU
+ {03A5E5F2-2E23-48F2-ABCC-6C41BAC9AC02}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU
+ {03A5E5F2-2E23-48F2-ABCC-6C41BAC9AC02}.CodeCoverage|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {03A5E5F2-2E23-48F2-ABCC-6C41BAC9AC02}.CodeCoverage|Mixed Platforms.Build.0 = Release|Any CPU
+ {03A5E5F2-2E23-48F2-ABCC-6C41BAC9AC02}.CodeCoverage|x86.ActiveCfg = Release|Any CPU
+ {03A5E5F2-2E23-48F2-ABCC-6C41BAC9AC02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {03A5E5F2-2E23-48F2-ABCC-6C41BAC9AC02}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {03A5E5F2-2E23-48F2-ABCC-6C41BAC9AC02}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {03A5E5F2-2E23-48F2-ABCC-6C41BAC9AC02}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {03A5E5F2-2E23-48F2-ABCC-6C41BAC9AC02}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {03A5E5F2-2E23-48F2-ABCC-6C41BAC9AC02}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {03A5E5F2-2E23-48F2-ABCC-6C41BAC9AC02}.Release|Any CPU.Build.0 = Release|Any CPU
+ {03A5E5F2-2E23-48F2-ABCC-6C41BAC9AC02}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {03A5E5F2-2E23-48F2-ABCC-6C41BAC9AC02}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {03A5E5F2-2E23-48F2-ABCC-6C41BAC9AC02}.Release|x86.ActiveCfg = Release|Any CPU
+ {66492E69-CE4C-4FB1-9B1F-88DEE09D06F1}.CodeCov|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {66492E69-CE4C-4FB1-9B1F-88DEE09D06F1}.CodeCov|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {66492E69-CE4C-4FB1-9B1F-88DEE09D06F1}.CodeCov|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {66492E69-CE4C-4FB1-9B1F-88DEE09D06F1}.CodeCov|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {66492E69-CE4C-4FB1-9B1F-88DEE09D06F1}.CodeCov|x86.ActiveCfg = CodeCoverage|Any CPU
+ {66492E69-CE4C-4FB1-9B1F-88DEE09D06F1}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU
+ {66492E69-CE4C-4FB1-9B1F-88DEE09D06F1}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU
+ {66492E69-CE4C-4FB1-9B1F-88DEE09D06F1}.CodeCoverage|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {66492E69-CE4C-4FB1-9B1F-88DEE09D06F1}.CodeCoverage|Mixed Platforms.Build.0 = Release|Any CPU
+ {66492E69-CE4C-4FB1-9B1F-88DEE09D06F1}.CodeCoverage|x86.ActiveCfg = Release|Any CPU
+ {66492E69-CE4C-4FB1-9B1F-88DEE09D06F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {66492E69-CE4C-4FB1-9B1F-88DEE09D06F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {66492E69-CE4C-4FB1-9B1F-88DEE09D06F1}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {66492E69-CE4C-4FB1-9B1F-88DEE09D06F1}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {66492E69-CE4C-4FB1-9B1F-88DEE09D06F1}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {66492E69-CE4C-4FB1-9B1F-88DEE09D06F1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {66492E69-CE4C-4FB1-9B1F-88DEE09D06F1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {66492E69-CE4C-4FB1-9B1F-88DEE09D06F1}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {66492E69-CE4C-4FB1-9B1F-88DEE09D06F1}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {66492E69-CE4C-4FB1-9B1F-88DEE09D06F1}.Release|x86.ActiveCfg = Release|Any CPU
+ {7FB5C0C0-5223-4C79-A8DA-D2A0F264A478}.CodeCov|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {7FB5C0C0-5223-4C79-A8DA-D2A0F264A478}.CodeCov|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {7FB5C0C0-5223-4C79-A8DA-D2A0F264A478}.CodeCov|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {7FB5C0C0-5223-4C79-A8DA-D2A0F264A478}.CodeCov|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {7FB5C0C0-5223-4C79-A8DA-D2A0F264A478}.CodeCov|x86.ActiveCfg = CodeCoverage|Any CPU
+ {7FB5C0C0-5223-4C79-A8DA-D2A0F264A478}.CodeCoverage|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {7FB5C0C0-5223-4C79-A8DA-D2A0F264A478}.CodeCoverage|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {7FB5C0C0-5223-4C79-A8DA-D2A0F264A478}.CodeCoverage|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {7FB5C0C0-5223-4C79-A8DA-D2A0F264A478}.CodeCoverage|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {7FB5C0C0-5223-4C79-A8DA-D2A0F264A478}.CodeCoverage|x86.ActiveCfg = CodeCoverage|Any CPU
+ {7FB5C0C0-5223-4C79-A8DA-D2A0F264A478}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7FB5C0C0-5223-4C79-A8DA-D2A0F264A478}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7FB5C0C0-5223-4C79-A8DA-D2A0F264A478}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {7FB5C0C0-5223-4C79-A8DA-D2A0F264A478}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {7FB5C0C0-5223-4C79-A8DA-D2A0F264A478}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {7FB5C0C0-5223-4C79-A8DA-D2A0F264A478}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7FB5C0C0-5223-4C79-A8DA-D2A0F264A478}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7FB5C0C0-5223-4C79-A8DA-D2A0F264A478}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {7FB5C0C0-5223-4C79-A8DA-D2A0F264A478}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {7FB5C0C0-5223-4C79-A8DA-D2A0F264A478}.Release|x86.ActiveCfg = Release|Any CPU
+ {A0187BC2-8325-4BB2-8697-7F955CF4173E}.CodeCov|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {A0187BC2-8325-4BB2-8697-7F955CF4173E}.CodeCov|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {A0187BC2-8325-4BB2-8697-7F955CF4173E}.CodeCov|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {A0187BC2-8325-4BB2-8697-7F955CF4173E}.CodeCov|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {A0187BC2-8325-4BB2-8697-7F955CF4173E}.CodeCov|x86.ActiveCfg = CodeCoverage|Any CPU
+ {A0187BC2-8325-4BB2-8697-7F955CF4173E}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU
+ {A0187BC2-8325-4BB2-8697-7F955CF4173E}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU
+ {A0187BC2-8325-4BB2-8697-7F955CF4173E}.CodeCoverage|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {A0187BC2-8325-4BB2-8697-7F955CF4173E}.CodeCoverage|Mixed Platforms.Build.0 = Release|Any CPU
+ {A0187BC2-8325-4BB2-8697-7F955CF4173E}.CodeCoverage|x86.ActiveCfg = Release|Any CPU
+ {A0187BC2-8325-4BB2-8697-7F955CF4173E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A0187BC2-8325-4BB2-8697-7F955CF4173E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A0187BC2-8325-4BB2-8697-7F955CF4173E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {A0187BC2-8325-4BB2-8697-7F955CF4173E}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {A0187BC2-8325-4BB2-8697-7F955CF4173E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {A0187BC2-8325-4BB2-8697-7F955CF4173E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A0187BC2-8325-4BB2-8697-7F955CF4173E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A0187BC2-8325-4BB2-8697-7F955CF4173E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {A0187BC2-8325-4BB2-8697-7F955CF4173E}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {A0187BC2-8325-4BB2-8697-7F955CF4173E}.Release|x86.ActiveCfg = Release|Any CPU
+ {ACE91549-D86E-4EB6-8C2A-5FF51386BB68}.CodeCov|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {ACE91549-D86E-4EB6-8C2A-5FF51386BB68}.CodeCov|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {ACE91549-D86E-4EB6-8C2A-5FF51386BB68}.CodeCov|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {ACE91549-D86E-4EB6-8C2A-5FF51386BB68}.CodeCov|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {ACE91549-D86E-4EB6-8C2A-5FF51386BB68}.CodeCov|x86.ActiveCfg = CodeCoverage|Any CPU
+ {ACE91549-D86E-4EB6-8C2A-5FF51386BB68}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU
+ {ACE91549-D86E-4EB6-8C2A-5FF51386BB68}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU
+ {ACE91549-D86E-4EB6-8C2A-5FF51386BB68}.CodeCoverage|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {ACE91549-D86E-4EB6-8C2A-5FF51386BB68}.CodeCoverage|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {ACE91549-D86E-4EB6-8C2A-5FF51386BB68}.CodeCoverage|x86.ActiveCfg = Release|Any CPU
+ {ACE91549-D86E-4EB6-8C2A-5FF51386BB68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {ACE91549-D86E-4EB6-8C2A-5FF51386BB68}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {ACE91549-D86E-4EB6-8C2A-5FF51386BB68}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {ACE91549-D86E-4EB6-8C2A-5FF51386BB68}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {ACE91549-D86E-4EB6-8C2A-5FF51386BB68}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {ACE91549-D86E-4EB6-8C2A-5FF51386BB68}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {ACE91549-D86E-4EB6-8C2A-5FF51386BB68}.Release|Any CPU.Build.0 = Release|Any CPU
+ {ACE91549-D86E-4EB6-8C2A-5FF51386BB68}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {ACE91549-D86E-4EB6-8C2A-5FF51386BB68}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {ACE91549-D86E-4EB6-8C2A-5FF51386BB68}.Release|x86.ActiveCfg = Release|Any CPU
+ {653F3946-541C-42D3-BBC1-CE89B392BDA9}.CodeCov|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {653F3946-541C-42D3-BBC1-CE89B392BDA9}.CodeCov|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {653F3946-541C-42D3-BBC1-CE89B392BDA9}.CodeCov|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {653F3946-541C-42D3-BBC1-CE89B392BDA9}.CodeCov|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {653F3946-541C-42D3-BBC1-CE89B392BDA9}.CodeCov|x86.ActiveCfg = CodeCoverage|Any CPU
+ {653F3946-541C-42D3-BBC1-CE89B392BDA9}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU
+ {653F3946-541C-42D3-BBC1-CE89B392BDA9}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU
+ {653F3946-541C-42D3-BBC1-CE89B392BDA9}.CodeCoverage|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {653F3946-541C-42D3-BBC1-CE89B392BDA9}.CodeCoverage|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {653F3946-541C-42D3-BBC1-CE89B392BDA9}.CodeCoverage|x86.ActiveCfg = Release|Any CPU
+ {653F3946-541C-42D3-BBC1-CE89B392BDA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {653F3946-541C-42D3-BBC1-CE89B392BDA9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {653F3946-541C-42D3-BBC1-CE89B392BDA9}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {653F3946-541C-42D3-BBC1-CE89B392BDA9}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {653F3946-541C-42D3-BBC1-CE89B392BDA9}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {653F3946-541C-42D3-BBC1-CE89B392BDA9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {653F3946-541C-42D3-BBC1-CE89B392BDA9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {653F3946-541C-42D3-BBC1-CE89B392BDA9}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {653F3946-541C-42D3-BBC1-CE89B392BDA9}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {653F3946-541C-42D3-BBC1-CE89B392BDA9}.Release|x86.ActiveCfg = Release|Any CPU
+ {81876811-6C36-492A-9609-F0E85990FBC9}.CodeCov|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {81876811-6C36-492A-9609-F0E85990FBC9}.CodeCov|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {81876811-6C36-492A-9609-F0E85990FBC9}.CodeCov|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {81876811-6C36-492A-9609-F0E85990FBC9}.CodeCov|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {81876811-6C36-492A-9609-F0E85990FBC9}.CodeCov|x86.ActiveCfg = CodeCoverage|Any CPU
+ {81876811-6C36-492A-9609-F0E85990FBC9}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU
+ {81876811-6C36-492A-9609-F0E85990FBC9}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU
+ {81876811-6C36-492A-9609-F0E85990FBC9}.CodeCoverage|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {81876811-6C36-492A-9609-F0E85990FBC9}.CodeCoverage|Mixed Platforms.Build.0 = Release|Any CPU
+ {81876811-6C36-492A-9609-F0E85990FBC9}.CodeCoverage|x86.ActiveCfg = Release|Any CPU
+ {81876811-6C36-492A-9609-F0E85990FBC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {81876811-6C36-492A-9609-F0E85990FBC9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {81876811-6C36-492A-9609-F0E85990FBC9}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {81876811-6C36-492A-9609-F0E85990FBC9}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {81876811-6C36-492A-9609-F0E85990FBC9}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {81876811-6C36-492A-9609-F0E85990FBC9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {81876811-6C36-492A-9609-F0E85990FBC9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {81876811-6C36-492A-9609-F0E85990FBC9}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {81876811-6C36-492A-9609-F0E85990FBC9}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {81876811-6C36-492A-9609-F0E85990FBC9}.Release|x86.ActiveCfg = Release|Any CPU
+ {3267DFC6-B34D-4011-BC0F-D3B56AF6F608}.CodeCov|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {3267DFC6-B34D-4011-BC0F-D3B56AF6F608}.CodeCov|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {3267DFC6-B34D-4011-BC0F-D3B56AF6F608}.CodeCov|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {3267DFC6-B34D-4011-BC0F-D3B56AF6F608}.CodeCov|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {3267DFC6-B34D-4011-BC0F-D3B56AF6F608}.CodeCov|x86.ActiveCfg = CodeCoverage|Any CPU
+ {3267DFC6-B34D-4011-BC0F-D3B56AF6F608}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU
+ {3267DFC6-B34D-4011-BC0F-D3B56AF6F608}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU
+ {3267DFC6-B34D-4011-BC0F-D3B56AF6F608}.CodeCoverage|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {3267DFC6-B34D-4011-BC0F-D3B56AF6F608}.CodeCoverage|Mixed Platforms.Build.0 = Release|Any CPU
+ {3267DFC6-B34D-4011-BC0F-D3B56AF6F608}.CodeCoverage|x86.ActiveCfg = Release|Any CPU
+ {3267DFC6-B34D-4011-BC0F-D3B56AF6F608}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3267DFC6-B34D-4011-BC0F-D3B56AF6F608}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3267DFC6-B34D-4011-BC0F-D3B56AF6F608}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {3267DFC6-B34D-4011-BC0F-D3B56AF6F608}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {3267DFC6-B34D-4011-BC0F-D3B56AF6F608}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {3267DFC6-B34D-4011-BC0F-D3B56AF6F608}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3267DFC6-B34D-4011-BC0F-D3B56AF6F608}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3267DFC6-B34D-4011-BC0F-D3B56AF6F608}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {3267DFC6-B34D-4011-BC0F-D3B56AF6F608}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {3267DFC6-B34D-4011-BC0F-D3B56AF6F608}.Release|x86.ActiveCfg = Release|Any CPU
+ {EA62944F-BD25-4730-9405-9BE8FF5BEACD}.CodeCov|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {EA62944F-BD25-4730-9405-9BE8FF5BEACD}.CodeCov|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {EA62944F-BD25-4730-9405-9BE8FF5BEACD}.CodeCov|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {EA62944F-BD25-4730-9405-9BE8FF5BEACD}.CodeCov|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {EA62944F-BD25-4730-9405-9BE8FF5BEACD}.CodeCov|x86.ActiveCfg = CodeCoverage|Any CPU
+ {EA62944F-BD25-4730-9405-9BE8FF5BEACD}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU
+ {EA62944F-BD25-4730-9405-9BE8FF5BEACD}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU
+ {EA62944F-BD25-4730-9405-9BE8FF5BEACD}.CodeCoverage|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {EA62944F-BD25-4730-9405-9BE8FF5BEACD}.CodeCoverage|Mixed Platforms.Build.0 = Release|Any CPU
+ {EA62944F-BD25-4730-9405-9BE8FF5BEACD}.CodeCoverage|x86.ActiveCfg = Release|Any CPU
+ {EA62944F-BD25-4730-9405-9BE8FF5BEACD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EA62944F-BD25-4730-9405-9BE8FF5BEACD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EA62944F-BD25-4730-9405-9BE8FF5BEACD}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {EA62944F-BD25-4730-9405-9BE8FF5BEACD}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {EA62944F-BD25-4730-9405-9BE8FF5BEACD}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {EA62944F-BD25-4730-9405-9BE8FF5BEACD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {EA62944F-BD25-4730-9405-9BE8FF5BEACD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {EA62944F-BD25-4730-9405-9BE8FF5BEACD}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {EA62944F-BD25-4730-9405-9BE8FF5BEACD}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {EA62944F-BD25-4730-9405-9BE8FF5BEACD}.Release|x86.ActiveCfg = Release|Any CPU
+ {B6895A1B-382F-4A69-99EC-E965E19B0AB3}.CodeCov|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {B6895A1B-382F-4A69-99EC-E965E19B0AB3}.CodeCov|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {B6895A1B-382F-4A69-99EC-E965E19B0AB3}.CodeCov|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {B6895A1B-382F-4A69-99EC-E965E19B0AB3}.CodeCov|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {B6895A1B-382F-4A69-99EC-E965E19B0AB3}.CodeCov|x86.ActiveCfg = CodeCoverage|Any CPU
+ {B6895A1B-382F-4A69-99EC-E965E19B0AB3}.CodeCoverage|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {B6895A1B-382F-4A69-99EC-E965E19B0AB3}.CodeCoverage|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {B6895A1B-382F-4A69-99EC-E965E19B0AB3}.CodeCoverage|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {B6895A1B-382F-4A69-99EC-E965E19B0AB3}.CodeCoverage|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {B6895A1B-382F-4A69-99EC-E965E19B0AB3}.CodeCoverage|x86.ActiveCfg = CodeCoverage|Any CPU
+ {B6895A1B-382F-4A69-99EC-E965E19B0AB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B6895A1B-382F-4A69-99EC-E965E19B0AB3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B6895A1B-382F-4A69-99EC-E965E19B0AB3}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {B6895A1B-382F-4A69-99EC-E965E19B0AB3}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {B6895A1B-382F-4A69-99EC-E965E19B0AB3}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {B6895A1B-382F-4A69-99EC-E965E19B0AB3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B6895A1B-382F-4A69-99EC-E965E19B0AB3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B6895A1B-382F-4A69-99EC-E965E19B0AB3}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {B6895A1B-382F-4A69-99EC-E965E19B0AB3}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {B6895A1B-382F-4A69-99EC-E965E19B0AB3}.Release|x86.ActiveCfg = Release|Any CPU
+ {1ACEF677-B6A0-4680-A076-7893DE176D6B}.CodeCov|Any CPU.ActiveCfg = CodeCov|Any CPU
+ {1ACEF677-B6A0-4680-A076-7893DE176D6B}.CodeCov|Any CPU.Build.0 = CodeCov|Any CPU
+ {1ACEF677-B6A0-4680-A076-7893DE176D6B}.CodeCov|Mixed Platforms.ActiveCfg = CodeCov|Any CPU
+ {1ACEF677-B6A0-4680-A076-7893DE176D6B}.CodeCov|Mixed Platforms.Build.0 = CodeCov|Any CPU
+ {1ACEF677-B6A0-4680-A076-7893DE176D6B}.CodeCov|x86.ActiveCfg = CodeCov|Any CPU
+ {1ACEF677-B6A0-4680-A076-7893DE176D6B}.CodeCoverage|Any CPU.ActiveCfg = CodeCov|Any CPU
+ {1ACEF677-B6A0-4680-A076-7893DE176D6B}.CodeCoverage|Any CPU.Build.0 = CodeCov|Any CPU
+ {1ACEF677-B6A0-4680-A076-7893DE176D6B}.CodeCoverage|Mixed Platforms.ActiveCfg = CodeCov|Any CPU
+ {1ACEF677-B6A0-4680-A076-7893DE176D6B}.CodeCoverage|Mixed Platforms.Build.0 = CodeCov|Any CPU
+ {1ACEF677-B6A0-4680-A076-7893DE176D6B}.CodeCoverage|x86.ActiveCfg = CodeCov|Any CPU
+ {1ACEF677-B6A0-4680-A076-7893DE176D6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1ACEF677-B6A0-4680-A076-7893DE176D6B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1ACEF677-B6A0-4680-A076-7893DE176D6B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {1ACEF677-B6A0-4680-A076-7893DE176D6B}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {1ACEF677-B6A0-4680-A076-7893DE176D6B}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {1ACEF677-B6A0-4680-A076-7893DE176D6B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1ACEF677-B6A0-4680-A076-7893DE176D6B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1ACEF677-B6A0-4680-A076-7893DE176D6B}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {1ACEF677-B6A0-4680-A076-7893DE176D6B}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {1ACEF677-B6A0-4680-A076-7893DE176D6B}.Release|x86.ActiveCfg = Release|Any CPU
+ {7B8601F8-8D1F-4B9C-8C20-772B673A2FA6}.CodeCov|Any CPU.ActiveCfg = Release|Any CPU
+ {7B8601F8-8D1F-4B9C-8C20-772B673A2FA6}.CodeCov|Any CPU.Build.0 = Release|Any CPU
+ {7B8601F8-8D1F-4B9C-8C20-772B673A2FA6}.CodeCov|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {7B8601F8-8D1F-4B9C-8C20-772B673A2FA6}.CodeCov|Mixed Platforms.Build.0 = Release|Any CPU
+ {7B8601F8-8D1F-4B9C-8C20-772B673A2FA6}.CodeCov|x86.ActiveCfg = Release|Any CPU
+ {7B8601F8-8D1F-4B9C-8C20-772B673A2FA6}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU
+ {7B8601F8-8D1F-4B9C-8C20-772B673A2FA6}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU
+ {7B8601F8-8D1F-4B9C-8C20-772B673A2FA6}.CodeCoverage|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {7B8601F8-8D1F-4B9C-8C20-772B673A2FA6}.CodeCoverage|Mixed Platforms.Build.0 = Release|Any CPU
+ {7B8601F8-8D1F-4B9C-8C20-772B673A2FA6}.CodeCoverage|x86.ActiveCfg = Release|Any CPU
+ {7B8601F8-8D1F-4B9C-8C20-772B673A2FA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7B8601F8-8D1F-4B9C-8C20-772B673A2FA6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7B8601F8-8D1F-4B9C-8C20-772B673A2FA6}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {7B8601F8-8D1F-4B9C-8C20-772B673A2FA6}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {7B8601F8-8D1F-4B9C-8C20-772B673A2FA6}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {7B8601F8-8D1F-4B9C-8C20-772B673A2FA6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7B8601F8-8D1F-4B9C-8C20-772B673A2FA6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7B8601F8-8D1F-4B9C-8C20-772B673A2FA6}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {7B8601F8-8D1F-4B9C-8C20-772B673A2FA6}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {7B8601F8-8D1F-4B9C-8C20-772B673A2FA6}.Release|x86.ActiveCfg = Release|Any CPU
+ {4CBFC7D3-1600-4CE5-BC6B-AC7BC2D6F853}.CodeCov|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {4CBFC7D3-1600-4CE5-BC6B-AC7BC2D6F853}.CodeCov|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {4CBFC7D3-1600-4CE5-BC6B-AC7BC2D6F853}.CodeCov|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {4CBFC7D3-1600-4CE5-BC6B-AC7BC2D6F853}.CodeCov|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {4CBFC7D3-1600-4CE5-BC6B-AC7BC2D6F853}.CodeCov|x86.ActiveCfg = CodeCoverage|Any CPU
+ {4CBFC7D3-1600-4CE5-BC6B-AC7BC2D6F853}.CodeCoverage|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {4CBFC7D3-1600-4CE5-BC6B-AC7BC2D6F853}.CodeCoverage|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {4CBFC7D3-1600-4CE5-BC6B-AC7BC2D6F853}.CodeCoverage|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {4CBFC7D3-1600-4CE5-BC6B-AC7BC2D6F853}.CodeCoverage|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {4CBFC7D3-1600-4CE5-BC6B-AC7BC2D6F853}.CodeCoverage|x86.ActiveCfg = CodeCoverage|Any CPU
+ {4CBFC7D3-1600-4CE5-BC6B-AC7BC2D6F853}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4CBFC7D3-1600-4CE5-BC6B-AC7BC2D6F853}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4CBFC7D3-1600-4CE5-BC6B-AC7BC2D6F853}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {4CBFC7D3-1600-4CE5-BC6B-AC7BC2D6F853}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {4CBFC7D3-1600-4CE5-BC6B-AC7BC2D6F853}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {4CBFC7D3-1600-4CE5-BC6B-AC7BC2D6F853}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4CBFC7D3-1600-4CE5-BC6B-AC7BC2D6F853}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4CBFC7D3-1600-4CE5-BC6B-AC7BC2D6F853}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {4CBFC7D3-1600-4CE5-BC6B-AC7BC2D6F853}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {4CBFC7D3-1600-4CE5-BC6B-AC7BC2D6F853}.Release|x86.ActiveCfg = Release|Any CPU
+ {694C6EDF-EA52-438F-B745-82B025ECC0E7}.CodeCov|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {694C6EDF-EA52-438F-B745-82B025ECC0E7}.CodeCov|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {694C6EDF-EA52-438F-B745-82B025ECC0E7}.CodeCov|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {694C6EDF-EA52-438F-B745-82B025ECC0E7}.CodeCov|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {694C6EDF-EA52-438F-B745-82B025ECC0E7}.CodeCov|x86.ActiveCfg = CodeCoverage|Any CPU
+ {694C6EDF-EA52-438F-B745-82B025ECC0E7}.CodeCoverage|Any CPU.ActiveCfg = CodeCoverage|Any CPU
+ {694C6EDF-EA52-438F-B745-82B025ECC0E7}.CodeCoverage|Any CPU.Build.0 = CodeCoverage|Any CPU
+ {694C6EDF-EA52-438F-B745-82B025ECC0E7}.CodeCoverage|Mixed Platforms.ActiveCfg = CodeCoverage|Any CPU
+ {694C6EDF-EA52-438F-B745-82B025ECC0E7}.CodeCoverage|Mixed Platforms.Build.0 = CodeCoverage|Any CPU
+ {694C6EDF-EA52-438F-B745-82B025ECC0E7}.CodeCoverage|x86.ActiveCfg = CodeCoverage|Any CPU
+ {694C6EDF-EA52-438F-B745-82B025ECC0E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {694C6EDF-EA52-438F-B745-82B025ECC0E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {694C6EDF-EA52-438F-B745-82B025ECC0E7}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {694C6EDF-EA52-438F-B745-82B025ECC0E7}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {694C6EDF-EA52-438F-B745-82B025ECC0E7}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {694C6EDF-EA52-438F-B745-82B025ECC0E7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {694C6EDF-EA52-438F-B745-82B025ECC0E7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {694C6EDF-EA52-438F-B745-82B025ECC0E7}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {694C6EDF-EA52-438F-B745-82B025ECC0E7}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {694C6EDF-EA52-438F-B745-82B025ECC0E7}.Release|x86.ActiveCfg = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {8F18041B-9410-4C36-A9C5-067813DF5F31} = {A9836F9E-6DB3-4D9F-ADCA-CF42D8C8BA93}
+ {22BABB60-8F02-4027-AFFC-ACF069954536} = {A9836F9E-6DB3-4D9F-ADCA-CF42D8C8BA93}
+ {76EFA9C5-8D7E-4FDF-B710-E20F8B6B00D2} = {A9836F9E-6DB3-4D9F-ADCA-CF42D8C8BA93}
+ {9B7E3740-6161-4548-833C-4BBCA43B970E} = {A9836F9E-6DB3-4D9F-ADCA-CF42D8C8BA93}
+ {0939B11A-FE4E-4BA1-8AD6-D97741EE314F} = {A9836F9E-6DB3-4D9F-ADCA-CF42D8C8BA93}
+ {4D39BAAF-8A96-473E-AB79-C8A341885137} = {A9836F9E-6DB3-4D9F-ADCA-CF42D8C8BA93}
+ {55A15F40-1435-4248-A7F2-2A146BB83586} = {A9836F9E-6DB3-4D9F-ADCA-CF42D8C8BA93}
+ {0C7CE809-0F72-4C19-8C64-D6573E4D9521} = {A9836F9E-6DB3-4D9F-ADCA-CF42D8C8BA93}
+ {C23F02FC-4538-43F5-ABBA-38BA069AEA8F} = {A9836F9E-6DB3-4D9F-ADCA-CF42D8C8BA93}
+ {3D3FFD8A-624D-4E9B-954B-E1C105507975} = {A9836F9E-6DB3-4D9F-ADCA-CF42D8C8BA93}
+ {D3CF7430-6DA4-42B0-BD90-CA39D16687B2} = {A9836F9E-6DB3-4D9F-ADCA-CF42D8C8BA93}
+ {DDC1CE0C-486E-4E35-BB3B-EAB61F8F9440} = {A9836F9E-6DB3-4D9F-ADCA-CF42D8C8BA93}
+ {F0441BE9-BDC0-4629-BE5A-8765FFAA2481} = {A9836F9E-6DB3-4D9F-ADCA-CF42D8C8BA93}
+ {668E9021-CE84-49D9-98FB-DF125A9FCDB0} = {A9836F9E-6DB3-4D9F-ADCA-CF42D8C8BA93}
+ {03A5E5F2-2E23-48F2-ABCC-6C41BAC9AC02} = {A9836F9E-6DB3-4D9F-ADCA-CF42D8C8BA93}
+ {66492E69-CE4C-4FB1-9B1F-88DEE09D06F1} = {A9836F9E-6DB3-4D9F-ADCA-CF42D8C8BA93}
+ {A0187BC2-8325-4BB2-8697-7F955CF4173E} = {A9836F9E-6DB3-4D9F-ADCA-CF42D8C8BA93}
+ {ACE91549-D86E-4EB6-8C2A-5FF51386BB68} = {A9836F9E-6DB3-4D9F-ADCA-CF42D8C8BA93}
+ {653F3946-541C-42D3-BBC1-CE89B392BDA9} = {A9836F9E-6DB3-4D9F-ADCA-CF42D8C8BA93}
+ {B6895A1B-382F-4A69-99EC-E965E19B0AB3} = {A9836F9E-6DB3-4D9F-ADCA-CF42D8C8BA93}
+ {1ACEF677-B6A0-4680-A076-7893DE176D6B} = {A9836F9E-6DB3-4D9F-ADCA-CF42D8C8BA93}
+ {4CBFC7D3-1600-4CE5-BC6B-AC7BC2D6F853} = {A9836F9E-6DB3-4D9F-ADCA-CF42D8C8BA93}
+ {0BB62A1D-E6B5-49FA-9E3C-6AF679A66DFE} = {C40883CD-366D-4534-8B58-3EA0D13136DF}
+ {268DEE9D-F323-4A00-8ED8-3784388C3E3A} = {C40883CD-366D-4534-8B58-3EA0D13136DF}
+ {0F4870DB-A799-4DBA-99DF-0D74BB52FEC2} = {C40883CD-366D-4534-8B58-3EA0D13136DF}
+ {D3313BDF-8071-4AC8-9D98-ABF7F9E88A57} = {C40883CD-366D-4534-8B58-3EA0D13136DF}
+ {66A74F3C-A106-4C1E-BAA0-001908FEA2CA} = {C40883CD-366D-4534-8B58-3EA0D13136DF}
+ {E2D008A9-4D1D-4F6B-8325-4ED717D6EA0A} = {C40883CD-366D-4534-8B58-3EA0D13136DF}
+ {CD48EB41-92A5-4628-A0F7-6A43DF58827E} = {C40883CD-366D-4534-8B58-3EA0D13136DF}
+ {2C653A66-8159-4A41-954F-A67915DFDA87} = {C40883CD-366D-4534-8B58-3EA0D13136DF}
+ {21C729D6-ECF8-47EF-A236-7C6A4272EAF0} = {C40883CD-366D-4534-8B58-3EA0D13136DF}
+ {8AC2A2E4-2F11-4D40-A887-62E2583A65E6} = {C40883CD-366D-4534-8B58-3EA0D13136DF}
+ {6C28DA70-60F1-4442-967F-591BF3962EC5} = {C40883CD-366D-4534-8B58-3EA0D13136DF}
+ {7F2C796F-43B2-4F8F-ABFF-A154EC8AAFA1} = {C40883CD-366D-4534-8B58-3EA0D13136DF}
+ {FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0} = {C40883CD-366D-4534-8B58-3EA0D13136DF}
+ {EB09CD33-992B-4A31-AB95-8673BA90F1CD} = {C40883CD-366D-4534-8B58-3EA0D13136DF}
+ {7AF77741-9158-4D5F-8782-8F21FADF025F} = {C40883CD-366D-4534-8B58-3EA0D13136DF}
+ {A7B1264E-BCE5-42A8-8B5E-001A5360B128} = {C40883CD-366D-4534-8B58-3EA0D13136DF}
+ {6C18CC83-1E4C-42D2-B93E-55D6C363850C} = {C40883CD-366D-4534-8B58-3EA0D13136DF}
+ {7FB5C0C0-5223-4C79-A8DA-D2A0F264A478} = {C40883CD-366D-4534-8B58-3EA0D13136DF}
+ {81876811-6C36-492A-9609-F0E85990FBC9} = {C40883CD-366D-4534-8B58-3EA0D13136DF}
+ {3267DFC6-B34D-4011-BC0F-D3B56AF6F608} = {C40883CD-366D-4534-8B58-3EA0D13136DF}
+ {EA62944F-BD25-4730-9405-9BE8FF5BEACD} = {C40883CD-366D-4534-8B58-3EA0D13136DF}
+ {7B8601F8-8D1F-4B9C-8C20-772B673A2FA6} = {C40883CD-366D-4534-8B58-3EA0D13136DF}
+ {694C6EDF-EA52-438F-B745-82B025ECC0E7} = {C40883CD-366D-4534-8B58-3EA0D13136DF}
+ EndGlobalSection
+EndGlobal
diff --git a/Runtime.xunit b/Runtime.xunit
new file mode 100644
index 00000000..1236b2fc
--- /dev/null
+++ b/Runtime.xunit
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<xunit>
+ <assemblies>
+ <assembly filename="bin\Debug\Test\Microsoft.Web.Helpers.Test.dll" shadow-copy="true" />
+ <assembly filename="bin\Debug\Test\System.Web.Http.Test.dll" shadow-copy="true" />
+ <assembly filename="bin\Debug\Test\Microsoft.Web.Mvc.Test.dll" shadow-copy="true" />
+ <assembly filename="bin\Debug\Test\System.Web.Helpers.Test.dll" shadow-copy="true" />
+ <assembly filename="bin\Debug\Test\System.Web.Mvc.Test.dll" shadow-copy="true" />
+ <assembly filename="bin\Debug\Test\System.Web.Razor.Test.dll" shadow-copy="true" />
+ <assembly filename="bin\Debug\Test\System.Web.WebPages.Administration.Test.dll" shadow-copy="true" />
+ <assembly filename="bin\Debug\Test\System.Web.WebPages.Deployment.Test.dll" shadow-copy="true" />
+ <assembly filename="bin\Debug\Test\System.Web.WebPages.Razor.Test.dll" shadow-copy="true" />
+ <assembly filename="bin\Debug\Test\System.Web.WebPages.Test.dll" shadow-copy="true" />
+ <assembly filename="bin\Debug\Test\WebMatrix.Data.Test.dll" shadow-copy="true" />
+ <assembly filename="bin\Debug\Test\WebMatrix.WebData.Test.dll" shadow-copy="true" />
+ <assembly filename="bin\Debug\System.Web.Http.Test.Sample.dll" shadow-copy="true" />
+ </assemblies>
+</xunit> \ No newline at end of file
diff --git a/Settings.StyleCop b/Settings.StyleCop
new file mode 100644
index 00000000..a1b0dae0
--- /dev/null
+++ b/Settings.StyleCop
@@ -0,0 +1,109 @@
+<StyleCopSettings Version="4.3">
+
+<!--
+ This file was cloned directly from ndp\cdf\src
+ to apply ADP conventions to all product source.
+-->
+ <GlobalSettings>
+ <StringProperty Name="MergeSettingsFiles">NoMerge</StringProperty>
+ </GlobalSettings>
+ <Analyzers>
+
+ <Analyzer AnalyzerId="Microsoft.StyleCop.CSharp.NamingRules">
+ <AnalyzerSettings>
+ <CollectionProperty Name="Hungarian">
+ <Value>as</Value>
+ <Value>db</Value>
+ <Value>dc</Value>
+ <Value>do</Value>
+ <Value>ef</Value>
+ <Value>id</Value>
+ <Value>if</Value>
+ <Value>in</Value>
+ <Value>is</Value>
+ <Value>my</Value>
+ <Value>no</Value>
+ <Value>on</Value>
+ <Value>sl</Value>
+ <Value>to</Value>
+ <Value>ui</Value>
+ <Value>vs</Value>
+ </CollectionProperty>
+ </AnalyzerSettings>
+ </Analyzer>
+
+ <Analyzer AnalyzerId="Microsoft.StyleCop.CSharp.DocumentationRules">
+ <AnalyzerSettings>
+ <BooleanProperty Name="IgnorePrivates">True</BooleanProperty>
+ <BooleanProperty Name="IgnoreInternals">True</BooleanProperty>
+ <StringProperty Name="Copyright">Copyright (c) Microsoft Corporation. All rights reserved.</StringProperty>
+ </AnalyzerSettings>
+
+ <Rules>
+ <Rule Name="FileMustHaveHeader">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+
+ <!-- Per ADP guidelines, the file header does not need to contain the name of the file. -->
+ <Rule Name="FileHeaderMustContainFileName">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="FileHeaderFileNameDocumentationMustMatchFileName">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+
+ <!-- Per ADP guidelines, the file header does not need to contain a Company attribute. -->
+ <Rule Name="FileHeaderMustHaveValidCompanyText">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+
+ <!-- Per ADP guidelines, constructor summary documentation does not have to match a specific format, since they are not directly consumed for external documentation. -->
+ <Rule Name="ConstructorSummaryDocumentationMustBeginWithStandardText">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+
+ <!-- Per ADP guidelines, destructor summary documentation does not have to match a specific format, since they are not directly consumed for external documentation. -->
+ <Rule Name="DestructorSummaryDocumentationMustBeginWithStandardText">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+
+ <!-- Per ADP guidelines, documentation headers can contain blank lines, since they are not directly consumed for external documentation. -->
+ <Rule Name="DocumentationHeadersMustNotContainBlankLines">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ </Rules>
+ </Analyzer>
+
+ <Analyzer AnalyzerId="Microsoft.StyleCop.CSharp.ReadabilityRules">
+ <Rules>
+ <!-- Per ADP guidelines, the use of regions is not allowed (copied from CSD guidelines doc) -->
+ <Rule Name="DoNotUseRegions">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">True</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <!-- Per ADP guidelines, method parameter are allowed to span across multiple lines (rather than having to be assigned to a temporary variable). -->
+ <Rule Name="ParameterMustNotSpanMultipleLines">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ </Rules>
+ </Analyzer>
+
+ </Analyzers>
+</StyleCopSettings> \ No newline at end of file
diff --git a/build.cmd b/build.cmd
new file mode 100644
index 00000000..0dde9583
--- /dev/null
+++ b/build.cmd
@@ -0,0 +1,30 @@
+@echo off
+pushd %~dp0
+
+if exist bin goto build
+mkdir bin
+
+:Build
+if "%1" == "" goto BuildDefaults
+
+%SystemRoot%\Microsoft.NET\Framework\v4.0.30319\msbuild Runtime.msbuild /m /t:%* /v:M /fl /flp:LogFile=bin\msbuild.log;Verbosity=Normal
+if errorlevel 1 goto BuildFail
+goto BuildSuccess
+
+:BuildDefaults
+%SystemRoot%\Microsoft.NET\Framework\v4.0.30319\msbuild Runtime.msbuild /m /v:M /fl /flp:LogFile=bin\msbuild.log;Verbosity=Normal
+if errorlevel 1 goto BuildFail
+goto BuildSuccess
+
+:BuildFail
+echo.
+echo *** BUILD FAILED ***
+goto End
+
+:BuildSuccess
+echo.
+echo **** BUILD SUCCESSFUL ***
+goto end
+
+:End
+popd
diff --git a/packages/repositories.config b/packages/repositories.config
new file mode 100644
index 00000000..dd8861b0
--- /dev/null
+++ b/packages/repositories.config
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<repositories>
+ <repository path="..\src\Microsoft.Web.Http.Data.EntityFramework\packages.config" />
+ <repository path="..\src\Microsoft.Web.Http.Data.Helpers\packages.config" />
+ <repository path="..\src\Microsoft.Web.Http.Data\packages.config" />
+ <repository path="..\src\Microsoft.Web.WebPages.OAuth\packages.config" />
+ <repository path="..\src\System.Net.Http.Formatting.OData\packages.config" />
+ <repository path="..\src\System.Net.Http.Formatting\packages.config" />
+ <repository path="..\src\System.Web.Http.Common\packages.config" />
+ <repository path="..\src\System.Web.Http.SelfHost\packages.config" />
+ <repository path="..\src\System.Web.Http.WebHost\packages.config" />
+ <repository path="..\src\System.Web.Http\packages.config" />
+ <repository path="..\src\System.Web.Mvc\packages.config" />
+ <repository path="..\src\System.Web.WebPages.Administration\packages.config" />
+ <repository path="..\src\System.Web.WebPages.Deployment\packages.config" />
+ <repository path="..\src\System.Web.WebPages\packages.config" />
+ <repository path="..\test\Microsoft.TestCommon\packages.config" />
+ <repository path="..\test\Microsoft.Web.Helpers.Test\packages.config" />
+ <repository path="..\test\Microsoft.Web.Http.Data.Test\packages.config" />
+ <repository path="..\test\Microsoft.Web.Mvc.Test\packages.config" />
+ <repository path="..\test\Microsoft.Web.WebPages.OAuth.Test\packages.config" />
+ <repository path="..\test\System.Json.Test.Integration\packages.config" />
+ <repository path="..\test\System.Json.Test.Unit\packages.config" />
+ <repository path="..\test\System.Net.Http.Formatting.OData.Test.Integration\packages.config" />
+ <repository path="..\test\System.Net.Http.Formatting.OData.Test.Unit\packages.config" />
+ <repository path="..\test\System.Net.Http.Formatting.Test.Integration\packages.config" />
+ <repository path="..\test\System.Net.Http.Formatting.Test.Unit\packages.config" />
+ <repository path="..\test\System.Web.Helpers.Test\packages.config" />
+ <repository path="..\test\System.Web.Http.Common.Test\packages.config" />
+ <repository path="..\test\System.Web.Http.Integration.Test\packages.config" />
+ <repository path="..\test\System.Web.Http.Test\packages.config" />
+ <repository path="..\test\System.Web.Http.WebHost.Test\packages.config" />
+ <repository path="..\test\System.Web.Mvc.Test\packages.config" />
+ <repository path="..\test\System.Web.Razor.Test\packages.config" />
+ <repository path="..\test\System.Web.WebPages.Administration.Test\packages.config" />
+ <repository path="..\test\System.Web.WebPages.Deployment.Test\packages.config" />
+ <repository path="..\test\System.Web.WebPages.Razor.Test\packages.config" />
+ <repository path="..\test\System.Web.WebPages.Test\packages.config" />
+ <repository path="..\test\WebMatrix.Data.Test\packages.config" />
+ <repository path="..\test\WebMatrix.WebData.Test\packages.config" />
+</repositories> \ No newline at end of file
diff --git a/src/AptcaCommonAssemblyInfo.cs b/src/AptcaCommonAssemblyInfo.cs
new file mode 100644
index 00000000..caa4bc0a
--- /dev/null
+++ b/src/AptcaCommonAssemblyInfo.cs
@@ -0,0 +1,11 @@
+using System.Security;
+
+//// REVIEW: RonCain -- This version is used by the WebStackRuntime assemblies that use types
+//// from System.ComponentModelDataAnnotations which is not [SecurityTransparent]
+//// in .Net 4.0. Attempting to make the WebStackRuntime assemblies be
+//// [SecurityTransparent] results in security exceptions on any type reference
+//// to DataAnnotations.
+//// Search for [SecuritySafeCritical] in WebStackRuntime
+//// assemblies to find the places that rely on this use of [Aptca]
+
+[assembly: AllowPartiallyTrustedCallers]
diff --git a/src/CodeAnalysisDictionary.xml b/src/CodeAnalysisDictionary.xml
new file mode 100644
index 00000000..6c50da70
--- /dev/null
+++ b/src/CodeAnalysisDictionary.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<Dictionary>
+ <Words>
+ <Recognized>
+ <Word>Multi</Word>
+ <Word>Bitly</Word>
+ <Word>Digg</Word>
+ <Word>Facebook</Word>
+ <Word>Reddit</Word>
+ <Word>Captcha</Word>
+ <Word>Facebook</Word>
+ <Word>Gravatar</Word>
+ <Word>JSON</Word>
+ <Word>Lookahead</Word>
+ <Word>MVC</Word>
+ <Word>Param</Word>
+ <Word>Params</Word>
+ <Word>Pluralizer</Word>
+ <Word>Pragma</Word>
+ <Word>Pragmas</Word>
+ <Word>Templating</Word>
+ <Word>Unvalidated</Word>
+ <Word>Validator</Word>
+ <Word>Validators</Word>
+ <Word>Validatable</Word>
+ <Word>WebPage</Word>
+ <Word>cshtml</Word>
+ <Word>vbhtml</Word>
+ <Word>asax</Word>
+ <Word>Eval</Word>
+ <Word>Src</Word>
+ <Word>Charset</Word>
+ <Word>Coords</Word>
+ <Word>Rel</Word>
+ <Word>Dto</Word>
+ <Word>Tokenizer</Word>
+ <Word>ReDim</Word>
+ <Word>OAuth</Word>
+ <Word>OpenID</Word>
+ <Word>Yadis</Word>
+ </Recognized>
+ <Compound>
+ <Term CompoundAlternate="WebPage">WebPage</Term>
+ <Term CompoundAlternate="TimeLine">TimeLine</Term>
+ <Term CompoundAlternate="OAuth">oAuth</Term>
+ <Term CompoundAlternate="UserName">userName</Term>
+ </Compound>
+ </Words>
+ <Acronyms>
+ <CasingExceptions>
+ <Acronym>ID</Acronym>
+ <Acronym>Db</Acronym>
+ <Acronym>Dto</Acronym>
+ </CasingExceptions>
+ </Acronyms>
+</Dictionary> \ No newline at end of file
diff --git a/src/CommonAssemblyInfo.cs b/src/CommonAssemblyInfo.cs
new file mode 100644
index 00000000..81c42c5f
--- /dev/null
+++ b/src/CommonAssemblyInfo.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Reflection;
+using System.Resources;
+using System.Runtime.InteropServices;
+
+[assembly: AssemblyCompany("Microsoft Corporation")]
+[assembly: AssemblyCopyright("© Microsoft Corporation. All rights reserved.")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyTrademark("")]
+[assembly: ComVisible(false)]
+[assembly: CLSCompliant(true)]
+
+#if ASPNETMVC && ASPNETWEBPAGES
+#error Runtime projects cannot define both ASPNETMVC and ASPNETWEBPAGES
+#endif
+
+#if ASPNETMVC
+[assembly: AssemblyVersion("4.0.0.0")]
+[assembly: AssemblyFileVersion("4.0.0.0")]
+[assembly: AssemblyProduct("Microsoft ASP.NET MVC")]
+#elif ASPNETWEBPAGES
+[assembly: AssemblyVersion("2.0.0.0")]
+[assembly: AssemblyFileVersion("2.0.0.0")]
+[assembly: AssemblyProduct("Microsoft ASP.NET Web Pages")]
+#else
+#error Runtime projects must define either ASPNETMVC or ASPNETWEBPAGES
+#endif
+
+[assembly: NeutralResourcesLanguage("en-US")]
diff --git a/src/CommonResources.Designer.cs b/src/CommonResources.Designer.cs
new file mode 100644
index 00000000..439cb0dc
--- /dev/null
+++ b/src/CommonResources.Designer.cs
@@ -0,0 +1,143 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.239
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Microsoft.Internal.Web.Utils {
+ using System;
+ using System.Linq;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class CommonResources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal CommonResources() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ // Find the CommonResources.resources file's full resource name in this assembly
+ string commonResourcesName = global::System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceNames().Where(s => s.EndsWith("CommonResources.resources", StringComparison.OrdinalIgnoreCase)).Single();
+
+ // Trim off the ".resources"
+ commonResourcesName = commonResourcesName.Substring(0, commonResourcesName.Length - 10);
+
+ // Load the resource manager
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager(commonResourcesName, typeof(CommonResources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Value cannot be null or an empty string..
+ /// </summary>
+ internal static string Argument_Cannot_Be_Null_Or_Empty {
+ get {
+ return ResourceManager.GetString("Argument_Cannot_Be_Null_Or_Empty", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Value must be between {0} and {1}..
+ /// </summary>
+ internal static string Argument_Must_Be_Between {
+ get {
+ return ResourceManager.GetString("Argument_Must_Be_Between", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Value must be a value from the &quot;{0}&quot; enumeration..
+ /// </summary>
+ internal static string Argument_Must_Be_Enum_Member {
+ get {
+ return ResourceManager.GetString("Argument_Must_Be_Enum_Member", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Value must be greater than {0}..
+ /// </summary>
+ internal static string Argument_Must_Be_GreaterThan {
+ get {
+ return ResourceManager.GetString("Argument_Must_Be_GreaterThan", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Value must be greater than or equal to {0}..
+ /// </summary>
+ internal static string Argument_Must_Be_GreaterThanOrEqualTo {
+ get {
+ return ResourceManager.GetString("Argument_Must_Be_GreaterThanOrEqualTo", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Value must be less than {0}..
+ /// </summary>
+ internal static string Argument_Must_Be_LessThan {
+ get {
+ return ResourceManager.GetString("Argument_Must_Be_LessThan", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Value must be less than or equal to {0}..
+ /// </summary>
+ internal static string Argument_Must_Be_LessThanOrEqualTo {
+ get {
+ return ResourceManager.GetString("Argument_Must_Be_LessThanOrEqualTo", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Value cannot be an empty string. It must either be null or a non-empty string..
+ /// </summary>
+ internal static string Argument_Must_Be_Null_Or_Non_Empty {
+ get {
+ return ResourceManager.GetString("Argument_Must_Be_Null_Or_Non_Empty", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/CommonResources.resx b/src/CommonResources.resx
new file mode 100644
index 00000000..24904193
--- /dev/null
+++ b/src/CommonResources.resx
@@ -0,0 +1,144 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="Argument_Cannot_Be_Null_Or_Empty" xml:space="preserve">
+ <value>Value cannot be null or an empty string.</value>
+ </data>
+ <data name="Argument_Must_Be_Between" xml:space="preserve">
+ <value>Value must be between {0} and {1}.</value>
+ </data>
+ <data name="Argument_Must_Be_Enum_Member" xml:space="preserve">
+ <value>Value must be a value from the "{0}" enumeration.</value>
+ </data>
+ <data name="Argument_Must_Be_GreaterThan" xml:space="preserve">
+ <value>Value must be greater than {0}.</value>
+ </data>
+ <data name="Argument_Must_Be_GreaterThanOrEqualTo" xml:space="preserve">
+ <value>Value must be greater than or equal to {0}.</value>
+ </data>
+ <data name="Argument_Must_Be_LessThan" xml:space="preserve">
+ <value>Value must be less than {0}.</value>
+ </data>
+ <data name="Argument_Must_Be_LessThanOrEqualTo" xml:space="preserve">
+ <value>Value must be less than or equal to {0}.</value>
+ </data>
+ <data name="Argument_Must_Be_Null_Or_Non_Empty" xml:space="preserve">
+ <value>Value cannot be an empty string. It must either be null or a non-empty string.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/DynamicHelper.cs b/src/DynamicHelper.cs
new file mode 100644
index 00000000..141ab7a5
--- /dev/null
+++ b/src/DynamicHelper.cs
@@ -0,0 +1,110 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Dynamic;
+using System.Linq.Expressions;
+using System.Runtime.CompilerServices;
+using Microsoft.CSharp.RuntimeBinder;
+
+namespace Microsoft.Internal.Web.Utils
+{
+ /// <summary>
+ /// Helper to evaluate different method on dynamic objects
+ /// </summary>
+ internal static class DynamicHelper
+ {
+ // We must pass in "object" instead of "dynamic" for the target dynamic object because if we use dynamic, the compiler will
+ // convert the call to this helper into a dynamic expression, even though we don't need it to be. Since this class is internal,
+ // it cannot be accessed from a dynamic expression and thus we get errors.
+
+ // Dev10 Bug 914027 - Changed the first parameter from dynamic to object, see comment at top for details
+ public static bool TryGetMemberValue(object obj, string memberName, out object result)
+ {
+ try
+ {
+ result = GetMemberValue(obj, memberName);
+ return true;
+ }
+ catch (RuntimeBinderException)
+ {
+ }
+ catch (RuntimeBinderInternalCompilerException)
+ {
+ }
+
+ // We catch the C# specific runtime binder exceptions since we're using the C# binder in this case
+ result = null;
+ return false;
+ }
+
+ // Dev10 Bug 914027 - Changed the first parameter from dynamic to object, see comment at top for details
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We want to swallow exceptions that happen during runtime binding")]
+ public static bool TryGetMemberValue(object obj, GetMemberBinder binder, out object result)
+ {
+ try
+ {
+ // VB us an instance of GetBinderAdapter that does not implement FallbackGetMemeber. This causes lookup of property expressions on dynamic objects to fail.
+ // Since all types are private to the assembly, we assume that as long as they belong to CSharp runtime, it is the right one.
+ if (typeof(Binder).Assembly.Equals(binder.GetType().Assembly))
+ {
+ // Only use the binder if its a C# binder.
+ result = GetMemberValue(obj, binder);
+ }
+ else
+ {
+ result = GetMemberValue(obj, binder.Name);
+ }
+ return true;
+ }
+ catch
+ {
+ result = null;
+ return false;
+ }
+ }
+
+ // Dev10 Bug 914027 - Changed the first parameter from dynamic to object, see comment at top for details
+ public static object GetMemberValue(object obj, string memberName)
+ {
+ var callSite = GetMemberAccessCallSite(memberName);
+ return callSite.Target(callSite, obj);
+ }
+
+ // Dev10 Bug 914027 - Changed the first parameter from dynamic to object, see comment at top for details
+ public static object GetMemberValue(object obj, GetMemberBinder binder)
+ {
+ var callSite = GetMemberAccessCallSite(binder);
+ return callSite.Target(callSite, obj);
+ }
+
+ // dynamic d = new object();
+ // object s = d.Name;
+ // The following code gets generated for this expression:
+ // callSite = CallSite<Func<CallSite, object, object>>.Create(Binder.GetMember(CSharpBinderFlags.None, "Name", typeof(Program), new CSharpArgumentInfo[] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) }));
+ // callSite.Target(callSite, d);
+ // typeof(Program) is the containing type of the dynamic operation.
+ // Dev10 Bug 914027 - Changed the callsite's target parameter from dynamic to object, see comment at top for details
+ public static CallSite<Func<CallSite, object, object>> GetMemberAccessCallSite(string memberName)
+ {
+ var binder = Binder.GetMember(CSharpBinderFlags.None, memberName, typeof(DynamicHelper), new[] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) });
+ return GetMemberAccessCallSite(binder);
+ }
+
+ // Dev10 Bug 914027 - Changed the callsite's target parameter from dynamic to object, see comment at top for details
+ public static CallSite<Func<CallSite, object, object>> GetMemberAccessCallSite(CallSiteBinder binder)
+ {
+ return CallSite<Func<CallSite, object, object>>.Create(binder);
+ }
+
+ // Dev10 Bug 914027 - Changed the first parameter from dynamic to object, see comment at top for details
+ public static IEnumerable<string> GetMemberNames(object obj)
+ {
+ var provider = obj as IDynamicMetaObjectProvider;
+ Debug.Assert(provider != null, "obj doesn't implement IDynamicMetaObjectProvider");
+
+ Expression parameter = Expression.Parameter(typeof(object));
+ return provider.GetMetaObject(parameter).GetDynamicMemberNames();
+ }
+ }
+}
diff --git a/src/ExceptionHelper.cs b/src/ExceptionHelper.cs
new file mode 100644
index 00000000..8588aa13
--- /dev/null
+++ b/src/ExceptionHelper.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Microsoft.Internal.Web.Utils
+{
+ internal static class ExceptionHelper
+ {
+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Method may not be used in every assembly it is imported into")]
+ internal static ArgumentException CreateArgumentNullOrEmptyException(string paramName)
+ {
+ return new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, paramName);
+ }
+ }
+}
diff --git a/src/GlobalSuppressions.cs b/src/GlobalSuppressions.cs
new file mode 100644
index 00000000..95e634f2
--- /dev/null
+++ b/src/GlobalSuppressions.cs
@@ -0,0 +1,3 @@
+using System.Diagnostics.CodeAnalysis;
+
+[assembly: SuppressMessage("Microsoft.Design", "CA2210:AssembliesShouldHaveValidStrongNames", Justification = "Assembly is delay-signed")]
diff --git a/src/HashCodeCombiner.cs b/src/HashCodeCombiner.cs
new file mode 100644
index 00000000..187aa86e
--- /dev/null
+++ b/src/HashCodeCombiner.cs
@@ -0,0 +1,51 @@
+using System.Collections;
+
+namespace Microsoft.Internal.Web.Utils
+{
+ internal class HashCodeCombiner
+ {
+ private long _combinedHash64 = 0x1505L;
+
+ public int CombinedHash
+ {
+ get { return _combinedHash64.GetHashCode(); }
+ }
+
+ public HashCodeCombiner Add(IEnumerable e)
+ {
+ if (e == null)
+ {
+ Add(0);
+ }
+ else
+ {
+ int count = 0;
+ foreach (object o in e)
+ {
+ Add(o);
+ count++;
+ }
+ Add(count);
+ }
+ return this;
+ }
+
+ public HashCodeCombiner Add(int i)
+ {
+ _combinedHash64 = ((_combinedHash64 << 5) + _combinedHash64) ^ i;
+ return this;
+ }
+
+ public HashCodeCombiner Add(object o)
+ {
+ int hashCode = (o != null) ? o.GetHashCode() : 0;
+ Add(hashCode);
+ return this;
+ }
+
+ public static HashCodeCombiner Start()
+ {
+ return new HashCodeCombiner();
+ }
+ }
+}
diff --git a/src/IVirtualPathUtility.cs b/src/IVirtualPathUtility.cs
new file mode 100644
index 00000000..bc614492
--- /dev/null
+++ b/src/IVirtualPathUtility.cs
@@ -0,0 +1,9 @@
+namespace Microsoft.Internal.Web.Utils
+{
+ internal interface IVirtualPathUtility
+ {
+ string Combine(string basePath, string relativePath);
+
+ string ToAbsolute(string virtualPath);
+ }
+}
diff --git a/src/Microsoft.Web.Helpers/Analytics.cshtml b/src/Microsoft.Web.Helpers/Analytics.cshtml
new file mode 100644
index 00000000..ba77b7cd
--- /dev/null
+++ b/src/Microsoft.Web.Helpers/Analytics.cshtml
@@ -0,0 +1,61 @@
+@* Generator: WebPagesHelper *@
+
+@helper GetGoogleHtml(string webPropertyId) {
+ var webPropertyIdJson = new HtmlString(HttpUtility.JavaScriptStringEncode(webPropertyId, addDoubleQuotes: false));
+
+ <script type="text/javascript">
+ var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
+ document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));
+ </script>
+ <script type="text/javascript">
+ try{
+ var pageTracker = _gat._getTracker("@webPropertyIdJson");
+ pageTracker._trackPageview();
+ } catch(err) {}
+ </script>
+}
+
+@helper GetGoogleAsyncHtml(string webPropertyId) {
+ var webPropertyIdJson = new HtmlString(HttpUtility.JavaScriptStringEncode(webPropertyId, addDoubleQuotes: false));
+ <script type="text/javascript">
+ var _gaq = _gaq || [];
+ _gaq.push(['_setAccount', '@webPropertyIdJson']);
+ _gaq.push(['_trackPageview']);
+ (function() {
+ var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
+ ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
+ var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
+ })();
+ </script>
+}
+
+@helper GetYahooHtml(string account) {
+ var accountJson = new HtmlString(HttpUtility.JavaScriptStringEncode(account, addDoubleQuotes: false));
+ <script type="text/javascript">
+ window.ysm_customData = new Object();
+ window.ysm_customData.conversion = "transId=,currency=,amount=";
+ var ysm_accountid = "@accountJson";
+ document.write("<SCR" + "IPT language='JavaScript' type='text/javascript' "
+ + "SRC=//" + "srv3.wa.marketingsolutions.yahoo.com" + "/script/ScriptServlet" + "?aid=" + ysm_accountid
+ + "></SCR" + "IPT>");
+ </script>
+}
+
+@helper GetStatCounterHtml(int project, string security) {
+ var securityJson = new HtmlString(HttpUtility.JavaScriptStringEncode(security, addDoubleQuotes: false));
+
+ <script type="text/javascript">
+ var sc_project=@project;
+ var sc_invisible=1;
+ var sc_security="@securityJson";
+ var sc_text=2;
+ var sc_https=1;
+ var scJsHost = (("https:" == document.location.protocol) ? "https://secure." : "http://www.");
+ document.write("<sc" + "ript type='text/javascript' src='" + scJsHost + "statcounter.com/counter/counter_xhtml.js'></" + "script>");
+ </script>
+ <noscript>
+ <div class="statcounter">
+ <a title="tumblrstatistics" class="statcounter" href="http://www.statcounter.com/tumblr/"><img class="statcounter" src="https://c.statcounter.com/@project/0/@security/1/" alt="tumblr statistics"/></a></div>
+ </noscript>
+}
+ \ No newline at end of file
diff --git a/src/Microsoft.Web.Helpers/Analytics.generated.cs b/src/Microsoft.Web.Helpers/Analytics.generated.cs
new file mode 100644
index 00000000..fa390efb
--- /dev/null
+++ b/src/Microsoft.Web.Helpers/Analytics.generated.cs
@@ -0,0 +1,247 @@
+#pragma warning disable 1591
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.235
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Microsoft.Web.Helpers
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+ using System.Net;
+ using System.Text;
+ using System.Web;
+ using System.Web.Helpers;
+ using System.Web.Security;
+ using System.Web.UI;
+ using System.Web.WebPages;
+ using System.Web.WebPages.Html;
+
+ public class Analytics : System.Web.WebPages.HelperPage
+ {
+
+public static System.Web.WebPages.HelperResult GetGoogleHtml(string webPropertyId) {
+return new System.Web.WebPages.HelperResult(__razor_helper_writer => {
+
+
+
+#line 3 "..\..\Analytics.cshtml"
+
+ var webPropertyIdJson = new HtmlString(HttpUtility.JavaScriptStringEncode(webPropertyId, addDoubleQuotes: false));
+
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, @" <script type=""text/javascript"">
+ var gaJsHost = ((""https:"" == document.location.protocol) ? ""https://ssl."" : ""http://www."");
+ document.write(unescape(""%3Cscript src='"" + gaJsHost + ""google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E""));
+ </script>
+");
+
+
+
+WriteLiteralTo(@__razor_helper_writer, " <script type=\"text/javascript\">\r\n try{\r\n var pageTracker = " +
+"_gat._getTracker(\"");
+
+
+
+#line 12 "..\..\Analytics.cshtml"
+ WriteTo(@__razor_helper_writer, webPropertyIdJson);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\");\r\n pageTracker._trackPageview();\r\n } catch(err) {}\r\n </sc" +
+"ript>\r\n");
+
+
+
+#line 16 "..\..\Analytics.cshtml"
+
+#line default
+#line hidden
+
+});
+
+}
+
+
+public static System.Web.WebPages.HelperResult GetGoogleAsyncHtml(string webPropertyId) {
+return new System.Web.WebPages.HelperResult(__razor_helper_writer => {
+
+
+
+#line 18 "..\..\Analytics.cshtml"
+
+ var webPropertyIdJson = new HtmlString(HttpUtility.JavaScriptStringEncode(webPropertyId, addDoubleQuotes: false));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " <script type=\"text/javascript\">\r\n var _gaq = _gaq || [];\r\n _gaq" +
+".push([\'_setAccount\', \'");
+
+
+
+#line 22 "..\..\Analytics.cshtml"
+ WriteTo(@__razor_helper_writer, webPropertyIdJson);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, @"']);
+ _gaq.push(['_trackPageview']);
+ (function() {
+ var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
+ ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
+ var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
+ })();
+ </script>
+");
+
+
+
+#line 30 "..\..\Analytics.cshtml"
+
+#line default
+#line hidden
+
+});
+
+}
+
+
+public static System.Web.WebPages.HelperResult GetYahooHtml(string account) {
+return new System.Web.WebPages.HelperResult(__razor_helper_writer => {
+
+
+
+#line 32 "..\..\Analytics.cshtml"
+
+ var accountJson = new HtmlString(HttpUtility.JavaScriptStringEncode(account, addDoubleQuotes: false));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " <script type=\"text/javascript\">\r\n window.ysm_customData = new Object()" +
+";\r\n window.ysm_customData.conversion = \"transId=,currency=,amount=\";\r\n " +
+" var ysm_accountid = \"");
+
+
+
+#line 37 "..\..\Analytics.cshtml"
+WriteTo(@__razor_helper_writer, accountJson);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\";\r\n document.write(\"<SCR\" + \"IPT language=\'JavaScript\' type=\'text/javascr" +
+"ipt\' \"\r\n + \"SRC=//\" + \"srv3.wa.marketingsolutions.yahoo.com\" + \"/script/S" +
+"criptServlet\" + \"?aid=\" + ysm_accountid\r\n + \"></SCR\" + \"IPT>\");\r\n </sc" +
+"ript>\r\n");
+
+
+
+#line 42 "..\..\Analytics.cshtml"
+
+#line default
+#line hidden
+
+});
+
+}
+
+
+public static System.Web.WebPages.HelperResult GetStatCounterHtml(int project, string security) {
+return new System.Web.WebPages.HelperResult(__razor_helper_writer => {
+
+
+
+#line 44 "..\..\Analytics.cshtml"
+
+ var securityJson = new HtmlString(HttpUtility.JavaScriptStringEncode(security, addDoubleQuotes: false));
+
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " <script type=\"text/javascript\">\r\n var sc_project=");
+
+
+
+#line 48 "..\..\Analytics.cshtml"
+WriteTo(@__razor_helper_writer, project);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ";\r\n var sc_invisible=1;\r\n var sc_security=\"");
+
+
+
+#line 50 "..\..\Analytics.cshtml"
+WriteTo(@__razor_helper_writer, securityJson);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, @""";
+ var sc_text=2;
+ var sc_https=1;
+ var scJsHost = ((""https:"" == document.location.protocol) ? ""https://secure."" : ""http://www."");
+ document.write(""<sc"" + ""ript type='text/javascript' src='"" + scJsHost + ""statcounter.com/counter/counter_xhtml.js'></"" + ""script>"");
+ </script>
+");
+
+
+
+WriteLiteralTo(@__razor_helper_writer, " <noscript>\r\n <div class=\"statcounter\">\r\n <a title=\"tumblrstatistics\" cl" +
+"ass=\"statcounter\" href=\"http://www.statcounter.com/tumblr/\"><img class=\"statcoun" +
+"ter\" src=\"https://c.statcounter.com/");
+
+
+
+#line 58 "..\..\Analytics.cshtml"
+ WriteTo(@__razor_helper_writer, project);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "/0/");
+
+
+
+#line 58 "..\..\Analytics.cshtml"
+ WriteTo(@__razor_helper_writer, security);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "/1/\" alt=\"tumblr statistics\"/></a></div>\r\n </noscript>\r\n");
+
+
+
+#line 60 "..\..\Analytics.cshtml"
+
+#line default
+#line hidden
+
+});
+
+}
+
+
+ public Analytics()
+ {
+ }
+ }
+}
+#pragma warning restore 1591
diff --git a/src/Microsoft.Web.Helpers/Bing.cshtml b/src/Microsoft.Web.Helpers/Bing.cshtml
new file mode 100644
index 00000000..1cca20b7
--- /dev/null
+++ b/src/Microsoft.Web.Helpers/Bing.cshtml
@@ -0,0 +1,93 @@
+@* Generator: WebPagesHelper *@
+
+@using System.Globalization
+@using System.Web
+@using System.Web.WebPages.Scope
+@using Microsoft.Internal.Web.Utils
+@using Resources
+
+@functions {
+ private const string DefaultBoxWidth = "322px";
+ internal static readonly object _siteTitleKey = new object();
+ internal static readonly object _siteUrlKey = new object();
+
+
+ public static string SiteTitle {
+ get {
+ return ScopeStorage.CurrentScope[_siteTitleKey] as string;
+ }
+
+ set {
+ if (value == null) {
+ throw new ArgumentNullException("SiteTitle");
+ }
+ ScopeStorage.CurrentScope[_siteTitleKey] = value;
+ }
+ }
+
+ public static string SiteUrl {
+ get {
+ return ScopeStorage.CurrentScope[_siteUrlKey] as string;
+ }
+
+ set {
+ if (value == null) {
+ throw new ArgumentNullException("SiteUrl");
+ }
+ ScopeStorage.CurrentScope[_siteUrlKey] = value;
+ }
+ }
+
+ private static int GetCodePageFromRequest(HttpContextBase httpContext) {
+ return httpContext.Response.ContentEncoding.CodePage;
+ }
+
+ private static string GetSiteUrl(IDictionary<object, object> scopeStorage, string siteUrl) {
+ object result;
+ if (siteUrl.IsEmpty() && scopeStorage.TryGetValue(_siteUrlKey, out result)){
+ siteUrl = result as string;
+ }
+ return siteUrl;
+ }
+
+ private static string GetSiteTitle(IDictionary<object, object> scopeStorage, string siteTitle) {
+ object result;
+ if (siteTitle.IsEmpty() && scopeStorage.TryGetValue(_siteTitleKey, out result)) {
+ siteTitle = result as string;
+ }
+ return siteTitle;
+ }
+}
+
+@helper SearchBox(string boxWidth = DefaultBoxWidth, string siteUrl = null, string siteTitle = null) {
+ @_SearchBox(boxWidth, siteUrl, siteTitle, new HttpContextWrapper(HttpContext.Current), ScopeStorage.CurrentScope)
+ }
+
+@helper _SearchBox(string boxWidth, string siteUrl, string siteTitle, HttpContextBase context, IDictionary<object, object> scopeStorage) {
+ siteTitle = GetSiteTitle(scopeStorage, siteTitle);
+ siteUrl = GetSiteUrl(scopeStorage, siteUrl);
+ string searchSite = String.IsNullOrEmpty(siteTitle) ? HelpersToolkitResources.BingSearch_DefaultSiteSearchText : siteTitle;
+
+ <form action="http://www.bing.com/search" class="BingSearch" method="get" target="_blank">
+ <input name="FORM" type="hidden" value="FREESS" />
+ <input name="cp" type="hidden" value="@GetCodePageFromRequest(context)" />
+ <table cellpadding="0" cellspacing="0" style="width:@boxWidth;">
+ <tr style="height: 32px">
+ <td style="width: 100%; border:solid 1px #ccc; border-right-style:none; padding-left:10px; padding-right:10px; vertical-align:middle;">
+ <input name="q" style="background-image:url(http://www.bing.com/siteowner/s/siteowner/searchbox_background_k.png); background-position:right; background-repeat:no-repeat; font-family:Arial; font-size:14px; color:#000; width:100%; border:none 0 transparent;" title="Search Bing" type="text" />
+ </td>
+ <td style="border:solid 1px #ccc; border-left-style:none; padding-left:0px; padding-right:3px;">
+ <input alt="Search" src="http://www.bing.com/siteowner/s/siteowner/searchbutton_normal_k.gif" style="border:none 0 transparent; height:24px; width:24px; vertical-align:top;" type="image" />
+ </td>
+ </tr>
+ @if (!String.IsNullOrEmpty(siteUrl)) {
+ <tr>
+ <td colspan="2" style="font-size: small">
+ <label><input checked="checked" name="q1" type="radio" value="site:@siteUrl" />@searchSite</label>&nbsp;<label><input name="q1" type="radio" value="" />@HelpersToolkitResources.BingSearch_DefaultWebSearchText</label>
+ </td>
+ </tr>
+ }
+ </table>
+ </form>
+}
+
diff --git a/src/Microsoft.Web.Helpers/Bing.generated.cs b/src/Microsoft.Web.Helpers/Bing.generated.cs
new file mode 100644
index 00000000..14e7e4a4
--- /dev/null
+++ b/src/Microsoft.Web.Helpers/Bing.generated.cs
@@ -0,0 +1,257 @@
+using Resources;
+
+#pragma warning disable 1591
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.235
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Microsoft.Web.Helpers
+{
+ using System;
+ using System.Collections.Generic;
+
+ #line 3 "..\..\Bing.cshtml"
+ using System.Globalization;
+
+ #line default
+ #line hidden
+ using System.IO;
+ using System.Linq;
+ using System.Net;
+ using System.Text;
+
+ #line 4 "..\..\Bing.cshtml"
+ using System.Web;
+
+ #line default
+ #line hidden
+ using System.Web.Helpers;
+ using System.Web.Security;
+ using System.Web.UI;
+ using System.Web.WebPages;
+ using System.Web.WebPages.Html;
+
+ #line 5 "..\..\Bing.cshtml"
+ using System.Web.WebPages.Scope;
+
+ #line default
+ #line hidden
+
+ #line 6 "..\..\Bing.cshtml"
+ using Microsoft.Internal.Web.Utils;
+
+ #line default
+ #line hidden
+
+ public class Bing : System.Web.WebPages.HelperPage
+ {
+
+ #line 8 "..\..\Bing.cshtml"
+
+ private const string DefaultBoxWidth = "322px";
+ internal static readonly object _siteTitleKey = new object();
+ internal static readonly object _siteUrlKey = new object();
+
+
+ public static string SiteTitle {
+ get {
+ return ScopeStorage.CurrentScope[_siteTitleKey] as string;
+ }
+
+ set {
+ if (value == null) {
+ throw new ArgumentNullException("SiteTitle");
+ }
+ ScopeStorage.CurrentScope[_siteTitleKey] = value;
+ }
+ }
+
+ public static string SiteUrl {
+ get {
+ return ScopeStorage.CurrentScope[_siteUrlKey] as string;
+ }
+
+ set {
+ if (value == null) {
+ throw new ArgumentNullException("SiteUrl");
+ }
+ ScopeStorage.CurrentScope[_siteUrlKey] = value;
+ }
+ }
+
+ private static int GetCodePageFromRequest(HttpContextBase httpContext) {
+ return httpContext.Response.ContentEncoding.CodePage;
+ }
+
+ private static string GetSiteUrl(IDictionary<object, object> scopeStorage, string siteUrl) {
+ object result;
+ if (siteUrl.IsEmpty() && scopeStorage.TryGetValue(_siteUrlKey, out result)){
+ siteUrl = result as string;
+ }
+ return siteUrl;
+ }
+
+ private static string GetSiteTitle(IDictionary<object, object> scopeStorage, string siteTitle) {
+ object result;
+ if (siteTitle.IsEmpty() && scopeStorage.TryGetValue(_siteTitleKey, out result)) {
+ siteTitle = result as string;
+ }
+ return siteTitle;
+ }
+
+ #line default
+ #line hidden
+
+public static System.Web.WebPages.HelperResult SearchBox(string boxWidth = DefaultBoxWidth, string siteUrl = null, string siteTitle = null) {
+return new System.Web.WebPages.HelperResult(__razor_helper_writer => {
+
+
+
+#line 61 "..\..\Bing.cshtml"
+
+
+#line default
+#line hidden
+
+
+#line 62 "..\..\Bing.cshtml"
+WriteTo(@__razor_helper_writer, _SearchBox(boxWidth, siteUrl, siteTitle, new HttpContextWrapper(HttpContext.Current), ScopeStorage.CurrentScope));
+
+#line default
+#line hidden
+
+
+#line 62 "..\..\Bing.cshtml"
+
+
+#line default
+#line hidden
+
+});
+
+ }
+
+
+internal static System.Web.WebPages.HelperResult _SearchBox(string boxWidth, string siteUrl, string siteTitle, HttpContextBase context, IDictionary<object, object> scopeStorage) {
+return new System.Web.WebPages.HelperResult(__razor_helper_writer => {
+
+
+
+#line 65 "..\..\Bing.cshtml"
+
+ siteTitle = GetSiteTitle(scopeStorage, siteTitle);
+ siteUrl = GetSiteUrl(scopeStorage, siteUrl);
+ string searchSite = String.IsNullOrEmpty(siteTitle) ? HelpersToolkitResources.BingSearch_DefaultSiteSearchText : siteTitle;
+
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " <form action=\"http://www.bing.com/search\" class=\"BingSearch\" method=\"get\" tar" +
+"get=\"_blank\">\r\n <input name=\"FORM\" type=\"hidden\" value=\"FREESS\" />\r\n <inpu" +
+"t name=\"cp\" type=\"hidden\" value=\"");
+
+
+
+#line 72 "..\..\Bing.cshtml"
+ WriteTo(@__razor_helper_writer, GetCodePageFromRequest(context));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\" />\r\n <table cellpadding=\"0\" cellspacing=\"0\" style=\"width:");
+
+
+
+#line 73 "..\..\Bing.cshtml"
+ WriteTo(@__razor_helper_writer, boxWidth);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, @";"">
+ <tr style=""height: 32px"">
+ <td style=""width: 100%; border:solid 1px #ccc; border-right-style:none; padding-left:10px; padding-right:10px; vertical-align:middle;"">
+ <input name=""q"" style=""background-image:url(http://www.bing.com/siteowner/s/siteowner/searchbox_background_k.png); background-position:right; background-repeat:no-repeat; font-family:Arial; font-size:14px; color:#000; width:100%; border:none 0 transparent;"" title=""Search Bing"" type=""text"" />
+ </td>
+ <td style=""border:solid 1px #ccc; border-left-style:none; padding-left:0px; padding-right:3px;"">
+ <input alt=""Search"" src=""http://www.bing.com/siteowner/s/siteowner/searchbutton_normal_k.gif"" style=""border:none 0 transparent; height:24px; width:24px; vertical-align:top;"" type=""image"" />
+ </td>
+ </tr>
+");
+
+
+
+#line 82 "..\..\Bing.cshtml"
+ if (!String.IsNullOrEmpty(siteUrl)) {
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " <tr>\r\n <td colspan=\"2\" style=\"font-size: small\">\r\n <label><" +
+"input checked=\"checked\" name=\"q1\" type=\"radio\" value=\"site:");
+
+
+
+#line 85 "..\..\Bing.cshtml"
+ WriteTo(@__razor_helper_writer, siteUrl);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\" />");
+
+
+
+#line 85 "..\..\Bing.cshtml"
+ WriteTo(@__razor_helper_writer, searchSite);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "</label>&nbsp;<label><input name=\"q1\" type=\"radio\" value=\"\" />");
+
+
+
+#line 85 "..\..\Bing.cshtml"
+ WriteTo(@__razor_helper_writer, HelpersToolkitResources.BingSearch_DefaultWebSearchText);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "</label>\r\n </td>\r\n </tr>\r\n");
+
+
+
+#line 88 "..\..\Bing.cshtml"
+ }
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " </table>\r\n </form>\r\n");
+
+
+
+#line 91 "..\..\Bing.cshtml"
+
+#line default
+#line hidden
+
+});
+
+}
+
+
+ public Bing()
+ {
+ }
+ }
+}
+#pragma warning restore 1591
diff --git a/src/Microsoft.Web.Helpers/Facebook.cshtml b/src/Microsoft.Web.Helpers/Facebook.cshtml
new file mode 100644
index 00000000..0b9d5e83
--- /dev/null
+++ b/src/Microsoft.Web.Helpers/Facebook.cshtml
@@ -0,0 +1,819 @@
+@* Generator: WebPagesHelper *@
+
+@using System.Collections.Specialized
+@using System.Globalization
+@using System.Security
+@using System.Text
+@using System.Web.Helpers
+@using System.Web.WebPages.Scope
+@using WebMatrix.Data
+@using WebMatrix.WebData
+
+@functions {
+
+ private const string FacebookCredentialsTableName = "webpages_FacebookCredentials";
+ private const string FacebookCredentialsIdColumn = "FacebookId";
+ private const string FacebookCredentialsUserIdColumn = "UserId";
+
+ private const string DefaultUserIdColumn = "UserId";
+ private const string DefaultUserNameColumn = "email";
+ private const string DefaultUserTableName = "UserProfile";
+
+ private const string DefaultFacebookPerms = "email";
+ private const string DefaultCallbackUrl = "~/Facebook/Login";
+ private const string FacebookApiProfileUrl = "https://graph.facebook.com/me";
+ private const string FacebookCookieAccessToken = "access_token";
+
+ private static readonly object _isInitializedKey = new object();
+ private static readonly object _membershipDBNameKey = new object();
+ private static readonly object _appIdKey = new object();
+ private static readonly object _appSecretKey = new object();
+ private static readonly object _language = new object();
+
+ public static bool HasMembershipIntegration {
+ get {
+ return !MembershipDBName.IsEmpty();
+ }
+ }
+
+ public static bool IsFacebookUserAuthenticated {
+ get {
+ return !GetFacebookCookieInfo(HttpContext, "uid").IsEmpty();
+ }
+ }
+
+ public static bool IsFacebookUserAssociated {
+ get {
+ return !GetAssociatedMembershipUserName().IsEmpty();
+ }
+ }
+
+ public static bool IsInitialized {
+ get {
+ return (bool)(ScopeStorage.CurrentScope[_isInitializedKey] ?? false);
+ }
+
+ private set {
+ ScopeStorage.CurrentScope[_isInitializedKey] = value;
+ }
+ }
+
+ public static string MembershipDBName {
+ get {
+ return (string)(ScopeStorage.CurrentScope[_membershipDBNameKey] ?? "");
+ }
+
+ set {
+ ScopeStorage.CurrentScope[_membershipDBNameKey] = value;
+ }
+ }
+
+ public static string AppId {
+ get {
+ return (string)(ScopeStorage.CurrentScope[_appIdKey] ?? "");
+ }
+
+ set {
+ ScopeStorage.CurrentScope[_appIdKey] = value;
+ }
+ }
+
+ public static string AppSecret {
+ get {
+ return (string)(ScopeStorage.CurrentScope[_appSecretKey] ?? "");
+ }
+
+ set {
+ ScopeStorage.CurrentScope[_appSecretKey] = value;
+ }
+ }
+
+ public static string Language {
+ get {
+ return (string)(ScopeStorage.CurrentScope[_language] ?? "en_US");
+ }
+
+ set {
+ ScopeStorage.CurrentScope[_language] = value;
+ }
+ }
+
+ private static HttpContextBase HttpContext {
+ get {
+ return new HttpContextWrapper(System.Web.HttpContext.Current);
+ }
+ }
+
+ ///<summary>
+ /// Initialize the helper with your Facebook application settings.
+ ///
+ /// If the 'membershipDBName' parameter is specified, Facebook membership integration will be enabled,
+ /// allowing users to register and associate their Facebook user account (identified with the e-mail)
+ /// with your site membership and the WebSecurity helper.
+ /// In this case, the helper will initialize the WebSecurity WebMatrix helper automatically (if not done previously)
+ /// and the store the Facebook membership information in the 'membershipDbName' database.
+ ///</summary>
+ ///<param name="appId">Facebook application id.</param>
+ ///<param name="appSecret">Facebook application secret.</param>
+ ///<param name="membershipDbName">Name of the database used for storing the membership data.</param>
+ public static void Initialize(string appId, string appSecret, string membershipDbName = "") {
+ AppId = appId;
+ AppSecret = appSecret;
+ IsInitialized = true;
+
+ if (!membershipDbName.IsEmpty()) {
+ MembershipDBName = membershipDbName;
+
+ InitializeMembershipProviderIfNeeded();
+ InitializeFacebookTableIfNeeded();
+ }
+ }
+
+ ///<summary>
+ /// Retrieves the Facebook profile of current logged in user.
+ ///</summary>
+ public static UserProfile GetFacebookUserProfile() {
+ var accessToken = GetFacebookCookieInfo(HttpContext, FacebookCookieAccessToken);
+
+ if (accessToken.IsEmpty()) {
+ return null;
+ }
+
+ var userProfileUrl = new Uri(new UrlBuilder(FacebookApiProfileUrl).AddParam(FacebookCookieAccessToken, accessToken));
+
+ using (var client = new WebClient()) {
+ using (var receiveStream = client.OpenRead(userProfileUrl)) {
+ var result = new StreamReader(receiveStream).ReadToEnd();
+ var profile = Json.Decode<UserProfile>(result);
+
+ return profile;
+ }
+ }
+ }
+
+ ///<summary>
+ /// Associates the specified User Name (e.g. email, depending on your membership model) with the current Facebook User Id from the logged user.
+ ///</summary>
+ ///<param name="userName">The user name to associate the current logged-in facebook account to.</param>
+ public static void AssociateMembershipAccount(string userName) {
+ if (!IsFacebookUserAuthenticated) {
+ throw new InvalidOperationException("No Facebook user is authenticated.");
+ }
+
+ if (IsFacebookUserAssociated) {
+ throw new InvalidOperationException("The authenticated Facebook user is already associated to a membership account.");
+ }
+
+ using (var db = Database.Open(MembershipDBName)) {
+ var facebookUserId = GetFacebookCookieInfo(HttpContext, "uid").As<long>();
+
+ var userId = WebSecurity.GetUserId(userName);
+ db.Execute(String.Format(CultureInfo.InvariantCulture, "INSERT INTO {0} ({1}, {2}) VALUES (@0, @1)", FacebookCredentialsTableName, FacebookCredentialsUserIdColumn, FacebookCredentialsIdColumn),
+ userId, facebookUserId);
+
+ // User is registered in the application
+ FormsAuthentication.SetAuthCookie(userName, false);
+ }
+ }
+
+ public static bool MembershipLogin() {
+ var user = GetAssociatedMembershipUserName();
+
+ if (!user.IsEmpty()) {
+ // User is registered in the application
+ FormsAuthentication.SetAuthCookie(user, false);
+ return true;
+ }
+ else {
+ return false;
+ }
+ }
+
+ private static void InitializeMembershipProviderIfNeeded() {
+ var provider = GetMembershipProvider();
+
+ if (IsMembershipProviderInitialized(provider)) {
+ WebSecurity.InitializeDatabaseConnection(MembershipDBName, DefaultUserTableName, DefaultUserIdColumn, DefaultUserNameColumn, true);
+ }
+ }
+
+ private static void InitializeFacebookTableIfNeeded() {
+ using (var db = Database.Open(MembershipDBName)) {
+ var table = db.QuerySingle("SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = @0", FacebookCredentialsTableName);
+
+ if (table == null) {
+ db.Execute(String.Format(CultureInfo.InvariantCulture, "CREATE TABLE {0} ({1} INT NOT NULL, {2} BIGINT NOT NULL)", FacebookCredentialsTableName, FacebookCredentialsUserIdColumn, FacebookCredentialsIdColumn));
+ }
+ }
+ }
+
+ private static string GetAssociatedMembershipUserName() {
+ var userName = "";
+
+ if (IsFacebookUserAuthenticated) {
+ using (var db = Database.Open(MembershipDBName)) {
+ var userId = db.QueryValue(String.Format(CultureInfo.InvariantCulture, "SELECT {0} FROM {1} WHERE {2} = LOWER(@0)", FacebookCredentialsUserIdColumn, FacebookCredentialsTableName, FacebookCredentialsIdColumn),
+ GetFacebookCookieInfo(HttpContext, "uid"));
+ if (userId != null) {
+ userName = GetUserName(userId);
+ }
+ }
+ }
+
+ return userName;
+ }
+
+ private static string GetUserName(int userId) {
+ var userName = "";
+
+ using (var db = Database.Open(MembershipDBName)) {
+ var provider = GetMembershipProvider();
+ userName = db.QueryValue(String.Format(CultureInfo.InvariantCulture, "SELECT {0} FROM {1} WHERE {2} = @0", provider.UserNameColumn, provider.UserTableName, provider.UserIdColumn), userId);
+ }
+
+ return userName;
+ }
+
+ private static SimpleMembershipProvider GetMembershipProvider() {
+ var provider = Membership.Provider as SimpleMembershipProvider;
+
+ if (provider == null) {
+ throw new InvalidOperationException("Simple Membership Provider not found.");
+ }
+
+ return provider;
+ }
+
+ private static bool IsMembershipProviderInitialized(SimpleMembershipProvider provider) {
+ return provider.UserTableName.IsEmpty() || provider.UserIdColumn.IsEmpty() || provider.UserNameColumn.IsEmpty();
+ }
+
+ internal static string GetFacebookCookieInfo(HttpContextBase httpContext, string key) {
+ var request = httpContext.Request;
+ var name = "fbs_" + AppId;
+
+ if (request.Cookies[name] != null) {
+ var value = request.Cookies[name].Value;
+ var args = HttpUtility.ParseQueryString(value.Replace("\"", ""));
+
+ if (!IsFacebookCookieValid(args)) {
+ throw new InvalidOperationException("Invalid Facebook cookie.");
+ }
+
+ return args[key];
+ }
+ else {
+ return "";
+ }
+ }
+
+ private static bool IsFacebookCookieValid(NameValueCollection args) {
+ var payload = new StringBuilder();
+ var keys = args.AllKeys;
+ Array.Sort(keys);
+ foreach (var key in keys) {
+ if (!key.Equals("sig", StringComparison.OrdinalIgnoreCase)) {
+ payload.AppendFormat("{0}={1}", key, args[key]);
+ }
+ }
+
+ payload.Append(AppSecret);
+
+ // Review: The HMAC uses MD5 which is not cryptographically secure. We need to investigate other Facebook authentication methods.
+ var signature = new StringBuilder();
+ using (var md5 = System.Security.Cryptography.MD5CryptoServiceProvider.Create()) {
+ var hash = md5.ComputeHash(Encoding.ASCII.GetBytes(payload.ToString()));
+ for (int i = 0; i < hash.Length; i++) {
+ signature.Append(hash[i].ToString("X2", CultureInfo.InvariantCulture));
+ }
+ }
+ return String.Equals(args["sig"], signature.ToString(), StringComparison.OrdinalIgnoreCase);
+ }
+
+ public class UserProfile {
+ public string Id { get; set; }
+ public string Name { get; set; }
+ public string First_Name { get; set; }
+ public string Last_Name { get; set; }
+ public string Link { get; set; }
+ public string Bio { get; set; }
+ public string Gender { get; set; }
+ public string Email { get; set; }
+ public string Timezone { get; set; }
+ public string Locale { get; set; }
+ public string Updated_Time { get; set; }
+ }
+
+ private static IHtmlString RawJS(string text) {
+ return new HtmlString(HttpUtility.JavaScriptStringEncode(text));
+ }
+}
+
+@*
+Summary:
+ Initialize the Facebook JavaScript SDK to be able to support the XFBML tags of the social plugins.
+*@
+@helper GetInitializationScripts() {
+ <div id="fb-root"></div>
+ <script type="text/javascript">
+ window.fbAsyncInit = function () {
+ FB.init({ appId: '@RawJS(AppId)', status: true, cookie: true, xfbml: true });
+ };
+ (function () {
+ var e = document.createElement('script'); e.async = true;
+ e.src = document.location.protocol +
+ '//connect.facebook.net/@RawJS(Language)/all.js';
+ document.getElementById('fb-root').appendChild(e);
+ } ());
+
+ function loginRedirect(url) { window.location = url; }
+ </script>
+}
+
+@*
+Summary:
+ Shows a Facebook Login Button, with site membership integration, allowing users to login on your site with their Facebook account (e-mail).
+ To use this method, you need to provide the 'membershipDbName' in the helper's Initialize() method.
+
+Parameter: registerUrl
+ Specifies the URL to register the logged user, and associate it with a Membership account.
+Parameter: returnUrl
+ Specifies URL to redirect after the user has successfully logged in (i.e. the Facebook User has a Membership Account).
+Parameter: callbackUrl
+ Specifies the URL of the WebMatrix page that will handle the Membership login (default URL is ~/Facebook/Login.cshtml).
+Parameter: buttonText
+ Specifies the Login Button Text.
+Parameter: autoLogoutLink
+ When set to true, if the user is logged into Facebook the button will display a logout button instead.
+Parameter: size
+ Specifies the button size: "small", "medium", "large", "xlarge".
+Paramter: length
+ Specifies the text lenght: "long" --> 'Connect with Facebook" or "short" --> 'Connect'.
+Parameter: showFaces
+ Specifies whether to display profile photos below the button.
+Parameter: extendedPermissions
+ The extendedPermissions parameter can be used to request extended permissions from the user. For example, if you want to incorporate a user's photos into your application, set this value to “user_photos”.
+*@
+@helper LoginButton(
+ string registerUrl,
+ string returnUrl = "~/",
+ string callbackUrl = DefaultCallbackUrl,
+ string buttonText = "",
+ bool autoLogoutLink = false,
+ string size = "medium",
+ string length = "long",
+ bool showFaces = false,
+ string extendedPermissions = "") {
+
+ var redirectUrl = new UrlBuilder(callbackUrl)
+ .AddParam("registerUrl", new UrlBuilder(registerUrl))
+ .AddParam("returnUrl", new UrlBuilder(returnUrl));
+ var onLogin = String.Format(CultureInfo.InvariantCulture, "loginRedirect('{0}')", RawJS(redirectUrl));
+ extendedPermissions = extendedPermissions.IsEmpty() ? "email" : String.Concat("email,", extendedPermissions);
+
+ <fb:login-button autologoutlink="@autoLogoutLink" size="@size" length="@length" onlogin="@onLogin" show-faces="@showFaces" perms="@extendedPermissions">@buttonText</fb:login-button>
+}
+
+@*
+Summary:
+ Shows a Facebook Login Button, without integrating Facebook login with your site membership.
+
+Parameter: buttonText
+ Specifies the Login Button Text.
+Parameter: autoLogoutLink
+ When set to true, if the user is logged into Facebook the button will display a logout button instead.
+Parameter: size
+ The button size: "small", "medium", "large", "xlarge".
+Parameter: length
+ The text lenght: "long" --> 'Connect with Facebook" or "short" --> 'Connect'
+Parameter: onLogin
+ Specifies the JavaScript action to execute after the user login.
+Parameter: showFaces
+ Whether to display profile photos below the button.
+Parameter: extendedPermissions
+ The extendedPermissions parameter can be used to request extended permissions from the user. For example, if you want to incorporate a user's photos into your application, set this value to “user_photos”.
+*@
+@helper LoginButtonTagOnly(
+ string buttonText = "",
+ bool autoLogoutLink = false,
+ string size = "long",
+ string length = "short",
+ string onLogin = "",
+ bool showFaces = false,
+ string extendedPermissions = "") {
+ <fb:login-button autologoutlink="@autoLogoutLink" size="@size" length="@length" onlogin="@onLogin" show-faces="@showFaces" perms="@extendedPermissions">@buttonText</fb:login-button>
+}
+
+@*
+Summary:
+ Shows a Facebook Like Button. When the user clicks the Like button on your site, a story appears in the user's friends' News Feed with a link back to your website.
+
+Parameter: href
+ The URL to like.
+Parameter: buttonLayout
+ The button layout:
+ - standard: Displays social text to the right of the button and friends' profile photos below.
+ Minimum width: 225 pixels. Default width: 450 pixels. Height: 35 pixels (without photos) or 80 pixels (with photos).
+ - button_count: Displays the total number of likes to the right of the button.
+ Minimum width: 90 pixels. Default width: 90 pixels. Height: 20 pixels.
+Parameter: showFaces
+ Specifies whether to display profile photos below the button (standard layout only).
+Parameter: width
+ The width of the Like button.
+Parameter: height
+ The height of the plugin in pixels.
+Parameter: action
+ The verb to display on the button: 'like', 'recommend'.
+Parameter: font
+ The font to display in the button: 'arial', 'lucida grande', 'segoe ui', 'tahoma', 'trebuchet ms', 'verdana'.
+Parameter: colorScheme
+ The color scheme for the like button: 'light' or 'dark'.
+Parameter: refLabel
+ A label for tracking referrals; must be less than 50 characters and can contain alphanumeric characters and some punctuation (currently +/=-.:_).
+
+*@
+@helper LikeButton(
+ string href = "",
+ string buttonLayout = "standard",
+ bool showFaces = true,
+ int width = 450,
+ int height = 80,
+ string action = "like",
+ string font = "",
+ string colorScheme = "light",
+ string refLabel = ""
+ ) {
+
+ if (href.IsEmpty()) {
+ href = Request.Url.OriginalString;
+ }
+
+ var src = new UrlBuilder("http://www.facebook.com/plugins/like.php")
+ .AddParam("href", href)
+ .AddParam("layout", buttonLayout)
+ .AddParam("show_faces", showFaces)
+ .AddParam("width", width)
+ .AddParam("action", action)
+ .AddParam("colorscheme", colorScheme)
+ .AddParam("height", height)
+ .AddParam("font", font)
+ .AddParam("locale", Language)
+ .AddParam("ref", refLabel);
+
+ <iframe src="@src" scrolling="no" frameborder="0" style="border:none; overflow:hidden; width:@(width)px; height:@(height)px;" allowTransparency="true"></iframe>
+}
+
+@*
+Summary:
+ Shows a Facebook Comments plugin.
+ The Comments Box easily enables your users to comment on your site's content — whether it's for a web page, article, photo, or other piece of content.
+ Then the user can share the comment on Facebook on their Wall and in their friends' streams.
+ An 'Administer Comments' link will appear below the 'Post' button for developers of the application.
+
+Parameter: xid
+ An id associated with the comments object (defaults to URL-encoded page URL).
+Parameter: width
+ The width of the plugin, in pixels.
+Parameter: numPosts
+ The number of comments to display, or 0 to hide all comments.
+Parameter: reverse
+ Changes the order of comments and comment area to allow greater customization.
+Parameter: removeRoundedBox
+ Removes the rounded box around the text area where comments are written to allow greater customization.
+*@
+@helper Comments(
+ string xid = "",
+ int width = 550,
+ int numPosts = 10,
+ bool reverseOrder = false,
+ bool removeRoundedBox = false) {
+ <fb:comments @if (!xid.IsEmpty()) { <text>xid="@xid" </text> }numposts="@numPosts" width="@width" reverse="@reverseOrder" simple="@removeRoundedBox" ></fb:comments>
+}
+
+@*
+Summary:
+ Shows a Facebook Recommendations plugin. The Recommendations plugin shows personalized recommendations to your users.
+ Since the content is hosted by Facebook, the plugin can display personalized recommendations whether or not the user has logged into your site.
+ To generate the recommendations, the plugin considers all the social interactions with URLs from your site.
+ For a logged in Facebook user, the plugin will give preference to and highlight objects her friends have interacted with.
+
+ *NOTE*: This helper method requires that your site is published into a public address where others can use it. Check this tutorial on publishing with WebMatrix: http://www.asp.net/webmatrix/tutorials/publish-a-website
+
+Parameter: site
+ The address of your published site. For example "www.fourthcoffee.com"
+Parameter: width
+ The width of the plugin in pixels.
+Parameter: height
+ The height of the plugin in pixels.
+Parameter: showHeader
+ Whether to show a 'Recommendations' title bar.
+Parameter: colorScheme
+ The color scheme of the plugin: 'light' or 'dark'.
+Parameter: font
+ The font name for the plugin.
+Parameter: borderColor
+ The border color of the plugin. Use common color names as Red, White, Black, and so on.
+Parameter: filter
+ Allows you to filter which URLs are shown in the plugin. The plugin will only include URLs which contain the filter term in the first two path parameters of the URL.
+Parameter: refLabel
+ A label for tracking referrals; must be less than 50 characters and can contain alphanumeric characters and some punctuation (currently +/=-.:_).
+*@
+@helper Recommendations(
+ string site = "",
+ int width = 300,
+ int height = 300,
+ bool showHeader = true,
+ string colorScheme = "light",
+ string font = "",
+ string borderColor = "",
+ string filter = "",
+ string refLabel = ""
+ ) {
+ if (site.IsEmpty()) {
+ site = Request.Url.Host;
+ }
+
+ var src = new UrlBuilder("http://www.facebook.com/plugins/recommendations.php")
+ .AddParam("site", site)
+ .AddParam("width", width)
+ .AddParam("height", height)
+ .AddParam("header", showHeader)
+ .AddParam("colorscheme", colorScheme)
+ .AddParam("font", font)
+ .AddParam("border_color", borderColor)
+ .AddParam("filter", filter)
+ .AddParam("ref", refLabel)
+ .AddParam("locale", Language);
+
+ <iframe src="@src" scrolling="no" frameborder="0" style="border:none; overflow:hidden; width:@(width)px; height:@(height)px;" allowTransparency="true"></iframe>
+}
+
+@*
+Summary:
+ Shows a Facebook Like Box. The Like Box is a social plugin that enables Facebook Page owners to attract and gain Likes from their own website.
+ The Like Box enables users to:
+ - See how many users already like this page, and which of their friends like it too
+ - Read recent posts from the page
+ - Like the page with one click, without needing to visit the page
+
+Parameter: href
+ The URL of the Facebook Page for this Like box.
+Parameter: width
+ The width of the plugin in pixels.
+Parameter: height
+ The height of the plugin in pixels.
+Parameter: colorScheme
+ The color scheme of the plugin: 'light' or 'dark'.
+Parameter: connections
+ Number of shown users who have liked this Page. Use 0 to avoid showing the users box.
+Parameter: showStream
+ Shows the profile stream for the public profile of the page.
+Parameter: showHeader
+ Shows the 'Find us on Facebook' bar at top. Only shown when either stream or connections are present.
+*@
+@helper LikeBox(
+ string href,
+ int width = 292,
+ int height = 587,
+ string colorScheme = "light",
+ int connections = 10,
+ bool showStream = true,
+ bool showHeader = true) {
+
+ var src = new UrlBuilder("http://www.facebook.com/plugins/recommendations.php")
+ .AddParam("href", href)
+ .AddParam("width", width)
+ .AddParam("height", height)
+ .AddParam("header", showHeader)
+ .AddParam("colorscheme", colorScheme)
+ .AddParam("connections", connections)
+ .AddParam("stream", showStream)
+ .AddParam("header", showHeader)
+ .AddParam("locale", Language);
+
+ <iframe src="@src" scrolling="no" frameborder="0" style="border:none; overflow:hidden; width:@(width)px; height:@(height)px;" allowTransparency="true"></iframe>
+}
+
+@*
+Summary:
+ Shows a Facebook Facepile plugin.
+ The Facepile plugin shows the Facebook profile pictures of the user's friends who have already signed up for your site.
+ The plugin doesn't show up if the user is logged out of Facebook or doesn't have friends who have signed up for your site using Facebook.
+
+Parameter: maxRows
+ The maximum number of rows of profile pictures to show. The plugin dynamically sizes its height; for example, if you specify a maximum of four rows, and there are only enough friends to fill two rows, the height of the plugin will be only what is needed for two rows of profile pictures.
+Parameter: width
+ The width of the plugin in pixels.
+*@
+@helper Facepile(
+ int maxRows = 1,
+ int width = 200) {
+ <fb:facepile max-rows="@maxRows" width="@width"></fb:facepile>
+}
+
+@*
+Summary:
+ Shows a Facebook Live Stream plugin.
+ The Live Stream plugin lets users visiting your site or application share activity and comments in real time.
+ The Live Stream Box works best when you are running a real-time event, like live streaming video for concerts, speeches, or webcasts, live Web chats,
+ webinars, massively multiplayer games.
+
+Parameter: width
+ The width of the plugin in pixels.
+Parameter: height
+ The height of the plugin in pixels.
+Parameter: xid
+ If you have multiple live stream boxes on the same page, specify a unique 'xid' for each.
+Parameter: viaUrl
+ The URL that users are redirected to when they click on your app name on a status (if not specified, your Connect URL is used).
+Parameter: alwaysPostToFriends
+ If set, all user posts will always go to their profile. This option should only be used when users' posts are likely to make sense outside of the context of the event.
+*@
+@helper LiveStream(
+ int width = 400,
+ int height = 500,
+ string xid = "",
+ string viaUrl = "",
+ bool alwaysPostToFriends = false) {
+
+
+ var builder = new UrlBuilder("http://www.facebook.com/plugins/live_stream_box.php")
+ .AddParam("app_id", AppId)
+ .AddParam("width", width)
+ .AddParam("height", height)
+ .AddParam("always_post_to_friends", alwaysPostToFriends)
+ .AddParam("locale", Language);
+
+ if (!xid.IsEmpty()) {
+ builder.AddParam("xid", xid);
+ builder.AddParam("via_url", viaUrl);
+ }
+
+ <iframe src="@builder" scrolling="no" frameborder="0" style="border:none; overflow:hidden; width:@(width)px; height:@(height)px;" allowTransparency="true"></iframe>
+}
+
+@*
+Summary:
+ Shows a Facebook Activity Feed plugin.
+ The Activity Feed plugin displays the most interesting recent activity taking place on your site. Since the content is hosted by Facebook,
+ the plugin can display personalized content whether or not the user has logged into your site. The activity feed displays stories both when
+ users like content on your site and when users share content from your site back to Facebook.
+
+ *NOTE*: This helper method requires that your site is published into a public address where others can use it. Check this tutorial on publishing with WebMatrix: http://www.asp.net/webmatrix/tutorials/publish-a-website
+
+Parameter: site
+ The address of your published site. For example "www.fourthcoffee.com"
+Parameter: width
+ The width of the plugin in pixels.
+Parameter: height
+ The height of the plugin in pixels.
+Parameter: showHeader
+ Show the 'Recent Activity' title bar.
+Parameter: colorScheme
+ The color scheme of the plugin: 'light' or 'dark'.
+Parameter: font
+ The font name for the plugin.
+Parameter: borderColor
+ The border color of the plugin. Use common color names as Red, White, Black, and so on.
+Parameter: showRecommendations
+ Whether to show recommendations on the activity feed.
+*@
+@helper ActivityFeed(
+ string site = "",
+ int width = 300,
+ int height = 300,
+ bool showHeader = true,
+ string colorScheme = "light",
+ string font = "",
+ string borderColor = "",
+ bool showRecommendations = false) {
+ if (site.IsEmpty()) {
+ site = Request.Url.Host;
+ }
+
+ var src = new UrlBuilder("http://www.facebook.com/plugins/activity.php")
+ .AddParam("site", site)
+ .AddParam("width", width)
+ .AddParam("height", height)
+ .AddParam("header", showHeader)
+ .AddParam("colorscheme", colorScheme)
+ .AddParam("font", font)
+ .AddParam("border_color", borderColor)
+ .AddParam("recommendations", showRecommendations)
+ .AddParam("locale", Language);
+
+ <iframe src="@src" scrolling="no" frameborder="0" style="border:none; overflow:hidden; width:300px; height:300px;" allowTransparency="true"></iframe>
+}
+
+@*
+Summary:
+ OpenGraph properties allows you to specify structured information about your web pages to show up your pages richly across Facebook and enable Facebook users to establish connections to your pages.
+
+ Use this method to show OpenGraph page data, as the page title, URL, and so on.
+
+ *NOTE*: It is important to notice that the OpenGraph properties are read directly from your site by Facebook, therefore your site needs to be published into a public address to see the OpenGraph properties working.
+ Check this tutorial on publishing with WebMatrix: http://www.asp.net/webmatrix/tutorials/publish-a-website
+
+Parameter: siteName
+ A human-readable name for your site, e.g., "Fourth Coffee".
+Parameter: title
+ The title of your page as it should appear within Facebook, e.g., "Pecan Pie".
+Parameter: type
+ The type of your page, e.g., "food". See the complete list of supported types here: http://developers.facebook.com/docs/opengraph#types
+Parameter: url
+ The canonical URL of your object that will be used as its permanent ID in the graph, e.g., http://www.fourthcoffe.com/Product/1/.
+Parameter: imageUrl
+ An image URL which should represent your page within the graph. The image must be at least 50px by 50px and have a maximum aspect ratio of 3:1.
+Parameter: description
+ A one to two sentence description of your page.
+*@
+@helper OpenGraphRequiredProperties(
+ string siteName,
+ string title,
+ string type,
+ string url,
+ string imageUrl,
+ string description = "") {
+ <meta property="og:site_name" content="@siteName"/>
+ <meta property="fb:app_id" content="@AppId"/>
+ <meta property="og:title" content="@title"/>
+ <meta property="og:type" content="@type"/>
+ <meta property="og:url" content="@url"/>
+ <meta property="og:image" content="@imageUrl"/>
+ if (!description.IsEmpty()) {
+ <meta property="og:description" content="@description"/>
+ }
+}
+
+@*
+Summary:
+ OpenGraph properties allows you to specify structured information about your web pages to show up your pages richly across Facebook and enable Facebook users to establish connections to your pages.
+
+ Use this method to show page location data. This is useful if your pages is a business profile or about anything else with a real-world location. You can specify location via latitude and longitude, a full address, or both.
+
+ *NOTE*: It is important to notice that the OpenGraph properties are read directly from your site by Facebook, therefore your site needs to be published into a public address to see the OpenGraph properties working.
+ Check this tutorial on publishing with WebMatrix: http://www.asp.net/webmatrix/tutorials/publish-a-website
+*@
+@helper OpenGraphLocationProperties(
+ string latitude = "",
+ string longitude = "",
+ string streetAddress = "",
+ string locality = "",
+ string region = "",
+ string postalCode = "",
+ string countryName = "") {
+ if (!latitude.IsEmpty()) {
+ <meta property="og:latitude" content="@latitude"/>
+ }
+ if (!longitude.IsEmpty()) {
+ <meta property="og:longitude" content="@longitude"/>
+ }
+ if (!streetAddress.IsEmpty()) {
+ <meta property="og:street-address" content="@streetAddress"/>
+ }
+ if (!locality.IsEmpty()) {
+ <meta property="og:locality" content="@locality"/>
+ }
+ if (!region.IsEmpty()) {
+ <meta property="og:region" content="@region"/>
+ }
+ if (!postalCode.IsEmpty()) {
+ <meta property="og:postal-code" content="@postalCode"/>
+ }
+ if (!countryName.IsEmpty()) {
+ <meta property="og:country-name" content="@countryName"/>
+ }
+}
+
+@*
+Summary:
+ OpenGraph properties allows you to specify structured information about your web pages to show up your pages richly across Facebook and enable Facebook users to establish connections to your pages.
+
+ Use this method to show contact information about your page. Consider including contact information if your page is about an entity that can be contacted.
+
+ *NOTE*: It is important to notice that the OpenGraph properties are read directly from your site by Facebook, therefore your site needs to be published into a public address to see the OpenGraph properties working.
+ Check this tutorial on publishing with WebMatrix: http://www.asp.net/webmatrix/tutorials/publish-a-website
+*@
+@helper OpenGraphContactProperties(
+ string email = "",
+ string phoneNumber = "",
+ string faxNumber = "") {
+
+ if (!email.IsEmpty()) {
+ <meta property="og:email" content="@email"/>
+ }
+ if (!phoneNumber.IsEmpty()) {
+ <meta property="og:phone_number" content="@phoneNumber"/>
+ }
+ if (!faxNumber.IsEmpty()) {
+ <meta property="og:fax_number" content="@faxNumber"/>
+ }
+
+}
+
+@*
+Summary:
+ Use this method inside your opening HTML tag for W3C compatibility.
+ For example: <html xmlns="http://www.w3.org/1999/xhtml" @Facebook.FbmlNamespaces()>
+*@
+@helper FbmlNamespaces() {<text>xmlns:fb="http://www.facebook.com/2008/fbml" xmlns:og="http://opengraphprotocol.org/schema/"</text>}
diff --git a/src/Microsoft.Web.Helpers/Facebook.generated.cs b/src/Microsoft.Web.Helpers/Facebook.generated.cs
new file mode 100644
index 00000000..a7b2751e
--- /dev/null
+++ b/src/Microsoft.Web.Helpers/Facebook.generated.cs
@@ -0,0 +1,1582 @@
+#pragma warning disable 1591
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.235
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Microsoft.Web.Helpers
+{
+ using System;
+ using System.Collections.Generic;
+
+ #line 3 "..\..\Facebook.cshtml"
+ using System.Collections.Specialized;
+
+ #line default
+ #line hidden
+
+ #line 4 "..\..\Facebook.cshtml"
+ using System.Globalization;
+
+ #line default
+ #line hidden
+ using System.IO;
+ using System.Linq;
+ using System.Net;
+
+ #line 5 "..\..\Facebook.cshtml"
+ using System.Security;
+
+ #line default
+ #line hidden
+
+ #line 6 "..\..\Facebook.cshtml"
+ using System.Text;
+
+ #line default
+ #line hidden
+ using System.Web;
+
+ #line 7 "..\..\Facebook.cshtml"
+ using System.Web.Helpers;
+
+ #line default
+ #line hidden
+ using System.Web.Security;
+ using System.Web.UI;
+ using System.Web.WebPages;
+ using System.Web.WebPages.Html;
+
+ #line 8 "..\..\Facebook.cshtml"
+ using System.Web.WebPages.Scope;
+
+ #line default
+ #line hidden
+
+ #line 9 "..\..\Facebook.cshtml"
+ using WebMatrix.Data;
+
+ #line default
+ #line hidden
+
+ #line 10 "..\..\Facebook.cshtml"
+ using WebMatrix.WebData;
+
+ #line default
+ #line hidden
+
+ public class Facebook : System.Web.WebPages.HelperPage
+ {
+
+ #line 12 "..\..\Facebook.cshtml"
+
+
+ private const string FacebookCredentialsTableName = "webpages_FacebookCredentials";
+ private const string FacebookCredentialsIdColumn = "FacebookId";
+ private const string FacebookCredentialsUserIdColumn = "UserId";
+
+ private const string DefaultUserIdColumn = "UserId";
+ private const string DefaultUserNameColumn = "email";
+ private const string DefaultUserTableName = "UserProfile";
+
+ private const string DefaultFacebookPerms = "email";
+ private const string DefaultCallbackUrl = "~/Facebook/Login";
+ private const string FacebookApiProfileUrl = "https://graph.facebook.com/me";
+ private const string FacebookCookieAccessToken = "access_token";
+
+ private static readonly object _isInitializedKey = new object();
+ private static readonly object _membershipDBNameKey = new object();
+ private static readonly object _appIdKey = new object();
+ private static readonly object _appSecretKey = new object();
+ private static readonly object _language = new object();
+
+ public static bool HasMembershipIntegration {
+ get {
+ return !MembershipDBName.IsEmpty();
+ }
+ }
+
+ public static bool IsFacebookUserAuthenticated {
+ get {
+ return !GetFacebookCookieInfo(HttpContext, "uid").IsEmpty();
+ }
+ }
+
+ public static bool IsFacebookUserAssociated {
+ get {
+ return !GetAssociatedMembershipUserName().IsEmpty();
+ }
+ }
+
+ public static bool IsInitialized {
+ get {
+ return (bool)(ScopeStorage.CurrentScope[_isInitializedKey] ?? false);
+ }
+
+ private set {
+ ScopeStorage.CurrentScope[_isInitializedKey] = value;
+ }
+ }
+
+ public static string MembershipDBName {
+ get {
+ return (string)(ScopeStorage.CurrentScope[_membershipDBNameKey] ?? "");
+ }
+
+ set {
+ ScopeStorage.CurrentScope[_membershipDBNameKey] = value;
+ }
+ }
+
+ public static string AppId {
+ get {
+ return (string)(ScopeStorage.CurrentScope[_appIdKey] ?? "");
+ }
+
+ set {
+ ScopeStorage.CurrentScope[_appIdKey] = value;
+ }
+ }
+
+ public static string AppSecret {
+ get {
+ return (string)(ScopeStorage.CurrentScope[_appSecretKey] ?? "");
+ }
+
+ set {
+ ScopeStorage.CurrentScope[_appSecretKey] = value;
+ }
+ }
+
+ public static string Language {
+ get {
+ return (string)(ScopeStorage.CurrentScope[_language] ?? "en_US");
+ }
+
+ set {
+ ScopeStorage.CurrentScope[_language] = value;
+ }
+ }
+
+ private static HttpContextBase HttpContext {
+ get {
+ return new HttpContextWrapper(System.Web.HttpContext.Current);
+ }
+ }
+
+ ///<summary>
+ /// Initialize the helper with your Facebook application settings.
+ ///
+ /// If the 'membershipDBName' parameter is specified, Facebook membership integration will be enabled,
+ /// allowing users to register and associate their Facebook user account (identified with the e-mail)
+ /// with your site membership and the WebSecurity helper.
+ /// In this case, the helper will initialize the WebSecurity WebMatrix helper automatically (if not done previously)
+ /// and the store the Facebook membership information in the 'membershipDbName' database.
+ ///</summary>
+ ///<param name="appId">Facebook application id.</param>
+ ///<param name="appSecret">Facebook application secret.</param>
+ ///<param name="membershipDbName">Name of the database used for storing the membership data.</param>
+ public static void Initialize(string appId, string appSecret, string membershipDbName = "") {
+ AppId = appId;
+ AppSecret = appSecret;
+ IsInitialized = true;
+
+ if (!membershipDbName.IsEmpty()) {
+ MembershipDBName = membershipDbName;
+
+ InitializeMembershipProviderIfNeeded();
+ InitializeFacebookTableIfNeeded();
+ }
+ }
+
+ ///<summary>
+ /// Retrieves the Facebook profile of current logged in user.
+ ///</summary>
+ public static UserProfile GetFacebookUserProfile() {
+ var accessToken = GetFacebookCookieInfo(HttpContext, FacebookCookieAccessToken);
+
+ if (accessToken.IsEmpty()) {
+ return null;
+ }
+
+ var userProfileUrl = new Uri(new UrlBuilder(FacebookApiProfileUrl).AddParam(FacebookCookieAccessToken, accessToken));
+
+ using (var client = new WebClient()) {
+ using (var receiveStream = client.OpenRead(userProfileUrl)) {
+ var result = new StreamReader(receiveStream).ReadToEnd();
+ var profile = Json.Decode<UserProfile>(result);
+
+ return profile;
+ }
+ }
+ }
+
+ ///<summary>
+ /// Associates the specified User Name (e.g. email, depending on your membership model) with the current Facebook User Id from the logged user.
+ ///</summary>
+ ///<param name="userName">The user name to associate the current logged-in facebook account to.</param>
+ public static void AssociateMembershipAccount(string userName) {
+ if (!IsFacebookUserAuthenticated) {
+ throw new InvalidOperationException("No Facebook user is authenticated.");
+ }
+
+ if (IsFacebookUserAssociated) {
+ throw new InvalidOperationException("The authenticated Facebook user is already associated to a membership account.");
+ }
+
+ using (var db = Database.Open(MembershipDBName)) {
+ var facebookUserId = GetFacebookCookieInfo(HttpContext, "uid").As<long>();
+
+ var userId = WebSecurity.GetUserId(userName);
+ db.Execute(String.Format(CultureInfo.InvariantCulture, "INSERT INTO {0} ({1}, {2}) VALUES (@0, @1)", FacebookCredentialsTableName, FacebookCredentialsUserIdColumn, FacebookCredentialsIdColumn),
+ userId, facebookUserId);
+
+ // User is registered in the application
+ FormsAuthentication.SetAuthCookie(userName, false);
+ }
+ }
+
+ public static bool MembershipLogin() {
+ var user = GetAssociatedMembershipUserName();
+
+ if (!user.IsEmpty()) {
+ // User is registered in the application
+ FormsAuthentication.SetAuthCookie(user, false);
+ return true;
+ }
+ else {
+ return false;
+ }
+ }
+
+ private static void InitializeMembershipProviderIfNeeded() {
+ var provider = GetMembershipProvider();
+
+ if (IsMembershipProviderInitialized(provider)) {
+ WebSecurity.InitializeDatabaseConnection(MembershipDBName, DefaultUserTableName, DefaultUserIdColumn, DefaultUserNameColumn, true);
+ }
+ }
+
+ private static void InitializeFacebookTableIfNeeded() {
+ using (var db = Database.Open(MembershipDBName)) {
+ var table = db.QuerySingle("SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = @0", FacebookCredentialsTableName);
+
+ if (table == null) {
+ db.Execute(String.Format(CultureInfo.InvariantCulture, "CREATE TABLE {0} ({1} INT NOT NULL, {2} BIGINT NOT NULL)", FacebookCredentialsTableName, FacebookCredentialsUserIdColumn, FacebookCredentialsIdColumn));
+ }
+ }
+ }
+
+ private static string GetAssociatedMembershipUserName() {
+ var userName = "";
+
+ if (IsFacebookUserAuthenticated) {
+ using (var db = Database.Open(MembershipDBName)) {
+ var userId = db.QueryValue(String.Format(CultureInfo.InvariantCulture, "SELECT {0} FROM {1} WHERE {2} = LOWER(@0)", FacebookCredentialsUserIdColumn, FacebookCredentialsTableName, FacebookCredentialsIdColumn),
+ GetFacebookCookieInfo(HttpContext, "uid"));
+ if (userId != null) {
+ userName = GetUserName(userId);
+ }
+ }
+ }
+
+ return userName;
+ }
+
+ private static string GetUserName(int userId) {
+ var userName = "";
+
+ using (var db = Database.Open(MembershipDBName)) {
+ var provider = GetMembershipProvider();
+ userName = db.QueryValue(String.Format(CultureInfo.InvariantCulture, "SELECT {0} FROM {1} WHERE {2} = @0", provider.UserNameColumn, provider.UserTableName, provider.UserIdColumn), userId);
+ }
+
+ return userName;
+ }
+
+ private static SimpleMembershipProvider GetMembershipProvider() {
+ var provider = Membership.Provider as SimpleMembershipProvider;
+
+ if (provider == null) {
+ throw new InvalidOperationException("Simple Membership Provider not found.");
+ }
+
+ return provider;
+ }
+
+ private static bool IsMembershipProviderInitialized(SimpleMembershipProvider provider) {
+ return provider.UserTableName.IsEmpty() || provider.UserIdColumn.IsEmpty() || provider.UserNameColumn.IsEmpty();
+ }
+
+ internal static string GetFacebookCookieInfo(HttpContextBase httpContext, string key) {
+ var request = httpContext.Request;
+ var name = "fbs_" + AppId;
+
+ if (request.Cookies[name] != null) {
+ var value = request.Cookies[name].Value;
+ var args = HttpUtility.ParseQueryString(value.Replace("\"", ""));
+
+ if (!IsFacebookCookieValid(args)) {
+ throw new InvalidOperationException("Invalid Facebook cookie.");
+ }
+
+ return args[key];
+ }
+ else {
+ return "";
+ }
+ }
+
+ private static bool IsFacebookCookieValid(NameValueCollection args) {
+ var payload = new StringBuilder();
+ var keys = args.AllKeys;
+ Array.Sort(keys);
+ foreach (var key in keys) {
+ if (!key.Equals("sig", StringComparison.OrdinalIgnoreCase)) {
+ payload.AppendFormat("{0}={1}", key, args[key]);
+ }
+ }
+
+ payload.Append(AppSecret);
+
+ // Review: The HMAC uses MD5 which is not cryptographically secure. We need to investigate other Facebook authentication methods.
+ var signature = new StringBuilder();
+ using (var md5 = System.Security.Cryptography.MD5CryptoServiceProvider.Create()) {
+ var hash = md5.ComputeHash(Encoding.ASCII.GetBytes(payload.ToString()));
+ for (int i = 0; i < hash.Length; i++) {
+ signature.Append(hash[i].ToString("X2", CultureInfo.InvariantCulture));
+ }
+ }
+ return String.Equals(args["sig"], signature.ToString(), StringComparison.OrdinalIgnoreCase);
+ }
+
+ public class UserProfile {
+ public string Id { get; set; }
+ public string Name { get; set; }
+ public string First_Name { get; set; }
+ public string Last_Name { get; set; }
+ public string Link { get; set; }
+ public string Bio { get; set; }
+ public string Gender { get; set; }
+ public string Email { get; set; }
+ public string Timezone { get; set; }
+ public string Locale { get; set; }
+ public string Updated_Time { get; set; }
+ }
+
+ private static IHtmlString RawJS(string text) {
+ return new HtmlString(HttpUtility.JavaScriptStringEncode(text));
+ }
+
+ #line default
+ #line hidden
+
+public static System.Web.WebPages.HelperResult GetInitializationScripts() {
+return new System.Web.WebPages.HelperResult(__razor_helper_writer => {
+
+
+
+#line 316 "..\..\Facebook.cshtml"
+
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " <div id=\"fb-root\"></div>\r\n");
+
+
+
+WriteLiteralTo(@__razor_helper_writer, " <script type=\"text/javascript\">\r\n window.fbAsyncInit = function () {\r\n" +
+" FB.init({ appId: \'");
+
+
+
+#line 320 "..\..\Facebook.cshtml"
+WriteTo(@__razor_helper_writer, RawJS(AppId));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\', status: true, cookie: true, xfbml: true });\r\n };\r\n (function () " +
+"{\r\n var e = document.createElement(\'script\'); e.async = true;\r\n " +
+" e.src = document.location.protocol +\r\n \'//connect.facebook.net/" +
+"");
+
+
+
+#line 325 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, RawJS(Language));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "/all.js\';\r\n document.getElementById(\'fb-root\').appendChild(e);\r\n " +
+" } ());\r\n\r\n function loginRedirect(url) { window.location = url; }\r\n " +
+"</script>\r\n");
+
+
+
+#line 331 "..\..\Facebook.cshtml"
+
+#line default
+#line hidden
+
+});
+
+}
+
+
+public static System.Web.WebPages.HelperResult LoginButton(
+ string registerUrl,
+ string returnUrl = "~/",
+ string callbackUrl = DefaultCallbackUrl,
+ string buttonText = "",
+ bool autoLogoutLink = false,
+ string size = "medium",
+ string length = "long",
+ bool showFaces = false,
+ string extendedPermissions = "") {
+return new System.Web.WebPages.HelperResult(__razor_helper_writer => {
+
+
+
+#line 366 "..\..\Facebook.cshtml"
+
+
+ var redirectUrl = new UrlBuilder(callbackUrl)
+ .AddParam("registerUrl", new UrlBuilder(registerUrl))
+ .AddParam("returnUrl", new UrlBuilder(returnUrl));
+ var onLogin = String.Format(CultureInfo.InvariantCulture, "loginRedirect('{0}')", RawJS(redirectUrl));
+ extendedPermissions = extendedPermissions.IsEmpty() ? "email" : String.Concat("email,", extendedPermissions);
+
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " <fb:login-button autologoutlink=\"");
+
+
+
+#line 374 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, autoLogoutLink);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\" size=\"");
+
+
+
+#line 374 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, size);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\" length=\"");
+
+
+
+#line 374 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, length);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\" onlogin=\"");
+
+
+
+#line 374 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, onLogin);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\" show-faces=\"");
+
+
+
+#line 374 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, showFaces);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\" perms=\"");
+
+
+
+#line 374 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, extendedPermissions);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\">");
+
+
+
+#line 374 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, buttonText);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "</fb:login-button> \r\n");
+
+
+
+#line 375 "..\..\Facebook.cshtml"
+
+#line default
+#line hidden
+
+});
+
+}
+
+
+public static System.Web.WebPages.HelperResult LoginButtonTagOnly(
+ string buttonText = "",
+ bool autoLogoutLink = false,
+ string size = "long",
+ string length = "short",
+ string onLogin = "",
+ bool showFaces = false,
+ string extendedPermissions = "") {
+return new System.Web.WebPages.HelperResult(__razor_helper_writer => {
+
+
+
+#line 403 "..\..\Facebook.cshtml"
+
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " <fb:login-button autologoutlink=\"");
+
+
+
+#line 404 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, autoLogoutLink);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\" size=\"");
+
+
+
+#line 404 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, size);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\" length=\"");
+
+
+
+#line 404 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, length);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\" onlogin=\"");
+
+
+
+#line 404 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, onLogin);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\" show-faces=\"");
+
+
+
+#line 404 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, showFaces);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\" perms=\"");
+
+
+
+#line 404 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, extendedPermissions);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\">");
+
+
+
+#line 404 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, buttonText);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "</fb:login-button>\r\n");
+
+
+
+#line 405 "..\..\Facebook.cshtml"
+
+#line default
+#line hidden
+
+});
+
+}
+
+
+public static System.Web.WebPages.HelperResult LikeButton(
+ string href = "",
+ string buttonLayout = "standard",
+ bool showFaces = true,
+ int width = 450,
+ int height = 80,
+ string action = "like",
+ string font = "",
+ string colorScheme = "light",
+ string refLabel = ""
+ ) {
+return new System.Web.WebPages.HelperResult(__razor_helper_writer => {
+
+
+
+#line 445 "..\..\Facebook.cshtml"
+
+
+ if (href.IsEmpty()) {
+ href = Request.Url.OriginalString;
+ }
+
+ var src = new UrlBuilder("http://www.facebook.com/plugins/like.php")
+ .AddParam("href", href)
+ .AddParam("layout", buttonLayout)
+ .AddParam("show_faces", showFaces)
+ .AddParam("width", width)
+ .AddParam("action", action)
+ .AddParam("colorscheme", colorScheme)
+ .AddParam("height", height)
+ .AddParam("font", font)
+ .AddParam("locale", Language)
+ .AddParam("ref", refLabel);
+
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " <iframe src=\"");
+
+
+
+#line 463 "..\..\Facebook.cshtml"
+WriteTo(@__razor_helper_writer, src);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\" scrolling=\"no\" frameborder=\"0\" style=\"border:none; overflow:hidden; width:");
+
+
+
+#line 463 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, width);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "px; height:");
+
+
+
+#line 463 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, height);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "px;\" allowTransparency=\"true\"></iframe>\r\n");
+
+
+
+#line 464 "..\..\Facebook.cshtml"
+
+#line default
+#line hidden
+
+});
+
+}
+
+
+public static System.Web.WebPages.HelperResult Comments(
+ string xid = "",
+ int width = 550,
+ int numPosts = 10,
+ bool reverseOrder = false,
+ bool removeRoundedBox = false) {
+return new System.Web.WebPages.HelperResult(__razor_helper_writer => {
+
+
+
+#line 489 "..\..\Facebook.cshtml"
+
+
+#line default
+#line hidden
+
+
+
+#line 490 "..\..\Facebook.cshtml"
+WriteLiteralTo(@__razor_helper_writer, " <fb:comments ");
+
+#line default
+#line hidden
+
+
+#line 490 "..\..\Facebook.cshtml"
+ if (!xid.IsEmpty()) {
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " ");
+
+WriteLiteralTo(@__razor_helper_writer, "xid=\"");
+
+
+
+#line 490 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, xid);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\" ");
+
+WriteLiteralTo(@__razor_helper_writer, " ");
+
+
+
+#line 490 "..\..\Facebook.cshtml"
+ }
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "numposts=\"");
+
+
+
+#line 490 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, numPosts);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\" width=\"");
+
+
+
+#line 490 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, width);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\" reverse=\"");
+
+
+
+#line 490 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, reverseOrder);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\" simple=\"");
+
+
+
+#line 490 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, removeRoundedBox);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\" ></fb:comments>\r\n");
+
+
+
+#line 491 "..\..\Facebook.cshtml"
+
+#line default
+#line hidden
+
+});
+
+}
+
+
+public static System.Web.WebPages.HelperResult Recommendations(
+ string site = "",
+ int width = 300,
+ int height = 300,
+ bool showHeader = true,
+ string colorScheme = "light",
+ string font = "",
+ string borderColor = "",
+ string filter = "",
+ string refLabel = ""
+ ) {
+return new System.Web.WebPages.HelperResult(__razor_helper_writer => {
+
+
+
+#line 531 "..\..\Facebook.cshtml"
+
+ if (site.IsEmpty()) {
+ site = Request.Url.Host;
+ }
+
+ var src = new UrlBuilder("http://www.facebook.com/plugins/recommendations.php")
+ .AddParam("site", site)
+ .AddParam("width", width)
+ .AddParam("height", height)
+ .AddParam("header", showHeader)
+ .AddParam("colorscheme", colorScheme)
+ .AddParam("font", font)
+ .AddParam("border_color", borderColor)
+ .AddParam("filter", filter)
+ .AddParam("ref", refLabel)
+ .AddParam("locale", Language);
+
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " <iframe src=\"");
+
+
+
+#line 548 "..\..\Facebook.cshtml"
+WriteTo(@__razor_helper_writer, src);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\" scrolling=\"no\" frameborder=\"0\" style=\"border:none; overflow:hidden; width:");
+
+
+
+#line 548 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, width);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "px; height:");
+
+
+
+#line 548 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, height);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "px;\" allowTransparency=\"true\"></iframe>\r\n");
+
+
+
+#line 549 "..\..\Facebook.cshtml"
+
+#line default
+#line hidden
+
+});
+
+}
+
+
+public static System.Web.WebPages.HelperResult LikeBox(
+ string href,
+ int width = 292,
+ int height = 587,
+ string colorScheme = "light",
+ int connections = 10,
+ bool showStream = true,
+ bool showHeader = true) {
+return new System.Web.WebPages.HelperResult(__razor_helper_writer => {
+
+
+
+#line 581 "..\..\Facebook.cshtml"
+
+
+ var src = new UrlBuilder("http://www.facebook.com/plugins/recommendations.php")
+ .AddParam("href", href)
+ .AddParam("width", width)
+ .AddParam("height", height)
+ .AddParam("header", showHeader)
+ .AddParam("colorscheme", colorScheme)
+ .AddParam("connections", connections)
+ .AddParam("stream", showStream)
+ .AddParam("header", showHeader)
+ .AddParam("locale", Language);
+
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " <iframe src=\"");
+
+
+
+#line 594 "..\..\Facebook.cshtml"
+WriteTo(@__razor_helper_writer, src);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\" scrolling=\"no\" frameborder=\"0\" style=\"border:none; overflow:hidden; width:");
+
+
+
+#line 594 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, width);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "px; height:");
+
+
+
+#line 594 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, height);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "px;\" allowTransparency=\"true\"></iframe> \r\n");
+
+
+
+#line 595 "..\..\Facebook.cshtml"
+
+#line default
+#line hidden
+
+});
+
+}
+
+
+public static System.Web.WebPages.HelperResult Facepile(
+ int maxRows = 1,
+ int width = 200) {
+return new System.Web.WebPages.HelperResult(__razor_helper_writer => {
+
+
+
+#line 610 "..\..\Facebook.cshtml"
+
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " <fb:facepile max-rows=\"");
+
+
+
+#line 611 "..\..\Facebook.cshtml"
+WriteTo(@__razor_helper_writer, maxRows);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\" width=\"");
+
+
+
+#line 611 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, width);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\"></fb:facepile>\r\n");
+
+
+
+#line 612 "..\..\Facebook.cshtml"
+
+#line default
+#line hidden
+
+});
+
+}
+
+
+public static System.Web.WebPages.HelperResult LiveStream(
+ int width = 400,
+ int height = 500,
+ string xid = "",
+ string viaUrl = "",
+ bool alwaysPostToFriends = false) {
+return new System.Web.WebPages.HelperResult(__razor_helper_writer => {
+
+
+
+#line 637 "..\..\Facebook.cshtml"
+
+
+
+ var builder = new UrlBuilder("http://www.facebook.com/plugins/live_stream_box.php")
+ .AddParam("app_id", AppId)
+ .AddParam("width", width)
+ .AddParam("height", height)
+ .AddParam("always_post_to_friends", alwaysPostToFriends)
+ .AddParam("locale", Language);
+
+ if (!xid.IsEmpty()) {
+ builder.AddParam("xid", xid);
+ builder.AddParam("via_url", viaUrl);
+ }
+
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " <iframe src=\"");
+
+
+
+#line 652 "..\..\Facebook.cshtml"
+WriteTo(@__razor_helper_writer, builder);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\" scrolling=\"no\" frameborder=\"0\" style=\"border:none; overflow:hidden; width:");
+
+
+
+#line 652 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, width);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "px; height:");
+
+
+
+#line 652 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, height);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "px;\" allowTransparency=\"true\"></iframe>\r\n");
+
+
+
+#line 653 "..\..\Facebook.cshtml"
+
+#line default
+#line hidden
+
+});
+
+}
+
+
+public static System.Web.WebPages.HelperResult ActivityFeed(
+ string site = "",
+ int width = 300,
+ int height = 300,
+ bool showHeader = true,
+ string colorScheme = "light",
+ string font = "",
+ string borderColor = "",
+ bool showRecommendations = false) {
+return new System.Web.WebPages.HelperResult(__razor_helper_writer => {
+
+
+
+#line 689 "..\..\Facebook.cshtml"
+
+ if (site.IsEmpty()) {
+ site = Request.Url.Host;
+ }
+
+ var src = new UrlBuilder("http://www.facebook.com/plugins/activity.php")
+ .AddParam("site", site)
+ .AddParam("width", width)
+ .AddParam("height", height)
+ .AddParam("header", showHeader)
+ .AddParam("colorscheme", colorScheme)
+ .AddParam("font", font)
+ .AddParam("border_color", borderColor)
+ .AddParam("recommendations", showRecommendations)
+ .AddParam("locale", Language);
+
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " <iframe src=\"");
+
+
+
+#line 705 "..\..\Facebook.cshtml"
+WriteTo(@__razor_helper_writer, src);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\" scrolling=\"no\" frameborder=\"0\" style=\"border:none; overflow:hidden; width:300px" +
+"; height:300px;\" allowTransparency=\"true\"></iframe>\r\n");
+
+
+
+#line 706 "..\..\Facebook.cshtml"
+
+#line default
+#line hidden
+
+});
+
+}
+
+
+public static System.Web.WebPages.HelperResult OpenGraphRequiredProperties(
+ string siteName,
+ string title,
+ string type,
+ string url,
+ string imageUrl,
+ string description = "") {
+return new System.Web.WebPages.HelperResult(__razor_helper_writer => {
+
+
+
+#line 736 "..\..\Facebook.cshtml"
+
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " <meta property=\"og:site_name\" content=\"");
+
+
+
+#line 737 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, siteName);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\"/>\r\n");
+
+
+
+WriteLiteralTo(@__razor_helper_writer, " <meta property=\"fb:app_id\" content=\"");
+
+
+
+#line 738 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, AppId);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\"/> \r\n");
+
+
+
+WriteLiteralTo(@__razor_helper_writer, " <meta property=\"og:title\" content=\"");
+
+
+
+#line 739 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, title);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\"/>\r\n");
+
+
+
+WriteLiteralTo(@__razor_helper_writer, " <meta property=\"og:type\" content=\"");
+
+
+
+#line 740 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, type);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\"/>\r\n");
+
+
+
+WriteLiteralTo(@__razor_helper_writer, " <meta property=\"og:url\" content=\"");
+
+
+
+#line 741 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, url);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\"/>\r\n");
+
+
+
+WriteLiteralTo(@__razor_helper_writer, " <meta property=\"og:image\" content=\"");
+
+
+
+#line 742 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, imageUrl);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\"/> \r\n");
+
+
+
+#line 743 "..\..\Facebook.cshtml"
+ if (!description.IsEmpty()) {
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " <meta property=\"og:description\" content=\"");
+
+
+
+#line 744 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, description);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\"/>\r\n");
+
+
+
+#line 745 "..\..\Facebook.cshtml"
+ }
+
+#line default
+#line hidden
+
+});
+
+}
+
+
+public static System.Web.WebPages.HelperResult OpenGraphLocationProperties(
+ string latitude = "",
+ string longitude = "",
+ string streetAddress = "",
+ string locality = "",
+ string region = "",
+ string postalCode = "",
+ string countryName = "") {
+return new System.Web.WebPages.HelperResult(__razor_helper_writer => {
+
+
+
+#line 764 "..\..\Facebook.cshtml"
+
+ if (!latitude.IsEmpty()) {
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " <meta property=\"og:latitude\" content=\"");
+
+
+
+#line 766 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, latitude);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\"/>\r\n");
+
+
+
+#line 767 "..\..\Facebook.cshtml"
+ }
+ if (!longitude.IsEmpty()) {
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " <meta property=\"og:longitude\" content=\"");
+
+
+
+#line 769 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, longitude);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\"/>\r\n");
+
+
+
+#line 770 "..\..\Facebook.cshtml"
+ }
+ if (!streetAddress.IsEmpty()) {
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " <meta property=\"og:street-address\" content=\"");
+
+
+
+#line 772 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, streetAddress);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\"/>\r\n");
+
+
+
+#line 773 "..\..\Facebook.cshtml"
+ }
+ if (!locality.IsEmpty()) {
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " <meta property=\"og:locality\" content=\"");
+
+
+
+#line 775 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, locality);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\"/>\r\n");
+
+
+
+#line 776 "..\..\Facebook.cshtml"
+ }
+ if (!region.IsEmpty()) {
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " <meta property=\"og:region\" content=\"");
+
+
+
+#line 778 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, region);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\"/>\r\n");
+
+
+
+#line 779 "..\..\Facebook.cshtml"
+ }
+ if (!postalCode.IsEmpty()) {
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " <meta property=\"og:postal-code\" content=\"");
+
+
+
+#line 781 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, postalCode);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\"/>\r\n");
+
+
+
+#line 782 "..\..\Facebook.cshtml"
+ }
+ if (!countryName.IsEmpty()) {
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " <meta property=\"og:country-name\" content=\"");
+
+
+
+#line 784 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, countryName);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\"/>\r\n");
+
+
+
+#line 785 "..\..\Facebook.cshtml"
+ }
+
+#line default
+#line hidden
+
+});
+
+}
+
+
+public static System.Web.WebPages.HelperResult OpenGraphContactProperties(
+ string email = "",
+ string phoneNumber = "",
+ string faxNumber = "") {
+return new System.Web.WebPages.HelperResult(__razor_helper_writer => {
+
+
+
+#line 800 "..\..\Facebook.cshtml"
+
+
+ if (!email.IsEmpty()) {
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " <meta property=\"og:email\" content=\"");
+
+
+
+#line 803 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, email);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\"/>\r\n");
+
+
+
+#line 804 "..\..\Facebook.cshtml"
+ }
+ if (!phoneNumber.IsEmpty()) {
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " <meta property=\"og:phone_number\" content=\"");
+
+
+
+#line 806 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, phoneNumber);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\"/>\r\n");
+
+
+
+#line 807 "..\..\Facebook.cshtml"
+ }
+ if (!faxNumber.IsEmpty()) {
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " <meta property=\"og:fax_number\" content=\"");
+
+
+
+#line 809 "..\..\Facebook.cshtml"
+ WriteTo(@__razor_helper_writer, faxNumber);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\"/>\r\n");
+
+
+
+#line 810 "..\..\Facebook.cshtml"
+ }
+
+
+#line default
+#line hidden
+
+});
+
+}
+
+
+public static System.Web.WebPages.HelperResult FbmlNamespaces() {
+return new System.Web.WebPages.HelperResult(__razor_helper_writer => {
+
+
+
+WriteLiteralTo(@__razor_helper_writer, "xmlns:fb=\"http://www.facebook.com/2008/fbml\" xmlns:og=\"http://opengraphprotocol.o" +
+"rg/schema/\"");
+
+
+
+#line 819 "..\..\Facebook.cshtml"
+
+#line default
+#line hidden
+
+});
+
+ }
+
+
+ public Facebook()
+ {
+ }
+ }
+}
+#pragma warning restore 1591
diff --git a/src/Microsoft.Web.Helpers/FileUpload.cshtml b/src/Microsoft.Web.Helpers/FileUpload.cshtml
new file mode 100644
index 00000000..b97b949b
--- /dev/null
+++ b/src/Microsoft.Web.Helpers/FileUpload.cshtml
@@ -0,0 +1,111 @@
+@* Generator: WebPagesHelper *@
+
+@using System.Globalization
+@using System.Web
+@using Microsoft.Internal.Web.Utils
+@using Resources
+
+@functions {
+ private class FileUploadTracker {
+ private static readonly object _countKey = new object();
+ private static readonly object _scriptAlreadyRendered = new object();
+ private readonly HttpContextBase _httpContext;
+
+ public FileUploadTracker(HttpContextBase httpContext) {
+ _httpContext = httpContext;
+ }
+
+ public bool ScriptAlreadyRendered {
+ get {
+ bool? rendered = _httpContext.Items[_scriptAlreadyRendered] as bool?;
+ return rendered.HasValue && rendered.Value;
+ }
+ set {
+ _httpContext.Items[_scriptAlreadyRendered] = value;
+ }
+ }
+
+ public int RenderCount {
+ get {
+ int? count = _httpContext.Items[_countKey] as int?;
+ if (!count.HasValue) {
+ count = 0;
+ }
+ return count.Value;
+ }
+ set {
+ _httpContext.Items[_countKey] = value;
+ }
+ }
+ }
+}
+
+@helper GetHtml(string name = null,
+ int initialNumberOfFiles = 1,
+ bool allowMoreFilesToBeAdded = true,
+ bool includeFormTag = true,
+ string addText = null,
+ string uploadText = null) {
+ @_GetHtml(new HttpContextWrapper(HttpContext.Current), name, initialNumberOfFiles, allowMoreFilesToBeAdded,
+ includeFormTag, addText, uploadText)
+}
+
+@helper _GetHtml(HttpContextBase context, string name, int initialNumberOfFiles,
+ bool allowMoreFilesToBeAdded, bool includeFormTag, string addText, string uploadText) {
+
+ if (initialNumberOfFiles < 0) {
+ throw new ArgumentOutOfRangeException(
+ "initialNumberOfFiles",
+ String.Format(CultureInfo.InvariantCulture, CommonResources.Argument_Must_Be_GreaterThanOrEqualTo, "0"));
+ }
+ var tracker = new FileUploadTracker(context);
+ int count = tracker.RenderCount++;
+
+ name = name ?? "fileUpload";
+ uploadText = uploadText ?? HelpersToolkitResources.FileUpload_Upload;
+ addText = addText ?? HelpersToolkitResources.FileUpload_AddMore;
+
+
+ if (allowMoreFilesToBeAdded && !tracker.ScriptAlreadyRendered) {
+ tracker.ScriptAlreadyRendered = true;
+
+ <script type="text/javascript">
+ if (!window["FileUploadHelper"]) window["FileUploadHelper"] = {};
+ FileUploadHelper.addInputElement = function(index, name) {
+ var inputElem = document.createElement("input");
+ inputElem.type = "file";
+ inputElem.name = name;
+ var divElem = document.createElement("div");
+ divElem.appendChild(inputElem.cloneNode(false));
+ var inputs = document.getElementById("file-upload-" + index);
+ inputs.appendChild(divElem);
+ }
+ </script>
+ }
+
+ if (includeFormTag) {
+ @:<form action="" enctype="multipart/form-data" method="post">
+ }
+ <div class="file-upload" id="file-upload-@(count)">
+ @for(int i = 0; i < initialNumberOfFiles; i++) {
+ <div>
+ <input name="@name" type="file" />
+ </div>
+ }
+ </div>
+
+ if (allowMoreFilesToBeAdded || includeFormTag) {
+ <div class="file-upload-buttons">
+ @if (allowMoreFilesToBeAdded) {
+ <a href="#" onclick="FileUploadHelper.addInputElement(@count, @HttpUtility.JavaScriptStringEncode(name, addDoubleQuotes: true)); return false;">@addText</a>
+ }
+ @if (includeFormTag) {
+ <input type="submit" value="@uploadText" />
+ }
+ </div>
+ }
+
+ if (includeFormTag) {
+ @:</form>
+ }
+}
diff --git a/src/Microsoft.Web.Helpers/FileUpload.generated.cs b/src/Microsoft.Web.Helpers/FileUpload.generated.cs
new file mode 100644
index 00000000..56aad6c1
--- /dev/null
+++ b/src/Microsoft.Web.Helpers/FileUpload.generated.cs
@@ -0,0 +1,351 @@
+using Resources;
+
+#pragma warning disable 1591
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.235
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Microsoft.Web.Helpers
+{
+ using System;
+ using System.Collections.Generic;
+
+ #line 3 "..\..\FileUpload.cshtml"
+ using System.Globalization;
+
+ #line default
+ #line hidden
+ using System.IO;
+ using System.Linq;
+ using System.Net;
+ using System.Text;
+
+ #line 4 "..\..\FileUpload.cshtml"
+ using System.Web;
+
+ #line default
+ #line hidden
+ using System.Web.Helpers;
+ using System.Web.Security;
+ using System.Web.UI;
+ using System.Web.WebPages;
+ using System.Web.WebPages.Html;
+
+ #line 5 "..\..\FileUpload.cshtml"
+ using Microsoft.Internal.Web.Utils;
+
+ #line default
+ #line hidden
+
+ public class FileUpload : System.Web.WebPages.HelperPage
+ {
+
+ #line 7 "..\..\FileUpload.cshtml"
+
+ private class FileUploadTracker {
+ private static readonly object _countKey = new object();
+ private static readonly object _scriptAlreadyRendered = new object();
+ private readonly HttpContextBase _httpContext;
+
+ public FileUploadTracker(HttpContextBase httpContext) {
+ _httpContext = httpContext;
+ }
+
+ public bool ScriptAlreadyRendered {
+ get {
+ bool? rendered = _httpContext.Items[_scriptAlreadyRendered] as bool?;
+ return rendered.HasValue && rendered.Value;
+ }
+ set {
+ _httpContext.Items[_scriptAlreadyRendered] = value;
+ }
+ }
+
+ public int RenderCount {
+ get {
+ int? count = _httpContext.Items[_countKey] as int?;
+ if (!count.HasValue) {
+ count = 0;
+ }
+ return count.Value;
+ }
+ set {
+ _httpContext.Items[_countKey] = value;
+ }
+ }
+ }
+
+ #line default
+ #line hidden
+
+public static System.Web.WebPages.HelperResult GetHtml(string name = null,
+ int initialNumberOfFiles = 1,
+ bool allowMoreFilesToBeAdded = true,
+ bool includeFormTag = true,
+ string addText = null,
+ string uploadText = null) {
+return new System.Web.WebPages.HelperResult(__razor_helper_writer => {
+
+
+
+#line 47 "..\..\FileUpload.cshtml"
+
+
+#line default
+#line hidden
+
+
+#line 48 "..\..\FileUpload.cshtml"
+WriteTo(@__razor_helper_writer, _GetHtml(new HttpContextWrapper(HttpContext.Current), name, initialNumberOfFiles, allowMoreFilesToBeAdded,
+ includeFormTag, addText, uploadText));
+
+#line default
+#line hidden
+
+
+#line 49 "..\..\FileUpload.cshtml"
+
+
+#line default
+#line hidden
+
+});
+
+}
+
+
+internal static System.Web.WebPages.HelperResult _GetHtml(HttpContextBase context, string name, int initialNumberOfFiles,
+ bool allowMoreFilesToBeAdded, bool includeFormTag, string addText, string uploadText) {
+return new System.Web.WebPages.HelperResult(__razor_helper_writer => {
+
+
+
+#line 53 "..\..\FileUpload.cshtml"
+
+
+ if (initialNumberOfFiles < 0) {
+ throw new ArgumentOutOfRangeException(
+ "initialNumberOfFiles",
+ String.Format(CultureInfo.InvariantCulture, CommonResources.Argument_Must_Be_GreaterThanOrEqualTo, "0"));
+ }
+ var tracker = new FileUploadTracker(context);
+ int count = tracker.RenderCount++;
+
+ name = name ?? "fileUpload";
+ uploadText = uploadText ?? HelpersToolkitResources.FileUpload_Upload;
+ addText = addText ?? HelpersToolkitResources.FileUpload_AddMore;
+
+
+ if (allowMoreFilesToBeAdded && !tracker.ScriptAlreadyRendered) {
+ tracker.ScriptAlreadyRendered = true;
+
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, @" <script type=""text/javascript"">
+ if (!window[""FileUploadHelper""]) window[""FileUploadHelper""] = {};
+ FileUploadHelper.addInputElement = function(index, name) {
+ var inputElem = document.createElement(""input"");
+ inputElem.type = ""file"";
+ inputElem.name = name;
+ var divElem = document.createElement(""div"");
+ divElem.appendChild(inputElem.cloneNode(false));
+ var inputs = document.getElementById(""file-upload-"" + index);
+ inputs.appendChild(divElem);
+ }
+ </script>
+");
+
+
+
+#line 83 "..\..\FileUpload.cshtml"
+ }
+
+ if (includeFormTag) {
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " ");
+
+WriteLiteralTo(@__razor_helper_writer, "<form action=\"\" enctype=\"multipart/form-data\" method=\"post\">\r\n");
+
+
+
+#line 87 "..\..\FileUpload.cshtml"
+ }
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " <div class=\"file-upload\" id=\"file-upload-");
+
+
+
+#line 88 "..\..\FileUpload.cshtml"
+ WriteTo(@__razor_helper_writer, count);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\">\r\n");
+
+
+
+#line 89 "..\..\FileUpload.cshtml"
+ for(int i = 0; i < initialNumberOfFiles; i++) {
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " <div>\r\n <input name=\"");
+
+
+
+#line 91 "..\..\FileUpload.cshtml"
+WriteTo(@__razor_helper_writer, name);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\" type=\"file\" />\r\n </div>\r\n");
+
+
+
+#line 93 "..\..\FileUpload.cshtml"
+ }
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " </div>\r\n");
+
+
+
+#line 95 "..\..\FileUpload.cshtml"
+
+ if (allowMoreFilesToBeAdded || includeFormTag) {
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " <div class=\"file-upload-buttons\">\r\n");
+
+
+
+#line 98 "..\..\FileUpload.cshtml"
+ if (allowMoreFilesToBeAdded) {
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " <a href=\"#\" onclick=\"FileUploadHelper.addInputElement(");
+
+
+
+#line 99 "..\..\FileUpload.cshtml"
+ WriteTo(@__razor_helper_writer, count);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ", ");
+
+
+
+#line 99 "..\..\FileUpload.cshtml"
+ WriteTo(@__razor_helper_writer, HttpUtility.JavaScriptStringEncode(name, addDoubleQuotes: true));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "); return false;\">");
+
+
+
+#line 99 "..\..\FileUpload.cshtml"
+ WriteTo(@__razor_helper_writer, addText);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "</a>\r\n");
+
+
+
+#line 100 "..\..\FileUpload.cshtml"
+ }
+
+#line default
+#line hidden
+
+
+
+#line 101 "..\..\FileUpload.cshtml"
+ if (includeFormTag) {
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " <input type=\"submit\" value=\"");
+
+
+
+#line 102 "..\..\FileUpload.cshtml"
+ WriteTo(@__razor_helper_writer, uploadText);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\" />\r\n");
+
+
+
+#line 103 "..\..\FileUpload.cshtml"
+ }
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " </div>\r\n");
+
+
+
+#line 105 "..\..\FileUpload.cshtml"
+ }
+
+ if (includeFormTag) {
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " ");
+
+WriteLiteralTo(@__razor_helper_writer, "</form>\r\n");
+
+
+
+#line 109 "..\..\FileUpload.cshtml"
+ }
+
+#line default
+#line hidden
+
+});
+
+}
+
+
+ public FileUpload()
+ {
+ }
+ }
+}
+#pragma warning restore 1591
diff --git a/src/Microsoft.Web.Helpers/GamerCard.cshtml b/src/Microsoft.Web.Helpers/GamerCard.cshtml
new file mode 100644
index 00000000..1ff93922
--- /dev/null
+++ b/src/Microsoft.Web.Helpers/GamerCard.cshtml
@@ -0,0 +1,10 @@
+@* Generator: WebPagesHelper *@
+
+@using Microsoft.Internal.Web.Utils
+
+@helper GetHtml(string gamerTag) {
+ if (gamerTag.IsEmpty()) {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "gamerTag");
+ }
+ <iframe frameborder="0" height="140" scrolling="no" src="http://gamercard.xbox.com/@(HttpUtility.UrlPathEncode(gamerTag)).card" width="204">@gamerTag</iframe>
+} \ No newline at end of file
diff --git a/src/Microsoft.Web.Helpers/GamerCard.generated.cs b/src/Microsoft.Web.Helpers/GamerCard.generated.cs
new file mode 100644
index 00000000..8c8a524e
--- /dev/null
+++ b/src/Microsoft.Web.Helpers/GamerCard.generated.cs
@@ -0,0 +1,90 @@
+#pragma warning disable 1591
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.235
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Microsoft.Web.Helpers
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+ using System.Net;
+ using System.Text;
+ using System.Web;
+ using System.Web.Helpers;
+ using System.Web.Security;
+ using System.Web.UI;
+ using System.Web.WebPages;
+ using System.Web.WebPages.Html;
+
+ #line 3 "..\..\GamerCard.cshtml"
+ using Microsoft.Internal.Web.Utils;
+
+ #line default
+ #line hidden
+
+ public class GamerCard : System.Web.WebPages.HelperPage
+ {
+
+public static System.Web.WebPages.HelperResult GetHtml(string gamerTag) {
+return new System.Web.WebPages.HelperResult(__razor_helper_writer => {
+
+
+
+#line 5 "..\..\GamerCard.cshtml"
+
+ if (gamerTag.IsEmpty()) {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "gamerTag");
+ }
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " <iframe frameborder=\"0\" height=\"140\" scrolling=\"no\" src=\"http://gamercard.xbo" +
+"x.com/");
+
+
+
+#line 9 "..\..\GamerCard.cshtml"
+ WriteTo(@__razor_helper_writer, HttpUtility.UrlPathEncode(gamerTag));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ".card\" width=\"204\">");
+
+
+
+#line 9 "..\..\GamerCard.cshtml"
+ WriteTo(@__razor_helper_writer, gamerTag);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "</iframe>\r\n");
+
+
+
+#line 10 "..\..\GamerCard.cshtml"
+
+#line default
+#line hidden
+
+});
+
+}
+
+
+ public GamerCard()
+ {
+ }
+ }
+}
+#pragma warning restore 1591
diff --git a/src/Microsoft.Web.Helpers/GlobalSuppressions.cs b/src/Microsoft.Web.Helpers/GlobalSuppressions.cs
new file mode 100644
index 00000000..0f982c2b
--- /dev/null
+++ b/src/Microsoft.Web.Helpers/GlobalSuppressions.cs
@@ -0,0 +1,68 @@
+using System.Diagnostics.CodeAnalysis;
+
+[assembly: SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Scope = "member", Target = "Microsoft.Web.Helpers.Facebook.#GetInitializationScripts()", Justification = "It is analogous to the get pattern users are familiar with")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Scope = "member", Target = "Microsoft.Web.Helpers.Facebook.#GetFacebookUserProfile()", Justification = "It is analogous to the get pattern users are familiar with")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Scope = "type", Target = "Microsoft.Web.Helpers.Maps+MapLocation",
+ Justification = "This type is only meant to be consumed but not instantiated by users.")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1053:StaticHolderTypesShouldNotHaveConstructors", Scope = "type", Target = "Microsoft.Web.Helpers.Analytics", Justification = "This is the default format in which helpers are generated.")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1053:StaticHolderTypesShouldNotHaveConstructors", Scope = "type", Target = "Microsoft.Web.Helpers.Bing", Justification = "This is the default format in which helpers are generated.")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1053:StaticHolderTypesShouldNotHaveConstructors", Scope = "type", Target = "Microsoft.Web.Helpers.Facebook", Justification = "This is the default format in which helpers are generated.")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1053:StaticHolderTypesShouldNotHaveConstructors", Scope = "type", Target = "Microsoft.Web.Helpers.FileUpload", Justification = "This is the default format in which helpers are generated.")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1053:StaticHolderTypesShouldNotHaveConstructors", Scope = "type", Target = "Microsoft.Web.Helpers.GamerCard", Justification = "This is the default format in which helpers are generated.")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1053:StaticHolderTypesShouldNotHaveConstructors", Scope = "type", Target = "Microsoft.Web.Helpers.LinkShare", Justification = "This is the default format in which helpers are generated.")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1053:StaticHolderTypesShouldNotHaveConstructors", Scope = "type", Target = "Microsoft.Web.Helpers.Maps", Justification = "This is the default format in which helpers are generated.")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1053:StaticHolderTypesShouldNotHaveConstructors", Scope = "type", Target = "Microsoft.Web.Helpers.ReCaptcha", Justification = "This is the default format in which helpers are generated.")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1053:StaticHolderTypesShouldNotHaveConstructors", Scope = "type", Target = "Microsoft.Web.Helpers.Twitter", Justification = "This is the default format in which helpers are generated.")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "2#", Scope = "member", Target = "Microsoft.Web.Helpers.Facebook.#LoginButton(System.String,System.String,System.String,System.String,System.Boolean,System.String,System.String,System.Boolean,System.String)", Justification = "We prefer strings to URIs for helpers")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "4#", Scope = "member", Target = "Microsoft.Web.Helpers.Facebook.#OpenGraphRequiredProperties(System.String,System.String,System.String,System.String,System.String,System.String)", Justification = "We prefer strings to URIs for helpers")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "0#", Scope = "member", Target = "Microsoft.Web.Helpers.Facebook.#LoginButton(System.String,System.String,System.String,System.String,System.Boolean,System.String,System.String,System.Boolean,System.String)", Justification = "We prefer strings to URIs for helpers")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Scope = "member", Target = "Microsoft.Web.Helpers.Facebook.#LoginButton(System.String,System.String,System.String,System.String,System.Boolean,System.String,System.String,System.Boolean,System.String)", Justification = "We prefer strings to URIs for helpers")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Scope = "member", Target = "Microsoft.Web.Helpers.Bing.#SearchBox(System.String,System.String,System.String)", Justification = "We prefer strings to URIs for helpers")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "3#", Scope = "member", Target = "Microsoft.Web.Helpers.Facebook.#OpenGraphRequiredProperties(System.String,System.String,System.String,System.String,System.String,System.String)", Justification = "We prefer strings to URIs for helpers")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "3#", Scope = "member", Target = "Microsoft.Web.Helpers.Twitter.#TweetButton(System.String,System.String,System.String,System.String,System.String,System.String,System.String,System.String)", Justification = "We prefer strings to URIs for helpers")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "3#", Scope = "member", Target = "Microsoft.Web.Helpers.Facebook.#LiveStream(System.Int32,System.Int32,System.String,System.String,System.Boolean)", Justification = "We prefer strings to URIs for helpers")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Scope = "member", Target = "Microsoft.Web.Helpers.Bing.#SiteUrl", Justification = "We prefer strings to URIs for helpers")]
+[assembly: SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "Microsoft.Web.Helpers.Twitter.RawJS(System.String)", Scope = "member", Target = "Microsoft.Web.Helpers.Twitter.#Faves(System.String,System.Int32,System.Int32,System.String,System.String,System.String,System.String,System.String,System.String,System.String,System.Int32,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.String,System.Int32)")]
+[assembly: SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "Microsoft.Web.Helpers.Twitter.RawJS(System.String)", Scope = "member", Target = "Microsoft.Web.Helpers.Twitter.#List(System.String,System.String,System.Int32,System.Int32,System.String,System.String,System.String,System.String,System.String,System.String,System.String,System.Int32,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.String,System.Int32)", Justification = "Value is a hex color code")]
+[assembly: SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "Microsoft.Web.Helpers.Twitter.RawJS(System.String)", Scope = "member", Target = "Microsoft.Web.Helpers.Twitter.#Profile(System.String,System.Int32,System.Int32,System.String,System.String,System.String,System.String,System.String,System.Int32,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.String,System.Int32)", Justification = "Value is a hex color code")]
+[assembly: SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "Microsoft.Web.Helpers.Twitter.RawJS(System.String)", Scope = "member", Target = "Microsoft.Web.Helpers.Twitter.#Search(System.String,System.Int32,System.Int32,System.String,System.String,System.String,System.String,System.String,System.String,System.String,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.String,System.Int32)", Justification = "Value is a hex color code")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Scope = "type", Target = "Microsoft.Web.Helpers.Facebook+UserProfile", Justification = "The type is consumed but never instantiated by a user")]
+[assembly: SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Scope = "member", Target = "Microsoft.Web.Helpers.Maps.#GetBingHtml(System.String,System.String,System.String,System.String,System.String,System.String,System.Int32,System.String,System.Boolean,System.String,System.Collections.Generic.IEnumerable`1<Microsoft.Web.Helpers.Maps+MapLocation>)", Justification = "We're printing a JSON value that needs to be lower case")]
+[assembly: SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Scope = "member", Target = "Microsoft.Web.Helpers.Twitter.#Faves(System.String,System.Int32,System.Int32,System.String,System.String,System.String,System.String,System.String,System.String,System.String,System.Int32,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.String,System.Int32)", Justification = "We're printing a JSON value that needs to be lower case")]
+[assembly: SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Scope = "member", Target = "Microsoft.Web.Helpers.Twitter.#List(System.String,System.String,System.Int32,System.Int32,System.String,System.String,System.String,System.String,System.String,System.String,System.String,System.Int32,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.String,System.Int32)", Justification = "We're printing a JSON value that needs to be lower case")]
+[assembly: SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Scope = "member", Target = "Microsoft.Web.Helpers.Twitter.#Profile(System.String,System.Int32,System.Int32,System.String,System.String,System.String,System.String,System.String,System.Int32,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.String,System.Int32)", Justification = "We're printing a JSON value that needs to be lower case")]
+[assembly: SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Scope = "member", Target = "Microsoft.Web.Helpers.Twitter.#Search(System.String,System.Int32,System.Int32,System.String,System.String,System.String,System.String,System.String,System.String,System.String,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.String,System.Int32)", Justification = "We're printing a JSON value that needs to be lower case")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Facepile", Scope = "member", Target = "Microsoft.Web.Helpers.Facebook.#Facepile(System.Int32,System.Int32)", Justification = "Facebook related term")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Faves", Scope = "member", Target = "Microsoft.Web.Helpers.Twitter.#Faves(System.String,System.Int32,System.Int32,System.String,System.String,System.String,System.String,System.String,System.String,System.String,System.Int32,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.String,System.Int32)", Justification = "Facebook related term")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Fbml", Scope = "member", Target = "Microsoft.Web.Helpers.Facebook.#FbmlNamespaces()", Justification = "Facebook related term")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "num", Scope = "member", Target = "Microsoft.Web.Helpers.Facebook.#Comments(System.String,System.Int32,System.Int32,System.Boolean,System.Boolean)", Justification = "num is not Hungarian notation")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "xid", Scope = "member", Target = "Microsoft.Web.Helpers.Facebook.#Comments(System.String,System.Int32,System.Int32,System.Boolean,System.Boolean)", Justification = "Facebook related term")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "xid", Scope = "member", Target = "Microsoft.Web.Helpers.Facebook.#LiveStream(System.Int32,System.Int32,System.String,System.String,System.Boolean)", Justification = "Facebook related term")]
+[assembly: SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Scope = "member", Target = "Microsoft.Web.Helpers.Twitter.#TweetButton(System.String,System.String,System.String,System.String,System.String,System.String,System.String,System.String)")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Re", Scope = "type", Target = "Microsoft.Web.Helpers.ReCaptcha")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1719:ParameterNamesShouldNotMatchMemberNames", MessageId = "1#", Scope = "member", Target = "Microsoft.Web.Helpers.Twitter.#List(System.String,System.String,System.Int32,System.Int32,System.String,System.String,System.String,System.String,System.String,System.String,System.String,System.Int32,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.String,System.Int32)", Justification = "The parameter corresponds a Twitter API parameter")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1726:UsePreferredTerms", MessageId = "Logout", Scope = "member", Target = "Microsoft.Web.Helpers.Facebook.#LoginButton(System.String,System.String,System.String,System.String,System.Boolean,System.String,System.String,System.Boolean,System.String)", Justification = "We use login and logout in WebMatrix.Security")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1726:UsePreferredTerms", MessageId = "Login", Scope = "member", Target = "Microsoft.Web.Helpers.Facebook.#LoginButtonTagOnly(System.String,System.Boolean,System.String,System.String,System.String,System.Boolean,System.String)", Justification = "We use login and logout in WebMatrix.Security")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1726:UsePreferredTerms", MessageId = "Logout", Scope = "member", Target = "Microsoft.Web.Helpers.Facebook.#LoginButtonTagOnly(System.String,System.Boolean,System.String,System.String,System.String,System.Boolean,System.String)", Justification = "We use login and logout in WebMatrix.Security")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1726:UsePreferredTerms", MessageId = "Login", Scope = "member", Target = "Microsoft.Web.Helpers.Facebook.#LoginButton(System.String,System.String,System.String,System.String,System.Boolean,System.String,System.String,System.Boolean,System.String)", Justification = "We use login and logout in WebMatrix.Security")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1726:UsePreferredTerms", MessageId = "Login", Scope = "member", Target = "Microsoft.Web.Helpers.Facebook.#MembershipLogin()", Justification = "We use login and logout in WebMatrix.Security")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1726:UsePreferredTerms", MessageId = "Login", Scope = "member", Target = "Microsoft.Web.Helpers.LinkShare.#BitlyLogin", Justification = "We use login and logout in WebMatrix.Security")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "ffffff", Scope = "member", Target = "Microsoft.Web.Helpers.Twitter.#Faves(System.String,System.Int32,System.Int32,System.String,System.String,System.String,System.String,System.String,System.String,System.String,System.Int32,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.String,System.Int32)", Justification = "Value is a hex color value")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "ffffff", Scope = "member", Target = "Microsoft.Web.Helpers.Twitter.#List(System.String,System.String,System.Int32,System.Int32,System.String,System.String,System.String,System.String,System.String,System.String,System.String,System.Int32,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.String,System.Int32)", Justification = "Value is a hex color value")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "ffffff", Scope = "member", Target = "Microsoft.Web.Helpers.Twitter.#Profile(System.String,System.Int32,System.Int32,System.String,System.String,System.String,System.String,System.String,System.Int32,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.String,System.Int32)", Justification = "Value is a hex color value")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "ffffff", Scope = "member", Target = "Microsoft.Web.Helpers.Twitter.#Search(System.String,System.Int32,System.Int32,System.String,System.String,System.String,System.String,System.String,System.String,System.String,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.String,System.Int32)", Justification = "Value is a hex color value")]
+[assembly: SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Scope = "member", Target = "Microsoft.Web.Helpers.Bing.#SiteUrl", Justification = "Property name is used instead of the generic term value to make it simpler to debug.")]
+[assembly: SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Scope = "member", Target = "Microsoft.Web.Helpers.GamerCard.#GetHtml(System.String)")]
+[assembly: SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Scope = "member", Target = "Microsoft.Web.Helpers.LinkShare.#GetHtml(System.String,System.String,System.String,System.String,Microsoft.Web.Helpers.LinkShareSite[])")]
+[assembly: SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Scope = "member", Target = "Microsoft.Web.Helpers.Maps.#GetBingHtml(System.String,System.String,System.String,System.String,System.String,System.String,System.Int32,System.String,System.Boolean,System.String,System.Collections.Generic.IEnumerable`1<Microsoft.Web.Helpers.Maps+MapLocation>)")]
+[assembly: SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Scope = "member", Target = "Microsoft.Web.Helpers.Maps.#GetMapQuestHtml(System.String,System.String,System.String,System.String,System.String,System.String,System.Int32,System.String,System.Boolean,System.String,System.Boolean,System.Collections.Generic.IEnumerable`1<Microsoft.Web.Helpers.Maps+MapLocation>)")]
+[assembly: SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Scope = "member", Target = "Microsoft.Web.Helpers.Maps.#GetYahooHtml(System.String,System.String,System.String,System.String,System.String,System.String,System.Int32,System.String,System.Boolean,System.String,System.Collections.Generic.IEnumerable`1<Microsoft.Web.Helpers.Maps+MapLocation>)")]
+[assembly: SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Scope = "member", Target = "Microsoft.Web.Helpers.Twitter.#Faves(System.String,System.Int32,System.Int32,System.String,System.String,System.String,System.String,System.String,System.String,System.String,System.Int32,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.String,System.Int32)")]
+[assembly: SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Scope = "member", Target = "Microsoft.Web.Helpers.Twitter.#List(System.String,System.String,System.Int32,System.Int32,System.String,System.String,System.String,System.String,System.String,System.String,System.String,System.Int32,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.String,System.Int32)")]
+[assembly: SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Scope = "member", Target = "Microsoft.Web.Helpers.Twitter.#Profile(System.String,System.Int32,System.Int32,System.String,System.String,System.String,System.String,System.String,System.Int32,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.String,System.Int32)")]
+[assembly: SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Scope = "member", Target = "Microsoft.Web.Helpers.Bing.#SiteTitle", Justification = "Property name is used instead of the generic term value to make it simpler to debug.")]
+[assembly: SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Scope = "member", Target = "Microsoft.Web.Helpers.Twitter.#Search(System.String,System.Int32,System.Int32,System.String,System.String,System.String,System.String,System.String,System.String,System.String,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.String,System.Int32)")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1707:IdentifiersShouldNotContainUnderscores", Scope = "member", Target = "Microsoft.Web.Helpers.Facebook+UserProfile.#Updated_Time", Justification = "This is serailzed from a JSON schema, so member names have to be exactly this way.")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1707:IdentifiersShouldNotContainUnderscores", Scope = "member", Target = "Microsoft.Web.Helpers.Facebook+UserProfile.#Last_Name", Justification = "This is serailzed from a JSON schema, so member names have to be exactly this way.")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1707:IdentifiersShouldNotContainUnderscores", Scope = "member", Target = "Microsoft.Web.Helpers.Facebook+UserProfile.#First_Name", Justification = "This is serailzed from a JSON schema, so member names have to be exactly this way.")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Timezone", Scope = "member", Target = "Microsoft.Web.Helpers.Facebook+UserProfile.#Timezone", Justification = "This is serailzed from a JSON schema, so member names have to be exactly this way.")]
diff --git a/src/Microsoft.Web.Helpers/Gravatar.cs b/src/Microsoft.Web.Helpers/Gravatar.cs
new file mode 100644
index 00000000..83d321be
--- /dev/null
+++ b/src/Microsoft.Web.Helpers/Gravatar.cs
@@ -0,0 +1,97 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Web;
+using System.Web.Helpers;
+using Microsoft.Internal.Web.Utils;
+using Resources;
+
+namespace Microsoft.Web.Helpers
+{
+ public static class Gravatar
+ {
+ private const string GravatarUrl = "http://www.gravatar.com/avatar/";
+
+ // review - extract conversion of anonymous object to html attributes string into separate helper
+ public static HtmlString GetHtml(string email, int imageSize = 80, string defaultImage = null,
+ GravatarRating rating = GravatarRating.Default, string imageExtension = null, object attributes = null)
+ {
+ bool altSpecified = false;
+ string url = GetUrl(email, imageSize, defaultImage, rating, imageExtension);
+ StringBuilder html = new StringBuilder(String.Format(CultureInfo.InvariantCulture, "<img src=\"{0}\" ", url));
+ if (attributes != null)
+ {
+ foreach (var p in attributes.GetType().GetProperties().OrderBy(p => p.Name))
+ {
+ if (!p.Name.Equals("src", StringComparison.OrdinalIgnoreCase))
+ {
+ object value = p.GetValue(attributes, null);
+ if (value != null)
+ {
+ string encodedValue = HttpUtility.HtmlAttributeEncode(value.ToString());
+ html.Append(String.Format(CultureInfo.InvariantCulture, "{0}=\"{1}\" ", p.Name, encodedValue));
+ }
+ if (p.Name.Equals("alt", StringComparison.OrdinalIgnoreCase))
+ {
+ altSpecified = true;
+ }
+ }
+ }
+ }
+ if (!altSpecified)
+ {
+ html.Append("alt=\"gravatar\" ");
+ }
+ html.Append("/>");
+ return new HtmlString(html.ToString());
+ }
+
+ // See: http://en.gravatar.com/site/implement/url
+ [SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings", Justification = "Strings are easier to work with for Plan9 scenario")]
+ [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "Gravatar.com requires lowercase")]
+ public static string GetUrl(string email, int imageSize = 80, string defaultImage = null,
+ GravatarRating rating = GravatarRating.Default, string imageExtension = null)
+ {
+ if (String.IsNullOrEmpty(email))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "email");
+ }
+ if ((imageSize <= 0) || (imageSize > 512))
+ {
+ throw new ArgumentException(HelpersToolkitResources.Gravatar_InvalidImageSize, "imageSize");
+ }
+
+ StringBuilder url = new StringBuilder(GravatarUrl);
+ email = email.Trim().ToLowerInvariant();
+ url.Append(Crypto.Hash(email, algorithm: "md5").ToLowerInvariant());
+
+ if (!String.IsNullOrEmpty(imageExtension))
+ {
+ if (!imageExtension.StartsWith(".", StringComparison.Ordinal))
+ {
+ url.Append('.');
+ }
+ url.Append(imageExtension);
+ }
+
+ url.Append("?s=");
+ url.Append(imageSize);
+
+ if (rating != GravatarRating.Default)
+ {
+ url.Append("&r=");
+ url.Append(rating.ToString().ToLowerInvariant());
+ }
+
+ if (!String.IsNullOrEmpty(defaultImage))
+ {
+ url.Append("&d=");
+ url.Append(HttpUtility.UrlEncode(defaultImage));
+ }
+
+ return HttpUtility.HtmlAttributeEncode(url.ToString());
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Helpers/GravatarRating.cs b/src/Microsoft.Web.Helpers/GravatarRating.cs
new file mode 100644
index 00000000..e66373d0
--- /dev/null
+++ b/src/Microsoft.Web.Helpers/GravatarRating.cs
@@ -0,0 +1,16 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace Microsoft.Web.Helpers
+{
+ public enum GravatarRating
+ {
+ Default,
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "G", Justification = "Matches the gravatar.com rating. Suppressed in source because this is a one-time occurrence")]
+ G,
+ PG,
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "R", Justification = "Matches the gravatar.com rating. Suppressed in source because this is a one-time occurrence")]
+ R,
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "X", Justification = "Matches the gravatar.com rating. Suppressed in source because this is a one-time occurrence")]
+ X
+ }
+}
diff --git a/src/Microsoft.Web.Helpers/LinkShare.cshtml b/src/Microsoft.Web.Helpers/LinkShare.cshtml
new file mode 100644
index 00000000..e9ce1078
--- /dev/null
+++ b/src/Microsoft.Web.Helpers/LinkShare.cshtml
@@ -0,0 +1,181 @@
+@* Generator: WebPagesHelper *@
+
+@using System.Globalization
+@using System.Text
+@using System.Web.WebPages.Scope
+@using Microsoft.Internal.Web.Utils
+
+@functions {
+ internal static readonly object _bitlyApiKey = new object();
+ internal static readonly object _bitlyLogin = new object();
+ private static readonly Lazy<IEnumerable<LinkShareSite>> _allSites = new Lazy<IEnumerable<LinkShareSite>>(() =>
+ from site in (LinkShareSite[])Enum.GetValues(typeof(LinkShareSite))
+ where site != LinkShareSite.All
+ select site
+ );
+
+ public static string BitlyApiKey {
+ get {
+ return ScopeStorage.CurrentScope[_bitlyApiKey] as string;
+ }
+
+ set {
+ if (value == null) {
+ throw new ArgumentNullException("value");
+ }
+ ScopeStorage.CurrentScope[_bitlyApiKey] = value;
+ }
+ }
+
+ public static string BitlyLogin {
+ get {
+ return ScopeStorage.CurrentScope[_bitlyLogin] as string;
+ }
+
+ set {
+ if (value == null) {
+ throw new ArgumentNullException("value");
+ }
+ ScopeStorage.CurrentScope[_bitlyLogin] = value;
+ }
+ }
+
+ private static string GetShortenedUrl(string pageLinkBack) {
+ if (BitlyLogin.IsEmpty() || BitlyApiKey.IsEmpty()) {
+ return pageLinkBack;
+ }
+ string encodedPageLinkBack = HttpUtility.UrlEncode(pageLinkBack);
+ string key = "Bitly_pageLinkBack_" + BitlyApiKey + "_" + encodedPageLinkBack;
+ string shortUrl = WebCache.Get(key) as string;
+ if (shortUrl != null) {
+ return shortUrl;
+ }
+
+ string bitlyReq = "http://api.bit.ly/v3/shorten?format=txt&longUrl=" + encodedPageLinkBack + "&login=" + BitlyLogin + "&apiKey=" + BitlyApiKey;
+ try {
+ shortUrl = GetWebResponse(bitlyReq);
+ }
+ catch (WebException) {
+ return pageLinkBack;
+ }
+ if (shortUrl != null) {
+ WebCache.Set(key, shortUrl);
+ return shortUrl;
+ }
+ return pageLinkBack;
+ }
+
+ private static string GetWebResponse(string address) {
+ WebRequest request = WebRequest.Create(address);
+ request.Method = "GET";
+ request.Timeout = 5 * 1000; //5 seconds
+ using (var response = (HttpWebResponse)request.GetResponse()) {
+ if (response.StatusCode != HttpStatusCode.OK) {
+ return null;
+ }
+ using (Stream stream = response.GetResponseStream()) {
+ using (MemoryStream memStream = new MemoryStream()) {
+ stream.CopyTo(memStream);
+ // Review: Should we use the ContentEncoding from response?
+ return Encoding.UTF8.GetString(memStream.ToArray());
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Returns an ordered list of LinkShareSite based on position of "All" parameter occurs in the list.
+ /// </summary>
+ /// <remarks>
+ /// The LinkShareSite is accepted as a params array.
+ /// In the event that no value is provided or the LinkShareSite.All is the first param, we display all the sites in the order they appear in the enum.
+ /// If not, the items we look for the first occurence of LinkShareSite.All in the array.
+ /// The items that appear before this appear in the order they are specified. The All is replaced by all items in the enum that were not already specified by the user
+ /// in the order they appear in the enum.
+ /// e.g. sites = [] { Twitter, Facebook, Digg, All }
+ /// Would result in returning {Twitter, Facebook, Digg, Delicious, GoogleBuzz, Reddit, StumbleUpon}
+ /// </remarks>
+ internal static IEnumerable<LinkShareSite> GetSitesInOrder(LinkShareSite[] linkSites) {
+ var allSites = _allSites.Value;
+ if (linkSites == null || !linkSites.Any() || linkSites.First() == LinkShareSite.All) {
+ // Show all sites
+ return allSites;
+ }
+ var result = linkSites.TakeWhile(c => c != LinkShareSite.All).ToList();
+ if (result.Count != linkSites.Length) {
+ return Enumerable.Concat(result, allSites.Except(result));
+ }
+ else {
+ return result;
+ }
+ }
+
+ private static void ConstructPageLinkBack(ref string pageLinkBack, out string shortenedUrl) {
+ HttpContext context = HttpContext.Current;
+ if ((pageLinkBack == null) && (context != null)) {
+ pageLinkBack = context.Request.Url.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.Unescaped);
+ }
+ shortenedUrl = GetShortenedUrl(pageLinkBack);
+ }
+}
+
+@helper GetHtml(string pageTitle,
+ string pageLinkBack = null,
+ string twitterUserName = null,
+ string additionalTweetText = null,
+ params LinkShareSite[] linkSites) {
+
+ if (pageTitle.IsEmpty()) {
+ throw new ArgumentException(String.Format(CultureInfo.CurrentUICulture, CommonResources.Argument_Cannot_Be_Null_Or_Empty, "pageTitle"), "pageTitle");
+ }
+
+ string shortenedUrl;
+ ConstructPageLinkBack(ref pageLinkBack, out shortenedUrl);
+
+ pageLinkBack = HttpUtility.UrlEncode(pageLinkBack);
+ shortenedUrl = HttpUtility.UrlEncode(shortenedUrl);
+ pageTitle = HttpUtility.UrlEncode(pageTitle);
+
+ foreach (var site in GetSitesInOrder(linkSites)) {
+ switch (site) {
+ case LinkShareSite.Delicious:
+ <a href="http://delicious.com/save?v=5&amp;noui&amp;jump=close&amp;url=@(shortenedUrl)&amp;title=@(pageTitle)" target="_blank" title="Add to del.icio.us">
+ <img alt="Add to del.icio.us" src="http://www.delicious.com/static/img/delicious.small.gif" style="border:0; height:16px; width:16px; margin:0 1px;" title="Add to del.icio.us" /></a>
+ break;
+
+ case LinkShareSite.Digg:
+ <a href="http://digg.com/submit?url=@(pageLinkBack)&amp;title=@(pageTitle)" target="_blank" title="Digg!">
+ <img alt="Digg!" src="http://digg.com/img/badges/16x16-digg-guy.gif" style="border:0; height:16px; width:16px; margin:0 1px;" title="Digg!" /></a>
+ break;
+ case LinkShareSite.Facebook:
+ <a href="http://www.facebook.com/sharer.php?u=@(shortenedUrl)&amp;t=@(pageTitle)" target="_blank" title="Share on Facebook">
+ <img alt="Share on Facebook" src="http://www.facebook.com/favicon.ico" style="border:0; height:16px; width:16px; margin:0 1px;" title="Share on Facebook" /></a>
+ break;
+ case LinkShareSite.Reddit:
+ <a href="http://reddit.com/submit?url=@(pageLinkBack)&amp;title=@(pageTitle)" target="_blank" title="Reddit!">
+ <img alt="Reddit!" src="http://www.Reddit.com/favicon.ico" style="border:0; height:16px; width:16px; margin:0 1px;" title="Reddit!" /></a>
+ break;
+ case LinkShareSite.StumbleUpon:
+ <a href="http://www.stumbleupon.com/submit?url=@(pageLinkBack)&amp;title=@(pageTitle)" target="_blank" title="Stumble it!">
+ <img alt="Stumble it!" src="http://cdn.stumble-upon.com/images/16x16_su_round.gif" style="border:0; height:16px; width:16px; margin:0 1px;" title="Stumble it!" /></a>
+ break;
+ case LinkShareSite.GoogleBuzz:
+ <a href="http://www.google.com/reader/link?url=@(shortenedUrl)&amp;title=@(pageTitle)" target="_blank" title="Share on Google Buzz">
+ <img alt="Share on Google Buzz" src="http://mail.google.com/mail/pimages/2/up/buzz-icon2.png" style="border:0; height:16px; width:16px; margin:0 1px;" title="Share on Google Buzz" /></a>
+ break;
+ case LinkShareSite.Twitter:
+ string status = String.Empty;
+ if (!twitterUserName.IsEmpty()) {
+ status += ", (via @@" + twitterUserName + ")";
+ }
+ if (!additionalTweetText.IsEmpty()) {
+ status += ' ' + additionalTweetText;
+ }
+ status = HttpUtility.UrlEncode(status);
+ <a href="http://twitter.com/home/?status=@(pageTitle)%3a+@(shortenedUrl)@(status)" target="_blank" title="Share on Twitter">
+ <img alt="Share on Twitter" src="http://twitter.com/favicon.ico" style="border:0; height:16px; width:16px; margin:0 1px;" title="Share on Twitter" />
+ </a>
+ break;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Microsoft.Web.Helpers/LinkShare.generated.cs b/src/Microsoft.Web.Helpers/LinkShare.generated.cs
new file mode 100644
index 00000000..96b5b443
--- /dev/null
+++ b/src/Microsoft.Web.Helpers/LinkShare.generated.cs
@@ -0,0 +1,461 @@
+#pragma warning disable 1591
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.239
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Microsoft.Web.Helpers
+{
+ using System;
+ using System.Collections.Generic;
+
+ #line 3 "..\..\LinkShare.cshtml"
+ using System.Globalization;
+
+ #line default
+ #line hidden
+ using System.IO;
+ using System.Linq;
+ using System.Net;
+
+ #line 4 "..\..\LinkShare.cshtml"
+ using System.Text;
+
+ #line default
+ #line hidden
+ using System.Web;
+ using System.Web.Helpers;
+ using System.Web.Security;
+ using System.Web.UI;
+ using System.Web.WebPages;
+ using System.Web.WebPages.Html;
+
+ #line 5 "..\..\LinkShare.cshtml"
+ using System.Web.WebPages.Scope;
+
+ #line default
+ #line hidden
+
+ #line 6 "..\..\LinkShare.cshtml"
+ using Microsoft.Internal.Web.Utils;
+
+ #line default
+ #line hidden
+
+ public class LinkShare : System.Web.WebPages.HelperPage
+ {
+
+ #line 8 "..\..\LinkShare.cshtml"
+
+ internal static readonly object _bitlyApiKey = new object();
+ internal static readonly object _bitlyLogin = new object();
+ private static readonly Lazy<IEnumerable<LinkShareSite>> _allSites = new Lazy<IEnumerable<LinkShareSite>>(() =>
+ from site in (LinkShareSite[])Enum.GetValues(typeof(LinkShareSite))
+ where site != LinkShareSite.All
+ select site
+ );
+
+ public static string BitlyApiKey {
+ get {
+ return ScopeStorage.CurrentScope[_bitlyApiKey] as string;
+ }
+
+ set {
+ if (value == null) {
+ throw new ArgumentNullException("value");
+ }
+ ScopeStorage.CurrentScope[_bitlyApiKey] = value;
+ }
+ }
+
+ public static string BitlyLogin {
+ get {
+ return ScopeStorage.CurrentScope[_bitlyLogin] as string;
+ }
+
+ set {
+ if (value == null) {
+ throw new ArgumentNullException("value");
+ }
+ ScopeStorage.CurrentScope[_bitlyLogin] = value;
+ }
+ }
+
+ private static string GetShortenedUrl(string pageLinkBack) {
+ if (BitlyLogin.IsEmpty() || BitlyApiKey.IsEmpty()) {
+ return pageLinkBack;
+ }
+ string encodedPageLinkBack = HttpUtility.UrlEncode(pageLinkBack);
+ string key = "Bitly_pageLinkBack_" + BitlyApiKey + "_" + encodedPageLinkBack;
+ string shortUrl = WebCache.Get(key) as string;
+ if (shortUrl != null) {
+ return shortUrl;
+ }
+
+ string bitlyReq = "http://api.bit.ly/v3/shorten?format=txt&longUrl=" + encodedPageLinkBack + "&login=" + BitlyLogin + "&apiKey=" + BitlyApiKey;
+ try {
+ shortUrl = GetWebResponse(bitlyReq);
+ }
+ catch (WebException) {
+ return pageLinkBack;
+ }
+ if (shortUrl != null) {
+ WebCache.Set(key, shortUrl);
+ return shortUrl;
+ }
+ return pageLinkBack;
+ }
+
+ private static string GetWebResponse(string address) {
+ WebRequest request = WebRequest.Create(address);
+ request.Method = "GET";
+ request.Timeout = 5 * 1000; //5 seconds
+ using (var response = (HttpWebResponse)request.GetResponse()) {
+ if (response.StatusCode != HttpStatusCode.OK) {
+ return null;
+ }
+ using (Stream stream = response.GetResponseStream()) {
+ using (MemoryStream memStream = new MemoryStream()) {
+ stream.CopyTo(memStream);
+ // Review: Should we use the ContentEncoding from response?
+ return Encoding.UTF8.GetString(memStream.ToArray());
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Returns an ordered list of LinkShareSite based on position of "All" parameter occurs in the list.
+ /// </summary>
+ /// <remarks>
+ /// The LinkShareSite is accepted as a params array.
+ /// In the event that no value is provided or the LinkShareSite.All is the first param, we display all the sites in the order they appear in the enum.
+ /// If not, the items we look for the first occurence of LinkShareSite.All in the array.
+ /// The items that appear before this appear in the order they are specified. The All is replaced by all items in the enum that were not already specified by the user
+ /// in the order they appear in the enum.
+ /// e.g. sites = [] { Twitter, Facebook, Digg, All }
+ /// Would result in returning {Twitter, Facebook, Digg, Delicious, GoogleBuzz, Reddit, StumbleUpon}
+ /// </remarks>
+ internal static IEnumerable<LinkShareSite> GetSitesInOrder(LinkShareSite[] linkSites) {
+ var allSites = _allSites.Value;
+ if (linkSites == null || !linkSites.Any() || linkSites.First() == LinkShareSite.All) {
+ // Show all sites
+ return allSites;
+ }
+ var result = linkSites.TakeWhile(c => c != LinkShareSite.All).ToList();
+ if (result.Count != linkSites.Length) {
+ return Enumerable.Concat(result, allSites.Except(result));
+ }
+ else {
+ return result;
+ }
+ }
+
+ private static void ConstructPageLinkBack(ref string pageLinkBack, out string shortenedUrl) {
+ HttpContext context = HttpContext.Current;
+ if ((pageLinkBack == null) && (context != null)) {
+ pageLinkBack = context.Request.Url.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.Unescaped);
+ }
+ shortenedUrl = GetShortenedUrl(pageLinkBack);
+ }
+
+ #line default
+ #line hidden
+
+public static System.Web.WebPages.HelperResult GetHtml(string pageTitle,
+ string pageLinkBack = null,
+ string twitterUserName = null,
+ string additionalTweetText = null,
+ params LinkShareSite[] linkSites) {
+return new System.Web.WebPages.HelperResult(__razor_helper_writer => {
+
+
+
+#line 126 "..\..\LinkShare.cshtml"
+
+
+ if (pageTitle.IsEmpty()) {
+ throw new ArgumentException(String.Format(CultureInfo.CurrentUICulture, CommonResources.Argument_Cannot_Be_Null_Or_Empty, "pageTitle"), "pageTitle");
+ }
+
+ string shortenedUrl;
+ ConstructPageLinkBack(ref pageLinkBack, out shortenedUrl);
+
+ pageLinkBack = HttpUtility.UrlEncode(pageLinkBack);
+ shortenedUrl = HttpUtility.UrlEncode(shortenedUrl);
+ pageTitle = HttpUtility.UrlEncode(pageTitle);
+
+ foreach (var site in GetSitesInOrder(linkSites)) {
+ switch (site) {
+ case LinkShareSite.Delicious:
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " <a href=\"http://delicious.com/save?v=5&amp;noui&amp;jump=close&am" +
+"p;url=");
+
+
+
+#line 142 "..\..\LinkShare.cshtml"
+ WriteTo(@__razor_helper_writer, shortenedUrl);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "&amp;title=");
+
+
+
+#line 142 "..\..\LinkShare.cshtml"
+ WriteTo(@__razor_helper_writer, pageTitle);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\" target=\"_blank\" title=\"Add to del.icio.us\">\r\n <img alt=\"Add " +
+"to del.icio.us\" src=\"http://www.delicious.com/static/img/delicious.small.gif\" st" +
+"yle=\"border:0; height:16px; width:16px; margin:0 1px;\" title=\"Add to del.icio.us" +
+"\" /></a>\r\n");
+
+
+
+#line 144 "..\..\LinkShare.cshtml"
+ break;
+
+ case LinkShareSite.Digg:
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " <a href=\"http://digg.com/submit?url=");
+
+
+
+#line 147 "..\..\LinkShare.cshtml"
+ WriteTo(@__razor_helper_writer, pageLinkBack);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "&amp;title=");
+
+
+
+#line 147 "..\..\LinkShare.cshtml"
+ WriteTo(@__razor_helper_writer, pageTitle);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\" target=\"_blank\" title=\"Digg!\">\r\n <img alt=\"Digg!\" src=\"http:" +
+"//digg.com/img/badges/16x16-digg-guy.gif\" style=\"border:0; height:16px; width:16" +
+"px; margin:0 1px;\" title=\"Digg!\" /></a>\r\n");
+
+
+
+#line 149 "..\..\LinkShare.cshtml"
+ break;
+ case LinkShareSite.Facebook:
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " <a href=\"http://www.facebook.com/sharer.php?u=");
+
+
+
+#line 151 "..\..\LinkShare.cshtml"
+ WriteTo(@__razor_helper_writer, shortenedUrl);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "&amp;t=");
+
+
+
+#line 151 "..\..\LinkShare.cshtml"
+ WriteTo(@__razor_helper_writer, pageTitle);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\" target=\"_blank\" title=\"Share on Facebook\">\r\n <img alt=\"Share" +
+" on Facebook\" src=\"http://www.facebook.com/favicon.ico\" style=\"border:0; height:" +
+"16px; width:16px; margin:0 1px;\" title=\"Share on Facebook\" /></a>\r\n");
+
+
+
+#line 153 "..\..\LinkShare.cshtml"
+ break;
+ case LinkShareSite.Reddit:
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " <a href=\"http://reddit.com/submit?url=");
+
+
+
+#line 155 "..\..\LinkShare.cshtml"
+ WriteTo(@__razor_helper_writer, pageLinkBack);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "&amp;title=");
+
+
+
+#line 155 "..\..\LinkShare.cshtml"
+ WriteTo(@__razor_helper_writer, pageTitle);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\" target=\"_blank\" title=\"Reddit!\">\r\n <img alt=\"Reddit!\" src=\"h" +
+"ttp://www.Reddit.com/favicon.ico\" style=\"border:0; height:16px; width:16px; marg" +
+"in:0 1px;\" title=\"Reddit!\" /></a>\r\n");
+
+
+
+#line 157 "..\..\LinkShare.cshtml"
+ break;
+ case LinkShareSite.StumbleUpon:
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " <a href=\"http://www.stumbleupon.com/submit?url=");
+
+
+
+#line 159 "..\..\LinkShare.cshtml"
+ WriteTo(@__razor_helper_writer, pageLinkBack);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "&amp;title=");
+
+
+
+#line 159 "..\..\LinkShare.cshtml"
+ WriteTo(@__razor_helper_writer, pageTitle);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\" target=\"_blank\" title=\"Stumble it!\">\r\n <img alt=\"Stumble it!" +
+"\" src=\"http://cdn.stumble-upon.com/images/16x16_su_round.gif\" style=\"border:0; h" +
+"eight:16px; width:16px; margin:0 1px;\" title=\"Stumble it!\" /></a>\r\n");
+
+
+
+#line 161 "..\..\LinkShare.cshtml"
+ break;
+ case LinkShareSite.GoogleBuzz:
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " <a href=\"http://www.google.com/reader/link?url=");
+
+
+
+#line 163 "..\..\LinkShare.cshtml"
+ WriteTo(@__razor_helper_writer, shortenedUrl);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "&amp;title=");
+
+
+
+#line 163 "..\..\LinkShare.cshtml"
+ WriteTo(@__razor_helper_writer, pageTitle);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, @""" target=""_blank"" title=""Share on Google Buzz"">
+ <img alt=""Share on Google Buzz"" src=""http://mail.google.com/mail/pimages/2/up/buzz-icon2.png"" style=""border:0; height:16px; width:16px; margin:0 1px;"" title=""Share on Google Buzz"" /></a>
+");
+
+
+
+#line 165 "..\..\LinkShare.cshtml"
+ break;
+ case LinkShareSite.Twitter:
+ string status = String.Empty;
+ if (!twitterUserName.IsEmpty()) {
+ status += ", (via @@" + twitterUserName + ")";
+ }
+ if (!additionalTweetText.IsEmpty()) {
+ status += ' ' + additionalTweetText;
+ }
+ status = HttpUtility.UrlEncode(status);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " <a href=\"http://twitter.com/home/?status=");
+
+
+
+#line 175 "..\..\LinkShare.cshtml"
+ WriteTo(@__razor_helper_writer, pageTitle);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "%3a+");
+
+
+
+#line 175 "..\..\LinkShare.cshtml"
+ WriteTo(@__razor_helper_writer, shortenedUrl);
+
+#line default
+#line hidden
+
+
+
+#line 175 "..\..\LinkShare.cshtml"
+ WriteTo(@__razor_helper_writer, status);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\" target=\"_blank\" title=\"Share on Twitter\">\r\n <img alt=\"Share " +
+"on Twitter\" src=\"http://twitter.com/favicon.ico\" style=\"border:0; height:16px; w" +
+"idth:16px; margin:0 1px;\" title=\"Share on Twitter\" />\r\n </a>\r\n");
+
+
+
+#line 178 "..\..\LinkShare.cshtml"
+ break;
+ }
+ }
+
+#line default
+#line hidden
+
+});
+
+}
+
+
+ public LinkShare()
+ {
+ }
+ }
+}
+#pragma warning restore 1591 \ No newline at end of file
diff --git a/src/Microsoft.Web.Helpers/LinkShareSite.cs b/src/Microsoft.Web.Helpers/LinkShareSite.cs
new file mode 100644
index 00000000..5a88608b
--- /dev/null
+++ b/src/Microsoft.Web.Helpers/LinkShareSite.cs
@@ -0,0 +1,14 @@
+namespace Microsoft.Web.Helpers
+{
+ public enum LinkShareSite
+ {
+ Delicious,
+ Digg,
+ GoogleBuzz,
+ Facebook,
+ Reddit,
+ StumbleUpon,
+ Twitter,
+ All
+ }
+}
diff --git a/src/Microsoft.Web.Helpers/Maps.cshtml b/src/Microsoft.Web.Helpers/Maps.cshtml
new file mode 100644
index 00000000..507b4ada
--- /dev/null
+++ b/src/Microsoft.Web.Helpers/Maps.cshtml
@@ -0,0 +1,385 @@
+@* Generator : WebPagesHelper *@
+
+@using System.Diagnostics
+@using System.Web.WebPages.Scope
+@using System.Web.UI.WebControls
+@using System.Globalization
+@using Microsoft.Internal.Web.Utils
+
+@functions {
+ private const string DefaultWidth = "300px";
+ private const string DefaultHeight = "300px";
+ private static readonly object _mapIdKey = new object();
+ private static readonly object _mapQuestApiKey = new object();
+ private static readonly object _bingApiKey = new object();
+ private static readonly object _yahooApiKey = new object();
+
+ public static string MapQuestApiKey {
+ get {
+ return (string)ScopeStorage.CurrentScope[_mapQuestApiKey];
+ }
+ set {
+ ScopeStorage.CurrentScope[_mapQuestApiKey] = value;
+ }
+ }
+
+ public static string YahooApiKey {
+ get {
+ return (string)ScopeStorage.CurrentScope[_yahooApiKey];
+ }
+ set {
+ ScopeStorage.CurrentScope[_yahooApiKey] = value;
+ }
+ }
+
+ public static string BingApiKey {
+ get {
+ return (string)ScopeStorage.CurrentScope[_bingApiKey];
+ }
+ set {
+ ScopeStorage.CurrentScope[_bingApiKey] = value;
+ }
+ }
+
+ private static int MapId {
+ get {
+ var value = (int?)HttpContext.Current.Items[_mapIdKey];
+ return value ?? 0;
+ }
+ set {
+ HttpContext.Current.Items[_mapIdKey] = value;
+ }
+ }
+
+ private static string GetMapElementId() {
+ return "map_" + MapId;
+ }
+
+ private static string TryParseUnit(string value, string defaultValue) {
+ if (String.IsNullOrEmpty(value)) {
+ return defaultValue;
+ }
+ try {
+ return Unit.Parse(value, CultureInfo.InvariantCulture).ToString(CultureInfo.InvariantCulture);
+ } catch (ArgumentException) {
+ return defaultValue;
+ }
+ }
+
+ private static IHtmlString RawJS(string text) {
+ return Raw(HttpUtility.JavaScriptStringEncode(text));
+ }
+
+ private static IHtmlString Raw(string text) {
+ return new HtmlString(text);
+ }
+
+ private static string GetApiKey(string apiKey, object scopeStorageKey) {
+ if (apiKey.IsEmpty()) {
+ return (string)ScopeStorage.CurrentScope[scopeStorageKey];
+ }
+ return apiKey;
+ }
+
+ public class MapLocation {
+ private readonly string _latitude;
+ private readonly string _longitude;
+ public MapLocation(string latitude, string longitude) {
+ _latitude = latitude;
+ _longitude = longitude;
+ }
+
+ public string Latitude {
+ get { return _latitude; }
+ }
+
+ public string Longitude {
+ get { return _longitude; }
+ }
+ }
+
+ internal static string GetDirectionsQuery(string location, string latitude, string longitude, Func<string, string> encoder = null) {
+ encoder = encoder ?? HttpUtility.UrlEncode;
+ Debug.Assert(!(location.IsEmpty() && latitude.IsEmpty() && longitude.IsEmpty()));
+ if (location.IsEmpty()) {
+ return encoder(latitude + "," + longitude);
+ }
+ return encoder(location);
+ }
+}
+
+@**
+Summary:
+ Generates Html to display a Map Quest map.
+Parameter:
+ key: Map Quest API key
+Parameter location:
+ Address of the location to center the map at
+Parameter latitude:
+ Latitude to center on. If both latitude and longitude are specified, location is ignored.
+Parameter longitude:
+ Longitude to center on. If both latitude and longitude are specified, location is ignored.
+Parameter width:
+ Width of the map with units such as 480px, 100% etc. Defaults to 300px.
+Parameter height:
+ Height of the map with units such as 480px, 100% etc. Defaults to 300px.
+Parameter zoom:
+ Initial zoom level of the map. Defaults to 7.
+Parameter type:
+ Map type to display. Valid values are "map", "sat" or "hyb".
+Parameter showDirectionsLink:
+ Determines if a link to get directions should be displayed when a location is specified. Defaults to true.
+Parameter directionsLinkText
+ The text for the get directions link. Defaults to "Get Directions".
+**@
+@helper GetMapQuestHtml(string key = null, string location = null, string latitude = null, string longitude = null, string width = "300px", string height = "300px", int zoom = 7, string type = "map",
+ bool showDirectionsLink = true, string directionsLinkText = "Get Directions", bool showZoomControl = true, IEnumerable<MapLocation> pushpins = null) {
+ key = GetApiKey(key, _mapQuestApiKey);
+ if (key.IsEmpty()) {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "key");
+ }
+
+ string mapElement = GetMapElementId();
+ string loc = "null"; // We want to print the value 'null' in the client
+ if (latitude != null && longitude != null) {
+ loc = String.Format(CultureInfo.InvariantCulture, "{{lat: {0}, lng: {1}}}",
+ HttpUtility.JavaScriptStringEncode(latitude, addDoubleQuotes: false), HttpUtility.JavaScriptStringEncode(longitude, addDoubleQuotes: false));
+ }
+
+ // The MapQuest key listed on their website is Url encoded to begin with.
+ <script src="http://mapquestapi.com/sdk/js/v6.0.0/mqa.toolkit.js?key=@key" type="text/javascript"></script>
+ <script type="text/javascript">
+ MQA.EventUtil.observe(window, 'load', function() {
+ var map = new MQA.TileMap(document.getElementById('@mapElement'), @zoom, @Raw(loc), '@RawJS(type)');
+ @if (showZoomControl) {
+ <text>
+ MQA.withModule('zoomcontrol3', function() {
+ map.addControl(new MQA.LargeZoomControl3(), new MQA.MapCornerPlacement(MQA.MapCorner.TOP_LEFT));
+ });
+ </text>
+ }
+ @if (!String.IsNullOrEmpty(location)) {
+ <text>
+ MQA.withModule('geocoder', function() {
+ map.geocodeAndAddLocations('@RawJS(location)');
+ });
+ </text>
+ }
+ @if (pushpins != null) {
+ foreach (var p in pushpins) {
+ @: map.addShape(new MQA.Poi({lat:@RawJS(p.Latitude),lng:@RawJS(p.Longitude)}));
+ }
+ }
+ });
+ </script>
+
+ <div id="@mapElement" style="width:@TryParseUnit(width, DefaultWidth); height:@TryParseUnit(height, DefaultHeight);">
+ </div>
+ if (showDirectionsLink) {
+ <a class="map-link" href="http://www.mapquest.com/?q=@GetDirectionsQuery(location, latitude, longitude)">@directionsLinkText</a>
+ }
+ MapId++;
+}
+
+@**
+Summary:
+ Generates Html to display Bing map.
+Parameter:
+ key: Bing Maps application key
+Parameter location:
+ Address of the location to center the map at
+Parameter latitude:
+ Latitude to center on. If both latitude and longitude are specified, location is ignored.
+Parameter longitude:
+ Longitude to center on. If both latitude and longitude are specified, location is ignored.
+Parameter width:
+ Width of the map with units such as 480px, 100% etc. Defaults to 300px.
+Parameter height:
+ Height of the map with units such as 480px, 100% etc. Defaults to 300px.
+Parameter zoom:
+ Initial zoom level of the map. Defaults to 14.
+Parameter type:
+ Map type to display. Valid values are "auto", "aerial", "birdeye", "road" and "mercator".
+Parameter showDirectionsLink:
+ Determines if a link to get directions should be displayed when a location is specified. Defaults to true.
+Parameter directionsLinkText
+ The text for the get directions link. Defaults to "Get Directions".
+**@
+@helper GetBingHtml(string key = null, string location = null, string latitude = null, string longitude = null, string width = null, string height = null, int zoom = 14, string type = "auto",
+ bool showDirectionsLink = true, string directionsLinkText = "Get Directions", IEnumerable<MapLocation> pushpins = null) {
+ key = GetApiKey(key, _bingApiKey);
+ if (key.IsEmpty()) {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "key");
+ }
+ string mapElement = GetMapElementId();
+
+ type = (type ?? "auto").ToLowerInvariant();
+
+ <script src="http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=7.0" type="text/javascript"></script>
+ <script type="text/javascript">
+ jQuery(window).load(function() {
+ var map = new Microsoft.Maps.Map(document.getElementById("@mapElement"), { credentials: '@RawJS(key)', mapTypeId: Microsoft.Maps.MapTypeId['@RawJS(type)'] });
+ @if (latitude != null && longitude != null) {
+ @: map.setView({zoom: @zoom, center: new Microsoft.Maps.Location(@RawJS(latitude), @RawJS(longitude))});
+ }
+ else if (location != null) {
+ <text>
+ map.getCredentials(function(credentials) {
+ $.ajax('http://dev.virtualearth.net/REST/v1/Locations/@RawJS(location)', {
+ data: { output: 'json', key: credentials }, dataType: 'json', jsonp: 'jsonp',
+ success: function(data) {
+ if (data && data.resourceSets && data.resourceSets.length > 0 && data.resourceSets[0].resources && data.resourceSets[0].resources.length > 0) {
+ var r = data.resourceSets[0].resources[0].point.coordinates;
+ var loc = new Microsoft.Maps.Location(r[0], r[1]);
+ map.setView({zoom: @zoom, center: loc});
+ map.entities.push(new Microsoft.Maps.Pushpin(loc, null));
+ }
+ }
+ });
+ });
+ </text>
+ }
+ @if (pushpins != null) {
+ foreach(var loc in pushpins) {
+ @: map.entities.push(new Microsoft.Maps.Pushpin(new Microsoft.Maps.Location(@RawJS(loc.Latitude), @RawJS(loc.Longitude)), null));
+ }
+ }
+ });
+ </script>
+
+ <div class="map" id="@mapElement" style="position:relative; width:@TryParseUnit(width, DefaultWidth); height:@TryParseUnit(height, DefaultHeight);">
+ </div>
+ if (showDirectionsLink) {
+ // Review: Need to figure out if the link needs to be localized.
+ <a class="map-link" href="http://www.bing.com/maps/?v=2&where1=@GetDirectionsQuery(location, latitude, longitude)">@directionsLinkText</a>
+ }
+ MapId++;
+}
+
+@**
+Summary:
+ Generates Html to display a Google map.
+Parameter location:
+ Address of the location to center the map at
+Parameter latitude:
+ Latitude to center on. If both latitude and longitude are specified, location is ignored.
+Parameter longitude:
+ Longitude to center on. If both latitude and longitude are specified, location is ignored.
+Parameter width:
+ Width of the map with units such as 480px, 100% etc. Defaults to 300px.
+Parameter height:
+ Height of the map with units such as 480px, 100% etc. Defaults to 300px.
+Parameter zoom:
+ Initial zoom level of the map. Defaults to 14.
+Parameter type:
+ Map type to display. Valid values are "ROADMAP", "HYBRID", "SATELLITE" and "TERRAIN".
+Parameter showDirectionsLink:
+ Determines if a link to get directions should be displayed when a location is specified. Defaults to true.
+Parameter directionsLinkText
+ The text for the get directions link. Defaults to "Get Directions".
+**@
+@helper GetGoogleHtml(string location = null, string latitude = null, string longitude = null, string width = null, string height = null, int zoom = 14, string type = "ROADMAP",
+ bool showDirectionsLink = true, string directionsLinkText = "Get Directions", IEnumerable<MapLocation> pushpins = null) {
+ string mapElement = GetMapElementId();
+ type = (type ?? "ROADMAP").ToUpperInvariant(); // Map types are in upper case
+
+ // Google maps does not support null centers. We'll set it to arbitrary values if they are null and only the location is provided.
+ // These locations are somewhere around Microsoft's Redmond Campus.
+ latitude = latitude ?? "47.652437";
+ longitude = longitude ?? "-122.132424";
+
+ <script src="http://maps.google.com/maps/api/js?sensor=false" type="text/javascript"></script>
+ <script type="text/javascript">
+ $(function() {
+ var map = new google.maps.Map(document.getElementById("@mapElement"), { zoom: @zoom, center: new google.maps.LatLng(@RawJS(latitude), @RawJS(longitude)), mapTypeId: google.maps.MapTypeId['@RawJS(type)'] });
+ @if (!String.IsNullOrEmpty(location)) {
+ <text>
+ new google.maps.Geocoder().geocode({address: '@RawJS(location)'}, function(response, status) {
+ if (status === google.maps.GeocoderStatus.OK) {
+ var best = response[0].geometry.location;
+ map.panTo(best);
+ new google.maps.Marker({map : map, position: best });
+ }
+ });
+ </text>
+ }
+ @if (pushpins != null) {
+ foreach(var loc in pushpins) {
+ @: new google.maps.Marker({map : map, position: new google.maps.LatLng(@RawJS(loc.Latitude), @RawJS(loc.Longitude))});
+ }
+ }
+ });
+ </script>
+
+ <div class="map" id="@mapElement" style="width:@TryParseUnit(width, DefaultWidth); height:@TryParseUnit(height, DefaultHeight);">
+ </div>
+ if (showDirectionsLink) {
+ <a class="map-link" href="http://maps.google.com/maps?q=@GetDirectionsQuery(location, latitude, longitude)">@directionsLinkText</a>
+ }
+ MapId++;
+}
+
+@**
+Summary:
+ Generates Html to display a Yahoo map.
+Parameter:
+ key: Yahoo application ID
+Parameter location:
+ Address of the location to center the map at
+Parameter latitude:
+ Latitude to center on. If both latitude and longitude are specified, location is ignored.
+Parameter longitude:
+ Longitude to center on. If both latitude and longitude are specified, location is ignored.
+Parameter width:
+ Width of the map with units such as 480px, 100% etc. Defaults to 300px.
+Parameter height:
+ Height of the map with units such as 480px, 100% etc. Defaults to 300px.
+Parameter zoom:
+ Initial zoom level of the map. Defaults to 4.
+Parameter type:
+ Map type to display. Valid values are "YAHOO_MAP_SAT", "YAHOO_MAP_HYB" and "YAHOO_MAP_REG".
+Parameter showDirectionsLink:
+ Determines if a link to get directions should be displayed when a location is specified. Defaults to true.
+Parameter directionsLinkText
+ The text for the get directions link. Defaults to "Get Directions".
+**@
+@helper GetYahooHtml(string key = null, string location = null, string latitude = null, string longitude = null, string width = null, string height = null, int zoom = 4, string type = "YAHOO_MAP_REG",
+ bool showDirectionsLink = true, string directionsLinkText = "Get Directions", IEnumerable<MapLocation> pushpins = null) {
+ key = GetApiKey(key, _yahooApiKey);
+ if (key.IsEmpty()) {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "key");
+ }
+ string mapElement = GetMapElementId();
+
+ <script src="http://api.maps.yahoo.com/ajaxymap?v=3.8&appid=@HttpUtility.UrlEncode(key)" type="text/javascript"></script>
+ <script type="text/javascript">
+ $(function() {
+ var map = new YMap(document.getElementById('@RawJS(mapElement)'));
+ map.addTypeControl();
+ map.setMapType(@RawJS(type));
+ @if (latitude != null && longitude != null) {
+ @: map.drawZoomAndCenter(new YGeoPoint(@RawJS(latitude), @RawJS(longitude)), @zoom);
+ }
+ else if (!String.IsNullOrEmpty(location)) {
+ @: map.drawZoomAndCenter('@RawJS(location)', @zoom);
+ }
+ else {
+ @: map.setZoomLevel(@zoom);
+ }
+ @if(pushpins != null) {
+ foreach (var loc in pushpins) {
+ @: map.addMarker(new YGeoPoint(@RawJS(loc.Latitude), @RawJS(loc.Longitude)));
+ }
+ }
+
+ });
+ </script>
+
+ <div id="@mapElement" style="width:@TryParseUnit(width, DefaultWidth); height:@TryParseUnit(height, DefaultHeight);">
+ </div>
+ if (showDirectionsLink) {
+ <a class="map-link" href="http://maps.yahoo.com/#q1=@GetDirectionsQuery(location, latitude, longitude, HttpUtility.UrlPathEncode)">@directionsLinkText</a>
+ }
+ MapId++;
+}
+
diff --git a/src/Microsoft.Web.Helpers/Maps.generated.cs b/src/Microsoft.Web.Helpers/Maps.generated.cs
new file mode 100644
index 00000000..c6516e28
--- /dev/null
+++ b/src/Microsoft.Web.Helpers/Maps.generated.cs
@@ -0,0 +1,761 @@
+#pragma warning disable 1591
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.239
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Microsoft.Web.Helpers
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+ using System.Net;
+ using System.Web;
+ using System.Web.Helpers;
+ using System.Web.Security;
+ using System.Web.UI;
+ using System.Web.WebPages;
+ using System.Web.WebPages.Html;
+ using System.Diagnostics;
+ using System.Web.WebPages.Scope;
+ using System.Web.UI.WebControls;
+ using System.Globalization;
+ using Microsoft.Internal.Web.Utils;
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorSingleFileGenerator", "0.6.0.0")]
+ public class Maps : System.Web.WebPages.HelperPage
+ {
+#line hidden
+
+ private const string DefaultWidth = "300px";
+ private const string DefaultHeight = "300px";
+ private static readonly object _mapIdKey = new object();
+ private static readonly object _mapQuestApiKey = new object();
+ private static readonly object _bingApiKey = new object();
+ private static readonly object _yahooApiKey = new object();
+
+ public static string MapQuestApiKey {
+ get {
+ return (string)ScopeStorage.CurrentScope[_mapQuestApiKey];
+ }
+ set {
+ ScopeStorage.CurrentScope[_mapQuestApiKey] = value;
+ }
+ }
+
+ public static string YahooApiKey {
+ get {
+ return (string)ScopeStorage.CurrentScope[_yahooApiKey];
+ }
+ set {
+ ScopeStorage.CurrentScope[_yahooApiKey] = value;
+ }
+ }
+
+ public static string BingApiKey {
+ get {
+ return (string)ScopeStorage.CurrentScope[_bingApiKey];
+ }
+ set {
+ ScopeStorage.CurrentScope[_bingApiKey] = value;
+ }
+ }
+
+ private static int MapId {
+ get {
+ var value = (int?)HttpContext.Current.Items[_mapIdKey];
+ return value ?? 0;
+ }
+ set {
+ HttpContext.Current.Items[_mapIdKey] = value;
+ }
+ }
+
+ private static string GetMapElementId() {
+ return "map_" + MapId;
+ }
+
+ private static string TryParseUnit(string value, string defaultValue) {
+ if (String.IsNullOrEmpty(value)) {
+ return defaultValue;
+ }
+ try {
+ return Unit.Parse(value, CultureInfo.InvariantCulture).ToString(CultureInfo.InvariantCulture);
+ } catch (ArgumentException) {
+ return defaultValue;
+ }
+ }
+
+ private static IHtmlString RawJS(string text) {
+ return Raw(HttpUtility.JavaScriptStringEncode(text));
+ }
+
+ private static IHtmlString Raw(string text) {
+ return new HtmlString(text);
+ }
+
+ private static string GetApiKey(string apiKey, object scopeStorageKey) {
+ if (apiKey.IsEmpty()) {
+ return (string)ScopeStorage.CurrentScope[scopeStorageKey];
+ }
+ return apiKey;
+ }
+
+ public class MapLocation {
+ private readonly string _latitude;
+ private readonly string _longitude;
+ public MapLocation(string latitude, string longitude) {
+ _latitude = latitude;
+ _longitude = longitude;
+ }
+
+ public string Latitude {
+ get { return _latitude; }
+ }
+
+ public string Longitude {
+ get { return _longitude; }
+ }
+ }
+
+ internal static string GetDirectionsQuery(string location, string latitude, string longitude, Func<string, string> encoder = null) {
+ encoder = encoder ?? HttpUtility.UrlEncode;
+ Debug.Assert(!(location.IsEmpty() && latitude.IsEmpty() && longitude.IsEmpty()));
+ if (location.IsEmpty()) {
+ return encoder(latitude + "," + longitude);
+ }
+ return encoder(location);
+ }
+#line hidden
+public static System.Web.WebPages.HelperResult GetMapQuestHtml(string key = null, string location = null, string latitude = null, string longitude = null, string width = "300px", string height = "300px", int zoom = 7, string type = "map",
+ bool showDirectionsLink = true, string directionsLinkText = "Get Directions", bool showZoomControl = true, IEnumerable<MapLocation> pushpins = null) {
+return new System.Web.WebPages.HelperResult(__razor_helper_writer => {
+
+
+
+ key = GetApiKey(key, _mapQuestApiKey);
+ if (key.IsEmpty()) {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "key");
+ }
+
+ string mapElement = GetMapElementId();
+ string loc = "null"; // We want to print the value 'null' in the client
+ if (latitude != null && longitude != null) {
+ loc = String.Format(CultureInfo.InvariantCulture, "{{lat: {0}, lng: {1}}}",
+ HttpUtility.JavaScriptStringEncode(latitude, addDoubleQuotes: false), HttpUtility.JavaScriptStringEncode(longitude, addDoubleQuotes: false));
+ }
+
+ // The MapQuest key listed on their website is Url encoded to begin with.
+
+WriteLiteralTo(@__razor_helper_writer, " <script src=\"http://mapquestapi.com/sdk/js/v6.0.0/mqa.toolkit.js?key=");
+
+
+ WriteTo(@__razor_helper_writer, key);
+
+WriteLiteralTo(@__razor_helper_writer, "\" type=\"text/javascript\"></script>\r\n");
+
+
+
+WriteLiteralTo(@__razor_helper_writer, " <script type=\"text/javascript\">\r\n MQA.EventUtil.observe(window, \'load\'" +
+", function() {\r\n var map = new MQA.TileMap(document.getElementById(\'");
+
+
+ WriteTo(@__razor_helper_writer, mapElement);
+
+WriteLiteralTo(@__razor_helper_writer, "\'), ");
+
+
+ WriteTo(@__razor_helper_writer, zoom);
+
+WriteLiteralTo(@__razor_helper_writer, ", ");
+
+
+ WriteTo(@__razor_helper_writer, Raw(loc));
+
+WriteLiteralTo(@__razor_helper_writer, ", \'");
+
+
+ WriteTo(@__razor_helper_writer, RawJS(type));
+
+WriteLiteralTo(@__razor_helper_writer, "\'); \r\n");
+
+
+ if (showZoomControl) {
+
+WriteLiteralTo(@__razor_helper_writer, " ");
+
+WriteLiteralTo(@__razor_helper_writer, "\r\n MQA.withModule(\'zoomcontrol3\', function() {\r\n\t m" +
+"ap.addControl(new MQA.LargeZoomControl3(), new MQA.MapCornerPlacement(MQA.MapCor" +
+"ner.TOP_LEFT));\r\n });\r\n ");
+
+WriteLiteralTo(@__razor_helper_writer, "\r\n");
+
+
+ }
+
+
+ if (!String.IsNullOrEmpty(location)) {
+
+WriteLiteralTo(@__razor_helper_writer, " ");
+
+WriteLiteralTo(@__razor_helper_writer, "\r\n MQA.withModule(\'geocoder\', function() {\r\n ma" +
+"p.geocodeAndAddLocations(\'");
+
+
+ WriteTo(@__razor_helper_writer, RawJS(location));
+
+WriteLiteralTo(@__razor_helper_writer, "\');\r\n });\r\n ");
+
+WriteLiteralTo(@__razor_helper_writer, "\r\n");
+
+
+ }
+
+
+ if (pushpins != null) {
+ foreach (var p in pushpins) {
+
+WriteLiteralTo(@__razor_helper_writer, " ");
+
+WriteLiteralTo(@__razor_helper_writer, " map.addShape(new MQA.Poi({lat:");
+
+
+ WriteTo(@__razor_helper_writer, RawJS(p.Latitude));
+
+WriteLiteralTo(@__razor_helper_writer, ",lng:");
+
+
+ WriteTo(@__razor_helper_writer, RawJS(p.Longitude));
+
+WriteLiteralTo(@__razor_helper_writer, "}));\r\n");
+
+
+ }
+ }
+
+WriteLiteralTo(@__razor_helper_writer, " });\r\n </script>\r\n");
+
+
+
+
+WriteLiteralTo(@__razor_helper_writer, " <div id=\"");
+
+
+WriteTo(@__razor_helper_writer, mapElement);
+
+WriteLiteralTo(@__razor_helper_writer, "\" style=\"width:");
+
+
+ WriteTo(@__razor_helper_writer, TryParseUnit(width, DefaultWidth));
+
+WriteLiteralTo(@__razor_helper_writer, "; height:");
+
+
+ WriteTo(@__razor_helper_writer, TryParseUnit(height, DefaultHeight));
+
+WriteLiteralTo(@__razor_helper_writer, ";\">\r\n </div>\r\n");
+
+
+ if (showDirectionsLink) {
+
+WriteLiteralTo(@__razor_helper_writer, " <a class=\"map-link\" href=\"http://www.mapquest.com/?q=");
+
+
+ WriteTo(@__razor_helper_writer, GetDirectionsQuery(location, latitude, longitude));
+
+WriteLiteralTo(@__razor_helper_writer, "\">");
+
+
+ WriteTo(@__razor_helper_writer, directionsLinkText);
+
+WriteLiteralTo(@__razor_helper_writer, "</a>\r\n");
+
+
+ }
+ MapId++;
+
+});
+
+}
+
+#line hidden
+public static System.Web.WebPages.HelperResult GetBingHtml(string key = null, string location = null, string latitude = null, string longitude = null, string width = null, string height = null, int zoom = 14, string type = "auto",
+ bool showDirectionsLink = true, string directionsLinkText = "Get Directions", IEnumerable<MapLocation> pushpins = null) {
+return new System.Web.WebPages.HelperResult(__razor_helper_writer => {
+
+
+
+ key = GetApiKey(key, _bingApiKey);
+ if (key.IsEmpty()) {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "key");
+ }
+ string mapElement = GetMapElementId();
+
+ type = (type ?? "auto").ToLowerInvariant();
+
+
+WriteLiteralTo(@__razor_helper_writer, " <script src=\"http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=7.0" +
+"\" type=\"text/javascript\"></script>\r\n");
+
+
+
+WriteLiteralTo(@__razor_helper_writer, " <script type=\"text/javascript\">\r\n jQuery(window).load(function() { \r\n " +
+" var map = new Microsoft.Maps.Map(document.getElementById(\"");
+
+
+ WriteTo(@__razor_helper_writer, mapElement);
+
+WriteLiteralTo(@__razor_helper_writer, "\"), { credentials: \'");
+
+
+ WriteTo(@__razor_helper_writer, RawJS(key));
+
+WriteLiteralTo(@__razor_helper_writer, "\', mapTypeId: Microsoft.Maps.MapTypeId[\'");
+
+
+ WriteTo(@__razor_helper_writer, RawJS(type));
+
+WriteLiteralTo(@__razor_helper_writer, "\'] });\r\n");
+
+
+ if (latitude != null && longitude != null) {
+
+WriteLiteralTo(@__razor_helper_writer, " ");
+
+WriteLiteralTo(@__razor_helper_writer, " map.setView({zoom: ");
+
+
+ WriteTo(@__razor_helper_writer, zoom);
+
+WriteLiteralTo(@__razor_helper_writer, ", center: new Microsoft.Maps.Location(");
+
+
+ WriteTo(@__razor_helper_writer, RawJS(latitude));
+
+WriteLiteralTo(@__razor_helper_writer, ", ");
+
+
+ WriteTo(@__razor_helper_writer, RawJS(longitude));
+
+WriteLiteralTo(@__razor_helper_writer, ")});\r\n");
+
+
+ }
+ else if (location != null) {
+
+WriteLiteralTo(@__razor_helper_writer, " ");
+
+WriteLiteralTo(@__razor_helper_writer, "\r\n map.getCredentials(function(credentials) {\r\n " +
+" $.ajax(\'http://dev.virtualearth.net/REST/v1/Locations/");
+
+
+ WriteTo(@__razor_helper_writer, RawJS(location));
+
+WriteLiteralTo(@__razor_helper_writer, @"', {
+ data: { output: 'json', key: credentials }, dataType: 'json', jsonp: 'jsonp',
+ success: function(data) {
+ if (data && data.resourceSets && data.resourceSets.length > 0 && data.resourceSets[0].resources && data.resourceSets[0].resources.length > 0) {
+ var r = data.resourceSets[0].resources[0].point.coordinates;
+ var loc = new Microsoft.Maps.Location(r[0], r[1]);
+ map.setView({zoom: ");
+
+
+ WriteTo(@__razor_helper_writer, zoom);
+
+WriteLiteralTo(@__razor_helper_writer, ", center: loc}); \r\n map.entities.push" +
+"(new Microsoft.Maps.Pushpin(loc, null));\r\n }\r\n " +
+" }\r\n });\r\n });\r\n " +
+" ");
+
+WriteLiteralTo(@__razor_helper_writer, "\r\n");
+
+
+ }
+
+
+ if (pushpins != null) {
+ foreach(var loc in pushpins) {
+
+WriteLiteralTo(@__razor_helper_writer, " ");
+
+WriteLiteralTo(@__razor_helper_writer, " map.entities.push(new Microsoft.Maps.Pushpin(new Microsoft.Maps.Location(");
+
+
+ WriteTo(@__razor_helper_writer, RawJS(loc.Latitude));
+
+WriteLiteralTo(@__razor_helper_writer, ", ");
+
+
+ WriteTo(@__razor_helper_writer, RawJS(loc.Longitude));
+
+WriteLiteralTo(@__razor_helper_writer, "), null));\r\n");
+
+
+ }
+ }
+
+WriteLiteralTo(@__razor_helper_writer, " });\r\n </script>\r\n");
+
+
+
+
+WriteLiteralTo(@__razor_helper_writer, " <div class=\"map\" id=\"");
+
+
+WriteTo(@__razor_helper_writer, mapElement);
+
+WriteLiteralTo(@__razor_helper_writer, "\" style=\"position:relative; width:");
+
+
+ WriteTo(@__razor_helper_writer, TryParseUnit(width, DefaultWidth));
+
+WriteLiteralTo(@__razor_helper_writer, "; height:");
+
+
+ WriteTo(@__razor_helper_writer, TryParseUnit(height, DefaultHeight));
+
+WriteLiteralTo(@__razor_helper_writer, ";\">\r\n </div>\r\n");
+
+
+ if (showDirectionsLink) {
+ // Review: Need to figure out if the link needs to be localized.
+
+WriteLiteralTo(@__razor_helper_writer, " <a class=\"map-link\" href=\"http://www.bing.com/maps/?v=2&where1=");
+
+
+ WriteTo(@__razor_helper_writer, GetDirectionsQuery(location, latitude, longitude));
+
+WriteLiteralTo(@__razor_helper_writer, "\">");
+
+
+ WriteTo(@__razor_helper_writer, directionsLinkText);
+
+WriteLiteralTo(@__razor_helper_writer, "</a>\r\n");
+
+
+ }
+ MapId++;
+
+});
+
+}
+
+#line hidden
+public static System.Web.WebPages.HelperResult GetGoogleHtml(string location = null, string latitude = null, string longitude = null, string width = null, string height = null, int zoom = 14, string type = "ROADMAP",
+ bool showDirectionsLink = true, string directionsLinkText = "Get Directions", IEnumerable<MapLocation> pushpins = null) {
+return new System.Web.WebPages.HelperResult(__razor_helper_writer => {
+
+
+
+ string mapElement = GetMapElementId();
+ type = (type ?? "ROADMAP").ToUpperInvariant(); // Map types are in upper case
+
+ // Google maps does not support null centers. We'll set it to arbitrary values if they are null and only the location is provided.
+ // These locations are somewhere around Microsoft's Redmond Campus.
+ latitude = latitude ?? "47.652437";
+ longitude = longitude ?? "-122.132424";
+
+
+WriteLiteralTo(@__razor_helper_writer, " <script src=\"http://maps.google.com/maps/api/js?sensor=false\" type=\"text/java" +
+"script\"></script>\r\n");
+
+
+
+WriteLiteralTo(@__razor_helper_writer, " <script type=\"text/javascript\">\r\n $(function() {\r\n var map " +
+"= new google.maps.Map(document.getElementById(\"");
+
+
+ WriteTo(@__razor_helper_writer, mapElement);
+
+WriteLiteralTo(@__razor_helper_writer, "\"), { zoom: ");
+
+
+ WriteTo(@__razor_helper_writer, zoom);
+
+WriteLiteralTo(@__razor_helper_writer, ", center: new google.maps.LatLng(");
+
+
+ WriteTo(@__razor_helper_writer, RawJS(latitude));
+
+WriteLiteralTo(@__razor_helper_writer, ", ");
+
+
+ WriteTo(@__razor_helper_writer, RawJS(longitude));
+
+WriteLiteralTo(@__razor_helper_writer, "), mapTypeId: google.maps.MapTypeId[\'");
+
+
+ WriteTo(@__razor_helper_writer, RawJS(type));
+
+WriteLiteralTo(@__razor_helper_writer, "\'] });\r\n");
+
+
+ if (!String.IsNullOrEmpty(location)) {
+
+WriteLiteralTo(@__razor_helper_writer, " ");
+
+WriteLiteralTo(@__razor_helper_writer, "\r\n new google.maps.Geocoder().geocode({address: \'");
+
+
+ WriteTo(@__razor_helper_writer, RawJS(location));
+
+WriteLiteralTo(@__razor_helper_writer, @"'}, function(response, status) {
+ if (status === google.maps.GeocoderStatus.OK) {
+ var best = response[0].geometry.location;
+ map.panTo(best);
+ new google.maps.Marker({map : map, position: best });
+ }
+ });
+ ");
+
+WriteLiteralTo(@__razor_helper_writer, "\r\n");
+
+
+ }
+
+
+ if (pushpins != null) {
+ foreach(var loc in pushpins) {
+
+WriteLiteralTo(@__razor_helper_writer, " ");
+
+WriteLiteralTo(@__razor_helper_writer, " new google.maps.Marker({map : map, position: new google.maps.LatLng(");
+
+
+ WriteTo(@__razor_helper_writer, RawJS(loc.Latitude));
+
+WriteLiteralTo(@__razor_helper_writer, ", ");
+
+
+ WriteTo(@__razor_helper_writer, RawJS(loc.Longitude));
+
+WriteLiteralTo(@__razor_helper_writer, ")});\r\n");
+
+
+ }
+ }
+
+WriteLiteralTo(@__razor_helper_writer, " });\r\n </script>\r\n");
+
+
+
+
+WriteLiteralTo(@__razor_helper_writer, " <div class=\"map\" id=\"");
+
+
+WriteTo(@__razor_helper_writer, mapElement);
+
+WriteLiteralTo(@__razor_helper_writer, "\" style=\"width:");
+
+
+ WriteTo(@__razor_helper_writer, TryParseUnit(width, DefaultWidth));
+
+WriteLiteralTo(@__razor_helper_writer, "; height:");
+
+
+ WriteTo(@__razor_helper_writer, TryParseUnit(height, DefaultHeight));
+
+WriteLiteralTo(@__razor_helper_writer, ";\">\r\n </div>\r\n");
+
+
+ if (showDirectionsLink) {
+
+WriteLiteralTo(@__razor_helper_writer, " <a class=\"map-link\" href=\"http://maps.google.com/maps?q=");
+
+
+ WriteTo(@__razor_helper_writer, GetDirectionsQuery(location, latitude, longitude));
+
+WriteLiteralTo(@__razor_helper_writer, "\">");
+
+
+ WriteTo(@__razor_helper_writer, directionsLinkText);
+
+WriteLiteralTo(@__razor_helper_writer, "</a>\r\n");
+
+
+ }
+ MapId++;
+
+});
+
+}
+
+#line hidden
+public static System.Web.WebPages.HelperResult GetYahooHtml(string key = null, string location = null, string latitude = null, string longitude = null, string width = null, string height = null, int zoom = 4, string type = "YAHOO_MAP_REG",
+ bool showDirectionsLink = true, string directionsLinkText = "Get Directions", IEnumerable<MapLocation> pushpins = null) {
+return new System.Web.WebPages.HelperResult(__razor_helper_writer => {
+
+
+
+ key = GetApiKey(key, _yahooApiKey);
+ if (key.IsEmpty()) {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "key");
+ }
+ string mapElement = GetMapElementId();
+
+
+WriteLiteralTo(@__razor_helper_writer, " <script src=\"http://api.maps.yahoo.com/ajaxymap?v=3.8&appid=");
+
+
+ WriteTo(@__razor_helper_writer, HttpUtility.UrlEncode(key));
+
+WriteLiteralTo(@__razor_helper_writer, "\" type=\"text/javascript\"></script>\r\n");
+
+
+
+WriteLiteralTo(@__razor_helper_writer, " <script type=\"text/javascript\">\r\n $(function() {\r\n var map " +
+"= new YMap(document.getElementById(\'");
+
+
+ WriteTo(@__razor_helper_writer, RawJS(mapElement));
+
+WriteLiteralTo(@__razor_helper_writer, "\')); \r\n map.addTypeControl(); \r\n map.setMapType(");
+
+
+WriteTo(@__razor_helper_writer, RawJS(type));
+
+WriteLiteralTo(@__razor_helper_writer, "); \r\n");
+
+
+ if (latitude != null && longitude != null) {
+
+WriteLiteralTo(@__razor_helper_writer, " ");
+
+WriteLiteralTo(@__razor_helper_writer, " map.drawZoomAndCenter(new YGeoPoint(");
+
+
+ WriteTo(@__razor_helper_writer, RawJS(latitude));
+
+WriteLiteralTo(@__razor_helper_writer, ", ");
+
+
+ WriteTo(@__razor_helper_writer, RawJS(longitude));
+
+WriteLiteralTo(@__razor_helper_writer, "), ");
+
+
+ WriteTo(@__razor_helper_writer, zoom);
+
+WriteLiteralTo(@__razor_helper_writer, ");\r\n");
+
+
+ }
+ else if (!String.IsNullOrEmpty(location)) {
+
+WriteLiteralTo(@__razor_helper_writer, " ");
+
+WriteLiteralTo(@__razor_helper_writer, " map.drawZoomAndCenter(\'");
+
+
+ WriteTo(@__razor_helper_writer, RawJS(location));
+
+WriteLiteralTo(@__razor_helper_writer, "\', ");
+
+
+ WriteTo(@__razor_helper_writer, zoom);
+
+WriteLiteralTo(@__razor_helper_writer, ");\r\n");
+
+
+ }
+ else {
+
+WriteLiteralTo(@__razor_helper_writer, " ");
+
+WriteLiteralTo(@__razor_helper_writer, " map.setZoomLevel(");
+
+
+ WriteTo(@__razor_helper_writer, zoom);
+
+WriteLiteralTo(@__razor_helper_writer, ");\r\n");
+
+
+ }
+
+
+ if(pushpins != null) {
+ foreach (var loc in pushpins) {
+
+WriteLiteralTo(@__razor_helper_writer, " ");
+
+WriteLiteralTo(@__razor_helper_writer, " map.addMarker(new YGeoPoint(");
+
+
+ WriteTo(@__razor_helper_writer, RawJS(loc.Latitude));
+
+WriteLiteralTo(@__razor_helper_writer, ", ");
+
+
+ WriteTo(@__razor_helper_writer, RawJS(loc.Longitude));
+
+WriteLiteralTo(@__razor_helper_writer, "));\r\n");
+
+
+ }
+ }
+
+WriteLiteralTo(@__razor_helper_writer, "\r\n });\r\n </script>\r\n");
+
+
+
+
+WriteLiteralTo(@__razor_helper_writer, " <div id=\"");
+
+
+WriteTo(@__razor_helper_writer, mapElement);
+
+WriteLiteralTo(@__razor_helper_writer, "\" style=\"width:");
+
+
+ WriteTo(@__razor_helper_writer, TryParseUnit(width, DefaultWidth));
+
+WriteLiteralTo(@__razor_helper_writer, "; height:");
+
+
+ WriteTo(@__razor_helper_writer, TryParseUnit(height, DefaultHeight));
+
+WriteLiteralTo(@__razor_helper_writer, ";\">\r\n </div>\r\n");
+
+
+ if (showDirectionsLink) {
+
+WriteLiteralTo(@__razor_helper_writer, " <a class=\"map-link\" href=\"http://maps.yahoo.com/#q1=");
+
+
+ WriteTo(@__razor_helper_writer, GetDirectionsQuery(location, latitude, longitude, HttpUtility.UrlPathEncode));
+
+WriteLiteralTo(@__razor_helper_writer, "\">");
+
+
+ WriteTo(@__razor_helper_writer, directionsLinkText);
+
+WriteLiteralTo(@__razor_helper_writer, "</a>\r\n");
+
+
+ }
+ MapId++;
+
+});
+
+}
+
+
+ public Maps()
+ {
+ }
+ protected static System.Web.HttpApplication ApplicationInstance
+ {
+ get
+ {
+ return ((System.Web.HttpApplication)(Context.ApplicationInstance));
+ }
+ }
+ }
+}
+#pragma warning restore 1591
diff --git a/src/Microsoft.Web.Helpers/Microsoft.Web.Helpers.csproj b/src/Microsoft.Web.Helpers/Microsoft.Web.Helpers.csproj
new file mode 100644
index 00000000..9ee71011
--- /dev/null
+++ b/src/Microsoft.Web.Helpers/Microsoft.Web.Helpers.csproj
@@ -0,0 +1,230 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <CodeAnalysis Condition=" '$(CodeAnalysis)' == '' ">false</CodeAnalysis>
+ <ProductVersion>9.0.30729</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{0C7CE809-0F72-4C19-8C64-D6573E4D9521}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>Microsoft.Web.Helpers</RootNamespace>
+ <AssemblyName>Microsoft.Web.Helpers</AssemblyName>
+ <FileAlignment>512</FileAlignment>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ <OutputType>Library</OutputType>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>..\..\bin\Debug\</OutputPath>
+ <DefineConstants>TRACE;DEBUG;ASPNETWEBPAGES</DefineConstants>
+ <CodeAnalysisRuleSet>..\WebHelpers.ruleset</CodeAnalysisRuleSet>
+ <DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
+ <NoWarn>1591</NoWarn>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>..\..\bin\Release\</OutputPath>
+ <DefineConstants>TRACE;ASPNETWEBPAGES</DefineConstants>
+ <CodeAnalysisRuleSet>..\WebHelpers.ruleset</CodeAnalysisRuleSet>
+ <RunCodeAnalysis>$(CodeAnalysis)</RunCodeAnalysis>
+ <DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
+ <NoWarn>1591</NoWarn>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'CodeCoverage|AnyCPU'">
+ <DebugSymbols>true</DebugSymbols>
+ <OutputPath>..\..\bin\CodeCoverage\</OutputPath>
+ <DefineConstants>TRACE;DEBUG;CODE_COVERAGE;ASPNETWEBPAGES</DefineConstants>
+ <DebugType>full</DebugType>
+ <CodeAnalysisRuleSet>..\WebHelpers.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <ItemGroup>
+ <CodeAnalysisDictionary Include="..\CodeAnalysisDictionary.xml" />
+ </ItemGroup>
+ <ItemGroup>
+ <Reference Include="Microsoft.CSharp" />
+ <Reference Include="System" />
+ <Reference Include="System.Configuration" />
+ <Reference Include="System.Web" />
+ <Reference Include="System.Web.ApplicationServices" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="..\CommonAssemblyInfo.cs">
+ <Link>Properties\CommonAssemblyInfo.cs</Link>
+ </Compile>
+ <Compile Include="..\CommonResources.Designer.cs">
+ <Link>Common\CommonResources.Designer.cs</Link>
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>CommonResources.resx</DependentUpon>
+ </Compile>
+ <Compile Include="..\TransparentCommonAssemblyInfo.cs">
+ <Link>Properties\TransparentCommonAssemblyInfo.cs</Link>
+ </Compile>
+ <Compile Include="Analytics.generated.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>Analytics.cshtml</DependentUpon>
+ </Compile>
+ <Compile Include="Bing.generated.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>Bing.cshtml</DependentUpon>
+ </Compile>
+ <Compile Include="Facebook.generated.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>Facebook.cshtml</DependentUpon>
+ </Compile>
+ <Compile Include="FileUpload.generated.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>FileUpload.cshtml</DependentUpon>
+ </Compile>
+ <Compile Include="GamerCard.generated.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>GamerCard.cshtml</DependentUpon>
+ </Compile>
+ <Compile Include="GlobalSuppressions.cs" />
+ <Compile Include="LinkShare.generated.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>LinkShare.cshtml</DependentUpon>
+ </Compile>
+ <Compile Include="Maps.generated.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>Maps.cshtml</DependentUpon>
+ </Compile>
+ <Compile Include="ReCaptcha.generated.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>ReCaptcha.cshtml</DependentUpon>
+ </Compile>
+ <Compile Include="..\GlobalSuppressions.cs">
+ <Link>Common\GlobalSuppressions.cs</Link>
+ </Compile>
+ <Compile Include="Gravatar.cs" />
+ <Compile Include="GravatarRating.cs" />
+ <Compile Include="PreApplicationStartCode.cs" />
+ <Compile Include="Resources\HelpersToolkitResources.Designer.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>HelpersToolkitResources.resx</DependentUpon>
+ </Compile>
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="Themes.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="Twitter.generated.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>Twitter.cshtml</DependentUpon>
+ </Compile>
+ <Compile Include="Video.cs" />
+ <Compile Include="VirtualPathUtilityBase.cs" />
+ <Compile Include="VirtualPathUtilityWrapper.cs" />
+ <Compile Include="UrlBuilder.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="..\CommonResources.resx">
+ <Link>Common\CommonResources.resx</Link>
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>CommonResources.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Include="Resources\HelpersToolkitResources.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>HelpersToolkitResources.Designer.cs</LastGenOutput>
+ <CustomToolNamespace>Resources</CustomToolNamespace>
+ </EmbeddedResource>
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\System.Web.Helpers\System.Web.Helpers.csproj">
+ <Project>{9B7E3740-6161-4548-833C-4BBCA43B970E}</Project>
+ <Name>System.Web.Helpers</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\System.Web.Razor\System.Web.Razor.csproj">
+ <Project>{8F18041B-9410-4C36-A9C5-067813DF5F31}</Project>
+ <Name>System.Web.Razor</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\System.Web.WebPages.Razor\System.Web.WebPages.Razor.csproj">
+ <Project>{0939B11A-FE4E-4BA1-8AD6-D97741EE314F}</Project>
+ <Name>System.Web.WebPages.Razor</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\System.Web.WebPages\System.Web.WebPages.csproj">
+ <Project>{76EFA9C5-8D7E-4FDF-B710-E20F8B6B00D2}</Project>
+ <Name>System.Web.WebPages</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\WebMatrix.Data\WebMatrix.Data.csproj">
+ <Project>{4D39BAAF-8A96-473E-AB79-C8A341885137}</Project>
+ <Name>WebMatrix.Data</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\WebMatrix.WebData\WebMatrix.WebData.csproj">
+ <Project>{55A15F40-1435-4248-A7F2-2A146BB83586}</Project>
+ <Name>WebMatrix.WebData</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="Analytics.cshtml">
+ <CustomToolNamespace>Microsoft.Web.Helpers</CustomToolNamespace>
+ <Generator>RazorGenerator</Generator>
+ <LastGenOutput>Analytics.generated.cs</LastGenOutput>
+ </None>
+ <None Include="Bing.cshtml">
+ <Generator>RazorGenerator</Generator>
+ <CustomToolNamespace>Microsoft.Web.Helpers</CustomToolNamespace>
+ <LastGenOutput>Bing.generated.cs</LastGenOutput>
+ </None>
+ <None Include="FileUpload.cshtml">
+ <CustomToolNamespace>Microsoft.Web.Helpers</CustomToolNamespace>
+ <Generator>RazorGenerator</Generator>
+ <LastGenOutput>FileUpload.generated.cs</LastGenOutput>
+ </None>
+ <None Include="GamerCard.cshtml">
+ <CustomToolNamespace>Microsoft.Web.Helpers</CustomToolNamespace>
+ <Generator>RazorGenerator</Generator>
+ <LastGenOutput>GamerCard.generated.cs</LastGenOutput>
+ </None>
+ <None Include="LinkShare.cshtml">
+ <CustomToolNamespace>Microsoft.Web.Helpers</CustomToolNamespace>
+ <Generator>RazorGenerator</Generator>
+ <LastGenOutput>LinkShare.generated.cs</LastGenOutput>
+ </None>
+ <Compile Include="LinkShareSite.cs">
+ <CustomToolNamespace>Microsoft.Web.Helpers</CustomToolNamespace>
+ </Compile>
+ <None Include="ReCaptcha.cshtml">
+ <CustomToolNamespace>Microsoft.Web.Helpers</CustomToolNamespace>
+ <Generator>RazorGenerator</Generator>
+ <LastGenOutput>ReCaptcha.generated.cs</LastGenOutput>
+ </None>
+ <None Include="Twitter.cshtml">
+ <CustomToolNamespace>Microsoft.Web.Helpers</CustomToolNamespace>
+ <Generator>RazorGenerator</Generator>
+ <LastGenOutput>Twitter.generated.cs</LastGenOutput>
+ </None>
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="Facebook.cshtml">
+ <Generator>RazorGenerator</Generator>
+ <CustomToolNamespace>Microsoft.Web.Helpers</CustomToolNamespace>
+ <LastGenOutput>Facebook.generated.cs</LastGenOutput>
+ </None>
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="Maps.cshtml">
+ <Generator>RazorGenerator</Generator>
+ <CustomToolNamespace>Microsoft.Web.Helpers</CustomToolNamespace>
+ <LastGenOutput>Maps.generated.cs</LastGenOutput>
+ </None>
+ </ItemGroup>
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+</Project> \ No newline at end of file
diff --git a/src/Microsoft.Web.Helpers/PreApplicationStartCode.cs b/src/Microsoft.Web.Helpers/PreApplicationStartCode.cs
new file mode 100644
index 00000000..ef84aeb3
--- /dev/null
+++ b/src/Microsoft.Web.Helpers/PreApplicationStartCode.cs
@@ -0,0 +1,28 @@
+using System.ComponentModel;
+using System.Web.WebPages.Razor;
+
+namespace Microsoft.Web.Helpers
+{
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static class PreApplicationStartCode
+ {
+ private static bool _startWasCalled;
+
+ public static void Start()
+ {
+ // Even though ASP.NET will only call each PreAppStart once, we sometimes internally call one PreAppStart from
+ // another PreAppStart to ensure that things get initialized in the right order. ASP.NET does not guarantee the
+ // order so we have to guard against multiple calls.
+ // All Start calls are made on same thread, so no lock needed here.
+
+ if (_startWasCalled)
+ {
+ return;
+ }
+ _startWasCalled = true;
+
+ // Auto import the Microsoft.Web.Helpers namespace to all apps that are executing.
+ WebPageRazorHost.AddGlobalImport(typeof(PreApplicationStartCode).Namespace);
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Helpers/Properties/AssemblyInfo.cs b/src/Microsoft.Web.Helpers/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..9f9a1672
--- /dev/null
+++ b/src/Microsoft.Web.Helpers/Properties/AssemblyInfo.cs
@@ -0,0 +1,13 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Web;
+using Microsoft.Web.Helpers;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+
+[assembly: AssemblyTitle("Microsoft.Web.Helpers")]
+[assembly: AssemblyDescription("")]
+[assembly: PreApplicationStartMethod(typeof(PreApplicationStartCode), "Start")]
+[assembly: InternalsVisibleTo("Microsoft.Web.Helpers.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
diff --git a/src/Microsoft.Web.Helpers/ReCaptcha.cshtml b/src/Microsoft.Web.Helpers/ReCaptcha.cshtml
new file mode 100644
index 00000000..7182fac5
--- /dev/null
+++ b/src/Microsoft.Web.Helpers/ReCaptcha.cshtml
@@ -0,0 +1,179 @@
+@* Generator: WebPagesHelper *@
+
+@using System
+@using System.Globalization
+@using System.IO
+@using System.Text
+@using System.Web.WebPages.Scope
+@using Microsoft.Internal.Web.Utils
+@using Resources
+
+@functions{
+ private const string _reCaptchaUrl = "http://www.google.com/recaptcha/api";
+ private const string _reCaptchaSecureUrl = "https://www.google.com/recaptcha/api";
+ private static readonly object _errorCodeCacheKey = new object();
+ internal static readonly object _privateKey = new object();
+ internal static readonly object _publicKey = new object();
+
+ public static string PrivateKey {
+ get {
+ return ScopeStorage.CurrentScope[_privateKey] as string;
+ }
+
+ set {
+ if (value == null) {
+ throw new ArgumentNullException("value");
+ }
+ ScopeStorage.CurrentScope[_privateKey] = value;
+ }
+ }
+
+ public static string PublicKey {
+ get {
+ return ScopeStorage.CurrentScope[_publicKey] as string;
+ }
+
+ set {
+ if (value == null) {
+ throw new ArgumentNullException("value");
+ }
+ ScopeStorage.CurrentScope[_publicKey] = value;
+ }
+ }
+
+ public static bool Validate(string privateKey = null) {
+ return Validate(HttpContext.Current == null ? null : new HttpContextWrapper(HttpContext.Current), privateKey, UrlBuilder.DefaultVirtualPathUtility);
+ }
+
+ internal static string GetLastError(HttpContextBase context) {
+ if (context.Items.Contains(_errorCodeCacheKey)) {
+ return context.Items[_errorCodeCacheKey] as string;
+ }
+ return String.Empty;
+ }
+
+ internal static bool Validate(HttpContextBase context, string privateKey, VirtualPathUtilityBase virtualPathUtility) {
+ privateKey = privateKey ?? PrivateKey;
+
+ if (String.IsNullOrEmpty(privateKey)) {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "privateKey");
+ }
+
+ SetLastError(context, String.Empty);
+ string postedBody = GetValidatePostData(context, privateKey, virtualPathUtility);
+ if (String.IsNullOrEmpty(postedBody)) {
+ return false;
+ }
+ string result = ExecuteValidateRequest(postedBody);
+ return HandleValidateResponse(context, result);
+ }
+
+ internal static string GetValidatePostData(HttpContextBase context, string privateKey, VirtualPathUtilityBase virtualPathUtility) {
+ string remoteIP = context.Request.ServerVariables["REMOTE_ADDR"];
+ if (String.IsNullOrEmpty(remoteIP)) {
+ throw new InvalidOperationException(HelpersToolkitResources.ReCaptcha_RemoteIPNotFound);
+ }
+
+ // Noscript rendering requires the user to copy and paste the challenge string to a textarea.
+ // When the challenge is invalid the recaptcha service doesn't return an error that affects
+ // UI rendering, so Validate should just return false without issuing the web request.
+ string challenge = context.Request.Form["recaptcha_challenge_field"];
+ if (String.IsNullOrEmpty(challenge)) {
+ return String.Empty;
+ }
+ string response = (context.Request.Form["recaptcha_response_field"] ?? String.Empty).Trim();
+
+ var builder = new UrlBuilder(context, virtualPathUtility, path: null, parameters: null)
+ .AddParam("privatekey", privateKey)
+ .AddParam("remoteip", context.Request.ServerVariables["REMOTE_ADDR"])
+ .AddParam("challenge", challenge)
+ .AddParam("response", response);
+
+ // Trim the leading ? and return the QueryString
+ return builder.QueryString.Substring(1);
+ }
+
+ internal static bool HandleValidateResponse(HttpContextBase context, string response) {
+ if (!String.IsNullOrEmpty(response)) {
+ string[] results = response.Split('\n');
+ if (results.Length > 0) {
+ bool rval = Convert.ToBoolean(results[0], CultureInfo.InvariantCulture);
+ if (!rval && (results.Length > 1)) {
+ SetLastError(context, results[1]);
+ }
+ return rval;
+ }
+ }
+ return false;
+ }
+
+ internal static string GetChallengeUrl(HttpContextBase httpContext, string publicKey = null, string errorCode = null) {
+ return GetUrlHelper(httpContext, "challenge", publicKey, errorCode: errorCode);
+ }
+
+ private static string ExecuteValidateRequest(string formData) {
+ WebRequest request = WebRequest.Create(_reCaptchaUrl + "/verify");
+ request.Method = "POST";
+ request.Timeout = 5000; //milliseconds
+ request.ContentType = "application/x-www-form-urlencoded";
+
+ byte[] content = Encoding.UTF8.GetBytes(formData);
+ using (Stream stream = request.GetRequestStream()) {
+ stream.Write(content, 0, content.Length);
+ }
+ using (WebResponse response = request.GetResponse()) {
+ using (StreamReader reader = new StreamReader(response.GetResponseStream())) {
+ return reader.ReadToEnd();
+ }
+ }
+ }
+
+ private static string GetUrlHelper(HttpContextBase context, string path, string publicKey, string errorCode) {
+
+ publicKey = publicKey ?? PublicKey;
+ if (String.IsNullOrEmpty(publicKey)) {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "publicKey");
+ }
+
+ var builder = new UrlBuilder(context.Request.IsSecureConnection ? _reCaptchaSecureUrl : _reCaptchaUrl);
+ builder.AddPath(path);
+ builder.AddParam("k", publicKey);
+ if (!String.IsNullOrEmpty(errorCode)) {
+ builder.AddParam("error", errorCode);
+ }
+ return builder;
+ }
+
+ private static void SetLastError(HttpContextBase context, string value) {
+ context.Items[_errorCodeCacheKey] = value;
+ }
+}
+
+@helper GetHtml(string publicKey = null, string theme = "red",
+ string language = "en", int tabIndex = 0) {
+
+ @GetHtmlWithOptions(publicKey, options: new Dictionary<string, object>() {
+ { "theme", theme }, { "lang", language }, { "tabindex", tabIndex }
+ })
+}
+
+@helper GetHtmlWithOptions(string publicKey = null, object options = null) {
+ @GetHtml(HttpContext.Current == null ? null : new HttpContextWrapper(HttpContext.Current), publicKey, options)
+}
+
+@helper GetHtml(HttpContextBase httpContext, string publicKey = null, object options = null) {
+ if (options != null) {
+ var optionJson = new HtmlString(Json.Encode(options));
+ <script type="text/javascript">
+ var RecaptchaOptions=@optionJson;
+ </script>
+ }
+ <script src="@GetChallengeUrl(httpContext, publicKey, GetLastError(httpContext))" type="text/javascript"></script>
+
+ <noscript>
+ <iframe frameborder="0" height="300px" src="@GetUrlHelper(httpContext, "noscript", publicKey, errorCode: null)" width="500px"></iframe>
+ <br /><br />
+ <textarea cols="40" name="recaptcha_challenge_field" rows="3"></textarea>
+ <input name="recaptcha_response_field" type="hidden" value="manual_challenge" />
+ </noscript>
+}
diff --git a/src/Microsoft.Web.Helpers/ReCaptcha.generated.cs b/src/Microsoft.Web.Helpers/ReCaptcha.generated.cs
new file mode 100644
index 00000000..1fc78a85
--- /dev/null
+++ b/src/Microsoft.Web.Helpers/ReCaptcha.generated.cs
@@ -0,0 +1,357 @@
+using Resources;
+
+#pragma warning disable 1591
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.235
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Microsoft.Web.Helpers
+{
+
+ #line 3 "..\..\ReCaptcha.cshtml"
+ using System;
+
+ #line default
+ #line hidden
+ using System.Collections.Generic;
+
+ #line 4 "..\..\ReCaptcha.cshtml"
+ using System.Globalization;
+
+ #line default
+ #line hidden
+
+ #line 5 "..\..\ReCaptcha.cshtml"
+ using System.IO;
+
+ #line default
+ #line hidden
+ using System.Linq;
+ using System.Net;
+
+ #line 6 "..\..\ReCaptcha.cshtml"
+ using System.Text;
+
+ #line default
+ #line hidden
+ using System.Web;
+ using System.Web.Helpers;
+ using System.Web.Security;
+ using System.Web.UI;
+ using System.Web.WebPages;
+ using System.Web.WebPages.Html;
+
+ #line 7 "..\..\ReCaptcha.cshtml"
+ using System.Web.WebPages.Scope;
+
+ #line default
+ #line hidden
+
+ #line 8 "..\..\ReCaptcha.cshtml"
+ using Microsoft.Internal.Web.Utils;
+
+ #line default
+ #line hidden
+
+ public class ReCaptcha : System.Web.WebPages.HelperPage
+ {
+
+ #line 10 "..\..\ReCaptcha.cshtml"
+
+ private const string _reCaptchaUrl = "http://www.google.com/recaptcha/api";
+ private const string _reCaptchaSecureUrl = "https://www.google.com/recaptcha/api";
+ private static readonly object _errorCodeCacheKey = new object();
+ internal static readonly object _privateKey = new object();
+ internal static readonly object _publicKey = new object();
+
+ public static string PrivateKey {
+ get {
+ return ScopeStorage.CurrentScope[_privateKey] as string;
+ }
+
+ set {
+ if (value == null) {
+ throw new ArgumentNullException("value");
+ }
+ ScopeStorage.CurrentScope[_privateKey] = value;
+ }
+ }
+
+ public static string PublicKey {
+ get {
+ return ScopeStorage.CurrentScope[_publicKey] as string;
+ }
+
+ set {
+ if (value == null) {
+ throw new ArgumentNullException("value");
+ }
+ ScopeStorage.CurrentScope[_publicKey] = value;
+ }
+ }
+
+ public static bool Validate(string privateKey = null) {
+ return Validate(HttpContext.Current == null ? null : new HttpContextWrapper(HttpContext.Current), privateKey, UrlBuilder.DefaultVirtualPathUtility);
+ }
+
+ internal static string GetLastError(HttpContextBase context) {
+ if (context.Items.Contains(_errorCodeCacheKey)) {
+ return context.Items[_errorCodeCacheKey] as string;
+ }
+ return String.Empty;
+ }
+
+ internal static bool Validate(HttpContextBase context, string privateKey, VirtualPathUtilityBase virtualPathUtility) {
+ privateKey = privateKey ?? PrivateKey;
+
+ if (String.IsNullOrEmpty(privateKey)) {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "privateKey");
+ }
+
+ SetLastError(context, String.Empty);
+ string postedBody = GetValidatePostData(context, privateKey, virtualPathUtility);
+ if (String.IsNullOrEmpty(postedBody)) {
+ return false;
+ }
+ string result = ExecuteValidateRequest(postedBody);
+ return HandleValidateResponse(context, result);
+ }
+
+ internal static string GetValidatePostData(HttpContextBase context, string privateKey, VirtualPathUtilityBase virtualPathUtility) {
+ string remoteIP = context.Request.ServerVariables["REMOTE_ADDR"];
+ if (String.IsNullOrEmpty(remoteIP)) {
+ throw new InvalidOperationException(HelpersToolkitResources.ReCaptcha_RemoteIPNotFound);
+ }
+
+ // Noscript rendering requires the user to copy and paste the challenge string to a textarea.
+ // When the challenge is invalid the recaptcha service doesn't return an error that affects
+ // UI rendering, so Validate should just return false without issuing the web request.
+ string challenge = context.Request.Form["recaptcha_challenge_field"];
+ if (String.IsNullOrEmpty(challenge)) {
+ return String.Empty;
+ }
+ string response = (context.Request.Form["recaptcha_response_field"] ?? String.Empty).Trim();
+
+ var builder = new UrlBuilder(context, virtualPathUtility, path: null, parameters: null)
+ .AddParam("privatekey", privateKey)
+ .AddParam("remoteip", context.Request.ServerVariables["REMOTE_ADDR"])
+ .AddParam("challenge", challenge)
+ .AddParam("response", response);
+
+ // Trim the leading ? and return the QueryString
+ return builder.QueryString.Substring(1);
+ }
+
+ internal static bool HandleValidateResponse(HttpContextBase context, string response) {
+ if (!String.IsNullOrEmpty(response)) {
+ string[] results = response.Split('\n');
+ if (results.Length > 0) {
+ bool rval = Convert.ToBoolean(results[0], CultureInfo.InvariantCulture);
+ if (!rval && (results.Length > 1)) {
+ SetLastError(context, results[1]);
+ }
+ return rval;
+ }
+ }
+ return false;
+ }
+
+ internal static string GetChallengeUrl(HttpContextBase httpContext, string publicKey = null, string errorCode = null) {
+ return GetUrlHelper(httpContext, "challenge", publicKey, errorCode: errorCode);
+ }
+
+ private static string ExecuteValidateRequest(string formData) {
+ WebRequest request = WebRequest.Create(_reCaptchaUrl + "/verify");
+ request.Method = "POST";
+ request.Timeout = 5000; //milliseconds
+ request.ContentType = "application/x-www-form-urlencoded";
+
+ byte[] content = Encoding.UTF8.GetBytes(formData);
+ using (Stream stream = request.GetRequestStream()) {
+ stream.Write(content, 0, content.Length);
+ }
+ using (WebResponse response = request.GetResponse()) {
+ using (StreamReader reader = new StreamReader(response.GetResponseStream())) {
+ return reader.ReadToEnd();
+ }
+ }
+ }
+
+ private static string GetUrlHelper(HttpContextBase context, string path, string publicKey, string errorCode) {
+
+ publicKey = publicKey ?? PublicKey;
+ if (String.IsNullOrEmpty(publicKey)) {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "publicKey");
+ }
+
+ var builder = new UrlBuilder(context.Request.IsSecureConnection ? _reCaptchaSecureUrl : _reCaptchaUrl);
+ builder.AddPath(path);
+ builder.AddParam("k", publicKey);
+ if (!String.IsNullOrEmpty(errorCode)) {
+ builder.AddParam("error", errorCode);
+ }
+ return builder;
+ }
+
+ private static void SetLastError(HttpContextBase context, string value) {
+ context.Items[_errorCodeCacheKey] = value;
+ }
+
+ #line default
+ #line hidden
+
+public static System.Web.WebPages.HelperResult GetHtml(string publicKey = null, string theme = "red",
+ string language = "en", int tabIndex = 0) {
+return new System.Web.WebPages.HelperResult(__razor_helper_writer => {
+
+
+
+#line 152 "..\..\ReCaptcha.cshtml"
+
+
+
+#line default
+#line hidden
+
+
+#line 154 "..\..\ReCaptcha.cshtml"
+WriteTo(@__razor_helper_writer, GetHtmlWithOptions(publicKey, options: new Dictionary<string, object>() {
+ { "theme", theme }, { "lang", language }, { "tabindex", tabIndex }
+ }));
+
+#line default
+#line hidden
+
+
+#line 156 "..\..\ReCaptcha.cshtml"
+
+
+#line default
+#line hidden
+
+});
+
+}
+
+
+public static System.Web.WebPages.HelperResult GetHtmlWithOptions(string publicKey = null, object options = null) {
+return new System.Web.WebPages.HelperResult(__razor_helper_writer => {
+
+
+
+#line 159 "..\..\ReCaptcha.cshtml"
+
+
+#line default
+#line hidden
+
+
+#line 160 "..\..\ReCaptcha.cshtml"
+WriteTo(@__razor_helper_writer, GetHtml(HttpContext.Current == null ? null : new HttpContextWrapper(HttpContext.Current), publicKey, options));
+
+#line default
+#line hidden
+
+
+#line 160 "..\..\ReCaptcha.cshtml"
+
+
+#line default
+#line hidden
+
+});
+
+}
+
+
+public static System.Web.WebPages.HelperResult GetHtml(HttpContextBase httpContext, string publicKey = null, object options = null) {
+return new System.Web.WebPages.HelperResult(__razor_helper_writer => {
+
+
+
+#line 163 "..\..\ReCaptcha.cshtml"
+
+ if (options != null) {
+ var optionJson = new HtmlString(Json.Encode(options));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " <script type=\"text/javascript\">\r\n var RecaptchaOptions=");
+
+
+
+#line 167 "..\..\ReCaptcha.cshtml"
+ WriteTo(@__razor_helper_writer, optionJson);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ";\r\n </script>\r\n");
+
+
+
+#line 169 "..\..\ReCaptcha.cshtml"
+ }
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " <script src=\"");
+
+
+
+#line 170 "..\..\ReCaptcha.cshtml"
+WriteTo(@__razor_helper_writer, GetChallengeUrl(httpContext, publicKey, GetLastError(httpContext)));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\" type=\"text/javascript\"></script>\r\n");
+
+
+
+#line 171 "..\..\ReCaptcha.cshtml"
+
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " <noscript>\r\n <iframe frameborder=\"0\" height=\"300px\" src=\"");
+
+
+
+#line 173 "..\..\ReCaptcha.cshtml"
+ WriteTo(@__razor_helper_writer, GetUrlHelper(httpContext, "noscript", publicKey, errorCode: null));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\" width=\"500px\"></iframe>\r\n <br /><br />\r\n <textarea cols=\"40\" name" +
+"=\"recaptcha_challenge_field\" rows=\"3\"></textarea>\r\n <input name=\"recaptch" +
+"a_response_field\" type=\"hidden\" value=\"manual_challenge\" />\r\n </noscript>\r\n");
+
+
+
+#line 178 "..\..\ReCaptcha.cshtml"
+
+#line default
+#line hidden
+
+});
+
+}
+
+
+ public ReCaptcha()
+ {
+ }
+ }
+}
+#pragma warning restore 1591
diff --git a/src/Microsoft.Web.Helpers/Resources/HelpersToolkitResources.Designer.cs b/src/Microsoft.Web.Helpers/Resources/HelpersToolkitResources.Designer.cs
new file mode 100644
index 00000000..d2327ca2
--- /dev/null
+++ b/src/Microsoft.Web.Helpers/Resources/HelpersToolkitResources.Designer.cs
@@ -0,0 +1,162 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.225
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Resources {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class HelpersToolkitResources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal HelpersToolkitResources() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.Web.Helpers.Resources.HelpersToolkitResources", typeof(HelpersToolkitResources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Search Site.
+ /// </summary>
+ internal static string BingSearch_DefaultSiteSearchText {
+ get {
+ return ResourceManager.GetString("BingSearch_DefaultSiteSearchText", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Search Web.
+ /// </summary>
+ internal static string BingSearch_DefaultWebSearchText {
+ get {
+ return ResourceManager.GetString("BingSearch_DefaultWebSearchText", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Add more files.
+ /// </summary>
+ internal static string FileUpload_AddMore {
+ get {
+ return ResourceManager.GetString("FileUpload_AddMore", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Upload.
+ /// </summary>
+ internal static string FileUpload_Upload {
+ get {
+ return ResourceManager.GetString("FileUpload_Upload", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The Gravatar image size must be between 1 and 512 pixels..
+ /// </summary>
+ internal static string Gravatar_InvalidImageSize {
+ get {
+ return ResourceManager.GetString("Gravatar_InvalidImageSize", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The captcha cannot be validated because the remote address was not found in the request..
+ /// </summary>
+ internal static string ReCaptcha_RemoteIPNotFound {
+ get {
+ return ResourceManager.GetString("ReCaptcha_RemoteIPNotFound", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to You cannot have a null folder. Try using Themes.GetResourcePath(string fileName) instead if you do not want to specify a folder..
+ /// </summary>
+ internal static string Themes_FolderCannotBeNull {
+ get {
+ return ResourceManager.GetString("Themes_FolderCannotBeNull", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Unknown theme &apos;{0}&apos;. Ensure that a directory labeled &apos;{0}&apos; exists under the theme directory..
+ /// </summary>
+ internal static string Themes_InvalidTheme {
+ get {
+ return ResourceManager.GetString("Themes_InvalidTheme", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to You must call the &quot;Themes.Initialize&quot; method before you call any other method of the &quot;Themes&quot; class..
+ /// </summary>
+ internal static string Themes_NotInitialized {
+ get {
+ return ResourceManager.GetString("Themes_NotInitialized", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The media file &quot;{0}&quot; does not exist..
+ /// </summary>
+ internal static string Video_FileDoesNotExist {
+ get {
+ return ResourceManager.GetString("Video_FileDoesNotExist", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Property &quot;{0}&quot; cannot be set through this argument..
+ /// </summary>
+ internal static string Video_PropertyCannotBeSet {
+ get {
+ return ResourceManager.GetString("Video_PropertyCannotBeSet", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Helpers/Resources/HelpersToolkitResources.resx b/src/Microsoft.Web.Helpers/Resources/HelpersToolkitResources.resx
new file mode 100644
index 00000000..1180a8a5
--- /dev/null
+++ b/src/Microsoft.Web.Helpers/Resources/HelpersToolkitResources.resx
@@ -0,0 +1,153 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="BingSearch_DefaultSiteSearchText" xml:space="preserve">
+ <value>Search Site</value>
+ </data>
+ <data name="BingSearch_DefaultWebSearchText" xml:space="preserve">
+ <value>Search Web</value>
+ </data>
+ <data name="FileUpload_AddMore" xml:space="preserve">
+ <value>Add more files</value>
+ </data>
+ <data name="FileUpload_Upload" xml:space="preserve">
+ <value>Upload</value>
+ </data>
+ <data name="Gravatar_InvalidImageSize" xml:space="preserve">
+ <value>The Gravatar image size must be between 1 and 512 pixels.</value>
+ </data>
+ <data name="ReCaptcha_RemoteIPNotFound" xml:space="preserve">
+ <value>The captcha cannot be validated because the remote address was not found in the request.</value>
+ </data>
+ <data name="Themes_FolderCannotBeNull" xml:space="preserve">
+ <value>You cannot have a null folder. Try using Themes.GetResourcePath(string fileName) instead if you do not want to specify a folder.</value>
+ </data>
+ <data name="Themes_InvalidTheme" xml:space="preserve">
+ <value>Unknown theme '{0}'. Ensure that a directory labeled '{0}' exists under the theme directory.</value>
+ </data>
+ <data name="Themes_NotInitialized" xml:space="preserve">
+ <value>You must call the "Themes.Initialize" method before you call any other method of the "Themes" class.</value>
+ </data>
+ <data name="Video_FileDoesNotExist" xml:space="preserve">
+ <value>The media file "{0}" does not exist.</value>
+ </data>
+ <data name="Video_PropertyCannotBeSet" xml:space="preserve">
+ <value>Property "{0}" cannot be set through this argument.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/Microsoft.Web.Helpers/Themes.cs b/src/Microsoft.Web.Helpers/Themes.cs
new file mode 100644
index 00000000..0dd73fd1
--- /dev/null
+++ b/src/Microsoft.Web.Helpers/Themes.cs
@@ -0,0 +1,283 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Web.Hosting;
+using System.Web.WebPages.Scope;
+using Microsoft.Internal.Web.Utils;
+using Resources;
+
+namespace Microsoft.Web.Helpers
+{
+ public static class Themes
+ {
+ public static string ThemeDirectory
+ {
+ get { return Implementation.ThemeDirectory; }
+ }
+
+ public static string CurrentTheme
+ {
+ get { return Implementation.CurrentTheme; }
+ set { Implementation.CurrentTheme = value; }
+ }
+
+ public static string DefaultTheme
+ {
+ get { return Implementation.DefaultTheme; }
+ }
+
+ public static ReadOnlyCollection<string> AvailableThemes
+ {
+ get { return Implementation.AvailableThemes; }
+ }
+
+ private static ThemesImplementation Implementation
+ {
+ get { return new ThemesImplementation(HostingEnvironment.VirtualPathProvider, ScopeStorage.CurrentScope); }
+ }
+
+ public static void Initialize(string themeDirectory, string defaultTheme)
+ {
+ Implementation.Initialize(themeDirectory, defaultTheme);
+ }
+
+ /// <summary>
+ /// Get a file that lives directly inside the theme directory
+ /// </summary>
+ /// <param name="fileName">The filename to look for</param>
+ /// <returns>The full path to the file that matches the requested file</returns>
+ public static string GetResourcePath(string fileName)
+ {
+ return Implementation.GetResourcePath(fileName);
+ }
+
+ public static string GetResourcePath(string folder, string fileName)
+ {
+ return Implementation.GetResourcePath(folder, fileName);
+ }
+ }
+
+ internal class ThemesImplementation
+ {
+ internal static readonly object CurrentThemeKey = new object();
+ internal static readonly object ThemeDirectoryKey = new object();
+ internal static readonly object DefaultThemeKey = new object();
+ internal static readonly object ThemesInitializedKey = new object();
+ private readonly VirtualPathProvider _vpp;
+ private readonly IDictionary<object, object> _currentScope;
+
+ public ThemesImplementation(VirtualPathProvider vpp, IDictionary<object, object> scopeStorage)
+ {
+ _vpp = vpp;
+ _currentScope = scopeStorage;
+ }
+
+ public string ThemeDirectory
+ {
+ get
+ {
+ EnsureInitialized();
+ return (string)_currentScope[ThemeDirectoryKey];
+ }
+ private set
+ {
+ Debug.Assert(value != null);
+ _currentScope[ThemeDirectoryKey] = value;
+ }
+ }
+
+ /// <summary>
+ /// This should live throughout the application life cycle
+ /// and be set in _appstart.cshtml
+ /// </summary>
+ public string DefaultTheme
+ {
+ get
+ {
+ EnsureInitialized();
+ return (string)_currentScope[DefaultThemeKey];
+ }
+ private set
+ {
+ Debug.Assert(value != null);
+ _currentScope[DefaultThemeKey] = value;
+ }
+ }
+
+ /// <summary>
+ /// The current theme to use. When this is set,
+ /// all GetResource checks will check if the CurrentTheme
+ /// contains the file, and if it doesn't it will fall back to
+ /// the DefaultTheme
+ /// </summary>
+ public string CurrentTheme
+ {
+ get
+ {
+ EnsureInitialized();
+ return (string)_currentScope[CurrentThemeKey] ?? DefaultTheme;
+ }
+ set
+ {
+ if (String.IsNullOrEmpty(value))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "value");
+ }
+
+ // EnsureValidTheme would verify if themes have been correctly initialized and that the value specified is a valid theme.
+ if (!IsValidTheme(AvailableThemes, value))
+ {
+ throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, HelpersToolkitResources.Themes_InvalidTheme, value), "value");
+ }
+ _currentScope[CurrentThemeKey] = value;
+ }
+ }
+
+ public ReadOnlyCollection<string> AvailableThemes
+ {
+ get
+ {
+ EnsureInitialized();
+ return GetAvailableThemes(ThemeDirectory);
+ }
+ }
+
+ private string CurrentThemePath
+ {
+ get { return Path.Combine(ThemeDirectory, CurrentTheme); }
+ }
+
+ private string DefaultThemePath
+ {
+ get { return Path.Combine(ThemeDirectory, DefaultTheme); }
+ }
+
+ private bool ThemesInitialized
+ {
+ get
+ {
+ bool? value = (bool?)_currentScope[ThemesInitializedKey];
+ return value != null && value.Value;
+ }
+ set { _currentScope[ThemesInitializedKey] = value; }
+ }
+
+ public void Initialize(string themeDirectory, string defaultTheme)
+ {
+ if (String.IsNullOrEmpty(themeDirectory))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "themeDirectory");
+ }
+
+ if (String.IsNullOrEmpty(defaultTheme))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "defaultTheme");
+ }
+
+ var availableThemes = GetAvailableThemes(themeDirectory);
+ if (!IsValidTheme(availableThemes, defaultTheme))
+ {
+ throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, HelpersToolkitResources.Themes_InvalidTheme, defaultTheme), "defaultTheme");
+ }
+
+ ThemeDirectory = themeDirectory;
+ DefaultTheme = defaultTheme;
+ ThemesInitialized = true;
+ }
+
+ /// <summary>
+ /// Get a file that lives directly inside the theme directory
+ /// </summary>
+ /// <param name="fileName">The filename to look for</param>
+ /// <returns>The full path to the file that matches the requested file</returns>
+ public string GetResourcePath(string fileName)
+ {
+ return GetResourcePath(String.Empty, fileName);
+ }
+
+ public string GetResourcePath(string folder, string fileName)
+ {
+ EnsureInitialized();
+
+ if (folder == null)
+ {
+ throw new ArgumentNullException("folder", HelpersToolkitResources.Themes_FolderCannotBeNull);
+ }
+
+ if (String.IsNullOrEmpty(fileName))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "fileName");
+ }
+
+ return FindMatchingFile(Path.Combine(CurrentThemePath, folder), fileName) ??
+ FindMatchingFile(Path.Combine(DefaultThemePath, folder), fileName);
+ }
+
+ /// <summary>
+ /// Try and find a file in the specified folder that matches name.
+ /// </summary>
+ /// <returns>The full path to the file that matches the requested file
+ /// or null if no matching file is found</returns>
+ internal string FindMatchingFile(string folder, string name)
+ {
+ Debug.Assert(!String.IsNullOrEmpty(folder));
+ Debug.Assert(!String.IsNullOrEmpty(name));
+
+ // Get the virtual path information
+ VirtualDirectory directory = _vpp.GetDirectory(folder);
+
+ // If the folder specified doesn't exist
+ // or it doesn't contain any files
+ if (directory == null || directory.Files == null)
+ {
+ return null;
+ }
+
+ // Go through every file in the directory
+ foreach (VirtualFile file in directory.Files)
+ {
+ string path = file.VirtualPath;
+
+ // Compare the filename to the filename that we passed
+ if (Path.GetFileName(path).Equals(name, StringComparison.OrdinalIgnoreCase))
+ {
+ return path;
+ }
+ }
+
+ // If no matching files, return null
+ return null;
+ }
+
+ private ReadOnlyCollection<string> GetAvailableThemes(string themesRoot)
+ {
+ VirtualDirectory directory = _vpp.GetDirectory(themesRoot);
+
+ var themes = new List<string>();
+
+ // Go through every file in the directory
+ foreach (VirtualDirectory dir in directory.Directories)
+ {
+ themes.Add(dir.Name);
+ }
+ return themes.AsReadOnly();
+ }
+
+ private void EnsureInitialized()
+ {
+ if (!ThemesInitialized)
+ {
+ throw new InvalidOperationException(HelpersToolkitResources.Themes_NotInitialized);
+ }
+ }
+
+ private static bool IsValidTheme(IEnumerable<string> availableThemes, string theme)
+ {
+ return availableThemes.Contains(theme, StringComparer.OrdinalIgnoreCase);
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Helpers/Twitter.cshtml b/src/Microsoft.Web.Helpers/Twitter.cshtml
new file mode 100644
index 00000000..ab9c7eb5
--- /dev/null
+++ b/src/Microsoft.Web.Helpers/Twitter.cshtml
@@ -0,0 +1,279 @@
+@* Generator: WebPagesHelper *@
+
+@using System.Globalization
+@using Microsoft.Internal.Web.Utils
+
+@helper TweetButton(
+ string dataCount = "vertical",
+ string shareText = "Tweet",
+ string tweetText = "",
+ string url = "",
+ string language = "",
+ string userName = "",
+ string relatedUserName = "",
+ string relatedUserDescription = "") {
+ var tweetTextAttribute = new HtmlString(!tweetText.IsEmpty() ? String.Format(CultureInfo.InvariantCulture, " data-text=\"{0}\"", HttpUtility.HtmlAttributeEncode(tweetText)) : "");
+ var urlAttribute = new HtmlString(!url.IsEmpty() ? String.Format(CultureInfo.InvariantCulture, " data-url=\"{0}\"", HttpUtility.HtmlAttributeEncode(url)) : "");
+ var languageAttribute = new HtmlString(!language.IsEmpty() && !language.Equals("en", StringComparison.OrdinalIgnoreCase) ? String.Format(CultureInfo.InvariantCulture, " data-lang=\"{0}\"", HttpUtility.HtmlAttributeEncode(language)) : "");
+ var userNameAttribute = new HtmlString(!userName.IsEmpty() ? String.Format(CultureInfo.InvariantCulture, " data-via=\"{0}\"", HttpUtility.HtmlAttributeEncode(userName)) : "");
+ var relatedAttribute = new HtmlString(!relatedUserName.IsEmpty() ? String.Format(CultureInfo.InvariantCulture, " data-related=\"{0}{1}\"", HttpUtility.HtmlAttributeEncode(relatedUserName),
+ !relatedUserDescription.IsEmpty() ? ":" + HttpUtility.HtmlAttributeEncode(relatedUserDescription) : "") : "");
+<a href="http://twitter.com/share" class="twitter-share-button"@tweetTextAttribute@urlAttribute@languageAttribute@userNameAttribute@relatedAttribute data-count="@dataCount.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()">@shareText</a><script type="text/javascript" src="http://platform.twitter.com/widgets.js"></script>
+}
+
+@helper FollowButton(
+ string userName,
+ string followStyle = "follow_me",
+ string followColor = "a") {
+<a href="http://www.twitter.com/@HttpUtility.UrlEncode(userName)"><img src="http://twitter-badges.s3.amazonaws.com/@(HttpUtility.UrlEncode(followStyle + '-' + followColor)).png" alt="Follow @userName on Twitter"/></a>
+}
+
+@helper Profile(
+ string userName,
+ int width = 250,
+ int height = 300,
+ string backgroundShellColor = "#333333",
+ string shellColor = "#ffffff",
+ string tweetsBackgroundColor = "#000000",
+ string tweetsColor = "#ffffff",
+ string tweetsLinksColor = "#4aed05",
+ int numberOfTweets = 4,
+ bool scrollBar = false,
+ bool loop = false,
+ bool live = false,
+ bool hashTags = true,
+ bool timestamp = true,
+ bool avatars = false,
+ string behavior = "default",
+ int interval = 6) {
+
+ if (String.IsNullOrEmpty(userName)) {
+ throw new ArgumentException(String.Format(CultureInfo.CurrentUICulture, CommonResources.Argument_Cannot_Be_Null_Or_Empty, "userName"), "userName");
+ }
+
+<script type="text/javascript" src="http://widgets.twimg.com/j/2/widget.js"></script>
+<script type="text/javascript">
+new TWTR.Widget({
+ version: 2,
+ type: 'profile',
+ rpp: @(numberOfTweets < 1 ? 1 : (numberOfTweets > 30 ? 30 : numberOfTweets)),
+ interval: @(interval < 2 ? 2000 : (interval > 20 ? 20000 : (interval * 1000))),
+ width: @(new HtmlString(width <= 0 ? "'auto'" : width.ToString(CultureInfo.InvariantCulture))),
+ height: @(height < 0 ? "0" : height.ToString(CultureInfo.InvariantCulture)),
+ theme: {
+ shell: {
+ background: '@RawJS(backgroundShellColor.IsEmpty() ? "#333333" : backgroundShellColor)',
+ color: '@RawJS(shellColor.IsEmpty() ? "#ffffff" : shellColor)'
+ },
+ tweets: {
+ background: '@RawJS(tweetsBackgroundColor.IsEmpty() ? "#000000" : tweetsBackgroundColor)',
+ color: '@RawJS(tweetsColor.IsEmpty() ? "#ffffff" : tweetsColor)',
+ links: '@RawJS(tweetsLinksColor.IsEmpty() ? "#4aed05" : tweetsLinksColor)'
+ }
+ },
+ features: {
+ scrollbar: @scrollBar.ToString(CultureInfo.InvariantCulture).ToLowerInvariant(),
+ loop: @loop.ToString(CultureInfo.InvariantCulture).ToLower(CultureInfo.InvariantCulture),
+ live: @live.ToString(CultureInfo.InvariantCulture).ToLower(CultureInfo.InvariantCulture),
+ hashtags: @hashTags.ToString(CultureInfo.InvariantCulture).ToLower(CultureInfo.InvariantCulture),
+ timestamp: @timestamp.ToString(CultureInfo.InvariantCulture).ToLowerInvariant(),
+ avatars: @avatars.ToString(CultureInfo.InvariantCulture).ToLowerInvariant(),
+ behavior: '@behavior.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()'
+ }
+}).render().setUser('@RawJS(userName)').start();
+</script>
+}
+
+@* Advanced queries: http://search.twitter.com/operators *@
+@helper Search(
+ string searchQuery,
+ int width = 250,
+ int height = 300,
+ string title = null,
+ string caption = null,
+ string backgroundShellColor = "#8ec1da",
+ string shellColor = "#ffffff",
+ string tweetsBackgroundColor = "#ffffff",
+ string tweetsColor = "#444444",
+ string tweetsLinksColor = "#1985b5",
+ bool scrollBar = false,
+ bool loop = true,
+ bool live = true,
+ bool hashTags = true,
+ bool timestamp = true,
+ bool avatars = true,
+ bool topTweets = true,
+ string behavior = "default",
+ int interval = 6) {
+
+ if (String.IsNullOrEmpty(searchQuery)) {
+ throw new ArgumentException(String.Format(CultureInfo.CurrentUICulture, CommonResources.Argument_Cannot_Be_Null_Or_Empty, "searchQuery"), "searchQuery");
+ }
+
+<script type="text/javascript" src="http://widgets.twimg.com/j/2/widget.js"></script>
+<script type="text/javascript">
+new TWTR.Widget({
+ version: 2,
+ type: 'search',
+ search: '@RawJS(searchQuery)',
+ interval: @(Math.Min(Math.Max(2, interval), 20) * 1000),
+ title: '@RawJS(title)',
+ subject: '@RawJS(caption)',
+ width: @(new HtmlString(width <= 0 ? "'auto'" : width.ToString(CultureInfo.InvariantCulture))),
+ height: @(height < 0 ? "0" : height.ToString(CultureInfo.InvariantCulture)),
+ theme: {
+ shell: {
+ background: '@RawJS(backgroundShellColor.IsEmpty() ? "#8ec1da" : backgroundShellColor)',
+ color: '@RawJS(shellColor.IsEmpty() ? "#ffffff" : shellColor)'
+ },
+ tweets: {
+ background: '@RawJS(tweetsBackgroundColor.IsEmpty() ? "#ffffff" : tweetsBackgroundColor)',
+ color: '@RawJS(tweetsColor.IsEmpty() ? "#444444" : tweetsColor)',
+ links: '@RawJS(tweetsLinksColor.IsEmpty() ? "#1985b5" : tweetsLinksColor)'
+ }
+ },
+ features: {
+ scrollbar: @scrollBar.ToString(CultureInfo.InvariantCulture).ToLowerInvariant(),
+ loop: @loop.ToString(CultureInfo.InvariantCulture).ToLowerInvariant(),
+ live: @live.ToString(CultureInfo.InvariantCulture).ToLowerInvariant(),
+ hashtags: @hashTags.ToString(CultureInfo.InvariantCulture).ToLowerInvariant(),
+ timestamp: @timestamp.ToString(CultureInfo.InvariantCulture).ToLowerInvariant(),
+ avatars: @avatars.ToString(CultureInfo.InvariantCulture).ToLowerInvariant(),
+ toptweets: @topTweets.ToString(CultureInfo.InvariantCulture).ToLowerInvariant(),
+ behavior: '@behavior.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()'
+ }
+}).render().start();
+</script>
+}
+
+@helper Faves(
+ string userName,
+ int width = 250,
+ int height = 300,
+ string title = null,
+ string caption = null,
+ string backgroundShellColor = "#43c43f",
+ string shellColor = "#ffffff",
+ string tweetsBackgroundColor = "#ffffff",
+ string tweetsColor = "#444444",
+ string tweetsLinksColor = "#43c43f",
+ int numberOfTweets = 10,
+ bool scrollBar = true,
+ bool loop = false,
+ bool live = true,
+ bool hashTags = true,
+ bool timestamp = true,
+ bool avatars = true,
+ string behavior = "default",
+ int interval = 6) {
+
+ if (String.IsNullOrEmpty(userName)) {
+ throw new ArgumentException(String.Format(CultureInfo.CurrentUICulture, CommonResources.Argument_Cannot_Be_Null_Or_Empty, "userName"), "userName");
+ }
+
+<script type="text/javascript" src="http://widgets.twimg.com/j/2/widget.js"></script>
+<script type="text/javascript">
+new TWTR.Widget({
+ version: 2,
+ type: 'faves',
+ rpp: @(numberOfTweets < 1 ? 1 : (numberOfTweets > 20 ? 20 : numberOfTweets)),
+ interval: @(interval < 2 ? 2000 : (interval > 20 ? 20000 : (interval * 1000))),
+ title: '@RawJS(title)',
+ subject: '@RawJS(caption)',
+ width: @(new HtmlString(width <= 0 ? "'auto'" : width.ToString(CultureInfo.InvariantCulture))),
+ height: @(height < 0 ? "0" : height.ToString(CultureInfo.InvariantCulture)),
+ theme: {
+ shell: {
+ background: '@RawJS(backgroundShellColor.IsEmpty() ? "#43c43f" : backgroundShellColor)',
+ color: '@RawJS(shellColor.IsEmpty() ? "#ffffff" : shellColor)'
+ },
+ tweets: {
+ background: '@RawJS(tweetsBackgroundColor.IsEmpty() ? "#ffffff" : tweetsBackgroundColor)',
+ color: '@RawJS(tweetsColor.IsEmpty() ? "#444444" : tweetsColor)',
+ links: '@RawJS(tweetsLinksColor.IsEmpty() ? "#43c43f" : tweetsLinksColor)'
+ }
+ },
+ features: {
+ scrollbar: @scrollBar.ToString(CultureInfo.InvariantCulture).ToLowerInvariant(),
+ loop: @loop.ToString(CultureInfo.InvariantCulture).ToLowerInvariant(),
+ live: @live.ToString(CultureInfo.InvariantCulture).ToLowerInvariant(),
+ hashtags: @hashTags.ToString(CultureInfo.InvariantCulture).ToLowerInvariant(),
+ timestamp: @timestamp.ToString(CultureInfo.InvariantCulture).ToLowerInvariant(),
+ avatars: @avatars.ToString(CultureInfo.InvariantCulture).ToLowerInvariant(),
+ behavior: '@behavior.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()'
+ }
+}).render().setUser('@RawJS(userName)').start();
+</script>
+}
+
+@helper List(
+ string userName,
+ string list,
+ int width = 250,
+ int height = 300,
+ string title = null,
+ string caption = null,
+ string backgroundShellColor = "#ff96e7",
+ string shellColor = "#ffffff",
+ string tweetsBackgroundColor = "#ffffff",
+ string tweetsColor = "#444444",
+ string tweetsLinksColor = "#b740c2",
+ int numberOfTweets = 30,
+ bool scrollBar = true,
+ bool loop = false,
+ bool live = true,
+ bool hashTags = true,
+ bool timestamp = true,
+ bool avatars = true,
+ string behavior = "default",
+ int interval = 6) {
+
+ if (String.IsNullOrEmpty(userName)) {
+ throw new ArgumentException(String.Format(CultureInfo.CurrentUICulture, CommonResources.Argument_Cannot_Be_Null_Or_Empty, "userName"), "userName");
+ }
+
+ if (String.IsNullOrEmpty(list)) {
+ throw new ArgumentException(String.Format(CultureInfo.CurrentUICulture, CommonResources.Argument_Cannot_Be_Null_Or_Empty, "list"), "list");
+ }
+
+<script type="text/javascript" src="http://widgets.twimg.com/j/2/widget.js"></script>
+<script type="text/javascript">
+new TWTR.Widget({
+ version: 2,
+ type: 'list',
+ rpp: @(numberOfTweets < 1 ? 1 : (numberOfTweets > 100 ? 100 : numberOfTweets)),
+ interval: @(interval < 2 ? 2000 : (interval > 20 ? 20000 : (interval * 1000))),
+ title: '@RawJS(title)',
+ subject: '@RawJS(caption)',
+ width: @(new HtmlString(width <= 0 ? "'auto'" : width.ToString(CultureInfo.InvariantCulture))),
+ height: @(height < 0 ? "0" : height.ToString(CultureInfo.InvariantCulture)),
+ theme: {
+ shell: {
+ background: '@RawJS(backgroundShellColor.IsEmpty() ? "#ff96e7" : backgroundShellColor)',
+ color: '@RawJS(shellColor.IsEmpty() ? "#ffffff" : shellColor)'
+ },
+ tweets: {
+ background: '@RawJS(tweetsBackgroundColor.IsEmpty() ? "#ffffff" : tweetsBackgroundColor)',
+ color: '@RawJS(tweetsColor.IsEmpty() ? "#444444" : tweetsColor)',
+ links: '@RawJS(tweetsLinksColor.IsEmpty() ? "#b740c2" : tweetsLinksColor)'
+ }
+ },
+ features: {
+ scrollbar: @scrollBar.ToString(CultureInfo.InvariantCulture).ToLowerInvariant(),
+ loop: @loop.ToString(CultureInfo.InvariantCulture).ToLowerInvariant(),
+ live: @live.ToString(CultureInfo.InvariantCulture).ToLowerInvariant(),
+ hashtags: @hashTags.ToString(CultureInfo.InvariantCulture).ToLowerInvariant(),
+ timestamp: @timestamp.ToString(CultureInfo.InvariantCulture).ToLowerInvariant(),
+ avatars: @avatars.ToString(CultureInfo.InvariantCulture).ToLowerInvariant(),
+ behavior: '@behavior.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()'
+ }
+}).render().setList('@RawJS(userName)', '@RawJS(list)').start();
+</script>
+}
+
+
+@functions {
+ private static IHtmlString RawJS(string text) {
+ return new HtmlString(HttpUtility.JavaScriptStringEncode(text));
+ }
+} \ No newline at end of file
diff --git a/src/Microsoft.Web.Helpers/Twitter.generated.cs b/src/Microsoft.Web.Helpers/Twitter.generated.cs
new file mode 100644
index 00000000..17f10690
--- /dev/null
+++ b/src/Microsoft.Web.Helpers/Twitter.generated.cs
@@ -0,0 +1,1193 @@
+#pragma warning disable 1591
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.235
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Microsoft.Web.Helpers
+{
+ using System;
+ using System.Collections.Generic;
+
+ #line 3 "..\..\Twitter.cshtml"
+ using System.Globalization;
+
+ #line default
+ #line hidden
+ using System.IO;
+ using System.Linq;
+ using System.Net;
+ using System.Text;
+ using System.Web;
+ using System.Web.Helpers;
+ using System.Web.Security;
+ using System.Web.UI;
+ using System.Web.WebPages;
+ using System.Web.WebPages.Html;
+
+ #line 4 "..\..\Twitter.cshtml"
+ using Microsoft.Internal.Web.Utils;
+
+ #line default
+ #line hidden
+
+ public class Twitter : System.Web.WebPages.HelperPage
+ {
+
+public static System.Web.WebPages.HelperResult TweetButton(
+ string dataCount = "vertical",
+ string shareText = "Tweet",
+ string tweetText = "",
+ string url = "",
+ string language = "",
+ string userName = "",
+ string relatedUserName = "",
+ string relatedUserDescription = "") {
+return new System.Web.WebPages.HelperResult(__razor_helper_writer => {
+
+
+
+#line 14 "..\..\Twitter.cshtml"
+
+ var tweetTextAttribute = new HtmlString(!tweetText.IsEmpty() ? String.Format(CultureInfo.InvariantCulture, " data-text=\"{0}\"", HttpUtility.HtmlAttributeEncode(tweetText)) : "");
+ var urlAttribute = new HtmlString(!url.IsEmpty() ? String.Format(CultureInfo.InvariantCulture, " data-url=\"{0}\"", HttpUtility.HtmlAttributeEncode(url)) : "");
+ var languageAttribute = new HtmlString(!language.IsEmpty() && !language.Equals("en", StringComparison.OrdinalIgnoreCase) ? String.Format(CultureInfo.InvariantCulture, " data-lang=\"{0}\"", HttpUtility.HtmlAttributeEncode(language)) : "");
+ var userNameAttribute = new HtmlString(!userName.IsEmpty() ? String.Format(CultureInfo.InvariantCulture, " data-via=\"{0}\"", HttpUtility.HtmlAttributeEncode(userName)) : "");
+ var relatedAttribute = new HtmlString(!relatedUserName.IsEmpty() ? String.Format(CultureInfo.InvariantCulture, " data-related=\"{0}{1}\"", HttpUtility.HtmlAttributeEncode(relatedUserName),
+ !relatedUserDescription.IsEmpty() ? ":" + HttpUtility.HtmlAttributeEncode(relatedUserDescription) : "") : "");
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "<a href=\"http://twitter.com/share\" class=\"twitter-share-button\"");
+
+
+
+#line 21 "..\..\Twitter.cshtml"
+ WriteTo(@__razor_helper_writer, tweetTextAttribute);
+
+#line default
+#line hidden
+
+
+
+#line 21 "..\..\Twitter.cshtml"
+ WriteTo(@__razor_helper_writer, urlAttribute);
+
+#line default
+#line hidden
+
+
+
+#line 21 "..\..\Twitter.cshtml"
+ WriteTo(@__razor_helper_writer, languageAttribute);
+
+#line default
+#line hidden
+
+
+
+#line 21 "..\..\Twitter.cshtml"
+ WriteTo(@__razor_helper_writer, userNameAttribute);
+
+#line default
+#line hidden
+
+
+
+#line 21 "..\..\Twitter.cshtml"
+ WriteTo(@__razor_helper_writer, relatedAttribute);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " data-count=\"");
+
+
+
+#line 21 "..\..\Twitter.cshtml"
+ WriteTo(@__razor_helper_writer, dataCount.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\">");
+
+
+
+#line 21 "..\..\Twitter.cshtml"
+ WriteTo(@__razor_helper_writer, shareText);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "</a>");
+
+
+
+WriteLiteralTo(@__razor_helper_writer, "<script type=\"text/javascript\" src=\"http://platform.twitter.com/widgets.js\"></scr" +
+"ipt>\r\n");
+
+
+
+#line 22 "..\..\Twitter.cshtml"
+
+#line default
+#line hidden
+
+});
+
+}
+
+
+public static System.Web.WebPages.HelperResult FollowButton(
+ string userName,
+ string followStyle = "follow_me",
+ string followColor = "a") {
+return new System.Web.WebPages.HelperResult(__razor_helper_writer => {
+
+
+
+#line 27 "..\..\Twitter.cshtml"
+
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "<a href=\"http://www.twitter.com/");
+
+
+
+#line 28 "..\..\Twitter.cshtml"
+ WriteTo(@__razor_helper_writer, HttpUtility.UrlEncode(userName));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\"><img src=\"http://twitter-badges.s3.amazonaws.com/");
+
+
+
+#line 28 "..\..\Twitter.cshtml"
+ WriteTo(@__razor_helper_writer, HttpUtility.UrlEncode(followStyle + '-' + followColor));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ".png\" alt=\"Follow ");
+
+
+
+#line 28 "..\..\Twitter.cshtml"
+ WriteTo(@__razor_helper_writer, userName);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, " on Twitter\"/></a>\r\n");
+
+
+
+#line 29 "..\..\Twitter.cshtml"
+
+#line default
+#line hidden
+
+});
+
+}
+
+
+public static System.Web.WebPages.HelperResult Profile(
+ string userName,
+ int width = 250,
+ int height = 300,
+ string backgroundShellColor = "#333333",
+ string shellColor = "#ffffff",
+ string tweetsBackgroundColor = "#000000",
+ string tweetsColor = "#ffffff",
+ string tweetsLinksColor = "#4aed05",
+ int numberOfTweets = 4,
+ bool scrollBar = false,
+ bool loop = false,
+ bool live = false,
+ bool hashTags = true,
+ bool timestamp = true,
+ bool avatars = false,
+ string behavior = "default",
+ int interval = 6) {
+return new System.Web.WebPages.HelperResult(__razor_helper_writer => {
+
+
+
+#line 48 "..\..\Twitter.cshtml"
+
+
+ if (String.IsNullOrEmpty(userName)) {
+ throw new ArgumentException(String.Format(CultureInfo.CurrentUICulture, CommonResources.Argument_Cannot_Be_Null_Or_Empty, "userName"), "userName");
+ }
+
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "<script type=\"text/javascript\" src=\"http://widgets.twimg.com/j/2/widget.js\"></scr" +
+"ipt>\r\n");
+
+
+
+WriteLiteralTo(@__razor_helper_writer, "<script type=\"text/javascript\">\r\nnew TWTR.Widget({\r\n version: 2,\r\n type: \'profi" +
+"le\',\r\n rpp: ");
+
+
+
+#line 59 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, numberOfTweets < 1 ? 1 : (numberOfTweets > 30 ? 30 : numberOfTweets));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ",\r\n interval: ");
+
+
+
+#line 60 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, interval < 2 ? 2000 : (interval > 20 ? 20000 : (interval * 1000)));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ",\r\n width: ");
+
+
+
+#line 61 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, new HtmlString(width <= 0 ? "'auto'" : width.ToString(CultureInfo.InvariantCulture)));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ",\r\n height: ");
+
+
+
+#line 62 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, height < 0 ? "0" : height.ToString(CultureInfo.InvariantCulture));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ",\r\n theme: {\r\n shell: {\r\n background: \'");
+
+
+
+#line 65 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, RawJS(backgroundShellColor.IsEmpty() ? "#333333" : backgroundShellColor));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\',\r\n color: \'");
+
+
+
+#line 66 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, RawJS(shellColor.IsEmpty() ? "#ffffff" : shellColor));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\'\r\n },\r\n tweets: {\r\n background: \'");
+
+
+
+#line 69 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, RawJS(tweetsBackgroundColor.IsEmpty() ? "#000000" : tweetsBackgroundColor));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\',\r\n color: \'");
+
+
+
+#line 70 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, RawJS(tweetsColor.IsEmpty() ? "#ffffff" : tweetsColor));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\',\r\n links: \'");
+
+
+
+#line 71 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, RawJS(tweetsLinksColor.IsEmpty() ? "#4aed05" : tweetsLinksColor));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\'\r\n }\r\n },\r\n features: {\r\n scrollbar: ");
+
+
+
+#line 75 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, scrollBar.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ",\r\n loop: ");
+
+
+
+#line 76 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, loop.ToString(CultureInfo.InvariantCulture).ToLower(CultureInfo.InvariantCulture));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ",\r\n live: ");
+
+
+
+#line 77 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, live.ToString(CultureInfo.InvariantCulture).ToLower(CultureInfo.InvariantCulture));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ",\r\n hashtags: ");
+
+
+
+#line 78 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, hashTags.ToString(CultureInfo.InvariantCulture).ToLower(CultureInfo.InvariantCulture));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ",\r\n timestamp: ");
+
+
+
+#line 79 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, timestamp.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ",\r\n avatars: ");
+
+
+
+#line 80 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, avatars.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ",\r\n behavior: \'");
+
+
+
+#line 81 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, behavior.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\'\r\n }\r\n}).render().setUser(\'");
+
+
+
+#line 83 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, RawJS(userName));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\').start();\r\n</script>\r\n");
+
+
+
+#line 85 "..\..\Twitter.cshtml"
+
+#line default
+#line hidden
+
+});
+
+}
+
+
+public static System.Web.WebPages.HelperResult Search(
+ string searchQuery,
+ int width = 250,
+ int height = 300,
+ string title = null,
+ string caption = null,
+ string backgroundShellColor = "#8ec1da",
+ string shellColor = "#ffffff",
+ string tweetsBackgroundColor = "#ffffff",
+ string tweetsColor = "#444444",
+ string tweetsLinksColor = "#1985b5",
+ bool scrollBar = false,
+ bool loop = true,
+ bool live = true,
+ bool hashTags = true,
+ bool timestamp = true,
+ bool avatars = true,
+ bool topTweets = true,
+ string behavior = "default",
+ int interval = 6) {
+return new System.Web.WebPages.HelperResult(__razor_helper_writer => {
+
+
+
+#line 107 "..\..\Twitter.cshtml"
+
+
+ if (String.IsNullOrEmpty(searchQuery)) {
+ throw new ArgumentException(String.Format(CultureInfo.CurrentUICulture, CommonResources.Argument_Cannot_Be_Null_Or_Empty, "searchQuery"), "searchQuery");
+ }
+
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "<script type=\"text/javascript\" src=\"http://widgets.twimg.com/j/2/widget.js\"></scr" +
+"ipt>\r\n");
+
+
+
+WriteLiteralTo(@__razor_helper_writer, "<script type=\"text/javascript\">\r\nnew TWTR.Widget({\r\n version: 2,\r\n type: \'searc" +
+"h\',\r\n search: \'");
+
+
+
+#line 118 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, RawJS(searchQuery));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\',\r\n interval: ");
+
+
+
+#line 119 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, Math.Min(Math.Max(2, interval), 20) * 1000);
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ",\r\n title: \'");
+
+
+
+#line 120 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, RawJS(title));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\',\r\n subject: \'");
+
+
+
+#line 121 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, RawJS(caption));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\',\r\n width: ");
+
+
+
+#line 122 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, new HtmlString(width <= 0 ? "'auto'" : width.ToString(CultureInfo.InvariantCulture)));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ",\r\n height: ");
+
+
+
+#line 123 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, height < 0 ? "0" : height.ToString(CultureInfo.InvariantCulture));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ",\r\n theme: {\r\n shell: {\r\n background: \'");
+
+
+
+#line 126 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, RawJS(backgroundShellColor.IsEmpty() ? "#8ec1da" : backgroundShellColor));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\',\r\n color: \'");
+
+
+
+#line 127 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, RawJS(shellColor.IsEmpty() ? "#ffffff" : shellColor));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\'\r\n },\r\n tweets: {\r\n background: \'");
+
+
+
+#line 130 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, RawJS(tweetsBackgroundColor.IsEmpty() ? "#ffffff" : tweetsBackgroundColor));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\',\r\n color: \'");
+
+
+
+#line 131 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, RawJS(tweetsColor.IsEmpty() ? "#444444" : tweetsColor));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\',\r\n links: \'");
+
+
+
+#line 132 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, RawJS(tweetsLinksColor.IsEmpty() ? "#1985b5" : tweetsLinksColor));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\'\r\n }\r\n },\r\n features: {\r\n scrollbar: ");
+
+
+
+#line 136 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, scrollBar.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ",\r\n loop: ");
+
+
+
+#line 137 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, loop.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ",\r\n live: ");
+
+
+
+#line 138 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, live.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ",\r\n hashtags: ");
+
+
+
+#line 139 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, hashTags.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ",\r\n timestamp: ");
+
+
+
+#line 140 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, timestamp.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ",\r\n avatars: ");
+
+
+
+#line 141 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, avatars.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ",\r\n toptweets: ");
+
+
+
+#line 142 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, topTweets.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ",\r\n behavior: \'");
+
+
+
+#line 143 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, behavior.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\'\r\n }\r\n}).render().start();\r\n</script>\r\n");
+
+
+
+#line 147 "..\..\Twitter.cshtml"
+
+#line default
+#line hidden
+
+});
+
+}
+
+
+public static System.Web.WebPages.HelperResult Faves(
+ string userName,
+ int width = 250,
+ int height = 300,
+ string title = null,
+ string caption = null,
+ string backgroundShellColor = "#43c43f",
+ string shellColor = "#ffffff",
+ string tweetsBackgroundColor = "#ffffff",
+ string tweetsColor = "#444444",
+ string tweetsLinksColor = "#43c43f",
+ int numberOfTweets = 10,
+ bool scrollBar = true,
+ bool loop = false,
+ bool live = true,
+ bool hashTags = true,
+ bool timestamp = true,
+ bool avatars = true,
+ string behavior = "default",
+ int interval = 6) {
+return new System.Web.WebPages.HelperResult(__razor_helper_writer => {
+
+
+
+#line 168 "..\..\Twitter.cshtml"
+
+
+ if (String.IsNullOrEmpty(userName)) {
+ throw new ArgumentException(String.Format(CultureInfo.CurrentUICulture, CommonResources.Argument_Cannot_Be_Null_Or_Empty, "userName"), "userName");
+ }
+
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "<script type=\"text/javascript\" src=\"http://widgets.twimg.com/j/2/widget.js\"></scr" +
+"ipt>\r\n");
+
+
+
+WriteLiteralTo(@__razor_helper_writer, "<script type=\"text/javascript\">\r\nnew TWTR.Widget({\r\n version: 2,\r\n type: \'faves" +
+"\',\r\n rpp: ");
+
+
+
+#line 179 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, numberOfTweets < 1 ? 1 : (numberOfTweets > 20 ? 20 : numberOfTweets));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ",\r\n interval: ");
+
+
+
+#line 180 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, interval < 2 ? 2000 : (interval > 20 ? 20000 : (interval * 1000)));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ",\r\n title: \'");
+
+
+
+#line 181 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, RawJS(title));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\',\r\n subject: \'");
+
+
+
+#line 182 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, RawJS(caption));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\',\r\n width: ");
+
+
+
+#line 183 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, new HtmlString(width <= 0 ? "'auto'" : width.ToString(CultureInfo.InvariantCulture)));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ",\r\n height: ");
+
+
+
+#line 184 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, height < 0 ? "0" : height.ToString(CultureInfo.InvariantCulture));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ",\r\n theme: {\r\n shell: {\r\n background: \'");
+
+
+
+#line 187 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, RawJS(backgroundShellColor.IsEmpty() ? "#43c43f" : backgroundShellColor));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\',\r\n color: \'");
+
+
+
+#line 188 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, RawJS(shellColor.IsEmpty() ? "#ffffff" : shellColor));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\'\r\n },\r\n tweets: {\r\n background: \'");
+
+
+
+#line 191 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, RawJS(tweetsBackgroundColor.IsEmpty() ? "#ffffff" : tweetsBackgroundColor));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\',\r\n color: \'");
+
+
+
+#line 192 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, RawJS(tweetsColor.IsEmpty() ? "#444444" : tweetsColor));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\',\r\n links: \'");
+
+
+
+#line 193 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, RawJS(tweetsLinksColor.IsEmpty() ? "#43c43f" : tweetsLinksColor));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\'\r\n }\r\n },\r\n features: {\r\n scrollbar: ");
+
+
+
+#line 197 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, scrollBar.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ",\r\n loop: ");
+
+
+
+#line 198 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, loop.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ",\r\n live: ");
+
+
+
+#line 199 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, live.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ",\r\n hashtags: ");
+
+
+
+#line 200 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, hashTags.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ",\r\n timestamp: ");
+
+
+
+#line 201 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, timestamp.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ",\r\n avatars: ");
+
+
+
+#line 202 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, avatars.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ",\r\n behavior: \'");
+
+
+
+#line 203 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, behavior.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\'\r\n }\r\n}).render().setUser(\'");
+
+
+
+#line 205 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, RawJS(userName));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\').start();\r\n</script>\r\n");
+
+
+
+#line 207 "..\..\Twitter.cshtml"
+
+#line default
+#line hidden
+
+});
+
+}
+
+
+public static System.Web.WebPages.HelperResult List(
+ string userName,
+ string list,
+ int width = 250,
+ int height = 300,
+ string title = null,
+ string caption = null,
+ string backgroundShellColor = "#ff96e7",
+ string shellColor = "#ffffff",
+ string tweetsBackgroundColor = "#ffffff",
+ string tweetsColor = "#444444",
+ string tweetsLinksColor = "#b740c2",
+ int numberOfTweets = 30,
+ bool scrollBar = true,
+ bool loop = false,
+ bool live = true,
+ bool hashTags = true,
+ bool timestamp = true,
+ bool avatars = true,
+ string behavior = "default",
+ int interval = 6) {
+return new System.Web.WebPages.HelperResult(__razor_helper_writer => {
+
+
+
+#line 229 "..\..\Twitter.cshtml"
+
+
+ if (String.IsNullOrEmpty(userName)) {
+ throw new ArgumentException(String.Format(CultureInfo.CurrentUICulture, CommonResources.Argument_Cannot_Be_Null_Or_Empty, "userName"), "userName");
+ }
+
+ if (String.IsNullOrEmpty(list)) {
+ throw new ArgumentException(String.Format(CultureInfo.CurrentUICulture, CommonResources.Argument_Cannot_Be_Null_Or_Empty, "list"), "list");
+ }
+
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "<script type=\"text/javascript\" src=\"http://widgets.twimg.com/j/2/widget.js\"></scr" +
+"ipt>\r\n");
+
+
+
+WriteLiteralTo(@__razor_helper_writer, "<script type=\"text/javascript\">\r\nnew TWTR.Widget({\r\n version: 2,\r\n type: \'list\'" +
+",\r\n rpp: ");
+
+
+
+#line 244 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, numberOfTweets < 1 ? 1 : (numberOfTweets > 100 ? 100 : numberOfTweets));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ",\r\n interval: ");
+
+
+
+#line 245 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, interval < 2 ? 2000 : (interval > 20 ? 20000 : (interval * 1000)));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ",\r\n title: \'");
+
+
+
+#line 246 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, RawJS(title));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\',\r\n subject: \'");
+
+
+
+#line 247 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, RawJS(caption));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\',\r\n width: ");
+
+
+
+#line 248 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, new HtmlString(width <= 0 ? "'auto'" : width.ToString(CultureInfo.InvariantCulture)));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ",\r\n height: ");
+
+
+
+#line 249 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, height < 0 ? "0" : height.ToString(CultureInfo.InvariantCulture));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ",\r\n theme: {\r\n shell: {\r\n background: \'");
+
+
+
+#line 252 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, RawJS(backgroundShellColor.IsEmpty() ? "#ff96e7" : backgroundShellColor));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\',\r\n color: \'");
+
+
+
+#line 253 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, RawJS(shellColor.IsEmpty() ? "#ffffff" : shellColor));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\'\r\n },\r\n tweets: {\r\n background: \'");
+
+
+
+#line 256 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, RawJS(tweetsBackgroundColor.IsEmpty() ? "#ffffff" : tweetsBackgroundColor));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\',\r\n color: \'");
+
+
+
+#line 257 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, RawJS(tweetsColor.IsEmpty() ? "#444444" : tweetsColor));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\',\r\n links: \'");
+
+
+
+#line 258 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, RawJS(tweetsLinksColor.IsEmpty() ? "#b740c2" : tweetsLinksColor));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\'\r\n }\r\n },\r\n features: {\r\n scrollbar: ");
+
+
+
+#line 262 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, scrollBar.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ",\r\n loop: ");
+
+
+
+#line 263 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, loop.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ",\r\n live: ");
+
+
+
+#line 264 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, live.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ",\r\n hashtags: ");
+
+
+
+#line 265 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, hashTags.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ",\r\n timestamp: ");
+
+
+
+#line 266 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, timestamp.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ",\r\n avatars: ");
+
+
+
+#line 267 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, avatars.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, ",\r\n behavior: \'");
+
+
+
+#line 268 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, behavior.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\'\r\n }\r\n}).render().setList(\'");
+
+
+
+#line 270 "..\..\Twitter.cshtml"
+WriteTo(@__razor_helper_writer, RawJS(userName));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\', \'");
+
+
+
+#line 270 "..\..\Twitter.cshtml"
+ WriteTo(@__razor_helper_writer, RawJS(list));
+
+#line default
+#line hidden
+
+WriteLiteralTo(@__razor_helper_writer, "\').start();\r\n</script>\r\n");
+
+
+
+#line 272 "..\..\Twitter.cshtml"
+
+#line default
+#line hidden
+
+});
+
+}
+
+
+ #line 275 "..\..\Twitter.cshtml"
+
+ private static IHtmlString RawJS(string text) {
+ return new HtmlString(HttpUtility.JavaScriptStringEncode(text));
+ }
+
+ #line default
+ #line hidden
+
+ public Twitter()
+ {
+ }
+ }
+}
+#pragma warning restore 1591
diff --git a/src/Microsoft.Web.Helpers/UrlBuilder.cs b/src/Microsoft.Web.Helpers/UrlBuilder.cs
new file mode 100644
index 00000000..ed68ef1e
--- /dev/null
+++ b/src/Microsoft.Web.Helpers/UrlBuilder.cs
@@ -0,0 +1,190 @@
+using System;
+using System.Globalization;
+using System.Text;
+using System.Web;
+using System.Web.Routing;
+using System.Web.WebPages;
+
+namespace Microsoft.Web.Helpers
+{
+ public class UrlBuilder
+ {
+ private static readonly VirtualPathUtilityWrapper _defaultVirtualPathUtility = new VirtualPathUtilityWrapper();
+ private readonly VirtualPathUtilityBase _virtualPathUtility;
+ private readonly StringBuilder _params = new StringBuilder();
+ private string _path;
+
+ /// <summary>
+ /// Constructs an Url with the current page's virtual path and no query string parameters
+ /// </summary>
+ public UrlBuilder()
+ : this(null, null)
+ {
+ }
+
+ /// <summary>
+ /// Constructs an Url with the specified path and no query string parameters.
+ /// </summary>
+ public UrlBuilder(string path)
+ : this(path, null)
+ {
+ }
+
+ /// <summary>
+ /// Constructs an Url with the current page's virtual path and the parameters
+ /// </summary>
+ /// <param name="parameters"></param>
+ public UrlBuilder(object parameters)
+ : this(null, parameters)
+ {
+ }
+
+ public UrlBuilder(string path, object parameters)
+ : this(GetHttpContext(), null, path, parameters)
+ {
+ }
+
+ internal UrlBuilder(HttpContextBase httpContext, VirtualPathUtilityBase virtualPathUtility, string path, object parameters)
+ {
+ _virtualPathUtility = virtualPathUtility;
+ Uri uri;
+ if (Uri.TryCreate(path, UriKind.Absolute, out uri))
+ {
+ _path = uri.GetLeftPart(UriPartial.Path);
+ _params.Append(uri.Query);
+ }
+ else
+ {
+ // If the url is being built as part of a WebPages request, use the template stack to identify the current template's virtual path.
+ _path = GetPageRelativePath(httpContext, path);
+ int queryStringIndex = (_path ?? String.Empty).IndexOf('?');
+ if (queryStringIndex != -1)
+ {
+ _params.Append(_path.Substring(queryStringIndex));
+ _path = _path.Substring(0, queryStringIndex);
+ }
+ }
+
+ if (parameters != null)
+ {
+ AddParam(parameters);
+ }
+ }
+
+ internal static VirtualPathUtilityBase DefaultVirtualPathUtility
+ {
+ get { return _defaultVirtualPathUtility; }
+ }
+
+ public string Path
+ {
+ get { return _path; }
+ }
+
+ public string QueryString
+ {
+ get { return _params.ToString(); }
+ }
+
+ private VirtualPathUtilityBase VirtualPathUtility
+ {
+ get { return _virtualPathUtility ?? _defaultVirtualPathUtility; }
+ }
+
+ /// <summary>
+ /// Factory method to create an UrlBuilder instance
+ /// </summary>
+ public static UrlBuilder Create(string path, object parameters = null)
+ {
+ return new UrlBuilder(path, parameters);
+ }
+
+ public UrlBuilder AddPath(string path)
+ {
+ _path = EnsureTrailingSlash(_path);
+ if (!path.IsEmpty())
+ {
+ _path += HttpUtility.UrlPathEncode(path.TrimStart('/'));
+ }
+ return this;
+ }
+
+ public UrlBuilder AddPath(params string[] pathTokens)
+ {
+ foreach (var token in pathTokens)
+ {
+ AddPath(token);
+ }
+ return this;
+ }
+
+ public UrlBuilder AddParam(string name, object value)
+ {
+ if (!String.IsNullOrEmpty(name))
+ {
+ _params.Append(_params.Length == 0 ? '?' : '&');
+ _params.Append(HttpUtility.UrlEncode(name))
+ .Append('=')
+ .Append(HttpUtility.UrlEncode(Convert.ToString(value, CultureInfo.InvariantCulture)));
+ }
+ return this;
+ }
+
+ public UrlBuilder AddParam(object values)
+ {
+ var dictionary = new RouteValueDictionary(values);
+ foreach (var item in dictionary)
+ {
+ AddParam(item.Key, item.Value);
+ }
+
+ return this;
+ }
+
+ public override string ToString()
+ {
+ return _path + _params;
+ }
+
+ private static HttpContextBase GetHttpContext()
+ {
+ return HttpContext.Current != null ? new HttpContextWrapper(HttpContext.Current) : null;
+ }
+
+ private static string EnsureTrailingSlash(string path)
+ {
+ if (!path.IsEmpty() && path[path.Length - 1] != '/')
+ {
+ path += '/';
+ }
+ return path;
+ }
+
+ private string GetPageRelativePath(HttpContextBase httpContext, string path)
+ {
+ if (httpContext == null)
+ {
+ return path;
+ }
+ var templateFile = TemplateStack.GetCurrentTemplate(httpContext);
+ if (templateFile != null)
+ {
+ var templateVirtualPath = templateFile.TemplateInfo.VirtualPath;
+ if (path.IsEmpty())
+ {
+ path = templateVirtualPath;
+ }
+ else
+ {
+ path = VirtualPathUtility.Combine(templateVirtualPath, path);
+ }
+ }
+ return VirtualPathUtility.ToAbsolute(path ?? "~/");
+ }
+
+ public static implicit operator string(UrlBuilder builder)
+ {
+ return builder.ToString();
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Helpers/Video.cs b/src/Microsoft.Web.Helpers/Video.cs
new file mode 100644
index 00000000..a92f3df0
--- /dev/null
+++ b/src/Microsoft.Web.Helpers/Video.cs
@@ -0,0 +1,347 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Web;
+using System.Web.Routing;
+using System.Web.WebPages;
+using Microsoft.Internal.Web.Utils;
+using Resources;
+
+namespace Microsoft.Web.Helpers
+{
+ public static class Video
+ {
+ private const string FlashCab =
+ "http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab";
+
+ private const string FlashClassId = "clsid:d27cdb6e-ae6d-11cf-96b8-444553540000";
+ private const string FlashMimeType = "application/x-shockwave-flash";
+ private const string MediaPlayerClassId = "clsid:6BF52A52-394A-11D3-B153-00C04F79FAA6";
+ private const string MediaPlayerMimeType = "application/x-mplayer2";
+ private const string OleMimeType = "application/x-oleobject";
+ private const string SilverlightMimeType = "application/x-silverlight-2";
+
+ // These attributes can't be specified using anonymous objects (either because they are available as separate arguments or because
+ // they don't make sense in the context of the helper).
+ private static readonly string[] _globalBlacklist = new[] { "width", "height", "type", "data", "classid", "codebase" };
+ private static readonly string[] _mediaPlayerBlacklist = new[] { "autoStart", "playCount", "uiMode", "stretchToFit", "enableContextMenu", "mute", "volume", "baseURL" };
+ private static readonly string[] _silverlightBlacklist = new[] { "background", "initparams", "minruntimeversion", "autoUpgrade" };
+ private static readonly string[] _flashBlacklist = new[] { "play", "loop", "menu", "bgColor", "quality", "scale", "wmode", "base" };
+
+ private static VirtualPathUtilityWrapper _pathUtility = new VirtualPathUtilityWrapper();
+
+#if CODE_COVERAGE
+ [ExcludeFromCodeCoverage]
+#endif
+
+ private static HttpContextBase HttpContext
+ {
+ get
+ {
+ var httpContext = System.Web.HttpContext.Current;
+ return httpContext == null ? null : new HttpContextWrapper(httpContext);
+ }
+ }
+
+ // see: http://kb2.adobe.com/cps/127/tn_12701.html
+#if CODE_COVERAGE
+ [ExcludeFromCodeCoverage]
+#endif
+
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "10#",
+ Justification = "string parameter passed to flash in object tag")]
+ [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", Target = "bgColor",
+ Justification = "This method is public and the parameter name cannot be changed")]
+ public static HelperResult Flash(string path, string width = null, string height = null,
+ bool play = true, bool loop = true, bool menu = true, string bgColor = null,
+ string quality = null, string scale = null, string windowMode = null, string baseUrl = null,
+ string version = null, object options = null, object htmlAttributes = null, string embedName = null)
+ {
+ return Flash(HttpContext, _pathUtility, path, width, height, play, loop, menu, bgColor,
+ quality, scale, windowMode, baseUrl, version, options, htmlAttributes, embedName);
+ }
+
+ // see: http://msdn.microsoft.com/en-us/library/aa392321
+#if CODE_COVERAGE
+ [ExcludeFromCodeCoverage]
+#endif
+
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "10#",
+ Justification = "string parameter passed to media player in object tag")]
+ public static HelperResult MediaPlayer(string path, string width = null, string height = null,
+ bool autoStart = true, int playCount = 1, string uiMode = null, bool stretchToFit = false,
+ bool enableContextMenu = true, bool mute = false, int volume = -1, string baseUrl = null,
+ object options = null, object htmlAttributes = null, string embedName = null)
+ {
+ return MediaPlayer(HttpContext, _pathUtility, path, width, height, autoStart, playCount, uiMode, stretchToFit,
+ enableContextMenu, mute, volume, baseUrl, options, htmlAttributes, embedName);
+ }
+
+ // should users really use Silverlight.js?
+ // see: http://msdn.microsoft.com/en-us/library/cc838259(v=VS.95).aspx
+#if CODE_COVERAGE
+ [ExcludeFromCodeCoverage]
+#endif
+
+ [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", Target = "bgColor",
+ Justification = "This method is public and the parameter name cannot be changed")]
+ public static HelperResult Silverlight(string path, string width, string height,
+ string bgColor = null, string initParameters = null, string minimumVersion = null, bool autoUpgrade = true,
+ object options = null, object htmlAttributes = null)
+ {
+ return Silverlight(HttpContext, _pathUtility, path, width, height, bgColor, initParameters, minimumVersion, autoUpgrade,
+ options, htmlAttributes);
+ }
+
+ internal static HelperResult Flash(HttpContextBase context, VirtualPathUtilityBase pathUtility, string path,
+ string width = null, string height = null, bool play = true, bool loop = true, bool menu = true,
+ string backgroundColor = null, string quality = null, string scale = null, string windowMode = null,
+ string baseUrl = null, string version = null, object options = null, object htmlAttributes = null, string embedName = null)
+ {
+ var parameters = ObjectToDictionary(options, "options", _flashBlacklist);
+ if (!play)
+ {
+ parameters["play"] = false;
+ }
+ if (!loop)
+ {
+ parameters["loop"] = false;
+ }
+ if (!menu)
+ {
+ parameters["menu"] = false;
+ }
+ if (!String.IsNullOrEmpty(backgroundColor))
+ {
+ parameters["bgColor"] = backgroundColor;
+ }
+ if (!String.IsNullOrEmpty(quality))
+ {
+ parameters["quality"] = quality;
+ }
+ if (!String.IsNullOrEmpty(scale))
+ {
+ parameters["scale"] = scale;
+ }
+ if (!String.IsNullOrEmpty(windowMode))
+ {
+ parameters["wmode"] = windowMode;
+ }
+ if (!String.IsNullOrEmpty(baseUrl))
+ {
+ parameters["base"] = baseUrl;
+ }
+
+ string cab = FlashCab;
+ if (!String.IsNullOrEmpty(version))
+ {
+ cab += "#version=" + version.Replace('.', ',');
+ }
+ return GetHtml(context, pathUtility, path, width, height,
+ OleMimeType, null, FlashClassId, cab, "movie", FlashMimeType, parameters, htmlAttributes, embedName);
+ }
+
+ internal static HelperResult MediaPlayer(HttpContextBase context, VirtualPathUtilityBase pathUtility, string path, string width = null, string height = null,
+ bool autoStart = true, int playCount = 1, string uiMode = null, bool stretchToFit = false,
+ bool enableContextMenu = true, bool mute = false, int volume = -1, string baseUrl = null,
+ object options = null, object htmlAttributes = null, string embedName = null)
+ {
+ var parameters = ObjectToDictionary(options, "options", _mediaPlayerBlacklist);
+ if (!autoStart)
+ {
+ parameters["autoStart"] = false;
+ }
+ if (playCount != 1)
+ {
+ parameters["playCount"] = playCount;
+ }
+ if (!String.IsNullOrEmpty(uiMode))
+ {
+ parameters["uiMode"] = uiMode;
+ }
+ if (stretchToFit)
+ {
+ parameters["stretchToFit"] = true;
+ }
+ if (!enableContextMenu)
+ {
+ parameters["enableContextMenu"] = false;
+ }
+ if (mute)
+ {
+ parameters["mute"] = true;
+ }
+ if (volume >= 0)
+ {
+ parameters["volume"] = Math.Min(volume, 100);
+ }
+ if (!String.IsNullOrEmpty(baseUrl))
+ {
+ parameters["baseURL"] = baseUrl;
+ }
+
+ return GetHtml(context, pathUtility, path, width, height,
+ null, null, MediaPlayerClassId, null, "URL", MediaPlayerMimeType, parameters, htmlAttributes, embedName);
+ }
+
+ internal static HelperResult Silverlight(HttpContextBase context, VirtualPathUtilityBase pathUtility, string path, string width, string height,
+ string backgroundColor = null, string initParameters = null, string minimumVersion = null, bool autoUpgrade = true,
+ object options = null, object htmlAttributes = null)
+ {
+ if (String.IsNullOrEmpty(width))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "width");
+ }
+ if (String.IsNullOrEmpty(height))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "height");
+ }
+
+ var parameters = ObjectToDictionary(options, "options", _silverlightBlacklist);
+ if (!String.IsNullOrEmpty(backgroundColor))
+ {
+ parameters["background"] = backgroundColor;
+ }
+ if (!String.IsNullOrEmpty(initParameters))
+ {
+ parameters["initparams"] = initParameters;
+ }
+ if (!String.IsNullOrEmpty(minimumVersion))
+ {
+ parameters["minruntimeversion"] = minimumVersion;
+ }
+ if (!autoUpgrade)
+ {
+ parameters["autoUpgrade"] = autoUpgrade;
+ }
+
+ return GetHtml(context, pathUtility, path, width, height,
+ SilverlightMimeType, "data:" + SilverlightMimeType + ",", // ',' required for Opera support
+ null, null, "source", null, parameters, htmlAttributes, null,
+ tw =>
+ {
+ tw.WriteLine("<a href=\"http://go.microsoft.com/fwlink/?LinkID=149156\" style=\"text-decoration:none\">");
+ tw.WriteLine("<img src=\"http://go.microsoft.com/fwlink?LinkId=108181\" alt=\"Get Microsoft Silverlight\" style=\"border-style:none\"/>");
+ tw.WriteLine("</a>");
+ });
+ }
+
+ private static IDictionary<string, object> ObjectToDictionary(object o, string argName, string[] blackList)
+ {
+ var dictionary = new RouteValueDictionary(o);
+
+ foreach (var key in dictionary.Keys)
+ {
+ if (blackList.Contains(key, StringComparer.OrdinalIgnoreCase))
+ {
+ throw new ArgumentException(String.Format(CultureInfo.CurrentUICulture,
+ HelpersToolkitResources.Video_PropertyCannotBeSet, key), argName);
+ }
+ }
+ return dictionary;
+ }
+
+ private static HelperResult GetHtml(HttpContextBase context, VirtualPathUtilityBase pathUtility,
+ string path, string width, string height, string objectType, string objectDataType,
+ string objectClassId, string objectCodeBase, string pathParamName, string embedContentType,
+ IDictionary<string, object> parameters = null, object htmlAttributes = null, string embedName = null,
+ Action<TextWriter> plugin = null)
+ {
+ path = ValidatePath(context, pathUtility, path);
+
+ var objectAttr = ObjectToDictionary(htmlAttributes, "htmlAttributes", _globalBlacklist);
+ objectAttr["width"] = width;
+ objectAttr["height"] = height;
+ objectAttr["type"] = objectType;
+ objectAttr["data"] = objectDataType;
+ objectAttr["classid"] = objectClassId;
+ objectAttr["codebase"] = objectCodeBase;
+
+ return new HelperResult(tw =>
+ {
+ tw.Write("<object ");
+ foreach (var a in objectAttr.OrderBy(a => a.Key, StringComparer.OrdinalIgnoreCase))
+ {
+ var value = (a.Value == null) ? null : a.Value.ToString();
+ WriteIfNotNullOrEmpty(tw, a.Key, value);
+ }
+ tw.WriteLine(">");
+
+ // object parameters
+ if (!String.IsNullOrEmpty(pathParamName))
+ {
+ tw.WriteLine("<param name=\"{0}\" value=\"{1}\" />",
+ HttpUtility.HtmlAttributeEncode(pathParamName),
+ HttpUtility.HtmlAttributeEncode(HttpUtility.UrlPathEncode(path)));
+ }
+ if (parameters != null)
+ {
+ foreach (var p in parameters)
+ {
+ tw.WriteLine("<param name=\"{0}\" value=\"{1}\" />",
+ HttpUtility.HtmlAttributeEncode(p.Key),
+ HttpUtility.HtmlAttributeEncode(p.Value.ToString()));
+ }
+ }
+
+ if (!String.IsNullOrEmpty(embedContentType))
+ {
+ tw.Write("<embed src=\"{0}\" ", HttpUtility.HtmlAttributeEncode(HttpUtility.UrlPathEncode(path)));
+ WriteIfNotNullOrEmpty(tw, "width", width);
+ WriteIfNotNullOrEmpty(tw, "height", height);
+ WriteIfNotNullOrEmpty(tw, "name", embedName);
+ WriteIfNotNullOrEmpty(tw, "type", embedContentType);
+ if (parameters != null)
+ {
+ foreach (var p in parameters)
+ {
+ tw.Write("{0}=\"{1}\" ", HttpUtility.HtmlEncode(p.Key), HttpUtility.HtmlAttributeEncode(p.Value.ToString()));
+ }
+ }
+ tw.WriteLine("/>");
+ }
+ if (plugin != null)
+ {
+ plugin(tw);
+ }
+ tw.WriteLine("</object>");
+ });
+ }
+
+ private static string ValidatePath(HttpContextBase context, VirtualPathUtilityBase pathUtility, string path)
+ {
+ if (String.IsNullOrEmpty(path))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "path");
+ }
+ string originalPath = path;
+ if (!path.StartsWith("http", StringComparison.OrdinalIgnoreCase))
+ {
+ // resolve relative paths
+ path = pathUtility.Combine(context.Request.AppRelativeCurrentExecutionFilePath, path);
+ // resolve to app absolute - SL doesn't support app relative
+ path = pathUtility.ToAbsolute(path);
+ if (!File.Exists(context.Server.MapPath(path)))
+ {
+ throw new InvalidOperationException(String.Format(CultureInfo.CurrentUICulture,
+ HelpersToolkitResources.Video_FileDoesNotExist, originalPath));
+ }
+ }
+ return path;
+ }
+
+ private static void WriteIfNotNullOrEmpty(TextWriter tw, string key, string value)
+ {
+ Debug.Assert(!String.IsNullOrEmpty(key));
+
+ if (!String.IsNullOrEmpty(value))
+ {
+ tw.Write("{0}=\"{1}\" ", HttpUtility.HtmlEncode(key), HttpUtility.HtmlAttributeEncode(value));
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Helpers/VirtualPathUtilityBase.cs b/src/Microsoft.Web.Helpers/VirtualPathUtilityBase.cs
new file mode 100644
index 00000000..207641df
--- /dev/null
+++ b/src/Microsoft.Web.Helpers/VirtualPathUtilityBase.cs
@@ -0,0 +1,9 @@
+namespace Microsoft.Web.Helpers
+{
+ public abstract class VirtualPathUtilityBase
+ {
+ public abstract string Combine(string basePath, string relativePath);
+
+ public abstract string ToAbsolute(string virtualPath);
+ }
+}
diff --git a/src/Microsoft.Web.Helpers/VirtualPathUtilityWrapper.cs b/src/Microsoft.Web.Helpers/VirtualPathUtilityWrapper.cs
new file mode 100644
index 00000000..b11e2e4a
--- /dev/null
+++ b/src/Microsoft.Web.Helpers/VirtualPathUtilityWrapper.cs
@@ -0,0 +1,17 @@
+using System.Web;
+
+namespace Microsoft.Web.Helpers
+{
+ internal sealed class VirtualPathUtilityWrapper : VirtualPathUtilityBase
+ {
+ public override string Combine(string basePath, string relativePath)
+ {
+ return VirtualPathUtility.Combine(basePath, relativePath);
+ }
+
+ public override string ToAbsolute(string virtualPath)
+ {
+ return VirtualPathUtility.ToAbsolute(virtualPath);
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data.EntityFramework/DbContextExtensions.cs b/src/Microsoft.Web.Http.Data.EntityFramework/DbContextExtensions.cs
new file mode 100644
index 00000000..0825cc0a
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data.EntityFramework/DbContextExtensions.cs
@@ -0,0 +1,182 @@
+using System.ComponentModel;
+using System.Data;
+using System.Data.Entity;
+using System.Data.Entity.Infrastructure;
+using System.Data.Objects;
+using System.Web.Http.Common;
+
+namespace Microsoft.Web.Http.Data.EntityFramework
+{
+ /// <summary>
+ /// DbContext extension methods
+ /// </summary>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static class DbContextExtensions
+ {
+ /// <summary>
+ /// Extension method used to attach the specified entity as modified,
+ /// with the specified original state.
+ /// </summary>
+ /// <typeparam name="TEntity">The entity Type</typeparam>
+ /// <param name="dbSet">The <see cref="DbSet"/> to attach to.</param>
+ /// <param name="current">The current entity.</param>
+ /// <param name="original">The original entity.</param>
+ /// <param name="dbContext">The corresponding <see cref="DbContext"/></param>
+ public static void AttachAsModified<TEntity>(this DbSet<TEntity> dbSet, TEntity current, TEntity original, DbContext dbContext) where TEntity : class
+ {
+ if (dbSet == null)
+ {
+ throw Error.ArgumentNull("dbSet");
+ }
+ if (current == null)
+ {
+ throw Error.ArgumentNull("current");
+ }
+ if (original == null)
+ {
+ throw Error.ArgumentNull("original");
+ }
+ if (dbContext == null)
+ {
+ throw Error.ArgumentNull("dbContext");
+ }
+
+ DbEntityEntry<TEntity> entityEntry = dbContext.Entry(current);
+ if (entityEntry.State == EntityState.Detached)
+ {
+ dbSet.Attach(current);
+ }
+ else
+ {
+ entityEntry.State = EntityState.Modified;
+ }
+
+ ObjectContext objectContext = (dbContext as IObjectContextAdapter).ObjectContext;
+ ObjectStateEntry stateEntry = ObjectContextUtilities.AttachAsModifiedInternal<TEntity>(current, original, objectContext);
+
+ if (stateEntry.State != EntityState.Modified)
+ {
+ // Ensure that when we leave this method, the entity is in a
+ // Modified state. For example, if current and original are the
+ // same, we still need to force the state transition
+ entityEntry.State = EntityState.Modified;
+ }
+ }
+
+ /// <summary>
+ /// Extension method used to attach the specified entity as modified,
+ /// with the specified original state. This is a non-generic version.
+ /// </summary>
+ /// <param name="dbSet">The <see cref="DbSet"/> to attach to.</param>
+ /// <param name="current">The current entity.</param>
+ /// <param name="original">The original entity.</param>
+ /// <param name="dbContext">The corresponding <see cref="DbContext"/></param>
+ public static void AttachAsModified(this DbSet dbSet, object current, object original, DbContext dbContext)
+ {
+ if (dbSet == null)
+ {
+ throw Error.ArgumentNull("dbSet");
+ }
+ if (current == null)
+ {
+ throw Error.ArgumentNull("current");
+ }
+ if (original == null)
+ {
+ throw Error.ArgumentNull("original");
+ }
+ if (dbContext == null)
+ {
+ throw Error.ArgumentNull("dbContext");
+ }
+
+ DbEntityEntry entityEntry = dbContext.Entry(current);
+ if (entityEntry.State == EntityState.Detached)
+ {
+ dbSet.Attach(current);
+ }
+ else
+ {
+ entityEntry.State = EntityState.Modified;
+ }
+
+ ObjectContext objectContext = (dbContext as IObjectContextAdapter).ObjectContext;
+ ObjectStateEntry stateEntry = ObjectContextUtilities.AttachAsModifiedInternal(current, original, objectContext);
+
+ if (stateEntry.State != EntityState.Modified)
+ {
+ // Ensure that when we leave this method, the entity is in a
+ // Modified state. For example, if current and original are the
+ // same, we still need to force the state transition
+ entityEntry.State = EntityState.Modified;
+ }
+ }
+
+ /// <summary>
+ /// Extension method used to attach the specified entity as modified. This overload
+ /// can be used in cases where the entity has a Timestamp member.
+ /// </summary>
+ /// <typeparam name="TEntity">The entity type</typeparam>
+ /// <param name="dbSet">The <see cref="DbSet"/> to attach to</param>
+ /// <param name="entity">The current entity</param>
+ /// <param name="dbContext">The coresponding <see cref="DbContext"/></param>
+ public static void AttachAsModified<TEntity>(this DbSet<TEntity> dbSet, TEntity entity, DbContext dbContext) where TEntity : class
+ {
+ if (dbSet == null)
+ {
+ throw Error.ArgumentNull("dbSet");
+ }
+ if (entity == null)
+ {
+ throw Error.ArgumentNull("entity");
+ }
+ if (dbContext == null)
+ {
+ throw Error.ArgumentNull("dbContext");
+ }
+
+ DbEntityEntry<TEntity> entityEntry = dbContext.Entry(entity);
+ if (entityEntry.State == EntityState.Detached)
+ {
+ // attach the entity
+ dbSet.Attach(entity);
+ }
+
+ // transition the entity to the modified state
+ entityEntry.State = EntityState.Modified;
+ }
+
+ /// <summary>
+ /// Extension method used to attach the specified entity as modified. This overload
+ /// can be used in cases where the entity has a Timestamp member. This is a non-generic version
+ /// </summary>
+ /// <param name="dbSet">The <see cref="DbSet"/> to attach to</param>
+ /// <param name="entity">The current entity</param>
+ /// <param name="dbContext">The coresponding <see cref="DbContext"/></param>
+ public static void AttachAsModified(this DbSet dbSet, object entity, DbContext dbContext)
+ {
+ if (dbSet == null)
+ {
+ throw Error.ArgumentNull("dbSet");
+ }
+ if (entity == null)
+ {
+ throw Error.ArgumentNull("entity");
+ }
+ if (dbContext == null)
+ {
+ throw Error.ArgumentNull("dbContext");
+ }
+
+ DbEntityEntry entityEntry = dbContext.Entry(entity);
+ if (entityEntry.State == EntityState.Detached)
+ {
+ // attach the entity
+ dbSet.Attach(entity);
+ }
+
+ // transition the entity to the modified state
+ entityEntry.State = EntityState.Modified;
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data.EntityFramework/DbDataController.cs b/src/Microsoft.Web.Http.Data.EntityFramework/DbDataController.cs
new file mode 100644
index 00000000..4f3ead5c
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data.EntityFramework/DbDataController.cs
@@ -0,0 +1,320 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Data;
+using System.Data.Entity;
+using System.Data.Entity.Infrastructure;
+using System.Data.Objects;
+using System.Linq;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using Microsoft.Web.Http.Data.EntityFramework.Metadata;
+
+namespace Microsoft.Web.Http.Data.EntityFramework
+{
+ [DbMetadataProvider]
+ public abstract class DbDataController<TContext> : DataController
+ where TContext : DbContext, new()
+ {
+ private TContext _dbContext;
+ private ObjectContext _refreshContext;
+
+ /// <summary>
+ /// Protected constructor for the abstract class.
+ /// </summary>
+ protected DbDataController()
+ {
+ }
+
+ /// <summary>
+ /// Gets the <see cref="ObjectContext"/> used for retrieving store values
+ /// </summary>
+ private ObjectContext RefreshContext
+ {
+ get
+ {
+ if (_refreshContext == null)
+ {
+ DbContext dbContext = CreateDbContext();
+ _refreshContext = (dbContext as IObjectContextAdapter).ObjectContext;
+ }
+ return _refreshContext;
+ }
+ }
+
+ /// <summary>
+ /// Gets the <see cref="DbContext"/>
+ /// </summary>
+ protected TContext DbContext
+ {
+ get
+ {
+ if (_dbContext == null)
+ {
+ _dbContext = CreateDbContext();
+ }
+ return _dbContext;
+ }
+ }
+
+ /// <summary>
+ /// Initializes the <see cref="DbDataController{T}"/>.
+ /// </summary>
+ /// <param name="controllerContext">The <see cref="HttpControllerContext"/> for this <see cref="DataController"/>
+ /// instance. Overrides must call the base method.</param>
+ protected override void Initialize(HttpControllerContext controllerContext)
+ {
+ base.Initialize(controllerContext);
+
+ ObjectContext objectContext = ((IObjectContextAdapter)DbContext).ObjectContext;
+ // We turn this off, since our deserializer isn't going to create
+ // the EF proxy types anyways. Proxies only really work if the entities
+ // are queried on the server.
+ objectContext.ContextOptions.ProxyCreationEnabled = false;
+
+ // Turn off DbContext validation.
+ DbContext.Configuration.ValidateOnSaveEnabled = false;
+
+ // Turn off AutoDetectChanges.
+ DbContext.Configuration.AutoDetectChangesEnabled = false;
+
+ DbContext.Configuration.LazyLoadingEnabled = false;
+ }
+
+ /// <summary>
+ /// Returns the DbContext object.
+ /// </summary>
+ /// <returns>The created DbContext object.</returns>
+ protected virtual TContext CreateDbContext()
+ {
+ return new TContext();
+ }
+
+ /// <summary>
+ /// This method is called to finalize changes after all the operations in the specified changeset
+ /// have been invoked. All changes are committed to the DbContext, and any resulting optimistic
+ /// concurrency errors are processed.
+ /// </summary>
+ /// <returns><c>True</c> if the <see cref="ChangeSet"/> was persisted successfully, <c>false</c> otherwise.</returns>
+ protected override bool PersistChangeSet()
+ {
+ return InvokeSaveChanges(true);
+ }
+
+ /// <summary>
+ /// This method is called to finalize changes after all the operations in the specified changeset
+ /// have been invoked. All changes are committed to the DbContext.
+ /// <remarks>If the submit fails due to concurrency conflicts <see cref="ResolveConflicts"/> will be called.
+ /// If <see cref="ResolveConflicts"/> returns true a single resubmit will be attempted.
+ /// </remarks>
+ /// </summary>
+ /// <param name="conflicts">The list of concurrency conflicts that occurred</param>
+ /// <returns>Returns <c>true</c> if the <see cref="ChangeSet"/> was persisted successfully, <c>false</c> otherwise.</returns>
+ protected virtual bool ResolveConflicts(IEnumerable<DbEntityEntry> conflicts)
+ {
+ return false;
+ }
+
+ /// <summary>
+ /// See <see cref="IDisposable"/>.
+ /// </summary>
+ /// <param name="disposing">A <see cref="Boolean"/> indicating whether or not the instance is currently disposing.</param>
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ if (DbContext != null)
+ {
+ DbContext.Dispose();
+ }
+ if (_refreshContext != null)
+ {
+ _refreshContext.Dispose();
+ }
+ }
+ base.Dispose(disposing);
+ }
+
+ /// <summary>
+ /// Called by PersistChangeSet method to save the changes to the database.
+ /// </summary>
+ /// <param name="retryOnConflict">Flag indicating whether to retry after resolving conflicts.</param>
+ /// <returns><c>true</c> if saved successfully and <c>false</c> otherwise.</returns>
+ private bool InvokeSaveChanges(bool retryOnConflict)
+ {
+ try
+ {
+ DbContext.SaveChanges();
+ }
+ catch (DbUpdateConcurrencyException ex)
+ {
+ // Map the operations that could have caused a conflict to an entity.
+ Dictionary<DbEntityEntry, ChangeSetEntry> operationConflictMap = new Dictionary<DbEntityEntry, ChangeSetEntry>();
+ foreach (DbEntityEntry conflict in ex.Entries)
+ {
+ ChangeSetEntry entry = ChangeSet.ChangeSetEntries.SingleOrDefault(p => Object.ReferenceEquals(p.Entity, conflict.Entity));
+ if (entry == null)
+ {
+ // If we're unable to find the object in our changeset, propagate
+ // the original exception
+ throw;
+ }
+ operationConflictMap.Add(conflict, entry);
+ }
+
+ SetChangeSetConflicts(operationConflictMap);
+
+ // Call out to any user resolve code and resubmit if all conflicts
+ // were resolved
+ if (retryOnConflict && ResolveConflicts(ex.Entries))
+ {
+ // clear the conflics from the entries
+ foreach (ChangeSetEntry entry in ChangeSet.ChangeSetEntries)
+ {
+ entry.StoreEntity = null;
+ entry.ConflictMembers = null;
+ entry.IsDeleteConflict = false;
+ }
+
+ // If all conflicts were resolved attempt a resubmit
+ return InvokeSaveChanges(retryOnConflict: false);
+ }
+
+ // if there was a conflict but no conflict information was
+ // extracted to the individual entries, we need to ensure the
+ // error makes it back to the client
+ if (!ChangeSet.HasError)
+ {
+ throw;
+ }
+ return false;
+ }
+ return true;
+ }
+
+ /// <summary>
+ /// Updates each entry in the ChangeSet with its corresponding conflict info.
+ /// </summary>
+ /// <param name="operationConflictMap">Map of conflicts to their corresponding operations entries.</param>
+ private void SetChangeSetConflicts(Dictionary<DbEntityEntry, ChangeSetEntry> operationConflictMap)
+ {
+ object storeValue;
+ EntityKey refreshEntityKey;
+
+ ObjectContext objectContext = ((IObjectContextAdapter)DbContext).ObjectContext;
+ ObjectStateManager objectStateManager = objectContext.ObjectStateManager;
+ if (objectStateManager == null)
+ {
+ throw Error.InvalidOperation(Resource.ObjectStateManagerNotFoundException, DbContext.GetType().Name);
+ }
+
+ foreach (var conflictEntry in operationConflictMap)
+ {
+ DbEntityEntry entityEntry = conflictEntry.Key;
+ ObjectStateEntry stateEntry = objectStateManager.GetObjectStateEntry(entityEntry.Entity);
+
+ if (stateEntry.State == EntityState.Unchanged)
+ {
+ continue;
+ }
+
+ // Note: we cannot call Refresh StoreWins since this will overwrite Current entity and remove the optimistic concurrency ex.
+ ChangeSetEntry operationInConflict = conflictEntry.Value;
+ refreshEntityKey = RefreshContext.CreateEntityKey(stateEntry.EntitySet.Name, stateEntry.Entity);
+ RefreshContext.TryGetObjectByKey(refreshEntityKey, out storeValue);
+ operationInConflict.StoreEntity = storeValue;
+
+ // StoreEntity will be null if the entity has been deleted in the store (i.e. Delete/Delete conflict)
+ bool isDeleted = (operationInConflict.StoreEntity == null);
+ if (isDeleted)
+ {
+ operationInConflict.IsDeleteConflict = true;
+ }
+ else
+ {
+ // Determine which members are in conflict by comparing original values to the current DB values
+ PropertyDescriptorCollection propDescriptors = TypeDescriptor.GetProperties(operationInConflict.Entity.GetType());
+ List<string> membersInConflict = new List<string>();
+ object originalValue;
+ PropertyDescriptor pd;
+ for (int i = 0; i < stateEntry.OriginalValues.FieldCount; i++)
+ {
+ originalValue = stateEntry.OriginalValues.GetValue(i);
+ if (originalValue is DBNull)
+ {
+ originalValue = null;
+ }
+
+ string propertyName = stateEntry.OriginalValues.GetName(i);
+ pd = propDescriptors[propertyName];
+ if (pd == null)
+ {
+ // This might happen in the case of a private model
+ // member that isn't mapped
+ continue;
+ }
+
+ if (!Object.Equals(originalValue, pd.GetValue(operationInConflict.StoreEntity)))
+ {
+ membersInConflict.Add(pd.Name);
+ }
+ }
+ operationInConflict.ConflictMembers = membersInConflict;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Insert an entity into the <see cref="DbContext" />, ensuring its <see cref="EntityState" /> is <see cref="EntityState.Added" />
+ /// </summary>
+ /// <param name="entity">The entity to be inserted</param>
+ protected virtual void InsertEntity(object entity)
+ {
+ DbEntityEntry dbEntityEntry = DbContext.Entry(entity);
+ if (dbEntityEntry.State != EntityState.Detached)
+ {
+ dbEntityEntry.State = EntityState.Added;
+ }
+ else
+ {
+ DbContext.Set(entity.GetType()).Add(entity);
+ }
+ }
+
+ /// <summary>
+ /// Update an entity in the <see cref="DbContext" />, ensuring it is treated as a modified entity
+ /// </summary>
+ /// <param name="entity">The entity to be updated</param>
+ protected virtual void UpdateEntity(object entity)
+ {
+ object original = ChangeSet.GetOriginal(entity);
+ DbSet dbSet = DbContext.Set(entity.GetType());
+ if (original == null)
+ {
+ dbSet.AttachAsModified(entity, DbContext);
+ }
+ else
+ {
+ dbSet.AttachAsModified(entity, original, DbContext);
+ }
+ }
+
+ /// <summary>
+ /// Delete an entity from the <see cref="DbContext" />, ensuring that its <see cref="EntityState" /> is <see cref="EntityState.Deleted" />
+ /// </summary>
+ /// <param name="entity">The entity to be deleted</param>
+ protected virtual void DeleteEntity(object entity)
+ {
+ DbEntityEntry entityEntry = DbContext.Entry(entity);
+ if (entityEntry.State != EntityState.Deleted)
+ {
+ entityEntry.State = EntityState.Deleted;
+ }
+ else
+ {
+ DbContext.Set(entity.GetType()).Attach(entity);
+ DbContext.Set(entity.GetType()).Remove(entity);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data.EntityFramework/GlobalSuppressions.cs b/src/Microsoft.Web.Http.Data.EntityFramework/GlobalSuppressions.cs
new file mode 100644
index 00000000..122bbbe5
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data.EntityFramework/GlobalSuppressions.cs
@@ -0,0 +1,14 @@
+// This file is used by Code Analysis to maintain SuppressMessage
+// attributes that are applied to this project.
+// Project-level suppressions either have no target or are given
+// a specific target and scoped to a namespace, type, member, etc.
+//
+// To add a suppression to this file, right-click the message in the
+// Error List, point to "Suppress Message(s)", and click
+// "In Project Suppression File".
+// You do not need to add suppressions to this file manually.
+
+using System.Diagnostics.CodeAnalysis;
+
+[assembly: SuppressMessage("Microsoft.Design", "CA2210:AssembliesShouldHaveValidStrongNames", Justification = "Assembly is delay signed")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "Microsoft.Web.Http.Data.EntityFramework.Metadata", Justification = "These types are in their own namespace to match folder structure.")]
diff --git a/src/Microsoft.Web.Http.Data.EntityFramework/LinqToEntitiesDataController.cs b/src/Microsoft.Web.Http.Data.EntityFramework/LinqToEntitiesDataController.cs
new file mode 100644
index 00000000..d3494a26
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data.EntityFramework/LinqToEntitiesDataController.cs
@@ -0,0 +1,310 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Data;
+using System.Data.Objects;
+using System.Linq;
+using System.Web.Http.Controllers;
+using Microsoft.Web.Http.Data.EntityFramework.Metadata;
+
+namespace Microsoft.Web.Http.Data.EntityFramework
+{
+ /// <summary>
+ /// Base class for DataControllers operating on LINQ To Entities data models
+ /// </summary>
+ /// <typeparam name="TContext">The Type of the LINQ To Entities ObjectContext</typeparam>
+ [LinqToEntitiesMetadataProvider]
+ public abstract class LinqToEntitiesDataController<TContext> : DataController where TContext : ObjectContext, new()
+ {
+ private TContext _objectContext;
+ private TContext _refreshContext;
+
+ /// <summary>
+ /// Protected constructor because this is an abstract class
+ /// </summary>
+ protected LinqToEntitiesDataController()
+ {
+ }
+
+ /// <summary>
+ /// Gets the <see cref="ObjectContext"/>
+ /// </summary>
+ protected internal TContext ObjectContext
+ {
+ get
+ {
+ if (_objectContext == null)
+ {
+ _objectContext = CreateObjectContext();
+ }
+ return _objectContext;
+ }
+ }
+
+ /// <summary>
+ /// Gets the <see cref="ObjectContext"/> used by retrieving store values
+ /// </summary>
+ private ObjectContext RefreshContext
+ {
+ get
+ {
+ if (_refreshContext == null)
+ {
+ _refreshContext = CreateObjectContext();
+ }
+ return _refreshContext;
+ }
+ }
+
+ /// <summary>
+ /// Initializes this <see cref="DataController"/>.
+ /// </summary>
+ /// <param name="controllerContext">The <see cref="HttpControllerContext"/> for this <see cref="DataController"/>
+ /// instance. Overrides must call the base method.</param>
+ protected override void Initialize(HttpControllerContext controllerContext)
+ {
+ base.Initialize(controllerContext);
+
+ // TODO: should we be turning this off categorically? Can we do this only
+ // for queries?
+ ObjectContext.ContextOptions.LazyLoadingEnabled = false;
+
+ // We turn this off, since our deserializer isn't going to create
+ // the EF proxy types anyways. Proxies only really work if the entities
+ // are queried on the server.
+ ObjectContext.ContextOptions.ProxyCreationEnabled = false;
+ }
+
+ /// <summary>
+ /// Creates and returns the <see cref="ObjectContext"/> instance that will
+ /// be used by this provider.
+ /// </summary>
+ /// <returns>The ObjectContext</returns>
+ protected virtual TContext CreateObjectContext()
+ {
+ return new TContext();
+ }
+
+ /// <summary>
+ /// See <see cref="IDisposable"/>.
+ /// </summary>
+ /// <param name="disposing">A <see cref="Boolean"/> indicating whether or not the instance is currently disposing.</param>
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ if (_objectContext != null)
+ {
+ _objectContext.Dispose();
+ }
+ if (_refreshContext != null)
+ {
+ _refreshContext.Dispose();
+ }
+ }
+ base.Dispose(disposing);
+ }
+
+ /// <summary>
+ /// This method is called to finalize changes after all the operations in the specified changeset
+ /// have been invoked. All changes are committed to the ObjectContext, and any resulting optimistic
+ /// concurrency errors are processed.
+ /// </summary>
+ /// <returns>True if the <see cref="ChangeSet"/> was persisted successfully, false otherwise.</returns>
+ protected override bool PersistChangeSet()
+ {
+ return InvokeSaveChanges(true);
+ }
+
+ private bool InvokeSaveChanges(bool retryOnConflict)
+ {
+ try
+ {
+ ObjectContext.SaveChanges();
+ }
+ catch (OptimisticConcurrencyException ex)
+ {
+ // Map the operations that could have caused a conflict to an entity.
+ Dictionary<ObjectStateEntry, ChangeSetEntry> operationConflictMap = new Dictionary<ObjectStateEntry, ChangeSetEntry>();
+ foreach (ObjectStateEntry conflict in ex.StateEntries)
+ {
+ ChangeSetEntry entry = ChangeSet.ChangeSetEntries.SingleOrDefault(p => Object.ReferenceEquals(p.Entity, conflict.Entity));
+ if (entry == null)
+ {
+ // If we're unable to find the object in our changeset, propagate
+ // the original exception
+ throw;
+ }
+ operationConflictMap.Add(conflict, entry);
+ }
+
+ SetChangeSetConflicts(operationConflictMap);
+
+ // Call out to any user resolve code and resubmit if all conflicts
+ // were resolved
+ if (retryOnConflict && ResolveConflicts(ex.StateEntries))
+ {
+ // clear the conflics from the entries
+ foreach (ChangeSetEntry entry in ChangeSet.ChangeSetEntries)
+ {
+ entry.StoreEntity = null;
+ entry.ConflictMembers = null;
+ entry.IsDeleteConflict = false;
+ }
+
+ // If all conflicts were resolved attempt a resubmit
+ return InvokeSaveChanges(retryOnConflict: false);
+ }
+
+ // if there was a conflict but no conflict information was
+ // extracted to the individual entries, we need to ensure the
+ // error makes it back to the client
+ if (!ChangeSet.HasError)
+ {
+ throw;
+ }
+
+ return false;
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// This method is called to finalize changes after all the operations in the specified changeset
+ /// have been invoked. All changes are committed to the ObjectContext.
+ /// <remarks>If the submit fails due to concurrency conflicts <see cref="ResolveConflicts"/> will be called.
+ /// If <see cref="ResolveConflicts"/> returns true a single resubmit will be attempted.
+ /// </remarks>
+ /// </summary>
+ /// <param name="conflicts">The list of concurrency conflicts that occurred</param>
+ /// <returns>Returns <c>true</c> if the <see cref="ChangeSet"/> was persisted successfully, <c>false</c> otherwise.</returns>
+ protected virtual bool ResolveConflicts(IEnumerable<ObjectStateEntry> conflicts)
+ {
+ return false;
+ }
+
+ /// <summary>
+ /// Insert an entity into the <see cref="ObjectContext" />, ensuring its <see cref="EntityState" /> is <see cref="EntityState.Added" />
+ /// </summary>
+ /// <typeparam name="TEntity">The entity type</typeparam>
+ /// <param name="entity">The entity to be inserted</param>
+ protected virtual void InsertEntity<TEntity>(TEntity entity) where TEntity : class
+ {
+ ObjectStateEntry stateEntry;
+ if (ObjectContext.ObjectStateManager.TryGetObjectStateEntry(entity, out stateEntry) &&
+ stateEntry.State != EntityState.Added)
+ {
+ ObjectContext.ObjectStateManager.ChangeObjectState(entity, EntityState.Added);
+ }
+ else
+ {
+ ObjectContext.CreateObjectSet<TEntity>().AddObject(entity);
+ }
+ }
+
+ /// <summary>
+ /// Update an entity in the <see cref="ObjectContext" />, ensuring it is treated as a modified entity
+ /// </summary>
+ /// <typeparam name="TEntity">The entity type</typeparam>
+ /// <param name="entity">The entity to be updated</param>
+ protected virtual void UpdateEntity<TEntity>(TEntity entity) where TEntity : class
+ {
+ TEntity original = ChangeSet.GetOriginal(entity);
+ ObjectSet<TEntity> objectSet = ObjectContext.CreateObjectSet<TEntity>();
+ if (original == null)
+ {
+ objectSet.AttachAsModified(entity);
+ }
+ else
+ {
+ objectSet.AttachAsModified(entity, original);
+ }
+ }
+
+ /// <summary>
+ /// Delete an entity from the <see cref="ObjectContext" />, ensuring that its <see cref="EntityState" /> is <see cref="EntityState.Deleted" />
+ /// </summary>
+ /// <typeparam name="TEntity">The entity type</typeparam>
+ /// <param name="entity">The entity to be deleted</param>
+ protected virtual void DeleteEntity<TEntity>(TEntity entity) where TEntity : class
+ {
+ ObjectStateEntry stateEntry;
+ if (ObjectContext.ObjectStateManager.TryGetObjectStateEntry(entity, out stateEntry) &&
+ stateEntry.State != EntityState.Deleted)
+ {
+ ObjectContext.ObjectStateManager.ChangeObjectState(entity, EntityState.Deleted);
+ }
+ else
+ {
+ ObjectSet<TEntity> objectSet = ObjectContext.CreateObjectSet<TEntity>();
+ objectSet.Attach(entity);
+ objectSet.DeleteObject(entity);
+ }
+ }
+
+ /// <summary>
+ /// Updates each entry in the ChangeSet with its corresponding conflict info.
+ /// </summary>
+ /// <param name="operationConflictMap">Map of conflicts to their corresponding operations entries.</param>
+ private void SetChangeSetConflicts(Dictionary<ObjectStateEntry, ChangeSetEntry> operationConflictMap)
+ {
+ object storeValue;
+ EntityKey refreshEntityKey;
+
+ foreach (var conflictEntry in operationConflictMap)
+ {
+ ObjectStateEntry stateEntry = conflictEntry.Key;
+
+ if (stateEntry.State == EntityState.Unchanged)
+ {
+ continue;
+ }
+
+ // Note: we cannot call Refresh StoreWins since this will overwrite Current entity and remove the optimistic concurrency ex.
+ ChangeSetEntry operationInConflict = conflictEntry.Value;
+ refreshEntityKey = RefreshContext.CreateEntityKey(stateEntry.EntitySet.Name, stateEntry.Entity);
+ RefreshContext.TryGetObjectByKey(refreshEntityKey, out storeValue);
+ operationInConflict.StoreEntity = storeValue;
+
+ // StoreEntity will be null if the entity has been deleted in the store (i.e. Delete/Delete conflict)
+ bool isDeleted = (operationInConflict.StoreEntity == null);
+ if (isDeleted)
+ {
+ operationInConflict.IsDeleteConflict = true;
+ }
+ else
+ {
+ // Determine which members are in conflict by comparing original values to the current DB values
+ PropertyDescriptorCollection propDescriptors = TypeDescriptor.GetProperties(operationInConflict.Entity.GetType());
+ List<string> membersInConflict = new List<string>();
+ object originalValue;
+ PropertyDescriptor pd;
+ for (int i = 0; i < stateEntry.OriginalValues.FieldCount; i++)
+ {
+ originalValue = stateEntry.OriginalValues.GetValue(i);
+ if (originalValue is DBNull)
+ {
+ originalValue = null;
+ }
+
+ string propertyName = stateEntry.OriginalValues.GetName(i);
+ pd = propDescriptors[propertyName];
+ if (pd == null)
+ {
+ // This might happen in the case of a private model
+ // member that isn't mapped
+ continue;
+ }
+
+ if (!Object.Equals(originalValue, pd.GetValue(operationInConflict.StoreEntity)))
+ {
+ membersInConflict.Add(pd.Name);
+ }
+ }
+ operationInConflict.ConflictMembers = membersInConflict;
+ }
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data.EntityFramework/Metadata/AssociationInfo.cs b/src/Microsoft.Web.Http.Data.EntityFramework/Metadata/AssociationInfo.cs
new file mode 100644
index 00000000..80bbd5e5
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data.EntityFramework/Metadata/AssociationInfo.cs
@@ -0,0 +1,34 @@
+namespace Microsoft.Web.Http.Data.EntityFramework.Metadata
+{
+ /// <summary>
+ /// Information about an Association
+ /// </summary>
+ internal sealed class AssociationInfo
+ {
+ /// <summary>
+ /// The name of the association
+ /// </summary>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// The key members on the FK side of the association
+ /// </summary>
+ public string[] ThisKey { get; set; }
+
+ /// <summary>
+ /// The key members on the non-FK side of the association
+ /// </summary>
+ public string[] OtherKey { get; set; }
+
+ /// <summary>
+ /// The foreign key role name for this association
+ /// </summary>
+ public string FKRole { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this association can have a
+ /// multiplicity of zero
+ /// </summary>
+ public bool IsRequired { get; set; }
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data.EntityFramework/Metadata/DbMetadataProviderAttribute.cs b/src/Microsoft.Web.Http.Data.EntityFramework/Metadata/DbMetadataProviderAttribute.cs
new file mode 100644
index 00000000..8228a1d2
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data.EntityFramework/Metadata/DbMetadataProviderAttribute.cs
@@ -0,0 +1,92 @@
+using System;
+using System.Data.Entity;
+using System.Web.Http.Common;
+using Microsoft.Web.Http.Data.Metadata;
+
+namespace Microsoft.Web.Http.Data.EntityFramework.Metadata
+{
+ /// <summary>
+ /// Attribute applied to a <see cref="DbDataController{DbContext}"/> that exposes LINQ to Entities mapped
+ /// Types.
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
+ public sealed class DbMetadataProviderAttribute : MetadataProviderAttribute
+ {
+ private Type _dbContextType;
+
+ /// <summary>
+ /// Default constructor. Using this constructor, the Type of the LINQ To Entities
+ /// DbContext will be inferred from the <see cref="DataController"/> the
+ /// attribute is applied to.
+ /// </summary>
+ public DbMetadataProviderAttribute()
+ : base(typeof(LinqToEntitiesMetadataProvider))
+ {
+ }
+
+ /// <summary>
+ /// Constructs an attribute for the specified LINQ To Entities
+ /// DbContext Type.
+ /// </summary>
+ /// <param name="dbContextType">The LINQ To Entities ObjectContext Type.</param>
+ public DbMetadataProviderAttribute(Type dbContextType)
+ : base(typeof(LinqToEntitiesMetadataProvider))
+ {
+ _dbContextType = dbContextType;
+ }
+
+ /// <summary>
+ /// The Linq To Entities DbContext Type.
+ /// </summary>
+ public Type DbContextType
+ {
+ get { return _dbContextType; }
+ }
+
+ /// <summary>
+ /// This method creates an instance of the <see cref="MetadataProvider"/>.
+ /// </summary>
+ /// <param name="controllerType">The <see cref="DataController"/> Type to create a metadata provider for.</param>
+ /// <param name="parent">The existing parent metadata provider.</param>
+ /// <returns>The metadata provider.</returns>
+ public override MetadataProvider CreateProvider(Type controllerType, MetadataProvider parent)
+ {
+ if (controllerType == null)
+ {
+ throw Error.ArgumentNull("controllerType");
+ }
+
+ if (_dbContextType == null)
+ {
+ _dbContextType = GetContextType(controllerType);
+ }
+
+ if (!typeof(DbContext).IsAssignableFrom(_dbContextType))
+ {
+ throw Error.InvalidOperation(Resource.InvalidDbMetadataProviderSpecification, _dbContextType);
+ }
+
+ return new LinqToEntitiesMetadataProvider(_dbContextType, parent, true);
+ }
+
+ /// <summary>
+ /// Extracts the context type from the specified <paramref name="dataControllerType"/>.
+ /// </summary>
+ /// <param name="dataControllerType">A LINQ to Entities data controller type.</param>
+ /// <returns>The type of the object context.</returns>
+ private static Type GetContextType(Type dataControllerType)
+ {
+ Type efDataControllerType = dataControllerType.BaseType;
+ while (!efDataControllerType.IsGenericType || efDataControllerType.GetGenericTypeDefinition() != typeof(DbDataController<>))
+ {
+ if (efDataControllerType == typeof(object))
+ {
+ throw Error.InvalidOperation(Resource.InvalidMetadataProviderSpecification, typeof(DbMetadataProviderAttribute).Name, dataControllerType.Name, typeof(DbDataController<>).Name);
+ }
+ efDataControllerType = efDataControllerType.BaseType;
+ }
+
+ return efDataControllerType.GetGenericArguments()[0];
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data.EntityFramework/Metadata/LinqToEntitiesMetadataProvider.cs b/src/Microsoft.Web.Http.Data.EntityFramework/Metadata/LinqToEntitiesMetadataProvider.cs
new file mode 100644
index 00000000..364701bf
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data.EntityFramework/Metadata/LinqToEntitiesMetadataProvider.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Data.Metadata.Edm;
+using Microsoft.Web.Http.Data.Metadata;
+
+namespace Microsoft.Web.Http.Data.EntityFramework.Metadata
+{
+ internal class LinqToEntitiesMetadataProvider : MetadataProvider
+ {
+ private static ConcurrentDictionary<Type, LinqToEntitiesTypeDescriptionContext> _tdpContextMap = new ConcurrentDictionary<Type, LinqToEntitiesTypeDescriptionContext>();
+ private readonly LinqToEntitiesTypeDescriptionContext _typeDescriptionContext;
+ private readonly bool _isDbContext;
+ private Dictionary<Type, ICustomTypeDescriptor> _descriptors = new Dictionary<Type, ICustomTypeDescriptor>();
+
+ public LinqToEntitiesMetadataProvider(Type contextType, MetadataProvider parent, bool isDbContext)
+ : base(parent)
+ {
+ _isDbContext = isDbContext;
+
+ _typeDescriptionContext = _tdpContextMap.GetOrAdd(contextType, type =>
+ {
+ // create and cache a context for this provider type
+ return new LinqToEntitiesTypeDescriptionContext(contextType, _isDbContext);
+ });
+ }
+
+ /// <summary>
+ /// Returns a custom type descriptor for the specified type (either an entity or complex type).
+ /// </summary>
+ /// <param name="objectType">Type of object for which we need the descriptor</param>
+ /// <param name="parent">The parent type descriptor</param>
+ /// <returns>Custom type description for the specified type</returns>
+ public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, ICustomTypeDescriptor parent)
+ {
+ // No need to deal with concurrency... Worst case scenario we have multiple
+ // instances of this thing.
+ ICustomTypeDescriptor td = null;
+ if (!_descriptors.TryGetValue(objectType, out td))
+ {
+ // call into base so the TDs are chained
+ parent = base.GetTypeDescriptor(objectType, parent);
+
+ StructuralType edmType = _typeDescriptionContext.GetEdmType(objectType);
+ if (edmType != null &&
+ (edmType.BuiltInTypeKind == BuiltInTypeKind.EntityType || edmType.BuiltInTypeKind == BuiltInTypeKind.ComplexType))
+ {
+ // only add an LTE TypeDescriptor if the type is an EF Entity or ComplexType
+ td = new LinqToEntitiesTypeDescriptor(_typeDescriptionContext, edmType, parent);
+ }
+ else
+ {
+ td = parent;
+ }
+
+ _descriptors[objectType] = td;
+ }
+
+ return td;
+ }
+
+ public override bool LookUpIsEntityType(Type type)
+ {
+ StructuralType edmType = _typeDescriptionContext.GetEdmType(type);
+ if (edmType != null && edmType.BuiltInTypeKind == BuiltInTypeKind.EntityType)
+ {
+ return true;
+ }
+ return false;
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data.EntityFramework/Metadata/LinqToEntitiesMetadataProviderAttribute.cs b/src/Microsoft.Web.Http.Data.EntityFramework/Metadata/LinqToEntitiesMetadataProviderAttribute.cs
new file mode 100644
index 00000000..336b1cfb
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data.EntityFramework/Metadata/LinqToEntitiesMetadataProviderAttribute.cs
@@ -0,0 +1,92 @@
+using System;
+using System.Data.Objects;
+using System.Web.Http.Common;
+using Microsoft.Web.Http.Data.Metadata;
+
+namespace Microsoft.Web.Http.Data.EntityFramework.Metadata
+{
+ /// <summary>
+ /// Attribute applied to a <see cref="DataController"/> that exposes LINQ to Entities mapped
+ /// Types.
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
+ public sealed class LinqToEntitiesMetadataProviderAttribute : MetadataProviderAttribute
+ {
+ private Type _objectContextType;
+
+ /// <summary>
+ /// Default constructor. Using this constructor, the Type of the LINQ To Entities
+ /// ObjectContext will be inferred from the <see cref="DataController"/> the
+ /// attribute is applied to.
+ /// </summary>
+ public LinqToEntitiesMetadataProviderAttribute()
+ : base(typeof(LinqToEntitiesMetadataProvider))
+ {
+ }
+
+ /// <summary>
+ /// Constructs an attribute for the specified LINQ To Entities
+ /// ObjectContext Type.
+ /// </summary>
+ /// <param name="objectContextType">The LINQ To Entities ObjectContext Type.</param>
+ public LinqToEntitiesMetadataProviderAttribute(Type objectContextType)
+ : base(typeof(LinqToEntitiesMetadataProvider))
+ {
+ _objectContextType = objectContextType;
+ }
+
+ /// <summary>
+ /// The Linq To Entities ObjectContext Type.
+ /// </summary>
+ public Type ObjectContextType
+ {
+ get { return _objectContextType; }
+ }
+
+ /// <summary>
+ /// This method creates an instance of the <see cref="MetadataProvider"/>.
+ /// </summary>
+ /// <param name="controllerType">The <see cref="DataController"/> Type to create a metadata provider for.</param>
+ /// <param name="parent">The existing parent metadata provider.</param>
+ /// <returns>The metadata provider.</returns>
+ public override MetadataProvider CreateProvider(Type controllerType, MetadataProvider parent)
+ {
+ if (controllerType == null)
+ {
+ throw Error.ArgumentNull("controllerType");
+ }
+
+ if (_objectContextType == null)
+ {
+ _objectContextType = GetContextType(controllerType);
+ }
+
+ if (!typeof(ObjectContext).IsAssignableFrom(_objectContextType))
+ {
+ throw Error.InvalidOperation(Resource.InvalidLinqToEntitiesMetadataProviderSpecification, _objectContextType);
+ }
+
+ return new LinqToEntitiesMetadataProvider(_objectContextType, parent, false);
+ }
+
+ /// <summary>
+ /// Extracts the context type from the specified <paramref name="dataControllerType"/>.
+ /// </summary>
+ /// <param name="dataControllerType">A LINQ to Entities data controller type.</param>
+ /// <returns>The type of the object context.</returns>
+ private static Type GetContextType(Type dataControllerType)
+ {
+ Type efDataControllerType = dataControllerType.BaseType;
+ while (!efDataControllerType.IsGenericType || efDataControllerType.GetGenericTypeDefinition() != typeof(LinqToEntitiesDataController<>))
+ {
+ if (efDataControllerType == typeof(object))
+ {
+ throw Error.InvalidOperation(Resource.InvalidMetadataProviderSpecification, typeof(LinqToEntitiesMetadataProviderAttribute).Name, dataControllerType.Name, typeof(LinqToEntitiesDataController<>).Name);
+ }
+ efDataControllerType = efDataControllerType.BaseType;
+ }
+
+ return efDataControllerType.GetGenericArguments()[0];
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data.EntityFramework/Metadata/LinqToEntitiesTypeDescriptionContext.cs b/src/Microsoft.Web.Http.Data.EntityFramework/Metadata/LinqToEntitiesTypeDescriptionContext.cs
new file mode 100644
index 00000000..209221fd
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data.EntityFramework/Metadata/LinqToEntitiesTypeDescriptionContext.cs
@@ -0,0 +1,148 @@
+using System;
+using System.Collections.Concurrent;
+using System.ComponentModel.DataAnnotations;
+using System.Data.Metadata.Edm;
+using System.Globalization;
+using System.Linq;
+using System.Web.Http.Common;
+
+namespace Microsoft.Web.Http.Data.EntityFramework.Metadata
+{
+ /// <summary>
+ /// Metadata context for LINQ To Entities controllers
+ /// </summary>
+ internal class LinqToEntitiesTypeDescriptionContext : TypeDescriptionContextBase
+ {
+ private readonly Type _contextType;
+ private readonly bool _isDbContext;
+ private ConcurrentDictionary<string, AssociationInfo> _associationMap = new ConcurrentDictionary<string, AssociationInfo>();
+ private MetadataWorkspace _metadataWorkspace;
+
+ /// <summary>
+ /// Constructor that accepts a LINQ To Entities context type
+ /// </summary>
+ /// <param name="contextType">The ObjectContext Type</param>
+ /// <param name="isDbContext">Set to <c>true</c> if context is a database context.</param>
+ public LinqToEntitiesTypeDescriptionContext(Type contextType, bool isDbContext)
+ {
+ if (contextType == null)
+ {
+ throw Error.ArgumentNull("contextType");
+ }
+ _contextType = contextType;
+ _isDbContext = isDbContext;
+ }
+
+ /// <summary>
+ /// Gets the MetadataWorkspace for the context
+ /// </summary>
+ public MetadataWorkspace MetadataWorkspace
+ {
+ get
+ {
+ if (_metadataWorkspace == null)
+ {
+ // we only support embedded mappings
+ _metadataWorkspace = MetadataWorkspaceUtilities.CreateMetadataWorkspace(_contextType, _isDbContext);
+ }
+ return _metadataWorkspace;
+ }
+ }
+
+ /// <summary>
+ /// Returns the <see cref="StructuralType"/> that corresponds to the given CLR type
+ /// </summary>
+ /// <param name="clrType">The CLR type</param>
+ /// <returns>The StructuralType that corresponds to the given CLR type</returns>
+ public StructuralType GetEdmType(Type clrType)
+ {
+ return ObjectContextUtilities.GetEdmType(MetadataWorkspace, clrType);
+ }
+
+ /// <summary>
+ /// Returns the association information for the specified navigation property.
+ /// </summary>
+ /// <param name="navigationProperty">The navigation property to return association information for</param>
+ /// <returns>The association info</returns>
+ internal AssociationInfo GetAssociationInfo(NavigationProperty navigationProperty)
+ {
+ return _associationMap.GetOrAdd(navigationProperty.RelationshipType.FullName, associationName =>
+ {
+ AssociationType associationType = (AssociationType)navigationProperty.RelationshipType;
+
+ if (!associationType.ReferentialConstraints.Any())
+ {
+ // We only support EF models where FK info is part of the model.
+ throw Error.NotSupported(Resource.LinqToEntitiesProvider_UnableToRetrieveAssociationInfo, associationName);
+ }
+
+ string toRoleName = associationType.ReferentialConstraints[0].ToRole.Name;
+ AssociationInfo associationInfo = new AssociationInfo()
+ {
+ FKRole = toRoleName,
+ Name = GetAssociationName(navigationProperty, toRoleName),
+ ThisKey = associationType.ReferentialConstraints[0].ToProperties.Select(p => p.Name).ToArray(),
+ OtherKey = associationType.ReferentialConstraints[0].FromProperties.Select(p => p.Name).ToArray(),
+ IsRequired = associationType.RelationshipEndMembers[0].RelationshipMultiplicity == RelationshipMultiplicity.One
+ };
+
+ return associationInfo;
+ });
+ }
+
+ /// <summary>
+ /// Creates an AssociationAttribute for the specified navigation property
+ /// </summary>
+ /// <param name="navigationProperty">The navigation property that corresponds to the association (it identifies the end points)</param>
+ /// <returns>A new AssociationAttribute that describes the given navigation property association</returns>
+ internal AssociationAttribute CreateAssociationAttribute(NavigationProperty navigationProperty)
+ {
+ AssociationInfo assocInfo = GetAssociationInfo(navigationProperty);
+ bool isForeignKey = navigationProperty.FromEndMember.Name == assocInfo.FKRole;
+ string thisKey;
+ string otherKey;
+ if (isForeignKey)
+ {
+ thisKey = String.Join(",", assocInfo.ThisKey);
+ otherKey = String.Join(",", assocInfo.OtherKey);
+ }
+ else
+ {
+ otherKey = String.Join(",", assocInfo.ThisKey);
+ thisKey = String.Join(",", assocInfo.OtherKey);
+ }
+
+ AssociationAttribute assocAttrib = new AssociationAttribute(assocInfo.Name, thisKey, otherKey);
+ assocAttrib.IsForeignKey = isForeignKey;
+ return assocAttrib;
+ }
+
+ /// <summary>
+ /// Returns a unique association name for the specified navigation property.
+ /// </summary>
+ /// <param name="navigationProperty">The navigation property</param>
+ /// <param name="foreignKeyRoleName">The foreign key role name for the property's association</param>
+ /// <returns>A unique association name for the specified navigation property.</returns>
+ private string GetAssociationName(NavigationProperty navigationProperty, string foreignKeyRoleName)
+ {
+ RelationshipEndMember fromMember = navigationProperty.FromEndMember;
+ RelationshipEndMember toMember = navigationProperty.ToEndMember;
+
+ RefType toRefType = toMember.TypeUsage.EdmType as RefType;
+ EntityType toEntityType = toRefType.ElementType as EntityType;
+
+ RefType fromRefType = fromMember.TypeUsage.EdmType as RefType;
+ EntityType fromEntityType = fromRefType.ElementType as EntityType;
+
+ bool isForeignKey = navigationProperty.FromEndMember.Name == foreignKeyRoleName;
+ string fromTypeName = isForeignKey ? fromEntityType.Name : toEntityType.Name;
+ string toTypeName = isForeignKey ? toEntityType.Name : fromEntityType.Name;
+
+ // names are always formatted non-FK side type name followed by FK side type name
+ string associationName = String.Format(CultureInfo.InvariantCulture, "{0}_{1}", toTypeName, fromTypeName);
+ associationName = MakeUniqueName(associationName, _associationMap.Values.Select(p => p.Name));
+
+ return associationName;
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data.EntityFramework/Metadata/LinqToEntitiesTypeDescriptor.cs b/src/Microsoft.Web.Http.Data.EntityFramework/Metadata/LinqToEntitiesTypeDescriptor.cs
new file mode 100644
index 00000000..fd0aaa6f
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data.EntityFramework/Metadata/LinqToEntitiesTypeDescriptor.cs
@@ -0,0 +1,271 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.Data;
+using System.Data.Metadata.Edm;
+using System.Data.Objects.DataClasses;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+
+namespace Microsoft.Web.Http.Data.EntityFramework.Metadata
+{
+ /// <summary>
+ /// CustomTypeDescriptor for LINQ To Entities
+ /// </summary>
+ internal class LinqToEntitiesTypeDescriptor : TypeDescriptorBase
+ {
+ private readonly LinqToEntitiesTypeDescriptionContext _typeDescriptionContext;
+ private readonly StructuralType _edmType;
+ private readonly EdmMember _timestampMember;
+ private readonly HashSet<EdmMember> _foreignKeyMembers;
+ private readonly bool _keyIsEditable;
+
+ /// <summary>
+ /// Constructor taking a metadata context, an structural type, and a parent custom type descriptor
+ /// </summary>
+ /// <param name="typeDescriptionContext">The <see cref="LinqToEntitiesTypeDescriptionContext"/> context.</param>
+ /// <param name="edmType">The <see cref="StructuralType"/> type (can be an entity or complex type).</param>
+ /// <param name="parent">The parent custom type descriptor.</param>
+ public LinqToEntitiesTypeDescriptor(LinqToEntitiesTypeDescriptionContext typeDescriptionContext, StructuralType edmType, ICustomTypeDescriptor parent)
+ : base(parent)
+ {
+ _typeDescriptionContext = typeDescriptionContext;
+ _edmType = edmType;
+
+ EdmMember[] timestampMembers = _edmType.Members.Where(p => ObjectContextUtilities.IsConcurrencyTimestamp(p)).ToArray();
+ if (timestampMembers.Length == 1)
+ {
+ _timestampMember = timestampMembers[0];
+ }
+
+ if (edmType.BuiltInTypeKind == BuiltInTypeKind.EntityType)
+ {
+ // if any FK member of any association is also part of the primary key, then the key cannot be marked
+ // Editable(false)
+ EntityType entityType = (EntityType)edmType;
+ _foreignKeyMembers = new HashSet<EdmMember>(entityType.NavigationProperties.SelectMany(p => p.GetDependentProperties()));
+ foreach (EdmProperty foreignKeyMember in _foreignKeyMembers)
+ {
+ if (entityType.KeyMembers.Contains(foreignKeyMember))
+ {
+ _keyIsEditable = true;
+ break;
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets the metadata context
+ /// </summary>
+ public LinqToEntitiesTypeDescriptionContext TypeDescriptionContext
+ {
+ get { return _typeDescriptionContext; }
+ }
+
+ /// <summary>
+ /// Gets the Edm type
+ /// </summary>
+ private StructuralType EdmType
+ {
+ get { return _edmType; }
+ }
+
+ /// <summary>
+ /// Returns a collection of all the <see cref="Attribute"/>s we infer from the metadata associated
+ /// with the metadata member corresponding to the given property descriptor
+ /// </summary>
+ /// <param name="pd">A <see cref="PropertyDescriptor"/> to examine</param>
+ /// <returns>A collection of attributes inferred from the metadata in the given descriptor.</returns>
+ [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "TODO refactor")]
+ [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "TODO refactor")]
+ protected override IEnumerable<Attribute> GetMemberAttributes(PropertyDescriptor pd)
+ {
+ List<Attribute> attributes = new List<Attribute>();
+
+ // Exclude any EntityState, EntityReference, etc. members
+ if (ShouldExcludeEntityMember(pd))
+ {
+ // for these members, we don't want to do any attribute inference
+ return attributes.ToArray();
+ }
+
+ EditableAttribute editableAttribute = null;
+ bool inferRoundtripOriginalAttribute = false;
+
+ bool hasKeyAttribute = (pd.Attributes[typeof(KeyAttribute)] != null);
+ bool isEntity = EdmType.BuiltInTypeKind == BuiltInTypeKind.EntityType;
+ if (isEntity)
+ {
+ EntityType entityType = (EntityType)EdmType;
+ EdmMember keyMember = entityType.KeyMembers.SingleOrDefault(k => k.Name == pd.Name);
+ if (keyMember != null && !hasKeyAttribute)
+ {
+ attributes.Add(new KeyAttribute());
+ hasKeyAttribute = true;
+ }
+ }
+
+ EdmProperty member = EdmType.Members.SingleOrDefault(p => p.Name == pd.Name) as EdmProperty;
+ if (member != null)
+ {
+ if (hasKeyAttribute)
+ {
+ // key members must always be roundtripped
+ inferRoundtripOriginalAttribute = true;
+
+ // key members that aren't also FK members are non-editable (but allow an initial value)
+ if (!_keyIsEditable)
+ {
+ editableAttribute = new EditableAttribute(false) { AllowInitialValue = true };
+ }
+ }
+
+ // Check if the member is DB generated and add the DatabaseGeneratedAttribute to it if not already present.
+ if (pd.Attributes[typeof(DatabaseGeneratedAttribute)] == null)
+ {
+ MetadataProperty md = ObjectContextUtilities.GetStoreGeneratedPattern(member);
+ if (md != null)
+ {
+ if ((string)md.Value == "Computed")
+ {
+ attributes.Add(new DatabaseGeneratedAttribute(DatabaseGeneratedOption.Computed));
+ }
+ else if ((string)md.Value == "Identity")
+ {
+ attributes.Add(new DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity));
+ }
+ }
+ }
+
+ // Add implicit ConcurrencyCheck attribute to metadata if ConcurrencyMode is anything other than ConcurrencyMode.None
+ Facet facet = member.TypeUsage.Facets.SingleOrDefault(p => p.Name == "ConcurrencyMode");
+ if (facet != null && facet.Value != null && (ConcurrencyMode)facet.Value != ConcurrencyMode.None &&
+ pd.Attributes[typeof(ConcurrencyCheckAttribute)] == null)
+ {
+ attributes.Add(new ConcurrencyCheckAttribute());
+ inferRoundtripOriginalAttribute = true;
+ }
+
+ bool isStringType = pd.PropertyType == typeof(string) || pd.PropertyType == typeof(char[]);
+
+ // Add Required attribute to metdata if the member cannot be null and it is either a reference type or a Nullable<T>
+ if (!member.Nullable && (!pd.PropertyType.IsValueType || IsNullableType(pd.PropertyType)) &&
+ pd.Attributes[typeof(RequiredAttribute)] == null)
+ {
+ attributes.Add(new RequiredAttribute());
+ }
+
+ if (isStringType &&
+ pd.Attributes[typeof(StringLengthAttribute)] == null)
+ {
+ facet = member.TypeUsage.Facets.SingleOrDefault(p => p.Name == "MaxLength");
+ if (facet != null && facet.Value != null && facet.Value.GetType() == typeof(int))
+ {
+ // need to test for Type int, since the value can also be of type
+ // System.Data.Metadata.Edm.EdmConstants.Unbounded
+ int maxLength = (int)facet.Value;
+ attributes.Add(new StringLengthAttribute(maxLength));
+ }
+ }
+
+ bool hasTimestampAttribute = (pd.Attributes[typeof(TimestampAttribute)] != null);
+
+ if (_timestampMember == member && !hasTimestampAttribute)
+ {
+ attributes.Add(new TimestampAttribute());
+ hasTimestampAttribute = true;
+ }
+
+ // All members marked with TimestampAttribute (inferred or explicit) need to
+ // have [Editable(false)] and [RoundtripOriginal] applied
+ if (hasTimestampAttribute)
+ {
+ inferRoundtripOriginalAttribute = true;
+
+ if (editableAttribute == null)
+ {
+ editableAttribute = new EditableAttribute(false);
+ }
+ }
+
+ // Add RTO to this member if required. If this type has a timestamp
+ // member that member should be the ONLY member we apply RTO to.
+ // Dont apply RTO if it is an association member.
+ bool isForeignKeyMember = _foreignKeyMembers != null && _foreignKeyMembers.Contains(member);
+ if ((_timestampMember == null || _timestampMember == member) &&
+ (inferRoundtripOriginalAttribute || isForeignKeyMember) &&
+ pd.Attributes[typeof(AssociationAttribute)] == null)
+ {
+ if (pd.Attributes[typeof(RoundtripOriginalAttribute)] == null)
+ {
+ attributes.Add(new RoundtripOriginalAttribute());
+ }
+ }
+ }
+
+ // Add the Editable attribute if required
+ if (editableAttribute != null && pd.Attributes[typeof(EditableAttribute)] == null)
+ {
+ attributes.Add(editableAttribute);
+ }
+
+ if (isEntity)
+ {
+ AddAssociationAttributes(pd, attributes);
+ }
+
+ return attributes.ToArray();
+ }
+
+ /// <summary>
+ /// Determines whether the specified property is an Entity member that
+ /// should be excluded.
+ /// </summary>
+ /// <param name="pd">The property to check.</param>
+ /// <returns>True if the property should be excluded, false otherwise.</returns>
+ internal static bool ShouldExcludeEntityMember(PropertyDescriptor pd)
+ {
+ // exclude EntityState members
+ if (pd.PropertyType == typeof(EntityState) &&
+ (pd.ComponentType == typeof(EntityObject) || typeof(IEntityChangeTracker).IsAssignableFrom(pd.ComponentType)))
+ {
+ return true;
+ }
+
+ // exclude entity reference properties
+ if (typeof(EntityReference).IsAssignableFrom(pd.PropertyType))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Add AssociationAttribute if required for the specified property
+ /// </summary>
+ /// <param name="pd">The property</param>
+ /// <param name="attributes">The list of attributes to append to</param>
+ private void AddAssociationAttributes(PropertyDescriptor pd, List<Attribute> attributes)
+ {
+ EntityType entityType = (EntityType)EdmType;
+ NavigationProperty navProperty = entityType.NavigationProperties.Where(n => n.Name == pd.Name).SingleOrDefault();
+ if (navProperty != null)
+ {
+ bool isManyToMany = navProperty.RelationshipType.RelationshipEndMembers[0].RelationshipMultiplicity == RelationshipMultiplicity.Many &&
+ navProperty.RelationshipType.RelationshipEndMembers[1].RelationshipMultiplicity == RelationshipMultiplicity.Many;
+ if (!isManyToMany)
+ {
+ AssociationAttribute assocAttrib = (AssociationAttribute)pd.Attributes[typeof(System.ComponentModel.DataAnnotations.AssociationAttribute)];
+ if (assocAttrib == null)
+ {
+ assocAttrib = TypeDescriptionContext.CreateAssociationAttribute(navProperty);
+ attributes.Add(assocAttrib);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data.EntityFramework/Metadata/MetadataPropertyDescriptorWrapper.cs b/src/Microsoft.Web.Http.Data.EntityFramework/Metadata/MetadataPropertyDescriptorWrapper.cs
new file mode 100644
index 00000000..791d2326
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data.EntityFramework/Metadata/MetadataPropertyDescriptorWrapper.cs
@@ -0,0 +1,71 @@
+using System;
+using System.ComponentModel;
+
+namespace Microsoft.Web.Http.Data.EntityFramework.Metadata
+{
+ internal class MetadataPropertyDescriptorWrapper : PropertyDescriptor
+ {
+ private readonly PropertyDescriptor _descriptor;
+
+ public MetadataPropertyDescriptorWrapper(PropertyDescriptor descriptor, Attribute[] attrs)
+ : base(descriptor, attrs)
+ {
+ _descriptor = descriptor;
+ }
+
+ public override Type ComponentType
+ {
+ get { return _descriptor.ComponentType; }
+ }
+
+ public override bool IsReadOnly
+ {
+ get { return _descriptor.IsReadOnly; }
+ }
+
+ public override Type PropertyType
+ {
+ get { return _descriptor.PropertyType; }
+ }
+
+ public override bool SupportsChangeEvents
+ {
+ get { return _descriptor.SupportsChangeEvents; }
+ }
+
+ public override void AddValueChanged(object component, EventHandler handler)
+ {
+ _descriptor.AddValueChanged(component, handler);
+ }
+
+ public override bool CanResetValue(object component)
+ {
+ return _descriptor.CanResetValue(component);
+ }
+
+ public override object GetValue(object component)
+ {
+ return _descriptor.GetValue(component);
+ }
+
+ public override void RemoveValueChanged(object component, EventHandler handler)
+ {
+ _descriptor.RemoveValueChanged(component, handler);
+ }
+
+ public override void ResetValue(object component)
+ {
+ _descriptor.ResetValue(component);
+ }
+
+ public override void SetValue(object component, object value)
+ {
+ _descriptor.SetValue(component, value);
+ }
+
+ public override bool ShouldSerializeValue(object component)
+ {
+ return _descriptor.ShouldSerializeValue(component);
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data.EntityFramework/Metadata/MetadataWorkspaceUtilities.cs b/src/Microsoft.Web.Http.Data.EntityFramework/Metadata/MetadataWorkspaceUtilities.cs
new file mode 100644
index 00000000..05e5aa40
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data.EntityFramework/Metadata/MetadataWorkspaceUtilities.cs
@@ -0,0 +1,208 @@
+using System;
+using System.Collections.Generic;
+using System.Data.Mapping;
+using System.Data.Metadata.Edm;
+using System.Data.Objects;
+using System.Globalization;
+using System.Linq;
+using System.Reflection;
+using System.Web.Http.Common;
+
+namespace Microsoft.Web.Http.Data.EntityFramework.Metadata
+{
+ /// <summary>
+ /// EF metadata utilities class.
+ /// </summary>
+ internal static class MetadataWorkspaceUtilities
+ {
+ /// <summary>
+ /// Creates a metadata workspace for the specified context.
+ /// </summary>
+ /// <param name="contextType">The type of the object context.</param>
+ /// <param name="isDbContext">Set to <c>true</c> if context is a database context.</param>
+ /// <returns>The metadata workspace.</returns>
+ public static MetadataWorkspace CreateMetadataWorkspace(Type contextType, bool isDbContext)
+ {
+ MetadataWorkspace metadataWorkspace = null;
+
+ if (!isDbContext)
+ {
+ metadataWorkspace = MetadataWorkspaceUtilities.CreateMetadataWorkspaceFromResources(contextType, typeof(ObjectContext));
+ }
+ else
+ {
+ metadataWorkspace = MetadataWorkspaceUtilities.CreateMetadataWorkspaceFromResources(contextType, typeof(System.Data.Entity.DbContext));
+ if (metadataWorkspace == null && typeof(System.Data.Entity.DbContext).IsAssignableFrom(contextType))
+ {
+ if (contextType.GetConstructor(Type.EmptyTypes) == null)
+ {
+ throw Error.InvalidOperation(Resource.DefaultCtorNotFound, contextType.FullName);
+ }
+
+ try
+ {
+ System.Data.Entity.DbContext dbContext = Activator.CreateInstance(contextType) as System.Data.Entity.DbContext;
+ ObjectContext objectContext = (dbContext as System.Data.Entity.Infrastructure.IObjectContextAdapter).ObjectContext;
+ metadataWorkspace = objectContext.MetadataWorkspace;
+ }
+ catch (Exception efException)
+ {
+ throw Error.InvalidOperation(efException, Resource.MetadataWorkspaceNotFound, contextType.FullName);
+ }
+ }
+ }
+ if (metadataWorkspace == null)
+ {
+ throw Error.InvalidOperation(Resource.LinqToEntitiesProvider_UnableToRetrieveMetadata, contextType.Name);
+ }
+ else
+ {
+ return metadataWorkspace;
+ }
+ }
+
+ /// <summary>
+ /// Creates the MetadataWorkspace for the given context type and base context type.
+ /// </summary>
+ /// <param name="contextType">The type of the context.</param>
+ /// <param name="baseContextType">The base context type (DbContext or ObjectContext).</param>
+ /// <returns>The generated <see cref="MetadataWorkspace"/></returns>
+ public static MetadataWorkspace CreateMetadataWorkspaceFromResources(Type contextType, Type baseContextType)
+ {
+ // get the set of embedded mapping resources for the target assembly and create
+ // a metadata workspace info for each group
+ IEnumerable<string> metadataResourcePaths = FindMetadataResources(contextType.Assembly);
+ IEnumerable<MetadataWorkspaceInfo> workspaceInfos = GetMetadataWorkspaceInfos(metadataResourcePaths);
+
+ // Search for the correct EntityContainer by name and if found, create
+ // a comlete MetadataWorkspace and return it
+ foreach (var workspaceInfo in workspaceInfos)
+ {
+ EdmItemCollection edmItemCollection = new EdmItemCollection(workspaceInfo.Csdl);
+
+ Type currentType = contextType;
+ while (currentType != baseContextType && currentType != typeof(object))
+ {
+ EntityContainer container;
+ if (edmItemCollection.TryGetEntityContainer(currentType.Name, out container))
+ {
+ StoreItemCollection store = new StoreItemCollection(workspaceInfo.Ssdl);
+ StorageMappingItemCollection mapping = new StorageMappingItemCollection(edmItemCollection, store, workspaceInfo.Msl);
+ MetadataWorkspace workspace = new MetadataWorkspace();
+ workspace.RegisterItemCollection(edmItemCollection);
+ workspace.RegisterItemCollection(store);
+ workspace.RegisterItemCollection(mapping);
+ workspace.RegisterItemCollection(new ObjectItemCollection());
+ return workspace;
+ }
+
+ currentType = currentType.BaseType;
+ }
+ }
+ return null;
+ }
+
+ /// <summary>
+ /// Gets the specified resource paths as metadata workspace info objects.
+ /// </summary>
+ /// <param name="resourcePaths">The metadata resource paths.</param>
+ /// <returns>The metadata workspace info objects.</returns>
+ private static IEnumerable<MetadataWorkspaceInfo> GetMetadataWorkspaceInfos(IEnumerable<string> resourcePaths)
+ {
+ // for file paths, you would want to group without the path or the extension like Path.GetFileNameWithoutExtension, but resource names can contain
+ // forbidden path chars, so don't use it on resource names
+ foreach (var group in resourcePaths.GroupBy(p => p.Substring(0, p.LastIndexOf('.')), StringComparer.InvariantCultureIgnoreCase))
+ {
+ yield return MetadataWorkspaceInfo.Create(group);
+ }
+ }
+
+ /// <summary>
+ /// Find all the EF metadata resources.
+ /// </summary>
+ /// <param name="assembly">The assembly to find the metadata resources in.</param>
+ /// <returns>The metadata paths that were found.</returns>
+ private static IEnumerable<string> FindMetadataResources(Assembly assembly)
+ {
+ List<string> result = new List<string>();
+ foreach (string name in assembly.GetManifestResourceNames())
+ {
+ if (MetadataWorkspaceInfo.IsMetadata(name))
+ {
+ result.Add(String.Format(CultureInfo.InvariantCulture, "res://{0}/{1}", assembly.FullName, name));
+ }
+ }
+
+ return result;
+ }
+
+ /// <summary>
+ /// Represents the paths for a single metadata workspace.
+ /// </summary>
+ private class MetadataWorkspaceInfo
+ {
+ private const string CsdlExtension = ".csdl";
+ private const string MslExtension = ".msl";
+ private const string SsdlExtension = ".ssdl";
+
+ public MetadataWorkspaceInfo(string csdlPath, string mslPath, string ssdlPath)
+ {
+ if (csdlPath == null)
+ {
+ throw Error.ArgumentNull("csdlPath");
+ }
+
+ if (mslPath == null)
+ {
+ throw Error.ArgumentNull("mslPath");
+ }
+
+ if (ssdlPath == null)
+ {
+ throw Error.ArgumentNull("ssdlPath");
+ }
+
+ Csdl = csdlPath;
+ Msl = mslPath;
+ Ssdl = ssdlPath;
+ }
+
+ public string Csdl { get; private set; }
+
+ public string Msl { get; private set; }
+
+ public string Ssdl { get; private set; }
+
+ public static MetadataWorkspaceInfo Create(IEnumerable<string> paths)
+ {
+ string csdlPath = null;
+ string mslPath = null;
+ string ssdlPath = null;
+ foreach (string path in paths)
+ {
+ if (path.EndsWith(CsdlExtension, StringComparison.OrdinalIgnoreCase))
+ {
+ csdlPath = path;
+ }
+ else if (path.EndsWith(MslExtension, StringComparison.OrdinalIgnoreCase))
+ {
+ mslPath = path;
+ }
+ else if (path.EndsWith(SsdlExtension, StringComparison.OrdinalIgnoreCase))
+ {
+ ssdlPath = path;
+ }
+ }
+
+ return new MetadataWorkspaceInfo(csdlPath, mslPath, ssdlPath);
+ }
+
+ public static bool IsMetadata(string path)
+ {
+ return path.EndsWith(CsdlExtension, StringComparison.OrdinalIgnoreCase) ||
+ path.EndsWith(MslExtension, StringComparison.OrdinalIgnoreCase) ||
+ path.EndsWith(SsdlExtension, StringComparison.OrdinalIgnoreCase);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data.EntityFramework/Metadata/TypeDescriptionContextBase.cs b/src/Microsoft.Web.Http.Data.EntityFramework/Metadata/TypeDescriptionContextBase.cs
new file mode 100644
index 00000000..6d3f398c
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data.EntityFramework/Metadata/TypeDescriptionContextBase.cs
@@ -0,0 +1,31 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+
+namespace Microsoft.Web.Http.Data.EntityFramework.Metadata
+{
+ /// <summary>
+ /// Base class for LTS and EF type description contexts
+ /// </summary>
+ internal abstract class TypeDescriptionContextBase
+ {
+ /// <summary>
+ /// Given a suggested name and a collection of existing names, this method
+ /// creates a unique name by appending a numerix suffix as required.
+ /// </summary>
+ /// <param name="suggested">The desired name</param>
+ /// <param name="existing">Collection of existing names</param>
+ /// <returns>The unique name</returns>
+ protected static string MakeUniqueName(string suggested, IEnumerable<string> existing)
+ {
+ int i = 1;
+ string currSuggestion = suggested;
+ while (existing.Contains(currSuggestion))
+ {
+ currSuggestion = suggested + (i++).ToString(CultureInfo.InvariantCulture);
+ }
+
+ return currSuggestion;
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data.EntityFramework/Metadata/TypeDescriptorBase.cs b/src/Microsoft.Web.Http.Data.EntityFramework/Metadata/TypeDescriptorBase.cs
new file mode 100644
index 00000000..9941f40d
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data.EntityFramework/Metadata/TypeDescriptorBase.cs
@@ -0,0 +1,90 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+
+namespace Microsoft.Web.Http.Data.EntityFramework.Metadata
+{
+ /// <summary>
+ /// CustomTypeDescriptor base type shared by LINQ To SQL and LINQ To Entities
+ /// </summary>
+ internal abstract class TypeDescriptorBase : CustomTypeDescriptor
+ {
+ private PropertyDescriptorCollection _properties;
+
+ /// <summary>
+ /// Main constructor that accepts the parent custom type descriptor
+ /// </summary>
+ /// <param name="parent">The parent custom type descriptor.</param>
+ public TypeDescriptorBase(ICustomTypeDescriptor parent)
+ : base(parent)
+ {
+ }
+
+ /// <summary>
+ /// Override of the <see cref="CustomTypeDescriptor.GetProperties()"/> to obtain the list
+ /// of properties for this type.
+ /// </summary>
+ /// <remarks>
+ /// This method is overridden so that it can merge this class's parent attributes with those
+ /// it infers from the DAL-specific attributes.
+ /// </remarks>
+ /// <returns>A list of properties for this type</returns>
+ public sealed override PropertyDescriptorCollection GetProperties()
+ {
+ // No need to lock anything... Worst case scenario we create the properties multiple times.
+ if (_properties == null)
+ {
+ // Get properties from our parent
+ PropertyDescriptorCollection originalCollection = base.GetProperties();
+
+ bool customDescriptorsCreated = false;
+ List<PropertyDescriptor> tempPropertyDescriptors = new List<PropertyDescriptor>();
+
+ // for every property exposed by our parent, see if we have additional metadata to add
+ foreach (PropertyDescriptor propDescriptor in originalCollection)
+ {
+ Attribute[] newMetadata = GetMemberAttributes(propDescriptor).ToArray();
+ if (newMetadata.Length > 0)
+ {
+ tempPropertyDescriptors.Add(new MetadataPropertyDescriptorWrapper(propDescriptor, newMetadata));
+ customDescriptorsCreated = true;
+ }
+ else
+ {
+ tempPropertyDescriptors.Add(propDescriptor);
+ }
+ }
+
+ if (customDescriptorsCreated)
+ {
+ _properties = new PropertyDescriptorCollection(tempPropertyDescriptors.ToArray(), true);
+ }
+ else
+ {
+ _properties = originalCollection;
+ }
+ }
+
+ return _properties;
+ }
+
+ /// <summary>
+ /// Abstract method specific DAL implementations must override to return the
+ /// list of RIA <see cref="Attribute"/>s implied by their DAL-specific attributes
+ /// </summary>
+ /// <param name="pd">A <see cref="PropertyDescriptor"/> to examine.</param>
+ /// <returns>A list of RIA attributes implied by the DAL specific attributes</returns>
+ protected abstract IEnumerable<Attribute> GetMemberAttributes(PropertyDescriptor pd);
+
+ /// <summary>
+ /// Returns <c>true</c> if the given type is a <see cref="Nullable"/>
+ /// </summary>
+ /// <param name="type">The type to test</param>
+ /// <returns><c>true</c> if the given type is a nullable type</returns>
+ public static bool IsNullableType(Type type)
+ {
+ return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data.EntityFramework/Microsoft.Web.Http.Data.EntityFramework.csproj b/src/Microsoft.Web.Http.Data.EntityFramework/Microsoft.Web.Http.Data.EntityFramework.csproj
new file mode 100644
index 00000000..498b403b
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data.EntityFramework/Microsoft.Web.Http.Data.EntityFramework.csproj
@@ -0,0 +1,126 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <CodeAnalysis Condition=" '$(CodeAnalysis)' == '' ">false</CodeAnalysis>
+ <ProductVersion>8.0.30703</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{653F3946-541C-42D3-BBC1-CE89B392BDA9}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>Microsoft.Web.Http.Data.EntityFramework</RootNamespace>
+ <AssemblyName>Microsoft.Web.Http.Data.EntityFramework</AssemblyName>
+ <FileAlignment>512</FileAlignment>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>..\..\bin\Debug\</OutputPath>
+ <DefineConstants>TRACE;DEBUG;ASPNETMVC</DefineConstants>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ <DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
+ <NoWarn>1591</NoWarn>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>..\..\bin\Release\</OutputPath>
+ <DefineConstants>TRACE;ASPNETMVC</DefineConstants>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ <RunCodeAnalysis>$(CodeAnalysis)</RunCodeAnalysis>
+ <DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
+ <NoWarn>1591</NoWarn>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'CodeCoverage|AnyCPU'">
+ <DebugSymbols>true</DebugSymbols>
+ <OutputPath>..\..\bin\CodeCoverage\</OutputPath>
+ <DefineConstants>TRACE;DEBUG;CODE_COVERAGE;ASPNETMVC</DefineConstants>
+ <DebugType>full</DebugType>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="EntityFramework">
+ <HintPath>..\..\packages\EntityFramework.4.1.10331.0\lib\EntityFramework.dll</HintPath>
+ </Reference>
+ <Reference Include="System" />
+ <Reference Include="System.ComponentModel.DataAnnotations" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.Data.Entity" />
+ <Reference Include="Microsoft.CSharp" />
+ <Reference Include="System.Data" />
+ <Reference Include="System.Net.Http">
+ <HintPath>..\..\packages\Microsoft.Net.Http.2.0.20302.1\lib\net40\System.Net.Http.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Net.Http.WebRequest">
+ <HintPath>..\..\packages\Microsoft.Net.Http.2.0.20302.1\lib\net40\System.Net.Http.WebRequest.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Xml" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="..\CommonAssemblyInfo.cs">
+ <Link>Properties\CommonAssemblyInfo.cs</Link>
+ </Compile>
+ <Compile Include="DbContextExtensions.cs" />
+ <Compile Include="DbDataController.cs" />
+ <Compile Include="GlobalSuppressions.cs" />
+ <Compile Include="LinqToEntitiesDataController.cs" />
+ <Compile Include="Metadata\AssociationInfo.cs" />
+ <Compile Include="Metadata\DbMetadataProviderAttribute.cs" />
+ <Compile Include="Metadata\LinqToEntitiesMetadataProvider.cs" />
+ <Compile Include="Metadata\LinqToEntitiesMetadataProviderAttribute.cs" />
+ <Compile Include="Metadata\LinqToEntitiesTypeDescriptionContext.cs" />
+ <Compile Include="Metadata\LinqToEntitiesTypeDescriptor.cs" />
+ <Compile Include="Metadata\MetadataPropertyDescriptorWrapper.cs" />
+ <Compile Include="Metadata\MetadataWorkspaceUtilities.cs" />
+ <Compile Include="Metadata\TypeDescriptionContextBase.cs" />
+ <Compile Include="Metadata\TypeDescriptorBase.cs" />
+ <Compile Include="ObjectContextExtensions.cs" />
+ <Compile Include="ObjectContextUtilities.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="..\AptcaCommonAssemblyInfo.cs">
+ <Link>Properties\AptcaCommonAssemblyInfo.cs</Link>
+ </Compile>
+ <Compile Include="Resource.Designer.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>Resource.resx</DependentUpon>
+ </Compile>
+ </ItemGroup>
+ <ItemGroup>
+ <CodeAnalysisDictionary Include="..\CodeAnalysisDictionary.xml" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\System.Net.Http.Formatting\System.Net.Http.Formatting.csproj">
+ <Project>{668E9021-CE84-49D9-98FB-DF125A9FCDB0}</Project>
+ <Name>System.Net.Http.Formatting</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\System.Web.Http.Common\System.Web.Http.Common.csproj">
+ <Project>{03A5E5F2-2E23-48F2-ABCC-6C41BAC9AC02}</Project>
+ <Name>System.Web.Http.Common</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\Microsoft.Web.Http.Data\Microsoft.Web.Http.Data.csproj">
+ <Project>{ACE91549-D86E-4EB6-8C2A-5FF51386BB68}</Project>
+ <Name>Microsoft.Web.Http.Data</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\System.Web.Http\System.Web.Http.csproj">
+ <Project>{DDC1CE0C-486E-4E35-BB3B-EAB61F8F9440}</Project>
+ <Name>System.Web.Http</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="Resource.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>Resource.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="packages.config" />
+ </ItemGroup>
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+</Project> \ No newline at end of file
diff --git a/src/Microsoft.Web.Http.Data.EntityFramework/ObjectContextExtensions.cs b/src/Microsoft.Web.Http.Data.EntityFramework/ObjectContextExtensions.cs
new file mode 100644
index 00000000..7b76d99b
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data.EntityFramework/ObjectContextExtensions.cs
@@ -0,0 +1,90 @@
+using System.ComponentModel;
+using System.Data;
+using System.Data.Objects;
+using System.Web.Http.Common;
+
+namespace Microsoft.Web.Http.Data.EntityFramework
+{
+ /// <summary>
+ /// ObjectContext extension methods
+ /// </summary>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static class ObjectContextExtensions
+ {
+ /// <summary>
+ /// Extension method used to attach the specified entity as modified,
+ /// with the specified original state.
+ /// </summary>
+ /// <typeparam name="TEntity">The entity Type</typeparam>
+ /// <param name="objectSet">The ObjectSet to attach to</param>
+ /// <param name="current">The current entity state</param>
+ /// <param name="original">The original entity state</param>
+ public static void AttachAsModified<TEntity>(this ObjectSet<TEntity> objectSet, TEntity current, TEntity original) where TEntity : class
+ {
+ if (objectSet == null)
+ {
+ throw Error.ArgumentNull("objectSet");
+ }
+ if (current == null)
+ {
+ throw Error.ArgumentNull("current");
+ }
+ if (original == null)
+ {
+ throw Error.ArgumentNull("original");
+ }
+
+ // Attach the entity if it is not already attached, or if it is already
+ // attached, transition to Modified
+ EntityState currState = ObjectContextUtilities.GetEntityState(objectSet.Context, current);
+ if (currState == EntityState.Detached)
+ {
+ objectSet.Attach(current);
+ }
+ else
+ {
+ objectSet.Context.ObjectStateManager.ChangeObjectState(current, EntityState.Modified);
+ }
+
+ ObjectStateEntry stateEntry = ObjectContextUtilities.AttachAsModifiedInternal<TEntity>(current, original, objectSet.Context);
+
+ if (stateEntry.State != EntityState.Modified)
+ {
+ // Ensure that when we leave this method, the entity is in a
+ // Modified state. For example, if current and original are the
+ // same, we still need to force the state transition
+ objectSet.Context.ObjectStateManager.ChangeObjectState(current, EntityState.Modified);
+ }
+ }
+
+ /// <summary>
+ /// Extension method used to attach the specified entity as modified. This overload
+ /// can be used in cases where the entity has a Timestamp member.
+ /// </summary>
+ /// <typeparam name="TEntity">The entity Type</typeparam>
+ /// <param name="objectSet">The ObjectSet to attach to</param>
+ /// <param name="entity">The current entity state</param>
+ public static void AttachAsModified<TEntity>(this ObjectSet<TEntity> objectSet, TEntity entity) where TEntity : class
+ {
+ if (objectSet == null)
+ {
+ throw Error.ArgumentNull("objectSet");
+ }
+ if (entity == null)
+ {
+ throw Error.ArgumentNull("entity");
+ }
+
+ ObjectContext context = objectSet.Context;
+ EntityState currState = ObjectContextUtilities.GetEntityState(context, entity);
+ if (currState == EntityState.Detached)
+ {
+ // attach the entity
+ objectSet.Attach(entity);
+ }
+
+ // transition the entity to the modified state
+ context.ObjectStateManager.ChangeObjectState(entity, EntityState.Modified);
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data.EntityFramework/ObjectContextUtilities.cs b/src/Microsoft.Web.Http.Data.EntityFramework/ObjectContextUtilities.cs
new file mode 100644
index 00000000..47a75494
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data.EntityFramework/ObjectContextUtilities.cs
@@ -0,0 +1,178 @@
+using System;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.Data;
+using System.Data.Metadata.Edm;
+using System.Data.Objects;
+using System.Linq;
+using System.Web.Http.Common;
+
+namespace Microsoft.Web.Http.Data.EntityFramework
+{
+ /// <summary>
+ /// Internal utility functions for dealing with EF types and metadata
+ /// </summary>
+ internal static class ObjectContextUtilities
+ {
+ /// <summary>
+ /// Retrieves the <see cref="StructuralType"/> corresponding to the given CLR type (where the
+ /// type is an entity or complex type).
+ /// </summary>
+ /// <remarks>
+ /// If no mapping exists for <paramref name="clrType"/>, but one does exist for one of its base
+ /// types, we will return the mapping for the base type.
+ /// </remarks>
+ /// <param name="workspace">The <see cref="MetadataWorkspace"/></param>
+ /// <param name="clrType">The CLR type</param>
+ /// <returns>The <see cref="StructuralType"/> corresponding to that CLR type, or <c>null</c> if the Type
+ /// is not mapped.</returns>
+ public static StructuralType GetEdmType(MetadataWorkspace workspace, Type clrType)
+ {
+ if (workspace == null)
+ {
+ throw Error.ArgumentNull("workspace");
+ }
+ if (clrType == null)
+ {
+ throw Error.ArgumentNull("clrType");
+ }
+
+ if (clrType.IsPrimitive || clrType == typeof(object))
+ {
+ // want to avoid loading searching system assemblies for
+ // types we know aren't entity or complex types
+ return null;
+ }
+
+ // We first locate the EdmType in "OSpace", which matches the name and namespace of the CLR type
+ EdmType edmType = null;
+ do
+ {
+ if (!workspace.TryGetType(clrType.Name, clrType.Namespace, DataSpace.OSpace, out edmType))
+ {
+ // If EF could not find this type, it could be because it is not loaded into
+ // its current workspace. In this case, we explicitly load the assembly containing
+ // the CLR type and try again.
+ workspace.LoadFromAssembly(clrType.Assembly);
+ workspace.TryGetType(clrType.Name, clrType.Namespace, DataSpace.OSpace, out edmType);
+ }
+ }
+ while (edmType == null && (clrType = clrType.BaseType) != typeof(object) && clrType != null);
+
+ // Next we locate the StructuralType from the EdmType.
+ // This 2-step process is necessary when the types CLR namespace does not match Edm namespace.
+ // Look at the EdmEntityTypeAttribute on the generated entity classes to see this Edm namespace.
+ StructuralType structuralType = null;
+ if (edmType != null &&
+ (edmType.BuiltInTypeKind == BuiltInTypeKind.EntityType || edmType.BuiltInTypeKind == BuiltInTypeKind.ComplexType))
+ {
+ workspace.TryGetEdmSpaceType((StructuralType)edmType, out structuralType);
+ }
+
+ return structuralType;
+ }
+
+ /// <summary>
+ /// Method used to return the current <see cref="EntityState"/> of the specified
+ /// entity.
+ /// </summary>
+ /// <param name="context">The <see cref="ObjectContext"/></param>
+ /// <param name="entity">The entity to return the <see cref="EntityState"/> for</param>
+ /// <returns>The current <see cref="EntityState"/> of the specified entity</returns>
+ public static EntityState GetEntityState(ObjectContext context, object entity)
+ {
+ if (context == null)
+ {
+ throw Error.ArgumentNull("context");
+ }
+ if (entity == null)
+ {
+ throw Error.ArgumentNull("entity");
+ }
+
+ ObjectStateEntry stateEntry = null;
+ if (!context.ObjectStateManager.TryGetObjectStateEntry(entity, out stateEntry))
+ {
+ return EntityState.Detached;
+ }
+ return stateEntry.State;
+ }
+
+ /// <summary>
+ /// Determines if the specified EdmMember is a concurrency timestamp.
+ /// </summary>
+ /// <remarks>Since EF doesn't expose "timestamp" as a first class
+ /// concept, we use the below criteria to infer this for ourselves.
+ /// </remarks>
+ /// <param name="member">The member to check.</param>
+ /// <returns>True or false.</returns>
+ public static bool IsConcurrencyTimestamp(EdmMember member)
+ {
+ Facet facet = member.TypeUsage.Facets.SingleOrDefault(p => p.Name == "ConcurrencyMode");
+ if (facet == null || facet.Value == null || (ConcurrencyMode)facet.Value != ConcurrencyMode.Fixed)
+ {
+ return false;
+ }
+
+ facet = member.TypeUsage.Facets.SingleOrDefault(p => p.Name == "FixedLength");
+ if (facet == null || facet.Value == null || !((bool)facet.Value))
+ {
+ return false;
+ }
+
+ facet = member.TypeUsage.Facets.SingleOrDefault(p => p.Name == "MaxLength");
+ if (facet == null || facet.Value == null || (int)facet.Value != 8)
+ {
+ return false;
+ }
+
+ MetadataProperty md = ObjectContextUtilities.GetStoreGeneratedPattern(member);
+ if (md == null || facet.Value == null || (string)md.Value != "Computed")
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Gets the <see cref="StoreGeneratedPattern"/> property value from the edm member.
+ /// </summary>
+ /// <param name="member">The EdmMember from which to get the StoreGeneratedPattern value.</param>
+ /// <returns>The <see cref="StoreGeneratedPattern"/> value.</returns>
+ public static MetadataProperty GetStoreGeneratedPattern(EdmMember member)
+ {
+ MetadataProperty md;
+ member.MetadataProperties.TryGetValue("http://schemas.microsoft.com/ado/2009/02/edm/annotation:StoreGeneratedPattern", ignoreCase: true, item: out md);
+ return md;
+ }
+
+ public static ObjectStateEntry AttachAsModifiedInternal<TEntity>(TEntity current, TEntity original, ObjectContext objectContext)
+ {
+ ObjectStateEntry stateEntry = objectContext.ObjectStateManager.GetObjectStateEntry(current);
+ stateEntry.ApplyOriginalValues(original);
+
+ // For any members that don't have RoundtripOriginal applied, EF can't determine modification
+ // state by doing value comparisons. To avoid losing updates in these cases, we must explicitly
+ // mark such members as modified.
+ PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(typeof(TEntity));
+ AttributeCollection attributes = TypeDescriptor.GetAttributes(typeof(TEntity));
+ bool isRoundtripType = attributes[typeof(RoundtripOriginalAttribute)] != null;
+ foreach (var fieldMetadata in stateEntry.CurrentValues.DataRecordInfo.FieldMetadata)
+ {
+ string memberName = stateEntry.CurrentValues.GetName(fieldMetadata.Ordinal);
+ PropertyDescriptor property = properties[memberName];
+ // TODO: below we need to replace ExcludeAttribute logic with corresponding
+ // DataContractMember/IgnoreDataMember logic
+ if (property != null &&
+ (property.Attributes[typeof(RoundtripOriginalAttribute)] == null && !isRoundtripType)
+ /* && property.Attributes[typeof(ExcludeAttribute)] == null */)
+ {
+ stateEntry.SetModifiedProperty(memberName);
+ }
+ }
+
+ return stateEntry;
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data.EntityFramework/Properties/AssemblyInfo.cs b/src/Microsoft.Web.Http.Data.EntityFramework/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..9d7c3eb0
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data.EntityFramework/Properties/AssemblyInfo.cs
@@ -0,0 +1,6 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+
+[assembly: AssemblyTitle("Microsoft.Web.Http.Data.EntityFramework")]
+[assembly: AssemblyDescription("Microsoft.Web.Http.Data.EntityFramework")]
+[assembly: InternalsVisibleTo("Microsoft.Web.Http.Data.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
diff --git a/src/Microsoft.Web.Http.Data.EntityFramework/Resource.Designer.cs b/src/Microsoft.Web.Http.Data.EntityFramework/Resource.Designer.cs
new file mode 100644
index 00000000..538aafc6
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data.EntityFramework/Resource.Designer.cs
@@ -0,0 +1,135 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.239
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Microsoft.Web.Http.Data.EntityFramework {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resource {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resource() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.Web.Http.Data.EntityFramework.Resource", typeof(Resource).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The DbContext type &apos;{0}&apos; does not contain a parameterless constructor. A parameterless constructor is required to use EntityFramework in the Code-First mode with a DataController..
+ /// </summary>
+ internal static string DefaultCtorNotFound {
+ get {
+ return ResourceManager.GetString("DefaultCtorNotFound", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Type &apos;{0}&apos; is not a valid DbMetadataProviderAttribute parameter because it does not derive from DbContext..
+ /// </summary>
+ internal static string InvalidDbMetadataProviderSpecification {
+ get {
+ return ResourceManager.GetString("InvalidDbMetadataProviderSpecification", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Type &apos;{0}&apos; is not a valid LinqToEntitiesMetadataProviderAttribute parameter because it does not derive from ObjectContext..
+ /// </summary>
+ internal static string InvalidLinqToEntitiesMetadataProviderSpecification {
+ get {
+ return ResourceManager.GetString("InvalidLinqToEntitiesMetadataProviderSpecification", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &apos;{0}&apos; cannot be applied to DataController type &apos;{1}&apos; because &apos;{1}&apos; does not derive from &apos;{2}&apos;..
+ /// </summary>
+ internal static string InvalidMetadataProviderSpecification {
+ get {
+ return ResourceManager.GetString("InvalidMetadataProviderSpecification", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Unable to retrieve association information for association &apos;{0}&apos;. Only models that include foreign key information are supported. See Entity Framework documentation for details on creating models that include foreign key information..
+ /// </summary>
+ internal static string LinqToEntitiesProvider_UnableToRetrieveAssociationInfo {
+ get {
+ return ResourceManager.GetString("LinqToEntitiesProvider_UnableToRetrieveAssociationInfo", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Unable to find metadata for &apos;{0}&apos;..
+ /// </summary>
+ internal static string LinqToEntitiesProvider_UnableToRetrieveMetadata {
+ get {
+ return ResourceManager.GetString("LinqToEntitiesProvider_UnableToRetrieveMetadata", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Failed to get the MetadataWorkspace for the DbContext type &apos;{0}&apos;..
+ /// </summary>
+ internal static string MetadataWorkspaceNotFound {
+ get {
+ return ResourceManager.GetString("MetadataWorkspaceNotFound", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to ObjectStateManager not initialized for the DbContext type &apos;{0}&apos;..
+ /// </summary>
+ internal static string ObjectStateManagerNotFoundException {
+ get {
+ return ResourceManager.GetString("ObjectStateManagerNotFoundException", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data.EntityFramework/Resource.resx b/src/Microsoft.Web.Http.Data.EntityFramework/Resource.resx
new file mode 100644
index 00000000..f1b51fa5
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data.EntityFramework/Resource.resx
@@ -0,0 +1,144 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="DefaultCtorNotFound" xml:space="preserve">
+ <value>The DbContext type '{0}' does not contain a parameterless constructor. A parameterless constructor is required to use EntityFramework in the Code-First mode with a DataController.</value>
+ </data>
+ <data name="InvalidDbMetadataProviderSpecification" xml:space="preserve">
+ <value>Type '{0}' is not a valid DbMetadataProviderAttribute parameter because it does not derive from DbContext.</value>
+ </data>
+ <data name="InvalidLinqToEntitiesMetadataProviderSpecification" xml:space="preserve">
+ <value>Type '{0}' is not a valid LinqToEntitiesMetadataProviderAttribute parameter because it does not derive from ObjectContext.</value>
+ </data>
+ <data name="InvalidMetadataProviderSpecification" xml:space="preserve">
+ <value>'{0}' cannot be applied to DataController type '{1}' because '{1}' does not derive from '{2}'.</value>
+ </data>
+ <data name="LinqToEntitiesProvider_UnableToRetrieveAssociationInfo" xml:space="preserve">
+ <value>Unable to retrieve association information for association '{0}'. Only models that include foreign key information are supported. See Entity Framework documentation for details on creating models that include foreign key information.</value>
+ </data>
+ <data name="LinqToEntitiesProvider_UnableToRetrieveMetadata" xml:space="preserve">
+ <value>Unable to find metadata for '{0}'.</value>
+ </data>
+ <data name="MetadataWorkspaceNotFound" xml:space="preserve">
+ <value>Failed to get the MetadataWorkspace for the DbContext type '{0}'.</value>
+ </data>
+ <data name="ObjectStateManagerNotFoundException" xml:space="preserve">
+ <value>ObjectStateManager not initialized for the DbContext type '{0}'.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/Microsoft.Web.Http.Data.EntityFramework/Settings.StyleCop b/src/Microsoft.Web.Http.Data.EntityFramework/Settings.StyleCop
new file mode 100644
index 00000000..2b55d67f
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data.EntityFramework/Settings.StyleCop
@@ -0,0 +1,12 @@
+<StyleCopSettings Version="105">
+ <Analyzers>
+ <Analyzer AnalyzerId="StyleCop.CSharp.NamingRules">
+ <AnalyzerSettings>
+ <CollectionProperty Name="Hungarian">
+ <Value>db</Value>
+ <Value>ef</Value>
+ </CollectionProperty>
+ </AnalyzerSettings>
+ </Analyzer>
+ </Analyzers>
+</StyleCopSettings> \ No newline at end of file
diff --git a/src/Microsoft.Web.Http.Data.EntityFramework/packages.config b/src/Microsoft.Web.Http.Data.EntityFramework/packages.config
new file mode 100644
index 00000000..86a77079
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data.EntityFramework/packages.config
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="EntityFramework" version="4.1.10331.0" />
+ <package id="Microsoft.Net.Http" version="2.0.20302.1" />
+</packages> \ No newline at end of file
diff --git a/src/Microsoft.Web.Http.Data.Helpers/DataControllerMetadataGenerator.cs b/src/Microsoft.Web.Http.Data.Helpers/DataControllerMetadataGenerator.cs
new file mode 100644
index 00000000..c4342138
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data.Helpers/DataControllerMetadataGenerator.cs
@@ -0,0 +1,391 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.Globalization;
+using System.Json;
+using System.Linq;
+
+namespace Microsoft.Web.Http.Data.Helpers
+{
+ internal static class DataControllerMetadataGenerator
+ {
+ private static readonly ConcurrentDictionary<DataControllerDescription, IEnumerable<TypeMetadata>> _metadataMap =
+ new ConcurrentDictionary<DataControllerDescription, IEnumerable<TypeMetadata>>();
+
+ private static readonly IEnumerable<KeyValuePair<string, JsonValue>> _emptyKeyValuePairEnumerable = Enumerable.Empty<KeyValuePair<string, JsonValue>>();
+
+ public static IEnumerable<TypeMetadata> GetMetadata(DataControllerDescription description)
+ {
+ return _metadataMap.GetOrAdd(description, desc =>
+ {
+ return GenerateMetadata(desc);
+ });
+ }
+
+ private static IEnumerable<TypeMetadata> GenerateMetadata(DataControllerDescription description)
+ {
+ List<TypeMetadata> metadata = new List<TypeMetadata>();
+ foreach (Type entityType in description.EntityTypes)
+ {
+ metadata.Add(new TypeMetadata(entityType));
+ }
+ // TODO: Complex types are NYI in DataControllerDescription
+ // foreach (Type complexType in description.ComplexTypes)
+ // {
+ // metadata.Add(new TypeMetadata(complexType));
+ // }
+ return metadata;
+ }
+
+ private static string EncodeTypeName(string typeName, string typeNamespace)
+ {
+ return String.Format(CultureInfo.InvariantCulture, "{0}{1}{2}", typeName, MetadataStrings.NamespaceMarker, typeNamespace);
+ }
+
+ private static class MetadataStrings
+ {
+ public const string NamespaceMarker = ":#";
+ public const string TypeString = "type";
+ public const string ArrayString = "array";
+ public const string AssociationString = "association";
+ public const string FieldsString = "fields";
+ public const string ThisKeyString = "thisKey";
+ public const string IsForeignKey = "isForeignKey";
+ public const string OtherKeyString = "otherKey";
+ public const string NameString = "name";
+ public const string ReadOnlyString = "readonly";
+ public const string KeyString = "key";
+ public const string RulesString = "rules";
+ public const string MessagesString = "messages";
+ }
+
+ public class TypeMetadata
+ {
+ private List<string> _key = new List<string>();
+ private List<TypePropertyMetadata> _properties = new List<TypePropertyMetadata>();
+
+ public TypeMetadata(Type entityType)
+ {
+ Type type = TypeUtility.GetElementType(entityType);
+ TypeName = type.Name;
+ TypeNamespace = type.Namespace;
+
+ IEnumerable<PropertyDescriptor> properties =
+ TypeDescriptor.GetProperties(entityType).Cast<PropertyDescriptor>().OrderBy(p => p.Name)
+ .Where(p => TypeUtility.IsDataMember(p));
+
+ foreach (PropertyDescriptor pd in properties)
+ {
+ _properties.Add(new TypePropertyMetadata(pd));
+ if (TypeDescriptorExtensions.ExplicitAttributes(pd)[typeof(KeyAttribute)] != null)
+ {
+ _key.Add(pd.Name);
+ }
+ }
+ }
+
+ public string TypeName { get; private set; }
+ public string TypeNamespace { get; private set; }
+
+ public string EncodedTypeName
+ {
+ get { return EncodeTypeName(TypeName, TypeNamespace); }
+ }
+
+ public IEnumerable<string> Key
+ {
+ get { return _key; }
+ }
+
+ public IEnumerable<TypePropertyMetadata> Properties
+ {
+ get { return _properties; }
+ }
+
+ public JsonValue ToJsonValue()
+ {
+ JsonObject value = new JsonObject();
+
+ value[MetadataStrings.KeyString] = new JsonArray(Key.Select(k => (JsonValue)k));
+ value[MetadataStrings.FieldsString] = new JsonObject(Properties.Select(p => new KeyValuePair<string, JsonValue>(p.Name, p.ToJsonValue())));
+
+ // TODO: Only include these properties when they'll have non-empty values. Need to update SPA T4 templates to tolerate null in scaffolded SPA JavaScript.
+ //if (Properties.Any(p => p.ValidationRules.Count > 0))
+ //{
+ value[MetadataStrings.RulesString] = new JsonObject(
+ Properties.SelectMany(
+ p => p.ValidationRules.Count == 0
+ ? _emptyKeyValuePairEnumerable
+ : new KeyValuePair<string, JsonValue>[]
+ {
+ new KeyValuePair<string, JsonValue>(
+ p.Name,
+ new JsonObject(p.ValidationRules.Select(
+ r => new KeyValuePair<string, JsonValue>(r.Name, r.ToJsonValue()))))
+ }));
+ //}
+ //if (Properties.Any(p => p.ValidationRules.Any(r => r.ErrorMessageString != null)))
+ //{
+ value[MetadataStrings.MessagesString] = new JsonObject(
+ Properties.SelectMany(
+ p => !p.ValidationRules.Any(r => r.ErrorMessageString != null)
+ ? _emptyKeyValuePairEnumerable
+ : new KeyValuePair<string, JsonValue>[]
+ {
+ new KeyValuePair<string, JsonValue>(
+ p.Name,
+ new JsonObject(p.ValidationRules.SelectMany(r =>
+ r.ErrorMessageString == null
+ ? _emptyKeyValuePairEnumerable
+ : new KeyValuePair<string, JsonValue>[]
+ {
+ new KeyValuePair<string, JsonValue>(r.Name, r.ErrorMessageString)
+ })))
+ }));
+ //}
+
+ return value;
+ }
+ }
+
+ public class TypePropertyAssociationMetadata
+ {
+ private List<string> _thisKeyMembers = new List<string>();
+ private List<string> _otherKeyMembers = new List<string>();
+
+ public TypePropertyAssociationMetadata(AssociationAttribute associationAttr)
+ {
+ Name = associationAttr.Name;
+ IsForeignKey = associationAttr.IsForeignKey;
+ _otherKeyMembers = associationAttr.OtherKeyMembers.ToList<string>();
+ _thisKeyMembers = associationAttr.ThisKeyMembers.ToList<string>();
+ }
+
+ public string Name { get; private set; }
+ public bool IsForeignKey { get; private set; }
+
+ public IEnumerable<string> ThisKeyMembers
+ {
+ get { return _thisKeyMembers; }
+ }
+
+ public IEnumerable<string> OtherKeyMembers
+ {
+ get { return _otherKeyMembers; }
+ }
+
+ public JsonValue ToJsonValue()
+ {
+ JsonObject value = new JsonObject();
+ value[MetadataStrings.NameString] = Name;
+ value[MetadataStrings.ThisKeyString] = new JsonArray(ThisKeyMembers.Select(k => (JsonValue)k));
+ value[MetadataStrings.OtherKeyString] = new JsonArray(OtherKeyMembers.Select(k => (JsonValue)k));
+ value[MetadataStrings.IsForeignKey] = IsForeignKey;
+ return value;
+ }
+ }
+
+ public class TypePropertyMetadata
+ {
+ private List<TypePropertyValidationRuleMetadata> _validationRules = new List<TypePropertyValidationRuleMetadata>();
+
+ public TypePropertyMetadata(PropertyDescriptor descriptor)
+ {
+ Name = descriptor.Name;
+
+ Type elementType = TypeUtility.GetElementType(descriptor.PropertyType);
+ IsArray = !elementType.Equals(descriptor.PropertyType);
+ // TODO: What should we do with nullable types here?
+ TypeName = elementType.Name;
+ TypeNamespace = elementType.Namespace;
+
+ AttributeCollection propertyAttributes = TypeDescriptorExtensions.ExplicitAttributes(descriptor);
+
+ // TODO, 336102, ReadOnlyAttribute for editability? RIA used EditableAttribute?
+ ReadOnlyAttribute readonlyAttr = (ReadOnlyAttribute)propertyAttributes[typeof(ReadOnlyAttribute)];
+ IsReadOnly = (readonlyAttr != null) ? readonlyAttr.IsReadOnly : false;
+
+ AssociationAttribute associationAttr = (AssociationAttribute)propertyAttributes[typeof(AssociationAttribute)];
+ if (associationAttr != null)
+ {
+ Association = new TypePropertyAssociationMetadata(associationAttr);
+ }
+
+ RequiredAttribute requiredAttribute = (RequiredAttribute)propertyAttributes[typeof(RequiredAttribute)];
+ if (requiredAttribute != null)
+ {
+ _validationRules.Add(new TypePropertyValidationRuleMetadata(requiredAttribute));
+ }
+
+ RangeAttribute rangeAttribute = (RangeAttribute)propertyAttributes[typeof(RangeAttribute)];
+ if (rangeAttribute != null)
+ {
+ Type operandType = rangeAttribute.OperandType;
+ operandType = Nullable.GetUnderlyingType(operandType) ?? operandType;
+ if (operandType.Equals(typeof(Double))
+ || operandType.Equals(typeof(Int16))
+ || operandType.Equals(typeof(Int32))
+ || operandType.Equals(typeof(Int64))
+ || operandType.Equals(typeof(Single)))
+ {
+ _validationRules.Add(new TypePropertyValidationRuleMetadata(rangeAttribute));
+ }
+ }
+
+ StringLengthAttribute stringLengthAttribute = (StringLengthAttribute)propertyAttributes[typeof(StringLengthAttribute)];
+ if (stringLengthAttribute != null)
+ {
+ _validationRules.Add(new TypePropertyValidationRuleMetadata(stringLengthAttribute));
+ }
+
+ DataTypeAttribute dataTypeAttribute = (DataTypeAttribute)propertyAttributes[typeof(DataTypeAttribute)];
+ if (dataTypeAttribute != null)
+ {
+ if (dataTypeAttribute.DataType.Equals(DataType.EmailAddress)
+ || dataTypeAttribute.DataType.Equals(DataType.Url))
+ {
+ _validationRules.Add(new TypePropertyValidationRuleMetadata(dataTypeAttribute));
+ }
+ }
+ }
+
+ public string Name { get; private set; }
+ public string TypeName { get; private set; }
+ public string TypeNamespace { get; private set; }
+ public bool IsReadOnly { get; private set; }
+ public bool IsArray { get; private set; }
+ public TypePropertyAssociationMetadata Association { get; private set; }
+
+ public IList<TypePropertyValidationRuleMetadata> ValidationRules
+ {
+ get { return _validationRules; }
+ }
+
+ public JsonValue ToJsonValue()
+ {
+ JsonObject value = new JsonObject();
+
+ value[MetadataStrings.TypeString] = EncodeTypeName(TypeName, TypeNamespace);
+
+ if (IsReadOnly)
+ {
+ value[MetadataStrings.ReadOnlyString] = true;
+ }
+
+ if (IsArray)
+ {
+ value[MetadataStrings.ArrayString] = true;
+ }
+
+ if (Association != null)
+ {
+ value[MetadataStrings.AssociationString] = Association.ToJsonValue();
+ }
+
+ return value;
+ }
+ }
+
+ public class TypePropertyValidationRuleMetadata
+ {
+ private string _type;
+
+ public TypePropertyValidationRuleMetadata(RequiredAttribute attribute)
+ : this((ValidationAttribute)attribute)
+ {
+ Name = "required";
+ Value1 = true;
+ _type = "boolean";
+ }
+
+ public TypePropertyValidationRuleMetadata(RangeAttribute attribute)
+ : this((ValidationAttribute)attribute)
+ {
+ Name = "range";
+ Value1 = attribute.Minimum;
+ Value2 = attribute.Maximum;
+ _type = "array";
+ }
+
+ public TypePropertyValidationRuleMetadata(StringLengthAttribute attribute)
+ : this((ValidationAttribute)attribute)
+ {
+ if (attribute.MinimumLength != 0)
+ {
+ Name = "rangelength";
+ Value1 = attribute.MinimumLength;
+ Value2 = attribute.MaximumLength;
+ _type = "array";
+ }
+ else
+ {
+ Name = "maxlength";
+ Value1 = attribute.MaximumLength;
+ _type = "number";
+ }
+ }
+
+ public TypePropertyValidationRuleMetadata(DataTypeAttribute attribute)
+ : this((ValidationAttribute)attribute)
+ {
+ switch (attribute.DataType)
+ {
+ case DataType.EmailAddress:
+ Name = "email";
+ break;
+ case DataType.Url:
+ Name = "url";
+ break;
+ default:
+ break;
+ }
+ Value1 = true;
+ _type = "boolean";
+ }
+
+ public TypePropertyValidationRuleMetadata(ValidationAttribute attribute)
+ {
+ if (attribute.ErrorMessage != null)
+ {
+ ErrorMessageString = attribute.ErrorMessage;
+ }
+ }
+
+ public string Name { get; private set; }
+ public object Value1 { get; private set; }
+ public object Value2 { get; private set; }
+ public string ErrorMessageString { get; private set; }
+
+ public JsonValue ToJsonValue()
+ {
+ // The output json is determined by the number of values. The object constructor takes care the value assignment.
+ // When we have two values, we have two numbers that are written as an array.
+ // When we have only one value, it is written as it's type only.
+ if (_type == "array")
+ {
+ JsonPrimitive value1;
+ JsonPrimitive.TryCreate(Value1, out value1);
+
+ JsonPrimitive value2;
+ JsonPrimitive.TryCreate(Value2, out value2);
+
+ return new JsonArray(value1, value2);
+ }
+ else if (_type == "boolean")
+ {
+ return (bool)Value1;
+ }
+ else if (_type == "number")
+ {
+ return (int)Value1;
+ }
+ else
+ {
+ throw new InvalidOperationException("Unexpected validation rule type.");
+ }
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data.Helpers/GlobalSuppressions.cs b/src/Microsoft.Web.Http.Data.Helpers/GlobalSuppressions.cs
new file mode 100644
index 00000000..15f5de02
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data.Helpers/GlobalSuppressions.cs
@@ -0,0 +1,17 @@
+// This file is used by Code Analysis to maintain SuppressMessage
+// attributes that are applied to this project.
+// Project-level suppressions either have no target or are given
+// a specific target and scoped to a namespace, type, member, etc.
+//
+// To add a suppression to this file, right-click the message in the
+// Error List, point to "Suppress Message(s)", and click
+// "In Project Suppression File".
+// You do not need to add suppressions to this file manually.
+
+using System.Diagnostics.CodeAnalysis;
+
+[assembly: SuppressMessage("Microsoft.Design", "CA2210:AssembliesShouldHaveValidStrongNames", Justification = "Assembly is delay signed")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "Microsoft.Web.Http.Data.Helpers", Justification = "There are just a few helpers for client generation.")]
+[assembly: SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", Target = "Microsoft.Web.Http.Data.TypeDescriptorExtensions.#ContainsAttributeType`1(System.ComponentModel.AttributeCollection)", Justification = "Used in Microsoft.Web.Http.Data assembly")]
+[assembly: SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", Target = "Microsoft.Web.Http.Data.TypeUtility.#GetKnownTypes(System.Type,System.Boolean)", Justification = "Used in Microsoft.Web.Http.Data assembly")]
+[assembly: SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", Target = "Microsoft.Web.Http.Data.TypeUtility.#UnwrapTaskInnerType(System.Type)", Justification = "Used in Microsoft.Web.Http.Data assembly")]
diff --git a/src/Microsoft.Web.Http.Data.Helpers/MetadataExtensions.cs b/src/Microsoft.Web.Http.Data.Helpers/MetadataExtensions.cs
new file mode 100644
index 00000000..de7d41dd
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data.Helpers/MetadataExtensions.cs
@@ -0,0 +1,35 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Json;
+using System.Linq;
+using System.Web;
+using System.Web.Http;
+using System.Web.Http.Controllers;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Http.Data.Helpers
+{
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static class MetadataExtensions
+ {
+ [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "Following established design pattern for HTML helpers.")]
+ public static IHtmlString Metadata<TDataController>(this HtmlHelper htmlHelper) where TDataController : DataController
+ {
+ HttpControllerDescriptor controllerDescriptor = new HttpControllerDescriptor
+ {
+ Configuration = GlobalConfiguration.Configuration, // This helper can't be run until after global app init.
+ ControllerType = typeof(TDataController)
+ };
+
+ DataControllerDescription description = DataControllerDescription.GetDescription(controllerDescriptor);
+ IEnumerable<DataControllerMetadataGenerator.TypeMetadata> metadata =
+ DataControllerMetadataGenerator.GetMetadata(description);
+
+ JsonValue metadataValue = new JsonObject(metadata.Select(
+ m => new KeyValuePair<string, JsonValue>(m.EncodedTypeName, m.ToJsonValue())));
+
+ return htmlHelper.Raw(metadataValue);
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data.Helpers/Microsoft.Web.Http.Data.Helpers.csproj b/src/Microsoft.Web.Http.Data.Helpers/Microsoft.Web.Http.Data.Helpers.csproj
new file mode 100644
index 00000000..c16088ee
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data.Helpers/Microsoft.Web.Http.Data.Helpers.csproj
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <CodeAnalysis Condition=" '$(CodeAnalysis)' == '' ">false</CodeAnalysis>
+ <ProductVersion>8.0.30703</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{B6895A1B-382F-4A69-99EC-E965E19B0AB3}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>Microsoft.Web.Http.Data.Helpers</RootNamespace>
+ <AssemblyName>Microsoft.Web.Http.Data.Helpers</AssemblyName>
+ <FileAlignment>512</FileAlignment>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>..\..\bin\Debug\</OutputPath>
+ <DefineConstants>TRACE;DEBUG;ASPNETMVC</DefineConstants>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ <DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
+ <NoWarn>1591</NoWarn>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>..\..\bin\Release\</OutputPath>
+ <DefineConstants>TRACE;ASPNETMVC</DefineConstants>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ <RunCodeAnalysis>$(CodeAnalysis)</RunCodeAnalysis>
+ <DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
+ <NoWarn>1591</NoWarn>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'CodeCoverage|AnyCPU'">
+ <DebugSymbols>true</DebugSymbols>
+ <OutputPath>..\..\bin\CodeCoverage\</OutputPath>
+ <DefineConstants>TRACE;DEBUG;CODE_COVERAGE;ASPNETMVC</DefineConstants>
+ <DebugType>full</DebugType>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="System" />
+ <Reference Include="System.ComponentModel.DataAnnotations" />
+ <Reference Include="System.configuration" />
+ <Reference Include="System.Net.Http">
+ <HintPath>..\..\packages\Microsoft.Net.Http.2.0.20302.1\lib\net40\System.Net.Http.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Net.Http.WebRequest">
+ <HintPath>..\..\packages\Microsoft.Net.Http.2.0.20302.1\lib\net40\System.Net.Http.WebRequest.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Runtime.Serialization" />
+ <Reference Include="System.Web" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="..\AptcaCommonAssemblyInfo.cs">
+ <Link>Properties\AptcaCommonAssemblyInfo.cs</Link>
+ </Compile>
+ <Compile Include="..\CommonAssemblyInfo.cs">
+ <Link>Properties\CommonAssemblyInfo.cs</Link>
+ </Compile>
+ <Compile Include="GlobalSuppressions.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="DataControllerMetadataGenerator.cs" />
+ <Compile Include="MetadataExtensions.cs" />
+ <Compile Include="..\Microsoft.Web.Http.Data\TypeUtility.cs">
+ <Link>TypeUtility.cs</Link>
+ </Compile>
+ <Compile Include="..\Microsoft.Web.Http.Data\TypeDescriptorExtensions.cs">
+ <Link>TypeDescriptorExtensions.cs</Link>
+ </Compile>
+ <Compile Include="UpshotExtensions.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\System.Json\System.Json.csproj">
+ <Project>{F0441BE9-BDC0-4629-BE5A-8765FFAA2481}</Project>
+ <Name>System.Json</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\Microsoft.Web.Http.Data\Microsoft.Web.Http.Data.csproj">
+ <Project>{ACE91549-D86E-4EB6-8C2A-5FF51386BB68}</Project>
+ <Name>Microsoft.Web.Http.Data</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\System.Web.Http.WebHost\System.Web.Http.WebHost.csproj">
+ <Project>{A0187BC2-8325-4BB2-8697-7F955CF4173E}</Project>
+ <Name>System.Web.Http.WebHost</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\System.Web.Http\System.Web.Http.csproj">
+ <Project>{DDC1CE0C-486E-4E35-BB3B-EAB61F8F9440}</Project>
+ <Name>System.Web.Http</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\System.Web.Mvc\System.Web.Mvc.csproj">
+ <Project>{3D3FFD8A-624D-4E9B-954B-E1C105507975}</Project>
+ <Name>System.Web.Mvc</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <CodeAnalysisDictionary Include="..\CodeAnalysisDictionary.xml" />
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="packages.config" />
+ </ItemGroup>
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+</Project> \ No newline at end of file
diff --git a/src/Microsoft.Web.Http.Data.Helpers/Properties/AssemblyInfo.cs b/src/Microsoft.Web.Http.Data.Helpers/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..015acfb1
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data.Helpers/Properties/AssemblyInfo.cs
@@ -0,0 +1,6 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+
+[assembly: AssemblyTitle("Microsoft.Web.Http.Data.Helpers")]
+[assembly: AssemblyDescription("Microsoft.Web.Http.Data.Helpers")]
+[assembly: InternalsVisibleTo("Microsoft.Web.Http.Data.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
diff --git a/src/Microsoft.Web.Http.Data.Helpers/UpshotExtensions.cs b/src/Microsoft.Web.Http.Data.Helpers/UpshotExtensions.cs
new file mode 100644
index 00000000..b524b978
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data.Helpers/UpshotExtensions.cs
@@ -0,0 +1,318 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Globalization;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Text;
+using System.Web;
+using System.Web.Http;
+using System.Web.Http.Controllers;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Http.Data.Helpers
+{
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static class UpshotExtensions
+ {
+ public static UpshotConfigBuilder UpshotContext(this HtmlHelper htmlHelper)
+ {
+ return UpshotContext(htmlHelper, false);
+ }
+
+ public static UpshotConfigBuilder UpshotContext(this HtmlHelper htmlHelper, bool bufferChanges)
+ {
+ return new UpshotConfigBuilder(htmlHelper, bufferChanges);
+ }
+ }
+
+ public class UpshotConfigBuilder : IHtmlString
+ {
+ private readonly HtmlHelper htmlHelper;
+ private readonly bool bufferChanges;
+ private readonly IDictionary<string, IDataSourceConfig> dataSources = new Dictionary<string, IDataSourceConfig>();
+ private readonly IDictionary<Type, string> clientMappings = new Dictionary<Type, string>();
+
+ public UpshotConfigBuilder(HtmlHelper htmlHelper, bool bufferChanges)
+ {
+ this.htmlHelper = htmlHelper;
+ this.bufferChanges = bufferChanges;
+ }
+
+ private interface IDataSourceConfig
+ {
+ string ClientName { get; }
+ Type DataControllerType { get; }
+ string SharedDataContextExpression { get; }
+ string DataContextExpression { set; }
+ string ClientMappingsJson { set; }
+ string GetInitializationScript();
+ }
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Following established design pattern for HTML helpers.")]
+ public UpshotConfigBuilder DataSource<TDataController>(Expression<Func<TDataController, object>> queryOperation) where TDataController : DataController
+ {
+ return this.DataSource<TDataController>(queryOperation, null, null);
+ }
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "Following established design pattern for HTML helpers."),
+ System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Following established design pattern for HTML helpers.")]
+ public UpshotConfigBuilder DataSource<TDataController>(Expression<Func<TDataController, object>> queryOperation, string serviceUrl, string clientName) where TDataController : DataController
+ {
+ IDataSourceConfig dataSourceConfig = new DataSourceConfig<TDataController>(htmlHelper, bufferChanges, queryOperation, serviceUrl, clientName);
+ if (dataSources.ContainsKey(dataSourceConfig.ClientName))
+ {
+ throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Cannot have multiple data sources with the same clientName. Found multiple data sources with the name '{0}'", dataSourceConfig.ClientName));
+ }
+ dataSources.Add(dataSourceConfig.ClientName, dataSourceConfig);
+ return this;
+ }
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "Following established design pattern for HTML helpers.")]
+ public UpshotConfigBuilder ClientMapping<TEntity>(string clientConstructor)
+ {
+ if (string.IsNullOrEmpty(clientConstructor))
+ {
+ throw new ArgumentException("clientConstructor cannot be null or empty", "clientConstructor");
+ }
+ if (clientMappings.ContainsKey(typeof(TEntity)))
+ {
+ throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Cannot have multiple client mappings for the same entity type. Found multiple client mappings for '{0}'", typeof(TEntity).FullName));
+ }
+ clientMappings.Add(typeof(TEntity), clientConstructor);
+ return this;
+ }
+
+ public string ToHtmlString()
+ {
+ StringBuilder js = new StringBuilder("upshot.dataSources = upshot.dataSources || {};\n");
+
+ // First emit metadata for each referenced DataController
+ IEnumerable<Type> dataControllerTypes = dataSources.Select(x => x.Value.DataControllerType).Distinct();
+ foreach (Type dataControllerType in dataControllerTypes)
+ {
+ js.AppendFormat("upshot.metadata({0});\n", GetMetadata(dataControllerType));
+ }
+
+ // Let the first dataSource construct a dataContext, and all subsequent ones share it
+ IEnumerable<IDataSourceConfig> allDataSources = dataSources.Values;
+ IDataSourceConfig firstDataSource = allDataSources.FirstOrDefault();
+ if (firstDataSource != null)
+ {
+ // All but the first data source share the DataContext implicitly instantiated by the first.
+ foreach (IDataSourceConfig dataSource in allDataSources.Skip(1))
+ {
+ dataSource.DataContextExpression = firstDataSource.SharedDataContextExpression;
+ }
+
+ // Let the first dataSource define the client mappings
+ firstDataSource.ClientMappingsJson = GetClientMappingsObjectLiteral();
+ }
+
+ // Now emit initialization code for each dataSource
+ foreach (IDataSourceConfig dataSource in allDataSources)
+ {
+ js.AppendLine("\n" + dataSource.GetInitializationScript());
+ }
+
+ // Also record the mapping functions in use
+ foreach (var mapping in clientMappings)
+ {
+ js.AppendFormat("upshot.registerType(\"{0}\", function() {{ return {1} }});\n", EncodeServerTypeName(mapping.Key), mapping.Value);
+ }
+
+ return string.Format(CultureInfo.InvariantCulture, "<script type='text/javascript'>\n{0}</script>", js);
+ }
+
+ private string GetMetadata(Type dataControllerType)
+ {
+ var methodInfo = typeof(MetadataExtensions).GetMethod("Metadata");
+ var result = (IHtmlString)methodInfo.MakeGenericMethod(dataControllerType).Invoke(null, new[] { htmlHelper });
+ return result.ToHtmlString();
+ }
+
+ private string GetClientMappingsObjectLiteral()
+ {
+ IEnumerable<string> clientMappingStrings =
+ clientMappings.Select(
+ clientMapping => string.Format(CultureInfo.InvariantCulture, "\"{0}\": function(data) {{ return new {1}(data) }}", EncodeServerTypeName(clientMapping.Key), clientMapping.Value));
+ return string.Format(CultureInfo.InvariantCulture, "{{{0}}}", string.Join(",", clientMappingStrings));
+ }
+
+ // TODO: Duplicated from DataControllerMetadataGenerator.cs. Refactor when combining this into the main System.Web.Http.Data.Helper assembly.
+ private static string EncodeServerTypeName(Type type)
+ {
+ return String.Format(CultureInfo.InvariantCulture, "{0}{1}{2}", type.Name, ":#", type.Namespace);
+ }
+
+ private class DataSourceConfig<TDataController> : IDataSourceConfig where TDataController : DataController
+ {
+ private readonly HtmlHelper htmlHelper;
+ private readonly bool bufferChanges;
+ private readonly Expression<Func<TDataController, object>> queryOperation;
+ private readonly string serviceUrlOverride;
+ private readonly string clientName;
+
+ public DataSourceConfig(HtmlHelper htmlHelper, bool bufferChanges, Expression<Func<TDataController, object>> queryOperation, string serviceUrlOverride, string clientName)
+ {
+ this.htmlHelper = htmlHelper;
+ this.bufferChanges = bufferChanges;
+ this.queryOperation = queryOperation;
+ this.serviceUrlOverride = serviceUrlOverride;
+ this.clientName = string.IsNullOrEmpty(clientName) ? DefaultClientName : clientName;
+ }
+
+ public string ClientName
+ {
+ get
+ {
+ return clientName;
+ }
+ }
+
+ public Type DataControllerType
+ {
+ get
+ {
+ return typeof(TDataController);
+ }
+ }
+
+ public string DataContextExpression { private get; set; }
+
+ public string ClientMappingsJson { private get; set; }
+
+ public string SharedDataContextExpression
+ {
+ get
+ {
+ return ClientExpression + ".getDataContext()";
+ }
+ }
+
+ private string ClientExpression
+ {
+ get
+ {
+ return "upshot.dataSources." + ClientName;
+ }
+ }
+
+ private Type EntityType
+ {
+ get
+ {
+ Type operationReturnType = OperationMethod.ReturnType;
+ Type genericTypeDefinition = operationReturnType.IsGenericType ? operationReturnType.GetGenericTypeDefinition() : null;
+ Type entityType;
+ if (genericTypeDefinition != null && (genericTypeDefinition == typeof(IQueryable<>) || genericTypeDefinition == typeof(IEnumerable<>)))
+ {
+ // Permits IQueryable<TEntity> and IEnumerable<TEntity>.
+ entityType = operationReturnType.GetGenericArguments().Single();
+ }
+ else
+ {
+ entityType = operationReturnType;
+ }
+
+ if (!Description.EntityTypes.Any(type => type == entityType))
+ {
+ throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "queryOperation '{0}' must return an entity type or an IEnumerable/IQueryable of an entity type", OperationMethod.Name));
+ }
+
+ return entityType;
+ }
+ }
+
+ private string ServiceUrl
+ {
+ get
+ {
+ if (!string.IsNullOrEmpty(serviceUrlOverride))
+ {
+ return serviceUrlOverride;
+ }
+
+ UrlHelper urlHelper = new UrlHelper(htmlHelper.ViewContext.RequestContext);
+ string dataControllerName = typeof(TDataController).Name;
+ if (!dataControllerName.EndsWith("Controller", StringComparison.OrdinalIgnoreCase))
+ {
+ throw new ArgumentException("DataController type name must end with 'Controller'");
+ }
+ string controllerRouteName = dataControllerName.Substring(0, dataControllerName.Length - "Controller".Length);
+ return urlHelper.RouteUrl(new { controller = controllerRouteName, action = UrlParameter.Optional, httproute = true });
+ }
+ }
+
+ private string DefaultClientName
+ {
+ get
+ {
+ string operationName = OperationMethod.Name;
+ // By convention, strip away any "Get" verb on the method. Clients can override by explictly specifying client name.
+ return operationName.StartsWith("Get", StringComparison.OrdinalIgnoreCase) && operationName.Length > 3 && char.IsLetter(operationName[3]) ? operationName.Substring(3) : operationName;
+ }
+ }
+
+ private MethodInfo OperationMethod
+ {
+ get
+ {
+ Expression body = queryOperation.Body;
+
+ // The VB compiler will inject a convert to object here.
+ if (body.NodeType == ExpressionType.Convert)
+ {
+ UnaryExpression convert = (UnaryExpression)body;
+ if (convert.Type == typeof(object))
+ {
+ body = convert.Operand;
+ }
+ }
+
+ MethodCallExpression methodCall = body as MethodCallExpression;
+ if (methodCall == null)
+ {
+ throw new ArgumentException("queryOperation must be a method call");
+ }
+
+ if (!methodCall.Method.DeclaringType.IsAssignableFrom(typeof(TDataController)))
+ {
+ throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "queryOperation must be a method on '{0}' or a base type", typeof(TDataController).Name));
+ }
+
+ return methodCall.Method;
+ }
+ }
+
+ private static DataControllerDescription Description
+ {
+ get
+ {
+ HttpControllerDescriptor controllerDescriptor = new HttpControllerDescriptor
+ {
+ Configuration = GlobalConfiguration.Configuration, // This helper can't be run until after global app init.
+ ControllerType = typeof(TDataController)
+ };
+
+ DataControllerDescription description = DataControllerDescription.GetDescription(controllerDescriptor);
+ return description;
+ }
+ }
+
+ public string GetInitializationScript()
+ {
+ return string.Format(CultureInfo.InvariantCulture, @"{0} = upshot.RemoteDataSource({{
+ providerParameters: {{ url: ""{1}"", operationName: ""{2}"" }},
+ entityType: ""{3}"",
+ bufferChanges: {4},
+ dataContext: {5},
+ mapping: {6}
+}});",
+ ClientExpression, ServiceUrl, OperationMethod.Name, EncodeServerTypeName(EntityType),
+ bufferChanges ? "true" : "false", DataContextExpression ?? "undefined", ClientMappingsJson ?? "undefined");
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data.Helpers/packages.config b/src/Microsoft.Web.Http.Data.Helpers/packages.config
new file mode 100644
index 00000000..c611f43d
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data.Helpers/packages.config
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="Microsoft.Net.Http" version="2.0.20302.1" />
+</packages> \ No newline at end of file
diff --git a/src/Microsoft.Web.Http.Data/ChangeOperation.cs b/src/Microsoft.Web.Http.Data/ChangeOperation.cs
new file mode 100644
index 00000000..2089e7b7
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data/ChangeOperation.cs
@@ -0,0 +1,33 @@
+namespace Microsoft.Web.Http.Data
+{
+ /// <summary>
+ /// Enumeration of the types of operations a <see cref="DataController"/> can perform.
+ /// </summary>
+ public enum ChangeOperation
+ {
+ /// <summary>
+ /// Indicates that no operation is to be performed
+ /// </summary>
+ None,
+
+ /// <summary>
+ /// Indicates an operation that inserts new data
+ /// </summary>
+ Insert,
+
+ /// <summary>
+ /// Indicates an operation that updates existing data
+ /// </summary>
+ Update,
+
+ /// <summary>
+ /// Indicates an operation that deletes existing data
+ /// </summary>
+ Delete,
+
+ /// <summary>
+ /// Indicates a custom update operation
+ /// </summary>
+ Custom
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data/ChangeSet.cs b/src/Microsoft.Web.Http.Data/ChangeSet.cs
new file mode 100644
index 00000000..d6f4d83e
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data/ChangeSet.cs
@@ -0,0 +1,335 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.Globalization;
+using System.Linq;
+using System.Reflection;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+
+namespace Microsoft.Web.Http.Data
+{
+ /// <summary>
+ /// Represents a set of changes to be processed by a <see cref="DataController"/>.
+ /// </summary>
+ public sealed class ChangeSet
+ {
+ private IEnumerable<ChangeSetEntry> _changeSetEntries;
+
+ /// <summary>
+ /// Initializes a new instance of the ChangeSet class
+ /// </summary>
+ /// <param name="changeSetEntries">The set of <see cref="ChangeSetEntry"/> items this <see cref="ChangeSet"/> represents.</param>
+ /// <exception cref="ArgumentNullException">if <paramref name="changeSetEntries"/> is null.</exception>
+ public ChangeSet(IEnumerable<ChangeSetEntry> changeSetEntries)
+ {
+ if (changeSetEntries == null)
+ {
+ throw Error.ArgumentNull("changeSetEntries");
+ }
+
+ // ensure the changeset is valid
+ ValidateChangeSetEntries(changeSetEntries);
+
+ _changeSetEntries = changeSetEntries;
+ }
+
+ /// <summary>
+ /// Gets the set of <see cref="ChangeSetEntry"/> items this <see cref="ChangeSet"/> represents.
+ /// </summary>
+ public ReadOnlyCollection<ChangeSetEntry> ChangeSetEntries
+ {
+ get { return _changeSetEntries.ToList().AsReadOnly(); }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether any of the <see cref="ChangeSetEntry"/> items has an error.
+ /// </summary>
+ public bool HasError
+ {
+ get { return _changeSetEntries.Any(op => op.HasConflict || (op.ValidationErrors != null && op.ValidationErrors.Any())); }
+ }
+
+ /// <summary>
+ /// Returns the original unmodified entity for the provided <paramref name="clientEntity"/>.
+ /// </summary>
+ /// <remarks>
+ /// Note that only members marked with <see cref="RoundtripOriginalAttribute"/> will be set
+ /// in the returned instance.
+ /// </remarks>
+ /// <typeparam name="TEntity">The entity type.</typeparam>
+ /// <param name="clientEntity">The client modified entity.</param>
+ /// <returns>The original unmodified entity for the provided <paramref name="clientEntity"/>.</returns>
+ /// <exception cref="ArgumentNullException">if <paramref name="clientEntity"/> is null.</exception>
+ /// <exception cref="ArgumentException">if <paramref name="clientEntity"/> is not in the change set.</exception>
+ public TEntity GetOriginal<TEntity>(TEntity clientEntity) where TEntity : class
+ {
+ if (clientEntity == null)
+ {
+ throw Error.ArgumentNull("clientEntity");
+ }
+
+ ChangeSetEntry entry = _changeSetEntries.FirstOrDefault(p => Object.ReferenceEquals(p.Entity, clientEntity));
+ if (entry == null)
+ {
+ throw Error.Argument(Resource.ChangeSet_ChangeSetEntryNotFound);
+ }
+
+ if (entry.Operation == ChangeOperation.Insert)
+ {
+ throw Error.InvalidOperation(Resource.ChangeSet_OriginalNotValidForInsert);
+ }
+
+ return (TEntity)entry.OriginalEntity;
+ }
+
+ /// <summary>
+ /// Validates that the specified entries are well formed.
+ /// </summary>
+ /// <param name="changeSetEntries">The changeset entries to validate.</param>
+ private static void ValidateChangeSetEntries(IEnumerable<ChangeSetEntry> changeSetEntries)
+ {
+ HashSet<int> idSet = new HashSet<int>();
+ HashSet<object> entitySet = new HashSet<object>();
+ foreach (ChangeSetEntry entry in changeSetEntries)
+ {
+ // ensure Entity is not null
+ if (entry.Entity == null)
+ {
+ throw Error.InvalidOperation(Resource.InvalidChangeSet, Resource.InvalidChangeSet_NullEntity);
+ }
+
+ // ensure unique client IDs
+ if (idSet.Contains(entry.Id))
+ {
+ throw Error.InvalidOperation(Resource.InvalidChangeSet, Resource.InvalidChangeSet_DuplicateId);
+ }
+ idSet.Add(entry.Id);
+
+ // ensure unique entity instances - there can only be a single entry
+ // for a given entity instance
+ if (entitySet.Contains(entry.Entity))
+ {
+ throw Error.InvalidOperation(Resource.InvalidChangeSet, Resource.InvalidChangeSet_DuplicateEntity);
+ }
+ entitySet.Add(entry.Entity);
+
+ // entities must be of the same type
+ if (entry.OriginalEntity != null && !(entry.Entity.GetType() == entry.OriginalEntity.GetType()))
+ {
+ throw Error.InvalidOperation(Resource.InvalidChangeSet, Resource.InvalidChangeSet_MustBeSameType);
+ }
+
+ if (entry.Operation == ChangeOperation.Insert && entry.OriginalEntity != null)
+ {
+ throw Error.InvalidOperation(Resource.InvalidChangeSet, Resource.InvalidChangeSet_InsertsCantHaveOriginal);
+ }
+ }
+
+ // now that we have the full Id space, we can validate associations
+ foreach (ChangeSetEntry entry in changeSetEntries)
+ {
+ if (entry.Associations != null)
+ {
+ ValidateAssociationMap(entry.Entity.GetType(), idSet, entry.Associations);
+ }
+
+ if (entry.OriginalAssociations != null)
+ {
+ ValidateAssociationMap(entry.Entity.GetType(), idSet, entry.OriginalAssociations);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Validates the specified association map.
+ /// </summary>
+ /// <param name="entityType">The entity type the association is on.</param>
+ /// <param name="idSet">The set of all unique Ids in the changeset.</param>
+ /// <param name="associationMap">The association map to validate.</param>
+ private static void ValidateAssociationMap(Type entityType, HashSet<int> idSet, IDictionary<string, int[]> associationMap)
+ {
+ PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(entityType);
+
+ foreach (var associationItem in associationMap)
+ {
+ // ensure that the member is an association member
+ string associationMemberName = associationItem.Key;
+ PropertyDescriptor associationMember = properties[associationMemberName];
+ if (associationMember == null || associationMember.Attributes[typeof(AssociationAttribute)] == null)
+ {
+ throw Error.InvalidOperation(Resource.InvalidChangeSet,
+ String.Format(CultureInfo.CurrentCulture, Resource.InvalidChangeSet_InvalidAssociationMember, entityType, associationMemberName));
+ }
+
+ // ensure that the id collection is not null
+ if (associationItem.Value == null)
+ {
+ throw Error.InvalidOperation(Resource.InvalidChangeSet,
+ String.Format(CultureInfo.CurrentCulture, Resource.InvalidChangeSet_AssociatedIdsCannotBeNull, entityType, associationMemberName));
+ }
+ // ensure that each Id specified is in the changeset
+ foreach (int id in associationItem.Value)
+ {
+ if (!idSet.Contains(id))
+ {
+ throw Error.InvalidOperation(Resource.InvalidChangeSet,
+ String.Format(CultureInfo.CurrentCulture, Resource.InvalidChangeSet_AssociatedIdNotInChangeset, id, entityType, associationMemberName));
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Reestablish associations based on Id lists by adding the referenced entities
+ /// to their association members
+ /// </summary>
+ internal void SetEntityAssociations()
+ {
+ // create a unique map from Id to entity instances, and update operations
+ // so Ids map to the same instances, since during deserialization reference
+ // identity is not maintained.
+ var entityIdMap = _changeSetEntries.ToDictionary(p => p.Id, p => new { Entity = p.Entity, OriginalEntity = p.OriginalEntity });
+ foreach (ChangeSetEntry changeSetEntry in _changeSetEntries)
+ {
+ object entity = entityIdMap[changeSetEntry.Id].Entity;
+ if (changeSetEntry.Entity != entity)
+ {
+ changeSetEntry.Entity = entity;
+ }
+
+ object original = entityIdMap[changeSetEntry.Id].OriginalEntity;
+ if (original != null && changeSetEntry.OriginalEntity != original)
+ {
+ changeSetEntry.OriginalEntity = original;
+ }
+ }
+
+ // for all entities with associations, reestablish the associations by mapping the Ids
+ // to entity instances and adding them to the association members
+ HashSet<int> visited = new HashSet<int>();
+ foreach (var entityGroup in _changeSetEntries.Where(p => (p.Associations != null && p.Associations.Count > 0) || (p.OriginalAssociations != null && p.OriginalAssociations.Count > 0)).GroupBy(p => p.Entity.GetType()))
+ {
+ Dictionary<string, PropertyDescriptor> associationMemberMap = TypeDescriptor.GetProperties(entityGroup.Key).Cast<PropertyDescriptor>().Where(p => p.Attributes[typeof(AssociationAttribute)] != null).ToDictionary(p => p.Name);
+ foreach (ChangeSetEntry changeSetEntry in entityGroup)
+ {
+ if (visited.Contains(changeSetEntry.Id))
+ {
+ continue;
+ }
+ visited.Add(changeSetEntry.Id);
+
+ // set current associations
+ if (changeSetEntry.Associations != null)
+ {
+ foreach (var associationItem in changeSetEntry.Associations)
+ {
+ PropertyDescriptor assocMember = associationMemberMap[associationItem.Key];
+ IEnumerable<object> children = associationItem.Value.Select(p => entityIdMap[p].Entity);
+ SetAssociationMember(changeSetEntry.Entity, assocMember, children);
+ }
+ }
+ }
+ }
+ }
+
+ internal bool Validate(HttpActionContext actionContext)
+ {
+ // Validate all entries except those with type None or Delete (since we don't want to validate
+ // entites we're going to delete).
+ bool success = true;
+ IEnumerable<ChangeSetEntry> entriesToValidate = ChangeSetEntries.Where(
+ p => (p.ActionDescriptor != null && p.Operation != ChangeOperation.None && p.Operation != ChangeOperation.Delete)
+ || (p.EntityActions != null && p.EntityActions.Any()));
+
+ foreach (ChangeSetEntry entry in entriesToValidate)
+ {
+ // TODO: optimize by determining whether a type actually requires any validation?
+ // TODO: support for method level / parameter validation?
+
+ List<ValidationResultInfo> validationErrors = new List<ValidationResultInfo>();
+ if (!DataControllerValidation.ValidateObject(entry.Entity, validationErrors, actionContext))
+ {
+ entry.ValidationErrors = validationErrors.Distinct(EqualityComparer<ValidationResultInfo>.Default).ToList();
+ success = false;
+ }
+
+ // clear after each validate call, since we've already
+ // copied over the errors
+ actionContext.ModelState.Clear();
+ }
+
+ return success;
+ }
+
+ /// <summary>
+ /// Adds the specified associated entities to the specified association member for the specified entity.
+ /// </summary>
+ /// <param name="entity">The entity</param>
+ /// <param name="associationProperty">The association member (singleton or collection)</param>
+ /// <param name="associatedEntities">Collection of associated entities</param>
+ private static void SetAssociationMember(object entity, PropertyDescriptor associationProperty, IEnumerable<object> associatedEntities)
+ {
+ if (associatedEntities.Count() == 0)
+ {
+ return;
+ }
+
+ object associationValue = associationProperty.GetValue(entity);
+ if (typeof(IEnumerable).IsAssignableFrom(associationProperty.PropertyType))
+ {
+ if (associationValue == null)
+ {
+ throw Error.InvalidOperation(Resource.DataController_AssociationCollectionPropertyIsNull, associationProperty.ComponentType.Name, associationProperty.Name);
+ }
+
+ IList list = associationValue as IList;
+ IEnumerable<object> associationSequence = null;
+ MethodInfo addMethod = null;
+ if (list == null)
+ {
+ // not an IList, so we have to use reflection
+ Type associatedEntityType = TypeUtility.GetElementType(associationValue.GetType());
+ addMethod = associationValue.GetType().GetMethod("Add", BindingFlags.Public | BindingFlags.Instance, null, new Type[] { associatedEntityType }, null);
+ if (addMethod == null)
+ {
+ throw Error.InvalidOperation(Resource.DataController_InvalidCollectionMember, associationProperty.Name);
+ }
+ associationSequence = ((IEnumerable)associationValue).Cast<object>();
+ }
+
+ foreach (object associatedEntity in associatedEntities)
+ {
+ // add the entity to the collection if it's not already there
+ if (list != null)
+ {
+ if (!list.Contains(associatedEntity))
+ {
+ list.Add(associatedEntity);
+ }
+ }
+ else
+ {
+ if (!associationSequence.Contains(associatedEntity))
+ {
+ addMethod.Invoke(associationValue, new object[] { associatedEntity });
+ }
+ }
+ }
+ }
+ else
+ {
+ // set the reference if it's not already set
+ object associatedEntity = associatedEntities.Single();
+ object currentValue = associationProperty.GetValue(entity);
+ if (!Object.Equals(currentValue, associatedEntity))
+ {
+ associationProperty.SetValue(entity, associatedEntity);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data/ChangeSetEntry.cs b/src/Microsoft.Web.Http.Data/ChangeSetEntry.cs
new file mode 100644
index 00000000..3c99097e
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data/ChangeSetEntry.cs
@@ -0,0 +1,100 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Runtime.Serialization;
+
+namespace Microsoft.Web.Http.Data
+{
+ /// <summary>
+ /// Represents a change operation to be performed on an entity.
+ /// </summary>
+ [DataContract]
+ [DebuggerDisplay("Operation = {Operation}, Type = {Entity.GetType().Name}")]
+ public sealed class ChangeSetEntry
+ {
+ /// <summary>
+ /// Gets or sets the client ID for the entity
+ /// </summary>
+ [DataMember]
+ public int Id { get; set; }
+
+ /// <summary>
+ /// Gets or sets the <see cref="ChangeOperation"/> to be performed on the entity.
+ /// </summary>
+ [DataMember]
+ public ChangeOperation Operation { get; set; }
+
+ /// <summary>
+ /// Gets or sets the <see cref="Entity"/> being operated on
+ /// </summary>
+ [DataMember]
+ public object Entity { get; set; }
+
+ /// <summary>
+ /// Gets or sets the original state of the entity being operated on
+ /// </summary>
+ [DataMember(EmitDefaultValue = false)]
+ public object OriginalEntity { get; set; }
+
+ /// <summary>
+ /// Gets or sets the state of the entity in the data store
+ /// </summary>
+ [DataMember(EmitDefaultValue = false)]
+ public object StoreEntity { get; set; }
+
+ /// <summary>
+ /// Gets or sets the custom methods invoked on the entity, as a set
+ /// of method name / parameter set pairs.
+ /// </summary>
+ [DataMember(EmitDefaultValue = false)]
+ public IDictionary<string, object[]> EntityActions { get; set; }
+
+ /// <summary>
+ /// Gets or sets the validation errors encountered during the processing of the operation.
+ /// </summary>
+ [DataMember(EmitDefaultValue = false)]
+ public IEnumerable<ValidationResultInfo> ValidationErrors { get; set; }
+
+ /// <summary>
+ /// Gets or sets the collection of members in conflict. The <see cref="StoreEntity"/> property
+ /// contains the current store value for each member in conflict.
+ /// </summary>
+ [DataMember(EmitDefaultValue = false)]
+ public IEnumerable<string> ConflictMembers { get; set; }
+
+ /// <summary>
+ /// Gets or sets whether the conflict is a delete conflict, meaning the
+ /// entity no longer exists in the store.
+ /// </summary>
+ [DataMember(EmitDefaultValue = false)]
+ public bool IsDeleteConflict { get; set; }
+
+ /// <summary>
+ /// Gets or sets the collection of IDs of the associated entities for
+ /// each association of the Entity
+ /// </summary>
+ [DataMember(EmitDefaultValue = false)]
+ public IDictionary<string, int[]> Associations { get; set; }
+
+ /// <summary>
+ /// Gets or sets the collection of IDs for each association of the <see cref="OriginalEntity"/>
+ /// </summary>
+ [DataMember(EmitDefaultValue = false)]
+ public IDictionary<string, int[]> OriginalAssociations { get; set; }
+
+ /// <summary>
+ /// Gets a value indicating whether the <see cref="ChangeSetEntry"/> contains conflicts.
+ /// </summary>
+ public bool HasConflict
+ {
+ get { return (IsDeleteConflict || (ConflictMembers != null && ConflictMembers.Any())); }
+ }
+
+ public bool HasError
+ {
+ get { return HasConflict || (ValidationErrors != null && ValidationErrors.Any()); }
+ }
+
+ internal UpdateActionDescriptor ActionDescriptor { get; set; }
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data/CustomizingActionDescriptor.cs b/src/Microsoft.Web.Http.Data/CustomizingActionDescriptor.cs
new file mode 100644
index 00000000..ee45d502
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data/CustomizingActionDescriptor.cs
@@ -0,0 +1,71 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Web.Http.Controllers;
+using System.Web.Http.Filters;
+
+namespace Microsoft.Web.Http.Data
+{
+ /// <summary>
+ /// A wrapper <see cref="HttpActionDescriptor"/> that customizes various aspects of the wrapped
+ /// inner descriptor, for example by adding additional action filters.
+ /// </summary>
+ internal sealed class CustomizingActionDescriptor : HttpActionDescriptor
+ {
+ private HttpActionDescriptor _innerDescriptor;
+
+ public CustomizingActionDescriptor(HttpActionDescriptor innerDescriptor)
+ {
+ _innerDescriptor = innerDescriptor;
+ Configuration = _innerDescriptor.Configuration;
+ ControllerDescriptor = _innerDescriptor.ControllerDescriptor;
+ }
+
+ public override string ActionName
+ {
+ get { return _innerDescriptor.ActionName; }
+ }
+
+ public override Type ReturnType
+ {
+ get { return _innerDescriptor.ReturnType; }
+ }
+
+ public override object Execute(HttpControllerContext controllerContext, IDictionary<string, object> arguments)
+ {
+ return _innerDescriptor.Execute(controllerContext, arguments);
+ }
+
+ public override Collection<HttpParameterDescriptor> GetParameters()
+ {
+ return _innerDescriptor.GetParameters();
+ }
+
+ public override Collection<FilterInfo> GetFilterPipeline()
+ {
+ Collection<FilterInfo> filters = new Collection<FilterInfo>(_innerDescriptor.GetFilterPipeline());
+
+ // for any actions that support query composition, we need to add our
+ // query filter as well. This must be added immediately after the
+ // QueryCompositionFilterAttribute.
+ // TODO: once filter ordering is supported, there may be a better way
+ // than searching on type name like this.
+ bool addFilter = false;
+ int idx = 0;
+ for (idx = 0; idx < filters.Count; idx++)
+ {
+ if (filters[idx].Instance.GetType().Name == "QueryCompositionFilterAttribute")
+ {
+ addFilter = true;
+ break;
+ }
+ }
+ if (addFilter)
+ {
+ filters.Insert(idx, new FilterInfo(new QueryFilterAttribute(), FilterScope.Action));
+ }
+
+ return filters;
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data/DataController.cs b/src/Microsoft.Web.Http.Data/DataController.cs
new file mode 100644
index 00000000..5def4978
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data/DataController.cs
@@ -0,0 +1,348 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel.DataAnnotations;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Net.Http;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+
+namespace Microsoft.Web.Http.Data
+{
+ [HttpControllerConfiguration(HttpActionInvoker = typeof(DataControllerActionInvoker), HttpActionSelector = typeof(DataControllerActionSelector), ActionValueBinder = typeof(DataControllerActionValueBinder))]
+ public abstract class DataController : ApiController
+ {
+ private ChangeSet _changeSet;
+ private DataControllerDescription _description;
+
+ /// <summary>
+ /// Gets the current <see cref="ChangeSet"/>. Returns null if no change operations are being performed.
+ /// </summary>
+ protected ChangeSet ChangeSet
+ {
+ get { return _changeSet; }
+ }
+
+ /// <summary>
+ /// Gets the <see cref="DataControllerDescription"/> for this <see cref="DataController"/>.
+ /// </summary>
+ protected DataControllerDescription Description
+ {
+ get { return _description; }
+ }
+
+ /// <summary>
+ /// Gets the <see cref="HttpActionContext"/> for the currently executing action.
+ /// </summary>
+ protected internal HttpActionContext ActionContext { get; internal set; }
+
+ protected override void Initialize(HttpControllerContext controllerContext)
+ {
+ // ensure that the service is valid and all custom metadata providers
+ // have been registered
+ _description = DataControllerDescription.GetDescription(controllerContext.ControllerDescriptor);
+
+ base.Initialize(controllerContext);
+ }
+
+ /// <summary>
+ /// Performs the operations indicated by the specified <see cref="ChangeSet"/> by invoking
+ /// the corresponding actions for each.
+ /// </summary>
+ /// <param name="changeSet">The changeset to submit</param>
+ /// <returns>True if the submit was successful, false otherwise.</returns>
+ public virtual bool Submit(ChangeSet changeSet)
+ {
+ if (changeSet == null)
+ {
+ throw Error.ArgumentNull("changeSet");
+ }
+ _changeSet = changeSet;
+
+ ResolveActions(_description, ChangeSet.ChangeSetEntries);
+
+ if (!AuthorizeChangeSet())
+ {
+ // Don't try to save if there were any errors.
+ return false;
+ }
+
+ // Before invoking any operations, validate the entire changeset
+ if (!ValidateChangeSet())
+ {
+ return false;
+ }
+
+ // Now that we're validated, proceed to invoke the actions.
+ if (!ExecuteChangeSet())
+ {
+ return false;
+ }
+
+ // persist the changes
+ if (!PersistChangeSetInternal())
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Caller is responsible for the lifetime of the object")]
+ public override Task<HttpResponseMessage> ExecuteAsync(HttpControllerContext controllerContext, CancellationToken cancellationToken)
+ {
+ return base.ExecuteAsync(controllerContext, cancellationToken)
+ .Then<HttpResponseMessage, HttpResponseMessage>(response =>
+ {
+ int totalCount;
+ IEnumerable results;
+ if (response != null &&
+ controllerContext.Request.Properties.TryGetValue<int>(QueryFilterAttribute.TotalCountKey, out totalCount) &&
+ response.TryGetObjectValue(out results))
+ {
+ HttpResponseMessage oldResponse = response;
+ // Client has requested the total count, so the actual response content will contain
+ // the query results as well as the count. Create a new ObjectContent for the query results.
+ // Because this code does not specify any formatters explicitly, it will use the
+ // formatters in the configuration.
+ QueryResult queryResult = new QueryResult(results, totalCount);
+ response = response.RequestMessage.CreateResponse(oldResponse.StatusCode, queryResult);
+
+ foreach (var header in oldResponse.Headers)
+ {
+ response.Headers.Add(header.Key, header.Value);
+ }
+ // TODO what about content headers?
+
+ oldResponse.RequestMessage = null;
+ oldResponse.Dispose();
+ }
+
+ return response;
+ });
+ }
+
+ /// <summary>
+ /// For all operations in the current changeset, validate that the operation exists, and
+ /// set the operation entry.
+ /// </summary>
+ internal static void ResolveActions(DataControllerDescription description, IEnumerable<ChangeSetEntry> changeSet)
+ {
+ // Resolve and set the action for each operation in the changeset
+ foreach (ChangeSetEntry changeSetEntry in changeSet)
+ {
+ Type entityType = changeSetEntry.Entity.GetType();
+ UpdateActionDescriptor actionDescriptor = null;
+ if (changeSetEntry.Operation == ChangeOperation.Insert ||
+ changeSetEntry.Operation == ChangeOperation.Update ||
+ changeSetEntry.Operation == ChangeOperation.Delete)
+ {
+ actionDescriptor = description.GetUpdateAction(entityType, changeSetEntry.Operation);
+ }
+
+ // if a custom method invocation is specified, validate that the method exists
+ bool isCustomUpdate = false;
+ if (changeSetEntry.EntityActions != null && changeSetEntry.EntityActions.Any())
+ {
+ var entityAction = changeSetEntry.EntityActions.Single();
+ UpdateActionDescriptor customMethodOperation = description.GetCustomMethod(entityType, entityAction.Key);
+ if (customMethodOperation == null)
+ {
+ throw Error.InvalidOperation(Resource.DataController_InvalidAction, entityAction.Key, entityType.Name);
+ }
+
+ // if the primary action for an update is null but the entry
+ // contains a valid custom update action, its considered a "custom update"
+ isCustomUpdate = actionDescriptor == null && customMethodOperation != null;
+ }
+
+ if (actionDescriptor == null && !isCustomUpdate)
+ {
+ throw Error.InvalidOperation(Resource.DataController_InvalidAction, changeSetEntry.Operation.ToString(), entityType.Name);
+ }
+
+ changeSetEntry.ActionDescriptor = actionDescriptor;
+ }
+ }
+
+ /// <summary>
+ /// Verifies the user is authorized to submit the current <see cref="ChangeSet"/>.
+ /// </summary>
+ /// <returns>True if the <see cref="ChangeSet"/> is authorized, false otherwise.</returns>
+ protected virtual bool AuthorizeChangeSet()
+ {
+ foreach (ChangeSetEntry changeSetEntry in ChangeSet.ChangeSetEntries)
+ {
+ if (!changeSetEntry.ActionDescriptor.Authorize(ActionContext))
+ {
+ return false;
+ }
+
+ // if there are any custom method invocations for this operation
+ // we need to authorize them as well
+ if (changeSetEntry.EntityActions != null && changeSetEntry.EntityActions.Any())
+ {
+ Type entityType = changeSetEntry.Entity.GetType();
+ foreach (var entityAction in changeSetEntry.EntityActions)
+ {
+ UpdateActionDescriptor customAction = Description.GetCustomMethod(entityType, entityAction.Key);
+ if (!customAction.Authorize(ActionContext))
+ {
+ return false;
+ }
+ }
+ }
+ }
+
+ return !ChangeSet.HasError;
+ }
+
+ /// <summary>
+ /// Validates the current <see cref="ChangeSet"/>. Any errors should be set on the individual <see cref="ChangeSetEntry"/>s
+ /// in the <see cref="ChangeSet"/>.
+ /// </summary>
+ /// <returns><c>True</c> if all operations in the <see cref="ChangeSet"/> passed validation, <c>false</c> otherwise.</returns>
+ protected virtual bool ValidateChangeSet()
+ {
+ return ChangeSet.Validate(ActionContext);
+ }
+
+ /// <summary>
+ /// This method invokes the action for each operation in the current <see cref="ChangeSet"/>.
+ /// </summary>
+ /// <returns>True if the <see cref="ChangeSet"/> was processed successfully, false otherwise.</returns>
+ protected virtual bool ExecuteChangeSet()
+ {
+ InvokeCUDOperations();
+ InvokeCustomUpdateOperations();
+
+ return !ChangeSet.HasError;
+ }
+
+ private void InvokeCUDOperations()
+ {
+ foreach (ChangeSetEntry changeSetEntry in ChangeSet.ChangeSetEntries
+ .Where(op => op.Operation == ChangeOperation.Insert ||
+ op.Operation == ChangeOperation.Update ||
+ op.Operation == ChangeOperation.Delete))
+ {
+ if (changeSetEntry.ActionDescriptor == null)
+ {
+ continue;
+ }
+
+ InvokeAction(changeSetEntry.ActionDescriptor, new object[] { changeSetEntry.Entity }, changeSetEntry);
+ }
+ }
+
+ private void InvokeCustomUpdateOperations()
+ {
+ foreach (ChangeSetEntry changeSetEntry in ChangeSet.ChangeSetEntries.Where(op => op.EntityActions != null && op.EntityActions.Any()))
+ {
+ Type entityType = changeSetEntry.Entity.GetType();
+ foreach (var entityAction in changeSetEntry.EntityActions)
+ {
+ UpdateActionDescriptor customUpdateAction = Description.GetCustomMethod(entityType, entityAction.Key);
+
+ List<object> customMethodParams = new List<object>(entityAction.Value);
+ customMethodParams.Insert(0, changeSetEntry.Entity);
+
+ InvokeAction(customUpdateAction, customMethodParams.ToArray(), changeSetEntry);
+ }
+ }
+ }
+
+ private void InvokeAction(HttpActionDescriptor action, object[] parameters, ChangeSetEntry changeSetEntry)
+ {
+ try
+ {
+ Collection<HttpParameterDescriptor> pds = action.GetParameters();
+ Dictionary<string, object> paramMap = new Dictionary<string, object>(pds.Count);
+ for (int i = 0; i < pds.Count; i++)
+ {
+ paramMap.Add(pds[i].ParameterName, parameters[i]);
+ }
+ action.Execute(ActionContext.ControllerContext, paramMap);
+ }
+ catch (TargetInvocationException tie)
+ {
+ ValidationException vex = tie.GetBaseException() as ValidationException;
+ if (vex != null)
+ {
+ ValidationResultInfo error = new ValidationResultInfo(vex.Message, 0, String.Empty, vex.ValidationResult.MemberNames);
+ if (changeSetEntry.ValidationErrors != null)
+ {
+ changeSetEntry.ValidationErrors = changeSetEntry.ValidationErrors.Concat(new ValidationResultInfo[] { error }).ToArray();
+ }
+ else
+ {
+ changeSetEntry.ValidationErrors = new ValidationResultInfo[] { error };
+ }
+ }
+ else
+ {
+ throw;
+ }
+ }
+ }
+
+ /// <summary>
+ /// This method is called to finalize changes after all the operations in the current <see cref="ChangeSet"/>
+ /// have been invoked. This method should commit the changes as necessary to the data store.
+ /// Any errors should be set on the individual <see cref="ChangeSetEntry"/>s in the <see cref="ChangeSet"/>.
+ /// </summary>
+ /// <returns>True if the <see cref="ChangeSet"/> was persisted successfully, false otherwise.</returns>
+ protected virtual bool PersistChangeSet()
+ {
+ return true;
+ }
+
+ /// <summary>
+ /// This method invokes the user overridable <see cref="PersistChangeSet"/> method wrapping the call
+ /// with the appropriate exception handling logic. All framework calls to <see cref="PersistChangeSet"/>
+ /// must go through this method. Some data sources have their own validation hook points,
+ /// so if a <see cref="ValidationException"/> is thrown at that level, we want to capture it.
+ /// </summary>
+ /// <returns>True if the <see cref="ChangeSet"/> was persisted successfully, false otherwise.</returns>
+ private bool PersistChangeSetInternal()
+ {
+ try
+ {
+ PersistChangeSet();
+ }
+ catch (ValidationException e)
+ {
+ // if a validation exception is thrown for one of the entities in the changeset
+ // set the error on the corresponding ChangeSetEntry
+ if (e.Value != null && e.ValidationResult != null)
+ {
+ IEnumerable<ChangeSetEntry> updateOperations =
+ ChangeSet.ChangeSetEntries.Where(
+ p => p.Operation == ChangeOperation.Insert ||
+ p.Operation == ChangeOperation.Update ||
+ p.Operation == ChangeOperation.Delete);
+
+ ChangeSetEntry operation = updateOperations.SingleOrDefault(p => Object.ReferenceEquals(p.Entity, e.Value));
+ if (operation != null)
+ {
+ ValidationResultInfo error = new ValidationResultInfo(e.ValidationResult.ErrorMessage, e.ValidationResult.MemberNames);
+ error.StackTrace = e.StackTrace;
+ operation.ValidationErrors = new List<ValidationResultInfo>() { error };
+ }
+ }
+ else
+ {
+ throw;
+ }
+ }
+
+ return !ChangeSet.HasError;
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data/DataControllerActionInvoker.cs b/src/Microsoft.Web.Http.Data/DataControllerActionInvoker.cs
new file mode 100644
index 00000000..a109ad1f
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data/DataControllerActionInvoker.cs
@@ -0,0 +1,17 @@
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Controllers;
+
+namespace Microsoft.Web.Http.Data
+{
+ public sealed class DataControllerActionInvoker : ApiControllerActionInvoker
+ {
+ public override Task<HttpResponseMessage> InvokeActionAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
+ {
+ DataController controller = (DataController)actionContext.ControllerContext.Controller;
+ controller.ActionContext = actionContext;
+ return base.InvokeActionAsync(actionContext, cancellationToken);
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data/DataControllerActionSelector.cs b/src/Microsoft.Web.Http.Data/DataControllerActionSelector.cs
new file mode 100644
index 00000000..c6489872
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data/DataControllerActionSelector.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Web.Http;
+using System.Web.Http.Controllers;
+
+namespace Microsoft.Web.Http.Data
+{
+ public sealed class DataControllerActionSelector : ApiControllerActionSelector
+ {
+ private const string ActionRouteKey = "action";
+ private const string SubmitActionValue = "Submit";
+
+ public override HttpActionDescriptor SelectAction(HttpControllerContext controllerContext)
+ {
+ // first check to see if this is a call to Submit
+ string actionName;
+ if (controllerContext.RouteData.Values.TryGetValue(ActionRouteKey, out actionName) && actionName.Equals(SubmitActionValue, StringComparison.Ordinal))
+ {
+ return new SubmitActionDescriptor(controllerContext.ControllerDescriptor, controllerContext.Controller.GetType());
+ }
+
+ // next check to see if this is a direct invocation of a CUD action
+ DataControllerDescription description = DataControllerDescription.GetDescription(controllerContext.ControllerDescriptor);
+ UpdateActionDescriptor action = description.GetUpdateAction(actionName);
+ if (action != null)
+ {
+ return new SubmitProxyActionDescriptor(action);
+ }
+
+ // for all other non-CUD operations, we wrap the descriptor in our
+ // customizing descriptor to layer on additional functionality.
+ return new CustomizingActionDescriptor(base.SelectAction(controllerContext));
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data/DataControllerActionValueBinder.cs b/src/Microsoft.Web.Http.Data/DataControllerActionValueBinder.cs
new file mode 100644
index 00000000..b8e514f9
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data/DataControllerActionValueBinder.cs
@@ -0,0 +1,105 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http.Formatting;
+using System.Runtime.Serialization;
+using System.Web.Http;
+using System.Web.Http.Controllers;
+using System.Web.Http.ModelBinding;
+using System.Web.Http.Validation;
+using Newtonsoft.Json;
+
+namespace Microsoft.Web.Http.Data
+{
+ public class DataControllerActionValueBinder : DefaultActionValueBinder
+ {
+ private static ConcurrentDictionary<Type, IEnumerable<SerializerInfo>> _serializerCache = new ConcurrentDictionary<Type, IEnumerable<SerializerInfo>>();
+
+ private MediaTypeFormatter[] _formatters;
+
+ protected override IEnumerable<MediaTypeFormatter> GetFormatters(HttpActionDescriptor actionDescriptor)
+ {
+ if (_formatters == null)
+ {
+ HttpControllerDescriptor descr = actionDescriptor.ControllerDescriptor;
+ HttpConfiguration config = actionDescriptor.Configuration;
+ DataControllerDescription dataDesc = DataControllerDescription.GetDescription(descr);
+
+ List<MediaTypeFormatter> list = new List<MediaTypeFormatter>();
+ AddFormattersFromConfig(list, config);
+ AddDataControllerFormatters(list, dataDesc);
+ _formatters = list.ToArray();
+ }
+
+ return _formatters;
+ }
+
+ protected override IBodyModelValidator GetBodyModelValidator(HttpActionDescriptor actionDescriptor)
+ {
+ return null;
+ }
+
+ private static void AddDataControllerFormatters(List<MediaTypeFormatter> formatters, DataControllerDescription description)
+ {
+ var cachedSerializers = _serializerCache.GetOrAdd(description.ControllerType, controllerType =>
+ {
+ // for the specified controller type, set the serializers for the built
+ // in framework types
+ List<SerializerInfo> serializers = new List<SerializerInfo>();
+
+ Type[] exposedTypes = description.EntityTypes.ToArray();
+ serializers.Add(GetSerializerInfo(typeof(ChangeSetEntry[]), exposedTypes));
+ serializers.Add(GetSerializerInfo(typeof(QueryResult), exposedTypes));
+
+ return serializers;
+ });
+
+ JsonMediaTypeFormatter formatterJson = new JsonMediaTypeFormatter();
+ formatterJson.Serializer = new JsonSerializer() { PreserveReferencesHandling = PreserveReferencesHandling.Objects, TypeNameHandling = TypeNameHandling.All };
+
+ XmlMediaTypeFormatter formatterXml = new XmlMediaTypeFormatter();
+
+ // apply the serializers to configuration
+ foreach (var serializerInfo in cachedSerializers)
+ {
+ formatterXml.SetSerializer(serializerInfo.ObjectType, serializerInfo.XmlSerializer);
+ }
+
+ formatters.Add(formatterJson);
+ formatters.Add(formatterXml);
+ }
+
+ // Get existing formatters from config, excluding Json/Xml formatters.
+ private static void AddFormattersFromConfig(List<MediaTypeFormatter> formatters, HttpConfiguration config)
+ {
+ foreach (var formatter in config.Formatters)
+ {
+ if (formatter.GetType() == typeof(JsonMediaTypeFormatter) ||
+ formatter.GetType() == typeof(XmlMediaTypeFormatter))
+ {
+ // skip copying the json/xml formatters since we're configuring those
+ // specifically per controller type and can't share instances between
+ // controllers
+ continue;
+ }
+ formatters.Add(formatter);
+ }
+ }
+
+ private static SerializerInfo GetSerializerInfo(Type type, IEnumerable<Type> knownTypes)
+ {
+ SerializerInfo info = new SerializerInfo();
+ info.ObjectType = type;
+
+ info.XmlSerializer = new DataContractSerializer(type, knownTypes);
+ return info;
+ }
+
+ private class SerializerInfo
+ {
+ public Type ObjectType { get; set; }
+ public DataContractSerializer XmlSerializer { get; set; }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data/DataControllerDescription.cs b/src/Microsoft.Web.Http.Data/DataControllerDescription.cs
new file mode 100644
index 00000000..e18e69ec
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data/DataControllerDescription.cs
@@ -0,0 +1,439 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Reflection;
+using System.Web.Http;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Filters;
+using Microsoft.Web.Http.Data.Metadata;
+
+namespace Microsoft.Web.Http.Data
+{
+ public class DataControllerDescription
+ {
+ private static readonly ConcurrentDictionary<Type, DataControllerDescription> _descriptionMap = new ConcurrentDictionary<Type, DataControllerDescription>();
+ private static ConcurrentDictionary<Type, HashSet<Type>> _typeDescriptionProviderMap = new ConcurrentDictionary<Type, HashSet<Type>>();
+
+ private static readonly string[] _deletePrefixes = { "Delete", "Remove" };
+ private static readonly string[] _insertPrefixes = { "Insert", "Add", "Create" };
+ private static readonly string[] _updatePrefixes = { "Update", "Change", "Modify" };
+ private Type _dataControllerType;
+ private ReadOnlyCollection<Type> _entityTypes;
+ private List<UpdateActionDescriptor> _updateActions;
+
+ internal DataControllerDescription(Type dataControllerType, IEnumerable<Type> entityTypes, List<UpdateActionDescriptor> actions)
+ {
+ _dataControllerType = dataControllerType;
+ _entityTypes = entityTypes.ToList().AsReadOnly();
+ _updateActions = actions;
+ }
+
+ /// <summary>
+ /// Gets the Type of the <see cref="DataController"/>
+ /// </summary>
+ public Type ControllerType
+ {
+ get { return _dataControllerType; }
+ }
+
+ /// <summary>
+ /// Gets the entity types exposed by the <see cref="DataController"/>
+ /// </summary>
+ public IEnumerable<Type> EntityTypes
+ {
+ get { return _entityTypes; }
+ }
+
+ public static DataControllerDescription GetDescription(HttpControllerDescriptor controllerDescriptor)
+ {
+ return _descriptionMap.GetOrAdd(controllerDescriptor.ControllerType, type =>
+ {
+ return CreateDescription(controllerDescriptor);
+ });
+ }
+
+ /// <summary>
+ /// Creates and returns the metadata provider for the specified DataController Type.
+ /// </summary>
+ /// <param name="dataControllerType">The DataController Type.</param>
+ /// <returns>The metadata provider.</returns>
+ internal static MetadataProvider CreateMetadataProvider(Type dataControllerType)
+ {
+ // construct a list of all types in the inheritance hierarchy for the controller
+ List<Type> baseTypes = new List<Type>();
+ Type currType = dataControllerType;
+ while (currType != typeof(DataController))
+ {
+ baseTypes.Add(currType);
+ currType = currType.BaseType;
+ }
+
+ // create our base reflection provider
+ List<MetadataProvider> providerList = new List<MetadataProvider>();
+ ReflectionMetadataProvider reflectionProvider = new ReflectionMetadataProvider();
+
+ // Set the IsEntity function which consults the chain of providers.
+ Func<Type, bool> isEntityTypeFunc = (t) => providerList.Any(p => p.LookUpIsEntityType(t));
+ reflectionProvider.SetIsEntityTypeFunc(isEntityTypeFunc);
+
+ // Now from most derived to base, create any declared metadata providers,
+ // chaining the instances as we progress. Note that ordering from derived to
+ // base is important - we want to ensure that any providers the user has placed on
+ // their DataController directly come before any DAL providers.
+ MetadataProvider currProvider = reflectionProvider;
+ providerList.Add(currProvider);
+ for (int i = 0; i < baseTypes.Count; i++)
+ {
+ currType = baseTypes[i];
+
+ // Reflection rather than TD is used here so we only get explicit
+ // Type attributes. TD inherits attributes by default, even if the
+ // attributes aren't inheritable.
+ foreach (MetadataProviderAttribute providerAttribute in
+ currType.GetCustomAttributes(typeof(MetadataProviderAttribute), false))
+ {
+ currProvider = providerAttribute.CreateProvider(dataControllerType, currProvider);
+ currProvider.SetIsEntityTypeFunc(isEntityTypeFunc);
+ providerList.Add(currProvider);
+ }
+ }
+
+ return currProvider;
+ }
+
+ private static DataControllerDescription CreateDescription(HttpControllerDescriptor controllerDescriptor)
+ {
+ Type dataControllerType = controllerDescriptor.ControllerType;
+ MetadataProvider metadataProvider = CreateMetadataProvider(dataControllerType);
+
+ // get all public candidate methods and create the operations
+ HashSet<Type> entityTypes = new HashSet<Type>();
+ List<UpdateActionDescriptor> actions = new List<UpdateActionDescriptor>();
+ IEnumerable<MethodInfo> methodsToInspect =
+ dataControllerType.GetMethods(BindingFlags.Instance | BindingFlags.Public)
+ .Where(p => (p.DeclaringType != typeof(DataController) && (p.DeclaringType != typeof(object))) && !p.IsSpecialName);
+
+ foreach (MethodInfo method in methodsToInspect)
+ {
+ if (method.GetCustomAttributes(typeof(NonActionAttribute), false).Length > 0)
+ {
+ continue;
+ }
+
+ if (method.IsVirtual && method.GetBaseDefinition().DeclaringType == typeof(DataController))
+ {
+ // don't want to infer overrides of DataController virtual methods as
+ // operations
+ continue;
+ }
+
+ // We need to ensure the buddy metadata provider is registered BEFORE we
+ // attempt to do convention, since we rely on IsEntity which relies on
+ // KeyAttributes being present (possibly from "buddy" classes)
+ RegisterAssociatedMetadataProvider(method);
+
+ ChangeOperation operationType = ClassifyUpdateOperation(method, metadataProvider);
+ if (operationType != ChangeOperation.None)
+ {
+ Type entityType = method.GetParameters()[0].ParameterType;
+ UpdateActionDescriptor actionDescriptor = new UpdateActionDescriptor(controllerDescriptor, method, entityType, operationType);
+ ValidateAction(actionDescriptor);
+ actions.Add(actionDescriptor);
+
+ // TODO : currently considering entity types w/o any query methods
+ // exposing them. Should we?
+ if (metadataProvider.IsEntityType(entityType))
+ {
+ AddEntityType(entityType, entityTypes, metadataProvider);
+ }
+ }
+ else
+ {
+ // if the method is a "query" operation returning an entity,
+ // add to entity types
+ if (method.ReturnType != typeof(void))
+ {
+ Type returnType = TypeUtility.UnwrapTaskInnerType(method.ReturnType);
+ Type elementType = TypeUtility.GetElementType(returnType);
+ if (metadataProvider.IsEntityType(elementType))
+ {
+ AddEntityType(elementType, entityTypes, metadataProvider);
+ }
+ }
+ }
+ }
+
+ return new DataControllerDescription(dataControllerType, entityTypes, actions);
+ }
+
+ /// <summary>
+ /// Adds the specified entity type and any associated entity types recursively to the specified set.
+ /// </summary>
+ /// <param name="entityType">The entity Type to add.</param>
+ /// <param name="entityTypes">The types set to accumulate in.</param>
+ /// <param name="metadataProvider">The metadata provider.</param>
+ private static void AddEntityType(Type entityType, HashSet<Type> entityTypes, MetadataProvider metadataProvider)
+ {
+ if (entityTypes.Contains(entityType))
+ {
+ // already added this type
+ return;
+ }
+
+ entityTypes.Add(entityType);
+ RegisterDataControllerTypeDescriptionProvider(entityType, metadataProvider);
+
+ foreach (PropertyDescriptor pd in TypeDescriptor.GetProperties(entityType))
+ {
+ // for any "exposed" association members, recursively add the associated
+ // entity type
+ if (pd.Attributes[typeof(AssociationAttribute)] != null && TypeUtility.IsDataMember(pd))
+ {
+ Type includedEntityType = TypeUtility.GetElementType(pd.PropertyType);
+ if (metadataProvider.IsEntityType(entityType))
+ {
+ AddEntityType(includedEntityType, entityTypes, metadataProvider);
+ }
+ }
+ }
+
+ // Recursively add any derived entity types specified by [KnownType]
+ // attributes
+ IEnumerable<Type> knownTypes = TypeUtility.GetKnownTypes(entityType, true);
+ foreach (Type knownType in knownTypes)
+ {
+ if (entityType.IsAssignableFrom(knownType))
+ {
+ AddEntityType(knownType, entityTypes, metadataProvider);
+ }
+ }
+ }
+
+ private static void ValidateAction(UpdateActionDescriptor updateAction)
+ {
+ // Only authorization filters are supported on CUD actions. This will capture 99% of user errors.
+ // There is the chance that someone might attempt to implement an attribute that implements both
+ // IAuthorizationFilter AND another filter type, but we don't want to have a black-list of filter
+ // types here.
+ if (updateAction.GetFilters().Any(p => !typeof(AuthorizationFilterAttribute).IsAssignableFrom(p.GetType())))
+ {
+ throw Error.NotSupported(Resource.InvalidAction_UnsupportedFilterType, updateAction.ControllerDescriptor.ControllerType.Name, updateAction.ActionName);
+ }
+ }
+
+ private static ChangeOperation ClassifyUpdateOperation(MethodInfo method, MetadataProvider metadataProvider)
+ {
+ ChangeOperation operationType;
+
+ AttributeCollection methodAttributes = new AttributeCollection(method.GetCustomAttributes(false).Cast<Attribute>().ToArray());
+
+ // Check if explicit attributes exist.
+ if (methodAttributes[typeof(InsertAttribute)] != null)
+ {
+ operationType = ChangeOperation.Insert;
+ }
+ else if (methodAttributes[typeof(UpdateAttribute)] != null)
+ {
+ UpdateAttribute updateAttribute = (UpdateAttribute)methodAttributes[typeof(UpdateAttribute)];
+ if (updateAttribute.UsingCustomMethod)
+ {
+ operationType = ChangeOperation.Custom;
+ }
+ else
+ {
+ operationType = ChangeOperation.Update;
+ }
+ }
+ else if (methodAttributes[typeof(DeleteAttribute)] != null)
+ {
+ operationType = ChangeOperation.Delete;
+ }
+ else
+ {
+ return TryClassifyUpdateOperationImplicit(method, metadataProvider);
+ }
+
+ return operationType;
+ }
+
+ private static ChangeOperation TryClassifyUpdateOperationImplicit(MethodInfo method, MetadataProvider metadataProvider)
+ {
+ ChangeOperation operationType = ChangeOperation.None;
+ if (method.ReturnType == typeof(void))
+ {
+ // Check if this looks like an insert, update or delete method.
+ if (_insertPrefixes.Any(p => method.Name.StartsWith(p, StringComparison.OrdinalIgnoreCase)))
+ {
+ operationType = ChangeOperation.Insert;
+ }
+ else if (_updatePrefixes.Any(p => method.Name.StartsWith(p, StringComparison.OrdinalIgnoreCase)))
+ {
+ operationType = ChangeOperation.Update;
+ }
+ else if (_deletePrefixes.Any(p => method.Name.StartsWith(p, StringComparison.OrdinalIgnoreCase)))
+ {
+ operationType = ChangeOperation.Delete;
+ }
+ else if (IsCustomUpdateMethod(method, metadataProvider))
+ {
+ operationType = ChangeOperation.Custom;
+ }
+ }
+
+ return operationType;
+ }
+
+ private static bool IsCustomUpdateMethod(MethodInfo method, MetadataProvider metadataProvider)
+ {
+ ParameterInfo[] parameters = method.GetParameters();
+ if (parameters.Length == 0)
+ {
+ return false;
+ }
+ if (method.ReturnType != typeof(void))
+ {
+ return false;
+ }
+
+ return metadataProvider.IsEntityType(parameters[0].ParameterType);
+ }
+
+ /// <summary>
+ /// Register the associated metadata provider for Types in the signature
+ /// of the specified method as required.
+ /// </summary>
+ /// <param name="methodInfo">The method to register for.</param>
+ private static void RegisterAssociatedMetadataProvider(MethodInfo methodInfo)
+ {
+ Type type = TypeUtility.GetElementType(methodInfo.ReturnType);
+ if (type != typeof(void) && type.GetCustomAttributes(typeof(MetadataTypeAttribute), true).Length != 0)
+ {
+ RegisterAssociatedMetadataTypeTypeDescriptor(type);
+ }
+ foreach (ParameterInfo parameter in methodInfo.GetParameters())
+ {
+ type = parameter.ParameterType;
+ if (type != typeof(void) && type.GetCustomAttributes(typeof(MetadataTypeAttribute), true).Length != 0)
+ {
+ RegisterAssociatedMetadataTypeTypeDescriptor(type);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Verifies that the <see cref="MetadataTypeAttribute"/> reference does not contain a cyclic reference and
+ /// registers the AssociatedMetadataTypeTypeDescriptionProvider in that case.
+ /// </summary>
+ /// <param name="type">The entity type with the MetadataType attribute.</param>
+ private static void RegisterAssociatedMetadataTypeTypeDescriptor(Type type)
+ {
+ Type currentType = type;
+ HashSet<Type> metadataTypeReferences = new HashSet<Type>();
+ metadataTypeReferences.Add(currentType);
+ while (true)
+ {
+ MetadataTypeAttribute attribute = (MetadataTypeAttribute)Attribute.GetCustomAttribute(currentType, typeof(MetadataTypeAttribute));
+ if (attribute == null)
+ {
+ break;
+ }
+ else
+ {
+ currentType = attribute.MetadataClassType;
+ // If we find a cyclic reference, throw an error.
+ if (metadataTypeReferences.Contains(currentType))
+ {
+ throw Error.InvalidOperation(Resource.CyclicMetadataTypeAttributesFound, type.FullName);
+ }
+ else
+ {
+ metadataTypeReferences.Add(currentType);
+ }
+ }
+ }
+
+ // If the MetadataType reference chain doesn't contain a cycle, register the use of the AssociatedMetadataTypeTypeDescriptionProvider.
+ RegisterCustomTypeDescriptor(new AssociatedMetadataTypeTypeDescriptionProvider(type), type);
+ }
+
+ // The JITer enforces CAS. By creating a separate method we can avoid getting SecurityExceptions
+ // when we weren't going to really call TypeDescriptor.AddProvider.
+ internal static void RegisterCustomTypeDescriptor(TypeDescriptionProvider tdp, Type type)
+ {
+ // Check if we already registered provider with the specified type.
+ HashSet<Type> existingProviders = _typeDescriptionProviderMap.GetOrAdd(type, t =>
+ {
+ return new HashSet<Type>();
+ });
+
+ if (!existingProviders.Contains(tdp.GetType()))
+ {
+ TypeDescriptor.AddProviderTransparent(tdp, type);
+ existingProviders.Add(tdp.GetType());
+ }
+ }
+
+ /// <summary>
+ /// Register our DataControllerTypeDescriptionProvider for the specified Type. This provider is responsible for surfacing the
+ /// custom TDs returned by metadata providers.
+ /// </summary>
+ /// <param name="type">The Type that we should register for.</param>
+ /// <param name="metadataProvider">The metadata provider.</param>
+ private static void RegisterDataControllerTypeDescriptionProvider(Type type, MetadataProvider metadataProvider)
+ {
+ DataControllerTypeDescriptionProvider tdp = new DataControllerTypeDescriptionProvider(type, metadataProvider);
+ RegisterCustomTypeDescriptor(tdp, type);
+ }
+
+ public UpdateActionDescriptor GetUpdateAction(string name)
+ {
+ return _updateActions.FirstOrDefault(p => p.ActionName == name);
+ }
+
+ public UpdateActionDescriptor GetUpdateAction(Type entityType, ChangeOperation operationType)
+ {
+ return _updateActions.FirstOrDefault(p => (p.EntityType == entityType) && (p.ChangeOperation == operationType));
+ }
+
+ public UpdateActionDescriptor GetCustomMethod(Type entityType, string methodName)
+ {
+ if (entityType == null)
+ {
+ throw Error.ArgumentNull("entityType");
+ }
+ if (methodName == null)
+ {
+ throw Error.ArgumentNull("methodName");
+ }
+
+ return _updateActions.FirstOrDefault(p => (p.EntityType == entityType) && (p.ChangeOperation == ChangeOperation.Custom) && (p.ActionName == methodName));
+ }
+
+ /// <summary>
+ /// This is the default provider in the metadata provider chain. It is based solely on
+ /// attributes applied directly to types (either via CLR attributes, or via "buddy" metadata class).
+ /// </summary>
+ private class ReflectionMetadataProvider : MetadataProvider
+ {
+ public ReflectionMetadataProvider()
+ : base(parent: null)
+ {
+ }
+
+ /// <summary>
+ /// Returns true if the Type has at least one member marked with KeyAttribute.
+ /// </summary>
+ /// <param name="type">The Type to check.</param>
+ /// <returns>True if the Type is an entity, false otherwise.</returns>
+ public override bool LookUpIsEntityType(Type type)
+ {
+ return TypeDescriptor.GetProperties(type).Cast<PropertyDescriptor>().Any(p => p.Attributes[typeof(KeyAttribute)] != null);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data/DataControllerValidation.cs b/src/Microsoft.Web.Http.Data/DataControllerValidation.cs
new file mode 100644
index 00000000..be3921a9
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data/DataControllerValidation.cs
@@ -0,0 +1,99 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Globalization;
+using System.Web.Http.Controllers;
+using System.Web.Http.Metadata;
+using System.Web.Http.ModelBinding;
+using System.Web.Http.Validation;
+using System.Web.Http.ValueProviders;
+
+namespace Microsoft.Web.Http.Data
+{
+ internal static class DataControllerValidation
+ {
+ internal static bool ValidateObject(object o, List<ValidationResultInfo> validationErrors, HttpActionContext actionContext)
+ {
+ // create a model validation node for the object
+ ModelMetadataProvider metadataProvider = actionContext.GetMetadataProvider();
+ string modelStateKey = String.Empty;
+ ModelValidationNode validationNode = CreateModelValidationNode(o, metadataProvider, actionContext.ModelState, modelStateKey);
+ validationNode.ValidateAllProperties = true;
+
+ // add the node to model state
+ ModelState modelState = new ModelState();
+ modelState.Value = new ValueProviderResult(o, String.Empty, CultureInfo.CurrentCulture);
+ actionContext.ModelState.Add(modelStateKey, modelState);
+
+ // invoke validation
+ validationNode.Validate(actionContext);
+
+ if (!actionContext.ModelState.IsValid)
+ {
+ foreach (var modelStateItem in actionContext.ModelState)
+ {
+ foreach (ModelError modelError in modelStateItem.Value.Errors)
+ {
+ validationErrors.Add(new ValidationResultInfo(modelError.ErrorMessage, new string[] { modelStateItem.Key }));
+ }
+ }
+ }
+
+ return actionContext.ModelState.IsValid;
+ }
+
+ private static ModelValidationNode CreateModelValidationNode(object o, ModelMetadataProvider metadataProvider, ModelStateDictionary modelStateDictionary, string modelStateKey)
+ {
+ ModelMetadata metadata = metadataProvider.GetMetadataForType(() =>
+ {
+ return o;
+ }, o.GetType());
+ ModelValidationNode validationNode = new ModelValidationNode(metadata, modelStateKey);
+
+ // for this root node, recursively add all child nodes
+ HashSet<object> visited = new HashSet<object>();
+ CreateModelValidationNodeRecursive(o, validationNode, metadataProvider, metadata, modelStateDictionary, modelStateKey, visited);
+
+ return validationNode;
+ }
+
+ private static void CreateModelValidationNodeRecursive(object o, ModelValidationNode parentNode, ModelMetadataProvider metadataProvider, ModelMetadata metadata, ModelStateDictionary modelStateDictionary, string modelStateKey, HashSet<object> visited)
+ {
+ if (visited.Contains(o))
+ {
+ return;
+ }
+ visited.Add(o);
+
+ foreach (PropertyDescriptor property in TypeDescriptor.GetProperties(o))
+ {
+ // append the current property name to the model state path
+ string propertyKey = modelStateKey;
+ if (propertyKey.Length > 0)
+ {
+ propertyKey += ".";
+ }
+ propertyKey += property.Name;
+
+ // create the node for this property and add to the parent node
+ object propertyValue = property.GetValue(o);
+ metadata = metadataProvider.GetMetadataForProperty(() =>
+ {
+ return propertyValue;
+ }, o.GetType(), property.Name);
+ ModelValidationNode childNode = new ModelValidationNode(metadata, propertyKey);
+ parentNode.ChildNodes.Add(childNode);
+
+ // add the property node to model state
+ ModelState modelState = new ModelState();
+ modelState.Value = new ValueProviderResult(propertyValue, null, CultureInfo.CurrentCulture);
+ modelStateDictionary.Add(propertyKey, modelState);
+
+ if (propertyValue != null)
+ {
+ CreateModelValidationNodeRecursive(propertyValue, childNode, metadataProvider, metadata, modelStateDictionary, propertyKey, visited);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data/DeleteAttribute.cs b/src/Microsoft.Web.Http.Data/DeleteAttribute.cs
new file mode 100644
index 00000000..5fdea8bb
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data/DeleteAttribute.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace Microsoft.Web.Http.Data
+{
+ /// <summary>
+ /// Attribute applied to a <see cref="DataController"/> method to indicate that it is a delete method.
+ /// </summary>
+ [AttributeUsage(
+ AttributeTargets.Field | AttributeTargets.Method | AttributeTargets.Property,
+ AllowMultiple = false, Inherited = true)]
+ public sealed class DeleteAttribute : Attribute
+ {
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data/GlobalSuppressions.cs b/src/Microsoft.Web.Http.Data/GlobalSuppressions.cs
new file mode 100644
index 00000000..a97931d5
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data/GlobalSuppressions.cs
@@ -0,0 +1,24 @@
+// This file is used by Code Analysis to maintain SuppressMessage
+// attributes that are applied to this project.
+// Project-level suppressions either have no target or are given
+// a specific target and scoped to a namespace, type, member, etc.
+//
+// To add a suppression to this file, right-click the message in the
+// Error List, point to "Suppress Message(s)", and click
+// "In Project Suppression File".
+// You do not need to add suppressions to this file manually.
+
+using System.Diagnostics.CodeAnalysis;
+
+[assembly: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope = "member", Target = "Microsoft.Web.Http.Data.ChangeSetEntry.#OriginalAssociations")]
+[assembly: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope = "member", Target = "Microsoft.Web.Http.Data.ChangeSetEntry.#Associations")]
+[assembly: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope = "member", Target = "Microsoft.Web.Http.Data.ChangeSetEntry.#EntityActions")]
+[assembly: SuppressMessage("Microsoft.Design", "CA2210:AssembliesShouldHaveValidStrongNames", Justification = "Assembly is delay signed")]
+[assembly: SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Scope = "member", Target = "Microsoft.Web.Http.Data.ChangeSet.#SetEntityAssociations()")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "System.ComponentModel.DataAnnotations")]
+[assembly: SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", Scope = "type", Target = "Microsoft.Web.Http.Data.Metadata.MetadataProviderAttribute", Justification = "We intend for people to derive from this attribute.")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "Microsoft.Web.Http.Data.Metadata", Justification = "These types are in their own namespace to match folder structure.")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "LookUp", Scope = "member", Target = "Microsoft.Web.Http.Data.Metadata.MetadataProvider.#LookUpIsEntityType(System.Type)", Justification = "This is intended to read as two words")]
+[assembly: SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "Microsoft.Web.Http.Data.SubmitActionDescriptor.#Execute(System.Web.Http.HttpControllerContext,System.Collections.Generic.IDictionary`2<System.String,System.Object>)", Justification = "This object is being returned - it can't be disposed.")]
+[assembly: SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "Microsoft.Web.Http.Data.DataControllerConfiguration.#CloneConfiguration(System.Web.Http.HttpConfiguration)", Justification = "This object cannot be disposed - it is being set on the execution context")]
+[assembly: SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "Microsoft.Web.Http.Data.QueryFilterAttribute.#GetQueryResult`1(System.Linq.IQueryable`1<!!0>,System.Web.Http.Controllers.HttpActionContext)", Justification = "This object cannot be disposed - it is being returned as the result.")]
diff --git a/src/Microsoft.Web.Http.Data/InsertAttribute.cs b/src/Microsoft.Web.Http.Data/InsertAttribute.cs
new file mode 100644
index 00000000..965b1063
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data/InsertAttribute.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace Microsoft.Web.Http.Data
+{
+ /// <summary>
+ /// Attribute applied to a <see cref="DataController"/> method to indicate that it is an insert method.
+ /// </summary>
+ [AttributeUsage(
+ AttributeTargets.Field | AttributeTargets.Method | AttributeTargets.Property,
+ AllowMultiple = false, Inherited = true)]
+ public sealed class InsertAttribute : Attribute
+ {
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data/Metadata/DataControllerTypeDescriptionProvider.cs b/src/Microsoft.Web.Http.Data/Metadata/DataControllerTypeDescriptionProvider.cs
new file mode 100644
index 00000000..a36dd9e7
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data/Metadata/DataControllerTypeDescriptionProvider.cs
@@ -0,0 +1,100 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.Web.Http.Common;
+
+namespace Microsoft.Web.Http.Data.Metadata
+{
+ /// <summary>
+ /// Custom TypeDescriptionProvider conditionally registered for Types exposed by a <see cref="DataController"/>.
+ /// </summary>
+ internal class DataControllerTypeDescriptionProvider : TypeDescriptionProvider
+ {
+ private readonly MetadataProvider _metadataProvider;
+ private readonly Type _type;
+ private ICustomTypeDescriptor _customTypeDescriptor;
+
+ public DataControllerTypeDescriptionProvider(Type type, MetadataProvider metadataProvider)
+ : base(TypeDescriptor.GetProvider(type))
+ {
+ if (metadataProvider == null)
+ {
+ throw Error.ArgumentNull("metadataProvider");
+ }
+
+ _type = type;
+ _metadataProvider = metadataProvider;
+ }
+
+ public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object instance)
+ {
+ if (objectType == null && instance != null)
+ {
+ objectType = instance.GetType();
+ }
+
+ if (_type != objectType)
+ {
+ // In inheritance scenarios, we might be called to provide a descriptor
+ // for a derived Type. In that case, we just return base.
+ return base.GetTypeDescriptor(objectType, instance);
+ }
+
+ if (_customTypeDescriptor == null)
+ {
+ // CLR, buddy class type descriptors
+ _customTypeDescriptor = base.GetTypeDescriptor(objectType, instance);
+
+ // EF, any other custom type descriptors provided through MetadataProviders.
+ _customTypeDescriptor = _metadataProvider.GetTypeDescriptor(objectType, _customTypeDescriptor);
+
+ // initialize FK members AFTER our type descriptors have chained
+ HashSet<string> foreignKeyMembers = GetForeignKeyMembers();
+
+ // if any FK member of any association is also part of the primary key, then the key cannot be marked
+ // Editable(false)
+ bool keyIsEditable = false;
+ foreach (PropertyDescriptor pd in _customTypeDescriptor.GetProperties())
+ {
+ if (pd.Attributes[typeof(KeyAttribute)] != null &&
+ foreignKeyMembers.Contains(pd.Name))
+ {
+ keyIsEditable = true;
+ break;
+ }
+ }
+
+ if (DataControllerTypeDescriptor.ShouldRegister(_customTypeDescriptor, keyIsEditable, foreignKeyMembers))
+ {
+ // Extend the chain with one more descriptor.
+ _customTypeDescriptor = new DataControllerTypeDescriptor(_customTypeDescriptor, keyIsEditable, foreignKeyMembers);
+ }
+ }
+
+ return _customTypeDescriptor;
+ }
+
+ /// <summary>
+ /// Returns the set of all foreign key members for the entity.
+ /// </summary>
+ /// <returns>The set of foreign keys.</returns>
+ private HashSet<string> GetForeignKeyMembers()
+ {
+ HashSet<string> foreignKeyMembers = new HashSet<string>();
+ foreach (PropertyDescriptor pd in _customTypeDescriptor.GetProperties())
+ {
+ AssociationAttribute assoc = (AssociationAttribute)pd.Attributes[typeof(AssociationAttribute)];
+ if (assoc != null && assoc.IsForeignKey)
+ {
+ foreach (string foreignKeyMember in assoc.ThisKeyMembers)
+ {
+ foreignKeyMembers.Add(foreignKeyMember);
+ }
+ }
+ }
+
+ return foreignKeyMembers;
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data/Metadata/DataControllerTypeDescriptor.cs b/src/Microsoft.Web.Http.Data/Metadata/DataControllerTypeDescriptor.cs
new file mode 100644
index 00000000..9ec070ba
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data/Metadata/DataControllerTypeDescriptor.cs
@@ -0,0 +1,245 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+
+namespace Microsoft.Web.Http.Data.Metadata
+{
+ /// <summary>
+ /// Custom TypeDescriptor for Types exposed by a <see cref="DataController"/>.
+ /// </summary>
+ internal class DataControllerTypeDescriptor : CustomTypeDescriptor
+ {
+ private readonly HashSet<string> _foreignKeyMembers;
+ private readonly bool _keyIsEditable;
+ private PropertyDescriptorCollection _properties;
+
+ public DataControllerTypeDescriptor(ICustomTypeDescriptor parent, bool keyIsEditable, HashSet<string> foreignKeyMembers)
+ : base(parent)
+ {
+ _keyIsEditable = keyIsEditable;
+ _foreignKeyMembers = foreignKeyMembers;
+ }
+
+ public override PropertyDescriptorCollection GetProperties()
+ {
+ if (_properties == null)
+ {
+ // Get properties from our parent
+ PropertyDescriptorCollection originalCollection = base.GetProperties();
+
+ // Set _properties to avoid a stack overflow when CreateProjectionProperties
+ // ends up recursively calling TypeDescriptor.GetProperties on a type.
+ _properties = originalCollection;
+
+ bool customDescriptorsCreated = false;
+ List<PropertyDescriptor> tempPropertyDescriptors = new List<PropertyDescriptor>();
+
+ // for every property exposed by our parent, see if we have additional metadata to add,
+ // and if we do we need to add a wrapper PropertyDescriptor to add the new attributes
+ foreach (PropertyDescriptor propDescriptor in _properties)
+ {
+ Attribute[] newMetadata = GetAdditionalAttributes(propDescriptor);
+ if (newMetadata.Length > 0)
+ {
+ tempPropertyDescriptors.Add(new DataControllerPropertyDescriptor(propDescriptor, newMetadata));
+ customDescriptorsCreated = true;
+ }
+ else
+ {
+ tempPropertyDescriptors.Add(propDescriptor);
+ }
+ }
+
+ if (customDescriptorsCreated)
+ {
+ _properties = new PropertyDescriptorCollection(tempPropertyDescriptors.ToArray(), true);
+ }
+ }
+
+ return _properties;
+ }
+
+ /// <summary>
+ /// Return an array of new attributes for the specified PropertyDescriptor. If no
+ /// attributes need to be added, return an empty array.
+ /// </summary>
+ /// <param name="pd">The property to add attributes for.</param>
+ /// <returns>The collection of new attributes.</returns>
+ private Attribute[] GetAdditionalAttributes(PropertyDescriptor pd)
+ {
+ List<Attribute> additionalAttributes = new List<Attribute>();
+
+ if (ShouldAddRoundTripAttribute(pd, _foreignKeyMembers.Contains(pd.Name)))
+ {
+ additionalAttributes.Add(new RoundtripOriginalAttribute());
+ }
+
+ bool allowInitialValue;
+ if (ShouldAddEditableFalseAttribute(pd, _keyIsEditable, out allowInitialValue))
+ {
+ additionalAttributes.Add(new EditableAttribute(false) { AllowInitialValue = allowInitialValue });
+ }
+
+ return additionalAttributes.ToArray();
+ }
+
+ /// <summary>
+ /// Determines whether a type uses any features requiring the
+ /// <see cref="DataControllerTypeDescriptor"/> to be registered. We do this
+ /// check as an optimization so we're not adding additional TDPs to the
+ /// chain when they're not necessary.
+ /// </summary>
+ /// <param name="descriptor">The descriptor for the type to check.</param>
+ /// <param name="keyIsEditable">Indicates whether the key for this Type is editable.</param>
+ /// <param name="foreignKeyMembers">The set of foreign key members for the Type.</param>
+ /// <returns>Returns <c>true</c> if the type uses any features requiring the
+ /// <see cref="DataControllerTypeDescriptionProvider"/> to be registered.</returns>
+ internal static bool ShouldRegister(ICustomTypeDescriptor descriptor, bool keyIsEditable, HashSet<string> foreignKeyMembers)
+ {
+ foreach (PropertyDescriptor pd in descriptor.GetProperties())
+ {
+ // If there are any attributes that should be inferred for this member, then
+ // we will register the descriptor
+ if (ShouldInferAttributes(pd, keyIsEditable, foreignKeyMembers))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Returns true if the specified member requires a RoundTripOriginalAttribute
+ /// and one isn't already present.
+ /// </summary>
+ /// <param name="pd">The member to check.</param>
+ /// <param name="isFkMember">True if the member is a foreign key, false otherwise.</param>
+ /// <returns>True if RoundTripOriginalAttribute should be added, false otherwise.</returns>
+ private static bool ShouldAddRoundTripAttribute(PropertyDescriptor pd, bool isFkMember)
+ {
+ if (pd.Attributes[typeof(RoundtripOriginalAttribute)] != null || pd.Attributes[typeof(AssociationAttribute)] != null)
+ {
+ // already has the attribute or is an association
+ return false;
+ }
+
+ if (isFkMember || pd.Attributes[typeof(ConcurrencyCheckAttribute)] != null ||
+ pd.Attributes[typeof(TimestampAttribute)] != null || pd.Attributes[typeof(KeyAttribute)] != null)
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Returns <c>true</c> if the specified member requires an <see cref="EditableAttribute"/>
+ /// to make the member read-only and one isn't already present.
+ /// </summary>
+ /// <param name="pd">The member to check.</param>
+ /// <param name="keyIsEditable">Indicates whether the key for this Type is editable.</param>
+ /// <param name="allowInitialValue">
+ /// The default that should be used for <see cref="EditableAttribute.AllowInitialValue"/> if the attribute
+ /// should be added to the member.
+ /// </param>
+ /// <returns><c>true</c> if <see cref="EditableAttribute"/> should be added, <c>false</c> otherwise.</returns>
+ private static bool ShouldAddEditableFalseAttribute(PropertyDescriptor pd, bool keyIsEditable, out bool allowInitialValue)
+ {
+ allowInitialValue = false;
+
+ if (pd.Attributes[typeof(EditableAttribute)] != null)
+ {
+ // already has the attribute
+ return false;
+ }
+
+ bool hasKeyAttribute = (pd.Attributes[typeof(KeyAttribute)] != null);
+ if (hasKeyAttribute && keyIsEditable)
+ {
+ return false;
+ }
+
+ if (hasKeyAttribute || pd.Attributes[typeof(TimestampAttribute)] != null)
+ {
+ // If we're inferring EditableAttribute because of a KeyAttribute
+ // we want to allow initial value for the member.
+ allowInitialValue = hasKeyAttribute;
+ return true;
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Determines if there are any attributes that can be inferred for the specified member.
+ /// </summary>
+ /// <param name="pd">The member to check.</param>
+ /// <param name="keyIsEditable">Indicates whether the key for this Type is editable.</param>
+ /// <param name="foreignKeyMembers">Collection of foreign key members for the Type.</param>
+ /// <returns><c>true</c> if there are attributes to be inferred, <c>false</c> otherwise.</returns>
+ private static bool ShouldInferAttributes(PropertyDescriptor pd, bool keyIsEditable, IEnumerable<string> foreignKeyMembers)
+ {
+ bool allowInitialValue;
+
+ return ShouldAddEditableFalseAttribute(pd, keyIsEditable, out allowInitialValue) ||
+ ShouldAddRoundTripAttribute(pd, foreignKeyMembers.Contains(pd.Name));
+ }
+ }
+
+ /// <summary>
+ /// PropertyDescriptor wrapper.
+ /// </summary>
+ internal class DataControllerPropertyDescriptor : PropertyDescriptor
+ {
+ private PropertyDescriptor _base;
+
+ public DataControllerPropertyDescriptor(PropertyDescriptor pd, Attribute[] attribs)
+ : base(pd, attribs)
+ {
+ _base = pd;
+ }
+
+ public override Type ComponentType
+ {
+ get { return _base.ComponentType; }
+ }
+
+ public override bool IsReadOnly
+ {
+ get { return _base.IsReadOnly; }
+ }
+
+ public override Type PropertyType
+ {
+ get { return _base.PropertyType; }
+ }
+
+ public override object GetValue(object component)
+ {
+ return _base.GetValue(component);
+ }
+
+ public override void SetValue(object component, object value)
+ {
+ _base.SetValue(component, value);
+ }
+
+ public override bool ShouldSerializeValue(object component)
+ {
+ return _base.ShouldSerializeValue(component);
+ }
+
+ public override bool CanResetValue(object component)
+ {
+ return _base.CanResetValue(component);
+ }
+
+ public override void ResetValue(object component)
+ {
+ _base.ResetValue(component);
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data/Metadata/MetadataProvider.cs b/src/Microsoft.Web.Http.Data/Metadata/MetadataProvider.cs
new file mode 100644
index 00000000..b9d47104
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data/Metadata/MetadataProvider.cs
@@ -0,0 +1,111 @@
+using System;
+using System.ComponentModel;
+using System.Web.Http.Common;
+
+namespace Microsoft.Web.Http.Data.Metadata
+{
+ /// <summary>
+ /// A <see cref="MetadataProvider"/> is used to provide the metadata description for
+ /// types exposed by a <see cref="DataController"/>.
+ /// </summary>
+ public abstract class MetadataProvider
+ {
+ private MetadataProvider _parentProvider;
+ private Func<Type, bool> _isEntityTypeFunc;
+
+ /// <summary>
+ /// Protected Constructor
+ /// </summary>
+ /// <param name="parent">The existing parent provider. May be null.</param>
+ protected MetadataProvider(MetadataProvider parent)
+ {
+ _parentProvider = parent;
+ }
+
+ /// <summary>
+ /// Gets the parent provider.
+ /// </summary>
+ internal MetadataProvider ParentProvider
+ {
+ get { return _parentProvider; }
+ }
+
+ /// <summary>
+ /// Gets the <see cref="TypeDescriptor"/> for the specified Type, using the specified parent descriptor
+ /// as the base. Overrides should call base to ensure the <see cref="TypeDescriptor"/>s are chained properly.
+ /// </summary>
+ /// <param name="type">The Type to return a descriptor for.</param>
+ /// <param name="parent">The parent descriptor.</param>
+ /// <returns>The <see cref="TypeDescriptor"/> for the specified Type.</returns>
+ public virtual ICustomTypeDescriptor GetTypeDescriptor(Type type, ICustomTypeDescriptor parent)
+ {
+ if (type == null)
+ {
+ throw Error.ArgumentNull("type");
+ }
+ if (parent == null)
+ {
+ throw Error.ArgumentNull("parent");
+ }
+
+ if (_parentProvider != null)
+ {
+ return _parentProvider.GetTypeDescriptor(type, parent);
+ }
+
+ return parent;
+ }
+
+ /// <summary>
+ /// Determines if the specified <see cref="Type"/> should be considered an entity <see cref="Type"/>.
+ /// The base implementation returns <c>false</c>.
+ /// </summary>
+ /// <remarks>Effectively, the return from this method is this provider's vote as to whether the specified
+ /// Type is an entity. The votes from this provider and all other providers in the chain are used
+ /// by <see cref="IsEntityType"/> to make it's determination.</remarks>
+ /// <param name="type">The <see cref="Type"/> to check.</param>
+ /// <returns>Returns <c>true</c> if the <see cref="Type"/> should be considered an entity,
+ /// <c>false</c> otherwise.</returns>
+ public virtual bool LookUpIsEntityType(Type type)
+ {
+ if (type == null)
+ {
+ throw Error.ArgumentNull("type");
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Determines if the specified <see cref="Type"/> is an entity <see cref="Type"/> by consulting
+ /// the <see cref="LookUpIsEntityType"/> method of all <see cref="MetadataProvider"/>s
+ /// in the provider chain for the <see cref="DataController"/>.
+ /// </summary>
+ /// <param name="type">The <see cref="Type"/> to check.</param>
+ /// <returns>Returns <c>true</c> if the <see cref="Type"/> is an entity, <c>false</c> otherwise.</returns>
+ protected internal bool IsEntityType(Type type)
+ {
+ if (type == null)
+ {
+ throw Error.ArgumentNull("type");
+ }
+
+ if (_isEntityTypeFunc != null)
+ {
+ return _isEntityTypeFunc(type);
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Sets the internal entity lookup function for this provider. The function consults
+ /// the entire provider chain to make its determination.
+ /// </summary>
+ /// <param name="isEntityTypeFunc">The entity function.</param>
+ internal void SetIsEntityTypeFunc(Func<Type, bool> isEntityTypeFunc)
+ {
+ _isEntityTypeFunc = isEntityTypeFunc;
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data/Metadata/MetadataProviderAttribute.cs b/src/Microsoft.Web.Http.Data/Metadata/MetadataProviderAttribute.cs
new file mode 100644
index 00000000..aed944a7
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data/Metadata/MetadataProviderAttribute.cs
@@ -0,0 +1,78 @@
+using System;
+using System.Web.Http.Common;
+
+namespace Microsoft.Web.Http.Data.Metadata
+{
+ /// <summary>
+ /// Attribute applied to a <see cref="DataController"/> type to specify the <see cref="MetadataProvider"/>
+ /// for the type.
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
+ public class MetadataProviderAttribute : Attribute
+ {
+ private Type _providerType;
+
+ /// <summary>
+ /// Initializes a new instance of the MetadataProviderAttribute class
+ /// </summary>
+ /// <param name="providerType">The <see cref="MetadataProvider"/> type</param>
+ public MetadataProviderAttribute(Type providerType)
+ {
+ if (providerType == null)
+ {
+ throw Error.ArgumentNull("providerType");
+ }
+
+ _providerType = providerType;
+ }
+
+ /// <summary>
+ /// Gets the <see cref="MetadataProvider"/> type
+ /// </summary>
+ public Type ProviderType
+ {
+ get { return _providerType; }
+ }
+
+ /// <summary>
+ /// Gets a unique identifier for this attribute.
+ /// </summary>
+ public override object TypeId
+ {
+ get { return this; }
+ }
+
+ /// <summary>
+ /// This method creates an instance of the <see cref="MetadataProvider"/>. Subclasses can override this
+ /// method to provide their own construction logic.
+ /// </summary>
+ /// <param name="controllerType">The <see cref="DataController"/> type to create a metadata provider for.</param>
+ /// <param name="parent">The parent provider. May be null.</param>
+ /// <returns>The metadata provider</returns>
+ public virtual MetadataProvider CreateProvider(Type controllerType, MetadataProvider parent)
+ {
+ if (controllerType == null)
+ {
+ throw Error.ArgumentNull("controllerType");
+ }
+
+ if (!typeof(DataController).IsAssignableFrom(controllerType))
+ {
+ throw Error.Argument("controllerType", Resource.InvalidType, controllerType.FullName, typeof(DataController).FullName);
+ }
+
+ if (!typeof(MetadataProvider).IsAssignableFrom(_providerType))
+ {
+ throw Error.InvalidOperation(Resource.InvalidType, _providerType.FullName, typeof(MetadataProvider).FullName);
+ }
+
+ // Verify the type has a .ctor(MetadataProvider).
+ if (_providerType.GetConstructor(new Type[] { typeof(MetadataProvider) }) == null)
+ {
+ throw Error.InvalidOperation(Resource.MetadataProviderAttribute_MissingConstructor, _providerType.FullName);
+ }
+
+ return (MetadataProvider)Activator.CreateInstance(_providerType, parent);
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data/Microsoft.Web.Http.Data.csproj b/src/Microsoft.Web.Http.Data/Microsoft.Web.Http.Data.csproj
new file mode 100644
index 00000000..173e0632
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data/Microsoft.Web.Http.Data.csproj
@@ -0,0 +1,138 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <CodeAnalysis Condition=" '$(CodeAnalysis)' == '' ">false</CodeAnalysis>
+ <ProductVersion>8.0.30703</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{ACE91549-D86E-4EB6-8C2A-5FF51386BB68}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>Microsoft.Web.Http.Data</RootNamespace>
+ <AssemblyName>Microsoft.Web.Http.Data</AssemblyName>
+ <FileAlignment>512</FileAlignment>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>..\..\bin\Debug\</OutputPath>
+ <DefineConstants>TRACE;DEBUG;ASPNETMVC</DefineConstants>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ <DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
+ <NoWarn>1591</NoWarn>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>..\..\bin\Release\</OutputPath>
+ <DefineConstants>TRACE;ASPNETMVC</DefineConstants>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ <RunCodeAnalysis>$(CodeAnalysis)</RunCodeAnalysis>
+ <DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
+ <NoWarn>1591</NoWarn>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'CodeCoverage|AnyCPU'">
+ <DebugSymbols>true</DebugSymbols>
+ <OutputPath>..\..\bin\CodeCoverage\</OutputPath>
+ <DefineConstants>TRACE;DEBUG;CODE_COVERAGE;ASPNETMVC</DefineConstants>
+ <DebugType>full</DebugType>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="Newtonsoft.Json, Version=4.0.8.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>..\..\packages\Newtonsoft.Json.4.0.8\lib\net40\Newtonsoft.Json.dll</HintPath>
+ </Reference>
+ <Reference Include="System" />
+ <Reference Include="System.ComponentModel.DataAnnotations" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.Net.Http">
+ <HintPath>..\..\packages\Microsoft.Net.Http.2.0.20302.1\lib\net40\System.Net.Http.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Net.Http.WebRequest">
+ <HintPath>..\..\packages\Microsoft.Net.Http.2.0.20302.1\lib\net40\System.Net.Http.WebRequest.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Runtime.Serialization" />
+ <Reference Include="System.XML" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="..\AptcaCommonAssemblyInfo.cs">
+ <Link>Properties\AptcaCommonAssemblyInfo.cs</Link>
+ </Compile>
+ <Compile Include="..\CommonAssemblyInfo.cs">
+ <Link>Properties\CommonAssemblyInfo.cs</Link>
+ </Compile>
+ <Compile Include="..\System.Web.Http.Common\TaskHelpers.cs">
+ <Link>TaskUtility\TaskHelpers.cs</Link>
+ </Compile>
+ <Compile Include="..\System.Web.Http.Common\TaskHelpersExtensions.cs">
+ <Link>TaskUtility\TaskHelpersExtensions.cs</Link>
+ </Compile>
+ <Compile Include="ChangeOperation.cs" />
+ <Compile Include="ChangeSet.cs" />
+ <Compile Include="ChangeSetEntry.cs" />
+ <Compile Include="DataControllerActionInvoker.cs" />
+ <Compile Include="CustomizingActionDescriptor.cs" />
+ <Compile Include="DataController.cs" />
+ <Compile Include="DataControllerActionSelector.cs" />
+ <Compile Include="DataControllerActionValueBinder.cs" />
+ <Compile Include="DataControllerDescription.cs" />
+ <Compile Include="DataControllerValidation.cs" />
+ <Compile Include="Metadata\DataControllerTypeDescriptor.cs" />
+ <Compile Include="Metadata\DataControllerTypeDescriptionProvider.cs" />
+ <Compile Include="DeleteAttribute.cs" />
+ <Compile Include="GlobalSuppressions.cs" />
+ <Compile Include="InsertAttribute.cs" />
+ <Compile Include="Metadata\MetadataProvider.cs" />
+ <Compile Include="Metadata\MetadataProviderAttribute.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="QueryFilterAttribute.cs" />
+ <Compile Include="QueryResult.cs" />
+ <Compile Include="Resource.Designer.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>Resource.resx</DependentUpon>
+ </Compile>
+ <Compile Include="RoundtripOriginalAttribute.cs" />
+ <Compile Include="SubmitActionDescriptor.cs" />
+ <Compile Include="SubmitProxyActionDescriptor.cs" />
+ <Compile Include="TypeDescriptorExtensions.cs" />
+ <Compile Include="TypeUtility.cs" />
+ <Compile Include="UpdateActionDescriptor.cs" />
+ <Compile Include="UpdateAttribute.cs" />
+ <Compile Include="ValidationResultInfo.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\System.Net.Http.Formatting\System.Net.Http.Formatting.csproj">
+ <Project>{668E9021-CE84-49D9-98FB-DF125A9FCDB0}</Project>
+ <Name>System.Net.Http.Formatting</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\System.Web.Http.Common\System.Web.Http.Common.csproj">
+ <Project>{03A5E5F2-2E23-48F2-ABCC-6C41BAC9AC02}</Project>
+ <Name>System.Web.Http.Common</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\System.Web.Http\System.Web.Http.csproj">
+ <Project>{DDC1CE0C-486E-4E35-BB3B-EAB61F8F9440}</Project>
+ <Name>System.Web.Http</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <CodeAnalysisDictionary Include="..\CodeAnalysisDictionary.xml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="Resource.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>Resource.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="packages.config" />
+ </ItemGroup>
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+</Project> \ No newline at end of file
diff --git a/src/Microsoft.Web.Http.Data/Properties/AssemblyInfo.cs b/src/Microsoft.Web.Http.Data/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..424fd8e0
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data/Properties/AssemblyInfo.cs
@@ -0,0 +1,6 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+
+[assembly: AssemblyTitle("Microsoft.Web.Http.Data")]
+[assembly: AssemblyDescription("Microsoft.Web.Http.Data")]
+[assembly: InternalsVisibleTo("Microsoft.Web.Http.Data.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
diff --git a/src/Microsoft.Web.Http.Data/QueryFilterAttribute.cs b/src/Microsoft.Web.Http.Data/QueryFilterAttribute.cs
new file mode 100644
index 00000000..95c615f5
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data/QueryFilterAttribute.cs
@@ -0,0 +1,131 @@
+using System;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Net.Http;
+using System.Reflection;
+using System.Web.Http;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Filters;
+
+namespace Microsoft.Web.Http.Data
+{
+ [AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
+ internal sealed class QueryFilterAttribute : ActionFilterAttribute
+ {
+ internal static readonly string TotalCountKey = "MS_InlineCountKey";
+ private static readonly MethodInfo _getTotalCountMethod = typeof(QueryFilterAttribute).GetMethod("GetTotalCount", BindingFlags.NonPublic | BindingFlags.Static);
+
+ public override void OnActionExecuted(HttpActionExecutedContext context)
+ {
+ if (context == null)
+ {
+ throw Error.ArgumentNull("context");
+ }
+
+ bool inlineCount = false;
+ HttpRequestMessage request = context.Request;
+ if (request != null && request.RequestUri != null &&
+ !String.IsNullOrWhiteSpace(request.RequestUri.Query))
+ {
+ // search the URI for an inline count request
+ var parsedQuery = request.RequestUri.ParseQueryString();
+ var inlineCountPart = parsedQuery["$inlinecount"];
+ if (inlineCountPart == "allpages")
+ {
+ inlineCount = true;
+ }
+ }
+
+ HttpResponseMessage response = context.Result;
+ if (!inlineCount || response == null)
+ {
+ return;
+ }
+
+ IQueryable results;
+ if (response.TryGetObjectValue(out results))
+ {
+ // Compute the total count and add the result as a request property. Later after all
+ // filters have run, DataController will transform the final result into a QueryResult
+ // which includes this value.
+ // TODO : use a compiled/cached deletate?
+ int totalCount = (int)_getTotalCountMethod.MakeGenericMethod(results.ElementType).Invoke(null, new object[] { results, context.ActionContext });
+ request.Properties.Add(QueryFilterAttribute.TotalCountKey, totalCount);
+ }
+ }
+
+ /// <summary>
+ /// Determine the total count for the specified query.
+ /// </summary>
+ private static int GetTotalCount<T>(IQueryable<T> results, HttpActionContext context)
+ {
+ // A total count of -1 indicates that the count is the result count. This
+ // is the default, unless we discover that skip/top operations will be
+ // performed, in which case we'll form and execute a count query.
+ int totalCount = -1;
+
+ IQueryable totalCountQuery = null;
+ if (TryRemovePaging(results, out totalCountQuery))
+ {
+ totalCount = ((IQueryable<T>)totalCountQuery).Count();
+ }
+ else if (context.ActionDescriptor.GetFilterPipeline().Any(p => p.Instance is ResultLimitAttribute))
+ {
+ // The client query didn't specify any skip/top paging operations.
+ // However, this action has a ResultLimitFilter applied.
+ // Therefore, we need to take the count now before that limit is applied.
+ totalCount = results.Count();
+ }
+
+ return totalCount;
+ }
+
+ /// <summary>
+ /// Inspects the specified query and if the query has any paging operators
+ /// at the end of it (either a single Take or a Skip/Take) the underlying
+ /// query w/o the Skip/Take is returned.
+ /// </summary>
+ /// <param name="query">The query to inspect.</param>
+ /// <param name="countQuery">The resulting count query. Null if there is no paging.</param>
+ /// <returns>True if a count query is returned, false otherwise.</returns>
+ internal static bool TryRemovePaging(IQueryable query, out IQueryable countQuery)
+ {
+ MethodCallExpression mce = query.Expression as MethodCallExpression;
+ Expression countExpr = null;
+
+ if (IsSequenceOperator("take", mce))
+ {
+ // strip off the Take operator
+ countExpr = mce.Arguments[0];
+
+ mce = countExpr as MethodCallExpression;
+ if (IsSequenceOperator("skip", mce))
+ {
+ // If there's a skip then we need to exclude that too. No skip means we're
+ // on the first page.
+ countExpr = mce.Arguments[0];
+ }
+ }
+
+ countQuery = null;
+ if (countExpr != null)
+ {
+ countQuery = query.Provider.CreateQuery(countExpr);
+ return true;
+ }
+
+ return false;
+ }
+
+ private static bool IsSequenceOperator(string operatorName, MethodCallExpression mce)
+ {
+ if (mce != null && mce.Method.DeclaringType == typeof(Queryable) &&
+ mce.Method.Name.Equals(operatorName, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ return false;
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data/QueryResult.cs b/src/Microsoft.Web.Http.Data/QueryResult.cs
new file mode 100644
index 00000000..34db82c8
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data/QueryResult.cs
@@ -0,0 +1,32 @@
+using System.Collections;
+using System.Runtime.Serialization;
+
+namespace Microsoft.Web.Http.Data
+{
+ /// <summary>
+ /// Represents the results of a query request along with its total count if requested.
+ /// </summary>
+ [DataContract]
+ public sealed class QueryResult
+ {
+ public QueryResult(IEnumerable results, int totalCount)
+ {
+ Results = results;
+ TotalCount = totalCount;
+ }
+
+ /// <summary>
+ /// The results of the query.
+ /// </summary>
+ [DataMember]
+ public IEnumerable Results { get; set; }
+
+ /// <summary>
+ /// The total count of the query, without any paging options applied.
+ /// A TotalCount equal to -1 indicates that the count is equal to the
+ /// result count.
+ /// </summary>
+ [DataMember]
+ public int TotalCount { get; set; }
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data/Resource.Designer.cs b/src/Microsoft.Web.Http.Data/Resource.Designer.cs
new file mode 100644
index 00000000..b66ab84b
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data/Resource.Designer.cs
@@ -0,0 +1,225 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.239
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Microsoft.Web.Http.Data {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resource {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resource() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.Web.Http.Data.Resource", typeof(Resource).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The specified entity does not exist in the ChangeSet..
+ /// </summary>
+ internal static string ChangeSet_ChangeSetEntryNotFound {
+ get {
+ return ResourceManager.GetString("ChangeSet_ChangeSetEntryNotFound", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to GetOriginal cannot be called for a new entity being inserted..
+ /// </summary>
+ internal static string ChangeSet_OriginalNotValidForInsert {
+ get {
+ return ResourceManager.GetString("ChangeSet_OriginalNotValidForInsert", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The MetadataTypeAttribute on type &apos;{0}&apos; results in a cyclic metadata provider chain. Either remove the attribute or remove the cycle..
+ /// </summary>
+ internal static string CyclicMetadataTypeAttributesFound {
+ get {
+ return ResourceManager.GetString("CyclicMetadataTypeAttributesFound", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to One or more associated objects were passed for collection property &apos;{1}&apos; on type &apos;{0}&apos;, but the target collection is null..
+ /// </summary>
+ internal static string DataController_AssociationCollectionPropertyIsNull {
+ get {
+ return ResourceManager.GetString("DataController_AssociationCollectionPropertyIsNull", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to This DataController does not support operation &apos;{0}&apos; for entity &apos;{1}&apos;..
+ /// </summary>
+ internal static string DataController_InvalidAction {
+ get {
+ return ResourceManager.GetString("DataController_InvalidAction", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Association collection member &apos;{0}&apos; does not implement IList and does not have an Add method..
+ /// </summary>
+ internal static string DataController_InvalidCollectionMember {
+ get {
+ return ResourceManager.GetString("DataController_InvalidCollectionMember", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Action &apos;{0}.{1}&apos; has one or more filters applied that do not derive from AuthorizationFilterAttribute. Only authorization filters are supported on DataController Insert/Update/Delete actions..
+ /// </summary>
+ internal static string InvalidAction_UnsupportedFilterType {
+ get {
+ return ResourceManager.GetString("InvalidAction_UnsupportedFilterType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Invalid ChangeSet: {0}.
+ /// </summary>
+ internal static string InvalidChangeSet {
+ get {
+ return ResourceManager.GetString("InvalidChangeSet", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Id &apos;{0}&apos; specified for association member &apos;{0}.{1}&apos; is invalid..
+ /// </summary>
+ internal static string InvalidChangeSet_AssociatedIdNotInChangeset {
+ get {
+ return ResourceManager.GetString("InvalidChangeSet_AssociatedIdNotInChangeset", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Associated Ids for member &apos;{0}.{1}&apos; cannot be null..
+ /// </summary>
+ internal static string InvalidChangeSet_AssociatedIdsCannotBeNull {
+ get {
+ return ResourceManager.GetString("InvalidChangeSet_AssociatedIdsCannotBeNull", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Only one entry for a given entity instance can exist in the ChangeSet..
+ /// </summary>
+ internal static string InvalidChangeSet_DuplicateEntity {
+ get {
+ return ResourceManager.GetString("InvalidChangeSet_DuplicateEntity", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Id must be unique for each entry..
+ /// </summary>
+ internal static string InvalidChangeSet_DuplicateId {
+ get {
+ return ResourceManager.GetString("InvalidChangeSet_DuplicateId", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to OriginalEntity cannot be specified for an Insert operation..
+ /// </summary>
+ internal static string InvalidChangeSet_InsertsCantHaveOriginal {
+ get {
+ return ResourceManager.GetString("InvalidChangeSet_InsertsCantHaveOriginal", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Association member &apos;{0}.{1}&apos; specified in the ChangeSet does not exist or is not marked with AssociationAttribute..
+ /// </summary>
+ internal static string InvalidChangeSet_InvalidAssociationMember {
+ get {
+ return ResourceManager.GetString("InvalidChangeSet_InvalidAssociationMember", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Entity and OriginalEntity must be of the same type..
+ /// </summary>
+ internal static string InvalidChangeSet_MustBeSameType {
+ get {
+ return ResourceManager.GetString("InvalidChangeSet_MustBeSameType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Entity cannot be null..
+ /// </summary>
+ internal static string InvalidChangeSet_NullEntity {
+ get {
+ return ResourceManager.GetString("InvalidChangeSet_NullEntity", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Type &apos;{0}&apos; must derive from &apos;{1}&apos;..
+ /// </summary>
+ internal static string InvalidType {
+ get {
+ return ResourceManager.GetString("InvalidType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to MetadataProvider type &apos;{0}&apos; must have a constructor with a single parameter of type &apos;MetadataProvider&apos;..
+ /// </summary>
+ internal static string MetadataProviderAttribute_MissingConstructor {
+ get {
+ return ResourceManager.GetString("MetadataProviderAttribute_MissingConstructor", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data/Resource.resx b/src/Microsoft.Web.Http.Data/Resource.resx
new file mode 100644
index 00000000..84b7d7dc
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data/Resource.resx
@@ -0,0 +1,174 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="ChangeSet_ChangeSetEntryNotFound" xml:space="preserve">
+ <value>The specified entity does not exist in the ChangeSet.</value>
+ </data>
+ <data name="ChangeSet_OriginalNotValidForInsert" xml:space="preserve">
+ <value>GetOriginal cannot be called for a new entity being inserted.</value>
+ </data>
+ <data name="CyclicMetadataTypeAttributesFound" xml:space="preserve">
+ <value>The MetadataTypeAttribute on type '{0}' results in a cyclic metadata provider chain. Either remove the attribute or remove the cycle.</value>
+ </data>
+ <data name="DataController_AssociationCollectionPropertyIsNull" xml:space="preserve">
+ <value>One or more associated objects were passed for collection property '{1}' on type '{0}', but the target collection is null.</value>
+ </data>
+ <data name="DataController_InvalidAction" xml:space="preserve">
+ <value>This DataController does not support operation '{0}' for entity '{1}'.</value>
+ </data>
+ <data name="DataController_InvalidCollectionMember" xml:space="preserve">
+ <value>Association collection member '{0}' does not implement IList and does not have an Add method.</value>
+ </data>
+ <data name="InvalidAction_UnsupportedFilterType" xml:space="preserve">
+ <value>Action '{0}.{1}' has one or more filters applied that do not derive from AuthorizationFilterAttribute. Only authorization filters are supported on DataController Insert/Update/Delete actions.</value>
+ </data>
+ <data name="InvalidChangeSet" xml:space="preserve">
+ <value>Invalid ChangeSet: {0}</value>
+ </data>
+ <data name="InvalidChangeSet_AssociatedIdNotInChangeset" xml:space="preserve">
+ <value>Id '{0}' specified for association member '{0}.{1}' is invalid.</value>
+ </data>
+ <data name="InvalidChangeSet_AssociatedIdsCannotBeNull" xml:space="preserve">
+ <value>Associated Ids for member '{0}.{1}' cannot be null.</value>
+ </data>
+ <data name="InvalidChangeSet_DuplicateEntity" xml:space="preserve">
+ <value>Only one entry for a given entity instance can exist in the ChangeSet.</value>
+ </data>
+ <data name="InvalidChangeSet_DuplicateId" xml:space="preserve">
+ <value>Id must be unique for each entry.</value>
+ </data>
+ <data name="InvalidChangeSet_InsertsCantHaveOriginal" xml:space="preserve">
+ <value>OriginalEntity cannot be specified for an Insert operation.</value>
+ </data>
+ <data name="InvalidChangeSet_InvalidAssociationMember" xml:space="preserve">
+ <value>Association member '{0}.{1}' specified in the ChangeSet does not exist or is not marked with AssociationAttribute.</value>
+ </data>
+ <data name="InvalidChangeSet_MustBeSameType" xml:space="preserve">
+ <value>Entity and OriginalEntity must be of the same type.</value>
+ </data>
+ <data name="InvalidChangeSet_NullEntity" xml:space="preserve">
+ <value>Entity cannot be null.</value>
+ </data>
+ <data name="InvalidType" xml:space="preserve">
+ <value>Type '{0}' must derive from '{1}'.</value>
+ </data>
+ <data name="MetadataProviderAttribute_MissingConstructor" xml:space="preserve">
+ <value>MetadataProvider type '{0}' must have a constructor with a single parameter of type 'MetadataProvider'.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/Microsoft.Web.Http.Data/RoundtripOriginalAttribute.cs b/src/Microsoft.Web.Http.Data/RoundtripOriginalAttribute.cs
new file mode 100644
index 00000000..79da905b
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data/RoundtripOriginalAttribute.cs
@@ -0,0 +1,13 @@
+namespace System.ComponentModel.DataAnnotations
+{
+ /// <summary>
+ /// When applied to a member, this attribute indicates that the original value of
+ /// the member should be sent back to the server when the object is updated. When applied
+ /// to a class, the attribute gets applied to each member of that class. If this attribute is not
+ /// present, the value of this member will be null in the original object sent back to the server.
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Class, AllowMultiple = false)]
+ public sealed class RoundtripOriginalAttribute : Attribute
+ {
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data/SubmitActionDescriptor.cs b/src/Microsoft.Web.Http.Data/SubmitActionDescriptor.cs
new file mode 100644
index 00000000..3445d455
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data/SubmitActionDescriptor.cs
@@ -0,0 +1,77 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Reflection;
+using System.Web.Http;
+using System.Web.Http.Controllers;
+
+namespace Microsoft.Web.Http.Data
+{
+ /// <summary>
+ /// This descriptor translates between the wire format for Submit (an enumerable
+ /// of ChangeSetEntry) and the actual Submit(ChangeSet) signature.
+ /// </summary>
+ internal sealed class SubmitActionDescriptor : ReflectedHttpActionDescriptor
+ {
+ private const string ChangeSetParameterName = "changeSet";
+ private Collection<HttpParameterDescriptor> _parameters;
+
+ public SubmitActionDescriptor(HttpControllerDescriptor controllerDescriptor, Type controllerType)
+ : base(controllerDescriptor, controllerType.GetMethod("Submit", BindingFlags.Instance | BindingFlags.Public))
+ {
+ _parameters = new Collection<HttpParameterDescriptor>(new List<HttpParameterDescriptor>() { new ChangeSetParameterDescriptor(this) });
+ }
+
+ public override Type ReturnType
+ {
+ get { return typeof(HttpResponseMessage); }
+ }
+
+ public override Collection<HttpParameterDescriptor> GetParameters()
+ {
+ return _parameters;
+ }
+
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Caller owns HttpResponseMessage.")]
+ public override object Execute(HttpControllerContext controllerContext, IDictionary<string, object> arguments)
+ {
+ // create a changeset from the entries
+ ChangeSet changeSet = new ChangeSet((IEnumerable<ChangeSetEntry>)arguments[ChangeSetParameterName]);
+ changeSet.SetEntityAssociations();
+
+ DataController controller = (DataController)controllerContext.Controller;
+ if (!controller.Submit(changeSet) &&
+ controller.ActionContext.Response != null)
+ {
+ // If the submit failed due to an authorization failure,
+ // return the authorization response directly
+ return controller.ActionContext.Response;
+ }
+
+ // return the entries
+ return controllerContext.Request.CreateResponse<ChangeSetEntry[]>(HttpStatusCode.OK, changeSet.ChangeSetEntries.ToArray());
+ }
+
+ /// <summary>
+ /// ParameterDescriptor representing the single Submit(ChangeSet) parameter,
+ /// but with the enumerable of ChangeSetEntry ParameterType so the formatters
+ /// deserialize the argument properly.
+ /// </summary>
+ private class ChangeSetParameterDescriptor : ReflectedHttpParameterDescriptor
+ {
+ public ChangeSetParameterDescriptor(HttpActionDescriptor actionDescriptor)
+ : base(actionDescriptor, actionDescriptor.ControllerDescriptor.ControllerType.GetMethod("Submit", BindingFlags.Instance | BindingFlags.Public).GetParameters()[0])
+ {
+ }
+
+ public override Type ParameterType
+ {
+ get { return typeof(ChangeSetEntry[]); }
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data/SubmitProxyActionDescriptor.cs b/src/Microsoft.Web.Http.Data/SubmitProxyActionDescriptor.cs
new file mode 100644
index 00000000..d61fb824
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data/SubmitProxyActionDescriptor.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Net.Http;
+using System.Net.Http.Formatting;
+using System.Reflection;
+using System.Web.Http.Controllers;
+
+namespace Microsoft.Web.Http.Data
+{
+ /// <summary>
+ /// This descriptor translates a direct CUD action invocation into a call to
+ /// Submit. This descriptor wraps the actual action, intercepting the execution
+ /// to do the transformation.
+ /// </summary>
+ internal sealed class SubmitProxyActionDescriptor : ReflectedHttpActionDescriptor
+ {
+ private UpdateActionDescriptor _updateAction;
+
+ public SubmitProxyActionDescriptor(UpdateActionDescriptor updateAction)
+ : base(updateAction.ControllerDescriptor, updateAction.ControllerDescriptor.ControllerType.GetMethod("Submit", BindingFlags.Instance | BindingFlags.Public))
+ {
+ _updateAction = updateAction;
+ }
+
+ public override string ActionName
+ {
+ get { return _updateAction.ActionName; }
+ }
+
+ public override Type ReturnType
+ {
+ get { return typeof(HttpResponseMessage); }
+ }
+
+ public override Collection<HttpParameterDescriptor> GetParameters()
+ {
+ return _updateAction.GetParameters();
+ }
+
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Caller is responsible for the lifetime of the object")]
+ public override object Execute(HttpControllerContext controllerContext, IDictionary<string, object> arguments)
+ {
+ // create the changeset
+ object entity = arguments.Single().Value; // there is only a single parameter - the entity being submitted
+ ChangeSetEntry[] changeSetEntries = new ChangeSetEntry[]
+ {
+ new ChangeSetEntry
+ {
+ Id = 1,
+ ActionDescriptor = _updateAction,
+ Entity = entity,
+ Operation = _updateAction.ChangeOperation
+ }
+ };
+ ChangeSet changeSet = new ChangeSet(changeSetEntries);
+ changeSet.SetEntityAssociations();
+
+ DataController controller = (DataController)controllerContext.Controller;
+ if (!controller.Submit(changeSet) &&
+ controller.ActionContext.Response != null)
+ {
+ // If the submit failed due to an authorization failure,
+ // return the authorization response directly
+ return controller.ActionContext.Response;
+ }
+
+ // return the entity
+ entity = changeSet.ChangeSetEntries[0].Entity;
+ // REVIEW does JSON make sense here?
+ return new HttpResponseMessage()
+ {
+ Content = new ObjectContent(_updateAction.EntityType, entity, new JsonMediaTypeFormatter())
+ };
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data/TypeDescriptorExtensions.cs b/src/Microsoft.Web.Http.Data/TypeDescriptorExtensions.cs
new file mode 100644
index 00000000..5e14ea5c
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data/TypeDescriptorExtensions.cs
@@ -0,0 +1,87 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+
+namespace Microsoft.Web.Http.Data
+{
+ /// <summary>
+ /// Extension methods for TypeDescriptors
+ /// </summary>
+ internal static class TypeDescriptorExtensions
+ {
+ /// <summary>
+ /// Extension method to extract only the explicitly specified attributes from a <see cref="PropertyDescriptor"/>.
+ /// </summary>
+ /// <remarks>
+ /// Normal TypeDescriptor semantics are to inherit the attributes of a property's type. This method
+ /// exists to suppress those inherited attributes.
+ /// </remarks>
+ /// <param name="propertyDescriptor">The property descriptor whose attributes are needed.</param>
+ /// <returns>A new <see cref="AttributeCollection"/> stripped of any attributes from the property's type.</returns>
+ public static AttributeCollection ExplicitAttributes(this PropertyDescriptor propertyDescriptor)
+ {
+ List<Attribute> attributes = new List<Attribute>(propertyDescriptor.Attributes.Cast<Attribute>());
+ AttributeCollection typeAttributes = TypeDescriptor.GetAttributes(propertyDescriptor.PropertyType);
+ bool removedAttribute = false;
+ foreach (Attribute attr in typeAttributes)
+ {
+ for (int i = attributes.Count - 1; i >= 0; --i)
+ {
+ // We must use ReferenceEquals since attributes could Match if they are the same.
+ // Only ReferenceEquals will catch actual duplications.
+ if (Object.ReferenceEquals(attr, attributes[i]))
+ {
+ attributes.RemoveAt(i);
+ removedAttribute = true;
+ }
+ }
+ }
+ return removedAttribute ? new AttributeCollection(attributes.ToArray()) : propertyDescriptor.Attributes;
+ }
+
+ /// <summary>
+ /// Extension method to extract attributes from a type taking into account the inheritance type of attributes
+ /// </summary>
+ /// <remarks>
+ /// Normal TypeDescriptor semantics are to inherit the attributes of a type's base type, regardless of their
+ /// inheritance type.
+ /// </remarks>
+ /// <param name="type">The type whose attributes are needed.</param>
+ /// <returns>A new <see cref="AttributeCollection"/> stripped of any incorrectly inherited attributes from the type.</returns>
+ public static AttributeCollection Attributes(this Type type)
+ {
+ AttributeCollection baseTypeAttributes = TypeDescriptor.GetAttributes(type.BaseType);
+ List<Attribute> typeAttributes = new List<Attribute>(TypeDescriptor.GetAttributes(type).Cast<Attribute>());
+ foreach (Attribute attr in baseTypeAttributes)
+ {
+ AttributeUsageAttribute attributeUsageAtt = (AttributeUsageAttribute)TypeDescriptor.GetAttributes(attr)[typeof(AttributeUsageAttribute)];
+ if (attributeUsageAtt != null && !attributeUsageAtt.Inherited)
+ {
+ for (int i = typeAttributes.Count - 1; i >= 0; --i)
+ {
+ // We must use ReferenceEquals since attributes could Match if they are the same.
+ // Only ReferenceEquals will catch actual duplications.
+ if (Object.ReferenceEquals(attr, typeAttributes[i]))
+ {
+ typeAttributes.RemoveAt(i);
+ break;
+ }
+ }
+ }
+ }
+ return new AttributeCollection(typeAttributes.ToArray());
+ }
+
+ /// <summary>
+ /// Checks to see if an attribute collection contains any attributes of the provided type.
+ /// </summary>
+ /// <typeparam name="TAttribute">The attribute type to check for</typeparam>
+ /// <param name="attributes">The attribute collection to inspect</param>
+ /// <returns><c>True</c> if an attribute of the provided type is contained in the attribute collection.</returns>
+ public static bool ContainsAttributeType<TAttribute>(this AttributeCollection attributes) where TAttribute : Attribute
+ {
+ return attributes.Cast<Attribute>().Any(a => a.GetType() == typeof(TAttribute));
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data/TypeUtility.cs b/src/Microsoft.Web.Http.Data/TypeUtility.cs
new file mode 100644
index 00000000..f3f29993
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data/TypeUtility.cs
@@ -0,0 +1,156 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.Serialization;
+using System.Threading.Tasks;
+
+namespace Microsoft.Web.Http.Data
+{
+ internal static class TypeUtility
+ {
+ public static Type GetElementType(Type type)
+ {
+ // Array, pointers, etc.
+ if (type.HasElementType)
+ {
+ return type.GetElementType();
+ }
+
+ // IEnumerable<T> returns T
+ Type ienum = FindIEnumerable(type);
+ if (ienum != null)
+ {
+ Type genericArg = ienum.GetGenericArguments()[0];
+ return genericArg;
+ }
+
+ return type;
+ }
+
+ internal static Type FindIEnumerable(Type seqType)
+ {
+ if (seqType == null || seqType == typeof(string))
+ {
+ return null;
+ }
+ if (seqType.IsArray)
+ {
+ return typeof(IEnumerable<>).MakeGenericType(seqType.GetElementType());
+ }
+ if (seqType.IsGenericType)
+ {
+ foreach (Type arg in seqType.GetGenericArguments())
+ {
+ Type ienum = typeof(IEnumerable<>).MakeGenericType(arg);
+ if (ienum.IsAssignableFrom(seqType))
+ {
+ return ienum;
+ }
+ }
+ }
+ Type[] ifaces = seqType.GetInterfaces();
+ if (ifaces != null && ifaces.Length > 0)
+ {
+ foreach (Type iface in ifaces)
+ {
+ Type ienum = FindIEnumerable(iface);
+ if (ienum != null)
+ {
+ return ienum;
+ }
+ }
+ }
+ if (seqType.BaseType != null && seqType.BaseType != typeof(object))
+ {
+ return FindIEnumerable(seqType.BaseType);
+ }
+ return null;
+ }
+
+ /// <summary>
+ /// Returns true if the specified PropertyDescriptor is publically
+ /// exposed, based on DataContract visibility rules.
+ /// </summary>
+ internal static bool IsDataMember(PropertyDescriptor pd)
+ {
+ AttributeCollection attrs = pd.ComponentType.Attributes();
+
+ if (attrs[typeof(DataContractAttribute)] != null)
+ {
+ if (pd.Attributes[typeof(DataMemberAttribute)] == null)
+ {
+ return false;
+ }
+ }
+ else
+ {
+ if (pd.Attributes[typeof(IgnoreDataMemberAttribute)] != null)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Obtains the set of known types from the <see cref="KnownTypeAttribute"/> custom attributes
+ /// attached to the specified <paramref name="type"/>.
+ /// </summary>
+ /// <remarks>
+ /// This utility function either retrieving the declared types or invokes the method declared in <see cref="KnownTypeAttribute.MethodName"/>.
+ /// </remarks>
+ /// <param name="type">The type to examine for <see cref="KnownTypeAttribute"/>s</param>
+ /// <param name="inherit"><c>true</c> to allow inheritance of <see cref="KnownTypeAttribute"/> from the base.</param>
+ /// <returns>The distinct set of types fould via the <see cref="KnownTypeAttribute"/>s</returns>
+ internal static IEnumerable<Type> GetKnownTypes(Type type, bool inherit)
+ {
+ IDictionary<Type, Type> knownTypes = new Dictionary<Type, Type>();
+ IEnumerable<KnownTypeAttribute> knownTypeAttributes = type.GetCustomAttributes(typeof(KnownTypeAttribute), inherit).Cast<KnownTypeAttribute>();
+
+ foreach (KnownTypeAttribute knownTypeAttribute in knownTypeAttributes)
+ {
+ Type knownType = knownTypeAttribute.Type;
+ if (knownType != null)
+ {
+ knownTypes[knownType] = knownType;
+ }
+
+ string methodName = knownTypeAttribute.MethodName;
+ if (!String.IsNullOrEmpty(methodName))
+ {
+ Type typeOfIEnumerableOfType = typeof(IEnumerable<Type>);
+ MethodInfo methodInfo = type.GetMethod(methodName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.DeclaredOnly);
+ if (methodInfo != null && typeOfIEnumerableOfType.IsAssignableFrom(methodInfo.ReturnType))
+ {
+ IEnumerable<Type> enumerable = methodInfo.Invoke(null, null) as IEnumerable<Type>;
+ if (enumerable != null)
+ {
+ foreach (Type t in enumerable)
+ {
+ knownTypes[t] = t;
+ }
+ }
+ }
+ }
+ }
+ return knownTypes.Keys;
+ }
+
+ /// <summary>
+ /// If the specified type is a generic Task, this function returns the
+ /// inner task type.
+ /// </summary>
+ internal static Type UnwrapTaskInnerType(Type t)
+ {
+ if (typeof(Task).IsAssignableFrom(t) && t.IsGenericType)
+ {
+ return t.GetGenericArguments()[0];
+ }
+
+ return t;
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data/UpdateActionDescriptor.cs b/src/Microsoft.Web.Http.Data/UpdateActionDescriptor.cs
new file mode 100644
index 00000000..a53e2e11
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data/UpdateActionDescriptor.cs
@@ -0,0 +1,68 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Reflection;
+using System.Web.Http.Controllers;
+using System.Web.Http.Filters;
+
+namespace Microsoft.Web.Http.Data
+{
+ [DebuggerDisplay("Action = {ActionName}, Type = {EntityType.Name}, Operation = {ChangeOperation}")]
+ public class UpdateActionDescriptor : ReflectedHttpActionDescriptor
+ {
+ private readonly ChangeOperation _changeOperation;
+ private readonly Type _entityType;
+ private readonly MethodInfo _method;
+
+ public UpdateActionDescriptor(HttpControllerDescriptor controllerDescriptor, MethodInfo method, Type entityType, ChangeOperation operationType)
+ : base(controllerDescriptor, method)
+ {
+ _entityType = entityType;
+ _changeOperation = operationType;
+ _method = method;
+ }
+
+ public Type EntityType
+ {
+ get { return _entityType; }
+ }
+
+ public ChangeOperation ChangeOperation
+ {
+ get { return _changeOperation; }
+ }
+
+ public bool Authorize(HttpActionContext context)
+ {
+ // We only select Action scope Authorization filters, since Global and Class level filters will already
+ // be executed when Submit is invoked. We only look at AuthorizationFilterAttributes because we are only
+ // interested in running synchronous (i.e., quick to run) attributes.
+ IEnumerable<AuthorizationFilterAttribute> authFilters =
+ GetFilterPipeline()
+ .Where(p => p.Scope == FilterScope.Action)
+ .Select(p => p.Instance)
+ .OfType<AuthorizationFilterAttribute>();
+
+ foreach (AuthorizationFilterAttribute authFilter in authFilters)
+ {
+ authFilter.OnAuthorization(context);
+
+ if (context.Response != null && !context.Response.IsSuccessStatusCode)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public override object Execute(HttpControllerContext controllerContext, IDictionary<string, object> arguments)
+ {
+ DataController controller = (DataController)controllerContext.Controller;
+ object[] paramValues = arguments.Select(p => p.Value).ToArray();
+
+ return _method.Invoke(controller, paramValues);
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data/UpdateAttribute.cs b/src/Microsoft.Web.Http.Data/UpdateAttribute.cs
new file mode 100644
index 00000000..43519b5e
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data/UpdateAttribute.cs
@@ -0,0 +1,18 @@
+using System;
+
+namespace Microsoft.Web.Http.Data
+{
+ /// <summary>
+ /// Attribute applied to a <see cref="DataController"/> method to indicate that it is an update method.
+ /// </summary>
+ [AttributeUsage(
+ AttributeTargets.Field | AttributeTargets.Method | AttributeTargets.Property,
+ AllowMultiple = false, Inherited = true)]
+ public sealed class UpdateAttribute : Attribute
+ {
+ /// <summary>
+ /// Gets or sets a value indicating whether the method is a custom update operation.
+ /// </summary>
+ public bool UsingCustomMethod { get; set; }
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data/ValidationResultInfo.cs b/src/Microsoft.Web.Http.Data/ValidationResultInfo.cs
new file mode 100644
index 00000000..9faede0a
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data/ValidationResultInfo.cs
@@ -0,0 +1,124 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.Serialization;
+using System.Web.Http.Common;
+
+namespace Microsoft.Web.Http.Data
+{
+ /// <summary>
+ /// The data contract of an error that has occurred
+ /// during the execution of an operation on the server.
+ /// This is sent back along with the action
+ /// result(s) to the client.
+ /// </summary>
+ [DataContract]
+ public sealed class ValidationResultInfo : IEquatable<ValidationResultInfo>
+ {
+ /// <summary>
+ /// Constructor accepting a localized error message and and collection
+ /// of the names of the members the error originated from.
+ /// </summary>
+ /// <param name="message">The localized message</param>
+ /// <param name="sourceMemberNames">A collection of the names of the members the error originated from.</param>
+ public ValidationResultInfo(string message, IEnumerable<string> sourceMemberNames)
+ {
+ if (message == null)
+ {
+ throw Error.ArgumentNull("message");
+ }
+
+ if (sourceMemberNames == null)
+ {
+ throw Error.ArgumentNull("sourceMemberNames");
+ }
+
+ Message = message;
+ SourceMemberNames = sourceMemberNames;
+ }
+
+ /// <summary>
+ /// Constructor accepting a localized error message, error code, optional stack trace,
+ /// and collection of the names of the members the error originated from.
+ /// </summary>
+ /// <param name="message">The localized error message</param>
+ /// <param name="errorCode">The custom error code</param>
+ /// <param name="stackTrace">The error stack trace</param>
+ /// <param name="sourceMemberNames">A collection of the names of the members the error originated from.</param>
+ public ValidationResultInfo(string message, int errorCode, string stackTrace, IEnumerable<string> sourceMemberNames)
+ {
+ if (message == null)
+ {
+ throw Error.ArgumentNull("message");
+ }
+
+ if (sourceMemberNames == null)
+ {
+ throw Error.ArgumentNull("sourceMemberNames");
+ }
+
+ Message = message;
+ ErrorCode = errorCode;
+ StackTrace = stackTrace;
+ SourceMemberNames = sourceMemberNames;
+ }
+
+ /// <summary>
+ /// Gets or sets the error message
+ /// </summary>
+ [DataMember(EmitDefaultValue = false)]
+ public string Message { get; set; }
+
+ /// <summary>
+ /// Gets or sets custom error code
+ /// </summary>
+ [DataMember(EmitDefaultValue = false)]
+ public int ErrorCode { get; set; }
+
+ /// <summary>
+ /// Gets or sets the stack trace of the error
+ /// </summary>
+ [DataMember(EmitDefaultValue = false)]
+ public string StackTrace { get; set; }
+
+ /// <summary>
+ /// Gets or sets the names of the members the error originated from.
+ /// </summary>
+ [DataMember(EmitDefaultValue = false)]
+ public IEnumerable<string> SourceMemberNames { get; set; }
+
+ /// <summary>
+ /// Returns the hash code for this object.
+ /// </summary>
+ /// <returns>The hash code for this object.</returns>
+ public override int GetHashCode()
+ {
+ return Message.GetHashCode();
+ }
+
+ #region IEquatable<ValidationResultInfo> Members
+
+ /// <summary>
+ /// Test the current instance against the specified instance for equality
+ /// </summary>
+ /// <param name="other">The ValidationResultInfo to compare to</param>
+ /// <returns>True if the instances are equal, false otherwise</returns>
+ bool IEquatable<ValidationResultInfo>.Equals(ValidationResultInfo other)
+ {
+ if (Object.ReferenceEquals(this, other))
+ {
+ return true;
+ }
+ if (Object.ReferenceEquals(null, other))
+ {
+ return false;
+ }
+ return ((Message == other.Message) &&
+ (ErrorCode == other.ErrorCode) &&
+ (StackTrace == other.StackTrace) &&
+ (SourceMemberNames.SequenceEqual(other.SourceMemberNames)));
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Microsoft.Web.Http.Data/packages.config b/src/Microsoft.Web.Http.Data/packages.config
new file mode 100644
index 00000000..0fc85828
--- /dev/null
+++ b/src/Microsoft.Web.Http.Data/packages.config
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="Microsoft.Net.Http" version="2.0.20302.1" />
+ <package id="Newtonsoft.Json" version="4.0.8" />
+</packages> \ No newline at end of file
diff --git a/src/Microsoft.Web.Mvc/ActionLinkAreaAttribute.cs b/src/Microsoft.Web.Mvc/ActionLinkAreaAttribute.cs
new file mode 100644
index 00000000..bbbc08a7
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ActionLinkAreaAttribute.cs
@@ -0,0 +1,20 @@
+using System;
+
+namespace Microsoft.Web.Mvc
+{
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
+ public sealed class ActionLinkAreaAttribute : Attribute
+ {
+ public ActionLinkAreaAttribute(string area)
+ {
+ if (area == null)
+ {
+ throw new ArgumentNullException("area");
+ }
+
+ Area = area;
+ }
+
+ public string Area { get; private set; }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/AjaxOnlyAttribute.cs b/src/Microsoft.Web.Mvc/AjaxOnlyAttribute.cs
new file mode 100644
index 00000000..8b74de76
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/AjaxOnlyAttribute.cs
@@ -0,0 +1,23 @@
+using System;
+using System.Reflection;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc
+{
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+ public sealed class AjaxOnlyAttribute : ActionMethodSelectorAttribute
+ {
+ public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
+ {
+ if (controllerContext == null)
+ {
+ throw new ArgumentNullException("controllerContext");
+ }
+
+ // Dev10 #939671 - If this attribute is going to say AJAX *only*, then we need to check the header
+ // specifically, as otherwise clients can modify the form or query string to contain the name/value
+ // pair we're looking for.
+ return (controllerContext.HttpContext.Request.Headers["X-Requested-With"] == "XMLHttpRequest");
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/AreaHelpers.cs b/src/Microsoft.Web.Mvc/AreaHelpers.cs
new file mode 100644
index 00000000..40042895
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/AreaHelpers.cs
@@ -0,0 +1,36 @@
+using System.Web.Mvc;
+using System.Web.Routing;
+
+namespace Microsoft.Web.Mvc
+{
+ public static class AreaHelpers
+ {
+ public static string GetAreaName(RouteBase route)
+ {
+ IRouteWithArea routeWithArea = route as IRouteWithArea;
+ if (routeWithArea != null)
+ {
+ return routeWithArea.Area;
+ }
+
+ Route castRoute = route as Route;
+ if (castRoute != null && castRoute.DataTokens != null)
+ {
+ return castRoute.DataTokens["area"] as string;
+ }
+
+ return null;
+ }
+
+ public static string GetAreaName(RouteData routeData)
+ {
+ object area;
+ if (routeData.DataTokens.TryGetValue("area", out area))
+ {
+ return area as string;
+ }
+
+ return GetAreaName(routeData.Route);
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/AsyncManagerExtensions.cs b/src/Microsoft.Web.Mvc/AsyncManagerExtensions.cs
new file mode 100644
index 00000000..e25f14f8
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/AsyncManagerExtensions.cs
@@ -0,0 +1,82 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Web.Mvc.Async;
+
+namespace Microsoft.Web.Mvc
+{
+ public static class AsyncManagerExtensions
+ {
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "An unhandled exception here will bring down the worker process.")]
+ [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1409:RemoveUnnecessaryCode",
+ Justification = "The empty lock statement is required for synchronization.")]
+ public static void RegisterTask(this AsyncManager asyncManager, Func<AsyncCallback, IAsyncResult> beginDelegate, Action<IAsyncResult> endDelegate)
+ {
+ if (asyncManager == null)
+ {
+ throw new ArgumentNullException("asyncManager");
+ }
+ if (beginDelegate == null)
+ {
+ throw new ArgumentNullException("beginDelegate");
+ }
+ if (endDelegate == null)
+ {
+ throw new ArgumentNullException("endDelegate");
+ }
+
+ // need to wait to execute the callback until after BeginXxx() has completed
+ object delegateExecutingLockObj = new object();
+
+ AsyncCallback callback = ar =>
+ {
+ lock (delegateExecutingLockObj)
+ {
+ // this empty lock is required to synchronized with the beginDelegate call
+ }
+ if (!ar.CompletedSynchronously)
+ {
+ try
+ {
+ asyncManager.Sync(() => endDelegate(ar)); // called on different thread, so have to take application lock
+ }
+ catch
+ {
+ // Need to swallow exceptions, as otherwise unhandled exceptions on a ThreadPool thread
+ // can bring down the entire worker process.
+ }
+ finally
+ {
+ asyncManager.OutstandingOperations.Decrement();
+ }
+ }
+ };
+
+ IAsyncResult asyncResult;
+ asyncManager.OutstandingOperations.Increment();
+ try
+ {
+ lock (delegateExecutingLockObj)
+ {
+ asyncResult = beginDelegate(callback);
+ }
+ }
+ catch
+ {
+ asyncManager.OutstandingOperations.Decrement();
+ throw;
+ }
+
+ if (asyncResult.CompletedSynchronously)
+ {
+ try
+ {
+ endDelegate(asyncResult); // call on same thread
+ }
+ finally
+ {
+ asyncManager.OutstandingOperations.Decrement();
+ }
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ButtonBuilder.cs b/src/Microsoft.Web.Mvc/ButtonBuilder.cs
new file mode 100644
index 00000000..cdbf34c3
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ButtonBuilder.cs
@@ -0,0 +1,87 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc
+{
+ public static class ButtonBuilder
+ {
+ public static TagBuilder SubmitButton(string name, string buttonText, IDictionary<string, object> htmlAttributes)
+ {
+ TagBuilder buttonTag = new TagBuilder("input");
+
+ buttonTag.MergeAttribute("type", "submit");
+ if (!buttonTag.Attributes.ContainsKey("id") && name != null)
+ {
+ buttonTag.GenerateId(name);
+ }
+
+ if (!String.IsNullOrEmpty(name))
+ {
+ buttonTag.MergeAttribute("name", name);
+ }
+
+ if (!String.IsNullOrEmpty(buttonText))
+ {
+ buttonTag.MergeAttribute("value", buttonText);
+ }
+
+ buttonTag.MergeAttributes(htmlAttributes, true);
+ return buttonTag;
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "The return value is not a regular URL since it may contain ~/ ASP.NET-specific characters")]
+ public static TagBuilder SubmitImage(string name, string sourceUrl, IDictionary<string, object> htmlAttributes)
+ {
+ TagBuilder buttonTag = new TagBuilder("input");
+
+ buttonTag.MergeAttribute("type", "image");
+
+ if (!buttonTag.Attributes.ContainsKey("id"))
+ {
+ buttonTag.GenerateId(name);
+ }
+
+ if (!String.IsNullOrEmpty(name))
+ {
+ buttonTag.MergeAttribute("name", name);
+ }
+
+ if (!String.IsNullOrEmpty(sourceUrl))
+ {
+ buttonTag.MergeAttribute("src", sourceUrl);
+ }
+ buttonTag.MergeAttributes(htmlAttributes, true);
+ return buttonTag;
+ }
+
+ [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "This conversion is appropriate because it's for HTML, not for comparsion normalization")]
+ public static TagBuilder Button(string name, string buttonText, HtmlButtonType type, string onClickMethod, IDictionary<string, object> htmlAttributes)
+ {
+ if (name == null)
+ {
+ throw new ArgumentNullException("name");
+ }
+
+ TagBuilder buttonTag = new TagBuilder("button");
+
+ if (!String.IsNullOrEmpty(name))
+ {
+ buttonTag.MergeAttribute("name", name);
+ }
+
+ buttonTag.MergeAttribute("type", type.ToString().ToLowerInvariant());
+
+ buttonTag.InnerHtml = buttonText;
+
+ if (!String.IsNullOrEmpty(onClickMethod))
+ {
+ buttonTag.MergeAttribute("onclick", onClickMethod);
+ }
+
+ buttonTag.MergeAttributes(htmlAttributes, true);
+ return buttonTag;
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ButtonsAndLinkExtensions.cs b/src/Microsoft.Web.Mvc/ButtonsAndLinkExtensions.cs
new file mode 100644
index 00000000..632b0539
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ButtonsAndLinkExtensions.cs
@@ -0,0 +1,171 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc
+{
+ public static class ButtonsAndLinkExtensions
+ {
+ /// <summary>
+ /// Creates a submit button for your form
+ /// </summary>
+ /// <param name="helper">The helper which we extend.</param>
+ /// <param name="name">The name.</param>
+ /// <returns></returns>
+ public static MvcHtmlString SubmitButton(this HtmlHelper helper, string name)
+ {
+ return SubmitButton(helper, name, null, null);
+ }
+
+ /// <summary>
+ /// Creates a submit button for your form
+ /// </summary>
+ /// <param name="helper">The helper which we extend.</param>
+ /// <param name="name">The name.</param>
+ /// <param name="buttonText">The text for the button face</param>
+ /// <returns></returns>
+ public static MvcHtmlString SubmitButton(this HtmlHelper helper, string name, string buttonText)
+ {
+ return SubmitButton(helper, name, buttonText, null);
+ }
+
+ /// <summary>
+ /// Creates a submit button for your form
+ /// </summary>
+ /// <param name="helper">The helper which we extend.</param>
+ public static MvcHtmlString SubmitButton(this HtmlHelper helper)
+ {
+ return SubmitButton(helper, null, null, null);
+ }
+
+ /// <summary>
+ /// Creates a submit button for your form
+ /// </summary>
+ /// <param name="helper">The helper which we extend.</param>
+ /// <param name="name">Name of the button</param>
+ /// <param name="buttonText">The text for the button face</param>
+ /// <param name="htmlAttributes">Any attributes you want set on the tag. Use anonymous-type declaration for this: new{class=cssclass}</param>
+ /// <returns></returns>
+ public static MvcHtmlString SubmitButton(this HtmlHelper helper, string name, string buttonText, object htmlAttributes)
+ {
+ return helper.SubmitButton(name, buttonText, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ /// <summary>
+ /// Creates a submit button for your form
+ /// </summary>
+ /// <param name="helper">The helper which we extend.</param>
+ /// <param name="name">Name of the button</param>
+ /// <param name="buttonText">The text for the button face</param>
+ /// <param name="htmlAttributes">Dictionary of HTML settings</param>
+ /// <returns></returns>
+ [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "helper", Justification = "Required for Extension Method")]
+ public static MvcHtmlString SubmitButton(this HtmlHelper helper, string name, string buttonText, IDictionary<string, object> htmlAttributes)
+ {
+ return MvcHtmlString.Create(ButtonBuilder.SubmitButton(name, buttonText, htmlAttributes).ToString(TagRenderMode.SelfClosing));
+ }
+
+ /// <summary>
+ /// Creates a submit button for your form using an image
+ /// </summary>
+ /// <param name="helper">The helper which we extend.</param>
+ /// <param name="name">Name of the button</param>
+ /// <param name="imageSrc">The URL for the image</param>
+ /// <returns></returns>
+ public static MvcHtmlString SubmitImage(this HtmlHelper helper, string name, string imageSrc)
+ {
+ return helper.SubmitImage(name, imageSrc, null);
+ }
+
+ /// <summary>
+ /// Creates a submit button for your form using an image
+ /// </summary>
+ /// <param name="helper">The helper which we extend.</param>
+ /// <param name="name">Name of the button</param>
+ /// <param name="imageSrc">The URL for the image</param>
+ /// <param name="htmlAttributes">Any attributes you want set on the tag. Use anonymous-type declaration for this: new{class=cssclass}</param>
+ /// <returns></returns>
+ public static MvcHtmlString SubmitImage(this HtmlHelper helper, string name, string imageSrc, object htmlAttributes)
+ {
+ return helper.SubmitImage(name, imageSrc, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ /// <summary>
+ /// Creates a submit button for your form using an image
+ /// </summary>
+ /// <param name="helper">The helper which we extend.</param>
+ /// <param name="name">Name of the button</param>
+ /// <param name="imageSrc">The URL for the image</param>
+ /// <param name="htmlAttributes">Dictionary of HTML settings</param>
+ /// <returns></returns>
+ public static MvcHtmlString SubmitImage(this HtmlHelper helper, string name, string imageSrc, IDictionary<string, object> htmlAttributes)
+ {
+ if (imageSrc == null)
+ {
+ throw new ArgumentNullException("imageSrc");
+ }
+
+ string resolvedUrl = UrlHelper.GenerateContentUrl(imageSrc, helper.ViewContext.HttpContext);
+ return MvcHtmlString.Create(ButtonBuilder.SubmitImage(name, resolvedUrl, htmlAttributes).ToString(TagRenderMode.SelfClosing));
+ }
+
+ /// <summary>
+ /// A Simple button you can use with javascript
+ /// </summary>
+ /// <param name="helper">The helper which we extend.</param>
+ /// <param name="name">Name of the button</param>
+ /// <param name="buttonText">The text for the button face</param>
+ /// <param name="buttonType">The button type (Button, Submit, or Reset)</param>
+ /// <returns></returns>
+ public static MvcHtmlString Button(this HtmlHelper helper, string name, string buttonText, HtmlButtonType buttonType)
+ {
+ return helper.Button(name, buttonText, buttonType, null, (IDictionary<string, object>)null);
+ }
+
+ /// <summary>
+ /// A Simple button you can use with javascript
+ /// </summary>
+ /// <param name="helper">The helper which we extend.</param>
+ /// <param name="name">Name of the button</param>
+ /// <param name="buttonText">The text for the button face</param>
+ /// <param name="buttonType">The button type (Button, Submit, or Reset)</param>
+ /// <param name="onClickMethod">The method or script routine to call when the button is clicked.</param>
+ /// <returns></returns>
+ public static MvcHtmlString Button(this HtmlHelper helper, string name, string buttonText, HtmlButtonType buttonType, string onClickMethod)
+ {
+ return helper.Button(name, buttonText, buttonType, onClickMethod, (IDictionary<string, object>)null);
+ }
+
+ /// <summary>
+ /// A Simple button you can use with javascript
+ /// </summary>
+ /// <param name="helper">The helper which we extend.</param>
+ /// <param name="name">Name of the button</param>
+ /// <param name="buttonText">The text for the button face</param>
+ /// <param name="buttonType">The button type (Button, Submit, or Reset)</param>
+ /// <param name="onClickMethod">The method or script routine to call when the button is clicked.</param>
+ /// <param name="htmlAttributes">Any attributes you want set on the tag. Use anonymous-type declaration for this: new{class=cssclass}</param>
+ /// <returns></returns>
+ public static MvcHtmlString Button(this HtmlHelper helper, string name, string buttonText, HtmlButtonType buttonType, string onClickMethod, object htmlAttributes)
+ {
+ return helper.Button(name, buttonText, buttonType, onClickMethod, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ /// <summary>
+ /// A Simple button you can use with javascript
+ /// </summary>
+ /// <param name="helper">The helper which we extend.</param>
+ /// <param name="name">Name of the button</param>
+ /// <param name="buttonText">The text for the button face</param>
+ /// <param name="buttonType">The button type (Button, Submit, or Reset)</param>
+ /// <param name="onClickMethod">The method or script routine to call when the button is clicked.</param>
+ /// <param name="htmlAttributes">Any attributes you want set on the tag. Use anonymous-type declaration for this: new{class=cssclass}</param>
+ /// <returns></returns>
+ [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "helper", Justification = "This is an Extension Method and requires this argument")]
+ public static MvcHtmlString Button(this HtmlHelper helper, string name, string buttonText, HtmlButtonType buttonType, string onClickMethod, IDictionary<string, object> htmlAttributes)
+ {
+ return MvcHtmlString.Create(ButtonBuilder.Button(name, buttonText, buttonType, onClickMethod, htmlAttributes).ToString(TagRenderMode.Normal));
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/CachedExpressionCompiler.cs b/src/Microsoft.Web.Mvc/CachedExpressionCompiler.cs
new file mode 100644
index 00000000..04f33ccd
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/CachedExpressionCompiler.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq.Expressions;
+
+namespace Microsoft.Web.Mvc
+{
+ // The caching expression tree compiler was copied from MVC core to MVC Futures so that Futures code could benefit
+ // from it and so that it could be exposed as a public API. This is the only public entry point into the system.
+ // See the comments in the ExpressionUtil namespace for more information.
+ //
+ // The unit tests for the ExpressionUtil.* types are in the System.Web.Mvc.Test project.
+ public static class CachedExpressionCompiler
+ {
+ private static readonly ParameterExpression _unusedParameterExpr = Expression.Parameter(typeof(object), "_unused");
+
+ // Implements caching around LambdaExpression.Compile() so that equivalent expression trees only have to be
+ // compiled once.
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static Func<TModel, TValue> Compile<TModel, TValue>(this Expression<Func<TModel, TValue>> lambdaExpression)
+ {
+ if (lambdaExpression == null)
+ {
+ throw new ArgumentNullException("lambdaExpression");
+ }
+
+ return ExpressionUtil.CachedExpressionCompiler.Process(lambdaExpression);
+ }
+
+ // Evaluates an expression (not a LambdaExpression), e.g. 2 + 2.
+ public static object Evaluate(Expression arg)
+ {
+ if (arg == null)
+ {
+ throw new ArgumentNullException("arg");
+ }
+
+ Func<object, object> func = Wrap(arg);
+ return func(null);
+ }
+
+ private static Func<object, object> Wrap(Expression arg)
+ {
+ Expression<Func<object, object>> lambdaExpr = Expression.Lambda<Func<object, object>>(Expression.Convert(arg, typeof(object)), _unusedParameterExpr);
+ return ExpressionUtil.CachedExpressionCompiler.Process(lambdaExpr);
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ContentTypeAttribute.cs b/src/Microsoft.Web.Mvc/ContentTypeAttribute.cs
new file mode 100644
index 00000000..3563f513
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ContentTypeAttribute.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Web.Mvc;
+using Microsoft.Web.Mvc.Properties;
+
+namespace Microsoft.Web.Mvc
+{
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
+ public sealed class ContentTypeAttribute : ActionFilterAttribute
+ {
+ public ContentTypeAttribute(string contentType)
+ {
+ if (String.IsNullOrEmpty(contentType))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "contentType");
+ }
+
+ ContentType = contentType;
+ }
+
+ public string ContentType { get; private set; }
+
+ public override void OnResultExecuting(ResultExecutingContext filterContext)
+ {
+ filterContext.HttpContext.Response.ContentType = ContentType;
+ }
+
+ public override void OnResultExecuted(ResultExecutedContext filterContext)
+ {
+ filterContext.HttpContext.Response.ContentType = ContentType;
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ControllerExtensions.cs b/src/Microsoft.Web.Mvc/ControllerExtensions.cs
new file mode 100644
index 00000000..65a89641
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ControllerExtensions.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq.Expressions;
+using System.Web.Mvc;
+using System.Web.Routing;
+using ExpressionHelper = Microsoft.Web.Mvc.Internal.ExpressionHelper;
+
+namespace Microsoft.Web.Mvc
+{
+ public static class ControllerExtensions
+ {
+ // Shortcut to allow users to write this.RedirectToAction(x => x.OtherMethod()) to redirect
+ // to a different method on the same controller.
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static RedirectToRouteResult RedirectToAction<TController>(this TController controller, Expression<Action<TController>> action) where TController : Controller
+ {
+ return RedirectToAction((Controller)controller, action);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static RedirectToRouteResult RedirectToAction<TController>(this Controller controller, Expression<Action<TController>> action) where TController : Controller
+ {
+ if (controller == null)
+ {
+ throw new ArgumentNullException("controller");
+ }
+
+ RouteValueDictionary routeValues = ExpressionHelper.GetRouteValuesFromExpression(action);
+ return new RedirectToRouteResult(routeValues);
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Controls/ActionLink.cs b/src/Microsoft.Web.Mvc/Controls/ActionLink.cs
new file mode 100644
index 00000000..d1bef13c
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Controls/ActionLink.cs
@@ -0,0 +1,109 @@
+using System;
+using System.ComponentModel;
+using System.Web.Mvc;
+using System.Web.Routing;
+using System.Web.UI;
+
+namespace Microsoft.Web.Mvc.Controls
+{
+ [ParseChildren(true)]
+ [PersistChildren(false)]
+ public class ActionLink : MvcControl
+ {
+ private string _actionName;
+ private string _controllerName;
+ private string _text;
+ private string _routeName;
+ private RouteValues _values;
+
+ [DefaultValue("")]
+ public string ActionName
+ {
+ get { return _actionName ?? String.Empty; }
+ set { _actionName = value; }
+ }
+
+ [DefaultValue("")]
+ public string ControllerName
+ {
+ get { return _controllerName ?? String.Empty; }
+ set { _controllerName = value; }
+ }
+
+ [DefaultValue("")]
+ public string RouteName
+ {
+ get { return _routeName ?? String.Empty; }
+ set { _routeName = value; }
+ }
+
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
+ [PersistenceMode(PersistenceMode.InnerProperty)]
+ public RouteValues Values
+ {
+ get
+ {
+ if (_values == null)
+ {
+ _values = new RouteValues();
+ }
+ return _values;
+ }
+ }
+
+ public string Text
+ {
+ get { return _text ?? String.Empty; }
+ set { _text = value; }
+ }
+
+ protected override void Render(HtmlTextWriter writer)
+ {
+ RouteValueDictionary routeValues = new RouteValueDictionary();
+ foreach (var attribute in Values.Attributes)
+ {
+ routeValues.Add(attribute.Key, attribute.Value);
+ }
+
+ if (!String.IsNullOrEmpty(ActionName) && !routeValues.ContainsKey("action"))
+ {
+ routeValues.Add("action", ActionName);
+ }
+ if (!String.IsNullOrEmpty(ControllerName) && !routeValues.ContainsKey("controller"))
+ {
+ routeValues.Add("controller", ControllerName);
+ }
+
+ string href = null;
+ if (DesignMode)
+ {
+ href = "/";
+ }
+ else
+ {
+ VirtualPathData vpd = RouteTable.Routes.GetVirtualPathForArea(ViewContext.RequestContext, RouteName, routeValues);
+ if (vpd == null)
+ {
+ throw new InvalidOperationException("A route that matches the requested values could not be located in the route table.");
+ }
+ href = vpd.VirtualPath;
+ }
+
+ foreach (var attribute in Attributes)
+ {
+ writer.AddAttribute(attribute.Key, attribute.Value);
+ }
+
+ if (!Attributes.ContainsKey("href"))
+ {
+ writer.AddAttribute(HtmlTextWriterAttribute.Href, href);
+ }
+
+ writer.RenderBeginTag(HtmlTextWriterTag.A);
+
+ writer.WriteEncodedText(Text);
+
+ writer.RenderEndTag();
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Controls/DropDownList.cs b/src/Microsoft.Web.Mvc/Controls/DropDownList.cs
new file mode 100644
index 00000000..2bc19e28
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Controls/DropDownList.cs
@@ -0,0 +1,205 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Globalization;
+using System.Linq;
+using System.Web.Mvc;
+using System.Web.UI;
+using Microsoft.Web.Mvc.Properties;
+
+namespace Microsoft.Web.Mvc.Controls
+{
+ // TODO: Have ListBoxBase class to use with DropDownList and ListBox?
+ // TODO: Do we need a way to explicitly specify the items? And only get the selected value(s) from ViewData?
+
+ public class DropDownList : MvcControl
+ {
+ private string _name;
+ private string _optionLabel;
+
+ [DefaultValue("")]
+ public string Name
+ {
+ get { return _name ?? String.Empty; }
+ set { _name = value; }
+ }
+
+ [DefaultValue("")]
+ public string OptionLabel
+ {
+ get { return _optionLabel ?? String.Empty; }
+ set { _optionLabel = value; }
+ }
+
+ private object GetModelStateValue(string key, Type destinationType)
+ {
+ ModelState modelState;
+ if (ViewData.ModelState.TryGetValue(key, out modelState))
+ {
+ return modelState.Value.ConvertTo(destinationType, null /* culture */);
+ }
+ return null;
+ }
+
+ private IEnumerable<SelectListItem> GetSelectData(string name)
+ {
+ object o = null;
+ if (ViewData != null)
+ {
+ o = ViewData.Eval(name);
+ }
+ if (o == null)
+ {
+ throw new InvalidOperationException(
+ String.Format(
+ CultureInfo.CurrentCulture,
+ MvcResources.HtmlHelper_MissingSelectData,
+ name,
+ "IEnumerable<SelectListItem>"));
+ }
+ IEnumerable<SelectListItem> selectList = o as IEnumerable<SelectListItem>;
+ if (selectList == null)
+ {
+ throw new InvalidOperationException(
+ String.Format(
+ CultureInfo.CurrentCulture,
+ MvcResources.HtmlHelper_WrongSelectDataType,
+ name,
+ o.GetType().FullName,
+ "IEnumerable<SelectListItem>"));
+ }
+ return selectList;
+ }
+
+ protected override void Render(HtmlTextWriter writer)
+ {
+ if (!DesignMode && String.IsNullOrEmpty(Name))
+ {
+ throw new InvalidOperationException(MvcResources.CommonControls_NameRequired);
+ }
+
+ if (DesignMode)
+ {
+ RenderDesignMode(writer);
+ }
+ else
+ {
+ RenderRuntime(writer);
+ }
+ }
+
+ private void RenderDesignMode(HtmlTextWriter writer)
+ {
+ writer.RenderBeginTag(HtmlTextWriterTag.Select);
+ writer.RenderBeginTag(HtmlTextWriterTag.Option);
+ if (String.IsNullOrEmpty(OptionLabel))
+ {
+ writer.WriteEncodedText(MvcResources.DropDownList_SampleItem);
+ }
+ else
+ {
+ writer.WriteEncodedText(OptionLabel);
+ }
+ writer.RenderEndTag();
+ writer.RenderEndTag();
+ }
+
+ private void RenderRuntime(HtmlTextWriter writer)
+ {
+ // TODO: Move this to the base class once it exists
+ bool allowMultiple = false;
+
+ SortedDictionary<string, string> attrs = new SortedDictionary<string, string>();
+
+ foreach (KeyValuePair<string, string> attribute in Attributes)
+ {
+ attrs.Add(attribute.Key, attribute.Value);
+ }
+
+ attrs.Add("name", Name);
+ if (!String.IsNullOrEmpty(ID))
+ {
+ attrs.Add("id", ID);
+ }
+ if (allowMultiple)
+ {
+ attrs.Add("multiple", "multiple");
+ }
+
+ // If there are any errors for a named field, we add the css attribute.
+ ModelState modelState;
+ if (ViewData.ModelState.TryGetValue(Name, out modelState))
+ {
+ if (modelState.Errors.Count > 0)
+ {
+ string currentValue;
+
+ if (attrs.TryGetValue("class", out currentValue))
+ {
+ attrs["class"] = HtmlHelper.ValidationInputCssClassName + " " + currentValue;
+ }
+ else
+ {
+ attrs["class"] = HtmlHelper.ValidationInputCssClassName;
+ }
+ }
+ }
+
+ foreach (KeyValuePair<string, string> attribute in attrs)
+ {
+ writer.AddAttribute(attribute.Key, Convert.ToString(attribute.Value, CultureInfo.CurrentCulture));
+ }
+
+ writer.RenderBeginTag(HtmlTextWriterTag.Select);
+
+ // Use ViewData to get the list of items
+ IEnumerable<SelectListItem> selectList = GetSelectData(Name);
+
+ object defaultValue = (allowMultiple) ? GetModelStateValue(Name, typeof(string[])) : GetModelStateValue(Name, typeof(string));
+
+ if (defaultValue != null)
+ {
+ IEnumerable defaultValues = (allowMultiple) ? defaultValue as IEnumerable : new[] { defaultValue };
+ IEnumerable<string> values = from object value in defaultValues
+ select Convert.ToString(value, CultureInfo.CurrentCulture);
+ HashSet<string> selectedValues = new HashSet<string>(values, StringComparer.OrdinalIgnoreCase);
+ List<SelectListItem> newSelectList = new List<SelectListItem>();
+
+ foreach (SelectListItem item in selectList)
+ {
+ item.Selected = (item.Value != null) ? selectedValues.Contains(item.Value) : selectedValues.Contains(item.Text);
+ newSelectList.Add(item);
+ }
+ selectList = newSelectList;
+ }
+
+ // Render the option label if it exists
+ if (!String.IsNullOrEmpty(OptionLabel))
+ {
+ writer.AddAttribute(HtmlTextWriterAttribute.Value, String.Empty);
+ writer.RenderBeginTag(HtmlTextWriterTag.Option);
+ writer.WriteEncodedText(OptionLabel);
+ writer.RenderEndTag();
+ }
+
+ // Render out the list items
+ foreach (SelectListItem listItem in selectList)
+ {
+ if (listItem.Value != null)
+ {
+ writer.AddAttribute(HtmlTextWriterAttribute.Value, listItem.Value);
+ }
+ if (listItem.Selected)
+ {
+ writer.AddAttribute(HtmlTextWriterAttribute.Selected, "selected");
+ }
+ writer.RenderBeginTag(HtmlTextWriterTag.Option);
+ writer.WriteEncodedText(listItem.Text);
+ writer.RenderEndTag();
+ }
+
+ writer.RenderEndTag();
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Controls/EncodeType.cs b/src/Microsoft.Web.Mvc/Controls/EncodeType.cs
new file mode 100644
index 00000000..fd4c67c4
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Controls/EncodeType.cs
@@ -0,0 +1,9 @@
+namespace Microsoft.Web.Mvc.Controls
+{
+ public enum EncodeType
+ {
+ Html,
+ HtmlAttribute,
+ None,
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Controls/Hidden.cs b/src/Microsoft.Web.Mvc/Controls/Hidden.cs
new file mode 100644
index 00000000..e10e83b5
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Controls/Hidden.cs
@@ -0,0 +1,10 @@
+namespace Microsoft.Web.Mvc.Controls
+{
+ public class Hidden : MvcInputControl
+ {
+ public Hidden()
+ : base("hidden")
+ {
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Controls/Label.cs b/src/Microsoft.Web.Mvc/Controls/Label.cs
new file mode 100644
index 00000000..49cd3d1c
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Controls/Label.cs
@@ -0,0 +1,111 @@
+using System;
+using System.ComponentModel;
+using System.Globalization;
+using System.Web;
+using System.Web.UI;
+using Microsoft.Web.Mvc.Properties;
+
+namespace Microsoft.Web.Mvc.Controls
+{
+ public class Label : MvcControl
+ {
+ private string _format;
+ private string _name;
+ private int _truncateLength = -1;
+ private string _truncateText = "...";
+
+ [DefaultValue(EncodeType.Html)]
+ public EncodeType EncodeType { get; set; }
+
+ [DefaultValue("")]
+ public string Format
+ {
+ get { return _format ?? String.Empty; }
+ set { _format = value; }
+ }
+
+ [DefaultValue("")]
+ public string Name
+ {
+ get { return _name ?? String.Empty; }
+ set { _name = value; }
+ }
+
+ [DefaultValue(-1)]
+ [Description("The length of the text at which to truncate the value. Set to -1 to never truncate.")]
+ public int TruncateLength
+ {
+ get { return _truncateLength; }
+ set
+ {
+ if (value < -1)
+ {
+ throw new ArgumentOutOfRangeException("value", "The TruncateLength property must be greater than or equal to -1.");
+ }
+ _truncateLength = value;
+ }
+ }
+
+ [DefaultValue("...")]
+ [Description("The text to display at the end of the string if it is truncated. This text is never encoded.")]
+ public string TruncateText
+ {
+ get { return _truncateText ?? String.Empty; }
+ set { _truncateText = value; }
+ }
+
+ protected override void Render(HtmlTextWriter writer)
+ {
+ if (!DesignMode && String.IsNullOrEmpty(Name))
+ {
+ throw new InvalidOperationException(MvcResources.CommonControls_NameRequired);
+ }
+
+ string stringValue = String.Empty;
+ if (ViewData != null)
+ {
+ object rawValue = ViewData.Eval(Name);
+
+ if (String.IsNullOrEmpty(Format))
+ {
+ stringValue = Convert.ToString(rawValue, CultureInfo.CurrentCulture);
+ }
+ else
+ {
+ stringValue = String.Format(CultureInfo.CurrentCulture, Format, rawValue);
+ }
+ }
+
+ writer.AddAttribute(HtmlTextWriterAttribute.Name, Name);
+ if (!String.IsNullOrEmpty(ID))
+ {
+ writer.AddAttribute(HtmlTextWriterAttribute.Id, ID);
+ }
+
+ bool wasTruncated = false;
+ if ((TruncateLength >= 0) && (stringValue.Length > TruncateLength))
+ {
+ stringValue = stringValue.Substring(0, TruncateLength);
+ wasTruncated = true;
+ }
+
+ switch (EncodeType)
+ {
+ case EncodeType.Html:
+ writer.Write(HttpUtility.HtmlEncode(stringValue));
+ break;
+ case EncodeType.HtmlAttribute:
+ writer.Write(HttpUtility.HtmlAttributeEncode(stringValue));
+ break;
+ case EncodeType.None:
+ writer.Write(stringValue);
+ break;
+ }
+
+ if (wasTruncated)
+ {
+ writer.Write(TruncateText);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Controls/MvcControl.cs b/src/Microsoft.Web.Mvc/Controls/MvcControl.cs
new file mode 100644
index 00000000..056ffdeb
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Controls/MvcControl.cs
@@ -0,0 +1,124 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Web.Mvc;
+using System.Web.UI;
+
+namespace Microsoft.Web.Mvc.Controls
+{
+ // TODO: Consider using custom HTML writer instead of the default one to get prettier rendering
+
+ public abstract class MvcControl : Control, IAttributeAccessor
+ {
+ private IDictionary<string, string> _attributes;
+ private IViewDataContainer _viewDataContainer;
+ private ViewContext _viewContext;
+
+ [Browsable(false)]
+ public IDictionary<string, string> Attributes
+ {
+ get
+ {
+ EnsureAttributes();
+ return _attributes;
+ }
+ }
+
+ [Browsable(false)]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public override bool EnableViewState
+ {
+ get { return base.EnableViewState; }
+ set { base.EnableViewState = value; }
+ }
+
+ public ViewContext ViewContext
+ {
+ get
+ {
+ if (_viewContext == null)
+ {
+ // TODO: Is this logic correct? Why not just case Page to ViewPage?
+ Control parent = Parent;
+ while (parent != null)
+ {
+ ViewPage viewPage = parent as ViewPage;
+ if (viewPage != null)
+ {
+ _viewContext = viewPage.ViewContext;
+ break;
+ }
+ parent = parent.Parent;
+ }
+ }
+ return _viewContext;
+ }
+ }
+
+ public IViewDataContainer ViewDataContainer
+ {
+ get
+ {
+ if (_viewDataContainer == null)
+ {
+ Control parent = Parent;
+ while (parent != null)
+ {
+ _viewDataContainer = parent as IViewDataContainer;
+ if (_viewDataContainer != null)
+ {
+ break;
+ }
+ parent = parent.Parent;
+ }
+ }
+ return _viewDataContainer;
+ }
+ }
+
+ public ViewDataDictionary ViewData
+ {
+ get
+ {
+ IViewDataContainer vdc = ViewDataContainer;
+ return (vdc == null) ? null : vdc.ViewData;
+ }
+ }
+
+ private void EnsureAttributes()
+ {
+ if (_attributes == null)
+ {
+ _attributes = new SortedDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ }
+ }
+
+ protected virtual string GetAttribute(string key)
+ {
+ EnsureAttributes();
+ string value;
+ _attributes.TryGetValue(key, out value);
+ return value;
+ }
+
+ protected virtual void SetAttribute(string key, string value)
+ {
+ EnsureAttributes();
+ _attributes[key] = value;
+ }
+
+ #region IAttributeAccessor Members
+
+ string IAttributeAccessor.GetAttribute(string key)
+ {
+ return GetAttribute(key);
+ }
+
+ void IAttributeAccessor.SetAttribute(string key, string value)
+ {
+ SetAttribute(key, value);
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Controls/MvcInputControl.cs b/src/Microsoft.Web.Mvc/Controls/MvcInputControl.cs
new file mode 100644
index 00000000..75508ac1
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Controls/MvcInputControl.cs
@@ -0,0 +1,143 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Globalization;
+using System.Web.Mvc;
+using System.Web.UI;
+using Microsoft.Web.Mvc.Properties;
+
+namespace Microsoft.Web.Mvc.Controls
+{
+ public abstract class MvcInputControl : MvcControl
+ {
+ private string _format;
+ private string _name;
+
+ protected MvcInputControl(string inputType)
+ {
+ if (String.IsNullOrEmpty(inputType))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "inputType");
+ }
+ InputType = inputType;
+ }
+
+ [DefaultValue("")]
+ public string Format
+ {
+ get { return _format ?? String.Empty; }
+ set { _format = value; }
+ }
+
+ [Browsable(false)]
+ public string InputType { get; private set; }
+
+ [DefaultValue("")]
+ public string Name
+ {
+ get { return _name ?? String.Empty; }
+ set { _name = value; }
+ }
+
+ private ModelState GetModelState()
+ {
+ return ViewData.ModelState[Name];
+ }
+
+ private object GetModelStateValue(Type destinationType)
+ {
+ ModelState modelState = GetModelState();
+ if (modelState != null)
+ {
+ return modelState.Value.ConvertTo(destinationType, null /* culture */);
+ }
+ return null;
+ }
+
+ protected override void Render(HtmlTextWriter writer)
+ {
+ if (!DesignMode && String.IsNullOrEmpty(Name))
+ {
+ throw new InvalidOperationException(MvcResources.CommonControls_NameRequired);
+ }
+
+ SortedDictionary<string, string> attrs = new SortedDictionary<string, string>();
+
+ foreach (KeyValuePair<string, string> attribute in Attributes)
+ {
+ attrs.Add(attribute.Key, attribute.Value);
+ }
+
+ if (!Attributes.ContainsKey("type"))
+ {
+ attrs.Add("type", InputType);
+ }
+ attrs.Add("name", Name);
+ if (!String.IsNullOrEmpty(ID))
+ {
+ attrs.Add("id", ID);
+ }
+
+ if (DesignMode)
+ {
+ // Use a dummy value in design mode
+ attrs.Add("value", "TextBox");
+ }
+ else
+ {
+ string attemptedValue = (string)GetModelStateValue(typeof(string));
+
+ if (attemptedValue != null)
+ {
+ // Never format the attempted value since it was already formatted in the previous request
+ attrs.Add("value", attemptedValue);
+ }
+ else
+ {
+ // Use an explicit value attribute if it is available. Otherwise get it from ViewData.
+ string attributeValue;
+ Attributes.TryGetValue("value", out attributeValue);
+ object rawValue = attributeValue ?? ViewData.Eval(Name);
+ string stringValue;
+
+ if (String.IsNullOrEmpty(Format))
+ {
+ stringValue = Convert.ToString(rawValue, CultureInfo.CurrentCulture);
+ }
+ else
+ {
+ stringValue = String.Format(CultureInfo.CurrentCulture, Format, rawValue);
+ }
+
+ // The HtmlTextWriter will automatically encode this value
+ attrs.Add("value", stringValue);
+ }
+
+ // If there are any errors for a named field, we add the CSS attribute.
+ ModelState modelState = GetModelState();
+ if ((modelState != null) && (modelState.Errors.Count > 0))
+ {
+ string currentValue;
+
+ if (attrs.TryGetValue("class", out currentValue))
+ {
+ attrs["class"] = HtmlHelper.ValidationInputCssClassName + " " + currentValue;
+ }
+ else
+ {
+ attrs["class"] = HtmlHelper.ValidationInputCssClassName;
+ }
+ }
+ }
+
+ foreach (KeyValuePair<string, string> attribute in attrs)
+ {
+ writer.AddAttribute(attribute.Key, Convert.ToString(attribute.Value, CultureInfo.CurrentCulture));
+ }
+
+ writer.RenderBeginTag(HtmlTextWriterTag.Input);
+
+ writer.RenderEndTag();
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Controls/Password.cs b/src/Microsoft.Web.Mvc/Controls/Password.cs
new file mode 100644
index 00000000..bb36edbc
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Controls/Password.cs
@@ -0,0 +1,10 @@
+namespace Microsoft.Web.Mvc.Controls
+{
+ public class Password : MvcInputControl
+ {
+ public Password()
+ : base("password")
+ {
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Controls/Repeater.cs b/src/Microsoft.Web.Mvc/Controls/Repeater.cs
new file mode 100644
index 00000000..7d68d1e4
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Controls/Repeater.cs
@@ -0,0 +1,77 @@
+using System;
+using System.Collections;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Web.Mvc;
+using System.Web.UI;
+
+namespace Microsoft.Web.Mvc.Controls
+{
+ [ParseChildren(true)]
+ [PersistChildren(false)]
+ public class Repeater : MvcControl
+ {
+ private string _name;
+
+ [DefaultValue(null)]
+ [Browsable(false)]
+ [PersistenceMode(PersistenceMode.InnerProperty)]
+ [TemplateContainer(typeof(RepeaterItem))]
+ [TemplateInstance(TemplateInstance.Multiple)]
+ public ITemplate ItemTemplate { get; set; }
+
+ [DefaultValue(null)]
+ [Browsable(false)]
+ [PersistenceMode(PersistenceMode.InnerProperty)]
+ [TemplateContainer(typeof(RepeaterItem))]
+ [TemplateInstance(TemplateInstance.Single)]
+ public ITemplate EmptyDataTemplate { get; set; }
+
+ [DefaultValue("")]
+ public string Name
+ {
+ get { return _name ?? String.Empty; }
+ set { _name = value; }
+ }
+
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "The child objects are disposed when their container is disposed")]
+ protected override void OnPreRender(EventArgs e)
+ {
+ base.OnPreRender(e);
+
+ // Dummy control to which we parent all the data item controls
+ Control containerControl = new Control();
+
+ IEnumerable dataItems = ViewData.Eval(Name) as IEnumerable;
+ bool hasData = false;
+ if (dataItems != null)
+ {
+ int index = 0;
+ foreach (object dataItem in dataItems)
+ {
+ hasData = true;
+ RepeaterItem repeaterItem = new RepeaterItem(index, dataItem)
+ {
+ ViewData = new ViewDataDictionary(dataItem),
+ };
+ ItemTemplate.InstantiateIn(repeaterItem);
+ containerControl.Controls.Add(repeaterItem);
+
+ index++;
+ }
+ }
+
+ if (!hasData)
+ {
+ // If there was no data, instantiate the EmptyDataTemplate
+ Control emptyDataContainer = new Control();
+ EmptyDataTemplate.InstantiateIn(emptyDataContainer);
+ containerControl.Controls.Add(emptyDataContainer);
+ }
+
+ Controls.Add(containerControl);
+
+ containerControl.DataBind();
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Controls/RepeaterItem.cs b/src/Microsoft.Web.Mvc/Controls/RepeaterItem.cs
new file mode 100644
index 00000000..ec894cfc
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Controls/RepeaterItem.cs
@@ -0,0 +1,36 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Web.Mvc;
+using System.Web.UI;
+
+namespace Microsoft.Web.Mvc.Controls
+{
+ public class RepeaterItem : Control, IDataItemContainer, IViewDataContainer
+ {
+ private object _dataItem;
+ private int _itemIndex;
+
+ public RepeaterItem(int itemIndex, object dataItem)
+ {
+ _itemIndex = itemIndex;
+ _dataItem = dataItem;
+ }
+
+ public object DataItem
+ {
+ get { return _dataItem; }
+ }
+
+ public int DataItemIndex
+ {
+ get { return _itemIndex; }
+ }
+
+ public int DisplayIndex
+ {
+ get { return _itemIndex; }
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This is intended to be settable for unit testing purposes.")]
+ public ViewDataDictionary ViewData { get; set; }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Controls/RouteValues.cs b/src/Microsoft.Web.Mvc/Controls/RouteValues.cs
new file mode 100644
index 00000000..c3a59632
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Controls/RouteValues.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Collections.Generic;
+using System.Web.UI;
+
+namespace Microsoft.Web.Mvc.Controls
+{
+ public class RouteValues : IAttributeAccessor
+ {
+ private IDictionary<string, string> _attributes;
+
+ public IDictionary<string, string> Attributes
+ {
+ get
+ {
+ EnsureAttributes();
+ return _attributes;
+ }
+ }
+
+ private void EnsureAttributes()
+ {
+ if (_attributes == null)
+ {
+ _attributes = new SortedDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ }
+ }
+
+ protected virtual string GetAttribute(string key)
+ {
+ EnsureAttributes();
+ string value;
+ _attributes.TryGetValue(key, out value);
+ return value;
+ }
+
+ protected virtual void SetAttribute(string key, string value)
+ {
+ EnsureAttributes();
+ _attributes[key] = value;
+ }
+
+ #region IAttributeAccessor Members
+
+ string IAttributeAccessor.GetAttribute(string key)
+ {
+ return GetAttribute(key);
+ }
+
+ void IAttributeAccessor.SetAttribute(string key, string value)
+ {
+ SetAttribute(key, value);
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Controls/TextBox.cs b/src/Microsoft.Web.Mvc/Controls/TextBox.cs
new file mode 100644
index 00000000..af4ab52d
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Controls/TextBox.cs
@@ -0,0 +1,10 @@
+namespace Microsoft.Web.Mvc.Controls
+{
+ public class TextBox : MvcInputControl
+ {
+ public TextBox()
+ : base("text")
+ {
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/CookieTempDataProvider.cs b/src/Microsoft.Web.Mvc/CookieTempDataProvider.cs
new file mode 100644
index 00000000..59705404
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/CookieTempDataProvider.cs
@@ -0,0 +1,99 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Runtime.Serialization.Formatters.Binary;
+using System.Web;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc
+{
+ public class CookieTempDataProvider : ITempDataProvider
+ {
+ internal const string TempDataCookieKey = "__ControllerTempData";
+ private HttpContextBase _httpContext;
+
+ public CookieTempDataProvider(HttpContextBase httpContext)
+ {
+ if (httpContext == null)
+ {
+ throw new ArgumentNullException("httpContext");
+ }
+ _httpContext = httpContext;
+ }
+
+ public HttpContextBase HttpContext
+ {
+ get { return _httpContext; }
+ }
+
+ protected virtual IDictionary<string, object> LoadTempData(ControllerContext controllerContext)
+ {
+ HttpCookie cookie = _httpContext.Request.Cookies[TempDataCookieKey];
+ if (cookie != null && !String.IsNullOrEmpty(cookie.Value))
+ {
+ IDictionary<string, object> deserializedDictionary = Base64StringToDictionary(cookie.Value);
+
+ cookie.Expires = DateTime.MinValue;
+ cookie.Value = String.Empty;
+
+ if (_httpContext.Response != null && _httpContext.Response.Cookies != null)
+ {
+ HttpCookie responseCookie = _httpContext.Response.Cookies[TempDataCookieKey];
+ if (responseCookie != null)
+ {
+ cookie.Expires = DateTime.MinValue;
+ cookie.Value = String.Empty;
+ }
+ }
+
+ return deserializedDictionary;
+ }
+
+ return new Dictionary<string, object>();
+ }
+
+ protected virtual void SaveTempData(ControllerContext controllerContext, IDictionary<string, object> values)
+ {
+ string cookieValue = DictionaryToBase64String(values);
+
+ var cookie = new HttpCookie(TempDataCookieKey);
+ cookie.HttpOnly = true;
+ cookie.Value = cookieValue;
+
+ _httpContext.Response.Cookies.Add(cookie);
+ }
+
+ public static IDictionary<string, object> Base64StringToDictionary(string base64EncodedSerializedTempData)
+ {
+ byte[] bytes = Convert.FromBase64String(base64EncodedSerializedTempData);
+ using (var memStream = new MemoryStream(bytes))
+ {
+ var binFormatter = new BinaryFormatter();
+ return binFormatter.Deserialize(memStream, null) as IDictionary<string, object>;
+ }
+ }
+
+ public static string DictionaryToBase64String(IDictionary<string, object> values)
+ {
+ using (MemoryStream memStream = new MemoryStream())
+ {
+ memStream.Seek(0, SeekOrigin.Begin);
+ var binFormatter = new BinaryFormatter();
+ binFormatter.Serialize(memStream, values);
+ memStream.Seek(0, SeekOrigin.Begin);
+ byte[] bytes = memStream.ToArray();
+ return Convert.ToBase64String(bytes);
+ }
+ }
+
+ IDictionary<string, object> ITempDataProvider.LoadTempData(ControllerContext controllerContext)
+ {
+ return LoadTempData(controllerContext);
+ }
+
+ void ITempDataProvider.SaveTempData(ControllerContext controllerContext, IDictionary<string, object> values)
+ {
+ SaveTempData(controllerContext, values);
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/CookieValueProviderFactory.cs b/src/Microsoft.Web.Mvc/CookieValueProviderFactory.cs
new file mode 100644
index 00000000..b3fe056d
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/CookieValueProviderFactory.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Web;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc
+{
+ public class CookieValueProviderFactory : ValueProviderFactory
+ {
+ public override IValueProvider GetValueProvider(ControllerContext controllerContext)
+ {
+ HttpCookieCollection cookies = controllerContext.HttpContext.Request.Cookies;
+
+ Dictionary<string, string> backingStore = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ for (int i = 0; i < cookies.Count; i++)
+ {
+ HttpCookie cookie = cookies[i];
+ if (!String.IsNullOrEmpty(cookie.Name))
+ {
+ backingStore[cookie.Name] = cookie.Value;
+ }
+ }
+
+ return new DictionaryValueProvider<string>(backingStore, CultureInfo.InvariantCulture);
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/CopyAsyncParametersAttribute.cs b/src/Microsoft.Web.Mvc/CopyAsyncParametersAttribute.cs
new file mode 100644
index 00000000..f6d68dbe
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/CopyAsyncParametersAttribute.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Web.Mvc;
+using System.Web.Mvc.Async;
+
+namespace Microsoft.Web.Mvc
+{
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
+ public sealed class CopyAsyncParametersAttribute : ActionFilterAttribute
+ {
+ public override void OnActionExecuting(ActionExecutingContext filterContext)
+ {
+ if (filterContext == null)
+ {
+ throw new ArgumentNullException("filterContext");
+ }
+
+ IAsyncManagerContainer container = filterContext.Controller as IAsyncManagerContainer;
+ if (container != null)
+ {
+ AsyncManager asyncManager = container.AsyncManager;
+ foreach (var entry in filterContext.ActionParameters)
+ {
+ asyncManager.Parameters[entry.Key] = entry.Value;
+ }
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/CreditCardAttribute.cs b/src/Microsoft.Web.Mvc/CreditCardAttribute.cs
new file mode 100644
index 00000000..15807788
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/CreditCardAttribute.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Web.Mvc;
+using Microsoft.Web.Mvc.Properties;
+
+namespace Microsoft.Web.Mvc
+{
+ [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
+ public sealed class CreditCardAttribute : DataTypeAttribute, IClientValidatable
+ {
+ public CreditCardAttribute()
+ : base("creditcard")
+ {
+ ErrorMessage = MvcResources.CreditCardAttribute_Invalid;
+ }
+
+ public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
+ {
+ yield return new ModelClientValidationRule
+ {
+ ValidationType = "creditcard",
+ ErrorMessage = FormatErrorMessage(metadata.GetDisplayName())
+ };
+ }
+
+ public override bool IsValid(object value)
+ {
+ if (value == null)
+ {
+ return true;
+ }
+
+ string creditCardNumber = value as string;
+ if (creditCardNumber == null)
+ {
+ return false;
+ }
+ creditCardNumber = creditCardNumber.Replace("-", String.Empty);
+
+ int checksum = 0;
+ bool evenDigit = false;
+
+ // http://www.beachnet.com/~hstiles/cardtype.html
+ foreach (char digit in creditCardNumber.Reverse())
+ {
+ if (!Char.IsDigit(digit))
+ {
+ return false;
+ }
+
+ int digitValue = (digit - '0') * (evenDigit ? 2 : 1);
+ evenDigit = !evenDigit;
+
+ while (digitValue > 0)
+ {
+ checksum += digitValue % 10;
+ digitValue /= 10;
+ }
+ }
+
+ return (checksum % 10) == 0;
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/CssExtensions.cs b/src/Microsoft.Web.Mvc/CssExtensions.cs
new file mode 100644
index 00000000..ec336697
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/CssExtensions.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Web.Mvc;
+using Microsoft.Web.Mvc.Properties;
+
+namespace Microsoft.Web.Mvc
+{
+ public static class CssExtensions
+ {
+ public static MvcHtmlString Css(this HtmlHelper helper, string file)
+ {
+ return Css(helper, file, null);
+ }
+
+ public static MvcHtmlString Css(this HtmlHelper helper, string file, string mediaType)
+ {
+ if (String.IsNullOrEmpty(file))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "file");
+ }
+
+ string src;
+ if (ScriptExtensions.IsRelativeToDefaultPath(file))
+ {
+ src = "~/Content/" + file;
+ }
+ else
+ {
+ src = file;
+ }
+
+ TagBuilder linkTag = new TagBuilder("link");
+ linkTag.MergeAttribute("type", "text/css");
+ linkTag.MergeAttribute("rel", "stylesheet");
+ if (mediaType != null)
+ {
+ linkTag.MergeAttribute("media", mediaType);
+ }
+ linkTag.MergeAttribute("href", UrlHelper.GenerateContentUrl(src, helper.ViewContext.HttpContext));
+ return MvcHtmlString.Create(linkTag.ToString(TagRenderMode.SelfClosing));
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/DeserializeAttribute.cs b/src/Microsoft.Web.Mvc/DeserializeAttribute.cs
new file mode 100644
index 00000000..51c7cba4
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/DeserializeAttribute.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc
+{
+ [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
+ public sealed class DeserializeAttribute : CustomModelBinderAttribute
+ {
+ public DeserializeAttribute()
+ : this(MvcSerializer.DefaultSerializationMode)
+ {
+ }
+
+ public DeserializeAttribute(SerializationMode mode)
+ {
+ Mode = mode;
+ }
+
+ public SerializationMode Mode { get; private set; }
+
+ internal MvcSerializer Serializer { get; set; }
+
+ public override IModelBinder GetBinder()
+ {
+ return new DeserializingModelBinder(Mode, Serializer);
+ }
+
+ private sealed class DeserializingModelBinder : IModelBinder
+ {
+ private readonly SerializationMode _mode;
+ private readonly MvcSerializer _serializer;
+
+ public DeserializingModelBinder(SerializationMode mode, MvcSerializer serializer)
+ {
+ _mode = mode;
+ _serializer = serializer ?? new MvcSerializer();
+ }
+
+ [SuppressMessage("Microsoft.Globalization", "CA1304:SpecifyCultureInfo", MessageId = "System.Web.Mvc.ValueProviderResult.ConvertTo(System.Type)", Justification = "The target object should make the correct culture determination, not this method.")]
+ public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
+ {
+ if (bindingContext == null)
+ {
+ throw new ArgumentNullException("bindingContext");
+ }
+
+ ValueProviderResult valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
+ if (valueProviderResult == null)
+ {
+ // nothing found
+ return null;
+ }
+
+ string serializedValue = (string)valueProviderResult.ConvertTo(typeof(string));
+ return _serializer.Deserialize(serializedValue, _mode);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/DynamicReflectionObject.cs b/src/Microsoft.Web.Mvc/DynamicReflectionObject.cs
new file mode 100644
index 00000000..511f66b2
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/DynamicReflectionObject.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Dynamic;
+using System.Globalization;
+using System.Linq;
+using System.Reflection;
+using Microsoft.Web.Mvc.Properties;
+
+namespace Microsoft.Web.Mvc
+{
+ internal class DynamicReflectionObject : DynamicObject
+ {
+ private readonly object _realObject;
+
+ private DynamicReflectionObject(object realObject)
+ {
+ _realObject = realObject;
+ }
+
+ public override bool TryGetMember(GetMemberBinder binder, out object result)
+ {
+ PropertyInfo propInfo = _realObject.GetType().GetProperty(binder.Name);
+
+ if (propInfo == null)
+ {
+ PropertyInfo[] properties = _realObject.GetType().GetProperties();
+ if (properties.Length == 0)
+ {
+ throw new InvalidOperationException(
+ String.Format(CultureInfo.CurrentCulture,
+ MvcResources.DynamicViewPage_NoProperties,
+ binder.Name));
+ }
+
+ string propNames = properties.Select(p => p.Name)
+ .OrderBy(name => name)
+ .Aggregate((left, right) => left + ", " + right);
+
+ throw new InvalidOperationException(
+ String.Format(CultureInfo.CurrentCulture,
+ MvcResources.DynamicViewPage_PropertyDoesNotExist,
+ binder.Name,
+ propNames));
+ }
+
+ result = Wrap(propInfo.GetValue(_realObject, null));
+ return true;
+ }
+
+ public static dynamic Wrap(object obj)
+ {
+ // We really only want to wrap anonymous objects, but there's no surefire way to determine
+ // that an object is anonymous. We'll use the best metrics we can (internal non-nested type
+ // that derives directly from Object).
+ if (obj != null)
+ {
+ Type type = obj.GetType();
+ if (!type.IsPublic && type.BaseType == typeof(Object) && type.DeclaringType == null)
+ {
+ return new DynamicReflectionObject(obj);
+ }
+ }
+
+ return obj;
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/DynamicViewDataDictionary.cs b/src/Microsoft.Web.Mvc/DynamicViewDataDictionary.cs
new file mode 100644
index 00000000..8fde3ab2
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/DynamicViewDataDictionary.cs
@@ -0,0 +1,72 @@
+using System;
+using System.Dynamic;
+using System.Web.Mvc;
+using Microsoft.Web.Mvc.Properties;
+
+namespace Microsoft.Web.Mvc
+{
+ internal class DynamicViewDataDictionary : DynamicObject
+ {
+ private readonly ViewDataDictionary _dictionary;
+
+ private DynamicViewDataDictionary(ViewDataDictionary dictionary)
+ {
+ _dictionary = dictionary;
+ }
+
+ public object Model
+ {
+ get { return _dictionary.Model; }
+ set { _dictionary.Model = value; }
+ }
+
+ public ModelMetadata ModelMetadata
+ {
+ get { return _dictionary.ModelMetadata; }
+ set { _dictionary.ModelMetadata = value; }
+ }
+
+ public ModelStateDictionary ModelState
+ {
+ get { return _dictionary.ModelState; }
+ }
+
+ public TemplateInfo TemplateInfo
+ {
+ get { return _dictionary.TemplateInfo; }
+ set { _dictionary.TemplateInfo = value; }
+ }
+
+ private bool GetValue(string name, out object result)
+ {
+ result = DynamicReflectionObject.Wrap(_dictionary.Eval(name)) ?? String.Empty;
+ return true;
+ }
+
+ public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result)
+ {
+ if (indexes.Length != 1)
+ {
+ throw new ArgumentException(MvcResources.DynamicViewDataDictionary_SingleIndexerOnly);
+ }
+
+ string name = indexes[0] as string;
+ if (name == null)
+ {
+ throw new ArgumentException(MvcResources.DynamicViewDataDictionary_StringIndexerOnly);
+ }
+
+ return GetValue(name, out result);
+ }
+
+ public override bool TryGetMember(GetMemberBinder binder, out object result)
+ {
+ return GetValue(binder.Name, out result);
+ }
+
+ public static dynamic Wrap(ViewDataDictionary dictionary)
+ {
+ return new DynamicViewDataDictionary(dictionary);
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/DynamicViewPage.cs b/src/Microsoft.Web.Mvc/DynamicViewPage.cs
new file mode 100644
index 00000000..b24716ef
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/DynamicViewPage.cs
@@ -0,0 +1,17 @@
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc
+{
+ public class DynamicViewPage : ViewPage
+ {
+ public new dynamic Model
+ {
+ get { return DynamicReflectionObject.Wrap(base.Model); }
+ }
+
+ public new dynamic ViewData
+ {
+ get { return DynamicViewDataDictionary.Wrap(base.ViewData); }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/DynamicViewPage`1.cs b/src/Microsoft.Web.Mvc/DynamicViewPage`1.cs
new file mode 100644
index 00000000..4ca52273
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/DynamicViewPage`1.cs
@@ -0,0 +1,12 @@
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc
+{
+ public class DynamicViewPage<TModel> : ViewPage<TModel>
+ {
+ public new dynamic ViewData
+ {
+ get { return DynamicViewDataDictionary.Wrap(base.ViewData); }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ElementalValueProvider.cs b/src/Microsoft.Web.Mvc/ElementalValueProvider.cs
new file mode 100644
index 00000000..45f7df40
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ElementalValueProvider.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Globalization;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc
+{
+ // Represents a value provider that contains a single value.
+ internal sealed class ElementalValueProvider : IValueProvider
+ {
+ public ElementalValueProvider(string name, object rawValue, CultureInfo culture)
+ {
+ Name = name;
+ RawValue = rawValue;
+ Culture = culture;
+ }
+
+ public CultureInfo Culture { get; private set; }
+
+ public string Name { get; private set; }
+
+ public object RawValue { get; private set; }
+
+ public bool ContainsPrefix(string prefix)
+ {
+ return ValueProviderUtil.IsPrefixMatch(Name, prefix);
+ }
+
+ public ValueProviderResult GetValue(string key)
+ {
+ return (String.Equals(key, Name, StringComparison.OrdinalIgnoreCase))
+ ? new ValueProviderResult(RawValue, Convert.ToString(RawValue, Culture), Culture)
+ : null;
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/EmailAddressAttribute.cs b/src/Microsoft.Web.Mvc/EmailAddressAttribute.cs
new file mode 100644
index 00000000..e4f2b8ce
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/EmailAddressAttribute.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Text.RegularExpressions;
+using System.Web.Mvc;
+using Microsoft.Web.Mvc.Properties;
+
+namespace Microsoft.Web.Mvc
+{
+ [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
+ public sealed class EmailAddressAttribute : DataTypeAttribute, IClientValidatable
+ {
+ private static Regex _regex = new Regex(@"^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture);
+
+ public EmailAddressAttribute()
+ : base(DataType.EmailAddress)
+ {
+ ErrorMessage = MvcResources.EmailAddressAttribute_Invalid;
+ }
+
+ public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
+ {
+ yield return new ModelClientValidationRule
+ {
+ ValidationType = "email",
+ ErrorMessage = FormatErrorMessage(metadata.GetDisplayName())
+ };
+ }
+
+ public override bool IsValid(object value)
+ {
+ if (value == null)
+ {
+ return true;
+ }
+
+ string valueAsString = value as string;
+ return valueAsString != null && _regex.Match(valueAsString).Length > 0;
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Error.cs b/src/Microsoft.Web.Mvc/Error.cs
new file mode 100644
index 00000000..24e6534c
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Error.cs
@@ -0,0 +1,80 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using Microsoft.Web.Mvc.Properties;
+
+namespace Microsoft.Web.Mvc
+{
+ internal static class Error
+ {
+ public static InvalidOperationException BindingBehavior_ValueNotFound(string fieldName)
+ {
+ string errorString = String.Format(CultureInfo.CurrentCulture, MvcResources.BindingBehavior_ValueNotFound,
+ fieldName);
+ return new InvalidOperationException(errorString);
+ }
+
+ public static ArgumentException Common_TypeMustImplementInterface(Type providedType, Type requiredInterfaceType, string parameterName)
+ {
+ string errorString = String.Format(CultureInfo.CurrentCulture, MvcResources.Common_TypeMustImplementInterface,
+ providedType, requiredInterfaceType);
+ return new ArgumentException(errorString, parameterName);
+ }
+
+ public static ArgumentException GenericModelBinderProvider_ParameterMustSpecifyOpenGenericType(Type specifiedType, string parameterName)
+ {
+ string errorString = String.Format(CultureInfo.CurrentCulture, MvcResources.GenericModelBinderProvider_ParameterMustSpecifyOpenGenericType,
+ specifiedType);
+ return new ArgumentException(errorString, parameterName);
+ }
+
+ public static ArgumentException GenericModelBinderProvider_TypeArgumentCountMismatch(Type modelType, Type modelBinderType)
+ {
+ string errorString = String.Format(CultureInfo.CurrentCulture, MvcResources.GenericModelBinderProvider_TypeArgumentCountMismatch,
+ modelType, modelType.GetGenericArguments().Length, modelBinderType, modelBinderType.GetGenericArguments().Length);
+ return new ArgumentException(errorString, "modelBinderType");
+ }
+
+ public static InvalidOperationException ModelBinderProviderCollection_BinderForTypeNotFound(Type modelType)
+ {
+ string errorString = String.Format(CultureInfo.CurrentCulture, MvcResources.ModelBinderProviderCollection_BinderForTypeNotFound,
+ modelType);
+ return new InvalidOperationException(errorString);
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Justification = "The purpose of this class is to throw errors on behalf of other methods")]
+ public static ArgumentException ModelBinderUtil_ModelCannotBeNull(Type expectedType)
+ {
+ string errorString = String.Format(CultureInfo.CurrentCulture, MvcResources.ModelBinderUtil_ModelCannotBeNull,
+ expectedType);
+ return new ArgumentException(errorString, "bindingContext");
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Justification = "The purpose of this class is to throw errors on behalf of other methods")]
+ public static ArgumentException ModelBinderUtil_ModelInstanceIsWrong(Type actualType, Type expectedType)
+ {
+ string errorString = String.Format(CultureInfo.CurrentCulture, MvcResources.ModelBinderUtil_ModelInstanceIsWrong,
+ actualType, expectedType);
+ return new ArgumentException(errorString, "bindingContext");
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Justification = "The purpose of this class is to throw errors on behalf of other methods")]
+ public static ArgumentException ModelBinderUtil_ModelMetadataCannotBeNull()
+ {
+ return new ArgumentException(MvcResources.ModelBinderUtil_ModelMetadataCannotBeNull, "bindingContext");
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Justification = "The purpose of this class is to throw errors on behalf of other methods")]
+ public static ArgumentException ModelBinderUtil_ModelTypeIsWrong(Type actualType, Type expectedType)
+ {
+ string errorString = String.Format(CultureInfo.CurrentCulture, MvcResources.ModelBinderUtil_ModelTypeIsWrong,
+ actualType, expectedType);
+ return new ArgumentException(errorString, "bindingContext");
+ }
+
+ public static InvalidOperationException ModelBindingContext_ModelMetadataMustBeSet()
+ {
+ return new InvalidOperationException(MvcResources.ModelBindingContext_ModelMetadataMustBeSet);
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ExpressionUtil/BinaryExpressionFingerprint.cs b/src/Microsoft.Web.Mvc/ExpressionUtil/BinaryExpressionFingerprint.cs
new file mode 100644
index 00000000..76c10a8c
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ExpressionUtil/BinaryExpressionFingerprint.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq.Expressions;
+using System.Reflection;
+
+#pragma warning disable 659 // overrides AddToHashCodeCombiner instead
+
+namespace Microsoft.Web.Mvc.ExpressionUtil
+{
+ // BinaryExpression fingerprint class
+ // Useful for things like array[index]
+
+ [SuppressMessage("Microsoft.Usage", "CA2218:OverrideGetHashCodeOnOverridingEquals", Justification = "Overrides AddToHashCodeCombiner() instead.")]
+ internal sealed class BinaryExpressionFingerprint : ExpressionFingerprint
+ {
+ public BinaryExpressionFingerprint(ExpressionType nodeType, Type type, MethodInfo method)
+ : base(nodeType, type)
+ {
+ // Other properties on BinaryExpression (like IsLifted / IsLiftedToNull) are simply derived
+ // from Type and NodeType, so they're not necessary for inclusion in the fingerprint.
+
+ Method = method;
+ }
+
+ // http://msdn.microsoft.com/en-us/library/system.linq.expressions.binaryexpression.method.aspx
+ public MethodInfo Method { get; private set; }
+
+ public override bool Equals(object obj)
+ {
+ BinaryExpressionFingerprint other = obj as BinaryExpressionFingerprint;
+ return (other != null)
+ && Equals(this.Method, other.Method)
+ && this.Equals(other);
+ }
+
+ internal override void AddToHashCodeCombiner(HashCodeCombiner combiner)
+ {
+ combiner.AddObject(Method);
+ base.AddToHashCodeCombiner(combiner);
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ExpressionUtil/CachedExpressionCompiler.cs b/src/Microsoft.Web.Mvc/ExpressionUtil/CachedExpressionCompiler.cs
new file mode 100644
index 00000000..2859b588
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ExpressionUtil/CachedExpressionCompiler.cs
@@ -0,0 +1,143 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq.Expressions;
+using System.Reflection;
+
+namespace Microsoft.Web.Mvc.ExpressionUtil
+{
+ internal static class CachedExpressionCompiler
+ {
+ // This is the entry point to the cached expression compilation system. The system
+ // will try to turn the expression into an actual delegate as quickly as possible,
+ // relying on cache lookups and other techniques to save time if appropriate.
+ // If the provided expression is particularly obscure and the system doesn't know
+ // how to handle it, we'll just compile the expression as normal.
+ public static Func<TModel, TValue> Process<TModel, TValue>(Expression<Func<TModel, TValue>> lambdaExpression)
+ {
+ return Compiler<TModel, TValue>.Compile(lambdaExpression);
+ }
+
+ private static class Compiler<TIn, TOut>
+ {
+ private static Func<TIn, TOut> _identityFunc;
+
+ private static readonly ConcurrentDictionary<MemberInfo, Func<TIn, TOut>> _simpleMemberAccessDict =
+ new ConcurrentDictionary<MemberInfo, Func<TIn, TOut>>();
+
+ private static readonly ConcurrentDictionary<MemberInfo, Func<object, TOut>> _constMemberAccessDict =
+ new ConcurrentDictionary<MemberInfo, Func<object, TOut>>();
+
+ private static readonly ConcurrentDictionary<ExpressionFingerprintChain, Hoisted<TIn, TOut>> _fingerprintedCache =
+ new ConcurrentDictionary<ExpressionFingerprintChain, Hoisted<TIn, TOut>>();
+
+ public static Func<TIn, TOut> Compile(Expression<Func<TIn, TOut>> expr)
+ {
+ return CompileFromIdentityFunc(expr)
+ ?? CompileFromConstLookup(expr)
+ ?? CompileFromMemberAccess(expr)
+ ?? CompileFromFingerprint(expr)
+ ?? CompileSlow(expr);
+ }
+
+ private static Func<TIn, TOut> CompileFromConstLookup(Expression<Func<TIn, TOut>> expr)
+ {
+ ConstantExpression constExpr = expr.Body as ConstantExpression;
+ if (constExpr != null)
+ {
+ // model => {const}
+
+ TOut constantValue = (TOut)constExpr.Value;
+ return _ => constantValue;
+ }
+
+ return null;
+ }
+
+ private static Func<TIn, TOut> CompileFromIdentityFunc(Expression<Func<TIn, TOut>> expr)
+ {
+ if (expr.Body == expr.Parameters[0])
+ {
+ // model => model
+
+ // don't need to lock, as all identity funcs are identical
+ if (_identityFunc == null)
+ {
+ _identityFunc = expr.Compile();
+ }
+
+ return _identityFunc;
+ }
+
+ return null;
+ }
+
+ private static Func<TIn, TOut> CompileFromFingerprint(Expression<Func<TIn, TOut>> expr)
+ {
+ List<object> capturedConstants;
+ ExpressionFingerprintChain fingerprint = FingerprintingExpressionVisitor.GetFingerprintChain(expr, out capturedConstants);
+
+ if (fingerprint != null)
+ {
+ var del = _fingerprintedCache.GetOrAdd(fingerprint, _ =>
+ {
+ // Fingerprinting succeeded, but there was a cache miss. Rewrite the expression
+ // and add the rewritten expression to the cache.
+
+ var hoistedExpr = HoistingExpressionVisitor<TIn, TOut>.Hoist(expr);
+ return hoistedExpr.Compile();
+ });
+ return model => del(model, capturedConstants);
+ }
+
+ // couldn't be fingerprinted
+ return null;
+ }
+
+ private static Func<TIn, TOut> CompileFromMemberAccess(Expression<Func<TIn, TOut>> expr)
+ {
+ // Performance tests show that on the x64 platform, special-casing static member and
+ // captured local variable accesses is faster than letting the fingerprinting system
+ // handle them. On the x86 platform, the fingerprinting system is faster, but only
+ // by around one microsecond, so it's not worth it to complicate the logic here with
+ // an architecture check.
+
+ MemberExpression memberExpr = expr.Body as MemberExpression;
+ if (memberExpr != null)
+ {
+ if (memberExpr.Expression == expr.Parameters[0] || memberExpr.Expression == null)
+ {
+ // model => model.Member or model => StaticMember
+ return _simpleMemberAccessDict.GetOrAdd(memberExpr.Member, _ => expr.Compile());
+ }
+
+ ConstantExpression constExpr = memberExpr.Expression as ConstantExpression;
+ if (constExpr != null)
+ {
+ // model => {const}.Member (captured local variable)
+ var del = _constMemberAccessDict.GetOrAdd(memberExpr.Member, _ =>
+ {
+ // rewrite as capturedLocal => ((TDeclaringType)capturedLocal).Member
+ var constParamExpr = Expression.Parameter(typeof(object), "capturedLocal");
+ var constCastExpr = Expression.Convert(constParamExpr, memberExpr.Member.DeclaringType);
+ var newMemberAccessExpr = memberExpr.Update(constCastExpr);
+ var newLambdaExpr = Expression.Lambda<Func<object, TOut>>(newMemberAccessExpr, constParamExpr);
+ return newLambdaExpr.Compile();
+ });
+
+ object capturedLocal = constExpr.Value;
+ return _ => del(capturedLocal);
+ }
+ }
+
+ return null;
+ }
+
+ private static Func<TIn, TOut> CompileSlow(Expression<Func<TIn, TOut>> expr)
+ {
+ // fallback compilation system - just compile the expression directly
+ return expr.Compile();
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ExpressionUtil/ConditionalExpressionFingerprint.cs b/src/Microsoft.Web.Mvc/ExpressionUtil/ConditionalExpressionFingerprint.cs
new file mode 100644
index 00000000..0eeba0a4
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ExpressionUtil/ConditionalExpressionFingerprint.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq.Expressions;
+
+#pragma warning disable 659 // overrides AddToHashCodeCombiner instead
+
+namespace Microsoft.Web.Mvc.ExpressionUtil
+{
+ // ConditionalExpression fingerprint class
+ // Expression of form (test) ? ifTrue : ifFalse
+
+ [SuppressMessage("Microsoft.Usage", "CA2218:OverrideGetHashCodeOnOverridingEquals", Justification = "Overrides AddToHashCodeCombiner() instead.")]
+ internal sealed class ConditionalExpressionFingerprint : ExpressionFingerprint
+ {
+ public ConditionalExpressionFingerprint(ExpressionType nodeType, Type type)
+ : base(nodeType, type)
+ {
+ // There are no properties on ConditionalExpression that are worth including in
+ // the fingerprint.
+ }
+
+ public override bool Equals(object obj)
+ {
+ ConditionalExpressionFingerprint other = obj as ConditionalExpressionFingerprint;
+ return (other != null)
+ && this.Equals(other);
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ExpressionUtil/ConstantExpressionFingerprint.cs b/src/Microsoft.Web.Mvc/ExpressionUtil/ConstantExpressionFingerprint.cs
new file mode 100644
index 00000000..07b29703
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ExpressionUtil/ConstantExpressionFingerprint.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq.Expressions;
+
+#pragma warning disable 659 // overrides AddToHashCodeCombiner instead
+
+namespace Microsoft.Web.Mvc.ExpressionUtil
+{
+ // ConstantExpression fingerprint class
+ //
+ // A ConstantExpression might represent a captured local variable, so we can't compile
+ // the value directly into the cached function. Instead, a placeholder is generated
+ // and the value is hoisted into a local variables array. This placeholder can then
+ // be compiled and cached, and the array lookup happens at runtime.
+
+ [SuppressMessage("Microsoft.Usage", "CA2218:OverrideGetHashCodeOnOverridingEquals", Justification = "Overrides AddToHashCodeCombiner() instead.")]
+ internal sealed class ConstantExpressionFingerprint : ExpressionFingerprint
+ {
+ public ConstantExpressionFingerprint(ExpressionType nodeType, Type type)
+ : base(nodeType, type)
+ {
+ // There are no properties on ConstantExpression that are worth including in
+ // the fingerprint.
+ }
+
+ public override bool Equals(object obj)
+ {
+ ConstantExpressionFingerprint other = obj as ConstantExpressionFingerprint;
+ return (other != null)
+ && this.Equals(other);
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ExpressionUtil/DefaultExpressionFingerprint.cs b/src/Microsoft.Web.Mvc/ExpressionUtil/DefaultExpressionFingerprint.cs
new file mode 100644
index 00000000..82a1ef00
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ExpressionUtil/DefaultExpressionFingerprint.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq.Expressions;
+
+#pragma warning disable 659 // overrides AddToHashCodeCombiner instead
+
+namespace Microsoft.Web.Mvc.ExpressionUtil
+{
+ // DefaultExpression fingerprint class
+ // Expression of form default(T)
+
+ [SuppressMessage("Microsoft.Usage", "CA2218:OverrideGetHashCodeOnOverridingEquals", Justification = "Overrides AddToHashCodeCombiner() instead.")]
+ internal sealed class DefaultExpressionFingerprint : ExpressionFingerprint
+ {
+ public DefaultExpressionFingerprint(ExpressionType nodeType, Type type)
+ : base(nodeType, type)
+ {
+ // There are no properties on DefaultExpression that are worth including in
+ // the fingerprint.
+ }
+
+ public override bool Equals(object obj)
+ {
+ DefaultExpressionFingerprint other = obj as DefaultExpressionFingerprint;
+ return (other != null)
+ && this.Equals(other);
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ExpressionUtil/ExpressionFingerprint.cs b/src/Microsoft.Web.Mvc/ExpressionUtil/ExpressionFingerprint.cs
new file mode 100644
index 00000000..6f12b1ee
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ExpressionUtil/ExpressionFingerprint.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Linq.Expressions;
+
+namespace Microsoft.Web.Mvc.ExpressionUtil
+{
+ // Serves as the base class for all expression fingerprints. Provides a default implementation
+ // of GetHashCode().
+
+ internal abstract class ExpressionFingerprint
+ {
+ protected ExpressionFingerprint(ExpressionType nodeType, Type type)
+ {
+ NodeType = nodeType;
+ Type = type;
+ }
+
+ // the type of expression node, e.g. OP_ADD, MEMBER_ACCESS, etc.
+ public ExpressionType NodeType { get; private set; }
+
+ // the CLR type resulting from this expression, e.g. int, string, etc.
+ public Type Type { get; private set; }
+
+ internal virtual void AddToHashCodeCombiner(HashCodeCombiner combiner)
+ {
+ combiner.AddInt32((int)NodeType);
+ combiner.AddObject(Type);
+ }
+
+ protected bool Equals(ExpressionFingerprint other)
+ {
+ return (other != null)
+ && (this.NodeType == other.NodeType)
+ && Equals(this.Type, other.Type);
+ }
+
+ public override bool Equals(object obj)
+ {
+ return Equals(obj as ExpressionFingerprint);
+ }
+
+ public override int GetHashCode()
+ {
+ HashCodeCombiner combiner = new HashCodeCombiner();
+ AddToHashCodeCombiner(combiner);
+ return combiner.CombinedHash;
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ExpressionUtil/ExpressionFingerprintChain.cs b/src/Microsoft.Web.Mvc/ExpressionUtil/ExpressionFingerprintChain.cs
new file mode 100644
index 00000000..2b793011
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ExpressionUtil/ExpressionFingerprintChain.cs
@@ -0,0 +1,86 @@
+using System;
+using System.Collections.Generic;
+
+namespace Microsoft.Web.Mvc.ExpressionUtil
+{
+ // Expression fingerprint chain class
+ // Contains information used for generalizing, comparing, and recreating Expression instances
+ //
+ // Since Expression objects are immutable and are recreated for every invocation of an expression
+ // helper method, they can't be compared directly. Fingerprinting Expression objects allows
+ // information about them to be abstracted away, and the fingerprints can be directly compared.
+ // Consider the process of fingerprinting that all values (parameters, constants, etc.) are hoisted
+ // and replaced with dummies. What remains can be decomposed into a sequence of operations on specific
+ // types and specific inputs.
+ //
+ // Some sample fingerprints chains:
+ //
+ // 2 + 4 -> OP_ADD, CONST:int, NULL, CONST:int
+ // 2 + 8 -> OP_ADD, CONST:int, NULL, CONST:int
+ // 2.0 + 4.0 -> OP_ADD, CONST:double, NULL, CONST:double
+ //
+ // 2 + 4 and 2 + 8 have the same fingerprint, but 2.0 + 4.0 has a different fingerprint since its
+ // underlying types differ. Note that this looks a bit like prefix notation and is a side effect
+ // of how the ExpressionVisitor class recurses into expressions. (Occasionally there will be a NULL
+ // in the fingerprint chain, which depending on context can denote a static member, a null Conversion
+ // in a BinaryExpression, and so forth.)
+ //
+ // "Hello " + "world" -> OP_ADD, CONST:string, NULL, CONST:string
+ // "Hello " + {model} -> OP_ADD, CONST:string, NULL, PARAM_0:string
+ //
+ // These string concatenations have different fingerprints since the inputs are provided differently:
+ // one is a constant, the other is a parameter.
+ //
+ // ({model} ?? "sample").Length -> MEMBER_ACCESS(String.Length), OP_COALESCE, PARAM_0:string, NULL, CONST:string
+ // ({model} ?? "other sample").Length -> MEMBER_ACCESS(String.Length), OP_COALESCE, PARAM_0:string, NULL, CONST:string
+ //
+ // These expressions have the same fingerprint since all constants of the same underlying type are
+ // treated equally.
+ //
+ // It's also important that the fingerprints don't reference the actual Expression objects that were
+ // used to generate them, as the fingerprints will be cached, and caching a fingerprint that references
+ // an Expression will root the Expression (and any objects it references).
+
+ internal sealed class ExpressionFingerprintChain : IEquatable<ExpressionFingerprintChain>
+ {
+ public readonly List<ExpressionFingerprint> Elements = new List<ExpressionFingerprint>();
+
+ public bool Equals(ExpressionFingerprintChain other)
+ {
+ // Two chains are considered equal if two elements appearing in the same index in
+ // each chain are equal (value equality, not referential equality).
+
+ if (other == null)
+ {
+ return false;
+ }
+
+ if (this.Elements.Count != other.Elements.Count)
+ {
+ return false;
+ }
+
+ for (int i = 0; i < this.Elements.Count; i++)
+ {
+ if (!Equals(this.Elements[i], other.Elements[i]))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public override bool Equals(object obj)
+ {
+ return Equals(obj as ExpressionFingerprintChain);
+ }
+
+ public override int GetHashCode()
+ {
+ HashCodeCombiner combiner = new HashCodeCombiner();
+ Elements.ForEach(combiner.AddFingerprint);
+ return combiner.CombinedHash;
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ExpressionUtil/FingerprintingExpressionVisitor.cs b/src/Microsoft.Web.Mvc/ExpressionUtil/FingerprintingExpressionVisitor.cs
new file mode 100644
index 00000000..3bed0dd7
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ExpressionUtil/FingerprintingExpressionVisitor.cs
@@ -0,0 +1,296 @@
+using System.Collections.Generic;
+using System.Linq.Expressions;
+
+namespace Microsoft.Web.Mvc.ExpressionUtil
+{
+ // This is a visitor which produces a fingerprint of an expression. It doesn't
+ // rewrite the expression in a form which can be compiled and cached.
+
+ internal sealed class FingerprintingExpressionVisitor : ExpressionVisitor
+ {
+ private readonly List<object> _seenConstants = new List<object>();
+ private readonly List<ParameterExpression> _seenParameters = new List<ParameterExpression>();
+ private readonly ExpressionFingerprintChain _currentChain = new ExpressionFingerprintChain();
+ private bool _gaveUp;
+
+ private FingerprintingExpressionVisitor()
+ {
+ }
+
+ private T GiveUp<T>(T node)
+ {
+ // We don't understand this node, so just quit.
+
+ _gaveUp = true;
+ return node;
+ }
+
+ // Returns the fingerprint chain + captured constants list for this expression, or null
+ // if the expression couldn't be fingerprinted.
+ public static ExpressionFingerprintChain GetFingerprintChain(Expression expr, out List<object> capturedConstants)
+ {
+ FingerprintingExpressionVisitor visitor = new FingerprintingExpressionVisitor();
+ visitor.Visit(expr);
+
+ if (visitor._gaveUp)
+ {
+ capturedConstants = null;
+ return null;
+ }
+ else
+ {
+ capturedConstants = visitor._seenConstants;
+ return visitor._currentChain;
+ }
+ }
+
+ public override Expression Visit(Expression node)
+ {
+ if (node == null)
+ {
+ _currentChain.Elements.Add(null);
+ return null;
+ }
+ else
+ {
+ return base.Visit(node);
+ }
+ }
+
+ protected override Expression VisitBinary(BinaryExpression node)
+ {
+ if (_gaveUp)
+ {
+ return node;
+ }
+ _currentChain.Elements.Add(new BinaryExpressionFingerprint(node.NodeType, node.Type, node.Method));
+ return base.VisitBinary(node);
+ }
+
+ protected override Expression VisitBlock(BlockExpression node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override CatchBlock VisitCatchBlock(CatchBlock node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override Expression VisitConditional(ConditionalExpression node)
+ {
+ if (_gaveUp)
+ {
+ return node;
+ }
+ _currentChain.Elements.Add(new ConditionalExpressionFingerprint(node.NodeType, node.Type));
+ return base.VisitConditional(node);
+ }
+
+ protected override Expression VisitConstant(ConstantExpression node)
+ {
+ if (_gaveUp)
+ {
+ return node;
+ }
+
+ _seenConstants.Add(node.Value);
+ _currentChain.Elements.Add(new ConstantExpressionFingerprint(node.NodeType, node.Type));
+ return base.VisitConstant(node);
+ }
+
+ protected override Expression VisitDebugInfo(DebugInfoExpression node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override Expression VisitDefault(DefaultExpression node)
+ {
+ if (_gaveUp)
+ {
+ return node;
+ }
+ _currentChain.Elements.Add(new DefaultExpressionFingerprint(node.NodeType, node.Type));
+ return base.VisitDefault(node);
+ }
+
+ protected override Expression VisitDynamic(DynamicExpression node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override ElementInit VisitElementInit(ElementInit node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override Expression VisitExtension(Expression node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override Expression VisitGoto(GotoExpression node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override Expression VisitIndex(IndexExpression node)
+ {
+ if (_gaveUp)
+ {
+ return node;
+ }
+ _currentChain.Elements.Add(new IndexExpressionFingerprint(node.NodeType, node.Type, node.Indexer));
+ return base.VisitIndex(node);
+ }
+
+ protected override Expression VisitInvocation(InvocationExpression node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override Expression VisitLabel(LabelExpression node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override LabelTarget VisitLabelTarget(LabelTarget node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override Expression VisitLambda<T>(Expression<T> node)
+ {
+ if (_gaveUp)
+ {
+ return node;
+ }
+ _currentChain.Elements.Add(new LambdaExpressionFingerprint(node.NodeType, node.Type));
+ return base.VisitLambda(node);
+ }
+
+ protected override Expression VisitListInit(ListInitExpression node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override Expression VisitLoop(LoopExpression node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override Expression VisitMember(MemberExpression node)
+ {
+ if (_gaveUp)
+ {
+ return node;
+ }
+ _currentChain.Elements.Add(new MemberExpressionFingerprint(node.NodeType, node.Type, node.Member));
+ return base.VisitMember(node);
+ }
+
+ protected override MemberAssignment VisitMemberAssignment(MemberAssignment node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override MemberBinding VisitMemberBinding(MemberBinding node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override Expression VisitMemberInit(MemberInitExpression node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override MemberListBinding VisitMemberListBinding(MemberListBinding node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override MemberMemberBinding VisitMemberMemberBinding(MemberMemberBinding node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override Expression VisitMethodCall(MethodCallExpression node)
+ {
+ if (_gaveUp)
+ {
+ return node;
+ }
+ _currentChain.Elements.Add(new MethodCallExpressionFingerprint(node.NodeType, node.Type, node.Method));
+ return base.VisitMethodCall(node);
+ }
+
+ protected override Expression VisitNew(NewExpression node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override Expression VisitNewArray(NewArrayExpression node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override Expression VisitParameter(ParameterExpression node)
+ {
+ if (_gaveUp)
+ {
+ return node;
+ }
+
+ int parameterIndex = _seenParameters.IndexOf(node);
+ if (parameterIndex < 0)
+ {
+ // first time seeing this parameter
+ parameterIndex = _seenParameters.Count;
+ _seenParameters.Add(node);
+ }
+
+ _currentChain.Elements.Add(new ParameterExpressionFingerprint(node.NodeType, node.Type, parameterIndex));
+ return base.VisitParameter(node);
+ }
+
+ protected override Expression VisitRuntimeVariables(RuntimeVariablesExpression node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override Expression VisitSwitch(SwitchExpression node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override SwitchCase VisitSwitchCase(SwitchCase node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override Expression VisitTry(TryExpression node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override Expression VisitTypeBinary(TypeBinaryExpression node)
+ {
+ if (_gaveUp)
+ {
+ return node;
+ }
+ _currentChain.Elements.Add(new TypeBinaryExpressionFingerprint(node.NodeType, node.Type, node.TypeOperand));
+ return base.VisitTypeBinary(node);
+ }
+
+ protected override Expression VisitUnary(UnaryExpression node)
+ {
+ if (_gaveUp)
+ {
+ return node;
+ }
+ _currentChain.Elements.Add(new UnaryExpressionFingerprint(node.NodeType, node.Type, node.Method));
+ return base.VisitUnary(node);
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ExpressionUtil/HashCodeCombiner.cs b/src/Microsoft.Web.Mvc/ExpressionUtil/HashCodeCombiner.cs
new file mode 100644
index 00000000..c44b749d
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ExpressionUtil/HashCodeCombiner.cs
@@ -0,0 +1,56 @@
+using System.Collections;
+
+namespace Microsoft.Web.Mvc.ExpressionUtil
+{
+ // based on System.Web.Util.HashCodeCombiner
+ internal class HashCodeCombiner
+ {
+ private long _combinedHash64 = 0x1505L;
+
+ public int CombinedHash
+ {
+ get { return _combinedHash64.GetHashCode(); }
+ }
+
+ public void AddFingerprint(ExpressionFingerprint fingerprint)
+ {
+ if (fingerprint != null)
+ {
+ fingerprint.AddToHashCodeCombiner(this);
+ }
+ else
+ {
+ AddInt32(0);
+ }
+ }
+
+ public void AddEnumerable(IEnumerable e)
+ {
+ if (e == null)
+ {
+ AddInt32(0);
+ }
+ else
+ {
+ int count = 0;
+ foreach (object o in e)
+ {
+ AddObject(o);
+ count++;
+ }
+ AddInt32(count);
+ }
+ }
+
+ public void AddInt32(int i)
+ {
+ _combinedHash64 = ((_combinedHash64 << 5) + _combinedHash64) ^ i;
+ }
+
+ public void AddObject(object o)
+ {
+ int hashCode = (o != null) ? o.GetHashCode() : 0;
+ AddInt32(hashCode);
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ExpressionUtil/Hoisted`2.cs b/src/Microsoft.Web.Mvc/ExpressionUtil/Hoisted`2.cs
new file mode 100644
index 00000000..10901dd6
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ExpressionUtil/Hoisted`2.cs
@@ -0,0 +1,6 @@
+using System.Collections.Generic;
+
+namespace Microsoft.Web.Mvc.ExpressionUtil
+{
+ internal delegate TValue Hoisted<TModel, TValue>(TModel model, List<object> capturedConstants);
+}
diff --git a/src/Microsoft.Web.Mvc/ExpressionUtil/HoistingExpressionVisitor.cs b/src/Microsoft.Web.Mvc/ExpressionUtil/HoistingExpressionVisitor.cs
new file mode 100644
index 00000000..bf1cf7a5
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ExpressionUtil/HoistingExpressionVisitor.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Collections.Generic;
+using System.Linq.Expressions;
+
+namespace Microsoft.Web.Mvc.ExpressionUtil
+{
+ // This is a visitor which rewrites constant expressions as parameter lookups. It's meant
+ // to produce an expression which can be cached safely.
+
+ internal sealed class HoistingExpressionVisitor<TIn, TOut> : ExpressionVisitor
+ {
+ private static readonly ParameterExpression _hoistedConstantsParamExpr = Expression.Parameter(typeof(List<object>), "hoistedConstants");
+ private int _numConstantsProcessed;
+
+ // factory will create instance
+ private HoistingExpressionVisitor()
+ {
+ }
+
+ public static Expression<Hoisted<TIn, TOut>> Hoist(Expression<Func<TIn, TOut>> expr)
+ {
+ // rewrite Expression<Func<TIn, TOut>> as Expression<Hoisted<TIn, TOut>>
+
+ var visitor = new HoistingExpressionVisitor<TIn, TOut>();
+ var rewrittenBodyExpr = visitor.Visit(expr.Body);
+ var rewrittenLambdaExpr = Expression.Lambda<Hoisted<TIn, TOut>>(rewrittenBodyExpr, expr.Parameters[0], _hoistedConstantsParamExpr);
+ return rewrittenLambdaExpr;
+ }
+
+ protected override Expression VisitConstant(ConstantExpression node)
+ {
+ // rewrite the constant expression as (TConst)hoistedConstants[i];
+ return Expression.Convert(Expression.Property(_hoistedConstantsParamExpr, "Item", Expression.Constant(_numConstantsProcessed++)), node.Type);
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ExpressionUtil/IndexExpressionFingerprint.cs b/src/Microsoft.Web.Mvc/ExpressionUtil/IndexExpressionFingerprint.cs
new file mode 100644
index 00000000..fa6aa219
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ExpressionUtil/IndexExpressionFingerprint.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq.Expressions;
+using System.Reflection;
+
+#pragma warning disable 659 // overrides AddToHashCodeCombiner instead
+
+namespace Microsoft.Web.Mvc.ExpressionUtil
+{
+ // IndexExpression fingerprint class
+ // Represents certain forms of array access or indexer property access
+
+ [SuppressMessage("Microsoft.Usage", "CA2218:OverrideGetHashCodeOnOverridingEquals", Justification = "Overrides AddToHashCodeCombiner() instead.")]
+ internal sealed class IndexExpressionFingerprint : ExpressionFingerprint
+ {
+ public IndexExpressionFingerprint(ExpressionType nodeType, Type type, PropertyInfo indexer)
+ : base(nodeType, type)
+ {
+ // Other properties on IndexExpression (like the argument count) are simply derived
+ // from Type and Indexer, so they're not necessary for inclusion in the fingerprint.
+
+ Indexer = indexer;
+ }
+
+ // http://msdn.microsoft.com/en-us/library/system.linq.expressions.indexexpression.indexer.aspx
+ public PropertyInfo Indexer { get; private set; }
+
+ public override bool Equals(object obj)
+ {
+ IndexExpressionFingerprint other = obj as IndexExpressionFingerprint;
+ return (other != null)
+ && Equals(this.Indexer, other.Indexer)
+ && this.Equals(other);
+ }
+
+ internal override void AddToHashCodeCombiner(HashCodeCombiner combiner)
+ {
+ combiner.AddObject(Indexer);
+ base.AddToHashCodeCombiner(combiner);
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ExpressionUtil/LambdaExpressionFingerprint.cs b/src/Microsoft.Web.Mvc/ExpressionUtil/LambdaExpressionFingerprint.cs
new file mode 100644
index 00000000..e669e22b
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ExpressionUtil/LambdaExpressionFingerprint.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq.Expressions;
+
+#pragma warning disable 659 // overrides AddToHashCodeCombiner instead
+
+namespace Microsoft.Web.Mvc.ExpressionUtil
+{
+ // LambdaExpression fingerprint class
+ // Represents a lambda expression (root element in Expression<T>)
+
+ [SuppressMessage("Microsoft.Usage", "CA2218:OverrideGetHashCodeOnOverridingEquals", Justification = "Overrides AddToHashCodeCombiner() instead.")]
+ internal sealed class LambdaExpressionFingerprint : ExpressionFingerprint
+ {
+ public LambdaExpressionFingerprint(ExpressionType nodeType, Type type)
+ : base(nodeType, type)
+ {
+ // There are no properties on LambdaExpression that are worth including in
+ // the fingerprint.
+ }
+
+ public override bool Equals(object obj)
+ {
+ LambdaExpressionFingerprint other = obj as LambdaExpressionFingerprint;
+ return (other != null)
+ && this.Equals(other);
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ExpressionUtil/MemberExpressionFingerprint.cs b/src/Microsoft.Web.Mvc/ExpressionUtil/MemberExpressionFingerprint.cs
new file mode 100644
index 00000000..152a2bc2
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ExpressionUtil/MemberExpressionFingerprint.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq.Expressions;
+using System.Reflection;
+
+#pragma warning disable 659 // overrides AddToHashCodeCombiner instead
+
+namespace Microsoft.Web.Mvc.ExpressionUtil
+{
+ // MemberExpression fingerprint class
+ // Expression of form xxx.FieldOrProperty
+
+ [SuppressMessage("Microsoft.Usage", "CA2218:OverrideGetHashCodeOnOverridingEquals", Justification = "Overrides AddToHashCodeCombiner() instead.")]
+ internal sealed class MemberExpressionFingerprint : ExpressionFingerprint
+ {
+ public MemberExpressionFingerprint(ExpressionType nodeType, Type type, MemberInfo member)
+ : base(nodeType, type)
+ {
+ Member = member;
+ }
+
+ // http://msdn.microsoft.com/en-us/library/system.linq.expressions.memberexpression.member.aspx
+ public MemberInfo Member { get; private set; }
+
+ public override bool Equals(object obj)
+ {
+ MemberExpressionFingerprint other = obj as MemberExpressionFingerprint;
+ return (other != null)
+ && Equals(this.Member, other.Member)
+ && this.Equals(other);
+ }
+
+ internal override void AddToHashCodeCombiner(HashCodeCombiner combiner)
+ {
+ combiner.AddObject(Member);
+ base.AddToHashCodeCombiner(combiner);
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ExpressionUtil/MethodCallExpressionFingerprint.cs b/src/Microsoft.Web.Mvc/ExpressionUtil/MethodCallExpressionFingerprint.cs
new file mode 100644
index 00000000..af6ab466
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ExpressionUtil/MethodCallExpressionFingerprint.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq.Expressions;
+using System.Reflection;
+
+#pragma warning disable 659 // overrides AddToHashCodeCombiner instead
+
+namespace Microsoft.Web.Mvc.ExpressionUtil
+{
+ // MethodCallExpression fingerprint class
+ // Expression of form xxx.Foo(...), xxx[...] (get_Item()), etc.
+
+ [SuppressMessage("Microsoft.Usage", "CA2218:OverrideGetHashCodeOnOverridingEquals", Justification = "Overrides AddToHashCodeCombiner() instead.")]
+ internal sealed class MethodCallExpressionFingerprint : ExpressionFingerprint
+ {
+ public MethodCallExpressionFingerprint(ExpressionType nodeType, Type type, MethodInfo method)
+ : base(nodeType, type)
+ {
+ // Other properties on MethodCallExpression (like the argument count) are simply derived
+ // from Type and Indexer, so they're not necessary for inclusion in the fingerprint.
+
+ Method = method;
+ }
+
+ // http://msdn.microsoft.com/en-us/library/system.linq.expressions.methodcallexpression.method.aspx
+ public MethodInfo Method { get; private set; }
+
+ public override bool Equals(object obj)
+ {
+ MethodCallExpressionFingerprint other = obj as MethodCallExpressionFingerprint;
+ return (other != null)
+ && Equals(this.Method, other.Method)
+ && this.Equals(other);
+ }
+
+ internal override void AddToHashCodeCombiner(HashCodeCombiner combiner)
+ {
+ combiner.AddObject(Method);
+ base.AddToHashCodeCombiner(combiner);
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ExpressionUtil/ParameterExpressionFingerprint.cs b/src/Microsoft.Web.Mvc/ExpressionUtil/ParameterExpressionFingerprint.cs
new file mode 100644
index 00000000..4a49a3bf
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ExpressionUtil/ParameterExpressionFingerprint.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq.Expressions;
+
+#pragma warning disable 659 // overrides AddToHashCodeCombiner instead
+
+namespace Microsoft.Web.Mvc.ExpressionUtil
+{
+ // ParameterExpression fingerprint class
+ // Can represent the model parameter or an inner parameter in an open lambda expression
+
+ [SuppressMessage("Microsoft.Usage", "CA2218:OverrideGetHashCodeOnOverridingEquals", Justification = "Overrides AddToHashCodeCombiner() instead.")]
+ internal sealed class ParameterExpressionFingerprint : ExpressionFingerprint
+ {
+ public ParameterExpressionFingerprint(ExpressionType nodeType, Type type, int parameterIndex)
+ : base(nodeType, type)
+ {
+ ParameterIndex = parameterIndex;
+ }
+
+ // Parameter position within the overall expression, used to maintain alpha equivalence.
+ public int ParameterIndex { get; private set; }
+
+ public override bool Equals(object obj)
+ {
+ ParameterExpressionFingerprint other = obj as ParameterExpressionFingerprint;
+ return (other != null)
+ && (this.ParameterIndex == other.ParameterIndex)
+ && this.Equals(other);
+ }
+
+ internal override void AddToHashCodeCombiner(HashCodeCombiner combiner)
+ {
+ combiner.AddInt32(ParameterIndex);
+ base.AddToHashCodeCombiner(combiner);
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ExpressionUtil/TypeBinaryExpressionFingerprint.cs b/src/Microsoft.Web.Mvc/ExpressionUtil/TypeBinaryExpressionFingerprint.cs
new file mode 100644
index 00000000..f30525d1
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ExpressionUtil/TypeBinaryExpressionFingerprint.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq.Expressions;
+
+#pragma warning disable 659 // overrides AddToHashCodeCombiner instead
+
+namespace Microsoft.Web.Mvc.ExpressionUtil
+{
+ // TypeBinary fingerprint class
+ // Expression of form "obj is T"
+
+ [SuppressMessage("Microsoft.Usage", "CA2218:OverrideGetHashCodeOnOverridingEquals", Justification = "Overrides AddToHashCodeCombiner() instead.")]
+ internal sealed class TypeBinaryExpressionFingerprint : ExpressionFingerprint
+ {
+ public TypeBinaryExpressionFingerprint(ExpressionType nodeType, Type type, Type typeOperand)
+ : base(nodeType, type)
+ {
+ TypeOperand = typeOperand;
+ }
+
+ // http://msdn.microsoft.com/en-us/library/system.linq.expressions.typebinaryexpression.typeoperand.aspx
+ public Type TypeOperand { get; private set; }
+
+ public override bool Equals(object obj)
+ {
+ TypeBinaryExpressionFingerprint other = obj as TypeBinaryExpressionFingerprint;
+ return (other != null)
+ && Equals(this.TypeOperand, other.TypeOperand)
+ && this.Equals(other);
+ }
+
+ internal override void AddToHashCodeCombiner(HashCodeCombiner combiner)
+ {
+ combiner.AddObject(TypeOperand);
+ base.AddToHashCodeCombiner(combiner);
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ExpressionUtil/UnaryExpressionFingerprint.cs b/src/Microsoft.Web.Mvc/ExpressionUtil/UnaryExpressionFingerprint.cs
new file mode 100644
index 00000000..878cb9ca
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ExpressionUtil/UnaryExpressionFingerprint.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq.Expressions;
+using System.Reflection;
+
+#pragma warning disable 659 // overrides AddToHashCodeCombiner instead
+
+namespace Microsoft.Web.Mvc.ExpressionUtil
+{
+ // UnaryExpression fingerprint class
+ // The most common appearance of a UnaryExpression is a cast or other conversion operator
+
+ [SuppressMessage("Microsoft.Usage", "CA2218:OverrideGetHashCodeOnOverridingEquals", Justification = "Overrides AddToHashCodeCombiner() instead.")]
+ internal sealed class UnaryExpressionFingerprint : ExpressionFingerprint
+ {
+ public UnaryExpressionFingerprint(ExpressionType nodeType, Type type, MethodInfo method)
+ : base(nodeType, type)
+ {
+ // Other properties on UnaryExpression (like IsLifted / IsLiftedToNull) are simply derived
+ // from Type and NodeType, so they're not necessary for inclusion in the fingerprint.
+
+ Method = method;
+ }
+
+ // http://msdn.microsoft.com/en-us/library/system.linq.expressions.unaryexpression.method.aspx
+ public MethodInfo Method { get; private set; }
+
+ public override bool Equals(object obj)
+ {
+ UnaryExpressionFingerprint other = obj as UnaryExpressionFingerprint;
+ return (other != null)
+ && Equals(this.Method, other.Method)
+ && this.Equals(other);
+ }
+
+ internal override void AddToHashCodeCombiner(HashCodeCombiner combiner)
+ {
+ combiner.AddObject(Method);
+ base.AddToHashCodeCombiner(combiner);
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/FileExtensionsAttribute.cs b/src/Microsoft.Web.Mvc/FileExtensionsAttribute.cs
new file mode 100644
index 00000000..14183ef0
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/FileExtensionsAttribute.cs
@@ -0,0 +1,98 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Web;
+using System.Web.Mvc;
+using Microsoft.Web.Mvc.Properties;
+
+namespace Microsoft.Web.Mvc
+{
+ [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
+ public sealed class FileExtensionsAttribute : DataTypeAttribute, IClientValidatable
+ {
+ private string _extensions;
+
+ public FileExtensionsAttribute()
+ : base("upload")
+ {
+ ErrorMessage = MvcResources.FileExtensionsAttribute_Invalid;
+ }
+
+ public string Extensions
+ {
+ get { return String.IsNullOrWhiteSpace(_extensions) ? "png,jpg,jpeg,gif" : _extensions; }
+ set { _extensions = value; }
+ }
+
+ private string ExtensionsFormatted
+ {
+ get { return ExtensionsParsed.Aggregate((left, right) => left + ", " + right); }
+ }
+
+ [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "These strings are normalized to lowercase because they are presented to the user in lowercase format")]
+ private string ExtensionsNormalized
+ {
+ get { return Extensions.Replace(" ", String.Empty).Replace(".", String.Empty).ToLowerInvariant(); }
+ }
+
+ private IEnumerable<string> ExtensionsParsed
+ {
+ get { return ExtensionsNormalized.Split(',').Select(e => "." + e); }
+ }
+
+ public override string FormatErrorMessage(string name)
+ {
+ return String.Format(CultureInfo.CurrentCulture, ErrorMessageString, name, ExtensionsFormatted);
+ }
+
+ public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
+ {
+ var rule = new ModelClientValidationRule
+ {
+ ValidationType = "accept",
+ ErrorMessage = FormatErrorMessage(metadata.GetDisplayName())
+ };
+ rule.ValidationParameters["exts"] = ExtensionsNormalized;
+ yield return rule;
+ }
+
+ public override bool IsValid(object value)
+ {
+ if (value == null)
+ {
+ return true;
+ }
+
+ HttpPostedFileBase valueAsFileBase = value as HttpPostedFileBase;
+ if (valueAsFileBase != null)
+ {
+ return ValidateExtension(valueAsFileBase.FileName);
+ }
+
+ string valueAsString = value as string;
+ if (valueAsString != null)
+ {
+ return ValidateExtension(valueAsString);
+ }
+
+ return false;
+ }
+
+ [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "These strings are normalized to lowercase because they are presented to the user in lowercase format")]
+ private bool ValidateExtension(string fileName)
+ {
+ try
+ {
+ return ExtensionsParsed.Contains(Path.GetExtension(fileName).ToLowerInvariant());
+ }
+ catch (ArgumentException)
+ {
+ return false;
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/FormExtensions.cs b/src/Microsoft.Web.Mvc/FormExtensions.cs
new file mode 100644
index 00000000..ebfadfd8
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/FormExtensions.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq.Expressions;
+using System.Web.Mvc;
+using System.Web.Mvc.Html;
+
+namespace Microsoft.Web.Mvc
+{
+ public static class FormExtensions
+ {
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcForm BeginForm<TController>(this HtmlHelper helper, Expression<Action<TController>> action) where TController : Controller
+ {
+ return BeginForm(helper, action, FormMethod.Post, null);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcForm BeginForm<TController>(this HtmlHelper helper, Expression<Action<TController>> action, FormMethod method) where TController : Controller
+ {
+ return BeginForm(helper, action, method, null);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcForm BeginForm<TController>(this HtmlHelper helper, Expression<Action<TController>> action, FormMethod method, object htmlAttributes) where TController : Controller
+ {
+ return BeginForm(helper, action, method, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcForm BeginForm<TController>(this HtmlHelper helper, Expression<Action<TController>> action, FormMethod method, IDictionary<string, object> htmlAttributes) where TController : Controller
+ {
+ TagBuilder tagBuilder = new TagBuilder("form");
+ tagBuilder.MergeAttributes(htmlAttributes);
+ string formAction = helper.BuildUrlFromExpression(action);
+ tagBuilder.MergeAttribute("action", formAction);
+ tagBuilder.MergeAttribute("method", HtmlHelper.GetFormMethodString(method));
+
+ helper.ViewContext.Writer.Write(tagBuilder.ToString(TagRenderMode.StartTag));
+ return new MvcForm(helper.ViewContext);
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/DisplayTemplates/Boolean.ascx b/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/DisplayTemplates/Boolean.ascx
new file mode 100644
index 00000000..84411368
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/DisplayTemplates/Boolean.ascx
@@ -0,0 +1,20 @@
+<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
+<script runat="server">
+ private bool? Value {
+ get {
+ if (ViewData.Model == null) {
+ return null;
+ }
+ return Convert.ToBoolean(ViewData.Model, System.Globalization.CultureInfo.InvariantCulture);
+ }
+ }
+</script>
+<% if (ViewData.ModelMetadata.IsNullableValueType) { %>
+ <select class="list-box tri-state" disabled="disabled">
+ <option value="" <%= Value.HasValue ? "" : "selected='selected'" %>>Not Set</option>
+ <option value="true" <%= Value.HasValue && Value.Value ? "selected='selected'" : "" %>>True</option>
+ <option value="false" <%= Value.HasValue && !Value.Value ? "selected='selected'" : "" %>>False</option>
+ </select>
+<% } else { %>
+ <input type="checkbox" class="check-box" disabled="disabled" <%= Value.HasValue && Value.Value ? "checked='checked'" : "" %> />
+<% } %> \ No newline at end of file
diff --git a/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/DisplayTemplates/Collection.ascx b/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/DisplayTemplates/Collection.ascx
new file mode 100644
index 00000000..396c5ca8
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/DisplayTemplates/Collection.ascx
@@ -0,0 +1,16 @@
+<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
+<%
+ if (Model != null) {
+ string oldPrefix = ViewData.TemplateInfo.HtmlFieldPrefix;
+ int index = 0;
+
+ ViewData.TemplateInfo.HtmlFieldPrefix = String.Empty;
+
+ foreach (object item in (IEnumerable)Model) {
+ string fieldName = String.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}[{1}]", oldPrefix, index++);
+ ViewContext.Writer.Write(Html.DisplayFor(m => item, null, fieldName));
+ }
+
+ ViewData.TemplateInfo.HtmlFieldPrefix = oldPrefix;
+ }
+%> \ No newline at end of file
diff --git a/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/DisplayTemplates/Decimal.ascx b/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/DisplayTemplates/Decimal.ascx
new file mode 100644
index 00000000..0f2c6d7e
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/DisplayTemplates/Decimal.ascx
@@ -0,0 +1,12 @@
+<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
+<script runat="server">
+ private object FormattedValue {
+ get {
+ if (ViewData.TemplateInfo.FormattedModelValue == ViewData.ModelMetadata.Model) {
+ return String.Format(System.Globalization.CultureInfo.CurrentCulture, "{0:0.00}", ViewData.ModelMetadata.Model);
+ }
+ return ViewData.TemplateInfo.FormattedModelValue;
+ }
+ }
+</script>
+<%= Html.Encode(FormattedValue) %> \ No newline at end of file
diff --git a/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/DisplayTemplates/EmailAddress.ascx b/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/DisplayTemplates/EmailAddress.ascx
new file mode 100644
index 00000000..ff14236b
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/DisplayTemplates/EmailAddress.ascx
@@ -0,0 +1,2 @@
+<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
+<a href="mailto:<%= Html.AttributeEncode(Model) %>"><%= Html.Encode(ViewData.TemplateInfo.FormattedModelValue) %></a> \ No newline at end of file
diff --git a/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/DisplayTemplates/HiddenInput.ascx b/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/DisplayTemplates/HiddenInput.ascx
new file mode 100644
index 00000000..2249a913
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/DisplayTemplates/HiddenInput.ascx
@@ -0,0 +1,4 @@
+<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
+<% if (!ViewData.ModelMetadata.HideSurroundingHtml) { %>
+ <%= Html.Encode(ViewData.TemplateInfo.FormattedModelValue) %>
+<% } %> \ No newline at end of file
diff --git a/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/DisplayTemplates/Html.ascx b/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/DisplayTemplates/Html.ascx
new file mode 100644
index 00000000..ea7f77ea
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/DisplayTemplates/Html.ascx
@@ -0,0 +1,2 @@
+<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
+<%= ViewData.TemplateInfo.FormattedModelValue %> \ No newline at end of file
diff --git a/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/DisplayTemplates/Object.ascx b/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/DisplayTemplates/Object.ascx
new file mode 100644
index 00000000..60aa8a3f
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/DisplayTemplates/Object.ascx
@@ -0,0 +1,25 @@
+<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
+<script runat="server">
+ bool ShouldShow(ModelMetadata metadata) {
+ return metadata.ShowForDisplay
+ && metadata.ModelType != typeof(System.Data.EntityState)
+ && !metadata.IsComplexType
+ && !ViewData.TemplateInfo.Visited(metadata);
+ }
+</script>
+<% if (Model == null) { %>
+ <%= ViewData.ModelMetadata.NullDisplayText %>
+<% } else if (ViewData.TemplateInfo.TemplateDepth > 1) { %>
+ <%= ViewData.ModelMetadata.SimpleDisplayText %>
+<% } else { %>
+ <% foreach (var prop in ViewData.ModelMetadata.Properties.Where(pm => ShouldShow(pm))) { %>
+ <% if (prop.HideSurroundingHtml) { %>
+ <%= Html.Display(prop.PropertyName) %>
+ <% } else { %>
+ <% if (!String.IsNullOrEmpty(prop.GetDisplayName())) { %>
+ <div class="display-label"><%= prop.GetDisplayName() %></div>
+ <% } %>
+ <div class="display-field"><%= Html.Display(prop.PropertyName) %></div>
+ <% } %>
+ <% } %>
+<% } %> \ No newline at end of file
diff --git a/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/DisplayTemplates/String.ascx b/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/DisplayTemplates/String.ascx
new file mode 100644
index 00000000..b7fe2871
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/DisplayTemplates/String.ascx
@@ -0,0 +1,2 @@
+<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
+<%= Html.Encode(ViewData.TemplateInfo.FormattedModelValue) %> \ No newline at end of file
diff --git a/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/DisplayTemplates/Url.ascx b/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/DisplayTemplates/Url.ascx
new file mode 100644
index 00000000..6e11e0c2
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/DisplayTemplates/Url.ascx
@@ -0,0 +1,2 @@
+<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
+<a href="<%= Html.AttributeEncode(Model) %>"><%= Html.Encode(ViewData.TemplateInfo.FormattedModelValue) %></a> \ No newline at end of file
diff --git a/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/EditorTemplates/Boolean.ascx b/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/EditorTemplates/Boolean.ascx
new file mode 100644
index 00000000..278209a1
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/EditorTemplates/Boolean.ascx
@@ -0,0 +1,25 @@
+<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
+<script runat="server">
+ private List<SelectListItem> TriStateValues {
+ get {
+ return new List<SelectListItem> {
+ new SelectListItem { Text = "Not Set", Value = String.Empty, Selected = !Value.HasValue },
+ new SelectListItem { Text = "True", Value = "true", Selected = Value.HasValue && Value.Value },
+ new SelectListItem { Text = "False", Value = "false", Selected = Value.HasValue && !Value.Value },
+ };
+ }
+ }
+ private bool? Value {
+ get {
+ if (ViewData.Model == null) {
+ return null;
+ }
+ return Convert.ToBoolean(ViewData.Model, System.Globalization.CultureInfo.InvariantCulture);
+ }
+ }
+</script>
+<% if (ViewData.ModelMetadata.IsNullableValueType) { %>
+ <%= Html.DropDownList("", TriStateValues, new { @class = "list-box tri-state" }) %>
+<% } else { %>
+ <%= Html.CheckBox("", Value ?? false, new { @class = "check-box" }) %>
+<% } %> \ No newline at end of file
diff --git a/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/EditorTemplates/Collection.ascx b/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/EditorTemplates/Collection.ascx
new file mode 100644
index 00000000..19a542c1
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/EditorTemplates/Collection.ascx
@@ -0,0 +1,16 @@
+<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
+<%
+ if (Model != null) {
+ string oldPrefix = ViewData.TemplateInfo.HtmlFieldPrefix;
+ int index = 0;
+
+ ViewData.TemplateInfo.HtmlFieldPrefix = String.Empty;
+
+ foreach (object item in (IEnumerable)Model) {
+ string fieldName = String.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}[{1}]", oldPrefix, index++);
+ ViewContext.Writer.Write(Html.EditorFor(m => item, null, fieldName));
+ }
+
+ ViewData.TemplateInfo.HtmlFieldPrefix = oldPrefix;
+ }
+%> \ No newline at end of file
diff --git a/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/EditorTemplates/Decimal.ascx b/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/EditorTemplates/Decimal.ascx
new file mode 100644
index 00000000..ed2a12ad
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/EditorTemplates/Decimal.ascx
@@ -0,0 +1,12 @@
+<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
+<script runat="server">
+ private object FormattedValue {
+ get {
+ if (ViewData.TemplateInfo.FormattedModelValue == ViewData.ModelMetadata.Model) {
+ return String.Format(System.Globalization.CultureInfo.CurrentCulture, "{0:0.00}", ViewData.ModelMetadata.Model);
+ }
+ return ViewData.TemplateInfo.FormattedModelValue;
+ }
+ }
+</script>
+<%= Html.TextBox("", FormattedValue, new { @class = "text-box single-line" }) %> \ No newline at end of file
diff --git a/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/EditorTemplates/HiddenInput.ascx b/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/EditorTemplates/HiddenInput.ascx
new file mode 100644
index 00000000..869b21c5
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/EditorTemplates/HiddenInput.ascx
@@ -0,0 +1,18 @@
+<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
+<script runat="server">
+ private object ModelValue {
+ get {
+ if (Model is System.Data.Linq.Binary) {
+ return Convert.ToBase64String(((System.Data.Linq.Binary)Model).ToArray());
+ }
+ if (Model is byte[]) {
+ return Convert.ToBase64String((byte[])Model);
+ }
+ return Model;
+ }
+ }
+</script>
+<% if (!ViewData.ModelMetadata.HideSurroundingHtml) { %>
+ <%= Html.Encode(ViewData.TemplateInfo.FormattedModelValue) %>
+<% } %>
+<%= Html.Hidden("", ModelValue) %> \ No newline at end of file
diff --git a/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/EditorTemplates/MultilineText.ascx b/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/EditorTemplates/MultilineText.ascx
new file mode 100644
index 00000000..4f7b3e81
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/EditorTemplates/MultilineText.ascx
@@ -0,0 +1,2 @@
+<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
+<%= Html.TextArea("", ViewData.TemplateInfo.FormattedModelValue.ToString(), 0, 0, new { @class = "text-box multi-line" }) %> \ No newline at end of file
diff --git a/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/EditorTemplates/Object.ascx b/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/EditorTemplates/Object.ascx
new file mode 100644
index 00000000..9fa4801b
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/EditorTemplates/Object.ascx
@@ -0,0 +1,27 @@
+<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
+<script runat="server">
+ bool ShouldShow(ModelMetadata metadata) {
+ return metadata.ShowForEdit
+ && metadata.ModelType != typeof(System.Data.EntityState)
+ && !metadata.IsComplexType
+ && !ViewData.TemplateInfo.Visited(metadata);
+ }
+</script>
+<% if (ViewData.TemplateInfo.TemplateDepth > 1) { %>
+ <% if (Model == null) { %>
+ <%= ViewData.ModelMetadata.NullDisplayText %>
+ <% } else { %>
+ <%= ViewData.ModelMetadata.SimpleDisplayText %>
+ <% } %>
+<% } else { %>
+ <% foreach (var prop in ViewData.ModelMetadata.Properties.Where(pm => ShouldShow(pm))) { %>
+ <% if (prop.HideSurroundingHtml) { %>
+ <%= Html.Editor(prop.PropertyName) %>
+ <% } else { %>
+ <% if (!String.IsNullOrEmpty(Html.Label(prop.PropertyName).ToHtmlString())) { %>
+ <div class="editor-label"><%= Html.Label(prop.PropertyName) %></div>
+ <% } %>
+ <div class="editor-field"><%= Html.Editor(prop.PropertyName) %> <%= Html.ValidationMessage(prop.PropertyName, "*") %></div>
+ <% } %>
+ <% } %>
+<% } %> \ No newline at end of file
diff --git a/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/EditorTemplates/Password.ascx b/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/EditorTemplates/Password.ascx
new file mode 100644
index 00000000..5cb56297
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/EditorTemplates/Password.ascx
@@ -0,0 +1,2 @@
+<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
+<%= Html.Password("", ViewData.TemplateInfo.FormattedModelValue, new { @class = "text-box single-line password" })%> \ No newline at end of file
diff --git a/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/EditorTemplates/String.ascx b/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/EditorTemplates/String.ascx
new file mode 100644
index 00000000..4b613dd3
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/FuturesFiles/DefaultTemplates/EditorTemplates/String.ascx
@@ -0,0 +1,2 @@
+<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
+<%= Html.TextBox("", ViewData.TemplateInfo.FormattedModelValue, new { @class = "text-box single-line" }) %> \ No newline at end of file
diff --git a/src/Microsoft.Web.Mvc/FuturesFiles/iismap.vbs b/src/Microsoft.Web.Mvc/FuturesFiles/iismap.vbs
new file mode 100644
index 00000000..5f0119dd
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/FuturesFiles/iismap.vbs
@@ -0,0 +1,128 @@
+Const HKEY_LOCAL_MACHINE = &H80000002
+Const MACHINE_NAME = "localhost"
+Const DEFAULT_PATH = "W3SVC"
+Const SCRIPT_MAPS = "ScriptMaps"
+
+Function ExtensionExists(extension, fxVersion)
+ Dim scriptExtension
+ iisScriptMaps = GetScriptMaps()
+
+ For scriptIndex = 0 To UBound(iisScriptMaps)
+ scriptMap = iisScriptMaps(scriptIndex)
+ decomposedScriptMap = Split(scriptMap, ",")
+ scriptExtension = Right(decomposedScriptMap(0), Len(decomposedScriptMap(0))-1)
+ If StrComp(LCase(scriptExtension), LCase(extension)) = 0 Then
+ If InStr(scriptMap, fxVersion) > 0 Then
+ ExtensionExists = true
+ End If
+ End If
+ Next
+
+ ExtensionExists = false
+End Function
+
+Function GetFrameworkPath(fxVersion)
+ strComputer = "."
+ Set registryObj = GetObject("winmgmts:{impersonationLevel=impersonate}!\\" & strComputer & "\root\default:StdRegProv")
+
+ strKeyPath = "SOFTWARE\Microsoft\.NETFramework"
+ strValueName = "InstallRoot"
+ registryObj.GetStringValue HKEY_LOCAL_MACHINE, strKeyPath, strValueName, strValue
+
+ GetFrameworkPath = strValue & fxVersion
+End Function
+
+Function GetFrameworkVersions()
+ strComputer = "."
+ Set registryObj = GetObject("winmgmts:{impersonationLevel=impersonate}!\\" & strComputer & "\root\default:StdRegProv")
+ strKeyPath = "SOFTWARE\Microsoft\.NETFramework"
+ registryObj.EnumKey HKEY_LOCAL_MACHINE, strKeyPath, arrSubKeys
+ Set regex = New RegExp
+ regex.Pattern = "^(v(2|4)\.\d+\.\d+(\.\d+)?)$"
+ regex.Global = True
+ ReDim fxVersions(0)
+
+ For Each key in arrSubKeys
+ If regex.Test(key) Then
+ ReDim Preserve fxVersions(UBound(fxVersions)+1)
+ fxVersions(UBound(fxVersions)-1) = key
+ End If
+ Next
+
+ If UBound(fxVersions) > 0 Then
+ Redim Preserve fxVersions(UBound(fxVersions)-1)
+ End If
+
+ GetFrameworkVersions = fxVersions
+End Function
+
+Function IsValidFrameworkVersion(fxVersion)
+ strComputer = "."
+ Set registryObj = GetObject("winmgmts:{impersonationLevel=impersonate}!\\" & strComputer & "\root\default:StdRegProv")
+ strKeyPath = "SOFTWARE\Microsoft\.NETFramework\" & fxVersion
+ registryObj.EnumKey HKEY_LOCAL_MACHINE, strKeyPath, arrSubKeys
+
+ IsValidFrameworkVersion = Not IsNull(arrSubKeys)
+End Function
+
+Function GetScriptMaps()
+ Dim iisObject
+ Set iisObject = GetObject("IIS://" & MACHINE_NAME & "/" & DEFAULT_PATH)
+ GetScriptMaps = iisObject.Get(SCRIPT_MAPS)
+End Function
+
+Sub RegisterExtension(extension, fxVersion)
+ Dim iisScriptMaps
+
+ Set iisObject = GetObject("IIS://" & MACHINE_NAME & "/" & DEFAULT_PATH)
+ iisScriptMaps = GetScriptMaps()
+
+ If ExtensionExists(extension, fxVersion) Then
+ WScript.Echo extension & " is already registered for .NET Framework " & fxVersion & "."
+ WScript.Quit
+ End If
+
+ ReDim Preserve iisScriptMaps(UBound(iisScriptMaps)+1)
+
+ iisScriptMaps(UBound(iisScriptMaps)) = "." & extension & "," & GetFrameworkPath(fxVersion) & "\aspnet_isapi.dll,1,GET,HEAD,POST"
+ iisObject.Put "ScriptMaps", iisScriptMaps
+ iisObject.Setinfo
+End Sub
+
+Sub SetScriptMaps(ScriptMaps)
+ Dim iisObject
+
+ Set iisObject = GetObject("ISS://" & MACHINE_NAME & "/" & DEFAULT_PATH)
+
+ iisObject.Put SCRIPT_MAPS, ScriptMaps
+ iisObject.Setinfo
+End Sub
+
+Sub UnregisterExtension(extension, fxVersion)
+ Dim newScriptMaps
+
+ Set iisObject = GetObject("IIS://" & MACHINE_NAME & "/" & DEFAULT_PATH)
+ iisScriptMaps = GetScriptMaps()
+
+ If Not ExtensionExists(extension, fxVersion) Then
+ WScript.Echo extension & " is not registered for .NET Framework " & fxVersion & "."
+ WScript.Quit
+ End If
+
+ ReDim newScriptMaps(UBound(iisScriptMaps)-1)
+ Dim newScriptIndex
+ newScriptIndex = 0
+
+ For scriptIndex = 0 To UBound(iisScriptMaps)
+ scriptMap = iisScriptMaps(scriptIndex)
+ decomposedScriptMap = Split(scriptMap, ",")
+ scriptExtension = Right(decomposedScriptMap(0), Len(decomposedScriptMap(0))-1)
+ If (StrComp(LCase(scriptExtension), LCase(extension)) <> 0) Or ((StrComp(LCase(scriptExtension), LCase(extension)) = 0) And (InStr(scriptMap, fxVersion) = 0)) Then
+ newScriptMaps(newScriptIndex) = scriptMap
+ newScriptIndex = newScriptIndex + 1
+ End If
+ Next
+
+ iisObject.Put "ScriptMaps", newScriptMaps
+ iisObject.Setinfo
+End Sub \ No newline at end of file
diff --git a/src/Microsoft.Web.Mvc/FuturesFiles/registermvc.wsf b/src/Microsoft.Web.Mvc/FuturesFiles/registermvc.wsf
new file mode 100644
index 00000000..45cb6b6a
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/FuturesFiles/registermvc.wsf
@@ -0,0 +1,20 @@
+<job>
+ <script language="VBScript" src="iismap.vbs">
+ If WScript.Arguments.Count = 0 Then
+ msg = "You need to specify a .NET Framework version. Please use one of the following:" & VbCrLf
+ For Each fxVersion in GetFrameworkVersions()
+ msg = msg & fxVersion & VbCrLf
+ Next
+ WScript.Echo msg
+ WScript.Quit
+ ElseIf Not IsValidFrameworkVersion(WScript.Arguments(0)) Then
+ msg = WScript.Arguments(0) & " is not a valid .NET Framework version. Please use one of the following:" & VbCrLf
+ For Each fxVersion in GetFrameworkVersions()
+ msg = msg & fxVersion & VbCrLf
+ Next
+ WScript.Echo msg
+ WScript.Quit
+ End If
+ RegisterExtension "mvc", WScript.Arguments(0)
+ </script>
+</job> \ No newline at end of file
diff --git a/src/Microsoft.Web.Mvc/FuturesFiles/unregistermvc.wsf b/src/Microsoft.Web.Mvc/FuturesFiles/unregistermvc.wsf
new file mode 100644
index 00000000..7ca38741
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/FuturesFiles/unregistermvc.wsf
@@ -0,0 +1,20 @@
+<job>
+ <script language="VBScript" src="iismap.vbs">
+ If WScript.Arguments.Count = 0 Then
+ msg = "You need to specify a .NET Framework version. Please use one of the following: " & VbCrLf
+ For Each fxVersion in GetFrameworkVersions()
+ msg = msg & fxVersion & VbCrLf
+ Next
+ WScript.Echo msg
+ WScript.Quit
+ ElseIf Not IsValidFrameworkVersion(WScript.Arguments(0)) Then
+ msg = WScript.Arguments(0) & " is not a valid .NET Framework version. Please use one of the following: " & VbCrLf
+ For Each fxVersion in GetFrameworkVersions()
+ msg = msg & fxVersion & VbCrLf
+ Next
+ WScript.Echo msg
+ WScript.Quit
+ End If
+ UnregisterExtension "mvc", WScript.Arguments(0)
+ </script>
+</job> \ No newline at end of file
diff --git a/src/Microsoft.Web.Mvc/GlobalSuppressions.cs b/src/Microsoft.Web.Mvc/GlobalSuppressions.cs
new file mode 100644
index 00000000..eff529bb
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/GlobalSuppressions.cs
@@ -0,0 +1,15 @@
+using System.Diagnostics.CodeAnalysis;
+
+// This file is used by Code Analysis to maintain SuppressMessage
+// attributes that are applied to this project.
+// Project-level suppressions either have no target or are given
+// a specific target and scoped to a namespace, type, member, etc.
+//
+// To add a suppression to this file, right-click the message in the
+// Error List, point to "Suppress Message(s)", and click
+// "In Project Suppression File".
+// You do not need to add suppressions to this file manually.
+
+[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "Microsoft.Web.Mvc.Html", Justification = "Helpers reside within a separate namespace to support alternate helper classes.")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId = "whitelist", Scope = "resource", Target = "Microsoft.Web.Mvc.Properties.MvcResources.resources", Justification = "The spelling is correct.")]
+[assembly: SuppressMessage("Microsoft.Design", "CA2210:AssembliesShouldHaveValidStrongNames", Justification = "Assembly is delay-signed.")]
diff --git a/src/Microsoft.Web.Mvc/Html/HtmlHelperExtensions.cs b/src/Microsoft.Web.Mvc/Html/HtmlHelperExtensions.cs
new file mode 100644
index 00000000..80a0a3fc
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Html/HtmlHelperExtensions.cs
@@ -0,0 +1,530 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq.Expressions;
+using System.Web.Mvc;
+using System.Web.Mvc.Html;
+using System.Web.Routing;
+
+namespace Microsoft.Web.Mvc.Html
+{
+ // This class contains definitions of single "super" HTML helper methods, which rely on
+ // CLR 4's default values for method parameters to make them more consumable. Methods
+ // which previously took an HTML attributes object/dictionary now have their legal
+ // attribute values all available as optional parameters. Some attributes are only
+ // applicable for some DTDs; deprecated attributes (like "align" on input) were
+ // specifically excluded.
+ //
+ // Since htmlAttributes was very often the last parameter to HTML helper methods,
+ // converting to these new syntaxes should be as simple as converting your anonymous
+ // object htmlAttributes collections into optional parameters.
+ //
+ // Where there were two overloads for route values (anonymous object and dictionary),
+ // there is only a single overload now, which takes type object. If what you pass is
+ // a dictionary of the correct type, then we'll use that; otherwise, we'll assume it's
+ // an anonymous object and create the dictionary for you. This should make it simple
+ // to port methods using route values, as they should just continue to work as before.
+ //
+ // Some HTML helpers did not take HTML attributes parameters. They are recreated here
+ // so that the user does not have to import both System.Web.Mvc.Html as well as this
+ // namespace, since the purpose of these methods is to get rid of all the overloads
+ // for the built-in HTML helpers.
+ //
+ // The legal attribute values were derived from: http://www.w3schools.com/tags/
+
+ public static class HtmlHelperExtensions
+ {
+ // ChildActionExtensions
+
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "The purpose of these helpers is to use default parameters to simplify common usage.")]
+ public static MvcHtmlString Action(this HtmlHelper htmlHelper, string actionName, string controllerName = null, object routeValues = null)
+ {
+ return ChildActionExtensions.Action(
+ htmlHelper,
+ actionName,
+ controllerName,
+ routeValues as IDictionary<string, object> ?? new RouteValueDictionary(routeValues));
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "The purpose of these helpers is to use default parameters to simplify common usage.")]
+ public static void RenderAction(this HtmlHelper htmlHelper, string actionName, string controllerName = null, object routeValues = null)
+ {
+ ChildActionExtensions.RenderAction(
+ htmlHelper,
+ actionName,
+ controllerName,
+ routeValues as IDictionary<string, object> ?? new RouteValueDictionary(routeValues));
+ }
+
+ // DisplayExtensions
+
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "The purpose of these helpers is to use default parameters to simplify common usage.")]
+ public static MvcHtmlString Display(this HtmlHelper htmlHelper, string expression, string templateName = null, string htmlFieldName = null)
+ {
+ return DisplayExtensions.Display(htmlHelper, expression, templateName, htmlFieldName);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "The purpose of these helpers is to use default parameters to simplify common usage.")]
+ public static MvcHtmlString DisplayFor<TModel, TValue>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TValue>> expression, string templateName = null, string htmlFieldName = null)
+ {
+ return DisplayExtensions.DisplayFor(htmlHelper, expression, templateName, htmlFieldName);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "The purpose of these helpers is to use default parameters to simplify common usage.")]
+ public static MvcHtmlString DisplayForModel(this HtmlHelper htmlHelper, string templateName = null, string htmlFieldName = null)
+ {
+ return DisplayExtensions.DisplayForModel(htmlHelper, templateName, htmlFieldName);
+ }
+
+ // DisplayTextExtensions
+
+ public static MvcHtmlString DisplayText(this HtmlHelper htmlHelper, string name)
+ {
+ return DisplayTextExtensions.DisplayText(htmlHelper, name);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString DisplayTextFor<TModel, TResult>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TResult>> expression)
+ {
+ return DisplayTextExtensions.DisplayTextFor(htmlHelper, expression);
+ }
+
+ // EditorExtensions
+
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "The purpose of these helpers is to use default parameters to simplify common usage.")]
+ public static MvcHtmlString Editor(this HtmlHelper htmlHelper, string expression, string templateName = null, string htmlFieldName = null)
+ {
+ return EditorExtensions.Editor(htmlHelper, expression, templateName, htmlFieldName);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "The purpose of these helpers is to use default parameters to simplify common usage.")]
+ public static MvcHtmlString EditorFor<TModel, TValue>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TValue>> expression, string templateName = null, string htmlFieldName = null)
+ {
+ return EditorExtensions.EditorFor(htmlHelper, expression, templateName, htmlFieldName);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "The purpose of these helpers is to use default parameters to simplify common usage.")]
+ public static MvcHtmlString EditorForModel(this HtmlHelper htmlHelper, string templateName = null, string htmlFieldName = null)
+ {
+ return EditorExtensions.EditorForModel(htmlHelper, templateName, htmlFieldName);
+ }
+
+ // FormExtensions
+
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "The purpose of these helpers is to use default parameters to simplify common usage.")]
+ public static MvcForm BeginForm(this HtmlHelper htmlHelper, string actionName = null, string controllerName = null, object routeValues = null, FormMethod method = FormMethod.Post, string accept = null, string acceptCharset = null, string cssClass = null, string dir = null, string encType = null, string id = null, string lang = null, string name = null, string style = null, string title = null)
+ {
+ return htmlHelper.BeginForm(
+ actionName,
+ controllerName,
+ routeValues as RouteValueDictionary ?? new RouteValueDictionary(routeValues),
+ method,
+ FormAttributes(accept, acceptCharset, cssClass, dir, encType, id, lang, name, style, title));
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "The purpose of these helpers is to use default parameters to simplify common usage.")]
+ public static MvcForm BeginRouteForm(this HtmlHelper htmlHelper, string routeName, RouteValueDictionary routeValues = null, FormMethod method = FormMethod.Post, string accept = null, string acceptCharset = null, string cssClass = null, string dir = null, string encType = null, string id = null, string lang = null, string name = null, string style = null, string title = null)
+ {
+ return htmlHelper.BeginRouteForm(
+ routeName,
+ routeValues ?? new RouteValueDictionary(routeValues),
+ method,
+ FormAttributes(accept, acceptCharset, cssClass, dir, encType, id, lang, name, style, title));
+ }
+
+ public static void EndForm(this HtmlHelper htmlHelper)
+ {
+ System.Web.Mvc.Html.FormExtensions.EndForm(htmlHelper);
+ }
+
+ // InputExtensions
+
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "The purpose of these helpers is to use default parameters to simplify common usage.")]
+ public static MvcHtmlString CheckBox(this HtmlHelper htmlHelper, string name, bool? isChecked = null, string cssClass = null, string dir = null, bool disabled = false, string id = null, string lang = null, int? maxLength = null, bool readOnly = false, int? size = null, string style = null, int? tabIndex = null, string title = null)
+ {
+ var htmlAttributes = InputAttributes(cssClass, dir, disabled, id, lang, maxLength, readOnly, size, style, tabIndex, title);
+
+ return isChecked.HasValue
+ ? htmlHelper.CheckBox(name, isChecked.Value, htmlAttributes)
+ : htmlHelper.CheckBox(name, htmlAttributes);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "The purpose of these helpers is to use default parameters to simplify common usage.")]
+ public static MvcHtmlString CheckBoxFor<TModel>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, bool>> expression, string cssClass = null, string dir = null, bool disabled = false, string id = null, string lang = null, int? maxLength = null, bool readOnly = false, int? size = null, string style = null, int? tabIndex = null, string title = null)
+ {
+ return htmlHelper.CheckBoxFor(
+ expression,
+ InputAttributes(cssClass, dir, disabled, id, lang, maxLength, readOnly, size, style, tabIndex, title));
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "The purpose of these helpers is to use default parameters to simplify common usage.")]
+ public static MvcHtmlString Hidden(this HtmlHelper htmlHelper, string name, object value = null, string cssClass = null, string id = null, string style = null)
+ {
+ return htmlHelper.Hidden(name, value, Attributes(cssClass, id, style));
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "The purpose of these helpers is to use default parameters to simplify common usage.")]
+ public static MvcHtmlString HiddenFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, string cssClass = null, string id = null, string style = null)
+ {
+ return htmlHelper.HiddenFor(expression, Attributes(cssClass, id, style));
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "The purpose of these helpers is to use default parameters to simplify common usage.")]
+ public static MvcHtmlString Password(this HtmlHelper htmlHelper, string name, object value = null, string cssClass = null, string dir = null, bool disabled = false, string id = null, string lang = null, int? maxLength = null, bool readOnly = false, int? size = null, string style = null, int? tabIndex = null, string title = null)
+ {
+ return htmlHelper.Password(
+ name,
+ value,
+ InputAttributes(cssClass, dir, disabled, id, lang, maxLength, readOnly, size, style, tabIndex, title));
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "The purpose of these helpers is to use default parameters to simplify common usage.")]
+ public static MvcHtmlString PasswordFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, string cssClass = null, bool disabled = false, string dir = null, string id = null, string lang = null, int? maxLength = null, bool readOnly = false, int? size = null, string style = null, int? tabIndex = null, string title = null)
+ {
+ return htmlHelper.PasswordFor(
+ expression,
+ InputAttributes(cssClass, dir, disabled, id, lang, maxLength, readOnly, size, style, tabIndex, title));
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "The purpose of these helpers is to use default parameters to simplify common usage.")]
+ public static MvcHtmlString RadioButton(this HtmlHelper htmlHelper, string name, object value, bool? isChecked = null, string cssClass = null, string dir = null, bool disabled = false, string id = null, string lang = null, int? maxLength = null, bool readOnly = false, int? size = null, string style = null, int? tabIndex = null, string title = null)
+ {
+ var htmlAttributes = InputAttributes(cssClass, dir, disabled, id, lang, maxLength, readOnly, size, style, tabIndex, title);
+
+ return isChecked.HasValue
+ ? htmlHelper.RadioButton(name, value, isChecked.Value, htmlAttributes)
+ : htmlHelper.RadioButton(name, value, htmlAttributes);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "The purpose of these helpers is to use default parameters to simplify common usage.")]
+ public static MvcHtmlString RadioButtonFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, object value, string cssClass = null, string dir = null, bool disabled = false, string id = null, string lang = null, int? maxLength = null, bool readOnly = false, int? size = null, string style = null, int? tabIndex = null, string title = null)
+ {
+ return htmlHelper.RadioButtonFor(
+ expression,
+ value,
+ InputAttributes(cssClass, dir, disabled, id, lang, maxLength, readOnly, size, style, tabIndex, title));
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "The purpose of these helpers is to use default parameters to simplify common usage.")]
+ public static MvcHtmlString TextBox(this HtmlHelper htmlHelper, string name, object value = null, string cssClass = null, string dir = null, bool disabled = false, string id = null, string lang = null, int? maxLength = null, bool readOnly = false, int? size = null, string style = null, int? tabIndex = null, string title = null)
+ {
+ return htmlHelper.TextBox(
+ name,
+ value,
+ InputAttributes(cssClass, dir, disabled, id, lang, maxLength, readOnly, size, style, tabIndex, title));
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "The purpose of these helpers is to use default parameters to simplify common usage.")]
+ public static MvcHtmlString TextBoxFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, string cssClass = null, string dir = null, bool disabled = false, string id = null, string lang = null, int? maxLength = null, bool readOnly = false, int? size = null, string style = null, int? tabIndex = null, string title = null)
+ {
+ return htmlHelper.TextBoxFor(
+ expression,
+ InputAttributes(cssClass, dir, disabled, id, lang, maxLength, readOnly, size, style, tabIndex, title));
+ }
+
+ // LabelExtensions
+
+ public static MvcHtmlString Label(this HtmlHelper htmlHelper, string expression)
+ {
+ return LabelExtensions.Label(htmlHelper, expression);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString LabelFor<TModel, TValue>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TValue>> expression)
+ {
+ return LabelExtensions.LabelFor(htmlHelper, expression);
+ }
+
+ public static MvcHtmlString LabelForModel(this HtmlHelper htmlHelper)
+ {
+ return LabelExtensions.LabelForModel(htmlHelper);
+ }
+
+ // LinkExtensions
+
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "The purpose of these helpers is to use default parameters to simplify common usage.")]
+ public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName = null, string protocol = null, string hostName = null, string fragment = null, object routeValues = null, string accessKey = null, string charset = null, string coords = null, string cssClass = null, string dir = null, string hrefLang = null, string id = null, string lang = null, string name = null, string rel = null, string rev = null, string shape = null, string style = null, string target = null, string title = null)
+ {
+ return htmlHelper.ActionLink(
+ linkText,
+ actionName,
+ controllerName,
+ protocol,
+ hostName,
+ fragment,
+ routeValues as RouteValueDictionary ?? new RouteValueDictionary(routeValues),
+ AnchorAttributes(accessKey, charset, coords, cssClass, dir, hrefLang, id, lang, name, rel, rev, shape, style, target, title));
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "The purpose of these helpers is to use default parameters to simplify common usage.")]
+ public static MvcHtmlString RouteLink(this HtmlHelper htmlHelper, string linkText, string routeName, string protocol = null, string hostName = null, string fragment = null, object routeValues = null, string accessKey = null, string charset = null, string coords = null, string cssClass = null, string dir = null, string hrefLang = null, string id = null, string lang = null, string name = null, string rel = null, string rev = null, string shape = null, string style = null, string target = null, string title = null)
+ {
+ return htmlHelper.RouteLink(
+ linkText,
+ routeName,
+ protocol,
+ hostName,
+ fragment,
+ routeValues as RouteValueDictionary ?? new RouteValueDictionary(routeValues),
+ AnchorAttributes(accessKey, charset, coords, cssClass, dir, hrefLang, id, lang, name, rel, rev, shape, style, target, title));
+ }
+
+ // PartialExtensions
+
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "The purpose of these helpers is to use default parameters to simplify common usage.")]
+ public static MvcHtmlString Partial(this HtmlHelper htmlHelper, string partialViewName, object model = null, ViewDataDictionary viewData = null)
+ {
+ return PartialExtensions.Partial(
+ htmlHelper,
+ partialViewName,
+ model,
+ viewData ?? htmlHelper.ViewData);
+ }
+
+ // RenderPartialExtensions
+
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "The purpose of these helpers is to use default parameters to simplify common usage.")]
+ public static void RenderPartial(this HtmlHelper htmlHelper, string partialViewName, object model = null, ViewDataDictionary viewData = null)
+ {
+ RenderPartialExtensions.RenderPartial(
+ htmlHelper,
+ partialViewName,
+ model,
+ viewData ?? htmlHelper.ViewData);
+ }
+
+ // SelectExtensions
+
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "The purpose of these helpers is to use default parameters to simplify common usage.")]
+ public static MvcHtmlString DropDownList(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList = null, string optionLabel = null, string cssClass = null, string dir = null, bool disabled = false, string id = null, string lang = null, int? size = null, string style = null, int? tabIndex = null, string title = null)
+ {
+ return htmlHelper.DropDownList(
+ name,
+ selectList,
+ optionLabel,
+ SelectAttributes(cssClass, dir, disabled, id, lang, size, style, tabIndex, title));
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "The purpose of these helpers is to use default parameters to simplify common usage.")]
+ public static MvcHtmlString DropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList = null, string optionLabel = null, string cssClass = null, string dir = null, bool disabled = false, string id = null, string lang = null, int? size = null, string style = null, int? tabIndex = null, string title = null)
+ {
+ return htmlHelper.DropDownListFor(
+ expression,
+ selectList,
+ optionLabel,
+ SelectAttributes(cssClass, dir, disabled, id, lang, size, style, tabIndex, title));
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "The purpose of these helpers is to use default parameters to simplify common usage.")]
+ public static MvcHtmlString ListBox(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList = null, string cssClass = null, string dir = null, bool disabled = false, string id = null, string lang = null, int? size = null, string style = null, int? tabIndex = null, string title = null)
+ {
+ return htmlHelper.ListBox(
+ name,
+ selectList,
+ SelectAttributes(cssClass, dir, disabled, id, lang, size, style, tabIndex, title));
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "The purpose of these helpers is to use default parameters to simplify common usage.")]
+ public static MvcHtmlString ListBoxFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList = null, string cssClass = null, string dir = null, bool disabled = false, string id = null, string lang = null, int? size = null, string style = null, int? tabIndex = null, string title = null)
+ {
+ return htmlHelper.ListBoxFor(
+ expression,
+ selectList,
+ SelectAttributes(cssClass, dir, disabled, id, lang, size, style, tabIndex, title));
+ }
+
+ // TextAreaExtensions
+
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "The purpose of these helpers is to use default parameters to simplify common usage.")]
+ public static MvcHtmlString TextArea(this HtmlHelper htmlHelper, string name, string value = null, string accessKey = null, string cssClass = null, int? cols = null, string dir = null, bool disabled = false, string id = null, string lang = null, bool readOnly = false, int? rows = null, string style = null, int? tabIndex = null, string title = null)
+ {
+ return htmlHelper.TextArea(
+ name,
+ value,
+ TextAreaAttributes(accessKey, cssClass, cols, dir, disabled, id, lang, readOnly, rows, style, tabIndex, title));
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "The purpose of these helpers is to use default parameters to simplify common usage.")]
+ public static MvcHtmlString TextAreaFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, string accessKey = null, string cssClass = null, int? cols = null, string dir = null, bool disabled = false, string id = null, string lang = null, bool readOnly = false, int? rows = null, string style = null, int? tabIndex = null, string title = null)
+ {
+ return htmlHelper.TextAreaFor(
+ expression,
+ TextAreaAttributes(accessKey, cssClass, cols, dir, disabled, id, lang, readOnly, rows, style, tabIndex, title));
+ }
+
+ // ValidationExtensions
+
+ public static void Validate(this HtmlHelper htmlHelper, string modelName)
+ {
+ ValidationExtensions.Validate(htmlHelper, modelName);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static void ValidateFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression)
+ {
+ ValidationExtensions.ValidateFor(htmlHelper, expression);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "The purpose of these helpers is to use default parameters to simplify common usage.")]
+ [SuppressMessage("Microsoft.Naming", "CA1719:ParameterNamesShouldNotMatchMemberNames", MessageId = "2#", Justification = "This API has already shipped.")]
+ public static MvcHtmlString ValidationMessage(this HtmlHelper htmlHelper, string modelName, string validationMessage = null, string cssClass = null, string dir = null, string id = null, string lang = null, string style = null, string title = null)
+ {
+ return htmlHelper.ValidationMessage(
+ modelName,
+ validationMessage,
+ SpanAttributes(cssClass, dir, id, lang, style, title));
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "The purpose of these helpers is to use default parameters to simplify common usage.")]
+ public static MvcHtmlString ValidationMessageFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, string validationMessage = null, string cssClass = null, string dir = null, string id = null, string lang = null, string style = null, string title = null)
+ {
+ return htmlHelper.ValidationMessageFor(
+ expression,
+ validationMessage,
+ SpanAttributes(cssClass, dir, id, lang, style, title));
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "The purpose of these helpers is to use default parameters to simplify common usage.")]
+ public static MvcHtmlString ValidationSummary(this HtmlHelper htmlHelper, string message = null, bool excludePropertyErrors = false, string cssClass = null, string dir = null, string id = null, string lang = null, string style = null, string title = null)
+ {
+ return htmlHelper.ValidationSummary(
+ excludePropertyErrors,
+ message,
+ SpanAttributes(cssClass, dir, id, lang, style, title));
+ }
+
+ // Helper methods
+
+ private static void AddOptional(this IDictionary<string, object> dictionary, string key, bool value)
+ {
+ if (value)
+ {
+ dictionary[key] = key;
+ }
+ }
+
+ private static void AddOptional(this IDictionary<string, object> dictionary, string key, object value)
+ {
+ if (value != null)
+ {
+ dictionary[key] = value;
+ }
+ }
+
+ private static IDictionary<string, object> Attributes(string cssClass, string id, string style)
+ {
+ var htmlAttributes = new RouteValueDictionary();
+
+ htmlAttributes.AddOptional("class", cssClass);
+ htmlAttributes.AddOptional("id", id);
+ htmlAttributes.AddOptional("style", style);
+
+ return htmlAttributes;
+ }
+
+ private static IDictionary<string, object> AnchorAttributes(string accessKey, string charset, string coords, string cssClass, string dir, string hrefLang, string id, string lang, string name, string rel, string rev, string shape, string style, string target, string title)
+ {
+ var htmlAttributes = Attributes(cssClass, id, style);
+
+ htmlAttributes.AddOptional("accesskey", accessKey);
+ htmlAttributes.AddOptional("charset", charset);
+ htmlAttributes.AddOptional("coords", coords);
+ htmlAttributes.AddOptional("dir", dir);
+ htmlAttributes.AddOptional("hreflang", hrefLang);
+ htmlAttributes.AddOptional("lang", lang);
+ htmlAttributes.AddOptional("name", name);
+ htmlAttributes.AddOptional("rel", rel);
+ htmlAttributes.AddOptional("rev", rev);
+ htmlAttributes.AddOptional("shape", shape);
+ htmlAttributes.AddOptional("target", target);
+ htmlAttributes.AddOptional("title", title);
+
+ return htmlAttributes;
+ }
+
+ private static IDictionary<string, object> FormAttributes(string accept, string acceptCharset, string cssClass, string dir, string encType, string id, string lang, string name, string style, string title)
+ {
+ var htmlAttributes = Attributes(cssClass, id, style);
+
+ htmlAttributes.AddOptional("accept", accept);
+ htmlAttributes.AddOptional("accept-charset", acceptCharset);
+ htmlAttributes.AddOptional("dir", dir);
+ htmlAttributes.AddOptional("enctype", encType);
+ htmlAttributes.AddOptional("lang", lang);
+ htmlAttributes.AddOptional("name", name);
+ htmlAttributes.AddOptional("title", title);
+
+ return htmlAttributes;
+ }
+
+ private static IDictionary<string, object> InputAttributes(string cssClass, string dir, bool disabled, string id, string lang, int? maxLength, bool readOnly, int? size, string style, int? tabIndex, string title)
+ {
+ var htmlAttributes = Attributes(cssClass, id, style);
+
+ htmlAttributes.AddOptional("dir", dir);
+ htmlAttributes.AddOptional("disabled", disabled);
+ htmlAttributes.AddOptional("lang", lang);
+ htmlAttributes.AddOptional("maxlength", maxLength);
+ htmlAttributes.AddOptional("readonly", readOnly);
+ htmlAttributes.AddOptional("size", size);
+ htmlAttributes.AddOptional("tabindex", tabIndex);
+ htmlAttributes.AddOptional("title", title);
+
+ return htmlAttributes;
+ }
+
+ private static IDictionary<string, object> SelectAttributes(string cssClass, string dir, bool disabled, string id, string lang, int? size, string style, int? tabIndex, string title)
+ {
+ var htmlAttributes = Attributes(cssClass, id, style);
+
+ htmlAttributes.AddOptional("dir", dir);
+ htmlAttributes.AddOptional("disabled", disabled);
+ htmlAttributes.AddOptional("lang", lang);
+ htmlAttributes.AddOptional("size", size);
+ htmlAttributes.AddOptional("tabindex", tabIndex);
+ htmlAttributes.AddOptional("title", title);
+
+ return htmlAttributes;
+ }
+
+ private static IDictionary<string, object> SpanAttributes(string cssClass, string dir, string id, string lang, string style, string title)
+ {
+ var htmlAttributes = Attributes(cssClass, id, style);
+
+ htmlAttributes.AddOptional("dir", dir);
+ htmlAttributes.AddOptional("lang", lang);
+ htmlAttributes.AddOptional("title", title);
+
+ return htmlAttributes;
+ }
+
+ private static IDictionary<string, object> TextAreaAttributes(string accessKey, string cssClass, int? cols, string dir, bool disabled, string id, string lang, bool readOnly, int? rows, string style, int? tabIndex, string title)
+ {
+ var htmlAttributes = Attributes(cssClass, id, style);
+
+ htmlAttributes.AddOptional("accesskey", accessKey);
+ htmlAttributes.AddOptional("cols", cols);
+ htmlAttributes.AddOptional("dir", dir);
+ htmlAttributes.AddOptional("disabled", disabled);
+ htmlAttributes.AddOptional("lang", lang);
+ htmlAttributes.AddOptional("readonly", readOnly);
+ htmlAttributes.AddOptional("rows", rows);
+ htmlAttributes.AddOptional("style", style);
+ htmlAttributes.AddOptional("tabindex", tabIndex);
+ htmlAttributes.AddOptional("title", title);
+
+ return htmlAttributes;
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/HtmlButtonType.cs b/src/Microsoft.Web.Mvc/HtmlButtonType.cs
new file mode 100644
index 00000000..95c6b941
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/HtmlButtonType.cs
@@ -0,0 +1,9 @@
+namespace Microsoft.Web.Mvc
+{
+ public enum HtmlButtonType
+ {
+ Button,
+ Submit,
+ Reset,
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/IMachineKey.cs b/src/Microsoft.Web.Mvc/IMachineKey.cs
new file mode 100644
index 00000000..d87a67bc
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/IMachineKey.cs
@@ -0,0 +1,12 @@
+using System.Web.Security;
+
+namespace Microsoft.Web.Mvc
+{
+ // Used for mocking out the static MachineKey type
+
+ internal interface IMachineKey
+ {
+ byte[] Decode(string encodedData, MachineKeyProtection protectionOption);
+ string Encode(byte[] data, MachineKeyProtection protectionOption);
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ImageExtensions.cs b/src/Microsoft.Web.Mvc/ImageExtensions.cs
new file mode 100644
index 00000000..48f3ec92
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ImageExtensions.cs
@@ -0,0 +1,82 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Web.Mvc;
+using Microsoft.Web.Mvc.Properties;
+
+namespace Microsoft.Web.Mvc
+{
+ public static class ImageExtensions
+ {
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "The return value is not a regular URL since it may contain ~/ ASP.NET-specific characters")]
+ public static MvcHtmlString Image(this HtmlHelper helper, string imageRelativeUrl)
+ {
+ return Image(helper, imageRelativeUrl, null, null);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "Required for Extension Method")]
+ public static MvcHtmlString Image(this HtmlHelper helper, string imageRelativeUrl, string alt)
+ {
+ return Image(helper, imageRelativeUrl, alt, null);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "The return value is not a regular URL since it may contain ~/ ASP.NET-specific characters")]
+ public static MvcHtmlString Image(this HtmlHelper helper, string imageRelativeUrl, string alt, object htmlAttributes)
+ {
+ return Image(helper, imageRelativeUrl, alt, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "The return value is not a regular URL since it may contain ~/ ASP.NET-specific characters")]
+ public static MvcHtmlString Image(this HtmlHelper helper, string imageRelativeUrl, object htmlAttributes)
+ {
+ return Image(helper, imageRelativeUrl, null, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "The return value is not a regular URL since it may contain ~/ ASP.NET-specific characters")]
+ public static MvcHtmlString Image(this HtmlHelper helper, string imageRelativeUrl, IDictionary<string, object> htmlAttributes)
+ {
+ return Image(helper, imageRelativeUrl, null, htmlAttributes);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "The return value is not a regular URL since it may contain ~/ ASP.NET-specific characters")]
+ public static MvcHtmlString Image(this HtmlHelper helper, string imageRelativeUrl, string alt, IDictionary<string, object> htmlAttributes)
+ {
+ if (String.IsNullOrEmpty(imageRelativeUrl))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "imageRelativeUrl");
+ }
+
+ string imageUrl = UrlHelper.GenerateContentUrl(imageRelativeUrl, helper.ViewContext.HttpContext);
+ return MvcHtmlString.Create(Image(imageUrl, alt, htmlAttributes).ToString(TagRenderMode.SelfClosing));
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "0#", Justification = "The value is a not a regular URL since it may contain ~/ ASP.NET-specific characters")]
+ public static TagBuilder Image(string imageUrl, string alt, IDictionary<string, object> htmlAttributes)
+ {
+ if (String.IsNullOrEmpty(imageUrl))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "imageUrl");
+ }
+
+ TagBuilder imageTag = new TagBuilder("img");
+
+ if (!String.IsNullOrEmpty(imageUrl))
+ {
+ imageTag.MergeAttribute("src", imageUrl);
+ }
+
+ if (!String.IsNullOrEmpty(alt))
+ {
+ imageTag.MergeAttribute("alt", alt);
+ }
+
+ imageTag.MergeAttributes(htmlAttributes, true);
+
+ if (imageTag.Attributes.ContainsKey("alt") && !imageTag.Attributes.ContainsKey("title"))
+ {
+ imageTag.MergeAttribute("title", (imageTag.Attributes["alt"] ?? String.Empty));
+ }
+ return imageTag;
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Internal/ExpressionHelper.cs b/src/Microsoft.Web.Mvc/Internal/ExpressionHelper.cs
new file mode 100644
index 00000000..cdac5111
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Internal/ExpressionHelper.cs
@@ -0,0 +1,150 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Web.Mvc;
+using System.Web.Routing;
+using Microsoft.Web.Mvc.Properties;
+
+namespace Microsoft.Web.Mvc.Internal
+{
+ public static class ExpressionHelper
+ {
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Users cannot use anonymous methods with the LambdaExpression type")]
+ public static RouteValueDictionary GetRouteValuesFromExpression<TController>(Expression<Action<TController>> action) where TController : Controller
+ {
+ if (action == null)
+ {
+ throw new ArgumentNullException("action");
+ }
+
+ MethodCallExpression call = action.Body as MethodCallExpression;
+ if (call == null)
+ {
+ throw new ArgumentException(MvcResources.ExpressionHelper_MustBeMethodCall, "action");
+ }
+
+ string controllerName = typeof(TController).Name;
+ if (!controllerName.EndsWith("Controller", StringComparison.OrdinalIgnoreCase))
+ {
+ throw new ArgumentException(MvcResources.ExpressionHelper_TargetMustEndInController, "action");
+ }
+ controllerName = controllerName.Substring(0, controllerName.Length - "Controller".Length);
+ if (controllerName.Length == 0)
+ {
+ throw new ArgumentException(MvcResources.ExpressionHelper_CannotRouteToController, "action");
+ }
+
+ // TODO: How do we know that this method is even web callable?
+ // For now, we just let the call itself throw an exception.
+
+ string actionName = GetTargetActionName(call.Method);
+
+ var rvd = new RouteValueDictionary();
+ rvd.Add("Controller", controllerName);
+ rvd.Add("Action", actionName);
+
+ ActionLinkAreaAttribute areaAttr = typeof(TController).GetCustomAttributes(typeof(ActionLinkAreaAttribute), true /* inherit */).FirstOrDefault() as ActionLinkAreaAttribute;
+ if (areaAttr != null)
+ {
+ string areaName = areaAttr.Area;
+ rvd.Add("Area", areaName);
+ }
+
+ AddParameterValuesFromExpressionToDictionary(rvd, call);
+ return rvd;
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Users cannot use anonymous methods with the LambdaExpression type")]
+ public static string GetInputName<TModel, TProperty>(Expression<Func<TModel, TProperty>> expression)
+ {
+ if (expression.Body.NodeType == ExpressionType.Call)
+ {
+ MethodCallExpression methodCallExpression = (MethodCallExpression)expression.Body;
+ string name = GetInputName(methodCallExpression);
+ return name.Substring(expression.Parameters[0].Name.Length + 1);
+ }
+ return expression.Body.ToString().Substring(expression.Parameters[0].Name.Length + 1);
+ }
+
+ private static string GetInputName(MethodCallExpression expression)
+ {
+ // p => p.Foo.Bar().Baz.ToString() => p.Foo OR throw...
+
+ MethodCallExpression methodCallExpression = expression.Object as MethodCallExpression;
+ if (methodCallExpression != null)
+ {
+ return GetInputName(methodCallExpression);
+ }
+ return expression.Object.ToString();
+ }
+
+ // This method contains some heuristics that will help determine the correct action name from a given MethodInfo
+ // assuming the default sync / async invokers are in use. The logic's not foolproof, but it should be good enough
+ // for most uses.
+ private static string GetTargetActionName(MethodInfo methodInfo)
+ {
+ string methodName = methodInfo.Name;
+
+ // do we know this not to be an action?
+ if (methodInfo.IsDefined(typeof(NonActionAttribute), true /* inherit */))
+ {
+ throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture,
+ MvcResources.ExpressionHelper_CannotCallNonAction, methodName));
+ }
+
+ // has this been renamed?
+ ActionNameAttribute nameAttr = methodInfo.GetCustomAttributes(typeof(ActionNameAttribute), true /* inherit */).OfType<ActionNameAttribute>().FirstOrDefault();
+ if (nameAttr != null)
+ {
+ return nameAttr.Name;
+ }
+
+ // targeting an async action?
+ if (methodInfo.DeclaringType.IsSubclassOf(typeof(AsyncController)))
+ {
+ if (methodName.EndsWith("Async", StringComparison.OrdinalIgnoreCase))
+ {
+ return methodName.Substring(0, methodName.Length - "Async".Length);
+ }
+ if (methodName.EndsWith("Completed", StringComparison.OrdinalIgnoreCase))
+ {
+ throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture,
+ MvcResources.ExpressionHelper_CannotCallCompletedMethod, methodName));
+ }
+ }
+
+ // fallback
+ return methodName;
+ }
+
+ private static void AddParameterValuesFromExpressionToDictionary(RouteValueDictionary rvd, MethodCallExpression call)
+ {
+ ParameterInfo[] parameters = call.Method.GetParameters();
+
+ if (parameters.Length > 0)
+ {
+ for (int i = 0; i < parameters.Length; i++)
+ {
+ Expression arg = call.Arguments[i];
+ object value = null;
+ ConstantExpression ce = arg as ConstantExpression;
+ if (ce != null)
+ {
+ // If argument is a constant expression, just get the value
+ value = ce.Value;
+ }
+ else
+ {
+ value = CachedExpressionCompiler.Evaluate(arg);
+ }
+ rvd.Add(parameters[i].Name, value);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/LinkBuilder.cs b/src/Microsoft.Web.Mvc/LinkBuilder.cs
new file mode 100644
index 00000000..5e35263c
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/LinkBuilder.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Web.Mvc;
+using System.Web.Routing;
+using ExpressionHelper = Microsoft.Web.Mvc.Internal.ExpressionHelper;
+
+namespace Microsoft.Web.Mvc
+{
+ public static class LinkBuilder
+ {
+ /// <summary>
+ /// Builds a URL based on the Expression passed in
+ /// </summary>
+ /// <typeparam name="TController">Controller Type Only</typeparam>
+ /// <param name="context">The current ViewContext</param>
+ /// <param name="routeCollection">The <see cref="RouteCollection"/> to use for building the URL.</param>
+ /// <param name="action">The action to invoke</param>
+ /// <returns></returns>
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ [SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings", Justification = "The return value is not a regular URL since it may contain ~/ ASP.NET-specific characters"), SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an Extension Method which allows the user to provide a strongly-typed argument via Expression"), SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Need to be sure the passed-in argument is of type Controller::Action")]
+ public static string BuildUrlFromExpression<TController>(RequestContext context, RouteCollection routeCollection, Expression<Action<TController>> action) where TController : Controller
+ {
+ RouteValueDictionary routeValues = ExpressionHelper.GetRouteValuesFromExpression(action);
+ VirtualPathData vpd = routeCollection.GetVirtualPathForArea(context, routeValues);
+ return (vpd == null) ? null : vpd.VirtualPath;
+ }
+
+ /// <summary>
+ /// Creates a querystring as a Dictionary based on the passed-in Lambda
+ /// </summary>
+ /// <param name="call">The Lambda of the Controller method</param>
+ /// <returns></returns>
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Allowing Lambda compilation to fail if it doesn't compile at run time - design-time compilation will not allow for runtime Exception")]
+ public static RouteValueDictionary BuildParameterValuesFromExpression(MethodCallExpression call)
+ {
+ RouteValueDictionary result = new RouteValueDictionary();
+
+ ParameterInfo[] parameters = call.Method.GetParameters();
+
+ if (parameters.Length > 0)
+ {
+ for (int i = 0; i < parameters.Length; i++)
+ {
+ Expression arg = call.Arguments[i];
+ object value;
+ ConstantExpression ce = arg as ConstantExpression;
+ if (ce != null)
+ {
+ // If argument is a constant expression, just get the value
+ value = ce.Value;
+ }
+ else
+ {
+ try
+ {
+ value = CachedExpressionCompiler.Evaluate(arg);
+ }
+ catch
+ {
+ // ?????
+ value = String.Empty;
+ }
+ }
+ // Code should be added here to appropriately escape the value string
+ result.Add(parameters[i].Name, value);
+ }
+ }
+ return result;
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/LinkExtensions.cs b/src/Microsoft.Web.Mvc/LinkExtensions.cs
new file mode 100644
index 00000000..46a33ca4
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/LinkExtensions.cs
@@ -0,0 +1,51 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq.Expressions;
+using System.Web.Mvc;
+using System.Web.Mvc.Html;
+using System.Web.Routing;
+using ExpressionHelper = Microsoft.Web.Mvc.Internal.ExpressionHelper;
+
+namespace Microsoft.Web.Mvc
+{
+ public static class LinkExtensions
+ {
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ [SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings", Justification = "This is a UI method and is required to use strings as Uri"), SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an Extension Method which allows the user to provide a strongly-typed argument via Expression")]
+ public static string BuildUrlFromExpression<TController>(this HtmlHelper helper, Expression<Action<TController>> action) where TController : Controller
+ {
+ return LinkBuilder.BuildUrlFromExpression(helper.ViewContext.RequestContext, helper.RouteCollection, action);
+ }
+
+ /// <summary>
+ /// Creates an anchor tag based on the passed in controller type and method
+ /// </summary>
+ /// <typeparam name="TController">The Controller Type</typeparam>
+ /// <param name="helper">The <see cref="HtmlHelper"/> where to create the link.</param>
+ /// <param name="action">The Method to route to</param>
+ /// <param name="linkText">The linked text to appear on the page</param>
+ /// <returns>The anchor tag.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString ActionLink<TController>(this HtmlHelper helper, Expression<Action<TController>> action, string linkText) where TController : Controller
+ {
+ return ActionLink(helper, action, linkText, null);
+ }
+
+ /// <summary>
+ /// Creates an anchor tag based on the passed in controller type and method
+ /// </summary>
+ /// <typeparam name="TController">The Controller Type</typeparam>
+ /// <param name="helper">The <see cref="HtmlHelper"/> where to create the link.</param>
+ /// <param name="action">The Method to route to</param>
+ /// <param name="linkText">The linked text to appear on the page</param>
+ /// <param name="htmlAttributes">Any additional HTML attributes.</param>
+ /// <returns>The anchor tag.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString ActionLink<TController>(this HtmlHelper helper, Expression<Action<TController>> action, string linkText, object htmlAttributes) where TController : Controller
+ {
+ RouteValueDictionary routingValues = ExpressionHelper.GetRouteValuesFromExpression(action);
+
+ return helper.RouteLink(linkText, routingValues, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/MachineKeyWrapper.cs b/src/Microsoft.Web.Mvc/MachineKeyWrapper.cs
new file mode 100644
index 00000000..7e3c0b94
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/MachineKeyWrapper.cs
@@ -0,0 +1,19 @@
+using System.Web.Security;
+
+namespace Microsoft.Web.Mvc
+{
+ // Concrete implementation of IMachineKey that talks to the static MachineKey type
+
+ internal sealed class MachineKeyWrapper : IMachineKey
+ {
+ public byte[] Decode(string encodedData, MachineKeyProtection protectionOption)
+ {
+ return MachineKey.Decode(encodedData, protectionOption);
+ }
+
+ public string Encode(byte[] data, MachineKeyProtection protectionOption)
+ {
+ return MachineKey.Encode(data, protectionOption);
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/MailToExtensions.cs b/src/Microsoft.Web.Mvc/MailToExtensions.cs
new file mode 100644
index 00000000..236d7dad
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/MailToExtensions.cs
@@ -0,0 +1,104 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc
+{
+ [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "MailTo", Justification = "This is correctly cased.")]
+ public static class MailToExtensions
+ {
+ public static MvcHtmlString Mailto(this HtmlHelper helper, string linkText, string emailAddress)
+ {
+ return Mailto(helper, linkText, emailAddress, null, null, null, null, null);
+ }
+
+ public static MvcHtmlString Mailto(this HtmlHelper helper, string linkText, string emailAddress, object htmlAttributes)
+ {
+ return Mailto(helper, linkText, emailAddress, null, null, null, null, htmlAttributes);
+ }
+
+ public static MvcHtmlString Mailto(this HtmlHelper helper, string linkText, string emailAddress, IDictionary<string, object> htmlAttributes)
+ {
+ return Mailto(helper, linkText, emailAddress, null, null, null, null, htmlAttributes);
+ }
+
+ public static MvcHtmlString Mailto(this HtmlHelper helper, string linkText, string emailAddress, string subject)
+ {
+ return Mailto(helper, linkText, emailAddress, subject, null, null, null, null);
+ }
+
+ public static MvcHtmlString Mailto(this HtmlHelper helper, string linkText, string emailAddress, string subject, object htmlAttributes)
+ {
+ return Mailto(helper, linkText, emailAddress, subject, null, null, null, htmlAttributes);
+ }
+
+ public static MvcHtmlString Mailto(this HtmlHelper helper, string linkText, string emailAddress, string subject, IDictionary<string, object> htmlAttributes)
+ {
+ return Mailto(helper, linkText, emailAddress, subject, null, null, null, htmlAttributes);
+ }
+
+ public static MvcHtmlString Mailto(this HtmlHelper helper, string linkText, string emailAddress, string subject, string body, string cc, string bcc, object htmlAttributes)
+ {
+ return Mailto(helper, linkText, emailAddress, subject, body, cc, bcc, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ public static MvcHtmlString Mailto(this HtmlHelper helper, string linkText, string emailAddress, string subject,
+ string body, string cc, string bcc, IDictionary<string, object> htmlAttributes)
+ {
+ if (emailAddress == null)
+ {
+ throw new ArgumentNullException("emailAddress"); // TODO: Resource message
+ }
+ if (linkText == null)
+ {
+ throw new ArgumentNullException("linkText"); // TODO: Resource message
+ }
+
+ string mailToUrl = "mailto:" + emailAddress;
+
+ List<string> mailQuery = new List<string>();
+ if (!String.IsNullOrEmpty(subject))
+ {
+ mailQuery.Add("subject=" + helper.Encode(subject));
+ }
+
+ if (!String.IsNullOrEmpty(cc))
+ {
+ mailQuery.Add("cc=" + helper.Encode(cc));
+ }
+
+ if (!String.IsNullOrEmpty(bcc))
+ {
+ mailQuery.Add("bcc=" + helper.Encode(bcc));
+ }
+
+ if (!String.IsNullOrEmpty(body))
+ {
+ string encodedBody = helper.Encode(body);
+ encodedBody = encodedBody.Replace(Environment.NewLine, "%0A");
+ mailQuery.Add("body=" + encodedBody);
+ }
+
+ string query = String.Empty;
+ for (int i = 0; i < mailQuery.Count; i++)
+ {
+ query += mailQuery[i];
+ if (i < mailQuery.Count - 1)
+ {
+ query += "&";
+ }
+ }
+ if (query.Length > 0)
+ {
+ mailToUrl += "?" + query;
+ }
+
+ TagBuilder mailtoAnchor = new TagBuilder("a");
+ mailtoAnchor.MergeAttribute("href", mailToUrl);
+ mailtoAnchor.MergeAttributes(htmlAttributes, true);
+ mailtoAnchor.InnerHtml = linkText;
+ return MvcHtmlString.Create(mailtoAnchor.ToString());
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Microsoft.Web.Mvc.csproj b/src/Microsoft.Web.Mvc/Microsoft.Web.Mvc.csproj
new file mode 100644
index 00000000..c9da29ef
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Microsoft.Web.Mvc.csproj
@@ -0,0 +1,276 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <CodeAnalysis Condition=" '$(CodeAnalysis)' == ''">false</CodeAnalysis>
+ <ProductVersion>9.0.30729</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{D3CF7430-6DA4-42B0-BD90-CA39D16687B2}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>Microsoft.Web.Mvc</RootNamespace>
+ <AssemblyName>Microsoft.Web.Mvc</AssemblyName>
+ <FileAlignment>512</FileAlignment>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ <!-- Force signing off -->
+ <SignAssembly>false</SignAssembly>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>..\..\bin\Debug\</OutputPath>
+ <DefineConstants>TRACE;DEBUG;ASPNETMVC</DefineConstants>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ <DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
+ <NoWarn>1591</NoWarn>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>..\..\bin\Release\</OutputPath>
+ <DefineConstants>TRACE;ASPNETMVC</DefineConstants>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ <RunCodeAnalysis>$(CodeAnalysis)</RunCodeAnalysis>
+ <DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
+ <NoWarn>1591</NoWarn>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'CodeCoverage|AnyCPU'">
+ <DebugSymbols>true</DebugSymbols>
+ <OutputPath>..\..\bin\CodeCoverage\</OutputPath>
+ <DefineConstants>TRACE;DEBUG;ASPNETMVC</DefineConstants>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ <DebugType>full</DebugType>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="System" />
+ <Reference Include="System.ComponentModel.DataAnnotations" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.Data.Linq" />
+ <Reference Include="System.Runtime.Serialization" />
+ <Reference Include="System.ServiceModel" />
+ <Reference Include="System.ServiceModel.Web" />
+ <Reference Include="System.Web" />
+ <Reference Include="System.Web.Abstractions" />
+ <Reference Include="System.Web.Routing" />
+ <Reference Include="System.Xml" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="..\CommonAssemblyInfo.cs">
+ <Link>Properties\CommonAssemblyInfo.cs</Link>
+ </Compile>
+ <Compile Include="..\TransparentCommonAssemblyInfo.cs">
+ <Link>Properties\TransparentCommonAssemblyInfo.cs</Link>
+ </Compile>
+ <Compile Include="GlobalSuppressions.cs" />
+ <Compile Include="MachineKeyWrapper.cs" />
+ <Compile Include="IMachineKey.cs" />
+ <Compile Include="CreditCardAttribute.cs" />
+ <Compile Include="EmailAddressAttribute.cs" />
+ <Compile Include="FileExtensionsAttribute.cs" />
+ <Compile Include="AreaHelpers.cs" />
+ <Compile Include="AsyncManagerExtensions.cs" />
+ <Compile Include="CookieValueProviderFactory.cs" />
+ <Compile Include="ActionLinkAreaAttribute.cs" />
+ <Compile Include="ControllerExtensions.cs" />
+ <Compile Include="DynamicReflectionObject.cs" />
+ <Compile Include="DynamicViewDataDictionary.cs" />
+ <Compile Include="DynamicViewPage.cs">
+ <SubType>ASPXCodeBehind</SubType>
+ </Compile>
+ <Compile Include="DynamicViewPage`1.cs">
+ <SubType>ASPXCodeBehind</SubType>
+ </Compile>
+ <Compile Include="ExpressionUtil\BinaryExpressionFingerprint.cs" />
+ <Compile Include="ExpressionUtil\CachedExpressionCompiler.cs" />
+ <Compile Include="ExpressionUtil\ConditionalExpressionFingerprint.cs" />
+ <Compile Include="ExpressionUtil\ConstantExpressionFingerprint.cs" />
+ <Compile Include="ExpressionUtil\DefaultExpressionFingerprint.cs" />
+ <Compile Include="ExpressionUtil\ExpressionFingerprint.cs" />
+ <Compile Include="ExpressionUtil\ExpressionFingerprintChain.cs" />
+ <Compile Include="ExpressionUtil\FingerprintingExpressionVisitor.cs" />
+ <Compile Include="ExpressionUtil\HashCodeCombiner.cs" />
+ <Compile Include="ExpressionUtil\Hoisted`2.cs" />
+ <Compile Include="ExpressionUtil\HoistingExpressionVisitor.cs" />
+ <Compile Include="ExpressionUtil\IndexExpressionFingerprint.cs" />
+ <Compile Include="ExpressionUtil\LambdaExpressionFingerprint.cs" />
+ <Compile Include="ExpressionUtil\MemberExpressionFingerprint.cs" />
+ <Compile Include="ExpressionUtil\MethodCallExpressionFingerprint.cs" />
+ <Compile Include="ExpressionUtil\ParameterExpressionFingerprint.cs" />
+ <Compile Include="ExpressionUtil\TypeBinaryExpressionFingerprint.cs" />
+ <Compile Include="ExpressionUtil\UnaryExpressionFingerprint.cs" />
+ <Compile Include="Html\HtmlHelperExtensions.cs" />
+ <Compile Include="ModelBinding\BinaryDataModelBinderProvider.cs" />
+ <Compile Include="ModelBinding\ExtensibleModelBinderAttribute.cs" />
+ <Compile Include="ModelBinding\ModelBinderProviderOptionsAttribute.cs" />
+ <Compile Include="ModelBinding\ExtensibleModelBinderAdapter.cs" />
+ <Compile Include="ModelBinding\ExtensibleModelBindingContext.cs" />
+ <Compile Include="ModelBinding\IExtensibleModelBinder.cs" />
+ <Compile Include="ModelBinding\ModelBinderErrorMessageProvider.cs" />
+ <Compile Include="ModelCopier.cs" />
+ <Compile Include="ModelBinding\CollectionModelBinderProvider.cs" />
+ <Compile Include="ModelBinding\CollectionModelBinderUtil.cs" />
+ <Compile Include="ModelBinding\CollectionModelBinder`1.cs" />
+ <Compile Include="ModelBinding\ComplexModelDtoModelBinderProvider.cs" />
+ <Compile Include="ModelBinding\ArrayModelBinderProvider.cs" />
+ <Compile Include="ModelBinding\ArrayModelBinder`1.cs" />
+ <Compile Include="ModelBinding\BindingBehaviorAttribute.cs" />
+ <Compile Include="ModelBinding\BindingBehavior.cs" />
+ <Compile Include="ModelBinding\BindNeverAttribute.cs" />
+ <Compile Include="ModelBinding\BindRequiredAttribute.cs" />
+ <Compile Include="ModelBinding\GenericModelBinderProvider.cs" />
+ <Compile Include="ModelBinding\DictionaryModelBinderProvider.cs" />
+ <Compile Include="ModelBinding\DictionaryModelBinder`2.cs" />
+ <Compile Include="UrlAttribute.cs" />
+ <Compile Include="ValueProviderUtil.cs" />
+ <Compile Include="ElementalValueProvider.cs" />
+ <Compile Include="ModelBinding\KeyValuePairModelBinderProvider.cs" />
+ <Compile Include="ModelBinding\KeyValuePairModelBinderUtil.cs" />
+ <Compile Include="ModelBinding\KeyValuePairModelBinder`2.cs" />
+ <Compile Include="TypeDescriptorHelper.cs" />
+ <Compile Include="Error.cs" />
+ <Compile Include="ModelBinding\ComplexModelDto.cs" />
+ <Compile Include="ModelBinding\ComplexModelDtoModelBinder.cs" />
+ <Compile Include="ModelBinding\ComplexModelDtoResult.cs" />
+ <Compile Include="ModelBinding\ModelBinderConfig.cs" />
+ <Compile Include="ModelBinding\MutableObjectModelBinder.cs" />
+ <Compile Include="ModelBinding\MutableObjectModelBinderProvider.cs" />
+ <Compile Include="ModelBinding\SimpleModelBinderProvider.cs" />
+ <Compile Include="ModelBinding\TypeConverterModelBinder.cs" />
+ <Compile Include="ModelBinding\TypeConverterModelBinderProvider.cs" />
+ <Compile Include="ModelBinding\TypeMatchModelBinderProvider.cs" />
+ <Compile Include="ModelBinding\TypeMatchModelBinder.cs" />
+ <Compile Include="ModelBinding\ModelBinderProviderCollection.cs" />
+ <Compile Include="ModelBinding\ModelBinderProvider.cs" />
+ <Compile Include="ModelBinding\ModelBinderUtil.cs" />
+ <Compile Include="ModelBinding\ModelValidatedEventArgs.cs" />
+ <Compile Include="ModelBinding\ModelValidatingEventArgs.cs" />
+ <Compile Include="ModelBinding\ModelValidationNode.cs" />
+ <Compile Include="ModelBinding\ModelBinderProviders.cs" />
+ <Compile Include="ServerVariablesValueProviderFactory.cs" />
+ <Compile Include="TempDataValueProviderFactory.cs" />
+ <Compile Include="SessionValueProviderFactory.cs" />
+ <Compile Include="CopyAsyncParametersAttribute.cs" />
+ <Compile Include="CssExtensions.cs" />
+ <Compile Include="Resources\ActionType.cs" />
+ <Compile Include="Resources\AjaxHelperExtensions.cs" />
+ <Compile Include="Resources\AtomEntryActionResult.cs" />
+ <Compile Include="Resources\AtomFeedActionResult.cs" />
+ <Compile Include="Resources\AtomServiceDocumentActionResult.cs" />
+ <Compile Include="Resources\DataContractJsonActionResult.cs" />
+ <Compile Include="Resources\JsonFormatHandler.cs" />
+ <Compile Include="Resources\RouteCollectionExtensions.cs" />
+ <Compile Include="Resources\UriHelperExtensions.cs" />
+ <Compile Include="Resources\WebApiEnabledAttribute.cs" />
+ <Compile Include="Resources\XmlFormatHandler.cs" />
+ <Compile Include="Resources\IResponseFormatHandler.cs" />
+ <Compile Include="Resources\IRequestFormatHandler.cs" />
+ <Compile Include="Resources\FormatManager.cs" />
+ <Compile Include="Resources\HtmlHelperExtensions.cs" />
+ <Compile Include="Resources\MultiFormatActionResult.cs" />
+ <Compile Include="Resources\RequestContextExtensions.cs" />
+ <Compile Include="Resources\ResourceControllerFactory.cs" />
+ <Compile Include="Resources\ResourceErrorActionResult.cs" />
+ <Compile Include="Resources\ResourceModelBinder.cs" />
+ <Compile Include="Resources\ResourceRedirectToRouteResult.cs" />
+ <Compile Include="Resources\DataContractXmlActionResult.cs" />
+ <Compile Include="Resources\HttpRequestBaseExtensions.cs" />
+ <Compile Include="Resources\IEnumerableExtensions.cs" />
+ <Compile Include="Resources\DefaultFormatHelper.cs" />
+ <Compile Include="Resources\DefaultFormatManager.cs" />
+ <Compile Include="Resources\FormatHelper.cs" />
+ <Compile Include="Internal\ExpressionHelper.cs" />
+ <Compile Include="DeserializeAttribute.cs" />
+ <Compile Include="ScriptExtensions.cs" />
+ <Compile Include="SerializationExtensions.cs" />
+ <Compile Include="SerializationMode.cs" />
+ <Compile Include="MvcSerializer.cs" />
+ <Compile Include="AjaxOnlyAttribute.cs" />
+ <Compile Include="CachedExpressionCompiler.cs" />
+ <Compile Include="ReaderWriterCache`2.cs" />
+ <Compile Include="ButtonBuilder.cs" />
+ <Compile Include="ButtonsAndLinkExtensions.cs" />
+ <Compile Include="ContentTypeAttribute.cs" />
+ <Compile Include="Controls\ActionLink.cs" />
+ <Compile Include="Controls\EncodeType.cs" />
+ <Compile Include="Controls\DropDownList.cs" />
+ <Compile Include="Controls\Hidden.cs" />
+ <Compile Include="Controls\Label.cs" />
+ <Compile Include="Controls\MvcControl.cs" />
+ <Compile Include="Controls\MvcInputControl.cs" />
+ <Compile Include="Controls\Password.cs" />
+ <Compile Include="Controls\Repeater.cs" />
+ <Compile Include="Controls\RepeaterItem.cs" />
+ <Compile Include="Controls\RouteValues.cs" />
+ <Compile Include="Controls\TextBox.cs" />
+ <Compile Include="CookieTempDataProvider.cs" />
+ <Compile Include="SkipBindingAttribute.cs" />
+ <Compile Include="FormExtensions.cs" />
+ <Compile Include="HtmlButtonType.cs" />
+ <Compile Include="LinkBuilder.cs" />
+ <Compile Include="LinkExtensions.cs" />
+ <Compile Include="RadioExtensions.cs" />
+ <Compile Include="ImageExtensions.cs" />
+ <Compile Include="MailToExtensions.cs" />
+ <Compile Include="TypeHelpers.cs" />
+ <Compile Include="ViewExtensions.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="Properties\MvcResources.Designer.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>MvcResources.resx</DependentUpon>
+ </Compile>
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\System.Web.Mvc\System.Web.Mvc.csproj">
+ <Project>{3D3FFD8A-624D-4E9B-954B-E1C105507975}</Project>
+ <Name>System.Web.Mvc</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\System.Web.WebPages\System.Web.WebPages.csproj">
+ <Project>{76EFA9C5-8D7E-4FDF-B710-E20F8B6B00D2}</Project>
+ <Name>System.Web.WebPages</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="Properties\MvcResources.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>MvcResources.Designer.cs</LastGenOutput>
+ <SubType>Designer</SubType>
+ </EmbeddedResource>
+ </ItemGroup>
+ <ItemGroup>
+ <CodeAnalysisDictionary Include="..\CodeAnalysisDictionary.xml" />
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="FuturesFiles\DefaultTemplates\DisplayTemplates\Boolean.ascx">
+ <SubType>ASPXCodeBehind</SubType>
+ </None>
+ <None Include="FuturesFiles\DefaultTemplates\DisplayTemplates\Collection.ascx" />
+ <None Include="FuturesFiles\DefaultTemplates\DisplayTemplates\Decimal.ascx" />
+ <None Include="FuturesFiles\DefaultTemplates\DisplayTemplates\EmailAddress.ascx" />
+ <None Include="FuturesFiles\DefaultTemplates\DisplayTemplates\HiddenInput.ascx" />
+ <None Include="FuturesFiles\DefaultTemplates\DisplayTemplates\Html.ascx" />
+ <None Include="FuturesFiles\DefaultTemplates\DisplayTemplates\Object.ascx" />
+ <None Include="FuturesFiles\DefaultTemplates\DisplayTemplates\String.ascx" />
+ <None Include="FuturesFiles\DefaultTemplates\DisplayTemplates\Url.ascx" />
+ <None Include="FuturesFiles\DefaultTemplates\EditorTemplates\Boolean.ascx" />
+ <None Include="FuturesFiles\DefaultTemplates\EditorTemplates\Collection.ascx" />
+ <None Include="FuturesFiles\DefaultTemplates\EditorTemplates\Decimal.ascx" />
+ <None Include="FuturesFiles\DefaultTemplates\EditorTemplates\HiddenInput.ascx" />
+ <None Include="FuturesFiles\DefaultTemplates\EditorTemplates\MultilineText.ascx" />
+ <None Include="FuturesFiles\DefaultTemplates\EditorTemplates\Object.ascx" />
+ <None Include="FuturesFiles\DefaultTemplates\EditorTemplates\Password.ascx" />
+ <None Include="FuturesFiles\DefaultTemplates\EditorTemplates\String.ascx" />
+ <None Include="FuturesFiles\iismap.vbs" />
+ <None Include="FuturesFiles\MvcDiagnostics.aspx">
+ <SubType>ASPXCodeBehind</SubType>
+ </None>
+ <None Include="FuturesFiles\registermvc.wsf" />
+ <None Include="FuturesFiles\unregistermvc.wsf" />
+ </ItemGroup>
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+</Project> \ No newline at end of file
diff --git a/src/Microsoft.Web.Mvc/ModelBinding/ArrayModelBinderProvider.cs b/src/Microsoft.Web.Mvc/ModelBinding/ArrayModelBinderProvider.cs
new file mode 100644
index 00000000..fcbff784
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ModelBinding/ArrayModelBinderProvider.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc.ModelBinding
+{
+ public sealed class ArrayModelBinderProvider : ModelBinderProvider
+ {
+ public override IExtensibleModelBinder GetBinder(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext)
+ {
+ ModelBinderUtil.ValidateBindingContext(bindingContext);
+
+ if (!bindingContext.ModelMetadata.IsReadOnly && bindingContext.ModelType.IsArray &&
+ bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName))
+ {
+ Type elementType = bindingContext.ModelType.GetElementType();
+ return (IExtensibleModelBinder)Activator.CreateInstance(typeof(ArrayModelBinder<>).MakeGenericType(elementType));
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ModelBinding/ArrayModelBinder`1.cs b/src/Microsoft.Web.Mvc/ModelBinding/ArrayModelBinder`1.cs
new file mode 100644
index 00000000..8a512e38
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ModelBinding/ArrayModelBinder`1.cs
@@ -0,0 +1,15 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc.ModelBinding
+{
+ public class ArrayModelBinder<TElement> : CollectionModelBinder<TElement>
+ {
+ protected override bool CreateOrReplaceCollection(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext, IList<TElement> newCollection)
+ {
+ bindingContext.Model = newCollection.ToArray();
+ return true;
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ModelBinding/BinaryDataModelBinderProvider.cs b/src/Microsoft.Web.Mvc/ModelBinding/BinaryDataModelBinderProvider.cs
new file mode 100644
index 00000000..64ce4d59
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ModelBinding/BinaryDataModelBinderProvider.cs
@@ -0,0 +1,80 @@
+using System;
+using System.Data.Linq;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc.ModelBinding
+{
+ // This is a single provider that can work with both byte[] and Binary models.
+ public sealed class BinaryDataModelBinderProvider : ModelBinderProvider
+ {
+ private static readonly ModelBinderProvider[] _providers = new ModelBinderProvider[]
+ {
+ new SimpleModelBinderProvider(typeof(byte[]), new ByteArrayExtensibleModelBinder()),
+ new SimpleModelBinderProvider(typeof(Binary), new LinqBinaryExtensibleModelBinder())
+ };
+
+ public override IExtensibleModelBinder GetBinder(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext)
+ {
+ return (from provider in _providers
+ let binder = provider.GetBinder(controllerContext, bindingContext)
+ where binder != null
+ select binder).FirstOrDefault();
+ }
+
+ // This is essentially a clone of the ByteArrayModelBinder from core
+ private class ByteArrayExtensibleModelBinder : IExtensibleModelBinder
+ {
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We want to ignore when the data is corrupted")]
+ [SuppressMessage("Microsoft.Globalization", "CA1304:SpecifyCultureInfo", MessageId = "System.Web.Mvc.ValueProviderResult.ConvertTo(System.Type)", Justification = "The target object should make the correct culture determination, not this method.")]
+ public bool BindModel(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext)
+ {
+ ModelBinderUtil.ValidateBindingContext(bindingContext);
+ ValueProviderResult valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
+
+ // case 1: there was no <input ... /> element containing this data
+ if (valueProviderResult == null)
+ {
+ return false;
+ }
+
+ string base64String = (string)valueProviderResult.ConvertTo(typeof(string));
+
+ // case 2: there was an <input ... /> element but it was left blank
+ if (String.IsNullOrEmpty(base64String))
+ {
+ return false;
+ }
+
+ // Future proofing. If the byte array is actually an instance of System.Data.Linq.Binary
+ // then we need to remove these quotes put in place by the ToString() method.
+ string realValue = base64String.Replace("\"", String.Empty);
+ try
+ {
+ bindingContext.Model = ConvertByteArray(Convert.FromBase64String(realValue));
+ return true;
+ }
+ catch
+ {
+ // corrupt data - just ignore
+ return false;
+ }
+ }
+
+ protected virtual object ConvertByteArray(byte[] originalModel)
+ {
+ return originalModel;
+ }
+ }
+
+ // This is essentially a clone of the LinqBinaryModelBinder from core
+ private class LinqBinaryExtensibleModelBinder : ByteArrayExtensibleModelBinder
+ {
+ protected override object ConvertByteArray(byte[] originalModel)
+ {
+ return new Binary(originalModel);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ModelBinding/BindNeverAttribute.cs b/src/Microsoft.Web.Mvc/ModelBinding/BindNeverAttribute.cs
new file mode 100644
index 00000000..2586a055
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ModelBinding/BindNeverAttribute.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace Microsoft.Web.Mvc.ModelBinding
+{
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
+ public sealed class BindNeverAttribute : BindingBehaviorAttribute
+ {
+ public BindNeverAttribute()
+ : base(BindingBehavior.Never)
+ {
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ModelBinding/BindRequiredAttribute.cs b/src/Microsoft.Web.Mvc/ModelBinding/BindRequiredAttribute.cs
new file mode 100644
index 00000000..5ff70870
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ModelBinding/BindRequiredAttribute.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace Microsoft.Web.Mvc.ModelBinding
+{
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
+ public sealed class BindRequiredAttribute : BindingBehaviorAttribute
+ {
+ public BindRequiredAttribute()
+ : base(BindingBehavior.Required)
+ {
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ModelBinding/BindingBehavior.cs b/src/Microsoft.Web.Mvc/ModelBinding/BindingBehavior.cs
new file mode 100644
index 00000000..b99afbd3
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ModelBinding/BindingBehavior.cs
@@ -0,0 +1,9 @@
+namespace Microsoft.Web.Mvc.ModelBinding
+{
+ public enum BindingBehavior
+ {
+ Optional = 0,
+ Never,
+ Required
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ModelBinding/BindingBehaviorAttribute.cs b/src/Microsoft.Web.Mvc/ModelBinding/BindingBehaviorAttribute.cs
new file mode 100644
index 00000000..743c4878
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ModelBinding/BindingBehaviorAttribute.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Microsoft.Web.Mvc.ModelBinding
+{
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
+ [SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", Justification = "This class is designed to be overridden")]
+ public class BindingBehaviorAttribute : Attribute
+ {
+ private static readonly object _typeId = new object();
+
+ public BindingBehaviorAttribute(BindingBehavior behavior)
+ {
+ Behavior = behavior;
+ }
+
+ public BindingBehavior Behavior { get; private set; }
+
+ public override object TypeId
+ {
+ get { return _typeId; }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ModelBinding/CollectionModelBinderProvider.cs b/src/Microsoft.Web.Mvc/ModelBinding/CollectionModelBinderProvider.cs
new file mode 100644
index 00000000..18611517
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ModelBinding/CollectionModelBinderProvider.cs
@@ -0,0 +1,22 @@
+using System.Collections.Generic;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc.ModelBinding
+{
+ public sealed class CollectionModelBinderProvider : ModelBinderProvider
+ {
+ public override IExtensibleModelBinder GetBinder(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext)
+ {
+ ModelBinderUtil.ValidateBindingContext(bindingContext);
+
+ if (bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName))
+ {
+ return CollectionModelBinderUtil.GetGenericBinder(typeof(ICollection<>), typeof(List<>), typeof(CollectionModelBinder<>), bindingContext.ModelMetadata);
+ }
+ else
+ {
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ModelBinding/CollectionModelBinderUtil.cs b/src/Microsoft.Web.Mvc/ModelBinding/CollectionModelBinderUtil.cs
new file mode 100644
index 00000000..a024a256
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ModelBinding/CollectionModelBinderUtil.cs
@@ -0,0 +1,144 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc.ModelBinding
+{
+ internal static class CollectionModelBinderUtil
+ {
+ public static void CreateOrReplaceCollection<TElement>(ExtensibleModelBindingContext bindingContext, IEnumerable<TElement> incomingElements, Func<ICollection<TElement>> creator)
+ {
+ ICollection<TElement> collection = bindingContext.Model as ICollection<TElement>;
+ if (collection == null || collection.IsReadOnly)
+ {
+ collection = creator();
+ bindingContext.Model = collection;
+ }
+
+ collection.Clear();
+ foreach (TElement element in incomingElements)
+ {
+ collection.Add(element);
+ }
+ }
+
+ public static void CreateOrReplaceDictionary<TKey, TValue>(ExtensibleModelBindingContext bindingContext, IEnumerable<KeyValuePair<TKey, TValue>> incomingElements, Func<IDictionary<TKey, TValue>> creator)
+ {
+ IDictionary<TKey, TValue> dictionary = bindingContext.Model as IDictionary<TKey, TValue>;
+ if (dictionary == null || dictionary.IsReadOnly)
+ {
+ dictionary = creator();
+ bindingContext.Model = dictionary;
+ }
+
+ dictionary.Clear();
+ foreach (var element in incomingElements)
+ {
+ if (element.Key != null)
+ {
+ dictionary[element.Key] = element.Value;
+ }
+ }
+ }
+
+ // supportedInterfaceType: type that is updatable by this binder
+ // newInstanceType: type that will be created by the binder if necessary
+ // openBinderType: model binder type
+ // modelMetadata: metadata for the model to bind
+ //
+ // example: GetGenericBinder(typeof(IList<>), typeof(List<>), typeof(ListBinder<>), ...) means that the ListBinder<T>
+ // type can update models that implement IList<T>, and if for some reason the existing model instance is not
+ // updatable the binder will create a List<T> object and bind to that instead. This method will return a ListBinder<T>
+ // or null, depending on whether the type and updatability checks succeed.
+ public static IExtensibleModelBinder GetGenericBinder(Type supportedInterfaceType, Type newInstanceType, Type openBinderType, ModelMetadata modelMetadata)
+ {
+ Type[] typeArguments = GetTypeArgumentsForUpdatableGenericCollection(supportedInterfaceType, newInstanceType, modelMetadata);
+ return (typeArguments != null) ? (IExtensibleModelBinder)Activator.CreateInstance(openBinderType.MakeGenericType(typeArguments)) : null;
+ }
+
+ [SuppressMessage("Microsoft.Globalization", "CA1304:SpecifyCultureInfo", MessageId = "System.Web.Mvc.ValueProviderResult.ConvertTo(System.Type)", Justification = "This model binder binds collections, so it does not need culture.")]
+ public static IEnumerable<string> GetIndexNamesFromValueProviderResult(ValueProviderResult valueProviderResultIndex)
+ {
+ IEnumerable<string> indexNames = null;
+ if (valueProviderResultIndex != null)
+ {
+ string[] indexes = (string[])(valueProviderResultIndex.ConvertTo(typeof(string[])));
+ if (indexes != null && indexes.Length > 0)
+ {
+ indexNames = indexes;
+ }
+ }
+ return indexNames;
+ }
+
+ public static IEnumerable<string> GetZeroBasedIndexes()
+ {
+ int i = 0;
+ while (true)
+ {
+ yield return i.ToString(CultureInfo.InvariantCulture);
+ i++;
+ }
+ }
+
+ // Returns the generic type arguments for the model type if updatable, else null.
+ // supportedInterfaceType: open type (like IList<>) of supported interface, must implement ICollection<>
+ // newInstanceType: open type (like List<>) of object that will be created, must implement supportedInterfaceType
+ public static Type[] GetTypeArgumentsForUpdatableGenericCollection(Type supportedInterfaceType, Type newInstanceType, ModelMetadata modelMetadata)
+ {
+ /*
+ * Check that we can extract proper type arguments from the model.
+ */
+
+ if (!modelMetadata.ModelType.IsGenericType || modelMetadata.ModelType.IsGenericTypeDefinition)
+ {
+ // not a closed generic type
+ return null;
+ }
+
+ Type[] modelTypeArguments = modelMetadata.ModelType.GetGenericArguments();
+ if (modelTypeArguments.Length != supportedInterfaceType.GetGenericArguments().Length)
+ {
+ // wrong number of generic type arguments
+ return null;
+ }
+
+ /*
+ * Is it possible just to change the reference rather than update the collection in-place?
+ */
+
+ if (!modelMetadata.IsReadOnly)
+ {
+ Type closedNewInstanceType = newInstanceType.MakeGenericType(modelTypeArguments);
+ if (modelMetadata.ModelType.IsAssignableFrom(closedNewInstanceType))
+ {
+ return modelTypeArguments;
+ }
+ }
+
+ /*
+ * At this point, we know we can't change the reference, so we need to verify that
+ * the model instance can be updated in-place.
+ */
+
+ Type closedSupportedInterfaceType = supportedInterfaceType.MakeGenericType(modelTypeArguments);
+ if (!closedSupportedInterfaceType.IsInstanceOfType(modelMetadata.Model))
+ {
+ return null; // not instance of correct interface
+ }
+
+ Type closedCollectionType = TypeHelpers.ExtractGenericInterface(closedSupportedInterfaceType, typeof(ICollection<>));
+ bool collectionInstanceIsReadOnly = (bool)closedCollectionType.GetProperty("IsReadOnly").GetValue(modelMetadata.Model, null);
+ if (collectionInstanceIsReadOnly)
+ {
+ return null;
+ }
+ else
+ {
+ return modelTypeArguments;
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ModelBinding/CollectionModelBinder`1.cs b/src/Microsoft.Web.Mvc/ModelBinding/CollectionModelBinder`1.cs
new file mode 100644
index 00000000..d7e17187
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ModelBinding/CollectionModelBinder`1.cs
@@ -0,0 +1,131 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc.ModelBinding
+{
+ public class CollectionModelBinder<TElement> : IExtensibleModelBinder
+ {
+ // Used when the ValueProvider contains the collection to be bound as multiple elements, e.g. foo[0], foo[1].
+ private static List<TElement> BindComplexCollection(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext)
+ {
+ string indexPropertyName = ModelBinderUtil.CreatePropertyModelName(bindingContext.ModelName, "index");
+ ValueProviderResult valueProviderResultIndex = bindingContext.ValueProvider.GetValue(indexPropertyName);
+ IEnumerable<string> indexNames = CollectionModelBinderUtil.GetIndexNamesFromValueProviderResult(valueProviderResultIndex);
+ return BindComplexCollectionFromIndexes(controllerContext, bindingContext, indexNames);
+ }
+
+ internal static List<TElement> BindComplexCollectionFromIndexes(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext, IEnumerable<string> indexNames)
+ {
+ bool indexNamesIsFinite;
+ if (indexNames != null)
+ {
+ indexNamesIsFinite = true;
+ }
+ else
+ {
+ indexNamesIsFinite = false;
+ indexNames = CollectionModelBinderUtil.GetZeroBasedIndexes();
+ }
+
+ List<TElement> boundCollection = new List<TElement>();
+ foreach (string indexName in indexNames)
+ {
+ string fullChildName = ModelBinderUtil.CreateIndexModelName(bindingContext.ModelName, indexName);
+ ExtensibleModelBindingContext childBindingContext = new ExtensibleModelBindingContext(bindingContext)
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(TElement)),
+ ModelName = fullChildName
+ };
+
+ object boundValue = null;
+ IExtensibleModelBinder childBinder = bindingContext.ModelBinderProviders.GetBinder(controllerContext, childBindingContext);
+ if (childBinder != null)
+ {
+ if (childBinder.BindModel(controllerContext, childBindingContext))
+ {
+ boundValue = childBindingContext.Model;
+
+ // merge validation up
+ bindingContext.ValidationNode.ChildNodes.Add(childBindingContext.ValidationNode);
+ }
+ }
+ else
+ {
+ // should we even bother continuing?
+ if (!indexNamesIsFinite)
+ {
+ break;
+ }
+ }
+
+ boundCollection.Add(ModelBinderUtil.CastOrDefault<TElement>(boundValue));
+ }
+
+ return boundCollection;
+ }
+
+ public virtual bool BindModel(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext)
+ {
+ ModelBinderUtil.ValidateBindingContext(bindingContext);
+
+ ValueProviderResult valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
+ List<TElement> boundCollection = (valueProviderResult != null)
+ ? BindSimpleCollection(controllerContext, bindingContext, valueProviderResult.RawValue, valueProviderResult.Culture)
+ : BindComplexCollection(controllerContext, bindingContext);
+
+ bool retVal = CreateOrReplaceCollection(controllerContext, bindingContext, boundCollection);
+ return retVal;
+ }
+
+ // Used when the ValueProvider contains the collection to be bound as a single element, e.g. the raw value
+ // is [ "1", "2" ] and needs to be converted to an int[].
+ internal static List<TElement> BindSimpleCollection(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext, object rawValue, CultureInfo culture)
+ {
+ if (rawValue == null)
+ {
+ return null; // nothing to do
+ }
+
+ List<TElement> boundCollection = new List<TElement>();
+
+ object[] rawValueArray = ModelBinderUtil.RawValueToObjectArray(rawValue);
+ foreach (object rawValueElement in rawValueArray)
+ {
+ ExtensibleModelBindingContext innerBindingContext = new ExtensibleModelBindingContext(bindingContext)
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(TElement)),
+ ModelName = bindingContext.ModelName,
+ ValueProvider = new ValueProviderCollection
+ {
+ // aggregate value provider
+ new ElementalValueProvider(bindingContext.ModelName, rawValueElement, culture), // our temporary provider goes at the front of the list
+ bindingContext.ValueProvider
+ }
+ };
+
+ object boundValue = null;
+ IExtensibleModelBinder childBinder = bindingContext.ModelBinderProviders.GetBinder(controllerContext, innerBindingContext);
+ if (childBinder != null)
+ {
+ if (childBinder.BindModel(controllerContext, innerBindingContext))
+ {
+ boundValue = innerBindingContext.Model;
+ bindingContext.ValidationNode.ChildNodes.Add(innerBindingContext.ValidationNode);
+ }
+ }
+ boundCollection.Add(ModelBinderUtil.CastOrDefault<TElement>(boundValue));
+ }
+
+ return boundCollection;
+ }
+
+ // Extensibility point that allows the bound collection to be manipulated or transformed before
+ // being returned from the binder.
+ protected virtual bool CreateOrReplaceCollection(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext, IList<TElement> newCollection)
+ {
+ CollectionModelBinderUtil.CreateOrReplaceCollection(bindingContext, newCollection, () => new List<TElement>());
+ return true;
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ModelBinding/ComplexModelDto.cs b/src/Microsoft.Web.Mvc/ModelBinding/ComplexModelDto.cs
new file mode 100644
index 00000000..09b144ce
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ModelBinding/ComplexModelDto.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc.ModelBinding
+{
+ // Describes a complex model, but uses a collection rather than individual properties as the data store.
+ public class ComplexModelDto
+ {
+ public ComplexModelDto(ModelMetadata modelMetadata, IEnumerable<ModelMetadata> propertyMetadata)
+ {
+ if (modelMetadata == null)
+ {
+ throw new ArgumentNullException("modelMetadata");
+ }
+ if (propertyMetadata == null)
+ {
+ throw new ArgumentNullException("propertyMetadata");
+ }
+
+ ModelMetadata = modelMetadata;
+ PropertyMetadata = new ReadOnlyCollection<ModelMetadata>(propertyMetadata.ToList());
+ Results = new Dictionary<ModelMetadata, ComplexModelDtoResult>();
+ }
+
+ public ModelMetadata ModelMetadata { get; private set; }
+
+ public ReadOnlyCollection<ModelMetadata> PropertyMetadata { get; private set; }
+
+ // Contains entries corresponding to each property against which binding was
+ // attempted. If binding failed, the entry's value will be null. If binding
+ // was never attempted, this dictionary will not contain a corresponding
+ // entry.
+ public IDictionary<ModelMetadata, ComplexModelDtoResult> Results { get; private set; }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ModelBinding/ComplexModelDtoModelBinder.cs b/src/Microsoft.Web.Mvc/ModelBinding/ComplexModelDtoModelBinder.cs
new file mode 100644
index 00000000..996a79ad
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ModelBinding/ComplexModelDtoModelBinder.cs
@@ -0,0 +1,38 @@
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc.ModelBinding
+{
+ public sealed class ComplexModelDtoModelBinder : IExtensibleModelBinder
+ {
+ public bool BindModel(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext)
+ {
+ ModelBinderUtil.ValidateBindingContext(bindingContext, typeof(ComplexModelDto), false /* allowNullModel */);
+
+ ComplexModelDto dto = (ComplexModelDto)bindingContext.Model;
+ foreach (ModelMetadata propertyMetadata in dto.PropertyMetadata)
+ {
+ ExtensibleModelBindingContext propertyBindingContext = new ExtensibleModelBindingContext(bindingContext)
+ {
+ ModelMetadata = propertyMetadata,
+ ModelName = ModelBinderUtil.CreatePropertyModelName(bindingContext.ModelName, propertyMetadata.PropertyName)
+ };
+
+ // bind and propagate the values
+ IExtensibleModelBinder propertyBinder = bindingContext.ModelBinderProviders.GetBinder(controllerContext, propertyBindingContext);
+ if (propertyBinder != null)
+ {
+ if (propertyBinder.BindModel(controllerContext, propertyBindingContext))
+ {
+ dto.Results[propertyMetadata] = new ComplexModelDtoResult(propertyBindingContext.Model, propertyBindingContext.ValidationNode);
+ }
+ else
+ {
+ dto.Results[propertyMetadata] = null;
+ }
+ }
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ModelBinding/ComplexModelDtoModelBinderProvider.cs b/src/Microsoft.Web.Mvc/ModelBinding/ComplexModelDtoModelBinderProvider.cs
new file mode 100644
index 00000000..6c540cf6
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ModelBinding/ComplexModelDtoModelBinderProvider.cs
@@ -0,0 +1,24 @@
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc.ModelBinding
+{
+ // Returns a binder that can bind ComplexModelDto objects.
+ public sealed class ComplexModelDtoModelBinderProvider : ModelBinderProvider
+ {
+ // This is really just a simple binder.
+ private static readonly SimpleModelBinderProvider _underlyingProvider = GetUnderlyingProvider();
+
+ public override IExtensibleModelBinder GetBinder(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext)
+ {
+ return _underlyingProvider.GetBinder(controllerContext, bindingContext);
+ }
+
+ private static SimpleModelBinderProvider GetUnderlyingProvider()
+ {
+ return new SimpleModelBinderProvider(typeof(ComplexModelDto), new ComplexModelDtoModelBinder())
+ {
+ SuppressPrefixCheck = true
+ };
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ModelBinding/ComplexModelDtoResult.cs b/src/Microsoft.Web.Mvc/ModelBinding/ComplexModelDtoResult.cs
new file mode 100644
index 00000000..5c2ec468
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ModelBinding/ComplexModelDtoResult.cs
@@ -0,0 +1,22 @@
+using System;
+
+namespace Microsoft.Web.Mvc.ModelBinding
+{
+ public sealed class ComplexModelDtoResult
+ {
+ public ComplexModelDtoResult(object model, ModelValidationNode validationNode)
+ {
+ if (validationNode == null)
+ {
+ throw new ArgumentNullException("validationNode");
+ }
+
+ Model = model;
+ ValidationNode = validationNode;
+ }
+
+ public object Model { get; private set; }
+
+ public ModelValidationNode ValidationNode { get; private set; }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ModelBinding/DictionaryModelBinderProvider.cs b/src/Microsoft.Web.Mvc/ModelBinding/DictionaryModelBinderProvider.cs
new file mode 100644
index 00000000..66442a2b
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ModelBinding/DictionaryModelBinderProvider.cs
@@ -0,0 +1,22 @@
+using System.Collections.Generic;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc.ModelBinding
+{
+ public sealed class DictionaryModelBinderProvider : ModelBinderProvider
+ {
+ public override IExtensibleModelBinder GetBinder(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext)
+ {
+ ModelBinderUtil.ValidateBindingContext(bindingContext);
+
+ if (bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName))
+ {
+ return CollectionModelBinderUtil.GetGenericBinder(typeof(IDictionary<,>), typeof(Dictionary<,>), typeof(DictionaryModelBinder<,>), bindingContext.ModelMetadata);
+ }
+ else
+ {
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ModelBinding/DictionaryModelBinder`2.cs b/src/Microsoft.Web.Mvc/ModelBinding/DictionaryModelBinder`2.cs
new file mode 100644
index 00000000..58260c78
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ModelBinding/DictionaryModelBinder`2.cs
@@ -0,0 +1,14 @@
+using System.Collections.Generic;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc.ModelBinding
+{
+ public class DictionaryModelBinder<TKey, TValue> : CollectionModelBinder<KeyValuePair<TKey, TValue>>
+ {
+ protected override bool CreateOrReplaceCollection(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext, IList<KeyValuePair<TKey, TValue>> newCollection)
+ {
+ CollectionModelBinderUtil.CreateOrReplaceDictionary(bindingContext, newCollection, () => new Dictionary<TKey, TValue>());
+ return true;
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ModelBinding/ExtensibleModelBinderAdapter.cs b/src/Microsoft.Web.Mvc/ModelBinding/ExtensibleModelBinderAdapter.cs
new file mode 100644
index 00000000..9bcc81aa
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ModelBinding/ExtensibleModelBinderAdapter.cs
@@ -0,0 +1,68 @@
+using System;
+using System.Linq;
+using System.Web.Mvc;
+using Microsoft.Web.Mvc.Properties;
+
+namespace Microsoft.Web.Mvc.ModelBinding
+{
+ // A model binder that is used to interface between the old system and the new system.
+ public sealed class ExtensibleModelBinderAdapter : IModelBinder
+ {
+ public ExtensibleModelBinderAdapter(ModelBinderProviderCollection providers)
+ {
+ Providers = providers ?? ModelBinderProviders.Providers;
+ }
+
+ public ModelBinderProviderCollection Providers { get; private set; }
+
+ public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
+ {
+ CheckPropertyFilter(bindingContext);
+ ExtensibleModelBindingContext newBindingContext = CreateNewBindingContext(bindingContext, bindingContext.ModelName);
+
+ IExtensibleModelBinder binder = Providers.GetBinder(controllerContext, newBindingContext);
+ if (binder == null && !String.IsNullOrEmpty(bindingContext.ModelName)
+ && bindingContext.FallbackToEmptyPrefix && bindingContext.ModelMetadata.IsComplexType)
+ {
+ // fallback to empty prefix?
+ newBindingContext = CreateNewBindingContext(bindingContext, String.Empty /* modelName */);
+ binder = Providers.GetBinder(controllerContext, newBindingContext);
+ }
+
+ if (binder != null)
+ {
+ bool boundSuccessfully = binder.BindModel(controllerContext, newBindingContext);
+ if (boundSuccessfully)
+ {
+ // run validation and return the model
+ newBindingContext.ValidationNode.Validate(controllerContext, null /* parentNode */);
+ return newBindingContext.Model;
+ }
+ }
+
+ return null; // something went wrong
+ }
+
+ private static void CheckPropertyFilter(ModelBindingContext bindingContext)
+ {
+ if (bindingContext.ModelType.GetProperties().Select(p => p.Name).Any(name => !bindingContext.PropertyFilter(name)))
+ {
+ throw new InvalidOperationException(MvcResources.ExtensibleModelBinderAdapter_PropertyFilterMustNotBeSet);
+ }
+ }
+
+ private ExtensibleModelBindingContext CreateNewBindingContext(ModelBindingContext oldBindingContext, string modelName)
+ {
+ ExtensibleModelBindingContext newBindingContext = new ExtensibleModelBindingContext
+ {
+ ModelBinderProviders = Providers,
+ ModelMetadata = oldBindingContext.ModelMetadata,
+ ModelName = modelName,
+ ModelState = oldBindingContext.ModelState,
+ ValueProvider = oldBindingContext.ValueProvider
+ };
+
+ return newBindingContext;
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ModelBinding/ExtensibleModelBinderAttribute.cs b/src/Microsoft.Web.Mvc/ModelBinding/ExtensibleModelBinderAttribute.cs
new file mode 100644
index 00000000..95db1623
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ModelBinding/ExtensibleModelBinderAttribute.cs
@@ -0,0 +1,17 @@
+using System;
+
+namespace Microsoft.Web.Mvc.ModelBinding
+{
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = false, Inherited = true)]
+ public sealed class ExtensibleModelBinderAttribute : Attribute
+ {
+ public ExtensibleModelBinderAttribute(Type binderType)
+ {
+ BinderType = binderType;
+ }
+
+ public Type BinderType { get; private set; }
+
+ public bool SuppressPrefixCheck { get; set; }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ModelBinding/ExtensibleModelBindingContext.cs b/src/Microsoft.Web.Mvc/ModelBinding/ExtensibleModelBindingContext.cs
new file mode 100644
index 00000000..e0a235be
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ModelBinding/ExtensibleModelBindingContext.cs
@@ -0,0 +1,136 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc.ModelBinding
+{
+ public class ExtensibleModelBindingContext
+ {
+ private ModelBinderProviderCollection _modelBinderProviders;
+ private string _modelName;
+ private ModelStateDictionary _modelState;
+ private Dictionary<string, ModelMetadata> _propertyMetadata;
+ private ModelValidationNode _validationNode;
+
+ public ExtensibleModelBindingContext()
+ : this(null)
+ {
+ }
+
+ // copies certain values that won't change between parent and child objects,
+ // e.g. ValueProvider, ModelState
+ public ExtensibleModelBindingContext(ExtensibleModelBindingContext bindingContext)
+ {
+ if (bindingContext != null)
+ {
+ ModelBinderProviders = bindingContext.ModelBinderProviders;
+ ModelState = bindingContext.ModelState;
+ ValueProvider = bindingContext.ValueProvider;
+ }
+ }
+
+ public object Model
+ {
+ get
+ {
+ EnsureModelMetadata();
+ return ModelMetadata.Model;
+ }
+ set
+ {
+ EnsureModelMetadata();
+ ModelMetadata.Model = value;
+ }
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This is writeable to support unit testing")]
+ public ModelBinderProviderCollection ModelBinderProviders
+ {
+ get
+ {
+ if (_modelBinderProviders == null)
+ {
+ _modelBinderProviders = ModelBinding.ModelBinderProviders.Providers;
+ }
+ return _modelBinderProviders;
+ }
+ set { _modelBinderProviders = value; }
+ }
+
+ public ModelMetadata ModelMetadata { get; set; }
+
+ public string ModelName
+ {
+ get
+ {
+ if (_modelName == null)
+ {
+ _modelName = String.Empty;
+ }
+ return _modelName;
+ }
+ set { _modelName = value; }
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This is writeable to support unit testing")]
+ public ModelStateDictionary ModelState
+ {
+ get
+ {
+ if (_modelState == null)
+ {
+ _modelState = new ModelStateDictionary();
+ }
+ return _modelState;
+ }
+ set { _modelState = value; }
+ }
+
+ public Type ModelType
+ {
+ get
+ {
+ EnsureModelMetadata();
+ return ModelMetadata.ModelType;
+ }
+ }
+
+ public IDictionary<string, ModelMetadata> PropertyMetadata
+ {
+ get
+ {
+ if (_propertyMetadata == null)
+ {
+ _propertyMetadata = ModelMetadata.Properties.ToDictionary(m => m.PropertyName, StringComparer.OrdinalIgnoreCase);
+ }
+
+ return _propertyMetadata;
+ }
+ }
+
+ public ModelValidationNode ValidationNode
+ {
+ get
+ {
+ if (_validationNode == null)
+ {
+ _validationNode = new ModelValidationNode(ModelMetadata, ModelName);
+ }
+ return _validationNode;
+ }
+ set { _validationNode = value; }
+ }
+
+ public IValueProvider ValueProvider { get; set; }
+
+ private void EnsureModelMetadata()
+ {
+ if (ModelMetadata == null)
+ {
+ throw Error.ModelBindingContext_ModelMetadataMustBeSet();
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ModelBinding/GenericModelBinderProvider.cs b/src/Microsoft.Web.Mvc/ModelBinding/GenericModelBinderProvider.cs
new file mode 100644
index 00000000..47c3131f
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ModelBinding/GenericModelBinderProvider.cs
@@ -0,0 +1,130 @@
+using System;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc.ModelBinding
+{
+ // Returns a user-specified binder for a given open generic type.
+ public sealed class GenericModelBinderProvider : ModelBinderProvider
+ {
+ private readonly Func<Type[], IExtensibleModelBinder> _modelBinderFactory;
+ private readonly Type _modelType;
+
+ public GenericModelBinderProvider(Type modelType, IExtensibleModelBinder modelBinder)
+ {
+ if (modelType == null)
+ {
+ throw new ArgumentNullException("modelType");
+ }
+ if (modelBinder == null)
+ {
+ throw new ArgumentNullException("modelBinder");
+ }
+
+ ValidateParameters(modelType, null /* modelBinderType */);
+
+ _modelType = modelType;
+ _modelBinderFactory = _ => modelBinder;
+ }
+
+ public GenericModelBinderProvider(Type modelType, Type modelBinderType)
+ {
+ // The binder can be a closed type, in which case it will be instantiated directly. If the binder
+ // is an open type, the type arguments will be determined at runtime and the corresponding closed
+ // type instantiated.
+
+ if (modelType == null)
+ {
+ throw new ArgumentNullException("modelType");
+ }
+ if (modelBinderType == null)
+ {
+ throw new ArgumentNullException("modelBinderType");
+ }
+
+ ValidateParameters(modelType, modelBinderType);
+ bool modelBinderTypeIsOpenGeneric = modelBinderType.IsGenericTypeDefinition;
+
+ _modelType = modelType;
+ _modelBinderFactory = typeArguments =>
+ {
+ Type closedModelBinderType = (modelBinderTypeIsOpenGeneric) ? modelBinderType.MakeGenericType(typeArguments) : modelBinderType;
+ return (IExtensibleModelBinder)Activator.CreateInstance(closedModelBinderType);
+ };
+ }
+
+ public GenericModelBinderProvider(Type modelType, Func<Type[], IExtensibleModelBinder> modelBinderFactory)
+ {
+ if (modelType == null)
+ {
+ throw new ArgumentNullException("modelType");
+ }
+ if (modelBinderFactory == null)
+ {
+ throw new ArgumentNullException("modelBinderFactory");
+ }
+
+ ValidateParameters(modelType, null /* modelBinderType */);
+
+ _modelType = modelType;
+ _modelBinderFactory = modelBinderFactory;
+ }
+
+ public Type ModelType
+ {
+ get { return _modelType; }
+ }
+
+ public bool SuppressPrefixCheck { get; set; }
+
+ public override IExtensibleModelBinder GetBinder(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext)
+ {
+ ModelBinderUtil.ValidateBindingContext(bindingContext);
+
+ Type[] typeArguments = null;
+ if (ModelType.IsInterface)
+ {
+ Type matchingClosedInterface = TypeHelpers.ExtractGenericInterface(bindingContext.ModelType, ModelType);
+ if (matchingClosedInterface != null)
+ {
+ typeArguments = matchingClosedInterface.GetGenericArguments();
+ }
+ }
+ else
+ {
+ typeArguments = TypeHelpers.GetTypeArgumentsIfMatch(bindingContext.ModelType, ModelType);
+ }
+
+ if (typeArguments != null)
+ {
+ if (SuppressPrefixCheck || bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName))
+ {
+ return _modelBinderFactory(typeArguments);
+ }
+ }
+
+ return null;
+ }
+
+ private static void ValidateParameters(Type modelType, Type modelBinderType)
+ {
+ if (!modelType.IsGenericTypeDefinition)
+ {
+ throw Error.GenericModelBinderProvider_ParameterMustSpecifyOpenGenericType(modelType, "modelType");
+ }
+ if (modelBinderType != null)
+ {
+ if (!typeof(IExtensibleModelBinder).IsAssignableFrom(modelBinderType))
+ {
+ throw Error.Common_TypeMustImplementInterface(modelBinderType, typeof(IExtensibleModelBinder), "modelBinderType");
+ }
+ if (modelBinderType.IsGenericTypeDefinition)
+ {
+ if (modelType.GetGenericArguments().Length != modelBinderType.GetGenericArguments().Length)
+ {
+ throw Error.GenericModelBinderProvider_TypeArgumentCountMismatch(modelType, modelBinderType);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ModelBinding/IExtensibleModelBinder.cs b/src/Microsoft.Web.Mvc/ModelBinding/IExtensibleModelBinder.cs
new file mode 100644
index 00000000..070c07ed
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ModelBinding/IExtensibleModelBinder.cs
@@ -0,0 +1,9 @@
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc.ModelBinding
+{
+ public interface IExtensibleModelBinder
+ {
+ bool BindModel(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext);
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ModelBinding/KeyValuePairModelBinderProvider.cs b/src/Microsoft.Web.Mvc/ModelBinding/KeyValuePairModelBinderProvider.cs
new file mode 100644
index 00000000..82619c62
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ModelBinding/KeyValuePairModelBinderProvider.cs
@@ -0,0 +1,26 @@
+using System.Collections.Generic;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc.ModelBinding
+{
+ public sealed class KeyValuePairModelBinderProvider : ModelBinderProvider
+ {
+ public override IExtensibleModelBinder GetBinder(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext)
+ {
+ ModelBinderUtil.ValidateBindingContext(bindingContext);
+
+ string keyFieldName = ModelBinderUtil.CreatePropertyModelName(bindingContext.ModelName, "key");
+ string valueFieldName = ModelBinderUtil.CreatePropertyModelName(bindingContext.ModelName, "value");
+
+ if (bindingContext.ValueProvider.ContainsPrefix(keyFieldName) && bindingContext.ValueProvider.ContainsPrefix(valueFieldName))
+ {
+ return ModelBinderUtil.GetPossibleBinderInstance(bindingContext.ModelType, typeof(KeyValuePair<,>) /* supported model type */, typeof(KeyValuePairModelBinder<,>) /* binder type */);
+ }
+ else
+ {
+ // 'key' or 'value' missing
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ModelBinding/KeyValuePairModelBinderUtil.cs b/src/Microsoft.Web.Mvc/ModelBinding/KeyValuePairModelBinderUtil.cs
new file mode 100644
index 00000000..f550308b
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ModelBinding/KeyValuePairModelBinderUtil.cs
@@ -0,0 +1,31 @@
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc.ModelBinding
+{
+ internal static class KeyValuePairModelBinderUtil
+ {
+ public static bool TryBindStrongModel<TModel>(ControllerContext controllerContext, ExtensibleModelBindingContext parentBindingContext, string propertyName, ModelMetadataProvider metadataProvider, out TModel model)
+ {
+ ExtensibleModelBindingContext propertyBindingContext = new ExtensibleModelBindingContext(parentBindingContext)
+ {
+ ModelMetadata = metadataProvider.GetMetadataForType(null, typeof(TModel)),
+ ModelName = ModelBinderUtil.CreatePropertyModelName(parentBindingContext.ModelName, propertyName)
+ };
+
+ IExtensibleModelBinder binder = parentBindingContext.ModelBinderProviders.GetBinder(controllerContext, propertyBindingContext);
+ if (binder != null)
+ {
+ if (binder.BindModel(controllerContext, propertyBindingContext))
+ {
+ object untypedModel = propertyBindingContext.Model;
+ model = ModelBinderUtil.CastOrDefault<TModel>(untypedModel);
+ parentBindingContext.ValidationNode.ChildNodes.Add(propertyBindingContext.ValidationNode);
+ return true;
+ }
+ }
+
+ model = default(TModel);
+ return false;
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ModelBinding/KeyValuePairModelBinder`2.cs b/src/Microsoft.Web.Mvc/ModelBinding/KeyValuePairModelBinder`2.cs
new file mode 100644
index 00000000..949f071f
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ModelBinding/KeyValuePairModelBinder`2.cs
@@ -0,0 +1,40 @@
+using System.Collections.Generic;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc.ModelBinding
+{
+ public sealed class KeyValuePairModelBinder<TKey, TValue> : IExtensibleModelBinder
+ {
+ private ModelMetadataProvider _metadataProvider;
+
+ internal ModelMetadataProvider MetadataProvider
+ {
+ get
+ {
+ if (_metadataProvider == null)
+ {
+ _metadataProvider = ModelMetadataProviders.Current;
+ }
+ return _metadataProvider;
+ }
+ set { _metadataProvider = value; }
+ }
+
+ public bool BindModel(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext)
+ {
+ ModelBinderUtil.ValidateBindingContext(bindingContext, typeof(KeyValuePair<TKey, TValue>), true /* allowNullModel */);
+
+ TKey key;
+ bool keyBindingSucceeded = KeyValuePairModelBinderUtil.TryBindStrongModel(controllerContext, bindingContext, "key", MetadataProvider, out key);
+
+ TValue value;
+ bool valueBindingSucceeded = KeyValuePairModelBinderUtil.TryBindStrongModel(controllerContext, bindingContext, "value", MetadataProvider, out value);
+
+ if (keyBindingSucceeded && valueBindingSucceeded)
+ {
+ bindingContext.Model = new KeyValuePair<TKey, TValue>(key, value);
+ }
+ return keyBindingSucceeded || valueBindingSucceeded;
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ModelBinding/ModelBinderConfig.cs b/src/Microsoft.Web.Mvc/ModelBinding/ModelBinderConfig.cs
new file mode 100644
index 00000000..4ec14d5e
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ModelBinding/ModelBinderConfig.cs
@@ -0,0 +1,99 @@
+using System;
+using System.Globalization;
+using System.Web.Mvc;
+using Microsoft.Web.Mvc.Properties;
+
+namespace Microsoft.Web.Mvc.ModelBinding
+{
+ // Provides configuration settings common to the new model binding system.
+ public static class ModelBinderConfig
+ {
+ private static ModelBinderErrorMessageProvider _typeConversionErrorMessageProvider;
+ private static ModelBinderErrorMessageProvider _valueRequiredErrorMessageProvider;
+
+ public static ModelBinderErrorMessageProvider TypeConversionErrorMessageProvider
+ {
+ get
+ {
+ if (_typeConversionErrorMessageProvider == null)
+ {
+ _typeConversionErrorMessageProvider = DefaultTypeConversionErrorMessageProvider;
+ }
+ return _typeConversionErrorMessageProvider;
+ }
+ set { _typeConversionErrorMessageProvider = value; }
+ }
+
+ public static ModelBinderErrorMessageProvider ValueRequiredErrorMessageProvider
+ {
+ get
+ {
+ if (_valueRequiredErrorMessageProvider == null)
+ {
+ _valueRequiredErrorMessageProvider = DefaultValueRequiredErrorMessageProvider;
+ }
+ return _valueRequiredErrorMessageProvider;
+ }
+ set { _valueRequiredErrorMessageProvider = value; }
+ }
+
+ private static string DefaultTypeConversionErrorMessageProvider(ControllerContext controllerContext, ModelMetadata modelMetadata, object incomingValue)
+ {
+ return GetResourceCommon(controllerContext, modelMetadata, incomingValue, GetValueInvalidResource);
+ }
+
+ private static string DefaultValueRequiredErrorMessageProvider(ControllerContext controllerContext, ModelMetadata modelMetadata, object incomingValue)
+ {
+ return GetResourceCommon(controllerContext, modelMetadata, incomingValue, GetValueRequiredResource);
+ }
+
+ private static string GetResourceCommon(ControllerContext controllerContext, ModelMetadata modelMetadata, object incomingValue, Func<ControllerContext, string> resourceAccessor)
+ {
+ string displayName = modelMetadata.GetDisplayName();
+ string errorMessageTemplate = resourceAccessor(controllerContext);
+ string errorMessage = String.Format(CultureInfo.CurrentCulture, errorMessageTemplate, incomingValue, displayName);
+ return errorMessage;
+ }
+
+ private static string GetUserResourceString(ControllerContext controllerContext, string resourceName)
+ {
+ return GetUserResourceString(controllerContext, resourceName, DefaultModelBinder.ResourceClassKey);
+ }
+
+ // If the user specified a ResourceClassKey try to load the resource they specified.
+ // If the class key is invalid, an exception will be thrown.
+ // If the class key is valid but the resource is not found, it returns null, in which
+ // case it will fall back to the MVC default error message.
+ internal static string GetUserResourceString(ControllerContext controllerContext, string resourceName, string resourceClassKey)
+ {
+ return (!String.IsNullOrEmpty(resourceClassKey) && (controllerContext != null) && (controllerContext.HttpContext != null))
+ ? controllerContext.HttpContext.GetGlobalResourceObject(resourceClassKey, resourceName, CultureInfo.CurrentUICulture) as string
+ : null;
+ }
+
+ private static string GetValueInvalidResource(ControllerContext controllerContext)
+ {
+ return GetUserResourceString(controllerContext, "PropertyValueInvalid") ?? MvcResources.ModelBinderConfig_ValueInvalid;
+ }
+
+ private static string GetValueRequiredResource(ControllerContext controllerContext)
+ {
+ return GetUserResourceString(controllerContext, "PropertyValueRequired") ?? MvcResources.ModelBinderConfig_ValueRequired;
+ }
+
+ /*
+ * Initialization routines which replace the default binder implementation with the new binder implementation.
+ */
+
+ public static void Initialize()
+ {
+ Initialize(ModelBinders.Binders, ModelBinderProviders.Providers);
+ }
+
+ internal static void Initialize(ModelBinderDictionary binders, ModelBinderProviderCollection providers)
+ {
+ binders.Clear();
+ binders.DefaultBinder = new ExtensibleModelBinderAdapter(providers);
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ModelBinding/ModelBinderErrorMessageProvider.cs b/src/Microsoft.Web.Mvc/ModelBinding/ModelBinderErrorMessageProvider.cs
new file mode 100644
index 00000000..80d6c5d1
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ModelBinding/ModelBinderErrorMessageProvider.cs
@@ -0,0 +1,6 @@
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc.ModelBinding
+{
+ public delegate string ModelBinderErrorMessageProvider(ControllerContext controllerContext, ModelMetadata modelMetadata, object incomingValue);
+}
diff --git a/src/Microsoft.Web.Mvc/ModelBinding/ModelBinderProvider.cs b/src/Microsoft.Web.Mvc/ModelBinding/ModelBinderProvider.cs
new file mode 100644
index 00000000..368a4f47
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ModelBinding/ModelBinderProvider.cs
@@ -0,0 +1,9 @@
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc.ModelBinding
+{
+ public abstract class ModelBinderProvider
+ {
+ public abstract IExtensibleModelBinder GetBinder(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext);
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ModelBinding/ModelBinderProviderCollection.cs b/src/Microsoft.Web.Mvc/ModelBinding/ModelBinderProviderCollection.cs
new file mode 100644
index 00000000..61d56bd2
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ModelBinding/ModelBinderProviderCollection.cs
@@ -0,0 +1,177 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Globalization;
+using System.Linq;
+using System.Web.Mvc;
+using Microsoft.Web.Mvc.Properties;
+
+namespace Microsoft.Web.Mvc.ModelBinding
+{
+ public sealed class ModelBinderProviderCollection : Collection<ModelBinderProvider>
+ {
+ public ModelBinderProviderCollection()
+ {
+ }
+
+ public ModelBinderProviderCollection(IList<ModelBinderProvider> list)
+ : base(list)
+ {
+ }
+
+ private static void EnsureNoBindAttribute(Type modelType)
+ {
+ if (TypeDescriptorHelper.Get(modelType).GetAttributes().OfType<BindAttribute>().Any())
+ {
+ string errorMessage = String.Format(CultureInfo.CurrentCulture, MvcResources.ModelBinderProviderCollection_TypeCannotHaveBindAttribute,
+ modelType);
+ throw new InvalidOperationException(errorMessage);
+ }
+ }
+
+ public IExtensibleModelBinder GetBinder(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext)
+ {
+ if (controllerContext == null)
+ {
+ throw new ArgumentNullException("controllerContext");
+ }
+ if (bindingContext == null)
+ {
+ throw new ArgumentNullException("bindingContext");
+ }
+
+ EnsureNoBindAttribute(bindingContext.ModelType);
+
+ ModelBinderProvider providerFromAttr;
+ if (TryGetProviderFromAttributes(bindingContext.ModelType, out providerFromAttr))
+ {
+ return providerFromAttr.GetBinder(controllerContext, bindingContext);
+ }
+
+ return (from provider in this
+ let binder = provider.GetBinder(controllerContext, bindingContext)
+ where binder != null
+ select binder).FirstOrDefault();
+ }
+
+ internal IExtensibleModelBinder GetRequiredBinder(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext)
+ {
+ IExtensibleModelBinder binder = GetBinder(controllerContext, bindingContext);
+ if (binder == null)
+ {
+ throw Error.ModelBinderProviderCollection_BinderForTypeNotFound(bindingContext.ModelType);
+ }
+ return binder;
+ }
+
+ protected override void InsertItem(int index, ModelBinderProvider item)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+
+ base.InsertItem(index, item);
+ }
+
+ private void InsertSimpleProviderAtFront(ModelBinderProvider provider)
+ {
+ // Don't want to insert simple providers before any that are marked as "should go first,"
+ // as that might throw off other providers like the exact type match provider.
+
+ int i = 0;
+ for (; i < Count; i++)
+ {
+ if (!ShouldProviderGoFirst(this[i]))
+ {
+ break;
+ }
+ }
+
+ base.InsertItem(i, provider);
+ }
+
+ public void RegisterBinderForGenericType(Type modelType, IExtensibleModelBinder modelBinder)
+ {
+ InsertSimpleProviderAtFront(new GenericModelBinderProvider(modelType, modelBinder));
+ }
+
+ public void RegisterBinderForGenericType(Type modelType, Func<Type[], IExtensibleModelBinder> modelBinderFactory)
+ {
+ InsertSimpleProviderAtFront(new GenericModelBinderProvider(modelType, modelBinderFactory));
+ }
+
+ public void RegisterBinderForGenericType(Type modelType, Type modelBinderType)
+ {
+ InsertSimpleProviderAtFront(new GenericModelBinderProvider(modelType, modelBinderType));
+ }
+
+ public void RegisterBinderForType(Type modelType, IExtensibleModelBinder modelBinder)
+ {
+ RegisterBinderForType(modelType, modelBinder, false /* suppressPrefixCheck */);
+ }
+
+ internal void RegisterBinderForType(Type modelType, IExtensibleModelBinder modelBinder, bool suppressPrefixCheck)
+ {
+ SimpleModelBinderProvider provider = new SimpleModelBinderProvider(modelType, modelBinder)
+ {
+ SuppressPrefixCheck = suppressPrefixCheck
+ };
+ InsertSimpleProviderAtFront(provider);
+ }
+
+ public void RegisterBinderForType(Type modelType, Func<IExtensibleModelBinder> modelBinderFactory)
+ {
+ InsertSimpleProviderAtFront(new SimpleModelBinderProvider(modelType, modelBinderFactory));
+ }
+
+ protected override void SetItem(int index, ModelBinderProvider item)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+
+ base.SetItem(index, item);
+ }
+
+ private static bool ShouldProviderGoFirst(ModelBinderProvider provider)
+ {
+ ModelBinderProviderOptionsAttribute options = provider.GetType()
+ .GetCustomAttributes(typeof(ModelBinderProviderOptionsAttribute), true /* inherit */)
+ .OfType<ModelBinderProviderOptionsAttribute>()
+ .FirstOrDefault();
+
+ return (options != null) ? options.FrontOfList : false;
+ }
+
+ private static bool TryGetProviderFromAttributes(Type modelType, out ModelBinderProvider provider)
+ {
+ ExtensibleModelBinderAttribute attr = TypeDescriptorHelper.Get(modelType).GetAttributes().OfType<ExtensibleModelBinderAttribute>().FirstOrDefault();
+ if (attr == null)
+ {
+ provider = null;
+ return false;
+ }
+
+ if (typeof(ModelBinderProvider).IsAssignableFrom(attr.BinderType))
+ {
+ provider = (ModelBinderProvider)Activator.CreateInstance(attr.BinderType);
+ }
+ else if (typeof(IExtensibleModelBinder).IsAssignableFrom(attr.BinderType))
+ {
+ Type closedBinderType = (attr.BinderType.IsGenericTypeDefinition) ? attr.BinderType.MakeGenericType(modelType.GetGenericArguments()) : attr.BinderType;
+ IExtensibleModelBinder binderInstance = (IExtensibleModelBinder)Activator.CreateInstance(closedBinderType);
+ provider = new SimpleModelBinderProvider(modelType, binderInstance) { SuppressPrefixCheck = attr.SuppressPrefixCheck };
+ }
+ else
+ {
+ string errorMessage = String.Format(CultureInfo.CurrentCulture, MvcResources.ModelBinderProviderCollection_InvalidBinderType,
+ attr.BinderType, typeof(ModelBinderProvider), typeof(IExtensibleModelBinder));
+ throw new InvalidOperationException(errorMessage);
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ModelBinding/ModelBinderProviderOptionsAttribute.cs b/src/Microsoft.Web.Mvc/ModelBinding/ModelBinderProviderOptionsAttribute.cs
new file mode 100644
index 00000000..2088ff5f
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ModelBinding/ModelBinderProviderOptionsAttribute.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace Microsoft.Web.Mvc.ModelBinding
+{
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
+ public sealed class ModelBinderProviderOptionsAttribute : Attribute
+ {
+ // Specifies that a provider should appear at the front of the list, e.g. other providers should
+ // not be auto-registered at the front unless explicitly requested.
+ public bool FrontOfList { get; set; }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ModelBinding/ModelBinderProviders.cs b/src/Microsoft.Web.Mvc/ModelBinding/ModelBinderProviders.cs
new file mode 100644
index 00000000..8dd80cb3
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ModelBinding/ModelBinderProviders.cs
@@ -0,0 +1,28 @@
+namespace Microsoft.Web.Mvc.ModelBinding
+{
+ public static class ModelBinderProviders
+ {
+ private static readonly ModelBinderProviderCollection _providers = CreateDefaultCollection();
+
+ public static ModelBinderProviderCollection Providers
+ {
+ get { return _providers; }
+ }
+
+ private static ModelBinderProviderCollection CreateDefaultCollection()
+ {
+ return new ModelBinderProviderCollection
+ {
+ new TypeMatchModelBinderProvider(),
+ new BinaryDataModelBinderProvider(),
+ new KeyValuePairModelBinderProvider(),
+ new ComplexModelDtoModelBinderProvider(),
+ new ArrayModelBinderProvider(),
+ new DictionaryModelBinderProvider(),
+ new CollectionModelBinderProvider(),
+ new TypeConverterModelBinderProvider(),
+ new MutableObjectModelBinderProvider()
+ };
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ModelBinding/ModelBinderUtil.cs b/src/Microsoft.Web.Mvc/ModelBinding/ModelBinderUtil.cs
new file mode 100644
index 00000000..b1615125
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ModelBinding/ModelBinderUtil.cs
@@ -0,0 +1,136 @@
+using System;
+using System.Collections;
+using System.Globalization;
+using System.Linq;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc.ModelBinding
+{
+ internal static class ModelBinderUtil
+ {
+ public static TModel CastOrDefault<TModel>(object model)
+ {
+ return (model is TModel) ? (TModel)model : default(TModel);
+ }
+
+ public static string CreateIndexModelName(string parentName, int index)
+ {
+ return CreateIndexModelName(parentName, index.ToString(CultureInfo.InvariantCulture));
+ }
+
+ public static string CreateIndexModelName(string parentName, string index)
+ {
+ return (parentName.Length == 0) ? "[" + index + "]" : parentName + "[" + index + "]";
+ }
+
+ public static string CreatePropertyModelName(string prefix, string propertyName)
+ {
+ if (String.IsNullOrEmpty(prefix))
+ {
+ return propertyName ?? String.Empty;
+ }
+ else if (String.IsNullOrEmpty(propertyName))
+ {
+ return prefix ?? String.Empty;
+ }
+ else
+ {
+ return prefix + "." + propertyName;
+ }
+ }
+
+ public static IExtensibleModelBinder GetPossibleBinderInstance(Type closedModelType, Type openModelType, Type openBinderType)
+ {
+ Type[] typeArguments = TypeHelpers.GetTypeArgumentsIfMatch(closedModelType, openModelType);
+ return (typeArguments != null) ? (IExtensibleModelBinder)Activator.CreateInstance(openBinderType.MakeGenericType(typeArguments)) : null;
+ }
+
+ public static object[] RawValueToObjectArray(object rawValue)
+ {
+ // precondition: rawValue is not null
+
+ // Need to special-case String so it's not caught by the IEnumerable check which follows
+ if (rawValue is string)
+ {
+ return new[] { rawValue };
+ }
+
+ object[] rawValueAsObjectArray = rawValue as object[];
+ if (rawValueAsObjectArray != null)
+ {
+ return rawValueAsObjectArray;
+ }
+
+ IEnumerable rawValueAsEnumerable = rawValue as IEnumerable;
+ if (rawValueAsEnumerable != null)
+ {
+ return rawValueAsEnumerable.Cast<object>().ToArray();
+ }
+
+ // fallback
+ return new[] { rawValue };
+ }
+
+ public static void ReplaceEmptyStringWithNull(ModelMetadata modelMetadata, ref object model)
+ {
+ if (modelMetadata.ConvertEmptyStringToNull && StringIsEmptyOrWhitespace(model as string))
+ {
+ model = null;
+ }
+ }
+
+ // Based on String.IsNullOrWhitespace
+ private static bool StringIsEmptyOrWhitespace(string s)
+ {
+ if (s == null)
+ {
+ return false;
+ }
+
+ if (s.Length != 0)
+ {
+ for (int i = 0; i < s.Length; i++)
+ {
+ if (!Char.IsWhiteSpace(s[i]))
+ {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ public static void ValidateBindingContext(ExtensibleModelBindingContext bindingContext)
+ {
+ if (bindingContext == null)
+ {
+ throw new ArgumentNullException("bindingContext");
+ }
+
+ if (bindingContext.ModelMetadata == null)
+ {
+ throw Error.ModelBinderUtil_ModelMetadataCannotBeNull();
+ }
+ }
+
+ public static void ValidateBindingContext(ExtensibleModelBindingContext bindingContext, Type requiredType, bool allowNullModel)
+ {
+ ValidateBindingContext(bindingContext);
+
+ if (bindingContext.ModelType != requiredType)
+ {
+ throw Error.ModelBinderUtil_ModelTypeIsWrong(bindingContext.ModelType, requiredType);
+ }
+
+ if (!allowNullModel && bindingContext.Model == null)
+ {
+ throw Error.ModelBinderUtil_ModelCannotBeNull(requiredType);
+ }
+
+ if (bindingContext.Model != null && !requiredType.IsInstanceOfType(bindingContext.Model))
+ {
+ throw Error.ModelBinderUtil_ModelInstanceIsWrong(bindingContext.Model.GetType(), requiredType);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ModelBinding/ModelValidatedEventArgs.cs b/src/Microsoft.Web.Mvc/ModelBinding/ModelValidatedEventArgs.cs
new file mode 100644
index 00000000..b55e9c8f
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ModelBinding/ModelValidatedEventArgs.cs
@@ -0,0 +1,23 @@
+using System;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc.ModelBinding
+{
+ public sealed class ModelValidatedEventArgs : EventArgs
+ {
+ public ModelValidatedEventArgs(ControllerContext controllerContext, ModelValidationNode parentNode)
+ {
+ if (controllerContext == null)
+ {
+ throw new ArgumentNullException("controllerContext");
+ }
+
+ ControllerContext = controllerContext;
+ ParentNode = parentNode;
+ }
+
+ public ControllerContext ControllerContext { get; private set; }
+
+ public ModelValidationNode ParentNode { get; private set; }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ModelBinding/ModelValidatingEventArgs.cs b/src/Microsoft.Web.Mvc/ModelBinding/ModelValidatingEventArgs.cs
new file mode 100644
index 00000000..aeaaf8ed
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ModelBinding/ModelValidatingEventArgs.cs
@@ -0,0 +1,24 @@
+using System;
+using System.ComponentModel;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc.ModelBinding
+{
+ public sealed class ModelValidatingEventArgs : CancelEventArgs
+ {
+ public ModelValidatingEventArgs(ControllerContext controllerContext, ModelValidationNode parentNode)
+ {
+ if (controllerContext == null)
+ {
+ throw new ArgumentNullException("controllerContext");
+ }
+
+ ControllerContext = controllerContext;
+ ParentNode = parentNode;
+ }
+
+ public ControllerContext ControllerContext { get; private set; }
+
+ public ModelValidationNode ParentNode { get; private set; }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ModelBinding/ModelValidationNode.cs b/src/Microsoft.Web.Mvc/ModelBinding/ModelValidationNode.cs
new file mode 100644
index 00000000..e328de85
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ModelBinding/ModelValidationNode.cs
@@ -0,0 +1,193 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc.ModelBinding
+{
+ public sealed class ModelValidationNode
+ {
+ public ModelValidationNode(ModelMetadata modelMetadata, string modelStateKey)
+ : this(modelMetadata, modelStateKey, null)
+ {
+ }
+
+ public ModelValidationNode(ModelMetadata modelMetadata, string modelStateKey, IEnumerable<ModelValidationNode> childNodes)
+ {
+ if (modelMetadata == null)
+ {
+ throw new ArgumentNullException("modelMetadata");
+ }
+ if (modelStateKey == null)
+ {
+ throw new ArgumentNullException("modelStateKey");
+ }
+
+ ModelMetadata = modelMetadata;
+ ModelStateKey = modelStateKey;
+ ChildNodes = (childNodes != null) ? childNodes.ToList() : new List<ModelValidationNode>();
+ }
+
+ public event EventHandler<ModelValidatedEventArgs> Validated;
+
+ public event EventHandler<ModelValidatingEventArgs> Validating;
+
+ public ICollection<ModelValidationNode> ChildNodes { get; private set; }
+
+ public ModelMetadata ModelMetadata { get; private set; }
+
+ public string ModelStateKey { get; private set; }
+
+ public bool ValidateAllProperties { get; set; }
+
+ public bool SuppressValidation { get; set; }
+
+ public void CombineWith(ModelValidationNode otherNode)
+ {
+ if (otherNode != null && !otherNode.SuppressValidation)
+ {
+ Validated += otherNode.Validated;
+ Validating += otherNode.Validating;
+ foreach (ModelValidationNode childNode in otherNode.ChildNodes)
+ {
+ ChildNodes.Add(childNode);
+ }
+ }
+ }
+
+ private void OnValidated(ModelValidatedEventArgs e)
+ {
+ EventHandler<ModelValidatedEventArgs> handler = Validated;
+ if (handler != null)
+ {
+ handler(this, e);
+ }
+ }
+
+ private void OnValidating(ModelValidatingEventArgs e)
+ {
+ EventHandler<ModelValidatingEventArgs> handler = Validating;
+ if (handler != null)
+ {
+ handler(this, e);
+ }
+ }
+
+ private object TryConvertContainerToMetadataType(ModelValidationNode parentNode)
+ {
+ if (parentNode != null)
+ {
+ object containerInstance = parentNode.ModelMetadata.Model;
+ if (containerInstance != null)
+ {
+ Type expectedContainerType = ModelMetadata.ContainerType;
+ if (expectedContainerType != null)
+ {
+ if (expectedContainerType.IsInstanceOfType(containerInstance))
+ {
+ return containerInstance;
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public void Validate(ControllerContext controllerContext)
+ {
+ Validate(controllerContext, null /* parentNode */);
+ }
+
+ public void Validate(ControllerContext controllerContext, ModelValidationNode parentNode)
+ {
+ if (controllerContext == null)
+ {
+ throw new ArgumentNullException("controllerContext");
+ }
+
+ if (SuppressValidation)
+ {
+ // no-op
+ return;
+ }
+
+ // pre-validation steps
+ ModelValidatingEventArgs validatingEventArgs = new ModelValidatingEventArgs(controllerContext, parentNode);
+ OnValidating(validatingEventArgs);
+ if (validatingEventArgs.Cancel)
+ {
+ return;
+ }
+
+ ValidateChildren(controllerContext);
+ ValidateThis(controllerContext, parentNode);
+
+ // post-validation steps
+ ModelValidatedEventArgs validatedEventArgs = new ModelValidatedEventArgs(controllerContext, parentNode);
+ OnValidated(validatedEventArgs);
+ }
+
+ private void ValidateChildren(ControllerContext controllerContext)
+ {
+ foreach (ModelValidationNode child in ChildNodes)
+ {
+ child.Validate(controllerContext, this);
+ }
+
+ if (ValidateAllProperties)
+ {
+ ValidateProperties(controllerContext);
+ }
+ }
+
+ private void ValidateProperties(ControllerContext controllerContext)
+ {
+ // Based off CompositeModelValidator.
+ ModelStateDictionary modelState = controllerContext.Controller.ViewData.ModelState;
+
+ // DevDiv Bugs #227802 - Caching problem in ModelMetadata requires us to manually regenerate
+ // the ModelMetadata.
+ object model = ModelMetadata.Model;
+ ModelMetadata updatedMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, ModelMetadata.ModelType);
+
+ foreach (ModelMetadata propertyMetadata in updatedMetadata.Properties)
+ {
+ // Only want to add errors to ModelState if something doesn't already exist for the property node,
+ // else we could end up with duplicate or irrelevant error messages.
+ string propertyKeyRoot = ModelBinderUtil.CreatePropertyModelName(ModelStateKey, propertyMetadata.PropertyName);
+
+ if (modelState.IsValidField(propertyKeyRoot))
+ {
+ foreach (ModelValidator propertyValidator in propertyMetadata.GetValidators(controllerContext))
+ {
+ foreach (ModelValidationResult propertyResult in propertyValidator.Validate(model))
+ {
+ string thisErrorKey = ModelBinderUtil.CreatePropertyModelName(propertyKeyRoot, propertyResult.MemberName);
+ modelState.AddModelError(thisErrorKey, propertyResult.Message);
+ }
+ }
+ }
+ }
+ }
+
+ private void ValidateThis(ControllerContext controllerContext, ModelValidationNode parentNode)
+ {
+ ModelStateDictionary modelState = controllerContext.Controller.ViewData.ModelState;
+ if (!modelState.IsValidField(ModelStateKey))
+ {
+ return; // short-circuit
+ }
+
+ object container = TryConvertContainerToMetadataType(parentNode);
+ foreach (ModelValidator validator in ModelMetadata.GetValidators(controllerContext))
+ {
+ foreach (ModelValidationResult validationResult in validator.Validate(container))
+ {
+ string trueModelStateKey = ModelBinderUtil.CreatePropertyModelName(ModelStateKey, validationResult.MemberName);
+ modelState.AddModelError(trueModelStateKey, validationResult.Message);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ModelBinding/MutableObjectModelBinder.cs b/src/Microsoft.Web.Mvc/ModelBinding/MutableObjectModelBinder.cs
new file mode 100644
index 00000000..6aa1e34f
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ModelBinding/MutableObjectModelBinder.cs
@@ -0,0 +1,264 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc.ModelBinding
+{
+ public class MutableObjectModelBinder : IExtensibleModelBinder
+ {
+ private ModelMetadataProvider _metadataProvider;
+
+ internal ModelMetadataProvider MetadataProvider
+ {
+ get
+ {
+ if (_metadataProvider == null)
+ {
+ _metadataProvider = ModelMetadataProviders.Current;
+ }
+ return _metadataProvider;
+ }
+ set { _metadataProvider = value; }
+ }
+
+ public virtual bool BindModel(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext)
+ {
+ ModelBinderUtil.ValidateBindingContext(bindingContext);
+
+ EnsureModel(controllerContext, bindingContext);
+ IEnumerable<ModelMetadata> propertyMetadatas = GetMetadataForProperties(controllerContext, bindingContext);
+ ComplexModelDto dto = CreateAndPopulateDto(controllerContext, bindingContext, propertyMetadatas);
+
+ // post-processing, e.g. property setters and hooking up validation
+ ProcessDto(controllerContext, bindingContext, dto);
+ bindingContext.ValidationNode.ValidateAllProperties = true; // complex models require full validation
+ return true;
+ }
+
+ protected virtual bool CanUpdateProperty(ModelMetadata propertyMetadata)
+ {
+ return CanUpdatePropertyInternal(propertyMetadata);
+ }
+
+ internal static bool CanUpdatePropertyInternal(ModelMetadata propertyMetadata)
+ {
+ return (!propertyMetadata.IsReadOnly || CanUpdateReadOnlyProperty(propertyMetadata.ModelType));
+ }
+
+ private static bool CanUpdateReadOnlyProperty(Type propertyType)
+ {
+ // Value types have copy-by-value semantics, which prevents us from updating
+ // properties that are marked readonly.
+ if (propertyType.IsValueType)
+ {
+ return false;
+ }
+
+ // Arrays are strange beasts since their contents are mutable but their sizes aren't.
+ // Therefore we shouldn't even try to update these. Further reading:
+ // http://blogs.msdn.com/ericlippert/archive/2008/09/22/arrays-considered-somewhat-harmful.aspx
+ if (propertyType.IsArray)
+ {
+ return false;
+ }
+
+ // Special-case known immutable reference types
+ if (propertyType == typeof(string))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ private ComplexModelDto CreateAndPopulateDto(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext, IEnumerable<ModelMetadata> propertyMetadatas)
+ {
+ // create a DTO and call into the DTO binder
+ ComplexModelDto originalDto = new ComplexModelDto(bindingContext.ModelMetadata, propertyMetadatas);
+ ExtensibleModelBindingContext dtoBindingContext = new ExtensibleModelBindingContext(bindingContext)
+ {
+ ModelMetadata = MetadataProvider.GetMetadataForType(() => originalDto, typeof(ComplexModelDto)),
+ ModelName = bindingContext.ModelName
+ };
+
+ IExtensibleModelBinder dtoBinder = bindingContext.ModelBinderProviders.GetRequiredBinder(controllerContext, dtoBindingContext);
+ dtoBinder.BindModel(controllerContext, dtoBindingContext);
+ return (ComplexModelDto)dtoBindingContext.Model;
+ }
+
+ protected virtual object CreateModel(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext)
+ {
+ // If the Activator throws an exception, we want to propagate it back up the call stack, since the application
+ // developer should know that this was an invalid type to try to bind to.
+ return Activator.CreateInstance(bindingContext.ModelType);
+ }
+
+ // Called when the property setter null check failed, allows us to add our own error message to ModelState.
+ internal static EventHandler<ModelValidatedEventArgs> CreateNullCheckFailedHandler(ControllerContext controllerContext, ModelMetadata modelMetadata, object incomingValue)
+ {
+ return (sender, e) =>
+ {
+ ModelValidationNode validationNode = (ModelValidationNode)sender;
+ ModelStateDictionary modelState = e.ControllerContext.Controller.ViewData.ModelState;
+
+ if (modelState.IsValidField(validationNode.ModelStateKey))
+ {
+ string errorMessage = ModelBinderConfig.ValueRequiredErrorMessageProvider(controllerContext, modelMetadata, incomingValue);
+ if (errorMessage != null)
+ {
+ modelState.AddModelError(validationNode.ModelStateKey, errorMessage);
+ }
+ }
+ };
+ }
+
+ protected virtual void EnsureModel(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext)
+ {
+ if (bindingContext.Model == null)
+ {
+ bindingContext.ModelMetadata.Model = CreateModel(controllerContext, bindingContext);
+ }
+ }
+
+ protected virtual IEnumerable<ModelMetadata> GetMetadataForProperties(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext)
+ {
+ // keep a set of the required properties so that we can cross-reference bound properties later
+ HashSet<string> requiredProperties;
+ HashSet<string> skipProperties;
+ GetRequiredPropertiesCollection(bindingContext.ModelType, out requiredProperties, out skipProperties);
+
+ return from propertyMetadata in bindingContext.ModelMetadata.Properties
+ let propertyName = propertyMetadata.PropertyName
+ let shouldUpdateProperty = requiredProperties.Contains(propertyName) || !skipProperties.Contains(propertyName)
+ where shouldUpdateProperty && CanUpdateProperty(propertyMetadata)
+ select propertyMetadata;
+ }
+
+ private static object GetPropertyDefaultValue(PropertyDescriptor propertyDescriptor)
+ {
+ DefaultValueAttribute attr = propertyDescriptor.Attributes.OfType<DefaultValueAttribute>().FirstOrDefault();
+ return (attr != null) ? attr.Value : null;
+ }
+
+ internal static void GetRequiredPropertiesCollection(Type modelType, out HashSet<string> requiredProperties, out HashSet<string> skipProperties)
+ {
+ requiredProperties = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+ skipProperties = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+
+ // Use attributes on the property before attributes on the type.
+ ICustomTypeDescriptor modelDescriptor = TypeDescriptorHelper.Get(modelType);
+ PropertyDescriptorCollection propertyDescriptors = modelDescriptor.GetProperties();
+ BindingBehaviorAttribute typeAttr = modelDescriptor.GetAttributes().OfType<BindingBehaviorAttribute>().SingleOrDefault();
+
+ foreach (PropertyDescriptor propertyDescriptor in propertyDescriptors)
+ {
+ BindingBehaviorAttribute propAttr = propertyDescriptor.Attributes.OfType<BindingBehaviorAttribute>().SingleOrDefault();
+ BindingBehaviorAttribute workingAttr = propAttr ?? typeAttr;
+ if (workingAttr != null)
+ {
+ switch (workingAttr.Behavior)
+ {
+ case BindingBehavior.Required:
+ requiredProperties.Add(propertyDescriptor.Name);
+ break;
+
+ case BindingBehavior.Never:
+ skipProperties.Add(propertyDescriptor.Name);
+ break;
+ }
+ }
+ }
+ }
+
+ internal void ProcessDto(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext, ComplexModelDto dto)
+ {
+ HashSet<string> requiredProperties;
+ HashSet<string> skipProperties;
+ GetRequiredPropertiesCollection(bindingContext.ModelType, out requiredProperties, out skipProperties);
+
+ // Are all of the required fields accounted for?
+ HashSet<string> missingRequiredProperties = new HashSet<string>(requiredProperties);
+ missingRequiredProperties.ExceptWith(dto.Results.Select(r => r.Key.PropertyName));
+ string missingPropertyName = missingRequiredProperties.FirstOrDefault();
+ if (missingPropertyName != null)
+ {
+ string fullPropertyKey = ModelBinderUtil.CreatePropertyModelName(bindingContext.ModelName, missingPropertyName);
+ throw Error.BindingBehavior_ValueNotFound(fullPropertyKey);
+ }
+
+ // for each property that was bound, call the setter, recording exceptions as necessary
+ foreach (var entry in dto.Results)
+ {
+ ModelMetadata propertyMetadata = entry.Key;
+
+ ComplexModelDtoResult dtoResult = entry.Value;
+ if (dtoResult != null)
+ {
+ SetProperty(controllerContext, bindingContext, propertyMetadata, dtoResult);
+ bindingContext.ValidationNode.ChildNodes.Add(dtoResult.ValidationNode);
+ }
+ }
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We're recording this exception so that we can act on it later.")]
+ protected virtual void SetProperty(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext, ModelMetadata propertyMetadata, ComplexModelDtoResult dtoResult)
+ {
+ PropertyDescriptor propertyDescriptor = TypeDescriptorHelper.Get(bindingContext.ModelType).GetProperties().Find(propertyMetadata.PropertyName, true /* ignoreCase */);
+ if (propertyDescriptor == null || propertyDescriptor.IsReadOnly)
+ {
+ return; // nothing to do
+ }
+
+ object value = dtoResult.Model ?? GetPropertyDefaultValue(propertyDescriptor);
+ propertyMetadata.Model = value;
+
+ // 'Required' validators need to run first so that we can provide useful error messages if
+ // the property setters throw, e.g. if we're setting entity keys to null. See comments in
+ // DefaultModelBinder.SetProperty() for more information.
+ if (value == null)
+ {
+ string modelStateKey = dtoResult.ValidationNode.ModelStateKey;
+ if (bindingContext.ModelState.IsValidField(modelStateKey))
+ {
+ ModelValidator requiredValidator = ModelValidatorProviders.Providers.GetValidators(propertyMetadata, controllerContext).Where(v => v.IsRequired).FirstOrDefault();
+ if (requiredValidator != null)
+ {
+ foreach (ModelValidationResult validationResult in requiredValidator.Validate(bindingContext.Model))
+ {
+ bindingContext.ModelState.AddModelError(modelStateKey, validationResult.Message);
+ }
+ }
+ }
+ }
+
+ if (value != null || TypeHelpers.TypeAllowsNullValue(propertyDescriptor.PropertyType))
+ {
+ try
+ {
+ propertyDescriptor.SetValue(bindingContext.Model, value);
+ }
+ catch (Exception ex)
+ {
+ // don't display a duplicate error message if a binding error has already occurred for this field
+ string modelStateKey = dtoResult.ValidationNode.ModelStateKey;
+ if (bindingContext.ModelState.IsValidField(modelStateKey))
+ {
+ bindingContext.ModelState.AddModelError(modelStateKey, ex);
+ }
+ }
+ }
+ else
+ {
+ // trying to set a non-nullable value type to null, need to make sure there's a message
+ string modelStateKey = dtoResult.ValidationNode.ModelStateKey;
+ if (bindingContext.ModelState.IsValidField(modelStateKey))
+ {
+ dtoResult.ValidationNode.Validated += CreateNullCheckFailedHandler(controllerContext, propertyMetadata, value);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ModelBinding/MutableObjectModelBinderProvider.cs b/src/Microsoft.Web.Mvc/ModelBinding/MutableObjectModelBinderProvider.cs
new file mode 100644
index 00000000..a98156e4
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ModelBinding/MutableObjectModelBinderProvider.cs
@@ -0,0 +1,26 @@
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc.ModelBinding
+{
+ public sealed class MutableObjectModelBinderProvider : ModelBinderProvider
+ {
+ public override IExtensibleModelBinder GetBinder(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext)
+ {
+ ModelBinderUtil.ValidateBindingContext(bindingContext);
+
+ if (!bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName))
+ {
+ // no values to bind
+ return null;
+ }
+
+ if (bindingContext.ModelType == typeof(ComplexModelDto))
+ {
+ // forbidden type - will cause a stack overflow if we try binding this type
+ return null;
+ }
+
+ return new MutableObjectModelBinder();
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ModelBinding/SimpleModelBinderProvider.cs b/src/Microsoft.Web.Mvc/ModelBinding/SimpleModelBinderProvider.cs
new file mode 100644
index 00000000..00ecd6e6
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ModelBinding/SimpleModelBinderProvider.cs
@@ -0,0 +1,64 @@
+using System;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc.ModelBinding
+{
+ // Returns a user-specified binder for a given type.
+ public sealed class SimpleModelBinderProvider : ModelBinderProvider
+ {
+ private readonly Func<IExtensibleModelBinder> _modelBinderFactory;
+ private readonly Type _modelType;
+
+ public SimpleModelBinderProvider(Type modelType, IExtensibleModelBinder modelBinder)
+ {
+ if (modelType == null)
+ {
+ throw new ArgumentNullException("modelType");
+ }
+ if (modelBinder == null)
+ {
+ throw new ArgumentNullException("modelBinder");
+ }
+
+ _modelType = modelType;
+ _modelBinderFactory = () => modelBinder;
+ }
+
+ public SimpleModelBinderProvider(Type modelType, Func<IExtensibleModelBinder> modelBinderFactory)
+ {
+ if (modelType == null)
+ {
+ throw new ArgumentNullException("modelType");
+ }
+ if (modelBinderFactory == null)
+ {
+ throw new ArgumentNullException("modelBinderFactory");
+ }
+
+ _modelType = modelType;
+ _modelBinderFactory = modelBinderFactory;
+ }
+
+ public Type ModelType
+ {
+ get { return _modelType; }
+ }
+
+ public bool SuppressPrefixCheck { get; set; }
+
+ public override IExtensibleModelBinder GetBinder(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext)
+ {
+ ModelBinderUtil.ValidateBindingContext(bindingContext);
+
+ if (bindingContext.ModelType == ModelType)
+ {
+ if (SuppressPrefixCheck || bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName))
+ {
+ return _modelBinderFactory();
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ModelBinding/TypeConverterModelBinder.cs b/src/Microsoft.Web.Mvc/ModelBinding/TypeConverterModelBinder.cs
new file mode 100644
index 00000000..227c95f3
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ModelBinding/TypeConverterModelBinder.cs
@@ -0,0 +1,62 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc.ModelBinding
+{
+ public sealed class TypeConverterModelBinder : IExtensibleModelBinder
+ {
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "The exception is recorded to be acted upon later.")]
+ [SuppressMessage("Microsoft.Globalization", "CA1304:SpecifyCultureInfo", MessageId = "System.Web.Mvc.ValueProviderResult.ConvertTo(System.Type)", Justification = "The ValueProviderResult already has the necessary context to perform a culture-aware conversion.")]
+ public bool BindModel(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext)
+ {
+ ModelBinderUtil.ValidateBindingContext(bindingContext);
+
+ ValueProviderResult valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
+ if (valueProviderResult == null)
+ {
+ return false; // no entry
+ }
+
+ object newModel;
+ bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
+ try
+ {
+ newModel = valueProviderResult.ConvertTo(bindingContext.ModelType);
+ }
+ catch (Exception ex)
+ {
+ if (IsFormatException(ex))
+ {
+ // there was a type conversion failure
+ string errorString = ModelBinderConfig.TypeConversionErrorMessageProvider(controllerContext, bindingContext.ModelMetadata, valueProviderResult.AttemptedValue);
+ if (errorString != null)
+ {
+ bindingContext.ModelState.AddModelError(bindingContext.ModelName, errorString);
+ }
+ }
+ else
+ {
+ bindingContext.ModelState.AddModelError(bindingContext.ModelName, ex);
+ }
+ return false;
+ }
+
+ ModelBinderUtil.ReplaceEmptyStringWithNull(bindingContext.ModelMetadata, ref newModel);
+ bindingContext.Model = newModel;
+ return true;
+ }
+
+ private static bool IsFormatException(Exception ex)
+ {
+ for (; ex != null; ex = ex.InnerException)
+ {
+ if (ex is FormatException)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ModelBinding/TypeConverterModelBinderProvider.cs b/src/Microsoft.Web.Mvc/ModelBinding/TypeConverterModelBinderProvider.cs
new file mode 100644
index 00000000..4e84a5e3
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ModelBinding/TypeConverterModelBinderProvider.cs
@@ -0,0 +1,27 @@
+using System.ComponentModel;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc.ModelBinding
+{
+ // Returns a binder that can perform conversions using a .NET TypeConverter.
+ public sealed class TypeConverterModelBinderProvider : ModelBinderProvider
+ {
+ public override IExtensibleModelBinder GetBinder(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext)
+ {
+ ModelBinderUtil.ValidateBindingContext(bindingContext);
+
+ ValueProviderResult valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
+ if (valueProviderResult == null)
+ {
+ return null; // no value to convert
+ }
+
+ if (!TypeDescriptor.GetConverter(bindingContext.ModelType).CanConvertFrom(typeof(string)))
+ {
+ return null; // this type cannot be converted
+ }
+
+ return new TypeConverterModelBinder();
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ModelBinding/TypeMatchModelBinder.cs b/src/Microsoft.Web.Mvc/ModelBinding/TypeMatchModelBinder.cs
new file mode 100644
index 00000000..2f7f73c1
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ModelBinding/TypeMatchModelBinder.cs
@@ -0,0 +1,41 @@
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc.ModelBinding
+{
+ public sealed class TypeMatchModelBinder : IExtensibleModelBinder
+ {
+ public bool BindModel(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext)
+ {
+ ValueProviderResult valueProviderResult = GetCompatibleValueProviderResult(bindingContext);
+ if (valueProviderResult == null)
+ {
+ return false; // conversion would have failed
+ }
+
+ bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
+ object model = valueProviderResult.RawValue;
+ ModelBinderUtil.ReplaceEmptyStringWithNull(bindingContext.ModelMetadata, ref model);
+ bindingContext.Model = model;
+
+ return true;
+ }
+
+ internal static ValueProviderResult GetCompatibleValueProviderResult(ExtensibleModelBindingContext bindingContext)
+ {
+ ModelBinderUtil.ValidateBindingContext(bindingContext);
+
+ ValueProviderResult valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
+ if (valueProviderResult == null)
+ {
+ return null; // the value doesn't exist
+ }
+
+ if (!TypeHelpers.IsCompatibleObject(bindingContext.ModelType, valueProviderResult.RawValue))
+ {
+ return null; // value is of incompatible type
+ }
+
+ return valueProviderResult;
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ModelBinding/TypeMatchModelBinderProvider.cs b/src/Microsoft.Web.Mvc/ModelBinding/TypeMatchModelBinderProvider.cs
new file mode 100644
index 00000000..8e28c831
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ModelBinding/TypeMatchModelBinderProvider.cs
@@ -0,0 +1,16 @@
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc.ModelBinding
+{
+ // Returns a binder that can extract a ValueProviderResult.RawValue and return it directly.
+ [ModelBinderProviderOptions(FrontOfList = true)]
+ public sealed class TypeMatchModelBinderProvider : ModelBinderProvider
+ {
+ public override IExtensibleModelBinder GetBinder(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext)
+ {
+ return (TypeMatchModelBinder.GetCompatibleValueProviderResult(bindingContext) != null)
+ ? new TypeMatchModelBinder()
+ : null /* no match */;
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ModelCopier.cs b/src/Microsoft.Web.Mvc/ModelCopier.cs
new file mode 100644
index 00000000..f87456a6
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ModelCopier.cs
@@ -0,0 +1,55 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+
+namespace Microsoft.Web.Mvc
+{
+ public static class ModelCopier
+ {
+ public static void CopyCollection<T>(IEnumerable<T> from, ICollection<T> to)
+ {
+ if (from == null || to == null || to.IsReadOnly)
+ {
+ return;
+ }
+
+ to.Clear();
+ foreach (T element in from)
+ {
+ to.Add(element);
+ }
+ }
+
+ public static void CopyModel(object from, object to)
+ {
+ if (from == null || to == null)
+ {
+ return;
+ }
+
+ PropertyDescriptorCollection fromProperties = TypeDescriptor.GetProperties(from);
+ PropertyDescriptorCollection toProperties = TypeDescriptor.GetProperties(to);
+
+ foreach (PropertyDescriptor fromProperty in fromProperties)
+ {
+ PropertyDescriptor toProperty = toProperties.Find(fromProperty.Name, true /* ignoreCase */);
+ if (toProperty != null && !toProperty.IsReadOnly)
+ {
+ // Can from.Property reference just be assigned directly to to.Property reference?
+ bool isDirectlyAssignable = toProperty.PropertyType.IsAssignableFrom(fromProperty.PropertyType);
+ // Is from.Property just the nullable form of to.Property?
+ bool liftedValueType = (isDirectlyAssignable) ? false : (Nullable.GetUnderlyingType(fromProperty.PropertyType) == toProperty.PropertyType);
+
+ if (isDirectlyAssignable || liftedValueType)
+ {
+ object fromValue = fromProperty.GetValue(from);
+ if (isDirectlyAssignable || (fromValue != null && liftedValueType))
+ {
+ toProperty.SetValue(to, fromValue);
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/MvcSerializer.cs b/src/Microsoft.Web.Mvc/MvcSerializer.cs
new file mode 100644
index 00000000..384b307c
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/MvcSerializer.cs
@@ -0,0 +1,141 @@
+using System;
+using System.IO;
+using System.Runtime.Serialization;
+using System.Web.Security;
+using System.Xml;
+using Microsoft.Web.Mvc.Properties;
+
+namespace Microsoft.Web.Mvc
+{
+ public class MvcSerializer
+ {
+ public static readonly SerializationMode DefaultSerializationMode = SerializationMode.Signed;
+
+ // Magic number (randomly generated) used to identify the MvcSerializer serialized stream format.
+ private static readonly byte[] _magicHeader = { 0x2c, 0xf8, 0x06, 0x23, 0x57, 0x73, 0x11, 0xba };
+
+ private static bool ArrayContainsMagicHeader(byte[] array)
+ {
+ if (array == null || array.Length < _magicHeader.Length)
+ {
+ return false;
+ }
+
+ for (int i = 0; i < _magicHeader.Length; i++)
+ {
+ if (_magicHeader[i] != array[i])
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static SerializationException CreateSerializationException(Exception innerException)
+ {
+ return new SerializationException(MvcResources.MvcSerializer_DeserializationFailed, innerException);
+ }
+
+ public virtual object Deserialize(string serializedValue, SerializationMode mode)
+ {
+ return Deserialize(serializedValue, mode, new MachineKeyWrapper());
+ }
+
+ internal static object Deserialize(string serializedValue, SerializationMode mode, IMachineKey machineKey)
+ {
+ if (String.IsNullOrEmpty(serializedValue))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "serializedValue");
+ }
+
+ MachineKeyProtection protectionMode = GetMachineKeyProtectionMode(mode);
+
+ try
+ {
+ // First, need to decrypt / verify data
+ byte[] rawBytes = machineKey.Decode(serializedValue, protectionMode);
+
+ // Next, verify magic header
+ if (!ArrayContainsMagicHeader(rawBytes))
+ {
+ throw new SerializationException(MvcResources.MvcSerializer_MagicHeaderCheckFailed);
+ }
+
+ // Finally, deserialize the object graph
+ using (MemoryStream ms = new MemoryStream(rawBytes, _magicHeader.Length, rawBytes.Length - _magicHeader.Length))
+ {
+ return DeserializeGraph(ms);
+ }
+ }
+ catch (Exception ex)
+ {
+ throw CreateSerializationException(ex);
+ }
+ }
+
+ // Deserializes a stream to a graph using the NetDataContractSerializer (binary mode)
+ private static object DeserializeGraph(Stream rawBytes)
+ {
+ using (XmlDictionaryReader dr = XmlDictionaryReader.CreateBinaryReader(rawBytes, XmlDictionaryReaderQuotas.Max))
+ {
+ object deserialized = new NetDataContractSerializer().ReadObject(dr);
+ return deserialized;
+ }
+ }
+
+ private static MachineKeyProtection GetMachineKeyProtectionMode(SerializationMode mode)
+ {
+ switch (mode)
+ {
+ case SerializationMode.Signed:
+ return MachineKeyProtection.Validation;
+
+ case SerializationMode.EncryptedAndSigned:
+ return MachineKeyProtection.All;
+
+ default:
+ // bad
+ throw new ArgumentOutOfRangeException("mode", MvcResources.MvcSerializer_InvalidSerializationMode);
+ }
+ }
+
+ public virtual string Serialize(object state, SerializationMode mode)
+ {
+ return Serialize(state, mode, new MachineKeyWrapper());
+ }
+
+ internal static string Serialize(object state, SerializationMode mode, IMachineKey machineKey)
+ {
+ MachineKeyProtection protectionMode = GetMachineKeyProtectionMode(mode);
+
+ try
+ {
+ // First, need to append the magic header and serialize the object graph
+ byte[] rawBytes;
+ using (MemoryStream ms = new MemoryStream())
+ {
+ ms.Write(_magicHeader, 0, _magicHeader.Length);
+ SerializeGraph(ms, state);
+ rawBytes = ms.ToArray();
+ }
+
+ // Then, encrypt / sign data
+ return machineKey.Encode(rawBytes, protectionMode);
+ }
+ catch (Exception ex)
+ {
+ throw CreateSerializationException(ex);
+ }
+ }
+
+ // Serializes a graph to a byte array using the NetDataContractSerializer (binary mode)
+ private static void SerializeGraph(Stream outputStream, object graph)
+ {
+ using (XmlDictionaryWriter dw = XmlDictionaryWriter.CreateBinaryWriter(outputStream, null, null, false /* ownsStream */))
+ {
+ new NetDataContractSerializer().WriteObject(dw, graph);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Properties/AssemblyInfo.cs b/src/Microsoft.Web.Mvc/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..3bf1eb82
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Properties/AssemblyInfo.cs
@@ -0,0 +1,8 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+[assembly: AssemblyTitle("Microsoft.Web.Mvc.dll")]
+[assembly: AssemblyDescription("Microsoft.Web.Mvc.dll")]
+[assembly: Guid("f3507a98-9429-404b-9e0e-1b426a5b3ad5")]
+[assembly: InternalsVisibleTo("Microsoft.Web.Mvc.Test")]
diff --git a/src/Microsoft.Web.Mvc/Properties/MvcResources.Designer.cs b/src/Microsoft.Web.Mvc/Properties/MvcResources.Designer.cs
new file mode 100644
index 00000000..b5471363
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Properties/MvcResources.Designer.cs
@@ -0,0 +1,423 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.239
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Microsoft.Web.Mvc.Properties {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class MvcResources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal MvcResources() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.Web.Mvc.Properties.MvcResources", typeof(MvcResources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A value for &apos;{0}&apos; is required but was not present in the request..
+ /// </summary>
+ internal static string BindingBehavior_ValueNotFound {
+ get {
+ return ResourceManager.GetString("BindingBehavior_ValueNotFound", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Value cannot be null or empty..
+ /// </summary>
+ internal static string Common_NullOrEmpty {
+ get {
+ return ResourceManager.GetString("Common_NullOrEmpty", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The type &apos;{0}&apos; does not implement the interface &apos;{1}&apos;..
+ /// </summary>
+ internal static string Common_TypeMustImplementInterface {
+ get {
+ return ResourceManager.GetString("Common_TypeMustImplementInterface", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The &apos;Name&apos; property must be set..
+ /// </summary>
+ internal static string CommonControls_NameRequired {
+ get {
+ return ResourceManager.GetString("CommonControls_NameRequired", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The {0} field is not a valid credit card number..
+ /// </summary>
+ internal static string CreditCardAttribute_Invalid {
+ get {
+ return ResourceManager.GetString("CreditCardAttribute_Invalid", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Sample Item.
+ /// </summary>
+ internal static string DropDownList_SampleItem {
+ get {
+ return ResourceManager.GetString("DropDownList_SampleItem", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to DynamicViewDataDictionary only supports single indexers..
+ /// </summary>
+ internal static string DynamicViewDataDictionary_SingleIndexerOnly {
+ get {
+ return ResourceManager.GetString("DynamicViewDataDictionary_SingleIndexerOnly", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to DynamicViewDataDictionary only supports string-based indexers..
+ /// </summary>
+ internal static string DynamicViewDataDictionary_StringIndexerOnly {
+ get {
+ return ResourceManager.GetString("DynamicViewDataDictionary_StringIndexerOnly", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The property {0} doesn&apos;t exist. There are no public properties on this object..
+ /// </summary>
+ internal static string DynamicViewPage_NoProperties {
+ get {
+ return ResourceManager.GetString("DynamicViewPage_NoProperties", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The property {0} doesn&apos;t exist. Supported properties are: {1}..
+ /// </summary>
+ internal static string DynamicViewPage_PropertyDoesNotExist {
+ get {
+ return ResourceManager.GetString("DynamicViewPage_PropertyDoesNotExist", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The {0} field is not a valid e-mail address..
+ /// </summary>
+ internal static string EmailAddressAttribute_Invalid {
+ get {
+ return ResourceManager.GetString("EmailAddressAttribute_Invalid", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The method &apos;{0}&apos; is an asynchronous completion method and cannot be called directly..
+ /// </summary>
+ internal static string ExpressionHelper_CannotCallCompletedMethod {
+ get {
+ return ResourceManager.GetString("ExpressionHelper_CannotCallCompletedMethod", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The method &apos;{0}&apos; is marked [NonAction] and cannot be called directly..
+ /// </summary>
+ internal static string ExpressionHelper_CannotCallNonAction {
+ get {
+ return ResourceManager.GetString("ExpressionHelper_CannotCallNonAction", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Cannot route to class named &apos;Controller&apos;..
+ /// </summary>
+ internal static string ExpressionHelper_CannotRouteToController {
+ get {
+ return ResourceManager.GetString("ExpressionHelper_CannotRouteToController", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Expression must be a method call..
+ /// </summary>
+ internal static string ExpressionHelper_MustBeMethodCall {
+ get {
+ return ResourceManager.GetString("ExpressionHelper_MustBeMethodCall", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Controller name must end in &apos;Controller&apos;..
+ /// </summary>
+ internal static string ExpressionHelper_TargetMustEndInController {
+ get {
+ return ResourceManager.GetString("ExpressionHelper_TargetMustEndInController", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The new model binding system cannot be used when a property whitelist or blacklist has been specified in [Bind] or via the call to UpdateModel() / TryUpdateModel(). Use the [BindRequired] and [BindNever] attributes on the model type or its properties instead..
+ /// </summary>
+ internal static string ExtensibleModelBinderAdapter_PropertyFilterMustNotBeSet {
+ get {
+ return ResourceManager.GetString("ExtensibleModelBinderAdapter_PropertyFilterMustNotBeSet", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The {0} field only accepts files with the following extensions: {1}.
+ /// </summary>
+ internal static string FileExtensionsAttribute_Invalid {
+ get {
+ return ResourceManager.GetString("FileExtensionsAttribute_Invalid", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The type &apos;{0}&apos; is not an open generic type..
+ /// </summary>
+ internal static string GenericModelBinderProvider_ParameterMustSpecifyOpenGenericType {
+ get {
+ return ResourceManager.GetString("GenericModelBinderProvider_ParameterMustSpecifyOpenGenericType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The open model type &apos;{0}&apos; has {1} generic type argument(s), but the open binder type &apos;{2}&apos; has {3} generic type argument(s). The binder type must not be an open generic type or must have the same number of generic arguments as the open model type..
+ /// </summary>
+ internal static string GenericModelBinderProvider_TypeArgumentCountMismatch {
+ get {
+ return ResourceManager.GetString("GenericModelBinderProvider_TypeArgumentCountMismatch", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to There is no ViewData item with the key &apos;{0}&apos; of type &apos;{1}&apos;..
+ /// </summary>
+ internal static string HtmlHelper_MissingSelectData {
+ get {
+ return ResourceManager.GetString("HtmlHelper_MissingSelectData", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The ViewData item with the key &apos;{0}&apos; is of type &apos;{1}&apos; but needs to be of type &apos;{2}&apos;..
+ /// </summary>
+ internal static string HtmlHelper_WrongSelectDataType {
+ get {
+ return ResourceManager.GetString("HtmlHelper_WrongSelectDataType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The value &apos;{0}&apos; is not valid for {1}..
+ /// </summary>
+ internal static string ModelBinderConfig_ValueInvalid {
+ get {
+ return ResourceManager.GetString("ModelBinderConfig_ValueInvalid", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A value is required..
+ /// </summary>
+ internal static string ModelBinderConfig_ValueRequired {
+ get {
+ return ResourceManager.GetString("ModelBinderConfig_ValueRequired", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A binder for type {0} could not be located..
+ /// </summary>
+ internal static string ModelBinderProviderCollection_BinderForTypeNotFound {
+ get {
+ return ResourceManager.GetString("ModelBinderProviderCollection_BinderForTypeNotFound", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The type &apos;{0}&apos; does not subclass {1} or implement the interface {2}..
+ /// </summary>
+ internal static string ModelBinderProviderCollection_InvalidBinderType {
+ get {
+ return ResourceManager.GetString("ModelBinderProviderCollection_InvalidBinderType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The model of type &apos;{0}&apos; has a [Bind] attribute. The new model binding system cannot be used with models that have type-level [Bind] attributes. Use the [BindRequired] and [BindNever] attributes on the model type or its properties instead..
+ /// </summary>
+ internal static string ModelBinderProviderCollection_TypeCannotHaveBindAttribute {
+ get {
+ return ResourceManager.GetString("ModelBinderProviderCollection_TypeCannotHaveBindAttribute", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The binding context has a null Model, but this binder requires a non-null model of type &apos;{0}&apos;..
+ /// </summary>
+ internal static string ModelBinderUtil_ModelCannotBeNull {
+ get {
+ return ResourceManager.GetString("ModelBinderUtil_ModelCannotBeNull", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The binding context has a Model of type &apos;{0}&apos;, but this binder can only operate on models of type &apos;{1}&apos;..
+ /// </summary>
+ internal static string ModelBinderUtil_ModelInstanceIsWrong {
+ get {
+ return ResourceManager.GetString("ModelBinderUtil_ModelInstanceIsWrong", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The binding context cannot have a null ModelMetadata..
+ /// </summary>
+ internal static string ModelBinderUtil_ModelMetadataCannotBeNull {
+ get {
+ return ResourceManager.GetString("ModelBinderUtil_ModelMetadataCannotBeNull", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The binding context has a ModelType of &apos;{0}&apos;, but this binder can only operate on models of type &apos;{1}&apos;..
+ /// </summary>
+ internal static string ModelBinderUtil_ModelTypeIsWrong {
+ get {
+ return ResourceManager.GetString("ModelBinderUtil_ModelTypeIsWrong", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The ModelMetadata property must be set before accessing this property..
+ /// </summary>
+ internal static string ModelBindingContext_ModelMetadataMustBeSet {
+ get {
+ return ResourceManager.GetString("ModelBindingContext_ModelMetadataMustBeSet", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Deserialization failed. Verify that the data is being deserialized using the same SerializationMode with which it was serialized. Otherwise see the inner exception..
+ /// </summary>
+ internal static string MvcSerializer_DeserializationFailed {
+ get {
+ return ResourceManager.GetString("MvcSerializer_DeserializationFailed", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The provided SerializationMode is invalid..
+ /// </summary>
+ internal static string MvcSerializer_InvalidSerializationMode {
+ get {
+ return ResourceManager.GetString("MvcSerializer_InvalidSerializationMode", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The data being serialized is corrupt..
+ /// </summary>
+ internal static string MvcSerializer_MagicHeaderCheckFailed {
+ get {
+ return ResourceManager.GetString("MvcSerializer_MagicHeaderCheckFailed", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Error dispatching on controller {0}, conflicting actions matched: {1}..
+ /// </summary>
+ internal static string ResourceControllerFactory_ConflictingActions {
+ get {
+ return ResourceManager.GetString("ResourceControllerFactory_ConflictingActions", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Error dispatching on controller {0}, no actions matched..
+ /// </summary>
+ internal static string ResourceControllerFactory_NoActions {
+ get {
+ return ResourceManager.GetString("ResourceControllerFactory_NoActions", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Format &apos;{0}&apos; is not supported..
+ /// </summary>
+ internal static string Resources_UnsupportedFormat {
+ get {
+ return ResourceManager.GetString("Resources_UnsupportedFormat", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Unsupported Media Type: &apos;{0}&apos;..
+ /// </summary>
+ internal static string Resources_UnsupportedMediaType {
+ get {
+ return ResourceManager.GetString("Resources_UnsupportedMediaType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The {0} field is not a valid fully-qualified http, https, or ftp URL..
+ /// </summary>
+ internal static string UrlAttribute_Invalid {
+ get {
+ return ResourceManager.GetString("UrlAttribute_Invalid", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Properties/MvcResources.resx b/src/Microsoft.Web.Mvc/Properties/MvcResources.resx
new file mode 100644
index 00000000..23e23698
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Properties/MvcResources.resx
@@ -0,0 +1,240 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="Common_NullOrEmpty" xml:space="preserve">
+ <value>Value cannot be null or empty.</value>
+ </data>
+ <data name="ExpressionHelper_CannotRouteToController" xml:space="preserve">
+ <value>Cannot route to class named 'Controller'.</value>
+ </data>
+ <data name="ExpressionHelper_MustBeMethodCall" xml:space="preserve">
+ <value>Expression must be a method call.</value>
+ </data>
+ <data name="ExpressionHelper_TargetMustEndInController" xml:space="preserve">
+ <value>Controller name must end in 'Controller'.</value>
+ </data>
+ <data name="HtmlHelper_MissingSelectData" xml:space="preserve">
+ <value>There is no ViewData item with the key '{0}' of type '{1}'.</value>
+ </data>
+ <data name="HtmlHelper_WrongSelectDataType" xml:space="preserve">
+ <value>The ViewData item with the key '{0}' is of type '{1}' but needs to be of type '{2}'.</value>
+ </data>
+ <data name="CommonControls_NameRequired" xml:space="preserve">
+ <value>The 'Name' property must be set.</value>
+ </data>
+ <data name="MvcSerializer_DeserializationFailed" xml:space="preserve">
+ <value>Deserialization failed. Verify that the data is being deserialized using the same SerializationMode with which it was serialized. Otherwise see the inner exception.</value>
+ </data>
+ <data name="MvcSerializer_InvalidSerializationMode" xml:space="preserve">
+ <value>The provided SerializationMode is invalid.</value>
+ </data>
+ <data name="Resources_UnsupportedMediaType" xml:space="preserve">
+ <value>Unsupported Media Type: '{0}'.</value>
+ </data>
+ <data name="Resources_UnsupportedFormat" xml:space="preserve">
+ <value>Format '{0}' is not supported.</value>
+ </data>
+ <data name="ExpressionHelper_CannotCallCompletedMethod" xml:space="preserve">
+ <value>The method '{0}' is an asynchronous completion method and cannot be called directly.</value>
+ </data>
+ <data name="ExpressionHelper_CannotCallNonAction" xml:space="preserve">
+ <value>The method '{0}' is marked [NonAction] and cannot be called directly.</value>
+ </data>
+ <data name="ModelBinderUtil_ModelCannotBeNull" xml:space="preserve">
+ <value>The binding context has a null Model, but this binder requires a non-null model of type '{0}'.</value>
+ </data>
+ <data name="ModelBinderUtil_ModelInstanceIsWrong" xml:space="preserve">
+ <value>The binding context has a Model of type '{0}', but this binder can only operate on models of type '{1}'.</value>
+ </data>
+ <data name="ModelBinderUtil_ModelMetadataCannotBeNull" xml:space="preserve">
+ <value>The binding context cannot have a null ModelMetadata.</value>
+ </data>
+ <data name="ModelBinderUtil_ModelTypeIsWrong" xml:space="preserve">
+ <value>The binding context has a ModelType of '{0}', but this binder can only operate on models of type '{1}'.</value>
+ </data>
+ <data name="ModelBinderConfig_ValueInvalid" xml:space="preserve">
+ <value>The value '{0}' is not valid for {1}.</value>
+ </data>
+ <data name="ModelBinderConfig_ValueRequired" xml:space="preserve">
+ <value>A value is required.</value>
+ </data>
+ <data name="ModelBinderProviderCollection_BinderForTypeNotFound" xml:space="preserve">
+ <value>A binder for type {0} could not be located.</value>
+ </data>
+ <data name="ModelBindingContext_ModelMetadataMustBeSet" xml:space="preserve">
+ <value>The ModelMetadata property must be set before accessing this property.</value>
+ </data>
+ <data name="Common_TypeMustImplementInterface" xml:space="preserve">
+ <value>The type '{0}' does not implement the interface '{1}'.</value>
+ </data>
+ <data name="GenericModelBinderProvider_ParameterMustSpecifyOpenGenericType" xml:space="preserve">
+ <value>The type '{0}' is not an open generic type.</value>
+ </data>
+ <data name="GenericModelBinderProvider_TypeArgumentCountMismatch" xml:space="preserve">
+ <value>The open model type '{0}' has {1} generic type argument(s), but the open binder type '{2}' has {3} generic type argument(s). The binder type must not be an open generic type or must have the same number of generic arguments as the open model type.</value>
+ </data>
+ <data name="BindingBehavior_ValueNotFound" xml:space="preserve">
+ <value>A value for '{0}' is required but was not present in the request.</value>
+ </data>
+ <data name="ExtensibleModelBinderAdapter_PropertyFilterMustNotBeSet" xml:space="preserve">
+ <value>The new model binding system cannot be used when a property whitelist or blacklist has been specified in [Bind] or via the call to UpdateModel() / TryUpdateModel(). Use the [BindRequired] and [BindNever] attributes on the model type or its properties instead.</value>
+ </data>
+ <data name="ModelBinderProviderCollection_TypeCannotHaveBindAttribute" xml:space="preserve">
+ <value>The model of type '{0}' has a [Bind] attribute. The new model binding system cannot be used with models that have type-level [Bind] attributes. Use the [BindRequired] and [BindNever] attributes on the model type or its properties instead.</value>
+ </data>
+ <data name="ModelBinderProviderCollection_InvalidBinderType" xml:space="preserve">
+ <value>The type '{0}' does not subclass {1} or implement the interface {2}.</value>
+ </data>
+ <data name="DynamicViewDataDictionary_SingleIndexerOnly" xml:space="preserve">
+ <value>DynamicViewDataDictionary only supports single indexers.</value>
+ </data>
+ <data name="DynamicViewDataDictionary_StringIndexerOnly" xml:space="preserve">
+ <value>DynamicViewDataDictionary only supports string-based indexers.</value>
+ </data>
+ <data name="DynamicViewPage_NoProperties" xml:space="preserve">
+ <value>The property {0} doesn't exist. There are no public properties on this object.</value>
+ </data>
+ <data name="DynamicViewPage_PropertyDoesNotExist" xml:space="preserve">
+ <value>The property {0} doesn't exist. Supported properties are: {1}.</value>
+ </data>
+ <data name="DropDownList_SampleItem" xml:space="preserve">
+ <value>Sample Item</value>
+ </data>
+ <data name="ResourceControllerFactory_ConflictingActions" xml:space="preserve">
+ <value>Error dispatching on controller {0}, conflicting actions matched: {1}.</value>
+ </data>
+ <data name="ResourceControllerFactory_NoActions" xml:space="preserve">
+ <value>Error dispatching on controller {0}, no actions matched.</value>
+ </data>
+ <data name="FileExtensionsAttribute_Invalid" xml:space="preserve">
+ <value>The {0} field only accepts files with the following extensions: {1}</value>
+ </data>
+ <data name="CreditCardAttribute_Invalid" xml:space="preserve">
+ <value>The {0} field is not a valid credit card number.</value>
+ </data>
+ <data name="EmailAddressAttribute_Invalid" xml:space="preserve">
+ <value>The {0} field is not a valid e-mail address.</value>
+ </data>
+ <data name="UrlAttribute_Invalid" xml:space="preserve">
+ <value>The {0} field is not a valid fully-qualified http, https, or ftp URL.</value>
+ </data>
+ <data name="MvcSerializer_MagicHeaderCheckFailed" xml:space="preserve">
+ <value>The data being serialized is corrupt.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/Microsoft.Web.Mvc/RadioExtensions.cs b/src/Microsoft.Web.Mvc/RadioExtensions.cs
new file mode 100644
index 00000000..a5ae8ebb
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/RadioExtensions.cs
@@ -0,0 +1,115 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Web.Mvc;
+using System.Web.Mvc.Html;
+using Microsoft.Web.Mvc.Properties;
+
+namespace Microsoft.Web.Mvc
+{
+ public static class RadioListExtensions
+ {
+ public static MvcHtmlString[] RadioButtonList(this HtmlHelper htmlHelper, string name)
+ {
+ return RadioButtonList(htmlHelper, name, (IDictionary<string, object>)null);
+ }
+
+ public static MvcHtmlString[] RadioButtonList(this HtmlHelper htmlHelper, string name, object htmlAttributes)
+ {
+ return RadioButtonList(htmlHelper, name, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ public static MvcHtmlString[] RadioButtonList(this HtmlHelper htmlHelper, string name, IDictionary<string, object> htmlAttributes)
+ {
+ IEnumerable<SelectListItem> selectList = htmlHelper.GetSelectData(name);
+ return htmlHelper.RadioButtonListInternal(name, selectList, true /* usedViewData */, htmlAttributes);
+ }
+
+ public static MvcHtmlString[] RadioButtonList(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList)
+ {
+ return RadioButtonList(htmlHelper, name, selectList, null);
+ }
+
+ public static MvcHtmlString[] RadioButtonList(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, object htmlAttributes)
+ {
+ return RadioButtonList(htmlHelper, name, selectList, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ public static MvcHtmlString[] RadioButtonList(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, IDictionary<string, object> htmlAttributes)
+ {
+ return htmlHelper.RadioButtonListInternal(name, selectList, false /* usedViewData */, htmlAttributes);
+ }
+
+ private static IEnumerable<SelectListItem> GetSelectData(this HtmlHelper htmlHelper, string name)
+ {
+ object o = null;
+ if (htmlHelper.ViewData != null)
+ {
+ o = htmlHelper.ViewData.Eval(name);
+ }
+ if (o == null)
+ {
+ throw new InvalidOperationException(
+ String.Format(
+ CultureInfo.CurrentCulture,
+ MvcResources.HtmlHelper_MissingSelectData,
+ name,
+ typeof(IEnumerable<SelectListItem>)));
+ }
+ IEnumerable<SelectListItem> selectList = o as IEnumerable<SelectListItem>;
+ if (selectList == null)
+ {
+ throw new InvalidOperationException(
+ String.Format(
+ CultureInfo.CurrentCulture,
+ MvcResources.HtmlHelper_WrongSelectDataType,
+ name,
+ o.GetType().FullName,
+ typeof(IEnumerable<SelectListItem>)));
+ }
+ return selectList;
+ }
+
+ private static MvcHtmlString[] RadioButtonListInternal(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, bool usedViewData, IDictionary<string, object> htmlAttributes)
+ {
+ if (String.IsNullOrEmpty(name))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "name");
+ }
+ if (selectList == null)
+ {
+ throw new ArgumentNullException("selectList");
+ }
+
+ // If we haven't already used ViewData to get the entire list of items then we need to
+ // use the ViewData-supplied value before using the parameter-supplied value.
+ if (!usedViewData)
+ {
+ object defaultValue = htmlHelper.ViewData.Eval(name);
+
+ if (defaultValue != null)
+ {
+ IEnumerable defaultValues = new[] { defaultValue };
+ IEnumerable<string> values = from object value in defaultValues
+ select Convert.ToString(value, CultureInfo.CurrentCulture);
+ HashSet<string> selectedValues = new HashSet<string>(values, StringComparer.OrdinalIgnoreCase);
+ List<SelectListItem> newSelectList = new List<SelectListItem>();
+
+ foreach (SelectListItem item in selectList)
+ {
+ item.Selected = (item.Value != null) ? selectedValues.Contains(item.Value) : selectedValues.Contains(item.Text);
+ newSelectList.Add(item);
+ }
+
+ selectList = newSelectList;
+ }
+ }
+
+ IEnumerable<MvcHtmlString> radioButtons = selectList.Select(item => htmlHelper.RadioButton(name, item.Value, item.Selected, htmlAttributes));
+
+ return radioButtons.ToArray();
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ReaderWriterCache`2.cs b/src/Microsoft.Web.Mvc/ReaderWriterCache`2.cs
new file mode 100644
index 00000000..19d9e432
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ReaderWriterCache`2.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Threading;
+
+namespace Microsoft.Web.Mvc
+{
+ [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "Instances of this type are meant to be singletons.")]
+ internal abstract class ReaderWriterCache<TKey, TValue>
+ {
+ private readonly Dictionary<TKey, TValue> _cache;
+ private readonly ReaderWriterLockSlim _readerWriterLock = new ReaderWriterLockSlim();
+
+ protected ReaderWriterCache()
+ : this(null)
+ {
+ }
+
+ protected ReaderWriterCache(IEqualityComparer<TKey> comparer)
+ {
+ _cache = new Dictionary<TKey, TValue>(comparer);
+ }
+
+ protected Dictionary<TKey, TValue> Cache
+ {
+ get { return _cache; }
+ }
+
+ protected TValue FetchOrCreateItem(TKey key, Func<TValue> creator)
+ {
+ // first, see if the item already exists in the cache
+ _readerWriterLock.EnterReadLock();
+ try
+ {
+ TValue existingEntry;
+ if (_cache.TryGetValue(key, out existingEntry))
+ {
+ return existingEntry;
+ }
+ }
+ finally
+ {
+ _readerWriterLock.ExitReadLock();
+ }
+
+ // insert the new item into the cache
+ TValue newEntry = creator();
+ _readerWriterLock.EnterWriteLock();
+ try
+ {
+ TValue existingEntry;
+ if (_cache.TryGetValue(key, out existingEntry))
+ {
+ // another thread already inserted an item, so use that one
+ return existingEntry;
+ }
+
+ _cache[key] = newEntry;
+ return newEntry;
+ }
+ finally
+ {
+ _readerWriterLock.ExitWriteLock();
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Resources/ActionType.cs b/src/Microsoft.Web.Mvc/Resources/ActionType.cs
new file mode 100644
index 00000000..1c588297
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Resources/ActionType.cs
@@ -0,0 +1,16 @@
+namespace Microsoft.Web.Mvc.Resources
+{
+ /// <summary>
+ /// This enum is used by the UrlHelper extension methods to create links within resource controllers
+ /// </summary>
+ public enum ActionType
+ {
+ Create,
+ GetCreateForm,
+ Index,
+ Retrieve,
+ Update,
+ GetUpdateForm,
+ Delete,
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Resources/AjaxHelperExtensions.cs b/src/Microsoft.Web.Mvc/Resources/AjaxHelperExtensions.cs
new file mode 100644
index 00000000..de2bcb56
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Resources/AjaxHelperExtensions.cs
@@ -0,0 +1,113 @@
+using System;
+using System.Web.Mvc;
+using System.Web.Mvc.Ajax;
+using System.Web.Mvc.Html;
+
+namespace Microsoft.Web.Mvc.Resources
+{
+ public static class AjaxHelperExtensions
+ {
+ /// <summary>
+ /// Generates the Form preamble, defaulting the link for the Retrieve action
+ /// </summary>
+ /// <param name="ajax"></param>
+ /// <param name="controllerName"></param>
+ /// <param name="routeValues"></param>
+ /// <param name="ajaxOptions"></param>
+ /// <returns></returns>
+ public static MvcForm BeginResourceForm(this AjaxHelper ajax, string controllerName, object routeValues, AjaxOptions ajaxOptions)
+ {
+ return ajax.BeginResourceForm(controllerName, routeValues, ajaxOptions, ActionType.Retrieve);
+ }
+
+ /// <summary>
+ /// Generates the Form preamble
+ /// </summary>
+ /// <param name="ajax"></param>
+ /// <param name="controllerName"></param>
+ /// <param name="routeValues"></param>
+ /// <param name="ajaxOptions"></param>
+ /// <param name="actionType"></param>
+ /// <returns></returns>
+ public static MvcForm BeginResourceForm(this AjaxHelper ajax, string controllerName, object routeValues, AjaxOptions ajaxOptions, ActionType actionType)
+ {
+ switch (actionType)
+ {
+ case ActionType.GetUpdateForm:
+ return ajax.BeginRouteForm(controllerName + "-editForm", routeValues, ajaxOptions);
+ case ActionType.GetCreateForm:
+ return ajax.BeginRouteForm(controllerName + "-createForm", ajaxOptions);
+ case ActionType.Retrieve:
+ case ActionType.Delete:
+ case ActionType.Update:
+ // can we use ajaxOptions to either add the header?
+ MvcForm form = ajax.BeginRouteForm(controllerName, routeValues, ajaxOptions);
+ return form;
+ case ActionType.Create:
+ return ajax.BeginRouteForm(controllerName + "-create", ajaxOptions);
+ case ActionType.Index:
+ return ajax.BeginRouteForm(controllerName + "-index", ajaxOptions);
+ default:
+ throw new ArgumentOutOfRangeException("actionType");
+ }
+ }
+
+ /// <summary>
+ /// Generates a link to the resource controller, defaulting to the Retrieve action
+ /// </summary>
+ /// <param name="ajax"></param>
+ /// <param name="controllerName"></param>
+ /// <param name="routeValues"></param>
+ /// <param name="ajaxOptions"></param>
+ /// <returns></returns>
+ public static MvcHtmlString ResourceLink(this AjaxHelper ajax, string controllerName, object routeValues, AjaxOptions ajaxOptions)
+ {
+ return ajax.ResourceLink(controllerName, controllerName, routeValues, ajaxOptions, ActionType.Retrieve);
+ }
+
+ /// <summary>
+ /// Generates a link to the resource controller, defaulting to the Retrieve action
+ /// </summary>
+ /// <param name="ajax"></param>
+ /// <param name="controllerName"></param>
+ /// <param name="linkText"></param>
+ /// <param name="routeValues"></param>
+ /// <param name="ajaxOptions"></param>
+ /// <returns></returns>
+ public static MvcHtmlString ResourceLink(this AjaxHelper ajax, string controllerName, string linkText, object routeValues, AjaxOptions ajaxOptions)
+ {
+ return ajax.ResourceLink(linkText, controllerName, routeValues, ajaxOptions, ActionType.Retrieve);
+ }
+
+ /// <summary>
+ /// Generates a link to the resource controller
+ /// </summary>
+ /// <param name="ajax"></param>
+ /// <param name="controllerName"></param>
+ /// <param name="linkText"></param>
+ /// <param name="routeValues"></param>
+ /// <param name="ajaxOptions"></param>
+ /// <param name="actionType"></param>
+ /// <returns></returns>
+ public static MvcHtmlString ResourceLink(this AjaxHelper ajax, string controllerName, string linkText, object routeValues, AjaxOptions ajaxOptions, ActionType actionType)
+ {
+ switch (actionType)
+ {
+ case ActionType.GetUpdateForm:
+ return ajax.RouteLink(linkText, controllerName + "-editForm", routeValues, ajaxOptions);
+ case ActionType.GetCreateForm:
+ return ajax.RouteLink(linkText, controllerName + "-createForm", ajaxOptions);
+ case ActionType.Retrieve:
+ case ActionType.Delete:
+ case ActionType.Update:
+ return ajax.RouteLink(linkText, controllerName, routeValues, ajaxOptions);
+ case ActionType.Create:
+ return ajax.RouteLink(linkText, controllerName + "-create", ajaxOptions);
+ case ActionType.Index:
+ return ajax.RouteLink(linkText, controllerName + "-index", ajaxOptions);
+ default:
+ throw new ArgumentOutOfRangeException("actionType");
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Resources/AtomEntryActionResult.cs b/src/Microsoft.Web.Mvc/Resources/AtomEntryActionResult.cs
new file mode 100644
index 00000000..fabf2926
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Resources/AtomEntryActionResult.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Globalization;
+using System.Net;
+using System.Net.Mime;
+using System.ServiceModel.Syndication;
+using System.Text;
+using System.Web;
+using System.Web.Mvc;
+using System.Xml;
+using Microsoft.Web.Mvc.Properties;
+
+namespace Microsoft.Web.Mvc.Resources
+{
+ /// <summary>
+ /// An ActionResult that can render a SyndicationItem to the Atom 1.0 entry format
+ /// </summary>
+ public class AtomEntryActionResult : ActionResult
+ {
+ private ContentType contentType;
+ private SyndicationItem item;
+
+ /// <summary>
+ /// The content type defaults to application/atom+xml; type=entry
+ /// </summary>
+ /// <param name="item"></param>
+ public AtomEntryActionResult(SyndicationItem item)
+ : this(item, new ContentType("application/atom+xml;type=entry"))
+ {
+ }
+
+ public AtomEntryActionResult(SyndicationItem item, ContentType contentType)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+ if (contentType == null)
+ {
+ throw new ArgumentNullException("contentType");
+ }
+ this.item = item;
+ this.contentType = contentType;
+ }
+
+ public ContentType ContentType
+ {
+ get { return this.contentType; }
+ }
+
+ public SyndicationItem Item
+ {
+ get { return this.item; }
+ }
+
+ public override void ExecuteResult(ControllerContext context)
+ {
+ Encoding encoding = Encoding.UTF8;
+ if (!String.IsNullOrEmpty(this.ContentType.CharSet))
+ {
+ try
+ {
+ encoding = Encoding.GetEncoding(this.ContentType.CharSet);
+ }
+ catch (ArgumentException)
+ {
+ throw new HttpException((int)HttpStatusCode.NotAcceptable, String.Format(CultureInfo.CurrentCulture, MvcResources.Resources_UnsupportedFormat, this.ContentType));
+ }
+ }
+ XmlWriterSettings settings = new XmlWriterSettings { Encoding = encoding };
+ this.ContentType.CharSet = settings.Encoding.HeaderName;
+ context.HttpContext.Response.ContentType = this.ContentType.ToString();
+ using (XmlWriter writer = XmlWriter.Create(context.HttpContext.Response.OutputStream, settings))
+ {
+ this.Item.GetAtom10Formatter().WriteTo(writer);
+ writer.Flush();
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Resources/AtomFeedActionResult.cs b/src/Microsoft.Web.Mvc/Resources/AtomFeedActionResult.cs
new file mode 100644
index 00000000..8211b4d6
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Resources/AtomFeedActionResult.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Globalization;
+using System.Net;
+using System.Net.Mime;
+using System.ServiceModel.Syndication;
+using System.Text;
+using System.Web;
+using System.Web.Mvc;
+using System.Xml;
+using Microsoft.Web.Mvc.Properties;
+
+namespace Microsoft.Web.Mvc.Resources
+{
+ /// <summary>
+ /// An ActionResult that can render a SyndicationFeed to the Atom 1.0 feed format
+ /// </summary>
+ public class AtomFeedActionResult : ActionResult
+ {
+ private ContentType contentType;
+ private SyndicationFeed feed;
+
+ /// <summary>
+ /// The content type defaults to application/atom+xml
+ /// </summary>
+ /// <param name="feed"></param>
+ public AtomFeedActionResult(SyndicationFeed feed)
+ : this(feed, new ContentType("application/atom+xml"))
+ {
+ }
+
+ public AtomFeedActionResult(SyndicationFeed feed, ContentType contentType)
+ {
+ if (feed == null)
+ {
+ throw new ArgumentNullException("feed");
+ }
+ if (contentType == null)
+ {
+ throw new ArgumentNullException("contentType");
+ }
+ this.feed = feed;
+ this.contentType = contentType;
+ }
+
+ public ContentType ContentType
+ {
+ get { return this.contentType; }
+ }
+
+ public SyndicationFeed Feed
+ {
+ get { return this.feed; }
+ }
+
+ public override void ExecuteResult(ControllerContext context)
+ {
+ Encoding encoding = Encoding.UTF8;
+ if (!String.IsNullOrEmpty(this.ContentType.CharSet))
+ {
+ try
+ {
+ encoding = Encoding.GetEncoding(this.ContentType.CharSet);
+ }
+ catch (ArgumentException)
+ {
+ throw new HttpException((int)HttpStatusCode.NotAcceptable, String.Format(CultureInfo.CurrentCulture, MvcResources.Resources_UnsupportedFormat, this.ContentType));
+ }
+ }
+ XmlWriterSettings settings = new XmlWriterSettings { Encoding = encoding };
+ this.ContentType.CharSet = settings.Encoding.HeaderName;
+ context.HttpContext.Response.ContentType = this.ContentType.ToString();
+ using (XmlWriter writer = XmlWriter.Create(context.HttpContext.Response.OutputStream, settings))
+ {
+ this.Feed.GetAtom10Formatter().WriteTo(writer);
+ writer.Flush();
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Resources/AtomServiceDocumentActionResult.cs b/src/Microsoft.Web.Mvc/Resources/AtomServiceDocumentActionResult.cs
new file mode 100644
index 00000000..af685399
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Resources/AtomServiceDocumentActionResult.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Globalization;
+using System.Net;
+using System.Net.Mime;
+using System.ServiceModel.Syndication;
+using System.Text;
+using System.Web;
+using System.Web.Mvc;
+using System.Xml;
+using Microsoft.Web.Mvc.Properties;
+
+namespace Microsoft.Web.Mvc.Resources
+{
+ /// <summary>
+ /// An ActionResult that can render a ServiceDocument to the Atom 1.0 ServiceDocument format
+ /// </summary>
+ public class AtomServiceDocumentActionResult : ActionResult
+ {
+ private ContentType contentType;
+ private ServiceDocument document;
+
+ /// <summary>
+ /// The content type defaults to application/atomsvc+xml
+ /// </summary>
+ /// <param name="document"></param>
+ public AtomServiceDocumentActionResult(ServiceDocument document)
+ : this(document, new ContentType("application/atomsvc+xml"))
+ {
+ }
+
+ public AtomServiceDocumentActionResult(ServiceDocument document, ContentType contentType)
+ {
+ if (document == null)
+ {
+ throw new ArgumentNullException("document");
+ }
+ if (contentType == null)
+ {
+ throw new ArgumentNullException("contentType");
+ }
+ this.document = document;
+ this.contentType = contentType;
+ }
+
+ public ContentType ContentType
+ {
+ get { return this.contentType; }
+ }
+
+ public ServiceDocument Document
+ {
+ get { return this.document; }
+ }
+
+ public override void ExecuteResult(ControllerContext context)
+ {
+ Encoding encoding = Encoding.UTF8;
+ if (!String.IsNullOrEmpty(this.ContentType.CharSet))
+ {
+ try
+ {
+ encoding = Encoding.GetEncoding(this.ContentType.CharSet);
+ }
+ catch (ArgumentException)
+ {
+ throw new HttpException((int)HttpStatusCode.NotAcceptable, String.Format(CultureInfo.CurrentCulture, MvcResources.Resources_UnsupportedFormat, this.ContentType));
+ }
+ }
+ XmlWriterSettings settings = new XmlWriterSettings { Encoding = encoding };
+ this.ContentType.CharSet = settings.Encoding.HeaderName;
+ context.HttpContext.Response.ContentType = this.ContentType.ToString();
+ using (XmlWriter writer = XmlWriter.Create(context.HttpContext.Response.OutputStream, settings))
+ {
+ this.Document.GetFormatter().WriteTo(writer);
+ writer.Flush();
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Resources/DataContractJsonActionResult.cs b/src/Microsoft.Web.Mvc/Resources/DataContractJsonActionResult.cs
new file mode 100644
index 00000000..526aadff
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Resources/DataContractJsonActionResult.cs
@@ -0,0 +1,71 @@
+using System;
+using System.Globalization;
+using System.Net;
+using System.Net.Mime;
+using System.Runtime.Serialization.Json;
+using System.Text;
+using System.Web;
+using System.Web.Mvc;
+using System.Xml;
+using Microsoft.Web.Mvc.Properties;
+
+namespace Microsoft.Web.Mvc.Resources
+{
+ /// <summary>
+ /// An ActionResult that can render an object to the json format using the DataContractJsonSerializer
+ /// </summary>
+ public class DataContractJsonActionResult : ActionResult
+ {
+ private ContentType contentType;
+ private object data;
+
+ /// <summary>
+ /// The default content type is application/json
+ /// </summary>
+ /// <param name="data"></param>
+ public DataContractJsonActionResult(object data)
+ : this(data, new ContentType("application/json"))
+ {
+ }
+
+ public DataContractJsonActionResult(object data, ContentType contentType)
+ {
+ this.data = data;
+ this.contentType = contentType;
+ }
+
+ public ContentType ContentType
+ {
+ get { return this.contentType; }
+ }
+
+ public object Data
+ {
+ get { return this.data; }
+ }
+
+ public override void ExecuteResult(ControllerContext context)
+ {
+ Encoding encoding = Encoding.UTF8;
+ if (!String.IsNullOrEmpty(this.ContentType.CharSet))
+ {
+ try
+ {
+ encoding = Encoding.GetEncoding(this.ContentType.CharSet);
+ }
+ catch (ArgumentException)
+ {
+ throw new HttpException((int)HttpStatusCode.NotAcceptable, String.Format(CultureInfo.CurrentCulture, MvcResources.Resources_UnsupportedFormat, this.ContentType));
+ }
+ }
+ DataContractJsonSerializer dcs = new DataContractJsonSerializer(this.Data.GetType());
+ this.ContentType.CharSet = encoding.HeaderName;
+ context.HttpContext.Response.ContentType = this.ContentType.ToString();
+ using (XmlWriter writer = JsonReaderWriterFactory.CreateJsonWriter(context.HttpContext.Response.OutputStream, encoding))
+ {
+ dcs.WriteObject(writer, this.Data);
+ writer.Flush();
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Resources/DataContractXmlActionResult.cs b/src/Microsoft.Web.Mvc/Resources/DataContractXmlActionResult.cs
new file mode 100644
index 00000000..ad6ff159
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Resources/DataContractXmlActionResult.cs
@@ -0,0 +1,71 @@
+using System;
+using System.Globalization;
+using System.Net;
+using System.Net.Mime;
+using System.Runtime.Serialization;
+using System.Text;
+using System.Web;
+using System.Web.Mvc;
+using System.Xml;
+using Microsoft.Web.Mvc.Properties;
+
+namespace Microsoft.Web.Mvc.Resources
+{
+ /// <summary>
+ /// An ActionResult that can render an object to the xml format using the DataContractSerializer
+ /// </summary>
+ public class DataContractXmlActionResult : ActionResult
+ {
+ private ContentType contentType;
+ private object data;
+
+ /// <summary>
+ /// The content type of the response defaults to application/xml
+ /// </summary>
+ public DataContractXmlActionResult(object data)
+ : this(data, new ContentType("application/xml"))
+ {
+ }
+
+ public DataContractXmlActionResult(object data, ContentType contentType)
+ {
+ this.data = data;
+ this.contentType = contentType;
+ }
+
+ public ContentType ContentType
+ {
+ get { return this.contentType; }
+ }
+
+ public object Data
+ {
+ get { return this.data; }
+ }
+
+ public override void ExecuteResult(ControllerContext context)
+ {
+ Encoding encoding = Encoding.UTF8;
+ if (!String.IsNullOrEmpty(this.ContentType.CharSet))
+ {
+ try
+ {
+ encoding = Encoding.GetEncoding(this.ContentType.CharSet);
+ }
+ catch (ArgumentException)
+ {
+ throw new HttpException((int)HttpStatusCode.NotAcceptable, String.Format(CultureInfo.CurrentCulture, MvcResources.Resources_UnsupportedFormat, this.ContentType));
+ }
+ }
+ XmlWriterSettings settings = new XmlWriterSettings { Encoding = encoding };
+ DataContractSerializer dcs = new DataContractSerializer(this.Data.GetType());
+ this.ContentType.CharSet = settings.Encoding.HeaderName;
+ context.HttpContext.Response.ContentType = this.ContentType.ToString();
+ using (XmlWriter writer = XmlWriter.Create(context.HttpContext.Response.OutputStream, settings))
+ {
+ dcs.WriteObject(writer, this.Data);
+ writer.Flush();
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Resources/DefaultFormatHelper.cs b/src/Microsoft.Web.Mvc/Resources/DefaultFormatHelper.cs
new file mode 100644
index 00000000..df214c8e
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Resources/DefaultFormatHelper.cs
@@ -0,0 +1,370 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Net;
+using System.Net.Mime;
+using System.Text;
+using System.Web;
+using System.Web.Mvc;
+using System.Web.Routing;
+using Microsoft.Web.Mvc.Properties;
+
+namespace Microsoft.Web.Mvc.Resources
+{
+ /// <summary>
+ /// Default implementation of FormatHelper
+ /// The results for GetRequestFormat() and GetResponseFormats() are cached on the HttpContext.Items dictionary:
+ /// HttpContext.Items["requestFormat"]
+ /// HttpContext.Items["responseFormat"]
+ /// </summary>
+ public class DefaultFormatHelper : FormatHelper
+ {
+ private const string FormatVariableName = "format";
+ private const string QualityFactor = "q";
+
+ public const string RequestFormatKey = "requestFormat";
+ public const string ResponseFormatKey = "responseFormat";
+
+ /// <summary>
+ /// Returns the format of a given request, according to the following
+ /// rules:
+ /// 1. If a Content-Type header exists it returns a ContentType for it or fails if one can't be created
+ /// 2. Otherwie, if a Content-Type header does not exists it provides the default ContentType of "application/octet-stream" (per RFC 2616 7.2.1)
+ /// </summary>
+ /// <param name="requestContext">The request.</param>
+ /// <returns>The format of the request.</returns>
+ /// <exception cref="HttpException">If the format is unrecognized or not supported.</exception>
+ public override ContentType GetRequestFormat(RequestContext requestContext)
+ {
+ ContentType result;
+ if (!requestContext.HttpContext.Items.Contains(RequestFormatKey))
+ {
+ result = GetRequestFormat(requestContext.HttpContext.Request, true);
+ requestContext.HttpContext.Items.Add(RequestFormatKey, result);
+ }
+ else
+ {
+ result = (ContentType)requestContext.HttpContext.Items[RequestFormatKey];
+ }
+ return result;
+ }
+
+ internal static ContentType GetRequestFormat(HttpRequestBase request, bool throwOnError)
+ {
+ if (!String.IsNullOrEmpty(request.ContentType))
+ {
+ ContentType contentType = ParseContentType(request.ContentType);
+ if (contentType != null)
+ {
+ return contentType;
+ }
+ if (throwOnError)
+ {
+ throw new HttpException((int)HttpStatusCode.UnsupportedMediaType, String.Format(CultureInfo.CurrentCulture, MvcResources.Resources_UnsupportedMediaType, request.ContentType));
+ }
+ return null;
+ }
+ return new ContentType();
+ }
+
+ /// <summary>
+ /// Returns the preferred content type to use for the response, based on the request, according to the following
+ /// rules:
+ /// 1. If the RouteData contains a value for a key called "format", its value is returned as the content type
+ /// 2. Otherwise, if the query string contains a key called "format", its value is returned as the content type
+ /// 3. Otherwise, if the request has an Accepts header, the list of content types in order of preference is returned
+ /// 4. Otherwise, if the request has a content type, its value is returned
+ /// </summary>
+ /// <param name="requestContext">The request.</param>
+ /// <returns>The formats to use for rendering a response.</returns>
+ public override IEnumerable<ContentType> GetResponseFormats(RequestContext requestContext)
+ {
+ IEnumerable<ContentType> result;
+ if (!requestContext.HttpContext.Items.Contains(ResponseFormatKey))
+ {
+ result = GetResponseFormatsRouteAware(requestContext);
+ requestContext.HttpContext.Items.Add(ResponseFormatKey, result);
+ }
+ else
+ {
+ result = (IEnumerable<ContentType>)requestContext.HttpContext.Items[ResponseFormatKey];
+ }
+ return result;
+ }
+
+ private static List<ContentType> GetResponseFormatsRouteAware(RequestContext requestContext)
+ {
+ List<ContentType> result = GetResponseFormatsCore(requestContext.HttpContext.Request);
+ ContentType contentType;
+ if (result == null)
+ {
+ contentType = FormatManager.Current.FormatHelper.GetRequestFormat(requestContext);
+ result = new List<ContentType>(new[] { contentType });
+ }
+ if (TryGetFromRouteData(requestContext.RouteData, out contentType))
+ {
+ result.Insert(0, contentType);
+ }
+ return result;
+ }
+
+ /// <summary>
+ /// Returns the preferred content type to use for the response, based on the request, according to the following
+ /// rules:
+ /// 1. If the query string contains a key called "format", its value is returned as the content type
+ /// 2. Otherwise, if the request has an Accepts header, the list of content types in order of preference is returned
+ /// 3. Otherwise, if the request has a content type, its value is returned
+ /// </summary>
+ /// <param name="request"></param>
+ /// <returns></returns>
+ internal static List<ContentType> GetResponseFormats(HttpRequestBase request)
+ {
+ List<ContentType> result = GetResponseFormatsCore(request);
+ if (result == null)
+ {
+ ContentType contentType = GetRequestFormat(request, true);
+ result = new List<ContentType>(new[] { contentType });
+ }
+ return result;
+ }
+
+ private static List<ContentType> GetResponseFormatsCore(HttpRequestBase request)
+ {
+ ContentType contentType;
+ if (TryGetFromUri(request, out contentType))
+ {
+ return new List<ContentType>(new[] { contentType });
+ }
+ string[] accepts = request.AcceptTypes;
+ if (accepts != null && accepts.Length > 0)
+ {
+ return GetAcceptHeaderElements(accepts);
+ }
+ return null;
+ }
+
+ // CONSIDER: we currently don't process the Accept-Charset header, need to take it into account, EG:
+ // Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3
+ private static List<ContentType> GetAcceptHeaderElements(string[] acceptHeaderElements)
+ {
+ List<ContentType> contentTypeList = new List<ContentType>(acceptHeaderElements.Length);
+ foreach (string acceptHeaderElement in acceptHeaderElements)
+ {
+ if (acceptHeaderElement != null)
+ {
+ ContentType contentType = ParseContentType(acceptHeaderElement);
+ // ignore unknown formats to allow fallback
+ if (contentType != null)
+ {
+ contentTypeList.Add(contentType);
+ }
+ }
+ }
+ contentTypeList.Sort(new AcceptHeaderElementComparer());
+ // CONSIDER: we used the "q" parameter for sorting, so now we strip it
+ // it might be ebtter to strip it later in case someone needs to access it
+ foreach (ContentType ct in contentTypeList)
+ {
+ if (ct.Parameters.ContainsKey(QualityFactor))
+ {
+ ct.Parameters.Remove(QualityFactor);
+ }
+ }
+ return contentTypeList;
+ }
+
+ public override bool IsBrowserRequest(RequestContext requestContext)
+ {
+ return IsBrowserRequest(requestContext.HttpContext.Request);
+ }
+
+ // Parses a string into a ContentType instance, supports
+ // friendly names and enforces a charset (which defaults to utf-8)
+ internal static ContentType ParseContentType(string contentTypeString)
+ {
+ ContentType contentType = null;
+ try
+ {
+ contentType = new ContentType(contentTypeString);
+ }
+ catch (FormatException)
+ {
+ // This may be a friendly name (for example, "xml" instead of "text/xml").
+ // if so, try mapping to a content type
+ if (!FormatManager.Current.TryMapFormatFriendlyName(contentTypeString, out contentType))
+ {
+ return null;
+ }
+ }
+ Encoding encoding = Encoding.UTF8;
+ if (!String.IsNullOrEmpty(contentType.CharSet))
+ {
+ try
+ {
+ encoding = Encoding.GetEncoding(contentType.CharSet);
+ }
+ catch (ArgumentException)
+ {
+ return null;
+ }
+ }
+ contentType.CharSet = encoding.HeaderName;
+ return contentType;
+ }
+
+ // Route-based format override so clients can use a route variable
+ private static bool TryGetFromRouteData(RouteData routeData, out ContentType contentType)
+ {
+ contentType = null;
+ if (routeData != null)
+ {
+ string fromRouteData = routeData.Values[FormatVariableName] as string;
+ if (!String.IsNullOrEmpty(fromRouteData))
+ {
+ contentType = ParseContentType(fromRouteData);
+ }
+ }
+ return contentType != null;
+ }
+
+ // Uri-based format override so clients can use a query string
+ // also useful when using the browser where you can't set headerss
+ private static bool TryGetFromUri(HttpRequestBase request, out ContentType contentType)
+ {
+ string fromParams = request.QueryString[FormatVariableName];
+ if (fromParams != null)
+ {
+ contentType = ParseContentType(fromParams);
+ if (contentType != null)
+ {
+ return true;
+ }
+ }
+ contentType = null;
+ return false;
+ }
+
+ /// <summary>
+ /// Determines whether the specified HTTP request was sent by a Browser.
+ /// A request is considered to be from the browser if:
+ /// it's a GET or POST
+ /// and does not have a non-HTML entity format (XML/JSON)
+ /// and has a known User-Agent header (as determined by the request's BrowserCapabilities property),
+ /// </summary>
+ /// <param name="request">The request.</param>
+ /// <returns>true if the specified HTTP request is a Browser request; otherwise, false.</returns>
+ internal static bool IsBrowserRequest(HttpRequestBase request)
+ {
+ if (!request.IsHttpMethod(HttpVerbs.Get) && !request.IsHttpMethod(HttpVerbs.Post))
+ {
+ return false;
+ }
+ ContentType requestFormat = GetRequestFormat(request, false);
+ if (requestFormat == null || String.Compare(requestFormat.MediaType, FormatManager.UrlEncoded, StringComparison.OrdinalIgnoreCase) != 0)
+ {
+ if (FormatManager.Current.CanDeserialize(requestFormat))
+ {
+ return false;
+ }
+ }
+ HttpBrowserCapabilitiesBase browserCapabilities = request.Browser;
+ if (browserCapabilities != null && !String.IsNullOrEmpty(request.Browser.Browser) && request.Browser.Browser != "Unknown")
+ {
+ return true;
+ }
+ return false;
+ }
+
+ private class AcceptHeaderElementComparer : IComparer<ContentType>
+ {
+ [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", Target = "x, y",
+ Justification = "No need to fix this since this is a private class.")]
+ public int Compare(ContentType x, ContentType y)
+ {
+ string[] xTypeSubType = x.MediaType.Split('/');
+ string[] yTypeSubType = y.MediaType.Split('/');
+
+ if (String.Equals(xTypeSubType[0], yTypeSubType[0], StringComparison.OrdinalIgnoreCase))
+ {
+ if (String.Equals(xTypeSubType[1], yTypeSubType[1], StringComparison.OrdinalIgnoreCase))
+ {
+ // need to check the number of parameters to determine which is more specific
+ bool xHasParam = HasParameters(x);
+ bool yHasParam = HasParameters(y);
+ if (xHasParam && !yHasParam)
+ {
+ return 1;
+ }
+ else if (!xHasParam && yHasParam)
+ {
+ return -1;
+ }
+ }
+ else
+ {
+ if (xTypeSubType[1][0] == '*' && xTypeSubType[1].Length == 1)
+ {
+ return 1;
+ }
+ if (yTypeSubType[1][0] == '*' && yTypeSubType[1].Length == 1)
+ {
+ return -1;
+ }
+ }
+ }
+ else if (xTypeSubType[0][0] == '*' && xTypeSubType[0].Length == 1)
+ {
+ return 1;
+ }
+ else if (yTypeSubType[0][0] == '*' && yTypeSubType[0].Length == 1)
+ {
+ return -1;
+ }
+
+ decimal qualityDifference = GetQualityFactor(x) - GetQualityFactor(y);
+ if (qualityDifference < 0)
+ {
+ return 1;
+ }
+ else if (qualityDifference > 0)
+ {
+ return -1;
+ }
+ return 0;
+ }
+
+ private static decimal GetQualityFactor(ContentType contentType)
+ {
+ decimal result;
+ foreach (string key in contentType.Parameters.Keys)
+ {
+ if (String.Equals(QualityFactor, key, StringComparison.OrdinalIgnoreCase))
+ {
+ if (Decimal.TryParse(contentType.Parameters[key], NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out result) &&
+ (result <= (decimal)1.0))
+ {
+ return result;
+ }
+ }
+ }
+
+ return (decimal)1.0;
+ }
+
+ private static bool HasParameters(ContentType contentType)
+ {
+ int number = 0;
+ foreach (string param in contentType.Parameters.Keys)
+ {
+ if (!String.Equals(QualityFactor, param, StringComparison.OrdinalIgnoreCase))
+ {
+ number++;
+ }
+ }
+
+ return (number > 0);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Resources/DefaultFormatManager.cs b/src/Microsoft.Web.Mvc/Resources/DefaultFormatManager.cs
new file mode 100644
index 00000000..1b282fb5
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Resources/DefaultFormatManager.cs
@@ -0,0 +1,15 @@
+namespace Microsoft.Web.Mvc.Resources
+{
+ public class DefaultFormatManager : FormatManager
+ {
+ public DefaultFormatManager()
+ {
+ XmlFormatHandler xmlHandler = new XmlFormatHandler();
+ JsonFormatHandler jsonHandler = new JsonFormatHandler();
+ this.RequestFormatHandlers.Add(xmlHandler);
+ this.RequestFormatHandlers.Add(jsonHandler);
+ this.ResponseFormatHandlers.Add(xmlHandler);
+ this.ResponseFormatHandlers.Add(jsonHandler);
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Resources/FormatHelper.cs b/src/Microsoft.Web.Mvc/Resources/FormatHelper.cs
new file mode 100644
index 00000000..2b4c171d
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Resources/FormatHelper.cs
@@ -0,0 +1,35 @@
+using System.Collections.Generic;
+using System.Net.Mime;
+using System.Web;
+using System.Web.Routing;
+
+namespace Microsoft.Web.Mvc.Resources
+{
+ /// <summary>
+ /// Base class for content negotiation support
+ /// </summary>
+ public abstract class FormatHelper
+ {
+ /// <summary>
+ /// Returns the ContentType of a given request.
+ /// </summary>
+ /// <param name="requestContext">The request.</param>
+ /// <returns>The format of the request.</returns>
+ /// <exception cref="HttpException">If the format is unrecognized or not supported.</exception>
+ public abstract ContentType GetRequestFormat(RequestContext requestContext);
+
+ /// <summary>
+ /// Returns a collection of ContentType instances that can be used to render a response to a given request, sorted in priority order.
+ /// </summary>
+ /// <param name="requestContext">The request.</param>
+ /// <returns>The formats to use for rendering a response.</returns>
+ public abstract IEnumerable<ContentType> GetResponseFormats(RequestContext requestContext);
+
+ /// <summary>
+ /// Determines whether the specified HTTP request was sent by a Browser.
+ /// </summary>
+ /// <param name="requestContext">The request.</param>
+ /// <returns></returns>
+ public abstract bool IsBrowserRequest(RequestContext requestContext);
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Resources/FormatManager.cs b/src/Microsoft.Web.Mvc/Resources/FormatManager.cs
new file mode 100644
index 00000000..6e9889f1
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Resources/FormatManager.cs
@@ -0,0 +1,139 @@
+using System;
+using System.Collections.ObjectModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Net.Mime;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc.Resources
+{
+ /// <summary>
+ /// Class that maintains a registration of handlers for
+ /// request and response formats
+ /// </summary>
+ public class FormatManager
+ {
+ public const string UrlEncoded = "application/x-www-form-urlencoded";
+
+ private static FormatManager _current = new DefaultFormatManager();
+
+ private Collection<IRequestFormatHandler> _requestHandlers;
+ private Collection<IResponseFormatHandler> _responseHandlers;
+ private FormatHelper _formatHelper;
+
+ public FormatManager()
+ {
+ this._requestHandlers = new Collection<IRequestFormatHandler>();
+ this._responseHandlers = new Collection<IResponseFormatHandler>();
+ this._formatHelper = new DefaultFormatHelper();
+ }
+
+ /// <summary>
+ /// The list of handlers that can parse the request body
+ /// </summary>
+ public Collection<IRequestFormatHandler> RequestFormatHandlers
+ {
+ get { return this._requestHandlers; }
+ }
+
+ /// <summary>
+ /// The list of handlers that can serialize the response body
+ /// </summary>
+ public Collection<IResponseFormatHandler> ResponseFormatHandlers
+ {
+ get { return this._responseHandlers; }
+ }
+
+ public static FormatManager Current
+ {
+ get { return _current; }
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException("value");
+ }
+ _current = value;
+ }
+ }
+
+ // CONSIDER: the FormatHelper is an abstraction that lets users extend the content negotiation process
+ // we must reconsider the FormatManager/FormatHelper factoring and provide a cleaner way of allowing this same extensibility
+ public FormatHelper FormatHelper
+ {
+ get { return _formatHelper; }
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException("value");
+ }
+ _formatHelper = value;
+ }
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1007:UseGenericsWhereAppropriate", Justification = "This is an existing API; this would be a breaking change")]
+ public bool TryDeserialize(ControllerContext controllerContext, ModelBindingContext bindingContext, ContentType requestFormat, out object model)
+ {
+ for (int i = 0; i < this.RequestFormatHandlers.Count; ++i)
+ {
+ if (this.RequestFormatHandlers[i].CanDeserialize(requestFormat))
+ {
+ model = this.RequestFormatHandlers[i].Deserialize(controllerContext, bindingContext, requestFormat);
+ return true;
+ }
+ }
+ model = null;
+ return false;
+ }
+
+ public bool CanDeserialize(ContentType contentType)
+ {
+ for (int i = 0; i < this.RequestFormatHandlers.Count; ++i)
+ {
+ if (this.RequestFormatHandlers[i].CanDeserialize(contentType))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public bool CanSerialize(ContentType responseFormat)
+ {
+ for (int i = 0; i < this.ResponseFormatHandlers.Count; ++i)
+ {
+ if (this.ResponseFormatHandlers[i].CanSerialize(responseFormat))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public void Serialize(ControllerContext context, object model, ContentType responseFormat)
+ {
+ for (int i = 0; i < this.ResponseFormatHandlers.Count; ++i)
+ {
+ if (this.ResponseFormatHandlers[i].CanSerialize(responseFormat))
+ {
+ this.ResponseFormatHandlers[i].Serialize(context, model, responseFormat);
+ return;
+ }
+ }
+ throw new NotSupportedException();
+ }
+
+ public bool TryMapFormatFriendlyName(string formatName, out ContentType contentType)
+ {
+ for (int i = 0; i < this.ResponseFormatHandlers.Count; ++i)
+ {
+ if (this.ResponseFormatHandlers[i].TryMapFormatFriendlyName(formatName, out contentType))
+ {
+ return true;
+ }
+ }
+ contentType = null;
+ return false;
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Resources/HtmlHelperExtensions.cs b/src/Microsoft.Web.Mvc/Resources/HtmlHelperExtensions.cs
new file mode 100644
index 00000000..48abe2bd
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Resources/HtmlHelperExtensions.cs
@@ -0,0 +1,135 @@
+using System;
+using System.Web.Mvc;
+using System.Web.Mvc.Html;
+
+namespace Microsoft.Web.Mvc.Resources
+{
+ public static class HtmlHelperExtensions
+ {
+ /// <summary>
+ /// Generates the Form preamble, defaulting the link for the Retrieve action
+ /// </summary>
+ /// <param name="html"></param>
+ /// <param name="controllerName"></param>
+ /// <param name="routeValues"></param>
+ /// <returns></returns>
+ public static MvcForm BeginResourceForm(this HtmlHelper html, string controllerName, object routeValues)
+ {
+ return html.BeginResourceForm(controllerName, routeValues, ActionType.Retrieve);
+ }
+
+ /// <summary>
+ /// Generates the Form preamble
+ /// </summary>
+ /// <param name="html"></param>
+ /// <param name="controllerName"></param>
+ /// <param name="routeValues"></param>
+ /// <param name="actionType"></param>
+ /// <returns></returns>
+ public static MvcForm BeginResourceForm(this HtmlHelper html, string controllerName, object routeValues, ActionType actionType)
+ {
+ return html.BeginResourceForm(controllerName, routeValues, null, actionType);
+ }
+
+ /// <summary>
+ /// Generates the Form preamble
+ /// </summary>
+ /// <param name="html"></param>
+ /// <param name="controllerName"></param>
+ /// <param name="routeValues"></param>
+ /// <param name="htmlAttributes"></param>
+ /// <param name="actionType"></param>
+ /// <returns></returns>
+ public static MvcForm BeginResourceForm(this HtmlHelper html, string controllerName, object routeValues, object htmlAttributes, ActionType actionType)
+ {
+ switch (actionType)
+ {
+ case ActionType.GetUpdateForm:
+ return html.BeginRouteForm(controllerName + "-editForm", routeValues, FormMethod.Post, htmlAttributes);
+ case ActionType.GetCreateForm:
+ return html.BeginRouteForm(controllerName + "-createForm", FormMethod.Post, htmlAttributes);
+ case ActionType.Retrieve:
+ case ActionType.Delete:
+ case ActionType.Update:
+ return html.BeginRouteForm(controllerName, routeValues, FormMethod.Post, htmlAttributes);
+ case ActionType.Create:
+ return html.BeginRouteForm(controllerName + "-create", FormMethod.Post, htmlAttributes);
+ case ActionType.Index:
+ return html.BeginRouteForm(controllerName + "-index", FormMethod.Post, htmlAttributes);
+ default:
+ throw new ArgumentOutOfRangeException("actionType");
+ }
+ }
+
+ /// <summary>
+ /// Generates a link to the resource controller, defaulting to the Retrieve action
+ /// </summary>
+ /// <param name="html"></param>
+ /// <param name="controllerName"></param>
+ /// <param name="routeValues"></param>
+ /// <returns></returns>
+ public static MvcHtmlString ResourceLink(this HtmlHelper html, string controllerName, object routeValues)
+ {
+ return html.ResourceLink(controllerName, controllerName, routeValues, ActionType.Retrieve);
+ }
+
+ /// <summary>
+ /// Generates a link to the resource controller, defaulting to the Retrieve action
+ /// </summary>
+ /// <param name="html"></param>
+ /// <param name="controllerName"></param>
+ /// <param name="linkText"></param>
+ /// <param name="routeValues"></param>
+ /// <returns></returns>
+ public static MvcHtmlString ResourceLink(this HtmlHelper html, string controllerName, string linkText, object routeValues)
+ {
+ return html.ResourceLink(controllerName, linkText, routeValues, ActionType.Retrieve);
+ }
+
+ /// <summary>
+ /// Generates a link to the resource controller
+ /// </summary>
+ /// <param name="html"></param>
+ /// <param name="controllerName"></param>
+ /// <param name="linkText"></param>
+ /// <param name="routeValues"></param>
+ /// <param name="actionType"></param>
+ /// <returns></returns>
+ public static MvcHtmlString ResourceLink(this HtmlHelper html, string controllerName, string linkText, object routeValues, ActionType actionType)
+ {
+ switch (actionType)
+ {
+ case ActionType.GetUpdateForm:
+ return html.RouteLink(linkText, controllerName + "-editForm", routeValues);
+ case ActionType.GetCreateForm:
+ return html.RouteLink(linkText, controllerName + "-createForm", routeValues);
+ case ActionType.Retrieve:
+ case ActionType.Delete:
+ case ActionType.Update:
+ return html.RouteLink(linkText, controllerName, routeValues);
+ case ActionType.Create:
+ return html.RouteLink(linkText, controllerName + "-create", routeValues);
+ case ActionType.Index:
+ return html.RouteLink(linkText, controllerName + "-index", routeValues);
+ default:
+ throw new ArgumentOutOfRangeException("actionType");
+ }
+ }
+
+ /// <summary>
+ /// Emits a hidden form variable for X-Http-Method-Override. The only valid values for actionType
+ /// are ActionType.Delete and ActionType.Update
+ /// </summary>
+ /// <param name="html"></param>
+ /// <param name="actionType"></param>
+ /// <returns></returns>
+ public static MvcHtmlString HttpMethodOverride(this HtmlHelper html, ActionType actionType)
+ {
+ if (actionType != ActionType.Delete && actionType != ActionType.Update)
+ {
+ throw new ArgumentOutOfRangeException("actionType");
+ }
+ return html.HttpMethodOverride(actionType == ActionType.Delete ? "DELETE" : "PUT");
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Resources/HttpRequestBaseExtensions.cs b/src/Microsoft.Web.Mvc/Resources/HttpRequestBaseExtensions.cs
new file mode 100644
index 00000000..8d0c331f
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Resources/HttpRequestBaseExtensions.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Collections.Generic;
+using System.Net.Mime;
+using System.Web;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc.Resources
+{
+ /// <summary>
+ /// HttpRequestBase extension methods that call directly into the DefaultFormatHelper
+ /// </summary>
+ public static class HttpRequestBaseExtensions
+ {
+ public static ContentType GetRequestFormat(this HttpRequestBase request)
+ {
+ return DefaultFormatHelper.GetRequestFormat(request, true);
+ }
+
+ public static IEnumerable<ContentType> GetResponseFormats(this HttpRequestBase request)
+ {
+ return DefaultFormatHelper.GetResponseFormats(request);
+ }
+
+ internal static bool HasBody(this HttpRequestBase request)
+ {
+ return request.ContentLength > 0 || String.Compare("chunked", request.Headers["Transfer-Encoding"], StringComparison.OrdinalIgnoreCase) == 0;
+ }
+
+ public static bool IsBrowserRequest(this HttpRequestBase request)
+ {
+ return DefaultFormatHelper.IsBrowserRequest(request);
+ }
+
+ public static bool IsHttpMethod(this HttpRequestBase request, HttpVerbs httpMethod)
+ {
+ return request.IsHttpMethod(httpMethod, false);
+ }
+
+ public static bool IsHttpMethod(this HttpRequestBase request, string httpMethod)
+ {
+ return request.IsHttpMethod(httpMethod, false);
+ }
+
+ // CODEREVIEW: this implementation kind of misses the point of HttpVerbs
+ // by falling back to string comparison, consider something better
+ // also, how do we keep this switch in sync?
+ public static bool IsHttpMethod(this HttpRequestBase request, HttpVerbs httpMethod, bool allowOverride)
+ {
+ switch (httpMethod)
+ {
+ case HttpVerbs.Get:
+ return request.IsHttpMethod("GET", allowOverride);
+ case HttpVerbs.Post:
+ return request.IsHttpMethod("POST", allowOverride);
+ case HttpVerbs.Put:
+ return request.IsHttpMethod("PUT", allowOverride);
+ case HttpVerbs.Delete:
+ return request.IsHttpMethod("DELETE", allowOverride);
+ case HttpVerbs.Head:
+ return request.IsHttpMethod("HEAD", allowOverride);
+ default:
+ // CODEREVIEW: does this look reasonable?
+ return request.IsHttpMethod(httpMethod.ToString().ToUpperInvariant(), allowOverride);
+ }
+ }
+
+ public static bool IsHttpMethod(this HttpRequestBase request, string httpMethod, bool allowOverride)
+ {
+ string requestHttpMethod = allowOverride ? request.GetHttpMethodOverride() : request.HttpMethod;
+ return String.Equals(requestHttpMethod, httpMethod, StringComparison.OrdinalIgnoreCase);
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Resources/IEnumerableExtensions.cs b/src/Microsoft.Web.Mvc/Resources/IEnumerableExtensions.cs
new file mode 100644
index 00000000..47817e2f
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Resources/IEnumerableExtensions.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Microsoft.Web.Mvc.Resources
+{
+ public static class IEnumerableExtensions
+ {
+ /// <summary>
+ /// Convenience API to allow an IEnumerable{T} (such as returned by Linq2Sql) to be serialized by DataContractSerilizer
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="collection"></param>
+ /// <returns></returns>
+ public static IEnumerable<T> AsSerializable<T>(this IEnumerable<T> collection) where T : class
+ {
+ return new IEnumerableWrapper<T>(collection);
+ }
+
+ // This wrapper allows IEnumerable<T> to be serialized by DataContractSerilizer
+ // it implements the minimal amount of surface needed for serialization.
+ private class IEnumerableWrapper<T> : IEnumerable<T>
+ where T : class
+ {
+ private IEnumerable<T> _collection;
+
+ // The DataContractSerilizer needs a default constructor to ensure the object can be
+ // deserialized. We have a dummy one since we don't actually need deserialization.
+ public IEnumerableWrapper()
+ {
+ throw new NotImplementedException();
+ }
+
+ internal IEnumerableWrapper(IEnumerable<T> collection)
+ {
+ this._collection = collection;
+ }
+
+ // The DataContractSerilizer needs an Add method to ensure the object can be
+ // deserialized. We have a dummy one since we don't actually need deserialization.
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Needed to satisfy the deserialization contract")]
+ [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "item", Justification = "Needed to satisfy the deserialization contract")]
+ public void Add(T item)
+ {
+ throw new NotImplementedException();
+ }
+
+ public IEnumerator<T> GetEnumerator()
+ {
+ return this._collection.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return ((IEnumerable)this._collection).GetEnumerator();
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Resources/IRequestFormatHandler.cs b/src/Microsoft.Web.Mvc/Resources/IRequestFormatHandler.cs
new file mode 100644
index 00000000..a450e0f3
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Resources/IRequestFormatHandler.cs
@@ -0,0 +1,30 @@
+using System.Net.Mime;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc.Resources
+{
+ /// <summary>
+ /// Extensibility mechanism for deserializing data in additional formats.
+ /// FormatManager.Current.RequestFormatHandlers contains the list of request formats
+ /// supported by the web application
+ /// </summary>
+ public interface IRequestFormatHandler
+ {
+ /// <summary>
+ /// Returns true if the handler can deserialize request's content type
+ /// </summary>
+ /// <param name="requestFormat"></param>
+ /// <returns></returns>
+ bool CanDeserialize(ContentType requestFormat);
+
+ /// <summary>
+ /// Deserialize the request body based on model binding context and return the object.
+ /// Note that the URI parameters are handled by the base infrastructure.
+ /// </summary>
+ /// <param name="controllerContext"></param>
+ /// <param name="bindingContext"></param>
+ /// <param name="requestFormat"></param>
+ /// <returns></returns>
+ object Deserialize(ControllerContext controllerContext, ModelBindingContext bindingContext, ContentType requestFormat);
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Resources/IResponseFormatHandler.cs b/src/Microsoft.Web.Mvc/Resources/IResponseFormatHandler.cs
new file mode 100644
index 00000000..a9e5fa12
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Resources/IResponseFormatHandler.cs
@@ -0,0 +1,43 @@
+using System.Net.Mime;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc.Resources
+{
+ /// <summary>
+ /// Extensibility mechanism for serializing response in
+ /// additional formats. FormatManager.Current.RequestFormatHandlers contains the list of request formats
+ /// supported by the web application
+ /// </summary>
+ public interface IResponseFormatHandler
+ {
+ /// <summary>
+ /// The preferred friendly name for the handled format
+ /// </summary>
+ string FriendlyName { get; }
+
+ /// <summary>
+ /// Return true if the specified friendly name ('xml' for instance) can
+ /// be mapped to a content type ('application/xml' for instance). If the mapping
+ /// can be performed return the content type that the friendlyName maps to
+ /// </summary>
+ /// <param name="friendlyName"></param>
+ /// <param name="contentType"></param>
+ /// <returns></returns>
+ bool TryMapFormatFriendlyName(string friendlyName, out ContentType contentType);
+
+ /// <summary>
+ /// Return true if the specified response format can be serialized
+ /// </summary>
+ /// <param name="responseFormat"></param>
+ /// <returns></returns>
+ bool CanSerialize(ContentType responseFormat);
+
+ /// <summary>
+ /// Serialize the model into the response body in the specified response format
+ /// </summary>
+ /// <param name="context"></param>
+ /// <param name="model"></param>
+ /// <param name="responseFormat"></param>
+ void Serialize(ControllerContext context, object model, ContentType responseFormat);
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Resources/JsonFormatHandler.cs b/src/Microsoft.Web.Mvc/Resources/JsonFormatHandler.cs
new file mode 100644
index 00000000..8cca6b90
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Resources/JsonFormatHandler.cs
@@ -0,0 +1,53 @@
+using System;
+using System.Net.Mime;
+using System.Runtime.Serialization.Json;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc.Resources
+{
+ public class JsonFormatHandler : IRequestFormatHandler, IResponseFormatHandler
+ {
+ public string FriendlyName
+ {
+ get { return "Json"; }
+ }
+
+ public bool CanDeserialize(ContentType requestFormat)
+ {
+ return requestFormat != null && IsCompatibleMediaType(requestFormat.MediaType);
+ }
+
+ public object Deserialize(ControllerContext controllerContext, ModelBindingContext bindingContext, ContentType requestFormat)
+ {
+ DataContractJsonSerializer json = new DataContractJsonSerializer(bindingContext.ModelType);
+ return json.ReadObject(controllerContext.HttpContext.Request.InputStream);
+ }
+
+ public bool CanSerialize(ContentType responseFormat)
+ {
+ return responseFormat != null && IsCompatibleMediaType(responseFormat.MediaType);
+ }
+
+ public void Serialize(ControllerContext context, object model, ContentType responseFormat)
+ {
+ DataContractJsonActionResult json = new DataContractJsonActionResult(model, responseFormat);
+ json.ExecuteResult(context);
+ }
+
+ protected virtual bool IsCompatibleMediaType(string mediaType)
+ {
+ return (mediaType == "text/json" || mediaType == "application/json");
+ }
+
+ public bool TryMapFormatFriendlyName(string friendlyName, out ContentType contentType)
+ {
+ if (String.Equals(friendlyName, this.FriendlyName, StringComparison.OrdinalIgnoreCase))
+ {
+ contentType = new ContentType("application/json");
+ return true;
+ }
+ contentType = null;
+ return false;
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Resources/MultiFormatActionResult.cs b/src/Microsoft.Web.Mvc/Resources/MultiFormatActionResult.cs
new file mode 100644
index 00000000..0f8791d8
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Resources/MultiFormatActionResult.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Net;
+using System.Net.Mime;
+using System.Web;
+using System.Web.Mvc;
+using Microsoft.Web.Mvc.Properties;
+
+namespace Microsoft.Web.Mvc.Resources
+{
+ /// <summary>
+ /// Returns the response in the format specified by the request. By default, supports returning the model
+ /// as a HTML view, XML and JSON.
+ /// If the response format requested is not supported, then the NotAcceptable status code is returned
+ /// </summary>
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Multi", Justification = "FxCop won't accept this in the custom dictionary, so we're suppressing it in source")]
+ public class MultiFormatActionResult : ActionResult
+ {
+ private object _model;
+ private ContentType _responseFormat;
+ private HttpStatusCode _statusCode;
+
+ public MultiFormatActionResult(object model, ContentType responseFormat)
+ : this(model, responseFormat, HttpStatusCode.OK)
+ {
+ }
+
+ public MultiFormatActionResult(object model, ContentType responseFormat, HttpStatusCode statusCode)
+ {
+ _model = model;
+ _responseFormat = responseFormat;
+ _statusCode = statusCode;
+ }
+
+ public override void ExecuteResult(ControllerContext context)
+ {
+ if (!TryExecuteResult(context, this._model, this._responseFormat))
+ {
+ throw new HttpException((int)HttpStatusCode.NotAcceptable, String.Format(CultureInfo.CurrentCulture, MvcResources.Resources_UnsupportedFormat, this._responseFormat));
+ }
+ }
+
+ public virtual bool TryExecuteResult(ControllerContext context, object model, ContentType responseFormat)
+ {
+ if (!FormatManager.Current.CanSerialize(responseFormat))
+ {
+ return false;
+ }
+ context.HttpContext.Response.ClearContent();
+ context.HttpContext.Response.StatusCode = (int)_statusCode;
+ FormatManager.Current.Serialize(context, model, responseFormat);
+ return true;
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Resources/RequestContextExtensions.cs b/src/Microsoft.Web.Mvc/Resources/RequestContextExtensions.cs
new file mode 100644
index 00000000..b743dc48
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Resources/RequestContextExtensions.cs
@@ -0,0 +1,27 @@
+using System.Collections.Generic;
+using System.Net.Mime;
+using System.Web.Routing;
+
+namespace Microsoft.Web.Mvc.Resources
+{
+ /// <summary>
+ /// RequestContext extension methods that call directly into the registered FormatHelper
+ /// </summary>
+ public static class RequestContextExtensions
+ {
+ public static ContentType GetRequestFormat(this RequestContext requestContext)
+ {
+ return FormatManager.Current.FormatHelper.GetRequestFormat(requestContext);
+ }
+
+ public static IEnumerable<ContentType> GetResponseFormats(this RequestContext requestContext)
+ {
+ return FormatManager.Current.FormatHelper.GetResponseFormats(requestContext);
+ }
+
+ public static bool IsBrowserRequest(this RequestContext requestContext)
+ {
+ return FormatManager.Current.FormatHelper.IsBrowserRequest(requestContext);
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Resources/ResourceControllerFactory.cs b/src/Microsoft.Web.Mvc/Resources/ResourceControllerFactory.cs
new file mode 100644
index 00000000..2d235b1b
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Resources/ResourceControllerFactory.cs
@@ -0,0 +1,179 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Net;
+using System.Net.Mime;
+using System.Text;
+using System.Web;
+using System.Web.Mvc;
+using System.Web.Routing;
+using System.Web.SessionState;
+using Microsoft.Web.Mvc.Properties;
+
+namespace Microsoft.Web.Mvc.Resources
+{
+ /// <summary>
+ /// Specialized ControllerFactory that augments the base controller factory to make it RESTful - specifically, adding
+ /// support for multiple formats, HTTP method based dispatch to controller methods and HTTP error handling
+ /// </summary>
+ public class ResourceControllerFactory : IControllerFactory
+ {
+ private const string RestActionToken = "$REST$";
+
+ private IControllerFactory _inner;
+
+ public ResourceControllerFactory()
+ {
+ _inner = ControllerBuilder.Current.GetControllerFactory();
+ }
+
+ public ResourceControllerFactory(IControllerFactory inner)
+ {
+ _inner = inner;
+ }
+
+ public IController CreateController(RequestContext requestContext, string controllerName)
+ {
+ IController ic = _inner.CreateController(requestContext, controllerName);
+ Controller c = ic as Controller;
+ if (c != null && WebApiEnabledAttribute.IsDefined(c))
+ {
+ IActionInvoker iai = c.ActionInvoker;
+ ControllerActionInvoker cai = iai as ControllerActionInvoker;
+ if (cai != null)
+ {
+ c.ActionInvoker = new ResourceControllerActionInvoker();
+
+ string actionName = requestContext.RouteData.Values["action"] as string;
+ if (String.IsNullOrEmpty(actionName))
+ {
+ // set it to a well known dummy value to avoid not having an action as that would prevent the fixup
+ // code in ResourceControllerActionInvoker, which is based on ActionDescriptor, from running
+ requestContext.RouteData.Values["action"] = RestActionToken;
+ }
+ }
+ }
+ return ic;
+ }
+
+ public SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName)
+ {
+ return _inner.GetControllerSessionBehavior(requestContext, controllerName);
+ }
+
+ public void ReleaseController(IController controller)
+ {
+ _inner.ReleaseController(controller);
+ }
+
+ // This ActionInvoker allows us to dispatch to a controller when no action was provided by the routing
+ // infrastructure, but the information is available in the request's HTTP verb (GET/PUT/POST/DELETE)
+ private class ResourceControllerActionInvoker : ControllerActionInvoker
+ {
+ protected override ActionDescriptor FindAction(ControllerContext controllerContext, ControllerDescriptor controllerDescriptor, string actionName)
+ {
+ if (actionName == RestActionToken)
+ {
+ // cleanup the restActionToken we set earlier
+ controllerContext.RequestContext.RouteData.Values["action"] = null;
+
+ List<ActionDescriptor> matches = new List<ActionDescriptor>();
+ foreach (ActionDescriptor ad in controllerDescriptor.GetCanonicalActions())
+ {
+ object[] acceptVerbs = ad.GetCustomAttributes(typeof(AcceptVerbsAttribute), false);
+ if (acceptVerbs.Length > 0)
+ {
+ foreach (object o in acceptVerbs)
+ {
+ AcceptVerbsAttribute ava = o as AcceptVerbsAttribute;
+ if (ava != null)
+ {
+ if (ava.Verbs.Contains(controllerContext.HttpContext.Request.GetHttpMethodOverride().ToUpperInvariant()))
+ {
+ matches.Add(ad);
+ }
+ }
+ }
+ }
+ }
+ switch (matches.Count)
+ {
+ case 0:
+ break;
+ case 1:
+ ActionDescriptor ad = matches[0];
+ actionName = ad.ActionName;
+ controllerContext.RequestContext.RouteData.Values["action"] = actionName;
+ return ad;
+ default:
+ StringBuilder matchesString = new StringBuilder(matches[0].ActionName);
+ for (int index = 1; index < matches.Count; index++)
+ {
+ matchesString.Append(", ");
+ matchesString.Append(matches[index].ActionName);
+ }
+ return new ResourceErrorActionDescriptor(
+ controllerDescriptor,
+ HttpStatusCode.Conflict,
+ String.Format(
+ CultureInfo.CurrentCulture,
+ MvcResources.ResourceControllerFactory_ConflictingActions,
+ controllerDescriptor.ControllerName,
+ matchesString));
+ }
+ }
+ return base.FindAction(controllerContext, controllerDescriptor, actionName) ??
+ new ResourceErrorActionDescriptor(
+ controllerDescriptor,
+ HttpStatusCode.NotFound,
+ String.Format(
+ CultureInfo.CurrentCulture,
+ MvcResources.ResourceControllerFactory_NoActions,
+ controllerDescriptor.ControllerName));
+ }
+
+ // This class is used when we don't find an ActionDescriptor or find multiple matches
+ // in this case we want to return an error response but throwing an HttpException from
+ // FindAction bypasses the InvokeExceptionFilters, so instead we throw in this custom ActionDescriptor
+ private class ResourceErrorActionDescriptor : ActionDescriptor
+ {
+ private ControllerDescriptor controllerDescriptor;
+ private string message;
+ private HttpStatusCode statusCode;
+
+ public ResourceErrorActionDescriptor(ControllerDescriptor controllerDescriptor, HttpStatusCode statusCode, string message)
+ {
+ this.message = message;
+ this.statusCode = statusCode;
+ this.controllerDescriptor = controllerDescriptor;
+ }
+
+ public override string ActionName
+ {
+ get { return RestActionToken; }
+ }
+
+ public override ControllerDescriptor ControllerDescriptor
+ {
+ get { return this.controllerDescriptor; }
+ }
+
+ public override object Execute(ControllerContext controllerContext, IDictionary<string, object> parameters)
+ {
+ HttpException he = new HttpException((int)this.statusCode, this.message);
+ ResourceErrorActionResult rear;
+ if (!WebApiEnabledAttribute.TryGetErrorResult2(controllerContext.RequestContext, he, out rear))
+ {
+ rear = new ResourceErrorActionResult(new HttpException((int)this.statusCode, this.message), new ContentType("text/plain"));
+ }
+ return rear;
+ }
+
+ public override ParameterDescriptor[] GetParameters()
+ {
+ return new ParameterDescriptor[0];
+ }
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Resources/ResourceErrorActionResult.cs b/src/Microsoft.Web.Mvc/Resources/ResourceErrorActionResult.cs
new file mode 100644
index 00000000..74ffb6d3
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Resources/ResourceErrorActionResult.cs
@@ -0,0 +1,48 @@
+using System.Net;
+using System.Net.Mime;
+using System.Web;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc.Resources
+{
+ /// <summary>
+ /// Action result for returning HTTP errors that result from performing operations on resources, including
+ /// an optional details in the HTTP body
+ /// </summary>
+ public class ResourceErrorActionResult : ActionResult
+ {
+ private object details;
+ private ContentType responseFormat;
+ private HttpStatusCode statusCode;
+
+ /// <summary>
+ /// Sends back a response using the status code in the HttpException.
+ /// The response body contains a details serialized in the responseFormat.
+ /// If the HttpException.Data has a key named "details", its value is used as the response body.
+ /// If there is no such key, HttpException.ToString() is used as the response body.
+ /// </summary>
+ /// <param name="httpException"></param>
+ /// <param name="responseFormat"></param>
+ public ResourceErrorActionResult(HttpException httpException, ContentType responseFormat)
+ {
+ this.statusCode = (HttpStatusCode)httpException.GetHttpCode();
+ this.details = httpException.Data.Contains("details") ? httpException.Data["details"] : httpException.ToString();
+ this.responseFormat = responseFormat;
+ }
+
+ public override void ExecuteResult(ControllerContext context)
+ {
+ if (this.details != null)
+ {
+ MultiFormatActionResult rar = new MultiFormatActionResult(this.details, this.responseFormat, this.statusCode);
+ if (rar.TryExecuteResult(context, this.details, this.responseFormat))
+ {
+ return;
+ }
+ }
+ context.HttpContext.Response.ClearContent();
+ context.HttpContext.Response.StatusCode = (int)this.statusCode;
+ context.HttpContext.Response.TrySkipIisCustomErrors = true;
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Resources/ResourceModelBinder.cs b/src/Microsoft.Web.Mvc/Resources/ResourceModelBinder.cs
new file mode 100644
index 00000000..d3659028
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Resources/ResourceModelBinder.cs
@@ -0,0 +1,98 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Net;
+using System.Net.Mime;
+using System.Web;
+using System.Web.Mvc;
+using Microsoft.Web.Mvc.Properties;
+
+namespace Microsoft.Web.Mvc.Resources
+{
+ /// <summary>
+ /// ModelBinder implementation that augments the inner model binder with support for binding to other formats -
+ /// XML and JSON by default.
+ /// </summary>
+ public class ResourceModelBinder : IModelBinder
+ {
+ private IModelBinder _inner;
+
+ /// <summary>
+ /// Wraps the ModelBinders.Binders.DefaultBinder
+ /// </summary>
+ public ResourceModelBinder()
+ : this(ModelBinders.Binders.DefaultBinder)
+ {
+ }
+
+ public ResourceModelBinder(IModelBinder inner)
+ {
+ this._inner = inner;
+ }
+
+ public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
+ {
+ if (WebApiEnabledAttribute.IsDefined(controllerContext.Controller))
+ {
+ if (!controllerContext.RouteData.Values.ContainsKey(bindingContext.ModelName) && controllerContext.HttpContext.Request.HasBody())
+ {
+ ContentType requestFormat = controllerContext.RequestContext.GetRequestFormat();
+ object model;
+ if (TryBindModel(controllerContext, bindingContext, requestFormat, out model))
+ {
+ bindingContext.ModelMetadata.Model = model;
+ MyDefaultModelBinder dmb = new MyDefaultModelBinder();
+ dmb.CallOnModelUpdated(controllerContext, bindingContext);
+ if (!MyDefaultModelBinder.IsModelValid(bindingContext))
+ {
+ List<ModelError> details = new List<ModelError>();
+ foreach (ModelState ms in bindingContext.ModelState.Values)
+ {
+ foreach (ModelError me in ms.Errors)
+ {
+ details.Add(me);
+ }
+ }
+ HttpException failure = new HttpException((int)HttpStatusCode.ExpectationFailed, "Invalid Model");
+ failure.Data["details"] = details;
+ throw failure;
+ }
+ return model;
+ }
+ throw new HttpException((int)HttpStatusCode.UnsupportedMediaType, String.Format(CultureInfo.CurrentCulture, MvcResources.Resources_UnsupportedMediaType, (requestFormat == null ? String.Empty : requestFormat.MediaType)));
+ }
+ }
+ return this._inner.BindModel(controllerContext, bindingContext);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1007:UseGenericsWhereAppropriate", Justification = "This is an existing API; this would be a breaking change")]
+ public bool TryBindModel(ControllerContext controllerContext, ModelBindingContext bindingContext, ContentType requestFormat, out object model)
+ {
+ if (requestFormat != null && String.Compare(requestFormat.MediaType, FormatManager.UrlEncoded, StringComparison.OrdinalIgnoreCase) == 0)
+ {
+ model = this._inner.BindModel(controllerContext, bindingContext);
+ return true;
+ }
+ if (!FormatManager.Current.TryDeserialize(controllerContext, bindingContext, requestFormat, out model))
+ {
+ model = null;
+ return false;
+ }
+ return true;
+ }
+
+ private class MyDefaultModelBinder : DefaultModelBinder
+ {
+ public void CallOnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
+ {
+ OnModelUpdated(controllerContext, bindingContext);
+ }
+
+ internal static new bool IsModelValid(ModelBindingContext bindingContext)
+ {
+ return DefaultModelBinder.IsModelValid(bindingContext);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Resources/ResourceRedirectToRouteResult.cs b/src/Microsoft.Web.Mvc/Resources/ResourceRedirectToRouteResult.cs
new file mode 100644
index 00000000..4aedd811
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Resources/ResourceRedirectToRouteResult.cs
@@ -0,0 +1,31 @@
+using System.Net;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc.Resources
+{
+ /// <summary>
+ /// Augments the RedirectToRouteResult behavior by sending Created HTTP status code in responses to POST, OK HTTP status code otherwise
+ /// </summary>
+ internal class ResourceRedirectToRouteResult : ActionResult
+ {
+ private RedirectToRouteResult inner;
+
+ public ResourceRedirectToRouteResult(RedirectToRouteResult inner)
+ {
+ this.inner = inner;
+ }
+
+ public override void ExecuteResult(ControllerContext context)
+ {
+ // call the base which we expect to be setting the Location header
+ this.inner.ExecuteResult(context);
+
+ if (!context.RequestContext.IsBrowserRequest())
+ {
+ // on POST we return Created, otherwise (EG: DELETE) we return OK
+ context.HttpContext.Response.ClearContent();
+ context.HttpContext.Response.StatusCode = context.HttpContext.Request.IsHttpMethod(HttpVerbs.Post, true) ? (int)HttpStatusCode.Created : (int)HttpStatusCode.OK;
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Resources/RouteCollectionExtensions.cs b/src/Microsoft.Web.Mvc/Resources/RouteCollectionExtensions.cs
new file mode 100644
index 00000000..38055bd9
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Resources/RouteCollectionExtensions.cs
@@ -0,0 +1,135 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Web.Mvc;
+using System.Web.Routing;
+
+namespace Microsoft.Web.Mvc.Resources
+{
+ public static class RouteCollectionExtensions
+ {
+ /// <summary>
+ /// Adds the routes to enable RESTful routing of requests to specified controller. The controllerName is the URL prefix to the controller.
+ /// The routeSuffix is used for more specific routing in the resource. For example, a controllerName of "books" and a routeSuffix of "{id}" will
+ /// result in the following routes being registered for the controller:
+ /// ~/books/, ~/books/{id} to the resource,
+ /// ~/books/CreateForm to the CreateForm controller action,
+ /// ~/books/{id}/EditForm to the EditForm controller action
+ /// </summary>
+ /// <param name="routes"></param>
+ /// <param name="controllerName"></param>
+ /// <param name="routeSuffix"></param>
+ public static void MapResourceRoute(this RouteCollection routes, string controllerName, string routeSuffix)
+ {
+ routes.MapResourceRoute(controllerName, null, routeSuffix, null);
+ }
+
+ /// <summary>
+ /// Adds the routes to enable RESTful routing of requests to specified controller. The controllerName is the URL prefix to the controller.
+ /// The routeSuffix is used for more specific routing in the resource. For example, a controllerName of "books" and a routeSuffix of "{id}" will
+ /// result in the following routes being registered for the controller:
+ /// ~/books/, ~/books/{id} to the resource,
+ /// ~/books/CreateForm to the CreateForm controller action,
+ /// ~/books/{id}/EditForm to the EditForm controller action
+ /// </summary>
+ /// <param name="routes"></param>
+ /// <param name="controllerName"></param>
+ /// <param name="routeSuffix"></param>
+ /// <param name="constraints"></param>
+ public static void MapResourceRoute(this RouteCollection routes, string controllerName, string routeSuffix, object constraints)
+ {
+ routes.MapResourceRoute(controllerName, null, routeSuffix, constraints);
+ }
+
+ /// <summary>
+ /// Adds the routes to enable RESTful routing of requests to specified controller. The routePrefix is the URL prefix to the controller.
+ /// The routeSuffix is used for more specific routing in the resource. For example, a routePrefix of "books" and a routeSuffix of "{id}" will
+ /// result in the following routes being registered for the controller:
+ /// ~/books/, ~/books/{id} to the resource,
+ /// ~/books/CreateForm to the CreateForm controller action,
+ /// ~/books/{id}/EditForm to the EditForm controller action
+ /// </summary>
+ /// <param name="routes"></param>
+ /// <param name="controllerName"></param>
+ /// <param name="routePrefix"></param>
+ /// <param name="routeSuffix"></param>
+ public static void MapResourceRoute(this RouteCollection routes, string controllerName, string routePrefix, string routeSuffix)
+ {
+ routes.MapResourceRoute(controllerName, routePrefix, routeSuffix, null);
+ }
+
+ /// <summary>
+ /// Adds the routes to enable RESTful routing of requests to specified controller. The routePrefix is the URL prefix to the controller.
+ /// The routeSuffix is used for more specific routing in the resource. For example, a routePrefix of "books" and a routeSuffix of "{id}" will
+ /// result in the following routes being registered for the controller:
+ /// ~/books/, ~/books/{id} to the resource,
+ /// ~/books/CreateForm to the CreateForm controller action,
+ /// ~/books/{id}/EditForm to the EditForm controller action
+ /// </summary>
+ /// <param name="routes"></param>
+ /// <param name="controllerName"></param>
+ /// <param name="routePrefix"></param>
+ /// <param name="routeSuffix"></param>
+ /// <param name="constraints"></param>
+ public static void MapResourceRoute(this RouteCollection routes, string controllerName, string routePrefix, string routeSuffix, object constraints)
+ {
+ if (String.IsNullOrEmpty(routePrefix))
+ {
+ routePrefix = controllerName;
+ }
+ else
+ {
+ routePrefix = routePrefix + "/" + controllerName;
+ }
+ if (!String.IsNullOrEmpty(routeSuffix))
+ {
+ routeSuffix = "/" + routeSuffix;
+ }
+
+ routes.MapRoute(
+ controllerName + "-editForm",
+ routePrefix + routeSuffix + "/EditForm",
+ new { controller = controllerName, action = "EditForm" },
+ constraints);
+ routes.MapRoute(
+ controllerName + "-createForm",
+ routePrefix + "/CreateForm",
+ new { controller = controllerName, action = "CreateForm" });
+ routes.MapRoute(
+ controllerName,
+ routePrefix + routeSuffix,
+ new { controller = controllerName },
+ constraints);
+ routes.MapRoute(
+ controllerName + "-create",
+ routePrefix,
+ new { controller = controllerName, action = "Create" },
+ new { postOnly = new HttpMethodConstraint("POST") });
+ routes.MapRoute(
+ controllerName + "-index",
+ routePrefix,
+ new { controller = controllerName, action = "Index" });
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "controller", Justification = "This is an extension method, the parameter is necessary to provide a place to hook the method")]
+ public static string GetResourceRouteName(this Controller controller, string controllerName, ActionType actionType)
+ {
+ switch (actionType)
+ {
+ case ActionType.GetUpdateForm:
+ return controllerName + "-editForm";
+ case ActionType.GetCreateForm:
+ return controllerName + "-createForm";
+ case ActionType.Retrieve:
+ case ActionType.Delete:
+ case ActionType.Update:
+ return controllerName;
+ case ActionType.Create:
+ return controllerName + "-create";
+ case ActionType.Index:
+ return controllerName + "-index";
+ default:
+ throw new ArgumentOutOfRangeException("actionType");
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Resources/UriHelperExtensions.cs b/src/Microsoft.Web.Mvc/Resources/UriHelperExtensions.cs
new file mode 100644
index 00000000..0cce82e8
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Resources/UriHelperExtensions.cs
@@ -0,0 +1,52 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc.Resources
+{
+ public static class UriHelperExtensions
+ {
+ /// <summary>
+ /// Generates the route URL for the resource controller's Retrieve action
+ /// </summary>
+ /// <param name="url"></param>
+ /// <param name="controllerName"></param>
+ /// <param name="routeValues"></param>
+ /// <returns></returns>
+ [SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings", Justification = "The return value is not a regular URL since it may contain ~/ ASP.NET-specific characters"), SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an Extension Method which allows the user to provide a strongly-typed argument via Expression"), SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Need to be sure the passed-in argument is of type Controller::Action")]
+ public static string ResourceUrl(this UrlHelper url, string controllerName, object routeValues)
+ {
+ return url.ResourceUrl(controllerName, routeValues, ActionType.Retrieve);
+ }
+
+ /// <summary>
+ /// Generates the route URL for the resource
+ /// </summary>
+ /// <param name="url"></param>
+ /// <param name="controllerName"></param>
+ /// <param name="routeValues"></param>
+ /// <param name="actionType"></param>
+ /// <returns></returns>
+ [SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings", Justification = "The return value is not a regular URL since it may contain ~/ ASP.NET-specific characters"), SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an Extension Method which allows the user to provide a strongly-typed argument via Expression"), SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Need to be sure the passed-in argument is of type Controller::Action")]
+ public static string ResourceUrl(this UrlHelper url, string controllerName, object routeValues, ActionType actionType)
+ {
+ switch (actionType)
+ {
+ case ActionType.GetUpdateForm:
+ return url.RouteUrl(controllerName + "-editForm", routeValues);
+ case ActionType.GetCreateForm:
+ return url.RouteUrl(controllerName + "-createForm");
+ case ActionType.Retrieve:
+ case ActionType.Delete:
+ case ActionType.Update:
+ return url.RouteUrl(controllerName, routeValues);
+ case ActionType.Create:
+ return url.RouteUrl(controllerName + "-create");
+ case ActionType.Index:
+ return url.RouteUrl(controllerName + "-index");
+ default:
+ throw new ArgumentOutOfRangeException("actionType");
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Resources/WebApiEnabledAttribute.cs b/src/Microsoft.Web.Mvc/Resources/WebApiEnabledAttribute.cs
new file mode 100644
index 00000000..f4464a6f
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Resources/WebApiEnabledAttribute.cs
@@ -0,0 +1,215 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Net;
+using System.Net.Mime;
+using System.Text;
+using System.Web;
+using System.Web.Mvc;
+using System.Web.Routing;
+using Microsoft.Web.Mvc.Properties;
+
+namespace Microsoft.Web.Mvc.Resources
+{
+ /// <summary>
+ /// Attribute indicating that the controller supports multiple formats (HTML, XML, JSON etc), HTTP method based dispatch
+ /// and HTTP error handling.
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
+ [SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", Justification = "This class is designed to be overridden")]
+ public class WebApiEnabledAttribute : ActionFilterAttribute, IExceptionFilter
+ {
+ public WebApiEnabledAttribute()
+ {
+ this.StatusOnNullModel = HttpStatusCode.NotFound;
+ }
+
+ /// <summary>
+ /// The HTTP status code to use in case a null value is returned from the controller action method.
+ /// The default is NotFound
+ /// </summary>
+ public HttpStatusCode StatusOnNullModel { get; set; }
+
+ public static bool IsDefined(ControllerBase controller)
+ {
+ Type controllerType = controller.GetType();
+ WebApiEnabledAttribute[] rea = controllerType.GetCustomAttributes(typeof(WebApiEnabledAttribute), true) as WebApiEnabledAttribute[];
+ return rea != null && rea.Length > 0;
+ }
+
+ public override void OnActionExecuted(ActionExecutedContext filterContext)
+ {
+ MultiFormatActionResult multiFormatResult = filterContext.Result as MultiFormatActionResult;
+ if (multiFormatResult == null)
+ {
+ ViewResultBase viewResult = filterContext.Result as ViewResultBase;
+ if (viewResult != null && viewResult.ViewData != null)
+ {
+ bool handled = false;
+ foreach (ContentType responseFormat in filterContext.RequestContext.GetResponseFormats())
+ {
+ // CONSIDER: making this lookup optional if perf is an issue
+ for (int i = 0; i < FormatManager.Current.ResponseFormatHandlers.Count; ++i)
+ {
+ IResponseFormatHandler handler = FormatManager.Current.ResponseFormatHandlers[i];
+ if (handler.CanSerialize(responseFormat))
+ {
+ // we can't use the full ContentType's name (EG: "text/xml")
+ // instead we use the FriendlyName on the matching IResponseFormatHandler
+ string friendlyName = handler.FriendlyName;
+ string viewName = viewResult.ViewName;
+ if (String.IsNullOrEmpty(viewName))
+ {
+ viewName = filterContext.RouteData.GetRequiredString("action");
+ }
+ // CONSIDER: is this naming convention sufficient? look at extensibility (how can I customize the FindView process?)
+ viewName = viewName + "." + friendlyName;
+ // CONSIDER: ViewEngineCollection queries view engines in registration order and returns 1st match,
+ // would it make sense to let the client provide a hint in case
+ ViewEngineResult result = viewResult.ViewEngineCollection.FindView(filterContext, viewName, null);
+ // ignore errors and fallback to default behavior
+ if (result != null && result.View != null)
+ {
+ Encoding encoding = Encoding.UTF8;
+ if (!String.IsNullOrEmpty(responseFormat.CharSet))
+ {
+ try
+ {
+ encoding = Encoding.GetEncoding(responseFormat.CharSet);
+ }
+ catch (ArgumentException)
+ {
+ throw new HttpException((int)HttpStatusCode.NotAcceptable, String.Format(CultureInfo.CurrentCulture, MvcResources.Resources_UnsupportedFormat, responseFormat));
+ }
+ }
+ responseFormat.CharSet = encoding.HeaderName;
+ filterContext.HttpContext.Response.ContentType = responseFormat.ToString();
+ filterContext.HttpContext.Response.ContentEncoding = encoding;
+ // we have set the Response.ContentType but know that the webforms view engine will override it
+ // a different ViewPage base class that sets this can be used to workaround this
+ // so we make the computed responseFormat available in ViewData
+ viewResult.ViewData[DefaultFormatHelper.ResponseFormatKey] = responseFormat;
+ viewResult.View = result.View;
+ viewResult.ViewName = viewName;
+ handled = true;
+ break;
+ }
+ }
+ }
+ if (handled)
+ {
+ break;
+ }
+ if (TryGetResult(viewResult, responseFormat, out multiFormatResult))
+ {
+ if (multiFormatResult != null)
+ {
+ filterContext.Result = multiFormatResult;
+ }
+ handled = true;
+ break;
+ }
+ }
+ if (!handled)
+ {
+ // if enumeration doesn't yield a handler the request is not acceptable
+ // CONSIDER: returning all formats considered in the exception messages
+ throw new HttpException((int)HttpStatusCode.NotAcceptable, "None of the formats specified by the accept header is supported.");
+ }
+ }
+ }
+ base.OnActionExecuted(filterContext);
+ RedirectToRouteResult redirectResult = filterContext.Result as RedirectToRouteResult;
+ if (redirectResult != null && !filterContext.RequestContext.IsBrowserRequest())
+ {
+ filterContext.Result = new ResourceRedirectToRouteResult(redirectResult);
+ }
+ }
+
+ public void OnException(ExceptionContext filterContext)
+ {
+ if (filterContext.ExceptionHandled)
+ {
+ return;
+ }
+ HttpException he = filterContext.Exception as HttpException;
+ if (he != null)
+ {
+ ResourceErrorActionResult rear;
+ if (TryGetErrorResult2(filterContext.RequestContext, he, out rear))
+ {
+ if (rear != null)
+ {
+ filterContext.Result = rear;
+ filterContext.ExceptionHandled = true;
+ }
+ return;
+ }
+ // if enumeration doesn't yield a handler the request is not acceptable
+ // CONSIDER: returning all formats considered in the exception messages
+ throw new HttpException((int)HttpStatusCode.NotAcceptable, "None of the formats specified by the accept header is supported.");
+ }
+ }
+
+ public virtual bool TryGetErrorResult(HttpException exception, ContentType responseFormat, out ResourceErrorActionResult actionResult)
+ {
+ if (FormatManager.Current.CanSerialize(responseFormat))
+ {
+ actionResult = new ResourceErrorActionResult(exception, responseFormat);
+ return true;
+ }
+ switch (responseFormat.MediaType)
+ {
+ case "application/octet-stream":
+ case "application/x-www-form-urlencoded":
+ case "text/html":
+ case "*/*":
+ actionResult = null;
+ return true;
+ default:
+ actionResult = null;
+ return false;
+ }
+ }
+
+ public virtual bool TryGetResult(ViewResultBase viewResult, ContentType responseFormat, out MultiFormatActionResult actionResult)
+ {
+ if (FormatManager.Current.CanSerialize(responseFormat))
+ {
+ if (viewResult.ViewData.Model == null)
+ {
+ throw new HttpException((int)this.StatusOnNullModel, this.StatusOnNullModel.ToString());
+ }
+ actionResult = new MultiFormatActionResult(viewResult.ViewData.Model, responseFormat);
+ return true;
+ }
+
+ switch (responseFormat.MediaType)
+ {
+ case "application/octet-stream":
+ case "application/x-www-form-urlencoded":
+ case "text/html":
+ case "*/*":
+ actionResult = null;
+ return true;
+ default:
+ actionResult = null;
+ return false;
+ }
+ }
+
+ internal static bool TryGetErrorResult2(RequestContext requestContext, HttpException he, out ResourceErrorActionResult actionResult)
+ {
+ foreach (ContentType responseFormat in requestContext.GetResponseFormats())
+ {
+ WebApiEnabledAttribute dummy = new WebApiEnabledAttribute();
+ if (dummy.TryGetErrorResult(he, responseFormat, out actionResult))
+ {
+ return true;
+ }
+ }
+ actionResult = null;
+ return false;
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/Resources/XmlFormatHandler.cs b/src/Microsoft.Web.Mvc/Resources/XmlFormatHandler.cs
new file mode 100644
index 00000000..ba779a4e
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/Resources/XmlFormatHandler.cs
@@ -0,0 +1,53 @@
+using System;
+using System.Net.Mime;
+using System.Runtime.Serialization;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc.Resources
+{
+ public class XmlFormatHandler : IRequestFormatHandler, IResponseFormatHandler
+ {
+ public string FriendlyName
+ {
+ get { return "Xml"; }
+ }
+
+ public bool CanDeserialize(ContentType requestFormat)
+ {
+ return requestFormat != null && IsCompatibleMediaType(requestFormat.MediaType);
+ }
+
+ public object Deserialize(ControllerContext controllerContext, ModelBindingContext bindingContext, ContentType requestFormat)
+ {
+ DataContractSerializer xml = new DataContractSerializer(bindingContext.ModelType, null, Int32.MaxValue, true, true, null);
+ return xml.ReadObject(controllerContext.HttpContext.Request.InputStream);
+ }
+
+ public bool CanSerialize(ContentType responseFormat)
+ {
+ return responseFormat != null && IsCompatibleMediaType(responseFormat.MediaType);
+ }
+
+ public void Serialize(ControllerContext context, object model, ContentType responseFormat)
+ {
+ DataContractXmlActionResult xml = new DataContractXmlActionResult(model, responseFormat);
+ xml.ExecuteResult(context);
+ }
+
+ protected virtual bool IsCompatibleMediaType(string mediaType)
+ {
+ return (mediaType == "text/xml" || mediaType == "application/xml");
+ }
+
+ public bool TryMapFormatFriendlyName(string friendlyName, out ContentType contentType)
+ {
+ if (String.Equals(friendlyName, this.FriendlyName, StringComparison.OrdinalIgnoreCase))
+ {
+ contentType = new ContentType("application/xml");
+ return true;
+ }
+ contentType = null;
+ return false;
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ScriptExtensions.cs b/src/Microsoft.Web.Mvc/ScriptExtensions.cs
new file mode 100644
index 00000000..fb233d9a
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ScriptExtensions.cs
@@ -0,0 +1,51 @@
+using System;
+using System.Web.Mvc;
+using Microsoft.Web.Mvc.Properties;
+
+namespace Microsoft.Web.Mvc
+{
+ public static class ScriptExtensions
+ {
+ public static MvcHtmlString Script(this HtmlHelper helper, string releaseFile)
+ {
+ return Script(helper, releaseFile, releaseFile);
+ }
+
+ public static MvcHtmlString Script(this HtmlHelper helper, string releaseFile, string debugFile)
+ {
+ if (String.IsNullOrEmpty(releaseFile))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "releaseFile");
+ }
+ if (String.IsNullOrEmpty(debugFile))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "debugFile");
+ }
+
+ string src;
+ string file = helper.ViewContext.HttpContext.IsDebuggingEnabled ? debugFile : releaseFile;
+ if (IsRelativeToDefaultPath(file))
+ {
+ src = "~/Scripts/" + file;
+ }
+ else
+ {
+ src = file;
+ }
+
+ TagBuilder scriptTag = new TagBuilder("script");
+ scriptTag.MergeAttribute("type", "text/javascript");
+ scriptTag.MergeAttribute("src", UrlHelper.GenerateContentUrl(src, helper.ViewContext.HttpContext));
+ return MvcHtmlString.Create(scriptTag.ToString(TagRenderMode.Normal));
+ }
+
+ internal static bool IsRelativeToDefaultPath(string file)
+ {
+ return !(file.StartsWith("~", StringComparison.Ordinal) ||
+ file.StartsWith("../", StringComparison.Ordinal) ||
+ file.StartsWith("/", StringComparison.Ordinal) ||
+ file.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
+ file.StartsWith("https://", StringComparison.OrdinalIgnoreCase));
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/SerializationExtensions.cs b/src/Microsoft.Web.Mvc/SerializationExtensions.cs
new file mode 100644
index 00000000..71b95779
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/SerializationExtensions.cs
@@ -0,0 +1,71 @@
+using System;
+using System.Web.Mvc;
+using Microsoft.Web.Mvc.Properties;
+
+namespace Microsoft.Web.Mvc
+{
+ public static class SerializationExtensions
+ {
+ public static MvcHtmlString Serialize(this HtmlHelper htmlHelper, string name)
+ {
+ return Serialize(htmlHelper, name, MvcSerializer.DefaultSerializationMode);
+ }
+
+ public static MvcHtmlString Serialize(this HtmlHelper htmlHelper, string name, SerializationMode mode)
+ {
+ return SerializeInternal(htmlHelper, name, null, mode, true /* useViewData */);
+ }
+
+ internal static MvcHtmlString Serialize(this HtmlHelper htmlHelper, string name, SerializationMode mode, MvcSerializer serializer)
+ {
+ return SerializeInternal(htmlHelper, name, null, mode, true /* useViewData */, serializer);
+ }
+
+ public static MvcHtmlString Serialize(this HtmlHelper htmlHelper, string name, object data)
+ {
+ return Serialize(htmlHelper, name, data, MvcSerializer.DefaultSerializationMode);
+ }
+
+ public static MvcHtmlString Serialize(this HtmlHelper htmlHelper, string name, object data, SerializationMode mode)
+ {
+ return SerializeInternal(htmlHelper, name, data, mode, false /* useViewData */);
+ }
+
+ internal static MvcHtmlString Serialize(this HtmlHelper htmlHelper, string name, object data, SerializationMode mode, MvcSerializer serializer)
+ {
+ return SerializeInternal(htmlHelper, name, data, mode, false /* useViewData */, serializer);
+ }
+
+ private static MvcHtmlString SerializeInternal(HtmlHelper htmlHelper, string name, object data, SerializationMode mode, bool useViewData)
+ {
+ return SerializeInternal(htmlHelper, name, data, mode, useViewData, null);
+ }
+
+ private static MvcHtmlString SerializeInternal(HtmlHelper htmlHelper, string name, object data, SerializationMode mode, bool useViewData, MvcSerializer serializer)
+ {
+ if (htmlHelper == null)
+ {
+ throw new ArgumentNullException("htmlHelper");
+ }
+
+ if (String.IsNullOrEmpty(name))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "name");
+ }
+
+ name = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);
+ if (useViewData)
+ {
+ data = htmlHelper.ViewData.Eval(name);
+ }
+
+ string serializedData = (serializer ?? new MvcSerializer()).Serialize(data, mode);
+
+ TagBuilder builder = new TagBuilder("input");
+ builder.Attributes["type"] = "hidden";
+ builder.Attributes["name"] = name;
+ builder.Attributes["value"] = serializedData;
+ return MvcHtmlString.Create(builder.ToString(TagRenderMode.SelfClosing));
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/SerializationMode.cs b/src/Microsoft.Web.Mvc/SerializationMode.cs
new file mode 100644
index 00000000..b8b20872
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/SerializationMode.cs
@@ -0,0 +1,8 @@
+namespace Microsoft.Web.Mvc
+{
+ public enum SerializationMode
+ {
+ Signed = 0,
+ EncryptedAndSigned = 1
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ServerVariablesValueProviderFactory.cs b/src/Microsoft.Web.Mvc/ServerVariablesValueProviderFactory.cs
new file mode 100644
index 00000000..bb1a5be2
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ServerVariablesValueProviderFactory.cs
@@ -0,0 +1,15 @@
+using System.Collections.Specialized;
+using System.Globalization;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc
+{
+ public class ServerVariablesValueProviderFactory : ValueProviderFactory
+ {
+ public override IValueProvider GetValueProvider(ControllerContext controllerContext)
+ {
+ NameValueCollection nvc = controllerContext.HttpContext.Request.ServerVariables;
+ return new NameValueCollectionValueProvider(nvc, CultureInfo.InvariantCulture);
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/SessionValueProviderFactory.cs b/src/Microsoft.Web.Mvc/SessionValueProviderFactory.cs
new file mode 100644
index 00000000..64242b5b
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/SessionValueProviderFactory.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Web;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc
+{
+ public class SessionValueProviderFactory : ValueProviderFactory
+ {
+ public override IValueProvider GetValueProvider(ControllerContext controllerContext)
+ {
+ HttpSessionStateBase session = controllerContext.HttpContext.Session;
+ if (session == null)
+ {
+ // session is disabled
+ return null;
+ }
+
+ Dictionary<string, object> backingStore = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
+ foreach (string key in session)
+ {
+ if (key != null)
+ {
+ backingStore[key] = session[key]; // copy to backing store
+ }
+ }
+
+ // use the invariant culture since Session contains serialized objects
+ return new DictionaryValueProvider<object>(backingStore, CultureInfo.InvariantCulture);
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/SkipBindingAttribute.cs b/src/Microsoft.Web.Mvc/SkipBindingAttribute.cs
new file mode 100644
index 00000000..384801da
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/SkipBindingAttribute.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc
+{
+ [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
+ public sealed class SkipBindingAttribute : CustomModelBinderAttribute
+ {
+ private static readonly NullBinder _nullBinder = new NullBinder();
+
+ public override IModelBinder GetBinder()
+ {
+ return _nullBinder;
+ }
+
+ private class NullBinder : IModelBinder
+ {
+ public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
+ {
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/TempDataValueProviderFactory.cs b/src/Microsoft.Web.Mvc/TempDataValueProviderFactory.cs
new file mode 100644
index 00000000..1e9dbb25
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/TempDataValueProviderFactory.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc
+{
+ public class TempDataValueProviderFactory : ValueProviderFactory
+ {
+ public override IValueProvider GetValueProvider(ControllerContext controllerContext)
+ {
+ TempDataDictionary tempData = controllerContext.Controller.TempData;
+ if (tempData.Count == 0)
+ {
+ // fast-track empty TempData
+ return null;
+ }
+
+ return new TempDataValueProvider(tempData);
+ }
+
+ /// <summary>
+ /// dummy struct that resembles Void but can be used in a generic context
+ /// </summary>
+ private struct TempDataVoid
+ {
+ }
+
+ private sealed class TempDataValueProvider : DictionaryValueProvider<TempDataVoid>
+ {
+ private readonly TempDataDictionary _tempData;
+
+ // use invariant culture since TempData should contain objects
+ public TempDataValueProvider(TempDataDictionary tempData)
+ : base(GetVoidDictionary(tempData), CultureInfo.InvariantCulture)
+ {
+ _tempData = tempData;
+ }
+
+ public override ValueProviderResult GetValue(string key)
+ {
+ object rawValue;
+
+ // TryGetValue will mark the entry for removal.
+ if (_tempData.TryGetValue(key, out rawValue))
+ {
+ string attemptedValue = Convert.ToString(rawValue, CultureInfo.InvariantCulture);
+ return new ValueProviderResult(rawValue, attemptedValue, CultureInfo.InvariantCulture);
+ }
+ else
+ {
+ // no value found
+ return null;
+ }
+ }
+
+ private static Dictionary<string, TempDataVoid> GetVoidDictionary(TempDataDictionary tempData)
+ {
+ // Create a special backing store that doesn't directly hold the values, since the DictionaryValueProvider
+ // enumerates over the backing store but enumerating over TempData marks everything for removal.
+ Dictionary<string, TempDataVoid> d = new Dictionary<string, TempDataVoid>(StringComparer.OrdinalIgnoreCase);
+
+ // Enumerating over TempDataDictionary.Keys doesn't mark them for removal.
+ foreach (string key in tempData.Keys)
+ {
+ d[key] = default(TempDataVoid);
+ }
+
+ return d;
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/TypeDescriptorHelper.cs b/src/Microsoft.Web.Mvc/TypeDescriptorHelper.cs
new file mode 100644
index 00000000..3927d54d
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/TypeDescriptorHelper.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.Mvc
+{
+ internal static class TypeDescriptorHelper
+ {
+ private static readonly MockMetadataProvider _mockMetadataProvider = new MockMetadataProvider();
+
+ public static ICustomTypeDescriptor Get(Type type)
+ {
+ return _mockMetadataProvider.GetTypeDescriptor(type);
+ }
+
+ // System.Web.Mvc.TypeDescriptorHelpers is internal, so this mock subclassed type provides
+ // access to it via the GetTypeDescriptor() virtual method.
+ private sealed class MockMetadataProvider : AssociatedMetadataProvider
+ {
+ protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
+ {
+ throw new NotImplementedException();
+ }
+
+ public new ICustomTypeDescriptor GetTypeDescriptor(Type type)
+ {
+ return base.GetTypeDescriptor(type);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/TypeHelpers.cs b/src/Microsoft.Web.Mvc/TypeHelpers.cs
new file mode 100644
index 00000000..1ad89493
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/TypeHelpers.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Linq;
+
+namespace Microsoft.Web.Mvc
+{
+ internal static class TypeHelpers
+ {
+ public static Type ExtractGenericInterface(Type queryType, Type interfaceType)
+ {
+ Func<Type, bool> matchesInterface = t => t.IsGenericType && t.GetGenericTypeDefinition() == interfaceType;
+ return (matchesInterface(queryType)) ? queryType : queryType.GetInterfaces().FirstOrDefault(matchesInterface);
+ }
+
+ public static Type[] GetTypeArgumentsIfMatch(Type closedType, Type matchingOpenType)
+ {
+ if (!closedType.IsGenericType)
+ {
+ return null;
+ }
+
+ Type openType = closedType.GetGenericTypeDefinition();
+ return (matchingOpenType == openType) ? closedType.GetGenericArguments() : null;
+ }
+
+ public static bool IsCompatibleObject(Type type, object value)
+ {
+ return ((value == null && TypeAllowsNullValue(type)) || type.IsInstanceOfType(value));
+ }
+
+ public static bool IsNullableValueType(Type type)
+ {
+ return Nullable.GetUnderlyingType(type) != null;
+ }
+
+ public static bool TypeAllowsNullValue(Type type)
+ {
+ return (!type.IsValueType || IsNullableValueType(type));
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/UrlAttribute.cs b/src/Microsoft.Web.Mvc/UrlAttribute.cs
new file mode 100644
index 00000000..f6148b38
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/UrlAttribute.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Text.RegularExpressions;
+using System.Web.Mvc;
+using Microsoft.Web.Mvc.Properties;
+
+namespace Microsoft.Web.Mvc
+{
+ [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
+ public sealed class UrlAttribute : DataTypeAttribute, IClientValidatable
+ {
+ private static Regex _regex = new Regex(@"^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture);
+
+ public UrlAttribute()
+ : base(DataType.Url)
+ {
+ ErrorMessage = MvcResources.UrlAttribute_Invalid;
+ }
+
+ public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
+ {
+ yield return new ModelClientValidationRule
+ {
+ ValidationType = "url",
+ ErrorMessage = FormatErrorMessage(metadata.GetDisplayName())
+ };
+ }
+
+ public override bool IsValid(object value)
+ {
+ if (value == null)
+ {
+ return true;
+ }
+
+ string valueAsString = value as string;
+ return valueAsString != null && _regex.Match(valueAsString).Length > 0;
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ValueProviderUtil.cs b/src/Microsoft.Web.Mvc/ValueProviderUtil.cs
new file mode 100644
index 00000000..64a087a0
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ValueProviderUtil.cs
@@ -0,0 +1,41 @@
+using System;
+
+namespace Microsoft.Web.Mvc
+{
+ internal static class ValueProviderUtil
+ {
+ public static bool IsPrefixMatch(string prefix, string testString)
+ {
+ if (testString == null)
+ {
+ return false;
+ }
+
+ if (prefix.Length == 0)
+ {
+ return true; // shortcut - non-null testString matches empty prefix
+ }
+
+ if (!testString.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+ {
+ return false; // prefix doesn't match
+ }
+
+ if (testString.Length == prefix.Length)
+ {
+ return true; // exact match
+ }
+
+ // invariant: testString.Length > prefix.Length
+ switch (testString[prefix.Length])
+ {
+ case '.':
+ case '[':
+ return true; // known delimiters
+
+ default:
+ return false; // not known delimiter
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.Mvc/ViewExtensions.cs b/src/Microsoft.Web.Mvc/ViewExtensions.cs
new file mode 100644
index 00000000..91e6be9b
--- /dev/null
+++ b/src/Microsoft.Web.Mvc/ViewExtensions.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq.Expressions;
+using System.Web.Mvc;
+using System.Web.Mvc.Html;
+using System.Web.Routing;
+using ExpressionHelper = Microsoft.Web.Mvc.Internal.ExpressionHelper;
+
+namespace Microsoft.Web.Mvc
+{
+ public static class ViewExtensions
+ {
+ public static void RenderRoute(this HtmlHelper helper, RouteValueDictionary routeValues)
+ {
+ if (routeValues == null)
+ {
+ throw new ArgumentNullException("routeValues");
+ }
+
+ string actionName = (string)routeValues["action"];
+ helper.RenderAction(actionName, routeValues);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static void RenderAction<TController>(this HtmlHelper helper, Expression<Action<TController>> action) where TController : Controller
+ {
+ RouteValueDictionary rvd = ExpressionHelper.GetRouteValuesFromExpression(action);
+
+ foreach (var entry in helper.ViewContext.RouteData.Values)
+ {
+ if (!rvd.ContainsKey(entry.Key))
+ {
+ rvd.Add(entry.Key, entry.Value);
+ }
+ }
+
+ RenderRoute(helper, rvd);
+ }
+ }
+}
diff --git a/src/Microsoft.Web.WebPages.OAuth/AuthenticationClientCollection.cs b/src/Microsoft.Web.WebPages.OAuth/AuthenticationClientCollection.cs
new file mode 100644
index 00000000..1c063af0
--- /dev/null
+++ b/src/Microsoft.Web.WebPages.OAuth/AuthenticationClientCollection.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Collections.ObjectModel;
+using DotNetOpenAuth.AspNet;
+
+namespace Microsoft.Web.WebPages.OAuth
+{
+ /// <summary>
+ /// A collection to store instances of IAuthenticationClient by keying off ProviderName.
+ /// </summary>
+ internal sealed class AuthenticationClientCollection : KeyedCollection<string, IAuthenticationClient>
+ {
+ public AuthenticationClientCollection()
+ : base(StringComparer.OrdinalIgnoreCase)
+ {
+ }
+
+ protected override string GetKeyForItem(IAuthenticationClient item)
+ {
+ return item.ProviderName;
+ }
+ }
+}
diff --git a/src/Microsoft.Web.WebPages.OAuth/BuiltInAuthenticationClient.cs b/src/Microsoft.Web.WebPages.OAuth/BuiltInAuthenticationClient.cs
new file mode 100644
index 00000000..199cc5ee
--- /dev/null
+++ b/src/Microsoft.Web.WebPages.OAuth/BuiltInAuthenticationClient.cs
@@ -0,0 +1,16 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace Microsoft.Web.WebPages.OAuth
+{
+ /// <summary>
+ /// Represents built in OAuth clients.
+ /// </summary>
+ [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "OAuth", Justification = "OAuth is a brand name.")]
+ public enum BuiltInOAuthClient
+ {
+ Twitter,
+ Facebook,
+ LinkedIn,
+ WindowsLive
+ }
+}
diff --git a/src/Microsoft.Web.WebPages.OAuth/BuiltInOpenIDClient.cs b/src/Microsoft.Web.WebPages.OAuth/BuiltInOpenIDClient.cs
new file mode 100644
index 00000000..1de81018
--- /dev/null
+++ b/src/Microsoft.Web.WebPages.OAuth/BuiltInOpenIDClient.cs
@@ -0,0 +1,11 @@
+namespace Microsoft.Web.WebPages.OAuth
+{
+ /// <summary>
+ /// Represents built in OpenID clients.
+ /// </summary>
+ public enum BuiltInOpenIDClient
+ {
+ Google,
+ Yahoo
+ }
+}
diff --git a/src/Microsoft.Web.WebPages.OAuth/Microsoft.Web.WebPages.OAuth.csproj b/src/Microsoft.Web.WebPages.OAuth/Microsoft.Web.WebPages.OAuth.csproj
new file mode 100644
index 00000000..e532058b
--- /dev/null
+++ b/src/Microsoft.Web.WebPages.OAuth/Microsoft.Web.WebPages.OAuth.csproj
@@ -0,0 +1,129 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProductVersion>8.0.30703</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{4CBFC7D3-1600-4CE5-BC6B-AC7BC2D6F853}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>Microsoft.Web.WebPages.OAuth</RootNamespace>
+ <AssemblyName>Microsoft.Web.WebPages.OAuth</AssemblyName>
+ <TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
+ <FileAlignment>512</FileAlignment>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>..\..\bin\Debug\</OutputPath>
+ <DefineConstants>TRACE;DEBUG;ASPNETWEBPAGES</DefineConstants>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ <DocumentationFile>..\..\bin\Debug\Microsoft.Web.WebPages.OAuth.xml</DocumentationFile>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>..\..\bin\Release\</OutputPath>
+ <DefineConstants>TRACE;ASPNETWEBPAGES</DefineConstants>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ <RunCodeAnalysis>$(CodeAnalysis)</RunCodeAnalysis>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'CodeCoverage|AnyCPU'">
+ <DebugSymbols>true</DebugSymbols>
+ <OutputPath>..\..\bin\CodeCoverage\</OutputPath>
+ <DefineConstants>TRACE;DEBUG;CODE_COVERAGE;ASPNETWEBPAGES</DefineConstants>
+ <DebugType>full</DebugType>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <ItemGroup>
+ <CodeAnalysisDictionary Include="..\CodeAnalysisDictionary.xml" />
+ </ItemGroup>
+ <ItemGroup>
+ <Reference Include="DotNetOpenAuth.AspNet, Version=4.0.0.12065, Culture=neutral, PublicKeyToken=2780ccd10d57b246, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>..\..\packages\DotNetOpenAuth.AspNet.4.0.0-beta2\lib\net40-full\DotNetOpenAuth.AspNet.dll</HintPath>
+ </Reference>
+ <Reference Include="DotNetOpenAuth.Core, Version=4.0.0.12065, Culture=neutral, PublicKeyToken=2780ccd10d57b246, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>..\..\packages\DotNetOpenAuth.Core.4.0.0-beta2\lib\net40-full\DotNetOpenAuth.Core.dll</HintPath>
+ </Reference>
+ <Reference Include="DotNetOpenAuth.OAuth, Version=4.0.0.12065, Culture=neutral, PublicKeyToken=2780ccd10d57b246, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>..\..\packages\DotNetOpenAuth.OAuth.Core.4.0.0-beta2\lib\net40-full\DotNetOpenAuth.OAuth.dll</HintPath>
+ </Reference>
+ <Reference Include="DotNetOpenAuth.OAuth.Consumer, Version=4.0.0.12065, Culture=neutral, PublicKeyToken=2780ccd10d57b246, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>..\..\packages\DotNetOpenAuth.OAuth.Consumer.4.0.0-beta2\lib\net40-full\DotNetOpenAuth.OAuth.Consumer.dll</HintPath>
+ </Reference>
+ <Reference Include="DotNetOpenAuth.OpenId, Version=4.0.0.12065, Culture=neutral, PublicKeyToken=2780ccd10d57b246, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>..\..\packages\DotNetOpenAuth.OpenId.Core.4.0.0-beta2\lib\net40-full\DotNetOpenAuth.OpenId.dll</HintPath>
+ </Reference>
+ <Reference Include="DotNetOpenAuth.OpenId.RelyingParty, Version=4.0.0.12065, Culture=neutral, PublicKeyToken=2780ccd10d57b246, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>..\..\packages\DotNetOpenAuth.OpenId.RelyingParty.4.0.0-beta2\lib\net40-full\DotNetOpenAuth.OpenId.RelyingParty.dll</HintPath>
+ </Reference>
+ <Reference Include="System" />
+ <Reference Include="System.Configuration" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.Web" />
+ <Reference Include="System.Web.ApplicationServices" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="..\CommonAssemblyInfo.cs">
+ <Link>Properties\CommonAssemblyInfo.cs</Link>
+ </Compile>
+ <Compile Include="..\GlobalSuppressions.cs">
+ <Link>Common\GlobalSuppressions.cs</Link>
+ </Compile>
+ <Compile Include="AuthenticationClientCollection.cs" />
+ <Compile Include="BuiltInAuthenticationClient.cs" />
+ <Compile Include="BuiltInOpenIDClient.cs" />
+ <Compile Include="OAuthAccount.cs" />
+ <Compile Include="OAuthWebSecurity.cs" />
+ <Compile Include="PreApplicationStartCode.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="Properties\WebResources.Designer.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>WebResources.resx</DependentUpon>
+ </Compile>
+ <Compile Include="WebPagesOAuthDataProvider.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\System.Web.Razor\System.Web.Razor.csproj">
+ <Project>{8F18041B-9410-4C36-A9C5-067813DF5F31}</Project>
+ <Name>System.Web.Razor</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\System.Web.WebPages.Razor\System.Web.WebPages.Razor.csproj">
+ <Project>{0939B11A-FE4E-4BA1-8AD6-D97741EE314F}</Project>
+ <Name>System.Web.WebPages.Razor</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\WebMatrix.WebData\WebMatrix.WebData.csproj">
+ <Project>{55A15F40-1435-4248-A7F2-2A146BB83586}</Project>
+ <Name>WebMatrix.WebData</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="Properties\WebResources.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>WebResources.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="packages.config">
+ <SubType>Designer</SubType>
+ </None>
+ </ItemGroup>
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+ <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
+ Other similar extension points exist, see Microsoft.Common.targets.
+ <Target Name="BeforeBuild">
+ </Target>
+ <Target Name="AfterBuild">
+ </Target>
+ -->
+</Project> \ No newline at end of file
diff --git a/src/Microsoft.Web.WebPages.OAuth/OAuthAccount.cs b/src/Microsoft.Web.WebPages.OAuth/OAuthAccount.cs
new file mode 100644
index 00000000..095e3602
--- /dev/null
+++ b/src/Microsoft.Web.WebPages.OAuth/OAuthAccount.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Globalization;
+using Microsoft.Web.WebPages.OAuth.Properties;
+
+namespace Microsoft.Web.WebPages.OAuth
+{
+ /// <summary>
+ /// Represents an OAuth & OpenID account.
+ /// </summary>
+ public class OAuthAccount
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OAuthAccountData"/> class.
+ /// </summary>
+ /// <param name="provider">The provider.</param>
+ /// <param name="providerUserId">The provider user id.</param>
+ public OAuthAccount(string provider, string providerUserId)
+ {
+ if (String.IsNullOrEmpty(provider))
+ {
+ throw new ArgumentException(
+ String.Format(CultureInfo.CurrentCulture, WebResources.Argument_Cannot_Be_Null_Or_Empty, "provider"),
+ "provider");
+ }
+
+ if (string.IsNullOrEmpty(providerUserId))
+ {
+ throw new ArgumentException(
+ String.Format(CultureInfo.CurrentCulture, WebResources.Argument_Cannot_Be_Null_Or_Empty, "providerUserId"),
+ "providerUserId");
+ }
+
+ Provider = provider;
+ ProviderUserId = providerUserId;
+ }
+
+ /// <summary>
+ /// Gets the provider name.
+ /// </summary>
+ public string Provider { get; private set; }
+
+ /// <summary>
+ /// Gets the provider user id.
+ /// </summary>
+ public string ProviderUserId { get; private set; }
+ }
+}
diff --git a/src/Microsoft.Web.WebPages.OAuth/OAuthWebSecurity.cs b/src/Microsoft.Web.WebPages.OAuth/OAuthWebSecurity.cs
new file mode 100644
index 00000000..4bcbc597
--- /dev/null
+++ b/src/Microsoft.Web.WebPages.OAuth/OAuthWebSecurity.cs
@@ -0,0 +1,311 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using System.Web;
+using System.Web.Security;
+using DotNetOpenAuth.AspNet;
+using DotNetOpenAuth.AspNet.Clients;
+using Microsoft.Web.WebPages.OAuth.Properties;
+using WebMatrix.WebData;
+
+namespace Microsoft.Web.WebPages.OAuth
+{
+ /// <summary>
+ /// Contains APIs to manage authentication against OAuth & OpenID service providers
+ /// </summary>
+ public static class OAuthWebSecurity
+ {
+ internal static IOpenAuthDataProvider OAuthDataProvider = new WebPagesOAuthDataProvider();
+
+ // contains all registered authentication clients
+ private static readonly AuthenticationClientCollection _authenticationClients = new AuthenticationClientCollection();
+
+ /// <summary>
+ /// Gets a value indicating whether the current user is authenticated by an OAuth provider.
+ /// </summary>
+ public static bool IsAuthenticatedWithOAuth
+ {
+ get
+ {
+ if (HttpContext.Current == null)
+ {
+ throw new InvalidOperationException(WebResources.HttpContextNotAvailable);
+ }
+
+ return GetIsAuthenticatedWithOAuthCore(new HttpContextWrapper(HttpContext.Current));
+ }
+ }
+
+ /// <summary>
+ /// Registers a supported OAuth client with the specified consumer key and consumer secret.
+ /// </summary>
+ /// <param name="client">One of the supported OAuth clients.</param>
+ /// <param name="consumerKey">The consumer key.</param>
+ /// <param name="consumerSecret">The consumer secret.</param>
+ public static void RegisterOAuthClient(BuiltInOAuthClient client, string consumerKey, string consumerSecret)
+ {
+ IAuthenticationClient authenticationClient;
+ switch (client)
+ {
+ case BuiltInOAuthClient.LinkedIn:
+ authenticationClient = new LinkedInClient(consumerKey, consumerSecret);
+ break;
+
+ case BuiltInOAuthClient.Twitter:
+ authenticationClient = new TwitterClient(consumerKey, consumerSecret);
+ break;
+
+ case BuiltInOAuthClient.Facebook:
+ authenticationClient = new FacebookClient(consumerKey, consumerSecret);
+ break;
+
+ case BuiltInOAuthClient.WindowsLive:
+ authenticationClient = new WindowsLiveClient(consumerKey, consumerSecret);
+ break;
+
+ default:
+ throw new ArgumentOutOfRangeException("client");
+ }
+ RegisterClient(authenticationClient);
+ }
+
+ /// <summary>
+ /// Registers a supported OpenID client
+ /// </summary>
+ public static void RegisterOpenIDClient(BuiltInOpenIDClient openIDClient)
+ {
+ IAuthenticationClient client;
+ switch (openIDClient)
+ {
+ case BuiltInOpenIDClient.Google:
+ client = new GoogleOpenIdClient();
+ break;
+
+ case BuiltInOpenIDClient.Yahoo:
+ client = new YahooOpenIdClient();
+ break;
+
+ default:
+ throw new ArgumentOutOfRangeException("openIDClient");
+ }
+
+ RegisterClient(client);
+ }
+
+ /// <summary>
+ /// Registers an authentication client.
+ /// </summary>
+ public static void RegisterClient(IAuthenticationClient client)
+ {
+ if (client == null)
+ {
+ throw new ArgumentNullException("client");
+ }
+
+ if (String.IsNullOrEmpty(client.ProviderName))
+ {
+ throw new ArgumentException(WebResources.InvalidServiceProviderName, "client");
+ }
+
+ if (_authenticationClients.Contains(client))
+ {
+ throw new ArgumentException(WebResources.ServiceProviderNameExists, "client");
+ }
+
+ _authenticationClients.Add(client);
+ }
+
+ /// <summary>
+ /// Requests the specified provider to start the authentication by directing users to an external website
+ /// </summary>
+ /// <param name="provider">The provider.</param>
+ public static void RequestAuthentication(string provider)
+ {
+ RequestAuthentication(provider, returnUrl: null);
+ }
+
+ /// <summary>
+ /// Requests the specified provider to start the authentication by directing users to an external website
+ /// </summary>
+ /// <param name="provider">The provider.</param>
+ /// <param name="returnUrl">The return url after user is authenticated.</param>
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "We want to allow relative app path, and support ~/")]
+ public static void RequestAuthentication(string provider, string returnUrl)
+ {
+ if (HttpContext.Current == null)
+ {
+ throw new InvalidOperationException(WebResources.HttpContextNotAvailable);
+ }
+
+ RequestAuthenticationCore(new HttpContextWrapper(HttpContext.Current), provider, returnUrl);
+ }
+
+ internal static void RequestAuthenticationCore(HttpContextBase context, string provider, string returnUrl)
+ {
+ IAuthenticationClient client = GetOAuthClient(provider);
+ var securityManager = new OpenAuthSecurityManager(context, client, OAuthDataProvider);
+ securityManager.RequestAuthentication(returnUrl);
+ }
+
+ /// <summary>
+ /// Checks if user is successfully authenticated when user is redirected back to this user.
+ /// </summary>
+ public static AuthenticationResult VerifyAuthentication()
+ {
+ if (HttpContext.Current == null)
+ {
+ throw new InvalidOperationException(WebResources.HttpContextNotAvailable);
+ }
+
+ return VerifyAuthenticationCore(new HttpContextWrapper(HttpContext.Current));
+ }
+
+ internal static AuthenticationResult VerifyAuthenticationCore(HttpContextBase context)
+ {
+ string providerName = OpenAuthSecurityManager.GetProviderName(context);
+ if (String.IsNullOrEmpty(providerName))
+ {
+ return AuthenticationResult.Failed;
+ }
+
+ IAuthenticationClient client;
+ if (TryGetOAuthClient(providerName, out client))
+ {
+ var securityManager = new OpenAuthSecurityManager(context, client, OAuthDataProvider);
+ return securityManager.VerifyAuthentication();
+ }
+ else
+ {
+ throw new InvalidOperationException(WebResources.InvalidServiceProviderName);
+ }
+ }
+
+ /// <summary>
+ /// Checks if the specified provider user id represents a valid account.
+ /// If it does, log user in.
+ /// </summary>
+ /// <param name="providerName">Name of the provider.</param>
+ /// <param name="providerUserId">The provider user id.</param>
+ /// <returns><c>true</c> if the login is successful.</returns>
+ [SuppressMessage("Microsoft.Naming", "CA1726:UsePreferredTerms", MessageId = "Login", Justification = "Login is used more consistently in ASP.Net")]
+ public static bool Login(string providerName, string providerUserId, bool createPersistentCookie)
+ {
+ if (HttpContext.Current == null)
+ {
+ throw new InvalidOperationException(WebResources.HttpContextNotAvailable);
+ }
+
+ return LoginCore(new HttpContextWrapper(HttpContext.Current), providerName, providerUserId, createPersistentCookie);
+ }
+
+ internal static bool LoginCore(HttpContextBase context, string providerName, string providerUserId, bool createPersistentCookie)
+ {
+ var provider = GetOAuthClient(providerName);
+ var securityManager = new OpenAuthSecurityManager(context, provider, OAuthDataProvider);
+ return securityManager.Login(providerUserId, createPersistentCookie);
+ }
+
+ internal static bool GetIsAuthenticatedWithOAuthCore(HttpContextBase context)
+ {
+ return new OpenAuthSecurityManager(context).IsAuthenticatedWithOpenAuth;
+ }
+
+ /// <summary>
+ /// Creates or update the account with the specified provider, provider user id and associate it with the specified user name.
+ /// </summary>
+ /// <param name="providerName">Name of the provider.</param>
+ /// <param name="providerUserId">The provider user id.</param>
+ /// <param name="userName">The user name.</param>
+ public static void CreateOrUpdateAccount(string providerName, string providerUserId, string userName)
+ {
+ ExtendedMembershipProvider provider = VerifyProvider();
+ provider.CreateOrUpdateOAuthAccount(providerName, providerUserId, userName);
+ }
+
+ public static string GetUserName(string providerName, string providerUserId)
+ {
+ return OAuthDataProvider.GetUserNameFromOpenAuth(providerName, providerUserId);
+ }
+
+ /// <summary>
+ /// Gets all OAuth & OpenID accounts which are associted with the specified user name.
+ /// </summary>
+ /// <param name="userName">The user name.</param>
+ public static ICollection<OAuthAccount> GetAccountsFromUserName(string userName)
+ {
+ if (String.IsNullOrEmpty(userName))
+ {
+ throw new ArgumentException(
+ String.Format(CultureInfo.CurrentCulture, WebResources.Argument_Cannot_Be_Null_Or_Empty, "userName"),
+ "userName");
+ }
+
+ ExtendedMembershipProvider provider = VerifyProvider();
+ return provider.GetAccountsForUser(userName).Select(p => new OAuthAccount(p.Provider, p.ProviderUserId)).ToList();
+ }
+
+ /// <summary>
+ /// Delete the specified OAuth & OpenID account
+ /// </summary>
+ /// <param name="providerName">Name of the provider.</param>
+ /// <param name="providerUserId">The provider user id.</param>
+ public static bool DeleteAccount(string providerName, string providerUserId)
+ {
+ ExtendedMembershipProvider provider = VerifyProvider();
+
+ string username = GetUserName(providerName, providerUserId);
+ if (String.IsNullOrEmpty(username))
+ {
+ // account doesn't exist
+ return false;
+ }
+
+ provider.DeleteOAuthAccount(providerName, providerName);
+ return true;
+ }
+
+ internal static IAuthenticationClient GetOAuthClient(string providerName)
+ {
+ if (!_authenticationClients.Contains(providerName))
+ {
+ throw new ArgumentException(WebResources.ServiceProviderNotFound, "providerName");
+ }
+
+ return _authenticationClients[providerName];
+ }
+
+ internal static bool TryGetOAuthClient(string provider, out IAuthenticationClient client)
+ {
+ if (_authenticationClients.Contains(provider))
+ {
+ client = _authenticationClients[provider];
+ return true;
+ }
+ else
+ {
+ client = null;
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// for unit tests
+ /// </summary>
+ internal static void ClearProviders()
+ {
+ _authenticationClients.Clear();
+ }
+
+ private static ExtendedMembershipProvider VerifyProvider()
+ {
+ var provider = Membership.Provider as ExtendedMembershipProvider;
+ if (provider == null)
+ {
+ throw new InvalidOperationException();
+ }
+ return provider;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Microsoft.Web.WebPages.OAuth/PreApplicationStartCode.cs b/src/Microsoft.Web.WebPages.OAuth/PreApplicationStartCode.cs
new file mode 100644
index 00000000..0a373318
--- /dev/null
+++ b/src/Microsoft.Web.WebPages.OAuth/PreApplicationStartCode.cs
@@ -0,0 +1,15 @@
+using System.ComponentModel;
+using System.Web.WebPages.Razor;
+
+namespace Microsoft.Web.WebPages.OAuth
+{
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static class PreApplicationStartCode
+ {
+ public static void Start()
+ {
+ WebPageRazorHost.AddGlobalImport("DotNetOpenAuth.AspNet");
+ WebPageRazorHost.AddGlobalImport("Microsoft.Web.WebPages.DotNetOpenAuth");
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Microsoft.Web.WebPages.OAuth/Properties/AssemblyInfo.cs b/src/Microsoft.Web.WebPages.OAuth/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..fa69a154
--- /dev/null
+++ b/src/Microsoft.Web.WebPages.OAuth/Properties/AssemblyInfo.cs
@@ -0,0 +1,9 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Web;
+
+[assembly: AssemblyTitle("Microsoft.Web.WebPages.OAuth")]
+[assembly: AssemblyDescription("")]
+
+[assembly: InternalsVisibleTo("Microsoft.Web.WebPages.OAuth.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
+[assembly: PreApplicationStartMethod(typeof(Microsoft.Web.WebPages.OAuth.PreApplicationStartCode), "Start")]
diff --git a/src/Microsoft.Web.WebPages.OAuth/Properties/WebResources.Designer.cs b/src/Microsoft.Web.WebPages.OAuth/Properties/WebResources.Designer.cs
new file mode 100644
index 00000000..6ffda7d3
--- /dev/null
+++ b/src/Microsoft.Web.WebPages.OAuth/Properties/WebResources.Designer.cs
@@ -0,0 +1,108 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.488
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Microsoft.Web.WebPages.OAuth.Properties {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class WebResources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal WebResources() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.Web.WebPages.OAuth.Properties.WebResources", typeof(WebResources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to {0} cannot be null or an empty string..
+ /// </summary>
+ internal static string Argument_Cannot_Be_Null_Or_Empty {
+ get {
+ return ResourceManager.GetString("Argument_Cannot_Be_Null_Or_Empty", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to HttpContext is not available in the current thread..
+ /// </summary>
+ internal static string HttpContextNotAvailable {
+ get {
+ return ResourceManager.GetString("HttpContextNotAvailable", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Invalid provider name..
+ /// </summary>
+ internal static string InvalidServiceProviderName {
+ get {
+ return ResourceManager.GetString("InvalidServiceProviderName", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Another service provider with the same name has already been registered..
+ /// </summary>
+ internal static string ServiceProviderNameExists {
+ get {
+ return ResourceManager.GetString("ServiceProviderNameExists", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A service provider could not be found by the specified name..
+ /// </summary>
+ internal static string ServiceProviderNotFound {
+ get {
+ return ResourceManager.GetString("ServiceProviderNotFound", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Web.WebPages.OAuth/Properties/WebResources.resx b/src/Microsoft.Web.WebPages.OAuth/Properties/WebResources.resx
new file mode 100644
index 00000000..28345787
--- /dev/null
+++ b/src/Microsoft.Web.WebPages.OAuth/Properties/WebResources.resx
@@ -0,0 +1,135 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="Argument_Cannot_Be_Null_Or_Empty" xml:space="preserve">
+ <value>{0} cannot be null or an empty string.</value>
+ </data>
+ <data name="HttpContextNotAvailable" xml:space="preserve">
+ <value>HttpContext is not available in the current thread.</value>
+ </data>
+ <data name="InvalidServiceProviderName" xml:space="preserve">
+ <value>Invalid provider name.</value>
+ </data>
+ <data name="ServiceProviderNameExists" xml:space="preserve">
+ <value>Another service provider with the same name has already been registered.</value>
+ </data>
+ <data name="ServiceProviderNotFound" xml:space="preserve">
+ <value>A service provider could not be found by the specified name.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/Microsoft.Web.WebPages.OAuth/WebPagesOAuthDataProvider.cs b/src/Microsoft.Web.WebPages.OAuth/WebPagesOAuthDataProvider.cs
new file mode 100644
index 00000000..720ea189
--- /dev/null
+++ b/src/Microsoft.Web.WebPages.OAuth/WebPagesOAuthDataProvider.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Web.Security;
+using DotNetOpenAuth.AspNet;
+using WebMatrix.WebData;
+
+namespace Microsoft.Web.WebPages.OAuth
+{
+ internal class WebPagesOAuthDataProvider : IOpenAuthDataProvider
+ {
+ private static ExtendedMembershipProvider VerifyProvider()
+ {
+ var provider = Membership.Provider as ExtendedMembershipProvider;
+ if (provider == null)
+ {
+ throw new InvalidOperationException();
+ }
+ return provider;
+ }
+
+ public string GetUserNameFromOpenAuth(string openAuthProvider, string openAuthId)
+ {
+ ExtendedMembershipProvider provider = VerifyProvider();
+
+ int userId = provider.GetUserIdFromOAuth(openAuthProvider, openAuthId);
+ if (userId == -1)
+ {
+ return null;
+ }
+
+ return provider.GetUserNameFromId(userId);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Microsoft.Web.WebPages.OAuth/packages.config b/src/Microsoft.Web.WebPages.OAuth/packages.config
new file mode 100644
index 00000000..f88c00d6
--- /dev/null
+++ b/src/Microsoft.Web.WebPages.OAuth/packages.config
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="DotNetOpenAuth.AspNet" version="4.0.0-beta2" />
+ <package id="DotNetOpenAuth.Core" version="4.0.0-beta2" />
+ <package id="DotNetOpenAuth.OAuth.Consumer" version="4.0.0-beta2" />
+ <package id="DotNetOpenAuth.OAuth.Core" version="4.0.0-beta2" />
+ <package id="DotNetOpenAuth.OpenId.Core" version="4.0.0-beta2" />
+ <package id="DotNetOpenAuth.OpenId.RelyingParty" version="4.0.0-beta2" />
+</packages> \ No newline at end of file
diff --git a/src/MimeMapping.cs b/src/MimeMapping.cs
new file mode 100644
index 00000000..cbaf85f7
--- /dev/null
+++ b/src/MimeMapping.cs
@@ -0,0 +1,378 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Microsoft.Internal.Web.Utils
+{
+ internal static class MimeMapping
+ {
+ private static readonly IDictionary<string, string> _mimeMappings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
+ {
+ { ".*", "application/octet-stream" },
+ { ".323", "text/h323" },
+ { ".aaf", "application/octet-stream" },
+ { ".aca", "application/octet-stream" },
+ { ".accdb", "application/msaccess" },
+ { ".accde", "application/msaccess" },
+ { ".accdt", "application/msaccess" },
+ { ".acx", "application/internet-property-stream" },
+ { ".afm", "application/octet-stream" },
+ { ".ai", "application/postscript" },
+ { ".aif", "audio/x-aiff" },
+ { ".aifc", "audio/aiff" },
+ { ".aiff", "audio/aiff" },
+ { ".application", "application/x-ms-application" },
+ { ".art", "image/x-jg" },
+ { ".asd", "application/octet-stream" },
+ { ".asf", "video/x-ms-asf" },
+ { ".asi", "application/octet-stream" },
+ { ".asm", "text/plain" },
+ { ".asr", "video/x-ms-asf" },
+ { ".asx", "video/x-ms-asf" },
+ { ".atom", "application/atom+xml" },
+ { ".au", "audio/basic" },
+ { ".avi", "video/x-msvideo" },
+ { ".axs", "application/olescript" },
+ { ".bas", "text/plain" },
+ { ".bcpio", "application/x-bcpio" },
+ { ".bin", "application/octet-stream" },
+ { ".bmp", "image/bmp" },
+ { ".c", "text/plain" },
+ { ".cab", "application/octet-stream" },
+ { ".calx", "application/vnd.ms-office.calx" },
+ { ".cat", "application/vnd.ms-pki.seccat" },
+ { ".cdf", "application/x-cdf" },
+ { ".chm", "application/octet-stream" },
+ { ".class", "application/x-java-applet" },
+ { ".clp", "application/x-msclip" },
+ { ".cmx", "image/x-cmx" },
+ { ".cnf", "text/plain" },
+ { ".cod", "image/cis-cod" },
+ { ".cpio", "application/x-cpio" },
+ { ".cpp", "text/plain" },
+ { ".crd", "application/x-mscardfile" },
+ { ".crl", "application/pkix-crl" },
+ { ".crt", "application/x-x509-ca-cert" },
+ { ".csh", "application/x-csh" },
+ { ".css", "text/css" },
+ { ".csv", "application/octet-stream" },
+ { ".cur", "application/octet-stream" },
+ { ".dcr", "application/x-director" },
+ { ".deploy", "application/octet-stream" },
+ { ".der", "application/x-x509-ca-cert" },
+ { ".dib", "image/bmp" },
+ { ".dir", "application/x-director" },
+ { ".disco", "text/xml" },
+ { ".dll", "application/x-msdownload" },
+ { ".dll.config", "text/xml" },
+ { ".dlm", "text/dlm" },
+ { ".doc", "application/msword" },
+ { ".docm", "application/vnd.ms-word.document.macroEnabled.12" },
+ { ".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document" },
+ { ".dot", "application/msword" },
+ { ".dotm", "application/vnd.ms-word.template.macroEnabled.12" },
+ { ".dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template" },
+ { ".dsp", "application/octet-stream" },
+ { ".dtd", "text/xml" },
+ { ".dvi", "application/x-dvi" },
+ { ".dwf", "drawing/x-dwf" },
+ { ".dwp", "application/octet-stream" },
+ { ".dxr", "application/x-director" },
+ { ".eml", "message/rfc822" },
+ { ".emz", "application/octet-stream" },
+ { ".eot", "application/octet-stream" },
+ { ".eps", "application/postscript" },
+ { ".etx", "text/x-setext" },
+ { ".evy", "application/envoy" },
+ { ".exe", "application/octet-stream" },
+ { ".exe.config", "text/xml" },
+ { ".fdf", "application/vnd.fdf" },
+ { ".fif", "application/fractals" },
+ { ".fla", "application/octet-stream" },
+ { ".flr", "x-world/x-vrml" },
+ { ".flv", "video/x-flv" },
+ { ".gif", "image/gif" },
+ { ".gtar", "application/x-gtar" },
+ { ".gz", "application/x-gzip" },
+ { ".h", "text/plain" },
+ { ".hdf", "application/x-hdf" },
+ { ".hdml", "text/x-hdml" },
+ { ".hhc", "application/x-oleobject" },
+ { ".hhk", "application/octet-stream" },
+ { ".hhp", "application/octet-stream" },
+ { ".hlp", "application/winhlp" },
+ { ".hqx", "application/mac-binhex40" },
+ { ".hta", "application/hta" },
+ { ".htc", "text/x-component" },
+ { ".htm", "text/html" },
+ { ".html", "text/html" },
+ { ".htt", "text/webviewhtml" },
+ { ".hxt", "text/html" },
+ { ".ico", "image/x-icon" },
+ { ".ics", "application/octet-stream" },
+ { ".ief", "image/ief" },
+ { ".iii", "application/x-iphone" },
+ { ".inf", "application/octet-stream" },
+ { ".ins", "application/x-internet-signup" },
+ { ".isp", "application/x-internet-signup" },
+ { ".IVF", "video/x-ivf" },
+ { ".jar", "application/java-archive" },
+ { ".java", "application/octet-stream" },
+ { ".jck", "application/liquidmotion" },
+ { ".jcz", "application/liquidmotion" },
+ { ".jfif", "image/pjpeg" },
+ { ".jpb", "application/octet-stream" },
+ { ".jpe", "image/jpeg" },
+ { ".jpeg", "image/jpeg" },
+ { ".jpg", "image/jpeg" },
+ { ".js", "application/x-javascript" },
+ { ".jsx", "text/jscript" },
+ { ".latex", "application/x-latex" },
+ { ".lit", "application/x-ms-reader" },
+ { ".lpk", "application/octet-stream" },
+ { ".lsf", "video/x-la-asf" },
+ { ".lsx", "video/x-la-asf" },
+ { ".lzh", "application/octet-stream" },
+ { ".m13", "application/x-msmediaview" },
+ { ".m14", "application/x-msmediaview" },
+ { ".m1v", "video/mpeg" },
+ { ".m3u", "audio/x-mpegurl" },
+ { ".man", "application/x-troff-man" },
+ { ".manifest", "application/x-ms-manifest" },
+ { ".map", "text/plain" },
+ { ".mdb", "application/x-msaccess" },
+ { ".mdp", "application/octet-stream" },
+ { ".me", "application/x-troff-me" },
+ { ".mht", "message/rfc822" },
+ { ".mhtml", "message/rfc822" },
+ { ".mid", "audio/mid" },
+ { ".midi", "audio/mid" },
+ { ".mix", "application/octet-stream" },
+ { ".mmf", "application/x-smaf" },
+ { ".mno", "text/xml" },
+ { ".mny", "application/x-msmoney" },
+ { ".mov", "video/quicktime" },
+ { ".movie", "video/x-sgi-movie" },
+ { ".mp2", "video/mpeg" },
+ { ".mp3", "audio/mpeg" },
+ { ".mpa", "video/mpeg" },
+ { ".mpe", "video/mpeg" },
+ { ".mpeg", "video/mpeg" },
+ { ".mpg", "video/mpeg" },
+ { ".mpp", "application/vnd.ms-project" },
+ { ".mpv2", "video/mpeg" },
+ { ".ms", "application/x-troff-ms" },
+ { ".msi", "application/octet-stream" },
+ { ".mso", "application/octet-stream" },
+ { ".mvb", "application/x-msmediaview" },
+ { ".mvc", "application/x-miva-compiled" },
+ { ".nc", "application/x-netcdf" },
+ { ".nsc", "video/x-ms-asf" },
+ { ".nws", "message/rfc822" },
+ { ".ocx", "application/octet-stream" },
+ { ".oda", "application/oda" },
+ { ".odc", "text/x-ms-odc" },
+ { ".ods", "application/oleobject" },
+ { ".one", "application/onenote" },
+ { ".onea", "application/onenote" },
+ { ".onetoc", "application/onenote" },
+ { ".onetoc2", "application/onenote" },
+ { ".onetmp", "application/onenote" },
+ { ".onepkg", "application/onenote" },
+ { ".osdx", "application/opensearchdescription+xml" },
+ { ".p10", "application/pkcs10" },
+ { ".p12", "application/x-pkcs12" },
+ { ".p7b", "application/x-pkcs7-certificates" },
+ { ".p7c", "application/pkcs7-mime" },
+ { ".p7m", "application/pkcs7-mime" },
+ { ".p7r", "application/x-pkcs7-certreqresp" },
+ { ".p7s", "application/pkcs7-signature" },
+ { ".pbm", "image/x-portable-bitmap" },
+ { ".pcx", "application/octet-stream" },
+ { ".pcz", "application/octet-stream" },
+ { ".pdf", "application/pdf" },
+ { ".pfb", "application/octet-stream" },
+ { ".pfm", "application/octet-stream" },
+ { ".pfx", "application/x-pkcs12" },
+ { ".pgm", "image/x-portable-graymap" },
+ { ".pko", "application/vnd.ms-pki.pko" },
+ { ".pma", "application/x-perfmon" },
+ { ".pmc", "application/x-perfmon" },
+ { ".pml", "application/x-perfmon" },
+ { ".pmr", "application/x-perfmon" },
+ { ".pmw", "application/x-perfmon" },
+ { ".png", "image/png" },
+ { ".pnm", "image/x-portable-anymap" },
+ { ".pnz", "image/png" },
+ { ".pot", "application/vnd.ms-powerpoint" },
+ { ".potm", "application/vnd.ms-powerpoint.template.macroEnabled.12" },
+ { ".potx", "application/vnd.openxmlformats-officedocument.presentationml.template" },
+ { ".ppam", "application/vnd.ms-powerpoint.addin.macroEnabled.12" },
+ { ".ppm", "image/x-portable-pixmap" },
+ { ".pps", "application/vnd.ms-powerpoint" },
+ { ".ppsm", "application/vnd.ms-powerpoint.slideshow.macroEnabled.12" },
+ { ".ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow" },
+ { ".ppt", "application/vnd.ms-powerpoint" },
+ { ".pptm", "application/vnd.ms-powerpoint.presentation.macroEnabled.12" },
+ { ".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation" },
+ { ".prf", "application/pics-rules" },
+ { ".prm", "application/octet-stream" },
+ { ".prx", "application/octet-stream" },
+ { ".ps", "application/postscript" },
+ { ".psd", "application/octet-stream" },
+ { ".psm", "application/octet-stream" },
+ { ".psp", "application/octet-stream" },
+ { ".pub", "application/x-mspublisher" },
+ { ".qt", "video/quicktime" },
+ { ".qtl", "application/x-quicktimeplayer" },
+ { ".qxd", "application/octet-stream" },
+ { ".ra", "audio/x-pn-realaudio" },
+ { ".ram", "audio/x-pn-realaudio" },
+ { ".rar", "application/octet-stream" },
+ { ".ras", "image/x-cmu-raster" },
+ { ".rf", "image/vnd.rn-realflash" },
+ { ".rgb", "image/x-rgb" },
+ { ".rm", "application/vnd.rn-realmedia" },
+ { ".rmi", "audio/mid" },
+ { ".roff", "application/x-troff" },
+ { ".rpm", "audio/x-pn-realaudio-plugin" },
+ { ".rtf", "application/rtf" },
+ { ".rtx", "text/richtext" },
+ { ".scd", "application/x-msschedule" },
+ { ".sct", "text/scriptlet" },
+ { ".sea", "application/octet-stream" },
+ { ".setpay", "application/set-payment-initiation" },
+ { ".setreg", "application/set-registration-initiation" },
+ { ".sgml", "text/sgml" },
+ { ".sh", "application/x-sh" },
+ { ".shar", "application/x-shar" },
+ { ".sit", "application/x-stuffit" },
+ { ".sldm", "application/vnd.ms-powerpoint.slide.macroEnabled.12" },
+ { ".sldx", "application/vnd.openxmlformats-officedocument.presentationml.slide" },
+ { ".smd", "audio/x-smd" },
+ { ".smi", "application/octet-stream" },
+ { ".smx", "audio/x-smd" },
+ { ".smz", "audio/x-smd" },
+ { ".snd", "audio/basic" },
+ { ".snp", "application/octet-stream" },
+ { ".spc", "application/x-pkcs7-certificates" },
+ { ".spl", "application/futuresplash" },
+ { ".src", "application/x-wais-source" },
+ { ".ssm", "application/streamingmedia" },
+ { ".sst", "application/vnd.ms-pki.certstore" },
+ { ".stl", "application/vnd.ms-pki.stl" },
+ { ".sv4cpio", "application/x-sv4cpio" },
+ { ".sv4crc", "application/x-sv4crc" },
+ { ".swf", "application/x-shockwave-flash" },
+ { ".t", "application/x-troff" },
+ { ".tar", "application/x-tar" },
+ { ".tcl", "application/x-tcl" },
+ { ".tex", "application/x-tex" },
+ { ".texi", "application/x-texinfo" },
+ { ".texinfo", "application/x-texinfo" },
+ { ".tgz", "application/x-compressed" },
+ { ".thmx", "application/vnd.ms-officetheme" },
+ { ".thn", "application/octet-stream" },
+ { ".tif", "image/tiff" },
+ { ".tiff", "image/tiff" },
+ { ".toc", "application/octet-stream" },
+ { ".tr", "application/x-troff" },
+ { ".trm", "application/x-msterminal" },
+ { ".tsv", "text/tab-separated-values" },
+ { ".ttf", "application/octet-stream" },
+ { ".txt", "text/plain" },
+ { ".u32", "application/octet-stream" },
+ { ".uls", "text/iuls" },
+ { ".ustar", "application/x-ustar" },
+ { ".vbs", "text/vbscript" },
+ { ".vcf", "text/x-vcard" },
+ { ".vcs", "text/plain" },
+ { ".vdx", "application/vnd.ms-visio.viewer" },
+ { ".vml", "text/xml" },
+ { ".vsd", "application/vnd.visio" },
+ { ".vss", "application/vnd.visio" },
+ { ".vst", "application/vnd.visio" },
+ { ".vsto", "application/x-ms-vsto" },
+ { ".vsw", "application/vnd.visio" },
+ { ".vsx", "application/vnd.visio" },
+ { ".vtx", "application/vnd.visio" },
+ { ".wav", "audio/wav" },
+ { ".wax", "audio/x-ms-wax" },
+ { ".wbmp", "image/vnd.wap.wbmp" },
+ { ".wcm", "application/vnd.ms-works" },
+ { ".wdb", "application/vnd.ms-works" },
+ { ".wks", "application/vnd.ms-works" },
+ { ".wm", "video/x-ms-wm" },
+ { ".wma", "audio/x-ms-wma" },
+ { ".wmd", "application/x-ms-wmd" },
+ { ".wmf", "application/x-msmetafile" },
+ { ".wml", "text/vnd.wap.wml" },
+ { ".wmlc", "application/vnd.wap.wmlc" },
+ { ".wmls", "text/vnd.wap.wmlscript" },
+ { ".wmlsc", "application/vnd.wap.wmlscriptc" },
+ { ".wmp", "video/x-ms-wmp" },
+ { ".wmv", "video/x-ms-wmv" },
+ { ".wmx", "video/x-ms-wmx" },
+ { ".wmz", "application/x-ms-wmz" },
+ { ".wps", "application/vnd.ms-works" },
+ { ".wri", "application/x-mswrite" },
+ { ".wrl", "x-world/x-vrml" },
+ { ".wrz", "x-world/x-vrml" },
+ { ".wsdl", "text/xml" },
+ { ".wvx", "video/x-ms-wvx" },
+ { ".x", "application/directx" },
+ { ".xaf", "x-world/x-vrml" },
+ { ".xaml", "application/xaml+xml" },
+ { ".xap", "application/x-silverlight-app" },
+ { ".xbap", "application/x-ms-xbap" },
+ { ".xbm", "image/x-xbitmap" },
+ { ".xdr", "text/plain" },
+ { ".xla", "application/vnd.ms-excel" },
+ { ".xlam", "application/vnd.ms-excel.addin.macroEnabled.12" },
+ { ".xlc", "application/vnd.ms-excel" },
+ { ".xlm", "application/vnd.ms-excel" },
+ { ".xls", "application/vnd.ms-excel" },
+ { ".xlsb", "application/vnd.ms-excel.sheet.binary.macroEnabled.12" },
+ { ".xlsm", "application/vnd.ms-excel.sheet.macroEnabled.12" },
+ { ".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" },
+ { ".xlt", "application/vnd.ms-excel" },
+ { ".xltm", "application/vnd.ms-excel.template.macroEnabled.12" },
+ { ".xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template" },
+ { ".xlw", "application/vnd.ms-excel" },
+ { ".xml", "text/xml" },
+ { ".xof", "x-world/x-vrml" },
+ { ".xpm", "image/x-xpixmap" },
+ { ".xps", "application/vnd.ms-xpsdocument" },
+ { ".xsd", "text/xml" },
+ { ".xsf", "text/xml" },
+ { ".xsl", "text/xml" },
+ { ".xslt", "text/xml" },
+ { ".xsn", "application/octet-stream" },
+ { ".xtp", "application/octet-stream" },
+ { ".xwd", "image/x-xwindowdump" },
+ { ".z", "application/x-compress" },
+ { ".zip", "application/x-zip-compressed" }
+ };
+
+ internal static string GetMimeMapping(string fileName)
+ {
+ if (fileName == null)
+ {
+ throw new ArgumentNullException("fileName");
+ }
+
+ string contentType = null;
+ string extension = Path.GetExtension(fileName);
+ if (_mimeMappings.TryGetValue(extension, out contentType))
+ {
+ return contentType;
+ }
+ return _mimeMappings[".*"];
+ }
+
+ internal static void AddMimeMapping(string extension, string mimeType)
+ {
+ _mimeMappings.Add(extension, mimeType);
+ }
+ }
+}
diff --git a/src/RS.cs b/src/RS.cs
new file mode 100644
index 00000000..de83ff75
--- /dev/null
+++ b/src/RS.cs
@@ -0,0 +1,10 @@
+using System;
+using System.Globalization;
+
+internal static class RS
+{
+ public static string Format(string format, params object[] args)
+ {
+ return String.Format(CultureInfo.CurrentCulture, format, args);
+ }
+}
diff --git a/src/SPA/Properties/AssemblyInfo.cs b/src/SPA/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..c2d6b73c
--- /dev/null
+++ b/src/SPA/Properties/AssemblyInfo.cs
@@ -0,0 +1,10 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+
+[assembly: AssemblyTitle("SPA")]
+[assembly: AssemblyDescription("This is a dummy assembly to satisfy the build process")]
+[assembly: Guid("6df72360-ebfc-4097-96fa-2ee418c04f7b")]
diff --git a/src/SPA/SPA.csproj b/src/SPA/SPA.csproj
new file mode 100644
index 00000000..e64179c3
--- /dev/null
+++ b/src/SPA/SPA.csproj
@@ -0,0 +1,166 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <ProductVersion>1.0.0</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{1ACEF677-B6A0-4680-A076-7893DE176D6B}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <ScriptOutputPath>$(OutputPath)</ScriptOutputPath>
+ <OutputPath>bin\$(Configuration)</OutputPath>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DefineConstants>DEBUG;TRACE</DefineConstants>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DefineConstants>TRACE</DefineConstants>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'CodeCov|AnyCPU' ">
+ <DefineConstants>DEBUG;TRACE;CODECOV</DefineConstants>
+ </PropertyGroup>
+ <ItemGroup>
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <Content Include="upshot\AssociatedEntitiesView.js" />
+ <Content Include="upshot\Core.js" />
+ <Content Include="upshot\DataContext.js" />
+ <Content Include="upshot\DataProvider.js" />
+ <Content Include="upshot\DataProvider.OData.js" />
+ <Content Include="upshot\DataProvider.ria.js" />
+ <Content Include="upshot\DataSource.js" />
+ <Content Include="upshot\EntitySet.js" />
+ <Content Include="upshot\EntitySource.js" />
+ <Content Include="upshot\EntityView.js" />
+ <Content Include="upshot\LocalDataSource.js" />
+ <Content Include="upshot\Metadata.js" />
+ <Content Include="upshot\Observability.js" />
+ <Content Include="upshot\RemoteDataSource.js" />
+ <Content Include="upshot\Upshot.Compat.jQueryUI.js" />
+ <Content Include="upshot\Upshot.Compat.JsViews.js" />
+ <Content Include="upshot\Upshot.Compat.Knockout.js" />
+ <Content Include="upshot\Upshot.Compat.WinJS.js" />
+ <Content Include="upshot\upshot.dataview.js" />
+ <Content Include="nav\nav.js">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>nav.coffee</DependentUpon>
+ </Content>
+ <Content Include="nav\nav.transitions.js">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>nav.transitions.coffee</DependentUpon>
+ </Content>
+ </ItemGroup>
+ <ItemGroup>
+ <IntelliSense Include="upshot\IntelliSense\Dependencies.js" />
+ <IntelliSense Include="upshot\IntelliSense\jquery-1.5.2-vsdoc.js" />
+ <IntelliSense Include="upshot\IntelliSense\knockout-2.0.0.debug.js" />
+ <IntelliSense Include="upshot\IntelliSense\References.js" />
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="nav\nav.coffee">
+ <Generator>CoffeeScriptGenerator</Generator>
+ <LastGenOutput>nav.js</LastGenOutput>
+ </None>
+ <None Include="nav\nav.transitions.coffee">
+ <Generator>CoffeeScriptGenerator</Generator>
+ <LastGenOutput>nav.transitions.js</LastGenOutput>
+ </None>
+ </ItemGroup>
+ <PropertyGroup>
+ <JSKnownGlobalNames>jQuery,$,upshot,WinJS,OData,JSON,ko</JSKnownGlobalNames>
+ </PropertyGroup>
+ <!-- We don't use these targets, but VS will try to upgrade the project when they aren't present -->
+ <Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v10.0\WebApplications\Microsoft.WebApplication.targets" />
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+ <ProjectExtensions />
+ <!-- SPA build targets -->
+ <Import Project="SPA.targets" />
+ <Target Name="CompileJs">
+ <!-- purpose of compile step here is to do some basic syntax checking on the files prior to packaging -->
+ <ItemGroup>
+ <!-- for validation, compile all js files in the project; this doesn't mean they will be included in the package -->
+ <_CompileJavascript Include="@(Content->'%(FullPath)')" Condition="%(Extension) == '.js'" />
+ </ItemGroup>
+ </Target>
+ <Target Name="BuildUpshot" BeforeTargets="Build">
+ <PropertyGroup>
+ <UpshotPkgOutputFile>$(ScriptOutputPath)\upshot.js</UpshotPkgOutputFile>
+ </PropertyGroup>
+ <ItemGroup>
+ <PkgOutputFile Include="$(UpshotPkgOutputFile)" />
+ <!-- order matters here -->
+ <UpshotPkg Include="upshot\Core.js" />
+ <UpshotPkg Include="upshot\Observability.js" />
+ <UpshotPkg Include="upshot\Metadata.js" />
+ <UpshotPkg Include="upshot\EntitySource.js" />
+ <UpshotPkg Include="upshot\EntityView.js" />
+ <UpshotPkg Include="upshot\DataSource.js" />
+ <UpshotPkg Include="upshot\EntitySet.js" />
+ <UpshotPkg Include="upshot\DataContext.js" />
+ <UpshotPkg Include="upshot\DataProvider.js" />
+ <UpshotPkg Include="upshot\RemoteDataSource.js" />
+ <UpshotPkg Include="upshot\AssociatedEntitiesView.js" />
+ <UpshotPkg Include="upshot\LocalDataSource.js" />
+ <UpshotPkg Include="upshot\DataProvider.OData.js" />
+ <UpshotPkg Include="upshot\DataProvider.ria.js" />
+ </ItemGroup>
+ <PropertyGroup>
+ <UpshotJQueryPkgOutputFile>$(ScriptOutputPath)\Upshot.Compat.jQueryUI.js</UpshotJQueryPkgOutputFile>
+ </PropertyGroup>
+ <ItemGroup>
+ <PkgOutputFile Include="$(UpshotJQueryPkgOutputFile)" />
+ <UpshotJQueryPkg Include="upshot\Upshot.Compat.jQueryUI.js" />
+ </ItemGroup>
+ <PropertyGroup>
+ <UpshotJsViewsPkgOutputFile>$(ScriptOutputPath)\Upshot.Compat.JsViews.js</UpshotJsViewsPkgOutputFile>
+ </PropertyGroup>
+ <ItemGroup>
+ <PkgOutputFile Include="$(UpshotJsViewsPkgOutputFile)" />
+ <UpshotJsViewsPkg Include="upshot\Upshot.Compat.JsViews.js" />
+ </ItemGroup>
+ <PropertyGroup>
+ <UpshotKnockoutPkgOutputFile>$(ScriptOutputPath)\Upshot.Compat.Knockout.js</UpshotKnockoutPkgOutputFile>
+ </PropertyGroup>
+ <ItemGroup>
+ <PkgOutputFile Include="$(UpshotKnockoutPkgOutputFile)" />
+ <UpshotKnockoutPkg Include="upshot\Upshot.Compat.Knockout.js" />
+ </ItemGroup>
+ <PropertyGroup>
+ <UpshotDataViewPkgOutputFile>$(ScriptOutputPath)\upshot.dataview.js</UpshotDataViewPkgOutputFile>
+ </PropertyGroup>
+ <ItemGroup>
+ <PkgOutputFile Include="$(UpshotDataViewPkgOutputFile)" />
+ <UpshotDataViewPkg Include="upshot\upshot.dataview.js" />
+ </ItemGroup>
+ <ProcessScriptFiles ScriptFiles="@(UpshotPkg)" OutputFile="$(UpshotPkgOutputFile)" />
+ <ProcessScriptFiles ScriptFiles="@(UpshotJQueryPkg)" OutputFile="$(UpshotJQueryPkgOutputFile)" />
+ <ProcessScriptFiles ScriptFiles="@(UpshotJsViewsPkg)" OutputFile="$(UpshotJsViewsPkgOutputFile)" />
+ <ProcessScriptFiles ScriptFiles="@(UpshotKnockoutPkg)" OutputFile="$(UpshotKnockoutPkgOutputFile)" />
+ <ProcessScriptFiles ScriptFiles="@(UpshotDataViewPkg)" OutputFile="$(UpshotDataViewPkgOutputFile)" />
+ </Target>
+ <Target Name="BuildNav" BeforeTargets="Build">
+ <ItemGroup>
+ <NavJsFiles Include="nav\nav.js;nav\nav.transitions.js" />
+ </ItemGroup>
+ <Copy SourceFiles="@(NavJsFiles)" DestinationFolder="$(ScriptOutputPath)" />
+ <ItemGroup>
+ <NavPkgOutputFile Include="$(ScriptOutputPath)\nav.js" />
+ <NavPkgOutputFile Include="$(ScriptOutputPath)\nav.transitions.js" />
+ </ItemGroup>
+ </Target>
+ <Target Name="CleanJs" BeforeTargets="Clean">
+ <ItemGroup>
+ <CleanPkgOutputFile Include="$(ScriptOutputPath)\upshot.js" />
+ <CleanPkgOutputFile Include="$(ScriptOutputPath)\Upshot.Compat.jQueryUI.js" />
+ <CleanPkgOutputFile Include="$(ScriptOutputPath)\Upshot.Compat.JsViews.js" />
+ <CleanPkgOutputFile Include="$(ScriptOutputPath)\Upshot.Compat.Knockout.js" />
+ <CleanPkgOutputFile Include="$(ScriptOutputPath)\upshot.dataview.js" />
+ <CleanPkgOutputFile Include="$(ScriptOutputPath)\nav.js" />
+ <CleanPkgOutputFile Include="$(ScriptOutputPath)\nav.transitions.js" />
+ </ItemGroup>
+ <Delete Files="@(CleanPkgOutputFile->'%(RootDir)%(Directory)%(Filename)%(Extension)')" />
+ </Target>
+</Project> \ No newline at end of file
diff --git a/src/SPA/SPA.targets b/src/SPA/SPA.targets
new file mode 100644
index 00000000..9ac4f829
--- /dev/null
+++ b/src/SPA/SPA.targets
@@ -0,0 +1,68 @@
+<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <UsingTask TaskName="ProcessScriptFiles" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
+ <ParameterGroup>
+ <ScriptFiles ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="true" />
+ <OutputFile ParameterType="Microsoft.Build.Framework.ITaskItem" Required="true" />
+ </ParameterGroup>
+ <Task>
+ <Using Namespace="System"/>
+ <Using Namespace="System.IO"/>
+ <Using Namespace="System.Text"/>
+ <Using Namespace="System.Text.RegularExpressions"/>
+ <Using Namespace="Microsoft.Build.Framework"/>
+ <Using Namespace="Microsoft.Build.Utilities"/>
+ <Code Type="Fragment" Language="cs">
+ <![CDATA[
+ try
+ {
+ string output = string.Empty;
+
+ // Adding copyright
+ output +=
+ "// Copyright (c) Microsoft. All rights reserved." + Environment.NewLine +
+ "// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation" + Environment.NewLine +
+ "// files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy," + Environment.NewLine +
+ "// modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the" + Environment.NewLine +
+ "// Software is furnished to do so, subject to the following conditions:" + Environment.NewLine +
+ "//" + Environment.NewLine +
+ "// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software." + Environment.NewLine +
+ "//" + Environment.NewLine +
+ "// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE" + Environment.NewLine +
+ "// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR" + Environment.NewLine +
+ "// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE," + Environment.NewLine +
+ "// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." + Environment.NewLine;
+
+ // Removing /// <reference path="script.js" /> lines
+ // Removing ///#RESTORE from lines
+ Regex regex = new Regex("/// <reference path=\"[^\"]*\" />\r+\n|///#RESTORE ", RegexOptions.Multiline | RegexOptions.Compiled);
+
+ foreach (ITaskItem file in ScriptFiles)
+ {
+ string fullPath = Path.GetFullPath(file.ItemSpec);
+
+ // Adding the original file name
+ output +=
+ Environment.NewLine +
+ "///" + Environment.NewLine +
+ "/// " + Path.GetFileName(fullPath) + Environment.NewLine +
+ "///" + Environment.NewLine +
+ Environment.NewLine;
+
+ output += regex.Replace(File.ReadAllText(fullPath), "");
+ }
+
+ string outputPath = Path.GetFullPath(OutputFile.ItemSpec);
+ File.WriteAllText(outputPath, output, Encoding.UTF8);
+
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Log.LogErrorFromException(ex);
+ return false;
+ }
+ ]]>
+ </Code>
+ </Task>
+ </UsingTask>
+</Project> \ No newline at end of file
diff --git a/src/SPA/nav/nav.coffee b/src/SPA/nav/nav.coffee
new file mode 100644
index 00000000..1bd1590d
--- /dev/null
+++ b/src/SPA/nav/nav.coffee
@@ -0,0 +1,222 @@
+###!
+nav.js v0.1 - (c) Microsoft Corporation
+###
+
+# Small helper function for creating jQuery-style read/write function wrappers around an underlying value
+readWriteValue = (initialValue) ->
+ currentValue = initialValue
+ return () ->
+ if arguments.length > 0 then currentValue = arguments[0]
+ currentValue
+
+class window.NavHistory
+ constructor: (opts) ->
+ @options = opts || {}
+ @options.params = @_extend({}, @options.params, @_asString) # Ensure all defaults are strings
+ @isLinkedToUrl = false
+
+ # If KO is referenced, @position and @entries will be observable. Otherwise, use plain JS objects.
+ # You can override this via @options.ko if you want.
+ useKo = if 'ko' of @options then @options.ko else ko?.observable?
+ @position = if useKo then ko.observable(-1) else readWriteValue(-1)
+ @entries = if useKo then ko.observableArray([]) else []
+
+ length: => @_entriesArray().length
+ relative: (offset) => @_entriesArray()[@position() + offset] || {}
+ current: => @relative(0)
+ params: => @current().params || {}
+ loadedData: => @current().loadedData
+ back: => if @position() > 0 then @navigateAll(@relative(-1).params)
+ forward: => if @position() < @length() - 1 then @navigateAll(@relative(1).params)
+ _entriesArray: => if typeof @entries == 'function' then @entries() else @entries
+
+ initialize: (opts) ->
+ if opts?.linkToUrl
+ @_linkToUrl()
+ else
+ @navigateAll(opts?.params || {})
+ return this
+
+ navigate: (newParams, opts) =>
+ # Retain any params not specified
+ newParamsPlusCurrent = @_extend(@_extend({}, @params()), newParams)
+ @navigateAll(newParamsPlusCurrent, opts)
+
+ navigateAll: (newParams, opts) =>
+ newParams = @_normalizeParams(newParams)
+ isBack = false
+ isForward = false
+ isNoChange = false
+ transition = opts?.transition
+ navEntry = null
+
+ if @length() && @_propsAreEqual(newParams, @params())
+ # We're already there. No need to create a new nav entry.
+ if opts?.force
+ isNoChange = true
+ navEntry = @current()
+ else
+ # In the absence of a "force" directive, we don't even need to navigate at all
+ return
+
+ else if @_propsAreEqual(newParams, @relative(-1)?.params)
+ # It's a "back" - reuse existing transition if no new one was given
+ isBack = true
+ transition = transition || @current().savedTransition
+ navEntry = @relative(-1)
+
+ else if @_propsAreEqual(newParams, @relative(1)?.params)
+ # It's a "forward" - reuse existing transition if no new one was given
+ isForward = true
+ navEntry = @relative(1)
+ transition = transition || @current().savedTransition
+
+ else
+ # Entirely new navigation - create new entry
+ navEntry = { params: newParams, navEntryId: "navEntry_" + @_getUniqueSequenceValue() }
+
+ # Extra param for beforeNavigate/onNavigate callbacks
+ # Note that navInfo.transition is what will be used during the current navigation (regardless of direction)
+ # whereas navEntry.forwardTransition is what we're storing in case we need to reuse transitions in the future
+ navInfo = { isFirst: @length() == 0, isBack: isBack, isForward: isForward, transition: transition }
+
+ beforeNavigateCallback = () =>
+ if isBack
+ # Consider also doing a History.back() here. This feels more natural if there's only a single NavHistory instance,
+ # because then programmatic back/forwards is the same as using the browser controls. But if you have multiple NavHistory
+ # instances, you can't be sure that History.back() will take you to the same place that this instance has logged.
+ @position(@position() - 1)
+ else if isForward
+ # As above comment, except with History.forwards
+ @position(@position() + 1)
+ else if !isNoChange
+ # Clear "forward" items, and add new entry
+ deleteCount = @length() - @position() - 1
+ @entries().splice(@position() + 1, deleteCount, navEntry)
+
+ # Move to it, possibly by removing the first entry if we've exceeded capacity
+ if @options.maxEntries && @length() > @options.maxEntries
+ @entries.shift() # This will notify subscribers to @entries, if it's observable
+ else
+ @position(@position() + 1)
+ # Notify subscribers to @entries, if it's observable
+ if typeof @entries.valueHasMutated == 'function'
+ @entries.valueHasMutated()
+
+ if !isBack && navInfo.transition
+ @current().savedTransition = navInfo.transition
+
+ # Consider only using pushState for totally new navigations (not isBack and not isForwards), and using History.back()
+ # and History.forwards() as in above comments.
+ if @isLinkedToUrl && (opts?.updateUrl != false) && !isNoChange
+ updatedQueryString = @_getUpdatedQueryString(@params())
+ window.NavHistory.historyProvider.pushState({ url: updatedQueryString })
+
+ if @options.onNavigate
+ @options.onNavigate.call(this, @current(), navInfo)
+
+ if !@options.beforeNavigate
+ beforeNavigateCallback()
+ else
+ threadLoadToken = @objectLoadToken = {}
+ @options.beforeNavigate.call(this, navEntry, navInfo, ((loadedData) =>
+ # Ignore the callback unless threadLoadToken still matches. Avoids race conditions.
+ if threadLoadToken == @objectLoadToken
+ if loadedData != undefined
+ navEntry.loadedData = loadedData
+ beforeNavigateCallback()
+ ))
+ this
+
+ _asString: (val) -> if val == null || val == undefined then "" else val.toString()
+
+ _extend: (target, source, mapFunction) ->
+ for own key, value of source
+ target[key] = if mapFunction then mapFunction(value) else value
+ target
+
+ _normalizeParams: (params) ->
+ # Normalized params are purely strings, and contain a value for every default
+ defaults = @options.params || {}
+ @_extend(@_extend({}, defaults), params || {}, @_asString)
+
+ _propsAreEqual: (obj1, obj2) ->
+ if !(obj1 && obj2)
+ return obj1 == obj2
+ for own obj1key, obj1value of obj1
+ if obj2[obj1key] != obj1value then return false
+ for own obj2key, obj2value of obj2
+ if obj1[obj2key] != obj2value then return false
+ true
+
+ _parseQueryString: (url) ->
+ if url.indexOf('?') < 0 then return {}
+ query = url.substring(url.lastIndexOf('?') + 1)
+ result = {}
+ for pair in query.split("&")
+ tokens = pair.split("=")
+ if (tokens.length == 2)
+ result[tokens[0]] = decodeURIComponent(tokens[1])
+ result
+
+ _formatQueryString: (params) ->
+ formattedUrl = '?'
+ for own key, value of params
+ if formattedUrl != '?'
+ formattedUrl += '&'
+ formattedUrl += key + '=' + encodeURIComponent(value)
+ formattedUrl
+
+ _getUpdatedQueryString: (params) ->
+ # Take the existing query string...
+ allUrlParams = @_parseQueryString(window.NavHistory.historyProvider.getState().url)
+
+ # ... update the params based on the supplied arg (removing any that correspond to our default)
+ for own key, defaultValue of @options.params
+ suppliedValue = params[key]
+ if suppliedValue == defaultValue
+ delete allUrlParams[key]
+ else
+ allUrlParams[key] = suppliedValue
+
+ # ... and return the resulting querystring
+ @_formatQueryString(allUrlParams)
+
+ _getUniqueSequenceValue: () ->
+ NavHistory._sequence = NavHistory._sequence || 0
+ (NavHistory._sequence++).toString()
+
+ _linkToUrl: ->
+ @isLinkedToUrl = true
+ onStateChange = =>
+ # Get the subset of URL params that applies to this NavHistory instance
+ applicableParams = {}
+ allUrlParams = @_parseQueryString(window.NavHistory.historyProvider.getState().url)
+ defaults = @options.params || {}
+ for own key, value of allUrlParams when defaults.hasOwnProperty(key)
+ applicableParams[key] = value
+
+ # ... and navigate to the new params
+ @navigateAll(applicableParams, { updateUrl: false })
+
+ # Perform initial navigation (loads state from the requested URL)
+ onStateChange()
+
+ # Respond to future URL changes too
+ window.NavHistory.historyProvider.onStateChange(onStateChange)
+
+# Default history provider is history.js
+window.NavHistory.historyProvider =
+ onStateChange: (handler) -> History.Adapter.bind(window, 'statechange', handler)
+ pushState: (data) -> History.pushState(null, null, data.url)
+ getState: -> History.getState()
+ back: -> History.back()
+
+# Helper to display a specific element, simultaneously hiding its siblings. Useful for toggling panes, tabs, etc.
+# Does not require any DOM library.
+window.NavHistory.showPane = (elementId, navInfo) ->
+ elemToShow = document.getElementById(elementId)
+ if elemToShow
+ for sibling in elemToShow.parentNode.childNodes when sibling.nodeType == 1
+ sibling.style.display = 'none'
+ elemToShow.style.display = 'block' \ No newline at end of file
diff --git a/src/SPA/nav/nav.js b/src/SPA/nav/nav.js
new file mode 100644
index 00000000..82114ece
--- /dev/null
+++ b/src/SPA/nav/nav.js
@@ -0,0 +1,317 @@
+(function() {
+
+ /*!
+ nav.js v0.1 - (c) Microsoft Corporation
+ */
+
+ var readWriteValue;
+ var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, __hasProp = Object.prototype.hasOwnProperty;
+
+ readWriteValue = function(initialValue) {
+ var currentValue;
+ currentValue = initialValue;
+ return function() {
+ if (arguments.length > 0) currentValue = arguments[0];
+ return currentValue;
+ };
+ };
+
+ window.NavHistory = (function() {
+
+ function NavHistory(opts) {
+ this.navigateAll = __bind(this.navigateAll, this);
+ this.navigate = __bind(this.navigate, this);
+ this._entriesArray = __bind(this._entriesArray, this);
+ this.forward = __bind(this.forward, this);
+ this.back = __bind(this.back, this);
+ this.loadedData = __bind(this.loadedData, this);
+ this.params = __bind(this.params, this);
+ this.current = __bind(this.current, this);
+ this.relative = __bind(this.relative, this);
+ this.length = __bind(this.length, this);
+ var useKo;
+ this.options = opts || {};
+ this.options.params = this._extend({}, this.options.params, this._asString);
+ this.isLinkedToUrl = false;
+ useKo = 'ko' in this.options ? this.options.ko : (typeof ko !== "undefined" && ko !== null ? ko.observable : void 0) != null;
+ this.position = useKo ? ko.observable(-1) : readWriteValue(-1);
+ this.entries = useKo ? ko.observableArray([]) : [];
+ }
+
+ NavHistory.prototype.length = function() {
+ return this._entriesArray().length;
+ };
+
+ NavHistory.prototype.relative = function(offset) {
+ return this._entriesArray()[this.position() + offset] || {};
+ };
+
+ NavHistory.prototype.current = function() {
+ return this.relative(0);
+ };
+
+ NavHistory.prototype.params = function() {
+ return this.current().params || {};
+ };
+
+ NavHistory.prototype.loadedData = function() {
+ return this.current().loadedData;
+ };
+
+ NavHistory.prototype.back = function() {
+ if (this.position() > 0) return this.navigateAll(this.relative(-1).params);
+ };
+
+ NavHistory.prototype.forward = function() {
+ if (this.position() < this.length() - 1) {
+ return this.navigateAll(this.relative(1).params);
+ }
+ };
+
+ NavHistory.prototype._entriesArray = function() {
+ if (typeof this.entries === 'function') {
+ return this.entries();
+ } else {
+ return this.entries;
+ }
+ };
+
+ NavHistory.prototype.initialize = function(opts) {
+ if (opts != null ? opts.linkToUrl : void 0) {
+ this._linkToUrl();
+ } else {
+ this.navigateAll((opts != null ? opts.params : void 0) || {});
+ }
+ return this;
+ };
+
+ NavHistory.prototype.navigate = function(newParams, opts) {
+ var newParamsPlusCurrent;
+ newParamsPlusCurrent = this._extend(this._extend({}, this.params()), newParams);
+ return this.navigateAll(newParamsPlusCurrent, opts);
+ };
+
+ NavHistory.prototype.navigateAll = function(newParams, opts) {
+ var beforeNavigateCallback, isBack, isForward, isNoChange, navEntry, navInfo, threadLoadToken, transition, _ref, _ref2;
+ var _this = this;
+ newParams = this._normalizeParams(newParams);
+ isBack = false;
+ isForward = false;
+ isNoChange = false;
+ transition = opts != null ? opts.transition : void 0;
+ navEntry = null;
+ if (this.length() && this._propsAreEqual(newParams, this.params())) {
+ if (opts != null ? opts.force : void 0) {
+ isNoChange = true;
+ navEntry = this.current();
+ } else {
+ return;
+ }
+ } else if (this._propsAreEqual(newParams, (_ref = this.relative(-1)) != null ? _ref.params : void 0)) {
+ isBack = true;
+ transition = transition || this.current().savedTransition;
+ navEntry = this.relative(-1);
+ } else if (this._propsAreEqual(newParams, (_ref2 = this.relative(1)) != null ? _ref2.params : void 0)) {
+ isForward = true;
+ navEntry = this.relative(1);
+ transition = transition || this.current().savedTransition;
+ } else {
+ navEntry = {
+ params: newParams,
+ navEntryId: "navEntry_" + this._getUniqueSequenceValue()
+ };
+ }
+ navInfo = {
+ isFirst: this.length() === 0,
+ isBack: isBack,
+ isForward: isForward,
+ transition: transition
+ };
+ beforeNavigateCallback = function() {
+ var deleteCount, updatedQueryString;
+ if (isBack) {
+ _this.position(_this.position() - 1);
+ } else if (isForward) {
+ _this.position(_this.position() + 1);
+ } else if (!isNoChange) {
+ deleteCount = _this.length() - _this.position() - 1;
+ _this.entries().splice(_this.position() + 1, deleteCount, navEntry);
+ if (_this.options.maxEntries && _this.length() > _this.options.maxEntries) {
+ _this.entries.shift();
+ } else {
+ _this.position(_this.position() + 1);
+ if (typeof _this.entries.valueHasMutated === 'function') {
+ _this.entries.valueHasMutated();
+ }
+ }
+ }
+ if (!isBack && navInfo.transition) {
+ _this.current().savedTransition = navInfo.transition;
+ }
+ if (_this.isLinkedToUrl && ((opts != null ? opts.updateUrl : void 0) !== false) && !isNoChange) {
+ updatedQueryString = _this._getUpdatedQueryString(_this.params());
+ window.NavHistory.historyProvider.pushState({
+ url: updatedQueryString
+ });
+ }
+ if (_this.options.onNavigate) {
+ return _this.options.onNavigate.call(_this, _this.current(), navInfo);
+ }
+ };
+ if (!this.options.beforeNavigate) {
+ beforeNavigateCallback();
+ } else {
+ threadLoadToken = this.objectLoadToken = {};
+ this.options.beforeNavigate.call(this, navEntry, navInfo, (function(loadedData) {
+ if (threadLoadToken === _this.objectLoadToken) {
+ if (loadedData !== void 0) navEntry.loadedData = loadedData;
+ return beforeNavigateCallback();
+ }
+ }));
+ }
+ return this;
+ };
+
+ NavHistory.prototype._asString = function(val) {
+ if (val === null || val === void 0) {
+ return "";
+ } else {
+ return val.toString();
+ }
+ };
+
+ NavHistory.prototype._extend = function(target, source, mapFunction) {
+ var key, value;
+ for (key in source) {
+ if (!__hasProp.call(source, key)) continue;
+ value = source[key];
+ target[key] = mapFunction ? mapFunction(value) : value;
+ }
+ return target;
+ };
+
+ NavHistory.prototype._normalizeParams = function(params) {
+ var defaults;
+ defaults = this.options.params || {};
+ return this._extend(this._extend({}, defaults), params || {}, this._asString);
+ };
+
+ NavHistory.prototype._propsAreEqual = function(obj1, obj2) {
+ var obj1key, obj1value, obj2key, obj2value;
+ if (!(obj1 && obj2)) return obj1 === obj2;
+ for (obj1key in obj1) {
+ if (!__hasProp.call(obj1, obj1key)) continue;
+ obj1value = obj1[obj1key];
+ if (obj2[obj1key] !== obj1value) return false;
+ }
+ for (obj2key in obj2) {
+ if (!__hasProp.call(obj2, obj2key)) continue;
+ obj2value = obj2[obj2key];
+ if (obj1[obj2key] !== obj2value) return false;
+ }
+ return true;
+ };
+
+ NavHistory.prototype._parseQueryString = function(url) {
+ var pair, query, result, tokens, _i, _len, _ref;
+ if (url.indexOf('?') < 0) return {};
+ query = url.substring(url.lastIndexOf('?') + 1);
+ result = {};
+ _ref = query.split("&");
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ pair = _ref[_i];
+ tokens = pair.split("=");
+ if (tokens.length === 2) result[tokens[0]] = decodeURIComponent(tokens[1]);
+ }
+ return result;
+ };
+
+ NavHistory.prototype._formatQueryString = function(params) {
+ var formattedUrl, key, value;
+ formattedUrl = '?';
+ for (key in params) {
+ if (!__hasProp.call(params, key)) continue;
+ value = params[key];
+ if (formattedUrl !== '?') formattedUrl += '&';
+ formattedUrl += key + '=' + encodeURIComponent(value);
+ }
+ return formattedUrl;
+ };
+
+ NavHistory.prototype._getUpdatedQueryString = function(params) {
+ var allUrlParams, defaultValue, key, suppliedValue, _ref;
+ allUrlParams = this._parseQueryString(window.NavHistory.historyProvider.getState().url);
+ _ref = this.options.params;
+ for (key in _ref) {
+ if (!__hasProp.call(_ref, key)) continue;
+ defaultValue = _ref[key];
+ suppliedValue = params[key];
+ if (suppliedValue === defaultValue) {
+ delete allUrlParams[key];
+ } else {
+ allUrlParams[key] = suppliedValue;
+ }
+ }
+ return this._formatQueryString(allUrlParams);
+ };
+
+ NavHistory.prototype._getUniqueSequenceValue = function() {
+ NavHistory._sequence = NavHistory._sequence || 0;
+ return (NavHistory._sequence++).toString();
+ };
+
+ NavHistory.prototype._linkToUrl = function() {
+ var onStateChange;
+ var _this = this;
+ this.isLinkedToUrl = true;
+ onStateChange = function() {
+ var allUrlParams, applicableParams, defaults, key, value;
+ applicableParams = {};
+ allUrlParams = _this._parseQueryString(window.NavHistory.historyProvider.getState().url);
+ defaults = _this.options.params || {};
+ for (key in allUrlParams) {
+ if (!__hasProp.call(allUrlParams, key)) continue;
+ value = allUrlParams[key];
+ if (defaults.hasOwnProperty(key)) applicableParams[key] = value;
+ }
+ return _this.navigateAll(applicableParams, {
+ updateUrl: false
+ });
+ };
+ onStateChange();
+ return window.NavHistory.historyProvider.onStateChange(onStateChange);
+ };
+
+ return NavHistory;
+
+ })();
+
+ window.NavHistory.historyProvider = {
+ onStateChange: function(handler) {
+ return History.Adapter.bind(window, 'statechange', handler);
+ },
+ pushState: function(data) {
+ return History.pushState(null, null, data.url);
+ },
+ getState: function() {
+ return History.getState();
+ },
+ back: function() {
+ return History.back();
+ }
+ };
+
+ window.NavHistory.showPane = function(elementId, navInfo) {
+ var elemToShow, sibling, _i, _len, _ref;
+ elemToShow = document.getElementById(elementId);
+ if (elemToShow) {
+ _ref = elemToShow.parentNode.childNodes;
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ sibling = _ref[_i];
+ if (sibling.nodeType === 1) sibling.style.display = 'none';
+ }
+ return elemToShow.style.display = 'block';
+ }
+ };
+
+}).call(this);
diff --git a/src/SPA/nav/nav.transitions.coffee b/src/SPA/nav/nav.transitions.coffee
new file mode 100644
index 00000000..4b55f342
--- /dev/null
+++ b/src/SPA/nav/nav.transitions.coffee
@@ -0,0 +1,231 @@
+###!
+nav.transitions.js v0.1 - (c) Microsoft Corporation
+###
+
+# Pane transitions. Currently assumes availability of XUI. Need to generalise for jQuery.
+$ = x$
+
+# Feature detection
+features =
+ vendor:
+ if (/webkit/i).test(navigator.appVersion) then 'webkit'
+ else if (/firefox/i).test(navigator.userAgent) then 'Moz'
+ else if 'opera' of window then 'O'
+ else ''
+ isAndroid: (/android/gi).test(navigator.appVersion)
+
+features.useCssTransform = (!features.isAndroid) && (features.vendor + 'Transform' of document.documentElement.style)
+features.cssTransformPrefix = "-" + features.vendor.toLowerCase() + "-"
+features.transitionEndEvent =
+ if features.vendor == 'webkit' then 'webkitTransitionEnd'
+ else if features.vendor == 'O' then 'oTransitionEnd'
+ else 'transitionend'
+
+$.isTouch = 'ontouchstart' of document.documentElement
+$.clickOrTouch = if $.isTouch then 'touchstart' else 'click'
+features.supportsCssTouchScroll = (typeof document.body.style.webkitOverflowScrolling != "undefined") # Currently only iOS 5 can do native touch scrolling
+features.supportsIScroll = (features.vendor == 'webkit' || features.vendor == "Moz")
+
+# Utilities
+findFirstChildWithClass = (elem, className) ->
+ child = elem.firstChild
+ while child
+ if $(child).hasClass(className) then return child
+ child = child.nextSibling
+ return null
+
+oppositeDirection = { left: 'right', right: 'left', top: 'bottom', bottom: 'top' }
+oppositeTransition = (transition) ->
+ if transition
+ for own key of transition
+ if $.paneTransitionInverters.hasOwnProperty(key)
+ return $.paneTransitionInverters[key](transition[key])
+ null
+
+# Implement $.getJSON and $.map like jQuery does
+$.getJSON = (url, options) ->
+ callback = if typeof options == "function" then options else options.callback
+ $(null).xhr(url, {
+ method: options.method,
+ async: true,
+ data: JSON.stringify(options.data),
+ headers: options.headers,
+ callback: ->
+ callback(JSON.parse(this.responseText))
+ })
+
+$.map = (items, map) ->
+ results = []
+ for item in items
+ mapped = map(item)
+ if mapped != undefined
+ results.push(mapped)
+ results
+
+# XUI extensions
+$.fn.togglePane = (show) ->
+ @each ->
+ # Can't just toggle display:block/none, as that resets any scroll positions within the pane
+ # Can't just toggle visibility:visible/hidden either, as Safari iOS still sends events (e.g., taps) into the element
+ # So, just move it very far away
+ if show
+ @style.display = 'block'
+ if @isOffScreen
+ @isOffScreen = false
+ @style.top = ''
+ @style.bottom = ''
+ else
+ @isOffScreen = true
+ @style.top = '-10000px'
+ @style.bottom = '10000px'
+ this
+
+$.fn.afterNextTransition = (callback) ->
+ @each ->
+ elem = this
+ handlerWrapper = () ->
+ callback.apply(this, arguments)
+ elem.removeEventListener(features.transitionEndEvent, handlerWrapper)
+ elem.addEventListener(features.transitionEndEvent, handlerWrapper);
+
+$.fn.animateTranslation = (finalPos, transition, callback) ->
+ callback = callback || () -> { }
+ @each ->
+ $this = $(this)
+ if features.useCssTransform
+ transform = {}
+ transform[features.cssTransformPrefix + "transform"] = "translate(" + finalPos.left + ", " + finalPos.top + ")"
+ transform[features.cssTransformPrefix + "transition"] = if transition then features.cssTransformPrefix + "transform 250ms ease-out" else null
+
+ if transition
+ $this.afterNextTransition(callback)
+
+ $this.css(transform)
+ if !transition
+ callback()
+ else
+ if transition
+ $this.tween(finalPos, callback)
+ else
+ $this.css(finalPos)
+ callback()
+
+$.fn.setPanePosition = (position, transition, callback) ->
+ callback = callback || () -> { }
+
+ @each ->
+ $this = $(this).togglePane(true)
+ x = 0
+ y = 0
+ width = @parentNode.offsetWidth
+ height = @parentNode.offsetHeight
+
+ switch position
+ when 'right' then x = width
+ when 'left' then x = -1 * width
+ when 'top' then y = -1 * height
+ when 'bottom' then y = height
+
+ finalPos = { left: x + 'px', right: (-1 * x) + 'px', top: y + 'px', bottom: (-1 * y) + 'px' }
+ $this.animateTranslation(finalPos, transition, callback)
+
+$.fn.slidePane = (options) ->
+ @each ->
+ $this = $(this)
+ afterSlide = ->
+ if options.to then $this.togglePane(false)
+ if options.callback then options.callback()
+ $this.setPanePosition(options.from, null)
+ .setPanePosition(options.to, true, afterSlide)
+
+$.fn.showPane = (options) ->
+ options = options || {}
+ @each ->
+ activePane = findFirstChildWithClass(this.parentNode, "active")
+ if activePane != this # Not already shown
+ $(this).has(".scroll-y.autoscroll").touchScroll({ hScroll: false })
+ $(this).has(".scroll-x.autoscroll").touchScroll({ yScroll: false })
+
+ # Find and invoke the requested transition
+ transitionToUse = 'default'
+ for own transitionKey of $.paneTransitions
+ if options.hasOwnProperty(transitionKey)
+ transitionToUse = transitionKey
+ break
+ $.paneTransitions[transitionKey](this, activePane, options[transitionKey])
+
+ # Keep track of which pane is active
+ $(this).addClass("active")
+ if activePane
+ $(activePane).removeClass("active")
+
+$.fn.showBySlidingParent = (options) ->
+ @each ->
+ targetPaneOffsetLeft = parseInt(@style.left) || 0
+ targetPaneOffsetTop = parseInt(@style.top) || 0
+ finalPos =
+ left: (-1*targetPaneOffsetLeft) + '%'
+ right: targetPaneOffsetLeft + '%'
+ top: (-1*targetPaneOffsetTop) + '%'
+ bottom: targetPaneOffsetTop + '%'
+ $(this).css({ display: 'block' })
+ $(@parentNode).css({ 'overflow': 'visible' }).animateTranslation(finalPos, options.animate != false)
+ this
+
+$.fn.touchScroll = (options) ->
+ if (!features.supportsCssTouchScroll) && features.supportsIScroll
+ @each ->
+ if !@hasIScroll
+ @hasIScroll = new iScroll(this, options)
+ doRefresh = => @hasIScroll.refresh()
+ setTimeout(doRefresh, 0)
+ this
+ this
+
+$.fn.clickOrTouch = (handler) ->
+ @on($.clickOrTouch, handler)
+
+# Create missing event shortcuts for IE version of XUI
+for eventName in ['click']
+ if (!$.fn[eventName])
+ $.fn[eventName] = (handler) -> @on(eventName, handler)
+
+# Transitions
+$.paneTransitions =
+ slideFrom: (incomingPane, outgoingPane, options) ->
+ $(incomingPane).slidePane({ from: options })
+ if outgoingPane
+ $(outgoingPane).slidePane({ to: oppositeDirection[options] })
+
+ coverFrom: (incomingPane, outgoingPane, options) ->
+ outgoingZIndex = outgoingPane?.style.zIndex || 0
+ $(incomingPane).css({ zIndex: outgoingZIndex + 1 })
+ .slidePane({
+ from: options,
+ callback: () -> $(outgoingPane).togglePane(false)
+ })
+
+ uncoverTo: (incomingPane, outgoingPane, options) ->
+ incomingZIndex = incomingPane.style.zIndex || 0;
+ $(incomingPane).togglePane(true).setPanePosition()
+ $(outgoingPane).css({ zIndex: incomingZIndex + 1 }).slidePane({ to: options })
+
+ default: (incomingPane, outgoingPane, options) ->
+ # No transition - just show instantly, and hide the previously active pane
+ $(incomingPane).togglePane(true).setPanePosition()
+ $(outgoingPane).togglePane(false)
+
+$.paneTransitionInverters =
+ slideFrom: (direction) -> { slideFrom: oppositeDirection[direction] }
+ coverFrom: (direction) -> { uncoverTo: direction }
+ uncoverTo: (direction) -> { coverFrom: direction }
+
+# Hook into nav.js so you can easily animate transitions on navigation
+window.NavHistory.animatePane = (elementId, navInfo) ->
+ transition = navInfo.transition || (if !navInfo.isFirst then { slideFrom: 'right' } else null)
+ if navInfo.isBack
+ transition = oppositeTransition(transition)
+ x$('#' + elementId).showPane(transition)
+
+window.NavHistory.slideParent = (elementId, navInfo) ->
+ x$('#' + elementId).showBySlidingParent({ animate: !navInfo.isFirst }) \ No newline at end of file
diff --git a/src/SPA/nav/nav.transitions.js b/src/SPA/nav/nav.transitions.js
new file mode 100644
index 00000000..920ec518
--- /dev/null
+++ b/src/SPA/nav/nav.transitions.js
@@ -0,0 +1,338 @@
+(function() {
+
+ /*!
+ nav.transitions.js v0.1 - (c) Microsoft Corporation
+ */
+
+ var $, eventName, features, findFirstChildWithClass, oppositeDirection, oppositeTransition, _i, _len, _ref;
+ var __hasProp = Object.prototype.hasOwnProperty;
+
+ $ = x$;
+
+ features = {
+ vendor: /webkit/i.test(navigator.appVersion) ? 'webkit' : /firefox/i.test(navigator.userAgent) ? 'Moz' : 'opera' in window ? 'O' : '',
+ isAndroid: /android/gi.test(navigator.appVersion)
+ };
+
+ features.useCssTransform = (!features.isAndroid) && (features.vendor + 'Transform' in document.documentElement.style);
+
+ features.cssTransformPrefix = "-" + features.vendor.toLowerCase() + "-";
+
+ features.transitionEndEvent = features.vendor === 'webkit' ? 'webkitTransitionEnd' : features.vendor === 'O' ? 'oTransitionEnd' : 'transitionend';
+
+ $.isTouch = 'ontouchstart' in document.documentElement;
+
+ $.clickOrTouch = $.isTouch ? 'touchstart' : 'click';
+
+ features.supportsCssTouchScroll = typeof document.body.style.webkitOverflowScrolling !== "undefined";
+
+ features.supportsIScroll = features.vendor === 'webkit' || features.vendor === "Moz";
+
+ findFirstChildWithClass = function(elem, className) {
+ var child;
+ child = elem.firstChild;
+ while (child) {
+ if ($(child).hasClass(className)) return child;
+ child = child.nextSibling;
+ }
+ return null;
+ };
+
+ oppositeDirection = {
+ left: 'right',
+ right: 'left',
+ top: 'bottom',
+ bottom: 'top'
+ };
+
+ oppositeTransition = function(transition) {
+ var key;
+ if (transition) {
+ for (key in transition) {
+ if (!__hasProp.call(transition, key)) continue;
+ if ($.paneTransitionInverters.hasOwnProperty(key)) {
+ return $.paneTransitionInverters[key](transition[key]);
+ }
+ }
+ }
+ return null;
+ };
+
+ $.getJSON = function(url, options) {
+ var callback;
+ callback = typeof options === "function" ? options : options.callback;
+ return $(null).xhr(url, {
+ method: options.method,
+ async: true,
+ data: JSON.stringify(options.data),
+ headers: options.headers,
+ callback: function() {
+ return callback(JSON.parse(this.responseText));
+ }
+ });
+ };
+
+ $.map = function(items, map) {
+ var item, mapped, results, _i, _len;
+ results = [];
+ for (_i = 0, _len = items.length; _i < _len; _i++) {
+ item = items[_i];
+ mapped = map(item);
+ if (mapped !== void 0) results.push(mapped);
+ }
+ return results;
+ };
+
+ $.fn.togglePane = function(show) {
+ return this.each(function() {
+ if (show) {
+ this.style.display = 'block';
+ if (this.isOffScreen) {
+ this.isOffScreen = false;
+ this.style.top = '';
+ this.style.bottom = '';
+ }
+ } else {
+ this.isOffScreen = true;
+ this.style.top = '-10000px';
+ this.style.bottom = '10000px';
+ }
+ return this;
+ });
+ };
+
+ $.fn.afterNextTransition = function(callback) {
+ return this.each(function() {
+ var elem, handlerWrapper;
+ elem = this;
+ handlerWrapper = function() {
+ callback.apply(this, arguments);
+ return elem.removeEventListener(features.transitionEndEvent, handlerWrapper);
+ };
+ return elem.addEventListener(features.transitionEndEvent, handlerWrapper);
+ });
+ };
+
+ $.fn.animateTranslation = function(finalPos, transition, callback) {
+ callback = callback || function() {
+ return {};
+ };
+ return this.each(function() {
+ var $this, transform;
+ $this = $(this);
+ if (features.useCssTransform) {
+ transform = {};
+ transform[features.cssTransformPrefix + "transform"] = "translate(" + finalPos.left + ", " + finalPos.top + ")";
+ transform[features.cssTransformPrefix + "transition"] = transition ? features.cssTransformPrefix + "transform 250ms ease-out" : null;
+ if (transition) $this.afterNextTransition(callback);
+ $this.css(transform);
+ if (!transition) return callback();
+ } else {
+ if (transition) {
+ return $this.tween(finalPos, callback);
+ } else {
+ $this.css(finalPos);
+ return callback();
+ }
+ }
+ });
+ };
+
+ $.fn.setPanePosition = function(position, transition, callback) {
+ callback = callback || function() {
+ return {};
+ };
+ return this.each(function() {
+ var $this, finalPos, height, width, x, y;
+ $this = $(this).togglePane(true);
+ x = 0;
+ y = 0;
+ width = this.parentNode.offsetWidth;
+ height = this.parentNode.offsetHeight;
+ switch (position) {
+ case 'right':
+ x = width;
+ break;
+ case 'left':
+ x = -1 * width;
+ break;
+ case 'top':
+ y = -1 * height;
+ break;
+ case 'bottom':
+ y = height;
+ }
+ finalPos = {
+ left: x + 'px',
+ right: (-1 * x) + 'px',
+ top: y + 'px',
+ bottom: (-1 * y) + 'px'
+ };
+ return $this.animateTranslation(finalPos, transition, callback);
+ });
+ };
+
+ $.fn.slidePane = function(options) {
+ return this.each(function() {
+ var $this, afterSlide;
+ $this = $(this);
+ afterSlide = function() {
+ if (options.to) $this.togglePane(false);
+ if (options.callback) return options.callback();
+ };
+ return $this.setPanePosition(options.from, null).setPanePosition(options.to, true, afterSlide);
+ });
+ };
+
+ $.fn.showPane = function(options) {
+ options = options || {};
+ return this.each(function() {
+ var activePane, transitionKey, transitionToUse, _ref;
+ activePane = findFirstChildWithClass(this.parentNode, "active");
+ if (activePane !== this) {
+ $(this).has(".scroll-y.autoscroll").touchScroll({
+ hScroll: false
+ });
+ $(this).has(".scroll-x.autoscroll").touchScroll({
+ yScroll: false
+ });
+ transitionToUse = 'default';
+ _ref = $.paneTransitions;
+ for (transitionKey in _ref) {
+ if (!__hasProp.call(_ref, transitionKey)) continue;
+ if (options.hasOwnProperty(transitionKey)) {
+ transitionToUse = transitionKey;
+ break;
+ }
+ }
+ $.paneTransitions[transitionKey](this, activePane, options[transitionKey]);
+ $(this).addClass("active");
+ if (activePane) return $(activePane).removeClass("active");
+ }
+ });
+ };
+
+ $.fn.showBySlidingParent = function(options) {
+ return this.each(function() {
+ var finalPos, targetPaneOffsetLeft, targetPaneOffsetTop;
+ targetPaneOffsetLeft = parseInt(this.style.left) || 0;
+ targetPaneOffsetTop = parseInt(this.style.top) || 0;
+ finalPos = {
+ left: (-1 * targetPaneOffsetLeft) + '%',
+ right: targetPaneOffsetLeft + '%',
+ top: (-1 * targetPaneOffsetTop) + '%',
+ bottom: targetPaneOffsetTop + '%'
+ };
+ $(this).css({
+ display: 'block'
+ });
+ $(this.parentNode).css({
+ 'overflow': 'visible'
+ }).animateTranslation(finalPos, options.animate !== false);
+ return this;
+ });
+ };
+
+ $.fn.touchScroll = function(options) {
+ if ((!features.supportsCssTouchScroll) && features.supportsIScroll) {
+ this.each(function() {
+ var doRefresh;
+ var _this = this;
+ if (!this.hasIScroll) this.hasIScroll = new iScroll(this, options);
+ doRefresh = function() {
+ return _this.hasIScroll.refresh();
+ };
+ setTimeout(doRefresh, 0);
+ return this;
+ });
+ }
+ return this;
+ };
+
+ $.fn.clickOrTouch = function(handler) {
+ return this.on($.clickOrTouch, handler);
+ };
+
+ _ref = ['click'];
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ eventName = _ref[_i];
+ if (!$.fn[eventName]) {
+ $.fn[eventName] = function(handler) {
+ return this.on(eventName, handler);
+ };
+ }
+ }
+
+ $.paneTransitions = {
+ slideFrom: function(incomingPane, outgoingPane, options) {
+ $(incomingPane).slidePane({
+ from: options
+ });
+ if (outgoingPane) {
+ return $(outgoingPane).slidePane({
+ to: oppositeDirection[options]
+ });
+ }
+ },
+ coverFrom: function(incomingPane, outgoingPane, options) {
+ var outgoingZIndex;
+ outgoingZIndex = (outgoingPane != null ? outgoingPane.style.zIndex : void 0) || 0;
+ return $(incomingPane).css({
+ zIndex: outgoingZIndex + 1
+ }).slidePane({
+ from: options,
+ callback: function() {
+ return $(outgoingPane).togglePane(false);
+ }
+ });
+ },
+ uncoverTo: function(incomingPane, outgoingPane, options) {
+ var incomingZIndex;
+ incomingZIndex = incomingPane.style.zIndex || 0;
+ $(incomingPane).togglePane(true).setPanePosition();
+ return $(outgoingPane).css({
+ zIndex: incomingZIndex + 1
+ }).slidePane({
+ to: options
+ });
+ },
+ "default": function(incomingPane, outgoingPane, options) {
+ $(incomingPane).togglePane(true).setPanePosition();
+ return $(outgoingPane).togglePane(false);
+ }
+ };
+
+ $.paneTransitionInverters = {
+ slideFrom: function(direction) {
+ return {
+ slideFrom: oppositeDirection[direction]
+ };
+ },
+ coverFrom: function(direction) {
+ return {
+ uncoverTo: direction
+ };
+ },
+ uncoverTo: function(direction) {
+ return {
+ coverFrom: direction
+ };
+ }
+ };
+
+ window.NavHistory.animatePane = function(elementId, navInfo) {
+ var transition;
+ transition = navInfo.transition || (!navInfo.isFirst ? {
+ slideFrom: 'right'
+ } : null);
+ if (navInfo.isBack) transition = oppositeTransition(transition);
+ return x$('#' + elementId).showPane(transition);
+ };
+
+ window.NavHistory.slideParent = function(elementId, navInfo) {
+ return x$('#' + elementId).showBySlidingParent({
+ animate: !navInfo.isFirst
+ });
+ };
+
+}).call(this);
diff --git a/src/SPA/upshot/AssociatedEntitiesView.js b/src/SPA/upshot/AssociatedEntitiesView.js
new file mode 100644
index 00000000..84ed67c3
--- /dev/null
+++ b/src/SPA/upshot/AssociatedEntitiesView.js
@@ -0,0 +1,221 @@
+/// <reference path="IntelliSense\References.js" />
+///#RESTORE (function (global, $, upshot, undefined)
+{
+ var base = upshot.EntityView.prototype;
+
+ var obs = upshot.observability;
+
+ var ctor = function (entity, parentEntitySet, childEntitySet, associationMetadata, parentPropertySetter, result) {
+ this._entity = entity;
+ this._parentEntitySet = parentEntitySet;
+ this._childEntitySet = childEntitySet;
+ this._associationMetadata = associationMetadata;
+ this._parentPropertySetter = parentPropertySetter;
+
+ // The EntityView base class observes its "source" option (which is the target entity set) for
+ // array- and property-changes.
+ // Additionally, we need to observe property changes on the source entity set to catch:
+ // - FK property changes that would affect a parent association property
+ // - PK (non-FK) property changes that would affect a child association property
+ var self = this;
+ this._sourceEntitySetObserver = function (entity, property, newValue) {
+ if (!self._needRecompute &&
+ $.inArray(property, associationMetadata.thisKey) >= 0) {
+ self._setNeedRecompute();
+ }
+ };
+ var sourceEntitySet = associationMetadata.isForeignKey ? childEntitySet : parentEntitySet;
+ sourceEntitySet.bind("propertyChanged", this._sourceEntitySetObserver);
+
+ var entitySource = associationMetadata.isForeignKey ? parentEntitySet : childEntitySet;
+ base.constructor.call(this, { source: entitySource, result: result });
+
+ // We only ever instantiate AssociatedEntitiesViews when adding entities to
+ // an EntitySet, which always ends with a recompute.
+ this._initialized = false;
+ this._setNeedRecompute();
+ };
+
+ var instanceMembers = {
+
+ // Internal methods
+
+ // This is called from EntitySet.js as it treats tracked changes to parent association
+ // properties on child entities.
+ __handleParentPropertySet: function (parentEntity) {
+ this._handleRelationshipEdit(this._entity, parentEntity);
+ },
+
+ // Private methods
+
+ _dispose: function () {
+ var sourceEntitySet = this._associationMetadata.isForeignKey ? this._childEntitySet : this._parentEntitySet;
+ sourceEntitySet.unbind("propertyChanged", this._sourceEntitySetObserver);
+ base._dispose.apply(this, arguments);
+ },
+
+ _handleEntityAdd: function (entity) {
+ this._handleRelationshipEdit(entity, this._entity);
+ },
+
+ // Do the appropriate EntitySet adds and FK property changes to reflect an editied relationship
+ // between childEntity and parentEntity.
+ _handleRelationshipEdit: function (childEntity, parentEntity) {
+ var associationMetadata = this._associationMetadata,
+ isForeignKey = associationMetadata.isForeignKey,
+ parentKeyValue;
+ if (!parentEntity) {
+ parentKeyValue = null;
+ } else {
+ if ($.inArray(parentEntity, obs.asArray(this._parentEntitySet.getEntities())) < 0) {
+ // TODO -- Should this implicitly add the parent entity? I doubt it.
+ throw "Parent entity is not in the parent entity set for this association.";
+ } else if ((this._parentEntitySet.getEntityState(parentEntity) || "").indexOf("Add") > 0) {
+ // TODO -- Add support for added parent entities without an established key value, fix-up after commit.
+ throw "NYI -- Cannot set foreign keys to key values computed from added entities. Commit the parent entity first.";
+ }
+
+ var parentKey = isForeignKey ? associationMetadata.otherKey : associationMetadata.thisKey;
+ parentKeyValue = obs.getProperty(parentEntity, parentKey[0]); // TODO -- Generalize to N fields.
+ if (parentKeyValue === undefined) {
+ throw "Parent entity has no value for its '" + parentKey[0] + "' key property.";
+ }
+ }
+
+ var childKey = isForeignKey ? associationMetadata.thisKey : associationMetadata.otherKey,
+ childKeyValue = obs.getProperty(childEntity, childKey[0]), // TODO -- Generalize to N fields.
+ setForeignKeyValue;
+ if (!parentEntity) {
+ if (childKeyValue !== null) {
+ setForeignKeyValue = true;
+ }
+ } else if (childKeyValue === undefined || childKeyValue !== parentKeyValue) {
+ setForeignKeyValue = true;
+ }
+
+ var isAddToChildEntities = !isForeignKey;
+ if (isAddToChildEntities && $.inArray(childEntity, obs.asArray(this._entitySource.getEntities())) < 0) {
+ // Base class will translate add to child entities into an add on our input EntitySet.
+ base._handleEntityAdd.call(this, childEntity);
+ }
+
+ if (setForeignKeyValue) {
+ // Do this after the entitySet add above. That way, the property change will be observable by clients
+ // interested in childEntitiesCollection or the EntitySet.
+ // Likewise, above, we will have done obs.track (as part of adding to the EntitySet) before
+ // obs.setProperty, in case establishing observable proxies is done implicitly w/in setProperty
+ // (as WinJS support does).
+ this._childEntitySet.__setProperty(childEntity, childKey[0], parentKeyValue); // TODO -- Generalize to N fields.
+ }
+ },
+
+ _onPropertyChanged: function (entity, property, newValue) {
+ if (!this._needRecompute &&
+ $.inArray(property, this._associationMetadata.otherKey) >= 0) {
+ this._setNeedRecompute();
+ }
+ base._onPropertyChanged.apply(this, arguments);
+ },
+
+ _onArrayChanged: function (type, eventArgs) {
+ if (this._needRecompute) {
+ return;
+ }
+
+ var needRecompute;
+ switch (type) {
+ case "insert":
+ case "remove":
+ var self = this;
+ $.each(eventArgs.items, function (index, entity) {
+ if (self._haveEntity(entity) ^ type === "insert") {
+ needRecompute = true;
+ return false;
+ }
+ });
+ break;
+
+ case "replaceAll":
+ needRecompute = true;
+ break;
+
+ default:
+ throw "NYI -- Array operation '" + type + "' is not supported.";
+ }
+
+ if (needRecompute) {
+ this._setNeedRecompute();
+ }
+ },
+
+ _recompute: function () {
+ var clientEntities = this._clientEntities,
+ newEntities = this._computeAssociatedEntities();
+
+ if (!this._initialized) {
+ this._initialized = true;
+
+ if (newEntities.length > 0) { // Don't event a replaceAll if we're not actually modifying the entities array.
+ var oldEntities = obs.asArray(clientEntities).slice(); // Here, assume a live array. It will be for jQuery compat.
+ obs.refresh(clientEntities, newEntities);
+ this._trigger("arrayChanged", "replaceAll", { oldItems: oldEntities, newItems: obs.asArray(clientEntities) });
+ }
+ } else {
+ // Perform adds/removes on clientEntities to have it reflect the same membership
+ // as newEntities. Issue change events for the adds/removes.
+ // Don't try to preserve ordering between clientEntities and newEntities.
+ // Assume that obs.asArray returns a non-live array instance. It will be for Knockout compat.
+ // Don't cache obs.asArray(clientEntities) below.
+ var self = this;
+
+ var addedEntities = $.grep(newEntities, function (entity) {
+ return $.inArray(entity, obs.asArray(clientEntities)) < 0;
+ });
+ $.each(addedEntities, function (unused, entity) {
+ var index = obs.asArray(clientEntities).length,
+ items = [entity];
+ obs.insert(clientEntities, index, items);
+ self._trigger("arrayChanged", "insert", { index: index, items: items });
+ });
+
+ var removedEntities = $.grep(obs.asArray(clientEntities), function (entity) {
+ return $.inArray(entity, newEntities) < 0;
+ });
+ $.each(removedEntities, function (unused, entity) {
+ var indexRemove = $.inArray(entity, obs.asArray(clientEntities));
+ obs.remove(clientEntities, indexRemove, 1);
+ self._trigger("arrayChanged", "remove", { index: indexRemove, items: [entity] });
+ });
+ }
+
+ if (this._parentPropertySetter) {
+ // EntitySet.js has supplied a handler with which to make observable changes
+ // to a parent association property on a child entity.
+ this._parentPropertySetter.apply(this);
+ }
+ },
+
+ _computeAssociatedEntities: function () {
+ var entity = this._entity,
+ associationMetadata = this._associationMetadata,
+ sourceKeyValue = obs.getProperty(entity, associationMetadata.thisKey[0]), // TODO -- Generalize to N fields.
+ targetEntitySet = associationMetadata.isForeignKey ? this._parentEntitySet : this._childEntitySet,
+ targetEntities = obs.asArray(targetEntitySet.getEntities()),
+ targetKey = associationMetadata.otherKey,
+ associatedEntities = [];
+ for (var i = 0; i < targetEntities.length; i++) {
+ var targetEntity = targetEntities[i],
+ targetKeyValue = obs.getProperty(targetEntity, targetKey[0]); // TODO -- Generalize to N fields.
+ if (targetKeyValue !== undefined && targetKeyValue === sourceKeyValue) {
+ associatedEntities.push(targetEntity);
+ }
+ }
+ return associatedEntities;
+ }
+
+ // TODO -- Make array removals from "_clientEntities" null out foreign key values.
+ };
+
+ upshot.AssociatedEntitiesView = upshot.deriveClass(base, ctor, instanceMembers);
+}
+///#RESTORE )(this, jQuery, upshot);
diff --git a/src/SPA/upshot/Core.js b/src/SPA/upshot/Core.js
new file mode 100644
index 00000000..7ef60150
--- /dev/null
+++ b/src/SPA/upshot/Core.js
@@ -0,0 +1,270 @@
+/// <reference path="IntelliSense\References.js" />
+///#RESTORE (function (global, undefined)
+{
+
+ function extend(target, members) {
+ for (var member in members) {
+ target[member] = members[member];
+ }
+ return target;
+ }
+
+ function defineNamespace(name) {
+ var names = name.split(".");
+ var current = global;
+ for (var i = 0; i < names.length; i++) {
+ var ns = current[names[i]];
+ if (!ns || typeof ns !== "object") {
+ current[names[i]] = ns = {};
+ }
+ current = ns;
+ }
+ return current;
+ }
+
+ function defineClass(ctor, instanceMembers, classMembers) {
+ ctor = ctor || function () { };
+ if (instanceMembers) {
+ extend(ctor.prototype, instanceMembers);
+ }
+ if (classMembers) {
+ extend(ctor, classMembers);
+ }
+ return ctor;
+ }
+
+ function deriveClass(basePrototype, ctor, instanceMembers) {
+ var prototype = {};
+ extend(prototype, basePrototype);
+ extend(prototype, instanceMembers); // Will override like-named members on basePrototype.
+
+ ctor = ctor || function () { };
+ ctor.prototype = prototype;
+ ctor.prototype.constructor = ctor;
+ return ctor;
+ }
+
+ function classof(o) {
+ if (o === null) {
+ return "null";
+ }
+ if (o === undefined) {
+ return "undefined";
+ }
+ return Object.prototype.toString.call(o).slice(8, -1).toLowerCase();
+ }
+
+ function isArray(o) {
+ return classof(o) === "array";
+ }
+
+ function isObject(o) {
+ return classof(o) === "object";
+ }
+
+ function isValueArray(o) {
+ return isArray(o) && (o.length === 0 || !(isArray(o[0]) || isObject(o[0])));
+ }
+
+ function isDate(o) {
+ return classof(o) === "date";
+ }
+
+ function isFunction(o) {
+ return classof(o) === "function";
+ }
+
+ function isGuid(value) {
+ return (typeof value === "string") && /[a-fA-F\d]{8}-(?:[a-fA-F\d]{4}-){3}[a-fA-F\d]{12}/.test(value);
+ }
+
+ var hasOwnProperty = Object.prototype.hasOwnProperty;
+ function isEmpty(obj) {
+ if (obj === null || obj === undefined) {
+ return true;
+ }
+ for (var key in obj) {
+ if (hasOwnProperty.call(obj, key)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ var idCounter = 0;
+ function uniqueId(prefix) {
+ /// <summary>Generates a unique id (unique within the entire client session)</summary>
+ /// <param name="prefix" type="String">Optional prefix to the id</param>
+ /// <returns type="String" />
+ prefix || (prefix = "");
+ return prefix + idCounter++;
+ }
+
+ function cache(object, key, value) {
+ if (!object) {
+ return;
+ }
+ if (arguments.length === 2) {
+ // read
+ var cacheName = upshot.cacheName;
+ if (cacheName && object[cacheName]) {
+ return object[cacheName][key];
+ }
+ return null;
+ } else {
+ // write
+ if (object.nodeType !== undefined) {
+ throw "upshot.cache cannot be used with DOM elements";
+ }
+ var cacheName = upshot.cacheName || (upshot.cacheName = uniqueId("__upshot__"));
+ object[cacheName] || (object[cacheName] = function () { });
+ return object[cacheName][key] = value;
+ }
+ }
+
+ function deleteCache(object, key) {
+ var cacheName = upshot.cacheName;
+ if (cacheName && object && object[cacheName]) {
+ if (key) {
+ delete object[cacheName][key];
+ }
+ if (!key || isEmpty(object[cacheName])) {
+ delete object[cacheName];
+ }
+ }
+ }
+
+ function sameArrayContents(array1, array2) {
+ if (array1.length !== array2.length) {
+ return false;
+ } else {
+ for (var i = 0; i < array1.length; i++) {
+ if (array1[i] !== array2[i]) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ // This routine provides an equivalent of array.push(item) missing from JavaScript array.
+ function arrayRemove(array, item) {
+ var callback = upshot.isFunction(item) ? item : undefined;
+ for (var index = 0; index < array.length; index++) {
+ if (callback ? callback(array[index]) : (array[index] === item)) {
+ array.splice(index, 1);
+ return index;
+ }
+ }
+ return -1;
+ }
+
+ // pre-defined ns
+ ///#RESTORE var upshot = defineNamespace("upshot");
+
+ // pre-defined routines
+ upshot.extend = extend;
+ upshot.defineNamespace = defineNamespace;
+ upshot.defineClass = defineClass;
+ upshot.deriveClass = deriveClass;
+ upshot.classof = classof;
+ upshot.isArray = isArray;
+ upshot.isObject = isObject;
+ upshot.isValueArray = isValueArray;
+ upshot.isDate = isDate;
+ upshot.isFunction = isFunction;
+ upshot.isGuid = isGuid;
+ upshot.isEmpty = isEmpty;
+ upshot.uniqueId = uniqueId;
+ upshot.cacheName = null;
+ upshot.cache = cache;
+ upshot.deleteCache = deleteCache;
+ upshot.sameArrayContents = sameArrayContents;
+ upshot.arrayRemove = arrayRemove;
+
+ upshot.EntityState = {
+ Unmodified: "Unmodified",
+ ClientUpdated: "ClientUpdated",
+ ClientAdded: "ClientAdded",
+ ClientDeleted: "ClientDeleted",
+ ServerUpdating: "ServerUpdating",
+ ServerAdding: "ServerAdding",
+ ServerDeleting: "ServerDeleting",
+ Deleted: "Deleted",
+
+ isClientModified: function (entityState) {
+ return entityState && entityState.indexOf("Client") === 0;
+ },
+ isServerSyncing: function (entityState) {
+ return entityState && entityState.indexOf("Server") === 0;
+ },
+ isUpdated: function (entityState) {
+ return entityState && entityState.indexOf("Updat") > 0;
+ },
+ isDeleted: function (entityState) {
+ return entityState && entityState.indexOf("Delet") > 0;
+ },
+ isAdded: function (entityState) {
+ return entityState && entityState.indexOf("Add") > 0;
+ }
+ };
+
+ ///#DEBUG
+ upshot.assert = function (cond, msg) {
+ if (!cond) {
+ alert(msg || "assert is encountered!");
+ }
+ }
+ ///#ENDDEBUG
+
+ var entitySources = [];
+ function registerRootEntitySource (entitySource) {
+ entitySources.push(entitySource);
+ }
+
+ function deregisterRootEntitySource (entitySource) {
+ entitySources.splice($.inArray(entitySource, entitySources), 1);
+ }
+
+ var recomputeInProgress;
+ function triggerRecompute () {
+ if (recomputeInProgress) {
+ throw "Cannot make observable edits from within an event callback.";
+ }
+
+ try {
+ recomputeInProgress = true;
+
+ var sources = entitySources.slice();
+ $.each(sources, function (index, source) {
+ source.__recomputeDependentViews();
+ });
+
+ $.each(sources, function (index, source) {
+ if (source.__flushEntityStateChangedEvents) {
+ source.__flushEntityStateChangedEvents();
+ }
+ });
+ }
+ finally {
+ recomputeInProgress = false;
+ }
+ }
+
+ function beginChange () {
+ if (recomputeInProgress) {
+ throw "Cannot make observable edits from within an event callback.";
+ }
+ }
+
+ function endChange () {
+ triggerRecompute();
+ }
+
+ upshot.__registerRootEntitySource = registerRootEntitySource;
+ upshot.__deregisterRootEntitySource = deregisterRootEntitySource;
+ upshot.__triggerRecompute = triggerRecompute;
+ upshot.__beginChange = beginChange;
+ upshot.__endChange = endChange;
+}
+///#RESTORE )(this);
diff --git a/src/SPA/upshot/DataContext.js b/src/SPA/upshot/DataContext.js
new file mode 100644
index 00000000..baee40c0
--- /dev/null
+++ b/src/SPA/upshot/DataContext.js
@@ -0,0 +1,508 @@
+/// <reference path="IntelliSense\References.js" />
+///#RESTORE (function (global, $, upshot, undefined)
+{
+ var obs = upshot.observability;
+
+ var ctor = function (dataProvider, implicitCommitHandler, mappings) {
+
+ // support no new ctor
+ if (this._trigger === undefined) {
+ return new upshot.DataContext(dataProvider, implicitCommitHandler);
+ }
+
+ this._dataProvider = dataProvider;
+ this.__manageAssociations = true; // TODO: Make this configurable by the app. Fix unmanaged associations.
+ this._implicitCommitHandler = implicitCommitHandler;
+
+ this._eventCallbacks = {};
+ this._entitySets = {};
+
+ this._mappings = {};
+ if (mappings) {
+ this.addMapping(mappings);
+ }
+ };
+
+ function getProviderParameters(type, parameters) {
+ var result;
+ if (parameters) {
+ // first include any explicit get/submit
+ // param properties
+ result = $.extend(result, parameters[type] || {});
+
+ // next, add any additional "outer" properties
+ for (var prop in parameters) {
+ if (prop !== "get" && prop !== "submit") {
+ result[prop] = parameters[prop];
+ }
+ }
+ }
+ return result;
+ }
+
+ var instanceMembers = {
+
+ // Public methods
+
+ dispose: function () {
+ /// <summary>
+ /// Disposes the DataContext instance.
+ /// </summary>
+
+ if (this._entitySets) { // Use _entitySets as an indicator as to whether we've been disposed.
+ $.each(this._entitySets, function (index, entitySet) {
+ entitySet.__dispose();
+ });
+ this._entitySets = null;
+ }
+ },
+
+ // TODO: bind/unbind/_trigger are duplicated in EntitySource and DataContext, consider common routine.
+ bind: function (event, callback) {
+ /// <summary>
+ /// Registers the supplied callback to be called when an event is raised.
+ /// </summary>
+ /// <param name="event" type="String">
+ /// &#10;The event name.
+ /// </param>
+ /// <param name="callback" type="Function">
+ /// &#10;The callback function.
+ /// </param>
+ /// <returns type="upshot.DataContext"/>
+
+ if (typeof event === "string") {
+ var list = this._eventCallbacks[event] || (this._eventCallbacks[event] = []);
+ list.push(callback);
+ } else {
+ for (var key in event) {
+ this.bind(key, event[key]);
+ }
+ }
+ return this;
+ },
+
+ unbind: function (event, callback) {
+ /// <summary>
+ /// Deregisters the supplied callback for the supplied event.
+ /// </summary>
+ /// <param name="event" type="String">
+ /// &#10;The event name.
+ /// </param>
+ /// <param name="callback" type="Function">
+ /// &#10;The callback function to be deregistered.
+ /// </param>
+ /// <returns type="upshot.DataContext"/>
+
+ if (typeof event === "string") {
+ var list = this._eventCallbacks[event];
+ if (list) {
+ for (var i = 0, l = list.length; i < l; i++) {
+ if (list[i] === callback) {
+ list.splice(i, 1);
+ break;
+ }
+ }
+ }
+ } else {
+ for (var key in event) {
+ this.unbind(key, event[key]);
+ }
+ }
+ return this;
+ },
+
+ addMapping: function (entityType, mapping) { // TODO: Should we support CTs here too? Or take steps to disallow?
+ // TODO: Need doc comments.
+
+ if (typeof entityType === "string") {
+ var mappingT = mapping;
+ mapping = {};
+ mapping[entityType] = mappingT;
+ } else {
+ mapping = entityType;
+ }
+
+ var self = this;
+ $.each(mapping, function (entityType, mapping) {
+ if ($.isFunction(mapping)) {
+ mapping = { map: mapping };
+ }
+
+ var existingMapping = self._mappings[entityType];
+ if (!existingMapping) {
+ var entitySet = self._entitySets[entityType];
+ if (entitySet && entitySet.getEntities().length > 0) {
+ throw "Supply a mapping for a type before loading data of that type";
+ }
+ self._mappings[entityType] = { map: mapping.map, unmap: mapping.unmap };
+ } else if (existingMapping.map !== mapping.map || existingMapping.unmap !== mapping.unmap) {
+ throw "For a given type, DataContext.addMapping must be supplied the same map/unmap functions.";
+ }
+ });
+
+ return this;
+ },
+
+ getEntitySet: function (entityType) {
+ /// <summary>
+ /// Returns the EntitySet for the supplied type.
+ /// </summary>
+ /// <param name="entityType" type="String"/>
+ /// <returns type="upshot.EntitySet"/>
+
+ var entitySet = this._entitySets[entityType];
+ if (!entitySet) {
+ entitySet = this._entitySets[entityType] = new upshot.EntitySet(this, entityType);
+ }
+ return entitySet;
+ },
+
+ getEntityErrors: function () {
+ /// <summary>
+ /// Returns an array of server errors by entity, of the form [ &#123; entity: &#60;entity&#62;, error: &#60;object&#62; &#125;, ... ].
+ /// </summary>
+ /// <returns type="Array"/>
+
+ var errors = [];
+ $.each(this._entitySets, function (type, entitySet) {
+ var spliceArguments = [errors.length, 0].concat(entitySet.getEntityErrors());
+ [ ].splice.apply(errors, spliceArguments);
+ });
+ return errors;
+ },
+
+ getEntityError: function (entity) {
+ /// <summary>
+ /// Returns server errors for the supplied entity.
+ /// </summary>
+ /// <param name="entity" type="Object">
+ /// &#10;The entity for which server errors are to be returned.
+ /// </param>
+ /// <returns type="Object"/>
+
+ var error;
+ // TODO: we should get related-entitySet for an entity, then getEntityError.
+ // this will need type rationalization across all providers.
+ $.each(this._entitySets, function (unused, entitySet) {
+ error = entitySet.getEntityError(entity);
+ return !error;
+ });
+ return error;
+ },
+
+ commitChanges: function (options, success, error) {
+ /// <summary>
+ /// Initiates an asynchronous commit of any model data edits collected by this DataContext.
+ /// </summary>
+ /// <param name="success" type="Function" optional="true">
+ /// &#10;A success callback.
+ /// </param>
+ /// <param name="error" type="Function" optional="true">
+ /// &#10;An error callback with signature function(httpStatus, errorText, context).
+ /// </param>
+ /// <returns type="upshot.DataContext"/>
+
+ if (this._implicitCommitHandler) {
+ throw "Data context must be in change-tracking mode to explicitly commit changes.";
+ }
+ this._commitChanges(options, success, error);
+ return this;
+ },
+
+ revertChanges: function () {
+ /// <summary>
+ /// Reverts any edits to model data (to entities) back to original entity values.
+ /// </summary>
+ /// <returns type="upshot.DataContext"/>
+
+ $.each(this._entitySets, function (type, entitySet) {
+ entitySet.__revertChanges();
+ });
+ upshot.__triggerRecompute();
+ return this;
+ },
+
+ merge: function (entities, type, includedEntities) {
+ /// <summary>Merges data into the cache</summary>
+ /// <param name="entities" type="Array">The array of entities to add or merge into the cache</param>
+ /// <param name="type" type="String">The type of the entities to be merge into the cache. This parameter can be null/undefined when no entities are supplied</param>
+ /// <param name="includedEntities" type="Array">An additional array of entities (possibly related) to add or merge into the cache. These entities will not be returned from this function. This parameter is optional</param>
+ /// <returns type="Array">The array of entities with newly merged values</returns>
+
+ var self = this;
+ includedEntities = includedEntities || {};
+
+ $.each(entities, function (unused, entity) {
+ // apply type info to the entity instances
+ // TODO: Do we want this to go through the compatibility layer?
+ obs.setProperty(entity, "__type", type);
+
+ self.__flatten(entity, type, includedEntities);
+ });
+
+ $.each(includedEntities, function (type, entities) {
+ $.each(entities, function (unused, entity) {
+ // apply type info to the entity instances
+ // TODO: Do we want this to go through the compatibility layer?
+ obs.setProperty(entity, "__type", type);
+ });
+
+ var entitySet = self.getEntitySet(type);
+ entitySet.__loadEntities(entities);
+ });
+
+ var entitySet = type && this.getEntitySet(type),
+ mergedEntities = entitySet ? entitySet.__loadEntities(entities) : [];
+
+ upshot.__triggerRecompute();
+
+ return mergedEntities;
+ },
+
+ // TODO -- We have no mechanism to similarly clear data sources.
+ //// clear: function () {
+ //// $.each(this._entitySets, function (type, entitySet) {
+ //// entitySet.__clear();
+ //// });
+ //// },
+
+ // Internal methods
+
+ // recursively visit the specified entity and its associations, accumulating all
+ // associated entities to the included entities collection
+ __flatten: function (entity, entityType, includedEntities) {
+ var self = this;
+
+ $.each(upshot.metadata.getProperties(entity, entityType, true), function (index, prop) {
+ var value = obs.getProperty(entity, prop.name);
+ if (value) {
+ if (prop.association) {
+ var associatedEntities = upshot.isArray(value) ? value : [value],
+ associatedEntityType = prop.type,
+ entities = includedEntities[associatedEntityType] || (includedEntities[associatedEntityType] = []);
+
+ $.each(associatedEntities, function (inner_index, associatedEntity) {
+ // add the associated entity
+ var identity = upshot.EntitySet.__getIdentity(associatedEntity, associatedEntityType);
+
+ if (!entities.identityMap) {
+ entities.identityMap = {};
+ }
+ if (!entities.identityMap[identity]) {
+ // add the entity and recursively flatten it
+ entities.identityMap[identity] = true;
+ entities.push(associatedEntity);
+ self.__flatten(associatedEntity, associatedEntityType, includedEntities);
+ }
+ ///#DEBUG
+ // TODO: For unmanaged associations, where is it that we should fix up internal reference
+ // refer only to the atomized entity for a given identity?
+ upshot.assert(self.__manageAssociations);
+ ///#ENDDEBUG
+ });
+ }
+ }
+ });
+ },
+
+ __load: function (options, success, error) {
+
+ var dataProvider = this._dataProvider,
+ self = this,
+ onSuccess = function (result) {
+ // add metadata if specified
+ if (result.metadata) {
+ upshot.metadata(result.metadata);
+ }
+
+ // determine the result type
+ var entityType = result.type || options.entityType;
+ if (!entityType) {
+ throw "Unable to determine entity type.";
+ }
+
+ var entities = $.map(result.entities, function (entity) {
+ return self._mapEntity(entity, entityType);
+ });
+ var includedEntities;
+ if (result.includedEntities) {
+ includedEntities = {};
+ $.each(result.includedEntities, function (type, entities) {
+ includedEntities[type] = $.map(entities, function (entity) {
+ return self._mapEntity(entity, type);
+ });
+ });
+ }
+
+ var mergedEntities = self.merge(entities, entityType, includedEntities);
+
+ success.call(self, self.getEntitySet(entityType), mergedEntities, result.totalCount);
+ },
+ onError = function (httpStatus, errorText, context) {
+ error.call(self, httpStatus, errorText, context);
+ };
+
+ var getParameters = getProviderParameters("get", options.providerParameters);
+
+ dataProvider.get(getParameters, options.queryParameters, onSuccess, onError);
+ },
+
+ __queueImplicitCommit: function () {
+ if (this._implicitCommitHandler) {
+ // when in implicit commit mode, we group all implicit commits within
+ // a single thread of execution by queueing a timer callback that expires
+ // immediately.
+ if (!this._implicitCommitQueued) {
+ this._implicitCommitQueued = true;
+
+ var self = this;
+ setTimeout(function () {
+ self._implicitCommitQueued = false;
+ self._implicitCommitHandler();
+ }, 0);
+ }
+ }
+ },
+
+ // Private methods
+
+ _trigger: function (eventType) {
+ var list = this._eventCallbacks[eventType];
+ if (list) {
+ var args = Array.prototype.slice.call(arguments, 1);
+ // clone the list to be robust against bind/unbind during callback
+ list = list.slice(0);
+ for (var i = 0, l = list.length; i < l; i++) {
+ list[i].apply(this, args);
+ }
+ }
+ return this;
+ },
+
+ _submitChanges: function (options, editedEntities, success, error) {
+
+ this._trigger("commitStart");
+
+ var edits = $.map(editedEntities, function (editedEntity) {
+ return editedEntity.entitySet.__getEntityEdit(editedEntity.entity);
+ });
+
+ $.each(edits, function (index, edit) { edit.updateEntityState(); });
+
+ var self = this;
+ var mapChange = function (entity, entityType, result) {
+ var change = {
+ Id: result.Id,
+ Operation: result.Operation
+ };
+ if (result.Operation !== 4) { // Only add/update operations require mapped entity.
+ change.Entity = self._mapEntity(result.Entity, entityType);
+ }
+ return change;
+ };
+
+ var unmapChange = function (index, entityType, operation) {
+ var change = {
+ Id: index.toString(),
+ Operation: operation.Operation,
+ Entity: operation.Entity,
+ OriginalEntity: operation.OriginalEntity
+ };
+ if (operation.Operation !== 4) { // Delete operations don't require unmapping
+ var unmap = (self._mappings[entityType] || {}).unmap || obs.unmap;
+ change.Entity = unmap(operation.Entity, entityType);
+ }
+ return change;
+ };
+
+ var changeSet = $.map(edits, function (edit, index) {
+ return unmapChange(index, edit.entityType, edit.operation);
+ });
+
+ var onSuccess = function (submitResult) {
+ // all updates in the changeset where successful
+ $.each(edits, function (index, edit) {
+ edit.succeeded(mapChange(edit.storeEntity, edit.entityType, submitResult[index]));
+ });
+ upshot.__triggerRecompute();
+ self._trigger("commitSuccess", submitResult);
+ if (success) {
+ success.call(self, submitResult);
+ }
+ },
+ onError = function (httpStatus, errorText, context, submitResult) {
+ // one or more updates in the changeset failed
+ $.each(edits, function (index, edit) {
+ if (submitResult) {
+ // if a submitResult was provided, we use that data in the
+ // completion of the edit
+ var editResult = submitResult[index];
+ if (editResult.Error) {
+ edit.failed(editResult.Error);
+ } else {
+ // even though there were failures in the changeset,
+ // this particular edit is marked as completed, so
+ // we need to accept changes for it
+ edit.succeeded(mapChange(edit.storeEntity, edit.entityType, editResult));
+ }
+ } else {
+ // if we don't have a submitResult, we still need to state
+ // transition the edit properly
+ edit.failed(null);
+ }
+ });
+
+ upshot.__triggerRecompute();
+ self._trigger("commitError", httpStatus, errorText, context, submitResult);
+ if (error) {
+ error.call(self, httpStatus, errorText, context, submitResult);
+ }
+ };
+
+ var submitParameters = getProviderParameters("submit", options.providerParameters);
+
+ this._dataProvider.submit(submitParameters, changeSet, onSuccess, onError);
+ },
+
+ _commitChanges: function (options, success, error) {
+ var editedEntities = [];
+ $.each(this._entitySets, function (type, entitySet) {
+ var entities = $.map(entitySet.__getEditedEntities(), function (entity) {
+ return { entitySet: entitySet, entity: entity };
+ });
+ [ ].push.apply(editedEntities, entities);
+ });
+
+ this._submitChanges(options, editedEntities, success, error);
+ upshot.__triggerRecompute();
+ },
+
+ _mapEntity: function (data, entityType) {
+ return this._map(data, entityType, true);
+ },
+
+ _map: function (data, entityType, isObject) {
+ if (isObject || upshot.isObject(data)) {
+ var map = (this._mappings[entityType] || {}).map;
+ if (map) {
+ // Don't pass "entityType"/"mapNested" as we do below for obs.map.
+ // This would pollute the signature for app-supplied map functions (especially
+ // when ctors are supplied).
+ return new map(data); // Use "new" here to allow ctors to be passed as map functions.
+ }
+ }
+
+ // The "map" function provided by the observability layer takes a function
+ // to map nested objects, so we take advantage of app-supplied mapping functions.
+ var self = this,
+ mapNested = function (data, entityType) {
+ return self._map(data, entityType);
+ };
+ return obs.map(data, entityType, mapNested);
+ }
+ };
+
+ upshot.DataContext = upshot.defineClass(ctor, instanceMembers);
+
+}
+///#RESTORE )(this, jQuery, upshot);
diff --git a/src/SPA/upshot/DataProvider.OData.js b/src/SPA/upshot/DataProvider.OData.js
new file mode 100644
index 00000000..3dc27b16
--- /dev/null
+++ b/src/SPA/upshot/DataProvider.OData.js
@@ -0,0 +1,172 @@
+/// <reference path="IntelliSense\References.js" />
+///#RESTORE (function (global, $, upshot, undefined)
+{
+ function pad(count, value) {
+ var str = "0000" + value;
+ return str.slice(str.length - count);
+ }
+
+ function formatDateTime(date) {
+ return "datetime" +
+ "'" + pad(4, date.getUTCFullYear()) +
+ "-" + pad(2, date.getUTCMonth() + 1) +
+ "-" + pad(2, date.getUTCDate()) +
+ "T" + pad(2, date.getUTCHours()) +
+ ":" + pad(2, date.getUTCMinutes()) +
+ ":" + pad(2, date.getUTCSeconds()) + "'";
+ }
+
+ function getQueryResult(getResult) {
+ var entities = getResult.results,
+ resultType = entities.length && entities[0].__metadata.type;
+
+ var metadata;
+ if (resultType) {
+ metadata = {};
+ metadata[resultType] = {
+ key: ["__metadata.uri"]
+ };
+ }
+
+ var count = getResult.__count,
+ totalCount = count === undefined ? null : +count;
+
+ return {
+ type: resultType,
+ metadata: metadata,
+ entities: entities,
+ totalCount: totalCount
+ };
+ }
+
+ var instanceMembers = {
+
+ // Public methods
+
+ get: function (parameters, queryParameters, success, error) {
+ /// <summary>
+ /// Asynchronously gets data from the server using the specified parameters
+ /// </summary>
+ /// <param name="parameters" type="String">The get parameters</param>
+ /// <param name="queryParameters" type="Object">An object where each property is a query to pass to the operation. This parameter is optional.</param>
+ /// <param name="success" type="Function">Optional success callback</param>
+ /// <param name="error" type="Function">Optional error callback</param>
+ /// <returns type="Promise">A Promise representing the result of the load operation</returns>
+
+ var operation, operationParameters;
+ if (parameters) {
+ operation = parameters.operationName;
+ operationParameters = parameters.operationParameters;
+ }
+
+ if ($.isFunction(operationParameters)) {
+ success = operationParameters;
+ error = queryParameters;
+ }
+
+ var self = this;
+
+ // $.map applied to objects is supported in jQuery >= 1.6. Our current baseline is jQuery 1.5
+ var parameterStrings = [];
+ $.each($.extend({}, operationParameters, upshot.ODataDataProvider.getODataQueryParameters(queryParameters)), function (key, value) {
+ parameterStrings.push(key.toString() + "=" + value.toString());
+ });
+ var queryString = parameterStrings.length ? ("?" + parameterStrings.join("&")) : "";
+
+ // Invoke the query
+ OData.read(upshot.DataProvider.normalizeUrl(parameters.url) + operation + queryString,
+ function (result) {
+ if (success) {
+ arguments[0] = getQueryResult(arguments[0]);
+ success.apply(self, arguments);
+ }
+ },
+ function (reason) {
+ if (error) {
+ error.call(self, -1, reason.message, reason);
+ }
+ }
+ );
+ },
+
+ submit: function () {
+ throw "Saving edits through the OData data provider is not supported.";
+ }
+ };
+
+ var classMembers = {
+ getODataQueryParameters: function (query) {
+ query = query || {};
+ var queryParameters = {};
+
+ // filters -> $filter
+ if (query.filters && query.filters.length) {
+ var filterParameter = "",
+ applyOperator = function (property, operator, value) {
+ if (typeof value === "string") {
+ if (upshot.isGuid(value)) {
+ value = "guid'" + value + "'";
+ } else {
+ value = "'" + value + "'";
+ }
+ } else if (upshot.isDate(value)) {
+ value = formatDateTime(value);
+ }
+
+ switch (operator) {
+ case "<": return property + " lt " + value;
+ case "<=": return property + " le " + value;
+ case "==": return property + " eq " + value;
+ case "!=": return property + " ne " + value;
+ case ">=": return property + " ge " + value;
+ case ">": return property + " gt " + value;
+ case "StartsWith": return "startswith(" + property + "," + value + ") eq true";
+ case "EndsWith": return "endswith(" + property + "," + value + ") eq true";
+ case "Contains": return "substringof(" + value + "," + property + ") eq true";
+ default: throw "The operator '" + operator + "' is not supported.";
+ }
+ };
+
+ $.each(query.filters, function (index, filter) {
+ if (filterParameter) {
+ filterParameter += " and ";
+ }
+ filterParameter += applyOperator(filter.property, filter.operator, filter.value);
+ });
+
+ queryParameters.$filter = filterParameter;
+ }
+
+ // sort -> $orderby
+ if (query.sort && query.sort.length) {
+ var formatSort = function (sort) {
+ return !!sort.descending ? (sort.property + " desc") : sort.property;
+ };
+ queryParameters.$orderby = $.map(query.sort, function (sort, index) {
+ return formatSort(sort);
+ }).join();
+ }
+
+ // skip -> $skip
+ if (query.skip) {
+ queryParameters.$skip = query.skip;
+ }
+
+ // take -> $top
+ if (query.take) {
+ queryParameters.$top = query.take;
+ }
+
+ // includeTotalCount -> $inlinecount
+ if (query.includeTotalCount) {
+ queryParameters.$inlinecount = "allpages";
+ }
+
+ return queryParameters;
+ }
+ }
+
+ upshot.ODataDataProvider = upshot.defineClass(null, instanceMembers, classMembers);
+
+}
+///#RESTORE )(this, jQuery, upshot);
diff --git a/src/SPA/upshot/DataProvider.js b/src/SPA/upshot/DataProvider.js
new file mode 100644
index 00000000..96820b0e
--- /dev/null
+++ b/src/SPA/upshot/DataProvider.js
@@ -0,0 +1,173 @@
+/// <reference path="IntelliSense\References.js" />
+///#RESTORE (function (global, $, upshot, undefined)
+{
+ function getQueryResult(getResult, wrappedResult) {
+ var entities, totalCount;
+
+ if (wrappedResult) {
+ entities = getResult.Results;
+ totalCount = getResult.TotalCount;
+ }
+ else {
+ entities = getResult;
+ }
+
+ return {
+ entities: upshot.isArray(entities) ? entities : [entities],
+ totalCount: totalCount
+ };
+ }
+
+ var instanceMembers = {
+
+ // Public methods
+
+ get: function (parameters, queryParameters, success, error) {
+ /// <summary>
+ /// Asynchronously gets data from the server using the specified parameters
+ /// </summary>
+ /// <param name="parameters" type="String">The get parameters</param>
+ /// <param name="queryParameters" type="Object">An object where each property is a query to pass to the operation. This parameter is optional.</param>
+ /// <param name="success" type="Function">Optional success callback</param>
+ /// <param name="error" type="Function">Optional error callback</param>
+ /// <returns type="Promise">A Promise representing the result of the load operation</returns>
+
+ var operation, operationParameters;
+ if (parameters) {
+ operation = parameters.operationName;
+ operationParameters = parameters.operationParameters;
+ }
+
+ if ($.isFunction(operationParameters)) {
+ success = operationParameters;
+ error = queryParameters;
+ }
+
+ var self = this;
+
+ // set up the request parameters
+ var url = upshot.DataProvider.normalizeUrl(parameters.url) + operation;
+ var oDataQueryParams = upshot.ODataDataProvider.getODataQueryParameters(queryParameters);
+ var data = $.extend({}, operationParameters, oDataQueryParams);
+ var wrappedResult = oDataQueryParams.$inlinecount == "allpages";
+
+ // invoke the query
+ $.ajax({
+ url: url,
+ data: data,
+ success: success && function () {
+ arguments[0] = getQueryResult(arguments[0], wrappedResult);
+ success.apply(self, arguments);
+ },
+ error: error && function (jqXHR, statusText, errorText) {
+ error.call(self, jqXHR.status, self._parseErrorText(jqXHR.responseText) || errorText, jqXHR);
+ },
+ dataType: "json"
+ });
+ },
+
+ submit: function (parameters, changeSet, success, error) {
+ /// <summary>
+ /// Asynchronously submits the specified changeset
+ /// </summary>
+ /// <param name="parameters" type="String">The submit parameters</param>
+ /// <param name="changeSet" type="Object">The changeset to submit</param>
+ /// <param name="success" type="Function">Optional success callback</param>
+ /// <param name="error" type="Function">Optional error callback</param>
+ /// <returns type="Promise">A Promise representing the result of the post operation</returns>
+
+ $.each(changeSet, function (index, changeSetEntry) {
+ switch (changeSetEntry.Operation) {
+ case 2: // insert
+ changeSetEntry.Operation = 1;
+ break;
+ case 3: // update
+ changeSetEntry.Operation = 2;
+ break;
+ case 4: // delete
+ changeSetEntry.Operation = 3;
+ break;
+ };
+ });
+
+ var self = this,
+ encodedChangeSet = JSON.stringify(changeSet);
+
+ $.ajax({
+ url: upshot.DataProvider.normalizeUrl(parameters.url) + "Submit",
+ contentType: "application/json",
+ data: encodedChangeSet,
+ dataType: "json",
+ type: "POST",
+ success: (success || error) && function (data, statusText, jqXHR) {
+ var result = data;
+ var hasErrors = false;
+ if (result) {
+ // transform to Error property
+ $.each(result, function (index, changeSetEntry) {
+ // even though upshot currently doesn't support reporting of concurrency conflicts,
+ // we must still identify such failures
+ $.each(["ConflictMembers", "ValidationErrors", "IsDeleteConflict"], function (index, property) {
+ if (changeSetEntry.hasOwnProperty(property)) {
+ changeSetEntry.Error = changeSetEntry.Error || {};
+ changeSetEntry.Error[property] = changeSetEntry[property];
+ hasErrors = true;
+ }
+ });
+ });
+ }
+
+ if (!hasErrors) {
+ if (success) {
+ success.call(self, result);
+ }
+ } else if (error) {
+ var errorText = "Submit failed.";
+ if (result) {
+ for (var i = 0; i < result.length; ++i) {
+ var validationError = (result[i].ValidationErrors && result[i].ValidationErrors[0] && result[i].ValidationErrors[0].Message);
+ if (validationError) {
+ errorText = validationError;
+ break;
+ }
+ }
+ }
+ error.call(self, jqXHR.status, errorText, jqXHR, result);
+ }
+ },
+ error: error && function (jqXHR, statusText, errorText) {
+ error.call(self, jqXHR.status, self._parseErrorText(jqXHR.responseText) || errorText, jqXHR);
+ }
+ });
+ },
+
+ _parseErrorText: function (responseText) {
+ var match = /Exception]: (.+)\r/g.exec(responseText);
+ if (match && match[1]) {
+ return match[1];
+ }
+ if (/^{.*}$/g.test(responseText)) {
+ var error = JSON.parse(responseText);
+ // TODO: error.Message returned by DataController
+ // Does ErrorMessage check still necessary?
+ if (error.ErrorMessage) {
+ return error.ErrorMessage;
+ } else if (error.Message) {
+ return error.Message;
+ }
+ }
+ }
+ }
+
+ var classMembers = {
+ normalizeUrl: function (url) {
+ if (url && url.substring(url.length - 1) !== "/") {
+ return url + "/";
+ }
+ return url;
+ }
+ }
+
+ upshot.DataProvider = upshot.defineClass(null, instanceMembers, classMembers);
+}
+///#RESTORE )(this, jQuery, upshot);
diff --git a/src/SPA/upshot/DataProvider.ria.js b/src/SPA/upshot/DataProvider.ria.js
new file mode 100644
index 00000000..078e0e79
--- /dev/null
+++ b/src/SPA/upshot/DataProvider.ria.js
@@ -0,0 +1,251 @@
+/// <reference path="IntelliSense\References.js" />
+///#RESTORE (function (global, $, upshot, undefined)
+{
+ function transformQuery(query) {
+ var queryParameters = {};
+
+ // filters -> $where
+ if (query.filters && query.filters.length) {
+ var whereParameter = "",
+ applyOperator = function (property, operator, value) {
+ if (typeof value === "string") {
+ if (upshot.isGuid(value)) {
+ value = "Guid(" + value + ")";
+ } else {
+ value = '"' + value + '"';
+ }
+ } else if (upshot.isDate(value)) {
+ // DomainService expects ticks; js Date.getTime() gives ms since epoch
+ value = "DateTime(" + (value.getTime() * 10000 + 621355968000000000) + ")";
+ }
+
+ switch (operator) {
+ case "<":
+ case "<=":
+ case "==":
+ case "!=":
+ case ">=":
+ case ">": return property + operator + value;
+ case "StartsWith":
+ case "EndsWith":
+ case "Contains": return property + "." + operator + "(" + value + ")";
+ default: throw "The operator '" + operator + "' is not supported.";
+ }
+ };
+
+ $.each(query.filters, function (index, filter) {
+ if (whereParameter) {
+ whereParameter += " AND ";
+ }
+ whereParameter += applyOperator(filter.property, filter.operator, filter.value);
+ });
+
+ queryParameters.$where = whereParameter;
+ }
+
+ // sort -> $orderby
+ if (query.sort && query.sort.length) {
+ var formatSort = function (sort) {
+ return !!sort.descending ? (sort.property + " desc") : sort.property;
+ };
+ queryParameters.$orderby = $.map(query.sort, function (sort, index) {
+ return formatSort(sort);
+ }).join();
+ }
+
+ // skip -> $skip
+ if (query.skip) {
+ queryParameters.$skip = query.skip;
+ }
+
+ // take -> $take
+ if (query.take) {
+ queryParameters.$take = query.take;
+ }
+
+ // includeTotalCount -> $includeTotalCount
+ if (query.includeTotalCount) {
+ queryParameters.$includeTotalCount = query.includeTotalCount;
+ }
+
+ return queryParameters;
+ }
+
+ function transformParameters(parameters) {
+ // perform any required transformations on the specified parameters
+ // before invoking the service, for example json serializing arrays
+ // and other complex parameters.
+ if (parameters) {
+ $.each(parameters || {}, function (key, value) {
+ if ($.isArray(value)) {
+ // json serialize arrays since this is the format the json
+ // endpoint expects.
+ parameters[key] = JSON.stringify(value);
+ }
+ });
+ }
+
+ return parameters;
+ }
+
+ function getQueryResult(getResult) {
+ var resultKey;
+ $.each(getResult, function (key) {
+ if (/Result$/.test(key)) {
+ resultKey = key;
+ return false;
+ }
+ });
+ var result = getResult[resultKey];
+
+ // process the metadata
+ var metadata = {};
+ $.each(result.Metadata, function (unused, metadataForType) {
+ metadata[metadataForType.type] = {
+ key: metadataForType.key,
+ fields: metadataForType.fields,
+ rules: metadataForType.rules,
+ messages: metadataForType.messages
+ };
+ });
+
+ var includedEntities;
+ if (result.IncludedResults) {
+ // group included entities by type
+ includedEntities = {};
+ $.each(result.IncludedResults, function (unused, entity) {
+ var entityType = entity.__type;
+ var entities = includedEntities[entityType] || (includedEntities[entityType] = []);
+ entities.push(entity);
+ });
+ }
+
+ return {
+ type: result.Metadata[0].type,
+ metadata: metadata,
+ entities: result.RootResults,
+ includedEntities: includedEntities,
+ totalCount: result.TotalCount || 0
+ };
+ }
+
+ var instanceMembers = {
+
+ // Public methods
+
+ get: function (parameters, queryParameters, success, error) {
+ /// <summary>
+ /// Asynchronously gets data from the server using the specified parameters
+ /// </summary>
+ /// <param name="parameters" type="String">The get parameters</param>
+ /// <param name="queryParameters" type="Object">An object where each property is a query to pass to the operation. This parameter is optional.</param>
+ /// <param name="success" type="Function">Optional success callback</param>
+ /// <param name="error" type="Function">Optional error callback</param>
+ /// <returns type="Promise">A Promise representing the result of the load operation</returns>
+
+ var operation, operationParameters;
+ if (parameters) {
+ operation = parameters.operationName;
+ operationParameters = parameters.operationParameters;
+ }
+
+ if ($.isFunction(operationParameters)) {
+ success = operationParameters;
+ error = queryParameters;
+ }
+
+ var self = this;
+
+ // Invoke the query
+ $.ajax({
+ url: upshot.DataProvider.normalizeUrl(parameters.url) + "json/" + operation,
+ data: $.extend({}, transformParameters(operationParameters), transformQuery(queryParameters || {})),
+ success: success && function () {
+ arguments[0] = getQueryResult(arguments[0]);
+ success.apply(self, arguments);
+ },
+ error: error && function (jqXHR, statusText, errorText) {
+ error.call(self, jqXHR.status, self._parseErrorText(jqXHR.responseText) || errorText, jqXHR);
+ },
+ dataType: "json"
+ });
+ },
+
+ submit: function (parameters, changeSet, success, error) {
+ /// <summary>
+ /// Asynchronously submits the specified changeset
+ /// </summary>
+ /// <param name="parameters" type="String">The submit parameters</param>
+ /// <param name="changeSet" type="Object">The changeset to submit</param>
+ /// <param name="success" type="Function">Optional success callback</param>
+ /// <param name="error" type="Function">Optional error callback</param>
+ /// <returns type="Promise">A Promise representing the result of the post operation</returns>
+
+ var self = this,
+ encodedChangeSet = JSON.stringify({ changeSet: changeSet });
+
+ $.ajax({
+ url: upshot.DataProvider.normalizeUrl(parameters.url) + "json/SubmitChanges",
+ contentType: "application/json",
+ data: encodedChangeSet,
+ dataType: "json",
+ type: "POST",
+ success: (success || error) && function (data, statusText, jqXHR) {
+ var result = data["SubmitChangesResult"];
+ var hasErrors = false;
+ if (result) {
+ // transform to Error property
+ $.each(result, function (index, changeSetEntry) {
+ // even though upshot currently doesn't support reporting of concurrency conflicts,
+ // we must still identify such failures
+ $.each(["ConflictMembers", "ValidationErrors", "IsDeleteConflict"], function (index, property) {
+ if (changeSetEntry.hasOwnProperty(property)) {
+ changeSetEntry.Error = changeSetEntry.Error || {};
+ changeSetEntry.Error[property] = changeSetEntry[property];
+ hasErrors = true;
+ }
+ });
+ });
+ }
+
+ if (!hasErrors) {
+ if (success) {
+ success.call(self, result);
+ }
+ } else if (error) {
+ var errorText = "Submit failed.";
+ if (result) {
+ for (var i = 0; i < result.length; ++i) {
+ var validationError = (result[i].ValidationErrors && result[i].ValidationErrors[0] && result[i].ValidationErrors[0].Message);
+ if (validationError) {
+ errorText = validationError;
+ break;
+ }
+ }
+ }
+ error.call(self, jqXHR.status, errorText, jqXHR, result);
+ }
+ },
+ error: error && function (jqXHR, statusText, errorText) {
+ error.call(self, jqXHR.status, self._parseErrorText(jqXHR.responseText) || errorText, jqXHR);
+ }
+ });
+ },
+
+ _parseErrorText: function (responseText) {
+ var match = /Exception]: (.+)\r/g.exec(responseText);
+ if (match && match[1]) {
+ return match[1];
+ }
+ if (/^{.*}$/g.test(responseText)) {
+ var error = JSON.parse(responseText);
+ if (error.ErrorMessage) {
+ return error.ErrorMessage;
+ }
+ }
+ }
+ }
+
+ upshot.riaDataProvider = upshot.defineClass(null, instanceMembers);
+}
+///#RESTORE )(this, jQuery, upshot);
diff --git a/src/SPA/upshot/DataSource.js b/src/SPA/upshot/DataSource.js
new file mode 100644
index 00000000..5f2034d3
--- /dev/null
+++ b/src/SPA/upshot/DataSource.js
@@ -0,0 +1,168 @@
+/// <reference path="IntelliSense\References.js" />
+///#RESTORE (function (global, $, upshot, undefined)
+{
+ var base = upshot.EntityView.prototype;
+
+ var obs = upshot.observability;
+ var queryOptions = { paging: "setPaging", sort: "setSort", filter: "setFilter" };
+
+ var ctor = function (options) {
+
+ if (options && options.result && options.result.length !== 0) {
+ throw "NYI -- Currently, \"result\" array must be empty to bind to a data source.";
+ }
+
+ this._skip = null;
+ this._take = null;
+ this._includeTotalCount = false;
+ this._lastRefreshTotalEntityCount = 0;
+ this._allowRefreshWithEdits = options && !!options.allowRefreshWithEdits;
+
+ if (options) {
+ var self = this;
+ $.each(options, function (key, value) {
+ if (queryOptions[key]) {
+ self[queryOptions[key]](value);
+ }
+ });
+ }
+
+ base.constructor.apply(this, arguments);
+
+ // Events specific to DataSource
+ this._bindFromOptions(options, [ "refreshStart", "refreshSuccess", "refreshError" ]);
+ };
+
+ var instanceMembers = {
+
+ // Public methods
+
+ // TODO -- These query set-* methods should be consolidated, passing a "settings" parameter.
+ // That way, we can issue a single "needs refresh" event when the client updates the query settings.
+ // TODO -- Changing query options should trigger "results stale".
+
+ setSort: function (sort) {
+ throw "Unreachable"; // Abstract/pure virtual method.
+ },
+
+ setFilter: function (filter) {
+ throw "Unreachable"; // Abstract/pure virtual method.
+ },
+
+ setPaging: function (paging) {
+ /// <summary>
+ /// Establishes the paging specification that is to be applied when loading model data.
+ /// </summary>
+ /// <param name="paging">
+ /// &#10;The paging specification to be applied when loading model data.
+ /// &#10;Should be supplied as an object of the form &#123; skip: &#60;number&#62;, take: &#60;number&#62;, includeTotalCount: &#60;bool&#62; &#125;. All properties on this object are optional.
+ /// &#10;When supplied as null or undefined, the paging specification for this DataSource is cleared.
+ /// </param>
+ /// <returns type="upshot.DataSource"/>
+
+ paging = paging || {};
+ this._skip = paging.skip;
+ this._take = paging.take;
+ this._includeTotalCount = !!paging.includeTotalCount;
+ return this;
+ },
+
+ getTotalEntityCount: function () {
+ /// <summary>
+ /// Returns the total entity count from the last refresh operation on this DataSource. This count will differ from DataSource.getEntities().length when a filter or paging specification is applied to this DataSource.
+ /// </summary>
+ /// <returns type="Number"/>
+
+ // NOTE: We had been updating this to reflect internal, client-only adds, but this doesn't
+ // generalize nicely. For instance, a RemoteDataSource might have server-only logic that
+ // determines whether an added entity should be included in a filtered query result.
+ // TODO: Revisit this conclusion.
+ return this._lastRefreshTotalEntityCount;
+ },
+
+ refresh: function (options) {
+ throw "Unreachable"; // Abstract/pure virtual method.
+ },
+
+ reset: function () {
+ /// <summary>
+ /// Empties the result array for this DataSource (that is, the array returned by DataSource.getEntities()). The result array can be repopulated using DataSource.refresh().
+ /// </summary>
+ /// <returns type="Number"/>
+
+ this._applyNewQueryResult([]);
+ return this;
+ },
+
+
+ // Private methods
+
+ // acceptable filter parameter
+ // { property: "Id", operator: "==", value: 1 } // default operator is "=="
+ // and an array of such
+ _normalizeFilters: function (filter) {
+ filter = upshot.isArray(filter) ? filter : [filter];
+ var filters = [];
+ for (var i = 0; i < filter.length; i++) {
+ var filterPart = filter[i];
+ if (filterPart) {
+ if (!$.isFunction(filterPart)) {
+ filterPart.operator = filterPart.operator || "==";
+ }
+ filters.push(filterPart);
+ }
+ }
+ return filters;
+ },
+
+ _verifyOkToRefresh: function () {
+ if (!this._allowRefreshWithEdits) {
+ var self = this;
+ $.each(obs.asArray(this._clientEntities), function (unused, entity) {
+ if (self.getEntityState(entity) !== upshot.EntityState.Unmodified) {
+ throw "Refreshing this DataSource will potentially remove unsaved entities. Such entities might encounter errors during save, and your app should have UI to view such errors. Either disallow DataSource.refresh() with edits or build error UI and suppress this exception with the 'allowRefreshWithEdits' DataSource option.";
+ }
+ });
+ }
+ },
+
+ _completeRefresh: function (entities, totalCount, success) {
+ if (this._applyNewQueryResult(entities, totalCount)) {
+ upshot.__triggerRecompute();
+ }
+
+ var newClientEntities = obs.asArray(this._clientEntities),
+ newTotalCount = this._lastRefreshTotalEntityCount;
+ this._trigger("refreshSuccess", newClientEntities, newTotalCount);
+ if ($.isFunction(success)) {
+ success.call(this, newClientEntities, newTotalCount);
+ }
+ },
+
+ _failRefresh: function (httpStatus, errorText, context, fail) {
+ this._trigger("refreshError", httpStatus, errorText, context);
+ if ($.isFunction(fail)) {
+ fail.call(this, httpStatus, errorText, context);
+ }
+ },
+
+ _applyNewQueryResult: function (entities, totalCount) {
+ this._lastRefreshTotalEntityCount = totalCount;
+
+ var sameEntities = upshot.sameArrayContents(obs.asArray(this._clientEntities), entities);
+ if (!sameEntities) {
+ // Update our client entities.
+ var oldEntities = obs.asArray(this._clientEntities).slice();
+ obs.refresh(this._clientEntities, entities);
+ this._trigger("arrayChanged", "replaceAll", { oldItems: oldEntities, newItems: obs.asArray(this._clientEntities) });
+ return true;
+ } else {
+ return false;
+ }
+ }
+ };
+
+ upshot.DataSource = upshot.deriveClass(base, ctor, instanceMembers);
+
+}
+///#RESTORE )(this, jQuery, upshot);
diff --git a/src/SPA/upshot/EntitySet.js b/src/SPA/upshot/EntitySet.js
new file mode 100644
index 00000000..36141bea
--- /dev/null
+++ b/src/SPA/upshot/EntitySet.js
@@ -0,0 +1,1491 @@
+/// <reference path="IntelliSense\References.js" />
+///#RESTORE (function (global, $, upshot, undefined)
+{
+ var base = upshot.EntitySource.prototype;
+
+ var obs = upshot.observability;
+
+ var tokenizePath = function (obj, path) {
+ var evalTokens = function (obj, tokens) {
+ if (tokens.length === 0) return [obj];
+ var objs = evalTokens(upshot.isArray(obj) ? obj[tokens.shift()] : obs.getProperty(obj, tokens.shift()), tokens);
+ objs.unshift(obj);
+ return objs;
+ };
+
+ var tokens = path.replace(/\]|\(|\)/g, "").replace(/\[/g, ".").split(".");
+ return { tokens: tokens, objs: evalTokens(obj, tokens.slice()) };
+ }
+
+ var ctor = function (dataContext, entityType) {
+
+ this._dataContext = dataContext;
+ this._entityType = entityType;
+
+ this._callbacks = {};
+ this._serverEntities = [];
+ this._entityStates = {};
+ this._addedEntities = [];
+ this._errors = [];
+ this._associatedEntitiesViews = {};
+ this._tracked = {};
+ this._tracker = null;
+ this._deferredEntityStateChangeEvents = [];
+
+ base.constructor.call(this);
+
+ upshot.__registerRootEntitySource(this);
+ };
+
+ var instanceMembers = {
+
+ // Public methods
+
+ dispose: function () {
+ throw "EntitySets should only be disposed by their DataContext.";
+ },
+
+ getEntityState: function (entity) {
+ /// <summary>
+ /// Returns the EntityState for the supplied entity.
+ /// </summary>
+ /// <param name="entity" type="Object">
+ /// &#10;The entity for which EntityState will be returned.
+ /// </param>
+ /// <returns type="upshot.EntityState"/>
+
+ var id = this.getEntityId(entity);
+ return id === null ? null : this._entityStates[id];
+ },
+
+ getEntityId: function (entity) {
+ /// <summary>
+ /// Returns an identifier for the supplied entity.
+ /// </summary>
+ /// <param name="entity" type="Object"/>
+ /// <returns type="String"/>
+
+ var addedEntity = this._getAddedEntityFromEntity(entity);
+ if (addedEntity) {
+ return addedEntity.clientId;
+ }
+
+ try { // This entity might not have valid PK property values (a reverted add, for instance).
+ // Trust only the property values on the original entity, allowing the client to update id properties.
+ // The only other way to compute this for some unvetted entity would be to do an O(n) search
+ // over this._serverEntities (too slow).
+ return this._getEntityIdentity(this._getChanges(entity) ? this._getOriginalValue(entity, this._entityType) : entity);
+ } catch (e) {
+ return null;
+ }
+ },
+
+ getDataContext: function () {
+ /// <summary>
+ /// Returns the DataContext used as a cache for model data.
+ /// </summary>
+ /// <returns type="upshot.DataContext"/>
+
+ return this._dataContext;
+ },
+
+ getEntityValidationRules: function () {
+ /// <summary>
+ /// Returns entity validation rules for the type of entity cached by this EntitySet.
+ /// </summary>
+ /// <returns type="Object"/>
+
+ var metadata = upshot.metadata(this._entityType);
+ return metadata && metadata.rules && {
+ rules: metadata.rules,
+ messages: metadata.messages
+ };
+ },
+
+ getEntityErrors: function () {
+ /// <summary>
+ /// Returns an array of server errors by entity, of the form [ &#123; entity: &#60;entity&#62;, error: &#60;object&#62; &#125;, ... ].
+ /// </summary>
+ /// <returns type="Array"/>
+
+ var self = this;
+ return $.map(this._errors, function (trackingId) {
+ var tracking = self._tracked[trackingId];
+ return { entity: tracking.obj, error: tracking.error };
+ });
+ },
+
+ getEntityError: function (entity) {
+ /// <summary>
+ /// Returns server errors for the supplied entity.
+ /// </summary>
+ /// <param name="entity" type="Object">
+ /// &#10;The entity for which server errors are to be returned.
+ /// </param>
+ /// <returns type="Object"/>
+
+ var trackingId = this._getTrackingId(entity);
+ if (trackingId) {
+ return this._tracked[trackingId].error;
+ }
+ },
+
+ revertUpdates: function (entity, path, skipChildren) {
+ /// <summary>
+ /// Reverts updates to the entity and all the objects or arrays it contains. When a path is specified, it will
+ /// revert only updates to the specified property and all of its children. This function is a no-op for entities
+ /// not in the 'ClientUpdated' state.
+ /// </summary>
+ /// <param name="entity" type="Object">
+ /// &#10;The entity to revert updates for
+ /// </param>
+ /// <param name="path" type="String" optional="true">
+ /// &#10;The path to the property to revert updates for. The path should be valid javascript; for example "Addresses[3].Street".
+ /// </param>
+ /// <param name="skipChildren" type="Boolean" optional="true">
+ /// &#10;Whether or not to revert updates to the children of the specified property
+ /// </param>
+ /// <returns type="upshot.EntitySet"/>
+
+ var id = this.getEntityId(entity);
+
+ var state = id !== null && this._entityStates[id];
+ if (!state || state === upshot.EntityState.Deleted) {
+ throw "Entity not cached in data context.";
+ } else if (upshot.EntityState.isServerSyncing(state)) {
+ throw "Can't revert an entity while changes are being committed.";
+ } else if (state !== upshot.EntityState.ClientUpdated) {
+ return this;
+ } else if (!path) {
+ return this.revertChanges(entity);
+ } else {
+ var tokens = tokenizePath(entity, path);
+
+ var snapshot = this._clearChangesOnPath(tokens.objs.slice(), tokens.tokens.slice(), skipChildren);
+ !this.isUpdated(entity) && this._updateEntityState(id, upshot.EntityState.Unmodified);
+ this._restoreOriginalValues(entity, this._entityType, snapshot);
+ this._triggerEntityUpdated(entity);
+
+ ///#DEBUG
+ this._verifyConsistency(entity, id);
+ ///#ENDDEBUG
+
+ upshot.__triggerRecompute();
+
+ return this;
+ }
+ },
+
+ revertChanges: function (entities) {
+ /// <summary>
+ /// Reverts the specified entities back to their original state.
+ /// </summary>
+ /// <param name="entities" type="Array" optional="true">
+ /// &#10;One or more entities to revert. This parameter is optional. When omitted, all entities will be reverted.
+ /// </param>
+ /// <returns type="upshot.EntitySet"/>
+
+ if (!entities) {
+ this.__revertChanges();
+ } else {
+ if (!upshot.isArray(entities)) {
+ entities = [entities];
+ }
+ var self = this;
+ $.each(entities, function (index, entity) {
+ var id = self.getEntityId(entity);
+
+ var state = id !== null && self._entityStates[id];
+ if (!state || state === upshot.EntityState.Deleted) {
+ throw "Entity no longer cached in data context.";
+ } else if (upshot.EntityState.isServerSyncing(state)) {
+ throw "Can't revert an entity while changes are being committed.";
+ } else if (state === upshot.EntityState.Unmodified) {
+ return;
+ } else if (state === upshot.EntityState.ClientDeleted || state === upshot.EntityState.ClientUpdated) {
+ // Do this before the model change, so listeners on data change events see consistent entity state.
+ self._updateEntityState(id, upshot.EntityState.Unmodified);
+
+ var snapshot = self._clearChanges(entity, true);
+ self._restoreOriginalValues(entity, self._entityType, snapshot);
+ self._triggerEntityUpdated(entity);
+ } else if (state === upshot.EntityState.ClientAdded) {
+ self._purgeUncommittedAddedEntity(self._getAddedEntityFromId(id), true);
+ } else {
+ throw "Entity changes cannot be reverted for entity in state '" + state + "'.";
+ }
+ });
+ }
+
+ upshot.__triggerRecompute();
+
+ return this;
+ },
+
+ isUpdated: function (entity, path, ignoreChildren) {
+ /// <summary>
+ /// Returns whether the entity of any of the objects or arrays it contains are updated. When a path is specified,
+ /// it returns whether the specified property of any of its children are updated. This function will never return
+ /// 'true' for entities not in the 'ClientUpdated' state.
+ /// </summary>
+ /// <param name="entity" type="Object">
+ /// &#10;The entity to check for updates
+ /// </param>
+ /// <param name="path" type="String" optional="true">
+ /// &#10;The path to the property to check for updates. The path should be valid javascript; for example "Addresses[3].Street".
+ /// </param>
+ /// <param name="ignoreChildren" type="Boolean" optional="true">
+ /// &#10;Whether or not updates to the children of the specified property should be considered in the result
+ /// </param>
+ /// <returns type="Boolean"/>
+ var id = this.getEntityId(entity);
+
+ var state = id !== null && this._entityStates[id];
+ if (!state || !upshot.EntityState.isUpdated(state)) {
+ return false;
+ }
+
+ var obj = entity,
+ property,
+ child;
+ if (path) {
+ var tokens = tokenizePath(entity, path);
+ // objs) [obj, A, B], [obj, A, B, B[0]], [obj, "str"]
+ // tokens) ["A", "B"], ["A", "B", "0"], ["C"]
+ obj = tokens.objs[tokens.objs.length - 2];
+ child = tokens.objs[tokens.objs.length - 1];
+ if (!upshot.isArray(obj)) {
+ property = tokens.tokens[tokens.tokens.length - 1];
+ }
+ }
+
+ var changes = this._getChanges(obj);
+ if (!changes) { return false; }
+ if (!path) { return true; }
+
+ return (property ? changes.original.hasOwnProperty(property) : false) || (ignoreChildren ? false : this._getChanges(child) !== null);
+ },
+
+ deleteEntity: function (entity) {
+ /// <summary>
+ /// Marks the supplied entity for deletion. This is a non-destructive operation, meaning that the entity will remain in the EntitySet.getEntities() array until the server commits the delete.
+ /// </summary>
+ /// <param name="entity" type="Object">
+ /// &#10;The entity to be marked for deletion.
+ /// </param>
+ /// <returns type="upshot.EntitySet"/>
+
+ var id = this.getEntityId(entity),
+ entityState = id !== null && this._entityStates[id];
+ if (!entityState) {
+ throw "Entity not cached in data context.";
+ } else if (entityState === upshot.EntityState.ClientAdded) {
+ // Deleting a entity that is uncommitted and only on the client.
+ this._purgeUncommittedAddedEntity(this._getAddedEntityFromId(id));
+ } else if (upshot.EntityState.isServerSyncing(entityState)) {
+ // Force the application to block deletes while saving any edit to this same entity.
+ // We don't have a mechanism to enqueue this edit, then apply when the commit succeeds,
+ // possibly discard it when the commit fails.
+ throw "Can't delete an entity while previous changes are being committed.";
+ } else {
+ // If the entity is ClientUpdated, we'll implicitly switch to ClientDeleted.
+ // This saves extra app code that would revert the update before allowing the delete.
+ this._updateEntityState(id, upshot.EntityState.ClientDeleted);
+
+ ///#DEBUG
+ this._verifyConsistency(entity, id);
+ ///#ENDDEBUG
+
+ // This treats the case where the entity is already ClientDeleted but an implicit
+ // commit failed. As a convenience, a subsequent deleteEntity call will retry
+ // the failed commit.
+ this._dataContext.__queueImplicitCommit();
+ }
+
+ upshot.__triggerRecompute();
+
+ return this;
+ },
+
+ // Internal methods
+
+ __dispose: function () {
+ var self = this;
+ $.each(this._tracked, function (key, value) {
+ self._deleteTracking(value.obj);
+ });
+ upshot.__deregisterRootEntitySource(this);
+ base.dispose.call(this);
+ },
+
+ __loadEntities: function (entities) {
+ // For each entity, either merge it with a cached entity or add it to the cache.
+ var self = this,
+ entitiesNewToEntitySet = [],
+ indexToInsertClientEntities = this._serverEntities.length;
+ // Re: indexToInsertClientEntities, by convention, _clientEntities are layed out as updated followed by
+ // added entities.
+ var mergedLoadedEntities = $.map(entities, function (entity) {
+ return self._loadEntity(entity, entitiesNewToEntitySet);
+ });
+
+ if (entitiesNewToEntitySet.length > 0) {
+ // Don't trigger a 'reset' here. What would RemoteDataSources do with such an event?
+ // They only have a subset of our entities as the entities they show their clients. They could
+ // only reapply their remote query in response to "refresh".
+ obs.insert(this._clientEntities, indexToInsertClientEntities, entitiesNewToEntitySet);
+ this._trigger("arrayChanged", "insert", { index: indexToInsertClientEntities, items: entitiesNewToEntitySet });
+ }
+
+ return mergedLoadedEntities;
+ },
+
+ __getEditedEntities: function () {
+ var self = this,
+ entities = [];
+ $.each(this._entityStates, function (id, state) {
+ if (upshot.EntityState.isClientModified(state)) {
+ entities.push(self._getEntityFromId(id));
+ }
+ });
+
+ return entities;
+ },
+
+ __getEntityEdit: function (entity) {
+ var id = this.getEntityId(entity),
+ self = this,
+ submittingState,
+ operation,
+ addEntityType = function (entityToExtend) {
+ return $.extend({ "__type": self._entityType }, entityToExtend);
+ };
+ switch (this._entityStates[id]) {
+ case upshot.EntityState.ClientUpdated:
+ submittingState = upshot.EntityState.ServerUpdating;
+ operation = {
+ Operation: 3,
+ Entity: addEntityType(this._getSerializableEntity(entity)),
+ OriginalEntity: addEntityType(this._getOriginalValue(entity, this._entityType))
+ };
+ break;
+
+ case upshot.EntityState.ClientAdded:
+ submittingState = upshot.EntityState.ServerAdding;
+ entity = this._getAddedEntityFromId(id).entity;
+ operation = {
+ Operation: 2,
+ Entity: addEntityType(this._getSerializableEntity(entity))
+ };
+ break;
+
+ case upshot.EntityState.ClientDeleted:
+ submittingState = upshot.EntityState.ServerDeleting;
+ operation = {
+ Operation: 4,
+ Entity: addEntityType(this._getOriginalValue(entity, this._entityType))
+ };
+ // TODO -- Do we allow for concurrency guards here?
+ break;
+
+ default:
+ throw "Unrecognized entity state.";
+ }
+
+ var lastError = self.getEntityError(entity);
+ var edit = {
+ entityType: this._entityType,
+ storeEntity: entity,
+ operation: operation,
+ updateEntityState: function () {
+ self._updateEntityState(id, submittingState, lastError);
+
+ ///#DEBUG
+ self._verifyConsistency(entity, id);
+ ///#ENDDEBUG
+ },
+ succeeded: function (result) {
+ self._handleSubmitSucceeded(id, operation, result);
+ },
+ failed: function (error) {
+ self._handleSubmitFailed(id, operation, error || lastError);
+ }
+ };
+ return edit;
+ },
+
+ __revertChanges: function () {
+ var synchronizing;
+ $.each(this._entityStates, function (unused, state) {
+ if (upshot.EntityState.isServerSyncing(state)) {
+ synchronizing = true;
+ return false;
+ }
+ });
+ if (synchronizing) {
+ throw "Can't revert changes to all entities while previous changes are being committed. Try 'revertChanges(entity)' for entities not presently being committed.";
+ }
+
+ var entities = [];
+ for (var id in this._entityStates) {
+ if (upshot.EntityState.isClientModified(this._entityStates[id])) {
+ entities.push(this._getEntityFromId(id));
+ }
+ }
+ if (entities.length > 0) {
+ this.revertChanges(entities);
+ }
+ },
+
+ __flushEntityStateChangedEvents: function () {
+ var self = this;
+ $.each(this._deferredEntityStateChangeEvents, function (index, eventArguments) {
+ self._trigger.apply(self, ["entityStateChanged"].concat(eventArguments));
+ });
+ this._deferredEntityStateChangeEvents.splice(0, this._deferredEntityStateChangeEvents.length);
+ },
+
+ // Used when AssociatedEntitiesView translates an entity add into a FK property change.
+ __setProperty: function (entity, propertyName, value) {
+ var eventArguments = { oldValues: {}, newValues: {} };
+ eventArguments.oldValues[propertyName] = obs.getProperty(entity, propertyName);
+ eventArguments.newValues[propertyName] = value;
+
+ // NOTE: Cribbed from _getTracker/beforeChange. Keep in sync.
+ this._copyOnWrite(entity, eventArguments);
+ this._removeTracking(this._getOldFromEvent("change", eventArguments));
+
+ obs.setProperty(entity, propertyName, value);
+
+ // NOTE: Cribbed from _getTracker/afterChange. Keep in sync.
+ this._addTracking([value], upshot.metadata.getPropertyType(this._entityType, propertyName), entity, propertyName);
+ this._bubbleChange(entity, null, "", eventArguments);
+ },
+
+
+ // Private methods
+
+ _loadEntity: function (entity, entitiesNewToEntitySet) {
+ var identity = this._getEntityIdentity(entity),
+ index = this._getEntityIndexFromIdentity(identity);
+ if (index >= 0) {
+ entity = this._merge(this._serverEntities[index].entity, entity);
+ } else {
+ var id = identity; // Ok to use this as an id, as this is a new, unmodified server entity.
+ this._addTracking([entity], this._entityType);
+ this._entityStates[id] = upshot.EntityState.Unmodified;
+ this._serverEntities.push({ entity: entity, identity: id });
+ this._updateEntityState(id, upshot.EntityState.Unmodified, null, entity);
+ this._addAssociationProperties(entity);
+
+ ///#DEBUG
+ this._verifyConsistency(entity, id);
+ ///#ENDDEBUG
+
+ entitiesNewToEntitySet.push(entity);
+ }
+ return entity;
+ },
+
+ _handleEntityAdd: function (entity) {
+ if (this._getEntityIndex(entity) >= 0 || // ...in server entities
+ this._getAddedEntityFromEntity(entity)) { // ...in added entities
+ throw "Entity already in data source.";
+ }
+
+ var id = upshot.uniqueId("added");
+ addedEntity = { entity: entity, clientId: id };
+ this._addedEntities.push(addedEntity);
+ // N.B. Entity will already have been added to this._clientEntities, as clients issue CUD operations
+ // against this._clientEntities.
+ this._addTracking([entity], this._entityType);
+ this._entityStates[id] = upshot.EntityState.Unmodified;
+ this._updateEntityState(id, upshot.EntityState.ClientAdded);
+ this._addAssociationProperties(entity);
+
+ ///#DEBUG
+ this._verifyConsistency(entity, id);
+ ///#ENDDEBUG
+
+ this._dataContext.__queueImplicitCommit();
+
+ base._handleEntityAdd.apply(this, arguments);
+ },
+
+ _changeEntityStateForUpdate: function (entity) {
+
+ var id = this.getEntityId(entity),
+ entityState = id !== null && this._entityStates[id];
+ if (!entityState) {
+ throw "Entity not cached in data context.";
+ } else if (entityState === upshot.EntityState.ClientAdded) {
+ // Updating a entity that is uncommitted and only on the client.
+ // Edit state remains "ClientAdded". We won't event an edit state change (so clients had
+ // better be listening on "change").
+ // Fall through and do implicit commit.
+ } else if (upshot.EntityState.isServerSyncing(entityState)) {
+ // Force the application to block updates while saving any edit to this same entity.
+ // We don't have a mechanism to enqueue this edit, then apply when the commit succeeds,
+ // possibly discard it when the commit fails.
+ throw "Can't update an entity while previous changes are being committed.";
+ } else {
+ // If this entity is ClientDeleted, we'll implicitly switch to ClientUpdated.
+ // This saves extra app code that would revert the delete before allowing updates.
+ this._updateEntityState(id, upshot.EntityState.ClientUpdated, this.getEntityError(entity));
+
+ ///#DEBUG
+ this._verifyConsistency(entity, id);
+ ///#ENDDEBUG
+ }
+
+ // This treats the case where the entity is already ClientAdded/ClientUpdated but an implicit
+ // commit failed. As a convenience, a subsequent update will retry the failed commit.
+ this._dataContext.__queueImplicitCommit();
+
+ // The caller is responsible for calling upshot.__triggerRecompute().
+ },
+
+ _updateEntityState: function (id, state, error, entity) {
+ /// <param name="errors" optional="true"></param>
+ /// <param name="entity" optional="true"></param>
+
+ entity = entity || this._getEntityFromId(id); // Notifying after a purge requires that we pass the entity for id.
+
+ var oldState = this._entityStates[id];
+ if (this._entityStates[id]) { // We'll purge the entity before raising "Deleted".
+ this._entityStates[id] = state;
+ if (oldState !== state) {
+ // TODO: The change event for EntityState won't be deferred here, like it is for _raiseEntityStateChangedEvent.
+ obs.setContextProperty(entity, "entity", "state", state);
+ }
+ }
+
+ var errorChanged = this._updateEntityError(entity, error);
+
+ if (oldState !== state || errorChanged) {
+ // We defer entityStateChange events here so that -- for adds -- they follow
+ // "insert" events for LocalDataSource, AssociationEntitiesView.
+ this._deferredEntityStateChangeEvents.push([entity, state, error]);
+ }
+ },
+
+ // ----------------------------------------
+ // | old | new | description |
+ // |-----|-----|--------------------------|
+ // | X | X | no-op |
+ // | / | X | remove old from _errors |
+ // | X | / | add new to _errors |
+ // | / | / | replace old with new |
+ // ----------------------------------------
+ _updateEntityError: function (entity, newError) {
+ var trackingId = this._getTrackingId(entity),
+ tracking = this._tracked[trackingId],
+ changed;
+
+ var oldIndex = $.inArray(trackingId, this._errors);
+ if (oldIndex <= -1) {
+ if (newError) {
+ this._errors.push(trackingId);
+ tracking.error = newError;
+ changed = true;
+ }
+ } else if (newError) {
+ if (newError !== tracking.error) {
+ tracking.error = newError;
+ changed = true;
+ }
+ } else {
+ this._errors.splice(oldIndex, 1);
+ delete tracking.error;
+ changed = true;
+ }
+
+ if (changed) {
+ obs.setContextProperty(entity, "entity", "error", newError);
+ }
+ return changed;
+ },
+
+ _purgeUncommittedAddedEntity: function (addedEntity) {
+ this._purgeEntityInternal(addedEntity.entity, addedEntity.clientId);
+ },
+
+ _purgeServerEntity: function (entity, id) {
+ this._serverEntities.splice(this._getEntityIndex(entity), 1);
+ this._purgeEntityInternal(entity, id);
+ },
+
+ _purgeEntityInternal: function (entity, id) {
+ var entityState = this._entityStates[id];
+
+ // Do this before the model change, so listeners on data change events see consistent entity state.
+ this._updateEntityState(id, upshot.EntityState.Deleted, null, entity);
+
+ // Remove our observable extensions from the entity being purged.
+ this._clearChanges(entity);
+ this._removeTracking([entity]);
+ // Remove this entity from _addedEntities, if it's there.
+ for (var i = 0; i < this._addedEntities.length; i++) {
+ if (this._addedEntities[i].clientId === id) {
+ this._addedEntities.splice(i, 1);
+ break;
+ }
+ }
+ delete this._entityStates[id];
+ this._disposeAssociationEntitiesViews(id);
+ this._purgeEntity(entity); // Superclass method that removes entity from EntitySource.getEntities().
+
+ ///#DEBUG
+ this._verifyConsistency(entity, id, true);
+ ///#ENDDEBUG
+ },
+
+ _getEntityIdentity: function (entity) {
+ return upshot.EntitySet.__getIdentity(entity, this._entityType);
+ },
+
+ _getEntityIndexFromIdentity: function (identity) {
+ var index = -1;
+ for (var i = 0; i < this._serverEntities.length; i++) {
+ if (this._serverEntities[i].identity === identity) {
+ index = i;
+ break;
+ }
+ }
+
+ return index;
+ },
+
+ _getEntityIndex: function (entity) {
+ var index = -1;
+ for (var i = 0; i < this._serverEntities.length; i++) {
+ if (this._serverEntities[i].entity === entity) {
+ index = i;
+ break;
+ }
+ }
+
+ return index;
+ },
+
+ _addTracking: function (objects, type, parent, property) {
+ var self = this;
+ $.each(objects, function (index, value) {
+ self._addTrackingRecursive(parent, property, value, type);
+ });
+ },
+
+ _addTrackingRecursive: function (parent, property, obj, type) {
+ if (upshot.isArray(obj) || upshot.isObject(obj)) {
+ var tracking = this._getTracking(obj);
+ if (tracking) {
+ if (tracking.active) {
+ throw "Value is already tracked";
+ }
+ } else {
+ var trackingId = upshot.cache(obj, "trackingId", upshot.uniqueId("tracking"));
+ tracking = this._tracked[trackingId] = {};
+ }
+
+ tracking.obj = obj;
+ tracking.type = type;
+ tracking.parentId = this._getTrackingId(parent) || null;
+ tracking.property = upshot.isArray(parent) ? null : property;
+ tracking.active = true;
+ tracking.changes = tracking.changes || null;
+
+ obs.track(obj, this._getTracker(), this._entityType);
+
+ if (upshot.isArray(obj)) {
+ // Primitive values don't get mapped. Avoid iteration over the potentially large array.
+ // TODO: This precludes heterogeneous arrays. Should we test for primitive element type here instead?
+ if (!upshot.isValueArray(obj)) {
+ // Since we're recursing through the entity, we won't need to use asArray on collection-typed properties
+ var self = this;
+ $.each(obj, function (index, value) {
+ self._addTrackingRecursive(obj, index, value, type);
+ });
+ }
+ } else {
+ var self = this;
+ $.each(upshot.metadata.getProperties(obj, type), function (index, prop) {
+ self._addTrackingRecursive(obj, prop.name, obs.getProperty(obj, prop.name), prop.type);
+ });
+ }
+ }
+ },
+
+ _getTracker: function () {
+ if (this._tracker === null) {
+ var self = this;
+ this._tracker = {
+ beforeChange: function (target, type, eventArguments) {
+ if (!self._isAssociationPropertySet(target, type, eventArguments)) {
+ self._copyOnWrite(target, eventArguments);
+ self._removeTracking(self._getOldFromEvent(type, eventArguments));
+ }
+ },
+ afterChange: function (target, type, eventArguments) {
+ upshot.__beginChange();
+ if (!self._handleAssociationPropertySet(target, type, eventArguments)) {
+ var tracking = self._getTracking(target);
+ if (type === "change") {
+ $.each(eventArguments.newValues, function (key, value) {
+ self._addTracking([value], upshot.metadata.getPropertyType(tracking.type, key), target, key);
+ });
+ } else {
+ self._addTracking(self._getNewFromEvent(type, eventArguments), tracking.type, target);
+ }
+ self._bubbleChange(target, null, [], eventArguments);
+ }
+ },
+ afterEvent: function (target, type, eventArguments) {
+ upshot.__endChange();
+ }
+ };
+ if (this._dataContext.__manageAssociations) {
+ this._tracker.includeAssociations = true;
+ }
+ }
+ return this._tracker;
+ },
+
+ _isAssociationPropertySet: function (target, type, eventArguments) {
+ if (type === "change" && this._dataContext.__manageAssociations && this._getTracking(target).parentId === null) {
+ var fieldsMetadata = (upshot.metadata(this._entityType) || {}).fields;
+ if (fieldsMetadata) {
+ for (var fieldName in fieldsMetadata) {
+ var fieldMetadata = fieldsMetadata[fieldName];
+ if (fieldMetadata.association &&
+ (fieldName in eventArguments.oldValues || fieldName in eventArguments.newValues)) {
+
+ // TODO: Treat case when oldValues/newValues contains multiple property names.
+ if (this._getOldFromEvent(type, eventArguments).length > 1 ||
+ this._getNewFromEvent(type, eventArguments).length > 1) {
+ throw "NYI -- Can't include association properties in N>1 property sets.";
+ }
+
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+ },
+
+ _handleAssociationPropertySet: function (target, type, eventArguments) {
+ if (!this._isAssociationPropertySet(target, type, eventArguments)) {
+ return false;
+ }
+ // TODO: Throw an exception when someone tries to replace the array under a child entities property.
+
+ var id = this.getEntityId(target);
+ if (id === null || !(id in this._entityStates)) {
+ throw "Entity not cached in data context.";
+ }
+
+ // Determine the single property whose value is being set.
+ var fieldName;
+ for (var fieldNameT in eventArguments.oldValues) {
+ fieldName = fieldNameT;
+ break;
+ }
+ if (!fieldName) {
+ for (var fieldNameT in eventArguments.newValues) {
+ fieldName = fieldNameT;
+ break;
+ }
+ }
+
+ var associatedEntitiesViews = this._associatedEntitiesViews[id],
+ associatedEntitiesView = associatedEntitiesViews[fieldName];
+ associatedEntitiesView.__handleParentPropertySet(eventArguments.newValues[fieldName]);
+
+ return true;
+ },
+
+ _removeTracking: function (objects) {
+ var self = this;
+ $.each(objects, function (index, value) {
+ self._removeTrackingRecursive(value);
+ });
+ },
+
+ _removeTrackingRecursive: function (obj) {
+ if (upshot.isArray(obj) || upshot.isObject(obj)) {
+ var tracking = this._getTracking(obj);
+ if (tracking) {
+ if (tracking.changes === null) {
+ this._deleteTracking(obj);
+ } else {
+ tracking.active = false;
+ }
+
+ obs.track(obj, null);
+
+ if (upshot.isArray(obj)) {
+ // Primitive values don't get mapped. Avoid iteration over the potentially large array.
+ // TODO: This precludes heterogeneous arrays. Should we test for primitive element type here instead?
+ if (!upshot.isValueArray(obj)) {
+ var self = this;
+ $.each(obj, function (index, value) {
+ self._removeTrackingRecursive(value);
+ });
+ }
+ } else {
+ var self = this;
+ $.each(obj, function (key) {
+ self._removeTrackingRecursive(obs.getProperty(obj, key));
+ });
+ }
+ }
+ }
+ },
+
+ _deleteTracking: function (obj) {
+ var trackingId = this._getTrackingId(obj);
+
+ upshot.deleteCache(obj, "trackingId");
+
+ delete this._tracked[trackingId];
+ },
+
+ _getOldFromEvent: function (type, eventArguments) {
+ if (type === "change") {
+ // TODO -- add test coverage for N-property updates
+ var old = [];
+ $.each(eventArguments.oldValues, function (key, value) {
+ old.push(value);
+ });
+ return old;
+ } else {
+ if (type === "remove") {
+ return eventArguments.items;
+ } else if (type === "replaceAll") {
+ return eventArguments.oldItems;
+ }
+ }
+ return [];
+ },
+
+ _getNewFromEvent: function (type, eventArguments) {
+ if (type === "change") {
+ // TODO -- add test coverage for N-property updates
+ var _new = [];
+ $.each(eventArguments.newValues, function (key, value) {
+ _new.push(value);
+ });
+ return _new;
+ } else {
+ if (type === "insert") {
+ return eventArguments.items;
+ } else if (type === "replaceAll") {
+ return eventArguments.newItems;
+ }
+ }
+ return [];
+ },
+
+ _getTrackingId: function (obj) {
+ return upshot.cache(obj, "trackingId");
+ },
+
+ _getTracking: function (obj) {
+ var trackingId = this._getTrackingId(obj);
+ return trackingId ? this._tracked[trackingId] : null;
+ },
+
+ _getChanges: function (obj) {
+ var tracking = this._getTracking(obj);
+ return tracking ? this._getTracking(obj).changes : null;
+ },
+
+ _bubbleChange: function (obj, child, path, eventArguments) {
+ var tracking = this._getTracking(obj);
+
+ if (child) {
+ this._recordChangedChild(obj, child);
+ }
+
+ if (tracking.parentId === null) {
+ if (path.length > 1) {
+ // strip the '.' off the path
+ path = path.slice(1);
+ }
+ this._handlePropertyChange(obj, eventArguments, child === null);
+ this._triggerEntityUpdated(obj, path, eventArguments);
+ } else {
+ var parent = this._tracked[tracking.parentId].obj;
+ path = (tracking.property ? "." + tracking.property : "[" + $.inArray(obj, parent) + "]") + path;
+ this._bubbleChange(parent, obj, path, eventArguments);
+ }
+ },
+
+ _copyOnWrite: function (obj, eventArguments) {
+ if (upshot.isArray(obj)) {
+ this._recordWriteToArray(obj);
+ } else {
+ this._recordWriteToObject(obj, eventArguments.oldValues);
+ }
+ },
+
+ _recordWriteToArray: function (array) {
+ var tracking = this._getTracking(array),
+ changes = tracking.changes || (tracking.changes = {
+ original: null,
+ children: {}
+ });
+
+ changes.original || (changes.original = array.slice(0));
+ },
+
+ _recordWriteToObject: function (obj, oldValues) {
+ var tracking = this._getTracking(obj),
+ changes = tracking.changes || (tracking.changes = {
+ original: {},
+ children: {}
+ });
+
+ $.each(oldValues, function (key, value) {
+ changes.original.hasOwnProperty(key) || (changes.original[key] = value);
+ obs.setContextProperty(obj, "property", key, true);
+ });
+ },
+
+ _recordChangedChild: function (obj, child) {
+ upshot.isArray(obj) ?
+ this._recordChangedChildOfArray(obj, child) :
+ this._recordChangedChildOfObject(obj, child);
+ },
+
+ _recordChangedChildOfArray: function (array, child) {
+ var tracking = this._getTracking(array),
+ changes = tracking.changes || (tracking.changes = {
+ original: null,
+ children: {}
+ }),
+ childId = this._getTrackingId(child);
+
+ changes.children[childId] = childId;
+ },
+
+ _recordChangedChildOfObject: function (obj, child) {
+ var tracking = this._getTracking(obj),
+ changes = tracking.changes || (tracking.changes = {
+ original: {},
+ children: {}
+ }),
+ childId = this._getTrackingId(child);
+
+ changes.children[childId] = childId;
+ },
+
+ _clearChanges: function (obj, revert) {
+ var tracking = this._getTracking(obj),
+ changes = tracking.changes,
+ snapshot = { original: {}, children: {} };
+
+ if (changes) {
+ if (upshot.isArray(obj)) {
+ snapshot.original = changes.original;
+ } else {
+ $.each(changes.original, function (key, value) {
+ snapshot.original[key] = value;
+ delete changes.original[key];
+ obs.setContextProperty(obj, "property", key, false);
+ });
+ }
+
+ var self = this;
+ $.each(changes.children, function (key, value) {
+ var childTracking = self._tracked[value];
+ snapshot.children[value] = self._clearChanges(childTracking.obj, revert);
+ });
+
+ tracking.changes = null;
+ if (!revert && !tracking.active) {
+ this._deleteTracking(obj);
+ }
+ }
+ return snapshot;
+ },
+
+ _clearChangesOnPath: function (objs, tokens, skipChildren) { // This is only used for revert scenarios
+ // objs) [obj, A, B], [obj, A, B, B[0]], [obj, "str"]
+ // tokens) ["A", "B"], ["A", "B", "0"], ["C"]
+ var obj = objs.shift(),
+ property = tokens.shift(),
+ tracking = this._getTracking(obj),
+ changes = tracking.changes,
+ snapshot = { original: {}, children: {} };
+
+ if (changes) {
+ if (tokens.length === 0) {
+ var children = [objs[0]];
+ if (changes.original.hasOwnProperty(property)) {
+ children.push(changes.original[property]);
+ snapshot.original[property] = children[1];
+ delete changes.original[property];
+ obs.setContextProperty(obj, "property", property, false);
+ }
+
+ if (!skipChildren) {
+ var self = this;
+ $.each(children, function (index, child) {
+ var childId = self._getTrackingId(child);
+ if (childId && changes.children.hasOwnProperty(childId)) {
+ snapshot.children[childId] = self._clearChanges(child, true);
+ delete changes.children[childId];
+ }
+ });
+ }
+ } else {
+ var childId = this._getTrackingId(objs[0]);
+ if (childId && changes.children.hasOwnProperty(childId)) {
+ snapshot.children[childId] = this._clearChangesOnPath(objs, tokens, skipChildren);
+ if (this._tracked[childId].changes === null) {
+ delete changes.children[childId];
+ }
+ }
+ }
+
+ if (upshot.isEmpty(changes.original) && upshot.isEmpty(changes.children)) {
+ tracking.changes = null;
+ }
+ }
+ return snapshot;
+ },
+
+ _getOriginalValue: function (obj, type, property) {
+ if (upshot.isArray(obj) && property) {
+ throw "property is not a supported parameter when getting original array values";
+ }
+
+ var self = this,
+ changes = this._getChanges(obj),
+ original;
+
+ if (property) {
+ return changes && changes.original.hasOwnProperty(property) ?
+ changes.original[property] :
+ obs.getProperty(obj, property);
+ } else if (upshot.isArray(obj)) {
+ original = (changes ? changes.original || obj : obj).slice(0);
+ $.each(original, function (index, value) {
+ original[index] = self._getOriginalValue(value, type);
+ });
+ return original;
+ } else if (upshot.isObject(obj)) {
+ original = {};
+ $.each(upshot.metadata.getProperties(obj, type), function (index, prop) {
+ var value = self._getOriginalValue(obj, type, prop.name);
+ original[prop.name] = self._getOriginalValue(value, prop.type);
+ });
+ return original;
+ }
+ return obj;
+ },
+
+ _restoreOriginalValues: function (obj, type, changes) {
+ if (changes) {
+ var self = this;
+ if (upshot.isArray(obj)) {
+ if (changes.original !== null) {
+ self._arrayRefresh(obj, type, changes.original);
+ }
+ } else {
+ $.each(changes.original, function (key, value) {
+ self._setProperty(obj, upshot.metadata.getPropertyType(type, key), key, value);
+ });
+ }
+
+ $.each(changes.children, function (key, value) {
+ var tracking = self._tracked[key];
+ self._restoreOriginalValues(tracking.obj, tracking.type, value);
+ });
+ }
+ },
+
+ _merge: function (entity, _new) {
+ if (!this.isUpdated(entity)) {
+ // Only merge entities without changes
+ this._mergeObject(entity, _new, this._entityType);
+ }
+ return entity;
+ },
+
+ _mergeObject: function (obj, _new, type) {
+ ///#DEBUG
+ // TODO: For unmanaged associations, we'll need to descend into associations below to
+ // pick up child association membership changes and parent association value changes.
+ upshot.assert(this._dataContext.__manageAssociations);
+ ///#ENDDEBUG
+
+ var self = this;
+ $.each(upshot.metadata.getProperties(_new, type, false /* see TODO above */), function (index, prop) {
+ var oldValue = obs.getProperty(obj, prop.name),
+ value = obs.getProperty(_new, prop.name);
+ if (oldValue !== value) {
+ if (upshot.classof(oldValue) === upshot.classof(value)) {
+ // We only try to deep-merge when classes match
+ if (upshot.isArray(oldValue)) {
+ if (!upshot.isValueArray(oldValue)) {
+ self._mergeArray(oldValue, value, prop.type);
+ }
+ return;
+ } else if (upshot.isObject(oldValue)) {
+ self._mergeObject(oldValue, value, prop.type);
+ return;
+ }
+ }
+ self._setProperty(obj, type, prop.name, value);
+ }
+ });
+ },
+
+ _mergeArray: function (array, _new, type) {
+ var self = this;
+ $.each(_new, function (index, value) {
+ var oldValue = array[index];
+ if (oldValue) {
+ self._mergeObject(oldValue, value, type);
+ }
+ });
+ if (array.length > _new.length) {
+ this._arrayRemove(array, type, _new.length, array.length - _new.length);
+ } else if (array.length < _new.length) {
+ this._arrayInsert(array, type, array.length, _new.slice(array.length));
+ }
+ },
+
+ _handlePropertyChange: function (entity, eventArguments, raisePropertyChanged) {
+ // TODO, suwatch: to support N properties
+ var path, value;
+ for (var propertyName in eventArguments.newValues) {
+ if (eventArguments.newValues.hasOwnProperty(propertyName)) {
+ if (path) {
+ throw "NYI -- Can only update one property at a time.";
+ }
+ path = propertyName;
+ value = eventArguments.newValues[propertyName];
+ }
+ }
+
+ if (path === "") {
+ // Data-linking sends all <input> changes to the linked object.
+ return;
+ }
+
+ if (raisePropertyChanged) {
+ this._trigger("propertyChanged", entity, path, value);
+ // Issue the "entity change" event prior to any related "entity state changed" event below...
+ }
+
+ this._changeEntityStateForUpdate(entity);
+ },
+
+ _setProperty: function (obj, type, key, value) {
+ var parentId = this._getTracking(obj).parentId;
+
+ this._removeTracking([obs.getProperty(obj, key)]);
+ obs.setProperty(obj, key, value); // TODO: Shouldn't we be _addTracking before obs.setProperty, so we won't be reentered in a inconsistent state?
+ this._addTracking([value], upshot.metadata.getPropertyType(type, key), obj, key);
+ if (parentId === null) {
+ // Only raise "propertyChanged" for entity-level property changes.
+ this._trigger("propertyChanged", obj, key, value);
+ }
+ },
+
+ _arrayRefresh: function (array, type, values) {
+ this._removeTracking(array);
+ obs.refresh(array, values);
+ this._addTracking(array, type, array);
+ },
+
+ _arrayRemove: function (array, type, index, numToRemove) {
+ this._removeTracking(array.slice(index, numToRemove));
+ obs.remove(array, index, numToRemove);
+ },
+
+ _arrayInsert: function (array, type, index, items) {
+ obs.insert(array, index, items);
+ this._addTracking(items, type, array);
+ },
+
+ _triggerEntityUpdated: function (entity, path, eventArguments) {
+ // Only raise events when the entity is updated or being reverted or accepted
+ if (this.isUpdated(entity) || !eventArguments) {
+ this._trigger("entityUpdated", entity, path, eventArguments);
+ }
+ },
+
+ _getAddedEntityFromId: function (id) {
+ var addedEntities = $.grep(this._addedEntities, function (addedEntity) { return addedEntity.clientId === id; });
+ ///#DEBUG
+ upshot.assert(addedEntities.length <= 1);
+ ///#ENDDEBUG
+ return addedEntities[0];
+ },
+
+ _getAddedEntityFromEntity: function (entity) {
+ var addedEntities = $.grep(this._addedEntities, function (addedEntity) { return addedEntity.entity === entity; });
+ ///#DEBUG
+ upshot.assert(addedEntities.length <= 1);
+ ///#ENDDEBUG
+ return addedEntities[0];
+ },
+
+ _handleSubmitSucceeded: function (id, operation, result) {
+ var entity = this._getEntityFromId(id); // ...before we purge.
+
+ switch (operation.Operation) {
+ case 2:
+ // Do this before the model change, so listeners on data change events see consistent entity state.
+ this._updateEntityState(id, upshot.EntityState.Unmodified);
+
+ // Keep entity in addedEntities to maintain its synthetic id as the client-known id.
+ this._getAddedEntityFromId(id).committed = true;
+
+ this._serverEntities.push({ entity: entity, identity: this._getEntityIdentity(result.Entity) });
+ this._clearChanges(entity, false);
+ this._merge(entity, result.Entity);
+
+ ///#DEBUG
+ this._verifyConsistency(entity, id, false, true);
+ ///#ENDDEBUG
+
+ break;
+
+ case 3:
+ // Do this before the model change, so listeners on data change events see consistent entity state.
+ this._updateEntityState(id, upshot.EntityState.Unmodified);
+
+ this._clearChanges(entity, false);
+ this._merge(entity, result.Entity);
+ this._triggerEntityUpdated(entity);
+
+ ///#DEBUG
+ this._verifyConsistency(entity, id);
+ ///#ENDDEBUG
+
+ break;
+
+ case 4:
+ this._purgeServerEntity(entity, id); // This updates entity state to Deleted, verifies consistency.
+ break;
+ }
+
+ ///#DEBUG
+ upshot.assert(!this.getEntityError(entity), "must not have error!");
+ ///#ENDDEBUG
+ },
+
+ _handleSubmitFailed: function (id, operation, error) {
+ var entity = this._getEntityFromId(id);
+
+ var state;
+ switch (operation.Operation) {
+ case 2: state = upshot.EntityState.ClientAdded; break;
+ case 3: state = upshot.EntityState.ClientUpdated; break;
+ case 4: state = upshot.EntityState.ClientDeleted; break;
+ }
+
+ this._updateEntityState(id, state, error);
+
+ ///#DEBUG
+ this._verifyConsistency(entity, id);
+ ///#ENDDEBUG
+ },
+
+ _getEntityFromId: function (id) {
+ var addedEntity = this._getAddedEntityFromId(id);
+ if (addedEntity) {
+ // 'id' is one of our synthesized ones for client-added entities.
+ return addedEntity.entity;
+ } else {
+ // 'id' is one computed based on server identity, which is assumed to be immutable.
+ for (var i = 0; i < this._serverEntities.length; i++) {
+ if (this._serverEntities[i].identity === id) {
+ return this._serverEntities[i].entity;
+ }
+ }
+ }
+ },
+
+ _addAssociationProperties: function (entity) {
+ if (!this._dataContext.__manageAssociations) {
+ return;
+ }
+
+ var fieldsMetadata = (upshot.metadata(this._entityType) || {}).fields;
+ if (fieldsMetadata) {
+ var id = this.getEntityId(entity);
+ ///#DEBUG
+ upshot.assert(id !== null && id in this._entityStates, "Entity should be cached in data context.");
+ ///#ENDDEBUG
+
+ var associatedEntitiesViews = this._associatedEntitiesViews[id] = {},
+ self = this;
+ $.each(fieldsMetadata, function (fieldName, fieldMetadata) {
+ if (fieldMetadata.association) {
+ if (associatedEntitiesViews[fieldName]) {
+ throw "Duplicate property metadata for property '" + fieldName + "'.";
+ }
+ associatedEntitiesViews[fieldName] = self._createAssociatedEntitiesView(entity, fieldName, fieldMetadata);
+ }
+ });
+ }
+ },
+
+ _createAssociatedEntitiesView: function (entity, fieldName, fieldMetadata) {
+ if (!fieldMetadata.association.isForeignKey && !fieldMetadata.array) {
+ // TODO -- Singleton child entities?
+ throw "NYI: Singleton child entities are not currently supported";
+ }
+
+ var targetEntitySet = this._dataContext.getEntitySet(fieldMetadata.type),
+ isForeignKey = fieldMetadata.association.isForeignKey,
+ parentPropertySetter = isForeignKey ? function () { // AssociatedEntitiesView for a parent property needs a function to do the property set.
+ var oldParent = obs.getProperty(entity, fieldName),
+ newParent = obs.asArray(this.getEntities())[0] || null; // TODO: What if there are N>1 parent entities?
+ if (oldParent !== newParent) {
+ // TODO: For KO, this won't be an observable set if KO's map didn't already establish a observable property here. Is this ok?
+ obs.setProperty(entity, fieldName, newParent);
+ this._trigger("propertyChanged", entity, fieldName, newParent);
+ }
+ } : null,
+ parentEntitySet = isForeignKey ? targetEntitySet : this,
+ childEntitySet = isForeignKey ? this : targetEntitySet;
+
+ var result;
+ if (fieldMetadata.array) {
+ // TODO: We can't reuse an existing KO observable array here. Without more work, that will double-track
+ // the reused KO observable array (once, during obs.track of entity...a second time in the EntitySource ctor).
+ result = obs.createCollection();
+
+ // TODO: KO's obs.setProperty doesn't do what we want here. It will set an already existing KO observable array to
+ // have a value which is _another_ observable array.
+ entity[fieldName] = result;
+ }
+
+ return new upshot.AssociatedEntitiesView(entity, parentEntitySet, childEntitySet, fieldMetadata.association, parentPropertySetter, result);
+ },
+
+ _disposeAssociationEntitiesViews: function (id) {
+ var associatedEntitiesViews = this._associatedEntitiesViews[id];
+ if (associatedEntitiesViews) {
+ $.each(associatedEntitiesViews, function (unused, associatedEntitiesView) {
+ associatedEntitiesView.dispose();
+ });
+ }
+ delete this._associatedEntitiesViews[id];
+ },
+
+ ///#DEBUG
+ _verifyConsistency: function (entity, id, isPurged, isNewlyCommittedAdd) {
+ var entityState = this._entityStates[id];
+ upshot.assert(isPurged || entityState, "Entities in EntitySet always have an entity state.");
+ upshot.assert(entityState !== upshot.EntityState.Deleted,
+ "The Deleted entity state is only for the 'entityStateChanged' event. It's never in _entityStates.");
+ entityState = entityState || upshot.EntityState.Deleted;
+
+ // isNewlyCommittedAdded is only supplied as true for a client-added entity that has
+ // just been committed (and is now in the Unmodified state).
+ upshot.assert(!isNewlyCommittedAdd || entityState === upshot.EntityState.Unmodified);
+
+ var addedEntity = this._getAddedEntityFromId(id);
+ upshot.assert(!(isNewlyCommittedAdd || entityState === upshot.EntityState.ClientAdded || entityState === upshot.EntityState.ServerAdding) ||
+ addedEntity,
+ "Client-added entities are always tracked in _addedEntities");
+
+ var committedAdd = addedEntity && addedEntity.committed;
+ upshot.assert(!committedAdd || !upshot.EntityState.isAdded(entityState),
+ "Committed, client-added entities should never be in the ClientAdded/ServerAdding states");
+
+ var isInServerEntities = this._getEntityIndex(entity) >= 0;
+ upshot.assert(!committedAdd || isInServerEntities, "Committed, client-added entities should always be in _serverEntities");
+
+ var uncommittedAdd = addedEntity && !addedEntity.committed;
+ upshot.assert(!uncommittedAdd || !isInServerEntities, "Uncommitted, client-added entities should never be in _serverEntities");
+
+ upshot.assert(entityState !== upshot.EntityState.Deleted ||
+ !(isInServerEntities || addedEntity || (id in this._entityStates) || this.getEntityError(entity)),
+ "Deleted/purged entities should never be found in EntitySet");
+
+ upshot.assert(entityState === upshot.EntityState.Deleted || this.getEntityId(entity) === id,
+ "Entities in an entity set always have a non-null, stable id");
+
+ upshot.assert(!(entityState === upshot.EntityState.Unmodified || entityState === upshot.EntityState.Deleted) ||
+ !this.getEntityError(entity),
+ "Unmodified and Deleted/purged entities should never have errors");
+
+ upshot.assert(upshot.EntityState.isUpdated(entityState) === this.isUpdated(entity),
+ "An entity 'isUpdated' iff ClientUpdated/ServerUpdating");
+ },
+ ///#ENDDEBUG
+
+ _getSerializableEntity: function (entity) {
+ if (!this._dataContext.__manageAssociations) {
+ return entity;
+ }
+
+ var sanitizedEntity = {};
+ $.each(upshot.metadata.getProperties(entity, this._entityType), function (index, property) {
+ sanitizedEntity[property.name] = entity[property.name];
+ });
+ return sanitizedEntity;
+ }
+ };
+
+ var classMembers = {
+ __getIdentity: function (entity, entityType) {
+ // Produce a unique identity string for the given entity, based on simple
+ // concatenation of key values.
+ var metadata = upshot.metadata(entityType);
+ if (!metadata) {
+ throw "No metadata available for type '" + entityType + "'. Register metadata using 'upshot.metadata(...)'.";
+ }
+ var keys = metadata.key;
+ if (!keys) {
+ throw "No key metadata specified for entity type '" + entityType + "'";
+ }
+
+ // optimize for the common single part key case
+ if (keys.length == 1 && (keys[0].indexOf('.') == -1)) {
+ var keyMember = keys[0];
+ upshot.EntitySet.__validateKeyMember(keyMember, keyMember, entity, entityType);
+ return obs.getProperty(entity, keyMember).toString();
+ }
+
+ var identity = "";
+ $.each(keys, function (index, key) {
+ if (identity.length > 0) {
+ identity += ",";
+ }
+
+ // support dotted paths
+ var parts = key.split(".")
+ var value = entity;
+ $.each(parts, function (index, part) {
+ upshot.EntitySet.__validateKeyMember(part, key, value, entityType);
+ value = obs.getProperty(value, part);
+ });
+
+ identity += value;
+ });
+ return identity;
+ },
+
+ __validateKeyMember: function (keyMember, fullKey, entity, entityType) {
+ if (!entity || !(keyMember in entity)) {
+ throw "Key member '" + fullKey + "' doesn't exist on entity type '" + entityType + "'";
+ }
+ }
+ };
+
+ upshot.EntitySet = upshot.deriveClass(base, ctor, instanceMembers);
+
+ $.extend(upshot.EntitySet, classMembers);
+}
+///#RESTORE )(this, jQuery, upshot);
diff --git a/src/SPA/upshot/EntitySource.js b/src/SPA/upshot/EntitySource.js
new file mode 100644
index 00000000..e6931b26
--- /dev/null
+++ b/src/SPA/upshot/EntitySource.js
@@ -0,0 +1,232 @@
+/// <reference path="IntelliSense\References.js" />
+///#RESTORE (function (global, $, upshot, undefined)
+{
+ var obs = upshot.observability;
+
+ var ctor = function (options) {
+ var result = options && options.result;
+ if (result) {
+ if (upshot.EntitySource.as(result)) {
+ throw "Array is already bound to an EntitySource.";
+ }
+ }
+
+ this._viewsToRecompute = [];
+ this._eventCallbacks = {};
+ this._clientEntities = result || obs.createCollection();
+
+ // Events shared by subclasses
+ this._bindFromOptions(options, [ "arrayChanged", "propertyChanged", "entityStateChanged" ]);
+
+ var self = this;
+ obs.track(this._clientEntities, {
+ afterChange: function (array, type, eventArguments) {
+ upshot.__beginChange();
+ self._handleArrayChange(type, eventArguments);
+ },
+ afterEvent: function () {
+ upshot.__endChange();
+ }
+ });
+
+ upshot.cache(this._clientEntities, "entitySource", this);
+ };
+
+ var instanceMembers = {
+
+ // Public methods
+
+ dispose: function () {
+ /// <summary>
+ /// Disposes the EntitySource instance.
+ /// </summary>
+
+ if (this._eventCallbacks) { // Use _eventCallbacks as an indicator as to whether we've been disposed.
+ obs.track(this._clientEntities, null);
+ upshot.deleteCache(this._clientEntities, "entitySource");
+ this._dispose(); // Give subclass code an opportunity to clean up.
+ this._eventCallbacks = null;
+ }
+ },
+
+ // TODO: bind/unbind/_trigger are duplicated in EntitySource and DataContext, consider common routine.
+ bind: function (event, callback) {
+ /// <summary>
+ /// Registers the supplied callback to be called when an event is raised.
+ /// </summary>
+ /// <param name="event" type="String">
+ /// &#10;The event name.
+ /// </param>
+ /// <param name="callback" type="Function">
+ /// &#10;The callback function.
+ /// </param>
+ /// <returns type="upshot.EntitySource"/>
+
+ if (typeof event === "string") {
+ var list = this._eventCallbacks[event] || (this._eventCallbacks[event] = []);
+ list.push(callback);
+ } else {
+ for (var key in event) {
+ this.bind(key, event[key]);
+ }
+ }
+ return this;
+ },
+
+ unbind: function (event, callback) {
+ /// <summary>
+ /// Deregisters the supplied callback for the supplied event.
+ /// </summary>
+ /// <param name="event" type="String">
+ /// &#10;The event name.
+ /// </param>
+ /// <param name="callback" type="Function">
+ /// &#10;The callback function to be deregistered.
+ /// </param>
+ /// <returns type="upshot.EntitySource"/>
+
+ if (typeof event === "string") {
+ var list = this._eventCallbacks && this._eventCallbacks[event];
+ if (list) {
+ for (var i = 0, l = list.length; i < l; i++) {
+ if (list[i] === callback) {
+ list.splice(i, 1);
+ break;
+ }
+ }
+ }
+ } else {
+ for (var key in event) {
+ this.unbind(key, event[key]);
+ }
+ }
+ return this;
+ },
+
+ getEntities: function () {
+ /// <summary>
+ /// Returns the stable, observable array of model data.
+ /// </summary>
+ /// <returns type="Array"/>
+
+ return this._clientEntities;
+ },
+
+
+ // Internal methods
+
+ __registerForRecompute: function (entityView) {
+ if ($.inArray(entityView, this._viewsToRecompute) <= 0) {
+ this._viewsToRecompute.push(entityView);
+ }
+ },
+
+ __recomputeDependentViews: function () {
+ while (this._viewsToRecompute.length > 0) { // Downstream entity views might be dirtied due to recompute.
+ var viewsToRecompute = this._viewsToRecompute.slice();
+ this._viewsToRecompute.splice(0, this._viewsToRecompute.length);
+ $.each(viewsToRecompute, function (index, entityView) {
+ entityView.__recompute();
+ });
+ }
+ },
+
+ // Used to translate entity inserts through EntityViews (and onto their input EntitySource).
+ __addEntity: function (entity) {
+ var index = obs.asArray(this._clientEntities).length;
+ obs.insert(this._clientEntities, index, [entity]);
+
+ this._handleArrayChange("insert", { index: index, items: [entity] });
+ },
+
+ // Used to translate entity removes through EntityViews (and onto their input EntitySource).
+ __deleteEntity: function (entity, index) {
+ var index = $.inArray(entity, obs.asArray(this._clientEntities));
+ ///#DEBUG
+ upshot.assert(index >= 0, "entity must exist!");
+ ///#ENDDEBUG
+ obs.remove(this._clientEntities, index, 1);
+
+ this._handleArrayChange("remove", { index: index, items: [entity] });
+ },
+
+ // Private methods
+
+ _bindFromOptions: function (options, events) {
+ if (options) {
+ var self = this;
+ $.each(events, function (unused, event) {
+ var callback = options && options[event];
+ if (callback) {
+ self.bind(event, callback);
+ }
+ });
+ }
+ },
+
+ _dispose: function () {
+ // Will be overridden by derived classes.
+ },
+
+ _handleArrayChange: function (type, eventArguments) {
+ switch (type) {
+ case "insert":
+ var entitiesToAdd = eventArguments.items;
+ if (entitiesToAdd.length > 1) {
+ throw "NYI -- Can only add a single entity to/from an array in one operation.";
+ }
+
+ var entityToAdd = entitiesToAdd[0];
+ this._handleEntityAdd(entityToAdd);
+ break;
+
+ case "remove":
+ throw "Use 'deleteEntity' to delete entities from your array. Destructive delete is not yet implemented.";
+
+ case "replaceAll":
+ if (!upshot.sameArrayContents(eventArguments.newItems, obs.asArray(this._clientEntities))) {
+ throw "NYI -- Can only replaceAll with own entities.";
+ }
+ break;
+
+ default:
+ throw "NYI -- Array operation '" + type + "' is not supported.";
+ }
+
+ this._trigger("arrayChanged", type, eventArguments);
+ },
+
+ _handleEntityAdd: function (entity) {
+ // Will be overridden by derived classes to do specific handling for an entity add.
+ },
+
+ _purgeEntity: function (entity) {
+ // TODO -- Should we try to handle duplicates here?
+ var index = $.inArray(entity, obs.asArray(this._clientEntities));
+ obs.remove(this._clientEntities, index, 1);
+ this._trigger("arrayChanged", "remove", { index: index, items: [entity] });
+ },
+
+ _trigger: function (eventType) {
+ var list = this._eventCallbacks[eventType];
+ if (list) {
+ var args = Array.prototype.slice.call(arguments, 1);
+ // clone the list to be robust against bind/unbind during callback
+ list = list.slice(0);
+ for (var i = 0, l = list.length; i < l; i++) {
+ list[i].apply(this, args);
+ }
+ }
+ return this;
+ }
+ };
+
+ var classMembers = {
+ as: function (array) {
+ return upshot.cache(array, "entitySource");
+ }
+ };
+
+ upshot.EntitySource = upshot.defineClass(ctor, instanceMembers, classMembers);
+}
+///#RESTORE )(this, jQuery, upshot);
diff --git a/src/SPA/upshot/EntityView.js b/src/SPA/upshot/EntityView.js
new file mode 100644
index 00000000..d61764dd
--- /dev/null
+++ b/src/SPA/upshot/EntityView.js
@@ -0,0 +1,327 @@
+/// <reference path="IntelliSense\References.js" />
+///#RESTORE (function (global, $, upshot, undefined)
+{
+ var base = upshot.EntitySource.prototype;
+
+ var obs = upshot.observability;
+
+ var ctor = function (options) {
+
+ this._needRecompute = false;
+
+ var self = this;
+ this._observer = {
+ propertyChanged: function (entity, property, newValue) { self._onPropertyChanged(entity, property, newValue); },
+ arrayChanged: function (type, eventArgs) { self._onArrayChanged(type, eventArgs); },
+ entityStateChanged: function (entity, state, error) { self._onEntityStateChanged(entity, state, error); },
+ entityUpdated: function (entity, path, eventArgs) { self._onEntityUpdated(entity, path, eventArgs); }
+ };
+
+ // RemoteDataSource may dynamically bind to its EntitySet as it refreshes.
+ this._entitySource = null; // Make JS runtime type inference happy?
+
+ var entitySource = options && options.source;
+ if (entitySource) {
+ this._bindToEntitySource(entitySource);
+ }
+
+ base.constructor.call(this, options);
+ };
+
+ var dataContextMethodNames = [
+ "getDataContext",
+ "getEntityState",
+ "getEntityValidationRules",
+ "getEntityId",
+ "revertChanges",
+ "deleteEntity",
+ "getEntityErrors",
+ "getEntityError",
+ "isUpdated",
+ "revertUpdates"
+ ];
+
+ var instanceMembers = {
+
+ ///#DEBUG
+ getDataContext: function () {
+ /// <summary>
+ /// Returns the DataContext used as a cache for model data.
+ /// </summary>
+ /// <returns type="upshot.DataContext"/>
+
+ throw "Not reached"; // For Intellisense only.
+ },
+
+ getEntityState: function (entity) {
+ /// <summary>
+ /// Returns the EntityState for the supplied entity.
+ /// </summary>
+ /// <param name="entity" type="Object">
+ /// &#10;The entity for which EntityState will be returned.
+ /// </param>
+ /// <returns type="upshot.EntityState"/>
+
+ throw "Not reached"; // For Intellisense only.
+ },
+
+ getEntityValidationRules: function () {
+ /// <summary>
+ /// Returns entity validation rules for the type of entity returned by this EntityView.
+ /// </summary>
+ /// <returns type="Object"/>
+
+ throw "Not reached"; // For Intellisense only.
+ },
+
+ getEntityId: function (entity) {
+ /// <summary>
+ /// Returns an identifier for the supplied entity.
+ /// </summary>
+ /// <param name="entity" type="Object"/>
+ /// <returns type="String"/>
+
+ throw "Not reached"; // For Intellisense only.
+ },
+
+ revertChanges: function (all) {
+ /// <summary>
+ /// Reverts any edits to model data (to entities) back to original entity values.
+ /// </summary>
+ /// <param name="all" type="Boolean" optional="true">
+ /// &#10;Revert all model edits in the underlying DataContext (to all types of entities). Otherwise, revert changes only to those entities of the type loaded by this EntityView.
+ /// </param>
+ /// <returns type="upshot.EntityView"/>
+
+ throw "Not reached"; // For Intellisense only.
+ },
+
+ deleteEntity: function (entity) {
+ /// <summary>
+ /// Marks the supplied entity for deletion. This is a non-destructive operation, meaning that the entity will remain in the EntityView.getEntities() array until the server commits the delete.
+ /// </summary>
+ /// <param name="entity" type="Object">
+ /// &#10;The entity to be marked for deletion.
+ /// </param>
+ /// <returns type="upshot.EntityView"/>
+
+ throw "Not reached"; // For Intellisense only.
+ },
+
+ getEntityErrors: function () {
+ /// <summary>
+ /// Returns an array of server errors by entity, of the form [ &#123; entity: &#60;entity&#62;, error: &#60;object&#62; &#125;, ... ].
+ /// </summary>
+ /// <returns type="Array"/>
+
+ throw "Not reached"; // For Intellisense only.
+ },
+
+ getEntityError: function (entity) {
+ /// <summary>
+ /// Returns server errors for the supplied entity.
+ /// </summary>
+ /// <param name="entity" type="Object">
+ /// &#10;The entity for which server errors are to be returned.
+ /// </param>
+ /// <returns type="Object"/>
+
+ throw "Not reached"; // For Intellisense only.
+ },
+
+ isUpdated: function (entity, path, ignoreChildren) {
+ /// <summary>
+ /// Returns whether the entity of any of the objects or arrays it contains are updated. When a path is specified,
+ /// it returns whether the specified property of any of its children are updated. This function will never return
+ /// 'true' for entities not in the 'ClientUpdated' state.
+ /// </summary>
+ /// <param name="entity" type="Object">
+ /// &#10;The entity to check for updates
+ /// </param>
+ /// <param name="path" type="String" optional="true">
+ /// &#10;The path to the property to check for updates. The path should be valid javascript; for example "Addresses[3].Street".
+ /// </param>
+ /// <param name="ignoreChildren" type="Boolean" optional="true">
+ /// &#10;Whether or not updates to the children of the specified property should be considered in the result
+ /// </param>
+ /// <returns type="Boolean"/>
+
+ throw "Not reached"; // For Intellisense only.
+ },
+
+ revertUpdates: function (entity, path, skipChildren) {
+ /// <summary>
+ /// Reverts updates to the entity and all the objects or arrays it contains. When a path is specified, it will
+ /// revert only updates to the specified property and all of its children. This function is a no-op for entities
+ /// not in the 'ClientUpdated' state.
+ /// </summary>
+ /// <param name="entity" type="Object">
+ /// &#10;The entity to revert updates for
+ /// </param>
+ /// <param name="path" type="String" optional="true">
+ /// &#10;The path to the property to revert updates for. The path should be valid javascript; for example "Addresses[3].Street".
+ /// </param>
+ /// <param name="skipChildren" type="Boolean" optional="true">
+ /// &#10;Whether or not to revert updates to the children of the specified property
+ /// </param>
+ /// <returns type="upshot.EntityView"/>
+
+ throw "Not reached"; // For Intellisense only.
+ },
+
+ ///#ENDDEBUG
+
+
+ // Internal methods
+
+ __registerForRecompute: function (entityView) {
+ // Some EntityView that depends on us wants to recompute.
+ base.__registerForRecompute.apply(this, arguments);
+
+ // Register on our input EntitySource transitively back to the root EntitySources, from which
+ // our recompute wave originates.
+ this._entitySource.__registerForRecompute(this);
+ },
+
+ __recompute: function () {
+ // Our input EntitySource is giving us an opportunity to recompute. Do so, if we've so-marked.
+ if (this._needRecompute) {
+ this._needRecompute = false;
+ this._recompute();
+ }
+
+ // Tell EntityViews that depend on us to recompute.
+ this.__recomputeDependentViews();
+ },
+
+
+ // Private methods
+
+ _dispose: function () {
+ if (this._entitySource) { // RemoteDataSource dynamically binds to its input EntitySource.
+ this._entitySource.unbind(this._observer);
+ }
+ base._dispose.apply(this, arguments);
+ },
+
+ _bindToEntitySource: function (entitySource) {
+
+ if (this._entitySource === entitySource) {
+ return;
+ }
+
+ var self = this;
+
+ // Remove proxied DataContext-derived methods.
+ if (this._entitySource) {
+ $.each(dataContextMethodNames, function (index, name) {
+ if (self[name]) {
+ delete self[name];
+ }
+ });
+
+ this._entitySource.unbind(this._observer);
+ }
+
+ this._entitySource = entitySource;
+
+ // Proxy these DataContext-derived methods, if they're available.
+ if (entitySource.getDataContext) {
+ $.each(dataContextMethodNames, function (index, name) {
+ if (name !== "getEntityErrors") {
+ // Don't use $.proxy here, as that will statically bind to entitySource[name] and
+ // RemoteDataSource will dynamically change entitySource[name].
+ self[name] = function () {
+ var ret = entitySource[name].apply(entitySource, arguments);
+ return (name === "deleteEntity") ? self : ret;
+ };
+ }
+ });
+ }
+
+ this.getEntityErrors = function () {
+ return $.grep(entitySource.getEntityErrors(), function (error) {
+ return self._haveEntity(error.entity);
+ });
+ };
+
+ entitySource.bind(this._observer);
+ },
+
+ _setNeedRecompute: function () {
+ // Sub-classes will call this method to mark themselves as being dirty, requiring recompute.
+ this._needRecompute = true;
+ this._entitySource.__registerForRecompute(this);
+ },
+
+ _recompute: function () {
+ // In response to a call to _setNeedRecompute, we're getting called to recompute as
+ // part of the next recompute wave.
+ throw "Unreachable"; // Abstract/pure virtual method.
+ },
+
+ _handleEntityAdd: function (entity) {
+ // Translate adds onto our input EntitySource.
+ this._entitySource.__addEntity(entity);
+
+ base._handleEntityAdd.apply(this, arguments);
+ },
+
+ _haveEntity: function (entity) {
+ return $.inArray(entity, obs.asArray(this._clientEntities)) >= 0;
+ },
+
+ _purgeEntity: function (entity) {
+ base._purgeEntity.apply(this, arguments);
+ },
+
+ _onPropertyChanged: function (entity, property, newValue) {
+ // Translate property changes from our input EntitySource onto our result, if appropriate.
+ // NOTE: _haveEntity will be with respect to our current, stable set of result entities.
+ // Ignoring direct, observable inserts and removes, this result set will only change
+ // as part of our separate recompute wave, which happens _after_ such data change events.
+ if (this._haveEntity(entity)) {
+ this._trigger("propertyChanged", entity, property, newValue);
+ }
+ },
+
+ _onArrayChanged: function (type, eventArgs) {
+ // NOTE: These are not translated directly in the same way that property and entity state
+ // change events are. Rather, subclasses have specific logic as to how changes to the
+ // membership of their input EntitySource impacts their result entity membership.
+
+ // Will be overridden by derived classes.
+ },
+
+ _onEntityStateChanged: function (entity, state, error) {
+ if (this._haveEntity(entity)) {
+ if (state === upshot.EntityState.Deleted) {
+ // Entities deleted from our cache (due to an accepted server delete or due to a
+ // reverted internal add) should disappear from all dependent EntityViews.
+ this._purgeEntity(entity);
+ }
+
+ // Translate entity state changes from our input EntitySource onto our result, if appropriate.
+ // NOTE: _haveEntity will be with respect to our current, stable set of result entities.
+ // Ignoring direct, observable inserts and removes, this result set will only change
+ // as part of our separate recompute wave, which happens _after_ such change events.
+ this._trigger("entityStateChanged", entity, state, error);
+ }
+ },
+
+ _onEntityUpdated: function (entity, path, eventArgs) {
+ // Translate property changes from our input EntitySource onto our result, if appropriate.
+ // NOTE: _haveEntity will be with respect to our current, stable set of result entities.
+ // Ignoring direct, observable inserts and removes, this result set will only change
+ // as part of our separate recompute wave, which happens _after_ such data change events.
+ if (this._haveEntity(entity)) {
+ this._trigger("entityUpdated", entity, path, eventArgs);
+ }
+ }
+ };
+
+ upshot.EntityView = upshot.deriveClass(base, ctor, instanceMembers);
+
+ upshot.EntityView.__dataContextMethodNames = dataContextMethodNames;
+}
+///#RESTORE )(this, jQuery, upshot);
diff --git a/src/SPA/upshot/IntelliSense/Dependencies.js b/src/SPA/upshot/IntelliSense/Dependencies.js
new file mode 100644
index 00000000..2d77dbea
--- /dev/null
+++ b/src/SPA/upshot/IntelliSense/Dependencies.js
@@ -0,0 +1,5 @@
+/// <reference path="jquery-1.5.2-vsdoc.js" />
+/// <reference path="knockout-2.0.0.debug.js" />
+// top-level objects - these could possibly be transitioned to script dependencies
+var upshot = {}, WinJS = {};
+WinJS.Namespace = {};
diff --git a/src/SPA/upshot/IntelliSense/References.js b/src/SPA/upshot/IntelliSense/References.js
new file mode 100644
index 00000000..e1aa5919
--- /dev/null
+++ b/src/SPA/upshot/IntelliSense/References.js
@@ -0,0 +1,21 @@
+/// <reference path="Dependencies.js" />
+/// <reference path="..\Core.js" />
+/// <reference path="..\AssociatedEntitiesView.js" />
+/// <reference path="..\Upshot.Compat.jQueryUI.js" />
+/// <reference path="..\Upshot.Compat.JsViews.js" />
+/// <reference path="..\Upshot.Compat.Knockout.js" />
+/// <reference path="..\Upshot.Compat.WinJS.js" />
+/// <reference path="..\DataContext.js" />
+/// <reference path="..\DataProvider.js" />
+/// <reference path="..\DataProvider.OData.js" />
+/// <reference path="..\DataProvider.datacontroller.js" />
+/// <reference path="..\DataProvider.json.js" />
+/// <reference path="..\DataSource.js" />
+/// <reference path="..\EntitySet.js" />
+/// <reference path="..\EntitySource.js" />
+/// <reference path="..\EntityView.js" />
+/// <reference path="..\LocalDataSource.js" />
+/// <reference path="..\Observability.js" />
+/// <reference path="..\RemoteDataSource.js" />
+
+// This file enables VS IntelliSense in JavaScript, otherwise it can be removed \ No newline at end of file
diff --git a/src/SPA/upshot/IntelliSense/jquery-1.5.2-vsdoc.js b/src/SPA/upshot/IntelliSense/jquery-1.5.2-vsdoc.js
new file mode 100644
index 00000000..7774a0ad
--- /dev/null
+++ b/src/SPA/upshot/IntelliSense/jquery-1.5.2-vsdoc.js
@@ -0,0 +1,6651 @@
+/*
+* This file has been generated to support Visual Studio IntelliSense.
+* You should not use this file at runtime inside the browser--it is only
+* intended to be used only for design-time IntelliSense. Please use the
+* standard jQuery library for all production use.
+*
+* Comment version: 1.5.2
+*/
+
+/*!
+* jQuery JavaScript Library v1.5.2
+* http://jquery.com/
+*
+* Distributed in whole under the terms of the MIT
+*
+* Copyright 2010, John Resig
+*
+* Permission is hereby granted, free of charge, to any person obtaining
+* a copy of this software and associated documentation files (the
+* "Software"), to deal in the Software without restriction, including
+* without limitation the rights to use, copy, modify, merge, publish,
+* distribute, sublicense, and/or sell copies of the Software, and to
+* permit persons to whom the Software is furnished to do so, subject to
+* the following conditions:
+*
+* The above copyright notice and this permission notice shall be
+* included in all copies or substantial portions of the Software.
+*
+* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*
+* Includes Sizzle.js
+* http://sizzlejs.com/
+* Copyright 2010, The Dojo Foundation
+* Released under the MIT and BSD Licenses.
+*/
+
+(function ( window, undefined ) {
+var jQuery = function( selector, context ) {
+/// <summary>
+/// 1: Accepts a string containing a CSS selector which is then used to match a set of elements.
+/// &#10; 1.1 - $(selector, context)
+/// &#10; 1.2 - $(element)
+/// &#10; 1.3 - $(elementArray)
+/// &#10; 1.4 - $(jQuery object)
+/// &#10; 1.5 - $()
+/// &#10;2: Creates DOM elements on the fly from the provided string of raw HTML.
+/// &#10; 2.1 - $(html, ownerDocument)
+/// &#10; 2.2 - $(html, props)
+/// &#10;3: Binds a function to be executed when the DOM has finished loading.
+/// &#10; 3.1 - $(callback)
+/// </summary>
+/// <param name="selector" type="String">
+/// A string containing a selector expression
+/// </param>
+/// <param name="context" type="jQuery">
+/// A DOM Element, Document, or jQuery to use as context
+/// </param>
+/// <returns type="jQuery" />
+
+ // The jQuery object is actually just the init constructor 'enhanced'
+ return new jQuery.fn.init( selector, context, rootjQuery );
+ };
+jQuery.Deferred = function( func ) {
+
+ var deferred = jQuery._Deferred(),
+ failDeferred = jQuery._Deferred(),
+ promise;
+ // Add errorDeferred methods, then and promise
+ jQuery.extend( deferred, {
+ then: function( doneCallbacks, failCallbacks ) {
+ deferred.done( doneCallbacks ).fail( failCallbacks );
+ return this;
+ },
+ fail: failDeferred.done,
+ rejectWith: failDeferred.resolveWith,
+ reject: failDeferred.resolve,
+ isRejected: failDeferred.isResolved,
+ // Get a promise for this deferred
+ // If obj is provided, the promise aspect is added to the object
+ promise: function( obj ) {
+ if ( obj == null ) {
+ if ( promise ) {
+ return promise;
+ }
+ promise = obj = {};
+ }
+ var i = promiseMethods.length;
+ while( i-- ) {
+ obj[ promiseMethods[i] ] = deferred[ promiseMethods[i] ];
+ }
+ return obj;
+ }
+ } );
+ // Make sure only one callback list will be used
+ deferred.done( failDeferred.cancel ).fail( deferred.cancel );
+ // Unexpose cancel
+ delete deferred.cancel;
+ // Call given func if any
+ if ( func ) {
+ func.call( deferred, deferred );
+ }
+ return deferred;
+ };
+jQuery.Event = function( src ) {
+
+ // Allow instantiation without the 'new' keyword
+ if ( !this.preventDefault ) {
+ return new jQuery.Event( src );
+ }
+
+ // Event object
+ if ( src && src.type ) {
+ this.originalEvent = src;
+ this.type = src.type;
+
+ // Events bubbling up the document may have been marked as prevented
+ // by a handler lower down the tree; reflect the correct value.
+ this.isDefaultPrevented = (src.defaultPrevented || src.returnValue === false ||
+ src.getPreventDefault && src.getPreventDefault()) ? returnTrue : returnFalse;
+
+ // Event type
+ } else {
+ this.type = src;
+ }
+
+ // timeStamp is buggy for some events on Firefox(#3843)
+ // So we won't rely on the native value
+ this.timeStamp = jQuery.now();
+
+ // Mark it as fixed
+ this[ jQuery.expando ] = true;
+};
+jQuery._Deferred = function() {
+
+ var // callbacks list
+ callbacks = [],
+ // stored [ context , args ]
+ fired,
+ // to avoid firing when already doing so
+ firing,
+ // flag to know if the deferred has been cancelled
+ cancelled,
+ // the deferred itself
+ deferred = {
+
+ // done( f1, f2, ...)
+ done: function() {
+ if ( !cancelled ) {
+ var args = arguments,
+ i,
+ length,
+ elem,
+ type,
+ _fired;
+ if ( fired ) {
+ _fired = fired;
+ fired = 0;
+ }
+ for ( i = 0, length = args.length; i < length; i++ ) {
+ elem = args[ i ];
+ type = jQuery.type( elem );
+ if ( type === "array" ) {
+ deferred.done.apply( deferred, elem );
+ } else if ( type === "function" ) {
+ callbacks.push( elem );
+ }
+ }
+ if ( _fired ) {
+ deferred.resolveWith( _fired[ 0 ], _fired[ 1 ] );
+ }
+ }
+ return this;
+ },
+
+ // resolve with given context and args
+ resolveWith: function( context, args ) {
+ if ( !cancelled && !fired && !firing ) {
+ // make sure args are available (#8421)
+ args = args || [];
+ firing = 1;
+ try {
+ while( callbacks[ 0 ] ) {
+ callbacks.shift().apply( context, args );
+ }
+ }
+ finally {
+ fired = [ context, args ];
+ firing = 0;
+ }
+ }
+ return this;
+ },
+
+ // resolve with this as context and given arguments
+ resolve: function() {
+ deferred.resolveWith( this, arguments );
+ return this;
+ },
+
+ // Has this deferred been resolved?
+ isResolved: function() {
+ return !!( firing || fired );
+ },
+
+ // Cancel
+ cancel: function() {
+ cancelled = 1;
+ callbacks = [];
+ return this;
+ }
+ };
+
+ return deferred;
+ };
+jQuery._data = function( elem, name, data ) {
+
+ return jQuery.data( elem, name, data, true );
+ };
+jQuery.acceptData = function( elem ) {
+
+ if ( elem.nodeName ) {
+ var match = jQuery.noData[ elem.nodeName.toLowerCase() ];
+
+ if ( match ) {
+ return !(match === true || elem.getAttribute("classid") !== match);
+ }
+ }
+
+ return true;
+ };
+jQuery.access = function( elems, key, value, exec, fn, pass ) {
+
+ var length = elems.length;
+
+ // Setting many attributes
+ if ( typeof key === "object" ) {
+ for ( var k in key ) {
+ jQuery.access( elems, k, key[k], exec, fn, value );
+ }
+ return elems;
+ }
+
+ // Setting one attribute
+ if ( value !== undefined ) {
+ // Optionally, function values get executed if exec is true
+ exec = !pass && exec && jQuery.isFunction(value);
+
+ for ( var i = 0; i < length; i++ ) {
+ fn( elems[i], key, exec ? value.call( elems[i], i, fn( elems[i], key ) ) : value, pass );
+ }
+
+ return elems;
+ }
+
+ // Getting an attribute
+ return length ? fn( elems[0], key ) : undefined;
+ };
+jQuery.active = 0;
+jQuery.ajax = function( url, options ) {
+/// <summary>
+/// Perform an asynchronous HTTP (Ajax) request.
+/// &#10;1 - jQuery.ajax(url, settings)
+/// &#10;2 - jQuery.ajax(settings)
+/// </summary>
+/// <param name="url" type="String">
+/// A string containing the URL to which the request is sent.
+/// </param>
+/// <param name="options" type="Object">
+/// A set of key/value pairs that configure the Ajax request. All settings are optional. A default can be set for any option with $.ajaxSetup(). See jQuery.ajax( settings ) below for a complete list of all settings.
+/// </param>
+
+
+ // If url is an object, simulate pre-1.5 signature
+ if ( typeof url === "object" ) {
+ options = url;
+ url = undefined;
+ }
+
+ // Force options to be an object
+ options = options || {};
+
+ var // Create the final options object
+ s = jQuery.ajaxSetup( {}, options ),
+ // Callbacks context
+ callbackContext = s.context || s,
+ // Context for global events
+ // It's the callbackContext if one was provided in the options
+ // and if it's a DOM node or a jQuery collection
+ globalEventContext = callbackContext !== s &&
+ ( callbackContext.nodeType || callbackContext instanceof jQuery ) ?
+ jQuery( callbackContext ) : jQuery.event,
+ // Deferreds
+ deferred = jQuery.Deferred(),
+ completeDeferred = jQuery._Deferred(),
+ // Status-dependent callbacks
+ statusCode = s.statusCode || {},
+ // ifModified key
+ ifModifiedKey,
+ // Headers (they are sent all at once)
+ requestHeaders = {},
+ // Response headers
+ responseHeadersString,
+ responseHeaders,
+ // transport
+ transport,
+ // timeout handle
+ timeoutTimer,
+ // Cross-domain detection vars
+ parts,
+ // The jqXHR state
+ state = 0,
+ // To know if global events are to be dispatched
+ fireGlobals,
+ // Loop variable
+ i,
+ // Fake xhr
+ jqXHR = {
+
+ readyState: 0,
+
+ // Caches the header
+ setRequestHeader: function( name, value ) {
+ if ( !state ) {
+ requestHeaders[ name.toLowerCase().replace( rucHeaders, rucHeadersFunc ) ] = value;
+ }
+ return this;
+ },
+
+ // Raw string
+ getAllResponseHeaders: function() {
+ return state === 2 ? responseHeadersString : null;
+ },
+
+ // Builds headers hashtable if needed
+ getResponseHeader: function( key ) {
+ var match;
+ if ( state === 2 ) {
+ if ( !responseHeaders ) {
+ responseHeaders = {};
+ while( ( match = rheaders.exec( responseHeadersString ) ) ) {
+ responseHeaders[ match[1].toLowerCase() ] = match[ 2 ];
+ }
+ }
+ match = responseHeaders[ key.toLowerCase() ];
+ }
+ return match === undefined ? null : match;
+ },
+
+ // Overrides response content-type header
+ overrideMimeType: function( type ) {
+ if ( !state ) {
+ s.mimeType = type;
+ }
+ return this;
+ },
+
+ // Cancel the request
+ abort: function( statusText ) {
+ statusText = statusText || "abort";
+ if ( transport ) {
+ transport.abort( statusText );
+ }
+ done( 0, statusText );
+ return this;
+ }
+ };
+
+ // Callback for when everything is done
+ // It is defined here because jslint complains if it is declared
+ // at the end of the function (which would be more logical and readable)
+ function done( status, statusText, responses, headers ) {
+
+ // Called once
+ if ( state === 2 ) {
+ return;
+ }
+
+ // State is "done" now
+ state = 2;
+
+ // Clear timeout if it exists
+ if ( timeoutTimer ) {
+ clearTimeout( timeoutTimer );
+ }
+
+ // Dereference transport for early garbage collection
+ // (no matter how long the jqXHR object will be used)
+ transport = undefined;
+
+ // Cache response headers
+ responseHeadersString = headers || "";
+
+ // Set readyState
+ jqXHR.readyState = status ? 4 : 0;
+
+ var isSuccess,
+ success,
+ error,
+ response = responses ? ajaxHandleResponses( s, jqXHR, responses ) : undefined,
+ lastModified,
+ etag;
+
+ // If successful, handle type chaining
+ if ( status >= 200 && status < 300 || status === 304 ) {
+
+ // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
+ if ( s.ifModified ) {
+
+ if ( ( lastModified = jqXHR.getResponseHeader( "Last-Modified" ) ) ) {
+ jQuery.lastModified[ ifModifiedKey ] = lastModified;
+ }
+ if ( ( etag = jqXHR.getResponseHeader( "Etag" ) ) ) {
+ jQuery.etag[ ifModifiedKey ] = etag;
+ }
+ }
+
+ // If not modified
+ if ( status === 304 ) {
+
+ statusText = "notmodified";
+ isSuccess = true;
+
+ // If we have data
+ } else {
+
+ try {
+ success = ajaxConvert( s, response );
+ statusText = "success";
+ isSuccess = true;
+ } catch(e) {
+ // We have a parsererror
+ statusText = "parsererror";
+ error = e;
+ }
+ }
+ } else {
+ // We extract error from statusText
+ // then normalize statusText and status for non-aborts
+ error = statusText;
+ if( !statusText || status ) {
+ statusText = "error";
+ if ( status < 0 ) {
+ status = 0;
+ }
+ }
+ }
+
+ // Set data for the fake xhr object
+ jqXHR.status = status;
+ jqXHR.statusText = statusText;
+
+ // Success/Error
+ if ( isSuccess ) {
+ deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] );
+ } else {
+ deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] );
+ }
+
+ // Status-dependent callbacks
+ jqXHR.statusCode( statusCode );
+ statusCode = undefined;
+
+ if ( fireGlobals ) {
+ globalEventContext.trigger( "ajax" + ( isSuccess ? "Success" : "Error" ),
+ [ jqXHR, s, isSuccess ? success : error ] );
+ }
+
+ // Complete
+ completeDeferred.resolveWith( callbackContext, [ jqXHR, statusText ] );
+
+ if ( fireGlobals ) {
+ globalEventContext.trigger( "ajaxComplete", [ jqXHR, s] );
+ // Handle the global AJAX counter
+ if ( !( --jQuery.active ) ) {
+ jQuery.event.trigger( "ajaxStop" );
+ }
+ }
+ }
+
+ // Attach deferreds
+ deferred.promise( jqXHR );
+ jqXHR.success = jqXHR.done;
+ jqXHR.error = jqXHR.fail;
+ jqXHR.complete = completeDeferred.done;
+
+ // Status-dependent callbacks
+ jqXHR.statusCode = function( map ) {
+ if ( map ) {
+ var tmp;
+ if ( state < 2 ) {
+ for( tmp in map ) {
+ statusCode[ tmp ] = [ statusCode[tmp], map[tmp] ];
+ }
+ } else {
+ tmp = map[ jqXHR.status ];
+ jqXHR.then( tmp, tmp );
+ }
+ }
+ return this;
+ };
+
+ // Remove hash character (#7531: and string promotion)
+ // Add protocol if not provided (#5866: IE7 issue with protocol-less urls)
+ // We also use the url parameter if available
+ s.url = ( ( url || s.url ) + "" ).replace( rhash, "" ).replace( rprotocol, ajaxLocParts[ 1 ] + "//" );
+
+ // Extract dataTypes list
+ s.dataTypes = jQuery.trim( s.dataType || "*" ).toLowerCase().split( rspacesAjax );
+
+ // Determine if a cross-domain request is in order
+ if ( s.crossDomain == null ) {
+ parts = rurl.exec( s.url.toLowerCase() );
+ s.crossDomain = !!( parts &&
+ ( parts[ 1 ] != ajaxLocParts[ 1 ] || parts[ 2 ] != ajaxLocParts[ 2 ] ||
+ ( parts[ 3 ] || ( parts[ 1 ] === "http:" ? 80 : 443 ) ) !=
+ ( ajaxLocParts[ 3 ] || ( ajaxLocParts[ 1 ] === "http:" ? 80 : 443 ) ) )
+ );
+ }
+
+ // Convert data if not already a string
+ if ( s.data && s.processData && typeof s.data !== "string" ) {
+ s.data = jQuery.param( s.data, s.traditional );
+ }
+
+ // Apply prefilters
+ inspectPrefiltersOrTransports( prefilters, s, options, jqXHR );
+
+ // If request was aborted inside a prefiler, stop there
+ if ( state === 2 ) {
+ return false;
+ }
+
+ // We can fire global events as of now if asked to
+ fireGlobals = s.global;
+
+ // Uppercase the type
+ s.type = s.type.toUpperCase();
+
+ // Determine if request has content
+ s.hasContent = !rnoContent.test( s.type );
+
+ // Watch for a new set of requests
+ if ( fireGlobals && jQuery.active++ === 0 ) {
+ jQuery.event.trigger( "ajaxStart" );
+ }
+
+ // More options handling for requests with no content
+ if ( !s.hasContent ) {
+
+ // If data is available, append data to url
+ if ( s.data ) {
+ s.url += ( rquery.test( s.url ) ? "&" : "?" ) + s.data;
+ }
+
+ // Get ifModifiedKey before adding the anti-cache parameter
+ ifModifiedKey = s.url;
+
+ // Add anti-cache in url if needed
+ if ( s.cache === false ) {
+
+ var ts = jQuery.now(),
+ // try replacing _= if it is there
+ ret = s.url.replace( rts, "$1_=" + ts );
+
+ // if nothing was replaced, add timestamp to the end
+ s.url = ret + ( (ret === s.url ) ? ( rquery.test( s.url ) ? "&" : "?" ) + "_=" + ts : "" );
+ }
+ }
+
+ // Set the correct header, if data is being sent
+ if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) {
+ requestHeaders[ "Content-Type" ] = s.contentType;
+ }
+
+ // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
+ if ( s.ifModified ) {
+ ifModifiedKey = ifModifiedKey || s.url;
+ if ( jQuery.lastModified[ ifModifiedKey ] ) {
+ requestHeaders[ "If-Modified-Since" ] = jQuery.lastModified[ ifModifiedKey ];
+ }
+ if ( jQuery.etag[ ifModifiedKey ] ) {
+ requestHeaders[ "If-None-Match" ] = jQuery.etag[ ifModifiedKey ];
+ }
+ }
+
+ // Set the Accepts header for the server, depending on the dataType
+ requestHeaders.Accept = s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[0] ] ?
+ s.accepts[ s.dataTypes[0] ] + ( s.dataTypes[ 0 ] !== "*" ? ", */*; q=0.01" : "" ) :
+ s.accepts[ "*" ];
+
+ // Check for headers option
+ for ( i in s.headers ) {
+ jqXHR.setRequestHeader( i, s.headers[ i ] );
+ }
+
+ // Allow custom headers/mimetypes and early abort
+ if ( s.beforeSend && ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || state === 2 ) ) {
+ // Abort if not done already
+ jqXHR.abort();
+ return false;
+
+ }
+
+ // Install callbacks on deferreds
+ for ( i in { success: 1, error: 1, complete: 1 } ) {
+ jqXHR[ i ]( s[ i ] );
+ }
+
+ // Get transport
+ transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR );
+
+ // If no transport, we auto-abort
+ if ( !transport ) {
+ done( -1, "No Transport" );
+ } else {
+ jqXHR.readyState = 1;
+ // Send global event
+ if ( fireGlobals ) {
+ globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] );
+ }
+ // Timeout
+ if ( s.async && s.timeout > 0 ) {
+ timeoutTimer = setTimeout( function(){
+ jqXHR.abort( "timeout" );
+ }, s.timeout );
+ }
+
+ try {
+ state = 1;
+ transport.send( requestHeaders, done );
+ } catch (e) {
+ // Propagate exception as error if not done
+ if ( status < 2 ) {
+ done( -1, e );
+ // Simply rethrow otherwise
+ } else {
+ jQuery.error( e );
+ }
+ }
+ }
+
+ return jqXHR;
+ };
+jQuery.ajaxPrefilter = function( dataTypeExpression, func ) {
+
+
+ if ( typeof dataTypeExpression !== "string" ) {
+ func = dataTypeExpression;
+ dataTypeExpression = "*";
+ }
+
+ if ( jQuery.isFunction( func ) ) {
+ var dataTypes = dataTypeExpression.toLowerCase().split( rspacesAjax ),
+ i = 0,
+ length = dataTypes.length,
+ dataType,
+ list,
+ placeBefore;
+
+ // For each dataType in the dataTypeExpression
+ for(; i < length; i++ ) {
+ dataType = dataTypes[ i ];
+ // We control if we're asked to add before
+ // any existing element
+ placeBefore = /^\+/.test( dataType );
+ if ( placeBefore ) {
+ dataType = dataType.substr( 1 ) || "*";
+ }
+ list = structure[ dataType ] = structure[ dataType ] || [];
+ // then we add to the structure accordingly
+ list[ placeBefore ? "unshift" : "push" ]( func );
+ }
+ }
+ };
+jQuery.ajaxSettings = { "url": 'http://localhost:25813/',
+"isLocal": false,
+"global": true,
+"type": 'GET',
+"contentType": 'application/x-www-form-urlencoded',
+"processData": true,
+"async": true,
+"accepts": {},
+"contents": {},
+"responseFields": {},
+"converters": {},
+"jsonp": 'callback' };
+jQuery.ajaxSetup = function ( target, settings ) {
+/// <summary>
+/// Set default values for future Ajax requests.
+/// </summary>
+/// <param name="target" type="Object">
+/// A set of key/value pairs that configure the default Ajax request. All options are optional.
+/// </param>
+
+ if ( !settings ) {
+ // Only one parameter, we extend ajaxSettings
+ settings = target;
+ target = jQuery.extend( true, jQuery.ajaxSettings, settings );
+ } else {
+ // target was provided, we extend into it
+ jQuery.extend( true, target, jQuery.ajaxSettings, settings );
+ }
+ // Flatten fields we don't want deep extended
+ for( var field in { context: 1, url: 1 } ) {
+ if ( field in settings ) {
+ target[ field ] = settings[ field ];
+ } else if( field in jQuery.ajaxSettings ) {
+ target[ field ] = jQuery.ajaxSettings[ field ];
+ }
+ }
+ return target;
+ };
+jQuery.ajaxTransport = function( dataTypeExpression, func ) {
+
+
+ if ( typeof dataTypeExpression !== "string" ) {
+ func = dataTypeExpression;
+ dataTypeExpression = "*";
+ }
+
+ if ( jQuery.isFunction( func ) ) {
+ var dataTypes = dataTypeExpression.toLowerCase().split( rspacesAjax ),
+ i = 0,
+ length = dataTypes.length,
+ dataType,
+ list,
+ placeBefore;
+
+ // For each dataType in the dataTypeExpression
+ for(; i < length; i++ ) {
+ dataType = dataTypes[ i ];
+ // We control if we're asked to add before
+ // any existing element
+ placeBefore = /^\+/.test( dataType );
+ if ( placeBefore ) {
+ dataType = dataType.substr( 1 ) || "*";
+ }
+ list = structure[ dataType ] = structure[ dataType ] || [];
+ // then we add to the structure accordingly
+ list[ placeBefore ? "unshift" : "push" ]( func );
+ }
+ }
+ };
+jQuery.attr = function( elem, name, value, pass ) {
+
+ // don't get/set attributes on text, comment and attribute nodes
+ if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || elem.nodeType === 2 ) {
+ return undefined;
+ }
+
+ if ( pass && name in jQuery.attrFn ) {
+ return jQuery(elem)[name](value);
+ }
+
+ var notxml = elem.nodeType !== 1 || !jQuery.isXMLDoc( elem ),
+ // Whether we are setting (or getting)
+ set = value !== undefined;
+
+ // Try to normalize/fix the name
+ name = notxml && jQuery.props[ name ] || name;
+
+ // Only do all the following if this is a node (faster for style)
+ if ( elem.nodeType === 1 ) {
+ // These attributes require special treatment
+ var special = rspecialurl.test( name );
+
+ // Safari mis-reports the default selected property of an option
+ // Accessing the parent's selectedIndex property fixes it
+ if ( name === "selected" && !jQuery.support.optSelected ) {
+ var parent = elem.parentNode;
+ if ( parent ) {
+ parent.selectedIndex;
+
+ // Make sure that it also works with optgroups, see #5701
+ if ( parent.parentNode ) {
+ parent.parentNode.selectedIndex;
+ }
+ }
+ }
+
+ // If applicable, access the attribute via the DOM 0 way
+ // 'in' checks fail in Blackberry 4.7 #6931
+ if ( (name in elem || elem[ name ] !== undefined) && notxml && !special ) {
+ if ( set ) {
+ // We can't allow the type property to be changed (since it causes problems in IE)
+ if ( name === "type" && rtype.test( elem.nodeName ) && elem.parentNode ) {
+ jQuery.error( "type property can't be changed" );
+ }
+
+ if ( value === null ) {
+ if ( elem.nodeType === 1 ) {
+ elem.removeAttribute( name );
+ }
+
+ } else {
+ elem[ name ] = value;
+ }
+ }
+
+ // browsers index elements by id/name on forms, give priority to attributes.
+ if ( jQuery.nodeName( elem, "form" ) && elem.getAttributeNode(name) ) {
+ return elem.getAttributeNode( name ).nodeValue;
+ }
+
+ // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set
+ // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/
+ if ( name === "tabIndex" ) {
+ var attributeNode = elem.getAttributeNode( "tabIndex" );
+
+ return attributeNode && attributeNode.specified ?
+ attributeNode.value :
+ rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ?
+ 0 :
+ undefined;
+ }
+
+ return elem[ name ];
+ }
+
+ if ( !jQuery.support.style && notxml && name === "style" ) {
+ if ( set ) {
+ elem.style.cssText = "" + value;
+ }
+
+ return elem.style.cssText;
+ }
+
+ if ( set ) {
+ // convert the value to a string (all browsers do this but IE) see #1070
+ elem.setAttribute( name, "" + value );
+ }
+
+ // Ensure that missing attributes return undefined
+ // Blackberry 4.7 returns "" from getAttribute #6938
+ if ( !elem.attributes[ name ] && (elem.hasAttribute && !elem.hasAttribute( name )) ) {
+ return undefined;
+ }
+
+ var attr = !jQuery.support.hrefNormalized && notxml && special ?
+ // Some attributes require a special call on IE
+ elem.getAttribute( name, 2 ) :
+ elem.getAttribute( name );
+
+ // Non-existent attributes return null, we normalize to undefined
+ return attr === null ? undefined : attr;
+ }
+ // Handle everything which isn't a DOM element node
+ if ( set ) {
+ elem[ name ] = value;
+ }
+ return elem[ name ];
+ };
+jQuery.attrFn = { "val": true,
+"css": true,
+"html": true,
+"text": true,
+"data": true,
+"width": true,
+"height": true,
+"offset": true,
+"blur": true,
+"focus": true,
+"focusin": true,
+"focusout": true,
+"load": true,
+"resize": true,
+"scroll": true,
+"unload": true,
+"click": true,
+"dblclick": true,
+"mousedown": true,
+"mouseup": true,
+"mousemove": true,
+"mouseover": true,
+"mouseout": true,
+"mouseenter": true,
+"mouseleave": true,
+"change": true,
+"select": true,
+"submit": true,
+"keydown": true,
+"keypress": true,
+"keyup": true,
+"error": true };
+jQuery.bindReady = function() {
+
+ if ( readyList ) {
+ return;
+ }
+
+ readyList = jQuery._Deferred();
+
+ // Catch cases where $(document).ready() is called after the
+ // browser event has already occurred.
+ if ( document.readyState === "complete" ) {
+ // Handle it asynchronously to allow scripts the opportunity to delay ready
+ return setTimeout( jQuery.ready, 1 );
+ }
+
+ // Mozilla, Opera and webkit nightlies currently support this event
+ if ( document.addEventListener ) {
+ // Use the handy event callback
+ document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
+
+ // A fallback to window.onload, that will always work
+ window.addEventListener( "load", jQuery.ready, false );
+
+ // If IE event model is used
+ } else if ( document.attachEvent ) {
+ // ensure firing before onload,
+ // maybe late but safe also for iframes
+ document.attachEvent("onreadystatechange", DOMContentLoaded);
+
+ // A fallback to window.onload, that will always work
+ window.attachEvent( "onload", jQuery.ready );
+
+ // If IE and not a frame
+ // continually check to see if the document is ready
+ var toplevel = false;
+
+ try {
+ toplevel = window.frameElement == null;
+ } catch(e) {}
+
+ if ( document.documentElement.doScroll && toplevel ) {
+ doScrollCheck();
+ }
+ }
+ };
+jQuery.boxModel = true;
+jQuery.browser = { "msie": true,
+"version": '9.0' };
+jQuery.buildFragment = function( args, nodes, scripts ) {
+
+ var fragment, cacheable, cacheresults,
+ doc = (nodes && nodes[0] ? nodes[0].ownerDocument || nodes[0] : document);
+
+ // Only cache "small" (1/2 KB) HTML strings that are associated with the main document
+ // Cloning options loses the selected state, so don't cache them
+ // IE 6 doesn't like it when you put <object> or <embed> elements in a fragment
+ // Also, WebKit does not clone 'checked' attributes on cloneNode, so don't cache
+ if ( args.length === 1 && typeof args[0] === "string" && args[0].length < 512 && doc === document &&
+ args[0].charAt(0) === "<" && !rnocache.test( args[0] ) && (jQuery.support.checkClone || !rchecked.test( args[0] )) ) {
+
+ cacheable = true;
+ cacheresults = jQuery.fragments[ args[0] ];
+ if ( cacheresults ) {
+ if ( cacheresults !== 1 ) {
+ fragment = cacheresults;
+ }
+ }
+ }
+
+ if ( !fragment ) {
+ fragment = doc.createDocumentFragment();
+ jQuery.clean( args, doc, fragment, scripts );
+ }
+
+ if ( cacheable ) {
+ jQuery.fragments[ args[0] ] = cacheresults ? fragment : 1;
+ }
+
+ return { fragment: fragment, cacheable: cacheable };
+};
+jQuery.cache = {};
+jQuery.camelCase = function( string ) {
+
+ return string.replace( rdashAlpha, fcamelCase );
+ };
+jQuery.clean = function( elems, context, fragment, scripts ) {
+
+ context = context || document;
+
+ // !context.createElement fails in IE with an error but returns typeof 'object'
+ if ( typeof context.createElement === "undefined" ) {
+ context = context.ownerDocument || context[0] && context[0].ownerDocument || document;
+ }
+
+ var ret = [];
+
+ for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
+ if ( typeof elem === "number" ) {
+ elem += "";
+ }
+
+ if ( !elem ) {
+ continue;
+ }
+
+ // Convert html string into DOM nodes
+ if ( typeof elem === "string" && !rhtml.test( elem ) ) {
+ elem = context.createTextNode( elem );
+
+ } else if ( typeof elem === "string" ) {
+ // Fix "XHTML"-style tags in all browsers
+ elem = elem.replace(rxhtmlTag, "<$1></$2>");
+
+ // Trim whitespace, otherwise indexOf won't work as expected
+ var tag = (rtagName.exec( elem ) || ["", ""])[1].toLowerCase(),
+ wrap = wrapMap[ tag ] || wrapMap._default,
+ depth = wrap[0],
+ div = context.createElement("div");
+
+ // Go to html and back, then peel off extra wrappers
+ div.innerHTML = wrap[1] + elem + wrap[2];
+
+ // Move to the right depth
+ while ( depth-- ) {
+ div = div.lastChild;
+ }
+
+ // Remove IE's autoinserted <tbody> from table fragments
+ if ( !jQuery.support.tbody ) {
+
+ // String was a <table>, *may* have spurious <tbody>
+ var hasBody = rtbody.test(elem),
+ tbody = tag === "table" && !hasBody ?
+ div.firstChild && div.firstChild.childNodes :
+
+ // String was a bare <thead> or <tfoot>
+ wrap[1] === "<table>" && !hasBody ?
+ div.childNodes :
+ [];
+
+ for ( var j = tbody.length - 1; j >= 0 ; --j ) {
+ if ( jQuery.nodeName( tbody[ j ], "tbody" ) && !tbody[ j ].childNodes.length ) {
+ tbody[ j ].parentNode.removeChild( tbody[ j ] );
+ }
+ }
+
+ }
+
+ // IE completely kills leading whitespace when innerHTML is used
+ if ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) {
+ div.insertBefore( context.createTextNode( rleadingWhitespace.exec(elem)[0] ), div.firstChild );
+ }
+
+ elem = div.childNodes;
+ }
+
+ if ( elem.nodeType ) {
+ ret.push( elem );
+ } else {
+ ret = jQuery.merge( ret, elem );
+ }
+ }
+
+ if ( fragment ) {
+ for ( i = 0; ret[i]; i++ ) {
+ if ( scripts && jQuery.nodeName( ret[i], "script" ) && (!ret[i].type || ret[i].type.toLowerCase() === "text/javascript") ) {
+ scripts.push( ret[i].parentNode ? ret[i].parentNode.removeChild( ret[i] ) : ret[i] );
+
+ } else {
+ if ( ret[i].nodeType === 1 ) {
+ ret.splice.apply( ret, [i + 1, 0].concat(jQuery.makeArray(ret[i].getElementsByTagName("script"))) );
+ }
+ fragment.appendChild( ret[i] );
+ }
+ }
+ }
+
+ return ret;
+ };
+jQuery.cleanData = function( elems ) {
+
+ var data, id, cache = jQuery.cache, internalKey = jQuery.expando, special = jQuery.event.special,
+ deleteExpando = jQuery.support.deleteExpando;
+
+ for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
+ if ( elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()] ) {
+ continue;
+ }
+
+ id = elem[ jQuery.expando ];
+
+ if ( id ) {
+ data = cache[ id ] && cache[ id ][ internalKey ];
+
+ if ( data && data.events ) {
+ for ( var type in data.events ) {
+ if ( special[ type ] ) {
+ jQuery.event.remove( elem, type );
+
+ // This is a shortcut to avoid jQuery.event.remove's overhead
+ } else {
+ jQuery.removeEvent( elem, type, data.handle );
+ }
+ }
+
+ // Null the DOM reference to avoid IE6/7/8 leak (#7054)
+ if ( data.handle ) {
+ data.handle.elem = null;
+ }
+ }
+
+ if ( deleteExpando ) {
+ delete elem[ jQuery.expando ];
+
+ } else if ( elem.removeAttribute ) {
+ elem.removeAttribute( jQuery.expando );
+ }
+
+ delete cache[ id ];
+ }
+ }
+ };
+jQuery.clone = function( elem, dataAndEvents, deepDataAndEvents ) {
+
+ var clone = elem.cloneNode(true),
+ srcElements,
+ destElements,
+ i;
+
+ if ( (!jQuery.support.noCloneEvent || !jQuery.support.noCloneChecked) &&
+ (elem.nodeType === 1 || elem.nodeType === 11) && !jQuery.isXMLDoc(elem) ) {
+ // IE copies events bound via attachEvent when using cloneNode.
+ // Calling detachEvent on the clone will also remove the events
+ // from the original. In order to get around this, we use some
+ // proprietary methods to clear the events. Thanks to MooTools
+ // guys for this hotness.
+
+ cloneFixAttributes( elem, clone );
+
+ // Using Sizzle here is crazy slow, so we use getElementsByTagName
+ // instead
+ srcElements = getAll( elem );
+ destElements = getAll( clone );
+
+ // Weird iteration because IE will replace the length property
+ // with an element if you are cloning the body and one of the
+ // elements on the page has a name or id of "length"
+ for ( i = 0; srcElements[i]; ++i ) {
+ cloneFixAttributes( srcElements[i], destElements[i] );
+ }
+ }
+
+ // Copy the events from the original to the clone
+ if ( dataAndEvents ) {
+ cloneCopyEvent( elem, clone );
+
+ if ( deepDataAndEvents ) {
+ srcElements = getAll( elem );
+ destElements = getAll( clone );
+
+ for ( i = 0; srcElements[i]; ++i ) {
+ cloneCopyEvent( srcElements[i], destElements[i] );
+ }
+ }
+ }
+
+ // Return the cloned set
+ return clone;
+};
+jQuery.contains = function( a, b ) {
+/// <summary>
+/// Check to see if a DOM node is within another DOM node.
+/// </summary>
+/// <param name="a" domElement="true">
+/// The DOM element that may contain the other element.
+/// </param>
+/// <param name="b" domElement="true">
+/// The DOM node that may be contained by the other element.
+/// </param>
+/// <returns type="Boolean" />
+
+ return a !== b && (a.contains ? a.contains(b) : true);
+ };
+jQuery.css = function( elem, name, extra ) {
+
+ // Make sure that we're working with the right name
+ var ret, origName = jQuery.camelCase( name ),
+ hooks = jQuery.cssHooks[ origName ];
+
+ name = jQuery.cssProps[ origName ] || origName;
+
+ // If a hook was provided get the computed value from there
+ if ( hooks && "get" in hooks && (ret = hooks.get( elem, true, extra )) !== undefined ) {
+ return ret;
+
+ // Otherwise, if a way to get the computed value exists, use that
+ } else if ( curCSS ) {
+ return curCSS( elem, name, origName );
+ }
+ };
+jQuery.cssHooks = { "opacity": {},
+"height": {},
+"width": {} };
+jQuery.cssNumber = { "zIndex": true,
+"fontWeight": true,
+"opacity": true,
+"zoom": true,
+"lineHeight": true };
+jQuery.cssProps = { "float": 'cssFloat' };
+jQuery.curCSS = function( elem, name, extra ) {
+
+ // Make sure that we're working with the right name
+ var ret, origName = jQuery.camelCase( name ),
+ hooks = jQuery.cssHooks[ origName ];
+
+ name = jQuery.cssProps[ origName ] || origName;
+
+ // If a hook was provided get the computed value from there
+ if ( hooks && "get" in hooks && (ret = hooks.get( elem, true, extra )) !== undefined ) {
+ return ret;
+
+ // Otherwise, if a way to get the computed value exists, use that
+ } else if ( curCSS ) {
+ return curCSS( elem, name, origName );
+ }
+ };
+jQuery.data = function( elem, name, data, pvt /* Internal Use Only */ ) {
+/// <summary>
+/// 1: Store arbitrary data associated with the specified element.
+/// &#10; 1.1 - jQuery.data(element, key, value)
+/// &#10;2: Returns value at named data store for the element, as set by jQuery.data(element, name, value), or the full data store for the element.
+/// &#10; 2.1 - jQuery.data(element, key)
+/// &#10; 2.2 - jQuery.data(element)
+/// </summary>
+/// <param name="elem" domElement="true">
+/// The DOM element to associate with the data.
+/// </param>
+/// <param name="name" type="String">
+/// A string naming the piece of data to set.
+/// </param>
+/// <param name="data" type="Object">
+/// The new data value.
+/// </param>
+/// <returns type="jQuery" />
+
+ if ( !jQuery.acceptData( elem ) ) {
+ return;
+ }
+
+ var internalKey = jQuery.expando, getByName = typeof name === "string", thisCache,
+
+ // We have to handle DOM nodes and JS objects differently because IE6-7
+ // can't GC object references properly across the DOM-JS boundary
+ isNode = elem.nodeType,
+
+ // Only DOM nodes need the global jQuery cache; JS object data is
+ // attached directly to the object so GC can occur automatically
+ cache = isNode ? jQuery.cache : elem,
+
+ // Only defining an ID for JS objects if its cache already exists allows
+ // the code to shortcut on the same path as a DOM node with no cache
+ id = isNode ? elem[ jQuery.expando ] : elem[ jQuery.expando ] && jQuery.expando;
+
+ // Avoid doing any more work than we need to when trying to get data on an
+ // object that has no data at all
+ if ( (!id || (pvt && id && !cache[ id ][ internalKey ])) && getByName && data === undefined ) {
+ return;
+ }
+
+ if ( !id ) {
+ // Only DOM nodes need a new unique ID for each element since their data
+ // ends up in the global cache
+ if ( isNode ) {
+ elem[ jQuery.expando ] = id = ++jQuery.uuid;
+ } else {
+ id = jQuery.expando;
+ }
+ }
+
+ if ( !cache[ id ] ) {
+ cache[ id ] = {};
+
+ // TODO: This is a hack for 1.5 ONLY. Avoids exposing jQuery
+ // metadata on plain JS objects when the object is serialized using
+ // JSON.stringify
+ if ( !isNode ) {
+ cache[ id ].toJSON = jQuery.noop;
+ }
+ }
+
+ // An object can be passed to jQuery.data instead of a key/value pair; this gets
+ // shallow copied over onto the existing cache
+ if ( typeof name === "object" || typeof name === "function" ) {
+ if ( pvt ) {
+ cache[ id ][ internalKey ] = jQuery.extend(cache[ id ][ internalKey ], name);
+ } else {
+ cache[ id ] = jQuery.extend(cache[ id ], name);
+ }
+ }
+
+ thisCache = cache[ id ];
+
+ // Internal jQuery data is stored in a separate object inside the object's data
+ // cache in order to avoid key collisions between internal data and user-defined
+ // data
+ if ( pvt ) {
+ if ( !thisCache[ internalKey ] ) {
+ thisCache[ internalKey ] = {};
+ }
+
+ thisCache = thisCache[ internalKey ];
+ }
+
+ if ( data !== undefined ) {
+ thisCache[ name ] = data;
+ }
+
+ // TODO: This is a hack for 1.5 ONLY. It will be removed in 1.6. Users should
+ // not attempt to inspect the internal events object using jQuery.data, as this
+ // internal data object is undocumented and subject to change.
+ if ( name === "events" && !thisCache[name] ) {
+ return thisCache[ internalKey ] && thisCache[ internalKey ].events;
+ }
+
+ return getByName ? thisCache[ name ] : thisCache;
+ };
+jQuery.dequeue = function( elem, type ) {
+/// <summary>
+/// Execute the next function on the queue for the matched element.
+/// </summary>
+/// <param name="elem" domElement="true">
+/// A DOM element from which to remove and execute a queued function.
+/// </param>
+/// <param name="type" type="String">
+/// A string containing the name of the queue. Defaults to fx, the standard effects queue.
+/// </param>
+/// <returns type="jQuery" />
+
+ type = type || "fx";
+
+ var queue = jQuery.queue( elem, type ),
+ fn = queue.shift();
+
+ // If the fx queue is dequeued, always remove the progress sentinel
+ if ( fn === "inprogress" ) {
+ fn = queue.shift();
+ }
+
+ if ( fn ) {
+ // Add a progress sentinel to prevent the fx queue from being
+ // automatically dequeued
+ if ( type === "fx" ) {
+ queue.unshift("inprogress");
+ }
+
+ fn.call(elem, function() {
+ jQuery.dequeue(elem, type);
+ });
+ }
+
+ if ( !queue.length ) {
+ jQuery.removeData( elem, type + "queue", true );
+ }
+ };
+jQuery.dir = function( elem, dir, until ) {
+
+ var matched = [],
+ cur = elem[ dir ];
+
+ while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) {
+ if ( cur.nodeType === 1 ) {
+ matched.push( cur );
+ }
+ cur = cur[dir];
+ }
+ return matched;
+ };
+jQuery.each = function( object, callback, args ) {
+/// <summary>
+/// A generic iterator function, which can be used to seamlessly iterate over both objects and arrays. Arrays and array-like objects with a length property (such as a function's arguments object) are iterated by numeric index, from 0 to length-1. Other objects are iterated via their named properties.
+/// </summary>
+/// <param name="object" type="Object">
+/// The object or array to iterate over.
+/// </param>
+/// <param name="callback" type="Function">
+/// The function that will be executed on every object.
+/// </param>
+/// <returns type="Object" />
+
+ var name, i = 0,
+ length = object.length,
+ isObj = length === undefined || jQuery.isFunction(object);
+
+ if ( args ) {
+ if ( isObj ) {
+ for ( name in object ) {
+ if ( callback.apply( object[ name ], args ) === false ) {
+ break;
+ }
+ }
+ } else {
+ for ( ; i < length; ) {
+ if ( callback.apply( object[ i++ ], args ) === false ) {
+ break;
+ }
+ }
+ }
+
+ // A special, fast, case for the most common use of each
+ } else {
+ if ( isObj ) {
+ for ( name in object ) {
+ if ( callback.call( object[ name ], name, object[ name ] ) === false ) {
+ break;
+ }
+ }
+ } else {
+ for ( var value = object[0];
+ i < length && callback.call( value, i, value ) !== false; value = object[++i] ) {}
+ }
+ }
+
+ return object;
+ };
+jQuery.easing = {};
+jQuery.error = function( msg ) {
+/// <summary>
+/// Takes a string and throws an exception containing it.
+/// </summary>
+/// <param name="msg" type="String">
+/// The message to send out.
+/// </param>
+
+ throw msg;
+ };
+jQuery.etag = {};
+jQuery.event = { "global": {},
+"props": ['altKey','attrChange','attrName','bubbles','button','cancelable','charCode','clientX','clientY','ctrlKey','currentTarget','data','detail','eventPhase','fromElement','handler','keyCode','layerX','layerY','metaKey','newValue','offsetX','offsetY','pageX','pageY','prevValue','relatedNode','relatedTarget','screenX','screenY','shiftKey','srcElement','target','toElement','view','wheelDelta','which'],
+"guid": 100000000,
+"special": {},
+"triggered": {} };
+jQuery.expr = { "order": ['ID','CLASS','NAME','TAG'],
+"match": {},
+"leftMatch": {},
+"attrMap": {},
+"attrHandle": {},
+"relative": {},
+"find": {},
+"preFilter": {},
+"filters": {},
+"setFilters": {},
+"filter": {},
+":": {} };
+jQuery.extend = function() {
+/// <summary>
+/// Merge the contents of two or more objects together into the first object.
+/// &#10;1 - jQuery.extend(target, object1, objectN)
+/// &#10;2 - jQuery.extend(deep, target, object1, objectN)
+/// </summary>
+/// <param name="" type="Boolean">
+/// If true, the merge becomes recursive (aka. deep copy).
+/// </param>
+/// <param name="" type="Object">
+/// The object to extend. It will receive the new properties.
+/// </param>
+/// <param name="" type="Object">
+/// An object containing additional properties to merge in.
+/// </param>
+/// <param name="" type="Object">
+/// Additional objects containing properties to merge in.
+/// </param>
+/// <returns type="Object" />
+
+ var options, name, src, copy, copyIsArray, clone,
+ target = arguments[0] || {},
+ i = 1,
+ length = arguments.length,
+ deep = false;
+
+ // Handle a deep copy situation
+ if ( typeof target === "boolean" ) {
+ deep = target;
+ target = arguments[1] || {};
+ // skip the boolean and the target
+ i = 2;
+ }
+
+ // Handle case when target is a string or something (possible in deep copy)
+ if ( typeof target !== "object" && !jQuery.isFunction(target) ) {
+ target = {};
+ }
+
+ // extend jQuery itself if only one argument is passed
+ if ( length === i ) {
+ target = this;
+ --i;
+ }
+
+ for ( ; i < length; i++ ) {
+ // Only deal with non-null/undefined values
+ if ( (options = arguments[ i ]) != null ) {
+ // Extend the base object
+ for ( name in options ) {
+ src = target[ name ];
+ copy = options[ name ];
+
+ // Prevent never-ending loop
+ if ( target === copy ) {
+ continue;
+ }
+
+ // Recurse if we're merging plain objects or arrays
+ if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) {
+ if ( copyIsArray ) {
+ copyIsArray = false;
+ clone = src && jQuery.isArray(src) ? src : [];
+
+ } else {
+ clone = src && jQuery.isPlainObject(src) ? src : {};
+ }
+
+ // Never move original objects, clone them
+ target[ name ] = jQuery.extend( deep, clone, copy );
+
+ // Don't bring in undefined values
+ } else if ( copy !== undefined ) {
+ target[ name ] = copy;
+ }
+ }
+ }
+ }
+
+ // Return the modified object
+ return target;
+};
+jQuery.filter = function( expr, elems, not ) {
+
+ if ( not ) {
+ expr = ":not(" + expr + ")";
+ }
+
+ return elems.length === 1 ?
+ jQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] :
+ jQuery.find.matches(expr, elems);
+ };
+jQuery.find = function( query, context, extra, seed ) {
+
+ context = context || document;
+
+ // Only use querySelectorAll on non-XML documents
+ // (ID selectors don't work in non-HTML documents)
+ if ( !seed && !Sizzle.isXML(context) ) {
+ // See if we find a selector to speed up
+ var match = /^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec( query );
+
+ if ( match && (context.nodeType === 1 || context.nodeType === 9) ) {
+ // Speed-up: Sizzle("TAG")
+ if ( match[1] ) {
+ return makeArray( context.getElementsByTagName( query ), extra );
+
+ // Speed-up: Sizzle(".CLASS")
+ } else if ( match[2] && Expr.find.CLASS && context.getElementsByClassName ) {
+ return makeArray( context.getElementsByClassName( match[2] ), extra );
+ }
+ }
+
+ if ( context.nodeType === 9 ) {
+ // Speed-up: Sizzle("body")
+ // The body element only exists once, optimize finding it
+ if ( query === "body" && context.body ) {
+ return makeArray( [ context.body ], extra );
+
+ // Speed-up: Sizzle("#ID")
+ } else if ( match && match[3] ) {
+ var elem = context.getElementById( match[3] );
+
+ // Check parentNode to catch when Blackberry 4.6 returns
+ // nodes that are no longer in the document #6963
+ if ( elem && elem.parentNode ) {
+ // Handle the case where IE and Opera return items
+ // by name instead of ID
+ if ( elem.id === match[3] ) {
+ return makeArray( [ elem ], extra );
+ }
+
+ } else {
+ return makeArray( [], extra );
+ }
+ }
+
+ try {
+ return makeArray( context.querySelectorAll(query), extra );
+ } catch(qsaError) {}
+
+ // qSA works strangely on Element-rooted queries
+ // We can work around this by specifying an extra ID on the root
+ // and working up from there (Thanks to Andrew Dupont for the technique)
+ // IE 8 doesn't work on object elements
+ } else if ( context.nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) {
+ var oldContext = context,
+ old = context.getAttribute( "id" ),
+ nid = old || id,
+ hasParent = context.parentNode,
+ relativeHierarchySelector = /^\s*[+~]/.test( query );
+
+ if ( !old ) {
+ context.setAttribute( "id", nid );
+ } else {
+ nid = nid.replace( /'/g, "\\$&" );
+ }
+ if ( relativeHierarchySelector && hasParent ) {
+ context = context.parentNode;
+ }
+
+ try {
+ if ( !relativeHierarchySelector || hasParent ) {
+ return makeArray( context.querySelectorAll( "[id='" + nid + "'] " + query ), extra );
+ }
+
+ } catch(pseudoError) {
+ } finally {
+ if ( !old ) {
+ oldContext.removeAttribute( "id" );
+ }
+ }
+ }
+ }
+
+ return oldSizzle(query, context, extra, seed);
+ };
+jQuery.fn = { "selector": '',
+"jquery": '1.5.2',
+"length": 0 };
+jQuery.fragments = {};
+jQuery.fx = function( elem, options, prop ) {
+
+ this.options = options;
+ this.elem = elem;
+ this.prop = prop;
+
+ if ( !options.orig ) {
+ options.orig = {};
+ }
+ };
+jQuery.get = function( url, data, callback, type ) {
+/// <summary>
+/// Load data from the server using a HTTP GET request.
+/// </summary>
+/// <param name="url" type="String">
+/// A string containing the URL to which the request is sent.
+/// </param>
+/// <param name="data" type="String">
+/// A map or string that is sent to the server with the request.
+/// </param>
+/// <param name="callback" type="Function">
+/// A callback function that is executed if the request succeeds.
+/// </param>
+/// <param name="type" type="String">
+/// The type of data expected from the server. Default: Intelligent Guess (xml, json, script, or html).
+/// </param>
+
+ // shift arguments if data argument was omitted
+ if ( jQuery.isFunction( data ) ) {
+ type = type || callback;
+ callback = data;
+ data = undefined;
+ }
+
+ return jQuery.ajax({
+ type: method,
+ url: url,
+ data: data,
+ success: callback,
+ dataType: type
+ });
+ };
+jQuery.getJSON = function( url, data, callback ) {
+/// <summary>
+/// Load JSON-encoded data from the server using a GET HTTP request.
+/// </summary>
+/// <param name="url" type="String">
+/// A string containing the URL to which the request is sent.
+/// </param>
+/// <param name="data" type="Object">
+/// A map or string that is sent to the server with the request.
+/// </param>
+/// <param name="callback" type="Function">
+/// A callback function that is executed if the request succeeds.
+/// </param>
+
+ return jQuery.get( url, data, callback, "json" );
+ };
+jQuery.getScript = function( url, callback ) {
+/// <summary>
+/// Load a JavaScript file from the server using a GET HTTP request, then execute it.
+/// </summary>
+/// <param name="url" type="String">
+/// A string containing the URL to which the request is sent.
+/// </param>
+/// <param name="callback" type="Function">
+/// A callback function that is executed if the request succeeds.
+/// </param>
+/// <returns type="XMLHttpRequest" />
+
+ return jQuery.get( url, undefined, callback, "script" );
+ };
+jQuery.globalEval = function( data ) {
+/// <summary>
+/// Execute some JavaScript code globally.
+/// </summary>
+/// <param name="data" type="String">
+/// The JavaScript code to execute.
+/// </param>
+
+ if ( data && rnotwhite.test(data) ) {
+ // Inspired by code by Andrea Giammarchi
+ // http://webreflection.blogspot.com/2007/08/global-scope-evaluation-and-dom.html
+ var head = document.head || document.getElementsByTagName( "head" )[0] || document.documentElement,
+ script = document.createElement( "script" );
+
+ if ( jQuery.support.scriptEval() ) {
+ script.appendChild( document.createTextNode( data ) );
+ } else {
+ script.text = data;
+ }
+
+ // Use insertBefore instead of appendChild to circumvent an IE6 bug.
+ // This arises when a base node is used (#2709).
+ head.insertBefore( script, head.firstChild );
+ head.removeChild( script );
+ }
+ };
+jQuery.grep = function( elems, callback, inv ) {
+/// <summary>
+/// Finds the elements of an array which satisfy a filter function. The original array is not affected.
+/// </summary>
+/// <param name="elems" type="Array">
+/// The array to search through.
+/// </param>
+/// <param name="callback" type="Function">
+/// The function to process each item against. The first argument to the function is the item, and the second argument is the index. The function should return a Boolean value. this will be the global window object.
+/// </param>
+/// <param name="inv" type="Boolean">
+/// If "invert" is false, or not provided, then the function returns an array consisting of all elements for which "callback" returns true. If "invert" is true, then the function returns an array consisting of all elements for which "callback" returns false.
+/// </param>
+/// <returns type="Array" />
+
+ var ret = [], retVal;
+ inv = !!inv;
+
+ // Go through the array, only saving the items
+ // that pass the validator function
+ for ( var i = 0, length = elems.length; i < length; i++ ) {
+ retVal = !!callback( elems[ i ], i );
+ if ( inv !== retVal ) {
+ ret.push( elems[ i ] );
+ }
+ }
+
+ return ret;
+ };
+jQuery.guid = 1;
+jQuery.hasData = function( elem ) {
+/// <summary>
+/// Determine whether an element has any jQuery data associated with it.
+/// </summary>
+/// <param name="elem" domElement="true">
+/// A DOM element to be checked for data.
+/// </param>
+/// <returns type="Boolean" />
+
+ elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ];
+
+ return !!elem && !isEmptyDataObject( elem );
+ };
+jQuery.inArray = function( elem, array ) {
+/// <summary>
+/// Search for a specified value within an array and return its index (or -1 if not found).
+/// </summary>
+/// <param name="elem" type="Object">
+/// The value to search for.
+/// </param>
+/// <param name="array" type="Array">
+/// An array through which to search.
+/// </param>
+/// <returns type="Number" />
+
+ return indexOf.call( array, elem );
+ };
+jQuery.isEmptyObject = function( obj ) {
+/// <summary>
+/// Check to see if an object is empty (contains no properties).
+/// </summary>
+/// <param name="obj" type="Object">
+/// The object that will be checked to see if it's empty.
+/// </param>
+/// <returns type="Boolean" />
+
+ for ( var name in obj ) {
+ return false;
+ }
+ return true;
+ };
+jQuery.isFunction = function( obj ) {
+/// <summary>
+/// Determine if the argument passed is a Javascript function object.
+/// </summary>
+/// <param name="obj" type="Object">
+/// Object to test whether or not it is a function.
+/// </param>
+/// <returns type="boolean" />
+
+ return jQuery.type(obj) === "function";
+ };
+jQuery.isNaN = function( obj ) {
+
+ return obj == null || !rdigit.test( obj ) || isNaN( obj );
+ };
+jQuery.isPlainObject = function( obj ) {
+/// <summary>
+/// Check to see if an object is a plain object (created using "{}" or "new Object").
+/// </summary>
+/// <param name="obj" type="Object">
+/// The object that will be checked to see if it's a plain object.
+/// </param>
+/// <returns type="Boolean" />
+
+ // Must be an Object.
+ // Because of IE, we also have to check the presence of the constructor property.
+ // Make sure that DOM nodes and window objects don't pass through, as well
+ if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) {
+ return false;
+ }
+
+ // Not own constructor property must be Object
+ if ( obj.constructor &&
+ !hasOwn.call(obj, "constructor") &&
+ !hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) {
+ return false;
+ }
+
+ // Own properties are enumerated firstly, so to speed up,
+ // if last one is own, then all properties are own.
+
+ var key;
+ for ( key in obj ) {}
+
+ return key === undefined || hasOwn.call( obj, key );
+ };
+jQuery.isReady = true;
+jQuery.isWindow = function( obj ) {
+/// <summary>
+/// Determine whether the argument is a window.
+/// </summary>
+/// <param name="obj" type="Object">
+/// Object to test whether or not it is a window.
+/// </param>
+/// <returns type="boolean" />
+
+ return obj && typeof obj === "object" && "setInterval" in obj;
+ };
+jQuery.isXMLDoc = function( elem ) {
+/// <summary>
+/// Check to see if a DOM node is within an XML document (or is an XML document).
+/// </summary>
+/// <param name="elem" domElement="true">
+/// The DOM node that will be checked to see if it's in an XML document.
+/// </param>
+/// <returns type="Boolean" />
+
+ // documentElement is verified for cases where it doesn't yet exist
+ // (such as loading iframes in IE - #4833)
+ var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement;
+
+ return documentElement ? documentElement.nodeName !== "HTML" : false;
+};
+jQuery.lastModified = {};
+jQuery.makeArray = function( array, results ) {
+/// <summary>
+/// Convert an array-like object into a true JavaScript array.
+/// </summary>
+/// <param name="array" type="Object">
+/// Any object to turn into a native Array.
+/// </param>
+/// <returns type="Array" />
+
+ var ret = results || [];
+
+ if ( array != null ) {
+ // The window, strings (and functions) also have 'length'
+ // The extra typeof function check is to prevent crashes
+ // in Safari 2 (See: #3039)
+ // Tweaked logic slightly to handle Blackberry 4.7 RegExp issues #6930
+ var type = jQuery.type(array);
+
+ if ( array.length == null || type === "string" || type === "function" || type === "regexp" || jQuery.isWindow( array ) ) {
+ push.call( ret, array );
+ } else {
+ jQuery.merge( ret, array );
+ }
+ }
+
+ return ret;
+ };
+jQuery.map = function( elems, callback, arg ) {
+/// <summary>
+/// Translate all items in an array or array-like object to another array of items.
+/// </summary>
+/// <param name="elems" type="Array">
+/// The Array to translate.
+/// </param>
+/// <param name="callback" type="Function">
+/// The function to process each item against. The first argument to the function is the list item, the second argument is the index in array The function can return any value. this will be the global window object.
+/// </param>
+/// <returns type="Array" />
+
+ var ret = [], value;
+
+ // Go through the array, translating each of the items to their
+ // new value (or values).
+ for ( var i = 0, length = elems.length; i < length; i++ ) {
+ value = callback( elems[ i ], i, arg );
+
+ if ( value != null ) {
+ ret[ ret.length ] = value;
+ }
+ }
+
+ // Flatten any nested arrays
+ return ret.concat.apply( [], ret );
+ };
+jQuery.merge = function( first, second ) {
+/// <summary>
+/// Merge the contents of two arrays together into the first array.
+/// </summary>
+/// <param name="first" type="Array">
+/// The first array to merge, the elements of second added.
+/// </param>
+/// <param name="second" type="Array">
+/// The second array to merge into the first, unaltered.
+/// </param>
+/// <returns type="Array" />
+
+ var i = first.length,
+ j = 0;
+
+ if ( typeof second.length === "number" ) {
+ for ( var l = second.length; j < l; j++ ) {
+ first[ i++ ] = second[ j ];
+ }
+
+ } else {
+ while ( second[j] !== undefined ) {
+ first[ i++ ] = second[ j++ ];
+ }
+ }
+
+ first.length = i;
+
+ return first;
+ };
+jQuery.noConflict = function( deep ) {
+/// <summary>
+/// Relinquish jQuery's control of the $ variable.
+/// </summary>
+/// <param name="deep" type="Boolean">
+/// A Boolean indicating whether to remove all jQuery variables from the global scope (including jQuery itself).
+/// </param>
+/// <returns type="Object" />
+
+ window.$ = _$;
+
+ if ( deep ) {
+ window.jQuery = _jQuery;
+ }
+
+ return jQuery;
+ };
+jQuery.noData = { "embed": true,
+"object": 'clsid:D27CDB6E-AE6D-11cf-96B8-444553540000',
+"applet": true };
+jQuery.nodeName = function( elem, name ) {
+
+ return elem.nodeName && elem.nodeName.toUpperCase() === name.toUpperCase();
+ };
+jQuery.noop = function() {
+/// <summary>
+/// An empty function.
+/// </summary>
+/// <returns type="Function" />
+};
+jQuery.now = function() {
+/// <summary>
+/// Return a number representing the current time.
+/// </summary>
+/// <returns type="Number" />
+
+ return (new Date()).getTime();
+ };
+jQuery.nth = function( cur, result, dir, elem ) {
+
+ result = result || 1;
+ var num = 0;
+
+ for ( ; cur; cur = cur[dir] ) {
+ if ( cur.nodeType === 1 && ++num === result ) {
+ break;
+ }
+ }
+
+ return cur;
+ };
+jQuery.offset = {};
+jQuery.param = function( a, traditional ) {
+/// <summary>
+/// Create a serialized representation of an array or object, suitable for use in a URL query string or Ajax request.
+/// &#10;1 - jQuery.param(obj)
+/// &#10;2 - jQuery.param(obj, traditional)
+/// </summary>
+/// <param name="a" type="Object">
+/// An array or object to serialize.
+/// </param>
+/// <param name="traditional" type="Boolean">
+/// A Boolean indicating whether to perform a traditional "shallow" serialization.
+/// </param>
+/// <returns type="String" />
+
+ var s = [],
+ add = function( key, value ) {
+ // If value is a function, invoke it and return its value
+ value = jQuery.isFunction( value ) ? value() : value;
+ s[ s.length ] = encodeURIComponent( key ) + "=" + encodeURIComponent( value );
+ };
+
+ // Set traditional to true for jQuery <= 1.3.2 behavior.
+ if ( traditional === undefined ) {
+ traditional = jQuery.ajaxSettings.traditional;
+ }
+
+ // If an array was passed in, assume that it is an array of form elements.
+ if ( jQuery.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) {
+ // Serialize the form elements
+ jQuery.each( a, function() {
+ add( this.name, this.value );
+ } );
+
+ } else {
+ // If traditional, encode the "old" way (the way 1.3.2 or older
+ // did it), otherwise encode params recursively.
+ for ( var prefix in a ) {
+ buildParams( prefix, a[ prefix ], traditional, add );
+ }
+ }
+
+ // Return the resulting serialization
+ return s.join( "&" ).replace( r20, "+" );
+ };
+jQuery.parseJSON = function( data ) {
+/// <summary>
+/// Takes a well-formed JSON string and returns the resulting JavaScript object.
+/// </summary>
+/// <param name="data" type="String">
+/// The JSON string to parse.
+/// </param>
+/// <returns type="Object" />
+
+ if ( typeof data !== "string" || !data ) {
+ return null;
+ }
+
+ // Make sure leading/trailing whitespace is removed (IE can't handle it)
+ data = jQuery.trim( data );
+
+ // Make sure the incoming data is actual JSON
+ // Logic borrowed from http://json.org/json2.js
+ if ( rvalidchars.test(data.replace(rvalidescape, "@")
+ .replace(rvalidtokens, "]")
+ .replace(rvalidbraces, "")) ) {
+
+ // Try to use the native JSON parser first
+ return window.JSON && window.JSON.parse ?
+ window.JSON.parse( data ) :
+ (new Function("return " + data))();
+
+ } else {
+ jQuery.error( "Invalid JSON: " + data );
+ }
+ };
+jQuery.parseXML = function( data , xml , tmp ) {
+/// <summary>
+/// Parses a string into an XML document.
+/// </summary>
+/// <param name="data" type="String">
+/// a well-formed XML string to be parsed
+/// </param>
+/// <returns type="XMLDocument" />
+
+
+ if ( window.DOMParser ) { // Standard
+ tmp = new DOMParser();
+ xml = tmp.parseFromString( data , "text/xml" );
+ } else { // IE
+ xml = new ActiveXObject( "Microsoft.XMLDOM" );
+ xml.async = "false";
+ xml.loadXML( data );
+ }
+
+ tmp = xml.documentElement;
+
+ if ( ! tmp || ! tmp.nodeName || tmp.nodeName === "parsererror" ) {
+ jQuery.error( "Invalid XML: " + data );
+ }
+
+ return xml;
+ };
+jQuery.post = function( url, data, callback, type ) {
+/// <summary>
+/// Load data from the server using a HTTP POST request.
+/// </summary>
+/// <param name="url" type="String">
+/// A string containing the URL to which the request is sent.
+/// </param>
+/// <param name="data" type="String">
+/// A map or string that is sent to the server with the request.
+/// </param>
+/// <param name="callback" type="Function">
+/// A callback function that is executed if the request succeeds.
+/// </param>
+/// <param name="type" type="String">
+/// The type of data expected from the server. Default: Intelligent Guess (xml, json, script, or html).
+/// </param>
+
+ // shift arguments if data argument was omitted
+ if ( jQuery.isFunction( data ) ) {
+ type = type || callback;
+ callback = data;
+ data = undefined;
+ }
+
+ return jQuery.ajax({
+ type: method,
+ url: url,
+ data: data,
+ success: callback,
+ dataType: type
+ });
+ };
+jQuery.props = { "for": 'htmlFor',
+"class": 'className',
+"readonly": 'readOnly',
+"maxlength": 'maxLength',
+"cellspacing": 'cellSpacing',
+"rowspan": 'rowSpan',
+"colspan": 'colSpan',
+"tabindex": 'tabIndex',
+"usemap": 'useMap',
+"frameborder": 'frameBorder' };
+jQuery.proxy = function( fn, proxy, thisObject ) {
+/// <summary>
+/// Takes a function and returns a new one that will always have a particular context.
+/// &#10;1 - jQuery.proxy(function, context)
+/// &#10;2 - jQuery.proxy(context, name)
+/// </summary>
+/// <param name="fn" type="Function">
+/// The function whose context will be changed.
+/// </param>
+/// <param name="proxy" type="Object">
+/// The object to which the context (this) of the function should be set.
+/// </param>
+/// <returns type="Function" />
+
+ if ( arguments.length === 2 ) {
+ if ( typeof proxy === "string" ) {
+ thisObject = fn;
+ fn = thisObject[ proxy ];
+ proxy = undefined;
+
+ } else if ( proxy && !jQuery.isFunction( proxy ) ) {
+ thisObject = proxy;
+ proxy = undefined;
+ }
+ }
+
+ if ( !proxy && fn ) {
+ proxy = function() {
+ return fn.apply( thisObject || this, arguments );
+ };
+ }
+
+ // Set the guid of unique handler to the same of original handler, so it can be removed
+ if ( fn ) {
+ proxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++;
+ }
+
+ // So proxy can be declared as an argument
+ return proxy;
+ };
+jQuery.queue = function( elem, type, data ) {
+/// <summary>
+/// 1: Show the queue of functions to be executed on the matched element.
+/// &#10; 1.1 - jQuery.queue(element, queueName)
+/// &#10;2: Manipulate the queue of functions to be executed on the matched element.
+/// &#10; 2.1 - jQuery.queue(element, queueName, newQueue)
+/// &#10; 2.2 - jQuery.queue(element, queueName, callback())
+/// </summary>
+/// <param name="elem" domElement="true">
+/// A DOM element where the array of queued functions is attached.
+/// </param>
+/// <param name="type" type="String">
+/// A string containing the name of the queue. Defaults to fx, the standard effects queue.
+/// </param>
+/// <param name="data" type="Array">
+/// An array of functions to replace the current queue contents.
+/// </param>
+/// <returns type="jQuery" />
+
+ if ( !elem ) {
+ return;
+ }
+
+ type = (type || "fx") + "queue";
+ var q = jQuery._data( elem, type );
+
+ // Speed up dequeue by getting out quickly if this is just a lookup
+ if ( !data ) {
+ return q || [];
+ }
+
+ if ( !q || jQuery.isArray(data) ) {
+ q = jQuery._data( elem, type, jQuery.makeArray(data) );
+
+ } else {
+ q.push( data );
+ }
+
+ return q;
+ };
+jQuery.ready = function( wait ) {
+
+ // A third-party is pushing the ready event forwards
+ if ( wait === true ) {
+ jQuery.readyWait--;
+ }
+
+ // Make sure that the DOM is not already loaded
+ if ( !jQuery.readyWait || (wait !== true && !jQuery.isReady) ) {
+ // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
+ if ( !document.body ) {
+ return setTimeout( jQuery.ready, 1 );
+ }
+
+ // Remember that the DOM is ready
+ jQuery.isReady = true;
+
+ // If a normal DOM Ready event fired, decrement, and wait if need be
+ if ( wait !== true && --jQuery.readyWait > 0 ) {
+ return;
+ }
+
+ // If there are functions bound, to execute
+ readyList.resolveWith( document, [ jQuery ] );
+
+ // Trigger any bound ready events
+ if ( jQuery.fn.trigger ) {
+ jQuery( document ).trigger( "ready" ).unbind( "ready" );
+ }
+ }
+ };
+jQuery.readyWait = 0;
+jQuery.removeData = function( elem, name, pvt /* Internal Use Only */ ) {
+/// <summary>
+/// Remove a previously-stored piece of data.
+/// </summary>
+/// <param name="elem" domElement="true">
+/// A DOM element from which to remove data.
+/// </param>
+/// <param name="name" type="String">
+/// A string naming the piece of data to remove.
+/// </param>
+/// <returns type="jQuery" />
+
+ if ( !jQuery.acceptData( elem ) ) {
+ return;
+ }
+
+ var internalKey = jQuery.expando, isNode = elem.nodeType,
+
+ // See jQuery.data for more information
+ cache = isNode ? jQuery.cache : elem,
+
+ // See jQuery.data for more information
+ id = isNode ? elem[ jQuery.expando ] : jQuery.expando;
+
+ // If there is already no cache entry for this object, there is no
+ // purpose in continuing
+ if ( !cache[ id ] ) {
+ return;
+ }
+
+ if ( name ) {
+ var thisCache = pvt ? cache[ id ][ internalKey ] : cache[ id ];
+
+ if ( thisCache ) {
+ delete thisCache[ name ];
+
+ // If there is no data left in the cache, we want to continue
+ // and let the cache object itself get destroyed
+ if ( !isEmptyDataObject(thisCache) ) {
+ return;
+ }
+ }
+ }
+
+ // See jQuery.data for more information
+ if ( pvt ) {
+ delete cache[ id ][ internalKey ];
+
+ // Don't destroy the parent cache unless the internal data object
+ // had been the only thing left in it
+ if ( !isEmptyDataObject(cache[ id ]) ) {
+ return;
+ }
+ }
+
+ var internalCache = cache[ id ][ internalKey ];
+
+ // Browsers that fail expando deletion also refuse to delete expandos on
+ // the window, but it will allow it on all other JS objects; other browsers
+ // don't care
+ if ( jQuery.support.deleteExpando || cache != window ) {
+ delete cache[ id ];
+ } else {
+ cache[ id ] = null;
+ }
+
+ // We destroyed the entire user cache at once because it's faster than
+ // iterating through each key, but we need to continue to persist internal
+ // data if it existed
+ if ( internalCache ) {
+ cache[ id ] = {};
+ // TODO: This is a hack for 1.5 ONLY. Avoids exposing jQuery
+ // metadata on plain JS objects when the object is serialized using
+ // JSON.stringify
+ if ( !isNode ) {
+ cache[ id ].toJSON = jQuery.noop;
+ }
+
+ cache[ id ][ internalKey ] = internalCache;
+
+ // Otherwise, we need to eliminate the expando on the node to avoid
+ // false lookups in the cache for entries that no longer exist
+ } else if ( isNode ) {
+ // IE does not allow us to delete expando properties from nodes,
+ // nor does it have a removeAttribute function on Document nodes;
+ // we must handle all of these cases
+ if ( jQuery.support.deleteExpando ) {
+ delete elem[ jQuery.expando ];
+ } else if ( elem.removeAttribute ) {
+ elem.removeAttribute( jQuery.expando );
+ } else {
+ elem[ jQuery.expando ] = null;
+ }
+ }
+ };
+jQuery.removeEvent = function( elem, type, handle ) {
+
+ if ( elem.removeEventListener ) {
+ elem.removeEventListener( type, handle, false );
+ }
+ };
+jQuery.sibling = function( n, elem ) {
+
+ var r = [];
+
+ for ( ; n; n = n.nextSibling ) {
+ if ( n.nodeType === 1 && n !== elem ) {
+ r.push( n );
+ }
+ }
+
+ return r;
+ };
+jQuery.speed = function( speed, easing, fn ) {
+
+ var opt = speed && typeof speed === "object" ? jQuery.extend({}, speed) : {
+ complete: fn || !fn && easing ||
+ jQuery.isFunction( speed ) && speed,
+ duration: speed,
+ easing: fn && easing || easing && !jQuery.isFunction(easing) && easing
+ };
+
+ opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration :
+ opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[opt.duration] : jQuery.fx.speeds._default;
+
+ // Queueing
+ opt.old = opt.complete;
+ opt.complete = function() {
+ if ( opt.queue !== false ) {
+ jQuery(this).dequeue();
+ }
+ if ( jQuery.isFunction( opt.old ) ) {
+ opt.old.call( this );
+ }
+ };
+
+ return opt;
+ };
+jQuery.style = function( elem, name, value, extra ) {
+
+ // Don't set styles on text and comment nodes
+ if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) {
+ return;
+ }
+
+ // Make sure that we're working with the right name
+ var ret, origName = jQuery.camelCase( name ),
+ style = elem.style, hooks = jQuery.cssHooks[ origName ];
+
+ name = jQuery.cssProps[ origName ] || origName;
+
+ // Check if we're setting a value
+ if ( value !== undefined ) {
+ // Make sure that NaN and null values aren't set. See: #7116
+ if ( typeof value === "number" && isNaN( value ) || value == null ) {
+ return;
+ }
+
+ // If a number was passed in, add 'px' to the (except for certain CSS properties)
+ if ( typeof value === "number" && !jQuery.cssNumber[ origName ] ) {
+ value += "px";
+ }
+
+ // If a hook was provided, use that value, otherwise just set the specified value
+ if ( !hooks || !("set" in hooks) || (value = hooks.set( elem, value )) !== undefined ) {
+ // Wrapped to prevent IE from throwing errors when 'invalid' values are provided
+ // Fixes bug #5509
+ try {
+ style[ name ] = value;
+ } catch(e) {}
+ }
+
+ } else {
+ // If a hook was provided get the non-computed value from there
+ if ( hooks && "get" in hooks && (ret = hooks.get( elem, false, extra )) !== undefined ) {
+ return ret;
+ }
+
+ // Otherwise just get the value from the style object
+ return style[ name ];
+ }
+ };
+jQuery.sub = function() {
+/// <summary>
+/// Creates a new copy of jQuery whose properties and methods can be modified without affecting the original jQuery object.
+/// </summary>
+/// <returns type="jQuery" />
+
+ function jQuerySubclass( selector, context ) {
+ return new jQuerySubclass.fn.init( selector, context );
+ }
+ jQuery.extend( true, jQuerySubclass, this );
+ jQuerySubclass.superclass = this;
+ jQuerySubclass.fn = jQuerySubclass.prototype = this();
+ jQuerySubclass.fn.constructor = jQuerySubclass;
+ jQuerySubclass.subclass = this.subclass;
+ jQuerySubclass.fn.init = function init( selector, context ) {
+ if ( context && context instanceof jQuery && !(context instanceof jQuerySubclass) ) {
+ context = jQuerySubclass(context);
+ }
+
+ return jQuery.fn.init.call( this, selector, context, rootjQuerySubclass );
+ };
+ jQuerySubclass.fn.init.prototype = jQuerySubclass.fn;
+ var rootjQuerySubclass = jQuerySubclass(document);
+ return jQuerySubclass;
+ };
+jQuery.support = { "leadingWhitespace": true,
+"tbody": true,
+"htmlSerialize": true,
+"style": true,
+"hrefNormalized": true,
+"opacity": true,
+"cssFloat": true,
+"checkOn": true,
+"optSelected": false,
+"deleteExpando": true,
+"optDisabled": true,
+"checkClone": true,
+"noCloneEvent": true,
+"noCloneChecked": false,
+"boxModel": true,
+"inlineBlockNeedsLayout": false,
+"shrinkWrapBlocks": false,
+"reliableHiddenOffsets": true,
+"reliableMarginRight": true,
+"submitBubbles": true,
+"changeBubbles": true,
+"ajax": true,
+"cors": false };
+jQuery.swap = function( elem, options, callback ) {
+
+ var old = {};
+
+ // Remember the old values, and insert the new ones
+ for ( var name in options ) {
+ old[ name ] = elem.style[ name ];
+ elem.style[ name ] = options[ name ];
+ }
+
+ callback.call( elem );
+
+ // Revert the old values
+ for ( name in options ) {
+ elem.style[ name ] = old[ name ];
+ }
+ };
+jQuery.text = function( elems ) {
+
+ var ret = "", elem;
+
+ for ( var i = 0; elems[i]; i++ ) {
+ elem = elems[i];
+
+ // Get the text from text nodes and CDATA nodes
+ if ( elem.nodeType === 3 || elem.nodeType === 4 ) {
+ ret += elem.nodeValue;
+
+ // Traverse everything else, except comment nodes
+ } else if ( elem.nodeType !== 8 ) {
+ ret += Sizzle.getText( elem.childNodes );
+ }
+ }
+
+ return ret;
+};
+jQuery.trim = function( text ) {
+/// <summary>
+/// Remove the whitespace from the beginning and end of a string.
+/// </summary>
+/// <param name="text" type="String">
+/// The string to trim.
+/// </param>
+/// <returns type="String" />
+
+ return text == null ?
+ "" :
+ trim.call( text );
+ };
+jQuery.type = function( obj ) {
+/// <summary>
+/// Determine the internal JavaScript [[Class]] of an object.
+/// </summary>
+/// <param name="obj" type="Object">
+/// Object to get the internal JavaScript [[Class]] of.
+/// </param>
+/// <returns type="String" />
+
+ return obj == null ?
+ String( obj ) :
+ class2type[ toString.call(obj) ] || "object";
+ };
+jQuery.uaMatch = function( ua ) {
+
+ ua = ua.toLowerCase();
+
+ var match = rwebkit.exec( ua ) ||
+ ropera.exec( ua ) ||
+ rmsie.exec( ua ) ||
+ ua.indexOf("compatible") < 0 && rmozilla.exec( ua ) ||
+ [];
+
+ return { browser: match[1] || "", version: match[2] || "0" };
+ };
+jQuery.unique = function( results ) {
+/// <summary>
+/// Sorts an array of DOM elements, in place, with the duplicates removed. Note that this only works on arrays of DOM elements, not strings or numbers.
+/// </summary>
+/// <param name="results" type="Array">
+/// The Array of DOM elements.
+/// </param>
+/// <returns type="Array" />
+
+ if ( sortOrder ) {
+ hasDuplicate = baseHasDuplicate;
+ results.sort( sortOrder );
+
+ if ( hasDuplicate ) {
+ for ( var i = 1; i < results.length; i++ ) {
+ if ( results[i] === results[ i - 1 ] ) {
+ results.splice( i--, 1 );
+ }
+ }
+ }
+ }
+
+ return results;
+};
+jQuery.uuid = 0;
+jQuery.when = function( firstParam ) {
+/// <summary>
+/// Provides a way to execute callback functions based on one or more objects, usually Deferred objects that represent asynchronous events.
+/// </summary>
+/// <param name="firstParam" type="Deferred">
+/// One or more Deferred objects, or plain JavaScript objects.
+/// </param>
+/// <returns type="Promise" />
+
+ var args = arguments,
+ i = 0,
+ length = args.length,
+ count = length,
+ deferred = length <= 1 && firstParam && jQuery.isFunction( firstParam.promise ) ?
+ firstParam :
+ jQuery.Deferred();
+ function resolveFunc( i ) {
+ return function( value ) {
+ args[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value;
+ if ( !( --count ) ) {
+ // Strange bug in FF4:
+ // Values changed onto the arguments object sometimes end up as undefined values
+ // outside the $.when method. Cloning the object into a fresh array solves the issue
+ deferred.resolveWith( deferred, sliceDeferred.call( args, 0 ) );
+ }
+ };
+ }
+ if ( length > 1 ) {
+ for( ; i < length; i++ ) {
+ if ( args[ i ] && jQuery.isFunction( args[ i ].promise ) ) {
+ args[ i ].promise().then( resolveFunc(i), deferred.reject );
+ } else {
+ --count;
+ }
+ }
+ if ( !count ) {
+ deferred.resolveWith( deferred, args );
+ }
+ } else if ( deferred !== firstParam ) {
+ deferred.resolveWith( deferred, length ? [ firstParam ] : [] );
+ }
+ return deferred.promise();
+ };
+jQuery.Event.prototype.isDefaultPrevented = function returnFalse() {
+/// <summary>
+/// Returns whether event.preventDefault() was ever called on this event object.
+/// </summary>
+/// <returns type="Boolean" />
+
+ return false;
+};
+jQuery.Event.prototype.isImmediatePropagationStopped = function returnFalse() {
+/// <summary>
+/// Returns whether event.stopImmediatePropagation() was ever called on this event object.
+/// </summary>
+/// <returns type="Boolean" />
+
+ return false;
+};
+jQuery.Event.prototype.isPropagationStopped = function returnFalse() {
+/// <summary>
+/// Returns whether event.stopPropagation() was ever called on this event object.
+/// </summary>
+/// <returns type="Boolean" />
+
+ return false;
+};
+jQuery.Event.prototype.preventDefault = function() {
+/// <summary>
+/// If this method is called, the default action of the event will not be triggered.
+/// </summary>
+/// <returns type="undefined" />
+
+ this.isDefaultPrevented = returnTrue;
+
+ var e = this.originalEvent;
+ if ( !e ) {
+ return;
+ }
+
+ // if preventDefault exists run it on the original event
+ if ( e.preventDefault ) {
+ e.preventDefault();
+
+ // otherwise set the returnValue property of the original event to false (IE)
+ } else {
+ e.returnValue = false;
+ }
+ };
+jQuery.Event.prototype.stopImmediatePropagation = function() {
+/// <summary>
+/// Keeps the rest of the handlers from being executed and prevents the event from bubbling up the DOM tree.
+/// </summary>
+
+ this.isImmediatePropagationStopped = returnTrue;
+ this.stopPropagation();
+ };
+jQuery.Event.prototype.stopPropagation = function() {
+/// <summary>
+/// Prevents the event from bubbling up the DOM tree, preventing any parent handlers from being notified of the event.
+/// </summary>
+
+ this.isPropagationStopped = returnTrue;
+
+ var e = this.originalEvent;
+ if ( !e ) {
+ return;
+ }
+ // if stopPropagation exists run it on the original event
+ if ( e.stopPropagation ) {
+ e.stopPropagation();
+ }
+ // otherwise set the cancelBubble property of the original event to true (IE)
+ e.cancelBubble = true;
+ };
+jQuery.prototype._toggle = function( fn ) {
+
+ // Save reference to arguments for access in closure
+ var args = arguments,
+ i = 1;
+
+ // link all the functions, so any of them can unbind this click handler
+ while ( i < args.length ) {
+ jQuery.proxy( fn, args[ i++ ] );
+ }
+
+ return this.click( jQuery.proxy( fn, function( event ) {
+ // Figure out which function to execute
+ var lastToggle = ( jQuery._data( this, "lastToggle" + fn.guid ) || 0 ) % i;
+ jQuery._data( this, "lastToggle" + fn.guid, lastToggle + 1 );
+
+ // Make sure that clicks stop
+ event.preventDefault();
+
+ // and execute the function
+ return args[ lastToggle ].apply( this, arguments ) || false;
+ }));
+ };
+jQuery.prototype.add = function( selector, context ) {
+/// <summary>
+/// Add elements to the set of matched elements.
+/// &#10;1 - add(selector)
+/// &#10;2 - add(elements)
+/// &#10;3 - add(html)
+/// &#10;4 - add(selector, context)
+/// </summary>
+/// <param name="selector" type="String">
+/// A string representing a selector expression to find additional elements to add to the set of matched elements.
+/// </param>
+/// <param name="context" domElement="true">
+/// The point in the document at which the selector should begin matching; similar to the context argument of the $(selector, context) method.
+/// </param>
+/// <returns type="jQuery" />
+
+ var set = typeof selector === "string" ?
+ jQuery( selector, context ) :
+ jQuery.makeArray( selector ),
+ all = jQuery.merge( this.get(), set );
+
+ return this.pushStack( isDisconnected( set[0] ) || isDisconnected( all[0] ) ?
+ all :
+ jQuery.unique( all ) );
+ };
+jQuery.prototype.addClass = function( value ) {
+/// <summary>
+/// Adds the specified class(es) to each of the set of matched elements.
+/// &#10;1 - addClass(className)
+/// &#10;2 - addClass(function(index, currentClass))
+/// </summary>
+/// <param name="value" type="String">
+/// One or more class names to be added to the class attribute of each matched element.
+/// </param>
+/// <returns type="jQuery" />
+
+ if ( jQuery.isFunction(value) ) {
+ return this.each(function(i) {
+ var self = jQuery(this);
+ self.addClass( value.call(this, i, self.attr("class")) );
+ });
+ }
+
+ if ( value && typeof value === "string" ) {
+ var classNames = (value || "").split( rspaces );
+
+ for ( var i = 0, l = this.length; i < l; i++ ) {
+ var elem = this[i];
+
+ if ( elem.nodeType === 1 ) {
+ if ( !elem.className ) {
+ elem.className = value;
+
+ } else {
+ var className = " " + elem.className + " ",
+ setClass = elem.className;
+
+ for ( var c = 0, cl = classNames.length; c < cl; c++ ) {
+ if ( className.indexOf( " " + classNames[c] + " " ) < 0 ) {
+ setClass += " " + classNames[c];
+ }
+ }
+ elem.className = jQuery.trim( setClass );
+ }
+ }
+ }
+ }
+
+ return this;
+ };
+jQuery.prototype.after = function() {
+/// <summary>
+/// Insert content, specified by the parameter, after each element in the set of matched elements.
+/// &#10;1 - after(content, content)
+/// &#10;2 - after(function(index))
+/// </summary>
+/// <param name="" type="jQuery">
+/// HTML string, DOM element, or jQuery object to insert after each element in the set of matched elements.
+/// </param>
+/// <param name="" type="jQuery">
+/// One or more additional DOM elements, arrays of elements, HTML strings, or jQuery objects to insert after each element in the set of matched elements.
+/// </param>
+/// <returns type="jQuery" />
+
+ if ( this[0] && this[0].parentNode ) {
+ return this.domManip(arguments, false, function( elem ) {
+ this.parentNode.insertBefore( elem, this.nextSibling );
+ });
+ } else if ( arguments.length ) {
+ var set = this.pushStack( this, "after", arguments );
+ set.push.apply( set, jQuery(arguments[0]).toArray() );
+ return set;
+ }
+ };
+jQuery.prototype.ajaxComplete = function( f ){
+/// <summary>
+/// Register a handler to be called when Ajax requests complete. This is an Ajax Event.
+/// </summary>
+/// <param name="f" type="Function">
+/// The function to be invoked.
+/// </param>
+/// <returns type="jQuery" />
+
+ return this.bind( o, f );
+ };
+jQuery.prototype.ajaxError = function( f ){
+/// <summary>
+/// Register a handler to be called when Ajax requests complete with an error. This is an Ajax Event.
+/// </summary>
+/// <param name="f" type="Function">
+/// The function to be invoked.
+/// </param>
+/// <returns type="jQuery" />
+
+ return this.bind( o, f );
+ };
+jQuery.prototype.ajaxSend = function( f ){
+/// <summary>
+/// Attach a function to be executed before an Ajax request is sent. This is an Ajax Event.
+/// </summary>
+/// <param name="f" type="Function">
+/// The function to be invoked.
+/// </param>
+/// <returns type="jQuery" />
+
+ return this.bind( o, f );
+ };
+jQuery.prototype.ajaxStart = function( f ){
+/// <summary>
+/// Register a handler to be called when the first Ajax request begins. This is an Ajax Event.
+/// </summary>
+/// <param name="f" type="Function">
+/// The function to be invoked.
+/// </param>
+/// <returns type="jQuery" />
+
+ return this.bind( o, f );
+ };
+jQuery.prototype.ajaxStop = function( f ){
+/// <summary>
+/// Register a handler to be called when all Ajax requests have completed. This is an Ajax Event.
+/// </summary>
+/// <param name="f" type="Function">
+/// The function to be invoked.
+/// </param>
+/// <returns type="jQuery" />
+
+ return this.bind( o, f );
+ };
+jQuery.prototype.ajaxSuccess = function( f ){
+/// <summary>
+/// Attach a function to be executed whenever an Ajax request completes successfully. This is an Ajax Event.
+/// </summary>
+/// <param name="f" type="Function">
+/// The function to be invoked.
+/// </param>
+/// <returns type="jQuery" />
+
+ return this.bind( o, f );
+ };
+jQuery.prototype.andSelf = function() {
+/// <summary>
+/// Add the previous set of elements on the stack to the current set.
+/// </summary>
+/// <returns type="jQuery" />
+
+ return this.add( this.prevObject );
+ };
+jQuery.prototype.animate = function( prop, speed, easing, callback ) {
+/// <summary>
+/// Perform a custom animation of a set of CSS properties.
+/// &#10;1 - animate(properties, duration, easing, complete)
+/// &#10;2 - animate(properties, options)
+/// </summary>
+/// <param name="prop" type="Object">
+/// A map of CSS properties that the animation will move toward.
+/// </param>
+/// <param name="speed" type="Number">
+/// A string or number determining how long the animation will run.
+/// </param>
+/// <param name="easing" type="String">
+/// A string indicating which easing function to use for the transition.
+/// </param>
+/// <param name="callback" type="Function">
+/// A function to call once the animation is complete.
+/// </param>
+/// <returns type="jQuery" />
+
+ var optall = jQuery.speed(speed, easing, callback);
+
+ if ( jQuery.isEmptyObject( prop ) ) {
+ return this.each( optall.complete );
+ }
+
+ return this[ optall.queue === false ? "each" : "queue" ](function() {
+ // XXX 'this' does not always have a nodeName when running the
+ // test suite
+
+ var opt = jQuery.extend({}, optall), p,
+ isElement = this.nodeType === 1,
+ hidden = isElement && jQuery(this).is(":hidden"),
+ self = this;
+
+ for ( p in prop ) {
+ var name = jQuery.camelCase( p );
+
+ if ( p !== name ) {
+ prop[ name ] = prop[ p ];
+ delete prop[ p ];
+ p = name;
+ }
+
+ if ( prop[p] === "hide" && hidden || prop[p] === "show" && !hidden ) {
+ return opt.complete.call(this);
+ }
+
+ if ( isElement && ( p === "height" || p === "width" ) ) {
+ // Make sure that nothing sneaks out
+ // Record all 3 overflow attributes because IE does not
+ // change the overflow attribute when overflowX and
+ // overflowY are set to the same value
+ opt.overflow = [ this.style.overflow, this.style.overflowX, this.style.overflowY ];
+
+ // Set display property to inline-block for height/width
+ // animations on inline elements that are having width/height
+ // animated
+ if ( jQuery.css( this, "display" ) === "inline" &&
+ jQuery.css( this, "float" ) === "none" ) {
+ if ( !jQuery.support.inlineBlockNeedsLayout ) {
+ this.style.display = "inline-block";
+
+ } else {
+ var display = defaultDisplay(this.nodeName);
+
+ // inline-level elements accept inline-block;
+ // block-level elements need to be inline with layout
+ if ( display === "inline" ) {
+ this.style.display = "inline-block";
+
+ } else {
+ this.style.display = "inline";
+ this.style.zoom = 1;
+ }
+ }
+ }
+ }
+
+ if ( jQuery.isArray( prop[p] ) ) {
+ // Create (if needed) and add to specialEasing
+ (opt.specialEasing = opt.specialEasing || {})[p] = prop[p][1];
+ prop[p] = prop[p][0];
+ }
+ }
+
+ if ( opt.overflow != null ) {
+ this.style.overflow = "hidden";
+ }
+
+ opt.curAnim = jQuery.extend({}, prop);
+
+ jQuery.each( prop, function( name, val ) {
+ var e = new jQuery.fx( self, opt, name );
+
+ if ( rfxtypes.test(val) ) {
+ e[ val === "toggle" ? hidden ? "show" : "hide" : val ]( prop );
+
+ } else {
+ var parts = rfxnum.exec(val),
+ start = e.cur();
+
+ if ( parts ) {
+ var end = parseFloat( parts[2] ),
+ unit = parts[3] || ( jQuery.cssNumber[ name ] ? "" : "px" );
+
+ // We need to compute starting value
+ if ( unit !== "px" ) {
+ jQuery.style( self, name, (end || 1) + unit);
+ start = ((end || 1) / e.cur()) * start;
+ jQuery.style( self, name, start + unit);
+ }
+
+ // If a +=/-= token was provided, we're doing a relative animation
+ if ( parts[1] ) {
+ end = ((parts[1] === "-=" ? -1 : 1) * end) + start;
+ }
+
+ e.custom( start, end, unit );
+
+ } else {
+ e.custom( start, val, "" );
+ }
+ }
+ });
+
+ // For JS strict compliance
+ return true;
+ });
+ };
+jQuery.prototype.append = function() {
+/// <summary>
+/// Insert content, specified by the parameter, to the end of each element in the set of matched elements.
+/// &#10;1 - append(content, content)
+/// &#10;2 - append(function(index, html))
+/// </summary>
+/// <param name="" type="jQuery">
+/// DOM element, HTML string, or jQuery object to insert at the end of each element in the set of matched elements.
+/// </param>
+/// <param name="" type="jQuery">
+/// One or more additional DOM elements, arrays of elements, HTML strings, or jQuery objects to insert at the end of each element in the set of matched elements.
+/// </param>
+/// <returns type="jQuery" />
+
+ return this.domManip(arguments, true, function( elem ) {
+ if ( this.nodeType === 1 ) {
+ this.appendChild( elem );
+ }
+ });
+ };
+jQuery.prototype.appendTo = function( selector ) {
+/// <summary>
+/// Insert every element in the set of matched elements to the end of the target.
+/// </summary>
+/// <param name="selector" type="jQuery">
+/// A selector, element, HTML string, or jQuery object; the matched set of elements will be inserted at the end of the element(s) specified by this parameter.
+/// </param>
+/// <returns type="jQuery" />
+
+ var ret = [],
+ insert = jQuery( selector ),
+ parent = this.length === 1 && this[0].parentNode;
+
+ if ( parent && parent.nodeType === 11 && parent.childNodes.length === 1 && insert.length === 1 ) {
+ insert[ original ]( this[0] );
+ return this;
+
+ } else {
+ for ( var i = 0, l = insert.length; i < l; i++ ) {
+ var elems = (i > 0 ? this.clone(true) : this).get();
+ jQuery( insert[i] )[ original ]( elems );
+ ret = ret.concat( elems );
+ }
+
+ return this.pushStack( ret, name, insert.selector );
+ }
+ };
+jQuery.prototype.attr = function( name, value ) {
+/// <summary>
+/// 1: Get the value of an attribute for the first element in the set of matched elements.
+/// &#10; 1.1 - attr(attributeName)
+/// &#10;2: Set one or more attributes for the set of matched elements.
+/// &#10; 2.1 - attr(attributeName, value)
+/// &#10; 2.2 - attr(map)
+/// &#10; 2.3 - attr(attributeName, function(index, attr))
+/// </summary>
+/// <param name="name" type="String">
+/// The name of the attribute to set.
+/// </param>
+/// <param name="value" type="Number">
+/// A value to set for the attribute.
+/// </param>
+/// <returns type="jQuery" />
+
+ return jQuery.access( this, name, value, true, jQuery.attr );
+ };
+jQuery.prototype.before = function() {
+/// <summary>
+/// Insert content, specified by the parameter, before each element in the set of matched elements.
+/// &#10;1 - before(content, content)
+/// &#10;2 - before(function)
+/// </summary>
+/// <param name="" type="jQuery">
+/// HTML string, DOM element, or jQuery object to insert before each element in the set of matched elements.
+/// </param>
+/// <param name="" type="jQuery">
+/// One or more additional DOM elements, arrays of elements, HTML strings, or jQuery objects to insert before each element in the set of matched elements.
+/// </param>
+/// <returns type="jQuery" />
+
+ if ( this[0] && this[0].parentNode ) {
+ return this.domManip(arguments, false, function( elem ) {
+ this.parentNode.insertBefore( elem, this );
+ });
+ } else if ( arguments.length ) {
+ var set = jQuery(arguments[0]);
+ set.push.apply( set, this.toArray() );
+ return this.pushStack( set, "before", arguments );
+ }
+ };
+jQuery.prototype.bind = function( type, data, fn ) {
+/// <summary>
+/// Attach a handler to an event for the elements.
+/// &#10;1 - bind(eventType, eventData, handler(eventObject))
+/// &#10;2 - bind(eventType, eventData, false)
+/// &#10;3 - bind(events)
+/// </summary>
+/// <param name="type" type="String">
+/// A string containing one or more JavaScript event types, such as "click" or "submit," or custom event names.
+/// </param>
+/// <param name="data" type="Object">
+/// A map of data that will be passed to the event handler.
+/// </param>
+/// <param name="fn" type="Function">
+/// A function to execute each time the event is triggered.
+/// </param>
+/// <returns type="jQuery" />
+
+ // Handle object literals
+ if ( typeof type === "object" ) {
+ for ( var key in type ) {
+ this[ name ](key, data, type[key], fn);
+ }
+ return this;
+ }
+
+ if ( jQuery.isFunction( data ) || data === false ) {
+ fn = data;
+ data = undefined;
+ }
+
+ var handler = name === "one" ? jQuery.proxy( fn, function( event ) {
+ jQuery( this ).unbind( event, handler );
+ return fn.apply( this, arguments );
+ }) : fn;
+
+ if ( type === "unload" && name !== "one" ) {
+ this.one( type, data, fn );
+
+ } else {
+ for ( var i = 0, l = this.length; i < l; i++ ) {
+ jQuery.event.add( this[i], type, handler, data );
+ }
+ }
+
+ return this;
+ };
+jQuery.prototype.blur = function( data, fn ) {
+/// <summary>
+/// Bind an event handler to the "blur" JavaScript event, or trigger that event on an element.
+/// &#10;1 - blur(handler(eventObject))
+/// &#10;2 - blur(eventData, handler(eventObject))
+/// &#10;3 - blur()
+/// </summary>
+/// <param name="data" type="Object">
+/// A map of data that will be passed to the event handler.
+/// </param>
+/// <param name="fn" type="Function">
+/// A function to execute each time the event is triggered.
+/// </param>
+/// <returns type="jQuery" />
+
+ if ( fn == null ) {
+ fn = data;
+ data = null;
+ }
+
+ return arguments.length > 0 ?
+ this.bind( name, data, fn ) :
+ this.trigger( name );
+ };
+jQuery.prototype.change = function( data, fn ) {
+/// <summary>
+/// Bind an event handler to the "change" JavaScript event, or trigger that event on an element.
+/// &#10;1 - change(handler(eventObject))
+/// &#10;2 - change(eventData, handler(eventObject))
+/// &#10;3 - change()
+/// </summary>
+/// <param name="data" type="Object">
+/// A map of data that will be passed to the event handler.
+/// </param>
+/// <param name="fn" type="Function">
+/// A function to execute each time the event is triggered.
+/// </param>
+/// <returns type="jQuery" />
+
+ if ( fn == null ) {
+ fn = data;
+ data = null;
+ }
+
+ return arguments.length > 0 ?
+ this.bind( name, data, fn ) :
+ this.trigger( name );
+ };
+jQuery.prototype.children = function( until, selector ) {
+/// <summary>
+/// Get the children of each element in the set of matched elements, optionally filtered by a selector.
+/// </summary>
+/// <param name="until" type="String">
+/// A string containing a selector expression to match elements against.
+/// </param>
+/// <returns type="jQuery" />
+
+ var ret = jQuery.map( this, fn, until ),
+ // The variable 'args' was introduced in
+ // https://github.com/jquery/jquery/commit/52a0238
+ // to work around a bug in Chrome 10 (Dev) and should be removed when the bug is fixed.
+ // http://code.google.com/p/v8/issues/detail?id=1050
+ args = slice.call(arguments);
+
+ if ( !runtil.test( name ) ) {
+ selector = until;
+ }
+
+ if ( selector && typeof selector === "string" ) {
+ ret = jQuery.filter( selector, ret );
+ }
+
+ ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret;
+
+ if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) {
+ ret = ret.reverse();
+ }
+
+ return this.pushStack( ret, name, args.join(",") );
+ };
+jQuery.prototype.clearQueue = function( type ) {
+/// <summary>
+/// Remove from the queue all items that have not yet been run.
+/// </summary>
+/// <param name="type" type="String">
+/// A string containing the name of the queue. Defaults to fx, the standard effects queue.
+/// </param>
+/// <returns type="jQuery" />
+
+ return this.queue( type || "fx", [] );
+ };
+jQuery.prototype.click = function( data, fn ) {
+/// <summary>
+/// Bind an event handler to the "click" JavaScript event, or trigger that event on an element.
+/// &#10;1 - click(handler(eventObject))
+/// &#10;2 - click(eventData, handler(eventObject))
+/// &#10;3 - click()
+/// </summary>
+/// <param name="data" type="Object">
+/// A map of data that will be passed to the event handler.
+/// </param>
+/// <param name="fn" type="Function">
+/// A function to execute each time the event is triggered.
+/// </param>
+/// <returns type="jQuery" />
+
+ if ( fn == null ) {
+ fn = data;
+ data = null;
+ }
+
+ return arguments.length > 0 ?
+ this.bind( name, data, fn ) :
+ this.trigger( name );
+ };
+jQuery.prototype.clone = function( dataAndEvents, deepDataAndEvents ) {
+/// <summary>
+/// Create a deep copy of the set of matched elements.
+/// &#10;1 - clone(withDataAndEvents)
+/// &#10;2 - clone(withDataAndEvents, deepWithDataAndEvents)
+/// </summary>
+/// <param name="dataAndEvents" type="Boolean">
+/// A Boolean indicating whether event handlers and data should be copied along with the elements. The default value is false. *For 1.5.0 the default value is incorrectly true. This will be changed back to false in 1.5.1 and up.
+/// </param>
+/// <param name="deepDataAndEvents" type="Boolean">
+/// A Boolean indicating whether event handlers and data for all children of the cloned element should be copied. By default its value matches the first argument's value (which defaults to false).
+/// </param>
+/// <returns type="jQuery" />
+
+ dataAndEvents = dataAndEvents == null ? false : dataAndEvents;
+ deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents;
+
+ return this.map( function () {
+ return jQuery.clone( this, dataAndEvents, deepDataAndEvents );
+ });
+ };
+jQuery.prototype.closest = function( selectors, context ) {
+/// <summary>
+/// 1: Get the first ancestor element that matches the selector, beginning at the current element and progressing up through the DOM tree.
+/// &#10; 1.1 - closest(selector)
+/// &#10; 1.2 - closest(selector, context)
+/// &#10;2: Gets an array of all the elements and selectors matched against the current element up through the DOM tree.
+/// &#10; 2.1 - closest(selectors, context)
+/// </summary>
+/// <param name="selectors" type="String">
+/// A string containing a selector expression to match elements against.
+/// </param>
+/// <param name="context" domElement="true">
+/// A DOM element within which a matching element may be found. If no context is passed in then the context of the jQuery set will be used instead.
+/// </param>
+/// <returns type="jQuery" />
+
+ var ret = [], i, l, cur = this[0];
+
+ if ( jQuery.isArray( selectors ) ) {
+ var match, selector,
+ matches = {},
+ level = 1;
+
+ if ( cur && selectors.length ) {
+ for ( i = 0, l = selectors.length; i < l; i++ ) {
+ selector = selectors[i];
+
+ if ( !matches[selector] ) {
+ matches[selector] = jQuery.expr.match.POS.test( selector ) ?
+ jQuery( selector, context || this.context ) :
+ selector;
+ }
+ }
+
+ while ( cur && cur.ownerDocument && cur !== context ) {
+ for ( selector in matches ) {
+ match = matches[selector];
+
+ if ( match.jquery ? match.index(cur) > -1 : jQuery(cur).is(match) ) {
+ ret.push({ selector: selector, elem: cur, level: level });
+ }
+ }
+
+ cur = cur.parentNode;
+ level++;
+ }
+ }
+
+ return ret;
+ }
+
+ var pos = POS.test( selectors ) ?
+ jQuery( selectors, context || this.context ) : null;
+
+ for ( i = 0, l = this.length; i < l; i++ ) {
+ cur = this[i];
+
+ while ( cur ) {
+ if ( pos ? pos.index(cur) > -1 : jQuery.find.matchesSelector(cur, selectors) ) {
+ ret.push( cur );
+ break;
+
+ } else {
+ cur = cur.parentNode;
+ if ( !cur || !cur.ownerDocument || cur === context ) {
+ break;
+ }
+ }
+ }
+ }
+
+ ret = ret.length > 1 ? jQuery.unique(ret) : ret;
+
+ return this.pushStack( ret, "closest", selectors );
+ };
+jQuery.prototype.constructor = function( selector, context ) {
+
+ // The jQuery object is actually just the init constructor 'enhanced'
+ return new jQuery.fn.init( selector, context, rootjQuery );
+ };
+jQuery.prototype.contents = function( until, selector ) {
+/// <summary>
+/// Get the children of each element in the set of matched elements, including text and comment nodes.
+/// </summary>
+/// <returns type="jQuery" />
+
+ var ret = jQuery.map( this, fn, until ),
+ // The variable 'args' was introduced in
+ // https://github.com/jquery/jquery/commit/52a0238
+ // to work around a bug in Chrome 10 (Dev) and should be removed when the bug is fixed.
+ // http://code.google.com/p/v8/issues/detail?id=1050
+ args = slice.call(arguments);
+
+ if ( !runtil.test( name ) ) {
+ selector = until;
+ }
+
+ if ( selector && typeof selector === "string" ) {
+ ret = jQuery.filter( selector, ret );
+ }
+
+ ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret;
+
+ if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) {
+ ret = ret.reverse();
+ }
+
+ return this.pushStack( ret, name, args.join(",") );
+ };
+jQuery.prototype.css = function( name, value ) {
+/// <summary>
+/// 1: Get the value of a style property for the first element in the set of matched elements.
+/// &#10; 1.1 - css(propertyName)
+/// &#10;2: Set one or more CSS properties for the set of matched elements.
+/// &#10; 2.1 - css(propertyName, value)
+/// &#10; 2.2 - css(propertyName, function(index, value))
+/// &#10; 2.3 - css(map)
+/// </summary>
+/// <param name="name" type="String">
+/// A CSS property name.
+/// </param>
+/// <param name="value" type="Number">
+/// A value to set for the property.
+/// </param>
+/// <returns type="jQuery" />
+
+ // Setting 'undefined' is a no-op
+ if ( arguments.length === 2 && value === undefined ) {
+ return this;
+ }
+
+ return jQuery.access( this, name, value, true, function( elem, name, value ) {
+ return value !== undefined ?
+ jQuery.style( elem, name, value ) :
+ jQuery.css( elem, name );
+ });
+};
+jQuery.prototype.data = function( key, value ) {
+/// <summary>
+/// 1: Store arbitrary data associated with the matched elements.
+/// &#10; 1.1 - data(key, value)
+/// &#10; 1.2 - data(obj)
+/// &#10;2: Returns value at named data store for the first element in the jQuery collection, as set by data(name, value).
+/// &#10; 2.1 - data(key)
+/// &#10; 2.2 - data()
+/// </summary>
+/// <param name="key" type="String">
+/// A string naming the piece of data to set.
+/// </param>
+/// <param name="value" type="Object">
+/// The new data value; it can be any Javascript type including Array or Object.
+/// </param>
+/// <returns type="jQuery" />
+
+ var data = null;
+
+ if ( typeof key === "undefined" ) {
+ if ( this.length ) {
+ data = jQuery.data( this[0] );
+
+ if ( this[0].nodeType === 1 ) {
+ var attr = this[0].attributes, name;
+ for ( var i = 0, l = attr.length; i < l; i++ ) {
+ name = attr[i].name;
+
+ if ( name.indexOf( "data-" ) === 0 ) {
+ name = name.substr( 5 );
+ dataAttr( this[0], name, data[ name ] );
+ }
+ }
+ }
+ }
+
+ return data;
+
+ } else if ( typeof key === "object" ) {
+ return this.each(function() {
+ jQuery.data( this, key );
+ });
+ }
+
+ var parts = key.split(".");
+ parts[1] = parts[1] ? "." + parts[1] : "";
+
+ if ( value === undefined ) {
+ data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]);
+
+ // Try to fetch any internally stored data first
+ if ( data === undefined && this.length ) {
+ data = jQuery.data( this[0], key );
+ data = dataAttr( this[0], key, data );
+ }
+
+ return data === undefined && parts[1] ?
+ this.data( parts[0] ) :
+ data;
+
+ } else {
+ return this.each(function() {
+ var $this = jQuery( this ),
+ args = [ parts[0], value ];
+
+ $this.triggerHandler( "setData" + parts[1] + "!", args );
+ jQuery.data( this, key, value );
+ $this.triggerHandler( "changeData" + parts[1] + "!", args );
+ });
+ }
+ };
+jQuery.prototype.dblclick = function( data, fn ) {
+/// <summary>
+/// Bind an event handler to the "dblclick" JavaScript event, or trigger that event on an element.
+/// &#10;1 - dblclick(handler(eventObject))
+/// &#10;2 - dblclick(eventData, handler(eventObject))
+/// &#10;3 - dblclick()
+/// </summary>
+/// <param name="data" type="Object">
+/// A map of data that will be passed to the event handler.
+/// </param>
+/// <param name="fn" type="Function">
+/// A function to execute each time the event is triggered.
+/// </param>
+/// <returns type="jQuery" />
+
+ if ( fn == null ) {
+ fn = data;
+ data = null;
+ }
+
+ return arguments.length > 0 ?
+ this.bind( name, data, fn ) :
+ this.trigger( name );
+ };
+jQuery.prototype.delay = function( time, type ) {
+/// <summary>
+/// Set a timer to delay execution of subsequent items in the queue.
+/// </summary>
+/// <param name="time" type="Number">
+/// An integer indicating the number of milliseconds to delay execution of the next item in the queue.
+/// </param>
+/// <param name="type" type="String">
+/// A string containing the name of the queue. Defaults to fx, the standard effects queue.
+/// </param>
+/// <returns type="jQuery" />
+
+ time = jQuery.fx ? jQuery.fx.speeds[time] || time : time;
+ type = type || "fx";
+
+ return this.queue( type, function() {
+ var elem = this;
+ setTimeout(function() {
+ jQuery.dequeue( elem, type );
+ }, time );
+ });
+ };
+jQuery.prototype.delegate = function( selector, types, data, fn ) {
+/// <summary>
+/// Attach a handler to one or more events for all elements that match the selector, now or in the future, based on a specific set of root elements.
+/// &#10;1 - delegate(selector, eventType, handler)
+/// &#10;2 - delegate(selector, eventType, eventData, handler)
+/// &#10;3 - delegate(selector, events)
+/// </summary>
+/// <param name="selector" type="String">
+/// A selector to filter the elements that trigger the event.
+/// </param>
+/// <param name="types" type="String">
+/// A string containing one or more space-separated JavaScript event types, such as "click" or "keydown," or custom event names.
+/// </param>
+/// <param name="data" type="Object">
+/// A map of data that will be passed to the event handler.
+/// </param>
+/// <param name="fn" type="Function">
+/// A function to execute at the time the event is triggered.
+/// </param>
+/// <returns type="jQuery" />
+
+ return this.live( types, data, fn, selector );
+ };
+jQuery.prototype.dequeue = function( type ) {
+/// <summary>
+/// Execute the next function on the queue for the matched elements.
+/// </summary>
+/// <param name="type" type="String">
+/// A string containing the name of the queue. Defaults to fx, the standard effects queue.
+/// </param>
+/// <returns type="jQuery" />
+
+ return this.each(function() {
+ jQuery.dequeue( this, type );
+ });
+ };
+jQuery.prototype.detach = function( selector ) {
+/// <summary>
+/// Remove the set of matched elements from the DOM.
+/// </summary>
+/// <param name="selector" type="String">
+/// A selector expression that filters the set of matched elements to be removed.
+/// </param>
+/// <returns type="jQuery" />
+
+ return this.remove( selector, true );
+ };
+jQuery.prototype.die = function( types, data, fn, origSelector /* Internal Use Only */ ) {
+/// <summary>
+/// 1: Remove all event handlers previously attached using .live() from the elements.
+/// &#10; 1.1 - die()
+/// &#10;2: Remove an event handler previously attached using .live() from the elements.
+/// &#10; 2.1 - die(eventType, handler)
+/// &#10; 2.2 - die(eventTypes)
+/// </summary>
+/// <param name="types" type="String">
+/// A string containing a JavaScript event type, such as click or keydown.
+/// </param>
+/// <param name="data" type="String">
+/// The function that is no longer to be executed.
+/// </param>
+/// <returns type="jQuery" />
+
+ var type, i = 0, match, namespaces, preType,
+ selector = origSelector || this.selector,
+ context = origSelector ? this : jQuery( this.context );
+
+ if ( typeof types === "object" && !types.preventDefault ) {
+ for ( var key in types ) {
+ context[ name ]( key, data, types[key], selector );
+ }
+
+ return this;
+ }
+
+ if ( jQuery.isFunction( data ) ) {
+ fn = data;
+ data = undefined;
+ }
+
+ types = (types || "").split(" ");
+
+ while ( (type = types[ i++ ]) != null ) {
+ match = rnamespaces.exec( type );
+ namespaces = "";
+
+ if ( match ) {
+ namespaces = match[0];
+ type = type.replace( rnamespaces, "" );
+ }
+
+ if ( type === "hover" ) {
+ types.push( "mouseenter" + namespaces, "mouseleave" + namespaces );
+ continue;
+ }
+
+ preType = type;
+
+ if ( type === "focus" || type === "blur" ) {
+ types.push( liveMap[ type ] + namespaces );
+ type = type + namespaces;
+
+ } else {
+ type = (liveMap[ type ] || type) + namespaces;
+ }
+
+ if ( name === "live" ) {
+ // bind live handler
+ for ( var j = 0, l = context.length; j < l; j++ ) {
+ jQuery.event.add( context[j], "live." + liveConvert( type, selector ),
+ { data: data, selector: selector, handler: fn, origType: type, origHandler: fn, preType: preType } );
+ }
+
+ } else {
+ // unbind live handler
+ context.unbind( "live." + liveConvert( type, selector ), fn );
+ }
+ }
+
+ return this;
+ };
+jQuery.prototype.domManip = function( args, table, callback ) {
+
+ var results, first, fragment, parent,
+ value = args[0],
+ scripts = [];
+
+ // We can't cloneNode fragments that contain checked, in WebKit
+ if ( !jQuery.support.checkClone && arguments.length === 3 && typeof value === "string" && rchecked.test( value ) ) {
+ return this.each(function() {
+ jQuery(this).domManip( args, table, callback, true );
+ });
+ }
+
+ if ( jQuery.isFunction(value) ) {
+ return this.each(function(i) {
+ var self = jQuery(this);
+ args[0] = value.call(this, i, table ? self.html() : undefined);
+ self.domManip( args, table, callback );
+ });
+ }
+
+ if ( this[0] ) {
+ parent = value && value.parentNode;
+
+ // If we're in a fragment, just use that instead of building a new one
+ if ( jQuery.support.parentNode && parent && parent.nodeType === 11 && parent.childNodes.length === this.length ) {
+ results = { fragment: parent };
+
+ } else {
+ results = jQuery.buildFragment( args, this, scripts );
+ }
+
+ fragment = results.fragment;
+
+ if ( fragment.childNodes.length === 1 ) {
+ first = fragment = fragment.firstChild;
+ } else {
+ first = fragment.firstChild;
+ }
+
+ if ( first ) {
+ table = table && jQuery.nodeName( first, "tr" );
+
+ for ( var i = 0, l = this.length, lastIndex = l - 1; i < l; i++ ) {
+ callback.call(
+ table ?
+ root(this[i], first) :
+ this[i],
+ // Make sure that we do not leak memory by inadvertently discarding
+ // the original fragment (which might have attached data) instead of
+ // using it; in addition, use the original fragment object for the last
+ // item instead of first because it can end up being emptied incorrectly
+ // in certain situations (Bug #8070).
+ // Fragments from the fragment cache must always be cloned and never used
+ // in place.
+ results.cacheable || (l > 1 && i < lastIndex) ?
+ jQuery.clone( fragment, true, true ) :
+ fragment
+ );
+ }
+ }
+
+ if ( scripts.length ) {
+ jQuery.each( scripts, evalScript );
+ }
+ }
+
+ return this;
+ };
+jQuery.prototype.each = function( callback, args ) {
+/// <summary>
+/// Iterate over a jQuery object, executing a function for each matched element.
+/// </summary>
+/// <param name="callback" type="Function">
+/// A function to execute for each matched element.
+/// </param>
+/// <returns type="jQuery" />
+
+ return jQuery.each( this, callback, args );
+ };
+jQuery.prototype.empty = function() {
+/// <summary>
+/// Remove all child nodes of the set of matched elements from the DOM.
+/// </summary>
+/// <returns type="jQuery" />
+
+ for ( var i = 0, elem; (elem = this[i]) != null; i++ ) {
+ // Remove element nodes and prevent memory leaks
+ if ( elem.nodeType === 1 ) {
+ jQuery.cleanData( elem.getElementsByTagName("*") );
+ }
+
+ // Remove any remaining nodes
+ while ( elem.firstChild ) {
+ elem.removeChild( elem.firstChild );
+ }
+ }
+
+ return this;
+ };
+jQuery.prototype.end = function() {
+/// <summary>
+/// End the most recent filtering operation in the current chain and return the set of matched elements to its previous state.
+/// </summary>
+/// <returns type="jQuery" />
+
+ return this.prevObject || this.constructor(null);
+ };
+jQuery.prototype.eq = function( i ) {
+/// <summary>
+/// Reduce the set of matched elements to the one at the specified index.
+/// &#10;1 - eq(index)
+/// &#10;2 - eq(-index)
+/// </summary>
+/// <param name="i" type="Number">
+/// An integer indicating the 0-based position of the element.
+/// </param>
+/// <returns type="jQuery" />
+
+ return i === -1 ?
+ this.slice( i ) :
+ this.slice( i, +i + 1 );
+ };
+jQuery.prototype.error = function( data, fn ) {
+/// <summary>
+/// Bind an event handler to the "error" JavaScript event.
+/// &#10;1 - error(handler(eventObject))
+/// &#10;2 - error(eventData, handler(eventObject))
+/// </summary>
+/// <param name="data" type="Object">
+/// A map of data that will be passed to the event handler.
+/// </param>
+/// <param name="fn" type="Function">
+/// A function to execute each time the event is triggered.
+/// </param>
+/// <returns type="jQuery" />
+
+ if ( fn == null ) {
+ fn = data;
+ data = null;
+ }
+
+ return arguments.length > 0 ?
+ this.bind( name, data, fn ) :
+ this.trigger( name );
+ };
+jQuery.prototype.extend = function() {
+
+ var options, name, src, copy, copyIsArray, clone,
+ target = arguments[0] || {},
+ i = 1,
+ length = arguments.length,
+ deep = false;
+
+ // Handle a deep copy situation
+ if ( typeof target === "boolean" ) {
+ deep = target;
+ target = arguments[1] || {};
+ // skip the boolean and the target
+ i = 2;
+ }
+
+ // Handle case when target is a string or something (possible in deep copy)
+ if ( typeof target !== "object" && !jQuery.isFunction(target) ) {
+ target = {};
+ }
+
+ // extend jQuery itself if only one argument is passed
+ if ( length === i ) {
+ target = this;
+ --i;
+ }
+
+ for ( ; i < length; i++ ) {
+ // Only deal with non-null/undefined values
+ if ( (options = arguments[ i ]) != null ) {
+ // Extend the base object
+ for ( name in options ) {
+ src = target[ name ];
+ copy = options[ name ];
+
+ // Prevent never-ending loop
+ if ( target === copy ) {
+ continue;
+ }
+
+ // Recurse if we're merging plain objects or arrays
+ if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) {
+ if ( copyIsArray ) {
+ copyIsArray = false;
+ clone = src && jQuery.isArray(src) ? src : [];
+
+ } else {
+ clone = src && jQuery.isPlainObject(src) ? src : {};
+ }
+
+ // Never move original objects, clone them
+ target[ name ] = jQuery.extend( deep, clone, copy );
+
+ // Don't bring in undefined values
+ } else if ( copy !== undefined ) {
+ target[ name ] = copy;
+ }
+ }
+ }
+ }
+
+ // Return the modified object
+ return target;
+};
+jQuery.prototype.fadeIn = function( speed, easing, callback ) {
+/// <summary>
+/// Display the matched elements by fading them to opaque.
+/// &#10;1 - fadeIn(duration, callback)
+/// &#10;2 - fadeIn(duration, easing, callback)
+/// </summary>
+/// <param name="speed" type="Number">
+/// A string or number determining how long the animation will run.
+/// </param>
+/// <param name="easing" type="String">
+/// A string indicating which easing function to use for the transition.
+/// </param>
+/// <param name="callback" type="Function">
+/// A function to call once the animation is complete.
+/// </param>
+/// <returns type="jQuery" />
+
+ return this.animate( props, speed, easing, callback );
+ };
+jQuery.prototype.fadeOut = function( speed, easing, callback ) {
+/// <summary>
+/// Hide the matched elements by fading them to transparent.
+/// &#10;1 - fadeOut(duration, callback)
+/// &#10;2 - fadeOut(duration, easing, callback)
+/// </summary>
+/// <param name="speed" type="Number">
+/// A string or number determining how long the animation will run.
+/// </param>
+/// <param name="easing" type="String">
+/// A string indicating which easing function to use for the transition.
+/// </param>
+/// <param name="callback" type="Function">
+/// A function to call once the animation is complete.
+/// </param>
+/// <returns type="jQuery" />
+
+ return this.animate( props, speed, easing, callback );
+ };
+jQuery.prototype.fadeTo = function( speed, to, easing, callback ) {
+/// <summary>
+/// Adjust the opacity of the matched elements.
+/// &#10;1 - fadeTo(duration, opacity, callback)
+/// &#10;2 - fadeTo(duration, opacity, easing, callback)
+/// </summary>
+/// <param name="speed" type="Number">
+/// A string or number determining how long the animation will run.
+/// </param>
+/// <param name="to" type="Number">
+/// A number between 0 and 1 denoting the target opacity.
+/// </param>
+/// <param name="easing" type="String">
+/// A string indicating which easing function to use for the transition.
+/// </param>
+/// <param name="callback" type="Function">
+/// A function to call once the animation is complete.
+/// </param>
+/// <returns type="jQuery" />
+
+ return this.filter(":hidden").css("opacity", 0).show().end()
+ .animate({opacity: to}, speed, easing, callback);
+ };
+jQuery.prototype.fadeToggle = function( speed, easing, callback ) {
+/// <summary>
+/// Display or hide the matched elements by animating their opacity.
+/// </summary>
+/// <param name="speed" type="Number">
+/// A string or number determining how long the animation will run.
+/// </param>
+/// <param name="easing" type="String">
+/// A string indicating which easing function to use for the transition.
+/// </param>
+/// <param name="callback" type="Function">
+/// A function to call once the animation is complete.
+/// </param>
+/// <returns type="jQuery" />
+
+ return this.animate( props, speed, easing, callback );
+ };
+jQuery.prototype.filter = function( selector ) {
+/// <summary>
+/// Reduce the set of matched elements to those that match the selector or pass the function's test.
+/// &#10;1 - filter(selector)
+/// &#10;2 - filter(function(index))
+/// &#10;3 - filter(element)
+/// &#10;4 - filter(jQuery object)
+/// </summary>
+/// <param name="selector" type="String">
+/// A string containing a selector expression to match the current set of elements against.
+/// </param>
+/// <returns type="jQuery" />
+
+ return this.pushStack( winnow(this, selector, true), "filter", selector );
+ };
+jQuery.prototype.find = function( selector ) {
+/// <summary>
+/// Get the descendants of each element in the current set of matched elements, filtered by a selector.
+/// </summary>
+/// <param name="selector" type="String">
+/// A string containing a selector expression to match elements against.
+/// </param>
+/// <returns type="jQuery" />
+
+ var ret = this.pushStack( "", "find", selector ),
+ length = 0;
+
+ for ( var i = 0, l = this.length; i < l; i++ ) {
+ length = ret.length;
+ jQuery.find( selector, this[i], ret );
+
+ if ( i > 0 ) {
+ // Make sure that the results are unique
+ for ( var n = length; n < ret.length; n++ ) {
+ for ( var r = 0; r < length; r++ ) {
+ if ( ret[r] === ret[n] ) {
+ ret.splice(n--, 1);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ return ret;
+ };
+jQuery.prototype.first = function() {
+/// <summary>
+/// Reduce the set of matched elements to the first in the set.
+/// </summary>
+/// <returns type="jQuery" />
+
+ return this.eq( 0 );
+ };
+jQuery.prototype.focus = function( data, fn ) {
+/// <summary>
+/// Bind an event handler to the "focus" JavaScript event, or trigger that event on an element.
+/// &#10;1 - focus(handler(eventObject))
+/// &#10;2 - focus(eventData, handler(eventObject))
+/// &#10;3 - focus()
+/// </summary>
+/// <param name="data" type="Object">
+/// A map of data that will be passed to the event handler.
+/// </param>
+/// <param name="fn" type="Function">
+/// A function to execute each time the event is triggered.
+/// </param>
+/// <returns type="jQuery" />
+
+ if ( fn == null ) {
+ fn = data;
+ data = null;
+ }
+
+ return arguments.length > 0 ?
+ this.bind( name, data, fn ) :
+ this.trigger( name );
+ };
+jQuery.prototype.focusin = function( data, fn ) {
+/// <summary>
+/// Bind an event handler to the "focusin" JavaScript event.
+/// &#10;1 - focusin(handler(eventObject))
+/// &#10;2 - focusin(eventData, handler(eventObject))
+/// </summary>
+/// <param name="data" type="Object">
+/// A map of data that will be passed to the event handler.
+/// </param>
+/// <param name="fn" type="Function">
+/// A function to execute each time the event is triggered.
+/// </param>
+/// <returns type="jQuery" />
+
+ if ( fn == null ) {
+ fn = data;
+ data = null;
+ }
+
+ return arguments.length > 0 ?
+ this.bind( name, data, fn ) :
+ this.trigger( name );
+ };
+jQuery.prototype.focusout = function( data, fn ) {
+/// <summary>
+/// Bind an event handler to the "focusout" JavaScript event.
+/// &#10;1 - focusout(handler(eventObject))
+/// &#10;2 - focusout(eventData, handler(eventObject))
+/// </summary>
+/// <param name="data" type="Object">
+/// A map of data that will be passed to the event handler.
+/// </param>
+/// <param name="fn" type="Function">
+/// A function to execute each time the event is triggered.
+/// </param>
+/// <returns type="jQuery" />
+
+ if ( fn == null ) {
+ fn = data;
+ data = null;
+ }
+
+ return arguments.length > 0 ?
+ this.bind( name, data, fn ) :
+ this.trigger( name );
+ };
+jQuery.prototype.get = function( num ) {
+/// <summary>
+/// Retrieve the DOM elements matched by the jQuery object.
+/// </summary>
+/// <param name="num" type="Number">
+/// A zero-based integer indicating which element to retrieve.
+/// </param>
+/// <returns type="Array" />
+
+ return num == null ?
+
+ // Return a 'clean' array
+ this.toArray() :
+
+ // Return just the object
+ ( num < 0 ? this[ this.length + num ] : this[ num ] );
+ };
+jQuery.prototype.has = function( target ) {
+/// <summary>
+/// Reduce the set of matched elements to those that have a descendant that matches the selector or DOM element.
+/// &#10;1 - has(selector)
+/// &#10;2 - has(contained)
+/// </summary>
+/// <param name="target" type="String">
+/// A string containing a selector expression to match elements against.
+/// </param>
+/// <returns type="jQuery" />
+
+ var targets = jQuery( target );
+ return this.filter(function() {
+ for ( var i = 0, l = targets.length; i < l; i++ ) {
+ if ( jQuery.contains( this, targets[i] ) ) {
+ return true;
+ }
+ }
+ });
+ };
+jQuery.prototype.hasClass = function( selector ) {
+/// <summary>
+/// Determine whether any of the matched elements are assigned the given class.
+/// </summary>
+/// <param name="selector" type="String">
+/// The class name to search for.
+/// </param>
+/// <returns type="Boolean" />
+
+ var className = " " + selector + " ";
+ for ( var i = 0, l = this.length; i < l; i++ ) {
+ if ( (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) > -1 ) {
+ return true;
+ }
+ }
+
+ return false;
+ };
+jQuery.prototype.height = function( size ) {
+/// <summary>
+/// 1: Get the current computed height for the first element in the set of matched elements.
+/// &#10; 1.1 - height()
+/// &#10;2: Set the CSS height of every matched element.
+/// &#10; 2.1 - height(value)
+/// &#10; 2.2 - height(function(index, height))
+/// </summary>
+/// <param name="size" type="Number">
+/// An integer representing the number of pixels, or an integer with an optional unit of measure appended (as a string).
+/// </param>
+/// <returns type="jQuery" />
+
+ // Get window width or height
+ var elem = this[0];
+ if ( !elem ) {
+ return size == null ? null : this;
+ }
+
+ if ( jQuery.isFunction( size ) ) {
+ return this.each(function( i ) {
+ var self = jQuery( this );
+ self[ type ]( size.call( this, i, self[ type ]() ) );
+ });
+ }
+
+ if ( jQuery.isWindow( elem ) ) {
+ // Everyone else use document.documentElement or document.body depending on Quirks vs Standards mode
+ // 3rd condition allows Nokia support, as it supports the docElem prop but not CSS1Compat
+ var docElemProp = elem.document.documentElement[ "client" + name ];
+ return elem.document.compatMode === "CSS1Compat" && docElemProp ||
+ elem.document.body[ "client" + name ] || docElemProp;
+
+ // Get document width or height
+ } else if ( elem.nodeType === 9 ) {
+ // Either scroll[Width/Height] or offset[Width/Height], whichever is greater
+ return Math.max(
+ elem.documentElement["client" + name],
+ elem.body["scroll" + name], elem.documentElement["scroll" + name],
+ elem.body["offset" + name], elem.documentElement["offset" + name]
+ );
+
+ // Get or set width or height on the element
+ } else if ( size === undefined ) {
+ var orig = jQuery.css( elem, type ),
+ ret = parseFloat( orig );
+
+ return jQuery.isNaN( ret ) ? orig : ret;
+
+ // Set the width or height on the element (default to pixels if value is unitless)
+ } else {
+ return this.css( type, typeof size === "string" ? size : size + "px" );
+ }
+ };
+jQuery.prototype.hide = function( speed, easing, callback ) {
+/// <summary>
+/// Hide the matched elements.
+/// &#10;1 - hide()
+/// &#10;2 - hide(duration, callback)
+/// &#10;3 - hide(duration, easing, callback)
+/// </summary>
+/// <param name="speed" type="Number">
+/// A string or number determining how long the animation will run.
+/// </param>
+/// <param name="easing" type="String">
+/// A string indicating which easing function to use for the transition.
+/// </param>
+/// <param name="callback" type="Function">
+/// A function to call once the animation is complete.
+/// </param>
+/// <returns type="jQuery" />
+
+ if ( speed || speed === 0 ) {
+ return this.animate( genFx("hide", 3), speed, easing, callback);
+
+ } else {
+ for ( var i = 0, j = this.length; i < j; i++ ) {
+ var display = jQuery.css( this[i], "display" );
+
+ if ( display !== "none" && !jQuery._data( this[i], "olddisplay" ) ) {
+ jQuery._data( this[i], "olddisplay", display );
+ }
+ }
+
+ // Set the display of the elements in a second loop
+ // to avoid the constant reflow
+ for ( i = 0; i < j; i++ ) {
+ this[i].style.display = "none";
+ }
+
+ return this;
+ }
+ };
+jQuery.prototype.hover = function( fnOver, fnOut ) {
+/// <summary>
+/// 1: Bind two handlers to the matched elements, to be executed when the mouse pointer enters and leaves the elements.
+/// &#10; 1.1 - hover(handlerIn(eventObject), handlerOut(eventObject))
+/// &#10;2: Bind a single handler to the matched elements, to be executed when the mouse pointer enters or leaves the elements.
+/// &#10; 2.1 - hover(handlerInOut(eventObject))
+/// </summary>
+/// <param name="fnOver" type="Function">
+/// A function to execute when the mouse pointer enters the element.
+/// </param>
+/// <param name="fnOut" type="Function">
+/// A function to execute when the mouse pointer leaves the element.
+/// </param>
+/// <returns type="jQuery" />
+
+ return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver );
+ };
+jQuery.prototype.html = function( value ) {
+/// <summary>
+/// 1: Get the HTML contents of the first element in the set of matched elements.
+/// &#10; 1.1 - html()
+/// &#10;2: Set the HTML contents of each element in the set of matched elements.
+/// &#10; 2.1 - html(htmlString)
+/// &#10; 2.2 - html(function(index, oldhtml))
+/// </summary>
+/// <param name="value" type="String">
+/// A string of HTML to set as the content of each matched element.
+/// </param>
+/// <returns type="jQuery" />
+
+ if ( value === undefined ) {
+ return this[0] && this[0].nodeType === 1 ?
+ this[0].innerHTML.replace(rinlinejQuery, "") :
+ null;
+
+ // See if we can take a shortcut and just use innerHTML
+ } else if ( typeof value === "string" && !rnocache.test( value ) &&
+ (jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value )) &&
+ !wrapMap[ (rtagName.exec( value ) || ["", ""])[1].toLowerCase() ] ) {
+
+ value = value.replace(rxhtmlTag, "<$1></$2>");
+
+ try {
+ for ( var i = 0, l = this.length; i < l; i++ ) {
+ // Remove element nodes and prevent memory leaks
+ if ( this[i].nodeType === 1 ) {
+ jQuery.cleanData( this[i].getElementsByTagName("*") );
+ this[i].innerHTML = value;
+ }
+ }
+
+ // If using innerHTML throws an exception, use the fallback method
+ } catch(e) {
+ this.empty().append( value );
+ }
+
+ } else if ( jQuery.isFunction( value ) ) {
+ this.each(function(i){
+ var self = jQuery( this );
+
+ self.html( value.call(this, i, self.html()) );
+ });
+
+ } else {
+ this.empty().append( value );
+ }
+
+ return this;
+ };
+jQuery.prototype.index = function( elem ) {
+/// <summary>
+/// Search for a given element from among the matched elements.
+/// &#10;1 - index()
+/// &#10;2 - index(selector)
+/// &#10;3 - index(element)
+/// </summary>
+/// <param name="elem" type="String">
+/// A selector representing a jQuery collection in which to look for an element.
+/// </param>
+/// <returns type="Number" />
+
+ if ( !elem || typeof elem === "string" ) {
+ return jQuery.inArray( this[0],
+ // If it receives a string, the selector is used
+ // If it receives nothing, the siblings are used
+ elem ? jQuery( elem ) : this.parent().children() );
+ }
+ // Locate the position of the desired element
+ return jQuery.inArray(
+ // If it receives a jQuery object, the first element is used
+ elem.jquery ? elem[0] : elem, this );
+ };
+jQuery.prototype.init = function( selector, context, rootjQuery ) {
+
+ var match, elem, ret, doc;
+
+ // Handle $(""), $(null), or $(undefined)
+ if ( !selector ) {
+ return this;
+ }
+
+ // Handle $(DOMElement)
+ if ( selector.nodeType ) {
+ this.context = this[0] = selector;
+ this.length = 1;
+ return this;
+ }
+
+ // The body element only exists once, optimize finding it
+ if ( selector === "body" && !context && document.body ) {
+ this.context = document;
+ this[0] = document.body;
+ this.selector = "body";
+ this.length = 1;
+ return this;
+ }
+
+ // Handle HTML strings
+ if ( typeof selector === "string" ) {
+ // Are we dealing with HTML string or an ID?
+ match = quickExpr.exec( selector );
+
+ // Verify a match, and that no context was specified for #id
+ if ( match && (match[1] || !context) ) {
+
+ // HANDLE: $(html) -> $(array)
+ if ( match[1] ) {
+ context = context instanceof jQuery ? context[0] : context;
+ doc = (context ? context.ownerDocument || context : document);
+
+ // If a single string is passed in and it's a single tag
+ // just do a createElement and skip the rest
+ ret = rsingleTag.exec( selector );
+
+ if ( ret ) {
+ if ( jQuery.isPlainObject( context ) ) {
+ selector = [ document.createElement( ret[1] ) ];
+ jQuery.fn.attr.call( selector, context, true );
+
+ } else {
+ selector = [ doc.createElement( ret[1] ) ];
+ }
+
+ } else {
+ ret = jQuery.buildFragment( [ match[1] ], [ doc ] );
+ selector = (ret.cacheable ? jQuery.clone(ret.fragment) : ret.fragment).childNodes;
+ }
+
+ return jQuery.merge( this, selector );
+
+ // HANDLE: $("#id")
+ } else {
+ elem = document.getElementById( match[2] );
+
+ // Check parentNode to catch when Blackberry 4.6 returns
+ // nodes that are no longer in the document #6963
+ if ( elem && elem.parentNode ) {
+ // Handle the case where IE and Opera return items
+ // by name instead of ID
+ if ( elem.id !== match[2] ) {
+ return rootjQuery.find( selector );
+ }
+
+ // Otherwise, we inject the element directly into the jQuery object
+ this.length = 1;
+ this[0] = elem;
+ }
+
+ this.context = document;
+ this.selector = selector;
+ return this;
+ }
+
+ // HANDLE: $(expr, $(...))
+ } else if ( !context || context.jquery ) {
+ return (context || rootjQuery).find( selector );
+
+ // HANDLE: $(expr, context)
+ // (which is just equivalent to: $(context).find(expr)
+ } else {
+ return this.constructor( context ).find( selector );
+ }
+
+ // HANDLE: $(function)
+ // Shortcut for document ready
+ } else if ( jQuery.isFunction( selector ) ) {
+ return rootjQuery.ready( selector );
+ }
+
+ if (selector.selector !== undefined) {
+ this.selector = selector.selector;
+ this.context = selector.context;
+ }
+
+ return jQuery.makeArray( selector, this );
+ };
+jQuery.prototype.innerHeight = function() {
+/// <summary>
+/// Get the current computed height for the first element in the set of matched elements, including padding but not border.
+/// </summary>
+/// <returns type="Number" />
+
+ return this[0] ?
+ parseFloat( jQuery.css( this[0], type, "padding" ) ) :
+ null;
+ };
+jQuery.prototype.innerWidth = function() {
+/// <summary>
+/// Get the current computed width for the first element in the set of matched elements, including padding but not border.
+/// </summary>
+/// <returns type="Number" />
+
+ return this[0] ?
+ parseFloat( jQuery.css( this[0], type, "padding" ) ) :
+ null;
+ };
+jQuery.prototype.insertAfter = function( selector ) {
+/// <summary>
+/// Insert every element in the set of matched elements after the target.
+/// </summary>
+/// <param name="selector" type="jQuery">
+/// A selector, element, HTML string, or jQuery object; the matched set of elements will be inserted after the element(s) specified by this parameter.
+/// </param>
+/// <returns type="jQuery" />
+
+ var ret = [],
+ insert = jQuery( selector ),
+ parent = this.length === 1 && this[0].parentNode;
+
+ if ( parent && parent.nodeType === 11 && parent.childNodes.length === 1 && insert.length === 1 ) {
+ insert[ original ]( this[0] );
+ return this;
+
+ } else {
+ for ( var i = 0, l = insert.length; i < l; i++ ) {
+ var elems = (i > 0 ? this.clone(true) : this).get();
+ jQuery( insert[i] )[ original ]( elems );
+ ret = ret.concat( elems );
+ }
+
+ return this.pushStack( ret, name, insert.selector );
+ }
+ };
+jQuery.prototype.insertBefore = function( selector ) {
+/// <summary>
+/// Insert every element in the set of matched elements before the target.
+/// </summary>
+/// <param name="selector" type="jQuery">
+/// A selector, element, HTML string, or jQuery object; the matched set of elements will be inserted before the element(s) specified by this parameter.
+/// </param>
+/// <returns type="jQuery" />
+
+ var ret = [],
+ insert = jQuery( selector ),
+ parent = this.length === 1 && this[0].parentNode;
+
+ if ( parent && parent.nodeType === 11 && parent.childNodes.length === 1 && insert.length === 1 ) {
+ insert[ original ]( this[0] );
+ return this;
+
+ } else {
+ for ( var i = 0, l = insert.length; i < l; i++ ) {
+ var elems = (i > 0 ? this.clone(true) : this).get();
+ jQuery( insert[i] )[ original ]( elems );
+ ret = ret.concat( elems );
+ }
+
+ return this.pushStack( ret, name, insert.selector );
+ }
+ };
+jQuery.prototype.is = function( selector ) {
+/// <summary>
+/// Check the current matched set of elements against a selector and return true if at least one of these elements matches the selector.
+/// </summary>
+/// <param name="selector" type="String">
+/// A string containing a selector expression to match elements against.
+/// </param>
+/// <returns type="Boolean" />
+
+ return !!selector && jQuery.filter( selector, this ).length > 0;
+ };
+jQuery.prototype.keydown = function( data, fn ) {
+/// <summary>
+/// Bind an event handler to the "keydown" JavaScript event, or trigger that event on an element.
+/// &#10;1 - keydown(handler(eventObject))
+/// &#10;2 - keydown(eventData, handler(eventObject))
+/// &#10;3 - keydown()
+/// </summary>
+/// <param name="data" type="Object">
+/// A map of data that will be passed to the event handler.
+/// </param>
+/// <param name="fn" type="Function">
+/// A function to execute each time the event is triggered.
+/// </param>
+/// <returns type="jQuery" />
+
+ if ( fn == null ) {
+ fn = data;
+ data = null;
+ }
+
+ return arguments.length > 0 ?
+ this.bind( name, data, fn ) :
+ this.trigger( name );
+ };
+jQuery.prototype.keypress = function( data, fn ) {
+/// <summary>
+/// Bind an event handler to the "keypress" JavaScript event, or trigger that event on an element.
+/// &#10;1 - keypress(handler(eventObject))
+/// &#10;2 - keypress(eventData, handler(eventObject))
+/// &#10;3 - keypress()
+/// </summary>
+/// <param name="data" type="Object">
+/// A map of data that will be passed to the event handler.
+/// </param>
+/// <param name="fn" type="Function">
+/// A function to execute each time the event is triggered.
+/// </param>
+/// <returns type="jQuery" />
+
+ if ( fn == null ) {
+ fn = data;
+ data = null;
+ }
+
+ return arguments.length > 0 ?
+ this.bind( name, data, fn ) :
+ this.trigger( name );
+ };
+jQuery.prototype.keyup = function( data, fn ) {
+/// <summary>
+/// Bind an event handler to the "keyup" JavaScript event, or trigger that event on an element.
+/// &#10;1 - keyup(handler(eventObject))
+/// &#10;2 - keyup(eventData, handler(eventObject))
+/// &#10;3 - keyup()
+/// </summary>
+/// <param name="data" type="Object">
+/// A map of data that will be passed to the event handler.
+/// </param>
+/// <param name="fn" type="Function">
+/// A function to execute each time the event is triggered.
+/// </param>
+/// <returns type="jQuery" />
+
+ if ( fn == null ) {
+ fn = data;
+ data = null;
+ }
+
+ return arguments.length > 0 ?
+ this.bind( name, data, fn ) :
+ this.trigger( name );
+ };
+jQuery.prototype.last = function() {
+/// <summary>
+/// Reduce the set of matched elements to the final one in the set.
+/// </summary>
+/// <returns type="jQuery" />
+
+ return this.eq( -1 );
+ };
+jQuery.prototype.length = 0;
+jQuery.prototype.live = function( types, data, fn, origSelector /* Internal Use Only */ ) {
+/// <summary>
+/// Attach a handler to the event for all elements which match the current selector, now and in the future.
+/// &#10;1 - live(eventType, handler)
+/// &#10;2 - live(eventType, eventData, handler)
+/// &#10;3 - live(events)
+/// </summary>
+/// <param name="types" type="String">
+/// A string containing a JavaScript event type, such as "click" or "keydown." As of jQuery 1.4 the string can contain multiple, space-separated event types or custom event names, as well.
+/// </param>
+/// <param name="data" type="Object">
+/// A map of data that will be passed to the event handler.
+/// </param>
+/// <param name="fn" type="Function">
+/// A function to execute at the time the event is triggered.
+/// </param>
+/// <returns type="jQuery" />
+
+ var type, i = 0, match, namespaces, preType,
+ selector = origSelector || this.selector,
+ context = origSelector ? this : jQuery( this.context );
+
+ if ( typeof types === "object" && !types.preventDefault ) {
+ for ( var key in types ) {
+ context[ name ]( key, data, types[key], selector );
+ }
+
+ return this;
+ }
+
+ if ( jQuery.isFunction( data ) ) {
+ fn = data;
+ data = undefined;
+ }
+
+ types = (types || "").split(" ");
+
+ while ( (type = types[ i++ ]) != null ) {
+ match = rnamespaces.exec( type );
+ namespaces = "";
+
+ if ( match ) {
+ namespaces = match[0];
+ type = type.replace( rnamespaces, "" );
+ }
+
+ if ( type === "hover" ) {
+ types.push( "mouseenter" + namespaces, "mouseleave" + namespaces );
+ continue;
+ }
+
+ preType = type;
+
+ if ( type === "focus" || type === "blur" ) {
+ types.push( liveMap[ type ] + namespaces );
+ type = type + namespaces;
+
+ } else {
+ type = (liveMap[ type ] || type) + namespaces;
+ }
+
+ if ( name === "live" ) {
+ // bind live handler
+ for ( var j = 0, l = context.length; j < l; j++ ) {
+ jQuery.event.add( context[j], "live." + liveConvert( type, selector ),
+ { data: data, selector: selector, handler: fn, origType: type, origHandler: fn, preType: preType } );
+ }
+
+ } else {
+ // unbind live handler
+ context.unbind( "live." + liveConvert( type, selector ), fn );
+ }
+ }
+
+ return this;
+ };
+jQuery.prototype.load = function( url, params, callback ) {
+/// <summary>
+/// 1: Bind an event handler to the "load" JavaScript event.
+/// &#10; 1.1 - load(handler(eventObject))
+/// &#10; 1.2 - load(eventData, handler(eventObject))
+/// &#10;2: Load data from the server and place the returned HTML into the matched element.
+/// &#10; 2.1 - load(url, data, complete(responseText, textStatus, XMLHttpRequest))
+/// </summary>
+/// <param name="url" type="String">
+/// A string containing the URL to which the request is sent.
+/// </param>
+/// <param name="params" type="String">
+/// A map or string that is sent to the server with the request.
+/// </param>
+/// <param name="callback" type="Function">
+/// A callback function that is executed when the request completes.
+/// </param>
+/// <returns type="jQuery" />
+
+ if ( typeof url !== "string" && _load ) {
+ return _load.apply( this, arguments );
+
+ // Don't do a request if no elements are being requested
+ } else if ( !this.length ) {
+ return this;
+ }
+
+ var off = url.indexOf( " " );
+ if ( off >= 0 ) {
+ var selector = url.slice( off, url.length );
+ url = url.slice( 0, off );
+ }
+
+ // Default to a GET request
+ var type = "GET";
+
+ // If the second parameter was provided
+ if ( params ) {
+ // If it's a function
+ if ( jQuery.isFunction( params ) ) {
+ // We assume that it's the callback
+ callback = params;
+ params = undefined;
+
+ // Otherwise, build a param string
+ } else if ( typeof params === "object" ) {
+ params = jQuery.param( params, jQuery.ajaxSettings.traditional );
+ type = "POST";
+ }
+ }
+
+ var self = this;
+
+ // Request the remote document
+ jQuery.ajax({
+ url: url,
+ type: type,
+ dataType: "html",
+ data: params,
+ // Complete callback (responseText is used internally)
+ complete: function( jqXHR, status, responseText ) {
+ // Store the response as specified by the jqXHR object
+ responseText = jqXHR.responseText;
+ // If successful, inject the HTML into all the matched elements
+ if ( jqXHR.isResolved() ) {
+ // #4825: Get the actual response in case
+ // a dataFilter is present in ajaxSettings
+ jqXHR.done(function( r ) {
+ responseText = r;
+ });
+ // See if a selector was specified
+ self.html( selector ?
+ // Create a dummy div to hold the results
+ jQuery("<div>")
+ // inject the contents of the document in, removing the scripts
+ // to avoid any 'Permission Denied' errors in IE
+ .append(responseText.replace(rscript, ""))
+
+ // Locate the specified elements
+ .find(selector) :
+
+ // If not, just inject the full result
+ responseText );
+ }
+
+ if ( callback ) {
+ self.each( callback, [ responseText, status, jqXHR ] );
+ }
+ }
+ });
+
+ return this;
+ };
+jQuery.prototype.map = function( callback ) {
+/// <summary>
+/// Pass each element in the current matched set through a function, producing a new jQuery object containing the return values.
+/// </summary>
+/// <param name="callback" type="Function">
+/// A function object that will be invoked for each element in the current set.
+/// </param>
+/// <returns type="jQuery" />
+
+ return this.pushStack( jQuery.map(this, function( elem, i ) {
+ return callback.call( elem, i, elem );
+ }));
+ };
+jQuery.prototype.mousedown = function( data, fn ) {
+/// <summary>
+/// Bind an event handler to the "mousedown" JavaScript event, or trigger that event on an element.
+/// &#10;1 - mousedown(handler(eventObject))
+/// &#10;2 - mousedown(eventData, handler(eventObject))
+/// &#10;3 - mousedown()
+/// </summary>
+/// <param name="data" type="Object">
+/// A map of data that will be passed to the event handler.
+/// </param>
+/// <param name="fn" type="Function">
+/// A function to execute each time the event is triggered.
+/// </param>
+/// <returns type="jQuery" />
+
+ if ( fn == null ) {
+ fn = data;
+ data = null;
+ }
+
+ return arguments.length > 0 ?
+ this.bind( name, data, fn ) :
+ this.trigger( name );
+ };
+jQuery.prototype.mouseenter = function( data, fn ) {
+/// <summary>
+/// Bind an event handler to be fired when the mouse enters an element, or trigger that handler on an element.
+/// &#10;1 - mouseenter(handler(eventObject))
+/// &#10;2 - mouseenter(eventData, handler(eventObject))
+/// &#10;3 - mouseenter()
+/// </summary>
+/// <param name="data" type="Object">
+/// A map of data that will be passed to the event handler.
+/// </param>
+/// <param name="fn" type="Function">
+/// A function to execute each time the event is triggered.
+/// </param>
+/// <returns type="jQuery" />
+
+ if ( fn == null ) {
+ fn = data;
+ data = null;
+ }
+
+ return arguments.length > 0 ?
+ this.bind( name, data, fn ) :
+ this.trigger( name );
+ };
+jQuery.prototype.mouseleave = function( data, fn ) {
+/// <summary>
+/// Bind an event handler to be fired when the mouse leaves an element, or trigger that handler on an element.
+/// &#10;1 - mouseleave(handler(eventObject))
+/// &#10;2 - mouseleave(eventData, handler(eventObject))
+/// &#10;3 - mouseleave()
+/// </summary>
+/// <param name="data" type="Object">
+/// A map of data that will be passed to the event handler.
+/// </param>
+/// <param name="fn" type="Function">
+/// A function to execute each time the event is triggered.
+/// </param>
+/// <returns type="jQuery" />
+
+ if ( fn == null ) {
+ fn = data;
+ data = null;
+ }
+
+ return arguments.length > 0 ?
+ this.bind( name, data, fn ) :
+ this.trigger( name );
+ };
+jQuery.prototype.mousemove = function( data, fn ) {
+/// <summary>
+/// Bind an event handler to the "mousemove" JavaScript event, or trigger that event on an element.
+/// &#10;1 - mousemove(handler(eventObject))
+/// &#10;2 - mousemove(eventData, handler(eventObject))
+/// &#10;3 - mousemove()
+/// </summary>
+/// <param name="data" type="Object">
+/// A map of data that will be passed to the event handler.
+/// </param>
+/// <param name="fn" type="Function">
+/// A function to execute each time the event is triggered.
+/// </param>
+/// <returns type="jQuery" />
+
+ if ( fn == null ) {
+ fn = data;
+ data = null;
+ }
+
+ return arguments.length > 0 ?
+ this.bind( name, data, fn ) :
+ this.trigger( name );
+ };
+jQuery.prototype.mouseout = function( data, fn ) {
+/// <summary>
+/// Bind an event handler to the "mouseout" JavaScript event, or trigger that event on an element.
+/// &#10;1 - mouseout(handler(eventObject))
+/// &#10;2 - mouseout(eventData, handler(eventObject))
+/// &#10;3 - mouseout()
+/// </summary>
+/// <param name="data" type="Object">
+/// A map of data that will be passed to the event handler.
+/// </param>
+/// <param name="fn" type="Function">
+/// A function to execute each time the event is triggered.
+/// </param>
+/// <returns type="jQuery" />
+
+ if ( fn == null ) {
+ fn = data;
+ data = null;
+ }
+
+ return arguments.length > 0 ?
+ this.bind( name, data, fn ) :
+ this.trigger( name );
+ };
+jQuery.prototype.mouseover = function( data, fn ) {
+/// <summary>
+/// Bind an event handler to the "mouseover" JavaScript event, or trigger that event on an element.
+/// &#10;1 - mouseover(handler(eventObject))
+/// &#10;2 - mouseover(eventData, handler(eventObject))
+/// &#10;3 - mouseover()
+/// </summary>
+/// <param name="data" type="Object">
+/// A map of data that will be passed to the event handler.
+/// </param>
+/// <param name="fn" type="Function">
+/// A function to execute each time the event is triggered.
+/// </param>
+/// <returns type="jQuery" />
+
+ if ( fn == null ) {
+ fn = data;
+ data = null;
+ }
+
+ return arguments.length > 0 ?
+ this.bind( name, data, fn ) :
+ this.trigger( name );
+ };
+jQuery.prototype.mouseup = function( data, fn ) {
+/// <summary>
+/// Bind an event handler to the "mouseup" JavaScript event, or trigger that event on an element.
+/// &#10;1 - mouseup(handler(eventObject))
+/// &#10;2 - mouseup(eventData, handler(eventObject))
+/// &#10;3 - mouseup()
+/// </summary>
+/// <param name="data" type="Object">
+/// A map of data that will be passed to the event handler.
+/// </param>
+/// <param name="fn" type="Function">
+/// A function to execute each time the event is triggered.
+/// </param>
+/// <returns type="jQuery" />
+
+ if ( fn == null ) {
+ fn = data;
+ data = null;
+ }
+
+ return arguments.length > 0 ?
+ this.bind( name, data, fn ) :
+ this.trigger( name );
+ };
+jQuery.prototype.next = function( until, selector ) {
+/// <summary>
+/// Get the immediately following sibling of each element in the set of matched elements. If a selector is provided, it retrieves the next sibling only if it matches that selector.
+/// </summary>
+/// <param name="until" type="String">
+/// A string containing a selector expression to match elements against.
+/// </param>
+/// <returns type="jQuery" />
+
+ var ret = jQuery.map( this, fn, until ),
+ // The variable 'args' was introduced in
+ // https://github.com/jquery/jquery/commit/52a0238
+ // to work around a bug in Chrome 10 (Dev) and should be removed when the bug is fixed.
+ // http://code.google.com/p/v8/issues/detail?id=1050
+ args = slice.call(arguments);
+
+ if ( !runtil.test( name ) ) {
+ selector = until;
+ }
+
+ if ( selector && typeof selector === "string" ) {
+ ret = jQuery.filter( selector, ret );
+ }
+
+ ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret;
+
+ if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) {
+ ret = ret.reverse();
+ }
+
+ return this.pushStack( ret, name, args.join(",") );
+ };
+jQuery.prototype.nextAll = function( until, selector ) {
+/// <summary>
+/// Get all following siblings of each element in the set of matched elements, optionally filtered by a selector.
+/// </summary>
+/// <param name="until" type="String">
+/// A string containing a selector expression to match elements against.
+/// </param>
+/// <returns type="jQuery" />
+
+ var ret = jQuery.map( this, fn, until ),
+ // The variable 'args' was introduced in
+ // https://github.com/jquery/jquery/commit/52a0238
+ // to work around a bug in Chrome 10 (Dev) and should be removed when the bug is fixed.
+ // http://code.google.com/p/v8/issues/detail?id=1050
+ args = slice.call(arguments);
+
+ if ( !runtil.test( name ) ) {
+ selector = until;
+ }
+
+ if ( selector && typeof selector === "string" ) {
+ ret = jQuery.filter( selector, ret );
+ }
+
+ ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret;
+
+ if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) {
+ ret = ret.reverse();
+ }
+
+ return this.pushStack( ret, name, args.join(",") );
+ };
+jQuery.prototype.nextUntil = function( until, selector ) {
+/// <summary>
+/// Get all following siblings of each element up to but not including the element matched by the selector.
+/// </summary>
+/// <param name="until" type="String">
+/// A string containing a selector expression to indicate where to stop matching following sibling elements.
+/// </param>
+/// <returns type="jQuery" />
+
+ var ret = jQuery.map( this, fn, until ),
+ // The variable 'args' was introduced in
+ // https://github.com/jquery/jquery/commit/52a0238
+ // to work around a bug in Chrome 10 (Dev) and should be removed when the bug is fixed.
+ // http://code.google.com/p/v8/issues/detail?id=1050
+ args = slice.call(arguments);
+
+ if ( !runtil.test( name ) ) {
+ selector = until;
+ }
+
+ if ( selector && typeof selector === "string" ) {
+ ret = jQuery.filter( selector, ret );
+ }
+
+ ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret;
+
+ if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) {
+ ret = ret.reverse();
+ }
+
+ return this.pushStack( ret, name, args.join(",") );
+ };
+jQuery.prototype.not = function( selector ) {
+/// <summary>
+/// Remove elements from the set of matched elements.
+/// &#10;1 - not(selector)
+/// &#10;2 - not(elements)
+/// &#10;3 - not(function(index))
+/// </summary>
+/// <param name="selector" type="String">
+/// A string containing a selector expression to match elements against.
+/// </param>
+/// <returns type="jQuery" />
+
+ return this.pushStack( winnow(this, selector, false), "not", selector);
+ };
+jQuery.prototype.offset = function( options ) {
+/// <summary>
+/// 1: Get the current coordinates of the first element in the set of matched elements, relative to the document.
+/// &#10; 1.1 - offset()
+/// &#10;2: Set the current coordinates of every element in the set of matched elements, relative to the document.
+/// &#10; 2.1 - offset(coordinates)
+/// &#10; 2.2 - offset(function(index, coords))
+/// </summary>
+/// <param name="options" type="Object">
+/// An object containing the properties top and left, which are integers indicating the new top and left coordinates for the elements.
+/// </param>
+/// <returns type="jQuery" />
+
+ var elem = this[0], box;
+
+ if ( options ) {
+ return this.each(function( i ) {
+ jQuery.offset.setOffset( this, options, i );
+ });
+ }
+
+ if ( !elem || !elem.ownerDocument ) {
+ return null;
+ }
+
+ if ( elem === elem.ownerDocument.body ) {
+ return jQuery.offset.bodyOffset( elem );
+ }
+
+ try {
+ box = elem.getBoundingClientRect();
+ } catch(e) {}
+
+ var doc = elem.ownerDocument,
+ docElem = doc.documentElement;
+
+ // Make sure we're not dealing with a disconnected DOM node
+ if ( !box || !jQuery.contains( docElem, elem ) ) {
+ return box ? { top: box.top, left: box.left } : { top: 0, left: 0 };
+ }
+
+ var body = doc.body,
+ win = getWindow(doc),
+ clientTop = docElem.clientTop || body.clientTop || 0,
+ clientLeft = docElem.clientLeft || body.clientLeft || 0,
+ scrollTop = win.pageYOffset || jQuery.support.boxModel && docElem.scrollTop || body.scrollTop,
+ scrollLeft = win.pageXOffset || jQuery.support.boxModel && docElem.scrollLeft || body.scrollLeft,
+ top = box.top + scrollTop - clientTop,
+ left = box.left + scrollLeft - clientLeft;
+
+ return { top: top, left: left };
+ };
+jQuery.prototype.offsetParent = function() {
+/// <summary>
+/// Get the closest ancestor element that is positioned.
+/// </summary>
+/// <returns type="jQuery" />
+
+ return this.map(function() {
+ var offsetParent = this.offsetParent || document.body;
+ while ( offsetParent && (!rroot.test(offsetParent.nodeName) && jQuery.css(offsetParent, "position") === "static") ) {
+ offsetParent = offsetParent.offsetParent;
+ }
+ return offsetParent;
+ });
+ };
+jQuery.prototype.one = function( type, data, fn ) {
+/// <summary>
+/// Attach a handler to an event for the elements. The handler is executed at most once per element.
+/// </summary>
+/// <param name="type" type="String">
+/// A string containing one or more JavaScript event types, such as "click" or "submit," or custom event names.
+/// </param>
+/// <param name="data" type="Object">
+/// A map of data that will be passed to the event handler.
+/// </param>
+/// <param name="fn" type="Function">
+/// A function to execute at the time the event is triggered.
+/// </param>
+/// <returns type="jQuery" />
+
+ // Handle object literals
+ if ( typeof type === "object" ) {
+ for ( var key in type ) {
+ this[ name ](key, data, type[key], fn);
+ }
+ return this;
+ }
+
+ if ( jQuery.isFunction( data ) || data === false ) {
+ fn = data;
+ data = undefined;
+ }
+
+ var handler = name === "one" ? jQuery.proxy( fn, function( event ) {
+ jQuery( this ).unbind( event, handler );
+ return fn.apply( this, arguments );
+ }) : fn;
+
+ if ( type === "unload" && name !== "one" ) {
+ this.one( type, data, fn );
+
+ } else {
+ for ( var i = 0, l = this.length; i < l; i++ ) {
+ jQuery.event.add( this[i], type, handler, data );
+ }
+ }
+
+ return this;
+ };
+jQuery.prototype.outerHeight = function( margin ) {
+/// <summary>
+/// Get the current computed height for the first element in the set of matched elements, including padding, border, and optionally margin.
+/// </summary>
+/// <param name="margin" type="Boolean">
+/// A Boolean indicating whether to include the element's margin in the calculation.
+/// </param>
+/// <returns type="Number" />
+
+ return this[0] ?
+ parseFloat( jQuery.css( this[0], type, margin ? "margin" : "border" ) ) :
+ null;
+ };
+jQuery.prototype.outerWidth = function( margin ) {
+/// <summary>
+/// Get the current computed width for the first element in the set of matched elements, including padding and border.
+/// </summary>
+/// <param name="margin" type="Boolean">
+/// A Boolean indicating whether to include the element's margin in the calculation.
+/// </param>
+/// <returns type="Number" />
+
+ return this[0] ?
+ parseFloat( jQuery.css( this[0], type, margin ? "margin" : "border" ) ) :
+ null;
+ };
+jQuery.prototype.parent = function( until, selector ) {
+/// <summary>
+/// Get the parent of each element in the current set of matched elements, optionally filtered by a selector.
+/// </summary>
+/// <param name="until" type="String">
+/// A string containing a selector expression to match elements against.
+/// </param>
+/// <returns type="jQuery" />
+
+ var ret = jQuery.map( this, fn, until ),
+ // The variable 'args' was introduced in
+ // https://github.com/jquery/jquery/commit/52a0238
+ // to work around a bug in Chrome 10 (Dev) and should be removed when the bug is fixed.
+ // http://code.google.com/p/v8/issues/detail?id=1050
+ args = slice.call(arguments);
+
+ if ( !runtil.test( name ) ) {
+ selector = until;
+ }
+
+ if ( selector && typeof selector === "string" ) {
+ ret = jQuery.filter( selector, ret );
+ }
+
+ ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret;
+
+ if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) {
+ ret = ret.reverse();
+ }
+
+ return this.pushStack( ret, name, args.join(",") );
+ };
+jQuery.prototype.parents = function( until, selector ) {
+/// <summary>
+/// Get the ancestors of each element in the current set of matched elements, optionally filtered by a selector.
+/// </summary>
+/// <param name="until" type="String">
+/// A string containing a selector expression to match elements against.
+/// </param>
+/// <returns type="jQuery" />
+
+ var ret = jQuery.map( this, fn, until ),
+ // The variable 'args' was introduced in
+ // https://github.com/jquery/jquery/commit/52a0238
+ // to work around a bug in Chrome 10 (Dev) and should be removed when the bug is fixed.
+ // http://code.google.com/p/v8/issues/detail?id=1050
+ args = slice.call(arguments);
+
+ if ( !runtil.test( name ) ) {
+ selector = until;
+ }
+
+ if ( selector && typeof selector === "string" ) {
+ ret = jQuery.filter( selector, ret );
+ }
+
+ ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret;
+
+ if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) {
+ ret = ret.reverse();
+ }
+
+ return this.pushStack( ret, name, args.join(",") );
+ };
+jQuery.prototype.parentsUntil = function( until, selector ) {
+/// <summary>
+/// Get the ancestors of each element in the current set of matched elements, up to but not including the element matched by the selector.
+/// </summary>
+/// <param name="until" type="String">
+/// A string containing a selector expression to indicate where to stop matching ancestor elements.
+/// </param>
+/// <returns type="jQuery" />
+
+ var ret = jQuery.map( this, fn, until ),
+ // The variable 'args' was introduced in
+ // https://github.com/jquery/jquery/commit/52a0238
+ // to work around a bug in Chrome 10 (Dev) and should be removed when the bug is fixed.
+ // http://code.google.com/p/v8/issues/detail?id=1050
+ args = slice.call(arguments);
+
+ if ( !runtil.test( name ) ) {
+ selector = until;
+ }
+
+ if ( selector && typeof selector === "string" ) {
+ ret = jQuery.filter( selector, ret );
+ }
+
+ ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret;
+
+ if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) {
+ ret = ret.reverse();
+ }
+
+ return this.pushStack( ret, name, args.join(",") );
+ };
+jQuery.prototype.position = function() {
+/// <summary>
+/// Get the current coordinates of the first element in the set of matched elements, relative to the offset parent.
+/// </summary>
+/// <returns type="Object" />
+
+ if ( !this[0] ) {
+ return null;
+ }
+
+ var elem = this[0],
+
+ // Get *real* offsetParent
+ offsetParent = this.offsetParent(),
+
+ // Get correct offsets
+ offset = this.offset(),
+ parentOffset = rroot.test(offsetParent[0].nodeName) ? { top: 0, left: 0 } : offsetParent.offset();
+
+ // Subtract element margins
+ // note: when an element has margin: auto the offsetLeft and marginLeft
+ // are the same in Safari causing offset.left to incorrectly be 0
+ offset.top -= parseFloat( jQuery.css(elem, "marginTop") ) || 0;
+ offset.left -= parseFloat( jQuery.css(elem, "marginLeft") ) || 0;
+
+ // Add offsetParent borders
+ parentOffset.top += parseFloat( jQuery.css(offsetParent[0], "borderTopWidth") ) || 0;
+ parentOffset.left += parseFloat( jQuery.css(offsetParent[0], "borderLeftWidth") ) || 0;
+
+ // Subtract the two offsets
+ return {
+ top: offset.top - parentOffset.top,
+ left: offset.left - parentOffset.left
+ };
+ };
+jQuery.prototype.prepend = function() {
+/// <summary>
+/// Insert content, specified by the parameter, to the beginning of each element in the set of matched elements.
+/// &#10;1 - prepend(content, content)
+/// &#10;2 - prepend(function(index, html))
+/// </summary>
+/// <param name="" type="jQuery">
+/// DOM element, array of elements, HTML string, or jQuery object to insert at the beginning of each element in the set of matched elements.
+/// </param>
+/// <param name="" type="jQuery">
+/// One or more additional DOM elements, arrays of elements, HTML strings, or jQuery objects to insert at the beginning of each element in the set of matched elements.
+/// </param>
+/// <returns type="jQuery" />
+
+ return this.domManip(arguments, true, function( elem ) {
+ if ( this.nodeType === 1 ) {
+ this.insertBefore( elem, this.firstChild );
+ }
+ });
+ };
+jQuery.prototype.prependTo = function( selector ) {
+/// <summary>
+/// Insert every element in the set of matched elements to the beginning of the target.
+/// </summary>
+/// <param name="selector" type="jQuery">
+/// A selector, element, HTML string, or jQuery object; the matched set of elements will be inserted at the beginning of the element(s) specified by this parameter.
+/// </param>
+/// <returns type="jQuery" />
+
+ var ret = [],
+ insert = jQuery( selector ),
+ parent = this.length === 1 && this[0].parentNode;
+
+ if ( parent && parent.nodeType === 11 && parent.childNodes.length === 1 && insert.length === 1 ) {
+ insert[ original ]( this[0] );
+ return this;
+
+ } else {
+ for ( var i = 0, l = insert.length; i < l; i++ ) {
+ var elems = (i > 0 ? this.clone(true) : this).get();
+ jQuery( insert[i] )[ original ]( elems );
+ ret = ret.concat( elems );
+ }
+
+ return this.pushStack( ret, name, insert.selector );
+ }
+ };
+jQuery.prototype.prev = function( until, selector ) {
+/// <summary>
+/// Get the immediately preceding sibling of each element in the set of matched elements, optionally filtered by a selector.
+/// </summary>
+/// <param name="until" type="String">
+/// A string containing a selector expression to match elements against.
+/// </param>
+/// <returns type="jQuery" />
+
+ var ret = jQuery.map( this, fn, until ),
+ // The variable 'args' was introduced in
+ // https://github.com/jquery/jquery/commit/52a0238
+ // to work around a bug in Chrome 10 (Dev) and should be removed when the bug is fixed.
+ // http://code.google.com/p/v8/issues/detail?id=1050
+ args = slice.call(arguments);
+
+ if ( !runtil.test( name ) ) {
+ selector = until;
+ }
+
+ if ( selector && typeof selector === "string" ) {
+ ret = jQuery.filter( selector, ret );
+ }
+
+ ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret;
+
+ if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) {
+ ret = ret.reverse();
+ }
+
+ return this.pushStack( ret, name, args.join(",") );
+ };
+jQuery.prototype.prevAll = function( until, selector ) {
+/// <summary>
+/// Get all preceding siblings of each element in the set of matched elements, optionally filtered by a selector.
+/// </summary>
+/// <param name="until" type="String">
+/// A string containing a selector expression to match elements against.
+/// </param>
+/// <returns type="jQuery" />
+
+ var ret = jQuery.map( this, fn, until ),
+ // The variable 'args' was introduced in
+ // https://github.com/jquery/jquery/commit/52a0238
+ // to work around a bug in Chrome 10 (Dev) and should be removed when the bug is fixed.
+ // http://code.google.com/p/v8/issues/detail?id=1050
+ args = slice.call(arguments);
+
+ if ( !runtil.test( name ) ) {
+ selector = until;
+ }
+
+ if ( selector && typeof selector === "string" ) {
+ ret = jQuery.filter( selector, ret );
+ }
+
+ ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret;
+
+ if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) {
+ ret = ret.reverse();
+ }
+
+ return this.pushStack( ret, name, args.join(",") );
+ };
+jQuery.prototype.prevUntil = function( until, selector ) {
+/// <summary>
+/// Get all preceding siblings of each element up to but not including the element matched by the selector.
+/// </summary>
+/// <param name="until" type="String">
+/// A string containing a selector expression to indicate where to stop matching preceding sibling elements.
+/// </param>
+/// <returns type="jQuery" />
+
+ var ret = jQuery.map( this, fn, until ),
+ // The variable 'args' was introduced in
+ // https://github.com/jquery/jquery/commit/52a0238
+ // to work around a bug in Chrome 10 (Dev) and should be removed when the bug is fixed.
+ // http://code.google.com/p/v8/issues/detail?id=1050
+ args = slice.call(arguments);
+
+ if ( !runtil.test( name ) ) {
+ selector = until;
+ }
+
+ if ( selector && typeof selector === "string" ) {
+ ret = jQuery.filter( selector, ret );
+ }
+
+ ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret;
+
+ if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) {
+ ret = ret.reverse();
+ }
+
+ return this.pushStack( ret, name, args.join(",") );
+ };
+jQuery.prototype.pushStack = function( elems, name, selector ) {
+/// <summary>
+/// Add a collection of DOM elements onto the jQuery stack.
+/// &#10;1 - pushStack(elements)
+/// &#10;2 - pushStack(elements, name, arguments)
+/// </summary>
+/// <param name="elems" type="Array">
+/// An array of elements to push onto the stack and make into a new jQuery object.
+/// </param>
+/// <param name="name" type="String">
+/// The name of a jQuery method that generated the array of elements.
+/// </param>
+/// <param name="selector" type="Array">
+/// The arguments that were passed in to the jQuery method (for serialization).
+/// </param>
+/// <returns type="jQuery" />
+
+ // Build a new jQuery matched element set
+ var ret = this.constructor();
+
+ if ( jQuery.isArray( elems ) ) {
+ push.apply( ret, elems );
+
+ } else {
+ jQuery.merge( ret, elems );
+ }
+
+ // Add the old object onto the stack (as a reference)
+ ret.prevObject = this;
+
+ ret.context = this.context;
+
+ if ( name === "find" ) {
+ ret.selector = this.selector + (this.selector ? " " : "") + selector;
+ } else if ( name ) {
+ ret.selector = this.selector + "." + name + "(" + selector + ")";
+ }
+
+ // Return the newly-formed element set
+ return ret;
+ };
+jQuery.prototype.queue = function( type, data ) {
+/// <summary>
+/// 1: Show the queue of functions to be executed on the matched elements.
+/// &#10; 1.1 - queue(queueName)
+/// &#10;2: Manipulate the queue of functions to be executed on the matched elements.
+/// &#10; 2.1 - queue(queueName, newQueue)
+/// &#10; 2.2 - queue(queueName, callback( next ))
+/// </summary>
+/// <param name="type" type="String">
+/// A string containing the name of the queue. Defaults to fx, the standard effects queue.
+/// </param>
+/// <param name="data" type="Array">
+/// An array of functions to replace the current queue contents.
+/// </param>
+/// <returns type="jQuery" />
+
+ if ( typeof type !== "string" ) {
+ data = type;
+ type = "fx";
+ }
+
+ if ( data === undefined ) {
+ return jQuery.queue( this[0], type );
+ }
+ return this.each(function( i ) {
+ var queue = jQuery.queue( this, type, data );
+
+ if ( type === "fx" && queue[0] !== "inprogress" ) {
+ jQuery.dequeue( this, type );
+ }
+ });
+ };
+jQuery.prototype.ready = function( fn ) {
+/// <summary>
+/// Specify a function to execute when the DOM is fully loaded.
+/// </summary>
+/// <param name="fn" type="Function">
+/// A function to execute after the DOM is ready.
+/// </param>
+/// <returns type="jQuery" />
+
+ // Attach the listeners
+ jQuery.bindReady();
+
+ // Add the callback
+ readyList.done( fn );
+
+ return this;
+ };
+jQuery.prototype.remove = function( selector, keepData ) {
+/// <summary>
+/// Remove the set of matched elements from the DOM.
+/// </summary>
+/// <param name="selector" type="String">
+/// A selector expression that filters the set of matched elements to be removed.
+/// </param>
+/// <returns type="jQuery" />
+
+ for ( var i = 0, elem; (elem = this[i]) != null; i++ ) {
+ if ( !selector || jQuery.filter( selector, [ elem ] ).length ) {
+ if ( !keepData && elem.nodeType === 1 ) {
+ jQuery.cleanData( elem.getElementsByTagName("*") );
+ jQuery.cleanData( [ elem ] );
+ }
+
+ if ( elem.parentNode ) {
+ elem.parentNode.removeChild( elem );
+ }
+ }
+ }
+
+ return this;
+ };
+jQuery.prototype.removeAttr = function( name, fn ) {
+/// <summary>
+/// Remove an attribute from each element in the set of matched elements.
+/// </summary>
+/// <param name="name" type="String">
+/// An attribute to remove.
+/// </param>
+/// <returns type="jQuery" />
+
+ return this.each(function(){
+ jQuery.attr( this, name, "" );
+ if ( this.nodeType === 1 ) {
+ this.removeAttribute( name );
+ }
+ });
+ };
+jQuery.prototype.removeClass = function( value ) {
+/// <summary>
+/// Remove a single class, multiple classes, or all classes from each element in the set of matched elements.
+/// &#10;1 - removeClass(className)
+/// &#10;2 - removeClass(function(index, class))
+/// </summary>
+/// <param name="value" type="String">
+/// A class name to be removed from the class attribute of each matched element.
+/// </param>
+/// <returns type="jQuery" />
+
+ if ( jQuery.isFunction(value) ) {
+ return this.each(function(i) {
+ var self = jQuery(this);
+ self.removeClass( value.call(this, i, self.attr("class")) );
+ });
+ }
+
+ if ( (value && typeof value === "string") || value === undefined ) {
+ var classNames = (value || "").split( rspaces );
+
+ for ( var i = 0, l = this.length; i < l; i++ ) {
+ var elem = this[i];
+
+ if ( elem.nodeType === 1 && elem.className ) {
+ if ( value ) {
+ var className = (" " + elem.className + " ").replace(rclass, " ");
+ for ( var c = 0, cl = classNames.length; c < cl; c++ ) {
+ className = className.replace(" " + classNames[c] + " ", " ");
+ }
+ elem.className = jQuery.trim( className );
+
+ } else {
+ elem.className = "";
+ }
+ }
+ }
+ }
+
+ return this;
+ };
+jQuery.prototype.removeData = function( key ) {
+/// <summary>
+/// Remove a previously-stored piece of data.
+/// </summary>
+/// <param name="key" type="String">
+/// A string naming the piece of data to delete.
+/// </param>
+/// <returns type="jQuery" />
+
+ return this.each(function() {
+ jQuery.removeData( this, key );
+ });
+ };
+jQuery.prototype.replaceAll = function( selector ) {
+/// <summary>
+/// Replace each target element with the set of matched elements.
+/// </summary>
+/// <param name="selector" type="String">
+/// A selector expression indicating which element(s) to replace.
+/// </param>
+/// <returns type="jQuery" />
+
+ var ret = [],
+ insert = jQuery( selector ),
+ parent = this.length === 1 && this[0].parentNode;
+
+ if ( parent && parent.nodeType === 11 && parent.childNodes.length === 1 && insert.length === 1 ) {
+ insert[ original ]( this[0] );
+ return this;
+
+ } else {
+ for ( var i = 0, l = insert.length; i < l; i++ ) {
+ var elems = (i > 0 ? this.clone(true) : this).get();
+ jQuery( insert[i] )[ original ]( elems );
+ ret = ret.concat( elems );
+ }
+
+ return this.pushStack( ret, name, insert.selector );
+ }
+ };
+jQuery.prototype.replaceWith = function( value ) {
+/// <summary>
+/// Replace each element in the set of matched elements with the provided new content.
+/// &#10;1 - replaceWith(newContent)
+/// &#10;2 - replaceWith(function)
+/// </summary>
+/// <param name="value" type="jQuery">
+/// The content to insert. May be an HTML string, DOM element, or jQuery object.
+/// </param>
+/// <returns type="jQuery" />
+
+ if ( this[0] && this[0].parentNode ) {
+ // Make sure that the elements are removed from the DOM before they are inserted
+ // this can help fix replacing a parent with child elements
+ if ( jQuery.isFunction( value ) ) {
+ return this.each(function(i) {
+ var self = jQuery(this), old = self.html();
+ self.replaceWith( value.call( this, i, old ) );
+ });
+ }
+
+ if ( typeof value !== "string" ) {
+ value = jQuery( value ).detach();
+ }
+
+ return this.each(function() {
+ var next = this.nextSibling,
+ parent = this.parentNode;
+
+ jQuery( this ).remove();
+
+ if ( next ) {
+ jQuery(next).before( value );
+ } else {
+ jQuery(parent).append( value );
+ }
+ });
+ } else {
+ return this.length ?
+ this.pushStack( jQuery(jQuery.isFunction(value) ? value() : value), "replaceWith", value ) :
+ this;
+ }
+ };
+jQuery.prototype.resize = function( data, fn ) {
+/// <summary>
+/// Bind an event handler to the "resize" JavaScript event, or trigger that event on an element.
+/// &#10;1 - resize(handler(eventObject))
+/// &#10;2 - resize(eventData, handler(eventObject))
+/// &#10;3 - resize()
+/// </summary>
+/// <param name="data" type="Object">
+/// A map of data that will be passed to the event handler.
+/// </param>
+/// <param name="fn" type="Function">
+/// A function to execute each time the event is triggered.
+/// </param>
+/// <returns type="jQuery" />
+
+ if ( fn == null ) {
+ fn = data;
+ data = null;
+ }
+
+ return arguments.length > 0 ?
+ this.bind( name, data, fn ) :
+ this.trigger( name );
+ };
+jQuery.prototype.scroll = function( data, fn ) {
+/// <summary>
+/// Bind an event handler to the "scroll" JavaScript event, or trigger that event on an element.
+/// &#10;1 - scroll(handler(eventObject))
+/// &#10;2 - scroll(eventData, handler(eventObject))
+/// &#10;3 - scroll()
+/// </summary>
+/// <param name="data" type="Object">
+/// A map of data that will be passed to the event handler.
+/// </param>
+/// <param name="fn" type="Function">
+/// A function to execute each time the event is triggered.
+/// </param>
+/// <returns type="jQuery" />
+
+ if ( fn == null ) {
+ fn = data;
+ data = null;
+ }
+
+ return arguments.length > 0 ?
+ this.bind( name, data, fn ) :
+ this.trigger( name );
+ };
+jQuery.prototype.scrollLeft = function(val) {
+/// <summary>
+/// 1: Get the current horizontal position of the scroll bar for the first element in the set of matched elements.
+/// &#10; 1.1 - scrollLeft()
+/// &#10;2: Set the current horizontal position of the scroll bar for each of the set of matched elements.
+/// &#10; 2.1 - scrollLeft(value)
+/// </summary>
+/// <param name="val" type="Number">
+/// An integer indicating the new position to set the scroll bar to.
+/// </param>
+/// <returns type="jQuery" />
+
+ var elem = this[0], win;
+
+ if ( !elem ) {
+ return null;
+ }
+
+ if ( val !== undefined ) {
+ // Set the scroll offset
+ return this.each(function() {
+ win = getWindow( this );
+
+ if ( win ) {
+ win.scrollTo(
+ !i ? val : jQuery(win).scrollLeft(),
+ i ? val : jQuery(win).scrollTop()
+ );
+
+ } else {
+ this[ method ] = val;
+ }
+ });
+ } else {
+ win = getWindow( elem );
+
+ // Return the scroll offset
+ return win ? ("pageXOffset" in win) ? win[ i ? "pageYOffset" : "pageXOffset" ] :
+ jQuery.support.boxModel && win.document.documentElement[ method ] ||
+ win.document.body[ method ] :
+ elem[ method ];
+ }
+ };
+jQuery.prototype.scrollTop = function(val) {
+/// <summary>
+/// 1: Get the current vertical position of the scroll bar for the first element in the set of matched elements.
+/// &#10; 1.1 - scrollTop()
+/// &#10;2: Set the current vertical position of the scroll bar for each of the set of matched elements.
+/// &#10; 2.1 - scrollTop(value)
+/// </summary>
+/// <param name="val" type="Number">
+/// An integer indicating the new position to set the scroll bar to.
+/// </param>
+/// <returns type="jQuery" />
+
+ var elem = this[0], win;
+
+ if ( !elem ) {
+ return null;
+ }
+
+ if ( val !== undefined ) {
+ // Set the scroll offset
+ return this.each(function() {
+ win = getWindow( this );
+
+ if ( win ) {
+ win.scrollTo(
+ !i ? val : jQuery(win).scrollLeft(),
+ i ? val : jQuery(win).scrollTop()
+ );
+
+ } else {
+ this[ method ] = val;
+ }
+ });
+ } else {
+ win = getWindow( elem );
+
+ // Return the scroll offset
+ return win ? ("pageXOffset" in win) ? win[ i ? "pageYOffset" : "pageXOffset" ] :
+ jQuery.support.boxModel && win.document.documentElement[ method ] ||
+ win.document.body[ method ] :
+ elem[ method ];
+ }
+ };
+jQuery.prototype.select = function( data, fn ) {
+/// <summary>
+/// Bind an event handler to the "select" JavaScript event, or trigger that event on an element.
+/// &#10;1 - select(handler(eventObject))
+/// &#10;2 - select(eventData, handler(eventObject))
+/// &#10;3 - select()
+/// </summary>
+/// <param name="data" type="Object">
+/// A map of data that will be passed to the event handler.
+/// </param>
+/// <param name="fn" type="Function">
+/// A function to execute each time the event is triggered.
+/// </param>
+/// <returns type="jQuery" />
+
+ if ( fn == null ) {
+ fn = data;
+ data = null;
+ }
+
+ return arguments.length > 0 ?
+ this.bind( name, data, fn ) :
+ this.trigger( name );
+ };
+jQuery.prototype.serialize = function() {
+/// <summary>
+/// Encode a set of form elements as a string for submission.
+/// </summary>
+/// <returns type="String" />
+
+ return jQuery.param( this.serializeArray() );
+ };
+jQuery.prototype.serializeArray = function() {
+/// <summary>
+/// Encode a set of form elements as an array of names and values.
+/// </summary>
+/// <returns type="Array" />
+
+ return this.map(function(){
+ return this.elements ? jQuery.makeArray( this.elements ) : this;
+ })
+ .filter(function(){
+ return this.name && !this.disabled &&
+ ( this.checked || rselectTextarea.test( this.nodeName ) ||
+ rinput.test( this.type ) );
+ })
+ .map(function( i, elem ){
+ var val = jQuery( this ).val();
+
+ return val == null ?
+ null :
+ jQuery.isArray( val ) ?
+ jQuery.map( val, function( val, i ){
+ return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
+ }) :
+ { name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
+ }).get();
+ };
+jQuery.prototype.show = function( speed, easing, callback ) {
+/// <summary>
+/// Display the matched elements.
+/// &#10;1 - show()
+/// &#10;2 - show(duration, callback)
+/// &#10;3 - show(duration, easing, callback)
+/// </summary>
+/// <param name="speed" type="Number">
+/// A string or number determining how long the animation will run.
+/// </param>
+/// <param name="easing" type="String">
+/// A string indicating which easing function to use for the transition.
+/// </param>
+/// <param name="callback" type="Function">
+/// A function to call once the animation is complete.
+/// </param>
+/// <returns type="jQuery" />
+
+ var elem, display;
+
+ if ( speed || speed === 0 ) {
+ return this.animate( genFx("show", 3), speed, easing, callback);
+
+ } else {
+ for ( var i = 0, j = this.length; i < j; i++ ) {
+ elem = this[i];
+ display = elem.style.display;
+
+ // Reset the inline display of this element to learn if it is
+ // being hidden by cascaded rules or not
+ if ( !jQuery._data(elem, "olddisplay") && display === "none" ) {
+ display = elem.style.display = "";
+ }
+
+ // Set elements which have been overridden with display: none
+ // in a stylesheet to whatever the default browser style is
+ // for such an element
+ if ( display === "" && jQuery.css( elem, "display" ) === "none" ) {
+ jQuery._data(elem, "olddisplay", defaultDisplay(elem.nodeName));
+ }
+ }
+
+ // Set the display of most of the elements in a second loop
+ // to avoid the constant reflow
+ for ( i = 0; i < j; i++ ) {
+ elem = this[i];
+ display = elem.style.display;
+
+ if ( display === "" || display === "none" ) {
+ elem.style.display = jQuery._data(elem, "olddisplay") || "";
+ }
+ }
+
+ return this;
+ }
+ };
+jQuery.prototype.siblings = function( until, selector ) {
+/// <summary>
+/// Get the siblings of each element in the set of matched elements, optionally filtered by a selector.
+/// </summary>
+/// <param name="until" type="String">
+/// A string containing a selector expression to match elements against.
+/// </param>
+/// <returns type="jQuery" />
+
+ var ret = jQuery.map( this, fn, until ),
+ // The variable 'args' was introduced in
+ // https://github.com/jquery/jquery/commit/52a0238
+ // to work around a bug in Chrome 10 (Dev) and should be removed when the bug is fixed.
+ // http://code.google.com/p/v8/issues/detail?id=1050
+ args = slice.call(arguments);
+
+ if ( !runtil.test( name ) ) {
+ selector = until;
+ }
+
+ if ( selector && typeof selector === "string" ) {
+ ret = jQuery.filter( selector, ret );
+ }
+
+ ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret;
+
+ if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) {
+ ret = ret.reverse();
+ }
+
+ return this.pushStack( ret, name, args.join(",") );
+ };
+jQuery.prototype.size = function() {
+/// <summary>
+/// Return the number of elements in the jQuery object.
+/// </summary>
+/// <returns type="Number" />
+
+ return this.length;
+ };
+jQuery.prototype.slice = function() {
+/// <summary>
+/// Reduce the set of matched elements to a subset specified by a range of indices.
+/// </summary>
+/// <param name="" type="Number">
+/// An integer indicating the 0-based position at which the elements begin to be selected. If negative, it indicates an offset from the end of the set.
+/// </param>
+/// <param name="" type="Number">
+/// An integer indicating the 0-based position at which the elements stop being selected. If negative, it indicates an offset from the end of the set. If omitted, the range continues until the end of the set.
+/// </param>
+/// <returns type="jQuery" />
+
+ return this.pushStack( slice.apply( this, arguments ),
+ "slice", slice.call(arguments).join(",") );
+ };
+jQuery.prototype.slideDown = function( speed, easing, callback ) {
+/// <summary>
+/// Display the matched elements with a sliding motion.
+/// &#10;1 - slideDown(duration, callback)
+/// &#10;2 - slideDown(duration, easing, callback)
+/// </summary>
+/// <param name="speed" type="Number">
+/// A string or number determining how long the animation will run.
+/// </param>
+/// <param name="easing" type="String">
+/// A string indicating which easing function to use for the transition.
+/// </param>
+/// <param name="callback" type="Function">
+/// A function to call once the animation is complete.
+/// </param>
+/// <returns type="jQuery" />
+
+ return this.animate( props, speed, easing, callback );
+ };
+jQuery.prototype.slideToggle = function( speed, easing, callback ) {
+/// <summary>
+/// Display or hide the matched elements with a sliding motion.
+/// &#10;1 - slideToggle(duration, callback)
+/// &#10;2 - slideToggle(duration, easing, callback)
+/// </summary>
+/// <param name="speed" type="Number">
+/// A string or number determining how long the animation will run.
+/// </param>
+/// <param name="easing" type="String">
+/// A string indicating which easing function to use for the transition.
+/// </param>
+/// <param name="callback" type="Function">
+/// A function to call once the animation is complete.
+/// </param>
+/// <returns type="jQuery" />
+
+ return this.animate( props, speed, easing, callback );
+ };
+jQuery.prototype.slideUp = function( speed, easing, callback ) {
+/// <summary>
+/// Hide the matched elements with a sliding motion.
+/// &#10;1 - slideUp(duration, callback)
+/// &#10;2 - slideUp(duration, easing, callback)
+/// </summary>
+/// <param name="speed" type="Number">
+/// A string or number determining how long the animation will run.
+/// </param>
+/// <param name="easing" type="String">
+/// A string indicating which easing function to use for the transition.
+/// </param>
+/// <param name="callback" type="Function">
+/// A function to call once the animation is complete.
+/// </param>
+/// <returns type="jQuery" />
+
+ return this.animate( props, speed, easing, callback );
+ };
+jQuery.prototype.stop = function( clearQueue, gotoEnd ) {
+/// <summary>
+/// Stop the currently-running animation on the matched elements.
+/// </summary>
+/// <param name="clearQueue" type="Boolean">
+/// A Boolean indicating whether to remove queued animation as well. Defaults to false.
+/// </param>
+/// <param name="gotoEnd" type="Boolean">
+/// A Boolean indicating whether to complete the current animation immediately. Defaults to false.
+/// </param>
+/// <returns type="jQuery" />
+
+ var timers = jQuery.timers;
+
+ if ( clearQueue ) {
+ this.queue([]);
+ }
+
+ this.each(function() {
+ // go in reverse order so anything added to the queue during the loop is ignored
+ for ( var i = timers.length - 1; i >= 0; i-- ) {
+ if ( timers[i].elem === this ) {
+ if (gotoEnd) {
+ // force the next step to be the last
+ timers[i](true);
+ }
+
+ timers.splice(i, 1);
+ }
+ }
+ });
+
+ // start the next in the queue if the last step wasn't forced
+ if ( !gotoEnd ) {
+ this.dequeue();
+ }
+
+ return this;
+ };
+jQuery.prototype.submit = function( data, fn ) {
+/// <summary>
+/// Bind an event handler to the "submit" JavaScript event, or trigger that event on an element.
+/// &#10;1 - submit(handler(eventObject))
+/// &#10;2 - submit(eventData, handler(eventObject))
+/// &#10;3 - submit()
+/// </summary>
+/// <param name="data" type="Object">
+/// A map of data that will be passed to the event handler.
+/// </param>
+/// <param name="fn" type="Function">
+/// A function to execute each time the event is triggered.
+/// </param>
+/// <returns type="jQuery" />
+
+ if ( fn == null ) {
+ fn = data;
+ data = null;
+ }
+
+ return arguments.length > 0 ?
+ this.bind( name, data, fn ) :
+ this.trigger( name );
+ };
+jQuery.prototype.text = function( text ) {
+/// <summary>
+/// 1: Get the combined text contents of each element in the set of matched elements, including their descendants.
+/// &#10; 1.1 - text()
+/// &#10;2: Set the content of each element in the set of matched elements to the specified text.
+/// &#10; 2.1 - text(textString)
+/// &#10; 2.2 - text(function(index, text))
+/// </summary>
+/// <param name="text" type="String">
+/// A string of text to set as the content of each matched element.
+/// </param>
+/// <returns type="jQuery" />
+
+ if ( jQuery.isFunction(text) ) {
+ return this.each(function(i) {
+ var self = jQuery( this );
+
+ self.text( text.call(this, i, self.text()) );
+ });
+ }
+
+ if ( typeof text !== "object" && text !== undefined ) {
+ return this.empty().append( (this[0] && this[0].ownerDocument || document).createTextNode( text ) );
+ }
+
+ return jQuery.text( this );
+ };
+jQuery.prototype.toArray = function() {
+/// <summary>
+/// Retrieve all the DOM elements contained in the jQuery set, as an array.
+/// </summary>
+/// <returns type="Array" />
+
+ return slice.call( this, 0 );
+ };
+jQuery.prototype.toggle = function( fn, fn2, callback ) {
+/// <summary>
+/// 1: Bind two or more handlers to the matched elements, to be executed on alternate clicks.
+/// &#10; 1.1 - toggle(handler(eventObject), handler(eventObject), handler(eventObject))
+/// &#10;2: Display or hide the matched elements.
+/// &#10; 2.1 - toggle(duration, callback)
+/// &#10; 2.2 - toggle(duration, easing, callback)
+/// &#10; 2.3 - toggle(showOrHide)
+/// </summary>
+/// <param name="fn" type="Function">
+/// A function to execute every even time the element is clicked.
+/// </param>
+/// <param name="fn2" type="Function">
+/// A function to execute every odd time the element is clicked.
+/// </param>
+/// <param name="callback" type="Function">
+/// Additional handlers to cycle through after clicks.
+/// </param>
+/// <returns type="jQuery" />
+
+ var bool = typeof fn === "boolean";
+
+ if ( jQuery.isFunction(fn) && jQuery.isFunction(fn2) ) {
+ this._toggle.apply( this, arguments );
+
+ } else if ( fn == null || bool ) {
+ this.each(function() {
+ var state = bool ? fn : jQuery(this).is(":hidden");
+ jQuery(this)[ state ? "show" : "hide" ]();
+ });
+
+ } else {
+ this.animate(genFx("toggle", 3), fn, fn2, callback);
+ }
+
+ return this;
+ };
+jQuery.prototype.toggleClass = function( value, stateVal ) {
+/// <summary>
+/// Add or remove one or more classes from each element in the set of matched elements, depending on either the class's presence or the value of the switch argument.
+/// &#10;1 - toggleClass(className)
+/// &#10;2 - toggleClass(className, switch)
+/// &#10;3 - toggleClass(function(index, class), switch)
+/// </summary>
+/// <param name="value" type="String">
+/// One or more class names (separated by spaces) to be toggled for each element in the matched set.
+/// </param>
+/// <param name="stateVal" type="Boolean">
+/// A boolean value to determine whether the class should be added or removed.
+/// </param>
+/// <returns type="jQuery" />
+
+ var type = typeof value,
+ isBool = typeof stateVal === "boolean";
+
+ if ( jQuery.isFunction( value ) ) {
+ return this.each(function(i) {
+ var self = jQuery(this);
+ self.toggleClass( value.call(this, i, self.attr("class"), stateVal), stateVal );
+ });
+ }
+
+ return this.each(function() {
+ if ( type === "string" ) {
+ // toggle individual class names
+ var className,
+ i = 0,
+ self = jQuery( this ),
+ state = stateVal,
+ classNames = value.split( rspaces );
+
+ while ( (className = classNames[ i++ ]) ) {
+ // check each className given, space seperated list
+ state = isBool ? state : !self.hasClass( className );
+ self[ state ? "addClass" : "removeClass" ]( className );
+ }
+
+ } else if ( type === "undefined" || type === "boolean" ) {
+ if ( this.className ) {
+ // store className if set
+ jQuery._data( this, "__className__", this.className );
+ }
+
+ // toggle whole className
+ this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || "";
+ }
+ });
+ };
+jQuery.prototype.trigger = function( type, data ) {
+/// <summary>
+/// Execute all handlers and behaviors attached to the matched elements for the given event type.
+/// &#10;1 - trigger(eventType, extraParameters)
+/// &#10;2 - trigger(event)
+/// </summary>
+/// <param name="type" type="String">
+/// A string containing a JavaScript event type, such as click or submit.
+/// </param>
+/// <param name="data" type="Array">
+/// An array of additional parameters to pass along to the event handler.
+/// </param>
+/// <returns type="jQuery" />
+
+ return this.each(function() {
+ jQuery.event.trigger( type, data, this );
+ });
+ };
+jQuery.prototype.triggerHandler = function( type, data ) {
+/// <summary>
+/// Execute all handlers attached to an element for an event.
+/// </summary>
+/// <param name="type" type="String">
+/// A string containing a JavaScript event type, such as click or submit.
+/// </param>
+/// <param name="data" type="Array">
+/// An array of additional parameters to pass along to the event handler.
+/// </param>
+/// <returns type="Object" />
+
+ if ( this[0] ) {
+ var event = jQuery.Event( type );
+ event.preventDefault();
+ event.stopPropagation();
+ jQuery.event.trigger( event, data, this[0] );
+ return event.result;
+ }
+ };
+jQuery.prototype.unbind = function( type, fn ) {
+/// <summary>
+/// Remove a previously-attached event handler from the elements.
+/// &#10;1 - unbind(eventType, handler(eventObject))
+/// &#10;2 - unbind(eventType, false)
+/// &#10;3 - unbind(event)
+/// </summary>
+/// <param name="type" type="String">
+/// A string containing a JavaScript event type, such as click or submit.
+/// </param>
+/// <param name="fn" type="Function">
+/// The function that is to be no longer executed.
+/// </param>
+/// <returns type="jQuery" />
+
+ // Handle object literals
+ if ( typeof type === "object" && !type.preventDefault ) {
+ for ( var key in type ) {
+ this.unbind(key, type[key]);
+ }
+
+ } else {
+ for ( var i = 0, l = this.length; i < l; i++ ) {
+ jQuery.event.remove( this[i], type, fn );
+ }
+ }
+
+ return this;
+ };
+jQuery.prototype.undelegate = function( selector, types, fn ) {
+/// <summary>
+/// Remove a handler from the event for all elements which match the current selector, now or in the future, based upon a specific set of root elements.
+/// &#10;1 - undelegate()
+/// &#10;2 - undelegate(selector, eventType)
+/// &#10;3 - undelegate(selector, eventType, handler)
+/// &#10;4 - undelegate(selector, events)
+/// </summary>
+/// <param name="selector" type="String">
+/// A selector which will be used to filter the event results.
+/// </param>
+/// <param name="types" type="String">
+/// A string containing a JavaScript event type, such as "click" or "keydown"
+/// </param>
+/// <param name="fn" type="Function">
+/// A function to execute at the time the event is triggered.
+/// </param>
+/// <returns type="jQuery" />
+
+ if ( arguments.length === 0 ) {
+ return this.unbind( "live" );
+
+ } else {
+ return this.die( types, null, fn, selector );
+ }
+ };
+jQuery.prototype.unload = function( data, fn ) {
+/// <summary>
+/// Bind an event handler to the "unload" JavaScript event.
+/// &#10;1 - unload(handler(eventObject))
+/// &#10;2 - unload(eventData, handler(eventObject))
+/// </summary>
+/// <param name="data" type="Object">
+/// A map of data that will be passed to the event handler.
+/// </param>
+/// <param name="fn" type="Function">
+/// A function to execute each time the event is triggered.
+/// </param>
+/// <returns type="jQuery" />
+
+ if ( fn == null ) {
+ fn = data;
+ data = null;
+ }
+
+ return arguments.length > 0 ?
+ this.bind( name, data, fn ) :
+ this.trigger( name );
+ };
+jQuery.prototype.unwrap = function() {
+/// <summary>
+/// Remove the parents of the set of matched elements from the DOM, leaving the matched elements in their place.
+/// </summary>
+/// <returns type="jQuery" />
+
+ return this.parent().each(function() {
+ if ( !jQuery.nodeName( this, "body" ) ) {
+ jQuery( this ).replaceWith( this.childNodes );
+ }
+ }).end();
+ };
+jQuery.prototype.val = function( value ) {
+/// <summary>
+/// 1: Get the current value of the first element in the set of matched elements.
+/// &#10; 1.1 - val()
+/// &#10;2: Set the value of each element in the set of matched elements.
+/// &#10; 2.1 - val(value)
+/// &#10; 2.2 - val(function(index, value))
+/// </summary>
+/// <param name="value" type="String">
+/// A string of text or an array of strings corresponding to the value of each matched element to set as selected/checked.
+/// </param>
+/// <returns type="jQuery" />
+
+ if ( !arguments.length ) {
+ var elem = this[0];
+
+ if ( elem ) {
+ if ( jQuery.nodeName( elem, "option" ) ) {
+ // attributes.value is undefined in Blackberry 4.7 but
+ // uses .value. See #6932
+ var val = elem.attributes.value;
+ return !val || val.specified ? elem.value : elem.text;
+ }
+
+ // We need to handle select boxes special
+ if ( jQuery.nodeName( elem, "select" ) ) {
+ var index = elem.selectedIndex,
+ values = [],
+ options = elem.options,
+ one = elem.type === "select-one";
+
+ // Nothing was selected
+ if ( index < 0 ) {
+ return null;
+ }
+
+ // Loop through all the selected options
+ for ( var i = one ? index : 0, max = one ? index + 1 : options.length; i < max; i++ ) {
+ var option = options[ i ];
+
+ // Don't return options that are disabled or in a disabled optgroup
+ if ( option.selected && (jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null) &&
+ (!option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" )) ) {
+
+ // Get the specific value for the option
+ value = jQuery(option).val();
+
+ // We don't need an array for one selects
+ if ( one ) {
+ return value;
+ }
+
+ // Multi-Selects return an array
+ values.push( value );
+ }
+ }
+
+ // Fixes Bug #2551 -- select.val() broken in IE after form.reset()
+ if ( one && !values.length && options.length ) {
+ return jQuery( options[ index ] ).val();
+ }
+
+ return values;
+ }
+
+ // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified
+ if ( rradiocheck.test( elem.type ) && !jQuery.support.checkOn ) {
+ return elem.getAttribute("value") === null ? "on" : elem.value;
+ }
+
+ // Everything else, we just grab the value
+ return (elem.value || "").replace(rreturn, "");
+
+ }
+
+ return undefined;
+ }
+
+ var isFunction = jQuery.isFunction(value);
+
+ return this.each(function(i) {
+ var self = jQuery(this), val = value;
+
+ if ( this.nodeType !== 1 ) {
+ return;
+ }
+
+ if ( isFunction ) {
+ val = value.call(this, i, self.val());
+ }
+
+ // Treat null/undefined as ""; convert numbers to string
+ if ( val == null ) {
+ val = "";
+ } else if ( typeof val === "number" ) {
+ val += "";
+ } else if ( jQuery.isArray(val) ) {
+ val = jQuery.map(val, function (value) {
+ return value == null ? "" : value + "";
+ });
+ }
+
+ if ( jQuery.isArray(val) && rradiocheck.test( this.type ) ) {
+ this.checked = jQuery.inArray( self.val(), val ) >= 0;
+
+ } else if ( jQuery.nodeName( this, "select" ) ) {
+ var values = jQuery.makeArray(val);
+
+ jQuery( "option", this ).each(function() {
+ this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0;
+ });
+
+ if ( !values.length ) {
+ this.selectedIndex = -1;
+ }
+
+ } else {
+ this.value = val;
+ }
+ });
+ };
+jQuery.prototype.width = function( size ) {
+/// <summary>
+/// 1: Get the current computed width for the first element in the set of matched elements.
+/// &#10; 1.1 - width()
+/// &#10;2: Set the CSS width of each element in the set of matched elements.
+/// &#10; 2.1 - width(value)
+/// &#10; 2.2 - width(function(index, width))
+/// </summary>
+/// <param name="size" type="Number">
+/// An integer representing the number of pixels, or an integer along with an optional unit of measure appended (as a string).
+/// </param>
+/// <returns type="jQuery" />
+
+ // Get window width or height
+ var elem = this[0];
+ if ( !elem ) {
+ return size == null ? null : this;
+ }
+
+ if ( jQuery.isFunction( size ) ) {
+ return this.each(function( i ) {
+ var self = jQuery( this );
+ self[ type ]( size.call( this, i, self[ type ]() ) );
+ });
+ }
+
+ if ( jQuery.isWindow( elem ) ) {
+ // Everyone else use document.documentElement or document.body depending on Quirks vs Standards mode
+ // 3rd condition allows Nokia support, as it supports the docElem prop but not CSS1Compat
+ var docElemProp = elem.document.documentElement[ "client" + name ];
+ return elem.document.compatMode === "CSS1Compat" && docElemProp ||
+ elem.document.body[ "client" + name ] || docElemProp;
+
+ // Get document width or height
+ } else if ( elem.nodeType === 9 ) {
+ // Either scroll[Width/Height] or offset[Width/Height], whichever is greater
+ return Math.max(
+ elem.documentElement["client" + name],
+ elem.body["scroll" + name], elem.documentElement["scroll" + name],
+ elem.body["offset" + name], elem.documentElement["offset" + name]
+ );
+
+ // Get or set width or height on the element
+ } else if ( size === undefined ) {
+ var orig = jQuery.css( elem, type ),
+ ret = parseFloat( orig );
+
+ return jQuery.isNaN( ret ) ? orig : ret;
+
+ // Set the width or height on the element (default to pixels if value is unitless)
+ } else {
+ return this.css( type, typeof size === "string" ? size : size + "px" );
+ }
+ };
+jQuery.prototype.wrap = function( html ) {
+/// <summary>
+/// Wrap an HTML structure around each element in the set of matched elements.
+/// &#10;1 - wrap(wrappingElement)
+/// &#10;2 - wrap(function(index))
+/// </summary>
+/// <param name="html" type="jQuery">
+/// An HTML snippet, selector expression, jQuery object, or DOM element specifying the structure to wrap around the matched elements.
+/// </param>
+/// <returns type="jQuery" />
+
+ return this.each(function() {
+ jQuery( this ).wrapAll( html );
+ });
+ };
+jQuery.prototype.wrapAll = function( html ) {
+/// <summary>
+/// Wrap an HTML structure around all elements in the set of matched elements.
+/// </summary>
+/// <param name="html" type="jQuery">
+/// An HTML snippet, selector expression, jQuery object, or DOM element specifying the structure to wrap around the matched elements.
+/// </param>
+/// <returns type="jQuery" />
+
+ if ( jQuery.isFunction( html ) ) {
+ return this.each(function(i) {
+ jQuery(this).wrapAll( html.call(this, i) );
+ });
+ }
+
+ if ( this[0] ) {
+ // The elements to wrap the target around
+ var wrap = jQuery( html, this[0].ownerDocument ).eq(0).clone(true);
+
+ if ( this[0].parentNode ) {
+ wrap.insertBefore( this[0] );
+ }
+
+ wrap.map(function() {
+ var elem = this;
+
+ while ( elem.firstChild && elem.firstChild.nodeType === 1 ) {
+ elem = elem.firstChild;
+ }
+
+ return elem;
+ }).append(this);
+ }
+
+ return this;
+ };
+jQuery.prototype.wrapInner = function( html ) {
+/// <summary>
+/// Wrap an HTML structure around the content of each element in the set of matched elements.
+/// &#10;1 - wrapInner(wrappingElement)
+/// &#10;2 - wrapInner(wrappingFunction)
+/// </summary>
+/// <param name="html" type="String">
+/// An HTML snippet, selector expression, jQuery object, or DOM element specifying the structure to wrap around the content of the matched elements.
+/// </param>
+/// <returns type="jQuery" />
+
+ if ( jQuery.isFunction( html ) ) {
+ return this.each(function(i) {
+ jQuery(this).wrapInner( html.call(this, i) );
+ });
+ }
+
+ return this.each(function() {
+ var self = jQuery( this ),
+ contents = self.contents();
+
+ if ( contents.length ) {
+ contents.wrapAll( html );
+
+ } else {
+ self.append( html );
+ }
+ });
+ };
+jQuery.fn = jQuery.prototype;
+jQuery.fn.init.prototype = jQuery.fn;
+window.jQuery = window.$ = jQuery;
+})(window); \ No newline at end of file
diff --git a/src/SPA/upshot/IntelliSense/knockout-2.0.0.debug.js b/src/SPA/upshot/IntelliSense/knockout-2.0.0.debug.js
new file mode 100644
index 00000000..a3f9c6ad
--- /dev/null
+++ b/src/SPA/upshot/IntelliSense/knockout-2.0.0.debug.js
@@ -0,0 +1,3223 @@
+// Knockout JavaScript library v2.0.0
+// (c) Steven Sanderson - http://knockoutjs.com/
+// License: MIT (http://www.opensource.org/licenses/mit-license.php)
+
+(function(window,undefined){
+var ko = window["ko"] = {};
+// Google Closure Compiler helpers (used only to make the minified file smaller)
+ko.exportSymbol = function(publicPath, object) {
+ var tokens = publicPath.split(".");
+ var target = window;
+ for (var i = 0; i < tokens.length - 1; i++)
+ target = target[tokens[i]];
+ target[tokens[tokens.length - 1]] = object;
+};
+ko.exportProperty = function(owner, publicName, object) {
+ owner[publicName] = object;
+};
+ko.utils = new (function () {
+ var stringTrimRegex = /^(\s|\u00A0)+|(\s|\u00A0)+$/g;
+
+ // Represent the known event types in a compact way, then at runtime transform it into a hash with event name as key (for fast lookup)
+ var knownEvents = {}, knownEventTypesByEventName = {};
+ var keyEventTypeName = /Firefox\/2/i.test(navigator.userAgent) ? 'KeyboardEvent' : 'UIEvents';
+ knownEvents[keyEventTypeName] = ['keyup', 'keydown', 'keypress'];
+ knownEvents['MouseEvents'] = ['click', 'dblclick', 'mousedown', 'mouseup', 'mousemove', 'mouseover', 'mouseout', 'mouseenter', 'mouseleave'];
+ for (var eventType in knownEvents) {
+ var knownEventsForType = knownEvents[eventType];
+ if (knownEventsForType.length) {
+ for (var i = 0, j = knownEventsForType.length; i < j; i++)
+ knownEventTypesByEventName[knownEventsForType[i]] = eventType;
+ }
+ }
+
+ // Detect IE versions for bug workarounds (uses IE conditionals, not UA string, for robustness)
+ var ieVersion = (function() {
+ var version = 3, div = document.createElement('div'), iElems = div.getElementsByTagName('i');
+
+ // Keep constructing conditional HTML blocks until we hit one that resolves to an empty fragment
+ while (
+ div.innerHTML = '<!--[if gt IE ' + (++version) + ']><i></i><![endif]-->',
+ iElems[0]
+ );
+ return version > 4 ? version : undefined;
+ }());
+ var isIe6 = ieVersion === 6,
+ isIe7 = ieVersion === 7;
+
+ function isClickOnCheckableElement(element, eventType) {
+ if ((element.tagName != "INPUT") || !element.type) return false;
+ if (eventType.toLowerCase() != "click") return false;
+ var inputType = element.type.toLowerCase();
+ return (inputType == "checkbox") || (inputType == "radio");
+ }
+
+ return {
+ fieldsIncludedWithJsonPost: ['authenticity_token', /^__RequestVerificationToken(_.*)?$/],
+
+ arrayForEach: function (array, action) {
+ for (var i = 0, j = array.length; i < j; i++)
+ action(array[i]);
+ },
+
+ arrayIndexOf: function (array, item) {
+ if (typeof Array.prototype.indexOf == "function")
+ return Array.prototype.indexOf.call(array, item);
+ for (var i = 0, j = array.length; i < j; i++)
+ if (array[i] === item)
+ return i;
+ return -1;
+ },
+
+ arrayFirst: function (array, predicate, predicateOwner) {
+ for (var i = 0, j = array.length; i < j; i++)
+ if (predicate.call(predicateOwner, array[i]))
+ return array[i];
+ return null;
+ },
+
+ arrayRemoveItem: function (array, itemToRemove) {
+ var index = ko.utils.arrayIndexOf(array, itemToRemove);
+ if (index >= 0)
+ array.splice(index, 1);
+ },
+
+ arrayGetDistinctValues: function (array) {
+ array = array || [];
+ var result = [];
+ for (var i = 0, j = array.length; i < j; i++) {
+ if (ko.utils.arrayIndexOf(result, array[i]) < 0)
+ result.push(array[i]);
+ }
+ return result;
+ },
+
+ arrayMap: function (array, mapping) {
+ array = array || [];
+ var result = [];
+ for (var i = 0, j = array.length; i < j; i++)
+ result.push(mapping(array[i]));
+ return result;
+ },
+
+ arrayFilter: function (array, predicate) {
+ array = array || [];
+ var result = [];
+ for (var i = 0, j = array.length; i < j; i++)
+ if (predicate(array[i]))
+ result.push(array[i]);
+ return result;
+ },
+
+ arrayPushAll: function (array, valuesToPush) {
+ for (var i = 0, j = valuesToPush.length; i < j; i++)
+ array.push(valuesToPush[i]);
+ return array;
+ },
+
+ extend: function (target, source) {
+ for(var prop in source) {
+ if(source.hasOwnProperty(prop)) {
+ target[prop] = source[prop];
+ }
+ }
+ return target;
+ },
+
+ emptyDomNode: function (domNode) {
+ while (domNode.firstChild) {
+ ko.removeNode(domNode.firstChild);
+ }
+ },
+
+ setDomNodeChildren: function (domNode, childNodes) {
+ ko.utils.emptyDomNode(domNode);
+ if (childNodes) {
+ ko.utils.arrayForEach(childNodes, function (childNode) {
+ domNode.appendChild(childNode);
+ });
+ }
+ },
+
+ replaceDomNodes: function (nodeToReplaceOrNodeArray, newNodesArray) {
+ var nodesToReplaceArray = nodeToReplaceOrNodeArray.nodeType ? [nodeToReplaceOrNodeArray] : nodeToReplaceOrNodeArray;
+ if (nodesToReplaceArray.length > 0) {
+ var insertionPoint = nodesToReplaceArray[0];
+ var parent = insertionPoint.parentNode;
+ for (var i = 0, j = newNodesArray.length; i < j; i++)
+ parent.insertBefore(newNodesArray[i], insertionPoint);
+ for (var i = 0, j = nodesToReplaceArray.length; i < j; i++) {
+ ko.removeNode(nodesToReplaceArray[i]);
+ }
+ }
+ },
+
+ setOptionNodeSelectionState: function (optionNode, isSelected) {
+ // IE6 sometimes throws "unknown error" if you try to write to .selected directly, whereas Firefox struggles with setAttribute. Pick one based on browser.
+ if (navigator.userAgent.indexOf("MSIE 6") >= 0)
+ optionNode.setAttribute("selected", isSelected);
+ else
+ optionNode.selected = isSelected;
+ },
+
+ stringTrim: function (string) {
+ return (string || "").replace(stringTrimRegex, "");
+ },
+
+ stringTokenize: function (string, delimiter) {
+ var result = [];
+ var tokens = (string || "").split(delimiter);
+ for (var i = 0, j = tokens.length; i < j; i++) {
+ var trimmed = ko.utils.stringTrim(tokens[i]);
+ if (trimmed !== "")
+ result.push(trimmed);
+ }
+ return result;
+ },
+
+ stringStartsWith: function (string, startsWith) {
+ string = string || "";
+ if (startsWith.length > string.length)
+ return false;
+ return string.substring(0, startsWith.length) === startsWith;
+ },
+
+ evalWithinScope: function (expression /*, scope1, scope2, scope3... */) {
+ // Build the source for a function that evaluates "expression"
+ // For each scope variable, add an extra level of "with" nesting
+ // Example result: with(sc[1]) { with(sc[0]) { return (expression) } }
+ var scopes = Array.prototype.slice.call(arguments, 1);
+ var functionBody = "return (" + expression + ")";
+ for (var i = 0; i < scopes.length; i++) {
+ if (scopes[i] && typeof scopes[i] == "object")
+ functionBody = "with(sc[" + i + "]) { " + functionBody + " } ";
+ }
+ return (new Function("sc", functionBody))(scopes);
+ },
+
+ domNodeIsContainedBy: function (node, containedByNode) {
+ if (containedByNode.compareDocumentPosition)
+ return (containedByNode.compareDocumentPosition(node) & 16) == 16;
+ while (node != null) {
+ if (node == containedByNode)
+ return true;
+ node = node.parentNode;
+ }
+ return false;
+ },
+
+ domNodeIsAttachedToDocument: function (node) {
+ return ko.utils.domNodeIsContainedBy(node, document);
+ },
+
+ registerEventHandler: function (element, eventType, handler) {
+ if (typeof jQuery != "undefined") {
+ if (isClickOnCheckableElement(element, eventType)) {
+ // For click events on checkboxes, jQuery interferes with the event handling in an awkward way:
+ // it toggles the element checked state *after* the click event handlers run, whereas native
+ // click events toggle the checked state *before* the event handler.
+ // Fix this by intecepting the handler and applying the correct checkedness before it runs.
+ var originalHandler = handler;
+ handler = function(event, eventData) {
+ var jQuerySuppliedCheckedState = this.checked;
+ if (eventData)
+ this.checked = eventData.checkedStateBeforeEvent !== true;
+ originalHandler.call(this, event);
+ this.checked = jQuerySuppliedCheckedState; // Restore the state jQuery applied
+ };
+ }
+ jQuery(element)['bind'](eventType, handler);
+ } else if (typeof element.addEventListener == "function")
+ element.addEventListener(eventType, handler, false);
+ else if (typeof element.attachEvent != "undefined")
+ element.attachEvent("on" + eventType, function (event) {
+ handler.call(element, event);
+ });
+ else
+ throw new Error("Browser doesn't support addEventListener or attachEvent");
+ },
+
+ triggerEvent: function (element, eventType) {
+ if (!(element && element.nodeType))
+ throw new Error("element must be a DOM node when calling triggerEvent");
+
+ if (typeof jQuery != "undefined") {
+ var eventData = [];
+ if (isClickOnCheckableElement(element, eventType)) {
+ // Work around the jQuery "click events on checkboxes" issue described above by storing the original checked state before triggering the handler
+ eventData.push({ checkedStateBeforeEvent: element.checked });
+ }
+ jQuery(element)['trigger'](eventType, eventData);
+ } else if (typeof document.createEvent == "function") {
+ if (typeof element.dispatchEvent == "function") {
+ var eventCategory = knownEventTypesByEventName[eventType] || "HTMLEvents";
+ var event = document.createEvent(eventCategory);
+ event.initEvent(eventType, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, element);
+ element.dispatchEvent(event);
+ }
+ else
+ throw new Error("The supplied element doesn't support dispatchEvent");
+ } else if (typeof element.fireEvent != "undefined") {
+ // Unlike other browsers, IE doesn't change the checked state of checkboxes/radiobuttons when you trigger their "click" event
+ // so to make it consistent, we'll do it manually here
+ if (eventType == "click") {
+ if ((element.tagName == "INPUT") && ((element.type.toLowerCase() == "checkbox") || (element.type.toLowerCase() == "radio")))
+ element.checked = element.checked !== true;
+ }
+ element.fireEvent("on" + eventType);
+ }
+ else
+ throw new Error("Browser doesn't support triggering events");
+ },
+
+ unwrapObservable: function (value) {
+ return ko.isObservable(value) ? value() : value;
+ },
+
+ domNodeHasCssClass: function (node, className) {
+ var currentClassNames = (node.className || "").split(/\s+/);
+ return ko.utils.arrayIndexOf(currentClassNames, className) >= 0;
+ },
+
+ toggleDomNodeCssClass: function (node, className, shouldHaveClass) {
+ var hasClass = ko.utils.domNodeHasCssClass(node, className);
+ if (shouldHaveClass && !hasClass) {
+ node.className = (node.className || "") + " " + className;
+ } else if (hasClass && !shouldHaveClass) {
+ var currentClassNames = (node.className || "").split(/\s+/);
+ var newClassName = "";
+ for (var i = 0; i < currentClassNames.length; i++)
+ if (currentClassNames[i] != className)
+ newClassName += currentClassNames[i] + " ";
+ node.className = ko.utils.stringTrim(newClassName);
+ }
+ },
+
+ outerHTML: function(node) {
+ // For Chrome on non-text nodes
+ // (Although IE supports outerHTML, the way it formats HTML is inconsistent - sometimes closing </li> tags are omitted, sometimes not. That caused https://github.com/SteveSanderson/knockout/issues/212.)
+ if (ieVersion === undefined) {
+ var nativeOuterHtml = node.outerHTML;
+ if (typeof nativeOuterHtml == "string")
+ return nativeOuterHtml;
+ }
+
+ // Other browsers
+ var dummyContainer = window.document.createElement("div");
+ dummyContainer.appendChild(node.cloneNode(true));
+ return dummyContainer.innerHTML;
+ },
+
+ setTextContent: function(element, textContent) {
+ var value = ko.utils.unwrapObservable(textContent);
+ if ((value === null) || (value === undefined))
+ value = "";
+
+ 'innerText' in element ? element.innerText = value
+ : element.textContent = value;
+
+ if (ieVersion >= 9) {
+ // Believe it or not, this actually fixes an IE9 rendering bug. Insane. https://github.com/SteveSanderson/knockout/issues/209
+ element.innerHTML = element.innerHTML;
+ }
+ },
+
+ range: function (min, max) {
+ min = ko.utils.unwrapObservable(min);
+ max = ko.utils.unwrapObservable(max);
+ var result = [];
+ for (var i = min; i <= max; i++)
+ result.push(i);
+ return result;
+ },
+
+ makeArray: function(arrayLikeObject) {
+ var result = [];
+ for (var i = 0, j = arrayLikeObject.length; i < j; i++) {
+ result.push(arrayLikeObject[i]);
+ };
+ return result;
+ },
+
+ isIe6 : isIe6,
+ isIe7 : isIe7,
+
+ getFormFields: function(form, fieldName) {
+ var fields = ko.utils.makeArray(form.getElementsByTagName("INPUT")).concat(ko.utils.makeArray(form.getElementsByTagName("TEXTAREA")));
+ var isMatchingField = (typeof fieldName == 'string')
+ ? function(field) { return field.name === fieldName }
+ : function(field) { return fieldName.test(field.name) }; // Treat fieldName as regex or object containing predicate
+ var matches = [];
+ for (var i = fields.length - 1; i >= 0; i--) {
+ if (isMatchingField(fields[i]))
+ matches.push(fields[i]);
+ };
+ return matches;
+ },
+
+ parseJson: function (jsonString) {
+ if (typeof jsonString == "string") {
+ jsonString = ko.utils.stringTrim(jsonString);
+ if (jsonString) {
+ if (window.JSON && window.JSON.parse) // Use native parsing where available
+ return window.JSON.parse(jsonString);
+ return (new Function("return " + jsonString))(); // Fallback on less safe parsing for older browsers
+ }
+ }
+ return null;
+ },
+
+ stringifyJson: function (data) {
+ if ((typeof JSON == "undefined") || (typeof JSON.stringify == "undefined"))
+ throw new Error("Cannot find JSON.stringify(). Some browsers (e.g., IE < 8) don't support it natively, but you can overcome this by adding a script reference to json2.js, downloadable from http://www.json.org/json2.js");
+ return JSON.stringify(ko.utils.unwrapObservable(data));
+ },
+
+ postJson: function (urlOrForm, data, options) {
+ options = options || {};
+ var params = options['params'] || {};
+ var includeFields = options['includeFields'] || this.fieldsIncludedWithJsonPost;
+ var url = urlOrForm;
+
+ // If we were given a form, use its 'action' URL and pick out any requested field values
+ if((typeof urlOrForm == 'object') && (urlOrForm.tagName == "FORM")) {
+ var originalForm = urlOrForm;
+ url = originalForm.action;
+ for (var i = includeFields.length - 1; i >= 0; i--) {
+ var fields = ko.utils.getFormFields(originalForm, includeFields[i]);
+ for (var j = fields.length - 1; j >= 0; j--)
+ params[fields[j].name] = fields[j].value;
+ }
+ }
+
+ data = ko.utils.unwrapObservable(data);
+ var form = document.createElement("FORM");
+ form.style.display = "none";
+ form.action = url;
+ form.method = "post";
+ for (var key in data) {
+ var input = document.createElement("INPUT");
+ input.name = key;
+ input.value = ko.utils.stringifyJson(ko.utils.unwrapObservable(data[key]));
+ form.appendChild(input);
+ }
+ for (var key in params) {
+ var input = document.createElement("INPUT");
+ input.name = key;
+ input.value = params[key];
+ form.appendChild(input);
+ }
+ document.body.appendChild(form);
+ options['submitter'] ? options['submitter'](form) : form.submit();
+ setTimeout(function () { form.parentNode.removeChild(form); }, 0);
+ }
+ }
+})();
+
+ko.exportSymbol('ko.utils', ko.utils);
+ko.utils.arrayForEach([
+ ['arrayForEach', ko.utils.arrayForEach],
+ ['arrayFirst', ko.utils.arrayFirst],
+ ['arrayFilter', ko.utils.arrayFilter],
+ ['arrayGetDistinctValues', ko.utils.arrayGetDistinctValues],
+ ['arrayIndexOf', ko.utils.arrayIndexOf],
+ ['arrayMap', ko.utils.arrayMap],
+ ['arrayPushAll', ko.utils.arrayPushAll],
+ ['arrayRemoveItem', ko.utils.arrayRemoveItem],
+ ['extend', ko.utils.extend],
+ ['fieldsIncludedWithJsonPost', ko.utils.fieldsIncludedWithJsonPost],
+ ['getFormFields', ko.utils.getFormFields],
+ ['postJson', ko.utils.postJson],
+ ['parseJson', ko.utils.parseJson],
+ ['registerEventHandler', ko.utils.registerEventHandler],
+ ['stringifyJson', ko.utils.stringifyJson],
+ ['range', ko.utils.range],
+ ['toggleDomNodeCssClass', ko.utils.toggleDomNodeCssClass],
+ ['triggerEvent', ko.utils.triggerEvent],
+ ['unwrapObservable', ko.utils.unwrapObservable]
+], function(item) {
+ ko.exportSymbol('ko.utils.' + item[0], item[1]);
+});
+
+if (!Function.prototype['bind']) {
+ // Function.prototype.bind is a standard part of ECMAScript 5th Edition (December 2009, http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-262.pdf)
+ // In case the browser doesn't implement it natively, provide a JavaScript implementation. This implementation is based on the one in prototype.js
+ Function.prototype['bind'] = function (object) {
+ var originalFunction = this, args = Array.prototype.slice.call(arguments), object = args.shift();
+ return function () {
+ return originalFunction.apply(object, args.concat(Array.prototype.slice.call(arguments)));
+ };
+ };
+}
+ko.utils.domData = new (function () {
+ var uniqueId = 0;
+ var dataStoreKeyExpandoPropertyName = "__ko__" + (new Date).getTime();
+ var dataStore = {};
+ return {
+ get: function (node, key) {
+ var allDataForNode = ko.utils.domData.getAll(node, false);
+ return allDataForNode === undefined ? undefined : allDataForNode[key];
+ },
+ set: function (node, key, value) {
+ if (value === undefined) {
+ // Make sure we don't actually create a new domData key if we are actually deleting a value
+ if (ko.utils.domData.getAll(node, false) === undefined)
+ return;
+ }
+ var allDataForNode = ko.utils.domData.getAll(node, true);
+ allDataForNode[key] = value;
+ },
+ getAll: function (node, createIfNotFound) {
+ var dataStoreKey = node[dataStoreKeyExpandoPropertyName];
+ var hasExistingDataStore = dataStoreKey && (dataStoreKey !== "null");
+ if (!hasExistingDataStore) {
+ if (!createIfNotFound)
+ return undefined;
+ dataStoreKey = node[dataStoreKeyExpandoPropertyName] = "ko" + uniqueId++;
+ dataStore[dataStoreKey] = {};
+ }
+ return dataStore[dataStoreKey];
+ },
+ clear: function (node) {
+ var dataStoreKey = node[dataStoreKeyExpandoPropertyName];
+ if (dataStoreKey) {
+ delete dataStore[dataStoreKey];
+ node[dataStoreKeyExpandoPropertyName] = null;
+ }
+ }
+ }
+})();
+
+ko.exportSymbol('ko.utils.domData', ko.utils.domData);
+ko.exportSymbol('ko.utils.domData.clear', ko.utils.domData.clear); // Exporting only so specs can clear up after themselves fully
+ko.utils.domNodeDisposal = new (function () {
+ var domDataKey = "__ko_domNodeDisposal__" + (new Date).getTime();
+
+ function getDisposeCallbacksCollection(node, createIfNotFound) {
+ var allDisposeCallbacks = ko.utils.domData.get(node, domDataKey);
+ if ((allDisposeCallbacks === undefined) && createIfNotFound) {
+ allDisposeCallbacks = [];
+ ko.utils.domData.set(node, domDataKey, allDisposeCallbacks);
+ }
+ return allDisposeCallbacks;
+ }
+ function destroyCallbacksCollection(node) {
+ ko.utils.domData.set(node, domDataKey, undefined);
+ }
+
+ function cleanSingleNode(node) {
+ // Run all the dispose callbacks
+ var callbacks = getDisposeCallbacksCollection(node, false);
+ if (callbacks) {
+ callbacks = callbacks.slice(0); // Clone, as the array may be modified during iteration (typically, callbacks will remove themselves)
+ for (var i = 0; i < callbacks.length; i++)
+ callbacks[i](node);
+ }
+
+ // Also erase the DOM data
+ ko.utils.domData.clear(node);
+
+ // Special support for jQuery here because it's so commonly used.
+ // Many jQuery plugins (including jquery.tmpl) store data using jQuery's equivalent of domData
+ // so notify it to tear down any resources associated with the node & descendants here.
+ if ((typeof jQuery == "function") && (typeof jQuery['cleanData'] == "function"))
+ jQuery['cleanData']([node]);
+ }
+
+ return {
+ addDisposeCallback : function(node, callback) {
+ if (typeof callback != "function")
+ throw new Error("Callback must be a function");
+ getDisposeCallbacksCollection(node, true).push(callback);
+ },
+
+ removeDisposeCallback : function(node, callback) {
+ var callbacksCollection = getDisposeCallbacksCollection(node, false);
+ if (callbacksCollection) {
+ ko.utils.arrayRemoveItem(callbacksCollection, callback);
+ if (callbacksCollection.length == 0)
+ destroyCallbacksCollection(node);
+ }
+ },
+
+ cleanNode : function(node) {
+ if ((node.nodeType != 1) && (node.nodeType != 9))
+ return;
+ cleanSingleNode(node);
+
+ // Clone the descendants list in case it changes during iteration
+ var descendants = [];
+ ko.utils.arrayPushAll(descendants, node.getElementsByTagName("*"));
+ for (var i = 0, j = descendants.length; i < j; i++)
+ cleanSingleNode(descendants[i]);
+ },
+
+ removeNode : function(node) {
+ ko.cleanNode(node);
+ if (node.parentNode)
+ node.parentNode.removeChild(node);
+ }
+ }
+})();
+ko.cleanNode = ko.utils.domNodeDisposal.cleanNode; // Shorthand name for convenience
+ko.removeNode = ko.utils.domNodeDisposal.removeNode; // Shorthand name for convenience
+ko.exportSymbol('ko.cleanNode', ko.cleanNode);
+ko.exportSymbol('ko.removeNode', ko.removeNode);
+ko.exportSymbol('ko.utils.domNodeDisposal', ko.utils.domNodeDisposal);
+ko.exportSymbol('ko.utils.domNodeDisposal.addDisposeCallback', ko.utils.domNodeDisposal.addDisposeCallback);
+ko.exportSymbol('ko.utils.domNodeDisposal.removeDisposeCallback', ko.utils.domNodeDisposal.removeDisposeCallback);(function () {
+ var leadingCommentRegex = /^(\s*)<!--(.*?)-->/;
+
+ function simpleHtmlParse(html) {
+ // Based on jQuery's "clean" function, but only accounting for table-related elements.
+ // If you have referenced jQuery, this won't be used anyway - KO will use jQuery's "clean" function directly
+
+ // Note that there's still an issue in IE < 9 whereby it will discard comment nodes that are the first child of
+ // a descendant node. For example: "<div><!-- mycomment -->abc</div>" will get parsed as "<div>abc</div>"
+ // This won't affect anyone who has referenced jQuery, and there's always the workaround of inserting a dummy node
+ // (possibly a text node) in front of the comment. So, KO does not attempt to workaround this IE issue automatically at present.
+
+ // Trim whitespace, otherwise indexOf won't work as expected
+ var tags = ko.utils.stringTrim(html).toLowerCase(), div = document.createElement("div");
+
+ // Finds the first match from the left column, and returns the corresponding "wrap" data from the right column
+ var wrap = tags.match(/^<(thead|tbody|tfoot)/) && [1, "<table>", "</table>"] ||
+ !tags.indexOf("<tr") && [2, "<table><tbody>", "</tbody></table>"] ||
+ (!tags.indexOf("<td") || !tags.indexOf("<th")) && [3, "<table><tbody><tr>", "</tr></tbody></table>"] ||
+ /* anything else */ [0, "", ""];
+
+ // Go to html and back, then peel off extra wrappers
+ // Note that we always prefix with some dummy text, because otherwise, IE<9 will strip out leading comment nodes in descendants. Total madness.
+ var markup = "ignored<div>" + wrap[1] + html + wrap[2] + "</div>";
+ if (typeof window['innerShiv'] == "function") {
+ div.appendChild(window['innerShiv'](markup));
+ } else {
+ div.innerHTML = markup;
+ }
+
+ // Move to the right depth
+ while (wrap[0]--)
+ div = div.lastChild;
+
+ return ko.utils.makeArray(div.lastChild.childNodes);
+ }
+
+ function jQueryHtmlParse(html) {
+ var elems = jQuery['clean']([html]);
+
+ // As of jQuery 1.7.1, jQuery parses the HTML by appending it to some dummy parent nodes held in an in-memory document fragment.
+ // Unfortunately, it never clears the dummy parent nodes from the document fragment, so it leaks memory over time.
+ // Fix this by finding the top-most dummy parent element, and detaching it from its owner fragment.
+ if (elems && elems[0]) {
+ // Find the top-most parent element that's a direct child of a document fragment
+ var elem = elems[0];
+ while (elem.parentNode && elem.parentNode.nodeType !== 11 /* i.e., DocumentFragment */)
+ elem = elem.parentNode;
+ // ... then detach it
+ if (elem.parentNode)
+ elem.parentNode.removeChild(elem);
+ }
+
+ return elems;
+ }
+
+ ko.utils.parseHtmlFragment = function(html) {
+ return typeof jQuery != 'undefined' ? jQueryHtmlParse(html) // As below, benefit from jQuery's optimisations where possible
+ : simpleHtmlParse(html); // ... otherwise, this simple logic will do in most common cases.
+ };
+
+ ko.utils.setHtml = function(node, html) {
+ ko.utils.emptyDomNode(node);
+
+ if ((html !== null) && (html !== undefined)) {
+ if (typeof html != 'string')
+ html = html.toString();
+
+ // jQuery contains a lot of sophisticated code to parse arbitrary HTML fragments,
+ // for example <tr> elements which are not normally allowed to exist on their own.
+ // If you've referenced jQuery we'll use that rather than duplicating its code.
+ if (typeof jQuery != 'undefined') {
+ jQuery(node)['html'](html);
+ } else {
+ // ... otherwise, use KO's own parsing logic.
+ var parsedNodes = ko.utils.parseHtmlFragment(html);
+ for (var i = 0; i < parsedNodes.length; i++)
+ node.appendChild(parsedNodes[i]);
+ }
+ }
+ };
+})();
+
+ko.exportSymbol('ko.utils.parseHtmlFragment', ko.utils.parseHtmlFragment);
+ko.exportSymbol('ko.utils.setHtml', ko.utils.setHtml);
+ko.memoization = (function () {
+ var memos = {};
+
+ function randomMax8HexChars() {
+ return (((1 + Math.random()) * 0x100000000) | 0).toString(16).substring(1);
+ }
+ function generateRandomId() {
+ return randomMax8HexChars() + randomMax8HexChars();
+ }
+ function findMemoNodes(rootNode, appendToArray) {
+ if (!rootNode)
+ return;
+ if (rootNode.nodeType == 8) {
+ var memoId = ko.memoization.parseMemoText(rootNode.nodeValue);
+ if (memoId != null)
+ appendToArray.push({ domNode: rootNode, memoId: memoId });
+ } else if (rootNode.nodeType == 1) {
+ for (var i = 0, childNodes = rootNode.childNodes, j = childNodes.length; i < j; i++)
+ findMemoNodes(childNodes[i], appendToArray);
+ }
+ }
+
+ return {
+ memoize: function (callback) {
+ if (typeof callback != "function")
+ throw new Error("You can only pass a function to ko.memoization.memoize()");
+ var memoId = generateRandomId();
+ memos[memoId] = callback;
+ return "<!--[ko_memo:" + memoId + "]-->";
+ },
+
+ unmemoize: function (memoId, callbackParams) {
+ var callback = memos[memoId];
+ if (callback === undefined)
+ throw new Error("Couldn't find any memo with ID " + memoId + ". Perhaps it's already been unmemoized.");
+ try {
+ callback.apply(null, callbackParams || []);
+ return true;
+ }
+ finally { delete memos[memoId]; }
+ },
+
+ unmemoizeDomNodeAndDescendants: function (domNode, extraCallbackParamsArray) {
+ var memos = [];
+ findMemoNodes(domNode, memos);
+ for (var i = 0, j = memos.length; i < j; i++) {
+ var node = memos[i].domNode;
+ var combinedParams = [node];
+ if (extraCallbackParamsArray)
+ ko.utils.arrayPushAll(combinedParams, extraCallbackParamsArray);
+ ko.memoization.unmemoize(memos[i].memoId, combinedParams);
+ node.nodeValue = ""; // Neuter this node so we don't try to unmemoize it again
+ if (node.parentNode)
+ node.parentNode.removeChild(node); // If possible, erase it totally (not always possible - someone else might just hold a reference to it then call unmemoizeDomNodeAndDescendants again)
+ }
+ },
+
+ parseMemoText: function (memoText) {
+ var match = memoText.match(/^\[ko_memo\:(.*?)\]$/);
+ return match ? match[1] : null;
+ }
+ };
+})();
+
+ko.exportSymbol('ko.memoization', ko.memoization);
+ko.exportSymbol('ko.memoization.memoize', ko.memoization.memoize);
+ko.exportSymbol('ko.memoization.unmemoize', ko.memoization.unmemoize);
+ko.exportSymbol('ko.memoization.parseMemoText', ko.memoization.parseMemoText);
+ko.exportSymbol('ko.memoization.unmemoizeDomNodeAndDescendants', ko.memoization.unmemoizeDomNodeAndDescendants);
+ko.extenders = {
+ 'throttle': function(target, timeout) {
+ // Throttling means two things:
+
+ // (1) For dependent observables, we throttle *evaluations* so that, no matter how fast its dependencies
+ // notify updates, the target doesn't re-evaluate (and hence doesn't notify) faster than a certain rate
+ target['throttleEvaluation'] = timeout;
+
+ // (2) For writable targets (observables, or writable dependent observables), we throttle *writes*
+ // so the target cannot change value synchronously or faster than a certain rate
+ var writeTimeoutInstance = null;
+ return ko.dependentObservable({
+ 'read': target,
+ 'write': function(value) {
+ clearTimeout(writeTimeoutInstance);
+ writeTimeoutInstance = setTimeout(function() {
+ target(value);
+ }, timeout);
+ }
+ });
+ },
+
+ 'notify': function(target, notifyWhen) {
+ target["equalityComparer"] = notifyWhen == "always"
+ ? function() { return false } // Treat all values as not equal
+ : ko.observable["fn"]["equalityComparer"];
+ return target;
+ }
+};
+
+function applyExtenders(requestedExtenders) {
+ var target = this;
+ if (requestedExtenders) {
+ for (var key in requestedExtenders) {
+ var extenderHandler = ko.extenders[key];
+ if (typeof extenderHandler == 'function') {
+ target = extenderHandler(target, requestedExtenders[key]);
+ }
+ }
+ }
+ return target;
+}
+
+ko.exportSymbol('ko.extenders', ko.extenders);
+ko.subscription = function (callback, disposeCallback) {
+ this.callback = callback;
+ this.disposeCallback = disposeCallback;
+ ko.exportProperty(this, 'dispose', this.dispose);
+};
+ko.subscription.prototype.dispose = function () {
+ this.isDisposed = true;
+ this.disposeCallback();
+};
+
+ko.subscribable = function () {
+ this._subscriptions = {};
+
+ ko.utils.extend(this, ko.subscribable['fn']);
+ ko.exportProperty(this, 'subscribe', this.subscribe);
+ ko.exportProperty(this, 'extend', this.extend);
+ ko.exportProperty(this, 'getSubscriptionsCount', this.getSubscriptionsCount);
+}
+
+var defaultEvent = "change";
+
+ko.subscribable['fn'] = {
+ subscribe: function (callback, callbackTarget, event) {
+ event = event || defaultEvent;
+ var boundCallback = callbackTarget ? callback.bind(callbackTarget) : callback;
+
+ var subscription = new ko.subscription(boundCallback, function () {
+ ko.utils.arrayRemoveItem(this._subscriptions[event], subscription);
+ }.bind(this));
+
+ if (!this._subscriptions[event])
+ this._subscriptions[event] = [];
+ this._subscriptions[event].push(subscription);
+ return subscription;
+ },
+
+ "notifySubscribers": function (valueToNotify, event) {
+ event = event || defaultEvent;
+ if (this._subscriptions[event]) {
+ ko.utils.arrayForEach(this._subscriptions[event].slice(0), function (subscription) {
+ // In case a subscription was disposed during the arrayForEach cycle, check
+ // for isDisposed on each subscription before invoking its callback
+ if (subscription && (subscription.isDisposed !== true))
+ subscription.callback(valueToNotify);
+ });
+ }
+ },
+
+ getSubscriptionsCount: function () {
+ var total = 0;
+ for (var eventName in this._subscriptions) {
+ if (this._subscriptions.hasOwnProperty(eventName))
+ total += this._subscriptions[eventName].length;
+ }
+ return total;
+ },
+
+ extend: applyExtenders
+};
+
+
+ko.isSubscribable = function (instance) {
+ return typeof instance.subscribe == "function" && typeof instance["notifySubscribers"] == "function";
+};
+
+ko.exportSymbol('ko.subscribable', ko.subscribable);
+ko.exportSymbol('ko.isSubscribable', ko.isSubscribable);
+
+ko.dependencyDetection = (function () {
+ var _frames = [];
+
+ return {
+ begin: function (callback) {
+ _frames.push({ callback: callback, distinctDependencies:[] });
+ },
+
+ end: function () {
+ _frames.pop();
+ },
+
+ registerDependency: function (subscribable) {
+ if (!ko.isSubscribable(subscribable))
+ throw "Only subscribable things can act as dependencies";
+ if (_frames.length > 0) {
+ var topFrame = _frames[_frames.length - 1];
+ if (ko.utils.arrayIndexOf(topFrame.distinctDependencies, subscribable) >= 0)
+ return;
+ topFrame.distinctDependencies.push(subscribable);
+ topFrame.callback(subscribable);
+ }
+ }
+ };
+})();var primitiveTypes = { 'undefined':true, 'boolean':true, 'number':true, 'string':true };
+
+ko.observable = function (initialValue) {
+ var _latestValue = initialValue;
+
+ function observable() {
+ if (arguments.length > 0) {
+ // Write
+
+ // Ignore writes if the value hasn't changed
+ if ((!observable['equalityComparer']) || !observable['equalityComparer'](_latestValue, arguments[0])) {
+ observable.valueWillMutate();
+ _latestValue = arguments[0];
+ observable.valueHasMutated();
+ }
+ return this; // Permits chained assignments
+ }
+ else {
+ // Read
+ ko.dependencyDetection.registerDependency(observable); // The caller only needs to be notified of changes if they did a "read" operation
+ return _latestValue;
+ }
+ }
+ ko.subscribable.call(observable);
+ observable.valueHasMutated = function () { observable["notifySubscribers"](_latestValue); }
+ observable.valueWillMutate = function () { observable["notifySubscribers"](_latestValue, "beforeChange"); }
+ ko.utils.extend(observable, ko.observable['fn']);
+
+ ko.exportProperty(observable, "valueHasMutated", observable.valueHasMutated);
+ ko.exportProperty(observable, "valueWillMutate", observable.valueWillMutate);
+
+ return observable;
+}
+
+ko.observable['fn'] = {
+ __ko_proto__: ko.observable,
+
+ "equalityComparer": function valuesArePrimitiveAndEqual(a, b) {
+ var oldValueIsPrimitive = (a === null) || (typeof(a) in primitiveTypes);
+ return oldValueIsPrimitive ? (a === b) : false;
+ }
+};
+
+ko.isObservable = function (instance) {
+ if ((instance === null) || (instance === undefined) || (instance.__ko_proto__ === undefined)) return false;
+ if (instance.__ko_proto__ === ko.observable) return true;
+ return ko.isObservable(instance.__ko_proto__); // Walk the prototype chain
+}
+ko.isWriteableObservable = function (instance) {
+ // Observable
+ if ((typeof instance == "function") && instance.__ko_proto__ === ko.observable)
+ return true;
+ // Writeable dependent observable
+ if ((typeof instance == "function") && (instance.__ko_proto__ === ko.dependentObservable) && (instance.hasWriteFunction))
+ return true;
+ // Anything else
+ return false;
+}
+
+
+ko.exportSymbol('ko.observable', ko.observable);
+ko.exportSymbol('ko.isObservable', ko.isObservable);
+ko.exportSymbol('ko.isWriteableObservable', ko.isWriteableObservable);
+ko.observableArray = function (initialValues) {
+ if (arguments.length == 0) {
+ // Zero-parameter constructor initializes to empty array
+ initialValues = [];
+ }
+ if ((initialValues !== null) && (initialValues !== undefined) && !('length' in initialValues))
+ throw new Error("The argument passed when initializing an observable array must be an array, or null, or undefined.");
+
+ var result = new ko.observable(initialValues);
+ ko.utils.extend(result, ko.observableArray['fn']);
+
+ ko.exportProperty(result, "remove", result.remove);
+ ko.exportProperty(result, "removeAll", result.removeAll);
+ ko.exportProperty(result, "destroy", result.destroy);
+ ko.exportProperty(result, "destroyAll", result.destroyAll);
+ ko.exportProperty(result, "indexOf", result.indexOf);
+ ko.exportProperty(result, "replace", result.replace);
+
+ return result;
+}
+
+ko.observableArray['fn'] = {
+ remove: function (valueOrPredicate) {
+ var underlyingArray = this();
+ var removedValues = [];
+ var predicate = typeof valueOrPredicate == "function" ? valueOrPredicate : function (value) { return value === valueOrPredicate; };
+ for (var i = 0; i < underlyingArray.length; i++) {
+ var value = underlyingArray[i];
+ if (predicate(value)) {
+ if (removedValues.length === 0) {
+ this.valueWillMutate();
+ }
+ removedValues.push(value);
+ underlyingArray.splice(i, 1);
+ i--;
+ }
+ }
+ if (removedValues.length) {
+ this.valueHasMutated();
+ }
+ return removedValues;
+ },
+
+ removeAll: function (arrayOfValues) {
+ // If you passed zero args, we remove everything
+ if (arrayOfValues === undefined) {
+ var underlyingArray = this();
+ var allValues = underlyingArray.slice(0);
+ this.valueWillMutate();
+ underlyingArray.splice(0, underlyingArray.length);
+ this.valueHasMutated();
+ return allValues;
+ }
+ // If you passed an arg, we interpret it as an array of entries to remove
+ if (!arrayOfValues)
+ return [];
+ return this.remove(function (value) {
+ return ko.utils.arrayIndexOf(arrayOfValues, value) >= 0;
+ });
+ },
+
+ destroy: function (valueOrPredicate) {
+ var underlyingArray = this();
+ var predicate = typeof valueOrPredicate == "function" ? valueOrPredicate : function (value) { return value === valueOrPredicate; };
+ this.valueWillMutate();
+ for (var i = underlyingArray.length - 1; i >= 0; i--) {
+ var value = underlyingArray[i];
+ if (predicate(value))
+ underlyingArray[i]["_destroy"] = true;
+ }
+ this.valueHasMutated();
+ },
+
+ destroyAll: function (arrayOfValues) {
+ // If you passed zero args, we destroy everything
+ if (arrayOfValues === undefined)
+ return this.destroy(function() { return true });
+
+ // If you passed an arg, we interpret it as an array of entries to destroy
+ if (!arrayOfValues)
+ return [];
+ return this.destroy(function (value) {
+ return ko.utils.arrayIndexOf(arrayOfValues, value) >= 0;
+ });
+ },
+
+ indexOf: function (item) {
+ var underlyingArray = this();
+ return ko.utils.arrayIndexOf(underlyingArray, item);
+ },
+
+ replace: function(oldItem, newItem) {
+ var index = this.indexOf(oldItem);
+ if (index >= 0) {
+ this.valueWillMutate();
+ this()[index] = newItem;
+ this.valueHasMutated();
+ }
+ }
+}
+
+// Populate ko.observableArray.fn with read/write functions from native arrays
+ko.utils.arrayForEach(["pop", "push", "reverse", "shift", "sort", "splice", "unshift"], function (methodName) {
+ ko.observableArray['fn'][methodName] = function () {
+ var underlyingArray = this();
+ this.valueWillMutate();
+ var methodCallResult = underlyingArray[methodName].apply(underlyingArray, arguments);
+ this.valueHasMutated();
+ return methodCallResult;
+ };
+});
+
+// Populate ko.observableArray.fn with read-only functions from native arrays
+ko.utils.arrayForEach(["slice"], function (methodName) {
+ ko.observableArray['fn'][methodName] = function () {
+ var underlyingArray = this();
+ return underlyingArray[methodName].apply(underlyingArray, arguments);
+ };
+});
+
+ko.exportSymbol('ko.observableArray', ko.observableArray);
+function prepareOptions(evaluatorFunctionOrOptions, evaluatorFunctionTarget, options) {
+ if (evaluatorFunctionOrOptions && typeof evaluatorFunctionOrOptions == "object") {
+ // Single-parameter syntax - everything is on this "options" param
+ options = evaluatorFunctionOrOptions;
+ } else {
+ // Multi-parameter syntax - construct the options according to the params passed
+ options = options || {};
+ options["read"] = evaluatorFunctionOrOptions || options["read"];
+ }
+ // By here, "options" is always non-null
+
+ if (typeof options["read"] != "function")
+ throw "Pass a function that returns the value of the dependentObservable";
+
+ return options;
+}
+
+ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunctionTarget, options) {
+ var _latestValue,
+ _hasBeenEvaluated = false,
+ options = prepareOptions(evaluatorFunctionOrOptions, evaluatorFunctionTarget, options);
+
+ // Build "disposeWhenNodeIsRemoved" and "disposeWhenNodeIsRemovedCallback" option values
+ // (Note: "disposeWhenNodeIsRemoved" option both proactively disposes as soon as the node is removed using ko.removeNode(),
+ // plus adds a "disposeWhen" callback that, on each evaluation, disposes if the node was removed by some other means.)
+ var disposeWhenNodeIsRemoved = (typeof options["disposeWhenNodeIsRemoved"] == "object") ? options["disposeWhenNodeIsRemoved"] : null;
+ var disposeWhenNodeIsRemovedCallback = null;
+ if (disposeWhenNodeIsRemoved) {
+ disposeWhenNodeIsRemovedCallback = function() { dependentObservable.dispose() };
+ ko.utils.domNodeDisposal.addDisposeCallback(disposeWhenNodeIsRemoved, disposeWhenNodeIsRemovedCallback);
+ var existingDisposeWhenFunction = options["disposeWhen"];
+ options["disposeWhen"] = function () {
+ return (!ko.utils.domNodeIsAttachedToDocument(disposeWhenNodeIsRemoved))
+ || ((typeof existingDisposeWhenFunction == "function") && existingDisposeWhenFunction());
+ }
+ }
+
+ var _subscriptionsToDependencies = [];
+ function disposeAllSubscriptionsToDependencies() {
+ ko.utils.arrayForEach(_subscriptionsToDependencies, function (subscription) {
+ subscription.dispose();
+ });
+ _subscriptionsToDependencies = [];
+ }
+
+ var evaluationTimeoutInstance = null;
+ function evaluatePossiblyAsync() {
+ var throttleEvaluationTimeout = dependentObservable['throttleEvaluation'];
+ if (throttleEvaluationTimeout && throttleEvaluationTimeout >= 0) {
+ clearTimeout(evaluationTimeoutInstance);
+ evaluationTimeoutInstance = setTimeout(evaluateImmediate, throttleEvaluationTimeout);
+ } else
+ evaluateImmediate();
+ }
+
+ function evaluateImmediate() {
+ // Don't dispose on first evaluation, because the "disposeWhen" callback might
+ // e.g., dispose when the associated DOM element isn't in the doc, and it's not
+ // going to be in the doc until *after* the first evaluation
+ if ((_hasBeenEvaluated) && typeof options["disposeWhen"] == "function") {
+ if (options["disposeWhen"]()) {
+ dependentObservable.dispose();
+ return;
+ }
+ }
+
+ try {
+ disposeAllSubscriptionsToDependencies();
+ ko.dependencyDetection.begin(function(subscribable) {
+ _subscriptionsToDependencies.push(subscribable.subscribe(evaluatePossiblyAsync));
+ });
+ var valueForThis = options["owner"] || evaluatorFunctionTarget; // If undefined, it will default to "window" by convention. This might change in the future.
+ var newValue = options["read"].call(valueForThis);
+ dependentObservable["notifySubscribers"](_latestValue, "beforeChange");
+ _latestValue = newValue;
+ } finally {
+ ko.dependencyDetection.end();
+ }
+
+ dependentObservable["notifySubscribers"](_latestValue);
+ _hasBeenEvaluated = true;
+ }
+
+ function dependentObservable() {
+ if (arguments.length > 0) {
+ if (typeof options["write"] === "function") {
+ // Writing a value
+ var valueForThis = options["owner"] || evaluatorFunctionTarget; // If undefined, it will default to "window" by convention. This might change in the future.
+ options["write"].apply(valueForThis, arguments);
+ } else {
+ throw "Cannot write a value to a dependentObservable unless you specify a 'write' option. If you wish to read the current value, don't pass any parameters.";
+ }
+ } else {
+ // Reading the value
+ if (!_hasBeenEvaluated)
+ evaluateImmediate();
+ ko.dependencyDetection.registerDependency(dependentObservable);
+ return _latestValue;
+ }
+ }
+ dependentObservable.getDependenciesCount = function () { return _subscriptionsToDependencies.length; }
+ dependentObservable.hasWriteFunction = typeof options["write"] === "function";
+ dependentObservable.dispose = function () {
+ if (disposeWhenNodeIsRemoved)
+ ko.utils.domNodeDisposal.removeDisposeCallback(disposeWhenNodeIsRemoved, disposeWhenNodeIsRemovedCallback);
+ disposeAllSubscriptionsToDependencies();
+ };
+
+ ko.subscribable.call(dependentObservable);
+ ko.utils.extend(dependentObservable, ko.dependentObservable['fn']);
+
+ if (options['deferEvaluation'] !== true)
+ evaluateImmediate();
+
+ ko.exportProperty(dependentObservable, 'dispose', dependentObservable.dispose);
+ ko.exportProperty(dependentObservable, 'getDependenciesCount', dependentObservable.getDependenciesCount);
+
+ return dependentObservable;
+};
+
+ko.dependentObservable['fn'] = {
+ __ko_proto__: ko.dependentObservable
+};
+
+ko.dependentObservable.__ko_proto__ = ko.observable;
+
+ko.exportSymbol('ko.dependentObservable', ko.dependentObservable);
+ko.exportSymbol('ko.computed', ko.dependentObservable); // Make "ko.computed" an alias for "ko.dependentObservable"
+(function() {
+ var maxNestedObservableDepth = 10; // Escape the (unlikely) pathalogical case where an observable's current value is itself (or similar reference cycle)
+
+ ko.toJS = function(rootObject) {
+ if (arguments.length == 0)
+ throw new Error("When calling ko.toJS, pass the object you want to convert.");
+
+ // We just unwrap everything at every level in the object graph
+ return mapJsObjectGraph(rootObject, function(valueToMap) {
+ // Loop because an observable's value might in turn be another observable wrapper
+ for (var i = 0; ko.isObservable(valueToMap) && (i < maxNestedObservableDepth); i++)
+ valueToMap = valueToMap();
+ return valueToMap;
+ });
+ };
+
+ ko.toJSON = function(rootObject) {
+ var plainJavaScriptObject = ko.toJS(rootObject);
+ return ko.utils.stringifyJson(plainJavaScriptObject);
+ };
+
+ function mapJsObjectGraph(rootObject, mapInputCallback, visitedObjects) {
+ visitedObjects = visitedObjects || new objectLookup();
+
+ rootObject = mapInputCallback(rootObject);
+ var canHaveProperties = (typeof rootObject == "object") && (rootObject !== null) && (rootObject !== undefined) && (!(rootObject instanceof Date));
+ if (!canHaveProperties)
+ return rootObject;
+
+ var outputProperties = rootObject instanceof Array ? [] : {};
+ visitedObjects.save(rootObject, outputProperties);
+
+ visitPropertiesOrArrayEntries(rootObject, function(indexer) {
+ var propertyValue = mapInputCallback(rootObject[indexer]);
+
+ switch (typeof propertyValue) {
+ case "boolean":
+ case "number":
+ case "string":
+ case "function":
+ outputProperties[indexer] = propertyValue;
+ break;
+ case "object":
+ case "undefined":
+ var previouslyMappedValue = visitedObjects.get(propertyValue);
+ outputProperties[indexer] = (previouslyMappedValue !== undefined)
+ ? previouslyMappedValue
+ : mapJsObjectGraph(propertyValue, mapInputCallback, visitedObjects);
+ break;
+ }
+ });
+
+ return outputProperties;
+ }
+
+ function visitPropertiesOrArrayEntries(rootObject, visitorCallback) {
+ if (rootObject instanceof Array) {
+ for (var i = 0; i < rootObject.length; i++)
+ visitorCallback(i);
+ } else {
+ for (var propertyName in rootObject)
+ visitorCallback(propertyName);
+ }
+ };
+
+ function objectLookup() {
+ var keys = [];
+ var values = [];
+ this.save = function(key, value) {
+ var existingIndex = ko.utils.arrayIndexOf(keys, key);
+ if (existingIndex >= 0)
+ values[existingIndex] = value;
+ else {
+ keys.push(key);
+ values.push(value);
+ }
+ };
+ this.get = function(key) {
+ var existingIndex = ko.utils.arrayIndexOf(keys, key);
+ return (existingIndex >= 0) ? values[existingIndex] : undefined;
+ };
+ };
+})();
+
+ko.exportSymbol('ko.toJS', ko.toJS);
+ko.exportSymbol('ko.toJSON', ko.toJSON);(function () {
+ var hasDomDataExpandoProperty = '__ko__hasDomDataOptionValue__';
+
+ // Normally, SELECT elements and their OPTIONs can only take value of type 'string' (because the values
+ // are stored on DOM attributes). ko.selectExtensions provides a way for SELECTs/OPTIONs to have values
+ // that are arbitrary objects. This is very convenient when implementing things like cascading dropdowns.
+ ko.selectExtensions = {
+ readValue : function(element) {
+ if (element.tagName == 'OPTION') {
+ if (element[hasDomDataExpandoProperty] === true)
+ return ko.utils.domData.get(element, ko.bindingHandlers.options.optionValueDomDataKey);
+ return element.getAttribute("value");
+ } else if (element.tagName == 'SELECT')
+ return element.selectedIndex >= 0 ? ko.selectExtensions.readValue(element.options[element.selectedIndex]) : undefined;
+ else
+ return element.value;
+ },
+
+ writeValue: function(element, value) {
+ if (element.tagName == 'OPTION') {
+ switch(typeof value) {
+ case "string":
+ ko.utils.domData.set(element, ko.bindingHandlers.options.optionValueDomDataKey, undefined);
+ if (hasDomDataExpandoProperty in element) { // IE <= 8 throws errors if you delete non-existent properties from a DOM node
+ delete element[hasDomDataExpandoProperty];
+ }
+ element.value = value;
+ break;
+ default:
+ // Store arbitrary object using DomData
+ ko.utils.domData.set(element, ko.bindingHandlers.options.optionValueDomDataKey, value);
+ element[hasDomDataExpandoProperty] = true;
+
+ // Special treatment of numbers is just for backward compatibility. KO 1.2.1 wrote numerical values to element.value.
+ element.value = typeof value === "number" ? value : "";
+ break;
+ }
+ } else if (element.tagName == 'SELECT') {
+ for (var i = element.options.length - 1; i >= 0; i--) {
+ if (ko.selectExtensions.readValue(element.options[i]) == value) {
+ element.selectedIndex = i;
+ break;
+ }
+ }
+ } else {
+ if ((value === null) || (value === undefined))
+ value = "";
+ element.value = value;
+ }
+ }
+ };
+})();
+
+ko.exportSymbol('ko.selectExtensions', ko.selectExtensions);
+ko.exportSymbol('ko.selectExtensions.readValue', ko.selectExtensions.readValue);
+ko.exportSymbol('ko.selectExtensions.writeValue', ko.selectExtensions.writeValue);
+
+ko.jsonExpressionRewriting = (function () {
+ var restoreCapturedTokensRegex = /\@ko_token_(\d+)\@/g;
+ var javaScriptAssignmentTarget = /^[\_$a-z][\_$a-z0-9]*(\[.*?\])*(\.[\_$a-z][\_$a-z0-9]*(\[.*?\])*)*$/i;
+ var javaScriptReservedWords = ["true", "false"];
+
+ function restoreTokens(string, tokens) {
+ var prevValue = null;
+ while (string != prevValue) { // Keep restoring tokens until it no longer makes a difference (they may be nested)
+ prevValue = string;
+ string = string.replace(restoreCapturedTokensRegex, function (match, tokenIndex) {
+ return tokens[tokenIndex];
+ });
+ }
+ return string;
+ }
+
+ function isWriteableValue(expression) {
+ if (ko.utils.arrayIndexOf(javaScriptReservedWords, ko.utils.stringTrim(expression).toLowerCase()) >= 0)
+ return false;
+ return expression.match(javaScriptAssignmentTarget) !== null;
+ }
+
+ function ensureQuoted(key) {
+ var trimmedKey = ko.utils.stringTrim(key);
+ switch (trimmedKey.length && trimmedKey.charAt(0)) {
+ case "'":
+ case '"':
+ return key;
+ default:
+ return "'" + trimmedKey + "'";
+ }
+ }
+
+ return {
+ bindingRewriteValidators: [],
+
+ parseObjectLiteral: function(objectLiteralString) {
+ // A full tokeniser+lexer would add too much weight to this library, so here's a simple parser
+ // that is sufficient just to split an object literal string into a set of top-level key-value pairs
+
+ var str = ko.utils.stringTrim(objectLiteralString);
+ if (str.length < 3)
+ return [];
+ if (str.charAt(0) === "{")// Ignore any braces surrounding the whole object literal
+ str = str.substring(1, str.length - 1);
+
+ // Pull out any string literals and regex literals
+ var tokens = [];
+ var tokenStart = null, tokenEndChar;
+ for (var position = 0; position < str.length; position++) {
+ var c = str.charAt(position);
+ if (tokenStart === null) {
+ switch (c) {
+ case '"':
+ case "'":
+ case "/":
+ tokenStart = position;
+ tokenEndChar = c;
+ break;
+ }
+ } else if ((c == tokenEndChar) && (str.charAt(position - 1) !== "\\")) {
+ var token = str.substring(tokenStart, position + 1);
+ tokens.push(token);
+ var replacement = "@ko_token_" + (tokens.length - 1) + "@";
+ str = str.substring(0, tokenStart) + replacement + str.substring(position + 1);
+ position -= (token.length - replacement.length);
+ tokenStart = null;
+ }
+ }
+
+ // Next pull out balanced paren, brace, and bracket blocks
+ tokenStart = null;
+ tokenEndChar = null;
+ var tokenDepth = 0, tokenStartChar = null;
+ for (var position = 0; position < str.length; position++) {
+ var c = str.charAt(position);
+ if (tokenStart === null) {
+ switch (c) {
+ case "{": tokenStart = position; tokenStartChar = c;
+ tokenEndChar = "}";
+ break;
+ case "(": tokenStart = position; tokenStartChar = c;
+ tokenEndChar = ")";
+ break;
+ case "[": tokenStart = position; tokenStartChar = c;
+ tokenEndChar = "]";
+ break;
+ }
+ }
+
+ if (c === tokenStartChar)
+ tokenDepth++;
+ else if (c === tokenEndChar) {
+ tokenDepth--;
+ if (tokenDepth === 0) {
+ var token = str.substring(tokenStart, position + 1);
+ tokens.push(token);
+ var replacement = "@ko_token_" + (tokens.length - 1) + "@";
+ str = str.substring(0, tokenStart) + replacement + str.substring(position + 1);
+ position -= (token.length - replacement.length);
+ tokenStart = null;
+ }
+ }
+ }
+
+ // Now we can safely split on commas to get the key/value pairs
+ var result = [];
+ var keyValuePairs = str.split(",");
+ for (var i = 0, j = keyValuePairs.length; i < j; i++) {
+ var pair = keyValuePairs[i];
+ var colonPos = pair.indexOf(":");
+ if ((colonPos > 0) && (colonPos < pair.length - 1)) {
+ var key = pair.substring(0, colonPos);
+ var value = pair.substring(colonPos + 1);
+ result.push({ 'key': restoreTokens(key, tokens), 'value': restoreTokens(value, tokens) });
+ } else {
+ result.push({ 'unknown': restoreTokens(pair, tokens) });
+ }
+ }
+ return result;
+ },
+
+ insertPropertyAccessorsIntoJson: function (objectLiteralStringOrKeyValueArray) {
+ var keyValueArray = typeof objectLiteralStringOrKeyValueArray === "string"
+ ? ko.jsonExpressionRewriting.parseObjectLiteral(objectLiteralStringOrKeyValueArray)
+ : objectLiteralStringOrKeyValueArray;
+ var resultStrings = [], propertyAccessorResultStrings = [];
+
+ var keyValueEntry;
+ for (var i = 0; keyValueEntry = keyValueArray[i]; i++) {
+ if (resultStrings.length > 0)
+ resultStrings.push(",");
+
+ if (keyValueEntry['key']) {
+ var quotedKey = ensureQuoted(keyValueEntry['key']), val = keyValueEntry['value'];
+ resultStrings.push(quotedKey);
+ resultStrings.push(":");
+ resultStrings.push(val);
+
+ if (isWriteableValue(ko.utils.stringTrim(val))) {
+ if (propertyAccessorResultStrings.length > 0)
+ propertyAccessorResultStrings.push(", ");
+ propertyAccessorResultStrings.push(quotedKey + " : function(__ko_value) { " + val + " = __ko_value; }");
+ }
+ } else if (keyValueEntry['unknown']) {
+ resultStrings.push(keyValueEntry['unknown']);
+ }
+ }
+
+ var combinedResult = resultStrings.join("");
+ if (propertyAccessorResultStrings.length > 0) {
+ var allPropertyAccessors = propertyAccessorResultStrings.join("");
+ combinedResult = combinedResult + ", '_ko_property_writers' : { " + allPropertyAccessors + " } ";
+ }
+
+ return combinedResult;
+ },
+
+ keyValueArrayContainsKey: function(keyValueArray, key) {
+ for (var i = 0; i < keyValueArray.length; i++)
+ if (ko.utils.stringTrim(keyValueArray[i]['key']) == key)
+ return true;
+ return false;
+ }
+ };
+})();
+
+ko.exportSymbol('ko.jsonExpressionRewriting', ko.jsonExpressionRewriting);
+ko.exportSymbol('ko.jsonExpressionRewriting.bindingRewriteValidators', ko.jsonExpressionRewriting.bindingRewriteValidators);
+ko.exportSymbol('ko.jsonExpressionRewriting.parseObjectLiteral', ko.jsonExpressionRewriting.parseObjectLiteral);
+ko.exportSymbol('ko.jsonExpressionRewriting.insertPropertyAccessorsIntoJson', ko.jsonExpressionRewriting.insertPropertyAccessorsIntoJson);
+(function() {
+ // "Virtual elements" is an abstraction on top of the usual DOM API which understands the notion that comment nodes
+ // may be used to represent hierarchy (in addition to the DOM's natural hierarchy).
+ // If you call the DOM-manipulating functions on ko.virtualElements, you will be able to read and write the state
+ // of that virtual hierarchy
+ //
+ // The point of all this is to support containerless templates (e.g., <!-- ko foreach:someCollection -->blah<!-- /ko -->)
+ // without having to scatter special cases all over the binding and templating code.
+
+ // IE 9 cannot reliably read the "nodeValue" property of a comment node (see https://github.com/SteveSanderson/knockout/issues/186)
+ // but it does give them a nonstandard alternative property called "text" that it can read reliably. Other browsers don't have that property.
+ // So, use node.text where available, and node.nodeValue elsewhere
+ var commentNodesHaveTextProperty = document.createComment("test").text === "<!--test-->";
+
+ var startCommentRegex = commentNodesHaveTextProperty ? /^<!--\s*ko\s+(.*\:.*)\s*-->$/ : /^\s*ko\s+(.*\:.*)\s*$/;
+ var endCommentRegex = commentNodesHaveTextProperty ? /^<!--\s*\/ko\s*-->$/ : /^\s*\/ko\s*$/;
+ var htmlTagsWithOptionallyClosingChildren = { 'ul': true, 'ol': true };
+
+ function isStartComment(node) {
+ return (node.nodeType == 8) && (commentNodesHaveTextProperty ? node.text : node.nodeValue).match(startCommentRegex);
+ }
+
+ function isEndComment(node) {
+ return (node.nodeType == 8) && (commentNodesHaveTextProperty ? node.text : node.nodeValue).match(endCommentRegex);
+ }
+
+ function getVirtualChildren(startComment, allowUnbalanced) {
+ var currentNode = startComment;
+ var depth = 1;
+ var children = [];
+ while (currentNode = currentNode.nextSibling) {
+ if (isEndComment(currentNode)) {
+ depth--;
+ if (depth === 0)
+ return children;
+ }
+
+ children.push(currentNode);
+
+ if (isStartComment(currentNode))
+ depth++;
+ }
+ if (!allowUnbalanced)
+ throw new Error("Cannot find closing comment tag to match: " + startComment.nodeValue);
+ return null;
+ }
+
+ function getMatchingEndComment(startComment, allowUnbalanced) {
+ var allVirtualChildren = getVirtualChildren(startComment, allowUnbalanced);
+ if (allVirtualChildren) {
+ if (allVirtualChildren.length > 0)
+ return allVirtualChildren[allVirtualChildren.length - 1].nextSibling;
+ return startComment.nextSibling;
+ } else
+ return null; // Must have no matching end comment, and allowUnbalanced is true
+ }
+
+ function nodeArrayToText(nodeArray, cleanNodes) {
+ var texts = [];
+ for (var i = 0, j = nodeArray.length; i < j; i++) {
+ if (cleanNodes)
+ ko.utils.domNodeDisposal.cleanNode(nodeArray[i]);
+ texts.push(ko.utils.outerHTML(nodeArray[i]));
+ }
+ return String.prototype.concat.apply("", texts);
+ }
+
+ function getUnbalancedChildTags(node) {
+ // e.g., from <div>OK</div><!-- ko blah --><span>Another</span>, returns: <!-- ko blah --><span>Another</span>
+ // from <div>OK</div><!-- /ko --><!-- /ko -->, returns: <!-- /ko --><!-- /ko -->
+ var childNode = node.firstChild, captureRemaining = null;
+ if (childNode) {
+ do {
+ if (captureRemaining) // We already hit an unbalanced node and are now just scooping up all subsequent nodes
+ captureRemaining.push(childNode);
+ else if (isStartComment(childNode)) {
+ var matchingEndComment = getMatchingEndComment(childNode, /* allowUnbalanced: */ true);
+ if (matchingEndComment) // It's a balanced tag, so skip immediately to the end of this virtual set
+ childNode = matchingEndComment;
+ else
+ captureRemaining = [childNode]; // It's unbalanced, so start capturing from this point
+ } else if (isEndComment(childNode)) {
+ captureRemaining = [childNode]; // It's unbalanced (if it wasn't, we'd have skipped over it already), so start capturing
+ }
+ } while (childNode = childNode.nextSibling);
+ }
+ return captureRemaining;
+ }
+
+ ko.virtualElements = {
+ allowedBindings: {},
+
+ childNodes: function(node) {
+ return isStartComment(node) ? getVirtualChildren(node) : node.childNodes;
+ },
+
+ emptyNode: function(node) {
+ if (!isStartComment(node))
+ ko.utils.emptyDomNode(node);
+ else {
+ var virtualChildren = ko.virtualElements.childNodes(node);
+ for (var i = 0, j = virtualChildren.length; i < j; i++)
+ ko.removeNode(virtualChildren[i]);
+ }
+ },
+
+ setDomNodeChildren: function(node, childNodes) {
+ if (!isStartComment(node))
+ ko.utils.setDomNodeChildren(node, childNodes);
+ else {
+ ko.virtualElements.emptyNode(node);
+ var endCommentNode = node.nextSibling; // Must be the next sibling, as we just emptied the children
+ for (var i = 0, j = childNodes.length; i < j; i++)
+ endCommentNode.parentNode.insertBefore(childNodes[i], endCommentNode);
+ }
+ },
+
+ prepend: function(containerNode, nodeToPrepend) {
+ if (!isStartComment(containerNode)) {
+ if (containerNode.firstChild)
+ containerNode.insertBefore(nodeToPrepend, containerNode.firstChild);
+ else
+ containerNode.appendChild(nodeToPrepend);
+ } else {
+ // Start comments must always have a parent and at least one following sibling (the end comment)
+ containerNode.parentNode.insertBefore(nodeToPrepend, containerNode.nextSibling);
+ }
+ },
+
+ insertAfter: function(containerNode, nodeToInsert, insertAfterNode) {
+ if (!isStartComment(containerNode)) {
+ // Insert after insertion point
+ if (insertAfterNode.nextSibling)
+ containerNode.insertBefore(nodeToInsert, insertAfterNode.nextSibling);
+ else
+ containerNode.appendChild(nodeToInsert);
+ } else {
+ // Children of start comments must always have a parent and at least one following sibling (the end comment)
+ containerNode.parentNode.insertBefore(nodeToInsert, insertAfterNode.nextSibling);
+ }
+ },
+
+ nextSibling: function(node) {
+ if (!isStartComment(node)) {
+ if (node.nextSibling && isEndComment(node.nextSibling))
+ return undefined;
+ return node.nextSibling;
+ } else {
+ return getMatchingEndComment(node).nextSibling;
+ }
+ },
+
+ virtualNodeBindingValue: function(node) {
+ var regexMatch = isStartComment(node);
+ return regexMatch ? regexMatch[1] : null;
+ },
+
+ extractAnonymousTemplateIfVirtualElement: function(node) {
+ if (ko.virtualElements.virtualNodeBindingValue(node)) {
+ // Empty out the virtual children, and associate "node" with an anonymous template matching its previous virtual children
+ var virtualChildren = ko.virtualElements.childNodes(node);
+ var anonymousTemplateText = nodeArrayToText(virtualChildren, true);
+ ko.virtualElements.emptyNode(node);
+ new ko.templateSources.anonymousTemplate(node).text(anonymousTemplateText);
+ }
+ },
+
+ normaliseVirtualElementDomStructure: function(elementVerified) {
+ // Workaround for https://github.com/SteveSanderson/knockout/issues/155
+ // (IE <= 8 or IE 9 quirks mode parses your HTML weirdly, treating closing </li> tags as if they don't exist, thereby moving comment nodes
+ // that are direct descendants of <ul> into the preceding <li>)
+ if (!htmlTagsWithOptionallyClosingChildren[elementVerified.tagName.toLowerCase()])
+ return;
+
+ // Scan immediate children to see if they contain unbalanced comment tags. If they do, those comment tags
+ // must be intended to appear *after* that child, so move them there.
+ var childNode = elementVerified.firstChild;
+ if (childNode) {
+ do {
+ if (childNode.nodeType === 1) {
+ var unbalancedTags = getUnbalancedChildTags(childNode);
+ if (unbalancedTags) {
+ // Fix up the DOM by moving the unbalanced tags to where they most likely were intended to be placed - *after* the child
+ var nodeToInsertBefore = childNode.nextSibling;
+ for (var i = 0; i < unbalancedTags.length; i++) {
+ if (nodeToInsertBefore)
+ elementVerified.insertBefore(unbalancedTags[i], nodeToInsertBefore);
+ else
+ elementVerified.appendChild(unbalancedTags[i]);
+ }
+ }
+ }
+ } while (childNode = childNode.nextSibling);
+ }
+ }
+ };
+})();
+(function() {
+ var defaultBindingAttributeName = "data-bind";
+
+ ko.bindingProvider = function() { };
+
+ ko.utils.extend(ko.bindingProvider.prototype, {
+ 'nodeHasBindings': function(node) {
+ switch (node.nodeType) {
+ case 1: return node.getAttribute(defaultBindingAttributeName) != null; // Element
+ case 8: return ko.virtualElements.virtualNodeBindingValue(node) != null; // Comment node
+ default: return false;
+ }
+ },
+
+ 'getBindings': function(node, bindingContext) {
+ var bindingsString = this['getBindingsString'](node, bindingContext);
+ return bindingsString ? this['parseBindingsString'](bindingsString, bindingContext) : null;
+ },
+
+ // The following function is only used internally by this default provider.
+ // It's not part of the interface definition for a general binding provider.
+ 'getBindingsString': function(node, bindingContext) {
+ switch (node.nodeType) {
+ case 1: return node.getAttribute(defaultBindingAttributeName); // Element
+ case 8: return ko.virtualElements.virtualNodeBindingValue(node); // Comment node
+ default: return null;
+ }
+ },
+
+ // The following function is only used internally by this default provider.
+ // It's not part of the interface definition for a general binding provider.
+ 'parseBindingsString': function(bindingsString, bindingContext) {
+ try {
+ var viewModel = bindingContext['$data'];
+ var rewrittenBindings = " { " + ko.jsonExpressionRewriting.insertPropertyAccessorsIntoJson(bindingsString) + " } ";
+ return ko.utils.evalWithinScope(rewrittenBindings, viewModel === null ? window : viewModel, bindingContext);
+ } catch (ex) {
+ throw new Error("Unable to parse bindings.\nMessage: " + ex + ";\nBindings value: " + bindingsString);
+ }
+ }
+ });
+
+ ko.bindingProvider['instance'] = new ko.bindingProvider();
+})();
+
+ko.exportSymbol('ko.bindingProvider', ko.bindingProvider);(function () {
+ ko.bindingHandlers = {};
+
+ ko.bindingContext = function(dataItem, parentBindingContext) {
+ this['$data'] = dataItem;
+ if (parentBindingContext) {
+ this['$parent'] = parentBindingContext['$data'];
+ this['$parents'] = (parentBindingContext['$parents'] || []).slice(0);
+ this['$parents'].unshift(this['$parent']);
+ this['$root'] = parentBindingContext['$root'];
+ } else {
+ this['$parents'] = [];
+ this['$root'] = dataItem;
+ }
+ }
+ ko.bindingContext.prototype['createChildContext'] = function (dataItem) {
+ return new ko.bindingContext(dataItem, this);
+ };
+
+ function validateThatBindingIsAllowedForVirtualElements(bindingName) {
+ var validator = ko.virtualElements.allowedBindings[bindingName];
+ if (!validator)
+ throw new Error("The binding '" + bindingName + "' cannot be used with virtual elements")
+ }
+
+ function applyBindingsToDescendantsInternal (viewModel, elementVerified) {
+ var currentChild, nextInQueue = elementVerified.childNodes[0];
+ while (currentChild = nextInQueue) {
+ // Keep a record of the next child *before* applying bindings, in case the binding removes the current child from its position
+ nextInQueue = ko.virtualElements.nextSibling(currentChild);
+ applyBindingsToNodeAndDescendantsInternal(viewModel, currentChild, false);
+ }
+ }
+
+ function applyBindingsToNodeAndDescendantsInternal (viewModel, nodeVerified, isRootNodeForBindingContext) {
+ var shouldBindDescendants = true;
+
+ // Perf optimisation: Apply bindings only if...
+ // (1) It's a root element for this binding context, as we will need to store the binding context on this node
+ // Note that we can't store binding contexts on non-elements (e.g., text nodes), as IE doesn't allow expando properties for those
+ // (2) It might have bindings (e.g., it has a data-bind attribute, or it's a marker for a containerless template)
+ var isElement = (nodeVerified.nodeType == 1);
+ if (isElement) // Workaround IE <= 8 HTML parsing weirdness
+ ko.virtualElements.normaliseVirtualElementDomStructure(nodeVerified);
+
+ var shouldApplyBindings = (isElement && isRootNodeForBindingContext) // Case (1)
+ || ko.bindingProvider['instance']['nodeHasBindings'](nodeVerified); // Case (2)
+ if (shouldApplyBindings)
+ shouldBindDescendants = applyBindingsToNodeInternal(nodeVerified, null, viewModel, isRootNodeForBindingContext).shouldBindDescendants;
+
+ if (isElement && shouldBindDescendants)
+ applyBindingsToDescendantsInternal(viewModel, nodeVerified);
+ }
+
+ function applyBindingsToNodeInternal (node, bindings, viewModelOrBindingContext, isRootNodeForBindingContext) {
+ // Need to be sure that inits are only run once, and updates never run until all the inits have been run
+ var initPhase = 0; // 0 = before all inits, 1 = during inits, 2 = after all inits
+
+ // Pre-process any anonymous template bounded by comment nodes
+ ko.virtualElements.extractAnonymousTemplateIfVirtualElement(node);
+
+ // Each time the dependentObservable is evaluated (after data changes),
+ // the binding attribute is reparsed so that it can pick out the correct
+ // model properties in the context of the changed data.
+ // DOM event callbacks need to be able to access this changed data,
+ // so we need a single parsedBindings variable (shared by all callbacks
+ // associated with this node's bindings) that all the closures can access.
+ var parsedBindings;
+ function makeValueAccessor(bindingKey) {
+ return function () { return parsedBindings[bindingKey] }
+ }
+ function parsedBindingsAccessor() {
+ return parsedBindings;
+ }
+
+ var bindingHandlerThatControlsDescendantBindings;
+ new ko.dependentObservable(
+ function () {
+ // Ensure we have a nonnull binding context to work with
+ var bindingContextInstance = viewModelOrBindingContext && (viewModelOrBindingContext instanceof ko.bindingContext)
+ ? viewModelOrBindingContext
+ : new ko.bindingContext(ko.utils.unwrapObservable(viewModelOrBindingContext));
+ var viewModel = bindingContextInstance['$data'];
+
+ // We only need to store the bindingContext at the root of the subtree where it applies
+ // as all descendants will be able to find it by scanning up their ancestry
+ if (isRootNodeForBindingContext)
+ ko.storedBindingContextForNode(node, bindingContextInstance);
+
+ // Use evaluatedBindings if given, otherwise fall back on asking the bindings provider to give us some bindings
+ var evaluatedBindings = (typeof bindings == "function") ? bindings() : bindings;
+ parsedBindings = evaluatedBindings || ko.bindingProvider['instance']['getBindings'](node, bindingContextInstance);
+
+ if (parsedBindings) {
+ // First run all the inits, so bindings can register for notification on changes
+ if (initPhase === 0) {
+ initPhase = 1;
+ for (var bindingKey in parsedBindings) {
+ var binding = ko.bindingHandlers[bindingKey];
+ if (binding && node.nodeType === 8)
+ validateThatBindingIsAllowedForVirtualElements(bindingKey);
+
+ if (binding && typeof binding["init"] == "function") {
+ var handlerInitFn = binding["init"];
+ var initResult = handlerInitFn(node, makeValueAccessor(bindingKey), parsedBindingsAccessor, viewModel, bindingContextInstance);
+
+ // If this binding handler claims to control descendant bindings, make a note of this
+ if (initResult && initResult['controlsDescendantBindings']) {
+ if (bindingHandlerThatControlsDescendantBindings !== undefined)
+ throw new Error("Multiple bindings (" + bindingHandlerThatControlsDescendantBindings + " and " + bindingKey + ") are trying to control descendant bindings of the same element. You cannot use these bindings together on the same element.");
+ bindingHandlerThatControlsDescendantBindings = bindingKey;
+ }
+ }
+ }
+ initPhase = 2;
+ }
+
+ // ... then run all the updates, which might trigger changes even on the first evaluation
+ if (initPhase === 2) {
+ for (var bindingKey in parsedBindings) {
+ var binding = ko.bindingHandlers[bindingKey];
+ if (binding && typeof binding["update"] == "function") {
+ var handlerUpdateFn = binding["update"];
+ handlerUpdateFn(node, makeValueAccessor(bindingKey), parsedBindingsAccessor, viewModel, bindingContextInstance);
+ }
+ }
+ }
+ }
+ },
+ null,
+ { 'disposeWhenNodeIsRemoved' : node }
+ );
+
+ return {
+ shouldBindDescendants: bindingHandlerThatControlsDescendantBindings === undefined
+ };
+ };
+
+ var storedBindingContextDomDataKey = "__ko_bindingContext__";
+ ko.storedBindingContextForNode = function (node, bindingContext) {
+ if (arguments.length == 2)
+ ko.utils.domData.set(node, storedBindingContextDomDataKey, bindingContext);
+ else
+ return ko.utils.domData.get(node, storedBindingContextDomDataKey);
+ }
+
+ ko.applyBindingsToNode = function (node, bindings, viewModel) {
+ if (node.nodeType === 1) // If it's an element, workaround IE <= 8 HTML parsing weirdness
+ ko.virtualElements.normaliseVirtualElementDomStructure(node);
+ return applyBindingsToNodeInternal(node, bindings, viewModel, true);
+ };
+
+ ko.applyBindingsToDescendants = function(viewModel, rootNode) {
+ if (rootNode.nodeType === 1)
+ applyBindingsToDescendantsInternal(viewModel, rootNode);
+ };
+
+ ko.applyBindings = function (viewModel, rootNode) {
+ if (rootNode && (rootNode.nodeType !== 1) && (rootNode.nodeType !== 8))
+ throw new Error("ko.applyBindings: first parameter should be your view model; second parameter should be a DOM node");
+ rootNode = rootNode || window.document.body; // Make "rootNode" parameter optional
+
+ applyBindingsToNodeAndDescendantsInternal(viewModel, rootNode, true);
+ };
+
+ // Retrieving binding context from arbitrary nodes
+ ko.contextFor = function(node) {
+ // We can only do something meaningful for elements and comment nodes (in particular, not text nodes, as IE can't store domdata for them)
+ switch (node.nodeType) {
+ case 1:
+ case 8:
+ var context = ko.storedBindingContextForNode(node);
+ if (context) return context;
+ if (node.parentNode) return ko.contextFor(node.parentNode);
+ break;
+ }
+ return undefined;
+ };
+ ko.dataFor = function(node) {
+ var context = ko.contextFor(node);
+ return context ? context['$data'] : undefined;
+ };
+
+ ko.exportSymbol('ko.bindingHandlers', ko.bindingHandlers);
+ ko.exportSymbol('ko.applyBindings', ko.applyBindings);
+ ko.exportSymbol('ko.applyBindingsToDescendants', ko.applyBindingsToDescendants);
+ ko.exportSymbol('ko.applyBindingsToNode', ko.applyBindingsToNode);
+ ko.exportSymbol('ko.contextFor', ko.contextFor);
+ ko.exportSymbol('ko.dataFor', ko.dataFor);
+})();// For certain common events (currently just 'click'), allow a simplified data-binding syntax
+// e.g. click:handler instead of the usual full-length event:{click:handler}
+var eventHandlersWithShortcuts = ['click'];
+ko.utils.arrayForEach(eventHandlersWithShortcuts, function(eventName) {
+ ko.bindingHandlers[eventName] = {
+ 'init': function(element, valueAccessor, allBindingsAccessor, viewModel) {
+ var newValueAccessor = function () {
+ var result = {};
+ result[eventName] = valueAccessor();
+ return result;
+ };
+ return ko.bindingHandlers['event']['init'].call(this, element, newValueAccessor, allBindingsAccessor, viewModel);
+ }
+ }
+});
+
+
+ko.bindingHandlers['event'] = {
+ 'init' : function (element, valueAccessor, allBindingsAccessor, viewModel) {
+ var eventsToHandle = valueAccessor() || {};
+ for(var eventNameOutsideClosure in eventsToHandle) {
+ (function() {
+ var eventName = eventNameOutsideClosure; // Separate variable to be captured by event handler closure
+ if (typeof eventName == "string") {
+ ko.utils.registerEventHandler(element, eventName, function (event) {
+ var handlerReturnValue;
+ var handlerFunction = valueAccessor()[eventName];
+ if (!handlerFunction)
+ return;
+ var allBindings = allBindingsAccessor();
+
+ try {
+ // Take all the event args, and prefix with the viewmodel
+ var argsForHandler = ko.utils.makeArray(arguments);
+ argsForHandler.unshift(viewModel);
+ handlerReturnValue = handlerFunction.apply(viewModel, argsForHandler);
+ } finally {
+ if (handlerReturnValue !== true) { // Normally we want to prevent default action. Developer can override this be explicitly returning true.
+ if (event.preventDefault)
+ event.preventDefault();
+ else
+ event.returnValue = false;
+ }
+ }
+
+ var bubble = allBindings[eventName + 'Bubble'] !== false;
+ if (!bubble) {
+ event.cancelBubble = true;
+ if (event.stopPropagation)
+ event.stopPropagation();
+ }
+ });
+ }
+ })();
+ }
+ }
+};
+
+ko.bindingHandlers['submit'] = {
+ 'init': function (element, valueAccessor, allBindingsAccessor, viewModel) {
+ if (typeof valueAccessor() != "function")
+ throw new Error("The value for a submit binding must be a function");
+ ko.utils.registerEventHandler(element, "submit", function (event) {
+ var handlerReturnValue;
+ var value = valueAccessor();
+ try { handlerReturnValue = value.call(viewModel, element); }
+ finally {
+ if (handlerReturnValue !== true) { // Normally we want to prevent default action. Developer can override this be explicitly returning true.
+ if (event.preventDefault)
+ event.preventDefault();
+ else
+ event.returnValue = false;
+ }
+ }
+ });
+ }
+};
+
+ko.bindingHandlers['visible'] = {
+ 'update': function (element, valueAccessor) {
+ var value = ko.utils.unwrapObservable(valueAccessor());
+ var isCurrentlyVisible = !(element.style.display == "none");
+ if (value && !isCurrentlyVisible)
+ element.style.display = "";
+ else if ((!value) && isCurrentlyVisible)
+ element.style.display = "none";
+ }
+}
+
+ko.bindingHandlers['enable'] = {
+ 'update': function (element, valueAccessor) {
+ var value = ko.utils.unwrapObservable(valueAccessor());
+ if (value && element.disabled)
+ element.removeAttribute("disabled");
+ else if ((!value) && (!element.disabled))
+ element.disabled = true;
+ }
+};
+
+ko.bindingHandlers['disable'] = {
+ 'update': function (element, valueAccessor) {
+ ko.bindingHandlers['enable']['update'](element, function() { return !ko.utils.unwrapObservable(valueAccessor()) });
+ }
+};
+
+function ensureDropdownSelectionIsConsistentWithModelValue(element, modelValue, preferModelValue) {
+ if (preferModelValue) {
+ if (modelValue !== ko.selectExtensions.readValue(element))
+ ko.selectExtensions.writeValue(element, modelValue);
+ }
+
+ // No matter which direction we're syncing in, we want the end result to be equality between dropdown value and model value.
+ // If they aren't equal, either we prefer the dropdown value, or the model value couldn't be represented, so either way,
+ // change the model value to match the dropdown.
+ if (modelValue !== ko.selectExtensions.readValue(element))
+ ko.utils.triggerEvent(element, "change");
+};
+
+ko.bindingHandlers['value'] = {
+ 'init': function (element, valueAccessor, allBindingsAccessor) {
+ // Always catch "change" event; possibly other events too if asked
+ var eventsToCatch = ["change"];
+ var requestedEventsToCatch = allBindingsAccessor()["valueUpdate"];
+ if (requestedEventsToCatch) {
+ if (typeof requestedEventsToCatch == "string") // Allow both individual event names, and arrays of event names
+ requestedEventsToCatch = [requestedEventsToCatch];
+ ko.utils.arrayPushAll(eventsToCatch, requestedEventsToCatch);
+ eventsToCatch = ko.utils.arrayGetDistinctValues(eventsToCatch);
+ }
+
+ ko.utils.arrayForEach(eventsToCatch, function(eventName) {
+ // The syntax "after<eventname>" means "run the handler asynchronously after the event"
+ // This is useful, for example, to catch "keydown" events after the browser has updated the control
+ // (otherwise, ko.selectExtensions.readValue(this) will receive the control's value *before* the key event)
+ var handleEventAsynchronously = false;
+ if (ko.utils.stringStartsWith(eventName, "after")) {
+ handleEventAsynchronously = true;
+ eventName = eventName.substring("after".length);
+ }
+ var runEventHandler = handleEventAsynchronously ? function(handler) { setTimeout(handler, 0) }
+ : function(handler) { handler() };
+
+ ko.utils.registerEventHandler(element, eventName, function () {
+ runEventHandler(function() {
+ var modelValue = valueAccessor();
+ var elementValue = ko.selectExtensions.readValue(element);
+ if (ko.isWriteableObservable(modelValue))
+ modelValue(elementValue);
+ else {
+ var allBindings = allBindingsAccessor();
+ if (allBindings['_ko_property_writers'] && allBindings['_ko_property_writers']['value'])
+ allBindings['_ko_property_writers']['value'](elementValue);
+ }
+ });
+ });
+ });
+ },
+ 'update': function (element, valueAccessor) {
+ var newValue = ko.utils.unwrapObservable(valueAccessor());
+ var elementValue = ko.selectExtensions.readValue(element);
+ var valueHasChanged = (newValue != elementValue);
+
+ // JavaScript's 0 == "" behavious is unfortunate here as it prevents writing 0 to an empty text box (loose equality suggests the values are the same).
+ // We don't want to do a strict equality comparison as that is more confusing for developers in certain cases, so we specifically special case 0 != "" here.
+ if ((newValue === 0) && (elementValue !== 0) && (elementValue !== "0"))
+ valueHasChanged = true;
+
+ if (valueHasChanged) {
+ var applyValueAction = function () { ko.selectExtensions.writeValue(element, newValue); };
+ applyValueAction();
+
+ // Workaround for IE6 bug: It won't reliably apply values to SELECT nodes during the same execution thread
+ // right after you've changed the set of OPTION nodes on it. So for that node type, we'll schedule a second thread
+ // to apply the value as well.
+ var alsoApplyAsynchronously = element.tagName == "SELECT";
+ if (alsoApplyAsynchronously)
+ setTimeout(applyValueAction, 0);
+ }
+
+ // If you try to set a model value that can't be represented in an already-populated dropdown, reject that change,
+ // because you're not allowed to have a model value that disagrees with a visible UI selection.
+ if ((element.tagName == "SELECT") && (element.length > 0))
+ ensureDropdownSelectionIsConsistentWithModelValue(element, newValue, /* preferModelValue */ false);
+ }
+};
+
+ko.bindingHandlers['options'] = {
+ 'update': function (element, valueAccessor, allBindingsAccessor) {
+ if (element.tagName != "SELECT")
+ throw new Error("options binding applies only to SELECT elements");
+
+ var selectWasPreviouslyEmpty = element.length == 0;
+ var previousSelectedValues = ko.utils.arrayMap(ko.utils.arrayFilter(element.childNodes, function (node) {
+ return node.tagName && node.tagName == "OPTION" && node.selected;
+ }), function (node) {
+ return ko.selectExtensions.readValue(node) || node.innerText || node.textContent;
+ });
+ var previousScrollTop = element.scrollTop;
+ element.scrollTop = 0; // Workaround for a Chrome rendering bug. Note that we restore the scroll position later. (https://github.com/SteveSanderson/knockout/issues/215)
+
+ var value = ko.utils.unwrapObservable(valueAccessor());
+ var selectedValue = element.value;
+
+ // Remove all existing <option>s.
+ // Need to use .remove() rather than .removeChild() for <option>s otherwise IE behaves oddly (https://github.com/SteveSanderson/knockout/issues/134)
+ while (element.length > 0) {
+ ko.cleanNode(element.options[0]);
+ element.remove(0);
+ }
+
+ if (value) {
+ var allBindings = allBindingsAccessor();
+ if (typeof value.length != "number")
+ value = [value];
+ if (allBindings['optionsCaption']) {
+ var option = document.createElement("OPTION");
+ ko.utils.setHtml(option, allBindings['optionsCaption']);
+ ko.selectExtensions.writeValue(option, undefined);
+ element.appendChild(option);
+ }
+ for (var i = 0, j = value.length; i < j; i++) {
+ var option = document.createElement("OPTION");
+
+ // Apply a value to the option element
+ var optionValue = typeof allBindings['optionsValue'] == "string" ? value[i][allBindings['optionsValue']] : value[i];
+ optionValue = ko.utils.unwrapObservable(optionValue);
+ ko.selectExtensions.writeValue(option, optionValue);
+
+ // Apply some text to the option element
+ var optionsTextValue = allBindings['optionsText'];
+ var optionText;
+ if (typeof optionsTextValue == "function")
+ optionText = optionsTextValue(value[i]); // Given a function; run it against the data value
+ else if (typeof optionsTextValue == "string")
+ optionText = value[i][optionsTextValue]; // Given a string; treat it as a property name on the data value
+ else
+ optionText = optionValue; // Given no optionsText arg; use the data value itself
+ if ((optionText === null) || (optionText === undefined))
+ optionText = "";
+
+ ko.utils.setTextContent(option, optionText);
+
+ element.appendChild(option);
+ }
+
+ // IE6 doesn't like us to assign selection to OPTION nodes before they're added to the document.
+ // That's why we first added them without selection. Now it's time to set the selection.
+ var newOptions = element.getElementsByTagName("OPTION");
+ var countSelectionsRetained = 0;
+ for (var i = 0, j = newOptions.length; i < j; i++) {
+ if (ko.utils.arrayIndexOf(previousSelectedValues, ko.selectExtensions.readValue(newOptions[i])) >= 0) {
+ ko.utils.setOptionNodeSelectionState(newOptions[i], true);
+ countSelectionsRetained++;
+ }
+ }
+
+ if (previousScrollTop)
+ element.scrollTop = previousScrollTop;
+
+ if (selectWasPreviouslyEmpty && ('value' in allBindings)) {
+ // Ensure consistency between model value and selected option.
+ // If the dropdown is being populated for the first time here (or was otherwise previously empty),
+ // the dropdown selection state is meaningless, so we preserve the model value.
+ ensureDropdownSelectionIsConsistentWithModelValue(element, ko.utils.unwrapObservable(allBindings['value']), /* preferModelValue */ true);
+ }
+ }
+ }
+};
+ko.bindingHandlers['options'].optionValueDomDataKey = '__ko.optionValueDomData__';
+
+ko.bindingHandlers['selectedOptions'] = {
+ getSelectedValuesFromSelectNode: function (selectNode) {
+ var result = [];
+ var nodes = selectNode.childNodes;
+ for (var i = 0, j = nodes.length; i < j; i++) {
+ var node = nodes[i];
+ if ((node.tagName == "OPTION") && node.selected)
+ result.push(ko.selectExtensions.readValue(node));
+ }
+ return result;
+ },
+ 'init': function (element, valueAccessor, allBindingsAccessor) {
+ ko.utils.registerEventHandler(element, "change", function () {
+ var value = valueAccessor();
+ if (ko.isWriteableObservable(value))
+ value(ko.bindingHandlers['selectedOptions'].getSelectedValuesFromSelectNode(this));
+ else {
+ var allBindings = allBindingsAccessor();
+ if (allBindings['_ko_property_writers'] && allBindings['_ko_property_writers']['value'])
+ allBindings['_ko_property_writers']['value'](ko.bindingHandlers['selectedOptions'].getSelectedValuesFromSelectNode(this));
+ }
+ });
+ },
+ 'update': function (element, valueAccessor) {
+ if (element.tagName != "SELECT")
+ throw new Error("values binding applies only to SELECT elements");
+
+ var newValue = ko.utils.unwrapObservable(valueAccessor());
+ if (newValue && typeof newValue.length == "number") {
+ var nodes = element.childNodes;
+ for (var i = 0, j = nodes.length; i < j; i++) {
+ var node = nodes[i];
+ if (node.tagName == "OPTION")
+ ko.utils.setOptionNodeSelectionState(node, ko.utils.arrayIndexOf(newValue, ko.selectExtensions.readValue(node)) >= 0);
+ }
+ }
+ }
+};
+
+ko.bindingHandlers['text'] = {
+ 'update': function (element, valueAccessor) {
+ ko.utils.setTextContent(element, valueAccessor());
+ }
+};
+
+ko.bindingHandlers['html'] = {
+ 'init': function() {
+ // Prevent binding on the dynamically-injected HTML (as developers are unlikely to expect that, and it has security implications)
+ return { 'controlsDescendantBindings': true };
+ },
+ 'update': function (element, valueAccessor) {
+ var value = ko.utils.unwrapObservable(valueAccessor());
+ ko.utils.setHtml(element, value);
+ }
+};
+
+ko.bindingHandlers['css'] = {
+ 'update': function (element, valueAccessor) {
+ var value = ko.utils.unwrapObservable(valueAccessor() || {});
+ for (var className in value) {
+ if (typeof className == "string") {
+ var shouldHaveClass = ko.utils.unwrapObservable(value[className]);
+ ko.utils.toggleDomNodeCssClass(element, className, shouldHaveClass);
+ }
+ }
+ }
+};
+
+ko.bindingHandlers['style'] = {
+ 'update': function (element, valueAccessor) {
+ var value = ko.utils.unwrapObservable(valueAccessor() || {});
+ for (var styleName in value) {
+ if (typeof styleName == "string") {
+ var styleValue = ko.utils.unwrapObservable(value[styleName]);
+ element.style[styleName] = styleValue || ""; // Empty string removes the value, whereas null/undefined have no effect
+ }
+ }
+ }
+};
+
+ko.bindingHandlers['uniqueName'] = {
+ 'init': function (element, valueAccessor) {
+ if (valueAccessor()) {
+ element.name = "ko_unique_" + (++ko.bindingHandlers['uniqueName'].currentIndex);
+
+ // Workaround IE 6/7 issue
+ // - https://github.com/SteveSanderson/knockout/issues/197
+ // - http://www.matts411.com/post/setting_the_name_attribute_in_ie_dom/
+ if (ko.utils.isIe6 || ko.utils.isIe7)
+ element.mergeAttributes(document.createElement("<input name='" + element.name + "'/>"), false);
+ }
+ }
+};
+ko.bindingHandlers['uniqueName'].currentIndex = 0;
+
+ko.bindingHandlers['checked'] = {
+ 'init': function (element, valueAccessor, allBindingsAccessor) {
+ var updateHandler = function() {
+ var valueToWrite;
+ if (element.type == "checkbox") {
+ valueToWrite = element.checked;
+ } else if ((element.type == "radio") && (element.checked)) {
+ valueToWrite = element.value;
+ } else {
+ return; // "checked" binding only responds to checkboxes and selected radio buttons
+ }
+
+ var modelValue = valueAccessor();
+ if ((element.type == "checkbox") && (ko.utils.unwrapObservable(modelValue) instanceof Array)) {
+ // For checkboxes bound to an array, we add/remove the checkbox value to that array
+ // This works for both observable and non-observable arrays
+ var existingEntryIndex = ko.utils.arrayIndexOf(ko.utils.unwrapObservable(modelValue), element.value);
+ if (element.checked && (existingEntryIndex < 0))
+ modelValue.push(element.value);
+ else if ((!element.checked) && (existingEntryIndex >= 0))
+ modelValue.splice(existingEntryIndex, 1);
+ } else if (ko.isWriteableObservable(modelValue)) {
+ if (modelValue() !== valueToWrite) { // Suppress repeated events when there's nothing new to notify (some browsers raise them)
+ modelValue(valueToWrite);
+ }
+ } else {
+ var allBindings = allBindingsAccessor();
+ if (allBindings['_ko_property_writers'] && allBindings['_ko_property_writers']['checked']) {
+ allBindings['_ko_property_writers']['checked'](valueToWrite);
+ }
+ }
+ };
+ ko.utils.registerEventHandler(element, "click", updateHandler);
+
+ // IE 6 won't allow radio buttons to be selected unless they have a name
+ if ((element.type == "radio") && !element.name)
+ ko.bindingHandlers['uniqueName']['init'](element, function() { return true });
+ },
+ 'update': function (element, valueAccessor) {
+ var value = ko.utils.unwrapObservable(valueAccessor());
+
+ if (element.type == "checkbox") {
+ if (value instanceof Array) {
+ // When bound to an array, the checkbox being checked represents its value being present in that array
+ element.checked = ko.utils.arrayIndexOf(value, element.value) >= 0;
+ } else {
+ // When bound to anything other value (not an array), the checkbox being checked represents the value being trueish
+ element.checked = value;
+ }
+ } else if (element.type == "radio") {
+ element.checked = (element.value == value);
+ }
+ }
+};
+
+ko.bindingHandlers['attr'] = {
+ 'update': function(element, valueAccessor, allBindingsAccessor) {
+ var value = ko.utils.unwrapObservable(valueAccessor()) || {};
+ for (var attrName in value) {
+ if (typeof attrName == "string") {
+ var attrValue = ko.utils.unwrapObservable(value[attrName]);
+
+ // To cover cases like "attr: { checked:someProp }", we want to remove the attribute entirely
+ // when someProp is a "no value"-like value (strictly null, false, or undefined)
+ // (because the absence of the "checked" attr is how to mark an element as not checked, etc.)
+ if ((attrValue === false) || (attrValue === null) || (attrValue === undefined))
+ element.removeAttribute(attrName);
+ else
+ element.setAttribute(attrName, attrValue.toString());
+ }
+ }
+ }
+};
+
+ko.bindingHandlers['hasfocus'] = {
+ 'init': function(element, valueAccessor, allBindingsAccessor) {
+ var writeValue = function(valueToWrite) {
+ var modelValue = valueAccessor();
+ if (valueToWrite == ko.utils.unwrapObservable(modelValue))
+ return;
+
+ if (ko.isWriteableObservable(modelValue))
+ modelValue(valueToWrite);
+ else {
+ var allBindings = allBindingsAccessor();
+ if (allBindings['_ko_property_writers'] && allBindings['_ko_property_writers']['hasfocus']) {
+ allBindings['_ko_property_writers']['hasfocus'](valueToWrite);
+ }
+ }
+ };
+ ko.utils.registerEventHandler(element, "focus", function() { writeValue(true) });
+ ko.utils.registerEventHandler(element, "focusin", function() { writeValue(true) }); // For IE
+ ko.utils.registerEventHandler(element, "blur", function() { writeValue(false) });
+ ko.utils.registerEventHandler(element, "focusout", function() { writeValue(false) }); // For IE
+ },
+ 'update': function(element, valueAccessor) {
+ var value = ko.utils.unwrapObservable(valueAccessor());
+ value ? element.focus() : element.blur();
+ ko.utils.triggerEvent(element, value ? "focusin" : "focusout"); // For IE, which doesn't reliably fire "focus" or "blur" events synchronously
+ }
+};
+
+// "with: someExpression" is equivalent to "template: { if: someExpression, data: someExpression }"
+ko.bindingHandlers['with'] = {
+ makeTemplateValueAccessor: function(valueAccessor) {
+ return function() { var value = valueAccessor(); return { 'if': value, 'data': value, 'templateEngine': ko.nativeTemplateEngine.instance } };
+ },
+ 'init': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
+ return ko.bindingHandlers['template']['init'](element, ko.bindingHandlers['with'].makeTemplateValueAccessor(valueAccessor));
+ },
+ 'update': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
+ return ko.bindingHandlers['template']['update'](element, ko.bindingHandlers['with'].makeTemplateValueAccessor(valueAccessor), allBindingsAccessor, viewModel, bindingContext);
+ }
+};
+ko.jsonExpressionRewriting.bindingRewriteValidators['with'] = false; // Can't rewrite control flow bindings
+ko.virtualElements.allowedBindings['with'] = true;
+
+// "if: someExpression" is equivalent to "template: { if: someExpression }"
+ko.bindingHandlers['if'] = {
+ makeTemplateValueAccessor: function(valueAccessor) {
+ return function() { return { 'if': valueAccessor(), 'templateEngine': ko.nativeTemplateEngine.instance } };
+ },
+ 'init': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
+ return ko.bindingHandlers['template']['init'](element, ko.bindingHandlers['if'].makeTemplateValueAccessor(valueAccessor));
+ },
+ 'update': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
+ return ko.bindingHandlers['template']['update'](element, ko.bindingHandlers['if'].makeTemplateValueAccessor(valueAccessor), allBindingsAccessor, viewModel, bindingContext);
+ }
+};
+ko.jsonExpressionRewriting.bindingRewriteValidators['if'] = false; // Can't rewrite control flow bindings
+ko.virtualElements.allowedBindings['if'] = true;
+
+// "ifnot: someExpression" is equivalent to "template: { ifnot: someExpression }"
+ko.bindingHandlers['ifnot'] = {
+ makeTemplateValueAccessor: function(valueAccessor) {
+ return function() { return { 'ifnot': valueAccessor(), 'templateEngine': ko.nativeTemplateEngine.instance } };
+ },
+ 'init': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
+ return ko.bindingHandlers['template']['init'](element, ko.bindingHandlers['ifnot'].makeTemplateValueAccessor(valueAccessor));
+ },
+ 'update': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
+ return ko.bindingHandlers['template']['update'](element, ko.bindingHandlers['ifnot'].makeTemplateValueAccessor(valueAccessor), allBindingsAccessor, viewModel, bindingContext);
+ }
+};
+ko.jsonExpressionRewriting.bindingRewriteValidators['ifnot'] = false; // Can't rewrite control flow bindings
+ko.virtualElements.allowedBindings['ifnot'] = true;
+
+// "foreach: someExpression" is equivalent to "template: { foreach: someExpression }"
+// "foreach: { data: someExpression, afterAdd: myfn }" is equivalent to "template: { foreach: someExpression, afterAdd: myfn }"
+ko.bindingHandlers['foreach'] = {
+ makeTemplateValueAccessor: function(valueAccessor) {
+ return function() {
+ var bindingValue = ko.utils.unwrapObservable(valueAccessor());
+
+ // If bindingValue is the array, just pass it on its own
+ if ((!bindingValue) || typeof bindingValue.length == "number")
+ return { 'foreach': bindingValue, 'templateEngine': ko.nativeTemplateEngine.instance };
+
+ // If bindingValue.data is the array, preserve all relevant options
+ return {
+ 'foreach': bindingValue['data'],
+ 'includeDestroyed': bindingValue['includeDestroyed'],
+ 'afterAdd': bindingValue['afterAdd'],
+ 'beforeRemove': bindingValue['beforeRemove'],
+ 'afterRender': bindingValue['afterRender'],
+ 'templateEngine': ko.nativeTemplateEngine.instance
+ };
+ };
+ },
+ 'init': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
+ return ko.bindingHandlers['template']['init'](element, ko.bindingHandlers['foreach'].makeTemplateValueAccessor(valueAccessor));
+ },
+ 'update': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
+ return ko.bindingHandlers['template']['update'](element, ko.bindingHandlers['foreach'].makeTemplateValueAccessor(valueAccessor), allBindingsAccessor, viewModel, bindingContext);
+ }
+};
+ko.jsonExpressionRewriting.bindingRewriteValidators['foreach'] = false; // Can't rewrite control flow bindings
+ko.virtualElements.allowedBindings['foreach'] = true;
+ko.exportSymbol('ko.allowedVirtualElementBindings', ko.virtualElements.allowedBindings);// If you want to make a custom template engine,
+//
+// [1] Inherit from this class (like ko.nativeTemplateEngine does)
+// [2] Override 'renderTemplateSource', supplying a function with this signature:
+//
+// function (templateSource, bindingContext, options) {
+// // - templateSource.text() is the text of the template you should render
+// // - bindingContext.$data is the data you should pass into the template
+// // - you might also want to make bindingContext.$parent, bindingContext.$parents,
+// // and bindingContext.$root available in the template too
+// // - options gives you access to any other properties set on "data-bind: { template: options }"
+// //
+// // Return value: an array of DOM nodes
+// }
+//
+// [3] Override 'createJavaScriptEvaluatorBlock', supplying a function with this signature:
+//
+// function (script) {
+// // Return value: Whatever syntax means "Evaluate the JavaScript statement 'script' and output the result"
+// // For example, the jquery.tmpl template engine converts 'someScript' to '${ someScript }'
+// }
+//
+// This is only necessary if you want to allow data-bind attributes to reference arbitrary template variables.
+// If you don't want to allow that, you can set the property 'allowTemplateRewriting' to false (like ko.nativeTemplateEngine does)
+// and then you don't need to override 'createJavaScriptEvaluatorBlock'.
+
+ko.templateEngine = function () { };
+
+ko.templateEngine.prototype['renderTemplateSource'] = function (templateSource, bindingContext, options) {
+ throw "Override renderTemplateSource";
+};
+
+ko.templateEngine.prototype['createJavaScriptEvaluatorBlock'] = function (script) {
+ throw "Override createJavaScriptEvaluatorBlock";
+};
+
+ko.templateEngine.prototype['makeTemplateSource'] = function(template) {
+ // Named template
+ if (typeof template == "string") {
+ var elem = document.getElementById(template);
+ if (!elem)
+ throw new Error("Cannot find template with ID " + template);
+ return new ko.templateSources.domElement(elem);
+ } else if ((template.nodeType == 1) || (template.nodeType == 8)) {
+ // Anonymous template
+ return new ko.templateSources.anonymousTemplate(template);
+ } else
+ throw new Error("Unknown template type: " + template);
+};
+
+ko.templateEngine.prototype['renderTemplate'] = function (template, bindingContext, options) {
+ var templateSource = this['makeTemplateSource'](template);
+ return this['renderTemplateSource'](templateSource, bindingContext, options);
+};
+
+ko.templateEngine.prototype['isTemplateRewritten'] = function (template) {
+ // Skip rewriting if requested
+ if (this['allowTemplateRewriting'] === false)
+ return true;
+
+ // Perf optimisation - see below
+ if (this.knownRewrittenTemplates && this.knownRewrittenTemplates[template])
+ return true;
+
+ return this['makeTemplateSource'](template)['data']("isRewritten");
+};
+
+ko.templateEngine.prototype['rewriteTemplate'] = function (template, rewriterCallback) {
+ var templateSource = this['makeTemplateSource'](template);
+ var rewritten = rewriterCallback(templateSource['text']());
+ templateSource['text'](rewritten);
+ templateSource['data']("isRewritten", true);
+
+ // Perf optimisation - for named templates, track which ones have been rewritten so we can
+ // answer 'isTemplateRewritten' *without* having to use getElementById (which is slow on IE < 8)
+ if (typeof template == "string") {
+ this.knownRewrittenTemplates = this.knownRewrittenTemplates || {};
+ this.knownRewrittenTemplates[template] = true;
+ }
+};
+
+ko.exportSymbol('ko.templateEngine', ko.templateEngine);
+ko.templateRewriting = (function () {
+ var memoizeDataBindingAttributeSyntaxRegex = /(<[a-z]+\d*(\s+(?!data-bind=)[a-z0-9\-]+(=(\"[^\"]*\"|\'[^\']*\'))?)*\s+)data-bind=(["'])([\s\S]*?)\5/gi;
+ var memoizeVirtualContainerBindingSyntaxRegex = /<!--\s*ko\b\s*([\s\S]*?)\s*-->/g;
+
+ function validateDataBindValuesForRewriting(keyValueArray) {
+ var allValidators = ko.jsonExpressionRewriting.bindingRewriteValidators;
+ for (var i = 0; i < keyValueArray.length; i++) {
+ var key = keyValueArray[i]['key'];
+ if (allValidators.hasOwnProperty(key)) {
+ var validator = allValidators[key];
+
+ if (typeof validator === "function") {
+ var possibleErrorMessage = validator(keyValueArray[i]['value']);
+ if (possibleErrorMessage)
+ throw new Error(possibleErrorMessage);
+ } else if (!validator) {
+ throw new Error("This template engine does not support the '" + key + "' binding within its templates");
+ }
+ }
+ }
+ }
+
+ function constructMemoizedTagReplacement(dataBindAttributeValue, tagToRetain, templateEngine) {
+ var dataBindKeyValueArray = ko.jsonExpressionRewriting.parseObjectLiteral(dataBindAttributeValue);
+ validateDataBindValuesForRewriting(dataBindKeyValueArray);
+ var rewrittenDataBindAttributeValue = ko.jsonExpressionRewriting.insertPropertyAccessorsIntoJson(dataBindKeyValueArray);
+
+ // For no obvious reason, Opera fails to evaluate rewrittenDataBindAttributeValue unless it's wrapped in an additional
+ // anonymous function, even though Opera's built-in debugger can evaluate it anyway. No other browser requires this
+ // extra indirection.
+ var applyBindingsToNextSiblingScript = "ko.templateRewriting.applyMemoizedBindingsToNextSibling(function() { \
+ return (function() { return { " + rewrittenDataBindAttributeValue + " } })() \
+ })";
+ return templateEngine['createJavaScriptEvaluatorBlock'](applyBindingsToNextSiblingScript) + tagToRetain;
+ }
+
+ return {
+ ensureTemplateIsRewritten: function (template, templateEngine) {
+ if (!templateEngine['isTemplateRewritten'](template))
+ templateEngine['rewriteTemplate'](template, function (htmlString) {
+ return ko.templateRewriting.memoizeBindingAttributeSyntax(htmlString, templateEngine);
+ });
+ },
+
+ memoizeBindingAttributeSyntax: function (htmlString, templateEngine) {
+ return htmlString.replace(memoizeDataBindingAttributeSyntaxRegex, function () {
+ return constructMemoizedTagReplacement(/* dataBindAttributeValue: */ arguments[6], /* tagToRetain: */ arguments[1], templateEngine);
+ }).replace(memoizeVirtualContainerBindingSyntaxRegex, function() {
+ return constructMemoizedTagReplacement(/* dataBindAttributeValue: */ arguments[1], /* tagToRetain: */ "<!-- ko -->", templateEngine);
+ });
+ },
+
+ applyMemoizedBindingsToNextSibling: function (bindings) {
+ return ko.memoization.memoize(function (domNode, bindingContext) {
+ if (domNode.nextSibling)
+ ko.applyBindingsToNode(domNode.nextSibling, bindings, bindingContext);
+ });
+ }
+ }
+})();
+
+ko.exportSymbol('ko.templateRewriting', ko.templateRewriting);
+ko.exportSymbol('ko.templateRewriting.applyMemoizedBindingsToNextSibling', ko.templateRewriting.applyMemoizedBindingsToNextSibling); // Exported only because it has to be referenced by string lookup from within rewritten template
+(function() {
+ // A template source represents a read/write way of accessing a template. This is to eliminate the need for template loading/saving
+ // logic to be duplicated in every template engine (and means they can all work with anonymous templates, etc.)
+ //
+ // Two are provided by default:
+ // 1. ko.templateSources.domElement - reads/writes the text content of an arbitrary DOM element
+ // 2. ko.templateSources.anonymousElement - uses ko.utils.domData to read/write text *associated* with the DOM element, but
+ // without reading/writing the actual element text content, since it will be overwritten
+ // with the rendered template output.
+ // You can implement your own template source if you want to fetch/store templates somewhere other than in DOM elements.
+ // Template sources need to have the following functions:
+ // text() - returns the template text from your storage location
+ // text(value) - writes the supplied template text to your storage location
+ // data(key) - reads values stored using data(key, value) - see below
+ // data(key, value) - associates "value" with this template and the key "key". Is used to store information like "isRewritten".
+ //
+ // Once you've implemented a templateSource, make your template engine use it by subclassing whatever template engine you were
+ // using and overriding "makeTemplateSource" to return an instance of your custom template source.
+
+ ko.templateSources = {};
+
+ // ---- ko.templateSources.domElement -----
+
+ ko.templateSources.domElement = function(element) {
+ this.domElement = element;
+ }
+
+ ko.templateSources.domElement.prototype['text'] = function(/* valueToWrite */) {
+ if (arguments.length == 0) {
+ return this.domElement.tagName.toLowerCase() == "script" ? this.domElement.text : this.domElement.innerHTML;
+ } else {
+ var valueToWrite = arguments[0];
+ if (this.domElement.tagName.toLowerCase() == "script")
+ this.domElement.text = valueToWrite;
+ else
+ ko.utils.setHtml(this.domElement, valueToWrite);
+ }
+ };
+
+ ko.templateSources.domElement.prototype['data'] = function(key /*, valueToWrite */) {
+ if (arguments.length === 1) {
+ return ko.utils.domData.get(this.domElement, "templateSourceData_" + key);
+ } else {
+ ko.utils.domData.set(this.domElement, "templateSourceData_" + key, arguments[1]);
+ }
+ };
+
+ // ---- ko.templateSources.anonymousTemplate -----
+
+ var anonymousTemplatesDomDataKey = "__ko_anon_template__";
+ ko.templateSources.anonymousTemplate = function(element) {
+ this.domElement = element;
+ }
+ ko.templateSources.anonymousTemplate.prototype = new ko.templateSources.domElement();
+ ko.templateSources.anonymousTemplate.prototype['text'] = function(/* valueToWrite */) {
+ if (arguments.length == 0) {
+ return ko.utils.domData.get(this.domElement, anonymousTemplatesDomDataKey);
+ } else {
+ var valueToWrite = arguments[0];
+ ko.utils.domData.set(this.domElement, anonymousTemplatesDomDataKey, valueToWrite);
+ }
+ };
+
+ ko.exportSymbol('ko.templateSources', ko.templateSources);
+ ko.exportSymbol('ko.templateSources.domElement', ko.templateSources.domElement);
+ ko.exportSymbol('ko.templateSources.anonymousTemplate', ko.templateSources.anonymousTemplate);
+})();
+(function () {
+ var _templateEngine;
+ ko.setTemplateEngine = function (templateEngine) {
+ if ((templateEngine != undefined) && !(templateEngine instanceof ko.templateEngine))
+ throw "templateEngine must inherit from ko.templateEngine";
+ _templateEngine = templateEngine;
+ }
+
+ function invokeForEachNodeOrCommentInParent(nodeArray, parent, action) {
+ for (var i = 0; node = nodeArray[i]; i++) {
+ if (node.parentNode !== parent) // Skip anything that has been removed during binding
+ continue;
+ if ((node.nodeType === 1) || (node.nodeType === 8))
+ action(node);
+ }
+ }
+
+ ko.activateBindingsOnTemplateRenderedNodes = function(nodeArray, bindingContext) {
+ // To be used on any nodes that have been rendered by a template and have been inserted into some parent element.
+ // Safely iterates through nodeArray (being tolerant of any changes made to it during binding, e.g.,
+ // if a binding inserts siblings), and for each:
+ // (1) Does a regular "applyBindings" to associate bindingContext with this node and to activate any non-memoized bindings
+ // (2) Unmemoizes any memos in the DOM subtree (e.g., to activate bindings that had been memoized during template rewriting)
+
+ var nodeArrayClone = ko.utils.arrayPushAll([], nodeArray); // So we can tolerate insertions/deletions during binding
+ var commonParentElement = (nodeArray.length > 0) ? nodeArray[0].parentNode : null; // All items must be in the same parent, so this is OK
+
+ // Need to applyBindings *before* unmemoziation, because unmemoization might introduce extra nodes (that we don't want to re-bind)
+ // whereas a regular applyBindings won't introduce new memoized nodes
+
+ invokeForEachNodeOrCommentInParent(nodeArrayClone, commonParentElement, function(node) {
+ ko.applyBindings(bindingContext, node);
+ });
+ invokeForEachNodeOrCommentInParent(nodeArrayClone, commonParentElement, function(node) {
+ ko.memoization.unmemoizeDomNodeAndDescendants(node, [bindingContext]);
+ });
+ }
+
+ function getFirstNodeFromPossibleArray(nodeOrNodeArray) {
+ return nodeOrNodeArray.nodeType ? nodeOrNodeArray
+ : nodeOrNodeArray.length > 0 ? nodeOrNodeArray[0]
+ : null;
+ }
+
+ function executeTemplate(targetNodeOrNodeArray, renderMode, template, bindingContext, options) {
+ options = options || {};
+ var templateEngineToUse = (options['templateEngine'] || _templateEngine);
+ ko.templateRewriting.ensureTemplateIsRewritten(template, templateEngineToUse);
+ var renderedNodesArray = templateEngineToUse['renderTemplate'](template, bindingContext, options);
+
+ // Loosely check result is an array of DOM nodes
+ if ((typeof renderedNodesArray.length != "number") || (renderedNodesArray.length > 0 && typeof renderedNodesArray[0].nodeType != "number"))
+ throw "Template engine must return an array of DOM nodes";
+
+ var haveAddedNodesToParent = false;
+ switch (renderMode) {
+ case "replaceChildren":
+ ko.virtualElements.setDomNodeChildren(targetNodeOrNodeArray, renderedNodesArray);
+ haveAddedNodesToParent = true;
+ break;
+ case "replaceNode":
+ ko.utils.replaceDomNodes(targetNodeOrNodeArray, renderedNodesArray);
+ haveAddedNodesToParent = true;
+ break;
+ case "ignoreTargetNode": break;
+ default:
+ throw new Error("Unknown renderMode: " + renderMode);
+ }
+
+ if (haveAddedNodesToParent) {
+ ko.activateBindingsOnTemplateRenderedNodes(renderedNodesArray, bindingContext);
+ if (options['afterRender'])
+ options['afterRender'](renderedNodesArray, bindingContext['$data']);
+ }
+
+ return renderedNodesArray;
+ }
+
+ ko.renderTemplate = function (template, dataOrBindingContext, options, targetNodeOrNodeArray, renderMode) {
+ options = options || {};
+ if ((options['templateEngine'] || _templateEngine) == undefined)
+ throw "Set a template engine before calling renderTemplate";
+ renderMode = renderMode || "replaceChildren";
+
+ if (targetNodeOrNodeArray) {
+ var firstTargetNode = getFirstNodeFromPossibleArray(targetNodeOrNodeArray);
+
+ var whenToDispose = function () { return (!firstTargetNode) || !ko.utils.domNodeIsAttachedToDocument(firstTargetNode); }; // Passive disposal (on next evaluation)
+ var activelyDisposeWhenNodeIsRemoved = (firstTargetNode && renderMode == "replaceNode") ? firstTargetNode.parentNode : firstTargetNode;
+
+ return new ko.dependentObservable( // So the DOM is automatically updated when any dependency changes
+ function () {
+ // Ensure we've got a proper binding context to work with
+ var bindingContext = (dataOrBindingContext && (dataOrBindingContext instanceof ko.bindingContext))
+ ? dataOrBindingContext
+ : new ko.bindingContext(ko.utils.unwrapObservable(dataOrBindingContext));
+
+ // Support selecting template as a function of the data being rendered
+ var templateName = typeof(template) == 'function' ? template(bindingContext['$data']) : template;
+
+ var renderedNodesArray = executeTemplate(targetNodeOrNodeArray, renderMode, templateName, bindingContext, options);
+ if (renderMode == "replaceNode") {
+ targetNodeOrNodeArray = renderedNodesArray;
+ firstTargetNode = getFirstNodeFromPossibleArray(targetNodeOrNodeArray);
+ }
+ },
+ null,
+ { 'disposeWhen': whenToDispose, 'disposeWhenNodeIsRemoved': activelyDisposeWhenNodeIsRemoved }
+ );
+ } else {
+ // We don't yet have a DOM node to evaluate, so use a memo and render the template later when there is a DOM node
+ return ko.memoization.memoize(function (domNode) {
+ ko.renderTemplate(template, dataOrBindingContext, options, domNode, "replaceNode");
+ });
+ }
+ };
+
+ ko.renderTemplateForEach = function (template, arrayOrObservableArray, options, targetNode, parentBindingContext) {
+ var createInnerBindingContext = function(arrayValue) {
+ return parentBindingContext['createChildContext'](ko.utils.unwrapObservable(arrayValue));
+ };
+
+ // This will be called whenever setDomNodeChildrenFromArrayMapping has added nodes to targetNode
+ var activateBindingsCallback = function(arrayValue, addedNodesArray) {
+ var bindingContext = createInnerBindingContext(arrayValue);
+ ko.activateBindingsOnTemplateRenderedNodes(addedNodesArray, bindingContext);
+ if (options['afterRender'])
+ options['afterRender'](addedNodesArray, bindingContext['$data']);
+ };
+
+ return new ko.dependentObservable(function () {
+ var unwrappedArray = ko.utils.unwrapObservable(arrayOrObservableArray) || [];
+ if (typeof unwrappedArray.length == "undefined") // Coerce single value into array
+ unwrappedArray = [unwrappedArray];
+
+ // Filter out any entries marked as destroyed
+ var filteredArray = ko.utils.arrayFilter(unwrappedArray, function(item) {
+ return options['includeDestroyed'] || item === undefined || item === null || !ko.utils.unwrapObservable(item['_destroy']);
+ });
+
+ ko.utils.setDomNodeChildrenFromArrayMapping(targetNode, filteredArray, function (arrayValue) {
+ // Support selecting template as a function of the data being rendered
+ var templateName = typeof(template) == 'function' ? template(arrayValue) : template;
+ return executeTemplate(null, "ignoreTargetNode", templateName, createInnerBindingContext(arrayValue), options);
+ }, options, activateBindingsCallback);
+
+ }, null, { 'disposeWhenNodeIsRemoved': targetNode });
+ };
+
+ var templateSubscriptionDomDataKey = '__ko__templateSubscriptionDomDataKey__';
+ function disposeOldSubscriptionAndStoreNewOne(element, newSubscription) {
+ var oldSubscription = ko.utils.domData.get(element, templateSubscriptionDomDataKey);
+ if (oldSubscription && (typeof(oldSubscription.dispose) == 'function'))
+ oldSubscription.dispose();
+ ko.utils.domData.set(element, templateSubscriptionDomDataKey, newSubscription);
+ }
+
+ ko.bindingHandlers['template'] = {
+ 'init': function(element, valueAccessor) {
+ // Support anonymous templates
+ var bindingValue = ko.utils.unwrapObservable(valueAccessor());
+ if ((typeof bindingValue != "string") && (!bindingValue.name) && (element.nodeType == 1)) {
+ // It's an anonymous template - store the element contents, then clear the element
+ new ko.templateSources.anonymousTemplate(element).text(element.innerHTML);
+ ko.utils.emptyDomNode(element);
+ }
+ return { 'controlsDescendantBindings': true };
+ },
+ 'update': function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
+ var bindingValue = ko.utils.unwrapObservable(valueAccessor());
+ var templateName;
+ var shouldDisplay = true;
+
+ if (typeof bindingValue == "string") {
+ templateName = bindingValue;
+ } else {
+ templateName = bindingValue.name;
+
+ // Support "if"/"ifnot" conditions
+ if ('if' in bindingValue)
+ shouldDisplay = shouldDisplay && ko.utils.unwrapObservable(bindingValue['if']);
+ if ('ifnot' in bindingValue)
+ shouldDisplay = shouldDisplay && !ko.utils.unwrapObservable(bindingValue['ifnot']);
+ }
+
+ var templateSubscription = null;
+
+ if ((typeof bindingValue === 'object') && ('foreach' in bindingValue)) { // Note: can't use 'in' operator on strings
+ // Render once for each data point (treating data set as empty if shouldDisplay==false)
+ var dataArray = (shouldDisplay && bindingValue['foreach']) || [];
+ templateSubscription = ko.renderTemplateForEach(templateName || element, dataArray, /* options: */ bindingValue, element, bindingContext);
+ } else {
+ if (shouldDisplay) {
+ // Render once for this single data point (or use the viewModel if no data was provided)
+ var innerBindingContext = (typeof bindingValue == 'object') && ('data' in bindingValue)
+ ? bindingContext['createChildContext'](ko.utils.unwrapObservable(bindingValue['data'])) // Given an explitit 'data' value, we create a child binding context for it
+ : bindingContext; // Given no explicit 'data' value, we retain the same binding context
+ templateSubscription = ko.renderTemplate(templateName || element, innerBindingContext, /* options: */ bindingValue, element);
+ } else
+ ko.virtualElements.emptyNode(element);
+ }
+
+ // It only makes sense to have a single template subscription per element (otherwise which one should have its output displayed?)
+ disposeOldSubscriptionAndStoreNewOne(element, templateSubscription);
+ }
+ };
+
+ // Anonymous templates can't be rewritten. Give a nice error message if you try to do it.
+ ko.jsonExpressionRewriting.bindingRewriteValidators['template'] = function(bindingValue) {
+ var parsedBindingValue = ko.jsonExpressionRewriting.parseObjectLiteral(bindingValue);
+
+ if ((parsedBindingValue.length == 1) && parsedBindingValue[0]['unknown'])
+ return null; // It looks like a string literal, not an object literal, so treat it as a named template (which is allowed for rewriting)
+
+ if (ko.jsonExpressionRewriting.keyValueArrayContainsKey(parsedBindingValue, "name"))
+ return null; // Named templates can be rewritten, so return "no error"
+ return "This template engine does not support anonymous templates nested within its templates";
+ };
+
+ ko.virtualElements.allowedBindings['template'] = true;
+})();
+
+ko.exportSymbol('ko.setTemplateEngine', ko.setTemplateEngine);
+ko.exportSymbol('ko.renderTemplate', ko.renderTemplate);
+(function () {
+ // Simple calculation based on Levenshtein distance.
+ function calculateEditDistanceMatrix(oldArray, newArray, maxAllowedDistance) {
+ var distances = [];
+ for (var i = 0; i <= newArray.length; i++)
+ distances[i] = [];
+
+ // Top row - transform old array into empty array via deletions
+ for (var i = 0, j = Math.min(oldArray.length, maxAllowedDistance); i <= j; i++)
+ distances[0][i] = i;
+
+ // Left row - transform empty array into new array via additions
+ for (var i = 1, j = Math.min(newArray.length, maxAllowedDistance); i <= j; i++) {
+ distances[i][0] = i;
+ }
+
+ // Fill out the body of the array
+ var oldIndex, oldIndexMax = oldArray.length, newIndex, newIndexMax = newArray.length;
+ var distanceViaAddition, distanceViaDeletion;
+ for (oldIndex = 1; oldIndex <= oldIndexMax; oldIndex++) {
+ var newIndexMinForRow = Math.max(1, oldIndex - maxAllowedDistance);
+ var newIndexMaxForRow = Math.min(newIndexMax, oldIndex + maxAllowedDistance);
+ for (newIndex = newIndexMinForRow; newIndex <= newIndexMaxForRow; newIndex++) {
+ if (oldArray[oldIndex - 1] === newArray[newIndex - 1])
+ distances[newIndex][oldIndex] = distances[newIndex - 1][oldIndex - 1];
+ else {
+ var northDistance = distances[newIndex - 1][oldIndex] === undefined ? Number.MAX_VALUE : distances[newIndex - 1][oldIndex] + 1;
+ var westDistance = distances[newIndex][oldIndex - 1] === undefined ? Number.MAX_VALUE : distances[newIndex][oldIndex - 1] + 1;
+ distances[newIndex][oldIndex] = Math.min(northDistance, westDistance);
+ }
+ }
+ }
+
+ return distances;
+ }
+
+ function findEditScriptFromEditDistanceMatrix(editDistanceMatrix, oldArray, newArray) {
+ var oldIndex = oldArray.length;
+ var newIndex = newArray.length;
+ var editScript = [];
+ var maxDistance = editDistanceMatrix[newIndex][oldIndex];
+ if (maxDistance === undefined)
+ return null; // maxAllowedDistance must be too small
+ while ((oldIndex > 0) || (newIndex > 0)) {
+ var me = editDistanceMatrix[newIndex][oldIndex];
+ var distanceViaAdd = (newIndex > 0) ? editDistanceMatrix[newIndex - 1][oldIndex] : maxDistance + 1;
+ var distanceViaDelete = (oldIndex > 0) ? editDistanceMatrix[newIndex][oldIndex - 1] : maxDistance + 1;
+ var distanceViaRetain = (newIndex > 0) && (oldIndex > 0) ? editDistanceMatrix[newIndex - 1][oldIndex - 1] : maxDistance + 1;
+ if ((distanceViaAdd === undefined) || (distanceViaAdd < me - 1)) distanceViaAdd = maxDistance + 1;
+ if ((distanceViaDelete === undefined) || (distanceViaDelete < me - 1)) distanceViaDelete = maxDistance + 1;
+ if (distanceViaRetain < me - 1) distanceViaRetain = maxDistance + 1;
+
+ if ((distanceViaAdd <= distanceViaDelete) && (distanceViaAdd < distanceViaRetain)) {
+ editScript.push({ status: "added", value: newArray[newIndex - 1] });
+ newIndex--;
+ } else if ((distanceViaDelete < distanceViaAdd) && (distanceViaDelete < distanceViaRetain)) {
+ editScript.push({ status: "deleted", value: oldArray[oldIndex - 1] });
+ oldIndex--;
+ } else {
+ editScript.push({ status: "retained", value: oldArray[oldIndex - 1] });
+ newIndex--;
+ oldIndex--;
+ }
+ }
+ return editScript.reverse();
+ }
+
+ ko.utils.compareArrays = function (oldArray, newArray, maxEditsToConsider) {
+ if (maxEditsToConsider === undefined) {
+ return ko.utils.compareArrays(oldArray, newArray, 1) // First consider likely case where there is at most one edit (very fast)
+ || ko.utils.compareArrays(oldArray, newArray, 10) // If that fails, account for a fair number of changes while still being fast
+ || ko.utils.compareArrays(oldArray, newArray, Number.MAX_VALUE); // Ultimately give the right answer, even though it may take a long time
+ } else {
+ oldArray = oldArray || [];
+ newArray = newArray || [];
+ var editDistanceMatrix = calculateEditDistanceMatrix(oldArray, newArray, maxEditsToConsider);
+ return findEditScriptFromEditDistanceMatrix(editDistanceMatrix, oldArray, newArray);
+ }
+ };
+})();
+
+ko.exportSymbol('ko.utils.compareArrays', ko.utils.compareArrays);
+
+(function () {
+ // Objective:
+ // * Given an input array, a container DOM node, and a function from array elements to arrays of DOM nodes,
+ // map the array elements to arrays of DOM nodes, concatenate together all these arrays, and use them to populate the container DOM node
+ // * Next time we're given the same combination of things (with the array possibly having mutated), update the container DOM node
+ // so that its children is again the concatenation of the mappings of the array elements, but don't re-map any array elements that we
+ // previously mapped - retain those nodes, and just insert/delete other ones
+
+ // "callbackAfterAddingNodes" will be invoked after any "mapping"-generated nodes are inserted into the container node
+ // You can use this, for example, to activate bindings on those nodes.
+
+ function fixUpVirtualElements(contiguousNodeArray) {
+ // Ensures that contiguousNodeArray really *is* an array of contiguous siblings, even if some of the interior
+ // ones have changed since your array was first built (e.g., because your array contains virtual elements, and
+ // their virtual children changed when binding was applied to them).
+ // This is needed so that we can reliably remove or update the nodes corresponding to a given array item
+
+ if (contiguousNodeArray.length > 2) {
+ // Build up the actual new contiguous node set
+ var current = contiguousNodeArray[0], last = contiguousNodeArray[contiguousNodeArray.length - 1], newContiguousSet = [current];
+ while (current !== last) {
+ current = current.nextSibling;
+ if (!current) // Won't happen, except if the developer has manually removed some DOM elements (then we're in an undefined scenario)
+ return;
+ newContiguousSet.push(current);
+ }
+
+ // ... then mutate the input array to match this.
+ // (The following line replaces the contents of contiguousNodeArray with newContiguousSet)
+ Array.prototype.splice.apply(contiguousNodeArray, [0, contiguousNodeArray.length].concat(newContiguousSet));
+ }
+ }
+
+ function mapNodeAndRefreshWhenChanged(containerNode, mapping, valueToMap, callbackAfterAddingNodes) {
+ // Map this array value inside a dependentObservable so we re-map when any dependency changes
+ var mappedNodes = [];
+ var dependentObservable = ko.dependentObservable(function() {
+ var newMappedNodes = mapping(valueToMap) || [];
+
+ // On subsequent evaluations, just replace the previously-inserted DOM nodes
+ if (mappedNodes.length > 0) {
+ fixUpVirtualElements(mappedNodes);
+ ko.utils.replaceDomNodes(mappedNodes, newMappedNodes);
+ if (callbackAfterAddingNodes)
+ callbackAfterAddingNodes(valueToMap, newMappedNodes);
+ }
+
+ // Replace the contents of the mappedNodes array, thereby updating the record
+ // of which nodes would be deleted if valueToMap was itself later removed
+ mappedNodes.splice(0, mappedNodes.length);
+ ko.utils.arrayPushAll(mappedNodes, newMappedNodes);
+ }, null, { 'disposeWhenNodeIsRemoved': containerNode, 'disposeWhen': function() { return (mappedNodes.length == 0) || !ko.utils.domNodeIsAttachedToDocument(mappedNodes[0]) } });
+ return { mappedNodes : mappedNodes, dependentObservable : dependentObservable };
+ }
+
+ var lastMappingResultDomDataKey = "setDomNodeChildrenFromArrayMapping_lastMappingResult";
+
+ ko.utils.setDomNodeChildrenFromArrayMapping = function (domNode, array, mapping, options, callbackAfterAddingNodes) {
+ // Compare the provided array against the previous one
+ array = array || [];
+ options = options || {};
+ var isFirstExecution = ko.utils.domData.get(domNode, lastMappingResultDomDataKey) === undefined;
+ var lastMappingResult = ko.utils.domData.get(domNode, lastMappingResultDomDataKey) || [];
+ var lastArray = ko.utils.arrayMap(lastMappingResult, function (x) { return x.arrayEntry; });
+ var editScript = ko.utils.compareArrays(lastArray, array);
+
+ // Build the new mapping result
+ var newMappingResult = [];
+ var lastMappingResultIndex = 0;
+ var nodesToDelete = [];
+ var nodesAdded = [];
+ var insertAfterNode = null;
+ for (var i = 0, j = editScript.length; i < j; i++) {
+ switch (editScript[i].status) {
+ case "retained":
+ // Just keep the information - don't touch the nodes
+ var dataToRetain = lastMappingResult[lastMappingResultIndex];
+ newMappingResult.push(dataToRetain);
+ if (dataToRetain.domNodes.length > 0)
+ insertAfterNode = dataToRetain.domNodes[dataToRetain.domNodes.length - 1];
+ lastMappingResultIndex++;
+ break;
+
+ case "deleted":
+ // Stop tracking changes to the mapping for these nodes
+ lastMappingResult[lastMappingResultIndex].dependentObservable.dispose();
+
+ // Queue these nodes for later removal
+ fixUpVirtualElements(lastMappingResult[lastMappingResultIndex].domNodes);
+ ko.utils.arrayForEach(lastMappingResult[lastMappingResultIndex].domNodes, function (node) {
+ nodesToDelete.push({
+ element: node,
+ index: i,
+ value: editScript[i].value
+ });
+ insertAfterNode = node;
+ });
+ lastMappingResultIndex++;
+ break;
+
+ case "added":
+ var valueToMap = editScript[i].value;
+ var mapData = mapNodeAndRefreshWhenChanged(domNode, mapping, valueToMap, callbackAfterAddingNodes);
+ var mappedNodes = mapData.mappedNodes;
+
+ // On the first evaluation, insert the nodes at the current insertion point
+ newMappingResult.push({ arrayEntry: editScript[i].value, domNodes: mappedNodes, dependentObservable: mapData.dependentObservable });
+ for (var nodeIndex = 0, nodeIndexMax = mappedNodes.length; nodeIndex < nodeIndexMax; nodeIndex++) {
+ var node = mappedNodes[nodeIndex];
+ nodesAdded.push({
+ element: node,
+ index: i,
+ value: editScript[i].value
+ });
+ if (insertAfterNode == null) {
+ // Insert "node" (the newly-created node) as domNode's first child
+ ko.virtualElements.prepend(domNode, node);
+ } else {
+ // Insert "node" into "domNode" immediately after "insertAfterNode"
+ ko.virtualElements.insertAfter(domNode, node, insertAfterNode);
+ }
+ insertAfterNode = node;
+ }
+ if (callbackAfterAddingNodes)
+ callbackAfterAddingNodes(valueToMap, mappedNodes);
+ break;
+ }
+ }
+
+ ko.utils.arrayForEach(nodesToDelete, function (node) { ko.cleanNode(node.element) });
+
+ var invokedBeforeRemoveCallback = false;
+ if (!isFirstExecution) {
+ if (options['afterAdd']) {
+ for (var i = 0; i < nodesAdded.length; i++)
+ options['afterAdd'](nodesAdded[i].element, nodesAdded[i].index, nodesAdded[i].value);
+ }
+ if (options['beforeRemove']) {
+ for (var i = 0; i < nodesToDelete.length; i++)
+ options['beforeRemove'](nodesToDelete[i].element, nodesToDelete[i].index, nodesToDelete[i].value);
+ invokedBeforeRemoveCallback = true;
+ }
+ }
+ if (!invokedBeforeRemoveCallback)
+ ko.utils.arrayForEach(nodesToDelete, function (node) {
+ ko.removeNode(node.element);
+ });
+
+ // Store a copy of the array items we just considered so we can difference it next time
+ ko.utils.domData.set(domNode, lastMappingResultDomDataKey, newMappingResult);
+ }
+})();
+
+ko.exportSymbol('ko.utils.setDomNodeChildrenFromArrayMapping', ko.utils.setDomNodeChildrenFromArrayMapping);
+ko.nativeTemplateEngine = function () {
+ this['allowTemplateRewriting'] = false;
+}
+
+ko.nativeTemplateEngine.prototype = new ko.templateEngine();
+ko.nativeTemplateEngine.prototype['renderTemplateSource'] = function (templateSource, bindingContext, options) {
+ var templateText = templateSource.text();
+ return ko.utils.parseHtmlFragment(templateText);
+};
+
+ko.nativeTemplateEngine.instance = new ko.nativeTemplateEngine();
+ko.setTemplateEngine(ko.nativeTemplateEngine.instance);
+
+ko.exportSymbol('ko.nativeTemplateEngine', ko.nativeTemplateEngine);(function() {
+ ko.jqueryTmplTemplateEngine = function () {
+ // Detect which version of jquery-tmpl you're using. Unfortunately jquery-tmpl
+ // doesn't expose a version number, so we have to infer it.
+ // Note that as of Knockout 1.3, we only support jQuery.tmpl 1.0.0pre and later,
+ // which KO internally refers to as version "2", so older versions are no longer detected.
+ var jQueryTmplVersion = this.jQueryTmplVersion = (function() {
+ if ((typeof(jQuery) == "undefined") || !(jQuery['tmpl']))
+ return 0;
+ // Since it exposes no official version number, we use our own numbering system. To be updated as jquery-tmpl evolves.
+ try {
+ if (jQuery['tmpl']['tag']['tmpl']['open'].toString().indexOf('__') >= 0) {
+ // Since 1.0.0pre, custom tags should append markup to an array called "__"
+ return 2; // Final version of jquery.tmpl
+ }
+ } catch(ex) { /* Apparently not the version we were looking for */ }
+
+ return 1; // Any older version that we don't support
+ })();
+
+ function ensureHasReferencedJQueryTemplates() {
+ if (jQueryTmplVersion < 2)
+ throw new Error("Your version of jQuery.tmpl is too old. Please upgrade to jQuery.tmpl 1.0.0pre or later.");
+ }
+
+ function executeTemplate(compiledTemplate, data, jQueryTemplateOptions) {
+ return jQuery['tmpl'](compiledTemplate, data, jQueryTemplateOptions);
+ }
+
+ this['renderTemplateSource'] = function(templateSource, bindingContext, options) {
+ options = options || {};
+ ensureHasReferencedJQueryTemplates();
+
+ // Ensure we have stored a precompiled version of this template (don't want to reparse on every render)
+ var precompiled = templateSource['data']('precompiled');
+ if (!precompiled) {
+ var templateText = templateSource.text() || "";
+ // Wrap in "with($whatever.koBindingContext) { ... }"
+ templateText = "{{ko_with $item.koBindingContext}}" + templateText + "{{/ko_with}}";
+
+ precompiled = jQuery['template'](null, templateText);
+ templateSource['data']('precompiled', precompiled);
+ }
+
+ var data = [bindingContext['$data']]; // Prewrap the data in an array to stop jquery.tmpl from trying to unwrap any arrays
+ var jQueryTemplateOptions = jQuery['extend']({ 'koBindingContext': bindingContext }, options['templateOptions']);
+
+ var resultNodes = executeTemplate(precompiled, data, jQueryTemplateOptions);
+ resultNodes['appendTo'](document.createElement("div")); // Using "appendTo" forces jQuery/jQuery.tmpl to perform necessary cleanup work
+ jQuery['fragments'] = {}; // Clear jQuery's fragment cache to avoid a memory leak after a large number of template renders
+ return resultNodes;
+ };
+
+ this['createJavaScriptEvaluatorBlock'] = function(script) {
+ return "{{ko_code ((function() { return " + script + " })()) }}";
+ };
+
+ this['addTemplate'] = function(templateName, templateMarkup) {
+ document.write("<script type='text/html' id='" + templateName + "'>" + templateMarkup + "</script>");
+ };
+
+ if (jQueryTmplVersion > 0) {
+ jQuery['tmpl']['tag']['ko_code'] = {
+ open: "__.push($1 || '');"
+ };
+ jQuery['tmpl']['tag']['ko_with'] = {
+ open: "with($1) {",
+ close: "} "
+ };
+ }
+ };
+
+ ko.jqueryTmplTemplateEngine.prototype = new ko.templateEngine();
+
+ // Use this one by default *only if jquery.tmpl is referenced*
+ var jqueryTmplTemplateEngineInstance = new ko.jqueryTmplTemplateEngine();
+ if (jqueryTmplTemplateEngineInstance.jQueryTmplVersion > 0)
+ ko.setTemplateEngine(jqueryTmplTemplateEngineInstance);
+
+ ko.exportSymbol('ko.jqueryTmplTemplateEngine', ko.jqueryTmplTemplateEngine);
+})();})(window);
diff --git a/src/SPA/upshot/LocalDataSource.js b/src/SPA/upshot/LocalDataSource.js
new file mode 100644
index 00000000..da94c806
--- /dev/null
+++ b/src/SPA/upshot/LocalDataSource.js
@@ -0,0 +1,470 @@
+/// <reference path="IntelliSense\References.js" />
+///#RESTORE (function (global, $, upshot, undefined)
+{
+ var base = upshot.DataSource.prototype;
+
+ var obs = upshot.observability;
+
+ var ctor = function (options) {
+ /// <summary>
+ /// LocalDataSource is used to load model data matching a query that is evaluated in-memory.
+ /// </summary>
+ /// <param name="options" optional="true">
+ /// Options used in the construction of the LocalDataSource:
+ /// &#10;source: The input data which is to be sorted, filtered and paged to produce the output for this LocalDataSource. Can be supplied as some instance deriving from EntitySource or as an array from some EntitySource.getEntities().
+ /// &#10;autoRefresh: (Optional) Instructs the LocalDataSource to implicitly reevaluate its query in response to edits to input data. Otherwise, LocalDataSource.refresh() must be used to reevaluate the query against modified input data.
+ /// </param>
+
+ // support no new ctor
+ if (this._trigger === undefined) {
+ return new upshot.LocalDataSource(options);
+ }
+
+ var input = options && options.source;
+ if (input && !input.__recomputeDependentViews) { // Test if "input" is an EntitySource.
+ var entitySource = obs.isArray(input) && upshot.EntitySource.as(input);
+ if (!entitySource) {
+ throw "Input data for a LocalDataSource must be an EntitySource or the array returned by EntitySource.getEntities().";
+ }
+
+ options.source = entitySource;
+
+ // TODO -- If this is an array that isn't already the output of an EntitySource,
+ // engage the compatibility layer to wrap the raw array in an EntitySource.
+ // Such an EntitySource would upshot.__registerRootEntitySource and would turn
+ // observable CUD into Upshot "change" and "arrayChange" events.
+ }
+
+ this._autoRefresh = options && options.autoRefresh; // TODO -- Should we make "auto refresh" a feature of RemoteDataSource too?
+
+ // Optional query options
+ this._sort = null;
+ this._filter = null;
+
+ // State
+ this._refreshAllInProgress = false;
+
+ base.constructor.call(this, options);
+
+ // Events specific to LocalDataSource
+ this._bindFromOptions(options, [ "refreshNeeded" ]);
+
+ if (this._autoRefresh) {
+ this.refresh();
+ }
+ };
+
+ var instanceMembers = {
+
+ // override "sort" option setter
+ setSort: function (sort) {
+ /// <summary>
+ /// Establishes the sort specification that is to be applied to the input data.
+ /// </summary>
+ /// <param name="sort">
+ /// &#10;The sort specification to applied when loading model data.
+ /// &#10;Should be supplied as an object of the form &#123; property: &#60;propertyName&#62; [, descending: &#60;bool&#62; ] &#125; or an array of ordered objects of this form.
+ /// &#10;When supplied as null or undefined, the sort specification for this LocalDataSource is cleared.
+ /// </param>
+ /// <returns type="upshot.LocalDataSource"/>
+
+ // TODO -- Should really raise "need refresh" event when changed (throughout).
+ // TODO -- Validate sort specification?
+ this._sort = sort;
+ return this;
+ },
+
+ // override "filter" option setter
+ setFilter: function (filter) {
+ /// <summary>
+ /// Establishes the filter specification that is to be applied to the input data.
+ /// </summary>
+ /// <param name="filter">
+ /// &#10;The filter specification to applied when loading model data.
+ /// &#10;Should be supplied as an object of the form &#123; property: &#60;propertyName&#62;, value: &#60;propertyValue&#62; [, operator: &#60;operator&#62; ] &#125; or a function(entity) returning Boolean or an ordered array of these forms.
+ /// &#10;When supplied as null or undefined, the filter specification for this LocalDataSource is cleared.
+ /// </param>
+ /// <returns type="upshot.LocalDataSource"/>
+
+ // TODO -- Should really raise "need refresh" event when changed (throughout).
+ this._filter = filter && this._createFilterFunction(filter);
+ },
+
+ refresh: function (options, success, fail) {
+ /// <summary>
+ /// Initiates an asynchronous reevaluation of the query established with setSort, setFilter and setPaging.
+ /// </summary>
+ /// <param name="options" type="Object" optional="true">
+ /// &#10;If supplied, an object with an "all" property, indicating that the input DataSource is to be refreshed prior to reevaluating this LocalDataSource query.
+ /// </param>
+ /// <param name="success" type="Function" optional="true">
+ /// &#10;A success callback with signature function(entities, totalCount).
+ /// </param>
+ /// <param name="error" type="Function" optional="true">
+ /// &#10;An error callback with signature function(httpStatus, errorText, context).
+ /// </param>
+ /// <returns type="upshot.LocalDataSource"/>
+
+ this._verifyOkToRefresh();
+
+ if ($.isFunction(options)) {
+ fail = success;
+ success = options;
+ options = undefined;
+ }
+
+ this._trigger("refreshStart");
+
+ var self = this,
+ sourceIsDataSource = this._entitySource.refresh; // Only DataSources will have "refresh" (and not EntitySet and AssociatedEntitiesView).
+ if (options && !!options.all && sourceIsDataSource) {
+ // N.B. "all" is a helper, in the sense that it saves a client from doing a serverDataSource.refresh and then,
+ // in response to serverDataSource.onRefresh, calling localDataSource.refresh. Also, it allows the app to listen
+ // on refreshStart/refresh events from this LDS alone (and not the inner SDS as well).
+ this._refreshAllInProgress = true;
+ this._entitySource.refresh({ all: true }, function (entities) {
+ completeRefresh(entities);
+ self._refreshAllInProgress = false;
+ }, function (httpStatus, errorText, context) {
+ self._failRefresh(httpStatus, errorText, context, fail);
+ });
+ } else {
+ // We do this refresh asynchronously so that, if this refresh was called during a callback,
+ // the app receives remaining callbacks first, before the new batch of callbacks with respect to this refresh.
+ // TODO -- We should only refresh once in response to N>1 "refresh" calls.
+ setTimeout(function () { completeRefresh(obs.asArray(self._entitySource.getEntities())); });
+ }
+
+ return this;
+
+ function completeRefresh(entities) {
+ self._needRecompute = false;
+
+ var results = self._applyQuery(entities);
+ self._completeRefresh(results.entities, results.totalCount, success);
+ };
+ },
+
+ refreshNeeded: function () {
+ /// <summary>
+ /// Indicates whether the input data has been modified in such a way that LocalDataSource.getEntities() would change on the next LocalDataSource.refresh() call.
+ /// </summary>
+ /// <returns type="Boolean"/>
+
+ return this._needRecompute;
+ },
+
+ // Private methods
+
+ _setNeedRecompute: function () {
+ ///#DEBUG
+ upshot.assert(!this._needRecompute); // Callers should only determine dirtiness if we're not already dirty.
+ ///#ENDDEBUG
+
+ if (this._autoRefresh) {
+ // Recompute our result entities according to our regular recompute cycle,
+ // just as AssociatedEntitiesView does in response to changes in its
+ // target EntitySet.
+ base._setNeedRecompute.call(this);
+ } else {
+ // Explicit use of "refresh" was requested here. Reuse (or abuse) the _needRecompute
+ // flag to track our dirtiness.
+ this._needRecompute = true;
+ this._trigger("refreshNeeded");
+ }
+ },
+
+ _recompute: function () {
+ ///#DEBUG
+ upshot.assert(this._autoRefresh); // We should only get here if we scheduled a recompute, for auto-refresh.
+ ///#ENDDEBUG
+
+ this._trigger("refreshStart");
+
+ var results = this._applyQuery(obs.asArray(this._entitySource.getEntities()));
+ // Don't __triggerRecompute here. Downstream listeners on this data source will recompute
+ // as part of this wave.
+ this._applyNewQueryResult(results.entities, results.totalCount);
+
+ this._trigger("refreshSuccess", obs.asArray(this._clientEntities), this._lastRefreshTotalEntityCount);
+ },
+
+ _normalizePropertyValue: function (entity, property) {
+ // TODO -- Should do this based on metadata and return default value of the correct scalar type.
+ return obs.getProperty(entity, property) || "";
+ },
+
+ _onPropertyChanged: function (entity, property, newValue) {
+ base._onPropertyChanged.apply(this, arguments);
+
+ if (this._refreshAllInProgress) {
+ // We don't want to event "need refresh" due to a "refresh all".
+ // Rather, we want to issue "refresh completed".
+ return;
+ }
+
+ if (!this._needRecompute) {
+ var needRecompute = false;
+ if (this._haveEntity(entity)) {
+ if (this._filter && !this._filter(entity)) {
+ needRecompute = true;
+ }
+ } else if (this._filter && this._filter(entity)) {
+ // This is overly pessimistic if we have paging options in place.
+ // It could be that this entity is already on a preceding page (and will
+ // stay there on recompute) or would be added to a following page (and
+ // excluded from the current page on recompute).
+ needRecompute = true;
+ }
+ if (this._haveEntity(entity) && this._sort) {
+ if ($.isFunction(this._sort)) {
+ needRecompute = true;
+ } else if (upshot.isArray(this._sort)) {
+ needRecompute = $.grep(this._sort, function (sortPart) {
+ return sortPart.property === property;
+ }).length > 0;
+ } else {
+ needRecompute = this._sort.property === property;
+ }
+ }
+
+ if (needRecompute) {
+ this._setNeedRecompute();
+ }
+ }
+ },
+
+ // support the following filter formats
+ // function, [functions], filterPart, [filterParts]
+ // return: function
+ _createFilterFunction: function (filter) {
+ var self = this;
+ if ($.isFunction(filter)) {
+ return filter;
+ }
+
+ var filters = this._normalizeFilters(filter);
+ var comparisonFunctions = []
+ for (var i = 0; i < filters.length; i++) {
+ var filterPart = filters[i];
+ if ($.isFunction(filterPart)) {
+ comparisonFunctions.push(filterPart);
+ } else {
+ var func = createFunction(filterPart.property, filterPart.operator, filterPart.value);
+ comparisonFunctions.push(func);
+ }
+ }
+ return function (entity) {
+ for (var i = 0; i < comparisonFunctions.length; i++) {
+ if (!comparisonFunctions[i](entity)) {
+ return false;
+ }
+ }
+ return true;
+ };
+
+ function createFunction(filterProperty, filterOperator, filterValue) {
+ var comparer;
+ switch (filterOperator) {
+ case "<": comparer = function (propertyValue) { return propertyValue < filterValue; }; break;
+ case "<=": comparer = function (propertyValue) { return propertyValue <= filterValue; }; break;
+ case "==": comparer = function (propertyValue) { return propertyValue == filterValue; }; break;
+ case "!=": comparer = function (propertyValue) { return propertyValue != filterValue; }; break;
+ case ">=": comparer = function (propertyValue) { return propertyValue >= filterValue; }; break;
+ case ">": comparer = function (propertyValue) { return propertyValue > filterValue; }; break;
+ case "Contains":
+ comparer = function (propertyValue) {
+ if (typeof propertyValue === "string" && typeof filterValue === "string") {
+ propertyValue = propertyValue.toLowerCase();
+ filterValue = filterValue.toLowerCase();
+ }
+ return propertyValue.indexOf(filterValue) >= 0;
+ };
+ break;
+ default: throw "Unrecognized filter operator.";
+ };
+
+ return function (entity) {
+ // Can't trust added entities, for instance, to have all required property values.
+ var propertyValue = self._normalizePropertyValue(entity, filterProperty);
+ return comparer(propertyValue);
+ };
+ };
+ },
+
+ _getSortFunction: function () {
+ var self = this;
+ if (!this._sort) {
+ return null;
+ } else if ($.isFunction(this._sort)) {
+ return this._sort;
+ } else if (upshot.isArray(this._sort)) {
+ var sortFunction;
+ $.each(this._sort, function (unused, sortPart) {
+ var sortPartFunction = getSortPartFunction(sortPart);
+ if (!sortFunction) {
+ sortFunction = sortPartFunction;
+ } else {
+ sortFunction = function (sortPartFunction1, sortPartFunction2) {
+ return function (entity1, entity2) {
+ var result = sortPartFunction1(entity1, entity2);
+ return result === 0 ? sortPartFunction2(entity1, entity2) : result;
+ };
+ } (sortFunction, sortPartFunction);
+ }
+ });
+ return sortFunction;
+ } else {
+ return getSortPartFunction(this._sort);
+ }
+
+ function getSortPartFunction(sortPart) {
+ return function (entity1, entity2) {
+ var isAscending = !sortPart.descending,
+ propertyName = sortPart.property,
+ propertyValue1 = self._normalizePropertyValue(entity1, propertyName),
+ propertyValue2 = self._normalizePropertyValue(entity2, propertyName);
+ if (propertyValue1 == propertyValue2) {
+ return 0;
+ } else if (propertyValue1 > propertyValue2) {
+ return isAscending ? 1 : -1;
+ } else {
+ return isAscending ? -1 : 1;
+ }
+ };
+ }
+ },
+
+ _applyQuery: function (entities) {
+ var self = this;
+
+ var filteredEntities;
+ if (this._filter) {
+ filteredEntities = $.grep(entities, function (entity, index) {
+ return self._filter(entity);
+ });
+ } else {
+ filteredEntities = entities;
+ }
+
+ var sortFunction = this._getSortFunction(),
+ sortedEntities;
+ if (sortFunction) {
+ // "sort" modifies filtered entities, so we must be operating against a copy
+ // by this point. Otherwise, we'll potentially update the LDS input array.
+ if (filteredEntities === entities) {
+ filteredEntities = filteredEntities.slice(0);
+ }
+ sortedEntities = filteredEntities.sort(sortFunction);
+ } else {
+ sortedEntities = filteredEntities;
+ }
+
+ var skip = this._skip || 0,
+ pagedEntities = skip > 0 ? sortedEntities.slice(skip) : sortedEntities;
+ if (this._take) {
+ pagedEntities = pagedEntities.slice(0, this._take);
+ }
+
+ return { entities: pagedEntities, totalCount: sortedEntities.length };
+ },
+
+ _onArrayChanged: function (type, eventArguments) {
+ base._onArrayChanged.apply(this, arguments);
+
+ if (this._refreshAllInProgress) {
+ // We don't want to event "need refresh" due to a "refresh all".
+ // Rather, we want to issue "refresh completed".
+ return;
+ }
+
+ if (!this._needRecompute) {
+ // See if the inner array change should cause us to raise the "need refresh" event.
+ var self = this,
+ needRecompute = false;
+
+ switch (type) {
+ case "insert":
+ var insertedEntities = eventArguments.items;
+ if (insertedEntities.length > 0) {
+ var anyExternallyInsertedEntitiesMatchFilter = $.grep(insertedEntities, function (entity) {
+ return (!self._filter || self._filter(entity)) && $.inArray(entity, obs.asArray(self._clientEntities)) < 0;
+ }).length > 0;
+ if (anyExternallyInsertedEntitiesMatchFilter) {
+ needRecompute = true;
+ }
+ }
+ break;
+
+ case "remove":
+ if (this._take > 0 || this._skip > 0) {
+ // If we have paging options, we have to conservatively assume that the result will be shy
+ // of the _limit due to this delete or the result should be shifted due to a delete from
+ // those entities preceding the _skip.
+ needRecompute = true;
+
+ // NOTE: This covers the case where an entity in our input reaches the upshot.EntityState.Deleted
+ // state. We assume that this will cause the entity to be removed from the input EntitySource.
+ } else {
+ var nonDeletedResultEntitiesRemoved = $.grep(eventArguments.items, function (entity) {
+ return self._haveEntity(entity) &&
+ (self.getEntityState(entity) || upshot.EntityState.Deleted) !== upshot.EntityState.Deleted;
+ });
+ if (nonDeletedResultEntitiesRemoved.length > 0) {
+ // If the input EntitySource happens to be an EntityView and entities leave that view
+ // for some other reason than reaching the upshot.EntityState.Deleted state, we should
+ // signal the need for a recompute to remove these entities from our results (but let
+ // the client control this for the non-auto-refresh case).
+ needRecompute = true;
+ }
+ }
+ break;
+
+ case "replaceAll":
+ if (!this._refreshAllInProgress) {
+ // We don't want to event "need refresh" due to a "refresh all".
+ // Rather, we want to issue "refresh completed".
+
+ var results = this._applyQuery(eventArguments.newItems);
+ if (this.totalCount !== results.totalCount) {
+ needRecompute = true;
+ } else {
+ // Reference comparison is enough here. "property changed" catches deeper causes of "need refresh".
+ needRecompute = !upshot.sameArrayContents(obs.asArray(this._clientEntities), results.entities);
+ }
+ }
+ break;
+
+ default:
+ throw "Unknown array operation '" + type + "'.";
+ }
+
+ if (needRecompute) {
+ this._setNeedRecompute();
+ }
+ }
+ },
+
+ _handleEntityAdd: function (entity) {
+ if (!this._needRecompute) {
+ if (this._filter && !this._filter(entity)) {
+ this._setNeedRecompute();
+ }
+ }
+ base._handleEntityAdd.apply(this, arguments);
+ },
+
+ _handleEntityDelete: function (entity) {
+ if (!this._needRecompute && this._take > 0) {
+ // If we have a _take, we have to conservatively assume that the result will be one entity shy of
+ // the take due to this delete.
+ this._setNeedRecompute();
+ }
+ base._handleEntityDelete.apply(this, arguments);
+ }
+ };
+
+ upshot.LocalDataSource = upshot.deriveClass(base, ctor, instanceMembers);
+
+}
+///#RESTORE )(this, jQuery, upshot);
diff --git a/src/SPA/upshot/Metadata.js b/src/SPA/upshot/Metadata.js
new file mode 100644
index 00000000..e9e3cc14
--- /dev/null
+++ b/src/SPA/upshot/Metadata.js
@@ -0,0 +1,124 @@
+/// <reference path="IntelliSense\References.js" />
+///#RESTORE (function (global, $, upshot, undefined)
+{
+ var obs = upshot.observability;
+
+ var metadata = {};
+
+ upshot.metadata = function (entityType) {
+ if (arguments.length === 0) {
+ return $.extend({}, metadata);
+ } else if (typeof entityType === "string") {
+ if (arguments.length === 1) {
+ return metadata[entityType];
+ } else {
+ if (!metadata[entityType]) {
+ metadata[entityType] = arguments[1];
+ }
+ // ...else assume the new metadata is the same as that previously registered for entityType.
+ }
+ } else {
+ $.each(entityType, function (entityType, metadata) {
+ upshot.metadata(entityType, metadata);
+ });
+ }
+ }
+
+ upshot.metadata.getProperties = function (entity, entityType, includeAssocations) {
+ var props = [];
+ if (entityType) {
+ var metadata = upshot.metadata(entityType);
+ if (metadata && metadata.fields) {
+ // if metadata is present, we'll loop through the fields
+ var fields = metadata.fields;
+ for (var prop in fields) {
+ if (includeAssocations || !fields[prop].association) {
+ props.push({ name: prop, type: fields[prop].type, association: fields[prop].association });
+ }
+ }
+ return props;
+ }
+ }
+ // otherwise we'll use the observability layer to infer the properties
+ for (var prop in entity) {
+ // TODO: determine if we want to allow the case where hasOwnProperty returns false (hierarchies, etc.)
+ if (entity.hasOwnProperty(prop) && obs.isProperty(entity, prop) && (prop.indexOf("jQuery") !== 0)) {
+ props.push({ name: prop });
+ }
+ }
+ return props;
+ }
+
+ upshot.metadata.getPropertyType = function (entityType, property) {
+ if (entityType) {
+ var metadata = upshot.metadata(entityType);
+ if (metadata && metadata.fields && metadata.fields[property]) {
+ return metadata.fields[property].type;
+ }
+ }
+ return null;
+ }
+
+ upshot.metadata.isEntityType = function (type) {
+ if (type) {
+ var metadata = upshot.metadata(type);
+ if (metadata && metadata.key) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ var types = {};
+
+ upshot.registerType = function (type, keyFunction) {
+ /// <summary>
+ /// Registers a type string for later access with a key. This facility is convenient to avoid duplicate type string literals throughout your application scripts. The key is expected to be returned by 'keyFunction', allowing the call to 'registerType' to precede the line of JavaScript declaring the key. Typically, the returned key will be a constructor function for a JavaScript class corresponding to 'type'.
+ /// </summary>
+ /// <param name="keyFunction" type="Function">
+ /// &#10;A function returning the key by which the type string will later be retrieved.
+ /// </param>
+ /// <returns type="String"/>
+
+ if (upshot.isObject(type)) {
+ // Allow for registrations that cover multiple types like:
+ // upshot.registerType({ "BarType": function () { return Bar; }, "FooType": function () { return Foo; } });
+ $.each(type, function (type, key) {
+ upshot.registerType(type, key);
+ });
+ } else {
+ // Allow for single-type registrations:
+ // upshot.registerType("BarType", function () { return Bar; });
+ var keyFunctions = types[type] || (types[type] = []);
+ if ($.inArray(keyFunction, keyFunctions) < 0) {
+ keyFunctions.push(keyFunction);
+ }
+ }
+ return upshot;
+ }
+
+ upshot.type = function (key) {
+ /// <summary>
+ /// Returns the type string registered for a particular key.
+ /// </summary>
+ /// <param name="key">
+ /// &#10;The key under which the desired type string is registered.
+ /// </param>
+ /// <returns type="String"/>
+
+ var result;
+ for (var type in types) {
+ if (types.hasOwnProperty(type)) {
+ var keyFunctions = types[type];
+ for (var i = 0; i < keyFunctions.length; i++) {
+ if (keyFunctions[i]() === key) {
+ return type;
+ }
+ }
+ }
+ }
+
+ throw "No type string registered for key '" + key + "'.";
+ }
+}
+///#RESTORE )(this, jQuery, upshot); \ No newline at end of file
diff --git a/src/SPA/upshot/Observability.js b/src/SPA/upshot/Observability.js
new file mode 100644
index 00000000..6b3afe36
--- /dev/null
+++ b/src/SPA/upshot/Observability.js
@@ -0,0 +1,24 @@
+/// <reference path="IntelliSense\References.js" />
+///#RESTORE (function (global, $, upshot, undefined)
+{
+ var observability = upshot.observability = upshot.observability || {};
+
+ $.each(["track", "insert", "remove", "refresh", "isProperty", "getProperty", "setProperty", "isArray", "createCollection", "asArray", "map", "unmap", "setContextProperty"], function (index, value) {
+ observability[value] = function () {
+ // NOTE: observability.configuration is expected to be established by a loaded Upshot.Compat.<platform>.js.
+ // TODO: Support apps and UI libraries that have no observability design.
+ var config = observability.configuration;
+ return config[value].apply(config, arguments);
+ };
+ });
+
+ upshot.map = function (data, entityType, target) {
+ // Interestingly, we don't use a "mapNested" parameter here (as Upshot proper does).
+ // As a consequence, apps that call upshot.map from a map function will not pick up custom
+ // map functions for nested entities/objects. They'd need to hand-code calls to custom map
+ // functions (ctors) for nested objects from the parent map function.
+ return observability.configuration.map(data, entityType, null, target);
+ };
+
+}
+///#RESTORE )(this, jQuery, upshot); \ No newline at end of file
diff --git a/src/SPA/upshot/RemoteDataSource.js b/src/SPA/upshot/RemoteDataSource.js
new file mode 100644
index 00000000..279726fb
--- /dev/null
+++ b/src/SPA/upshot/RemoteDataSource.js
@@ -0,0 +1,259 @@
+/// <reference path="IntelliSense\References.js" />
+///#RESTORE (function (global, $, upshot, undefined)
+{
+ var base = upshot.DataSource.prototype;
+
+ var commitEvents = ["commitStart", "commitSuccess", "commitError"];
+
+ var ctor = function (options) {
+ /// <summary>
+ /// RemoteDataSource is used to load model data matching a query that is evaluated on the server.
+ /// </summary>
+ /// <param name="options" optional="true">
+ /// Options used in the construction of the RemoteDataSource:
+ /// &#10;bufferChanges: (Optional) If 'true', edits to model data are buffered until RemoteDataSource.commitChanges. Otherwise, edits are committed to the server immediately.
+ /// &#10;result: (Optional) The observable array into which the RemoteDataSource will load model data.
+ /// &#10;dataContext: (Optional) A DataContext instance that acts as a shared cache for multiple DataSource instances. When not supplied, a DataContext instance is instantiated for this RemoteDataSource.
+ /// &#10;entityType: The type of model data that will be loaded by this RemoteDataSource instance.
+ /// &#10;provider: (Optional) Specifies the DataProvider that will be used to get model data and commit edits to the model data. Defaults to upshot.DataProvider which works with Microsoft.Web.Http.Data.DataController.
+ /// &#10;providerParameters: (Optional) Parameters that are supplied to the DataProvider for this RemoteDataSource and used by the DataProvider when it gets model data from the server.
+ /// &#10;mapping: (Optional) A function (typically a constructor) used to translate raw model data loaded via the DataProvider into model data that will be surfaced by this RemoteDataSource.
+ /// </param>
+
+ // support no new ctor
+ if (this._trigger === undefined) {
+ return new upshot.RemoteDataSource(options);
+ }
+
+ // Optional query options
+ this._sort = null;
+ this._filters = null;
+
+ var dataProvider, dataContext, mapping;
+ if (options) {
+ this._providerParameters = options.providerParameters;
+ this._entityType = options.entityType;
+ dataContext = options.dataContext;
+
+ // support both specification of a provider instance as well as
+ // a provider function. In the latter case, we create the provider
+ // for the user
+ if (!options.provider && !options.dataContext) {
+ // we're in a remote scenario but no context or provider has been specified.
+ // use our default provider in this case.
+ dataProvider = upshot.DataProvider;
+ } else {
+ dataProvider = options.provider;
+ }
+
+ if ($.isFunction(dataProvider)) {
+ dataProvider = new dataProvider();
+ }
+
+ // Acceptable formats for "mapping":
+ // { entityType: "Customer", mapping: Customer }
+ // { entityType: "Customer", mapping: { map: Customer, unmap: unmapCustomer } }
+ // { mapping: { "Customer": Customer } }
+ // { mapping: { "Customer": { map: Customer, unmap: unmapCustomer } } }
+ mapping = options.mapping;
+ if (mapping &&
+ ($.isFunction(mapping) || // Mapping supplied as a "map" function.
+ ($.isFunction(mapping.map) || $.isFunction(mapping.unmap)))) { // Mapping supplied as map/unmap functions.
+
+ if (!this._entityType) {
+ // TODO: Build out the no-type scenario where the DataSource supplies a mapping
+ // function and our merge algorithm ignores any typing from the DataProvider.
+ throw "Need 'entityType' option in order to supply " +
+ ($.isFunction(mapping) ? "a function" : "map/unmap functions") +
+ " for 'mapping' option.";
+ }
+
+ var mappingForType = mapping;
+ mapping = {};
+ mapping[this._entityType] = mappingForType;
+ }
+ }
+
+ var self = this;
+ if (!dataContext) {
+ var implicitCommitHandler;
+ if (!options.bufferChanges) {
+ // since we're not change tracking, define an implicit commit callback
+ // and pass into the DC
+ implicitCommitHandler = function () {
+ self._dataContext._commitChanges({ providerParameters: self._providerParameters });
+ }
+ }
+
+ dataContext = new upshot.DataContext(dataProvider, implicitCommitHandler, mapping);
+ // TODO -- If DS exclusively owns the DC, can we make it non-accumulating?
+ } else if (mapping) {
+ // This will throw if the app is supplying a different mapping for a given entityType.
+ dataContext.addMapping(mapping);
+ }
+
+ this._dataContext = dataContext;
+
+ // define commit[Start,Success,Error] observers
+ var observer = {};
+ $.each(commitEvents, function (unused, name) {
+ observer[name] = function () {
+ self._trigger.apply(self, [name].concat(Array.prototype.slice.call(arguments)));
+ };
+ });
+
+ this._dataContextObserver = observer;
+ this._dataContext.bind(this._dataContextObserver);
+
+ var entitySource = options && options.entityType && this._dataContext.getEntitySet(options.entityType);
+ if (entitySource) {
+ options = $.extend({}, options, { source: entitySource });
+ } else {
+ // Until we can bindToEntitySource, fill in the DataContext-specific methods with some usable defaults.
+ $.each(upshot.EntityView.__dataContextMethodNames, function (index, name) {
+ if (name !== "getDataContext") {
+ self[name] = function () {
+ throw "DataContext-specific methods are not available on RemoteDataSource a result type can be determined. Consider supplying the \"entityType\" option when creating a RemoteDataSource or execute an initial query against your RemoteDatasource to determine the result type.";
+ };
+ }
+ });
+ this.getDataContext = function () {
+ return this._dataContext;
+ };
+ }
+
+ base.constructor.call(this, options);
+
+ // Events specific to RemoteDataSource
+ this._bindFromOptions(options, commitEvents);
+ };
+
+ var instanceMembers = {
+
+ setSort: function (sort) {
+ /// <summary>
+ /// Establishes the sort specification that is to be applied as part of a server query when loading model data.
+ /// </summary>
+ /// <param name="sort">
+ /// &#10;The sort specification to applied when loading model data.
+ /// &#10;Should be supplied as an object of the form &#123; property: &#60;propertyName&#62; [, descending: &#60;bool&#62; ] &#125; or an array of ordered objects of this form.
+ /// &#10;When supplied as null or undefined, the sort specification for this RemoteDataSource is cleared.
+ /// </param>
+ /// <returns type="upshot.RemoteDataSource"/>
+
+ // TODO -- Validate sort specification?
+ this._sort = (sort && !upshot.isArray(sort)) ? [sort] : sort;
+ return this;
+ },
+
+ setFilter: function (filter) {
+ /// <summary>
+ /// Establishes the filter specification that is to be applied as part of a server query when loading model data.
+ /// </summary>
+ /// <param name="filter">
+ /// &#10;The filter specification to applied when loading model data.
+ /// &#10;Should be supplied as an object of the form &#123; property: &#60;propertyName&#62;, value: &#60;propertyValue&#62; [, operator: &#60;operator&#62; ] &#125; or an array of ordered objects of this form.
+ /// &#10;When supplied as null or undefined, the filter specification for this RemoteDataSource is cleared.
+ /// </param>
+ /// <returns type="upshot.RemoteDataSource"/>
+
+ this._filters = filter && this._normalizeFilters(filter);
+ return this;
+ },
+
+ // TODO -- We should do a single setTimeout here instead, just in case N clients request a refresh
+ // in response to callbacks.
+ refresh: function (options, success, error) {
+ /// <summary>
+ /// Initiates an asynchronous get to load model data matching the query established with setSort, setFilter and setPaging.
+ /// </summary>
+ /// <param name="options" optional="true">
+ /// &#10;There are no valid options recognized by RemoteDataSource.
+ /// </param>
+ /// <param name="success" type="Function" optional="true">
+ /// &#10;A success callback with signature function(entities, totalCount).
+ /// </param>
+ /// <param name="error" type="Function" optional="true">
+ /// &#10;An error callback with signature function(httpStatus, errorText, context).
+ /// </param>
+ /// <returns type="upshot.RemoteDataSource"/>
+
+ this._verifyOkToRefresh();
+
+ if ($.isFunction(options)) {
+ error = success;
+ success = options;
+ options = undefined;
+ }
+
+ this._trigger("refreshStart");
+
+ var self = this,
+ onSuccess = function (entitySet, entities, totalCount) {
+ self._bindToEntitySource(entitySet);
+ self._completeRefresh(entities, totalCount, success);
+ },
+ onError = function (httpStatus, errorText, context) {
+ self._failRefresh(httpStatus, errorText, context, error);
+ };
+
+ this._dataContext.__load({
+ entityType: this._entityType,
+ providerParameters: this._providerParameters,
+
+ queryParameters: {
+ filters: this._filters,
+ sort: this._sort,
+ skip: this._skip,
+ take: this._take,
+ includeTotalCount: this._includeTotalCount
+ }
+ }, onSuccess, onError);
+ return this;
+ },
+
+ commitChanges: function (success, error) {
+ /// <summary>
+ /// Initiates an asynchronous commit of any model data edits collected by the DataContext for this RemoteDataSource.
+ /// </summary>
+ /// <param name="success" type="Function" optional="true">
+ /// &#10;A success callback.
+ /// </param>
+ /// <param name="error" type="Function" optional="true">
+ /// &#10;An error callback with signature function(httpStatus, errorText, context).
+ /// </param>
+ /// <returns type="upshot.RemoteDataSource"/>
+
+ this._dataContext.commitChanges({
+ providerParameters: this._providerParameters
+ }, $.proxy(success, this), $.proxy(error, this));
+ return this;
+ },
+
+
+ // Private methods
+
+ _dispose: function () {
+ this._dataContext.unbind(this._dataContextObserver);
+ base._dispose.apply(this, arguments);
+ },
+
+ _bindToEntitySource: function (entitySource) {
+
+ base._bindToEntitySource.call(this, entitySource);
+
+ // Reverting changes at this level with no "entities" arguments will revert all changes in the data context.
+ // TODO -- should AssociatedEntitiesView do the same thing with respect to revertChanges
+ this.revertChanges = function () {
+ return arguments.length > 0
+ ? this._entitySource.revertChanges.apply(this._entitySource, arguments)
+ : this._dataContext.revertChanges();
+ };
+ }
+
+ };
+
+ upshot.RemoteDataSource = upshot.deriveClass(base, ctor, instanceMembers);
+
+}
+///#RESTORE )(this, jQuery, upshot);
diff --git a/src/SPA/upshot/Upshot.Compat.JsViews.js b/src/SPA/upshot/Upshot.Compat.JsViews.js
new file mode 100644
index 00000000..7d64a470
--- /dev/null
+++ b/src/SPA/upshot/Upshot.Compat.JsViews.js
@@ -0,0 +1,135 @@
+/// <reference path="IntelliSense\References.js" />
+///#RESTORE (function (global, $, upshot, undefined)
+{
+ function track(data, options) {
+ if (!options) {
+ $.observable.track(data, null);
+ } else {
+ $.observable.track(data, {
+ beforeChange: options.beforeChange && wrapCallback(options.beforeChange),
+ afterChange: options.afterChange && wrapCallback(options.afterChange),
+ afterEvent: options.afterEvent && wrapCallback(options.afterEvent)
+ });
+ }
+ }
+
+ // transform JsViews to upshot eventArg style
+ function wrapCallback(callback) {
+ return function ($target, type, data) {
+ if (type == "propertyChange") {
+ var eventArg = { newValues: {}, oldValues: {} };
+ eventArg.newValues[data.path] = data.value;
+ eventArg.oldValues[data.path] = $target[data.path];
+ return callback.call(this, $target, "change", eventArg);
+ } else if (type == "arrayChange") {
+ switch (data.change) {
+ case "insert":
+ return callback.call(this, $target, "insert", { index: data.index, items: data.items });
+ case "remove":
+ return callback.call(this, $target, "remove", { index: data.index, items: data.items });
+ case "refresh":
+ return callback.call(this, $target, "replaceAll", { newItems: data.newItems, oldItems: data.oldItems });
+ }
+ }
+ throw "NYI - event '" + type + "' and '" + data.change + "' is not supported!";
+ };
+ }
+
+ function insert(array, index, items) {
+ array.splice.apply(array, [index, 0].concat(items));
+ var eventArguments = {
+ change: "insert",
+ index: index,
+ items: items
+ };
+ $([array]).triggerHandler("arrayChange", eventArguments);
+ }
+
+ function remove(array, index, numToRemove) {
+ var itemsRemoved = array.slice(index, index + numToRemove);
+ array.splice(index, numToRemove);
+ var eventArguments = {
+ change: "remove",
+ index: index,
+ items: itemsRemoved
+ };
+ $([array]).triggerHandler("arrayChange", eventArguments);
+ }
+
+ function refresh(array, newItems) {
+ var oldItems = array.slice(0);
+ array.splice.apply(array, [0, array.length].concat(newItems));
+ var eventArguments = {
+ change: "refresh",
+ oldItems: oldItems,
+ newItems: newItems
+ };
+ $([array]).triggerHandler("arrayChange", eventArguments);
+ }
+
+ function isProperty(item, name) {
+ return !$.isFunction(item[name]);
+ }
+
+ function getProperty(item, name) {
+ return item[name];
+ }
+
+ function setProperty(item, name, value) {
+ var oldValue = item[name];
+ item[name] = value;
+ var eventArguments = {
+ path: name,
+ value: value
+ };
+ $(item).triggerHandler("propertyChange", eventArguments);
+ }
+
+ function isArray(item) {
+ return upshot.isArray(item);
+ }
+
+ function createCollection(initialValues) {
+ return initialValues || [];
+ }
+
+ function asArray(collection) {
+ return collection;
+ }
+
+ function map(item) {
+ return item;
+ }
+
+ function unmap(item) {
+ return item;
+ }
+
+ var observability = upshot.defineNamespace("upshot.observability");
+
+ observability.jsviews = {
+ track: track,
+
+ insert: insert,
+ remove: remove,
+ refresh: refresh,
+
+ isProperty: isProperty,
+ getProperty: getProperty,
+ setProperty: setProperty,
+
+ isArray: isArray,
+ createCollection: createCollection,
+ asArray: asArray,
+
+ map: map,
+ unmap: unmap,
+
+ setContextProperty: $.noop
+ };
+
+ observability.configuration = observability.jsviews;
+
+}
+///#RESTORE )(this, jQuery, upshot);
+
diff --git a/src/SPA/upshot/Upshot.Compat.Knockout.js b/src/SPA/upshot/Upshot.Compat.Knockout.js
new file mode 100644
index 00000000..9b2b0446
--- /dev/null
+++ b/src/SPA/upshot/Upshot.Compat.Knockout.js
@@ -0,0 +1,400 @@
+/// <reference path="IntelliSense\References.js" />
+///#RESTORE (function (global, ko, upshot, undefined)
+{
+ var cacheKey = "koConfig",
+ stateProperty = "EntityState",
+ errorProperty = "EntityError",
+ updatedProperty = "IsUpdated",
+ addedProperty = "IsAdded",
+ deletedProperty = "IsDeleted",
+ changedProperty = "IsChanged",
+ canDeleteProperty = "CanDelete",
+ validationErrorProperty = "ValidationError";
+
+ var splice = function (array, start, howmany, items) {
+ array.splice.apply(array, items ? [start, howmany].concat(items) : [start, howmany]);
+ };
+ var copy = function (value) {
+ var copy = upshot.isArray(value) ? value.slice(0) : value;
+ if (value && (value[upshot.cacheName] !== undefined)) {
+ // Handles in-place array edits
+ copy[upshot.cacheName] = value[upshot.cacheName];
+ }
+ return copy;
+ };
+ var executeAndIgnoreChanges = function (observable, fn) {
+ var tracker = upshot.cache(observable, cacheKey);
+ try {
+ tracker && (tracker.skip = true);
+ fn();
+ } finally {
+ tracker && (tracker.skip = false);
+ }
+ }
+ var punchKoMinified = function (obj, fn, _new) {
+ // replaces all instance of a function with the new version
+ for (var prop in obj) {
+ if (obj[prop] === fn) {
+ obj[prop] = _new;
+ }
+ }
+ }
+
+ // Observability configuration
+ function track(data, options, entityType) {
+ if (!options) {
+ if (upshot.isObject(data)) {
+ for (var prop in data) {
+ var tracker = upshot.cache(data[prop], cacheKey);
+ tracker && tracker.dispose();
+ upshot.deleteCache(data[prop], cacheKey);
+ }
+ } else if (ko.isObservable(data) && data.hasOwnProperty("push")) { // observableArray
+ var tracker = upshot.cache(data[prop], cacheKey);
+ tracker && tracker.dispose();
+ upshot.deleteCache(data, cacheKey);
+ }
+ } else {
+ if (upshot.isObject(data)) {
+ trackObject(data, options, entityType);
+ } else if (ko.isObservable(data) && data.hasOwnProperty("push")) { // observableArray
+ trackArray(data, options);
+ }
+ }
+ }
+
+ function trackObject(data, options, entityType) {
+ ko.utils.arrayForEach(upshot.metadata.getProperties(data, entityType, !!options.includeAssociations), function (property) {
+ var observable = data[property.name];
+ if (ko.isObservable(observable)) {
+ var tracker = function () {
+ var self = this,
+ oldValue = null,
+ createArgs = function (old, _new) {
+ var oldValues = {},
+ newValues = {};
+ oldValues[property.name] = old;
+ newValues[property.name] = _new;
+ return { oldValues: oldValues, newValues: newValues };
+ },
+ realNotifySubscribers = observable.notifySubscribers,
+ notifySubscribers = function (valueToNotify, event) {
+ var before = (event === "beforeChange"),
+ after = (event === undefined || event === "change");
+
+ if (before && options.beforeChange) {
+ oldValue = copy(valueToNotify);
+ }
+ if (self.skip) {
+ realNotifySubscribers.apply(this, arguments);
+ } else {
+ if (before) {
+ if (options.beforeChange) {
+ options.beforeChange(data, "change", createArgs(oldValue));
+ }
+ } else if (after && options.afterChange) {
+ options.afterChange(data, "change", createArgs(oldValue, valueToNotify));
+ }
+ realNotifySubscribers.apply(this, arguments);
+ if (after && options.afterEvent) {
+ options.afterEvent(data, "change", createArgs(oldValue, valueToNotify));
+ }
+ }
+ };
+
+ this.skip = false;
+ this.dispose = function () {
+ punchKoMinified(observable, notifySubscribers, realNotifySubscribers);
+ };
+
+ punchKoMinified(observable, realNotifySubscribers, notifySubscribers);
+ };
+ upshot.cache(observable, cacheKey, new tracker());
+ }
+ });
+ }
+
+ function trackArray(data, options) {
+ // Creates a tracker to raise before/after callbacks for the array
+ var tracker = function (observable) {
+ var self = this,
+ oldValue = null,
+ eventArgs = null,
+ createArgs = function (old, _new) {
+ var args = null,
+ edits = ko.utils.compareArrays(old, _new);
+ for (var i = 0, length = edits.length; i < length; i++) {
+ var type = null;
+ switch (edits[i].status) {
+ case "added":
+ type = "insert";
+ break;
+ case "deleted":
+ type = "remove";
+ break;
+ default:
+ continue;
+ }
+ if (type !== null) {
+ if (args === null) {
+ args = { type: type, args: { index: i, items: [edits[i].value]} };
+ } else {
+ // We'll aggregate two separate edits into a single event
+ args = { type: "replaceAll", args: { newItems: _new, oldItems: old} };
+ break;
+ }
+ }
+ }
+ return eventArgs = args;
+ },
+ realNotifySubscribers = observable.notifySubscribers,
+ notifySubscribers = function (valueToNotify, event) {
+ var before = (event === "beforeChange"),
+ after = (event === undefined || event === "change");
+
+ if (before) {
+ oldValue = copy(valueToNotify);
+ }
+ if (self.skip) {
+ realNotifySubscribers.apply(this, arguments);
+ } else {
+ if (before) {
+ // TODO, kylemc, What does this comment mean?
+ // do nothing; there isn't a meaningful callback we can make with our data
+ } else if (after && options.afterChange) {
+ createArgs(oldValue, valueToNotify);
+ options.afterChange(data, eventArgs.type, eventArgs.args);
+ }
+ realNotifySubscribers.apply(this, arguments);
+ if (after && options.afterEvent) {
+ options.afterEvent(data, eventArgs.type, eventArgs.args);
+ }
+ }
+ };
+
+ this.skip = false;
+ this.dispose = function () {
+ punchKoMinified(observable, notifySubscribers, realNotifySubscribers);
+ };
+
+ punchKoMinified(observable, realNotifySubscribers, notifySubscribers);
+ };
+ upshot.cache(data, cacheKey, new tracker(data));
+ }
+
+ function insert(array, index, items) {
+ executeAndIgnoreChanges(array, function () {
+ splice(array, index, 0, items);
+ });
+ }
+
+ function remove(array, index, numToRemove) {
+ executeAndIgnoreChanges(array, function () {
+ splice(array, index, numToRemove);
+ });
+ }
+
+ function refresh(array, newItems) {
+ executeAndIgnoreChanges(array, function () {
+ splice(array, 0, array().length, newItems);
+ });
+ }
+
+ function isProperty(item, name) {
+ if (name === stateProperty || name === errorProperty) {
+ return false;
+ }
+ var value = item[name];
+ // filter out dependent observables
+ if (ko.isObservable(value) && value.hasOwnProperty("getDependenciesCount")) {
+ return false;
+ }
+ return true;
+ }
+
+ function getProperty(item, name) {
+ return ko.utils.unwrapObservable(item[name]);
+ }
+
+ function setProperty(item, name, value) {
+ executeAndIgnoreChanges(item[name], function () {
+ ko.isObservable(item[name]) ? item[name](value) : item[name] = value;
+ });
+ }
+
+ function isArray(item) {
+ return upshot.isArray(ko.utils.unwrapObservable(item));
+ }
+
+ function createCollection(initialValues) {
+ return ko.observableArray(initialValues || []);
+ }
+
+ function asArray(collection) {
+ return collection();
+ }
+
+ function map(item, type, mapNested, context) {
+ if (upshot.isArray(item)) {
+ var array;
+ if (upshot.isValueArray(item)) {
+ // Primitive values don't get mapped. Avoid iteration over the potentially large array.
+ // TODO: This precludes heterogeneous arrays. Should we test for primitive element type here instead?
+ array = item;
+ } else {
+ array = ko.utils.arrayMap(item, function (value) {
+ return (mapNested || map)(value, type);
+ });
+ }
+ return ko.observableArray(array);
+ } else if (upshot.isObject(item)) {
+ var obj = context || {};
+ // Often, server entities will not carry null-valued nullable-type properties.
+ // Use getProperties here so that we'll map missing property value like these to observables.
+ ko.utils.arrayForEach(upshot.metadata.getProperties(item, type, true), function (prop) {
+ var value = (mapNested || map)(item[prop.name], prop.type);
+ obj[prop.name] = ko.isObservable(value) ? value : ko.observable(value);
+ });
+ if (upshot.metadata.isEntityType(type)) {
+ upshot.addEntityProperties(obj, type);
+ }
+ // addUpdatedProperties is applied shallowly. This allows map (which is applied deeply) to add the
+ // properties at each level or for custom type mapping to decide whether the properties should be added
+ upshot.addUpdatedProperties(obj, type);
+ return obj;
+ }
+ return item;
+ }
+
+ function unmap(item, entityType) {
+ item = ko.utils.unwrapObservable(item);
+ if (upshot.isArray(item)) {
+ var array = ko.utils.arrayMap(item, function (value) {
+ return unmap(value);
+ });
+ return array;
+ } else if (upshot.isObject(item)) {
+ var obj = {};
+ if (item.hasOwnProperty("__type")) { // make sure __type flows through first
+ obj["__type"] = ko.utils.unwrapObservable(item["__type"]);
+ }
+ ko.utils.arrayForEach(upshot.metadata.getProperties(item, entityType), function (prop) {
+ // TODO: determine if there are scenarios where we want to support hasOwnProperty returning false
+ if (item.hasOwnProperty(prop.name)) {
+ obj[prop.name] = unmap(item[prop.name], prop.type);
+ }
+ });
+ return obj;
+ }
+ return item;
+ }
+
+ function setContextProperty(item, kind, name, value) {
+ if (kind === "entity") {
+ // TODO -- do we want these 'reserved' properties to be configurable?
+ var prop;
+ if (name === "state") {
+ // set item.EntityState
+ prop = stateProperty;
+ } else if (name === "error") {
+ // set item.EntityError
+ prop = errorProperty;
+ }
+ if (ko.isObservable(item[prop])) {
+ setProperty(item, prop, value);
+ }
+ } else if (kind === "property") {
+ // set item.name.IsUpdated
+ if (ko.isObservable(item[name])) {
+ var observable = item[name][updatedProperty];
+ observable && observable(value);
+ }
+ }
+ }
+
+ var observability = upshot.defineNamespace("upshot.observability");
+
+ observability.knockout = {
+ track: track,
+
+ insert: insert,
+ remove: remove,
+ refresh: refresh,
+
+ isProperty: isProperty,
+ getProperty: getProperty,
+ setProperty: setProperty,
+
+ isArray: isArray,
+ createCollection: createCollection,
+ asArray: asArray,
+
+ map: map,
+ unmap: unmap,
+
+ setContextProperty: setContextProperty
+ };
+
+ observability.configuration = observability.knockout;
+
+ // KO entity extensions
+ function addValidationErrorProperty(entity, prop) {
+ var observable = entity[prop];
+ if (observable) { // Custom mappings might not include this property.
+ observable[validationErrorProperty] = ko.computed(function () {
+ var allErrors = entity[errorProperty]();
+ if (allErrors && allErrors.ValidationErrors) {
+ var matchingError = ko.utils.arrayFirst(allErrors.ValidationErrors, function (error) {
+ return ko.utils.arrayIndexOf(error.SourceMemberNames, prop) >= 0;
+ });
+ if (matchingError) {
+ return matchingError.Message;
+ }
+ }
+ return null;
+ });
+ }
+ }
+
+ upshot.addEntityProperties = function (entity, entityType) {
+ // Adds properties to an entity that will be managed by upshot
+ entity[stateProperty] = ko.observable(upshot.EntityState.Unmodified);
+ entity[errorProperty] = ko.observable();
+
+ // Minimize view boilerplate by exposing state flags
+ var containsState = function (states) {
+ return ko.utils.arrayIndexOf(states, entity[stateProperty]()) !== -1;
+ };
+ var es = upshot.EntityState;
+ entity[updatedProperty] = ko.computed(function () {
+ return containsState([es.ClientUpdated, es.ServerUpdating]);
+ });
+ entity[addedProperty] = ko.computed(function () {
+ return containsState([es.ClientAdded, es.ServerAdding]);
+ });
+ entity[deletedProperty] = ko.computed(function () {
+ return containsState([es.ClientDeleted, es.ServerDeleting, es.Deleted]);
+ });
+ entity[changedProperty] = ko.computed(function () {
+ return !containsState([es.Unmodified, es.Deleted]);
+ });
+ entity[canDeleteProperty] = ko.computed(function () {
+ return !(entity[addedProperty]() || entity[deletedProperty]());
+ });
+
+ // TODO -- these are only applied a single level deep; see if there's a consistent way to apply CTs
+ ko.utils.arrayForEach(upshot.metadata.getProperties(entity, entityType, true), function (prop) {
+ addValidationErrorProperty(entity, prop.name);
+ });
+ }
+
+ upshot.addUpdatedProperties = function (obj, type) {
+ ko.utils.arrayForEach(upshot.metadata.getProperties(obj, type, false), function (prop) {
+ var observable = obj[prop.name];
+ if (ko.isObservable(observable)) {
+ observable[updatedProperty] = ko.observable(false);
+ }
+ });
+ }
+}
+///#RESTORE )(this, ko, upshot); \ No newline at end of file
diff --git a/src/SPA/upshot/Upshot.Compat.WinJS.js b/src/SPA/upshot/Upshot.Compat.WinJS.js
new file mode 100644
index 00000000..5c5191a5
--- /dev/null
+++ b/src/SPA/upshot/Upshot.Compat.WinJS.js
@@ -0,0 +1,495 @@
+/// <reference path="IntelliSense\References.js" />
+///#RESTORE (function (global, $, WinJS, upshot, undefined)
+{
+ // Customizations to WinJS observability to interop with Upshot.
+
+ var ObservableProxy = WinJS.Binding.as({}).constructor;
+ var base = ObservableProxy.prototype;
+ var RiaObservableProxy = WinJS.Class.derive(base, function (data, beforeChange, afterChange, afterEvent) {
+ base.constructor.call(this, data);
+ this._beforeChange = beforeChange;
+ this._afterChange = afterChange;
+ this._afterEvent = afterEvent;
+ }, {
+ updateProperty: function (name, value) {
+ var oldValue = this._backingData[name];
+ var newValue = WinJS.Binding.unwrap(value);
+ if (oldValue !== newValue) {
+ var eventArguments = { path: name, value: value };
+
+ if (this._beforeChange) {
+ this._beforeChange(this._backingData, "propertyChange", eventArguments);
+ }
+
+ this._backingData[name] = newValue;
+
+ if (this._afterChange) {
+ this._afterChange(this._backingData, "propertyChange", eventArguments);
+ }
+
+ var notifyPromise = this._notifyListeners(name, newValue, oldValue),
+ afterEvent = this._afterEvent;
+ if (afterEvent) {
+ return notifyPromise.then(function () {
+ afterEvent(this._backingData, "propertyChange", eventArguments);
+ }); // TODO: Implement "cancel" too, as that's what we'll get when events are coalesced.
+ } else {
+ return notifyPromise;
+ }
+ }
+ return WinJS.Promise.as();
+ }
+ });
+
+ function track(data, options) {
+ if (!options) {
+ delete data._getObservable;
+ } else {
+ if ($.isArray(data)) {
+ // TODO: Integrate WinJS array observability when it exists. We assume jQuery observability for arrays until then.
+ $.observable.track(data, {
+ beforeChange: options.beforeChange,
+ afterChange: options.afterChange,
+ afterEvent: options.afterEvent
+ });
+ } else {
+ if ($.inArray("_getObservable", Object.getOwnPropertyNames(data)) >= 0) {
+ throw "Entities added via Upshot/JS must not previously have been treated with WinJS.Binding.as";
+ }
+
+ var observable = new RiaObservableProxy(data, options.beforeChange, options.afterChange, options.afterEvent);
+ observable.backingData = data;
+ Object.defineProperty(data, "_getObservable", {
+ value: function () { return observable; },
+ enumerable: false,
+ writable: false
+ });
+ }
+ }
+ }
+
+ // TODO: Integrate WinJS array observability when it exists. We assume jQuery observability for arrays until then.
+ function createInsertDeferredEvent(array, index, items) {
+ return function () {
+ var eventArguments = {
+ change: "insert",
+ index: index,
+ items: items
+ };
+ $([array]).triggerHandler("arrayChange", eventArguments);
+ };
+ }
+
+ // TODO: Integrate WinJS array observability when it exists. We assume jQuery observability for arrays until then.
+ function createRemoveDeferredEvent(array, index, itemsRemoved) {
+ return function () {
+ var eventArguments = {
+ change: "remove",
+ index: index,
+ items: itemsRemoved
+ };
+ $([array]).triggerHandler("arrayChange", eventArguments);
+ };
+ }
+
+ // TODO: Integrate WinJS array observability when it exists. We assume jQuery observability for arrays until then.
+ function createRefreshDeferredEvent(array, oldItems, newItems) {
+ return function () {
+ var eventArguments = {
+ change: "refresh",
+ oldItems: oldItems,
+ newItems: newItems
+ };
+ $([array]).triggerHandler("arrayChange", eventArguments);
+ };
+ }
+
+ function createSetPropertyDeferredEvent(item, name, oldValue, newValue) {
+ var observable = WinJS.Binding.as(item);
+ return function () {
+ observable._notifyListeners(name, newValue, oldValue);
+ };
+ }
+
+ var observability = upshot.defineNamespace("upshot.observability");
+
+ observability.winjs = {
+ track: track,
+
+ createInsertDeferredEvent: createInsertDeferredEvent,
+ createRemoveDeferredEvent: createRemoveDeferredEvent,
+ createRefreshDeferredEvent: createRefreshDeferredEvent,
+ createSetPropertyDeferredEvent: createSetPropertyDeferredEvent
+ };
+
+ observability.configuration = observability.winjs;
+
+ // WinJS DataSource support follows.
+
+ var DataAdaptor = WinJS.Class.define(function (riaDataSource) {
+ var dataSource;
+ if (riaDataSource.applyLocalQuery) {
+ dataSource = $.dataSource.unwrapHack(riaDataSource.getEntities());
+ } else {
+ dataSource = riaDataSource;
+ }
+
+ this._dataSource = dataSource;
+ this._dataSourceObserver = null;
+ this._arrayChangeHandler = null;
+
+ this.compareByIdentity = true;
+ }, {
+ dispose: function () {
+ if (this._dataSource) {
+ this._dataSource.removeObserver(this._dataSourceObserver);
+ $([this._dataSource.getEntities()]).unbind("arrayChange", this._arrayChangeHandler);
+ this._dataSource = null;
+ }
+ },
+
+ setNotificationHandler: function (notificationHandler) {
+ var that = this;
+
+ function handleArrayChange(event, eventArgs) {
+ var index;
+ notificationHandler.beginNotifications();
+ switch (eventArgs.change) {
+ case "refresh":
+ notificationHandler.invalidateAll();
+ break;
+
+ case "insert":
+ var entities = that._dataSource.getEntities(),
+ previousItem = index === 0 ? null : entities[index - 1],
+ nextItem = index === entities.length - 1 ? null : entities[index + 1];
+
+ index = eventArgs.index;
+ $.each(eventArgs.items, function () {
+ var item = { key: that._getEntityKey(this), data: this };
+ notificationHandler.inserted(item, previousItem && that._getEntityKey(previousItem), nextItem && that._getEntityKey(nextItem), index);
+ previousItem = this;
+ index++;
+ });
+ break;
+
+ case "remove":
+ index = eventArgs.index;
+ $.each(eventArgs.items, function () {
+ notificationHandler.removed(that._getEntityKey(this), index);
+ });
+ break;
+
+ case "move": // TODO: Our data sources never issue move events presently, but this could still be implemented.
+ default:
+ throw "Unexpected array changed event.";
+ }
+ notificationHandler.endNotifications();
+ }
+
+ this._dataSourceObserver = {
+ propertyChanged: function (entity, property, value) {
+ notificationHandler.changed({ key: that._getEntityKey(entity), data: entity });
+ }
+ };
+ this._dataSource.addObserver(this._dataSourceObserver);
+
+ // TODO: We should have a platform-neutral way of communicating array changes, so we
+ // don't have a hard dependency on a single observability pattern.
+ this._arrayChangeHandler = handleArrayChange;
+ $([this._dataSource.getEntities()]).bind("arrayChange", handleArrayChange);
+ },
+
+ // compareByIdentity: set in constructor
+ // itemsFromStart: not implemented
+ // itemsFromEnd: not implemented
+ // itemsFromKey: not implemented
+
+ itemsFromIndex: function (index, countBefore, countAfter) {
+ var skip = index - countBefore,
+ take = countBefore + 1 + countAfter,
+ allEntities = this._dataSource.getEntities(),
+ entities = allEntities.slice(skip, skip + take),
+ that = this;
+ return WinJS.Promise.wrap({
+ items: $.map(entities, function (entity) {
+ return { key: that._getEntityKey(entity), data: entity };
+ }),
+ offset: countBefore,
+ totalCount: allEntities.length,
+ absoluteIndex: index
+ // TODO: atEnd?
+ });
+ },
+
+ // itemsFromDescription: not implemented
+
+ getCount: function () {
+ return WinJS.Promise.wrap(this._dataSource.getEntities().length);
+ },
+
+ // NOTE: We don't implement these, as there is an edit model inherent in the
+ // Upshot data source passed into this adaptor. This adaptor component becomes much
+ // more complex if we allow for editing over both WinJS's and Upshot's data sources.
+ //
+ // insertAtStart: function (key, data) {
+ // insertBefore: function (key, data, nextKey, nextIndexHint) {
+ // insertAfter: function (key, data, previousKey, previousIndexHint) {
+ // insertAtEnd: function (key, data) {
+ // change: function (key, newData, indexHint) {
+ // moveToStart: not implemented
+ // moveBefore: not implemented
+ // moveAfter: not implemented
+ // moveToEnd: not implemented
+ // remove: not implemented
+
+ _getEntityKey: function (entity) {
+ return this._dataSource.getEntityId(entity);
+ }
+ });
+
+ var VirtualizingDataAdaptor = WinJS.Class.define(function (options) {
+
+ this._dataContext = options.dataContext;
+ if (!this._dataContext) {
+ var implicitCommitHandler;
+ if (!options.bufferChanges) {
+ // since we're not change tracking, define an implicit commit callback
+ // and pass into the DC
+ var self = this;
+ implicitCommitHandler = function () {
+ self._dataContext._commitChanges({ providerParameters: self._providerParameters });
+ }
+ }
+ this._dataContext = new upshot.DataContext(new upshot.riaDataProvider(), implicitCommitHandler);
+ }
+ this._providerParameters = options.providerParameters;
+ this._entityType = options.entityType;
+
+ this._lastTotalCount = null;
+ this._notificationHandler = null;
+ this._entitySet = null;
+ this._entitySetObserver = null;
+ this._arrayChangeHandler = null;
+
+ this._sort = null;
+ this._filters = null;
+
+ // TODO: Ick!
+ this._setFilter = upshot.RemoteDataSource.prototype.setFilter;
+ this._processFilter = upshot.DataSource.prototype._processFilter;
+
+ this.compareByIdentity = true; // TODO: How do this control list rerender vs. div surgery?
+ }, {
+ entitySet: {
+ get: function () {
+ return this._entitySet;
+ }
+ },
+
+ filter: {
+ set: function (filter) {
+ this._setFilter(filter);
+ }
+ },
+
+ sort: {
+ set: function (sort) {
+ this._sort = sort;
+ }
+ },
+
+ refresh: function () {
+ this._notificationHandler.invalidateAll();
+ },
+
+ dispose: function () {
+ if (this._entitySet) {
+ this._entitySet.removeObserver(this._entitySetObserver);
+ $([this._entitySet.getEntities()]).unbind("arrayChange", this._arrayChangeHandler);
+ this._entitySet = null;
+ }
+ },
+
+ setNotificationHandler: function (notificationHandler) {
+ this._notificationHandler = notificationHandler;
+ },
+
+ // compareByIdentity: set in constructor
+ // itemsFromStart: not implemented
+ // itemsFromEnd: not implemented
+ // itemsFromKey: not implemented
+
+ itemsFromIndex: function (index, countBefore, countAfter) {
+ var that = this;
+ return new WinJS.Promise(function (complete, error) {
+ var skip = index - countBefore,
+ take = countBefore + 1 + countAfter,
+ success = function (entities) {
+ var result = {
+ items: $.map(entities, function (entity) {
+ return { key: that._getEntityKey(entity), data: entity };
+ }),
+ offset: countBefore,
+ totalCount: that._lastTotalCount,
+ absoluteIndex: index
+ // TODO: atEnd?
+ };
+ complete(result);
+ };
+ that._loadEntities(skip, take, success);
+ });
+ },
+
+ // itemsFromDescription: not implemented
+
+ getCount: function () {
+ if (this._lastTotalCount !== null) {
+ return WinJS.Promise.wrap(this._lastTotalCount);
+ } else {
+ var that = this;
+ return new WinJS.Promise(function (complete, error) {
+ // Ask for zero entities here. We merely want the total count from the server.
+ that._loadEntities(null, 0, function () {
+ complete(that._lastTotalCount);
+ });
+ });
+ }
+ },
+
+ // insertAtStart: function (key, data) {
+ // insertBefore: function (key, data, nextKey, nextIndexHint) {
+ // insertAfter: function (key, data, previousKey, previousIndexHint) {
+ // insertAtEnd: function (key, data) {
+ // change: function (key, newData, indexHint) {
+ // moveToStart: not implemented
+ // moveBefore: not implemented
+ // moveAfter: not implemented
+ // moveToEnd: not implemented
+ // remove: not implemented
+
+ _loadEntities: function (skip, take, success, fail) {
+ var that = this,
+ loadSucceeded = function (entitySet, entities, totalCount) {
+ that._bindToEntitySet(entitySet);
+ that._lastTotalCount = totalCount; // TODO: Is there a way to event to the ListDataSource that the count has changed? Is it invalidateAll?
+ if (success) {
+ success.call(that, entities);
+ }
+ },
+ loadFailed = function (statusText, error) {
+ if (fail) {
+ fail.call(that, statusText, error);
+ }
+ };
+
+ this._dataContext.__load({
+ providerParameters: this._providerParameters,
+ entityType: this._entityType,
+
+ queryParameters: {
+ filters: this._filters,
+ sort: this._sort,
+ skip: skip,
+ take: take,
+ includeTotalCount: true
+ }
+ }, loadSucceeded, loadFailed);
+ },
+
+ _bindToEntitySet: function (entitySet) {
+ if (this._entitySet) {
+ return;
+ }
+
+ this._entitySet = entitySet;
+
+ var that = this;
+
+ function handleArrayChange(event, eventArgs) {
+ that._notificationHandler.beginNotifications();
+ switch (eventArgs.change) {
+ case "insert":
+ // Such inserts are from other queries returning entities of this type
+ // or they are internal inserts that might not even be committed yet.
+ // The app should explicitly refresh their ListView here.
+ break;
+
+ case "remove":
+ $.each(eventArgs.items, function () {
+ that._notificationHandler.removed(that._getEntityKey(this));
+ });
+ break;
+
+ case "move": // TODO: Our entity sets don't issue move events presently.
+ case "refresh": // TODO: Our entity sets don't issue refresh events presently.
+ default:
+ throw "Unexpected array changed event.";
+ break;
+ }
+ that._notificationHandler.endNotifications();
+ }
+
+ this._entitySetObserver = {
+ propertyChanged: function (entity, property, value) {
+ that._notificationHandler.changed({ key: that._getEntityKey(entity), data: entity });
+ }
+ };
+ entitySet.addObserver(this._entitySetObserver);
+
+ // TODO: We should have a platform-neutral way of communicating array changes, so we
+ // don't have a hard dependency on a single observability pattern.
+ this._arrayChangeHandler = handleArrayChange;
+ $([entitySet.getEntities()]).bind("arrayChange", handleArrayChange);
+ },
+
+ _getEntityKey: function (entity) {
+ return this._entitySet.getEntityId(entity);
+ }
+
+ // TODO: Investigate why the ListView stops loading if you drag to the right too aggressive.
+ });
+
+ WinJS.Namespace.define("upshot.WinJS", {
+ DataSource: function (riaDataSource) {
+ var dataAdaptor = new DataAdaptor(riaDataSource),
+ dataSource = new WinJS.UI.ListDataSource(dataAdaptor);
+
+ dataSource.dispose = function () {
+ dataAdaptor.dispose();
+ };
+
+ return dataSource;
+ },
+ VirtualizingDataSource: function (options) {
+ var dataAdaptor = new VirtualizingDataAdaptor(options),
+ dataSource = new WinJS.UI.ListDataSource(dataAdaptor);
+
+ Object.defineProperty(dataSource, "filter", {
+ set: function (filter) {
+ dataAdaptor.filter = filter;
+ }
+ });
+ Object.defineProperty(dataSource, "sort", {
+ set: function (sort) {
+ dataAdaptor.sort = sort;
+ }
+ });
+ Object.defineProperty(dataSource, "entitySet", {
+ get: function () {
+ return dataAdaptor.entitySet;
+ }
+ });
+ dataSource.refresh = function () {
+ dataAdaptor.refresh();
+ };
+ dataSource.dispose = function () {
+ dataAdaptor.dispose();
+ };
+
+ return dataSource;
+ }
+ });
+
+}
+///#RESTORE )(this, jQuery, WinJS, upshot);
diff --git a/src/SPA/upshot/Upshot.Compat.jQueryUI.js b/src/SPA/upshot/Upshot.Compat.jQueryUI.js
new file mode 100644
index 00000000..1b38e5a0
--- /dev/null
+++ b/src/SPA/upshot/Upshot.Compat.jQueryUI.js
@@ -0,0 +1,175 @@
+/// <reference path="IntelliSense\References.js" />
+///#RESTORE (function (global, $, upshot, undefined)
+{
+
+ var cachedObservableKey = "__cachedObservable__",
+ observable = $.observable.Observable;
+
+ $.observable = function (data) {
+ return upshot.cache(data, cachedObservableKey) || new observable(data);
+ };
+
+ var base = observable.prototype;
+
+ var UpshotObservable = upshot.deriveClass(base, function (data, beforeChange, afterChange, afterEvent) {
+ observable.call(this, data);
+ this.beforeChange = beforeChange;
+ this.afterChange = afterChange;
+ this.afterEvent = afterEvent;
+ }, {
+ _property: function (oldValues, newValues) {
+ if (this.beforeChange) {
+ this.beforeChange(this.data, "change", { oldValues: oldValues, newValues: newValues });
+ }
+
+ return base._property.apply(this, arguments);
+ },
+
+ _insert: function (index, items) {
+ if (this.beforeChange) {
+ this.beforeChange(this.data, "insert", { index: index, items: items });
+ }
+
+ base._insert.apply(this, arguments);
+ },
+
+ _remove: function (index, numToRemove) {
+ if (this.beforeChange) {
+ var items = this.data.slice(index, index + numToRemove);
+ this.beforeChange(this.data, "remove", { index: index, items: items });
+ }
+
+ base._remove.apply(this, arguments);
+ },
+
+ replaceAll: function (newItems) {
+ if (this.beforeChange) {
+ this.beforeChange(this.data, "replaceAll", { oldItems: this.data.slice(0), newItems: newItems });
+ }
+
+ base.replaceAll.apply(this, arguments);
+ },
+
+ _trigger: function (type, eventData) {
+ if (this.afterChange) {
+ this.afterChange(this.data, type, eventData);
+ }
+
+ base._trigger.apply(this, arguments);
+
+ if (this.afterEvent) {
+ this.afterEvent(this.data, type, eventData);
+ }
+ }
+
+ });
+
+ // TODO, kylemc, Knockout compatibility uses an "entityType" parameter here and tracks only metadata-specified properties.
+ function track(data, options) {
+ if (!options) {
+ upshot.deleteCache(data, cachedObservableKey);
+ } else {
+ upshot.cache(data, cachedObservableKey, new UpshotObservable(data, options.beforeChange, options.afterChange, options.afterEvent));
+ }
+ }
+
+ function insert(array, index, items) {
+ array.splice.apply(array, [index, 0].concat(items));
+ var eventArguments = {
+ index: index,
+ items: items
+ };
+ $([array]).triggerHandler("insert", eventArguments);
+ }
+
+ function remove(array, index, numToRemove) {
+ var itemsRemoved = array.slice(index, index + numToRemove);
+ array.splice(index, numToRemove);
+ var eventArguments = {
+ index: index,
+ items: itemsRemoved
+ };
+ $([array]).triggerHandler("remove", eventArguments);
+ }
+
+ function refresh(array, newItems) {
+ var oldItems = array.slice(0);
+ array.splice.apply(array, [0, array.length].concat(newItems));
+ var eventArguments = {
+ oldItems: oldItems,
+ newItems: newItems
+ };
+ $([array]).triggerHandler("replaceAll", eventArguments);
+ }
+
+ function isProperty(item, name) {
+ return !$.isFunction(item[name]);
+ }
+
+ function getProperty(item, name) {
+ return item[name];
+ }
+
+ function setProperty(item, name, value) {
+ var oldValue = item[name];
+ item[name] = value;
+
+ var oldValues = {},
+ newValues = {};
+ oldValues[name] = oldValue;
+ newValues[name] = value;
+ var eventArguments = {
+ oldValues: oldValues,
+ newValues: newValues
+ };
+ $(item).triggerHandler("change", eventArguments);
+ }
+
+ function isArray(item) {
+ return upshot.isArray(item);
+ }
+
+ function createCollection(initialValues) {
+ return initialValues || [];
+ }
+
+ function asArray(collection) {
+ return collection;
+ }
+
+ function map(item) {
+ return item;
+ }
+
+ function unmap(item) {
+ return item;
+ }
+
+ var observability = upshot.defineNamespace("upshot.observability");
+
+ observability.jquery = {
+ track: track,
+
+ insert: insert,
+ remove: remove,
+ refresh: refresh,
+
+ isProperty: isProperty,
+ getProperty: getProperty,
+ setProperty: setProperty,
+
+ isArray: isArray,
+ createCollection: createCollection,
+ asArray: asArray,
+
+ map: map,
+ unmap: unmap,
+
+ setContextProperty: $.noop
+ };
+
+ observability.configuration = observability.jquery;
+
+}
+///#RESTORE )(this, jQuery, upshot);
+
diff --git a/src/SPA/upshot/upshot.dataview.js b/src/SPA/upshot/upshot.dataview.js
new file mode 100644
index 00000000..b787866a
--- /dev/null
+++ b/src/SPA/upshot/upshot.dataview.js
@@ -0,0 +1,162 @@
+/// <reference path="IntelliSense\References.js" />
+///#RESTORE (function (global, $, upshot, undefined)
+{
+ var dataSourceEvents = ["refreshStart", "refreshSuccess", "refreshError", "commitStart", "commitSuccess", "commitError", "entityStateChanged", "refreshNeeded"];
+
+ function normalizeSort(sort) {
+ if (!sort || $.isFunction(sort)) {
+ return sort;
+ }
+
+ if (upshot.isArray(sort)) {
+ return $.map(sort, function (item) {
+ var descending = item.charAt(0) === "-";
+ var property = descending ? item.substr(1) : item;
+ return { property: property, descending: descending };
+ });
+ }
+
+ var descending = sort.charAt(0) === "-";
+ var property = descending ? sort.substr(1) : sort;
+ return { property: property, descending: descending };
+ }
+
+ function normalizeFilter(filter) {
+ if (!filter) {
+ return filter;
+ }
+
+ filter = upshot.isArray(filter) ? filter : [filter];
+ var filters = [];
+ for (var i = 0; i < filter.length; i++) {
+ var filterPart = filter[i];
+ if (!$.isFunction(filterPart)) {
+ for (var filterProperty in filterPart) {
+ if (filterPart.hasOwnProperty(filterProperty)) {
+ var filterValue = filterPart[filterProperty];
+ if (filterValue && filterValue.hasOwnProperty("value")) {
+ filters.push({ property: filterProperty, operator: filterValue.operator, value: filterValue.value });
+ } else {
+ filters.push({ property: filterProperty, value: filterValue });
+ }
+ }
+ }
+ } else {
+ filters.push(filterPart);
+ }
+ }
+ return filters;
+ }
+
+ function normalizePaging(paging) {
+ return paging && { skip: paging.offset, take: paging.limit, includeTotalCount: !!paging.includeTotalCount };
+ }
+
+ var queryOptions = {
+ paging: { setter: "setPaging", normalize: normalizePaging },
+ filter: { setter: "setFilter", normalize: normalizeFilter },
+ sort: { setter: "setSort", normalize: normalizeSort }
+ };
+
+ $.widget("upshot.dataview", $.ui.dataview, {
+
+ _create: function () {
+ var that = this;
+ this.widgetEventPrefix = "dataview";
+ this.options.source = function (request, success, error) {
+ that.dataSource.refresh(request.refreshOptions, success, error);
+ };
+ },
+
+ _init: function () {
+ this._super("_init");
+ this.dataSource = this._createDataSource();
+ this.result = this.dataSource.getEntities();
+
+ var that = this;
+ var observer = {};
+ $.each(dataSourceEvents, function (unused, name) {
+ observer[name] = function () {
+ $(that).trigger(name, arguments);
+ };
+ });
+ this._observer = observer;
+ this.dataSource.bind(observer);
+
+ var slice = Array.prototype.slice;
+ $.each(["commitChanges", "revertUpdates", "revertChanges"], function (unused, key) {
+ if (that.dataSource[key]) {
+ that[key] = function () {
+ return that.dataSource[key].apply(that.dataSource, $.map(slice.call(arguments), function (arg) {
+ return $.proxy(arg, that) || arg;
+ }));
+ }
+ }
+ });
+ },
+
+ _destroy: function () {
+ if (this._observer) {
+ this.dataSource.unbind(this._observer);
+ this._observer = null;
+ }
+ this._super("_destroy");
+ },
+
+ _setOption: function (key, value) {
+ var query = queryOptions[key];
+ if (query) {
+ this.dataSource[query.setter](query.normalize(value));
+ } else {
+ this.dataSource.option(key, value && $.inArray(key, dataSourceEvents) >= 0 ? $.proxy(value, this) : value);
+ }
+ this._super("_setOption", key, value);
+ }
+
+ });
+
+ $.widget("upshot.remoteDataview", $.upshot.dataview, {
+
+ _createDataSource: function () {
+ var options = {};
+ for (var key in this.options) {
+ var query = queryOptions[key];
+ if (query) {
+ options[key] = query.normalize(this.options[key]);
+ } else if (key !== "source") {
+ options[key] = $.inArray(key, dataSourceEvents) >= 0 ? $.proxy(this.options[key], this) : this.options[key];
+ }
+ }
+ options.result = options.result || this.result;
+ return new upshot.RemoteDataSource(options);
+ }
+
+ });
+
+ $.widget("upshot.localDataview", $.upshot.dataview, {
+
+ _createDataSource: function () {
+ var options = {};
+ for (var key in this.options) {
+ var query = queryOptions[key];
+ if (query) {
+ options[key] = query.normalize(this.options[key]);
+ } else if (key === "input") {
+ if ($.isArray(this.options.input)) {
+ options.source = this.options.input;
+ } else {
+ options.source = this.options.input.dataSource;
+ }
+ } else if (key !== "source") {
+ options[key] = $.inArray(key, dataSourceEvents) >= 0 ? $.proxy(this.options[key], this) : this.options[key];
+ }
+ }
+ options.result = options.result || this.result;
+ return new upshot.LocalDataSource(options);
+ }
+
+ });
+
+}
+///#RESTORE )(this, jQuery, upshot);
+
diff --git a/src/Settings.StyleCop b/src/Settings.StyleCop
new file mode 100644
index 00000000..30e8d205
--- /dev/null
+++ b/src/Settings.StyleCop
@@ -0,0 +1,381 @@
+<StyleCopSettings Version="105">
+ <GlobalSettings>
+ <StringProperty Name="MergeSettingsFiles">NoMerge</StringProperty>
+ </GlobalSettings>
+ <Parsers>
+ <Parser ParserId="StyleCop.CSharp.CsParser">
+ <ParserSettings>
+ <BooleanProperty Name="AnalyzeDesignerFiles">False</BooleanProperty>
+ </ParserSettings>
+ </Parser>
+ </Parsers>
+ <Analyzers>
+ <Analyzer AnalyzerId="StyleCop.CSharp.DocumentationRules">
+ <Rules>
+ <Rule Name="ElementsMustBeDocumented">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="PartialElementsMustBeDocumented">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="EnumerationItemsMustBeDocumented">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="DocumentationMustContainValidXml">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="ElementDocumentationMustHaveSummary">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="PartialElementDocumentationMustHaveSummary">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="ElementDocumentationMustHaveSummaryText">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="PartialElementDocumentationMustHaveSummaryText">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="ElementDocumentationMustNotHaveDefaultSummary">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="ElementParametersMustBeDocumented">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="ElementParameterDocumentationMustMatchElementParameters">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="ElementParameterDocumentationMustDeclareParameterName">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="ElementParameterDocumentationMustHaveText">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="ElementReturnValueMustBeDocumented">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="ElementReturnValueDocumentationMustHaveText">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="VoidReturnValueMustNotBeDocumented">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="GenericTypeParametersMustBeDocumented">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="GenericTypeParametersMustBeDocumentedPartialClass">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="GenericTypeParameterDocumentationMustMatchTypeParameters">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="GenericTypeParameterDocumentationMustDeclareParameterName">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="GenericTypeParameterDocumentationMustHaveText">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="PropertySummaryDocumentationMustMatchAccessors">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="PropertySummaryDocumentationMustOmitSetAccessorWithRestrictedAccess">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="ElementDocumentationMustNotBeCopiedAndPasted">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="SingleLineCommentsMustNotUseDocumentationStyleSlashes">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="DocumentationTextMustNotBeEmpty">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="DocumentationTextMustContainWhitespace">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="DocumentationMustMeetCharacterPercentage">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="IncludedDocumentationXPathDoesNotExist">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="IncludeNodeDoesNotContainValidFileAndPath">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="InheritDocMustBeUsedWithInheritingClass">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="FileMustHaveHeader">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="FileHeaderMustShowCopyright">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="FileHeaderMustHaveCopyrightText">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="FileHeaderFileNameDocumentationMustMatchTypeName">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="ConstructorSummaryDocumentationMustBeginWithStandardText">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="DestructorSummaryDocumentationMustBeginWithStandardText">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="DocumentationHeadersMustNotContainBlankLines">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="FileHeaderMustContainFileName">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="FileHeaderFileNameDocumentationMustMatchFileName">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="FileHeaderMustHaveValidCompanyText">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ </Rules>
+ <AnalyzerSettings>
+ <BooleanProperty Name="IgnorePrivates">True</BooleanProperty>
+ <BooleanProperty Name="IgnoreInternals">True</BooleanProperty>
+ </AnalyzerSettings>
+ </Analyzer>
+ <Analyzer AnalyzerId="StyleCop.CSharp.LayoutRules">
+ <Rules>
+ <Rule Name="ClosingCurlyBracketMustBeFollowedByBlankLine">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="ElementsMustBeSeparatedByBlankLine">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="SingleLineCommentMustBePrecededByBlankLine">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="SingleLineCommentsMustNotBeFollowedByBlankLine">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="AllAccessorsMustBeMultiLineOrSingleLine">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ </Rules>
+ <AnalyzerSettings />
+ </Analyzer>
+ <Analyzer AnalyzerId="StyleCop.CSharp.MaintainabilityRules">
+ <Rules>
+ <Rule Name="DebugAssertMustProvideMessageText">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="FileMayOnlyContainASingleClass">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="StatementMustNotUseUnnecessaryParenthesis">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="CodeAnalysisSuppressionMustHaveJustification">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">True</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="FieldsMustBePrivate">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ </Rules>
+ <AnalyzerSettings />
+ </Analyzer>
+ <Analyzer AnalyzerId="StyleCop.CSharp.NamingRules">
+ <Rules>
+ <Rule Name="FieldNamesMustNotBeginWithUnderscore">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ </Rules>
+ <AnalyzerSettings>
+ <CollectionProperty Name="Hungarian">
+ <Value>as</Value>
+ <Value>do</Value>
+ <Value>id</Value>
+ <Value>if</Value>
+ <Value>in</Value>
+ <Value>is</Value>
+ <Value>my</Value>
+ <Value>no</Value>
+ <Value>on</Value>
+ <Value>to</Value>
+ <Value>ui</Value>
+ </CollectionProperty>
+ </AnalyzerSettings>
+ </Analyzer>
+ <Analyzer AnalyzerId="StyleCop.CSharp.OrderingRules">
+ <Rules>
+ <Rule Name="UsingDirectivesMustBePlacedWithinNamespace">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="ElementsMustBeOrderedByAccess">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="StaticElementsMustAppearBeforeInstanceElements">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="ConstantsMustAppearBeforeFields">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ </Rules>
+ <AnalyzerSettings />
+ </Analyzer>
+ <Analyzer AnalyzerId="StyleCop.CSharp.ReadabilityRules">
+ <Rules>
+ <Rule Name="PrefixLocalCallsWithThis">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="SplitParametersMustStartOnLineAfterDeclaration">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="ParametersMustBeOnSameLineOrSeparateLines">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="UseBuiltInTypeAlias">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="ParameterMustFollowComma">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="ParameterMustNotSpanMultipleLines">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ </Rules>
+ <AnalyzerSettings />
+ </Analyzer>
+ <Analyzer AnalyzerId="StyleCop.CSharp.SpacingRules">
+ <Rules>
+ <Rule Name="SingleLineCommentsMustBeginWithSingleSpace">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ </Rules>
+ <AnalyzerSettings />
+ </Analyzer>
+ </Analyzers>
+</StyleCopSettings> \ No newline at end of file
diff --git a/src/Strict.ruleset b/src/Strict.ruleset
new file mode 100644
index 00000000..1308bade
--- /dev/null
+++ b/src/Strict.ruleset
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RuleSet Name="FxCop rules for ASP.NET Web Stack" Description="This rule set contains the rules for ASP.NET Web Stack." ToolsVersion="10.0">
+ <RuleHintPaths>
+ <Path>..\packages\Microsoft.Web.FxCop</Path>
+ </RuleHintPaths>
+ <IncludeAll Action="Error" />
+ <Rules AnalyzerId="Microsoft.Analyzers.ManagedCodeAnalysis" RuleNamespace="Microsoft.Rules.Managed">
+ <Rule Id="CA1062" Action="None" />
+ </Rules>
+</RuleSet> \ No newline at end of file
diff --git a/src/System.Json/Extensions/JsonValueExtensions.cs b/src/System.Json/Extensions/JsonValueExtensions.cs
new file mode 100644
index 00000000..9882a484
--- /dev/null
+++ b/src/System.Json/Extensions/JsonValueExtensions.cs
@@ -0,0 +1,380 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Dynamic;
+using System.IO;
+using System.Json;
+using System.Linq.Expressions;
+
+namespace System.Runtime.Serialization.Json
+{
+ /// <summary>
+ /// This class extends the functionality of the <see cref="JsonValue"/> type.
+ /// </summary>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static class JsonValueExtensions
+ {
+ /// <summary>
+ /// Creates a <see cref="System.Json.JsonValue"/> object based on an arbitrary CLR object.
+ /// </summary>
+ /// <param name="value">The object to be converted to <see cref="System.Json.JsonValue"/>.</param>
+ /// <returns>The <see cref="System.Json.JsonValue"/> which represents the given object.</returns>
+ /// <remarks>The conversion is done through the <see cref="System.Runtime.Serialization.Json.DataContractJsonSerializer"/>;
+ /// the object is first serialized into JSON using the serializer, then parsed into a <see cref="System.Json.JsonValue"/>
+ /// object.</remarks>
+ public static JsonValue CreateFrom(object value)
+ {
+ JsonValue jsonValue = null;
+
+ if (value != null)
+ {
+ jsonValue = value as JsonValue;
+
+ if (jsonValue == null)
+ {
+ jsonValue = JsonValueExtensions.CreatePrimitive(value);
+
+ if (jsonValue == null)
+ {
+ jsonValue = JsonValueExtensions.CreateFromDynamic(value);
+
+ if (jsonValue == null)
+ {
+ jsonValue = JsonValueExtensions.CreateFromComplex(value);
+ }
+ }
+ }
+ }
+
+ return jsonValue;
+ }
+
+ /// <summary>
+ /// Attempts to convert this <see cref="System.Json.JsonValue"/> instance into the type T.
+ /// </summary>
+ /// <typeparam name="T">The type to which the conversion is being performed.</typeparam>
+ /// <param name="jsonValue">The <see cref="JsonValue"/> instance this method extension is to be applied to.</param>
+ /// <param name="valueOfT">An instance of T initialized with this instance, or the default
+ /// value of T, if the conversion cannot be performed.</param>
+ /// <returns>true if this <see cref="System.Json.JsonValue"/> instance can be read as type T; otherwise, false.</returns>
+ public static bool TryReadAsType<T>(this JsonValue jsonValue, out T valueOfT)
+ {
+ if (jsonValue == null)
+ {
+ throw new ArgumentNullException("jsonValue");
+ }
+
+ object value;
+ if (JsonValueExtensions.TryReadAsType(jsonValue, typeof(T), out value))
+ {
+ valueOfT = (T)value;
+ return true;
+ }
+
+ valueOfT = default(T);
+ return false;
+ }
+
+ /// <summary>
+ /// Attempts to convert this <see cref="System.Json.JsonValue"/> instance into the type T.
+ /// </summary>
+ /// <typeparam name="T">The type to which the conversion is being performed.</typeparam>
+ /// <param name="jsonValue">The <see cref="JsonValue"/> instance this method extension is to be applied to.</param>
+ /// <returns>An instance of T initialized with the <see cref="System.Json.JsonValue"/> value
+ /// specified if the conversion.</returns>
+ /// <exception cref="System.NotSupportedException">If this <see cref="System.Json.JsonValue"/> value cannot be
+ /// converted into the type T.</exception>
+ [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter",
+ Justification = "The generic parameter is used to specify the output type")]
+ public static T ReadAsType<T>(this JsonValue jsonValue)
+ {
+ if (jsonValue == null)
+ {
+ throw new ArgumentNullException("jsonValue");
+ }
+
+ return (T)JsonValueExtensions.ReadAsType(jsonValue, typeof(T));
+ }
+
+ /// <summary>
+ /// Attempts to convert this <see cref="System.Json.JsonValue"/> instance into the type T, returning a fallback value
+ /// if the conversion fails.
+ /// </summary>
+ /// <typeparam name="T">The type to which the conversion is being performed.</typeparam>
+ /// <param name="jsonValue">The <see cref="JsonValue"/> instance this method extension is to be applied to.</param>
+ /// <param name="fallback">A fallback value to be retuned in case the conversion cannot be performed.</param>
+ /// <returns>An instance of T initialized with the <see cref="System.Json.JsonValue"/> value
+ /// specified if the conversion succeeds or the specified fallback value if it fails.</returns>
+ public static T ReadAsType<T>(this JsonValue jsonValue, T fallback)
+ {
+ if (jsonValue == null)
+ {
+ throw new ArgumentNullException("jsonValue");
+ }
+
+ T outVal;
+ if (JsonValueExtensions.TryReadAsType<T>(jsonValue, out outVal))
+ {
+ return outVal;
+ }
+
+ return fallback;
+ }
+
+ /// <summary>
+ /// Attempts to convert this <see cref="System.Json.JsonValue"/> instance into an instance of the specified type.
+ /// </summary>
+ /// <param name="jsonValue">The <see cref="JsonValue"/> instance this method extension is to be applied to.</param>
+ /// <param name="type">The type to which the conversion is being performed.</param>
+ /// <returns>An object instance initialized with the <see cref="System.Json.JsonValue"/> value
+ /// specified if the conversion.</returns>
+ /// <exception cref="System.NotSupportedException">If this <see cref="System.Json.JsonValue"/> value cannot be
+ /// converted into the type T.</exception>
+ public static object ReadAsType(this JsonValue jsonValue, Type type)
+ {
+ if (jsonValue == null)
+ {
+ throw new ArgumentNullException("jsonValue");
+ }
+
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+
+ object result;
+ if (JsonValueExtensions.TryReadAsType(jsonValue, type, out result))
+ {
+ return result;
+ }
+
+ throw new NotSupportedException(RS.Format(System.Json.Properties.Resources.CannotReadAsType, jsonValue.GetType().FullName, type.FullName));
+ }
+
+ /// <summary>
+ /// Attempts to convert this <see cref="System.Json.JsonValue"/> instance into an instance of the specified type.
+ /// </summary>
+ /// <param name="jsonValue">The <see cref="JsonValue"/> instance this method extension is to be applied to.</param>
+ /// <param name="type">The type to which the conversion is being performed.</param>
+ /// <param name="value">An object to be initialized with this instance or null if the conversion cannot be performed.</param>
+ /// <returns>true if this <see cref="System.Json.JsonValue"/> instance can be read as the specified type; otherwise, false.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1007:UseGenericsWhereAppropriate",
+ Justification = "This is the non-generic version of the method.")]
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception translates to fail.")]
+ public static bool TryReadAsType(this JsonValue jsonValue, Type type, out object value)
+ {
+ if (jsonValue == null)
+ {
+ throw new ArgumentNullException("jsonValue");
+ }
+
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+
+ if (type == typeof(JsonValue) || type == typeof(object))
+ {
+ value = jsonValue;
+ return true;
+ }
+
+ if (type == typeof(object[]) || type == typeof(Dictionary<string, object>))
+ {
+ if (!JsonValueExtensions.CanConvertToClrCollection(jsonValue, type))
+ {
+ value = null;
+ return false;
+ }
+ }
+
+ if (jsonValue.TryReadAs(type, out value))
+ {
+ return true;
+ }
+
+ try
+ {
+ using (MemoryStream ms = new MemoryStream())
+ {
+ jsonValue.Save(ms);
+ ms.Position = 0;
+ DataContractJsonSerializer dcjs = new DataContractJsonSerializer(type);
+ value = dcjs.ReadObject(ms);
+ }
+
+ return true;
+ }
+ catch (Exception)
+ {
+ value = null;
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="JsonValue"/> instance can be converted to the specified collection <see cref="Type"/>.
+ /// </summary>
+ /// <param name="jsonValue">The instance to be converted.</param>
+ /// <param name="collectionType">The collection type to convert the instance to.</param>
+ /// <returns>true if the instance can be converted, false otherwise</returns>
+ private static bool CanConvertToClrCollection(JsonValue jsonValue, Type collectionType)
+ {
+ if (jsonValue != null)
+ {
+ return (jsonValue.JsonType == JsonType.Object && collectionType == typeof(Dictionary<string, object>)) ||
+ (jsonValue.JsonType == JsonType.Array && collectionType == typeof(object[]));
+ }
+
+ return false;
+ }
+
+ private static JsonValue CreatePrimitive(object value)
+ {
+ JsonPrimitive jsonPrimitive;
+
+ if (JsonPrimitive.TryCreate(value, out jsonPrimitive))
+ {
+ return jsonPrimitive;
+ }
+
+ return null;
+ }
+
+ private static JsonValue CreateFromComplex(object value)
+ {
+ DataContractJsonSerializer dcjs = new DataContractJsonSerializer(value.GetType());
+ using (MemoryStream ms = new MemoryStream())
+ {
+ dcjs.WriteObject(ms, value);
+ ms.Position = 0;
+ return JsonValue.Load(ms);
+ }
+ }
+
+ [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "value is not the same")]
+ private static JsonValue CreateFromDynamic(object value)
+ {
+ JsonObject parent = null;
+ DynamicObject dynObj = value as DynamicObject;
+
+ if (dynObj != null)
+ {
+ parent = new JsonObject();
+ Stack<CreateFromTypeStackInfo> infoStack = new Stack<CreateFromTypeStackInfo>();
+ IEnumerator<string> keys = null;
+
+ do
+ {
+ if (keys == null)
+ {
+ keys = dynObj.GetDynamicMemberNames().GetEnumerator();
+ }
+
+ while (keys.MoveNext())
+ {
+ JsonValue child = null;
+ string key = keys.Current;
+ SimpleGetMemberBinder binder = new SimpleGetMemberBinder(key);
+
+ if (dynObj.TryGetMember(binder, out value))
+ {
+ DynamicObject childDynObj = value as DynamicObject;
+
+ if (childDynObj != null)
+ {
+ child = new JsonObject();
+ parent.Add(key, child);
+
+ infoStack.Push(new CreateFromTypeStackInfo(parent, dynObj, keys));
+
+ parent = child as JsonObject;
+ dynObj = childDynObj;
+ keys = null;
+
+ break;
+ }
+ else
+ {
+ if (value != null)
+ {
+ child = value as JsonValue;
+
+ if (child == null)
+ {
+ child = JsonValueExtensions.CreatePrimitive(value);
+
+ if (child == null)
+ {
+ child = JsonValueExtensions.CreateFromComplex(value);
+ }
+ }
+ }
+
+ parent.Add(key, child);
+ }
+ }
+ }
+
+ if (infoStack.Count > 0 && keys != null)
+ {
+ CreateFromTypeStackInfo info = infoStack.Pop();
+
+ parent = info.JsonObject;
+ dynObj = info.DynamicObject;
+ keys = info.Keys;
+ }
+ }
+ while (infoStack.Count > 0);
+ }
+
+ return parent;
+ }
+
+ private class CreateFromTypeStackInfo
+ {
+ public CreateFromTypeStackInfo(JsonObject jsonObject, DynamicObject dynamicObject, IEnumerator<string> keyEnumerator)
+ {
+ JsonObject = jsonObject;
+ DynamicObject = dynamicObject;
+ Keys = keyEnumerator;
+ }
+
+ /// <summary>
+ /// Gets of sets
+ /// </summary>
+ public JsonObject JsonObject { get; set; }
+
+ /// <summary>
+ /// Gets of sets
+ /// </summary>
+ public DynamicObject DynamicObject { get; set; }
+
+ /// <summary>
+ /// Gets of sets
+ /// </summary>
+ public IEnumerator<string> Keys { get; set; }
+ }
+
+ private class SimpleGetMemberBinder : GetMemberBinder
+ {
+ public SimpleGetMemberBinder(string name)
+ : base(name, false)
+ {
+ }
+
+ public override DynamicMetaObject FallbackGetMember(DynamicMetaObject target, DynamicMetaObject errorSuggestion)
+ {
+ if (target != null && errorSuggestion == null)
+ {
+ string exceptionMessage = RS.Format(System.Json.Properties.Resources.DynamicPropertyNotDefined, target.LimitType, Name);
+ Expression throwExpression = Expression.Throw(Expression.Constant(new InvalidOperationException(exceptionMessage)), typeof(object));
+
+ errorSuggestion = new DynamicMetaObject(throwExpression, target.Restrictions);
+ }
+
+ return errorSuggestion;
+ }
+ }
+ }
+}
diff --git a/src/System.Json/FormUrlEncodedJson.cs b/src/System.Json/FormUrlEncodedJson.cs
new file mode 100644
index 00000000..7ff5519e
--- /dev/null
+++ b/src/System.Json/FormUrlEncodedJson.cs
@@ -0,0 +1,589 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+
+namespace System.Json
+{
+ /// <summary>
+ /// This class provides a low-level API for parsing HTML form URL-encoded data, also known as <c>application/x-www-form-urlencoded</c>
+ /// data. The output of the parser is a <see cref="JsonObject"/> instance.
+ /// <remarks>This is a low-level API intended for use by other APIs. It has been optimized for performance and
+ /// is not intended to be called directly from user code.</remarks>
+ /// </summary>
+ public static class FormUrlEncodedJson
+ {
+ private const string ApplicationFormUrlEncoded = @"application/x-www-form-urlencoded";
+ private const int MinDepth = 0;
+
+ private static readonly string[] _emptyPath = new string[]
+ {
+ String.Empty
+ };
+
+ /// <summary>
+ /// Parses a collection of query string values as a <see cref="System.Json.JsonObject"/>.
+ /// </summary>
+ /// <remarks>This is a low-level API intended for use by other APIs. It has been optimized for performance and
+ /// is not intended to be called directly from user code.</remarks>
+ /// <param name="nameValuePairs">The collection of query string name-value pairs parsed in lexical order. Both names
+ /// and values must be un-escaped so that they don't contain any <see cref="Uri"/> encoding.</param>
+ /// <returns>The <see cref="System.Json.JsonObject"/> corresponding to the given query string values.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is a low-level API used by other APIs to provide end-user functionality.")]
+ public static JsonObject Parse(IEnumerable<KeyValuePair<string, string>> nameValuePairs)
+ {
+ return ParseInternal(nameValuePairs, Int32.MaxValue, true);
+ }
+
+ /// <summary>
+ /// Parses a collection of query string values as a <see cref="System.Json.JsonObject"/>.
+ /// </summary>
+ /// <remarks>This is a low-level API intended for use by other APIs. It has been optimized for performance and
+ /// is not intended to be called directly from user code.</remarks>
+ /// <param name="nameValuePairs">The collection of query string name-value pairs parsed in lexical order. Both names
+ /// and values must be un-escaped so that they don't contain any <see cref="Uri"/> encoding.</param>
+ /// <param name="maxDepth">The maximum depth of object graph encoded as <c>x-www-form-urlencoded</c>.</param>
+ /// <returns>The <see cref="System.Json.JsonObject"/> corresponding to the given query string values.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is a low-level API used by other APIs to provide end-user functionality.")]
+ public static JsonObject Parse(IEnumerable<KeyValuePair<string, string>> nameValuePairs, int maxDepth)
+ {
+ return ParseInternal(nameValuePairs, maxDepth, true);
+ }
+
+ /// <summary>
+ /// Parses a collection of query string values as a <see cref="System.Json.JsonObject"/>.
+ /// </summary>
+ /// <remarks>This is a low-level API intended for use by other APIs. It has been optimized for performance and
+ /// is not intended to be called directly from user code.</remarks>
+ /// <param name="nameValuePairs">The collection of query string name-value pairs parsed in lexical order. Both names
+ /// and values must be un-escaped so that they don't contain any <see cref="Uri"/> encoding.</param>
+ /// <param name="value">The parsed result or null if parsing failed.</param>
+ /// <returns><c>true</c> if <paramref name="nameValuePairs"/> was parsed successfully; otherwise false.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is a low-level API used by other APIs to provide end-user functionality.")]
+ public static bool TryParse(IEnumerable<KeyValuePair<string, string>> nameValuePairs, out JsonObject value)
+ {
+ return (value = ParseInternal(nameValuePairs, Int32.MaxValue, false)) != null;
+ }
+
+ /// <summary>
+ /// Parses a collection of query string values as a <see cref="System.Json.JsonObject"/>.
+ /// </summary>
+ /// <remarks>This is a low-level API intended for use by other APIs. It has been optimized for performance and
+ /// is not intended to be called directly from user code.</remarks>
+ /// <param name="nameValuePairs">The collection of query string name-value pairs parsed in lexical order. Both names
+ /// and values must be un-escaped so that they don't contain any <see cref="Uri"/> encoding.</param>
+ /// <param name="maxDepth">The maximum depth of object graph encoded as <c>x-www-form-urlencoded</c>.</param>
+ /// <param name="value">The parsed result or null if parsing failed.</param>
+ /// <returns><c>true</c> if <paramref name="nameValuePairs"/> was parsed successfully; otherwise false.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is a low-level API used by other APIs to provide end-user functionality.")]
+ public static bool TryParse(IEnumerable<KeyValuePair<string, string>> nameValuePairs, int maxDepth, out JsonObject value)
+ {
+ return (value = ParseInternal(nameValuePairs, maxDepth, false)) != null;
+ }
+
+ /// <summary>
+ /// Parses a collection of query string values as a <see cref="System.Json.JsonObject"/>.
+ /// </summary>
+ /// <remarks>This is a low-level API intended for use by other APIs. It has been optimized for performance and
+ /// is not intended to be called directly from user code.</remarks>
+ /// <param name="nameValuePairs">The collection of query string name-value pairs parsed in lexical order. Both names
+ /// and values must be un-escaped so that they don't contain any <see cref="Uri"/> encoding.</param>
+ /// <param name="maxDepth">The maximum depth of object graph encoded as <c>x-www-form-urlencoded</c>.</param>
+ /// <param name="throwOnError">Indicates whether to throw an exception on error or return false</param>
+ /// <returns>The <see cref="System.Json.JsonObject"/> corresponding to the given query string values.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is a low-level API used by other APIs to provide end-user functionality.")]
+ private static JsonObject ParseInternal(IEnumerable<KeyValuePair<string, string>> nameValuePairs, int maxDepth, bool throwOnError)
+ {
+ if (nameValuePairs == null)
+ {
+ throw new ArgumentNullException("nameValuePairs");
+ }
+
+ if (maxDepth <= MinDepth)
+ {
+ throw new ArgumentException(RS.Format(Properties.Resources.MinParameterSize, MinDepth), "maxDepth");
+ }
+
+ JsonObject result = new JsonObject();
+ foreach (var nameValuePair in nameValuePairs)
+ {
+ if (nameValuePair.Key == null)
+ {
+ if (String.IsNullOrEmpty(nameValuePair.Value))
+ {
+ if (throwOnError)
+ {
+ throw new ArgumentException(Properties.Resources.QueryStringNameShouldNotNull, "nameValuePairs");
+ }
+
+ return null;
+ }
+
+ string[] path = new string[] { nameValuePair.Value };
+ if (!Insert(result, path, null, throwOnError))
+ {
+ return null;
+ }
+ }
+ else
+ {
+ string[] path = GetPath(nameValuePair.Key, maxDepth, throwOnError);
+ if (path == null || !Insert(result, path, nameValuePair.Value, throwOnError))
+ {
+ return null;
+ }
+ }
+ }
+
+ FixContiguousArrays(result);
+ return result;
+ }
+
+ private static string[] GetPath(string key, int maxDepth, bool throwOnError)
+ {
+ Contract.Assert(key != null, "Key cannot be null (this function is only called by Parse if key != null)");
+
+ if (String.IsNullOrWhiteSpace(key))
+ {
+ return _emptyPath;
+ }
+
+ if (!ValidateQueryString(key, throwOnError))
+ {
+ return null;
+ }
+
+ string[] path = key.Split('[');
+ for (int i = 0; i < path.Length; i++)
+ {
+ if (path[i].EndsWith("]", StringComparison.Ordinal))
+ {
+ path[i] = path[i].Substring(0, path[i].Length - 1);
+ }
+ }
+
+ // For consistency with JSON, the depth of a[b]=1 is 3 (which is the depth of {a:{b:1}}, given
+ // that in the JSON-XML mapping there's a <root> element wrapping the JSON object:
+ // <root><a><b>1</b></a></root>. So if the length of the path is greater than *or equal* to
+ // maxDepth, then we throw.
+ if (path.Length >= maxDepth)
+ {
+ if (throwOnError)
+ {
+ throw new ArgumentException(RS.Format(Properties.Resources.MaxDepthExceeded, maxDepth));
+ }
+
+ return null;
+ }
+
+ return path;
+ }
+
+ private static bool ValidateQueryString(string key, bool throwOnError)
+ {
+ bool hasUnMatchedLeftBraket = false;
+ for (int i = 0; i < key.Length; i++)
+ {
+ switch (key[i])
+ {
+ case '[':
+ if (!hasUnMatchedLeftBraket)
+ {
+ hasUnMatchedLeftBraket = true;
+ }
+ else
+ {
+ if (throwOnError)
+ {
+ throw new ArgumentException(RS.Format(Properties.Resources.NestedBracketNotValid, ApplicationFormUrlEncoded, i));
+ }
+
+ return false;
+ }
+
+ break;
+ case ']':
+ if (hasUnMatchedLeftBraket)
+ {
+ hasUnMatchedLeftBraket = false;
+ }
+ else
+ {
+ if (throwOnError)
+ {
+ throw new ArgumentException(RS.Format(Properties.Resources.UnMatchedBracketNotValid, ApplicationFormUrlEncoded, i));
+ }
+
+ return false;
+ }
+
+ break;
+ }
+ }
+
+ if (hasUnMatchedLeftBraket)
+ {
+ if (throwOnError)
+ {
+ throw new ArgumentException(RS.Format(Properties.Resources.NestedBracketNotValid, ApplicationFormUrlEncoded, key.LastIndexOf('[')));
+ }
+
+ return false;
+ }
+
+ return true;
+ }
+
+ private static bool Insert(JsonObject root, string[] path, string value, bool throwOnError)
+ {
+ // to-do: verify consistent with new parsing, whether single value is in path or value
+ Contract.Assert(root != null, "Root object can't be null");
+
+ JsonObject current = root;
+ JsonObject parent = null;
+
+ for (int i = 0; i < path.Length - 1; i++)
+ {
+ if (String.IsNullOrEmpty(path[i]))
+ {
+ if (throwOnError)
+ {
+ throw new ArgumentException(RS.Format(Properties.Resources.InvalidArrayInsert, BuildPathString(path, i)));
+ }
+
+ return false;
+ }
+
+ if (!current.ContainsKey(path[i]))
+ {
+ current[path[i]] = new JsonObject();
+ }
+ else
+ {
+ // Since the loop goes up to the next-to-last item in the path, if we hit a null
+ // or a primitive, then we have a mismatching node.
+ if (current[path[i]] == null || current[path[i]] is JsonPrimitive)
+ {
+ if (throwOnError)
+ {
+ throw new ArgumentException(RS.Format(Properties.Resources.FormUrlEncodedMismatchingTypes, BuildPathString(path, i)));
+ }
+
+ return false;
+ }
+ }
+
+ parent = current;
+ current = current[path[i]] as JsonObject;
+ }
+
+ string lastKey = path[path.Length - 1];
+ if (String.IsNullOrEmpty(lastKey) && path.Length > 1)
+ {
+ if (!AddToArray(parent, path, value, throwOnError))
+ {
+ return false;
+ }
+ }
+ else
+ {
+ if (current == null)
+ {
+ if (throwOnError)
+ {
+ throw new ArgumentException(RS.Format(Properties.Resources.FormUrlEncodedMismatchingTypes, BuildPathString(path, path.Length - 1)));
+ }
+
+ return false;
+ }
+
+ if (!AddToObject(current, path, value, throwOnError))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static bool AddToObject(JsonObject obj, string[] path, string value, bool throwOnError)
+ {
+ Contract.Assert(obj != null, "JsonObject cannot be null");
+
+ int pathIndex = path.Length - 1;
+ string key = path[pathIndex];
+
+ if (obj.ContainsKey(key))
+ {
+ if (obj[key] == null)
+ {
+ if (throwOnError)
+ {
+ throw new ArgumentException(RS.Format(Properties.Resources.FormUrlEncodedMismatchingTypes, BuildPathString(path, pathIndex)));
+ }
+
+ return false;
+ }
+
+ bool isRoot = path.Length == 1;
+ if (isRoot)
+ {
+ // jQuery 1.3 behavior, make it into an array(object) if primitive
+ if (obj[key].JsonType == JsonType.String)
+ {
+ string oldValue = obj[key].ReadAs<string>();
+ JsonObject jo = new JsonObject();
+ jo.Add("0", oldValue);
+ jo.Add("1", value);
+ obj[key] = jo;
+ }
+ else if (obj[key] is JsonObject)
+ {
+ // if it was already an object, simply add the value
+ JsonObject jo = obj[key] as JsonObject;
+ string index = GetIndex(jo.Keys, throwOnError);
+ if (index == null)
+ {
+ return false;
+ }
+
+ jo.Add(index, value);
+ }
+ }
+ else
+ {
+ if (throwOnError)
+ {
+ throw new ArgumentException(RS.Format(Properties.Resources.JQuery13CompatModeNotSupportNestedJson, BuildPathString(path, pathIndex)));
+ }
+
+ return false;
+ }
+ }
+ else
+ {
+ // if the object didn't contain the key, simply add it now
+ obj[key] = value;
+ }
+
+ return true;
+ }
+
+ // JsonObject passed in is semantically an array
+ private static bool AddToArray(JsonObject parent, string[] path, string value, bool throwOnError)
+ {
+ Contract.Assert(parent != null, "Parent cannot be null");
+ Contract.Assert(path.Length >= 2, "The path must be at least 2, one for the ending [], and one for before the '[' (which can be empty)");
+
+ string parentPath = path[path.Length - 2];
+
+ Contract.Assert(parent.ContainsKey(parentPath), "It was added on insert to get to this point");
+ JsonObject jo = parent[parentPath] as JsonObject;
+
+ if (jo == null)
+ {
+ // a[b][c]=1&a[b][]=2 => invalid
+ if (throwOnError)
+ {
+ throw new ArgumentException(RS.Format(Properties.Resources.FormUrlEncodedMismatchingTypes, BuildPathString(path, path.Length - 1)));
+ }
+
+ return false;
+ }
+ else
+ {
+ string index = GetIndex(jo.Keys, throwOnError);
+ if (index == null)
+ {
+ return false;
+ }
+
+ jo.Add(index, value);
+ }
+
+ return true;
+ }
+
+ // TODO: consider optimize it by only look at the last one
+ private static string GetIndex(IEnumerable<string> keys, bool throwOnError)
+ {
+ int max = -1;
+ foreach (var key in keys)
+ {
+ int tempInt;
+ if (Int32.TryParse(key, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out tempInt) && tempInt > max)
+ {
+ max = tempInt;
+ }
+ else
+ {
+ if (throwOnError)
+ {
+ throw new ArgumentException(RS.Format(Properties.Resources.FormUrlEncodedMismatchingTypes, key));
+ }
+
+ return null;
+ }
+ }
+
+ max++;
+ return max.ToString(CultureInfo.InvariantCulture);
+ }
+
+ private static void FixContiguousArrays(JsonValue jv)
+ {
+ JsonArray ja = jv as JsonArray;
+
+ if (ja != null)
+ {
+ for (int i = 0; i < ja.Count; i++)
+ {
+ if (ja[i] != null)
+ {
+ ja[i] = FixSingleContiguousArray(ja[i]);
+ FixContiguousArrays(ja[i]);
+ }
+ }
+ }
+ else
+ {
+ JsonObject jo = jv as JsonObject;
+
+ if (jo != null)
+ {
+ List<string> keys = new List<string>(jo.Keys);
+ foreach (string key in keys)
+ {
+ if (jo[key] != null)
+ {
+ jo[key] = FixSingleContiguousArray(jo[key]);
+ FixContiguousArrays(jo[key]);
+ }
+ }
+ }
+ }
+
+ //// do nothing for primitives
+ }
+
+ private static JsonValue FixSingleContiguousArray(JsonValue original)
+ {
+ JsonObject jo = original as JsonObject;
+ if (jo != null && jo.Count > 0)
+ {
+ List<string> childKeys = new List<string>(jo.Keys);
+ List<string> sortedKeys;
+ if (CanBecomeArray(childKeys, out sortedKeys))
+ {
+ JsonArray newResult = new JsonArray();
+ foreach (string sortedKey in sortedKeys)
+ {
+ newResult.Add(jo[sortedKey]);
+ }
+
+ return newResult;
+ }
+ }
+
+ return original;
+ }
+
+ private static bool CanBecomeArray(List<string> keys, out List<string> sortedKeys)
+ {
+ List<ArrayCandidate> intKeys = new List<ArrayCandidate>();
+ sortedKeys = null;
+ bool areContiguousIndices = true;
+ foreach (string key in keys)
+ {
+ int intKey;
+ if (!Int32.TryParse(key, NumberStyles.None, CultureInfo.InvariantCulture, out intKey))
+ {
+ // if not a non-negative number, it cannot become an array
+ areContiguousIndices = false;
+ break;
+ }
+
+ string strKey = intKey.ToString(CultureInfo.InvariantCulture);
+ if (!strKey.Equals(key, StringComparison.Ordinal))
+ {
+ // int.Parse returned true, but it's not really the same number.
+ // It's the case for strings such as "1\0".
+ areContiguousIndices = false;
+ break;
+ }
+
+ intKeys.Add(new ArrayCandidate(intKey, strKey));
+ }
+
+ if (areContiguousIndices)
+ {
+ intKeys.Sort((x, y) => x.Key - y.Key);
+
+ for (int i = 0; i < intKeys.Count; i++)
+ {
+ if (intKeys[i].Key != i)
+ {
+ areContiguousIndices = false;
+ break;
+ }
+ }
+ }
+
+ if (areContiguousIndices)
+ {
+ sortedKeys = new List<string>(intKeys.Select(x => x.Value));
+ }
+
+ return areContiguousIndices;
+ }
+
+ private static string BuildPathString(string[] path, int i)
+ {
+ StringBuilder errorPath = new StringBuilder(path[0]);
+ for (int p = 1; p <= i; p++)
+ {
+ errorPath.AppendFormat("[{0}]", path[p]);
+ }
+
+ return errorPath.ToString();
+ }
+
+ /// <summary>
+ /// Class that wraps key-value pairs.
+ /// </summary>
+ /// <remarks>
+ /// This use of this class avoids a FxCop warning CA908 which happens if using various generic types.
+ /// </remarks>
+ private class ArrayCandidate
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ArrayCandidate"/> class.
+ /// </summary>
+ /// <param name="key">The key of this <see cref="ArrayCandidate"/> instance.</param>
+ /// <param name="value">The value of this <see cref="ArrayCandidate"/> instance.</param>
+ public ArrayCandidate(int key, string value)
+ {
+ Key = key;
+ Value = value;
+ }
+
+ /// <summary>
+ /// Gets or sets the key of this <see cref="ArrayCandidate"/> instance.
+ /// </summary>
+ /// <value>
+ /// The key of this <see cref="ArrayCandidate"/> instance.
+ /// </value>
+ public int Key { get; set; }
+
+ /// <summary>
+ /// Gets or sets the value of this <see cref="ArrayCandidate"/> instance.
+ /// </summary>
+ /// <value>
+ /// The value of this <see cref="ArrayCandidate"/> instance.
+ /// </value>
+ public string Value { get; set; }
+ }
+ }
+}
diff --git a/src/System.Json/GlobalSuppressions.cs b/src/System.Json/GlobalSuppressions.cs
new file mode 100644
index 00000000..d0ff21a3
--- /dev/null
+++ b/src/System.Json/GlobalSuppressions.cs
@@ -0,0 +1,4 @@
+using System.Diagnostics.CodeAnalysis;
+
+[assembly: SuppressMessage("Microsoft.Design", "CA2210:AssembliesShouldHaveValidStrongNames", Justification = "These assemblies are delay-signed.")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Justification = "Classes are grouped logically for user clarity.", Scope = "namespace", Target = "System.Runtime.Serialization.Json")]
diff --git a/src/System.Json/JXmlToJsonValueConverter.cs b/src/System.Json/JXmlToJsonValueConverter.cs
new file mode 100644
index 00000000..cd3dddbb
--- /dev/null
+++ b/src/System.Json/JXmlToJsonValueConverter.cs
@@ -0,0 +1,302 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Runtime.Serialization.Json;
+using System.Text;
+using System.Xml;
+
+namespace System.Json
+{
+ internal static class JXmlToJsonValueConverter
+ {
+ internal const string RootElementName = "root";
+ internal const string ItemElementName = "item";
+ internal const string TypeAttributeName = "type";
+ internal const string ArrayAttributeValue = "array";
+ internal const string BooleanAttributeValue = "boolean";
+ internal const string NullAttributeValue = "null";
+ internal const string NumberAttributeValue = "number";
+ internal const string ObjectAttributeValue = "object";
+ internal const string StringAttributeValue = "string";
+ private const string TypeHintAttributeName = "__type";
+
+ private static readonly char[] _floatingPointChars = new char[] { '.', 'e', 'E' };
+
+ public static JsonValue JXMLToJsonValue(Stream jsonStream)
+ {
+ if (jsonStream == null)
+ {
+ throw new ArgumentNullException("jsonStream");
+ }
+
+ return JXMLToJsonValue(jsonStream, null);
+ }
+
+ public static JsonValue JXMLToJsonValue(string jsonString)
+ {
+ if (jsonString == null)
+ {
+ throw new ArgumentNullException("jsonString");
+ }
+
+ if (jsonString.Length == 0)
+ {
+ throw new ArgumentException(Properties.Resources.JsonStringCannotBeEmpty, "jsonString");
+ }
+
+ byte[] jsonBytes = Encoding.UTF8.GetBytes(jsonString);
+
+ return JXMLToJsonValue(null, jsonBytes);
+ }
+
+ public static JsonValue JXMLToJsonValue(XmlDictionaryReader jsonReader)
+ {
+ if (jsonReader == null)
+ {
+ throw new ArgumentNullException("jsonReader");
+ }
+
+ const string RootObjectName = "RootObject";
+ Stack<JsonValue> jsonStack = new Stack<JsonValue>();
+ string nodeType = null;
+ bool isEmptyElement = false;
+
+ JsonValue parent = new JsonObject();
+ jsonStack.Push(parent);
+ string currentName = RootObjectName;
+
+ try
+ {
+ MoveToRootNode(jsonReader);
+
+ while (jsonStack.Count > 0 && jsonReader.NodeType != XmlNodeType.None)
+ {
+ if (parent is JsonObject && currentName == null)
+ {
+ currentName = GetMemberName(jsonReader);
+ }
+
+ nodeType = jsonReader.GetAttribute(TypeAttributeName) ?? StringAttributeValue;
+
+ if (parent is JsonArray)
+ {
+ // For arrays, the element name has to be "item"
+ if (jsonReader.Name != ItemElementName)
+ {
+ throw new FormatException(Properties.Resources.IncorrectJsonFormat);
+ }
+ }
+
+ switch (nodeType)
+ {
+ case NullAttributeValue:
+ case BooleanAttributeValue:
+ case StringAttributeValue:
+ case NumberAttributeValue:
+ JsonPrimitive jsonPrimitive = ReadPrimitive(nodeType, jsonReader);
+ InsertJsonValue(jsonStack, ref parent, ref currentName, jsonPrimitive, true);
+ break;
+ case ArrayAttributeValue:
+ JsonArray jsonArray = CreateJsonArray(jsonReader, ref isEmptyElement);
+ InsertJsonValue(jsonStack, ref parent, ref currentName, jsonArray, isEmptyElement);
+ break;
+ case ObjectAttributeValue:
+ JsonObject jsonObject = CreateObjectWithTypeHint(jsonReader, ref isEmptyElement);
+ InsertJsonValue(jsonStack, ref parent, ref currentName, jsonObject, isEmptyElement);
+ break;
+ default:
+ throw new FormatException(Properties.Resources.IncorrectJsonFormat);
+ }
+
+ while (jsonReader.NodeType == XmlNodeType.EndElement && jsonStack.Count > 0)
+ {
+ jsonReader.Read();
+ SkipWhitespace(jsonReader);
+ jsonStack.Pop();
+ if (jsonStack.Count > 0)
+ {
+ parent = jsonStack.Peek();
+ }
+ }
+ }
+ }
+ catch (XmlException xmlException)
+ {
+ throw new FormatException(Properties.Resources.IncorrectJsonFormat, xmlException);
+ }
+
+ if (jsonStack.Count != 1)
+ {
+ throw new FormatException(Properties.Resources.IncorrectJsonFormat);
+ }
+
+ return parent[RootObjectName];
+ }
+
+ private static JsonValue JXMLToJsonValue(Stream jsonStream, byte[] jsonBytes)
+ {
+ try
+ {
+ using (XmlDictionaryReader jsonReader =
+ jsonStream != null
+ ? JsonReaderWriterFactory.CreateJsonReader(jsonStream, XmlDictionaryReaderQuotas.Max)
+ : JsonReaderWriterFactory.CreateJsonReader(jsonBytes, XmlDictionaryReaderQuotas.Max))
+ {
+ return JXMLToJsonValue(jsonReader);
+ }
+ }
+ catch (XmlException)
+ {
+ throw new FormatException(Properties.Resources.IncorrectJsonFormat);
+ }
+ }
+
+ private static void InsertJsonValue(Stack<JsonValue> jsonStack, ref JsonValue parent, ref string currentName, JsonValue jsonValue, bool isEmptyElement)
+ {
+ if (parent is JsonArray)
+ {
+ ((JsonArray)parent).Add(jsonValue);
+ }
+ else
+ {
+ if (currentName != null)
+ {
+ ((JsonObject)parent)[currentName] = jsonValue;
+ currentName = null;
+ }
+ }
+
+ if (!isEmptyElement)
+ {
+ jsonStack.Push(jsonValue);
+ parent = jsonValue;
+ }
+ }
+
+ private static string GetMemberName(XmlDictionaryReader jsonReader)
+ {
+ string name;
+ if (jsonReader.NamespaceURI == ItemElementName && jsonReader.LocalName == ItemElementName)
+ {
+ // JXML special case for names which aren't valid XML names
+ name = jsonReader.GetAttribute(ItemElementName);
+
+ if (name == null)
+ {
+ throw new FormatException(Properties.Resources.IncorrectJsonFormat);
+ }
+ }
+ else
+ {
+ name = jsonReader.Name;
+ }
+
+ return name;
+ }
+
+ private static JsonObject CreateObjectWithTypeHint(XmlDictionaryReader jsonReader, ref bool isEmptyElement)
+ {
+ JsonObject jsonObject = new JsonObject();
+ string typeHintAttribute = jsonReader.GetAttribute(TypeHintAttributeName);
+ isEmptyElement = jsonReader.IsEmptyElement;
+ jsonReader.ReadStartElement();
+ SkipWhitespace(jsonReader);
+
+ if (typeHintAttribute != null)
+ {
+ jsonObject.Add(TypeHintAttributeName, typeHintAttribute);
+ }
+
+ return jsonObject;
+ }
+
+ private static JsonArray CreateJsonArray(XmlDictionaryReader jsonReader, ref bool isEmptyElement)
+ {
+ JsonArray jsonArray = new JsonArray();
+ isEmptyElement = jsonReader.IsEmptyElement;
+ jsonReader.ReadStartElement();
+ SkipWhitespace(jsonReader);
+ return jsonArray;
+ }
+
+ private static void MoveToRootNode(XmlDictionaryReader jsonReader)
+ {
+ while (!jsonReader.EOF && (jsonReader.NodeType == XmlNodeType.None || jsonReader.NodeType == XmlNodeType.XmlDeclaration))
+ {
+ // read into <root> node
+ jsonReader.Read();
+ SkipWhitespace(jsonReader);
+ }
+
+ if (jsonReader.NodeType != XmlNodeType.Element || !String.IsNullOrEmpty(jsonReader.NamespaceURI) || jsonReader.Name != RootElementName)
+ {
+ throw new FormatException(Properties.Resources.IncorrectJsonFormat);
+ }
+ }
+
+ private static JsonPrimitive ReadPrimitive(string type, XmlDictionaryReader jsonReader)
+ {
+ JsonValue result = null;
+ switch (type)
+ {
+ case NullAttributeValue:
+ jsonReader.Skip();
+ result = null;
+ break;
+ case BooleanAttributeValue:
+ result = jsonReader.ReadElementContentAsBoolean();
+ break;
+ case StringAttributeValue:
+ result = jsonReader.ReadElementContentAsString();
+ break;
+ case NumberAttributeValue:
+ string temp = jsonReader.ReadElementContentAsString();
+ result = ConvertStringToJsonNumber(temp);
+ break;
+ }
+
+ SkipWhitespace(jsonReader);
+ return (JsonPrimitive)result;
+ }
+
+ private static void SkipWhitespace(XmlDictionaryReader reader)
+ {
+ while (!reader.EOF && (reader.NodeType == XmlNodeType.Whitespace || reader.NodeType == XmlNodeType.SignificantWhitespace))
+ {
+ reader.Read();
+ }
+ }
+
+ private static JsonValue ConvertStringToJsonNumber(string value)
+ {
+ if (value.IndexOfAny(_floatingPointChars) < 0)
+ {
+ int intVal;
+ if (Int32.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out intVal))
+ {
+ return intVal;
+ }
+
+ long longVal;
+ if (Int64.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out longVal))
+ {
+ return longVal;
+ }
+ }
+
+ decimal decValue;
+ if (Decimal.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out decValue) && decValue != 0)
+ {
+ return decValue;
+ }
+
+ double dblValue;
+ if (Double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out dblValue))
+ {
+ return dblValue;
+ }
+
+ throw new ArgumentException(RS.Format(Properties.Resources.InvalidJsonPrimitive, value.ToString()), "value");
+ }
+ }
+}
diff --git a/src/System.Json/JsonArray.cs b/src/System.Json/JsonArray.cs
new file mode 100644
index 00000000..a1857c02
--- /dev/null
+++ b/src/System.Json/JsonArray.cs
@@ -0,0 +1,387 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Runtime.Serialization;
+using System.Xml;
+
+namespace System.Json
+{
+ /// <summary>
+ /// A JsonArray is an ordered sequence of zero or more <see cref="System.Json.JsonValue"/> objects.
+ /// </summary>
+ [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix",
+ Justification = "Array already conveys the meaning of collection")]
+ [DataContract]
+ public sealed class JsonArray : JsonValue, IList<JsonValue>
+ {
+ [DataMember]
+ private List<JsonValue> values = new List<JsonValue>();
+
+ /// <summary>
+ /// Creates an instance of the <see cref="System.Json.JsonArray"/> class initialized by
+ /// an <see cref="System.Collections.Generic.IEnumerable{T}"/> enumeration of
+ /// objects of type <see cref="System.Json.JsonValue"/>.
+ /// </summary>
+ /// <param name="items">The <see cref="System.Collections.Generic.IEnumerable{T}"/> enumeration
+ /// of objects of type <see cref="System.Json.JsonValue"/> used to initialize the JavaScript Object Notation (JSON)
+ /// array.</param>
+ /// <exception cref="System.ArgumentNullException">If items is null.</exception>
+ /// <exception cref="System.ArgumentException">If any of the items in the collection
+ /// is a <see cref="System.Json.JsonValue"/> with <see cref="System.Json.JsonValue.JsonType"/> property of
+ /// value <see cref="F:System.Json.JsonType.Default"/>.</exception>
+ public JsonArray(IEnumerable<JsonValue> items)
+ {
+ AddRange(items);
+ }
+
+ /// <summary>
+ /// Creates an instance of the <see cref="System.Json.JsonArray"/> class, initialized by an array of type <see cref="System.Json.JsonValue"/>.
+ /// </summary>
+ /// <param name="items">The array of type <see cref="System.Json.JsonValue"/> used to initialize the
+ /// JavaScript Object Notation (JSON) array.</param>
+ /// <exception cref="System.ArgumentException">If any of the items in the collection
+ /// is a <see cref="System.Json.JsonValue"/> with <see cref="System.Json.JsonValue.JsonType"/> property of
+ /// value <see cref="F:System.Json.JsonType.Default"/>.</exception>
+ public JsonArray(params JsonValue[] items)
+ {
+ if (items != null)
+ {
+ AddRange(items);
+ }
+ }
+
+ /// <summary>
+ /// Gets the JSON type of this <see cref="System.Json.JsonArray"/>. The return value
+ /// is always <see cref="F:System.Json.JsonType.Array"/>.
+ /// </summary>
+ public override JsonType JsonType
+ {
+ get { return JsonType.Array; }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether the <see cref="System.Json.JsonArray"/> is read-only.
+ /// </summary>
+ public bool IsReadOnly
+ {
+ get { return ((IList)values).IsReadOnly; }
+ }
+
+ /// <summary>
+ /// Returns the number of <see cref="System.Json.JsonValue"/> elements in the array.
+ /// </summary>
+ public override int Count
+ {
+ get { return values.Count; }
+ }
+
+ /// <summary>
+ /// Gets or sets the JSON value at a specified index.
+ /// </summary>
+ /// <param name="index">The zero-based index of the element to get or set.</param>
+ /// <returns>The <see cref="System.Json.JsonValue"/> element at the specified index.</returns>
+ /// <exception cref="System.ArgumentOutOfRangeException">If index is not a valid index for this array.</exception>
+ /// <exception cref="System.ArgumentException">The property is set and the value is a
+ /// <see cref="System.Json.JsonValue"/> with <see cref="System.Json.JsonValue.JsonType"/>
+ /// property of value <see cref="F:System.Json.JsonType.Default"/>.</exception>
+ public override JsonValue this[int index]
+ {
+ get { return values[index]; }
+
+ set
+ {
+ if (value != null && value.JsonType == JsonType.Default)
+ {
+ throw new ArgumentNullException("value", Properties.Resources.UseOfDefaultNotAllowed);
+ }
+
+ JsonValue oldValue = values[index];
+ RaiseItemChanging(value, JsonValueChange.Replace, index);
+ values[index] = value;
+ RaiseItemChanged(oldValue, JsonValueChange.Replace, index);
+ }
+ }
+
+ /// <summary>
+ /// Adds the elements from a collection of type <see cref="System.Json.JsonValue"/> to this instance.
+ /// </summary>
+ /// <param name="items">Collection of items to add.</param>
+ /// <exception cref="System.ArgumentNullException">If items is null.</exception>
+ /// <exception cref="System.ArgumentException">If any of the items in the collection
+ /// is a <see cref="System.Json.JsonValue"/> with <see cref="System.Json.JsonValue.JsonType"/> property of
+ /// value <see cref="F:System.Json.JsonType.Default"/>.</exception>
+ public void AddRange(IEnumerable<JsonValue> items)
+ {
+ if (items == null)
+ {
+ throw new ArgumentNullException("items");
+ }
+
+ if (ChangingListenersCount > 0)
+ {
+ int index = Count;
+ foreach (JsonValue toBeAdded in items)
+ {
+ RaiseItemChanging(toBeAdded, JsonValueChange.Add, index++);
+ }
+ }
+
+ foreach (JsonValue item in items)
+ {
+ if (item != null && item.JsonType == JsonType.Default)
+ {
+ throw new ArgumentNullException("items", Properties.Resources.UseOfDefaultNotAllowed);
+ }
+
+ values.Add(item);
+ RaiseItemChanged(item, JsonValueChange.Add, values.Count - 1);
+ }
+ }
+
+ /// <summary>
+ /// Adds the elements from an array of type <see cref="System.Json.JsonValue"/> to this instance.
+ /// </summary>
+ /// <param name="items">The array of type JsonValue to be added to this instance.</param>
+ /// <exception cref="System.ArgumentNullException">If items is null.</exception>
+ /// <exception cref="System.ArgumentException">If any of the items in the array
+ /// is a <see cref="System.Json.JsonValue"/> with <see cref="System.Json.JsonValue.JsonType"/> property of
+ /// value <see cref="F:System.Json.JsonType.Default"/>.</exception>
+ public void AddRange(params JsonValue[] items)
+ {
+ AddRange(items as IEnumerable<JsonValue>);
+ }
+
+ /// <summary>
+ /// Searches for a specified object and returns the zero-based index of its first
+ /// occurrence within this <see cref="System.Json.JsonArray"/>.
+ /// </summary>
+ /// <param name="item">The <see cref="System.Json.JsonValue"/> object to look up.</param>
+ /// <returns>The zero-based index of the first occurrence of item within the
+ /// <see cref="System.Json.JsonArray"/>, if found; otherwise, -1.</returns>
+ public int IndexOf(JsonValue item)
+ {
+ return values.IndexOf(item);
+ }
+
+ /// <summary>
+ /// Insert a JSON CLR type into the array at a specified index.
+ /// </summary>
+ /// <param name="index">The zero-based index at which the item should be inserted.</param>
+ /// <param name="item">The <see cref="System.Json.JsonValue"/> object to insert.</param>
+ /// <exception cref="System.ArgumentOutOfRangeException">If index is less than zero or larger than
+ /// the size of the array.</exception>
+ /// <exception cref="System.ArgumentException">If the object to insert has a
+ /// <see cref="System.Json.JsonValue.JsonType"/> property of value
+ /// <see cref="F:System.Json.JsonType.Default"/>.</exception>
+ public void Insert(int index, JsonValue item)
+ {
+ if (item != null && item.JsonType == JsonType.Default)
+ {
+ throw new ArgumentNullException("item", Properties.Resources.UseOfDefaultNotAllowed);
+ }
+
+ RaiseItemChanging(item, JsonValueChange.Add, index);
+ values.Insert(index, item);
+ RaiseItemChanged(item, JsonValueChange.Add, index);
+ }
+
+ /// <summary>
+ /// Remove the JSON value at a specified index of <see cref="System.Json.JsonArray"/>.
+ /// </summary>
+ /// <param name="index">The zero-based index at which to remove the <see cref="System.Json.JsonValue"/>.</param>
+ /// <exception cref="System.ArgumentOutOfRangeException">If index is less than zero or index
+ /// is equal or larger than the size of the array.</exception>
+ public void RemoveAt(int index)
+ {
+ JsonValue item = values[index];
+ RaiseItemChanging(item, JsonValueChange.Remove, index);
+ values.RemoveAt(index);
+ RaiseItemChanged(item, JsonValueChange.Remove, index);
+ }
+
+ /// <summary>
+ /// Adds a <see cref="System.Json.JsonValue"/> object to the end of the array.
+ /// </summary>
+ /// <param name="item">The <see cref="System.Json.JsonValue"/> object to add.</param>
+ /// <exception cref="System.ArgumentException">If the object to add has a
+ /// <see cref="System.Json.JsonValue.JsonType"/> property of value
+ /// <see cref="F:System.Json.JsonType.Default"/>.</exception>
+ public void Add(JsonValue item)
+ {
+ if (item != null && item.JsonType == JsonType.Default)
+ {
+ throw new ArgumentNullException("item", Properties.Resources.UseOfDefaultNotAllowed);
+ }
+
+ int index = Count;
+ RaiseItemChanging(item, JsonValueChange.Add, index);
+ values.Add(item);
+ RaiseItemChanged(item, JsonValueChange.Add, index);
+ }
+
+ /// <summary>
+ /// Removes all JSON CLR types from the <see cref="System.Json.JsonArray"/>.
+ /// </summary>
+ public void Clear()
+ {
+ RaiseItemChanging(null, JsonValueChange.Clear, 0);
+ values.Clear();
+ RaiseItemChanged(null, JsonValueChange.Clear, 0);
+ }
+
+ /// <summary>
+ /// Checks whether a specified JSON CLR type is in the <see cref="System.Json.JsonArray"/>.
+ /// </summary>
+ /// <param name="item">The <see cref="System.Json.JsonValue"/> to check for in the array.</param>
+ /// <returns>true if item is found in the <see cref="System.Json.JsonArray"/>; otherwise, false.</returns>
+ public bool Contains(JsonValue item)
+ {
+ return values.Contains(item);
+ }
+
+ /// <summary>
+ /// Copies the contents of the current JSON CLR array instance into a specified
+ /// destination array beginning at the specified index.
+ /// </summary>
+ /// <param name="array">The destination array to which the elements of the current
+ /// <see cref="System.Json.JsonArray"/> object are copied.</param>
+ /// <param name="arrayIndex">The zero-based index in the destination array at which the
+ /// copying of the elements of the JSON CLR array begins.</param>
+ public void CopyTo(JsonValue[] array, int arrayIndex)
+ {
+ values.CopyTo(array, arrayIndex);
+ }
+
+ /// <summary>
+ /// Removes the first occurrence of the specified JSON value from the array.
+ /// </summary>
+ /// <param name="item">The <see cref="System.Json.JsonValue"/> to remove from the <see cref="System.Json.JsonArray"/>.</param>
+ /// <returns>true if item is successfully removed; otherwise, false. This method
+ /// also returns false if item was not found in the <see cref="System.Json.JsonArray"/>.</returns>
+ public bool Remove(JsonValue item)
+ {
+ int index = -1;
+ if (ChangingListenersCount > 0 || ChangedListenersCount > 0)
+ {
+ index = IndexOf(item);
+ }
+
+ if (index >= 0)
+ {
+ RaiseItemChanging(item, JsonValueChange.Remove, index);
+ }
+
+ bool result = values.Remove(item);
+ if (index >= 0)
+ {
+ RaiseItemChanged(item, JsonValueChange.Remove, index);
+ }
+
+ return result;
+ }
+
+ /// <summary>
+ /// Returns an enumerator that iterates through the <see cref="System.Json.JsonValue"/> objects in the array.
+ /// </summary>
+ /// <returns>Returns an <see cref="System.Collections.IEnumerator"/> object that
+ /// iterates through the <see cref="System.Json.JsonValue"/> elements in this <see cref="System.Json.JsonArray"/>.</returns>
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return values.GetEnumerator();
+ }
+
+ /// <summary>
+ /// Safe indexer for the <see cref="System.Json.JsonValue"/> type.
+ /// </summary>
+ /// <param name="index">The zero-based index of the element to get.</param>
+ /// <returns>If the index is within the array bounds and the value corresponding to the
+ /// index is not null, then it will return that value. Otherwise it will return a
+ /// <see cref="System.Json.JsonValue"/> instance with <see cref="System.Json.JsonValue.JsonType"/>
+ /// equals to <see cref="F:System.Json.JsonType.Default"/>.</returns>
+ public override JsonValue ValueOrDefault(int index)
+ {
+ if (index >= 0 && index < Count && this[index] != null)
+ {
+ return this[index];
+ }
+
+ return base.ValueOrDefault(index);
+ }
+
+ /// <summary>
+ /// Returns an enumerator that iterates through the <see cref="System.Json.JsonValue"/> objects in the array.
+ /// </summary>
+ /// <returns>Returns an <see cref="System.Collections.Generic.IEnumerator{T}"/> object that
+ /// iterates through the <see cref="System.Json.JsonValue"/> elements in this <see cref="System.Json.JsonArray"/>.</returns>
+ public new IEnumerator<JsonValue> GetEnumerator()
+ {
+ return values.GetEnumerator();
+ }
+
+ /// <summary>
+ /// Returns an enumerator which iterates through the values in this object.
+ /// </summary>
+ /// <returns>An <see cref="System.Collections.Generic.IEnumerator{T}"/> which iterates through the values in this object.</returns>
+ /// <remarks>The enumerator returned by this class contains one pair for each element
+ /// in this array, whose key is the element index (as a string), and the value is the
+ /// element itself.</remarks>
+ protected override IEnumerator<KeyValuePair<string, JsonValue>> GetKeyValuePairEnumerator()
+ {
+ for (int i = 0; i < values.Count; i++)
+ {
+ yield return new KeyValuePair<string, JsonValue>(i.ToString(CultureInfo.InvariantCulture), values[i]);
+ }
+ }
+
+ /// <summary>
+ /// Callback method called to let an instance write the proper JXML attribute when saving this
+ /// instance.
+ /// </summary>
+ /// <param name="jsonWriter">The JXML writer used to write JSON.</param>
+ internal override void WriteAttributeString(XmlDictionaryWriter jsonWriter)
+ {
+ if (jsonWriter == null)
+ {
+ throw new ArgumentNullException("jsonWriter");
+ }
+
+ jsonWriter.WriteAttributeString(JXmlToJsonValueConverter.TypeAttributeName, JXmlToJsonValueConverter.ArrayAttributeValue);
+ }
+
+ /// <summary>
+ /// Callback method called during Save operations to let the instance write the start element
+ /// and return the next element in the collection.
+ /// </summary>
+ /// <param name="jsonWriter">The JXML writer used to write JSON.</param>
+ /// <param name="currentIndex">The index within this collection.</param>
+ /// <returns>The next item in the collection, or null of there are no more items.</returns>
+ internal override JsonValue WriteStartElementAndGetNext(XmlDictionaryWriter jsonWriter, int currentIndex)
+ {
+ if (jsonWriter == null)
+ {
+ throw new ArgumentNullException("jsonWriter");
+ }
+
+ jsonWriter.WriteStartElement(JXmlToJsonValueConverter.ItemElementName);
+ JsonValue nextValue = this[currentIndex];
+ return nextValue;
+ }
+
+ private void RaiseItemChanging(JsonValue child, JsonValueChange change, int index)
+ {
+ if (ChangingListenersCount > 0)
+ {
+ RaiseChangingEvent(this, new JsonValueChangeEventArgs(child, change, index));
+ }
+ }
+
+ private void RaiseItemChanged(JsonValue child, JsonValueChange change, int index)
+ {
+ if (ChangedListenersCount > 0)
+ {
+ RaiseChangedEvent(this, new JsonValueChangeEventArgs(child, change, index));
+ }
+ }
+ }
+}
diff --git a/src/System.Json/JsonObject.cs b/src/System.Json/JsonObject.cs
new file mode 100644
index 00000000..de69582c
--- /dev/null
+++ b/src/System.Json/JsonObject.cs
@@ -0,0 +1,472 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.Serialization;
+using System.Xml;
+using WrappedPair = System.Json.NGenWrapper<System.Collections.Generic.KeyValuePair<string, System.Json.JsonValue>>;
+
+namespace System.Json
+{
+ /// <summary>
+ /// A JsonObject is an unordered collection of zero or more key/value pairs.
+ /// </summary>
+ /// <remarks>A JsonObject is an unordered collection of zero or more key/value pairs,
+ /// where each key is a String and each value is a <see cref="System.Json.JsonValue"/>, which can be a
+ /// <see cref="System.Json.JsonPrimitive"/>, a <see cref="System.Json.JsonArray"/>, or a JsonObject.</remarks>
+ [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix",
+ Justification = "Object in the context of JSON already conveys the meaning of dictionary")]
+ [DataContract]
+ public sealed class JsonObject : JsonValue, IDictionary<string, JsonValue>
+ {
+ [DataMember]
+ private Dictionary<string, JsonValue> values = new Dictionary<string, JsonValue>(StringComparer.Ordinal);
+
+ private List<WrappedPair> indexedPairs;
+ private int instanceSaveCount;
+ private object saveLock = new object();
+
+ /// <summary>
+ /// Creates an instance of the <see cref="System.Json.JsonObject"/> class initialized with an
+ /// <see cref="System.Collections.Generic.IEnumerable{T}"/> collection of key/value pairs.
+ /// </summary>
+ /// <param name="items">The <see cref="System.Collections.Generic.IEnumerable{T}"/> collection of
+ /// <see cref="System.Collections.Generic.KeyValuePair{K, V}"/> used to initialize the
+ /// key/value pairs</param>
+ /// <exception cref="System.ArgumentNullException">If items is null.</exception>
+ /// <exception cref="System.ArgumentException">If any of the values in the collection
+ /// is a <see cref="System.Json.JsonValue"/> with <see cref="System.Json.JsonValue.JsonType"/> property of
+ /// value <see cref="F:System.Json.JsonType.Default"/>.</exception>
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures",
+ Justification = "There's no complexity using this design because nested generic type is atomic type not another collection")]
+ public JsonObject(IEnumerable<KeyValuePair<string, JsonValue>> items)
+ {
+ AddRange(items);
+ }
+
+ /// <summary>
+ /// Creates an instance of the <see cref="System.Json.JsonObject"/> class initialized with a collection of key/value pairs.
+ /// </summary>
+ /// <param name="items">The <see cref="System.Collections.Generic.KeyValuePair{K, V}"/> objects used to initialize the key/value pairs.</param>
+ /// <exception cref="System.ArgumentException">If any of the values in the collection
+ /// is a <see cref="System.Json.JsonValue"/> with <see cref="System.Json.JsonValue.JsonType"/> property of
+ /// value <see cref="F:System.Json.JsonType.Default"/>.</exception>
+ public JsonObject(params KeyValuePair<string, JsonValue>[] items)
+ {
+ if (items != null)
+ {
+ AddRange(items);
+ }
+ }
+
+ /// <summary>
+ /// Gets the JSON type of this <see cref="System.Json.JsonObject"/>. The return value
+ /// is always <see cref="F:System.Json.JsonType.Object"/>.
+ /// </summary>
+ public override JsonType JsonType
+ {
+ get { return JsonType.Object; }
+ }
+
+ /// <summary>
+ /// Gets a collection that contains the keys in this <see cref="System.Json.JsonObject"/>.
+ /// </summary>
+ public ICollection<string> Keys
+ {
+ get { return values.Keys; }
+ }
+
+ /// <summary>
+ /// Gets a collection that contains the values in this <see cref="System.Json.JsonObject"/>.
+ /// </summary>
+ public ICollection<JsonValue> Values
+ {
+ get { return values.Values; }
+ }
+
+ /// <summary>
+ /// Returns the number of key/value pairs in this <see cref="System.Json.JsonObject"/>.
+ /// </summary>
+ public override int Count
+ {
+ get { return values.Count; }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether this JSON CLR object is read-only.
+ /// </summary>
+ bool ICollection<KeyValuePair<string, JsonValue>>.IsReadOnly
+ {
+ get { return ((ICollection<KeyValuePair<string, JsonValue>>)values).IsReadOnly; }
+ }
+
+ /// <summary>
+ /// Gets or sets the value associated with the specified key.
+ /// </summary>
+ /// <param name="key">The key of the value to get or set.</param>
+ /// <returns>The <see cref="System.Json.JsonValue"/> associated to the specified key.</returns>
+ /// <exception cref="System.ArgumentNullException">If key is null.</exception>
+ /// <exception cref="System.ArgumentException">The property is set and the value is a
+ /// <see cref="System.Json.JsonValue"/> with <see cref="System.Json.JsonValue.JsonType"/>
+ /// property of value <see cref="F:System.Json.JsonType.Default"/>.</exception>
+ public override JsonValue this[string key]
+ {
+ get
+ {
+ if (key == null)
+ {
+ throw new ArgumentNullException("key");
+ }
+
+ return values[key];
+ }
+
+ set
+ {
+ if (value != null && value.JsonType == JsonType.Default)
+ {
+ throw new ArgumentNullException("value", Properties.Resources.UseOfDefaultNotAllowed);
+ }
+
+ if (key == null)
+ {
+ throw new ArgumentNullException("key");
+ }
+
+ bool replacement = values.ContainsKey(key);
+ JsonValue oldValue = null;
+ if (replacement)
+ {
+ oldValue = values[key];
+ RaiseItemChanging(value, JsonValueChange.Replace, key);
+ }
+ else
+ {
+ RaiseItemChanging(value, JsonValueChange.Add, key);
+ }
+
+ values[key] = value;
+ if (replacement)
+ {
+ RaiseItemChanged(oldValue, JsonValueChange.Replace, key);
+ }
+ else
+ {
+ RaiseItemChanged(value, JsonValueChange.Add, key);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Safe string indexer for the <see cref="System.Json.JsonValue"/> type.
+ /// </summary>
+ /// <param name="key">The key of the element to get.</param>
+ /// <returns>If this instance contains the given key and the value corresponding to
+ /// the key is not null, then it will return that value. Otherwise it will return a
+ /// <see cref="System.Json.JsonValue"/> instance with <see cref="System.Json.JsonValue.JsonType"/>
+ /// equals to <see cref="F:System.Json.JsonType.Default"/>.</returns>
+ public override JsonValue ValueOrDefault(string key)
+ {
+ if (key != null && ContainsKey(key) && this[key] != null)
+ {
+ return this[key];
+ }
+
+ return base.ValueOrDefault(key);
+ }
+
+ /// <summary>
+ /// Adds a specified collection of key/value pairs to this instance.
+ /// </summary>
+ /// <param name="items">The collection of key/value pairs to add.</param>
+ /// <exception cref="System.ArgumentNullException">If items is null.</exception>
+ /// <exception cref="System.ArgumentException">If the value of any of the items in the collection
+ /// is a <see cref="System.Json.JsonValue"/> with <see cref="System.Json.JsonValue.JsonType"/> property of
+ /// value <see cref="F:System.Json.JsonType.Default"/>.</exception>
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures",
+ Justification = "There's no complexity using this design because nested generic type is atomic type not another collection")]
+ public void AddRange(IEnumerable<KeyValuePair<string, JsonValue>> items)
+ {
+ if (items == null)
+ {
+ throw new ArgumentNullException("items");
+ }
+
+ if (ChangingListenersCount > 0)
+ {
+ foreach (KeyValuePair<string, JsonValue> item in items)
+ {
+ RaiseItemChanging(item.Value, JsonValueChange.Add, item.Key);
+ }
+ }
+
+ foreach (KeyValuePair<string, JsonValue> item in items)
+ {
+ if (item.Value != null && item.Value.JsonType == JsonType.Default)
+ {
+ throw new ArgumentNullException("items", Properties.Resources.UseOfDefaultNotAllowed);
+ }
+
+ values.Add(item.Key, item.Value);
+ RaiseItemChanged(item.Value, JsonValueChange.Add, item.Key);
+ }
+ }
+
+ /// <summary>
+ /// Adds the elements from an array of type <see cref="System.Json.JsonValue"/> to this instance.
+ /// </summary>
+ /// <param name="items">The array of key/value paris to be added to this instance.</param>
+ /// <exception cref="System.ArgumentException">If the value of any of the items in the array
+ /// is a <see cref="System.Json.JsonValue"/> with <see cref="System.Json.JsonValue.JsonType"/> property of
+ /// value <see cref="F:System.Json.JsonType.Default"/>.</exception>
+ public void AddRange(params KeyValuePair<string, JsonValue>[] items)
+ {
+ AddRange(items as IEnumerable<KeyValuePair<string, JsonValue>>);
+ }
+
+ /// <summary>
+ ///
+ /// </summary>
+ /// <returns></returns>
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return ((IEnumerable)values).GetEnumerator();
+ }
+
+ /// <summary>
+ /// Adds a key/value pair to this <see cref="System.Json.JsonObject"/> instance.
+ /// </summary>
+ /// <param name="key">The key for the element added.</param>
+ /// <param name="value">The <see cref="System.Json.JsonValue"/> for the element added.</param>
+ /// <exception cref="System.ArgumentException">If the value is a <see cref="System.Json.JsonValue"/>
+ /// with <see cref="System.Json.JsonValue.JsonType"/> property of
+ /// value <see cref="F:System.Json.JsonType.Default"/>.</exception>
+ public void Add(string key, JsonValue value)
+ {
+ if (value != null && value.JsonType == JsonType.Default)
+ {
+ throw new ArgumentNullException("value", Properties.Resources.UseOfDefaultNotAllowed);
+ }
+
+ RaiseItemChanging(value, JsonValueChange.Add, key);
+ values.Add(key, value);
+ RaiseItemChanged(value, JsonValueChange.Add, key);
+ }
+
+ /// <summary>
+ /// Adds a key/value pair to this <see cref="System.Json.JsonObject"/> instance.
+ /// </summary>
+ /// <param name="item">The key/value pair to be added.</param>
+ /// <exception cref="System.ArgumentException">If the value of the pair is a
+ /// <see cref="System.Json.JsonValue"/> with <see cref="System.Json.JsonValue.JsonType"/>
+ /// property of value <see cref="F:System.Json.JsonType.Default"/>.</exception>
+ public void Add(KeyValuePair<string, JsonValue> item)
+ {
+ Add(item.Key, item.Value);
+ }
+
+ /// <summary>
+ /// Checks whether a key/value pair with a specified key exists in this <see cref="System.Json.JsonObject"/> instance.
+ /// </summary>
+ /// <param name="key">The key to check for.</param>
+ /// <returns>true if this instance contains the key; otherwise, false.</returns>
+ public override bool ContainsKey(string key)
+ {
+ return values.ContainsKey(key);
+ }
+
+ /// <summary>
+ /// Removes the key/value pair with a specified key from this <see cref="System.Json.JsonObject"/> instance.
+ /// </summary>
+ /// <param name="key">The key of the item to remove.</param>
+ /// <returns>true if the element is successfully found and removed; otherwise, false.
+ /// This method returns false if key is not found in this <see cref="System.Json.JsonObject"/> instance.</returns>
+ public bool Remove(string key)
+ {
+ JsonValue original = null;
+ bool containsKey = false;
+ if (ChangingListenersCount > 0 || ChangedListenersCount > 0)
+ {
+ containsKey = TryGetValue(key, out original);
+ }
+
+ if (containsKey && ChangingListenersCount > 0)
+ {
+ RaiseItemChanging(original, JsonValueChange.Remove, key);
+ }
+
+ bool result = values.Remove(key);
+
+ if (containsKey && ChangedListenersCount > 0)
+ {
+ RaiseItemChanged(original, JsonValueChange.Remove, key);
+ }
+
+ return result;
+ }
+
+ /// <summary>
+ /// Attempts to get the value that corresponds to the specified key.
+ /// </summary>
+ /// <param name="key">The key of the value to retrieve.</param>
+ /// <param name="value">The primitive or structured <see cref="System.Json.JsonValue"/> object that has the key
+ /// specified. If this object does not contain a key/value pair with the given key,
+ /// this parameter is set to null.</param>
+ /// <returns>true if the instance of the <see cref="System.Json.JsonObject"/> contains an element with the
+ /// specified key; otherwise, false.</returns>
+ public bool TryGetValue(string key, out JsonValue value)
+ {
+ return values.TryGetValue(key, out value);
+ }
+
+ /// <summary>
+ /// Removes all key/value pairs from this <see cref="System.Json.JsonObject"/> instance.
+ /// </summary>
+ public void Clear()
+ {
+ RaiseItemChanging(null, JsonValueChange.Clear, null);
+ values.Clear();
+ RaiseItemChanged(null, JsonValueChange.Clear, null);
+ }
+
+ bool ICollection<KeyValuePair<string, JsonValue>>.Contains(KeyValuePair<string, JsonValue> item)
+ {
+ return ((ICollection<KeyValuePair<string, JsonValue>>)values).Contains(item);
+ }
+
+ /// <summary>
+ /// Copies the contents of this <see cref="System.Json.JsonObject"/> instance into a specified
+ /// key/value destination array beginning at a specified index.
+ /// </summary>
+ /// <param name="array">The destination array of type <see cref="System.Collections.Generic.KeyValuePair{K, V}"/>
+ /// to which the elements of this <see cref="System.Json.JsonObject"/> are copied.</param>
+ /// <param name="arrayIndex">The zero-based index at which to begin the insertion of the
+ /// contents from this <see cref="System.Json.JsonObject"/> instance.</param>
+ public void CopyTo(KeyValuePair<string, JsonValue>[] array, int arrayIndex)
+ {
+ ((ICollection<KeyValuePair<string, JsonValue>>)values).CopyTo(array, arrayIndex);
+ }
+
+ bool ICollection<KeyValuePair<string, JsonValue>>.Remove(KeyValuePair<string, JsonValue> item)
+ {
+ if (ChangingListenersCount > 0)
+ {
+ if (ContainsKey(item.Key) && EqualityComparer<JsonValue>.Default.Equals(item.Value, values[item.Key]))
+ {
+ RaiseItemChanging(item.Value, JsonValueChange.Remove, item.Key);
+ }
+ }
+
+ bool result = ((ICollection<KeyValuePair<string, JsonValue>>)values).Remove(item);
+ if (result)
+ {
+ RaiseItemChanged(item.Value, JsonValueChange.Remove, item.Key);
+ }
+
+ return result;
+ }
+
+ /// <summary>
+ /// Returns an enumerator over the key/value pairs contained in this <see cref="System.Json.JsonObject"/> instance.
+ /// </summary>
+ /// <returns>An <see cref="System.Collections.Generic.IEnumerator{T}"/> which iterates
+ /// through the members of this instance.</returns>
+ protected override IEnumerator<KeyValuePair<string, JsonValue>> GetKeyValuePairEnumerator()
+ {
+ return values.GetEnumerator();
+ }
+
+ /// <summary>
+ /// Callback method called when a Save operation is starting for this instance.
+ /// </summary>
+ protected override void OnSaveStarted()
+ {
+ lock (saveLock)
+ {
+ instanceSaveCount++;
+ if (indexedPairs == null)
+ {
+ indexedPairs = new List<WrappedPair>();
+
+ foreach (KeyValuePair<string, JsonValue> item in values)
+ {
+ indexedPairs.Add(new WrappedPair(item));
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Callback method called when a Save operation is finished for this instance.
+ /// </summary>
+ protected override void OnSaveEnded()
+ {
+ lock (saveLock)
+ {
+ instanceSaveCount--;
+ if (instanceSaveCount == 0)
+ {
+ indexedPairs = null;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Callback method called to let an instance write the proper JXML attribute when saving this
+ /// instance.
+ /// </summary>
+ /// <param name="jsonWriter">The JXML writer used to write JSON.</param>
+ internal override void WriteAttributeString(XmlDictionaryWriter jsonWriter)
+ {
+ jsonWriter.WriteAttributeString(JXmlToJsonValueConverter.TypeAttributeName, JXmlToJsonValueConverter.ObjectAttributeValue);
+ }
+
+ /// <summary>
+ /// Callback method called during Save operations to let the instance write the start element
+ /// and return the next element in the collection.
+ /// </summary>
+ /// <param name="jsonWriter">The JXML writer used to write JSON.</param>
+ /// <param name="currentIndex">The index within this collection.</param>
+ /// <returns>The next item in the collection, or null of there are no more items.</returns>
+ internal override JsonValue WriteStartElementAndGetNext(XmlDictionaryWriter jsonWriter, int currentIndex)
+ {
+ KeyValuePair<string, JsonValue> currentPair = indexedPairs[currentIndex];
+ string currentKey = currentPair.Key;
+
+ if (currentKey.Length == 0)
+ {
+ // special case in JXML world
+ jsonWriter.WriteStartElement(JXmlToJsonValueConverter.ItemElementName, JXmlToJsonValueConverter.ItemElementName);
+ jsonWriter.WriteAttributeString(JXmlToJsonValueConverter.ItemElementName, String.Empty);
+ }
+ else
+ {
+ jsonWriter.WriteStartElement(currentKey);
+ }
+
+ return currentPair.Value;
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", Justification = "Context is required by CLR for this to work.")]
+ [OnDeserialized]
+ private void OnDeserialized(StreamingContext context)
+ {
+ saveLock = new object();
+ }
+
+ private void RaiseItemChanging(JsonValue child, JsonValueChange change, string key)
+ {
+ if (ChangingListenersCount > 0)
+ {
+ RaiseChangingEvent(this, new JsonValueChangeEventArgs(child, change, key));
+ }
+ }
+
+ private void RaiseItemChanged(JsonValue child, JsonValueChange change, string key)
+ {
+ if (ChangedListenersCount > 0)
+ {
+ RaiseChangedEvent(this, new JsonValueChangeEventArgs(child, change, key));
+ }
+ }
+ }
+}
diff --git a/src/System.Json/JsonPrimitive.cs b/src/System.Json/JsonPrimitive.cs
new file mode 100644
index 00000000..0c62e723
--- /dev/null
+++ b/src/System.Json/JsonPrimitive.cs
@@ -0,0 +1,1128 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+using System.Globalization;
+using System.Runtime.Serialization;
+using System.Text;
+using System.Xml;
+
+namespace System.Json
+{
+ /// <summary>
+ /// Represents a JavaScript Object Notation (JSON) primitive type in the common language runtime (CLR).
+ /// </summary>
+ [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix",
+ Justification = "JsonPrimitive does not represent a collection.")]
+ [DataContract]
+ public sealed class JsonPrimitive : JsonValue
+ {
+ internal const string DateTimeIsoFormat = "yyyy-MM-ddTHH:mm:ss.fffK";
+ private const string UtcString = "UTC";
+ private const string GmtString = "GMT";
+ private static readonly long UnixEpochTicks = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).Ticks;
+ private static readonly char[] FloatingPointChars = new char[] { '.', 'e', 'E' };
+ private static readonly Type jsonPrimitiveType = typeof(JsonPrimitive);
+ private static readonly Type uriType = typeof(Uri);
+
+ private static readonly Dictionary<Type, Func<string, ConvertResult>> stringConverters = new Dictionary<Type, Func<string, ConvertResult>>
+ {
+ { typeof(bool), new Func<string, ConvertResult>(StringToBool) },
+ { typeof(byte), new Func<string, ConvertResult>(StringToByte) },
+ { typeof(char), new Func<string, ConvertResult>(StringToChar) },
+ { typeof(sbyte), new Func<string, ConvertResult>(StringToSByte) },
+ { typeof(short), new Func<string, ConvertResult>(StringToShort) },
+ { typeof(int), new Func<string, ConvertResult>(StringToInt) },
+ { typeof(long), new Func<string, ConvertResult>(StringToLong) },
+ { typeof(ushort), new Func<string, ConvertResult>(StringToUShort) },
+ { typeof(uint), new Func<string, ConvertResult>(StringToUInt) },
+ { typeof(ulong), new Func<string, ConvertResult>(StringToULong) },
+ { typeof(float), new Func<string, ConvertResult>(StringToFloat) },
+ { typeof(double), new Func<string, ConvertResult>(StringToDouble) },
+ { typeof(decimal), new Func<string, ConvertResult>(StringToDecimal) },
+ { typeof(DateTime), new Func<string, ConvertResult>(StringToDateTime) },
+ { typeof(DateTimeOffset), new Func<string, ConvertResult>(StringToDateTimeOffset) },
+ { typeof(Guid), new Func<string, ConvertResult>(StringToGuid) },
+ { typeof(Uri), new Func<string, ConvertResult>(StringToUri) },
+ };
+
+ [DataMember]
+ private object value;
+
+ [DataMember]
+ private JsonType jsonType;
+
+ /// <summary>
+ /// Initializes a new instance of a <see cref="System.Json.JsonPrimitive"/> type with a <see cref="System.Boolean"/> type.
+ /// </summary>
+ /// <param name="value">The <see cref="System.Boolean"/> object that initializes the new instance.</param>
+ /// <remarks>A <see cref="System.Json.JsonPrimitive"/> object stores a <see cref="System.Json.JsonType"/> and the value used to initialize it.
+ /// When initialized with a <see cref="System.Boolean"/> object, the <see cref="System.Json.JsonType"/> is a <see cref="F:System.Json.JsonType.Boolean"/>, which can be
+ /// recovered using the <see cref="System.Json.JsonPrimitive.JsonType"/> property. The value used to initialize the <see cref="System.Json.JsonPrimitive"/>
+ /// object can be recovered by casting the <see cref="System.Json.JsonPrimitive"/> to <see cref="System.Boolean"/>.</remarks>
+ public JsonPrimitive(bool value)
+ {
+ jsonType = JsonType.Boolean;
+ this.value = value;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of a <see cref="System.Json.JsonPrimitive"/> type with a <see cref="System.Byte"/> type.
+ /// </summary>
+ /// <param name="value">The <see cref="System.Byte"/> object that initializes the new instance.</param>
+ /// <remarks>A <see cref="System.Json.JsonPrimitive"/> object stores a <see cref="System.Json.JsonType"/> and the value used to initialize it.
+ /// When initialized with a <see cref="System.Byte"/> object, the <see cref="System.Json.JsonType"/> is a <see cref="F:System.Json.JsonType.Number"/>, which can be
+ /// recovered using the <see cref="System.Json.JsonPrimitive.JsonType"/> property. The value used to initialize the <see cref="System.Json.JsonPrimitive"/>
+ /// object can be recovered by casting the <see cref="System.Json.JsonPrimitive"/> to <see cref="System.Byte"/>.</remarks>
+ public JsonPrimitive(byte value)
+ {
+ jsonType = JsonType.Number;
+ this.value = value;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of a <see cref="System.Json.JsonPrimitive"/> type with a <see cref="System.SByte"/> type.
+ /// </summary>
+ /// <param name="value">The <see cref="System.SByte"/> object that initializes the new instance.</param>
+ /// <remarks>A <see cref="System.Json.JsonPrimitive"/> object stores a <see cref="System.Json.JsonType"/> and the value used to initialize it.
+ /// When initialized with a <see cref="System.SByte"/> object, the <see cref="System.Json.JsonType"/> is a <see cref="F:System.Json.JsonType.Number"/>, which can be
+ /// recovered using the <see cref="System.Json.JsonPrimitive.JsonType"/> property. The value used to initialize the <see cref="System.Json.JsonPrimitive"/>
+ /// object can be recovered by casting the <see cref="System.Json.JsonPrimitive"/> to <see cref="System.SByte"/>.</remarks>
+ [CLSCompliant(false)]
+ public JsonPrimitive(sbyte value)
+ {
+ jsonType = JsonType.Number;
+ this.value = value;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of a <see cref="System.Json.JsonPrimitive"/> type with a <see cref="System.Decimal"/> type.
+ /// </summary>
+ /// <param name="value">The <see cref="System.Decimal"/> object that initializes the new instance.</param>
+ /// <remarks>A <see cref="System.Json.JsonPrimitive"/> object stores a <see cref="System.Json.JsonType"/> and the value used to initialize it.
+ /// When initialized with a <see cref="System.Decimal"/> object, the <see cref="System.Json.JsonType"/> is a <see cref="F:System.Json.JsonType.Number"/>, which can be
+ /// recovered using the <see cref="System.Json.JsonPrimitive.JsonType"/> property. The value used to initialize the <see cref="System.Json.JsonPrimitive"/>
+ /// object can be recovered by casting the <see cref="System.Json.JsonPrimitive"/> to <see cref="System.Decimal"/>.</remarks>
+ public JsonPrimitive(decimal value)
+ {
+ jsonType = JsonType.Number;
+ this.value = value;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of a <see cref="System.Json.JsonPrimitive"/> type with a <see cref="System.Int16"/> type.
+ /// </summary>
+ /// <param name="value">The <see cref="System.Int16"/> object that initializes the new instance.</param>
+ /// <remarks>A <see cref="System.Json.JsonPrimitive"/> object stores a <see cref="System.Json.JsonType"/> and the value used to initialize it.
+ /// When initialized with a <see cref="System.Int16"/> object, the <see cref="System.Json.JsonType"/> is a <see cref="F:System.Json.JsonType.Number"/>, which can be
+ /// recovered using the <see cref="System.Json.JsonPrimitive.JsonType"/> property. The value used to initialize the <see cref="System.Json.JsonPrimitive"/>
+ /// object can be recovered by casting the <see cref="System.Json.JsonPrimitive"/> to <see cref="System.Int16"/>.</remarks>
+ public JsonPrimitive(short value)
+ {
+ jsonType = JsonType.Number;
+ this.value = value;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of a <see cref="System.Json.JsonPrimitive"/> type with a <see cref="System.UInt16"/> type.
+ /// </summary>
+ /// <param name="value">The <see cref="System.UInt16"/> object that initializes the new instance.</param>
+ /// <remarks>A <see cref="System.Json.JsonPrimitive"/> object stores a <see cref="System.Json.JsonType"/> and the value used to initialize it.
+ /// When initialized with a <see cref="System.UInt16"/> object, the <see cref="System.Json.JsonType"/> is a <see cref="F:System.Json.JsonType.Number"/>, which can be
+ /// recovered using the <see cref="System.Json.JsonPrimitive.JsonType"/> property. The value used to initialize the <see cref="System.Json.JsonPrimitive"/>
+ /// object can be recovered by casting the <see cref="System.Json.JsonPrimitive"/> to <see cref="System.UInt16"/>.</remarks>
+ [CLSCompliant(false)]
+ public JsonPrimitive(ushort value)
+ {
+ jsonType = JsonType.Number;
+ this.value = value;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of a <see cref="System.Json.JsonPrimitive"/> type with a <see cref="System.Int32"/> type.
+ /// </summary>
+ /// <param name="value">The <see cref="System.Int32"/> object that initializes the new instance.</param>
+ /// <remarks>A <see cref="System.Json.JsonPrimitive"/> object stores a <see cref="System.Json.JsonType"/> and the value used to initialize it.
+ /// When initialized with a <see cref="System.Int32"/> object, the <see cref="System.Json.JsonType"/> is a <see cref="F:System.Json.JsonType.Number"/>, which can be
+ /// recovered using the <see cref="System.Json.JsonPrimitive.JsonType"/> property. The value used to initialize the <see cref="System.Json.JsonPrimitive"/>
+ /// object can be recovered by casting the <see cref="System.Json.JsonPrimitive"/> to <see cref="System.Int32"/>.</remarks>
+ public JsonPrimitive(int value)
+ {
+ jsonType = JsonType.Number;
+ this.value = value;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of a <see cref="System.Json.JsonPrimitive"/> type with a <see cref="System.UInt32"/> type.
+ /// </summary>
+ /// <param name="value">The <see cref="System.UInt32"/> object that initializes the new instance.</param>
+ /// <remarks>A <see cref="System.Json.JsonPrimitive"/> object stores a <see cref="System.Json.JsonType"/> and the value used to initialize it.
+ /// When initialized with a <see cref="System.UInt32"/> object, the <see cref="System.Json.JsonType"/> is a <see cref="F:System.Json.JsonType.Number"/>, which can be
+ /// recovered using the <see cref="System.Json.JsonPrimitive.JsonType"/> property. The value used to initialize the <see cref="System.Json.JsonPrimitive"/>
+ /// object can be recovered by casting the <see cref="System.Json.JsonPrimitive"/> to <see cref="System.UInt32"/>.</remarks>
+ [CLSCompliant(false)]
+ public JsonPrimitive(uint value)
+ {
+ jsonType = JsonType.Number;
+ this.value = value;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of a <see cref="System.Json.JsonPrimitive"/> type with a <see cref="System.Int64"/> type.
+ /// </summary>
+ /// <param name="value">The <see cref="System.Int64"/> object that initializes the new instance.</param>
+ /// <remarks>A <see cref="System.Json.JsonPrimitive"/> object stores a <see cref="System.Json.JsonType"/> and the value used to initialize it.
+ /// When initialized with a <see cref="System.Int64"/> object, the <see cref="System.Json.JsonType"/> is a <see cref="F:System.Json.JsonType.Number"/>, which can be
+ /// recovered using the <see cref="System.Json.JsonPrimitive.JsonType"/> property. The value used to initialize the <see cref="System.Json.JsonPrimitive"/>
+ /// object can be recovered by casting the <see cref="System.Json.JsonPrimitive"/> to <see cref="System.Int64"/>.</remarks>
+ public JsonPrimitive(long value)
+ {
+ jsonType = JsonType.Number;
+ this.value = value;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of a <see cref="System.Json.JsonPrimitive"/> type with a <see cref="System.UInt64"/> type.
+ /// </summary>
+ /// <param name="value">The <see cref="System.UInt64"/> object that initializes the new instance.</param>
+ /// <remarks>A <see cref="System.Json.JsonPrimitive"/> object stores a <see cref="System.Json.JsonType"/> and the value used to initialize it.
+ /// When initialized with a <see cref="System.UInt64"/> object, the <see cref="System.Json.JsonType"/> is a <see cref="F:System.Json.JsonType.Number"/>, which can be
+ /// recovered using the <see cref="System.Json.JsonPrimitive.JsonType"/> property. The value used to initialize the <see cref="System.Json.JsonPrimitive"/>
+ /// object can be recovered by casting the <see cref="System.Json.JsonPrimitive"/> to <see cref="System.UInt64"/>.</remarks>
+ [CLSCompliant(false)]
+ public JsonPrimitive(ulong value)
+ {
+ jsonType = JsonType.Number;
+ this.value = value;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of a <see cref="System.Json.JsonPrimitive"/> type with a <see cref="System.Single"/> type.
+ /// </summary>
+ /// <param name="value">The <see cref="System.Single"/> object that initializes the new instance.</param>
+ /// <remarks>A <see cref="System.Json.JsonPrimitive"/> object stores a <see cref="System.Json.JsonType"/> and the value used to initialize it.
+ /// When initialized with a <see cref="System.Single"/> object, the <see cref="System.Json.JsonType"/> is a <see cref="F:System.Json.JsonType.Number"/>, which can be
+ /// recovered using the <see cref="System.Json.JsonPrimitive.JsonType"/> property. The value used to initialize the <see cref="System.Json.JsonPrimitive"/>
+ /// object can be recovered by casting the <see cref="System.Json.JsonPrimitive"/> to <see cref="System.Single"/>.</remarks>
+ public JsonPrimitive(float value)
+ {
+ jsonType = JsonType.Number;
+ this.value = value;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of a <see cref="System.Json.JsonPrimitive"/> type with a <see cref="System.Double"/> type.
+ /// </summary>
+ /// <param name="value">The <see cref="System.Double"/> object that initializes the new instance.</param>
+ /// <remarks>A <see cref="System.Json.JsonPrimitive"/> object stores a <see cref="System.Json.JsonType"/> and the value used to initialize it.
+ /// When initialized with a <see cref="System.Double"/> object, the <see cref="System.Json.JsonType"/> is a <see cref="F:System.Json.JsonType.Number"/>, which can be
+ /// recovered using the <see cref="System.Json.JsonPrimitive.JsonType"/> property. The value used to initialize the <see cref="System.Json.JsonPrimitive"/>
+ /// object can be recovered by casting the <see cref="System.Json.JsonPrimitive"/> to <see cref="System.Double"/>.</remarks>
+ public JsonPrimitive(double value)
+ {
+ jsonType = JsonType.Number;
+ this.value = value;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of a <see cref="System.Json.JsonPrimitive"/> type with a <see cref="System.String"/> type.
+ /// </summary>
+ /// <param name="value">The <see cref="System.String"/> object that initializes the new instance.</param>
+ /// <remarks>A <see cref="System.Json.JsonPrimitive"/> object stores a <see cref="System.Json.JsonType"/> and the value used to initialize it.
+ /// When initialized with a <see cref="System.String"/> object, the <see cref="System.Json.JsonType"/> is a <see cref="F:System.Json.JsonType.String"/>, which can be
+ /// recovered using the <see cref="System.Json.JsonPrimitive.JsonType"/> property. The value used to initialize the <see cref="System.Json.JsonPrimitive"/>
+ /// object can be recovered by casting the <see cref="System.Json.JsonPrimitive"/> to <see cref="System.String"/>.</remarks>
+ /// <exception cref="System.ArgumentNullException">value is null.</exception>
+ [SuppressMessage("Microsoft.Design", "CA1057:StringUriOverloadsCallSystemUriOverloads",
+ Justification = "This operator does not intend to represent a Uri overload.")]
+ public JsonPrimitive(string value)
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException("value");
+ }
+
+ jsonType = JsonType.String;
+ this.value = value;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of a <see cref="System.Json.JsonPrimitive"/> type with a <see cref="System.Char"/> type.
+ /// </summary>
+ /// <param name="value">The <see cref="System.Char"/> object that initializes the new instance.</param>
+ /// <remarks>A <see cref="System.Json.JsonPrimitive"/> object stores a <see cref="System.Json.JsonType"/> and the value used to initialize it.
+ /// When initialized with a <see cref="System.Char"/> object, the <see cref="System.Json.JsonType"/> is a <see cref="F:System.Json.JsonType.String"/>, which can be
+ /// recovered using the <see cref="System.Json.JsonPrimitive.JsonType"/> property. The value used to initialize the <see cref="System.Json.JsonPrimitive"/>
+ /// object can be recovered by casting the <see cref="System.Json.JsonPrimitive"/> to <see cref="System.Char"/>.</remarks>
+ public JsonPrimitive(char value)
+ {
+ jsonType = JsonType.String;
+ this.value = value;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of a <see cref="System.Json.JsonPrimitive"/> type with a <see cref="System.DateTime"/> type.
+ /// </summary>
+ /// <param name="value">The <see cref="System.DateTime"/> object that initializes the new instance.</param>
+ /// <remarks>A <see cref="System.Json.JsonPrimitive"/> object stores a <see cref="System.Json.JsonType"/> and the value used to initialize it.
+ /// When initialized with a <see cref="System.DateTime"/> object, the <see cref="System.Json.JsonType"/> is a <see cref="F:System.Json.JsonType.String"/>, which can be
+ /// recovered using the <see cref="System.Json.JsonPrimitive.JsonType"/> property. The value used to initialize the <see cref="System.Json.JsonPrimitive"/>
+ /// object can be recovered by casting the <see cref="System.Json.JsonPrimitive"/> to <see cref="System.DateTime"/>.</remarks>
+ public JsonPrimitive(DateTime value)
+ {
+ jsonType = JsonType.String;
+ this.value = value;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of a <see cref="System.Json.JsonPrimitive"/> type with a <see cref="System.DateTimeOffset"/> type.
+ /// </summary>
+ /// <param name="value">The <see cref="System.DateTimeOffset"/> object that initializes the new instance.</param>
+ /// <remarks>A <see cref="System.Json.JsonPrimitive"/> object stores a <see cref="System.Json.JsonType"/> and the value used to initialize it.
+ /// When initialized with a <see cref="System.DateTimeOffset"/> object, the <see cref="System.Json.JsonType"/> is a <see cref="F:System.Json.JsonType.String"/>, which can be
+ /// recovered using the <see cref="System.Json.JsonPrimitive.JsonType"/> property. The value used to initialize the <see cref="System.Json.JsonPrimitive"/>
+ /// object can be recovered by casting the <see cref="System.Json.JsonPrimitive"/> to <see cref="System.DateTimeOffset"/>.</remarks>
+ public JsonPrimitive(DateTimeOffset value)
+ {
+ jsonType = JsonType.String;
+ this.value = value;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of a <see cref="System.Json.JsonPrimitive"/> type with a <see cref="System.Uri"/> type.
+ /// </summary>
+ /// <param name="value">The <see cref="System.Uri"/> object that initializes the new instance.</param>
+ /// <remarks>A <see cref="System.Json.JsonPrimitive"/> object stores a <see cref="System.Json.JsonType"/> and the value used to initialize it.
+ /// When initialized with a <see cref="System.Uri"/> object, the <see cref="System.Json.JsonType"/> is a <see cref="F:System.Json.JsonType.String"/>, which can be
+ /// recovered using the <see cref="System.Json.JsonPrimitive.JsonType"/> property. The value used to initialize the <see cref="System.Json.JsonPrimitive"/>
+ /// object can be recovered by casting the <see cref="System.Json.JsonPrimitive"/> to <see cref="System.Uri"/>.</remarks>
+ /// <exception cref="System.ArgumentNullException">value is null.</exception>
+ public JsonPrimitive(Uri value)
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException("value");
+ }
+
+ jsonType = JsonType.String;
+ this.value = value;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of a <see cref="System.Json.JsonPrimitive"/> type with a <see cref="System.Guid"/> type.
+ /// </summary>
+ /// <param name="value">The <see cref="System.Guid"/> object that initializes the new instance.</param>
+ /// <remarks>A <see cref="System.Json.JsonPrimitive"/> object stores a <see cref="System.Json.JsonType"/> and the value used to initialize it.
+ /// When initialized with a <see cref="System.Guid"/> object, the <see cref="System.Json.JsonType"/> is a <see cref="F:System.Json.JsonType.String"/>, which can be
+ /// recovered using the <see cref="System.Json.JsonPrimitive.JsonType"/> property. The value used to initialize the <see cref="System.Json.JsonPrimitive"/>
+ /// object can be recovered by casting the <see cref="System.Json.JsonPrimitive"/> to <see cref="System.Guid"/>.</remarks>
+ public JsonPrimitive(Guid value)
+ {
+ jsonType = JsonType.String;
+ this.value = value;
+ }
+
+ private JsonPrimitive(object value, JsonType type)
+ {
+ jsonType = type;
+ this.value = value;
+ }
+
+ private enum ReadAsFailureKind
+ {
+ NoFailure,
+ InvalidCast,
+ InvalidDateFormat,
+ InvalidFormat,
+ InvalidUriFormat,
+ Overflow,
+ }
+
+ /// <summary>
+ /// Gets the JsonType that is associated with this <see cref="System.Json.JsonPrimitive"/> object.
+ /// </summary>
+ public override JsonType JsonType
+ {
+ get { return jsonType; }
+ }
+
+ /// <summary>
+ /// Gets the value represented by this instance.
+ /// </summary>
+ [SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods",
+ Justification = "Value in this context clearly refers to the underlying CLR value")]
+ public object Value
+ {
+ get { return value; }
+ }
+
+ /// <summary>
+ /// Attempts to create a <see cref="JsonPrimitive"/> instance from the specified <see cref="object"/> value.
+ /// </summary>
+ /// <param name="value">The <see cref="object"/> value to create the <see cref="JsonPrimitive"/> instance.</param>
+ /// <param name="result">The resulting <see cref="JsonPrimitive"/> instance on success, null otherwise.</param>
+ /// <returns>true if the operation is successful, false otherwise.</returns>
+ public static bool TryCreate(object value, out JsonPrimitive result)
+ {
+ bool allowedType = true;
+ JsonType jsonType = default(JsonType);
+
+ if (value != null)
+ {
+ Type type = value.GetType();
+ switch (Type.GetTypeCode(type))
+ {
+ case TypeCode.Boolean:
+ jsonType = JsonType.Boolean;
+ break;
+ case TypeCode.Byte:
+ case TypeCode.SByte:
+ case TypeCode.Decimal:
+ case TypeCode.Double:
+ case TypeCode.Int16:
+ case TypeCode.Int32:
+ case TypeCode.Int64:
+ case TypeCode.UInt16:
+ case TypeCode.UInt32:
+ case TypeCode.UInt64:
+ case TypeCode.Single:
+ jsonType = JsonType.Number;
+ break;
+ case TypeCode.String:
+ case TypeCode.Char:
+ case TypeCode.DateTime:
+ jsonType = JsonType.String;
+ break;
+ default:
+ if (type == typeof(Uri) || type == typeof(Guid) || type == typeof(DateTimeOffset))
+ {
+ jsonType = JsonType.String;
+ }
+ else
+ {
+ allowedType = false;
+ }
+
+ break;
+ }
+ }
+ else
+ {
+ allowedType = false;
+ }
+
+ if (allowedType)
+ {
+ result = new JsonPrimitive(value, jsonType);
+ return true;
+ }
+ else
+ {
+ result = null;
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// Attempts to convert this <see cref="System.Json.JsonPrimitive"/> instance into an instance of the specified type.
+ /// </summary>
+ /// <param name="type">The type to which the conversion is being performed.</param>
+ /// <returns>An object instance initialized with the <see cref="System.Json.JsonValue"/> value
+ /// specified if the conversion.</returns>
+ /// <exception cref="System.UriFormatException">If T is <see cref="System.Uri"/> and this value does
+ /// not represent a valid Uri.</exception>
+ /// <exception cref="OverflowException">If T is a numeric type, and a narrowing conversion would result
+ /// in a loss of data. For example, if this instance holds an <see cref="System.Int32"/> value of 10000,
+ /// and T is <see cref="System.Byte"/>, this operation would throw an <see cref="System.OverflowException"/>
+ /// because 10000 is outside the range of the <see cref="System.Byte"/> data type.</exception>
+ /// <exception cref="System.FormatException">If the conversion from the string representation of this
+ /// value into another fails because the string is not in the proper format.</exception>
+ /// <exception cref="System.InvalidCastException">If this instance cannot be read as type T.</exception>
+ public override object ReadAs(Type type)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+
+ object result;
+ ReadAsFailureKind failure = TryReadAsInternal(type, out result);
+ if (failure == ReadAsFailureKind.NoFailure)
+ {
+ return result;
+ }
+ else
+ {
+ string valueStr = value.ToString();
+ string typeOfTName = type.Name;
+ switch (failure)
+ {
+ case ReadAsFailureKind.InvalidFormat:
+ throw new FormatException(RS.Format(Properties.Resources.CannotReadPrimitiveAsType, valueStr, typeOfTName));
+ case ReadAsFailureKind.InvalidDateFormat:
+ throw new FormatException(RS.Format(Properties.Resources.InvalidDateFormat, valueStr, typeOfTName));
+ case ReadAsFailureKind.InvalidUriFormat:
+ throw new UriFormatException(RS.Format(Properties.Resources.InvalidUriFormat, jsonPrimitiveType.Name, valueStr, typeOfTName, uriType.Name));
+ case ReadAsFailureKind.Overflow:
+ throw new OverflowException(RS.Format(Properties.Resources.OverflowReadAs, valueStr, typeOfTName));
+ case ReadAsFailureKind.InvalidCast:
+ default:
+ throw new InvalidCastException(RS.Format(Properties.Resources.CannotReadPrimitiveAsType, valueStr, typeOfTName));
+ }
+ }
+ }
+
+ /// <summary>
+ /// Attempts to convert this <see cref="System.Json.JsonPrimitive"/> instance into an instance of the specified type.
+ /// </summary>
+ /// <param name="type">The type to which the conversion is being performed.</param>
+ /// <param name="value">An object instance to be initialized with this instance or null if the conversion cannot be performed.</param>
+ /// <returns>true if this <see cref="System.Json.JsonPrimitive"/> instance can be read as the specified type; otherwise, false.</returns>
+ [SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "value",
+ Justification = "field is used with 'this' and arg is out param which makes it harder to be misused.")]
+ public override bool TryReadAs(Type type, out object value)
+ {
+ return TryReadAsInternal(type, out value) == ReadAsFailureKind.NoFailure;
+ }
+
+ /// <summary>
+ /// Returns the value this object wraps (if any).
+ /// </summary>
+ /// <returns>The value wrapped by this instance or null if none.</returns>
+ internal override object Read()
+ {
+ return value;
+ }
+
+ internal override void Save(XmlDictionaryWriter jsonWriter)
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException("jsonWriter");
+ }
+
+ switch (jsonType)
+ {
+ case JsonType.Boolean:
+ jsonWriter.WriteAttributeString(JXmlToJsonValueConverter.TypeAttributeName, JXmlToJsonValueConverter.BooleanAttributeValue);
+ break;
+ case JsonType.Number:
+ jsonWriter.WriteAttributeString(JXmlToJsonValueConverter.TypeAttributeName, JXmlToJsonValueConverter.NumberAttributeValue);
+ break;
+ default:
+ jsonWriter.WriteAttributeString(JXmlToJsonValueConverter.TypeAttributeName, JXmlToJsonValueConverter.StringAttributeValue);
+ break;
+ }
+
+ WriteValue(jsonWriter);
+ }
+
+ private static ConvertResult StringToBool(string valueString)
+ {
+ ConvertResult result = new ConvertResult();
+ bool tempBool;
+ result.ReadAsFailureKind = Boolean.TryParse(valueString, out tempBool) ? ReadAsFailureKind.NoFailure : ReadAsFailureKind.InvalidFormat;
+ result.Value = tempBool;
+ return result;
+ }
+
+ private static ConvertResult StringToByte(string valueString)
+ {
+ ConvertResult result = new ConvertResult();
+ byte tempByte;
+ result.ReadAsFailureKind = Byte.TryParse(valueString, out tempByte) ? ReadAsFailureKind.NoFailure : ReadAsFailureKind.InvalidCast;
+ if (result.ReadAsFailureKind != ReadAsFailureKind.NoFailure)
+ {
+ result.ReadAsFailureKind = StringToNumberConverter<byte>(valueString, out tempByte);
+ }
+
+ result.Value = tempByte;
+ return result;
+ }
+
+ private static ConvertResult StringToChar(string valueString)
+ {
+ ConvertResult result = new ConvertResult();
+ char tempChar;
+ result.ReadAsFailureKind = Char.TryParse(valueString, out tempChar) ? ReadAsFailureKind.NoFailure : ReadAsFailureKind.InvalidFormat;
+ result.Value = tempChar;
+ return result;
+ }
+
+ private static ConvertResult StringToDecimal(string valueString)
+ {
+ ConvertResult result = new ConvertResult();
+ decimal tempDecimal;
+ result.ReadAsFailureKind = Decimal.TryParse(valueString, NumberStyles.Float, NumberFormatInfo.InvariantInfo, out tempDecimal) ? ReadAsFailureKind.NoFailure : ReadAsFailureKind.InvalidCast;
+ if (result.ReadAsFailureKind != ReadAsFailureKind.NoFailure)
+ {
+ result.ReadAsFailureKind = StringToNumberConverter<decimal>(valueString, out tempDecimal);
+ }
+
+ result.Value = tempDecimal;
+ return result;
+ }
+
+ private static ConvertResult StringToDateTime(string valueString)
+ {
+ ConvertResult result = new ConvertResult();
+ DateTime tempDateTime;
+ result.ReadAsFailureKind = TryParseDateTime(valueString, out tempDateTime) ? ReadAsFailureKind.NoFailure : ReadAsFailureKind.InvalidDateFormat;
+ result.Value = tempDateTime;
+ return result;
+ }
+
+ private static ConvertResult StringToDateTimeOffset(string valueString)
+ {
+ ConvertResult result = new ConvertResult();
+ DateTimeOffset tempDateTimeOffset;
+ result.ReadAsFailureKind = TryParseDateTimeOffset(valueString, out tempDateTimeOffset) ? ReadAsFailureKind.NoFailure : ReadAsFailureKind.InvalidDateFormat;
+ result.Value = tempDateTimeOffset;
+ return result;
+ }
+
+ private static ConvertResult StringToDouble(string valueString)
+ {
+ ConvertResult result = new ConvertResult();
+ double tempDouble;
+ result.ReadAsFailureKind = Double.TryParse(valueString, NumberStyles.Float, NumberFormatInfo.InvariantInfo, out tempDouble) ? ReadAsFailureKind.NoFailure : ReadAsFailureKind.InvalidCast;
+ if (result.ReadAsFailureKind != ReadAsFailureKind.NoFailure)
+ {
+ result.ReadAsFailureKind = StringToNumberConverter<double>(valueString, out tempDouble);
+ }
+
+ result.Value = tempDouble;
+ return result;
+ }
+
+ private static ConvertResult StringToGuid(string valueString)
+ {
+ ConvertResult result = new ConvertResult();
+ Guid tempGuid;
+ result.ReadAsFailureKind = Guid.TryParse(valueString, out tempGuid) ? ReadAsFailureKind.NoFailure : ReadAsFailureKind.InvalidFormat;
+ result.Value = tempGuid;
+ return result;
+ }
+
+ private static ConvertResult StringToShort(string valueString)
+ {
+ ConvertResult result = new ConvertResult();
+ short tempShort;
+ result.ReadAsFailureKind = Int16.TryParse(valueString, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out tempShort) ? ReadAsFailureKind.NoFailure : ReadAsFailureKind.InvalidCast;
+ if (result.ReadAsFailureKind != ReadAsFailureKind.NoFailure)
+ {
+ result.ReadAsFailureKind = StringToNumberConverter<short>(valueString, out tempShort);
+ }
+
+ result.Value = tempShort;
+ return result;
+ }
+
+ private static ConvertResult StringToInt(string valueString)
+ {
+ ConvertResult result = new ConvertResult();
+ int tempInt;
+ result.ReadAsFailureKind = Int32.TryParse(valueString, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out tempInt) ? ReadAsFailureKind.NoFailure : ReadAsFailureKind.InvalidCast;
+ if (result.ReadAsFailureKind != ReadAsFailureKind.NoFailure)
+ {
+ result.ReadAsFailureKind = StringToNumberConverter<int>(valueString, out tempInt);
+ }
+
+ result.Value = tempInt;
+ return result;
+ }
+
+ private static ConvertResult StringToLong(string valueString)
+ {
+ ConvertResult result = new ConvertResult();
+ long tempLong;
+ result.ReadAsFailureKind = Int64.TryParse(valueString, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out tempLong) ? ReadAsFailureKind.NoFailure : ReadAsFailureKind.InvalidCast;
+ if (result.ReadAsFailureKind != ReadAsFailureKind.NoFailure)
+ {
+ result.ReadAsFailureKind = StringToNumberConverter<long>(valueString, out tempLong);
+ }
+
+ result.Value = tempLong;
+ return result;
+ }
+
+ private static ConvertResult StringToSByte(string valueString)
+ {
+ ConvertResult result = new ConvertResult();
+ sbyte tempSByte;
+ result.ReadAsFailureKind = SByte.TryParse(valueString, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out tempSByte) ? ReadAsFailureKind.NoFailure : ReadAsFailureKind.InvalidCast;
+ if (result.ReadAsFailureKind != ReadAsFailureKind.NoFailure)
+ {
+ result.ReadAsFailureKind = StringToNumberConverter<sbyte>(valueString, out tempSByte);
+ }
+
+ result.Value = tempSByte;
+ return result;
+ }
+
+ private static ConvertResult StringToFloat(string valueString)
+ {
+ ConvertResult result = new ConvertResult();
+ float tempFloat;
+ result.ReadAsFailureKind = Single.TryParse(valueString, NumberStyles.Float, NumberFormatInfo.InvariantInfo, out tempFloat) ? ReadAsFailureKind.NoFailure : ReadAsFailureKind.InvalidCast;
+ if (result.ReadAsFailureKind != ReadAsFailureKind.NoFailure)
+ {
+ result.ReadAsFailureKind = StringToNumberConverter<float>(valueString, out tempFloat);
+ }
+
+ result.Value = tempFloat;
+ return result;
+ }
+
+ private static ConvertResult StringToUShort(string valueString)
+ {
+ ConvertResult result = new ConvertResult();
+ ushort tempUShort;
+ result.ReadAsFailureKind = UInt16.TryParse(valueString, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out tempUShort) ? ReadAsFailureKind.NoFailure : ReadAsFailureKind.InvalidCast;
+ if (result.ReadAsFailureKind != ReadAsFailureKind.NoFailure)
+ {
+ result.ReadAsFailureKind = StringToNumberConverter<ushort>(valueString, out tempUShort);
+ }
+
+ result.Value = tempUShort;
+ return result;
+ }
+
+ private static ConvertResult StringToUInt(string valueString)
+ {
+ ConvertResult result = new ConvertResult();
+ uint tempUInt;
+ result.ReadAsFailureKind = UInt32.TryParse(valueString, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out tempUInt) ? ReadAsFailureKind.NoFailure : ReadAsFailureKind.InvalidCast;
+ if (result.ReadAsFailureKind != ReadAsFailureKind.NoFailure)
+ {
+ result.ReadAsFailureKind = StringToNumberConverter<uint>(valueString, out tempUInt);
+ }
+
+ result.Value = tempUInt;
+ return result;
+ }
+
+ private static ConvertResult StringToULong(string valueString)
+ {
+ ConvertResult result = new ConvertResult();
+ ulong tempULong;
+ result.ReadAsFailureKind = UInt64.TryParse(valueString, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out tempULong) ? ReadAsFailureKind.NoFailure : ReadAsFailureKind.InvalidCast;
+ if (result.ReadAsFailureKind != ReadAsFailureKind.NoFailure)
+ {
+ result.ReadAsFailureKind = StringToNumberConverter<ulong>(valueString, out tempULong);
+ }
+
+ result.Value = tempULong;
+ return result;
+ }
+
+ private static ConvertResult StringToUri(string valueString)
+ {
+ ConvertResult result = new ConvertResult();
+ Uri tempUri;
+ result.ReadAsFailureKind = Uri.TryCreate(valueString, UriKind.RelativeOrAbsolute, out tempUri) ? ReadAsFailureKind.NoFailure : ReadAsFailureKind.InvalidUriFormat;
+ result.Value = tempUri;
+ return result;
+ }
+
+ private static ReadAsFailureKind StringToNumberConverter<T>(string valueString, out T valueNumber)
+ {
+ string str = valueString.Trim();
+
+ if (str.IndexOfAny(FloatingPointChars) < 0)
+ {
+ long longVal;
+ if (Int64.TryParse(str, NumberStyles.Float, CultureInfo.InvariantCulture, out longVal))
+ {
+ return NumberToNumberConverter<T>(longVal, out valueNumber);
+ }
+ }
+
+ decimal decValue;
+ if (Decimal.TryParse(str, NumberStyles.Float, CultureInfo.InvariantCulture, out decValue) && decValue != 0)
+ {
+ return NumberToNumberConverter<T>(decValue, out valueNumber);
+ }
+
+ double dblValue;
+ if (Double.TryParse(str, NumberStyles.Float, CultureInfo.InvariantCulture, out dblValue))
+ {
+ return NumberToNumberConverter<T>(dblValue, out valueNumber);
+ }
+
+ valueNumber = default(T);
+ return ReadAsFailureKind.InvalidFormat;
+ }
+
+ private static ReadAsFailureKind NumberToNumberConverter<T>(object valueObject, out T valueNumber)
+ {
+ object value;
+ ReadAsFailureKind failureKind = NumberToNumberConverter(typeof(T), valueObject, out value);
+ if (failureKind == ReadAsFailureKind.NoFailure)
+ {
+ valueNumber = (T)value;
+ }
+ else
+ {
+ valueNumber = default(T);
+ }
+
+ return failureKind;
+ }
+
+ private static ReadAsFailureKind NumberToNumberConverter(Type type, object valueObject, out object valueNumber)
+ {
+ try
+ {
+ valueNumber = System.Convert.ChangeType(valueObject, type, CultureInfo.InvariantCulture);
+ return ReadAsFailureKind.NoFailure;
+ }
+ catch (OverflowException)
+ {
+ valueNumber = null;
+ return ReadAsFailureKind.Overflow;
+ }
+ }
+
+ private static bool TryParseDateTime(string valueString, out DateTime dateTime)
+ {
+ string filteredValue = valueString.EndsWith(UtcString, StringComparison.Ordinal) ? valueString.Replace(UtcString, GmtString) : valueString;
+
+ if (DateTime.TryParse(filteredValue, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out dateTime))
+ {
+ return true;
+ }
+
+ if (TryParseAspNetDateTimeFormat(valueString, out dateTime))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ private static bool TryParseDateTimeOffset(string valueString, out DateTimeOffset dateTimeOffset)
+ {
+ string filteredValue = valueString.EndsWith(UtcString, StringComparison.Ordinal) ? valueString.Replace(UtcString, GmtString) : valueString;
+
+ if (DateTimeOffset.TryParse(filteredValue, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out dateTimeOffset))
+ {
+ return true;
+ }
+
+ if (TryParseAspNetDateTimeFormat(valueString, out dateTimeOffset))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ private static bool TryParseAspNetDateTimeFormat(string valueString, out DateTime dateTime)
+ {
+ // Reference to the format is available at these sources:
+ // http://msdn.microsoft.com/en-us/library/bb299886.aspx#intro_to_json_sidebarb
+ // http://msdn.microsoft.com/en-us/library/bb412170.aspx
+
+ // The format for the value is given by the following regex:
+ // \/Date\((?<milliseconds>\-?\d+)(?<offset>[\+\-]?\d{4})\)\/
+ // where milliseconds is the number of milliseconds since 1970/01/01:00:00:00.000 UTC (the "unix baseline")
+ // and offset is an optional which indicates whether the value is local or UTC.
+ // The actual value of the offset is ignored, since the ticks represent the UTC offset. The value is converted to local time based on that info.
+ const string DateTimePrefix = "/Date(";
+ const int DateTimePrefixLength = 6;
+ const string DateTimeSuffix = ")/";
+ const int DateTimeSuffixLength = 2;
+
+ if (valueString.StartsWith(DateTimePrefix, StringComparison.Ordinal) && valueString.EndsWith(DateTimeSuffix, StringComparison.Ordinal))
+ {
+ string ticksValue = valueString.Substring(DateTimePrefixLength, valueString.Length - DateTimePrefixLength - DateTimeSuffixLength);
+ DateTimeKind dateTimeKind = DateTimeKind.Utc;
+
+ int indexOfTimeZoneOffset = ticksValue.IndexOf('+', 1);
+
+ if (indexOfTimeZoneOffset < 0)
+ {
+ indexOfTimeZoneOffset = ticksValue.IndexOf('-', 1);
+ }
+
+ // If an offset is present, verify it is properly formatted. Actual value is ignored (see spec).
+ if (indexOfTimeZoneOffset != -1)
+ {
+ if (indexOfTimeZoneOffset + 5 == ticksValue.Length
+ && IsLatinDigit(ticksValue[indexOfTimeZoneOffset + 1])
+ && IsLatinDigit(ticksValue[indexOfTimeZoneOffset + 2])
+ && IsLatinDigit(ticksValue[indexOfTimeZoneOffset + 3])
+ && IsLatinDigit(ticksValue[indexOfTimeZoneOffset + 4]))
+ {
+ ticksValue = ticksValue.Substring(0, indexOfTimeZoneOffset);
+ dateTimeKind = DateTimeKind.Local;
+ }
+ else
+ {
+ dateTime = new DateTime();
+ return false;
+ }
+ }
+
+ long millisecondsSinceUnixEpoch;
+ if (Int64.TryParse(ticksValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out millisecondsSinceUnixEpoch))
+ {
+ long ticks = (millisecondsSinceUnixEpoch * 10000) + UnixEpochTicks;
+ if (ticks < DateTime.MaxValue.Ticks)
+ {
+ dateTime = new DateTime(ticks, DateTimeKind.Utc);
+ if (dateTimeKind == DateTimeKind.Local)
+ {
+ dateTime = dateTime.ToLocalTime();
+ }
+
+ return true;
+ }
+ }
+ }
+
+ dateTime = new DateTime();
+ return false;
+ }
+
+ private static bool TryParseAspNetDateTimeFormat(string valueString, out DateTimeOffset dateTimeOffset)
+ {
+ DateTime dateTime;
+ if (TryParseAspNetDateTimeFormat(valueString, out dateTime))
+ {
+ dateTimeOffset = new DateTimeOffset(dateTime);
+ return true;
+ }
+
+ dateTimeOffset = new DateTimeOffset();
+ return false;
+ }
+
+ private static bool IsLatinDigit(char c)
+ {
+ return (c >= '0') && (c <= '9');
+ }
+
+ private static string UnescapeJsonString(string val)
+ {
+ if (val == null)
+ {
+ return null;
+ }
+
+ StringBuilder sb = null;
+ int startIndex = 0, count = 0;
+ for (int i = 0; i < val.Length; i++)
+ {
+ if (val[i] == '\\')
+ {
+ i++;
+ if (sb == null)
+ {
+ sb = new StringBuilder();
+ }
+
+ sb.Append(val, startIndex, count);
+ Contract.Assert(i < val.Length, "Found that a '\' was the last character in a string, which is invalid JSON. Verify the calling method uses a valid JSON string as the input parameter of this method.");
+ switch (val[i])
+ {
+ case '"':
+ case '\'':
+ case '/':
+ case '\\':
+ sb.Append(val[i]);
+ break;
+ case 'b':
+ sb.Append('\b');
+ break;
+ case 'f':
+ sb.Append('\f');
+ break;
+ case 'n':
+ sb.Append('\n');
+ break;
+ case 'r':
+ sb.Append('\r');
+ break;
+ case 't':
+ sb.Append('\t');
+ break;
+ case 'u':
+ Contract.Assert((i + 3) < val.Length, String.Format(CultureInfo.CurrentCulture, "Unexpected char {0} at position {1}. The unicode escape sequence should be followed by 4 digits.", val[i], i));
+ sb.Append(ParseChar(val.Substring(i + 1, 4), NumberStyles.HexNumber));
+ i += 4;
+ break;
+ }
+
+ startIndex = i + 1;
+ count = 0;
+ }
+ else
+ {
+ count++;
+ }
+ }
+
+ if (sb == null)
+ {
+ return val;
+ }
+
+ if (count > 0)
+ {
+ sb.Append(val, startIndex, count);
+ }
+
+ return sb.ToString();
+ }
+
+ private static char ParseChar(string value, NumberStyles style)
+ {
+ try
+ {
+ int intValue = Int32.Parse(value, style, NumberFormatInfo.InvariantInfo);
+ return System.Convert.ToChar(intValue);
+ }
+ catch (ArgumentException exception)
+ {
+ throw new InvalidCastException(exception.Message, exception);
+ }
+ catch (FormatException exception)
+ {
+ throw new InvalidCastException(exception.Message, exception);
+ }
+ catch (OverflowException exception)
+ {
+ throw new InvalidCastException(exception.Message, exception);
+ }
+ }
+
+ [SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "value",
+ Justification = "field is used with 'this' and arg is out param which makes it harder to be misused.")]
+ private ReadAsFailureKind TryReadAsInternal(Type type, out object value)
+ {
+ if (base.TryReadAs(type, out value))
+ {
+ return ReadAsFailureKind.NoFailure;
+ }
+
+ if (type == this.value.GetType())
+ {
+ value = this.value;
+ return ReadAsFailureKind.NoFailure;
+ }
+
+ if (jsonType == JsonType.Number)
+ {
+ switch (Type.GetTypeCode(type))
+ {
+ case TypeCode.Byte:
+ case TypeCode.SByte:
+ case TypeCode.Int16:
+ case TypeCode.Int32:
+ case TypeCode.Int64:
+ case TypeCode.UInt16:
+ case TypeCode.UInt32:
+ case TypeCode.UInt64:
+ case TypeCode.Single:
+ case TypeCode.Double:
+ case TypeCode.Decimal:
+ return NumberToNumberConverter(type, this.value, out value);
+ case TypeCode.String:
+ value = ToString();
+ return ReadAsFailureKind.NoFailure;
+ }
+ }
+
+ if (jsonType == JsonType.Boolean)
+ {
+ if (type == typeof(string))
+ {
+ value = ToString();
+ return ReadAsFailureKind.NoFailure;
+ }
+ }
+
+ if (jsonType == JsonType.String)
+ {
+ string str = UnescapeJsonString(ToString());
+ Contract.Assert(str.Length >= 2 && str.StartsWith("\"", StringComparison.Ordinal) && str.EndsWith("\"", StringComparison.Ordinal), "The unescaped string must begin and end with quotes.");
+ str = str.Substring(1, str.Length - 2);
+
+ if (stringConverters.ContainsKey(type))
+ {
+ ConvertResult result = stringConverters[type].Invoke(str);
+ value = result.Value;
+ return result.ReadAsFailureKind;
+ }
+
+ if (type == typeof(string))
+ {
+ value = str;
+ return ReadAsFailureKind.NoFailure;
+ }
+ }
+
+ value = null;
+ return ReadAsFailureKind.InvalidCast;
+ }
+
+ private void WriteValue(XmlDictionaryWriter jsonWriter)
+ {
+ Type valueType = value.GetType();
+ switch (Type.GetTypeCode(valueType))
+ {
+ case TypeCode.Boolean:
+ jsonWriter.WriteValue((bool)value);
+ break;
+ case TypeCode.Byte:
+ case TypeCode.Int16:
+ case TypeCode.Int32:
+ case TypeCode.Int64:
+ case TypeCode.SByte:
+ case TypeCode.UInt16:
+ case TypeCode.UInt32:
+ case TypeCode.UInt64:
+ case TypeCode.Decimal:
+ jsonWriter.WriteValue(String.Format(CultureInfo.InvariantCulture, "{0}", value));
+ break;
+ case TypeCode.Single:
+ case TypeCode.Double:
+ jsonWriter.WriteValue(String.Format(CultureInfo.InvariantCulture, "{0:R}", value));
+ break;
+ case TypeCode.Char:
+ jsonWriter.WriteValue(new string((char)value, 1));
+ break;
+ case TypeCode.String:
+ jsonWriter.WriteValue((string)value);
+ break;
+ case TypeCode.DateTime:
+ jsonWriter.WriteValue(((DateTime)value).ToString(DateTimeIsoFormat, CultureInfo.InvariantCulture));
+ break;
+ default:
+ if (valueType == typeof(Uri))
+ {
+ Uri uri = (Uri)value;
+ jsonWriter.WriteValue(uri.GetComponents(UriComponents.SerializationInfoString, UriFormat.UriEscaped));
+ }
+ else if (valueType == typeof(DateTimeOffset))
+ {
+ jsonWriter.WriteValue(((DateTimeOffset)value).ToString(DateTimeIsoFormat, CultureInfo.InvariantCulture));
+ }
+ else
+ {
+ jsonWriter.WriteValue(value);
+ }
+
+ break;
+ }
+ }
+
+ private class ConvertResult
+ {
+ public ReadAsFailureKind ReadAsFailureKind { get; set; }
+
+ public object Value { get; set; }
+ }
+ }
+}
diff --git a/src/System.Json/JsonType.cs b/src/System.Json/JsonType.cs
new file mode 100644
index 00000000..eceedd70
--- /dev/null
+++ b/src/System.Json/JsonType.cs
@@ -0,0 +1,48 @@
+namespace System.Json
+{
+ /// <summary>
+ /// An enumeration that specifies primitive and structured JavaScript Object
+ /// Notation (JSON) common language runtime (CLR) types.
+ /// </summary>
+ public enum JsonType
+ {
+ /// <summary>
+ /// Specifies the JSON string CLR type.
+ /// </summary>
+ String,
+
+ /// <summary>
+ /// Specifies the JSON number CLR type.
+ /// </summary>
+ Number,
+
+ /// <summary>
+ /// Specifies the JSON object CLR type that consists of an unordered collection
+ /// of key/value pairs, where the key is of type String and the value is of
+ /// type <see cref="System.Json.JsonValue"/>, which can, in turn, be either a
+ /// primitive or a structured JSON type.
+ /// </summary>
+ Object,
+
+ /// <summary>
+ /// Specifies the JSON array CLR type that consists of an ordered collection of
+ /// <see cref="System.Json.JsonValue"/>types, which can, in turn, be either
+ /// primitive or structured JSON types.
+ /// </summary>
+ Array,
+
+ /// <summary>
+ /// Specifies the JSON Boolean CLR type.
+ /// </summary>
+ Boolean,
+
+ /// <summary>
+ /// Specifies the type returned by calls to <see cref="System.Json.JsonValue.ValueOrDefault(string)"/>
+ /// or <see cref="System.Json.JsonValue.ValueOrDefault(int)"/>
+ /// when the element searches doesn't exist in the JSON collection. This is a special
+ /// value which does not represent any JSON element, and cannot be added to any
+ /// JSON collections.
+ /// </summary>
+ Default
+ }
+}
diff --git a/src/System.Json/JsonValue.cs b/src/System.Json/JsonValue.cs
new file mode 100644
index 00000000..7c41d677
--- /dev/null
+++ b/src/System.Json/JsonValue.cs
@@ -0,0 +1,1254 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Dynamic;
+using System.Globalization;
+using System.IO;
+using System.Linq.Expressions;
+using System.Runtime.Serialization;
+using System.Runtime.Serialization.Json;
+using System.Text;
+using System.Xml;
+
+namespace System.Json
+{
+ /// <summary>
+ /// This is the base class for JavaScript Object Notation (JSON) common language runtime (CLR) types.
+ /// </summary>
+ [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix",
+ Justification = "JsonValue is by definition either a collection or a single object.")]
+ [DataContract]
+ public class JsonValue : IEnumerable<KeyValuePair<string, JsonValue>>, IDynamicMetaObjectProvider
+ {
+ private static object lockKey = new object();
+
+ // Double-checked locking pattern requires volatile for read/write synchronization
+ private static volatile JsonValue defaultInstance;
+ private int changingListenersCount;
+ private int changedListenersCount;
+
+ internal JsonValue()
+ {
+ }
+
+ /// <summary>
+ /// Raised when this <see cref="System.Json.JsonValue"/> or any of its members have changed.
+ /// </summary>
+ /// <remarks><p>Events are raised when elements are added or removed to <see cref="System.Json.JsonValue"/>
+ /// instances. It applies to both complex descendants of <see cref="System.Json.JsonValue"/>: <see cref="System.Json.JsonArray"/>
+ /// and <see cref="System.Json.JsonObject"/>.</p>
+ /// <p>You should be careful when modifying a <see cref="System.Json.JsonValue"/> tree within one of these events,
+ /// because doing this might lead to unexpected results. For example, if you receive a Changing event, and while
+ /// the event is being processed you remove the node from the tree, you might not receive the Changed event. When
+ /// an event is being processed, it is valid to modify a tree other than the one that contains the node that is
+ /// receiving the event; it is even valid to modify the same tree provided the modifications do not affect the
+ /// specific nodes on which the event was raised. However, if you modify the area of the tree that contains the
+ /// node receiving the event, the events that you receive and the impact to the tree are undefined.</p></remarks>
+ public event EventHandler<JsonValueChangeEventArgs> Changed
+ {
+ add
+ {
+ changedListenersCount++;
+ OnChanged += value;
+ }
+
+ remove
+ {
+ changedListenersCount--;
+ OnChanged -= value;
+ }
+ }
+
+ /// <summary>
+ /// Raised when this <see cref="System.Json.JsonValue"/> or any of its members are about to be changed.
+ /// </summary>
+ /// <remarks><p>Events are raised when elements are added or removed to <see cref="System.Json.JsonValue"/>
+ /// instances. It applies to both complex descendants of <see cref="System.Json.JsonValue"/>: <see cref="System.Json.JsonArray"/>
+ /// and <see cref="System.Json.JsonObject"/>.</p>
+ /// <p>You should be careful when modifying a <see cref="System.Json.JsonValue"/> tree within one of these events,
+ /// because doing this might lead to unexpected results. For example, if you receive a Changing event, and while
+ /// the event is being processed you remove the node from the tree, you might not receive the Changed event. When
+ /// an event is being processed, it is valid to modify a tree other than the one that contains the node that is
+ /// receiving the event; it is even valid to modify the same tree provided the modifications do not affect the
+ /// specific nodes on which the event was raised. However, if you modify the area of the tree that contains the
+ /// node receiving the event, the events that you receive and the impact to the tree are undefined.</p></remarks>
+ public event EventHandler<JsonValueChangeEventArgs> Changing
+ {
+ add
+ {
+ changingListenersCount++;
+ OnChanging += value;
+ }
+
+ remove
+ {
+ changingListenersCount--;
+ OnChanging -= value;
+ }
+ }
+
+ private event EventHandler<JsonValueChangeEventArgs> OnChanged;
+ private event EventHandler<JsonValueChangeEventArgs> OnChanging;
+
+ /// <summary>
+ /// Gets the JSON CLR type represented by this instance.
+ /// </summary>
+ public virtual JsonType JsonType
+ {
+ get { return JsonType.Default; }
+ }
+
+ /// <summary>
+ /// Gets the number of items in this object.
+ /// </summary>
+ public virtual int Count
+ {
+ get { return 0; }
+ }
+
+ /// <summary>
+ /// Gets the number of listeners to the <see cref="Changing"/> event for this instance.
+ /// </summary>
+ protected int ChangingListenersCount
+ {
+ get { return changingListenersCount; }
+ }
+
+ /// <summary>
+ /// Gets the number of listeners to the <see cref="Changed"/> event for this instance.
+ /// </summary>
+ protected int ChangedListenersCount
+ {
+ get { return changedListenersCount; }
+ }
+
+ /// <summary>
+ /// Gets the default JsonValue instance.
+ /// This instance enables safe-chaining of JsonValue operations and resolves to 'null'
+ /// when this instance is used as dynamic, mapping to the JavaScript 'null' value.
+ /// </summary>
+ private static JsonValue DefaultInstance
+ {
+ get
+ {
+ if (defaultInstance == null)
+ {
+ lock (lockKey)
+ {
+ if (defaultInstance == null)
+ {
+ defaultInstance = new JsonValue();
+ }
+ }
+ }
+
+ return defaultInstance;
+ }
+ }
+
+ /// <summary>
+ /// This indexer is not supported for this base class and throws an exception.
+ /// </summary>
+ /// <param name="key">The key of the element to get or set.</param>
+ /// <returns><see cref="System.Json.JsonValue"/>.</returns>
+ /// <remarks>The exception thrown is the <see cref="System.InvalidOperationException"/>.
+ /// This method is overloaded in the implementation of the <see cref="System.Json.JsonObject"/>
+ /// class, which inherits from this class.</remarks>
+ public virtual JsonValue this[string key]
+ {
+ get { throw new InvalidOperationException(RS.Format(Properties.Resources.IndexerNotSupportedOnJsonType, typeof(string), JsonType)); }
+
+ set { throw new InvalidOperationException(RS.Format(Properties.Resources.IndexerNotSupportedOnJsonType, typeof(string), JsonType)); }
+ }
+
+ /// <summary>
+ /// This indexer is not supported for this base class and throws an exception.
+ /// </summary>
+ /// <param name="index">The zero-based index of the element to get or set.</param>
+ /// <returns><see cref="System.Json.JsonValue"/>.</returns>
+ /// <remarks>The exception thrown is the <see cref="System.InvalidOperationException"/>.
+ /// This method is overloaded in the implementation of the <see cref="System.Json.JsonArray"/>
+ /// class, which inherits from this class.</remarks>
+ public virtual JsonValue this[int index]
+ {
+ get { throw new InvalidOperationException(RS.Format(Properties.Resources.IndexerNotSupportedOnJsonType, typeof(int), JsonType)); }
+
+ set { throw new InvalidOperationException(RS.Format(Properties.Resources.IndexerNotSupportedOnJsonType, typeof(int), JsonType)); }
+ }
+
+ /// <summary>
+ /// Deserializes text-based JSON into a JSON CLR type.
+ /// </summary>
+ /// <param name="json">The text-based JSON to be parsed into a JSON CLR type.</param>
+ /// <returns>The <see cref="System.Json.JsonValue"/> object that represents the parsed
+ /// text-based JSON as a CLR type.</returns>
+ /// <exception cref="System.ArgumentException">The length of jsonString is zero.</exception>
+ /// <exception cref="System.ArgumentNullException">jsonString is null.</exception>
+ /// <remarks>The result will be an instance of either <see cref="System.Json.JsonArray"/>,
+ /// <see cref="System.Json.JsonObject"/> or <see cref="System.Json.JsonPrimitive"/>,
+ /// depending on the text-based JSON supplied to the method.</remarks>
+ public static JsonValue Parse(string json)
+ {
+ return JXmlToJsonValueConverter.JXMLToJsonValue(json);
+ }
+
+ /// <summary>
+ /// Deserializes text-based JSON from a text reader into a JSON CLR type.
+ /// </summary>
+ /// <param name="textReader">A <see cref="System.IO.TextReader"/> over text-based JSON content.</param>
+ /// <returns>The <see cref="System.Json.JsonValue"/> object that represents the parsed
+ /// text-based JSON as a CLR type.</returns>
+ /// <exception cref="System.ArgumentNullException">textReader is null.</exception>
+ /// <remarks>The result will be an instance of either <see cref="System.Json.JsonArray"/>,
+ /// <see cref="System.Json.JsonObject"/> or <see cref="System.Json.JsonPrimitive"/>,
+ /// depending on the text-based JSON supplied to the method.</remarks>
+ public static JsonValue Load(TextReader textReader)
+ {
+ if (textReader == null)
+ {
+ throw new ArgumentNullException("textReader");
+ }
+
+ return JsonValue.Parse(textReader.ReadToEnd());
+ }
+
+ /// <summary>
+ /// Deserializes text-based JSON from a stream into a JSON CLR type.
+ /// </summary>
+ /// <param name="stream">A <see cref="System.IO.Stream"/> that contains text-based JSON content.</param>
+ /// <returns>The <see cref="System.Json.JsonValue"/> object that represents the parsed
+ /// text-based JSON as a CLR type.</returns>
+ /// <exception cref="System.ArgumentNullException">stream is null.</exception>
+ /// <remarks>The result will be an instance of either <see cref="System.Json.JsonArray"/>,
+ /// <see cref="System.Json.JsonObject"/> or <see cref="System.Json.JsonPrimitive"/>,
+ /// depending on the text-based JSON supplied to the method.</remarks>
+ public static JsonValue Load(Stream stream)
+ {
+ return JXmlToJsonValueConverter.JXMLToJsonValue(stream);
+ }
+
+ /// <summary>
+ /// Performs a cast operation from a <see cref="JsonValue"/> instance into the specified type parameter./>
+ /// </summary>
+ /// <typeparam name="T">The type to cast the instance to.</typeparam>
+ /// <param name="value">The <see cref="System.Json.JsonValue"/> instance.</param>
+ /// <returns>An object of type T initialized with the <see cref="System.Json.JsonValue"/> value specified.</returns>
+ /// <remarks>This method is to support the framework and is not intended to be used externally, use explicit type cast instead.</remarks>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter",
+ Justification = "The generic parameter is used to specify the output type")]
+ public static T CastValue<T>(JsonValue value)
+ {
+ Type typeofT = typeof(T);
+
+ if ((value != null && typeofT.IsAssignableFrom(value.GetType())) || typeofT == typeof(object))
+ {
+ return (T)(object)value;
+ }
+
+ if (value == null || value.JsonType == JsonType.Default)
+ {
+ if (typeofT.IsValueType)
+ {
+ throw new InvalidCastException(RS.Format(Properties.Resources.InvalidCastNonNullable, typeofT.FullName));
+ }
+ else
+ {
+ return default(T);
+ }
+ }
+
+ try
+ {
+ return value.ReadAs<T>();
+ }
+ catch (Exception ex)
+ {
+ if (ex is FormatException || ex is NotSupportedException || ex is InvalidCastException)
+ {
+ throw new InvalidCastException(RS.Format(Properties.Resources.CannotCastJsonValue, value.GetType().FullName, typeofT.FullName), ex);
+ }
+
+ throw;
+ }
+ }
+
+ /// <summary>
+ /// Returns an enumerator which iterates through the values in this object.
+ /// </summary>
+ /// <returns>An enumerator which which iterates through the values in this object.</returns>
+ /// <remarks>The enumerator returned by this class is empty; subclasses will override this method to return appropriate enumerators for themselves.</remarks>
+ public IEnumerator<KeyValuePair<string, JsonValue>> GetEnumerator()
+ {
+ return GetKeyValuePairEnumerator();
+ }
+
+ /// <summary>
+ /// Returns an enumerator which iterates through the values in this object.
+ /// </summary>
+ /// <returns>An <see cref="System.Collections.IEnumerator"/> which iterates through the values in this object.</returns>
+ /// <remarks>The enumerator returned by this class is empty; subclasses will override this method to return appropriate enumerators for themselves.</remarks>
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetKeyValuePairEnumerator();
+ }
+
+ /// <summary>
+ /// Gets this instance as a <code>dynamic</code> object.
+ /// </summary>
+ /// <returns>This instance as <code>dynamic</code>.</returns>
+ public dynamic AsDynamic()
+ {
+ return this;
+ }
+
+ /// <summary>
+ /// Attempts to convert this <see cref="System.Json.JsonValue"/> instance into the type T.
+ /// </summary>
+ /// <typeparam name="T">The type to which the conversion is being performed.</typeparam>
+ /// <param name="valueOfT">An instance of T initialized with this instance, or the default value of T if the conversion cannot be performed.</param>
+ /// <returns>true if this <see cref="System.Json.JsonValue"/> instance can be read as type T; otherwise, false.</returns>
+ public bool TryReadAs<T>(out T valueOfT)
+ {
+ object value;
+ if (TryReadAs(typeof(T), out value))
+ {
+ valueOfT = (T)value;
+ return true;
+ }
+
+ valueOfT = default(T);
+ return false;
+ }
+
+ /// <summary>
+ /// Attempts to convert this <see cref="System.Json.JsonValue"/> instance into the type T.
+ /// </summary>
+ /// <typeparam name="T">The type to which the conversion is being performed.</typeparam>
+ /// <returns>An instance of T initialized with the value from the conversion of this instance.</returns>
+ /// <exception cref="System.NotSupportedException">If this <see cref="System.Json.JsonValue"/> value cannot be converted into the type T.</exception>
+ [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter",
+ Justification = "The generic parameter is used to specify the output type")]
+ public T ReadAs<T>()
+ {
+ return (T)ReadAs(typeof(T));
+ }
+
+ /// <summary>
+ /// Attempts to convert this <see cref="System.Json.JsonValue"/> instance into the type T.
+ /// </summary>
+ /// <typeparam name="T">The type to which the conversion is being performed.</typeparam>
+ /// <param name="fallback">The fallback value to be returned if the conversion cannot be made.</param>
+ /// <returns>An instance of T initialized with the value from the conversion of this instance, or the specified fallback value if the conversion cannot be made.</returns>
+ public T ReadAs<T>(T fallback)
+ {
+ return (T)ReadAs(typeof(T), fallback);
+ }
+
+ /// <summary>
+ /// Attempts to convert this <see cref="System.Json.JsonValue"/> instance to an instance of the specified type.
+ /// </summary>
+ /// <param name="type">The type to which the conversion is being performed.</param>
+ /// <param name="fallback">The fallback value to be returned if the conversion cannot be made.</param>
+ /// <returns>An instance of the specified type initialized with the value from the conversion of this instance, or the specified fallback value if the conversion cannot be made.</returns>
+ public object ReadAs(Type type, object fallback)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+
+ object result;
+ if (JsonType != JsonType.Default && TryReadAs(type, out result))
+ {
+ return result;
+ }
+ else
+ {
+ return fallback;
+ }
+ }
+
+ /// <summary>
+ /// Attempts to convert this <see cref="System.Json.JsonValue"/> instance into an instance of the specified type.
+ /// </summary>
+ /// <param name="type">The type to which the conversion is being performed.</param>
+ /// <returns>An instance of the specified type initialized with the value from the conversion of this instance.</returns>
+ /// <exception cref="System.NotSupportedException">If this <see cref="System.Json.JsonValue"/> value cannot be converted into the type T.</exception>
+ public virtual object ReadAs(Type type)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+
+ object result;
+ if (TryReadAs(type, out result))
+ {
+ return result;
+ }
+
+ throw new NotSupportedException(RS.Format(Properties.Resources.CannotReadAsType, GetType().FullName, type.FullName));
+ }
+
+ /// <summary>
+ /// Attempts to convert this <see cref="System.Json.JsonValue"/> instance into an instance of the specified type.
+ /// </summary>
+ /// <param name="type">The type to which the conversion is being performed.</param>
+ /// <param name="value">An object to be initialized with this instance or null if the conversion cannot be performed.</param>
+ /// <returns>true if this <see cref="System.Json.JsonValue"/> instance can be read as the specified type; otherwise, false.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1007:UseGenericsWhereAppropriate",
+ Justification = "This is the non-generic version of the method.")]
+ public virtual bool TryReadAs(Type type, out object value)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+
+ if (type.IsAssignableFrom(GetType()) || type == typeof(object))
+ {
+ value = this;
+ return true;
+ }
+
+ value = null;
+ return false;
+ }
+
+ /// <summary>
+ /// Writes this <see cref="System.Json.JsonValue"/> instance to a <see cref="System.IO.Stream"/>.
+ /// </summary>
+ /// <param name="stream">Stream to which to write text-based JSON.</param>
+ public void Save(Stream stream)
+ {
+ if (JsonType == JsonType.Default)
+ {
+ throw new InvalidOperationException(Properties.Resources.UseOfDefaultNotAllowed);
+ }
+
+ if (stream == null)
+ {
+ throw new ArgumentNullException("stream");
+ }
+
+ using (XmlDictionaryWriter jsonWriter = JsonReaderWriterFactory.CreateJsonWriter(stream, Encoding.UTF8, false))
+ {
+ jsonWriter.WriteStartElement(JXmlToJsonValueConverter.RootElementName);
+ Save(jsonWriter);
+ jsonWriter.WriteEndElement();
+ }
+ }
+
+ /// <summary>
+ /// Writes <see cref="System.Json.JsonValue"/> instance to a <see cref="TextWriter"/>.
+ /// </summary>
+ /// <param name="textWriter">The <see cref="System.IO.TextWriter"/> used to write text-based JSON.</param>
+ public void Save(TextWriter textWriter)
+ {
+ if (JsonType == JsonType.Default)
+ {
+ throw new InvalidOperationException(Properties.Resources.UseOfDefaultNotAllowed);
+ }
+
+ if (textWriter == null)
+ {
+ throw new ArgumentNullException("textWriter");
+ }
+
+ using (MemoryStream ms = new MemoryStream())
+ {
+ Save(ms);
+ ms.Position = 0;
+ textWriter.Write(new StreamReader(ms).ReadToEnd());
+ }
+ }
+
+ /// <summary>
+ /// Provides a textual representation of this <see cref="System.Json.JsonValue"/> instance.
+ /// </summary>
+ /// <returns>A <see cref="System.String"/> containing text-based JSON.</returns>
+ public override string ToString()
+ {
+ if (JsonType == JsonType.Default)
+ {
+ return "Default";
+ }
+
+ using (MemoryStream ms = new MemoryStream())
+ {
+ Save(ms);
+ ms.Position = 0;
+ return new StreamReader(ms).ReadToEnd();
+ }
+ }
+
+ /// <summary>
+ /// Checks whether a key/value pair with a specified key exists in the JSON CLR object type.
+ /// </summary>
+ /// <param name="key">The key to check for.</param>
+ /// <returns>false in this class; subclasses may override this method to return other values.</returns>
+ /// <remarks>This method is overloaded in the implementation of the <see cref="System.Json.JsonObject"/>
+ /// class, which inherits from this class.</remarks>
+ public virtual bool ContainsKey(string key)
+ {
+ return false;
+ }
+
+ /// <summary>
+ /// Returns the value returned by the safe string indexer for this instance.
+ /// </summary>
+ /// <param name="key">The key of the element to get.</param>
+ /// <returns>If this is an instance of <see cref="System.Json.JsonObject"/>, it contains
+ /// the given key and the value corresponding to the key is not null, then it will return that value.
+ /// Otherwise it will return a <see cref="System.Json.JsonValue"/> instance with <see cref="System.Json.JsonValue.JsonType"/>
+ /// equals to <see cref="F:System.Json.JsonType.Default"/>.</returns>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public virtual JsonValue GetValue(string key)
+ {
+ return ValueOrDefault(key);
+ }
+
+ /// <summary>
+ /// Returns the value returned by the safe int indexer for this instance.
+ /// </summary>
+ /// <param name="index">The zero-based index of the element to get.</param>
+ /// <returns>If this is an instance of <see cref="System.Json.JsonArray"/>, the index is within the array
+ /// bounds, and the value corresponding to the index is not null, then it will return that value.
+ /// Otherwise it will return a <see cref="System.Json.JsonValue"/> instance with <see cref="System.Json.JsonValue.JsonType"/>
+ /// equals to <see cref="F:System.Json.JsonType.Default"/>.</returns>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public virtual JsonValue GetValue(int index)
+ {
+ return ValueOrDefault(index);
+ }
+
+ /// <summary>
+ /// Sets the value and returns it.
+ /// </summary>
+ /// <param name="key">The key of the element to set.</param>
+ /// <param name="value">The value to be set.</param>
+ /// <returns>The value, converted into a JsonValue, set in this collection.</returns>
+ /// <exception cref="System.ArgumentException">If the value cannot be converted into a JsonValue.</exception>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public virtual JsonValue SetValue(string key, object value)
+ {
+ this[key] = ResolveObject(value);
+ return this[key];
+ }
+
+ /// <summary>
+ /// Sets the value and returns it.
+ /// </summary>
+ /// <param name="index">The zero-based index of the element to set.</param>
+ /// <param name="value">The value to be set.</param>
+ /// <returns>The value, converted into a JsonValue, set in this collection.</returns>
+ /// <exception cref="System.ArgumentException">If the value cannot be converted into a JsonValue.</exception>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public virtual JsonValue SetValue(int index, object value)
+ {
+ this[index] = ResolveObject(value);
+ return this[index];
+ }
+
+ /// <summary>
+ /// Safe string indexer for the <see cref="System.Json.JsonValue"/> type.
+ /// </summary>
+ /// <param name="key">The key of the element to get.</param>
+ /// <returns>If this is an instance of <see cref="System.Json.JsonObject"/>, it contains
+ /// the given key and the value corresponding to the key is not null, then it will return that value.
+ /// Otherwise it will return a <see cref="System.Json.JsonValue"/> instance with <see cref="System.Json.JsonValue.JsonType"/>
+ /// equals to <see cref="F:System.Json.JsonType.Default"/>.</returns>
+ public virtual JsonValue ValueOrDefault(string key)
+ {
+ return JsonValue.DefaultInstance;
+ }
+
+ /// <summary>
+ /// Safe indexer for the <see cref="System.Json.JsonValue"/> type.
+ /// </summary>
+ /// <param name="index">The zero-based index of the element to get.</param>
+ /// <returns>If this is an instance of <see cref="System.Json.JsonArray"/>, the index is within the array
+ /// bounds, and the value corresponding to the index is not null, then it will return that value.
+ /// Otherwise it will return a <see cref="System.Json.JsonValue"/> instance with <see cref="System.Json.JsonValue.JsonType"/>
+ /// equals to <see cref="F:System.Json.JsonType.Default"/>.</returns>
+ public virtual JsonValue ValueOrDefault(int index)
+ {
+ return JsonValue.DefaultInstance;
+ }
+
+ /// <summary>
+ /// Safe deep indexer for the <see cref="JsonValue"/> type.
+ /// </summary>
+ /// <param name="indexes">The indices to index this type. The indices can be
+ /// of type <see cref="System.Int32"/> or <see cref="System.String"/>.</param>
+ /// <returns>A <see cref="JsonValue"/> which is equivalent to calling<see cref="ValueOrDefault(int)"/> or
+ /// <see cref="ValueOrDefault(string)"/> on the first index, then calling it again on the result
+ /// for the second index and so on.</returns>
+ /// <exception cref="System.ArgumentException">If any of the indices is not of type
+ /// <see cref="System.Int32"/> or <see cref="System.String"/>.</exception>
+ public JsonValue ValueOrDefault(params object[] indexes)
+ {
+ if (indexes == null)
+ {
+ return JsonValue.DefaultInstance;
+ }
+
+ if (indexes.Length == 0)
+ {
+ return this;
+ }
+
+ JsonValue result = this;
+ for (int i = 0; i < indexes.Length; i++)
+ {
+ object index = indexes[i];
+ if (index == null)
+ {
+ result = JsonValue.DefaultInstance;
+ continue;
+ }
+
+ Type indexType = index.GetType();
+
+ switch (Type.GetTypeCode(indexType))
+ {
+ case TypeCode.Char:
+ case TypeCode.Int16:
+ case TypeCode.UInt16:
+ case TypeCode.Byte:
+ case TypeCode.SByte:
+ index = System.Convert.ChangeType(index, typeof(int), CultureInfo.InvariantCulture);
+ goto case TypeCode.Int32;
+
+ case TypeCode.Int32:
+ result = result.ValueOrDefault((int)index);
+ break;
+
+ case TypeCode.String:
+ result = result.ValueOrDefault((string)index);
+ break;
+
+ default:
+ throw new ArgumentException(RS.Format(Properties.Resources.InvalidIndexType, index.GetType()), "indexes");
+ }
+ }
+
+ return result;
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes",
+ Justification = "Cannot make this class sealed, it needs to have subclasses. But its subclasses are sealed themselves.")]
+ DynamicMetaObject IDynamicMetaObjectProvider.GetMetaObject(Expression parameter)
+ {
+ if (parameter == null)
+ {
+ throw new ArgumentNullException("parameter");
+ }
+
+ return new JsonValueDynamicMetaObject(parameter, this);
+ }
+
+ /// <summary>
+ /// Resolves the specified object to an appropriate JsonValue instance.
+ /// </summary>
+ /// <param name="value">The object to resolve.</param>
+ /// <returns>A <see cref="JsonValue"/> instance resolved from the specified object.</returns>
+ internal static JsonValue ResolveObject(object value)
+ {
+ JsonPrimitive primitive;
+
+ if (value == null)
+ {
+ return null;
+ }
+
+ JsonValue jsonValue = value as JsonValue;
+
+ if (jsonValue != null)
+ {
+ return jsonValue;
+ }
+
+ if (JsonPrimitive.TryCreate(value, out primitive))
+ {
+ return primitive;
+ }
+
+ throw new ArgumentException(Properties.Resources.TypeNotSupported, "value");
+ }
+
+ /// <summary>
+ /// Determines whether an explicit cast to JsonValue is provided from the specified type.
+ /// </summary>
+ /// <param name="type">The type to check.</param>
+ /// <returns>true if an explicit cast exists for the specified type, false otherwise.</returns>
+ internal static bool IsSupportedExplicitCastType(Type type)
+ {
+ TypeCode typeCode = Type.GetTypeCode(type);
+
+ switch (typeCode)
+ {
+ case TypeCode.Boolean:
+ case TypeCode.Byte:
+ case TypeCode.Char:
+ case TypeCode.DateTime:
+ case TypeCode.Decimal:
+ case TypeCode.Double:
+ case TypeCode.Int16:
+ case TypeCode.Int32:
+ case TypeCode.Int64:
+ case TypeCode.SByte:
+ case TypeCode.Single:
+ case TypeCode.String:
+ case TypeCode.UInt16:
+ case TypeCode.UInt32:
+ case TypeCode.UInt64:
+ return true;
+
+ default:
+ return type == typeof(DateTimeOffset) || type == typeof(Guid) || type == typeof(Uri) ||
+ type == typeof(List<object>) || type == typeof(Array) || type == typeof(object[]) ||
+ type == typeof(Dictionary<string, object>);
+ }
+ }
+
+ /// <summary>
+ /// Returns the value this object wraps (if any).
+ /// </summary>
+ /// <returns>The value wrapped by this instance or null if none.</returns>
+ internal virtual object Read()
+ {
+ return null;
+ }
+
+ /// <summary>
+ /// Serializes this object into the specified <see cref="XmlDictionaryWriter"/> instance.
+ /// </summary>
+ /// <param name="jsonWriter">An <see cref="XmlDictionaryWriter"/> instance to serialize this instance into.</param>
+ internal virtual void Save(XmlDictionaryWriter jsonWriter)
+ {
+ if (jsonWriter == null)
+ {
+ throw new ArgumentNullException("jsonWriter");
+ }
+
+ Stack<JsonValue> objectStack = new Stack<JsonValue>();
+ Stack<int> indexStack = new Stack<int>();
+ int currentIndex = 0;
+ JsonValue currentValue = this;
+
+ OnSaveStarted();
+
+ WriteAttributeString(jsonWriter);
+
+ while (currentIndex < currentValue.Count || objectStack.Count > 0)
+ {
+ if (currentValue.Count > currentIndex)
+ {
+ JsonValue nextValue = currentValue.WriteStartElementAndGetNext(jsonWriter, currentIndex);
+
+ if (JsonValue.IsJsonCollection(nextValue))
+ {
+ nextValue.OnSaveStarted();
+ nextValue.WriteAttributeString(jsonWriter);
+
+ objectStack.Push(currentValue);
+ indexStack.Push(currentIndex);
+ currentValue = nextValue;
+ currentIndex = 0;
+ }
+ else
+ {
+ if (nextValue == null)
+ {
+ jsonWriter.WriteAttributeString(JXmlToJsonValueConverter.TypeAttributeName, JXmlToJsonValueConverter.NullAttributeValue);
+ }
+ else
+ {
+ nextValue.Save(jsonWriter);
+ }
+
+ currentIndex++;
+ jsonWriter.WriteEndElement();
+ }
+ }
+ else
+ {
+ if (objectStack.Count > 0)
+ {
+ currentValue.OnSaveEnded();
+ jsonWriter.WriteEndElement();
+
+ currentValue = objectStack.Pop();
+ currentIndex = indexStack.Pop() + 1;
+ }
+ }
+ }
+
+ OnSaveEnded();
+ }
+
+ /// <summary>
+ /// Returns an enumerator which iterates through the values in this object.
+ /// </summary>
+ /// <returns>An <see cref="System.Collections.IEnumerator"/> which iterates through the values in this object.</returns>
+ /// <remarks>This method is the virtual version of the IEnumerator.GetEnumerator method and is provided to allow derived classes to implement the
+ /// appropriate version of the generic interface (enumerator of values or key/value pairs).</remarks>
+ [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate",
+ Justification = "This method is a virtual version of the IEnumerable.GetEnumerator method.")]
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures",
+ Justification = "This class is a collection that is properly represented by the nested generic type.")]
+ protected virtual IEnumerator<KeyValuePair<string, JsonValue>> GetKeyValuePairEnumerator()
+ {
+ yield break;
+ }
+
+ /// <summary>
+ /// Callback method called during Save operations to let the instance write the start element
+ /// and return the next element in the collection.
+ /// </summary>
+ /// <param name="jsonWriter">The JXML writer used to write JSON.</param>
+ /// <param name="index">The index within this collection.</param>
+ /// <returns>The next item in the collection, or null of there are no more items.</returns>
+ internal virtual JsonValue WriteStartElementAndGetNext(XmlDictionaryWriter jsonWriter, int index)
+ {
+ return null;
+ }
+
+ /// <summary>
+ /// Callback method called to let an instance write the proper JXML attribute when saving this
+ /// instance.
+ /// </summary>
+ /// <param name="jsonWriter">The JXML writer used to write JSON.</param>
+ internal virtual void WriteAttributeString(XmlDictionaryWriter jsonWriter)
+ {
+ }
+
+ /// <summary>
+ /// Callback method called when a Save operation is starting for this instance.
+ /// </summary>
+ protected virtual void OnSaveStarted()
+ {
+ }
+
+ /// <summary>
+ /// Callback method called when a Save operation is finished for this instance.
+ /// </summary>
+ protected virtual void OnSaveEnded()
+ {
+ }
+
+ /// <summary>
+ /// Called internally to raise the <see cref="Changing"/> event.
+ /// </summary>
+ /// <param name="sender">The object which caused the event to be raised.</param>
+ /// <param name="eventArgs">The arguments to the event.</param>
+ [SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate",
+ Justification = "This is a helper function used to raise the event.")]
+ [SuppressMessage("Microsoft.Security", "CA2109:ReviewVisibleEventHandlers",
+ Justification = "This is not externally visible, since the constructor for this class is internal (cannot be directly derived) and all its subclasses are sealed.")]
+ protected void RaiseChangingEvent(object sender, JsonValueChangeEventArgs eventArgs)
+ {
+ EventHandler<JsonValueChangeEventArgs> changing = OnChanging;
+ if (changing != null)
+ {
+ changing(sender, eventArgs);
+ }
+ }
+
+ /// <summary>
+ /// Called internally to raise the <see cref="Changed"/> event.
+ /// </summary>
+ /// <param name="sender">The object which caused the event to be raised.</param>
+ /// <param name="eventArgs">The arguments to the event.</param>
+ [SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate",
+ Justification = "This is a helper function used to raise the event.")]
+ [SuppressMessage("Microsoft.Security", "CA2109:ReviewVisibleEventHandlers",
+ Justification = "This is not externally visible, since the constructor for this class is internal (cannot be directly derived) and all its subclasses are sealed.")]
+ protected void RaiseChangedEvent(object sender, JsonValueChangeEventArgs eventArgs)
+ {
+ EventHandler<JsonValueChangeEventArgs> changed = OnChanged;
+ if (changed != null)
+ {
+ changed(sender, eventArgs);
+ }
+ }
+
+ private static bool IsJsonCollection(JsonValue value)
+ {
+ return value != null && (value.JsonType == JsonType.Array || value.JsonType == JsonType.Object);
+ }
+
+ /// <summary>
+ /// Enables explicit casts from an instance of type <see cref="System.Json.JsonValue"/> to a <see cref="System.String"/> object.
+ /// </summary>
+ /// <param name="value">The instance of <see cref="System.Json.JsonValue"/> used to initialize the <see cref="System.String"/> object.</param>
+ /// <returns>The <see cref="System.String"/> initialized with the <see cref="System.Json.JsonValue"/> value specified or null if value is null.</returns>
+ public static explicit operator string(JsonValue value)
+ {
+ return CastValue<string>(value);
+ }
+
+ /// <summary>
+ /// Enables explicit casts from an instance of type <see cref="System.Json.JsonValue"/> to a <see cref="System.Double"/> object.
+ /// </summary>
+ /// <param name="value">The instance of <see cref="System.Json.JsonValue"/> used to initialize the <see cref="System.Double"/> object.</param>
+ /// <returns>The <see cref="System.Double"/> initialized with the <see cref="System.Json.JsonValue"/> value specified.</returns>
+ public static explicit operator double(JsonValue value)
+ {
+ return CastValue<double>(value);
+ }
+
+ /// <summary>
+ /// Enables explicit casts from an instance of type <see cref="System.Json.JsonValue"/> to a <see cref="System.Single"/> object.
+ /// </summary>
+ /// <param name="value">The instance of <see cref="System.Json.JsonValue"/> used to initialize the <see cref="System.Single"/> object.</param>
+ /// <returns>The <see cref="System.Single"/> initialized with the <see cref="System.Json.JsonValue"/> value specified.</returns>
+ public static explicit operator float(JsonValue value)
+ {
+ return CastValue<float>(value);
+ }
+
+ /// <summary>
+ /// Enables explicit casts from an instance of type <see cref="System.Json.JsonValue"/> to a <see cref="System.Decimal"/> object.
+ /// </summary>
+ /// <param name="value">The instance of <see cref="System.Json.JsonValue"/> used to initialize the <see cref="System.Decimal"/> object.</param>
+ /// <returns>The <see cref="System.Decimal"/> initialized with the <see cref="System.Json.JsonValue"/> value specified.</returns>
+ public static explicit operator decimal(JsonValue value)
+ {
+ return CastValue<decimal>(value);
+ }
+
+ /// <summary>
+ /// Enables explicit casts from an instance of type <see cref="System.Json.JsonValue"/> to a <see cref="System.Int64"/> object.
+ /// </summary>
+ /// <param name="value">The instance of <see cref="System.Json.JsonValue"/> used to initialize the <see cref="System.Int64"/> object.</param>
+ /// <returns>The <see cref="System.Int64"/> initialized with the <see cref="System.Json.JsonValue"/> value specified.</returns>
+ public static explicit operator long(JsonValue value)
+ {
+ return CastValue<long>(value);
+ }
+
+ /// <summary>
+ /// Enables explicit casts from an instance of type <see cref="System.Json.JsonValue"/> to a <see cref="System.UInt64"/> object.
+ /// </summary>
+ /// <param name="value">The instance of <see cref="System.Json.JsonValue"/> used to initialize the <see cref="System.UInt64"/> object.</param>
+ /// <returns>The <see cref="System.UInt64"/> initialized with the <see cref="System.Json.JsonValue"/> value specified.</returns>
+ [CLSCompliant(false)]
+ public static explicit operator ulong(JsonValue value)
+ {
+ return CastValue<ulong>(value);
+ }
+
+ /// <summary>
+ /// Enables explicit casts from an instance of type <see cref="System.Json.JsonValue"/> to a <see cref="System.Int32"/> object.
+ /// </summary>
+ /// <param name="value">The instance of <see cref="System.Json.JsonValue"/> used to initialize the <see cref="System.Int32"/> object.</param>
+ /// <returns>The <see cref="System.Int32"/> initialized with the <see cref="System.Json.JsonValue"/> value specified.</returns>
+ public static explicit operator int(JsonValue value)
+ {
+ return CastValue<int>(value);
+ }
+
+ /// <summary>
+ /// Enables explicit casts from an instance of type <see cref="System.Json.JsonValue"/> to a <see cref="System.UInt32"/> object.
+ /// </summary>
+ /// <param name="value">The instance of <see cref="System.Json.JsonValue"/> used to initialize the <see cref="System.UInt32"/> object.</param>
+ /// <returns>The <see cref="System.UInt32"/> initialized with the <see cref="System.Json.JsonValue"/> value specified.</returns>
+ [CLSCompliant(false)]
+ public static explicit operator uint(JsonValue value)
+ {
+ return CastValue<uint>(value);
+ }
+
+ /// <summary>
+ /// Enables explicit casts from an instance of type <see cref="System.Json.JsonValue"/> to a <see cref="System.Int16"/> object.
+ /// </summary>
+ /// <param name="value">The instance of <see cref="System.Json.JsonValue"/> used to initialize the <see cref="System.Int16"/> object.</param>
+ /// <returns>The <see cref="System.Int16"/> initialized with the <see cref="System.Json.JsonValue"/> value specified.</returns>
+ public static explicit operator short(JsonValue value)
+ {
+ return CastValue<short>(value);
+ }
+
+ /// <summary>
+ /// Enables explicit casts from an instance of type <see cref="System.Json.JsonValue"/> to a <see cref="System.UInt16"/> object.
+ /// </summary>
+ /// <param name="value">The instance of <see cref="System.Json.JsonValue"/> used to initialize the <see cref="System.UInt16"/> object.</param>
+ /// <returns>The <see cref="System.UInt16"/> initialized with the <see cref="System.Json.JsonValue"/> value specified.</returns>
+ [CLSCompliant(false)]
+ public static explicit operator ushort(JsonValue value)
+ {
+ return CastValue<ushort>(value);
+ }
+
+ /// <summary>
+ /// Enables explicit casts from an instance of type <see cref="System.Json.JsonValue"/> to a <see cref="System.SByte"/> object.
+ /// </summary>
+ /// <param name="value">The instance of <see cref="System.Json.JsonValue"/> used to initialize the <see cref="System.SByte"/> object.</param>
+ /// <returns>The <see cref="System.SByte"/> initialized with the <see cref="System.Json.JsonValue"/> value specified.</returns>
+ [CLSCompliant(false)]
+ public static explicit operator sbyte(JsonValue value)
+ {
+ return CastValue<sbyte>(value);
+ }
+
+ /// <summary>
+ /// Enables explicit casts from an instance of type <see cref="System.Json.JsonValue"/> to a <see cref="System.Byte"/> object.
+ /// </summary>
+ /// <param name="value">The instance of <see cref="System.Json.JsonValue"/> used to initialize the <see cref="System.Byte"/> object.</param>
+ /// <returns>The <see cref="System.Byte"/> initialized with the <see cref="System.Json.JsonValue"/> value specified.</returns>
+ public static explicit operator byte(JsonValue value)
+ {
+ return CastValue<byte>(value);
+ }
+
+ /// <summary>
+ /// Enables explicit casts from an instance of type <see cref="System.Json.JsonValue"/> to a <see cref="System.Uri"/> object.
+ /// </summary>
+ /// <param name="value">The instance of <see cref="System.Json.JsonValue"/> used to initialize the <see cref="System.Uri"/> object.</param>
+ /// <returns>The <see cref="System.Uri"/> initialized with the <see cref="System.Json.JsonValue"/> value specified or null if value is null.</returns>
+ public static explicit operator Uri(JsonValue value)
+ {
+ return CastValue<Uri>(value);
+ }
+
+ /// <summary>
+ /// Enables explicit casts from an instance of type <see cref="System.Json.JsonValue"/> to a <see cref="System.Guid"/> object.
+ /// </summary>
+ /// <param name="value">The instance of <see cref="System.Json.JsonValue"/> used to initialize the <see cref="System.Guid"/> object.</param>
+ /// <returns>The <see cref="System.Guid"/> initialized with the <see cref="System.Json.JsonValue"/> value specified.</returns>
+ public static explicit operator Guid(JsonValue value)
+ {
+ return CastValue<Guid>(value);
+ }
+
+ /// <summary>
+ /// Enables explicit casts from an instance of type <see cref="System.Json.JsonValue"/> to a <see cref="System.DateTime"/> object.
+ /// </summary>
+ /// <param name="value">The instance of <see cref="System.Json.JsonValue"/> used to initialize the <see cref="System.DateTime"/> object.</param>
+ /// <returns>The <see cref="System.DateTime"/> initialized with the <see cref="System.Json.JsonValue"/> value specified.</returns>
+ public static explicit operator DateTime(JsonValue value)
+ {
+ return CastValue<DateTime>(value);
+ }
+
+ /// <summary>
+ /// Enables explicit casts from an instance of type <see cref="System.Json.JsonValue"/> to a <see cref="System.Char"/> object.
+ /// </summary>
+ /// <param name="value">The instance of <see cref="System.Json.JsonValue"/> used to initialize the <see cref="System.Char"/> object.</param>
+ /// <returns>The <see cref="System.Char"/> initialized with the <see cref="System.Json.JsonValue"/> value specified.</returns>
+ public static explicit operator char(JsonValue value)
+ {
+ return CastValue<char>(value);
+ }
+
+ /// <summary>
+ /// Enables explicit casts from an instance of type <see cref="System.Json.JsonValue"/> to a <see cref="System.Boolean"/> object.
+ /// </summary>
+ /// <param name="value">The instance of <see cref="System.Json.JsonValue"/> used to initialize the <see cref="System.Boolean"/> object.</param>
+ /// <returns>The <see cref="System.Boolean"/> initialized with the <see cref="System.Json.JsonValue"/> value specified.</returns>
+ public static explicit operator bool(JsonValue value)
+ {
+ return CastValue<bool>(value);
+ }
+
+ /// <summary>
+ /// Enables explicit casts from an instance of type <see cref="System.Json.JsonValue"/> to a <see cref="System.DateTimeOffset"/> object.
+ /// </summary>
+ /// <param name="value">The instance of <see cref="System.Json.JsonValue"/> used to initialize the <see cref="System.DateTimeOffset"/> object.</param>
+ /// <returns>The <see cref="System.DateTimeOffset"/> initialized with the <see cref="System.Json.JsonValue"/> value specified.</returns>
+ public static explicit operator DateTimeOffset(JsonValue value)
+ {
+ return CastValue<DateTimeOffset>(value);
+ }
+
+ /// <summary>
+ /// Enables implicit casts from type <see cref="System.Boolean"/> to a <see cref="System.Json.JsonPrimitive"/>.
+ /// </summary>
+ /// <param name="value">The <see cref="System.Boolean"/> instance used to initialize the <see cref="System.Json.JsonPrimitive"/>.</param>
+ /// <returns>The <see cref="System.Json.JsonValue"/> initialized with the <see cref="System.Boolean"/> specified.</returns>
+ public static implicit operator JsonValue(bool value)
+ {
+ return new JsonPrimitive(value);
+ }
+
+ /// <summary>
+ /// Enables implicit casts from type <see cref="System.Byte"/> to a <see cref="System.Json.JsonPrimitive"/>.
+ /// </summary>
+ /// <param name="value">The <see cref="System.Byte"/> instance used to initialize the <see cref="System.Json.JsonPrimitive"/>.</param>
+ /// <returns>The <see cref="System.Json.JsonValue"/> initialized with the <see cref="System.Byte"/> specified.</returns>
+ public static implicit operator JsonValue(byte value)
+ {
+ return new JsonPrimitive(value);
+ }
+
+ /// <summary>
+ /// Enables implicit casts from type <see cref="System.Decimal"/> to a <see cref="System.Json.JsonPrimitive"/>.
+ /// </summary>
+ /// <param name="value">The <see cref="System.Decimal"/> instance used to initialize the <see cref="System.Json.JsonPrimitive"/>.</param>
+ /// <returns>The <see cref="System.Json.JsonValue"/> initialized with the <see cref="System.Decimal"/> specified.</returns>
+ public static implicit operator JsonValue(decimal value)
+ {
+ return new JsonPrimitive(value);
+ }
+
+ /// <summary>
+ /// Enables implicit casts from type <see cref="System.Double"/> to a <see cref="System.Json.JsonPrimitive"/>.
+ /// </summary>
+ /// <param name="value">The <see cref="System.Double"/> instance used to initialize the <see cref="System.Json.JsonPrimitive"/>.</param>
+ /// <returns>The <see cref="System.Json.JsonValue"/> initialized with the <see cref="System.Double"/> specified.</returns>
+ public static implicit operator JsonValue(double value)
+ {
+ return new JsonPrimitive(value);
+ }
+
+ /// <summary>
+ /// Enables implicit casts from type <see cref="System.Int16"/> to a <see cref="System.Json.JsonPrimitive"/>.
+ /// </summary>
+ /// <param name="value">The <see cref="System.Int16"/> instance used to initialize the <see cref="System.Json.JsonPrimitive"/>.</param>
+ /// <returns>The <see cref="System.Json.JsonValue"/> initialized with the <see cref="System.Int16"/> specified.</returns>
+ public static implicit operator JsonValue(short value)
+ {
+ return new JsonPrimitive(value);
+ }
+
+ /// <summary>
+ /// Enables implicit casts from type <see cref="System.Int32"/> to a <see cref="System.Json.JsonPrimitive"/>.
+ /// </summary>
+ /// <param name="value">The <see cref="System.Int32"/> instance used to initialize the <see cref="System.Json.JsonPrimitive"/>.</param>
+ /// <returns>The <see cref="System.Json.JsonValue"/> initialized with the <see cref="System.Int32"/> specified.</returns>
+ public static implicit operator JsonValue(int value)
+ {
+ return new JsonPrimitive(value);
+ }
+
+ /// <summary>
+ /// Enables implicit casts from type <see cref="System.Int64"/> to a <see cref="System.Json.JsonPrimitive"/>.
+ /// </summary>
+ /// <param name="value">The <see cref="System.Int64"/> instance used to initialize the <see cref="System.Json.JsonPrimitive"/>.</param>
+ /// <returns>The <see cref="System.Json.JsonValue"/> initialized with the <see cref="System.Int64"/> specified.</returns>
+ public static implicit operator JsonValue(long value)
+ {
+ return new JsonPrimitive(value);
+ }
+
+ /// <summary>
+ /// Enables implicit casts from type <see cref="System.Single"/> to a <see cref="System.Json.JsonPrimitive"/>.
+ /// </summary>
+ /// <param name="value">The <see cref="System.Single"/> instance used to initialize the <see cref="System.Json.JsonPrimitive"/>.</param>
+ /// <returns>The <see cref="System.Json.JsonValue"/> initialized with the <see cref="System.Single"/> specified.</returns>
+ public static implicit operator JsonValue(float value)
+ {
+ return new JsonPrimitive(value);
+ }
+
+ /// <summary>
+ /// Enables implicit casts from type <see cref="System.String"/> to a <see cref="System.Json.JsonPrimitive"/>.
+ /// </summary>
+ /// <param name="value">The <see cref="System.String"/> instance used to initialize the <see cref="System.Json.JsonPrimitive"/>.</param>
+ /// <returns>The <see cref="System.Json.JsonValue"/> initialized with the <see cref="System.String"/> specified, or null if the value is null.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1057:StringUriOverloadsCallSystemUriOverloads",
+ Justification = "This operator does not intend to represent a Uri overload.")]
+ public static implicit operator JsonValue(string value)
+ {
+ return value == null ? null : new JsonPrimitive(value);
+ }
+
+ /// <summary>
+ /// Enables implicit casts from type <see cref="System.Char"/> to a <see cref="System.Json.JsonPrimitive"/>.
+ /// </summary>
+ /// <param name="value">The <see cref="System.Char"/> instance used to initialize the <see cref="System.Json.JsonPrimitive"/>.</param>
+ /// <returns>The <see cref="System.Json.JsonValue"/> initialized with the <see cref="System.Char"/> specified.</returns>
+ public static implicit operator JsonValue(char value)
+ {
+ return new JsonPrimitive(value);
+ }
+
+ /// <summary>
+ /// Enables implicit casts from type <see cref="System.DateTime"/> to a <see cref="System.Json.JsonPrimitive"/>.
+ /// </summary>
+ /// <param name="value">The <see cref="System.DateTime"/> instance used to initialize the <see cref="System.Json.JsonPrimitive"/>.</param>
+ /// <returns>The <see cref="System.Json.JsonValue"/> initialized with the <see cref="System.DateTime"/> specified.</returns>
+ public static implicit operator JsonValue(DateTime value)
+ {
+ return new JsonPrimitive(value);
+ }
+
+ /// <summary>
+ /// Enables implicit casts from type <see cref="System.Guid"/> to a <see cref="System.Json.JsonPrimitive"/>.
+ /// </summary>
+ /// <param name="value">The <see cref="System.Guid"/> instance used to initialize the <see cref="System.Json.JsonPrimitive"/>.</param>
+ /// <returns>The <see cref="System.Json.JsonValue"/> initialized with the <see cref="System.Guid"/> specified.</returns>
+ public static implicit operator JsonValue(Guid value)
+ {
+ return new JsonPrimitive(value);
+ }
+
+ /// <summary>
+ /// Enables implicit casts from type <see cref="System.Uri"/> to a <see cref="System.Json.JsonPrimitive"/>.
+ /// </summary>
+ /// <param name="value">The <see cref="System.Uri"/> instance used to initialize the <see cref="System.Json.JsonPrimitive"/>.</param>
+ /// <returns>The <see cref="System.Json.JsonValue"/> initialized with the <see cref="System.Uri"/> specified, or null if the value is null.</returns>
+ public static implicit operator JsonValue(Uri value)
+ {
+ return value == null ? null : new JsonPrimitive(value);
+ }
+
+ /// <summary>
+ /// Enables implicit casts from type <see cref="System.SByte"/> to a <see cref="System.Json.JsonPrimitive"/>.
+ /// </summary>
+ /// <param name="value">The <see cref="System.SByte"/> instance used to initialize the <see cref="System.Json.JsonPrimitive"/>.</param>
+ /// <returns>The <see cref="System.Json.JsonValue"/> initialized with the <see cref="System.SByte"/> specified.</returns>
+ [CLSCompliant(false)]
+ public static implicit operator JsonValue(sbyte value)
+ {
+ return new JsonPrimitive(value);
+ }
+
+ /// <summary>
+ /// Enables implicit casts from type <see cref="System.UInt16"/> to a <see cref="System.Json.JsonPrimitive"/>.
+ /// </summary>
+ /// <param name="value">The <see cref="System.UInt16"/> instance used to initialize the <see cref="System.Json.JsonPrimitive"/>.</param>
+ /// <returns>The <see cref="System.Json.JsonValue"/> initialized with the <see cref="System.UInt16"/> specified.</returns>
+ [CLSCompliant(false)]
+ public static implicit operator JsonValue(ushort value)
+ {
+ return new JsonPrimitive(value);
+ }
+
+ /// <summary>
+ /// Enables implicit casts from type <see cref="System.UInt32"/> to a <see cref="System.Json.JsonPrimitive"/>.
+ /// </summary>
+ /// <param name="value">The <see cref="System.UInt32"/> instance used to initialize the <see cref="System.Json.JsonPrimitive"/>.</param>
+ /// <returns>The <see cref="System.Json.JsonValue"/> initialized with the <see cref="System.UInt32"/> specified.</returns>
+ [CLSCompliant(false)]
+ public static implicit operator JsonValue(uint value)
+ {
+ return new JsonPrimitive(value);
+ }
+
+ /// <summary>
+ /// Enables implicit casts from type <see cref="System.UInt64"/> to a <see cref="System.Json.JsonPrimitive"/>.
+ /// </summary>
+ /// <param name="value">The <see cref="System.UInt64"/> instance used to initialize the <see cref="System.Json.JsonPrimitive"/>.</param>
+ /// <returns>The <see cref="System.Json.JsonValue"/> initialized with the <see cref="System.UInt64"/> specified.</returns>
+ [CLSCompliant(false)]
+ public static implicit operator JsonValue(ulong value)
+ {
+ return new JsonPrimitive(value);
+ }
+
+ /// <summary>
+ /// Enables implicit casts from type <see cref="System.DateTimeOffset"/> to a <see cref="System.Json.JsonPrimitive"/>.
+ /// </summary>
+ /// <param name="value">The <see cref="System.DateTimeOffset"/> instance used to initialize the <see cref="System.Json.JsonPrimitive"/>.</param>
+ /// <returns>The <see cref="System.Json.JsonValue"/> initialized with the <see cref="System.DateTimeOffset"/> specified.</returns>
+ public static implicit operator JsonValue(DateTimeOffset value)
+ {
+ return new JsonPrimitive(value);
+ }
+ }
+}
diff --git a/src/System.Json/JsonValueChange.cs b/src/System.Json/JsonValueChange.cs
new file mode 100644
index 00000000..7bc9f901
--- /dev/null
+++ b/src/System.Json/JsonValueChange.cs
@@ -0,0 +1,28 @@
+namespace System.Json
+{
+ /// <summary>
+ /// Specifies the event type when an event is raised for a <see cref="System.Json.JsonValue"/>.
+ /// </summary>
+ public enum JsonValueChange
+ {
+ /// <summary>
+ /// An element has been or will be added to the collection.
+ /// </summary>
+ Add,
+
+ /// <summary>
+ /// An element has been or will be removed from the collection.
+ /// </summary>
+ Remove,
+
+ /// <summary>
+ /// An element has been or will be replaced in the collection. Used on indexers.
+ /// </summary>
+ Replace,
+
+ /// <summary>
+ /// All elements of the collection have been or will be removed.
+ /// </summary>
+ Clear,
+ }
+}
diff --git a/src/System.Json/JsonValueChangeEventArgs.cs b/src/System.Json/JsonValueChangeEventArgs.cs
new file mode 100644
index 00000000..9088df25
--- /dev/null
+++ b/src/System.Json/JsonValueChangeEventArgs.cs
@@ -0,0 +1,96 @@
+namespace System.Json
+{
+ /// <summary>
+ /// Provide data for the <see cref="System.Json.JsonValue.Changing"/> and <see cref="System.Json.JsonValue.Changed"/> events.
+ /// </summary>
+ public class JsonValueChangeEventArgs : EventArgs
+ {
+ private JsonValue child;
+ private JsonValueChange change;
+ private int index;
+ private string key;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="System.Json.JsonValueChangeEventArgs"/> class for
+ /// changes in a <see cref="System.Json.JsonArray"/>.
+ /// </summary>
+ /// <param name="child">The <see cref="System.Json.JsonValue"/> instance which will be or has been modified.</param>
+ /// <param name="change">The type of change of the <see cref="System.Json.JsonValue"/> event.</param>
+ /// <param name="index">The index of the element being changed in a <see cref="System.Json.JsonArray"/>.</param>
+ public JsonValueChangeEventArgs(JsonValue child, JsonValueChange change, int index)
+ {
+ if (index < 0)
+ {
+ throw new ArgumentOutOfRangeException("index", RS.Format(Properties.Resources.ArgumentMustBeGreaterThanOrEqualTo, index, 0));
+ }
+
+ this.child = child;
+ this.change = change;
+ this.index = index;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="System.Json.JsonValueChangeEventArgs"/> class for
+ /// changes in a <see cref="System.Json.JsonObject"/>.
+ /// </summary>
+ /// <param name="child">The <see cref="System.Json.JsonValue"/> instance which will be or has been modified.</param>
+ /// <param name="change">The type of change of the <see cref="System.Json.JsonValue"/> event.</param>
+ /// <param name="key">The key of the element being changed in a <see cref="System.Json.JsonObject"/>.</param>
+ public JsonValueChangeEventArgs(JsonValue child, JsonValueChange change, string key)
+ {
+ if (change != JsonValueChange.Clear)
+ {
+ if (key == null)
+ {
+ throw new ArgumentNullException("key");
+ }
+ }
+
+ this.child = child;
+ this.change = change;
+ index = -1;
+ this.key = key;
+ }
+
+ /// <summary>
+ /// Gets the child which will be or has been modified.
+ /// </summary>
+ /// <remarks><p>This property is <code>null</code> for <see cref="System.Json.JsonValueChange.Clear"/> event types
+ /// raised by <see cref="System.Json.JsonValue"/> instances.</p>
+ /// <p>For <see cref="System.Json.JsonValueChange">Replace</see> events, this property contains the new value in
+ /// the <see cref="System.Json.JsonValue.Changing"/> event, and the old value (the one being replaced) in the
+ /// <see cref="System.Json.JsonValue.Changed"/> event.</p></remarks>
+ public JsonValue Child
+ {
+ get { return child; }
+ }
+
+ /// <summary>
+ /// Gets the type of change.
+ /// </summary>
+ public JsonValueChange Change
+ {
+ get { return change; }
+ }
+
+ /// <summary>
+ /// Gets the index in the <see cref="System.Json.JsonArray"/> where the change happened, or
+ /// <code>-1</code> if the change happened in a <see cref="System.Json.JsonValue"/> of a different type.
+ /// </summary>
+ public int Index
+ {
+ get { return index; }
+ }
+
+ /// <summary>
+ /// Gets the key in the <see cref="System.Json.JsonObject"/> where the change happened, or
+ /// <code>null</code> if the change happened in a <see cref="System.Json.JsonValue"/> of a different type.
+ /// </summary>
+ /// <remarks>This property can also be <code>null</code> if the event type is
+ /// <see cref="System.Json.JsonValueChange">Clear</see>.</remarks>
+ public string Key
+ {
+ get { return key; }
+ }
+ }
+}
diff --git a/src/System.Json/JsonValueDynamicMetaObject.cs b/src/System.Json/JsonValueDynamicMetaObject.cs
new file mode 100644
index 00000000..daaa27a9
--- /dev/null
+++ b/src/System.Json/JsonValueDynamicMetaObject.cs
@@ -0,0 +1,381 @@
+using System.Collections.Generic;
+using System.Dynamic;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Runtime.Serialization.Json;
+
+namespace System.Json
+{
+ /// <summary>
+ /// This class provides dynamic behavior support for the JsonValue types.
+ /// </summary>
+ internal class JsonValueDynamicMetaObject : DynamicMetaObject
+ {
+ private static readonly MethodInfo _getValueByIndexMethodInfo = typeof(JsonValue).GetMethod("GetValue", new Type[] { typeof(int) });
+ private static readonly MethodInfo _getValueByKeyMethodInfo = typeof(JsonValue).GetMethod("GetValue", new Type[] { typeof(string) });
+ private static readonly MethodInfo _setValueByIndexMethodInfo = typeof(JsonValue).GetMethod("SetValue", new Type[] { typeof(int), typeof(object) });
+ private static readonly MethodInfo _setValueByKeyMethodInfo = typeof(JsonValue).GetMethod("SetValue", new Type[] { typeof(string), typeof(object) });
+ private static readonly MethodInfo _castValueMethodInfo = typeof(JsonValue).GetMethod("CastValue", new Type[] { typeof(JsonValue) });
+ private static readonly MethodInfo _changeTypeMethodInfo = typeof(Convert).GetMethod("ChangeType", new Type[] { typeof(object), typeof(Type) });
+
+ /// <summary>
+ /// Class constructor.
+ /// </summary>
+ /// <param name="parameter">The expression representing this <see cref="DynamicMetaObject"/> during the dynamic binding process.</param>
+ /// <param name="value">The runtime value represented by the <see cref="DynamicMetaObject"/>.</param>
+ internal JsonValueDynamicMetaObject(Expression parameter, JsonValue value)
+ : base(parameter, BindingRestrictions.Empty, value)
+ {
+ }
+
+ /// <summary>
+ /// Gets the default binding restrictions for this type.
+ /// </summary>
+ private BindingRestrictions DefaultRestrictions
+ {
+ get { return BindingRestrictions.GetTypeRestriction(Expression, LimitType); }
+ }
+
+ /// <summary>
+ /// Implements dynamic cast for JsonValue types.
+ /// </summary>
+ /// <param name="binder">An instance of the <see cref="ConvertBinder"/> that represents the details of the dynamic operation.</param>
+ /// <returns>The new <see cref="DynamicMetaObject"/> representing the result of the binding.</returns>
+ public override DynamicMetaObject BindConvert(ConvertBinder binder)
+ {
+ if (binder == null)
+ {
+ throw new ArgumentNullException("binder");
+ }
+
+ Expression expression = Expression;
+
+ bool implicitCastSupported =
+ binder.Type.IsAssignableFrom(LimitType) ||
+ binder.Type == typeof(IEnumerable<KeyValuePair<string, JsonValue>>) ||
+ binder.Type == typeof(IDynamicMetaObjectProvider) ||
+ binder.Type == typeof(object);
+
+ if (!implicitCastSupported)
+ {
+ if (JsonValue.IsSupportedExplicitCastType(binder.Type))
+ {
+ Expression instance = Expression.Convert(Expression, LimitType);
+ expression = Expression.Call(_castValueMethodInfo.MakeGenericMethod(binder.Type), new Expression[] { instance });
+ }
+ else
+ {
+ string exceptionMessage = RS.Format(Properties.Resources.CannotCastJsonValue, LimitType.FullName, binder.Type.FullName);
+ expression = Expression.Throw(Expression.Constant(new InvalidCastException(exceptionMessage)), typeof(object));
+ }
+ }
+
+ expression = Expression.Convert(expression, binder.Type);
+
+ return new DynamicMetaObject(expression, DefaultRestrictions);
+ }
+
+ /// <summary>
+ /// Implements setter for dynamic indexer by index (JsonArray)
+ /// </summary>
+ /// <param name="binder">An instance of the <see cref="GetIndexBinder"/> that represents the details of the dynamic operation.</param>
+ /// <param name="indexes">An array of <see cref="DynamicMetaObject"/> instances - indexes for the get index operation.</param>
+ /// <returns>The new <see cref="DynamicMetaObject"/> representing the result of the binding.</returns>
+ public override DynamicMetaObject BindGetIndex(GetIndexBinder binder, DynamicMetaObject[] indexes)
+ {
+ if (binder == null)
+ {
+ throw new ArgumentNullException("binder");
+ }
+
+ if (indexes == null)
+ {
+ throw new ArgumentNullException("indexes");
+ }
+
+ Expression indexExpression;
+ if (!JsonValueDynamicMetaObject.TryGetIndexExpression(indexes, out indexExpression))
+ {
+ return new DynamicMetaObject(indexExpression, DefaultRestrictions);
+ }
+
+ MethodInfo methodInfo = indexExpression.Type == typeof(string) ? _getValueByKeyMethodInfo : _getValueByIndexMethodInfo;
+ Expression[] args = new Expression[] { indexExpression };
+
+ return GetMethodMetaObject(methodInfo, args);
+ }
+
+ /// <summary>
+ /// Implements getter for dynamic indexer by index (JsonArray).
+ /// </summary>
+ /// <param name="binder">An instance of the <see cref="SetIndexBinder"/> that represents the details of the dynamic operation.</param>
+ /// <param name="indexes">An array of <see cref="DynamicMetaObject"/> instances - indexes for the set index operation.</param>
+ /// <param name="value">The <see cref="DynamicMetaObject"/> representing the value for the set index operation.</param>
+ /// <returns>The new <see cref="DynamicMetaObject"/> representing the result of the binding.</returns>
+ public override DynamicMetaObject BindSetIndex(SetIndexBinder binder, DynamicMetaObject[] indexes, DynamicMetaObject value)
+ {
+ if (binder == null)
+ {
+ throw new ArgumentNullException("binder");
+ }
+
+ if (indexes == null)
+ {
+ throw new ArgumentNullException("indexes");
+ }
+
+ if (value == null)
+ {
+ throw new ArgumentNullException("value");
+ }
+
+ Expression indexExpression;
+ if (!JsonValueDynamicMetaObject.TryGetIndexExpression(indexes, out indexExpression))
+ {
+ return new DynamicMetaObject(indexExpression, DefaultRestrictions);
+ }
+
+ MethodInfo methodInfo = indexExpression.Type == typeof(string) ? _setValueByKeyMethodInfo : _setValueByIndexMethodInfo;
+ Expression[] args = new Expression[] { indexExpression, Expression.Convert(value.Expression, typeof(object)) };
+
+ return GetMethodMetaObject(methodInfo, args);
+ }
+
+ /// <summary>
+ /// Implements getter for dynamic indexer by key (JsonObject).
+ /// </summary>
+ /// <param name="binder">An instance of the <see cref="GetMemberBinder"/> that represents the details of the dynamic operation.</param>
+ /// <returns>The new <see cref="DynamicMetaObject"/> representing the result of the binding.</returns>
+ public override DynamicMetaObject BindGetMember(GetMemberBinder binder)
+ {
+ if (binder == null)
+ {
+ throw new ArgumentNullException("binder");
+ }
+
+ PropertyInfo propInfo = LimitType.GetProperty(binder.Name, BindingFlags.Instance | BindingFlags.Public);
+
+ if (propInfo != null)
+ {
+ return base.BindGetMember(binder);
+ }
+
+ Expression[] args = new Expression[] { Expression.Constant(binder.Name) };
+
+ return GetMethodMetaObject(_getValueByKeyMethodInfo, args);
+ }
+
+ /// <summary>
+ /// Implements setter for dynamic indexer by key (JsonObject).
+ /// </summary>
+ /// <param name="binder">An instance of the <see cref="SetMemberBinder"/> that represents the details of the dynamic operation.</param>
+ /// <param name="value">The <see cref="DynamicMetaObject"/> representing the value for the set member operation.</param>
+ /// <returns>The new <see cref="DynamicMetaObject"/> representing the result of the binding.</returns>
+ public override DynamicMetaObject BindSetMember(SetMemberBinder binder, DynamicMetaObject value)
+ {
+ if (binder == null)
+ {
+ throw new ArgumentNullException("binder");
+ }
+
+ if (value == null)
+ {
+ throw new ArgumentNullException("value");
+ }
+
+ Expression[] args = new Expression[] { Expression.Constant(binder.Name), Expression.Convert(value.Expression, typeof(object)) };
+
+ return GetMethodMetaObject(_setValueByKeyMethodInfo, args);
+ }
+
+ /// <summary>
+ /// Performs the binding of the dynamic invoke member operation.
+ /// Implemented to support extension methods defined in <see cref="JsonValueExtensions"/> type.
+ /// </summary>
+ /// <param name="binder">An instance of the InvokeMemberBinder that represents the details of the dynamic operation.</param>
+ /// <param name="args">An array of DynamicMetaObject instances - arguments to the invoke member operation.</param>
+ /// <returns>The new DynamicMetaObject representing the result of the binding.</returns>
+ public override DynamicMetaObject BindInvokeMember(InvokeMemberBinder binder, DynamicMetaObject[] args)
+ {
+ if (binder == null)
+ {
+ throw new ArgumentNullException("binder");
+ }
+
+ if (args == null)
+ {
+ throw new ArgumentNullException("args");
+ }
+
+ List<Type> argTypeList = new List<Type>();
+
+ for (int idx = 0; idx < args.Length; idx++)
+ {
+ argTypeList.Add(args[idx].LimitType);
+ }
+
+ MethodInfo methodInfo = Value.GetType().GetMethod(binder.Name, argTypeList.ToArray());
+
+ if (methodInfo == null)
+ {
+ argTypeList.Insert(0, typeof(JsonValue));
+
+ Type[] argTypes = argTypeList.ToArray();
+
+ methodInfo = JsonValueDynamicMetaObject.GetExtensionMethod(typeof(JsonValueExtensions), binder.Name, argTypes);
+
+ if (methodInfo != null)
+ {
+ Expression thisInstance = Expression.Convert(Expression, LimitType);
+ Expression[] argsExpression = new Expression[argTypes.Length];
+
+ argsExpression[0] = thisInstance;
+ for (int i = 0; i < args.Length; i++)
+ {
+ argsExpression[i + 1] = args[i].Expression;
+ }
+
+ Expression callExpression = Expression.Call(methodInfo, argsExpression);
+
+ if (methodInfo.ReturnType == typeof(void))
+ {
+ callExpression = Expression.Block(callExpression, Expression.Default(binder.ReturnType));
+ }
+ else
+ {
+ callExpression = Expression.Convert(Expression.Call(methodInfo, argsExpression), binder.ReturnType);
+ }
+
+ return new DynamicMetaObject(callExpression, DefaultRestrictions);
+ }
+ }
+
+ return base.BindInvokeMember(binder, args);
+ }
+
+ /// <summary>
+ /// Returns the enumeration of all dynamic member names.
+ /// </summary>
+ /// <returns>An <see cref="IEnumerable{T}"/> of string reprenseting the dynamic member names.</returns>
+ public override IEnumerable<string> GetDynamicMemberNames()
+ {
+ JsonValue jsonValue = Value as JsonValue;
+
+ if (jsonValue != null)
+ {
+ List<string> names = new List<string>();
+
+ foreach (KeyValuePair<string, JsonValue> pair in jsonValue)
+ {
+ names.Add(pair.Key);
+ }
+
+ return names;
+ }
+
+ return base.GetDynamicMemberNames();
+ }
+
+ /// <summary>
+ /// Gets a <see cref="MethodInfo"/> instance for the specified method name in the specified type.
+ /// </summary>
+ /// <param name="extensionProviderType">The extension provider type.</param>
+ /// <param name="methodName">The name of the method to get the info for.</param>
+ /// <param name="argTypes">The types of the method arguments.</param>
+ /// <returns>A <see cref="MethodInfo"/>instance or null if the method cannot be resolved.</returns>
+ private static MethodInfo GetExtensionMethod(Type extensionProviderType, string methodName, Type[] argTypes)
+ {
+ MethodInfo methodInfo = null;
+ MethodInfo[] methods = extensionProviderType.GetMethods();
+
+ foreach (MethodInfo info in methods)
+ {
+ if (info.Name == methodName)
+ {
+ methodInfo = info;
+
+ if (!info.IsGenericMethodDefinition)
+ {
+ bool paramsMatch = true;
+ ParameterInfo[] args = methodInfo.GetParameters();
+
+ if (args.Length == argTypes.Length)
+ {
+ for (int idx = 0; idx < args.Length; idx++)
+ {
+ if (!args[idx].ParameterType.IsAssignableFrom(argTypes[idx]))
+ {
+ paramsMatch = false;
+ break;
+ }
+ }
+
+ if (paramsMatch)
+ {
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ return methodInfo;
+ }
+
+ /// <summary>
+ /// Attempts to get an expression for an index parameter.
+ /// </summary>
+ /// <param name="indexes">The operation indexes parameter.</param>
+ /// <param name="expression">A <see cref="Expression"/> to be initialized to the index expression if the operation is successful, otherwise an error expression.</param>
+ /// <returns>true the operation is successful, false otherwise.</returns>
+ private static bool TryGetIndexExpression(DynamicMetaObject[] indexes, out Expression expression)
+ {
+ if (indexes.Length == 1 && indexes[0] != null && indexes[0].Value != null)
+ {
+ DynamicMetaObject index = indexes[0];
+ Type indexType = indexes[0].Value.GetType();
+
+ switch (Type.GetTypeCode(indexType))
+ {
+ case TypeCode.Char:
+ case TypeCode.Int16:
+ case TypeCode.UInt16:
+ case TypeCode.Byte:
+ case TypeCode.SByte:
+ Expression argExp = Expression.Convert(index.Expression, typeof(object));
+ Expression typeExp = Expression.Constant(typeof(int));
+ expression = Expression.Convert(Expression.Call(_changeTypeMethodInfo, new Expression[] { argExp, typeExp }), typeof(int));
+ return true;
+
+ case TypeCode.Int32:
+ case TypeCode.String:
+ expression = index.Expression;
+ return true;
+ }
+
+ expression = Expression.Throw(Expression.Constant(new ArgumentException(RS.Format(Properties.Resources.InvalidIndexType, indexType))), typeof(object));
+ return false;
+ }
+
+ expression = Expression.Throw(Expression.Constant(new ArgumentException(Properties.Resources.NonSingleNonNullIndexNotSupported)), typeof(object));
+ return false;
+ }
+
+ /// <summary>
+ /// Gets a <see cref="DynamicMetaObject"/> for a method call.
+ /// </summary>
+ /// <param name="methodInfo">Info for the method to be performed.</param>
+ /// <param name="args">expression array representing the method arguments</param>
+ /// <returns>A meta object for the method call.</returns>
+ private DynamicMetaObject GetMethodMetaObject(MethodInfo methodInfo, Expression[] args)
+ {
+ Expression instance = Expression.Convert(Expression, LimitType);
+ Expression methodCall = Expression.Call(instance, methodInfo, args);
+ BindingRestrictions restrictions = DefaultRestrictions;
+
+ DynamicMetaObject metaObj = new DynamicMetaObject(methodCall, restrictions);
+
+ return metaObj;
+ }
+ }
+}
diff --git a/src/System.Json/JsonValueLinqExtensions.cs b/src/System.Json/JsonValueLinqExtensions.cs
new file mode 100644
index 00000000..5aa6ba71
--- /dev/null
+++ b/src/System.Json/JsonValueLinqExtensions.cs
@@ -0,0 +1,35 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Json
+{
+ /// <summary>
+ /// This class extends the funcionality of the <see cref="JsonValue"/> type for better Linq support .
+ /// </summary>
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Justification = "Linq is a technical name.")]
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static class JsonValueLinqExtensions
+ {
+ /// <summary>
+ /// Extension method for creating a <see cref="JsonValue"/> from an <see cref="IEnumerable{T}"/> collection of <see cref="JsonValue"/> types.
+ /// </summary>
+ /// <param name="items">The enumerable instance.</param>
+ /// <returns>A <see cref="JsonArray"/> created from the specified items.</returns>
+ public static JsonArray ToJsonArray(this IEnumerable<JsonValue> items)
+ {
+ return new JsonArray(items);
+ }
+
+ /// <summary>
+ /// Extension method for creating a <see cref="JsonValue"/> from an <see cref="IEnumerable{T}"/> collection of <see cref="KeyValuePair{K,V}"/> of <see cref="String"/> and <see cref="JsonValue"/> types.
+ /// </summary>
+ /// <param name="items">The enumerable instance.</param>
+ /// <returns>A <see cref="JsonValue"/> created from the specified items.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "JsonValue implements the nested type in param.")]
+ public static JsonObject ToJsonObject(this IEnumerable<KeyValuePair<string, JsonValue>> items)
+ {
+ return new JsonObject(items);
+ }
+ }
+}
diff --git a/src/System.Json/NGenWrapper.cs b/src/System.Json/NGenWrapper.cs
new file mode 100644
index 00000000..4de0b930
--- /dev/null
+++ b/src/System.Json/NGenWrapper.cs
@@ -0,0 +1,46 @@
+namespace System.Json
+{
+ /// <summary>
+ /// Struct that wraps values which cause JIT compilation at runtime.
+ /// This Struct is added to solve the FxCop warning CA908 in JsonObject.cs.
+ /// </summary>
+ /// <typeparam name="T">Wrapped type.</typeparam>
+ internal struct NGenWrapper<T>
+ {
+ /// <summary>
+ /// Value of type T which represents the actual data which is currently in hold.
+ /// </summary>
+ public T Value;
+
+ /// <summary>
+ /// Creates an instance of the <see cref="System.Json.NGenWrapper{T}"/> class
+ /// </summary>
+ /// <param name="value">The wrapped object of T</param>
+ public NGenWrapper(T value)
+ {
+ Value = value;
+ }
+
+ /// <summary>
+ /// Cast operator from <see cref="System.Json.NGenWrapper{T}"/> to <typeparamref name="T"/>
+ /// </summary>
+ /// <param name="value">Object in type <see cref="System.Json.NGenWrapper{T}"/></param>
+ /// <returns>Object in type <typeparamref name="T">The wrapped element type</typeparamref></returns>
+ /// <typeparamref name="T">The wrapped element type</typeparamref>
+ public static implicit operator T(NGenWrapper<T> value)
+ {
+ return value.Value;
+ }
+
+ /// <summary>
+ /// Cast operator from <typeparamref name="T"/> to <see cref="System.Json.NGenWrapper{T}"/>
+ /// </summary>
+ /// <param name="value">Object in type <typeparamref name="T"/></param>
+ /// <returns>Object in type <see cref="System.Json.NGenWrapper{T}"/></returns>
+ /// <typeparamref name="T">The wrapped element type</typeparamref>
+ public static implicit operator NGenWrapper<T>(T value)
+ {
+ return new NGenWrapper<T>(value);
+ }
+ }
+}
diff --git a/src/System.Json/Properties/AssemblyInfo.cs b/src/System.Json/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..cf08e4e4
--- /dev/null
+++ b/src/System.Json/Properties/AssemblyInfo.cs
@@ -0,0 +1,10 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+
+[assembly: AssemblyTitle("System.Json")]
+[assembly: AssemblyDescription("")]
+[assembly: Guid("6fd72360-ebfc-4097-96fa-2ee418c04f7b")]
diff --git a/src/System.Json/Properties/Resources.Designer.cs b/src/System.Json/Properties/Resources.Designer.cs
new file mode 100644
index 00000000..a418daee
--- /dev/null
+++ b/src/System.Json/Properties/Resources.Designer.cs
@@ -0,0 +1,288 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.17369
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace System.Json.Properties {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("System.Json.Properties.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The argument &apos;{0}&apos; must be greater than or equal to {1}..
+ /// </summary>
+ internal static string ArgumentMustBeGreaterThanOrEqualTo {
+ get {
+ return ResourceManager.GetString("ArgumentMustBeGreaterThanOrEqualTo", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Unable to cast object of type &apos;{0}&apos; to type &apos;{1}&apos;..
+ /// </summary>
+ internal static string CannotCastJsonValue {
+ get {
+ return ResourceManager.GetString("CannotCastJsonValue", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to CannotReadAsType=Cannot read &apos;{0}&apos; as &apos;{1}&apos; type..
+ /// </summary>
+ internal static string CannotReadAsType {
+ get {
+ return ResourceManager.GetString("CannotReadAsType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Cannot read JsonPrimitive value &apos;{0}&apos; as &apos;{1}&apos;..
+ /// </summary>
+ internal static string CannotReadPrimitiveAsType {
+ get {
+ return ResourceManager.GetString("CannotReadPrimitiveAsType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &apos;{0}&apos; does not contain a definition for property &apos;{1}&apos;..
+ /// </summary>
+ internal static string DynamicPropertyNotDefined {
+ get {
+ return ResourceManager.GetString("DynamicPropertyNotDefined", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Mismatched types at node &apos;{0}&apos;..
+ /// </summary>
+ internal static string FormUrlEncodedMismatchingTypes {
+ get {
+ return ResourceManager.GetString("FormUrlEncodedMismatchingTypes", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The input source is not correctly formatted..
+ /// </summary>
+ internal static string IncorrectJsonFormat {
+ get {
+ return ResourceManager.GetString("IncorrectJsonFormat", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &apos;{0}&apos; type indexer is not supported on JsonValue of &apos;JsonType.{1}&apos; type..
+ /// </summary>
+ internal static string IndexerNotSupportedOnJsonType {
+ get {
+ return ResourceManager.GetString("IndexerNotSupportedOnJsonType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Invalid array at node &apos;{0}&apos;..
+ /// </summary>
+ internal static string InvalidArrayInsert {
+ get {
+ return ResourceManager.GetString("InvalidArrayInsert", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Cannot convert null to &apos;{0}&apos; because it is a non-nullable value type..
+ /// </summary>
+ internal static string InvalidCastNonNullable {
+ get {
+ return ResourceManager.GetString("InvalidCastNonNullable", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Cannot cast JsonPrimitive value &apos;{0}&apos; as &apos;{1}&apos;. It is not in a valid date format..
+ /// </summary>
+ internal static string InvalidDateFormat {
+ get {
+ return ResourceManager.GetString("InvalidDateFormat", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Invalid &apos;{0}&apos; index type; only &apos;System.String&apos; and non-negative &apos;System.Int32&apos; types are supported..
+ /// </summary>
+ internal static string InvalidIndexType {
+ get {
+ return ResourceManager.GetString("InvalidIndexType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Invalid JSON primitive: {0}..
+ /// </summary>
+ internal static string InvalidJsonPrimitive {
+ get {
+ return ResourceManager.GetString("InvalidJsonPrimitive", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Cannot cast &apos;{0}&apos; value &apos;{1}.{2}&apos; as a type of &apos;{3}&apos;. The provided string is not a valid relative or absolute &apos;{3}&apos;..
+ /// </summary>
+ internal static string InvalidUriFormat {
+ get {
+ return ResourceManager.GetString("InvalidUriFormat", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Traditional style array without &apos;[]&apos; is not supported with nested object at location {0}..
+ /// </summary>
+ internal static string JQuery13CompatModeNotSupportNestedJson {
+ get {
+ return ResourceManager.GetString("JQuery13CompatModeNotSupportNestedJson", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to An empty string cannot be parsed as JSON..
+ /// </summary>
+ internal static string JsonStringCannotBeEmpty {
+ get {
+ return ResourceManager.GetString("JsonStringCannotBeEmpty", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The maximum read depth ({0}) has been exceeded because the form url-encoded data being read has more levels of nesting than is allowed..
+ /// </summary>
+ internal static string MaxDepthExceeded {
+ get {
+ return ResourceManager.GetString("MaxDepthExceeded", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The parameter must be greater than {0}..
+ /// </summary>
+ internal static string MinParameterSize {
+ get {
+ return ResourceManager.GetString("MinParameterSize", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Nested bracket is not valid for &apos;{0}&apos; data at position {1}..
+ /// </summary>
+ internal static string NestedBracketNotValid {
+ get {
+ return ResourceManager.GetString("NestedBracketNotValid", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Null index or multidimensional indexing is not supported by this indexer; use &apos;System.Int32&apos; or &apos;System.String&apos; for array and object indexing respectively..
+ /// </summary>
+ internal static string NonSingleNonNullIndexNotSupported {
+ get {
+ return ResourceManager.GetString("NonSingleNonNullIndexNotSupported", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Cannot cast JsonPrimitive value &apos;{0}&apos; as &apos;{1}&apos;. The value is either too large or too small for the specified CLR type..
+ /// </summary>
+ internal static string OverflowReadAs {
+ get {
+ return ResourceManager.GetString("OverflowReadAs", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Query string name cannot be null..
+ /// </summary>
+ internal static string QueryStringNameShouldNotNull {
+ get {
+ return ResourceManager.GetString("QueryStringNameShouldNotNull", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Object type not supported..
+ /// </summary>
+ internal static string TypeNotSupported {
+ get {
+ return ResourceManager.GetString("TypeNotSupported", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to There is an unmatched opened bracket for the &apos;{0}&apos; at position {1}..
+ /// </summary>
+ internal static string UnMatchedBracketNotValid {
+ get {
+ return ResourceManager.GetString("UnMatchedBracketNotValid", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Operation not supported on JsonValue instances of &apos;JsonType.Default&apos; type..
+ /// </summary>
+ internal static string UseOfDefaultNotAllowed {
+ get {
+ return ResourceManager.GetString("UseOfDefaultNotAllowed", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/System.Json/Properties/Resources.resx b/src/System.Json/Properties/Resources.resx
new file mode 100644
index 00000000..7f438495
--- /dev/null
+++ b/src/System.Json/Properties/Resources.resx
@@ -0,0 +1,195 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="ArgumentMustBeGreaterThanOrEqualTo" xml:space="preserve">
+ <value>The argument '{0}' must be greater than or equal to {1}.</value>
+ </data>
+ <data name="CannotCastJsonValue" xml:space="preserve">
+ <value>Unable to cast object of type '{0}' to type '{1}'.</value>
+ </data>
+ <data name="CannotReadAsType" xml:space="preserve">
+ <value>CannotReadAsType=Cannot read '{0}' as '{1}' type.</value>
+ </data>
+ <data name="CannotReadPrimitiveAsType" xml:space="preserve">
+ <value>Cannot read JsonPrimitive value '{0}' as '{1}'.</value>
+ </data>
+ <data name="DynamicPropertyNotDefined" xml:space="preserve">
+ <value>'{0}' does not contain a definition for property '{1}'.</value>
+ </data>
+ <data name="FormUrlEncodedMismatchingTypes" xml:space="preserve">
+ <value>Mismatched types at node '{0}'.</value>
+ </data>
+ <data name="IncorrectJsonFormat" xml:space="preserve">
+ <value>The input source is not correctly formatted.</value>
+ </data>
+ <data name="IndexerNotSupportedOnJsonType" xml:space="preserve">
+ <value>'{0}' type indexer is not supported on JsonValue of 'JsonType.{1}' type.</value>
+ </data>
+ <data name="InvalidArrayInsert" xml:space="preserve">
+ <value>Invalid array at node '{0}'.</value>
+ </data>
+ <data name="InvalidCastNonNullable" xml:space="preserve">
+ <value>Cannot convert null to '{0}' because it is a non-nullable value type.</value>
+ </data>
+ <data name="InvalidDateFormat" xml:space="preserve">
+ <value>Cannot cast JsonPrimitive value '{0}' as '{1}'. It is not in a valid date format.</value>
+ </data>
+ <data name="InvalidIndexType" xml:space="preserve">
+ <value>Invalid '{0}' index type; only 'System.String' and non-negative 'System.Int32' types are supported.</value>
+ </data>
+ <data name="InvalidJsonPrimitive" xml:space="preserve">
+ <value>Invalid JSON primitive: {0}.</value>
+ </data>
+ <data name="InvalidUriFormat" xml:space="preserve">
+ <value>Cannot cast '{0}' value '{1}.{2}' as a type of '{3}'. The provided string is not a valid relative or absolute '{3}'.</value>
+ </data>
+ <data name="JQuery13CompatModeNotSupportNestedJson" xml:space="preserve">
+ <value>Traditional style array without '[]' is not supported with nested object at location {0}.</value>
+ </data>
+ <data name="JsonStringCannotBeEmpty" xml:space="preserve">
+ <value>An empty string cannot be parsed as JSON.</value>
+ </data>
+ <data name="MaxDepthExceeded" xml:space="preserve">
+ <value>The maximum read depth ({0}) has been exceeded because the form url-encoded data being read has more levels of nesting than is allowed.</value>
+ </data>
+ <data name="MinParameterSize" xml:space="preserve">
+ <value>The parameter must be greater than {0}.</value>
+ </data>
+ <data name="NestedBracketNotValid" xml:space="preserve">
+ <value>Nested bracket is not valid for '{0}' data at position {1}.</value>
+ </data>
+ <data name="NonSingleNonNullIndexNotSupported" xml:space="preserve">
+ <value>Null index or multidimensional indexing is not supported by this indexer; use 'System.Int32' or 'System.String' for array and object indexing respectively.</value>
+ </data>
+ <data name="OverflowReadAs" xml:space="preserve">
+ <value>Cannot cast JsonPrimitive value '{0}' as '{1}'. The value is either too large or too small for the specified CLR type.</value>
+ </data>
+ <data name="QueryStringNameShouldNotNull" xml:space="preserve">
+ <value>Query string name cannot be null.</value>
+ </data>
+ <data name="TypeNotSupported" xml:space="preserve">
+ <value>Object type not supported.</value>
+ </data>
+ <data name="UnMatchedBracketNotValid" xml:space="preserve">
+ <value>There is an unmatched opened bracket for the '{0}' at position {1}.</value>
+ </data>
+ <data name="UseOfDefaultNotAllowed" xml:space="preserve">
+ <value>Operation not supported on JsonValue instances of 'JsonType.Default' type.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/System.Json/Settings.StyleCop b/src/System.Json/Settings.StyleCop
new file mode 100644
index 00000000..83ad56e9
--- /dev/null
+++ b/src/System.Json/Settings.StyleCop
@@ -0,0 +1,198 @@
+<StyleCopSettings Version="4.3">
+
+ <!-- Files in this SourceFileList are excluded from DocumentationRules only -->
+ <SourceFileList>
+ <Settings>
+ <GlobalSettings>
+ <StringProperty Name="MergeSettingsFiles">Merge</StringProperty>
+ </GlobalSettings>
+
+ <Analyzers>
+ <Analyzer AnalyzerId="Microsoft.StyleCop.CSharp.DocumentationRules">
+ <Rules>
+ <Rule Name="ElementsMustBeDocumented">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="PartialElementsMustBeDocumented">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="EnumerationItemsMustBeDocumented">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="DocumentationMustContainValidXml">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="ElementDocumentationMustHaveSummary">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="PartialElementDocumentationMustHaveSummary">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="ElementDocumentationMustHaveSummaryText">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="PartialElementDocumentationMustHaveSummaryText">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="ElementDocumentationMustNotHaveDefaultSummary">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="ElementParametersMustBeDocumented">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="ElementParameterDocumentationMustMatchElementParameters">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="ElementParameterDocumentationMustDeclareParameterName">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="ElementParameterDocumentationMustHaveText">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="ElementReturnValueMustBeDocumented">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="ElementReturnValueDocumentationMustHaveText">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="VoidReturnValueMustNotBeDocumented">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="GenericTypeParametersMustBeDocumented">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="GenericTypeParametersMustBeDocumentedPartialClass">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="GenericTypeParameterDocumentationMustMatchTypeParameters">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="GenericTypeParameterDocumentationMustDeclareParameterName">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="GenericTypeParameterDocumentationMustHaveText">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="PropertySummaryDocumentationMustMatchAccessors">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="PropertySummaryDocumentationMustOmitSetAccessorWithRestrictedAccess">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="ElementDocumentationMustNotBeCopiedAndPasted">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="SingleLineCommentsMustNotUseDocumentationStyleSlashes">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="DocumentationTextMustNotBeEmpty">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="DocumentationTextMustContainWhitespace">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="DocumentationMustMeetCharacterPercentage">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="DocumentationTextMustMeetMinimumCharacterLength">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="IncludedDocumentationXPathDoesNotExist">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="IncludeNodeDoesNotContainValidFileAndPath">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="FileMustHaveHeader">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="FileHeaderMustShowCopyright">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="FileHeaderMustHaveCopyrightText">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ </Rules>
+ <AnalyzerSettings />
+ </Analyzer>
+ </Analyzers>
+
+ </Settings>
+
+ <SourceFile>JXmlToJsonValueConverter.cs</SourceFile>
+ <SourceFile>JsonPrimitive.cs</SourceFile>
+ <SourceFile>JsonObject.cs</SourceFile>
+ <SourceFile>JsonValue.cs</SourceFile>
+ <SourceFile>JsonValueExtensions.cs</SourceFile>
+
+ </SourceFileList>
+
+</StyleCopSettings>
diff --git a/src/System.Json/System.Json.csproj b/src/System.Json/System.Json.csproj
new file mode 100644
index 00000000..65698565
--- /dev/null
+++ b/src/System.Json/System.Json.csproj
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <CodeAnalysis Condition=" '$(CodeAnalysis)' == '' ">false</CodeAnalysis>
+ <ProductVersion>9.0.30729</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{F0441BE9-BDC0-4629-BE5A-8765FFAA2481}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>System.Json</RootNamespace>
+ <AssemblyName>System.Json</AssemblyName>
+ <TargetFrameworkProfile Condition="'$(TargetFrameworkVersion)' != 'v4.5'">Client</TargetFrameworkProfile>
+ <FileAlignment>512</FileAlignment>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>..\..\bin\Debug\</OutputPath>
+ <DefineConstants>TRACE;DEBUG;ASPNETMVC</DefineConstants>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ <DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
+ <NoWarn>1591</NoWarn>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>..\..\bin\Release\</OutputPath>
+ <DefineConstants>TRACE;ASPNETMVC</DefineConstants>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ <RunCodeAnalysis>$(CodeAnalysis)</RunCodeAnalysis>
+ <DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
+ <NoWarn>1591</NoWarn>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'CodeCoverage|AnyCPU'">
+ <DebugSymbols>true</DebugSymbols>
+ <OutputPath>..\..\bin\CodeCoverage\</OutputPath>
+ <DefineConstants>TRACE;DEBUG;CODE_COVERAGE;ASPNETMVC</DefineConstants>
+ <DebugType>full</DebugType>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="System" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.Runtime.Serialization" />
+ <Reference Include="System.Xml" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="..\CommonAssemblyInfo.cs">
+ <Link>Properties\CommonAssemblyInfo.cs</Link>
+ </Compile>
+ <Compile Include="..\RS.cs">
+ <Link>RS.cs</Link>
+ </Compile>
+ <Compile Include="..\TransparentCommonAssemblyInfo.cs">
+ <Link>Properties\TransparentCommonAssemblyInfo.cs</Link>
+ </Compile>
+ <Compile Include="GlobalSuppressions.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="Properties\Resources.Designer.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>Resources.resx</DependentUpon>
+ </Compile>
+ <Compile Include="FormUrlEncodedJson.cs" />
+ <Compile Include="JsonArray.cs" />
+ <Compile Include="JsonObject.cs" />
+ <Compile Include="JsonPrimitive.cs" />
+ <Compile Include="JsonType.cs" />
+ <Compile Include="JsonValue.cs" />
+ <Compile Include="JsonValueChange.cs" />
+ <Compile Include="JsonValueChangeEventArgs.cs" />
+ <Compile Include="JsonValueDynamicMetaObject.cs" />
+ <Compile Include="JsonValueLinqExtensions.cs" />
+ <Compile Include="JXmlToJsonValueConverter.cs" />
+ <Compile Include="NGenWrapper.cs" />
+ <Compile Include="Extensions\JsonValueExtensions.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="Properties\Resources.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>Resources.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ </ItemGroup>
+ <ItemGroup>
+ <CodeAnalysisDictionary Include="..\CodeAnalysisDictionary.xml">
+ <Link>CodeAnalysisDictionary.xml</Link>
+ </CodeAnalysisDictionary>
+ </ItemGroup>
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+</Project> \ No newline at end of file
diff --git a/src/System.Net.Http.Formatting/CloneableExtensions.cs b/src/System.Net.Http.Formatting/CloneableExtensions.cs
new file mode 100644
index 00000000..ff78d77c
--- /dev/null
+++ b/src/System.Net.Http.Formatting/CloneableExtensions.cs
@@ -0,0 +1,16 @@
+namespace System.Net.Http
+{
+ internal static class CloneableExtensions
+ {
+ /// <summary>
+ /// Convenience method for cloning objects that implement <see cref="ICloneable"/> explicitly.
+ /// </summary>
+ /// <typeparam name="T">The type of the cloneable object.</typeparam>
+ /// <param name="value">The cloneable object.</param>
+ /// <returns>The result of cloning the <paramref name="value"/>.</returns>
+ internal static T Clone<T>(this T value) where T : ICloneable
+ {
+ return (T)value.Clone();
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/ContentDispositionHeaderValueExtensions.cs b/src/System.Net.Http.Formatting/ContentDispositionHeaderValueExtensions.cs
new file mode 100644
index 00000000..14c6f9be
--- /dev/null
+++ b/src/System.Net.Http.Formatting/ContentDispositionHeaderValueExtensions.cs
@@ -0,0 +1,52 @@
+using System.IO;
+using System.Net.Http.Headers;
+
+namespace System.Net.Http
+{
+ /// <summary>
+ /// Extension methods for <see cref="ContentDispositionHeaderValue"/>.
+ /// </summary>
+ internal static class ContentDispositionHeaderValueExtensions
+ {
+ private static readonly Type _contentDispositionHeaderValueType = typeof(ContentDispositionHeaderValue);
+
+ /// <summary>
+ /// Returns a file name suitable for use on the local file system. The file name is extracted from
+ /// <see cref="ContentDispositionHeaderValue.FileNameStar"/> and <see cref="ContentDispositionHeaderValue.FileName"/>
+ /// in that order.
+ /// </summary>
+ /// <param name="contentDisposition">The content disposition to extract a local file name from.</param>
+ /// <returns>A file name (without any path components) suitable for use on local file system.</returns>
+ public static string ExtractLocalFileName(this ContentDispositionHeaderValue contentDisposition)
+ {
+ if (contentDisposition == null)
+ {
+ throw new ArgumentNullException("contentDisposition");
+ }
+
+ string candidate = contentDisposition.FileNameStar;
+ if (String.IsNullOrEmpty(candidate))
+ {
+ candidate = contentDisposition.FileName;
+ }
+
+ if (String.IsNullOrWhiteSpace(candidate))
+ {
+ throw new ArgumentException(
+ RS.Format(Properties.Resources.ContentDispositionInvalidFileName, _contentDispositionHeaderValueType.Name, candidate),
+ "contentDisposition");
+ }
+
+ string unquotedFileName = FormattingUtilities.UnquoteToken(candidate);
+ if (String.IsNullOrWhiteSpace(unquotedFileName))
+ {
+ throw new ArgumentException(
+ RS.Format(Properties.Resources.ContentDispositionInvalidFileName, _contentDispositionHeaderValueType.Name, unquotedFileName),
+ "contentDisposition");
+ }
+
+ // Get rid of all path components
+ return Path.GetFileName(unquotedFileName);
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Formatting/BufferedMediaTypeFormatter.cs b/src/System.Net.Http.Formatting/Formatting/BufferedMediaTypeFormatter.cs
new file mode 100644
index 00000000..b6d2e7e2
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Formatting/BufferedMediaTypeFormatter.cs
@@ -0,0 +1,119 @@
+using System.IO;
+using System.Net.Http.Headers;
+using System.Net.Http.Internal;
+using System.Threading.Tasks;
+
+namespace System.Net.Http.Formatting
+{
+ /// <summary>
+ /// Helper class to allow a synchronous formatter on top of the async formatter infrastructure.
+ /// This does not guarantee non-blocking threads. The only way to guarantee that we don't block a thread on IO is:
+ /// a) use the async form, or
+ /// b) fully buffer the entire write operation.
+ /// The user opted out of the async form, meaning they can tolerate potential thread blockages.
+ /// This class just tries to do smart buffering to minimize that blockage.
+ /// It also gives us a place to do future optimizations on synchronous usage.
+ /// </summary>
+ public abstract class BufferedMediaTypeFormatter : MediaTypeFormatter
+ {
+ private const int DefaultBufferSize = 16 * 1024;
+
+ private int _bufferSizeInBytes = DefaultBufferSize;
+
+ /// <summary>
+ /// Suggested size of buffer to use with streams, in bytes.
+ /// </summary>
+ public int BufferSize
+ {
+ get { return _bufferSizeInBytes; }
+ set
+ {
+ if (value < 0)
+ {
+ throw new ArgumentOutOfRangeException("value");
+ }
+ _bufferSizeInBytes = value;
+ }
+ }
+
+ // Synchronous write
+ public virtual void OnWriteToStream(Type type, object value, Stream stream, HttpContentHeaders contentHeaders, TransportContext transportContext)
+ {
+ throw new NotSupportedException(RS.Format(Properties.Resources.MediaTypeFormatterCannotWriteSync, this.GetType()));
+ }
+
+ // Synchronous read.
+ public virtual object OnReadFromStream(Type type, Stream stream, HttpContentHeaders contentHeaders, IFormatterLogger formatterLogger)
+ {
+ throw new NotSupportedException(RS.Format(Properties.Resources.MediaTypeFormatterCannotReadSync, this.GetType()));
+ }
+
+ // Sealed because derived classes shouldn't override the async version. Override sync version instead.
+ public sealed override Task WriteToStreamAsync(Type type, object value, Stream stream, HttpContentHeaders contentHeaders, TransportContext transportContext)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+
+ if (stream == null)
+ {
+ throw new ArgumentNullException("stream");
+ }
+
+ // Underlying stream will do encoding into separate sections. This is just buffering.
+ return TaskHelpers.RunSynchronously(
+ () =>
+ {
+ Stream bufferedStream = GetBufferStream(stream);
+
+ try
+ {
+ OnWriteToStream(type, value, bufferedStream, contentHeaders, transportContext);
+ }
+ finally
+ {
+ // Disposing the bufferStream will dispose the underlying stream.
+ // So Flush any remaining bytes that have been written, but don't actually close the stream.
+ bufferedStream.Flush();
+ }
+ });
+ }
+
+ public sealed override Task<object> ReadFromStreamAsync(Type type, Stream stream, HttpContentHeaders contentHeaders, IFormatterLogger formatterLogger)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+
+ if (stream == null)
+ {
+ throw new ArgumentNullException("stream");
+ }
+
+ // See explanation in OnWriteToStreamAsync.
+ return TaskHelpers.RunSynchronously<object>(
+ () =>
+ {
+ // When using a buffered read, the buffer really owns the underlying stream because it's whole purpose
+ // is to eagerly read bytes from the underlying stream.
+ // This means this reader can't cooperate with other readers (in the same way that writers can).
+ // So when this reader is done, we close the stream to prevent subsequent readers from getting random bytes.
+ using (Stream bufferedStream = GetBufferStream(stream))
+ {
+ return OnReadFromStream(type, bufferedStream, contentHeaders, formatterLogger);
+ }
+ });
+ }
+
+ private Stream GetBufferStream(Stream inner)
+ {
+ // This uses a naive buffering. BufferedStream() will block the thread while it drains the buffer.
+ // We can explore a smarter implementation that async drains the buffer.
+ Stream bufferedStream = new BufferedStream(inner, _bufferSizeInBytes);
+
+ return bufferedStream;
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Formatting/DefaultContentNegotiator.cs b/src/System.Net.Http.Formatting/Formatting/DefaultContentNegotiator.cs
new file mode 100644
index 00000000..3620b689
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Formatting/DefaultContentNegotiator.cs
@@ -0,0 +1,182 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Net.Http.Headers;
+
+namespace System.Net.Http.Formatting
+{
+ /// <summary>
+ /// Class that selects a <see cref="MediaTypeFormatter"/> for an <see cref="HttpRequestMessage"/>
+ /// or <see cref="HttpResponseMessage"/>.
+ /// </summary>
+ public class DefaultContentNegotiator : IContentNegotiator
+ {
+ /// <summary>
+ /// Performs content negotiating by selecting the most appropriate <see cref="MediaTypeFormatter"/> out of the passed in
+ /// <paramref name="formatters"/> for the given <paramref name="request"/> that can serialize an object of the given
+ /// <paramref name="type"/>.
+ /// </summary>
+ /// <param name="type">The type to be serialized.</param>
+ /// <param name="request">The request.</param>
+ /// <param name="formatters">The set of <see cref="MediaTypeFormatter"/> objects from which to choose.</param>
+ /// <param name="mediaType">The media type that is associated with the formatter chosen for serialization.</param>
+ /// <returns>The <see cref="MediaTypeFormatter"/> chosen for serialization or null if their is no appropriate formatter.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "3#", Justification = "This requirement is inherited from the interface.")]
+ public virtual MediaTypeFormatter Negotiate(Type type, HttpRequestMessage request, IEnumerable<MediaTypeFormatter> formatters, out MediaTypeHeaderValue mediaType)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+ if (request == null)
+ {
+ throw new ArgumentNullException("request");
+ }
+ if (formatters == null)
+ {
+ throw new ArgumentNullException("formatters");
+ }
+
+ MediaTypeFormatter formatter = RunNegotiation(type, request, formatters, out mediaType);
+ if (formatter != null)
+ {
+ formatter = formatter.GetPerRequestFormatterInstance(type, request, mediaType);
+ }
+ return formatter;
+ }
+
+ private static MediaTypeFormatter RunNegotiation(Type type, HttpRequestMessage request, IEnumerable<MediaTypeFormatter> formatters, out MediaTypeHeaderValue mediaType)
+ {
+ // Asking to serialize a response. This is the nominal code path.
+ // We ask all formatters for their best kind of match, and then we
+ // choose the best among those.
+ MediaTypeFormatter formatterMatchOnType = null;
+ ResponseMediaTypeMatch mediaTypeMatchOnType = null;
+
+ MediaTypeFormatter formatterMatchOnAcceptHeader = null;
+ ResponseMediaTypeMatch mediaTypeMatchOnAcceptHeader = null;
+
+ MediaTypeFormatter formatterMatchOnAcceptHeaderWithMapping = null;
+ ResponseMediaTypeMatch mediaTypeMatchOnAcceptHeaderWithMapping = null;
+
+ MediaTypeFormatter formatterMatchOnRequestContentType = null;
+ ResponseMediaTypeMatch mediaTypeMatchOnRequestContentType = null;
+
+ foreach (MediaTypeFormatter formatter in formatters)
+ {
+ ResponseMediaTypeMatch match = formatter.SelectResponseMediaType(type, request);
+ if (match == null)
+ {
+ // Null signifies no match
+ continue;
+ }
+
+ ResponseFormatterSelectionResult matchResult = match.ResponseFormatterSelectionResult;
+ switch (matchResult)
+ {
+ case ResponseFormatterSelectionResult.MatchOnCanWriteType:
+
+ // First match by type trumps all other type matches
+ if (formatterMatchOnType == null)
+ {
+ formatterMatchOnType = formatter;
+ mediaTypeMatchOnType = match;
+ }
+
+ break;
+
+ case ResponseFormatterSelectionResult.MatchOnResponseContentType:
+
+ // Match on response content trumps all other choices
+ mediaType = match.MediaTypeMatch.MediaType;
+ return formatter;
+
+ case ResponseFormatterSelectionResult.MatchOnRequestAcceptHeader:
+
+ // Matches on accept headers must choose the highest quality match
+ double thisQuality = match.MediaTypeMatch.Quality;
+ if (formatterMatchOnAcceptHeader != null)
+ {
+ double bestQualitySeen = mediaTypeMatchOnAcceptHeader.MediaTypeMatch.Quality;
+ if (thisQuality <= bestQualitySeen)
+ {
+ continue;
+ }
+ }
+
+ formatterMatchOnAcceptHeader = formatter;
+ mediaTypeMatchOnAcceptHeader = match;
+
+ break;
+
+ case ResponseFormatterSelectionResult.MatchOnRequestAcceptHeaderWithMediaTypeMapping:
+
+ // Matches on accept headers using mappings must choose the highest quality match
+ double thisMappingQuality = match.MediaTypeMatch.Quality;
+ if (mediaTypeMatchOnAcceptHeaderWithMapping != null)
+ {
+ double bestMappingQualitySeen = mediaTypeMatchOnAcceptHeaderWithMapping.MediaTypeMatch.Quality;
+ if (thisMappingQuality <= bestMappingQualitySeen)
+ {
+ continue;
+ }
+ }
+
+ formatterMatchOnAcceptHeaderWithMapping = formatter;
+ mediaTypeMatchOnAcceptHeaderWithMapping = match;
+
+ break;
+
+ case ResponseFormatterSelectionResult.MatchOnRequestContentType:
+
+ // First match on request content type trumps other request content matches
+ if (formatterMatchOnRequestContentType == null)
+ {
+ formatterMatchOnRequestContentType = formatter;
+ mediaTypeMatchOnRequestContentType = match;
+ }
+
+ break;
+ }
+ }
+
+ // If we received matches based on both supported media types and from media type mappings,
+ // we want to give precedence to the media type mappings, but only if their quality is >= that of the supported media type.
+ // We do this because media type mappings are the user's extensibility point and must take precedence over normal
+ // supported media types in the case of a tie. The 99% case is where both have quality 1.0.
+ if (mediaTypeMatchOnAcceptHeaderWithMapping != null && mediaTypeMatchOnAcceptHeader != null)
+ {
+ if (mediaTypeMatchOnAcceptHeader.MediaTypeMatch.Quality > mediaTypeMatchOnAcceptHeaderWithMapping.MediaTypeMatch.Quality)
+ {
+ formatterMatchOnAcceptHeaderWithMapping = null;
+ }
+ }
+
+ // now select the formatter and media type
+ // A MediaTypeMapping is highest precedence -- it is an extensibility point
+ // allowing the user to override normal accept header matching
+ if (formatterMatchOnAcceptHeaderWithMapping != null)
+ {
+ mediaType = mediaTypeMatchOnAcceptHeaderWithMapping.MediaTypeMatch.MediaType;
+ return formatterMatchOnAcceptHeaderWithMapping;
+ }
+ else if (formatterMatchOnAcceptHeader != null)
+ {
+ mediaType = mediaTypeMatchOnAcceptHeader.MediaTypeMatch.MediaType;
+ return formatterMatchOnAcceptHeader;
+ }
+ else if (formatterMatchOnRequestContentType != null)
+ {
+ mediaType = mediaTypeMatchOnRequestContentType.MediaTypeMatch.MediaType;
+ return formatterMatchOnRequestContentType;
+ }
+ else if (formatterMatchOnType != null)
+ {
+ mediaType = mediaTypeMatchOnType.MediaTypeMatch.MediaType;
+ return formatterMatchOnType;
+ }
+
+ mediaType = null;
+ return null;
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Formatting/DelegatingEnumerable.cs b/src/System.Net.Http.Formatting/Formatting/DelegatingEnumerable.cs
new file mode 100644
index 00000000..8f6efde6
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Formatting/DelegatingEnumerable.cs
@@ -0,0 +1,66 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+
+namespace System.Net.Http.Formatting
+{
+ /// <summary>
+ /// Helper class to serialize <see cref="IEnumerable{T}"/> types by delegating them through a concrete implementation."/>.
+ /// </summary>
+ /// <typeparam name="T">The interface implementing <see cref="IEnumerable{T}"/> to proxy.</typeparam>
+ [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix", Justification = "Enumerable conveys the meaning of collection")]
+ public sealed class DelegatingEnumerable<T> : IEnumerable<T>
+ {
+ private IEnumerable<T> _source;
+
+ /// <summary>
+ /// Initialize a DelegatingEnumerable. This constructor is necessary for <see cref="System.Runtime.Serialization.DataContractSerializer"/> to work.
+ /// </summary>
+ public DelegatingEnumerable()
+ {
+ _source = Enumerable.Empty<T>();
+ }
+
+ /// <summary>
+ /// Initialize a DelegatingEnumerable with an <see cref="IEnumerable{T}"/>. This is a helper class to proxy <see cref="IEnumerable{T}"/> interfaces for <see cref="System.Xml.Serialization.XmlSerializer"/>.
+ /// </summary>
+ /// <param name="source">The <see cref="IEnumerable{T}"/> instance to get the enumerator from.</param>
+ public DelegatingEnumerable(IEnumerable<T> source)
+ {
+ if (source == null)
+ {
+ throw new ArgumentNullException("source");
+ }
+ _source = source;
+ }
+
+ /// <summary>
+ /// Get the enumerator of the associated <see cref="IEnumerable{T}"/>.
+ /// </summary>
+ /// <returns>The enumerator of the <see cref="IEnumerable{T}"/> source.</returns>
+ public IEnumerator<T> GetEnumerator()
+ {
+ return _source.GetEnumerator();
+ }
+
+ /// <summary>
+ /// This method is not implemented but is required method for serialization to work. Do not use.
+ /// </summary>
+ /// <param name="item">The item to add. Unused.</param>
+ [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", Justification = "Required by XmlSerializer, never used.")]
+ public void Add(object item)
+ {
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// Get the enumerator of the associated <see cref="IEnumerable{T}"/>.
+ /// </summary>
+ /// <returns>The enumerator of the <see cref="IEnumerable{T}"/> source.</returns>
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return _source.GetEnumerator();
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Formatting/FormDataCollection.cs b/src/System.Net.Http.Formatting/Formatting/FormDataCollection.cs
new file mode 100644
index 00000000..ea5fcbb8
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Formatting/FormDataCollection.cs
@@ -0,0 +1,122 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Net.Http.Formatting.Parsers;
+
+namespace System.Net.Http.Formatting
+{
+ /// <summary>
+ /// Represent the form data.
+ /// - This has 100% fidelity (including ordering, which is important for deserializing ordered array).
+ /// - using interfaces allows us to optimize the implementation. Eg, we can avoid eagerly string-splitting a 10gb file.
+ /// - This also provides a convenient place to put extension methods.
+ /// </summary>
+ public class FormDataCollection : IEnumerable<KeyValuePair<string, string>>
+ {
+ private readonly IEnumerable<KeyValuePair<string, string>> _pairs;
+ private NameValueCollection _nameValueCollection;
+
+ /// <summary>
+ /// Initialize a form collection around incoming data.
+ /// The key value enumeration should be immutable.
+ /// </summary>
+ /// <param name="pairs">incoming set of key value pairs. Ordering is preserved.</param>
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is the convention for representing FormData")]
+ public FormDataCollection(IEnumerable<KeyValuePair<string, string>> pairs)
+ {
+ if (pairs == null)
+ {
+ throw new ArgumentNullException("pairs");
+ }
+ _pairs = pairs;
+ }
+
+ /// <summary>
+ /// Initialize a form collection from a query string.
+ /// Uri and FormURl body have the same schema.
+ /// </summary>
+ /// <param name="uri"></param>
+ public FormDataCollection(Uri uri)
+ {
+ if (uri == null)
+ {
+ throw new ArgumentNullException("uri");
+ }
+
+ string query = uri.Query;
+ if (query.Length > 0 && query[0] == '?')
+ {
+ query = query.Substring(1);
+ }
+
+ byte[] bytes = System.Text.Encoding.UTF8.GetBytes(query);
+
+ List<KeyValuePair<string, string>> result = new List<KeyValuePair<string, string>>();
+ FormUrlEncodedParser parser = new FormUrlEncodedParser(result, Int64.MaxValue);
+
+ int bytesConsumed = 0;
+ ParserState state = parser.ParseBuffer(bytes, bytes.Length, ref bytesConsumed, isFinal: true);
+
+ if (state != ParserState.Done)
+ {
+ throw new InvalidOperationException(RS.Format(Properties.Resources.FormUrlEncodedParseError, bytesConsumed));
+ }
+
+ _pairs = result;
+ }
+
+ /// <summary>
+ /// Get the collection as a NameValueCollection.
+ /// Beware this loses some ordering. Values are ordered within a key,
+ /// but keys are no longer ordered against each other.
+ /// </summary>
+ public NameValueCollection ReadAsNameValueCollection()
+ {
+ if (_nameValueCollection == null)
+ {
+ // Ordering example:
+ // k=A&j=B&k=C --> k:[A,C];j=[B].
+ NameValueCollection nvc = new NameValueCollection();
+ foreach (KeyValuePair<string, string> kv in this)
+ {
+ string key = kv.Key;
+ nvc.Add(key, kv.Value);
+ }
+
+ // Initialize in a private collection to be thread-safe, and swap the finished object.
+ // Ok to double initialize this.
+ _nameValueCollection = nvc;
+ }
+ return _nameValueCollection;
+ }
+
+ /// <summary>
+ /// Get values associated with a given key. If there are multiple values, they're concatenated.
+ /// </summary>
+ public string Get(string key)
+ {
+ return ReadAsNameValueCollection().Get(key);
+ }
+
+ /// <summary>
+ /// Get a value associated with a given key.
+ /// </summary>
+ public string[] GetValues(string key)
+ {
+ return ReadAsNameValueCollection().GetValues(key);
+ }
+
+ public IEnumerator<KeyValuePair<string, string>> GetEnumerator()
+ {
+ return _pairs.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ IEnumerable ie = _pairs;
+ return ie.GetEnumerator();
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Formatting/FormUrlEncodedMediaTypeFormatter.cs b/src/System.Net.Http.Formatting/Formatting/FormUrlEncodedMediaTypeFormatter.cs
new file mode 100644
index 00000000..872da2f7
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Formatting/FormUrlEncodedMediaTypeFormatter.cs
@@ -0,0 +1,215 @@
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Diagnostics.Contracts;
+using System.IO;
+using System.Json;
+using System.Net.Http.Formatting.Parsers;
+using System.Net.Http.Headers;
+using System.Net.Http.Internal;
+using System.Runtime.Serialization.Json;
+using System.Threading.Tasks;
+
+namespace System.Net.Http.Formatting
+{
+ /// <summary>
+ /// <see cref="MediaTypeFormatter"/> class for handling HTML form URL-ended data, also known as <c>application/x-www-form-urlencoded</c>.
+ /// </summary>
+ public class FormUrlEncodedMediaTypeFormatter : MediaTypeFormatter
+ {
+ private const int MinBufferSize = 256;
+ private const int DefaultBufferSize = 32 * 1024;
+
+ private static readonly MediaTypeHeaderValue[] _supportedMediaTypes = new MediaTypeHeaderValue[]
+ {
+ MediaTypeConstants.ApplicationFormUrlEncodedMediaType
+ };
+
+ private int _readBufferSize = DefaultBufferSize;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="FormUrlEncodedMediaTypeFormatter"/> class.
+ /// </summary>
+ public FormUrlEncodedMediaTypeFormatter()
+ {
+ foreach (MediaTypeHeaderValue value in _supportedMediaTypes)
+ {
+ SupportedMediaTypes.Add(value);
+ }
+ }
+
+ /// <summary>
+ /// Gets the default media type for HTML Form URL encoded data, namely <c>application/x-www-form-urlencoded</c>.
+ /// </summary>
+ /// <value>
+ /// Because <see cref="MediaTypeHeaderValue"/> is mutable, the value
+ /// returned will be a new instance every time.
+ /// </value>
+ public static MediaTypeHeaderValue DefaultMediaType
+ {
+ get { return MediaTypeConstants.ApplicationFormUrlEncodedMediaType; }
+ }
+
+ /// <summary>
+ /// Gets or sets the size of the buffer when reading the incoming stream.
+ /// </summary>
+ /// <value>
+ /// The size of the read buffer.
+ /// </value>
+ public int ReadBufferSize
+ {
+ get { return _readBufferSize; }
+
+ set
+ {
+ if (value < MinBufferSize)
+ {
+ throw new ArgumentException(RS.Format(Properties.Resources.MinParameterSize, MinBufferSize), "value");
+ }
+
+ _readBufferSize = value;
+ }
+ }
+
+ /// <summary>
+ /// Determines whether this <see cref="FormUrlEncodedMediaTypeFormatter"/> can read objects
+ /// of the specified <paramref name="type"/>.
+ /// </summary>
+ /// <param name="type">The type of object that will be read.</param>
+ /// <returns><c>true</c> if objects of this <paramref name="type"/> can be read, otherwise <c>false</c>.</returns>
+ public override bool CanReadType(Type type)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+
+ // Can't read arbitrary types.
+ return type == typeof(FormDataCollection) || type == typeof(IKeyValueModel) || FormattingUtilities.IsJsonValueType(type);
+ }
+
+ /// <summary>
+ /// Determines whether this <see cref="FormUrlEncodedMediaTypeFormatter"/> can write objects
+ /// of the specified <paramref name="type"/>.
+ /// </summary>
+ /// <param name="type">The type of object that will be written.</param>
+ /// <returns><c>true</c> if objects of this <paramref name="type"/> can be written, otherwise <c>false</c>.</returns>
+ public override bool CanWriteType(Type type)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Wrap a stream to limit the number of potential keys in the deserialized object.
+ /// </summary>
+ private static Stream WrapReadStream(Stream stream)
+ {
+ if (SkipStreamLimitChecks)
+ {
+ return stream;
+ }
+ byte delimeter = (byte)'&'; // delimiter for form-url encoding keys
+ return new ThresholdStream(stream, delimeter);
+ }
+
+ /// <summary>
+ /// Called during deserialization to read an object of the specified <paramref name="type"/>
+ /// from the specified <paramref name="stream"/>.
+ /// </summary>
+ /// <param name="type">The type of object to read.</param>
+ /// <param name="stream">The <see cref="Stream"/> from which to read.</param>
+ /// <param name="contentHeaders">The <see cref="HttpContentHeaders"/> for the content being read.</param>
+ /// <param name="formatterLogger">The <see cref="IFormatterLogger"/> to log events to.</param>
+ /// <returns>A <see cref="Task"/> whose result will be the object instance that has been read.</returns>
+ public override Task<object> ReadFromStreamAsync(Type type, Stream stream, HttpContentHeaders contentHeaders, IFormatterLogger formatterLogger)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+
+ if (stream == null)
+ {
+ throw new ArgumentNullException("stream");
+ }
+
+ stream = WrapReadStream(stream);
+
+ return TaskHelpers.RunSynchronously<object>(() =>
+ {
+ IEnumerable<KeyValuePair<string, string>> nameValuePairs = ReadFormUrlEncoded(stream, ReadBufferSize);
+
+ if (type == typeof(FormDataCollection))
+ {
+ return new FormDataCollection(nameValuePairs);
+ }
+
+ JsonObject jsonObject = FormUrlEncodedJson.Parse(nameValuePairs);
+ if (FormattingUtilities.IsJsonValueType(type))
+ {
+ return jsonObject;
+ }
+ if (type == typeof(IKeyValueModel))
+ {
+ return new JsonKeyValueModel(jsonObject);
+ }
+
+ // Passed us an unsupported type. Should have called CanReadType() first.
+ throw new InvalidOperationException(
+ RS.Format(Properties.Resources.SerializerCannotSerializeType, GetType().Name, type.Name));
+ });
+ }
+
+ /// <summary>
+ /// Reads all name-value pairs encoded as HTML Form URL encoded data and add them to
+ /// a collection as UNescaped URI strings.
+ /// </summary>
+ /// <param name="input">Stream to read from.</param>
+ /// <param name="bufferSize">Size of the buffer used to read the contents.</param>
+ /// <returns>Collection of name-value pairs.</returns>
+ private static IEnumerable<KeyValuePair<string, string>> ReadFormUrlEncoded(Stream input, int bufferSize)
+ {
+ Contract.Assert(input != null, "input stream cannot be null");
+ Contract.Assert(bufferSize >= MinBufferSize, "buffer size cannot be less than MinBufferSize");
+
+ byte[] data = new byte[bufferSize];
+ int bytesConsumed = 0;
+ int bytesRead;
+ bool isFinal = false;
+ List<KeyValuePair<string, string>> result = new List<KeyValuePair<string, string>>();
+ FormUrlEncodedParser parser = new FormUrlEncodedParser(result, Int64.MaxValue);
+ ParserState state;
+
+ while (true)
+ {
+ try
+ {
+ bytesRead = input.Read(data, 0, data.Length);
+ if (bytesRead == 0)
+ {
+ isFinal = true;
+ }
+ }
+ catch (Exception e)
+ {
+ throw new IOException(Properties.Resources.ErrorReadingFormUrlEncodedStream, e);
+ }
+
+ state = parser.ParseBuffer(data, bytesRead, ref bytesConsumed, isFinal);
+ if (state != ParserState.NeedMoreData && state != ParserState.Done)
+ {
+ throw new IOException(RS.Format(Properties.Resources.FormUrlEncodedParseError, bytesConsumed));
+ }
+
+ if (isFinal)
+ {
+ return result;
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Formatting/IContentNegotiator.cs b/src/System.Net.Http.Formatting/Formatting/IContentNegotiator.cs
new file mode 100644
index 00000000..21a50ed4
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Formatting/IContentNegotiator.cs
@@ -0,0 +1,30 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Net.Http.Headers;
+
+namespace System.Net.Http.Formatting
+{
+ /// <summary>
+ /// Performs content negotiation.
+ /// This is the process of selecting a response writer (formatter) in compliance with header values in the request.
+ /// </summary>
+ public interface IContentNegotiator
+ {
+ /// <summary>
+ /// Performs content negotiating by selecting the most appropriate <see cref="MediaTypeFormatter"/> out of the passed in
+ /// <paramref name="formatters"/> for the given <paramref name="request"/> that can serialize an object of the given
+ /// <paramref name="type"/>.
+ /// </summary>
+ /// <remarks>
+ /// Implementations of this method should call <see cref="MediaTypeFormatter.GetPerRequestFormatterInstance(Type, HttpRequestMessage, MediaTypeHeaderValue)"/>
+ /// on the selected <see cref="MediaTypeFormatter">formatter</see> and return the result of that method.
+ /// </remarks>
+ /// <param name="type">The type to be serialized.</param>
+ /// <param name="request">Request message, which contains the header values used to negotiate with.</param>
+ /// <param name="formatters">The set of <see cref="MediaTypeFormatter"/> objects from which to choose.</param>
+ /// <param name="mediaType">The media type that is associated with the formatter chosen for serialization.</param>
+ /// <returns>The <see cref="MediaTypeFormatter"/> chosen for serialization or <c>null</c> if their is no appropriate formatter.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "3#", Justification = "The out parameter is necessary for this API.")]
+ MediaTypeFormatter Negotiate(Type type, HttpRequestMessage request, IEnumerable<MediaTypeFormatter> formatters, out MediaTypeHeaderValue mediaType);
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Formatting/IFormatterLogger.cs b/src/System.Net.Http.Formatting/Formatting/IFormatterLogger.cs
new file mode 100644
index 00000000..42665a98
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Formatting/IFormatterLogger.cs
@@ -0,0 +1,15 @@
+namespace System.Net.Http.Formatting
+{
+ /// <summary>
+ /// Interface to log events that occur during formatter reads.
+ /// </summary>
+ public interface IFormatterLogger
+ {
+ /// <summary>
+ /// Logs an error.
+ /// </summary>
+ /// <param name="errorPath">The path to the member for which the error is being logged.</param>
+ /// <param name="errorMessage">The error message to be logged.</param>
+ void LogError(string errorPath, string errorMessage);
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Formatting/IKeyValueModel.cs b/src/System.Net.Http.Formatting/Formatting/IKeyValueModel.cs
new file mode 100644
index 00000000..c541c22c
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Formatting/IKeyValueModel.cs
@@ -0,0 +1,31 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Net.Http.Formatting
+{
+ /// <summary>
+ /// Interface to provide a key/value model of an object graph.
+ /// </summary>
+ /// <remarks>
+ /// This interface is used primarily to provide a simple key/value facade over
+ /// richer DOM's such as <see cref="T:System.Xml.Linq.XElement"/> or
+ /// <see cref="T:System.Json.JsonValue"/>
+ /// </remarks>
+ public interface IKeyValueModel
+ {
+ /// <summary>
+ /// Gets all the keys for all the values.
+ /// </summary>
+ /// <returns>The set of all keys.</returns>
+ IEnumerable<string> Keys { get; }
+
+ /// <summary>
+ /// Attempts to retrieve the value associated with the given key.
+ /// </summary>
+ /// <param name="key">The key of the value to retrieve.</param>
+ /// <param name="value">The value associated with that key.</param>
+ /// <returns><c>If there was a value associated with that key</c></returns>
+ [SuppressMessage("Microsoft.Design", "CA1007:UseGenericsWhereAppropriate", Justification = "This is a non-generic API.")]
+ bool TryGetValue(string key, out object value);
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Formatting/IRequiredMemberSelector.cs b/src/System.Net.Http.Formatting/Formatting/IRequiredMemberSelector.cs
new file mode 100644
index 00000000..f13ef3ee
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Formatting/IRequiredMemberSelector.cs
@@ -0,0 +1,17 @@
+using System.Reflection;
+
+namespace System.Net.Http.Formatting
+{
+ /// <summary>
+ /// Interface to determine which data members on a particular type are required.
+ /// </summary>
+ public interface IRequiredMemberSelector
+ {
+ /// <summary>
+ /// Determines whether a given member is required on deserialization.
+ /// </summary>
+ /// <param name="member">The <see cref="MemberInfo"/> that will be deserialized.</param>
+ /// <returns><c>true</c> if <paramref name="member"/> should be treated as a required member, otherwise <c>false</c>.</returns>
+ bool IsRequiredMember(MemberInfo member);
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Formatting/JsonContractResolver.cs b/src/System.Net.Http.Formatting/Formatting/JsonContractResolver.cs
new file mode 100644
index 00000000..71da9f0c
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Formatting/JsonContractResolver.cs
@@ -0,0 +1,68 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.Serialization;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Serialization;
+
+namespace System.Net.Http.Formatting
+{
+ // Contract resolver to handle types that DCJS supports, but Json.NET doesn't support out of the box (like [Serializable])
+ internal class JsonContractResolver : DefaultContractResolver
+ {
+ private const BindingFlags AllInstanceMemberFlag = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
+
+ protected override JsonObjectContract CreateObjectContract(Type type)
+ {
+ JsonObjectContract contract = base.CreateObjectContract(type);
+
+ // Handling [Serializable] types
+ if (type.IsSerializable && !IsTypeNullable(type) && !IsTypeDataContract(type) && !IsTypeJsonObject(type))
+ {
+ contract.Properties.Clear();
+ foreach (JsonProperty property in CreateSerializableJsonProperties(type))
+ {
+ contract.Properties.Add(property);
+ }
+ }
+ return contract;
+ }
+
+ private static IEnumerable<JsonProperty> CreateSerializableJsonProperties(Type type)
+ {
+ return type.GetFields(AllInstanceMemberFlag)
+ .Where(field => !field.IsNotSerialized)
+ .Select(field => PrivateMemberContractResolver.Instance.CreatePrivateProperty(field, MemberSerialization.OptOut));
+ }
+
+ private static bool IsTypeNullable(Type type)
+ {
+ return Nullable.GetUnderlyingType(type) != null;
+ }
+
+ private static bool IsTypeDataContract(Type type)
+ {
+ return type.GetCustomAttributes(typeof(DataContractAttribute), false).Length > 0;
+ }
+
+ private static bool IsTypeJsonObject(Type type)
+ {
+ return type.GetCustomAttributes(typeof(JsonObjectAttribute), false).Length > 0;
+ }
+
+ private class PrivateMemberContractResolver : DefaultContractResolver
+ {
+ internal static PrivateMemberContractResolver Instance = new PrivateMemberContractResolver();
+
+ internal PrivateMemberContractResolver()
+ {
+ DefaultMembersSearchFlags = JsonContractResolver.AllInstanceMemberFlag;
+ }
+
+ internal JsonProperty CreatePrivateProperty(MemberInfo member, MemberSerialization memberSerialization)
+ {
+ return CreateProperty(member, memberSerialization);
+ }
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Formatting/JsonKeyValueModel.cs b/src/System.Net.Http.Formatting/Formatting/JsonKeyValueModel.cs
new file mode 100644
index 00000000..ca901b38
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Formatting/JsonKeyValueModel.cs
@@ -0,0 +1,131 @@
+using System.Collections.Generic;
+using System.Diagnostics.Contracts;
+using System.Json;
+
+namespace System.Net.Http.Formatting
+{
+ /// <summary>
+ /// Class that provides an <see cref="IKeyValueModel"/> facade over
+ /// a <see cref="JsonValue"/>.
+ /// </summary>
+ internal class JsonKeyValueModel : IKeyValueModel
+ {
+ private readonly Dictionary<string, object> _valuesByPrefix = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
+ private JsonValue _innerJsonValue;
+
+ /// <summary>
+ /// Creates an instance of the <see cref="JsonKeyValueModel"/> class.
+ /// </summary>
+ /// <param name="jsonValue">The <see cref="JsonValue"/> from which to extract the keys and values</param>
+ public JsonKeyValueModel(JsonValue jsonValue)
+ {
+ Contract.Assert(jsonValue != null, "jsonValue cannot be null.");
+
+ _innerJsonValue = jsonValue;
+ CreateValuesByPrefix();
+ }
+
+ /// <summary>
+ /// Gets all the keys for all the values.
+ /// </summary>
+ /// <returns>The set of all keys.</returns>
+ public IEnumerable<string> Keys
+ {
+ get { return _valuesByPrefix.Keys; }
+ }
+
+ /// <summary>
+ /// Attempts to retrieve the value associated with the given key.
+ /// </summary>
+ /// <param name="key">The key of the value to retrieve.</param>
+ /// <param name="value">The value associated with that key.</param>
+ /// <returns>
+ /// <c>If there was a value associated with that key</c>
+ /// </returns>
+ public bool TryGetValue(string key, out object value)
+ {
+ if (key == null)
+ {
+ throw new ArgumentNullException("key");
+ }
+
+ return _valuesByPrefix.TryGetValue(key, out value);
+ }
+
+ private static object GetJsonValueContent(JsonValue jsonValue)
+ {
+ Contract.Assert(jsonValue.JsonType != JsonType.Array && jsonValue.JsonType != JsonType.Object);
+
+ switch (jsonValue.JsonType)
+ {
+ case JsonType.Boolean:
+ return jsonValue.ReadAs<bool>();
+
+ case JsonType.Default:
+ return jsonValue.ReadAs<object>();
+
+ case JsonType.Number:
+ // preserve numbers as strings and let model binding convert based on culture
+ return jsonValue.ReadAs<string>();
+
+ case JsonType.String:
+ return jsonValue.ReadAs<string>();
+
+ default:
+ Contract.Assert(false, "unknown JsonType");
+ return null;
+ }
+ }
+
+ private void CreateValuesByPrefix()
+ {
+ // A single value has no key name
+ if (_innerJsonValue.JsonType != JsonType.Array && _innerJsonValue.JsonType != JsonType.Object)
+ {
+ _valuesByPrefix[String.Empty] = GetJsonValueContent(_innerJsonValue);
+ }
+ else
+ {
+ if (_innerJsonValue.JsonType != JsonType.Array)
+ {
+ // if we are a complex object and not an array, we need to set this so
+ // that the collection modelbinder can bind a single object non-collection
+ // to a collection parameter (array or IEnumerable)
+ _valuesByPrefix[String.Empty] = String.Empty;
+ }
+
+ ExpandJsonValue(null, _innerJsonValue);
+ }
+ }
+
+ private void ExpandJsonValue(string currentPrefix, JsonValue jsonValue)
+ {
+ bool isJsonArray = jsonValue.JsonType == JsonType.Array;
+
+ foreach (KeyValuePair<string, JsonValue> pair in jsonValue)
+ {
+ string seperator = (isJsonArray || currentPrefix == null) ? String.Empty : "."; // no seperator if we are an array or we are the outermost object (i[0] or rootnode)
+ string currentSuffix = isJsonArray ? '[' + pair.Key + ']' : pair.Key; // use square brackets for indexing arrays
+
+ string thisPrefix = currentPrefix + seperator + currentSuffix; // foo + . + bar , foo + + [0]
+
+ JsonValue thisJsonValue = pair.Value;
+ if (thisJsonValue == null)
+ {
+ _valuesByPrefix[thisPrefix] = null;
+ }
+ else
+ {
+ if (thisJsonValue.JsonType != JsonType.Object && thisJsonValue.JsonType != JsonType.Array)
+ {
+ _valuesByPrefix[thisPrefix] = GetJsonValueContent(thisJsonValue);
+ }
+ else
+ {
+ ExpandJsonValue(thisPrefix, thisJsonValue);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Formatting/JsonMediaTypeFormatter.cs b/src/System.Net.Http.Formatting/Formatting/JsonMediaTypeFormatter.cs
new file mode 100644
index 00000000..90123b6b
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Formatting/JsonMediaTypeFormatter.cs
@@ -0,0 +1,447 @@
+using System.Collections;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+using System.IO;
+using System.Json;
+using System.Net.Http.Headers;
+using System.Net.Http.Internal;
+using System.Runtime.Serialization.Json;
+using System.Text;
+using System.Threading.Tasks;
+using System.Xml;
+using Newtonsoft.Json;
+
+namespace System.Net.Http.Formatting
+{
+ /// <summary>
+ /// <see cref="MediaTypeFormatter"/> class to handle Json.
+ /// </summary>
+ public class JsonMediaTypeFormatter : MediaTypeFormatter
+ {
+ private const int DefaultMaxDepth = 1024;
+ private static readonly MediaTypeHeaderValue[] _supportedMediaTypes = new MediaTypeHeaderValue[]
+ {
+ MediaTypeConstants.ApplicationJsonMediaType,
+ MediaTypeConstants.TextJsonMediaType
+ };
+ private JsonSerializer _jsonSerializer = CreateDefaultSerializer();
+ private int _maxDepth = DefaultMaxDepth;
+ private XmlDictionaryReaderQuotas _readerQuotas = CreateDefaultReaderQuotas();
+
+ // Encoders used for reading data based on charset parameter and default encoder doesn't match
+ private readonly Dictionary<string, Encoding> _decoders = new Dictionary<string, Encoding>(StringComparer.OrdinalIgnoreCase)
+ {
+ { Encoding.UTF8.WebName, new UTF8Encoding(false, true) },
+ { Encoding.Unicode.WebName, new UnicodeEncoding(false, true, true) },
+ };
+
+ private ConcurrentDictionary<Type, DataContractJsonSerializer> _dataContractSerializerCache = new ConcurrentDictionary<Type, DataContractJsonSerializer>();
+ private RequestHeaderMapping _requestHeaderMapping;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="JsonMediaTypeFormatter"/> class.
+ /// </summary>
+ public JsonMediaTypeFormatter()
+ {
+ Encoding = new UTF8Encoding(false, true);
+ foreach (MediaTypeHeaderValue value in _supportedMediaTypes)
+ {
+ SupportedMediaTypes.Add(value);
+ }
+
+ _requestHeaderMapping = new XHRRequestHeaderMapping();
+ MediaTypeMappings.Add(_requestHeaderMapping);
+ }
+
+ /// <summary>
+ /// Gets the default media type for Json, namely "application/json".
+ /// </summary>
+ /// <remarks>
+ /// The default media type does not have any <c>charset</c> parameter as
+ /// the <see cref="Encoding"/> can be configured on a per <see cref="JsonMediaTypeFormatter"/>
+ /// instance basis.
+ /// </remarks>
+ /// <value>
+ /// Because <see cref="MediaTypeHeaderValue"/> is mutable, the value
+ /// returned will be a new instance every time.
+ /// </value>
+ public static MediaTypeHeaderValue DefaultMediaType
+ {
+ get { return MediaTypeConstants.ApplicationJsonMediaType; }
+ }
+
+ /// <summary>
+ /// Gets or sets the <see cref="Encoding"/> to use when writing data.
+ /// </summary>
+ /// <remarks>The default encoding is <see cref="UTF8Encoding"/>.</remarks>
+ /// <value>
+ /// The <see cref="Encoding"/> to use when writing data.
+ /// </value>
+ public Encoding CharacterEncoding
+ {
+ get { return Encoding; }
+
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException("value");
+ }
+
+ Type valueType = value.GetType();
+ if (FormattingUtilities.Utf8EncodingType.IsAssignableFrom(valueType) || FormattingUtilities.Utf16EncodingType.IsAssignableFrom(valueType))
+ {
+ Encoding = value;
+ return;
+ }
+
+ throw new ArgumentException(
+ RS.Format(Properties.Resources.UnsupportedEncoding, typeof(JsonMediaTypeFormatter).Name, FormattingUtilities.Utf8EncodingType.Name, FormattingUtilities.Utf16EncodingType.Name), "value");
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the <see cref="JsonSerializer"/> used for Json.
+ /// </summary>
+ public JsonSerializer Serializer
+ {
+ get
+ {
+ return _jsonSerializer;
+ }
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException("value");
+ }
+
+ _jsonSerializer = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to use <see cref="DataContractJsonSerializer"/> by default.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if use <see cref="DataContractJsonSerializer"/> by default; otherwise, <c>false</c>. The default is <c>false</c>.
+ /// </value>
+ public bool UseDataContractJsonSerializer { get; set; }
+
+ /// <summary>
+ /// Gets or sets the maximum depth allowed by this formatter.
+ /// </summary>
+ public int MaxDepth
+ {
+ get
+ {
+ return _maxDepth;
+ }
+ set
+ {
+ _maxDepth = value;
+ _readerQuotas.MaxDepth = value;
+ }
+ }
+
+ /// <summary>
+ /// Creates a <see cref="JsonSerializer"/> with the default settings used by the <see cref="JsonMediaTypeFormatter"/>.
+ /// </summary>
+ public static JsonSerializer CreateDefaultSerializer()
+ {
+ JsonSerializer defaultSerializer = new JsonSerializer();
+ defaultSerializer.ContractResolver = new JsonContractResolver();
+ defaultSerializer.Converters.Add(new JsonValueConverter());
+
+ // Do not change this setting
+ // Setting this to None prevents Json.NET from loading malicious, unsafe, or security-sensitive types
+ defaultSerializer.TypeNameHandling = TypeNameHandling.None;
+ return defaultSerializer;
+ }
+
+ private static XmlDictionaryReaderQuotas CreateDefaultReaderQuotas()
+ {
+ return new XmlDictionaryReaderQuotas()
+ {
+ MaxArrayLength = int.MaxValue,
+ MaxBytesPerRead = int.MaxValue,
+ MaxDepth = DefaultMaxDepth,
+ MaxNameTableCharCount = int.MaxValue,
+ MaxStringContentLength = int.MaxValue
+ };
+ }
+
+ internal bool ContainsSerializerForType(Type type)
+ {
+ return _dataContractSerializerCache.ContainsKey(type);
+ }
+
+ /// <summary>
+ /// Determines whether this <see cref="JsonMediaTypeFormatter"/> can read objects
+ /// of the specified <paramref name="type"/>.
+ /// </summary>
+ /// <param name="type">The type of object that will be read.</param>
+ /// <returns><c>true</c> if objects of this <paramref name="type"/> can be read, otherwise <c>false</c>.</returns>
+ public override bool CanReadType(Type type)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+
+ if (FormattingUtilities.IsJsonValueType(type))
+ {
+ return true;
+ }
+
+ if (type == typeof(IKeyValueModel))
+ {
+ return true;
+ }
+
+ if (UseDataContractJsonSerializer)
+ {
+ // If there is a registered non-null serializer, we can support this type.
+ DataContractJsonSerializer serializer =
+ _dataContractSerializerCache.GetOrAdd(type, (t) => CreateDataContractSerializer(t));
+
+ // Null means we tested it before and know it is not supported
+ return serializer != null;
+ }
+ else
+ {
+ return true;
+ }
+ }
+
+ /// <summary>
+ /// Determines whether this <see cref="JsonMediaTypeFormatter"/> can write objects
+ /// of the specified <paramref name="type"/>.
+ /// </summary>
+ /// <param name="type">The type of object that will be written.</param>
+ /// <returns><c>true</c> if objects of this <paramref name="type"/> can be written, otherwise <c>false</c>.</returns>
+ public override bool CanWriteType(Type type)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+
+ if (FormattingUtilities.IsJsonValueType(type))
+ {
+ return true;
+ }
+
+ if (UseDataContractJsonSerializer)
+ {
+ MediaTypeFormatter.TryGetDelegatingTypeForIQueryableGenericOrSame(ref type);
+
+ // If there is a registered non-null serializer, we can support this type.
+ object serializer =
+ _dataContractSerializerCache.GetOrAdd(type, (t) => CreateDataContractSerializer(t));
+
+ // Null means we tested it before and know it is not supported
+ return serializer != null;
+ }
+ else
+ {
+ return true;
+ }
+ }
+
+ /// <summary>
+ /// Wrap a stream to limit the number of potential keys in the deserialized object.
+ /// </summary>
+ private static Stream WrapReadStream(Stream stream)
+ {
+ if (SkipStreamLimitChecks)
+ {
+ return stream;
+ }
+ byte delimiter = (byte)':'; // delimiter for JSON key-value pairs.
+ return new ThresholdStream(stream, delimiter);
+ }
+
+ /// <summary>
+ /// Called during deserialization to read an object of the specified <paramref name="type"/>
+ /// from the specified <paramref name="stream"/>.
+ /// </summary>
+ /// <param name="type">The type of object to read.</param>
+ /// <param name="stream">The <see cref="Stream"/> from which to read.</param>
+ /// <param name="contentHeaders">The <see cref="HttpContentHeaders"/> for the content being written.</param>
+ /// <param name="formatterLogger">The <see cref="IFormatterLogger"/> to log events to.</param>
+ /// <returns>A <see cref="Task"/> whose result will be the object instance that has been read.</returns>
+ public override Task<object> ReadFromStreamAsync(Type type, Stream stream, HttpContentHeaders contentHeaders, IFormatterLogger formatterLogger)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+
+ if (stream == null)
+ {
+ throw new ArgumentNullException("stream");
+ }
+
+ stream = WrapReadStream(stream);
+
+ return TaskHelpers.RunSynchronously<object>(() =>
+ {
+ if (type == typeof(IKeyValueModel))
+ {
+ return new JsonKeyValueModel(JsonValue.Load(stream));
+ }
+ else
+ {
+ Encoding effectiveEncoding = Encoding;
+
+ if (contentHeaders != null && contentHeaders.ContentType != null)
+ {
+ string charset = contentHeaders.ContentType.CharSet;
+ if (!String.IsNullOrWhiteSpace(charset) &&
+ !String.Equals(charset, Encoding.WebName) &&
+ !_decoders.TryGetValue(charset, out effectiveEncoding))
+ {
+ effectiveEncoding = Encoding;
+ }
+ }
+
+ if (FormattingUtilities.IsJsonValueType(type) || !UseDataContractJsonSerializer)
+ {
+ using (JsonTextReader jsonTextReader = new SecureJsonTextReader(new StreamReader(stream, effectiveEncoding), _maxDepth))
+ {
+ return _jsonSerializer.Deserialize(jsonTextReader, type);
+ }
+ }
+ else
+ {
+ DataContractJsonSerializer dataContractSerializer = GetDataContractSerializer(type);
+ using (XmlReader reader = JsonReaderWriterFactory.CreateJsonReader(stream, effectiveEncoding, _readerQuotas, null))
+ {
+ return dataContractSerializer.ReadObject(reader);
+ }
+ }
+ }
+ });
+ }
+
+ /// <summary>
+ /// Called during serialization to write an object of the specified <paramref name="type"/>
+ /// to the specified <paramref name="stream"/>.
+ /// </summary>
+ /// <param name="type">The type of object to write.</param>
+ /// <param name="value">The object to write.</param>
+ /// <param name="stream">The <see cref="Stream"/> to which to write.</param>
+ /// <param name="contentHeaders">The <see cref="HttpContentHeaders"/> for the content being written.</param>
+ /// <param name="transportContext">The <see cref="TransportContext"/>.</param>
+ /// <returns>A <see cref="Task"/> that will write the value to the stream.</returns>
+ public override Task WriteToStreamAsync(Type type, object value, Stream stream, HttpContentHeaders contentHeaders, TransportContext transportContext)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+
+ if (stream == null)
+ {
+ throw new ArgumentNullException("stream");
+ }
+
+ return TaskHelpers.RunSynchronously(() =>
+ {
+ if (FormattingUtilities.IsJsonValueType(type) || !UseDataContractJsonSerializer)
+ {
+ using (JsonTextWriter jsonTextWriter = new JsonTextWriter(new StreamWriter(stream, Encoding)) { CloseOutput = false })
+ {
+ Serializer.Serialize(jsonTextWriter, value);
+ jsonTextWriter.Flush();
+ }
+ }
+ else
+ {
+ if (MediaTypeFormatter.TryGetDelegatingTypeForIQueryableGenericOrSame(ref type))
+ {
+ value = MediaTypeFormatter.GetTypeRemappingConstructor(type).Invoke(new object[] { value });
+ }
+
+ DataContractJsonSerializer dataContractSerializer = GetDataContractSerializer(type);
+ // TODO: CSDMain 235508: Should formatters close write stream on completion or leave that to somebody else?
+ using (XmlWriter writer = JsonReaderWriterFactory.CreateJsonWriter(stream, Encoding, ownsStream: false))
+ {
+ dataContractSerializer.WriteObject(writer, value);
+ }
+ }
+ });
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is propagated.")]
+ private static DataContractJsonSerializer CreateDataContractSerializer(Type type)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+
+ DataContractJsonSerializer serializer = null;
+
+ try
+ {
+ if (IsKnownUnserializableType(type))
+ {
+ return null;
+ }
+
+ //// TODO: CSDMAIN 211321 -- determine the correct algorithm to know what is serializable.
+ serializer = new DataContractJsonSerializer(type);
+ }
+ catch (Exception)
+ {
+ //// TODO: CSDMain 232171 -- review and fix swallowed exception
+ }
+
+ return serializer;
+ }
+
+ private static bool IsKnownUnserializableType(Type type)
+ {
+ if (type.IsGenericType)
+ {
+ if (typeof(IEnumerable).IsAssignableFrom(type))
+ {
+ return IsKnownUnserializableType(type.GetGenericArguments()[0]);
+ }
+ }
+
+ if (!type.IsVisible)
+ {
+ return true;
+ }
+
+ if (type.HasElementType && IsKnownUnserializableType(type.GetElementType()))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ private DataContractJsonSerializer GetDataContractSerializer(Type type)
+ {
+ Contract.Assert(type != null, "Type cannot be null");
+
+ DataContractJsonSerializer serializer =
+ _dataContractSerializerCache.GetOrAdd(type, (t) => CreateDataContractSerializer(type));
+
+ if (serializer == null)
+ {
+ // A null serializer means the type cannot be serialized
+ throw new InvalidOperationException(
+ RS.Format(Properties.Resources.SerializerCannotSerializeType, typeof(DataContractJsonSerializer).Name, type.Name));
+ }
+
+ return serializer;
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Formatting/JsonValueConverter.cs b/src/System.Net.Http.Formatting/Formatting/JsonValueConverter.cs
new file mode 100644
index 00000000..f713417a
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Formatting/JsonValueConverter.cs
@@ -0,0 +1,107 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Json;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace System.Net.Http.Formatting
+{
+ internal class JsonValueConverter : JsonConverter
+ {
+ public override bool CanConvert(Type objectType)
+ {
+ return typeof(JsonValue).IsAssignableFrom(objectType);
+ }
+
+ public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+ {
+ JToken token = JToken.ReadFrom(reader);
+ return ConvertJTokenToJsonValue(token);
+ }
+
+ public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
+ {
+ JToken token = ConvertJsonValueToJToken(value as JsonValue);
+ token.WriteTo(writer);
+ }
+
+ private static JToken ConvertJsonValueToJToken(JsonValue value)
+ {
+ if (value == null)
+ {
+ return new JValue((object)null);
+ }
+
+ switch (value.JsonType)
+ {
+ case JsonType.Boolean:
+ case JsonType.Number:
+ case JsonType.String:
+ JsonPrimitive primitive = value as JsonPrimitive;
+ return new JValue(primitive.Value);
+
+ case JsonType.Array:
+ JArray jsonArray = new JArray();
+ foreach (JsonValue property in value as JsonArray)
+ {
+ jsonArray.Add(ConvertJsonValueToJToken(property));
+ }
+ return jsonArray;
+
+ case JsonType.Object:
+ JObject jsonObject = new JObject();
+ foreach (KeyValuePair<string, JsonValue> kvp in value as JsonObject)
+ {
+ jsonObject.Add(kvp.Key, ConvertJsonValueToJToken(kvp.Value));
+ }
+ return jsonObject;
+
+ case JsonType.Default:
+ default:
+ throw new NotSupportedException();
+ }
+ }
+
+ [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "token is never cast to JValue twice in the same code path")]
+ private static JsonValue ConvertJTokenToJsonValue(JToken token)
+ {
+ switch (token.Type)
+ {
+ case JTokenType.Null:
+ return null;
+
+ case JTokenType.Boolean:
+ return new JsonPrimitive((bool)((JValue)token).Value);
+
+ case JTokenType.Float:
+ return new JsonPrimitive((double)((JValue)token).Value);
+
+ case JTokenType.Integer:
+ return new JsonPrimitive((long)((JValue)token).Value);
+
+ case JTokenType.String:
+ return new JsonPrimitive((string)((JValue)token).Value);
+
+ case JTokenType.Array:
+ JsonArray jsonArray = new JsonArray();
+ foreach (JToken item in (JArray)token)
+ {
+ jsonArray.Add(ConvertJTokenToJsonValue(item));
+ }
+ return jsonArray;
+
+ case JTokenType.Object:
+ JsonObject jsonObject = new JsonObject();
+ foreach (KeyValuePair<string, JToken> kvp in (JObject)token)
+ {
+ jsonObject.Add(kvp.Key, ConvertJTokenToJsonValue(kvp.Value));
+ }
+ return jsonObject;
+
+ case JTokenType.None:
+ default:
+ throw new NotSupportedException();
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/System.Net.Http.Formatting/Formatting/MediaRangeMapping.cs b/src/System.Net.Http.Formatting/Formatting/MediaRangeMapping.cs
new file mode 100644
index 00000000..29c18840
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Formatting/MediaRangeMapping.cs
@@ -0,0 +1,90 @@
+using System.Collections.Generic;
+using System.Net.Http.Headers;
+
+namespace System.Net.Http.Formatting
+{
+ /// <summary>
+ /// Class that provides <see cref="MediaTypeHeaderValue"/>s for a request or response
+ /// from a media range.
+ /// </summary>
+ public sealed class MediaRangeMapping : MediaTypeMapping
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MediaRangeMapping"/> class.
+ /// </summary>
+ /// <param name="mediaRange">The <see cref="MediaTypeHeaderValue"/> that provides a description
+ /// of the media range.</param>
+ /// <param name="mediaType">The <see cref="MediaTypeHeaderValue"/> to return on a match.</param>
+ public MediaRangeMapping(MediaTypeHeaderValue mediaRange, MediaTypeHeaderValue mediaType)
+ : base(mediaType)
+ {
+ Initialize(mediaRange);
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MediaRangeMapping"/> class.
+ /// </summary>
+ /// <param name="mediaRange">The description of the media range.</param>
+ /// <param name="mediaType">The media type to return on a match.</param>
+ public MediaRangeMapping(string mediaRange, string mediaType)
+ : base(mediaType)
+ {
+ if (String.IsNullOrWhiteSpace(mediaRange))
+ {
+ throw new ArgumentNullException("mediaRange");
+ }
+
+ Initialize(new MediaTypeHeaderValue(mediaRange));
+ }
+
+ /// <summary>
+ /// Gets the <see cref="MediaTypeHeaderValue"/>
+ /// describing the known media range.
+ /// </summary>
+ public MediaTypeHeaderValue MediaRange { get; private set; }
+
+ /// <summary>
+ /// Returns a value indicating whether this <see cref="MediaRangeMapping"/>
+ /// instance can provide a <see cref="MediaTypeHeaderValue"/> for the <paramref name="request"/>.
+ /// </summary>
+ /// <param name="request">The <see cref="HttpRequestMessage"/> to check.</param>
+ /// <returns>If this instance can match <paramref name="request"/>
+ /// it returns the quality of the match otherwise <c>0.0</c>.</returns>
+ public override double TryMatchMediaType(HttpRequestMessage request)
+ {
+ if (request == null)
+ {
+ throw new ArgumentNullException("request");
+ }
+
+ ICollection<MediaTypeWithQualityHeaderValue> acceptHeader = request.Headers.Accept;
+ if (acceptHeader != null)
+ {
+ foreach (MediaTypeWithQualityHeaderValue mediaType in acceptHeader)
+ {
+ if (mediaType != null && MediaTypeHeaderValueEqualityComparer.EqualityComparer.Equals(MediaRange, mediaType))
+ {
+ return mediaType.Quality.HasValue ? mediaType.Quality.Value : MediaTypeMatch.Match;
+ }
+ }
+ }
+
+ return MediaTypeMatch.NoMatch;
+ }
+
+ private void Initialize(MediaTypeHeaderValue mediaRange)
+ {
+ if (mediaRange == null)
+ {
+ throw new ArgumentNullException("mediaRange");
+ }
+
+ if (!mediaRange.IsMediaRange())
+ {
+ throw new InvalidOperationException(RS.Format(Properties.Resources.InvalidMediaRange, mediaRange.ToString()));
+ }
+
+ MediaRange = mediaRange;
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Formatting/MediaTypeConstants.cs b/src/System.Net.Http.Formatting/Formatting/MediaTypeConstants.cs
new file mode 100644
index 00000000..69f3e576
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Formatting/MediaTypeConstants.cs
@@ -0,0 +1,84 @@
+using System.Net.Http.Headers;
+using System.Text;
+
+namespace System.Net.Http.Formatting
+{
+ /// <summary>
+ /// Constants related to media types.
+ /// </summary>
+ internal static class MediaTypeConstants
+ {
+ private static readonly MediaTypeHeaderValue _defaultApplicationXmlMediaType = new MediaTypeHeaderValue("application/xml");
+ private static readonly MediaTypeHeaderValue _defaultTextXmlMediaType = new MediaTypeHeaderValue("text/xml");
+ private static readonly MediaTypeHeaderValue _defaultApplicationJsonMediaType = new MediaTypeHeaderValue("application/json");
+ private static readonly MediaTypeHeaderValue _defaultTextJsonMediaType = new MediaTypeHeaderValue("text/json");
+ private static readonly MediaTypeHeaderValue _defaultTextHtmlMediaType = new MediaTypeHeaderValue("text/html") { CharSet = Encoding.UTF8.WebName };
+ private static readonly MediaTypeHeaderValue _defaultApplicationFormUrlEncodedMediaType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
+
+ /// <summary>
+ /// Gets a <see cref="MediaTypeHeaderValue"/> instance representing <c>text/html</c>.
+ /// </summary>
+ /// <value>
+ /// A new <see cref="MediaTypeHeaderValue"/> instance representing <c>text/html</c>.
+ /// </value>
+ public static MediaTypeHeaderValue HtmlMediaType
+ {
+ get { return _defaultTextHtmlMediaType.Clone(); }
+ }
+
+ /// <summary>
+ /// Gets a <see cref="MediaTypeHeaderValue"/> instance representing <c>application/xml</c>.
+ /// </summary>
+ /// <value>
+ /// A new <see cref="MediaTypeHeaderValue"/> instance representing <c>application/xml</c>.
+ /// </value>
+ public static MediaTypeHeaderValue ApplicationXmlMediaType
+ {
+ get { return _defaultApplicationXmlMediaType.Clone(); }
+ }
+
+ /// <summary>
+ /// Gets a <see cref="MediaTypeHeaderValue"/> instance representing <c>application/json</c>.
+ /// </summary>
+ /// <value>
+ /// A new <see cref="MediaTypeHeaderValue"/> instance representing <c>application/json</c>.
+ /// </value>
+ public static MediaTypeHeaderValue ApplicationJsonMediaType
+ {
+ get { return _defaultApplicationJsonMediaType.Clone(); }
+ }
+
+ /// <summary>
+ /// Gets a <see cref="MediaTypeHeaderValue"/> instance representing <c>text/xml</c>.
+ /// </summary>
+ /// <value>
+ /// A new <see cref="MediaTypeHeaderValue"/> instance representing <c>text/xml</c>.
+ /// </value>
+ public static MediaTypeHeaderValue TextXmlMediaType
+ {
+ get { return _defaultTextXmlMediaType.Clone(); }
+ }
+
+ /// <summary>
+ /// Gets a <see cref="MediaTypeHeaderValue"/> instance representing <c>text/json</c>.
+ /// </summary>
+ /// <value>
+ /// A new <see cref="MediaTypeHeaderValue"/> instance representing <c>text/json</c>.
+ /// </value>
+ public static MediaTypeHeaderValue TextJsonMediaType
+ {
+ get { return _defaultTextJsonMediaType.Clone(); }
+ }
+
+ /// <summary>
+ /// Gets a <see cref="MediaTypeHeaderValue"/> instance representing <c>application/x-www-form-urlencoded</c>.
+ /// </summary>
+ /// <value>
+ /// A new <see cref="MediaTypeHeaderValue"/> instance representing <c>application/x-www-form-urlencoded</c>.
+ /// </value>
+ public static MediaTypeHeaderValue ApplicationFormUrlEncodedMediaType
+ {
+ get { return _defaultApplicationFormUrlEncodedMediaType.Clone(); }
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Formatting/MediaTypeFormatter.cs b/src/System.Net.Http.Formatting/Formatting/MediaTypeFormatter.cs
new file mode 100644
index 00000000..b29bcbfe
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Formatting/MediaTypeFormatter.cs
@@ -0,0 +1,493 @@
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+using System.IO;
+using System.Linq;
+using System.Net.Http.Headers;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace System.Net.Http.Formatting
+{
+ /// <summary>
+ /// Base class to handle serializing and deserializing strongly-typed objects using <see cref="ObjectContent"/>.
+ /// </summary>
+ public abstract class MediaTypeFormatter
+ {
+ private static readonly ConcurrentDictionary<Type, Type> _delegatingEnumerableCache = new ConcurrentDictionary<Type, Type>();
+ private static ConcurrentDictionary<Type, ConstructorInfo> _delegatingEnumerableConstructorCache = new ConcurrentDictionary<Type, ConstructorInfo>();
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MediaTypeFormatter"/> class.
+ /// </summary>
+ protected MediaTypeFormatter()
+ {
+ SupportedMediaTypes = new MediaTypeHeaderValueCollection();
+ MediaTypeMappings = new Collection<MediaTypeMapping>();
+ }
+
+ // Some serialization paths (particularly things using dictionaries) may not scale to arbitrarily large inputs.
+ // By default, we have checks to limit the size of stream inputs to protect the serialization path.
+ // If this flag is true, disable those checks.
+ // Todo 342889 - remove this check.
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static bool SkipStreamLimitChecks { get; set; }
+
+ /// <summary>
+ /// Gets the mutable collection of <see cref="MediaTypeHeaderValue"/> elements supported by
+ /// this <see cref="MediaTypeFormatter"/> instance.
+ /// </summary>
+ public Collection<MediaTypeHeaderValue> SupportedMediaTypes { get; private set; }
+
+ /// <summary>
+ /// Gets the mutable collection of <see cref="MediaTypeMapping"/> elements used
+ /// by this <see cref="MediaTypeFormatter"/> instance to determine the
+ /// <see cref="MediaTypeHeaderValue"/> of requests or responses.
+ /// </summary>
+ public Collection<MediaTypeMapping> MediaTypeMappings { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the <see cref="IRequiredMemberSelector"/> used to determine required members.
+ /// </summary>
+ public IRequiredMemberSelector RequiredMemberSelector { get; set; }
+
+ /// <summary>
+ /// Gets or sets the <see cref="Encoding"/> to use when reading and writing data.
+ /// </summary>
+ /// <value>
+ /// The <see cref="Encoding"/> to use when reading and writing data.
+ /// </value>
+ protected Encoding Encoding { get; set; }
+
+ /// <summary>
+ /// Returns a <see cref="Task"/> to deserialize an object of the given <paramref name="type"/> from the given <paramref name="stream"/>
+ /// </summary>
+ /// <remarks>This implementation throws a <see cref="NotSupportedException"/>. Derived types should override this method if the formatter
+ /// supports reading.</remarks>
+ /// <param name="type">The type of the object to deserialize.</param>
+ /// <param name="stream">The <see cref="Stream"/> to read.</param>
+ /// <param name="contentHeaders">The <see cref="HttpContentHeaders"/> if available. It may be <c>null</c>.</param>
+ /// <param name="formatterLogger">The <see cref="IFormatterLogger"/> to log events to.</param>
+ /// <returns>A <see cref="Task"/> whose result will be an object of the given type.</returns>
+ /// <exception cref="NotSupportedException">Derived types need to support reading.</exception>
+ /// <seealso cref="CanWriteType(Type)"/>
+ public virtual Task<object> ReadFromStreamAsync(Type type, Stream stream, HttpContentHeaders contentHeaders, IFormatterLogger formatterLogger)
+ {
+ throw new NotSupportedException(
+ RS.Format(Properties.Resources.MediaTypeFormatterCannotRead, GetType()));
+ }
+
+ /// <summary>
+ /// Returns a <see cref="Task"/> that serializes the given <paramref name="value"/> of the given <paramref name="type"/>
+ /// to the given <paramref name="stream"/>.
+ /// </summary>
+ /// <remarks>This implementation throws a <see cref="NotSupportedException"/>. Derived types should override this method if the formatter
+ /// supports writing.</remarks>
+ /// <param name="type">The type of the object to write.</param>
+ /// <param name="value">The object value to write. It may be <c>null</c>.</param>
+ /// <param name="stream">The <see cref="Stream"/> to which to write.</param>
+ /// <param name="contentHeaders">The <see cref="HttpContentHeaders"/> if available. It may be <c>null</c>.</param>
+ /// <param name="transportContext">The <see cref="TransportContext"/> if available. It may be <c>null</c>.</param>
+ /// <returns>A <see cref="Task"/> that will perform the write.</returns>
+ /// <exception cref="NotSupportedException">Derived types need to support writing.</exception>
+ /// <seealso cref="CanReadType(Type)"/>
+ public virtual Task WriteToStreamAsync(Type type, object value, Stream stream, HttpContentHeaders contentHeaders, TransportContext transportContext)
+ {
+ throw new NotSupportedException(
+ RS.Format(Properties.Resources.MediaTypeFormatterCannotWrite, GetType()));
+ }
+
+ private static bool TryGetDelegatingType(Type interfaceType, ref Type type)
+ {
+ if (type != null
+ && type.IsInterface
+ && type.IsGenericType
+ && (type.GetInterface(interfaceType.FullName) != null || type.GetGenericTypeDefinition().Equals(interfaceType)))
+ {
+ type = GetOrAddDelegatingType(type);
+ return true;
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// This method converts <see cref="IEnumerable{T}"/> (and interfaces that mandate it) to a <see cref="DelegatingEnumerable{T}"/> for serialization purposes.
+ /// </summary>
+ /// <param name="type">The type to potentially be wrapped. If the type is wrapped, it's changed in place.</param>
+ /// <returns>Returns <c>true</c> if the type was wrapped; <c>false</c>, otherwise</returns>
+ [SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "0#", Justification = "This API is designed to morph the type parameter appropriately")]
+ protected internal static bool TryGetDelegatingTypeForIEnumerableGenericOrSame(ref Type type)
+ {
+ return TryGetDelegatingType(FormattingUtilities.EnumerableInterfaceGenericType, ref type);
+ }
+
+ /// <summary>
+ /// This method converts <see cref="IQueryable{T}"/> (and interfaces that mandate it) to a <see cref="DelegatingEnumerable{T}"/> for serialization purposes.
+ /// </summary>
+ /// <param name="type">The type to potentially be wrapped. If the type is wrapped, it's changed in place.</param>
+ /// <returns>Returns <c>true</c> if the type was wrapped; <c>false</c>, otherwise</returns>
+ [SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "0#", Justification = "This API is designed to morph the type parameter appropriately")]
+ protected internal static bool TryGetDelegatingTypeForIQueryableGenericOrSame(ref Type type)
+ {
+ return TryGetDelegatingType(FormattingUtilities.QueryableInterfaceGenericType, ref type);
+ }
+
+ protected internal static ConstructorInfo GetTypeRemappingConstructor(Type type)
+ {
+ ConstructorInfo constructorInfo;
+ _delegatingEnumerableConstructorCache.TryGetValue(type, out constructorInfo);
+ return constructorInfo;
+ }
+
+ internal bool CanReadAs(Type type, MediaTypeHeaderValue mediaType)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+
+ if (mediaType == null)
+ {
+ throw new ArgumentNullException("mediaType");
+ }
+
+ if (!CanReadType(type))
+ {
+ return false;
+ }
+
+ MediaTypeMatch mediaTypeMatch;
+ return TryMatchSupportedMediaType(mediaType, out mediaTypeMatch);
+ }
+
+ internal bool CanWriteAs(Type type, MediaTypeHeaderValue mediaType, out MediaTypeHeaderValue matchedMediaType)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+
+ if (mediaType == null)
+ {
+ throw new ArgumentNullException("mediaType");
+ }
+
+ if (!CanWriteType(type))
+ {
+ matchedMediaType = null;
+ return false;
+ }
+
+ MediaTypeMatch mediaTypeMatch;
+ if (TryMatchSupportedMediaType(mediaType, out mediaTypeMatch))
+ {
+ matchedMediaType = mediaTypeMatch.MediaType;
+ return true;
+ }
+
+ matchedMediaType = null;
+ return false;
+ }
+
+ internal ResponseMediaTypeMatch SelectResponseMediaType(Type type, HttpRequestMessage request)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+
+ if (request == null)
+ {
+ throw new ArgumentNullException("request");
+ }
+
+ if (!CanWriteType(type))
+ {
+ return null;
+ }
+
+ // Match against the request.
+ IEnumerable<MediaTypeWithQualityHeaderValue> acceptHeaderMediaTypes =
+ request.Headers.Accept.OrderBy(m => m, MediaTypeHeaderValueComparer.Comparer);
+
+ MediaTypeMatch mediaTypeMatch = null;
+ if (TryMatchSupportedMediaType(acceptHeaderMediaTypes, out mediaTypeMatch))
+ {
+ return new ResponseMediaTypeMatch(
+ mediaTypeMatch,
+ ResponseFormatterSelectionResult.MatchOnRequestAcceptHeader);
+ }
+
+ if (TryMatchMediaTypeMapping(request, out mediaTypeMatch))
+ {
+ return new ResponseMediaTypeMatch(
+ mediaTypeMatch,
+ ResponseFormatterSelectionResult.MatchOnRequestAcceptHeaderWithMediaTypeMapping);
+ }
+
+ HttpContent requestContent = request.Content;
+ if (requestContent != null)
+ {
+ MediaTypeHeaderValue requestContentType = requestContent.Headers.ContentType;
+ if (requestContentType != null && TryMatchSupportedMediaType(requestContentType, out mediaTypeMatch))
+ {
+ return new ResponseMediaTypeMatch(
+ mediaTypeMatch,
+ ResponseFormatterSelectionResult.MatchOnRequestContentType);
+ }
+ }
+
+ // No match at all.
+ // Pick the first supported media type and indicate we've matched only on type
+ MediaTypeHeaderValue mediaType = SupportedMediaTypes.FirstOrDefault();
+ if (mediaType != null && Encoding != null)
+ {
+ mediaType = mediaType.Clone();
+ mediaType.CharSet = Encoding.WebName;
+ }
+
+ return new ResponseMediaTypeMatch(
+ new MediaTypeMatch(mediaType),
+ ResponseFormatterSelectionResult.MatchOnCanWriteType);
+ }
+
+ internal bool TryMatchSupportedMediaType(MediaTypeHeaderValue mediaType, out MediaTypeMatch mediaTypeMatch)
+ {
+ Contract.Assert(mediaType != null, "mediaType cannot be null.");
+
+ foreach (MediaTypeHeaderValue supportedMediaType in SupportedMediaTypes)
+ {
+ if (MediaTypeHeaderValueEqualityComparer.EqualityComparer.Equals(supportedMediaType, mediaType))
+ {
+ // If the incoming media type had an associated quality factor, propagate it to the match
+ MediaTypeWithQualityHeaderValue mediaTypeWithQualityHeaderValue = mediaType as MediaTypeWithQualityHeaderValue;
+ double quality = mediaTypeWithQualityHeaderValue != null && mediaTypeWithQualityHeaderValue.Quality.HasValue
+ ? mediaTypeWithQualityHeaderValue.Quality.Value
+ : MediaTypeMatch.Match;
+
+ MediaTypeHeaderValue effectiveMediaType = supportedMediaType;
+ if (Encoding != null)
+ {
+ effectiveMediaType = supportedMediaType.Clone();
+ effectiveMediaType.CharSet = Encoding.WebName;
+ }
+
+ mediaTypeMatch = new MediaTypeMatch(effectiveMediaType, quality);
+ return true;
+ }
+ }
+
+ mediaTypeMatch = null;
+ return false;
+ }
+
+ internal bool TryMatchSupportedMediaType(IEnumerable<MediaTypeHeaderValue> mediaTypes, out MediaTypeMatch mediaTypeMatch)
+ {
+ Contract.Assert(mediaTypes != null, "mediaTypes cannot be null.");
+ foreach (MediaTypeHeaderValue mediaType in mediaTypes)
+ {
+ if (TryMatchSupportedMediaType(mediaType, out mediaTypeMatch))
+ {
+ return true;
+ }
+ }
+
+ mediaTypeMatch = null;
+ return false;
+ }
+
+ internal bool TryMatchMediaTypeMapping(HttpRequestMessage request, out MediaTypeMatch mediaTypeMatch)
+ {
+ Contract.Assert(request != null, "request cannot be null.");
+
+ foreach (MediaTypeMapping mapping in MediaTypeMappings)
+ {
+ // Collection<T> is not protected against null, so avoid them
+ double quality;
+ if (mapping != null && ((quality = mapping.TryMatchMediaType(request)) > 0.0))
+ {
+ mediaTypeMatch = new MediaTypeMatch(mapping.MediaType, quality);
+ return true;
+ }
+ }
+
+ mediaTypeMatch = null;
+ return false;
+ }
+
+ /// <summary>
+ /// Sets the default headers for content that will be formatted using this formatter. This method
+ /// is called from the <see cref="ObjectContent"/> constructor.
+ /// This implementation sets the Content-Type header to the value of <paramref name="mediaType"/> if it is
+ /// not <c>null</c>. If it is <c>null</c> it sets the Content-Type to the default media type of this formatter.
+ /// If the Content-Type does not specify a charset it will set it using this formatters configured
+ /// <see cref="Encoding"/>.
+ /// </summary>
+ /// <remarks>
+ /// Subclasses can override this method to set content headers such as Content-Type etc. Subclasses should
+ /// call the base implementation. Subclasses should treat the passed in <paramref name="mediaType"/> (if not <c>null</c>)
+ /// as the authoritative media type and use that as the Content-Type.
+ /// </remarks>
+ /// <param name="type">The type of the object being serialized. See <see cref="ObjectContent"/>.</param>
+ /// <param name="headers">The content headers that should be configured.</param>
+ /// <param name="mediaType">The authoritative media type. Can be <c>null</c>.</param>
+ public virtual void SetDefaultContentHeaders(Type type, HttpContentHeaders headers, string mediaType)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+ if (headers == null)
+ {
+ throw new ArgumentNullException("headers");
+ }
+
+ if (!String.IsNullOrEmpty(mediaType))
+ {
+ var parsedMediaType = MediaTypeHeaderValue.Parse(mediaType);
+ headers.ContentType = parsedMediaType;
+ }
+
+ if (headers.ContentType == null)
+ {
+ MediaTypeHeaderValue defaultMediaType = SupportedMediaTypes.FirstOrDefault();
+ if (defaultMediaType != null)
+ {
+ headers.ContentType = defaultMediaType.Clone();
+ }
+ }
+
+ if (headers.ContentType != null && headers.ContentType.CharSet == null && Encoding != null)
+ {
+ headers.ContentType.CharSet = Encoding.WebName;
+ }
+ }
+
+ /// <summary>
+ /// Returns a specialized instance of the <see cref="MediaTypeFormatter"/> that can handle formatting a response for the given
+ /// parameters. This method is called by <see cref="DefaultContentNegotiator"/> after a formatter has been selected through content
+ /// negotiation.
+ /// </summary>
+ /// <remarks>
+ /// The default implementation returns <c>this</c> instance. Derived classes can choose to return a new instance if
+ /// they need to close over any of the parameters.
+ /// </remarks>
+ /// <param name="type">The type being serialized.</param>
+ /// <param name="request">The request.</param>
+ /// <param name="mediaType">The media type chosen for the serialization. Can be <c>null</c>.</param>
+ /// <returns>An instance that can format a response to the given <paramref name="request"/>.</returns>
+ public virtual MediaTypeFormatter GetPerRequestFormatterInstance(Type type, HttpRequestMessage request, MediaTypeHeaderValue mediaType)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+ if (request == null)
+ {
+ throw new ArgumentNullException("request");
+ }
+
+ return this;
+ }
+
+ /// <summary>
+ /// Determines whether this <see cref="MediaTypeFormatter"/> can deserialize
+ /// an object of the specified type.
+ /// </summary>
+ /// <remarks>
+ /// Derived classes must implement this method and indicate if a type can or cannot be deserialized.
+ /// </remarks>
+ /// <param name="type">The type of object that will be deserialized.</param>
+ /// <returns><c>true</c> if this <see cref="MediaTypeFormatter"/> can deserialize an object of that type; otherwise <c>false</c>.</returns>
+ public abstract bool CanReadType(Type type);
+
+ /// <summary>
+ /// Determines whether this <see cref="MediaTypeFormatter"/> can serialize
+ /// an object of the specified type.
+ /// </summary>
+ /// <remarks>
+ /// Derived classes must implement this method and indicate if a type can or cannot be serialized.
+ /// </remarks>
+ /// <param name="type">The type of object that will be serialized.</param>
+ /// <returns><c>true</c> if this <see cref="MediaTypeFormatter"/> can serialize an object of that type; otherwise <c>false</c>.</returns>
+ public abstract bool CanWriteType(Type type);
+
+ private static Type GetOrAddDelegatingType(Type type)
+ {
+ return _delegatingEnumerableCache.GetOrAdd(
+ type,
+ (typeToRemap) =>
+ {
+ // The current method is called by methods that already checked the type for is not null, is generic and is or implements IEnumerable<T>
+ // This retrieves the T type of the IEnumerable<T> interface.
+ Type elementType;
+ if (typeToRemap.GetGenericTypeDefinition().Equals(FormattingUtilities.EnumerableInterfaceGenericType))
+ {
+ elementType = typeToRemap.GetGenericArguments()[0];
+ }
+ else
+ {
+ elementType = typeToRemap.GetInterface(FormattingUtilities.EnumerableInterfaceGenericType.FullName).GetGenericArguments()[0];
+ }
+
+ Type delegatingType = FormattingUtilities.DelegatingEnumerableGenericType.MakeGenericType(elementType);
+ ConstructorInfo delegatingConstructor = delegatingType.GetConstructor(new Type[] { FormattingUtilities.EnumerableInterfaceGenericType.MakeGenericType(elementType) });
+ _delegatingEnumerableConstructorCache.TryAdd(delegatingType, delegatingConstructor);
+
+ return delegatingType;
+ });
+ }
+
+ /// <summary>
+ /// Collection class that validates it contains only <see cref="MediaTypeHeaderValue"/> instances
+ /// that are not null and not media ranges.
+ /// </summary>
+ internal class MediaTypeHeaderValueCollection : Collection<MediaTypeHeaderValue>
+ {
+ private static readonly Type _mediaTypeHeaderValueType = typeof(MediaTypeHeaderValue);
+
+ /// <summary>
+ /// Inserts the <paramref name="item"/> into the collection at the specified <paramref name="index"/>.
+ /// </summary>
+ /// <param name="index">The zero-based index at which item should be inserted.</param>
+ /// <param name="item">The object to insert. It cannot be <c>null</c>.</param>
+ protected override void InsertItem(int index, MediaTypeHeaderValue item)
+ {
+ ValidateMediaType(item);
+ base.InsertItem(index, item);
+ }
+
+ /// <summary>
+ /// Replaces the element at the specified <paramref name="index"/>.
+ /// </summary>
+ /// <param name="index">The zero-based index of the item that should be replaced.</param>
+ /// <param name="item">The new value for the element at the specified index. It cannot be <c>null</c>.</param>
+ protected override void SetItem(int index, MediaTypeHeaderValue item)
+ {
+ ValidateMediaType(item);
+ base.SetItem(index, item);
+ }
+
+ private static void ValidateMediaType(MediaTypeHeaderValue item)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+
+ ParsedMediaTypeHeaderValue parsedMediaType = new ParsedMediaTypeHeaderValue(item);
+ if (parsedMediaType.IsAllMediaRange || parsedMediaType.IsSubTypeMediaRange)
+ {
+ throw new ArgumentException(
+ RS.Format(Properties.Resources.CannotUseMediaRangeForSupportedMediaType, _mediaTypeHeaderValueType.Name, item.MediaType),
+ "item");
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Formatting/MediaTypeFormatterCollection.cs b/src/System.Net.Http.Formatting/Formatting/MediaTypeFormatterCollection.cs
new file mode 100644
index 00000000..878831d4
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Formatting/MediaTypeFormatterCollection.cs
@@ -0,0 +1,137 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Net.Http.Headers;
+
+namespace System.Net.Http.Formatting
+{
+ /// <summary>
+ /// Collection class that contains <see cref="MediaTypeFormatter"/> instances.
+ /// </summary>
+ public class MediaTypeFormatterCollection : Collection<MediaTypeFormatter>
+ {
+ private static readonly Type _mediaTypeFormatterType = typeof(MediaTypeFormatter);
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MediaTypeFormatterCollection"/> class.
+ /// </summary>
+ /// <remarks>
+ /// This collection will be initialized to contain default <see cref="MediaTypeFormatter"/>
+ /// instances for Xml, JsonValue and Json.
+ /// </remarks>
+ public MediaTypeFormatterCollection()
+ : this(CreateDefaultFormatters())
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MediaTypeFormatterCollection"/> class.
+ /// </summary>
+ /// <param name="formatters">A collection of <see cref="MediaTypeFormatter"/> instances to place in the collection.</param>
+ public MediaTypeFormatterCollection(IEnumerable<MediaTypeFormatter> formatters)
+ {
+ VerifyAndSetFormatters(formatters);
+ }
+
+ /// <summary>
+ /// Gets the <see cref="MediaTypeFormatter"/> to use for Xml.
+ /// </summary>
+ public XmlMediaTypeFormatter XmlFormatter
+ {
+ get { return Items.OfType<XmlMediaTypeFormatter>().FirstOrDefault(); }
+ }
+
+ /// <summary>
+ /// Gets the <see cref="MediaTypeFormatter"/> to use for Json.
+ /// </summary>
+ public JsonMediaTypeFormatter JsonFormatter
+ {
+ get { return Items.OfType<JsonMediaTypeFormatter>().FirstOrDefault(); }
+ }
+
+ /// <summary>
+ /// Gets the <see cref="MediaTypeFormatter"/> to use for <c>application/x-www-form-urlencoded</c> data.
+ /// </summary>
+ public FormUrlEncodedMediaTypeFormatter FormUrlEncodedFormatter
+ {
+ get { return Items.OfType<FormUrlEncodedMediaTypeFormatter>().FirstOrDefault(); }
+ }
+
+ public MediaTypeFormatter Find(string mediaType)
+ {
+ MediaTypeHeaderValue val = MediaTypeHeaderValue.Parse(mediaType);
+ return Find(val);
+ }
+
+ /// <summary>
+ /// Find a formatter in this collection that matches the requested media type.
+ /// </summary>
+ /// <returns>Returns a formatter or null if not found.</returns>
+ public MediaTypeFormatter Find(MediaTypeHeaderValue mediaType)
+ {
+ var comparer = MediaTypeHeaderValueEqualityComparer.EqualityComparer;
+ MediaTypeFormatter formatter = Items.FirstOrDefault(f => f.SupportedMediaTypes.Any(mt => comparer.Equals(mt, mediaType)));
+ return formatter;
+ }
+
+ /// <summary>
+ /// Helper to search a collection for a formatter that can read the .NET type in the given mediaType.
+ /// </summary>
+ /// <param name="type">.NET type to read</param>
+ /// <param name="mediaType">media type to match on.</param>
+ /// <returns>Formatter that can read the type. Null if no formatter found.</returns>
+ public MediaTypeFormatter FindReader(Type type, MediaTypeHeaderValue mediaType)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+ if (mediaType == null)
+ {
+ throw new ArgumentNullException("mediaType");
+ }
+
+ foreach (MediaTypeFormatter formatter in this.Items)
+ {
+ if (formatter.CanReadAs(type, mediaType))
+ {
+ return formatter;
+ }
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Creates a collection of new instances of the default <see cref="MediaTypeFormatter"/>s.
+ /// </summary>
+ /// <returns>The collection of default <see cref="MediaTypeFormatter"/> instances.</returns>
+ private static IEnumerable<MediaTypeFormatter> CreateDefaultFormatters()
+ {
+ return new MediaTypeFormatter[]
+ {
+ new JsonMediaTypeFormatter(),
+ new XmlMediaTypeFormatter(),
+ new FormUrlEncodedMediaTypeFormatter()
+ };
+ }
+
+ private void VerifyAndSetFormatters(IEnumerable<MediaTypeFormatter> formatters)
+ {
+ if (formatters == null)
+ {
+ throw new ArgumentNullException("formatters");
+ }
+
+ foreach (MediaTypeFormatter formatter in formatters)
+ {
+ if (formatter == null)
+ {
+ throw new ArgumentException(RS.Format(Properties.Resources.CannotHaveNullInList, _mediaTypeFormatterType.Name), "formatters");
+ }
+
+ Add(formatter);
+ }
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Formatting/MediaTypeFormatterExtensions.cs b/src/System.Net.Http.Formatting/Formatting/MediaTypeFormatterExtensions.cs
new file mode 100644
index 00000000..3688d3c4
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Formatting/MediaTypeFormatterExtensions.cs
@@ -0,0 +1,217 @@
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Net.Http.Headers;
+
+namespace System.Net.Http.Formatting
+{
+ /// <summary>
+ /// Extensions for adding <see cref="MediaTypeMapping"/> items to a <see cref="MediaTypeFormatter"/>.
+ /// </summary>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static class MediaTypeFormatterExtensions
+ {
+ /// <summary>
+ /// Updates the given <paramref name="formatter"/>'s set of <see cref="MediaTypeMapping"/> elements
+ /// so that it associates the <paramref name="mediaType"/> with <see cref="Uri"/>s containing
+ /// a specific query parameter and value.
+ /// </summary>
+ /// <param name="formatter">The <see cref="MediaTypeFormatter"/> to receive the new <see cref="QueryStringMapping"/> item.</param>
+ /// <param name="queryStringParameterName">The name of the query parameter.</param>
+ /// <param name="queryStringParameterValue">The value assigned to that query parameter.</param>
+ /// <param name="mediaType">The <see cref="MediaTypeHeaderValue"/> to associate
+ /// with a <see cref="Uri"/> containing a query string matching <paramref name="queryStringParameterName"/>
+ /// and <paramref name="queryStringParameterValue"/>.</param>
+ public static void AddQueryStringMapping(
+ this MediaTypeFormatter formatter,
+ string queryStringParameterName,
+ string queryStringParameterValue,
+ MediaTypeHeaderValue mediaType)
+ {
+ if (formatter == null)
+ {
+ throw new ArgumentNullException("formatter");
+ }
+
+ QueryStringMapping mapping = new QueryStringMapping(queryStringParameterName, queryStringParameterValue, mediaType);
+ formatter.MediaTypeMappings.Add(mapping);
+ }
+
+ /// <summary>
+ /// Updates the given <paramref name="formatter"/>'s set of <see cref="MediaTypeMapping"/> elements
+ /// so that it associates the <paramref name="mediaType"/> with <see cref="Uri"/>s containing
+ /// a specific query parameter and value.
+ /// </summary>
+ /// <param name="formatter">The <see cref="MediaTypeFormatter"/> to receive the new <see cref="QueryStringMapping"/> item.</param>
+ /// <param name="queryStringParameterName">The name of the query parameter.</param>
+ /// <param name="queryStringParameterValue">The value assigned to that query parameter.</param>
+ /// <param name="mediaType">The media type to associate
+ /// with a <see cref="Uri"/> containing a query string matching <paramref name="queryStringParameterName"/>
+ /// and <paramref name="queryStringParameterValue"/>.</param>
+ public static void AddQueryStringMapping(
+ this MediaTypeFormatter formatter,
+ string queryStringParameterName,
+ string queryStringParameterValue,
+ string mediaType)
+ {
+ if (formatter == null)
+ {
+ throw new ArgumentNullException("formatter");
+ }
+
+ QueryStringMapping mapping = new QueryStringMapping(queryStringParameterName, queryStringParameterValue, mediaType);
+ formatter.MediaTypeMappings.Add(mapping);
+ }
+
+ /// <summary>
+ /// Updates the given <paramref name="formatter"/>'s set of <see cref="MediaTypeMapping"/> elements
+ /// so that it associates the <paramref name="mediaType"/> with <see cref="Uri"/>s ending with
+ /// the given <paramref name="uriPathExtension"/>.
+ /// </summary>
+ /// <param name="formatter">The <see cref="MediaTypeFormatter"/> to receive the new <see cref="UriPathExtensionMapping"/> item.</param>
+ /// <param name="uriPathExtension">The string of the <see cref="Uri"/> path extension.</param>
+ /// <param name="mediaType">The <see cref="MediaTypeHeaderValue"/> to associate with <see cref="Uri"/>s
+ /// ending with <paramref name="uriPathExtension"/>.</param>
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", Justification = "There is no meaningful System.Uri representation for a path suffix such as '.xml'")]
+ public static void AddUriPathExtensionMapping(
+ this MediaTypeFormatter formatter,
+ string uriPathExtension,
+ MediaTypeHeaderValue mediaType)
+ {
+ if (formatter == null)
+ {
+ throw new ArgumentNullException("formatter");
+ }
+
+ UriPathExtensionMapping mapping = new UriPathExtensionMapping(uriPathExtension, mediaType);
+ formatter.MediaTypeMappings.Add(mapping);
+ }
+
+ /// <summary>
+ /// Updates the given <paramref name="formatter"/>'s set of <see cref="MediaTypeMapping"/> elements
+ /// so that it associates the <paramref name="mediaType"/> with <see cref="Uri"/>s ending with
+ /// the given <paramref name="uriPathExtension"/>.
+ /// </summary>
+ /// <param name="formatter">The <see cref="MediaTypeFormatter"/> to receive the new <see cref="UriPathExtensionMapping"/> item.</param>
+ /// <param name="uriPathExtension">The string of the <see cref="Uri"/> path extension.</param>
+ /// <param name="mediaType">The string media type to associate with <see cref="Uri"/>s
+ /// ending with <paramref name="uriPathExtension"/>.</param>
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", Justification = "There is no meaningful System.Uri representation for a path suffix such as '.xml'")]
+ public static void AddUriPathExtensionMapping(this MediaTypeFormatter formatter, string uriPathExtension, string mediaType)
+ {
+ if (formatter == null)
+ {
+ throw new ArgumentNullException("formatter");
+ }
+
+ UriPathExtensionMapping mapping = new UriPathExtensionMapping(uriPathExtension, mediaType);
+ formatter.MediaTypeMappings.Add(mapping);
+ }
+
+ /// <summary>
+ /// Updates the given <paramref name="formatter"/>'s set of <see cref="MediaTypeMapping"/> elements
+ /// so that it associates the <paramref name="mediaType"/> with requests or responses containing
+ /// <paramref name="mediaRange"/> in the content headers.
+ /// </summary>
+ /// <param name="formatter">The <see cref="MediaTypeFormatter"/> to receive the new <see cref="MediaRangeMapping"/> item.</param>
+ /// <param name="mediaRange">The media range that will appear in the content headers.</param>
+ /// <param name="mediaType">The media type to associate with that <paramref name="mediaRange"/>.</param>
+ public static void AddMediaRangeMapping(this MediaTypeFormatter formatter, string mediaRange, string mediaType)
+ {
+ if (formatter == null)
+ {
+ throw new ArgumentNullException("formatter");
+ }
+
+ MediaRangeMapping mapping = new MediaRangeMapping(mediaRange, mediaType);
+ formatter.MediaTypeMappings.Add(mapping);
+ }
+
+ /// <summary>
+ /// Updates the given <paramref name="formatter"/>'s set of <see cref="MediaTypeMapping"/> elements
+ /// so that it associates the <paramref name="mediaType"/> with requests or responses containing
+ /// <paramref name="mediaRange"/> in the content headers.
+ /// </summary>
+ /// <param name="formatter">The <see cref="MediaTypeFormatter"/> to receive the new <see cref="MediaRangeMapping"/> item.</param>
+ /// <param name="mediaRange">The media range that will appear in the content headers.</param>
+ /// <param name="mediaType">The media type to associate with that <paramref name="mediaRange"/>.</param>
+ public static void AddMediaRangeMapping(
+ this MediaTypeFormatter formatter,
+ MediaTypeHeaderValue mediaRange,
+ MediaTypeHeaderValue mediaType)
+ {
+ if (formatter == null)
+ {
+ throw new ArgumentNullException("formatter");
+ }
+
+ MediaRangeMapping mapping = new MediaRangeMapping(mediaRange, mediaType);
+ formatter.MediaTypeMappings.Add(mapping);
+ }
+
+ /// <summary>
+ /// Updates the given <paramref name="formatter"/>'s set of <see cref="MediaTypeMapping"/> elements
+ /// so that it associates the <paramref name="mediaType"/> with a specific HTTP request header field
+ /// with a specific value.
+ /// </summary>
+ /// <remarks><see cref="RequestHeaderMapping"/> checks header fields associated with <see cref="M:HttpRequestMessage.Headers"/> for a match. It does
+ /// not check header fields associated with <see cref="M:HttpResponseMessage.Headers"/> or <see cref="M:HttpContent.Headers"/> instances.</remarks>
+ /// <param name="formatter">The <see cref="MediaTypeFormatter"/> to receive the new <see cref="MediaRangeMapping"/> item.</param>
+ /// <param name="headerName">Name of the header to match.</param>
+ /// <param name="headerValue">The header value to match.</param>
+ /// <param name="valueComparison">The <see cref="StringComparison"/> to use when matching <paramref name="headerValue"/>.</param>
+ /// <param name="isValueSubstring">if set to <c>true</c> then <paramref name="headerValue"/> is
+ /// considered a match if it matches a substring of the actual header value.</param>
+ /// <param name="mediaType">The <see cref="MediaTypeHeaderValue"/> to associate
+ /// with a <see cref="M:HttpRequestMessage.Header"/> entry with a name matching <paramref name="headerName"/>
+ /// and a value matching <paramref name="headerValue"/>.</param>
+ public static void AddRequestHeaderMapping(
+ this MediaTypeFormatter formatter,
+ string headerName,
+ string headerValue,
+ StringComparison valueComparison,
+ bool isValueSubstring,
+ MediaTypeHeaderValue mediaType)
+ {
+ if (formatter == null)
+ {
+ throw new ArgumentNullException("formatter");
+ }
+
+ RequestHeaderMapping mapping = new RequestHeaderMapping(headerName, headerValue, valueComparison, isValueSubstring, mediaType);
+ formatter.MediaTypeMappings.Add(mapping);
+ }
+
+ /// <summary>
+ /// Updates the given <paramref name="formatter"/>'s set of <see cref="MediaTypeMapping"/> elements
+ /// so that it associates the <paramref name="mediaType"/> with a specific HTTP request header field
+ /// with a specific value.
+ /// </summary>
+ /// <remarks><see cref="RequestHeaderMapping"/> checks header fields associated with <see cref="M:HttpRequestMessage.Headers"/> for a match. It does
+ /// not check header fields associated with <see cref="M:HttpResponseMessage.Headers"/> or <see cref="M:HttpContent.Headers"/> instances.</remarks>
+ /// <param name="formatter">The <see cref="MediaTypeFormatter"/> to receive the new <see cref="MediaRangeMapping"/> item.</param>
+ /// <param name="headerName">Name of the header to match.</param>
+ /// <param name="headerValue">The header value to match.</param>
+ /// <param name="valueComparison">The <see cref="StringComparison"/> to use when matching <paramref name="headerValue"/>.</param>
+ /// <param name="isValueSubstring">if set to <c>true</c> then <paramref name="headerValue"/> is
+ /// considered a match if it matches a substring of the actual header value.</param>
+ /// <param name="mediaType">The media type to associate
+ /// with a <see cref="M:HttpRequestMessage.Header"/> entry with a name matching <paramref name="headerName"/>
+ /// and a value matching <paramref name="headerValue"/>.</param>
+ public static void AddRequestHeaderMapping(
+ this MediaTypeFormatter formatter,
+ string headerName,
+ string headerValue,
+ StringComparison valueComparison,
+ bool isValueSubstring,
+ string mediaType)
+ {
+ if (formatter == null)
+ {
+ throw new ArgumentNullException("formatter");
+ }
+
+ RequestHeaderMapping mapping = new RequestHeaderMapping(headerName, headerValue, valueComparison, isValueSubstring, mediaType);
+ formatter.MediaTypeMappings.Add(mapping);
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Formatting/MediaTypeHeaderValueComparer.cs b/src/System.Net.Http.Formatting/Formatting/MediaTypeHeaderValueComparer.cs
new file mode 100644
index 00000000..57d2243a
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Formatting/MediaTypeHeaderValueComparer.cs
@@ -0,0 +1,91 @@
+using System.Collections.Generic;
+using System.Diagnostics.Contracts;
+using System.Net.Http.Headers;
+
+namespace System.Net.Http.Formatting
+{
+ internal class MediaTypeHeaderValueComparer : IComparer<MediaTypeHeaderValue>
+ {
+ private static readonly MediaTypeHeaderValueComparer _mediaTypeComparer = new MediaTypeHeaderValueComparer();
+
+ private MediaTypeHeaderValueComparer()
+ {
+ }
+
+ public static MediaTypeHeaderValueComparer Comparer
+ {
+ get { return _mediaTypeComparer; }
+ }
+
+ public int Compare(MediaTypeHeaderValue mediaType1, MediaTypeHeaderValue mediaType2)
+ {
+ Contract.Assert(mediaType1 != null, "The 'mediaType1' parameter should not be null.");
+ Contract.Assert(mediaType2 != null, "The 'mediaType2' parameter should not be null.");
+
+ ParsedMediaTypeHeaderValue parsedMediaType1 = new ParsedMediaTypeHeaderValue(mediaType1);
+ ParsedMediaTypeHeaderValue parsedMediaType2 = new ParsedMediaTypeHeaderValue(mediaType2);
+
+ int returnValue = CompareBasedOnQualityFactor(parsedMediaType1, parsedMediaType2);
+
+ if (returnValue == 0)
+ {
+ if (!String.Equals(parsedMediaType1.Type, parsedMediaType2.Type, StringComparison.OrdinalIgnoreCase))
+ {
+ if (parsedMediaType1.IsAllMediaRange)
+ {
+ return 1;
+ }
+ else if (parsedMediaType2.IsAllMediaRange)
+ {
+ return -1;
+ }
+ }
+ else if (!String.Equals(parsedMediaType1.SubType, parsedMediaType2.SubType, StringComparison.OrdinalIgnoreCase))
+ {
+ if (parsedMediaType1.IsSubTypeMediaRange)
+ {
+ return 1;
+ }
+ else if (parsedMediaType2.IsSubTypeMediaRange)
+ {
+ return -1;
+ }
+ }
+ else
+ {
+ if (!parsedMediaType1.HasNonQualityFactorParameter)
+ {
+ if (parsedMediaType2.HasNonQualityFactorParameter)
+ {
+ return 1;
+ }
+ }
+ else if (!parsedMediaType2.HasNonQualityFactorParameter)
+ {
+ return -1;
+ }
+ }
+ }
+
+ return returnValue;
+ }
+
+ private static int CompareBasedOnQualityFactor(ParsedMediaTypeHeaderValue parsedMediaType1, ParsedMediaTypeHeaderValue parsedMediaType2)
+ {
+ Contract.Assert(parsedMediaType1 != null, "The 'parsedMediaType1' parameter should not be null.");
+ Contract.Assert(parsedMediaType2 != null, "The 'parsedMediaType2' parameter should not be null.");
+
+ double qualityDifference = parsedMediaType1.QualityFactor - parsedMediaType2.QualityFactor;
+ if (qualityDifference < 0)
+ {
+ return 1;
+ }
+ else if (qualityDifference > 0)
+ {
+ return -1;
+ }
+
+ return 0;
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Formatting/MediaTypeHeaderValueEqualityComparer.cs b/src/System.Net.Http.Formatting/Formatting/MediaTypeHeaderValueEqualityComparer.cs
new file mode 100644
index 00000000..15307f9e
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Formatting/MediaTypeHeaderValueEqualityComparer.cs
@@ -0,0 +1,79 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+using System.Linq;
+using System.Net.Http.Headers;
+
+namespace System.Net.Http.Formatting
+{
+ /// <summary>
+ /// Provides a special <see cref="MediaTypeHeaderValue"/> comparer function
+ /// </summary>
+ internal class MediaTypeHeaderValueEqualityComparer : IEqualityComparer<MediaTypeHeaderValue>
+ {
+ private static readonly MediaTypeHeaderValueEqualityComparer _mediaTypeEqualityComparer = new MediaTypeHeaderValueEqualityComparer();
+
+ private MediaTypeHeaderValueEqualityComparer()
+ {
+ }
+
+ /// <summary>
+ /// Gets the equality comparer.
+ /// </summary>
+ public static MediaTypeHeaderValueEqualityComparer EqualityComparer
+ {
+ get { return _mediaTypeEqualityComparer; }
+ }
+
+ /// <summary>
+ /// Determines whether two <see cref="MediaTypeHeaderValue"/> instances match. The instance
+ /// <paramref name="mediaType1"/> is said to match <paramref name="mediaType2"/> if and only if
+ /// <paramref name="mediaType1"/> is a strict subset of the values and parameters of <paramref name="mediaType2"/>.
+ /// That is, if the media type and media type parameters of <paramref name="mediaType1"/> are all present
+ /// and match those of <paramref name="mediaType2"/> then it is a match even though <paramref name="mediaType2"/> may have additional
+ /// parameters.
+ /// </summary>
+ /// <param name="mediaType1">The first media type.</param>
+ /// <param name="mediaType2">The second media type.</param>
+ /// <returns><c>true</c> if the two media types are considered equal based on the algorithm above.</returns>
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This is part of implementing IEqualityComparer.")]
+ public bool Equals(MediaTypeHeaderValue mediaType1, MediaTypeHeaderValue mediaType2)
+ {
+ Contract.Assert(mediaType1 != null, "The 'mediaType1' parameter should not be null.");
+ Contract.Assert(mediaType2 != null, "The 'mediaType2' parameter should not be null.");
+
+ if (!String.Equals(mediaType1.MediaType, mediaType2.MediaType, StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ foreach (NameValueHeaderValue parameter1 in mediaType1.Parameters)
+ {
+ if (mediaType2.Parameters.FirstOrDefault(
+ (parameter2) =>
+ {
+ return
+ String.Equals(parameter1.Name, parameter2.Name, StringComparison.OrdinalIgnoreCase) &&
+ String.Equals(parameter1.Value, parameter2.Value, StringComparison.OrdinalIgnoreCase);
+ }) == null)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Returns a hash code for this instance.
+ /// </summary>
+ /// <param name="mediaType">Type of the media.</param>
+ /// <returns>
+ /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.
+ /// </returns>
+ public int GetHashCode(MediaTypeHeaderValue mediaType)
+ {
+ return mediaType.MediaType.ToUpperInvariant().GetHashCode();
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Formatting/MediaTypeHeaderValueExtensions.cs b/src/System.Net.Http.Formatting/Formatting/MediaTypeHeaderValueExtensions.cs
new file mode 100644
index 00000000..4c1d7fd3
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Formatting/MediaTypeHeaderValueExtensions.cs
@@ -0,0 +1,42 @@
+using System.Diagnostics.Contracts;
+using System.Net.Http.Headers;
+
+namespace System.Net.Http.Formatting
+{
+ /// <summary>
+ /// Extension methods for <see cref="MediaTypeHeaderValue"/>.
+ /// </summary>
+ internal static class MediaTypeHeaderValueExtensions
+ {
+ public static bool IsMediaRange(this MediaTypeHeaderValue mediaType)
+ {
+ Contract.Assert(mediaType != null, "The 'mediaType' parameter should not be null.");
+ return new ParsedMediaTypeHeaderValue(mediaType).IsSubTypeMediaRange;
+ }
+
+ public static bool IsWithinMediaRange(this MediaTypeHeaderValue mediaType, MediaTypeHeaderValue mediaRange)
+ {
+ Contract.Assert(mediaType != null, "The 'mediaType' parameter should not be null.");
+ Contract.Assert(mediaRange != null, "The 'mediaRange' parameter should not be null.");
+
+ ParsedMediaTypeHeaderValue parsedMediaType = new ParsedMediaTypeHeaderValue(mediaType);
+ ParsedMediaTypeHeaderValue parsedMediaRange = new ParsedMediaTypeHeaderValue(mediaRange);
+
+ if (!String.Equals(parsedMediaType.Type, parsedMediaRange.Type, StringComparison.OrdinalIgnoreCase))
+ {
+ return parsedMediaRange.IsAllMediaRange;
+ }
+ else if (!String.Equals(parsedMediaType.SubType, parsedMediaRange.SubType, StringComparison.OrdinalIgnoreCase))
+ {
+ return parsedMediaRange.IsSubTypeMediaRange;
+ }
+
+ if (!String.IsNullOrWhiteSpace(parsedMediaRange.CharSet))
+ {
+ return String.Equals(parsedMediaRange.CharSet, parsedMediaType.CharSet, StringComparison.OrdinalIgnoreCase);
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Formatting/MediaTypeMapping.cs b/src/System.Net.Http.Formatting/Formatting/MediaTypeMapping.cs
new file mode 100644
index 00000000..816acc46
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Formatting/MediaTypeMapping.cs
@@ -0,0 +1,73 @@
+using System.Net.Http.Headers;
+
+namespace System.Net.Http.Formatting
+{
+ /// <summary>
+ /// An abstract base class used to create an association between <see cref="HttpRequestMessage"/> or
+ /// <see cref="HttpResponseMessage"/> instances that have certain characteristics
+ /// and a specific <see cref="MediaTypeHeaderValue"/>.
+ /// </summary>
+ public abstract class MediaTypeMapping
+ {
+ /// <summary>
+ /// Initializes a new instance of a <see cref="MediaTypeMapping"/> with the
+ /// given <paramref name="mediaType"/> value.
+ /// </summary>
+ /// <param name="mediaType">
+ /// The <see cref="MediaTypeHeaderValue"/> that is associated with <see cref="HttpRequestMessage"/> or
+ /// <see cref="HttpResponseMessage"/> instances that have the given characteristics of the
+ /// <see cref="MediaTypeMapping"/>.
+ /// </param>
+ protected MediaTypeMapping(MediaTypeHeaderValue mediaType)
+ {
+ if (mediaType == null)
+ {
+ throw new ArgumentNullException("mediaType");
+ }
+
+ MediaType = mediaType;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of a <see cref="MediaTypeMapping"/> with the
+ /// given <paramref name="mediaType"/> value.
+ /// </summary>
+ /// <param name="mediaType">
+ /// The <see cref="string"/> that is associated with <see cref="HttpRequestMessage"/> or
+ /// <see cref="HttpResponseMessage"/> instances that have the given characteristics of the
+ /// <see cref="MediaTypeMapping"/>.
+ /// </param>
+ protected MediaTypeMapping(string mediaType)
+ {
+ if (String.IsNullOrWhiteSpace(mediaType))
+ {
+ throw new ArgumentNullException("mediaType");
+ }
+
+ MediaType = new MediaTypeHeaderValue(mediaType);
+ }
+
+ /// <summary>
+ /// Gets the <see cref="MediaTypeHeaderValue"/> that is associated with <see cref="HttpRequestMessage"/> or
+ /// <see cref="HttpResponseMessage"/> instances that have the given characteristics of the
+ /// <see cref="MediaTypeMapping"/>.
+ /// </summary>
+ public MediaTypeHeaderValue MediaType { get; private set; }
+
+ /// <summary>
+ /// Returns the quality of the match of the <see cref="MediaTypeHeaderValue"/>
+ /// associated with <paramref name="request"/>.
+ /// </summary>
+ /// <param name="request">
+ /// The <see cref="HttpRequestMessage"/> to evaluate for the characteristics
+ /// associated with the <see cref="MediaTypeHeaderValue"/>
+ /// of the <see cref="MediaTypeMapping"/>.
+ /// </param>
+ /// <returns>
+ /// The quality of the match. It must be between <c>0.0</c> and <c>1.0</c>.
+ /// A value of <c>0.0</c> signifies no match.
+ /// A value of <c>1.0</c> signifies a complete match.
+ /// </returns>
+ public abstract double TryMatchMediaType(HttpRequestMessage request);
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Formatting/MediaTypeMatch.cs b/src/System.Net.Http.Formatting/Formatting/MediaTypeMatch.cs
new file mode 100644
index 00000000..c2e6e9e1
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Formatting/MediaTypeMatch.cs
@@ -0,0 +1,55 @@
+using System.Diagnostics.Contracts;
+using System.Net.Http.Headers;
+
+namespace System.Net.Http.Formatting
+{
+ /// <summary>
+ /// Class that associates a <see cref="MediaTypeHeaderValue"/> with the
+ /// the quality factor of the match.
+ /// </summary>
+ internal class MediaTypeMatch
+ {
+ /// <summary>
+ /// Quality factor to indicate a perfect match.
+ /// </summary>
+ public const double Match = 1.0;
+
+ /// <summary>
+ /// Quality factor to indicate no match.
+ /// </summary>
+ public const double NoMatch = 0.0;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MediaTypeMatch"/> class.
+ /// </summary>
+ /// <param name="mediaType">The media type that has matched.</param>
+ public MediaTypeMatch(MediaTypeHeaderValue mediaType)
+ : this(mediaType, Match)
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MediaTypeMatch"/> class.
+ /// </summary>
+ /// <param name="mediaType">The media type that has matched. A <c>null</c> value is allowed.</param>
+ /// <param name="quality">The quality of the match.</param>
+ public MediaTypeMatch(MediaTypeHeaderValue mediaType, double quality)
+ {
+ Contract.Assert(quality >= 0 && quality <= 1.0, "Quality must be from 0.0 to 1.0, inclusive.");
+
+ // We always clone the media type because it is mutable and we do not want the original source modified.
+ MediaType = mediaType == null ? null : mediaType.Clone();
+ Quality = quality;
+ }
+
+ /// <summary>
+ /// Gets the matched media type.
+ /// </summary>
+ public MediaTypeHeaderValue MediaType { get; private set; }
+
+ /// <summary>
+ /// Gets the quality of the match
+ /// </summary>
+ public double Quality { get; private set; }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Formatting/ParsedMediaTypeHeaderValue.cs b/src/System.Net.Http.Formatting/Formatting/ParsedMediaTypeHeaderValue.cs
new file mode 100644
index 00000000..0f0a4dd3
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Formatting/ParsedMediaTypeHeaderValue.cs
@@ -0,0 +1,99 @@
+using System.Diagnostics.Contracts;
+using System.Net.Http.Headers;
+
+namespace System.Net.Http.Formatting
+{
+ internal class ParsedMediaTypeHeaderValue
+ {
+ private const string MediaRangeAsterisk = "*";
+ private const char MediaTypeSubTypeDelimiter = '/';
+ private const string QualityFactorParameterName = "q";
+ private const double DefaultQualityFactor = 1.0;
+
+ private MediaTypeHeaderValue _mediaType;
+ private string _type;
+ private string _subType;
+ private bool? _hasNonQualityFactorParameter;
+ private double? _qualityFactor;
+
+ public ParsedMediaTypeHeaderValue(MediaTypeHeaderValue mediaType)
+ {
+ Contract.Assert(mediaType != null, "The 'mediaType' parameter should not be null.");
+
+ _mediaType = mediaType;
+ string[] splitMediaType = mediaType.MediaType.Split(MediaTypeSubTypeDelimiter);
+
+ Contract.Assert(splitMediaType.Length == 2, "The constructor of the MediaTypeHeaderValue would have failed if there wasn't a type and subtype.");
+
+ _type = splitMediaType[0];
+ _subType = splitMediaType[1];
+ }
+
+ public string Type
+ {
+ get { return _type; }
+ }
+
+ public string SubType
+ {
+ get { return _subType; }
+ }
+
+ public bool IsAllMediaRange
+ {
+ get { return IsSubTypeMediaRange && String.Equals(MediaRangeAsterisk, Type, StringComparison.Ordinal); }
+ }
+
+ public bool IsSubTypeMediaRange
+ {
+ get { return String.Equals(MediaRangeAsterisk, SubType, StringComparison.Ordinal); }
+ }
+
+ public bool HasNonQualityFactorParameter
+ {
+ get
+ {
+ if (!_hasNonQualityFactorParameter.HasValue)
+ {
+ _hasNonQualityFactorParameter = false;
+ foreach (NameValueHeaderValue param in _mediaType.Parameters)
+ {
+ if (!String.Equals(QualityFactorParameterName, param.Name, StringComparison.Ordinal))
+ {
+ _hasNonQualityFactorParameter = true;
+ }
+ }
+ }
+
+ return _hasNonQualityFactorParameter.Value;
+ }
+ }
+
+ public string CharSet
+ {
+ get { return _mediaType.CharSet; }
+ }
+
+ public double QualityFactor
+ {
+ get
+ {
+ if (!_qualityFactor.HasValue)
+ {
+ MediaTypeWithQualityHeaderValue mediaTypeWithQuality = _mediaType as MediaTypeWithQualityHeaderValue;
+ if (mediaTypeWithQuality != null)
+ {
+ _qualityFactor = mediaTypeWithQuality.Quality;
+ }
+
+ if (!_qualityFactor.HasValue)
+ {
+ _qualityFactor = DefaultQualityFactor;
+ }
+ }
+
+ return _qualityFactor.Value;
+ }
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Formatting/Parsers/FormUrlEncodedParser.cs b/src/System.Net.Http.Formatting/Formatting/Parsers/FormUrlEncodedParser.cs
new file mode 100644
index 00000000..56828f49
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Formatting/Parsers/FormUrlEncodedParser.cs
@@ -0,0 +1,308 @@
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+using System.Net.Http.Internal;
+using System.Text;
+
+namespace System.Net.Http.Formatting.Parsers
+{
+ /// <summary>
+ /// Buffer-oriented parsing of HTML form URL-ended, also known as <c>application/x-www-form-urlencoded</c>, data.
+ /// </summary>
+ internal class FormUrlEncodedParser
+ {
+ private const int MinMessageSize = 1;
+ private long _totalBytesConsumed;
+ private long _maxMessageSize;
+
+ private NameValueState _nameValueState;
+ private ICollection<KeyValuePair<string, string>> _nameValuePairs;
+ private CurrentNameValuePair _currentNameValuePair;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="FormUrlEncodedParser"/> class.
+ /// </summary>
+ /// <param name="nameValuePairs">The collection to which name value pairs are added as they are parsed.</param>
+ /// <param name="maxMessageSize">Maximum length of all the individual name value pairs.</param>
+ public FormUrlEncodedParser(ICollection<KeyValuePair<string, string>> nameValuePairs, long maxMessageSize)
+ {
+ // The minimum length which would be an empty buffer
+ if (maxMessageSize < MinMessageSize)
+ {
+ throw new ArgumentException(RS.Format(Properties.Resources.MinParameterSize, MinMessageSize), "maxMessageSize");
+ }
+
+ if (nameValuePairs == null)
+ {
+ throw new ArgumentNullException("nameValuePairs");
+ }
+
+ _nameValuePairs = nameValuePairs;
+ _maxMessageSize = maxMessageSize;
+ _currentNameValuePair = new CurrentNameValuePair();
+ }
+
+ private enum NameValueState
+ {
+ Name = 0,
+ Value
+ }
+
+ /// <summary>
+ /// Parse a buffer of URL form-encoded name-value pairs and add them to the <see cref="NameValueCollection"/>.
+ /// Bytes are parsed in a consuming manner from the beginning of the buffer meaning that the same bytes can not be
+ /// present in the buffer.
+ /// </summary>
+ /// <param name="buffer">Buffer from where data is read</param>
+ /// <param name="bytesReady">Size of buffer</param>
+ /// <param name="bytesConsumed">Offset into buffer</param>
+ /// <param name="isFinal">Indicates whether the end of the URL form-encoded data has been reached.</param>
+ /// <returns>State of the parser. Call this method with new data until it reaches a final state.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is translated to parse state.")]
+ public ParserState ParseBuffer(
+ byte[] buffer,
+ int bytesReady,
+ ref int bytesConsumed,
+ bool isFinal)
+ {
+ if (buffer == null)
+ {
+ throw new ArgumentNullException("buffer");
+ }
+
+ ParserState parseStatus = ParserState.NeedMoreData;
+
+ if (bytesConsumed >= bytesReady)
+ {
+ if (isFinal)
+ {
+ parseStatus = CopyCurrent(parseStatus);
+ }
+
+ // We either can already tell we need more data or we are done
+ return parseStatus;
+ }
+
+ try
+ {
+ parseStatus = ParseNameValuePairs(
+ buffer,
+ bytesReady,
+ ref bytesConsumed,
+ ref _nameValueState,
+ _maxMessageSize,
+ ref _totalBytesConsumed,
+ _currentNameValuePair,
+ _nameValuePairs);
+
+ if (isFinal)
+ {
+ parseStatus = CopyCurrent(parseStatus);
+ }
+ }
+ catch (Exception)
+ {
+ parseStatus = ParserState.Invalid;
+ }
+
+ return parseStatus;
+ }
+
+ [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "This is a parser which cannot be split up for performance reasons.")]
+ private static ParserState ParseNameValuePairs(
+ byte[] buffer,
+ int bytesReady,
+ ref int bytesConsumed,
+ ref NameValueState nameValueState,
+ long maximumLength,
+ ref long totalBytesConsumed,
+ CurrentNameValuePair currentNameValuePair,
+ ICollection<KeyValuePair<string, string>> nameValuePairs)
+ {
+ Contract.Assert((bytesReady - bytesConsumed) >= 0, "ParseNameValuePairs()|(inputBufferLength - bytesParsed) < 0");
+ Contract.Assert(maximumLength <= 0 || totalBytesConsumed <= maximumLength, "ParseNameValuePairs()|Headers already read exceeds limit.");
+
+ // Remember where we started.
+ int initialBytesParsed = bytesConsumed;
+ int segmentStart;
+
+ // Set up parsing status with what will happen if we exceed the buffer.
+ ParserState parseStatus = ParserState.DataTooBig;
+ long effectiveMax = maximumLength <= 0 ? Int64.MaxValue : maximumLength - totalBytesConsumed + initialBytesParsed;
+ if (bytesReady < effectiveMax)
+ {
+ parseStatus = ParserState.NeedMoreData;
+ effectiveMax = bytesReady;
+ }
+
+ Contract.Assert(bytesConsumed < effectiveMax, "We have already consumed more than the max buffer length.");
+
+ switch (nameValueState)
+ {
+ case NameValueState.Name:
+ segmentStart = bytesConsumed;
+ while (buffer[bytesConsumed] != '=' && buffer[bytesConsumed] != '&')
+ {
+ if (++bytesConsumed == effectiveMax)
+ {
+ string name = Encoding.UTF8.GetString(buffer, segmentStart, bytesConsumed - segmentStart);
+ currentNameValuePair.Name.Append(name);
+ goto quit;
+ }
+ }
+
+ if (bytesConsumed > segmentStart)
+ {
+ string name = Encoding.UTF8.GetString(buffer, segmentStart, bytesConsumed - segmentStart);
+ currentNameValuePair.Name.Append(name);
+ }
+
+ // Check if we got name=value or just name
+ if (buffer[bytesConsumed] == '=')
+ {
+ // Move part the '='
+ nameValueState = NameValueState.Value;
+ if (++bytesConsumed == effectiveMax)
+ {
+ goto quit;
+ }
+
+ goto case NameValueState.Value;
+ }
+ else
+ {
+ // Copy parsed name-only to collection
+ currentNameValuePair.CopyNameOnlyTo(nameValuePairs);
+
+ // Move past the '&' but stay in same state
+ if (++bytesConsumed == effectiveMax)
+ {
+ goto quit;
+ }
+
+ goto case NameValueState.Name;
+ }
+
+ case NameValueState.Value:
+ segmentStart = bytesConsumed;
+ while (buffer[bytesConsumed] != '&')
+ {
+ if (++bytesConsumed == effectiveMax)
+ {
+ string value = Encoding.UTF8.GetString(buffer, segmentStart, bytesConsumed - segmentStart);
+ currentNameValuePair.Value.Append(value);
+ goto quit;
+ }
+ }
+
+ if (bytesConsumed > segmentStart)
+ {
+ string value = Encoding.UTF8.GetString(buffer, segmentStart, bytesConsumed - segmentStart);
+ currentNameValuePair.Value.Append(value);
+ }
+
+ // Copy parsed name value pair to collection
+ currentNameValuePair.CopyTo(nameValuePairs);
+
+ // Move past the '&'
+ nameValueState = NameValueState.Name;
+ if (++bytesConsumed == effectiveMax)
+ {
+ goto quit;
+ }
+
+ goto case NameValueState.Name;
+ }
+
+ quit:
+ totalBytesConsumed += bytesConsumed - initialBytesParsed;
+ return parseStatus;
+ }
+
+ private ParserState CopyCurrent(ParserState parseState)
+ {
+ // Copy parsed name value pair to collection
+ if (_nameValueState == NameValueState.Name)
+ {
+ if (_totalBytesConsumed > 0)
+ {
+ _currentNameValuePair.CopyNameOnlyTo(_nameValuePairs);
+ }
+ }
+ else
+ {
+ _currentNameValuePair.CopyTo(_nameValuePairs);
+ }
+
+ // We are done (or in an error state)
+ return parseState == ParserState.NeedMoreData ? ParserState.Done : parseState;
+ }
+
+ /// <summary>
+ /// Maintains information about the current header field being parsed.
+ /// </summary>
+ private class CurrentNameValuePair
+ {
+ private const int DefaultNameAllocation = 128;
+ private const int DefaultValueAllocation = 2 * 1024;
+
+ private readonly StringBuilder _name = new StringBuilder(DefaultNameAllocation);
+ private readonly StringBuilder _value = new StringBuilder(DefaultValueAllocation);
+
+ /// <summary>
+ /// Gets the name of the name value pair.
+ /// </summary>
+ public StringBuilder Name
+ {
+ get { return _name; }
+ }
+
+ /// <summary>
+ /// Gets the value of the name value pair
+ /// </summary>
+ public StringBuilder Value
+ {
+ get { return _value; }
+ }
+
+ /// <summary>
+ /// Copies current name value pair field to the provided <see cref="NameValueCollection"/> instance.
+ /// </summary>
+ /// <param name="nameValuePairs">The <see cref="NameValueCollection"/>.</param>
+ public void CopyTo(ICollection<KeyValuePair<string, string>> nameValuePairs)
+ {
+ string unescapedName = UriQueryUtility.UrlDecode(_name.ToString());
+ string escapedValue = _value.ToString();
+
+ nameValuePairs.Add(new KeyValuePair<string, string>(
+ unescapedName,
+ escapedValue.Equals(FormattingUtilities.JsonNullLiteral, StringComparison.Ordinal)
+ ? null
+ : UriQueryUtility.UrlDecode(escapedValue)));
+
+ Clear();
+ }
+
+ /// <summary>
+ /// Copies current name-only to the provided <see cref="NameValueCollection"/> instance.
+ /// </summary>
+ /// <param name="nameValuePairs">The <see cref="NameValueCollection"/>.</param>
+ public void CopyNameOnlyTo(ICollection<KeyValuePair<string, string>> nameValuePairs)
+ {
+ nameValuePairs.Add(new KeyValuePair<string, string>(null, UriQueryUtility.UrlDecode(_name.ToString())));
+
+ Clear();
+ }
+
+ /// <summary>
+ /// Clears this instance.
+ /// </summary>
+ private void Clear()
+ {
+ _name.Clear();
+ _value.Clear();
+ }
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Formatting/Parsers/HttpRequestHeaderParser.cs b/src/System.Net.Http.Formatting/Formatting/Parsers/HttpRequestHeaderParser.cs
new file mode 100644
index 00000000..6cc239e2
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Formatting/Parsers/HttpRequestHeaderParser.cs
@@ -0,0 +1,137 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Net.Http.Formatting.Parsers
+{
+ /// <summary>
+ /// The <see cref="HttpRequestHeaderParser"/> combines <see cref="HttpRequestLineParser"/> for parsing the HTTP Request Line
+ /// and <see cref="InternetMessageFormatHeaderParser"/> for parsing each header field.
+ /// </summary>
+ internal class HttpRequestHeaderParser
+ {
+ private const int DefaultMaxRequestLineSize = 2 * 1024;
+ private const int DefaultMaxHeaderSize = 16 * 1024; // Same default size as IIS has for regular requests
+
+ private HttpUnsortedRequest _httpRequest;
+ private HttpRequestState _requestStatus = HttpRequestState.RequestLine;
+
+ private HttpRequestLineParser _requestLineParser;
+ private InternetMessageFormatHeaderParser _headerParser;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpRequestHeaderParser"/> class.
+ /// </summary>
+ /// <param name="httpRequest">The parsed HTTP request without any header sorting.</param>
+ public HttpRequestHeaderParser(HttpUnsortedRequest httpRequest)
+ : this(httpRequest, DefaultMaxRequestLineSize, DefaultMaxHeaderSize)
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpRequestHeaderParser"/> class.
+ /// </summary>
+ /// <param name="httpRequest">The parsed HTTP request without any header sorting.</param>
+ /// <param name="maxRequestLineSize">The max length of the HTTP request line.</param>
+ /// <param name="maxHeaderSize">The max length of the HTTP header.</param>
+ public HttpRequestHeaderParser(HttpUnsortedRequest httpRequest, int maxRequestLineSize, int maxHeaderSize)
+ {
+ if (httpRequest == null)
+ {
+ throw new ArgumentNullException("httpRequest");
+ }
+
+ _httpRequest = httpRequest;
+
+ // Create request line parser
+ _requestLineParser = new HttpRequestLineParser(_httpRequest, maxRequestLineSize);
+
+ // Create header parser
+ _headerParser = new InternetMessageFormatHeaderParser(_httpRequest.HttpHeaders, maxHeaderSize);
+ }
+
+ private enum HttpRequestState
+ {
+ RequestLine = 0, // parsing request line
+ RequestHeaders // reading headers
+ }
+
+ /// <summary>
+ /// Parse an HTTP request header and fill in the <see cref="HttpRequestMessage"/> instance.
+ /// </summary>
+ /// <param name="buffer">Request buffer from where request is read</param>
+ /// <param name="bytesReady">Size of request buffer</param>
+ /// <param name="bytesConsumed">Offset into request buffer</param>
+ /// <returns>State of the parser.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is propagated.")]
+ public ParserState ParseBuffer(
+ byte[] buffer,
+ int bytesReady,
+ ref int bytesConsumed)
+ {
+ if (buffer == null)
+ {
+ throw new ArgumentNullException("buffer");
+ }
+
+ ParserState parseStatus = ParserState.NeedMoreData;
+ ParserState subParseStatus = ParserState.NeedMoreData;
+
+ switch (_requestStatus)
+ {
+ case HttpRequestState.RequestLine:
+ try
+ {
+ subParseStatus = _requestLineParser.ParseBuffer(buffer, bytesReady, ref bytesConsumed);
+ }
+ catch (Exception)
+ {
+ subParseStatus = ParserState.Invalid;
+ }
+
+ if (subParseStatus == ParserState.Done)
+ {
+ _requestStatus = HttpRequestState.RequestHeaders;
+ subParseStatus = ParserState.NeedMoreData;
+ goto case HttpRequestState.RequestHeaders;
+ }
+ else if (subParseStatus != ParserState.NeedMoreData)
+ {
+ // Report error - either Invalid or DataTooBig
+ parseStatus = subParseStatus;
+ break;
+ }
+
+ break; // read more data
+
+ case HttpRequestState.RequestHeaders:
+ if (bytesConsumed >= bytesReady)
+ {
+ // we already can tell we need more data
+ break;
+ }
+
+ try
+ {
+ subParseStatus = _headerParser.ParseBuffer(buffer, bytesReady, ref bytesConsumed);
+ }
+ catch (Exception)
+ {
+ subParseStatus = ParserState.Invalid;
+ }
+
+ if (subParseStatus == ParserState.Done)
+ {
+ parseStatus = subParseStatus;
+ }
+ else if (subParseStatus != ParserState.NeedMoreData)
+ {
+ parseStatus = subParseStatus;
+ break;
+ }
+
+ break; // need more data
+ }
+
+ return parseStatus;
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Formatting/Parsers/HttpRequestLineParser.cs b/src/System.Net.Http.Formatting/Formatting/Parsers/HttpRequestLineParser.cs
new file mode 100644
index 00000000..760de58a
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Formatting/Parsers/HttpRequestLineParser.cs
@@ -0,0 +1,343 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+using System.Text;
+
+namespace System.Net.Http.Formatting.Parsers
+{
+ /// <summary>
+ /// HTTP Request Line parser for parsing the first line (the request line) in an HTTP request.
+ /// </summary>
+ internal class HttpRequestLineParser
+ {
+ internal const int MinRequestLineSize = 14;
+ private const int DefaultTokenAllocation = 2 * 1024;
+
+ private int _totalBytesConsumed;
+ private int _maximumHeaderLength;
+
+ private HttpRequestLineState _requestLineState;
+ private HttpUnsortedRequest _httpRequest;
+ private StringBuilder _currentToken = new StringBuilder(DefaultTokenAllocation);
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpRequestLineParser"/> class.
+ /// </summary>
+ /// <param name="httpRequest"><see cref="HttpUnsortedRequest"/> instance where the request line properties will be set as they are parsed.</param>
+ /// <param name="maxRequestLineSize">Maximum length of HTTP header.</param>
+ public HttpRequestLineParser(HttpUnsortedRequest httpRequest, int maxRequestLineSize)
+ {
+ // The minimum length which would be an empty header terminated by CRLF
+ if (maxRequestLineSize < MinRequestLineSize)
+ {
+ throw new ArgumentException(RS.Format(Properties.Resources.MinParameterSize, MinRequestLineSize), "maxRequestLineSize");
+ }
+
+ if (httpRequest == null)
+ {
+ throw new ArgumentNullException("httpRequest");
+ }
+
+ _httpRequest = httpRequest;
+ _maximumHeaderLength = maxRequestLineSize;
+ }
+
+ private enum HttpRequestLineState
+ {
+ RequestMethod = 0,
+ RequestUri,
+ BeforeVersionNumbers,
+ MajorVersionNumber,
+ MinorVersionNumber,
+ AfterCarriageReturn
+ }
+
+ /// <summary>
+ /// Parse an HTTP request line.
+ /// Bytes are parsed in a consuming manner from the beginning of the request buffer meaning that the same bytes can not be
+ /// present in the request buffer.
+ /// </summary>
+ /// <param name="buffer">Request buffer from where request is read</param>
+ /// <param name="bytesReady">Size of request buffer</param>
+ /// <param name="bytesConsumed">Offset into request buffer</param>
+ /// <returns>State of the parser.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is translated to parse state.")]
+ public ParserState ParseBuffer(
+ byte[] buffer,
+ int bytesReady,
+ ref int bytesConsumed)
+ {
+ if (buffer == null)
+ {
+ throw new ArgumentNullException("buffer");
+ }
+
+ ParserState parseStatus = ParserState.NeedMoreData;
+
+ if (bytesConsumed >= bytesReady)
+ {
+ // We already can tell we need more data
+ return parseStatus;
+ }
+
+ try
+ {
+ parseStatus = ParseRequestLine(
+ buffer,
+ bytesReady,
+ ref bytesConsumed,
+ ref _requestLineState,
+ _maximumHeaderLength,
+ ref _totalBytesConsumed,
+ _currentToken,
+ _httpRequest);
+ }
+ catch (Exception)
+ {
+ parseStatus = ParserState.Invalid;
+ }
+
+ return parseStatus;
+ }
+
+ [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "This is a parser which cannot be split up for performance reasons.")]
+ private static ParserState ParseRequestLine(
+ byte[] buffer,
+ int bytesReady,
+ ref int bytesConsumed,
+ ref HttpRequestLineState requestLineState,
+ int maximumHeaderLength,
+ ref int totalBytesConsumed,
+ StringBuilder currentToken,
+ HttpUnsortedRequest httpRequest)
+ {
+ Contract.Assert((bytesReady - bytesConsumed) >= 0, "ParseRequestLine()|(bytesReady - bytesConsumed) < 0");
+ Contract.Assert(maximumHeaderLength <= 0 || totalBytesConsumed <= maximumHeaderLength, "ParseRequestLine()|Headers already read exceeds limit.");
+
+ // Remember where we started.
+ int initialBytesParsed = bytesConsumed;
+ int segmentStart;
+
+ // Set up parsing status with what will happen if we exceed the buffer.
+ ParserState parseStatus = ParserState.DataTooBig;
+ int effectiveMax = maximumHeaderLength <= 0 ? Int32.MaxValue : (maximumHeaderLength - totalBytesConsumed + bytesConsumed);
+ if (bytesReady < effectiveMax)
+ {
+ parseStatus = ParserState.NeedMoreData;
+ effectiveMax = bytesReady;
+ }
+
+ Contract.Assert(bytesConsumed < effectiveMax, "We have already consumed more than the max header length.");
+
+ switch (requestLineState)
+ {
+ case HttpRequestLineState.RequestMethod:
+ segmentStart = bytesConsumed;
+ while (buffer[bytesConsumed] != ' ')
+ {
+ if (buffer[bytesConsumed] < 0x21 || buffer[bytesConsumed] > 0x7a)
+ {
+ parseStatus = ParserState.Invalid;
+ goto quit;
+ }
+
+ if (++bytesConsumed == effectiveMax)
+ {
+ string method = Encoding.UTF8.GetString(buffer, segmentStart, bytesConsumed - segmentStart);
+ currentToken.Append(method);
+ goto quit;
+ }
+ }
+
+ if (bytesConsumed > segmentStart)
+ {
+ string method = Encoding.UTF8.GetString(buffer, segmentStart, bytesConsumed - segmentStart);
+ currentToken.Append(method);
+ }
+
+ // Copy value out
+ httpRequest.Method = new HttpMethod(currentToken.ToString());
+ currentToken.Clear();
+
+ // Move past the SP
+ requestLineState = HttpRequestLineState.RequestUri;
+ if (++bytesConsumed == effectiveMax)
+ {
+ goto quit;
+ }
+
+ goto case HttpRequestLineState.RequestUri;
+
+ case HttpRequestLineState.RequestUri:
+ segmentStart = bytesConsumed;
+ while (buffer[bytesConsumed] != ' ')
+ {
+ if (buffer[bytesConsumed] == '\r')
+ {
+ parseStatus = ParserState.Invalid;
+ goto quit;
+ }
+
+ if (++bytesConsumed == effectiveMax)
+ {
+ string addr = Encoding.UTF8.GetString(buffer, segmentStart, bytesConsumed - segmentStart);
+ currentToken.Append(addr);
+ goto quit;
+ }
+ }
+
+ if (bytesConsumed > segmentStart)
+ {
+ string addr = Encoding.UTF8.GetString(buffer, segmentStart, bytesConsumed - segmentStart);
+ currentToken.Append(addr);
+ }
+
+ // URI validation happens when we create the URI later.
+ if (currentToken.Length == 0)
+ {
+ throw new FormatException(Properties.Resources.HttpMessageParserEmptyUri);
+ }
+
+ // Copy value out
+ httpRequest.RequestUri = currentToken.ToString();
+ currentToken.Clear();
+
+ // Move past the SP
+ requestLineState = HttpRequestLineState.BeforeVersionNumbers;
+ if (++bytesConsumed == effectiveMax)
+ {
+ goto quit;
+ }
+
+ goto case HttpRequestLineState.BeforeVersionNumbers;
+
+ case HttpRequestLineState.BeforeVersionNumbers:
+ segmentStart = bytesConsumed;
+ while (buffer[bytesConsumed] != '/')
+ {
+ if (buffer[bytesConsumed] < 0x21 || buffer[bytesConsumed] > 0x7a)
+ {
+ parseStatus = ParserState.Invalid;
+ goto quit;
+ }
+
+ if (++bytesConsumed == effectiveMax)
+ {
+ string token = Encoding.UTF8.GetString(buffer, segmentStart, bytesConsumed - segmentStart);
+ currentToken.Append(token);
+ goto quit;
+ }
+ }
+
+ if (bytesConsumed > segmentStart)
+ {
+ string token = Encoding.UTF8.GetString(buffer, segmentStart, bytesConsumed - segmentStart);
+ currentToken.Append(token);
+ }
+
+ // Validate value
+ string version = currentToken.ToString();
+ if (String.CompareOrdinal(FormattingUtilities.HttpVersionToken, version) != 0)
+ {
+ throw new FormatException(RS.Format(Properties.Resources.HttpInvalidVersion, version, FormattingUtilities.HttpVersionToken));
+ }
+
+ currentToken.Clear();
+
+ // Move past the '/'
+ requestLineState = HttpRequestLineState.MajorVersionNumber;
+ if (++bytesConsumed == effectiveMax)
+ {
+ goto quit;
+ }
+
+ goto case HttpRequestLineState.MajorVersionNumber;
+
+ case HttpRequestLineState.MajorVersionNumber:
+ segmentStart = bytesConsumed;
+ while (buffer[bytesConsumed] != '.')
+ {
+ if (buffer[bytesConsumed] < '0' || buffer[bytesConsumed] > '9')
+ {
+ parseStatus = ParserState.Invalid;
+ goto quit;
+ }
+
+ if (++bytesConsumed == effectiveMax)
+ {
+ string major = Encoding.UTF8.GetString(buffer, segmentStart, bytesConsumed - segmentStart);
+ currentToken.Append(major);
+ goto quit;
+ }
+ }
+
+ if (bytesConsumed > segmentStart)
+ {
+ string major = Encoding.UTF8.GetString(buffer, segmentStart, bytesConsumed - segmentStart);
+ currentToken.Append(major);
+ }
+
+ // Move past the "."
+ currentToken.Append('.');
+ requestLineState = HttpRequestLineState.MinorVersionNumber;
+ if (++bytesConsumed == effectiveMax)
+ {
+ goto quit;
+ }
+
+ goto case HttpRequestLineState.MinorVersionNumber;
+
+ case HttpRequestLineState.MinorVersionNumber:
+ segmentStart = bytesConsumed;
+ while (buffer[bytesConsumed] != '\r')
+ {
+ if (buffer[bytesConsumed] < '0' || buffer[bytesConsumed] > '9')
+ {
+ parseStatus = ParserState.Invalid;
+ goto quit;
+ }
+
+ if (++bytesConsumed == effectiveMax)
+ {
+ string minor = Encoding.UTF8.GetString(buffer, segmentStart, bytesConsumed - segmentStart);
+ currentToken.Append(minor);
+ goto quit;
+ }
+ }
+
+ if (bytesConsumed > segmentStart)
+ {
+ string minor = Encoding.UTF8.GetString(buffer, segmentStart, bytesConsumed - segmentStart);
+ currentToken.Append(minor);
+ }
+
+ // Copy out value
+ httpRequest.Version = Version.Parse(currentToken.ToString());
+ currentToken.Clear();
+
+ // Move past the CR
+ requestLineState = HttpRequestLineState.AfterCarriageReturn;
+ if (++bytesConsumed == effectiveMax)
+ {
+ goto quit;
+ }
+
+ goto case HttpRequestLineState.AfterCarriageReturn;
+
+ case HttpRequestLineState.AfterCarriageReturn:
+ if (buffer[bytesConsumed] != '\n')
+ {
+ parseStatus = ParserState.Invalid;
+ goto quit;
+ }
+
+ parseStatus = ParserState.Done;
+ bytesConsumed++;
+ break;
+ }
+
+ quit:
+ totalBytesConsumed += bytesConsumed - initialBytesParsed;
+ return parseStatus;
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Formatting/Parsers/HttpResponseHeaderParser.cs b/src/System.Net.Http.Formatting/Formatting/Parsers/HttpResponseHeaderParser.cs
new file mode 100644
index 00000000..2f4fe3ac
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Formatting/Parsers/HttpResponseHeaderParser.cs
@@ -0,0 +1,137 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Net.Http.Formatting.Parsers
+{
+ /// <summary>
+ /// The <see cref="HttpResponseHeaderParser"/> combines <see cref="HttpStatusLineParser"/> for parsing the HTTP Status Line
+ /// and <see cref="InternetMessageFormatHeaderParser"/> for parsing each header field.
+ /// </summary>
+ internal class HttpResponseHeaderParser
+ {
+ private const int DefaultMaxStatusLineSize = 2 * 1024;
+ private const int DefaultMaxHeaderSize = 16 * 1024; // Same default size as IIS has for HTTP requests
+
+ private HttpUnsortedResponse _httpResponse;
+ private HttpResponseState _responseStatus = HttpResponseState.StatusLine;
+
+ private HttpStatusLineParser _statusLineParser;
+ private InternetMessageFormatHeaderParser _headerParser;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpResponseHeaderParser"/> class.
+ /// </summary>
+ /// <param name="httpResponse">The parsed HTTP response without any header sorting.</param>
+ public HttpResponseHeaderParser(HttpUnsortedResponse httpResponse)
+ : this(httpResponse, DefaultMaxStatusLineSize, DefaultMaxHeaderSize)
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpResponseHeaderParser"/> class.
+ /// </summary>
+ /// <param name="httpResponse">The parsed HTTP response without any header sorting.</param>
+ /// <param name="maxResponseLineSize">The max length of the HTTP status line.</param>
+ /// <param name="maxHeaderSize">The max length of the HTTP header.</param>
+ public HttpResponseHeaderParser(HttpUnsortedResponse httpResponse, int maxResponseLineSize, int maxHeaderSize)
+ {
+ if (httpResponse == null)
+ {
+ throw new ArgumentNullException("httpResponse");
+ }
+
+ _httpResponse = httpResponse;
+
+ // Create status line parser
+ _statusLineParser = new HttpStatusLineParser(_httpResponse, maxResponseLineSize);
+
+ // Create header parser
+ _headerParser = new InternetMessageFormatHeaderParser(_httpResponse.HttpHeaders, maxHeaderSize);
+ }
+
+ private enum HttpResponseState
+ {
+ StatusLine = 0, // parsing status line
+ ResponseHeaders // reading headers
+ }
+
+ /// <summary>
+ /// Parse an HTTP response header and fill in the <see cref="HttpResponseMessage"/> instance.
+ /// </summary>
+ /// <param name="buffer">Response buffer from where response is read</param>
+ /// <param name="bytesReady">Size of response buffer</param>
+ /// <param name="bytesConsumed">Offset into response buffer</param>
+ /// <returns>State of the parser.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is propagated.")]
+ public ParserState ParseBuffer(
+ byte[] buffer,
+ int bytesReady,
+ ref int bytesConsumed)
+ {
+ if (buffer == null)
+ {
+ throw new ArgumentNullException("buffer");
+ }
+
+ ParserState parseStatus = ParserState.NeedMoreData;
+ ParserState subParseStatus = ParserState.NeedMoreData;
+
+ switch (_responseStatus)
+ {
+ case HttpResponseState.StatusLine:
+ try
+ {
+ subParseStatus = _statusLineParser.ParseBuffer(buffer, bytesReady, ref bytesConsumed);
+ }
+ catch (Exception)
+ {
+ subParseStatus = ParserState.Invalid;
+ }
+
+ if (subParseStatus == ParserState.Done)
+ {
+ _responseStatus = HttpResponseState.ResponseHeaders;
+ subParseStatus = ParserState.NeedMoreData;
+ goto case HttpResponseState.ResponseHeaders;
+ }
+ else if (subParseStatus != ParserState.NeedMoreData)
+ {
+ // Report error - either Invalid or DataTooBig
+ parseStatus = subParseStatus;
+ break;
+ }
+
+ break; // read more data
+
+ case HttpResponseState.ResponseHeaders:
+ if (bytesConsumed >= bytesReady)
+ {
+ // we already can tell we need more data
+ break;
+ }
+
+ try
+ {
+ subParseStatus = _headerParser.ParseBuffer(buffer, bytesReady, ref bytesConsumed);
+ }
+ catch (Exception)
+ {
+ subParseStatus = ParserState.Invalid;
+ }
+
+ if (subParseStatus == ParserState.Done)
+ {
+ parseStatus = subParseStatus;
+ }
+ else if (subParseStatus != ParserState.NeedMoreData)
+ {
+ parseStatus = subParseStatus;
+ break;
+ }
+
+ break; // need more data
+ }
+
+ return parseStatus;
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Formatting/Parsers/HttpStatusLineParser.cs b/src/System.Net.Http.Formatting/Formatting/Parsers/HttpStatusLineParser.cs
new file mode 100644
index 00000000..600899e7
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Formatting/Parsers/HttpStatusLineParser.cs
@@ -0,0 +1,345 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+using System.Globalization;
+using System.Text;
+
+namespace System.Net.Http.Formatting.Parsers
+{
+ /// <summary>
+ /// HTTP Status line parser for parsing the first line (the status line) in an HTTP response.
+ /// </summary>
+ internal class HttpStatusLineParser
+ {
+ internal const int MinStatusLineSize = 15;
+ private const int DefaultTokenAllocation = 2 * 1024;
+ private const int MaxStatusCode = 1000;
+
+ private int _totalBytesConsumed;
+ private int _maximumHeaderLength;
+
+ private HttpStatusLineState _statusLineState;
+ private HttpUnsortedResponse _httpResponse;
+ private StringBuilder _currentToken = new StringBuilder(DefaultTokenAllocation);
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpStatusLineParser"/> class.
+ /// </summary>
+ /// <param name="httpResponse"><see cref="HttpUnsortedResponse"/> instance where the response line properties will be set as they are parsed.</param>
+ /// <param name="maxStatusLineSize">Maximum length of HTTP header.</param>
+ public HttpStatusLineParser(HttpUnsortedResponse httpResponse, int maxStatusLineSize)
+ {
+ // The minimum length which would be an empty header terminated by CRLF
+ if (maxStatusLineSize < MinStatusLineSize)
+ {
+ throw new ArgumentException(RS.Format(Properties.Resources.MinParameterSize, MinStatusLineSize), "maxStatusLineSize");
+ }
+
+ if (httpResponse == null)
+ {
+ throw new ArgumentNullException("httpResponse");
+ }
+
+ _httpResponse = httpResponse;
+ _maximumHeaderLength = maxStatusLineSize;
+ }
+
+ private enum HttpStatusLineState
+ {
+ BeforeVersionNumbers = 0,
+ MajorVersionNumber,
+ MinorVersionNumber,
+ StatusCode,
+ ReasonPhrase,
+ AfterCarriageReturn
+ }
+
+ /// <summary>
+ /// Parse an HTTP status line.
+ /// Bytes are parsed in a consuming manner from the beginning of the response buffer meaning that the same bytes can not be
+ /// present in the response buffer.
+ /// </summary>
+ /// <param name="buffer">Response buffer from where response is read</param>
+ /// <param name="bytesReady">Size of response buffer</param>
+ /// <param name="bytesConsumed">Offset into response buffer</param>
+ /// <returns>State of the parser.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is translated to parse state.")]
+ public ParserState ParseBuffer(
+ byte[] buffer,
+ int bytesReady,
+ ref int bytesConsumed)
+ {
+ if (buffer == null)
+ {
+ throw new ArgumentNullException("buffer");
+ }
+
+ ParserState parseStatus = ParserState.NeedMoreData;
+
+ if (bytesConsumed >= bytesReady)
+ {
+ // We already can tell we need more data
+ return parseStatus;
+ }
+
+ try
+ {
+ parseStatus = ParseStatusLine(
+ buffer,
+ bytesReady,
+ ref bytesConsumed,
+ ref _statusLineState,
+ _maximumHeaderLength,
+ ref _totalBytesConsumed,
+ _currentToken,
+ _httpResponse);
+ }
+ catch (Exception)
+ {
+ parseStatus = ParserState.Invalid;
+ }
+
+ return parseStatus;
+ }
+
+ [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "This is a parser which cannot be split up for performance reasons.")]
+ private static ParserState ParseStatusLine(
+ byte[] buffer,
+ int bytesReady,
+ ref int bytesConsumed,
+ ref HttpStatusLineState statusLineState,
+ int maximumHeaderLength,
+ ref int totalBytesConsumed,
+ StringBuilder currentToken,
+ HttpUnsortedResponse httpResponse)
+ {
+ Contract.Assert((bytesReady - bytesConsumed) >= 0, "ParseRequestLine()|(bytesReady - bytesConsumed) < 0");
+ Contract.Assert(maximumHeaderLength <= 0 || totalBytesConsumed <= maximumHeaderLength, "ParseRequestLine()|Headers already read exceeds limit.");
+
+ // Remember where we started.
+ int initialBytesParsed = bytesConsumed;
+ int segmentStart;
+
+ // Set up parsing status with what will happen if we exceed the buffer.
+ ParserState parseStatus = ParserState.DataTooBig;
+ int effectiveMax = maximumHeaderLength <= 0 ? Int32.MaxValue : (maximumHeaderLength - totalBytesConsumed + bytesConsumed);
+ if (bytesReady < effectiveMax)
+ {
+ parseStatus = ParserState.NeedMoreData;
+ effectiveMax = bytesReady;
+ }
+
+ Contract.Assert(bytesConsumed < effectiveMax, "We have already consumed more than the max header length.");
+
+ switch (statusLineState)
+ {
+ case HttpStatusLineState.BeforeVersionNumbers:
+ segmentStart = bytesConsumed;
+ while (buffer[bytesConsumed] != '/')
+ {
+ if (buffer[bytesConsumed] < 0x21 || buffer[bytesConsumed] > 0x7a)
+ {
+ parseStatus = ParserState.Invalid;
+ goto quit;
+ }
+
+ if (++bytesConsumed == effectiveMax)
+ {
+ string token = Encoding.UTF8.GetString(buffer, segmentStart, bytesConsumed - segmentStart);
+ currentToken.Append(token);
+ goto quit;
+ }
+ }
+
+ if (bytesConsumed > segmentStart)
+ {
+ string token = Encoding.UTF8.GetString(buffer, segmentStart, bytesConsumed - segmentStart);
+ currentToken.Append(token);
+ }
+
+ // Validate value
+ string version = currentToken.ToString();
+ if (String.CompareOrdinal(FormattingUtilities.HttpVersionToken, version) != 0)
+ {
+ throw new FormatException(RS.Format(Properties.Resources.HttpInvalidVersion, version, FormattingUtilities.HttpVersionToken));
+ }
+
+ currentToken.Clear();
+
+ // Move past the '/'
+ statusLineState = HttpStatusLineState.MajorVersionNumber;
+ if (++bytesConsumed == effectiveMax)
+ {
+ goto quit;
+ }
+
+ goto case HttpStatusLineState.MajorVersionNumber;
+
+ case HttpStatusLineState.MajorVersionNumber:
+ segmentStart = bytesConsumed;
+ while (buffer[bytesConsumed] != '.')
+ {
+ if (buffer[bytesConsumed] < '0' || buffer[bytesConsumed] > '9')
+ {
+ parseStatus = ParserState.Invalid;
+ goto quit;
+ }
+
+ if (++bytesConsumed == effectiveMax)
+ {
+ string major = Encoding.UTF8.GetString(buffer, segmentStart, bytesConsumed - segmentStart);
+ currentToken.Append(major);
+ goto quit;
+ }
+ }
+
+ if (bytesConsumed > segmentStart)
+ {
+ string major = Encoding.UTF8.GetString(buffer, segmentStart, bytesConsumed - segmentStart);
+ currentToken.Append(major);
+ }
+
+ // Move past the "."
+ currentToken.Append('.');
+ statusLineState = HttpStatusLineState.MinorVersionNumber;
+ if (++bytesConsumed == effectiveMax)
+ {
+ goto quit;
+ }
+
+ goto case HttpStatusLineState.MinorVersionNumber;
+
+ case HttpStatusLineState.MinorVersionNumber:
+ segmentStart = bytesConsumed;
+ while (buffer[bytesConsumed] != ' ')
+ {
+ if (buffer[bytesConsumed] < '0' || buffer[bytesConsumed] > '9')
+ {
+ parseStatus = ParserState.Invalid;
+ goto quit;
+ }
+
+ if (++bytesConsumed == effectiveMax)
+ {
+ string minor = Encoding.UTF8.GetString(buffer, segmentStart, bytesConsumed - segmentStart);
+ currentToken.Append(minor);
+ goto quit;
+ }
+ }
+
+ if (bytesConsumed > segmentStart)
+ {
+ string minor = Encoding.UTF8.GetString(buffer, segmentStart, bytesConsumed - segmentStart);
+ currentToken.Append(minor);
+ }
+
+ // Copy out value
+ httpResponse.Version = Version.Parse(currentToken.ToString());
+ currentToken.Clear();
+
+ // Move past the SP
+ statusLineState = HttpStatusLineState.StatusCode;
+ if (++bytesConsumed == effectiveMax)
+ {
+ goto quit;
+ }
+
+ goto case HttpStatusLineState.StatusCode;
+
+ case HttpStatusLineState.StatusCode:
+ segmentStart = bytesConsumed;
+ while (buffer[bytesConsumed] != ' ')
+ {
+ if (buffer[bytesConsumed] < '0' || buffer[bytesConsumed] > '9')
+ {
+ parseStatus = ParserState.Invalid;
+ goto quit;
+ }
+
+ if (++bytesConsumed == effectiveMax)
+ {
+ string method = Encoding.UTF8.GetString(buffer, segmentStart, bytesConsumed - segmentStart);
+ currentToken.Append(method);
+ goto quit;
+ }
+ }
+
+ if (bytesConsumed > segmentStart)
+ {
+ string method = Encoding.UTF8.GetString(buffer, segmentStart, bytesConsumed - segmentStart);
+ currentToken.Append(method);
+ }
+
+ // Copy value out
+ int statusCode = Int32.Parse(currentToken.ToString(), CultureInfo.InvariantCulture);
+ if (statusCode < 100 || statusCode > 1000)
+ {
+ throw new FormatException(RS.Format(Properties.Resources.HttpInvalidStatusCode, statusCode, 100, 1000));
+ }
+
+ httpResponse.StatusCode = (HttpStatusCode)statusCode;
+ currentToken.Clear();
+
+ // Move past the SP
+ statusLineState = HttpStatusLineState.ReasonPhrase;
+ if (++bytesConsumed == effectiveMax)
+ {
+ goto quit;
+ }
+
+ goto case HttpStatusLineState.ReasonPhrase;
+
+ case HttpStatusLineState.ReasonPhrase:
+ segmentStart = bytesConsumed;
+ while (buffer[bytesConsumed] != '\r')
+ {
+ if (buffer[bytesConsumed] < 0x20 || buffer[bytesConsumed] > 0x7a)
+ {
+ parseStatus = ParserState.Invalid;
+ goto quit;
+ }
+
+ if (++bytesConsumed == effectiveMax)
+ {
+ string addr = Encoding.UTF8.GetString(buffer, segmentStart, bytesConsumed - segmentStart);
+ currentToken.Append(addr);
+ goto quit;
+ }
+ }
+
+ if (bytesConsumed > segmentStart)
+ {
+ string addr = Encoding.UTF8.GetString(buffer, segmentStart, bytesConsumed - segmentStart);
+ currentToken.Append(addr);
+ }
+
+ // Copy value out
+ httpResponse.ReasonPhrase = currentToken.ToString();
+ currentToken.Clear();
+
+ // Move past the CR
+ statusLineState = HttpStatusLineState.AfterCarriageReturn;
+ if (++bytesConsumed == effectiveMax)
+ {
+ goto quit;
+ }
+
+ goto case HttpStatusLineState.AfterCarriageReturn;
+
+ case HttpStatusLineState.AfterCarriageReturn:
+ if (buffer[bytesConsumed] != '\n')
+ {
+ parseStatus = ParserState.Invalid;
+ goto quit;
+ }
+
+ parseStatus = ParserState.Done;
+ bytesConsumed++;
+ break;
+ }
+
+ quit:
+ totalBytesConsumed += bytesConsumed - initialBytesParsed;
+ return parseStatus;
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Formatting/Parsers/InternetMessageFormatHeaderParser.cs b/src/System.Net.Http.Formatting/Formatting/Parsers/InternetMessageFormatHeaderParser.cs
new file mode 100644
index 00000000..06747bcf
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Formatting/Parsers/InternetMessageFormatHeaderParser.cs
@@ -0,0 +1,320 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+using System.Net.Http.Headers;
+using System.Text;
+
+namespace System.Net.Http.Formatting.Parsers
+{
+ /// <summary>
+ /// Buffer-oriented RFC 5322 style Internet Message Format parser which can be used to pass header
+ /// fields used in HTTP and MIME message entities.
+ /// </summary>
+ internal class InternetMessageFormatHeaderParser
+ {
+ internal const int MinHeaderSize = 2;
+
+ private int _totalBytesConsumed;
+ private int _maxHeaderSize;
+
+ private HeaderFieldState _headerState;
+ private HttpHeaders _headers;
+ private CurrentHeaderFieldStore _currentHeader;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="InternetMessageFormatHeaderParser"/> class.
+ /// </summary>
+ /// <param name="headers">Concrete <see cref="HttpHeaders"/> instance where header fields are added as they are parsed.</param>
+ /// <param name="maxHeaderSize">Maximum length of complete header containing all the individual header fields.</param>
+ public InternetMessageFormatHeaderParser(HttpHeaders headers, int maxHeaderSize)
+ {
+ // The minimum length which would be an empty header terminated by CRLF
+ if (maxHeaderSize < InternetMessageFormatHeaderParser.MinHeaderSize)
+ {
+ throw new ArgumentException(RS.Format(Properties.Resources.MinParameterSize, InternetMessageFormatHeaderParser.MinHeaderSize), "maxHeaderSize");
+ }
+
+ if (headers == null)
+ {
+ throw new ArgumentNullException("headers");
+ }
+
+ _headers = headers;
+ _maxHeaderSize = maxHeaderSize;
+ _currentHeader = new CurrentHeaderFieldStore();
+ }
+
+ private enum HeaderFieldState
+ {
+ Name = 0,
+ Value,
+ AfterCarriageReturn,
+ FoldingLine
+ }
+
+ /// <summary>
+ /// Parse a buffer of RFC 5322 style header fields and add them to the <see cref="HttpHeaders"/> collection.
+ /// Bytes are parsed in a consuming manner from the beginning of the buffer meaning that the same bytes can not be
+ /// present in the buffer.
+ /// </summary>
+ /// <param name="buffer">Request buffer from where request is read</param>
+ /// <param name="bytesReady">Size of request buffer</param>
+ /// <param name="bytesConsumed">Offset into request buffer</param>
+ /// <returns>State of the parser. Call this method with new data until it reaches a final state.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is translated to parse state.")]
+ public ParserState ParseBuffer(
+ byte[] buffer,
+ int bytesReady,
+ ref int bytesConsumed)
+ {
+ if (buffer == null)
+ {
+ throw new ArgumentNullException("buffer");
+ }
+
+ ParserState parseStatus = ParserState.NeedMoreData;
+
+ if (bytesConsumed >= bytesReady)
+ {
+ // We already can tell we need more data
+ return parseStatus;
+ }
+
+ try
+ {
+ parseStatus = InternetMessageFormatHeaderParser.ParseHeaderFields(
+ buffer,
+ bytesReady,
+ ref bytesConsumed,
+ ref _headerState,
+ _maxHeaderSize,
+ ref _totalBytesConsumed,
+ _currentHeader,
+ _headers);
+ }
+ catch (Exception)
+ {
+ parseStatus = ParserState.Invalid;
+ }
+
+ return parseStatus;
+ }
+
+ [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "This is a parser which cannot be split up for performance reasons.")]
+ private static ParserState ParseHeaderFields(
+ byte[] buffer,
+ int bytesReady,
+ ref int bytesConsumed,
+ ref HeaderFieldState requestHeaderState,
+ int maximumHeaderLength,
+ ref int totalBytesConsumed,
+ CurrentHeaderFieldStore currentField,
+ HttpHeaders headers)
+ {
+ Contract.Assert((bytesReady - bytesConsumed) >= 0, "ParseHeaderFields()|(inputBufferLength - bytesParsed) < 0");
+ Contract.Assert(maximumHeaderLength <= 0 || totalBytesConsumed <= maximumHeaderLength, "ParseHeaderFields()|Headers already read exceeds limit.");
+
+ // Remember where we started.
+ int initialBytesParsed = bytesConsumed;
+ int segmentStart;
+
+ // Set up parsing status with what will happen if we exceed the buffer.
+ ParserState parseStatus = ParserState.DataTooBig;
+ int effectiveMax = maximumHeaderLength <= 0 ? Int32.MaxValue : maximumHeaderLength - totalBytesConsumed + initialBytesParsed;
+ if (bytesReady < effectiveMax)
+ {
+ parseStatus = ParserState.NeedMoreData;
+ effectiveMax = bytesReady;
+ }
+
+ Contract.Assert(bytesConsumed < effectiveMax, "We have already consumed more than the max header length.");
+
+ switch (requestHeaderState)
+ {
+ case HeaderFieldState.Name:
+ segmentStart = bytesConsumed;
+ while (buffer[bytesConsumed] != ':')
+ {
+ if (buffer[bytesConsumed] == '\r')
+ {
+ if (!currentField.IsEmpty())
+ {
+ parseStatus = ParserState.Invalid;
+ goto quit;
+ }
+ else
+ {
+ // Move past the '\r'
+ requestHeaderState = HeaderFieldState.AfterCarriageReturn;
+ if (++bytesConsumed == effectiveMax)
+ {
+ goto quit;
+ }
+
+ goto case HeaderFieldState.AfterCarriageReturn;
+ }
+ }
+
+ if (++bytesConsumed == effectiveMax)
+ {
+ string headerFieldName = Encoding.UTF8.GetString(buffer, segmentStart, bytesConsumed - segmentStart);
+ currentField.Name.Append(headerFieldName);
+ goto quit;
+ }
+ }
+
+ if (bytesConsumed > segmentStart)
+ {
+ string headerFieldName = Encoding.UTF8.GetString(buffer, segmentStart, bytesConsumed - segmentStart);
+ currentField.Name.Append(headerFieldName);
+ }
+
+ // Move past the ':'
+ requestHeaderState = HeaderFieldState.Value;
+ if (++bytesConsumed == effectiveMax)
+ {
+ goto quit;
+ }
+
+ goto case HeaderFieldState.Value;
+
+ case HeaderFieldState.Value:
+ segmentStart = bytesConsumed;
+ while (buffer[bytesConsumed] != '\r')
+ {
+ if (++bytesConsumed == effectiveMax)
+ {
+ string headerFieldValue = Encoding.UTF8.GetString(buffer, segmentStart, bytesConsumed - segmentStart);
+ currentField.Value.Append(headerFieldValue);
+ goto quit;
+ }
+ }
+
+ if (bytesConsumed > segmentStart)
+ {
+ string headerFieldValue = Encoding.UTF8.GetString(buffer, segmentStart, bytesConsumed - segmentStart);
+ currentField.Value.Append(headerFieldValue);
+ }
+
+ // Move past the CR
+ requestHeaderState = HeaderFieldState.AfterCarriageReturn;
+ if (++bytesConsumed == effectiveMax)
+ {
+ goto quit;
+ }
+
+ goto case HeaderFieldState.AfterCarriageReturn;
+
+ case HeaderFieldState.AfterCarriageReturn:
+ if (buffer[bytesConsumed] != '\n')
+ {
+ parseStatus = ParserState.Invalid;
+ goto quit;
+ }
+
+ if (currentField.IsEmpty())
+ {
+ parseStatus = ParserState.Done;
+ bytesConsumed++;
+ goto quit;
+ }
+
+ requestHeaderState = HeaderFieldState.FoldingLine;
+ if (++bytesConsumed == effectiveMax)
+ {
+ goto quit;
+ }
+
+ goto case HeaderFieldState.FoldingLine;
+
+ case HeaderFieldState.FoldingLine:
+ if (buffer[bytesConsumed] != ' ' && buffer[bytesConsumed] != '\t')
+ {
+ currentField.CopyTo(headers);
+ requestHeaderState = HeaderFieldState.Name;
+ if (bytesConsumed == effectiveMax)
+ {
+ goto quit;
+ }
+
+ goto case HeaderFieldState.Name;
+ }
+
+ // Unfold line by inserting SP instead
+ currentField.Value.Append(' ');
+
+ // Continue parsing header field value
+ requestHeaderState = HeaderFieldState.Value;
+ if (++bytesConsumed == effectiveMax)
+ {
+ goto quit;
+ }
+
+ goto case HeaderFieldState.Value;
+ }
+
+ quit:
+ totalBytesConsumed += bytesConsumed - initialBytesParsed;
+ return parseStatus;
+ }
+
+ /// <summary>
+ /// Maintains information about the current header field being parsed.
+ /// </summary>
+ private class CurrentHeaderFieldStore
+ {
+ private const int DefaultFieldNameAllocation = 128;
+ private const int DefaultFieldValueAllocation = 2 * 1024;
+
+ private static readonly char[] _linearWhiteSpace = new char[] { ' ', '\t' };
+
+ private readonly StringBuilder _name = new StringBuilder(CurrentHeaderFieldStore.DefaultFieldNameAllocation);
+ private readonly StringBuilder _value = new StringBuilder(CurrentHeaderFieldStore.DefaultFieldValueAllocation);
+
+ /// <summary>
+ /// Gets the header field name.
+ /// </summary>
+ public StringBuilder Name
+ {
+ get { return _name; }
+ }
+
+ /// <summary>
+ /// Gets the header field value.
+ /// </summary>
+ public StringBuilder Value
+ {
+ get { return _value; }
+ }
+
+ /// <summary>
+ /// Copies current header field to the provided <see cref="HttpHeaders"/> instance.
+ /// </summary>
+ /// <param name="headers">The headers.</param>
+ public void CopyTo(HttpHeaders headers)
+ {
+ headers.Add(_name.ToString(), _value.ToString().Trim(CurrentHeaderFieldStore._linearWhiteSpace));
+ Clear();
+ }
+
+ /// <summary>
+ /// Determines whether this instance is empty.
+ /// </summary>
+ /// <returns>
+ /// <c>true</c> if this instance is empty; otherwise, <c>false</c>.
+ /// </returns>
+ public bool IsEmpty()
+ {
+ return _name.Length == 0 && _value.Length == 0;
+ }
+
+ /// <summary>
+ /// Clears this instance.
+ /// </summary>
+ private void Clear()
+ {
+ _name.Clear();
+ _value.Clear();
+ }
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Formatting/Parsers/MimeMultipartBodyPartParser.cs b/src/System.Net.Http.Formatting/Formatting/Parsers/MimeMultipartBodyPartParser.cs
new file mode 100644
index 00000000..a99c9be4
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Formatting/Parsers/MimeMultipartBodyPartParser.cs
@@ -0,0 +1,278 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+using System.IO;
+using System.Net.Http.Headers;
+
+namespace System.Net.Http.Formatting.Parsers
+{
+ /// <summary>
+ /// Complete MIME multipart parser that combines <see cref="MimeMultipartParser"/> for parsing the MIME message into individual body parts
+ /// and <see cref="InternetMessageFormatHeaderParser"/> for parsing each body part into a MIME header and a MIME body. The caller of the parser is returned
+ /// the resulting MIME bodies which can then be written to some output.
+ /// </summary>
+ internal class MimeMultipartBodyPartParser : IDisposable
+ {
+ private const long DefaultMaxMessageSize = Int64.MaxValue;
+ private const int DefaultMaxBodyPartHeaderSize = 4 * 1024;
+
+ // MIME parser
+ private MimeMultipartParser _mimeParser;
+ private MimeMultipartParser.State _mimeStatus = MimeMultipartParser.State.NeedMoreData;
+ private ArraySegment<byte>[] _parsedBodyPart = new ArraySegment<byte>[2];
+ private MimeBodyPart _currentBodyPart;
+ private bool _isFirst = true;
+
+ // Header field parser
+ private ParserState _bodyPartHeaderStatus = ParserState.NeedMoreData;
+ private int _maxBodyPartHeaderSize;
+
+ // Stream provider
+ private IMultipartStreamProvider _streamProvider;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MimeMultipartBodyPartParser"/> class.
+ /// </summary>
+ /// <param name="content">An existing <see cref="HttpContent"/> instance to use for the object's content.</param>
+ /// <param name="streamProvider">A stream provider providing output streams for where to write body parts as they are parsed.</param>
+ public MimeMultipartBodyPartParser(HttpContent content, IMultipartStreamProvider streamProvider)
+ : this(content, streamProvider, DefaultMaxMessageSize, DefaultMaxBodyPartHeaderSize)
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MimeMultipartBodyPartParser"/> class.
+ /// </summary>
+ /// <param name="content">An existing <see cref="HttpContent"/> instance to use for the object's content.</param>
+ /// <param name="streamProvider">A stream provider providing output streams for where to write body parts as they are parsed.</param>
+ /// <param name="maxMessageSize">The max length of the entire MIME multipart message.</param>
+ /// <param name="maxBodyPartHeaderSize">The max length of the MIME header within each MIME body part.</param>
+ public MimeMultipartBodyPartParser(
+ HttpContent content,
+ IMultipartStreamProvider streamProvider,
+ long maxMessageSize,
+ int maxBodyPartHeaderSize)
+ {
+ Contract.Assert(content != null, "content cannot be null.");
+ Contract.Assert(streamProvider != null, "streamProvider cannot be null.");
+
+ string boundary = ValidateArguments(content, maxMessageSize, true);
+
+ _mimeParser = new MimeMultipartParser(boundary, maxMessageSize);
+ _currentBodyPart = new MimeBodyPart(streamProvider, maxBodyPartHeaderSize);
+
+ _maxBodyPartHeaderSize = maxBodyPartHeaderSize;
+
+ _streamProvider = streamProvider;
+ }
+
+ /// <summary>
+ /// Determines whether the specified content is MIME multipart content.
+ /// </summary>
+ /// <param name="content">The content.</param>
+ /// <returns>
+ /// <c>true</c> if the specified content is MIME multipart content; otherwise, <c>false</c>.
+ /// </returns>
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is translated to false return.")]
+ public static bool IsMimeMultipartContent(HttpContent content)
+ {
+ Contract.Assert(content != null, "content cannot be null.");
+ try
+ {
+ string boundary = ValidateArguments(content, DefaultMaxMessageSize, false);
+ return boundary != null ? true : false;
+ }
+ catch (Exception)
+ {
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Parses the data provided and generates parsed MIME body part bodies in the form of <see cref="ArraySegment{T}"/> which are ready to
+ /// write to the output stream.
+ /// </summary>
+ /// <param name="data">The data to parse</param>
+ /// <param name="bytesRead">The number of bytes available in the input data</param>
+ /// <returns>Parsed <see cref="MimeBodyPart"/> instances.</returns>
+ public IEnumerable<MimeBodyPart> ParseBuffer(byte[] data, int bytesRead)
+ {
+ int bytesConsumed = 0;
+ bool isFinal = false;
+
+ if (bytesRead == 0)
+ {
+ CleanupCurrentBodyPart();
+ throw new IOException(Properties.Resources.ReadAsMimeMultipartUnexpectedTermination);
+ }
+
+ // Make sure we remove an old array segments.
+ _currentBodyPart.Segments.Clear();
+
+ while (bytesConsumed < bytesRead)
+ {
+ _mimeStatus = _mimeParser.ParseBuffer(data, bytesRead, ref bytesConsumed, out _parsedBodyPart[0], out _parsedBodyPart[1], out isFinal);
+ if (_mimeStatus != MimeMultipartParser.State.BodyPartCompleted && _mimeStatus != MimeMultipartParser.State.NeedMoreData)
+ {
+ CleanupCurrentBodyPart();
+ throw new IOException(RS.Format(Properties.Resources.ReadAsMimeMultipartParseError, bytesConsumed, data));
+ }
+
+ // First body is empty preamble which we just ignore
+ if (_isFirst)
+ {
+ if (_mimeStatus == MimeMultipartParser.State.BodyPartCompleted)
+ {
+ _isFirst = false;
+ }
+
+ continue;
+ }
+
+ // Parse the two array segments containing parsed body parts that the MIME parser gave us
+ foreach (ArraySegment<byte> part in _parsedBodyPart)
+ {
+ if (part.Count == 0)
+ {
+ continue;
+ }
+
+ if (_bodyPartHeaderStatus != ParserState.Done)
+ {
+ int headerConsumed = part.Offset;
+ _bodyPartHeaderStatus = _currentBodyPart.HeaderParser.ParseBuffer(part.Array, part.Count + part.Offset, ref headerConsumed);
+ if (_bodyPartHeaderStatus == ParserState.Done)
+ {
+ // Add the remainder as body part content
+ _currentBodyPart.Segments.Add(new ArraySegment<byte>(part.Array, headerConsumed, part.Count + part.Offset - headerConsumed));
+ }
+ else if (_bodyPartHeaderStatus != ParserState.NeedMoreData)
+ {
+ CleanupCurrentBodyPart();
+ throw new IOException(RS.Format(Properties.Resources.ReadAsMimeMultipartHeaderParseError, headerConsumed, part.Array));
+ }
+ }
+ else
+ {
+ // Add the data as body part content
+ _currentBodyPart.Segments.Add(part);
+ }
+ }
+
+ if (_mimeStatus == MimeMultipartParser.State.BodyPartCompleted)
+ {
+ // If body is completed then swap current body part
+ MimeBodyPart completed = _currentBodyPart;
+ completed.IsComplete = true;
+ completed.IsFinal = isFinal;
+
+ _currentBodyPart = new MimeBodyPart(_streamProvider, _maxBodyPartHeaderSize);
+ _mimeStatus = MimeMultipartParser.State.NeedMoreData;
+ _bodyPartHeaderStatus = ParserState.NeedMoreData;
+ yield return completed;
+ }
+ else
+ {
+ // Otherwise return what we have
+ yield return _currentBodyPart;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources
+ /// </summary>
+ /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ protected void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _mimeParser = null;
+ CleanupCurrentBodyPart();
+ }
+ }
+
+ private static string ValidateArguments(HttpContent content, long maxMessageSize, bool throwOnError)
+ {
+ Contract.Assert(content != null, "content cannot be null.");
+ if (maxMessageSize < MimeMultipartParser.MinMessageSize)
+ {
+ if (throwOnError)
+ {
+ throw new ArgumentException(RS.Format(Properties.Resources.MinParameterSize, MimeMultipartParser.MinMessageSize), "maxMessageSize");
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ MediaTypeHeaderValue contentType = content.Headers.ContentType;
+ if (contentType == null)
+ {
+ if (throwOnError)
+ {
+ throw new ArgumentException(RS.Format(Properties.Resources.ReadAsMimeMultipartArgumentNoContentType, typeof(HttpContent).Name, "multipart/"), "content");
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ if (!contentType.MediaType.StartsWith("multipart", StringComparison.OrdinalIgnoreCase))
+ {
+ if (throwOnError)
+ {
+ throw new ArgumentException(RS.Format(Properties.Resources.ReadAsMimeMultipartArgumentNoMultipart, typeof(HttpContent).Name, "multipart/"), "content");
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ string boundary = null;
+ foreach (NameValueHeaderValue p in contentType.Parameters)
+ {
+ if (p.Name.Equals("boundary", StringComparison.OrdinalIgnoreCase))
+ {
+ boundary = FormattingUtilities.UnquoteToken(p.Value);
+ break;
+ }
+ }
+
+ if (boundary == null)
+ {
+ if (throwOnError)
+ {
+ throw new ArgumentException(RS.Format(Properties.Resources.ReadAsMimeMultipartArgumentNoBoundary, typeof(HttpContent).Name, "multipart", "boundary"), "content");
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ return boundary;
+ }
+
+ private void CleanupCurrentBodyPart()
+ {
+ if (_currentBodyPart != null)
+ {
+ _currentBodyPart.Dispose();
+ _currentBodyPart = null;
+ }
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Formatting/Parsers/MimeMultipartParser.cs b/src/System.Net.Http.Formatting/Formatting/Parsers/MimeMultipartParser.cs
new file mode 100644
index 00000000..e5ada79c
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Formatting/Parsers/MimeMultipartParser.cs
@@ -0,0 +1,612 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+using System.Text;
+
+namespace System.Net.Http.Formatting.Parsers
+{
+ /// <summary>
+ /// Buffer-oriented MIME multipart parser.
+ /// </summary>
+ internal class MimeMultipartParser
+ {
+ internal const int MinMessageSize = 10;
+
+ private const byte HTAB = 0x09;
+ private const byte SP = 0x20;
+ private const byte CR = 0x0D;
+ private const byte LF = 0x0A;
+ private const byte Dash = 0x2D;
+ private static readonly ArraySegment<byte> _emptyBodyPart = new ArraySegment<byte>(new byte[0]);
+
+ private long _totalBytesConsumed;
+ private long _maxMessageSize;
+
+ private BodyPartState _bodyPartState;
+ private string _boundary;
+ private CurrentBodyPartStore _currentBoundary;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MimeMultipartParser"/> class.
+ /// </summary>
+ /// <param name="boundary">Message boundary</param>
+ /// <param name="maxMessageSize">Maximum length of entire MIME multipart message.</param>
+ public MimeMultipartParser(string boundary, long maxMessageSize)
+ {
+ // The minimum length which would be an empty message terminated by CRLF
+ if (maxMessageSize < MimeMultipartParser.MinMessageSize)
+ {
+ throw new ArgumentException(RS.Format(Properties.Resources.MinParameterSize, MimeMultipartParser.MinMessageSize), "maxMessageSize");
+ }
+
+ if (String.IsNullOrWhiteSpace(boundary))
+ {
+ throw new ArgumentNullException("boundary");
+ }
+
+ if (boundary.EndsWith(" ", StringComparison.Ordinal))
+ {
+ throw new ArgumentException(Properties.Resources.MimeMultipartParserBadBoundary, "boundary");
+ }
+
+ _maxMessageSize = maxMessageSize;
+ _boundary = boundary;
+ _currentBoundary = new CurrentBodyPartStore(_boundary);
+ _bodyPartState = BodyPartState.AfterFirstLineFeed;
+ }
+
+ private enum BodyPartState
+ {
+ BodyPart = 0,
+ AfterFirstCarriageReturn,
+ AfterFirstLineFeed,
+ AfterFirstDash,
+ Boundary,
+ AfterSecondCarriageReturn
+ }
+
+ private enum MessageState
+ {
+ Boundary = 0, // about to parse boundary
+ BodyPart, // about to parse body-part
+ CloseDelimiter // about to read close-delimiter
+ }
+
+ /// <summary>
+ /// Represents the overall state of the <see cref="MimeMultipartParser"/>.
+ /// </summary>
+ public enum State
+ {
+ /// <summary>
+ /// Need more data
+ /// </summary>
+ NeedMoreData = 0,
+
+ /// <summary>
+ /// Parsing of a complete body part succeeded.
+ /// </summary>
+ BodyPartCompleted,
+
+ /// <summary>
+ /// Bad data format
+ /// </summary>
+ Invalid,
+
+ /// <summary>
+ /// Data exceeds the allowed size
+ /// </summary>
+ DataTooBig,
+ }
+
+ /// <summary>
+ /// Parse a MIME multipart message. Bytes are parsed in a consuming
+ /// manner from the beginning of the request buffer meaning that the same bytes can not be
+ /// present in the request buffer.
+ /// </summary>
+ /// <param name="buffer">Request buffer from where request is read</param>
+ /// <param name="bytesReady">Size of request buffer</param>
+ /// <param name="bytesConsumed">Offset into request buffer</param>
+ /// <param name="remainingBodyPart">Any body part that was considered as a potential MIME multipart boundary but which was in fact part of the body.</param>
+ /// <param name="bodyPart">The bulk of the body part.</param>
+ /// <param name="isFinalBodyPart">Indicates whether the final body part has been found.</param>
+ /// <remarks>In order to get the complete body part, the caller is responsible for concatenating the contents of the
+ /// <paramref name="remainingBodyPart"/> and <paramref name="bodyPart"/> out parameters.</remarks>
+ /// <returns>State of the parser.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is translated to parse state.")]
+ public State ParseBuffer(
+ byte[] buffer,
+ int bytesReady,
+ ref int bytesConsumed,
+ out ArraySegment<byte> remainingBodyPart,
+ out ArraySegment<byte> bodyPart,
+ out bool isFinalBodyPart)
+ {
+ if (buffer == null)
+ {
+ throw new ArgumentNullException("buffer");
+ }
+
+ State parseStatus = State.NeedMoreData;
+ remainingBodyPart = MimeMultipartParser._emptyBodyPart;
+ bodyPart = MimeMultipartParser._emptyBodyPart;
+ isFinalBodyPart = false;
+
+ if (bytesConsumed >= bytesReady)
+ {
+ // we already can tell we need more data
+ return parseStatus;
+ }
+
+ try
+ {
+ parseStatus = MimeMultipartParser.ParseBodyPart(
+ buffer,
+ bytesReady,
+ ref bytesConsumed,
+ ref _bodyPartState,
+ _maxMessageSize,
+ ref _totalBytesConsumed,
+ _currentBoundary);
+ }
+ catch (Exception)
+ {
+ parseStatus = State.Invalid;
+ }
+
+ remainingBodyPart = _currentBoundary.GetDiscardedBoundary();
+ bodyPart = _currentBoundary.BodyPart;
+ if (parseStatus == State.BodyPartCompleted)
+ {
+ isFinalBodyPart = _currentBoundary.IsFinal;
+ _currentBoundary.ClearAll();
+ }
+ else
+ {
+ _currentBoundary.ClearBodyPart();
+ }
+
+ return parseStatus;
+ }
+
+ [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "This is a parser which cannot be split up for performance reasons.")]
+ private static State ParseBodyPart(
+ byte[] buffer,
+ int bytesReady,
+ ref int bytesConsumed,
+ ref BodyPartState bodyPartState,
+ long maximumMessageLength,
+ ref long totalBytesConsumed,
+ CurrentBodyPartStore currentBodyPart)
+ {
+ Contract.Assert((bytesReady - bytesConsumed) >= 0, "ParseBodyPart()|(bytesReady - bytesConsumed) < 0");
+ Contract.Assert(maximumMessageLength <= 0 || totalBytesConsumed <= maximumMessageLength, "ParseBodyPart()|Message already read exceeds limit.");
+
+ // Remember where we started.
+ int segmentStart;
+ int initialBytesParsed = bytesConsumed;
+
+ // Set up parsing status with what will happen if we exceed the buffer.
+ State parseStatus = State.DataTooBig;
+ long effectiveMax = maximumMessageLength <= 0 ? Int64.MaxValue : (maximumMessageLength - totalBytesConsumed + bytesConsumed);
+ if (bytesReady < effectiveMax)
+ {
+ parseStatus = State.NeedMoreData;
+ effectiveMax = bytesReady;
+ }
+
+ currentBodyPart.ResetBoundaryOffset();
+
+ Contract.Assert(bytesConsumed < effectiveMax, "We have already consumed more than the max header length.");
+
+ switch (bodyPartState)
+ {
+ case BodyPartState.BodyPart:
+ while (buffer[bytesConsumed] != MimeMultipartParser.CR)
+ {
+ if (++bytesConsumed == effectiveMax)
+ {
+ goto quit;
+ }
+ }
+
+ // Remember potential boundary
+ currentBodyPart.AppendBoundary(MimeMultipartParser.CR);
+
+ // Move past the CR
+ bodyPartState = BodyPartState.AfterFirstCarriageReturn;
+ if (++bytesConsumed == effectiveMax)
+ {
+ goto quit;
+ }
+
+ goto case BodyPartState.AfterFirstCarriageReturn;
+
+ case BodyPartState.AfterFirstCarriageReturn:
+ if (buffer[bytesConsumed] != MimeMultipartParser.LF)
+ {
+ currentBodyPart.ResetBoundary();
+ bodyPartState = BodyPartState.BodyPart;
+ if (++bytesConsumed == effectiveMax)
+ {
+ goto quit;
+ }
+
+ goto case BodyPartState.BodyPart;
+ }
+
+ // Remember potential boundary
+ currentBodyPart.AppendBoundary(MimeMultipartParser.LF);
+
+ // Move past the CR
+ bodyPartState = BodyPartState.AfterFirstLineFeed;
+ if (++bytesConsumed == effectiveMax)
+ {
+ goto quit;
+ }
+
+ goto case BodyPartState.AfterFirstLineFeed;
+
+ case BodyPartState.AfterFirstLineFeed:
+ if (buffer[bytesConsumed] == MimeMultipartParser.CR)
+ {
+ // Remember potential boundary
+ currentBodyPart.ResetBoundary();
+ currentBodyPart.AppendBoundary(MimeMultipartParser.CR);
+
+ // Move past the CR
+ bodyPartState = BodyPartState.AfterFirstCarriageReturn;
+ if (++bytesConsumed == effectiveMax)
+ {
+ goto quit;
+ }
+
+ goto case BodyPartState.AfterFirstCarriageReturn;
+ }
+
+ if (buffer[bytesConsumed] != MimeMultipartParser.Dash)
+ {
+ currentBodyPart.ResetBoundary();
+ bodyPartState = BodyPartState.BodyPart;
+ if (++bytesConsumed == effectiveMax)
+ {
+ goto quit;
+ }
+
+ goto case BodyPartState.BodyPart;
+ }
+
+ // Remember potential boundary
+ currentBodyPart.AppendBoundary(MimeMultipartParser.Dash);
+
+ // Move past the Dash
+ bodyPartState = BodyPartState.AfterFirstDash;
+ if (++bytesConsumed == effectiveMax)
+ {
+ goto quit;
+ }
+
+ goto case BodyPartState.AfterFirstDash;
+
+ case BodyPartState.AfterFirstDash:
+ if (buffer[bytesConsumed] != MimeMultipartParser.Dash)
+ {
+ currentBodyPart.ResetBoundary();
+ bodyPartState = BodyPartState.BodyPart;
+ if (++bytesConsumed == effectiveMax)
+ {
+ goto quit;
+ }
+
+ goto case BodyPartState.BodyPart;
+ }
+
+ // Remember potential boundary
+ currentBodyPart.AppendBoundary(MimeMultipartParser.Dash);
+
+ // Move past the Dash
+ bodyPartState = BodyPartState.Boundary;
+ if (++bytesConsumed == effectiveMax)
+ {
+ goto quit;
+ }
+
+ goto case BodyPartState.Boundary;
+
+ case BodyPartState.Boundary:
+ segmentStart = bytesConsumed;
+ while (buffer[bytesConsumed] != MimeMultipartParser.CR)
+ {
+ if (++bytesConsumed == effectiveMax)
+ {
+ currentBodyPart.AppendBoundary(buffer, segmentStart, bytesConsumed - segmentStart);
+ goto quit;
+ }
+ }
+
+ if (bytesConsumed > segmentStart)
+ {
+ currentBodyPart.AppendBoundary(buffer, segmentStart, bytesConsumed - segmentStart);
+ }
+
+ // Remember potential boundary
+ currentBodyPart.AppendBoundary(MimeMultipartParser.CR);
+
+ // Move past the CR
+ bodyPartState = BodyPartState.AfterSecondCarriageReturn;
+ if (++bytesConsumed == effectiveMax)
+ {
+ goto quit;
+ }
+
+ goto case BodyPartState.AfterSecondCarriageReturn;
+
+ case BodyPartState.AfterSecondCarriageReturn:
+ if (buffer[bytesConsumed] != MimeMultipartParser.LF)
+ {
+ currentBodyPart.ResetBoundary();
+ bodyPartState = BodyPartState.BodyPart;
+ if (++bytesConsumed == effectiveMax)
+ {
+ goto quit;
+ }
+
+ goto case BodyPartState.BodyPart;
+ }
+
+ // Remember potential boundary
+ currentBodyPart.AppendBoundary(MimeMultipartParser.LF);
+
+ // Move past the LF
+ bytesConsumed++;
+
+ bodyPartState = BodyPartState.BodyPart;
+ if (currentBodyPart.IsBoundaryValid())
+ {
+ parseStatus = State.BodyPartCompleted;
+ }
+ else
+ {
+ currentBodyPart.ResetBoundary();
+ if (bytesConsumed == effectiveMax)
+ {
+ goto quit;
+ }
+
+ goto case BodyPartState.BodyPart;
+ }
+
+ goto quit;
+ }
+
+ quit:
+ if (initialBytesParsed < bytesConsumed)
+ {
+ int boundaryLength = currentBodyPart.BoundaryDelta;
+ if (boundaryLength > 0 && parseStatus != State.BodyPartCompleted)
+ {
+ currentBodyPart.HasPotentialBoundaryLeftOver = true;
+ }
+
+ int bodyPartEnd = bytesConsumed - initialBytesParsed - boundaryLength;
+
+ currentBodyPart.BodyPart = new ArraySegment<byte>(buffer, initialBytesParsed, bodyPartEnd);
+ }
+
+ totalBytesConsumed += bytesConsumed - initialBytesParsed;
+ return parseStatus;
+ }
+
+ /// <summary>
+ /// Maintains information about the current body part being parsed.
+ /// </summary>
+ private class CurrentBodyPartStore
+ {
+ private const int MaxBoundarySize = 256;
+ private const int InitialOffset = 2;
+
+ private byte[] _boundaryStore = new byte[CurrentBodyPartStore.MaxBoundarySize];
+ private int _boundaryStoreLength;
+
+ private byte[] _referenceBoundary = new byte[CurrentBodyPartStore.MaxBoundarySize];
+ private int _referenceBoundaryLength;
+
+ private byte[] _boundary = new byte[CurrentBodyPartStore.MaxBoundarySize];
+ private int _boundaryLength = 0;
+
+ private ArraySegment<byte> _bodyPart = MimeMultipartParser._emptyBodyPart;
+ private bool _isFinal;
+ private bool _isFirst = true;
+ private bool _releaseDiscardedBoundary;
+ private int _boundaryOffset;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CurrentBodyPartStore"/> class.
+ /// </summary>
+ /// <param name="referenceBoundary">The reference boundary.</param>
+ public CurrentBodyPartStore(string referenceBoundary)
+ {
+ _referenceBoundary[0] = MimeMultipartParser.CR;
+ _referenceBoundary[1] = MimeMultipartParser.LF;
+ _referenceBoundary[2] = MimeMultipartParser.Dash;
+ _referenceBoundary[3] = MimeMultipartParser.Dash;
+ _referenceBoundaryLength = 4 + Encoding.UTF8.GetBytes(referenceBoundary, 0, referenceBoundary.Length, _referenceBoundary, 4);
+
+ _boundary[0] = MimeMultipartParser.CR;
+ _boundary[1] = MimeMultipartParser.LF;
+ _boundaryLength = CurrentBodyPartStore.InitialOffset;
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance has potential boundary left over.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if this instance has potential boundary left over; otherwise, <c>false</c>.
+ /// </value>
+ public bool HasPotentialBoundaryLeftOver { get; set; }
+
+ /// <summary>
+ /// Gets the boundary delta.
+ /// </summary>
+ public int BoundaryDelta
+ {
+ get { return (_boundaryLength - _boundaryOffset > 0) ? _boundaryLength - _boundaryOffset : _boundaryLength; }
+ }
+
+ /// <summary>
+ /// Gets or sets the body part.
+ /// </summary>
+ /// <value>
+ /// The body part.
+ /// </value>
+ public ArraySegment<byte> BodyPart
+ {
+ get { return _bodyPart; }
+
+ set { _bodyPart = value; }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether this body part instance is final.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if this body part instance is final; otherwise, <c>false</c>.
+ /// </value>
+ public bool IsFinal
+ {
+ get { return _isFinal; }
+ }
+
+ /// <summary>
+ /// Resets the boundary offset.
+ /// </summary>
+ public void ResetBoundaryOffset()
+ {
+ _boundaryOffset = _boundaryLength;
+ }
+
+ /// <summary>
+ /// Resets the boundary.
+ /// </summary>
+ public void ResetBoundary()
+ {
+ // If we had a potential boundary left over then store it so that we don't loose it
+ if (HasPotentialBoundaryLeftOver)
+ {
+ Buffer.BlockCopy(_boundary, 0, _boundaryStore, 0, _boundaryOffset);
+ _boundaryStoreLength = _boundaryOffset;
+ HasPotentialBoundaryLeftOver = false;
+ _releaseDiscardedBoundary = true;
+ }
+
+ _boundaryLength = 0;
+ _boundaryOffset = 0;
+ }
+
+ /// <summary>
+ /// Appends byte to the current boundary.
+ /// </summary>
+ /// <param name="data">The data to append to the boundary.</param>
+ public void AppendBoundary(byte data)
+ {
+ _boundary[_boundaryLength++] = data;
+ }
+
+ /// <summary>
+ /// Appends array of bytes to the current boundary.
+ /// </summary>
+ /// <param name="data">The data to append to the boundary.</param>
+ /// <param name="offset">The offset into the data.</param>
+ /// <param name="count">The number of bytes to append.</param>
+ public void AppendBoundary(byte[] data, int offset, int count)
+ {
+ Buffer.BlockCopy(data, offset, _boundary, _boundaryLength, count);
+ _boundaryLength += count;
+ }
+
+ /// <summary>
+ /// Gets the discarded boundary.
+ /// </summary>
+ /// <returns>An <see cref="ArraySegment{T}"/> containing the discarded boundary.</returns>
+ public ArraySegment<byte> GetDiscardedBoundary()
+ {
+ if (_boundaryStoreLength > 0 && _releaseDiscardedBoundary)
+ {
+ ArraySegment<byte> discarded = new ArraySegment<byte>(_boundaryStore, 0, _boundaryStoreLength);
+ _boundaryStoreLength = 0;
+ return discarded;
+ }
+
+ return MimeMultipartParser._emptyBodyPart;
+ }
+
+ /// <summary>
+ /// Determines whether current boundary is valid.
+ /// </summary>
+ /// <returns>
+ /// <c>true</c> if curent boundary is valid; otherwise, <c>false</c>.
+ /// </returns>
+ public bool IsBoundaryValid()
+ {
+ int offset = 0;
+ if (_isFirst)
+ {
+ offset = CurrentBodyPartStore.InitialOffset;
+ }
+
+ int cnt = offset;
+ for (; cnt < _referenceBoundaryLength; cnt++)
+ {
+ if (_boundary[cnt] != _referenceBoundary[cnt])
+ {
+ return false;
+ }
+ }
+
+ // Check for final
+ bool boundaryIsFinal = false;
+ if (_boundary[cnt] == MimeMultipartParser.Dash &&
+ _boundary[cnt + 1] == MimeMultipartParser.Dash)
+ {
+ boundaryIsFinal = true;
+ cnt += 2;
+ }
+
+ // Rest of boundary must LWS in order for it to match
+ for (; cnt < _boundaryLength - 2; cnt++)
+ {
+ if (_boundary[cnt] != MimeMultipartParser.SP && _boundary[cnt] != MimeMultipartParser.HTAB)
+ {
+ return false;
+ }
+ }
+
+ // We have a valid boundary so whatever we stored in the boundary story is no longer needed
+ _isFinal = boundaryIsFinal;
+ _isFirst = false;
+
+ return true;
+ }
+
+ /// <summary>
+ /// Clears the body part.
+ /// </summary>
+ public void ClearBodyPart()
+ {
+ BodyPart = MimeMultipartParser._emptyBodyPart;
+ }
+
+ /// <summary>
+ /// Clears all.
+ /// </summary>
+ public void ClearAll()
+ {
+ _releaseDiscardedBoundary = false;
+ HasPotentialBoundaryLeftOver = false;
+ _boundaryLength = 0;
+ _boundaryOffset = 0;
+ _boundaryStoreLength = 0;
+ _isFinal = false;
+ ClearBodyPart();
+ }
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Formatting/Parsers/ParserState.cs b/src/System.Net.Http.Formatting/Formatting/Parsers/ParserState.cs
new file mode 100644
index 00000000..255b6d87
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Formatting/Parsers/ParserState.cs
@@ -0,0 +1,28 @@
+namespace System.Net.Http.Formatting.Parsers
+{
+ /// <summary>
+ /// Represents the overall state of various parsers.
+ /// </summary>
+ internal enum ParserState
+ {
+ /// <summary>
+ /// Need more data
+ /// </summary>
+ NeedMoreData = 0,
+
+ /// <summary>
+ /// Parsing completed (final)
+ /// </summary>
+ Done,
+
+ /// <summary>
+ /// Bad data format (final)
+ /// </summary>
+ Invalid,
+
+ /// <summary>
+ /// Data exceeds the allowed size (final)
+ /// </summary>
+ DataTooBig,
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Formatting/QueryStringMapping.cs b/src/System.Net.Http.Formatting/Formatting/QueryStringMapping.cs
new file mode 100644
index 00000000..5868351c
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Formatting/QueryStringMapping.cs
@@ -0,0 +1,114 @@
+using System.Collections.Specialized;
+using System.Net.Http.Headers;
+using System.Net.Http.Internal;
+
+namespace System.Net.Http.Formatting
+{
+ /// <summary>
+ /// Class that provides <see cref="MediaTypeHeaderValue"/>s from query strings.
+ /// </summary>
+ public sealed class QueryStringMapping : MediaTypeMapping
+ {
+ private static readonly Type _queryStringMappingType = typeof(QueryStringMapping);
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="QueryStringMapping"/> class.
+ /// </summary>
+ /// <param name="queryStringParameterName">The name of the query string parameter to match, if present.</param>
+ /// <param name="queryStringParameterValue">The value of the query string parameter specified by <paramref name="queryStringParameterName"/>.</param>
+ /// <param name="mediaType">The media type to use if the query parameter specified by <paramref name="queryStringParameterName"/> is present
+ /// and assigned the value specified by <paramref name="queryStringParameterValue"/>.</param>
+ public QueryStringMapping(string queryStringParameterName, string queryStringParameterValue, string mediaType)
+ : base(mediaType)
+ {
+ Initialize(queryStringParameterName, queryStringParameterValue);
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="QueryStringMapping"/> class.
+ /// </summary>
+ /// <param name="queryStringParameterName">The name of the query string parameter to match, if present.</param>
+ /// <param name="queryStringParameterValue">The value of the query string parameter specified by <paramref name="queryStringParameterName"/>.</param>
+ /// <param name="mediaType">The <see cref="MediaTypeHeaderValue"/> to use if the query parameter specified by <paramref name="queryStringParameterName"/> is present
+ /// and assigned the value specified by <paramref name="queryStringParameterValue"/>.</param>
+ public QueryStringMapping(string queryStringParameterName, string queryStringParameterValue, MediaTypeHeaderValue mediaType)
+ : base(mediaType)
+ {
+ Initialize(queryStringParameterName, queryStringParameterValue);
+ }
+
+ /// <summary>
+ /// Gets the query string parameter name.
+ /// </summary>
+ public string QueryStringParameterName { get; private set; }
+
+ /// <summary>
+ /// Gets the query string parameter value.
+ /// </summary>
+ public string QueryStringParameterValue { get; private set; }
+
+ /// <summary>
+ /// Returns a value indicating whether the current <see cref="QueryStringMapping"/>
+ /// instance can return a <see cref="MediaTypeHeaderValue"/> from <paramref name="request"/>.
+ /// </summary>
+ /// <param name="request">The <see cref="HttpRequestMessage"/> to check.</param>
+ /// <returns>If this instance can produce a <see cref="MediaTypeHeaderValue"/> from <paramref name="request"/>
+ /// it returns <c>1.0</c> otherwise <c>0.0</c>.</returns>
+ public override double TryMatchMediaType(HttpRequestMessage request)
+ {
+ if (request == null)
+ {
+ throw new ArgumentNullException("request");
+ }
+
+ NameValueCollection queryString = GetQueryString(request.RequestUri);
+ return DoesQueryStringMatch(queryString) ? MediaTypeMatch.Match : MediaTypeMatch.NoMatch;
+ }
+
+ private static NameValueCollection GetQueryString(Uri uri)
+ {
+ if (uri == null)
+ {
+ throw new InvalidOperationException(RS.Format(Properties.Resources.NonNullUriRequiredForMediaTypeMapping, _queryStringMappingType.Name));
+ }
+
+ return UriQueryUtility.ParseQueryString(uri.Query);
+ }
+
+ private void Initialize(string queryStringParameterName, string queryStringParameterValue)
+ {
+ if (String.IsNullOrWhiteSpace(queryStringParameterName))
+ {
+ throw new ArgumentNullException("queryStringParameterName");
+ }
+
+ if (String.IsNullOrWhiteSpace(queryStringParameterValue))
+ {
+ throw new ArgumentNullException("queryStringParameterValue");
+ }
+
+ QueryStringParameterName = queryStringParameterName.Trim();
+ QueryStringParameterValue = queryStringParameterValue.Trim();
+ }
+
+ private bool DoesQueryStringMatch(NameValueCollection queryString)
+ {
+ if (queryString != null)
+ {
+ foreach (string queryParameter in queryString.AllKeys)
+ {
+ if (String.Equals(queryParameter, QueryStringParameterName, StringComparison.OrdinalIgnoreCase))
+ {
+ string queryValue = queryString[queryParameter];
+ if (String.Equals(queryValue, QueryStringParameterValue, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Formatting/RequestHeaderMapping.cs b/src/System.Net.Http.Formatting/Formatting/RequestHeaderMapping.cs
new file mode 100644
index 00000000..e939f79b
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Formatting/RequestHeaderMapping.cs
@@ -0,0 +1,144 @@
+using System.Collections.Generic;
+using System.Diagnostics.Contracts;
+using System.Net.Http.Headers;
+
+namespace System.Net.Http.Formatting
+{
+ /// <summary>
+ /// This class provides a mapping from an arbitrary HTTP request header field to a <see cref="MediaTypeHeaderValue"/>
+ /// used to select <see cref="MediaTypeFormatter"/> instances for handling the entity body of an <see cref="HttpRequestMessage"/>
+ /// or <see cref="HttpResponseMessage"/>.
+ /// <remarks>This class only checks header fields associated with <see cref="HttpRequestMessage.Headers"/> for a match. It does
+ /// not check header fields associated with <see cref="HttpResponseMessage.Headers"/> or <see cref="HttpContent.Headers"/> instances.</remarks>
+ /// </summary>
+ public class RequestHeaderMapping : MediaTypeMapping
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="RequestHeaderMapping"/> class.
+ /// </summary>
+ /// <param name="headerName">Name of the header to match.</param>
+ /// <param name="headerValue">The header value to match.</param>
+ /// <param name="valueComparison">The value comparison to use when matching <paramref name="headerValue"/>.</param>
+ /// <param name="isValueSubstring">if set to <c>true</c> then <paramref name="headerValue"/> is
+ /// considered a match if it matches a substring of the actual header value.</param>
+ /// <param name="mediaType">The media type to use if <paramref name="headerName"/> and <paramref name="headerValue"/>
+ /// is considered a match.</param>
+ public RequestHeaderMapping(string headerName, string headerValue, StringComparison valueComparison, bool isValueSubstring, string mediaType)
+ : base(mediaType)
+ {
+ Initialize(headerName, headerValue, valueComparison, isValueSubstring);
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="RequestHeaderMapping"/> class.
+ /// </summary>
+ /// <param name="headerName">Name of the header to match.</param>
+ /// <param name="headerValue">The header value to match.</param>
+ /// <param name="valueComparison">The <see cref="StringComparison"/> to use when matching <paramref name="headerValue"/>.</param>
+ /// <param name="isValueSubstring">if set to <c>true</c> then <paramref name="headerValue"/> is
+ /// considered a match if it matches a substring of the actual header value.</param>
+ /// <param name="mediaType">The <see cref="MediaTypeHeaderValue"/> to use if <paramref name="headerName"/> and <paramref name="headerValue"/>
+ /// is considered a match.</param>
+ public RequestHeaderMapping(string headerName, string headerValue, StringComparison valueComparison, bool isValueSubstring, MediaTypeHeaderValue mediaType)
+ : base(mediaType)
+ {
+ Initialize(headerName, headerValue, valueComparison, isValueSubstring);
+ }
+
+ /// <summary>
+ /// Gets the name of the header to match.
+ /// </summary>
+ public string HeaderName { get; private set; }
+
+ /// <summary>
+ /// Gets the header value to match.
+ /// </summary>
+ public string HeaderValue { get; private set; }
+
+ /// <summary>
+ /// Gets the <see cref="StringComparison"/> to use when matching <see cref="HeaderValue"/>.
+ /// </summary>
+ public StringComparison HeaderValueComparison { get; private set; }
+
+ /// <summary>
+ /// Gets a value indicating whether <see cref="HeaderValue"/> is
+ /// a matched as a substring of the actual header value.
+ /// this instance is value substring.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if <see cref="HeaderValue"/> is to be matched as a substring; otherwise <c>false</c>.
+ /// </value>
+ public bool IsValueSubstring { get; private set; }
+
+ /// <summary>
+ /// Returns a value indicating whether the current <see cref="RequestHeaderMapping"/>
+ /// instance can return a <see cref="MediaTypeHeaderValue"/> from <paramref name="request"/>.
+ /// </summary>
+ /// <param name="request">The <see cref="HttpRequestMessage"/> to check.</param>
+ /// <returns>
+ /// The quality of the match. It must be between <c>0.0</c> and <c>1.0</c>.
+ /// A value of <c>0.0</c> signifies no match.
+ /// A value of <c>1.0</c> signifies a complete match.
+ /// </returns>
+ public override double TryMatchMediaType(HttpRequestMessage request)
+ {
+ if (request == null)
+ {
+ throw new ArgumentNullException("request");
+ }
+
+ return MatchHeaderValue(request, HeaderName, HeaderValue, HeaderValueComparison, IsValueSubstring);
+ }
+
+ private static double MatchHeaderValue(HttpRequestMessage request, string headerName, string headerValue, StringComparison valueComparison, bool isValueSubstring)
+ {
+ Contract.Assert(request != null, "request should not be null");
+ Contract.Assert(headerName != null, "header name should not be null");
+ Contract.Assert(headerValue != null, "header value should not be null");
+
+ IEnumerable<string> values;
+ if (request.Headers.TryGetValues(headerName, out values))
+ {
+ foreach (string value in values)
+ {
+ if (isValueSubstring)
+ {
+ if (value.IndexOf(headerValue, valueComparison) != -1)
+ {
+ return MediaTypeMatch.Match;
+ }
+ }
+ else
+ {
+ if (value.Equals(headerValue, valueComparison))
+ {
+ return MediaTypeMatch.Match;
+ }
+ }
+ }
+ }
+
+ return MediaTypeMatch.NoMatch;
+ }
+
+ private void Initialize(string headerName, string headerValue, StringComparison valueComparison, bool isValueSubstring)
+ {
+ if (String.IsNullOrWhiteSpace(headerName))
+ {
+ throw new ArgumentNullException("headerName");
+ }
+
+ if (String.IsNullOrWhiteSpace(headerValue))
+ {
+ throw new ArgumentNullException("headerValue");
+ }
+
+ StringComparisonHelper.Validate(valueComparison, "valueComparison");
+
+ HeaderName = headerName;
+ HeaderValue = headerValue;
+ HeaderValueComparison = valueComparison;
+ IsValueSubstring = isValueSubstring;
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Formatting/ResponseFormatterSelectionResult.cs b/src/System.Net.Http.Formatting/Formatting/ResponseFormatterSelectionResult.cs
new file mode 100644
index 00000000..0d6790a4
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Formatting/ResponseFormatterSelectionResult.cs
@@ -0,0 +1,40 @@
+namespace System.Net.Http.Formatting
+{
+ /// <summary>
+ /// Contains information about the degree to which a <see cref="MediaTypeFormatter"/> matches the
+ /// explicit or implicit preferences found in an incoming request.
+ /// </summary>
+ internal enum ResponseFormatterSelectionResult
+ {
+ /// <summary>
+ /// No match was found
+ /// </summary>
+ None,
+
+ /// <summary>
+ /// Matched on type meaning that the formatter is able to serialize the type
+ /// </summary>
+ MatchOnCanWriteType,
+
+ /// <summary>
+ /// Matched on explicit content-type set on the <see cref="HttpResponseMessage"/>.
+ /// </summary>
+ MatchOnResponseContentType,
+
+ /// <summary>
+ /// Matched on explicit accept header set in <see cref="HttpRequestMessage"/>.
+ /// </summary>
+ MatchOnRequestAcceptHeader,
+
+ /// <summary>
+ /// Matched on <see cref="HttpRequestMessage"/> after having applied
+ /// the various <see cref="MediaTypeMapping"/>s.
+ /// </summary>
+ MatchOnRequestAcceptHeaderWithMediaTypeMapping,
+
+ /// <summary>
+ /// Matched on the media type of the <see cref="HttpContent"/> of the <see cref="HttpRequestMessage"/>.
+ /// </summary>
+ MatchOnRequestContentType,
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Formatting/ResponseMediaTypeMatch.cs b/src/System.Net.Http.Formatting/Formatting/ResponseMediaTypeMatch.cs
new file mode 100644
index 00000000..62a544c8
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Formatting/ResponseMediaTypeMatch.cs
@@ -0,0 +1,35 @@
+using System.Diagnostics.Contracts;
+
+namespace System.Net.Http.Formatting
+{
+ /// <summary>
+ /// Class that describes the media type that will be used for a response for a
+ /// specific <see cref="MediaTypeFormatter"/>.
+ /// </summary>
+ internal class ResponseMediaTypeMatch
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ResponseMediaTypeMatch"/> class.
+ /// </summary>
+ /// <param name="mediaTypeMatch">The <see cref="MediaTypeMatch"/> containing the media type and its quality factor.</param>
+ /// <param name="result">The kind of match.</param>
+ public ResponseMediaTypeMatch(MediaTypeMatch mediaTypeMatch, ResponseFormatterSelectionResult result)
+ {
+ Contract.Assert(mediaTypeMatch != null, "mediaTypeMatch cannot be null.");
+ Contract.Assert(Enum.IsDefined(typeof(ResponseFormatterSelectionResult), result), "result must be valid ResponseFormatterSelectionResult.");
+
+ ResponseFormatterSelectionResult = result;
+ MediaTypeMatch = mediaTypeMatch;
+ }
+
+ /// <summary>
+ /// Gets the kind of match that occurred.
+ /// </summary>
+ public ResponseFormatterSelectionResult ResponseFormatterSelectionResult { get; private set; }
+
+ /// <summary>
+ /// Gets the media type that was the source of the match.
+ /// </summary>
+ public MediaTypeMatch MediaTypeMatch { get; private set; }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Formatting/SecureJsonTextReader.cs b/src/System.Net.Http.Formatting/Formatting/SecureJsonTextReader.cs
new file mode 100644
index 00000000..f1ca3086
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Formatting/SecureJsonTextReader.cs
@@ -0,0 +1,78 @@
+using System.IO;
+using System.Text;
+using Newtonsoft.Json;
+
+namespace System.Net.Http.Formatting
+{
+ internal class SecureJsonTextReader : JsonTextReader
+ {
+ private const char UnicodeReplacementChar = '\uFFFD';
+ private readonly int _maxDepth;
+
+ public SecureJsonTextReader(TextReader reader, int maxDepth)
+ : base(reader)
+ {
+ _maxDepth = maxDepth;
+ }
+
+ public override object Value
+ {
+ get
+ {
+ if (this.ValueType == typeof(string))
+ {
+ return FixUpInvalidUnicodeString(base.Value as string);
+ }
+ return base.Value;
+ }
+ }
+
+ public override bool Read()
+ {
+ if (this.Depth > _maxDepth)
+ {
+ throw new JsonSerializationException(RS.Format(Properties.Resources.JsonTooDeep, _maxDepth));
+ }
+ return base.Read();
+ }
+
+ private static string FixUpInvalidUnicodeString(string s)
+ {
+ StringBuilder sb = new StringBuilder(s);
+ for (int i = 0; i < sb.Length; i++)
+ {
+ char ch = sb[i];
+ if (Char.IsLowSurrogate(ch))
+ {
+ // Low surrogate with no preceding high surrogate; this char is replaced
+ sb[i] = UnicodeReplacementChar;
+ }
+ else if (Char.IsHighSurrogate(ch))
+ {
+ // Potential start of a surrogate pair
+ if (i + 1 == sb.Length)
+ {
+ // last character is an unmatched surrogate - replace
+ sb[i] = UnicodeReplacementChar;
+ }
+ else
+ {
+ char nextChar = sb[i + 1];
+ if (Char.IsLowSurrogate(nextChar))
+ {
+ // the surrogate pair is valid
+ // skip the low surrogate char
+ i++;
+ }
+ else
+ {
+ // High surrogate not followed by low surrogate; original char is replaced
+ sb[i] = UnicodeReplacementChar;
+ }
+ }
+ }
+ }
+ return sb.ToString();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/System.Net.Http.Formatting/Formatting/StringComparisonHelper.cs b/src/System.Net.Http.Formatting/Formatting/StringComparisonHelper.cs
new file mode 100644
index 00000000..eec55277
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Formatting/StringComparisonHelper.cs
@@ -0,0 +1,44 @@
+using System.ComponentModel;
+
+namespace System.Net.Http.Formatting
+{
+ /// <summary>
+ /// Helper class for validating <see cref="StringComparison"/> values.
+ /// </summary>
+ internal static class StringComparisonHelper
+ {
+ private static readonly Type _stringComparisonType = typeof(StringComparison);
+
+ /// <summary>
+ /// Determines whether the specified <paramref name="value"/> is defined by the <see cref="StringComparison"/>
+ /// enumeration.
+ /// </summary>
+ /// <param name="value">The value to verify.</param>
+ /// <returns>
+ /// <c>true</c> if the specified options is defined; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool IsDefined(StringComparison value)
+ {
+ return value == StringComparison.CurrentCulture ||
+ value == StringComparison.CurrentCultureIgnoreCase ||
+ value == StringComparison.InvariantCulture ||
+ value == StringComparison.InvariantCultureIgnoreCase ||
+ value == StringComparison.Ordinal ||
+ value == StringComparison.OrdinalIgnoreCase;
+ }
+
+ /// <summary>
+ /// Validates the specified <paramref name="value"/> and throws an <see cref="InvalidEnumArgumentException"/>
+ /// exception if not valid.
+ /// </summary>
+ /// <param name="value">The value to validate.</param>
+ /// <param name="parameterName">Name of the parameter to use if throwing exception.</param>
+ public static void Validate(StringComparison value, string parameterName)
+ {
+ if (!IsDefined(value))
+ {
+ throw new InvalidEnumArgumentException(parameterName, (int)value, _stringComparisonType);
+ }
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Formatting/UriPathExtensionMapping.cs b/src/System.Net.Http.Formatting/Formatting/UriPathExtensionMapping.cs
new file mode 100644
index 00000000..d9312130
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Formatting/UriPathExtensionMapping.cs
@@ -0,0 +1,99 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Net.Http.Headers;
+
+namespace System.Net.Http.Formatting
+{
+ /// <summary>
+ /// Class that provides <see cref="MediaTypeHeaderValue"/>s from path extensions appearing
+ /// in a <see cref="Uri"/>.
+ /// </summary>
+ public sealed class UriPathExtensionMapping : MediaTypeMapping
+ {
+ private static readonly Type _uriPathExtensionMappingType = typeof(UriPathExtensionMapping);
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="UriPathExtensionMapping"/> class.
+ /// </summary>
+ /// <param name="uriPathExtension">The extension corresponding to <paramref name="mediaType"/>.
+ /// This value should not include a dot or wildcards.</param>
+ /// <param name="mediaType">The media type that will be returned
+ /// if <paramref name="uriPathExtension"/> is matched.</param>
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", Justification = "There is no meaningful System.Uri representation for a path suffix such as '.xml'")]
+ public UriPathExtensionMapping(string uriPathExtension, string mediaType)
+ : base(mediaType)
+ {
+ Initialize(uriPathExtension);
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="UriPathExtensionMapping"/> class.
+ /// </summary>
+ /// <param name="uriPathExtension">The extension corresponding to <paramref name="mediaType"/>.
+ /// This value should not include a dot or wildcards.</param>
+ /// <param name="mediaType">The <see cref="MediaTypeHeaderValue"/> that will be returned
+ /// if <paramref name="uriPathExtension"/> is matched.</param>
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", Justification = "There is no meaningful System.Uri representation for a path suffix such as '.xml'")]
+ public UriPathExtensionMapping(string uriPathExtension, MediaTypeHeaderValue mediaType)
+ : base(mediaType)
+ {
+ Initialize(uriPathExtension);
+ }
+
+ /// <summary>
+ /// Gets the <see cref="Uri"/> path extension.
+ /// </summary>
+ [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "There is no meaningful System.Uri representation for a path suffix such as '.xml'")]
+ public string UriPathExtension { get; private set; }
+
+ /// <summary>
+ /// Returns a value indicating whether this <see cref="UriPathExtensionMapping"/>
+ /// instance can provide a <see cref="MediaTypeHeaderValue"/> for the <see cref="Uri"/>
+ /// of <paramref name="request"/>.
+ /// </summary>
+ /// <param name="request">The <see cref="HttpRequestMessage"/> to check.</param>
+ /// <returns>If this instance can match a file extension in <paramref name="request"/>
+ /// it returns <c>1.0</c> otherwise <c>0.0</c>.</returns>
+ public override double TryMatchMediaType(HttpRequestMessage request)
+ {
+ if (request == null)
+ {
+ throw new ArgumentNullException("request");
+ }
+
+ string extension = GetUriPathExtensionOrNull(request.RequestUri);
+ return String.Equals(extension, UriPathExtension, StringComparison.OrdinalIgnoreCase) ? MediaTypeMatch.Match : MediaTypeMatch.NoMatch;
+ }
+
+ private static string GetUriPathExtensionOrNull(Uri uri)
+ {
+ if (uri == null)
+ {
+ throw new InvalidOperationException(RS.Format(Properties.Resources.NonNullUriRequiredForMediaTypeMapping, _uriPathExtensionMappingType.Name));
+ }
+
+ string uriPathExtension = null;
+ int numberOfSegments = uri.Segments.Length;
+ if (numberOfSegments > 0)
+ {
+ string lastSegment = uri.Segments[numberOfSegments - 1];
+ int indexAfterFirstPeriod = lastSegment.IndexOf('.') + 1;
+ if (indexAfterFirstPeriod > 0 && indexAfterFirstPeriod < lastSegment.Length)
+ {
+ uriPathExtension = lastSegment.Substring(indexAfterFirstPeriod);
+ }
+ }
+
+ return uriPathExtension;
+ }
+
+ private void Initialize(string uriPathExtension)
+ {
+ if (String.IsNullOrWhiteSpace(uriPathExtension))
+ {
+ throw new ArgumentNullException("uriPathExtension");
+ }
+
+ UriPathExtension = uriPathExtension.Trim().TrimStart('.');
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Formatting/XHRRequestHeaderMapping.cs b/src/System.Net.Http.Formatting/Formatting/XHRRequestHeaderMapping.cs
new file mode 100644
index 00000000..b65d889c
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Formatting/XHRRequestHeaderMapping.cs
@@ -0,0 +1,49 @@
+using System.Linq;
+using System.Net.Http.Headers;
+
+namespace System.Net.Http.Formatting
+{
+ /// <summary>
+ /// A RequestHeaderMapping for the x-requested-with http header set by ajax XHR's
+ /// </summary>
+ internal sealed class XHRRequestHeaderMapping : RequestHeaderMapping
+ {
+ /// <summary>
+ /// Initializes a new instance of <see cref="XHRRequestHeaderMapping" /> class
+ /// </summary>
+ public XHRRequestHeaderMapping() :
+ base(FormattingUtilities.HttpRequestedWithHeader, FormattingUtilities.HttpRequestedWithHeaderValue, StringComparison.OrdinalIgnoreCase, true, MediaTypeConstants.ApplicationJsonMediaType)
+ {
+ }
+
+ /// <summary>
+ /// Returns a value indicating whether the current <see cref="RequestHeaderMapping"/>
+ /// instance can return a <see cref="MediaTypeHeaderValue"/> from <paramref name="request"/>.
+ /// </summary>
+ /// <param name="request">The <see cref="HttpRequestMessage"/> to check.</param>
+ /// <returns>
+ /// The quality of the match.
+ /// A value of <c>0.0</c> signifies no match.
+ /// A value of <c>1.0</c> signifies a complete match.
+ /// </returns>
+ public override double TryMatchMediaType(HttpRequestMessage request)
+ {
+ if (request == null)
+ {
+ throw new ArgumentNullException("request");
+ }
+
+ // Accept header trumps XHR mapping.
+ // Accept: */* is equivalent to passing no Accept header.
+ if (request.Headers.Accept == null
+ || (request.Headers.Accept.Count == 1 && request.Headers.Accept.First().MediaType.Equals("*/*", StringComparison.Ordinal)))
+ {
+ return base.TryMatchMediaType(request);
+ }
+ else
+ {
+ return MediaTypeMatch.NoMatch;
+ }
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Formatting/XmlKeyValueModel.cs b/src/System.Net.Http.Formatting/Formatting/XmlKeyValueModel.cs
new file mode 100644
index 00000000..58b957e3
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Formatting/XmlKeyValueModel.cs
@@ -0,0 +1,63 @@
+using System.Collections.Generic;
+using System.Xml.Linq;
+
+namespace System.Net.Http.Formatting
+{
+ /// <summary>
+ /// Provides an <see cref="IKeyValueModel"/> facade over an XML DOM.
+ /// </summary>
+ internal class XmlKeyValueModel : IKeyValueModel
+ {
+ private readonly IDictionary<string, string> _keys = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+
+ // This implementation will eagerly read the entire DOM and populate the dictionary.
+ public XmlKeyValueModel(XElement root)
+ {
+ if (root.HasElements)
+ {
+ _keys[String.Empty] = String.Empty;
+ ExpandValues(root, prefix: null);
+ }
+ else
+ {
+ // if this is the only element in the document, we can also bind it to any prefix.
+ _keys[String.Empty] = root.Value;
+ }
+ }
+
+ /// <summary>
+ /// Gets all the keys for all the values.
+ /// </summary>
+ /// <returns>The set of all keys.</returns>
+ public IEnumerable<string> Keys
+ {
+ get { return _keys.Keys; }
+ }
+
+ public bool TryGetValue(string key, out object value)
+ {
+ string result;
+ bool found = _keys.TryGetValue(key, out result);
+ value = result;
+ return found;
+ }
+
+ private void ExpandValues(XElement node, string prefix)
+ {
+ foreach (var child in node.Elements())
+ {
+ string name = child.Name.LocalName;
+ string key = (prefix == null) ? name : prefix + "." + name;
+
+ if (child.HasElements)
+ {
+ ExpandValues(child, name);
+ }
+ else
+ {
+ _keys[key] = child.Value;
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Formatting/XmlMediaTypeFormatter.cs b/src/System.Net.Http.Formatting/Formatting/XmlMediaTypeFormatter.cs
new file mode 100644
index 00000000..20db1b41
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Formatting/XmlMediaTypeFormatter.cs
@@ -0,0 +1,474 @@
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+using System.IO;
+using System.Net.Http.Headers;
+using System.Net.Http.Internal;
+using System.Runtime.Serialization;
+using System.Text;
+using System.Threading.Tasks;
+using System.Xml;
+using System.Xml.Linq;
+using System.Xml.Serialization;
+
+namespace System.Net.Http.Formatting
+{
+ /// <summary>
+ /// <see cref="MediaTypeFormatter"/> class to handle Xml.
+ /// </summary>
+ public class XmlMediaTypeFormatter : MediaTypeFormatter
+ {
+ private static readonly Type _xmlSerializerType = typeof(XmlSerializer);
+ private static readonly Type _dataContractSerializerType = typeof(DataContractSerializer);
+ private static readonly Type _xmlMediaTypeFormatterType = typeof(XmlMediaTypeFormatter);
+
+ private static readonly MediaTypeHeaderValue[] _supportedMediaTypes = new MediaTypeHeaderValue[]
+ {
+ MediaTypeConstants.ApplicationXmlMediaType,
+ MediaTypeConstants.TextXmlMediaType
+ };
+
+ // Encoders used for reading data based on charset parameter and default encoder doesn't match
+ private readonly Dictionary<string, Encoding> _decoders = new Dictionary<string, Encoding>(StringComparer.OrdinalIgnoreCase)
+ {
+ { Encoding.UTF8.WebName, new UTF8Encoding(false, true) },
+ { Encoding.Unicode.WebName, new UnicodeEncoding(false, true, true) },
+ };
+
+ private ConcurrentDictionary<Type, object> _serializerCache = new ConcurrentDictionary<Type, object>();
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="XmlMediaTypeFormatter"/> class.
+ /// </summary>
+ public XmlMediaTypeFormatter()
+ {
+ Encoding = new UTF8Encoding(false, true);
+ foreach (MediaTypeHeaderValue value in _supportedMediaTypes)
+ {
+ SupportedMediaTypes.Add(value);
+ }
+ }
+
+ /// <summary>
+ /// Gets the default media type for xml, namely "application/xml".
+ /// </summary>
+ /// <value>
+ /// <remarks>
+ /// The default media type does not have any <c>charset</c> parameter as
+ /// the <see cref="Encoding"/> can be configured on a per <see cref="XmlMediaTypeFormatter"/>
+ /// instance basis.
+ /// </remarks>
+ /// Because <see cref="MediaTypeHeaderValue"/> is mutable, the value
+ /// returned will be a new instance every time.
+ /// </value>
+ public static MediaTypeHeaderValue DefaultMediaType
+ {
+ get { return MediaTypeConstants.ApplicationXmlMediaType; }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to use <see cref="DataContractSerializer"/> by default.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if use <see cref="DataContractSerializer"/> by default; otherwise, <c>false</c>. The default is <c>false</c>.
+ /// </value>
+ [DefaultValue(false)]
+ public bool UseDataContractSerializer { get; set; }
+
+ /// <summary>
+ /// Gets or sets the <see cref="Encoding"/> to use when writing data.
+ /// </summary>
+ /// <remarks>The default encoding is <see cref="UTF8Encoding"/>.</remarks>
+ /// <value>
+ /// The <see cref="Encoding"/> to use when writing data.
+ /// </value>
+ public Encoding CharacterEncoding
+ {
+ get { return Encoding; }
+
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException("value");
+ }
+
+ Type valueType = value.GetType();
+ if (FormattingUtilities.Utf8EncodingType.IsAssignableFrom(valueType) || FormattingUtilities.Utf16EncodingType.IsAssignableFrom(valueType))
+ {
+ Encoding = value;
+ return;
+ }
+
+ throw new ArgumentException(
+ RS.Format(Properties.Resources.UnsupportedEncoding, _xmlMediaTypeFormatterType.Name, FormattingUtilities.Utf8EncodingType.Name, FormattingUtilities.Utf16EncodingType.Name), "value");
+ }
+ }
+
+ /// <summary>
+ /// Registers the <see cref="XmlObjectSerializer"/> to use to read or write
+ /// the specified <paramref name="type"/>.
+ /// </summary>
+ /// <param name="type">The type of object that will be serialized or deserialized with <paramref name="serializer"/>.</param>
+ /// <param name="serializer">The <see cref="XmlObjectSerializer"/> instance to use.</param>
+ public void SetSerializer(Type type, XmlObjectSerializer serializer)
+ {
+ VerifyAndSetSerializer(type, serializer);
+ }
+
+ /// <summary>
+ /// Registers the <see cref="XmlObjectSerializer"/> to use to read or write
+ /// the specified <typeparamref name="T"/> type.
+ /// </summary>
+ /// <typeparam name="T">The type of object that will be serialized or deserialized with <paramref name="serializer"/>.</typeparam>
+ /// <param name="serializer">The <see cref="XmlObjectSerializer"/> instance to use.</param>
+ [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "The T represents a Type parameter.")]
+ public void SetSerializer<T>(XmlObjectSerializer serializer)
+ {
+ SetSerializer(typeof(T), serializer);
+ }
+
+ /// <summary>
+ /// Registers the <see cref="XmlSerializer"/> to use to read or write
+ /// the specified <paramref name="type"/>.
+ /// </summary>
+ /// <param name="type">The type of objects for which <paramref name="serializer"/> will be used.</param>
+ /// <param name="serializer">The <see cref="XmlSerializer"/> instance to use.</param>
+ public void SetSerializer(Type type, XmlSerializer serializer)
+ {
+ VerifyAndSetSerializer(type, serializer);
+ }
+
+ /// <summary>
+ /// Registers the <see cref="XmlSerializer"/> to use to read or write
+ /// the specified <typeparamref name="T"/> type.
+ /// </summary>
+ /// <typeparam name="T">The type of object that will be serialized or deserialized with <paramref name="serializer"/>.</typeparam>
+ /// <param name="serializer">The <see cref="XmlSerializer"/> instance to use.</param>
+ [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "The T represents a Type parameter.")]
+ public void SetSerializer<T>(XmlSerializer serializer)
+ {
+ SetSerializer(typeof(T), serializer);
+ }
+
+ /// <summary>
+ /// Unregisters the serializer currently associated with the given <paramref name="type"/>.
+ /// </summary>
+ /// <remarks>
+ /// Unless another serializer is registered for the <paramref name="type"/>, a default one will be created.
+ /// </remarks>
+ /// <param name="type">The type of object whose serializer should be removed.</param>
+ /// <returns><c>true</c> if a serializer was registered for the <paramref name="type"/>; otherwise <c>false</c>.</returns>
+ public bool RemoveSerializer(Type type)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+
+ object value;
+ return _serializerCache.TryRemove(type, out value);
+ }
+
+ internal bool ContainsSerializerForType(Type type)
+ {
+ return _serializerCache.ContainsKey(type);
+ }
+
+ /// <summary>
+ /// Determines whether this <see cref="XmlMediaTypeFormatter"/> can read objects
+ /// of the specified <paramref name="type"/>.
+ /// </summary>
+ /// <param name="type">The type of object that will be read.</param>
+ /// <returns><c>true</c> if objects of this <paramref name="type"/> can be read, otherwise <c>false</c>.</returns>
+ public override bool CanReadType(Type type)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+
+ if (FormattingUtilities.IsJsonValueType(type))
+ {
+ return false;
+ }
+
+ if (type == typeof(IKeyValueModel))
+ {
+ return true;
+ }
+
+ // If there is a registered non-null serializer, we can support this type.
+ // Otherwise attempt to create the default serializer.
+ object serializer = _serializerCache.GetOrAdd(
+ type,
+ (t) => CreateDefaultSerializer(t, throwOnError: false));
+
+ // Null means we tested it before and know it is not supported
+ return serializer != null;
+ }
+
+ /// <summary>
+ /// Determines whether this <see cref="XmlMediaTypeFormatter"/> can write objects
+ /// of the specified <paramref name="type"/>.
+ /// </summary>
+ /// <param name="type">The type of object that will be written.</param>
+ /// <returns><c>true</c> if objects of this <paramref name="type"/> can be written, otherwise <c>false</c>.</returns>
+ public override bool CanWriteType(Type type)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+
+ if (FormattingUtilities.IsJsonValueType(type))
+ {
+ return false;
+ }
+
+ if (UseDataContractSerializer)
+ {
+ MediaTypeFormatter.TryGetDelegatingTypeForIQueryableGenericOrSame(ref type);
+ }
+ else
+ {
+ MediaTypeFormatter.TryGetDelegatingTypeForIEnumerableGenericOrSame(ref type);
+ }
+
+ // If there is a registered non-null serializer, we can support this type.
+ object serializer = _serializerCache.GetOrAdd(
+ type,
+ (t) => CreateDefaultSerializer(t, throwOnError: false));
+
+ // Null means we tested it before and know it is not supported
+ return serializer != null;
+ }
+
+ /// <summary>
+ /// Wrap a stream to limit the number of potential keys in the deserialized object.
+ /// </summary>
+ private static Stream WrapReadStream(Stream stream)
+ {
+ if (SkipStreamLimitChecks)
+ {
+ return stream;
+ }
+ // XML has 2 tags (open and close) for each field.
+ const int DelimiterDetectionThreshold = 2 * ThresholdStream.DefaultDelimeterThreshold;
+ byte delimeter = (byte)'<'; // delimiter for XML keys
+ return new ThresholdStream(stream, delimeter, DelimiterDetectionThreshold);
+ }
+
+ /// <summary>
+ /// Called during deserialization to read an object of the specified <paramref name="type"/>
+ /// from the specified <paramref name="stream"/>.
+ /// </summary>
+ /// <param name="type">The type of object to read.</param>
+ /// <param name="stream">The <see cref="Stream"/> from which to read.</param>
+ /// <param name="contentHeaders">The <see cref="HttpContentHeaders"/> for the content being read.</param>
+ /// <param name="formatterLogger">The <see cref="IFormatterLogger"/> to log events to.</param>
+ /// <returns>A <see cref="Task"/> whose result will be the object instance that has been read.</returns>
+ public override Task<object> ReadFromStreamAsync(Type type, Stream stream, HttpContentHeaders contentHeaders, IFormatterLogger formatterLogger)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+
+ if (stream == null)
+ {
+ throw new ArgumentNullException("stream");
+ }
+
+ stream = WrapReadStream(stream);
+
+ return TaskHelpers.RunSynchronously<object>(() =>
+ {
+ Encoding effectiveEncoding = Encoding;
+
+ if (contentHeaders != null && contentHeaders.ContentType != null)
+ {
+ string charset = contentHeaders.ContentType.CharSet;
+ if (!String.IsNullOrWhiteSpace(charset) &&
+ !String.Equals(charset, Encoding.WebName) &&
+ !_decoders.TryGetValue(charset, out effectiveEncoding))
+ {
+ effectiveEncoding = Encoding;
+ }
+ }
+
+ if (type == typeof(IKeyValueModel))
+ {
+ using (XmlReader reader = XmlDictionaryReader.CreateTextReader(stream, effectiveEncoding, XmlDictionaryReaderQuotas.Max, null))
+ {
+ XElement root = XElement.Load(reader);
+ return new XmlKeyValueModel(root);
+ }
+ }
+ else
+ {
+ object serializer = GetSerializerForType(type);
+
+ using (XmlReader reader = XmlDictionaryReader.CreateTextReader(stream, effectiveEncoding, XmlDictionaryReaderQuotas.Max, null))
+ {
+ XmlSerializer xmlSerializer = serializer as XmlSerializer;
+ if (xmlSerializer != null)
+ {
+ return xmlSerializer.Deserialize(reader);
+ }
+ else
+ {
+ XmlObjectSerializer xmlObjectSerializer = (XmlObjectSerializer)serializer;
+ return xmlObjectSerializer.ReadObject(reader);
+ }
+ }
+ }
+ });
+ }
+
+ /// <summary>
+ /// Called during serialization to write an object of the specified <paramref name="type"/>
+ /// to the specified <paramref name="stream"/>.
+ /// </summary>
+ /// <param name="type">The type of object to write.</param>
+ /// <param name="value">The object to write.</param>
+ /// <param name="stream">The <see cref="Stream"/> to which to write.</param>
+ /// <param name="contentHeaders">The <see cref="HttpContentHeaders"/> for the content being written.</param>
+ /// <param name="transportContext">The <see cref="TransportContext"/>.</param>
+ /// <returns>A <see cref="Task"/> that will write the value to the stream.</returns>
+ public override Task WriteToStreamAsync(Type type, object value, Stream stream, HttpContentHeaders contentHeaders, TransportContext transportContext)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+
+ if (stream == null)
+ {
+ throw new ArgumentNullException("stream");
+ }
+
+ return TaskHelpers.RunSynchronously(() =>
+ {
+ bool isRemapped = false;
+ if (UseDataContractSerializer)
+ {
+ isRemapped = MediaTypeFormatter.TryGetDelegatingTypeForIQueryableGenericOrSame(ref type);
+ }
+ else
+ {
+ isRemapped = MediaTypeFormatter.TryGetDelegatingTypeForIEnumerableGenericOrSame(ref type);
+ }
+
+ if (isRemapped)
+ {
+ value = MediaTypeFormatter.GetTypeRemappingConstructor(type).Invoke(new object[] { value });
+ }
+
+ object serializer = GetSerializerForType(type);
+
+ // TODO: CSDMain 235508: Should formatters close write stream on completion or leave that to somebody else?
+ using (XmlWriter writer = XmlDictionaryWriter.CreateTextWriter(stream, Encoding, ownsStream: false))
+ {
+ XmlSerializer xmlSerializer = serializer as XmlSerializer;
+ if (xmlSerializer != null)
+ {
+ xmlSerializer.Serialize(writer, value);
+ }
+ else
+ {
+ XmlObjectSerializer xmlObjectSerializer = (XmlObjectSerializer)serializer;
+ xmlObjectSerializer.WriteObject(writer, value);
+ }
+ }
+ });
+ }
+
+ private object CreateDefaultSerializer(Type type, bool throwOnError)
+ {
+ Contract.Assert(type != null, "type cannot be null.");
+ Exception exception = null;
+ object serializer = null;
+
+ try
+ {
+ if (UseDataContractSerializer)
+ {
+ serializer = new DataContractSerializer(type);
+ }
+ else
+ {
+ serializer = new XmlSerializer(type);
+ }
+ }
+ catch (InvalidOperationException invalidOperationException)
+ {
+ exception = invalidOperationException;
+ }
+ catch (NotSupportedException notSupportedException)
+ {
+ exception = notSupportedException;
+ }
+
+ // The serializer throws one of the exceptions above if it cannot
+ // support this type.
+ if (exception != null)
+ {
+ if (throwOnError)
+ {
+ throw new InvalidOperationException(
+ RS.Format(Properties.Resources.SerializerCannotSerializeType,
+ UseDataContractSerializer ? _dataContractSerializerType.Name : _xmlSerializerType.Name,
+ type.Name),
+ exception);
+ }
+ }
+
+ return serializer;
+ }
+
+ private void VerifyAndSetSerializer(Type type, object serializer)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+
+ if (serializer == null)
+ {
+ throw new ArgumentNullException("serializer");
+ }
+
+ SetSerializerInternal(type, serializer);
+ }
+
+ private void SetSerializerInternal(Type type, object serializer)
+ {
+ Contract.Assert(type != null, "type cannot be null.");
+ Contract.Assert(serializer != null, "serializer cannot be null.");
+
+ _serializerCache.AddOrUpdate(type, serializer, (key, value) => serializer);
+ }
+
+ private object GetSerializerForType(Type type)
+ {
+ Contract.Assert(type != null, "Type cannot be null");
+ object serializer = _serializerCache.GetOrAdd(type, (t) => CreateDefaultSerializer(t, throwOnError: true));
+
+ if (serializer == null)
+ {
+ // A null serializer indicates the type has already been tested
+ // and found unsupportable.
+ throw new InvalidOperationException(
+ RS.Format(Properties.Resources.SerializerCannotSerializeType,
+ UseDataContractSerializer ? _dataContractSerializerType.Name : _xmlSerializerType.Name,
+ type.Name));
+ }
+
+ Contract.Assert(serializer is XmlSerializer || serializer is XmlObjectSerializer, "Only XmlSerializer or XmlObjectSerializer are supported.");
+ return serializer;
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/FormattingUtilities.cs b/src/System.Net.Http.Formatting/FormattingUtilities.cs
new file mode 100644
index 00000000..67f64c72
--- /dev/null
+++ b/src/System.Net.Http.Formatting/FormattingUtilities.cs
@@ -0,0 +1,194 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Json;
+using System.Linq;
+using System.Net.Http.Formatting;
+using System.Net.Http.Headers;
+using System.Text;
+
+namespace System.Net.Http
+{
+ /// <summary>
+ /// Provides various internal utility functions
+ /// </summary>
+ internal static class FormattingUtilities
+ {
+ /// <summary>
+ /// HTTP X-Requested-With header field name
+ /// </summary>
+ public const string HttpRequestedWithHeader = @"x-requested-with";
+
+ /// <summary>
+ /// HTTP X-Requested-With header field value
+ /// </summary>
+ public const string HttpRequestedWithHeaderValue = @"xmlhttprequest";
+
+ /// <summary>
+ /// JSON literal for 'null'
+ /// </summary>
+ public const string JsonNullLiteral = "null";
+
+ /// <summary>
+ /// HTTP Host header field name
+ /// </summary>
+ public const string HttpHostHeader = "Host";
+
+ /// <summary>
+ /// HTTP Version token
+ /// </summary>
+ public const string HttpVersionToken = "HTTP";
+
+ /// <summary>
+ /// A <see cref="Type"/> representing <see cref="UTF8Encoding"/>.
+ /// </summary>
+ public static readonly Type Utf8EncodingType = typeof(UTF8Encoding);
+
+ /// <summary>
+ /// A <see cref="Type"/> representing <see cref="UnicodeEncoding"/>.
+ /// </summary>
+ public static readonly Type Utf16EncodingType = typeof(UnicodeEncoding);
+
+ /// <summary>
+ /// A <see cref="Type"/> representing <see cref="HttpRequestMessage"/>.
+ /// </summary>
+ public static readonly Type HttpRequestMessageType = typeof(HttpRequestMessage);
+
+ /// <summary>
+ /// A <see cref="Type"/> representing <see cref="HttpResponseMessage"/>.
+ /// </summary>
+ public static readonly Type HttpResponseMessageType = typeof(HttpResponseMessage);
+
+ /// <summary>
+ /// A <see cref="Type"/> representing <see cref="HttpContent"/>.
+ /// </summary>
+ public static readonly Type HttpContentType = typeof(HttpContent);
+
+ /// <summary>
+ /// A <see cref="Type"/> representing <see cref="DelegatingEnumerable{T}"/>.
+ /// </summary>
+ public static readonly Type DelegatingEnumerableGenericType = typeof(DelegatingEnumerable<>);
+
+ /// <summary>
+ /// A <see cref="Type"/> representing <see cref="IEnumerable{T}"/>.
+ /// </summary>
+ public static readonly Type EnumerableInterfaceGenericType = typeof(IEnumerable<>);
+
+ /// <summary>
+ /// A <see cref="Type"/> representing <see cref="IQueryable{T}"/>.
+ /// </summary>
+ public static readonly Type QueryableInterfaceGenericType = typeof(IQueryable<>);
+
+ /// <summary>
+ /// A <see cref="Type"/> representing <see cref="JsonValue"/>.
+ /// </summary>
+ public static readonly Type JsonValueType = typeof(JsonValue);
+
+ // This list should be kept in sync with the list of headers supported by HttpContentHeaders
+ // TODO: CSDMAIN 231195 -- Change hard-coded list of HttpContentHeaders to dynamic list provided by DCR #225156
+ private static readonly HashSet<string> _httpContentHeaders = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+ {
+ "Allow",
+ "Content-Disposition",
+ "Content-Encoding",
+ "Content-Language",
+ "Content-Length",
+ "Content-Location",
+ "Content-MD5",
+ "Content-Range",
+ "Content-Type",
+ "Expires",
+ "Last-Modified",
+ };
+
+ /// <summary>
+ /// Gets the HTTP headers that are associated with <see cref="HttpContentHeaders"/>.
+ /// </summary>
+ public static HashSet<string> HttpContentHeaders
+ {
+ get { return FormattingUtilities._httpContentHeaders; }
+ }
+
+ /// <summary>
+ /// Determines whether <paramref name="type"/> is a <see cref="JsonValue"/> type.
+ /// </summary>
+ /// <param name="type">The type to test.</param>
+ /// <returns>
+ /// <c>true</c> if <paramref name="type"/> is a <see cref="JsonValue"/> type; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool IsJsonValueType(Type type)
+ {
+ return JsonValueType.IsAssignableFrom(type);
+ }
+
+ /// <summary>
+ /// Creates an empty <see cref="HttpContentHeaders"/> instance. The only way is to get it from a dummy
+ /// <see cref="HttpContent"/> instance.
+ /// </summary>
+ /// <returns>The created instance.</returns>
+ public static HttpContentHeaders CreateEmptyContentHeaders()
+ {
+ HttpContent tempContent = null;
+ HttpContentHeaders contentHeaders = null;
+ try
+ {
+ tempContent = new StringContent(String.Empty);
+ contentHeaders = tempContent.Headers;
+ contentHeaders.Clear();
+ }
+ finally
+ {
+ // We can dispose the content without touching the headers
+ if (tempContent != null)
+ {
+ tempContent.Dispose();
+ }
+ }
+
+ return contentHeaders;
+ }
+
+ /// <summary>
+ /// Ensure the actual collection is identical to the expected one
+ /// </summary>
+ /// <param name="actual">The actual collection of the instance</param>
+ /// <param name="expected">The expected collection of the instance</param>
+ /// <returns>Returns true if they are identical</returns>
+ public static bool ValidateCollection(Collection<MediaTypeHeaderValue> actual, MediaTypeHeaderValue[] expected)
+ {
+ if (actual.Count != expected.Length)
+ {
+ return false;
+ }
+
+ foreach (MediaTypeHeaderValue value in expected)
+ {
+ if (!actual.Contains(value))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Remove bounding quotes on a token if present
+ /// </summary>
+ /// <param name="token">Token to unquote.</param>
+ /// <returns>Unquoted token.</returns>
+ public static string UnquoteToken(string token)
+ {
+ if (String.IsNullOrWhiteSpace(token))
+ {
+ return token;
+ }
+
+ if (token.StartsWith("\"", StringComparison.Ordinal) && token.EndsWith("\"", StringComparison.Ordinal) && token.Length > 1)
+ {
+ return token.Substring(1, token.Length - 2);
+ }
+
+ return token;
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/GlobalSuppressions.cs b/src/System.Net.Http.Formatting/GlobalSuppressions.cs
new file mode 100644
index 00000000..0479618e
--- /dev/null
+++ b/src/System.Net.Http.Formatting/GlobalSuppressions.cs
@@ -0,0 +1,3 @@
+using System.Diagnostics.CodeAnalysis;
+
+[assembly: SuppressMessage("Microsoft.Design", "CA2210:AssembliesShouldHaveValidStrongNames")]
diff --git a/src/System.Net.Http.Formatting/HttpClientExtensions.cs b/src/System.Net.Http.Formatting/HttpClientExtensions.cs
new file mode 100644
index 00000000..e4d9c224
--- /dev/null
+++ b/src/System.Net.Http.Formatting/HttpClientExtensions.cs
@@ -0,0 +1,327 @@
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Net.Http.Formatting;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace System.Net.Http
+{
+ /// <summary>
+ /// Extension methods that aid in making formatted requests using <see cref="HttpClient"/>.
+ /// </summary>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static class HttpClientExtensions
+ {
+ /// <summary>
+ /// Sends a POST request as an asynchronous operation to the specified Uri with the given <paramref name="value"/> serialized
+ /// as JSON.
+ /// </summary>
+ /// <remarks>
+ /// This method uses the default instance of <see cref="JsonMediaTypeFormatter"/>.
+ /// </remarks>
+ /// <typeparam name="T">The type of <paramref name="value"/>.</typeparam>
+ /// <param name="client">The client used to make the request.</param>
+ /// <param name="requestUri">The Uri the request is sent to.</param>
+ /// <param name="value">The value that will be placed in the request's entity body.</param>
+ /// <returns>A task object representing the asynchronous operation.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "We want to support URIs as strings")]
+ public static Task<HttpResponseMessage> PostAsJsonAsync<T>(this HttpClient client, string requestUri, T value)
+ {
+ return client.PostAsJsonAsync(requestUri, value, CancellationToken.None);
+ }
+
+ /// <summary>
+ /// Sends a POST request as an asynchronous operation to the specified Uri with the given <paramref name="value"/> serialized
+ /// as JSON.
+ /// </summary>
+ /// <remarks>
+ /// This method uses the default instance of <see cref="JsonMediaTypeFormatter"/>.
+ /// </remarks>
+ /// <typeparam name="T">The type of <paramref name="value"/>.</typeparam>
+ /// <param name="client">The client used to make the request.</param>
+ /// <param name="requestUri">The Uri the request is sent to.</param>
+ /// <param name="value">The value that will be placed in the request's entity body.</param>
+ /// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
+ /// <returns>A task object representing the asynchronous operation.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "We want to support URIs as strings")]
+ public static Task<HttpResponseMessage> PostAsJsonAsync<T>(this HttpClient client, string requestUri, T value, CancellationToken cancellationToken)
+ {
+ return client.PostAsync(requestUri, value, new JsonMediaTypeFormatter(), cancellationToken);
+ }
+
+ /// <summary>
+ /// Sends a POST request as an asynchronous operation to the specified Uri with the given <paramref name="value"/> serialized
+ /// as XML.
+ /// </summary>
+ /// <remarks>
+ /// This method uses the default instance of <see cref="XmlMediaTypeFormatter"/>.
+ /// </remarks>
+ /// <typeparam name="T">The type of <paramref name="value"/>.</typeparam>
+ /// <param name="client">The client used to make the request.</param>
+ /// <param name="requestUri">The Uri the request is sent to.</param>
+ /// <param name="value">The value that will be placed in the request's entity body.</param>
+ /// <returns>A task object representing the asynchronous operation.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "We want to support URIs as strings")]
+ public static Task<HttpResponseMessage> PostAsXmlAsync<T>(this HttpClient client, string requestUri, T value)
+ {
+ return client.PostAsXmlAsync(requestUri, value, CancellationToken.None);
+ }
+
+ /// <summary>
+ /// Sends a POST request as an asynchronous operation to the specified Uri with the given <paramref name="value"/> serialized
+ /// as XML.
+ /// </summary>
+ /// <remarks>
+ /// This method uses the default instance of <see cref="XmlMediaTypeFormatter"/>.
+ /// </remarks>
+ /// <typeparam name="T">The type of <paramref name="value"/>.</typeparam>
+ /// <param name="client">The client used to make the request.</param>
+ /// <param name="requestUri">The Uri the request is sent to.</param>
+ /// <param name="value">The value that will be placed in the request's entity body.</param>
+ /// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
+ /// <returns>A task object representing the asynchronous operation.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "We want to support URIs as strings")]
+ public static Task<HttpResponseMessage> PostAsXmlAsync<T>(this HttpClient client, string requestUri, T value, CancellationToken cancellationToken)
+ {
+ return client.PostAsync(requestUri, value, new XmlMediaTypeFormatter(), cancellationToken);
+ }
+
+ /// <summary>
+ /// Sends a POST request as an asynchronous operation to the specified Uri with <paramref name="value"/>
+ /// serialized using the given <paramref name="formatter"/>.
+ /// </summary>
+ /// <seealso cref="PostAsync{T}(HttpClient, string, T, MediaTypeFormatter, string, CancellationToken)"/>
+ /// <typeparam name="T">The type of <paramref name="value"/>.</typeparam>
+ /// <param name="client">The client used to make the request.</param>
+ /// <param name="requestUri">The Uri the request is sent to.</param>
+ /// <param name="value">The value that will be placed in the request's entity body.</param>
+ /// <param name="formatter">The formatter used to serialize the <paramref name="value"/>.</param>
+ /// <returns>A task object representing the asynchronous operation.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "We want to support URIs as strings")]
+ public static Task<HttpResponseMessage> PostAsync<T>(this HttpClient client, string requestUri, T value, MediaTypeFormatter formatter)
+ {
+ return client.PostAsync(requestUri, value, formatter, CancellationToken.None);
+ }
+
+ /// <summary>
+ /// Sends a POST request as an asynchronous operation to the specified Uri with <paramref name="value"/>
+ /// serialized using the given <paramref name="formatter"/>.
+ /// </summary>
+ /// <seealso cref="PostAsync{T}(HttpClient, string, T, MediaTypeFormatter, string, CancellationToken)"/>
+ /// <typeparam name="T">The type of <paramref name="value"/>.</typeparam>
+ /// <param name="client">The client used to make the request.</param>
+ /// <param name="requestUri">The Uri the request is sent to.</param>
+ /// <param name="value">The value that will be placed in the request's entity body.</param>
+ /// <param name="formatter">The formatter used to serialize the <paramref name="value"/>.</param>
+ /// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
+ /// <returns>A task object representing the asynchronous operation.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "We want to support URIs as strings")]
+ public static Task<HttpResponseMessage> PostAsync<T>(this HttpClient client, string requestUri, T value, MediaTypeFormatter formatter, CancellationToken cancellationToken)
+ {
+ return client.PostAsync(requestUri, value, formatter, mediaType: null, cancellationToken: cancellationToken);
+ }
+
+ /// <summary>
+ /// Sends a POST request as an asynchronous operation to the specified Uri with <paramref name="value"/>
+ /// serialized using the given <paramref name="formatter"/>.
+ /// </summary>
+ /// <seealso cref="PostAsync{T}(HttpClient, string, T, MediaTypeFormatter, string, CancellationToken)"/>
+ /// <typeparam name="T">The type of <paramref name="value"/>.</typeparam>
+ /// <param name="client">The client used to make the request.</param>
+ /// <param name="requestUri">The Uri the request is sent to.</param>
+ /// <param name="value">The value that will be placed in the request's entity body.</param>
+ /// <param name="formatter">The formatter used to serialize the <paramref name="value"/>.</param>
+ /// <param name="mediaType">The authoritative value of the request's content's Content-Type header. Can be <c>null</c> in which case the
+ /// <paramref name="formatter">formatter's</paramref> default content type will be used.</param>
+ /// <returns>A task object representing the asynchronous operation.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "We want to support URIs as strings")]
+ public static Task<HttpResponseMessage> PostAsync<T>(this HttpClient client, string requestUri, T value, MediaTypeFormatter formatter, string mediaType)
+ {
+ return client.PostAsync(requestUri, value, formatter, mediaType, CancellationToken.None);
+ }
+
+ /// <summary>
+ /// Sends a POST request as an asynchronous operation to the specified Uri with <paramref name="value"/>
+ /// serialized using the given <paramref name="formatter"/>.
+ /// </summary>
+ /// <typeparam name="T">The type of <paramref name="value"/>.</typeparam>
+ /// <param name="client">The client used to make the request.</param>
+ /// <param name="requestUri">The Uri the request is sent to.</param>
+ /// <param name="value">The value that will be placed in the request's entity body.</param>
+ /// <param name="formatter">The formatter used to serialize the <paramref name="value"/>.</param>
+ /// <param name="mediaType">The authoritative value of the request's content's Content-Type header. Can be <c>null</c> in which case the
+ /// <paramref name="formatter">formatter's</paramref> default content type will be used.</param>
+ /// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
+ /// <returns>A task object representing the asynchronous operation.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "We want to support URIs as strings")]
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "The caller is responsible for disposing the object")]
+ [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "The called method will convert to Uri instance.")]
+ public static Task<HttpResponseMessage> PostAsync<T>(this HttpClient client, string requestUri, T value, MediaTypeFormatter formatter, string mediaType, CancellationToken cancellationToken)
+ {
+ if (client == null)
+ {
+ throw new ArgumentNullException("client");
+ }
+
+ var content = new ObjectContent<T>(value, formatter, mediaType);
+
+ return client.PostAsync(requestUri, content, cancellationToken);
+ }
+
+ /// <summary>
+ /// Sends a PUT request as an asynchronous operation to the specified Uri with the given <paramref name="value"/> serialized
+ /// as JSON.
+ /// </summary>
+ /// <remarks>
+ /// This method uses the default instance of <see cref="JsonMediaTypeFormatter"/>.
+ /// </remarks>
+ /// <typeparam name="T">The type of <paramref name="value"/>.</typeparam>
+ /// <param name="client">The client used to make the request.</param>
+ /// <param name="requestUri">The Uri the request is sent to.</param>
+ /// <param name="value">The value that will be placed in the request's entity body.</param>
+ /// <returns>A task object representing the asynchronous operation.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "We want to support URIs as strings")]
+ public static Task<HttpResponseMessage> PutAsJsonAsync<T>(this HttpClient client, string requestUri, T value)
+ {
+ return client.PutAsJsonAsync(requestUri, value, CancellationToken.None);
+ }
+
+ /// <summary>
+ /// Sends a PUT request as an asynchronous operation to the specified Uri with the given <paramref name="value"/> serialized
+ /// as JSON.
+ /// </summary>
+ /// <remarks>
+ /// This method uses the default instance of <see cref="JsonMediaTypeFormatter"/>.
+ /// </remarks>
+ /// <typeparam name="T">The type of <paramref name="value"/>.</typeparam>
+ /// <param name="client">The client used to make the request.</param>
+ /// <param name="requestUri">The Uri the request is sent to.</param>
+ /// <param name="value">The value that will be placed in the request's entity body.</param>
+ /// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
+ /// <returns>A task object representing the asynchronous operation.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "We want to support URIs as strings")]
+ public static Task<HttpResponseMessage> PutAsJsonAsync<T>(this HttpClient client, string requestUri, T value, CancellationToken cancellationToken)
+ {
+ return client.PutAsync(requestUri, value, new JsonMediaTypeFormatter(), cancellationToken);
+ }
+
+ /// <summary>
+ /// Sends a PUT request as an asynchronous operation to the specified Uri with the given <paramref name="value"/> serialized
+ /// as XML.
+ /// </summary>
+ /// <remarks>
+ /// This method uses the default instance of <see cref="XmlMediaTypeFormatter"/>.
+ /// </remarks>
+ /// <typeparam name="T">The type of <paramref name="value"/>.</typeparam>
+ /// <param name="client">The client used to make the request.</param>
+ /// <param name="requestUri">The Uri the request is sent to.</param>
+ /// <param name="value">The value that will be placed in the request's entity body.</param>
+ /// <returns>A task object representing the asynchronous operation.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "We want to support URIs as strings")]
+ public static Task<HttpResponseMessage> PutAsXmlAsync<T>(this HttpClient client, string requestUri, T value)
+ {
+ return client.PutAsXmlAsync(requestUri, value, CancellationToken.None);
+ }
+
+ /// <summary>
+ /// Sends a PUT request as an asynchronous operation to the specified Uri with the given <paramref name="value"/> serialized
+ /// as XML.
+ /// </summary>
+ /// <remarks>
+ /// This method uses the default instance of <see cref="XmlMediaTypeFormatter"/>.
+ /// </remarks>
+ /// <typeparam name="T">The type of <paramref name="value"/>.</typeparam>
+ /// <param name="client">The client used to make the request.</param>
+ /// <param name="requestUri">The Uri the request is sent to.</param>
+ /// <param name="value">The value that will be placed in the request's entity body.</param>
+ /// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
+ /// <returns>A task object representing the asynchronous operation.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "We want to support URIs as strings")]
+ public static Task<HttpResponseMessage> PutAsXmlAsync<T>(this HttpClient client, string requestUri, T value, CancellationToken cancellationToken)
+ {
+ return client.PutAsync(requestUri, value, new XmlMediaTypeFormatter(), cancellationToken);
+ }
+
+ /// <summary>
+ /// Sends a PUT request as an asynchronous operation to the specified Uri with <paramref name="value"/>
+ /// serialized using the given <paramref name="formatter"/>.
+ /// </summary>
+ /// <seealso cref="PutAsync{T}(HttpClient, string, T, MediaTypeFormatter, string, CancellationToken)"/>
+ /// <typeparam name="T">The type of <paramref name="value"/>.</typeparam>
+ /// <param name="client">The client used to make the request.</param>
+ /// <param name="requestUri">The Uri the request is sent to.</param>
+ /// <param name="value">The value that will be placed in the request's entity body.</param>
+ /// <param name="formatter">The formatter used to serialize the <paramref name="value"/>.</param>
+ /// <returns>A task object representing the asynchronous operation.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "We want to support URIs as strings")]
+ public static Task<HttpResponseMessage> PutAsync<T>(this HttpClient client, string requestUri, T value, MediaTypeFormatter formatter)
+ {
+ return client.PutAsync(requestUri, value, formatter, CancellationToken.None);
+ }
+
+ /// <summary>
+ /// Sends a PUT request as an asynchronous operation to the specified Uri with <paramref name="value"/>
+ /// serialized using the given <paramref name="formatter"/>.
+ /// </summary>
+ /// <seealso cref="PutAsync{T}(HttpClient, string, T, MediaTypeFormatter, string, CancellationToken)"/>
+ /// <typeparam name="T">The type of <paramref name="value"/>.</typeparam>
+ /// <param name="client">The client used to make the request.</param>
+ /// <param name="requestUri">The Uri the request is sent to.</param>
+ /// <param name="value">The value that will be placed in the request's entity body.</param>
+ /// <param name="formatter">The formatter used to serialize the <paramref name="value"/>.</param>
+ /// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
+ /// <returns>A task object representing the asynchronous operation.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "We want to support URIs as strings")]
+ public static Task<HttpResponseMessage> PutAsync<T>(this HttpClient client, string requestUri, T value, MediaTypeFormatter formatter, CancellationToken cancellationToken)
+ {
+ return client.PutAsync(requestUri, value, formatter, mediaType: null, cancellationToken: cancellationToken);
+ }
+
+ /// <summary>
+ /// Sends a PUT request as an asynchronous operation to the specified Uri with <paramref name="value"/>
+ /// serialized using the given <paramref name="formatter"/>.
+ /// </summary>
+ /// <seealso cref="PutAsync{T}(HttpClient, string, T, MediaTypeFormatter, string, CancellationToken)"/>
+ /// <typeparam name="T">The type of <paramref name="value"/>.</typeparam>
+ /// <param name="client">The client used to make the request.</param>
+ /// <param name="requestUri">The Uri the request is sent to.</param>
+ /// <param name="value">The value that will be placed in the request's entity body.</param>
+ /// <param name="formatter">The formatter used to serialize the <paramref name="value"/>.</param>
+ /// <param name="mediaType">The authoritative value of the request's content's Content-Type header. Can be <c>null</c> in which case the
+ /// <paramref name="formatter">formatter's</paramref> default content type will be used.</param>
+ /// <returns>A task object representing the asynchronous operation.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "We want to support URIs as strings")]
+ public static Task<HttpResponseMessage> PutAsync<T>(this HttpClient client, string requestUri, T value, MediaTypeFormatter formatter, string mediaType)
+ {
+ return client.PutAsync(requestUri, value, formatter, mediaType, CancellationToken.None);
+ }
+
+ /// <summary>
+ /// Sends a PUT request as an asynchronous operation to the specified Uri with <paramref name="value"/>
+ /// serialized using the given <paramref name="formatter"/>.
+ /// </summary>
+ /// <typeparam name="T">The type of <paramref name="value"/>.</typeparam>
+ /// <param name="client">The client used to make the request.</param>
+ /// <param name="requestUri">The Uri the request is sent to.</param>
+ /// <param name="value">The value that will be placed in the request's entity body.</param>
+ /// <param name="formatter">The formatter used to serialize the <paramref name="value"/>.</param>
+ /// <param name="mediaType">The authoritative value of the request's content's Content-Type header. Can be <c>null</c> in which case the
+ /// <paramref name="formatter">formatter's</paramref> default content type will be used.</param>
+ /// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
+ /// <returns>A task object representing the asynchronous operation.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "We want to support URIs as strings")]
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "The caller is responsible for disposing the object")]
+ [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "The called method will convert to Uri instance.")]
+ public static Task<HttpResponseMessage> PutAsync<T>(this HttpClient client, string requestUri, T value, MediaTypeFormatter formatter, string mediaType, CancellationToken cancellationToken)
+ {
+ if (client == null)
+ {
+ throw new ArgumentNullException("client");
+ }
+
+ var content = new ObjectContent<T>(value, formatter, mediaType);
+
+ return client.PutAsync(requestUri, content, cancellationToken);
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/HttpContentCollectionExtensions.cs b/src/System.Net.Http.Formatting/HttpContentCollectionExtensions.cs
new file mode 100644
index 00000000..5f0808e9
--- /dev/null
+++ b/src/System.Net.Http.Formatting/HttpContentCollectionExtensions.cs
@@ -0,0 +1,271 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics.Contracts;
+using System.Linq;
+using System.Net.Http.Headers;
+
+namespace System.Net.Http
+{
+ /// <summary>
+ /// Extension methods to provide convenience methods for finding <see cref="HttpContent"/> items
+ /// within a <see cref="IEnumerable{HttpContent}"/> collection.
+ /// </summary>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static class HttpContentCollectionExtensions
+ {
+ private const string ContentID = @"Content-ID";
+
+ /// <summary>
+ /// Returns the first <see cref="HttpContent"/> in a sequence that has a <see cref="ContentDispositionHeaderValue"/> header field
+ /// with a <see cref="ContentDispositionHeaderValue.DispositionType"/> property equal to <paramref name="dispositionType"/>.
+ /// </summary>
+ /// <param name="contents">The contents to evaluate</param>
+ /// <param name="dispositionType">The disposition type to look for.</param>
+ /// <returns>The first <see cref="HttpContent"/> in the sequence with a matching disposition type.</returns>
+ public static HttpContent FirstDispositionType(this IEnumerable<HttpContent> contents, string dispositionType)
+ {
+ if (contents == null)
+ {
+ throw new ArgumentNullException("contents");
+ }
+
+ if (String.IsNullOrWhiteSpace(dispositionType))
+ {
+ throw new ArgumentNullException("dispositionType");
+ }
+
+ return contents.First((item) =>
+ {
+ return HttpContentCollectionExtensions.FirstDispositionType(item, dispositionType);
+ });
+ }
+
+ /// <summary>
+ /// Returns the first <see cref="HttpContent"/> in a sequence that has a <see cref="ContentDispositionHeaderValue"/> header field
+ /// with a <see cref="ContentDispositionHeaderValue.DispositionType"/> property equal to <paramref name="dispositionType"/>.
+ /// </summary>
+ /// <param name="contents">The contents to evaluate</param>
+ /// <param name="dispositionType">The disposition type to look for.</param>
+ /// <returns>null if source is empty or if no element matches; otherwise the first <see cref="HttpContent"/> in
+ /// the sequence with a matching disposition type.</returns>
+ public static HttpContent FirstDispositionTypeOrDefault(this IEnumerable<HttpContent> contents, string dispositionType)
+ {
+ if (contents == null)
+ {
+ throw new ArgumentNullException("contents");
+ }
+
+ if (String.IsNullOrWhiteSpace(dispositionType))
+ {
+ throw new ArgumentNullException("dispositionType");
+ }
+
+ return contents.FirstOrDefault((item) =>
+ {
+ return HttpContentCollectionExtensions.FirstDispositionType(item, dispositionType);
+ });
+ }
+
+ /// <summary>
+ /// Returns the first <see cref="HttpContent"/> in a sequence that has a <see cref="ContentDispositionHeaderValue"/> header field
+ /// with a <see cref="ContentDispositionHeaderValue.Name"/> property equal to <paramref name="dispositionName"/>.
+ /// </summary>
+ /// <param name="contents">The contents to evaluate</param>
+ /// <param name="dispositionName">The disposition name to look for.</param>
+ /// <returns>The first <see cref="HttpContent"/> in the sequence with a matching disposition name.</returns>
+ public static HttpContent FirstDispositionName(this IEnumerable<HttpContent> contents, string dispositionName)
+ {
+ if (contents == null)
+ {
+ throw new ArgumentNullException("contents");
+ }
+
+ if (String.IsNullOrWhiteSpace(dispositionName))
+ {
+ throw new ArgumentNullException("dispositionName");
+ }
+
+ return contents.First((item) =>
+ {
+ return HttpContentCollectionExtensions.FirstDispositionName(item, dispositionName);
+ });
+ }
+
+ /// <summary>
+ /// Returns the first <see cref="HttpContent"/> in a sequence that has a <see cref="ContentDispositionHeaderValue"/> header field
+ /// with a <see cref="ContentDispositionHeaderValue.Name"/> property equal to <paramref name="dispositionName"/>.
+ /// </summary>
+ /// <param name="contents">The contents to evaluate</param>
+ /// <param name="dispositionName">The disposition name to look for.</param>
+ /// <returns>null if source is empty or if no element matches; otherwise the first <see cref="HttpContent"/> in
+ /// the sequence with a matching disposition name.</returns>
+ public static HttpContent FirstDispositionNameOrDefault(this IEnumerable<HttpContent> contents, string dispositionName)
+ {
+ if (contents == null)
+ {
+ throw new ArgumentNullException("contents");
+ }
+
+ if (String.IsNullOrWhiteSpace(dispositionName))
+ {
+ throw new ArgumentNullException("dispositionName");
+ }
+
+ return contents.FirstOrDefault((item) =>
+ {
+ return HttpContentCollectionExtensions.FirstDispositionName(item, dispositionName);
+ });
+ }
+
+ /// <summary>
+ /// Returns the <c>start</c> multipart body part. The <c>start</c> is used to identify the main body
+ /// in <c>multipart/related</c> content (see RFC 2387).
+ /// </summary>
+ /// <param name="contents">The contents to evaluate.</param>
+ /// <param name="start">The <c>start</c> value to look for.
+ /// A match is found if a <see cref="HttpContent"/> has a <c>Content-ID</c>
+ /// header field with the given value.</param>
+ /// <returns>The first <see cref="HttpContent"/> in the sequence with a matching value.</returns>
+ public static HttpContent FirstStart(this IEnumerable<HttpContent> contents, string start)
+ {
+ if (contents == null)
+ {
+ throw new ArgumentNullException("contents");
+ }
+
+ if (String.IsNullOrWhiteSpace(start))
+ {
+ throw new ArgumentNullException("start");
+ }
+
+ return contents.First((item) =>
+ {
+ return HttpContentCollectionExtensions.FirstStart(item, start);
+ });
+ }
+
+ /// <summary>
+ /// Returns the first <see cref="HttpContent"/> in a sequence that has a <see cref="ContentDispositionHeaderValue"/> header field
+ /// parameter equal to <paramref name="start"/>. This parameter is typically used in connection with <c>multipart/related</c>
+ /// content (see RFC 2387).
+ /// </summary>
+ /// <param name="contents">The contents to evaluate.</param>
+ /// <param name="start">The start value to look for. A match is found if a <see cref="HttpContent"/> has a <c>Content-ID</c>
+ /// header field with the given value.</param>
+ /// <returns>null if source is empty or if no element matches; otherwise the first <see cref="HttpContent"/> in
+ /// the sequence with a matching value.</returns>
+ public static HttpContent FirstStartOrDefault(this IEnumerable<HttpContent> contents, string start)
+ {
+ if (contents == null)
+ {
+ throw new ArgumentNullException("contents");
+ }
+
+ if (String.IsNullOrWhiteSpace(start))
+ {
+ throw new ArgumentNullException("start");
+ }
+
+ return contents.FirstOrDefault((item) =>
+ {
+ return HttpContentCollectionExtensions.FirstStart(item, start);
+ });
+ }
+
+ /// <summary>
+ /// Returns all instances of <see cref="HttpContent"/> in a sequence that has a <see cref="MediaTypeHeaderValue"/> header field
+ /// with a <see cref="MediaTypeHeaderValue.MediaType"/> property equal to the provided <paramref name="contentType"/>.
+ /// </summary>
+ /// <param name="contents">The content to evaluate</param>
+ /// <param name="contentType">The media type to look for.</param>
+ /// <returns>null if source is empty or if no element matches; otherwise the first <see cref="HttpContent"/> in
+ /// the sequence with a matching media type.</returns>
+ public static IEnumerable<HttpContent> FindAllContentType(this IEnumerable<HttpContent> contents, string contentType)
+ {
+ if (String.IsNullOrWhiteSpace(contentType))
+ {
+ throw new ArgumentNullException("contentType");
+ }
+
+ return HttpContentCollectionExtensions.FindAllContentType(contents, new MediaTypeHeaderValue(contentType));
+ }
+
+ /// <summary>
+ /// Returns all instances of <see cref="HttpContent"/> in a sequence that has a <see cref="MediaTypeHeaderValue"/> header field
+ /// with a <see cref="MediaTypeHeaderValue.MediaType"/> property equal to the provided <paramref name="contentType"/>.
+ /// </summary>
+ /// <param name="contents">The content to evaluate</param>
+ /// <param name="contentType">The media type to look for.</param>
+ /// <returns>null if source is empty or if no element matches; otherwise the first <see cref="HttpContent"/> in
+ /// the sequence with a matching media type.</returns>
+ public static IEnumerable<HttpContent> FindAllContentType(this IEnumerable<HttpContent> contents, MediaTypeHeaderValue contentType)
+ {
+ if (contents == null)
+ {
+ throw new ArgumentNullException("contents");
+ }
+
+ if (contentType == null)
+ {
+ throw new ArgumentNullException("contentType");
+ }
+
+ return contents.Where((item) =>
+ {
+ return HttpContentCollectionExtensions.FindAllContentType(item, contentType);
+ });
+ }
+
+ private static bool FirstStart(HttpContent content, string start)
+ {
+ Contract.Assert(content != null, "content cannot be null");
+ Contract.Assert(start != null, "start cannot be null");
+ if (content.Headers != null)
+ {
+ IEnumerable<string> values;
+ if (content.Headers.TryGetValues(ContentID, out values))
+ {
+ return String.Equals(
+ FormattingUtilities.UnquoteToken(values.ElementAt(0)),
+ FormattingUtilities.UnquoteToken(start),
+ StringComparison.OrdinalIgnoreCase);
+ }
+ }
+
+ return false;
+ }
+
+ private static bool FirstDispositionType(HttpContent content, string dispositionType)
+ {
+ Contract.Assert(content != null, "content cannot be null");
+ Contract.Assert(dispositionType != null, "dispositionType cannot be null");
+ return content.Headers != null && content.Headers.ContentDisposition != null &&
+ String.Equals(
+ FormattingUtilities.UnquoteToken(content.Headers.ContentDisposition.DispositionType),
+ FormattingUtilities.UnquoteToken(dispositionType),
+ StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static bool FirstDispositionName(HttpContent content, string dispositionName)
+ {
+ Contract.Assert(content != null, "content cannot be null");
+ Contract.Assert(dispositionName != null, "dispositionName cannot be null");
+ return content.Headers != null && content.Headers.ContentDisposition != null &&
+ String.Equals(
+ FormattingUtilities.UnquoteToken(content.Headers.ContentDisposition.Name),
+ FormattingUtilities.UnquoteToken(dispositionName),
+ StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static bool FindAllContentType(HttpContent content, MediaTypeHeaderValue contentType)
+ {
+ Contract.Assert(content != null, "content cannot be null");
+ Contract.Assert(contentType != null, "contentType cannot be null");
+ return content.Headers != null && content.Headers.ContentType != null &&
+ String.Equals(
+ content.Headers.ContentType.MediaType,
+ contentType.MediaType,
+ StringComparison.OrdinalIgnoreCase);
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/HttpContentExtensions.cs b/src/System.Net.Http.Formatting/HttpContentExtensions.cs
new file mode 100644
index 00000000..bdda1ec3
--- /dev/null
+++ b/src/System.Net.Http.Formatting/HttpContentExtensions.cs
@@ -0,0 +1,158 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Net.Http.Formatting;
+using System.Net.Http.Headers;
+using System.Net.Http.Internal;
+using System.Threading.Tasks;
+
+namespace System.Net.Http
+{
+ /// <summary>
+ /// Extension methods to allow strongly typed objects to be read from <see cref="HttpContent"/> instances.
+ /// </summary>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static class HttpContentExtensions
+ {
+ /// <summary>
+ /// Returns a <see cref="Task"/> that will yield an object of the specified <paramref name="type"/>
+ /// from the <paramref name="content"/> instance.
+ /// </summary>
+ /// <remarks>This override use the built-in collection of formatters.</remarks>
+ /// <param name="content">The <see cref="HttpContent"/> instance from which to read.</param>
+ /// <param name="type">The type of the object to read.</param>
+ /// <returns>A <see cref="Task"/> that will yield an object instance of the specified type.</returns>
+ public static Task<object> ReadAsAsync(this HttpContent content, Type type)
+ {
+ return content.ReadAsAsync(type, new MediaTypeFormatterCollection());
+ }
+
+ /// <summary>
+ /// Returns a <see cref="Task"/> that will yield an object of the specified <paramref name="type"/>
+ /// from the <paramref name="content"/> instance using one of the provided <paramref name="formatters"/>
+ /// to deserialize the content.
+ /// </summary>
+ /// <param name="content">The <see cref="HttpContent"/> instance from which to read.</param>
+ /// <param name="type">The type of the object to read.</param>
+ /// <param name="formatters">The collection of <see cref="MediaTypeFormatter"/> instances to use.</param>
+ /// <returns>An object instance of the specified type.</returns>
+ public static Task<object> ReadAsAsync(this HttpContent content, Type type, IEnumerable<MediaTypeFormatter> formatters)
+ {
+ return ReadAsAsync<object>(content, type, formatters, null);
+ }
+
+ /// <summary>
+ /// Returns a <see cref="Task"/> that will yield an object of the specified <paramref name="type"/>
+ /// from the <paramref name="content"/> instance using one of the provided <paramref name="formatters"/>
+ /// to deserialize the content.
+ /// </summary>
+ /// <param name="content">The <see cref="HttpContent"/> instance from which to read.</param>
+ /// <param name="type">The type of the object to read.</param>
+ /// <param name="formatters">The collection of <see cref="MediaTypeFormatter"/> instances to use.</param>
+ /// <param name="formatterLogger">The <see cref="IFormatterLogger"/> to log events to.</param>
+ /// <returns>An object instance of the specified type.</returns>
+ public static Task<object> ReadAsAsync(this HttpContent content, Type type, IEnumerable<MediaTypeFormatter> formatters, IFormatterLogger formatterLogger)
+ {
+ return ReadAsAsync<object>(content, type, formatters, formatterLogger);
+ }
+
+ /// <summary>
+ /// Returns a <see cref="Task"/> that will yield an object of the specified
+ /// type <typeparamref name="T"/> from the <paramref name="content"/> instance.
+ /// </summary>
+ /// <remarks>This override use the built-in collection of formatters.</remarks>
+ /// <typeparam name="T">The type of the object to read.</typeparam>
+ /// <param name="content">The <see cref="HttpContent"/> instance from which to read.</param>
+ /// <returns>An object instance of the specified type.</returns>
+ public static Task<T> ReadAsAsync<T>(this HttpContent content)
+ {
+ return content.ReadAsAsync<T>(new MediaTypeFormatterCollection());
+ }
+
+ /// <summary>
+ /// Returns a <see cref="Task"/> that will yield an object of the specified
+ /// type <typeparamref name="T"/> from the <paramref name="content"/> instance.
+ /// </summary>
+ /// <typeparam name="T">The type of the object to read.</typeparam>
+ /// <param name="content">The <see cref="HttpContent"/> instance from which to read.</param>
+ /// <param name="formatters">The collection of <see cref="MediaTypeFormatter"/> instances to use.</param>
+ /// <returns>An object instance of the specified type.</returns>
+ public static Task<T> ReadAsAsync<T>(this HttpContent content, IEnumerable<MediaTypeFormatter> formatters)
+ {
+ return ReadAsAsync<T>(content, typeof(T), formatters, null);
+ }
+
+ /// <summary>
+ /// Returns a <see cref="Task"/> that will yield an object of the specified
+ /// type <typeparamref name="T"/> from the <paramref name="content"/> instance.
+ /// </summary>
+ /// <typeparam name="T">The type of the object to read.</typeparam>
+ /// <param name="content">The <see cref="HttpContent"/> instance from which to read.</param>
+ /// <param name="formatters">The collection of <see cref="MediaTypeFormatter"/> instances to use.</param>
+ /// <param name="formatterLogger">The <see cref="IFormatterLogger"/> to log events to.</param>
+ /// <returns>An object instance of the specified type.</returns>
+ public static Task<T> ReadAsAsync<T>(this HttpContent content, IEnumerable<MediaTypeFormatter> formatters, IFormatterLogger formatterLogger)
+ {
+ return ReadAsAsync<T>(content, typeof(T), formatters, formatterLogger);
+ }
+
+ // There are many helper overloads for ReadAs*(). Provide one worker function to ensure the logic is shared.
+ //
+ // For loosely typed, T = Object, type = specific class.
+ // For strongly typed, T == type.GetType()
+ private static Task<T> ReadAsAsync<T>(HttpContent content, Type type, IEnumerable<MediaTypeFormatter> formatters, IFormatterLogger formatterLogger)
+ {
+ if (content == null)
+ {
+ throw new ArgumentNullException("content");
+ }
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+ if (formatters == null)
+ {
+ throw new ArgumentNullException("formatters");
+ }
+
+ ObjectContent obj = content as ObjectContent;
+ if (obj != null)
+ {
+ return TaskHelpers.FromResult((T)obj.Value);
+ }
+
+ if (content.Headers.ContentLength == 0)
+ {
+ object defaultValue = GetDefaultValueForType(type);
+ return TaskHelpers.FromResult((T)defaultValue);
+ }
+
+ MediaTypeFormatter formatter = null;
+ MediaTypeHeaderValue mediaType = content.Headers.ContentType;
+ if (mediaType != null)
+ {
+ formatter = new MediaTypeFormatterCollection(formatters).FindReader(type, mediaType);
+ }
+
+ if (formatter == null)
+ {
+ string mediaTypeAsString = mediaType != null ? mediaType.MediaType : Properties.Resources.UndefinedMediaType;
+ throw new InvalidOperationException(
+ RS.Format(Properties.Resources.NoReadSerializerAvailable, type.Name, mediaTypeAsString));
+ }
+
+ return content.ReadAsStreamAsync()
+ .Then(stream => formatter.ReadFromStreamAsync(type, stream, content.Headers, formatterLogger)
+ .Then(value => (T)value));
+ }
+
+ private static object GetDefaultValueForType(Type type)
+ {
+ if (!type.IsValueType)
+ {
+ return null;
+ }
+
+ return Activator.CreateInstance(type);
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/HttpContentMessageExtensions.cs b/src/System.Net.Http.Formatting/HttpContentMessageExtensions.cs
new file mode 100644
index 00000000..62e38a64
--- /dev/null
+++ b/src/System.Net.Http.Formatting/HttpContentMessageExtensions.cs
@@ -0,0 +1,384 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Net.Http.Formatting.Parsers;
+using System.Net.Http.Headers;
+using System.Net.Http.Internal;
+using System.Threading.Tasks;
+
+namespace System.Net.Http
+{
+ /// <summary>
+ /// Extension methods to read <see cref="HttpRequestMessage"/> and <see cref="HttpResponseMessage"/> entities from <see cref="HttpContent"/> instances.
+ /// </summary>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static class HttpContentMessageExtensions
+ {
+ private const string HttpContentLengthHeader = "Content-Length";
+ private const int MinBufferSize = 256;
+ private const int DefaultBufferSize = 32 * 1024;
+
+ /// <summary>
+ /// Determines whether the specified content is HTTP request message content.
+ /// </summary>
+ /// <param name="content">The content.</param>
+ /// <returns>
+ /// <c>true</c> if the specified content is HTTP message content; otherwise, <c>false</c>.
+ /// </returns>
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception translates to false.")]
+ public static bool IsHttpRequestMessageContent(this HttpContent content)
+ {
+ if (content == null)
+ {
+ throw new ArgumentNullException("content");
+ }
+
+ try
+ {
+ return HttpMessageContent.ValidateHttpMessageContent(content, true, false);
+ }
+ catch (Exception)
+ {
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// Determines whether the specified content is HTTP response message content.
+ /// </summary>
+ /// <param name="content">The content.</param>
+ /// <returns>
+ /// <c>true</c> if the specified content is HTTP message content; otherwise, <c>false</c>.
+ /// </returns>
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception translates to false.")]
+ public static bool IsHttpResponseMessageContent(this HttpContent content)
+ {
+ if (content == null)
+ {
+ throw new ArgumentNullException("content");
+ }
+
+ try
+ {
+ return HttpMessageContent.ValidateHttpMessageContent(content, false, false);
+ }
+ catch (Exception)
+ {
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// Read the <see cref="HttpContent"/> as an <see cref="HttpRequestMessage"/>.
+ /// </summary>
+ /// <param name="content">The content to read.</param>
+ /// <returns>The parsed <see cref="HttpRequestMessage"/> instance.</returns>
+ public static Task<HttpRequestMessage> ReadAsHttpRequestMessageAsync(this HttpContent content)
+ {
+ return ReadAsHttpRequestMessageAsync(content, Uri.UriSchemeHttp, DefaultBufferSize);
+ }
+
+ /// <summary>
+ /// Read the <see cref="HttpContent"/> as an <see cref="HttpRequestMessage"/>.
+ /// </summary>
+ /// <param name="content">The content to read.</param>
+ /// <param name="uriScheme">The URI scheme to use for the request URI.</param>
+ /// <returns>The parsed <see cref="HttpRequestMessage"/> instance.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "This is not a full URI but only the URI scheme")]
+ public static Task<HttpRequestMessage> ReadAsHttpRequestMessageAsync(this HttpContent content, string uriScheme)
+ {
+ return ReadAsHttpRequestMessageAsync(content, uriScheme, DefaultBufferSize);
+ }
+
+ /// <summary>
+ /// Read the <see cref="HttpContent"/> as an <see cref="HttpRequestMessage"/>.
+ /// </summary>
+ /// <param name="content">The content to read.</param>
+ /// <param name="uriScheme">The URI scheme to use for the request URI (the
+ /// URI scheme is not actually part of the HTTP Request URI and so must be provided externally).</param>
+ /// <param name="bufferSize">Size of the buffer.</param>
+ /// <returns>The parsed <see cref="HttpRequestMessage"/> instance.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "This is not a full URI but only the URI scheme")]
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception translates to parser state.")]
+ public static Task<HttpRequestMessage> ReadAsHttpRequestMessageAsync(this HttpContent content, string uriScheme, int bufferSize)
+ {
+ if (content == null)
+ {
+ throw new ArgumentNullException("content");
+ }
+
+ if (uriScheme == null)
+ {
+ throw new ArgumentNullException("uriScheme");
+ }
+
+ if (!Uri.CheckSchemeName(uriScheme))
+ {
+ throw new ArgumentException(RS.Format(Properties.Resources.HttpMessageParserInvalidUriScheme, uriScheme, typeof(Uri).Name), "uriScheme");
+ }
+
+ if (bufferSize < MinBufferSize)
+ {
+ throw new ArgumentException(RS.Format(Properties.Resources.MinParameterSize, MinBufferSize), "bufferSize");
+ }
+
+ HttpMessageContent.ValidateHttpMessageContent(content, true, true);
+
+ return content.ReadAsStreamAsync().Then(stream =>
+ {
+ HttpUnsortedRequest httpRequest = new HttpUnsortedRequest();
+ HttpRequestHeaderParser parser = new HttpRequestHeaderParser(httpRequest);
+ ParserState parseStatus;
+
+ byte[] buffer = new byte[bufferSize];
+ int bytesRead = 0;
+ int headerConsumed = 0;
+
+ while (true)
+ {
+ try
+ {
+ bytesRead = stream.Read(buffer, 0, buffer.Length);
+ }
+ catch (Exception e)
+ {
+ throw new IOException(Properties.Resources.HttpMessageErrorReading, e);
+ }
+
+ try
+ {
+ parseStatus = parser.ParseBuffer(buffer, bytesRead, ref headerConsumed);
+ }
+ catch (Exception)
+ {
+ parseStatus = ParserState.Invalid;
+ }
+
+ if (parseStatus == ParserState.Done)
+ {
+ return CreateHttpRequestMessage(uriScheme, httpRequest, stream, bytesRead - headerConsumed);
+ }
+ else if (parseStatus != ParserState.NeedMoreData)
+ {
+ throw new IOException(RS.Format(Properties.Resources.HttpMessageParserError, headerConsumed, buffer));
+ }
+ }
+ });
+ }
+
+ /// <summary>
+ /// Read the <see cref="HttpContent"/> as an <see cref="HttpResponseMessage"/>.
+ /// </summary>
+ /// <param name="content">The content to read.</param>
+ /// <returns>The parsed <see cref="HttpResponseMessage"/> instance.</returns>
+ public static Task<HttpResponseMessage> ReadAsHttpResponseMessageAsync(this HttpContent content)
+ {
+ return ReadAsHttpResponseMessageAsync(content, DefaultBufferSize);
+ }
+
+ /// <summary>
+ /// Read the <see cref="HttpContent"/> as an <see cref="HttpResponseMessage"/>.
+ /// </summary>
+ /// <param name="content">The content to read.</param>
+ /// <param name="bufferSize">Size of the buffer.</param>
+ /// <returns>The parsed <see cref="HttpResponseMessage"/> instance.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception translates to parser state.")]
+ public static Task<HttpResponseMessage> ReadAsHttpResponseMessageAsync(this HttpContent content, int bufferSize)
+ {
+ if (content == null)
+ {
+ throw new ArgumentNullException("content");
+ }
+
+ if (bufferSize < MinBufferSize)
+ {
+ throw new ArgumentException(RS.Format(Properties.Resources.MinParameterSize, MinBufferSize), "bufferSize");
+ }
+
+ HttpMessageContent.ValidateHttpMessageContent(content, false, true);
+
+ return content.ReadAsStreamAsync().Then(stream =>
+ {
+ HttpUnsortedResponse httpResponse = new HttpUnsortedResponse();
+ HttpResponseHeaderParser parser = new HttpResponseHeaderParser(httpResponse);
+ ParserState parseStatus;
+
+ byte[] buffer = new byte[bufferSize];
+ int bytesRead = 0;
+ int headerConsumed = 0;
+
+ while (true)
+ {
+ try
+ {
+ bytesRead = stream.Read(buffer, 0, buffer.Length);
+ }
+ catch (Exception e)
+ {
+ throw new IOException(Properties.Resources.HttpMessageErrorReading, e);
+ }
+
+ try
+ {
+ parseStatus = parser.ParseBuffer(buffer, bytesRead, ref headerConsumed);
+ }
+ catch (Exception)
+ {
+ parseStatus = ParserState.Invalid;
+ }
+
+ if (parseStatus == ParserState.Done)
+ {
+ // Create and return parsed HttpResponseMessage
+ return CreateHttpResponseMessage(httpResponse, stream, bytesRead - headerConsumed);
+ }
+ else if (parseStatus != ParserState.NeedMoreData)
+ {
+ throw new IOException(RS.Format(Properties.Resources.HttpMessageParserError, headerConsumed, buffer));
+ }
+ }
+ });
+ }
+
+ /// <summary>
+ /// Creates the request URI by combining scheme (provided) with parsed values of
+ /// host and path.
+ /// </summary>
+ /// <param name="uriScheme">The URI scheme to use for the request URI.</param>
+ /// <param name="httpRequest">The unsorted HTTP request.</param>
+ /// <returns>A fully qualified request URI.</returns>
+ private static Uri CreateRequestUri(string uriScheme, HttpUnsortedRequest httpRequest)
+ {
+ Contract.Assert(httpRequest != null, "httpRequest cannot be null.");
+ Contract.Assert(uriScheme != null, "uriScheme cannot be null");
+
+ IEnumerable<string> hostValues;
+ if (httpRequest.HttpHeaders.TryGetValues(FormattingUtilities.HttpHostHeader, out hostValues))
+ {
+ int hostCount = hostValues.Count();
+ if (hostCount != 1)
+ {
+ throw new IOException(RS.Format(Properties.Resources.HttpMessageParserInvalidHostCount, FormattingUtilities.HttpHostHeader, hostCount));
+ }
+ }
+ else
+ {
+ throw new IOException(RS.Format(Properties.Resources.HttpMessageParserInvalidHostCount, FormattingUtilities.HttpHostHeader, 0));
+ }
+
+ // We don't use UriBuilder as hostValues.ElementAt(0) contains 'host:port' and UriBuilder needs these split out into separate host and port.
+ string requestUri = String.Format(CultureInfo.InvariantCulture, "{0}://{1}{2}", uriScheme, hostValues.ElementAt(0), httpRequest.RequestUri);
+ return new Uri(requestUri);
+ }
+
+ /// <summary>
+ /// Copies the unsorted header fields to a sorted collection.
+ /// </summary>
+ /// <param name="source">The unsorted source headers</param>
+ /// <param name="destination">The destination <see cref="HttpRequestHeaders"/> or <see cref="HttpResponseHeaders"/>.</param>
+ /// <param name="contentStream">The input <see cref="Stream"/> used to form any <see cref="HttpContent"/> being part of this HTTP request.</param>
+ /// <param name="rewind">Start location of any request entity within the <paramref name="contentStream"/>.</param>
+ /// <returns>An <see cref="HttpContent"/> instance if header fields contained and <see cref="HttpContentHeaders"/>.</returns>
+ private static HttpContent CreateHeaderFields(HttpHeaders source, HttpHeaders destination, Stream contentStream, int rewind)
+ {
+ Contract.Assert(source != null, "source headers cannot be null");
+ Contract.Assert(destination != null, "destination headers cannot be null");
+ Contract.Assert(contentStream != null, "contentStream must be non null");
+ HttpContentHeaders contentHeaders = null;
+ HttpContent content = null;
+
+ // Set the header fields
+ foreach (KeyValuePair<string, IEnumerable<string>> header in source)
+ {
+ if (FormattingUtilities.HttpContentHeaders.Contains(header.Key))
+ {
+ if (contentHeaders == null)
+ {
+ contentHeaders = FormattingUtilities.CreateEmptyContentHeaders();
+ }
+
+ contentHeaders.TryAddWithoutValidation(header.Key, header.Value);
+ }
+ else
+ {
+ destination.TryAddWithoutValidation(header.Key, header.Value);
+ }
+ }
+
+ // If we have content headers then create an HttpContent for this Response
+ if (contentHeaders != null)
+ {
+ // Need to rewind the input stream to be at the position right after the HTTP header
+ // which we may already have parsed as we read the content stream.
+ if (!contentStream.CanSeek)
+ {
+ throw new IOException(RS.Format(Properties.Resources.HttpMessageContentStreamMustBeSeekable, "ContentReadStream", FormattingUtilities.HttpResponseMessageType.Name));
+ }
+
+ contentStream.Seek(0 - rewind, SeekOrigin.Current);
+ content = new StreamContent(contentStream);
+ contentHeaders.CopyTo(content.Headers);
+ }
+
+ return content;
+ }
+
+ /// <summary>
+ /// Creates an <see cref="HttpRequestMessage"/> based on information provided in <see cref="HttpUnsortedRequest"/>.
+ /// </summary>
+ /// <param name="uriScheme">The URI scheme to use for the request URI.</param>
+ /// <param name="httpRequest">The unsorted HTTP request.</param>
+ /// <param name="contentStream">The input <see cref="Stream"/> used to form any <see cref="HttpContent"/> being part of this HTTP request.</param>
+ /// <param name="rewind">Start location of any request entity within the <paramref name="contentStream"/>.</param>
+ /// <returns>A newly created <see cref="HttpRequestMessage"/> instance.</returns>
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "caller becomes owner.")]
+ private static HttpRequestMessage CreateHttpRequestMessage(string uriScheme, HttpUnsortedRequest httpRequest, Stream contentStream, int rewind)
+ {
+ Contract.Assert(uriScheme != null, "URI scheme must be non null");
+ Contract.Assert(httpRequest != null, "httpRequest must be non null");
+ Contract.Assert(contentStream != null, "contentStream must be non null");
+
+ HttpRequestMessage httpRequestMessage = new HttpRequestMessage();
+
+ // Set method, requestURI, and version
+ httpRequestMessage.Method = httpRequest.Method;
+ httpRequestMessage.RequestUri = CreateRequestUri(uriScheme, httpRequest);
+ httpRequestMessage.Version = httpRequest.Version;
+
+ // Set the header fields and content if any
+ httpRequestMessage.Content = CreateHeaderFields(httpRequest.HttpHeaders, httpRequestMessage.Headers, contentStream, rewind);
+
+ return httpRequestMessage;
+ }
+
+ /// <summary>
+ /// Creates an <see cref="HttpResponseMessage"/> based on information provided in <see cref="HttpUnsortedResponse"/>.
+ /// </summary>
+ /// <param name="httpResponse">The unsorted HTTP Response.</param>
+ /// <param name="contentStream">The input <see cref="Stream"/> used to form any <see cref="HttpContent"/> being part of this HTTP Response.</param>
+ /// <param name="rewind">Start location of any Response entity within the <paramref name="contentStream"/>.</param>
+ /// <returns>A newly created <see cref="HttpResponseMessage"/> instance.</returns>
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "caller becomes owner.")]
+ private static HttpResponseMessage CreateHttpResponseMessage(HttpUnsortedResponse httpResponse, Stream contentStream, int rewind)
+ {
+ Contract.Assert(httpResponse != null, "httpResponse must be non null");
+ Contract.Assert(contentStream != null, "contentStream must be non null");
+
+ HttpResponseMessage httpResponseMessage = new HttpResponseMessage();
+
+ // Set version, status code and reason phrase
+ httpResponseMessage.Version = httpResponse.Version;
+ httpResponseMessage.StatusCode = httpResponse.StatusCode;
+ httpResponseMessage.ReasonPhrase = httpResponse.ReasonPhrase;
+
+ // Set the header fields and content if any
+ httpResponseMessage.Content = CreateHeaderFields(httpResponse.HttpHeaders, httpResponseMessage.Headers, contentStream, rewind);
+
+ return httpResponseMessage;
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/HttpContentMultipartExtensions.cs b/src/System.Net.Http.Formatting/HttpContentMultipartExtensions.cs
new file mode 100644
index 00000000..57c3a3c0
--- /dev/null
+++ b/src/System.Net.Http.Formatting/HttpContentMultipartExtensions.cs
@@ -0,0 +1,390 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+using System.IO;
+using System.Net.Http.Formatting.Parsers;
+using System.Net.Http.Internal;
+using System.Threading.Tasks;
+
+namespace System.Net.Http
+{
+ /// <summary>
+ /// Extension methods to read MIME multipart entities from <see cref="HttpContent"/> instances.
+ /// </summary>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static class HttpContentMultipartExtensions
+ {
+ private const int MinBufferSize = 256;
+ private const int DefaultBufferSize = 32 * 1024;
+
+ private static readonly AsyncCallback _onMultipartReadAsyncComplete = new AsyncCallback(OnMultipartReadAsyncComplete);
+ private static readonly AsyncCallback _onMultipartWriteSegmentAsyncComplete = new AsyncCallback(OnMultipartWriteSegmentAsyncComplete);
+
+ /// <summary>
+ /// Determines whether the specified content is MIME multipart content.
+ /// </summary>
+ /// <param name="content">The content.</param>
+ /// <returns>
+ /// <c>true</c> if the specified content is MIME multipart content; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool IsMimeMultipartContent(this HttpContent content)
+ {
+ if (content == null)
+ {
+ throw new ArgumentNullException("content");
+ }
+
+ return MimeMultipartBodyPartParser.IsMimeMultipartContent(content);
+ }
+
+ /// <summary>
+ /// Determines whether the specified content is MIME multipart content with the
+ /// specified subtype. For example, the subtype <c>mixed</c> would match content
+ /// with a content type of <c>multipart/mixed</c>.
+ /// </summary>
+ /// <param name="content">The content.</param>
+ /// <param name="subtype">The MIME multipart subtype to match.</param>
+ /// <returns>
+ /// <c>true</c> if the specified content is MIME multipart content with the specified subtype; otherwise, <c>false</c>.
+ /// </returns>
+ public static bool IsMimeMultipartContent(this HttpContent content, string subtype)
+ {
+ if (String.IsNullOrWhiteSpace(subtype))
+ {
+ throw new ArgumentNullException("subtype");
+ }
+
+ if (IsMimeMultipartContent(content))
+ {
+ if (content.Headers.ContentType.MediaType.Equals("multipart/" + subtype, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Reads all body parts within a MIME multipart message and produces a set of <see cref="HttpContent"/> instances as a result.
+ /// </summary>
+ /// <param name="content">An existing <see cref="HttpContent"/> instance to use for the object's content.</param>
+ /// <returns>A <see cref="Task{T}"/> representing the tasks of getting the collection of <see cref="HttpContent"/> instances where each instance represents a body part.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Nesting of generic types is required with Task<T>")]
+ public static Task<IEnumerable<HttpContent>> ReadAsMultipartAsync(this HttpContent content)
+ {
+ return ReadAsMultipartAsync(content, MultipartMemoryStreamProvider.Instance, DefaultBufferSize);
+ }
+
+ /// <summary>
+ /// Reads all body parts within a MIME multipart message and produces a set of <see cref="HttpContent"/> instances as a result
+ /// using the <paramref name="streamProvider"/> instance to determine where the contents of each body part is written.
+ /// </summary>
+ /// <param name="content">An existing <see cref="HttpContent"/> instance to use for the object's content.</param>
+ /// <param name="streamProvider">A stream provider providing output streams for where to write body parts as they are parsed.</param>
+ /// <returns>A <see cref="Task{T}"/> representing the tasks of getting the collection of <see cref="HttpContent"/> instances where each instance represents a body part.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Nesting of generic types is required with Task<T>")]
+ public static Task<IEnumerable<HttpContent>> ReadAsMultipartAsync(this HttpContent content, IMultipartStreamProvider streamProvider)
+ {
+ return ReadAsMultipartAsync(content, streamProvider, DefaultBufferSize);
+ }
+
+ /// <summary>
+ /// Reads all body parts within a MIME multipart message and produces a set of <see cref="HttpContent"/> instances as a result
+ /// using the <paramref name="streamProvider"/> instance to determine where the contents of each body part is written and
+ /// <paramref name="bufferSize"/> as read buffer size.
+ /// </summary>
+ /// <param name="content">An existing <see cref="HttpContent"/> instance to use for the object's content.</param>
+ /// <param name="streamProvider">A stream provider providing output streams for where to write body parts as they are parsed.</param>
+ /// <param name="bufferSize">Size of the buffer used to read the contents.</param>
+ /// <returns>A <see cref="Task{T}"/> representing the tasks of getting the collection of <see cref="HttpContent"/> instances where each instance represents a body part.</returns>
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "caller becomes owner.")]
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Nesting of generic types is required with Task<T>")]
+ public static Task<IEnumerable<HttpContent>> ReadAsMultipartAsync(this HttpContent content, IMultipartStreamProvider streamProvider, int bufferSize)
+ {
+ if (content == null)
+ {
+ throw new ArgumentNullException("content");
+ }
+
+ if (streamProvider == null)
+ {
+ throw new ArgumentNullException("streamProvider");
+ }
+
+ if (bufferSize < MinBufferSize)
+ {
+ throw new ArgumentException(RS.Format(Properties.Resources.MinParameterSize, MinBufferSize), "bufferSize");
+ }
+
+ return content.ReadAsStreamAsync().Then(stream =>
+ {
+ TaskCompletionSource<IEnumerable<HttpContent>> taskCompletionSource = new TaskCompletionSource<IEnumerable<HttpContent>>();
+ MimeMultipartBodyPartParser parser = new MimeMultipartBodyPartParser(content, streamProvider);
+ byte[] data = new byte[bufferSize];
+ MultipartAsyncContext context = new MultipartAsyncContext(stream, taskCompletionSource, parser, data);
+
+ // Start async read/write loop
+ MultipartReadAsync(context);
+
+ // Return task and complete later
+ return taskCompletionSource.Task;
+ });
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is propagated.")]
+ private static void MultipartReadAsync(MultipartAsyncContext context)
+ {
+ Contract.Assert(context != null, "context cannot be null");
+ IAsyncResult result = null;
+ try
+ {
+ result = context.ContentStream.BeginRead(context.Data, 0, context.Data.Length, _onMultipartReadAsyncComplete, context);
+ if (result.CompletedSynchronously)
+ {
+ MultipartReadAsyncComplete(result);
+ }
+ }
+ catch (Exception e)
+ {
+ Exception exception = (result != null && result.CompletedSynchronously) ? e : new IOException(Properties.Resources.ReadAsMimeMultipartErrorReading, e);
+ context.TaskCompletionSource.TrySetException(exception);
+ }
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is propagated.")]
+ private static void OnMultipartReadAsyncComplete(IAsyncResult result)
+ {
+ if (result.CompletedSynchronously)
+ {
+ return;
+ }
+
+ MultipartAsyncContext context = (MultipartAsyncContext)result.AsyncState;
+ Contract.Assert(context != null, "context cannot be null");
+ try
+ {
+ MultipartReadAsyncComplete(result);
+ }
+ catch (Exception e)
+ {
+ context.TaskCompletionSource.TrySetException(e);
+ }
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is propagated.")]
+ private static void MultipartReadAsyncComplete(IAsyncResult result)
+ {
+ Contract.Assert(result != null, "result cannot be null");
+ MultipartAsyncContext context = (MultipartAsyncContext)result.AsyncState;
+ int bytesRead = 0;
+
+ try
+ {
+ bytesRead = context.ContentStream.EndRead(result);
+ }
+ catch (Exception e)
+ {
+ context.TaskCompletionSource.TrySetException(new IOException(Properties.Resources.ReadAsMimeMultipartErrorReading, e));
+ }
+
+ IEnumerable<MimeBodyPart> parts = context.MimeParser.ParseBuffer(context.Data, bytesRead);
+ context.PartsEnumerator = parts.GetEnumerator();
+ MoveNextPart(context);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is propagated.")]
+ private static void MultipartWriteSegmentAsync(MultipartAsyncContext context)
+ {
+ Contract.Assert(context != null, "context cannot be null.");
+ Stream output = context.PartsEnumerator.Current.GetOutputStream();
+ ArraySegment<byte> segment = (ArraySegment<byte>)context.SegmentsEnumerator.Current;
+ try
+ {
+ IAsyncResult result = output.BeginWrite(segment.Array, segment.Offset, segment.Count, _onMultipartWriteSegmentAsyncComplete, context);
+ if (result.CompletedSynchronously)
+ {
+ MultipartWriteSegmentAsyncComplete(result);
+ }
+ }
+ catch (Exception e)
+ {
+ context.PartsEnumerator.Current.Dispose();
+ context.TaskCompletionSource.TrySetException(new IOException(Properties.Resources.ReadAsMimeMultipartErrorWriting, e));
+ }
+ }
+
+ private static void OnMultipartWriteSegmentAsyncComplete(IAsyncResult result)
+ {
+ if (result.CompletedSynchronously)
+ {
+ return;
+ }
+
+ MultipartWriteSegmentAsyncComplete(result);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is propagated.")]
+ private static void MultipartWriteSegmentAsyncComplete(IAsyncResult result)
+ {
+ Contract.Assert(result != null, "result cannot be null.");
+ MultipartAsyncContext context = (MultipartAsyncContext)result.AsyncState;
+ Contract.Assert(context != null, "context cannot be null");
+
+ MimeBodyPart part = context.PartsEnumerator.Current;
+ try
+ {
+ Stream output = context.PartsEnumerator.Current.GetOutputStream();
+ output.EndWrite(result);
+ }
+ catch (Exception e)
+ {
+ part.Dispose();
+ context.TaskCompletionSource.TrySetException(new IOException(Properties.Resources.ReadAsMimeMultipartErrorWriting, e));
+ }
+
+ if (!MoveNextSegment(context))
+ {
+ MoveNextPart(context);
+ }
+ }
+
+ private static void MoveNextPart(MultipartAsyncContext context)
+ {
+ Contract.Assert(context != null, "context cannot be null");
+ while (context.PartsEnumerator.MoveNext())
+ {
+ context.SegmentsEnumerator = context.PartsEnumerator.Current.Segments.GetEnumerator();
+ if (MoveNextSegment(context))
+ {
+ return;
+ }
+ }
+
+ // Read some more
+ MultipartReadAsync(context);
+ }
+
+ private static bool MoveNextSegment(MultipartAsyncContext context)
+ {
+ Contract.Assert(context != null, "context cannot be null");
+ if (context.SegmentsEnumerator.MoveNext())
+ {
+ MultipartWriteSegmentAsync(context);
+ return true;
+ }
+ else if (CheckPartCompletion(context.PartsEnumerator.Current, context.Result))
+ {
+ // We are done parsing
+ context.TaskCompletionSource.TrySetResult(context.Result);
+ return true;
+ }
+
+ return false;
+ }
+
+ private static bool CheckPartCompletion(MimeBodyPart part, List<HttpContent> result)
+ {
+ Contract.Assert(part != null, "part cannot be null.");
+ Contract.Assert(result != null, "result cannot be null.");
+ if (part.IsComplete)
+ {
+ if (part.HttpContent != null)
+ {
+ result.Add(part.HttpContent);
+ }
+
+ bool isFinal = part.IsFinal;
+ part.Dispose();
+ return isFinal;
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Managing state for asynchronous read and write operations
+ /// </summary>
+ private class MultipartAsyncContext
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MultipartAsyncContext"/> class.
+ /// </summary>
+ /// <param name="contentStream">The content stream.</param>
+ /// <param name="taskCompletionSource">The task completion source.</param>
+ /// <param name="mimeParser">The MIME parser.</param>
+ /// <param name="data">The buffer that we read data from.</param>
+ public MultipartAsyncContext(Stream contentStream, TaskCompletionSource<IEnumerable<HttpContent>> taskCompletionSource, MimeMultipartBodyPartParser mimeParser, byte[] data)
+ {
+ Contract.Assert(contentStream != null, "contentStream cannot be null");
+ Contract.Assert(taskCompletionSource != null, "task cannot be null");
+ Contract.Assert(mimeParser != null, "mimeParser cannot be null");
+ Contract.Assert(data != null, "data cannot be null");
+
+ ContentStream = contentStream;
+ Result = new List<HttpContent>();
+ TaskCompletionSource = taskCompletionSource;
+ MimeParser = mimeParser;
+ Data = data;
+ }
+
+ /// <summary>
+ /// Gets the <see cref="Stream"/> that we read from.
+ /// </summary>
+ /// <value>
+ /// The content stream.
+ /// </value>
+ public Stream ContentStream { get; private set; }
+
+ /// <summary>
+ /// Gets the collection of parsed <see cref="HttpContent"/> instances.
+ /// </summary>
+ /// <value>
+ /// The result collection.
+ /// </value>
+ public List<HttpContent> Result { get; private set; }
+
+ /// <summary>
+ /// Gets the task completion source.
+ /// </summary>
+ /// <value>
+ /// The task completion source.
+ /// </value>
+ public TaskCompletionSource<IEnumerable<HttpContent>> TaskCompletionSource { get; private set; }
+
+ /// <summary>
+ /// Gets the data.
+ /// </summary>
+ /// <value>
+ /// The buffer that we read data from.
+ /// </value>
+ public byte[] Data { get; private set; }
+
+ /// <summary>
+ /// Gets the MIME parser.
+ /// </summary>
+ /// <value>
+ /// The MIME parser.
+ /// </value>
+ public MimeMultipartBodyPartParser MimeParser { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the parts enumerator for going through the parsed parts.
+ /// </summary>
+ /// <value>
+ /// The parts enumerator.
+ /// </value>
+ public IEnumerator<MimeBodyPart> PartsEnumerator { get; set; }
+
+ /// <summary>
+ /// Gets or sets the segments enumerator for going through the segments within each part.
+ /// </summary>
+ /// <value>
+ /// The segments enumerator.
+ /// </value>
+ public IEnumerator SegmentsEnumerator { get; set; }
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/HttpHeaderExtensions.cs b/src/System.Net.Http.Formatting/HttpHeaderExtensions.cs
new file mode 100644
index 00000000..e3e679df
--- /dev/null
+++ b/src/System.Net.Http.Formatting/HttpHeaderExtensions.cs
@@ -0,0 +1,42 @@
+using System.Collections.Generic;
+using System.Diagnostics.Contracts;
+using System.Net.Http.Headers;
+
+namespace System.Net.Http
+{
+ internal static class HttpHeaderExtensions
+ {
+ public static void CopyTo(this HttpContentHeaders fromHeaders, HttpContentHeaders toHeaders)
+ {
+ Contract.Assert(fromHeaders != null, "fromHeaders cannot be null.");
+ Contract.Assert(toHeaders != null, "toHeaders cannot be null.");
+
+ foreach (KeyValuePair<string, IEnumerable<string>> header in fromHeaders)
+ {
+ toHeaders.TryAddWithoutValidation(header.Key, header.Value);
+ }
+ }
+
+ public static void CopyTo(this HttpRequestHeaders fromHeaders, HttpRequestHeaders toHeaders)
+ {
+ Contract.Assert(fromHeaders != null, "fromHeaders cannot be null.");
+ Contract.Assert(toHeaders != null, "toHeaders cannot be null.");
+
+ foreach (KeyValuePair<string, IEnumerable<string>> header in fromHeaders)
+ {
+ toHeaders.TryAddWithoutValidation(header.Key, header.Value);
+ }
+ }
+
+ public static void CopyTo(this HttpResponseHeaders fromHeaders, HttpResponseHeaders toHeaders)
+ {
+ Contract.Assert(fromHeaders != null, "fromHeaders cannot be null.");
+ Contract.Assert(toHeaders != null, "toHeaders cannot be null.");
+
+ foreach (KeyValuePair<string, IEnumerable<string>> header in fromHeaders)
+ {
+ toHeaders.TryAddWithoutValidation(header.Key, header.Value);
+ }
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/HttpMessageContent.cs b/src/System.Net.Http.Formatting/HttpMessageContent.cs
new file mode 100644
index 00000000..23bd8c27
--- /dev/null
+++ b/src/System.Net.Http.Formatting/HttpMessageContent.cs
@@ -0,0 +1,457 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+using System.IO;
+using System.Net.Http.Headers;
+using System.Net.Http.Internal;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace System.Net.Http
+{
+ /// <summary>
+ /// Derived <see cref="HttpContent"/> class which can encapsulate an <see cref="HttpResponseMessage"/>
+ /// or an <see cref="HttpRequestMessage"/> as an entity with media type "application/http".
+ /// </summary>
+ public class HttpMessageContent : HttpContent
+ {
+ private const string SP = " ";
+ private const string CRLF = "\r\n";
+ private const string CommaSeparator = ", ";
+
+ private const int DefaultHeaderAllocation = 2 * 1024;
+
+ private const string DefaultMediaType = "application/http";
+
+ private const string MsgTypeParameter = "msgtype";
+ private const string DefaultRequestMsgType = "request";
+ private const string DefaultResponseMsgType = "response";
+
+ private const string DefaultRequestMediaType = DefaultMediaType + "; " + MsgTypeParameter + "=" + DefaultRequestMsgType;
+ private const string DefaultResponseMediaType = DefaultMediaType + "; " + MsgTypeParameter + "=" + DefaultResponseMsgType;
+ private static readonly Task<HttpContent> _nullContentTask = TaskHelpers.FromResult<HttpContent>(null);
+ private static readonly AsyncCallback _onWriteComplete = new AsyncCallback(OnWriteComplete);
+
+ /// <summary>
+ /// Set of header fields that only support single values such as Set-Cookie.
+ /// </summary>
+ private static readonly HashSet<string> _singleValueHeaderFields = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+ {
+ "Set-Cookie",
+ "X-Powered-By",
+ };
+
+ private bool _contentConsumed;
+ private Lazy<Task<Stream>> _streamTask;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpMessageContent"/> class encapsulating an
+ /// <see cref="HttpRequestMessage"/>.
+ /// </summary>
+ /// <param name="httpRequest">The <see cref="HttpResponseMessage"/> instance to encapsulate.</param>
+ public HttpMessageContent(HttpRequestMessage httpRequest)
+ {
+ if (httpRequest == null)
+ {
+ throw new ArgumentNullException("httpRequest");
+ }
+
+ HttpRequestMessage = httpRequest;
+ Headers.ContentType = new MediaTypeHeaderValue(DefaultMediaType);
+ Headers.ContentType.Parameters.Add(new NameValueHeaderValue(MsgTypeParameter, DefaultRequestMsgType));
+
+ InitializeStreamTask();
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpMessageContent"/> class encapsulating an
+ /// <see cref="HttpResponseMessage"/>.
+ /// </summary>
+ /// <param name="httpResponse">The <see cref="HttpResponseMessage"/> instance to encapsulate.</param>
+ public HttpMessageContent(HttpResponseMessage httpResponse)
+ {
+ if (httpResponse == null)
+ {
+ throw new ArgumentNullException("httpResponse");
+ }
+
+ HttpResponseMessage = httpResponse;
+ Headers.ContentType = new MediaTypeHeaderValue(DefaultMediaType);
+ Headers.ContentType.Parameters.Add(new NameValueHeaderValue(MsgTypeParameter, DefaultResponseMsgType));
+
+ InitializeStreamTask();
+ }
+
+ private HttpContent Content
+ {
+ get { return HttpRequestMessage != null ? HttpRequestMessage.Content : HttpResponseMessage.Content; }
+ }
+
+ /// <summary>
+ /// Gets the HTTP request message.
+ /// </summary>
+ public HttpRequestMessage HttpRequestMessage { get; private set; }
+
+ /// <summary>
+ /// Gets the HTTP response message.
+ /// </summary>
+ public HttpResponseMessage HttpResponseMessage { get; private set; }
+
+ private void InitializeStreamTask()
+ {
+ _streamTask = new Lazy<Task<Stream>>(() => Content == null ? null : Content.ReadAsStreamAsync());
+ }
+
+ /// <summary>
+ /// Validates whether the content contains an HTTP Request or an HTTP Response.
+ /// </summary>
+ /// <param name="content">The content to validate.</param>
+ /// <param name="isRequest">if set to <c>true</c> if the content is either an HTTP Request or an HTTP Response.</param>
+ /// <param name="throwOnError">Indicates whether validation failure should result in an <see cref="Exception"/> or not.</param>
+ /// <returns><c>true</c> if content is either an HTTP Request or an HTTP Response</returns>
+ internal static bool ValidateHttpMessageContent(HttpContent content, bool isRequest, bool throwOnError)
+ {
+ if (content == null)
+ {
+ throw new ArgumentNullException("content");
+ }
+
+ MediaTypeHeaderValue contentType = content.Headers.ContentType;
+ if (contentType != null)
+ {
+ if (!contentType.MediaType.Equals(DefaultMediaType, StringComparison.OrdinalIgnoreCase))
+ {
+ if (throwOnError)
+ {
+ throw new ArgumentException(
+ RS.Format(Properties.Resources.HttpMessageInvalidMediaType, FormattingUtilities.HttpContentType.Name,
+ isRequest ? DefaultRequestMediaType : DefaultResponseMediaType),
+ "content");
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ foreach (NameValueHeaderValue parameter in contentType.Parameters)
+ {
+ if (parameter.Name.Equals(MsgTypeParameter, StringComparison.OrdinalIgnoreCase))
+ {
+ string msgType = FormattingUtilities.UnquoteToken(parameter.Value);
+ if (!msgType.Equals(isRequest ? DefaultRequestMsgType : DefaultResponseMsgType, StringComparison.OrdinalIgnoreCase))
+ {
+ if (throwOnError)
+ {
+ throw new ArgumentException(
+ RS.Format(Properties.Resources.HttpMessageInvalidMediaType, FormattingUtilities.HttpContentType.Name, isRequest ? DefaultRequestMediaType : DefaultResponseMediaType),
+ "content");
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+ }
+
+ if (throwOnError)
+ {
+ throw new ArgumentException(
+ RS.Format(Properties.Resources.HttpMessageInvalidMediaType, FormattingUtilities.HttpContentType.Name, isRequest ? DefaultRequestMediaType : DefaultResponseMediaType),
+ "content");
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// Asynchronously serializes the object's content to the given <paramref name="stream"/>.
+ /// </summary>
+ /// <param name="stream">The <see cref="Stream"/> to which to write.</param>
+ /// <param name="context">The associated <see cref="TransportContext"/>.</param>
+ /// <returns>A <see cref="Task"/> instance that is asynchronously serializing the object's content.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is propagated.")]
+ protected override Task SerializeToStreamAsync(Stream stream, TransportContext context)
+ {
+ Contract.Assert(stream != null);
+
+ // Serialize header
+ byte[] header = SerializeHeader();
+
+ TaskCompletionSource<object> writeTask = new TaskCompletionSource<object>();
+ try
+ {
+ // We don't use TaskFactory.FromAsync as it generates an FxCop CA908 error
+ Tuple<HttpMessageContent, Stream, TaskCompletionSource<object>> state =
+ new Tuple<HttpMessageContent, Stream, TaskCompletionSource<object>>(this, stream, writeTask);
+ IAsyncResult result = stream.BeginWrite(header, 0, header.Length, _onWriteComplete, state);
+ if (result.CompletedSynchronously)
+ {
+ WriteComplete(result, this, stream, writeTask);
+ }
+ }
+ catch (Exception e)
+ {
+ writeTask.TrySetException(e);
+ }
+
+ return writeTask.Task;
+ }
+
+ /// <summary>
+ /// Computes the length of the stream if possible.
+ /// </summary>
+ /// <param name="length">The computed length of the stream.</param>
+ /// <returns><c>true</c> if the length has been computed; otherwise <c>false</c>.</returns>
+ [SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1108:BlockStatementsMustNotContainEmbeddedComments",
+ Justification = "The code is more readable with such comments")]
+ protected override bool TryComputeLength(out long length)
+ {
+ // We have four states we could be in:
+ // 1. We have content, but the task is still running or finished without success
+ // 2. We have content, the task has finished successfully, and the stream came back as a null or non-seekable
+ // 3. We have content, the task has finished successfully, and the stream is seekable, so we know its length
+ // 4. We don't have content (streamTask.Value == null)
+ //
+ // For #1 and #2, we return false.
+ // For #3, we return true & the size of our headers + the content length
+ // For #4, we return true & the size of our headers
+
+ bool hasContent = _streamTask.Value != null;
+ length = 0;
+
+ // Cases #1, #2, #3
+ if (hasContent)
+ {
+ Stream readStream;
+ if (!_streamTask.Value.TryGetResult(out readStream) // Case #1
+ || readStream == null || !readStream.CanSeek) // Case #2
+ {
+ length = -1;
+ return false;
+ }
+
+ length = readStream.Length; // Case #3
+ }
+
+ // We serialize header to a StringBuilder so that we can determine the length
+ // following the pattern for HttpContent to try and determine the message length.
+ // The perf overhead is no larger than for the other HttpContent implementations.
+ byte[] header = SerializeHeader();
+ length += header.Length;
+ return true;
+ }
+
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources
+ /// </summary>
+ /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ // If we ended up spinning up a task to get the content stream, make sure we observe any
+ // exceptions so that the task finalizer doesn't tear down our app domain.
+ if (_streamTask != null && _streamTask.IsValueCreated && _streamTask.Value != null)
+ {
+ _streamTask.Value.Catch(ex => TaskHelpers.Completed());
+ _streamTask = null;
+ }
+
+ if (HttpRequestMessage != null)
+ {
+ HttpRequestMessage.Dispose();
+ HttpRequestMessage = null;
+ }
+
+ if (HttpResponseMessage != null)
+ {
+ HttpResponseMessage.Dispose();
+ HttpResponseMessage = null;
+ }
+ }
+
+ base.Dispose(disposing);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is propagated.")]
+ private static void OnWriteComplete(IAsyncResult result)
+ {
+ if (result.CompletedSynchronously)
+ {
+ return;
+ }
+
+ Tuple<HttpMessageContent, Stream, TaskCompletionSource<object>> state =
+ (Tuple<HttpMessageContent, Stream, TaskCompletionSource<object>>)result.AsyncState;
+ Contract.Assert(state != null, "state cannot be null");
+ try
+ {
+ WriteComplete(result, state.Item1, state.Item2, state.Item3);
+ }
+ catch (Exception e)
+ {
+ state.Item3.TrySetException(e);
+ }
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is propagated.")]
+ private static void WriteComplete(IAsyncResult result, HttpMessageContent thisPtr, Stream stream, TaskCompletionSource<object> writeTask)
+ {
+ Contract.Assert(result != null, "result cannot be null");
+ Contract.Assert(thisPtr != null, "thisPtr cannot be null");
+ Contract.Assert(stream != null, "stream cannot be null");
+ Contract.Assert(writeTask != null, "writeTask cannot be null");
+
+ try
+ {
+ stream.EndWrite(result);
+ }
+ catch (Exception e)
+ {
+ writeTask.TrySetException(e);
+ }
+
+ thisPtr.PrepareContentAsync().Then(content =>
+ {
+ if (content != null)
+ {
+ content.CopyToAsync(stream)
+ .CopyResultToCompletionSource(writeTask, completionResult: null);
+ }
+ else
+ {
+ writeTask.TrySetResult(null);
+ }
+ });
+ }
+
+ /// <summary>
+ /// Serializes the HTTP request line.
+ /// </summary>
+ /// <param name="message">Where to write the request line.</param>
+ /// <param name="httpRequest">The HTTP request.</param>
+ private static void SerializeRequestLine(StringBuilder message, HttpRequestMessage httpRequest)
+ {
+ Contract.Assert(message != null, "message cannot be null");
+ message.Append(httpRequest.Method + SP);
+ message.Append(httpRequest.RequestUri.PathAndQuery + SP);
+ message.Append(FormattingUtilities.HttpVersionToken + "/" + (httpRequest.Version != null ? httpRequest.Version.ToString(2) : "1.1") + CRLF);
+
+ // Only insert host header if not already present.
+ if (httpRequest.Headers.Host == null)
+ {
+ message.Append(FormattingUtilities.HttpHostHeader + ":" + SP + httpRequest.RequestUri.Authority + CRLF);
+ }
+ }
+
+ /// <summary>
+ /// Serializes the HTTP status line.
+ /// </summary>
+ /// <param name="message">Where to write the status line.</param>
+ /// <param name="httpResponse">The HTTP response.</param>
+ private static void SerializeStatusLine(StringBuilder message, HttpResponseMessage httpResponse)
+ {
+ Contract.Assert(message != null, "message cannot be null");
+ message.Append(FormattingUtilities.HttpVersionToken + "/" + (httpResponse.Version != null ? httpResponse.Version.ToString(2) : "1.1") + SP);
+ message.Append((int)httpResponse.StatusCode + SP);
+ message.Append(httpResponse.ReasonPhrase + CRLF);
+ }
+
+ /// <summary>
+ /// Serializes the header fields.
+ /// </summary>
+ /// <param name="message">Where to write the status line.</param>
+ /// <param name="headers">The headers to write.</param>
+ private static void SerializeHeaderFields(StringBuilder message, HttpHeaders headers)
+ {
+ Contract.Assert(message != null, "message cannot be null");
+ if (headers != null)
+ {
+ foreach (KeyValuePair<string, IEnumerable<string>> header in headers)
+ {
+ if (_singleValueHeaderFields.Contains(header.Key))
+ {
+ foreach (string value in header.Value)
+ {
+ message.Append(header.Key + ":" + SP + value + CRLF);
+ }
+ }
+ else
+ {
+ message.Append(header.Key + ":" + SP + String.Join(CommaSeparator, header.Value) + CRLF);
+ }
+ }
+ }
+ }
+
+ private Task<HttpContent> PrepareContentAsync()
+ {
+ if (Content == null)
+ {
+ return _nullContentTask;
+ }
+
+ return _streamTask.Value.Then(readStream =>
+ {
+ // If the content needs to be written to a target stream a 2nd time, then the stream must support
+ // seeking (e.g. a FileStream), otherwise the stream can't be copied a second time to a target
+ // stream (e.g. a NetworkStream).
+ if (_contentConsumed)
+ {
+ if (readStream != null && readStream.CanRead)
+ {
+ readStream.Position = 0;
+ }
+ else
+ {
+ throw new InvalidOperationException(
+ RS.Format(Properties.Resources.HttpMessageContentAlreadyRead,
+ FormattingUtilities.HttpContentType.Name,
+ HttpRequestMessage != null
+ ? FormattingUtilities.HttpRequestMessageType.Name
+ : FormattingUtilities.HttpResponseMessageType.Name));
+ }
+
+ _contentConsumed = true;
+ }
+
+ return Content;
+ });
+ }
+
+ private byte[] SerializeHeader()
+ {
+ StringBuilder message = new StringBuilder(DefaultHeaderAllocation);
+ HttpHeaders headers = null;
+ HttpContent content = null;
+ if (HttpRequestMessage != null)
+ {
+ SerializeRequestLine(message, HttpRequestMessage);
+ headers = HttpRequestMessage.Headers;
+ content = HttpRequestMessage.Content;
+ }
+ else
+ {
+ SerializeStatusLine(message, HttpResponseMessage);
+ headers = HttpResponseMessage.Headers;
+ content = HttpResponseMessage.Content;
+ }
+
+ SerializeHeaderFields(message, headers);
+ if (content != null)
+ {
+ SerializeHeaderFields(message, content.Headers);
+ }
+
+ message.Append(CRLF);
+ return Encoding.UTF8.GetBytes(message.ToString());
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/HttpUnsortedHeaders.cs b/src/System.Net.Http.Formatting/HttpUnsortedHeaders.cs
new file mode 100644
index 00000000..b9bfe93a
--- /dev/null
+++ b/src/System.Net.Http.Formatting/HttpUnsortedHeaders.cs
@@ -0,0 +1,15 @@
+using System.Net.Http.Headers;
+
+namespace System.Net.Http
+{
+ /// <summary>
+ /// All of the existing non-abstract <see cref="HttpHeaders"/> implementations, namely
+ /// <see cref="HttpRequestHeaders"/>, <see cref="HttpResponseHeaders"/>, and <see cref="HttpContentHeaders"/>
+ /// enforce strict rules on what kinds of HTTP header fields can be added to each collection.
+ /// When parsing the "application/http" media type we need to just get the unsorted list. It
+ /// will get sorted later.
+ /// </summary>
+ internal class HttpUnsortedHeaders : HttpHeaders
+ {
+ }
+}
diff --git a/src/System.Net.Http.Formatting/HttpUnsortedRequest.cs b/src/System.Net.Http.Formatting/HttpUnsortedRequest.cs
new file mode 100644
index 00000000..8e625abd
--- /dev/null
+++ b/src/System.Net.Http.Formatting/HttpUnsortedRequest.cs
@@ -0,0 +1,51 @@
+using System.Net.Http.Formatting.Parsers;
+using System.Net.Http.Headers;
+
+namespace System.Net.Http
+{
+ /// <summary>
+ /// Represents the HTTP Request Line and header parameters parsed by <see cref="HttpRequestLineParser"/>
+ /// and <see cref="HttpRequestHeaderParser"/>.
+ /// </summary>
+ internal class HttpUnsortedRequest
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpUnsortedRequest"/> class.
+ /// </summary>
+ public HttpUnsortedRequest()
+ {
+ // Collection of unsorted headers. Later we will sort it into the appropriate
+ // HttpContentHeaders, HttpRequestHeaders, and HttpResponseHeaders.
+ HttpHeaders = new HttpUnsortedHeaders();
+ }
+
+ /// <summary>
+ /// Gets or sets the HTTP method.
+ /// </summary>
+ /// <value>
+ /// The HTTP method.
+ /// </value>
+ public HttpMethod Method { get; set; }
+
+ /// <summary>
+ /// Gets or sets the HTTP request URI portion that is carried in the RequestLine (i.e the URI path + query).
+ /// </summary>
+ /// <value>
+ /// The request URI.
+ /// </value>
+ public string RequestUri { get; set; }
+
+ /// <summary>
+ /// Gets or sets the HTTP version.
+ /// </summary>
+ /// <value>
+ /// The HTTP version.
+ /// </value>
+ public Version Version { get; set; }
+
+ /// <summary>
+ /// Gets the unsorted HTTP request headers.
+ /// </summary>
+ public HttpHeaders HttpHeaders { get; private set; }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/HttpUnsortedResponse.cs b/src/System.Net.Http.Formatting/HttpUnsortedResponse.cs
new file mode 100644
index 00000000..3cd7cbdb
--- /dev/null
+++ b/src/System.Net.Http.Formatting/HttpUnsortedResponse.cs
@@ -0,0 +1,51 @@
+using System.Net.Http.Formatting.Parsers;
+using System.Net.Http.Headers;
+
+namespace System.Net.Http
+{
+ /// <summary>
+ /// Represents the HTTP Status Line and header parameters parsed by <see cref="HttpStatusLineParser"/>
+ /// and <see cref="HttpResponseHeaderParser"/>.
+ /// </summary>
+ internal class HttpUnsortedResponse
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpUnsortedRequest"/> class.
+ /// </summary>
+ public HttpUnsortedResponse()
+ {
+ // Collection of unsorted headers. Later we will sort it into the appropriate
+ // HttpContentHeaders, HttpRequestHeaders, and HttpResponseHeaders.
+ HttpHeaders = new HttpUnsortedHeaders();
+ }
+
+ /// <summary>
+ /// Gets or sets the HTTP version.
+ /// </summary>
+ /// <value>
+ /// The HTTP version.
+ /// </value>
+ public Version Version { get; set; }
+
+ /// <summary>
+ /// Gets or sets the <see cref="HttpStatusCode"/>
+ /// </summary>
+ /// <value>
+ /// The HTTP status code
+ /// </value>
+ public HttpStatusCode StatusCode { get; set; }
+
+ /// <summary>
+ /// Gets or sets the HTTP reason phrase
+ /// </summary>
+ /// <value>
+ /// The response reason phrase
+ /// </value>
+ public string ReasonPhrase { get; set; }
+
+ /// <summary>
+ /// Gets the unsorted HTTP request headers.
+ /// </summary>
+ public HttpHeaders HttpHeaders { get; private set; }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/IMultipartStreamProvider.cs b/src/System.Net.Http.Formatting/IMultipartStreamProvider.cs
new file mode 100644
index 00000000..823cc34b
--- /dev/null
+++ b/src/System.Net.Http.Formatting/IMultipartStreamProvider.cs
@@ -0,0 +1,20 @@
+using System.IO;
+using System.Net.Http.Headers;
+
+namespace System.Net.Http
+{
+ /// <summary>
+ /// An <see cref="IMultipartStreamProvider"/> implementation examines the headers provided by the MIME multipart parser
+ /// as part of the MIME multipart extension methods (see <see cref="HttpContentMultipartExtensions"/>) and decides
+ /// what kind of stream to return for the body part to be written to.
+ /// </summary>
+ public interface IMultipartStreamProvider
+ {
+ /// <summary>
+ /// When a MIME multipart body part has been parsed this method is called to get a stream for where to write the body part to.
+ /// </summary>
+ /// <param name="headers">Header fields describing the body part.</param>
+ /// <returns>The <see cref="Stream"/> instance where the message body part is written to.</returns>
+ Stream GetStream(HttpContentHeaders headers);
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Internal/AsyncResultWithExtraData.cs b/src/System.Net.Http.Formatting/Internal/AsyncResultWithExtraData.cs
new file mode 100644
index 00000000..4172d59b
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Internal/AsyncResultWithExtraData.cs
@@ -0,0 +1,36 @@
+namespace System.Net.Http.Internal
+{
+ // Wrapper around async result to store additional data. This is useful to pass data between BeginXYZ / EndXYZ.
+ internal class AsyncResultWithExtraData<T> : IAsyncResult
+ {
+ public AsyncResultWithExtraData(IAsyncResult inner, T extraData)
+ {
+ Inner = inner;
+ ExtraData = extraData;
+ }
+
+ public IAsyncResult Inner { get; private set; }
+
+ public T ExtraData { get; private set; }
+
+ public object AsyncState
+ {
+ get { return Inner.AsyncState; }
+ }
+
+ public Threading.WaitHandle AsyncWaitHandle
+ {
+ get { return Inner.AsyncWaitHandle; }
+ }
+
+ public bool CompletedSynchronously
+ {
+ get { return Inner.CompletedSynchronously; }
+ }
+
+ public bool IsCompleted
+ {
+ get { return Inner.IsCompleted; }
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Internal/DelegatingStream.cs b/src/System.Net.Http.Formatting/Internal/DelegatingStream.cs
new file mode 100644
index 00000000..6e4e32d3
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Internal/DelegatingStream.cs
@@ -0,0 +1,127 @@
+using System.Diagnostics.Contracts;
+using System.IO;
+
+namespace System.Net.Http.Internal
+{
+ /// <summary>
+ /// Stream that delegates to inner stream.
+ /// This is taken from System.Net.Http
+ /// </summary>
+ internal abstract class DelegatingStream : Stream
+ {
+ protected Stream _innerStream;
+
+ protected DelegatingStream(Stream innerStream)
+ {
+ Contract.Assert(innerStream != null);
+ _innerStream = innerStream;
+ }
+
+ public override bool CanRead
+ {
+ get { return _innerStream.CanRead; }
+ }
+
+ public override bool CanSeek
+ {
+ get { return _innerStream.CanSeek; }
+ }
+
+ public override bool CanWrite
+ {
+ get { return _innerStream.CanWrite; }
+ }
+
+ public override long Length
+ {
+ get { return _innerStream.Length; }
+ }
+
+ public override long Position
+ {
+ get { return _innerStream.Position; }
+ set { _innerStream.Position = value; }
+ }
+
+ public override int ReadTimeout
+ {
+ get { return _innerStream.ReadTimeout; }
+ set { _innerStream.ReadTimeout = value; }
+ }
+
+ public override bool CanTimeout
+ {
+ get { return _innerStream.CanTimeout; }
+ }
+
+ public override int WriteTimeout
+ {
+ get { return _innerStream.WriteTimeout; }
+ set { _innerStream.WriteTimeout = value; }
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _innerStream.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ return _innerStream.Seek(offset, origin);
+ }
+
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ return _innerStream.Read(buffer, offset, count);
+ }
+
+ public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
+ {
+ return _innerStream.BeginRead(buffer, offset, count, callback, state);
+ }
+
+ public override int EndRead(IAsyncResult asyncResult)
+ {
+ return _innerStream.EndRead(asyncResult);
+ }
+
+ public override int ReadByte()
+ {
+ return _innerStream.ReadByte();
+ }
+
+ public override void Flush()
+ {
+ _innerStream.Flush();
+ }
+
+ public override void SetLength(long value)
+ {
+ _innerStream.SetLength(value);
+ }
+
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ _innerStream.Write(buffer, offset, count);
+ }
+
+ public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
+ {
+ return _innerStream.BeginWrite(buffer, offset, count, callback, state);
+ }
+
+ public override void EndWrite(IAsyncResult asyncResult)
+ {
+ _innerStream.EndWrite(asyncResult);
+ }
+
+ public override void WriteByte(byte value)
+ {
+ _innerStream.WriteByte(value);
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Internal/TaskExtensions.cs b/src/System.Net.Http.Formatting/Internal/TaskExtensions.cs
new file mode 100644
index 00000000..041e04e8
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Internal/TaskExtensions.cs
@@ -0,0 +1,547 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace System.Net.Http.Internal
+{
+ // TODO, DevDiv 336175, This is copied from System.Web.Http.Common, remove this copy once the issue is addressed.
+ internal static class TaskExtensions
+ {
+ private static Task<AsyncVoid> _defaultCompleted = TaskHelpers.FromResult<AsyncVoid>(default(AsyncVoid));
+ private static readonly Action<Task> _rethrowWithNoStackLossDelegate = GetRethrowWithNoStackLossDelegate();
+
+ /// <summary>
+ /// Calls the given continuation, after the given task completes, if it ends in a faulted state.
+ /// Will not be called if the task did not fault (meaning, it will not be called if the task ran
+ /// to completion or was canceled). Intended to roughly emulate C# 5's support for "try/catch" in
+ /// async methods. Note that this method allows you to return a Task, so that you can either return
+ /// a completed Task (indicating that you swallowed the exception) or a faulted task (indicating that
+ /// that the exception should be propagated). In C#, you cannot normally use await within a catch
+ /// block, so returning a real async task should never be done from Catch().
+ /// </summary>
+ internal static Task Catch(this Task task, Func<Exception, Task> continuation, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ return task.CatchImpl(ex => continuation(ex).ToTask<AsyncVoid>(), cancellationToken);
+ }
+
+ /// <summary>
+ /// Calls the given continuation, after the given task completes, if it ends in a faulted state.
+ /// Will not be called if the task did not fault (meaning, it will not be called if the task ran
+ /// to completion or was canceled). Intended to roughly emulate C# 5's support for "try/catch" in
+ /// async methods. Note that this method allows you to return a Task, so that you can either return
+ /// a completed Task (indicating that you swallowed the exception) or a faulted task (indicating that
+ /// that the exception should be propagated). In C#, you cannot normally use await within a catch
+ /// block, so returning a real async task should never be done from Catch().
+ /// </summary>
+ internal static Task<TResult> Catch<TResult>(this Task<TResult> task, Func<Exception, Task<TResult>> continuation, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ return task.CatchImpl(continuation, cancellationToken);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "The caught exception type is reflected into a faulted task.")]
+ [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")]
+ private static Task<TResult> CatchImpl<TResult>(this Task task, Func<Exception, Task<TResult>> continuation, CancellationToken cancellationToken)
+ {
+ // Stay on the same thread if we can
+ if (task.IsCanceled || cancellationToken.IsCancellationRequested)
+ {
+ return TaskHelpers.Canceled<TResult>();
+ }
+ if (task.IsFaulted)
+ {
+ try
+ {
+ Task<TResult> resultTask = continuation(task.Exception.GetBaseException());
+ if (resultTask == null)
+ {
+ throw new InvalidOperationException(System.Net.Http.Properties.Resources.TaskExtensions_Catch_CannotReturnNull);
+ }
+
+ return resultTask;
+ }
+ catch (Exception ex)
+ {
+ return TaskHelpers.FromError<TResult>(ex);
+ }
+ }
+ if (task.Status == TaskStatus.RanToCompletion)
+ {
+ TaskCompletionSource<TResult> tcs = new TaskCompletionSource<TResult>();
+ tcs.TrySetFromTask(task);
+ return tcs.Task;
+ }
+
+ SynchronizationContext syncContext = SynchronizationContext.Current;
+
+ return task.ContinueWith(innerTask =>
+ {
+ TaskCompletionSource<Task<TResult>> tcs = new TaskCompletionSource<Task<TResult>>();
+
+ if (innerTask.IsFaulted)
+ {
+ if (syncContext != null)
+ {
+ syncContext.Post(state =>
+ {
+ try
+ {
+ Task<TResult> resultTask = continuation(innerTask.Exception.GetBaseException());
+ if (resultTask == null)
+ {
+ throw new InvalidOperationException(System.Net.Http.Properties.Resources.TaskExtensions_Catch_CannotReturnNull);
+ }
+
+ tcs.TrySetResult(resultTask);
+ }
+ catch (Exception ex)
+ {
+ tcs.TrySetException(ex);
+ }
+ }, state: null);
+ }
+ else
+ {
+ Task<TResult> resultTask = continuation(innerTask.Exception.GetBaseException());
+ if (resultTask == null)
+ {
+ throw new InvalidOperationException(System.Net.Http.Properties.Resources.TaskExtensions_Catch_CannotReturnNull);
+ }
+
+ tcs.TrySetResult(resultTask);
+ }
+ }
+ else
+ {
+ tcs.TrySetFromTask(innerTask);
+ }
+
+ return tcs.Task.FastUnwrap();
+ }, cancellationToken).FastUnwrap();
+ }
+
+ /// <summary>
+ /// Upon completion of the task, copies its result into the given task completion source, regardless of the
+ /// completion state. This causes the original task to be fully observed, and the task that is returned by
+ /// this method will always successfully run to completion, regardless of the original task state.
+ /// Since this method consumes a task with no return value, you must provide the return value to be used
+ /// when the inner task ran to successful completion.
+ /// </summary>
+ internal static Task CopyResultToCompletionSource<TResult>(this Task task, TaskCompletionSource<TResult> tcs, TResult completionResult)
+ {
+ return task.CopyResultToCompletionSourceImpl(tcs, innerTask => completionResult);
+ }
+
+ /// <summary>
+ /// Upon completion of the task, copies its result into the given task completion source, regardless of the
+ /// completion state. This causes the original task to be fully observed, and the task that is returned by
+ /// this method will always successfully run to completion, regardless of the original task state.
+ /// </summary>
+ [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")]
+ internal static Task CopyResultToCompletionSource<TResult>(this Task<TResult> task, TaskCompletionSource<TResult> tcs)
+ {
+ return task.CopyResultToCompletionSourceImpl(tcs, innerTask => innerTask.Result);
+ }
+
+ [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")]
+ private static Task CopyResultToCompletionSourceImpl<TTask, TResult>(this TTask task, TaskCompletionSource<TResult> tcs, Func<TTask, TResult> resultThunk)
+ where TTask : Task
+ {
+ if (task.IsCompleted)
+ {
+ switch (task.Status)
+ {
+ case TaskStatus.Canceled:
+ case TaskStatus.Faulted:
+ TaskHelpers.TrySetFromTask(tcs, task);
+ break;
+
+ case TaskStatus.RanToCompletion:
+ tcs.TrySetResult(resultThunk(task));
+ break;
+ }
+
+ return TaskHelpers.Completed();
+ }
+
+ return task.ContinueWith(innerTask =>
+ {
+ switch (innerTask.Status)
+ {
+ case TaskStatus.Canceled:
+ case TaskStatus.Faulted:
+ TaskHelpers.TrySetFromTask(tcs, innerTask);
+ break;
+
+ case TaskStatus.RanToCompletion:
+ tcs.TrySetResult(resultThunk(task));
+ break;
+ }
+ }, TaskContinuationOptions.ExecuteSynchronously);
+ }
+
+ /// <summary>
+ /// A version of task.Unwrap that is optimized to prevent unnecessarily capturing the
+ /// execution context when the antecedent task is already completed.
+ /// </summary>
+ [SuppressMessage("Microsoft.WebAPI", "CR4000:DoNotUseProblematicTaskTypes", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")]
+ [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")]
+ internal static Task FastUnwrap(this Task<Task> task)
+ {
+ Task innerTask = task.Status == TaskStatus.RanToCompletion ? task.Result : null;
+ return innerTask ?? task.Unwrap();
+ }
+
+ /// <summary>
+ /// A version of task.Unwrap that is optimized to prevent unnecessarily capturing the
+ /// execution context when the antecedent task is already completed.
+ /// </summary>
+ [SuppressMessage("Microsoft.WebAPI", "CR4000:DoNotUseProblematicTaskTypes", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")]
+ [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")]
+ internal static Task<TResult> FastUnwrap<TResult>(this Task<Task<TResult>> task)
+ {
+ Task<TResult> innerTask = task.Status == TaskStatus.RanToCompletion ? task.Result : null;
+ return innerTask ?? task.Unwrap();
+ }
+
+ /// <summary>
+ /// Calls the given continuation, after the given task has completed, regardless of the state
+ /// the task ended in. Intended to roughly emulate C# 5's support for "finally" in async methods.
+ /// </summary>
+ internal static Task Finally(this Task task, Action continuation)
+ {
+ return task.FinallyImpl<AsyncVoid>(continuation);
+ }
+
+ /// <summary>
+ /// Calls the given continuation, after the given task has completed, regardless of the state
+ /// the task ended in. Intended to roughly emulate C# 5's support for "finally" in async methods.
+ /// </summary>
+ internal static Task<TResult> Finally<TResult>(this Task<TResult> task, Action continuation)
+ {
+ return task.FinallyImpl<TResult>(continuation);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "The caught exception type is reflected into a faulted task.")]
+ [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")]
+ private static Task<TResult> FinallyImpl<TResult>(this Task task, Action continuation)
+ {
+ // Stay on the same thread if we can
+ if (task.IsCompleted)
+ {
+ TaskCompletionSource<TResult> tcs = new TaskCompletionSource<TResult>();
+ try
+ {
+ continuation();
+ tcs.TrySetFromTask(task);
+ }
+ catch (Exception ex)
+ {
+ tcs.TrySetException(ex);
+ }
+ return tcs.Task;
+ }
+
+ SynchronizationContext syncContext = SynchronizationContext.Current;
+
+ return task.ContinueWith(innerTask =>
+ {
+ TaskCompletionSource<TResult> tcs = new TaskCompletionSource<TResult>();
+ if (syncContext != null)
+ {
+ syncContext.Post(state =>
+ {
+ try
+ {
+ continuation();
+ tcs.TrySetFromTask(innerTask);
+ }
+ catch (Exception ex)
+ {
+ tcs.SetException(ex);
+ }
+ }, state: null);
+ }
+ else
+ {
+ continuation();
+ tcs.TrySetFromTask(innerTask);
+ }
+
+ return tcs.Task;
+ }).FastUnwrap();
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2201:DoNotRaiseReservedExceptionTypes", Justification = "This general exception is not intended to be seen by the user")]
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "This general exception is not intended to be seen by the user")]
+ [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")]
+ private static Action<Task> GetRethrowWithNoStackLossDelegate()
+ {
+ MethodInfo getAwaiterMethod = typeof(Task).GetMethod("GetAwaiter", Type.EmptyTypes);
+ if (getAwaiterMethod != null)
+ {
+ // .NET 4.5 - dump the same code the 'await' keyword would have dumped
+ // >> task.GetAwaiter().GetResult()
+ // No-ops if the task completed successfully, else throws the originating exception complete with the correct call stack.
+ var taskParameter = Expression.Parameter(typeof(Task));
+ var getAwaiterCall = Expression.Call(taskParameter, getAwaiterMethod);
+ var getResultCall = Expression.Call(getAwaiterCall, "GetResult", Type.EmptyTypes);
+ var lambda = Expression.Lambda<Action<Task>>(getResultCall, taskParameter);
+ return lambda.Compile();
+ }
+ else
+ {
+ Func<Exception, Exception> prepForRemoting = null;
+
+ try
+ {
+ if (AppDomain.CurrentDomain.IsFullyTrusted)
+ {
+ // .NET 4 - do the same thing Lazy<T> does by calling Exception.PrepForRemoting
+ // This is an internal method in mscorlib.dll, so pass a test Exception to it to make sure we can call it.
+ var exceptionParameter = Expression.Parameter(typeof(Exception));
+ var prepForRemotingCall = Expression.Call(exceptionParameter, "PrepForRemoting", Type.EmptyTypes);
+ var lambda = Expression.Lambda<Func<Exception, Exception>>(prepForRemotingCall, exceptionParameter);
+ var func = lambda.Compile();
+ func(new Exception()); // make sure the method call succeeds before assigning the 'prepForRemoting' local variable
+ prepForRemoting = func;
+ }
+ }
+ catch
+ {
+ } // If delegate creation fails (medium trust) we will simply throw the base exception.
+
+ return task =>
+ {
+ try
+ {
+ task.Wait();
+ }
+ catch (AggregateException ex)
+ {
+ Exception baseException = ex.GetBaseException();
+ if (prepForRemoting != null)
+ {
+ baseException = prepForRemoting(baseException);
+ }
+ throw baseException;
+ }
+ };
+ }
+ }
+
+ /// <summary>
+ /// Calls the given continuation, after the given task has completed, if the task successfully ran
+ /// to completion (i.e., was not cancelled and did not fault).
+ /// </summary>
+ internal static Task Then(this Task task, Action continuation, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ return task.ThenImpl(t => ToAsyncVoidTask(continuation), cancellationToken);
+ }
+
+ /// <summary>
+ /// Calls the given continuation, after the given task has completed, if the task successfully ran
+ /// to completion (i.e., was not cancelled and did not fault).
+ /// </summary>
+ internal static Task<TOuterResult> Then<TOuterResult>(this Task task, Func<TOuterResult> continuation, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ return task.ThenImpl(t => TaskHelpers.FromResult(continuation()), cancellationToken);
+ }
+
+ /// <summary>
+ /// Calls the given continuation, after the given task has completed, if the task successfully ran
+ /// to completion (i.e., was not cancelled and did not fault).
+ /// </summary>
+ internal static Task<TOuterResult> Then<TOuterResult>(this Task task, Func<Task<TOuterResult>> continuation, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ return task.ThenImpl(t => continuation(), cancellationToken);
+ }
+
+ /// <summary>
+ /// Calls the given continuation, after the given task has completed, if the task successfully ran
+ /// to completion (i.e., was not cancelled and did not fault). The continuation is provided with the
+ /// result of the task as its sole parameter.
+ /// </summary>
+ [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")]
+ internal static Task Then<TInnerResult>(this Task<TInnerResult> task, Action<TInnerResult> continuation, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ return task.ThenImpl(t => ToAsyncVoidTask(() => continuation(t.Result)), cancellationToken);
+ }
+
+ /// <summary>
+ /// Calls the given continuation, after the given task has completed, if the task successfully ran
+ /// to completion (i.e., was not cancelled and did not fault). The continuation is provided with the
+ /// result of the task as its sole parameter.
+ /// </summary>
+ [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")]
+ internal static Task<TOuterResult> Then<TInnerResult, TOuterResult>(this Task<TInnerResult> task, Func<TInnerResult, TOuterResult> continuation, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ return task.ThenImpl(t => TaskHelpers.FromResult(continuation(t.Result)), cancellationToken);
+ }
+
+ /// <summary>
+ /// Calls the given continuation, after the given task has completed, if the task successfully ran
+ /// to completion (i.e., was not cancelled and did not fault). The continuation is provided with the
+ /// result of the task as its sole parameter.
+ /// </summary>
+ [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")]
+ internal static Task<TOuterResult> Then<TInnerResult, TOuterResult>(this Task<TInnerResult> task, Func<TInnerResult, Task<TOuterResult>> continuation, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ return task.ThenImpl(t => continuation(t.Result), cancellationToken);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "The caught exception type is reflected into a faulted task.")]
+ [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")]
+ private static Task<TOuterResult> ThenImpl<TTask, TOuterResult>(this TTask task, Func<TTask, Task<TOuterResult>> continuation, CancellationToken cancellationToken)
+ where TTask : Task
+ {
+ // Stay on the same thread if we can
+ if (task.IsCanceled || cancellationToken.IsCancellationRequested)
+ {
+ return TaskHelpers.Canceled<TOuterResult>();
+ }
+ if (task.IsFaulted)
+ {
+ return TaskHelpers.FromErrors<TOuterResult>(task.Exception.InnerExceptions);
+ }
+ if (task.Status == TaskStatus.RanToCompletion)
+ {
+ try
+ {
+ return continuation(task);
+ }
+ catch (Exception ex)
+ {
+ return TaskHelpers.FromError<TOuterResult>(ex);
+ }
+ }
+
+ SynchronizationContext syncContext = SynchronizationContext.Current;
+
+ return task.ContinueWith(innerTask =>
+ {
+ if (innerTask.IsFaulted)
+ {
+ return TaskHelpers.FromErrors<TOuterResult>(innerTask.Exception.InnerExceptions);
+ }
+ if (innerTask.IsCanceled)
+ {
+ return TaskHelpers.Canceled<TOuterResult>();
+ }
+
+ TaskCompletionSource<Task<TOuterResult>> tcs = new TaskCompletionSource<Task<TOuterResult>>();
+ if (syncContext != null)
+ {
+ syncContext.Post(state =>
+ {
+ try
+ {
+ tcs.TrySetResult(continuation(task));
+ }
+ catch (Exception ex)
+ {
+ tcs.TrySetException(ex);
+ }
+ }, state: null);
+ }
+ else
+ {
+ tcs.TrySetResult(continuation(task));
+ }
+ return tcs.Task.FastUnwrap();
+ }, cancellationToken).FastUnwrap();
+ }
+
+ /// <summary>
+ /// Throws the first faulting exception for a task which is faulted. It attempts to preserve the original
+ /// stack trace when throwing the exception (which should always work in 4.5, and should also work in 4.0
+ /// when running in full trust). Note: It is the caller's responsibility not to pass incomplete tasks to
+ /// this method, because it does degenerate into a call to the equivalent of .Wait() on the task when it
+ /// hasn't yet completed.
+ /// </summary>
+ internal static void ThrowIfFaulted(this Task task)
+ {
+ _rethrowWithNoStackLossDelegate(task);
+ }
+
+ /// <summary>
+ /// Adapts any action into a Task (returning AsyncVoid, so that it's usable with Task{T} extension methods).
+ /// </summary>
+ private static Task<AsyncVoid> ToAsyncVoidTask(Action action)
+ {
+ return TaskHelpers.RunSynchronously<AsyncVoid>(() =>
+ {
+ action();
+ return _defaultCompleted;
+ });
+ }
+
+ /// <summary>
+ /// Changes the return value of a task to the given result, if the task ends in the RanToCompletion state.
+ /// This potentially imposes an extra ContinueWith to convert a non-completed task, so use this with caution.
+ /// </summary>
+ [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")]
+ internal static Task<TResult> ToTask<TResult>(this Task task, CancellationToken cancellationToken = default(CancellationToken), TResult result = default(TResult))
+ {
+ if (task == null)
+ {
+ return null;
+ }
+
+ // Stay on the same thread if we can
+ if (task.IsCanceled || cancellationToken.IsCancellationRequested)
+ {
+ return TaskHelpers.Canceled<TResult>();
+ }
+ if (task.IsFaulted)
+ {
+ return TaskHelpers.FromErrors<TResult>(task.Exception.InnerExceptions);
+ }
+ if (task.Status == TaskStatus.RanToCompletion)
+ {
+ return TaskHelpers.FromResult(result);
+ }
+
+ return task.ContinueWith(innerTask =>
+ {
+ TaskCompletionSource<TResult> tcs = new TaskCompletionSource<TResult>();
+
+ if (task.Status == TaskStatus.RanToCompletion)
+ {
+ tcs.TrySetResult(result);
+ }
+ else
+ {
+ tcs.TrySetFromTask(innerTask);
+ }
+
+ return tcs.Task;
+ }, TaskContinuationOptions.ExecuteSynchronously).FastUnwrap();
+ }
+
+ /// <summary>
+ /// Attempts to get the result value for the given task. If the task ran to completion, then
+ /// it will return true and set the result value; otherwise, it will return false.
+ /// </summary>
+ [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")]
+ internal static bool TryGetResult<TResult>(this Task<TResult> task, out TResult result)
+ {
+ if (task.Status == TaskStatus.RanToCompletion)
+ {
+ result = task.Result;
+ return true;
+ }
+
+ result = default(TResult);
+ return false;
+ }
+
+ /// <summary>
+ /// Used as the T in a "conversion" of a Task into a Task{T}.
+ /// </summary>
+ private struct AsyncVoid
+ {
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Internal/TaskHelpers.cs b/src/System.Net.Http.Formatting/Internal/TaskHelpers.cs
new file mode 100644
index 00000000..ed94de52
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Internal/TaskHelpers.cs
@@ -0,0 +1,384 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace System.Net.Http.Internal
+{
+ // TODO, DevDiv 336175, This is copied from System.Web.Http.Common, remove this copy once the issue is addressed.
+
+ /// <summary>
+ /// Helpers for safely using Task libraries.
+ /// </summary>
+ internal static class TaskHelpers
+ {
+ private static Task _defaultCompleted = FromResult<AsyncVoid>(default(AsyncVoid));
+
+ /// <summary>
+ /// Returns a canceled Task. The task is completed, IsCanceled = True, IsFaulted = False.
+ /// </summary>
+ internal static Task Canceled()
+ {
+ return CancelCache<AsyncVoid>.Canceled;
+ }
+
+ /// <summary>
+ /// Returns a canceled Task of the given type. The task is completed, IsCanceled = True, IsFaulted = False.
+ /// </summary>
+ internal static Task<TResult> Canceled<TResult>()
+ {
+ return CancelCache<TResult>.Canceled;
+ }
+
+ /// <summary>
+ /// Returns a completed task that has no result.
+ /// </summary>
+ internal static Task Completed()
+ {
+ return _defaultCompleted;
+ }
+
+ /// <summary>
+ /// Returns an error task. The task is Completed, IsCanceled = False, IsFaulted = True
+ /// </summary>
+ internal static Task FromError(Exception exception)
+ {
+ return FromError<AsyncVoid>(exception);
+ }
+
+ /// <summary>
+ /// Returns an error task of the given type. The task is Completed, IsCanceled = False, IsFaulted = True
+ /// </summary>
+ /// <typeparam name="TResult"></typeparam>
+ internal static Task<TResult> FromError<TResult>(Exception exception)
+ {
+ TaskCompletionSource<TResult> tcs = new TaskCompletionSource<TResult>();
+ tcs.SetException(exception);
+ return tcs.Task;
+ }
+
+ /// <summary>
+ /// Returns an error task of the given type. The task is Completed, IsCanceled = False, IsFaulted = True
+ /// </summary>
+ internal static Task FromErrors(IEnumerable<Exception> exceptions)
+ {
+ return FromErrors<AsyncVoid>(exceptions);
+ }
+
+ /// <summary>
+ /// Returns an error task of the given type. The task is Completed, IsCanceled = False, IsFaulted = True
+ /// </summary>
+ internal static Task<TResult> FromErrors<TResult>(IEnumerable<Exception> exceptions)
+ {
+ TaskCompletionSource<TResult> tcs = new TaskCompletionSource<TResult>();
+ tcs.SetException(exceptions);
+ return tcs.Task;
+ }
+
+ /// <summary>
+ /// Returns a successful completed task with the given result.
+ /// </summary>
+ internal static Task<TResult> FromResult<TResult>(TResult result)
+ {
+ TaskCompletionSource<TResult> tcs = new TaskCompletionSource<TResult>();
+ tcs.SetResult(result);
+ return tcs.Task;
+ }
+
+ /// <summary>
+ /// Return a task that runs all the tasks inside the iterator sequentially. It stops as soon
+ /// as one of the tasks fails or cancels, or after all the tasks have run succesfully.
+ /// </summary>
+ /// <param name="asyncIterator">collection of tasks to wait on</param>
+ /// <param name="cancellationToken">cancellation token</param>
+ /// <returns>a task that signals completed when all the incoming tasks are finished.</returns>
+ internal static Task Iterate(IEnumerable<Task> asyncIterator, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ return Iterate(WrapIterator(asyncIterator), cancellationToken);
+ }
+
+ /// <summary>
+ /// Return a task that runs all the tasks inside the iterator sequentially and collects the results.
+ /// It stops as soon as one of the tasks fails or cancels, or after all the tasks have run succesfully.
+ /// </summary>
+ /// <param name="asyncIterator">collection of tasks to wait on</param>
+ /// <param name="cancellationToken">cancellation token</param>
+ /// <returns>A task that, upon successful completion, returns the list of results.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "The caught exception type is reflected into a faulted task.")]
+ [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "This usage is known to be safe.")]
+ internal static Task<IEnumerable<TResult>> Iterate<TResult>(IEnumerable<Task<TResult>> asyncIterator, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ Contract.Assert(asyncIterator != null);
+
+ List<TResult> results = new List<TResult>();
+ IEnumerator<Task<TResult>> enumerator = asyncIterator.GetEnumerator();
+ TaskCompletionSource<IEnumerable<TResult>> tcs = new TaskCompletionSource<IEnumerable<TResult>>();
+ Action recursiveBody = null;
+
+ recursiveBody = () =>
+ {
+ try
+ {
+ if (cancellationToken.IsCancellationRequested)
+ {
+ tcs.TrySetCanceled();
+ }
+ else if (enumerator.MoveNext())
+ {
+ enumerator.Current.ContinueWith(previous =>
+ {
+ switch (previous.Status)
+ {
+ case TaskStatus.Faulted:
+ case TaskStatus.Canceled:
+ tcs.TrySetFromTask(previous);
+ break;
+
+ default:
+ results.Add(previous.Result);
+ recursiveBody();
+ break;
+ }
+ });
+ }
+ else
+ {
+ tcs.TrySetResult(results);
+ }
+ }
+ catch (Exception e)
+ {
+ tcs.TrySetException(e);
+ }
+ };
+
+ recursiveBody();
+ return tcs.Task.Finally(enumerator.Dispose);
+ }
+
+ /// <summary>
+ /// Replacement for Task.Factory.StartNew when the code can run synchronously.
+ /// We run the code immediately and avoid the thread switch.
+ /// This is used to help synchronous code implement task interfaces.
+ /// </summary>
+ /// <param name="action">action to run synchronouslyt</param>
+ /// <param name="token">cancellation token. This is only checked before we run the task, and if cancelled, we immediately return a cancelled task.</param>
+ /// <returns>a task who result is the result from Func()</returns>
+ /// <remarks>
+ /// Avoid calling Task.Factory.StartNew.
+ /// This avoids gotchas with StartNew:
+ /// - ensures cancellation token is checked (StartNew doesn't check cancellation tokens).
+ /// - Keeps on the same thread.
+ /// - Avoids switching synchronization contexts.
+ /// Also take in a lambda so that we can wrap in a try catch and honor task failure semantics.
+ /// </remarks>
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "The caught exception type is reflected into a faulted task.")]
+ public static Task RunSynchronously(Action action, CancellationToken token = default(CancellationToken))
+ {
+ if (token.IsCancellationRequested)
+ {
+ return Canceled();
+ }
+
+ try
+ {
+ action();
+ return Completed();
+ }
+ catch (Exception e)
+ {
+ return FromError(e);
+ }
+ }
+
+ /// <summary>
+ /// Replacement for Task.Factory.StartNew when the code can run synchronously.
+ /// We run the code immediately and avoid the thread switch.
+ /// This is used to help synchronous code implement task interfaces.
+ /// </summary>
+ /// <typeparam name="TResult">type of result that task will return.</typeparam>
+ /// <param name="func">function to run synchronously and produce result</param>
+ /// <param name="cancellationToken">cancellation token. This is only checked before we run the task, and if cancelled, we immediately return a cancelled task.</param>
+ /// <returns>a task who result is the result from Func()</returns>
+ /// <remarks>
+ /// Avoid calling Task.Factory.StartNew.
+ /// This avoids gotchas with StartNew:
+ /// - ensures cancellation token is checked (StartNew doesn't check cancellation tokens).
+ /// - Keeps on the same thread.
+ /// - Avoids switching synchronization contexts.
+ /// Also take in a lambda so that we can wrap in a try catch and honor task failure semantics.
+ /// </remarks>
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "The caught exception type is reflected into a faulted task.")]
+ internal static Task<TResult> RunSynchronously<TResult>(Func<TResult> func, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ if (cancellationToken.IsCancellationRequested)
+ {
+ return Canceled<TResult>();
+ }
+
+ try
+ {
+ return FromResult(func());
+ }
+ catch (Exception e)
+ {
+ return FromError<TResult>(e);
+ }
+ }
+
+ /// <summary>
+ /// Overload of RunSynchronously that avoids a call to Unwrap().
+ /// This overload is useful when func() starts doing some synchronous work and then hits IO and
+ /// needs to create a task to finish the work.
+ /// </summary>
+ /// <typeparam name="TResult">type of result that Task will return</typeparam>
+ /// <param name="func">function that returns a task</param>
+ /// <param name="cancellationToken">cancellation token. This is only checked before we run the task, and if cancelled, we immediately return a cancelled task.</param>
+ /// <returns>a task, created by running func().</returns>
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "The caught exception type is reflected into a faulted task.")]
+ internal static Task<TResult> RunSynchronously<TResult>(Func<Task<TResult>> func, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ if (cancellationToken.IsCancellationRequested)
+ {
+ return Canceled<TResult>();
+ }
+
+ try
+ {
+ return func();
+ }
+ catch (Exception e)
+ {
+ return FromError<TResult>(e);
+ }
+ }
+
+ /// <summary>
+ /// Update the completion source if the task failed (cancelled or faulted). No change to completion source if the task succeeded.
+ /// </summary>
+ /// <typeparam name="TResult">result type of completion source</typeparam>
+ /// <param name="tcs">completion source to update</param>
+ /// <param name="source">task to update from.</param>
+ /// <returns>true on success</returns>
+ internal static bool SetIfTaskFailed<TResult>(this TaskCompletionSource<TResult> tcs, Task source)
+ {
+ switch (source.Status)
+ {
+ case TaskStatus.Canceled:
+ case TaskStatus.Faulted:
+ return tcs.TrySetFromTask(source);
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Set a completion source from the given Task.
+ /// </summary>
+ /// <typeparam name="TResult">result type for completion source.</typeparam>
+ /// <param name="tcs">completion source to set</param>
+ /// <param name="source">Task to get values from.</param>
+ /// <returns>true if this successfully sets the completion source.</returns>
+ [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "This is a known safe usage of Task.Result, since it only occurs when we know the task's state to be completed.")]
+ internal static bool TrySetFromTask<TResult>(this TaskCompletionSource<TResult> tcs, Task source)
+ {
+ if (source.Status == TaskStatus.Canceled)
+ {
+ return tcs.TrySetCanceled();
+ }
+
+ if (source.Status == TaskStatus.Faulted)
+ {
+ return tcs.TrySetException(source.Exception.InnerExceptions);
+ }
+
+ if (source.Status == TaskStatus.RanToCompletion)
+ {
+ Task<TResult> taskOfResult = source as Task<TResult>;
+ return tcs.TrySetResult(taskOfResult == null ? default(TResult) : taskOfResult.Result);
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Set a completion source from the given Task. If the task ran to completion and the result type doesn't match
+ /// the type of the completion source, then a default value will be used. This is useful for converting Task into
+ /// Task{AsyncVoid}, but it can also accidentally be used to introduce data loss (by passing the wrong
+ /// task type), so please execute this method with care.
+ /// </summary>
+ /// <typeparam name="TResult">result type for completion source.</typeparam>
+ /// <param name="tcs">completion source to set</param>
+ /// <param name="source">Task to get values from.</param>
+ /// <returns>true if this successfully sets the completion source.</returns>
+ [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "This is a known safe usage of Task.Result, since it only occurs when we know the task's state to be completed.")]
+ internal static bool TrySetFromTask<TResult>(this TaskCompletionSource<Task<TResult>> tcs, Task source)
+ {
+ if (source.Status == TaskStatus.Canceled)
+ {
+ return tcs.TrySetCanceled();
+ }
+
+ if (source.Status == TaskStatus.Faulted)
+ {
+ return tcs.TrySetException(source.Exception.InnerExceptions);
+ }
+
+ if (source.Status == TaskStatus.RanToCompletion)
+ {
+ // Sometimes the source task is Task<Task<TResult>>, and sometimes it's Task<TResult>.
+ // The latter usually happens when we're in the middle of a sync-block postback where
+ // the continuation is a function which returns Task<TResult> rather than just TResult,
+ // but the originating task was itself just Task<TResult>. An example of this can be
+ // found in TaskExtensions.CatchImpl().
+ Task<Task<TResult>> taskOfTaskOfResult = source as Task<Task<TResult>>;
+ if (taskOfTaskOfResult != null)
+ {
+ return tcs.TrySetResult(taskOfTaskOfResult.Result);
+ }
+
+ Task<TResult> taskOfResult = source as Task<TResult>;
+ if (taskOfResult != null)
+ {
+ return tcs.TrySetResult(taskOfResult);
+ }
+
+ return tcs.TrySetResult(TaskHelpers.FromResult(default(TResult)));
+ }
+
+ return false;
+ }
+
+ private static IEnumerable<Task<AsyncVoid>> WrapIterator(IEnumerable<Task> tasks)
+ {
+ foreach (Task task in tasks)
+ {
+ yield return task.ToTask<AsyncVoid>();
+ }
+ }
+
+ /// <summary>
+ /// Used as the T in a "conversion" of a Task into a Task{T}
+ /// </summary>
+ private struct AsyncVoid
+ {
+ }
+
+ /// <summary>
+ /// This class is a convenient cache for per-type cancelled tasks
+ /// </summary>
+ /// <typeparam name="TResult"></typeparam>
+ private static class CancelCache<TResult>
+ {
+ public static readonly Task<TResult> Canceled = GetCancelledTask();
+
+ private static Task<TResult> GetCancelledTask()
+ {
+ TaskCompletionSource<TResult> tcs = new TaskCompletionSource<TResult>();
+ tcs.SetCanceled();
+ return tcs.Task;
+ }
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Internal/ThresholdStream.cs b/src/System.Net.Http.Formatting/Internal/ThresholdStream.cs
new file mode 100644
index 00000000..a3cac7c2
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Internal/ThresholdStream.cs
@@ -0,0 +1,87 @@
+using System.IO;
+
+namespace System.Net.Http.Internal
+{
+ /// <summary>
+ /// Wraps a stream and limit the number of certain bytes that pass through.
+ /// This is intended to mitigate against DOS attacks where input is constructed to blow up hashtables.
+ /// We want a cheap way to limit the number of keys in the object that will be deserialized from the stream.
+ /// We don't want to maintain a parser's state machine, so we need rough approximation. Naively, we could limit the total number of
+ /// bytes in the stream, but that's too harsh (it would prevent too many safe cases).
+ /// Instead, we count number of delimiter characters (where delimiter is set by the caller. Eg, a ',' for JSON).
+ /// </summary>
+ internal class ThresholdStream : DelegatingStream
+ {
+ public const int DefaultDelimeterThreshold = 1000;
+
+ private readonly byte _delimiter;
+ private readonly int _threshold;
+ private int _counter;
+
+ public ThresholdStream(Stream inner, byte delimeter, int threshold = DefaultDelimeterThreshold)
+ : base(inner)
+ {
+ _delimiter = delimeter;
+ _threshold = threshold;
+ }
+
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ var countRead = base.Read(buffer, offset, count);
+ CheckBytes(buffer, offset, countRead);
+ return countRead;
+ }
+
+ public override int ReadByte()
+ {
+ int value = base.ReadByte();
+ CheckByte((byte)value);
+ return value;
+ }
+
+ private void CheckByte(byte b)
+ {
+ if (b == _delimiter)
+ {
+ _counter++;
+ if (_counter > _threshold)
+ {
+ throw new InvalidOperationException(Properties.Resources.InputStreamHasTooManyDelimiters);
+ }
+ }
+ }
+
+ private void CheckBytes(byte[] buffer, int offset, int count)
+ {
+ for (int i = offset; i < offset + count; i++)
+ {
+ CheckByte(buffer[i]);
+ }
+ }
+
+ public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
+ {
+ IAsyncResult inner = base.BeginRead(buffer, offset, count, callback, state);
+
+ Action<int> verifyAction = (countRead) => CheckBytes(buffer, offset, countRead);
+ return new AsyncResultWithExtraData<Action<int>>(inner, verifyAction);
+ }
+
+ // EndRead must be called for every BeginRead.
+ public override int EndRead(IAsyncResult asyncResult)
+ {
+ if (asyncResult == null)
+ {
+ throw new ArgumentNullException("asyncResult");
+ }
+
+ AsyncResultWithExtraData<Action<int>> result = (AsyncResultWithExtraData<Action<int>>)asyncResult;
+ int countRead = base.EndRead(result.Inner);
+
+ Action<int> verifyAction = result.ExtraData;
+ verifyAction(countRead);
+
+ return countRead;
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Internal/UriQueryUtility.cs b/src/System.Net.Http.Formatting/Internal/UriQueryUtility.cs
new file mode 100644
index 00000000..554075fc
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Internal/UriQueryUtility.cs
@@ -0,0 +1,541 @@
+using System.Collections;
+using System.Collections.Specialized;
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+using System.Runtime.Serialization;
+using System.Text;
+
+namespace System.Net.Http.Internal
+{
+ /// <summary>
+ /// Helpers for encoding, decoding, and parsing URI query components.
+ /// </summary>
+ internal static class UriQueryUtility
+ {
+ public static NameValueCollection ParseQueryString(string query)
+ {
+ if (query == null)
+ {
+ throw new ArgumentNullException("query");
+ }
+
+ if (query.Length > 0 && query[0] == '?')
+ {
+ query = query.Substring(1);
+ }
+
+ return new HttpValueCollection(query);
+ }
+
+ [Serializable]
+ internal class HttpValueCollection : NameValueCollection
+ {
+ [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "Ported from WCF")]
+ internal HttpValueCollection(string str)
+ : base(StringComparer.OrdinalIgnoreCase)
+ {
+ if (!String.IsNullOrEmpty(str))
+ {
+ FillFromString(str, true);
+ }
+
+ IsReadOnly = false;
+ }
+
+ protected HttpValueCollection(SerializationInfo info, StreamingContext context)
+ : base(info, context)
+ {
+ }
+
+ public override string ToString()
+ {
+ return ToString(true, null);
+ }
+
+ internal void FillFromString(string s, bool urlencoded)
+ {
+ int l = (s != null) ? s.Length : 0;
+ int i = 0;
+
+ while (i < l)
+ {
+ // find next & while noting first = on the way (and if there are more)
+ int si = i;
+ int ti = -1;
+
+ while (i < l)
+ {
+ char ch = s[i];
+
+ if (ch == '=')
+ {
+ if (ti < 0)
+ {
+ ti = i;
+ }
+ }
+ else if (ch == '&')
+ {
+ break;
+ }
+
+ i++;
+ }
+
+ // extract the name / value pair
+ string name = String.Empty;
+ string value = String.Empty;
+
+ if (ti >= 0)
+ {
+ name = s.Substring(si, ti - si);
+ value = s.Substring(ti + 1, i - ti - 1);
+ }
+ else
+ {
+ value = s.Substring(si, i - si);
+ }
+
+ // add name / value pair to the collection
+ if (urlencoded)
+ {
+ Add(UriQueryUtility.UrlDecode(name), UriQueryUtility.UrlDecode(value));
+ }
+ else
+ {
+ Add(name, value);
+ }
+
+ // trailing '&'
+ if (i == l - 1 && s[i] == '&')
+ {
+ Add(String.Empty, String.Empty);
+ }
+
+ i++;
+ }
+ }
+
+ string ToString(bool urlencoded, IDictionary excludeKeys)
+ {
+ int n = Count;
+ if (n == 0)
+ {
+ return String.Empty;
+ }
+
+ StringBuilder s = new StringBuilder();
+ string key, keyPrefix, item;
+
+ for (int i = 0; i < n; i++)
+ {
+ key = GetKey(i);
+
+ if (excludeKeys != null && key != null && excludeKeys[key] != null)
+ {
+ continue;
+ }
+
+ if (urlencoded)
+ {
+ key = UriQueryUtility.UrlEncode(key);
+ }
+
+ keyPrefix = (!String.IsNullOrEmpty(key)) ? (key + "=") : String.Empty;
+
+ ArrayList values = (ArrayList)BaseGet(i);
+ int numValues = (values != null) ? values.Count : 0;
+
+ if (s.Length > 0)
+ {
+ s.Append('&');
+ }
+
+ if (numValues == 1)
+ {
+ s.Append(keyPrefix);
+ item = (string)values[0];
+ if (urlencoded)
+ {
+ item = UriQueryUtility.UrlEncode(item);
+ }
+
+ s.Append(item);
+ }
+ else if (numValues == 0)
+ {
+ s.Append(keyPrefix);
+ }
+ else
+ {
+ for (int j = 0; j < numValues; j++)
+ {
+ if (j > 0)
+ {
+ s.Append('&');
+ }
+
+ s.Append(keyPrefix);
+ item = (string)values[j];
+ if (urlencoded)
+ {
+ item = UriQueryUtility.UrlEncode(item);
+ }
+
+ s.Append(item);
+ }
+ }
+ }
+
+ return s.ToString();
+ }
+ }
+
+ // The implementation below is ported from WebUtility for use in .Net 4
+
+ #region UrlEncode implementation
+
+ private static byte[] UrlEncode(byte[] bytes, int offset, int count, bool alwaysCreateNewReturnValue)
+ {
+ byte[] encoded = UrlEncode(bytes, offset, count);
+
+ return (alwaysCreateNewReturnValue && (encoded != null) && (encoded == bytes))
+ ? (byte[])encoded.Clone()
+ : encoded;
+ }
+
+ private static byte[] UrlEncode(byte[] bytes, int offset, int count)
+ {
+ if (!ValidateUrlEncodingParameters(bytes, offset, count))
+ {
+ return null;
+ }
+
+ int cSpaces = 0;
+ int cUnsafe = 0;
+
+ // count them first
+ for (int i = 0; i < count; i++)
+ {
+ char ch = (char)bytes[offset + i];
+
+ if (ch == ' ')
+ cSpaces++;
+ else if (!IsUrlSafeChar(ch))
+ cUnsafe++;
+ }
+
+ // nothing to expand?
+ if (cSpaces == 0 && cUnsafe == 0)
+ return bytes;
+
+ // expand not 'safe' characters into %XX, spaces to +s
+ byte[] expandedBytes = new byte[count + cUnsafe * 2];
+ int pos = 0;
+
+ for (int i = 0; i < count; i++)
+ {
+ byte b = bytes[offset + i];
+ char ch = (char)b;
+
+ if (IsUrlSafeChar(ch))
+ {
+ expandedBytes[pos++] = b;
+ }
+ else if (ch == ' ')
+ {
+ expandedBytes[pos++] = (byte)'+';
+ }
+ else
+ {
+ expandedBytes[pos++] = (byte)'%';
+ expandedBytes[pos++] = (byte)IntToHex((b >> 4) & 0xf);
+ expandedBytes[pos++] = (byte)IntToHex(b & 0x0f);
+ }
+ }
+
+ return expandedBytes;
+ }
+
+ #endregion
+
+ #region UrlEncode public methods
+
+ public static string UrlEncode(string str)
+ {
+ if (str == null)
+ return null;
+
+ byte[] bytes = Encoding.UTF8.GetBytes(str);
+ return Encoding.ASCII.GetString(UrlEncode(bytes, 0, bytes.Length, false /* alwaysCreateNewReturnValue */));
+ }
+
+ public static byte[] UrlEncodeToBytes(byte[] bytes, int offset, int count)
+ {
+ return UrlEncode(bytes, offset, count, true /* alwaysCreateNewReturnValue */);
+ }
+
+ #endregion
+
+ #region UrlDecode implementation
+
+ private static string UrlDecodeInternal(string value, Encoding encoding)
+ {
+ if (value == null)
+ {
+ return null;
+ }
+
+ int count = value.Length;
+ UrlDecoder helper = new UrlDecoder(count, encoding);
+
+ // go through the string's chars collapsing %XX and %uXXXX and
+ // appending each char as char, with exception of %XX constructs
+ // that are appended as bytes
+
+ for (int pos = 0; pos < count; pos++)
+ {
+ char ch = value[pos];
+
+ if (ch == '+')
+ {
+ ch = ' ';
+ }
+ else if (ch == '%' && pos < count - 2)
+ {
+ if (value[pos + 1] == 'u' && pos < count - 5)
+ {
+ int h1 = HexToInt(value[pos + 2]);
+ int h2 = HexToInt(value[pos + 3]);
+ int h3 = HexToInt(value[pos + 4]);
+ int h4 = HexToInt(value[pos + 5]);
+
+ if (h1 >= 0 && h2 >= 0 && h3 >= 0 && h4 >= 0)
+ { // valid 4 hex chars
+ ch = (char)((h1 << 12) | (h2 << 8) | (h3 << 4) | h4);
+ pos += 5;
+
+ // only add as char
+ helper.AddChar(ch);
+ continue;
+ }
+ }
+ else
+ {
+ int h1 = HexToInt(value[pos + 1]);
+ int h2 = HexToInt(value[pos + 2]);
+
+ if (h1 >= 0 && h2 >= 0)
+ { // valid 2 hex chars
+ byte b = (byte)((h1 << 4) | h2);
+ pos += 2;
+
+ // don't add as char
+ helper.AddByte(b);
+ continue;
+ }
+ }
+ }
+
+ if ((ch & 0xFF80) == 0)
+ helper.AddByte((byte)ch); // 7 bit have to go as bytes because of Unicode
+ else
+ helper.AddChar(ch);
+ }
+
+ return helper.GetString();
+ }
+
+ private static byte[] UrlDecodeInternal(byte[] bytes, int offset, int count)
+ {
+ if (!ValidateUrlEncodingParameters(bytes, offset, count))
+ {
+ return null;
+ }
+
+ int decodedBytesCount = 0;
+ byte[] decodedBytes = new byte[count];
+
+ for (int i = 0; i < count; i++)
+ {
+ int pos = offset + i;
+ byte b = bytes[pos];
+
+ if (b == '+')
+ {
+ b = (byte)' ';
+ }
+ else if (b == '%' && i < count - 2)
+ {
+ int h1 = HexToInt((char)bytes[pos + 1]);
+ int h2 = HexToInt((char)bytes[pos + 2]);
+
+ if (h1 >= 0 && h2 >= 0)
+ { // valid 2 hex chars
+ b = (byte)((h1 << 4) | h2);
+ i += 2;
+ }
+ }
+
+ decodedBytes[decodedBytesCount++] = b;
+ }
+
+ if (decodedBytesCount < decodedBytes.Length)
+ {
+ byte[] newDecodedBytes = new byte[decodedBytesCount];
+ Array.Copy(decodedBytes, newDecodedBytes, decodedBytesCount);
+ decodedBytes = newDecodedBytes;
+ }
+
+ return decodedBytes;
+ }
+
+ #endregion
+
+ #region UrlDecode public methods
+
+ public static string UrlDecode(string str)
+ {
+ if (str == null)
+ return null;
+
+ return UrlDecodeInternal(str, Encoding.UTF8);
+ }
+
+ public static byte[] UrlDecodeToBytes(byte[] bytes, int offset, int count)
+ {
+ return UrlDecodeInternal(bytes, offset, count);
+ }
+
+ #endregion
+
+ #region Helper methods
+
+ private static int HexToInt(char h)
+ {
+ return (h >= '0' && h <= '9') ? h - '0' :
+ (h >= 'a' && h <= 'f') ? h - 'a' + 10 :
+ (h >= 'A' && h <= 'F') ? h - 'A' + 10 :
+ -1;
+ }
+
+ private static char IntToHex(int n)
+ {
+ Contract.Assert(n < 0x10);
+
+ if (n <= 9)
+ return (char)(n + (int)'0');
+ else
+ return (char)(n - 10 + (int)'a');
+ }
+
+ // Set of safe chars, from RFC 1738.4 minus '+'
+ private static bool IsUrlSafeChar(char ch)
+ {
+ if (ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z' || ch >= '0' && ch <= '9')
+ return true;
+
+ switch (ch)
+ {
+ case '-':
+ case '_':
+ case '.':
+ case '!':
+ case '*':
+ case '(':
+ case ')':
+ return true;
+ }
+
+ return false;
+ }
+
+ private static bool ValidateUrlEncodingParameters(byte[] bytes, int offset, int count)
+ {
+ if (bytes == null && count == 0)
+ return false;
+ if (bytes == null)
+ {
+ throw new ArgumentNullException("bytes");
+ }
+ if (offset < 0 || offset > bytes.Length)
+ {
+ throw new ArgumentOutOfRangeException("offset");
+ }
+ if (count < 0 || offset + count > bytes.Length)
+ {
+ throw new ArgumentOutOfRangeException("count");
+ }
+
+ return true;
+ }
+
+ #endregion
+
+ #region UrlDecoder nested class
+
+ // Internal class to facilitate URL decoding -- keeps char buffer and byte buffer, allows appending of either chars or bytes
+ private class UrlDecoder
+ {
+ private int _bufferSize;
+
+ // Accumulate characters in a special array
+ private int _numChars;
+ private char[] _charBuffer;
+
+ // Accumulate bytes for decoding into characters in a special array
+ private int _numBytes;
+ private byte[] _byteBuffer;
+
+ // Encoding to convert chars to bytes
+ private Encoding _encoding;
+
+ private void FlushBytes()
+ {
+ if (_numBytes > 0)
+ {
+ _numChars += _encoding.GetChars(_byteBuffer, 0, _numBytes, _charBuffer, _numChars);
+ _numBytes = 0;
+ }
+ }
+
+ internal UrlDecoder(int bufferSize, Encoding encoding)
+ {
+ _bufferSize = bufferSize;
+ _encoding = encoding;
+
+ _charBuffer = new char[bufferSize];
+ // byte buffer created on demand
+ }
+
+ internal void AddChar(char ch)
+ {
+ if (_numBytes > 0)
+ FlushBytes();
+
+ _charBuffer[_numChars++] = ch;
+ }
+
+ internal void AddByte(byte b)
+ {
+ if (_byteBuffer == null)
+ _byteBuffer = new byte[_bufferSize];
+
+ _byteBuffer[_numBytes++] = b;
+ }
+
+ internal String GetString()
+ {
+ if (_numBytes > 0)
+ FlushBytes();
+
+ if (_numChars > 0)
+ return new String(_charBuffer, 0, _numChars);
+ else
+ return String.Empty;
+ }
+ }
+
+ #endregion
+ }
+} \ No newline at end of file
diff --git a/src/System.Net.Http.Formatting/MimeBodyPart.cs b/src/System.Net.Http.Formatting/MimeBodyPart.cs
new file mode 100644
index 00000000..28b62c2d
--- /dev/null
+++ b/src/System.Net.Http.Formatting/MimeBodyPart.cs
@@ -0,0 +1,162 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics.Contracts;
+using System.IO;
+using System.Net.Http.Formatting.Parsers;
+using System.Net.Http.Headers;
+
+namespace System.Net.Http
+{
+ /// <summary>
+ /// Maintains information about MIME body parts parsed by <see cref="MimeMultipartBodyPartParser"/>.
+ /// </summary>
+ internal class MimeBodyPart : IDisposable
+ {
+ private static readonly Type _streamType = typeof(Stream);
+ private Stream _outputStream;
+ private IMultipartStreamProvider _streamProvider;
+ private HttpContentHeaders _headers;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MimeBodyPart"/> class.
+ /// </summary>
+ /// <param name="streamProvider">The stream provider.</param>
+ /// <param name="maxBodyPartHeaderSize">The max length of the MIME header within each MIME body part.</param>
+ public MimeBodyPart(IMultipartStreamProvider streamProvider, int maxBodyPartHeaderSize)
+ {
+ Contract.Assert(streamProvider != null, "Stream provider cannot be null.");
+ _streamProvider = streamProvider;
+ Segments = new ArrayList(2);
+ _headers = FormattingUtilities.CreateEmptyContentHeaders();
+ HeaderParser = new InternetMessageFormatHeaderParser(_headers, maxBodyPartHeaderSize);
+ }
+
+ /// <summary>
+ /// Gets the header parser.
+ /// </summary>
+ /// <value>
+ /// The header parser.
+ /// </value>
+ public InternetMessageFormatHeaderParser HeaderParser { get; private set; }
+
+ /// <summary>
+ /// Gets the content of the HTTP.
+ /// </summary>
+ /// <value>
+ /// The content of the HTTP.
+ /// </value>
+ public HttpContent HttpContent { get; private set; }
+
+ /// <summary>
+ /// Gets the set of <see cref="ArraySegment{T}"/> pointing to the read buffer with
+ /// contents of this body part.
+ /// </summary>
+ /// <remarks>We deliberately use <see cref="ArrayList"/> as List{ArraySegment{byte}} and Collection{ArraySegment{byte}} trip FxCop rule CA908
+ /// which states the following: "Assemblies that are precompiled (using ngen.exe) should only instantiate generic types that will not cause JIT
+ /// compilation at runtime. Generic types with value type type parameters (outside of a special set of supported runtime generic types) will
+ /// always cause JIT compilation, even if the encapsulating assembly has been precompiled". As we don't want to force JIT'ing we use the old
+ /// non-generic form which doesn't have this problem (ArraySegment{byte} of course is a value type).</remarks>
+ public ArrayList Segments { get; private set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the body part has been completed.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if this instance is complete; otherwise, <c>false</c>.
+ /// </value>
+ public bool IsComplete { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this is the final body part.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if this instance is complete; otherwise, <c>false</c>.
+ /// </value>
+ public bool IsFinal { get; set; }
+
+ /// <summary>
+ /// Gets the output stream.
+ /// </summary>
+ /// <returns>The output stream to write the body part to.</returns>
+ public Stream GetOutputStream()
+ {
+ if (_outputStream == null)
+ {
+ try
+ {
+ _outputStream = _streamProvider.GetStream(_headers);
+ }
+ catch (Exception e)
+ {
+ throw new InvalidOperationException(RS.Format(Properties.Resources.ReadAsMimeMultipartStreamProviderException, _streamProvider.GetType().Name), e);
+ }
+
+ if (_outputStream == null)
+ {
+ throw new InvalidOperationException(
+ RS.Format(Properties.Resources.ReadAsMimeMultipartStreamProviderNull, _streamProvider.GetType().Name, _streamType.Name));
+ }
+
+ if (!_outputStream.CanWrite)
+ {
+ throw new InvalidOperationException(
+ RS.Format(Properties.Resources.ReadAsMimeMultipartStreamProviderReadOnly, _streamProvider.GetType().Name, _streamType.Name));
+ }
+
+ HttpContent = new StreamContent(_outputStream);
+ foreach (KeyValuePair<string, IEnumerable<string>> header in _headers)
+ {
+ HttpContent.Headers.TryAddWithoutValidation(header.Key, header.Value);
+ }
+ }
+
+ return _outputStream;
+ }
+
+ /// <summary>
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources
+ /// </summary>
+ /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ protected void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ CleanupOutputStream();
+ HttpContent = null;
+ HeaderParser = null;
+ Segments.Clear();
+ }
+ }
+
+ /// <summary>
+ /// Resets the output stream by either closing it or, in the case of a <see cref="MemoryStream"/> resetting
+ /// position to 0 so that it can be read by the caller.
+ /// </summary>
+ private void CleanupOutputStream()
+ {
+ if (_outputStream != null)
+ {
+ MemoryStream output = _outputStream as MemoryStream;
+ if (output != null)
+ {
+ output.Position = 0;
+ }
+ else
+ {
+ _outputStream.Close();
+ }
+
+ _outputStream = null;
+ }
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/MultipartFileStreamProvider.cs b/src/System.Net.Http.Formatting/MultipartFileStreamProvider.cs
new file mode 100644
index 00000000..ec8d52b9
--- /dev/null
+++ b/src/System.Net.Http.Formatting/MultipartFileStreamProvider.cs
@@ -0,0 +1,167 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.IO;
+using System.Net.Http.Headers;
+
+namespace System.Net.Http
+{
+ /// <summary>
+ /// An <see cref="IMultipartStreamProvider"/> suited for writing each MIME body parts of the MIME multipart
+ /// message to a file using a <see cref="FileStream"/>.
+ /// </summary>
+ public class MultipartFileStreamProvider : IMultipartStreamProvider
+ {
+ private const int DefaultBufferSize = 0x1000;
+
+ private List<string> _bodyPartFileNames = new List<string>();
+ private readonly object _thisLock = new object();
+ private string _rootPath;
+ private int _bufferSize = DefaultBufferSize;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MultipartFileStreamProvider"/> class.
+ /// </summary>
+ public MultipartFileStreamProvider()
+ : this(Path.GetTempPath(), DefaultBufferSize)
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MultipartFileStreamProvider"/> class.
+ /// </summary>
+ /// <param name="rootPath">The root path where the content of MIME multipart body parts are written to.</param>
+ public MultipartFileStreamProvider(string rootPath)
+ : this(rootPath, DefaultBufferSize)
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MultipartFileStreamProvider"/> class.
+ /// </summary>
+ /// <param name="rootPath">The root path where the content of MIME multipart body parts are written to.</param>
+ /// <param name="bufferSize">The number of bytes buffered for writes to the file.</param>
+ public MultipartFileStreamProvider(string rootPath, int bufferSize)
+ {
+ if (String.IsNullOrWhiteSpace(rootPath))
+ {
+ throw new ArgumentNullException("rootPath");
+ }
+
+ if (bufferSize <= 0)
+ {
+ throw new ArgumentOutOfRangeException("bufferSize", bufferSize, Properties.Resources.NonZeroParameterSize);
+ }
+
+ _rootPath = Path.GetFullPath(rootPath);
+ }
+
+ /// <summary>
+ /// Gets an <see cref="IEnumerable{T}"/> containing the files names of MIME
+ /// body part written to file.
+ /// </summary>
+ public IEnumerable<string> BodyPartFileNames
+ {
+ get
+ {
+ lock (_thisLock)
+ {
+ return _bodyPartFileNames != null
+ ? new List<string>(_bodyPartFileNames)
+ : new List<string>();
+ }
+ }
+ }
+
+ /// <summary>
+ /// This body part stream provider examines the headers provided by the MIME multipart parser
+ /// and decides which <see cref="FileStream"/> to write the body part to.
+ /// </summary>
+ /// <param name="headers">Header fields describing the body part</param>
+ /// <returns>The <see cref="Stream"/> instance where the message body part is written to.</returns>
+ public Stream GetStream(HttpContentHeaders headers)
+ {
+ if (headers == null)
+ {
+ throw new ArgumentNullException("headers");
+ }
+
+ return OnGetStream(headers);
+ }
+
+ /// <summary>
+ /// Override this method in a derived class to examine the headers provided by the MIME multipart parser
+ /// and decides which <see cref="FileStream"/> to write the body part to.
+ /// </summary>
+ /// <param name="headers">Header fields describing the body part</param>
+ /// <returns>The <see cref="Stream"/> instance where the message body part is written to.</returns>
+ protected virtual Stream OnGetStream(HttpContentHeaders headers)
+ {
+ if (headers == null)
+ {
+ throw new ArgumentNullException("headers");
+ }
+
+ string localFilePath;
+ try
+ {
+ string filename = GetLocalFileName(headers);
+ localFilePath = Path.Combine(_rootPath, Path.GetFileName(filename));
+ }
+ catch (Exception e)
+ {
+ throw new InvalidOperationException(Properties.Resources.MultipartStreamProviderInvalidLocalFileName, e);
+ }
+
+ if (!Directory.Exists(_rootPath))
+ {
+ Directory.CreateDirectory(_rootPath);
+ }
+
+ // Add local file name
+ lock (_thisLock)
+ {
+ _bodyPartFileNames.Add(localFilePath);
+ }
+
+ return File.Create(localFilePath, _bufferSize, FileOptions.Asynchronous);
+ }
+
+ /// <summary>
+ /// Gets the name of the local file which will be combined with the root path to
+ /// create an absolute file name where the contents of the current MIME body part
+ /// will be stored.
+ /// </summary>
+ /// <param name="headers">The headers for the current MIME body part.</param>
+ /// <returns>A relative filename with no path component.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is propagated.")]
+ protected virtual string GetLocalFileName(HttpContentHeaders headers)
+ {
+ if (headers == null)
+ {
+ throw new ArgumentNullException("headers");
+ }
+
+ string filename = null;
+ try
+ {
+ ContentDispositionHeaderValue contentDisposition = headers.ContentDisposition;
+ if (contentDisposition != null)
+ {
+ filename = contentDisposition.ExtractLocalFileName();
+ }
+ }
+ catch (Exception)
+ {
+ //// TODO: CSDMain 232171 -- review and fix swallowed exception
+ }
+
+ if (filename == null)
+ {
+ filename = String.Format(CultureInfo.InvariantCulture, "BodyPart_{0}", Guid.NewGuid());
+ }
+
+ return filename;
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/MultipartFormDataStreamProvider.cs b/src/System.Net.Http.Formatting/MultipartFormDataStreamProvider.cs
new file mode 100644
index 00000000..adb0b93f
--- /dev/null
+++ b/src/System.Net.Http.Formatting/MultipartFormDataStreamProvider.cs
@@ -0,0 +1,190 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.IO;
+using System.Net.Http.Headers;
+
+namespace System.Net.Http
+{
+ /// <summary>
+ /// An <see cref="IMultipartStreamProvider"/> suited for use with HTML file uploads for writing file
+ /// content to a <see cref="FileStream"/>. The stream provider looks at the <b>Content-Disposition</b> header
+ /// field and determines an output <see cref="Stream"/> based on the presence of a <b>filename</b> parameter.
+ /// If a <b>filename</b> parameter is present in the <b>Content-Disposition</b> header field then the body
+ /// part is written to a <see cref="FileStream"/>, otherwise it is written to a <see cref="MemoryStream"/>.
+ /// This makes it convenient to process MIME Multipart HTML Form data which is a combination of form
+ /// data and file content.
+ /// </summary>
+ public class MultipartFormDataStreamProvider : IMultipartStreamProvider
+ {
+ private const int DefaultBufferSize = 0x1000;
+
+ private Dictionary<string, string> _bodyPartFileNames = new Dictionary<string, string>();
+ private readonly object _thisLock = new object();
+ private string _rootPath;
+ private int _bufferSize = DefaultBufferSize;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MultipartFormDataStreamProvider"/> class.
+ /// </summary>
+ public MultipartFormDataStreamProvider()
+ : this(Path.GetTempPath(), DefaultBufferSize)
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MultipartFormDataStreamProvider"/> class.
+ /// </summary>
+ /// <param name="rootPath">The root path where the content of MIME multipart body parts are written to.</param>
+ public MultipartFormDataStreamProvider(string rootPath)
+ : this(rootPath, DefaultBufferSize)
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MultipartFormDataStreamProvider"/> class.
+ /// </summary>
+ /// <param name="rootPath">The root path where the content of MIME multipart body parts are written to.</param>
+ /// <param name="bufferSize">The number of bytes buffered for writes to the file.</param>
+ public MultipartFormDataStreamProvider(string rootPath, int bufferSize)
+ {
+ if (String.IsNullOrWhiteSpace(rootPath))
+ {
+ throw new ArgumentNullException("rootPath");
+ }
+
+ if (bufferSize <= 0)
+ {
+ throw new ArgumentOutOfRangeException("bufferSize", bufferSize, Properties.Resources.NonZeroParameterSize);
+ }
+
+ _rootPath = Path.GetFullPath(rootPath);
+ }
+
+ /// <summary>
+ /// Gets an <see cref="IDictionary{T1, T2}"/> instance containing mappings of each
+ /// <b>filename</b> parameter provided in a <b>Content-Disposition</b> header field
+ /// (represented as the keys) to a local file name where the contents of the body part is
+ /// stored (represented as the values).
+ /// </summary>
+ public IDictionary<string, string> BodyPartFileNames
+ {
+ get
+ {
+ lock (_thisLock)
+ {
+ return _bodyPartFileNames != null
+ ? new Dictionary<string, string>(_bodyPartFileNames)
+ : new Dictionary<string, string>();
+ }
+ }
+ }
+
+ /// <summary>
+ /// This body part stream provider examines the headers provided by the MIME multipart parser
+ /// and decides whether it should return a file stream or a memory stream for the body part to be
+ /// written to.
+ /// </summary>
+ /// <param name="headers">Header fields describing the body part</param>
+ /// <returns>The <see cref="Stream"/> instance where the message body part is written to.</returns>
+ public Stream GetStream(HttpContentHeaders headers)
+ {
+ if (headers == null)
+ {
+ throw new ArgumentNullException("headers");
+ }
+
+ return this.OnGetStream(headers);
+ }
+
+ /// <summary>
+ /// Override this method in a derived class to examine the headers provided by the MIME multipart parser
+ /// and decide whether it should return a file stream or a memory stream for the body part to be
+ /// written to.
+ /// </summary>
+ /// <param name="headers">Header fields describing the body part</param>
+ /// <returns>The <see cref="Stream"/> instance where the message body part is written to.</returns>
+ protected virtual Stream OnGetStream(HttpContentHeaders headers)
+ {
+ if (headers == null)
+ {
+ throw new ArgumentNullException("headers");
+ }
+
+ ContentDispositionHeaderValue contentDisposition = headers.ContentDisposition;
+ if (contentDisposition != null)
+ {
+ // If we have a file name then write contents out to temporary file. Otherwise just write to MemoryStream
+ if (!String.IsNullOrEmpty(contentDisposition.FileName))
+ {
+ string localFilePath;
+ try
+ {
+ string filename = this.GetLocalFileName(headers);
+ localFilePath = Path.Combine(_rootPath, Path.GetFileName(filename));
+ }
+ catch (Exception e)
+ {
+ throw new InvalidOperationException(Properties.Resources.MultipartStreamProviderInvalidLocalFileName, e);
+ }
+
+ if (!Directory.Exists(_rootPath))
+ {
+ Directory.CreateDirectory(_rootPath);
+ }
+
+ // Add mapping from Content-Disposition FileName parameter to local file name.
+ lock (_thisLock)
+ {
+ _bodyPartFileNames.Add(contentDisposition.FileName, localFilePath);
+ }
+
+ return File.Create(localFilePath, _bufferSize, FileOptions.Asynchronous);
+ }
+
+ // If no filename parameter was found in the Content-Disposition header then return a memory stream.
+ return new MemoryStream();
+ }
+
+ // If no Content-Disposition header was present.
+ throw new IOException(RS.Format(Properties.Resources.MultipartFormDataStreamProviderNoContentDisposition, "Content-Disposition"));
+ }
+
+ /// <summary>
+ /// Gets the name of the local file which will be combined with the root path to
+ /// create an absolute file name where the contents of the current MIME body part
+ /// will be stored.
+ /// </summary>
+ /// <param name="headers">The headers for the current MIME body part.</param>
+ /// <returns>A relative filename with no path component.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is propagated.")]
+ protected virtual string GetLocalFileName(HttpContentHeaders headers)
+ {
+ if (headers == null)
+ {
+ throw new ArgumentNullException("headers");
+ }
+
+ string filename = null;
+ try
+ {
+ ContentDispositionHeaderValue contentDisposition = headers.ContentDisposition;
+ if (contentDisposition != null)
+ {
+ filename = contentDisposition.ExtractLocalFileName();
+ }
+ }
+ catch (Exception)
+ {
+ //// TODO: CSDMain 232171 -- review and fix swallowed exception
+ }
+
+ if (filename == null)
+ {
+ filename = String.Format(CultureInfo.InvariantCulture, "BodyPart_{0}", Guid.NewGuid());
+ }
+
+ return filename;
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/MultipartMemoryStreamProvider.cs b/src/System.Net.Http.Formatting/MultipartMemoryStreamProvider.cs
new file mode 100644
index 00000000..7fbb5ccf
--- /dev/null
+++ b/src/System.Net.Http.Formatting/MultipartMemoryStreamProvider.cs
@@ -0,0 +1,46 @@
+using System.IO;
+using System.Net.Http.Headers;
+
+namespace System.Net.Http
+{
+ /// <summary>
+ /// Provides a <see cref="IMultipartStreamProvider"/> implementation that returns a <see cref="MemoryStream"/> instance.
+ /// This facilitates deserialization or other manipulation of the contents in memory.
+ /// </summary>
+ internal class MultipartMemoryStreamProvider : IMultipartStreamProvider
+ {
+ private static MultipartMemoryStreamProvider instance = new MultipartMemoryStreamProvider();
+
+ private MultipartMemoryStreamProvider()
+ {
+ }
+
+ /// <summary>
+ /// Gets a static instance of the <see cref="MultipartMemoryStreamProvider"/>
+ /// </summary>
+ public static MultipartMemoryStreamProvider Instance
+ {
+ get { return MultipartMemoryStreamProvider.instance; }
+ }
+
+ /// <summary>
+ /// This <see cref="IMultipartStreamProvider"/> implementation returns a <see cref="MemoryStream"/> instance.
+ /// This facilitates deserialization or other manipulation of the contents in memory.
+ /// </summary>
+ /// <param name="headers">The header fields describing the body parts content. Looking for header fields such as
+ /// Content-Type and Content-Disposition can help provide the appropriate stream. In addition to using the information
+ /// in the provided header fields, it is also possible to add new header fields or modify existing header fields. This can
+ /// be useful to get around situations where the Content-type may say <b>application/octet-stream</b> but based on
+ /// analyzing the <b>Content-Disposition</b> header field it is found that the content in fact is <b>application/json</b>, for example.</param>
+ /// <returns>A stream instance where the contents of a body part will be written to.</returns>
+ public Stream GetStream(HttpContentHeaders headers)
+ {
+ if (headers == null)
+ {
+ throw new ArgumentNullException("headers");
+ }
+
+ return new MemoryStream();
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/ObjectContent.cs b/src/System.Net.Http.Formatting/ObjectContent.cs
new file mode 100644
index 00000000..e7ac3de0
--- /dev/null
+++ b/src/System.Net.Http.Formatting/ObjectContent.cs
@@ -0,0 +1,159 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+using System.IO;
+using System.Net.Http.Formatting;
+using System.Threading.Tasks;
+
+namespace System.Net.Http
+{
+ /// <summary>
+ /// Contains a value as well as an associated <see cref="MediaTypeFormatter"/> that will be
+ /// used to serialize the value when writing this content.
+ /// </summary>
+ public class ObjectContent : HttpContent
+ {
+ private object _value;
+ private readonly MediaTypeFormatter _formatter;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ObjectContent"/> class.
+ /// </summary>
+ /// <param name="type">The type of object this instance will contain.</param>
+ /// <param name="value">The value of the object this instance will contain.</param>
+ /// <param name="formatter">The formatter to use when serializing the value.</param>
+ public ObjectContent(Type type, object value, MediaTypeFormatter formatter)
+ : this(type, value, formatter, null)
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ObjectContent"/> class.
+ /// </summary>
+ /// <param name="type">The type of object this instance will contain.</param>
+ /// <param name="value">The value of the object this instance will contain.</param>
+ /// <param name="formatter">The formatter to use when serializing the value.</param>
+ /// <param name="mediaType">The media type to associate with this object.</param>
+ public ObjectContent(Type type, object value, MediaTypeFormatter formatter, string mediaType)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+ if (formatter == null)
+ {
+ throw new ArgumentNullException("formatter");
+ }
+
+ _formatter = formatter;
+ ObjectType = type;
+
+ VerifyAndSetObject(value);
+ _formatter.SetDefaultContentHeaders(type, Headers, mediaType);
+ }
+
+ /// <summary>
+ /// Gets the type of object managed by this <see cref="ObjectContent"/> instance.
+ /// </summary>
+ public Type ObjectType { get; private set; }
+
+ /// <summary>
+ /// The <see cref="MediaTypeFormatter">formatter</see> associated with this content instance.
+ /// </summary>
+ public MediaTypeFormatter Formatter
+ {
+ get { return _formatter; }
+ }
+
+ /// <summary>
+ /// Gets or sets the value of the current <see cref="ObjectContent"/>.
+ /// </summary>
+ internal object Value
+ {
+ get { return _value; }
+ set { VerifyAndSetObject(value); }
+ }
+
+ /// <summary>
+ /// Asynchronously serializes the object's content to the given <paramref name="stream"/>.
+ /// </summary>
+ /// <param name="stream">The <see cref="Stream"/> to which to write.</param>
+ /// <param name="context">The associated <see cref="TransportContext"/>.</param>
+ /// <returns>A <see cref="Task"/> instance that is asynchronously serializing the object's content.</returns>
+ protected override Task SerializeToStreamAsync(Stream stream, TransportContext context)
+ {
+ return _formatter.WriteToStreamAsync(ObjectType, Value, stream, Headers, context);
+ }
+
+ /// <summary>
+ /// Computes the length of the stream if possible.
+ /// </summary>
+ /// <param name="length">The computed length of the stream.</param>
+ /// <returns><c>true</c> if the length has been computed; otherwise <c>false</c>.</returns>
+ protected override bool TryComputeLength(out long length)
+ {
+ length = -1;
+ return false;
+ }
+
+ private static bool IsTypeNullable(Type type)
+ {
+ return !type.IsValueType ||
+ (type.IsGenericType &&
+ type.GetGenericTypeDefinition() == typeof(Nullable<>));
+ }
+
+ private void VerifyAndSetObject(object value)
+ {
+ Contract.Assert(ObjectType != null, "Type cannot be null");
+
+ if (value == null)
+ {
+ // Null may not be assigned to value types (unless Nullable<T>)
+ // We allow an ObjectContent of type void and value null as a special case
+ if (ObjectType != typeof(void) && !IsTypeNullable(ObjectType))
+ {
+ throw new InvalidOperationException(RS.Format(Properties.Resources.CannotUseNullValueType, typeof(ObjectContent).Name, ObjectType.Name));
+ }
+ }
+ else
+ {
+ // Non-null objects must be a type assignable to Type
+ Type objectType = value.GetType();
+ if (!ObjectType.IsAssignableFrom(objectType))
+ {
+ throw new ArgumentException(RS.Format(Properties.Resources.ObjectAndTypeDisagree, objectType.Name, ObjectType.Name), "value");
+ }
+ }
+ _value = value;
+ }
+ }
+
+ /// <summary>
+ /// Generic form of <see cref="ObjectContent"/>.
+ /// </summary>
+ /// <typeparam name="T">The type of object this <see cref="ObjectContent"/> class will contain.</typeparam>
+ [SuppressMessage("Microsoft.StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleClass", Justification = "Class contains generic forms")]
+ public class ObjectContent<T> : ObjectContent
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ObjectContent{T}"/> class.
+ /// </summary>
+ /// <param name="value">The value of the object this instance will contain.</param>
+ /// <param name="formatter">The formatter to use when serializing the value.</param>
+ public ObjectContent(T value, MediaTypeFormatter formatter)
+ : this(value, formatter, null)
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ObjectContent{T}"/> class.
+ /// </summary>
+ /// <param name="value">The value of the object this instance will contain.</param>
+ /// <param name="formatter">The formatter to use when serializing the value.</param>
+ /// <param name="mediaType">The media type to associate with this object.</param>
+ public ObjectContent(T value, MediaTypeFormatter formatter, string mediaType)
+ : base(typeof(T), value, formatter, mediaType)
+ {
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Properties/AssemblyInfo.cs b/src/System.Net.Http.Formatting/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..b8332890
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Properties/AssemblyInfo.cs
@@ -0,0 +1,16 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+
+[assembly: AssemblyTitle("System.Net.Http.Formatting")]
+[assembly: AssemblyDescription("")]
+[assembly: Guid("7fa1ae84-36e2-46b6-812c-c985a8e65e9a")]
+[assembly: InternalsVisibleTo("System.Net.Http.Formatting.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
+[assembly: InternalsVisibleTo("System.Net.Http.Formatting.Test.Integration, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
+[assembly: InternalsVisibleTo("System.Net.Http.Formatting.OData.Test.Unit, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
+/* TODO: Remove */
+[assembly: InternalsVisibleTo("System.Web.Http, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
diff --git a/src/System.Net.Http.Formatting/Properties/Resources.Designer.cs b/src/System.Net.Http.Formatting/Properties/Resources.Designer.cs
new file mode 100644
index 00000000..61096acc
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Properties/Resources.Designer.cs
@@ -0,0 +1,477 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.239
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace System.Net.Http.Properties {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("System.Net.Http.Properties.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A null &apos;{0}&apos; is not valid..
+ /// </summary>
+ internal static string CannotHaveNullInList {
+ get {
+ return ResourceManager.GetString("CannotHaveNullInList", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The &apos;{0}&apos; of &apos;{1}&apos; cannot be used as a supported media type because it is a media range..
+ /// </summary>
+ internal static string CannotUseMediaRangeForSupportedMediaType {
+ get {
+ return ResourceManager.GetString("CannotUseMediaRangeForSupportedMediaType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The &apos;{0}&apos; type cannot accept a null value for the value type &apos;{1}&apos;..
+ /// </summary>
+ internal static string CannotUseNullValueType {
+ get {
+ return ResourceManager.GetString("CannotUseNullValueType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &apos;{0}&apos; did not contain a valid file name property: &apos;{1}&apos;..
+ /// </summary>
+ internal static string ContentDispositionInvalidFileName {
+ get {
+ return ResourceManager.GetString("ContentDispositionInvalidFileName", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Error reading HTML form URL-encoded data stream..
+ /// </summary>
+ internal static string ErrorReadingFormUrlEncodedStream {
+ get {
+ return ResourceManager.GetString("ErrorReadingFormUrlEncodedStream", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Error parsing HTML form URL-encoded data, byte {0}..
+ /// </summary>
+ internal static string FormUrlEncodedParseError {
+ get {
+ return ResourceManager.GetString("FormUrlEncodedParseError", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Invalid HTTP status code: &apos;{0}&apos;. The status code must be between {1} and {2}..
+ /// </summary>
+ internal static string HttpInvalidStatusCode {
+ get {
+ return ResourceManager.GetString("HttpInvalidStatusCode", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Invalid HTTP version: &apos;{0}&apos;. The version must start with the characters &apos;{1}&apos;..
+ /// </summary>
+ internal static string HttpInvalidVersion {
+ get {
+ return ResourceManager.GetString("HttpInvalidVersion", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The &apos;{0}&apos; of the &apos;{1}&apos; has already been read..
+ /// </summary>
+ internal static string HttpMessageContentAlreadyRead {
+ get {
+ return ResourceManager.GetString("HttpMessageContentAlreadyRead", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The &apos;{0}&apos; must be seekable in order to create an &apos;{1}&apos; instance containing an entity body. .
+ /// </summary>
+ internal static string HttpMessageContentStreamMustBeSeekable {
+ get {
+ return ResourceManager.GetString("HttpMessageContentStreamMustBeSeekable", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Error reading HTTP message..
+ /// </summary>
+ internal static string HttpMessageErrorReading {
+ get {
+ return ResourceManager.GetString("HttpMessageErrorReading", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Invalid &apos;{0}&apos; instance provided. It does not have a content type header with a value of &apos;{1}&apos;..
+ /// </summary>
+ internal static string HttpMessageInvalidMediaType {
+ get {
+ return ResourceManager.GetString("HttpMessageInvalidMediaType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to HTTP Request URI cannot be an empty string..
+ /// </summary>
+ internal static string HttpMessageParserEmptyUri {
+ get {
+ return ResourceManager.GetString("HttpMessageParserEmptyUri", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Error parsing HTTP message header byte {0} of message {1}..
+ /// </summary>
+ internal static string HttpMessageParserError {
+ get {
+ return ResourceManager.GetString("HttpMessageParserError", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to An invalid number of &apos;{0}&apos; header fields were present in the HTTP Request. It must contain exactly one &apos;{0}&apos; header field but found {1}..
+ /// </summary>
+ internal static string HttpMessageParserInvalidHostCount {
+ get {
+ return ResourceManager.GetString("HttpMessageParserInvalidHostCount", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Invalid URI scheme: &apos;{0}&apos;. The URI scheme must be a valid &apos;{1}&apos; scheme..
+ /// </summary>
+ internal static string HttpMessageParserInvalidUriScheme {
+ get {
+ return ResourceManager.GetString("HttpMessageParserInvalidUriScheme", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The input stream contains too many delimiter characters which may be a sign that the incoming data may be malicious..
+ /// </summary>
+ internal static string InputStreamHasTooManyDelimiters {
+ get {
+ return ResourceManager.GetString("InputStreamHasTooManyDelimiters", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The value &apos;{0}&apos; is not a valid media range..
+ /// </summary>
+ internal static string InvalidMediaRange {
+ get {
+ return ResourceManager.GetString("InvalidMediaRange", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to This reader&apos;s MaxDepth of {0} has been exceeded. Consider using a larger MaxDepth..
+ /// </summary>
+ internal static string JsonTooDeep {
+ get {
+ return ResourceManager.GetString("JsonTooDeep", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The media type formatter of type &apos;{0}&apos; does not support reading since it does not implement the ReadFromStreamAsync method..
+ /// </summary>
+ internal static string MediaTypeFormatterCannotRead {
+ get {
+ return ResourceManager.GetString("MediaTypeFormatterCannotRead", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The media type formatter of type &apos;{0}&apos; does not support reading since it does not implement the OnReadFromStream method..
+ /// </summary>
+ internal static string MediaTypeFormatterCannotReadSync {
+ get {
+ return ResourceManager.GetString("MediaTypeFormatterCannotReadSync", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The media type formatter of type &apos;{0}&apos; does not support writing since it does not implement the WriteToStreamAsync method..
+ /// </summary>
+ internal static string MediaTypeFormatterCannotWrite {
+ get {
+ return ResourceManager.GetString("MediaTypeFormatterCannotWrite", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The media type formatter of type &apos;{0}&apos; does not support writing since it does not implement the OnWriteToStream method..
+ /// </summary>
+ internal static string MediaTypeFormatterCannotWriteSync {
+ get {
+ return ResourceManager.GetString("MediaTypeFormatterCannotWriteSync", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to MIME multipart boundary cannot end with an empty space..
+ /// </summary>
+ internal static string MimeMultipartParserBadBoundary {
+ get {
+ return ResourceManager.GetString("MimeMultipartParserBadBoundary", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The parameter must be greater than {0}..
+ /// </summary>
+ internal static string MinParameterSize {
+ get {
+ return ResourceManager.GetString("MinParameterSize", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Did not find required &apos;{0}&apos; header field in MIME multipart body part..
+ /// </summary>
+ internal static string MultipartFormDataStreamProviderNoContentDisposition {
+ get {
+ return ResourceManager.GetString("MultipartFormDataStreamProviderNoContentDisposition", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Could not determine a valid local file name for the multipart body part..
+ /// </summary>
+ internal static string MultipartStreamProviderInvalidLocalFileName {
+ get {
+ return ResourceManager.GetString("MultipartStreamProviderInvalidLocalFileName", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A non-null request URI must be provided to determine if a &apos;{0}&apos; matches a given request or response message..
+ /// </summary>
+ internal static string NonNullUriRequiredForMediaTypeMapping {
+ get {
+ return ResourceManager.GetString("NonNullUriRequiredForMediaTypeMapping", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The parameter must be a non-zero positive integer..
+ /// </summary>
+ internal static string NonZeroParameterSize {
+ get {
+ return ResourceManager.GetString("NonZeroParameterSize", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to No MediaTypeFormatter is available to read an object of type &apos;{0}&apos; from content with media type &apos;{1}&apos;..
+ /// </summary>
+ internal static string NoReadSerializerAvailable {
+ get {
+ return ResourceManager.GetString("NoReadSerializerAvailable", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to An object of type &apos;{0}&apos; cannot be used with a type parameter of &apos;{1}&apos;..
+ /// </summary>
+ internal static string ObjectAndTypeDisagree {
+ get {
+ return ResourceManager.GetString("ObjectAndTypeDisagree", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Invalid &apos;{0}&apos; instance provided. It does not have a &apos;{1}&apos; content-type header with a &apos;{2}&apos; parameter..
+ /// </summary>
+ internal static string ReadAsMimeMultipartArgumentNoBoundary {
+ get {
+ return ResourceManager.GetString("ReadAsMimeMultipartArgumentNoBoundary", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Invalid &apos;{0}&apos; instance provided. It does not have a content-type header value. &apos;{0}&apos; instances must have a content-type header starting with &apos;{1}&apos;..
+ /// </summary>
+ internal static string ReadAsMimeMultipartArgumentNoContentType {
+ get {
+ return ResourceManager.GetString("ReadAsMimeMultipartArgumentNoContentType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Invalid &apos;{0}&apos; instance provided. It does not have a content type header starting with &apos;{1}&apos;..
+ /// </summary>
+ internal static string ReadAsMimeMultipartArgumentNoMultipart {
+ get {
+ return ResourceManager.GetString("ReadAsMimeMultipartArgumentNoMultipart", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Error reading MIME multipart body part..
+ /// </summary>
+ internal static string ReadAsMimeMultipartErrorReading {
+ get {
+ return ResourceManager.GetString("ReadAsMimeMultipartErrorReading", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Error writing MIME multipart body part to output stream..
+ /// </summary>
+ internal static string ReadAsMimeMultipartErrorWriting {
+ get {
+ return ResourceManager.GetString("ReadAsMimeMultipartErrorWriting", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Error parsing MIME multipart body part header byte {0} of data segment {1}..
+ /// </summary>
+ internal static string ReadAsMimeMultipartHeaderParseError {
+ get {
+ return ResourceManager.GetString("ReadAsMimeMultipartHeaderParseError", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Error parsing MIME multipart message byte {0} of data segment {1}..
+ /// </summary>
+ internal static string ReadAsMimeMultipartParseError {
+ get {
+ return ResourceManager.GetString("ReadAsMimeMultipartParseError", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The stream provider of type &apos;{0}&apos; threw an exception..
+ /// </summary>
+ internal static string ReadAsMimeMultipartStreamProviderException {
+ get {
+ return ResourceManager.GetString("ReadAsMimeMultipartStreamProviderException", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The stream provider of type &apos;{0}&apos; returned null. It must return a writable &apos;{1}&apos; instance..
+ /// </summary>
+ internal static string ReadAsMimeMultipartStreamProviderNull {
+ get {
+ return ResourceManager.GetString("ReadAsMimeMultipartStreamProviderNull", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The stream provider of type &apos;{0}&apos; returned a read-only stream. It must return a writable &apos;{1}&apos; instance..
+ /// </summary>
+ internal static string ReadAsMimeMultipartStreamProviderReadOnly {
+ get {
+ return ResourceManager.GetString("ReadAsMimeMultipartStreamProviderReadOnly", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Unexpected end of MIME multipart stream. MIME multipart message is not complete..
+ /// </summary>
+ internal static string ReadAsMimeMultipartUnexpectedTermination {
+ get {
+ return ResourceManager.GetString("ReadAsMimeMultipartUnexpectedTermination", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The &apos;{0}&apos; serializer cannot serialize the type &apos;{1}&apos;..
+ /// </summary>
+ internal static string SerializerCannotSerializeType {
+ get {
+ return ResourceManager.GetString("SerializerCannotSerializeType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The return value from a Catch() lambda cannot return null..
+ /// </summary>
+ internal static string TaskExtensions_Catch_CannotReturnNull {
+ get {
+ return ResourceManager.GetString("TaskExtensions_Catch_CannotReturnNull", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &apos;undefined&apos;.
+ /// </summary>
+ internal static string UndefinedMediaType {
+ get {
+ return ResourceManager.GetString("UndefinedMediaType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The character encoding used by &apos;{0}&apos; for writing data must be either &apos;{1}&apos; or &apos;{2}&apos;.&apos;.
+ /// </summary>
+ internal static string UnsupportedEncoding {
+ get {
+ return ResourceManager.GetString("UnsupportedEncoding", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/Properties/Resources.resx b/src/System.Net.Http.Formatting/Properties/Resources.resx
new file mode 100644
index 00000000..f6028a31
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Properties/Resources.resx
@@ -0,0 +1,259 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="CannotHaveNullInList" xml:space="preserve">
+ <value>A null '{0}' is not valid.</value>
+ </data>
+ <data name="CannotUseMediaRangeForSupportedMediaType" xml:space="preserve">
+ <value>The '{0}' of '{1}' cannot be used as a supported media type because it is a media range.</value>
+ </data>
+ <data name="CannotUseNullValueType" xml:space="preserve">
+ <value>The '{0}' type cannot accept a null value for the value type '{1}'.</value>
+ </data>
+ <data name="ContentDispositionInvalidFileName" xml:space="preserve">
+ <value>'{0}' did not contain a valid file name property: '{1}'.</value>
+ </data>
+ <data name="ErrorReadingFormUrlEncodedStream" xml:space="preserve">
+ <value>Error reading HTML form URL-encoded data stream.</value>
+ </data>
+ <data name="FormUrlEncodedParseError" xml:space="preserve">
+ <value>Error parsing HTML form URL-encoded data, byte {0}.</value>
+ </data>
+ <data name="HttpInvalidStatusCode" xml:space="preserve">
+ <value>Invalid HTTP status code: '{0}'. The status code must be between {1} and {2}.</value>
+ </data>
+ <data name="HttpInvalidVersion" xml:space="preserve">
+ <value>Invalid HTTP version: '{0}'. The version must start with the characters '{1}'.</value>
+ </data>
+ <data name="HttpMessageContentAlreadyRead" xml:space="preserve">
+ <value>The '{0}' of the '{1}' has already been read.</value>
+ </data>
+ <data name="HttpMessageContentStreamMustBeSeekable" xml:space="preserve">
+ <value>The '{0}' must be seekable in order to create an '{1}' instance containing an entity body. </value>
+ </data>
+ <data name="HttpMessageErrorReading" xml:space="preserve">
+ <value>Error reading HTTP message.</value>
+ </data>
+ <data name="HttpMessageInvalidMediaType" xml:space="preserve">
+ <value>Invalid '{0}' instance provided. It does not have a content type header with a value of '{1}'.</value>
+ </data>
+ <data name="HttpMessageParserEmptyUri" xml:space="preserve">
+ <value>HTTP Request URI cannot be an empty string.</value>
+ </data>
+ <data name="HttpMessageParserError" xml:space="preserve">
+ <value>Error parsing HTTP message header byte {0} of message {1}.</value>
+ </data>
+ <data name="HttpMessageParserInvalidHostCount" xml:space="preserve">
+ <value>An invalid number of '{0}' header fields were present in the HTTP Request. It must contain exactly one '{0}' header field but found {1}.</value>
+ </data>
+ <data name="HttpMessageParserInvalidUriScheme" xml:space="preserve">
+ <value>Invalid URI scheme: '{0}'. The URI scheme must be a valid '{1}' scheme.</value>
+ </data>
+ <data name="InvalidMediaRange" xml:space="preserve">
+ <value>The value '{0}' is not a valid media range.</value>
+ </data>
+ <data name="MimeMultipartParserBadBoundary" xml:space="preserve">
+ <value>MIME multipart boundary cannot end with an empty space.</value>
+ </data>
+ <data name="MinParameterSize" xml:space="preserve">
+ <value>The parameter must be greater than {0}.</value>
+ </data>
+ <data name="MultipartFormDataStreamProviderNoContentDisposition" xml:space="preserve">
+ <value>Did not find required '{0}' header field in MIME multipart body part.</value>
+ </data>
+ <data name="MultipartStreamProviderInvalidLocalFileName" xml:space="preserve">
+ <value>Could not determine a valid local file name for the multipart body part.</value>
+ </data>
+ <data name="NonNullUriRequiredForMediaTypeMapping" xml:space="preserve">
+ <value>A non-null request URI must be provided to determine if a '{0}' matches a given request or response message.</value>
+ </data>
+ <data name="NonZeroParameterSize" xml:space="preserve">
+ <value>The parameter must be a non-zero positive integer.</value>
+ </data>
+ <data name="NoReadSerializerAvailable" xml:space="preserve">
+ <value>No MediaTypeFormatter is available to read an object of type '{0}' from content with media type '{1}'.</value>
+ </data>
+ <data name="ObjectAndTypeDisagree" xml:space="preserve">
+ <value>An object of type '{0}' cannot be used with a type parameter of '{1}'.</value>
+ </data>
+ <data name="ReadAsMimeMultipartArgumentNoBoundary" xml:space="preserve">
+ <value>Invalid '{0}' instance provided. It does not have a '{1}' content-type header with a '{2}' parameter.</value>
+ </data>
+ <data name="ReadAsMimeMultipartArgumentNoContentType" xml:space="preserve">
+ <value>Invalid '{0}' instance provided. It does not have a content-type header value. '{0}' instances must have a content-type header starting with '{1}'.</value>
+ </data>
+ <data name="ReadAsMimeMultipartArgumentNoMultipart" xml:space="preserve">
+ <value>Invalid '{0}' instance provided. It does not have a content type header starting with '{1}'.</value>
+ </data>
+ <data name="ReadAsMimeMultipartErrorReading" xml:space="preserve">
+ <value>Error reading MIME multipart body part.</value>
+ </data>
+ <data name="ReadAsMimeMultipartErrorWriting" xml:space="preserve">
+ <value>Error writing MIME multipart body part to output stream.</value>
+ </data>
+ <data name="ReadAsMimeMultipartHeaderParseError" xml:space="preserve">
+ <value>Error parsing MIME multipart body part header byte {0} of data segment {1}.</value>
+ </data>
+ <data name="ReadAsMimeMultipartParseError" xml:space="preserve">
+ <value>Error parsing MIME multipart message byte {0} of data segment {1}.</value>
+ </data>
+ <data name="ReadAsMimeMultipartStreamProviderException" xml:space="preserve">
+ <value>The stream provider of type '{0}' threw an exception.</value>
+ </data>
+ <data name="ReadAsMimeMultipartStreamProviderNull" xml:space="preserve">
+ <value>The stream provider of type '{0}' returned null. It must return a writable '{1}' instance.</value>
+ </data>
+ <data name="ReadAsMimeMultipartStreamProviderReadOnly" xml:space="preserve">
+ <value>The stream provider of type '{0}' returned a read-only stream. It must return a writable '{1}' instance.</value>
+ </data>
+ <data name="ReadAsMimeMultipartUnexpectedTermination" xml:space="preserve">
+ <value>Unexpected end of MIME multipart stream. MIME multipart message is not complete.</value>
+ </data>
+ <data name="SerializerCannotSerializeType" xml:space="preserve">
+ <value>The '{0}' serializer cannot serialize the type '{1}'.</value>
+ </data>
+ <data name="UndefinedMediaType" xml:space="preserve">
+ <value>'undefined'</value>
+ </data>
+ <data name="UnsupportedEncoding" xml:space="preserve">
+ <value>The character encoding used by '{0}' for writing data must be either '{1}' or '{2}'.'</value>
+ </data>
+ <data name="TaskExtensions_Catch_CannotReturnNull" xml:space="preserve">
+ <value>The return value from a Catch() lambda cannot return null.</value>
+ <comment>TaskExtensions_Catch_CannotReturnNull description</comment>
+ </data>
+ <data name="InputStreamHasTooManyDelimiters" xml:space="preserve">
+ <value>The input stream contains too many delimiter characters which may be a sign that the incoming data may be malicious.</value>
+ </data>
+ <data name="MediaTypeFormatterCannotRead" xml:space="preserve">
+ <value>The media type formatter of type '{0}' does not support reading since it does not implement the ReadFromStreamAsync method.</value>
+ </data>
+ <data name="MediaTypeFormatterCannotWrite" xml:space="preserve">
+ <value>The media type formatter of type '{0}' does not support writing since it does not implement the WriteToStreamAsync method.</value>
+ </data>
+ <data name="MediaTypeFormatterCannotReadSync" xml:space="preserve">
+ <value>The media type formatter of type '{0}' does not support reading since it does not implement the OnReadFromStream method.</value>
+ </data>
+ <data name="MediaTypeFormatterCannotWriteSync" xml:space="preserve">
+ <value>The media type formatter of type '{0}' does not support writing since it does not implement the OnWriteToStream method.</value>
+ </data>
+ <data name="JsonTooDeep" xml:space="preserve">
+ <value>This reader's MaxDepth of {0} has been exceeded. Consider using a larger MaxDepth.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/System.Net.Http.Formatting/SR.resx b/src/System.Net.Http.Formatting/SR.resx
new file mode 100644
index 00000000..9d73ccfd
--- /dev/null
+++ b/src/System.Net.Http.Formatting/SR.resx
@@ -0,0 +1,261 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="CannotHaveNullInList" xml:space="preserve">
+ <value>A null '{0}' is not valid.</value>
+ </data>
+ <data name="CannotUseMediaRangeForSupportedMediaType" xml:space="preserve">
+ <value>The '{0}' of '{1}' cannot be used as a supported media type because it is a media range.</value>
+ </data>
+ <data name="CannotUseNullValueType" xml:space="preserve">
+ <value>The '{0}' type cannot accept a null value for the value type '{1}'.</value>
+ </data>
+ <data name="CannotUseThisParameterType" xml:space="preserve">
+ <value>The type '{0}' cannot be used as the type parameter for '{1}'.</value>
+ </data>
+ <data name="ContentDispositionInvalidFileName" xml:space="preserve">
+ <value>'{0}' did not contain a valid file name property: '{1}'.</value>
+ </data>
+ <data name="ErrorReadingFormUrlEncodedStream" xml:space="preserve">
+ <value>Error reading HTML form URL-encoded data stream.</value>
+ </data>
+ <data name="FormUrlEncodedParseError" xml:space="preserve">
+ <value>Error parsing HTML form URL-encoded data, byte {0}.</value>
+ </data>
+ <data name="HttpInvalidStatusCode" xml:space="preserve">
+ <value>Invalid HTTP status code: '{0}'. The status code must be between {1} and {2}.</value>
+ </data>
+ <data name="HttpInvalidVersion" xml:space="preserve">
+ <value>Invalid HTTP version: '{0}'. The version must start with the characters '{1}'.</value>
+ </data>
+ <data name="HttpMessageContentAlreadyRead" xml:space="preserve">
+ <value>The '{0}' of the '{1}' has already been read.</value>
+ </data>
+ <data name="HttpMessageContentStreamMustBeSeekable" xml:space="preserve">
+ <value>The '{0}' must be seekable in order to create an '{1}' instance containing an entity body. </value>
+ </data>
+ <data name="HttpMessageErrorReading" xml:space="preserve">
+ <value>Error reading HTTP message.</value>
+ </data>
+ <data name="HttpMessageInvalidMediaType" xml:space="preserve">
+ <value>Invalid '{0}' instance provided. It does not have a content type header with a value of '{1}'.</value>
+ </data>
+ <data name="HttpMessageParserEmptyUri" xml:space="preserve">
+ <value>HTTP Request URI cannot be an empty string.</value>
+ </data>
+ <data name="HttpMessageParserError" xml:space="preserve">
+ <value>Error parsing HTTP message header byte {0} of message {1}.</value>
+ </data>
+ <data name="HttpMessageParserInvalidHostCount" xml:space="preserve">
+ <value>An invalid number of '{0}' header fields were present in the HTTP Request. It must contain exactly one '{0}' header field but found {1}.</value>
+ </data>
+ <data name="HttpMessageParserInvalidUriScheme" xml:space="preserve">
+ <value>Invalid URI scheme: '{0}'. The URI scheme must be a valid '{1}' scheme.</value>
+ </data>
+ <data name="HttpMessageParserMissingContentLength" xml:space="preserve">
+ <value>HTTP messages containing an entity body must include a valid '{0}' header field.</value>
+ </data>
+ <data name="InvalidMediaRange" xml:space="preserve">
+ <value>The value '{0}' is not a valid media range.</value>
+ </data>
+ <data name="InvalidMediaType" xml:space="preserve">
+ <value>The media type string '{0}' is not a legal '{1}'.</value>
+ </data>
+ <data name="MediaTypeCanNotBeMediaRange" xml:space="preserve">
+ <value>The media range '{0}' cannot be a supported media type.</value>
+ </data>
+ <data name="MediaTypeFormatterWriteUnsupported" xml:space="preserve">
+ <value>The '{0}' media type formatter does not support writing content.</value>
+ </data>
+ <data name="MediaTypeMustBeSetBeforeWrite" xml:space="preserve">
+ <value>'{0}' must be set before '{1}' can serialize its content.</value>
+ </data>
+ <data name="MimeMultipartParserBadBoundary" xml:space="preserve">
+ <value>MIME multipart boundary cannot end with an empty space.</value>
+ </data>
+ <data name="MinParameterSize" xml:space="preserve">
+ <value>The parameter must be greater than {0}.</value>
+ </data>
+ <data name="MultipartFormDataStreamProviderNoContentDisposition" xml:space="preserve">
+ <value>Did not find required '{0}' header field in MIME multipart body part.</value>
+ </data>
+ <data name="MultipartStreamProviderInvalidLocalFileName" xml:space="preserve">
+ <value>Could not determine a valid local file name for the multipart body part.</value>
+ </data>
+ <data name="NonNullUriRequiredForMediaTypeMapping" xml:space="preserve">
+ <value>A non-null request URI must be provided to determine if a '{0}' matches a given request or response message.</value>
+ </data>
+ <data name="NonZeroParameterSize" xml:space="preserve">
+ <value>The parameter must be a non-zero positive integer.</value>
+ </data>
+ <data name="NoReadSerializerAvailable" xml:space="preserve">
+ <value>No '{0}' is available to read an object of type '{1}' with the media type '{2}'.</value>
+ </data>
+ <data name="NoWriteSerializerAvailable" xml:space="preserve">
+ <value>No '{0}' is available to write an object of type '{1}' with the media type '{2}'.</value>
+ </data>
+ <data name="ObjectAndTypeDisagree" xml:space="preserve">
+ <value>An object of type '{0}' cannot be used with a type parameter of '{1}'.</value>
+ </data>
+ <data name="ReadAsMimeMultipartArgumentNoBoundary" xml:space="preserve">
+ <value>Invalid '{0}' instance provided. It does not have a '{1}' content-type header with a '{2}' parameter.</value>
+ </data>
+ <data name="ReadAsMimeMultipartArgumentNoContentType" xml:space="preserve">
+ <value>Invalid '{0}' instance provided. It does not have a content-type header value. '{0}' instances must have a content-type header starting with '{1}'.</value>
+ </data>
+ <data name="ReadAsMimeMultipartArgumentNoMultipart" xml:space="preserve">
+ <value>Invalid '{0}' instance provided. It does not have a content type header starting with '{1}'.</value>
+ </data>
+ <data name="ReadAsMimeMultipartErrorReading" xml:space="preserve">
+ <value>Error reading MIME multipart body part.</value>
+ </data>
+ <data name="ReadAsMimeMultipartErrorWriting" xml:space="preserve">
+ <value>Error writing MIME multipart body part to output stream.</value>
+ </data>
+ <data name="ReadAsMimeMultipartHeaderParseError" xml:space="preserve">
+ <value>Error parsing MIME multipart body part header byte {0} of data segment {1}.</value>
+ </data>
+ <data name="ReadAsMimeMultipartParseError" xml:space="preserve">
+ <value>Error parsing MIME multipart message byte {0} of data segment {1}.</value>
+ </data>
+ <data name="ReadAsMimeMultipartStreamProviderException" xml:space="preserve">
+ <value>The stream provider of type '{0}' threw an exception.</value>
+ </data>
+ <data name="ReadAsMimeMultipartStreamProviderNull" xml:space="preserve">
+ <value>The stream provider of type '{0}' returned null. It must return a writable '{1}' instance.</value>
+ </data>
+ <data name="ReadAsMimeMultipartStreamProviderReadOnly" xml:space="preserve">
+ <value>The stream provider of type '{0}' returned a read-only stream. It must return a writable '{1}' instance.</value>
+ </data>
+ <data name="ReadAsMimeMultipartUnexpectedTermination" xml:space="preserve">
+ <value>Unexpected end of MIME multipart stream. MIME multipart message is not complete.</value>
+ </data>
+ <data name="ResponseMustReferenceRequest" xml:space="preserve">
+ <value>The '{0}' '{1}' parameter must have a reference to a '{2}' via the '{3}' property.</value>
+ </data>
+ <data name="SerializerCannotSerializeType" xml:space="preserve">
+ <value>The '{0}' serializer cannot serialize the type '{1}'.</value>
+ </data>
+ <data name="UndefinedMediaType" xml:space="preserve">
+ <value>'undefined'</value>
+ </data>
+ <data name="UnsupportedEncoding" xml:space="preserve">
+ <value>The character encoding used by '{0}' for writing data must be either '{1}' or '{2}'.'</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/System.Net.Http.Formatting/Settings.StyleCop b/src/System.Net.Http.Formatting/Settings.StyleCop
new file mode 100644
index 00000000..5b387864
--- /dev/null
+++ b/src/System.Net.Http.Formatting/Settings.StyleCop
@@ -0,0 +1,10 @@
+<StyleCopSettings Version="105">
+ <SourceFileList>
+ <SourceFile>UriQueryUtility.cs</SourceFile>
+ <Settings>
+ <GlobalSettings>
+ <BooleanProperty Name="RulesEnabledByDefault">False</BooleanProperty>
+ </GlobalSettings>
+ </Settings>
+ </SourceFileList>
+</StyleCopSettings> \ No newline at end of file
diff --git a/src/System.Net.Http.Formatting/System.Net.Http.Formatting.csproj b/src/System.Net.Http.Formatting/System.Net.Http.Formatting.csproj
new file mode 100644
index 00000000..04c31ff0
--- /dev/null
+++ b/src/System.Net.Http.Formatting/System.Net.Http.Formatting.csproj
@@ -0,0 +1,174 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <CodeAnalysis Condition=" '$(CodeAnalysis)' == '' ">false</CodeAnalysis>
+ <ProductVersion>9.0.30729</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{668E9021-CE84-49D9-98FB-DF125A9FCDB0}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>System.Net.Http</RootNamespace>
+ <AssemblyName>System.Net.Http.Formatting</AssemblyName>
+ <TargetFrameworkProfile Condition="'$(TargetFrameworkVersion)' != 'v4.5'">Client</TargetFrameworkProfile>
+ <FileAlignment>512</FileAlignment>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>..\..\bin\Debug\</OutputPath>
+ <DefineConstants>TRACE;DEBUG;ASPNETMVC</DefineConstants>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ <DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
+ <NoWarn>1591</NoWarn>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>..\..\bin\Release\</OutputPath>
+ <DefineConstants>TRACE;ASPNETMVC</DefineConstants>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ <RunCodeAnalysis>$(CodeAnalysis)</RunCodeAnalysis>
+ <DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
+ <NoWarn>1591</NoWarn>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'CodeCoverage|AnyCPU'">
+ <DebugSymbols>true</DebugSymbols>
+ <OutputPath>..\..\bin\CodeCoverage\</OutputPath>
+ <DefineConstants>TRACE;DEBUG;CODE_COVERAGE;ASPNETMVC</DefineConstants>
+ <DebugType>full</DebugType>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="Newtonsoft.Json, Version=4.0.8.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>..\..\packages\Newtonsoft.Json.4.0.8\lib\net40\Newtonsoft.Json.dll</HintPath>
+ </Reference>
+ <Reference Include="System" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.Net.Http">
+ <HintPath>..\..\packages\Microsoft.Net.Http.2.0.20302.1\lib\net40\System.Net.Http.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Net.Http.WebRequest">
+ <HintPath>..\..\packages\Microsoft.Net.Http.2.0.20302.1\lib\net40\System.Net.Http.WebRequest.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Runtime.Serialization" />
+ <Reference Include="System.Xml" />
+ <Reference Include="System.Xml.Linq" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="..\CommonAssemblyInfo.cs">
+ <Link>Properties\CommonAssemblyInfo.cs</Link>
+ </Compile>
+ <Compile Include="..\RS.cs">
+ <Link>RS.cs</Link>
+ </Compile>
+ <Compile Include="..\TransparentCommonAssemblyInfo.cs">
+ <Link>Properties\TransparentCommonAssemblyInfo.cs</Link>
+ </Compile>
+ <Compile Include="CloneableExtensions.cs" />
+ <Compile Include="Formatting\FormDataCollection.cs" />
+ <Compile Include="Formatting\SecureJsonTextReader.cs" />
+ <Compile Include="GlobalSuppressions.cs" />
+ <Compile Include="Formatting\IFormatterLogger.cs" />
+ <Compile Include="Formatting\IRequiredMemberSelector.cs" />
+ <Compile Include="HttpClientExtensions.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="Properties\Resources.Designer.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>Resources.resx</DependentUpon>
+ </Compile>
+ <Compile Include="ContentDispositionHeaderValueExtensions.cs" />
+ <Compile Include="FormattingUtilities.cs" />
+ <Compile Include="Formatting\JsonContractResolver.cs" />
+ <Compile Include="Formatting\BufferedMediaTypeFormatter.cs" />
+ <Compile Include="Formatting\DelegatingEnumerable.cs" />
+ <Compile Include="Formatting\DefaultContentNegotiator.cs" />
+ <Compile Include="Formatting\FormUrlEncodedMediaTypeFormatter.cs" />
+ <Compile Include="Formatting\IKeyValueModel.cs" />
+ <Compile Include="Formatting\IContentNegotiator.cs" />
+ <Compile Include="Formatting\JsonMediaTypeFormatter.cs" />
+ <Compile Include="Formatting\JsonKeyValueModel.cs" />
+ <Compile Include="Formatting\JsonValueConverter.cs" />
+ <Compile Include="Formatting\MediaRangeMapping.cs" />
+ <Compile Include="Formatting\MediaTypeConstants.cs" />
+ <Compile Include="Formatting\MediaTypeFormatter.cs" />
+ <Compile Include="Formatting\MediaTypeFormatterCollection.cs" />
+ <Compile Include="Formatting\MediaTypeFormatterExtensions.cs" />
+ <Compile Include="Formatting\MediaTypeHeaderValueComparer.cs" />
+ <Compile Include="Formatting\MediaTypeHeaderValueEqualityComparer.cs" />
+ <Compile Include="Formatting\MediaTypeHeaderValueExtensions.cs" />
+ <Compile Include="Formatting\MediaTypeMapping.cs" />
+ <Compile Include="Formatting\MediaTypeMatch.cs" />
+ <Compile Include="Formatting\ParsedMediaTypeHeaderValue.cs" />
+ <Compile Include="Formatting\QueryStringMapping.cs" />
+ <Compile Include="Formatting\RequestHeaderMapping.cs" />
+ <Compile Include="Formatting\ResponseFormatterSelectionResult.cs" />
+ <Compile Include="Formatting\ResponseMediaTypeMatch.cs" />
+ <Compile Include="Formatting\StringComparisonHelper.cs" />
+ <Compile Include="Formatting\UriPathExtensionMapping.cs" />
+ <Compile Include="Formatting\XHRRequestHeaderMapping.cs" />
+ <Compile Include="Formatting\XmlKeyValueModel.cs" />
+ <Compile Include="Formatting\XmlMediaTypeFormatter.cs" />
+ <Compile Include="Formatting\Parsers\FormUrlEncodedParser.cs" />
+ <Compile Include="HttpContentCollectionExtensions.cs" />
+ <Compile Include="HttpContentExtensions.cs" />
+ <Compile Include="HttpContentMessageExtensions.cs" />
+ <Compile Include="HttpContentMultipartExtensions.cs" />
+ <Compile Include="HttpHeaderExtensions.cs" />
+ <Compile Include="HttpMessageContent.cs" />
+ <Compile Include="Formatting\Parsers\HttpRequestHeaderParser.cs" />
+ <Compile Include="Formatting\Parsers\HttpRequestLineParser.cs" />
+ <Compile Include="Formatting\Parsers\HttpResponseHeaderParser.cs" />
+ <Compile Include="Formatting\Parsers\HttpStatusLineParser.cs" />
+ <Compile Include="HttpUnsortedHeaders.cs" />
+ <Compile Include="HttpUnsortedRequest.cs" />
+ <Compile Include="HttpUnsortedResponse.cs" />
+ <Compile Include="IMultipartStreamProvider.cs" />
+ <Compile Include="Internal\AsyncResultWithExtraData.cs" />
+ <Compile Include="Internal\DelegatingStream.cs" />
+ <Compile Include="Internal\ThresholdStream.cs" />
+ <Compile Include="Formatting\Parsers\InternetMessageFormatHeaderParser.cs" />
+ <Compile Include="MimeBodyPart.cs" />
+ <Compile Include="Formatting\Parsers\MimeMultipartBodyPartParser.cs" />
+ <Compile Include="Formatting\Parsers\MimeMultipartParser.cs" />
+ <Compile Include="MultipartFileStreamProvider.cs" />
+ <Compile Include="MultipartFormDataStreamProvider.cs" />
+ <Compile Include="MultipartMemoryStreamProvider.cs" />
+ <Compile Include="ObjectContent.cs" />
+ <Compile Include="Formatting\Parsers\ParserState.cs" />
+ <Compile Include="Internal\TaskHelpers.cs" />
+ <Compile Include="UriExtensions.cs" />
+ <Compile Include="Internal\UriQueryUtility.cs" />
+ <Compile Include="Internal\TaskExtensions.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="Properties\Resources.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>Resources.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\System.Json\System.Json.csproj">
+ <Project>{F0441BE9-BDC0-4629-BE5A-8765FFAA2481}</Project>
+ <Name>System.Json</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <CodeAnalysisDictionary Include="..\CodeAnalysisDictionary.xml">
+ <Link>CodeAnalysisDictionary.xml</Link>
+ </CodeAnalysisDictionary>
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="packages.config" />
+ </ItemGroup>
+ <ItemGroup />
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+</Project> \ No newline at end of file
diff --git a/src/System.Net.Http.Formatting/UriExtensions.cs b/src/System.Net.Http.Formatting/UriExtensions.cs
new file mode 100644
index 00000000..a8ae3237
--- /dev/null
+++ b/src/System.Net.Http.Formatting/UriExtensions.cs
@@ -0,0 +1,137 @@
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Json;
+using System.Net.Http.Internal;
+using System.Runtime.Serialization.Json;
+
+namespace System.Net.Http
+{
+ /// <summary>
+ /// Extension methods to allow strongly typed objects to be read from the query component of <see cref="Uri"/> instances.
+ /// </summary>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static class UriExtensions
+ {
+ /// <summary>
+ /// Parses the query portion of the specified <see cref="Uri"/>.
+ /// </summary>
+ /// <param name="address">The <see cref="Uri"/> instance from which to read.</param>
+ /// <returns>A <see cref="NameValueCollection"/> containing the parsed result.</returns>
+ public static NameValueCollection ParseQueryString(this Uri address)
+ {
+ if (address == null)
+ {
+ throw new ArgumentNullException("address");
+ }
+
+ return UriQueryUtility.ParseQueryString(address.Query);
+ }
+
+ /// <summary>
+ /// Reads HTML form URL encoded data provided in the <see cref="Uri"/> query component as a <see cref="JsonValue"/> object.
+ /// </summary>
+ /// <param name="address">The <see cref="Uri"/> instance from which to read.</param>
+ /// <param name="value">An object to be initialized with this instance or null if the conversion cannot be performed.</param>
+ /// <returns><c>true</c> if the query component can be read as <see cref="JsonValue"/>; otherwise <c>false</c>.</returns>
+ public static bool TryReadQueryAsJson(this Uri address, out JsonObject value)
+ {
+ if (address == null)
+ {
+ throw new ArgumentNullException("address");
+ }
+
+ IEnumerable<KeyValuePair<string, string>> query = ParseQueryString(address.Query);
+ return FormUrlEncodedJson.TryParse(query, out value);
+ }
+
+ /// <summary>
+ /// Reads HTML form URL encoded data provided in the <see cref="Uri"/> query component as an <see cref="Object"/> of the given <paramref name="type"/>.
+ /// </summary>
+ /// <param name="address">The <see cref="Uri"/> instance from which to read.</param>
+ /// <param name="type">The type of the object to read.</param>
+ /// <param name="value">An object to be initialized with this instance or null if the conversion cannot be performed.</param>
+ /// <returns><c>true</c> if the query component can be read as the specified type; otherwise <c>false</c>.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1007:UseGenericsWhereAppropriate", Justification = "This is the non-generic version.")]
+ public static bool TryReadQueryAs(this Uri address, Type type, out object value)
+ {
+ if (address == null)
+ {
+ throw new ArgumentNullException("address");
+ }
+
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+
+ IEnumerable<KeyValuePair<string, string>> query = ParseQueryString(address.Query);
+ JsonObject jsonObject;
+ if (FormUrlEncodedJson.TryParse(query, out jsonObject))
+ {
+ return jsonObject.TryReadAsType(type, out value);
+ }
+
+ value = null;
+ return false;
+ }
+
+ /// <summary>
+ /// Reads HTML form URL encoded data provided in the <see cref="Uri"/> query component as an <see cref="Object"/> of type <typeparamref name="T"/>.
+ /// </summary>
+ /// <typeparam name="T">The type of the object to read.</typeparam>
+ /// <param name="address">The <see cref="Uri"/> instance from which to read.</param>
+ /// <param name="value">An object to be initialized with this instance or null if the conversion cannot be performed.</param>
+ /// <returns><c>true</c> if the query component can be read as the specified type; otherwise <c>false</c>.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "The T represents the output parameter, not an input parameter.")]
+ public static bool TryReadQueryAs<T>(this Uri address, out T value)
+ {
+ if (address == null)
+ {
+ throw new ArgumentNullException("address");
+ }
+
+ IEnumerable<KeyValuePair<string, string>> query = ParseQueryString(address.Query);
+ JsonObject jsonObject;
+ if (FormUrlEncodedJson.TryParse(query, out jsonObject))
+ {
+ return jsonObject.TryReadAsType<T>(out value);
+ }
+
+ value = default(T);
+ return false;
+ }
+
+ private static IEnumerable<KeyValuePair<string, string>> ParseQueryString(string queryString)
+ {
+ if (!String.IsNullOrEmpty(queryString))
+ {
+ if ((queryString.Length > 0) && (queryString[0] == '?'))
+ {
+ queryString = queryString.Substring(1);
+ }
+
+ if (!String.IsNullOrEmpty(queryString))
+ {
+ string[] pairs = queryString.Split('&');
+ foreach (string str in pairs)
+ {
+ string[] keyValue = str.Split('=');
+ if (keyValue.Length == 2)
+ {
+ yield return
+ keyValue[1].Equals(FormattingUtilities.JsonNullLiteral, StringComparison.Ordinal)
+ ? new KeyValuePair<string, string>(UriQueryUtility.UrlDecode(keyValue[0]), null)
+ : new KeyValuePair<string, string>(UriQueryUtility.UrlDecode(keyValue[0]), UriQueryUtility.UrlDecode(keyValue[1]));
+ }
+ else if (keyValue.Length == 1)
+ {
+ yield return new KeyValuePair<string, string>(null, UriQueryUtility.UrlDecode(keyValue[0]));
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Net.Http.Formatting/packages.config b/src/System.Net.Http.Formatting/packages.config
new file mode 100644
index 00000000..0fc85828
--- /dev/null
+++ b/src/System.Net.Http.Formatting/packages.config
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="Microsoft.Net.Http" version="2.0.20302.1" />
+ <package id="Newtonsoft.Json" version="4.0.8" />
+</packages> \ No newline at end of file
diff --git a/src/System.Web.Helpers/Chart/Chart.cs b/src/System.Web.Helpers/Chart/Chart.cs
new file mode 100644
index 00000000..ba8a5b79
--- /dev/null
+++ b/src/System.Web.Helpers/Chart/Chart.cs
@@ -0,0 +1,792 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.IO;
+using System.Text;
+using System.Web.Helpers.Resources;
+using System.Web.Hosting;
+using System.Web.UI.DataVisualization.Charting;
+using System.Web.UI.WebControls;
+using System.Web.WebPages;
+using System.Xml;
+using Microsoft.Internal.Web.Utils;
+using DV = System.Web.UI.DataVisualization.Charting;
+
+namespace System.Web.Helpers
+{
+ // Post-Beta Work:
+ // -DataBind and Points.DataBind - need to find scenarios
+ // -Elements: Annotations, MapAreas
+ // -Interactivity / AJAX support?
+ public class Chart
+ {
+ private readonly int _height;
+ private readonly int _width;
+ private readonly string _themePath;
+ private readonly string _theme;
+
+ private readonly List<LegendData> _legends = new List<LegendData>();
+ private readonly List<SeriesData> _series = new List<SeriesData>();
+ private readonly List<TitleData> _titles = new List<TitleData>();
+
+ private HttpContextBase _httpContext;
+ private VirtualPathProvider _virtualPathProvider;
+
+ private string _path;
+
+ private DataSourceData _dataSource;
+ [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", Target = "_xAxis, _yAxis",
+ Justification = "These names make most sense.")]
+ private ChartAxisData _xAxis, _yAxis;
+
+#if CODE_COVERAGE
+ [ExcludeFromCodeCoverage]
+#endif
+
+ /// <param name="width">Chart width in pixels.</param>
+ /// <param name="height">Chart height in pixels.</param>
+ /// <param name="theme">String containing chart theme definition. Chart's theme defines properties like colors, positions, etc.
+ /// This parameter is primarily meant for one of the predefined Chart themes, however any valid chart theme is acceptable.</param>
+ /// <param name="themePath">Path to a file containing definition of chart theme, default is none.</param>
+ /// <remarks>Both the theme and themePath parameters can be specified. In this case, the Chart class applies the theme xml first
+ /// followed by the content of file at themePath.
+ /// </remarks>
+ /// <example>
+ /// Chart(100, 100, theme: ChartTheme.Blue)
+ /// Chart(100, 100, theme: ChartTheme.Vanilla, themePath: "my-theme.xml")
+ /// Chart(100, 100, theme: ".... definition inline ...." )
+ /// Chart(100, 100, themePath: "my-theme.xml")
+ /// Any valid theme definition can be used as content of the file specified in themePath
+ /// </example>
+ public Chart(
+ int width,
+ int height,
+ string theme = null,
+ string themePath = null)
+ : this(GetDefaultContext(), HostingEnvironment.VirtualPathProvider, width, height, theme, themePath)
+ {
+ }
+
+ internal Chart(HttpContextBase httpContext, VirtualPathProvider virtualPathProvider, int width, int height,
+ string theme = null, string themePath = null)
+ {
+ Debug.Assert(httpContext != null);
+
+ if (width < 0)
+ {
+ throw new ArgumentOutOfRangeException("width", String.Format(
+ CultureInfo.CurrentCulture,
+ CommonResources.Argument_Must_Be_GreaterThanOrEqualTo,
+ 0));
+ }
+ if (height < 0)
+ {
+ throw new ArgumentOutOfRangeException("height", String.Format(
+ CultureInfo.CurrentCulture,
+ CommonResources.Argument_Must_Be_GreaterThanOrEqualTo,
+ 0));
+ }
+
+ _httpContext = httpContext;
+ _virtualPathProvider = virtualPathProvider;
+ _width = width;
+ _height = height;
+ _theme = theme;
+
+ // path must be app-relative in case chart is rendered from handler in different directory
+ if (!String.IsNullOrEmpty(themePath))
+ {
+ _themePath = VirtualPathUtil.ResolvePath(TemplateStack.GetCurrentTemplate(httpContext), httpContext, themePath);
+ if (!_virtualPathProvider.FileExists(_themePath))
+ {
+ throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, HelpersResources.Chart_ThemeFileNotFound, _themePath), "themePath");
+ }
+ }
+ }
+
+ public string FileName
+ {
+ get { return _path; }
+ }
+
+ public int Height
+ {
+ get { return _height; }
+ }
+
+ public int Width
+ {
+ get { return _width; }
+ }
+
+ /// <param name="title">Legend title.</param>
+ /// <param name="name">Legend name.</param>
+ public Chart AddLegend(
+ string title = null,
+ string name = null)
+ {
+ _legends.Add(new LegendData
+ {
+ Name = name,
+ Title = title
+ });
+ return this;
+ }
+
+ /// <param name="name">Series name.</param>
+ /// <param name="chartType">Chart type (see: SeriesChartType).</param>
+ /// <param name="chartArea">Chart area where the series is displayed.</param>
+ /// <param name="axisLabel">Axis label for the series.</param>
+ /// <param name="legend">Legend for the series.</param>
+ /// <param name="markerStep">Axis marker step.</param>
+ /// <param name="xValue">X data source, if data-binding the series.</param>
+ /// <param name="xField">Column for the X data points, if data-binding the series.</param>
+ /// <param name="yValues">Y data source(s), if data-binding the series.</param>
+ /// <param name="yFields">Column(s) for the Y data points, if data-binding the series.</param>
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "x",
+ Justification = "Name based on X-axis. Suppressed in source because this is a one-time occurrence")]
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "y",
+ Justification = "Name based on Y-axis. Suppressed in source because this is a one-time occurrence")]
+ [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", Target = "xValue, xField, yValues, yFields",
+ Justification = "These names cannot be changed because this is a public method.")]
+ public Chart AddSeries(
+ string name = null,
+ string chartType = "Column",
+ string chartArea = null,
+ string axisLabel = null,
+ string legend = null,
+ int markerStep = 1,
+ IEnumerable xValue = null,
+ string xField = null,
+ IEnumerable yValues = null,
+ string yFields = null)
+ {
+ if (String.IsNullOrEmpty(chartType))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "chartType");
+ }
+
+ DataSourceData dataSource = null;
+ if (yValues != null)
+ {
+ dataSource = new DataSourceData
+ {
+ XDataSource = xValue,
+ XField = xField,
+ DataSource = yValues,
+ YFields = yFields
+ };
+ }
+
+ _series.Add(new SeriesData
+ {
+ Name = name,
+ ChartType = ConvertStringArgument<SeriesChartType>("chartType", chartType),
+ ChartArea = chartArea,
+ AxisLabel = axisLabel,
+ Legend = legend,
+ MarkerStep = markerStep,
+ DataSource = dataSource
+ });
+ return this;
+ }
+
+ /// <param name="text">Title text.</param>
+ /// <param name="name">Title name.</param>
+ public Chart AddTitle(
+ string text = null,
+ string name = null)
+ {
+ _titles.Add(new TitleData
+ {
+ Name = name,
+ Text = text
+ });
+ return this;
+ }
+
+ /// <param name="title">Title for X-axis</param>
+ /// <param name="min">The minimum value on X-axis. Default 0</param>
+ /// <param name="max">The maximum value on X-axis. Default NaN</param>
+ public Chart SetXAxis(
+ string title = "",
+ double min = 0,
+ double max = Double.NaN)
+ {
+ _xAxis = new ChartAxisData { Title = title, Minimum = min, Maximum = max };
+ return this;
+ }
+
+ /// <param name="title">Title for Y-axis</param>
+ /// <param name="min">The minimum value on Y-axis. Default 0</param>
+ /// <param name="max">The maximum value on Y-axis. Default NaN</param>
+ public Chart SetYAxis(
+ string title = "",
+ double min = 0,
+ double max = Double.NaN)
+ {
+ _yAxis = new ChartAxisData { Title = title, Minimum = min, Maximum = max };
+ return this;
+ }
+
+ /// <summary>
+ /// Data-binds the chart by grouping values in a series. The series will be created by the chart.
+ /// </summary>
+ /// <param name="dataSource">Chart data source.</param>
+ /// <param name="groupByField">Column which series should be grouped by.</param>
+ /// <param name="xField">Column for the X data points.</param>
+ /// <param name="yFields">Column(s) for the Y data points, separated by comma.</param>
+ /// <param name="otherFields"></param>
+ /// <param name="pointSortOrder">Sort order (see: PointSortOrder).</param>
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "x",
+ Justification = "Name based on X-axis. Suppressed in source because this is a one-time occurrence")]
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "y",
+ Justification = "Name based on Y-axis. Suppressed in source because this is a one-time occurrence")]
+ [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", Target = "xField, yFields",
+ Justification = "These names cannot be changed because this is a public method.")]
+ public Chart DataBindCrossTable(IEnumerable dataSource, string groupByField, string xField, string yFields,
+ string otherFields = null, string pointSortOrder = "Ascending")
+ {
+ if (dataSource == null)
+ {
+ throw new ArgumentNullException("dataSource");
+ }
+ if (dataSource is string)
+ {
+ throw new ArgumentException(HelpersResources.Chart_ExceptionDataBindSeriesToString, "dataSource");
+ }
+ if (String.IsNullOrEmpty(groupByField))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "groupByField");
+ }
+ if (String.IsNullOrEmpty(yFields))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "yFields");
+ }
+
+ _dataSource = new DataSourceData
+ {
+ DataSource = dataSource,
+ GroupByField = groupByField,
+ XField = xField,
+ YFields = yFields,
+ OtherFields = otherFields,
+ PointSortOrder = ConvertStringArgument<PointSortOrder>("pointSortOrder", pointSortOrder)
+ };
+ return this;
+ }
+
+ /// <summary>
+ /// Data-binds the chart using a data source, with multiple y values supported. The series will be created by the chart.
+ /// </summary>
+ /// <param name="dataSource">Chart data source.</param>
+ /// <param name="xField">Column for the X data points.</param>
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "x",
+ Justification = "Name based on X-axis. Suppressed in source because this is a one-time occurrence")]
+ [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", Target = "xField",
+ Justification = "These names cannot be changed because this is a public method.")]
+ public Chart DataBindTable(IEnumerable dataSource, string xField = null)
+ {
+ if (dataSource == null)
+ {
+ throw new ArgumentNullException("dataSource");
+ }
+ if (dataSource is string)
+ {
+ throw new ArgumentException(HelpersResources.Chart_ExceptionDataBindSeriesToString, "dataSource");
+ }
+
+ _dataSource = new DataSourceData
+ {
+ DataBindTable = true,
+ DataSource = dataSource,
+ XField = xField
+ };
+ return this;
+ }
+
+ /// <summary>
+ /// Get the bytes for the chart image.
+ /// </summary>
+ /// <param name="format">Image format (see: ChartImageFormat).</param>
+ public byte[] GetBytes(string format = "jpeg")
+ {
+ var imageFormat = ConvertStringToChartImageFormat(format);
+ using (MemoryStream stream = new MemoryStream())
+ {
+ ExecuteChartAction(c =>
+ {
+ c.SaveImage(stream, imageFormat);
+ });
+ return stream.ToArray();
+ }
+ }
+
+#if CODE_COVERAGE
+ [ExcludeFromCodeCoverage]
+#endif
+
+ /// <summary>
+ /// Loads a chart from the cache. This can be used to render from an image handler.
+ /// </summary>
+ /// <param name="key">Cache key.</param>
+ public static Chart GetFromCache(string key)
+ {
+ return GetFromCache(GetDefaultContext(), key);
+ }
+
+ /// <summary>
+ /// Saves the chart image to a file.
+ /// </summary>
+ /// <param name="path">File path.</param>
+ /// <param name="format">Chart image format (see: ChartImageFormat).</param>
+ public Chart Save(string path, string format = "jpeg")
+ {
+ return Save(GetDefaultContext(), path, format);
+ }
+
+ internal Chart Save(HttpContextBase httpContext, string path, string format)
+ {
+ if (String.IsNullOrEmpty(path))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "path");
+ }
+ var imageFormat = ConvertStringToChartImageFormat(format);
+
+ _path = VirtualPathUtil.MapPath(httpContext, path);
+ ExecuteChartAction(c =>
+ {
+ c.RenderType = RenderType.ImageTag;
+ c.SaveImage(FileName, imageFormat);
+ });
+ return this;
+ }
+
+ /// <summary>
+ /// Saves the chart in cache. This can be used to render from an image handler.
+ /// </summary>
+ /// <param name="key">Cache key. Uses new GUID by default.</param>
+ /// <param name="minutesToCache">Number of minutes to save in cache.</param>
+ /// <param name="slidingExpiration">Whether a sliding expiration policy is used.</param>
+ /// <returns>Cache key.</returns>
+ public string SaveToCache(string key = null, int minutesToCache = 20, bool slidingExpiration = true)
+ {
+ if (String.IsNullOrEmpty(key))
+ {
+ key = GetUniqueKey();
+ }
+
+ WebCache.Set(key, this, minutesToCache, slidingExpiration);
+ return key;
+ }
+
+ /// <summary>
+ /// Saves the chart to the specified template file.
+ /// </summary>
+ /// <param name="path">XML template file path.</param>
+ public Chart SaveXml(string path)
+ {
+ return SaveXml(GetDefaultContext(), path);
+ }
+
+ /// <summary>
+ /// Saves the chart to the specified template file.
+ /// </summary>
+ /// <param name="httpContext">The <see cref="HttpContextBase"/>.</param>
+ /// <param name="path">XML template file path.</param>
+ internal Chart SaveXml(HttpContextBase httpContext, string path)
+ {
+ if (String.IsNullOrEmpty(path))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "path");
+ }
+
+ ExecuteChartAction(c =>
+ {
+ c.SaveXml(VirtualPathUtil.MapPath(httpContext, path));
+ });
+ return this;
+ }
+
+ public WebImage ToWebImage(string format = "jpeg")
+ {
+ return new WebImage(GetBytes(format));
+ }
+
+ /// <summary>
+ /// Writes the chart image to the response stream. This can be used to render from an image handler.
+ /// </summary>
+ /// <param name="format">Image format (see: ChartImageFormat).</param>
+ public Chart Write(string format = "jpeg")
+ {
+ var response = _httpContext.Response;
+ response.Charset = String.Empty;
+ response.ContentType = "image/" + NormalizeFormat(format);
+ response.BinaryWrite(GetBytes(format));
+ return this;
+ }
+
+#if CODE_COVERAGE
+ [ExcludeFromCodeCoverage]
+#endif
+
+ /// <summary>
+ /// Writes a chart stored in cache to the response stream. This can be used to render from an image handler.
+ /// </summary>
+ /// <param name="key">Cache key.</param>
+ /// <param name="format">Image format (see: ChartImageFormat).</param>
+ public static Chart WriteFromCache(string key, string format = "jpeg")
+ {
+ return WriteFromCache(GetDefaultContext(), key, format);
+ }
+
+ // create and execute an action against the WebForm control in a limited scope since the control is disposable.
+ internal void ExecuteChartAction(Action<UI.DataVisualization.Charting.Chart> action)
+ {
+ using (UI.DataVisualization.Charting.Chart chart = new UI.DataVisualization.Charting.Chart())
+ {
+ chart.Width = new Unit(_width);
+ chart.Height = new Unit(_height);
+
+ ApplyChartArea(chart);
+ ApplyLegends(chart);
+ ApplySeries(chart);
+ ApplyTitles(chart);
+
+ DataBindChart(chart);
+
+ // load the template last so that it can be applied to all the chart elements
+ LoadThemes(chart);
+
+ action(chart);
+ }
+ }
+
+ private void LoadThemes(UI.DataVisualization.Charting.Chart chart)
+ {
+ if (!String.IsNullOrEmpty(_theme))
+ {
+ using (MemoryStream memoryStream = new MemoryStream())
+ {
+ byte[] themeContent = Encoding.UTF8.GetBytes(_theme);
+ memoryStream.Write(themeContent, 0, themeContent.Length);
+ memoryStream.Seek(0, SeekOrigin.Begin);
+
+ LoadChartThemeFromFile(chart, memoryStream);
+ }
+ }
+
+ if (!String.IsNullOrEmpty(_themePath))
+ {
+ using (Stream stream = _virtualPathProvider.GetFile(_themePath).Open())
+ {
+ LoadChartThemeFromFile(chart, stream);
+ }
+ }
+ }
+
+ private static void LoadChartThemeFromFile(UI.DataVisualization.Charting.Chart chart, Stream templateStream)
+ {
+ // workarounds for Chart templating bugs mentioned in:
+ // http://social.msdn.microsoft.com/Forums/en-US/MSWinWebChart/thread/b50d5b7e-30e2-4948-af7a-370d9be1268a
+ chart.Serializer.Content = SerializationContents.All;
+ chart.Serializer.SerializableContent = String.Empty; // deserialize all content
+ chart.Serializer.IsTemplateMode = true;
+ chart.Serializer.IsResetWhenLoading = false;
+ // loading serializer with stream to avoid bug with template file getting locked in VS
+
+ // The default xml reader used by the serializer does not ignore comments
+ // Using the IsUnknownAttributeIgnored fixes this, but then it would give no feedback to the user
+ // if member names do not match the spelling and casing of Chart properties.
+ XmlReader reader = XmlReader.Create(templateStream, new XmlReaderSettings { IgnoreComments = true });
+ chart.Serializer.Load(reader);
+ }
+
+ internal static Chart GetFromCache(HttpContextBase context, string key)
+ {
+ Debug.Assert(context != null);
+
+ if (String.IsNullOrEmpty(key))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "key");
+ }
+
+ var chart = WebCache.Get(key) as Chart;
+ if (chart != null)
+ {
+ chart._httpContext = context;
+ }
+ return chart;
+ }
+
+ internal static Chart WriteFromCache(HttpContextBase context, string key, string format = "jpeg")
+ {
+ var chart = GetFromCache(context, key);
+ if (chart != null)
+ {
+ chart.Write(format);
+ }
+ return chart;
+ }
+
+ // Notes on ApplyXXX methods:
+ // Chart elements should be configured before they are added to the chart, otherwise there
+ // will be some rendering problems.
+ // We must catch all exceptions when configuring chart elements and dispose of them manually
+ // if they have not been added to the chart yet, otherwise FxCop will complain.
+
+ private void ApplyChartArea(UI.DataVisualization.Charting.Chart chart)
+ {
+ ChartArea chartArea = new ChartArea("Default");
+ try
+ {
+ ApplyAxis(chartArea.AxisX, _xAxis);
+ ApplyAxis(chartArea.AxisY, _yAxis);
+ chart.ChartAreas.Add(chartArea);
+ }
+ catch
+ {
+ // This is to appease FxCop
+ chartArea.Dispose();
+ throw;
+ }
+ }
+
+ private static void ApplyAxis(Axis axis, ChartAxisData axisData)
+ {
+ if (axisData == null)
+ {
+ return;
+ }
+
+ if (!String.IsNullOrEmpty(axisData.Title))
+ {
+ axis.Title = axisData.Title;
+ }
+ axis.Minimum = axisData.Minimum;
+ axis.Maximum = axisData.Maximum;
+ }
+
+ private void ApplyLegends(UI.DataVisualization.Charting.Chart chart)
+ {
+ foreach (var legendData in _legends)
+ {
+ var legend = new Legend();
+ try
+ {
+ legend.Name = legendData.Name ?? String.Empty;
+ legend.Title = legendData.Title ?? String.Empty;
+ }
+ catch (Exception)
+ {
+ // see notes above
+ legend.Dispose();
+ throw;
+ }
+ chart.Legends.Add(legend);
+ }
+ }
+
+ [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", Target = "yValues, yValuesArray",
+ Justification = "These names make the most sense.")]
+ private void ApplySeries(UI.DataVisualization.Charting.Chart chart)
+ {
+ foreach (var seriesData in _series)
+ {
+ var series = new Series();
+ try
+ {
+ series.AxisLabel = seriesData.AxisLabel ?? String.Empty;
+ series.ChartArea = seriesData.ChartArea ?? String.Empty;
+ series.ChartType = seriesData.ChartType;
+ series.Legend = seriesData.Legend ?? String.Empty;
+ series.MarkerStep = seriesData.MarkerStep;
+ series.Name = seriesData.Name ?? String.Empty;
+
+ // data-bind the series (todo - support o.Points.DataBind())
+ if (seriesData.DataSource != null)
+ {
+ if (String.IsNullOrEmpty(seriesData.DataSource.YFields))
+ {
+ var yValues = seriesData.DataSource.DataSource;
+ var yValuesArray = yValues as IEnumerable[];
+ if ((yValuesArray != null) && !(yValues is string[]))
+ {
+ series.Points.DataBindXY(seriesData.DataSource.XDataSource, yValuesArray);
+ }
+ else
+ {
+ series.Points.DataBindXY(seriesData.DataSource.XDataSource, yValues);
+ }
+ }
+ else
+ {
+ series.Points.DataBindXY(seriesData.DataSource.XDataSource, seriesData.DataSource.XField,
+ seriesData.DataSource.DataSource, seriesData.DataSource.YFields);
+ }
+ }
+ }
+ catch (Exception)
+ {
+ // see notes above
+ series.Dispose();
+ throw;
+ }
+ chart.Series.Add(series);
+ }
+ }
+
+ private void ApplyTitles(UI.DataVisualization.Charting.Chart chart)
+ {
+ foreach (var titleData in _titles)
+ {
+ var title = new Title();
+ try
+ {
+ title.Name = titleData.Name;
+ title.Text = titleData.Text;
+ }
+ catch (Exception)
+ {
+ // see notes above
+ title.Dispose();
+ throw;
+ }
+ chart.Titles.Add(title);
+ }
+ }
+
+ private static T ConvertStringArgument<T>(string paramName, string value)
+ {
+ object result;
+ if (!ConversionUtil.TryFromString(typeof(T), value, out result))
+ {
+ throw new ArgumentException(String.Format(CultureInfo.CurrentCulture,
+ HelpersResources.Chart_ArgumentConversionFailed, typeof(T).FullName), paramName);
+ }
+ return (T)result;
+ }
+
+ /// <summary>
+ /// Method to convert a string to a ChartImageFormat.
+ /// The chart image needs to be normalized to allow for alternate names such as 'jpg', 'xpng' etc
+ /// to be mapped to their appropriate ChartImageFormat.
+ /// </summary>
+ private static ChartImageFormat ConvertStringToChartImageFormat(string format)
+ {
+ object result;
+ format = NormalizeFormat(format);
+ if (!ConversionUtil.TryFromString(typeof(ChartImageFormat), format, out result))
+ {
+ throw new ArgumentException(String.Format(CultureInfo.CurrentCulture,
+ HelpersResources.Image_IncorrectImageFormat, format), "format");
+ }
+ return (ChartImageFormat)result;
+ }
+
+ private void DataBindChart(UI.DataVisualization.Charting.Chart chart)
+ {
+ // NOTE: WebForms chart will throw null refs if optional values are set to null
+ if (_dataSource != null)
+ {
+ if (!String.IsNullOrEmpty(_dataSource.GroupByField))
+ {
+ chart.DataBindCrossTable(
+ _dataSource.DataSource,
+ _dataSource.GroupByField,
+ _dataSource.XField ?? String.Empty,
+ _dataSource.YFields,
+ _dataSource.OtherFields ?? String.Empty,
+ _dataSource.PointSortOrder);
+ }
+ else if (_dataSource.DataBindTable)
+ {
+ chart.DataBindTable(
+ _dataSource.DataSource,
+ _dataSource.XField ?? String.Empty);
+ }
+ else
+ {
+ Debug.Assert(false, "Chart.DataBind was removed - should not reach here");
+ //chart.DataSource = _dataSource.DataSource;
+ //chart.DataBind();
+ }
+ }
+ }
+
+#if CODE_COVERAGE
+ [ExcludeFromCodeCoverage]
+#endif
+
+ private static HttpContextBase GetDefaultContext()
+ {
+ return new HttpContextWrapper(HttpContext.Current);
+ }
+
+ // review: should GUIDs be used in a handler's querystring?
+ private static string GetUniqueKey()
+ {
+ return Guid.NewGuid().ToString();
+ }
+
+ private static string NormalizeFormat(string format)
+ {
+ if (String.IsNullOrEmpty(format))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "format");
+ }
+ if (format.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
+ {
+ format = format.Substring(6);
+ }
+ return ConversionUtil.NormalizeImageFormat(format);
+ }
+
+ // data-binding can be done through Chart or individual Series
+ private class DataSourceData
+ {
+ public bool DataBindTable { get; set; }
+ public IEnumerable DataSource { get; set; }
+ public string GroupByField { get; set; }
+ public string OtherFields { get; set; }
+ public string XField { get; set; }
+ public string YFields { get; set; }
+ public PointSortOrder PointSortOrder { get; set; }
+
+ // optional XValue for Series.Points.DataBindXY only:
+ public IEnumerable XDataSource { get; set; }
+ }
+
+ private class LegendData
+ {
+ public string Name { get; set; }
+ public string Title { get; set; }
+ }
+
+ private class SeriesData
+ {
+ public string AxisLabel { get; set; }
+ public string ChartArea { get; set; }
+ public SeriesChartType ChartType { get; set; }
+ public string Legend { get; set; }
+ public int MarkerStep { get; set; }
+ public string Name { get; set; }
+ public DataSourceData DataSource { get; set; }
+ }
+
+ private class TitleData
+ {
+ public string Name { get; set; }
+ public string Text { get; set; }
+ }
+
+ private class ChartAxisData
+ {
+ public double Minimum { get; set; }
+ public double Maximum { get; set; }
+ public string Title { get; set; }
+ }
+ }
+}
diff --git a/src/System.Web.Helpers/Chart/ChartTheme.cs b/src/System.Web.Helpers/Chart/ChartTheme.cs
new file mode 100644
index 00000000..b4aa2d06
--- /dev/null
+++ b/src/System.Web.Helpers/Chart/ChartTheme.cs
@@ -0,0 +1,84 @@
+namespace System.Web.Helpers
+{
+ public static class ChartTheme
+ {
+ // Review: Need better names.
+
+ public const string Blue =
+ @"<Chart BackColor=""#D3DFF0"" BackGradientStyle=""TopBottom"" BackSecondaryColor=""White"" BorderColor=""26, 59, 105"" BorderlineDashStyle=""Solid"" BorderWidth=""2"" Palette=""BrightPastel"">
+ <ChartAreas>
+ <ChartArea Name=""Default"" _Template_=""All"" BackColor=""64, 165, 191, 228"" BackGradientStyle=""TopBottom"" BackSecondaryColor=""White"" BorderColor=""64, 64, 64, 64"" BorderDashStyle=""Solid"" ShadowColor=""Transparent"" />
+ </ChartAreas>
+ <Legends>
+ <Legend _Template_=""All"" BackColor=""Transparent"" Font=""Trebuchet MS, 8.25pt, style=Bold"" IsTextAutoFit=""False"" />
+ </Legends>
+ <BorderSkin SkinStyle=""Emboss"" />
+ </Chart>";
+
+ public const string Green =
+ @"<Chart BackColor=""#C9DC87"" BackGradientStyle=""TopBottom"" BorderColor=""181, 64, 1"" BorderWidth=""2"" BorderlineDashStyle=""Solid"" Palette=""BrightPastel"">
+ <ChartAreas>
+ <ChartArea Name=""Default"" _Template_=""All"" BackColor=""Transparent"" BackSecondaryColor=""White"" BorderColor=""64, 64, 64, 64"" BorderDashStyle=""Solid"" ShadowColor=""Transparent"">
+ <AxisY LineColor=""64, 64, 64, 64"">
+ <MajorGrid Interval=""Auto"" LineColor=""64, 64, 64, 64"" />
+ <LabelStyle Font=""Trebuchet MS, 8.25pt, style=Bold"" />
+ </AxisY>
+ <AxisX LineColor=""64, 64, 64, 64"">
+ <MajorGrid LineColor=""64, 64, 64, 64"" />
+ <LabelStyle Font=""Trebuchet MS, 8.25pt, style=Bold"" />
+ </AxisX>
+ <Area3DStyle Inclination=""15"" IsClustered=""False"" IsRightAngleAxes=""False"" Perspective=""10"" Rotation=""10"" WallWidth=""0"" />
+ </ChartArea>
+ </ChartAreas>
+ <Legends>
+ <Legend _Template_=""All"" Alignment=""Center"" BackColor=""Transparent"" Docking=""Bottom"" Font=""Trebuchet MS, 8.25pt, style=Bold"" IsTextAutoFit =""False"" LegendStyle=""Row"">
+ </Legend>
+ </Legends>
+ <BorderSkin SkinStyle=""Emboss"" />
+</Chart>";
+
+ public const string Vanilla =
+ @"<Chart Palette=""SemiTransparent"" BorderColor=""#000"" BorderWidth=""2"" BorderlineDashStyle=""Solid"">
+<ChartAreas>
+ <ChartArea _Template_=""All"" Name=""Default"">
+ <AxisX>
+ <MinorGrid Enabled=""False"" />
+ <MajorGrid Enabled=""False"" />
+ </AxisX>
+ <AxisY>
+ <MajorGrid Enabled=""False"" />
+ <MinorGrid Enabled=""False"" />
+ </AxisY>
+ </ChartArea>
+</ChartAreas>
+</Chart>";
+
+ public const string Vanilla3D =
+ @"<Chart BackColor=""#555"" BackGradientStyle=""TopBottom"" BorderColor=""181, 64, 1"" BorderWidth=""2"" BorderlineDashStyle=""Solid"" Palette=""SemiTransparent"" AntiAliasing=""All"">
+ <ChartAreas>
+ <ChartArea Name=""Default"" _Template_=""All"" BackColor=""Transparent"" BackSecondaryColor=""White"" BorderColor=""64, 64, 64, 64"" BorderDashStyle=""Solid"" ShadowColor=""Transparent"">
+ <Area3DStyle LightStyle=""Simplistic"" Enable3D=""True"" Inclination=""30"" IsClustered=""False"" IsRightAngleAxes=""False"" Perspective=""10"" Rotation=""-30"" WallWidth=""0"" />
+ </ChartArea>
+ </ChartAreas>
+</Chart>";
+
+ public const string Yellow =
+ @"<Chart BackColor=""#FADA5E"" BackGradientStyle=""TopBottom"" BorderColor=""#B8860B"" BorderWidth=""2"" BorderlineDashStyle=""Solid"" Palette=""EarthTones"">
+ <ChartAreas>
+ <ChartArea Name=""Default"" _Template_=""All"" BackColor=""Transparent"" BackSecondaryColor=""White"" BorderColor=""64, 64, 64, 64"" BorderDashStyle=""Solid"" ShadowColor=""Transparent"">
+ <AxisY>
+ <LabelStyle Font=""Trebuchet MS, 8.25pt, style=Bold"" />
+ </AxisY>
+ <AxisX LineColor=""64, 64, 64, 64"">
+ <LabelStyle Font=""Trebuchet MS, 8.25pt, style=Bold"" />
+ </AxisX>
+ </ChartArea>
+ </ChartAreas>
+ <Legends>
+ <Legend _Template_=""All"" BackColor=""Transparent"" Docking=""Bottom"" Font=""Trebuchet MS, 8.25pt, style=Bold"" LegendStyle=""Row"">
+ </Legend>
+ </Legends>
+ <BorderSkin SkinStyle=""Emboss"" />
+</Chart>";
+ }
+}
diff --git a/src/System.Web.Helpers/Common/VirtualPathUtil.cs b/src/System.Web.Helpers/Common/VirtualPathUtil.cs
new file mode 100644
index 00000000..88584cee
--- /dev/null
+++ b/src/System.Web.Helpers/Common/VirtualPathUtil.cs
@@ -0,0 +1,78 @@
+using System.Diagnostics;
+using System.Globalization;
+using System.IO;
+using System.Web.Helpers.Resources;
+using System.Web.WebPages;
+
+namespace System.Web.Helpers
+{
+ internal static class VirtualPathUtil
+ {
+ /// <summary>
+ /// Resolves and maps a path (physical or virtual) to a physical path on the server.
+ /// </summary>
+ /// <param name="httpContext">The <see cref="HttpContextBase"/>.</param>
+ /// <param name="path">Either a physical rooted path or a virtual path to be mapped.
+ /// Physical paths are returned without modifications. Virtual paths are resolved relative to the current executing page.
+ /// </param>
+ /// <remarks>Result of this call should not be shown to the user (e.g. in an exception message) since
+ /// it could be security sensitive. But we need to pass this result to the file APIs like File.WriteAllBytes
+ /// which will show it if exceptions are raised from them. Unfortunately VirtualPathProvider doesn't have
+ /// APIs for writing so we can't use that.</remarks>
+ public static string MapPath(HttpContextBase httpContext, string path)
+ {
+ Debug.Assert(!String.IsNullOrEmpty(path));
+
+ if (Path.IsPathRooted(path))
+ {
+ return path;
+ }
+
+ // There is no TryMapPath API so we have to catch HttpException if we want to
+ // throw ArgumentException instead.
+ try
+ {
+ return httpContext.Request.MapPath(ResolvePath(TemplateStack.GetCurrentTemplate(httpContext), httpContext, path));
+ }
+ catch (HttpException)
+ {
+ throw new ArgumentException(
+ String.Format(CultureInfo.InvariantCulture, HelpersResources.PathUtils_IncorrectPath, path), "path");
+ }
+ }
+
+ /// <summary>
+ /// Resolves path relative to the current executing page
+ /// </summary>
+ public static string ResolvePath(string virtualPath)
+ {
+ if (String.IsNullOrEmpty(virtualPath))
+ {
+ return virtualPath;
+ }
+
+ if (HttpContext.Current == null)
+ {
+ return virtualPath;
+ }
+ var httpContext = new HttpContextWrapper(HttpContext.Current);
+ return ResolvePath(TemplateStack.GetCurrentTemplate(httpContext), httpContext, virtualPath);
+ }
+
+ internal static string ResolvePath(ITemplateFile templateFile, HttpContextBase httpContext, string virtualPath)
+ {
+ Debug.Assert(!String.IsNullOrEmpty(virtualPath));
+ string basePath;
+ if (templateFile != null)
+ {
+ // If a page is available resolve paths relative to it.
+ basePath = templateFile.TemplateInfo.VirtualPath;
+ }
+ else
+ {
+ basePath = httpContext.Request.AppRelativeCurrentExecutionFilePath;
+ }
+ return VirtualPathUtility.Combine(basePath, virtualPath);
+ }
+ }
+}
diff --git a/src/System.Web.Helpers/ConversionUtil.cs b/src/System.Web.Helpers/ConversionUtil.cs
new file mode 100644
index 00000000..95f348b7
--- /dev/null
+++ b/src/System.Web.Helpers/ConversionUtil.cs
@@ -0,0 +1,201 @@
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Drawing;
+using System.Drawing.Imaging;
+using System.Reflection;
+
+namespace System.Web.Helpers
+{
+ internal static class ConversionUtil
+ {
+ private static MethodInfo _stringToEnumMethod;
+
+ internal static string ToString<T>(T obj)
+ {
+ Type type = typeof(T);
+ if (type.IsEnum)
+ {
+ return obj.ToString();
+ }
+ TypeConverter converter = TypeDescriptor.GetConverter(type);
+ if ((converter != null) && (converter.CanConvertTo(typeof(string))))
+ {
+ return converter.ConvertToInvariantString(obj);
+ }
+ return null;
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes",
+ Justification = "TypeConverter throws System.Exception instead of a more specific one.")]
+ internal static bool TryFromString(Type type, string value, out object result)
+ {
+ result = null;
+ if (type == typeof(string))
+ {
+ result = value;
+ return true;
+ }
+ if (type.IsEnum)
+ {
+ return TryFromStringToEnumHelper(type, value, out result);
+ }
+ if (type == typeof(Color))
+ {
+ Color color;
+ bool rval = TryFromStringToColor(value, out color);
+ result = color;
+ return rval;
+ }
+ // TypeConverter doesn't really have TryConvert APIs. We should avoid TypeConverter.IsValid
+ // which performs a duplicate conversion, and just handle the general exception ourselves.
+ TypeConverter converter = TypeDescriptor.GetConverter(type);
+ if ((converter != null) && converter.CanConvertFrom(typeof(string)))
+ {
+ try
+ {
+ result = converter.ConvertFromInvariantString(value);
+ return true;
+ }
+ catch
+ {
+ // Do nothing
+ }
+ }
+ return false;
+ }
+
+ internal static bool TryFromStringToEnum<T>(string value, out T result) where T : struct
+ {
+ return Enum.TryParse(value, ignoreCase: true, result: out result);
+ }
+
+ private static bool TryFromStringToEnumHelper(Type enumType, string value, out object result)
+ {
+ result = null;
+ if (_stringToEnumMethod == null)
+ {
+ _stringToEnumMethod = typeof(ConversionUtil).GetMethod("TryFromStringToEnum",
+ BindingFlags.Static | BindingFlags.NonPublic);
+ Debug.Assert(_stringToEnumMethod != null);
+ }
+ var args = new object[] { value, null };
+ var rval = (bool)_stringToEnumMethod.MakeGenericMethod(enumType).Invoke(null, args);
+ result = args[1];
+ return rval;
+ }
+
+ internal static bool TryFromStringToFontFamily(string fontFamily, out FontFamily result)
+ {
+ result = null;
+ bool converted = false;
+ foreach (FontFamily fontFamilyTemp in FontFamily.Families)
+ {
+ if (fontFamily.Equals(fontFamilyTemp.Name, StringComparison.OrdinalIgnoreCase))
+ {
+ result = fontFamilyTemp;
+ converted = true;
+ break;
+ }
+ }
+ return converted;
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes",
+ Justification = "TypeConverter throws System.Exception instad of a more specific one.")]
+ internal static bool TryFromStringToColor(string value, out Color result)
+ {
+ result = default(Color);
+
+ // Parse color specified as hex number
+ if (value.StartsWith("#", StringComparison.OrdinalIgnoreCase))
+ {
+ // Only allow colors in form of #RRGGBB or #RGB
+ if ((value.Length != 7) && (value.Length != 4))
+ {
+ return false;
+ }
+
+ // Expand short version
+ if (value.Length == 4)
+ {
+ char[] newValue = new char[7];
+ newValue[0] = '#';
+ newValue[1] = newValue[2] = value[1];
+ newValue[3] = newValue[4] = value[2];
+ newValue[5] = newValue[6] = value[3];
+ value = new string(newValue);
+ }
+ }
+
+ TypeConverter converter = TypeDescriptor.GetConverter(typeof(Color));
+ Debug.Assert((converter != null) && (converter.CanConvertFrom(typeof(string))));
+
+ // There are no TryConvert APIs on TypeConverter so we have to catch exception.
+ // In addition to that, invalid conversion just throws System.Exception with misleading message,
+ // instead of a more specific exception type.
+ try
+ {
+ result = (Color)converter.ConvertFromInvariantString(value);
+ }
+ catch (Exception)
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase",
+ Justification = "Format names are used in Http headers and are usually specified in lower case")]
+ internal static string NormalizeImageFormat(string value)
+ {
+ value = value.ToLowerInvariant();
+ switch (value)
+ {
+ case "jpeg":
+ case "jpg":
+ case "pjpeg":
+ return "jpeg";
+
+ case "png":
+ case "x-png":
+ return "png";
+
+ case "icon":
+ case "ico":
+ return "icon";
+ }
+ return value;
+ }
+
+ internal static bool TryFromStringToImageFormat(string value, out ImageFormat result)
+ {
+ result = default(ImageFormat);
+
+ if (String.IsNullOrEmpty(value))
+ {
+ return false;
+ }
+ if (value.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
+ {
+ value = value.Substring("image/".Length);
+ }
+ value = NormalizeImageFormat(value);
+
+ TypeConverter converter = TypeDescriptor.GetConverter(typeof(ImageFormat));
+ Debug.Assert((converter != null) && (converter.CanConvertFrom(typeof(string))));
+
+ try
+ {
+ result = (ImageFormat)converter.ConvertFromInvariantString(value);
+ }
+ catch (NotSupportedException)
+ {
+ return false;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/src/System.Web.Helpers/Crypto.cs b/src/System.Web.Helpers/Crypto.cs
new file mode 100644
index 00000000..f745388a
--- /dev/null
+++ b/src/System.Web.Helpers/Crypto.cs
@@ -0,0 +1,179 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Runtime.CompilerServices;
+using System.Security.Cryptography;
+using System.Text;
+using System.Web.Helpers.Resources;
+
+namespace System.Web.Helpers
+{
+ public static class Crypto
+ {
+ private const int PBKDF2IterCount = 1000; // default for Rfc2898DeriveBytes
+ private const int PBKDF2SubkeyLength = 256 / 8; // 256 bits
+ private const int SaltSize = 128 / 8; // 128 bits
+
+ [SuppressMessage("Microsoft.Naming", "CA1720:IdentifiersShouldNotContainTypeNames", MessageId = "byte", Justification = "It really is a byte length")]
+ internal static byte[] GenerateSaltInternal(int byteLength = SaltSize)
+ {
+ byte[] buf = new byte[byteLength];
+ using (var rng = new RNGCryptoServiceProvider())
+ {
+ rng.GetBytes(buf);
+ }
+ return buf;
+ }
+
+ [SuppressMessage("Microsoft.Naming", "CA1720:IdentifiersShouldNotContainTypeNames", MessageId = "byte", Justification = "It really is a byte length")]
+ public static string GenerateSalt(int byteLength = SaltSize)
+ {
+ return Convert.ToBase64String(GenerateSaltInternal(byteLength));
+ }
+
+ public static string Hash(string input, string algorithm = "sha256")
+ {
+ if (input == null)
+ {
+ throw new ArgumentNullException("input");
+ }
+
+ return Hash(Encoding.UTF8.GetBytes(input), algorithm);
+ }
+
+ public static string Hash(byte[] input, string algorithm = "sha256")
+ {
+ if (input == null)
+ {
+ throw new ArgumentNullException("input");
+ }
+
+ using (HashAlgorithm alg = HashAlgorithm.Create(algorithm))
+ {
+ if (alg != null)
+ {
+ byte[] hashData = alg.ComputeHash(input);
+ return BinaryToHex(hashData);
+ }
+ else
+ {
+ throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, HelpersResources.Crypto_NotSupportedHashAlg, algorithm));
+ }
+ }
+ }
+
+ [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SHA", Justification = "Consistent with the Framework, which uses SHA")]
+ public static string SHA1(string input)
+ {
+ return Hash(input, "sha1");
+ }
+
+ [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SHA", Justification = "Consistent with the Framework, which uses SHA")]
+ public static string SHA256(string input)
+ {
+ return Hash(input, "sha256");
+ }
+
+ /* =======================
+ * HASHED PASSWORD FORMATS
+ * =======================
+ *
+ * Version 0:
+ * PBKDF2 with HMAC-SHA1, 128-bit salt, 256-bit subkey, 1000 iterations.
+ * (See also: SDL crypto guidelines v5.1, Part III)
+ * Format: { 0x00, salt, subkey }
+ */
+
+ public static string HashPassword(string password)
+ {
+ if (password == null)
+ {
+ throw new ArgumentNullException("password");
+ }
+
+ // Produce a version 0 (see comment above) password hash.
+ byte[] salt;
+ byte[] subkey;
+ using (var deriveBytes = new Rfc2898DeriveBytes(password, SaltSize, PBKDF2IterCount))
+ {
+ salt = deriveBytes.Salt;
+ subkey = deriveBytes.GetBytes(PBKDF2SubkeyLength);
+ }
+
+ byte[] outputBytes = new byte[1 + SaltSize + PBKDF2SubkeyLength];
+ Buffer.BlockCopy(salt, 0, outputBytes, 1, SaltSize);
+ Buffer.BlockCopy(subkey, 0, outputBytes, 1 + SaltSize, PBKDF2SubkeyLength);
+ return Convert.ToBase64String(outputBytes);
+ }
+
+ // hashedPassword must be of the format of HashWithPassword (salt + Hash(salt+input)
+ public static bool VerifyHashedPassword(string hashedPassword, string password)
+ {
+ if (hashedPassword == null)
+ {
+ throw new ArgumentNullException("hashedPassword");
+ }
+ if (password == null)
+ {
+ throw new ArgumentNullException("password");
+ }
+
+ byte[] hashedPasswordBytes = Convert.FromBase64String(hashedPassword);
+
+ // Verify a version 0 (see comment above) password hash.
+
+ if (hashedPasswordBytes.Length != (1 + SaltSize + PBKDF2SubkeyLength) || hashedPasswordBytes[0] != 0x00)
+ {
+ // Wrong length or version header.
+ return false;
+ }
+
+ byte[] salt = new byte[SaltSize];
+ Buffer.BlockCopy(hashedPasswordBytes, 1, salt, 0, SaltSize);
+ byte[] storedSubkey = new byte[PBKDF2SubkeyLength];
+ Buffer.BlockCopy(hashedPasswordBytes, 1 + SaltSize, storedSubkey, 0, PBKDF2SubkeyLength);
+
+ byte[] generatedSubkey;
+ using (var deriveBytes = new Rfc2898DeriveBytes(password, salt, PBKDF2IterCount))
+ {
+ generatedSubkey = deriveBytes.GetBytes(PBKDF2SubkeyLength);
+ }
+ return ByteArraysEqual(storedSubkey, generatedSubkey);
+ }
+
+ internal static string BinaryToHex(byte[] data)
+ {
+ char[] hex = new char[data.Length * 2];
+
+ for (int iter = 0; iter < data.Length; iter++)
+ {
+ byte hexChar = ((byte)(data[iter] >> 4));
+ hex[iter * 2] = (char)(hexChar > 9 ? hexChar + 0x37 : hexChar + 0x30);
+ hexChar = ((byte)(data[iter] & 0xF));
+ hex[(iter * 2) + 1] = (char)(hexChar > 9 ? hexChar + 0x37 : hexChar + 0x30);
+ }
+ return new string(hex);
+ }
+
+ // Compares two byte arrays for equality. The method is specifically written so that the loop is not optimized.
+ [MethodImpl(MethodImplOptions.NoOptimization)]
+ private static bool ByteArraysEqual(byte[] a, byte[] b)
+ {
+ if (ReferenceEquals(a, b))
+ {
+ return true;
+ }
+
+ if (a == null || b == null || a.Length != b.Length)
+ {
+ return false;
+ }
+
+ bool areSame = true;
+ for (int i = 0; i < a.Length; i++)
+ {
+ areSame &= (a[i] == b[i]);
+ }
+ return areSame;
+ }
+ }
+}
diff --git a/src/System.Web.Helpers/DynamicJavaScriptConverter.cs b/src/System.Web.Helpers/DynamicJavaScriptConverter.cs
new file mode 100644
index 00000000..5913e069
--- /dev/null
+++ b/src/System.Web.Helpers/DynamicJavaScriptConverter.cs
@@ -0,0 +1,46 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Dynamic;
+using System.Web.Script.Serialization;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.Helpers
+{
+ /// <summary>
+ /// Converter that knows how to get the member values from a dynamic object.
+ /// </summary>
+ internal class DynamicJavaScriptConverter : JavaScriptConverter
+ {
+ public override IEnumerable<Type> SupportedTypes
+ {
+ get
+ {
+ // REVIEW: For some reason the converters don't pick up interfaces
+ yield return typeof(IDynamicMetaObjectProvider);
+ yield return typeof(DynamicObject);
+ }
+ }
+
+ public override object Deserialize(IDictionary<string, object> dictionary, Type type, JavaScriptSerializer serializer)
+ {
+ throw new NotSupportedException();
+ }
+
+ public override IDictionary<string, object> Serialize(object obj, JavaScriptSerializer serializer)
+ {
+ var values = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
+ var memberNames = DynamicHelper.GetMemberNames(obj);
+
+ // This should never happen
+ Debug.Assert(memberNames != null);
+
+ // Get the value for each member in the dynamic object
+ foreach (string memberName in memberNames)
+ {
+ values[memberName] = DynamicHelper.GetMemberValue(obj, memberName);
+ }
+
+ return values;
+ }
+ }
+}
diff --git a/src/System.Web.Helpers/DynamicJsonArray.cs b/src/System.Web.Helpers/DynamicJsonArray.cs
new file mode 100644
index 00000000..739ce1e6
--- /dev/null
+++ b/src/System.Web.Helpers/DynamicJsonArray.cs
@@ -0,0 +1,78 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Dynamic;
+using System.Linq;
+
+namespace System.Web.Helpers
+{
+ [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix", Justification = "This class isn't meant to be used directly")]
+ public class DynamicJsonArray : DynamicObject, IEnumerable<object>
+ {
+ private readonly object[] _arrayValues;
+
+ public DynamicJsonArray(object[] arrayValues)
+ {
+ Debug.Assert(arrayValues != null);
+ _arrayValues = arrayValues.Select(Json.WrapObject).ToArray();
+ }
+
+ public int Length
+ {
+ get { return _arrayValues.Length; }
+ }
+
+ public dynamic this[int index]
+ {
+ get { return _arrayValues[index]; }
+ set { _arrayValues[index] = Json.WrapObject(value); }
+ }
+
+ public override bool TryConvert(ConvertBinder binder, out object result)
+ {
+ if (_arrayValues.GetType().IsAssignableFrom(binder.Type))
+ {
+ result = _arrayValues;
+ return true;
+ }
+ return base.TryConvert(binder, out result);
+ }
+
+ public override bool TryGetMember(GetMemberBinder binder, out object result)
+ {
+ // Testing for members should never throw. This is important when dealing with
+ // services that return different json results. Testing for a member shouldn't throw,
+ // it should just return null (or undefined)
+ result = null;
+ return true;
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ return _arrayValues.GetEnumerator();
+ }
+
+ private IEnumerable<object> GetEnumerable()
+ {
+ return _arrayValues.AsEnumerable();
+ }
+
+ IEnumerator<object> IEnumerable<object>.GetEnumerator()
+ {
+ return GetEnumerable().GetEnumerator();
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2225:OperatorOverloadsHaveNamedAlternates", Justification = "This class isn't meant to be used directly")]
+ public static implicit operator object[](DynamicJsonArray obj)
+ {
+ return obj._arrayValues;
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2225:OperatorOverloadsHaveNamedAlternates", Justification = "This class isn't meant to be used directly")]
+ public static implicit operator Array(DynamicJsonArray obj)
+ {
+ return obj._arrayValues;
+ }
+ }
+}
diff --git a/src/System.Web.Helpers/DynamicJsonObject.cs b/src/System.Web.Helpers/DynamicJsonObject.cs
new file mode 100644
index 00000000..1acda878
--- /dev/null
+++ b/src/System.Web.Helpers/DynamicJsonObject.cs
@@ -0,0 +1,94 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Dynamic;
+using System.Globalization;
+using System.Linq;
+using System.Web.Helpers.Resources;
+
+namespace System.Web.Helpers
+{
+ // REVIEW: Consider implementing ICustomTypeDescriptor and IDictionary<string, object>
+ public class DynamicJsonObject : DynamicObject
+ {
+ private readonly IDictionary<string, object> _values;
+
+ public DynamicJsonObject(IDictionary<string, object> values)
+ {
+ Debug.Assert(values != null);
+ _values = values.ToDictionary(p => p.Key, p => Json.WrapObject(p.Value),
+ StringComparer.OrdinalIgnoreCase);
+ }
+
+ public override bool TryConvert(ConvertBinder binder, out object result)
+ {
+ result = null;
+ if (binder.Type.IsAssignableFrom(_values.GetType()))
+ {
+ result = _values;
+ }
+ else
+ {
+ throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, HelpersResources.Json_UnableToConvertType, binder.Type));
+ }
+ return true;
+ }
+
+ public override bool TryGetMember(GetMemberBinder binder, out object result)
+ {
+ result = GetValue(binder.Name);
+ return true;
+ }
+
+ public override bool TrySetMember(SetMemberBinder binder, object value)
+ {
+ _values[binder.Name] = Json.WrapObject(value);
+ return true;
+ }
+
+ public override bool TrySetIndex(SetIndexBinder binder, object[] indexes, object value)
+ {
+ string key = GetKey(indexes);
+ if (!String.IsNullOrEmpty(key))
+ {
+ _values[key] = Json.WrapObject(value);
+ }
+ return true;
+ }
+
+ public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result)
+ {
+ string key = GetKey(indexes);
+ result = null;
+ if (!String.IsNullOrEmpty(key))
+ {
+ result = GetValue(key);
+ }
+ return true;
+ }
+
+ private static string GetKey(object[] indexes)
+ {
+ if (indexes.Length == 1)
+ {
+ return (string)indexes[0];
+ }
+ // REVIEW: Should this throw?
+ return null;
+ }
+
+ public override IEnumerable<string> GetDynamicMemberNames()
+ {
+ return _values.Keys;
+ }
+
+ private object GetValue(string name)
+ {
+ object result;
+ if (_values.TryGetValue(name, out result))
+ {
+ return result;
+ }
+ return null;
+ }
+ }
+}
diff --git a/src/System.Web.Helpers/GlobalSuppressions.cs b/src/System.Web.Helpers/GlobalSuppressions.cs
new file mode 100644
index 00000000..f2a21ee3
--- /dev/null
+++ b/src/System.Web.Helpers/GlobalSuppressions.cs
@@ -0,0 +1,13 @@
+// This file is used by Code Analysis to maintain SuppressMessage
+// attributes that are applied to this project.
+// Project-level suppressions either have no target or are given
+// a specific target and scoped to a namespace, type, member, etc.
+//
+// To add a suppression to this file, right-click the message in the
+// Error List, point to "Suppress Message(s)", and click
+// "In Project Suppression File".
+// You do not need to add suppressions to this file manually.
+
+using System.Diagnostics.CodeAnalysis;
+
+[assembly: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId = "sha", Scope = "resource", Target = "System.Web.Helpers.Resources.HelpersResources.resources", Justification = "sha is the algorithm")]
diff --git a/src/System.Web.Helpers/HtmlElement.cs b/src/System.Web.Helpers/HtmlElement.cs
new file mode 100644
index 00000000..a507d947
--- /dev/null
+++ b/src/System.Web.Helpers/HtmlElement.cs
@@ -0,0 +1,122 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.IO;
+using System.Web.UI;
+
+namespace System.Web.Helpers
+{
+ internal class HtmlElement
+ {
+ public HtmlElement(string tagName)
+ {
+ TagName = tagName;
+ Attributes = new Dictionary<string, string>();
+ Children = new List<HtmlElement>();
+ }
+
+ internal string TagName { get; set; }
+
+ internal string InnerText { get; set; }
+
+ public IList<HtmlElement> Children { get; set; }
+
+ private IDictionary<string, string> Attributes { get; set; }
+
+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "It is there for completeness")]
+ public string this[string name]
+ {
+ get { return Attributes[name]; }
+ set { MergeAttribute(name, value); }
+ }
+
+ public HtmlElement SetInnerText(string innerText)
+ {
+ InnerText = innerText;
+ Children.Clear();
+ return this;
+ }
+
+ public HtmlElement AppendChild(HtmlElement e)
+ {
+ Children.Add(e);
+ return this;
+ }
+
+ public HtmlElement AppendChild(string innerText)
+ {
+ AppendChild(CreateSpan(innerText));
+ return this;
+ }
+
+ private void MergeAttribute(string name, string value)
+ {
+ Attributes[name] = value;
+ }
+
+ public HtmlElement AddCssClass(string className)
+ {
+ string currentValue;
+ if (!Attributes.TryGetValue("class", out currentValue))
+ {
+ Attributes["class"] = className;
+ }
+ else
+ {
+ Attributes["class"] = currentValue + " " + className;
+ }
+ return this;
+ }
+
+ public IHtmlString ToHtmlString()
+ {
+ using (StringWriter sw = new StringWriter(CultureInfo.InvariantCulture))
+ {
+ WriteTo(sw);
+ return new HtmlString(sw.ToString());
+ }
+ }
+
+ public void WriteTo(TextWriter writer)
+ {
+ WriteToInternal(new HtmlTextWriter(writer));
+ }
+
+ private void WriteToInternal(HtmlTextWriter writer)
+ {
+ foreach (var a in Attributes)
+ {
+ writer.AddAttribute(a.Key, a.Value, true);
+ }
+ writer.RenderBeginTag(TagName);
+ if (!String.IsNullOrEmpty(InnerText))
+ {
+ writer.WriteEncodedText(InnerText);
+ }
+ else
+ {
+ foreach (var e in Children)
+ {
+ e.WriteToInternal(writer);
+ }
+ }
+ writer.RenderEndTag();
+ }
+
+ public override string ToString()
+ {
+ return ToHtmlString().ToString();
+ }
+
+ internal static HtmlElement CreateSpan(string innerText, string cssClass = null)
+ {
+ var span = new HtmlElement("span");
+ span.SetInnerText(innerText);
+ if (!String.IsNullOrEmpty(cssClass))
+ {
+ span.AddCssClass(cssClass);
+ }
+ return span;
+ }
+ }
+}
diff --git a/src/System.Web.Helpers/HtmlObjectPrinter.cs b/src/System.Web.Helpers/HtmlObjectPrinter.cs
new file mode 100644
index 00000000..740c9afe
--- /dev/null
+++ b/src/System.Web.Helpers/HtmlObjectPrinter.cs
@@ -0,0 +1,411 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Web.Helpers.Resources;
+
+namespace System.Web.Helpers
+{
+ internal class HtmlObjectPrinter : ObjectVisitor
+ {
+ private const string Styles =
+ @"<style type=""text/css"">
+ .objectinfo { font-size: 13px; }
+ .objectinfo .type { color: #0000ff; }
+ .objectinfo .complexType { color: #2b91af; }
+ .objectinfo .name { color: Black; }
+ .objectinfo .value { color: Black; }
+ .objectinfo .quote { color: Brown; }
+ .objectinfo .null { color: Red; }
+ .objectinfo .exception { color:Red; }
+ .objectinfo .typeContainer { border-left: solid 2px #7C888A; padding-left: 3px; margin-left:3px; }
+ .objectinfo h3, h2 { margin:0; padding:0; }
+ .objectinfo ul { margin-top:0; margin-bottom:0; list-style-type:none; padding-left:10px; margin-left:10px; }
+</style>
+";
+
+ private static readonly HtmlElement _nullSpan = HtmlElement.CreateSpan("(null)", "null");
+ // List of chars to escape within strings
+ private static readonly Dictionary<char, string> _printableEscapeChars = new Dictionary<char, string>
+ {
+ { '\0', "\\0" },
+ { '\\', "\\\\" },
+ { '\'', "'" },
+ { '\"', "\\\"" },
+ { '\a', "\\a" },
+ { '\b', "\\b" },
+ { '\f', "\\f" },
+ { '\n', "\\n" },
+ { '\r', "\\r" },
+ { '\t', "\\t" },
+ { '\v', "\\v" },
+ };
+
+ // We want to exclude the type name next to the value for members
+ private bool _excludeTypeName;
+ private Stack<HtmlElement> _elementStack = new Stack<HtmlElement>();
+
+ public HtmlObjectPrinter(int recursionLimit, int enumerationLimit)
+ : base(recursionLimit, enumerationLimit)
+ {
+ }
+
+ private HtmlElement Current
+ {
+ get
+ {
+ Debug.Assert(_elementStack.Count > 0);
+ return _elementStack.Peek();
+ }
+ }
+
+ public void WriteTo(object value, TextWriter writer)
+ {
+ HtmlElement rootElement = new HtmlElement("div");
+ rootElement.AddCssClass("objectinfo");
+
+ PushElement(rootElement);
+ Visit(value, 0);
+ PopElement();
+
+ Debug.Assert(_elementStack.Count == 0, "Stack should be empty");
+
+ // REVIEW: We should only do this once per page/request
+ writer.Write(Styles);
+ rootElement.WriteTo(writer);
+ }
+
+ public override void VisitKeyValues(object value, IEnumerable<object> keys, Func<object, object> valueSelector, int depth)
+ {
+ string id = GetObjectId(value);
+ HtmlElement ul = new HtmlElement("ul");
+ ul.AddCssClass("typeEnumeration");
+ ul["id"] = id;
+
+ PushElement(ul);
+ base.VisitKeyValues(value, keys, valueSelector, depth);
+ PopElement();
+
+ Current.AppendChild(ul);
+ }
+
+ public override void VisitKeyValue(object key, object value, int depth)
+ {
+ HtmlElement keyElement = new HtmlElement("span");
+ PushElement(keyElement);
+ Visit(key, depth);
+ PopElement();
+
+ HtmlElement valueElement = new HtmlElement("span");
+ PushElement(valueElement);
+ Visit(value, depth);
+ PopElement();
+
+ // Append the elements to the li
+ HtmlElement li = new HtmlElement("li");
+ li.AppendChild(keyElement);
+ li.AppendChild(" = ");
+ li.AppendChild(valueElement);
+ Current.AppendChild(li);
+ }
+
+ public override void VisitEnumerable(IEnumerable enumerable, int depth)
+ {
+ string id = GetObjectId(enumerable);
+
+ HtmlElement ul = new HtmlElement("ul");
+ ul.AddCssClass("typeEnumeration");
+ ul["id"] = id;
+
+ PushElement(ul);
+ base.VisitEnumerable(enumerable, depth);
+ PopElement();
+
+ Current.AppendChild(ul);
+ }
+
+ public override void VisitIndexedEnumeratedValue(int index, object item, int depth)
+ {
+ HtmlElement li = new HtmlElement("li");
+ li.AppendChild(String.Format(CultureInfo.InvariantCulture, "[{0}] = ", index));
+ PushElement(li);
+ base.VisitIndexedEnumeratedValue(index, item, depth);
+ PopElement();
+ Current.AppendChild(li);
+ }
+
+ public override void VisitEnumeratedValue(object item, int depth)
+ {
+ HtmlElement li = new HtmlElement("li");
+ PushElement(li);
+ base.VisitEnumeratedValue(item, depth);
+ PopElement();
+ Current.AppendChild(li);
+ }
+
+ public override void VisitEnumeratonLimitExceeded()
+ {
+ HtmlElement li = new HtmlElement("li");
+ li.AppendChild("...");
+ Current.AppendChild(li);
+ }
+
+ public override void VisitMembers(IEnumerable<string> names, Func<string, Type> typeSelector, Func<string, object> valueSelector, int depth)
+ {
+ HtmlElement ul = new HtmlElement("ul");
+ ul.AddCssClass("typeProperties");
+
+ PushElement(ul);
+ base.VisitMembers(names, typeSelector, valueSelector, depth);
+ PopElement();
+
+ Current.AppendChild(ul);
+ }
+
+ public override void VisitMember(string name, Type type, object value, int depth)
+ {
+ HtmlElement li = new HtmlElement("li");
+
+ if (type != null)
+ {
+ li.AppendChild(CreateTypeNameSpan(type));
+ li.AppendChild(" ");
+ }
+
+ li.AppendChild(CreateNameSpan(name));
+ li.AppendChild(" = ");
+
+ PushElement(li);
+
+ _excludeTypeName = true;
+ base.VisitMember(name, type, value, depth);
+ _excludeTypeName = false;
+
+ PopElement();
+
+ Current.AppendChild(li);
+ }
+
+ public override void VisitComplexObject(object value, int depth)
+ {
+ string id = GetObjectId(value);
+
+ HtmlElement objectElement = new HtmlElement("div");
+ objectElement.AddCssClass("typeContainer");
+ objectElement["id"] = id;
+
+ PushElement(objectElement);
+ base.VisitComplexObject(value, depth);
+ PopElement();
+
+ if (objectElement.Children.Any())
+ {
+ Current.AppendChild(objectElement);
+ }
+ }
+
+ public override void VisitNull()
+ {
+ Current.AppendChild(_nullSpan);
+ }
+
+ public override void VisitStringValue(string stringValue)
+ {
+ // Convert the string escape sequences
+ stringValue = "\"" + ConvertEscapseSequences(stringValue) + "\"";
+ Current.AppendChild(CreateQuotedSpan(stringValue));
+ }
+
+ public override void VisitVisitedObject(string id, object value)
+ {
+ Current.AppendChild(CreateVisitedLink(id));
+ }
+
+ public override void Visit(object value, int depth)
+ {
+ if (value != null)
+ {
+ if (!_excludeTypeName)
+ {
+ Current.AppendChild(CreateTypeNameSpan(value.GetType()));
+ Current.AppendChild(" ");
+ }
+ _excludeTypeName = false;
+ }
+
+ base.Visit(value, depth);
+ }
+
+ public override void VisitObjectVisitorException(ObjectVisitorException exception)
+ {
+ Current.AppendChild(CreateExceptionSpan(exception));
+ }
+
+ [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "Making the value lowercase has nothing to do with normalization. It's used to show true or false instead of the Title case version")]
+ public override void VisitConvertedValue(object value, string convertedValue)
+ {
+ Type type = value.GetType();
+ if (type.Equals(typeof(bool)))
+ {
+ // Convert True or False to lowercase
+ convertedValue = convertedValue.ToLowerInvariant();
+ Current.AppendChild(CreateTypeSpan(convertedValue));
+ return;
+ }
+
+ if (type.Equals(typeof(char)))
+ {
+ string charValue = GetCharValue((char)value);
+ Current.AppendChild(CreateQuotedSpan("'" + charValue + "'"));
+ return;
+ }
+
+ // See if the value is a Type itself
+ Type valueAsType = value as Type;
+ if (valueAsType != null)
+ {
+ // For types we're going to generate elements that print typeof(TypeName)
+ Current.AppendChild(CreateParentSpan(CreateTypeSpan("typeof"),
+ CreateOperatorSpan("("),
+ CreateTypeNameSpan(valueAsType),
+ CreateOperatorSpan(")")));
+ }
+ else
+ {
+ Current.AppendChild(CreateValueSpan(convertedValue));
+ }
+ }
+
+ private static HtmlElement CreateParentSpan(params HtmlElement[] elements)
+ {
+ HtmlElement span = new HtmlElement("span");
+ foreach (var e in elements)
+ {
+ span.AppendChild(e);
+ }
+ return span;
+ }
+
+ private static HtmlElement CreateNameSpan(string name)
+ {
+ return HtmlElement.CreateSpan(name, "name");
+ }
+
+ private static HtmlElement CreateOperatorSpan(string @operator)
+ {
+ return HtmlElement.CreateSpan(@operator, "operator");
+ }
+
+ private static HtmlElement CreateValueSpan(string value)
+ {
+ return HtmlElement.CreateSpan(value, "value");
+ }
+
+ private static HtmlElement CreateExceptionSpan(ObjectVisitorException exception)
+ {
+ HtmlElement span = new HtmlElement("span");
+ span.AppendChild(HelpersResources.ObjectInfo_PropertyThrewException);
+ span.AppendChild(HtmlElement.CreateSpan(exception.InnerException.Message, "exception"));
+ return span;
+ }
+
+ private static HtmlElement CreateQuotedSpan(string value)
+ {
+ return HtmlElement.CreateSpan(value, "quote");
+ }
+
+ private static HtmlElement CreateLink(string href, string linkText, string cssClass = null)
+ {
+ HtmlElement a = new HtmlElement("a");
+ a.SetInnerText(linkText);
+ a["href"] = href;
+ if (!String.IsNullOrEmpty(cssClass))
+ {
+ a.AddCssClass(cssClass);
+ }
+ return a;
+ }
+
+ private static HtmlElement CreateVisitedLink(string id)
+ {
+ string text = String.Format(CultureInfo.InvariantCulture, "[{0}]", HelpersResources.ObjectInfo_PreviousDisplayed);
+ return CreateLink("#" + id, text);
+ }
+
+ private static HtmlElement CreateTypeSpan(string value)
+ {
+ return HtmlElement.CreateSpan(value, "type");
+ }
+
+ private static HtmlElement CreateTypeNameSpan(Type type)
+ {
+ string typeName = GetTypeName(type);
+ HtmlElement span = new HtmlElement("span");
+ StringBuilder sb = new StringBuilder();
+ // Convert the type name into html elements with different css classes
+ foreach (var ch in typeName)
+ {
+ if (IsOperator(ch))
+ {
+ if (sb.Length > 0)
+ {
+ span.AppendChild(CreateTypeSpan(sb.ToString()));
+ sb.Clear();
+ }
+ span.AppendChild(CreateOperatorSpan(ch.ToString()));
+ }
+ else
+ {
+ sb.Append(ch);
+ }
+ }
+ if (sb.Length > 0)
+ {
+ span.AppendChild(CreateTypeSpan(sb.ToString()));
+ }
+ return span;
+ }
+
+ private static bool IsOperator(char ch)
+ {
+ // These are the operators we expect to see within type names
+ return ch == '[' || ch == ']' || ch == '<' || ch == '>' || ch == '&' || ch == '*';
+ }
+
+ internal void PushElement(HtmlElement element)
+ {
+ _elementStack.Push(element);
+ }
+
+ internal HtmlElement PopElement()
+ {
+ Debug.Assert(_elementStack.Count > 0);
+ return _elementStack.Pop();
+ }
+
+ internal static string ConvertEscapseSequences(string value)
+ {
+ StringBuilder sb = new StringBuilder();
+ foreach (var ch in value)
+ {
+ sb.Append(GetCharValue(ch));
+ }
+ return sb.ToString();
+ }
+
+ private static string GetCharValue(char ch)
+ {
+ string value;
+ if (_printableEscapeChars.TryGetValue(ch, out value))
+ {
+ return value;
+ }
+ // REVIEW: Perf?
+ return ch.ToString();
+ }
+ }
+}
diff --git a/src/System.Web.Helpers/Json.cs b/src/System.Web.Helpers/Json.cs
new file mode 100644
index 00000000..e3e3a741
--- /dev/null
+++ b/src/System.Web.Helpers/Json.cs
@@ -0,0 +1,70 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Web.Script.Serialization;
+
+namespace System.Web.Helpers
+{
+ public static class Json
+ {
+ private static readonly JavaScriptSerializer _serializer = CreateSerializer();
+
+ public static string Encode(object value)
+ {
+ // Serialize our dynamic array type as an array
+ DynamicJsonArray jsonArray = value as DynamicJsonArray;
+ if (jsonArray != null)
+ {
+ return _serializer.Serialize((object[])jsonArray);
+ }
+
+ return _serializer.Serialize(value);
+ }
+
+ public static void Write(object value, TextWriter writer)
+ {
+ writer.Write(_serializer.Serialize(value));
+ }
+
+ public static dynamic Decode(string value)
+ {
+ return WrapObject(_serializer.DeserializeObject(value));
+ }
+
+ public static dynamic Decode(string value, Type targetType)
+ {
+ return WrapObject(_serializer.Deserialize(value, targetType));
+ }
+
+ public static T Decode<T>(string value)
+ {
+ return _serializer.Deserialize<T>(value);
+ }
+
+ private static JavaScriptSerializer CreateSerializer()
+ {
+ JavaScriptSerializer serializer = new JavaScriptSerializer();
+ serializer.RegisterConverters(new[] { new DynamicJavaScriptConverter() });
+ return serializer;
+ }
+
+ internal static dynamic WrapObject(object value)
+ {
+ // The JavaScriptSerializer returns IDictionary<string, object> for objects
+ // and object[] for arrays, so we wrap those in different dynamic objects
+ // so we can access the object graph using dynamic
+ var dictionaryValues = value as IDictionary<string, object>;
+ if (dictionaryValues != null)
+ {
+ return new DynamicJsonObject(dictionaryValues);
+ }
+
+ var arrayValues = value as object[];
+ if (arrayValues != null)
+ {
+ return new DynamicJsonArray(arrayValues);
+ }
+
+ return value;
+ }
+ }
+}
diff --git a/src/System.Web.Helpers/ObjectInfo.cs b/src/System.Web.Helpers/ObjectInfo.cs
new file mode 100644
index 00000000..2f55426f
--- /dev/null
+++ b/src/System.Web.Helpers/ObjectInfo.cs
@@ -0,0 +1,31 @@
+using System.Globalization;
+using System.Web.WebPages;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.Helpers
+{
+ public static class ObjectInfo
+ {
+ private const int DefaultRecursionLimit = 10;
+ private const int DefaultEnumerationLimit = 1000;
+
+ public static HelperResult Print(object value, int depth = DefaultRecursionLimit, int enumerationLength = DefaultEnumerationLimit)
+ {
+ if (depth < 0)
+ {
+ throw new ArgumentOutOfRangeException(
+ "depth",
+ String.Format(CultureInfo.InvariantCulture, CommonResources.Argument_Must_Be_GreaterThanOrEqualTo, 0));
+ }
+ if (enumerationLength <= 0)
+ {
+ throw new ArgumentOutOfRangeException(
+ "enumerationLength",
+ String.Format(CultureInfo.InvariantCulture, CommonResources.Argument_Must_Be_GreaterThan, 0));
+ }
+
+ HtmlObjectPrinter printer = new HtmlObjectPrinter(depth, enumerationLength);
+ return new HelperResult(writer => printer.WriteTo(value, writer));
+ }
+ }
+}
diff --git a/src/System.Web.Helpers/ObjectVisitor.cs b/src/System.Web.Helpers/ObjectVisitor.cs
new file mode 100644
index 00000000..deb950a4
--- /dev/null
+++ b/src/System.Web.Helpers/ObjectVisitor.cs
@@ -0,0 +1,465 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Dynamic;
+using System.Globalization;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.Serialization;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.Helpers
+{
+ internal class ObjectVisitor
+ {
+ private static readonly Dictionary<Type, string> _typeNames = new Dictionary<Type, string>
+ {
+ { typeof(string), "string" },
+ { typeof(object), "object" },
+ { typeof(int), "int" },
+ { typeof(byte), "byte" },
+ { typeof(short), "short" },
+ { typeof(long), "long" },
+ { typeof(decimal), "decimal" },
+ { typeof(float), "float" },
+ { typeof(double), "double" },
+ { typeof(bool), "bool" },
+ { typeof(char), "char" },
+ { typeof(void), "void" }
+ };
+
+ private static readonly char[] _separators = { '&', '[', '*' };
+
+ private readonly int _recursionLimit;
+ private readonly int _enumerationLimit;
+ private Dictionary<object, string> _visited = new Dictionary<object, string>();
+
+ public ObjectVisitor(int recursionLimit, int enumerationLimit)
+ {
+ Debug.Assert(enumerationLimit > 0);
+ Debug.Assert(recursionLimit >= 0);
+ _enumerationLimit = enumerationLimit;
+ _recursionLimit = recursionLimit;
+ }
+
+ protected string GetObjectId(object value)
+ {
+ string id;
+ if (_visited.TryGetValue(value, out id))
+ {
+ return id;
+ }
+ return null;
+ }
+
+ public virtual void Visit(object value, int depth)
+ {
+ if (value == null || DBNull.Value.Equals(value))
+ {
+ VisitNull();
+ return;
+ }
+
+ // Check to see if the we've already visited this object
+ string id;
+ if (_visited.TryGetValue(value, out id))
+ {
+ VisitVisitedObject(id, value);
+ return;
+ }
+
+ string stringValue = value as string;
+ if (stringValue != null)
+ {
+ VisitStringValue(stringValue);
+ return;
+ }
+
+ if (TryConvertToString(value, out stringValue))
+ {
+ VisitConvertedValue(value, stringValue);
+ return;
+ }
+
+ // This exceptin occurs when we try to access the property and it fails
+ // for some reason. The actual exception is wrapped in the ObjectVisitorException
+ ObjectVisitorException exception = value as ObjectVisitorException;
+ if (exception != null)
+ {
+ VisitObjectVisitorException(exception);
+ return;
+ }
+
+ // Mark the object as visited
+ id = CreateObjectId(value);
+ _visited.Add(value, id);
+
+ NameValueCollection nameValueCollection = value as NameValueCollection;
+ if (nameValueCollection != null)
+ {
+ VisitNameValueCollection(nameValueCollection, depth);
+ return;
+ }
+
+ IDictionary dictionary = value as IDictionary;
+ if (dictionary != null)
+ {
+ VisitDictionary(dictionary, depth);
+ return;
+ }
+
+ IEnumerable enumerable = value as IEnumerable;
+ if (enumerable != null)
+ {
+ VisitEnumerable(enumerable, depth);
+ return;
+ }
+
+ VisitComplexObject(value, depth + 1);
+ }
+
+ public virtual void VisitObjectVisitorException(ObjectVisitorException exception)
+ {
+ }
+
+ public virtual void VisitConvertedValue(object value, string convertedValue)
+ {
+ VisitStringValue(convertedValue);
+ }
+
+ public virtual void VisitVisitedObject(string id, object value)
+ {
+ }
+
+ public virtual void VisitNull()
+ {
+ }
+
+ public virtual void VisitStringValue(string stringValue)
+ {
+ }
+
+ public virtual void VisitComplexObject(object value, int depth)
+ {
+ if (depth > _recursionLimit)
+ {
+ return;
+ }
+
+ Debug.Assert(value != null, "Value should not be null");
+
+ var dynamicObject = value as IDynamicMetaObjectProvider;
+ // Only look at dynamic objects that do not implement ICustomTypeDescriptor
+ if (dynamicObject != null && !(dynamicObject is ICustomTypeDescriptor))
+ {
+ var memberNames = DynamicHelper.GetMemberNames(dynamicObject);
+
+ if (memberNames != null)
+ {
+ // Always use the runtime type for dynamic objects since there is no metadata
+ VisitMembers(memberNames,
+ name => null,
+ name => DynamicHelper.GetMemberValue(dynamicObject, name),
+ depth);
+ }
+ }
+ else
+ {
+ // REVIEW: We should try to filter out properties of certain types
+
+ // Dump properties using type descriptor
+ var props = TypeDescriptor.GetProperties(value);
+ var propNames = from PropertyDescriptor p in props
+ select p.Name;
+
+ VisitMembers(propNames,
+ name => props.Find(name, ignoreCase: true).PropertyType,
+ name => GetPropertyDescriptorValue(value, name, props),
+ depth);
+
+ // Dump fields
+ var fields = value.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance)
+ .ToDictionary(field => field.Name);
+
+ VisitMembers(fields.Keys,
+ name => fields[name].FieldType,
+ name => GetFieldValue(value, name, fields),
+ depth);
+ }
+ }
+
+ public virtual void VisitNameValueCollection(NameValueCollection collection, int depth)
+ {
+ VisitKeyValues(collection, collection.AllKeys.Cast<object>(), key => collection[(string)key], depth);
+ }
+
+ public virtual void VisitDictionary(IDictionary dictionary, int depth)
+ {
+ VisitKeyValues(dictionary, dictionary.Keys.Cast<object>(), key => dictionary[key], depth);
+ }
+
+ public virtual void VisitEnumerable(IEnumerable enumerable, int depth)
+ {
+ if (depth > _recursionLimit)
+ {
+ return;
+ }
+
+ Type enumerableType = enumerable.GetType();
+ bool isIndexedEnumeration = ImplementsInterface(enumerableType, typeof(IList<>))
+ || ImplementsInterface(enumerableType, typeof(IList));
+
+ int index = 0;
+ foreach (var item in enumerable)
+ {
+ if (index >= _enumerationLimit)
+ {
+ VisitEnumeratonLimitExceeded();
+ break;
+ }
+ if (isIndexedEnumeration)
+ {
+ VisitIndexedEnumeratedValue(index, item, depth);
+ }
+ else
+ {
+ VisitEnumeratedValue(item, depth);
+ }
+ index++;
+ }
+ }
+
+ public virtual void VisitEnumeratedValue(object item, int depth)
+ {
+ Visit(item, depth);
+ }
+
+ public virtual void VisitIndexedEnumeratedValue(int index, object item, int depth)
+ {
+ Visit(item, depth);
+ }
+
+ public virtual void VisitEnumeratonLimitExceeded()
+ {
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We don't want to fail surface any exceptions throw from getting property accessors")]
+ public virtual void VisitMembers(IEnumerable<string> names, Func<string, Type> typeSelector, Func<string, object> valueSelector, int depth)
+ {
+ foreach (string name in names)
+ {
+ Type type = null;
+ object value = null;
+ try
+ {
+ // Get the type and value
+ type = typeSelector(name);
+ value = valueSelector(name);
+
+ // If the type is null try using the runtime type
+ if (value != null && type == null)
+ {
+ type = value.GetType();
+ }
+ }
+ catch (Exception ex)
+ {
+ // Set the value as an exception we know about
+ value = new ObjectVisitorException(null, ex);
+ }
+ finally
+ {
+ VisitMember(name, type, value, depth);
+ }
+ }
+ }
+
+ public virtual void VisitMember(string name, Type type, object value, int depth)
+ {
+ Visit(value, depth);
+ }
+
+ public virtual void VisitKeyValues(object value, IEnumerable<object> keys, Func<object, object> valueSelector, int depth)
+ {
+ if (depth > _recursionLimit)
+ {
+ return;
+ }
+
+ foreach (var key in keys)
+ {
+ VisitKeyValue(key, valueSelector(key), depth);
+ }
+ }
+
+ public virtual void VisitKeyValue(object key, object value, int depth)
+ {
+ // Dump the key and value
+ Visit(key, depth);
+ Visit(value, depth);
+ }
+
+ protected virtual string CreateObjectId(object value)
+ {
+ // REVIEW: Maybe use a guid?
+ return value.GetHashCode().ToString(CultureInfo.InvariantCulture);
+ }
+
+ internal static string GetTypeName(Type type)
+ {
+ // See if we have the type name stored
+ string typeName;
+ if (_typeNames.TryGetValue(type, out typeName))
+ {
+ return typeName;
+ }
+
+ if (type.IsGenericType)
+ {
+ // Get the generic type name without arguments
+ string genericTypeName = GetGenericTypeName(type);
+ // Create a user friendly type name
+ var arguments = from argType in type.GetGenericArguments()
+ select GetTypeName(argType);
+
+ return String.Format(CultureInfo.InvariantCulture, "{0}<{1}>", genericTypeName, String.Join(", ", arguments));
+ }
+
+ if (type.IsByRef || type.IsArray || type.IsPointer)
+ {
+ // Get the element type name
+ string elementTypeName = GetTypeName(type.GetElementType());
+ // Append the separator
+ int sepIndex = type.Name.IndexOfAny(_separators);
+ return elementTypeName + type.Name.Substring(sepIndex);
+ }
+
+ // Fallback to using the type name as is
+ return type.Name;
+ }
+
+ private static string GetGenericTypeName(Type type)
+ {
+ Debug.Assert(type.IsGenericType, "Type is not a generic type");
+
+ // Check for anonymous types
+ if (IsAnonymousType(type))
+ {
+ return "AnonymousType";
+ }
+
+ string genericTypeDefinitionName = type.GetGenericTypeDefinition().Name;
+ int index = genericTypeDefinitionName.IndexOf('`');
+ Debug.Assert(index >= 0);
+ // Get the generic type name without the `
+ return genericTypeDefinitionName.Substring(0, index);
+ }
+
+ // Copied from System.Web.WebPages/Util/TypeHelpers.cs
+ private static bool IsAnonymousType(Type type)
+ {
+ Debug.Assert(type != null, "Type should not be null");
+
+ // TODO: The only way to detect anonymous types right now.
+ return Attribute.IsDefined(type, typeof(CompilerGeneratedAttribute), false)
+ && type.IsGenericType && type.Name.Contains("AnonymousType")
+ && (type.Name.StartsWith("<>", StringComparison.OrdinalIgnoreCase) || type.Name.StartsWith("VB$", StringComparison.OrdinalIgnoreCase))
+ && (type.Attributes & TypeAttributes.NotPublic) == TypeAttributes.NotPublic;
+ }
+
+ private static bool ImplementsInterface(Type type, Type targetInterfaceType)
+ {
+ Func<Type, bool> implementsInterface = t => targetInterfaceType.IsAssignableFrom(t);
+ if (targetInterfaceType.IsGenericType)
+ {
+ implementsInterface = t => t.IsGenericType && targetInterfaceType.IsAssignableFrom(t.GetGenericTypeDefinition());
+ }
+ return implementsInterface(type) || type.GetInterfaces().Any(implementsInterface);
+ }
+
+ private static object GetFieldValue(object value, string name, IDictionary<string, FieldInfo> fields)
+ {
+ FieldInfo fieldInfo;
+ // Get the value from the dictionary
+ bool result = fields.TryGetValue(name, out fieldInfo);
+ Debug.Assert(result, "Entry should exist");
+ return fieldInfo.GetValue(value);
+ }
+
+ private static object GetPropertyDescriptorValue(object value, string name, PropertyDescriptorCollection props)
+ {
+ PropertyDescriptor propertyDescriptor = props.Find(name, ignoreCase: true);
+ Debug.Assert(propertyDescriptor != null, "Property descriptor shouldn't be null");
+ return propertyDescriptor.GetValue(value);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We don't want to surface any exceptions while trying to convert from string")]
+ private static bool TryConvertToString(object value, out string stringValue)
+ {
+ stringValue = null;
+ try
+ {
+ IConvertible convertibe = value as IConvertible;
+ if (convertibe != null)
+ {
+ stringValue = convertibe.ToString(CultureInfo.CurrentCulture);
+ return true;
+ }
+
+ TypeConverter converter = TypeDescriptor.GetConverter(value);
+ if (converter.CanConvertFrom(typeof(string)))
+ {
+ stringValue = converter.ConvertToString(value);
+ return true;
+ }
+
+ Type type = value.GetType();
+ if (type == typeof(object))
+ {
+ stringValue = value.ToString();
+ return true;
+ }
+ Type valueAsType = value as Type;
+ if (valueAsType != null)
+ {
+ stringValue = "typeof(" + GetTypeName(valueAsType) + ")";
+ return true;
+ }
+ }
+ catch (Exception)
+ {
+ // If we failed to convert the type for any reason return false
+ }
+ return false;
+ }
+
+ [Serializable]
+ public class ObjectVisitorException : Exception
+ {
+ public ObjectVisitorException()
+ {
+ }
+
+ public ObjectVisitorException(string message)
+ : base(message)
+ {
+ }
+
+ public ObjectVisitorException(string message, Exception inner)
+ : base(message, inner)
+ {
+ }
+
+ protected ObjectVisitorException(
+ SerializationInfo info,
+ StreamingContext context)
+ : base(info, context)
+ {
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Helpers/Properties/AssemblyInfo.cs b/src/System.Web.Helpers/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..cdd31f7d
--- /dev/null
+++ b/src/System.Web.Helpers/Properties/AssemblyInfo.cs
@@ -0,0 +1,10 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+
+[assembly: AssemblyTitle("System.Web.Helpers")]
+[assembly: AssemblyDescription("")]
+[assembly: InternalsVisibleTo("System.Web.Helpers.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
diff --git a/src/System.Web.Helpers/Resources/ChartTemplates.Designer.cs b/src/System.Web.Helpers/Resources/ChartTemplates.Designer.cs
new file mode 100644
index 00000000..82aa7b04
--- /dev/null
+++ b/src/System.Web.Helpers/Resources/ChartTemplates.Designer.cs
@@ -0,0 +1,141 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.1
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Microsoft.WebPages.Helpers.Resources {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class ChartTemplates {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal ChartTemplates() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.WebPages.Helpers.Resources.ChartTemplates", typeof(ChartTemplates).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to - &lt;Chart BackColor=&quot;#D3DFF0&quot; BackGradientStyle=&quot;TopBottom&quot; BackSecondaryColor=&quot;White&quot; BorderColor=&quot;26, 59, 105&quot; BorderlineDashStyle=&quot;Solid&quot; BorderWidth=&quot;2&quot; Palette=&quot;BrightPastel&quot;&gt;
+ ///- &lt;ChartAreas&gt;
+ /// &lt;ChartArea Name=&quot;Default&quot; _Template_=&quot;All&quot; BackColor=&quot;64, 165, 191, 228&quot; BackGradientStyle=&quot;TopBottom&quot; BackSecondaryColor=&quot;White&quot; BorderColor=&quot;64, 64, 64, 64&quot; BorderDashStyle=&quot;Solid&quot; ShadowColor=&quot;Transparent&quot; /&gt;
+ /// &lt;/ChartAreas&gt;
+ /// &lt;Legends&gt;
+ /// &lt;Legend _Template_=&quot;All&quot; BackColor=&quot;Transparent&quot; Font=&quot;Trebuchet MS, [rest of string was truncated]&quot;;.
+ /// </summary>
+ internal static string Blue {
+ get {
+ return ResourceManager.GetString("Blue", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &lt;Chart BackColor=&quot;#C9DC87&quot; BackGradientStyle=&quot;TopBottom&quot; BorderColor=&quot;181, 64, 1&quot; BorderWidth=&quot;2&quot; BorderlineDashStyle=&quot;Solid&quot; Palette=&quot;BrightPastel&quot;&gt;
+ /// &lt;ChartAreas&gt;
+ /// &lt;ChartArea Name=&quot;Default&quot; _Template_=&quot;All&quot; BackColor=&quot;Transparent&quot; BackSecondaryColor=&quot;White&quot; BorderColor=&quot;64, 64, 64, 64&quot; BorderDashStyle=&quot;Solid&quot; ShadowColor=&quot;Transparent&quot;&gt;
+ /// &lt;AxisY LineColor=&quot;64, 64, 64, 64&quot;&gt;
+ /// &lt;MajorGrid Interval=&quot;Auto&quot; LineColor=&quot;64, 64, 64, 64&quot; /&gt;
+ /// &lt;LabelStyle Font=&quot;Trebuchet MS, 8.25pt, style=Bold [rest of string was truncated]&quot;;.
+ /// </summary>
+ internal static string Green {
+ get {
+ return ResourceManager.GetString("Green", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &lt;Chart Palette=&quot;SemiTransparent&quot; BorderColor=&quot;#000&quot; BorderWidth=&quot;2&quot; BorderlineDashStyle=&quot;Solid&quot;&gt;
+ ///&lt;ChartAreas&gt;
+ /// &lt;ChartArea _Template_=&quot;All&quot; Name=&quot;Default&quot;&gt;
+ /// &lt;AxisX&gt;
+ /// &lt;MinorGrid Enabled=&quot;False&quot; /&gt;
+ /// &lt;MajorGrid Enabled=&quot;False&quot; /&gt;
+ /// &lt;/AxisX&gt;
+ /// &lt;AxisY&gt;
+ /// &lt;MajorGrid Enabled=&quot;False&quot; /&gt;
+ /// &lt;MinorGrid Enabled=&quot;False&quot; /&gt;
+ /// &lt;/AxisY&gt;
+ /// &lt;/ChartArea&gt;
+ ///&lt;/ChartAreas&gt;
+ ///&lt;/Chart&gt;.
+ /// </summary>
+ internal static string Vanilla {
+ get {
+ return ResourceManager.GetString("Vanilla", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &lt;Chart BackColor=&quot;#555&quot; BackGradientStyle=&quot;TopBottom&quot; BorderColor=&quot;181, 64, 1&quot; BorderWidth=&quot;2&quot; BorderlineDashStyle=&quot;Solid&quot; Palette=&quot;Excel&quot; AntiAliasing=&quot;All&quot;&gt;
+ /// &lt;ChartAreas&gt;
+ /// &lt;ChartArea Name=&quot;Default&quot; _Template_=&quot;All&quot; BackColor=&quot;Transparent&quot; BackSecondaryColor=&quot;White&quot; BorderColor=&quot;64, 64, 64, 64&quot; BorderDashStyle=&quot;Solid&quot; ShadowColor=&quot;Transparent&quot;&gt;
+ /// &lt;Area3DStyle LightStyle=&quot;Simplistic&quot; Enable3D=&quot;True&quot; Inclination=&quot;5&quot; IsClustered=&quot;False&quot; IsRightAngleAxes=&quot;False&quot; Perspective=&quot;10&quot; Rotation [rest of string was truncated]&quot;;.
+ /// </summary>
+ internal static string Vanilla3D {
+ get {
+ return ResourceManager.GetString("Vanilla3D", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &lt;Chart BackColor=&quot;#FADA5E&quot; BackGradientStyle=&quot;TopBottom&quot; BorderColor=&quot;#B8860B&quot; BorderWidth=&quot;2&quot; BorderlineDashStyle=&quot;Solid&quot; Palette=&quot;EarthTones&quot;&gt;
+ /// &lt;ChartAreas&gt;
+ /// &lt;ChartArea Name=&quot;Default&quot; _Template_=&quot;All&quot; BackColor=&quot;Transparent&quot; BackSecondaryColor=&quot;White&quot; BorderColor=&quot;64, 64, 64, 64&quot; BorderDashStyle=&quot;Solid&quot; ShadowColor=&quot;Transparent&quot;&gt;
+ /// &lt;AxisY&gt;
+ /// &lt;LabelStyle Font=&quot;Trebuchet MS, 8.25pt, style=Bold&quot; /&gt;
+ /// &lt;/AxisY&gt;
+ /// &lt;AxisX LineColor=&quot;64, 64, 64, 64&quot;&gt;
+ /// &lt;LabelStyle Font=&quot;Trebuch [rest of string was truncated]&quot;;.
+ /// </summary>
+ internal static string Yellow {
+ get {
+ return ResourceManager.GetString("Yellow", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Helpers/Resources/ChartTemplates.resx b/src/System.Web.Helpers/Resources/ChartTemplates.resx
new file mode 100644
index 00000000..d48468da
--- /dev/null
+++ b/src/System.Web.Helpers/Resources/ChartTemplates.resx
@@ -0,0 +1,197 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="Blue" xml:space="preserve">
+ <value>&lt;Chart BackColor="#D3DFF0" BackGradientStyle="TopBottom" BackSecondaryColor="White" BorderColor="26, 59, 105" BorderlineDashStyle="Solid" BorderWidth="2" Palette="BrightPastel"&gt;
+ &lt;ChartAreas&gt;
+ &lt;ChartArea Name="Default" _Template_="All" BackColor="64, 165, 191, 228" BackGradientStyle="TopBottom" BackSecondaryColor="White" BorderColor="64, 64, 64, 64" BorderDashStyle="Solid" ShadowColor="Transparent" /&gt;
+ &lt;/ChartAreas&gt;
+ &lt;Legends&gt;
+ &lt;Legend _Template_="All" BackColor="Transparent" Font="Trebuchet MS, 8.25pt, style=Bold" IsTextAutoFit="False" /&gt;
+ &lt;/Legends&gt;
+ &lt;BorderSkin SkinStyle="Emboss" /&gt;
+ &lt;/Chart&gt;</value>
+ </data>
+ <data name="Green" xml:space="preserve">
+ <value>&lt;Chart BackColor="#C9DC87" BackGradientStyle="TopBottom" BorderColor="181, 64, 1" BorderWidth="2" BorderlineDashStyle="Solid" Palette="BrightPastel"&gt;
+ &lt;ChartAreas&gt;
+ &lt;ChartArea Name="Default" _Template_="All" BackColor="Transparent" BackSecondaryColor="White" BorderColor="64, 64, 64, 64" BorderDashStyle="Solid" ShadowColor="Transparent"&gt;
+ &lt;AxisY LineColor="64, 64, 64, 64"&gt;
+ &lt;MajorGrid Interval="Auto" LineColor="64, 64, 64, 64" /&gt;
+ &lt;LabelStyle Font="Trebuchet MS, 8.25pt, style=Bold" /&gt;
+ &lt;/AxisY&gt;
+ &lt;AxisX LineColor="64, 64, 64, 64"&gt;
+ &lt;MajorGrid LineColor="64, 64, 64, 64" /&gt;
+ &lt;LabelStyle Font="Trebuchet MS, 8.25pt, style=Bold" /&gt;
+ &lt;/AxisX&gt;
+ &lt;Area3DStyle Inclination="15" IsClustered="False" IsRightAngleAxes="False" Perspective="10" Rotation="10" WallWidth="0" /&gt;
+ &lt;/ChartArea&gt;
+ &lt;/ChartAreas&gt;
+ &lt;Legends&gt;
+ &lt;Legend _Template_="All" Alignment="Center" BackColor="Transparent" Docking="Bottom" Font="Trebuchet MS, 8.25pt, style=Bold" IsTextAutoFit ="False" LegendStyle="Row"&gt;
+ &lt;/Legend&gt;
+ &lt;/Legends&gt;
+ &lt;BorderSkin SkinStyle="Emboss" /&gt;
+&lt;/Chart&gt;</value>
+ </data>
+ <data name="Vanilla" xml:space="preserve">
+ <value>&lt;Chart Palette="SemiTransparent" BorderColor="#000" BorderWidth="2" BorderlineDashStyle="Solid"&gt;
+&lt;ChartAreas&gt;
+ &lt;ChartArea _Template_="All" Name="Default"&gt;
+ &lt;AxisX&gt;
+ &lt;MinorGrid Enabled="False" /&gt;
+ &lt;MajorGrid Enabled="False" /&gt;
+ &lt;/AxisX&gt;
+ &lt;AxisY&gt;
+ &lt;MajorGrid Enabled="False" /&gt;
+ &lt;MinorGrid Enabled="False" /&gt;
+ &lt;/AxisY&gt;
+ &lt;/ChartArea&gt;
+&lt;/ChartAreas&gt;
+&lt;/Chart&gt;</value>
+ </data>
+ <data name="Vanilla3D" xml:space="preserve">
+ <value>&lt;Chart BackColor="#555" BackGradientStyle="TopBottom" BorderColor="181, 64, 1" BorderWidth="2" BorderlineDashStyle="Solid" Palette="SemiTransparent" AntiAliasing="All"&gt;
+ &lt;ChartAreas&gt;
+ &lt;ChartArea Name="Default" _Template_="All" BackColor="Transparent" BackSecondaryColor="White" BorderColor="64, 64, 64, 64" BorderDashStyle="Solid" ShadowColor="Transparent"&gt;
+ &lt;Area3DStyle LightStyle="Simplistic" Enable3D="True" Inclination="30" IsClustered="False" IsRightAngleAxes="False" Perspective="10" Rotation="-30" WallWidth="0" /&gt;
+ &lt;/ChartArea&gt;
+ &lt;/ChartAreas&gt;
+&lt;/Chart&gt;</value>
+ </data>
+ <data name="Yellow" xml:space="preserve">
+ <value>&lt;Chart BackColor="#FADA5E" BackGradientStyle="TopBottom" BorderColor="#B8860B" BorderWidth="2" BorderlineDashStyle="Solid" Palette="EarthTones"&gt;
+ &lt;ChartAreas&gt;
+ &lt;ChartArea Name="Default" _Template_="All" BackColor="Transparent" BackSecondaryColor="White" BorderColor="64, 64, 64, 64" BorderDashStyle="Solid" ShadowColor="Transparent"&gt;
+ &lt;AxisY&gt;
+ &lt;LabelStyle Font="Trebuchet MS, 8.25pt, style=Bold" /&gt;
+ &lt;/AxisY&gt;
+ &lt;AxisX LineColor="64, 64, 64, 64"&gt;
+ &lt;LabelStyle Font="Trebuchet MS, 8.25pt, style=Bold" /&gt;
+ &lt;/AxisX&gt;
+ &lt;/ChartArea&gt;
+ &lt;/ChartAreas&gt;
+ &lt;Legends&gt;
+ &lt;Legend _Template_="All" Alignment="Left" BackColor="Transparent" Docking="Bottom" Font="Trebuchet MS, 8.25pt, style=Bold" LegendStyle="Row"&gt;
+ &lt;/Legend&gt;
+ &lt;/Legends&gt;
+ &lt;BorderSkin SkinStyle="Emboss" /&gt;
+&lt;/Chart&gt;</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/System.Web.Helpers/Resources/HelpersResources.Designer.cs b/src/System.Web.Helpers/Resources/HelpersResources.Designer.cs
new file mode 100644
index 00000000..843a48b4
--- /dev/null
+++ b/src/System.Web.Helpers/Resources/HelpersResources.Designer.cs
@@ -0,0 +1,414 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.225
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace System.Web.Helpers.Resources {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class HelpersResources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal HelpersResources() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("System.Web.Helpers.Resources.HelpersResources", typeof(HelpersResources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Argument conversion to type &quot;{0}&quot; failed..
+ /// </summary>
+ internal static string Chart_ArgumentConversionFailed {
+ get {
+ return ResourceManager.GetString("Chart_ArgumentConversionFailed", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A series cannot be data-bound to a string object..
+ /// </summary>
+ internal static string Chart_ExceptionDataBindSeriesToString {
+ get {
+ return ResourceManager.GetString("Chart_ExceptionDataBindSeriesToString", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The theme file &quot;{0}&quot; could not be found..
+ /// </summary>
+ internal static string Chart_ThemeFileNotFound {
+ get {
+ return ResourceManager.GetString("Chart_ThemeFileNotFound", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The hash algorithm &apos;{0}&apos; is not supported, valid values are: sha256, sha1, md5.
+ /// </summary>
+ internal static string Crypto_NotSupportedHashAlg {
+ get {
+ return ResourceManager.GetString("Crypto_NotSupportedHashAlg", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &quot;{0}&quot; is invalid image format. Valid values are image format names like: &quot;JPEG&quot;, &quot;BMP&quot;, &quot;GIF&quot;, &quot;PNG&quot;, etc..
+ /// </summary>
+ internal static string Image_IncorrectImageFormat {
+ get {
+ return ResourceManager.GetString("Image_IncorrectImageFormat", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Unable to convert to &quot;{0}&quot;. Use Json.Decode&lt;T&gt; instead..
+ /// </summary>
+ internal static string Json_UnableToConvertType {
+ get {
+ return ResourceManager.GetString("Json_UnableToConvertType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Previously Displayed.
+ /// </summary>
+ internal static string ObjectInfo_PreviousDisplayed {
+ get {
+ return ResourceManager.GetString("ObjectInfo_PreviousDisplayed", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Accessing a property threw an exception: .
+ /// </summary>
+ internal static string ObjectInfo_PropertyThrewException {
+ get {
+ return ResourceManager.GetString("ObjectInfo_PropertyThrewException", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to File path &quot;{0}&quot; is invalid..
+ /// </summary>
+ internal static string PathUtils_IncorrectPath {
+ get {
+ return ResourceManager.GetString("PathUtils_IncorrectPath", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Additional server information is available when the page is running with high trust..
+ /// </summary>
+ internal static string ServerInfo_AdditionalInfo {
+ get {
+ return ResourceManager.GetString("ServerInfo_AdditionalInfo", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Environment Variables.
+ /// </summary>
+ internal static string ServerInfo_EnvVars {
+ get {
+ return ResourceManager.GetString("ServerInfo_EnvVars", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to ASP.NET Server Information.
+ /// </summary>
+ internal static string ServerInfo_Header {
+ get {
+ return ResourceManager.GetString("ServerInfo_Header", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to HTTP Runtime Information.
+ /// </summary>
+ internal static string ServerInfo_HttpRuntime {
+ get {
+ return ResourceManager.GetString("ServerInfo_HttpRuntime", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Legacy Code Access Security.
+ /// </summary>
+ internal static string ServerInfo_LegacyCAS {
+ get {
+ return ResourceManager.GetString("ServerInfo_LegacyCAS", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Legacy Code Access Security has been detected on your system. Microsoft WebPage features require the ASP.NET 4 Code Access Security model. For information about how to resolve this, contact your server administrator..
+ /// </summary>
+ internal static string ServerInfo_LegacyCasHelpInfo {
+ get {
+ return ResourceManager.GetString("ServerInfo_LegacyCasHelpInfo", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to no value.
+ /// </summary>
+ internal static string ServerInfo_NoValue {
+ get {
+ return ResourceManager.GetString("ServerInfo_NoValue", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Server Configuration.
+ /// </summary>
+ internal static string ServerInfo_ServerConfigTable {
+ get {
+ return ResourceManager.GetString("ServerInfo_ServerConfigTable", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to ASP.NET Server Variables.
+ /// </summary>
+ internal static string ServerInfo_ServerVars {
+ get {
+ return ResourceManager.GetString("ServerInfo_ServerVars", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The column name cannot be null or an empty string unless a custom format is specified..
+ /// </summary>
+ internal static string WebGrid_ColumnNameOrFormatRequired {
+ get {
+ return ResourceManager.GetString("WebGrid_ColumnNameOrFormatRequired", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Column &quot;{0}&quot; does not exist..
+ /// </summary>
+ internal static string WebGrid_ColumnNotFound {
+ get {
+ return ResourceManager.GetString("WebGrid_ColumnNotFound", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The WebGrid instance is already bound to a data source..
+ /// </summary>
+ internal static string WebGrid_DataSourceBound {
+ get {
+ return ResourceManager.GetString("WebGrid_DataSourceBound", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A data source must be bound before this operation can be performed..
+ /// </summary>
+ internal static string WebGrid_NoDataSourceBound {
+ get {
+ return ResourceManager.GetString("WebGrid_NoDataSourceBound", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to This operation is not supported when paging is disabled for the &quot;WebGrid&quot; object..
+ /// </summary>
+ internal static string WebGrid_NotSupportedIfPagingIsDisabled {
+ get {
+ return ResourceManager.GetString("WebGrid_NotSupportedIfPagingIsDisabled", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to This operation is not supported when sorting is disabled for the &quot;WebGrid&quot; object..
+ /// </summary>
+ internal static string WebGrid_NotSupportedIfSortingIsDisabled {
+ get {
+ return ResourceManager.GetString("WebGrid_NotSupportedIfSortingIsDisabled", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to To use this argument, pager mode &quot;{0}&quot; must be enabled..
+ /// </summary>
+ internal static string WebGrid_PagerModeMustBeEnabled {
+ get {
+ return ResourceManager.GetString("WebGrid_PagerModeMustBeEnabled", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to This property cannot be set after the &quot;WebGrid&quot; object has been sorted or paged. Make sure that this property is set prior to invoking the &quot;Rows&quot; property directly or indirectly through other methods such as &quot;GetHtml&quot;, &quot;Pager&quot;, &quot;Table&quot;, etc..
+ /// </summary>
+ internal static string WebGrid_PropertySetterNotSupportedAfterDataBound {
+ get {
+ return ResourceManager.GetString("WebGrid_PropertySetterNotSupportedAfterDataBound", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A value for &quot;rowCount&quot; must be specified when &quot;autoSortAndPage&quot; is set to true and paging is enabled..
+ /// </summary>
+ internal static string WebGrid_RowCountNotSpecified {
+ get {
+ return ResourceManager.GetString("WebGrid_RowCountNotSpecified", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Select.
+ /// </summary>
+ internal static string WebGrid_SelectLinkText {
+ get {
+ return ResourceManager.GetString("WebGrid_SelectLinkText", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The &quot;fontColor&quot; value is invalid. Valid values are names like &quot;White&quot;, &quot;Black&quot;, or &quot;DarkBlue&quot;, or hexadecimal values in the form &quot;#RRGGBB&quot; or &quot;#RGB&quot;..
+ /// </summary>
+ internal static string WebImage_IncorrectColorName {
+ get {
+ return ResourceManager.GetString("WebImage_IncorrectColorName", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The &quot;fontFamily&quot; value is invalid. Valid values are font family names like: &quot;Arial&quot;, &quot;Times New Roman&quot;, etc. Make sure that the font family you are trying to use is installed on the server..
+ /// </summary>
+ internal static string WebImage_IncorrectFontFamily {
+ get {
+ return ResourceManager.GetString("WebImage_IncorrectFontFamily", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The &quot;fontStyle&quot; value is invalid. Valid values are: &quot;Regular&quot;, &quot;Bold&quot;, &quot;Italic&quot;, &quot;Underline&quot;, and &quot;Strikeout&quot;..
+ /// </summary>
+ internal static string WebImage_IncorrectFontStyle {
+ get {
+ return ResourceManager.GetString("WebImage_IncorrectFontStyle", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The &quot;horizontalAlign&quot; value is invalid. Valid values are: &quot;Right&quot;, &quot;Left&quot;, and &quot;Center&quot;..
+ /// </summary>
+ internal static string WebImage_IncorrectHorizontalAlignment {
+ get {
+ return ResourceManager.GetString("WebImage_IncorrectHorizontalAlignment", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The &quot;verticalAlign&quot; value is invalid. Valid values are: &quot;Top&quot;, &quot;Bottom&quot;, and &quot;Middle&quot;..
+ /// </summary>
+ internal static string WebImage_IncorrectVerticalAlignment {
+ get {
+ return ResourceManager.GetString("WebImage_IncorrectVerticalAlignment", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Watermark width and height must both be positive or both be zero..
+ /// </summary>
+ internal static string WebImage_IncorrectWidthAndHeight {
+ get {
+ return ResourceManager.GetString("WebImage_IncorrectWidthAndHeight", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to An image could not be constructed from the content provided..
+ /// </summary>
+ internal static string WebImage_InvalidImageContents {
+ get {
+ return ResourceManager.GetString("WebImage_InvalidImageContents", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The &quot;priority&quot; value is invalid. Valid values are &quot;Low&quot;, &quot;Normal&quot; and &quot;High&quot;..
+ /// </summary>
+ internal static string WebMail_InvalidPriority {
+ get {
+ return ResourceManager.GetString("WebMail_InvalidPriority", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A string in the collection is null or empty..
+ /// </summary>
+ internal static string WebMail_ItemInCollectionIsNull {
+ get {
+ return ResourceManager.GetString("WebMail_ItemInCollectionIsNull", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &quot;SmtpServer&quot; was not specified..
+ /// </summary>
+ internal static string WebMail_SmtpServerNotSpecified {
+ get {
+ return ResourceManager.GetString("WebMail_SmtpServerNotSpecified", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to No &quot;From&quot; email address was specified and a default value could not be assigned..
+ /// </summary>
+ internal static string WebMail_UnableToDetermineFrom {
+ get {
+ return ResourceManager.GetString("WebMail_UnableToDetermineFrom", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Helpers/Resources/HelpersResources.resx b/src/System.Web.Helpers/Resources/HelpersResources.resx
new file mode 100644
index 00000000..c07fd75e
--- /dev/null
+++ b/src/System.Web.Helpers/Resources/HelpersResources.resx
@@ -0,0 +1,237 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="Chart_ArgumentConversionFailed" xml:space="preserve">
+ <value>Argument conversion to type "{0}" failed.</value>
+ </data>
+ <data name="Chart_ExceptionDataBindSeriesToString" xml:space="preserve">
+ <value>A series cannot be data-bound to a string object.</value>
+ </data>
+ <data name="Chart_ThemeFileNotFound" xml:space="preserve">
+ <value>The theme file "{0}" could not be found.</value>
+ </data>
+ <data name="Crypto_NotSupportedHashAlg" xml:space="preserve">
+ <value>The hash algorithm '{0}' is not supported, valid values are: sha256, sha1, md5</value>
+ </data>
+ <data name="Image_IncorrectImageFormat" xml:space="preserve">
+ <value>"{0}" is invalid image format. Valid values are image format names like: "JPEG", "BMP", "GIF", "PNG", etc.</value>
+ </data>
+ <data name="Json_UnableToConvertType" xml:space="preserve">
+ <value>Unable to convert to "{0}". Use Json.Decode&lt;T&gt; instead.</value>
+ </data>
+ <data name="ObjectInfo_PreviousDisplayed" xml:space="preserve">
+ <value>Previously Displayed</value>
+ </data>
+ <data name="ObjectInfo_PropertyThrewException" xml:space="preserve">
+ <value>Accessing a property threw an exception: </value>
+ </data>
+ <data name="PathUtils_IncorrectPath" xml:space="preserve">
+ <value>File path "{0}" is invalid.</value>
+ </data>
+ <data name="ServerInfo_AdditionalInfo" xml:space="preserve">
+ <value>Additional server information is available when the page is running with high trust.</value>
+ </data>
+ <data name="ServerInfo_EnvVars" xml:space="preserve">
+ <value>Environment Variables</value>
+ </data>
+ <data name="ServerInfo_Header" xml:space="preserve">
+ <value>ASP.NET Server Information</value>
+ </data>
+ <data name="ServerInfo_HttpRuntime" xml:space="preserve">
+ <value>HTTP Runtime Information</value>
+ </data>
+ <data name="ServerInfo_LegacyCAS" xml:space="preserve">
+ <value>Legacy Code Access Security</value>
+ </data>
+ <data name="ServerInfo_LegacyCasHelpInfo" xml:space="preserve">
+ <value>Legacy Code Access Security has been detected on your system. Microsoft WebPage features require the ASP.NET 4 Code Access Security model. For information about how to resolve this, contact your server administrator.</value>
+ </data>
+ <data name="ServerInfo_NoValue" xml:space="preserve">
+ <value>no value</value>
+ </data>
+ <data name="ServerInfo_ServerConfigTable" xml:space="preserve">
+ <value>Server Configuration</value>
+ </data>
+ <data name="ServerInfo_ServerVars" xml:space="preserve">
+ <value>ASP.NET Server Variables</value>
+ </data>
+ <data name="WebGrid_ColumnNameOrFormatRequired" xml:space="preserve">
+ <value>The column name cannot be null or an empty string unless a custom format is specified.</value>
+ </data>
+ <data name="WebGrid_ColumnNotFound" xml:space="preserve">
+ <value>Column "{0}" does not exist.</value>
+ </data>
+ <data name="WebGrid_DataSourceBound" xml:space="preserve">
+ <value>The WebGrid instance is already bound to a data source.</value>
+ </data>
+ <data name="WebGrid_NoDataSourceBound" xml:space="preserve">
+ <value>A data source must be bound before this operation can be performed.</value>
+ </data>
+ <data name="WebGrid_NotSupportedIfPagingIsDisabled" xml:space="preserve">
+ <value>This operation is not supported when paging is disabled for the "WebGrid" object.</value>
+ </data>
+ <data name="WebGrid_NotSupportedIfSortingIsDisabled" xml:space="preserve">
+ <value>This operation is not supported when sorting is disabled for the "WebGrid" object.</value>
+ </data>
+ <data name="WebGrid_PagerModeMustBeEnabled" xml:space="preserve">
+ <value>To use this argument, pager mode "{0}" must be enabled.</value>
+ </data>
+ <data name="WebGrid_PropertySetterNotSupportedAfterDataBound" xml:space="preserve">
+ <value>This property cannot be set after the "WebGrid" object has been sorted or paged. Make sure that this property is set prior to invoking the "Rows" property directly or indirectly through other methods such as "GetHtml", "Pager", "Table", etc.</value>
+ </data>
+ <data name="WebGrid_RowCountNotSpecified" xml:space="preserve">
+ <value>A value for "rowCount" must be specified when "autoSortAndPage" is set to true and paging is enabled.</value>
+ </data>
+ <data name="WebGrid_SelectLinkText" xml:space="preserve">
+ <value>Select</value>
+ </data>
+ <data name="WebImage_IncorrectColorName" xml:space="preserve">
+ <value>The "fontColor" value is invalid. Valid values are names like "White", "Black", or "DarkBlue", or hexadecimal values in the form "#RRGGBB" or "#RGB".</value>
+ </data>
+ <data name="WebImage_IncorrectFontFamily" xml:space="preserve">
+ <value>The "fontFamily" value is invalid. Valid values are font family names like: "Arial", "Times New Roman", etc. Make sure that the font family you are trying to use is installed on the server.</value>
+ </data>
+ <data name="WebImage_IncorrectFontStyle" xml:space="preserve">
+ <value>The "fontStyle" value is invalid. Valid values are: "Regular", "Bold", "Italic", "Underline", and "Strikeout".</value>
+ </data>
+ <data name="WebImage_IncorrectHorizontalAlignment" xml:space="preserve">
+ <value>The "horizontalAlign" value is invalid. Valid values are: "Right", "Left", and "Center".</value>
+ </data>
+ <data name="WebImage_IncorrectVerticalAlignment" xml:space="preserve">
+ <value>The "verticalAlign" value is invalid. Valid values are: "Top", "Bottom", and "Middle".</value>
+ </data>
+ <data name="WebImage_IncorrectWidthAndHeight" xml:space="preserve">
+ <value>Watermark width and height must both be positive or both be zero.</value>
+ </data>
+ <data name="WebImage_InvalidImageContents" xml:space="preserve">
+ <value>An image could not be constructed from the content provided.</value>
+ </data>
+ <data name="WebMail_InvalidPriority" xml:space="preserve">
+ <value>The "priority" value is invalid. Valid values are "Low", "Normal" and "High".</value>
+ </data>
+ <data name="WebMail_ItemInCollectionIsNull" xml:space="preserve">
+ <value>A string in the collection is null or empty.</value>
+ </data>
+ <data name="WebMail_SmtpServerNotSpecified" xml:space="preserve">
+ <value>"SmtpServer" was not specified.</value>
+ </data>
+ <data name="WebMail_UnableToDetermineFrom" xml:space="preserve">
+ <value>No "From" email address was specified and a default value could not be assigned.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/System.Web.Helpers/ServerInfo.cs b/src/System.Web.Helpers/ServerInfo.cs
new file mode 100644
index 00000000..04cf3d01
--- /dev/null
+++ b/src/System.Web.Helpers/ServerInfo.cs
@@ -0,0 +1,324 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using System.Security;
+using System.Text;
+using System.Web.Helpers.Resources;
+
+namespace System.Web.Helpers
+{
+ /// <summary>
+ /// Provides various info about ASP.NET server.
+ /// </summary>
+ public static class ServerInfo
+ {
+ /// <remarks>
+ /// todo: figure out right place for this
+ /// </remarks>
+ private const string Style =
+ "<style type=\"text/css\">" +
+ " div.server-info { text-align: center; }" +
+ " table.server-info { border-collapse:collapse; text-align:center; margin: auto; width:600px; direction: ltr; }" +
+ " table.server-info tbody tr:nth-child(even){ background-color: #EEE; }" +
+ " table.server-info, table.server-info th, table.server-info td { border:1px solid black; }" +
+ " table.server-info th, table.server-info td " +
+ " { text-align:left; padding:2px; font-family:Tahoma, Arial, sans-serif; font-size:0.75em; }" +
+ " h1.server-info { font-family:Tahoma, Arial, sans-serif; font-size:150%; text-align:center; }" +
+ " table.server-info h2 { font-family:Tahoma, Arial, sans-serif; font-size:125%; text-align:center; }" +
+ " p.server-info { text-align:center; font-family:Tahoma, Arial, sans-serif; font-size:0.75em; }" +
+ " .ital { font-style: italic; } " +
+ " .warn { color: #F00; } " +
+ "</style>";
+
+ internal static IDictionary<string, string> EnvironmentVariables()
+ {
+ // todo: extract well defined subset for special use?
+
+ // use a case-insensitive dictionary since environment variables are case-insensitive.
+ IDictionary<string, string> environmentVariablesResult = new SortedDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ IDictionary environmentVariables;
+
+ // todo: better way to deal with security; query config for trust level?
+ try
+ {
+ environmentVariables = Environment.GetEnvironmentVariables();
+ }
+ catch (SecurityException)
+ {
+ return environmentVariablesResult;
+ }
+
+ foreach (DictionaryEntry entry in environmentVariables)
+ {
+ environmentVariablesResult.Add(entry.Key.ToString(), InsertWhiteSpace(entry.Value.ToString()));
+ }
+
+ return environmentVariablesResult;
+ }
+
+ internal static IDictionary<string, string> ServerVariables()
+ {
+ var httpContext = HttpContext.Current;
+ return ServerVariables(httpContext != null ? new HttpContextWrapper(httpContext) : null);
+ }
+
+ internal static IDictionary<string, string> ServerVariables(HttpContextBase context)
+ {
+ // todo: extract well defined subset for special use?
+
+ IDictionary<string, string> serverVariablesResult = new SortedDictionary<string, string>();
+ NameValueCollection serverVariables;
+
+ // todo: better way to deal with security; query config for trust level?
+ try
+ {
+ if ((context != null) && (context.Request != null))
+ {
+ serverVariables = context.Request.ServerVariables;
+ }
+ else
+ {
+ // Just return empty collection when there is no context available.
+ return serverVariablesResult;
+ }
+ }
+ catch (SecurityException)
+ {
+ return serverVariablesResult;
+ }
+
+ foreach (string key in serverVariables.AllKeys)
+ {
+ // todo: these values contains very long strings with no spaces that distorts table layout - figure out how to deal with it
+ if (key.Equals("ALL_HTTP", StringComparison.OrdinalIgnoreCase) ||
+ key.Equals("ALL_RAW", StringComparison.OrdinalIgnoreCase) ||
+ key.Equals("HTTP_AUTHORIZATION", StringComparison.OrdinalIgnoreCase) ||
+ key.Equals("HTTP_COOKIE", StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ serverVariablesResult.Add(key, InsertWhiteSpace(serverVariables[key]));
+ }
+
+ return serverVariablesResult;
+ }
+
+ internal static IDictionary<string, string> Configuration()
+ {
+ IDictionary<string, string> info = new Dictionary<string, string>();
+
+ // todo: do we need to localize these strings or would that be confusing
+ // (Since we just display API names that are all in English)
+
+ info.Add("Current Local Time", DateTime.Now.ToString(CultureInfo.CurrentCulture));
+ info.Add("Current UTC Time", DateTime.UtcNow.ToString(CultureInfo.CurrentCulture));
+ info.Add("Current Culture", CultureInfo.CurrentCulture.DisplayName);
+
+ info.Add("Machine Name", Environment.MachineName);
+ info.Add("OS Version", Environment.OSVersion.ToString());
+ info.Add("ASP.NET Version", Environment.Version.ToString());
+ info.Add("User Name", Environment.UserName);
+ info.Add("User Interactive", Environment.UserInteractive.ToString());
+ info.Add("Processor Count", Environment.ProcessorCount.ToString(CultureInfo.InvariantCulture));
+ info.Add("Tick Count", Environment.TickCount.ToString(CultureInfo.InvariantCulture));
+
+ // Calls bellow require full trust.
+
+ try
+ {
+ info.Add("Current Directory", Environment.CurrentDirectory);
+ }
+ catch (SecurityException)
+ {
+ return info;
+ }
+
+ info.Add("System Directory", Environment.SystemDirectory);
+ info.Add("User Domain Name", Environment.UserDomainName);
+ info.Add("Working Set", Environment.WorkingSet.ToString(CultureInfo.InvariantCulture) + " bytes");
+
+ return info;
+ }
+
+ internal static IDictionary<string, string> HttpRuntimeInfo()
+ {
+ IDictionary<string, string> info = new Dictionary<string, string>();
+
+ // todo: better way to deal with security; query config for trust level?
+ try
+ {
+ info.Add("CLR Install Directory", HttpRuntime.ClrInstallDirectory);
+ }
+ catch (SecurityException)
+ {
+ return info;
+ }
+
+ try
+ {
+ info.Add("Codegen Directory", HttpRuntime.CodegenDir);
+ info.Add("Bin Directory", HttpRuntime.BinDirectory);
+ info.Add("AppDomain Application Path", HttpRuntime.AppDomainAppPath);
+ }
+ catch (ArgumentException)
+ {
+ // do nothing
+ // These APIs don't check if path is set before setting security demands, which causes exception.
+ // So far this happens only when running from unit tests.
+ }
+
+ info.Add("Asp Install Directory", HttpRuntime.AspInstallDirectory);
+ info.Add("Machine Configuration Directory", HttpRuntime.MachineConfigurationDirectory);
+
+ info.Add("AppDomain Id", HttpRuntime.AppDomainId);
+ info.Add("AppDomain Application Id", HttpRuntime.AppDomainAppId);
+ info.Add("AppDomain Application Virtual Path", HttpRuntime.AppDomainAppVirtualPath);
+
+ info.Add("Asp Client Script Physical Path", HttpRuntime.AspClientScriptPhysicalPath);
+
+ info.Add("Asp Client Script Virtual Path", HttpRuntime.AspClientScriptVirtualPath);
+
+ info.Add("Cache Size", HttpRuntime.Cache.Count.ToString(CultureInfo.InvariantCulture));
+ info.Add("Cache Effective Percentage Physical Memory Limit", HttpRuntime.Cache.EffectivePercentagePhysicalMemoryLimit.ToString(CultureInfo.InvariantCulture));
+ info.Add("Cache Effective Private Bytes Limit", HttpRuntime.Cache.EffectivePrivateBytesLimit.ToString(CultureInfo.InvariantCulture));
+
+ info.Add("On UNC Share", HttpRuntime.IsOnUNCShare.ToString());
+
+ return info;
+ }
+
+ internal static IDictionary<string, string> LegacyCAS()
+ {
+ return LegacyCAS(AppDomain.CurrentDomain);
+ }
+
+ internal static IDictionary<string, string> LegacyCAS(AppDomain appDomain)
+ {
+ IDictionary<string, string> info = new Dictionary<string, string>();
+
+ try
+ {
+ bool legacyCasModeEnabled = !appDomain.IsHomogenous;
+ if (legacyCasModeEnabled)
+ {
+ info[HelpersResources.ServerInfo_LegacyCAS] = HelpersResources.ServerInfo_LegacyCasHelpInfo;
+ }
+ }
+ catch (SecurityException)
+ {
+ return info;
+ }
+ return info;
+ }
+
+ /// <summary>
+ /// Generates HTML required to display server information.
+ /// </summary>
+ /// <remarks>
+ /// HTML generated is XHTML 1.0 compliant but not XHTML 1.1 or HTML5 compliant. The reason is that we
+ /// generate &lt;style&gt; tag inside &lt;body&gt; tag, which is not allowed. This is by design for now since ServerInfo
+ /// is debugging aid and should not be used as a permanent part of any web page.
+ /// </remarks>
+ [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate",
+ Justification = "This could be time consuming operation that does not just retrieve a field.")]
+ public static HtmlString GetHtml()
+ {
+ StringBuilder sb = new StringBuilder(Style);
+
+ sb.AppendLine(String.Format(CultureInfo.InvariantCulture, "<h1 class=\"server-info\">{0}</h1>",
+ HttpUtility.HtmlEncode(HelpersResources.ServerInfo_Header)));
+
+ var configuration = Configuration();
+ Debug.Assert((configuration != null) && (configuration.Count > 0));
+ PrintInfoSection(sb, HttpUtility.HtmlEncode(HelpersResources.ServerInfo_ServerConfigTable), configuration);
+
+ var serverVariables = ServerVariables();
+ Debug.Assert((serverVariables != null));
+ PrintInfoSection(sb, HelpersResources.ServerInfo_ServerVars, serverVariables);
+
+ var legacyCAS = LegacyCAS();
+ if (legacyCAS.Any())
+ {
+ PrintInfoSection(sb, HelpersResources.ServerInfo_LegacyCAS, legacyCAS);
+ }
+
+ // Info below is not available in medium trust.
+
+ var httpRuntimeInfo = HttpRuntimeInfo();
+ Debug.Assert(httpRuntimeInfo != null);
+
+ if (!httpRuntimeInfo.Any())
+ {
+ sb.AppendLine(String.Format(CultureInfo.InvariantCulture, "<p class=\"server-info\">{0}</p>",
+ HttpUtility.HtmlEncode(HelpersResources.ServerInfo_AdditionalInfo)));
+ return new HtmlString(sb.ToString());
+ }
+ else
+ {
+ PrintInfoSection(sb, HelpersResources.ServerInfo_HttpRuntime, httpRuntimeInfo);
+
+ var envVariables = EnvironmentVariables();
+ Debug.Assert(envVariables != null);
+ PrintInfoSection(sb, HelpersResources.ServerInfo_EnvVars, envVariables);
+ }
+
+ return new HtmlString(sb.ToString());
+ }
+
+ /// <summary>
+ /// Renders a table section printing out rows and columns.
+ /// </summary>
+ private static void PrintInfoSection(StringBuilder builder, string sectionTitle, IDictionary<string, string> entries)
+ {
+ builder.AppendLine("<div class=\"server-info\">");
+ builder.AppendLine("<table class=\"server-info\" dir=\"ltr\">");
+ if (!String.IsNullOrEmpty(sectionTitle))
+ {
+ builder.AppendLine("<caption>");
+ builder.AppendFormat(CultureInfo.InvariantCulture, "<h2>{0}</h2>", HttpUtility.HtmlEncode(sectionTitle)).AppendLine();
+ builder.AppendLine("</caption>");
+ }
+ builder.AppendLine("<colgroup><col style=\"width:30%;\" /> <col style=\"width:70%;\" /></colgroup>");
+ builder.AppendLine("<tbody>");
+ foreach (var entry in entries)
+ {
+ var css = String.Empty;
+ string value = entry.Value;
+ if (entry.Key == HelpersResources.ServerInfo_LegacyCAS)
+ {
+ // TODO: suboptimal solution, but its easier to do this than come up with something that works better
+ css = "warn";
+ }
+ else if (String.IsNullOrEmpty(entry.Value))
+ {
+ css = "ital";
+ value = HelpersResources.ServerInfo_NoValue;
+ }
+ if (css.Any())
+ {
+ css = " class=\"" + css + "\"";
+ }
+ builder.Append("<tr>");
+ builder.AppendFormat(CultureInfo.InvariantCulture, "<th scope=\"row\">{0}</th>", HttpUtility.HtmlEncode(entry.Key));
+ builder.AppendFormat(CultureInfo.InvariantCulture, "<td{0}>{1}</td>", css, HttpUtility.HtmlEncode(value));
+ builder.AppendLine("</tr>");
+ }
+ builder.AppendLine("</tbody>");
+ builder.AppendLine("</table>");
+ builder.AppendLine("</div>");
+ }
+
+ /// <summary>
+ /// Inserts spaces after ',' and ';' so table can be rendered properly.
+ /// </summary>
+ private static string InsertWhiteSpace(string s)
+ {
+ return s.Replace(",", ", ").Replace(";", "; ");
+ }
+ }
+}
diff --git a/src/System.Web.Helpers/SortDirection.cs b/src/System.Web.Helpers/SortDirection.cs
new file mode 100644
index 00000000..37b5d113
--- /dev/null
+++ b/src/System.Web.Helpers/SortDirection.cs
@@ -0,0 +1,8 @@
+namespace System.Web.Helpers
+{
+ public enum SortDirection
+ {
+ Ascending,
+ Descending
+ }
+}
diff --git a/src/System.Web.Helpers/System.Web.Helpers.csproj b/src/System.Web.Helpers/System.Web.Helpers.csproj
new file mode 100644
index 00000000..e14311bf
--- /dev/null
+++ b/src/System.Web.Helpers/System.Web.Helpers.csproj
@@ -0,0 +1,133 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <CodeAnalysis Condition=" '$(CodeAnalysis)' == '' ">false</CodeAnalysis>
+ <ProductVersion>9.0.30729</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{9B7E3740-6161-4548-833C-4BBCA43B970E}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <RootNamespace>System.Web.Helpers</RootNamespace>
+ <AssemblyName>System.Web.Helpers</AssemblyName>
+ <FileAlignment>512</FileAlignment>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>..\..\bin\Debug\</OutputPath>
+ <DefineConstants>TRACE;DEBUG;ASPNETWEBPAGES</DefineConstants>
+ <CodeAnalysisRuleSet>..\WebHelpers.ruleset</CodeAnalysisRuleSet>
+ <DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
+ <NoWarn>1591</NoWarn>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>..\..\bin\Release\</OutputPath>
+ <DefineConstants>TRACE;ASPNETWEBPAGES</DefineConstants>
+ <CodeAnalysisRuleSet>..\WebHelpers.ruleset</CodeAnalysisRuleSet>
+ <RunCodeAnalysis>$(CodeAnalysis)</RunCodeAnalysis>
+ <DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
+ <NoWarn>1591</NoWarn>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'CodeCoverage|AnyCPU'">
+ <DebugSymbols>true</DebugSymbols>
+ <OutputPath>..\..\bin\CodeCoverage\</OutputPath>
+ <DefineConstants>TRACE;DEBUG;CODE_COVERAGE;ASPNETWEBPAGES</DefineConstants>
+ <DebugType>full</DebugType>
+ <CodeAnalysisRuleSet>..\WebHelpers.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="System" />
+ <Reference Include="System.Configuration" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.Drawing" />
+ <Reference Include="System.Runtime.Caching" />
+ <Reference Include="System.Web" />
+ <Reference Include="System.Web.DataVisualization" />
+ <Reference Include="System.Web.Extensions" />
+ <Reference Include="Microsoft.CSharp" />
+ <Reference Include="System.XML" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="..\DynamicHelper.cs">
+ <Link>Common\DynamicHelper.cs</Link>
+ </Compile>
+ <Compile Include="..\CommonAssemblyInfo.cs">
+ <Link>Properties\CommonAssemblyInfo.cs</Link>
+ </Compile>
+ <Compile Include="..\GlobalSuppressions.cs">
+ <Link>Common\GlobalSuppressions.cs</Link>
+ </Compile>
+ <Compile Include="..\TransparentCommonAssemblyInfo.cs">
+ <Link>Properties\TransparentCommonAssemblyInfo.cs</Link>
+ </Compile>
+ <Compile Include="Chart\Chart.cs" />
+ <Compile Include="Chart\ChartTheme.cs" />
+ <Compile Include="Common\VirtualPathUtil.cs" />
+ <Compile Include="ConversionUtil.cs" />
+ <Compile Include="Crypto.cs" />
+ <Compile Include="DynamicJavaScriptConverter.cs" />
+ <Compile Include="DynamicJsonArray.cs" />
+ <Compile Include="DynamicJsonObject.cs" />
+ <Compile Include="GlobalSuppressions.cs" />
+ <Compile Include="HtmlElement.cs" />
+ <Compile Include="HtmlObjectPrinter.cs" />
+ <Compile Include="Json.cs" />
+ <Compile Include="ObjectInfo.cs" />
+ <Compile Include="ObjectVisitor.cs" />
+ <Compile Include="WebCache.cs" />
+ <Compile Include="ServerInfo.cs" />
+ <Compile Include="Resources\HelpersResources.Designer.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>HelpersResources.resx</DependentUpon>
+ </Compile>
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="SortDirection.cs" />
+ <Compile Include="WebGrid\IWebGridDataSource.cs" />
+ <Compile Include="WebGrid\PreComputedGridDataSource.cs" />
+ <Compile Include="WebGrid\SortInfo.cs" />
+ <Compile Include="WebGrid\WebGridDataSource.cs" />
+ <Compile Include="WebGrid\WebGrid.cs" />
+ <Compile Include="WebGrid\WebGridColumn.cs" />
+ <Compile Include="WebGrid\WebGridPagerModes.cs" />
+ <Compile Include="WebGrid\_WebGridRenderer.generated.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>_WebGridRenderer.cshtml</DependentUpon>
+ </Compile>
+ <Compile Include="WebGrid\WebGridRow.cs" />
+ <Compile Include="WebImage.cs" />
+ <Compile Include="WebMail.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="Resources\HelpersResources.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>HelpersResources.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\System.Web.WebPages\System.Web.WebPages.csproj">
+ <Project>{76EFA9C5-8D7E-4FDF-B710-E20F8B6B00D2}</Project>
+ <Name>System.Web.WebPages</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <Content Include="WebGrid\_WebGridRenderer.cshtml">
+ <Generator>RazorHelperGenerator</Generator>
+ <LastGenOutput>_WebGridRenderer.generated.cs</LastGenOutput>
+ <CustomToolNamespace>System.Web.Helpers</CustomToolNamespace>
+ </Content>
+ </ItemGroup>
+ <ItemGroup>
+ <CodeAnalysisDictionary Include="..\CodeAnalysisDictionary.xml" />
+ </ItemGroup>
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+</Project> \ No newline at end of file
diff --git a/src/System.Web.Helpers/WebCache.cs b/src/System.Web.Helpers/WebCache.cs
new file mode 100644
index 00000000..a55d87bd
--- /dev/null
+++ b/src/System.Web.Helpers/WebCache.cs
@@ -0,0 +1,48 @@
+using System.Globalization;
+using System.Runtime.Caching;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.Helpers
+{
+ public static class WebCache
+ {
+ public static void Set(string key, object value, int minutesToCache = 20, bool slidingExpiration = true)
+ {
+ if (minutesToCache <= 0)
+ {
+ throw new ArgumentOutOfRangeException("minutesToCache",
+ String.Format(CultureInfo.CurrentCulture, CommonResources.Argument_Must_Be_GreaterThan, 0));
+ }
+ else if (slidingExpiration && (minutesToCache > 365 * 24 * 60))
+ {
+ // For sliding expiration policies, MemoryCache has a time limit of 365 days.
+ throw new ArgumentOutOfRangeException("minutesToCache",
+ String.Format(CultureInfo.CurrentCulture, CommonResources.Argument_Must_Be_LessThanOrEqualTo, 365 * 24 * 60));
+ }
+
+ CacheItemPolicy policy = new CacheItemPolicy();
+ TimeSpan expireTime = new TimeSpan(0, minutesToCache, 0);
+
+ if (slidingExpiration)
+ {
+ policy.SlidingExpiration = expireTime;
+ }
+ else
+ {
+ policy.AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(minutesToCache);
+ }
+
+ MemoryCache.Default.Set(key, value, policy);
+ }
+
+ public static dynamic Get(string key)
+ {
+ return MemoryCache.Default.Get(key);
+ }
+
+ public static dynamic Remove(string key)
+ {
+ return MemoryCache.Default.Remove(key);
+ }
+ }
+}
diff --git a/src/System.Web.Helpers/WebGrid/IWebGridDataSource.cs b/src/System.Web.Helpers/WebGrid/IWebGridDataSource.cs
new file mode 100644
index 00000000..3d6d5d19
--- /dev/null
+++ b/src/System.Web.Helpers/WebGrid/IWebGridDataSource.cs
@@ -0,0 +1,11 @@
+using System.Collections.Generic;
+
+namespace System.Web.Helpers
+{
+ internal interface IWebGridDataSource
+ {
+ int TotalRowCount { get; }
+
+ IList<WebGridRow> GetRows(SortInfo sortInfo, int pageIndex);
+ }
+}
diff --git a/src/System.Web.Helpers/WebGrid/PreComputedGridDataSource.cs b/src/System.Web.Helpers/WebGrid/PreComputedGridDataSource.cs
new file mode 100644
index 00000000..710c9da7
--- /dev/null
+++ b/src/System.Web.Helpers/WebGrid/PreComputedGridDataSource.cs
@@ -0,0 +1,36 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+
+namespace System.Web.Helpers
+{
+ /// <summary>
+ /// Source wrapper for data provided by the user that is already sorted and paged. The user provides the WebGrid the rows to bind and additionally the total number of rows that
+ /// are available.
+ /// </summary>
+ internal sealed class PreComputedGridDataSource : IWebGridDataSource
+ {
+ private readonly int _totalRows;
+ private readonly IList<WebGridRow> _rows;
+
+ public PreComputedGridDataSource(WebGrid grid, IEnumerable<dynamic> values, int totalRows)
+ {
+ Debug.Assert(grid != null);
+ Debug.Assert(values != null);
+
+ _totalRows = totalRows;
+ _rows = values.Select((value, index) => new WebGridRow(grid, value: value, rowIndex: index)).ToList();
+ }
+
+ public int TotalRowCount
+ {
+ get { return _totalRows; }
+ }
+
+ public IList<WebGridRow> GetRows(SortInfo sortInfo, int pageIndex)
+ {
+ // Data is already sorted and paged. Ignore parameters.
+ return _rows;
+ }
+ }
+}
diff --git a/src/System.Web.Helpers/WebGrid/SortInfo.cs b/src/System.Web.Helpers/WebGrid/SortInfo.cs
new file mode 100644
index 00000000..3d58fe5f
--- /dev/null
+++ b/src/System.Web.Helpers/WebGrid/SortInfo.cs
@@ -0,0 +1,31 @@
+namespace System.Web.Helpers
+{
+ internal sealed class SortInfo : IEquatable<SortInfo>
+ {
+ public string SortColumn { get; set; }
+
+ public SortDirection SortDirection { get; set; }
+
+ public bool Equals(SortInfo other)
+ {
+ return other != null
+ && String.Equals(SortColumn, other.SortColumn, StringComparison.OrdinalIgnoreCase)
+ && SortDirection == other.SortDirection;
+ }
+
+ public override bool Equals(object obj)
+ {
+ SortInfo sortInfo = obj as SortInfo;
+ if (sortInfo != null)
+ {
+ return Equals(sortInfo);
+ }
+ return base.Equals(obj);
+ }
+
+ public override int GetHashCode()
+ {
+ return SortColumn.GetHashCode();
+ }
+ }
+}
diff --git a/src/System.Web.Helpers/WebGrid/WebGrid.cs b/src/System.Web.Helpers/WebGrid/WebGrid.cs
new file mode 100644
index 00000000..3ab5ab5d
--- /dev/null
+++ b/src/System.Web.Helpers/WebGrid/WebGrid.cs
@@ -0,0 +1,947 @@
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Dynamic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Web.Helpers.Resources;
+using System.Web.WebPages;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.Helpers
+{
+ public class WebGrid
+ {
+ private const string AjaxUpdateScript = "$({1}).swhgLoad({0},{1}{2});";
+ private readonly HttpContextBase _context;
+ private readonly bool _canPage;
+ private readonly bool _canSort;
+ private readonly string _ajaxUpdateContainerId;
+ private readonly string _ajaxUpdateCallback;
+ private readonly string _defaultSort;
+ private readonly string _pageFieldName = "page";
+ private readonly string _sortDirectionFieldName = "sortdir";
+ private readonly string _selectionFieldName = "row";
+ private readonly string _sortFieldName = "sort";
+ private readonly string _fieldNamePrefix;
+ private int _pageIndex = -1;
+ private bool _pageIndexSet;
+ private int _rowsPerPage;
+ private int _selectedIndex = -1;
+ private bool _selectedIndexSet;
+ private string _sortColumn;
+ private bool _sortColumnSet;
+ private bool _sortColumnExplicitlySet;
+ private SortDirection _sortDirection;
+ private bool _sortDirectionSet;
+ private IWebGridDataSource _dataSource;
+ private bool _dataSourceBound;
+ private bool _dataSourceMaterialized;
+ private IEnumerable<string> _columnNames;
+ private Type _elementType;
+ private IList<WebGridRow> _rows;
+
+ /// <param name="source">Data source</param>
+ /// <param name="columnNames">Data source column names. Auto-populated by default.</param>
+ /// <param name="defaultSort">Default sort column.</param>
+ /// <param name="rowsPerPage">Number of rows per page.</param>
+ /// <param name="canPage"></param>
+ /// <param name="canSort"></param>
+ /// <param name="ajaxUpdateContainerId">ID for the grid's container element. This enables AJAX support.</param>
+ /// <param name="ajaxUpdateCallback">Callback function for the AJAX functionality once the update is complete</param>
+ /// <param name="fieldNamePrefix">Prefix for query string fields to support multiple grids.</param>
+ /// <param name="pageFieldName">Query string field name for page number.</param>
+ /// <param name="selectionFieldName">Query string field name for selected row number.</param>
+ /// <param name="sortFieldName">Query string field name for sort column.</param>
+ /// <param name="sortDirectionFieldName">Query string field name for sort direction.</param>
+#if CODE_COVERAGE
+ [ExcludeFromCodeCoverage]
+#endif
+ public WebGrid(
+ IEnumerable<dynamic> source = null,
+ IEnumerable<string> columnNames = null,
+ string defaultSort = null,
+ int rowsPerPage = 10,
+ bool canPage = true,
+ bool canSort = true,
+ string ajaxUpdateContainerId = null,
+ string ajaxUpdateCallback = null,
+ string fieldNamePrefix = null,
+ string pageFieldName = null,
+ string selectionFieldName = null,
+ string sortFieldName = null,
+ string sortDirectionFieldName = null)
+ : this(new HttpContextWrapper(Web.HttpContext.Current), defaultSort: defaultSort, rowsPerPage: rowsPerPage, canPage: canPage,
+ canSort: canSort, ajaxUpdateContainerId: ajaxUpdateContainerId, ajaxUpdateCallback: ajaxUpdateCallback, fieldNamePrefix: fieldNamePrefix, pageFieldName: pageFieldName,
+ selectionFieldName: selectionFieldName, sortFieldName: sortFieldName, sortDirectionFieldName: sortDirectionFieldName)
+ {
+ if (source != null)
+ {
+ Bind(source, columnNames);
+ }
+ }
+
+ // NOTE: WebGrid uses an IEnumerable<dynamic> data source instead of IEnumerable<T> to avoid generics in the syntax.
+ internal WebGrid(
+ HttpContextBase context,
+ string defaultSort = null,
+ int rowsPerPage = 10,
+ bool canPage = true,
+ bool canSort = true,
+ string ajaxUpdateContainerId = null,
+ string ajaxUpdateCallback = null,
+ string fieldNamePrefix = null,
+ string pageFieldName = null,
+ string selectionFieldName = null,
+ string sortFieldName = null,
+ string sortDirectionFieldName = null)
+ {
+ Debug.Assert(context != null);
+
+ if (rowsPerPage < 1)
+ {
+ throw new ArgumentOutOfRangeException("rowsPerPage", String.Format(CultureInfo.CurrentCulture,
+ CommonResources.Argument_Must_Be_GreaterThanOrEqualTo, 1));
+ }
+
+ _context = context;
+ _defaultSort = defaultSort;
+ _rowsPerPage = rowsPerPage;
+ _canPage = canPage;
+ _canSort = canSort;
+ _ajaxUpdateContainerId = ajaxUpdateContainerId;
+ _ajaxUpdateCallback = ajaxUpdateCallback;
+
+ _fieldNamePrefix = fieldNamePrefix;
+
+ if (!String.IsNullOrEmpty(pageFieldName))
+ {
+ _pageFieldName = pageFieldName;
+ }
+ if (!String.IsNullOrEmpty(selectionFieldName))
+ {
+ _selectionFieldName = selectionFieldName;
+ }
+ if (!String.IsNullOrEmpty(sortFieldName))
+ {
+ _sortFieldName = sortFieldName;
+ }
+ if (!String.IsNullOrEmpty(sortDirectionFieldName))
+ {
+ _sortDirectionFieldName = sortDirectionFieldName;
+ }
+ }
+
+ public IEnumerable<string> ColumnNames
+ {
+ get
+ {
+ // Review: Assuming that the users always binds the source and provides column names / we infer the default columns names on binding
+ // Would not work if we want to allow column names to be independently set.
+ EnsureDataBound();
+ return _columnNames;
+ }
+ }
+
+ public bool CanSort
+ {
+ get { return _canSort; }
+ }
+
+ public string AjaxUpdateContainerId
+ {
+ get { return _ajaxUpdateContainerId; }
+ }
+
+ public bool IsAjaxEnabled
+ {
+ get { return !String.IsNullOrEmpty(_ajaxUpdateContainerId); }
+ }
+
+ public string AjaxUpdateCallback
+ {
+ get { return _ajaxUpdateCallback; }
+ }
+
+ public string FieldNamePrefix
+ {
+ get { return _fieldNamePrefix ?? String.Empty; }
+ }
+
+ public bool HasSelection
+ {
+ get { return SelectedIndex >= 0; }
+ }
+
+ public int PageCount
+ {
+ get
+ {
+ if (!_canPage)
+ {
+ return 1;
+ }
+ return (int)Math.Ceiling((double)TotalRowCount / RowsPerPage);
+ }
+ }
+
+ public string PageFieldName
+ {
+ get { return FieldNamePrefix + _pageFieldName; }
+ }
+
+ public int PageIndex
+ {
+ get
+ {
+ if (!_canPage)
+ {
+ //Default page index is 0
+ return 0;
+ }
+ if (!_pageIndexSet)
+ {
+ int page;
+ if (!_canPage || !Int32.TryParse(QueryString[PageFieldName], out page) || (page < 1))
+ {
+ page = 1;
+ }
+
+ if (_dataSourceBound && page > PageCount)
+ {
+ page = PageCount;
+ }
+
+ _pageIndex = page - 1;
+ _pageIndexSet = true;
+ }
+ return _pageIndex;
+ }
+ set
+ {
+ if (!_canPage)
+ {
+ throw new NotSupportedException(HelpersResources.WebGrid_NotSupportedIfPagingIsDisabled);
+ }
+
+ if (!_dataSourceBound)
+ {
+ // Allow the user to specify arbitrary non-negative values before data binding
+ if (value < 0)
+ {
+ throw new ArgumentOutOfRangeException("value", String.Format(CultureInfo.CurrentCulture,
+ CommonResources.Argument_Must_Be_GreaterThanOrEqualTo, 0));
+ }
+ else
+ {
+ _pageIndex = value;
+ _pageIndexSet = true;
+ }
+ }
+ else
+ {
+ // Once data bound, perform bounds check on the PageIndex. Also ensure the data source has not been materialized.
+ if ((value < 0) || (value >= PageCount))
+ {
+ throw new ArgumentOutOfRangeException("value", String.Format(CultureInfo.CurrentCulture,
+ CommonResources.Argument_Must_Be_Between, 0, (PageCount - 1)));
+ }
+ else if (value != _pageIndex)
+ {
+ EnsureDataSourceNotMaterialized();
+ _pageIndex = value;
+ _pageIndexSet = true;
+ }
+ }
+ }
+ }
+
+ public IList<WebGridRow> Rows
+ {
+ get
+ {
+ EnsureDataBound();
+ if (!_dataSourceMaterialized)
+ {
+ _rows = _dataSource.GetRows(SortInfo, PageIndex);
+ _dataSourceMaterialized = true;
+ }
+ return _rows;
+ }
+ }
+
+ public int RowsPerPage
+ {
+ get { return _rowsPerPage; }
+ }
+
+ public WebGridRow SelectedRow
+ {
+ get
+ {
+ if ((SelectedIndex >= 0) && (SelectedIndex < Rows.Count))
+ {
+ return Rows[SelectedIndex];
+ }
+ return null;
+ }
+ }
+
+ public int SelectedIndex
+ {
+ get
+ {
+ if (!_selectedIndexSet)
+ {
+ int row;
+ // Range checking should not use Rows.Count since this will cause paging and sorting.
+ // Review: side effect is that HasSelection will return true if Rows.Count (current page's
+ // row count) is less than both SelectedIndex and RowsPerPage. This scenario should only
+ // happen if someone manually modifies the query string.
+ // If paging isn't enabled, this getter isn't doing a upper bounds check on the value.
+ if ((!Int32.TryParse(QueryString[SelectionFieldName], out row)) || (row < 1) || (_canPage && (row > RowsPerPage)))
+ {
+ row = 0;
+ }
+ _selectedIndex = row - 1;
+ _selectedIndexSet = true;
+ }
+ return _selectedIndex;
+ }
+ set
+ {
+ if (_selectedIndex != value)
+ {
+ EnsureDataSourceNotMaterialized();
+ _selectedIndex = value;
+ }
+ _selectedIndexSet = true;
+ }
+ }
+
+ public string SelectionFieldName
+ {
+ get { return FieldNamePrefix + _selectionFieldName; }
+ }
+
+ public string SortColumn
+ {
+ get
+ {
+ if (!_sortColumnSet)
+ {
+ string sortColumn = QueryString[SortFieldName];
+ if (!_dataSourceBound || ValidateSortColumn(sortColumn))
+ {
+ _sortColumn = sortColumn;
+ _sortColumnSet = true;
+ }
+ }
+ if (String.IsNullOrEmpty(_sortColumn))
+ {
+ return _defaultSort ?? String.Empty;
+ }
+ return _sortColumn;
+ }
+ set
+ {
+ EnsureDataBound();
+ if (!SortColumn.Equals(value, StringComparison.OrdinalIgnoreCase))
+ {
+ EnsureDataSourceNotMaterialized();
+ _sortColumn = value;
+ }
+ _sortColumnSet = true;
+ _sortColumnExplicitlySet = true;
+ }
+ }
+
+ public SortDirection SortDirection
+ {
+ get
+ {
+ if (!_sortDirectionSet)
+ {
+ string sortDirection = QueryString[SortDirectionFieldName];
+ if (sortDirection != null)
+ {
+ if (sortDirection.Equals("DESC", StringComparison.OrdinalIgnoreCase) ||
+ sortDirection.Equals("DESCENDING", StringComparison.OrdinalIgnoreCase))
+ {
+ _sortDirection = SortDirection.Descending;
+ }
+ }
+ _sortDirectionSet = true;
+ }
+ return _sortDirection;
+ }
+ set
+ {
+ if (!_dataSourceBound)
+ {
+ _sortDirection = value;
+ }
+ else if (_sortDirection != value)
+ {
+ EnsureDataSourceNotMaterialized();
+ _sortDirection = value;
+ }
+ _sortDirectionSet = true;
+ }
+ }
+
+ private SortInfo SortInfo
+ {
+ get { return new SortInfo { SortColumn = SortColumn, SortDirection = SortDirection }; }
+ }
+
+ public string SortDirectionFieldName
+ {
+ get { return FieldNamePrefix + _sortDirectionFieldName; }
+ }
+
+ public string SortFieldName
+ {
+ get { return FieldNamePrefix + _sortFieldName; }
+ }
+
+ public int TotalRowCount
+ {
+ get
+ {
+ EnsureDataBound();
+ return _dataSource.TotalRowCount;
+ }
+ }
+
+ private HttpContextBase HttpContext
+ {
+ get { return _context; }
+ }
+
+ private NameValueCollection QueryString
+ {
+ get { return HttpContext.Request.QueryString; }
+ }
+
+ internal static Type GetElementType(IEnumerable<dynamic> source)
+ {
+ Debug.Assert(source != null, "source cannot be null");
+ Type sourceType = source.GetType();
+
+ if (source.FirstOrDefault() is IDynamicMetaObjectProvider)
+ {
+ return typeof(IDynamicMetaObjectProvider);
+ }
+ else if (sourceType.IsArray)
+ {
+ return sourceType.GetElementType();
+ }
+ Type elementType = sourceType.GetInterfaces().Select(GetGenericEnumerableType).FirstOrDefault(t => t != null);
+
+ Debug.Assert(elementType != null);
+ return elementType;
+ }
+
+ private static Type GetGenericEnumerableType(Type type)
+ {
+ Type enumerableType = typeof(IEnumerable<>);
+ if (type.IsGenericType && enumerableType.IsAssignableFrom(type.GetGenericTypeDefinition()))
+ {
+ return type.GetGenericArguments()[0];
+ }
+ return null;
+ }
+
+ public WebGrid Bind(IEnumerable<dynamic> source, IEnumerable<string> columnNames = null, bool autoSortAndPage = true, int rowCount = -1)
+ {
+ if (_dataSourceBound)
+ {
+ throw new InvalidOperationException(HelpersResources.WebGrid_DataSourceBound);
+ }
+ if (source == null)
+ {
+ throw new ArgumentNullException("source");
+ }
+ if (!autoSortAndPage && _canPage && rowCount == -1)
+ {
+ throw new ArgumentException(HelpersResources.WebGrid_RowCountNotSpecified, "rowCount");
+ }
+
+ _elementType = GetElementType(source);
+ if (_columnNames == null)
+ {
+ _columnNames = columnNames ?? GetDefaultColumnNames(source, elementType: _elementType);
+ }
+
+ if (!autoSortAndPage)
+ {
+ _dataSource = new PreComputedGridDataSource(grid: this, values: source, totalRows: rowCount);
+ }
+ else
+ {
+ WebGridDataSource dataSource = new WebGridDataSource(grid: this, values: source, elementType: _elementType, canPage: _canPage, canSort: _canSort);
+ dataSource.DefaultSort = new SortInfo { SortColumn = _defaultSort, SortDirection = SortDirection.Ascending };
+ dataSource.RowsPerPage = _rowsPerPage;
+ _dataSource = dataSource;
+ }
+ _dataSourceBound = true;
+ ValidatePreDataBoundValues();
+ return this;
+ }
+
+ // todo: add templating from file support
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Non-static for syntax, and in case we want to check column existence.")]
+ public WebGridColumn Column(string columnName = null, string header = null, Func<dynamic, object> format = null, string style = null,
+ bool canSort = true)
+ {
+ if (String.IsNullOrEmpty(columnName))
+ {
+ if (format == null)
+ {
+ throw new ArgumentException(HelpersResources.WebGrid_ColumnNameOrFormatRequired, "columnName");
+ }
+ }
+
+ return new WebGridColumn { ColumnName = columnName, Header = header, Format = format, Style = style, CanSort = canSort };
+ }
+
+ // Should we keep this no-op API for improved WebGrid syntax? Alternatives are:
+ // 1. columns: grid.Columns(
+ // grid.Column(...), grid.Column(...)
+ // )
+ // 2. columns: new[] {
+ // grid.Column(...), grid.Column(...)
+ // }
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Review: No-op API for syntax simplification?")]
+ public WebGridColumn[] Columns(params WebGridColumn[] columnSet)
+ {
+ return columnSet;
+ }
+
+ public IHtmlString GetContainerUpdateScript(string path)
+ {
+ var script = String.Format(CultureInfo.InvariantCulture, AjaxUpdateScript,
+ HttpUtility.JavaScriptStringEncode(path, addDoubleQuotes: true),
+ HttpUtility.JavaScriptStringEncode('#' + AjaxUpdateContainerId, addDoubleQuotes: true),
+ !String.IsNullOrEmpty(AjaxUpdateCallback) ? ',' + HttpUtility.JavaScriptStringEncode(AjaxUpdateCallback) : String.Empty);
+
+ return new HtmlString(HttpUtility.HtmlAttributeEncode(script));
+ }
+
+ /// <summary>
+ /// Gets the HTML for a table with a pager.
+ /// </summary>
+ /// <param name="tableStyle">Table class for styling.</param>
+ /// <param name="headerStyle">Header row class for styling.</param>
+ /// <param name="footerStyle">Footer row class for styling.</param>
+ /// <param name="rowStyle">Row class for styling (odd rows only).</param>
+ /// <param name="alternatingRowStyle">Row class for styling (even rows only).</param>
+ /// <param name="selectedRowStyle">Selected row class for styling.</param>
+ /// <param name="displayHeader">Whether the header row should be displayed.</param>
+ /// <param name="caption">The string displayed as the table caption</param>
+ /// <param name="fillEmptyRows">Whether the table can add empty rows to ensure the rowsPerPage row count.</param>
+ /// <param name="emptyRowCellValue">Value used to populate empty rows. This property is only used when <paramref name="fillEmptyRows"/> is set</param>
+ /// <param name="columns">Column model for customizing column rendering.</param>
+ /// <param name="exclusions">Columns to exclude when auto-populating columns.</param>
+ /// <param name="mode">Modes for pager rendering.</param>
+ /// <param name="firstText">Text for link to first page.</param>
+ /// <param name="previousText">Text for link to previous page.</param>
+ /// <param name="nextText">Text for link to next page.</param>
+ /// <param name="lastText">Text for link to last page.</param>
+ /// <param name="numericLinksCount">Number of numeric links that should display.</param>
+ /// <param name="htmlAttributes">An object that contains the HTML attributes to set for the element.</param>
+ public IHtmlString GetHtml(
+ string tableStyle = null,
+ string headerStyle = null,
+ string footerStyle = null,
+ string rowStyle = null,
+ string alternatingRowStyle = null,
+ string selectedRowStyle = null,
+ string caption = null,
+ bool displayHeader = true,
+ bool fillEmptyRows = false,
+ string emptyRowCellValue = null,
+ IEnumerable<WebGridColumn> columns = null,
+ IEnumerable<string> exclusions = null,
+ WebGridPagerModes mode = WebGridPagerModes.NextPrevious | WebGridPagerModes.Numeric,
+ string firstText = null,
+ string previousText = null,
+ string nextText = null,
+ string lastText = null,
+ int numericLinksCount = 5,
+ object htmlAttributes = null)
+ {
+ Func<dynamic, object> footer = null;
+ if (_canPage && (PageCount > 1))
+ {
+ footer = item => Pager(mode, firstText, previousText, nextText, lastText, numericLinksCount, explicitlyCalled: false);
+ }
+
+ return Table(tableStyle, headerStyle, footerStyle, rowStyle, alternatingRowStyle, selectedRowStyle, caption, displayHeader,
+ fillEmptyRows, emptyRowCellValue, columns, exclusions, footer: footer,
+ htmlAttributes: htmlAttributes);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings", Justification = "Strings are easier for Plan9 developer to work with")]
+ public string GetPageUrl(int pageIndex)
+ {
+ if (!_canPage)
+ {
+ throw new NotSupportedException(HelpersResources.WebGrid_NotSupportedIfPagingIsDisabled);
+ }
+ if ((pageIndex < 0) || (pageIndex >= PageCount))
+ {
+ throw new ArgumentOutOfRangeException("pageIndex", String.Format(CultureInfo.CurrentCulture,
+ CommonResources.Argument_Must_Be_Between, 0, (PageCount - 1)));
+ }
+
+ NameValueCollection queryString = new NameValueCollection(1);
+ queryString[PageFieldName] = (pageIndex + 1L).ToString(CultureInfo.CurrentCulture);
+ return GetPath(queryString, SelectionFieldName);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings", Justification = "Strings are easier for Plan9 developer to work with")]
+ public string GetSortUrl(string column)
+ {
+ if (!_canSort)
+ {
+ throw new NotSupportedException(HelpersResources.WebGrid_NotSupportedIfSortingIsDisabled);
+ }
+ if (String.IsNullOrEmpty(column))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "column");
+ }
+
+ var sort = SortColumn;
+ var sortDir = SortDirection.Ascending;
+ if (column.Equals(sort, StringComparison.OrdinalIgnoreCase))
+ {
+ if (SortDirection == SortDirection.Ascending)
+ {
+ sortDir = SortDirection.Descending;
+ }
+ }
+
+ NameValueCollection queryString = new NameValueCollection(2);
+ queryString[SortFieldName] = column;
+ queryString[SortDirectionFieldName] = GetSortDirectionString(sortDir);
+ return GetPath(queryString, PageFieldName, SelectionFieldName);
+ }
+
+ /// <summary>
+ /// Gets the HTML for a pager.
+ /// </summary>
+ /// <param name="mode">Modes for pager rendering.</param>
+ /// <param name="firstText">Text for link to first page.</param>
+ /// <param name="previousText">Text for link to previous page.</param>
+ /// <param name="nextText">Text for link to next page.</param>
+ /// <param name="lastText">Text for link to last page.</param>
+ /// <param name="numericLinksCount">Number of numeric links that should display.</param>
+ public HelperResult Pager(
+ WebGridPagerModes mode = WebGridPagerModes.NextPrevious | WebGridPagerModes.Numeric,
+ string firstText = null,
+ string previousText = null,
+ string nextText = null,
+ string lastText = null,
+ int numericLinksCount = 5)
+ {
+ return Pager(mode, firstText, previousText, nextText, lastText, numericLinksCount, explicitlyCalled: true);
+ }
+
+ /// <param name="mode">Modes for pager rendering.</param>
+ /// <param name="firstText">Text for link to first page.</param>
+ /// <param name="previousText">Text for link to previous page.</param>
+ /// <param name="nextText">Text for link to next page.</param>
+ /// <param name="lastText">Text for link to last page.</param>
+ /// <param name="numericLinksCount">Number of numeric links that should display.</param>
+ /// <param name="explicitlyCalled">The Pager can be explicitly called by the public API or is called by the WebGrid when no footer is provided.
+ /// In the explicit scenario, we would need to render a container for the pager to allow identifying the pager links.
+ /// In the implicit scenario, the grid table would be the container.
+ /// </param>
+ private HelperResult Pager(
+ WebGridPagerModes mode,
+ string firstText,
+ string previousText,
+ string nextText,
+ string lastText,
+ int numericLinksCount,
+ bool explicitlyCalled)
+ {
+ if (!_canPage)
+ {
+ throw new NotSupportedException(HelpersResources.WebGrid_NotSupportedIfPagingIsDisabled);
+ }
+ if (!ModeEnabled(mode, WebGridPagerModes.FirstLast) && (firstText != null))
+ {
+ throw new ArgumentException(String.Format(CultureInfo.CurrentCulture,
+ HelpersResources.WebGrid_PagerModeMustBeEnabled, "FirstLast"), "firstText");
+ }
+ if (!ModeEnabled(mode, WebGridPagerModes.NextPrevious) && (previousText != null))
+ {
+ throw new ArgumentException(String.Format(CultureInfo.CurrentCulture,
+ HelpersResources.WebGrid_PagerModeMustBeEnabled, "NextPrevious"), "previousText");
+ }
+ if (!ModeEnabled(mode, WebGridPagerModes.NextPrevious) && (nextText != null))
+ {
+ throw new ArgumentException(String.Format(CultureInfo.CurrentCulture,
+ HelpersResources.WebGrid_PagerModeMustBeEnabled, "NextPrevious"), "nextText");
+ }
+ if (!ModeEnabled(mode, WebGridPagerModes.FirstLast) && (lastText != null))
+ {
+ throw new ArgumentException(String.Format(CultureInfo.CurrentCulture,
+ HelpersResources.WebGrid_PagerModeMustBeEnabled, "FirstLast"), "lastText");
+ }
+ if (numericLinksCount < 0)
+ {
+ throw new ArgumentOutOfRangeException("numericLinksCount",
+ String.Format(CultureInfo.CurrentCulture, CommonResources.Argument_Must_Be_GreaterThanOrEqualTo, 0));
+ }
+
+ return WebGridRenderer.Pager(this, HttpContext, mode: mode, firstText: firstText, previousText: previousText, nextText: nextText, lastText: lastText,
+ numericLinksCount: numericLinksCount, renderAjaxContainer: explicitlyCalled);
+ }
+
+ /// <summary>
+ /// Gets the HTML for a table with a pager.
+ /// </summary>
+ /// <param name="tableStyle">Table class for styling.</param>
+ /// <param name="headerStyle">Header row class for styling.</param>
+ /// <param name="footerStyle">Footer row class for styling.</param>
+ /// <param name="rowStyle">Row class for styling (odd rows only).</param>
+ /// <param name="alternatingRowStyle">Row class for styling (even rows only).</param>
+ /// <param name="selectedRowStyle">Selected row class for styling.</param>
+ /// <param name="caption">The table caption</param>
+ /// <param name="displayHeader">Whether the header row should be displayed.</param>
+ /// <param name="fillEmptyRows">Whether the table can add empty rows to ensure the rowsPerPage row count.</param>
+ /// <param name="emptyRowCellValue">Value used to populate empty rows. This property is only used when <paramref name="fillEmptyRows"/> is set</param>
+ /// <param name="columns">Column model for customizing column rendering.</param>
+ /// <param name="exclusions">Columns to exclude when auto-populating columns.</param>
+ /// <param name="footer">Table footer template.</param>
+ /// <param name="htmlAttributes">An object that contains the HTML attributes to set for the element.</param>
+ public IHtmlString Table(
+ string tableStyle = null,
+ string headerStyle = null,
+ string footerStyle = null,
+ string rowStyle = null,
+ string alternatingRowStyle = null,
+ string selectedRowStyle = null,
+ string caption = null,
+ bool displayHeader = true,
+ bool fillEmptyRows = false,
+ string emptyRowCellValue = null,
+ IEnumerable<WebGridColumn> columns = null,
+ IEnumerable<string> exclusions = null,
+ Func<dynamic, object> footer = null,
+ object htmlAttributes = null)
+ {
+ if (columns == null)
+ {
+ columns = GetDefaultColumns(exclusions);
+ }
+ // In order of precedence, the parameters that affect the visibility of columns in WebGrid -
+ // (1) "columns" argument of this method
+ // (2) "exclusion" argument of this method
+ // (3) "columnNames" argument of the constructor.
+ // At the time of binding we can verify if a simple property specified in the query string is a column that would be visible to the user.
+ // However, for complex properties or if either of (1) or (2) arguments are specified, we can only verify at this point.
+ EnsureColumnIsSortable(columns);
+
+ if (emptyRowCellValue == null)
+ {
+ emptyRowCellValue = "&nbsp;";
+ }
+
+ return WebGridRenderer.Table(this, HttpContext, tableStyle: tableStyle, headerStyle: headerStyle, footerStyle: footerStyle, rowStyle: rowStyle,
+ alternatingRowStyle: alternatingRowStyle, selectedRowStyle: selectedRowStyle, caption: caption, displayHeader: displayHeader, fillEmptyRows: fillEmptyRows,
+ emptyRowCellValue: emptyRowCellValue, columns: columns, exclusions: exclusions, footer: footer, htmlAttributes: htmlAttributes);
+ }
+
+ /// <param name="columns">The set of columns that are rendered to the client.</param>
+ private void EnsureColumnIsSortable(IEnumerable<WebGridColumn> columns)
+ {
+ // Fix for bug 941102
+ // The ValidateSortColumn can validate a few regular cases for sorting and reset those values to default. However, for sort columns that are complex expressions,
+ // or if the user specifies a subset of columns in the GetHtml method (via columns / exclusions), the method is ineffective.
+ // Review: Should this method not throw if the data was not explicitly sorted and paged by the user
+ if (_canSort && !_sortColumnExplicitlySet && !String.IsNullOrEmpty(SortColumn) && !StringComparer.OrdinalIgnoreCase.Equals(_defaultSort, SortColumn)
+ && !columns.Select(c => c.ColumnName).Contains(SortColumn, StringComparer.OrdinalIgnoreCase))
+ {
+ throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, HelpersResources.WebGrid_ColumnNotFound, SortColumn));
+ }
+ }
+
+ internal static dynamic GetMember(WebGridRow row, string name)
+ {
+ object result;
+ if (row.TryGetMember(name, out result))
+ {
+ return result;
+ }
+ throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture,
+ HelpersResources.WebGrid_ColumnNotFound, name));
+ }
+
+ // review: make sure this is ordered
+ internal string GetPath(NameValueCollection queryString, params string[] exclusions)
+ {
+ NameValueCollection temp = new NameValueCollection(QueryString);
+ // update current query string in case values were set programmatically
+ if (temp.AllKeys.Contains(PageFieldName))
+ {
+ temp.Set(PageFieldName, (PageIndex + 1L).ToString(CultureInfo.CurrentCulture));
+ }
+ if (temp.AllKeys.Contains(SelectionFieldName))
+ {
+ if (SelectedIndex < 0)
+ {
+ temp.Remove(SelectionFieldName);
+ }
+ else
+ {
+ temp.Set(SelectionFieldName, (SelectedIndex + 1L).ToString(CultureInfo.CurrentCulture));
+ }
+ }
+ if (temp.AllKeys.Contains(SortFieldName))
+ {
+ if (String.IsNullOrEmpty(SortColumn))
+ {
+ temp.Remove(SortFieldName);
+ }
+ else
+ {
+ temp.Set(SortFieldName, SortColumn);
+ }
+ }
+ if (temp.AllKeys.Contains(SortDirectionFieldName))
+ {
+ temp.Set(SortDirectionFieldName, GetSortDirectionString(SortDirection));
+ }
+
+ // remove fields from exclusions list
+ foreach (var key in exclusions)
+ {
+ temp.Remove(key);
+ }
+ // replace with new field values
+ foreach (string key in queryString.Keys)
+ {
+ temp.Set(key, queryString[key]);
+ }
+ queryString = temp;
+
+ StringBuilder sb = new StringBuilder(HttpContext.Request.Path);
+
+ sb.Append("?");
+ for (int i = 0; i < queryString.Count; i++)
+ {
+ if (i > 0)
+ {
+ sb.Append("&");
+ }
+ sb.Append(HttpUtility.UrlEncode(queryString.Keys[i]));
+ sb.Append("=");
+ sb.Append(HttpUtility.UrlEncode(queryString[i]));
+ }
+ return sb.ToString();
+ }
+
+ internal static string GetSortDirectionString(SortDirection sortDir)
+ {
+ return (sortDir == SortDirection.Ascending) ? "ASC" : "DESC";
+ }
+
+ private void EnsureDataBound()
+ {
+ if (!_dataSourceBound)
+ {
+ throw new InvalidOperationException(HelpersResources.WebGrid_NoDataSourceBound);
+ }
+ }
+
+ private void EnsureDataSourceNotMaterialized()
+ {
+ if (_dataSourceMaterialized)
+ {
+ throw new InvalidOperationException(HelpersResources.WebGrid_PropertySetterNotSupportedAfterDataBound);
+ }
+ }
+
+ private void ValidatePreDataBoundValues()
+ {
+ if (_canPage && _pageIndexSet && PageIndex > PageCount)
+ {
+ PageIndex = PageCount;
+ }
+ else if (_canSort && _sortColumnSet && !ValidateSortColumn(SortColumn))
+ {
+ SortColumn = _defaultSort;
+ }
+ }
+
+ private bool ValidateSortColumn(string value)
+ {
+ Debug.Assert(ColumnNames != null);
+
+ // Navigation columns that contain '.' will be validated during the Sort operation
+ // Validate other properties up-front and ignore any bad columns passed via the query string
+ return _sortColumnExplicitlySet
+ || String.IsNullOrEmpty(value)
+ || StringComparer.OrdinalIgnoreCase.Equals(_defaultSort, value)
+ || ColumnNames.Contains(value, StringComparer.OrdinalIgnoreCase)
+ || value.Contains('.');
+ }
+
+ private static IEnumerable<string> GetDefaultColumnNames(IEnumerable<dynamic> source, Type elementType)
+ {
+ var dynObj = source.FirstOrDefault() as IDynamicMetaObjectProvider;
+ if (dynObj != null)
+ {
+ return DynamicHelper.GetMemberNames(dynObj);
+ }
+ else
+ {
+ return (from p in elementType.GetProperties()
+ where IsBindableType(p.PropertyType) && (p.GetIndexParameters().Length == 0)
+ select p.Name).OrderBy(n => n, StringComparer.OrdinalIgnoreCase).ToArray();
+ }
+ }
+
+ private IEnumerable<WebGridColumn> GetDefaultColumns(IEnumerable<string> exclusions)
+ {
+ IEnumerable<string> names = ColumnNames;
+ if (exclusions != null)
+ {
+ names = names.Except(exclusions);
+ }
+ return (from n in names
+ select new WebGridColumn { ColumnName = n, CanSort = true }).ToArray();
+ }
+
+ // see: DataBoundControlHelper.IsBindableType
+ private static bool IsBindableType(Type type)
+ {
+ Debug.Assert(type != null);
+
+ Type underlyingType = Nullable.GetUnderlyingType(type);
+ if (underlyingType != null)
+ {
+ type = underlyingType;
+ }
+ return (type.IsPrimitive ||
+ type.Equals(typeof(string)) ||
+ type.Equals(typeof(DateTime)) ||
+ type.Equals(typeof(Decimal)) ||
+ type.Equals(typeof(Guid)) ||
+ type.Equals(typeof(DateTimeOffset)) ||
+ type.Equals(typeof(TimeSpan)));
+ }
+
+ private static bool ModeEnabled(WebGridPagerModes mode, WebGridPagerModes modeCheck)
+ {
+ return (mode & modeCheck) == modeCheck;
+ }
+ }
+}
diff --git a/src/System.Web.Helpers/WebGrid/WebGridColumn.cs b/src/System.Web.Helpers/WebGrid/WebGridColumn.cs
new file mode 100644
index 00000000..51b84ae7
--- /dev/null
+++ b/src/System.Web.Helpers/WebGrid/WebGridColumn.cs
@@ -0,0 +1,15 @@
+namespace System.Web.Helpers
+{
+ public class WebGridColumn
+ {
+ public bool CanSort { get; set; }
+
+ public string ColumnName { get; set; }
+
+ public Func<dynamic, object> Format { get; set; }
+
+ public string Header { get; set; }
+
+ public string Style { get; set; }
+ }
+}
diff --git a/src/System.Web.Helpers/WebGrid/WebGridDataSource.cs b/src/System.Web.Helpers/WebGrid/WebGridDataSource.cs
new file mode 100644
index 00000000..682bb2e2
--- /dev/null
+++ b/src/System.Web.Helpers/WebGrid/WebGridDataSource.cs
@@ -0,0 +1,159 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Dynamic;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using Microsoft.CSharp.RuntimeBinder;
+using Binder = Microsoft.CSharp.RuntimeBinder.Binder;
+
+namespace System.Web.Helpers
+{
+ /// <summary>
+ /// Default data source that sorts results if a sort column is specified.
+ /// </summary>
+ internal sealed class WebGridDataSource : IWebGridDataSource
+ {
+ private readonly WebGrid _grid;
+ private readonly Type _elementType;
+ private readonly IEnumerable<dynamic> _values;
+ private readonly bool _canPage;
+ private readonly bool _canSort;
+
+ public WebGridDataSource(WebGrid grid, IEnumerable<dynamic> values, Type elementType, bool canPage, bool canSort)
+ {
+ Debug.Assert(grid != null);
+ Debug.Assert(values != null);
+
+ _grid = grid;
+ _values = values;
+ _elementType = elementType;
+ _canPage = canPage;
+ _canSort = canSort;
+ }
+
+ public SortInfo DefaultSort { get; set; }
+
+ public int RowsPerPage { get; set; }
+
+ public int TotalRowCount
+ {
+ get { return _values.Count(); }
+ }
+
+ public IList<WebGridRow> GetRows(SortInfo sortInfo, int pageIndex)
+ {
+ IEnumerable<dynamic> rowData = _values;
+
+ if (_canSort)
+ {
+ rowData = Sort(_values.AsQueryable(), sortInfo);
+ }
+
+ rowData = Page(rowData, pageIndex);
+
+ try
+ {
+ // Force compile the underlying IQueryable
+ rowData = rowData.ToList();
+ }
+ catch (ArgumentException)
+ {
+ // The OrderBy method uses a generic comparer which fails when the collection contains 2 or more
+ // items that cannot be compared (e.g. DBNulls, mixed types such as strings and ints et al) with the exception
+ // System.ArgumentException: At least one object must implement IComparable.
+ // Silently fail if this exception occurs and declare that the two items are equivalent
+ rowData = Page(_values.AsQueryable(), pageIndex);
+ }
+ return rowData.Select((value, index) => new WebGridRow(_grid, value: value, rowIndex: index)).ToList();
+ }
+
+ private IQueryable<dynamic> Sort(IQueryable<dynamic> data, SortInfo sortInfo)
+ {
+ if (!String.IsNullOrEmpty(sortInfo.SortColumn) || ((DefaultSort != null) && !String.IsNullOrEmpty(DefaultSort.SortColumn)))
+ {
+ return Sort(data, _elementType, sortInfo);
+ }
+ return data;
+ }
+
+ private IEnumerable<dynamic> Page(IEnumerable<dynamic> data, int pageIndex)
+ {
+ if (_canPage)
+ {
+ Debug.Assert(RowsPerPage > 0);
+ return data.Skip(pageIndex * RowsPerPage).Take(RowsPerPage);
+ }
+ return data;
+ }
+
+ private IQueryable<dynamic> Sort(IQueryable<dynamic> data, Type elementType, SortInfo sort)
+ {
+ Debug.Assert(data != null);
+
+ if (typeof(IDynamicMetaObjectProvider).IsAssignableFrom(elementType))
+ {
+ // IDynamicMetaObjectProvider properties are only available through a runtime binder, so we
+ // must build a custom LINQ expression for getting the dynamic property value.
+ // Lambda: o => o.Property (where Property is obtained by runtime binder)
+ // NOTE: lambda must not use internals otherwise this will fail in partial trust when Helpers assembly is in GAC
+ var binder = Binder.GetMember(CSharpBinderFlags.None, sort.SortColumn, typeof(WebGrid), new[]
+ {
+ CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null)
+ });
+ var param = Expression.Parameter(typeof(IDynamicMetaObjectProvider), "o");
+ var getter = Expression.Dynamic(binder, typeof(object), param);
+ return SortGenericExpression<IDynamicMetaObjectProvider, object>(data, getter, param, sort.SortDirection);
+ }
+ else
+ {
+ // The IQueryable<dynamic> data source is cast as IQueryable<object> at runtime. We must call
+ // SortGenericExpression using reflection so that the LINQ expressions use the actual element type.
+ // Lambda: o => o.Property[.NavigationProperty,etc]
+ var param = Expression.Parameter(elementType, "o");
+ Expression member = param;
+ var type = elementType;
+ var sorts = sort.SortColumn.Split('.');
+ foreach (var name in sorts)
+ {
+ PropertyInfo prop = type.GetProperty(name);
+ if (prop == null)
+ {
+ // no-op in case navigation property came from querystring (falls back to default sort)
+ if ((DefaultSort != null) && !sort.Equals(DefaultSort) && !String.IsNullOrEmpty(DefaultSort.SortColumn))
+ {
+ return Sort(data, elementType, DefaultSort);
+ }
+ return data;
+ }
+ member = Expression.Property(member, prop);
+ type = prop.PropertyType;
+ }
+ MethodInfo m = GetType().GetMethod("SortGenericExpression", BindingFlags.Static | BindingFlags.NonPublic);
+ m = m.MakeGenericMethod(elementType, member.Type);
+ return (IQueryable<dynamic>)m.Invoke(null, new object[] { data, member, param, sort.SortDirection });
+ }
+ }
+
+ private static IQueryable<TElement> SortGenericExpression<TElement, TProperty>(IQueryable<dynamic> data, Expression body,
+ ParameterExpression param, SortDirection sortDirection)
+ {
+ Debug.Assert(data != null);
+ Debug.Assert(body != null);
+ Debug.Assert(param != null);
+
+ // The IQueryable<dynamic> data source is cast as an IQueryable<object> at runtime. We must cast
+ // this to an IQueryable<TElement> so that the reflection done by the LINQ expressions will work.
+ IQueryable<TElement> data2 = data.Cast<TElement>();
+ Expression<Func<TElement, TProperty>> lambda = Expression.Lambda<Func<TElement, TProperty>>(body, param);
+ if (sortDirection == SortDirection.Descending)
+ {
+ return data2.OrderByDescending(lambda);
+ }
+ else
+ {
+ return data2.OrderBy(lambda);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Helpers/WebGrid/WebGridPagerModes.cs b/src/System.Web.Helpers/WebGrid/WebGridPagerModes.cs
new file mode 100644
index 00000000..6796b9c6
--- /dev/null
+++ b/src/System.Web.Helpers/WebGrid/WebGridPagerModes.cs
@@ -0,0 +1,11 @@
+namespace System.Web.Helpers
+{
+ [Flags]
+ public enum WebGridPagerModes
+ {
+ Numeric = 0x1,
+ NextPrevious = 0x2,
+ FirstLast = 0x4,
+ All = 0x7
+ }
+}
diff --git a/src/System.Web.Helpers/WebGrid/WebGridRow.cs b/src/System.Web.Helpers/WebGrid/WebGridRow.cs
new file mode 100644
index 00000000..a0d69885
--- /dev/null
+++ b/src/System.Web.Helpers/WebGrid/WebGridRow.cs
@@ -0,0 +1,195 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Diagnostics.CodeAnalysis;
+using System.Dynamic;
+using System.Globalization;
+using System.Linq;
+using System.Reflection;
+using System.Web.Helpers.Resources;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.Helpers
+{
+ [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix", Justification = "Collection is not an appropriate suffix for this class")]
+ public class WebGridRow : DynamicObject, IEnumerable<object>
+ {
+ private const string RowIndexMemberName = "ROW";
+ private const BindingFlags BindFlags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.IgnoreCase;
+
+ private WebGrid _grid;
+ private IDynamicMetaObjectProvider _dynamic;
+ private int _rowIndex;
+ private object _value;
+ private IEnumerable<dynamic> _values;
+
+ public WebGridRow(WebGrid webGrid, object value, int rowIndex)
+ {
+ _grid = webGrid;
+ _value = value;
+ _rowIndex = rowIndex;
+ _dynamic = value as IDynamicMetaObjectProvider;
+ }
+
+ public dynamic Value
+ {
+ get { return _value; }
+ }
+
+ public WebGrid WebGrid
+ {
+ get { return _grid; }
+ }
+
+ public object this[string name]
+ {
+ get
+ {
+ if (String.IsNullOrEmpty(name))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "name");
+ }
+ object value = null;
+ if (!TryGetMember(name, out value))
+ {
+ throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture,
+ HelpersResources.WebGrid_ColumnNotFound, name));
+ }
+ return value;
+ }
+ }
+
+ public object this[int index]
+ {
+ get
+ {
+ if ((index < 0) || (index >= _grid.ColumnNames.Count()))
+ {
+ throw new ArgumentOutOfRangeException("index");
+ }
+ return this.Skip(index).First();
+ }
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ public IEnumerator<object> GetEnumerator()
+ {
+ if (_values == null)
+ {
+ _values = _grid.ColumnNames.Select(c => WebGrid.GetMember(this, c));
+ }
+ return _values.GetEnumerator();
+ }
+
+ public IHtmlString GetSelectLink(string text = null)
+ {
+ if (String.IsNullOrEmpty(text))
+ {
+ text = HelpersResources.WebGrid_SelectLinkText;
+ }
+ return WebGridRenderer.GridLink(_grid, GetSelectUrl(), text);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "review: I think a method is more appropriate here")]
+ [SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings", Justification = "Strings are easier for Plan9 developer to work with")]
+ public string GetSelectUrl()
+ {
+ NameValueCollection queryString = new NameValueCollection(1);
+ queryString[WebGrid.SelectionFieldName] = (_rowIndex + 1L).ToString(CultureInfo.CurrentCulture);
+ return WebGrid.GetPath(queryString);
+ }
+
+ public override bool TryGetMember(GetMemberBinder binder, out object result)
+ {
+ result = null;
+ // Try to get the row index
+ if (TryGetRowIndex(binder.Name, out result))
+ {
+ return true;
+ }
+
+ // Try to evaluate the dynamic member based on the binder
+ if (_dynamic != null && DynamicHelper.TryGetMemberValue(_dynamic, binder, out result))
+ {
+ return true;
+ }
+
+ return TryGetComplexMember(_value, binder.Name, out result);
+ }
+
+ internal bool TryGetMember(string memberName, out object result)
+ {
+ result = null;
+
+ // Try to get the row index
+ if (TryGetRowIndex(memberName, out result))
+ {
+ return true;
+ }
+
+ // Try to evaluate the dynamic member based on the name
+ if (_dynamic != null && DynamicHelper.TryGetMemberValue(_dynamic, memberName, out result))
+ {
+ return true;
+ }
+
+ // Support '.' for navigation properties
+ return TryGetComplexMember(_value, memberName, out result);
+ }
+
+ public override string ToString()
+ {
+ return _value.ToString();
+ }
+
+ private bool TryGetRowIndex(string memberName, out object result)
+ {
+ result = null;
+ if (String.IsNullOrEmpty(memberName))
+ {
+ return false;
+ }
+
+ if (memberName == RowIndexMemberName)
+ {
+ result = _rowIndex;
+ return true;
+ }
+
+ return false;
+ }
+
+ private static bool TryGetComplexMember(object obj, string name, out object result)
+ {
+ result = null;
+
+ string[] names = name.Split('.');
+ for (int i = 0; i < names.Length; i++)
+ {
+ if ((obj == null) || !TryGetMember(obj, names[i], out result))
+ {
+ result = null;
+ return false;
+ }
+ obj = result;
+ }
+ return true;
+ }
+
+ private static bool TryGetMember(object obj, string name, out object result)
+ {
+ PropertyInfo property = obj.GetType().GetProperty(name, BindFlags);
+ if ((property != null) && (property.GetIndexParameters().Length == 0))
+ {
+ result = property.GetValue(obj, null);
+ return true;
+ }
+ result = null;
+ return false;
+ }
+ }
+}
diff --git a/src/System.Web.Helpers/WebGrid/_WebGridRenderer.cshtml b/src/System.Web.Helpers/WebGrid/_WebGridRenderer.cshtml
new file mode 100644
index 00000000..2c8bd838
--- /dev/null
+++ b/src/System.Web.Helpers/WebGrid/_WebGridRenderer.cshtml
@@ -0,0 +1,321 @@
+@using System.Globalization
+@using System.Text
+@using System.Web.Helpers.Resources
+@using System.Web.Mvc
+@using System.Web.WebPages.Scope
+@using Microsoft.Internal.Web.Utils
+
+@helper GridInitScript(WebGrid webGrid, HttpContextBase httpContext) {
+ if (!webGrid.IsAjaxEnabled) {
+ return;
+ }
+ if (!IsGridScriptRendered(httpContext)) {
+ SetGridScriptRendered(httpContext, true);
+ <script type="text/javascript">
+ (function($) {
+ $.fn.swhgLoad = function(url, containerId, callback) {
+ url = url + (url.indexOf('?') == -1 ? '?' : '&') + '__swhg=' + new Date().getTime();
+
+ $('<div/>').load(url + ' ' + containerId, function(data, status, xhr) {
+ $(containerId).replaceWith($(this).html());
+ if (typeof(callback) === 'function') {
+ callback.apply(this, arguments);
+ }
+ });
+ return this;
+ }
+
+ $(function() {
+ $('table[data-swhgajax="true"],span[data-swhgajax="true"]').each(function() {
+ var self = $(this);
+ var containerId = '#' + self.data('swhgcontainer');
+ var callback = getFunction(self.data('swhgcallback'));
+
+ $(containerId).parent().delegate(containerId + ' a[data-swhglnk="true"]', 'click', function() {
+ $(containerId).swhgLoad($(this).attr('href'), containerId, callback);
+ return false;
+ });
+ })
+ });
+
+ function getFunction(code, argNames) {
+ argNames = argNames || [];
+ var fn = window, parts = (code || "").split(".");
+ while (fn && parts.length) {
+ fn = fn[parts.shift()];
+ }
+ if (typeof (fn) === "function") {
+ return fn;
+ }
+ argNames.push(code);
+ return Function.constructor.apply(null, argNames);
+ }
+ })(jQuery);
+ </script>
+ }
+}
+
+@helper Table(WebGrid webGrid,
+ HttpContextBase httpContext,
+ string tableStyle,
+ string headerStyle,
+ string footerStyle,
+ string rowStyle,
+ string alternatingRowStyle,
+ string selectedRowStyle,
+ string caption,
+ bool displayHeader,
+ bool fillEmptyRows,
+ string emptyRowCellValue,
+ IEnumerable<WebGridColumn> columns,
+ IEnumerable<string> exclusions,
+ Func<dynamic, object> footer,
+ object htmlAttributes) {
+
+ if (emptyRowCellValue == null) {
+ emptyRowCellValue = "&nbsp;";
+ }
+
+ @GridInitScript(webGrid, httpContext)
+
+ var htmlAttributeDictionary = TypeHelper.ObjectToDictionary(htmlAttributes);
+ if (webGrid.IsAjaxEnabled) {
+ htmlAttributeDictionary["data-swhgajax"] = "true";
+ htmlAttributeDictionary["data-swhgcontainer"] = webGrid.AjaxUpdateContainerId;
+ htmlAttributeDictionary["data-swhgcallback"] = webGrid.AjaxUpdateCallback;
+ }
+
+ <table@(tableStyle.IsEmpty() ? null : Raw(" class=\"" + HttpUtility.HtmlAttributeEncode(tableStyle) + "\""))@PrintAttributes(htmlAttributeDictionary)>
+ @if (!caption.IsEmpty()) {
+ <caption>@caption</caption>
+ }
+ @if (displayHeader) {
+ <thead>
+ <tr@CssClass(headerStyle)>
+ @foreach (var column in columns) {
+ <th scope="col">
+ @if (ShowSortableColumnHeader(webGrid, column)) {
+ var text = column.Header.IsEmpty() ? column.ColumnName : column.Header;
+ @GridLink(webGrid, webGrid.GetSortUrl(column.ColumnName), text)
+ }
+ else {
+ @(column.Header ?? column.ColumnName)
+ }
+ </th>
+ }
+ </tr>
+ </thead>
+ }
+ @if (footer != null) {
+ <tfoot>
+ <tr @CssClass(footerStyle)>
+ <td colspan="@columns.Count()">@Format(footer, null)</td>
+ </tr>
+ </tfoot>
+ }
+ <tbody>
+ @{
+ int rowIndex = 0;
+ }
+ @foreach (var row in webGrid.Rows) {
+ string style = GetRowStyle(webGrid, rowIndex++, rowStyle, alternatingRowStyle, selectedRowStyle);
+ <tr@CssClass(style)>
+ @foreach (var column in columns) {
+ var value = (column.Format == null) ? HttpUtility.HtmlEncode(row[column.ColumnName]) : Format(column.Format, row).ToString();
+ <td@CssClass(column.Style)>@Raw(value)</td>
+ }
+ </tr>
+ }
+ @if (fillEmptyRows) {
+ rowIndex = webGrid.Rows.Count;
+ while (rowIndex < webGrid.RowsPerPage) {
+ string style = GetRowStyle(webGrid, rowIndex++, rowStyle, alternatingRowStyle, null);
+ <tr@CssClass(style)>
+ @foreach (var column in columns) {
+ <td@CssClass(column.Style)>@Raw(emptyRowCellValue)</td>
+ }
+ </tr>
+ }
+ }
+ </tbody>
+ </table>
+}
+
+
+@helper Pager(
+ WebGrid webGrid,
+ HttpContextBase httpContext,
+ WebGridPagerModes mode,
+ string firstText,
+ string previousText,
+ string nextText,
+ string lastText,
+ int numericLinksCount,
+ bool renderAjaxContainer) {
+
+ int currentPage = webGrid.PageIndex;
+ int totalPages = webGrid.PageCount;
+ int lastPage = totalPages - 1;
+
+ @GridInitScript(webGrid, httpContext)
+
+ if (renderAjaxContainer && webGrid.IsAjaxEnabled) {
+ @:<span data-swhgajax="true" data-swhgcontainer="@webGrid.AjaxUpdateContainerId" data-swhgcallback="@webGrid.AjaxUpdateCallback">
+ }
+
+ if (ModeEnabled(mode, WebGridPagerModes.FirstLast) && currentPage > 1) {
+ if (String.IsNullOrEmpty(firstText)) {
+ firstText = "<<";
+ }
+ @GridLink(webGrid, webGrid.GetPageUrl(0), firstText)
+ @Raw(" ")
+ }
+
+ if (ModeEnabled(mode, WebGridPagerModes.NextPrevious) && currentPage > 0) {
+ if (String.IsNullOrEmpty(previousText)) {
+ previousText = "<";
+ }
+ @GridLink(webGrid, webGrid.GetPageUrl(currentPage - 1), previousText)
+ @Raw(" ")
+ }
+
+ if (ModeEnabled(mode, WebGridPagerModes.Numeric) && (totalPages > 1)) {
+ int last = currentPage + (numericLinksCount / 2);
+ int first = last - numericLinksCount + 1;
+ if (last > lastPage) {
+ first -= last - lastPage;
+ last = lastPage;
+ }
+ if (first < 0) {
+ last = Math.Min(last + (0 - first), lastPage);
+ first = 0;
+ }
+ for (int i = first; i <= last; i++) {
+ var pageText = (i + 1).ToString(CultureInfo.InvariantCulture);
+ if (i == currentPage) {
+ <span>@pageText</span>
+ }
+ else {
+ @GridLink(webGrid, webGrid.GetPageUrl(i), pageText)
+ }
+ @Raw(" ")
+ }
+ }
+
+ if (ModeEnabled(mode, WebGridPagerModes.NextPrevious) && (currentPage < lastPage)) {
+ if (String.IsNullOrEmpty(nextText)) {
+ nextText = ">";
+ }
+ @GridLink(webGrid, webGrid.GetPageUrl(currentPage + 1), nextText)
+ @Raw(" ")
+ }
+
+ if (ModeEnabled(mode, WebGridPagerModes.FirstLast) && (currentPage < lastPage - 1)) {
+ if (String.IsNullOrEmpty(lastText)) {
+ lastText = ">>";
+ }
+ @GridLink(webGrid, webGrid.GetPageUrl(lastPage), lastText)
+ }
+
+ if (renderAjaxContainer && webGrid.IsAjaxEnabled) {
+ @:</span>
+ }
+}
+
+@functions{
+ private static readonly object _gridScriptRenderedKey = new object();
+
+ private static bool IsGridScriptRendered(HttpContextBase context) {
+ bool? value = (bool?)context.Items[_gridScriptRenderedKey];
+ return value.HasValue && value.Value;
+ }
+
+ private static void SetGridScriptRendered(HttpContextBase context, bool value) {
+ context.Items[_gridScriptRenderedKey] = value;
+ }
+
+ private static bool ShowSortableColumnHeader(WebGrid grid, WebGridColumn column) {
+ return grid.CanSort && column.CanSort && !column.ColumnName.IsEmpty();
+ }
+
+ public static IHtmlString GridLink(WebGrid webGrid, string url, string text) {
+ TagBuilder builder = new TagBuilder("a");
+ builder.SetInnerText(text);
+ builder.MergeAttribute("href", url);
+ if (webGrid.IsAjaxEnabled) {
+ builder.MergeAttribute("data-swhglnk", "true");
+ }
+ return builder.ToHtmlString(TagRenderMode.Normal);
+ }
+
+ private static IHtmlString Raw(string text) {
+ return new HtmlString(text);
+ }
+
+ private static IHtmlString RawJS(string text) {
+ return new HtmlString(HttpUtility.JavaScriptStringEncode(text));
+ }
+
+ private static IHtmlString CssClass(string className) {
+ return new HtmlString((!className.IsEmpty()) ? " class=\"" + HttpUtility.HtmlAttributeEncode(className) + "\"" : String.Empty);
+ }
+
+ private static string GetRowStyle(WebGrid webGrid, int rowIndex, string rowStyle, string alternatingRowStyle, string selectedRowStyle) {
+ StringBuilder style = new StringBuilder();
+
+ if (rowIndex % 2 == 0) {
+ if (!String.IsNullOrEmpty(rowStyle)) {
+ style.Append(rowStyle);
+ }
+ }
+ else {
+ if (!String.IsNullOrEmpty(alternatingRowStyle)) {
+ style.Append(alternatingRowStyle);
+ }
+ }
+
+ if (!String.IsNullOrEmpty(selectedRowStyle) && (rowIndex == webGrid.SelectedIndex)) {
+ if (style.Length > 0) {
+ style.Append(" ");
+ }
+ style.Append(selectedRowStyle);
+ }
+ return style.ToString();
+ }
+
+ private static HelperResult Format(Func<dynamic, object> format, dynamic arg) {
+ var result = format(arg);
+ return new HelperResult(tw => {
+ var helper = result as HelperResult;
+ if (helper != null) {
+ helper.WriteTo(tw);
+ return;
+ }
+ IHtmlString htmlString = result as IHtmlString;
+ if (htmlString != null) {
+ tw.Write(htmlString);
+ return;
+ }
+ if (result != null) {
+ tw.Write(HttpUtility.HtmlEncode(result));
+ }
+ });
+ }
+
+ private static IHtmlString PrintAttributes(IDictionary<string, object> attributes) {
+ var builder = new StringBuilder();
+ foreach (var item in attributes) {
+ var value = Convert.ToString(item.Value, CultureInfo.InvariantCulture);
+ builder.Append(' ')
+ .Append(HttpUtility.HtmlEncode(item.Key))
+ .Append("=\"")
+ .Append(HttpUtility.HtmlAttributeEncode(value))
+ .Append('"');
+ }
+ return new HtmlString(builder.ToString());
+ }
+
+ private static bool ModeEnabled(WebGridPagerModes mode, WebGridPagerModes modeCheck) {
+ return (mode & modeCheck) == modeCheck;
+ }
+} \ No newline at end of file
diff --git a/src/System.Web.Helpers/WebGrid/_WebGridRenderer.generated.cs b/src/System.Web.Helpers/WebGrid/_WebGridRenderer.generated.cs
new file mode 100644
index 00000000..54566b0c
--- /dev/null
+++ b/src/System.Web.Helpers/WebGrid/_WebGridRenderer.generated.cs
Binary files differ
diff --git a/src/System.Web.Helpers/WebImage.cs b/src/System.Web.Helpers/WebImage.cs
new file mode 100644
index 00000000..020b6c84
--- /dev/null
+++ b/src/System.Web.Helpers/WebImage.cs
@@ -0,0 +1,1172 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Drawing;
+using System.Drawing.Drawing2D;
+using System.Drawing.Imaging;
+using System.Drawing.Text;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Web.Helpers.Resources;
+using System.Web.UI.WebControls;
+using Microsoft.Internal.Web.Utils;
+using Image = System.Drawing.Image;
+
+namespace System.Web.Helpers
+{
+ public class WebImage
+ {
+ // Default resolution to use when getting bitmap from image
+ private const float FixedResolution = 96f;
+
+ private static readonly IDictionary<Guid, ImageFormat> _imageFormatLookup = new[]
+ {
+ Drawing.Imaging.ImageFormat.Bmp, Drawing.Imaging.ImageFormat.Emf, Drawing.Imaging.ImageFormat.Exif,
+ Drawing.Imaging.ImageFormat.Gif, Drawing.Imaging.ImageFormat.Icon, Drawing.Imaging.ImageFormat.Jpeg,
+ Drawing.Imaging.ImageFormat.MemoryBmp, Drawing.Imaging.ImageFormat.Png, Drawing.Imaging.ImageFormat.Tiff,
+ Drawing.Imaging.ImageFormat.Wmf
+ }.ToDictionary(format => format.Guid, format => format);
+
+ private static readonly Func<string, byte[]> _defaultReadAction = File.ReadAllBytes;
+
+ // Initial format is the format of the image when it was constructed.
+ // Current format is the format currently stored in the content buffer. This can
+ // be different than initial format since image transformations can change format.
+ private readonly ImageFormat _initialFormat;
+ private readonly List<ImageTransformation> _transformations = new List<ImageTransformation>();
+ private ImageFormat _currentFormat;
+ private byte[] _content;
+ private string _fileName;
+ private int _height = -1;
+ private int _width = -1;
+
+ private PropertyItem[] _properties; // image metadata
+
+ public WebImage(byte[] content)
+ {
+ _initialFormat = ValidateImageContent(content, "content");
+ _currentFormat = _initialFormat;
+ _content = (byte[])content.Clone();
+ }
+
+ public WebImage(string filePath)
+ : this(new HttpContextWrapper(HttpContext.Current), _defaultReadAction, filePath)
+ {
+ }
+
+ public WebImage(Stream imageStream)
+ {
+ if (imageStream.CanSeek)
+ {
+ imageStream.Seek(0, SeekOrigin.Begin);
+
+ _content = new byte[imageStream.Length];
+ using (BinaryReader reader = new BinaryReader(imageStream))
+ {
+ reader.Read(_content, 0, (int)imageStream.Length);
+ }
+ }
+ else
+ {
+ List<byte[]> chunks = new List<byte[]>();
+ int totalSize = 0;
+ using (BinaryReader reader = new BinaryReader(imageStream))
+ {
+ // Pick some size for chunks that is still under limit
+ // that causes them to be placed on the large object heap.
+ int chunkSizeInBytes = 1024 * 50;
+ byte[] nextChunk = null;
+ do
+ {
+ nextChunk = reader.ReadBytes(chunkSizeInBytes);
+ totalSize += nextChunk.Length;
+ chunks.Add(nextChunk);
+ }
+ while (nextChunk.Length == chunkSizeInBytes);
+ }
+
+ _content = new byte[totalSize];
+ int startIndex = 0;
+ foreach (var chunk in chunks)
+ {
+ chunk.CopyTo(_content, startIndex);
+ startIndex += chunk.Length;
+ }
+ }
+ _initialFormat = ValidateImageContent(_content, "imageStream");
+ _currentFormat = _initialFormat;
+ }
+
+ internal WebImage(HttpContextBase httpContext, Func<string, byte[]> readAction, string filePath)
+ {
+ if (String.IsNullOrEmpty(filePath))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "filePath");
+ }
+
+ _fileName = filePath;
+ _content = readAction(VirtualPathUtil.MapPath(httpContext, filePath));
+ _initialFormat = ValidateImageContent(_content, "filePath");
+ _currentFormat = _initialFormat;
+ }
+
+ private WebImage(WebImage other)
+ {
+ Debug.Assert(other != null);
+ Debug.Assert(other._content != null, "Incorrectly constructed instance.");
+
+ // We are not validating the contents from this constructor since its a copy constructor.
+ _content = (byte[])other._content.Clone();
+ _initialFormat = other._initialFormat;
+ _currentFormat = other._currentFormat;
+ _fileName = other._fileName;
+
+ _height = other._height;
+ _width = other._width;
+
+ _properties = (other._properties != null) ? (PropertyItem[])other._properties.Clone() : null;
+
+ _transformations = new List<ImageTransformation>(other._transformations);
+ }
+
+ public int Height
+ {
+ get
+ {
+ if ((_transformations.Count > 0) || (_height < 0))
+ {
+ ApplyTransformationsAndSetProperties();
+ }
+ return _height;
+ }
+ }
+
+ public int Width
+ {
+ get
+ {
+ if ((_transformations.Count > 0) || (_width < 0))
+ {
+ ApplyTransformationsAndSetProperties();
+ }
+ return _width;
+ }
+ }
+
+ public string FileName
+ {
+ get { return _fileName; }
+ set { _fileName = value; }
+ }
+
+ [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase",
+ Justification = "No security decision is made based on this string. It's customary to display MIME format in lowercase.")]
+ public string ImageFormat
+ {
+ get
+ {
+ if (_transformations.Any())
+ {
+ ApplyTransformationsAndSetProperties();
+ }
+
+ Debug.Assert(_currentFormat != null);
+ return ConversionUtil.ToString(_currentFormat).ToLowerInvariant();
+ }
+ }
+
+ public static WebImage GetImageFromRequest(string postedFileName = null)
+ {
+ var request = new HttpRequestWrapper(HttpContext.Current.Request);
+ return GetImageFromRequest(request, postedFileName);
+ }
+
+ internal static WebImage GetImageFromRequest(HttpRequestBase request, string postedFileName = null)
+ {
+ Debug.Assert(request != null);
+ if ((request.Files == null) || (request.Files.Count == 0))
+ {
+ return null;
+ }
+ HttpPostedFileBase file = String.IsNullOrEmpty(postedFileName) ? request.Files[0] : request.Files[postedFileName];
+ if (file == null || file.ContentLength < 1)
+ {
+ return null;
+ }
+
+ // The content type is specified by the browser and is unreliable.
+ // Disregard content type, acquire mime type.
+ ImageFormat format;
+ string mimeType = MimeMapping.GetMimeMapping(file.FileName);
+ if (!ConversionUtil.TryFromStringToImageFormat(mimeType, out format))
+ {
+ // Unsupported image format.
+ return null;
+ }
+
+ WebImage webImage = new WebImage(file.InputStream);
+ webImage.FileName = file.FileName;
+ return webImage;
+ }
+
+ public WebImage Clone()
+ {
+ return new WebImage(this);
+ }
+
+ public byte[] GetBytes(string requestedFormat = null)
+ {
+ if (_transformations.Count > 0)
+ {
+ ApplyTransformationsAndSetProperties();
+ }
+
+ ImageFormat requestedImageFormat = null;
+ if (!String.IsNullOrEmpty(requestedFormat))
+ {
+ // This will throw if image format is incorrect.
+ requestedImageFormat = GetImageFormat(requestedFormat);
+ }
+
+ requestedImageFormat = requestedImageFormat ?? _initialFormat;
+ Debug.Assert(requestedImageFormat != null, "Initial format can never be null");
+ if (requestedImageFormat.Equals(_currentFormat))
+ {
+ return (byte[])_content.Clone();
+ }
+
+ // Conversion from one format to another
+ using (MemoryStream sourceBuffer = new MemoryStream(_content))
+ {
+ using (Image image = Image.FromStream(sourceBuffer))
+ {
+ // if _properties are not initialized that means image did not go through any
+ // transformations yet and original byte array contains all metadata available
+ if (_properties != null)
+ {
+ CopyMetadata(_properties, image);
+ }
+
+ using (MemoryStream destinationBuffer = new MemoryStream())
+ {
+ image.Save(destinationBuffer, requestedImageFormat);
+ return destinationBuffer.ToArray();
+ }
+ }
+ }
+ }
+
+ public WebImage Resize(int width, int height, bool preserveAspectRatio = true, bool preventEnlarge = false)
+ {
+ if (width <= 0)
+ {
+ throw new ArgumentOutOfRangeException(
+ "width",
+ String.Format(CultureInfo.InvariantCulture, CommonResources.Argument_Must_Be_GreaterThan, 0));
+ }
+ if (height <= 0)
+ {
+ throw new ArgumentOutOfRangeException(
+ "height",
+ String.Format(CultureInfo.InvariantCulture, CommonResources.Argument_Must_Be_GreaterThan, 0));
+ }
+
+ ResizeTransformation trans = new ResizeTransformation(height, width, preserveAspectRatio, preventEnlarge);
+ _transformations.Add(trans);
+ return this;
+ }
+
+ public WebImage Crop(int top = 0, int left = 0, int bottom = 0, int right = 0)
+ {
+ if (top < 0)
+ {
+ throw new ArgumentOutOfRangeException(
+ "top",
+ String.Format(CultureInfo.InvariantCulture, CommonResources.Argument_Must_Be_GreaterThanOrEqualTo, 0));
+ }
+ if (left < 0)
+ {
+ throw new ArgumentOutOfRangeException(
+ "left",
+ String.Format(CultureInfo.InvariantCulture, CommonResources.Argument_Must_Be_GreaterThanOrEqualTo, 0));
+ }
+ if (bottom < 0)
+ {
+ throw new ArgumentOutOfRangeException(
+ "bottom",
+ String.Format(CultureInfo.InvariantCulture, CommonResources.Argument_Must_Be_GreaterThanOrEqualTo, 0));
+ }
+ if (right < 0)
+ {
+ throw new ArgumentOutOfRangeException(
+ "right",
+ String.Format(CultureInfo.InvariantCulture, CommonResources.Argument_Must_Be_GreaterThanOrEqualTo, 0));
+ }
+
+ CropTransformation crop = new CropTransformation(top, right, bottom, left);
+ _transformations.Add(crop);
+ return this;
+ }
+
+ public WebImage RotateLeft()
+ {
+ ImageTransformation transform = new RotateTransformation(RotateFlipType.Rotate270FlipNone);
+ _transformations.Add(transform);
+ return this;
+ }
+
+ public WebImage RotateRight()
+ {
+ ImageTransformation transform = new RotateTransformation(RotateFlipType.Rotate90FlipNone);
+ _transformations.Add(transform);
+ return this;
+ }
+
+ public WebImage FlipVertical()
+ {
+ ImageTransformation transform = new RotateTransformation(RotateFlipType.RotateNoneFlipY);
+ _transformations.Add(transform);
+ return this;
+ }
+
+ public WebImage FlipHorizontal()
+ {
+ ImageTransformation transform = new RotateTransformation(RotateFlipType.RotateNoneFlipX);
+ _transformations.Add(transform);
+ return this;
+ }
+
+ /// <summary>
+ /// Adds text watermark to a WebImage.
+ /// </summary>
+ /// <param name="text">Text to use as a watermark.</param>
+ /// <param name="fontColor">Watermark color. Can be specified as a string (e.g. "White") or as a hex value (e.g. "#00FF00").</param>
+ /// <param name="fontSize">Font size in points.</param>
+ /// <param name="fontStyle">Font style: bold, italics, etc.</param>
+ /// <param name="fontFamily">Font family name: e.g. Microsoft Sans Serif</param>
+ /// <param name="horizontalAlign">Horizontal alignment for watermark text. Can be "right", "left", or "center".</param>
+ /// <param name="verticalAlign">Vertical alignment for watermark text. Can be "top", "bottom", or "middle".</param>
+ /// <param name="opacity">Watermark text opacity. Should be between 0 and 100.</param>
+ /// <param name="padding">Size of padding around watermark text in pixels.</param>
+ /// <returns>Modified WebImage instance with added watermark.</returns>
+ public WebImage AddTextWatermark(
+ string text,
+ string fontColor = "Black",
+ int fontSize = 12,
+ string fontStyle = "Regular",
+ string fontFamily = "Microsoft Sans Serif",
+ string horizontalAlign = "Right",
+ string verticalAlign = "Bottom",
+ int opacity = 100,
+ int padding = 5)
+ {
+ if (String.IsNullOrEmpty(text))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "text");
+ }
+
+ Color color;
+ if (!ConversionUtil.TryFromStringToColor(fontColor, out color))
+ {
+ throw new ArgumentException(HelpersResources.WebImage_IncorrectColorName);
+ }
+
+ if ((opacity < 0) || (opacity > 100))
+ {
+ throw new ArgumentOutOfRangeException("opacity", String.Format(CultureInfo.InvariantCulture, CommonResources.Argument_Must_Be_Between, 0, 100));
+ }
+
+ int alpha = 255 * opacity / 100;
+ color = Color.FromArgb(alpha, color);
+
+ if (fontSize <= 0)
+ {
+ throw new ArgumentOutOfRangeException(
+ "fontSize",
+ String.Format(CultureInfo.InvariantCulture, CommonResources.Argument_Must_Be_GreaterThan, 0));
+ }
+
+ FontStyle fontStyleEnum;
+ if (!ConversionUtil.TryFromStringToEnum(fontStyle, out fontStyleEnum))
+ {
+ throw new ArgumentException(HelpersResources.WebImage_IncorrectFontStyle);
+ }
+
+ FontFamily fontFamilyClass;
+ if (!ConversionUtil.TryFromStringToFontFamily(fontFamily, out fontFamilyClass))
+ {
+ throw new ArgumentException(HelpersResources.WebImage_IncorrectFontFamily);
+ }
+
+ HorizontalAlign horizontalAlignEnum = ParseHorizontalAlign(horizontalAlign);
+ VerticalAlign verticalAlignEnum = ParseVerticalAlign(verticalAlign);
+
+ if (padding < 0)
+ {
+ throw new ArgumentOutOfRangeException(
+ "padding",
+ String.Format(CultureInfo.InvariantCulture, CommonResources.Argument_Must_Be_GreaterThanOrEqualTo, 0));
+ }
+
+ WatermarkTextTransformation transformation =
+ new WatermarkTextTransformation(text, color, fontSize, fontStyleEnum, fontFamilyClass, horizontalAlignEnum, verticalAlignEnum, padding);
+ _transformations.Add(transformation);
+ return this;
+ }
+
+ /// <summary>
+ /// Adds image watermark to an image.
+ /// </summary>
+ /// <param name="watermarkImage">Image to use as a watermark.</param>
+ /// <param name="width">Width of watermark.</param>
+ /// <param name="height">Height of watermark.</param>
+ /// <param name="horizontalAlign">Horizontal alignment for watermark image. Can be "right", "left", or "center".</param>
+ /// <param name="verticalAlign">Vertical alignment for watermark image. Can be "top", "bottom", or "middle".</param>
+ /// <param name="opacity">Watermark text opacity. Should be between 0 and 100.</param>
+ /// <param name="padding">Size of padding around watermark image in pixels.</param>
+ /// <returns>Modified WebImage instance with added watermark.</returns>
+ public WebImage AddImageWatermark(
+ WebImage watermarkImage,
+ int width = 0,
+ int height = 0,
+ string horizontalAlign = "Right",
+ string verticalAlign = "Bottom",
+ int opacity = 100,
+ int padding = 5)
+ {
+ if (watermarkImage == null)
+ {
+ throw new ArgumentNullException("watermarkImage");
+ }
+
+ if (width < 0)
+ {
+ throw new ArgumentOutOfRangeException(
+ "width",
+ String.Format(CultureInfo.InvariantCulture, CommonResources.Argument_Must_Be_GreaterThanOrEqualTo, 0));
+ }
+ if (height < 0)
+ {
+ throw new ArgumentOutOfRangeException(
+ "height",
+ String.Format(CultureInfo.InvariantCulture, CommonResources.Argument_Must_Be_GreaterThanOrEqualTo, 0));
+ }
+ if (((width == 0) && (height > 0)) || ((width > 0) && (height == 0)))
+ {
+ throw new ArgumentException(HelpersResources.WebImage_IncorrectWidthAndHeight);
+ }
+ if ((opacity < 0) || (opacity > 100))
+ {
+ throw new ArgumentOutOfRangeException("opacity", String.Format(CultureInfo.InvariantCulture, CommonResources.Argument_Must_Be_Between, 0, 100));
+ }
+
+ HorizontalAlign horizontalAlignEnum = ParseHorizontalAlign(horizontalAlign);
+ VerticalAlign verticalAlignEnum = ParseVerticalAlign(verticalAlign);
+
+ if (padding < 0)
+ {
+ throw new ArgumentOutOfRangeException(
+ "padding",
+ String.Format(CultureInfo.InvariantCulture, CommonResources.Argument_Must_Be_GreaterThanOrEqualTo, 0));
+ }
+
+ WatermarkImageTransformation transformation =
+ new WatermarkImageTransformation(watermarkImage.Clone(), width, height, horizontalAlignEnum, verticalAlignEnum, opacity, padding);
+ _transformations.Add(transformation);
+ return this;
+ }
+
+ /// <summary>
+ /// Adds image watermark to an image.
+ /// </summary>
+ /// <param name="watermarkImageFilePath">File to read watermark image from.</param>
+ /// <param name="width">Width of watermark.</param>
+ /// <param name="height">Height of watermark.</param>
+ /// <param name="horizontalAlign">Horizontal alignment for watermark image. Can be "right", "left", or "center".</param>
+ /// <param name="verticalAlign">Vertical alignment for watermark image. Can be "top", "bottom", or "middle".</param>
+ /// <param name="opacity">Watermark text opacity. Should be between 0 and 100.</param>
+ /// <param name="padding">Size of padding around watermark image in pixels.</param>
+ /// <returns>WebImage instance with added watermark.</returns>
+ public WebImage AddImageWatermark(
+ string watermarkImageFilePath,
+ int width = 0,
+ int height = 0,
+ string horizontalAlign = "Right",
+ string verticalAlign = "Bottom",
+ int opacity = 100,
+ int padding = 5)
+ {
+ return AddImageWatermark(new HttpContextWrapper(HttpContext.Current), _defaultReadAction, watermarkImageFilePath, width, height,
+ horizontalAlign, verticalAlign, opacity, padding);
+ }
+
+ internal WebImage AddImageWatermark(
+ HttpContextBase httpContext,
+ Func<string, byte[]> readAction,
+ string watermarkImageFilePath,
+ int width,
+ int height,
+ string horizontalAlign,
+ string verticalAlign,
+ int opacity,
+ int padding)
+ {
+ return AddImageWatermark(new WebImage(httpContext, readAction, watermarkImageFilePath), width, height, horizontalAlign, verticalAlign, opacity, padding);
+ }
+
+ [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase",
+ Justification = "No security decision is made based on this string. It's customary to display MIME format in lowercase.")]
+ public WebImage Write(string requestedFormat = null)
+ {
+ // GetBytes takes care of executing pending transformations and
+ // determining current image format if we didn't have it set before.
+ // todo: this could be made more efficient by avoiding cloning array
+ // when format is same
+ requestedFormat = requestedFormat ?? _initialFormat.ToString();
+ Debug.Assert(requestedFormat != null);
+ byte[] content = GetBytes(requestedFormat);
+
+ string requestedFormatWithPrefix;
+ if (requestedFormat.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
+ {
+ requestedFormatWithPrefix = requestedFormat;
+ }
+ else
+ {
+ requestedFormatWithPrefix = "image/" + requestedFormat;
+ }
+
+ HttpResponse response = HttpContext.Current.Response;
+ response.ContentType = requestedFormatWithPrefix;
+ response.BinaryWrite(content);
+
+ return this;
+ }
+
+ /// <param name="filePath">If no filePath is specified, the method falls back to the file name if the image was constructed from a file or
+ /// the file name on the client (the browser machine) if the image was built off GetImageFromRequest
+ /// </param>
+ /// <param name="imageFormat">The format the image is saved in</param>
+ /// <param name="forceCorrectExtension">Appends a well known extension to the filePath based on the imageFormat specified.
+ /// If the filePath uses a valid extension, no change is made.
+ /// e.g. format: "jpg", filePath: "foo.txt". Image saved at = "foo.txt.jpeg"
+ /// format: "png", filePath: "foo.png". Image saved at = "foo.txt.png"
+ /// </param>
+ [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase",
+ Justification = "File extensions are typically specified in lower case.")]
+ public WebImage Save(string filePath = null, string imageFormat = null, bool forceCorrectExtension = true)
+ {
+ return Save(new HttpContextWrapper(HttpContext.Current), File.WriteAllBytes, filePath, imageFormat, forceCorrectExtension);
+ }
+
+ [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "The string is a file extension which is typically lower case")]
+ internal WebImage Save(HttpContextBase context, Action<string, byte[]> saveAction, string filePath, string imageFormat, bool forceWellKnownExtension)
+ {
+ filePath = filePath ?? FileName;
+ if (String.IsNullOrEmpty(filePath))
+ {
+ throw new ArgumentNullException("filePath", CommonResources.Argument_Cannot_Be_Null_Or_Empty);
+ }
+
+ // GetBytes takes care of executing pending transformations.
+ // todo: this could be made more efficient by avoiding cloning array
+ // when format is same
+ byte[] content = GetBytes(imageFormat);
+ if (forceWellKnownExtension)
+ {
+ ImageFormat saveImageFormat;
+ ImageFormat requestedImageFormat = String.IsNullOrEmpty(imageFormat) ? _initialFormat : GetImageFormat(imageFormat);
+ var extension = Path.GetExtension(filePath).TrimStart('.');
+ // TryFromStringToImageFormat accepts mime types and image names. For images supported by System.Drawing.Imaging, the image name maps to the extension.
+ // Replace the extension with the current format in the following two events:
+ // * The extension format cannot be converted to a known format
+ // * The format does not match.
+ if (!ConversionUtil.TryFromStringToImageFormat(extension, out saveImageFormat) || !saveImageFormat.Equals(requestedImageFormat))
+ {
+ extension = requestedImageFormat.ToString().ToLowerInvariant();
+ filePath = filePath + "." + extension;
+ }
+ }
+ saveAction(VirtualPathUtil.MapPath(context, filePath), content);
+ // Update the FileName since it may have changed whilst saving.
+ FileName = filePath;
+ return this;
+ }
+
+ /// <summary>
+ /// Constructs a System.Drawing.Image instance from the content which validates the contents of the image.
+ /// </summary>
+ /// <exception cref="System.ArgumentException">When an Image construction fails.</exception>
+ private static ImageFormat ValidateImageContent(byte[] content, string paramName)
+ {
+ try
+ {
+ using (MemoryStream stream = new MemoryStream(content))
+ {
+ using (Image image = Image.FromStream(stream, useEmbeddedColorManagement: false))
+ {
+ var rawFormat = image.RawFormat;
+ ImageFormat actualFormat;
+ // RawFormat returns a ImageFormat instance with the same Guid as the predefined types
+ // This instance is not very useful when it comes to printing human readable strings and file extensions.
+ // Therefore, lookup the predefined instance
+ if (!_imageFormatLookup.TryGetValue(rawFormat.Guid, out actualFormat))
+ {
+ actualFormat = rawFormat;
+ }
+ return actualFormat;
+ }
+ }
+ }
+ catch (ArgumentException exception)
+ {
+ throw new ArgumentException(HelpersResources.WebImage_InvalidImageContents, paramName, exception);
+ }
+ }
+
+ private static ImageFormat GetImageFormat(string format)
+ {
+ Debug.Assert(!String.IsNullOrEmpty(format), "format cannot be null");
+
+ ImageFormat result;
+ if (!ConversionUtil.TryFromStringToImageFormat(format, out result))
+ {
+ throw new ArgumentException(String.Format(CultureInfo.InvariantCulture, HelpersResources.Image_IncorrectImageFormat, format), "format");
+ }
+
+ return result;
+ }
+
+ private static HorizontalAlign ParseHorizontalAlign(string alignment)
+ {
+ bool conversionOk;
+ HorizontalAlign horizontalAlign;
+ conversionOk = ConversionUtil.TryFromStringToEnum(alignment, out horizontalAlign);
+ if (!conversionOk || (horizontalAlign == HorizontalAlign.Justify) || (horizontalAlign == HorizontalAlign.NotSet))
+ {
+ throw new ArgumentException(HelpersResources.WebImage_IncorrectHorizontalAlignment);
+ }
+ return horizontalAlign;
+ }
+
+ private static VerticalAlign ParseVerticalAlign(string alignment)
+ {
+ bool conversionOk;
+ VerticalAlign verticalAlign;
+ conversionOk = ConversionUtil.TryFromStringToEnum(alignment, out verticalAlign);
+ if (!conversionOk || (verticalAlign == VerticalAlign.NotSet))
+ {
+ throw new ArgumentException(HelpersResources.WebImage_IncorrectVerticalAlignment);
+ }
+ return verticalAlign;
+ }
+
+ private void GetContentFromImageAndUpdateFormat(Image image)
+ {
+ using (MemoryStream buffer = new MemoryStream())
+ {
+ if (image.RawFormat.Equals(Drawing.Imaging.ImageFormat.MemoryBmp))
+ {
+ // Memory Bmps are an in-memory format and do not have encoders to save to disk / stream.
+ // Save it in the current format whenever we encounter which ensures we preserve image information such as transparency.
+ image.Save(buffer, _currentFormat);
+ }
+ else
+ {
+ // If the RawFormat has an encoder, save it as-is to prevent the cost of encoding it to another format such as the initial or current format.
+ image.Save(buffer, image.RawFormat);
+ _currentFormat = image.RawFormat;
+ }
+
+ _content = buffer.ToArray();
+ }
+ }
+
+ private void ApplyTransformationsAndSetProperties()
+ {
+ Debug.Assert(_content != null, "Incorrectly constructed instance.");
+
+ MemoryStream stream = null;
+ Image image = null;
+ try
+ {
+ stream = new MemoryStream(_content);
+ image = Image.FromStream(stream);
+
+ if (_properties == null)
+ {
+ // makes sure properties is never null after initialization
+ _properties = image.PropertyItems ?? new PropertyItem[0];
+ }
+
+ foreach (ImageTransformation trans in _transformations)
+ {
+ Image tempImage = trans.ApplyTransformation(image);
+
+ // ApplyTransformation could return the same image if no transformations are made or if
+ // transformations are made on the image itself.
+ if (tempImage != image)
+ {
+ if (stream != null)
+ {
+ stream.Dispose();
+ stream = null;
+ }
+
+ Debug.Assert((image != null) && (tempImage != null), "Image instances should not be null.");
+ image.Dispose();
+ image = tempImage;
+ }
+
+ // This is just to keep FxCop happy. Otherwise it thinks that tempImage could be diposed twice.
+ tempImage = null;
+ }
+
+ // If there were any transformations we need to get new content. This will also update the current format to the RawFormat.
+ if (_transformations.Any())
+ {
+ GetContentFromImageAndUpdateFormat(image);
+ _transformations.Clear();
+ }
+
+ _height = image.Size.Height;
+ _width = image.Size.Width;
+ }
+ finally
+ {
+ if (image != null)
+ {
+ image.Dispose();
+ }
+ if (stream != null)
+ {
+ stream.Dispose();
+ }
+ }
+ }
+
+ /// <remarks>Caller has to dispose of returned Bitmap object.</remarks>
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope",
+ Justification = "Callers of this method are responsible for disposing returned Bitmap")]
+ private static Bitmap GetBitmapFromImage(Image image, int width, int height, bool preserveResolution = true)
+ {
+ bool indexed = (image.PixelFormat == PixelFormat.Format1bppIndexed ||
+ image.PixelFormat == PixelFormat.Format4bppIndexed ||
+ image.PixelFormat == PixelFormat.Format8bppIndexed ||
+ image.PixelFormat == PixelFormat.Indexed);
+
+ Bitmap bitmap = indexed ? new Bitmap(width, height) : new Bitmap(width, height, image.PixelFormat);
+ if (preserveResolution)
+ {
+ bitmap.SetResolution(image.HorizontalResolution, image.VerticalResolution);
+ }
+ else
+ {
+ bitmap.SetResolution(FixedResolution, FixedResolution);
+ }
+
+ using (Graphics graphic = Graphics.FromImage(bitmap))
+ {
+ if (indexed)
+ {
+ graphic.FillRectangle(Brushes.White, 0, 0, width, height);
+ }
+ graphic.InterpolationMode = InterpolationMode.HighQualityBicubic;
+ graphic.DrawImage(image, 0, 0, width, height);
+ }
+
+ return bitmap;
+ }
+
+ private static void CopyMetadata(PropertyItem[] properties, Image target)
+ {
+ foreach (PropertyItem property in properties)
+ {
+ try
+ {
+ target.SetPropertyItem(property);
+ }
+ catch (ArgumentException)
+ {
+ // just ignore it; on some configurations this fails
+ }
+ }
+ }
+
+ private class CropTransformation : ImageTransformation
+ {
+ public CropTransformation(int top, int right, int bottom, int left)
+ {
+ Top = top;
+ Right = right;
+ Bottom = bottom;
+ Left = left;
+ }
+
+ public int Top { get; set; }
+ public int Right { get; set; }
+ public int Bottom { get; set; }
+ public int Left { get; set; }
+
+ public override Image ApplyTransformation(Image image)
+ {
+ if ((Top + Bottom > image.Height) || (Left + Right > image.Width))
+ {
+ // If Crop arguments are too big (i.e. whole image is cropped) we don't make any changes.
+ return image;
+ }
+
+ int width = image.Width - (Left + Right);
+ int height = image.Height - (Top + Bottom);
+
+ RectangleF rect = new RectangleF(Left, Top, width, height);
+
+ // todo: check if we can guarantee that rect is inside the image at this point
+ using (Bitmap bitmap = GetBitmapFromImage(image, image.Width, image.Height))
+ {
+ try
+ {
+ return bitmap.Clone(rect, image.PixelFormat);
+ }
+ catch (OutOfMemoryException)
+ {
+ // Bitmap.Clone unfortunately throws OOM exception when rect is
+ // outside of the source bitmap bounds
+ return image;
+ }
+ }
+ }
+ }
+
+ private abstract class ImageTransformation
+ {
+ public abstract Image ApplyTransformation(Image image);
+ }
+
+ private class ResizeTransformation : ImageTransformation
+ {
+ public ResizeTransformation(int height, int width, bool preserveAspectRatio, bool preventEnlarge)
+ {
+ Height = height;
+ Width = width;
+ PreserveAspectRatio = preserveAspectRatio;
+ PreventEnlarge = preventEnlarge;
+ }
+
+ public int Height { get; set; }
+ public int Width { get; set; }
+ public bool PreserveAspectRatio { get; set; }
+ public bool PreventEnlarge { get; set; }
+
+ public override Image ApplyTransformation(Image image)
+ {
+ int height = Height;
+ int width = Width;
+
+ if (PreserveAspectRatio)
+ {
+ double heightRatio = (height * 100.0) / image.Height;
+ double widthRatio = (width * 100.0) / image.Width;
+ if (heightRatio > widthRatio)
+ {
+ height = (int)Math.Round((widthRatio * image.Height) / 100);
+ }
+ else if (heightRatio < widthRatio)
+ {
+ width = (int)Math.Round((heightRatio * image.Width) / 100);
+ }
+ }
+
+ if (PreventEnlarge)
+ {
+ if (height > image.Height)
+ {
+ height = image.Height;
+ }
+ if (width > image.Width)
+ {
+ width = image.Width;
+ }
+ }
+
+ if ((image.Height == height) && (image.Width == width))
+ {
+ return image;
+ }
+
+ return GetBitmapFromImage(image, width, height);
+ }
+ }
+
+ private class RotateTransformation : ImageTransformation
+ {
+ public RotateTransformation(RotateFlipType direction)
+ {
+ Direction = direction;
+ }
+
+ public RotateFlipType Direction { get; set; }
+
+ public override Image ApplyTransformation(Image image)
+ {
+ image.RotateFlip(Direction);
+ return image;
+ }
+ }
+
+ private class WatermarkImageTransformation : WatermarkTransformation
+ {
+ public WatermarkImageTransformation(
+ WebImage image,
+ int width,
+ int height,
+ HorizontalAlign horizontalAlign,
+ VerticalAlign verticalAlign,
+ int opacity,
+ int padding)
+ : base(horizontalAlign, verticalAlign, padding)
+ {
+ WatermarkImage = image;
+ Width = width;
+ Height = height;
+ Opacity = opacity;
+ }
+
+ public WebImage WatermarkImage { get; set; }
+ public int Width { get; set; }
+ public int Height { get; set; }
+ public int Opacity { get; set; }
+
+ public override Image ApplyTransformation(Image image)
+ {
+ // Use original image dimensions if user didn't specify any.
+ if (Width == 0)
+ {
+ Debug.Assert(Height == 0, "If one dimension is zero the other one must be too.");
+ Width = WatermarkImage.Width;
+ Height = WatermarkImage.Height;
+ }
+
+ if (((Padding * 2) + Width >= image.Width) || ((Padding * 2) + Height >= image.Height))
+ {
+ // If watermark image + padding is too big we don't make any changes.
+ return image;
+ }
+
+ WatermarkImage.Resize(Width, Height, preserveAspectRatio: false, preventEnlarge: false);
+ float alphaScaling = ((float)Opacity) / 100;
+
+ byte[] watermarkBuffer = WatermarkImage.GetBytes();
+ Rectangle rect = GetRectangleInsideImage(image, Width, Height);
+
+ using (Graphics targetGraphics = Graphics.FromImage(image))
+ {
+ using (MemoryStream memStream = new MemoryStream(watermarkBuffer))
+ {
+ using (Image watermarkImage = Image.FromStream(memStream))
+ {
+ AddWatermark(targetGraphics, watermarkImage, rect, alphaScaling);
+ }
+ }
+ }
+
+ return image;
+ }
+ }
+
+ private class WatermarkTextTransformation : WatermarkTransformation
+ {
+ public WatermarkTextTransformation(
+ string text,
+ Color fontColor,
+ int fontSize,
+ FontStyle fontStyle,
+ FontFamily fontFamily,
+ HorizontalAlign alignX,
+ VerticalAlign alignY,
+ int padding)
+ : base(alignX, alignY, padding)
+ {
+ Text = text;
+ FontColor = fontColor;
+ FontSize = fontSize;
+ FontStyle = fontStyle;
+ FontFamily = fontFamily;
+ }
+
+ public string Text { get; set; }
+ public Color FontColor { get; set; }
+ public int FontSize { get; set; }
+ public FontStyle FontStyle { get; set; }
+ public FontFamily FontFamily { get; set; }
+
+ public override Image ApplyTransformation(Image image)
+ {
+ if ((Padding * 2 >= image.Width) || (Padding * 2 >= image.Height))
+ {
+ // If padding is too big we don't make any changes.
+ return image;
+ }
+
+ // Get font size and text area that text fits into using fixed size resolution version of the image.
+ // This is needed so we create the same size/position text watermark even when images have different
+ // resolutions. Otherwise, watermark is slightly different even when text & size arguments are the same.
+ int fontSize;
+ SizeF textArea;
+ using (Bitmap fixedResolutionImage = GetBitmapFromImage(image, image.Width, image.Height, preserveResolution: false))
+ {
+ using (Graphics graphics = Graphics.FromImage(fixedResolutionImage))
+ {
+ fontSize = GetBestFontSize(image, graphics, out textArea);
+ }
+ }
+
+ int textWidth = (int)Math.Ceiling(textArea.Width);
+ int textHeight = (int)Math.Ceiling(textArea.Height);
+ Rectangle area = GetRectangleInsideImage(image, textWidth, textHeight);
+
+ // Create new bitmap that only contains text in the right size.
+ using (Bitmap textBitmap = new Bitmap(textWidth, textHeight))
+ {
+ using (Graphics graphics = Graphics.FromImage(textBitmap))
+ {
+ using (Font font = new Font(FontFamily, fontSize, FontStyle))
+ {
+ using (Brush brushToUse = new SolidBrush(FontColor))
+ {
+ graphics.TextRenderingHint = TextRenderingHint.AntiAlias;
+ graphics.DrawString(Text, font, brushToUse, new PointF(0F, 0F));
+ }
+ }
+ }
+
+ // Use generated bitmap with text to apply as image watermark.
+ using (Graphics targetGraphics = Graphics.FromImage(image))
+ {
+ AddWatermark(targetGraphics, textBitmap, area, 1f);
+ }
+ }
+
+ return image;
+ }
+
+ private int GetBestFontSize(Image image, Graphics graphics, out SizeF textArea)
+ {
+ SizeF layoutArea = new SizeF(image.Width - (Padding * 2), image.Height - (Padding * 2));
+ int bestFontSize = FontSize;
+ textArea = layoutArea;
+
+ using (StringFormat format = new StringFormat(StringFormatFlags.NoClip | StringFormatFlags.MeasureTrailingSpaces))
+ {
+ for (int fontSize = FontSize; fontSize >= 2; fontSize--)
+ {
+ int numChars = 0, numLines = 0;
+ using (Font font = new Font(FontFamily, fontSize, FontStyle))
+ {
+ textArea = graphics.MeasureString(Text, font, layoutArea, format, out numChars, out numLines);
+ }
+
+ if ((numChars >= Text.Length) && (textArea.Width <= layoutArea.Width) && (textArea.Height <= layoutArea.Height))
+ {
+ // it fits! Exit now
+ return fontSize;
+ }
+ else
+ {
+ bestFontSize = fontSize;
+ }
+ }
+ }
+ return bestFontSize;
+ }
+ }
+
+ private abstract class WatermarkTransformation : ImageTransformation
+ {
+ private static readonly float[][] _identityScalingMatrix =
+ {
+ new float[] { 1, 0, 0, 0, 0 },
+ new float[] { 0, 1, 0, 0, 0 },
+ new float[] { 0, 0, 1, 0, 0 },
+ new float[] { 0, 0, 0, 1, 0 },
+ new float[] { 0, 0, 0, 0, 1 }
+ };
+
+ public WatermarkTransformation(HorizontalAlign alignX, VerticalAlign alignY, int padding)
+ {
+ HorizontalAlign = alignX;
+ VerticalAlign = alignY;
+ Padding = padding;
+ }
+
+ public HorizontalAlign HorizontalAlign { get; set; }
+ public VerticalAlign VerticalAlign { get; set; }
+ public int Padding { get; set; }
+
+ public Rectangle GetRectangleInsideImage(Image image, int width, int height)
+ {
+ int posX, posY;
+
+ switch (HorizontalAlign)
+ {
+ case HorizontalAlign.Left:
+ posX = Padding;
+ break;
+ case HorizontalAlign.Right:
+ posX = image.Width - width - Padding;
+ break;
+ case HorizontalAlign.Center:
+ default:
+ posX = (image.Width - width) / 2;
+ break;
+ }
+ switch (VerticalAlign)
+ {
+ case VerticalAlign.Top:
+ posY = Padding;
+ break;
+ case VerticalAlign.Bottom:
+ posY = image.Height - height - Padding;
+ break;
+ case VerticalAlign.Middle:
+ default:
+ posY = (image.Height - height) / 2;
+ break;
+ }
+
+ return new Rectangle(posX, posY, width, height);
+ }
+
+ private static float[][] GetScalingMatrix(float alphaScaling)
+ {
+ if (alphaScaling == 1)
+ {
+ return _identityScalingMatrix;
+ }
+
+ float[][] scalingMatrix =
+ {
+ new float[] { 1, 0, 0, 0, 0 },
+ new float[] { 0, 1, 0, 0, 0 },
+ new float[] { 0, 0, 1, 0, 0 },
+ new[] { 0, 0, 0, alphaScaling, 0 },
+ new float[] { 0, 0, 0, 0, 1 }
+ };
+ return scalingMatrix;
+ }
+
+ public static void AddWatermark(Graphics targetGraphics, Image watermark, Rectangle rect, float alphaScaling)
+ {
+ float[][] scalingMatrix = GetScalingMatrix(alphaScaling);
+ ColorMatrix colorMatrix = new ColorMatrix(scalingMatrix);
+
+ using (ImageAttributes imageAtt = new ImageAttributes())
+ {
+ imageAtt.SetColorMatrix(colorMatrix, ColorMatrixFlag.Default, ColorAdjustType.Default);
+ targetGraphics.DrawImage(watermark, rect, 0, 0, watermark.Width, watermark.Height, GraphicsUnit.Pixel, imageAtt);
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Helpers/WebMail.cs b/src/System.Web.Helpers/WebMail.cs
new file mode 100644
index 00000000..5595c470
--- /dev/null
+++ b/src/System.Web.Helpers/WebMail.cs
@@ -0,0 +1,364 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Net;
+using System.Net.Mail;
+using System.Text;
+using System.Web.Helpers.Resources;
+using System.Web.WebPages.Scope;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.Helpers
+{
+ [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "WebMail", Justification = "The name of this class is consistent with the naming convention followed in other helpers")]
+ public static class WebMail
+ {
+ internal static readonly object SmtpServerKey = new object();
+ internal static readonly object SmtpPortKey = new object();
+ internal static readonly object SmtpUseDefaultCredentialsKey = new object();
+ internal static readonly object EnableSslKey = new object();
+ internal static readonly object PasswordKey = new object();
+ internal static readonly object UserNameKey = new object();
+ internal static readonly object FromKey = new object();
+ internal static readonly Lazy<IDictionary<object, object>> SmtpDefaults = new Lazy<IDictionary<object, object>>(ReadSmtpDefaults);
+
+ /// <summary>
+ /// MailMessage dictates that headers values that have equivalent properties would be discarded or overwritten. The list of values is available at
+ /// http://msdn.microsoft.com/en-us/library/system.net.mail.mailmessage.aspx
+ /// </summary>
+ private static readonly Dictionary<string, Action<MailMessage, string>> _actionableHeaders = new Dictionary<string, Action<MailMessage, string>>(StringComparer.OrdinalIgnoreCase)
+ {
+ { "Bcc", (message, value) => message.Bcc.Add(value) },
+ { "Cc", (message, value) => message.CC.Add(value) },
+ { "From", (mailMessage, value) =>
+ {
+ mailMessage.From = new MailAddress(value);
+ }
+ },
+ { "Priority", SetPriority },
+ { "Reply-To", (mailMessage, value) =>
+ {
+ mailMessage.ReplyToList.Add(value);
+ }
+ },
+ { "Sender", (mailMessage, value) =>
+ {
+ mailMessage.Sender = new MailAddress(value);
+ }
+ },
+ { "To", (mailMessage, value) =>
+ {
+ mailMessage.To.Add(value);
+ }
+ },
+ };
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Public Properties
+ [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Justification = "SmtpServer is more descriptive as compared to the actual argument \"value\"")]
+ public static string SmtpServer
+ {
+ get { return ReadValue<string>(SmtpServerKey); }
+ set
+ {
+ if (String.IsNullOrEmpty(value))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "SmtpServer");
+ }
+ ScopeStorage.CurrentScope[SmtpServerKey] = value;
+ }
+ }
+
+ public static int SmtpPort
+ {
+ get { return ReadValue<int>(SmtpPortKey); }
+ set { ScopeStorage.CurrentScope[SmtpPortKey] = value; }
+ }
+
+ public static string From
+ {
+ get { return ReadValue<string>(FromKey); }
+ set { ScopeStorage.CurrentScope[FromKey] = value; }
+ }
+
+ public static bool SmtpUseDefaultCredentials
+ {
+ get { return ReadValue<bool>(SmtpUseDefaultCredentialsKey); }
+ set { ScopeStorage.CurrentScope[SmtpUseDefaultCredentialsKey] = value; }
+ }
+
+ public static bool EnableSsl
+ {
+ get { return ReadValue<bool>(EnableSslKey); }
+ set { ScopeStorage.CurrentScope[EnableSslKey] = value; }
+ }
+
+ public static string UserName
+ {
+ get { return ReadValue<string>(UserNameKey); }
+ set { ScopeStorage.CurrentScope[UserNameKey] = value; }
+ }
+
+ public static string Password
+ {
+ get { return ReadValue<string>(PasswordKey); }
+ set { ScopeStorage.CurrentScope[PasswordKey] = value; }
+ }
+
+ public static void Send(string to,
+ string subject,
+ string body,
+ string from = null,
+ string cc = null,
+ IEnumerable<string> filesToAttach = null,
+ bool isBodyHtml = true,
+ IEnumerable<string> additionalHeaders = null,
+ string bcc = null,
+ string contentEncoding = null,
+ string headerEncoding = null,
+ string priority = null,
+ string replyTo = null)
+ {
+ if (filesToAttach != null)
+ {
+ foreach (string fileName in filesToAttach)
+ {
+ if (String.IsNullOrEmpty(fileName))
+ {
+ throw new ArgumentException(HelpersResources.WebMail_ItemInCollectionIsNull, "filesToAttach");
+ }
+ }
+ }
+
+ if (additionalHeaders != null)
+ {
+ foreach (string header in additionalHeaders)
+ {
+ if (String.IsNullOrEmpty(header))
+ {
+ throw new ArgumentException(HelpersResources.WebMail_ItemInCollectionIsNull, "additionalHeaders");
+ }
+ }
+ }
+
+ MailPriority priorityValue = MailPriority.Normal;
+ if (!String.IsNullOrEmpty(priority) && !ConversionUtil.TryFromStringToEnum(priority, out priorityValue))
+ {
+ throw new ArgumentException(HelpersResources.WebMail_InvalidPriority, "priority");
+ }
+
+ if (String.IsNullOrEmpty(SmtpServer))
+ {
+ throw new InvalidOperationException(HelpersResources.WebMail_SmtpServerNotSpecified);
+ }
+
+ using (MailMessage message = new MailMessage())
+ {
+ SetPropertiesOnMessage(message, to, subject, body, from, cc, bcc, replyTo, contentEncoding, headerEncoding, priorityValue,
+ filesToAttach, isBodyHtml, additionalHeaders);
+ using (SmtpClient client = new SmtpClient())
+ {
+ SetPropertiesOnClient(client);
+ client.Send(message);
+ }
+ }
+ }
+
+ private static TValue ReadValue<TValue>(object key)
+ {
+ return (TValue)(ScopeStorage.CurrentScope[key] ?? SmtpDefaults.Value[key]);
+ }
+
+ private static IDictionary<object, object> ReadSmtpDefaults()
+ {
+ Dictionary<object, object> smtpDefaults = new Dictionary<object, object>();
+ try
+ {
+ // Create a new SmtpClient object: this will read config & tell us what the default value is
+ using (SmtpClient client = new SmtpClient())
+ {
+ smtpDefaults[SmtpServerKey] = client.Host;
+ smtpDefaults[SmtpPortKey] = client.Port;
+ smtpDefaults[EnableSslKey] = client.EnableSsl;
+ smtpDefaults[SmtpUseDefaultCredentialsKey] = client.UseDefaultCredentials;
+
+ var credentials = client.Credentials as NetworkCredential;
+ if (credentials != null)
+ {
+ smtpDefaults[UserNameKey] = credentials.UserName;
+ smtpDefaults[PasswordKey] = credentials.Password;
+ }
+ else
+ {
+ smtpDefaults[UserNameKey] = null;
+ smtpDefaults[PasswordKey] = null;
+ }
+ using (MailMessage message = new MailMessage())
+ {
+ smtpDefaults[FromKey] = (message.From != null) ? message.From.Address : null;
+ }
+ }
+ }
+ catch (InvalidOperationException)
+ {
+ // Due to Bug Dev10 PS 337470 ("SmtpClient reports InvalidOperationException when disposed"), we need to ignore the spurious InvalidOperationException
+ }
+ return smtpDefaults;
+ }
+
+ internal static void SetPropertiesOnClient(SmtpClient client)
+ {
+ // If no value has been assigned to these properties, at the very worst we will simply
+ // write back the values we just read from the SmtpClient
+ if (SmtpServer != null)
+ {
+ client.Host = SmtpServer;
+ }
+ client.Port = SmtpPort;
+ client.UseDefaultCredentials = SmtpUseDefaultCredentials;
+ client.EnableSsl = EnableSsl;
+ if (!String.IsNullOrEmpty(UserName))
+ {
+ client.Credentials = new NetworkCredential(UserName, Password);
+ }
+ }
+
+ internal static void SetPropertiesOnMessage(MailMessage message, string to, string subject,
+ string body, string from, string cc, string bcc, string replyTo,
+ string contentEncoding, string headerEncoding, MailPriority priority,
+ IEnumerable<string> filesToAttach, bool isBodyHtml,
+ IEnumerable<string> additionalHeaders)
+ {
+ message.Subject = subject;
+ message.Body = body;
+ message.IsBodyHtml = isBodyHtml;
+
+ if (additionalHeaders != null)
+ {
+ AssignHeaderValues(message, additionalHeaders);
+ }
+
+ if (to != null)
+ {
+ message.To.Add(to);
+ }
+
+ if (!String.IsNullOrEmpty(cc))
+ {
+ message.CC.Add(cc);
+ }
+
+ if (!String.IsNullOrEmpty(bcc))
+ {
+ message.Bcc.Add(bcc);
+ }
+
+ if (!String.IsNullOrEmpty(replyTo))
+ {
+ message.ReplyToList.Add(replyTo);
+ }
+
+ if (!String.IsNullOrEmpty(contentEncoding))
+ {
+ message.BodyEncoding = Encoding.GetEncoding(contentEncoding);
+ }
+
+ if (!String.IsNullOrEmpty(headerEncoding))
+ {
+ message.HeadersEncoding = Encoding.GetEncoding(headerEncoding);
+ }
+
+ message.Priority = priority;
+
+ if (from != null)
+ {
+ message.From = new MailAddress(from);
+ }
+ else if (!String.IsNullOrEmpty(From))
+ {
+ message.From = new MailAddress(From);
+ }
+ else if (message.From == null || String.IsNullOrEmpty(message.From.Address))
+ {
+ var httpContext = HttpContext.Current;
+ if (httpContext != null)
+ {
+ message.From = new MailAddress("DoNotReply@" + httpContext.Request.Url.Host);
+ }
+ else
+ {
+ throw new InvalidOperationException(HelpersResources.WebMail_UnableToDetermineFrom);
+ }
+ }
+
+ if (filesToAttach != null)
+ {
+ foreach (string file in filesToAttach)
+ {
+ if (!Path.IsPathRooted(file) && HttpRuntime.AppDomainAppPath != null)
+ {
+ message.Attachments.Add(new Attachment(Path.Combine(HttpRuntime.AppDomainAppPath, file)));
+ }
+ else
+ {
+ message.Attachments.Add(new Attachment(file));
+ }
+ }
+ }
+ }
+
+ internal static void AssignHeaderValues(MailMessage message, IEnumerable<string> headerValues)
+ {
+ // Parse the header value. If this
+ foreach (var header in headerValues)
+ {
+ string key, value;
+ if (TryParseHeader(header, out key, out value))
+ {
+ // Verify if the header key maps to a property on MailMessage.
+ Action<MailMessage, string> action;
+ if (_actionableHeaders.TryGetValue(key, out action))
+ {
+ try
+ {
+ action(message, value);
+ }
+ catch (FormatException)
+ {
+ // If the mail address is invalid, swallow the exception.
+ }
+ }
+ message.Headers.Add(key, value);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Parses a SMTP Mail header of the format "name: value"
+ /// </summary>
+ /// <returns>True if the header was parsed.</returns>
+ internal static bool TryParseHeader(string header, out string key, out string value)
+ {
+ int pos = header.IndexOf(':');
+ if (pos > 0)
+ {
+ key = header.Substring(0, pos).TrimEnd();
+ value = header.Substring(pos + 1).TrimStart();
+ return key.Length > 0 && value.Length > 0;
+ }
+ key = null;
+ value = null;
+ return false;
+ }
+
+ private static void SetPriority(MailMessage message, string priority)
+ {
+ MailPriority priorityValue;
+ if (!String.IsNullOrEmpty(priority) && ConversionUtil.TryFromStringToEnum(priority, out priorityValue))
+ {
+ // If we can parse it, set it. Do nothing otherwise
+ message.Priority = priorityValue;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http.Common/Error.cs b/src/System.Web.Http.Common/Error.cs
new file mode 100644
index 00000000..97c71257
--- /dev/null
+++ b/src/System.Web.Http.Common/Error.cs
@@ -0,0 +1,261 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Web.Http.Common.Properties;
+
+namespace System.Web.Http.Common
+{
+ /// <summary>
+ /// Utility class for creating and unwrapping <see cref="Exception"/> instances.
+ /// </summary>
+ [SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords", MessageId = "Error", Justification = "This usage is okay.")]
+ public static class Error
+ {
+ /// <summary>
+ /// Formats the specified resource string using <see cref="M:CultureInfo.CurrentCulture"/>.
+ /// </summary>
+ /// <param name="format">A composite format string.</param>
+ /// <param name="args">An object array that contains zero or more objects to format.</param>
+ /// <returns>The formatted string.</returns>
+ [SuppressMessage("Microsoft.Naming", "CA1719:ParameterNamesShouldNotMatchMemberNames", MessageId = "0#", Justification = "Standard String.Format pattern and names.")]
+ public static string Format(string format, params object[] args)
+ {
+ return String.Format(CultureInfo.CurrentCulture, format, args);
+ }
+
+ /// <summary>
+ /// Creates an <see cref="ArgumentException"/> with the provided properties and logs it with <see cref="F:TraceLevel.Error"/>.
+ /// </summary>
+ /// <param name="messageFormat">A composite format string explaining the reason for the exception.</param>
+ /// <param name="messageArgs">An object array that contains zero or more objects to format.</param>
+ /// <returns>The logged <see cref="Exception"/>.</returns>
+ public static ArgumentException Argument(string messageFormat, params object[] messageArgs)
+ {
+ return new ArgumentException(Error.Format(messageFormat, messageArgs));
+ }
+
+ /// <summary>
+ /// Creates an <see cref="ArgumentException"/> with the provided properties and logs it with <see cref="F:TraceLevel.Error"/>.
+ /// </summary>
+ /// <param name="parameterName">The name of the parameter that caused the current exception.</param>
+ /// <param name="messageFormat">A composite format string explaining the reason for the exception.</param>
+ /// <param name="messageArgs">An object array that contains zero or more objects to format.</param>
+ /// <returns>The logged <see cref="Exception"/>.</returns>
+ public static ArgumentException Argument(string parameterName, string messageFormat, params object[] messageArgs)
+ {
+ return new ArgumentException(Error.Format(messageFormat, messageArgs), parameterName);
+ }
+
+ /// <summary>
+ /// Creates an <see cref="ArgumentException"/> with a message saying that the argument must be an "http" or "https" URI and logs it with <see cref="F:TraceLevel.Error"/>.
+ /// </summary>
+ /// <param name="parameterName">The name of the parameter that caused the current exception.</param>
+ /// <param name="actualValue">The value of the argument that causes this exception.</param>
+ /// <returns>The logged <see cref="Exception"/>.</returns>
+ public static ArgumentException ArgumentUriNotHttpOrHttpsScheme(string parameterName, Uri actualValue)
+ {
+ return new ArgumentException(Error.Format(SRResources.ArgumentInvalidHttpUriScheme, actualValue, Uri.UriSchemeHttp, Uri.UriSchemeHttps), parameterName);
+ }
+
+ /// <summary>
+ /// Creates an <see cref="ArgumentException"/> with a message saying that the argument must be an absolute URI and logs it with <see cref="F:TraceLevel.Error"/>.
+ /// </summary>
+ /// <param name="parameterName">The name of the parameter that caused the current exception.</param>
+ /// <param name="actualValue">The value of the argument that causes this exception.</param>
+ /// <returns>The logged <see cref="Exception"/>.</returns>
+ public static ArgumentException ArgumentUriNotAbsolute(string parameterName, Uri actualValue)
+ {
+ return new ArgumentException(Error.Format(SRResources.ArgumentInvalidAbsoluteUri, actualValue), parameterName);
+ }
+
+ /// <summary>
+ /// Creates an <see cref="ArgumentException"/> with a message saying that the argument must be an absolute URI
+ /// without a query or fragment identifier and then logs it with <see cref="F:TraceLevel.Error"/>.
+ /// </summary>
+ /// <param name="parameterName">The name of the parameter that caused the current exception.</param>
+ /// <param name="actualValue">The value of the argument that causes this exception.</param>
+ /// <returns>The logged <see cref="Exception"/>.</returns>
+ public static ArgumentException ArgumentUriHasQueryOrFragment(string parameterName, Uri actualValue)
+ {
+ return new ArgumentException(Error.Format(SRResources.ArgumentUriHasQueryOrFragment, actualValue), parameterName);
+ }
+
+ /// <summary>
+ /// Creates an <see cref="ArgumentNullException"/> with the provided properties and logs it with <see cref="F:TraceLevel.Error"/>.
+ /// </summary>
+ /// <returns>The logged <see cref="Exception"/>.</returns>
+ [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Justification = "The purpose of this API is to return an error for properties")]
+ public static ArgumentNullException PropertyNull()
+ {
+ return new ArgumentNullException("value");
+ }
+
+ /// <summary>
+ /// Creates an <see cref="ArgumentNullException"/> with the provided properties and logs it with <see cref="F:TraceLevel.Error"/>.
+ /// </summary>
+ /// <param name="parameterName">The name of the parameter that caused the current exception.</param>
+ /// <returns>The logged <see cref="Exception"/>.</returns>
+ public static ArgumentNullException ArgumentNull(string parameterName)
+ {
+ return new ArgumentNullException(parameterName);
+ }
+
+ /// <summary>
+ /// Creates an <see cref="ArgumentNullException"/> with the provided properties and logs it with <see cref="F:TraceLevel.Error"/>.
+ /// </summary>
+ /// <param name="parameterName">The name of the parameter that caused the current exception.</param>
+ /// <param name="messageFormat">A composite format string explaining the reason for the exception.</param>
+ /// <param name="messageArgs">An object array that contains zero or more objects to format.</param>
+ /// <returns>The logged <see cref="Exception"/>.</returns>
+ public static ArgumentNullException ArgumentNull(string parameterName, string messageFormat, params object[] messageArgs)
+ {
+ return new ArgumentNullException(parameterName, Error.Format(messageFormat, messageArgs));
+ }
+
+ /// <summary>
+ /// Creates an <see cref="ArgumentException"/> with a default message and logs it with <see cref="F:TraceLevel.Error"/>.
+ /// </summary>
+ /// <param name="parameterName">The name of the parameter that caused the current exception.</param>
+ /// <returns>The logged <see cref="Exception"/>.</returns>
+ public static ArgumentException ArgumentNullOrEmpty(string parameterName)
+ {
+ return Error.Argument(parameterName, SRResources.ArgumentNullOrEmpty, parameterName);
+ }
+
+ /// <summary>
+ /// Creates an <see cref="ArgumentOutOfRangeException"/> with the provided properties and logs it with <see cref="F:TraceLevel.Error"/>.
+ /// </summary>
+ /// <param name="parameterName">The name of the parameter that caused the current exception.</param>
+ /// <param name="actualValue">The value of the argument that causes this exception.</param>
+ /// <param name="messageFormat">A composite format string explaining the reason for the exception.</param>
+ /// <param name="messageArgs">An object array that contains zero or more objects to format.</param>
+ /// <returns>The logged <see cref="Exception"/>.</returns>
+ public static ArgumentOutOfRangeException ArgumentOutOfRange(string parameterName, object actualValue, string messageFormat, params object[] messageArgs)
+ {
+ return new ArgumentOutOfRangeException(parameterName, actualValue, Error.Format(messageFormat, messageArgs));
+ }
+
+ /// <summary>
+ /// Creates an <see cref="ArgumentOutOfRangeException"/> with a message saying that the argument must be greater than or equal to <paramref name="minValue"/> and logs it with <see cref="F:TraceLevel.Error"/>.
+ /// </summary>
+ /// <param name="parameterName">The name of the parameter that caused the current exception.</param>
+ /// <param name="actualValue">The value of the argument that causes this exception.</param>
+ /// <param name="minValue">The minimum size of the argument.</param>
+ /// <returns>The logged <see cref="Exception"/>.</returns>
+ public static ArgumentOutOfRangeException ArgumentTooSmall(string parameterName, object actualValue, object minValue)
+ {
+ return new ArgumentOutOfRangeException(parameterName, actualValue, Error.Format(SRResources.ArgumentMustBeGreaterThanOrEqualTo, actualValue, minValue));
+ }
+
+ /// <summary>
+ /// Creates an <see cref="ArgumentOutOfRangeException"/> with a message saying that the argument must be less than or equal to <paramref name="maxValue"/> and logs it with <see cref="F:TraceLevel.Error"/>.
+ /// </summary>
+ /// <param name="parameterName">The name of the parameter that caused the current exception.</param>
+ /// <param name="actualValue">The value of the argument that causes this exception.</param>
+ /// <param name="maxValue">The maximum size of the argument.</param>
+ /// <returns>The logged <see cref="Exception"/>.</returns>
+ public static ArgumentOutOfRangeException ArgumentTooLarge(string parameterName, object actualValue, object maxValue)
+ {
+ return new ArgumentOutOfRangeException(parameterName, actualValue, Error.Format(SRResources.ArgumentMustBeLessThanOrEqualTo, actualValue, maxValue));
+ }
+
+ /// <summary>
+ /// Creates an <see cref="KeyNotFoundException"/> with a message saying that the key was not found and logs it with <see cref="F:TraceLevel.Error"/>.
+ /// </summary>
+ /// <returns>The logged <see cref="Exception"/>.</returns>
+ public static KeyNotFoundException KeyNotFound()
+ {
+ return new KeyNotFoundException();
+ }
+
+ /// <summary>
+ /// Creates an <see cref="KeyNotFoundException"/> with a message saying that the key was not found and logs it with <see cref="F:TraceLevel.Error"/>.
+ /// </summary>
+ /// <param name="messageFormat">A composite format string explaining the reason for the exception.</param>
+ /// <param name="messageArgs">An object array that contains zero or more objects to format.</param>
+ /// <returns>The logged <see cref="Exception"/>.</returns>
+ public static KeyNotFoundException KeyNotFound(string messageFormat, params object[] messageArgs)
+ {
+ return new KeyNotFoundException(Error.Format(messageFormat, messageArgs));
+ }
+
+ /// <summary>
+ /// Creates an <see cref="ObjectDisposedException"/> initialized according to guidelines and logs it with <see cref="F:TraceLevel.Error"/>.
+ /// </summary>
+ /// <param name="messageFormat">A composite format string explaining the reason for the exception.</param>
+ /// <param name="messageArgs">An object array that contains zero or more objects to format.</param>
+ /// <returns>The logged <see cref="Exception"/>.</returns>
+ public static ObjectDisposedException ObjectDisposed(string messageFormat, params object[] messageArgs)
+ {
+ // Pass in null, not disposedObject.GetType().FullName as per the above guideline
+ return new ObjectDisposedException(null, Error.Format(messageFormat, messageArgs));
+ }
+
+ /// <summary>
+ /// Creates an <see cref="OperationCanceledException"/> initialized with the provided parameters and logs it with <see cref="F:TraceLevel.Error"/>.
+ /// </summary>
+ /// <returns>The logged <see cref="Exception"/>.</returns>
+ public static OperationCanceledException OperationCanceled()
+ {
+ return new OperationCanceledException();
+ }
+
+ /// <summary>
+ /// Creates an <see cref="OperationCanceledException"/> initialized with the provided parameters and logs it with <see cref="F:TraceLevel.Error"/>.
+ /// </summary>
+ /// <param name="messageFormat">A composite format string explaining the reason for the exception.</param>
+ /// <param name="messageArgs">An object array that contains zero or more objects to format.</param>
+ /// <returns>The logged <see cref="Exception"/>.</returns>
+ public static OperationCanceledException OperationCanceled(string messageFormat, params object[] messageArgs)
+ {
+ return new OperationCanceledException(Error.Format(messageFormat, messageArgs));
+ }
+
+ /// <summary>
+ /// Creates an <see cref="InvalidEnumArgumentException"/> and logs it with <see cref="F:TraceLevel.Error"/>.
+ /// </summary>
+ /// <param name="parameterName">The name of the parameter that caused the current exception.</param>
+ /// <param name="invalidValue">The value of the argument that failed.</param>
+ /// <param name="enumClass">A <see cref="Type"/> that represents the enumeration class with the valid values.</param>
+ /// <returns>The logged <see cref="Exception"/>.</returns>
+ public static InvalidEnumArgumentException InvalidEnumArgument(string parameterName, int invalidValue, Type enumClass)
+ {
+ return new InvalidEnumArgumentException(parameterName, invalidValue, enumClass);
+ }
+
+ /// <summary>
+ /// Creates an <see cref="InvalidOperationException"/> and logs it with <see cref="F:TraceLevel.Error"/>.
+ /// </summary>
+ /// <param name="messageFormat">A composite format string explaining the reason for the exception.</param>
+ /// <param name="messageArgs">An object array that contains zero or more objects to format.</param>
+ /// <returns>The logged <see cref="Exception"/>.</returns>
+ public static InvalidOperationException InvalidOperation(string messageFormat, params object[] messageArgs)
+ {
+ return new InvalidOperationException(Error.Format(messageFormat, messageArgs));
+ }
+
+ /// <summary>
+ /// Creates an <see cref="InvalidOperationException"/> and logs it with <see cref="F:TraceLevel.Error"/>.
+ /// </summary>
+ /// <param name="innerException">Inner exception</param>
+ /// <param name="messageFormat">A composite format string explaining the reason for the exception.</param>
+ /// <param name="messageArgs">An object array that contains zero or more objects to format.</param>
+ /// <returns>The logged <see cref="Exception"/>.</returns>
+ public static InvalidOperationException InvalidOperation(Exception innerException, string messageFormat, params object[] messageArgs)
+ {
+ return new InvalidOperationException(Error.Format(messageFormat, messageArgs), innerException);
+ }
+
+ /// <summary>
+ /// Creates an <see cref="NotSupportedException"/> and logs it with <see cref="F:TraceLevel.Error"/>.
+ /// </summary>
+ /// <param name="messageFormat">A composite format string explaining the reason for the exception.</param>
+ /// <param name="messageArgs">An object array that contains zero or more objects to format.</param>
+ /// <returns>The logged <see cref="Exception"/>.</returns>
+ public static NotSupportedException NotSupported(string messageFormat, params object[] messageArgs)
+ {
+ return new NotSupportedException(Error.Format(messageFormat, messageArgs));
+ }
+ }
+}
diff --git a/src/System.Web.Http.Common/GlobalSuppressions.cs b/src/System.Web.Http.Common/GlobalSuppressions.cs
new file mode 100644
index 00000000..6b0d4e59
--- /dev/null
+++ b/src/System.Web.Http.Common/GlobalSuppressions.cs
@@ -0,0 +1,5 @@
+using System.Diagnostics.CodeAnalysis;
+
+[assembly: SuppressMessage("Microsoft.Design", "CA2210:AssembliesShouldHaveValidStrongNames", Justification = "Will be signed")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "System.Web.Http")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "System.Web.Http.Common")]
diff --git a/src/System.Web.Http.Common/HttpMethodHelper.cs b/src/System.Web.Http.Common/HttpMethodHelper.cs
new file mode 100644
index 00000000..0fdc4d28
--- /dev/null
+++ b/src/System.Web.Http.Common/HttpMethodHelper.cs
@@ -0,0 +1,60 @@
+using System.Net.Http;
+
+namespace System.Web.Http.Common
+{
+ /// <summary>
+ /// Various helper methods for the static members of <see cref="HttpMethod"/>.
+ /// </summary>
+ public static class HttpMethodHelper
+ {
+ /// <summary>
+ /// Gets the static <see cref="HttpMethod"/> instance for any given HTTP method name.
+ /// </summary>
+ /// <param name="method">The HTTP request method.</param>
+ /// <returns>An existing static <see cref="HttpMethod"/> or a new instance if the method was not found.</returns>
+ public static HttpMethod GetHttpMethod(string method)
+ {
+ if (String.IsNullOrEmpty(method))
+ {
+ return null;
+ }
+
+ if (String.Equals("GET", method, StringComparison.OrdinalIgnoreCase))
+ {
+ return HttpMethod.Get;
+ }
+
+ if (String.Equals("POST", method, StringComparison.OrdinalIgnoreCase))
+ {
+ return HttpMethod.Post;
+ }
+
+ if (String.Equals("PUT", method, StringComparison.OrdinalIgnoreCase))
+ {
+ return HttpMethod.Put;
+ }
+
+ if (String.Equals("DELETE", method, StringComparison.OrdinalIgnoreCase))
+ {
+ return HttpMethod.Delete;
+ }
+
+ if (String.Equals("HEAD", method, StringComparison.OrdinalIgnoreCase))
+ {
+ return HttpMethod.Head;
+ }
+
+ if (String.Equals("OPTIONS", method, StringComparison.OrdinalIgnoreCase))
+ {
+ return HttpMethod.Options;
+ }
+
+ if (String.Equals("TRACE", method, StringComparison.OrdinalIgnoreCase))
+ {
+ return HttpMethod.Trace;
+ }
+
+ return new HttpMethod(method);
+ }
+ }
+}
diff --git a/src/System.Web.Http.Common/HttpRequestMessageCommonExtensions.cs b/src/System.Web.Http.Common/HttpRequestMessageCommonExtensions.cs
new file mode 100644
index 00000000..436c7212
--- /dev/null
+++ b/src/System.Web.Http.Common/HttpRequestMessageCommonExtensions.cs
@@ -0,0 +1,55 @@
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Net;
+using System.Net.Http;
+using System.Web.Http.Common;
+
+namespace System.Web.Http
+{
+ /// <summary>
+ /// Provides extension methods for the <see cref="HttpRequestMessage"/> class.
+ /// </summary>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static class HttpRequestMessageCommonExtensions
+ {
+ /// <summary>
+ /// Creates an <see cref="HttpResponseMessage"/> wired up to the associated <see cref="HttpRequestMessage"/>.
+ /// </summary>
+ /// <param name="request">The HTTP request.</param>
+ /// <param name="statusCode">The HTTP status code.</param>
+ /// <returns>An initialized <see cref="HttpResponseMessage"/>.</returns>
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Caller will dispose")]
+ public static HttpResponseMessage CreateResponse(this HttpRequestMessage request, HttpStatusCode statusCode)
+ {
+ if (request == null)
+ {
+ throw Error.ArgumentNull("request");
+ }
+
+ return new HttpResponseMessage
+ {
+ StatusCode = statusCode,
+ RequestMessage = request
+ };
+ }
+
+ /// <summary>
+ /// Creates an <see cref="HttpResponseMessage"/> wired up to the associated <see cref="HttpRequestMessage"/>.
+ /// </summary>
+ /// <param name="request">The HTTP request.</param>
+ /// <returns>An initialized <see cref="HttpResponseMessage"/>.</returns>
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Caller will dispose")]
+ public static HttpResponseMessage CreateResponse(this HttpRequestMessage request)
+ {
+ if (request == null)
+ {
+ throw Error.ArgumentNull("request");
+ }
+
+ return new HttpResponseMessage
+ {
+ RequestMessage = request
+ };
+ }
+ }
+}
diff --git a/src/System.Web.Http.Common/Properties/AssemblyInfo.cs b/src/System.Web.Http.Common/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..56b085e1
--- /dev/null
+++ b/src/System.Web.Http.Common/Properties/AssemblyInfo.cs
@@ -0,0 +1,16 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+
+[assembly: AssemblyTitle("System.Web.Http.Common")]
+[assembly: AssemblyDescription("")]
+[assembly: InternalsVisibleTo("System.Web.Http, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
+[assembly: InternalsVisibleTo("System.Web.Http.Common.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
+[assembly: InternalsVisibleTo("System.Web.Http.SelfHost, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
+[assembly: InternalsVisibleTo("System.Web.Http.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
+[assembly: InternalsVisibleTo("System.Web.Http.WebHost, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
+[assembly: InternalsVisibleTo("System.Web.Mvc, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
+[assembly: InternalsVisibleTo("System.Web.Mvc.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
diff --git a/src/System.Web.Http.Common/Properties/SRResources.Designer.cs b/src/System.Web.Http.Common/Properties/SRResources.Designer.cs
new file mode 100644
index 00000000..82dfb987
--- /dev/null
+++ b/src/System.Web.Http.Common/Properties/SRResources.Designer.cs
@@ -0,0 +1,117 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.239
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace System.Web.Http.Common.Properties {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class SRResources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal SRResources() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("System.Web.Http.Common.Properties.SRResources", typeof(SRResources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Relative URI values are not supported: &apos;{0}&apos;. The URI must be absolute..
+ /// </summary>
+ internal static string ArgumentInvalidAbsoluteUri {
+ get {
+ return ResourceManager.GetString("ArgumentInvalidAbsoluteUri", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Unsupported URI scheme: &apos;{0}&apos;. The URI scheme must be either &apos;{1}&apos; or &apos;{2}&apos;..
+ /// </summary>
+ internal static string ArgumentInvalidHttpUriScheme {
+ get {
+ return ResourceManager.GetString("ArgumentInvalidHttpUriScheme", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The argument &apos;{0}&apos; must be greater than or equal to {1}..
+ /// </summary>
+ internal static string ArgumentMustBeGreaterThanOrEqualTo {
+ get {
+ return ResourceManager.GetString("ArgumentMustBeGreaterThanOrEqualTo", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The argument &apos;{0}&apos; must be less than or equal to {1}..
+ /// </summary>
+ internal static string ArgumentMustBeLessThanOrEqualTo {
+ get {
+ return ResourceManager.GetString("ArgumentMustBeLessThanOrEqualTo", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The argument &apos;{0}&apos; is null or empty..
+ /// </summary>
+ internal static string ArgumentNullOrEmpty {
+ get {
+ return ResourceManager.GetString("ArgumentNullOrEmpty", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to URI must not contain a query component or a fragment identifier..
+ /// </summary>
+ internal static string ArgumentUriHasQueryOrFragment {
+ get {
+ return ResourceManager.GetString("ArgumentUriHasQueryOrFragment", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http.Common/Properties/SRResources.resx b/src/System.Web.Http.Common/Properties/SRResources.resx
new file mode 100644
index 00000000..14322733
--- /dev/null
+++ b/src/System.Web.Http.Common/Properties/SRResources.resx
@@ -0,0 +1,138 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="ArgumentInvalidAbsoluteUri" xml:space="preserve">
+ <value>Relative URI values are not supported: '{0}'. The URI must be absolute.</value>
+ </data>
+ <data name="ArgumentInvalidHttpUriScheme" xml:space="preserve">
+ <value>Unsupported URI scheme: '{0}'. The URI scheme must be either '{1}' or '{2}'.</value>
+ </data>
+ <data name="ArgumentMustBeGreaterThanOrEqualTo" xml:space="preserve">
+ <value>The argument '{0}' must be greater than or equal to {1}.</value>
+ </data>
+ <data name="ArgumentMustBeLessThanOrEqualTo" xml:space="preserve">
+ <value>The argument '{0}' must be less than or equal to {1}.</value>
+ </data>
+ <data name="ArgumentNullOrEmpty" xml:space="preserve">
+ <value>The argument '{0}' is null or empty.</value>
+ </data>
+ <data name="ArgumentUriHasQueryOrFragment" xml:space="preserve">
+ <value>URI must not contain a query component or a fragment identifier.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/System.Web.Http.Common/RouteParameter.cs b/src/System.Web.Http.Common/RouteParameter.cs
new file mode 100644
index 00000000..dc75e048
--- /dev/null
+++ b/src/System.Web.Http.Common/RouteParameter.cs
@@ -0,0 +1,34 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Web.Http
+{
+ /// <summary>
+ /// The <see cref="RouteParameter"/> class can be used to indicate properties about a route parameter (the literals and placeholders
+ /// located within segments of a <see cref="M:IHttpRoute.RouteTemplate"/>).
+ /// It can for example be used to indicate that a route parameter is optional.
+ /// </summary>
+ public sealed class RouteParameter
+ {
+ /// <summary>
+ /// Optional Parameter
+ /// </summary>
+ [SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", Justification = "This type is immutable.")]
+ public static readonly RouteParameter Optional = new RouteParameter();
+
+ // singleton constructor
+ private RouteParameter()
+ {
+ }
+
+ /// <summary>
+ /// Returns a <see cref="System.String"/> that represents this instance.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="System.String"/> that represents this instance.
+ /// </returns>
+ public override string ToString()
+ {
+ return String.Empty;
+ }
+ }
+}
diff --git a/src/System.Web.Http.Common/System.Web.Http.Common.csproj b/src/System.Web.Http.Common/System.Web.Http.Common.csproj
new file mode 100644
index 00000000..48f723d8
--- /dev/null
+++ b/src/System.Web.Http.Common/System.Web.Http.Common.csproj
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <CodeAnalysis Condition=" '$(CodeAnalysis)' == '' ">false</CodeAnalysis>
+ <ProductVersion>9.0.30729</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{03A5E5F2-2E23-48F2-ABCC-6C41BAC9AC02}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>System.Web.Http.Common</RootNamespace>
+ <AssemblyName>System.Web.Http.Common</AssemblyName>
+ <FileAlignment>512</FileAlignment>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>..\..\bin\Debug\</OutputPath>
+ <DefineConstants>TRACE;DEBUG;ASPNETMVC</DefineConstants>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ <DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
+ <NoWarn>1591</NoWarn>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>..\..\bin\Release\</OutputPath>
+ <DefineConstants>TRACE;ASPNETMVC</DefineConstants>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ <RunCodeAnalysis>$(CodeAnalysis)</RunCodeAnalysis>
+ <DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
+ <NoWarn>1591</NoWarn>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'CodeCoverage|AnyCPU'">
+ <DebugSymbols>true</DebugSymbols>
+ <OutputPath>..\..\bin\CodeCoverage\</OutputPath>
+ <DefineConstants>TRACE;DEBUG;CODE_COVERAGE;ASPNETMVC</DefineConstants>
+ <DebugType>full</DebugType>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="System" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.Net.Http">
+ <HintPath>..\..\packages\Microsoft.Net.Http.2.0.20302.1\lib\net40\System.Net.Http.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Net.Http.WebRequest">
+ <HintPath>..\..\packages\Microsoft.Net.Http.2.0.20302.1\lib\net40\System.Net.Http.WebRequest.dll</HintPath>
+ </Reference>
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="..\CommonAssemblyInfo.cs">
+ <Link>Properties\CommonAssemblyInfo.cs</Link>
+ </Compile>
+ <Compile Include="..\TransparentCommonAssemblyInfo.cs">
+ <Link>Properties\TransparentCommonAssemblyInfo.cs</Link>
+ </Compile>
+ <Compile Include="Error.cs" />
+ <Compile Include="GlobalSuppressions.cs" />
+ <Compile Include="HttpMethodHelper.cs" />
+ <Compile Include="HttpRequestMessageCommonExtensions.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="Properties\SRResources.Designer.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>SRResources.resx</DependentUpon>
+ </Compile>
+ <Compile Include="TaskHelpersExtensions.cs" />
+ <Compile Include="TaskHelpers.cs" />
+ <Compile Include="RouteParameter.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="Properties\SRResources.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>SRResources.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ </ItemGroup>
+ <ItemGroup>
+ <CodeAnalysisDictionary Include="..\CodeAnalysisDictionary.xml">
+ <Link>CodeAnalysisDictionary.xml</Link>
+ </CodeAnalysisDictionary>
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="packages.config" />
+ </ItemGroup>
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+</Project> \ No newline at end of file
diff --git a/src/System.Web.Http.Common/TaskHelpers.cs b/src/System.Web.Http.Common/TaskHelpers.cs
new file mode 100644
index 00000000..66dccc2a
--- /dev/null
+++ b/src/System.Web.Http.Common/TaskHelpers.cs
@@ -0,0 +1,421 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+using System.Linq;
+
+namespace System.Threading.Tasks
+{
+ /// <summary>
+ /// Helpers for safely using Task libraries.
+ /// </summary>
+ internal static class TaskHelpers
+ {
+ private static Task _defaultCompleted = FromResult<AsyncVoid>(default(AsyncVoid));
+
+ /// <summary>
+ /// Returns a canceled Task. The task is completed, IsCanceled = True, IsFaulted = False.
+ /// </summary>
+ internal static Task Canceled()
+ {
+ return CancelCache<AsyncVoid>.Canceled;
+ }
+
+ /// <summary>
+ /// Returns a canceled Task of the given type. The task is completed, IsCanceled = True, IsFaulted = False.
+ /// </summary>
+ internal static Task<TResult> Canceled<TResult>()
+ {
+ return CancelCache<TResult>.Canceled;
+ }
+
+ /// <summary>
+ /// Returns a completed task that has no result.
+ /// </summary>
+ internal static Task Completed()
+ {
+ return _defaultCompleted;
+ }
+
+ /// <summary>
+ /// Returns an error task. The task is Completed, IsCanceled = False, IsFaulted = True
+ /// </summary>
+ internal static Task FromError(Exception exception)
+ {
+ return FromError<AsyncVoid>(exception);
+ }
+
+ /// <summary>
+ /// Returns an error task of the given type. The task is Completed, IsCanceled = False, IsFaulted = True
+ /// </summary>
+ /// <typeparam name="TResult"></typeparam>
+ internal static Task<TResult> FromError<TResult>(Exception exception)
+ {
+ TaskCompletionSource<TResult> tcs = new TaskCompletionSource<TResult>();
+ tcs.SetException(exception);
+ return tcs.Task;
+ }
+
+ /// <summary>
+ /// Returns an error task of the given type. The task is Completed, IsCanceled = False, IsFaulted = True
+ /// </summary>
+ internal static Task FromErrors(IEnumerable<Exception> exceptions)
+ {
+ return FromErrors<AsyncVoid>(exceptions);
+ }
+
+ /// <summary>
+ /// Returns an error task of the given type. The task is Completed, IsCanceled = False, IsFaulted = True
+ /// </summary>
+ internal static Task<TResult> FromErrors<TResult>(IEnumerable<Exception> exceptions)
+ {
+ TaskCompletionSource<TResult> tcs = new TaskCompletionSource<TResult>();
+ tcs.SetException(exceptions);
+ return tcs.Task;
+ }
+
+ /// <summary>
+ /// Returns a successful completed task with the given result.
+ /// </summary>
+ internal static Task<TResult> FromResult<TResult>(TResult result)
+ {
+ TaskCompletionSource<TResult> tcs = new TaskCompletionSource<TResult>();
+ tcs.SetResult(result);
+ return tcs.Task;
+ }
+
+ /// <summary>
+ /// Return a task that runs all the tasks inside the iterator sequentially. It stops as soon
+ /// as one of the tasks fails or cancels, or after all the tasks have run succesfully.
+ /// </summary>
+ /// <param name="asyncIterator">collection of tasks to wait on</param>
+ /// <param name="cancellationToken">cancellation token</param>
+ /// <returns>a task that signals completed when all the incoming tasks are finished.</returns>
+ internal static Task Iterate(IEnumerable<Task> asyncIterator, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ Contract.Assert(asyncIterator != null);
+
+ return IterateEngine.Run(asyncIterator, cancellationToken);
+ }
+
+ /// <summary>
+ /// Return a task that runs all the tasks inside the iterator sequentially and collects the results.
+ /// It stops as soon as one of the tasks fails or cancels, or after all the tasks have run succesfully.
+ /// </summary>
+ /// <param name="asyncIterator">collection of tasks to wait on</param>
+ /// <param name="cancellationToken">cancellation token</param>
+ /// <returns>A task that, upon successful completion, returns the list of results.</returns>
+ internal static Task<IEnumerable<TResult>> Iterate<TResult>(IEnumerable<Task<TResult>> asyncIterator, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ Contract.Assert(asyncIterator != null);
+
+ return IterateEngine<TResult>.Run(asyncIterator, cancellationToken);
+ }
+
+ /// <summary>
+ /// Replacement for Task.Factory.StartNew when the code can run synchronously.
+ /// We run the code immediately and avoid the thread switch.
+ /// This is used to help synchronous code implement task interfaces.
+ /// </summary>
+ /// <param name="action">action to run synchronouslyt</param>
+ /// <param name="token">cancellation token. This is only checked before we run the task, and if cancelled, we immediately return a cancelled task.</param>
+ /// <returns>a task who result is the result from Func()</returns>
+ /// <remarks>
+ /// Avoid calling Task.Factory.StartNew.
+ /// This avoids gotchas with StartNew:
+ /// - ensures cancellation token is checked (StartNew doesn't check cancellation tokens).
+ /// - Keeps on the same thread.
+ /// - Avoids switching synchronization contexts.
+ /// Also take in a lambda so that we can wrap in a try catch and honor task failure semantics.
+ /// </remarks>
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "The caught exception type is reflected into a faulted task.")]
+ public static Task RunSynchronously(Action action, CancellationToken token = default(CancellationToken))
+ {
+ if (token.IsCancellationRequested)
+ {
+ return Canceled();
+ }
+
+ try
+ {
+ action();
+ return Completed();
+ }
+ catch (Exception e)
+ {
+ return FromError(e);
+ }
+ }
+
+ /// <summary>
+ /// Replacement for Task.Factory.StartNew when the code can run synchronously.
+ /// We run the code immediately and avoid the thread switch.
+ /// This is used to help synchronous code implement task interfaces.
+ /// </summary>
+ /// <typeparam name="TResult">type of result that task will return.</typeparam>
+ /// <param name="func">function to run synchronously and produce result</param>
+ /// <param name="cancellationToken">cancellation token. This is only checked before we run the task, and if cancelled, we immediately return a cancelled task.</param>
+ /// <returns>a task who result is the result from Func()</returns>
+ /// <remarks>
+ /// Avoid calling Task.Factory.StartNew.
+ /// This avoids gotchas with StartNew:
+ /// - ensures cancellation token is checked (StartNew doesn't check cancellation tokens).
+ /// - Keeps on the same thread.
+ /// - Avoids switching synchronization contexts.
+ /// Also take in a lambda so that we can wrap in a try catch and honor task failure semantics.
+ /// </remarks>
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "The caught exception type is reflected into a faulted task.")]
+ internal static Task<TResult> RunSynchronously<TResult>(Func<TResult> func, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ if (cancellationToken.IsCancellationRequested)
+ {
+ return Canceled<TResult>();
+ }
+
+ try
+ {
+ return FromResult(func());
+ }
+ catch (Exception e)
+ {
+ return FromError<TResult>(e);
+ }
+ }
+
+ /// <summary>
+ /// Overload of RunSynchronously that avoids a call to Unwrap().
+ /// This overload is useful when func() starts doing some synchronous work and then hits IO and
+ /// needs to create a task to finish the work.
+ /// </summary>
+ /// <typeparam name="TResult">type of result that Task will return</typeparam>
+ /// <param name="func">function that returns a task</param>
+ /// <param name="cancellationToken">cancellation token. This is only checked before we run the task, and if cancelled, we immediately return a cancelled task.</param>
+ /// <returns>a task, created by running func().</returns>
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "The caught exception type is reflected into a faulted task.")]
+ internal static Task<TResult> RunSynchronously<TResult>(Func<Task<TResult>> func, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ if (cancellationToken.IsCancellationRequested)
+ {
+ return Canceled<TResult>();
+ }
+
+ try
+ {
+ return func();
+ }
+ catch (Exception e)
+ {
+ return FromError<TResult>(e);
+ }
+ }
+
+ /// <summary>
+ /// Update the completion source if the task failed (cancelled or faulted). No change to completion source if the task succeeded.
+ /// </summary>
+ /// <typeparam name="TResult">result type of completion source</typeparam>
+ /// <param name="tcs">completion source to update</param>
+ /// <param name="source">task to update from.</param>
+ /// <returns>true on success</returns>
+ internal static bool SetIfTaskFailed<TResult>(this TaskCompletionSource<TResult> tcs, Task source)
+ {
+ switch (source.Status)
+ {
+ case TaskStatus.Canceled:
+ case TaskStatus.Faulted:
+ return tcs.TrySetFromTask(source);
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Set a completion source from the given Task.
+ /// </summary>
+ /// <typeparam name="TResult">result type for completion source.</typeparam>
+ /// <param name="tcs">completion source to set</param>
+ /// <param name="source">Task to get values from.</param>
+ /// <returns>true if this successfully sets the completion source.</returns>
+ [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "This is a known safe usage of Task.Result, since it only occurs when we know the task's state to be completed.")]
+ internal static bool TrySetFromTask<TResult>(this TaskCompletionSource<TResult> tcs, Task source)
+ {
+ if (source.Status == TaskStatus.Canceled)
+ {
+ return tcs.TrySetCanceled();
+ }
+
+ if (source.Status == TaskStatus.Faulted)
+ {
+ return tcs.TrySetException(source.Exception.InnerExceptions);
+ }
+
+ if (source.Status == TaskStatus.RanToCompletion)
+ {
+ Task<TResult> taskOfResult = source as Task<TResult>;
+ return tcs.TrySetResult(taskOfResult == null ? default(TResult) : taskOfResult.Result);
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Set a completion source from the given Task. If the task ran to completion and the result type doesn't match
+ /// the type of the completion source, then a default value will be used. This is useful for converting Task into
+ /// Task{AsyncVoid}, but it can also accidentally be used to introduce data loss (by passing the wrong
+ /// task type), so please execute this method with care.
+ /// </summary>
+ /// <typeparam name="TResult">result type for completion source.</typeparam>
+ /// <param name="tcs">completion source to set</param>
+ /// <param name="source">Task to get values from.</param>
+ /// <returns>true if this successfully sets the completion source.</returns>
+ [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "This is a known safe usage of Task.Result, since it only occurs when we know the task's state to be completed.")]
+ internal static bool TrySetFromTask<TResult>(this TaskCompletionSource<Task<TResult>> tcs, Task source)
+ {
+ if (source.Status == TaskStatus.Canceled)
+ {
+ return tcs.TrySetCanceled();
+ }
+
+ if (source.Status == TaskStatus.Faulted)
+ {
+ return tcs.TrySetException(source.Exception.InnerExceptions);
+ }
+
+ if (source.Status == TaskStatus.RanToCompletion)
+ {
+ // Sometimes the source task is Task<Task<TResult>>, and sometimes it's Task<TResult>.
+ // The latter usually happens when we're in the middle of a sync-block postback where
+ // the continuation is a function which returns Task<TResult> rather than just TResult,
+ // but the originating task was itself just Task<TResult>. An example of this can be
+ // found in TaskExtensions.CatchImpl().
+ Task<Task<TResult>> taskOfTaskOfResult = source as Task<Task<TResult>>;
+ if (taskOfTaskOfResult != null)
+ {
+ return tcs.TrySetResult(taskOfTaskOfResult.Result);
+ }
+
+ Task<TResult> taskOfResult = source as Task<TResult>;
+ if (taskOfResult != null)
+ {
+ return tcs.TrySetResult(taskOfResult);
+ }
+
+ return tcs.TrySetResult(TaskHelpers.FromResult(default(TResult)));
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Used as the T in a "conversion" of a Task into a Task{T}
+ /// </summary>
+ private struct AsyncVoid
+ {
+ }
+
+ /// <summary>
+ /// This class is a convenient cache for per-type cancelled tasks
+ /// </summary>
+ private static class CancelCache<TResult>
+ {
+ public static readonly Task<TResult> Canceled = GetCancelledTask();
+
+ private static Task<TResult> GetCancelledTask()
+ {
+ TaskCompletionSource<TResult> tcs = new TaskCompletionSource<TResult>();
+ tcs.SetCanceled();
+ return tcs.Task;
+ }
+ }
+
+ // These classes are the engine that implements Iterate and Iterate<T>
+ private static class IterateEngine
+ {
+ public static Task Run(IEnumerable<Task> iterator, CancellationToken cancellationToken)
+ {
+ // WARNING: This code uses LINQ Select to ensure that we get deferred execution (i.e., we
+ // don't start running all the tasks all at once). If you touch this code, please ensure
+ // that this behavior is preserved.
+ return IterateEngine<AsyncVoid>.Run(iterator.Select(t => t.ToTask<AsyncVoid>()), cancellationToken);
+ }
+ }
+
+ private class IterateEngine<TResult>
+ {
+ private CancellationToken _cancellationToken;
+ private TaskCompletionSource<IEnumerable<TResult>> _completionSource;
+ private IEnumerator<Task<TResult>> _enumerator;
+ private List<TResult> _results;
+ private SynchronizationContext _syncContext;
+
+ public static Task<IEnumerable<TResult>> Run(IEnumerable<Task<TResult>> iterator, CancellationToken cancellationToken)
+ {
+ IterateEngine<TResult> engine = new IterateEngine<TResult>
+ {
+ _cancellationToken = cancellationToken,
+ _completionSource = new TaskCompletionSource<IEnumerable<TResult>>(),
+ _enumerator = iterator.GetEnumerator(),
+ _results = new List<TResult>(),
+ _syncContext = SynchronizationContext.Current
+ };
+
+ RunNext(engine);
+ return engine._completionSource.Task.Finally(engine._enumerator.Dispose);
+ }
+
+ private static void RunNext(IterateEngine<TResult> engine)
+ {
+ if (engine._syncContext != null && engine._syncContext != SynchronizationContext.Current)
+ {
+ engine._syncContext.Post(RunNextCallback, engine);
+ }
+ else
+ {
+ RunNextCallback(engine);
+ }
+ }
+
+ // TODO: This class can become more efficient once we take a hard 4.5 dependency. In 4.0, ContinueWith
+ // does not offer you the ability to pass a state object; once it does, we can change the implementation
+ // of RunNextCallback to remove the closure around "engine".
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "The caught exception type is reflected into a faulted task.")]
+ [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "This usage is known to be safe.")]
+ private static void RunNextCallback(object state)
+ {
+ IterateEngine<TResult> engine = (IterateEngine<TResult>)state;
+
+ try
+ {
+ if (engine._cancellationToken.IsCancellationRequested)
+ {
+ engine._completionSource.TrySetCanceled();
+ }
+ else if (engine._enumerator.MoveNext())
+ {
+ engine._enumerator.Current.ContinueWith(previous =>
+ {
+ switch (previous.Status)
+ {
+ case TaskStatus.Faulted:
+ case TaskStatus.Canceled:
+ engine._completionSource.TrySetFromTask(previous);
+ break;
+
+ default:
+ engine._results.Add(previous.Result);
+ RunNext(engine);
+ break;
+ }
+ });
+ }
+ else
+ {
+ engine._completionSource.TrySetResult(engine._results);
+ }
+ }
+ catch (Exception e)
+ {
+ engine._completionSource.TrySetException(e);
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http.Common/TaskHelpersExtensions.cs b/src/System.Web.Http.Common/TaskHelpersExtensions.cs
new file mode 100644
index 00000000..2501434d
--- /dev/null
+++ b/src/System.Web.Http.Common/TaskHelpersExtensions.cs
@@ -0,0 +1,557 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Linq.Expressions;
+using System.Reflection;
+
+namespace System.Threading.Tasks
+{
+ internal static class TaskHelpersExtensions
+ {
+ private static Task<AsyncVoid> _defaultCompleted = TaskHelpers.FromResult<AsyncVoid>(default(AsyncVoid));
+ private static readonly Action<Task> _rethrowWithNoStackLossDelegate = GetRethrowWithNoStackLossDelegate();
+
+ /// <summary>
+ /// Calls the given continuation, after the given task completes, if it ends in a faulted state.
+ /// Will not be called if the task did not fault (meaning, it will not be called if the task ran
+ /// to completion or was canceled). Intended to roughly emulate C# 5's support for "try/catch" in
+ /// async methods. Note that this method allows you to return a Task, so that you can either return
+ /// a completed Task (indicating that you swallowed the exception) or a faulted task (indicating that
+ /// that the exception should be propagated). In C#, you cannot normally use await within a catch
+ /// block, so returning a real async task should never be done from Catch().
+ /// </summary>
+ internal static Task Catch(this Task task, Func<Exception, Task> continuation, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ return task.CatchImpl(ex => continuation(ex).ToTask<AsyncVoid>(), cancellationToken);
+ }
+
+ /// <summary>
+ /// Calls the given continuation, after the given task completes, if it ends in a faulted state.
+ /// Will not be called if the task did not fault (meaning, it will not be called if the task ran
+ /// to completion or was canceled). Intended to roughly emulate C# 5's support for "try/catch" in
+ /// async methods. Note that this method allows you to return a Task, so that you can either return
+ /// a completed Task (indicating that you swallowed the exception) or a faulted task (indicating that
+ /// that the exception should be propagated). In C#, you cannot normally use await within a catch
+ /// block, so returning a real async task should never be done from Catch().
+ /// </summary>
+ internal static Task<TResult> Catch<TResult>(this Task<TResult> task, Func<Exception, Task<TResult>> continuation, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ return task.CatchImpl(continuation, cancellationToken);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "The caught exception type is reflected into a faulted task.")]
+ [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "TaskHelpersExtensions", Justification = "This is the name of a class.")]
+ [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")]
+ private static Task<TResult> CatchImpl<TResult>(this Task task, Func<Exception, Task<TResult>> continuation, CancellationToken cancellationToken)
+ {
+ // Stay on the same thread if we can
+ if (task.IsCanceled || cancellationToken.IsCancellationRequested)
+ {
+ return TaskHelpers.Canceled<TResult>();
+ }
+ if (task.IsFaulted)
+ {
+ try
+ {
+ Task<TResult> resultTask = continuation(task.Exception.GetBaseException());
+ if (resultTask == null)
+ {
+ // Not a resource because this is an internal class, and this is a guard clause that's intended
+ // to be thrown by us to us, never escaping out to end users.
+ throw new InvalidOperationException("You cannot return null from the TaskHelpersExtensions.Catch continuation. You must return a valid task or throw an exception.");
+ }
+
+ return resultTask;
+ }
+ catch (Exception ex)
+ {
+ return TaskHelpers.FromError<TResult>(ex);
+ }
+ }
+ if (task.Status == TaskStatus.RanToCompletion)
+ {
+ TaskCompletionSource<TResult> tcs = new TaskCompletionSource<TResult>();
+ tcs.TrySetFromTask(task);
+ return tcs.Task;
+ }
+
+ SynchronizationContext syncContext = SynchronizationContext.Current;
+
+ return task.ContinueWith(innerTask =>
+ {
+ TaskCompletionSource<Task<TResult>> tcs = new TaskCompletionSource<Task<TResult>>();
+
+ if (innerTask.IsFaulted)
+ {
+ if (syncContext != null)
+ {
+ syncContext.Post(state =>
+ {
+ try
+ {
+ Task<TResult> resultTask = continuation(innerTask.Exception.GetBaseException());
+ if (resultTask == null)
+ {
+ throw new InvalidOperationException("You cannot return null from the TaskHelpersExtensions.Catch continuation. You must return a valid task or throw an exception.");
+ }
+
+ tcs.TrySetResult(resultTask);
+ }
+ catch (Exception ex)
+ {
+ tcs.TrySetException(ex);
+ }
+ }, state: null);
+ }
+ else
+ {
+ Task<TResult> resultTask = continuation(innerTask.Exception.GetBaseException());
+ if (resultTask == null)
+ {
+ throw new InvalidOperationException("You cannot return null from the TaskHelpersExtensions.Catch continuation. You must return a valid task or throw an exception.");
+ }
+
+ tcs.TrySetResult(resultTask);
+ }
+ }
+ else
+ {
+ tcs.TrySetFromTask(innerTask);
+ }
+
+ return tcs.Task.FastUnwrap();
+ }, cancellationToken).FastUnwrap();
+ }
+
+ /// <summary>
+ /// Upon completion of the task, copies its result into the given task completion source, regardless of the
+ /// completion state. This causes the original task to be fully observed, and the task that is returned by
+ /// this method will always successfully run to completion, regardless of the original task state.
+ /// Since this method consumes a task with no return value, you must provide the return value to be used
+ /// when the inner task ran to successful completion.
+ /// </summary>
+ internal static Task CopyResultToCompletionSource<TResult>(this Task task, TaskCompletionSource<TResult> tcs, TResult completionResult)
+ {
+ return task.CopyResultToCompletionSourceImpl(tcs, innerTask => completionResult);
+ }
+
+ /// <summary>
+ /// Upon completion of the task, copies its result into the given task completion source, regardless of the
+ /// completion state. This causes the original task to be fully observed, and the task that is returned by
+ /// this method will always successfully run to completion, regardless of the original task state.
+ /// </summary>
+ [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")]
+ internal static Task CopyResultToCompletionSource<TResult>(this Task<TResult> task, TaskCompletionSource<TResult> tcs)
+ {
+ return task.CopyResultToCompletionSourceImpl(tcs, innerTask => innerTask.Result);
+ }
+
+ [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")]
+ private static Task CopyResultToCompletionSourceImpl<TTask, TResult>(this TTask task, TaskCompletionSource<TResult> tcs, Func<TTask, TResult> resultThunk)
+ where TTask : Task
+ {
+ if (task.IsCompleted)
+ {
+ switch (task.Status)
+ {
+ case TaskStatus.Canceled:
+ case TaskStatus.Faulted:
+ TaskHelpers.TrySetFromTask(tcs, task);
+ break;
+
+ case TaskStatus.RanToCompletion:
+ tcs.TrySetResult(resultThunk(task));
+ break;
+ }
+
+ return TaskHelpers.Completed();
+ }
+
+ return task.ContinueWith(innerTask =>
+ {
+ switch (innerTask.Status)
+ {
+ case TaskStatus.Canceled:
+ case TaskStatus.Faulted:
+ TaskHelpers.TrySetFromTask(tcs, innerTask);
+ break;
+
+ case TaskStatus.RanToCompletion:
+ tcs.TrySetResult(resultThunk(task));
+ break;
+ }
+ }, TaskContinuationOptions.ExecuteSynchronously);
+ }
+
+ /// <summary>
+ /// A version of task.Unwrap that is optimized to prevent unnecessarily capturing the
+ /// execution context when the antecedent task is already completed.
+ /// </summary>
+ [SuppressMessage("Microsoft.WebAPI", "CR4000:DoNotUseProblematicTaskTypes", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")]
+ [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")]
+ internal static Task FastUnwrap(this Task<Task> task)
+ {
+ Task innerTask = task.Status == TaskStatus.RanToCompletion ? task.Result : null;
+ return innerTask ?? task.Unwrap();
+ }
+
+ /// <summary>
+ /// A version of task.Unwrap that is optimized to prevent unnecessarily capturing the
+ /// execution context when the antecedent task is already completed.
+ /// </summary>
+ [SuppressMessage("Microsoft.WebAPI", "CR4000:DoNotUseProblematicTaskTypes", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")]
+ [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")]
+ internal static Task<TResult> FastUnwrap<TResult>(this Task<Task<TResult>> task)
+ {
+ Task<TResult> innerTask = task.Status == TaskStatus.RanToCompletion ? task.Result : null;
+ return innerTask ?? task.Unwrap();
+ }
+
+ /// <summary>
+ /// Calls the given continuation, after the given task has completed, regardless of the state
+ /// the task ended in. Intended to roughly emulate C# 5's support for "finally" in async methods.
+ /// </summary>
+ internal static Task Finally(this Task task, Action continuation)
+ {
+ return task.FinallyImpl<AsyncVoid>(continuation);
+ }
+
+ /// <summary>
+ /// Calls the given continuation, after the given task has completed, regardless of the state
+ /// the task ended in. Intended to roughly emulate C# 5's support for "finally" in async methods.
+ /// </summary>
+ internal static Task<TResult> Finally<TResult>(this Task<TResult> task, Action continuation)
+ {
+ return task.FinallyImpl<TResult>(continuation);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "The caught exception type is reflected into a faulted task.")]
+ [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")]
+ private static Task<TResult> FinallyImpl<TResult>(this Task task, Action continuation)
+ {
+ // Stay on the same thread if we can
+ if (task.IsCompleted)
+ {
+ TaskCompletionSource<TResult> tcs = new TaskCompletionSource<TResult>();
+ try
+ {
+ continuation();
+ tcs.TrySetFromTask(task);
+ }
+ catch (Exception ex)
+ {
+ tcs.TrySetException(ex);
+ }
+ return tcs.Task;
+ }
+
+ SynchronizationContext syncContext = SynchronizationContext.Current;
+
+ return task.ContinueWith(innerTask =>
+ {
+ TaskCompletionSource<TResult> tcs = new TaskCompletionSource<TResult>();
+ if (syncContext != null)
+ {
+ syncContext.Post(state =>
+ {
+ try
+ {
+ continuation();
+ tcs.TrySetFromTask(innerTask);
+ }
+ catch (Exception ex)
+ {
+ tcs.SetException(ex);
+ }
+ }, state: null);
+ }
+ else
+ {
+ continuation();
+ tcs.TrySetFromTask(innerTask);
+ }
+
+ return tcs.Task;
+ }).FastUnwrap();
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2201:DoNotRaiseReservedExceptionTypes", Justification = "This general exception is not intended to be seen by the user")]
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "This general exception is not intended to be seen by the user")]
+ [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")]
+ private static Action<Task> GetRethrowWithNoStackLossDelegate()
+ {
+ MethodInfo getAwaiterMethod = typeof(Task).GetMethod("GetAwaiter", Type.EmptyTypes);
+ if (getAwaiterMethod != null)
+ {
+ // .NET 4.5 - dump the same code the 'await' keyword would have dumped
+ // >> task.GetAwaiter().GetResult()
+ // No-ops if the task completed successfully, else throws the originating exception complete with the correct call stack.
+ var taskParameter = Expression.Parameter(typeof(Task));
+ var getAwaiterCall = Expression.Call(taskParameter, getAwaiterMethod);
+ var getResultCall = Expression.Call(getAwaiterCall, "GetResult", Type.EmptyTypes);
+ var lambda = Expression.Lambda<Action<Task>>(getResultCall, taskParameter);
+ return lambda.Compile();
+ }
+ else
+ {
+ Func<Exception, Exception> prepForRemoting = null;
+
+ try
+ {
+ if (AppDomain.CurrentDomain.IsFullyTrusted)
+ {
+ // .NET 4 - do the same thing Lazy<T> does by calling Exception.PrepForRemoting
+ // This is an internal method in mscorlib.dll, so pass a test Exception to it to make sure we can call it.
+ var exceptionParameter = Expression.Parameter(typeof(Exception));
+ var prepForRemotingCall = Expression.Call(exceptionParameter, "PrepForRemoting", Type.EmptyTypes);
+ var lambda = Expression.Lambda<Func<Exception, Exception>>(prepForRemotingCall, exceptionParameter);
+ var func = lambda.Compile();
+ func(new Exception()); // make sure the method call succeeds before assigning the 'prepForRemoting' local variable
+ prepForRemoting = func;
+ }
+ }
+ catch
+ {
+ } // If delegate creation fails (medium trust) we will simply throw the base exception.
+
+ return task =>
+ {
+ try
+ {
+ task.Wait();
+ }
+ catch (AggregateException ex)
+ {
+ Exception baseException = ex.GetBaseException();
+ if (prepForRemoting != null)
+ {
+ baseException = prepForRemoting(baseException);
+ }
+ throw baseException;
+ }
+ };
+ }
+ }
+
+ /// <summary>
+ /// Calls the given continuation, after the given task has completed, if the task successfully ran
+ /// to completion (i.e., was not cancelled and did not fault).
+ /// </summary>
+ internal static Task Then(this Task task, Action continuation, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ return task.ThenImpl(t => ToAsyncVoidTask(continuation), cancellationToken);
+ }
+
+ /// <summary>
+ /// Calls the given continuation, after the given task has completed, if the task successfully ran
+ /// to completion (i.e., was not cancelled and did not fault).
+ /// </summary>
+ internal static Task<TOuterResult> Then<TOuterResult>(this Task task, Func<TOuterResult> continuation, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ return task.ThenImpl(t => TaskHelpers.FromResult(continuation()), cancellationToken);
+ }
+
+ /// <summary>
+ /// Calls the given continuation, after the given task has completed, if the task successfully ran
+ /// to completion (i.e., was not cancelled and did not fault).
+ /// </summary>
+ internal static Task Then(this Task task, Func<Task> continuation, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ return task.Then(() => continuation().Then(() => default(AsyncVoid)),
+ cancellationToken);
+ }
+
+ /// <summary>
+ /// Calls the given continuation, after the given task has completed, if the task successfully ran
+ /// to completion (i.e., was not cancelled and did not fault).
+ /// </summary>
+ internal static Task<TOuterResult> Then<TOuterResult>(this Task task, Func<Task<TOuterResult>> continuation, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ return task.ThenImpl(t => continuation(), cancellationToken);
+ }
+
+ /// <summary>
+ /// Calls the given continuation, after the given task has completed, if the task successfully ran
+ /// to completion (i.e., was not cancelled and did not fault). The continuation is provided with the
+ /// result of the task as its sole parameter.
+ /// </summary>
+ [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")]
+ internal static Task Then<TInnerResult>(this Task<TInnerResult> task, Action<TInnerResult> continuation, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ return task.ThenImpl(t => ToAsyncVoidTask(() => continuation(t.Result)), cancellationToken);
+ }
+
+ /// <summary>
+ /// Calls the given continuation, after the given task has completed, if the task successfully ran
+ /// to completion (i.e., was not cancelled and did not fault). The continuation is provided with the
+ /// result of the task as its sole parameter.
+ /// </summary>
+ [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")]
+ internal static Task<TOuterResult> Then<TInnerResult, TOuterResult>(this Task<TInnerResult> task, Func<TInnerResult, TOuterResult> continuation, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ return task.ThenImpl(t => TaskHelpers.FromResult(continuation(t.Result)), cancellationToken);
+ }
+
+ /// <summary>
+ /// Calls the given continuation, after the given task has completed, if the task successfully ran
+ /// to completion (i.e., was not cancelled and did not fault). The continuation is provided with the
+ /// result of the task as its sole parameter.
+ /// </summary>
+ [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")]
+ internal static Task<TOuterResult> Then<TInnerResult, TOuterResult>(this Task<TInnerResult> task, Func<TInnerResult, Task<TOuterResult>> continuation, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ return task.ThenImpl(t => continuation(t.Result), cancellationToken);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "The caught exception type is reflected into a faulted task.")]
+ [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")]
+ private static Task<TOuterResult> ThenImpl<TTask, TOuterResult>(this TTask task, Func<TTask, Task<TOuterResult>> continuation, CancellationToken cancellationToken)
+ where TTask : Task
+ {
+ // Stay on the same thread if we can
+ if (task.IsCanceled || cancellationToken.IsCancellationRequested)
+ {
+ return TaskHelpers.Canceled<TOuterResult>();
+ }
+ if (task.IsFaulted)
+ {
+ return TaskHelpers.FromErrors<TOuterResult>(task.Exception.InnerExceptions);
+ }
+ if (task.Status == TaskStatus.RanToCompletion)
+ {
+ try
+ {
+ return continuation(task);
+ }
+ catch (Exception ex)
+ {
+ return TaskHelpers.FromError<TOuterResult>(ex);
+ }
+ }
+
+ SynchronizationContext syncContext = SynchronizationContext.Current;
+
+ return task.ContinueWith(innerTask =>
+ {
+ if (innerTask.IsFaulted)
+ {
+ return TaskHelpers.FromErrors<TOuterResult>(innerTask.Exception.InnerExceptions);
+ }
+ if (innerTask.IsCanceled)
+ {
+ return TaskHelpers.Canceled<TOuterResult>();
+ }
+
+ TaskCompletionSource<Task<TOuterResult>> tcs = new TaskCompletionSource<Task<TOuterResult>>();
+ if (syncContext != null)
+ {
+ syncContext.Post(state =>
+ {
+ try
+ {
+ tcs.TrySetResult(continuation(task));
+ }
+ catch (Exception ex)
+ {
+ tcs.TrySetException(ex);
+ }
+ }, state: null);
+ }
+ else
+ {
+ tcs.TrySetResult(continuation(task));
+ }
+ return tcs.Task.FastUnwrap();
+ }, cancellationToken).FastUnwrap();
+ }
+
+ /// <summary>
+ /// Throws the first faulting exception for a task which is faulted. It attempts to preserve the original
+ /// stack trace when throwing the exception (which should always work in 4.5, and should also work in 4.0
+ /// when running in full trust). Note: It is the caller's responsibility not to pass incomplete tasks to
+ /// this method, because it does degenerate into a call to the equivalent of .Wait() on the task when it
+ /// hasn't yet completed.
+ /// </summary>
+ internal static void ThrowIfFaulted(this Task task)
+ {
+ _rethrowWithNoStackLossDelegate(task);
+ }
+
+ /// <summary>
+ /// Adapts any action into a Task (returning AsyncVoid, so that it's usable with Task{T} extension methods).
+ /// </summary>
+ private static Task<AsyncVoid> ToAsyncVoidTask(Action action)
+ {
+ return TaskHelpers.RunSynchronously<AsyncVoid>(() =>
+ {
+ action();
+ return _defaultCompleted;
+ });
+ }
+
+ /// <summary>
+ /// Changes the return value of a task to the given result, if the task ends in the RanToCompletion state.
+ /// This potentially imposes an extra ContinueWith to convert a non-completed task, so use this with caution.
+ /// </summary>
+ [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")]
+ internal static Task<TResult> ToTask<TResult>(this Task task, CancellationToken cancellationToken = default(CancellationToken), TResult result = default(TResult))
+ {
+ if (task == null)
+ {
+ return null;
+ }
+
+ // Stay on the same thread if we can
+ if (task.IsCanceled || cancellationToken.IsCancellationRequested)
+ {
+ return TaskHelpers.Canceled<TResult>();
+ }
+ if (task.IsFaulted)
+ {
+ return TaskHelpers.FromErrors<TResult>(task.Exception.InnerExceptions);
+ }
+ if (task.Status == TaskStatus.RanToCompletion)
+ {
+ return TaskHelpers.FromResult(result);
+ }
+
+ return task.ContinueWith(innerTask =>
+ {
+ TaskCompletionSource<TResult> tcs = new TaskCompletionSource<TResult>();
+
+ if (task.Status == TaskStatus.RanToCompletion)
+ {
+ tcs.TrySetResult(result);
+ }
+ else
+ {
+ tcs.TrySetFromTask(innerTask);
+ }
+
+ return tcs.Task;
+ }, TaskContinuationOptions.ExecuteSynchronously).FastUnwrap();
+ }
+
+ /// <summary>
+ /// Attempts to get the result value for the given task. If the task ran to completion, then
+ /// it will return true and set the result value; otherwise, it will return false.
+ /// </summary>
+ [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "The usages here are deemed safe, and provide the implementations that this rule relies upon.")]
+ internal static bool TryGetResult<TResult>(this Task<TResult> task, out TResult result)
+ {
+ if (task.Status == TaskStatus.RanToCompletion)
+ {
+ result = task.Result;
+ return true;
+ }
+
+ result = default(TResult);
+ return false;
+ }
+
+ /// <summary>
+ /// Used as the T in a "conversion" of a Task into a Task{T}
+ /// </summary>
+ private struct AsyncVoid
+ {
+ }
+ }
+}
diff --git a/src/System.Web.Http.Common/packages.config b/src/System.Web.Http.Common/packages.config
new file mode 100644
index 00000000..c611f43d
--- /dev/null
+++ b/src/System.Web.Http.Common/packages.config
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="Microsoft.Net.Http" version="2.0.20302.1" />
+</packages> \ No newline at end of file
diff --git a/src/System.Web.Http.SelfHost/Channels/HttpBinding.cs b/src/System.Web.Http.SelfHost/Channels/HttpBinding.cs
new file mode 100644
index 00000000..730453d8
--- /dev/null
+++ b/src/System.Web.Http.SelfHost/Channels/HttpBinding.cs
@@ -0,0 +1,221 @@
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.ServiceModel;
+using System.ServiceModel.Channels;
+using System.Web.Http.Common;
+using System.Web.Http.SelfHost.ServiceModel;
+using System.Web.Http.SelfHost.ServiceModel.Channels;
+
+namespace System.Web.Http.SelfHost.Channels
+{
+ /// <summary>
+ /// A binding used with endpoints for web services that use strongly-type HTTP request
+ /// and response messages.
+ /// </summary>
+ public class HttpBinding : Binding, IBindingRuntimePreferences
+ {
+ internal const string CollectionElementName = "httpBinding";
+ internal const TransferMode DefaultTransferMode = System.ServiceModel.TransferMode.Buffered;
+
+ private HttpsTransportBindingElement _httpsTransportBindingElement;
+ private HttpTransportBindingElement _httpTransportBindingElement;
+ private HttpBindingSecurity _security;
+ private HttpMessageEncodingBindingElement _httpMessageEncodingBindingElement;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpBinding"/> class.
+ /// </summary>
+ public HttpBinding()
+ {
+ Initialize();
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpBinding"/> class with the
+ /// type of security used by the binding explicitly specified.
+ /// </summary>
+ /// <param name="securityMode">The value of <see cref="HttpBindingSecurityMode"/> that
+ /// specifies the type of security that is used to configure a service endpoint using the
+ /// <see cref="HttpBinding"/> binding.
+ /// </param>
+ public HttpBinding(HttpBindingSecurityMode securityMode)
+ : this()
+ {
+ _security.Mode = securityMode;
+ }
+
+ /// <summary>
+ /// Gets the envelope version that is used by endpoints that are configured to use an
+ /// <see cref="HttpBinding"/> binding. Always returns <see cref="System.ServiceModel.EnvelopeVersion.None"/>.
+ /// </summary>
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This is existing public API")]
+ public EnvelopeVersion EnvelopeVersion
+ {
+ get { return EnvelopeVersion.None; }
+ }
+
+ /// <summary>
+ /// Gets or sets a value that indicates whether the hostname is used to reach the
+ /// service when matching the URI.
+ /// </summary>
+ [DefaultValue(HttpTransportDefaults.HostNameComparisonMode)]
+ public HostNameComparisonMode HostNameComparisonMode
+ {
+ get { return _httpTransportBindingElement.HostNameComparisonMode; }
+
+ set
+ {
+ _httpTransportBindingElement.HostNameComparisonMode = value;
+ _httpsTransportBindingElement.HostNameComparisonMode = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the maximum amount of memory allocated for the buffer manager that manages the buffers
+ /// required by endpoints that use this binding.
+ /// </summary>
+ [DefaultValue(TransportDefaults.MaxBufferPoolSize)]
+ public long MaxBufferPoolSize
+ {
+ get { return _httpTransportBindingElement.MaxBufferPoolSize; }
+
+ set
+ {
+ _httpTransportBindingElement.MaxBufferPoolSize = value;
+ _httpsTransportBindingElement.MaxBufferPoolSize = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the maximum amount of memory that is allocated for use by the manager of the message
+ /// buffers that receive messages from the channel.
+ /// </summary>
+ [DefaultValue(TransportDefaults.MaxBufferSize)]
+ public int MaxBufferSize
+ {
+ get { return _httpTransportBindingElement.MaxBufferSize; }
+
+ set
+ {
+ _httpTransportBindingElement.MaxBufferSize = value;
+ _httpsTransportBindingElement.MaxBufferSize = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the maximum size for a message that can be processed by the binding.
+ /// </summary>
+ [DefaultValue(TransportDefaults.MaxReceivedMessageSize)]
+ public long MaxReceivedMessageSize
+ {
+ get { return _httpTransportBindingElement.MaxReceivedMessageSize; }
+
+ set
+ {
+ _httpTransportBindingElement.MaxReceivedMessageSize = value;
+ _httpsTransportBindingElement.MaxReceivedMessageSize = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets the URI transport scheme for the channels and listeners that are configured
+ /// with this binding. (Overrides <see cref="System.ServiceModel.Channels.Binding.Scheme">
+ /// Binding.Scheme</see>.)
+ /// </summary>
+ public override string Scheme
+ {
+ get { return GetTransport().Scheme; }
+ }
+
+ /// <summary>
+ /// Gets or sets the security settings used with this binding.
+ /// </summary>
+ public HttpBindingSecurity Security
+ {
+ get { return _security; }
+
+ set
+ {
+ if (value == null)
+ {
+ throw Error.ArgumentNull("value");
+ }
+
+ _security = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets a value that indicates whether the service configured with the
+ /// binding uses streamed or buffered (or both) modes of message transfer.
+ /// </summary>
+ [DefaultValue(HttpTransportDefaults.TransferMode)]
+ public TransferMode TransferMode
+ {
+ get { return _httpTransportBindingElement.TransferMode; }
+
+ set
+ {
+ _httpTransportBindingElement.TransferMode = value;
+ _httpsTransportBindingElement.TransferMode = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether incoming requests can be handled more efficiently synchronously or asynchronously.
+ /// </summary>
+ [SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Justification = "This is the pattern used by all standard bindings.")]
+ bool IBindingRuntimePreferences.ReceiveSynchronously
+ {
+ get { return false; }
+ }
+
+ /// <summary>
+ /// Returns an ordered collection of binding elements contained in the current binding.
+ /// (Overrides <see cref="System.ServiceModel.Channels.Binding.CreateBindingElements">
+ /// Binding.CreateBindingElements</see>.)
+ /// </summary>
+ /// <returns>
+ /// An ordered collection of binding elements contained in the current binding.
+ /// </returns>
+ public override BindingElementCollection CreateBindingElements()
+ {
+ BindingElementCollection bindingElements = new BindingElementCollection();
+
+ bindingElements.Add(_httpMessageEncodingBindingElement);
+ bindingElements.Add(GetTransport());
+
+ return bindingElements.Clone();
+ }
+
+ private TransportBindingElement GetTransport()
+ {
+ if (_security.Mode == HttpBindingSecurityMode.Transport)
+ {
+ _security.Transport.ConfigureTransportProtectionAndAuthentication(_httpsTransportBindingElement);
+ return _httpsTransportBindingElement;
+ }
+ else if (_security.Mode == HttpBindingSecurityMode.TransportCredentialOnly)
+ {
+ _security.Transport.ConfigureTransportAuthentication(_httpTransportBindingElement);
+ return _httpTransportBindingElement;
+ }
+
+ _security.Transport.DisableTransportAuthentication(_httpTransportBindingElement);
+ return _httpTransportBindingElement;
+ }
+
+ private void Initialize()
+ {
+ _security = new HttpBindingSecurity();
+
+ _httpTransportBindingElement = new HttpTransportBindingElement();
+ _httpTransportBindingElement.ManualAddressing = true;
+
+ _httpsTransportBindingElement = new HttpsTransportBindingElement();
+ _httpsTransportBindingElement.ManualAddressing = true;
+
+ _httpMessageEncodingBindingElement = new HttpMessageEncodingBindingElement();
+ }
+ }
+}
diff --git a/src/System.Web.Http.SelfHost/Channels/HttpBindingSecurity.cs b/src/System.Web.Http.SelfHost/Channels/HttpBindingSecurity.cs
new file mode 100644
index 00000000..ef305aa7
--- /dev/null
+++ b/src/System.Web.Http.SelfHost/Channels/HttpBindingSecurity.cs
@@ -0,0 +1,54 @@
+using System.ServiceModel;
+
+namespace System.Web.Http.SelfHost.Channels
+{
+ /// <summary>
+ /// Specifies the types of security available to a service endpoint configured to use an
+ /// <see cref="HttpBinding"/> binding.
+ /// </summary>
+ public sealed class HttpBindingSecurity
+ {
+ internal const HttpBindingSecurityMode DefaultMode = HttpBindingSecurityMode.None;
+
+ private HttpBindingSecurityMode _mode;
+ private HttpTransportSecurity _transportSecurity;
+
+ /// <summary>
+ /// Creates a new instance of the <see cref="HttpBindingSecurity"/> class.
+ /// </summary>
+ public HttpBindingSecurity()
+ {
+ _mode = DefaultMode;
+ _transportSecurity = new HttpTransportSecurity();
+ }
+
+ /// <summary>
+ /// Gets or sets the mode of security that is used by an endpoint configured to use an
+ /// <see cref="HttpBinding"/> binding.
+ /// </summary>
+ public HttpBindingSecurityMode Mode
+ {
+ get { return _mode; }
+
+ set
+ {
+ HttpBindingSecurityModeHelper.Validate(value, "value");
+ IsModeSet = true;
+ _mode = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets an object that contains the transport-level security settings for the
+ /// <see cref="HttpBinding"/> binding.
+ /// </summary>
+ public HttpTransportSecurity Transport
+ {
+ get { return _transportSecurity; }
+
+ set { _transportSecurity = value ?? new HttpTransportSecurity(); }
+ }
+
+ internal bool IsModeSet { get; private set; }
+ }
+}
diff --git a/src/System.Web.Http.SelfHost/Channels/HttpBindingSecurityMode.cs b/src/System.Web.Http.SelfHost/Channels/HttpBindingSecurityMode.cs
new file mode 100644
index 00000000..f3b6f01e
--- /dev/null
+++ b/src/System.Web.Http.SelfHost/Channels/HttpBindingSecurityMode.cs
@@ -0,0 +1,24 @@
+namespace System.Web.Http.SelfHost.Channels
+{
+ /// <summary>
+ /// Defines the modes of security that can be used to configure a service endpoint that uses the
+ /// <see cref="HttpBinding"/>.
+ /// </summary>
+ public enum HttpBindingSecurityMode
+ {
+ /// <summary>
+ /// Indicates no security is used with HTTP requests.
+ /// </summary>
+ None,
+
+ /// <summary>
+ /// Indicates that transport-level security is used with HTTP requests.
+ /// </summary>
+ Transport,
+
+ /// <summary>
+ /// Indicates that only HTTP-based client authentication is provided.
+ /// </summary>
+ TransportCredentialOnly,
+ }
+}
diff --git a/src/System.Web.Http.SelfHost/Channels/HttpBindingSecurityModeHelper.cs b/src/System.Web.Http.SelfHost/Channels/HttpBindingSecurityModeHelper.cs
new file mode 100644
index 00000000..d5e9b3c3
--- /dev/null
+++ b/src/System.Web.Http.SelfHost/Channels/HttpBindingSecurityModeHelper.cs
@@ -0,0 +1,40 @@
+using System.ComponentModel;
+using System.Web.Http.Common;
+
+namespace System.Web.Http.SelfHost.Channels
+{
+ /// <summary>
+ /// Internal helper class to validate <see cref="HttpBindingSecurityMode"/> enum values.
+ /// </summary>
+ internal static class HttpBindingSecurityModeHelper
+ {
+ private static readonly Type _httpBindingSecurityMode = typeof(HttpBindingSecurityMode);
+
+ /// <summary>
+ /// Determines whether the specified <paramref name="value"/> is defined by the <see cref="HttpBindingSecurityMode"/>
+ /// enumeration.
+ /// </summary>
+ /// <param name="value">The value to test.</param>
+ /// <returns><c>true</c> if <paramref name="value"/> is a valid <see cref="HttpBindingSecurityMode"/> value; otherwise<c> false</c>.</returns>
+ public static bool IsDefined(HttpBindingSecurityMode value)
+ {
+ return value == HttpBindingSecurityMode.None ||
+ value == HttpBindingSecurityMode.Transport ||
+ value == HttpBindingSecurityMode.TransportCredentialOnly;
+ }
+
+ /// <summary>
+ /// Validates the specified <paramref name="value"/> and throws an <see cref="InvalidEnumArgumentException"/>
+ /// exception if not valid.
+ /// </summary>
+ /// <param name="value">The value to validate.</param>
+ /// <param name="parameterName">Name of the parameter to use if throwing exception.</param>
+ public static void Validate(HttpBindingSecurityMode value, string parameterName)
+ {
+ if (!IsDefined(value))
+ {
+ throw Error.InvalidEnumArgument(parameterName, (int)value, _httpBindingSecurityMode);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http.SelfHost/Channels/HttpMessage.cs b/src/System.Web.Http.SelfHost/Channels/HttpMessage.cs
new file mode 100644
index 00000000..2d57987c
--- /dev/null
+++ b/src/System.Web.Http.SelfHost/Channels/HttpMessage.cs
@@ -0,0 +1,228 @@
+using System.Diagnostics.Contracts;
+using System.Net.Http;
+using System.ServiceModel.Channels;
+using System.Web.Http.Common;
+using System.Web.Http.SelfHost.Properties;
+using System.Xml;
+
+namespace System.Web.Http.SelfHost.Channels
+{
+ internal sealed class HttpMessage : Message
+ {
+ private HttpRequestMessage _request;
+ private HttpResponseMessage _response;
+ private MessageHeaders _headers;
+ private MessageProperties _properties;
+
+ public HttpMessage(HttpRequestMessage request)
+ {
+ Contract.Assert(request != null, "The 'request' parameter should not be null.");
+ _request = request;
+ Headers.To = request.RequestUri;
+ IsRequest = true;
+ }
+
+ public HttpMessage(HttpResponseMessage response)
+ {
+ Contract.Assert(response != null, "The 'response' parameter should not be null.");
+ _response = response;
+ IsRequest = false;
+ }
+
+ public override MessageVersion Version
+ {
+ get
+ {
+ EnsureNotDisposed();
+ return MessageVersion.None;
+ }
+ }
+
+ public override MessageHeaders Headers
+ {
+ get
+ {
+ EnsureNotDisposed();
+ if (_headers == null)
+ {
+ _headers = new MessageHeaders(MessageVersion.None);
+ }
+
+ return _headers;
+ }
+ }
+
+ public override MessageProperties Properties
+ {
+ get
+ {
+ EnsureNotDisposed();
+ if (_properties == null)
+ {
+ _properties = new MessageProperties();
+ _properties.AllowOutputBatching = false;
+ }
+
+ return _properties;
+ }
+ }
+
+ public override bool IsEmpty
+ {
+ get
+ {
+ long? contentLength = GetHttpContentLength();
+ return contentLength.HasValue && contentLength.Value == 0;
+ }
+ }
+
+ public override bool IsFault
+ {
+ get { return false; }
+ }
+
+ public bool IsRequest { get; private set; }
+
+ public HttpRequestMessage GetHttpRequestMessage(bool extract)
+ {
+ EnsureNotDisposed();
+ Contract.Assert(IsRequest, "This method should only be called when IsRequest is true.");
+ if (extract)
+ {
+ HttpRequestMessage req = _request;
+ _request = null;
+ return req;
+ }
+
+ return _request;
+ }
+
+ public HttpResponseMessage GetHttpResponseMessage(bool extract)
+ {
+ EnsureNotDisposed();
+ Contract.Assert(!IsRequest, "This method should only be called when IsRequest is false.");
+ if (extract)
+ {
+ HttpResponseMessage res = _response;
+ _response = null;
+ return res;
+ }
+
+ return _response;
+ }
+
+ protected override void OnWriteBodyContents(XmlDictionaryWriter writer)
+ {
+ throw Error.NotSupported(GetNotSupportedMessage());
+ }
+
+ protected override MessageBuffer OnCreateBufferedCopy(int maxBufferSize)
+ {
+ throw Error.NotSupported(GetNotSupportedMessage());
+ }
+
+ protected override XmlDictionaryReader OnGetReaderAtBodyContents()
+ {
+ throw Error.NotSupported(GetNotSupportedMessage());
+ }
+
+ protected override string OnGetBodyAttribute(string localName, string ns)
+ {
+ throw Error.NotSupported(GetNotSupportedMessage());
+ }
+
+ protected override void OnWriteStartBody(XmlDictionaryWriter writer)
+ {
+ throw Error.NotSupported(GetNotSupportedMessage());
+ }
+
+ protected override void OnWriteStartEnvelope(XmlDictionaryWriter writer)
+ {
+ throw Error.NotSupported(GetNotSupportedMessage());
+ }
+
+ protected override void OnBodyToString(XmlDictionaryWriter writer)
+ {
+ if (writer == null)
+ {
+ throw Error.ArgumentNull("writer");
+ }
+
+ long? contentLength = GetHttpContentLength();
+ string contentString = null;
+
+ if (IsRequest)
+ {
+ contentString = contentLength.HasValue
+ ? Error.Format(SRResources.MessageBodyIsHttpRequestMessageWithKnownContentLength, contentLength.Value)
+ : SRResources.MessageBodyIsHttpRequestMessageWithUnknownContentLength;
+ }
+ else
+ {
+ contentString = contentLength.HasValue
+ ? Error.Format(SRResources.MessageBodyIsHttpResponseMessageWithKnownContentLength, contentLength.Value)
+ : SRResources.MessageBodyIsHttpResponseMessageWithUnknownContentLength;
+ }
+
+ writer.WriteString(contentString);
+ }
+
+ protected override void OnWriteMessage(XmlDictionaryWriter writer)
+ {
+ throw Error.NotSupported(GetNotSupportedMessage());
+ }
+
+ protected override void OnWriteStartHeaders(XmlDictionaryWriter writer)
+ {
+ throw Error.NotSupported(GetNotSupportedMessage());
+ }
+
+ protected override void OnClose()
+ {
+ base.OnClose();
+ if (_request != null)
+ {
+ _request.DisposeRequestResources();
+ _request.Dispose();
+ _request = null;
+ }
+
+ if (_response != null)
+ {
+ _response.Dispose();
+ _response = null;
+ }
+ }
+
+ private static string GetNotSupportedMessage()
+ {
+ return Error.Format(
+ SRResources.MessageReadWriteCopyNotSupported,
+ HttpMessageExtensions.ToHttpRequestMessageMethodName,
+ HttpMessageExtensions.ToHttpResponseMessageMethodName,
+ typeof(HttpMessage).Name);
+ }
+
+ private void EnsureNotDisposed()
+ {
+ if (IsDisposed)
+ {
+ throw Error.ObjectDisposed(SRResources.MessageClosed, typeof(Message).Name);
+ }
+ }
+
+ private long? GetHttpContentLength()
+ {
+ HttpContent content = IsRequest
+ ? GetHttpRequestMessage(false).Content
+ : GetHttpResponseMessage(false).Content;
+
+ if (content == null)
+ {
+ return 0;
+ }
+
+ return content.Headers.ContentLength;
+ }
+ }
+}
diff --git a/src/System.Web.Http.SelfHost/Channels/HttpMessageEncoderFactory.cs b/src/System.Web.Http.SelfHost/Channels/HttpMessageEncoderFactory.cs
new file mode 100644
index 00000000..029a29be
--- /dev/null
+++ b/src/System.Web.Http.SelfHost/Channels/HttpMessageEncoderFactory.cs
@@ -0,0 +1,239 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+using System.IO;
+using System.Net.Http;
+using System.ServiceModel;
+using System.ServiceModel.Channels;
+using System.Web.Http.Common;
+using System.Web.Http.SelfHost.Properties;
+using System.Web.Http.SelfHost.ServiceModel.Channels;
+
+namespace System.Web.Http.SelfHost.Channels
+{
+ internal class HttpMessageEncoderFactory : MessageEncoderFactory
+ {
+ private HttpMessageEncoder _encoder;
+
+ public HttpMessageEncoderFactory()
+ {
+ _encoder = new HttpMessageEncoder();
+ }
+
+ public override MessageEncoder Encoder
+ {
+ get { return _encoder; }
+ }
+
+ public override MessageVersion MessageVersion
+ {
+ get { return MessageVersion.None; }
+ }
+
+ public override MessageEncoder CreateSessionEncoder()
+ {
+ throw Error.NotSupported(SRResources.HttpMessageEncoderFactoryDoesNotSupportSessionEncoder, typeof(HttpMessageEncoderFactory).Name);
+ }
+
+ private class HttpMessageEncoder : MessageEncoder
+ {
+ private const string ContentTypeHeaderName = "Content-Type";
+ private const string MaxSentMessageSizeExceededResourceStringName = "MaxSentMessageSizeExceeded";
+ private static readonly string _httpBindingClassName = typeof(HttpBinding).FullName;
+ private static readonly string _httpResponseMessageClassName = typeof(HttpResponseMessage).FullName;
+
+ public override string ContentType
+ {
+ get { return String.Empty; }
+ }
+
+ public override string MediaType
+ {
+ get { return String.Empty; }
+ }
+
+ public override MessageVersion MessageVersion
+ {
+ get { return MessageVersion.None; }
+ }
+
+ public override bool IsContentTypeSupported(string contentType)
+ {
+ if (contentType == null)
+ {
+ throw Error.ArgumentNull("contentType");
+ }
+
+ return true;
+ }
+
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "disposed later.")]
+ public override Message ReadMessage(ArraySegment<byte> buffer, BufferManager bufferManager, string contentType)
+ {
+ if (bufferManager == null)
+ {
+ throw Error.ArgumentNull("bufferManager");
+ }
+
+ HttpRequestMessage request = new HttpRequestMessage();
+ request.Content = new ByteArrayBufferManagerContent(bufferManager, buffer.Array, buffer.Offset, buffer.Count);
+ if (!String.IsNullOrEmpty(contentType))
+ {
+ request.Content.Headers.Add(ContentTypeHeaderName, contentType);
+ }
+
+ Message message = request.ToMessage();
+ message.Properties.Encoder = this;
+
+ return message;
+ }
+
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "disposed later.")]
+ public override Message ReadMessage(Stream stream, int maxSizeOfHeaders, string contentType)
+ {
+ if (stream == null)
+ {
+ throw Error.ArgumentNull("stream");
+ }
+
+ HttpRequestMessage request = new HttpRequestMessage();
+ request.Content = new StreamContent(stream);
+ if (!String.IsNullOrEmpty(contentType))
+ {
+ request.Content.Headers.Add(ContentTypeHeaderName, contentType);
+ }
+
+ Message message = request.ToMessage();
+ message.Properties.Encoder = this;
+
+ return message;
+ }
+
+ public override ArraySegment<byte> WriteMessage(Message message, int maxMessageSize, BufferManager bufferManager, int messageOffset)
+ {
+ if (message == null)
+ {
+ throw Error.ArgumentNull("message");
+ }
+
+ if (bufferManager == null)
+ {
+ throw Error.ArgumentNull("bufferManager");
+ }
+
+ if (maxMessageSize < 0)
+ {
+ throw Error.ArgumentOutOfRange("maxMessageSize", maxMessageSize, SRResources.NonnegativeNumberRequired);
+ }
+
+ if (messageOffset < 0)
+ {
+ throw Error.ArgumentOutOfRange("messageOffset", messageOffset, SRResources.NonnegativeNumberRequired);
+ }
+
+ if (messageOffset > maxMessageSize)
+ {
+ throw Error.Argument(String.Empty, SRResources.ParameterMustBeLessThanOrEqualSecondParameter, "messageOffset", "maxMessageSize");
+ }
+
+ using (BufferManagerOutputStream stream = new BufferManagerOutputStream(MaxSentMessageSizeExceededResourceStringName, 0, maxMessageSize, bufferManager))
+ {
+ int num;
+ stream.Skip(messageOffset);
+ WriteMessage(message, stream);
+ ArraySegment<byte> messageData = new ArraySegment<byte>(stream.ToArray(out num), 0, num - messageOffset);
+
+ return messageData;
+ }
+ }
+
+ [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "The WriteMessage() API is synchronous, and Wait() won't deadlock in self-host.")]
+ public override void WriteMessage(Message message, Stream stream)
+ {
+ if (message == null)
+ {
+ throw Error.ArgumentNull("message");
+ }
+
+ if (stream == null)
+ {
+ throw Error.ArgumentNull("stream");
+ }
+
+ ThrowIfMismatchedMessageVersion(message);
+
+ message.Properties.Encoder = this;
+
+ HttpResponseMessage response = GetHttpResponseMessageOrThrow(message);
+
+ if (response.Content != null)
+ {
+ response.Content.CopyToAsync(stream).Wait();
+ }
+ }
+
+ internal void ThrowIfMismatchedMessageVersion(Message message)
+ {
+ if (message.Version != MessageVersion)
+ {
+ throw new ProtocolException(Error.Format(SRResources.EncoderMessageVersionMismatch, message.Version, MessageVersion));
+ }
+ }
+
+ private static HttpResponseMessage GetHttpResponseMessageOrThrow(Message message)
+ {
+ HttpResponseMessage response = message.ToHttpResponseMessage();
+ if (response == null)
+ {
+ throw Error.InvalidOperation(
+ SRResources.MessageInvalidForHttpMessageEncoder,
+ _httpBindingClassName,
+ HttpMessageExtensions.ToMessageMethodName,
+ _httpResponseMessageClassName);
+ }
+
+ return response;
+ }
+
+ private class ByteArrayBufferManagerContent : ByteArrayContent
+ {
+ private bool _disposed;
+ private BufferManager _bufferManager;
+ private byte[] _content;
+ private object _disposingLock;
+
+ public ByteArrayBufferManagerContent(BufferManager bufferManager, byte[] content, int offset, int count)
+ : base(content, offset, count)
+ {
+ Contract.Assert(bufferManager != null, "The 'bufferManager' parameter should never be null.");
+
+ _bufferManager = bufferManager;
+ _content = content;
+ _disposingLock = new object();
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ try
+ {
+ if (disposing && !_disposed)
+ {
+ lock (_disposingLock)
+ {
+ if (!_disposed)
+ {
+ _disposed = true;
+ _bufferManager.ReturnBuffer(_content);
+ _content = null;
+ }
+ }
+ }
+ }
+ finally
+ {
+ base.Dispose(disposing);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http.SelfHost/Channels/HttpMessageEncodingBindingElement.cs b/src/System.Web.Http.SelfHost/Channels/HttpMessageEncodingBindingElement.cs
new file mode 100644
index 00000000..6c6d8e5b
--- /dev/null
+++ b/src/System.Web.Http.SelfHost/Channels/HttpMessageEncodingBindingElement.cs
@@ -0,0 +1,139 @@
+using System.Diagnostics.CodeAnalysis;
+using System.ServiceModel.Channels;
+using System.Web.Http.Common;
+using System.Web.Http.SelfHost.Properties;
+
+namespace System.Web.Http.SelfHost.Channels
+{
+ /// <summary>
+ /// Provides an <see cref="HttpMessageEncoderFactory"/> that returns a <see cref="MessageEncoder"/>
+ /// that is able to produce and consume <see cref="HttpMessage"/> instances.
+ /// </summary>
+ internal sealed class HttpMessageEncodingBindingElement : MessageEncodingBindingElement
+ {
+ private static readonly Type _replyChannelType = typeof(IReplyChannel);
+
+ /// <summary>
+ /// Gets or sets the message version that can be handled by the message encoders produced by the message encoder factory.
+ /// </summary>
+ /// <returns>The <see cref="MessageVersion"/> used by the encoders produced by the message encoder factory.</returns>
+ public override MessageVersion MessageVersion
+ {
+ get { return MessageVersion.None; }
+
+ set
+ {
+ if (value == null)
+ {
+ throw Error.ArgumentNull("value");
+ }
+
+ if (value != MessageVersion.None)
+ {
+ throw Error.NotSupported(SRResources.OnlyMessageVersionNoneSupportedOnHttpMessageEncodingBindingElement, typeof(HttpMessageEncodingBindingElement).Name);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Returns a value that indicates whether the binding element can build a listener for a specific type of channel.
+ /// </summary>
+ /// <typeparam name="TChannel">The type of channel the listener accepts.</typeparam>
+ /// <param name="context">The <see cref="BindingContext"/> that provides context for the binding element</param>
+ /// <returns>true if the <see cref="IChannelListener{TChannel}"/> of type <see cref="IChannel"/> can be built by the binding element; otherwise, false.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "Existing public API")]
+ public override bool CanBuildChannelFactory<TChannel>(BindingContext context)
+ {
+ return false;
+ }
+
+ /// <summary>
+ /// Returns a value that indicates whether the binding element can build a channel factory for a specific type of channel.
+ /// </summary>
+ /// <typeparam name="TChannel">The type of channel the channel factory produces.</typeparam>
+ /// <param name="context">The <see cref="BindingContext"/> that provides context for the binding element</param>
+ /// <returns>ALways false.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "Existing public API")]
+ public override IChannelFactory<TChannel> BuildChannelFactory<TChannel>(BindingContext context)
+ {
+ throw Error.NotSupported(SRResources.ChannelFactoryNotSupported, typeof(HttpMessageEncodingBindingElement).Name, typeof(IChannelFactory).Name);
+ }
+
+ /// <summary>
+ /// Returns a value that indicates whether the binding element can build a channel factory for a specific type of channel.
+ /// </summary>
+ /// <typeparam name="TChannel">The type of channel the channel factory produces.</typeparam>
+ /// <param name="context">The <see cref="BindingContext"/> that provides context for the binding element</param>
+ /// <returns>ALways false.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "Existing public API")]
+ public override bool CanBuildChannelListener<TChannel>(BindingContext context)
+ {
+ if (context == null)
+ {
+ throw Error.ArgumentNull("context");
+ }
+
+ context.BindingParameters.Add(this);
+
+ return IsChannelShapeSupported<TChannel>() && context.CanBuildInnerChannelListener<TChannel>();
+ }
+
+ /// <summary>
+ /// Initializes a channel listener to accept channels of a specified type from the binding context.
+ /// </summary>
+ /// <typeparam name="TChannel">The type of channel the listener is built to accept.</typeparam>
+ /// <param name="context">The <see cref="BindingContext"/> that provides context for the binding element</param>
+ /// <returns>The <see cref="IChannelListener{TChannel}"/> of type <see cref="IChannel"/> initialized from the context.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "Existing public API")]
+ public override IChannelListener<TChannel> BuildChannelListener<TChannel>(BindingContext context)
+ {
+ if (context == null)
+ {
+ throw Error.ArgumentNull("context");
+ }
+
+ if (!IsChannelShapeSupported<TChannel>())
+ {
+ throw Error.NotSupported(SRResources.ChannelShapeNotSupported, typeof(HttpMessageEncodingBindingElement).Name, typeof(IReplyChannel).Name);
+ }
+
+ context.BindingParameters.Add(this);
+
+ IChannelListener<IReplyChannel> innerListener = context.BuildInnerChannelListener<IReplyChannel>();
+
+ if (innerListener == null)
+ {
+ return null;
+ }
+
+ return (IChannelListener<TChannel>)new HttpMessageEncodingChannelListener(context.Binding, innerListener);
+ }
+
+ /// <summary>
+ /// Returns a copy of the binding element object.
+ /// </summary>
+ /// <returns>A <see cref="BindingElement"/> object that is a deep clone of the original.</returns>
+ public override BindingElement Clone()
+ {
+ return new HttpMessageEncodingBindingElement();
+ }
+
+ /// <summary>
+ /// Creates a factory for producing message encoders that are able to
+ /// produce and consume <see cref="HttpMessage"/> instances.
+ /// </summary>
+ /// <returns>
+ /// The <see cref="MessageEncoderFactory"/> used to produce message encoders that are able to
+ /// produce and consume <see cref="HttpMessage"/> instances.
+ /// </returns>
+ public override MessageEncoderFactory CreateMessageEncoderFactory()
+ {
+ return new HttpMessageEncoderFactory();
+ }
+
+ private static bool IsChannelShapeSupported<TChannel>()
+ {
+ return typeof(TChannel) == _replyChannelType;
+ }
+ }
+}
diff --git a/src/System.Web.Http.SelfHost/Channels/HttpMessageEncodingChannelListener.cs b/src/System.Web.Http.SelfHost/Channels/HttpMessageEncodingChannelListener.cs
new file mode 100644
index 00000000..8bc2546a
--- /dev/null
+++ b/src/System.Web.Http.SelfHost/Channels/HttpMessageEncodingChannelListener.cs
@@ -0,0 +1,60 @@
+using System.ServiceModel.Channels;
+using System.Web.Http.SelfHost.ServiceModel.Channels;
+
+namespace System.Web.Http.SelfHost.Channels
+{
+ internal class HttpMessageEncodingChannelListener : LayeredChannelListener<IReplyChannel>
+ {
+ private IChannelListener<IReplyChannel> _innerChannelListener;
+
+ public HttpMessageEncodingChannelListener(Binding binding, IChannelListener<IReplyChannel> innerListener) :
+ base(binding, innerListener)
+ {
+ }
+
+ protected override void OnOpening()
+ {
+ _innerChannelListener = (IChannelListener<IReplyChannel>)InnerChannelListener;
+ base.OnOpening();
+ }
+
+ protected override IReplyChannel OnAcceptChannel(TimeSpan timeout)
+ {
+ IReplyChannel innerChannel = _innerChannelListener.AcceptChannel(timeout);
+ return WrapInnerChannel(innerChannel);
+ }
+
+ protected override IAsyncResult OnBeginAcceptChannel(TimeSpan timeout, AsyncCallback callback, object state)
+ {
+ return _innerChannelListener.BeginAcceptChannel(timeout, callback, state);
+ }
+
+ protected override IReplyChannel OnEndAcceptChannel(IAsyncResult result)
+ {
+ IReplyChannel innerChannel = _innerChannelListener.EndAcceptChannel(result);
+ return WrapInnerChannel(innerChannel);
+ }
+
+ protected override IAsyncResult OnBeginWaitForChannel(TimeSpan timeout, AsyncCallback callback, object state)
+ {
+ return _innerChannelListener.BeginWaitForChannel(timeout, callback, state);
+ }
+
+ protected override bool OnEndWaitForChannel(IAsyncResult result)
+ {
+ return _innerChannelListener.EndWaitForChannel(result);
+ }
+
+ protected override bool OnWaitForChannel(TimeSpan timeout)
+ {
+ return _innerChannelListener.WaitForChannel(timeout);
+ }
+
+ private IReplyChannel WrapInnerChannel(IReplyChannel innerChannel)
+ {
+ return (innerChannel != null)
+ ? new HttpMessageEncodingReplyChannel(this, innerChannel)
+ : (IReplyChannel)null;
+ }
+ }
+}
diff --git a/src/System.Web.Http.SelfHost/Channels/HttpMessageEncodingReplyChannel.cs b/src/System.Web.Http.SelfHost/Channels/HttpMessageEncodingReplyChannel.cs
new file mode 100644
index 00000000..5d1d0e1f
--- /dev/null
+++ b/src/System.Web.Http.SelfHost/Channels/HttpMessageEncodingReplyChannel.cs
@@ -0,0 +1,102 @@
+using System.Diagnostics.CodeAnalysis;
+using System.ServiceModel;
+using System.ServiceModel.Channels;
+using System.Web.Http.SelfHost.ServiceModel.Channels;
+
+namespace System.Web.Http.SelfHost.Channels
+{
+ internal class HttpMessageEncodingReplyChannel : LayeredChannel<IReplyChannel>, IReplyChannel, IChannel, ICommunicationObject
+ {
+ public HttpMessageEncodingReplyChannel(ChannelManagerBase channelManager, IReplyChannel innerChannel)
+ : base(channelManager, innerChannel)
+ {
+ }
+
+ public EndpointAddress LocalAddress
+ {
+ get { return InnerChannel.LocalAddress; }
+ }
+
+ public IAsyncResult BeginReceiveRequest(AsyncCallback callback, object state)
+ {
+ return InnerChannel.BeginReceiveRequest(callback, state);
+ }
+
+ public IAsyncResult BeginReceiveRequest(TimeSpan timeout, AsyncCallback callback, object state)
+ {
+ return InnerChannel.BeginReceiveRequest(timeout, callback, state);
+ }
+
+ public IAsyncResult BeginTryReceiveRequest(TimeSpan timeout, AsyncCallback callback, object state)
+ {
+ return InnerChannel.BeginTryReceiveRequest(timeout, callback, state);
+ }
+
+ public IAsyncResult BeginWaitForRequest(TimeSpan timeout, AsyncCallback callback, object state)
+ {
+ return InnerChannel.BeginWaitForRequest(timeout, callback, state);
+ }
+
+ public RequestContext EndReceiveRequest(IAsyncResult result)
+ {
+ RequestContext innerContext = InnerChannel.EndReceiveRequest(result);
+ return WrapRequestContext(innerContext);
+ }
+
+ public bool EndTryReceiveRequest(IAsyncResult result, out RequestContext context)
+ {
+ RequestContext innerContext;
+ context = null;
+ if (!InnerChannel.EndTryReceiveRequest(result, out innerContext))
+ {
+ return false;
+ }
+
+ context = WrapRequestContext(innerContext);
+ return true;
+ }
+
+ public bool EndWaitForRequest(IAsyncResult result)
+ {
+ return InnerChannel.EndWaitForRequest(result);
+ }
+
+ public RequestContext ReceiveRequest()
+ {
+ RequestContext innerContext = InnerChannel.ReceiveRequest();
+ return WrapRequestContext(innerContext);
+ }
+
+ public RequestContext ReceiveRequest(TimeSpan timeout)
+ {
+ RequestContext innerContext = InnerChannel.ReceiveRequest(timeout);
+ return WrapRequestContext(innerContext);
+ }
+
+ public bool TryReceiveRequest(TimeSpan timeout, out RequestContext context)
+ {
+ RequestContext innerContext;
+ if (InnerChannel.TryReceiveRequest(timeout, out innerContext))
+ {
+ context = WrapRequestContext(innerContext);
+ return true;
+ }
+
+ context = null;
+ return false;
+ }
+
+ public bool WaitForRequest(TimeSpan timeout)
+ {
+ return InnerChannel.WaitForRequest(timeout);
+ }
+
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "disposed later.")]
+ private static RequestContext WrapRequestContext(RequestContext innerContext)
+ {
+ return (innerContext != null)
+ ? new HttpMessageEncodingRequestContext(innerContext)
+ : (RequestContext)null;
+ }
+ }
+}
diff --git a/src/System.Web.Http.SelfHost/Channels/HttpMessageEncodingRequestContext.cs b/src/System.Web.Http.SelfHost/Channels/HttpMessageEncodingRequestContext.cs
new file mode 100644
index 00000000..4c72d1c5
--- /dev/null
+++ b/src/System.Web.Http.SelfHost/Channels/HttpMessageEncodingRequestContext.cs
@@ -0,0 +1,289 @@
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.ServiceModel.Channels;
+using System.Web.Http.Common;
+using System.Web.Http.SelfHost.Properties;
+
+namespace System.Web.Http.SelfHost.Channels
+{
+ internal class HttpMessageEncodingRequestContext : RequestContext
+ {
+ private const string ContentLengthHeader = "Content-Length";
+ private const string DefaultReasonPhrase = "OK";
+
+ // TODO: Remove this list of content-type headers once the NCL team publicly exposes
+ // this list. Opened bug #50459 in DevDiv2 TFS on the NCL team.
+ // Opened #189321 in CSDmain to track [randallt]
+ private static readonly HashSet<string> _httpContentHeaders = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+ {
+ "Allow", "Content-Encoding", "Content-Language", "Content-Location", "Content-MD5",
+ "Content-Range", "Expires", "Last-Modified", "Content-Type", ContentLengthHeader
+ };
+
+ private RequestContext _innerContext;
+ private Message _configuredRequestMessage;
+ private bool _isRequestConfigured;
+ private object _requestConfigurationLock;
+
+ public HttpMessageEncodingRequestContext(RequestContext innerContext)
+ {
+ Contract.Assert(innerContext != null, "The 'innerContext' parameter should not be null.");
+ _innerContext = innerContext;
+ _requestConfigurationLock = new object();
+ }
+
+ public override Message RequestMessage
+ {
+ get
+ {
+ if (!_isRequestConfigured)
+ {
+ lock (_requestConfigurationLock)
+ {
+ if (!_isRequestConfigured)
+ {
+ Message innerMessage = _innerContext.RequestMessage;
+ _configuredRequestMessage = ConfigureRequestMessage(innerMessage);
+ _isRequestConfigured = true;
+ }
+ }
+ }
+
+ return _configuredRequestMessage;
+ }
+ }
+
+ public override void Abort()
+ {
+ Cleanup();
+ _innerContext.Abort();
+ }
+
+ public override IAsyncResult BeginReply(Message message, TimeSpan timeout, AsyncCallback callback, object state)
+ {
+ message = ConfigureResponseMessage(message);
+ return _innerContext.BeginReply(message, timeout, callback, state);
+ }
+
+ public override IAsyncResult BeginReply(Message message, AsyncCallback callback, object state)
+ {
+ message = ConfigureResponseMessage(message);
+ return _innerContext.BeginReply(message, callback, state);
+ }
+
+ public override void Close(TimeSpan timeout)
+ {
+ Cleanup();
+ _innerContext.Close(timeout);
+ }
+
+ public override void Close()
+ {
+ Cleanup();
+ _innerContext.Close();
+ }
+
+ public override void EndReply(IAsyncResult result)
+ {
+ _innerContext.EndReply(result);
+ }
+
+ public override void Reply(Message message, TimeSpan timeout)
+ {
+ message = ConfigureResponseMessage(message);
+ _innerContext.Reply(message, timeout);
+ }
+
+ public override void Reply(Message message)
+ {
+ ConfigureResponseMessage(message);
+ _innerContext.Reply(message);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Cleanup exceptions are ignored as they are not fatal to the host.")]
+ private void Cleanup()
+ {
+ if (_configuredRequestMessage != null)
+ {
+ try
+ {
+ _configuredRequestMessage.Close();
+ }
+ catch
+ {
+ }
+ }
+ }
+
+ private static void AddHeaderToHttpRequestMessageAndHandleExceptions(HttpRequestMessage httpRequestMessage, string headerName, string headerValue)
+ {
+ try
+ {
+ AddHeaderToHttpRequestMessage(httpRequestMessage, headerName, headerValue);
+ }
+ catch (FormatException)
+ {
+ }
+ catch (InvalidOperationException)
+ {
+ }
+ }
+
+ private static void AddHeaderToHttpRequestMessage(HttpRequestMessage httpRequestMessage, string headerName, string headerValue)
+ {
+ if (_httpContentHeaders.Contains(headerName))
+ {
+ // Only set the content-length header if it is not already set
+ if (String.Equals(headerName, ContentLengthHeader, StringComparison.Ordinal) &&
+ httpRequestMessage.Content.Headers.ContentLength != null)
+ {
+ return;
+ }
+
+ httpRequestMessage.Content.Headers.Add(headerName, headerValue);
+ }
+ else
+ {
+ httpRequestMessage.Headers.Add(headerName, headerValue);
+ }
+ }
+
+ private static void CopyHeadersToNameValueCollection(HttpHeaders headers, NameValueCollection nameValueCollection)
+ {
+ foreach (KeyValuePair<string, IEnumerable<string>> header in headers)
+ {
+ foreach (string value in header.Value)
+ {
+ nameValueCollection.Add(header.Key, value);
+ }
+ }
+ }
+
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "disposed later.")]
+ private static Message ConfigureRequestMessage(Message message)
+ {
+ if (message == null)
+ {
+ return null;
+ }
+
+ HttpRequestMessageProperty requestProperty;
+ if (!message.Properties.TryGetValue(HttpRequestMessageProperty.Name, out requestProperty))
+ {
+ throw Error.InvalidOperation(
+ SRResources.RequestMissingHttpRequestMessageProperty,
+ HttpRequestMessageProperty.Name,
+ typeof(HttpRequestMessageProperty).Name);
+ }
+
+ Uri uri = message.Headers.To;
+ if (uri == null)
+ {
+ throw Error.InvalidOperation(SRResources.RequestMissingToHeader);
+ }
+
+ HttpRequestMessage httpRequestMessage = message.ToHttpRequestMessage();
+ if (httpRequestMessage == null)
+ {
+ if (!message.IsEmpty)
+ {
+ throw Error.InvalidOperation(SRResources.NonHttpMessageMustBeEmpty, HttpMessageExtensions.ToHttpRequestMessageMethodName, typeof(HttpMessage).Name);
+ }
+
+ httpRequestMessage = new HttpRequestMessage();
+ Message oldMessage = message;
+ message = httpRequestMessage.ToMessage();
+ message.Properties.CopyProperties(oldMessage.Properties);
+ oldMessage.Close();
+ }
+ else
+ {
+ // Clear headers but not properties.
+ message.Headers.Clear();
+ }
+
+ // Copy message properties to HttpRequestMessage. While it does have the
+ // risk of allowing properties to get out of sync they in virtually all cases are
+ // read-only so the risk is low. The downside to not doing it is that it isn't
+ // possible to access anything from HttpRequestMessage (or OperationContent.Current)
+ // which is worse.
+ foreach (KeyValuePair<string, object> kv in message.Properties)
+ {
+ httpRequestMessage.Properties.Add(kv.Key, kv.Value);
+ }
+
+ if (httpRequestMessage.Content == null)
+ {
+ httpRequestMessage.Content = new ByteArrayContent(new byte[0]);
+ }
+ else
+ {
+ httpRequestMessage.Content.Headers.Clear();
+ }
+
+ message.Headers.To = uri;
+
+ httpRequestMessage.RequestUri = uri;
+ httpRequestMessage.Method = HttpMethodHelper.GetHttpMethod(requestProperty.Method);
+
+ foreach (var headerName in requestProperty.Headers.AllKeys)
+ {
+ AddHeaderToHttpRequestMessageAndHandleExceptions(
+ httpRequestMessage,
+ headerName,
+ requestProperty.Headers[headerName]);
+ }
+
+ return message;
+ }
+
+ private static Message ConfigureResponseMessage(Message message)
+ {
+ if (message == null)
+ {
+ return null;
+ }
+
+ HttpResponseMessageProperty responseProperty = new HttpResponseMessageProperty();
+
+ HttpResponseMessage httpResponseMessage = message.ToHttpResponseMessage();
+ if (httpResponseMessage == null)
+ {
+ responseProperty.StatusCode = HttpStatusCode.InternalServerError;
+ responseProperty.SuppressEntityBody = true;
+ }
+ else
+ {
+ responseProperty.StatusCode = httpResponseMessage.StatusCode;
+ if (httpResponseMessage.ReasonPhrase != null &&
+ httpResponseMessage.ReasonPhrase != DefaultReasonPhrase)
+ {
+ responseProperty.StatusDescription = httpResponseMessage.ReasonPhrase;
+ }
+
+ CopyHeadersToNameValueCollection(httpResponseMessage.Headers, responseProperty.Headers);
+ HttpContent content = httpResponseMessage.Content;
+ if (content != null)
+ {
+ CopyHeadersToNameValueCollection(httpResponseMessage.Content.Headers, responseProperty.Headers);
+ }
+ else
+ {
+ responseProperty.SuppressEntityBody = true;
+ }
+ }
+
+ message.Properties.Clear();
+ message.Headers.Clear();
+
+ message.Properties.Add(HttpResponseMessageProperty.Name, responseProperty);
+
+ return message;
+ }
+ }
+}
diff --git a/src/System.Web.Http.SelfHost/Channels/HttpMessageExtensions.cs b/src/System.Web.Http.SelfHost/Channels/HttpMessageExtensions.cs
new file mode 100644
index 00000000..e68aee60
--- /dev/null
+++ b/src/System.Web.Http.SelfHost/Channels/HttpMessageExtensions.cs
@@ -0,0 +1,188 @@
+using System.Net.Http;
+using System.ServiceModel.Channels;
+using System.Web.Http.Common;
+
+namespace System.Web.Http.SelfHost.Channels
+{
+ /// <summary>
+ /// Provides extension methods for getting an <see cref="HttpRequestMessage"/> instance or
+ /// an <see cref="HttpResponseMessage"/> instance from a <see cref="Message"/> instance and
+ /// provides extension methods for creating a <see cref="Message"/> instance from either an
+ /// <see cref="HttpRequestMessage"/> instance or an
+ /// <see cref="HttpResponseMessage"/> instance.
+ /// </summary>
+ internal static class HttpMessageExtensions
+ {
+ internal const string ToHttpRequestMessageMethodName = "ToHttpRequestMessage";
+ internal const string ToHttpResponseMessageMethodName = "ToHttpResponseMessage";
+ internal const string ToMessageMethodName = "ToMessage";
+
+ /// <summary>
+ /// Returns a reference to the <see cref="HttpRequestMessage"/>
+ /// instance held by the given <see cref="Message"/> or null if the <see cref="Message"/> does not
+ /// hold a reference to an <see cref="HttpRequestMessage"/>
+ /// instance.
+ /// </summary>
+ /// <param name="message">The given <see cref="Message"/> that holds a reference to an
+ /// <see cref="HttpRequestMessage"/> instance.
+ /// </param>
+ /// <returns>
+ /// A reference to the <see cref="HttpRequestMessage"/>
+ /// instance held by the given <see cref="Message"/> or null if the <see cref="Message"/> does not
+ /// hold a reference to an <see cref="HttpRequestMessage"/>
+ /// instance.
+ /// </returns>
+ public static HttpRequestMessage ToHttpRequestMessage(this Message message)
+ {
+ if (message == null)
+ {
+ throw Error.ArgumentNull("message");
+ }
+
+ HttpMessage httpMessage = message as HttpMessage;
+ if (httpMessage != null && httpMessage.IsRequest)
+ {
+ return httpMessage.GetHttpRequestMessage(false);
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Returns a reference to the <see cref="HttpRequestMessage"/>
+ /// instance held by the given <see cref="Message"/> or null if the <see cref="Message"/> does not
+ /// hold a reference to an <see cref="HttpRequestMessage"/>
+ /// instance.
+ /// </summary>
+ /// <remarks>The caller takes over the ownership of the associated <see cref="HttpRequestMessage"/> and is responsible for its disposal.</remarks>
+ /// <param name="message">The given <see cref="Message"/> that holds a reference to an
+ /// <see cref="HttpRequestMessage"/> instance.
+ /// </param>
+ /// <returns>
+ /// A reference to the <see cref="HttpRequestMessage"/>
+ /// instance held by the given <see cref="Message"/> or null if the <see cref="Message"/> does not
+ /// hold a reference to an <see cref="HttpRequestMessage"/>
+ /// instance.
+ /// The caller is responsible for disposing any <see cref="HttpRequestMessage"/> instance returned.
+ /// </returns>
+ public static HttpRequestMessage ExtractHttpRequestMessage(this Message message)
+ {
+ if (message == null)
+ {
+ throw Error.ArgumentNull("message");
+ }
+
+ HttpMessage httpMessage = message as HttpMessage;
+ if (httpMessage != null && httpMessage.IsRequest)
+ {
+ return httpMessage.GetHttpRequestMessage(true);
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Returns a reference to the <see cref="HttpResponseMessage"/>
+ /// instance held by the given <see cref="Message"/> or null if the <see cref="Message"/> does not
+ /// hold a reference to an <see cref="HttpResponseMessage"/>
+ /// instance.
+ /// </summary>
+ /// <param name="message">The given <see cref="Message"/> that holds a reference to an
+ /// <see cref="HttpResponseMessage"/> instance.
+ /// </param>
+ /// <returns>
+ /// A reference to the <see cref="HttpResponseMessage"/>
+ /// instance held by the given <see cref="Message"/> or null if the <see cref="Message"/> does not
+ /// hold a reference to an <see cref="HttpResponseMessage"/>
+ /// instance.
+ /// </returns>
+ public static HttpResponseMessage ToHttpResponseMessage(this Message message)
+ {
+ if (message == null)
+ {
+ throw Error.ArgumentNull("message");
+ }
+
+ HttpMessage httpMessage = message as HttpMessage;
+ if (httpMessage != null && !httpMessage.IsRequest)
+ {
+ return httpMessage.GetHttpResponseMessage(false);
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Returns a reference to the <see cref="HttpResponseMessage"/>
+ /// instance held by the given <see cref="Message"/> or null if the <see cref="Message"/> does not
+ /// hold a reference to an <see cref="HttpResponseMessage"/>
+ /// instance.
+ /// </summary>
+ /// <remarks>The caller takes over the ownership of the associated <see cref="HttpRequestMessage"/> and is responsible for its disposal.</remarks>
+ /// <param name="message">The given <see cref="Message"/> that holds a reference to an
+ /// <see cref="HttpResponseMessage"/> instance.
+ /// </param>
+ /// <returns>
+ /// A reference to the <see cref="HttpResponseMessage"/>
+ /// instance held by the given <see cref="Message"/> or null if the <see cref="Message"/> does not
+ /// hold a reference to an <see cref="HttpResponseMessage"/>
+ /// instance.
+ /// The caller is responsible for disposing any <see cref="HttpResponseMessage"/> instance returned.
+ /// </returns>
+ public static HttpResponseMessage ExtractHttpResponseMessage(this Message message)
+ {
+ if (message == null)
+ {
+ throw Error.ArgumentNull("message");
+ }
+
+ HttpMessage httpMessage = message as HttpMessage;
+ if (httpMessage != null && !httpMessage.IsRequest)
+ {
+ return httpMessage.GetHttpResponseMessage(true);
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Creates a new <see cref="Message"/> that holds a reference to the given
+ /// <see cref="HttpRequestMessage"/> instance.
+ /// </summary>
+ /// <param name="request">The <see cref="HttpRequestMessage"/>
+ /// instance to which the new <see cref="Message"/> should hold a reference.
+ /// </param>
+ /// <returns>A <see cref="Message"/> that holds a reference to the given
+ /// <see cref="HttpRequestMessage"/> instance.
+ /// </returns>
+ public static Message ToMessage(this HttpRequestMessage request)
+ {
+ if (request == null)
+ {
+ throw Error.ArgumentNull("request");
+ }
+
+ return new HttpMessage(request);
+ }
+
+ /// <summary>
+ /// Creates a new <see cref="Message"/> that holds a reference to the given
+ /// <see cref="HttpResponseMessage"/> instance.
+ /// </summary>
+ /// <param name="response">The <see cref="HttpResponseMessage"/>
+ /// instance to which the new <see cref="Message"/> should hold a reference.
+ /// </param>
+ /// <returns>A <see cref="Message"/> that holds a reference to the given
+ /// <see cref="HttpResponseMessage"/> instance.
+ /// </returns>
+ public static Message ToMessage(this HttpResponseMessage response)
+ {
+ if (response == null)
+ {
+ throw Error.ArgumentNull("response");
+ }
+
+ return new HttpMessage(response);
+ }
+ }
+}
diff --git a/src/System.Web.Http.SelfHost/GlobalSuppressions.cs b/src/System.Web.Http.SelfHost/GlobalSuppressions.cs
new file mode 100644
index 00000000..56a5a765
--- /dev/null
+++ b/src/System.Web.Http.SelfHost/GlobalSuppressions.cs
@@ -0,0 +1,8 @@
+using System.Diagnostics.CodeAnalysis;
+
+[assembly: SuppressMessage("Microsoft.Design", "CA2210:AssembliesShouldHaveValidStrongNames", Justification = "These assemblies are delay-signed.")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Justification = "Classes are grouped logically for user clarity.", Scope = "Namespace", Target = "System.Web.Http.SelfHost")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Justification = "Classes are grouped logically for user clarity.", Scope = "Namespace", Target = "System.Web.Http.SelfHost.Activation")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Justification = "Classes are grouped logically for user clarity.", Scope = "Namespace", Target = "System.Web.Http.SelfHost.Channels")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1701:ResourceStringCompoundWordsShouldBeCasedCorrectly", MessageId = "URIs", Scope = "resource", Target = "System.Web.Http.SelfHost.Properties.SRResources.resources", Justification = "FxCop does not seem to allow adding an exception to this term in its dictionary.")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId = "URIs", Scope = "resource", Target = "System.Web.Http.SelfHost.Properties.SRResources.resources", Justification = "FxCop does not seem to allow adding an exception to this term in its dictionary.")]
diff --git a/src/System.Web.Http.SelfHost/HttpSelfHostConfiguration.cs b/src/System.Web.Http.SelfHost/HttpSelfHostConfiguration.cs
new file mode 100644
index 00000000..85603ce2
--- /dev/null
+++ b/src/System.Web.Http.SelfHost/HttpSelfHostConfiguration.cs
@@ -0,0 +1,298 @@
+using System.Diagnostics.CodeAnalysis;
+using System.IdentityModel.Selectors;
+using System.ServiceModel;
+using System.ServiceModel.Channels;
+using System.ServiceModel.Description;
+using System.ServiceModel.Security;
+using System.Web.Http.Common;
+using System.Web.Http.SelfHost.Channels;
+using System.Web.Http.SelfHost.Properties;
+using System.Web.Http.SelfHost.ServiceModel;
+using System.Web.Http.SelfHost.ServiceModel.Channels;
+
+namespace System.Web.Http.SelfHost
+{
+ /// <summary>
+ /// The configuration class for Http Services
+ /// </summary>
+ public class HttpSelfHostConfiguration : HttpConfiguration
+ {
+ private const int DefaultMaxConcurrentRequests = 100;
+ private const int PendingContextFactor = 100;
+ private const int MinConcurrentRequests = 1;
+ private const int MinBufferSize = 0;
+ private const int MinReceivedMessageSize = 0;
+
+ private Uri _baseAddress;
+ private int _maxConcurrentRequests;
+ private ServiceCredentials _credentials = new ServiceCredentials();
+ private bool _useWindowsAuth;
+ private TransferMode _transferMode;
+ private int _maxBufferSize;
+ private long _maxReceivedMessageSize;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpSelfHostConfiguration"/> class.
+ /// </summary>
+ /// <param name="baseAddress">The base address.</param>
+ public HttpSelfHostConfiguration(string baseAddress)
+ : this(new Uri(baseAddress, UriKind.RelativeOrAbsolute))
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpSelfHostConfiguration"/> class.
+ /// </summary>
+ /// <param name="baseAddress">The base address.</param>
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "caller owns object")]
+ public HttpSelfHostConfiguration(Uri baseAddress)
+ : base(new HttpRouteCollection(ValidateBaseAddress(baseAddress).AbsolutePath))
+ {
+ _baseAddress = ValidateBaseAddress(baseAddress);
+ _maxConcurrentRequests = GetDefaultMaxConcurrentRequests();
+ _maxBufferSize = TransportDefaults.MaxBufferSize;
+ _maxReceivedMessageSize = TransportDefaults.MaxReceivedMessageSize;
+ }
+
+ /// <summary>
+ /// Gets the base address.
+ /// </summary>
+ /// <value>
+ /// The base address.
+ /// </value>
+ public Uri BaseAddress
+ {
+ get { return _baseAddress; }
+ }
+
+ /// <summary>
+ /// Gets or sets the upper limit of how many concurrent <see cref="T:System.Net.Http.HttpRequestMessage"/> instances
+ /// can be processed at any given time. The default is 100 times the number of CPU cores.
+ /// </summary>
+ /// <value>
+ /// The maximum concurrent <see cref="T:System.Net.Http.HttpRequestMessage"/> instances processed at any given time.
+ /// </value>
+ public int MaxConcurrentRequests
+ {
+ get { return _maxConcurrentRequests; }
+
+ set
+ {
+ if (value < MinConcurrentRequests)
+ {
+ throw Error.ArgumentTooSmall("value", value, MinConcurrentRequests);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the transfer mode.
+ /// </summary>
+ /// <value>
+ /// The transfer mode.
+ /// </value>
+ public TransferMode TransferMode
+ {
+ get { return _transferMode; }
+
+ set
+ {
+ TransferModeHelper.Validate(value);
+ _transferMode = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the size of the max buffer.
+ /// </summary>
+ /// <value>
+ /// The size of the max buffer.
+ /// </value>
+ public int MaxBufferSize
+ {
+ get { return _maxBufferSize; }
+
+ set
+ {
+ if (value < MinBufferSize)
+ {
+ throw Error.ArgumentTooSmall("value", value, MinBufferSize);
+ }
+
+ _maxBufferSize = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the size of the max received message.
+ /// </summary>
+ /// <value>
+ /// The size of the max received message.
+ /// </value>
+ public long MaxReceivedMessageSize
+ {
+ get { return _maxReceivedMessageSize; }
+
+ set
+ {
+ if (value < MinReceivedMessageSize)
+ {
+ throw Error.ArgumentTooSmall("value", value, MinReceivedMessageSize);
+ }
+
+ _maxReceivedMessageSize = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets UserNamePasswordValidator so that it can be used to validate the username and password
+ /// sent over HTTP or HTTPS
+ /// </summary>
+ /// <value>
+ /// The server certificate.
+ /// </value>
+ public UserNamePasswordValidator UserNamePasswordValidator
+ {
+ get { return _credentials.UserNameAuthentication.CustomUserNamePasswordValidator; }
+
+ set { _credentials.UserNameAuthentication.CustomUserNamePasswordValidator = value; }
+ }
+
+ /// <summary>
+ /// Use this flag to indicate that you want to use windows authentication. This flag can
+ /// not be used together with UserNamePasswordValidator property since you can either use
+ /// Windows or Username Password as client credential.
+ /// </summary>
+ /// <value>
+ /// set it true if you want to use windows authentication
+ /// </value>
+ public bool UseWindowsAuthentication
+ {
+ get { return _useWindowsAuth; }
+
+ set { _useWindowsAuth = value; }
+ }
+
+ /// <summary>
+ /// Internal method called to configure <see cref="HttpBinding"/> settings.
+ /// </summary>
+ /// <param name="httpBinding">Http binding.</param>
+ /// <returns>The <see cref="BindingParameterCollection"/> to use when building the <see cref="IChannelListener"/> or null if no binding parameters are present.</returns>
+ internal BindingParameterCollection ConfigureBinding(HttpBinding httpBinding)
+ {
+ return OnConfigureBinding(httpBinding);
+ }
+
+ /// <summary>
+ /// Called to apply the configuration on the endpoint level.
+ /// </summary>
+ /// <param name="httpBinding">Http endpoint.</param>
+ /// <returns>The <see cref="BindingParameterCollection"/> to use when building the <see cref="IChannelListener"/> or null if no binding parameters are present.</returns>
+ protected virtual BindingParameterCollection OnConfigureBinding(HttpBinding httpBinding)
+ {
+ if (httpBinding == null)
+ {
+ throw Error.ArgumentNull("httpBinding");
+ }
+
+ if (_useWindowsAuth && _credentials.UserNameAuthentication.CustomUserNamePasswordValidator != null)
+ {
+ throw Error.InvalidOperation(SRResources.CannotUseWindowsAuthWithUserNamePasswordValidator);
+ }
+
+ httpBinding.MaxBufferSize = MaxBufferSize;
+ httpBinding.MaxReceivedMessageSize = MaxReceivedMessageSize;
+ httpBinding.TransferMode = TransferMode;
+ if (_baseAddress.Scheme == Uri.UriSchemeHttps)
+ {
+ // we need to use SSL
+ httpBinding.Security = new HttpBindingSecurity()
+ {
+ Mode = HttpBindingSecurityMode.Transport,
+ };
+ }
+
+ // Set up binding parameters
+ if (_credentials.UserNameAuthentication.CustomUserNamePasswordValidator != null)
+ {
+ _credentials.UserNameAuthentication.UserNamePasswordValidationMode = UserNamePasswordValidationMode.Custom;
+ if (httpBinding.Security == null || httpBinding.Security.Mode == HttpBindingSecurityMode.None)
+ {
+ // Basic over HTTP case
+ httpBinding.Security = new HttpBindingSecurity()
+ {
+ Mode = HttpBindingSecurityMode.TransportCredentialOnly,
+ };
+ }
+
+ // We have validator, so we can set the client credential type to be basic
+ httpBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Basic;
+
+ return AddCredentialsToBindingParameters();
+ }
+ else if (_useWindowsAuth)
+ {
+ if (httpBinding.Security == null)
+ {
+ // Basic over HTTP case, should we even allow this?
+ httpBinding.Security = new HttpBindingSecurity()
+ {
+ Mode = HttpBindingSecurityMode.TransportCredentialOnly,
+ };
+ }
+
+ // We have validator, so we can set the client credential type to be windows
+ httpBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Windows;
+
+ return AddCredentialsToBindingParameters();
+ }
+
+ return null;
+ }
+
+ private BindingParameterCollection AddCredentialsToBindingParameters()
+ {
+ BindingParameterCollection bindingParameters = new BindingParameterCollection();
+ bindingParameters.Add(_credentials);
+ return bindingParameters;
+ }
+
+ private static Uri ValidateBaseAddress(Uri baseAddress)
+ {
+ if (baseAddress == null)
+ {
+ throw Error.ArgumentNull("baseAddress");
+ }
+
+ if (!baseAddress.IsAbsoluteUri)
+ {
+ throw Error.ArgumentUriNotAbsolute("baseAddress", baseAddress);
+ }
+
+ if (!String.IsNullOrEmpty(baseAddress.Query) || !String.IsNullOrEmpty(baseAddress.Fragment))
+ {
+ throw Error.ArgumentUriHasQueryOrFragment("baseAddress", baseAddress);
+ }
+
+ if (!ReferenceEquals(baseAddress.Scheme, Uri.UriSchemeHttp) && !ReferenceEquals(baseAddress.Scheme, Uri.UriSchemeHttps))
+ {
+ throw Error.ArgumentUriNotHttpOrHttpsScheme("baseAddress", baseAddress);
+ }
+
+ return baseAddress;
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We never want to fail here so we have to catch all exceptions.")]
+ private static int GetDefaultMaxConcurrentRequests()
+ {
+ try
+ {
+ return Math.Max(Environment.ProcessorCount * DefaultMaxConcurrentRequests, DefaultMaxConcurrentRequests);
+ }
+ catch
+ {
+ return DefaultMaxConcurrentRequests;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http.SelfHost/HttpSelfHostServer.cs b/src/System.Web.Http.SelfHost/HttpSelfHostServer.cs
new file mode 100644
index 00000000..896cfe6a
--- /dev/null
+++ b/src/System.Web.Http.SelfHost/HttpSelfHostServer.cs
@@ -0,0 +1,893 @@
+using System.Collections.Concurrent;
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+using System.Net;
+using System.Net.Http;
+using System.Security.Principal;
+using System.ServiceModel;
+using System.ServiceModel.Channels;
+using System.ServiceModel.Security;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Common;
+using System.Web.Http.Hosting;
+using System.Web.Http.SelfHost.Channels;
+using System.Web.Http.SelfHost.Properties;
+using System.Web.Http.SelfHost.ServiceModel.Channels;
+
+namespace System.Web.Http.SelfHost
+{
+ /// <summary>
+ /// Implementation of an <see cref="HttpServer"/> which listens directly to HTTP.
+ /// </summary>
+ public sealed class HttpSelfHostServer : HttpServer
+ {
+ private static readonly AsyncCallback _onOpenListenerComplete = new AsyncCallback(OnOpenListenerComplete);
+ private static readonly AsyncCallback _onAcceptChannelComplete = new AsyncCallback(OnAcceptChannelComplete);
+ private static readonly AsyncCallback _onOpenChannelComplete = new AsyncCallback(OnOpenChannelComplete);
+ private static readonly AsyncCallback _onReceiveRequestContextComplete = new AsyncCallback(OnReceiveRequestContextComplete);
+ private static readonly AsyncCallback _onReplyComplete = new AsyncCallback(OnReplyComplete);
+
+ private static readonly AsyncCallback _onCloseListenerComplete = new AsyncCallback(OnCloseListenerComplete);
+ private static readonly AsyncCallback _onCloseChannelComplete = new AsyncCallback(OnCloseChannelComplete);
+
+ private static readonly TimeSpan _acceptTimeout = TimeSpan.MaxValue;
+ private static readonly TimeSpan _receiveTimeout = TimeSpan.MaxValue;
+
+ /// <summary>
+ /// Provides a key for the current <see cref="T:System.ServiceModel.Security.SecurityMessageProperty"/> stored in <see cref="M:HttpRequestMessage.Properties"/>.
+ /// Do not change this key as this is being used by WCF.
+ /// </summary>
+ public static readonly string SecurityKey = "Security";
+
+ private ConcurrentBag<IReplyChannel> _channels = new ConcurrentBag<IReplyChannel>();
+ private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
+
+ private bool _disposed;
+ private HttpSelfHostConfiguration _configuration;
+ private IChannelListener<IReplyChannel> _listener;
+ private TaskCompletionSource<bool> _openTaskCompletionSource;
+ private TaskCompletionSource<bool> _closeTaskCompletionSource;
+
+ // State: 0 = new, 1 = open, 2 = closed
+ private int _state;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpSelfHostServer"/> class.
+ /// </summary>
+ /// <param name="configuration">The configuration.</param>
+ public HttpSelfHostServer(HttpSelfHostConfiguration configuration)
+ : base(configuration)
+ {
+ if (configuration == null)
+ {
+ throw Error.ArgumentNull("configuration");
+ }
+
+ _configuration = configuration;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpSelfHostServer"/> class.
+ /// </summary>
+ /// <param name="configuration">The configuration.</param>
+ /// <param name="dispatcher">The dispatcher.</param>
+ public HttpSelfHostServer(HttpSelfHostConfiguration configuration, HttpMessageHandler dispatcher)
+ : base(configuration, dispatcher)
+ {
+ if (configuration == null)
+ {
+ throw Error.ArgumentNull("configuration");
+ }
+
+ _configuration = configuration;
+ }
+
+ /// <summary>
+ /// Opens the current <see cref="HttpServer"/> instance.
+ /// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous <see cref="HttpServer"/> open operation. Once this task completes successfully the server is running.</returns>
+ public Task OpenAsync()
+ {
+ if (Interlocked.CompareExchange(ref _state, 1, 0) == 1)
+ {
+ throw Error.InvalidOperation(SRResources.HttpServerAlreadyRunning, typeof(HttpSelfHostServer).Name);
+ }
+
+ _openTaskCompletionSource = new TaskCompletionSource<bool>();
+ BeginOpenListener(this);
+ return _openTaskCompletionSource.Task;
+ }
+
+ /// <summary>
+ /// Closes the current <see cref="HttpServer"/> instance.
+ /// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous <see cref="HttpServer"/> close operation.</returns>
+ public Task CloseAsync()
+ {
+ if (Interlocked.CompareExchange(ref _state, 2, 1) != 1)
+ {
+ return TaskHelpers.Completed();
+ }
+
+ _closeTaskCompletionSource = new TaskCompletionSource<bool>();
+
+ // Cancel requests currently being processed
+ _cancellationTokenSource.Cancel();
+
+ BeginCloseListener(this);
+ return _closeTaskCompletionSource.Task;
+ }
+
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources
+ /// </summary>
+ /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged SRResources.</param>
+ protected override void Dispose(bool disposing)
+ {
+ if (!_disposed)
+ {
+ _disposed = true;
+ if (_cancellationTokenSource != null)
+ {
+ _cancellationTokenSource.Dispose();
+ _cancellationTokenSource = null;
+ }
+ }
+
+ base.Dispose(disposing);
+ }
+
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "ReplyContext and HttpResponseMessage are disposed later.")]
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We never want to fail here so we have to catch all exceptions.")]
+ private static void ProcessRequestContext(ChannelContext channelContext, System.ServiceModel.Channels.RequestContext requestContext)
+ {
+ Contract.Assert(channelContext != null);
+ Contract.Assert(requestContext != null);
+
+ // Get the HTTP request from the WCF Message
+ HttpRequestMessage request = requestContext.RequestMessage.ToHttpRequestMessage();
+ if (request == null)
+ {
+ Error.InvalidOperation(SRResources.HttpMessageHandlerInvalidMessage, requestContext.RequestMessage.GetType());
+ return;
+ }
+
+ // create principal information and add it the request for the windows auth case
+ SecurityMessageProperty property;
+ if (request.Properties.TryGetValue<SecurityMessageProperty>(SecurityKey, out property))
+ {
+ ServiceSecurityContext context = property.ServiceSecurityContext;
+ if (context != null && context.PrimaryIdentity != null)
+ {
+ WindowsIdentity windowsIdentity = context.PrimaryIdentity as WindowsIdentity;
+
+ if (windowsIdentity != null)
+ {
+ request.Properties.Add(HttpPropertyKeys.UserPrincipalKey, new WindowsPrincipal(windowsIdentity));
+ }
+ }
+ }
+
+ // Submit request up the stack
+ try
+ {
+ HttpResponseMessage responseMessage = null;
+
+ channelContext.Server.SendAsync(request, channelContext.Server._cancellationTokenSource.Token)
+ .Then(response =>
+ {
+ responseMessage = response ?? request.CreateResponse(HttpStatusCode.OK);
+ })
+ .Catch(ex =>
+ {
+ // REVIEW: Shouldn't the response contain the exception so it can be serialized?
+ responseMessage = request.CreateResponse(HttpStatusCode.InternalServerError);
+ return TaskHelpers.Completed();
+ })
+ .Finally(() =>
+ {
+ if (responseMessage == null) // No Then or Catch, must've been canceled
+ {
+ responseMessage = request.CreateResponse(HttpStatusCode.ServiceUnavailable);
+ }
+
+ Message reply = responseMessage.ToMessage();
+ BeginReply(new ReplyContext(channelContext, requestContext, reply));
+ });
+ }
+ catch
+ {
+ // REVIEW: Shouldn't the response contain the exception so it can be serialized?
+ HttpResponseMessage response = request.CreateResponse(HttpStatusCode.InternalServerError);
+ Message reply = response.ToMessage();
+ BeginReply(new ReplyContext(channelContext, requestContext, reply));
+ }
+ }
+
+ private static void CancelTask(TaskCompletionSource<bool> taskCompletionSource)
+ {
+ Contract.Assert(taskCompletionSource != null);
+ taskCompletionSource.TrySetCanceled();
+ }
+
+ private static void FaultTask(TaskCompletionSource<bool> taskCompletionSource, Exception exception)
+ {
+ Contract.Assert(taskCompletionSource != null);
+ taskCompletionSource.TrySetException(exception);
+ }
+
+ private static void CompleteTask(TaskCompletionSource<bool> taskCompletionSource)
+ {
+ Contract.Assert(taskCompletionSource != null);
+ taskCompletionSource.TrySetResult(true);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We never want to fail here so we have to catch all exceptions.")]
+ private static void BeginOpenListener(HttpSelfHostServer server)
+ {
+ Contract.Assert(server != null);
+
+ try
+ {
+ // Create WCF HTTP transport channel
+ HttpBinding binding = new HttpBinding();
+
+ // Get it configured
+ BindingParameterCollection bindingParameters = server._configuration.ConfigureBinding(binding);
+ if (bindingParameters == null)
+ {
+ bindingParameters = new BindingParameterCollection();
+ }
+
+ // Build channel listener
+ server._listener = binding.BuildChannelListener<IReplyChannel>(server._configuration.BaseAddress, bindingParameters);
+ if (server._listener == null)
+ {
+ throw Error.InvalidOperation(SRResources.InvalidChannelListener, typeof(IChannelListener).Name, typeof(IReplyChannel).Name);
+ }
+
+ IAsyncResult result = server._listener.BeginOpen(_onOpenListenerComplete, server);
+ if (result.CompletedSynchronously)
+ {
+ OpenListenerComplete(result);
+ }
+ }
+ catch (Exception e)
+ {
+ FaultTask(server._openTaskCompletionSource, e);
+ }
+ }
+
+ private static void OnOpenListenerComplete(IAsyncResult result)
+ {
+ Contract.Assert(result != null);
+
+ if (result.CompletedSynchronously)
+ {
+ return;
+ }
+
+ OpenListenerComplete(result);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We never want to fail here so we have to catch all exceptions.")]
+ private static void OpenListenerComplete(IAsyncResult result)
+ {
+ HttpSelfHostServer server = (HttpSelfHostServer)result.AsyncState;
+ Contract.Assert(server != null);
+ Contract.Assert(server._listener != null);
+
+ try
+ {
+ server._listener.EndOpen(result);
+
+ // Start accepting channel
+ BeginAcceptChannel(server);
+ }
+ catch (Exception e)
+ {
+ FaultTask(server._openTaskCompletionSource, e);
+ }
+ }
+
+ private static void BeginAcceptChannel(HttpSelfHostServer server)
+ {
+ Contract.Assert(server != null);
+ Contract.Assert(server._listener != null);
+
+ IAsyncResult result = BeginTryAcceptChannel(server, _onAcceptChannelComplete);
+ if (result.CompletedSynchronously)
+ {
+ AcceptChannelComplete(result);
+ }
+ }
+
+ private static void OnAcceptChannelComplete(IAsyncResult result)
+ {
+ if (result.CompletedSynchronously)
+ {
+ return;
+ }
+
+ AcceptChannelComplete(result);
+ }
+
+ private static void AcceptChannelComplete(IAsyncResult result)
+ {
+ Contract.Assert(result != null);
+
+ HttpSelfHostServer server = (HttpSelfHostServer)result.AsyncState;
+ Contract.Assert(server != null, "host cannot be null");
+ Contract.Assert(server._listener != null, "host.listener cannot be null");
+
+ IReplyChannel channel;
+ if (EndTryAcceptChannel(result, out channel))
+ {
+ // If we didn't get a channel then we stop accepting new channels
+ if (channel != null)
+ {
+ server._channels.Add(channel);
+ BeginOpenChannel(new ChannelContext(server, channel));
+ }
+ else
+ {
+ CancelTask(server._openTaskCompletionSource);
+ }
+ }
+ else
+ {
+ // Start accepting next channel
+ BeginAcceptChannel(server);
+ }
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We never want to fail here so we have to catch all exceptions.")]
+ private static IAsyncResult BeginTryAcceptChannel(HttpSelfHostServer server, AsyncCallback callback)
+ {
+ Contract.Assert(server != null);
+ Contract.Assert(server._listener != null);
+ Contract.Assert(callback != null);
+
+ try
+ {
+ return server._listener.BeginAcceptChannel(_acceptTimeout, callback, server);
+ }
+ catch (CommunicationObjectAbortedException)
+ {
+ return new CompletedAsyncResult<bool>(true, callback, server);
+ }
+ catch (CommunicationObjectFaultedException)
+ {
+ return new CompletedAsyncResult<bool>(true, callback, server);
+ }
+ catch (TimeoutException)
+ {
+ return new CompletedAsyncResult<bool>(false, callback, server);
+ }
+ catch (CommunicationException)
+ {
+ return new CompletedAsyncResult<bool>(false, callback, server);
+ }
+ catch
+ {
+ return new CompletedAsyncResult<bool>(false, callback, server);
+ }
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We never want to fail here so we have to catch all exceptions.")]
+ private static bool EndTryAcceptChannel(IAsyncResult result, out IReplyChannel channel)
+ {
+ Contract.Assert(result != null);
+
+ CompletedAsyncResult<bool> handlerResult = result as CompletedAsyncResult<bool>;
+ if (handlerResult != null)
+ {
+ channel = null;
+ return CompletedAsyncResult<bool>.End(handlerResult);
+ }
+ else
+ {
+ try
+ {
+ HttpSelfHostServer server = (HttpSelfHostServer)result.AsyncState;
+ Contract.Assert(server != null);
+ Contract.Assert(server._listener != null);
+ channel = server._listener.EndAcceptChannel(result);
+ return true;
+ }
+ catch (CommunicationObjectAbortedException)
+ {
+ channel = null;
+ return true;
+ }
+ catch (CommunicationObjectFaultedException)
+ {
+ channel = null;
+ return true;
+ }
+ catch (TimeoutException)
+ {
+ channel = null;
+ return false;
+ }
+ catch (CommunicationException)
+ {
+ channel = null;
+ return false;
+ }
+ catch
+ {
+ channel = null;
+ return false;
+ }
+ }
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We never want to fail here so we have to catch all exceptions.")]
+ private static void BeginOpenChannel(ChannelContext channelContext)
+ {
+ Contract.Assert(channelContext != null);
+ Contract.Assert(channelContext.Channel != null);
+
+ try
+ {
+ IAsyncResult result = channelContext.Channel.BeginOpen(_onOpenChannelComplete, channelContext);
+ if (result.CompletedSynchronously)
+ {
+ OpenChannelComplete(result);
+ }
+ }
+ catch (Exception e)
+ {
+ FaultTask(channelContext.Server._openTaskCompletionSource, e);
+ }
+ }
+
+ private static void OnOpenChannelComplete(IAsyncResult result)
+ {
+ Contract.Assert(result != null);
+
+ if (result.CompletedSynchronously)
+ {
+ return;
+ }
+
+ OpenChannelComplete(result);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We never want to fail here so we have to catch all exceptions.")]
+ private static void OpenChannelComplete(IAsyncResult result)
+ {
+ ChannelContext channelContext = (ChannelContext)result.AsyncState;
+ Contract.Assert(channelContext != null);
+ Contract.Assert(channelContext.Channel != null);
+
+ try
+ {
+ channelContext.Channel.EndOpen(result);
+
+ // The channel is open and we can complete the open task
+ CompleteTask(channelContext.Server._openTaskCompletionSource);
+
+ // Start pumping messages
+ for (int index = 0; index < channelContext.Server._configuration.MaxConcurrentRequests; index++)
+ {
+ BeginReceiveRequestContext(channelContext);
+ }
+
+ // Start accepting next channel
+ BeginAcceptChannel(channelContext.Server);
+ }
+ catch (Exception e)
+ {
+ FaultTask(channelContext.Server._openTaskCompletionSource, e);
+ }
+ }
+
+ private static void BeginReceiveRequestContext(ChannelContext context)
+ {
+ Contract.Assert(context != null);
+
+ if (context.Channel.State != CommunicationState.Opened)
+ {
+ return;
+ }
+
+ IAsyncResult result = BeginTryReceiveRequestContext(context, _onReceiveRequestContextComplete);
+ if (result.CompletedSynchronously)
+ {
+ ReceiveRequestContextComplete(result);
+ }
+ }
+
+ private static void OnReceiveRequestContextComplete(IAsyncResult result)
+ {
+ if (result.CompletedSynchronously)
+ {
+ return;
+ }
+
+ ReceiveRequestContextComplete(result);
+ }
+
+ private static void ReceiveRequestContextComplete(IAsyncResult result)
+ {
+ Contract.Assert(result != null);
+
+ ChannelContext channelContext = (ChannelContext)result.AsyncState;
+ Contract.Assert(channelContext != null);
+
+ System.ServiceModel.Channels.RequestContext requestContext;
+ if (EndTryReceiveRequestContext(result, out requestContext))
+ {
+ if (requestContext != null)
+ {
+ ProcessRequestContext(channelContext, requestContext);
+ }
+ }
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We never want to fail here so we have to catch all exceptions.")]
+ private static IAsyncResult BeginTryReceiveRequestContext(ChannelContext channelContext, AsyncCallback callback)
+ {
+ Contract.Assert(channelContext != null);
+ Contract.Assert(callback != null);
+
+ try
+ {
+ return channelContext.Channel.BeginTryReceiveRequest(_receiveTimeout, callback, channelContext);
+ }
+ catch (CommunicationObjectAbortedException)
+ {
+ return new CompletedAsyncResult<bool>(true, callback, channelContext);
+ }
+ catch (CommunicationObjectFaultedException)
+ {
+ return new CompletedAsyncResult<bool>(true, callback, channelContext);
+ }
+ catch (CommunicationException)
+ {
+ return new CompletedAsyncResult<bool>(false, callback, channelContext);
+ }
+ catch (TimeoutException)
+ {
+ return new CompletedAsyncResult<bool>(false, callback, channelContext);
+ }
+ catch
+ {
+ return new CompletedAsyncResult<bool>(false, callback, channelContext);
+ }
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We never want to fail here so we have to catch all exceptions.")]
+ private static bool EndTryReceiveRequestContext(IAsyncResult result, out System.ServiceModel.Channels.RequestContext requestContext)
+ {
+ Contract.Assert(result != null);
+
+ CompletedAsyncResult<bool> handlerResult = result as CompletedAsyncResult<bool>;
+ if (handlerResult != null)
+ {
+ requestContext = null;
+ return CompletedAsyncResult<bool>.End(handlerResult);
+ }
+ else
+ {
+ try
+ {
+ ChannelContext channelContext = (ChannelContext)result.AsyncState;
+ Contract.Assert(channelContext != null, "context cannot be null");
+ return channelContext.Channel.EndTryReceiveRequest(result, out requestContext);
+ }
+ catch (CommunicationObjectAbortedException)
+ {
+ requestContext = null;
+ return true;
+ }
+ catch (CommunicationObjectFaultedException)
+ {
+ requestContext = null;
+ return true;
+ }
+ catch (CommunicationException)
+ {
+ requestContext = null;
+ return false;
+ }
+ catch (TimeoutException)
+ {
+ requestContext = null;
+ return false;
+ }
+ catch
+ {
+ requestContext = null;
+ return false;
+ }
+ }
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We never want to fail here so we have to catch all exceptions.")]
+ private static void BeginReply(ReplyContext replyContext)
+ {
+ Contract.Assert(replyContext != null);
+
+ try
+ {
+ IAsyncResult result = replyContext.RequestContext.BeginReply(replyContext.Reply, _onReplyComplete, replyContext);
+ if (result.CompletedSynchronously)
+ {
+ ReplyComplete(result);
+ }
+ }
+ catch
+ {
+ BeginReceiveRequestContext(replyContext.ChannelContext);
+ replyContext.Dispose();
+ }
+ }
+
+ private static void OnReplyComplete(IAsyncResult result)
+ {
+ Contract.Assert(result != null);
+
+ if (result.CompletedSynchronously)
+ {
+ return;
+ }
+
+ ReplyComplete(result);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We never want to fail here so we have to catch all exceptions.")]
+ private static void ReplyComplete(IAsyncResult result)
+ {
+ ReplyContext replyContext = (ReplyContext)result.AsyncState;
+ Contract.Assert(replyContext != null);
+
+ try
+ {
+ replyContext.RequestContext.EndReply(result);
+ }
+ catch
+ {
+ }
+ finally
+ {
+ BeginReceiveRequestContext(replyContext.ChannelContext);
+ replyContext.Dispose();
+ }
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We never want to fail here so we have to catch all exceptions.")]
+ private static void BeginCloseListener(HttpSelfHostServer server)
+ {
+ Contract.Assert(server != null);
+
+ try
+ {
+ if (server._listener != null)
+ {
+ IAsyncResult result = server._listener.BeginClose(_onCloseListenerComplete, server);
+ if (result.CompletedSynchronously)
+ {
+ CloseListenerComplete(result);
+ }
+ }
+ }
+ catch
+ {
+ CloseNextChannel(server);
+ }
+ }
+
+ private static void OnCloseListenerComplete(IAsyncResult result)
+ {
+ Contract.Assert(result != null);
+
+ if (result.CompletedSynchronously)
+ {
+ return;
+ }
+
+ CloseListenerComplete(result);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We never want to fail here so we have to catch all exceptions.")]
+ private static void CloseListenerComplete(IAsyncResult result)
+ {
+ HttpSelfHostServer server = (HttpSelfHostServer)result.AsyncState;
+ Contract.Assert(server != null);
+ Contract.Assert(server._listener != null);
+
+ try
+ {
+ server._listener.EndClose(result);
+ }
+ catch
+ {
+ }
+ finally
+ {
+ CloseNextChannel(server);
+ }
+ }
+
+ private static void CloseNextChannel(HttpSelfHostServer server)
+ {
+ Contract.Assert(server != null);
+
+ IReplyChannel channel;
+ if (server._channels.TryTake(out channel))
+ {
+ BeginCloseChannel(new ChannelContext(server, channel));
+ }
+ else
+ {
+ CompleteTask(server._closeTaskCompletionSource);
+ }
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We never want to fail here so we have to catch all exceptions.")]
+ private static void BeginCloseChannel(ChannelContext channelContext)
+ {
+ Contract.Assert(channelContext != null);
+
+ try
+ {
+ IAsyncResult result = channelContext.Channel.BeginClose(_onCloseChannelComplete, channelContext);
+ if (result.CompletedSynchronously)
+ {
+ CloseChannelComplete(result);
+ }
+ }
+ catch
+ {
+ CloseNextChannel(channelContext.Server);
+ }
+ }
+
+ private static void OnCloseChannelComplete(IAsyncResult result)
+ {
+ Contract.Assert(result != null);
+
+ if (result.CompletedSynchronously)
+ {
+ return;
+ }
+
+ CloseChannelComplete(result);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We never want to fail here so we have to catch all exceptions.")]
+ private static void CloseChannelComplete(IAsyncResult result)
+ {
+ ChannelContext channelContext = (ChannelContext)result.AsyncState;
+ Contract.Assert(channelContext != null);
+
+ try
+ {
+ channelContext.Channel.EndClose(result);
+ }
+ catch
+ {
+ }
+ finally
+ {
+ CloseNextChannel(channelContext.Server);
+ }
+ }
+
+ /// <summary>
+ /// Provides context for receiving an <see cref="System.ServiceModel.Channels.RequestContext"/> instance asynchronously.
+ /// </summary>
+ private class ChannelContext
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ChannelContext"/> class.
+ /// </summary>
+ /// <param name="server">The host to associate with this context.</param>
+ /// <param name="channel">The channel to associate with this channel.</param>
+ public ChannelContext(HttpSelfHostServer server, IReplyChannel channel)
+ {
+ Contract.Assert(server != null);
+ Contract.Assert(channel != null);
+ Server = server;
+ Channel = channel;
+ }
+
+ /// <summary>
+ /// Gets the <see cref="HttpSelfHostServer"/> instance.
+ /// </summary>
+ /// <value>
+ /// The <see cref="HttpSelfHostServer"/> instance.
+ /// </value>
+ public HttpSelfHostServer Server { get; private set; }
+
+ /// <summary>
+ /// Gets the <see cref="IReplyChannel"/> instance.
+ /// </summary>
+ /// <value>
+ /// The <see cref="System.ServiceModel.Channels.RequestContext"/> instance.
+ /// </value>
+ public IReplyChannel Channel { get; private set; }
+ }
+
+ /// <summary>
+ /// Provides context for sending a <see cref="System.ServiceModel.Channels.Message"/> instance asynchronously in response
+ /// to a request.
+ /// </summary>
+ private class ReplyContext : IDisposable
+ {
+ private bool _disposed;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ChannelContext"/> class.
+ /// </summary>
+ /// <param name="channelContext">The channel context to associate with this reply context.</param>
+ /// <param name="requestContext">The request context to associate with this reply context.</param>
+ /// <param name="reply">The reply to associate with this reply context.</param>
+ public ReplyContext(ChannelContext channelContext, RequestContext requestContext, Message reply)
+ {
+ Contract.Assert(channelContext != null);
+ Contract.Assert(requestContext != null);
+ Contract.Assert(reply != null);
+
+ ChannelContext = channelContext;
+ RequestContext = requestContext;
+ Reply = reply;
+ }
+
+ /// <summary>
+ /// Gets the <see cref="ChannelContext"/> instance.
+ /// </summary>
+ /// <value>
+ /// The <see cref="ChannelContext"/> instance.
+ /// </value>
+ public ChannelContext ChannelContext { get; private set; }
+
+ /// <summary>
+ /// Gets the <see cref="System.ServiceModel.Channels.RequestContext"/> instance.
+ /// </summary>
+ /// <value>
+ /// The <see cref="System.ServiceModel.Channels.RequestContext"/> instance.
+ /// </value>
+ public RequestContext RequestContext { get; private set; }
+
+ /// <summary>
+ /// Gets the reply <see cref="System.ServiceModel.Channels.Message"/> instance.
+ /// </summary>
+ /// <value>
+ /// The reply <see cref="System.ServiceModel.Channels.Message"/> instance.
+ /// </value>
+ public Message Reply { get; private set; }
+
+ /// <summary>
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged SRResources.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources
+ /// </summary>
+ /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged SRResources.</param>
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_disposed)
+ {
+ if (disposing)
+ {
+ RequestContext.Close();
+ Reply.Close();
+ }
+
+ _disposed = true;
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http.SelfHost/Properties/AssemblyInfo.cs b/src/System.Web.Http.SelfHost/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..55990ed5
--- /dev/null
+++ b/src/System.Web.Http.SelfHost/Properties/AssemblyInfo.cs
@@ -0,0 +1,7 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+
+[assembly: AssemblyTitle("System.Web.Http.SelfHost")]
+[assembly: AssemblyDescription("")]
+[assembly: InternalsVisibleTo("System.Web.Http.SelfHost.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
+[assembly: InternalsVisibleTo("System.Web.Http.SelfHost.Test.Unit, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
diff --git a/src/System.Web.Http.SelfHost/Properties/SRResources.Designer.cs b/src/System.Web.Http.SelfHost/Properties/SRResources.Designer.cs
new file mode 100644
index 00000000..5f42eb94
--- /dev/null
+++ b/src/System.Web.Http.SelfHost/Properties/SRResources.Designer.cs
@@ -0,0 +1,378 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.239
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace System.Web.Http.SelfHost.Properties {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class SRResources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal SRResources() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("System.Web.Http.SelfHost.Properties.SRResources", typeof(SRResources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to End cannot be called twice on an AsyncResult..
+ /// </summary>
+ internal static string AsyncResultAlreadyEnded {
+ get {
+ return ResourceManager.GetString("AsyncResultAlreadyEnded", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The IAsyncResult implementation &apos;{0}&apos; tried to complete a single operation multiple times. This could be caused by an incorrect application IAsyncResult implementation or other extensibility code, such as an IAsyncResult that returns incorrect CompletedSynchronously values or invokes the AsyncCallback multiple times..
+ /// </summary>
+ internal static string AsyncResultCompletedTwice {
+ get {
+ return ResourceManager.GetString("AsyncResultCompletedTwice", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Failed to allocate a managed memory buffer of {0} bytes. The amount of available memory may be low..
+ /// </summary>
+ internal static string BufferAllocationFailed {
+ get {
+ return ResourceManager.GetString("BufferAllocationFailed", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The size quota for this stream ({0}) has been exceeded..
+ /// </summary>
+ internal static string BufferedOutputStreamQuotaExceeded {
+ get {
+ return ResourceManager.GetString("BufferedOutputStreamQuotaExceeded", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to This buffer cannot be returned to the buffer manager because it is the wrong size..
+ /// </summary>
+ internal static string BufferIsNotRightSizeForBufferManager {
+ get {
+ return ResourceManager.GetString("BufferIsNotRightSizeForBufferManager", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The UseWindowsAuthentication option cannot be enabled when a UserNamePasswordValidator is specified on the HttpSelfHostConfiguration.&quot;.
+ /// </summary>
+ internal static string CannotUseWindowsAuthWithUserNamePasswordValidator {
+ get {
+ return ResourceManager.GetString("CannotUseWindowsAuthWithUserNamePasswordValidator", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Certificate-based client authentication is not supported in TransportCredentialOnly security mode. Select the Transport security mode..
+ /// </summary>
+ internal static string CertificateUnsupportedForHttpTransportCredentialOnly {
+ get {
+ return ResourceManager.GetString("CertificateUnsupportedForHttpTransportCredentialOnly", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The &apos;{0}&apos; type does not support building &apos;{1}&apos; instances..
+ /// </summary>
+ internal static string ChannelFactoryNotSupported {
+ get {
+ return ResourceManager.GetString("ChannelFactoryNotSupported", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The &apos;{0}&apos; type does not support the &apos;{1}&apos; channel shape. Implement the &apos;{2}&apos; channel shape to use this type..
+ /// </summary>
+ internal static string ChannelShapeNotSupported {
+ get {
+ return ResourceManager.GetString("ChannelShapeNotSupported", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The message version of the outgoing message ({0}) does not match that of the encoder ({1}). Make sure the binding is configured with the same version as the message..
+ /// </summary>
+ internal static string EncoderMessageVersionMismatch {
+ get {
+ return ResourceManager.GetString("EncoderMessageVersionMismatch", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The &apos;{0}&apos; type does not support a session encoder..
+ /// </summary>
+ internal static string HttpMessageEncoderFactoryDoesNotSupportSessionEncoder {
+ get {
+ return ResourceManager.GetString("HttpMessageEncoderFactoryDoesNotSupportSessionEncoder", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Could not obtain an HTTP request from message of type &apos;{0}&apos;..
+ /// </summary>
+ internal static string HttpMessageHandlerInvalidMessage {
+ get {
+ return ResourceManager.GetString("HttpMessageHandlerInvalidMessage", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to This &apos;{0}&apos; instance has already been started once. To start another instance, please create a new &apos;{0}&apos; object and start that..
+ /// </summary>
+ internal static string HttpServerAlreadyRunning {
+ get {
+ return ResourceManager.GetString("HttpServerAlreadyRunning", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The inner listener factory of {0} must be set before this operation..
+ /// </summary>
+ internal static string InnerListenerFactoryNotSet {
+ get {
+ return ResourceManager.GetString("InnerListenerFactoryNotSet", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to An incorrect IAsyncResult was provided to an &apos;End&apos; method. The IAsyncResult object passed to &apos;End&apos; must be the one returned from the matching &apos;Begin&apos; or passed to the callback provided to &apos;Begin&apos;..
+ /// </summary>
+ internal static string InvalidAsyncResult {
+ get {
+ return ResourceManager.GetString("InvalidAsyncResult", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to An incorrect implementation of the IAsyncResult interface may be returning incorrect values from the CompletedSynchronously property or calling the AsyncCallback more than once. The type {0} could be the incorrect implementation..
+ /// </summary>
+ internal static string InvalidAsyncResultImplementation {
+ get {
+ return ResourceManager.GetString("InvalidAsyncResultImplementation", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to An incorrect implementation of the IAsyncResult interface may be returning incorrect values from the CompletedSynchronously property or calling the AsyncCallback more than once..
+ /// </summary>
+ internal static string InvalidAsyncResultImplementationGeneric {
+ get {
+ return ResourceManager.GetString("InvalidAsyncResultImplementationGeneric", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Error creating &apos;{0}&apos; instance using &apos;{1}&apos;..
+ /// </summary>
+ internal static string InvalidChannelListener {
+ get {
+ return ResourceManager.GetString("InvalidChannelListener", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A null value was returned from an async &apos;Begin&apos; method or passed to an AsyncCallback. Async &apos;Begin&apos; implementations must return a non-null IAsyncResult and pass the same IAsyncResult object as the parameter to the AsyncCallback..
+ /// </summary>
+ internal static string InvalidNullAsyncResult {
+ get {
+ return ResourceManager.GetString("InvalidNullAsyncResult", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to HTTP request message body with a content length of &apos;{0}&apos; bytes..
+ /// </summary>
+ internal static string MessageBodyIsHttpRequestMessageWithKnownContentLength {
+ get {
+ return ResourceManager.GetString("MessageBodyIsHttpRequestMessageWithKnownContentLength", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to HTTP request message body with an undetermined content length..
+ /// </summary>
+ internal static string MessageBodyIsHttpRequestMessageWithUnknownContentLength {
+ get {
+ return ResourceManager.GetString("MessageBodyIsHttpRequestMessageWithUnknownContentLength", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to HTTP response message body with a content length of &apos;{0}&apos; bytes..
+ /// </summary>
+ internal static string MessageBodyIsHttpResponseMessageWithKnownContentLength {
+ get {
+ return ResourceManager.GetString("MessageBodyIsHttpResponseMessageWithKnownContentLength", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to HTTP response message body with an undetermined content length..
+ /// </summary>
+ internal static string MessageBodyIsHttpResponseMessageWithUnknownContentLength {
+ get {
+ return ResourceManager.GetString("MessageBodyIsHttpResponseMessageWithUnknownContentLength", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The &apos;{0}&apos; is closed and can no longer be used..
+ /// </summary>
+ internal static string MessageClosed {
+ get {
+ return ResourceManager.GetString("MessageClosed", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The response message is not valid for the encoder used by the &apos;{0}&apos; binding, which requires that the response message have been created with the &apos;{1}&apos; extension method on the &apos;{2}&apos; class..
+ /// </summary>
+ internal static string MessageInvalidForHttpMessageEncoder {
+ get {
+ return ResourceManager.GetString("MessageInvalidForHttpMessageEncoder", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The message instance does not support being read, written out or copied. Use the &apos;{0}&apos; or &apos;{1}&apos; extension methods on the &apos;{2}&apos; class to access the message content..
+ /// </summary>
+ internal static string MessageReadWriteCopyNotSupported {
+ get {
+ return ResourceManager.GetString("MessageReadWriteCopyNotSupported", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The message instance is non-empty but the &apos;{0}&apos; extension method on the &apos;{1}&apos; class returned null. Message instances that do not support the &apos;{0}&apos; extension method must be empty. .
+ /// </summary>
+ internal static string NonHttpMessageMustBeEmpty {
+ get {
+ return ResourceManager.GetString("NonHttpMessageMustBeEmpty", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Nonnegative number required.
+ /// </summary>
+ internal static string NonnegativeNumberRequired {
+ get {
+ return ResourceManager.GetString("NonnegativeNumberRequired", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The &apos;{0}&apos; type supports only MessageVersion.None. .
+ /// </summary>
+ internal static string OnlyMessageVersionNoneSupportedOnHttpMessageEncodingBindingElement {
+ get {
+ return ResourceManager.GetString("OnlyMessageVersionNoneSupportedOnHttpMessageEncodingBindingElement", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The value of the &apos;{0}&apos; parameter must be less than or equal to the value of the &apos;{1}&apos; parameter..
+ /// </summary>
+ internal static string ParameterMustBeLessThanOrEqualSecondParameter {
+ get {
+ return ResourceManager.GetString("ParameterMustBeLessThanOrEqualSecondParameter", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Read not supported on this stream..
+ /// </summary>
+ internal static string ReadNotSupported {
+ get {
+ return ResourceManager.GetString("ReadNotSupported", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The incoming message does not have the required &apos;{0}&apos; property of type &apos;{1}&apos;..
+ /// </summary>
+ internal static string RequestMissingHttpRequestMessageProperty {
+ get {
+ return ResourceManager.GetString("RequestMissingHttpRequestMessageProperty", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The incoming message does not have the required &apos;To&apos; header..
+ /// </summary>
+ internal static string RequestMissingToHeader {
+ get {
+ return ResourceManager.GetString("RequestMissingToHeader", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Seek not supported on this stream..
+ /// </summary>
+ internal static string SeekNotSupported {
+ get {
+ return ResourceManager.GetString("SeekNotSupported", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Value must be non-negative..
+ /// </summary>
+ internal static string ValueMustBeNonNegative {
+ get {
+ return ResourceManager.GetString("ValueMustBeNonNegative", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http.SelfHost/Properties/SRResources.resx b/src/System.Web.Http.SelfHost/Properties/SRResources.resx
new file mode 100644
index 00000000..d86d5912
--- /dev/null
+++ b/src/System.Web.Http.SelfHost/Properties/SRResources.resx
@@ -0,0 +1,225 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="AsyncResultAlreadyEnded" xml:space="preserve">
+ <value>End cannot be called twice on an AsyncResult.</value>
+ </data>
+ <data name="AsyncResultCompletedTwice" xml:space="preserve">
+ <value>The IAsyncResult implementation '{0}' tried to complete a single operation multiple times. This could be caused by an incorrect application IAsyncResult implementation or other extensibility code, such as an IAsyncResult that returns incorrect CompletedSynchronously values or invokes the AsyncCallback multiple times.</value>
+ </data>
+ <data name="CannotUseWindowsAuthWithUserNamePasswordValidator" xml:space="preserve">
+ <value>The UseWindowsAuthentication option cannot be enabled when a UserNamePasswordValidator is specified on the HttpSelfHostConfiguration."</value>
+ </data>
+ <data name="CertificateUnsupportedForHttpTransportCredentialOnly" xml:space="preserve">
+ <value>Certificate-based client authentication is not supported in TransportCredentialOnly security mode. Select the Transport security mode.</value>
+ </data>
+ <data name="ChannelFactoryNotSupported" xml:space="preserve">
+ <value>The '{0}' type does not support building '{1}' instances.</value>
+ </data>
+ <data name="ChannelShapeNotSupported" xml:space="preserve">
+ <value>The '{0}' type does not support the '{1}' channel shape. Implement the '{2}' channel shape to use this type.</value>
+ </data>
+ <data name="EncoderMessageVersionMismatch" xml:space="preserve">
+ <value>The message version of the outgoing message ({0}) does not match that of the encoder ({1}). Make sure the binding is configured with the same version as the message.</value>
+ </data>
+ <data name="HttpMessageEncoderFactoryDoesNotSupportSessionEncoder" xml:space="preserve">
+ <value>The '{0}' type does not support a session encoder.</value>
+ </data>
+ <data name="HttpMessageHandlerInvalidMessage" xml:space="preserve">
+ <value>Could not obtain an HTTP request from message of type '{0}'.</value>
+ </data>
+ <data name="HttpServerAlreadyRunning" xml:space="preserve">
+ <value>This '{0}' instance has already been started once. To start another instance, please create a new '{0}' object and start that.</value>
+ </data>
+ <data name="InnerListenerFactoryNotSet" xml:space="preserve">
+ <value>The inner listener factory of {0} must be set before this operation.</value>
+ </data>
+ <data name="InvalidAsyncResult" xml:space="preserve">
+ <value>An incorrect IAsyncResult was provided to an 'End' method. The IAsyncResult object passed to 'End' must be the one returned from the matching 'Begin' or passed to the callback provided to 'Begin'.</value>
+ </data>
+ <data name="InvalidAsyncResultImplementation" xml:space="preserve">
+ <value>An incorrect implementation of the IAsyncResult interface may be returning incorrect values from the CompletedSynchronously property or calling the AsyncCallback more than once. The type {0} could be the incorrect implementation.</value>
+ </data>
+ <data name="InvalidAsyncResultImplementationGeneric" xml:space="preserve">
+ <value>An incorrect implementation of the IAsyncResult interface may be returning incorrect values from the CompletedSynchronously property or calling the AsyncCallback more than once.</value>
+ </data>
+ <data name="InvalidChannelListener" xml:space="preserve">
+ <value>Error creating '{0}' instance using '{1}'.</value>
+ </data>
+ <data name="InvalidNullAsyncResult" xml:space="preserve">
+ <value>A null value was returned from an async 'Begin' method or passed to an AsyncCallback. Async 'Begin' implementations must return a non-null IAsyncResult and pass the same IAsyncResult object as the parameter to the AsyncCallback.</value>
+ </data>
+ <data name="MessageBodyIsHttpRequestMessageWithKnownContentLength" xml:space="preserve">
+ <value>HTTP request message body with a content length of '{0}' bytes.</value>
+ </data>
+ <data name="MessageBodyIsHttpRequestMessageWithUnknownContentLength" xml:space="preserve">
+ <value>HTTP request message body with an undetermined content length.</value>
+ </data>
+ <data name="MessageBodyIsHttpResponseMessageWithKnownContentLength" xml:space="preserve">
+ <value>HTTP response message body with a content length of '{0}' bytes.</value>
+ </data>
+ <data name="MessageBodyIsHttpResponseMessageWithUnknownContentLength" xml:space="preserve">
+ <value>HTTP response message body with an undetermined content length.</value>
+ </data>
+ <data name="MessageClosed" xml:space="preserve">
+ <value>The '{0}' is closed and can no longer be used.</value>
+ </data>
+ <data name="MessageInvalidForHttpMessageEncoder" xml:space="preserve">
+ <value>The response message is not valid for the encoder used by the '{0}' binding, which requires that the response message have been created with the '{1}' extension method on the '{2}' class.</value>
+ </data>
+ <data name="MessageReadWriteCopyNotSupported" xml:space="preserve">
+ <value>The message instance does not support being read, written out or copied. Use the '{0}' or '{1}' extension methods on the '{2}' class to access the message content.</value>
+ </data>
+ <data name="NonHttpMessageMustBeEmpty" xml:space="preserve">
+ <value>The message instance is non-empty but the '{0}' extension method on the '{1}' class returned null. Message instances that do not support the '{0}' extension method must be empty. </value>
+ </data>
+ <data name="NonnegativeNumberRequired" xml:space="preserve">
+ <value>Nonnegative number required</value>
+ </data>
+ <data name="OnlyMessageVersionNoneSupportedOnHttpMessageEncodingBindingElement" xml:space="preserve">
+ <value>The '{0}' type supports only MessageVersion.None. </value>
+ </data>
+ <data name="ParameterMustBeLessThanOrEqualSecondParameter" xml:space="preserve">
+ <value>The value of the '{0}' parameter must be less than or equal to the value of the '{1}' parameter.</value>
+ </data>
+ <data name="RequestMissingHttpRequestMessageProperty" xml:space="preserve">
+ <value>The incoming message does not have the required '{0}' property of type '{1}'.</value>
+ </data>
+ <data name="RequestMissingToHeader" xml:space="preserve">
+ <value>The incoming message does not have the required 'To' header.</value>
+ </data>
+ <data name="BufferedOutputStreamQuotaExceeded" xml:space="preserve">
+ <value>The size quota for this stream ({0}) has been exceeded.</value>
+ </data>
+ <data name="BufferIsNotRightSizeForBufferManager" xml:space="preserve">
+ <value>This buffer cannot be returned to the buffer manager because it is the wrong size.</value>
+ </data>
+ <data name="ReadNotSupported" xml:space="preserve">
+ <value>Read not supported on this stream.</value>
+ </data>
+ <data name="SeekNotSupported" xml:space="preserve">
+ <value>Seek not supported on this stream.</value>
+ </data>
+ <data name="ValueMustBeNonNegative" xml:space="preserve">
+ <value>Value must be non-negative.</value>
+ </data>
+ <data name="BufferAllocationFailed" xml:space="preserve">
+ <value>Failed to allocate a managed memory buffer of {0} bytes. The amount of available memory may be low.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/System.Web.Http.SelfHost/ServiceModel/Activation/AspNetEnvironment.cs b/src/System.Web.Http.SelfHost/ServiceModel/Activation/AspNetEnvironment.cs
new file mode 100644
index 00000000..14f4f24d
--- /dev/null
+++ b/src/System.Web.Http.SelfHost/ServiceModel/Activation/AspNetEnvironment.cs
@@ -0,0 +1,60 @@
+using System.Configuration;
+using System.Diagnostics.CodeAnalysis;
+using System.ServiceModel.Channels;
+
+namespace System.Web.Http.SelfHost.ServiceModel.Activation
+{
+ internal class AspNetEnvironment
+ {
+ public const string HostingMessagePropertyName = "webhost";
+
+ private const string HostingMessagePropertyTypeName = "System.ServiceModel.Activation.HostingMessageProperty";
+ private static AspNetEnvironment _current;
+ private static readonly object _thisLock = new object();
+
+ public static AspNetEnvironment Current
+ {
+ get
+ {
+ if (_current == null)
+ {
+ lock (_thisLock)
+ {
+ if (_current == null)
+ {
+ _current = new AspNetEnvironment();
+ }
+ }
+ }
+
+ return _current;
+ }
+ }
+
+ // ALTERED_FOR_PORT:
+ // The GetHostingProperty() code below is an altered implementation from the System.ServiceModel.Activation.HostedAspNetEnvironment class.
+ // The original implementation casts the hostingProperty to type System.ServiceModel.Activation.HostingMessageProperty. However,
+ // this class is internal sealed, therefore we simply check the type name.
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This is existing public API")]
+ public object GetHostingProperty(Message message)
+ {
+ object hostingProperty;
+ if (message.Properties.TryGetValue(HostingMessagePropertyName, out hostingProperty))
+ {
+ string hostingPropertyName = hostingProperty.GetType().FullName;
+ if (String.Equals(hostingPropertyName, HostingMessagePropertyTypeName, System.StringComparison.Ordinal))
+ {
+ return hostingProperty;
+ }
+ }
+
+ return null;
+ }
+
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This is existing public API")]
+ public object GetConfigurationSection(string sectionPath)
+ {
+ return ConfigurationManager.GetSection(sectionPath);
+ }
+ }
+}
diff --git a/src/System.Web.Http.SelfHost/ServiceModel/Channels/AsyncResult.cs b/src/System.Web.Http.SelfHost/ServiceModel/Channels/AsyncResult.cs
new file mode 100644
index 00000000..c43dc1ea
--- /dev/null
+++ b/src/System.Web.Http.SelfHost/ServiceModel/Channels/AsyncResult.cs
@@ -0,0 +1,360 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+using System.Threading;
+using System.Web.Http.Common;
+using System.Web.Http.SelfHost.Properties;
+
+namespace System.Web.Http.SelfHost.ServiceModel.Channels
+{
+ // AsyncResult starts acquired; Complete releases.
+ [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "Ported from WCF")]
+ internal abstract class AsyncResult : IAsyncResult
+ {
+ private static AsyncCallback _asyncCompletionWrapperCallback;
+ private AsyncCallback _completionCallback;
+ private bool _completedSynchronously;
+ private bool _endCalled;
+ private Exception _exception;
+ private bool _isCompleted;
+ private AsyncCompletion _nextAsyncCompletion;
+ private object _state;
+ private Action _beforePrepareAsyncCompletionAction;
+ private Func<IAsyncResult, bool> _checkSyncValidationFunc;
+
+ private ManualResetEvent _manualResetEvent;
+
+ private readonly object _thisLock;
+
+ protected AsyncResult(AsyncCallback callback, object state)
+ {
+ _completionCallback = callback;
+ _state = state;
+ _thisLock = new object();
+ }
+
+ /// <summary>
+ /// Can be utilized by subclasses to write core completion code for both the sync and async paths
+ /// in one location, signalling chainable synchronous completion with the boolean result,
+ /// and leveraging PrepareAsyncCompletion for conversion to an AsyncCallback.
+ /// </summary>
+ /// <remarks>NOTE: requires that "this" is passed in as the state object to the asynchronous sub-call being used with a completion routine.</remarks>
+ protected delegate bool AsyncCompletion(IAsyncResult result);
+
+ public object AsyncState
+ {
+ get { return _state; }
+ }
+
+ public WaitHandle AsyncWaitHandle
+ {
+ get
+ {
+ if (_manualResetEvent != null)
+ {
+ return _manualResetEvent;
+ }
+
+ lock (ThisLock)
+ {
+ if (_manualResetEvent == null)
+ {
+ _manualResetEvent = new ManualResetEvent(_isCompleted);
+ }
+ }
+
+ return _manualResetEvent;
+ }
+ }
+
+ public bool CompletedSynchronously
+ {
+ get { return _completedSynchronously; }
+ }
+
+ public bool HasCallback
+ {
+ get { return _completionCallback != null; }
+ }
+
+ public bool IsCompleted
+ {
+ get { return _isCompleted; }
+ }
+
+ // used in conjunction with PrepareAsyncCompletion to allow for finally blocks
+ protected Action<AsyncResult, Exception> OnCompleting { get; set; }
+
+ private object ThisLock
+ {
+ get { return _thisLock; }
+ }
+
+ // subclasses like TraceAsyncResult can use this to wrap the callback functionality in a scope
+ protected Action<AsyncCallback, IAsyncResult> VirtualCallback { get; set; }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is propagated or FailFast")]
+ protected void Complete(bool didCompleteSynchronously)
+ {
+ if (_isCompleted)
+ {
+ throw Error.InvalidOperation(SRResources.AsyncResultCompletedTwice, GetType());
+ }
+
+ _completedSynchronously = didCompleteSynchronously;
+ if (OnCompleting != null)
+ {
+ // Allow exception replacement, like a catch/throw pattern.
+ try
+ {
+ OnCompleting(this, _exception);
+ }
+ catch (Exception e)
+ {
+ _exception = e;
+ }
+ }
+
+ if (didCompleteSynchronously)
+ {
+ // If we completedSynchronously, then there's no chance that the manualResetEvent was created so
+ // we don't need to worry about a race
+ Debug.Assert(_manualResetEvent == null, "No ManualResetEvent should be created for a synchronous AsyncResult.");
+ _isCompleted = true;
+ }
+ else
+ {
+ lock (ThisLock)
+ {
+ _isCompleted = true;
+ if (_manualResetEvent != null)
+ {
+ _manualResetEvent.Set();
+ }
+ }
+ }
+
+ if (_completionCallback != null)
+ {
+ try
+ {
+ if (VirtualCallback != null)
+ {
+ VirtualCallback(_completionCallback, this);
+ }
+ else
+ {
+ _completionCallback(this);
+ }
+ }
+#pragma warning disable 1634
+#pragma warning suppress 56500 // transferring exception to another thread
+ catch (Exception e)
+ {
+ // String is not resourced because it occurs only in debug build, only with a fatal assert,
+ // and because it appears as an unused resource in a release build
+ Contract.Assert(false, Error.Format("{0}{1}{2}", "Async Callback threw an exception.", Environment.NewLine, e.ToString()));
+ }
+#pragma warning restore 1634
+ }
+ }
+
+ protected void Complete(bool didCompleteSynchronously, Exception error)
+ {
+ _exception = error;
+ Complete(didCompleteSynchronously);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is propagated")]
+ private static void AsyncCompletionWrapperCallback(IAsyncResult result)
+ {
+ if (result == null)
+ {
+ throw Error.InvalidOperation(SRResources.InvalidNullAsyncResult);
+ }
+ if (result.CompletedSynchronously)
+ {
+ return;
+ }
+
+ AsyncResult thisPtr = (AsyncResult)result.AsyncState;
+ if (!thisPtr.OnContinueAsyncCompletion(result))
+ {
+ return;
+ }
+
+ AsyncCompletion callback = thisPtr.GetNextCompletion();
+ if (callback == null)
+ {
+ ThrowInvalidAsyncResult(result);
+ }
+
+ bool completeSelf = false;
+ Exception completionException = null;
+ try
+ {
+ completeSelf = callback(result);
+ }
+ catch (Exception e)
+ {
+ completeSelf = true;
+ completionException = e;
+ }
+
+ if (completeSelf)
+ {
+ thisPtr.Complete(false, completionException);
+ }
+ }
+
+ // Note: this should be only derived by the TransactedAsyncResult
+ protected virtual bool OnContinueAsyncCompletion(IAsyncResult result)
+ {
+ return true;
+ }
+
+ // Note: this should be used only by the TransactedAsyncResult
+ protected void SetBeforePrepareAsyncCompletionAction(Action completionAction)
+ {
+ _beforePrepareAsyncCompletionAction = completionAction;
+ }
+
+ // Note: this should be used only by the TransactedAsyncResult
+ protected void SetCheckSyncValidationFunc(Func<IAsyncResult, bool> validationFunc)
+ {
+ _checkSyncValidationFunc = validationFunc;
+ }
+
+ protected AsyncCallback PrepareAsyncCompletion(AsyncCompletion callback)
+ {
+ if (_beforePrepareAsyncCompletionAction != null)
+ {
+ _beforePrepareAsyncCompletionAction();
+ }
+
+ _nextAsyncCompletion = callback;
+ if (AsyncResult._asyncCompletionWrapperCallback == null)
+ {
+ AsyncResult._asyncCompletionWrapperCallback = new AsyncCallback(AsyncCompletionWrapperCallback);
+ }
+ return AsyncResult._asyncCompletionWrapperCallback;
+ }
+
+ protected bool CheckSyncContinue(IAsyncResult result)
+ {
+ AsyncCompletion dummy;
+ return TryContinueHelper(result, out dummy);
+ }
+
+ protected bool SyncContinue(IAsyncResult result)
+ {
+ AsyncCompletion callback;
+ if (TryContinueHelper(result, out callback))
+ {
+ return callback(result);
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ private bool TryContinueHelper(IAsyncResult result, out AsyncCompletion callback)
+ {
+ if (result == null)
+ {
+ throw Error.InvalidOperation(SRResources.InvalidNullAsyncResult);
+ }
+
+ callback = null;
+ if (_checkSyncValidationFunc != null)
+ {
+ if (!_checkSyncValidationFunc(result))
+ {
+ return false;
+ }
+ }
+ else if (!result.CompletedSynchronously)
+ {
+ return false;
+ }
+
+ callback = GetNextCompletion();
+ if (callback == null)
+ {
+ ThrowInvalidAsyncResult("Only call Check/SyncContinue once per async operation (once per PrepareAsyncCompletion).");
+ }
+ return true;
+ }
+
+ private AsyncCompletion GetNextCompletion()
+ {
+ AsyncCompletion result = _nextAsyncCompletion;
+ _nextAsyncCompletion = null;
+ return result;
+ }
+
+ protected static void ThrowInvalidAsyncResult(IAsyncResult result)
+ {
+ if (result == null)
+ {
+ throw Error.ArgumentNull("result");
+ }
+
+ throw Error.InvalidOperation(SRResources.InvalidAsyncResultImplementation, result.GetType());
+ }
+
+ protected static void ThrowInvalidAsyncResult(string debugText)
+ {
+ string message = SRResources.InvalidAsyncResultImplementationGeneric;
+ if (debugText != null)
+ {
+#if DEBUG
+ message += " " + debugText;
+#endif
+ }
+ throw Error.InvalidOperation(message);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "Existing API")]
+ protected static TAsyncResult End<TAsyncResult>(IAsyncResult result)
+ where TAsyncResult : AsyncResult
+ {
+ if (result == null)
+ {
+ throw Error.ArgumentNull("result");
+ }
+
+ TAsyncResult asyncResult = result as TAsyncResult;
+
+ if (asyncResult == null)
+ {
+ throw Error.Argument("result", SRResources.InvalidAsyncResult);
+ }
+
+ if (asyncResult._endCalled)
+ {
+ throw Error.InvalidOperation(SRResources.AsyncResultAlreadyEnded);
+ }
+
+ asyncResult._endCalled = true;
+
+ if (!asyncResult._isCompleted)
+ {
+ asyncResult.AsyncWaitHandle.WaitOne();
+ }
+
+ if (asyncResult._manualResetEvent != null)
+ {
+ asyncResult._manualResetEvent.Close();
+ }
+
+ if (asyncResult._exception != null)
+ {
+ throw asyncResult._exception;
+ }
+
+ return asyncResult;
+ }
+ }
+}
diff --git a/src/System.Web.Http.SelfHost/ServiceModel/Channels/BufferManagerOutputStream.cs b/src/System.Web.Http.SelfHost/ServiceModel/Channels/BufferManagerOutputStream.cs
new file mode 100644
index 00000000..04644829
--- /dev/null
+++ b/src/System.Web.Http.SelfHost/ServiceModel/Channels/BufferManagerOutputStream.cs
@@ -0,0 +1,83 @@
+using System.Diagnostics;
+using System.ServiceModel;
+using System.ServiceModel.Channels;
+using System.Web.Http.Common;
+
+namespace System.Web.Http.SelfHost.ServiceModel.Channels
+{
+ internal class BufferManagerOutputStream : BufferedOutputStream
+ {
+ private string _quotaExceededString;
+
+ public BufferManagerOutputStream(string quotaExceededString)
+ {
+ _quotaExceededString = quotaExceededString;
+ }
+
+ public BufferManagerOutputStream(string quotaExceededString, int maxSize)
+ : base(maxSize)
+ {
+ _quotaExceededString = quotaExceededString;
+ }
+
+ // ALTERED_FOR_PORT:
+ // We're not getting the internal buffer manager as we do in the framework but just wrapping the bufferManager
+ public BufferManagerOutputStream(string quotaExceededString, int initialSize, int maxSize, BufferManager bufferManager)
+ : base(initialSize, maxSize, GetInternalBufferManager(bufferManager))
+ {
+ _quotaExceededString = quotaExceededString;
+ }
+
+ public void Init(int initialSize, int maxSizeQuota, BufferManager bufferManager)
+ {
+ Init(initialSize, maxSizeQuota, maxSizeQuota, bufferManager);
+ }
+
+ public void Init(int initialSize, int maxSizeQuota, int effectiveMaxSize, BufferManager bufferManager)
+ {
+ // ALTERED_FOR_PORT:
+ // We're not getting the internal buffer manager as we do in the framework but just wrapping the bufferManager
+ Reinitialize(initialSize, maxSizeQuota, effectiveMaxSize, GetInternalBufferManager(bufferManager));
+ }
+
+ protected override Exception CreateQuotaExceededException(int maxSizeQuota)
+ {
+ string excMsg = Error.Format(_quotaExceededString, maxSizeQuota);
+ return new QuotaExceededException(excMsg);
+ }
+
+ private static InternalBufferManager GetInternalBufferManager(BufferManager bufferManager)
+ {
+ Debug.Assert(bufferManager != null, "The 'bufferManager' parameter should not be null.");
+
+ return new WrappingInternalBufferManager(bufferManager);
+ }
+
+ private class WrappingInternalBufferManager : InternalBufferManager
+ {
+ private BufferManager _bufferManager;
+
+ public WrappingInternalBufferManager(BufferManager bufferManager)
+ {
+ Debug.Assert(bufferManager != null, "The 'bufferManager' parameter should not be null.");
+
+ _bufferManager = bufferManager;
+ }
+
+ public override byte[] TakeBuffer(int bufferSize)
+ {
+ return _bufferManager.TakeBuffer(bufferSize);
+ }
+
+ public override void ReturnBuffer(byte[] buffer)
+ {
+ _bufferManager.ReturnBuffer(buffer);
+ }
+
+ public override void Clear()
+ {
+ _bufferManager.Clear();
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http.SelfHost/ServiceModel/Channels/BufferedOutputStream.cs b/src/System.Web.Http.SelfHost/ServiceModel/Channels/BufferedOutputStream.cs
new file mode 100644
index 00000000..4d26d5fb
--- /dev/null
+++ b/src/System.Web.Http.SelfHost/ServiceModel/Channels/BufferedOutputStream.cs
@@ -0,0 +1,316 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Web.Http.Common;
+using System.Web.Http.SelfHost.Properties;
+
+namespace System.Web.Http.SelfHost.ServiceModel.Channels
+{
+ internal class BufferedOutputStream : Stream
+ {
+ private InternalBufferManager _theBufferManager;
+
+ private byte[][] _chunks;
+
+ private int _chunkCount;
+ private byte[] _currentChunk;
+ private int _currentChunkSize;
+ private int _maxSize;
+ private int _theMaxSizeQuota;
+ private int _totalSize;
+ private bool _callerReturnsBuffer;
+
+ [SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields", Justification = "Used for internal checking")]
+ private bool _bufferReturned;
+
+ [SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields", Justification = "Used for internal checking")]
+ private bool _initialized;
+
+ // requires an explicit call to Init() by the caller
+ public BufferedOutputStream()
+ {
+ _chunks = new byte[4][];
+ }
+
+ public BufferedOutputStream(int initialSize, int maxSize, InternalBufferManager bufferManager)
+ : this()
+ {
+ Reinitialize(initialSize, maxSize, bufferManager);
+ }
+
+ public BufferedOutputStream(int maxSize)
+ : this(0, maxSize, InternalBufferManager.Create(0, Int32.MaxValue))
+ {
+ }
+
+ public override bool CanRead
+ {
+ get { return false; }
+ }
+
+ public override bool CanSeek
+ {
+ get { return false; }
+ }
+
+ public override bool CanWrite
+ {
+ get { return true; }
+ }
+
+ public override long Length
+ {
+ get { return _totalSize; }
+ }
+
+ public override long Position
+ {
+ get { throw Error.NotSupported(SRResources.SeekNotSupported); }
+
+ set { throw Error.NotSupported(SRResources.SeekNotSupported); }
+ }
+
+ public void Reinitialize(int initialSize, int maxSizeQuota, InternalBufferManager bufferManager)
+ {
+ Reinitialize(initialSize, maxSizeQuota, maxSizeQuota, bufferManager);
+ }
+
+ public void Reinitialize(int initialSize, int maxSizeQuota, int effectiveMaxSize, InternalBufferManager bufferManager)
+ {
+ Debug.Assert(!_initialized, "Clear must be called before re-initializing stream");
+
+ if (bufferManager == null)
+ {
+ throw Error.ArgumentNull("bufferManager");
+ }
+
+ _theMaxSizeQuota = maxSizeQuota;
+ _maxSize = effectiveMaxSize;
+ _theBufferManager = bufferManager;
+ _currentChunk = bufferManager.TakeBuffer(initialSize);
+ _currentChunkSize = 0;
+ _totalSize = 0;
+ _chunkCount = 1;
+ _chunks[0] = _currentChunk;
+ _initialized = true;
+ }
+
+ public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
+ {
+ throw Error.NotSupported(SRResources.ReadNotSupported);
+ }
+
+ public override int EndRead(IAsyncResult asyncResult)
+ {
+ throw Error.NotSupported(SRResources.ReadNotSupported);
+ }
+
+ public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
+ {
+ Write(buffer, offset, count);
+ return new CompletedAsyncResult(callback, state);
+ }
+
+ public override void EndWrite(IAsyncResult asyncResult)
+ {
+ CompletedAsyncResult.End(asyncResult);
+ }
+
+ public void Clear()
+ {
+ if (!_callerReturnsBuffer)
+ {
+ for (int i = 0; i < _chunkCount; i++)
+ {
+ _theBufferManager.ReturnBuffer(_chunks[i]);
+ _chunks[i] = null;
+ }
+ }
+
+ _callerReturnsBuffer = false;
+ _initialized = false;
+ _bufferReturned = false;
+ _chunkCount = 0;
+ _currentChunk = null;
+ }
+
+ public override void Close()
+ {
+ }
+
+ public override void Flush()
+ {
+ }
+
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ throw Error.NotSupported(SRResources.ReadNotSupported);
+ }
+
+ public override int ReadByte()
+ {
+ throw Error.NotSupported(SRResources.ReadNotSupported);
+ }
+
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ throw Error.NotSupported(SRResources.SeekNotSupported);
+ }
+
+ public override void SetLength(long value)
+ {
+ throw Error.NotSupported(SRResources.SeekNotSupported);
+ }
+
+ public MemoryStream ToMemoryStream()
+ {
+ int bufferSize;
+ byte[] buffer = ToArray(out bufferSize);
+ return new MemoryStream(buffer, 0, bufferSize);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", Justification = "Out parameter is fine here.")]
+ public byte[] ToArray(out int bufferSize)
+ {
+ Debug.Assert(_initialized, "No data to return from uninitialized stream");
+ Debug.Assert(!_bufferReturned, "ToArray cannot be called more than once");
+
+ byte[] buffer;
+ if (_chunkCount == 1)
+ {
+ buffer = _currentChunk;
+ bufferSize = _currentChunkSize;
+ _callerReturnsBuffer = true;
+ }
+ else
+ {
+ buffer = _theBufferManager.TakeBuffer(_totalSize);
+ int offset = 0;
+ int count = _chunkCount - 1;
+ for (int i = 0; i < count; i++)
+ {
+ byte[] chunk = _chunks[i];
+ Buffer.BlockCopy(chunk, 0, buffer, offset, chunk.Length);
+ offset += chunk.Length;
+ }
+
+ Buffer.BlockCopy(_currentChunk, 0, buffer, offset, _currentChunkSize);
+ bufferSize = _totalSize;
+ }
+
+ _bufferReturned = true;
+ return buffer;
+ }
+
+ public void Skip(int size)
+ {
+ WriteCore(null, 0, size);
+ }
+
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ WriteCore(buffer, offset, count);
+ }
+
+ public override void WriteByte(byte value)
+ {
+ Debug.Assert(_initialized, "Cannot write to uninitialized stream");
+ Debug.Assert(!_bufferReturned, "Cannot write to stream once ToArray has been called.");
+
+ if (_totalSize == _maxSize)
+ {
+ throw CreateQuotaExceededException(_maxSize);
+ }
+
+ if (_currentChunkSize == _currentChunk.Length)
+ {
+ AllocNextChunk(1);
+ }
+
+ _currentChunk[_currentChunkSize++] = value;
+ }
+
+ protected virtual Exception CreateQuotaExceededException(int maxSizeQuota)
+ {
+ return new InvalidOperationException(Error.Format(SRResources.BufferedOutputStreamQuotaExceeded, maxSizeQuota));
+ }
+
+ private void WriteCore(byte[] buffer, int offset, int size)
+ {
+ Debug.Assert(_initialized, "Cannot write to uninitialized stream");
+ Debug.Assert(!_bufferReturned, "Cannot write to stream once ToArray has been called.");
+
+ if (size < 0)
+ {
+ throw Error.ArgumentOutOfRange("size", size, SRResources.ValueMustBeNonNegative);
+ }
+
+ if ((Int32.MaxValue - size) < _totalSize)
+ {
+ throw CreateQuotaExceededException(_theMaxSizeQuota);
+ }
+
+ int newTotalSize = _totalSize + size;
+ if (newTotalSize > _maxSize)
+ {
+ throw CreateQuotaExceededException(_theMaxSizeQuota);
+ }
+
+ int remainingSizeInChunk = _currentChunk.Length - _currentChunkSize;
+ if (size > remainingSizeInChunk)
+ {
+ if (remainingSizeInChunk > 0)
+ {
+ if (buffer != null)
+ {
+ Buffer.BlockCopy(buffer, offset, _currentChunk, _currentChunkSize, remainingSizeInChunk);
+ }
+
+ _currentChunkSize = _currentChunk.Length;
+ offset += remainingSizeInChunk;
+ size -= remainingSizeInChunk;
+ }
+
+ AllocNextChunk(size);
+ }
+
+ if (buffer != null)
+ {
+ Buffer.BlockCopy(buffer, offset, _currentChunk, _currentChunkSize, size);
+ }
+
+ _totalSize = newTotalSize;
+ _currentChunkSize += size;
+ }
+
+ private void AllocNextChunk(int minimumChunkSize)
+ {
+ int newChunkSize;
+ if (_currentChunk.Length > (Int32.MaxValue / 2))
+ {
+ newChunkSize = Int32.MaxValue;
+ }
+ else
+ {
+ newChunkSize = _currentChunk.Length * 2;
+ }
+
+ if (minimumChunkSize > newChunkSize)
+ {
+ newChunkSize = minimumChunkSize;
+ }
+
+ byte[] newChunk = _theBufferManager.TakeBuffer(newChunkSize);
+ if (_chunkCount == _chunks.Length)
+ {
+ byte[][] newChunks = new byte[_chunks.Length * 2][];
+ Array.Copy(_chunks, newChunks, _chunks.Length);
+ _chunks = newChunks;
+ }
+
+ _chunks[_chunkCount++] = newChunk;
+ _currentChunk = newChunk;
+ _currentChunkSize = 0;
+ }
+ }
+}
diff --git a/src/System.Web.Http.SelfHost/ServiceModel/Channels/ChannelAcceptor.cs b/src/System.Web.Http.SelfHost/ServiceModel/Channels/ChannelAcceptor.cs
new file mode 100644
index 00000000..cb16cf81
--- /dev/null
+++ b/src/System.Web.Http.SelfHost/ServiceModel/Channels/ChannelAcceptor.cs
@@ -0,0 +1,75 @@
+using System.ServiceModel;
+using System.ServiceModel.Channels;
+
+namespace System.Web.Http.SelfHost.ServiceModel.Channels
+{
+ internal abstract class ChannelAcceptor<TChannel> : CommunicationObject, IChannelAcceptor<TChannel>
+ where TChannel : class, IChannel
+ {
+ private ChannelManagerBase _channelManager;
+
+ protected ChannelAcceptor(ChannelManagerBase channelManager)
+ {
+ _channelManager = channelManager;
+ }
+
+ protected ChannelManagerBase ChannelManager
+ {
+ get { return _channelManager; }
+ }
+
+ protected override TimeSpan DefaultCloseTimeout
+ {
+ get { return ((IDefaultCommunicationTimeouts)_channelManager).CloseTimeout; }
+ }
+
+ protected override TimeSpan DefaultOpenTimeout
+ {
+ get { return ((IDefaultCommunicationTimeouts)_channelManager).OpenTimeout; }
+ }
+
+ public abstract TChannel AcceptChannel(TimeSpan timeout);
+
+ public abstract IAsyncResult BeginAcceptChannel(TimeSpan timeout, AsyncCallback callback, object state);
+
+ public abstract TChannel EndAcceptChannel(IAsyncResult result);
+
+ public abstract bool WaitForChannel(TimeSpan timeout);
+
+ public abstract IAsyncResult BeginWaitForChannel(TimeSpan timeout, AsyncCallback callback, object state);
+
+ public abstract bool EndWaitForChannel(IAsyncResult result);
+
+ protected override void OnAbort()
+ {
+ }
+
+ protected override IAsyncResult OnBeginClose(TimeSpan timeout, AsyncCallback callback, object state)
+ {
+ return new CompletedAsyncResult(callback, state);
+ }
+
+ protected override void OnEndClose(IAsyncResult result)
+ {
+ CompletedAsyncResult.End(result);
+ }
+
+ protected override void OnClose(TimeSpan timeout)
+ {
+ }
+
+ protected override IAsyncResult OnBeginOpen(TimeSpan timeout, AsyncCallback callback, object state)
+ {
+ return new CompletedAsyncResult(callback, state);
+ }
+
+ protected override void OnEndOpen(IAsyncResult result)
+ {
+ CompletedAsyncResult.End(result);
+ }
+
+ protected override void OnOpen(TimeSpan timeout)
+ {
+ }
+ }
+}
diff --git a/src/System.Web.Http.SelfHost/ServiceModel/Channels/ChannelBindingUtility.cs b/src/System.Web.Http.SelfHost/ServiceModel/Channels/ChannelBindingUtility.cs
new file mode 100644
index 00000000..c1b2b1cc
--- /dev/null
+++ b/src/System.Web.Http.SelfHost/ServiceModel/Channels/ChannelBindingUtility.cs
@@ -0,0 +1,60 @@
+using System.Security.Authentication.ExtendedProtection;
+using System.Security.Authentication.ExtendedProtection.Configuration;
+
+namespace System.Web.Http.SelfHost.ServiceModel.Channels
+{
+ internal static class ChannelBindingUtility
+ {
+ private static ExtendedProtectionPolicy disabledPolicy = new ExtendedProtectionPolicy(PolicyEnforcement.Never);
+ private static ExtendedProtectionPolicy defaultPolicy = disabledPolicy;
+
+ public static ExtendedProtectionPolicy DisabledPolicy
+ {
+ get { return disabledPolicy; }
+ }
+
+ public static ExtendedProtectionPolicy DefaultPolicy
+ {
+ get { return defaultPolicy; }
+ }
+
+ public static bool IsDefaultPolicy(ExtendedProtectionPolicy policy)
+ {
+ return Object.ReferenceEquals(policy, defaultPolicy);
+ }
+
+ public static void InitializeFrom(ExtendedProtectionPolicy source, ExtendedProtectionPolicyElement destination)
+ {
+ if (!IsDefaultPolicy(source))
+ {
+ destination.PolicyEnforcement = source.PolicyEnforcement;
+ destination.ProtectionScenario = source.ProtectionScenario;
+ destination.CustomServiceNames.Clear();
+
+ if (source.CustomServiceNames != null)
+ {
+ foreach (string name in source.CustomServiceNames)
+ {
+ ServiceNameElement entry = new ServiceNameElement();
+ entry.Name = name;
+ destination.CustomServiceNames.Add(entry);
+ }
+ }
+ }
+ }
+
+ public static ExtendedProtectionPolicy BuildPolicy(ExtendedProtectionPolicyElement configurationPolicy)
+ {
+ // using this pattern allows us to have a different default policy
+ // than the NCL team chooses.
+ if (configurationPolicy.ElementInformation.IsPresent)
+ {
+ return configurationPolicy.BuildPolicy();
+ }
+ else
+ {
+ return ChannelBindingUtility.DefaultPolicy;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http.SelfHost/ServiceModel/Channels/CompletedAsyncResult.cs b/src/System.Web.Http.SelfHost/ServiceModel/Channels/CompletedAsyncResult.cs
new file mode 100644
index 00000000..ec1b6eed
--- /dev/null
+++ b/src/System.Web.Http.SelfHost/ServiceModel/Channels/CompletedAsyncResult.cs
@@ -0,0 +1,45 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+
+namespace System.Web.Http.SelfHost.ServiceModel.Channels
+{
+ //An AsyncResult that completes as soon as it is instantiated.
+ internal class CompletedAsyncResult : AsyncResult
+ {
+ public CompletedAsyncResult(AsyncCallback callback, object state)
+ : base(callback, state)
+ {
+ Complete(true);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Result is validated.")]
+ public static void End(IAsyncResult result)
+ {
+ Contract.Assert(result != null, "CompletedAsyncResult was null.");
+ Contract.Assert(result.IsCompleted, "CompletedAsyncResult was not completed!");
+ AsyncResult.End<CompletedAsyncResult>(result);
+ }
+ }
+
+ internal class CompletedAsyncResult<T> : AsyncResult
+ {
+ private T data;
+
+ public CompletedAsyncResult(T data, AsyncCallback callback, object state)
+ : base(callback, state)
+ {
+ this.data = data;
+ Complete(true);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1000:DoNotDeclareStaticMembersOnGenericTypes", Justification = "Existing API")]
+ [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Result is validated.")]
+ public static T End(IAsyncResult result)
+ {
+ Contract.Assert(result != null, "CompletedAsyncResult<T> was null.");
+ Contract.Assert(result.IsCompleted, "CompletedAsyncResult<T> was not completed!");
+ CompletedAsyncResult<T> completedResult = AsyncResult.End<CompletedAsyncResult<T>>(result);
+ return completedResult.data;
+ }
+ }
+}
diff --git a/src/System.Web.Http.SelfHost/ServiceModel/Channels/HttpTransportDefaults.cs b/src/System.Web.Http.SelfHost/ServiceModel/Channels/HttpTransportDefaults.cs
new file mode 100644
index 00000000..4898caa0
--- /dev/null
+++ b/src/System.Web.Http.SelfHost/ServiceModel/Channels/HttpTransportDefaults.cs
@@ -0,0 +1,10 @@
+using System.ServiceModel;
+
+namespace System.Web.Http.SelfHost.ServiceModel.Channels
+{
+ internal static class HttpTransportDefaults
+ {
+ internal const HostNameComparisonMode HostNameComparisonMode = System.ServiceModel.HostNameComparisonMode.StrongWildcard;
+ internal const TransferMode TransferMode = System.ServiceModel.TransferMode.Buffered;
+ }
+}
diff --git a/src/System.Web.Http.SelfHost/ServiceModel/Channels/IChannelAcceptor.cs b/src/System.Web.Http.SelfHost/ServiceModel/Channels/IChannelAcceptor.cs
new file mode 100644
index 00000000..cd197f7f
--- /dev/null
+++ b/src/System.Web.Http.SelfHost/ServiceModel/Channels/IChannelAcceptor.cs
@@ -0,0 +1,21 @@
+using System.ServiceModel;
+using System.ServiceModel.Channels;
+
+namespace System.Web.Http.SelfHost.ServiceModel.Channels
+{
+ internal interface IChannelAcceptor<TChannel> : ICommunicationObject
+ where TChannel : class, IChannel
+ {
+ TChannel AcceptChannel(TimeSpan timeout);
+
+ IAsyncResult BeginAcceptChannel(TimeSpan timeout, AsyncCallback callback, object state);
+
+ TChannel EndAcceptChannel(IAsyncResult result);
+
+ bool WaitForChannel(TimeSpan timeout);
+
+ IAsyncResult BeginWaitForChannel(TimeSpan timeout, AsyncCallback callback, object state);
+
+ bool EndWaitForChannel(IAsyncResult result);
+ }
+}
diff --git a/src/System.Web.Http.SelfHost/ServiceModel/Channels/InternalBufferManager.cs b/src/System.Web.Http.SelfHost/ServiceModel/Channels/InternalBufferManager.cs
new file mode 100644
index 00000000..680ca90f
--- /dev/null
+++ b/src/System.Web.Http.SelfHost/ServiceModel/Channels/InternalBufferManager.cs
@@ -0,0 +1,502 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Threading;
+using System.Web.Http.Common;
+using System.Web.Http.SelfHost.Properties;
+
+namespace System.Web.Http.SelfHost.ServiceModel.Channels
+{
+ internal abstract class InternalBufferManager
+ {
+ protected InternalBufferManager()
+ {
+ }
+
+ public abstract byte[] TakeBuffer(int bufferSize);
+ public abstract void ReturnBuffer(byte[] buffer);
+ public abstract void Clear();
+
+ public static InternalBufferManager Create(long maxBufferPoolSize, int maxBufferSize)
+ {
+ if (maxBufferPoolSize == 0)
+ {
+ return GCBufferManager.Value;
+ }
+ else
+ {
+ Debug.Assert(maxBufferPoolSize > 0 && maxBufferSize >= 0, "bad params, caller should verify");
+ return new PooledBufferManager(maxBufferPoolSize, maxBufferSize);
+ }
+ }
+
+ private static byte[] AllocateByteArray(int size)
+ {
+ try
+ {
+ // Safe to catch OOM from this as long as the ONLY thing it does is a simple allocation of a primitive type (no method calls).
+ return new byte[size];
+ }
+ catch (OutOfMemoryException exception)
+ {
+ // Convert OOM into an exception that can be safely handled by higher layers.
+ throw new InsufficientMemoryException(Error.Format(SRResources.BufferAllocationFailed, size), exception);
+ }
+ }
+
+ private class GCBufferManager : InternalBufferManager
+ {
+ private static readonly GCBufferManager _value = new GCBufferManager();
+
+ private GCBufferManager()
+ {
+ }
+
+ public static GCBufferManager Value
+ {
+ get { return _value; }
+ }
+
+ public override void Clear()
+ {
+ }
+
+ public override byte[] TakeBuffer(int bufferSize)
+ {
+ return AllocateByteArray(bufferSize);
+ }
+
+ public override void ReturnBuffer(byte[] buffer)
+ {
+ // do nothing, GC will reclaim this buffer
+ }
+ }
+
+ private class PooledBufferManager : InternalBufferManager
+ {
+ private const int MinBufferSize = 128;
+ private const int MaxMissesBeforeTuning = 8;
+ private const int InitialBufferCount = 1;
+ private readonly object _tuningLock;
+
+ private int[] _bufferSizes;
+ private BufferPool[] _bufferPools;
+ private long _remainingMemory;
+ private bool _areQuotasBeingTuned;
+ private int _totalMisses;
+
+ public PooledBufferManager(long maxMemoryToPool, int maxBufferSize)
+ {
+ _tuningLock = new object();
+ _remainingMemory = maxMemoryToPool;
+ List<BufferPool> bufferPoolList = new List<BufferPool>();
+
+ int bufferSize = MinBufferSize;
+ while (true)
+ {
+ long bufferCountLong = _remainingMemory / bufferSize;
+
+ int bufferCount = bufferCountLong > Int32.MaxValue ? Int32.MaxValue : (int)bufferCountLong;
+
+ if (bufferCount > InitialBufferCount)
+ {
+ bufferCount = InitialBufferCount;
+ }
+
+ bufferPoolList.Add(BufferPool.CreatePool(bufferSize, bufferCount));
+
+ _remainingMemory -= (long)bufferCount * bufferSize;
+
+ if (bufferSize >= maxBufferSize)
+ {
+ break;
+ }
+
+ long newBufferSizeLong = (long)bufferSize * 2;
+
+ if (newBufferSizeLong > (long)maxBufferSize)
+ {
+ bufferSize = maxBufferSize;
+ }
+ else
+ {
+ bufferSize = (int)newBufferSizeLong;
+ }
+ }
+
+ _bufferPools = bufferPoolList.ToArray();
+ _bufferSizes = new int[_bufferPools.Length];
+ for (int i = 0; i < _bufferPools.Length; i++)
+ {
+ _bufferSizes[i] = _bufferPools[i].BufferSize;
+ }
+ }
+
+ public override void Clear()
+ {
+ for (int i = 0; i < _bufferPools.Length; i++)
+ {
+ BufferPool bufferPool = _bufferPools[i];
+ bufferPool.Clear();
+ }
+ }
+
+ private void ChangeQuota(ref BufferPool bufferPool, int delta)
+ {
+ BufferPool oldBufferPool = bufferPool;
+ int newLimit = oldBufferPool.Limit + delta;
+ BufferPool newBufferPool = BufferPool.CreatePool(oldBufferPool.BufferSize, newLimit);
+ for (int i = 0; i < newLimit; i++)
+ {
+ byte[] buffer = oldBufferPool.Take();
+ if (buffer == null)
+ {
+ break;
+ }
+ newBufferPool.Return(buffer);
+ newBufferPool.IncrementCount();
+ }
+ _remainingMemory -= oldBufferPool.BufferSize * delta;
+ bufferPool = newBufferPool;
+ }
+
+ private void DecreaseQuota(ref BufferPool bufferPool)
+ {
+ ChangeQuota(ref bufferPool, -1);
+ }
+
+ private int FindMostExcessivePool()
+ {
+ long maxBytesInExcess = 0;
+ int index = -1;
+
+ for (int i = 0; i < _bufferPools.Length; i++)
+ {
+ BufferPool bufferPool = _bufferPools[i];
+
+ if (bufferPool.Peak < bufferPool.Limit)
+ {
+ long bytesInExcess = (bufferPool.Limit - bufferPool.Peak) * (long)bufferPool.BufferSize;
+
+ if (bytesInExcess > maxBytesInExcess)
+ {
+ index = i;
+ maxBytesInExcess = bytesInExcess;
+ }
+ }
+ }
+
+ return index;
+ }
+
+ private int FindMostStarvedPool()
+ {
+ long maxBytesMissed = 0;
+ int index = -1;
+
+ for (int i = 0; i < _bufferPools.Length; i++)
+ {
+ BufferPool bufferPool = _bufferPools[i];
+
+ if (bufferPool.Peak == bufferPool.Limit)
+ {
+ long bytesMissed = bufferPool.Misses * (long)bufferPool.BufferSize;
+
+ if (bytesMissed > maxBytesMissed)
+ {
+ index = i;
+ maxBytesMissed = bytesMissed;
+ }
+ }
+ }
+
+ return index;
+ }
+
+ private BufferPool FindPool(int desiredBufferSize)
+ {
+ for (int i = 0; i < _bufferSizes.Length; i++)
+ {
+ if (desiredBufferSize <= _bufferSizes[i])
+ {
+ return _bufferPools[i];
+ }
+ }
+
+ return null;
+ }
+
+ private void IncreaseQuota(ref BufferPool bufferPool)
+ {
+ ChangeQuota(ref bufferPool, 1);
+ }
+
+ public override void ReturnBuffer(byte[] buffer)
+ {
+ Debug.Assert(buffer != null, "caller must verify");
+ BufferPool bufferPool = FindPool(buffer.Length);
+ if (bufferPool != null)
+ {
+ if (buffer.Length != bufferPool.BufferSize)
+ {
+ throw Error.Argument("buffer", SRResources.BufferIsNotRightSizeForBufferManager);
+ }
+
+ if (bufferPool.Return(buffer))
+ {
+ bufferPool.IncrementCount();
+ }
+ }
+ }
+
+ public override byte[] TakeBuffer(int bufferSize)
+ {
+ Debug.Assert(bufferSize >= 0, "caller must ensure a non-negative argument");
+
+ BufferPool bufferPool = FindPool(bufferSize);
+ if (bufferPool != null)
+ {
+ byte[] buffer = bufferPool.Take();
+ if (buffer != null)
+ {
+ bufferPool.DecrementCount();
+ return buffer;
+ }
+ if (bufferPool.Peak == bufferPool.Limit)
+ {
+ bufferPool.Misses++;
+ if (++_totalMisses >= MaxMissesBeforeTuning)
+ {
+ TuneQuotas();
+ }
+ }
+
+ return AllocateByteArray(bufferPool.BufferSize);
+ }
+ else
+ {
+ return AllocateByteArray(bufferSize);
+ }
+ }
+
+ private void TuneQuotas()
+ {
+ if (_areQuotasBeingTuned)
+ {
+ return;
+ }
+
+ bool lockHeld = false;
+ try
+ {
+ Monitor.TryEnter(_tuningLock, ref lockHeld);
+
+ // Don't bother if another thread already has the lock
+ if (!lockHeld || _areQuotasBeingTuned)
+ {
+ return;
+ }
+
+ _areQuotasBeingTuned = true;
+ }
+ finally
+ {
+ if (lockHeld)
+ {
+ Monitor.Exit(_tuningLock);
+ }
+ }
+
+ // find the "poorest" pool
+ int starvedIndex = FindMostStarvedPool();
+ if (starvedIndex >= 0)
+ {
+ BufferPool starvedBufferPool = _bufferPools[starvedIndex];
+
+ if (_remainingMemory < starvedBufferPool.BufferSize)
+ {
+ // find the "richest" pool
+ int excessiveIndex = FindMostExcessivePool();
+ if (excessiveIndex >= 0)
+ {
+ // steal from the richest
+ DecreaseQuota(ref _bufferPools[excessiveIndex]);
+ }
+ }
+
+ if (_remainingMemory >= starvedBufferPool.BufferSize)
+ {
+ // give to the poorest
+ IncreaseQuota(ref _bufferPools[starvedIndex]);
+ }
+ }
+
+ // reset statistics
+ for (int i = 0; i < _bufferPools.Length; i++)
+ {
+ BufferPool bufferPool = _bufferPools[i];
+ bufferPool.Misses = 0;
+ }
+
+ _totalMisses = 0;
+ _areQuotasBeingTuned = false;
+ }
+
+ private abstract class BufferPool
+ {
+ private int _bufferSize;
+ private int _count;
+ private int _limit;
+ private int _peak;
+
+ public BufferPool(int bufferSize, int limit)
+ {
+ _bufferSize = bufferSize;
+ _limit = limit;
+ }
+
+ public int BufferSize
+ {
+ get { return _bufferSize; }
+ }
+
+ public int Limit
+ {
+ get { return _limit; }
+ }
+
+ public int Misses { get; set; }
+
+ public int Peak
+ {
+ get { return _peak; }
+ }
+
+ public void Clear()
+ {
+ OnClear();
+ _count = 0;
+ }
+
+ public void DecrementCount()
+ {
+ int newValue = _count - 1;
+ if (newValue >= 0)
+ {
+ _count = newValue;
+ }
+ }
+
+ public void IncrementCount()
+ {
+ int newValue = _count + 1;
+ if (newValue <= _limit)
+ {
+ _count = newValue;
+ if (newValue > _peak)
+ {
+ _peak = newValue;
+ }
+ }
+ }
+
+ internal abstract byte[] Take();
+ internal abstract bool Return(byte[] buffer);
+ internal abstract void OnClear();
+
+ internal static BufferPool CreatePool(int bufferSize, int limit)
+ {
+ // To avoid many buffer drops during training of large objects which
+ // get allocated on the LOH, we use the LargeBufferPool and for
+ // bufferSize < 85000, the SynchronizedPool. However if bufferSize < 85000
+ // and (bufferSize + array-overhead) > 85000, this would still use
+ // the SynchronizedPool even though it is allocated on the LOH.
+ if (bufferSize < 85000)
+ {
+ return new SynchronizedBufferPool(bufferSize, limit);
+ }
+ else
+ {
+ return new LargeBufferPool(bufferSize, limit);
+ }
+ }
+
+ private class LargeBufferPool : BufferPool
+ {
+ private Stack<byte[]> _items;
+
+ internal LargeBufferPool(int bufferSize, int limit)
+ : base(bufferSize, limit)
+ {
+ _items = new Stack<byte[]>(limit);
+ }
+
+ private object ThisLock
+ {
+ get { return _items; }
+ }
+
+ internal override void OnClear()
+ {
+ lock (ThisLock)
+ {
+ _items.Clear();
+ }
+ }
+
+ internal override byte[] Take()
+ {
+ lock (ThisLock)
+ {
+ if (_items.Count > 0)
+ {
+ return _items.Pop();
+ }
+ }
+
+ return null;
+ }
+
+ internal override bool Return(byte[] buffer)
+ {
+ lock (ThisLock)
+ {
+ if (_items.Count < Limit)
+ {
+ _items.Push(buffer);
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+
+ private class SynchronizedBufferPool : BufferPool
+ {
+ private SynchronizedPool<byte[]> _innerPool;
+
+ internal SynchronizedBufferPool(int bufferSize, int limit)
+ : base(bufferSize, limit)
+ {
+ _innerPool = new SynchronizedPool<byte[]>(limit);
+ }
+
+ internal override void OnClear()
+ {
+ _innerPool.Clear();
+ }
+
+ internal override byte[] Take()
+ {
+ return _innerPool.Take();
+ }
+
+ internal override bool Return(byte[] buffer)
+ {
+ return _innerPool.Return(buffer);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http.SelfHost/ServiceModel/Channels/LayeredChannel.cs b/src/System.Web.Http.SelfHost/ServiceModel/Channels/LayeredChannel.cs
new file mode 100644
index 00000000..cccb18d9
--- /dev/null
+++ b/src/System.Web.Http.SelfHost/ServiceModel/Channels/LayeredChannel.cs
@@ -0,0 +1,86 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.ServiceModel.Channels;
+
+namespace System.Web.Http.SelfHost.ServiceModel.Channels
+{
+ internal abstract class LayeredChannel<TInnerChannel> : ChannelBase
+ where TInnerChannel : class, IChannel
+ {
+ private TInnerChannel _innerChannel;
+ private EventHandler _onInnerChannelFaulted;
+
+ protected LayeredChannel(ChannelManagerBase channelManager, TInnerChannel innerChannel)
+ : base(channelManager)
+ {
+ Debug.Assert(innerChannel != null, "innerChannel cannot be null");
+
+ _innerChannel = innerChannel;
+ _onInnerChannelFaulted = new EventHandler(OnInnerChannelFaulted);
+ _innerChannel.Faulted += _onInnerChannelFaulted;
+ }
+
+ protected TInnerChannel InnerChannel
+ {
+ get { return _innerChannel; }
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "Existing public API")]
+ public override T GetProperty<T>()
+ {
+ T baseProperty = base.GetProperty<T>();
+ if (baseProperty != null)
+ {
+ return baseProperty;
+ }
+
+ return InnerChannel.GetProperty<T>();
+ }
+
+ protected override void OnClosing()
+ {
+ _innerChannel.Faulted -= _onInnerChannelFaulted;
+ base.OnClosing();
+ }
+
+ protected override void OnAbort()
+ {
+ _innerChannel.Abort();
+ }
+
+ protected override void OnClose(TimeSpan timeout)
+ {
+ _innerChannel.Close(timeout);
+ }
+
+ protected override IAsyncResult OnBeginClose(TimeSpan timeout, AsyncCallback callback, object state)
+ {
+ return _innerChannel.BeginClose(timeout, callback, state);
+ }
+
+ protected override void OnEndClose(IAsyncResult result)
+ {
+ _innerChannel.EndClose(result);
+ }
+
+ protected override void OnOpen(TimeSpan timeout)
+ {
+ _innerChannel.Open(timeout);
+ }
+
+ protected override IAsyncResult OnBeginOpen(TimeSpan timeout, AsyncCallback callback, object state)
+ {
+ return _innerChannel.BeginOpen(timeout, callback, state);
+ }
+
+ protected override void OnEndOpen(IAsyncResult result)
+ {
+ _innerChannel.EndOpen(result);
+ }
+
+ private void OnInnerChannelFaulted(object sender, EventArgs e)
+ {
+ Fault();
+ }
+ }
+}
diff --git a/src/System.Web.Http.SelfHost/ServiceModel/Channels/LayeredChannelAcceptor.cs b/src/System.Web.Http.SelfHost/ServiceModel/Channels/LayeredChannelAcceptor.cs
new file mode 100644
index 00000000..965b7a76
--- /dev/null
+++ b/src/System.Web.Http.SelfHost/ServiceModel/Channels/LayeredChannelAcceptor.cs
@@ -0,0 +1,65 @@
+using System.ServiceModel.Channels;
+
+namespace System.Web.Http.SelfHost.ServiceModel.Channels
+{
+ internal abstract class LayeredChannelAcceptor<TChannel, TInnerChannel> : ChannelAcceptor<TChannel>
+ where TChannel : class, IChannel
+ where TInnerChannel : class, IChannel
+ {
+ private IChannelListener<TInnerChannel> _innerListener;
+
+ protected LayeredChannelAcceptor(ChannelManagerBase channelManager, IChannelListener<TInnerChannel> innerListener)
+ : base(channelManager)
+ {
+ _innerListener = innerListener;
+ }
+
+ public override TChannel AcceptChannel(TimeSpan timeout)
+ {
+ TInnerChannel innerChannel = _innerListener.AcceptChannel(timeout);
+ if (innerChannel == null)
+ {
+ return null;
+ }
+ else
+ {
+ return OnAcceptChannel(innerChannel);
+ }
+ }
+
+ public override IAsyncResult BeginAcceptChannel(TimeSpan timeout, AsyncCallback callback, object state)
+ {
+ return _innerListener.BeginAcceptChannel(timeout, callback, state);
+ }
+
+ public override TChannel EndAcceptChannel(IAsyncResult result)
+ {
+ TInnerChannel innerChannel = _innerListener.EndAcceptChannel(result);
+ if (innerChannel == null)
+ {
+ return null;
+ }
+ else
+ {
+ return OnAcceptChannel(innerChannel);
+ }
+ }
+
+ public override bool WaitForChannel(TimeSpan timeout)
+ {
+ return _innerListener.WaitForChannel(timeout);
+ }
+
+ public override IAsyncResult BeginWaitForChannel(TimeSpan timeout, AsyncCallback callback, object state)
+ {
+ return _innerListener.BeginWaitForChannel(timeout, callback, state);
+ }
+
+ public override bool EndWaitForChannel(IAsyncResult result)
+ {
+ return _innerListener.EndWaitForChannel(result);
+ }
+
+ protected abstract TChannel OnAcceptChannel(TInnerChannel innerChannel);
+ }
+}
diff --git a/src/System.Web.Http.SelfHost/ServiceModel/Channels/LayeredChannelListener.cs b/src/System.Web.Http.SelfHost/ServiceModel/Channels/LayeredChannelListener.cs
new file mode 100644
index 00000000..d2b10dfe
--- /dev/null
+++ b/src/System.Web.Http.SelfHost/ServiceModel/Channels/LayeredChannelListener.cs
@@ -0,0 +1,298 @@
+using System.Diagnostics.CodeAnalysis;
+using System.ServiceModel;
+using System.ServiceModel.Channels;
+using System.Web.Http.Common;
+using System.Web.Http.SelfHost.Properties;
+
+namespace System.Web.Http.SelfHost.ServiceModel.Channels
+{
+ internal abstract class LayeredChannelListener<TChannel> : ChannelListenerBase<TChannel>
+ where TChannel : class, IChannel
+ {
+ private IChannelListener _innerChannelListener;
+ private bool _sharedInnerListener;
+ private EventHandler _onInnerListenerFaulted;
+
+ protected LayeredChannelListener(bool sharedInnerListener, IDefaultCommunicationTimeouts timeouts, IChannelListener innerChannelListener)
+ : base(timeouts)
+ {
+ _sharedInnerListener = sharedInnerListener;
+ _innerChannelListener = innerChannelListener;
+ _onInnerListenerFaulted = new EventHandler(OnInnerListenerFaulted);
+ if (_innerChannelListener != null)
+ {
+ _innerChannelListener.Faulted += _onInnerListenerFaulted;
+ }
+ }
+
+ protected LayeredChannelListener(bool sharedInnerListener, IDefaultCommunicationTimeouts timeouts)
+ : this(sharedInnerListener, timeouts, null)
+ {
+ }
+
+ protected LayeredChannelListener(bool sharedInnerListener)
+ : this(sharedInnerListener, null, null)
+ {
+ }
+
+ protected LayeredChannelListener(IDefaultCommunicationTimeouts timeouts, IChannelListener innerChannelListener)
+ : this(false, timeouts, innerChannelListener)
+ {
+ }
+
+ public override Uri Uri
+ {
+ get { return GetInnerListenerSnapshot().Uri; }
+ }
+
+ internal virtual IChannelListener InnerChannelListener
+ {
+ get { return _innerChannelListener; }
+
+ set
+ {
+ lock (ThisLock)
+ {
+ ThrowIfDisposedOrImmutable();
+ if (_innerChannelListener != null)
+ {
+ _innerChannelListener.Faulted -= _onInnerListenerFaulted;
+ }
+
+ _innerChannelListener = value;
+ if (_innerChannelListener != null)
+ {
+ _innerChannelListener.Faulted += _onInnerListenerFaulted;
+ }
+ }
+ }
+ }
+
+ internal bool SharedInnerListener
+ {
+ get { return _sharedInnerListener; }
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "Existing public API")]
+ public override T GetProperty<T>()
+ {
+ T baseProperty = base.GetProperty<T>();
+ if (baseProperty != null)
+ {
+ return baseProperty;
+ }
+
+ IChannelListener channelListener = InnerChannelListener;
+ if (channelListener != null)
+ {
+ return channelListener.GetProperty<T>();
+ }
+ else
+ {
+ return default(T);
+ }
+ }
+
+ internal void ThrowIfInnerListenerNotSet()
+ {
+ if (InnerChannelListener == null)
+ {
+ throw Error.InvalidOperation(SRResources.InnerListenerFactoryNotSet, GetType().ToString());
+ }
+ }
+
+ internal IChannelListener GetInnerListenerSnapshot()
+ {
+ IChannelListener innerListener = InnerChannelListener;
+
+ if (innerListener == null)
+ {
+ throw Error.InvalidOperation(SRResources.InnerListenerFactoryNotSet, GetType().ToString());
+ }
+
+ return innerListener;
+ }
+
+ protected override void OnOpening()
+ {
+ base.OnOpening();
+ ThrowIfInnerListenerNotSet();
+ }
+
+ protected override void OnOpen(TimeSpan timeout)
+ {
+ if (InnerChannelListener != null && !_sharedInnerListener)
+ {
+ InnerChannelListener.Open(timeout);
+ }
+ }
+
+ protected override void OnEndOpen(IAsyncResult result)
+ {
+ OpenAsyncResult.End(result);
+ }
+
+ protected override IAsyncResult OnBeginOpen(TimeSpan timeout, AsyncCallback callback, object state)
+ {
+ return new OpenAsyncResult(InnerChannelListener, _sharedInnerListener, timeout, callback, state);
+ }
+
+ protected override void OnClose(TimeSpan timeout)
+ {
+ OnCloseOrAbort();
+ if (InnerChannelListener != null && !_sharedInnerListener)
+ {
+ InnerChannelListener.Close(timeout);
+ }
+ }
+
+ protected override void OnEndClose(IAsyncResult result)
+ {
+ CloseAsyncResult.End(result);
+ }
+
+ protected override IAsyncResult OnBeginClose(TimeSpan timeout, AsyncCallback callback, object state)
+ {
+ OnCloseOrAbort();
+ return new CloseAsyncResult(InnerChannelListener, _sharedInnerListener, timeout, callback, state);
+ }
+
+ protected override void OnAbort()
+ {
+ lock (ThisLock)
+ {
+ OnCloseOrAbort();
+ }
+
+ IChannelListener channelListener = InnerChannelListener;
+ if (channelListener != null && !_sharedInnerListener)
+ {
+ channelListener.Abort();
+ }
+ }
+
+ private void OnInnerListenerFaulted(object sender, EventArgs e)
+ {
+ // if our inner listener faulted, we should fault as well
+ Fault();
+ }
+
+ private void OnCloseOrAbort()
+ {
+ IChannelListener channelListener = InnerChannelListener;
+ if (channelListener != null)
+ {
+ channelListener.Faulted -= _onInnerListenerFaulted;
+ }
+ }
+
+ private class CloseAsyncResult : AsyncResult
+ {
+ private static AsyncCallback _onCloseComplete = new AsyncCallback(OnCloseComplete);
+
+ private ICommunicationObject _communicationObject;
+
+ public CloseAsyncResult(ICommunicationObject communicationObject, bool sharedInnerListener, TimeSpan timeout, AsyncCallback callback, object state)
+ : base(callback, state)
+ {
+ _communicationObject = communicationObject;
+
+ if (_communicationObject == null || sharedInnerListener)
+ {
+ Complete(true);
+ return;
+ }
+
+ IAsyncResult result = _communicationObject.BeginClose(timeout, _onCloseComplete, this);
+
+ if (result.CompletedSynchronously)
+ {
+ _communicationObject.EndClose(result);
+ Complete(true);
+ }
+ }
+
+ public static void End(IAsyncResult result)
+ {
+ AsyncResult.End<CloseAsyncResult>(result);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is propagated.")]
+ private static void OnCloseComplete(IAsyncResult result)
+ {
+ if (result.CompletedSynchronously)
+ {
+ return;
+ }
+
+ CloseAsyncResult thisPtr = (CloseAsyncResult)result.AsyncState;
+ Exception exception = null;
+
+ try
+ {
+ thisPtr._communicationObject.EndClose(result);
+ }
+ catch (Exception e)
+ {
+ exception = e;
+ }
+
+ thisPtr.Complete(false, exception);
+ }
+ }
+
+ private class OpenAsyncResult : AsyncResult
+ {
+ private static AsyncCallback _onOpenComplete = new AsyncCallback(OnOpenComplete);
+
+ private ICommunicationObject _communicationObject;
+
+ public OpenAsyncResult(ICommunicationObject communicationObject, bool sharedInnerListener, TimeSpan timeout, AsyncCallback callback, object state)
+ : base(callback, state)
+ {
+ _communicationObject = communicationObject;
+
+ if (_communicationObject == null || sharedInnerListener)
+ {
+ Complete(true);
+ return;
+ }
+
+ IAsyncResult result = _communicationObject.BeginOpen(timeout, _onOpenComplete, this);
+ if (result.CompletedSynchronously)
+ {
+ _communicationObject.EndOpen(result);
+ Complete(true);
+ }
+ }
+
+ public static void End(IAsyncResult result)
+ {
+ AsyncResult.End<OpenAsyncResult>(result);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is propagated.")]
+ private static void OnOpenComplete(IAsyncResult result)
+ {
+ if (result.CompletedSynchronously)
+ {
+ return;
+ }
+
+ OpenAsyncResult thisPtr = (OpenAsyncResult)result.AsyncState;
+ Exception exception = null;
+
+ try
+ {
+ thisPtr._communicationObject.EndOpen(result);
+ }
+ catch (Exception e)
+ {
+ exception = e;
+ }
+
+ thisPtr.Complete(false, exception);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http.SelfHost/ServiceModel/Channels/SynchronizedPool.cs b/src/System.Web.Http.SelfHost/ServiceModel/Channels/SynchronizedPool.cs
new file mode 100644
index 00000000..d071588a
--- /dev/null
+++ b/src/System.Web.Http.SelfHost/ServiceModel/Channels/SynchronizedPool.cs
@@ -0,0 +1,418 @@
+using System.Collections.Generic;
+using System.Threading;
+
+namespace System.Web.Http.SelfHost.ServiceModel.Channels
+{
+ // A simple synchronized pool would simply lock a stack and push/pop on return/take.
+ //
+ // This implementation tries to reduce locking by exploiting the case where an item
+ // is taken and returned by the same thread, which turns out to be common in our
+ // scenarios.
+ //
+ // Initially, all the quota is allocated to a global (non-thread-specific) pool,
+ // which takes locks. As different threads take and return values, we record their IDs,
+ // and if we detect that a thread is taking and returning "enough" on the same thread,
+ // then we decide to "promote" the thread. When a thread is promoted, we decrease the
+ // quota of the global pool by one, and allocate a thread-specific entry for the thread
+ // to store it's value. Once this entry is allocated, the thread can take and return
+ // it's value from that entry without taking any locks. Not only does this avoid
+ // locks, but it affinitizes pooled items to a particular thread.
+ //
+ // There are a couple of additional things worth noting:
+ //
+ // It is possible for a thread that we have reserved an entry for to exit. This means
+ // we will still have a entry allocated for it, but the pooled item stored there
+ // will never be used. After a while, we could end up with a number of these, and
+ // as a result we would begin to exhaust the quota of the overall pool. To mitigate this
+ // case, we throw away the entire per-thread pool, and return all the quota back to
+ // the global pool if we are unable to promote a thread (due to lack of space). Then
+ // the set of active threads will be re-promoted as they take and return items.
+ //
+ // You may notice that the code does not immediately promote a thread, and does not
+ // immediately throw away the entire per-thread pool when it is unable to promote a
+ // thread. Instead, it uses counters (based on the number of calls to the pool)
+ // and a threshold to figure out when to do these operations. In the case where the
+ // pool to misconfigured to have too few items for the workload, this avoids constant
+ // promoting and rebuilding of the per thread entries.
+ //
+ // You may also notice that we do not use interlocked methods when adjusting statistics.
+ // Since the statistics are a heuristic as to how often something is happening, they
+ // do not need to be perfect.
+ internal class SynchronizedPool<T> where T : class
+ {
+ private const int MaxPendingEntries = 128;
+ private const int MaxPromotionFailures = 64;
+ private const int MaxReturnsBeforePromotion = 64;
+ private const int MaxThreadItemsPerProcessor = 16;
+
+ private Entry[] _entries;
+ private GlobalPool _globalPool;
+ private int _maxCount;
+ private PendingEntry[] _pending;
+ private int _promotionFailures;
+
+ public SynchronizedPool(int maxCount)
+ {
+ int threadCount = maxCount;
+ int maxThreadCount = MaxThreadItemsPerProcessor + SynchronizedPoolHelper.ProcessorCount;
+ if (threadCount > maxThreadCount)
+ {
+ threadCount = maxThreadCount;
+ }
+ _maxCount = maxCount;
+ _entries = new Entry[threadCount];
+ _pending = new PendingEntry[4];
+ _globalPool = new GlobalPool(maxCount);
+ }
+
+ private object ThisLock
+ {
+ get { return this; }
+ }
+
+ public void Clear()
+ {
+ Entry[] entriesArray = _entries;
+
+ for (int i = 0; i < entriesArray.Length; i++)
+ {
+ entriesArray[i].Value = null;
+ }
+
+ _globalPool.Clear();
+ }
+
+ private void HandlePromotionFailure(int thisThreadID)
+ {
+ int newPromotionFailures = _promotionFailures + 1;
+
+ if (newPromotionFailures >= MaxPromotionFailures)
+ {
+ lock (ThisLock)
+ {
+ _entries = new Entry[_entries.Length];
+
+ _globalPool.MaxCount = _maxCount;
+ }
+
+ PromoteThread(thisThreadID);
+ }
+ else
+ {
+ _promotionFailures = newPromotionFailures;
+ }
+ }
+
+ private bool PromoteThread(int thisThreadID)
+ {
+ lock (ThisLock)
+ {
+ for (int i = 0; i < _entries.Length; i++)
+ {
+ int threadID = _entries[i].ThreadId;
+
+ if (threadID == thisThreadID)
+ {
+ return true;
+ }
+ else if (threadID == 0)
+ {
+ _globalPool.DecrementMaxCount();
+ _entries[i].ThreadId = thisThreadID;
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private void RecordReturnToGlobalPool(int thisThreadID)
+ {
+ PendingEntry[] localPending = _pending;
+
+ for (int i = 0; i < localPending.Length; i++)
+ {
+ int threadID = localPending[i].ThreadId;
+
+ if (threadID == thisThreadID)
+ {
+ int newReturnCount = localPending[i].ReturnCount + 1;
+
+ if (newReturnCount >= MaxReturnsBeforePromotion)
+ {
+ localPending[i].ReturnCount = 0;
+
+ if (!PromoteThread(thisThreadID))
+ {
+ HandlePromotionFailure(thisThreadID);
+ }
+ }
+ else
+ {
+ localPending[i].ReturnCount = newReturnCount;
+ }
+ break;
+ }
+ else if (threadID == 0)
+ {
+ break;
+ }
+ }
+ }
+
+ private void RecordTakeFromGlobalPool(int thisThreadID)
+ {
+ PendingEntry[] localPending = _pending;
+
+ for (int i = 0; i < localPending.Length; i++)
+ {
+ int threadID = localPending[i].ThreadId;
+
+ if (threadID == thisThreadID)
+ {
+ return;
+ }
+ else if (threadID == 0)
+ {
+ lock (localPending)
+ {
+ if (localPending[i].ThreadId == 0)
+ {
+ localPending[i].ThreadId = thisThreadID;
+ return;
+ }
+ }
+ }
+ }
+
+ if (localPending.Length >= MaxPendingEntries)
+ {
+ _pending = new PendingEntry[localPending.Length];
+ }
+ else
+ {
+ PendingEntry[] newPending = new PendingEntry[localPending.Length * 2];
+ Array.Copy(localPending, newPending, localPending.Length);
+ _pending = newPending;
+ }
+ }
+
+ public bool Return(T value)
+ {
+ int thisThreadID = Thread.CurrentThread.ManagedThreadId;
+
+ if (thisThreadID == 0)
+ {
+ return false;
+ }
+
+ if (ReturnToPerThreadPool(thisThreadID, value))
+ {
+ return true;
+ }
+
+ return ReturnToGlobalPool(thisThreadID, value);
+ }
+
+ private bool ReturnToPerThreadPool(int thisThreadID, T value)
+ {
+ Entry[] entriesArray = _entries;
+
+ for (int i = 0; i < entriesArray.Length; i++)
+ {
+ int threadID = entriesArray[i].ThreadId;
+
+ if (threadID == thisThreadID)
+ {
+ if (entriesArray[i].Value == null)
+ {
+ entriesArray[i].Value = value;
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+ else if (threadID == 0)
+ {
+ break;
+ }
+ }
+
+ return false;
+ }
+
+ private bool ReturnToGlobalPool(int thisThreadID, T value)
+ {
+ RecordReturnToGlobalPool(thisThreadID);
+
+ return _globalPool.Return(value);
+ }
+
+ public T Take()
+ {
+ int thisThreadID = Thread.CurrentThread.ManagedThreadId;
+
+ if (thisThreadID == 0)
+ {
+ return null;
+ }
+
+ T value = TakeFromPerThreadPool(thisThreadID);
+
+ if (value != null)
+ {
+ return value;
+ }
+
+ return TakeFromGlobalPool(thisThreadID);
+ }
+
+ private T TakeFromPerThreadPool(int thisThreadID)
+ {
+ Entry[] entriesArray = _entries;
+
+ for (int i = 0; i < entriesArray.Length; i++)
+ {
+ int threadID = entriesArray[i].ThreadId;
+
+ if (threadID == thisThreadID)
+ {
+ T value = entriesArray[i].Value;
+
+ if (value != null)
+ {
+ entriesArray[i].Value = null;
+ return value;
+ }
+ else
+ {
+ return null;
+ }
+ }
+ else if (threadID == 0)
+ {
+ break;
+ }
+ }
+
+ return null;
+ }
+
+ private T TakeFromGlobalPool(int thisThreadID)
+ {
+ RecordTakeFromGlobalPool(thisThreadID);
+
+ return _globalPool.Take();
+ }
+
+ private struct Entry
+ {
+ public int ThreadId;
+ public T Value;
+ }
+
+ private struct PendingEntry
+ {
+ public int ReturnCount;
+ public int ThreadId;
+ }
+
+ private class GlobalPool
+ {
+ private Stack<T> _items;
+
+ private int _maxCount;
+
+ public GlobalPool(int maxCount)
+ {
+ _items = new Stack<T>();
+ _maxCount = maxCount;
+ }
+
+ public int MaxCount
+ {
+ get { return _maxCount; }
+ set
+ {
+ lock (ThisLock)
+ {
+ while (_items.Count > value)
+ {
+ _items.Pop();
+ }
+ _maxCount = value;
+ }
+ }
+ }
+
+ private object ThisLock
+ {
+ get { return this; }
+ }
+
+ public void DecrementMaxCount()
+ {
+ lock (ThisLock)
+ {
+ if (_items.Count == _maxCount)
+ {
+ _items.Pop();
+ }
+ _maxCount--;
+ }
+ }
+
+ public T Take()
+ {
+ if (_items.Count > 0)
+ {
+ lock (ThisLock)
+ {
+ if (_items.Count > 0)
+ {
+ return _items.Pop();
+ }
+ }
+ }
+ return null;
+ }
+
+ public bool Return(T value)
+ {
+ if (_items.Count < MaxCount)
+ {
+ lock (ThisLock)
+ {
+ if (_items.Count < MaxCount)
+ {
+ _items.Push(value);
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ public void Clear()
+ {
+ lock (ThisLock)
+ {
+ _items.Clear();
+ }
+ }
+ }
+
+ private static class SynchronizedPoolHelper
+ {
+ public static readonly int ProcessorCount = GetProcessorCount();
+
+ private static int GetProcessorCount()
+ {
+ return Environment.ProcessorCount;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http.SelfHost/ServiceModel/Channels/TransportDefaults.cs b/src/System.Web.Http.SelfHost/ServiceModel/Channels/TransportDefaults.cs
new file mode 100644
index 00000000..0b669b1d
--- /dev/null
+++ b/src/System.Web.Http.SelfHost/ServiceModel/Channels/TransportDefaults.cs
@@ -0,0 +1,10 @@
+namespace System.Web.Http.SelfHost.ServiceModel.Channels
+{
+ internal static class TransportDefaults
+ {
+ internal const long MaxReceivedMessageSize = 65536;
+ internal const long MaxBufferPoolSize = 512 * 1024;
+ internal const int MaxBufferSize = (int)MaxReceivedMessageSize;
+ internal const int MaxFaultSize = MaxBufferSize;
+ }
+}
diff --git a/src/System.Web.Http.SelfHost/ServiceModel/HostNameComparisonModeHelper.cs b/src/System.Web.Http.SelfHost/ServiceModel/HostNameComparisonModeHelper.cs
new file mode 100644
index 00000000..97d503ca
--- /dev/null
+++ b/src/System.Web.Http.SelfHost/ServiceModel/HostNameComparisonModeHelper.cs
@@ -0,0 +1,24 @@
+using System.ServiceModel;
+using System.Web.Http.Common;
+
+namespace System.Web.Http.SelfHost.ServiceModel
+{
+ internal static class HostNameComparisonModeHelper
+ {
+ public static bool IsDefined(HostNameComparisonMode value)
+ {
+ return
+ value == HostNameComparisonMode.StrongWildcard
+ || value == HostNameComparisonMode.Exact
+ || value == HostNameComparisonMode.WeakWildcard;
+ }
+
+ public static void Validate(HostNameComparisonMode value)
+ {
+ if (!IsDefined(value))
+ {
+ throw Error.InvalidEnumArgument("value", (int)value, typeof(HostNameComparisonMode));
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http.SelfHost/ServiceModel/HttpClientCredentialTypeHelper.cs b/src/System.Web.Http.SelfHost/ServiceModel/HttpClientCredentialTypeHelper.cs
new file mode 100644
index 00000000..c1c28b44
--- /dev/null
+++ b/src/System.Web.Http.SelfHost/ServiceModel/HttpClientCredentialTypeHelper.cs
@@ -0,0 +1,34 @@
+using System.Diagnostics;
+using System.Net;
+using System.ServiceModel;
+
+namespace System.Web.Http.SelfHost.ServiceModel
+{
+ internal static class HttpClientCredentialTypeHelper
+ {
+ internal static AuthenticationSchemes MapToAuthenticationScheme(HttpClientCredentialType clientCredentialType)
+ {
+ switch (clientCredentialType)
+ {
+ case HttpClientCredentialType.None:
+ case HttpClientCredentialType.Certificate:
+ return AuthenticationSchemes.Anonymous;
+
+ case HttpClientCredentialType.Basic:
+ return AuthenticationSchemes.Basic;
+
+ case HttpClientCredentialType.Digest:
+ return AuthenticationSchemes.Digest;
+
+ case HttpClientCredentialType.Ntlm:
+ return AuthenticationSchemes.Ntlm;
+
+ case HttpClientCredentialType.Windows:
+ return AuthenticationSchemes.Negotiate;
+ }
+
+ Debug.Assert(false, "Invalid clientCredentialType " + clientCredentialType);
+ return AuthenticationSchemes.Anonymous;
+ }
+ }
+}
diff --git a/src/System.Web.Http.SelfHost/ServiceModel/HttpProxyCredentialTypeHelper.cs b/src/System.Web.Http.SelfHost/ServiceModel/HttpProxyCredentialTypeHelper.cs
new file mode 100644
index 00000000..4fc0d159
--- /dev/null
+++ b/src/System.Web.Http.SelfHost/ServiceModel/HttpProxyCredentialTypeHelper.cs
@@ -0,0 +1,33 @@
+using System.Diagnostics;
+using System.Net;
+using System.ServiceModel;
+
+namespace System.Web.Http.SelfHost.ServiceModel
+{
+ internal class HttpProxyCredentialTypeHelper
+ {
+ internal static AuthenticationSchemes MapToAuthenticationScheme(HttpProxyCredentialType proxyCredentialType)
+ {
+ switch (proxyCredentialType)
+ {
+ case HttpProxyCredentialType.None:
+ return AuthenticationSchemes.Anonymous;
+
+ case HttpProxyCredentialType.Basic:
+ return AuthenticationSchemes.Basic;
+
+ case HttpProxyCredentialType.Digest:
+ return AuthenticationSchemes.Digest;
+
+ case HttpProxyCredentialType.Ntlm:
+ return AuthenticationSchemes.Ntlm;
+
+ case HttpProxyCredentialType.Windows:
+ return AuthenticationSchemes.Negotiate;
+ }
+
+ Debug.Assert(false, "Invalid proxyCredentialType " + proxyCredentialType);
+ return AuthenticationSchemes.Anonymous;
+ }
+ }
+}
diff --git a/src/System.Web.Http.SelfHost/ServiceModel/HttpTransportSecurityExtensionMethods.cs b/src/System.Web.Http.SelfHost/ServiceModel/HttpTransportSecurityExtensionMethods.cs
new file mode 100644
index 00000000..55751cb8
--- /dev/null
+++ b/src/System.Web.Http.SelfHost/ServiceModel/HttpTransportSecurityExtensionMethods.cs
@@ -0,0 +1,56 @@
+using System.Diagnostics;
+using System.Net;
+using System.ServiceModel;
+using System.ServiceModel.Channels;
+using System.Web.Http.Common;
+using System.Web.Http.SelfHost.Properties;
+
+namespace System.Web.Http.SelfHost.ServiceModel
+{
+ internal static class HttpTransportSecurityExtensionMethods
+ {
+ internal static void ConfigureTransportProtectionAndAuthentication(this HttpTransportSecurity httpTransportSecurity, HttpsTransportBindingElement httpsTransportBindingElement)
+ {
+ Debug.Assert(httpTransportSecurity != null, "httpTransportSecurity cannot be null");
+ Debug.Assert(httpsTransportBindingElement != null, "httpsTransportBindingElement cannot be null");
+
+ httpTransportSecurity.ConfigureAuthentication(httpsTransportBindingElement);
+ httpsTransportBindingElement.RequireClientCertificate = httpTransportSecurity.ClientCredentialType == HttpClientCredentialType.Certificate;
+ }
+
+ internal static void ConfigureTransportAuthentication(this HttpTransportSecurity httpTransportSecurity, HttpTransportBindingElement httpTransportBindingElement)
+ {
+ Debug.Assert(httpTransportSecurity != null, "httpTransportSecurity cannot be null");
+ Debug.Assert(httpTransportBindingElement != null, "httpTransportBindingElement cannot be null");
+
+ if (httpTransportSecurity.ClientCredentialType == HttpClientCredentialType.Certificate)
+ {
+ throw Error.InvalidOperation(SRResources.CertificateUnsupportedForHttpTransportCredentialOnly);
+ }
+
+ httpTransportSecurity.ConfigureAuthentication(httpTransportBindingElement);
+ }
+
+ internal static void DisableTransportAuthentication(this HttpTransportSecurity httpTransportSecurity, HttpTransportBindingElement httpTransportBindingElement)
+ {
+ Debug.Assert(httpTransportSecurity != null, "httpTransportSecurity cannot be null");
+ Debug.Assert(httpTransportBindingElement != null, "httpTransportBindingElement cannot be null");
+
+ httpTransportBindingElement.AuthenticationScheme = AuthenticationSchemes.Anonymous;
+ httpTransportBindingElement.ProxyAuthenticationScheme = AuthenticationSchemes.Anonymous;
+ httpTransportBindingElement.Realm = String.Empty;
+ httpTransportBindingElement.ExtendedProtectionPolicy = httpTransportSecurity.ExtendedProtectionPolicy;
+ }
+
+ private static void ConfigureAuthentication(this HttpTransportSecurity httpTransportSecurity, HttpTransportBindingElement httpTransportBindingElement)
+ {
+ Debug.Assert(httpTransportSecurity != null, "httpTransportSecurity cannot be null");
+ Debug.Assert(httpTransportBindingElement != null, "httpTransportBindingElement cannot be null");
+
+ httpTransportBindingElement.AuthenticationScheme = HttpClientCredentialTypeHelper.MapToAuthenticationScheme(httpTransportSecurity.ClientCredentialType);
+ httpTransportBindingElement.ProxyAuthenticationScheme = HttpProxyCredentialTypeHelper.MapToAuthenticationScheme(httpTransportSecurity.ProxyCredentialType);
+ httpTransportBindingElement.Realm = httpTransportSecurity.Realm;
+ httpTransportBindingElement.ExtendedProtectionPolicy = httpTransportSecurity.ExtendedProtectionPolicy;
+ }
+ }
+}
diff --git a/src/System.Web.Http.SelfHost/ServiceModel/TransferModeHelper.cs b/src/System.Web.Http.SelfHost/ServiceModel/TransferModeHelper.cs
new file mode 100644
index 00000000..cc5be6d1
--- /dev/null
+++ b/src/System.Web.Http.SelfHost/ServiceModel/TransferModeHelper.cs
@@ -0,0 +1,34 @@
+using System.ServiceModel;
+using System.Web.Http.Common;
+
+namespace System.Web.Http.SelfHost.ServiceModel
+{
+ internal static class TransferModeHelper
+ {
+ public static bool IsDefined(TransferMode transferMode)
+ {
+ return transferMode == TransferMode.Buffered ||
+ transferMode == TransferMode.Streamed ||
+ transferMode == TransferMode.StreamedRequest ||
+ transferMode == TransferMode.StreamedResponse;
+ }
+
+ public static bool IsRequestStreamed(TransferMode transferMode)
+ {
+ return transferMode == TransferMode.StreamedRequest || transferMode == TransferMode.Streamed;
+ }
+
+ public static bool IsResponseStreamed(TransferMode transferMode)
+ {
+ return transferMode == TransferMode.StreamedResponse || transferMode == TransferMode.Streamed;
+ }
+
+ public static void Validate(TransferMode value)
+ {
+ if (!IsDefined(value))
+ {
+ throw Error.InvalidEnumArgument("value", (int)value, typeof(TransferMode));
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http.SelfHost/System.Web.Http.SelfHost.csproj b/src/System.Web.Http.SelfHost/System.Web.Http.SelfHost.csproj
new file mode 100644
index 00000000..7c63a859
--- /dev/null
+++ b/src/System.Web.Http.SelfHost/System.Web.Http.SelfHost.csproj
@@ -0,0 +1,135 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <CodeAnalysis Condition=" '$(CodeAnalysis)' == '' ">false</CodeAnalysis>
+ <ProductVersion>9.0.30729</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{66492E69-CE4C-4FB1-9B1F-88DEE09D06F1}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>System.Web.Http.SelfHost</RootNamespace>
+ <AssemblyName>System.Web.Http.SelfHost</AssemblyName>
+ <FileAlignment>512</FileAlignment>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>..\..\bin\Debug\</OutputPath>
+ <DefineConstants>TRACE;DEBUG;ASPNETMVC</DefineConstants>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ <DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
+ <NoWarn>1591</NoWarn>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>..\..\bin\Release\</OutputPath>
+ <DefineConstants>TRACE;ASPNETMVC</DefineConstants>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ <RunCodeAnalysis>$(CodeAnalysis)</RunCodeAnalysis>
+ <DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
+ <NoWarn>1591</NoWarn>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'CodeCoverage|AnyCPU'">
+ <DebugSymbols>true</DebugSymbols>
+ <OutputPath>..\..\bin\CodeCoverage\</OutputPath>
+ <DefineConstants>TRACE;DEBUG;CODE_COVERAGE;ASPNETMVC</DefineConstants>
+ <DebugType>full</DebugType>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="System" />
+ <Reference Include="System.Configuration" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.IdentityModel" />
+ <Reference Include="System.Net.Http">
+ <HintPath>..\..\packages\Microsoft.Net.Http.2.0.20302.1\lib\net40\System.Net.Http.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Net.Http.WebRequest">
+ <HintPath>..\..\packages\Microsoft.Net.Http.2.0.20302.1\lib\net40\System.Net.Http.WebRequest.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Runtime.Serialization" />
+ <Reference Include="System.ServiceModel" />
+ <Reference Include="System.Xml" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="..\CommonAssemblyInfo.cs">
+ <Link>Properties\CommonAssemblyInfo.cs</Link>
+ </Compile>
+ <Compile Include="..\TransparentCommonAssemblyInfo.cs">
+ <Link>Properties\TransparentCommonAssemblyInfo.cs</Link>
+ </Compile>
+ <Compile Include="Properties\SRResources.Designer.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>SRResources.resx</DependentUpon>
+ </Compile>
+ <Compile Include="Channels\HttpMessage.cs" />
+ <Compile Include="Channels\HttpMessageEncoderFactory.cs" />
+ <Compile Include="Channels\HttpMessageEncodingBindingElement.cs" />
+ <Compile Include="Channels\HttpMessageEncodingChannelListener.cs" />
+ <Compile Include="Channels\HttpMessageEncodingReplyChannel.cs" />
+ <Compile Include="Channels\HttpMessageEncodingRequestContext.cs" />
+ <Compile Include="Channels\HttpMessageExtensions.cs" />
+ <Compile Include="Channels\HttpBinding.cs" />
+ <Compile Include="Channels\HttpBindingSecurity.cs" />
+ <Compile Include="Channels\HttpBindingSecurityMode.cs" />
+ <Compile Include="Channels\HttpBindingSecurityModeHelper.cs" />
+ <Compile Include="HttpSelfHostConfiguration.cs" />
+ <Compile Include="HttpSelfHostServer.cs" />
+ <Compile Include="ServiceModel\Activation\AspNetEnvironment.cs" />
+ <Compile Include="ServiceModel\Channels\AsyncResult.cs" />
+ <Compile Include="ServiceModel\Channels\BufferedOutputStream.cs" />
+ <Compile Include="ServiceModel\Channels\BufferManagerOutputStream.cs" />
+ <Compile Include="ServiceModel\Channels\ChannelAcceptor.cs" />
+ <Compile Include="ServiceModel\Channels\ChannelBindingUtility.cs" />
+ <Compile Include="ServiceModel\Channels\CompletedAsyncResult.cs" />
+ <Compile Include="ServiceModel\Channels\HttpTransportDefaults.cs" />
+ <Compile Include="ServiceModel\Channels\IChannelAcceptor.cs" />
+ <Compile Include="ServiceModel\Channels\InternalBufferManager.cs" />
+ <Compile Include="ServiceModel\Channels\LayeredChannel.cs" />
+ <Compile Include="ServiceModel\Channels\LayeredChannelAcceptor.cs" />
+ <Compile Include="ServiceModel\Channels\LayeredChannelListener.cs" />
+ <Compile Include="ServiceModel\Channels\SynchronizedPool.cs" />
+ <Compile Include="ServiceModel\Channels\TransportDefaults.cs" />
+ <Compile Include="ServiceModel\HostNameComparisonModeHelper.cs" />
+ <Compile Include="ServiceModel\HttpClientCredentialTypeHelper.cs" />
+ <Compile Include="ServiceModel\HttpProxyCredentialTypeHelper.cs" />
+ <Compile Include="ServiceModel\HttpTransportSecurityExtensionMethods.cs" />
+ <Compile Include="ServiceModel\TransferModeHelper.cs" />
+ <Compile Include="GlobalSuppressions.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="Properties\SRResources.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>SRResources.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\System.Web.Http.Common\System.Web.Http.Common.csproj">
+ <Project>{03A5E5F2-2E23-48F2-ABCC-6C41BAC9AC02}</Project>
+ <Name>System.Web.Http.Common</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\System.Web.Http\System.Web.Http.csproj">
+ <Project>{DDC1CE0C-486E-4E35-BB3B-EAB61F8F9440}</Project>
+ <Name>System.Web.Http</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <CodeAnalysisDictionary Include="..\CodeAnalysisDictionary.xml">
+ <Link>CodeAnalysisDictionary.xml</Link>
+ </CodeAnalysisDictionary>
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="packages.config" />
+ </ItemGroup>
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+</Project> \ No newline at end of file
diff --git a/src/System.Web.Http.SelfHost/packages.config b/src/System.Web.Http.SelfHost/packages.config
new file mode 100644
index 00000000..c611f43d
--- /dev/null
+++ b/src/System.Web.Http.SelfHost/packages.config
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="Microsoft.Net.Http" version="2.0.20302.1" />
+</packages> \ No newline at end of file
diff --git a/src/System.Web.Http.WebHost/GlobalConfiguration.cs b/src/System.Web.Http.WebHost/GlobalConfiguration.cs
new file mode 100644
index 00000000..ac34aa36
--- /dev/null
+++ b/src/System.Web.Http.WebHost/GlobalConfiguration.cs
@@ -0,0 +1,43 @@
+using System.Web.Http.Dispatcher;
+using System.Web.Http.WebHost;
+using System.Web.Http.WebHost.Routing;
+using System.Web.Routing;
+
+namespace System.Web.Http
+{
+ /// <summary>
+ /// Provides a global <see cref="T:System.Web.Http.HttpConfiguration"/> for ASP applications.
+ /// </summary>
+ public static class GlobalConfiguration
+ {
+ private static Lazy<HttpConfiguration> _configuration = new Lazy<HttpConfiguration>(
+ () =>
+ {
+ HttpConfiguration config = new HttpConfiguration(new HostedHttpRouteCollection(RouteTable.Routes));
+ config.ServiceResolver.SetService(typeof(IBuildManager), new WebHostBuildManager());
+ return config;
+ });
+
+ private static Lazy<HttpControllerDispatcher> _dispatcher = new Lazy<HttpControllerDispatcher>(
+ () =>
+ {
+ return new HttpControllerDispatcher(_configuration.Value);
+ });
+
+ /// <summary>
+ /// Gets the global <see cref="T:System.Web.Http.HttpConfiguration"/>.
+ /// </summary>
+ public static HttpConfiguration Configuration
+ {
+ get { return _configuration.Value; }
+ }
+
+ /// <summary>
+ /// Gets the global <see cref="T:System.Web.Http.Dispatcher.HttpControllerDispatcher"/>.
+ /// </summary>
+ public static HttpControllerDispatcher Dispatcher
+ {
+ get { return _dispatcher.Value; }
+ }
+ }
+}
diff --git a/src/System.Web.Http.WebHost/GlobalSuppressions.cs b/src/System.Web.Http.WebHost/GlobalSuppressions.cs
new file mode 100644
index 00000000..b6fd164a
--- /dev/null
+++ b/src/System.Web.Http.WebHost/GlobalSuppressions.cs
@@ -0,0 +1,7 @@
+using System.Diagnostics.CodeAnalysis;
+
+[assembly: SuppressMessage("Microsoft.Design", "CA2210:AssembliesShouldHaveValidStrongNames", Justification = "These assemblies are delay-signed.")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Justification = "Classes are grouped logically for user clarity.", Scope = "Namespace", Target = "System.Web.Http.WebHost")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Justification = "Classes are here so that they're shared with the main DLL's namespace", Scope = "Namespace", Target = "System.Web.Http")]
+[assembly: SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "System.Web.Http.GlobalConfiguration.#.cctor()")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "System.Web.Http.WebHost.Routing", Justification = "This is the most logical namespace for this type.")]
diff --git a/src/System.Web.Http.WebHost/HttpControllerHandler.cs b/src/System.Web.Http.WebHost/HttpControllerHandler.cs
new file mode 100644
index 00000000..fb3dd34c
--- /dev/null
+++ b/src/System.Web.Http.WebHost/HttpControllerHandler.cs
@@ -0,0 +1,299 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Common;
+using System.Web.Http.Hosting;
+using System.Web.Http.Routing;
+using System.Web.Http.WebHost.Properties;
+using System.Web.Http.WebHost.Routing;
+using System.Web.Routing;
+
+namespace System.Web.Http.WebHost
+{
+ /// <summary>
+ /// A <see cref="IHttpAsyncHandler"/> that passes ASP.NET requests into the <see cref="HttpServer"/>
+ /// pipeline and write the result back.
+ /// </summary>
+ public class HttpControllerHandler : IHttpAsyncHandler
+ {
+ internal static readonly string HttpContextBaseKey = "MS_HttpContext";
+
+ private static readonly Lazy<HttpServer> _server =
+ new Lazy<HttpServer>(() => new HttpServer(GlobalConfiguration.Configuration, GlobalConfiguration.Dispatcher));
+
+ private IHttpRouteData _routeData;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpControllerHandler"/> class.
+ /// </summary>
+ /// <param name="routeData">The route data.</param>
+ public HttpControllerHandler(RouteData routeData)
+ {
+ if (routeData == null)
+ {
+ throw Error.ArgumentNull("routeData");
+ }
+
+ _routeData = new HostedHttpRouteData(routeData);
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether another request can use the <see cref="T:System.Web.IHttpHandler"/> instance.
+ /// </summary>
+ /// <returns>true if the <see cref="T:System.Web.IHttpHandler"/> instance is reusable; otherwise, false.</returns>
+ bool IHttpHandler.IsReusable
+ {
+ get { return IsReusable; }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether another request can use the <see cref="T:System.Web.IHttpHandler"/> instance.
+ /// </summary>
+ /// <returns>true if the <see cref="T:System.Web.IHttpHandler"/> instance is reusable; otherwise, false.</returns>
+ protected virtual bool IsReusable
+ {
+ get { return false; }
+ }
+
+ /// <summary>
+ /// Processes the request.
+ /// </summary>
+ /// <param name="httpContext">The HTTP context base.</param>
+ void IHttpHandler.ProcessRequest(HttpContext httpContext)
+ {
+ ProcessRequest(new HttpContextWrapper(httpContext));
+ }
+
+ /// <summary>
+ /// Begins processing the request.
+ /// </summary>
+ /// <param name="httpContext">The HTTP context.</param>
+ /// <param name="callback">The callback.</param>
+ /// <param name="state">The state.</param>
+ /// <returns>An <see cref="IAsyncResult"/> that contains information about the status of the process. </returns>
+ IAsyncResult IHttpAsyncHandler.BeginProcessRequest(HttpContext httpContext, AsyncCallback callback, object state)
+ {
+ return BeginProcessRequest(new HttpContextWrapper(httpContext), callback, state);
+ }
+
+ /// <summary>
+ /// Provides an asynchronous process End method when the process ends.
+ /// </summary>
+ /// <param name="result">An <see cref="T:System.IAsyncResult"/> that contains information about the status of the process.</param>
+ void IHttpAsyncHandler.EndProcessRequest(IAsyncResult result)
+ {
+ EndProcessRequest(result);
+ }
+
+ /// <summary>
+ /// Processes the request.
+ /// </summary>
+ /// <param name="httpContextBase">The HTTP context base.</param>
+ protected virtual void ProcessRequest(HttpContextBase httpContextBase)
+ {
+ throw Error.NotSupported(SRResources.ProcessRequestNotSupported, typeof(HttpControllerHandler));
+ }
+
+ /// <summary>
+ /// Begins the process request.
+ /// </summary>
+ /// <param name="httpContextBase">The HTTP context base.</param>
+ /// <param name="callback">The callback.</param>
+ /// <param name="state">The state.</param>
+ /// <returns>An <see cref="IAsyncResult"/> that contains information about the status of the process. </returns>
+ [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "This is commented in great details.")]
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Object gets passed to a task")]
+ protected virtual IAsyncResult BeginProcessRequest(HttpContextBase httpContextBase, AsyncCallback callback, object state)
+ {
+ HttpRequestMessage request = ConvertRequest(httpContextBase);
+
+ // Add route data
+ request.Properties[HttpPropertyKeys.HttpRouteDataKey] = _routeData;
+
+ Task responseBodyTask = _server.Value.SubmitRequestAsync(request, CancellationToken.None)
+ .Then(response => ConvertResponse(httpContextBase, response, request))
+ .FastUnwrap();
+
+ TaskWrapperAsyncResult result = new TaskWrapperAsyncResult(responseBodyTask, state);
+
+ if (callback != null)
+ {
+ if (result.IsCompleted)
+ {
+ // If the underlying task is already finished, from our caller's perspective this is just
+ // a synchronous completion. See also DevDiv #346170.
+ result.CompletedSynchronously = true;
+ callback(result);
+ }
+ else
+ {
+ // If the underlying task isn't yet finished, from our caller's perspective this will be
+ // an asynchronous completion. We'll use ContinueWith instead of Finally for two reasons:
+ //
+ // - Finally propagates the antecedent Task's exception, which we don't need to do here.
+ // Out caller will eventually call EndProcessRequest, which correctly observes the
+ // antecedent Task's exception anyway if it faulted.
+ //
+ // - Finally invokes the callback on the captured SynchronizationContext, which is
+ // unnecessary when using APM (Begin / End). APM assumes that the callback is invoked
+ // on an arbitrary ThreadPool thread with no SynchronizationContext set up, so
+ // ContinueWith gets us closer to the desired semantic.
+ //
+ // There is still a race here: the Task might complete after the IsCompleted check above,
+ // so the callback might be invoked on another thread concurrently with the original
+ // thread's call to BeginProcessRequest. But we shouldn't concern ourselves with that;
+ // the caller has to be prepared for that possibility and do the right thing. We also
+ // don't need to worry about the callback throwing since the caller should give us a
+ // callback which is well-behaved.
+ result.CompletedSynchronously = false;
+ responseBodyTask.ContinueWith(_ =>
+ {
+ callback(result);
+ });
+ }
+ }
+
+ return result;
+ }
+
+ /// <summary>
+ /// Provides an asynchronous process End method when the process ends.
+ /// </summary>
+ /// <param name="result">An <see cref="T:System.IAsyncResult"/> that contains information about the status of the process.</param>
+ protected virtual void EndProcessRequest(IAsyncResult result)
+ {
+ TaskWrapperAsyncResult asyncResult = (TaskWrapperAsyncResult)result;
+ Contract.Assert(asyncResult != null);
+ Task task = asyncResult.Task;
+
+ // Check task result and unwrap any exceptions
+ if (task.IsCanceled)
+ {
+ throw Error.OperationCanceled();
+ }
+ else if (task.IsFaulted)
+ {
+ throw task.Exception.GetBaseException();
+ }
+ }
+
+ private static void CopyHeaders(HttpHeaders from, HttpContextBase to)
+ {
+ Contract.Assert(from != null);
+ Contract.Assert(to != null);
+
+ foreach (var header in from)
+ {
+ string name = header.Key;
+ foreach (var value in header.Value)
+ {
+ to.Response.AppendHeader(name, value);
+ }
+ }
+ }
+
+ private static void AddHeaderToHttpRequestMessage(HttpRequestMessage httpRequestMessage, string headerName, string[] headerValues)
+ {
+ Contract.Assert(httpRequestMessage != null);
+ Contract.Assert(headerName != null);
+ Contract.Assert(headerValues != null);
+
+ if (!httpRequestMessage.Headers.TryAddWithoutValidation(headerName, headerValues))
+ {
+ httpRequestMessage.Content.Headers.TryAddWithoutValidation(headerName, headerValues);
+ }
+ }
+
+ /// <summary>
+ /// Converts a <see cref="HttpResponseMessage"/> to an <see cref="HttpResponseBase"/> and disposes the
+ /// <see cref="HttpResponseMessage"/> and <see cref="HttpRequestMessage"/> upon completion.
+ /// </summary>
+ /// <param name="httpContextBase">The HTTP context base.</param>
+ /// <param name="response">The response to convert.</param>
+ /// <param name="request">The request (which will be disposed).</param>
+ /// <returns>A <see cref="Task"/> representing the conversion of an <see cref="HttpResponseMessage"/> to an <see cref="HttpResponseBase"/>
+ /// including writing out any entity body.</returns>
+ internal static Task ConvertResponse(HttpContextBase httpContextBase, HttpResponseMessage response, HttpRequestMessage request)
+ {
+ Contract.Assert(httpContextBase != null);
+ Contract.Assert(response != null);
+ Contract.Assert(request != null);
+
+ HttpResponseBase httpResponseBase = httpContextBase.Response;
+ httpResponseBase.StatusCode = (int)response.StatusCode;
+ httpResponseBase.StatusDescription = response.ReasonPhrase;
+ httpResponseBase.TrySkipIisCustomErrors = true;
+ CopyHeaders(response.Headers, httpContextBase);
+ CacheControlHeaderValue cacheControl = response.Headers.CacheControl;
+
+ // TODO 335085: Consider this when coming up with our caching story
+ if (cacheControl == null)
+ {
+ // DevDiv2 #332323. ASP.NET by default always emits a cache-control: private header.
+ // However, we don't want requests to be cached by default.
+ // If nobody set an explicit CacheControl then explicitly set to no-cache to override the
+ // default behavior. This will cause the following response headers to be emitted:
+ // Cache-Control: no-cache
+ // Pragma: no-cache
+ // Expires: -1
+ httpContextBase.Response.Cache.SetCacheability(HttpCacheability.NoCache);
+ }
+
+ Task responseTask = null;
+ if (response.Content != null)
+ {
+ CopyHeaders(response.Content.Headers, httpContextBase);
+
+ // Turn off ASP output caching
+ httpResponseBase.BufferOutput = false;
+
+ responseTask = response.Content.CopyToAsync(httpResponseBase.OutputStream);
+ }
+ else
+ {
+ responseTask = TaskHelpers.Completed();
+ }
+
+ return responseTask.Finally(
+ () =>
+ {
+ request.DisposeRequestResources();
+ request.Dispose();
+ response.Dispose();
+ });
+ }
+
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Caller becomes owner")]
+ private static HttpRequestMessage ConvertRequest(HttpContextBase httpContextBase)
+ {
+ Contract.Assert(httpContextBase != null);
+
+ HttpRequestBase requestBase = httpContextBase.Request;
+ HttpMethod method = HttpMethodHelper.GetHttpMethod(requestBase.HttpMethod);
+ Uri uri = requestBase.Url;
+ HttpRequestMessage request = new HttpRequestMessage(method, uri);
+
+ // TODO: Should we use GetBufferlessInputStream? Yes, as we don't need any of the parsing from ASP
+ request.Content = new StreamContent(requestBase.InputStream);
+ foreach (string headerName in requestBase.Headers)
+ {
+ string[] values = requestBase.Headers.GetValues(headerName);
+ AddHeaderToHttpRequestMessage(request, headerName, values);
+ }
+
+ // Carry over properties
+ if (httpContextBase.User != null)
+ {
+ request.Properties.Add(HttpPropertyKeys.UserPrincipalKey, httpContextBase.User);
+ }
+
+ // Add context to enable route lookup later on
+ request.Properties.Add(HttpContextBaseKey, httpContextBase);
+
+ return request;
+ }
+ }
+}
diff --git a/src/System.Web.Http.WebHost/HttpControllerRouteHandler.cs b/src/System.Web.Http.WebHost/HttpControllerRouteHandler.cs
new file mode 100644
index 00000000..4d8a46ad
--- /dev/null
+++ b/src/System.Web.Http.WebHost/HttpControllerRouteHandler.cs
@@ -0,0 +1,53 @@
+using System.Web.Routing;
+
+namespace System.Web.Http.WebHost
+{
+ /// <summary>
+ /// A <see cref="IRouteHandler"/> that returns instances of <see cref="HttpControllerHandler"/> that
+ /// can pass requests to a given <see cref="HttpServer"/> instance.
+ /// </summary>
+ public class HttpControllerRouteHandler : IRouteHandler
+ {
+ private static readonly Lazy<HttpControllerRouteHandler> _instance =
+ new Lazy<HttpControllerRouteHandler>(() => new HttpControllerRouteHandler(), isThreadSafe: true);
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpControllerRouteHandler"/> class.
+ /// </summary>
+ protected HttpControllerRouteHandler()
+ {
+ }
+
+ /// <summary>
+ /// Gets the singleton <see cref="HttpControllerRouteHandler"/> instance.
+ /// </summary>
+ public static HttpControllerRouteHandler Instance
+ {
+ get { return _instance.Value; }
+ }
+
+ /// <summary>
+ /// Provides the object that processes the request.
+ /// </summary>
+ /// <param name="requestContext">An object that encapsulates information about the request.</param>
+ /// <returns>
+ /// An object that processes the request.
+ /// </returns>
+ IHttpHandler IRouteHandler.GetHttpHandler(RequestContext requestContext)
+ {
+ return GetHttpHandler(requestContext);
+ }
+
+ /// <summary>
+ /// Provides the object that processes the request.
+ /// </summary>
+ /// <param name="requestContext">An object that encapsulates information about the request.</param>
+ /// <returns>
+ /// An object that processes the request.
+ /// </returns>
+ protected virtual IHttpHandler GetHttpHandler(RequestContext requestContext)
+ {
+ return new HttpControllerHandler(requestContext.RouteData);
+ }
+ }
+}
diff --git a/src/System.Web.Http.WebHost/Properties/AssemblyInfo.cs b/src/System.Web.Http.WebHost/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..17486660
--- /dev/null
+++ b/src/System.Web.Http.WebHost/Properties/AssemblyInfo.cs
@@ -0,0 +1,6 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+
+[assembly: AssemblyTitle("System.Web.Http.WebHost")]
+[assembly: AssemblyDescription("")]
+[assembly: InternalsVisibleTo("System.Web.Http.WebHost.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
diff --git a/src/System.Web.Http.WebHost/Properties/SRResources.Designer.cs b/src/System.Web.Http.WebHost/Properties/SRResources.Designer.cs
new file mode 100644
index 00000000..b7afc946
--- /dev/null
+++ b/src/System.Web.Http.WebHost/Properties/SRResources.Designer.cs
@@ -0,0 +1,99 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.239
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace System.Web.Http.WebHost.Properties {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class SRResources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal SRResources() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("System.Web.Http.WebHost.Properties.SRResources", typeof(SRResources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The &apos;{0}&apos; class only supports asynchronous processing of HTTP requests..
+ /// </summary>
+ internal static string ProcessRequestNotSupported {
+ get {
+ return ResourceManager.GetString("ProcessRequestNotSupported", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to This operation is not supported by &apos;{0}&apos;..
+ /// </summary>
+ internal static string RouteCollectionNotSupported {
+ get {
+ return ResourceManager.GetString("RouteCollectionNotSupported", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The index cannot be less than 0 or equal to or larger than the number of items in the collection..
+ /// </summary>
+ internal static string RouteCollectionOutOfRange {
+ get {
+ return ResourceManager.GetString("RouteCollectionOutOfRange", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to This operation is only supported by directly calling it on &apos;{0}&apos;..
+ /// </summary>
+ internal static string RouteCollectionUseDirectly {
+ get {
+ return ResourceManager.GetString("RouteCollectionUseDirectly", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http.WebHost/Properties/SRResources.resx b/src/System.Web.Http.WebHost/Properties/SRResources.resx
new file mode 100644
index 00000000..2ab7d689
--- /dev/null
+++ b/src/System.Web.Http.WebHost/Properties/SRResources.resx
@@ -0,0 +1,132 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="ProcessRequestNotSupported" xml:space="preserve">
+ <value>The '{0}' class only supports asynchronous processing of HTTP requests.</value>
+ </data>
+ <data name="RouteCollectionNotSupported" xml:space="preserve">
+ <value>This operation is not supported by '{0}'.</value>
+ </data>
+ <data name="RouteCollectionOutOfRange" xml:space="preserve">
+ <value>The index cannot be less than 0 or equal to or larger than the number of items in the collection.</value>
+ </data>
+ <data name="RouteCollectionUseDirectly" xml:space="preserve">
+ <value>This operation is only supported by directly calling it on '{0}'.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/System.Web.Http.WebHost/RouteCollectionExtensions.cs b/src/System.Web.Http.WebHost/RouteCollectionExtensions.cs
new file mode 100644
index 00000000..aa0fe0f0
--- /dev/null
+++ b/src/System.Web.Http.WebHost/RouteCollectionExtensions.cs
@@ -0,0 +1,67 @@
+using System.ComponentModel;
+using System.Web.Http.Common;
+using System.Web.Http.WebHost;
+using System.Web.Http.WebHost.Routing;
+using System.Web.Routing;
+
+namespace System.Web.Http
+{
+ /// <summary>
+ /// Extension methods for <see cref="RouteCollection"/>
+ /// </summary>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static class RouteCollectionExtensions
+ {
+ /// <summary>
+ /// Maps the specified route template.
+ /// </summary>
+ /// <param name="routes">A collection of routes for the application.</param>
+ /// <param name="name">The name of the route to map.</param>
+ /// <param name="routeTemplate">The route template for the route.</param>
+ /// <returns>A reference to the mapped route.</returns>
+ public static Route MapHttpRoute(this RouteCollection routes, string name, string routeTemplate)
+ {
+ return MapHttpRoute(routes, name, routeTemplate, defaults: null, constraints: null);
+ }
+
+ /// <summary>
+ /// Maps the specified route template and sets default constraints, and namespaces.
+ /// </summary>
+ /// <param name="routes">A collection of routes for the application.</param>
+ /// <param name="name">The name of the route to map.</param>
+ /// <param name="routeTemplate">The route template for the route.</param>
+ /// <param name="defaults">An object that contains default route values.</param>
+ /// <returns>A reference to the mapped route.</returns>
+ public static Route MapHttpRoute(this RouteCollection routes, string name, string routeTemplate, object defaults)
+ {
+ return MapHttpRoute(routes, name, routeTemplate, defaults, constraints: null);
+ }
+
+ /// <summary>
+ /// Maps the specified route template and sets default route values, constraints, and namespaces.
+ /// </summary>
+ /// <param name="routes">A collection of routes for the application.</param>
+ /// <param name="name">The name of the route to map.</param>
+ /// <param name="routeTemplate">The route template for the route.</param>
+ /// <param name="defaults">An object that contains default route values.</param>
+ /// <param name="constraints">A set of expressions that specify values for <paramref name="routeTemplate"/>.</param>
+ /// <returns>A reference to the mapped route.</returns>
+ public static Route MapHttpRoute(this RouteCollection routes, string name, string routeTemplate, object defaults, object constraints)
+ {
+ if (routes == null)
+ {
+ throw Error.ArgumentNull("routes");
+ }
+
+ HttpWebRoute route = new HttpWebRoute(routeTemplate, HttpControllerRouteHandler.Instance)
+ {
+ Defaults = new RouteValueDictionary(defaults),
+ Constraints = new RouteValueDictionary(constraints),
+ DataTokens = new RouteValueDictionary()
+ };
+
+ routes.Add(name, route);
+ return route;
+ }
+ }
+}
diff --git a/src/System.Web.Http.WebHost/Routing/HostedHttpRoute.cs b/src/System.Web.Http.WebHost/Routing/HostedHttpRoute.cs
new file mode 100644
index 00000000..9a85c0ca
--- /dev/null
+++ b/src/System.Web.Http.WebHost/Routing/HostedHttpRoute.cs
@@ -0,0 +1,99 @@
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Routing;
+using System.Web.Routing;
+
+namespace System.Web.Http.WebHost.Routing
+{
+ internal class HostedHttpRoute : IHttpRoute
+ {
+ private readonly Route _route;
+
+ public HostedHttpRoute(Route route)
+ {
+ if (route == null)
+ {
+ throw Error.ArgumentNull("route");
+ }
+
+ _route = route;
+ }
+
+ public string RouteTemplate
+ {
+ get { return _route.Url; }
+ }
+
+ public IDictionary<string, object> Defaults
+ {
+ get { return _route.Defaults; }
+ }
+
+ public IDictionary<string, object> Constraints
+ {
+ get { return _route.Constraints; }
+ }
+
+ public IDictionary<string, object> DataTokens
+ {
+ get { return _route.DataTokens; }
+ }
+
+ internal Route OriginalRoute
+ {
+ get { return _route; }
+ }
+
+ public IHttpRouteData GetRouteData(string rootVirtualPath, HttpRequestMessage request)
+ {
+ if (rootVirtualPath == null)
+ {
+ throw Error.ArgumentNull("rootVirtualPath");
+ }
+
+ if (request == null)
+ {
+ throw Error.ArgumentNull("request");
+ }
+
+ HttpContextBase httpContextBase;
+ if (request.Properties.TryGetValue(HttpControllerHandler.HttpContextBaseKey, out httpContextBase))
+ {
+ RouteData routeData = _route.GetRouteData(httpContextBase);
+ if (routeData != null)
+ {
+ return new HostedHttpRouteData(routeData);
+ }
+ }
+
+ return null;
+ }
+
+ public IHttpVirtualPathData GetVirtualPath(HttpControllerContext controllerContext, IDictionary<string, object> values)
+ {
+ if (controllerContext == null)
+ {
+ throw Error.ArgumentNull("controllerContext");
+ }
+
+ HttpContextBase httpContextBase;
+ if (controllerContext.Request.Properties.TryGetValue(HttpControllerHandler.HttpContextBaseKey, out httpContextBase))
+ {
+ HostedHttpRouteData routeData = controllerContext.RouteData as HostedHttpRouteData;
+ if (routeData != null)
+ {
+ RequestContext requestContext = new RequestContext(httpContextBase, routeData.OriginalRouteData);
+ VirtualPathData virtualPathData = _route.GetVirtualPath(requestContext, new RouteValueDictionary(values));
+ if (virtualPathData != null)
+ {
+ return new HostedHttpVirtualPathData(virtualPathData);
+ }
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/System.Web.Http.WebHost/Routing/HostedHttpRouteCollection.cs b/src/System.Web.Http.WebHost/Routing/HostedHttpRouteCollection.cs
new file mode 100644
index 00000000..b6d081bb
--- /dev/null
+++ b/src/System.Web.Http.WebHost/Routing/HostedHttpRouteCollection.cs
@@ -0,0 +1,195 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Web.Hosting;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Routing;
+using System.Web.Http.WebHost.Properties;
+using System.Web.Routing;
+
+namespace System.Web.Http.WebHost.Routing
+{
+ internal class HostedHttpRouteCollection : HttpRouteCollection
+ {
+ private readonly RouteCollection _routeCollection;
+
+ public HostedHttpRouteCollection(RouteCollection routeCollection)
+ {
+ if (routeCollection == null)
+ {
+ throw Error.ArgumentNull("routeCollection");
+ }
+
+ _routeCollection = routeCollection;
+ }
+
+ public override string VirtualPathRoot
+ {
+ get { return HostingEnvironment.ApplicationVirtualPath; }
+ }
+
+ public override int Count
+ {
+ get { return _routeCollection.Count; }
+ }
+
+ public override IHttpRoute this[string name]
+ {
+ get
+ {
+ Route route = _routeCollection[name] as Route;
+ if (route != null)
+ {
+ return new HostedHttpRoute(route);
+ }
+
+ throw Error.KeyNotFound();
+ }
+ }
+
+ public override IHttpRoute this[int index]
+ {
+ get
+ {
+ Route route = _routeCollection[index] as Route;
+ if (route != null)
+ {
+ return new HostedHttpRoute(route);
+ }
+
+ throw Error.ArgumentOutOfRange("index", index, SRResources.RouteCollectionOutOfRange);
+ }
+ }
+
+ public override IHttpRouteData GetRouteData(HttpRequestMessage request)
+ {
+ if (request == null)
+ {
+ throw Error.ArgumentNull("request");
+ }
+
+ HttpContextBase httpContextBase;
+ if (request.Properties.TryGetValue(HttpControllerHandler.HttpContextBaseKey, out httpContextBase))
+ {
+ RouteData routeData = _routeCollection.GetRouteData(httpContextBase);
+ if (routeData != null)
+ {
+ return new HostedHttpRouteData(routeData);
+ }
+ }
+
+ return null;
+ }
+
+ public override IHttpVirtualPathData GetVirtualPath(HttpControllerContext controllerContext, string name, IDictionary<string, object> values)
+ {
+ if (controllerContext == null)
+ {
+ throw Error.ArgumentNull("controllerContext");
+ }
+
+ HttpRequestMessage request = controllerContext.Request;
+ HttpContextBase httpContextBase;
+ if (request.Properties.TryGetValue(HttpControllerHandler.HttpContextBaseKey, out httpContextBase))
+ {
+ RequestContext requestContext = new RequestContext(httpContextBase, controllerContext.RouteData.ToRouteData());
+ RouteValueDictionary routeValues = values != null ? new RouteValueDictionary(values) : new RouteValueDictionary();
+ VirtualPathData virtualPathData = _routeCollection.GetVirtualPath(requestContext, name, routeValues);
+ if (virtualPathData != null)
+ {
+ return new HostedHttpVirtualPathData(virtualPathData);
+ }
+ }
+
+ return null;
+ }
+
+ public override IHttpRoute CreateRoute(string uriTemplate, IDictionary<string, object> defaults, IDictionary<string, object> constraints, IDictionary<string, object> dataTokens, IDictionary<string, object> parameters)
+ {
+ RouteValueDictionary routeDefaults = defaults != null ? new RouteValueDictionary(defaults) : null;
+ RouteValueDictionary routeConstraints = constraints != null ? new RouteValueDictionary(constraints) : null;
+ RouteValueDictionary routeDataTokens = dataTokens != null ? new RouteValueDictionary(dataTokens) : null;
+ HttpWebRoute route = new HttpWebRoute(uriTemplate, routeDefaults, routeConstraints, routeDataTokens, HttpControllerRouteHandler.Instance);
+ return new HostedHttpRoute(route);
+ }
+
+ public override void Add(string name, IHttpRoute route)
+ {
+ _routeCollection.Add(name, route.ToRoute());
+ }
+
+ public override void Clear()
+ {
+ _routeCollection.Clear();
+ }
+
+ public override bool Contains(IHttpRoute item)
+ {
+ HostedHttpRoute hostedHttpRoute = item as HostedHttpRoute;
+ if (hostedHttpRoute != null)
+ {
+ return _routeCollection.Contains(hostedHttpRoute.OriginalRoute);
+ }
+
+ return false;
+ }
+
+ public override bool ContainsKey(string name)
+ {
+ return _routeCollection[name] != null;
+ }
+
+ public override void CopyTo(IHttpRoute[] array, int arrayIndex)
+ {
+ throw NotSupportedByHostedRouteCollection();
+ }
+
+ public override void CopyTo(KeyValuePair<string, IHttpRoute>[] array, int arrayIndex)
+ {
+ throw NotSupportedByRouteCollection();
+ }
+
+ public override void Insert(int index, string name, IHttpRoute value)
+ {
+ throw NotSupportedByRouteCollection();
+ }
+
+ public override bool Remove(string name)
+ {
+ throw NotSupportedByRouteCollection();
+ }
+
+ public override IEnumerator<IHttpRoute> GetEnumerator()
+ {
+ // Here we only care about Web API routes.
+ return _routeCollection
+ .OfType<HttpWebRoute>()
+ .Select(httpWebRoute => new HostedHttpRoute(httpWebRoute))
+ .GetEnumerator();
+ }
+
+ public override bool TryGetValue(string name, out IHttpRoute route)
+ {
+ Route rt = _routeCollection[name] as Route;
+ if (rt != null)
+ {
+ route = new HostedHttpRoute(rt);
+ return true;
+ }
+
+ route = null;
+ return false;
+ }
+
+ private static NotSupportedException NotSupportedByRouteCollection()
+ {
+ return Error.NotSupported(SRResources.RouteCollectionNotSupported, typeof(RouteCollection).Name);
+ }
+
+ private static NotSupportedException NotSupportedByHostedRouteCollection()
+ {
+ return Error.NotSupported(SRResources.RouteCollectionUseDirectly, typeof(RouteCollection).Name);
+ }
+ }
+}
diff --git a/src/System.Web.Http.WebHost/Routing/HostedHttpRouteData.cs b/src/System.Web.Http.WebHost/Routing/HostedHttpRouteData.cs
new file mode 100644
index 00000000..5cc9eb18
--- /dev/null
+++ b/src/System.Web.Http.WebHost/Routing/HostedHttpRouteData.cs
@@ -0,0 +1,39 @@
+using System.Collections.Generic;
+using System.Web.Http.Common;
+using System.Web.Http.Routing;
+using System.Web.Routing;
+
+namespace System.Web.Http.WebHost.Routing
+{
+ internal class HostedHttpRouteData : IHttpRouteData
+ {
+ private readonly RouteData _routeData;
+ private readonly HostedHttpRoute _hostedHttpRoute;
+
+ public HostedHttpRouteData(RouteData routeData)
+ {
+ if (routeData == null)
+ {
+ throw Error.ArgumentNull("routeData");
+ }
+
+ _routeData = routeData;
+ _hostedHttpRoute = new HostedHttpRoute(_routeData.Route as Route);
+ }
+
+ public IHttpRoute Route
+ {
+ get { return _hostedHttpRoute; }
+ }
+
+ public IDictionary<string, object> Values
+ {
+ get { return _routeData.Values; }
+ }
+
+ internal RouteData OriginalRouteData
+ {
+ get { return _routeData; }
+ }
+ }
+}
diff --git a/src/System.Web.Http.WebHost/Routing/HostedHttpVirtualPathData.cs b/src/System.Web.Http.WebHost/Routing/HostedHttpVirtualPathData.cs
new file mode 100644
index 00000000..13fce709
--- /dev/null
+++ b/src/System.Web.Http.WebHost/Routing/HostedHttpVirtualPathData.cs
@@ -0,0 +1,33 @@
+using System.Web.Http.Common;
+using System.Web.Http.Routing;
+using System.Web.Routing;
+
+namespace System.Web.Http.WebHost.Routing
+{
+ internal class HostedHttpVirtualPathData : IHttpVirtualPathData
+ {
+ private readonly VirtualPathData _virtualPath;
+ private readonly HostedHttpRoute _hostedHttpRoute;
+
+ public HostedHttpVirtualPathData(VirtualPathData virtualPath)
+ {
+ if (virtualPath == null)
+ {
+ throw Error.ArgumentNull("route");
+ }
+
+ _virtualPath = virtualPath;
+ _hostedHttpRoute = new HostedHttpRoute(_virtualPath.Route as Route);
+ }
+
+ public IHttpRoute Route
+ {
+ get { return _hostedHttpRoute; }
+ }
+
+ public string VirtualPath
+ {
+ get { return _virtualPath.VirtualPath; }
+ }
+ }
+}
diff --git a/src/System.Web.Http.WebHost/Routing/HttpRouteDataExtensions.cs b/src/System.Web.Http.WebHost/Routing/HttpRouteDataExtensions.cs
new file mode 100644
index 00000000..9022f0ff
--- /dev/null
+++ b/src/System.Web.Http.WebHost/Routing/HttpRouteDataExtensions.cs
@@ -0,0 +1,26 @@
+using System.Web.Http.Common;
+using System.Web.Http.Routing;
+using System.Web.Routing;
+
+namespace System.Web.Http.WebHost.Routing
+{
+ internal static class HttpRouteDataExtensions
+ {
+ public static RouteData ToRouteData(this IHttpRouteData httpRouteData)
+ {
+ if (httpRouteData == null)
+ {
+ throw Error.ArgumentNull("httpRouteData");
+ }
+
+ HostedHttpRouteData hostedHttpRouteData = httpRouteData as HostedHttpRouteData;
+ if (hostedHttpRouteData != null)
+ {
+ return hostedHttpRouteData.OriginalRouteData;
+ }
+
+ Route route = httpRouteData.Route.ToRoute();
+ return new RouteData(route, HttpControllerRouteHandler.Instance);
+ }
+ }
+}
diff --git a/src/System.Web.Http.WebHost/Routing/HttpRouteExtensions.cs b/src/System.Web.Http.WebHost/Routing/HttpRouteExtensions.cs
new file mode 100644
index 00000000..c50fc304
--- /dev/null
+++ b/src/System.Web.Http.WebHost/Routing/HttpRouteExtensions.cs
@@ -0,0 +1,30 @@
+using System.Web.Http.Common;
+using System.Web.Http.Routing;
+using System.Web.Routing;
+
+namespace System.Web.Http.WebHost.Routing
+{
+ internal static class HttpRouteExtensions
+ {
+ public static Route ToRoute(this IHttpRoute httpRoute)
+ {
+ if (httpRoute == null)
+ {
+ throw Error.ArgumentNull("httpRoute");
+ }
+
+ HostedHttpRoute hostedHttpRoute = httpRoute as HostedHttpRoute;
+ if (hostedHttpRoute != null)
+ {
+ return hostedHttpRoute.OriginalRoute;
+ }
+
+ return new HttpWebRoute(httpRoute.RouteTemplate, HttpControllerRouteHandler.Instance)
+ {
+ Defaults = new RouteValueDictionary(httpRoute.Defaults),
+ Constraints = new RouteValueDictionary(httpRoute.Constraints),
+ DataTokens = new RouteValueDictionary(httpRoute.DataTokens),
+ };
+ }
+ }
+}
diff --git a/src/System.Web.Http.WebHost/Routing/HttpWebRoute.cs b/src/System.Web.Http.WebHost/Routing/HttpWebRoute.cs
new file mode 100644
index 00000000..29b34c1e
--- /dev/null
+++ b/src/System.Web.Http.WebHost/Routing/HttpWebRoute.cs
@@ -0,0 +1,74 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Web.Routing;
+
+namespace System.Web.Http.WebHost.Routing
+{
+ /// <summary>
+ /// Mimics the System.Web.Routing.Route class to work better for Web API scenarios. The only
+ /// difference between the base class and this class is that this one will match only when
+ /// a special "httproute" key is specified when generating URLs. There is no special behavior
+ /// for incoming URLs.
+ /// </summary>
+ public class HttpWebRoute : Route
+ {
+ /// <summary>
+ /// Key used to signify that a route URL generation request should include HTTP routes (e.g. Web API).
+ /// If this key is not specified then no HTTP routes will match.
+ /// </summary>
+ private const string HttpRouteKey = "httproute";
+
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "0#", Justification = "Matches the base class's parameter names.")]
+ public HttpWebRoute(string url, IRouteHandler routeHandler)
+ : base(url, routeHandler)
+ {
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "0#", Justification = "Matches the base class's parameter names.")]
+ public HttpWebRoute(string url, RouteValueDictionary defaults, IRouteHandler routeHandler)
+ : base(url, defaults, routeHandler)
+ {
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "0#", Justification = "Matches the base class's parameter names.")]
+ public HttpWebRoute(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, IRouteHandler routeHandler)
+ : base(url, defaults, constraints, routeHandler)
+ {
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "0#", Justification = "Matches the base class's parameter names.")]
+ public HttpWebRoute(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, RouteValueDictionary dataTokens, IRouteHandler routeHandler)
+ : base(url, defaults, constraints, dataTokens, routeHandler)
+ {
+ }
+
+ public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
+ {
+ // Only perform URL generation if the "httproute" key was specified. This allows these
+ // routes to be ignored when a regular MVC app tries to generate URLs. Without this special
+ // key an HTTP route used for Web API would normally take over almost all the routes in a
+ // typical app.
+ if (!values.ContainsKey(HttpRouteKey))
+ {
+ return null;
+ }
+ // Remove the value from the collection so that it doesn't affect the generated URL
+ RouteValueDictionary newValues = GetRouteDictionaryWithoutHttpRouteKey(values);
+
+ return base.GetVirtualPath(requestContext, newValues);
+ }
+
+ private static RouteValueDictionary GetRouteDictionaryWithoutHttpRouteKey(IDictionary<string, object> routeValues)
+ {
+ var newRouteValues = new RouteValueDictionary();
+ foreach (var routeValue in routeValues)
+ {
+ if (!String.Equals(routeValue.Key, HttpRouteKey, StringComparison.OrdinalIgnoreCase))
+ {
+ newRouteValues.Add(routeValue.Key, routeValue.Value);
+ }
+ }
+ return newRouteValues;
+ }
+ }
+}
diff --git a/src/System.Web.Http.WebHost/System.Web.Http.WebHost.csproj b/src/System.Web.Http.WebHost/System.Web.Http.WebHost.csproj
new file mode 100644
index 00000000..13adb3b7
--- /dev/null
+++ b/src/System.Web.Http.WebHost/System.Web.Http.WebHost.csproj
@@ -0,0 +1,118 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <CodeAnalysis Condition=" '$(CodeAnalysis)' == '' ">false</CodeAnalysis>
+ <ProductVersion>9.0.30729</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{A0187BC2-8325-4BB2-8697-7F955CF4173E}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>System.Web.Http.WebHost</RootNamespace>
+ <AssemblyName>System.Web.Http.WebHost</AssemblyName>
+ <FileAlignment>512</FileAlignment>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>..\..\bin\Debug\</OutputPath>
+ <DefineConstants>TRACE;DEBUG;ASPNETMVC</DefineConstants>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ <DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
+ <NoWarn>1591</NoWarn>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>..\..\bin\Release\</OutputPath>
+ <DefineConstants>TRACE;ASPNETMVC</DefineConstants>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ <RunCodeAnalysis>$(CodeAnalysis)</RunCodeAnalysis>
+ <DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
+ <NoWarn>1591</NoWarn>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'CodeCoverage|AnyCPU'">
+ <DebugSymbols>true</DebugSymbols>
+ <OutputPath>..\..\bin\CodeCoverage\</OutputPath>
+ <DefineConstants>TRACE;DEBUG;CODE_COVERAGE;ASPNETMVC</DefineConstants>
+ <DebugType>full</DebugType>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="System" />
+ <Reference Include="System.configuration" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.Net.Http">
+ <HintPath>..\..\packages\Microsoft.Net.Http.2.0.20302.1\lib\net40\System.Net.Http.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Net.Http.WebRequest">
+ <HintPath>..\..\packages\Microsoft.Net.Http.2.0.20302.1\lib\net40\System.Net.Http.WebRequest.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Web" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="..\CommonAssemblyInfo.cs">
+ <Link>Properties\CommonAssemblyInfo.cs</Link>
+ </Compile>
+ <Compile Include="Routing\HostedHttpRouteCollection.cs" />
+ <Compile Include="Routing\HostedHttpRoute.cs" />
+ <Compile Include="Routing\HostedHttpRouteData.cs" />
+ <Compile Include="Routing\HostedHttpVirtualPathData.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="..\TransparentCommonAssemblyInfo.cs">
+ <Link>Properties\TransparentCommonAssemblyInfo.cs</Link>
+ </Compile>
+ <Compile Include="GlobalConfiguration.cs" />
+ <Compile Include="Routing\HttpRouteDataExtensions.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="Routing\HttpRouteExtensions.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="Routing\HttpWebRoute.cs" />
+ <Compile Include="WebHostBuildManager.cs" />
+ <Compile Include="Properties\SRResources.Designer.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>SRResources.resx</DependentUpon>
+ </Compile>
+ <Compile Include="GlobalSuppressions.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="HttpControllerHandler.cs" />
+ <Compile Include="HttpControllerRouteHandler.cs" />
+ <Compile Include="RouteCollectionExtensions.cs" />
+ <Compile Include="TaskWrapperAsyncResult.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="Properties\SRResources.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>SRResources.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\System.Web.Http.Common\System.Web.Http.Common.csproj">
+ <Project>{03A5E5F2-2E23-48F2-ABCC-6C41BAC9AC02}</Project>
+ <Name>System.Web.Http.Common</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\System.Web.Http\System.Web.Http.csproj">
+ <Project>{DDC1CE0C-486E-4E35-BB3B-EAB61F8F9440}</Project>
+ <Name>System.Web.Http</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <CodeAnalysisDictionary Include="..\CodeAnalysisDictionary.xml">
+ <Link>CodeAnalysisDictionary.xml</Link>
+ </CodeAnalysisDictionary>
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="packages.config" />
+ </ItemGroup>
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+</Project> \ No newline at end of file
diff --git a/src/System.Web.Http.WebHost/TaskWrapperAsyncResult.cs b/src/System.Web.Http.WebHost/TaskWrapperAsyncResult.cs
new file mode 100644
index 00000000..be0bd8bb
--- /dev/null
+++ b/src/System.Web.Http.WebHost/TaskWrapperAsyncResult.cs
@@ -0,0 +1,70 @@
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Common;
+
+namespace System.Web.Http.WebHost
+{
+ /// <summary>
+ /// Wraps a <see cref="Task"/>, optionally overriding the State object (since the Task Asynchronous Pattern doesn't normally use it).
+ /// </summary>
+ /// <remarks>Class copied from System.Web.Mvc, but with modifications</remarks>
+ internal sealed class TaskWrapperAsyncResult : IAsyncResult
+ {
+ private bool? _completedSynchronously;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="TaskWrapperAsyncResult"/> class.
+ /// </summary>
+ /// <param name="task">The <see cref="Task"/> to wrap.</param>
+ /// <param name="asyncState">User-defined object that qualifies or contains information about an asynchronous operation.</param>
+ public TaskWrapperAsyncResult(Task task, object asyncState)
+ {
+ if (task == null)
+ {
+ throw Error.ArgumentNull("task");
+ }
+
+ Task = task;
+ AsyncState = asyncState;
+ }
+
+ /// <summary>
+ /// Gets a user-defined object that qualifies or contains information about an asynchronous operation.
+ /// </summary>
+ /// <returns>A user-defined object that qualifies or contains information about an asynchronous operation.</returns>
+ public object AsyncState { get; private set; }
+
+ /// <summary>
+ /// Gets a <see cref="T:System.Threading.WaitHandle"/> that is used to wait for an asynchronous operation to complete.
+ /// </summary>
+ /// <returns>A <see cref="T:System.Threading.WaitHandle"/> that is used to wait for an asynchronous operation to complete.</returns>
+ public WaitHandle AsyncWaitHandle
+ {
+ get { return ((IAsyncResult)Task).AsyncWaitHandle; }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether the asynchronous operation completed synchronously.
+ /// </summary>
+ /// <returns>true if the asynchronous operation completed synchronously; otherwise, false.</returns>
+ public bool CompletedSynchronously
+ {
+ get { return _completedSynchronously ?? ((IAsyncResult)Task).CompletedSynchronously; }
+ internal set { _completedSynchronously = value; }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether the asynchronous operation has completed.
+ /// </summary>
+ /// <returns>true if the operation is complete; otherwise, false.</returns>
+ public bool IsCompleted
+ {
+ get { return ((IAsyncResult)Task).IsCompleted; }
+ }
+
+ /// <summary>
+ /// Gets the task.
+ /// </summary>
+ public Task Task { get; private set; }
+ }
+}
diff --git a/src/System.Web.Http.WebHost/WebHostBuildManager.cs b/src/System.Web.Http.WebHost/WebHostBuildManager.cs
new file mode 100644
index 00000000..e325f436
--- /dev/null
+++ b/src/System.Web.Http.WebHost/WebHostBuildManager.cs
@@ -0,0 +1,62 @@
+using System.Collections;
+using System.IO;
+using System.Web.Compilation;
+using System.Web.Http.Dispatcher;
+
+namespace System.Web.Http.WebHost
+{
+ /// <summary>
+ /// Wraps ASP build manager
+ /// </summary>
+ internal sealed class WebHostBuildManager : IBuildManager
+ {
+ /// <summary>
+ /// Gets an object factory for the specified virtual path.
+ /// </summary>
+ /// <param name="virtualPath">The virtual path.</param>
+ /// <returns><c>true</c> if file exists; otherwise false.</returns>
+ bool IBuildManager.FileExists(string virtualPath)
+ {
+ return BuildManager.GetObjectFactory(virtualPath, false) != null;
+ }
+
+ /// <summary>
+ /// Compiles a file, given its virtual path, and returns the compiled type.
+ /// </summary>
+ /// <param name="virtualPath">The virtual path.</param>
+ /// <returns>The compiled <see cref="Type"/>.</returns>
+ Type IBuildManager.GetCompiledType(string virtualPath)
+ {
+ return BuildManager.GetCompiledType(virtualPath);
+ }
+
+ /// <summary>
+ /// Returns a list of assembly references that all page compilations must reference.
+ /// </summary>
+ /// <returns>An <see cref="ICollection"/> of assembly references.</returns>
+ ICollection IBuildManager.GetReferencedAssemblies()
+ {
+ return BuildManager.GetReferencedAssemblies();
+ }
+
+ /// <summary>
+ /// Reads a cached file.
+ /// </summary>
+ /// <param name="fileName">Name of the file.</param>
+ /// <returns>The <see cref="Stream"/> object for the file, or <c>null</c> if the file does not exist.</returns>
+ Stream IBuildManager.ReadCachedFile(string fileName)
+ {
+ return BuildManager.ReadCachedFile(fileName);
+ }
+
+ /// <summary>
+ /// Creates a cached file.
+ /// </summary>
+ /// <param name="fileName">Name of the file.</param>
+ /// <returns>The <see cref="Stream"/> object for the new file.</returns>
+ Stream IBuildManager.CreateCachedFile(string fileName)
+ {
+ return BuildManager.CreateCachedFile(fileName);
+ }
+ }
+}
diff --git a/src/System.Web.Http.WebHost/packages.config b/src/System.Web.Http.WebHost/packages.config
new file mode 100644
index 00000000..c611f43d
--- /dev/null
+++ b/src/System.Web.Http.WebHost/packages.config
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="Microsoft.Net.Http" version="2.0.20302.1" />
+</packages> \ No newline at end of file
diff --git a/src/System.Web.Http/AcceptVerbsAttribute.cs b/src/System.Web.Http/AcceptVerbsAttribute.cs
new file mode 100644
index 00000000..f182abc8
--- /dev/null
+++ b/src/System.Web.Http/AcceptVerbsAttribute.cs
@@ -0,0 +1,38 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Net.Http;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+
+namespace System.Web.Http
+{
+ [SuppressMessage("Microsoft.Design", "CA1019:DefineAccessorsForAttributeArguments", Justification = "The accessor is exposed as an ICollection<HttpMethod>.")]
+ [CLSCompliant(false)]
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+ public sealed class AcceptVerbsAttribute : Attribute, IActionHttpMethodProvider
+ {
+ private readonly Collection<HttpMethod> _httpMethods;
+
+ public AcceptVerbsAttribute(params string[] methods)
+ {
+ _httpMethods = methods != null
+ ? new Collection<HttpMethod>(methods.Select(method => HttpMethodHelper.GetHttpMethod(method)).ToArray())
+ : new Collection<HttpMethod>(new HttpMethod[0]);
+ }
+
+ internal AcceptVerbsAttribute(params HttpMethod[] methods)
+ {
+ _httpMethods = new Collection<HttpMethod>(methods);
+ }
+
+ public Collection<HttpMethod> HttpMethods
+ {
+ get
+ {
+ return _httpMethods;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/ActionNameAttribute.cs b/src/System.Web.Http/ActionNameAttribute.cs
new file mode 100644
index 00000000..c67fa39d
--- /dev/null
+++ b/src/System.Web.Http/ActionNameAttribute.cs
@@ -0,0 +1,15 @@
+using System.Web.Http.Controllers;
+
+namespace System.Web.Http
+{
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+ public sealed class ActionNameAttribute : Attribute
+ {
+ public ActionNameAttribute(string name)
+ {
+ Name = name;
+ }
+
+ public string Name { get; private set; }
+ }
+}
diff --git a/src/System.Web.Http/AllowAnonymousAttribute.cs b/src/System.Web.Http/AllowAnonymousAttribute.cs
new file mode 100644
index 00000000..d747fa26
--- /dev/null
+++ b/src/System.Web.Http/AllowAnonymousAttribute.cs
@@ -0,0 +1,10 @@
+namespace System.Web.Http
+{
+ /// <summary>
+ /// Actions and controllers marked with this attribute are skipped by <see cref="AuthorizeAttribute"/> during authorization.
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = false)]
+ public sealed class AllowAnonymousAttribute : Attribute
+ {
+ }
+}
diff --git a/src/System.Web.Http/ApiController.cs b/src/System.Web.Http/ApiController.cs
new file mode 100644
index 00000000..27968f88
--- /dev/null
+++ b/src/System.Web.Http/ApiController.cs
@@ -0,0 +1,307 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics.Contracts;
+using System.Linq;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Dispatcher;
+using System.Web.Http.Filters;
+using System.Web.Http.ModelBinding;
+using System.Web.Http.Properties;
+using System.Web.Http.Routing;
+
+namespace System.Web.Http
+{
+ public abstract class ApiController : IHttpController, IDisposable
+ {
+ private bool _disposed;
+ private HttpRequestMessage _request;
+ private ModelStateDictionary _modelState;
+ private HttpConfiguration _configuration;
+ private HttpControllerContext _controllerContext;
+
+ /// <summary>
+ /// Gets the <see name="HttpRequestMessage"/> of the current ApiController.
+ ///
+ /// The setter is not intended to be used other than for unit testing purpose.
+ /// </summary>
+ public HttpRequestMessage Request
+ {
+ get { return _request; }
+ set
+ {
+ if (value == null)
+ {
+ throw Error.ArgumentNull("value");
+ }
+
+ _request = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets the <see name="HttpConfiguration"/> of the current ApiController.
+ ///
+ /// The setter is not intended to be used other than for unit testing purpose.
+ /// </summary>
+ public HttpConfiguration Configuration
+ {
+ get { return _configuration; }
+ set
+ {
+ if (value == null)
+ {
+ throw Error.ArgumentNull("value");
+ }
+
+ _configuration = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets the <see name="HttpControllerContext"/> of the current ApiController.
+ ///
+ /// The setter is not intended to be used other than for unit testing purpose.
+ /// </summary>
+ public HttpControllerContext ControllerContext
+ {
+ get { return _controllerContext; }
+ set
+ {
+ if (value == null)
+ {
+ throw Error.ArgumentNull("value");
+ }
+
+ _controllerContext = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets model state after the model binding process. This ModelState will be empty before model binding happens.
+ /// Please do not populate this property other than for unit testing purpose.
+ /// </summary>
+ public ModelStateDictionary ModelState
+ {
+ get
+ {
+ if (_modelState == null)
+ {
+ // The getter is not intended to be used by multiple threads, so it is fine to initialize here
+ _modelState = new ModelStateDictionary();
+ }
+
+ return _modelState;
+ }
+ }
+
+ /// <summary>
+ /// Returns an instance of a UrlHelper, which is used to generate URLs to other APIs.
+ /// </summary>
+ public UrlHelper Url
+ {
+ get { return ControllerContext.Url; }
+ }
+
+ public virtual Task<HttpResponseMessage> ExecuteAsync(HttpControllerContext controllerContext, CancellationToken cancellationToken)
+ {
+ if (_request != null)
+ {
+ // if user has registered a controller factory which produces the same controller instance, we should throw here
+ throw Error.InvalidOperation(SRResources.CannotSupportSingletonInstance, typeof(ApiController).Name, typeof(IHttpControllerFactory).Name);
+ }
+
+ Initialize(controllerContext);
+ HttpControllerDescriptor controllerDescriptor = controllerContext.ControllerDescriptor;
+
+ HttpActionDescriptor actionDescriptor = controllerDescriptor.HttpActionSelector.SelectAction(controllerContext);
+ HttpActionContext actionContext = new HttpActionContext(controllerContext, actionDescriptor);
+
+ IEnumerable<FilterInfo> filters = actionDescriptor.GetFilterPipeline();
+
+ FilterGrouping filterGrouping = new FilterGrouping(filters);
+
+ IEnumerable<IActionFilter> actionFilters = filterGrouping.ActionFilters;
+ IEnumerable<IAuthorizationFilter> authorizationFilters = filterGrouping.AuthorizationFilters;
+ IEnumerable<IExceptionFilter> exceptionFilters = filterGrouping.ExceptionFilters;
+
+ // Func<Task<HttpResponseMessage>>
+ Task<HttpResponseMessage> result = InvokeActionWithAuthorizationFilters(actionContext, cancellationToken, authorizationFilters, () =>
+ {
+ IActionValueBinder actionValueBinder = controllerDescriptor.ActionValueBinder;
+ HttpActionBinding actionBinding = actionValueBinder.GetBinding(actionDescriptor);
+ Task bindTask = actionBinding.ExecuteBindingAsync(actionContext, cancellationToken);
+ return bindTask.Then<HttpResponseMessage>(() =>
+ {
+ _modelState = actionContext.ModelState;
+ Func<Task<HttpResponseMessage>> invokeFunc = InvokeActionWithActionFilters(actionContext, cancellationToken, actionFilters, () =>
+ {
+ return controllerDescriptor.HttpActionInvoker.InvokeActionAsync(actionContext, cancellationToken);
+ });
+ return invokeFunc();
+ });
+ })();
+
+ result = InvokeActionWithExceptionFilters(result, actionContext, cancellationToken, exceptionFilters);
+
+ return result;
+ }
+
+ protected virtual void Initialize(HttpControllerContext controllerContext)
+ {
+ if (controllerContext == null)
+ {
+ throw Error.ArgumentNull("controllerContext");
+ }
+
+ ControllerContext = controllerContext;
+
+ _request = controllerContext.Request;
+ _configuration = controllerContext.Configuration;
+ }
+
+ internal static Task<HttpResponseMessage> InvokeActionWithExceptionFilters(Task<HttpResponseMessage> actionTask, HttpActionContext actionContext, CancellationToken cancellationToken, IEnumerable<IExceptionFilter> filters)
+ {
+ Contract.Assert(actionTask != null);
+ Contract.Assert(actionContext != null);
+ Contract.Assert(filters != null);
+
+ return actionTask.Catch<HttpResponseMessage>(
+ (Exception exception) =>
+ {
+ HttpActionExecutedContext executedContext = new HttpActionExecutedContext(actionContext, exception);
+
+ // Note: exception filters need to be scheduled in the reverse order so that
+ // the more specific filter (e.g. Action) executes before the less specific ones (e.g. Global)
+ filters = filters.Reverse();
+
+ // Note: in order to work correctly with the TaskHelpers.Iterate method, the lazyTaskEnumeration
+ // must be lazily evaluated. Otherwise all the tasks might start executing even though we want to run them
+ // sequentially and not invoke any of the following ones if an earlier fails.
+ IEnumerable<Task> lazyTaskEnumeration = filters.Select(filter => filter.ExecuteExceptionFilterAsync(executedContext, cancellationToken));
+ Task iterationTask = TaskHelpers.Iterate(lazyTaskEnumeration, cancellationToken);
+
+ return iterationTask.Then<HttpResponseMessage>(() =>
+ {
+ if (executedContext.Result != null)
+ {
+ return TaskHelpers.FromResult<HttpResponseMessage>(executedContext.Result);
+ }
+ else
+ {
+ return TaskHelpers.FromError<HttpResponseMessage>(executedContext.Exception);
+ }
+ });
+ });
+ }
+
+ internal static Func<Task<HttpResponseMessage>> InvokeActionWithAuthorizationFilters(HttpActionContext actionContext, CancellationToken cancellationToken, IEnumerable<IAuthorizationFilter> filters, Func<Task<HttpResponseMessage>> innerAction)
+ {
+ Contract.Assert(actionContext != null);
+ Contract.Assert(filters != null);
+ Contract.Assert(innerAction != null);
+
+ // Because the continuation gets built from the inside out we need to reverse the filter list
+ // so that least specific filters (Global) get run first and the most specific filters (Action) get run last.
+ filters = filters.Reverse();
+
+ Func<Task<HttpResponseMessage>> result = filters.Aggregate(innerAction, (continuation, filter) =>
+ {
+ return () => filter.ExecuteAuthorizationFilterAsync(actionContext, cancellationToken, continuation);
+ });
+
+ return result;
+ }
+
+ internal static Func<Task<HttpResponseMessage>> InvokeActionWithActionFilters(HttpActionContext actionContext, CancellationToken cancellationToken, IEnumerable<IActionFilter> filters, Func<Task<HttpResponseMessage>> innerAction)
+ {
+ Contract.Assert(actionContext != null);
+ Contract.Assert(filters != null);
+ Contract.Assert(innerAction != null);
+
+ // Because the continuation gets built from the inside out we need to reverse the filter list
+ // so that least specific filters (Global) get run first and the most specific filters (Action) get run last.
+ filters = filters.Reverse();
+
+ Func<Task<HttpResponseMessage>> result = filters.Aggregate(innerAction, (continuation, filter) =>
+ {
+ return () => filter.ExecuteActionFilterAsync(actionContext, cancellationToken, continuation);
+ });
+
+ return result;
+ }
+
+ #region IDisposable
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_disposed)
+ {
+ _disposed = true;
+ if (disposing)
+ {
+ // TODO: Dispose controller state
+ }
+ }
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Quickly split filters into different types
+ /// </summary>
+ /// <remarks>Avoid <see cref="M:ReadOnlyCollection.Select"/> because it has a very slow implementation that shows on profiles.</remarks>
+ private class FilterGrouping
+ {
+ private List<IActionFilter> _actionFilters = new List<IActionFilter>();
+ private List<IAuthorizationFilter> _authorizationFilters = new List<IAuthorizationFilter>();
+ private List<IExceptionFilter> _exceptionFilters = new List<IExceptionFilter>();
+
+ public FilterGrouping(IEnumerable<FilterInfo> filters)
+ {
+ Contract.Assert(filters != null);
+
+ foreach (FilterInfo f in filters)
+ {
+ var filter = f.Instance;
+ Categorize(filter, _actionFilters);
+ Categorize(filter, _authorizationFilters);
+ Categorize(filter, _exceptionFilters);
+ }
+ }
+
+ public IEnumerable<IActionFilter> ActionFilters
+ {
+ get { return _actionFilters; }
+ }
+
+ public IEnumerable<IAuthorizationFilter> AuthorizationFilters
+ {
+ get { return _authorizationFilters; }
+ }
+
+ public IEnumerable<IExceptionFilter> ExceptionFilters
+ {
+ get { return _exceptionFilters; }
+ }
+
+ private static void Categorize<T>(IFilter filter, List<T> list) where T : class
+ {
+ T match = filter as T;
+ if (match != null)
+ {
+ list.Add(match);
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/AuthorizeAttribute.cs b/src/System.Web.Http/AuthorizeAttribute.cs
new file mode 100644
index 00000000..56474a7f
--- /dev/null
+++ b/src/System.Web.Http/AuthorizeAttribute.cs
@@ -0,0 +1,178 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Security.Principal;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Filters;
+
+namespace System.Web.Http
+{
+ /// <summary>
+ /// An authorization filter that verifies the request's <see cref="IPrincipal"/>.
+ /// </summary>
+ /// <remarks>You can declare multiple of these attributes per action. You can also use <see cref="AllowAnonymousAttribute"/>
+ /// to disable authorization for a specific action.</remarks>
+ /// <seealso cref="M:AuthorizeCore"/>
+ [SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", Justification = "We want to support extensibility")]
+ [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = true)]
+ public class AuthorizeAttribute : AuthorizationFilterAttribute
+ {
+ private static readonly string[] _emptyArray = new string[0];
+
+ private readonly object _typeId = new object();
+
+ private string _roles;
+ private string[] _rolesSplit = _emptyArray;
+ private string _users;
+ private string[] _usersSplit = _emptyArray;
+
+ /// <summary>
+ /// Gets or sets the authorized roles.
+ /// </summary>
+ /// <value>
+ /// The roles string.
+ /// </value>
+ /// <remarks>Multiple role names can be specified using the comma character as a separator.</remarks>
+ public string Roles
+ {
+ get { return _roles ?? String.Empty; }
+ set
+ {
+ _roles = value;
+ _rolesSplit = SplitString(value);
+ }
+ }
+
+ /// <summary>
+ /// Gets a unique identifier for this <see cref="T:System.Attribute"/>.
+ /// </summary>
+ /// <returns>The unique identifier for the attribute.</returns>
+ public override object TypeId
+ {
+ get { return _typeId; }
+ }
+
+ /// <summary>
+ /// Gets or sets the authorized users.
+ /// </summary>
+ /// <value>
+ /// The users string.
+ /// </value>
+ /// <remarks>Multiple role names can be specified using the comma character as a separator.</remarks>
+ public string Users
+ {
+ get { return _users ?? String.Empty; }
+ set
+ {
+ _users = value;
+ _usersSplit = SplitString(value);
+ }
+ }
+
+ /// <summary>
+ /// Determines whether access for this particular <paramref name="request"/> is authorized. This method uses the user <see cref="IPrincipal"/>
+ /// returned via <see cref="M:HttpRequestMessageExtensions.GetUserPrincipal"/>. Authorization is denied if the user is not authenticated,
+ /// the user is not in the authorized group of <see cref="P:Users"/> (if defined), or if the user is not in any of the authorized
+ /// <see cref="P:Roles"/> (if defined).
+ /// </summary>
+ /// <param name="request">The request.</param>
+ /// <returns><c>true</c> if access is authorized; otherwise <c>false</c>.</returns>
+ private bool AuthorizeCore(HttpRequestMessage request)
+ {
+ IPrincipal user = request.GetUserPrincipal();
+ if (user == null || !user.Identity.IsAuthenticated)
+ {
+ return false;
+ }
+
+ if (_usersSplit.Length > 0 && !_usersSplit.Contains(user.Identity.Name, StringComparer.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ if (_rolesSplit.Length > 0 && !_rolesSplit.Any(user.IsInRole))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Called when an action is being authorized. This method uses the user <see cref="IPrincipal"/>
+ /// returned via <see cref="M:HttpRequestMessageExtensions.GetUserPrincipal"/>. Authorization is denied if
+ /// - the request is not associated with any user.
+ /// - the user is not authenticated,
+ /// - the user is authenticated but is not in the authorized group of <see cref="P:Users"/> (if defined), or if the user
+ /// is not in any of the authorized <see cref="P:Roles"/> (if defined).
+ ///
+ /// If authorization is denied then this method will invoke <see cref="M:HandleUnauthorizedRequest"/> to process the unauthorized request.
+ /// </summary>
+ /// <remarks>You can use <see cref="AllowAnonymousAttribute"/> to cause authorization checks to be skipped for a particular
+ /// action or controller.</remarks>
+ /// <param name="actionContext">The context.</param>
+ /// <exception cref="ArgumentNullException">The context parameter is null.</exception>
+ public override void OnAuthorization(HttpActionContext actionContext)
+ {
+ if (actionContext == null)
+ {
+ throw Error.ArgumentNull("actionContext");
+ }
+
+ if (SkipAuthorization(actionContext))
+ {
+ return;
+ }
+
+ if (!AuthorizeCore(actionContext.ControllerContext.Request))
+ {
+ HandleUnauthorizedRequest(actionContext);
+ }
+ }
+
+ /// <summary>
+ /// Processes requests that fail authorization. This default implementation creates a new response with the
+ /// Unauthorized status code. Override this method to provide your own handling for unauthorized requests.
+ /// </summary>
+ /// <param name="actionContext">The context.</param>
+ protected virtual void HandleUnauthorizedRequest(HttpActionContext actionContext)
+ {
+ if (actionContext == null)
+ {
+ throw Error.ArgumentNull("actionContext");
+ }
+
+ actionContext.Response = actionContext.ControllerContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
+ }
+
+ private static bool SkipAuthorization(HttpActionContext actionContext)
+ {
+ Contract.Assert(actionContext != null);
+
+ return actionContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Any()
+ || actionContext.ControllerContext.ControllerDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Any();
+ }
+
+ /// <summary>
+ /// Splits the string on commas and removes any leading/trailing whitespace from each result item.
+ /// </summary>
+ /// <param name="original">The input string.</param>
+ /// <returns>An array of strings parsed from the input <paramref name="original"/> string.</returns>
+ internal static string[] SplitString(string original)
+ {
+ if (String.IsNullOrEmpty(original))
+ {
+ return _emptyArray;
+ }
+
+ var split = from piece in original.Split(',')
+ let trimmed = piece.Trim()
+ where !String.IsNullOrEmpty(trimmed)
+ select trimmed;
+ return split.ToArray();
+ }
+ }
+}
diff --git a/src/System.Web.Http/Controllers/ActionResponseConverter.cs b/src/System.Web.Http/Controllers/ActionResponseConverter.cs
new file mode 100644
index 00000000..9bc8119e
--- /dev/null
+++ b/src/System.Web.Http/Controllers/ActionResponseConverter.cs
@@ -0,0 +1,76 @@
+using System.Net.Http;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Internal;
+
+namespace System.Web.Http.Controllers
+{
+ internal abstract class ActionResponseConverter
+ {
+ private static readonly Type _genericResponseMessageConverterType = typeof(HttpResponseMessageConverter<>);
+ private static readonly Type _genericTaskActionResponseConverterType = typeof(TaskActionResponseConverter<>);
+ protected static readonly ActionResponseConverter SimpleHttpResponseMessageConverter = new HttpResponseMessageConverter();
+ protected static readonly ActionResponseConverter HttpContentMessageConverter = new HttpContentMessageConverter();
+ protected static readonly ActionResponseConverter VoidHttpResponseMessageConverter = new VoidHttpResponseMessageConverter();
+ protected static readonly ActionResponseConverter TaskActionResponseConverter = new TaskActionResponseConverter();
+
+ public abstract Task<HttpResponseMessage> Convert(HttpControllerContext controllerContext, object responseValue, CancellationToken cancellation);
+
+ public static ActionResponseConverter GetResponseMessageConverter(Type responseContentType)
+ {
+ if (typeof(Task).IsAssignableFrom(responseContentType))
+ {
+ if (responseContentType.IsGenericType)
+ {
+ Type actualResponseContentType = responseContentType.GetGenericArguments()[0];
+ return GetGenericTaskActionResponseConverter(actualResponseContentType);
+ }
+ else
+ {
+ return TaskActionResponseConverter;
+ }
+ }
+ else if (responseContentType == typeof(void))
+ {
+ return VoidHttpResponseMessageConverter;
+ }
+ else
+ {
+ responseContentType = TypeHelper.GetHttpResponseOrContentInnerTypeOrNull(responseContentType) ?? responseContentType;
+
+ if (TypeHelper.IsHttpResponse(responseContentType))
+ {
+ return SimpleHttpResponseMessageConverter;
+ }
+ else if (TypeHelper.IsHttpContent(responseContentType))
+ {
+ return HttpContentMessageConverter;
+ }
+ else
+ {
+ return GetGenericHttpResponseMessageConverter(responseContentType);
+ }
+ }
+ }
+
+ // This is the general purpose catch-all converter for arbitrary objects.
+ private static ActionResponseConverter GetGenericHttpResponseMessageConverter(Type responseContentType)
+ {
+ Type closedConverterType = _genericResponseMessageConverterType.MakeGenericType(new Type[] { responseContentType });
+ ConstructorInfo constructor = closedConverterType.GetConstructor(Type.EmptyTypes);
+
+ // REVIEW: Cache converter?
+ return constructor.Invoke(null) as ActionResponseConverter;
+ }
+
+ private static ActionResponseConverter GetGenericTaskActionResponseConverter(Type responseContentType)
+ {
+ Type closedConverterType = _genericTaskActionResponseConverterType.MakeGenericType(new Type[] { responseContentType });
+ ConstructorInfo constructor = closedConverterType.GetConstructor(new Type[] { typeof(ActionResponseConverter) });
+
+ // REVIEW: Cache converter?
+ return constructor.Invoke(new object[] { GetResponseMessageConverter(responseContentType) }) as ActionResponseConverter;
+ }
+ }
+}
diff --git a/src/System.Web.Http/Controllers/ApiControllerActionInvoker.cs b/src/System.Web.Http/Controllers/ApiControllerActionInvoker.cs
new file mode 100644
index 00000000..a5c704fe
--- /dev/null
+++ b/src/System.Web.Http/Controllers/ApiControllerActionInvoker.cs
@@ -0,0 +1,71 @@
+using System.Collections.Concurrent;
+using System.Diagnostics.Contracts;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Common;
+
+namespace System.Web.Http.Controllers
+{
+ public class ApiControllerActionInvoker : IHttpActionInvoker
+ {
+ private static ConcurrentDictionary<Type, ActionResponseConverter> _actionResponseConverterCache = new ConcurrentDictionary<Type, ActionResponseConverter>();
+
+ public virtual Task<HttpResponseMessage> InvokeActionAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
+ {
+ if (actionContext == null)
+ {
+ throw Error.ArgumentNull("actionContext");
+ }
+
+ Contract.Assert(actionContext.ActionDescriptor != null);
+ HttpActionDescriptor actionDescriptor = actionContext.ActionDescriptor;
+ HttpControllerContext controllerContext = actionContext.ControllerContext;
+ Task<HttpResponseMessage> invocationTask = TaskHelpers.RunSynchronously(
+ () =>
+ {
+ // Action always returns synchronously.
+ // 1. Either it runs synchronously and
+ // a. returns an immediate result The ActionResponseConverter will then wrap that in a task
+ // b. or throws an exception, which this helper may catch and wrap as a task.
+ // 2. Or if it needs to do IO, it created a task and returns the task. We can then return that task.
+ object result = actionDescriptor.Execute(controllerContext, actionContext.ActionArguments);
+
+ // The static signature for the action may return object. So check the runtime result type.
+ // Serializers key off the return type. Sometimes They may only understand a base class and not a derived class.
+ // So if the action specifies something more specific than object, use the precise return type.
+ Type returnType = actionDescriptor.ReturnType;
+ if ((returnType == typeof(object)) && (result != null))
+ {
+ returnType = result.GetType();
+ }
+
+ ActionResponseConverter responseConverter = _actionResponseConverterCache.GetOrAdd(returnType, ActionResponseConverter.GetResponseMessageConverter);
+ return responseConverter.Convert(controllerContext, result, cancellationToken);
+ },
+ cancellationToken);
+
+ // Error handling for HttpResponseException
+ return invocationTask.Catch<HttpResponseMessage>(
+ (exception) =>
+ {
+ HttpResponseException httpResponseException = exception as HttpResponseException;
+
+ if (httpResponseException != null)
+ {
+ HttpResponseMessage response = httpResponseException.Response;
+ if (response.RequestMessage == null)
+ {
+ response.RequestMessage = actionContext.ControllerContext.Request;
+ }
+
+ return TaskHelpers.FromResult<HttpResponseMessage>(response);
+ }
+
+ // Propagate all other exceptions
+ return TaskHelpers.FromError<HttpResponseMessage>(exception);
+ },
+ cancellationToken);
+ }
+ }
+}
diff --git a/src/System.Web.Http/Controllers/ApiControllerActionSelector.cs b/src/System.Web.Http/Controllers/ApiControllerActionSelector.cs
new file mode 100644
index 00000000..cddab12b
--- /dev/null
+++ b/src/System.Web.Http/Controllers/ApiControllerActionSelector.cs
@@ -0,0 +1,381 @@
+using System.Collections.Generic;
+using System.Diagnostics.Contracts;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Reflection;
+using System.Text;
+using System.Threading;
+using System.Web.Http.Common;
+using System.Web.Http.Internal;
+using System.Web.Http.Properties;
+
+namespace System.Web.Http.Controllers
+{
+ /// <summary>
+ /// Reflection based action selector.
+ /// We optimize for the case where we have an <see cref="ApiControllerActionSelector"/> instance per <see cref="HttpControllerDescriptor"/>
+ /// instance but can support cases where there are many <see cref="HttpControllerDescriptor"/> instances for one
+ /// <see cref="ApiControllerActionSelector"/> as well. In the latter case the lookup is slightly slower because it goes through
+ /// the <see cref="P:HttpControllerDescriptor.Properties"/> dictionary.
+ /// </summary>
+ public class ApiControllerActionSelector : IHttpActionSelector
+ {
+ private const string ActionRouteKey = "action";
+ private const string ControllerRouteKey = "controller";
+
+ private ActionSelectorCacheItem _fastCache;
+ private readonly object _cacheKey = new object();
+
+ public virtual HttpActionDescriptor SelectAction(HttpControllerContext controllerContext)
+ {
+ if (controllerContext == null)
+ {
+ throw Error.ArgumentNull("controllerContext");
+ }
+
+ ActionSelectorCacheItem internalSelector = GetInternalSelector(controllerContext.ControllerDescriptor);
+ return internalSelector.SelectAction(controllerContext);
+ }
+
+ public virtual ILookup<string, HttpActionDescriptor> GetActionMapping(HttpControllerDescriptor controllerDescriptor)
+ {
+ if (controllerDescriptor == null)
+ {
+ throw Error.ArgumentNull("controllerDescriptor");
+ }
+
+ ActionSelectorCacheItem internalSelector = GetInternalSelector(controllerDescriptor);
+ return internalSelector.GetActionMapping();
+ }
+
+ private ActionSelectorCacheItem GetInternalSelector(HttpControllerDescriptor controllerDescriptor)
+ {
+ // First check in the local fast cache and if not a match then look in the broader
+ // HttpControllerDescriptor.Properties cache
+ if (_fastCache == null)
+ {
+ ActionSelectorCacheItem selector = new ActionSelectorCacheItem(controllerDescriptor);
+ Interlocked.CompareExchange(ref _fastCache, selector, null);
+ return selector;
+ }
+ else if (_fastCache.HttpControllerDescriptor == controllerDescriptor)
+ {
+ // If the key matches and we already have the delegate for creating an instance then just execute it
+ return _fastCache;
+ }
+ else
+ {
+ // If the key doesn't match then lookup/create delegate in the HttpControllerDescriptor.Properties for
+ // that HttpControllerDescriptor instance
+ ActionSelectorCacheItem selector = (ActionSelectorCacheItem)controllerDescriptor.Properties.GetOrAdd(
+ _cacheKey,
+ _ => new ActionSelectorCacheItem(controllerDescriptor));
+ return selector;
+ }
+ }
+
+ // All caching is in a dedicated cache class, which may be optionally shared across selector instances.
+ // Make this a private nested class so that nobody else can conflict with our state.
+ // Cache is initialized during ctor on a single thread.
+ private class ActionSelectorCacheItem
+ {
+ private readonly HttpControllerDescriptor _controllerDescriptor;
+
+ private readonly HttpActionDescriptor[] _actionDescriptors;
+
+ private readonly IDictionary<HttpActionDescriptor, IEnumerable<string>> _actionParameterNames = new Dictionary<HttpActionDescriptor, IEnumerable<string>>();
+
+ private readonly ILookup<string, HttpActionDescriptor> _actionNameMapping;
+
+ // Selection commonly looks up an action by verb.
+ // Cache this mapping. These caches are completely optional and we still behave correctly if we cache miss.
+ // We can adjust the specific set we cache based on profiler information.
+ // Conceptually, this set of caches could be a HttpMethod --> ReflectedHttpActionDescriptor[].
+ // - Beware that HttpMethod has a very slow hash function (it does case-insensitive string hashing). So don't use Dict.
+ // - there are unbounded number of http methods, so make sure the cache doesn't grow indefinitely.
+ // - we can build the cache at startup and don't need to continually add to it.
+ private readonly HttpMethod[] _cacheListVerbKinds = new HttpMethod[] { HttpMethod.Get, HttpMethod.Put, HttpMethod.Post };
+ private readonly ReflectedHttpActionDescriptor[][] _cacheListVerbs;
+
+ public ActionSelectorCacheItem(HttpControllerDescriptor controllerDescriptor)
+ {
+ Contract.Assert(controllerDescriptor != null);
+
+ // Initialize the cache entirely in the ctor on a single thread.
+ _controllerDescriptor = controllerDescriptor;
+
+ MethodInfo[] allMethods = _controllerDescriptor.ControllerType.GetMethods(BindingFlags.Instance | BindingFlags.Public);
+ MethodInfo[] validMethods = Array.FindAll(allMethods, IsValidActionMethod);
+
+ _actionDescriptors = new HttpActionDescriptor[validMethods.Length];
+ for (int i = 0; i < validMethods.Length; i++)
+ {
+ MethodInfo method = validMethods[i];
+ HttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor(_controllerDescriptor, method);
+ _actionDescriptors[i] = actionDescriptor;
+
+ // Build action parameter name mapping, only consider parameters that are simple types and doesn't have default values
+ _actionParameterNames.Add(
+ actionDescriptor,
+ method.GetParameters()
+ .Where(parameter => TypeHelper.IsSimpleType(parameter.ParameterType) && !parameter.IsOptional)
+ .Select(parameter => parameter.Name));
+ }
+
+ _actionNameMapping = _actionDescriptors.ToLookup(actionDesc => actionDesc.ActionName, StringComparer.OrdinalIgnoreCase);
+
+ // Bucket the action descriptors by common verbs.
+ int len = _cacheListVerbKinds.Length;
+ _cacheListVerbs = new ReflectedHttpActionDescriptor[len][];
+ for (int i = 0; i < len; i++)
+ {
+ _cacheListVerbs[i] = FindActionsForVerbWorker(_cacheListVerbKinds[i]);
+ }
+ }
+
+ public HttpControllerDescriptor HttpControllerDescriptor
+ {
+ get { return _controllerDescriptor; }
+ }
+
+ public HttpActionDescriptor SelectAction(HttpControllerContext controllerContext)
+ {
+ string actionName;
+ bool useActionName = controllerContext.RouteData.Values.TryGetValue(ActionRouteKey, out actionName);
+
+ ICollection<HttpActionDescriptor> actionsFoundByHttpMethods;
+
+ HttpMethod incomingMethod = controllerContext.Request.Method;
+
+ // First get an initial candidate list.
+ if (useActionName)
+ {
+ // We have an explicit {action} value, do traditional binding. Just lookup by actionName
+ HttpActionDescriptor[] actionsFoundByName = _actionNameMapping[actionName].ToArray();
+
+ // Throws HttpResponseException with NotFound status because no action matches the Name
+ if (actionsFoundByName.Length == 0)
+ {
+ throw new HttpResponseException(
+ Error.Format(SRResources.ApiControllerActionSelector_ActionNameNotFound, _controllerDescriptor.ControllerName, actionName),
+ HttpStatusCode.NotFound);
+ }
+
+ // filter by verbs.
+ actionsFoundByHttpMethods = RemoveIncompatibleVerbs(incomingMethod, actionsFoundByName).ToArray();
+ }
+ else
+ {
+ // No {action} parameter, infer it from the verb.
+ actionsFoundByHttpMethods = FindActionsForVerb(incomingMethod);
+ }
+
+ // Throws HttpResponseException with MethodNotAllowed status because no action matches the Http Method
+ if (actionsFoundByHttpMethods.Count == 0)
+ {
+ throw new HttpResponseException(
+ Error.Format(SRResources.ApiControllerActionSelector_HttpMethodNotSupported, incomingMethod),
+ HttpStatusCode.MethodNotAllowed);
+ }
+
+ // If there are multiple candidates, then apply overload resolution logic.
+ if (actionsFoundByHttpMethods.Count > 1)
+ {
+ actionsFoundByHttpMethods = FindActionUsingRouteAndQueryParameters(controllerContext, actionsFoundByHttpMethods).ToArray();
+ }
+
+ List<ReflectedHttpActionDescriptor> selectedActions = RunSelectionFilters(controllerContext, actionsFoundByHttpMethods);
+ actionsFoundByHttpMethods = null;
+
+ switch (selectedActions.Count)
+ {
+ case 0:
+ // Throws HttpResponseException with NotFound status because no action matches the request
+ throw new HttpResponseException(
+ Error.Format(SRResources.ApiControllerActionSelector_ActionNotFound, _controllerDescriptor.ControllerName),
+ HttpStatusCode.NotFound);
+ case 1:
+ return selectedActions[0];
+ default:
+ // Throws HttpResponseException with InternalServerError status because multiple action matches the request
+ string ambiguityList = CreateAmbiguousMatchList(selectedActions);
+ throw new HttpResponseException(
+ Error.Format(SRResources.ApiControllerActionSelector_AmbiguousMatch, ambiguityList),
+ HttpStatusCode.InternalServerError);
+ }
+ }
+
+ public ILookup<string, HttpActionDescriptor> GetActionMapping()
+ {
+ return _actionNameMapping;
+ }
+
+ private IEnumerable<HttpActionDescriptor> FindActionUsingRouteAndQueryParameters(HttpControllerContext controllerContext, IEnumerable<HttpActionDescriptor> actionsFound)
+ {
+ // TODO, DevDiv 320655, improve performance of this method.
+ IDictionary<string, object> routeValues = controllerContext.RouteData.Values;
+ IEnumerable<string> routeParameterNames = routeValues.Select(route => route.Key)
+ .Where(key =>
+ !String.Equals(key, ControllerRouteKey, StringComparison.OrdinalIgnoreCase) &&
+ !String.Equals(key, ActionRouteKey, StringComparison.OrdinalIgnoreCase));
+
+ IEnumerable<string> queryParameterNames = controllerContext.Request.RequestUri.ParseQueryString().AllKeys;
+ bool hasRouteParameters = routeParameterNames.Any();
+ bool hasQueryParameters = queryParameterNames.Any();
+
+ if (hasRouteParameters || hasQueryParameters)
+ {
+ // refine the results based on route parameters to make sure that route parameters take precedence over query parameters
+ if (hasRouteParameters && hasQueryParameters)
+ {
+ // route parameters is a subset of action parameters
+ actionsFound = actionsFound.Where(descriptor => !routeParameterNames.Except(_actionParameterNames[descriptor], StringComparer.OrdinalIgnoreCase).Any());
+ }
+
+ // further refine the results making sure that action parameters is a subset of route parameters and query parameters
+ if (actionsFound.Count() > 1)
+ {
+ IEnumerable<string> combinedParameterNames = queryParameterNames.Union(routeParameterNames);
+
+ // action parameters is a subset of route parameters and query parameters
+ actionsFound = actionsFound.Where(descriptor => !_actionParameterNames[descriptor].Except(combinedParameterNames, StringComparer.OrdinalIgnoreCase).Any());
+
+ // select the results with the longest parameter match
+ if (actionsFound.Count() > 1)
+ {
+ actionsFound = actionsFound
+ .GroupBy(descriptor => _actionParameterNames[descriptor].Count())
+ .OrderByDescending(g => g.Key)
+ .First();
+ }
+ }
+ }
+ else
+ {
+ // return actions with no parameters
+ actionsFound = actionsFound.Where(descriptor => !_actionParameterNames[descriptor].Any());
+ }
+
+ return actionsFound;
+ }
+
+ private static List<ReflectedHttpActionDescriptor> RunSelectionFilters(HttpControllerContext controllerContext, IEnumerable<HttpActionDescriptor> descriptorsFound)
+ {
+ // remove all methods which are opting out of this request
+ // to opt out, at least one attribute defined on the method must return false
+
+ List<ReflectedHttpActionDescriptor> matchesWithSelectionAttributes = new List<ReflectedHttpActionDescriptor>();
+ List<ReflectedHttpActionDescriptor> matchesWithoutSelectionAttributes = new List<ReflectedHttpActionDescriptor>();
+
+ foreach (ReflectedHttpActionDescriptor actionDescriptor in descriptorsFound)
+ {
+ IActionMethodSelector[] attrs = actionDescriptor.CacheAttrsIActionMethodSelector;
+ if (attrs.Length == 0)
+ {
+ matchesWithoutSelectionAttributes.Add(actionDescriptor);
+ }
+ else
+ {
+ bool match = Array.TrueForAll(attrs, selector => selector.IsValidForRequest(controllerContext, actionDescriptor.MethodInfo));
+ if (match)
+ {
+ matchesWithSelectionAttributes.Add(actionDescriptor);
+ }
+ }
+ }
+
+ // if a matching action method had a selection attribute, consider it more specific than a matching action method
+ // without a selection attribute
+ return (matchesWithSelectionAttributes.Count > 0) ? matchesWithSelectionAttributes : matchesWithoutSelectionAttributes;
+ }
+
+ // This is called when we don't specify an Action name
+ // Get list of actions that match a given verb. This can match by name or IActionHttpMethodSelecto
+ private ReflectedHttpActionDescriptor[] FindActionsForVerb(HttpMethod verb)
+ {
+ // Check cache for common verbs.
+ for (int i = 0; i < _cacheListVerbKinds.Length; i++)
+ {
+ if (verb == _cacheListVerbKinds[i])
+ {
+ return _cacheListVerbs[i];
+ }
+ }
+
+ // General case for any verbs.
+ return FindActionsForVerbWorker(verb);
+ }
+
+ // This is called when we don't specify an Action name
+ // Get list of actions that match a given verb. This can match by name or IActionHttpMethodSelector.
+ // Since this list is fixed for a given verb type, it can be pre-computed and cached.
+ // This function should not do caching. It's the helper that builds the caches.
+ private ReflectedHttpActionDescriptor[] FindActionsForVerbWorker(HttpMethod verb)
+ {
+ List<ReflectedHttpActionDescriptor> listMethods = new List<ReflectedHttpActionDescriptor>();
+
+ foreach (ReflectedHttpActionDescriptor descriptor in _actionDescriptors)
+ {
+ if (descriptor.SupportedHttpMethods.Contains(verb))
+ {
+ listMethods.Add(descriptor);
+ }
+ }
+
+ return listMethods.ToArray();
+ }
+
+ // This is called when we have an action name.
+ // This filters our any incompatible verbs from the incoming action list
+ private static IEnumerable<HttpActionDescriptor> RemoveIncompatibleVerbs(HttpMethod incomingMethod, IEnumerable<HttpActionDescriptor> descriptorsFound)
+ {
+ return descriptorsFound.Where(actionDescriptor =>
+ {
+ if (actionDescriptor.SupportedHttpMethods.Count > 0)
+ {
+ return actionDescriptor.SupportedHttpMethods.Contains(incomingMethod);
+ }
+ else
+ {
+ // No http verb attribute - Match all verbs when action name is used.
+ return true;
+ }
+ });
+ }
+
+ private static string CreateAmbiguousMatchList(IEnumerable<HttpActionDescriptor> ambiguousDescriptors)
+ {
+ StringBuilder exceptionMessageBuilder = new StringBuilder();
+ foreach (ReflectedHttpActionDescriptor descriptor in ambiguousDescriptors)
+ {
+ MethodInfo methodInfo = descriptor.MethodInfo;
+
+ exceptionMessageBuilder.AppendLine();
+ exceptionMessageBuilder.Append(Error.Format(
+ SRResources.ActionSelector_AmbiguousMatchType,
+ methodInfo, methodInfo.DeclaringType.FullName));
+ }
+
+ return exceptionMessageBuilder.ToString();
+ }
+
+ private static bool IsValidActionMethod(MethodInfo methodInfo)
+ {
+ if (methodInfo.IsSpecialName)
+ {
+ // not a normal method, e.g. a constructor or an event
+ return false;
+ }
+
+ if (methodInfo.GetBaseDefinition().DeclaringType.IsAssignableFrom(TypeHelper.ApiControllerType))
+ {
+ // is a method on Object, IHttpController, ApiController
+ return false;
+ }
+
+ return true;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/Controllers/HttpActionContext.cs b/src/System.Web.Http/Controllers/HttpActionContext.cs
new file mode 100644
index 00000000..f5bf9375
--- /dev/null
+++ b/src/System.Web.Http/Controllers/HttpActionContext.cs
@@ -0,0 +1,91 @@
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Net.Http.Formatting;
+using System.Web.Http.Common;
+using System.Web.Http.ModelBinding;
+
+namespace System.Web.Http.Controllers
+{
+ /// <summary>
+ /// Contains information for the executing action.
+ /// </summary>
+ public class HttpActionContext
+ {
+ private readonly ModelStateDictionary _modelState = new ModelStateDictionary();
+ private readonly Dictionary<string, object> _operationArguments = new Dictionary<string, object>();
+ private HttpActionDescriptor _actionDescriptor;
+ private HttpControllerContext _controllerContext;
+
+ public HttpActionContext(HttpControllerContext controllerContext, HttpActionDescriptor actionDescriptor)
+ {
+ if (controllerContext == null)
+ {
+ throw Error.ArgumentNull("controllerContext");
+ }
+
+ if (actionDescriptor == null)
+ {
+ throw Error.ArgumentNull("actionDescriptor");
+ }
+
+ _controllerContext = controllerContext;
+ _actionDescriptor = actionDescriptor;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpActionContext"/> class.
+ /// </summary>
+ /// <remarks>The default constructor is intended for use by unit testing only.</remarks>
+ public HttpActionContext()
+ {
+ }
+
+ public HttpControllerContext ControllerContext
+ {
+ get { return _controllerContext; }
+ set
+ {
+ if (value == null)
+ {
+ throw Error.PropertyNull();
+ }
+
+ _controllerContext = value;
+ }
+ }
+
+ public HttpActionDescriptor ActionDescriptor
+ {
+ get { return _actionDescriptor; }
+ set
+ {
+ if (value == null)
+ {
+ throw Error.PropertyNull();
+ }
+
+ _actionDescriptor = value;
+ }
+ }
+
+ public ModelStateDictionary ModelState
+ {
+ get { return _modelState; }
+ }
+
+ public Dictionary<string, object> ActionArguments
+ {
+ get { return _operationArguments; }
+ }
+
+ public HttpResponseMessage Response { get; set; }
+
+ /// <summary>
+ /// Gets the current <see cref="HttpRequestMessage"/>.
+ /// </summary>
+ public HttpRequestMessage Request
+ {
+ get { return _controllerContext != null ? _controllerContext.Request : null; }
+ }
+ }
+}
diff --git a/src/System.Web.Http/Controllers/HttpActionContextExtensions.cs b/src/System.Web.Http/Controllers/HttpActionContextExtensions.cs
new file mode 100644
index 00000000..4e20a6b4
--- /dev/null
+++ b/src/System.Web.Http/Controllers/HttpActionContextExtensions.cs
@@ -0,0 +1,123 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Web.Http.Common;
+using System.Web.Http.Metadata;
+using System.Web.Http.ModelBinding;
+using System.Web.Http.Properties;
+using System.Web.Http.Validation;
+
+namespace System.Web.Http.Controllers
+{
+ /// <summary>
+ /// Extension methods for <see cref="HttpActionContext"/>.
+ /// </summary>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static class HttpActionContextExtensions
+ {
+ /// <summary>
+ /// Gets the <see cref="ModelMetadataProvider"/> instance for a given <see cref="HttpActionContext"/>.
+ /// </summary>
+ /// <param name="actionContext">The context.</param>
+ /// <returns>An <see cref="ModelMetadataProvider"/> instance.</returns>
+ public static ModelMetadataProvider GetMetadataProvider(this HttpActionContext actionContext)
+ {
+ if (actionContext == null)
+ {
+ throw Error.ArgumentNull("actionContext");
+ }
+
+ return actionContext.ControllerContext.Configuration.ServiceResolver.GetModelMetadataProvider();
+ }
+
+ /// <summary>
+ /// Gets the collection of registered <see cref="ModelValidatorProvider"/> instances.
+ /// </summary>
+ /// <param name="actionContext">The context.</param>
+ /// <returns>A collection of <see cref="ModelValidatorProvider"/> instances.</returns>
+ public static IEnumerable<ModelValidatorProvider> GetValidatorProviders(this HttpActionContext actionContext)
+ {
+ if (actionContext == null)
+ {
+ throw Error.ArgumentNull("actionContext");
+ }
+
+ return actionContext.ControllerContext.Configuration.ServiceResolver.GetModelValidatorProviders();
+ }
+
+ /// <summary>
+ /// Gets the collection of registered <see cref="ModelValidator"/> instances.
+ /// </summary>
+ /// <param name="actionContext">The context.</param>
+ /// <param name="metadata">The metadata.</param>
+ /// <returns>A collection of registered <see cref="ModelValidator"/> instances.</returns>
+ public static IEnumerable<ModelValidator> GetValidators(this HttpActionContext actionContext, ModelMetadata metadata)
+ {
+ if (actionContext == null)
+ {
+ throw Error.ArgumentNull("actionContext");
+ }
+
+ IEnumerable<ModelValidatorProvider> validatorProviders = GetValidatorProviders(actionContext);
+ return validatorProviders.SelectMany(provider => provider.GetValidators(metadata, validatorProviders));
+ }
+
+ /// <summary>
+ /// Gets the <see cref="ModelBindingContext"/> for this <see cref="HttpActionContext"/>.
+ /// </summary>
+ /// <param name="actionContext">The action context.</param>
+ /// <param name="bindingContext">The binding context.</param>
+ /// <param name="binder">When this method returns, the value associated with the specified binding context, if the context is found; otherwise, the default value for the type of the value parameter.</param>
+ /// <returns><c>true</c> if <see cref="ModelBindingContext"/> was present; otherwise <c>false</c>.</returns>
+ public static bool TryGetBinder(this HttpActionContext actionContext, ModelBindingContext bindingContext, out IModelBinder binder)
+ {
+ if (actionContext == null)
+ {
+ throw Error.ArgumentNull("actionContext");
+ }
+
+ if (bindingContext == null)
+ {
+ throw Error.ArgumentNull("bindingContext");
+ }
+
+ binder = null;
+ ModelBinderProvider providerFromAttr;
+ if (ModelBindingHelper.TryGetProviderFromAttributes(bindingContext.ModelType, out providerFromAttr))
+ {
+ binder = providerFromAttr.GetBinder(actionContext, bindingContext);
+ }
+ else
+ {
+ binder = actionContext.ControllerContext.Configuration.ServiceResolver.GetModelBinderProviders()
+ .Select(p => p.GetBinder(actionContext, bindingContext))
+ .Where(b => b != null)
+ .FirstOrDefault();
+ }
+
+ return binder != null;
+ }
+
+ /// <summary>
+ /// Gets the <see cref="ModelBindingContext"/> for this <see cref="HttpActionContext"/>.
+ /// </summary>
+ /// <param name="actionContext">The execution context.</param>
+ /// <param name="bindingContext">The binding context.</param>
+ /// <returns>The <see cref="ModelBindingContext"/>.</returns>
+ public static IModelBinder GetBinder(this HttpActionContext actionContext, ModelBindingContext bindingContext)
+ {
+ if (actionContext == null)
+ {
+ throw Error.ArgumentNull("actionContext");
+ }
+
+ IModelBinder binder;
+ if (TryGetBinder(actionContext, bindingContext, out binder))
+ {
+ return binder;
+ }
+
+ throw Error.InvalidOperation(SRResources.ModelBinderProviderCollection_BinderForTypeNotFound, bindingContext.ModelType);
+ }
+ }
+}
diff --git a/src/System.Web.Http/Controllers/HttpActionDescriptor.cs b/src/System.Web.Http/Controllers/HttpActionDescriptor.cs
new file mode 100644
index 00000000..7ab7bf30
--- /dev/null
+++ b/src/System.Web.Http/Controllers/HttpActionDescriptor.cs
@@ -0,0 +1,164 @@
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+using System.Linq;
+using System.Net.Http;
+using System.Web.Http.Common;
+using System.Web.Http.Filters;
+
+namespace System.Web.Http.Controllers
+{
+ public abstract class HttpActionDescriptor
+ {
+ private readonly ConcurrentDictionary<object, object> _properties = new ConcurrentDictionary<object, object>();
+
+ private readonly object _thisLock = new object();
+ private Collection<FilterInfo> _filterPipeline;
+
+ private HttpConfiguration _configuration;
+ private HttpControllerDescriptor _controllerDescriptor;
+ private readonly Collection<HttpMethod> _supportedHttpMethods = new Collection<HttpMethod>();
+
+ protected HttpActionDescriptor()
+ {
+ }
+
+ protected HttpActionDescriptor(HttpControllerDescriptor controllerDescriptor)
+ {
+ if (controllerDescriptor == null)
+ {
+ throw Error.ArgumentNull("controllerDesriptor");
+ }
+
+ _controllerDescriptor = controllerDescriptor;
+ _configuration = _controllerDescriptor.Configuration;
+ }
+
+ public abstract string ActionName { get; }
+
+ public HttpConfiguration Configuration
+ {
+ get { return _configuration; }
+ set
+ {
+ if (value == null)
+ {
+ throw Error.PropertyNull();
+ }
+ _configuration = value;
+ }
+ }
+
+ public HttpControllerDescriptor ControllerDescriptor
+ {
+ get { return _controllerDescriptor; }
+ set
+ {
+ if (value == null)
+ {
+ throw Error.PropertyNull();
+ }
+ _controllerDescriptor = value;
+ }
+ }
+
+ public abstract Type ReturnType { get; }
+
+ public virtual Collection<HttpMethod> SupportedHttpMethods
+ {
+ get { return _supportedHttpMethods; }
+ }
+
+ /// <summary>
+ /// Gets the properties associated with this instance.
+ /// </summary>
+ public ConcurrentDictionary<object, object> Properties
+ {
+ get { return _properties; }
+ }
+
+ public virtual IEnumerable<T> GetCustomAttributes<T>() where T : class
+ {
+ return Enumerable.Empty<T>();
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Filters can be built dynamically")]
+ public virtual IEnumerable<IFilter> GetFilters()
+ {
+ return Enumerable.Empty<IFilter>();
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Parameters can be built dynamically")]
+ public abstract Collection<HttpParameterDescriptor> GetParameters();
+
+ /// <summary>
+ /// Executes the described action.
+ /// </summary>
+ /// <param name="controllerContext">The context.</param>
+ /// <param name="arguments">The arguments.</param>
+ /// <returns>The return value of the action.</returns>
+ public abstract object Execute(HttpControllerContext controllerContext, IDictionary<string, object> arguments);
+
+ /// <summary>
+ /// Returns the filters for the given configuration and action. The filter collection is ordered
+ /// according to the FilterScope (in order from least specific to most specific: First, Global, Controller, Action).
+ ///
+ /// If a given filter disallows duplicates (AllowMultiple=False) then the most specific filter is maintained
+ /// and less specific filters get removed (e.g. if there is a Authorize filter with a Controller scope and another
+ /// one with an Action scope then the one with the Action scope will be maintained and the one with the Controller
+ /// scope will be discarded).
+ /// </summary>
+ /// <returns>A <see cref="Collection{T}"/> of all filters associated with this <see cref="HttpActionDescriptor"/>.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Filter pipeline can be built dynamically")]
+ public virtual Collection<FilterInfo> GetFilterPipeline()
+ {
+ if (_filterPipeline == null)
+ {
+ lock (_thisLock)
+ {
+ if (_filterPipeline == null)
+ {
+ IEnumerable<IFilterProvider> filterProviders = _configuration.ServiceResolver.GetFilterProviders();
+
+ IEnumerable<FilterInfo> filters = filterProviders.SelectMany(fp => fp.GetFilters(_configuration, this)).OrderBy(f => f, FilterInfoComparer.Instance);
+
+ // Need to discard duplicate filters from the end, so that most specific ones get kept (Action scope) and
+ // less specific ones get removed (Global)
+ filters = RemoveDuplicates(filters.Reverse()).Reverse();
+
+ _filterPipeline = new Collection<FilterInfo>(filters.ToList());
+ }
+ }
+ }
+
+ return _filterPipeline;
+ }
+
+ private static IEnumerable<FilterInfo> RemoveDuplicates(IEnumerable<FilterInfo> filters)
+ {
+ Contract.Assert(filters != null);
+
+ HashSet<Type> visitedTypes = new HashSet<Type>();
+
+ foreach (FilterInfo filter in filters)
+ {
+ object filterInstance = filter.Instance;
+ Type filterInstanceType = filterInstance.GetType();
+
+ if (!visitedTypes.Contains(filterInstanceType) || AllowMultiple(filterInstance))
+ {
+ yield return filter;
+ visitedTypes.Add(filterInstanceType);
+ }
+ }
+ }
+
+ private static bool AllowMultiple(object filterInstance)
+ {
+ IFilter filter = filterInstance as IFilter;
+ return filter == null || filter.AllowMultiple;
+ }
+ }
+}
diff --git a/src/System.Web.Http/Controllers/HttpContentMessageConverter.cs b/src/System.Web.Http/Controllers/HttpContentMessageConverter.cs
new file mode 100644
index 00000000..a1361c72
--- /dev/null
+++ b/src/System.Web.Http/Controllers/HttpContentMessageConverter.cs
@@ -0,0 +1,31 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace System.Web.Http.Controllers
+{
+ // Used when converting a HttpContent.
+ internal sealed class HttpContentMessageConverter : ActionResponseConverter
+ {
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "caller becomes owner.")]
+ public override Task<HttpResponseMessage> Convert(HttpControllerContext controllerContext, object responseValue, CancellationToken cancellation)
+ {
+ Contract.Assert(controllerContext != null);
+ return TaskHelpers.RunSynchronously(() => ConvertSync(controllerContext, (HttpContent)responseValue), cancellation);
+ }
+
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "caller becomes owner.")]
+ internal static HttpResponseMessage ConvertSync(HttpControllerContext controllerContext, HttpContent responseValue)
+ {
+ Contract.Assert(controllerContext != null);
+
+ HttpResponseMessage response = new HttpResponseMessage();
+ response.Content = responseValue;
+
+ // Wrap the content in a Response and chain.
+ return HttpResponseMessageConverter.ConvertSync(controllerContext, response);
+ }
+ }
+}
diff --git a/src/System.Web.Http/Controllers/HttpControllerConfigurationAttribute.cs b/src/System.Web.Http/Controllers/HttpControllerConfigurationAttribute.cs
new file mode 100644
index 00000000..32d202da
--- /dev/null
+++ b/src/System.Web.Http/Controllers/HttpControllerConfigurationAttribute.cs
@@ -0,0 +1,23 @@
+using System.Web.Http.Dispatcher;
+
+namespace System.Web.Http.Controllers
+{
+ /// <summary>
+ /// Provides a mechanism for a <see cref="IHttpController"/> implementation to indicate
+ /// what kind of <see cref="IHttpControllerActivator"/>, <see cref="IHttpActionSelector"/>, <see cref="IActionValueBinder"/>
+ /// and <see cref="IHttpActionInvoker"/> to use for that controller. The types are
+ /// first looked up in the <see cref="Services.DependencyResolver"/> and if not found there
+ /// then created directly.
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
+ public sealed class HttpControllerConfigurationAttribute : Attribute
+ {
+ public Type HttpControllerActivator { get; set; }
+
+ public Type HttpActionSelector { get; set; }
+
+ public Type HttpActionInvoker { get; set; }
+
+ public Type ActionValueBinder { get; set; }
+ }
+}
diff --git a/src/System.Web.Http/Controllers/HttpControllerContext.cs b/src/System.Web.Http/Controllers/HttpControllerContext.cs
new file mode 100644
index 00000000..c5f25e78
--- /dev/null
+++ b/src/System.Web.Http/Controllers/HttpControllerContext.cs
@@ -0,0 +1,150 @@
+using System.Net.Http;
+using System.Web.Http.Common;
+using System.Web.Http.Routing;
+
+namespace System.Web.Http.Controllers
+{
+ /// <summary>
+ /// Contains information for a single HTTP operation.
+ /// </summary>
+ public class HttpControllerContext
+ {
+ private HttpConfiguration _configuration;
+ private IHttpRouteData _routeData;
+ private HttpRequestMessage _request;
+
+ private HttpControllerDescriptor _controllerDescriptor;
+ private IHttpController _controller;
+ private UrlHelper _urlHelper;
+
+ public HttpControllerContext(HttpConfiguration configuration, IHttpRouteData routeData, HttpRequestMessage request)
+ {
+ if (configuration == null)
+ {
+ throw Error.ArgumentNull("configuration");
+ }
+
+ if (routeData == null)
+ {
+ throw Error.ArgumentNull("routeData");
+ }
+
+ if (request == null)
+ {
+ throw Error.ArgumentNull("request");
+ }
+
+ _configuration = configuration;
+ _routeData = routeData;
+ _request = request;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpControllerContext"/> class.
+ /// </summary>
+ /// <remarks>The default constructor is intended for use by unit testing only.</remarks>
+ public HttpControllerContext()
+ {
+ }
+
+ public HttpConfiguration Configuration
+ {
+ get { return _configuration; }
+ set
+ {
+ if (value == null)
+ {
+ throw Error.PropertyNull();
+ }
+
+ _configuration = value;
+ }
+ }
+
+ public HttpRequestMessage Request
+ {
+ get { return _request; }
+ set
+ {
+ if (value == null)
+ {
+ throw Error.PropertyNull();
+ }
+
+ _request = value;
+ }
+ }
+
+ public IHttpRouteData RouteData
+ {
+ get { return _routeData; }
+ set
+ {
+ if (value == null)
+ {
+ throw Error.PropertyNull();
+ }
+
+ _routeData = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the controller descriptor.
+ /// </summary>
+ /// <remarks>This property must be filled in by the <see cref="Dispatcher.IHttpControllerFactory"/>.</remarks>
+ /// <value>
+ /// The controller descriptor.
+ /// </value>
+ public HttpControllerDescriptor ControllerDescriptor
+ {
+ get { return _controllerDescriptor; }
+ set
+ {
+ if (value == null)
+ {
+ throw Error.PropertyNull();
+ }
+
+ _controllerDescriptor = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the HTTP controller.
+ /// </summary>
+ /// <remarks>This property must be filled in by the <see cref="Dispatcher.IHttpControllerFactory"/>.</remarks>
+ /// <value>
+ /// The HTTP controller.
+ /// </value>
+ public IHttpController Controller
+ {
+ get { return _controller; }
+ set
+ {
+ if (value == null)
+ {
+ throw Error.PropertyNull();
+ }
+
+ _controller = value;
+ }
+ }
+
+ /// <summary>
+ /// Returns an instance of a UrlHelper, which is used to generate URLs to other APIs.
+ /// </summary>
+ public UrlHelper Url
+ {
+ get
+ {
+ if (_urlHelper == null)
+ {
+ _urlHelper = new UrlHelper(this);
+ }
+ return _urlHelper;
+ }
+ set { _urlHelper = value; }
+ }
+ }
+}
diff --git a/src/System.Web.Http/Controllers/HttpControllerDescriptor.cs b/src/System.Web.Http/Controllers/HttpControllerDescriptor.cs
new file mode 100644
index 00000000..b836af57
--- /dev/null
+++ b/src/System.Web.Http/Controllers/HttpControllerDescriptor.cs
@@ -0,0 +1,265 @@
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+using System.Linq;
+using System.Web.Http.Common;
+using System.Web.Http.Dispatcher;
+using System.Web.Http.Filters;
+using System.Web.Http.Internal;
+
+namespace System.Web.Http.Controllers
+{
+ public class HttpControllerDescriptor
+ {
+ private readonly ConcurrentDictionary<object, object> _properties = new ConcurrentDictionary<object, object>();
+
+ private HttpConfiguration _configuration;
+ private string _controllerName;
+ private Type _controllerType;
+ private IHttpControllerActivator _controllerActivator;
+ private IHttpActionSelector _actionSelector;
+ private IHttpActionInvoker _actionInvoker;
+ private IActionValueBinder _actionValueBinder;
+
+ private object[] _attrCached;
+
+ public HttpControllerDescriptor(HttpConfiguration configuration, string controllerName, Type controllerType)
+ {
+ if (configuration == null)
+ {
+ throw Error.ArgumentNull("configuration");
+ }
+
+ if (controllerName == null)
+ {
+ throw Error.ArgumentNull("controllerName");
+ }
+
+ if (controllerType == null)
+ {
+ throw Error.ArgumentNull("controllerType");
+ }
+
+ _configuration = configuration;
+ _controllerName = controllerName;
+ _controllerType = controllerType;
+
+ Initialize();
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpControllerDescriptor"/> class.
+ /// </summary>
+ /// <remarks>The default constructor is intended for use by unit testing only.</remarks>
+ public HttpControllerDescriptor()
+ {
+ }
+
+ /// <summary>
+ /// Gets the properties associated with this instance.
+ /// </summary>
+ public ConcurrentDictionary<object, object> Properties
+ {
+ get { return _properties; }
+ }
+
+ public HttpConfiguration Configuration
+ {
+ get { return _configuration; }
+ set
+ {
+ if (value == null)
+ {
+ throw Error.PropertyNull();
+ }
+ _configuration = value;
+ }
+ }
+
+ public string ControllerName
+ {
+ get { return _controllerName; }
+ set
+ {
+ if (value == null)
+ {
+ throw Error.PropertyNull();
+ }
+ _controllerName = value;
+ }
+ }
+
+ public Type ControllerType
+ {
+ get { return _controllerType; }
+ set
+ {
+ if (value == null)
+ {
+ throw Error.PropertyNull();
+ }
+ _controllerType = value;
+ }
+ }
+
+ public IHttpControllerActivator HttpControllerActivator
+ {
+ get { return _controllerActivator; }
+ set
+ {
+ if (value == null)
+ {
+ throw Error.PropertyNull();
+ }
+ _controllerActivator = value;
+ }
+ }
+
+ public IHttpActionSelector HttpActionSelector
+ {
+ get { return _actionSelector; }
+ set
+ {
+ if (value == null)
+ {
+ throw Error.PropertyNull();
+ }
+ _actionSelector = value;
+ }
+ }
+
+ public IHttpActionInvoker HttpActionInvoker
+ {
+ get { return _actionInvoker; }
+ set
+ {
+ if (value == null)
+ {
+ throw Error.PropertyNull();
+ }
+ _actionInvoker = value;
+ }
+ }
+
+ public IActionValueBinder ActionValueBinder
+ {
+ get { return _actionValueBinder; }
+ set
+ {
+ if (value == null)
+ {
+ throw Error.PropertyNull();
+ }
+ _actionValueBinder = value;
+ }
+ }
+
+ /// <summary>
+ /// Returns the collection of <see cref="IFilter">filters</see> associated with this descriptor's controller.
+ /// </summary>
+ /// <remarks>The default implementation calls <see cref="GetCustomAttributes{IFilter}()"/>.</remarks>
+ /// <returns>A collection of filters associated with this controller.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Filters can be built dynamically")]
+ public virtual IEnumerable<IFilter> GetFilters()
+ {
+ return GetCustomAttributes<IFilter>();
+ }
+
+ /// <summary>
+ /// Returns a collection of attributes that can be assigned to <typeparamref name="T"/> for this descriptor's controller.
+ /// </summary>
+ /// <remarks>The default implementation retrieves the matching set of attributes declared on <see cref="ControllerType"/>.</remarks>
+ /// <typeparam name="T">Used to filter the collection of attributes. Use a value of <see cref="Object"/> to retrieve all attributes.</typeparam>
+ /// <returns>A collection of attributes associated with this controller.</returns>
+ public virtual IEnumerable<T> GetCustomAttributes<T>() where T : class
+ {
+ // Getting custom attributes via reflection is slow.
+ // But iterating over a object[] to pick out specific types is fast.
+ // Furthermore, many different services may call to ask for different attributes, so we have multiple callers.
+ // That means there's not a single cache for the callers, which means there's some value caching here.
+ if (_attrCached == null)
+ {
+ // Even in a race, we'll just ask for the custom attributes twice.
+ _attrCached = ControllerType.GetCustomAttributes(inherit: true);
+ }
+
+ return TypeHelper.OfType<T>(_attrCached);
+ }
+
+ private void Initialize()
+ {
+ // Look for attribute to provide specialized information for this controller type
+ HttpControllerConfigurationAttribute controllerConfig =
+ _controllerType.GetCustomAttributes<HttpControllerConfigurationAttribute>(inherit: true).FirstOrDefault();
+
+ // If we find attribute then first ask dependency resolver and if we get null then create it ourselves
+ if (controllerConfig != null)
+ {
+ if (controllerConfig.HttpControllerActivator != null)
+ {
+ _controllerActivator = GetService<IHttpControllerActivator>(_configuration, controllerConfig.HttpControllerActivator);
+ }
+
+ if (controllerConfig.HttpActionSelector != null)
+ {
+ _actionSelector = GetService<IHttpActionSelector>(_configuration, controllerConfig.HttpActionSelector);
+ }
+
+ if (controllerConfig.HttpActionInvoker != null)
+ {
+ _actionInvoker = GetService<IHttpActionInvoker>(_configuration, controllerConfig.HttpActionInvoker);
+ }
+
+ if (controllerConfig.ActionValueBinder != null)
+ {
+ _actionValueBinder = GetService<IActionValueBinder>(_configuration, controllerConfig.ActionValueBinder);
+ }
+ }
+
+ // For everything still null we call the dependency resolver as normal.
+ if (_controllerActivator == null)
+ {
+ _controllerActivator = Configuration.ServiceResolver.GetHttpControllerActivator();
+ }
+
+ if (_actionSelector == null)
+ {
+ _actionSelector = Configuration.ServiceResolver.GetActionSelector();
+ }
+
+ if (_actionInvoker == null)
+ {
+ _actionInvoker = Configuration.ServiceResolver.GetActionInvoker();
+ }
+
+ if (_actionValueBinder == null)
+ {
+ _actionValueBinder = Configuration.ServiceResolver.GetActionValueBinder();
+ }
+ }
+
+ /// <summary>
+ /// Helper for looking up or activating <see cref="IHttpControllerActivator"/>, <see cref="IHttpActionSelector"/>,
+ /// and <see cref="IHttpActionInvoker"/>. Note that we here use the slow <see cref="M:Activator.CreateInstance"/>
+ /// as the instances live for the lifetime of the <see cref="HttpControllerDescriptor"/> instance itself so there is
+ /// little benefit in caching a delegate.
+ /// </summary>
+ /// <typeparam name="TBase">The type of the base.</typeparam>
+ /// <param name="configuration">The configuration.</param>
+ /// <param name="serviceType">Type of the service.</param>
+ /// <returns>A new instance.</returns>
+ private static TBase GetService<TBase>(HttpConfiguration configuration, Type serviceType) where TBase : class
+ {
+ Contract.Assert(configuration != null);
+ if (serviceType != null)
+ {
+ return (TBase)configuration.ServiceResolver.GetService(serviceType) ??
+ (TBase)Activator.CreateInstance(serviceType);
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/System.Web.Http/Controllers/HttpParameterDescriptor.cs b/src/System.Web.Http/Controllers/HttpParameterDescriptor.cs
new file mode 100644
index 00000000..ef487a07
--- /dev/null
+++ b/src/System.Web.Http/Controllers/HttpParameterDescriptor.cs
@@ -0,0 +1,120 @@
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Web.Http.Common;
+using System.Web.Http.Internal;
+using System.Web.Http.ModelBinding;
+
+namespace System.Web.Http.Controllers
+{
+ public abstract class HttpParameterDescriptor
+ {
+ private readonly ConcurrentDictionary<object, object> _properties = new ConcurrentDictionary<object, object>();
+
+ private ModelBinderAttribute _modelBinderAttribute;
+ private bool _searchedModelBinderAttribute;
+ private HttpConfiguration _configuration;
+ private HttpActionDescriptor _actionDescriptor;
+
+ protected HttpParameterDescriptor()
+ {
+ }
+
+ protected HttpParameterDescriptor(HttpActionDescriptor actionDescriptor)
+ {
+ if (actionDescriptor == null)
+ {
+ throw Error.ArgumentNull("actionDescriptor");
+ }
+
+ _actionDescriptor = actionDescriptor;
+ _configuration = _actionDescriptor.Configuration;
+ }
+
+ public HttpConfiguration Configuration
+ {
+ get { return _configuration; }
+ set
+ {
+ if (value == null)
+ {
+ throw Error.PropertyNull();
+ }
+ _configuration = value;
+ }
+ }
+
+ public HttpActionDescriptor ActionDescriptor
+ {
+ get { return _actionDescriptor; }
+ set
+ {
+ if (value == null)
+ {
+ throw Error.PropertyNull();
+ }
+ _actionDescriptor = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets the properties associated with this instance.
+ /// </summary>
+ public ConcurrentDictionary<object, object> Properties
+ {
+ get { return _properties; }
+ }
+
+ public virtual object DefaultValue
+ {
+ get { return null; }
+ }
+
+ public abstract string ParameterName { get; }
+
+ public abstract Type ParameterType { get; }
+
+ public virtual string Prefix
+ {
+ get
+ {
+ ModelBinderAttribute attribute = ModelBinderAttribute;
+ return attribute != null && !String.IsNullOrEmpty(attribute.Prefix)
+ ? attribute.Prefix
+ : null;
+ }
+ }
+
+ public virtual ModelBinderAttribute ModelBinderAttribute
+ {
+ get
+ {
+ if (_modelBinderAttribute == null)
+ {
+ if (!_searchedModelBinderAttribute)
+ {
+ _searchedModelBinderAttribute = true;
+ _modelBinderAttribute = FindModelBinderAttribute();
+ }
+ }
+
+ return _modelBinderAttribute;
+ }
+
+ set { _modelBinderAttribute = value; }
+ }
+
+ public virtual IEnumerable<T> GetCustomAttributes<T>() where T : class
+ {
+ return Enumerable.Empty<T>();
+ }
+
+ private ModelBinderAttribute FindModelBinderAttribute()
+ {
+ // Can be on parameter itself or on the parameter's type. Nearest wins.
+ return GetCustomAttributes<ModelBinderAttribute>().SingleOrDefault()
+ ?? ParameterType.GetCustomAttributes<ModelBinderAttribute>(false).SingleOrDefault();
+ }
+ }
+}
diff --git a/src/System.Web.Http/Controllers/HttpResponseMessageConverter.cs b/src/System.Web.Http/Controllers/HttpResponseMessageConverter.cs
new file mode 100644
index 00000000..604d54f1
--- /dev/null
+++ b/src/System.Web.Http/Controllers/HttpResponseMessageConverter.cs
@@ -0,0 +1,57 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+using System.Net;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace System.Web.Http.Controllers
+{
+ // Used when the action's return value is already a HttpResponseMessage
+ internal sealed class HttpResponseMessageConverter : ActionResponseConverter
+ {
+ public override Task<HttpResponseMessage> Convert(HttpControllerContext controllerContext, object responseValue, CancellationToken cancellation)
+ {
+ Contract.Assert(controllerContext != null);
+ return TaskHelpers.RunSynchronously(() => ConvertSync(controllerContext, (HttpResponseMessage)responseValue), cancellation);
+ }
+
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "caller becomes owner.")]
+ internal static HttpResponseMessage ConvertSync(HttpControllerContext controllerContext, HttpResponseMessage responseValue)
+ {
+ Contract.Assert(controllerContext != null);
+
+ responseValue.RequestMessage = controllerContext.Request;
+ return responseValue;
+ }
+ }
+
+ // Used when converting a TResponseValue, which could be an arbitrary type for which we
+ // don't have a more specific converter (eg, it's not HttpContent, HttpRequestMessage).
+ internal sealed class HttpResponseMessageConverter<TResponseValue> : ActionResponseConverter
+ {
+ public override Task<HttpResponseMessage> Convert(HttpControllerContext controllerContext, object responseValue, CancellationToken cancellation)
+ {
+ Contract.Assert(controllerContext != null);
+ return TaskHelpers.RunSynchronously(() => ConvertSync(controllerContext, responseValue), cancellation);
+ }
+
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "caller becomes owner.")]
+ private static HttpResponseMessage ConvertSync(HttpControllerContext controllerContext, object responseValue)
+ {
+ HttpRequestMessage request = controllerContext.Request;
+
+ HttpContent responseAsContent = responseValue as HttpContent;
+ if (responseAsContent != null)
+ {
+ var resp = request.CreateResponse();
+ resp.Content = responseAsContent;
+ return resp;
+ }
+
+ HttpConfiguration config = controllerContext.Configuration;
+ HttpResponseMessage response = request.CreateResponse<TResponseValue>(HttpStatusCode.OK, (TResponseValue)responseValue, config);
+ return response;
+ }
+ }
+}
diff --git a/src/System.Web.Http/Controllers/IActionHttpMethodProvider.cs b/src/System.Web.Http/Controllers/IActionHttpMethodProvider.cs
new file mode 100644
index 00000000..2e1c7b4f
--- /dev/null
+++ b/src/System.Web.Http/Controllers/IActionHttpMethodProvider.cs
@@ -0,0 +1,11 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Net.Http;
+
+namespace System.Web.Http.Controllers
+{
+ internal interface IActionHttpMethodProvider
+ {
+ Collection<HttpMethod> HttpMethods { get; }
+ }
+}
diff --git a/src/System.Web.Http/Controllers/IActionMethodSelector.cs b/src/System.Web.Http/Controllers/IActionMethodSelector.cs
new file mode 100644
index 00000000..06b5f0c2
--- /dev/null
+++ b/src/System.Web.Http/Controllers/IActionMethodSelector.cs
@@ -0,0 +1,9 @@
+using System.Reflection;
+
+namespace System.Web.Http.Controllers
+{
+ internal interface IActionMethodSelector
+ {
+ bool IsValidForRequest(HttpControllerContext controllerContext, MethodInfo methodInfo);
+ }
+}
diff --git a/src/System.Web.Http/Controllers/IActionValueBinder.cs b/src/System.Web.Http/Controllers/IActionValueBinder.cs
new file mode 100644
index 00000000..492f1e2a
--- /dev/null
+++ b/src/System.Web.Http/Controllers/IActionValueBinder.cs
@@ -0,0 +1,11 @@
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.ModelBinding;
+
+namespace System.Web.Http.Controllers
+{
+ public interface IActionValueBinder
+ {
+ HttpActionBinding GetBinding(HttpActionDescriptor actionDescriptor);
+ }
+}
diff --git a/src/System.Web.Http/Controllers/IHttpActionInvoker.cs b/src/System.Web.Http/Controllers/IHttpActionInvoker.cs
new file mode 100644
index 00000000..74b220db
--- /dev/null
+++ b/src/System.Web.Http/Controllers/IHttpActionInvoker.cs
@@ -0,0 +1,11 @@
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace System.Web.Http.Controllers
+{
+ public interface IHttpActionInvoker
+ {
+ Task<HttpResponseMessage> InvokeActionAsync(HttpActionContext actionContext, CancellationToken cancellationToken);
+ }
+}
diff --git a/src/System.Web.Http/Controllers/IHttpActionSelector.cs b/src/System.Web.Http/Controllers/IHttpActionSelector.cs
new file mode 100644
index 00000000..5d11945a
--- /dev/null
+++ b/src/System.Web.Http/Controllers/IHttpActionSelector.cs
@@ -0,0 +1,21 @@
+using System.Linq;
+namespace System.Web.Http.Controllers
+{
+ public interface IHttpActionSelector
+ {
+ /// <summary>
+ /// Selects the action.
+ /// </summary>
+ /// <param name="controllerContext">The controller context.</param>
+ /// <returns>The selected action.</returns>
+ HttpActionDescriptor SelectAction(HttpControllerContext controllerContext);
+
+ /// <summary>
+ /// Returns a map, keyed by action string, of all <see cref="HttpActionDescriptor"/> that the selector can select.
+ /// This is primarily called by <see cref="System.Web.Http.Description.IApiExplorer"/> to discover all the possible actions in the controller.
+ /// </summary>
+ /// <param name="controllerDescriptor">The controller descriptor.</param>
+ /// <returns>A map of <see cref="HttpActionDescriptor"/> that the selector can select, or null if the selector does not have a well-defined mapping of <see cref="HttpActionDescriptor"/>.</returns>
+ ILookup<string, HttpActionDescriptor> GetActionMapping(HttpControllerDescriptor controllerDescriptor);
+ }
+}
diff --git a/src/System.Web.Http/Controllers/IHttpController.cs b/src/System.Web.Http/Controllers/IHttpController.cs
new file mode 100644
index 00000000..143f0e46
--- /dev/null
+++ b/src/System.Web.Http/Controllers/IHttpController.cs
@@ -0,0 +1,11 @@
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace System.Web.Http.Controllers
+{
+ public interface IHttpController
+ {
+ Task<HttpResponseMessage> ExecuteAsync(HttpControllerContext controllerContext, CancellationToken cancellationToken);
+ }
+}
diff --git a/src/System.Web.Http/Controllers/ReflectedHttpActionDescriptor.cs b/src/System.Web.Http/Controllers/ReflectedHttpActionDescriptor.cs
new file mode 100644
index 00000000..cb874429
--- /dev/null
+++ b/src/System.Web.Http/Controllers/ReflectedHttpActionDescriptor.cs
@@ -0,0 +1,284 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics.Contracts;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Net;
+using System.Net.Http;
+using System.Reflection;
+using System.Web.Http.Common;
+using System.Web.Http.Filters;
+using System.Web.Http.Internal;
+using System.Web.Http.Properties;
+
+namespace System.Web.Http.Controllers
+{
+ public class ReflectedHttpActionDescriptor : HttpActionDescriptor
+ {
+ private readonly Lazy<Collection<HttpParameterDescriptor>> _parameters;
+
+ private ActionExecutor _actionExecutor;
+ private MethodInfo _methodInfo;
+ private string _actionName;
+ private Collection<HttpMethod> _supportedHttpMethods;
+
+ // Getting custom attributes via reflection is slow.
+ // But iterating over a object[] to pick out specific types is fast.
+ // Furthermore, many different services may call to ask for different attributes, so we have multiple callers.
+ // That means there's not a single cache for the callers, which means there's some value caching here.
+ // This cache can be a 2x speedup in some benchmarks.
+ private object[] _attrCached;
+
+ private static readonly HttpMethod[] _supportedHttpMethodsByConvention = { HttpMethod.Get, HttpMethod.Post, HttpMethod.Put, HttpMethod.Delete };
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ReflectedHttpActionDescriptor"/> class.
+ /// </summary>
+ /// <remarks>The default constructor is intended for use by unit testing only.</remarks>
+ public ReflectedHttpActionDescriptor()
+ {
+ _parameters = new Lazy<Collection<HttpParameterDescriptor>>(() => InitializeParameterDescriptors());
+ _supportedHttpMethods = new Collection<HttpMethod>();
+ }
+
+ public ReflectedHttpActionDescriptor(HttpControllerDescriptor controllerDescriptor, MethodInfo methodInfo)
+ : base(controllerDescriptor)
+ {
+ if (methodInfo == null)
+ {
+ throw Error.ArgumentNull("methodInfo");
+ }
+
+ InitializeProperties(methodInfo);
+ _parameters = new Lazy<Collection<HttpParameterDescriptor>>(() => InitializeParameterDescriptors());
+ }
+
+ /// <summary>
+ /// Caches that the ActionSelector use.
+ /// </summary>
+ internal IActionMethodSelector[] CacheAttrsIActionMethodSelector { get; private set; }
+
+ public override string ActionName
+ {
+ get { return _actionName; }
+ }
+
+ public override Collection<HttpMethod> SupportedHttpMethods
+ {
+ get { return _supportedHttpMethods; }
+ }
+
+ public MethodInfo MethodInfo
+ {
+ get { return _methodInfo; }
+ set
+ {
+ if (value == null)
+ {
+ throw Error.PropertyNull();
+ }
+
+ InitializeProperties(value);
+ }
+ }
+
+ public override Type ReturnType
+ {
+ get { return _methodInfo == null ? null : _methodInfo.ReturnType; }
+ }
+
+ public override IEnumerable<T> GetCustomAttributes<T>()
+ {
+ Contract.Assert(_methodInfo != null); // can't get attributes without the method set!
+ Contract.Assert(_attrCached != null); // setting the method should build the attr cache
+ return TypeHelper.OfType<T>(_attrCached);
+ }
+
+ public override object Execute(HttpControllerContext controllerContext, IDictionary<string, object> arguments)
+ {
+ if (controllerContext == null)
+ {
+ throw Error.ArgumentNull("controllerContext");
+ }
+
+ if (arguments == null)
+ {
+ throw Error.ArgumentNull("arguments");
+ }
+
+ object[] argumentValues = PrepareParameters(arguments);
+ return _actionExecutor.Execute(controllerContext.Controller, argumentValues);
+ }
+
+ public override IEnumerable<IFilter> GetFilters()
+ {
+ return GetCustomAttributes<IFilter>().Concat(base.GetFilters());
+ }
+
+ public override Collection<HttpParameterDescriptor> GetParameters()
+ {
+ return _parameters.Value;
+ }
+
+ private void InitializeProperties(MethodInfo methodInfo)
+ {
+ _methodInfo = methodInfo;
+ _actionExecutor = new ActionExecutor(methodInfo);
+ _attrCached = _methodInfo.GetCustomAttributes(inherit: true);
+ CacheAttrsIActionMethodSelector = _attrCached.OfType<IActionMethodSelector>().ToArray();
+ _actionName = GetActionName(_methodInfo, _attrCached);
+ _supportedHttpMethods = GetSupportedHttpMethods(_actionName, _attrCached);
+ }
+
+ private Collection<HttpParameterDescriptor> InitializeParameterDescriptors()
+ {
+ Contract.Assert(_methodInfo != null);
+
+ List<HttpParameterDescriptor> parameterInfos = _methodInfo.GetParameters().Select(
+ (item) => new ReflectedHttpParameterDescriptor(this, item)).ToList<HttpParameterDescriptor>();
+ return new Collection<HttpParameterDescriptor>(parameterInfos);
+ }
+
+ private object[] PrepareParameters(IDictionary<string, object> parameters)
+ {
+ ParameterInfo[] parameterInfos = MethodInfo.GetParameters();
+ var rawParameterValues = from parameterInfo in parameterInfos
+ select ExtractParameterFromDictionary(parameterInfo, parameters);
+ object[] parametersArray = rawParameterValues.ToArray();
+ return parametersArray;
+ }
+
+ private object ExtractParameterFromDictionary(ParameterInfo parameterInfo, IDictionary<string, object> parameters)
+ {
+ object value;
+
+ if (!parameters.TryGetValue(parameterInfo.Name, out value))
+ {
+ // the key should always be present, even if the parameter value is null
+ throw new HttpResponseException(
+ Error.Format(SRResources.ReflectedActionDescriptor_ParameterNotInDictionary,
+ parameterInfo.Name, parameterInfo.ParameterType, MethodInfo, MethodInfo.DeclaringType),
+ HttpStatusCode.BadRequest);
+ }
+
+ if (value == null && !TypeHelper.TypeAllowsNullValue(parameterInfo.ParameterType))
+ {
+ // tried to pass a null value for a non-nullable parameter type
+ throw new HttpResponseException(
+ Error.Format(SRResources.ReflectedActionDescriptor_ParameterCannotBeNull,
+ parameterInfo.Name, parameterInfo.ParameterType, MethodInfo, MethodInfo.DeclaringType),
+ HttpStatusCode.BadRequest);
+ }
+
+ if (value != null && !parameterInfo.ParameterType.IsInstanceOfType(value))
+ {
+ // value was supplied but is not of the proper type
+ throw new HttpResponseException(
+ Error.Format(SRResources.ReflectedActionDescriptor_ParameterValueHasWrongType,
+ parameterInfo.Name, MethodInfo, MethodInfo.DeclaringType, value.GetType(), parameterInfo.ParameterType),
+ HttpStatusCode.BadRequest);
+ }
+
+ return value;
+ }
+
+ private static string GetActionName(MethodInfo methodInfo, object[] actionAttributes)
+ {
+ ActionNameAttribute nameAttribute = TypeHelper.OfType<ActionNameAttribute>(actionAttributes).FirstOrDefault();
+ return nameAttribute != null
+ ? nameAttribute.Name
+ : methodInfo.Name;
+ }
+
+ private static Collection<HttpMethod> GetSupportedHttpMethods(string actionName, object[] actionAttributes)
+ {
+ Collection<HttpMethod> supportedHttpMethods = new Collection<HttpMethod>();
+ ICollection<IActionHttpMethodProvider> httpMethodProviders = TypeHelper.OfType<IActionHttpMethodProvider>(actionAttributes);
+ if (httpMethodProviders.Count > 0)
+ {
+ // Get HttpMethod from attributes
+ foreach (IActionHttpMethodProvider httpMethodSelector in httpMethodProviders)
+ {
+ foreach (HttpMethod httpMethod in httpMethodSelector.HttpMethods)
+ {
+ supportedHttpMethods.Add(httpMethod);
+ }
+ }
+ }
+ else
+ {
+ // Get HttpMethod from action name convention
+ for (int i = 0; i < _supportedHttpMethodsByConvention.Length; i++)
+ {
+ if (actionName.StartsWith(_supportedHttpMethodsByConvention[i].Method, StringComparison.OrdinalIgnoreCase))
+ {
+ supportedHttpMethods.Add(_supportedHttpMethodsByConvention[i]);
+ break;
+ }
+ }
+ }
+
+ return supportedHttpMethods;
+ }
+
+ private sealed class ActionExecutor
+ {
+ private Func<object, object[], object> _executor;
+
+ public ActionExecutor(MethodInfo methodInfo)
+ {
+ Contract.Assert(methodInfo != null);
+ _executor = GetExecutor(methodInfo);
+ }
+
+ public object Execute(object instance, object[] arguments)
+ {
+ return _executor(instance, arguments);
+ }
+
+ private static Func<object, object[], object> GetExecutor(MethodInfo methodInfo)
+ {
+ // Parameters to executor
+ ParameterExpression instanceParameter = Expression.Parameter(typeof(object), "instance");
+ ParameterExpression parametersParameter = Expression.Parameter(typeof(object[]), "parameters");
+
+ // Build parameter list
+ List<Expression> parameters = new List<Expression>();
+ ParameterInfo[] paramInfos = methodInfo.GetParameters();
+ for (int i = 0; i < paramInfos.Length; i++)
+ {
+ ParameterInfo paramInfo = paramInfos[i];
+ BinaryExpression valueObj = Expression.ArrayIndex(parametersParameter, Expression.Constant(i));
+ UnaryExpression valueCast = Expression.Convert(valueObj, paramInfo.ParameterType);
+
+ // valueCast is "(Ti) parameters[i]"
+ parameters.Add(valueCast);
+ }
+
+ // Call method
+ UnaryExpression instanceCast = (!methodInfo.IsStatic) ? Expression.Convert(instanceParameter, methodInfo.ReflectedType) : null;
+ MethodCallExpression methodCall = methodCall = Expression.Call(instanceCast, methodInfo, parameters);
+
+ // methodCall is "((MethodInstanceType) instance).method((T0) parameters[0], (T1) parameters[1], ...)"
+ // Create function
+ if (methodCall.Type == typeof(void))
+ {
+ Expression<Action<object, object[]>> lambda = Expression.Lambda<Action<object, object[]>>(methodCall, instanceParameter, parametersParameter);
+ Action<object, object[]> voidExecutor = lambda.Compile();
+ return (instance, methodParameters) =>
+ {
+ voidExecutor(instance, methodParameters);
+ return null;
+ };
+ }
+ else
+ {
+ // must coerce methodCall to match Func<object, object[], object> signature
+ UnaryExpression castMethodCall = Expression.Convert(methodCall, typeof(object));
+ Expression<Func<object, object[], object>> lambda = Expression.Lambda<Func<object, object[], object>>(castMethodCall, instanceParameter, parametersParameter);
+ return lambda.Compile();
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/Controllers/ReflectedHttpParameterDescriptor.cs b/src/System.Web.Http/Controllers/ReflectedHttpParameterDescriptor.cs
new file mode 100644
index 00000000..c0f15496
--- /dev/null
+++ b/src/System.Web.Http/Controllers/ReflectedHttpParameterDescriptor.cs
@@ -0,0 +1,77 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Reflection;
+using System.Web.Http.Common;
+using System.Web.Http.Internal;
+
+namespace System.Web.Http.Controllers
+{
+ public class ReflectedHttpParameterDescriptor : HttpParameterDescriptor
+ {
+ private ParameterInfo _parameterInfo;
+
+ public ReflectedHttpParameterDescriptor(HttpActionDescriptor actionDescriptor, ParameterInfo parameterInfo)
+ : base(actionDescriptor)
+ {
+ if (parameterInfo == null)
+ {
+ throw Error.ArgumentNull("parameterInfo");
+ }
+
+ ParameterInfo = parameterInfo;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ReflectedHttpParameterDescriptor"/> class.
+ /// </summary>
+ /// <remarks>The default constructor is intended for use by unit testing only.</remarks>
+ public ReflectedHttpParameterDescriptor()
+ {
+ }
+
+ public override object DefaultValue
+ {
+ get
+ {
+ object value;
+ if (ParameterInfo.TryGetDefaultValue(out value))
+ {
+ return value;
+ }
+ else
+ {
+ return base.DefaultValue;
+ }
+ }
+ }
+
+ public ParameterInfo ParameterInfo
+ {
+ get { return _parameterInfo; }
+ set
+ {
+ if (value == null)
+ {
+ throw Error.PropertyNull();
+ }
+ _parameterInfo = value;
+ }
+ }
+
+ public override string ParameterName
+ {
+ get { return ParameterInfo.Name; }
+ }
+
+ public override Type ParameterType
+ {
+ get { return ParameterInfo.ParameterType; }
+ }
+
+ public override IEnumerable<T> GetCustomAttributes<T>()
+ {
+ return ParameterInfo.GetCustomAttributes<T>(false);
+ }
+ }
+}
diff --git a/src/System.Web.Http/Controllers/TaskActionResponseConverter.cs b/src/System.Web.Http/Controllers/TaskActionResponseConverter.cs
new file mode 100644
index 00000000..6d0a0d98
--- /dev/null
+++ b/src/System.Web.Http/Controllers/TaskActionResponseConverter.cs
@@ -0,0 +1,37 @@
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace System.Web.Http.Controllers
+{
+ internal sealed class TaskActionResponseConverter : ActionResponseConverter
+ {
+ public override Task<HttpResponseMessage> Convert(HttpControllerContext controllerContext, object responseValue, CancellationToken cancellation)
+ {
+ Task responseTask = (Task)responseValue;
+ return responseTask.Then(() =>
+ {
+ return VoidHttpResponseMessageConverter.Convert(controllerContext, null, cancellation);
+ }, cancellation);
+ }
+ }
+
+ internal sealed class TaskActionResponseConverter<TResponseValue> : ActionResponseConverter
+ {
+ private readonly ActionResponseConverter _innerConverter;
+
+ public TaskActionResponseConverter(ActionResponseConverter innerConverter)
+ {
+ _innerConverter = innerConverter;
+ }
+
+ public override Task<HttpResponseMessage> Convert(HttpControllerContext controllerContext, object responseValue, CancellationToken cancellation)
+ {
+ Task<TResponseValue> responseTask = (Task<TResponseValue>)responseValue;
+ return responseTask.Then<TResponseValue, HttpResponseMessage>(result =>
+ {
+ return _innerConverter.Convert(controllerContext, result, cancellation);
+ }, cancellation);
+ }
+ }
+}
diff --git a/src/System.Web.Http/Controllers/VoidHttpResponseMessageConverter.cs b/src/System.Web.Http/Controllers/VoidHttpResponseMessageConverter.cs
new file mode 100644
index 00000000..26e53941
--- /dev/null
+++ b/src/System.Web.Http/Controllers/VoidHttpResponseMessageConverter.cs
@@ -0,0 +1,20 @@
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace System.Web.Http.Controllers
+{
+ internal sealed class VoidHttpResponseMessageConverter : ActionResponseConverter
+ {
+ public override Task<HttpResponseMessage> Convert(HttpControllerContext controllerContext, object responseValue, CancellationToken cancellation)
+ {
+ // responseValue is ignored because the Action returns void.
+ return TaskHelpers.RunSynchronously(() => ConvertSync(controllerContext), cancellation);
+ }
+
+ private static HttpResponseMessage ConvertSync(HttpControllerContext controllerContext)
+ {
+ return controllerContext.Request.CreateResponse();
+ }
+ }
+}
diff --git a/src/System.Web.Http/DependencyResolverExtensions.cs b/src/System.Web.Http/DependencyResolverExtensions.cs
new file mode 100644
index 00000000..048a7649
--- /dev/null
+++ b/src/System.Web.Http/DependencyResolverExtensions.cs
@@ -0,0 +1,160 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Net.Http.Formatting;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Description;
+using System.Web.Http.Dispatcher;
+using System.Web.Http.Filters;
+using System.Web.Http.Metadata;
+using System.Web.Http.ModelBinding;
+using System.Web.Http.Properties;
+using System.Web.Http.Services;
+using System.Web.Http.Tracing;
+using System.Web.Http.Validation;
+using System.Web.Http.ValueProviders;
+
+namespace System.Web.Http
+{
+ /// <summary>
+ /// This provides a centralized list of type-safe accessors describing where and how we use the dependency resolver.
+ /// This also provides a single entry point for each service request. That makes it easy
+ /// to see which parts of the code use it, and provides a single place to comment usage.
+ /// Accessors encapsulate usage like:
+ /// <list type="bullet">
+ /// <item>Type-safe using {T} instead of unsafe <see cref="System.Type"/>.</item>
+ /// <item>which type do we key off? This is interesting with type hierarchies.</item>
+ /// <item>do we ask for singular or plural?</item>
+ /// <item>is it optional or mandatory?</item>
+ /// <item>what are the ordering semantics</item>
+ /// <item>Do we use a cached value or not?</item>
+ /// </list>
+ /// Expected that any <see cref="IEnumerable{T}"/> we return is non-null, although possibly empty.
+ /// </summary>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static class DependencyResolverExtensions
+ {
+ /// <summary>
+ /// Get ValueProviderFactories. The order of returned providers is the priority order that we search the factories.
+ /// </summary>
+ public static IEnumerable<ValueProviderFactory> GetValueProviderFactories(this DependencyResolver resolver)
+ {
+ return resolver.GetServices<ValueProviderFactory>();
+ }
+
+ /// <summary>
+ /// Get a controller factory, which instantiates a string name into an <see cref="IHttpController"/>.
+ /// This may be implemented by first getting the <see cref="Type"/> from the controller name, and
+ /// then using a <see cref="IHttpControllerActivator"/>.
+ /// </summary>
+ public static IHttpControllerFactory GetHttpControllerFactory(this DependencyResolver resolver)
+ {
+ return resolver.GetServiceOrThrow<IHttpControllerFactory>();
+ }
+
+ /// <summary>
+ /// Controller activator is used to instantiate an <see cref="IHttpController"/>.
+ /// </summary>
+ /// <returns>
+ /// An <see cref="IHttpControllerActivator"/> instance or null if none are registered.
+ /// </returns>
+ public static IHttpControllerActivator GetHttpControllerActivator(this DependencyResolver resolver)
+ {
+ return resolver.GetServiceOrThrow<IHttpControllerActivator>();
+ }
+
+ public static IBuildManager GetBuildManager(this DependencyResolver resolver)
+ {
+ return resolver.GetServiceOrThrow<IBuildManager>();
+ }
+
+ public static IHttpActionSelector GetActionSelector(this DependencyResolver resolver)
+ {
+ return resolver.GetServiceOrThrow<IHttpActionSelector>();
+ }
+
+ public static IHttpActionInvoker GetActionInvoker(this DependencyResolver resolver)
+ {
+ return resolver.GetServiceOrThrow<IHttpActionInvoker>();
+ }
+
+ public static IApiExplorer GetApiExplorer(this DependencyResolver resolver)
+ {
+ return resolver.GetServiceOrThrow<IApiExplorer>();
+ }
+
+ public static IDocumentationProvider GetDocumentationProvider(this DependencyResolver resolver)
+ {
+ return resolver.GetService<IDocumentationProvider>();
+ }
+
+ public static IEnumerable<IFilterProvider> GetFilterProviders(this DependencyResolver resolver)
+ {
+ return resolver.GetServices<IFilterProvider>();
+ }
+
+ public static ModelMetadataProvider GetModelMetadataProvider(this DependencyResolver resolver)
+ {
+ // TODO: this is called a lot - should we use this.GetCachedService<T>? instead
+ return resolver.GetServiceOrThrow<ModelMetadataProvider>();
+ }
+
+ public static IEnumerable<ModelBinderProvider> GetModelBinderProviders(this DependencyResolver resolver)
+ {
+ return resolver.GetServices<ModelBinderProvider>();
+ }
+
+ public static IEnumerable<ModelValidatorProvider> GetModelValidatorProviders(this DependencyResolver resolver)
+ {
+ return resolver.GetServices<ModelValidatorProvider>();
+ }
+
+ public static IContentNegotiator GetContentNegotiator(this DependencyResolver resolver)
+ {
+ return resolver.GetService<IContentNegotiator>();
+ }
+
+ public static IActionValueBinder GetActionValueBinder(this DependencyResolver resolver)
+ {
+ return resolver.GetService<IActionValueBinder>();
+ }
+
+ public static ITraceManager GetTraceManager(this DependencyResolver resolver)
+ {
+ return resolver.GetService<ITraceManager>();
+ }
+
+ public static ITraceWriter GetTraceWriter(this DependencyResolver resolver)
+ {
+ return resolver.GetService<ITraceWriter>();
+ }
+
+ public static IBodyModelValidator GetBodyModelValidator(this DependencyResolver resolver)
+ {
+ return resolver.GetService<IBodyModelValidator>();
+ }
+
+ // Runtime code shouldn't call GetService() directly. Instead, have a wrapper (like the ones above) and call through the wrapper.
+ private static TService GetService<TService>(this DependencyResolver resolver)
+ {
+ return (TService)resolver.GetService(typeof(TService));
+ }
+
+ private static IEnumerable<TService> GetServices<TService>(this DependencyResolver resolver)
+ {
+ return resolver.GetServices(typeof(TService)).Cast<TService>();
+ }
+
+ private static T GetServiceOrThrow<T>(this DependencyResolver resolver)
+ {
+ T result = resolver.GetService<T>();
+ if (result == null)
+ {
+ throw Error.InvalidOperation(SRResources.DependencyResolverNoService, typeof(T).FullName);
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/src/System.Web.Http/Description/ApiDescription.cs b/src/System.Web.Http/Description/ApiDescription.cs
new file mode 100644
index 00000000..7d4ece56
--- /dev/null
+++ b/src/System.Web.Http/Description/ApiDescription.cs
@@ -0,0 +1,91 @@
+using System.Collections.ObjectModel;
+using System.Net.Http;
+using System.Net.Http.Formatting;
+using System.Web.Http.Controllers;
+using System.Web.Http.Routing;
+
+namespace System.Web.Http.Description
+{
+ /// <summary>
+ /// Describes an API defined by relative URI path and HTTP method.
+ /// </summary>
+ public class ApiDescription
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ApiDescription"/> class.
+ /// </summary>
+ public ApiDescription()
+ {
+ SupportedRequestBodyFormatters = new Collection<MediaTypeFormatter>();
+ SupportedResponseFormatters = new Collection<MediaTypeFormatter>();
+ ParameterDescriptions = new Collection<ApiParameterDescription>();
+ }
+
+ /// <summary>
+ /// Gets or sets the HTTP method.
+ /// </summary>
+ /// <value>
+ /// The HTTP method.
+ /// </value>
+ public HttpMethod HttpMethod { get; set; }
+
+ /// <summary>
+ /// Gets or sets the relative path.
+ /// </summary>
+ /// <value>
+ /// The relative path.
+ /// </value>
+ public string RelativePath { get; set; }
+
+ /// <summary>
+ /// Gets or sets the action descriptor that will handle the API.
+ /// </summary>
+ /// <value>
+ /// The action descriptor.
+ /// </value>
+ public HttpActionDescriptor ActionDescriptor { get; set; }
+
+ /// <summary>
+ /// Gets or sets the registered route for the API.
+ /// </summary>
+ /// <value>
+ /// The route.
+ /// </value>
+ public IHttpRoute Route { get; set; }
+
+ /// <summary>
+ /// Gets or sets the documentation of the API.
+ /// </summary>
+ /// <value>
+ /// The documentation.
+ /// </value>
+ public string Documentation { get; set; }
+
+ /// <summary>
+ /// Gets the supported response formatters.
+ /// </summary>
+ public Collection<MediaTypeFormatter> SupportedResponseFormatters { get; internal set; }
+
+ /// <summary>
+ /// Gets the supported request body formatters.
+ /// </summary>
+ public Collection<MediaTypeFormatter> SupportedRequestBodyFormatters { get; internal set; }
+
+ /// <summary>
+ /// Gets the parameter descriptions.
+ /// </summary>
+ public Collection<ApiParameterDescription> ParameterDescriptions { get; internal set; }
+
+ /// <summary>
+ /// Gets the ID. The ID is unique within <see cref="HttpServer"/>.
+ /// </summary>
+ public string ID
+ {
+ get
+ {
+ return (HttpMethod != null ? HttpMethod.Method : String.Empty) +
+ (RelativePath ?? String.Empty);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/Description/ApiExplorer.cs b/src/System.Web.Http/Description/ApiExplorer.cs
new file mode 100644
index 00000000..462cfe13
--- /dev/null
+++ b/src/System.Web.Http/Description/ApiExplorer.cs
@@ -0,0 +1,486 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Globalization;
+using System.Linq;
+using System.Net.Http;
+using System.Net.Http.Formatting;
+using System.Text.RegularExpressions;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Dispatcher;
+using System.Web.Http.Internal;
+using System.Web.Http.ModelBinding;
+using System.Web.Http.Properties;
+using System.Web.Http.Routing;
+using System.Web.Http.ValueProviders;
+
+namespace System.Web.Http.Description
+{
+ /// <summary>
+ /// Explores the URI space of the service based on routes, controllers and actions available in the system.
+ /// </summary>
+ public class ApiExplorer : IApiExplorer
+ {
+ private Lazy<Collection<ApiDescription>> _apiDescriptions;
+ private readonly HttpConfiguration _config;
+ private const string ActionVariableName = "action";
+ private const string ControllerVariableName = "controller";
+ private static readonly Regex _actionVariableRegex = new Regex(String.Format(CultureInfo.CurrentCulture, "{{{0}}}", ActionVariableName), RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
+ private static readonly Regex _controllerVariableRegex = new Regex(String.Format(CultureInfo.CurrentCulture, "{{{0}}}", ControllerVariableName), RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
+ private static readonly HttpMethod _anyHttpMethod = new HttpMethod("ANY");
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ApiExplorer"/> class.
+ /// </summary>
+ /// <param name="configuration">The configuration.</param>
+ public ApiExplorer(HttpConfiguration configuration)
+ {
+ _config = configuration;
+ _apiDescriptions = new Lazy<Collection<ApiDescription>>(InitializeApiDescriptions);
+ }
+
+ /// <summary>
+ /// Gets the API descriptions. The descriptions are initialized on the first access.
+ /// </summary>
+ public Collection<ApiDescription> ApiDescriptions
+ {
+ get
+ {
+ return _apiDescriptions.Value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the documentation provider. The provider will be responsible for documenting the API.
+ /// </summary>
+ /// <value>
+ /// The documentation provider.
+ /// </value>
+ public IDocumentationProvider DocumentationProvider { get; set; }
+
+ /// <summary>
+ /// Determines whether the controller should be considered for <see cref="ApiExplorer.ApiDescriptions"/> generation. Called when initializing the <see cref="ApiExplorer.ApiDescriptions"/>.
+ /// </summary>
+ /// <param name="controllerVariableValue">The controller variable value from the route.</param>
+ /// <param name="controllerDescriptor">The controller descriptor.</param>
+ /// <param name="route">The route.</param>
+ /// <returns><c>true</c> if the controller should be considered for <see cref="ApiExplorer.ApiDescriptions"/> generation, <c>false</c> otherwise.</returns>
+ public virtual bool ShouldExploreController(string controllerVariableValue, HttpControllerDescriptor controllerDescriptor, IHttpRoute route)
+ {
+ if (controllerDescriptor == null)
+ {
+ throw Error.ArgumentNull("controllerDescriptor");
+ }
+
+ if (route == null)
+ {
+ throw Error.ArgumentNull("route");
+ }
+
+ ApiExplorerSettingsAttribute setting = controllerDescriptor.GetCustomAttributes<ApiExplorerSettingsAttribute>().FirstOrDefault();
+ return (setting == null || !setting.IgnoreApi) &&
+ MatchRegexConstraint(route, ControllerVariableName, controllerVariableValue);
+ }
+
+ /// <summary>
+ /// Determines whether the action should be considered for <see cref="ApiExplorer.ApiDescriptions"/> generation. Called when initializing the <see cref="ApiExplorer.ApiDescriptions"/>.
+ /// </summary>
+ /// <param name="actionVariableValue">The action variable value from the route.</param>
+ /// <param name="actionDescriptor">The action descriptor.</param>
+ /// <param name="route">The route.</param>
+ /// <returns><c>true</c> if the action should be considered for <see cref="ApiExplorer.ApiDescriptions"/> generation, <c>false</c> otherwise.</returns>
+ public virtual bool ShouldExploreAction(string actionVariableValue, HttpActionDescriptor actionDescriptor, IHttpRoute route)
+ {
+ if (actionDescriptor == null)
+ {
+ throw Error.ArgumentNull("actionDescriptor");
+ }
+
+ if (route == null)
+ {
+ throw Error.ArgumentNull("route");
+ }
+
+ ApiExplorerSettingsAttribute setting = actionDescriptor.GetCustomAttributes<ApiExplorerSettingsAttribute>().FirstOrDefault();
+ NonActionAttribute nonAction = actionDescriptor.GetCustomAttributes<NonActionAttribute>().FirstOrDefault();
+ return (setting == null || !setting.IgnoreApi) &&
+ (nonAction == null) &&
+ MatchRegexConstraint(route, ActionVariableName, actionVariableValue);
+ }
+
+ /// <summary>
+ /// Gets a collection of HttpMethods supported by the action. Called when initializing the <see cref="ApiExplorer.ApiDescriptions"/>.
+ /// </summary>
+ /// <param name="route">The route.</param>
+ /// <param name="actionDescriptor">The action descriptor.</param>
+ /// <returns>A collection of HttpMethods supported by the action.</returns>
+ public virtual IList<HttpMethod> GetHttpMethodsSupportedByAction(IHttpRoute route, HttpActionDescriptor actionDescriptor)
+ {
+ if (route == null)
+ {
+ throw Error.ArgumentNull("route");
+ }
+
+ if (actionDescriptor == null)
+ {
+ throw Error.ArgumentNull("actionDescriptor");
+ }
+
+ bool isActionVariableSpecified = _actionVariableRegex.IsMatch(route.RouteTemplate) || route.Defaults.ContainsKey(ActionVariableName);
+ IList<HttpMethod> supportedMethods = new List<HttpMethod>();
+ IList<HttpMethod> actionHttpMethods = actionDescriptor.SupportedHttpMethods;
+ HttpMethodConstraint httpMethodConstraint = route.Constraints.Values.FirstOrDefault(c => typeof(HttpMethodConstraint).IsAssignableFrom(c.GetType())) as HttpMethodConstraint;
+
+ if (actionHttpMethods.Count == 0)
+ {
+ if (isActionVariableSpecified)
+ {
+ // reachable by any HTTP method
+ if (httpMethodConstraint == null)
+ {
+ supportedMethods.Add(_anyHttpMethod);
+ }
+ else
+ {
+ // limit the HTTP methods to the ones specified in the constraint
+ supportedMethods = httpMethodConstraint.AllowedMethods;
+ }
+ }
+ // if no {action} is specified and the action method doesn't have any http method attribute ([HttpGet], [HttpPost], etc) then it won't be selected by our action selector
+ }
+ else
+ {
+ if (httpMethodConstraint == null)
+ {
+ supportedMethods = actionHttpMethods;
+ }
+ else
+ {
+ supportedMethods = httpMethodConstraint.AllowedMethods.Intersect(actionHttpMethods).ToList();
+ }
+ }
+
+ return supportedMethods;
+ }
+
+ private Collection<ApiDescription> InitializeApiDescriptions()
+ {
+ Collection<ApiDescription> apiDescriptions = new Collection<ApiDescription>();
+ IHttpControllerFactory controllerFactory = _config.ServiceResolver.GetHttpControllerFactory();
+ IDictionary<string, HttpControllerDescriptor> controllerMappings = controllerFactory.GetControllerMapping();
+ if (controllerMappings != null)
+ {
+ foreach (var route in _config.Routes)
+ {
+ ExploreRouteControllers(controllerMappings, route, apiDescriptions);
+ }
+
+ // remove ApiDescription that will lead to ambiguous action matching. E.g. a controller with Post() and PostComment(). When the route template is {controller}, it produces POST /controller and POST /controller.
+ apiDescriptions = RemoveInvalidApiDescriptions(apiDescriptions);
+ }
+
+ return apiDescriptions;
+ }
+
+ private void ExploreRouteControllers(IDictionary<string, HttpControllerDescriptor> controllerMappings, IHttpRoute route, Collection<ApiDescription> apiDescriptions)
+ {
+ string routeTemplate = route.RouteTemplate;
+ string controllerVariableValue;
+ if (_controllerVariableRegex.IsMatch(routeTemplate))
+ {
+ // unbound controller variable, {controller}
+ foreach (KeyValuePair<string, HttpControllerDescriptor> controllerMapping in controllerMappings)
+ {
+ controllerVariableValue = controllerMapping.Key;
+ HttpControllerDescriptor controllerDescriptor = controllerMapping.Value;
+ if (ShouldExploreController(controllerVariableValue, controllerDescriptor, route))
+ {
+ // expand {controller} variable
+ string expandedRouteTemplate = _controllerVariableRegex.Replace(routeTemplate, controllerVariableValue);
+ ExploreRouteActions(route, expandedRouteTemplate, controllerDescriptor, apiDescriptions);
+ }
+ }
+ }
+ else
+ {
+ // bound controller variable, {controller = "controllerName"}
+ controllerVariableValue = route.Defaults[ControllerVariableName] as string;
+ HttpControllerDescriptor controllerDescriptor;
+ if (controllerMappings.TryGetValue(controllerVariableValue, out controllerDescriptor) && ShouldExploreController(controllerVariableValue, controllerDescriptor, route))
+ {
+ ExploreRouteActions(route, routeTemplate, controllerDescriptor, apiDescriptions);
+ }
+ }
+ }
+
+ private void ExploreRouteActions(IHttpRoute route, string localPath, HttpControllerDescriptor controllerDescriptor, Collection<ApiDescription> apiDescriptions)
+ {
+ ILookup<string, HttpActionDescriptor> actionMappings = controllerDescriptor.HttpActionSelector.GetActionMapping(controllerDescriptor);
+ string actionVariableValue;
+ if (actionMappings != null)
+ {
+ if (_actionVariableRegex.IsMatch(localPath))
+ {
+ // unbound action variable, {action}
+ foreach (IGrouping<string, HttpActionDescriptor> actionMapping in actionMappings)
+ {
+ // expand {action} variable
+ actionVariableValue = actionMapping.Key;
+ string expandedLocalPath = _actionVariableRegex.Replace(localPath, actionVariableValue);
+ PopulateActionDescriptions(actionMapping, actionVariableValue, route, expandedLocalPath, apiDescriptions);
+ }
+ }
+ else if (route.Defaults.TryGetValue(ActionVariableName, out actionVariableValue))
+ {
+ // bound action variable, { action = "actionName" }
+ PopulateActionDescriptions(actionMappings[actionVariableValue], actionVariableValue, route, localPath, apiDescriptions);
+ }
+ else
+ {
+ // no {action} specified, e.g. {controller}/{id}
+ foreach (IGrouping<string, HttpActionDescriptor> actionMapping in actionMappings)
+ {
+ PopulateActionDescriptions(actionMapping, null, route, localPath, apiDescriptions);
+ }
+ }
+ }
+ }
+
+ private void PopulateActionDescriptions(IEnumerable<HttpActionDescriptor> actionDescriptors, string actionVariableValue, IHttpRoute route, string localPath, Collection<ApiDescription> apiDescriptions)
+ {
+ foreach (HttpActionDescriptor actionDescriptor in actionDescriptors)
+ {
+ if (ShouldExploreAction(actionVariableValue, actionDescriptor, route))
+ {
+ PopulateActionDescriptions(actionDescriptor, route, localPath, apiDescriptions);
+ }
+ }
+ }
+
+ private void PopulateActionDescriptions(HttpActionDescriptor actionDescriptor, IHttpRoute route, string localPath, Collection<ApiDescription> apiDescriptions)
+ {
+ string apiDocumentation = GetApiDocumentation(actionDescriptor);
+
+ // parameters
+ IList<ApiParameterDescription> parameterDescriptions = CreateParameterDescriptions(actionDescriptor);
+
+ // expand all parameter variables
+ string finalPath;
+
+ if (!TryExpandUriParameters(route, localPath, parameterDescriptions, out finalPath))
+ {
+ // the action cannot be reached due to parameter mismatch, e.g. routeTemplate = "/users/{name}" and GetUsers(id)
+ return;
+ }
+
+ // request formatters
+ ApiParameterDescription bodyParameter = parameterDescriptions.FirstOrDefault(description => description.Source == ApiParameterSource.FromBody);
+ IEnumerable<MediaTypeFormatter> supportedRequestBodyFormatters = bodyParameter != null ?
+ _config.Formatters.Where(f => f.CanReadType(bodyParameter.ParameterDescriptor.ParameterType)) :
+ Enumerable.Empty<MediaTypeFormatter>();
+
+ // response formatters
+ Type returnType = TypeHelper.UnwrapIfTask(actionDescriptor.ReturnType);
+ IEnumerable<MediaTypeFormatter> supportedResponseFormatters = returnType != typeof(void) ?
+ _config.Formatters.Where(f => f.CanWriteType(returnType)) :
+ Enumerable.Empty<MediaTypeFormatter>();
+
+ // get HttpMethods supported by an action. Usually there is one HttpMethod per action but we allow multiple of them per action as well.
+ IList<HttpMethod> supportedMethods = GetHttpMethodsSupportedByAction(route, actionDescriptor);
+
+ foreach (HttpMethod method in supportedMethods)
+ {
+ apiDescriptions.Add(new ApiDescription
+ {
+ Documentation = apiDocumentation,
+ HttpMethod = method,
+ RelativePath = finalPath,
+ ActionDescriptor = actionDescriptor,
+ Route = route,
+ SupportedResponseFormatters = new Collection<MediaTypeFormatter>(supportedResponseFormatters.ToList()),
+ SupportedRequestBodyFormatters = new Collection<MediaTypeFormatter>(supportedRequestBodyFormatters.ToList()),
+ ParameterDescriptions = new Collection<ApiParameterDescription>(parameterDescriptions)
+ });
+ }
+ }
+
+ private static bool TryExpandUriParameters(IHttpRoute route, string routeTemplate, ICollection<ApiParameterDescription> parameterDescriptions, out string expandedRouteTemplate)
+ {
+ HttpParsedRoute parsedRoute = HttpRouteParser.Parse(routeTemplate);
+ Dictionary<string, object> parameterValuesForRoute = new Dictionary<string, object>();
+ foreach (ApiParameterDescription parameterDescriptor in parameterDescriptions)
+ {
+ Type parameterType = parameterDescriptor.ParameterDescriptor.ParameterType;
+ if (parameterDescriptor.Source == ApiParameterSource.FromUri && TypeHelper.IsSimpleUnderlyingType(parameterType))
+ {
+ parameterValuesForRoute.Add(parameterDescriptor.Name, "{" + parameterDescriptor.Name + "}");
+ }
+ }
+
+ BoundRouteTemplate boundRouteTemplate = parsedRoute.Bind(null, parameterValuesForRoute, new HttpRouteValueDictionary(route.Defaults), new HttpRouteValueDictionary(route.Constraints));
+ if (boundRouteTemplate == null)
+ {
+ expandedRouteTemplate = null;
+ return false;
+ }
+
+ expandedRouteTemplate = Uri.UnescapeDataString(boundRouteTemplate.BoundTemplate);
+ return true;
+ }
+
+ private IList<ApiParameterDescription> CreateParameterDescriptions(HttpActionDescriptor actionDescriptor)
+ {
+ IList<ApiParameterDescription> parameterDescriptions = new List<ApiParameterDescription>();
+ HttpActionBinding actionBinding = GetActionBinding(actionDescriptor);
+
+ // try get parameter binding information if available
+ if (actionBinding != null)
+ {
+ HttpParameterBinding[] parameterBindings = actionBinding.ParameterBindings;
+ if (parameterBindings != null)
+ {
+ foreach (HttpParameterBinding parameter in parameterBindings)
+ {
+ parameterDescriptions.Add(CreateParameterDescriptionFromBinding(parameter));
+ }
+ }
+ }
+ else
+ {
+ Collection<HttpParameterDescriptor> parameters = actionDescriptor.GetParameters();
+ if (parameters != null)
+ {
+ foreach (HttpParameterDescriptor parameter in parameters)
+ {
+ parameterDescriptions.Add(CreateParameterDescriptionFromDescriptor(parameter));
+ }
+ }
+ }
+
+ return parameterDescriptions;
+ }
+
+ private ApiParameterDescription CreateParameterDescriptionFromDescriptor(HttpParameterDescriptor parameter)
+ {
+ ApiParameterDescription parameterDescription = new ApiParameterDescription();
+ parameterDescription.ParameterDescriptor = parameter;
+ parameterDescription.Name = parameter.ParameterName;
+ parameterDescription.Documentation = GetApiParameterDocumentation(parameter);
+ parameterDescription.Source = ApiParameterSource.Unknown;
+ return parameterDescription;
+ }
+
+ private ApiParameterDescription CreateParameterDescriptionFromBinding(HttpParameterBinding parameterBinding)
+ {
+ ApiParameterDescription parameterDescription = CreateParameterDescriptionFromDescriptor(parameterBinding.Descriptor);
+ if (parameterBinding.WillReadBody)
+ {
+ parameterDescription.Source = ApiParameterSource.FromBody;
+ }
+ else
+ {
+ // TODO, 371284, Provide a better model for getting the HttpActionBinding from HttpControllerDescriptor, and ValueProviderFactory from the HttpParameterBinding.
+ ModelBinderParameterBinding modelParameterBinding = parameterBinding as ModelBinderParameterBinding;
+ if (modelParameterBinding != null)
+ {
+ if (modelParameterBinding.ValueProviderFactories.All(factory => factory is IUriValueProviderFactory))
+ {
+ parameterDescription.Source = ApiParameterSource.FromUri;
+ }
+ }
+ }
+
+ return parameterDescription;
+ }
+
+ private string GetApiDocumentation(HttpActionDescriptor actionDescriptor)
+ {
+ IDocumentationProvider documentationProvider = DocumentationProvider ?? _config.ServiceResolver.GetDocumentationProvider();
+ if (documentationProvider == null)
+ {
+ return string.Format(CultureInfo.CurrentCulture, SRResources.ApiExplorer_DefaultDocumentation, actionDescriptor.ActionName);
+ }
+
+ return documentationProvider.GetDocumentation(actionDescriptor);
+ }
+
+ private string GetApiParameterDocumentation(HttpParameterDescriptor parameterDescriptor)
+ {
+ IDocumentationProvider documentationProvider = DocumentationProvider ?? _config.ServiceResolver.GetDocumentationProvider();
+ if (documentationProvider == null)
+ {
+ return string.Format(CultureInfo.CurrentCulture, SRResources.ApiExplorer_DefaultDocumentation, parameterDescriptor.ParameterName);
+ }
+
+ return documentationProvider.GetDocumentation(parameterDescriptor);
+ }
+
+ // remove ApiDescription that will lead to ambiguous action matching.
+ private static Collection<ApiDescription> RemoveInvalidApiDescriptions(Collection<ApiDescription> apiDescriptions)
+ {
+ HashSet<string> duplicateApiDescriptionIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+ HashSet<string> visitedApiDescriptionIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+
+ foreach (ApiDescription description in apiDescriptions)
+ {
+ string apiDescriptionId = description.ID;
+ if (visitedApiDescriptionIds.Contains(apiDescriptionId))
+ {
+ duplicateApiDescriptionIds.Add(apiDescriptionId);
+ }
+ else
+ {
+ visitedApiDescriptionIds.Add(apiDescriptionId);
+ }
+ }
+
+ Collection<ApiDescription> filteredApiDescriptions = new Collection<ApiDescription>();
+ foreach (ApiDescription apiDescription in apiDescriptions)
+ {
+ string apiDescriptionId = apiDescription.ID;
+ if (!duplicateApiDescriptionIds.Contains(apiDescriptionId))
+ {
+ filteredApiDescriptions.Add(apiDescription);
+ }
+ }
+
+ return filteredApiDescriptions;
+ }
+
+ private static bool MatchRegexConstraint(IHttpRoute route, string parameterName, string parameterValue)
+ {
+ IDictionary<string, object> constraints = route.Constraints;
+ if (constraints != null)
+ {
+ object constraint;
+ if (constraints.TryGetValue(parameterName, out constraint))
+ {
+ // treat the constraint as a string which represents a Regex.
+ // note that we don't support custom constraint (IHttpRouteConstraint) because it might rely on the request and some runtime states
+ string constraintsRule = constraint as string;
+ if (constraintsRule != null)
+ {
+ string constraintsRegEx = "^(" + constraintsRule + ")$";
+ return parameterValue != null && Regex.IsMatch(parameterValue, constraintsRegEx, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
+ }
+ }
+ }
+
+ return true;
+ }
+
+ private static HttpActionBinding GetActionBinding(HttpActionDescriptor actionDescriptor)
+ {
+ HttpControllerDescriptor controllerDescriptor = actionDescriptor.ControllerDescriptor;
+ if (controllerDescriptor == null)
+ {
+ return null;
+ }
+
+ IActionValueBinder actionValueBinder = controllerDescriptor.ActionValueBinder;
+ HttpActionBinding actionBinding = actionValueBinder != null ? actionValueBinder.GetBinding(actionDescriptor) : null;
+ return actionBinding;
+ }
+ }
+}
diff --git a/src/System.Web.Http/Description/ApiExplorerSettingsAttribute.cs b/src/System.Web.Http/Description/ApiExplorerSettingsAttribute.cs
new file mode 100644
index 00000000..7a6f639d
--- /dev/null
+++ b/src/System.Web.Http/Description/ApiExplorerSettingsAttribute.cs
@@ -0,0 +1,17 @@
+namespace System.Web.Http.Description
+{
+ /// <summary>
+ /// This attribute can be used on the controllers and actions to influence the behavior of <see cref="ApiExplorer"/>.
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
+ public sealed class ApiExplorerSettingsAttribute : Attribute
+ {
+ /// <summary>
+ /// Gets or sets a value indicating whether to exclude the controller or action from the ApiDescriptions generated by <see cref="ApiExplorer"/>.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if the controller or action should be ignored; otherwise, <c>false</c>.
+ /// </value>
+ public bool IgnoreApi { get; set; }
+ }
+}
diff --git a/src/System.Web.Http/Description/ApiParameterDescription.cs b/src/System.Web.Http/Description/ApiParameterDescription.cs
new file mode 100644
index 00000000..09c51fd3
--- /dev/null
+++ b/src/System.Web.Http/Description/ApiParameterDescription.cs
@@ -0,0 +1,42 @@
+using System.Web.Http.Controllers;
+
+namespace System.Web.Http.Description
+{
+ /// <summary>
+ /// Describes a parameter on the API defined by relative URI path and HTTP method.
+ /// </summary>
+ public class ApiParameterDescription
+ {
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>
+ /// The name.
+ /// </value>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the documentation.
+ /// </summary>
+ /// <value>
+ /// The documentation.
+ /// </value>
+ public string Documentation { get; set; }
+
+ /// <summary>
+ /// Gets or sets the source of the parameter. It may come from the request URI, request body or other places.
+ /// </summary>
+ /// <value>
+ /// The source.
+ /// </value>
+ public ApiParameterSource Source { get; set; }
+
+ /// <summary>
+ /// Gets or sets the parameter descriptor.
+ /// </summary>
+ /// <value>
+ /// The parameter descriptor.
+ /// </value>
+ public HttpParameterDescriptor ParameterDescriptor { get; set; }
+ }
+}
diff --git a/src/System.Web.Http/Description/ApiParameterSource.cs b/src/System.Web.Http/Description/ApiParameterSource.cs
new file mode 100644
index 00000000..541644e8
--- /dev/null
+++ b/src/System.Web.Http/Description/ApiParameterSource.cs
@@ -0,0 +1,12 @@
+namespace System.Web.Http.Description
+{
+ /// <summary>
+ /// Describes where the parameter come from.
+ /// </summary>
+ public enum ApiParameterSource
+ {
+ FromUri = 0,
+ FromBody,
+ Unknown
+ }
+}
diff --git a/src/System.Web.Http/Description/IApiExplorer.cs b/src/System.Web.Http/Description/IApiExplorer.cs
new file mode 100644
index 00000000..b37d81ba
--- /dev/null
+++ b/src/System.Web.Http/Description/IApiExplorer.cs
@@ -0,0 +1,15 @@
+using System.Collections.ObjectModel;
+
+namespace System.Web.Http.Description
+{
+ /// <summary>
+ /// Defines the interface for getting a collection of <see cref="ApiDescription"/>.
+ /// </summary>
+ public interface IApiExplorer
+ {
+ /// <summary>
+ /// Gets the API descriptions.
+ /// </summary>
+ Collection<ApiDescription> ApiDescriptions { get; }
+ }
+}
diff --git a/src/System.Web.Http/Description/IDocumentationProvider.cs b/src/System.Web.Http/Description/IDocumentationProvider.cs
new file mode 100644
index 00000000..dcec9891
--- /dev/null
+++ b/src/System.Web.Http/Description/IDocumentationProvider.cs
@@ -0,0 +1,24 @@
+using System.Web.Http.Controllers;
+
+namespace System.Web.Http.Description
+{
+ /// <summary>
+ /// Defines the provider responsible for documenting the service.
+ /// </summary>
+ public interface IDocumentationProvider
+ {
+ /// <summary>
+ /// Gets the documentation based on <see cref="HttpActionDescriptor"/>.
+ /// </summary>
+ /// <param name="actionDescriptor">The action descriptor.</param>
+ /// <returns>Documentation for the controller.</returns>
+ string GetDocumentation(HttpActionDescriptor actionDescriptor);
+
+ /// <summary>
+ /// Gets the documentation based on <see cref="HttpParameterDescriptor"/>.
+ /// </summary>
+ /// <param name="parameterDescriptor">The parameter descriptor.</param>
+ /// <returns>Documentation for the controller.</returns>
+ string GetDocumentation(HttpParameterDescriptor parameterDescriptor);
+ }
+}
diff --git a/src/System.Web.Http/DictionaryExtensions.cs b/src/System.Web.Http/DictionaryExtensions.cs
new file mode 100644
index 00000000..3ff3fd22
--- /dev/null
+++ b/src/System.Web.Http/DictionaryExtensions.cs
@@ -0,0 +1,140 @@
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics.Contracts;
+using System.Linq;
+using System.Web.Http.Common;
+using System.Web.Http.Properties;
+
+namespace System.Web.Http
+{
+ /// <summary>
+ /// Extension methods for <see cref="IDictionary{TKey,TValue}"/>.
+ /// </summary>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static class DictionaryExtensions
+ {
+ /// <summary>
+ /// Gets the value of <typeparamref name="T"/> associated with the specified key or <c>default</c> value if
+ /// either the key is not present or the value is not of type <typeparamref name="T"/>.
+ /// </summary>
+ /// <typeparam name="T">The type of the value associated with the specified key.</typeparam>
+ /// <param name="collection">The <see cref="IDictionary{TKey,TValue}"/> instance where <c>TValue</c> is <c>object</c>.</param>
+ /// <param name="key">The key whose value to get.</param>
+ /// <param name="value">When this method returns, the value associated with the specified key, if the key is found; otherwise, the default value for the type of the value parameter.</param>
+ /// <returns><c>true</c> if key was found, value is non-null, and value is of type <typeparamref name="T"/>; otherwise false.</returns>
+ public static bool TryGetValue<T>(this IDictionary<string, object> collection, string key, out T value)
+ {
+ if (collection == null)
+ {
+ throw Error.ArgumentNull("collection");
+ }
+
+ object valueObj;
+ if (collection.TryGetValue(key, out valueObj))
+ {
+ if (valueObj is T)
+ {
+ value = (T)valueObj;
+ return true;
+ }
+ }
+
+ value = default(T);
+ return false;
+ }
+
+ /// <summary>
+ /// Gets the value of <typeparamref name="T"/> associated with the specified key or throw an <see cref="T:System.InvalidOperationException"/>
+ /// if either the key is not present or the value is not of type <typeparamref name="T"/>.
+ /// </summary>
+ /// <param name="collection">The <see cref="IDictionary{TKey,TValue}"/> instance where <c>TValue</c> is <c>object</c>.</param>
+ /// <param name="key">The key whose value to get.</param>
+ /// <returns>An instance of type <typeparam name="T"/>.</returns>
+ public static T GetValue<T>(this IDictionary<string, object> collection, string key)
+ {
+ if (collection == null)
+ {
+ throw Error.ArgumentNull("collection");
+ }
+
+ T value;
+ if (collection.TryGetValue(key, out value))
+ {
+ return value;
+ }
+
+ throw Error.InvalidOperation(SRResources.DictionaryMissingRequiredValue, collection.GetType().Name, key, typeof(T).Name);
+ }
+
+ internal static IEnumerable<KeyValuePair<string, TValue>> FindKeysWithPrefix<TValue>(this IDictionary<string, TValue> dictionary, string prefix)
+ {
+ if (dictionary == null)
+ {
+ throw Error.ArgumentNull("dictionary");
+ }
+
+ if (prefix == null)
+ {
+ throw Error.ArgumentNull("prefix");
+ }
+
+ TValue exactMatchValue;
+ if (dictionary.TryGetValue(prefix, out exactMatchValue))
+ {
+ yield return new KeyValuePair<string, TValue>(prefix, exactMatchValue);
+ }
+
+ foreach (var entry in dictionary)
+ {
+ string key = entry.Key;
+
+ if (key.Length <= prefix.Length)
+ {
+ continue;
+ }
+
+ if (!key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ char charAfterPrefix = key[prefix.Length];
+ switch (charAfterPrefix)
+ {
+ case '[':
+ case '.':
+ yield return entry;
+ break;
+ }
+ }
+ }
+
+ internal static bool DoesAnyKeyHavePrefix<TValue>(this IDictionary<string, TValue> dictionary, string prefix)
+ {
+ return FindKeysWithPrefix(dictionary, prefix).Any();
+ }
+
+ /// <summary>
+ /// Adds a key/value pair of type <typeparamref name="T"/> to the <see cref="T:System.Collections.Concurrent.ConcurrentDictionary{object, object}"/>
+ /// if the key does not already exist.
+ /// </summary>
+ /// <typeparam name="T">The actual type of the dictionary value.</typeparam>
+ /// <param name="concurrentPropertyBag">A dictionary.</param>
+ /// <param name="key">The key of the element to add.</param>
+ /// <param name="factory">The function used to generate a value for the <paramref name="key"/>.</param>
+ /// <returns> The value for the key. This will be either the existing value for the <paramref name="key"/> if the key is already in the dictionary,
+ /// or the new value for the key as returned by <paramref name="factory"/> if the key was not in the dictionary.</returns>
+ internal static T GetOrAdd<T>(this ConcurrentDictionary<object, object> concurrentPropertyBag, object key, Func<object, T> factory)
+ {
+ Contract.Assert(concurrentPropertyBag != null);
+ Contract.Assert(key != null);
+ Contract.Assert(factory != null);
+
+ // SIMPLIFYING ASSUMPTION: this method is internal and keys are private so it's assumed that client code won't be able to
+ // replace the value with an object of a different type.
+
+ return (T)concurrentPropertyBag.GetOrAdd(key, valueFactory: k => factory(k));
+ }
+ }
+}
diff --git a/src/System.Web.Http/Dispatcher/DefaultBuildManager.cs b/src/System.Web.Http/Dispatcher/DefaultBuildManager.cs
new file mode 100644
index 00000000..45aacf6d
--- /dev/null
+++ b/src/System.Web.Http/Dispatcher/DefaultBuildManager.cs
@@ -0,0 +1,60 @@
+using System.Collections;
+using System.IO;
+
+namespace System.Web.Http.Dispatcher
+{
+ /// <summary>
+ /// Provides an implementation of <see cref="IBuildManager"/> with no external dependencies.
+ /// </summary>
+ internal class DefaultBuildManager : IBuildManager
+ {
+ /// <summary>
+ /// Gets an object factory for the specified virtual path.
+ /// </summary>
+ /// <param name="virtualPath">The virtual path.</param>
+ /// <returns><c>true</c> if file exists; otherwise false.</returns>
+ bool IBuildManager.FileExists(string virtualPath)
+ {
+ return false;
+ }
+
+ /// <summary>
+ /// Compiles a file, given its virtual path, and returns the compiled type.
+ /// </summary>
+ /// <param name="virtualPath">The virtual path.</param>
+ /// <returns>The compiled <see cref="Type"/>.</returns>
+ Type IBuildManager.GetCompiledType(string virtualPath)
+ {
+ return null;
+ }
+
+ /// <summary>
+ /// Returns a list of assembly references that all page compilations must reference.
+ /// </summary>
+ /// <returns>An <see cref="ICollection"/> of assembly references.</returns>
+ ICollection IBuildManager.GetReferencedAssemblies()
+ {
+ return AppDomain.CurrentDomain.GetAssemblies();
+ }
+
+ /// <summary>
+ /// Reads a cached file.
+ /// </summary>
+ /// <param name="fileName">Name of the file.</param>
+ /// <returns>The <see cref="Stream"/> object for the file, or <c>null</c> if the file does not exist.</returns>
+ Stream IBuildManager.ReadCachedFile(string fileName)
+ {
+ return null;
+ }
+
+ /// <summary>
+ /// Creates a cached file.
+ /// </summary>
+ /// <param name="fileName">Name of the file.</param>
+ /// <returns>The <see cref="Stream"/> object for the new file.</returns>
+ Stream IBuildManager.CreateCachedFile(string fileName)
+ {
+ return Stream.Null;
+ }
+ }
+}
diff --git a/src/System.Web.Http/Dispatcher/DefaultHttpControllerActivator.cs b/src/System.Web.Http/Dispatcher/DefaultHttpControllerActivator.cs
new file mode 100644
index 00000000..4d2adf31
--- /dev/null
+++ b/src/System.Web.Http/Dispatcher/DefaultHttpControllerActivator.cs
@@ -0,0 +1,94 @@
+using System.Threading;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Internal;
+using System.Web.Http.Properties;
+
+namespace System.Web.Http.Dispatcher
+{
+ /// <summary>
+ /// Default implementation of an <see cref="IHttpControllerActivator"/>.
+ /// A different implementation can be registered via the <see cref="T:System.Web.Http.Services.DependencyResolver"/>.
+ /// We optimize for the case where we have an <see cref="Controllers.ApiControllerActionInvoker"/>
+ /// instance per <see cref="HttpControllerDescriptor"/> instance but can support cases where there are
+ /// many <see cref="HttpControllerDescriptor"/> instances for one <see cref="System.Web.Http.Controllers.ApiControllerActionInvoker"/>
+ /// as well. In the latter case the lookup is slightly slower because it goes through the
+ /// <see cref="P:HttpControllerDescriptor.Properties"/> dictionary.
+ /// </summary>
+ public class DefaultHttpControllerActivator : IHttpControllerActivator
+ {
+ private readonly HttpConfiguration _configuration;
+ private Tuple<HttpControllerDescriptor, Func<IHttpController>> _fastCache;
+ private object _cacheKey = new object();
+
+ public DefaultHttpControllerActivator(HttpConfiguration configuration)
+ {
+ if (configuration == null)
+ {
+ throw Error.ArgumentNull("configuration");
+ }
+
+ _configuration = configuration;
+ }
+
+ /// <summary>
+ /// Creates the <see cref="IHttpController"/> specified by <paramref name="controllerType"/> using the given <paramref name="controllerContext"/>
+ /// </summary>
+ /// <param name="controllerContext">The controller context.</param>
+ /// <param name="controllerType">Type of the controller.</param>
+ /// <returns>An instance of type <paramref name="controllerType"/>.</returns>
+ public IHttpController Create(HttpControllerContext controllerContext, Type controllerType)
+ {
+ if (controllerContext == null)
+ {
+ throw Error.ArgumentNull("controllerContext");
+ }
+
+ if (controllerType == null)
+ {
+ throw Error.ArgumentNull("controllerType");
+ }
+
+ try
+ {
+ // First check in the local fast cache and if not a match then look in the broader
+ // HttpControllerDescriptor.Properties cache
+ if (_fastCache == null)
+ {
+ // If service resolver returns controller object then keep asking it whenever we need a new instance
+ IHttpController instance = (IHttpController)_configuration.ServiceResolver.GetService(controllerType);
+ if (instance != null)
+ {
+ return instance;
+ }
+
+ // Otherwise create a delegate for creating a new instance of the type
+ Func<IHttpController> activator = TypeActivator.Create<IHttpController>(controllerType);
+ Tuple<HttpControllerDescriptor, Func<IHttpController>> cacheItem = Tuple.Create(controllerContext.ControllerDescriptor, activator);
+ Interlocked.CompareExchange(ref _fastCache, cacheItem, null);
+
+ // Execute the delegate
+ return activator();
+ }
+ else if (_fastCache.Item1 == controllerContext.ControllerDescriptor)
+ {
+ // If the key matches and we already have the delegate for creating an instance then just execute it
+ return _fastCache.Item2();
+ }
+ else
+ {
+ // If the key doesn't match then lookup/create delegate in the HttpControllerDescriptor.Properties for
+ // that HttpControllerDescriptor instance
+ Func<IHttpController> activator = (Func<IHttpController>)controllerContext.ControllerDescriptor.Properties.GetOrAdd(
+ _cacheKey,
+ key => TypeActivator.Create<IHttpController>(controllerType));
+ return activator();
+ }
+ }
+ catch (Exception ex)
+ {
+ throw Error.InvalidOperation(ex, SRResources.DefaultControllerFactory_ErrorCreatingController, controllerType);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/Dispatcher/DefaultHttpControllerFactory.cs b/src/System.Web.Http/Dispatcher/DefaultHttpControllerFactory.cs
new file mode 100644
index 00000000..82fadf22
--- /dev/null
+++ b/src/System.Web.Http/Dispatcher/DefaultHttpControllerFactory.cs
@@ -0,0 +1,168 @@
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics.Contracts;
+using System.Linq;
+using System.Net;
+using System.Text;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Properties;
+using System.Web.Http.Routing;
+using System.Web.Http.Services;
+
+namespace System.Web.Http.Dispatcher
+{
+ /// <summary>
+ /// Default <see cref="IHttpControllerFactory"/> instance creating new <see cref="IHttpController"/> instances.
+ /// A different implementation can be registered via the <see cref="DependencyResolver"/>.
+ /// </summary>
+ public class DefaultHttpControllerFactory : IHttpControllerFactory
+ {
+ public static readonly string ControllerSuffix = "Controller";
+
+ private readonly HttpConfiguration _configuration;
+ private readonly HttpControllerTypeCache _controllerTypeCache;
+ private readonly ConcurrentDictionary<string, HttpControllerDescriptor> _controllerInfoCache = new ConcurrentDictionary<string, HttpControllerDescriptor>();
+ private bool _isControllerInfoCacheInitialized;
+ private object _controllerInfoCacheLock = new object();
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DefaultHttpControllerFactory"/> class.
+ /// </summary>
+ /// <param name="configuration">The configuration.</param>
+ public DefaultHttpControllerFactory(HttpConfiguration configuration)
+ {
+ if (configuration == null)
+ {
+ throw Error.ArgumentNull("resolver");
+ }
+
+ _configuration = configuration;
+ _controllerTypeCache = new HttpControllerTypeCache(_configuration);
+ }
+
+ public virtual IHttpController CreateController(HttpControllerContext controllerContext, string controllerName)
+ {
+ if (controllerContext == null)
+ {
+ throw Error.ArgumentNull("controllerContext");
+ }
+
+ if (String.IsNullOrEmpty(controllerName))
+ {
+ throw Error.ArgumentNullOrEmpty("controllerName");
+ }
+
+ HttpControllerDescriptor controllerDescriptor;
+ if (_controllerInfoCache.TryGetValue(controllerName, out controllerDescriptor))
+ {
+ // Create controller instance
+ return CreateInstance(controllerContext, controllerDescriptor);
+ }
+
+ ICollection<Type> matchingTypes = _controllerTypeCache.GetControllerTypes(controllerName);
+ switch (matchingTypes.Count)
+ {
+ case 0:
+ // no matching types
+ throw new HttpResponseException(
+ Error.Format(SRResources.DefaultControllerFactory_ControllerNameNotFound, controllerName),
+ HttpStatusCode.NotFound);
+
+ case 1:
+ // single matching type
+ Type match = matchingTypes.First();
+
+ // Add controller descriptor to cache
+ controllerDescriptor = new HttpControllerDescriptor(_configuration, controllerName, match);
+ _controllerInfoCache.TryAdd(controllerName, controllerDescriptor);
+
+ // Create controller instance
+ return CreateInstance(controllerContext, controllerDescriptor);
+
+ default:
+ // multiple matching types
+ throw new HttpResponseException(
+ CreateAmbiguousControllerExceptionMessage(controllerContext.RouteData.Route, controllerName, matchingTypes),
+ HttpStatusCode.InternalServerError);
+ }
+ }
+
+ public virtual void ReleaseController(HttpControllerContext controllerContext, IHttpController controller)
+ {
+ IDisposable disposable = controller as IDisposable;
+ if (disposable != null)
+ {
+ disposable.Dispose();
+ }
+ }
+
+ public virtual IDictionary<string, HttpControllerDescriptor> GetControllerMapping()
+ {
+ EnsureControllerInfoCacheInitialized();
+ return _controllerInfoCache.ToDictionary(c => c.Key, c => c.Value, StringComparer.OrdinalIgnoreCase);
+ }
+
+ private static IHttpController CreateInstance(HttpControllerContext controllerContext, HttpControllerDescriptor controllerDescriptor)
+ {
+ Contract.Assert(controllerContext != null);
+ Contract.Assert(controllerDescriptor != null);
+
+ // Fill in controller descriptor on execution context
+ controllerContext.ControllerDescriptor = controllerDescriptor;
+
+ // Invoke the controller activator
+ IHttpController instance = controllerDescriptor.HttpControllerActivator.Create(controllerContext, controllerDescriptor.ControllerType);
+
+ // Fill in controller instance on execution context
+ controllerContext.Controller = instance;
+
+ return instance;
+ }
+
+ private static string CreateAmbiguousControllerExceptionMessage(IHttpRoute route, string controllerName, ICollection<Type> matchingTypes)
+ {
+ Contract.Assert(route != null);
+ Contract.Assert(controllerName != null);
+ Contract.Assert(matchingTypes != null);
+
+ // Generate an exception containing all the controller types
+ StringBuilder typeList = new StringBuilder();
+ foreach (Type matchedType in matchingTypes)
+ {
+ typeList.AppendLine();
+ typeList.Append(matchedType.FullName);
+ }
+
+ return Error.Format(SRResources.DefaultControllerFactory_ControllerNameAmbiguous_WithRouteTemplate, controllerName, route.RouteTemplate, typeList);
+ }
+
+ private void EnsureControllerInfoCacheInitialized()
+ {
+ if (!_isControllerInfoCacheInitialized)
+ {
+ lock (_controllerInfoCacheLock)
+ {
+ if (!_isControllerInfoCacheInitialized)
+ {
+ Dictionary<string, ILookup<string, Type>> controllerTypeGroups = _controllerTypeCache.Cache;
+ foreach (KeyValuePair<string, ILookup<string, Type>> controllerTypeGroup in controllerTypeGroups)
+ {
+ string controllerName = controllerTypeGroup.Key;
+ foreach (var controllerTypesGroupedByNs in controllerTypeGroup.Value)
+ {
+ foreach (var controllerType in controllerTypesGroupedByNs)
+ {
+ var controllerDescriptor = new HttpControllerDescriptor(_configuration, controllerName, controllerType);
+ _controllerInfoCache.TryAdd(controllerName, controllerDescriptor);
+ }
+ }
+ }
+
+ _isControllerInfoCacheInitialized = true;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/Dispatcher/ExceptionSurrogate.cs b/src/System.Web.Http/Dispatcher/ExceptionSurrogate.cs
new file mode 100644
index 00000000..76a2d9da
--- /dev/null
+++ b/src/System.Web.Http/Dispatcher/ExceptionSurrogate.cs
@@ -0,0 +1,42 @@
+using System.Diagnostics.Contracts;
+using System.Runtime.Serialization;
+using System.Xml.Serialization;
+
+namespace System.Web.Http.Dispatcher
+{
+ // Infrastructure class used to serialize unhandled exceptions at the dispatcher layer. Not meant to be used by end users.
+ // Only public to enable its serialization by XmlSerializer and in partial trust.
+ [DataContract(Name = "Exception")]
+ [XmlRoot("Exception")]
+ public class ExceptionSurrogate
+ {
+ private ExceptionSurrogate()
+ {
+ }
+
+ internal ExceptionSurrogate(Exception exception)
+ {
+ Contract.Assert(exception != null);
+
+ Message = exception.Message;
+ StackTrace = exception.StackTrace;
+ if (exception.InnerException != null)
+ {
+ InnerException = new ExceptionSurrogate(exception.InnerException);
+ }
+ ExceptionType = exception.GetType().FullName;
+ }
+
+ [DataMember]
+ public string ExceptionType { get; set; }
+
+ [DataMember(EmitDefaultValue = false, IsRequired = false)]
+ public string Message { get; set; }
+
+ [DataMember(EmitDefaultValue = false, IsRequired = false)]
+ public string StackTrace { get; set; }
+
+ [DataMember(EmitDefaultValue = false, IsRequired = false)]
+ public ExceptionSurrogate InnerException { get; set; }
+ }
+}
diff --git a/src/System.Web.Http/Dispatcher/HttpControllerDispatcher.cs b/src/System.Web.Http/Dispatcher/HttpControllerDispatcher.cs
new file mode 100644
index 00000000..19fc8e0a
--- /dev/null
+++ b/src/System.Web.Http/Dispatcher/HttpControllerDispatcher.cs
@@ -0,0 +1,227 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Formatting;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Hosting;
+using System.Web.Http.Properties;
+using System.Web.Http.Routing;
+
+namespace System.Web.Http.Dispatcher
+{
+ /// <summary>
+ /// Dispatches an incoming <see cref="HttpRequestMessage"/> to an <see cref="IHttpController"/> implementation for processing.
+ /// </summary>
+ public class HttpControllerDispatcher : HttpMessageHandler
+ {
+ private const string ControllerKey = "controller";
+
+ private IHttpControllerFactory _controllerFactory;
+ private readonly HttpConfiguration _configuration;
+ private bool _disposed;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpControllerDispatcher"/> class using default <see cref="HttpConfiguration"/>.
+ /// </summary>
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope",
+ Justification = "The configuration object is disposed as part of this class.")]
+ public HttpControllerDispatcher()
+ : this(new HttpConfiguration())
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpControllerDispatcher"/> class.
+ /// </summary>
+ public HttpControllerDispatcher(HttpConfiguration configuration)
+ {
+ if (configuration == null)
+ {
+ throw Error.ArgumentNull("configuration");
+ }
+
+ _configuration = configuration;
+ }
+
+ /// <summary>
+ /// Gets the <see cref="HttpConfiguration"/>.
+ /// </summary>
+ public HttpConfiguration Configuration
+ {
+ get { return _configuration; }
+ }
+
+ private IHttpControllerFactory ControllerFactory
+ {
+ get
+ {
+ if (_controllerFactory == null)
+ {
+ _controllerFactory = _configuration.ServiceResolver.GetHttpControllerFactory();
+ }
+
+ return _controllerFactory;
+ }
+ }
+
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources
+ /// </summary>
+ /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged SRResources.</param>
+ protected override void Dispose(bool disposing)
+ {
+ if (!_disposed)
+ {
+ _disposed = true;
+ if (disposing)
+ {
+ _configuration.Dispose();
+ }
+ }
+
+ base.Dispose(disposing);
+ }
+
+ /// <summary>
+ /// Dispatches an incoming <see cref="HttpRequestMessage"/> to an <see cref="IHttpController"/>.
+ /// </summary>
+ /// <param name="request">The request to dispatch</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>A <see cref="Task{HttpResponseMessage}"/> representing the ongoing operation.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We report the error in the HTTP response.")]
+ protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ // Runs Content Negotiation and Error Handling on the result of SendAsyncInternal
+ try
+ {
+ return SendAsyncInternal(request, cancellationToken).Catch((exception) =>
+ {
+ return HandleException(request, exception, _configuration);
+ });
+ }
+ catch (Exception exception)
+ {
+ return HandleException(request, exception, _configuration);
+ }
+ }
+
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Caller becomes owner.")]
+ private Task<HttpResponseMessage> SendAsyncInternal(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ if (request == null)
+ {
+ throw Error.ArgumentNull("request");
+ }
+
+ if (_disposed)
+ {
+ throw Error.ObjectDisposed(SRResources.HttpMessageHandlerDisposed, typeof(HttpControllerDispatcher).Name);
+ }
+
+ // Lookup route data, or if not found as a request property then we look it up in the route table
+ IHttpRouteData routeData;
+ if (!request.Properties.TryGetValue(HttpPropertyKeys.HttpRouteDataKey, out routeData))
+ {
+ routeData = _configuration.Routes.GetRouteData(request);
+ if (routeData != null)
+ {
+ request.Properties.Add(HttpPropertyKeys.HttpRouteDataKey, routeData);
+ }
+ else
+ {
+ // TODO, 328927, add an error message in the response body
+ return TaskHelpers.FromResult(request.CreateResponse(HttpStatusCode.NotFound));
+ }
+ }
+
+ // Look up controller in route data
+ string controllerName;
+ if (!routeData.Values.TryGetValue(ControllerKey, out controllerName))
+ {
+ // TODO, 328927, add an error message in the response body
+ return TaskHelpers.FromResult(request.CreateResponse(HttpStatusCode.NotFound));
+ }
+
+ RemoveOptionalRoutingParameters(routeData.Values);
+
+ // Create context
+ HttpControllerContext controllerContext = new HttpControllerContext(_configuration, routeData, request);
+
+ IHttpController httpController = ControllerFactory.CreateController(controllerContext, controllerName);
+ if (httpController == null)
+ {
+ // TODO, 328927, add an error message in the response body
+ return TaskHelpers.FromResult(request.CreateResponse(HttpStatusCode.NotFound));
+ }
+
+ try
+ {
+ return httpController.ExecuteAsync(controllerContext, cancellationToken).Finally(() =>
+ {
+ ControllerFactory.ReleaseController(controllerContext, httpController);
+ });
+ }
+ catch
+ {
+ ControllerFactory.ReleaseController(controllerContext, httpController);
+ throw;
+ }
+ }
+
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Caller owns HttpResponseMessage instance.")]
+ internal static Task<HttpResponseMessage> HandleException(HttpRequestMessage request, Exception exception, HttpConfiguration configuration)
+ {
+ Exception unwrappedException = exception.GetBaseException();
+ HttpResponseException httpResponseException = unwrappedException as HttpResponseException;
+ HttpResponseMessage response;
+
+ if (httpResponseException == null)
+ {
+ if (configuration.ShouldIncludeErrorDetail(request))
+ {
+ response = request.CreateResponse<ExceptionSurrogate>(HttpStatusCode.InternalServerError, new ExceptionSurrogate(unwrappedException));
+ }
+ else
+ {
+ response = new HttpResponseMessage(HttpStatusCode.InternalServerError);
+ }
+ }
+ else
+ {
+ response = httpResponseException.Response;
+ }
+
+ return TaskHelpers.FromResult(response);
+ }
+
+ private static void RemoveOptionalRoutingParameters(IDictionary<string, object> routeValueDictionary)
+ {
+ Contract.Assert(routeValueDictionary != null);
+
+ // Get all keys for which the corresponding value is 'Optional'.
+ // Having a separate array is necessary so that we don't manipulate the dictionary while enumerating.
+ // This is on a hot-path and linq expressions are showing up on the profile, so do array manipulation.
+ int max = routeValueDictionary.Count;
+ int i = 0;
+ string[] matching = new string[max];
+ foreach (KeyValuePair<string, object> kv in routeValueDictionary)
+ {
+ if (kv.Value == RouteParameter.Optional)
+ {
+ matching[i] = kv.Key;
+ i++;
+ }
+ }
+ for (int j = 0; j < i; j++)
+ {
+ string key = matching[j];
+ routeValueDictionary.Remove(key);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/Dispatcher/HttpControllerTypeCache.cs b/src/System.Web.Http/Dispatcher/HttpControllerTypeCache.cs
new file mode 100644
index 00000000..ce346567
--- /dev/null
+++ b/src/System.Web.Http/Dispatcher/HttpControllerTypeCache.cs
@@ -0,0 +1,83 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Web.Http.Common;
+using System.Web.Http.Internal;
+
+namespace System.Web.Http.Dispatcher
+{
+ /// <summary>
+ /// Manages a cache of <see cref="System.Web.Http.Controllers.IHttpController"/> types detected in the system.
+ /// </summary>
+ internal sealed class HttpControllerTypeCache
+ {
+ private const string TypeCacheName = "MS-ControllerTypeCache.xml";
+ private const string WildcardNamespace = ".*";
+
+ private readonly HttpConfiguration _configuration;
+ private readonly IBuildManager _buildManager;
+ private readonly Dictionary<string, ILookup<string, Type>> _cache;
+
+ public HttpControllerTypeCache(HttpConfiguration configuration)
+ {
+ if (configuration == null)
+ {
+ throw Error.ArgumentNull("configuration");
+ }
+
+ _configuration = configuration;
+ _buildManager = _configuration.ServiceResolver.GetBuildManager();
+ _cache = InitializeCache();
+ }
+
+ internal Dictionary<string, ILookup<string, Type>> Cache
+ {
+ get { return _cache; }
+ }
+
+ public ICollection<Type> GetControllerTypes(string controllerName)
+ {
+ if (String.IsNullOrEmpty(controllerName))
+ {
+ throw Error.ArgumentNullOrEmpty("controllerName");
+ }
+
+ HashSet<Type> matchingTypes = new HashSet<Type>();
+
+ ILookup<string, Type> namespaceLookup;
+ if (_cache.TryGetValue(controllerName, out namespaceLookup))
+ {
+ foreach (var namespaceGroup in namespaceLookup)
+ {
+ matchingTypes.UnionWith(namespaceGroup);
+ }
+ }
+
+ return matchingTypes;
+ }
+
+ private Dictionary<string, ILookup<string, Type>> InitializeCache()
+ {
+ List<Type> controllerTypes = HttpControllerTypeCacheUtil.GetFilteredTypesFromAssemblies(TypeCacheName, IsControllerType, _buildManager);
+ var groupedByName = controllerTypes.GroupBy(
+ t => t.Name.Substring(0, t.Name.Length - DefaultHttpControllerFactory.ControllerSuffix.Length),
+ StringComparer.OrdinalIgnoreCase);
+
+ return groupedByName.ToDictionary(
+ g => g.Key,
+ g => g.ToLookup(t => t.Namespace ?? String.Empty, StringComparer.OrdinalIgnoreCase),
+ StringComparer.OrdinalIgnoreCase);
+ }
+
+ // TODO: Shouldn't "Controller" suffix be matched case-SENsitive so that we don't match "testcontroller" but only "testController"?
+ public static bool IsControllerType(Type t)
+ {
+ return
+ t != null &&
+ t.IsClass &&
+ t.IsPublic &&
+ t.Name.EndsWith(DefaultHttpControllerFactory.ControllerSuffix, StringComparison.OrdinalIgnoreCase) &&
+ !t.IsAbstract &&
+ TypeHelper.HttpControllerType.IsAssignableFrom(t);
+ }
+ }
+}
diff --git a/src/System.Web.Http/Dispatcher/HttpControllerTypeCacheSerializer.cs b/src/System.Web.Http/Dispatcher/HttpControllerTypeCacheSerializer.cs
new file mode 100644
index 00000000..d623bfbd
--- /dev/null
+++ b/src/System.Web.Http/Dispatcher/HttpControllerTypeCacheSerializer.cs
@@ -0,0 +1,126 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Web.Http.Properties;
+using System.Xml;
+
+namespace System.Web.Http.Dispatcher
+{
+ // Processes files with this format:
+ //
+ // <typeCache lastModified=... mvcVersionId=...>
+ // <assembly name=...>
+ // <module versionId=...>
+ // <type>...</type>
+ // </module>
+ // </assembly>
+ // </typeCache>
+ //
+ // This is used to store caches of files between AppDomain resets, leading to improved cold boot time
+ // and more efficient use of memory.
+
+ /// <summary>
+ /// Manages serializing and deserializing the cache managed by <see cref="HttpControllerTypeCache"/>.
+ /// </summary>
+ internal sealed class HttpControllerTypeCacheSerializer
+ {
+ private static readonly Guid _mvcVersionId = typeof(HttpControllerTypeCacheSerializer).Module.ModuleVersionId;
+
+ // used for unit testing
+
+ private DateTime CurrentDate
+ {
+ get { return CurrentDateOverride ?? DateTime.Now; }
+ }
+
+ internal DateTime? CurrentDateOverride { get; set; }
+
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This is an instance method for consistency with the SerializeTypes() method.")]
+ public List<Type> DeserializeTypes(TextReader input)
+ {
+ // DevDiv: 314059: TypeCacheSerializer should use regular serialization instead of DOM
+ XmlDocument doc = new XmlDocument();
+ doc.Load(input);
+ XmlElement rootElement = doc.DocumentElement;
+
+ Guid readMvcVersionId = new Guid(rootElement.Attributes["mvcVersionId"].Value);
+ if (readMvcVersionId != _mvcVersionId)
+ {
+ // The cache is outdated because the cache file was produced by a different version
+ // of MVC.
+ return null;
+ }
+
+ List<Type> deserializedTypes = new List<Type>();
+ foreach (XmlNode assemblyNode in rootElement.ChildNodes)
+ {
+ string assemblyName = assemblyNode.Attributes["name"].Value;
+ Assembly assembly = Assembly.Load(assemblyName);
+
+ foreach (XmlNode moduleNode in assemblyNode.ChildNodes)
+ {
+ Guid moduleVersionId = new Guid(moduleNode.Attributes["versionId"].Value);
+
+ foreach (XmlNode typeNode in moduleNode.ChildNodes)
+ {
+ string typeName = typeNode.InnerText;
+ Type type = assembly.GetType(typeName);
+ if (type == null || type.Module.ModuleVersionId != moduleVersionId)
+ {
+ // The cache is outdated because we couldn't find a previously recorded
+ // type or the type's containing module was modified.
+ return null;
+ }
+ else
+ {
+ deserializedTypes.Add(type);
+ }
+ }
+ }
+ }
+
+ return deserializedTypes;
+ }
+
+ public void SerializeTypes(IEnumerable<Type> types, TextWriter output)
+ {
+ var groupedByAssembly = from type in types
+ group type by type.Module
+ into groupedByModule
+ group groupedByModule by groupedByModule.Key.Assembly;
+
+ XmlDocument doc = new XmlDocument();
+ doc.AppendChild(doc.CreateComment(SRResources.TypeCache_DoNotModify));
+
+ XmlElement typeCacheElement = doc.CreateElement("typeCache");
+ doc.AppendChild(typeCacheElement);
+ typeCacheElement.SetAttribute("lastModified", CurrentDate.ToString());
+ typeCacheElement.SetAttribute("mvcVersionId", _mvcVersionId.ToString());
+
+ foreach (var assemblyGroup in groupedByAssembly)
+ {
+ XmlElement assemblyElement = doc.CreateElement("assembly");
+ typeCacheElement.AppendChild(assemblyElement);
+ assemblyElement.SetAttribute("name", assemblyGroup.Key.FullName);
+
+ foreach (var moduleGroup in assemblyGroup)
+ {
+ XmlElement moduleElement = doc.CreateElement("module");
+ assemblyElement.AppendChild(moduleElement);
+ moduleElement.SetAttribute("versionId", moduleGroup.Key.ModuleVersionId.ToString());
+
+ foreach (Type type in moduleGroup)
+ {
+ XmlElement typeElement = doc.CreateElement("type");
+ moduleElement.AppendChild(typeElement);
+ typeElement.AppendChild(doc.CreateTextNode(type.FullName));
+ }
+ }
+ }
+
+ doc.Save(output);
+ }
+ }
+}
diff --git a/src/System.Web.Http/Dispatcher/HttpControllerTypeCacheUtil.cs b/src/System.Web.Http/Dispatcher/HttpControllerTypeCacheUtil.cs
new file mode 100644
index 00000000..123256b3
--- /dev/null
+++ b/src/System.Web.Http/Dispatcher/HttpControllerTypeCacheUtil.cs
@@ -0,0 +1,103 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Web.Http.Common;
+
+namespace System.Web.Http.Dispatcher
+{
+ /// <summary>
+ /// Provides various utilities for the <see cref="HttpControllerTypeCache"/>.
+ /// </summary>
+ internal static class HttpControllerTypeCacheUtil
+ {
+ public static List<Type> GetFilteredTypesFromAssemblies(string cacheName, Predicate<Type> predicate, IBuildManager buildManager)
+ {
+ HttpControllerTypeCacheSerializer serializer = new HttpControllerTypeCacheSerializer();
+
+ // first, try reading from the cache on disk
+ List<Type> matchingTypes = ReadTypesFromCache(cacheName, predicate, buildManager, serializer);
+ if (matchingTypes != null)
+ {
+ return matchingTypes;
+ }
+
+ // if reading from the cache failed, enumerate over every assembly looking for a matching type
+ matchingTypes = FilterTypesInAssemblies(buildManager, predicate).ToList();
+
+ // finally, save the cache back to disk
+ SaveTypesToCache(cacheName, matchingTypes, buildManager, serializer);
+
+ return matchingTypes;
+ }
+
+ private static IEnumerable<Type> FilterTypesInAssemblies(IBuildManager buildManager, Predicate<Type> predicate)
+ {
+ // Go through all assemblies referenced by the application and search for types matching a predicate
+ IEnumerable<Type> typesSoFar = Type.EmptyTypes;
+
+ ICollection assemblies = buildManager.GetReferencedAssemblies();
+ foreach (Assembly assembly in assemblies)
+ {
+ if (assembly.IsDynamic)
+ {
+ // can't call GetExportedTypes on a dynamic assembly
+ continue;
+ }
+
+ typesSoFar = typesSoFar.Concat(assembly.GetExportedTypes());
+ }
+
+ return typesSoFar.Where(type => predicate(type));
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Cache failures are not fatal, and the code should continue executing normally.")]
+ private static List<Type> ReadTypesFromCache(string cacheName, Predicate<Type> predicate, IBuildManager buildManager, HttpControllerTypeCacheSerializer serializer)
+ {
+ try
+ {
+ Stream stream = buildManager.ReadCachedFile(cacheName);
+ if (stream != null)
+ {
+ using (StreamReader reader = new StreamReader(stream))
+ {
+ List<Type> deserializedTypes = serializer.DeserializeTypes(reader);
+ if (deserializedTypes != null && deserializedTypes.All(type => predicate(type)))
+ {
+ // If all read types still match the predicate, success!
+ return deserializedTypes;
+ }
+ }
+ }
+ }
+ catch
+ {
+ // cache failures are not considered fatal -- keep running.
+ }
+
+ return null;
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Cache failures are not fatal, and the code should continue executing normally.")]
+ private static void SaveTypesToCache(string cacheName, IList<Type> matchingTypes, IBuildManager buildManager, HttpControllerTypeCacheSerializer serializer)
+ {
+ try
+ {
+ Stream stream = buildManager.CreateCachedFile(cacheName);
+ if (stream != null)
+ {
+ using (StreamWriter writer = new StreamWriter(stream))
+ {
+ serializer.SerializeTypes(matchingTypes, writer);
+ }
+ }
+ }
+ catch
+ {
+ // cache failures are not considered fatal -- keep running.
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/Dispatcher/IBuildManager.cs b/src/System.Web.Http/Dispatcher/IBuildManager.cs
new file mode 100644
index 00000000..4e4b43c6
--- /dev/null
+++ b/src/System.Web.Http/Dispatcher/IBuildManager.cs
@@ -0,0 +1,48 @@
+using System.Collections;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+
+namespace System.Web.Http.Dispatcher
+{
+ /// <summary>
+ /// Provides an abstraction for managing the compilation of an application. A different
+ /// implementation can be registered via the <see cref="T:System.Web.Http.Servies.DependencyResolver"/>.
+ /// </summary>
+ public interface IBuildManager
+ {
+ /// <summary>
+ /// Gets an object factory for the specified virtual path.
+ /// </summary>
+ /// <param name="virtualPath">The virtual path.</param>
+ /// <returns><c>true</c> if file exists; otherwise false.</returns>
+ bool FileExists(string virtualPath);
+
+ /// <summary>
+ /// Compiles a file, given its virtual path, and returns the compiled type.
+ /// </summary>
+ /// <param name="virtualPath">The virtual path.</param>
+ /// <returns>The compiled <see cref="Type"/>.</returns>
+ Type GetCompiledType(string virtualPath);
+
+ /// <summary>
+ /// Returns a list of assembly references that all page compilations must reference.
+ /// </summary>
+ /// <returns>An <see cref="ICollection"/> of assembly references.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This is better handled as a method.")]
+ ICollection GetReferencedAssemblies();
+
+ /// <summary>
+ /// Reads a cached file.
+ /// </summary>
+ /// <param name="fileName">Name of the file.</param>
+ /// <returns>The <see cref="Stream"/> object for the file, or <c>null</c> if the file does not exist.</returns>
+ Stream ReadCachedFile(string fileName);
+
+ /// <summary>
+ /// Creates a cached file.
+ /// </summary>
+ /// <param name="fileName">Name of the file.</param>
+ /// <returns>The <see cref="Stream"/> object for the new file.</returns>
+ Stream CreateCachedFile(string fileName);
+ }
+}
diff --git a/src/System.Web.Http/Dispatcher/IHttpControllerActivator.cs b/src/System.Web.Http/Dispatcher/IHttpControllerActivator.cs
new file mode 100644
index 00000000..ff8bd0ce
--- /dev/null
+++ b/src/System.Web.Http/Dispatcher/IHttpControllerActivator.cs
@@ -0,0 +1,12 @@
+using System.Web.Http.Controllers;
+
+namespace System.Web.Http.Dispatcher
+{
+ /// <summary>
+ /// Defines the methods that are required for an <see cref="IHttpControllerActivator"/>.
+ /// </summary>
+ public interface IHttpControllerActivator
+ {
+ IHttpController Create(HttpControllerContext controllerContext, Type controllerType);
+ }
+}
diff --git a/src/System.Web.Http/Dispatcher/IHttpControllerFactory.cs b/src/System.Web.Http/Dispatcher/IHttpControllerFactory.cs
new file mode 100644
index 00000000..f2d8920a
--- /dev/null
+++ b/src/System.Web.Http/Dispatcher/IHttpControllerFactory.cs
@@ -0,0 +1,35 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Web.Http.Controllers;
+
+namespace System.Web.Http.Dispatcher
+{
+ /// <summary>
+ /// Defines the methods that are required for an <see cref="IHttpController"/> factory.
+ /// </summary>
+ public interface IHttpControllerFactory
+ {
+ /// <summary>
+ /// Creates the <see cref="IHttpController"/> using the specified context and controller name.
+ /// </summary>
+ /// <param name="controllerContext">The controller context.</param>
+ /// <param name="controllerName">Name of the controller.</param>
+ /// <returns>An <see cref="IHttpController"/> instance.</returns>
+ IHttpController CreateController(HttpControllerContext controllerContext, string controllerName);
+
+ /// <summary>
+ /// Releases an <see cref="IHttpController"/> instance.
+ /// </summary>
+ /// <param name="controllerContext">The controller context.</param>
+ /// <param name="controller">The controller.</param>
+ void ReleaseController(HttpControllerContext controllerContext, IHttpController controller);
+
+ /// <summary>
+ /// Returns a map, keyed by controller string, of all <see cref="HttpControllerDescriptor"/> that the factory can produce.
+ /// This is primarily called by <see cref="System.Web.Http.Description.IApiExplorer"/> to discover all the possible controllers in the system.
+ /// </summary>
+ /// <returns>A map of all <see cref="HttpControllerDescriptor"/> that the factory can produce, or null if the factory does not have a well-defined mapping of <see cref="HttpControllerDescriptor"/>.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This is better handled as a method.")]
+ IDictionary<string, HttpControllerDescriptor> GetControllerMapping();
+ }
+}
diff --git a/src/System.Web.Http/Filters/ActionDescriptorFilterProvider.cs b/src/System.Web.Http/Filters/ActionDescriptorFilterProvider.cs
new file mode 100644
index 00000000..d0cad469
--- /dev/null
+++ b/src/System.Web.Http/Filters/ActionDescriptorFilterProvider.cs
@@ -0,0 +1,41 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+
+namespace System.Web.Http.Filters
+{
+ /// <summary>
+ /// This <see cref="IFilterProvider"/> implementation retrieves <see cref="FilterInfo">filters</see> associated with an <see cref="HttpActionDescriptor"/>
+ /// instance.
+ /// </summary>
+ public class ActionDescriptorFilterProvider : IFilterProvider
+ {
+ /// <summary>
+ /// Returns the collection of filters associated with <paramref name="actionDescriptor"/>.
+ /// </summary>
+ /// <remarks>
+ /// The implementation invokes <see cref="HttpActionDescriptor.GetFilters()"/> and <see cref="HttpControllerDescriptor.GetFilters()"/>.
+ /// </remarks>
+ /// <param name="configuration">The configuration. This value is not used.</param>
+ /// <param name="actionDescriptor">The action descriptor.</param>
+ /// <returns>A collection of filters.</returns>
+ public IEnumerable<FilterInfo> GetFilters(HttpConfiguration configuration, HttpActionDescriptor actionDescriptor)
+ {
+ if (configuration == null)
+ {
+ throw Error.ArgumentNull("configuration");
+ }
+
+ if (actionDescriptor == null)
+ {
+ throw Error.ArgumentNull("actionDescriptor");
+ }
+
+ IEnumerable<FilterInfo> controllerFilters = actionDescriptor.ControllerDescriptor.GetFilters().Select(instance => new FilterInfo(instance, FilterScope.Controller));
+ IEnumerable<FilterInfo> actionFilters = actionDescriptor.GetFilters().Select(instance => new FilterInfo(instance, FilterScope.Action));
+
+ return controllerFilters.Concat(actionFilters);
+ }
+ }
+}
diff --git a/src/System.Web.Http/Filters/ActionFilterAttribute.cs b/src/System.Web.Http/Filters/ActionFilterAttribute.cs
new file mode 100644
index 00000000..a0238fe1
--- /dev/null
+++ b/src/System.Web.Http/Filters/ActionFilterAttribute.cs
@@ -0,0 +1,93 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Properties;
+
+namespace System.Web.Http.Filters
+{
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
+ public abstract class ActionFilterAttribute : FilterAttribute, IActionFilter
+ {
+ public virtual void OnActionExecuting(HttpActionContext actionContext)
+ {
+ }
+
+ public virtual void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
+ {
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We want to intercept all exceptions")]
+ Task<HttpResponseMessage> IActionFilter.ExecuteActionFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation)
+ {
+ if (actionContext == null)
+ {
+ throw Error.ArgumentNull("actionContext");
+ }
+ if (continuation == null)
+ {
+ throw Error.ArgumentNull("continuation");
+ }
+
+ try
+ {
+ OnActionExecuting(actionContext);
+ }
+ catch (Exception e)
+ {
+ return TaskHelpers.FromError<HttpResponseMessage>(e);
+ }
+
+ if (actionContext.Response != null)
+ {
+ return TaskHelpers.FromResult(actionContext.Response);
+ }
+
+ Task<HttpResponseMessage> internalTask = continuation();
+ bool calledOnActionExecuted = false;
+
+ return internalTask
+ .Then(response =>
+ {
+ calledOnActionExecuted = true;
+ return CallOnActionExecuted(actionContext, response: response);
+ }, cancellationToken)
+ .Catch(ex =>
+ {
+ // If we've already called OnActionExecuted, that means this catch is running because
+ // OnActionExecuted threw an exception, so we just want to re-throw the exception rather
+ // that calling OnActionExecuted again.
+ if (calledOnActionExecuted)
+ {
+ return TaskHelpers.FromError<HttpResponseMessage>(ex);
+ }
+
+ return CallOnActionExecuted(actionContext, exception: ex);
+ }, cancellationToken);
+ }
+
+ private Task<HttpResponseMessage> CallOnActionExecuted(HttpActionContext actionContext, HttpResponseMessage response = null, Exception exception = null)
+ {
+ Contract.Assert(actionContext != null);
+ Contract.Assert(response != null || exception != null);
+
+ HttpActionExecutedContext executedContext = new HttpActionExecutedContext(actionContext, exception) { Result = response };
+
+ OnActionExecuted(executedContext);
+
+ if (executedContext.Result != null)
+ {
+ return TaskHelpers.FromResult(executedContext.Result);
+ }
+ if (executedContext.Exception != null)
+ {
+ return TaskHelpers.FromError<HttpResponseMessage>(executedContext.Exception);
+ }
+
+ throw Error.InvalidOperation(SRResources.ActionFilterAttribute_MustSupplyResponseOrException, GetType().Name);
+ }
+ }
+}
diff --git a/src/System.Web.Http/Filters/AuthorizationFilterAttribute.cs b/src/System.Web.Http/Filters/AuthorizationFilterAttribute.cs
new file mode 100644
index 00000000..9b632118
--- /dev/null
+++ b/src/System.Web.Http/Filters/AuthorizationFilterAttribute.cs
@@ -0,0 +1,48 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+
+namespace System.Web.Http.Filters
+{
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
+ public abstract class AuthorizationFilterAttribute : FilterAttribute, IAuthorizationFilter
+ {
+ public virtual void OnAuthorization(HttpActionContext actionContext)
+ {
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We want to intercept all exceptions")]
+ Task<HttpResponseMessage> IAuthorizationFilter.ExecuteAuthorizationFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation)
+ {
+ if (actionContext == null)
+ {
+ throw Error.ArgumentNull("actionContext");
+ }
+ if (continuation == null)
+ {
+ throw Error.ArgumentNull("continuation");
+ }
+
+ try
+ {
+ OnAuthorization(actionContext);
+ }
+ catch (Exception e)
+ {
+ return TaskHelpers.FromError<HttpResponseMessage>(e);
+ }
+
+ if (actionContext.Response != null)
+ {
+ return TaskHelpers.FromResult(actionContext.Response);
+ }
+ else
+ {
+ return continuation();
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/Filters/ConfigurationFilterProvider.cs b/src/System.Web.Http/Filters/ConfigurationFilterProvider.cs
new file mode 100644
index 00000000..14160a05
--- /dev/null
+++ b/src/System.Web.Http/Filters/ConfigurationFilterProvider.cs
@@ -0,0 +1,19 @@
+using System.Collections.Generic;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+
+namespace System.Web.Http.Filters
+{
+ public class ConfigurationFilterProvider : IFilterProvider
+ {
+ public IEnumerable<FilterInfo> GetFilters(HttpConfiguration configuration, HttpActionDescriptor actionDescriptor)
+ {
+ if (configuration == null)
+ {
+ throw Error.ArgumentNull("configuration");
+ }
+
+ return configuration.Filters;
+ }
+ }
+}
diff --git a/src/System.Web.Http/Filters/EnumerableEvaluatorFilter.cs b/src/System.Web.Http/Filters/EnumerableEvaluatorFilter.cs
new file mode 100644
index 00000000..79d5d5e2
--- /dev/null
+++ b/src/System.Web.Http/Filters/EnumerableEvaluatorFilter.cs
@@ -0,0 +1,125 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics.Contracts;
+using System.Linq;
+using System.Net.Http;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Internal;
+
+namespace System.Web.Http.Filters
+{
+ /// <summary>
+ /// An action filter that eagerly evaluates the results of any actions methods that return <see cref="IEnumerable{T}"/> or <see cref="IQueryable{T}"/>.
+ /// </summary>
+ /// <remarks>
+ /// This filter is required to run so that any lazily evaluated results are evaluated within the <see cref="ApiController"/> filter pipeline.
+ /// This ensures that any exceptions thrown during the evaluation will be processes by any registered exception filters and
+ /// that any context objects backing an <see cref="IQueryable{T}"/> result are safe to be disposed in the controller.
+ /// </remarks>
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+ internal sealed class EnumerableEvaluatorFilter : ActionFilterAttribute
+ {
+ private static object _conversionDelegateCacheKey = new object();
+ private static MethodInfo _convertMethod = typeof(EnumerableEvaluatorFilter).GetMethod("Convert", BindingFlags.Static | BindingFlags.NonPublic);
+ private static EnumerableEvaluatorFilter _instance = new EnumerableEvaluatorFilter();
+
+ internal static EnumerableEvaluatorFilter Instance
+ {
+ get { return _instance; }
+ }
+
+ public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
+ {
+ if (actionExecutedContext == null)
+ {
+ throw Error.ArgumentNull("actionExecutedContext");
+ }
+
+ HttpResponseMessage response = actionExecutedContext.Result;
+ IEnumerable valueAsEnumerable;
+ if (response == null || !response.TryGetObjectValue(out valueAsEnumerable))
+ {
+ return;
+ }
+
+ HttpActionDescriptor actionDescriptor = actionExecutedContext.ActionContext.ActionDescriptor;
+ Type declaredContentType = TypeHelper.GetUnderlyingContentInnerType(actionDescriptor.ReturnType);
+
+ if (!IsSupportedDeclaredContentType(declaredContentType))
+ {
+ return;
+ }
+
+ if (!declaredContentType.IsAssignableFrom(valueAsEnumerable.GetType()))
+ {
+ // If the current value in the response message is no longer of a type that's compatible with
+ // the action method's declared content type then do nothing. This could happen if some other filter
+ // decided to short-circuit the response with a different content.
+ return;
+ }
+
+ Func<object, object> conversionDelegate = GetConversionDelegate(actionDescriptor, declaredContentType);
+
+ if (conversionDelegate != null)
+ {
+ object valueAsList = conversionDelegate(valueAsEnumerable);
+ response.TrySetObjectValue(valueAsList);
+ }
+ }
+
+ private static Func<object, object> GetConversionDelegate(HttpActionDescriptor actionDescriptor, Type contentType)
+ {
+ Contract.Assert(actionDescriptor != null);
+ Contract.Assert(contentType != null);
+ Contract.Assert(contentType.IsGenericType);
+ Contract.Assert((contentType.GetGenericTypeDefinition() == typeof(IEnumerable<>) || contentType.GetGenericTypeDefinition() == typeof(IQueryable<>)));
+
+ Func<object, object> conversionDelegate = actionDescriptor.Properties.GetOrAdd<Func<object, object>>(_conversionDelegateCacheKey, _ =>
+ {
+ Type genericEnumerableType = TypeHelper.ExtractGenericInterface(contentType, typeof(IEnumerable<>));
+ Type enumerableParameterType = TypeHelper.GetTypeArgumentsIfMatch(genericEnumerableType, typeof(IEnumerable<>))[0];
+ return CompileConversionDelegate(enumerableParameterType);
+ });
+ return conversionDelegate;
+ }
+
+ internal static bool IsSupportedDeclaredContentType(Type contentType)
+ {
+ Contract.Assert(contentType != null);
+ // Only action methods that declare their returned content type as exactly IEnumerable<T> or
+ // IQueryable<T> are supported by this filter. Derived types (i.e. List<T>, etc) will not be
+ // processed by this filter.
+ if (!contentType.IsGenericType)
+ {
+ return false;
+ }
+ Type genericTypeDefinition = contentType.GetGenericTypeDefinition();
+ return genericTypeDefinition == typeof(IEnumerable<>) || genericTypeDefinition == typeof(IQueryable<>);
+ }
+
+ // Do not inline or optimize this method to avoid stack-related reflection demand issues when
+ // running from the GAC in medium trust
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
+ private static Func<object, object> CompileConversionDelegate(Type itemType)
+ {
+ Contract.Assert(itemType != null);
+
+ return (Func<object, object>)Delegate.CreateDelegate(typeof(Func<object, object>), _convertMethod.MakeGenericMethod(itemType));
+ }
+
+ private static object Convert<T>(object input)
+ {
+ // This method is called from the delegate constructed in CompileConversionDelegate()
+ IEnumerable<T> result = new List<T>((IEnumerable<T>)input);
+ if (input is IQueryable<T>)
+ {
+ // If the input is actually an IQueryable<T> return the result also typed as IQueryable<T>
+ result = result.AsQueryable<T>();
+ }
+ return result;
+ }
+ }
+}
diff --git a/src/System.Web.Http/Filters/EnumerableEvaluatorFilterProvider.cs b/src/System.Web.Http/Filters/EnumerableEvaluatorFilterProvider.cs
new file mode 100644
index 00000000..46809bd9
--- /dev/null
+++ b/src/System.Web.Http/Filters/EnumerableEvaluatorFilterProvider.cs
@@ -0,0 +1,36 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Internal;
+
+namespace System.Web.Http.Filters
+{
+ internal class EnumerableEvaluatorFilterProvider : IFilterProvider
+ {
+ public IEnumerable<FilterInfo> GetFilters(HttpConfiguration configuration, HttpActionDescriptor actionDescriptor)
+ {
+ if (configuration == null)
+ {
+ throw Error.ArgumentNull("configuration");
+ }
+ if (actionDescriptor == null)
+ {
+ throw Error.ArgumentNull("actionDescriptor");
+ }
+
+ var contentType = TypeHelper.GetUnderlyingContentInnerType(actionDescriptor.ReturnType);
+
+ if (EnumerableEvaluatorFilter.IsSupportedDeclaredContentType(contentType))
+ {
+ // Register filter in FilterScope.First so that it's closest to HttpDispatcher. This means
+ // the filter's "after" code path will be one of the last things to run.
+ return new[] { new FilterInfo(EnumerableEvaluatorFilter.Instance, FilterScope.First) };
+ }
+ else
+ {
+ return Enumerable.Empty<FilterInfo>();
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/Filters/ExceptionFilterAttribute.cs b/src/System.Web.Http/Filters/ExceptionFilterAttribute.cs
new file mode 100644
index 00000000..75a3bddc
--- /dev/null
+++ b/src/System.Web.Http/Filters/ExceptionFilterAttribute.cs
@@ -0,0 +1,25 @@
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Common;
+
+namespace System.Web.Http.Filters
+{
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
+ public abstract class ExceptionFilterAttribute : FilterAttribute, IExceptionFilter
+ {
+ public virtual void OnException(HttpActionExecutedContext actionExecutedContext)
+ {
+ }
+
+ Task IExceptionFilter.ExecuteExceptionFilterAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken)
+ {
+ if (actionExecutedContext == null)
+ {
+ throw Error.ArgumentNull("actionExecutedContext");
+ }
+
+ OnException(actionExecutedContext);
+ return TaskHelpers.Completed();
+ }
+ }
+}
diff --git a/src/System.Web.Http/Filters/FilterAttribute.cs b/src/System.Web.Http/Filters/FilterAttribute.cs
new file mode 100644
index 00000000..32c9fd21
--- /dev/null
+++ b/src/System.Web.Http/Filters/FilterAttribute.cs
@@ -0,0 +1,26 @@
+using System.Collections.Concurrent;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Web.Http.Internal;
+
+namespace System.Web.Http.Filters
+{
+ [SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", Justification = "We want to allow inheritance")]
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
+ public abstract class FilterAttribute : Attribute, IFilter
+ {
+ private static readonly ConcurrentDictionary<Type, bool> _attributeUsageCache = new ConcurrentDictionary<Type, bool>();
+
+ public bool AllowMultiple
+ {
+ get { return AllowsMultiple(GetType()); }
+ }
+
+ private static bool AllowsMultiple(Type attributeType)
+ {
+ return _attributeUsageCache.GetOrAdd(
+ attributeType,
+ type => type.GetCustomAttributes<AttributeUsageAttribute>(inherit: true).First().AllowMultiple);
+ }
+ }
+}
diff --git a/src/System.Web.Http/Filters/FilterInfo.cs b/src/System.Web.Http/Filters/FilterInfo.cs
new file mode 100644
index 00000000..2d3bb236
--- /dev/null
+++ b/src/System.Web.Http/Filters/FilterInfo.cs
@@ -0,0 +1,22 @@
+using System.Web.Http.Common;
+
+namespace System.Web.Http.Filters
+{
+ public sealed class FilterInfo
+ {
+ public FilterInfo(IFilter instance, FilterScope scope)
+ {
+ if (instance == null)
+ {
+ throw Error.ArgumentNull("instance");
+ }
+
+ Instance = instance;
+ Scope = scope;
+ }
+
+ public IFilter Instance { get; private set; }
+
+ public FilterScope Scope { get; private set; }
+ }
+}
diff --git a/src/System.Web.Http/Filters/FilterInfoComparer.cs b/src/System.Web.Http/Filters/FilterInfoComparer.cs
new file mode 100644
index 00000000..fe6bd3be
--- /dev/null
+++ b/src/System.Web.Http/Filters/FilterInfoComparer.cs
@@ -0,0 +1,35 @@
+using System.Collections.Generic;
+
+namespace System.Web.Http.Filters
+{
+ internal sealed class FilterInfoComparer : IComparer<FilterInfo>
+ {
+ private static readonly FilterInfoComparer _instance = new FilterInfoComparer();
+
+ public static FilterInfoComparer Instance
+ {
+ get { return _instance; }
+ }
+
+ public int Compare(FilterInfo x, FilterInfo y)
+ {
+ if (x == null && y == null)
+ {
+ return 0;
+ }
+ else if (x == null)
+ {
+ return -1;
+ }
+ else if (y == null)
+ {
+ return 1;
+ }
+ else
+ {
+ var r = x.Scope - y.Scope;
+ return r;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/Filters/FilterScope.cs b/src/System.Web.Http/Filters/FilterScope.cs
new file mode 100644
index 00000000..2bc7d4d2
--- /dev/null
+++ b/src/System.Web.Http/Filters/FilterScope.cs
@@ -0,0 +1,11 @@
+namespace System.Web.Http.Filters
+{
+ public enum FilterScope
+ {
+ First = 0,
+ Global = 10,
+ Controller = 20,
+ Action = 30,
+ Last = 100
+ }
+}
diff --git a/src/System.Web.Http/Filters/HttpActionExecutedContext.cs b/src/System.Web.Http/Filters/HttpActionExecutedContext.cs
new file mode 100644
index 00000000..f98d9ead
--- /dev/null
+++ b/src/System.Web.Http/Filters/HttpActionExecutedContext.cs
@@ -0,0 +1,60 @@
+using System.Net.Http;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+
+namespace System.Web.Http.Filters
+{
+ public class HttpActionExecutedContext
+ {
+ private HttpActionContext _actionContext;
+
+ public HttpActionExecutedContext(HttpActionContext actionContext, Exception exception)
+ {
+ if (actionContext == null)
+ {
+ throw Error.ArgumentNull("actionContext");
+ }
+
+ Exception = exception;
+ _actionContext = actionContext;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpActionExecutedContext"/> class.
+ /// </summary>
+ /// <remarks>The default constructor is intended for use by unit testing only.</remarks>
+ public HttpActionExecutedContext()
+ {
+ }
+
+ public HttpActionContext ActionContext
+ {
+ get { return _actionContext; }
+ set
+ {
+ if (value == null)
+ {
+ throw Error.PropertyNull();
+ }
+ _actionContext = value;
+ }
+ }
+
+ public Exception Exception { get; set; }
+
+ public HttpResponseMessage Result { get; set; }
+
+ /// <summary>
+ /// Gets the current <see cref="HttpRequestMessage"/>.
+ /// </summary>
+ public HttpRequestMessage Request
+ {
+ get
+ {
+ return (ActionContext != null && ActionContext.ControllerContext != null)
+ ? ActionContext.ControllerContext.Request
+ : null;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/Filters/HttpFilterCollection.cs b/src/System.Web.Http/Filters/HttpFilterCollection.cs
new file mode 100644
index 00000000..8819463f
--- /dev/null
+++ b/src/System.Web.Http/Filters/HttpFilterCollection.cs
@@ -0,0 +1,57 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Web.Http.Common;
+
+namespace System.Web.Http.Filters
+{
+ public class HttpFilterCollection : IEnumerable<FilterInfo>
+ {
+ private readonly List<FilterInfo> _filters = new List<FilterInfo>();
+
+ public int Count
+ {
+ get { return _filters.Count; }
+ }
+
+ public void Add(IFilter filter)
+ {
+ if (filter == null)
+ {
+ throw Error.ArgumentNull("filter");
+ }
+
+ AddInternal(new FilterInfo(filter, FilterScope.Global));
+ }
+
+ private void AddInternal(FilterInfo filter)
+ {
+ _filters.Add(filter);
+ }
+
+ public void Clear()
+ {
+ _filters.Clear();
+ }
+
+ public bool Contains(IFilter filter)
+ {
+ return _filters.Any(f => f.Instance == filter);
+ }
+
+ public IEnumerator<FilterInfo> GetEnumerator()
+ {
+ return _filters.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ public void Remove(IFilter filter)
+ {
+ _filters.RemoveAll(f => f.Instance == filter);
+ }
+ }
+}
diff --git a/src/System.Web.Http/Filters/IActionFilter.cs b/src/System.Web.Http/Filters/IActionFilter.cs
new file mode 100644
index 00000000..a48c651b
--- /dev/null
+++ b/src/System.Web.Http/Filters/IActionFilter.cs
@@ -0,0 +1,14 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Controllers;
+
+namespace System.Web.Http.Filters
+{
+ public interface IActionFilter : IFilter
+ {
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Represents a continuation call")]
+ Task<HttpResponseMessage> ExecuteActionFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation);
+ }
+}
diff --git a/src/System.Web.Http/Filters/IAuthorizationFilter.cs b/src/System.Web.Http/Filters/IAuthorizationFilter.cs
new file mode 100644
index 00000000..94aa4423
--- /dev/null
+++ b/src/System.Web.Http/Filters/IAuthorizationFilter.cs
@@ -0,0 +1,14 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Controllers;
+
+namespace System.Web.Http.Filters
+{
+ public interface IAuthorizationFilter : IFilter
+ {
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Represents a continuation call")]
+ Task<HttpResponseMessage> ExecuteAuthorizationFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation);
+ }
+}
diff --git a/src/System.Web.Http/Filters/IExceptionFilter.cs b/src/System.Web.Http/Filters/IExceptionFilter.cs
new file mode 100644
index 00000000..ecdc7ec1
--- /dev/null
+++ b/src/System.Web.Http/Filters/IExceptionFilter.cs
@@ -0,0 +1,10 @@
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace System.Web.Http.Filters
+{
+ public interface IExceptionFilter : IFilter
+ {
+ Task ExecuteExceptionFilterAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken);
+ }
+}
diff --git a/src/System.Web.Http/Filters/IFilter.cs b/src/System.Web.Http/Filters/IFilter.cs
new file mode 100644
index 00000000..b86e1e25
--- /dev/null
+++ b/src/System.Web.Http/Filters/IFilter.cs
@@ -0,0 +1,7 @@
+namespace System.Web.Http.Filters
+{
+ public interface IFilter
+ {
+ bool AllowMultiple { get; }
+ }
+}
diff --git a/src/System.Web.Http/Filters/IFilterProvider.cs b/src/System.Web.Http/Filters/IFilterProvider.cs
new file mode 100644
index 00000000..c0db133b
--- /dev/null
+++ b/src/System.Web.Http/Filters/IFilterProvider.cs
@@ -0,0 +1,10 @@
+using System.Collections.Generic;
+using System.Web.Http.Controllers;
+
+namespace System.Web.Http.Filters
+{
+ public interface IFilterProvider
+ {
+ IEnumerable<FilterInfo> GetFilters(HttpConfiguration configuration, HttpActionDescriptor actionDescriptor);
+ }
+}
diff --git a/src/System.Web.Http/Filters/QueryCompositionFilterAttribute.cs b/src/System.Web.Http/Filters/QueryCompositionFilterAttribute.cs
new file mode 100644
index 00000000..1ea42e8a
--- /dev/null
+++ b/src/System.Web.Http/Filters/QueryCompositionFilterAttribute.cs
@@ -0,0 +1,104 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+using System.Linq;
+using System.Net.Http;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Internal;
+using System.Web.Http.Properties;
+using System.Web.Http.Query;
+
+namespace System.Web.Http.Filters
+{
+ [AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
+ internal sealed class QueryCompositionFilterAttribute : ActionFilterAttribute
+ {
+ [SuppressMessage("Microsoft.Performance", "CA1802: Use Literals Where Appropriate", Justification = "to be consistent with usages elsewhere")]
+ private static readonly string QueryKey = "MS_QueryKey";
+
+ private readonly IQueryable _baseQuery;
+
+ public QueryCompositionFilterAttribute(Type queryElementType, QueryValidator queryValidator)
+ {
+ if (queryElementType == null)
+ {
+ throw Error.ArgumentNull("queryElementType");
+ }
+
+ QueryValidator = queryValidator;
+ QueryElementType = queryElementType;
+ _baseQuery = Array.CreateInstance(queryElementType, 0).AsQueryable(); // T[]
+ }
+
+ private QueryValidator QueryValidator { get; set; }
+
+ public Type QueryElementType { get; private set; }
+
+ public override void OnActionExecuting(HttpActionContext actioncContext)
+ {
+ if (actioncContext == null)
+ {
+ throw Error.ArgumentNull("actionContext");
+ }
+
+ HttpRequestMessage request = actioncContext.ControllerContext.Request;
+ if (request != null && request.RequestUri != null && !String.IsNullOrWhiteSpace(request.RequestUri.Query))
+ {
+ Uri requestUri = request.RequestUri;
+ try
+ {
+ ServiceQuery serviceQuery = ODataQueryDeserializer.GetServiceQuery(requestUri);
+
+ if (serviceQuery.QueryParts.Count() > 0)
+ {
+ IQueryable deserializedQuery = ODataQueryDeserializer.Deserialize(_baseQuery, serviceQuery.QueryParts);
+ if (QueryValidator != null)
+ {
+ QueryValidator.Validate(deserializedQuery);
+ }
+
+ request.Properties.Add(QueryCompositionFilterAttribute.QueryKey, deserializedQuery);
+ }
+ }
+ catch (InvalidOperationException e)
+ {
+ throw new HttpRequestException(SRResources.UriQueryStringInvalid, e);
+ }
+ catch (ParseException e)
+ {
+ throw new HttpRequestException(SRResources.UriQueryStringInvalid, e);
+ }
+ catch (FormatException e)
+ {
+ throw new HttpRequestException(SRResources.UriQueryStringInvalid, e);
+ }
+ }
+ }
+
+ public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
+ {
+ if (actionExecutedContext == null)
+ {
+ throw Error.ArgumentNull("actionExecutedContext");
+ }
+
+ Contract.Assert(actionExecutedContext.Request != null);
+
+ HttpRequestMessage request = actionExecutedContext.Request;
+ HttpResponseMessage response = actionExecutedContext.Result;
+
+ IQueryable query;
+ if (request == null || !request.Properties.TryGetValue(QueryCompositionFilterAttribute.QueryKey, out query))
+ {
+ return; // No Query to compose, return
+ }
+
+ IQueryable source;
+ if (response != null && response.TryGetObjectValue(out source))
+ {
+ IQueryable composedQuery = QueryComposer.Compose(source, query);
+ response.TrySetObjectValue(composedQuery);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/Filters/QueryCompositionFilterProvider.cs b/src/System.Web.Http/Filters/QueryCompositionFilterProvider.cs
new file mode 100644
index 00000000..174891c5
--- /dev/null
+++ b/src/System.Web.Http/Filters/QueryCompositionFilterProvider.cs
@@ -0,0 +1,35 @@
+using System.Collections.Generic;
+using System.Diagnostics.Contracts;
+using System.Linq;
+using System.Web.Http.Controllers;
+using System.Web.Http.Internal;
+using System.Web.Http.Query;
+
+namespace System.Web.Http.Filters
+{
+ internal class QueryCompositionFilterProvider : IFilterProvider
+ {
+ public IEnumerable<FilterInfo> GetFilters(HttpConfiguration configuration, HttpActionDescriptor actionDescriptor)
+ {
+ if (actionDescriptor.ReturnType != null)
+ {
+ Type queryElementType = GetQueryElementTypeOrNull(actionDescriptor.ReturnType);
+ if (queryElementType != null)
+ {
+ QueryCompositionFilterAttribute filter = new QueryCompositionFilterAttribute(queryElementType, QueryValidator.Instance);
+ return new List<FilterInfo> { new FilterInfo(filter, FilterScope.Last) };
+ }
+ }
+
+ return Enumerable.Empty<FilterInfo>();
+ }
+
+ private static Type GetQueryElementTypeOrNull(Type returnType)
+ {
+ Contract.Assert(returnType != null);
+
+ returnType = TypeHelper.GetUnderlyingContentInnerType(returnType);
+ return QueryTypeHelper.GetQueryableInterfaceInnerTypeOrNull(returnType); // IQueryable<T> => T
+ }
+ }
+}
diff --git a/src/System.Web.Http/FromBodyAttribute.cs b/src/System.Web.Http/FromBodyAttribute.cs
new file mode 100644
index 00000000..809d3a01
--- /dev/null
+++ b/src/System.Web.Http/FromBodyAttribute.cs
@@ -0,0 +1,14 @@
+using System.Net.Http;
+using System.Web.Http.ValueProviders;
+
+namespace System.Web.Http
+{
+ /// <summary>
+ /// This attribute is used on action parameters to indicate
+ /// they come only from the content body of the incoming <see cref="HttpRequestMessage"/>.
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Parameter, Inherited = true, AllowMultiple = false)]
+ public sealed class FromBodyAttribute : Attribute
+ {
+ }
+}
diff --git a/src/System.Web.Http/FromUriAttribute.cs b/src/System.Web.Http/FromUriAttribute.cs
new file mode 100644
index 00000000..2607f4ac
--- /dev/null
+++ b/src/System.Web.Http/FromUriAttribute.cs
@@ -0,0 +1,17 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Web.Http.ModelBinding;
+using System.Web.Http.ValueProviders;
+
+namespace System.Web.Http
+{
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Parameter, Inherited = true, AllowMultiple = false)]
+ public sealed class FromUriAttribute : ModelBinderAttribute
+ {
+ public override IEnumerable<ValueProviderFactory> GetValueProviderFactories(HttpConfiguration configuration)
+ {
+ var factories = from f in base.GetValueProviderFactories(configuration) where f is IUriValueProviderFactory select f;
+ return factories;
+ }
+ }
+}
diff --git a/src/System.Web.Http/GlobalSuppressions.cs b/src/System.Web.Http/GlobalSuppressions.cs
new file mode 100644
index 00000000..e500f1ae
--- /dev/null
+++ b/src/System.Web.Http/GlobalSuppressions.cs
@@ -0,0 +1,17 @@
+using System.Diagnostics.CodeAnalysis;
+
+[assembly: SuppressMessage("Microsoft.Design", "CA2210:AssembliesShouldHaveValidStrongNames", Justification = "The assembly is delay signed")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "System.Web.Http.Controllers")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "System.Web.Http.Dispatcher")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "System.Web.Http.Hosting")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "System.Web.Http.Metadata")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "System.Web.Http.Validation.ClientRules")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "System.Web.Http.ValueProviders")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "System.Web.Http.ValueProviders.Providers")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "System.Web.Http.Services")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Scope = "member", Target = "System.Web.Http.Filters.AuthorizationFilterAttribute.#System.Web.Http.Filters.IAuthorizationFilter.ExecuteAuthorizationFilterAsync(System.Web.Http.Controllers.HttpActionContext,System.Threading.CancellationToken,System.Func`1<System.Threading.Tasks.Task`1<System.Net.Http.HttpResponseMessage>>)")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Scope = "member", Target = "System.Web.Http.Filters.ExceptionFilterAttribute.#System.Web.Http.Filters.IExceptionFilter.ExecuteExceptionFilterAsync(System.Web.Http.Filters.HttpActionExecutedContext,System.Threading.CancellationToken)")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Scope = "member", Target = "System.Web.Http.Filters.ActionFilterAttribute.#System.Web.Http.Filters.IActionFilter.ExecuteActionFilterAsync(System.Web.Http.Controllers.HttpActionContext,System.Threading.CancellationToken,System.Func`1<System.Threading.Tasks.Task`1<System.Net.Http.HttpResponseMessage>>)")]
+[assembly: SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Scope = "member", Target = "System.Web.Http.Tracing.Tracers.ActionInvokerTracer.#System.Web.Http.Controllers.IHttpActionInvoker.InvokeActionAsync(System.Web.Http.Controllers.HttpActionContext,System.Threading.CancellationToken)", Justification = "Tracing layer needs to observe all Task completion paths")]
+[assembly: SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Scope = "member", Target = "System.Web.Http.Tracing.Tracers.ApiControllerTracer.#System.Web.Http.Controllers.IHttpController.ExecuteAsync(System.Web.Http.Controllers.HttpControllerContext,System.Threading.CancellationToken)", Justification = "Tracing layer needs to observe all Task completion paths")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "System.Web.Http.Tracing.Tracers", Justification = "Namespace follows folder structure")]
diff --git a/src/System.Web.Http/Hosting/HttpPipelineFactory.cs b/src/System.Web.Http/Hosting/HttpPipelineFactory.cs
new file mode 100644
index 00000000..0e3b05f0
--- /dev/null
+++ b/src/System.Web.Http/Hosting/HttpPipelineFactory.cs
@@ -0,0 +1,54 @@
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Web.Http.Common;
+using System.Web.Http.Properties;
+
+namespace System.Web.Http.Hosting
+{
+ /// <summary>
+ /// Initializing a <see cref="DelegatingHandler"/> pipeline.
+ /// </summary>
+ internal static class HttpPipelineFactory
+ {
+ /// <summary>
+ /// Creates an instance of an <see cref="HttpMessageHandler"/> using the <see cref="DelegatingHandler"/> instances
+ /// provided by <paramref name="handlers"/>.
+ /// </summary>
+ /// <param name="handlers">An ordered list of <see cref="DelegatingHandler"/> instances to be invoked as an
+ /// <see cref="HttpRequestMessage"/> travels up the stack and an <see cref="HttpResponseMessage"/> travels down.</param>
+ /// <param name="innerChannel">The inner channel represents the destination of the HTTP message channel.</param>
+ /// <returns>The HTTP message channel.</returns>
+ public static HttpMessageHandler Create(IEnumerable<DelegatingHandler> handlers, HttpMessageHandler innerChannel)
+ {
+ if (innerChannel == null)
+ {
+ throw Error.ArgumentNull("innerChannel");
+ }
+
+ if (handlers == null)
+ {
+ return innerChannel;
+ }
+
+ // Wire handlers up
+ HttpMessageHandler pipeline = innerChannel;
+ foreach (DelegatingHandler handler in handlers)
+ {
+ if (handler == null)
+ {
+ throw Error.Argument("handlers", SRResources.DelegatingHandlerArrayContainsNullItem, typeof(DelegatingHandler).Name);
+ }
+
+ if (handler.InnerHandler != null)
+ {
+ throw Error.Argument("handlers", SRResources.DelegatingHandlerArrayHasNonNullInnerHandler, typeof(DelegatingHandler).Name, "InnerHandler", handler.GetType().Name);
+ }
+
+ handler.InnerHandler = pipeline;
+ pipeline = handler;
+ }
+
+ return pipeline;
+ }
+ }
+}
diff --git a/src/System.Web.Http/Hosting/HttpPropertyKeys.cs b/src/System.Web.Http/Hosting/HttpPropertyKeys.cs
new file mode 100644
index 00000000..8cb31c32
--- /dev/null
+++ b/src/System.Web.Http/Hosting/HttpPropertyKeys.cs
@@ -0,0 +1,45 @@
+using System.Net.Http;
+using System.Security.Principal;
+using System.Threading;
+using System.Web.Http.Routing;
+
+namespace System.Web.Http.Hosting
+{
+ /// <summary>
+ /// Provides common keys for properties stored in the <see cref="HttpRequestMessage.Properties"/>
+ /// </summary>
+ public static class HttpPropertyKeys
+ {
+ /// <summary>
+ /// Provides a key for the <see cref="HttpConfiguration"/> associated with this request.
+ /// </summary>
+ public static readonly string HttpConfigurationKey = "MS_HttpConfiguration";
+
+ /// <summary>
+ /// Provides a key for the <see cref="IHttpRouteData"/> associated with this request.
+ /// </summary>
+ public static readonly string HttpRouteDataKey = "MS_HttpRouteData";
+
+ /// <summary>
+ /// Provides a key for the current <see cref="SynchronizationContext"/> stored in <see cref="HttpRequestMessage.Properties"/>.
+ /// If <see cref="SynchronizationContext.Current"/> is <c>null</c> then no context is stored.
+ /// </summary>
+ public static readonly string SynchronizationContextKey = "MS_SynchronizationContext";
+
+ /// <summary>
+ /// Provides a key for the current <see cref="IPrincipal"/> stored in <see cref="HttpRequestMessage.Properties"/>.
+ /// </summary>
+ public static readonly string UserPrincipalKey = "MS_UserPrincipal";
+
+ /// <summary>
+ /// Provides a key for the collection of resources that should be disposed when a request is disposed.
+ /// </summary>
+ public static readonly string DisposableRequestResourcesKey = "MS_DisposableRequestResources";
+
+ /// <summary>
+ /// Provides a key for the <see cref="Guid"/> stored in <see cref="HttpRequestMessage.Properties"/>.
+ /// This is the correlation id for that request.
+ /// </summary>
+ public static readonly string RequestCorrelationKey = "MS_RequestId";
+ }
+}
diff --git a/src/System.Web.Http/HttpBindNeverAttribute.cs b/src/System.Web.Http/HttpBindNeverAttribute.cs
new file mode 100644
index 00000000..81c2e458
--- /dev/null
+++ b/src/System.Web.Http/HttpBindNeverAttribute.cs
@@ -0,0 +1,11 @@
+namespace System.Web.Http
+{
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
+ public sealed class HttpBindNeverAttribute : HttpBindingBehaviorAttribute
+ {
+ public HttpBindNeverAttribute()
+ : base(HttpBindingBehavior.Never)
+ {
+ }
+ }
+}
diff --git a/src/System.Web.Http/HttpBindRequiredAttribute.cs b/src/System.Web.Http/HttpBindRequiredAttribute.cs
new file mode 100644
index 00000000..a98650da
--- /dev/null
+++ b/src/System.Web.Http/HttpBindRequiredAttribute.cs
@@ -0,0 +1,11 @@
+namespace System.Web.Http
+{
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
+ public sealed class HttpBindRequiredAttribute : HttpBindingBehaviorAttribute
+ {
+ public HttpBindRequiredAttribute()
+ : base(HttpBindingBehavior.Required)
+ {
+ }
+ }
+}
diff --git a/src/System.Web.Http/HttpBindingBehavior.cs b/src/System.Web.Http/HttpBindingBehavior.cs
new file mode 100644
index 00000000..b2186a2b
--- /dev/null
+++ b/src/System.Web.Http/HttpBindingBehavior.cs
@@ -0,0 +1,11 @@
+// Taken from Microsoft.Web.Mvc
+
+namespace System.Web.Http
+{
+ public enum HttpBindingBehavior
+ {
+ Optional = 0,
+ Never,
+ Required
+ }
+}
diff --git a/src/System.Web.Http/HttpBindingBehaviorAttribute.cs b/src/System.Web.Http/HttpBindingBehaviorAttribute.cs
new file mode 100644
index 00000000..72f47701
--- /dev/null
+++ b/src/System.Web.Http/HttpBindingBehaviorAttribute.cs
@@ -0,0 +1,23 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Web.Http
+{
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
+ [SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", Justification = "This class is designed to be overridden")]
+ public class HttpBindingBehaviorAttribute : Attribute
+ {
+ private static readonly object _typeId = new object();
+
+ public HttpBindingBehaviorAttribute(HttpBindingBehavior behavior)
+ {
+ Behavior = behavior;
+ }
+
+ public HttpBindingBehavior Behavior { get; private set; }
+
+ public override object TypeId
+ {
+ get { return _typeId; }
+ }
+ }
+}
diff --git a/src/System.Web.Http/HttpConfiguration.cs b/src/System.Web.Http/HttpConfiguration.cs
new file mode 100644
index 00000000..77349812
--- /dev/null
+++ b/src/System.Web.Http/HttpConfiguration.cs
@@ -0,0 +1,183 @@
+using System.Collections.Concurrent;
+using System.Collections.ObjectModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Net.Http;
+using System.Net.Http.Formatting;
+using System.Web.Http.Common;
+using System.Web.Http.Filters;
+using System.Web.Http.ModelBinding;
+using System.Web.Http.Services;
+using System.Web.Http.Validation;
+
+namespace System.Web.Http
+{
+ /// <summary>
+ /// Configuration of <see cref="HttpServer"/> instances.
+ /// </summary>
+ public class HttpConfiguration : IDisposable
+ {
+ private readonly HttpRouteCollection _routes;
+ private readonly DependencyResolver _serviceResolver;
+ private readonly ConcurrentDictionary<object, object> _properties = new ConcurrentDictionary<object, object>();
+ private readonly MediaTypeFormatterCollection _formatters = DefaultFormatters();
+ private readonly Collection<DelegatingHandler> _messageHandlers = new Collection<DelegatingHandler>();
+ private readonly HttpFilterCollection _filters = new HttpFilterCollection();
+ private bool _disposed;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpConfiguration"/> class.
+ /// </summary>
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope",
+ Justification = "The route collection is disposed as part of this class.")]
+ public HttpConfiguration()
+ : this(new HttpRouteCollection(String.Empty))
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpConfiguration"/> class.
+ /// </summary>
+ /// <param name="routes">The <see cref="HttpRouteCollection"/> to associate with this instance.</param>
+ public HttpConfiguration(HttpRouteCollection routes)
+ {
+ if (routes == null)
+ {
+ throw Error.ArgumentNull("routes");
+ }
+
+ _routes = routes;
+ _serviceResolver = new DependencyResolver(this);
+
+ IRequiredMemberSelector requiredMemberSelector = new ModelValidationRequiredMemberSelector(this);
+ foreach (MediaTypeFormatter formatter in _formatters)
+ {
+ formatter.RequiredMemberSelector = requiredMemberSelector;
+ }
+ }
+
+ /// <summary>
+ /// Gets the list of filters that apply to all requests served using this HttpConfiguration instance.
+ /// </summary>
+ public HttpFilterCollection Filters
+ {
+ get { return _filters; }
+ }
+
+ /// <summary>
+ /// Gets an ordered list of <see cref="DelegatingHandler"/> instances to be invoked as an
+ /// <see cref="HttpRequestMessage"/> travels up the stack and an <see cref="HttpResponseMessage"/> travels down in
+ /// stack in return. The handlers are invoked in a bottom-up fashion in the incoming path and top-down in the outgoing
+ /// path. That is, the last entry is called first for an incoming request message but invoked last for an outgoing
+ /// response message.
+ /// </summary>
+ /// <value>
+ /// The message handler collection.
+ /// </value>
+ public Collection<DelegatingHandler> MessageHandlers
+ {
+ get { return _messageHandlers; }
+ }
+
+ /// <summary>
+ /// Gets the <see cref="HttpRouteCollection"/> associated with this <see cref="HttpServer"/> instance.
+ /// </summary>
+ /// <value>
+ /// The <see cref="HttpRouteCollection"/>.
+ /// </value>
+ public HttpRouteCollection Routes
+ {
+ get { return _routes; }
+ }
+
+ /// <summary>
+ /// Gets the properties associated with this instance.
+ /// </summary>
+ public ConcurrentDictionary<object, object> Properties
+ {
+ get { return _properties; }
+ }
+
+ /// <summary>
+ /// Gets the root virtual path. The <see cref="VirtualPathRoot"/> property always returns
+ /// "/" as the first character of the returned value.
+ /// </summary>
+ public string VirtualPathRoot
+ {
+ get { return _routes.VirtualPathRoot; }
+ }
+
+ /// <summary>
+ /// Gets the <see cref="DependencyResolver"/> used to resolve services to use by this <see cref="HttpServer"/>.
+ /// </summary>
+ /// <value>
+ /// The <see cref="DependencyResolver"/>.
+ /// </value>
+ public DependencyResolver ServiceResolver
+ {
+ get { return _serviceResolver; }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether error details should be included in error messages.
+ /// </summary>
+ public IncludeErrorDetailPolicy IncludeErrorDetailPolicy { get; set; }
+
+ /// <summary>
+ /// Gets the media type formatters.
+ /// </summary>
+ public MediaTypeFormatterCollection Formatters
+ {
+ get { return _formatters; }
+ }
+
+ private static MediaTypeFormatterCollection DefaultFormatters()
+ {
+ var formatters = new MediaTypeFormatterCollection();
+
+ // Basic FormUrlFormatter does not support binding to a T.
+ // Use our JQuery formatter instead.
+ formatters.Add(new JQueryMvcFormUrlEncodedFormatter());
+
+ return formatters;
+ }
+
+ internal bool ShouldIncludeErrorDetail(HttpRequestMessage request)
+ {
+ switch (IncludeErrorDetailPolicy)
+ {
+ case IncludeErrorDetailPolicy.LocalOnly:
+ Uri requestUri = request.RequestUri;
+ return requestUri.IsAbsoluteUri && requestUri.IsLoopback;
+
+ case IncludeErrorDetailPolicy.Always:
+ return true;
+
+ case IncludeErrorDetailPolicy.Never:
+ default:
+ return false;
+ }
+ }
+
+ #region IDisposable
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_disposed)
+ {
+ _disposed = true;
+ if (disposing)
+ {
+ _routes.Dispose();
+ }
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/System.Web.Http/HttpDeleteAttribute.cs b/src/System.Web.Http/HttpDeleteAttribute.cs
new file mode 100644
index 00000000..bf589da8
--- /dev/null
+++ b/src/System.Web.Http/HttpDeleteAttribute.cs
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Net.Http;
+using System.Web.Http.Controllers;
+
+namespace System.Web.Http
+{
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+ public sealed class HttpDeleteAttribute : Attribute, IActionHttpMethodProvider
+ {
+ private static readonly Collection<HttpMethod> _supportedMethods = new Collection<HttpMethod>(new HttpMethod[] { HttpMethod.Delete });
+
+ public Collection<HttpMethod> HttpMethods
+ {
+ get
+ {
+ return _supportedMethods;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/HttpGetAttribute.cs b/src/System.Web.Http/HttpGetAttribute.cs
new file mode 100644
index 00000000..2efb3ff6
--- /dev/null
+++ b/src/System.Web.Http/HttpGetAttribute.cs
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Net.Http;
+using System.Web.Http.Controllers;
+
+namespace System.Web.Http
+{
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+ public sealed class HttpGetAttribute : Attribute, IActionHttpMethodProvider
+ {
+ private static readonly Collection<HttpMethod> _supportedMethods = new Collection<HttpMethod>(new HttpMethod[] { HttpMethod.Get });
+
+ public Collection<HttpMethod> HttpMethods
+ {
+ get
+ {
+ return _supportedMethods;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/HttpPostAttribute.cs b/src/System.Web.Http/HttpPostAttribute.cs
new file mode 100644
index 00000000..8895fc15
--- /dev/null
+++ b/src/System.Web.Http/HttpPostAttribute.cs
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Net.Http;
+using System.Web.Http.Controllers;
+
+namespace System.Web.Http
+{
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+ public sealed class HttpPostAttribute : Attribute, IActionHttpMethodProvider
+ {
+ private static readonly Collection<HttpMethod> _supportedMethods = new Collection<HttpMethod>(new HttpMethod[] { HttpMethod.Post });
+
+ public Collection<HttpMethod> HttpMethods
+ {
+ get
+ {
+ return _supportedMethods;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/HttpPutAttribute.cs b/src/System.Web.Http/HttpPutAttribute.cs
new file mode 100644
index 00000000..e39031c4
--- /dev/null
+++ b/src/System.Web.Http/HttpPutAttribute.cs
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Net.Http;
+using System.Web.Http.Controllers;
+
+namespace System.Web.Http
+{
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+ public sealed class HttpPutAttribute : Attribute, IActionHttpMethodProvider
+ {
+ private static readonly Collection<HttpMethod> _supportedMethods = new Collection<HttpMethod>(new HttpMethod[] { HttpMethod.Put });
+
+ public Collection<HttpMethod> HttpMethods
+ {
+ get
+ {
+ return _supportedMethods;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/HttpRequestMessageExtensions.cs b/src/System.Web.Http/HttpRequestMessageExtensions.cs
new file mode 100644
index 00000000..bec5c12c
--- /dev/null
+++ b/src/System.Web.Http/HttpRequestMessageExtensions.cs
@@ -0,0 +1,242 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Formatting;
+using System.Net.Http.Headers;
+using System.Security.Principal;
+using System.Threading;
+using System.Web.Http.Common;
+using System.Web.Http.Hosting;
+using System.Web.Http.Properties;
+using System.Web.Http.Routing;
+
+namespace System.Web.Http
+{
+ /// <summary>
+ /// Provides extension methods for the <see cref="HttpRequestMessage"/> class.
+ /// </summary>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static class HttpRequestMessageExtensions
+ {
+ /// <summary>
+ /// Gets the <see cref="HttpConfiguration"/> for the given request.
+ /// </summary>
+ /// <param name="request">The HTTP request.</param>
+ /// <returns>The <see cref="HttpConfiguration"/>.</returns>
+ public static HttpConfiguration GetConfiguration(this HttpRequestMessage request)
+ {
+ if (request == null)
+ {
+ throw Error.ArgumentNull("request");
+ }
+
+ return request.GetProperty<HttpConfiguration>(HttpPropertyKeys.HttpConfigurationKey);
+ }
+
+ /// <summary>
+ /// Gets the <see cref="System.Security.Principal.IPrincipal"/> for the given request or null if not available.
+ /// </summary>
+ /// <param name="request">The HTTP request.</param>
+ /// <returns>The <see cref="System.Security.Principal.IPrincipal"/> or null.</returns>
+ public static IPrincipal GetUserPrincipal(this HttpRequestMessage request)
+ {
+ if (request == null)
+ {
+ throw Error.ArgumentNull("request");
+ }
+
+ return request.GetProperty<IPrincipal>(HttpPropertyKeys.UserPrincipalKey);
+ }
+
+ /// <summary>
+ /// Sets the <see cref="System.Security.Principal.IPrincipal"/> for the given request
+ /// </summary>
+ /// <param name="request">The HTTP request.</param>
+ /// <param name="principal">The IPrincipal value to set to.</param>
+ public static void SetUserPrincipal(this HttpRequestMessage request, IPrincipal principal)
+ {
+ if (request == null)
+ {
+ throw Error.ArgumentNull("request");
+ }
+
+ if (principal == null)
+ {
+ throw Error.ArgumentNull("principal");
+ }
+
+ request.Properties[HttpPropertyKeys.UserPrincipalKey] = principal;
+ }
+
+ /// <summary>
+ /// Gets the <see cref="System.Threading.SynchronizationContext"/> for the given request or null if not available.
+ /// </summary>
+ /// <param name="request">The HTTP request.</param>
+ /// <returns>The <see cref="System.Threading.SynchronizationContext"/> or null.</returns>
+ public static SynchronizationContext GetSynchronizationContext(this HttpRequestMessage request)
+ {
+ if (request == null)
+ {
+ throw Error.ArgumentNull("request");
+ }
+
+ return request.GetProperty<SynchronizationContext>(HttpPropertyKeys.SynchronizationContextKey);
+ }
+
+ /// <summary>
+ /// Gets the <see cref="System.Web.Http.Routing.IHttpRouteData"/> for the given request or null if not available.
+ /// </summary>
+ /// <param name="request">The HTTP request.</param>
+ /// <returns>The <see cref="System.Web.Http.Routing.IHttpRouteData"/> or null.</returns>
+ public static IHttpRouteData GetRouteData(this HttpRequestMessage request)
+ {
+ if (request == null)
+ {
+ throw Error.ArgumentNull("request");
+ }
+
+ return request.GetProperty<IHttpRouteData>(HttpPropertyKeys.HttpRouteDataKey);
+ }
+
+ private static T GetProperty<T>(this HttpRequestMessage request, string key)
+ {
+ T value;
+ request.Properties.TryGetValue(key, out value);
+ return value;
+ }
+
+ /// <summary>
+ /// Helper method that performs content negotiation and creates a <see cref="HttpResponseMessage"/> with an instance
+ /// of <see cref="ObjectContent{T}"/> as the content. This forwards the call to
+ /// <see cref="CreateResponse{T}(HttpRequestMessage, HttpStatusCode, T, HttpConfiguration)"/> with a <c>null</c>
+ /// configuration.
+ /// </summary>
+ /// <remarks>
+ /// This method requires that <paramref name="request"/> has been associated with an instance of
+ /// <see cref="HttpConfiguration"/>.
+ /// </remarks>
+ /// <typeparam name="T">The type of the value.</typeparam>
+ /// <param name="request">The request.</param>
+ /// <param name="statusCode">The status code of the created response.</param>
+ /// <param name="value">The value to wrap. Can be <c>null</c>.</param>
+ /// <returns>A response wrapping <paramref name="value"/> with <paramref name="statusCode"/>.</returns>
+ public static HttpResponseMessage CreateResponse<T>(this HttpRequestMessage request, HttpStatusCode statusCode, T value)
+ {
+ if (request == null)
+ {
+ throw Error.ArgumentNull("request");
+ }
+
+ return request.CreateResponse<T>(statusCode, value, configuration: null);
+ }
+
+ /// <summary>
+ /// Helper method that performs content negotiation and creates a <see cref="HttpResponseMessage"/> with an instance
+ /// of <see cref="ObjectContent{T}"/> as the content.
+ /// </summary>
+ /// <remarks>
+ /// This method will use the provided <paramref name="configuration"/> or it will get the
+ /// <see cref="HttpConfiguration"/> instance associated with <paramref name="request"/>.
+ /// </remarks>
+ /// <typeparam name="T">The type of the value.</typeparam>
+ /// <param name="request">The request.</param>
+ /// <param name="statusCode">The status code of the created response.</param>
+ /// <param name="value">The value to wrap. Can be <c>null</c>.</param>
+ /// <param name="configuration">The configuration to use. Can be <c>null</c>.</param>
+ /// <returns>A response wrapping <paramref name="value"/> with <paramref name="statusCode"/>.</returns>
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Caller will dispose")]
+ public static HttpResponseMessage CreateResponse<T>(this HttpRequestMessage request, HttpStatusCode statusCode, T value, HttpConfiguration configuration)
+ {
+ if (request == null)
+ {
+ throw Error.ArgumentNull("request");
+ }
+
+ configuration = configuration ?? request.GetConfiguration();
+ if (configuration == null)
+ {
+ throw Error.InvalidOperation(SRResources.HttpRequestMessageExtensions_NoConfiguration);
+ }
+
+ IContentNegotiator contentNegotiator = configuration.ServiceResolver.GetContentNegotiator();
+ if (contentNegotiator == null)
+ {
+ // TODO ???
+ }
+
+ IEnumerable<MediaTypeFormatter> formatters = configuration.Formatters;
+
+ // Run content negotiation
+ MediaTypeHeaderValue mediaType;
+ MediaTypeFormatter formatter = contentNegotiator.Negotiate(typeof(T), request, formatters, out mediaType);
+
+ return new HttpResponseMessage
+ {
+ Content = new ObjectContent<T>(value, formatter, mediaType != null ? mediaType.ToString() : null),
+ StatusCode = statusCode,
+ RequestMessage = request
+ };
+ }
+
+ /// <summary>
+ /// Adds the given <paramref name="resource"/> to a list of resources that will be disposed by a host once
+ /// the <paramref name="request"/> is disposed.
+ /// </summary>
+ /// <param name="request">The request controlling the lifecycle of <paramref name="resource"/>.</param>
+ /// <param name="resource">The resource to dispose when <paramref name="request"/> is being disposed. Can be <c>null</c>.</param>
+ public static void RegisterForDispose(this HttpRequestMessage request, IDisposable resource)
+ {
+ if (request == null)
+ {
+ throw Error.ArgumentNull("request");
+ }
+
+ if (resource == null)
+ {
+ return;
+ }
+
+ List<IDisposable> trackedResources;
+ if (!request.Properties.TryGetValue(HttpPropertyKeys.DisposableRequestResourcesKey, out trackedResources))
+ {
+ trackedResources = new List<IDisposable>();
+ request.Properties[HttpPropertyKeys.DisposableRequestResourcesKey] = trackedResources;
+ }
+
+ trackedResources.Add(resource);
+ }
+
+ /// <summary>
+ /// Disposes of all tracked resources associated with the <paramref name="request"/> which were added via the
+ /// <see cref="RegisterForDispose(HttpRequestMessage, IDisposable)"/> method.
+ /// </summary>
+ /// <param name="request">The request.</param>
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We want to ignore all exceptions.")]
+ public static void DisposeRequestResources(this HttpRequestMessage request)
+ {
+ if (request == null)
+ {
+ throw Error.ArgumentNull("request");
+ }
+
+ List<IDisposable> trackedResources;
+ if (request.Properties.TryGetValue(HttpPropertyKeys.DisposableRequestResourcesKey, out trackedResources))
+ {
+ foreach (IDisposable resource in trackedResources)
+ {
+ try
+ {
+ resource.Dispose();
+ }
+ catch
+ {
+ // ignore exceptions
+ }
+ }
+ trackedResources.Clear();
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/HttpResponseException.cs b/src/System.Web.Http/HttpResponseException.cs
new file mode 100644
index 00000000..60137ba1
--- /dev/null
+++ b/src/System.Web.Http/HttpResponseException.cs
@@ -0,0 +1,104 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Formatting;
+using System.Runtime.Serialization;
+using System.Web.Http.Common;
+using System.Web.Http.Properties;
+
+namespace System.Web.Http
+{
+ /// <summary>
+ /// An exception that allows for a given <see cref="HttpResponseMessage"/>
+ /// to be returned to the client.
+ /// </summary>
+ [SuppressMessage("Microsoft.Usage", "CA2240:Implement ISerializable correctly", Justification = "This type has no additional serializable state")]
+ [SuppressMessage("Microsoft.Design", "CA1032:ImplementStandardExceptionConstructors", Justification = "HttpResponseException is not a real exception and is just an easy way to return HttpResponseMessage")]
+ [Serializable]
+ public class HttpResponseException : Exception
+ {
+ private const string ResponsePropertyName = "Response";
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpResponseException"/> class.
+ /// </summary>
+ public HttpResponseException()
+ : this(HttpStatusCode.InternalServerError)
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpResponseException"/> class.
+ /// </summary>
+ /// <param name="message">The message that describes the error.</param>
+ public HttpResponseException(string message)
+ : this(message, HttpStatusCode.InternalServerError)
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpResponseException"/> class.
+ /// </summary>
+ /// <param name="message">The message that describes the error.</param>
+ /// <param name="statusCode">The status code to use with the <see cref="HttpResponseMessage"/>.</param>
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "disposed later.")]
+ public HttpResponseException(string message, HttpStatusCode statusCode)
+ : base(message)
+ {
+ HttpResponseMessage response = new HttpResponseMessage(statusCode)
+ {
+ Content = new ObjectContent<string>(message, new JsonMediaTypeFormatter())
+ };
+ InitializeResponse(response);
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpResponseException"/> class.
+ /// </summary>
+ /// <param name="statusCode">The status code to use with the <see cref="HttpResponseMessage"/>.</param>
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "disposed later.")]
+ public HttpResponseException(HttpStatusCode statusCode)
+ : this(new HttpResponseMessage(statusCode))
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpResponseException"/> class.
+ /// </summary>
+ /// <param name="response">The response message.</param>
+ public HttpResponseException(HttpResponseMessage response)
+ : base(Error.Format(SRResources.HttpResponseExceptionMessage, ResponsePropertyName))
+ {
+ if (response == null)
+ {
+ throw Error.ArgumentNull("response");
+ }
+
+ InitializeResponse(response);
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpResponseException"/> class.
+ /// </summary>
+ /// <param name="serializationInfo">The <see cref="SerializationInfo"/> that holds the serialized object data about the exception being thrown.</param>
+ /// <param name="streamingContext">The <see cref="StreamingContext"/> that contains contextual information about the source or destination.</param>
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "disposed later.")]
+ protected HttpResponseException(SerializationInfo serializationInfo, StreamingContext streamingContext)
+ : base(serializationInfo, streamingContext)
+ {
+ InitializeResponse(new HttpResponseMessage(HttpStatusCode.InternalServerError));
+ }
+
+ /// <summary>
+ /// Gets the <see cref="HttpResponseMessage"/> to return to the client.
+ /// </summary>
+ public HttpResponseMessage Response { get; private set; }
+
+ private void InitializeResponse(HttpResponseMessage response)
+ {
+ Contract.Assert(response != null, "Response cannot be null!");
+ Response = response;
+ }
+ }
+}
diff --git a/src/System.Web.Http/HttpResponseMessageExtensions.cs b/src/System.Web.Http/HttpResponseMessageExtensions.cs
new file mode 100644
index 00000000..76778608
--- /dev/null
+++ b/src/System.Web.Http/HttpResponseMessageExtensions.cs
@@ -0,0 +1,58 @@
+using System.ComponentModel;
+using System.Net.Http;
+using System.Web.Http.Common;
+
+namespace System.Web.Http
+{
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static class HttpResponseMessageExtensions
+ {
+ public static bool TryGetObjectValue<T>(this HttpResponseMessage response, out T value) where T : class
+ {
+ if (response == null)
+ {
+ throw Error.ArgumentNull("response");
+ }
+
+ ObjectContent content = response.Content as ObjectContent;
+ if (content != null)
+ {
+ value = content.Value as T;
+ return value != null;
+ }
+
+ value = null;
+ return false;
+ }
+
+ public static bool TrySetObjectValue<T>(this HttpResponseMessage response, T value) where T : class
+ {
+ if (response == null)
+ {
+ throw Error.ArgumentNull("response");
+ }
+
+ ObjectContent content = response.Content as ObjectContent;
+ if (content != null)
+ {
+ try
+ {
+ content.Value = value;
+ }
+ catch (ArgumentException)
+ {
+ return false;
+ }
+ catch (InvalidOperationException)
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ value = null;
+ return false;
+ }
+ }
+}
diff --git a/src/System.Web.Http/HttpRouteCollection.cs b/src/System.Web.Http/HttpRouteCollection.cs
new file mode 100644
index 00000000..bb6e817b
--- /dev/null
+++ b/src/System.Web.Http/HttpRouteCollection.cs
@@ -0,0 +1,297 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+using System.Net.Http;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Properties;
+using System.Web.Http.Routing;
+
+namespace System.Web.Http
+{
+ public class HttpRouteCollection : ICollection<IHttpRoute>, IDisposable
+ {
+ // Arbitrary base address for evaluating the root virtual path
+ private static readonly Uri _referenceBaseAddress = new Uri("http://localhost");
+
+ private readonly string _virtualPathRoot;
+ private readonly Collection<IHttpRoute> _collection = new Collection<IHttpRoute>();
+ private readonly IDictionary<string, IHttpRoute> _dictionary = new Dictionary<string, IHttpRoute>(StringComparer.OrdinalIgnoreCase);
+ private bool _disposed;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpRouteCollection"/> class with a <see cref="M:VirtualPathRoot"/>
+ /// value of "/".
+ /// </summary>
+ public HttpRouteCollection()
+ : this("/")
+ {
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "Relative URIs are not URIs")]
+ public HttpRouteCollection(string virtualPathRoot)
+ {
+ if (virtualPathRoot == null)
+ {
+ throw Error.ArgumentNull("virtualPathRoot");
+ }
+
+ // Validate virtual path
+ Uri address = new Uri(_referenceBaseAddress, virtualPathRoot);
+ _virtualPathRoot = address.AbsolutePath;
+ }
+
+ public virtual string VirtualPathRoot
+ {
+ get { return _virtualPathRoot; }
+ }
+
+ public virtual int Count
+ {
+ get { return _collection.Count; }
+ }
+
+ public virtual bool IsReadOnly
+ {
+ get { return false; }
+ }
+
+ public virtual IHttpRoute this[int index]
+ {
+ get { return _collection[index]; }
+ }
+
+ public virtual IHttpRoute this[string name]
+ {
+ get { return _dictionary[name]; }
+ }
+
+ public virtual IHttpRouteData GetRouteData(HttpRequestMessage request)
+ {
+ if (request == null)
+ {
+ throw Error.ArgumentNull("request");
+ }
+
+ foreach (IHttpRoute route in _collection)
+ {
+ IHttpRouteData routeData = route.GetRouteData(_virtualPathRoot, request);
+ if (routeData != null)
+ {
+ return routeData;
+ }
+ }
+
+ return null;
+ }
+
+ public virtual IHttpVirtualPathData GetVirtualPath(HttpControllerContext controllerContext, string name, IDictionary<string, object> values)
+ {
+ if (controllerContext == null)
+ {
+ throw Error.ArgumentNull("controllerContext");
+ }
+
+ if (name == null)
+ {
+ throw Error.ArgumentNull("name");
+ }
+
+ IHttpRoute route;
+ if (!_dictionary.TryGetValue(name, out route))
+ {
+ throw Error.Argument("name", SRResources.RouteCollection_NameNotFound, name);
+ }
+ IHttpVirtualPathData virtualPath = route.GetVirtualPath(controllerContext, values);
+ if (virtualPath == null)
+ {
+ return null;
+ }
+
+ // Construct a new VirtualPathData with the resolved app path
+
+ string virtualPathRoot = _virtualPathRoot;
+ if (!virtualPathRoot.EndsWith("/", StringComparison.Ordinal))
+ {
+ virtualPathRoot += "/";
+ }
+
+ // Note: The virtual path root here always ends with a "/" and the
+ // virtual path never starts with a "/" (that's how routes work).
+ return new HttpVirtualPathData(virtualPath.Route, virtualPathRoot + virtualPath.VirtualPath);
+ }
+
+ public IHttpRoute CreateRoute(string routeTemplate, object defaults, object constraints, IDictionary<string, object> parameters)
+ {
+ IDictionary<string, object> dataTokens = new Dictionary<string, object>();
+
+ return CreateRoute(routeTemplate, GetTypeProperties(defaults), GetTypeProperties(constraints), dataTokens, parameters);
+ }
+
+ public virtual IHttpRoute CreateRoute(string routeTemplate, IDictionary<string, object> defaults, IDictionary<string, object> constraints, IDictionary<string, object> dataTokens, IDictionary<string, object> parameters)
+ {
+ HttpRouteValueDictionary routeDefaults = defaults != null ? new HttpRouteValueDictionary(defaults) : null;
+ HttpRouteValueDictionary routeConstraints = constraints != null ? new HttpRouteValueDictionary(constraints) : null;
+ HttpRouteValueDictionary routeDataTokens = dataTokens != null ? new HttpRouteValueDictionary(dataTokens) : null;
+ return new HttpRoute(routeTemplate, routeDefaults, routeConstraints, routeDataTokens);
+ }
+
+ void ICollection<IHttpRoute>.Add(IHttpRoute route)
+ {
+ throw Error.NotSupported(SRResources.Route_AddRemoveWithNoKeyNotSupported, typeof(HttpRouteCollection).Name);
+ }
+
+ public virtual void Add(string name, IHttpRoute route)
+ {
+ if (name == null)
+ {
+ throw Error.ArgumentNull("name");
+ }
+
+ if (route == null)
+ {
+ throw Error.ArgumentNull("route");
+ }
+
+ _dictionary.Add(name, route);
+ _collection.Add(route);
+ }
+
+ public virtual void Clear()
+ {
+ _dictionary.Clear();
+ _collection.Clear();
+ }
+
+ public virtual bool Contains(IHttpRoute item)
+ {
+ if (item == null)
+ {
+ throw Error.ArgumentNull("item");
+ }
+
+ return _collection.Contains(item);
+ }
+
+ public virtual bool ContainsKey(string name)
+ {
+ if (name == null)
+ {
+ throw Error.ArgumentNull("name");
+ }
+
+ return _dictionary.ContainsKey(name);
+ }
+
+ public virtual void CopyTo(IHttpRoute[] array, int arrayIndex)
+ {
+ _collection.CopyTo(array, arrayIndex);
+ }
+
+ public virtual void CopyTo(KeyValuePair<string, IHttpRoute>[] array, int arrayIndex)
+ {
+ _dictionary.CopyTo(array, arrayIndex);
+ }
+
+ public virtual void Insert(int index, string name, IHttpRoute value)
+ {
+ if (name == null)
+ {
+ throw Error.ArgumentNull("name");
+ }
+
+ if (value == null)
+ {
+ throw Error.ArgumentNull("value");
+ }
+
+ // Check that index is valid
+ if (_collection[index] != null)
+ {
+ _dictionary.Add(name, value);
+ _collection.Insert(index, value);
+ }
+ }
+
+ bool ICollection<IHttpRoute>.Remove(IHttpRoute route)
+ {
+ throw Error.NotSupported(SRResources.Route_AddRemoveWithNoKeyNotSupported, typeof(HttpRouteCollection).Name);
+ }
+
+ public virtual bool Remove(string name)
+ {
+ if (name == null)
+ {
+ throw Error.ArgumentNull("name");
+ }
+
+ IHttpRoute value;
+ if (_dictionary.TryGetValue(name, out value))
+ {
+ bool dictionaryRemove = _dictionary.Remove(name);
+ bool collectionRemove = _collection.Remove(value);
+ Contract.Assert(dictionaryRemove == collectionRemove);
+ return dictionaryRemove;
+ }
+
+ return false;
+ }
+
+ public virtual IEnumerator<IHttpRoute> GetEnumerator()
+ {
+ return _collection.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return OnGetEnumerator();
+ }
+
+ protected virtual IEnumerator OnGetEnumerator()
+ {
+ return _collection.GetEnumerator();
+ }
+
+ public virtual bool TryGetValue(string name, out IHttpRoute route)
+ {
+ return _dictionary.TryGetValue(name, out route);
+ }
+
+ internal static IDictionary<string, object> GetTypeProperties(object instance)
+ {
+ Dictionary<string, object> result = new Dictionary<string, object>();
+ if (instance != null)
+ {
+ PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(instance);
+ foreach (PropertyDescriptor prop in properties)
+ {
+ object val = prop.GetValue(instance);
+ result.Add(prop.Name, val);
+ }
+ }
+
+ return result;
+ }
+
+ #region IDisposable
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_disposed)
+ {
+ _disposed = true;
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/System.Web.Http/HttpRouteCollectionExtensions.cs b/src/System.Web.Http/HttpRouteCollectionExtensions.cs
new file mode 100644
index 00000000..20440b86
--- /dev/null
+++ b/src/System.Web.Http/HttpRouteCollectionExtensions.cs
@@ -0,0 +1,59 @@
+using System.ComponentModel;
+using System.Web.Http.Common;
+using System.Web.Http.Routing;
+
+namespace System.Web.Http
+{
+ /// <summary>
+ /// Extension methods for <see cref="HttpRouteCollection"/>
+ /// </summary>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static class HttpRouteCollectionExtensions
+ {
+ /// <summary>
+ /// Maps the specified route template.
+ /// </summary>
+ /// <param name="routes">A collection of routes for the application.</param>
+ /// <param name="name">The name of the route to map.</param>
+ /// <param name="routeTemplate">The route template for the route.</param>
+ /// <returns>A reference to the mapped route.</returns>
+ public static IHttpRoute MapHttpRoute(this HttpRouteCollection routes, string name, string routeTemplate)
+ {
+ return MapHttpRoute(routes, name, routeTemplate, defaults: null, constraints: null);
+ }
+
+ /// <summary>
+ /// Maps the specified route template and sets default constraints.
+ /// </summary>
+ /// <param name="routes">A collection of routes for the application.</param>
+ /// <param name="name">The name of the route to map.</param>
+ /// <param name="routeTemplate">The route template for the route.</param>
+ /// <param name="defaults">An object that contains default route values.</param>
+ /// <returns>A reference to the mapped route.</returns>
+ public static IHttpRoute MapHttpRoute(this HttpRouteCollection routes, string name, string routeTemplate, object defaults)
+ {
+ return MapHttpRoute(routes, name, routeTemplate, defaults, constraints: null);
+ }
+
+ /// <summary>
+ /// Maps the specified route template and sets default route values and constraints.
+ /// </summary>
+ /// <param name="routes">A collection of routes for the application.</param>
+ /// <param name="name">The name of the route to map.</param>
+ /// <param name="routeTemplate">The route template for the route.</param>
+ /// <param name="defaults">An object that contains default route values.</param>
+ /// <param name="constraints">A set of expressions that specify values for <paramref name="routeTemplate"/>.</param>
+ /// <returns>A reference to the mapped route.</returns>
+ public static IHttpRoute MapHttpRoute(this HttpRouteCollection routes, string name, string routeTemplate, object defaults, object constraints)
+ {
+ if (routes == null)
+ {
+ throw Error.ArgumentNull("routes");
+ }
+
+ IHttpRoute route = routes.CreateRoute(routeTemplate, defaults, constraints, parameters: null);
+ routes.Add(name, route);
+ return route;
+ }
+ }
+}
diff --git a/src/System.Web.Http/HttpServer.cs b/src/System.Web.Http/HttpServer.cs
new file mode 100644
index 00000000..c9ddf099
--- /dev/null
+++ b/src/System.Web.Http/HttpServer.cs
@@ -0,0 +1,190 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+using System.Net;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Common;
+using System.Web.Http.Dispatcher;
+using System.Web.Http.Hosting;
+using System.Web.Http.Tracing;
+
+namespace System.Web.Http
+{
+ /// <summary>
+ /// Defines an implementation of an <see cref="HttpMessageHandler"/> which dispatches an
+ /// incoming <see cref="HttpRequestMessage"/> and creates an <see cref="HttpResponseMessage"/> as a result.
+ /// </summary>
+ public class HttpServer : DelegatingHandler
+ {
+ private readonly HttpConfiguration _configuration;
+ private readonly HttpMessageHandler _dispatcher;
+ private bool _disposed;
+ private bool _initialized;
+ private object _initializationLock = new object();
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpServer"/> class with default configuration and dispatcher.
+ /// </summary>
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope",
+ Justification = "The configuration object is disposed as part of this class.")]
+ public HttpServer()
+ : this(new HttpConfiguration())
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpServer"/> class with default dispatcher.
+ /// </summary>
+ /// <param name="configuration">The <see cref="HttpConfiguration"/> used to configure this <see cref="HttpServer"/> instance.</param>
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope",
+ Justification = "The configuration object is disposed as part of this class.")]
+ public HttpServer(HttpConfiguration configuration)
+ : this(configuration, new HttpControllerDispatcher(configuration))
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpServer"/> class with a custom dispatcher.
+ /// </summary>
+ /// <param name="dispatcher">Http dispatcher responsible for handling incoming requests.</param>
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope",
+ Justification = "The configuration object is disposed as part of this class.")]
+ public HttpServer(HttpControllerDispatcher dispatcher)
+ : this(new HttpConfiguration(), dispatcher)
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpServer"/> class.
+ /// </summary>
+ /// <param name="configuration">The <see cref="HttpConfiguration"/> used to configure this <see cref="HttpServer"/> instance.</param>
+ /// <param name="dispatcher">Http dispatcher responsible for handling incoming requests.</param>
+ public HttpServer(HttpConfiguration configuration, HttpMessageHandler dispatcher)
+ {
+ if (configuration == null)
+ {
+ throw Error.ArgumentNull("configuration");
+ }
+
+ if (dispatcher == null)
+ {
+ throw Error.ArgumentNull("dispatcher");
+ }
+
+ _dispatcher = dispatcher;
+ _configuration = configuration;
+ }
+
+ /// <summary>
+ /// Gets the dispatcher.
+ /// </summary>
+ public HttpMessageHandler Dispatcher
+ {
+ get { return _dispatcher; }
+ }
+
+ /// <summary>
+ /// Gets the <see cref="HttpConfiguration"/>.
+ /// </summary>
+ public HttpConfiguration Configuration
+ {
+ get { return _configuration; }
+ }
+
+ /// <summary>
+ /// Submits the request asynchronously for dispatching.
+ /// </summary>
+ /// <param name="request"><see cref="HttpRequestMessage"/> to submit</param>
+ /// <param name="cancellationToken">Token used to cancel operation.</param>
+ /// <returns>A <see cref="Task{T}"/> representing the operation.</returns>
+ public Task<HttpResponseMessage> SubmitRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ // TODO: DevDiv 315316: Remove once we have HttpMessageInvoker from NCL
+ return SendAsync(request, cancellationToken);
+ }
+
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources
+ /// </summary>
+ /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged SRResources.</param>
+ protected override void Dispose(bool disposing)
+ {
+ if (!_disposed)
+ {
+ _disposed = true;
+ if (disposing)
+ {
+ _configuration.Dispose();
+ }
+ }
+
+ base.Dispose(disposing);
+ }
+
+ /// <summary>
+ /// Dispatches an incoming <see cref="HttpRequestMessage"/>.
+ /// </summary>
+ /// <param name="request">The request to dispatch</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>A <see cref="Task{HttpResponseMessage}"/> representing the ongoing operation.</returns>
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Caller becomes owner.")]
+ protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ if (request == null)
+ {
+ throw Error.ArgumentNull("request");
+ }
+
+ if (_disposed)
+ {
+ return TaskHelpers.FromResult(request.CreateResponse(HttpStatusCode.ServiceUnavailable));
+ }
+
+ // The first request initializes the server
+ Initialize();
+
+ // Capture current synchronization context and add it as a parameter to the request
+ SynchronizationContext context = SynchronizationContext.Current;
+ if (context != null)
+ {
+ request.Properties.Add(HttpPropertyKeys.SynchronizationContextKey, context);
+ }
+
+ // Add HttpConfiguration object as a parameter to the request
+ request.Properties.Add(HttpPropertyKeys.HttpConfigurationKey, _configuration);
+
+ return base.SendAsync(request, cancellationToken);
+ }
+
+ /// <summary>
+ /// Prepares the server for operation.
+ /// </summary>
+ /// <remarks>
+ /// This method must be called after all configuration is complete
+ /// but before the first request is processed. It is idempotent and
+ /// will immediately return if initialization has been done before.
+ /// </remarks>
+ protected void Initialize()
+ {
+ if (!_initialized && !_disposed)
+ {
+ lock (_initializationLock)
+ {
+ if (!_initialized)
+ {
+ _initialized = true;
+
+ // Attach tracing before creating pipeline to allow injection of message handlers
+ ITraceManager traceManager = _configuration.ServiceResolver.GetTraceManager();
+ Contract.Assert(traceManager != null);
+ traceManager.Initialize(_configuration);
+
+ // Create pipeline
+ InnerHandler = HttpPipelineFactory.Create(_configuration.MessageHandlers, _dispatcher);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/IncludeErrorDetailPolicy.cs b/src/System.Web.Http/IncludeErrorDetailPolicy.cs
new file mode 100644
index 00000000..a6e725d5
--- /dev/null
+++ b/src/System.Web.Http/IncludeErrorDetailPolicy.cs
@@ -0,0 +1,23 @@
+namespace System.Web.Http
+{
+ /// <summary>
+ /// Enum to indicate whether error details, such as exception messages and stack traces, should be included in error messages.
+ /// </summary>
+ public enum IncludeErrorDetailPolicy
+ {
+ /// <summary>
+ /// Only include error details when responding to a local request.
+ /// </summary>
+ LocalOnly = 0,
+
+ /// <summary>
+ /// Always include error details.
+ /// </summary>
+ Always,
+
+ /// <summary>
+ /// Never include error details.
+ /// </summary>
+ Never
+ }
+}
diff --git a/src/System.Web.Http/Internal/CollectionModelBinderUtil.cs b/src/System.Web.Http/Internal/CollectionModelBinderUtil.cs
new file mode 100644
index 00000000..403667b6
--- /dev/null
+++ b/src/System.Web.Http/Internal/CollectionModelBinderUtil.cs
@@ -0,0 +1,145 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Web.Http.Metadata;
+using System.Web.Http.ModelBinding;
+using System.Web.Http.ValueProviders;
+
+namespace System.Web.Http.Internal
+{
+ internal static class CollectionModelBinderUtil
+ {
+ internal static void CreateOrReplaceCollection<TElement>(ModelBindingContext bindingContext, IEnumerable<TElement> incomingElements, Func<ICollection<TElement>> creator)
+ {
+ ICollection<TElement> collection = bindingContext.Model as ICollection<TElement>;
+ if (collection == null || collection.IsReadOnly)
+ {
+ collection = creator();
+ bindingContext.Model = collection;
+ }
+
+ collection.Clear();
+ foreach (TElement element in incomingElements)
+ {
+ collection.Add(element);
+ }
+ }
+
+ internal static void CreateOrReplaceDictionary<TKey, TValue>(ModelBindingContext bindingContext, IEnumerable<KeyValuePair<TKey, TValue>> incomingElements, Func<IDictionary<TKey, TValue>> creator)
+ {
+ IDictionary<TKey, TValue> dictionary = bindingContext.Model as IDictionary<TKey, TValue>;
+ if (dictionary == null || dictionary.IsReadOnly)
+ {
+ dictionary = creator();
+ bindingContext.Model = dictionary;
+ }
+
+ dictionary.Clear();
+ foreach (var element in incomingElements)
+ {
+ if (element.Key != null)
+ {
+ dictionary[element.Key] = element.Value;
+ }
+ }
+ }
+
+ // supportedInterfaceType: type that is updatable by this binder
+ // newInstanceType: type that will be created by the binder if necessary
+ // openBinderType: model binder type
+ // modelMetadata: metadata for the model to bind
+ //
+ // example: GetGenericBinder(typeof(IList<>), typeof(List<>), typeof(ListBinder<>), ...) means that the ListBinder<T>
+ // type can update models that implement IList<T>, and if for some reason the existing model instance is not
+ // updatable the binder will create a List<T> object and bind to that instead. This method will return a ListBinder<T>
+ // or null, depending on whether the type and updatability checks succeed.
+ internal static IModelBinder GetGenericBinder(Type supportedInterfaceType, Type newInstanceType, Type openBinderType, ModelMetadata modelMetadata)
+ {
+ Type[] typeArguments = GetTypeArgumentsForUpdatableGenericCollection(supportedInterfaceType, newInstanceType, modelMetadata);
+ return (typeArguments != null) ? (IModelBinder)Activator.CreateInstance(openBinderType.MakeGenericType(typeArguments)) : null;
+ }
+
+ [SuppressMessage("Microsoft.Globalization", "CA1304:SpecifyCultureInfo", MessageId = "System.Web.Http.ValueProviders.ValueProviderResult.ConvertTo(System.Type)", Justification = "The ValueProviderResult already has the necessary context to perform a culture-aware conversion.")]
+ internal static IEnumerable<string> GetIndexNamesFromValueProviderResult(ValueProviderResult valueProviderResultIndex)
+ {
+ IEnumerable<string> indexNames = null;
+ if (valueProviderResultIndex != null)
+ {
+ string[] indexes = (string[])valueProviderResultIndex.ConvertTo(typeof(string[]));
+ if (indexes != null && indexes.Length > 0)
+ {
+ indexNames = indexes;
+ }
+ }
+ return indexNames;
+ }
+
+ internal static IEnumerable<string> GetZeroBasedIndexes()
+ {
+ int i = 0;
+ while (true)
+ {
+ yield return i.ToString(CultureInfo.InvariantCulture);
+ i++;
+ }
+ }
+
+ // Returns the generic type arguments for the model type if updatable, else null.
+ // supportedInterfaceType: open type (like IList<>) of supported interface, must implement ICollection<>
+ // newInstanceType: open type (like List<>) of object that will be created, must implement supportedInterfaceType
+ internal static Type[] GetTypeArgumentsForUpdatableGenericCollection(Type supportedInterfaceType, Type newInstanceType, ModelMetadata modelMetadata)
+ {
+ /*
+ * Check that we can extract proper type arguments from the model.
+ */
+
+ if (!modelMetadata.ModelType.IsGenericType || modelMetadata.ModelType.IsGenericTypeDefinition)
+ {
+ // not a closed generic type
+ return null;
+ }
+
+ Type[] modelTypeArguments = modelMetadata.ModelType.GetGenericArguments();
+ if (modelTypeArguments.Length != supportedInterfaceType.GetGenericArguments().Length)
+ {
+ // wrong number of generic type arguments
+ return null;
+ }
+
+ /*
+ * Is it possible just to change the reference rather than update the collection in-place?
+ */
+
+ if (!modelMetadata.IsReadOnly)
+ {
+ Type closedNewInstanceType = newInstanceType.MakeGenericType(modelTypeArguments);
+ if (modelMetadata.ModelType.IsAssignableFrom(closedNewInstanceType))
+ {
+ return modelTypeArguments;
+ }
+ }
+
+ /*
+ * At this point, we know we can't change the reference, so we need to verify that
+ * the model instance can be updated in-place.
+ */
+
+ Type closedSupportedInterfaceType = supportedInterfaceType.MakeGenericType(modelTypeArguments);
+ if (!closedSupportedInterfaceType.IsInstanceOfType(modelMetadata.Model))
+ {
+ return null; // not instance of correct interface
+ }
+
+ Type closedCollectionType = TypeHelper.ExtractGenericInterface(closedSupportedInterfaceType, typeof(ICollection<>));
+ bool collectionInstanceIsReadOnly = (bool)closedCollectionType.GetProperty("IsReadOnly").GetValue(modelMetadata.Model, null);
+ if (collectionInstanceIsReadOnly)
+ {
+ return null;
+ }
+ else
+ {
+ return modelTypeArguments;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/Internal/DataTypeUtil.cs b/src/System.Web.Http/Internal/DataTypeUtil.cs
new file mode 100644
index 00000000..264ca435
--- /dev/null
+++ b/src/System.Web.Http/Internal/DataTypeUtil.cs
@@ -0,0 +1,72 @@
+using System.ComponentModel.DataAnnotations;
+using System.Security;
+
+namespace System.Web.Http.Internal
+{
+ // [SecuritySafeCritical] because it uses DataAnnotations types in static fields and in static method
+ [SecuritySafeCritical]
+ internal static class DataTypeUtil
+ {
+ internal static readonly string CurrencyTypeName = DataType.Currency.ToString();
+ internal static readonly string DateTypeName = DataType.Date.ToString();
+ internal static readonly string DateTimeTypeName = DataType.DateTime.ToString();
+ internal static readonly string DurationTypeName = DataType.Duration.ToString();
+ internal static readonly string EmailAddressTypeName = DataType.EmailAddress.ToString();
+ internal static readonly string HtmlTypeName = DataType.Html.ToString();
+ internal static readonly string ImageUrlTypeName = DataType.ImageUrl.ToString();
+ internal static readonly string MultiLineTextTypeName = DataType.MultilineText.ToString();
+ internal static readonly string PasswordTypeName = DataType.Password.ToString();
+ internal static readonly string PhoneNumberTypeName = DataType.PhoneNumber.ToString();
+ internal static readonly string TextTypeName = DataType.Text.ToString();
+ internal static readonly string TimeTypeName = DataType.Time.ToString();
+ internal static readonly string UrlTypeName = DataType.Url.ToString();
+
+ // This is a faster version of GetDataTypeName(). It internally calls ToString() on the enum
+ // value, which can be quite slow because of value verification.
+ internal static string ToDataTypeName(this DataTypeAttribute attribute, Func<DataTypeAttribute, Boolean> isDataType = null)
+ {
+ if (isDataType == null)
+ {
+ isDataType = t => t.GetType().Equals(typeof(DataTypeAttribute));
+ }
+
+ // GetDataTypeName is virtual, so this is only safe if they haven't derived from DataTypeAttribute.
+ // However, if they derive from DataTypeAttribute, they can help their own perf by overriding GetDataTypeName
+ // and returning an appropriate string without invoking the ToString() on the enum.
+ if (isDataType(attribute))
+ {
+ switch (attribute.DataType)
+ {
+ case DataType.Currency:
+ return CurrencyTypeName;
+ case DataType.Date:
+ return DateTypeName;
+ case DataType.DateTime:
+ return DateTimeTypeName;
+ case DataType.Duration:
+ return DurationTypeName;
+ case DataType.EmailAddress:
+ return EmailAddressTypeName;
+ case DataType.Html:
+ return HtmlTypeName;
+ case DataType.ImageUrl:
+ return ImageUrlTypeName;
+ case DataType.MultilineText:
+ return MultiLineTextTypeName;
+ case DataType.Password:
+ return PasswordTypeName;
+ case DataType.PhoneNumber:
+ return PhoneNumberTypeName;
+ case DataType.Text:
+ return TextTypeName;
+ case DataType.Time:
+ return TimeTypeName;
+ case DataType.Url:
+ return UrlTypeName;
+ }
+ }
+
+ return attribute.GetDataTypeName();
+ }
+ }
+}
diff --git a/src/System.Web.Http/Internal/HttpActionContextExtensions.cs b/src/System.Web.Http/Internal/HttpActionContextExtensions.cs
new file mode 100644
index 00000000..93a35140
--- /dev/null
+++ b/src/System.Web.Http/Internal/HttpActionContextExtensions.cs
@@ -0,0 +1,39 @@
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Metadata;
+using System.Web.Http.ModelBinding;
+
+namespace System.Web.Http.Internal
+{
+ internal static class HttpActionContextExtensions
+ {
+ public static bool TryBindStrongModel<TModel>(this HttpActionContext actionContext, ModelBindingContext parentBindingContext, string propertyName, ModelMetadataProvider metadataProvider, out TModel model)
+ {
+ if (actionContext == null)
+ {
+ throw Error.ArgumentNull("actionContext");
+ }
+
+ ModelBindingContext propertyBindingContext = new ModelBindingContext(parentBindingContext)
+ {
+ ModelMetadata = metadataProvider.GetMetadataForType(null, typeof(TModel)),
+ ModelName = ModelBindingHelper.CreatePropertyModelName(parentBindingContext.ModelName, propertyName)
+ };
+
+ IModelBinder binder;
+ if (actionContext.TryGetBinder(propertyBindingContext, out binder))
+ {
+ if (binder.BindModel(actionContext, propertyBindingContext))
+ {
+ object untypedModel = propertyBindingContext.Model;
+ model = ModelBindingHelper.CastOrDefault<TModel>(untypedModel);
+ parentBindingContext.ValidationNode.ChildNodes.Add(propertyBindingContext.ValidationNode);
+ return true;
+ }
+ }
+
+ model = default(TModel);
+ return false;
+ }
+ }
+}
diff --git a/src/System.Web.Http/Internal/MemberInfoExtensions.cs b/src/System.Web.Http/Internal/MemberInfoExtensions.cs
new file mode 100644
index 00000000..6f874bf7
--- /dev/null
+++ b/src/System.Web.Http/Internal/MemberInfoExtensions.cs
@@ -0,0 +1,18 @@
+using System.Reflection;
+using System.Web.Http.Common;
+
+namespace System.Web.Http.Internal
+{
+ internal static class MemberInfoExtensions
+ {
+ public static TAttribute[] GetCustomAttributes<TAttribute>(this MemberInfo member, bool inherit) where TAttribute : class
+ {
+ if (member == null)
+ {
+ throw Error.ArgumentNull("member");
+ }
+
+ return (TAttribute[])member.GetCustomAttributes(typeof(TAttribute), inherit);
+ }
+ }
+}
diff --git a/src/System.Web.Http/Internal/ParameterDescriptorExtensionMethods.cs b/src/System.Web.Http/Internal/ParameterDescriptorExtensionMethods.cs
new file mode 100644
index 00000000..3d3455c9
--- /dev/null
+++ b/src/System.Web.Http/Internal/ParameterDescriptorExtensionMethods.cs
@@ -0,0 +1,16 @@
+using System.Diagnostics.Contracts;
+using System.Web.Http.Controllers;
+using System.Web.Http.ValueProviders;
+
+namespace System.Web.Http.Internal
+{
+ internal static class ParameterDescriptorExtensionMethods
+ {
+ public static bool IsStructuredBodyParameter(this HttpParameterDescriptor parameterDescriptor)
+ {
+ Contract.Assert(parameterDescriptor != null, "parameterDescriptor cannot be null.");
+
+ return TypeHelper.IsStructuredBodyContentType(parameterDescriptor.ParameterType);
+ }
+ }
+}
diff --git a/src/System.Web.Http/Internal/ParameterInfoExtensions.cs b/src/System.Web.Http/Internal/ParameterInfoExtensions.cs
new file mode 100644
index 00000000..baeea9b7
--- /dev/null
+++ b/src/System.Web.Http/Internal/ParameterInfoExtensions.cs
@@ -0,0 +1,49 @@
+using System.ComponentModel;
+using System.Reflection;
+using System.Web.Http.Common;
+
+namespace System.Web.Http.Internal
+{
+ internal static class ParameterInfoExtensions
+ {
+ public static TAttribute[] GetCustomAttributes<TAttribute>(this ParameterInfo parameterInfo, bool inherit) where TAttribute : class
+ {
+ if (parameterInfo == null)
+ {
+ throw Error.ArgumentNull("parameterInfo");
+ }
+
+ return (TAttribute[])parameterInfo.GetCustomAttributes(typeof(TAttribute), inherit);
+ }
+
+ public static bool TryGetDefaultValue(this ParameterInfo parameterInfo, out object value)
+ {
+ if (parameterInfo == null)
+ {
+ throw Error.ArgumentNull("parameterInfo");
+ }
+
+ // this will get the default value as seen by the VB / C# compilers
+ // if no value was baked in, RawDefaultValue returns DBNull.Value
+ object defaultValue = parameterInfo.DefaultValue;
+ if (defaultValue != DBNull.Value)
+ {
+ value = defaultValue;
+ return true;
+ }
+
+ // if the compiler did not bake in a default value, check the [DefaultValue] attribute
+ DefaultValueAttribute[] attrs = (DefaultValueAttribute[])parameterInfo.GetCustomAttributes(typeof(DefaultValueAttribute), false);
+ if (attrs == null || attrs.Length == 0)
+ {
+ value = default(object);
+ return false;
+ }
+ else
+ {
+ value = attrs[0].Value;
+ return true;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/Internal/TypeActivator.cs b/src/System.Web.Http/Internal/TypeActivator.cs
new file mode 100644
index 00000000..40a11fde
--- /dev/null
+++ b/src/System.Web.Http/Internal/TypeActivator.cs
@@ -0,0 +1,26 @@
+using System.Diagnostics.Contracts;
+using System.Linq.Expressions;
+
+namespace System.Web.Http.Internal
+{
+ internal static class TypeActivator
+ {
+ public static Func<TBase> Create<TBase>(Type instanceType) where TBase : class
+ {
+ Contract.Assert(instanceType != null);
+ NewExpression newInstanceExpression = Expression.New(instanceType);
+ return Expression.Lambda<Func<TBase>>(newInstanceExpression).Compile();
+ }
+
+ public static Func<TInstance> Create<TInstance>() where TInstance : class
+ {
+ return Create<TInstance>(typeof(TInstance));
+ }
+
+ public static Func<object> Create(Type instanceType)
+ {
+ Contract.Assert(instanceType != null);
+ return Create<object>(instanceType);
+ }
+ }
+}
diff --git a/src/System.Web.Http/Internal/TypeDescriptorHelper.cs b/src/System.Web.Http/Internal/TypeDescriptorHelper.cs
new file mode 100644
index 00000000..4526f4fd
--- /dev/null
+++ b/src/System.Web.Http/Internal/TypeDescriptorHelper.cs
@@ -0,0 +1,17 @@
+using System.ComponentModel;
+
+namespace System.Web.Http.Internal
+{
+ // REVIEW: Rename to match XxxUtil pattern
+ // REVIEW: Ensure that using this directly still indirectly uses any user-registered descriptor provider
+ internal static class TypeDescriptorHelper
+ {
+ internal static ICustomTypeDescriptor Get(Type type)
+ {
+ //// REVIEW: this will cause a security exception
+ ////return new AssociatedMetadataTypeTypeDescriptionProvider(type).GetTypeDescriptor(type);
+
+ return TypeDescriptor.GetProvider(type).GetTypeDescriptor(type);
+ }
+ }
+}
diff --git a/src/System.Web.Http/Internal/TypeHelper.cs b/src/System.Web.Http/Internal/TypeHelper.cs
new file mode 100644
index 00000000..ffb6ff46
--- /dev/null
+++ b/src/System.Web.Http/Internal/TypeHelper.cs
@@ -0,0 +1,366 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Diagnostics.Contracts;
+using System.Json;
+using System.Linq;
+using System.Net.Http;
+using System.Threading.Tasks;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Properties;
+
+namespace System.Web.Http.Internal
+{
+ /// <summary>
+ /// A static class that provides various <see cref="Type"/> related helpers.
+ /// </summary>
+ internal static class TypeHelper
+ {
+ private static readonly Type HttpContentType = typeof(HttpContent);
+ private static readonly Type HttpRequestMessageType = typeof(HttpRequestMessage);
+ private static readonly Type HttpResponseMessageType = typeof(HttpResponseMessage);
+ private static readonly Type ObjectContentGenericType = typeof(ObjectContent<>);
+ private static readonly Type TaskGenericType = typeof(Task<>);
+
+ internal static readonly Type HttpControllerType = typeof(IHttpController);
+ internal static readonly Type ResponseMessageConverterType = typeof(Func<object, HttpControllerContext, HttpResponseMessage>);
+ internal static readonly Type ApiControllerType = typeof(ApiController);
+
+ internal static bool IsHttpContent(Type type)
+ {
+ Contract.Assert(type != null);
+ return HttpContentType.IsAssignableFrom(type);
+ }
+
+ internal static bool IsHttpResponse(Type type)
+ {
+ Contract.Assert(type != null);
+ return HttpResponseMessageType.IsAssignableFrom(type);
+ }
+
+ internal static bool IsHttpRequest(Type type)
+ {
+ Contract.Assert(type != null);
+ return HttpRequestMessageType.IsAssignableFrom(type);
+ }
+
+ internal static bool IsHttpResponseOrContent(Type type)
+ {
+ Contract.Assert(type != null);
+ return HttpContentType.IsAssignableFrom(type) ||
+ HttpResponseMessageType.IsAssignableFrom(type);
+ }
+
+ internal static bool IsHttpRequestOrContent(Type type)
+ {
+ Contract.Assert(type != null);
+ return HttpContentType.IsAssignableFrom(type) ||
+ HttpRequestMessageType.IsAssignableFrom(type);
+ }
+
+ internal static bool IsHttp(Type type)
+ {
+ Contract.Assert(type != null);
+ return HttpContentType.IsAssignableFrom(type) ||
+ HttpRequestMessageType.IsAssignableFrom(type) ||
+ HttpResponseMessageType.IsAssignableFrom(type);
+ }
+
+ internal static bool IsHttpContentGenericTypeDefinition(Type type)
+ {
+ Contract.Assert(type != null);
+ if (type.IsGenericTypeDefinition && ObjectContentGenericType.IsAssignableFrom(type))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ internal static bool IsHttpRequestOrContentGenericTypeDefinition(Type type)
+ {
+ Contract.Assert(type != null);
+ if (type.IsGenericTypeDefinition)
+ {
+ if (ObjectContentGenericType.IsAssignableFrom(type))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ internal static bool IsHttpResponseOrContentGenericTypeDefinition(Type type)
+ {
+ Contract.Assert(type != null);
+ if (type.IsGenericTypeDefinition)
+ {
+ if (ObjectContentGenericType.IsAssignableFrom(type))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ internal static bool IsHttpGenericTypeDefinition(Type type)
+ {
+ Contract.Assert(type != null);
+ if (type.IsGenericTypeDefinition)
+ {
+ if (ObjectContentGenericType.IsAssignableFrom(type))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ internal static Type GetHttpContentInnerTypeOrNull(Type type)
+ {
+ Contract.Assert(type != null);
+ if (type.IsGenericType && !type.IsGenericTypeDefinition)
+ {
+ Type genericTypeDefinition = type.GetGenericTypeDefinition();
+ if (IsHttpContentGenericTypeDefinition(genericTypeDefinition))
+ {
+ Type[] typeArgs = type.GetGenericArguments();
+ if (typeArgs.Length > 1)
+ {
+ throw Error.InvalidOperation(SRResources.MultipleTypeParametersForHttpContentType, type.Name);
+ }
+
+ return typeArgs[0];
+ }
+ }
+
+ return null;
+ }
+
+ internal static Type GetHttpRequestOrContentInnerTypeOrNull(Type type)
+ {
+ Contract.Assert(type != null);
+ if (type.IsGenericType && !type.IsGenericTypeDefinition)
+ {
+ Type genericTypeDefinition = type.GetGenericTypeDefinition();
+ if (IsHttpRequestOrContentGenericTypeDefinition(genericTypeDefinition))
+ {
+ Type[] typeArgs = type.GetGenericArguments();
+ if (typeArgs.Length > 1)
+ {
+ throw Error.InvalidOperation(SRResources.MultipleTypeParametersForHttpContentType, type.Name);
+ }
+
+ return typeArgs[0];
+ }
+ }
+
+ return null;
+ }
+
+ internal static Type GetHttpResponseOrContentInnerTypeOrNull(Type type)
+ {
+ Contract.Assert(type != null);
+ if (type.IsGenericType && !type.IsGenericTypeDefinition)
+ {
+ Type genericTypeDefinition = type.GetGenericTypeDefinition();
+ if (IsHttpResponseOrContentGenericTypeDefinition(genericTypeDefinition))
+ {
+ Type[] typeArgs = type.GetGenericArguments();
+ if (typeArgs.Length > 1)
+ {
+ throw Error.InvalidOperation(SRResources.MultipleTypeParametersForHttpContentType, type.Name);
+ }
+
+ return typeArgs[0];
+ }
+ }
+
+ return null;
+ }
+
+ internal static Type GetTaskInnerTypeOrNull(Type type)
+ {
+ Contract.Assert(type != null);
+ if (type.IsGenericType && !type.IsGenericTypeDefinition)
+ {
+ Type genericTypeDefinition = type.GetGenericTypeDefinition();
+ // REVIEW: should we consider subclasses of Task<> ??
+ if (TaskGenericType == genericTypeDefinition)
+ {
+ return type.GetGenericArguments()[0];
+ }
+ }
+
+ return null;
+ }
+
+ internal static Type GetUnderlyingContentInnerType(Type type)
+ {
+ Contract.Assert(type != null);
+
+ Type httpResponseMessageType = TypeHelper.GetTaskInnerTypeOrNull(type) ?? type;
+ Type contentType = TypeHelper.GetHttpResponseOrContentInnerTypeOrNull(httpResponseMessageType) ?? httpResponseMessageType;
+ return contentType;
+ }
+
+ internal static Type MakeObjectContentOf(Type type)
+ {
+ Contract.Assert(type != null);
+ Type[] typeParams = new Type[] { type };
+ return TypeHelper.ObjectContentGenericType.MakeGenericType(typeParams);
+ }
+
+ internal static Type ExtractGenericInterface(Type queryType, Type interfaceType)
+ {
+ Func<Type, bool> matchesInterface = t => t.IsGenericType && t.GetGenericTypeDefinition() == interfaceType;
+ return matchesInterface(queryType) ? queryType : queryType.GetInterfaces().FirstOrDefault(matchesInterface);
+ }
+
+ internal static Type[] GetTypeArgumentsIfMatch(Type closedType, Type matchingOpenType)
+ {
+ if (!closedType.IsGenericType)
+ {
+ return null;
+ }
+
+ Type openType = closedType.GetGenericTypeDefinition();
+ return (matchingOpenType == openType) ? closedType.GetGenericArguments() : null;
+ }
+
+ internal static bool IsCompatibleObject(Type type, object value)
+ {
+ return (value == null && TypeAllowsNullValue(type)) || type.IsInstanceOfType(value);
+ }
+
+ internal static bool IsNullableValueType(Type type)
+ {
+ return Nullable.GetUnderlyingType(type) != null;
+ }
+
+ internal static bool TypeAllowsNullValue(Type type)
+ {
+ return !type.IsValueType || IsNullableValueType(type);
+ }
+
+ internal static bool IsSimpleType(Type type)
+ {
+ return type.IsPrimitive ||
+ type.Equals(typeof(string)) ||
+ type.Equals(typeof(DateTime)) ||
+ type.Equals(typeof(Decimal)) ||
+ type.Equals(typeof(Guid)) ||
+ type.Equals(typeof(DateTimeOffset)) ||
+ type.Equals(typeof(TimeSpan));
+ }
+
+ internal static bool IsSimpleUnderlyingType(Type type)
+ {
+ Type underlyingType = Nullable.GetUnderlyingType(type);
+ if (underlyingType != null)
+ {
+ type = underlyingType;
+ }
+
+ return TypeHelper.IsSimpleType(type);
+ }
+
+ internal static bool HasStringConverter(Type type)
+ {
+ return TypeDescriptor.GetConverter(type).CanConvertFrom(typeof(string));
+ }
+
+ internal static IEnumerable GetAsEnumerable(object o)
+ {
+ // string implements IEnumerable<char>, but we want to treat it as a primitive type.
+ if (o.GetType() == typeof(string))
+ {
+ return null;
+ }
+ return o as IEnumerable;
+ }
+
+ /// <summary>
+ /// Determines whether the given type is one of the special known types
+ /// that must be read from the request body via the formatter and cannot
+ /// be validated as a user's complex type.
+ /// </summary>
+ /// <param name="type">The type to check.</param>
+ /// <returns><c>true</c> if this is a known body content type.</returns>
+ internal static bool IsStructuredBodyContentType(Type type)
+ {
+ Contract.Assert(type != null);
+
+ // TODO: [DevDiv2 327725] Consider model that allows model binding to multiple JsonValues inside an outer JsonValue.
+ // TODO: This helper method exists only to say "these types are atomic can only be read as a single object from the body".
+ return type == typeof(JsonValue);
+ }
+
+ /// <summary>
+ /// Determines whether the given type is a generic "http intrinsic"
+ /// type, such as <see cref="ObjectContent{T}"/>.
+ /// </summary>
+ /// <param name="type">The type to check.</param>
+ /// <returns><c>true</c> if the type is a generic http intrinsic type.</returns>
+ internal static bool IsGenericIntrinsicHttpType(Type type)
+ {
+ Contract.Assert(type != null);
+ if (type.IsGenericType && !type.IsGenericTypeDefinition)
+ {
+ Type genericTypeDefinition = type.GetGenericTypeDefinition();
+ return IsHttpRequestOrContentGenericTypeDefinition(genericTypeDefinition);
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Fast implementation to get the subset of a given type.
+ /// </summary>
+ /// <typeparam name="T">type to search for</typeparam>
+ /// <returns>subset of objects that can be assigned to T</returns>
+ internal static ReadOnlyCollection<T> OfType<T>(object[] objects) where T : class
+ {
+ int max = objects.Length;
+ List<T> list = new List<T>(max);
+ int idx = 0;
+ for (int i = 0; i < max; i++)
+ {
+ T attr = objects[i] as T;
+ if (attr != null)
+ {
+ list.Add(attr);
+ idx++;
+ }
+ }
+ list.Capacity = idx;
+
+ return new ReadOnlyCollection<T>(list);
+ }
+
+ internal static Type UnwrapIfTask(Type type)
+ {
+ if (typeof(Task).IsAssignableFrom(type))
+ {
+ if (type.IsGenericType)
+ {
+ Type innerType = type.GetGenericArguments()[0];
+ return innerType;
+ }
+ else
+ {
+ return typeof(void);
+ }
+ }
+ else
+ {
+ return type;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/Internal/UriQueryUtility.cs b/src/System.Web.Http/Internal/UriQueryUtility.cs
new file mode 100644
index 00000000..b0f3b0de
--- /dev/null
+++ b/src/System.Web.Http/Internal/UriQueryUtility.cs
@@ -0,0 +1,544 @@
+using System.Collections;
+using System.Collections.Specialized;
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+using System.Runtime.Serialization;
+using System.Text;
+
+namespace System.Web.Http.Internal
+{
+ // TODO: Once IVT relationship from Formatting DLL has been removed we flip to a link
+ // so that we don't have two copies of this code.
+
+ /// <summary>
+ /// Helpers for encoding, decoding, and parsing URI query components.
+ /// </summary>
+ internal static class UriQueryUtility
+ {
+ public static NameValueCollection ParseQueryString(string query)
+ {
+ if (query == null)
+ {
+ throw new ArgumentNullException("query");
+ }
+
+ if (query.Length > 0 && query[0] == '?')
+ {
+ query = query.Substring(1);
+ }
+
+ return new HttpValueCollection(query);
+ }
+
+ [Serializable]
+ internal class HttpValueCollection : NameValueCollection
+ {
+ [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "Ported from WCF")]
+ internal HttpValueCollection(string str)
+ : base(StringComparer.OrdinalIgnoreCase)
+ {
+ if (!string.IsNullOrEmpty(str))
+ {
+ FillFromString(str, true);
+ }
+
+ IsReadOnly = false;
+ }
+
+ protected HttpValueCollection(SerializationInfo info, StreamingContext context)
+ : base(info, context)
+ {
+ }
+
+ public override string ToString()
+ {
+ return ToString(true, null);
+ }
+
+ internal void FillFromString(string s, bool urlencoded)
+ {
+ int l = (s != null) ? s.Length : 0;
+ int i = 0;
+
+ while (i < l)
+ {
+ // find next & while noting first = on the way (and if there are more)
+ int si = i;
+ int ti = -1;
+
+ while (i < l)
+ {
+ char ch = s[i];
+
+ if (ch == '=')
+ {
+ if (ti < 0)
+ {
+ ti = i;
+ }
+ }
+ else if (ch == '&')
+ {
+ break;
+ }
+
+ i++;
+ }
+
+ // extract the name / value pair
+ string name = string.Empty;
+ string value = string.Empty;
+
+ if (ti >= 0)
+ {
+ name = s.Substring(si, ti - si);
+ value = s.Substring(ti + 1, i - ti - 1);
+ }
+ else
+ {
+ value = s.Substring(si, i - si);
+ }
+
+ // add name / value pair to the collection
+ if (urlencoded)
+ {
+ Add(UriQueryUtility.UrlDecode(name), UriQueryUtility.UrlDecode(value));
+ }
+ else
+ {
+ Add(name, value);
+ }
+
+ // trailing '&'
+ if (i == l - 1 && s[i] == '&')
+ {
+ Add(string.Empty, string.Empty);
+ }
+
+ i++;
+ }
+ }
+
+ string ToString(bool urlencoded, IDictionary excludeKeys)
+ {
+ int n = Count;
+ if (n == 0)
+ {
+ return string.Empty;
+ }
+
+ StringBuilder s = new StringBuilder();
+ string key, keyPrefix, item;
+
+ for (int i = 0; i < n; i++)
+ {
+ key = GetKey(i);
+
+ if (excludeKeys != null && key != null && excludeKeys[key] != null)
+ {
+ continue;
+ }
+
+ if (urlencoded)
+ {
+ key = UriQueryUtility.UrlEncode(key);
+ }
+
+ keyPrefix = (!string.IsNullOrEmpty(key)) ? (key + "=") : string.Empty;
+
+ ArrayList values = (ArrayList)BaseGet(i);
+ int numValues = (values != null) ? values.Count : 0;
+
+ if (s.Length > 0)
+ {
+ s.Append('&');
+ }
+
+ if (numValues == 1)
+ {
+ s.Append(keyPrefix);
+ item = (string)values[0];
+ if (urlencoded)
+ {
+ item = UriQueryUtility.UrlEncode(item);
+ }
+
+ s.Append(item);
+ }
+ else if (numValues == 0)
+ {
+ s.Append(keyPrefix);
+ }
+ else
+ {
+ for (int j = 0; j < numValues; j++)
+ {
+ if (j > 0)
+ {
+ s.Append('&');
+ }
+
+ s.Append(keyPrefix);
+ item = (string)values[j];
+ if (urlencoded)
+ {
+ item = UriQueryUtility.UrlEncode(item);
+ }
+
+ s.Append(item);
+ }
+ }
+ }
+
+ return s.ToString();
+ }
+ }
+
+ // The implementation below is ported from WebUtility for use in .Net 4
+
+ #region UrlEncode implementation
+
+ private static byte[] UrlEncode(byte[] bytes, int offset, int count, bool alwaysCreateNewReturnValue)
+ {
+ byte[] encoded = UrlEncode(bytes, offset, count);
+
+ return (alwaysCreateNewReturnValue && (encoded != null) && (encoded == bytes))
+ ? (byte[])encoded.Clone()
+ : encoded;
+ }
+
+ private static byte[] UrlEncode(byte[] bytes, int offset, int count)
+ {
+ if (!ValidateUrlEncodingParameters(bytes, offset, count))
+ {
+ return null;
+ }
+
+ int cSpaces = 0;
+ int cUnsafe = 0;
+
+ // count them first
+ for (int i = 0; i < count; i++)
+ {
+ char ch = (char)bytes[offset + i];
+
+ if (ch == ' ')
+ cSpaces++;
+ else if (!IsUrlSafeChar(ch))
+ cUnsafe++;
+ }
+
+ // nothing to expand?
+ if (cSpaces == 0 && cUnsafe == 0)
+ return bytes;
+
+ // expand not 'safe' characters into %XX, spaces to +s
+ byte[] expandedBytes = new byte[count + cUnsafe * 2];
+ int pos = 0;
+
+ for (int i = 0; i < count; i++)
+ {
+ byte b = bytes[offset + i];
+ char ch = (char)b;
+
+ if (IsUrlSafeChar(ch))
+ {
+ expandedBytes[pos++] = b;
+ }
+ else if (ch == ' ')
+ {
+ expandedBytes[pos++] = (byte)'+';
+ }
+ else
+ {
+ expandedBytes[pos++] = (byte)'%';
+ expandedBytes[pos++] = (byte)IntToHex((b >> 4) & 0xf);
+ expandedBytes[pos++] = (byte)IntToHex(b & 0x0f);
+ }
+ }
+
+ return expandedBytes;
+ }
+
+ #endregion
+
+ #region UrlEncode public methods
+
+ public static string UrlEncode(string str)
+ {
+ if (str == null)
+ return null;
+
+ byte[] bytes = Encoding.UTF8.GetBytes(str);
+ return Encoding.ASCII.GetString(UrlEncode(bytes, 0, bytes.Length, false /* alwaysCreateNewReturnValue */));
+ }
+
+ public static byte[] UrlEncodeToBytes(byte[] bytes, int offset, int count)
+ {
+ return UrlEncode(bytes, offset, count, true /* alwaysCreateNewReturnValue */);
+ }
+
+ #endregion
+
+ #region UrlDecode implementation
+
+ private static string UrlDecodeInternal(string value, Encoding encoding)
+ {
+ if (value == null)
+ {
+ return null;
+ }
+
+ int count = value.Length;
+ UrlDecoder helper = new UrlDecoder(count, encoding);
+
+ // go through the string's chars collapsing %XX and %uXXXX and
+ // appending each char as char, with exception of %XX constructs
+ // that are appended as bytes
+
+ for (int pos = 0; pos < count; pos++)
+ {
+ char ch = value[pos];
+
+ if (ch == '+')
+ {
+ ch = ' ';
+ }
+ else if (ch == '%' && pos < count - 2)
+ {
+ if (value[pos + 1] == 'u' && pos < count - 5)
+ {
+ int h1 = HexToInt(value[pos + 2]);
+ int h2 = HexToInt(value[pos + 3]);
+ int h3 = HexToInt(value[pos + 4]);
+ int h4 = HexToInt(value[pos + 5]);
+
+ if (h1 >= 0 && h2 >= 0 && h3 >= 0 && h4 >= 0)
+ { // valid 4 hex chars
+ ch = (char)((h1 << 12) | (h2 << 8) | (h3 << 4) | h4);
+ pos += 5;
+
+ // only add as char
+ helper.AddChar(ch);
+ continue;
+ }
+ }
+ else
+ {
+ int h1 = HexToInt(value[pos + 1]);
+ int h2 = HexToInt(value[pos + 2]);
+
+ if (h1 >= 0 && h2 >= 0)
+ { // valid 2 hex chars
+ byte b = (byte)((h1 << 4) | h2);
+ pos += 2;
+
+ // don't add as char
+ helper.AddByte(b);
+ continue;
+ }
+ }
+ }
+
+ if ((ch & 0xFF80) == 0)
+ helper.AddByte((byte)ch); // 7 bit have to go as bytes because of Unicode
+ else
+ helper.AddChar(ch);
+ }
+
+ return helper.GetString();
+ }
+
+ private static byte[] UrlDecodeInternal(byte[] bytes, int offset, int count)
+ {
+ if (!ValidateUrlEncodingParameters(bytes, offset, count))
+ {
+ return null;
+ }
+
+ int decodedBytesCount = 0;
+ byte[] decodedBytes = new byte[count];
+
+ for (int i = 0; i < count; i++)
+ {
+ int pos = offset + i;
+ byte b = bytes[pos];
+
+ if (b == '+')
+ {
+ b = (byte)' ';
+ }
+ else if (b == '%' && i < count - 2)
+ {
+ int h1 = HexToInt((char)bytes[pos + 1]);
+ int h2 = HexToInt((char)bytes[pos + 2]);
+
+ if (h1 >= 0 && h2 >= 0)
+ { // valid 2 hex chars
+ b = (byte)((h1 << 4) | h2);
+ i += 2;
+ }
+ }
+
+ decodedBytes[decodedBytesCount++] = b;
+ }
+
+ if (decodedBytesCount < decodedBytes.Length)
+ {
+ byte[] newDecodedBytes = new byte[decodedBytesCount];
+ Array.Copy(decodedBytes, newDecodedBytes, decodedBytesCount);
+ decodedBytes = newDecodedBytes;
+ }
+
+ return decodedBytes;
+ }
+
+ #endregion
+
+ #region UrlDecode public methods
+
+ public static string UrlDecode(string str)
+ {
+ if (str == null)
+ return null;
+
+ return UrlDecodeInternal(str, Encoding.UTF8);
+ }
+
+ public static byte[] UrlDecodeToBytes(byte[] bytes, int offset, int count)
+ {
+ return UrlDecodeInternal(bytes, offset, count);
+ }
+
+ #endregion
+
+ #region Helper methods
+
+ private static int HexToInt(char h)
+ {
+ return (h >= '0' && h <= '9') ? h - '0' :
+ (h >= 'a' && h <= 'f') ? h - 'a' + 10 :
+ (h >= 'A' && h <= 'F') ? h - 'A' + 10 :
+ -1;
+ }
+
+ private static char IntToHex(int n)
+ {
+ Contract.Assert(n < 0x10);
+
+ if (n <= 9)
+ return (char)(n + (int)'0');
+ else
+ return (char)(n - 10 + (int)'a');
+ }
+
+ // Set of safe chars, from RFC 1738.4 minus '+'
+ private static bool IsUrlSafeChar(char ch)
+ {
+ if (ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z' || ch >= '0' && ch <= '9')
+ return true;
+
+ switch (ch)
+ {
+ case '-':
+ case '_':
+ case '.':
+ case '!':
+ case '*':
+ case '(':
+ case ')':
+ return true;
+ }
+
+ return false;
+ }
+
+ private static bool ValidateUrlEncodingParameters(byte[] bytes, int offset, int count)
+ {
+ if (bytes == null && count == 0)
+ return false;
+ if (bytes == null)
+ {
+ throw new ArgumentNullException("bytes");
+ }
+ if (offset < 0 || offset > bytes.Length)
+ {
+ throw new ArgumentOutOfRangeException("offset");
+ }
+ if (count < 0 || offset + count > bytes.Length)
+ {
+ throw new ArgumentOutOfRangeException("count");
+ }
+
+ return true;
+ }
+
+ #endregion
+
+ #region UrlDecoder nested class
+
+ // Internal class to facilitate URL decoding -- keeps char buffer and byte buffer, allows appending of either chars or bytes
+ private class UrlDecoder
+ {
+ private int _bufferSize;
+
+ // Accumulate characters in a special array
+ private int _numChars;
+ private char[] _charBuffer;
+
+ // Accumulate bytes for decoding into characters in a special array
+ private int _numBytes;
+ private byte[] _byteBuffer;
+
+ // Encoding to convert chars to bytes
+ private Encoding _encoding;
+
+ private void FlushBytes()
+ {
+ if (_numBytes > 0)
+ {
+ _numChars += _encoding.GetChars(_byteBuffer, 0, _numBytes, _charBuffer, _numChars);
+ _numBytes = 0;
+ }
+ }
+
+ internal UrlDecoder(int bufferSize, Encoding encoding)
+ {
+ _bufferSize = bufferSize;
+ _encoding = encoding;
+
+ _charBuffer = new char[bufferSize];
+ // byte buffer created on demand
+ }
+
+ internal void AddChar(char ch)
+ {
+ if (_numBytes > 0)
+ FlushBytes();
+
+ _charBuffer[_numChars++] = ch;
+ }
+
+ internal void AddByte(byte b)
+ {
+ if (_byteBuffer == null)
+ _byteBuffer = new byte[_bufferSize];
+
+ _byteBuffer[_numBytes++] = b;
+ }
+
+ internal String GetString()
+ {
+ if (_numBytes > 0)
+ FlushBytes();
+
+ if (_numChars > 0)
+ return new String(_charBuffer, 0, _numChars);
+ else
+ return String.Empty;
+ }
+ }
+
+ #endregion
+ }
+} \ No newline at end of file
diff --git a/src/System.Web.Http/Internal/ValueProviderUtil.cs b/src/System.Web.Http/Internal/ValueProviderUtil.cs
new file mode 100644
index 00000000..dc2efc23
--- /dev/null
+++ b/src/System.Web.Http/Internal/ValueProviderUtil.cs
@@ -0,0 +1,161 @@
+// Combination of code from System.Web.Mvc and Microsoft.Web.Mvc
+
+using System.Collections.Generic;
+using System.Linq;
+
+namespace System.Web.Http.Internal
+{
+ internal static class ValueProviderUtil
+ {
+ internal static bool CollectionContainsPrefix(IEnumerable<string> collection, string prefix)
+ {
+ foreach (string key in collection)
+ {
+ if (key != null)
+ {
+ if (prefix.Length == 0)
+ {
+ return true; // shortcut - non-null key matches empty prefix
+ }
+
+ if (key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+ {
+ if (key.Length == prefix.Length)
+ {
+ return true; // exact match
+ }
+ else
+ {
+ switch (key[prefix.Length])
+ {
+ case '.': // known separator characters
+ case '[':
+ return true;
+ }
+ }
+ }
+ }
+ }
+
+ return false; // nothing found
+ }
+
+ // Given "foo.bar", "foo.hello", "something.other", foo[abc].baz and asking for prefix "foo" will return:
+ // - "bar"/"foo.bar"
+ // - "hello"/"foo.hello"
+ // - "abc"/"foo[abc]"
+ internal static IDictionary<string, string> GetKeysFromPrefix(IEnumerable<string> collection, string prefix)
+ {
+ if (String.IsNullOrWhiteSpace(prefix))
+ {
+ return collection.ToDictionary(value => value, StringComparer.OrdinalIgnoreCase);
+ }
+
+ IDictionary<string, string> keys = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ foreach (var entry in collection)
+ {
+ if (entry != null)
+ {
+ string key = null;
+ string fullName = null;
+
+ if (entry.Length == prefix.Length)
+ {
+ // No key in this entry
+ continue;
+ }
+
+ if (entry.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+ {
+ int keyPosition = prefix.Length + 1;
+ switch (entry[prefix.Length])
+ {
+ case '.':
+ int dotPosition = entry.IndexOf('.', keyPosition);
+ if (dotPosition == -1)
+ {
+ dotPosition = entry.Length;
+ }
+
+ key = entry.Substring(keyPosition, dotPosition - keyPosition);
+ fullName = entry.Substring(0, dotPosition);
+ break;
+ case '[':
+ int bracketPosition = entry.IndexOf(']', keyPosition);
+ if (bracketPosition == -1)
+ {
+ // Malformed for dictionary
+ continue;
+ }
+
+ key = entry.Substring(keyPosition, bracketPosition - keyPosition);
+ fullName = entry.Substring(0, bracketPosition + 1);
+ break;
+ }
+
+ if (!keys.ContainsKey(key))
+ {
+ keys.Add(key, fullName);
+ }
+ }
+ }
+ }
+
+ return keys;
+ }
+
+ // Given "foo.bar[baz].quux", this method will return:
+ // - "foo.bar[baz].quux"
+ // - "foo.bar[baz]"
+ // - "foo.bar"
+ // - "foo"
+ internal static IEnumerable<string> GetPrefixes(string key)
+ {
+ yield return key;
+ for (int i = key.Length - 1; i >= 0; i--)
+ {
+ switch (key[i])
+ {
+ case '.':
+ case '[':
+ yield return key.Substring(0, i);
+ break;
+ }
+ }
+ }
+
+ internal static bool IsPrefixMatch(string prefix, string testString)
+ {
+ if (testString == null)
+ {
+ return false;
+ }
+
+ if (prefix.Length == 0)
+ {
+ return true; // shortcut - non-null testString matches empty prefix
+ }
+
+ if (!testString.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+ {
+ return false; // prefix doesn't match
+ }
+
+ if (testString.Length == prefix.Length)
+ {
+ return true; // exact match
+ }
+
+ // invariant: testString.Length > prefix.Length
+ switch (testString[prefix.Length])
+ {
+ case '.':
+ case '[':
+ return true; // known delimiters
+
+ default:
+ return false; // not known delimiter
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/Metadata/IMetadataAware.cs b/src/System.Web.Http/Metadata/IMetadataAware.cs
new file mode 100644
index 00000000..036ab962
--- /dev/null
+++ b/src/System.Web.Http/Metadata/IMetadataAware.cs
@@ -0,0 +1,12 @@
+namespace System.Web.Http.Metadata
+{
+ // This interface is implemented by attributes which wish to contribute to the
+ // ModelMetadata creation process without needing to write a custom metadata
+ // provider. It is consumed by AssociatedMetadataProvider, so this behavior is
+ // automatically inherited by all classes which derive from it (notably, the
+ // DataAnnotationsModelMetadataProvider).
+ public interface IMetadataAware
+ {
+ void OnMetadataCreated(ModelMetadata metadata);
+ }
+}
diff --git a/src/System.Web.Http/Metadata/ModelMetadata.cs b/src/System.Web.Http/Metadata/ModelMetadata.cs
new file mode 100644
index 00000000..ea880191
--- /dev/null
+++ b/src/System.Web.Http/Metadata/ModelMetadata.cs
@@ -0,0 +1,264 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Internal;
+using System.Web.Http.Validation;
+
+namespace System.Web.Http.Metadata
+{
+ public class ModelMetadata
+ {
+ public const int DefaultOrder = 10000;
+
+ private readonly Type _containerType;
+ private readonly Type _modelType;
+ private readonly string _propertyName;
+
+ /// <summary>
+ /// Explicit backing store for the things we want initialized by default, so don't have to call
+ /// the protected virtual setters of an auto-generated property.
+ /// </summary>
+ private Dictionary<string, object> _additionalValues = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
+ private bool _convertEmptyStringToNull = true;
+ private bool _isRequired;
+ private object _model;
+ private Func<object> _modelAccessor;
+ private int _order = DefaultOrder;
+ private IEnumerable<ModelMetadata> _properties;
+ private Type _realModelType;
+ private bool _requestValidationEnabled = true;
+ private bool _showForDisplay = true;
+ private bool _showForEdit = true;
+ private string _simpleDisplayText;
+
+ public ModelMetadata(ModelMetadataProvider provider, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
+ {
+ if (provider == null)
+ {
+ throw Error.ArgumentNull("provider");
+ }
+ if (modelType == null)
+ {
+ throw Error.ArgumentNull("modelType");
+ }
+
+ Provider = provider;
+
+ _containerType = containerType;
+ _isRequired = !TypeHelper.TypeAllowsNullValue(modelType);
+ _modelAccessor = modelAccessor;
+ _modelType = modelType;
+ _propertyName = propertyName;
+ }
+
+ public virtual Dictionary<string, object> AdditionalValues
+ {
+ get { return _additionalValues; }
+ }
+
+ public Type ContainerType
+ {
+ get { return _containerType; }
+ }
+
+ public virtual bool ConvertEmptyStringToNull
+ {
+ get { return _convertEmptyStringToNull; }
+ set { _convertEmptyStringToNull = value; }
+ }
+
+ public virtual string DataTypeName { get; set; }
+
+ public virtual string Description { get; set; }
+
+ public virtual string DisplayFormatString { get; set; }
+
+ [SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods", Justification = "The method is a delegating helper to choose among multiple property values")]
+ public virtual string DisplayName { get; set; }
+
+ public virtual string EditFormatString { get; set; }
+
+ public virtual bool HideSurroundingHtml { get; set; }
+
+ public virtual bool IsComplexType
+ {
+ get { return !TypeDescriptor.GetConverter(ModelType).CanConvertFrom(typeof(string)); }
+ }
+
+ public bool IsNullableValueType
+ {
+ get { return TypeHelper.IsNullableValueType(ModelType); }
+ }
+
+ public virtual bool IsReadOnly { get; set; }
+
+ public virtual bool IsRequired
+ {
+ get { return _isRequired; }
+ set { _isRequired = value; }
+ }
+
+ public object Model
+ {
+ get
+ {
+ if (_modelAccessor != null)
+ {
+ _model = _modelAccessor();
+ _modelAccessor = null;
+ }
+ return _model;
+ }
+ set
+ {
+ _model = value;
+ _modelAccessor = null;
+ _properties = null;
+ _realModelType = null;
+ }
+ }
+
+ public Type ModelType
+ {
+ get { return _modelType; }
+ }
+
+ public virtual string NullDisplayText { get; set; }
+
+ public virtual int Order
+ {
+ get { return _order; }
+ set { _order = value; }
+ }
+
+ public virtual IEnumerable<ModelMetadata> Properties
+ {
+ get
+ {
+ if (_properties == null)
+ {
+ _properties = Provider.GetMetadataForProperties(Model, RealModelType).OrderBy(m => m.Order);
+ }
+ return _properties;
+ }
+ }
+
+ public string PropertyName
+ {
+ get { return _propertyName; }
+ }
+
+ protected ModelMetadataProvider Provider { get; set; }
+
+ internal Type RealModelType
+ {
+ get
+ {
+ if (_realModelType == null)
+ {
+ _realModelType = ModelType;
+
+ // Don't call GetType() if the model is Nullable<T>, because it will
+ // turn Nullable<T> into T for non-null values
+ if (Model != null && !TypeHelper.IsNullableValueType(ModelType))
+ {
+ _realModelType = Model.GetType();
+ }
+ }
+
+ return _realModelType;
+ }
+ }
+
+ public virtual bool RequestValidationEnabled
+ {
+ get { return _requestValidationEnabled; }
+ set { _requestValidationEnabled = value; }
+ }
+
+ public virtual string ShortDisplayName { get; set; }
+
+ public virtual bool ShowForDisplay
+ {
+ get { return _showForDisplay; }
+ set { _showForDisplay = value; }
+ }
+
+ public virtual bool ShowForEdit
+ {
+ get { return _showForEdit; }
+ set { _showForEdit = value; }
+ }
+
+ [SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods", Justification = "This property delegates to the method when the user has not yet set a simple display text value.")]
+ public virtual string SimpleDisplayText
+ {
+ get
+ {
+ if (_simpleDisplayText == null)
+ {
+ _simpleDisplayText = GetSimpleDisplayText();
+ }
+ return _simpleDisplayText;
+ }
+ set { _simpleDisplayText = value; }
+ }
+
+ public virtual string TemplateHint { get; set; }
+
+ public virtual string Watermark { get; set; }
+
+ [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "The method is a delegating helper to choose among multiple property values")]
+ public string GetDisplayName()
+ {
+ return DisplayName ?? PropertyName ?? ModelType.Name;
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This method is used to resolve the simple display text when it was not explicitly set through other means.")]
+ protected virtual string GetSimpleDisplayText()
+ {
+ if (Model == null)
+ {
+ return NullDisplayText;
+ }
+
+ string toStringResult = Convert.ToString(Model, CultureInfo.CurrentCulture);
+ if (toStringResult == null)
+ {
+ return String.Empty;
+ }
+
+ if (!toStringResult.Equals(Model.GetType().FullName, StringComparison.Ordinal))
+ {
+ return toStringResult;
+ }
+
+ ModelMetadata firstProperty = Properties.FirstOrDefault();
+ if (firstProperty == null)
+ {
+ return String.Empty;
+ }
+
+ if (firstProperty.Model == null)
+ {
+ return firstProperty.NullDisplayText;
+ }
+
+ return Convert.ToString(firstProperty.Model, CultureInfo.CurrentCulture);
+ }
+
+ public virtual IEnumerable<ModelValidator> GetValidators(IEnumerable<ModelValidatorProvider> validatorProviders)
+ {
+ if (validatorProviders == null)
+ {
+ throw Error.ArgumentNull("validatorProviders");
+ }
+
+ return validatorProviders.SelectMany(provider => provider.GetValidators(this, validatorProviders));
+ }
+ }
+}
diff --git a/src/System.Web.Http/Metadata/ModelMetadataProvider.cs b/src/System.Web.Http/Metadata/ModelMetadataProvider.cs
new file mode 100644
index 00000000..e5d7d6f1
--- /dev/null
+++ b/src/System.Web.Http/Metadata/ModelMetadataProvider.cs
@@ -0,0 +1,13 @@
+using System.Collections.Generic;
+
+namespace System.Web.Http.Metadata
+{
+ public abstract class ModelMetadataProvider
+ {
+ public abstract IEnumerable<ModelMetadata> GetMetadataForProperties(object container, Type containerType);
+
+ public abstract ModelMetadata GetMetadataForProperty(Func<object> modelAccessor, Type containerType, string propertyName);
+
+ public abstract ModelMetadata GetMetadataForType(Func<object> modelAccessor, Type modelType);
+ }
+}
diff --git a/src/System.Web.Http/Metadata/Providers/AssociatedMetadataProvider.cs b/src/System.Web.Http/Metadata/Providers/AssociatedMetadataProvider.cs
new file mode 100644
index 00000000..3e4276df
--- /dev/null
+++ b/src/System.Web.Http/Metadata/Providers/AssociatedMetadataProvider.cs
@@ -0,0 +1,110 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Web.Http.Common;
+using System.Web.Http.Internal;
+using System.Web.Http.Properties;
+
+namespace System.Web.Http.Metadata.Providers
+{
+ // This class provides a good implementation of ModelMetadataProvider for people who will be
+ // using traditional classes with properties. It uses the buddy class support from
+ // DataAnnotations, and consolidates the three operations down to a single override
+ // for reading the attribute values and creating the metadata class.
+ public abstract class AssociatedMetadataProvider : ModelMetadataProvider
+ {
+ private static void ApplyMetadataAwareAttributes(IEnumerable<Attribute> attributes, ModelMetadata result)
+ {
+ foreach (IMetadataAware awareAttribute in attributes.OfType<IMetadataAware>())
+ {
+ awareAttribute.OnMetadataCreated(result);
+ }
+ }
+
+ protected abstract ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName);
+
+ protected virtual IEnumerable<Attribute> FilterAttributes(Type containerType, PropertyDescriptor propertyDescriptor, IEnumerable<Attribute> attributes)
+ {
+ // The Model property on ViewPage and ViewUserControl is marked as ReadOnly
+#if false
+ if (typeof(ViewPage).IsAssignableFrom(containerType) || typeof(ViewUserControl).IsAssignableFrom(containerType))
+ {
+ return attributes.Where(a => !(a is ReadOnlyAttribute));
+ }
+#endif
+
+ return attributes;
+ }
+
+ public override IEnumerable<ModelMetadata> GetMetadataForProperties(object container, Type containerType)
+ {
+ if (containerType == null)
+ {
+ throw Error.ArgumentNull("containerType");
+ }
+
+ return GetMetadataForPropertiesImpl(container, containerType);
+ }
+
+ private IEnumerable<ModelMetadata> GetMetadataForPropertiesImpl(object container, Type containerType)
+ {
+ foreach (PropertyDescriptor property in GetTypeDescriptor(containerType).GetProperties())
+ {
+ Func<object> modelAccessor = container == null ? null : GetPropertyValueAccessor(container, property);
+ yield return GetMetadataForProperty(modelAccessor, containerType, property);
+ }
+ }
+
+ public override ModelMetadata GetMetadataForProperty(Func<object> modelAccessor, Type containerType, string propertyName)
+ {
+ if (containerType == null)
+ {
+ throw Error.ArgumentNull("containerType");
+ }
+ if (String.IsNullOrEmpty(propertyName))
+ {
+ throw Error.ArgumentNullOrEmpty("propertyName");
+ }
+
+ ICustomTypeDescriptor typeDescriptor = GetTypeDescriptor(containerType);
+ PropertyDescriptor property = typeDescriptor.GetProperties().Find(propertyName, true);
+ if (property == null)
+ {
+ throw Error.Argument("propertyName", SRResources.Common_PropertyNotFound, containerType, propertyName);
+ }
+
+ return GetMetadataForProperty(modelAccessor, containerType, property);
+ }
+
+ protected virtual ModelMetadata GetMetadataForProperty(Func<object> modelAccessor, Type containerType, PropertyDescriptor propertyDescriptor)
+ {
+ IEnumerable<Attribute> attributes = FilterAttributes(containerType, propertyDescriptor, propertyDescriptor.Attributes.Cast<Attribute>());
+ ModelMetadata result = CreateMetadata(attributes, containerType, modelAccessor, propertyDescriptor.PropertyType, propertyDescriptor.Name);
+ ApplyMetadataAwareAttributes(attributes, result);
+ return result;
+ }
+
+ public override ModelMetadata GetMetadataForType(Func<object> modelAccessor, Type modelType)
+ {
+ if (modelType == null)
+ {
+ throw Error.ArgumentNull("modelType");
+ }
+
+ IEnumerable<Attribute> attributes = GetTypeDescriptor(modelType).GetAttributes().Cast<Attribute>();
+ ModelMetadata result = CreateMetadata(attributes, null /* containerType */, modelAccessor, modelType, null /* propertyName */);
+ ApplyMetadataAwareAttributes(attributes, result);
+ return result;
+ }
+
+ private static Func<object> GetPropertyValueAccessor(object container, PropertyDescriptor property)
+ {
+ return () => property.GetValue(container);
+ }
+
+ protected virtual ICustomTypeDescriptor GetTypeDescriptor(Type type)
+ {
+ return TypeDescriptorHelper.Get(type);
+ }
+ }
+}
diff --git a/src/System.Web.Http/Metadata/Providers/CachedAssociatedMetadataProvider.cs b/src/System.Web.Http/Metadata/Providers/CachedAssociatedMetadataProvider.cs
new file mode 100644
index 00000000..09162f80
--- /dev/null
+++ b/src/System.Web.Http/Metadata/Providers/CachedAssociatedMetadataProvider.cs
@@ -0,0 +1,94 @@
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Runtime.Caching;
+
+namespace System.Web.Http.Metadata.Providers
+{
+ public abstract class CachedAssociatedMetadataProvider<TModelMetadata> : AssociatedMetadataProvider
+ where TModelMetadata : ModelMetadata
+ {
+ private static ConcurrentDictionary<Type, string> _typeIds = new ConcurrentDictionary<Type, string>();
+ private string _cacheKeyPrefix;
+ private CacheItemPolicy _cacheItemPolicy = new CacheItemPolicy { SlidingExpiration = TimeSpan.FromMinutes(20) };
+ private ObjectCache _prototypeCache;
+
+ protected internal CacheItemPolicy CacheItemPolicy
+ {
+ get { return _cacheItemPolicy; }
+ set { _cacheItemPolicy = value; }
+ }
+
+ protected string CacheKeyPrefix
+ {
+ get
+ {
+ if (_cacheKeyPrefix == null)
+ {
+ _cacheKeyPrefix = "MetadataPrototypes::" + GetType().GUID.ToString("B");
+ }
+ return _cacheKeyPrefix;
+ }
+ }
+
+ protected internal ObjectCache PrototypeCache
+ {
+ get { return _prototypeCache ?? MemoryCache.Default; }
+ set { _prototypeCache = value; }
+ }
+
+ protected sealed override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
+ {
+ // If metadata is being created for a property then containerType != null && propertyName != null
+ // If metadata is being created for a type then containerType == null && propertyName == null, so we have to use modelType for the cache key.
+ Type typeForCache = containerType ?? modelType;
+ string cacheKey = GetCacheKey(typeForCache, propertyName);
+ TModelMetadata prototype = PrototypeCache.Get(cacheKey) as TModelMetadata;
+ if (prototype == null)
+ {
+ prototype = CreateMetadataPrototype(attributes, containerType, modelType, propertyName);
+ PrototypeCache.Add(cacheKey, prototype, CacheItemPolicy);
+ }
+
+ return CreateMetadataFromPrototype(prototype, modelAccessor);
+ }
+
+ // New override for creating the prototype metadata (without the accessor)
+ protected abstract TModelMetadata CreateMetadataPrototype(IEnumerable<Attribute> attributes, Type containerType, Type modelType, string propertyName);
+
+ // New override for applying the prototype + modelAccess to yield the final metadata
+ protected abstract TModelMetadata CreateMetadataFromPrototype(TModelMetadata prototype, Func<object> modelAccessor);
+
+ internal string GetCacheKey(Type type, string propertyName = null)
+ {
+ propertyName = propertyName ?? String.Empty;
+ return CacheKeyPrefix + GetTypeId(type) + propertyName;
+ }
+
+ public sealed override ModelMetadata GetMetadataForProperty(Func<object> modelAccessor, Type containerType, string propertyName)
+ {
+ return base.GetMetadataForProperty(modelAccessor, containerType, propertyName);
+ }
+
+ protected sealed override ModelMetadata GetMetadataForProperty(Func<object> modelAccessor, Type containerType, PropertyDescriptor propertyDescriptor)
+ {
+ return base.GetMetadataForProperty(modelAccessor, containerType, propertyDescriptor);
+ }
+
+ public sealed override IEnumerable<ModelMetadata> GetMetadataForProperties(object container, Type containerType)
+ {
+ return base.GetMetadataForProperties(container, containerType);
+ }
+
+ public sealed override ModelMetadata GetMetadataForType(Func<object> modelAccessor, Type modelType)
+ {
+ return base.GetMetadataForType(modelAccessor, modelType);
+ }
+
+ private static string GetTypeId(Type type)
+ {
+ // It's fine using a random Guid since we store the mapping for types to guids.
+ return _typeIds.GetOrAdd(type, _ => Guid.NewGuid().ToString("B"));
+ }
+ }
+}
diff --git a/src/System.Web.Http/Metadata/Providers/CachedDataAnnotationsMetadataAttributes.cs b/src/System.Web.Http/Metadata/Providers/CachedDataAnnotationsMetadataAttributes.cs
new file mode 100644
index 00000000..91e25428
--- /dev/null
+++ b/src/System.Web.Http/Metadata/Providers/CachedDataAnnotationsMetadataAttributes.cs
@@ -0,0 +1,92 @@
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Security;
+
+namespace System.Web.Http.Metadata.Providers
+{
+ // REVIEW: No access to HiddenInputAttribute
+ public class CachedDataAnnotationsMetadataAttributes
+ {
+ public CachedDataAnnotationsMetadataAttributes(Attribute[] attributes)
+ {
+ CacheAttributes(attributes);
+ }
+
+ // [SecuritySafeCritical] because it uses DataAnnotations type DataTypeAttribute
+ public DataTypeAttribute DataType { [SecuritySafeCritical] get; [SecuritySafeCritical] protected set; }
+
+ // [SecuritySafeCritical] because it uses DataAnnotations type DisplayAttribute
+ public DisplayAttribute Display { [SecuritySafeCritical] get; [SecuritySafeCritical] protected set; }
+
+ // [SecuritySafeCritical] because it uses DataAnnotations type DisplayColumnAttribute
+ public DisplayColumnAttribute DisplayColumn { [SecuritySafeCritical] get; [SecuritySafeCritical] protected set; }
+
+ // [SecuritySafeCritical] because it uses DataAnnotations type DisplayFormatAttribute
+ public DisplayFormatAttribute DisplayFormat { [SecuritySafeCritical] get; [SecuritySafeCritical] protected set; }
+
+ // [SecuritySafeCritical] because it uses DataAnnotations type DisplayNameAttribute
+ public DisplayNameAttribute DisplayName { [SecuritySafeCritical] get; [SecuritySafeCritical] protected set; }
+
+ // [SecuritySafeCritical] because it uses DataAnnotations type EditableAttribute
+ public EditableAttribute Editable { [SecuritySafeCritical] get; [SecuritySafeCritical] protected set; }
+#if false
+ public HiddenInputAttribute HiddenInput { get; protected set; }
+#endif
+ public ReadOnlyAttribute ReadOnly { get; protected set; }
+
+ // [SecuritySafeCritical] because it uses DataAnnotations type RequiredAttribute
+ public RequiredAttribute Required { [SecuritySafeCritical] get; [SecuritySafeCritical] protected set; }
+
+ // [SecuritySafeCritical] because it uses DataAnnotations type
+ public ScaffoldColumnAttribute ScaffoldColumn { [SecuritySafeCritical] get; [SecuritySafeCritical] protected set; }
+
+ // [SecuritySafeCritical] because it uses DataAnnotations type ScaffoldColumnAttribute
+ public UIHintAttribute UIHint { [SecuritySafeCritical] get; [SecuritySafeCritical] protected set; }
+
+ // [SecuritySafeCritical] because it uses several DataAnnotations attribute types
+ [SecuritySafeCritical]
+ private void CacheAttributes(Attribute[] attributes)
+ {
+ DataType = attributes.OfType<DataTypeAttribute>().FirstOrDefault();
+ Display = attributes.OfType<DisplayAttribute>().FirstOrDefault();
+ DisplayColumn = attributes.OfType<DisplayColumnAttribute>().FirstOrDefault();
+ DisplayFormat = attributes.OfType<DisplayFormatAttribute>().FirstOrDefault();
+ DisplayName = attributes.OfType<DisplayNameAttribute>().FirstOrDefault();
+ Editable = attributes.OfType<EditableAttribute>().FirstOrDefault();
+#if false
+ HiddenInput = attributes.OfType<HiddenInputAttribute>().FirstOrDefault();
+#endif
+ ReadOnly = attributes.OfType<ReadOnlyAttribute>().FirstOrDefault();
+ Required = attributes.OfType<RequiredAttribute>().FirstOrDefault();
+ ScaffoldColumn = attributes.OfType<ScaffoldColumnAttribute>().FirstOrDefault();
+
+ var uiHintAttributes = attributes.OfType<UIHintAttribute>();
+
+ // Developer note: this loop is explicitly unrolled because Linq lambdas methods are not
+ // [SecuritySafeCritical] and generate security exceptions accessing DataAnnotations types.
+ UIHintAttribute bestUIHint = null;
+ foreach (UIHintAttribute uiHintAttribute in uiHintAttributes)
+ {
+ string presentationLayer = uiHintAttribute.PresentationLayer;
+ if (String.Equals(presentationLayer, "MVC", StringComparison.OrdinalIgnoreCase))
+ {
+ bestUIHint = uiHintAttribute;
+ break;
+ }
+
+ if (bestUIHint == null && String.IsNullOrEmpty(presentationLayer))
+ {
+ bestUIHint = uiHintAttribute;
+ }
+ }
+
+ UIHint = bestUIHint;
+
+ if (DisplayFormat == null && DataType != null)
+ {
+ DisplayFormat = DataType.DisplayFormat;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/Metadata/Providers/CachedDataAnnotationsModelMetadata.cs b/src/System.Web.Http/Metadata/Providers/CachedDataAnnotationsModelMetadata.cs
new file mode 100644
index 00000000..913966b0
--- /dev/null
+++ b/src/System.Web.Http/Metadata/Providers/CachedDataAnnotationsModelMetadata.cs
@@ -0,0 +1,268 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Reflection;
+using System.Security;
+using System.Web.Http.Common;
+using System.Web.Http.Internal;
+using System.Web.Http.Properties;
+
+namespace System.Web.Http.Metadata.Providers
+{
+ // REVIEW: No access to HiddenInputAttribute
+ public class CachedDataAnnotationsModelMetadata : CachedModelMetadata<CachedDataAnnotationsMetadataAttributes>
+ {
+ public CachedDataAnnotationsModelMetadata(CachedDataAnnotationsModelMetadata prototype, Func<object> modelAccessor)
+ : base(prototype, modelAccessor)
+ {
+ }
+
+ public CachedDataAnnotationsModelMetadata(CachedDataAnnotationsModelMetadataProvider provider, Type containerType, Type modelType, string propertyName, IEnumerable<Attribute> attributes)
+ : base(provider, containerType, modelType, propertyName, new CachedDataAnnotationsMetadataAttributes(attributes.ToArray()))
+ {
+ }
+
+ // [SecuritySafeCritical] because it uses DataAnnotations type
+ [SecuritySafeCritical]
+ protected override bool ComputeConvertEmptyStringToNull()
+ {
+ return PrototypeCache.DisplayFormat != null
+ ? PrototypeCache.DisplayFormat.ConvertEmptyStringToNull
+ : base.ComputeConvertEmptyStringToNull();
+ }
+
+ // [SecuritySafeCritical] because it uses DataAnnotations type
+ [SecuritySafeCritical]
+ protected override string ComputeDataTypeName()
+ {
+ if (PrototypeCache.DataType != null)
+ {
+ return PrototypeCache.DataType.ToDataTypeName();
+ }
+
+ if (PrototypeCache.DisplayFormat != null && !PrototypeCache.DisplayFormat.HtmlEncode)
+ {
+ return DataTypeUtil.HtmlTypeName;
+ }
+
+ return base.ComputeDataTypeName();
+ }
+
+ // [SecuritySafeCritical] because it uses DataAnnotations type
+ [SecuritySafeCritical]
+ protected override string ComputeDescription()
+ {
+ return PrototypeCache.Display != null
+ ? PrototypeCache.Display.GetDescription()
+ : base.ComputeDescription();
+ }
+
+ // [SecuritySafeCritical] because it uses DataAnnotations type
+ [SecuritySafeCritical]
+ protected override string ComputeDisplayFormatString()
+ {
+ return PrototypeCache.DisplayFormat != null
+ ? PrototypeCache.DisplayFormat.DataFormatString
+ : base.ComputeDisplayFormatString();
+ }
+
+ // [SecuritySafeCritical] because it uses DataAnnotations type
+ [SecuritySafeCritical]
+ protected override string ComputeDisplayName()
+ {
+ string result = null;
+
+ if (PrototypeCache.Display != null)
+ {
+ result = PrototypeCache.Display.GetName();
+ }
+
+ if (result == null && PrototypeCache.DisplayName != null)
+ {
+ result = PrototypeCache.DisplayName.DisplayName;
+ }
+
+ return result ?? base.ComputeDisplayName();
+ }
+
+ // [SecuritySafeCritical] because it uses DataAnnotations type
+ [SecuritySafeCritical]
+ protected override string ComputeEditFormatString()
+ {
+ if (PrototypeCache.DisplayFormat != null && PrototypeCache.DisplayFormat.ApplyFormatInEditMode)
+ {
+ return PrototypeCache.DisplayFormat.DataFormatString;
+ }
+
+ return base.ComputeEditFormatString();
+ }
+
+ protected override bool ComputeHideSurroundingHtml()
+ {
+ return /*PrototypeCache.HiddenInput != null
+ ? !PrototypeCache.HiddenInput.DisplayValue
+ :*/ base.ComputeHideSurroundingHtml();
+ }
+
+ // [SecuritySafeCritical] because it uses DataAnnotations type EditableAttribute
+ [SecuritySafeCritical]
+ protected override bool ComputeIsReadOnly()
+ {
+ if (PrototypeCache.Editable != null)
+ {
+ return !PrototypeCache.Editable.AllowEdit;
+ }
+
+ if (PrototypeCache.ReadOnly != null)
+ {
+ return PrototypeCache.ReadOnly.IsReadOnly;
+ }
+
+ return base.ComputeIsReadOnly();
+ }
+
+ // [SecuritySafeCritical] because it uses DataAnnotations type RequiredAttribute
+ [SecuritySafeCritical]
+ protected override bool ComputeIsRequired()
+ {
+ return PrototypeCache.Required != null
+ ? true
+ : base.ComputeIsRequired();
+ }
+
+ // [SecuritySafeCritical] because it uses DataAnnotations type DisplayFormatAttribute
+ [SecuritySafeCritical]
+ protected override string ComputeNullDisplayText()
+ {
+ return PrototypeCache.DisplayFormat != null
+ ? PrototypeCache.DisplayFormat.NullDisplayText
+ : base.ComputeNullDisplayText();
+ }
+
+ // [SecuritySafeCritical] because it uses DataAnnotations type OrderAttribute
+ [SecuritySafeCritical]
+ protected override int ComputeOrder()
+ {
+ int? result = null;
+
+ if (PrototypeCache.Display != null)
+ {
+ result = PrototypeCache.Display.GetOrder();
+ }
+
+ return result ?? base.ComputeOrder();
+ }
+
+ // [SecuritySafeCritical] because it uses DataAnnotations type DisplayAttribute
+ [SecuritySafeCritical]
+ protected override string ComputeShortDisplayName()
+ {
+ return PrototypeCache.Display != null
+ ? PrototypeCache.Display.GetShortName()
+ : base.ComputeShortDisplayName();
+ }
+
+ // [SecuritySafeCritical] because it uses DataAnnotations type ScaffoldColumnAttribute
+ [SecuritySafeCritical]
+ protected override bool ComputeShowForDisplay()
+ {
+ return PrototypeCache.ScaffoldColumn != null
+ ? PrototypeCache.ScaffoldColumn.Scaffold
+ : base.ComputeShowForDisplay();
+ }
+
+ // [SecuritySafeCritical] because it uses DataAnnotations type ScaffoldColumnAttribute
+ [SecuritySafeCritical]
+ protected override bool ComputeShowForEdit()
+ {
+ return PrototypeCache.ScaffoldColumn != null
+ ? PrototypeCache.ScaffoldColumn.Scaffold
+ : base.ComputeShowForEdit();
+ }
+
+ // [SecuritySafeCritical] because it uses DataAnnotations type DisplayColumnAttribute
+ [SecuritySafeCritical]
+ protected override string ComputeSimpleDisplayText()
+ {
+ if (Model != null)
+ {
+ if (PrototypeCache.DisplayColumn != null && !String.IsNullOrEmpty(PrototypeCache.DisplayColumn.DisplayColumn))
+ {
+ PropertyInfo displayColumnProperty = GetPropertyInfoViaReflection(PrototypeCache.DisplayColumn.DisplayColumn);
+ ValidateDisplayColumnAttribute(PrototypeCache.DisplayColumn, displayColumnProperty, ModelType);
+
+ string simpleDisplayTextValue;
+ if (TryGetPropertyValueAsStringViaReflection(displayColumnProperty, out simpleDisplayTextValue))
+ {
+ return simpleDisplayTextValue;
+ }
+ }
+ }
+
+ return base.ComputeSimpleDisplayText();
+ }
+
+ // [SecuritySafeCritical] because it uses DataAnnotations type UIHintAttribute
+ [SecuritySafeCritical]
+ protected override string ComputeTemplateHint()
+ {
+ if (PrototypeCache.UIHint != null)
+ {
+ return PrototypeCache.UIHint.UIHint;
+ }
+#if false
+ if (PrototypeCache.HiddenInput != null)
+ {
+ return "HiddenInput";
+ }
+#endif
+ return base.ComputeTemplateHint();
+ }
+
+ // [SecuritySafeCritical] because it uses DataAnnotations type DisplayAttribute
+ [SecuritySafeCritical]
+ protected override string ComputeWatermark()
+ {
+ return PrototypeCache.Display != null
+ ? PrototypeCache.Display.GetPrompt()
+ : base.ComputeWatermark();
+ }
+
+ // [SecuritySafeCritical] because it uses DataAnnotations type DisplayColumnAttribute
+ [SecuritySafeCritical]
+ private static void ValidateDisplayColumnAttribute(DisplayColumnAttribute displayColumnAttribute, PropertyInfo displayColumnProperty, Type modelType)
+ {
+ if (displayColumnProperty == null)
+ {
+ throw Error.InvalidOperation(SRResources.DataAnnotationsModelMetadataProvider_UnknownProperty, modelType, displayColumnAttribute.DisplayColumn);
+ }
+ if (displayColumnProperty.GetGetMethod() == null)
+ {
+ throw Error.InvalidOperation(SRResources.DataAnnotationsModelMetadataProvider_UnreadableProperty, modelType, displayColumnAttribute.DisplayColumn);
+ }
+ }
+
+ // This method is [SecurityTransparent] and is used only so that we don't call Reflection from
+ // a [SecuritySafeCritical] method. Reflection works differently when called from [SecurityTransparent].
+ private PropertyInfo GetPropertyInfoViaReflection(string propertyName)
+ {
+ return ModelType.GetProperty(propertyName, BindingFlags.Public | BindingFlags.IgnoreCase | BindingFlags.Instance);
+ }
+
+ // This method is [SecurityTransparent] and is used only so that we don't call Reflection from
+ // a [SecuritySafeCritical] method. Reflection works differently when called from [SecurityTransparent].
+ private bool TryGetPropertyValueAsStringViaReflection(PropertyInfo propertyInfo, out string valueAsString)
+ {
+ // PropertyInfo.GetValue() done here to avoid danger of the reflected method call under [SecuritySafeCritical]
+ object value = propertyInfo.GetValue(Model, new object[0]);
+ if (value == null)
+ {
+ valueAsString = null;
+ return false;
+ }
+
+ valueAsString = value.ToString();
+ return true;
+ }
+ }
+}
diff --git a/src/System.Web.Http/Metadata/Providers/CachedDataAnnotationsModelMetadataProvider.cs b/src/System.Web.Http/Metadata/Providers/CachedDataAnnotationsModelMetadataProvider.cs
new file mode 100644
index 00000000..54e7f4f4
--- /dev/null
+++ b/src/System.Web.Http/Metadata/Providers/CachedDataAnnotationsModelMetadataProvider.cs
@@ -0,0 +1,17 @@
+using System.Collections.Generic;
+
+namespace System.Web.Http.Metadata.Providers
+{
+ public class CachedDataAnnotationsModelMetadataProvider : CachedAssociatedMetadataProvider<CachedDataAnnotationsModelMetadata>
+ {
+ protected override CachedDataAnnotationsModelMetadata CreateMetadataPrototype(IEnumerable<Attribute> attributes, Type containerType, Type modelType, string propertyName)
+ {
+ return new CachedDataAnnotationsModelMetadata(this, containerType, modelType, propertyName, attributes);
+ }
+
+ protected override CachedDataAnnotationsModelMetadata CreateMetadataFromPrototype(CachedDataAnnotationsModelMetadata prototype, Func<object> modelAccessor)
+ {
+ return new CachedDataAnnotationsModelMetadata(prototype, modelAccessor);
+ }
+ }
+}
diff --git a/src/System.Web.Http/Metadata/Providers/CachedModelMetadata.cs b/src/System.Web.Http/Metadata/Providers/CachedModelMetadata.cs
new file mode 100644
index 00000000..5735ceca
--- /dev/null
+++ b/src/System.Web.Http/Metadata/Providers/CachedModelMetadata.cs
@@ -0,0 +1,415 @@
+namespace System.Web.Http.Metadata.Providers
+{
+ // This class assumes that model metadata is expensive to create, and allows the user to
+ // stash a cache object that can be copied around as a prototype to make creation and
+ // computation quicker. It delegates the retrieval of values to getter methods, the results
+ // of which are cached on a per-metadata-instance basis.
+ //
+ // This allows flexible caching strategies: either caching the source of information across
+ // instances or caching of the actual information itself, depending on what the developer
+ // decides to put into the prototype cache.
+ public abstract class CachedModelMetadata<TPrototypeCache> : ModelMetadata
+ {
+ private bool _convertEmptyStringToNull;
+ private string _dataTypeName;
+ private string _description;
+ private string _displayFormatString;
+ private string _displayName;
+ private string _editFormatString;
+ private bool _hideSurroundingHtml;
+ private bool _isReadOnly;
+ private bool _isRequired;
+ private string _nullDisplayText;
+ private int _order;
+ private string _shortDisplayName;
+ private bool _showForDisplay;
+ private bool _showForEdit;
+ private string _templateHint;
+ private string _watermark;
+
+ private bool _convertEmptyStringToNullComputed;
+ private bool _dataTypeNameComputed;
+ private bool _descriptionComputed;
+ private bool _displayFormatStringComputed;
+ private bool _displayNameComputed;
+ private bool _editFormatStringComputed;
+ private bool _hideSurroundingHtmlComputed;
+ private bool _isReadOnlyComputed;
+ private bool _isRequiredComputed;
+ private bool _nullDisplayTextComputed;
+ private bool _orderComputed;
+ private bool _shortDisplayNameComputed;
+ private bool _showForDisplayComputed;
+ private bool _showForEditComputed;
+ private bool _templateHintComputed;
+ private bool _watermarkComputed;
+
+ // Constructor for creating real instances of the metadata class based on a prototype
+ protected CachedModelMetadata(CachedModelMetadata<TPrototypeCache> prototype, Func<object> modelAccessor)
+ : base(prototype.Provider, prototype.ContainerType, modelAccessor, prototype.ModelType, prototype.PropertyName)
+ {
+ PrototypeCache = prototype.PrototypeCache;
+ }
+
+ // Constructor for creating the prototype instances of the metadata class
+ protected CachedModelMetadata(CachedDataAnnotationsModelMetadataProvider provider, Type containerType, Type modelType, string propertyName, TPrototypeCache prototypeCache)
+ : base(provider, containerType, null /* modelAccessor */, modelType, propertyName)
+ {
+ PrototypeCache = prototypeCache;
+ }
+
+ public sealed override bool ConvertEmptyStringToNull
+ {
+ get
+ {
+ return CacheOrCompute(ComputeConvertEmptyStringToNull,
+ ref _convertEmptyStringToNull,
+ ref _convertEmptyStringToNullComputed);
+ }
+ set
+ {
+ _convertEmptyStringToNull = value;
+ _convertEmptyStringToNullComputed = true;
+ }
+ }
+
+ public sealed override string DataTypeName
+ {
+ get
+ {
+ return CacheOrCompute(ComputeDataTypeName,
+ ref _dataTypeName,
+ ref _dataTypeNameComputed);
+ }
+ set
+ {
+ _dataTypeName = value;
+ _dataTypeNameComputed = true;
+ }
+ }
+
+ public sealed override string Description
+ {
+ get
+ {
+ return CacheOrCompute(ComputeDescription,
+ ref _description,
+ ref _descriptionComputed);
+ }
+ set
+ {
+ _description = value;
+ _descriptionComputed = true;
+ }
+ }
+
+ public sealed override string DisplayFormatString
+ {
+ get
+ {
+ return CacheOrCompute(ComputeDisplayFormatString,
+ ref _displayFormatString,
+ ref _displayFormatStringComputed);
+ }
+ set
+ {
+ _displayFormatString = value;
+ _displayFormatStringComputed = true;
+ }
+ }
+
+ public sealed override string DisplayName
+ {
+ get
+ {
+ return CacheOrCompute(ComputeDisplayName,
+ ref _displayName,
+ ref _displayNameComputed);
+ }
+ set
+ {
+ _displayName = value;
+ _displayNameComputed = true;
+ }
+ }
+
+ public sealed override string EditFormatString
+ {
+ get
+ {
+ return CacheOrCompute(ComputeEditFormatString,
+ ref _editFormatString,
+ ref _editFormatStringComputed);
+ }
+ set
+ {
+ _editFormatString = value;
+ _editFormatStringComputed = true;
+ }
+ }
+
+ public sealed override bool HideSurroundingHtml
+ {
+ get
+ {
+ return CacheOrCompute(ComputeHideSurroundingHtml,
+ ref _hideSurroundingHtml,
+ ref _hideSurroundingHtmlComputed);
+ }
+ set
+ {
+ _hideSurroundingHtml = value;
+ _hideSurroundingHtmlComputed = true;
+ }
+ }
+
+ public sealed override bool IsReadOnly
+ {
+ get
+ {
+ return CacheOrCompute(ComputeIsReadOnly,
+ ref _isReadOnly,
+ ref _isReadOnlyComputed);
+ }
+ set
+ {
+ _isReadOnly = value;
+ _isReadOnlyComputed = true;
+ }
+ }
+
+ public sealed override bool IsRequired
+ {
+ get
+ {
+ return CacheOrCompute(ComputeIsRequired,
+ ref _isRequired,
+ ref _isRequiredComputed);
+ }
+ set
+ {
+ _isRequired = value;
+ _isRequiredComputed = true;
+ }
+ }
+
+ public sealed override string NullDisplayText
+ {
+ get
+ {
+ return CacheOrCompute(ComputeNullDisplayText,
+ ref _nullDisplayText,
+ ref _nullDisplayTextComputed);
+ }
+ set
+ {
+ _nullDisplayText = value;
+ _nullDisplayTextComputed = true;
+ }
+ }
+
+ public sealed override int Order
+ {
+ get
+ {
+ return CacheOrCompute(ComputeOrder,
+ ref _order,
+ ref _orderComputed);
+ }
+ set
+ {
+ _order = value;
+ _orderComputed = true;
+ }
+ }
+
+ protected TPrototypeCache PrototypeCache { get; set; }
+
+ public sealed override string ShortDisplayName
+ {
+ get
+ {
+ return CacheOrCompute(ComputeShortDisplayName,
+ ref _shortDisplayName,
+ ref _shortDisplayNameComputed);
+ }
+ set
+ {
+ _shortDisplayName = value;
+ _shortDisplayNameComputed = true;
+ }
+ }
+
+ public sealed override bool ShowForDisplay
+ {
+ get
+ {
+ return CacheOrCompute(ComputeShowForDisplay,
+ ref _showForDisplay,
+ ref _showForDisplayComputed);
+ }
+ set
+ {
+ _showForDisplay = value;
+ _showForDisplayComputed = true;
+ }
+ }
+
+ public sealed override bool ShowForEdit
+ {
+ get
+ {
+ return CacheOrCompute(ComputeShowForEdit,
+ ref _showForEdit,
+ ref _showForEditComputed);
+ }
+ set
+ {
+ _showForEdit = value;
+ _showForEditComputed = true;
+ }
+ }
+
+ public sealed override string SimpleDisplayText
+ {
+ get
+ {
+ // This is already cached in the base class with an appropriate override available
+ return base.SimpleDisplayText;
+ }
+ set { base.SimpleDisplayText = value; }
+ }
+
+ public sealed override string TemplateHint
+ {
+ get
+ {
+ return CacheOrCompute(ComputeTemplateHint,
+ ref _templateHint,
+ ref _templateHintComputed);
+ }
+ set
+ {
+ _templateHint = value;
+ _templateHintComputed = true;
+ }
+ }
+
+ public sealed override string Watermark
+ {
+ get
+ {
+ return CacheOrCompute(ComputeWatermark,
+ ref _watermark,
+ ref _watermarkComputed);
+ }
+ set
+ {
+ _watermark = value;
+ _watermarkComputed = true;
+ }
+ }
+
+ private static TResult CacheOrCompute<TResult>(Func<TResult> computeThunk, ref TResult value, ref bool computed)
+ {
+ if (!computed)
+ {
+ value = computeThunk();
+ computed = true;
+ }
+
+ return value;
+ }
+
+ protected virtual bool ComputeConvertEmptyStringToNull()
+ {
+ return base.ConvertEmptyStringToNull;
+ }
+
+ protected virtual string ComputeDataTypeName()
+ {
+ return base.DataTypeName;
+ }
+
+ protected virtual string ComputeDescription()
+ {
+ return base.Description;
+ }
+
+ protected virtual string ComputeDisplayFormatString()
+ {
+ return base.DisplayFormatString;
+ }
+
+ protected virtual string ComputeDisplayName()
+ {
+ return base.DisplayName;
+ }
+
+ protected virtual string ComputeEditFormatString()
+ {
+ return base.EditFormatString;
+ }
+
+ protected virtual bool ComputeHideSurroundingHtml()
+ {
+ return base.HideSurroundingHtml;
+ }
+
+ protected virtual bool ComputeIsReadOnly()
+ {
+ return base.IsReadOnly;
+ }
+
+ protected virtual bool ComputeIsRequired()
+ {
+ return base.IsRequired;
+ }
+
+ protected virtual string ComputeNullDisplayText()
+ {
+ return base.NullDisplayText;
+ }
+
+ protected virtual int ComputeOrder()
+ {
+ return base.Order;
+ }
+
+ protected virtual string ComputeShortDisplayName()
+ {
+ return base.ShortDisplayName;
+ }
+
+ protected virtual bool ComputeShowForDisplay()
+ {
+ return base.ShowForDisplay;
+ }
+
+ protected virtual bool ComputeShowForEdit()
+ {
+ return base.ShowForEdit;
+ }
+
+ protected virtual string ComputeSimpleDisplayText()
+ {
+ return base.GetSimpleDisplayText();
+ }
+
+ protected virtual string ComputeTemplateHint()
+ {
+ return base.TemplateHint;
+ }
+
+ protected virtual string ComputeWatermark()
+ {
+ return base.Watermark;
+ }
+
+ protected sealed override string GetSimpleDisplayText()
+ {
+ // Rename for consistency
+ return ComputeSimpleDisplayText();
+ }
+ }
+}
diff --git a/src/System.Web.Http/Metadata/Providers/EmptyMetadataProvider.cs b/src/System.Web.Http/Metadata/Providers/EmptyMetadataProvider.cs
new file mode 100644
index 00000000..a23f586b
--- /dev/null
+++ b/src/System.Web.Http/Metadata/Providers/EmptyMetadataProvider.cs
@@ -0,0 +1,12 @@
+using System.Collections.Generic;
+
+namespace System.Web.Http.Metadata.Providers
+{
+ public class EmptyModelMetadataProvider : AssociatedMetadataProvider
+ {
+ protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
+ {
+ return new ModelMetadata(this, containerType, modelAccessor, modelType, propertyName);
+ }
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/Binders/ArrayModelBinder.cs b/src/System.Web.Http/ModelBinding/Binders/ArrayModelBinder.cs
new file mode 100644
index 00000000..4fb44e9d
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/Binders/ArrayModelBinder.cs
@@ -0,0 +1,15 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Web.Http.Controllers;
+
+namespace System.Web.Http.ModelBinding.Binders
+{
+ public class ArrayModelBinder<TElement> : CollectionModelBinder<TElement>
+ {
+ protected override bool CreateOrReplaceCollection(HttpActionContext actionContext, ModelBindingContext bindingContext, IList<TElement> newCollection)
+ {
+ bindingContext.Model = newCollection.ToArray();
+ return true;
+ }
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/Binders/ArrayModelBinderProvider.cs b/src/System.Web.Http/ModelBinding/Binders/ArrayModelBinderProvider.cs
new file mode 100644
index 00000000..d92f0bbd
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/Binders/ArrayModelBinderProvider.cs
@@ -0,0 +1,21 @@
+using System.Web.Http.Controllers;
+
+namespace System.Web.Http.ModelBinding.Binders
+{
+ public sealed class ArrayModelBinderProvider : ModelBinderProvider
+ {
+ public override IModelBinder GetBinder(HttpActionContext actionContext, ModelBindingContext bindingContext)
+ {
+ ModelBindingHelper.ValidateBindingContext(bindingContext);
+
+ if (!bindingContext.ModelMetadata.IsReadOnly && bindingContext.ModelType.IsArray &&
+ bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName))
+ {
+ Type elementType = bindingContext.ModelType.GetElementType();
+ return (IModelBinder)Activator.CreateInstance(typeof(ArrayModelBinder<>).MakeGenericType(elementType));
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/Binders/BinaryDataModelBinderProvider.cs b/src/System.Web.Http/ModelBinding/Binders/BinaryDataModelBinderProvider.cs
new file mode 100644
index 00000000..5a0978ee
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/Binders/BinaryDataModelBinderProvider.cs
@@ -0,0 +1,80 @@
+using System.Data.Linq;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Web.Http.Controllers;
+using System.Web.Http.ValueProviders;
+
+namespace System.Web.Http.ModelBinding.Binders
+{
+ // This is a single provider that can work with both byte[] and Binary models.
+ public sealed class BinaryDataModelBinderProvider : ModelBinderProvider
+ {
+ private static readonly ModelBinderProvider[] _providers = new ModelBinderProvider[]
+ {
+ new SimpleModelBinderProvider(typeof(byte[]), new ByteArrayExtensibleModelBinder()),
+ new SimpleModelBinderProvider(typeof(Binary), new LinqBinaryExtensibleModelBinder())
+ };
+
+ public override IModelBinder GetBinder(HttpActionContext actionContext, ModelBindingContext bindingContext)
+ {
+ return (from provider in _providers
+ let binder = provider.GetBinder(actionContext, bindingContext)
+ where binder != null
+ select binder).FirstOrDefault();
+ }
+
+ // This is essentially a clone of the ByteArrayModelBinder from core
+ private class ByteArrayExtensibleModelBinder : IModelBinder
+ {
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We want to ignore when the data is corrupted")]
+ [SuppressMessage("Microsoft.Globalization", "CA1304:SpecifyCultureInfo", MessageId = "System.Web.Http.ValueProviders.ValueProviderResult.ConvertTo(System.Type)", Justification = "The ValueProviderResult already has the necessary context to perform a culture-aware conversion.")]
+ public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
+ {
+ ModelBindingHelper.ValidateBindingContext(bindingContext);
+ ValueProviderResult valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
+
+ // case 1: there was no <input ... /> element containing this data
+ if (valueProviderResult == null)
+ {
+ return false;
+ }
+
+ string base64String = (string)valueProviderResult.ConvertTo(typeof(string));
+
+ // case 2: there was an <input ... /> element but it was left blank
+ if (String.IsNullOrEmpty(base64String))
+ {
+ return false;
+ }
+
+ // Future proofing. If the byte array is actually an instance of System.Data.Linq.Binary
+ // then we need to remove these quotes put in place by the ToString() method.
+ string realValue = base64String.Replace("\"", String.Empty);
+ try
+ {
+ bindingContext.Model = ConvertByteArray(Convert.FromBase64String(realValue));
+ return true;
+ }
+ catch
+ {
+ // corrupt data - just ignore
+ return false;
+ }
+ }
+
+ protected virtual object ConvertByteArray(byte[] originalModel)
+ {
+ return originalModel;
+ }
+ }
+
+ // This is essentially a clone of the LinqBinaryModelBinder from core
+ private class LinqBinaryExtensibleModelBinder : ByteArrayExtensibleModelBinder
+ {
+ protected override object ConvertByteArray(byte[] originalModel)
+ {
+ return new Binary(originalModel);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/Binders/CollectionModelBinder.cs b/src/System.Web.Http/ModelBinding/Binders/CollectionModelBinder.cs
new file mode 100644
index 00000000..b4552754
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/Binders/CollectionModelBinder.cs
@@ -0,0 +1,133 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.Web.Http.Controllers;
+using System.Web.Http.Internal;
+using System.Web.Http.ValueProviders;
+using System.Web.Http.ValueProviders.Providers;
+
+namespace System.Web.Http.ModelBinding.Binders
+{
+ public class CollectionModelBinder<TElement> : IModelBinder
+ {
+ // Used when the ValueProvider contains the collection to be bound as multiple elements, e.g. foo[0], foo[1].
+ private static List<TElement> BindComplexCollection(HttpActionContext actionContext, ModelBindingContext bindingContext)
+ {
+ string indexPropertyName = ModelBindingHelper.CreatePropertyModelName(bindingContext.ModelName, "index");
+ ValueProviderResult valueProviderResultIndex = bindingContext.ValueProvider.GetValue(indexPropertyName);
+ IEnumerable<string> indexNames = CollectionModelBinderUtil.GetIndexNamesFromValueProviderResult(valueProviderResultIndex);
+ return BindComplexCollectionFromIndexes(actionContext, bindingContext, indexNames);
+ }
+
+ internal static List<TElement> BindComplexCollectionFromIndexes(HttpActionContext actionContext, ModelBindingContext bindingContext, IEnumerable<string> indexNames)
+ {
+ bool indexNamesIsFinite;
+ if (indexNames != null)
+ {
+ indexNamesIsFinite = true;
+ }
+ else
+ {
+ indexNamesIsFinite = false;
+ indexNames = CollectionModelBinderUtil.GetZeroBasedIndexes();
+ }
+
+ List<TElement> boundCollection = new List<TElement>();
+ foreach (string indexName in indexNames)
+ {
+ string fullChildName = ModelBindingHelper.CreateIndexModelName(bindingContext.ModelName, indexName);
+ ModelBindingContext childBindingContext = new ModelBindingContext(bindingContext)
+ {
+ ModelMetadata = actionContext.GetMetadataProvider().GetMetadataForType(null, typeof(TElement)),
+ ModelName = fullChildName
+ };
+
+ bool didBind = false;
+ object boundValue = null;
+ IModelBinder childBinder;
+ if (actionContext.TryGetBinder(childBindingContext, out childBinder))
+ {
+ didBind = childBinder.BindModel(actionContext, childBindingContext);
+ if (didBind)
+ {
+ boundValue = childBindingContext.Model;
+
+ // merge validation up
+ bindingContext.ValidationNode.ChildNodes.Add(childBindingContext.ValidationNode);
+ }
+ }
+
+ // infinite size collection stops on first bind failure
+ if (!didBind && !indexNamesIsFinite)
+ {
+ break;
+ }
+
+ boundCollection.Add(ModelBindingHelper.CastOrDefault<TElement>(boundValue));
+ }
+
+ return boundCollection;
+ }
+
+ public virtual bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
+ {
+ ModelBindingHelper.ValidateBindingContext(bindingContext);
+
+ ValueProviderResult valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
+ List<TElement> boundCollection = (valueProviderResult != null)
+ ? BindSimpleCollection(actionContext, bindingContext, valueProviderResult.RawValue, valueProviderResult.Culture)
+ : BindComplexCollection(actionContext, bindingContext);
+
+ bool retVal = CreateOrReplaceCollection(actionContext, bindingContext, boundCollection);
+ return retVal;
+ }
+
+ // Used when the ValueProvider contains the collection to be bound as a single element, e.g. the raw value
+ // is [ "1", "2" ] and needs to be converted to an int[].
+ internal static List<TElement> BindSimpleCollection(HttpActionContext actionContext, ModelBindingContext bindingContext, object rawValue, CultureInfo culture)
+ {
+ if (rawValue == null)
+ {
+ return null; // nothing to do
+ }
+
+ List<TElement> boundCollection = new List<TElement>();
+
+ object[] rawValueArray = ModelBindingHelper.RawValueToObjectArray(rawValue);
+ foreach (object rawValueElement in rawValueArray)
+ {
+ ModelBindingContext innerBindingContext = new ModelBindingContext(bindingContext)
+ {
+ ModelMetadata = actionContext.GetMetadataProvider().GetMetadataForType(null, typeof(TElement)),
+ ModelName = bindingContext.ModelName,
+ ValueProvider = new CompositeValueProvider
+ {
+ new ElementalValueProvider(bindingContext.ModelName, rawValueElement, culture), // our temporary provider goes at the front of the list
+ bindingContext.ValueProvider
+ }
+ };
+
+ object boundValue = null;
+ IModelBinder childBinder;
+ if (actionContext.TryGetBinder(innerBindingContext, out childBinder))
+ {
+ if (childBinder.BindModel(actionContext, innerBindingContext))
+ {
+ boundValue = innerBindingContext.Model;
+ bindingContext.ValidationNode.ChildNodes.Add(innerBindingContext.ValidationNode);
+ }
+ }
+ boundCollection.Add(ModelBindingHelper.CastOrDefault<TElement>(boundValue));
+ }
+
+ return boundCollection;
+ }
+
+ // Extensibility point that allows the bound collection to be manipulated or transformed before
+ // being returned from the binder.
+ protected virtual bool CreateOrReplaceCollection(HttpActionContext actionContext, ModelBindingContext bindingContext, IList<TElement> newCollection)
+ {
+ CollectionModelBinderUtil.CreateOrReplaceCollection(bindingContext, newCollection, () => new List<TElement>());
+ return true;
+ }
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/Binders/CollectionModelBinderProvider.cs b/src/System.Web.Http/ModelBinding/Binders/CollectionModelBinderProvider.cs
new file mode 100644
index 00000000..012351ef
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/Binders/CollectionModelBinderProvider.cs
@@ -0,0 +1,23 @@
+using System.Collections.Generic;
+using System.Web.Http.Controllers;
+using System.Web.Http.Internal;
+
+namespace System.Web.Http.ModelBinding.Binders
+{
+ public sealed class CollectionModelBinderProvider : ModelBinderProvider
+ {
+ public override IModelBinder GetBinder(HttpActionContext actionContext, ModelBindingContext bindingContext)
+ {
+ ModelBindingHelper.ValidateBindingContext(bindingContext);
+
+ if (bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName))
+ {
+ return CollectionModelBinderUtil.GetGenericBinder(typeof(ICollection<>), typeof(List<>), typeof(CollectionModelBinder<>), bindingContext.ModelMetadata);
+ }
+ else
+ {
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/Binders/ComplexModelDto.cs b/src/System.Web.Http/ModelBinding/Binders/ComplexModelDto.cs
new file mode 100644
index 00000000..449c257c
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/Binders/ComplexModelDto.cs
@@ -0,0 +1,39 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Web.Http.Common;
+using System.Web.Http.Metadata;
+
+namespace System.Web.Http.ModelBinding.Binders
+{
+ // Describes a complex model, but uses a collection rather than individual properties as the data store.
+ public class ComplexModelDto
+ {
+ public ComplexModelDto(ModelMetadata modelMetadata, IEnumerable<ModelMetadata> propertyMetadata)
+ {
+ if (modelMetadata == null)
+ {
+ throw Error.ArgumentNull("modelMetadata");
+ }
+
+ if (propertyMetadata == null)
+ {
+ throw Error.ArgumentNull("propertyMetadata");
+ }
+
+ ModelMetadata = modelMetadata;
+ PropertyMetadata = new Collection<ModelMetadata>(propertyMetadata.ToList());
+ Results = new Dictionary<ModelMetadata, ComplexModelDtoResult>();
+ }
+
+ public ModelMetadata ModelMetadata { get; private set; }
+
+ public Collection<ModelMetadata> PropertyMetadata { get; private set; }
+
+ // Contains entries corresponding to each property against which binding was
+ // attempted. If binding failed, the entry's value will be null. If binding
+ // was never attempted, this dictionary will not contain a corresponding
+ // entry.
+ public IDictionary<ModelMetadata, ComplexModelDtoResult> Results { get; private set; }
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/Binders/ComplexModelDtoModelBinder.cs b/src/System.Web.Http/ModelBinding/Binders/ComplexModelDtoModelBinder.cs
new file mode 100644
index 00000000..eaa5722b
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/Binders/ComplexModelDtoModelBinder.cs
@@ -0,0 +1,39 @@
+using System.Web.Http.Controllers;
+using System.Web.Http.Metadata;
+
+namespace System.Web.Http.ModelBinding.Binders
+{
+ public sealed class ComplexModelDtoModelBinder : IModelBinder
+ {
+ public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
+ {
+ ModelBindingHelper.ValidateBindingContext(bindingContext, typeof(ComplexModelDto), false /* allowNullModel */);
+
+ ComplexModelDto dto = (ComplexModelDto)bindingContext.Model;
+ foreach (ModelMetadata propertyMetadata in dto.PropertyMetadata)
+ {
+ ModelBindingContext propertyBindingContext = new ModelBindingContext(bindingContext)
+ {
+ ModelMetadata = propertyMetadata,
+ ModelName = ModelBindingHelper.CreatePropertyModelName(bindingContext.ModelName, propertyMetadata.PropertyName)
+ };
+
+ // bind and propagate the values
+ IModelBinder propertyBinder;
+ if (actionContext.TryGetBinder(propertyBindingContext, out propertyBinder))
+ {
+ if (propertyBinder.BindModel(actionContext, propertyBindingContext))
+ {
+ dto.Results[propertyMetadata] = new ComplexModelDtoResult(propertyBindingContext.Model, propertyBindingContext.ValidationNode);
+ }
+ else
+ {
+ dto.Results[propertyMetadata] = null;
+ }
+ }
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/Binders/ComplexModelDtoModelBinderProvider.cs b/src/System.Web.Http/ModelBinding/Binders/ComplexModelDtoModelBinderProvider.cs
new file mode 100644
index 00000000..4c0e2c63
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/Binders/ComplexModelDtoModelBinderProvider.cs
@@ -0,0 +1,24 @@
+using System.Web.Http.Controllers;
+
+namespace System.Web.Http.ModelBinding.Binders
+{
+ // Returns a binder that can bind ComplexModelDto objects.
+ public sealed class ComplexModelDtoModelBinderProvider : ModelBinderProvider
+ {
+ // This is really just a simple binder.
+ private static readonly SimpleModelBinderProvider _underlyingProvider = GetUnderlyingProvider();
+
+ public override IModelBinder GetBinder(HttpActionContext actionContext, ModelBindingContext bindingContext)
+ {
+ return _underlyingProvider.GetBinder(actionContext, bindingContext);
+ }
+
+ private static SimpleModelBinderProvider GetUnderlyingProvider()
+ {
+ return new SimpleModelBinderProvider(typeof(ComplexModelDto), new ComplexModelDtoModelBinder())
+ {
+ SuppressPrefixCheck = true
+ };
+ }
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/Binders/ComplexModelDtoResult.cs b/src/System.Web.Http/ModelBinding/Binders/ComplexModelDtoResult.cs
new file mode 100644
index 00000000..15fa9a60
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/Binders/ComplexModelDtoResult.cs
@@ -0,0 +1,23 @@
+using System.Web.Http.Common;
+using System.Web.Http.Validation;
+
+namespace System.Web.Http.ModelBinding.Binders
+{
+ public sealed class ComplexModelDtoResult
+ {
+ public ComplexModelDtoResult(object model, ModelValidationNode validationNode)
+ {
+ if (validationNode == null)
+ {
+ throw Error.ArgumentNull("validationNode");
+ }
+
+ Model = model;
+ ValidationNode = validationNode;
+ }
+
+ public object Model { get; private set; }
+
+ public ModelValidationNode ValidationNode { get; private set; }
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/Binders/CompositeModelBinder.cs b/src/System.Web.Http/ModelBinding/Binders/CompositeModelBinder.cs
new file mode 100644
index 00000000..f3d05868
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/Binders/CompositeModelBinder.cs
@@ -0,0 +1,130 @@
+using System.Collections.Generic;
+using System.Diagnostics.Contracts;
+using System.Linq;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Internal;
+using System.Web.Http.Properties;
+
+namespace System.Web.Http.ModelBinding.Binders
+{
+ /// <summary>
+ /// This class is an <see cref="IModelBinder"/> that delegates to one of a collection of
+ /// <see cref="ModelBinderProvider"/> instances.
+ /// </summary>
+ /// <remarks>
+ /// If no binder is available and the <see cref="ModelBindingContext"/> allows it,
+ /// this class tries to find a binder using an empty prefix.
+ /// </remarks>
+ public class CompositeModelBinder : IModelBinder
+ {
+ public CompositeModelBinder(IEnumerable<ModelBinderProvider> modelBinderProviders)
+ {
+ Providers = modelBinderProviders.ToList();
+ }
+
+ private List<ModelBinderProvider> Providers { get; set; }
+
+ public virtual bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
+ {
+ //// REVIEW: from MVC Futures
+ ////CheckPropertyFilter(bindingContext);
+
+ ModelBindingContext newBindingContext = CreateNewBindingContext(bindingContext, bindingContext.ModelName);
+
+ IModelBinder binder = GetBinder(actionContext, newBindingContext);
+ if (binder == null && !String.IsNullOrEmpty(bindingContext.ModelName)
+ && bindingContext.FallbackToEmptyPrefix)
+ {
+ // fallback to empty prefix?
+ newBindingContext = CreateNewBindingContext(bindingContext, String.Empty /* modelName */);
+ binder = GetBinder(actionContext, newBindingContext);
+ }
+
+ if (binder != null)
+ {
+ bool boundSuccessfully = binder.BindModel(actionContext, newBindingContext);
+ if (boundSuccessfully)
+ {
+ // run validation and return the model
+ // If we fell back to an empty prefix above and are dealing with simple types,
+ // propagate the non-blank model name through for user clarity in validation errors.
+ // Complex types will reveal their individual properties as model names and do not require this.
+ if (!newBindingContext.ModelMetadata.IsComplexType && String.IsNullOrEmpty(newBindingContext.ModelName))
+ {
+ newBindingContext.ValidationNode = new Validation.ModelValidationNode(newBindingContext.ModelMetadata, bindingContext.ModelName);
+ }
+
+ newBindingContext.ValidationNode.Validate(actionContext, null /* parentNode */);
+ bindingContext.Model = newBindingContext.Model;
+ return true;
+ }
+ }
+
+ return false; // something went wrong
+ }
+
+ //// REVIEW: from MVC Futures -- activate when Filters are in
+ ////private static void CheckPropertyFilter(ModelBindingContext bindingContext)
+ ////{
+ //// if (bindingContext.ModelType.GetProperties().Select(p => p.Name).Any(name => !bindingContext.PropertyFilter(name)))
+ //// {
+ //// throw Error.InvalidOperation(SRResources.ExtensibleModelBinderAdapter_PropertyFilterMustNotBeSet);
+ //// }
+ ////}
+
+ private IModelBinder GetBinder(HttpActionContext actionContext, ModelBindingContext bindingContext)
+ {
+ Contract.Assert(actionContext != null);
+ Contract.Assert(bindingContext != null);
+
+ //// REVIEW: this was in MVC Futures
+ ////EnsureNoBindAttribute(bindingContext.ModelType);
+
+ ModelBinderProvider providerFromAttr;
+ if (TryGetProviderFromAttributes(bindingContext.ModelType, out providerFromAttr))
+ {
+ return providerFromAttr.GetBinder(actionContext, bindingContext);
+ }
+
+ return (from provider in Providers
+ let binder = provider.GetBinder(actionContext, bindingContext)
+ where binder != null
+ select binder).FirstOrDefault();
+ }
+
+ private static ModelBindingContext CreateNewBindingContext(ModelBindingContext oldBindingContext, string modelName)
+ {
+ ModelBindingContext newBindingContext = new ModelBindingContext
+ {
+ ModelMetadata = oldBindingContext.ModelMetadata,
+ ModelName = modelName,
+ ModelState = oldBindingContext.ModelState,
+ ValueProvider = oldBindingContext.ValueProvider
+ };
+
+ return newBindingContext;
+ }
+
+ private static bool TryGetProviderFromAttributes(Type modelType, out ModelBinderProvider provider)
+ {
+ ModelBinderAttribute attr = TypeDescriptorHelper.Get(modelType).GetAttributes().OfType<ModelBinderAttribute>().FirstOrDefault();
+ if (attr == null)
+ {
+ provider = null;
+ return false;
+ }
+
+ if (typeof(ModelBinderProvider).IsAssignableFrom(attr.BinderType))
+ {
+ provider = (ModelBinderProvider)Activator.CreateInstance(attr.BinderType);
+ }
+ else
+ {
+ throw Error.InvalidOperation(SRResources.ModelBinderProviderCollection_InvalidBinderType, attr.BinderType.Name, typeof(ModelBinderProvider).Name, typeof(IModelBinder).Name);
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/Binders/CompositeModelBinderProvider.cs b/src/System.Web.Http/ModelBinding/Binders/CompositeModelBinderProvider.cs
new file mode 100644
index 00000000..c7668da8
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/Binders/CompositeModelBinderProvider.cs
@@ -0,0 +1,42 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+
+namespace System.Web.Http.ModelBinding.Binders
+{
+ public sealed class CompositeModelBinderProvider : ModelBinderProvider
+ {
+ private ModelBinderProvider[] _providers;
+
+ public CompositeModelBinderProvider()
+ {
+ }
+
+ public CompositeModelBinderProvider(IEnumerable<ModelBinderProvider> providers)
+ {
+ if (providers == null)
+ {
+ throw Error.ArgumentNull("providers");
+ }
+
+ _providers = providers.ToArray();
+ }
+
+ public IEnumerable<ModelBinderProvider> Providers
+ {
+ get { return _providers; }
+ }
+
+ public override IModelBinder GetBinder(HttpActionContext actionContext, ModelBindingContext bindingContext)
+ {
+ // Extract all providers from the resolver except the the type of the executing one (else would cause recursion),
+ // or use the set of providers we were given.
+ IEnumerable<ModelBinderProvider> providers = _providers != null
+ ? _providers
+ : actionContext.ControllerContext.Configuration.ServiceResolver.GetModelBinderProviders().Where((p) => !typeof(CompositeModelBinderProvider).IsAssignableFrom(p.GetType()));
+
+ return new CompositeModelBinder(providers);
+ }
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/Binders/DictionaryModelBinder.cs b/src/System.Web.Http/ModelBinding/Binders/DictionaryModelBinder.cs
new file mode 100644
index 00000000..cadfed88
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/Binders/DictionaryModelBinder.cs
@@ -0,0 +1,15 @@
+using System.Collections.Generic;
+using System.Web.Http.Controllers;
+using System.Web.Http.Internal;
+
+namespace System.Web.Http.ModelBinding.Binders
+{
+ public class DictionaryModelBinder<TKey, TValue> : CollectionModelBinder<KeyValuePair<TKey, TValue>>
+ {
+ protected override bool CreateOrReplaceCollection(HttpActionContext actionContext, ModelBindingContext bindingContext, IList<KeyValuePair<TKey, TValue>> newCollection)
+ {
+ CollectionModelBinderUtil.CreateOrReplaceDictionary(bindingContext, newCollection, () => new Dictionary<TKey, TValue>());
+ return true;
+ }
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/Binders/DictionaryModelBinderProvider.cs b/src/System.Web.Http/ModelBinding/Binders/DictionaryModelBinderProvider.cs
new file mode 100644
index 00000000..4f4f84f8
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/Binders/DictionaryModelBinderProvider.cs
@@ -0,0 +1,23 @@
+using System.Collections.Generic;
+using System.Web.Http.Controllers;
+using System.Web.Http.Internal;
+
+namespace System.Web.Http.ModelBinding.Binders
+{
+ public sealed class DictionaryModelBinderProvider : ModelBinderProvider
+ {
+ public override IModelBinder GetBinder(HttpActionContext actionContext, ModelBindingContext bindingContext)
+ {
+ ModelBindingHelper.ValidateBindingContext(bindingContext);
+
+ if (bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName))
+ {
+ return CollectionModelBinderUtil.GetGenericBinder(typeof(IDictionary<,>), typeof(Dictionary<,>), typeof(DictionaryModelBinder<,>), bindingContext.ModelMetadata);
+ }
+ else
+ {
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/Binders/GenericModelBinderProvider.cs b/src/System.Web.Http/ModelBinding/Binders/GenericModelBinderProvider.cs
new file mode 100644
index 00000000..b1d48b7b
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/Binders/GenericModelBinderProvider.cs
@@ -0,0 +1,137 @@
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Internal;
+using System.Web.Http.Properties;
+
+namespace System.Web.Http.ModelBinding.Binders
+{
+ // Returns a user-specified binder for a given open generic type.
+ public sealed class GenericModelBinderProvider : ModelBinderProvider
+ {
+ private readonly Func<Type[], IModelBinder> _modelBinderFactory;
+ private readonly Type _modelType;
+
+ public GenericModelBinderProvider(Type modelType, IModelBinder modelBinder)
+ {
+ if (modelType == null)
+ {
+ throw Error.ArgumentNull("modelType");
+ }
+ if (modelBinder == null)
+ {
+ throw Error.ArgumentNull("modelBinder");
+ }
+
+ ValidateParameters(modelType, null /* modelBinderType */);
+
+ _modelType = modelType;
+ _modelBinderFactory = _ => modelBinder;
+ }
+
+ public GenericModelBinderProvider(Type modelType, Type modelBinderType)
+ {
+ // The binder can be a closed type, in which case it will be instantiated directly. If the binder
+ // is an open type, the type arguments will be determined at runtime and the corresponding closed
+ // type instantiated.
+
+ if (modelType == null)
+ {
+ throw Error.ArgumentNull("modelType");
+ }
+ if (modelBinderType == null)
+ {
+ throw Error.ArgumentNull("modelBinderType");
+ }
+
+ ValidateParameters(modelType, modelBinderType);
+ bool modelBinderTypeIsOpenGeneric = modelBinderType.IsGenericTypeDefinition;
+
+ _modelType = modelType;
+ _modelBinderFactory = typeArguments =>
+ {
+ Type closedModelBinderType = modelBinderTypeIsOpenGeneric ? modelBinderType.MakeGenericType(typeArguments) : modelBinderType;
+ return (IModelBinder)Activator.CreateInstance(closedModelBinderType);
+ };
+ }
+
+ public GenericModelBinderProvider(Type modelType, Func<Type[], IModelBinder> modelBinderFactory)
+ {
+ if (modelType == null)
+ {
+ throw Error.ArgumentNull("modelType");
+ }
+ if (modelBinderFactory == null)
+ {
+ throw Error.ArgumentNull("modelBinderFactory");
+ }
+
+ ValidateParameters(modelType, null /* modelBinderType */);
+
+ _modelType = modelType;
+ _modelBinderFactory = modelBinderFactory;
+ }
+
+ public Type ModelType
+ {
+ get { return _modelType; }
+ }
+
+ public bool SuppressPrefixCheck { get; set; }
+
+ public override IModelBinder GetBinder(HttpActionContext actionContext, ModelBindingContext bindingContext)
+ {
+ ModelBindingHelper.ValidateBindingContext(bindingContext);
+
+ Type[] typeArguments = null;
+ if (ModelType.IsInterface)
+ {
+ Type matchingClosedInterface = TypeHelper.ExtractGenericInterface(bindingContext.ModelType, ModelType);
+ if (matchingClosedInterface != null)
+ {
+ typeArguments = matchingClosedInterface.GetGenericArguments();
+ }
+ }
+ else
+ {
+ typeArguments = TypeHelper.GetTypeArgumentsIfMatch(bindingContext.ModelType, ModelType);
+ }
+
+ if (typeArguments != null)
+ {
+ if (SuppressPrefixCheck || bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName))
+ {
+ return _modelBinderFactory(typeArguments);
+ }
+ }
+
+ return null;
+ }
+
+ private static void ValidateParameters(Type modelType, Type modelBinderType)
+ {
+ if (!modelType.IsGenericTypeDefinition)
+ {
+ throw Error.Argument("modelType", SRResources.GenericModelBinderProvider_ParameterMustSpecifyOpenGenericType, modelType);
+ }
+ if (modelBinderType != null)
+ {
+ if (!typeof(IModelBinder).IsAssignableFrom(modelBinderType))
+ {
+ throw Error.Argument("modelBinderType", SRResources.Common_TypeMustImplementInterface, modelBinderType, typeof(IModelBinder));
+ }
+ if (modelBinderType.IsGenericTypeDefinition)
+ {
+ if (modelType.GetGenericArguments().Length != modelBinderType.GetGenericArguments().Length)
+ {
+ throw Error.Argument("modelBinderType",
+ SRResources.GenericModelBinderProvider_TypeArgumentCountMismatch,
+ modelType,
+ modelType.GetGenericArguments().Length,
+ modelBinderType,
+ modelBinderType.GetGenericArguments().Length);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/Binders/KeyValuePairModelBinder.cs b/src/System.Web.Http/ModelBinding/Binders/KeyValuePairModelBinder.cs
new file mode 100644
index 00000000..4c3e49ca
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/Binders/KeyValuePairModelBinder.cs
@@ -0,0 +1,30 @@
+using System.Collections.Generic;
+using System.Web.Http.Controllers;
+using System.Web.Http.Internal;
+using System.Web.Http.Metadata;
+
+namespace System.Web.Http.ModelBinding.Binders
+{
+ public sealed class KeyValuePairModelBinder<TKey, TValue> : IModelBinder
+ {
+ internal ModelMetadataProvider MetadataProvider { private get; set; }
+
+ public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
+ {
+ ModelMetadataProvider metadataProvider = MetadataProvider ?? actionContext.GetMetadataProvider();
+ ModelBindingHelper.ValidateBindingContext(bindingContext, typeof(KeyValuePair<TKey, TValue>), true /* allowNullModel */);
+
+ TKey key;
+ bool keyBindingSucceeded = actionContext.TryBindStrongModel(bindingContext, "key", metadataProvider, out key);
+
+ TValue value;
+ bool valueBindingSucceeded = actionContext.TryBindStrongModel(bindingContext, "value", metadataProvider, out value);
+
+ if (keyBindingSucceeded && valueBindingSucceeded)
+ {
+ bindingContext.Model = new KeyValuePair<TKey, TValue>(key, value);
+ }
+ return keyBindingSucceeded || valueBindingSucceeded;
+ }
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/Binders/KeyValuePairModelBinderProvider.cs b/src/System.Web.Http/ModelBinding/Binders/KeyValuePairModelBinderProvider.cs
new file mode 100644
index 00000000..b28d05f4
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/Binders/KeyValuePairModelBinderProvider.cs
@@ -0,0 +1,26 @@
+using System.Collections.Generic;
+using System.Web.Http.Controllers;
+
+namespace System.Web.Http.ModelBinding.Binders
+{
+ public sealed class KeyValuePairModelBinderProvider : ModelBinderProvider
+ {
+ public override IModelBinder GetBinder(HttpActionContext actionContext, ModelBindingContext bindingContext)
+ {
+ ModelBindingHelper.ValidateBindingContext(bindingContext);
+
+ string keyFieldName = ModelBindingHelper.CreatePropertyModelName(bindingContext.ModelName, "key");
+ string valueFieldName = ModelBindingHelper.CreatePropertyModelName(bindingContext.ModelName, "value");
+
+ if (bindingContext.ValueProvider.ContainsPrefix(keyFieldName) && bindingContext.ValueProvider.ContainsPrefix(valueFieldName))
+ {
+ return ModelBindingHelper.GetPossibleBinderInstance(bindingContext.ModelType, typeof(KeyValuePair<,>) /* supported model type */, typeof(KeyValuePairModelBinder<,>) /* binder type */);
+ }
+ else
+ {
+ // 'key' or 'value' missing
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/Binders/MutableObjectModelBinder.cs b/src/System.Web.Http/ModelBinding/Binders/MutableObjectModelBinder.cs
new file mode 100644
index 00000000..4340f9ac
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/Binders/MutableObjectModelBinder.cs
@@ -0,0 +1,257 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Internal;
+using System.Web.Http.Metadata;
+using System.Web.Http.Properties;
+using System.Web.Http.Validation;
+
+namespace System.Web.Http.ModelBinding.Binders
+{
+ public class MutableObjectModelBinder : IModelBinder
+ {
+ internal ModelMetadataProvider MetadataProvider { private get; set; }
+
+ public virtual bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
+ {
+ ModelBindingHelper.ValidateBindingContext(bindingContext);
+
+ EnsureModel(actionContext, bindingContext);
+ IEnumerable<ModelMetadata> propertyMetadatas = GetMetadataForProperties(actionContext, bindingContext);
+ ComplexModelDto dto = CreateAndPopulateDto(actionContext, bindingContext, propertyMetadatas);
+
+ // post-processing, e.g. property setters and hooking up validation
+ ProcessDto(actionContext, bindingContext, dto);
+ bindingContext.ValidationNode.ValidateAllProperties = true; // complex models require full validation
+ return true;
+ }
+
+ protected virtual bool CanUpdateProperty(ModelMetadata propertyMetadata)
+ {
+ return CanUpdatePropertyInternal(propertyMetadata);
+ }
+
+ internal static bool CanUpdatePropertyInternal(ModelMetadata propertyMetadata)
+ {
+ return !propertyMetadata.IsReadOnly || CanUpdateReadOnlyProperty(propertyMetadata.ModelType);
+ }
+
+ private static bool CanUpdateReadOnlyProperty(Type propertyType)
+ {
+ // Value types have copy-by-value semantics, which prevents us from updating
+ // properties that are marked readonly.
+ if (propertyType.IsValueType)
+ {
+ return false;
+ }
+
+ // Arrays are strange beasts since their contents are mutable but their sizes aren't.
+ // Therefore we shouldn't even try to update these. Further reading:
+ // http://blogs.msdn.com/ericlippert/archive/2008/09/22/arrays-considered-somewhat-harmful.aspx
+ if (propertyType.IsArray)
+ {
+ return false;
+ }
+
+ // Special-case known immutable reference types
+ if (propertyType == typeof(string))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ private ComplexModelDto CreateAndPopulateDto(HttpActionContext actionContext, ModelBindingContext bindingContext, IEnumerable<ModelMetadata> propertyMetadatas)
+ {
+ ModelMetadataProvider metadataProvider = MetadataProvider ?? actionContext.GetMetadataProvider();
+
+ // create a DTO and call into the DTO binder
+ ComplexModelDto originalDto = new ComplexModelDto(bindingContext.ModelMetadata, propertyMetadatas);
+ ModelBindingContext dtoBindingContext = new ModelBindingContext(bindingContext)
+ {
+ ModelMetadata = metadataProvider.GetMetadataForType(() => originalDto, typeof(ComplexModelDto)),
+ ModelName = bindingContext.ModelName
+ };
+
+ IModelBinder dtoBinder = actionContext.GetBinder(dtoBindingContext);
+ dtoBinder.BindModel(actionContext, dtoBindingContext);
+ return (ComplexModelDto)dtoBindingContext.Model;
+ }
+
+ protected virtual object CreateModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
+ {
+ // If the Activator throws an exception, we want to propagate it back up the call stack, since the application
+ // developer should know that this was an invalid type to try to bind to.
+ return Activator.CreateInstance(bindingContext.ModelType);
+ }
+
+ // Called when the property setter null check failed, allows us to add our own error message to ModelState.
+ internal static EventHandler<ModelValidatedEventArgs> CreateNullCheckFailedHandler(ModelMetadata modelMetadata, object incomingValue)
+ {
+ return (sender, e) =>
+ {
+ ModelValidationNode validationNode = (ModelValidationNode)sender;
+ ModelStateDictionary modelState = e.ActionContext.ModelState;
+
+ if (modelState.IsValidField(validationNode.ModelStateKey))
+ {
+ string errorMessage = ModelBinderConfig.ValueRequiredErrorMessageProvider(e.ActionContext, modelMetadata, incomingValue);
+ if (errorMessage != null)
+ {
+ modelState.AddModelError(validationNode.ModelStateKey, errorMessage);
+ }
+ }
+ };
+ }
+
+ protected virtual void EnsureModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
+ {
+ if (bindingContext.Model == null)
+ {
+ bindingContext.ModelMetadata.Model = CreateModel(actionContext, bindingContext);
+ }
+ }
+
+ protected virtual IEnumerable<ModelMetadata> GetMetadataForProperties(HttpActionContext actionContext, ModelBindingContext bindingContext)
+ {
+ // keep a set of the required properties so that we can cross-reference bound properties later
+ HashSet<string> requiredProperties;
+ HashSet<string> skipProperties;
+ GetRequiredPropertiesCollection(bindingContext.ModelType, out requiredProperties, out skipProperties);
+
+ return from propertyMetadata in bindingContext.ModelMetadata.Properties
+ let propertyName = propertyMetadata.PropertyName
+ let shouldUpdateProperty = requiredProperties.Contains(propertyName) || !skipProperties.Contains(propertyName)
+ where shouldUpdateProperty && CanUpdateProperty(propertyMetadata)
+ select propertyMetadata;
+ }
+
+ private static object GetPropertyDefaultValue(PropertyDescriptor propertyDescriptor)
+ {
+ DefaultValueAttribute attr = propertyDescriptor.Attributes.OfType<DefaultValueAttribute>().FirstOrDefault();
+ return (attr != null) ? attr.Value : null;
+ }
+
+ internal static void GetRequiredPropertiesCollection(Type modelType, out HashSet<string> requiredProperties, out HashSet<string> skipProperties)
+ {
+ requiredProperties = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+ skipProperties = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+
+ // Use attributes on the property before attributes on the type.
+ ICustomTypeDescriptor modelDescriptor = TypeDescriptorHelper.Get(modelType);
+ PropertyDescriptorCollection propertyDescriptors = modelDescriptor.GetProperties();
+ HttpBindingBehaviorAttribute typeAttr = modelDescriptor.GetAttributes().OfType<HttpBindingBehaviorAttribute>().SingleOrDefault();
+
+ foreach (PropertyDescriptor propertyDescriptor in propertyDescriptors)
+ {
+ HttpBindingBehaviorAttribute propAttr = propertyDescriptor.Attributes.OfType<HttpBindingBehaviorAttribute>().SingleOrDefault();
+ HttpBindingBehaviorAttribute workingAttr = propAttr ?? typeAttr;
+ if (workingAttr != null)
+ {
+ switch (workingAttr.Behavior)
+ {
+ case HttpBindingBehavior.Required:
+ requiredProperties.Add(propertyDescriptor.Name);
+ break;
+
+ case HttpBindingBehavior.Never:
+ skipProperties.Add(propertyDescriptor.Name);
+ break;
+ }
+ }
+ }
+ }
+
+ internal void ProcessDto(HttpActionContext actionContext, ModelBindingContext bindingContext, ComplexModelDto dto)
+ {
+ HashSet<string> requiredProperties;
+ HashSet<string> skipProperties;
+ GetRequiredPropertiesCollection(bindingContext.ModelType, out requiredProperties, out skipProperties);
+
+ // Are all of the required fields accounted for?
+ HashSet<string> missingRequiredProperties = new HashSet<string>(requiredProperties, StringComparer.Ordinal);
+ missingRequiredProperties.ExceptWith(dto.Results.Select(r => r.Key.PropertyName));
+ string missingPropertyName = missingRequiredProperties.FirstOrDefault();
+ if (missingPropertyName != null)
+ {
+ string fullPropertyKey = ModelBindingHelper.CreatePropertyModelName(bindingContext.ModelName, missingPropertyName);
+ throw Error.InvalidOperation(SRResources.BindingBehavior_ValueNotFound, fullPropertyKey);
+ }
+
+ // for each property that was bound, call the setter, recording exceptions as necessary
+ foreach (var entry in dto.Results)
+ {
+ ModelMetadata propertyMetadata = entry.Key;
+
+ ComplexModelDtoResult dtoResult = entry.Value;
+ if (dtoResult != null)
+ {
+ SetProperty(actionContext, bindingContext, propertyMetadata, dtoResult);
+ bindingContext.ValidationNode.ChildNodes.Add(dtoResult.ValidationNode);
+ }
+ }
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We're recording this exception so that we can act on it later.")]
+ protected virtual void SetProperty(HttpActionContext actionContext, ModelBindingContext bindingContext, ModelMetadata propertyMetadata, ComplexModelDtoResult dtoResult)
+ {
+ PropertyDescriptor propertyDescriptor = TypeDescriptorHelper.Get(bindingContext.ModelType).GetProperties().Find(propertyMetadata.PropertyName, true /* ignoreCase */);
+ if (propertyDescriptor == null || propertyDescriptor.IsReadOnly)
+ {
+ return; // nothing to do
+ }
+
+ object value = dtoResult.Model ?? GetPropertyDefaultValue(propertyDescriptor);
+ propertyMetadata.Model = value;
+
+ // 'Required' validators need to run first so that we can provide useful error messages if
+ // the property setters throw, e.g. if we're setting entity keys to null. See comments in
+ // DefaultModelBinder.SetProperty() for more information.
+ if (value == null)
+ {
+ string modelStateKey = dtoResult.ValidationNode.ModelStateKey;
+ if (bindingContext.ModelState.IsValidField(modelStateKey))
+ {
+ ModelValidator requiredValidator = actionContext.GetValidators(propertyMetadata).Where(v => v.IsRequired).FirstOrDefault();
+ if (requiredValidator != null)
+ {
+ foreach (ModelValidationResult validationResult in requiredValidator.Validate(bindingContext.Model))
+ {
+ bindingContext.ModelState.AddModelError(modelStateKey, validationResult.Message);
+ }
+ }
+ }
+ }
+
+ if (value != null || TypeHelper.TypeAllowsNullValue(propertyDescriptor.PropertyType))
+ {
+ try
+ {
+ propertyDescriptor.SetValue(bindingContext.Model, value);
+ }
+ catch (Exception ex)
+ {
+ // don't display a duplicate error message if a binding error has already occurred for this field
+ string modelStateKey = dtoResult.ValidationNode.ModelStateKey;
+ if (bindingContext.ModelState.IsValidField(modelStateKey))
+ {
+ bindingContext.ModelState.AddModelError(modelStateKey, ex);
+ }
+ }
+ }
+ else
+ {
+ // trying to set a non-nullable value type to null, need to make sure there's a message
+ string modelStateKey = dtoResult.ValidationNode.ModelStateKey;
+ if (bindingContext.ModelState.IsValidField(modelStateKey))
+ {
+ dtoResult.ValidationNode.Validated += CreateNullCheckFailedHandler(propertyMetadata, value);
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/Binders/MutableObjectModelBinderProvider.cs b/src/System.Web.Http/ModelBinding/Binders/MutableObjectModelBinderProvider.cs
new file mode 100644
index 00000000..36865349
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/Binders/MutableObjectModelBinderProvider.cs
@@ -0,0 +1,32 @@
+using System.Web.Http.Controllers;
+
+namespace System.Web.Http.ModelBinding.Binders
+{
+ public sealed class MutableObjectModelBinderProvider : ModelBinderProvider
+ {
+ public override IModelBinder GetBinder(HttpActionContext actionContext, ModelBindingContext bindingContext)
+ {
+ ModelBindingHelper.ValidateBindingContext(bindingContext);
+
+ if (!bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName))
+ {
+ // no values to bind
+ return null;
+ }
+
+ // Simple types cannot use this binder
+ if (!bindingContext.ModelMetadata.IsComplexType)
+ {
+ return null;
+ }
+
+ if (bindingContext.ModelType == typeof(ComplexModelDto))
+ {
+ // forbidden type - will cause a stack overflow if we try binding this type
+ return null;
+ }
+
+ return new MutableObjectModelBinder();
+ }
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/Binders/SimpleModelBinderProvider.cs b/src/System.Web.Http/ModelBinding/Binders/SimpleModelBinderProvider.cs
new file mode 100644
index 00000000..553ccb99
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/Binders/SimpleModelBinderProvider.cs
@@ -0,0 +1,64 @@
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+
+namespace System.Web.Http.ModelBinding.Binders
+{
+ // Returns a user-specified binder for a given type.
+ public sealed class SimpleModelBinderProvider : ModelBinderProvider
+ {
+ private readonly Func<IModelBinder> _modelBinderFactory;
+ private readonly Type _modelType;
+
+ public SimpleModelBinderProvider(Type modelType, IModelBinder modelBinder)
+ {
+ if (modelType == null)
+ {
+ throw Error.ArgumentNull("modelType");
+ }
+ if (modelBinder == null)
+ {
+ throw Error.ArgumentNull("modelBinder");
+ }
+
+ _modelType = modelType;
+ _modelBinderFactory = () => modelBinder;
+ }
+
+ public SimpleModelBinderProvider(Type modelType, Func<IModelBinder> modelBinderFactory)
+ {
+ if (modelType == null)
+ {
+ throw Error.ArgumentNull("modelType");
+ }
+ if (modelBinderFactory == null)
+ {
+ throw Error.ArgumentNull("modelBinderFactory");
+ }
+
+ _modelType = modelType;
+ _modelBinderFactory = modelBinderFactory;
+ }
+
+ public Type ModelType
+ {
+ get { return _modelType; }
+ }
+
+ public bool SuppressPrefixCheck { get; set; }
+
+ public override IModelBinder GetBinder(HttpActionContext actionContext, ModelBindingContext bindingContext)
+ {
+ ModelBindingHelper.ValidateBindingContext(bindingContext);
+
+ if (bindingContext.ModelType == ModelType)
+ {
+ if (SuppressPrefixCheck || bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName))
+ {
+ return _modelBinderFactory();
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/Binders/TypeConverterModelBinder.cs b/src/System.Web.Http/ModelBinding/Binders/TypeConverterModelBinder.cs
new file mode 100644
index 00000000..9ad9a6a8
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/Binders/TypeConverterModelBinder.cs
@@ -0,0 +1,62 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Web.Http.Controllers;
+using System.Web.Http.ValueProviders;
+
+namespace System.Web.Http.ModelBinding.Binders
+{
+ public sealed class TypeConverterModelBinder : IModelBinder
+ {
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "The exception is recorded to be acted upon later.")]
+ [SuppressMessage("Microsoft.Globalization", "CA1304:SpecifyCultureInfo", MessageId = "System.Web.Http.ValueProviders.ValueProviderResult.ConvertTo(System.Type)", Justification = "The ValueProviderResult already has the necessary context to perform a culture-aware conversion.")]
+ public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
+ {
+ ModelBindingHelper.ValidateBindingContext(bindingContext);
+
+ ValueProviderResult valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
+ if (valueProviderResult == null)
+ {
+ return false; // no entry
+ }
+
+ object newModel;
+ bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
+ try
+ {
+ newModel = valueProviderResult.ConvertTo(bindingContext.ModelType);
+ }
+ catch (Exception ex)
+ {
+ if (IsFormatException(ex))
+ {
+ // there was a type conversion failure
+ string errorString = ModelBinderConfig.TypeConversionErrorMessageProvider(actionContext, bindingContext.ModelMetadata, valueProviderResult.AttemptedValue);
+ if (errorString != null)
+ {
+ bindingContext.ModelState.AddModelError(bindingContext.ModelName, errorString);
+ }
+ }
+ else
+ {
+ bindingContext.ModelState.AddModelError(bindingContext.ModelName, ex);
+ }
+ return false;
+ }
+
+ ModelBindingHelper.ReplaceEmptyStringWithNull(bindingContext.ModelMetadata, ref newModel);
+ bindingContext.Model = newModel;
+ return true;
+ }
+
+ private static bool IsFormatException(Exception ex)
+ {
+ for (; ex != null; ex = ex.InnerException)
+ {
+ if (ex is FormatException)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/Binders/TypeConverterModelBinderProvider.cs b/src/System.Web.Http/ModelBinding/Binders/TypeConverterModelBinderProvider.cs
new file mode 100644
index 00000000..b4099050
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/Binders/TypeConverterModelBinderProvider.cs
@@ -0,0 +1,28 @@
+using System.Web.Http.Controllers;
+using System.Web.Http.Internal;
+using System.Web.Http.ValueProviders;
+
+namespace System.Web.Http.ModelBinding.Binders
+{
+ // Returns a binder that can perform conversions using a .NET TypeConverter.
+ public sealed class TypeConverterModelBinderProvider : ModelBinderProvider
+ {
+ public override IModelBinder GetBinder(HttpActionContext actionContext, ModelBindingContext bindingContext)
+ {
+ ModelBindingHelper.ValidateBindingContext(bindingContext);
+
+ ValueProviderResult valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
+ if (valueProviderResult == null)
+ {
+ return null; // no value to convert
+ }
+
+ if (!TypeHelper.HasStringConverter(bindingContext.ModelType))
+ {
+ return null; // this type cannot be converted
+ }
+
+ return new TypeConverterModelBinder();
+ }
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/Binders/TypeMatchModelBinder.cs b/src/System.Web.Http/ModelBinding/Binders/TypeMatchModelBinder.cs
new file mode 100644
index 00000000..9266192b
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/Binders/TypeMatchModelBinder.cs
@@ -0,0 +1,43 @@
+using System.Web.Http.Controllers;
+using System.Web.Http.Internal;
+using System.Web.Http.ValueProviders;
+
+namespace System.Web.Http.ModelBinding.Binders
+{
+ public sealed class TypeMatchModelBinder : IModelBinder
+ {
+ public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
+ {
+ ValueProviderResult valueProviderResult = GetCompatibleValueProviderResult(bindingContext);
+ if (valueProviderResult == null)
+ {
+ return false; // conversion would have failed
+ }
+
+ bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
+ object model = valueProviderResult.RawValue;
+ ModelBindingHelper.ReplaceEmptyStringWithNull(bindingContext.ModelMetadata, ref model);
+ bindingContext.Model = model;
+
+ return true;
+ }
+
+ internal static ValueProviderResult GetCompatibleValueProviderResult(ModelBindingContext bindingContext)
+ {
+ ModelBindingHelper.ValidateBindingContext(bindingContext);
+
+ ValueProviderResult valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
+ if (valueProviderResult == null)
+ {
+ return null; // the value doesn't exist
+ }
+
+ if (!TypeHelper.IsCompatibleObject(bindingContext.ModelType, valueProviderResult.RawValue))
+ {
+ return null; // value is of incompatible type
+ }
+
+ return valueProviderResult;
+ }
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/Binders/TypeMatchModelBinderProvider.cs b/src/System.Web.Http/ModelBinding/Binders/TypeMatchModelBinderProvider.cs
new file mode 100644
index 00000000..61700d55
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/Binders/TypeMatchModelBinderProvider.cs
@@ -0,0 +1,15 @@
+using System.Web.Http.Controllers;
+
+namespace System.Web.Http.ModelBinding.Binders
+{
+ // Returns a binder that can extract a ValueProviderResult.RawValue and return it directly.
+ public sealed class TypeMatchModelBinderProvider : ModelBinderProvider
+ {
+ public override IModelBinder GetBinder(HttpActionContext actionContext, ModelBindingContext bindingContext)
+ {
+ return (TypeMatchModelBinder.GetCompatibleValueProviderResult(bindingContext) != null)
+ ? new TypeMatchModelBinder()
+ : null /* no match */;
+ }
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/CancellationTokenParameterBinding.cs b/src/System.Web.Http/ModelBinding/CancellationTokenParameterBinding.cs
new file mode 100644
index 00000000..91ca007c
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/CancellationTokenParameterBinding.cs
@@ -0,0 +1,25 @@
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Controllers;
+using System.Web.Http.Metadata;
+
+namespace System.Web.Http.ModelBinding
+{
+ /// <summary>
+ /// Bind directly to the cancellation token
+ /// </summary>
+ public class CancellationTokenParameterBinding : HttpParameterBinding
+ {
+ public CancellationTokenParameterBinding(HttpParameterDescriptor descriptor)
+ : base(descriptor)
+ {
+ }
+
+ public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider, HttpActionContext actionContext, CancellationToken cancellationToken)
+ {
+ string name = Descriptor.ParameterName;
+ actionContext.ActionArguments.Add(name, cancellationToken);
+ return TaskHelpers.Completed();
+ }
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/CustomModelBinderAttribute.cs b/src/System.Web.Http/ModelBinding/CustomModelBinderAttribute.cs
new file mode 100644
index 00000000..0fc22d32
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/CustomModelBinderAttribute.cs
@@ -0,0 +1,13 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Web.Http.ModelBinding
+{
+ [AttributeUsage(ValidTargets, AllowMultiple = false, Inherited = false)]
+ public abstract class CustomModelBinderAttribute : Attribute
+ {
+ internal const AttributeTargets ValidTargets = AttributeTargets.Class | AttributeTargets.Enum | AttributeTargets.Interface | AttributeTargets.Parameter | AttributeTargets.Struct;
+
+ [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This method can potentially perform a non-trivial amount of work.")]
+ public abstract IModelBinder GetBinder();
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/DefaultActionValueBinder.cs b/src/System.Web.Http/ModelBinding/DefaultActionValueBinder.cs
new file mode 100644
index 00000000..44452838
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/DefaultActionValueBinder.cs
@@ -0,0 +1,149 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Formatting;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Internal;
+using System.Web.Http.Metadata;
+using System.Web.Http.Properties;
+using System.Web.Http.Validation;
+
+namespace System.Web.Http.ModelBinding
+{
+ public class DefaultActionValueBinder : IActionValueBinder
+ {
+ // Derived binder may customize the formatters and use per-controller specific formatters instead of config formatters.
+ protected virtual IEnumerable<MediaTypeFormatter> GetFormatters(HttpActionDescriptor actionDescriptor)
+ {
+ IEnumerable<MediaTypeFormatter> formatters = actionDescriptor.Configuration.Formatters;
+ return formatters;
+ }
+
+ protected virtual IBodyModelValidator GetBodyModelValidator(HttpActionDescriptor actionDescriptor)
+ {
+ return actionDescriptor.Configuration.ServiceResolver.GetBodyModelValidator();
+ }
+
+ /// <summary>
+ /// Implementation of <see cref="IActionValueBinder"/>, Primary entry point for binding parameters for an action.
+ /// </summary>
+ public virtual HttpActionBinding GetBinding(HttpActionDescriptor actionDescriptor)
+ {
+ if (actionDescriptor == null)
+ {
+ throw Error.ArgumentNull("actionDescriptor");
+ }
+
+ HttpParameterDescriptor[] parameters = actionDescriptor.GetParameters().ToArray();
+ HttpParameterBinding[] binders = Array.ConvertAll(parameters, DetermineBinding);
+
+ HttpActionBinding actionBinding = new HttpActionBinding(actionDescriptor, binders);
+
+ EnsureOneBodyParameter(actionBinding);
+
+ return actionBinding;
+ }
+
+ /// <summary>
+ /// Update actionBinding to enforce there is at most 1 body parameter.
+ /// If there are multiple, convert them all to <see cref="ErrorParameterBinding"/>
+ /// </summary>
+ private static void EnsureOneBodyParameter(HttpActionBinding actionBinding)
+ {
+ IList<HttpParameterDescriptor> parameters = actionBinding.ActionDescriptor.GetParameters();
+
+ int idxFromBody = -1;
+ for (int i = 0; i < actionBinding.ParameterBindings.Length; i++)
+ {
+ if (actionBinding.ParameterBindings[i].WillReadBody)
+ {
+ if (idxFromBody >= 0)
+ {
+ // This is the 2nd parameter to read from the body. Flag an error.
+ string name1 = parameters[idxFromBody].ParameterName;
+ string name2 = parameters[i].ParameterName;
+
+ string message = Error.Format(SRResources.ParameterBindingCantHaveMultipleBodyParameters, name1, name2);
+ actionBinding.ParameterBindings[i] = new ErrorParameterBinding(parameters[i], message);
+ actionBinding.ParameterBindings[idxFromBody] = new ErrorParameterBinding(parameters[idxFromBody], message);
+ }
+ else
+ {
+ idxFromBody = i;
+ }
+ }
+ }
+ }
+
+ private HttpParameterBinding MakeBodyBinding(HttpParameterDescriptor parameter)
+ {
+ HttpActionDescriptor actionDescriptor = parameter.ActionDescriptor;
+ return new FormatterParameterBinding(parameter, GetFormatters(actionDescriptor), GetBodyModelValidator(actionDescriptor));
+ }
+
+ // Determine how a single parameter will get bound.
+ // This is all sync. We don't need to actually read the body just to determine that we'll bind to the body.
+ private HttpParameterBinding DetermineBinding(HttpParameterDescriptor parameter)
+ {
+ // Look at Parameter attributes?
+ // [FromBody] - we use Formatter.
+ bool hasFromBody = parameter.GetCustomAttributes<FromBodyAttribute>().Any();
+ ModelBinderAttribute attr = parameter.ModelBinderAttribute;
+
+ if (hasFromBody)
+ {
+ if (attr != null)
+ {
+ string message = Error.Format(SRResources.ParameterBindingConflictingAttributes, parameter.ParameterName);
+ return new ErrorParameterBinding(parameter, message);
+ }
+
+ return MakeBodyBinding(parameter); // It's from the body. Uses a formatter.
+ }
+
+ // Presence of a model binder attribute overrides.
+ if (attr != null)
+ {
+ return GetBinding(parameter, attr);
+ }
+
+ // No attribute, key off type
+ Type type = parameter.ParameterType;
+ if (TypeHelper.IsSimpleUnderlyingType(type) || TypeHelper.HasStringConverter(type))
+ {
+ attr = new ModelBinderAttribute(); // use default settings
+ return GetBinding(parameter, attr);
+ }
+
+ // Handle special types
+ if (type == typeof(CancellationToken))
+ {
+ return new CancellationTokenParameterBinding(parameter);
+ }
+ if (type == typeof(HttpRequestMessage))
+ {
+ return new HttpRequestParameterBinding(parameter);
+ }
+ if (typeof(HttpContent).IsAssignableFrom(type))
+ {
+ string message = Error.Format(SRResources.ParameterBindingIllegalType, type.Name, parameter.ParameterName);
+ return new ErrorParameterBinding(parameter, message);
+ }
+
+ // Fallback. Must be a complex type. Default is to look in body.
+ return MakeBodyBinding(parameter);
+ }
+
+ private static HttpParameterBinding GetBinding(HttpParameterDescriptor parameter, ModelBinderAttribute attr)
+ {
+ HttpConfiguration config = parameter.Configuration;
+ return new ModelBinderParameterBinding(parameter,
+ attr.GetModelBinderProvider(config),
+ attr.GetValueProviderFactories(config));
+ }
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/ErrorParameterBinding.cs b/src/System.Web.Http/ModelBinding/ErrorParameterBinding.cs
new file mode 100644
index 00000000..b3ed1739
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/ErrorParameterBinding.cs
@@ -0,0 +1,40 @@
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Metadata;
+
+namespace System.Web.Http.ModelBinding
+{
+ /// <summary>
+ /// Describe a binding error. This includes a message that can give meaningful information to a client.
+ /// </summary>
+ public class ErrorParameterBinding : HttpParameterBinding
+ {
+ private readonly string _message;
+
+ public ErrorParameterBinding(HttpParameterDescriptor descriptor, string message)
+ : base(descriptor)
+ {
+ if (message == null)
+ {
+ throw Error.ArgumentNull(message);
+ }
+ _message = message;
+ }
+
+ public override string ErrorMessage
+ {
+ get
+ {
+ return _message;
+ }
+ }
+
+ public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider, HttpActionContext actionContext, CancellationToken cancellationToken)
+ {
+ // Caller should have already checked IsError before executing, so we shoulnd't be here.
+ return TaskHelpers.FromError(new InvalidOperationException());
+ }
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/FormDataCollectionExtensions.cs b/src/System.Web.Http/ModelBinding/FormDataCollectionExtensions.cs
new file mode 100644
index 00000000..a42fff81
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/FormDataCollectionExtensions.cs
@@ -0,0 +1,181 @@
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Globalization;
+using System.Net.Http.Formatting;
+using System.Text;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Metadata;
+using System.Web.Http.ModelBinding.Binders;
+using System.Web.Http.Properties;
+using System.Web.Http.ValueProviders;
+using System.Web.Http.ValueProviders.Providers;
+
+namespace System.Web.Http.ModelBinding
+{
+ public static class FormDataCollectionExtensions
+ {
+ // This is a helper method to use Model Binding over a JQuery syntax.
+ // Normalize from JQuery to MVC keys. The model binding infrastructure uses MVC keys
+ // x[] --> x
+ // [] --> ""
+ // x[field] --> x.field, where field is not a number
+ internal static string NormalizeJQueryToMvc(string key)
+ {
+ if (key == null)
+ {
+ return string.Empty;
+ }
+
+ StringBuilder sb = new StringBuilder();
+ int i = 0;
+ while (true)
+ {
+ int indexOpen = key.IndexOf('[', i);
+ if (indexOpen < 0)
+ {
+ sb.Append(key, i, key.Length - i);
+ break; // no more brackets
+ }
+
+ sb.Append(key, i, indexOpen - i); // everything up to "["
+
+ // Find closing bracket.
+ int indexClose = key.IndexOf(']', indexOpen);
+ if (indexClose == -1)
+ {
+ throw Error.Argument("key", SRResources.JQuerySyntaxMissingClosingBracket);
+ }
+
+ if (indexClose == indexOpen + 1)
+ {
+ // Empty bracket. Signifies array. Just remove.
+ }
+ else
+ {
+ if (char.IsDigit(key[indexOpen + 1]))
+ {
+ // array index. Leave unchanged.
+ sb.Append(key, indexOpen, indexClose - indexOpen + 1);
+ }
+ else
+ {
+ // Field name. Convert to dot notation.
+ sb.Append('.');
+ sb.Append(key, indexOpen + 1, indexClose - indexOpen - 1);
+ }
+ }
+
+ i = indexClose + 1;
+ if (i >= key.Length)
+ {
+ break; // end of string
+ }
+ }
+ return sb.ToString();
+ }
+
+ internal static NameValueCollection GetJQueryValueNameValueCollection(this FormDataCollection formData)
+ {
+ if (formData == null)
+ {
+ throw Error.ArgumentNull("formData");
+ }
+
+ NameValueCollection nvc = new NameValueCollection();
+ foreach (var kv in formData)
+ {
+ string key = NormalizeJQueryToMvc(kv.Key);
+ nvc.Add(key, kv.Value);
+ }
+ return nvc;
+ }
+
+ // Create a IValueProvider for the given form, assuming a JQuery syntax.
+ internal static IValueProvider GetJQueryValueProvider(this FormDataCollection formData)
+ {
+ if (formData == null)
+ {
+ throw Error.ArgumentNull("formData");
+ }
+
+ NameValueCollection nvc = formData.GetJQueryValueNameValueCollection();
+ return new NameValueCollectionValueProvider(nvc, CultureInfo.InvariantCulture);
+ }
+
+ public static T ReadAs<T>(this FormDataCollection formData)
+ {
+ return (T)ReadAs(formData, typeof(T));
+ }
+
+ public static object ReadAs(this FormDataCollection formData, Type type)
+ {
+ return ReadAs(formData, type, string.Empty);
+ }
+
+ public static T ReadAs<T>(this FormDataCollection formData, string modelName)
+ {
+ return (T)ReadAs(formData, typeof(T), modelName);
+ }
+
+ /// <summary>
+ /// Deserialize the form data to the given type, using model binding.
+ /// </summary>
+ /// <param name="formData">collection with parsed form url data</param>
+ /// <param name="type">target type to read as</param>
+ /// <param name="modelName">null or empty to read the entire form as a single object. This is common for body data.
+ /// Or the name of a model to do a partial binding against the form data. This is common for extracting individual fields.</param>
+ /// <returns>best attempt to bind the object. The best attempt may be null.</returns>
+ public static object ReadAs(this FormDataCollection formData, Type type, string modelName)
+ {
+ if (formData == null)
+ {
+ throw Error.ArgumentNull("formData");
+ }
+ if (type == null)
+ {
+ throw Error.ArgumentNull("type");
+ }
+
+ if (modelName == null)
+ {
+ modelName = string.Empty;
+ }
+
+ using (HttpConfiguration config = new HttpConfiguration())
+ {
+ // Looks like HttpActionContext is just a way of getting to the config, which we really
+ // just need to get a list of modelbinderPRoviders for composition.
+ HttpControllerContext controllerContext = new HttpControllerContext() { Configuration = config };
+ HttpActionContext actionContext = new HttpActionContext { ControllerContext = controllerContext };
+
+ ModelMetadataProvider metadataProvider = config.ServiceResolver.GetModelMetadataProvider();
+
+ // Create default over config
+ IEnumerable<ModelBinderProvider> providers = config.ServiceResolver.GetModelBinderProviders();
+ ModelBinderProvider modelBinderProvider = new CompositeModelBinderProvider(providers);
+
+ IValueProvider vp = formData.GetJQueryValueProvider();
+
+ ModelBindingContext ctx = new ModelBindingContext()
+ {
+ ModelName = modelName,
+ FallbackToEmptyPrefix = false,
+ ModelMetadata = metadataProvider.GetMetadataForType(null, type),
+ ModelState = actionContext.ModelState,
+ ValueProvider = vp
+ };
+
+ IModelBinder binder = modelBinderProvider.GetBinder(actionContext, ctx);
+
+ bool haveResult = binder.BindModel(actionContext, ctx);
+ if (haveResult)
+ {
+ object model = ctx.Model;
+ return model;
+ }
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/FormatterParameterBinding.cs b/src/System.Web.Http/ModelBinding/FormatterParameterBinding.cs
new file mode 100644
index 00000000..db03586a
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/FormatterParameterBinding.cs
@@ -0,0 +1,81 @@
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Net.Http.Formatting;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Metadata;
+using System.Web.Http.Validation;
+
+namespace System.Web.Http.ModelBinding
+{
+ /// <summary>
+ /// Parameter binding that will read from the body and invoke the formatters.
+ /// </summary>
+ public class FormatterParameterBinding : HttpParameterBinding
+ {
+ private IEnumerable<MediaTypeFormatter> _formatters;
+
+ public FormatterParameterBinding(HttpParameterDescriptor descriptor, IEnumerable<MediaTypeFormatter> formatters, IBodyModelValidator bodyModelValidator)
+ : base(descriptor)
+ {
+ Formatters = formatters;
+ BodyModelValidator = bodyModelValidator;
+ }
+
+ public override bool WillReadBody
+ {
+ get { return true; }
+ }
+
+ public IEnumerable<MediaTypeFormatter> Formatters
+ {
+ get { return _formatters; }
+ set
+ {
+ if (value == null)
+ {
+ throw Error.ArgumentNull("formatters");
+ }
+ _formatters = value;
+ }
+ }
+
+ public IBodyModelValidator BodyModelValidator
+ {
+ get;
+ set;
+ }
+
+ protected virtual Task<object> ReadContentAsync(HttpRequestMessage request, Type type, IEnumerable<MediaTypeFormatter> formatters, IFormatterLogger formatterLogger)
+ {
+ return request.Content.ReadAsAsync(type, formatters, formatterLogger);
+ }
+
+ public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider, HttpActionContext actionContext, CancellationToken cancellationToken)
+ {
+ HttpParameterDescriptor paramFromBody = this.Descriptor;
+
+ Type type = paramFromBody.ParameterType;
+ HttpRequestMessage request = actionContext.ControllerContext.Request;
+ IFormatterLogger formatterLogger = new ModelStateFormatterLogger(actionContext.ModelState);
+ Task<object> task = ReadContentAsync(request, type, _formatters, formatterLogger);
+
+ return task.Then(
+ (model) =>
+ {
+ // Put the parameter result into the action context.
+ string name = paramFromBody.ParameterName;
+ actionContext.ActionArguments.Add(name, model);
+
+ // validate the object graph.
+ // null indicates we want no body parameter validation
+ if (BodyModelValidator != null)
+ {
+ BodyModelValidator.Validate(model, type, metadataProvider, actionContext, paramFromBody.ParameterName);
+ }
+ });
+ }
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/HttpActionBinding.cs b/src/System.Web.Http/ModelBinding/HttpActionBinding.cs
new file mode 100644
index 00000000..545c6a6b
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/HttpActionBinding.cs
@@ -0,0 +1,91 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Controllers;
+using System.Web.Http.Metadata;
+
+namespace System.Web.Http.ModelBinding
+{
+ /// <summary>
+ /// This describes *how* the binding will happen. Does not actually bind.
+ /// This is static for a given action descriptor and can be reused across requests.
+ /// This may be a nice thing to log. Or set a breakpoint after we create and preview what's about to happen.
+ /// In theory, this could be precompiled for each Action descriptor.
+ /// </summary>
+ public class HttpActionBinding
+ {
+ private HttpActionDescriptor _actionDescriptor;
+ private HttpParameterBinding[] _parameterBindings;
+
+ public HttpActionBinding()
+ {
+ }
+ public HttpActionBinding(HttpActionDescriptor actionDescriptor, HttpParameterBinding[] bindings)
+ {
+ ActionDescriptor = actionDescriptor;
+ ParameterBindings = bindings;
+ }
+
+ /// <summary>
+ /// Back pointer to the action this binding is for.
+ /// This can also provide the Type[], string[] names for the parameters.
+ /// </summary>
+ public HttpActionDescriptor ActionDescriptor
+ {
+ get
+ {
+ return _actionDescriptor;
+ }
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException("value");
+ }
+ _actionDescriptor = value;
+ }
+ }
+
+ /// <summary>
+ /// Specifies synchronous bindings for each parameter.This is a parallel array to the ActionDescriptor's parameter array.
+ /// </summary>
+ [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "Want an array")]
+ public HttpParameterBinding[] ParameterBindings
+ {
+ get
+ {
+ return _parameterBindings;
+ }
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException("value");
+ }
+ _parameterBindings = value;
+ }
+ }
+
+ public virtual Task ExecuteBindingAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
+ {
+ // First, make sure the actionBinding is valid before trying to execute it. This keeps us in a known state in case of errors.
+ foreach (HttpParameterBinding parameterBinder in ParameterBindings)
+ {
+ if (!parameterBinder.IsValid)
+ {
+ // Error code here is 500 because the WebService developer's action signature is bad.
+ return TaskHelpers.FromError(new HttpResponseException(parameterBinder.ErrorMessage, HttpStatusCode.InternalServerError));
+ }
+ }
+
+ ModelMetadataProvider metadataProvider = actionContext.ControllerContext.Configuration.ServiceResolver.GetModelMetadataProvider();
+
+ // Execute all the binders.
+ IEnumerable<Task> tasks = from parameterBinder in ParameterBindings select parameterBinder.ExecuteBindingAsync(metadataProvider, actionContext, cancellationToken);
+ return TaskHelpers.Iterate(tasks, cancellationToken);
+ }
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/HttpParameterBinding.cs b/src/System.Web.Http/ModelBinding/HttpParameterBinding.cs
new file mode 100644
index 00000000..e926a7cf
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/HttpParameterBinding.cs
@@ -0,0 +1,70 @@
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Metadata;
+
+namespace System.Web.Http.ModelBinding
+{
+ /// <summary>
+ /// Describes how a parameter is bound. The binding should be static (based purely on the descriptor) and
+ /// can be shared across requests.
+ /// </summary>
+ public abstract class HttpParameterBinding
+ {
+ private readonly HttpParameterDescriptor _descriptor;
+
+ protected HttpParameterBinding(HttpParameterDescriptor descriptor)
+ {
+ if (descriptor == null)
+ {
+ throw Error.ArgumentNull("descriptor");
+ }
+ _descriptor = descriptor;
+ }
+
+ /// <summary>
+ /// True iff this binding owns the body. This is important since the body can be a stream that is only read once.
+ /// This lets us know who is trying to read the body, and enforce that there is only one reader.
+ /// </summary>
+ public virtual bool WillReadBody
+ {
+ get { return false; }
+ }
+
+ /// <summary>
+ /// True if the binding was successful and ExecuteBinding can be called.
+ /// False if there was an error determining this binding. This means a developer error somewhere, such as
+ /// configuration, parameter types, proper attributes, etc.
+ /// </summary>
+ public bool IsValid
+ {
+ get { return ErrorMessage == null; }
+ }
+
+ /// <summary>
+ /// Get an error message describing why this binding is invalid.
+ /// </summary>
+ public virtual string ErrorMessage
+ {
+ get { return null; }
+ }
+
+ public HttpParameterDescriptor Descriptor
+ {
+ get { return _descriptor; }
+ }
+
+ /// <summary>
+ /// Execute the binding for the given request.
+ /// On success, this will add the parameter to the actionContext.ActionArguments dictionary.
+ /// Caller ensures <see cref="IsValid"/> is true.
+ /// </summary>
+ /// <param name="metadataProvider">metadata provider to use for validation.</param>
+ /// <param name="actionContext">action context for the binding. This contains the parameter dictionary that will get populated.</param>
+ /// <param name="cancellationToken">Cancellation token for cancelling the binding operation. Or a binder can also bind a parameter to this.</param>
+ /// <returns>Task that is signaled when the binding is complete. For simple bindings from a URI, this should be signalled immediately.
+ /// For bindings that read the content body, this may do network IO.</returns>
+ public abstract Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider, HttpActionContext actionContext, CancellationToken cancellationToken);
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/HttpRequestParameterBinding.cs b/src/System.Web.Http/ModelBinding/HttpRequestParameterBinding.cs
new file mode 100644
index 00000000..686a5ff6
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/HttpRequestParameterBinding.cs
@@ -0,0 +1,31 @@
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Controllers;
+using System.Web.Http.Metadata;
+
+namespace System.Web.Http.ModelBinding
+{
+ /// <summary>
+ /// Parameter binds to the request
+ /// </summary>
+ public class HttpRequestParameterBinding : HttpParameterBinding
+ {
+ public HttpRequestParameterBinding(HttpParameterDescriptor descriptor)
+ : base(descriptor)
+ {
+ }
+
+ // Execute the binding for the given request.
+ // On success, this will add the parameter to the actionContext.ActionArguments dictionary.
+ // Caller ensures IsError==false.
+ public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider, HttpActionContext actionContext, CancellationToken cancellationToken)
+ {
+ string name = Descriptor.ParameterName;
+ HttpRequestMessage request = actionContext.ControllerContext.Request;
+ actionContext.ActionArguments.Add(name, request);
+
+ return TaskHelpers.Completed();
+ }
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/IModelBinder.cs b/src/System.Web.Http/ModelBinding/IModelBinder.cs
new file mode 100644
index 00000000..b0baa8f4
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/IModelBinder.cs
@@ -0,0 +1,9 @@
+using System.Web.Http.Controllers;
+
+namespace System.Web.Http.ModelBinding
+{
+ public interface IModelBinder
+ {
+ bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext);
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/JQueryMVCFormUrlEncodedFormatter.cs b/src/System.Web.Http/ModelBinding/JQueryMVCFormUrlEncodedFormatter.cs
new file mode 100644
index 00000000..af117abf
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/JQueryMVCFormUrlEncodedFormatter.cs
@@ -0,0 +1,53 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net.Http.Formatting;
+using System.Net.Http.Headers;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace System.Web.Http.ModelBinding
+{
+ // Supports JQuery schema on FormURL.
+ public class JQueryMvcFormUrlEncodedFormatter : FormUrlEncodedMediaTypeFormatter
+ {
+ public override bool CanReadType(Type type)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+
+ return true;
+ }
+
+ public override Task<object> ReadFromStreamAsync(Type type, Stream stream, HttpContentHeaders contentHeaders, IFormatterLogger formatterLogger)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+
+ if (stream == null)
+ {
+ throw new ArgumentNullException("stream");
+ }
+
+ // For simple types, defer to base class
+ if (base.CanReadType(type))
+ {
+ return base.ReadFromStreamAsync(type, stream, contentHeaders, formatterLogger);
+ }
+
+ return base.ReadFromStreamAsync(typeof(FormDataCollection), stream, contentHeaders, formatterLogger).Then(
+ (obj) =>
+ {
+ FormDataCollection fd = (FormDataCollection)obj;
+ object result = fd.ReadAs(type);
+
+ return result;
+ });
+ }
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/ModelBinderAttribute.cs b/src/System.Web.Http/ModelBinding/ModelBinderAttribute.cs
new file mode 100644
index 00000000..95931245
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/ModelBinderAttribute.cs
@@ -0,0 +1,92 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Web.Http.Common;
+using System.Web.Http.ModelBinding.Binders;
+using System.Web.Http.Properties;
+using System.Web.Http.ValueProviders;
+
+namespace System.Web.Http.ModelBinding
+{
+ /// <summary>
+ /// Specify this parameter uses a model binder. This can optionally specify the specific model binder and
+ /// value providers that drive that model binder.
+ /// Derived attributes may provide convenience settings for the model binder or value provider.
+ /// </summary>
+ [SuppressMessage("Microsoft.Design", "CA1019:DefineAccessorsForAttributeArguments", Justification = "want constructor argument shortcut")]
+ [SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", Justification = "part of a class hierarchy")]
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Parameter, Inherited = true, AllowMultiple = false)]
+ public class ModelBinderAttribute : Attribute
+ {
+ public ModelBinderAttribute()
+ : this(null)
+ {
+ }
+
+ public ModelBinderAttribute(Type binderType)
+ {
+ BinderType = binderType;
+ }
+
+ /// <summary>
+ /// Sets the type of the model binder.
+ /// This type must be a subclass of <see cref="ModelBinderProvider"/>
+ /// If null, uses the default from the configuration.
+ /// </summary>
+ public Type BinderType { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name to consider as the parameter name during model binding
+ /// </summary>
+ public string Prefix { get; set; }
+
+ public bool SuppressPrefixCheck { get; set; }
+
+ // This will get called by a parameter binding, which will cache the results.
+ public ModelBinderProvider GetModelBinderProvider(HttpConfiguration configuration)
+ {
+ if (BinderType != null)
+ {
+ object value = configuration.ServiceResolver.GetService(BinderType);
+ if (value == null)
+ {
+ value = Activator.CreateInstance(BinderType);
+ }
+ if (value != null)
+ {
+ VerifyBinderType(value.GetType());
+ ModelBinderProvider result = (ModelBinderProvider)value;
+ return result;
+ }
+ }
+
+ // Create default over config
+ IEnumerable<ModelBinderProvider> providers = configuration.ServiceResolver.GetModelBinderProviders();
+
+ if (providers.Count() == 1)
+ {
+ return providers.First();
+ }
+
+ return new CompositeModelBinderProvider(providers);
+ }
+
+ /// <summary>
+ /// Value providers that will be fed to the model binder.
+ /// </summary>
+ public virtual IEnumerable<ValueProviderFactory> GetValueProviderFactories(HttpConfiguration configuration)
+ {
+ // By default, just get all registered value provider factories
+ return configuration.ServiceResolver.GetValueProviderFactories();
+ }
+
+ private static void VerifyBinderType(Type attemptedType)
+ {
+ Type required = typeof(ModelBinderProvider);
+ if (!required.IsAssignableFrom(attemptedType))
+ {
+ throw Error.InvalidOperation(SRResources.ValueProviderFactory_Cannot_Create, required.Name, attemptedType.Name, required.Name);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/ModelBinderConfig.cs b/src/System.Web.Http/ModelBinding/ModelBinderConfig.cs
new file mode 100644
index 00000000..c5791b92
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/ModelBinderConfig.cs
@@ -0,0 +1,99 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Metadata;
+using System.Web.Http.Properties;
+
+namespace System.Web.Http.ModelBinding
+{
+ // REVIEW: Need a way to get the user's resource string choice
+ // Provides configuration settings common to the new model binding system.
+ public static class ModelBinderConfig
+ {
+ private static string _resourceClassKey;
+ private static ModelBinderErrorMessageProvider _typeConversionErrorMessageProvider;
+ private static ModelBinderErrorMessageProvider _valueRequiredErrorMessageProvider;
+
+ public static string ResourceClassKey
+ {
+ get { return _resourceClassKey ?? String.Empty; }
+ set { _resourceClassKey = value; }
+ }
+
+ public static ModelBinderErrorMessageProvider TypeConversionErrorMessageProvider
+ {
+ get
+ {
+ if (_typeConversionErrorMessageProvider == null)
+ {
+ _typeConversionErrorMessageProvider = DefaultTypeConversionErrorMessageProvider;
+ }
+ return _typeConversionErrorMessageProvider;
+ }
+ set { _typeConversionErrorMessageProvider = value; }
+ }
+
+ public static ModelBinderErrorMessageProvider ValueRequiredErrorMessageProvider
+ {
+ get
+ {
+ if (_valueRequiredErrorMessageProvider == null)
+ {
+ _valueRequiredErrorMessageProvider = DefaultValueRequiredErrorMessageProvider;
+ }
+ return _valueRequiredErrorMessageProvider;
+ }
+ set { _valueRequiredErrorMessageProvider = value; }
+ }
+
+ private static string DefaultTypeConversionErrorMessageProvider(HttpActionContext actionContext, ModelMetadata modelMetadata, object incomingValue)
+ {
+ return GetResourceCommon(actionContext, modelMetadata, incomingValue, GetValueInvalidResource);
+ }
+
+ private static string DefaultValueRequiredErrorMessageProvider(HttpActionContext actionContext, ModelMetadata modelMetadata, object incomingValue)
+ {
+ return GetResourceCommon(actionContext, modelMetadata, incomingValue, GetValueRequiredResource);
+ }
+
+ private static string GetResourceCommon(HttpActionContext actionContext, ModelMetadata modelMetadata, object incomingValue, Func<HttpActionContext, string> resourceAccessor)
+ {
+ string displayName = modelMetadata.GetDisplayName();
+ string errorMessageTemplate = resourceAccessor(actionContext);
+ return Error.Format(errorMessageTemplate, incomingValue, displayName);
+ }
+
+ private static string GetUserResourceString(HttpActionContext actionContext, string resourceName)
+ {
+ return GetUserResourceString(actionContext, resourceName, ResourceClassKey);
+ }
+
+ // If the user specified a ResourceClassKey try to load the resource they specified.
+ // If the class key is invalid, an exception will be thrown.
+ // If the class key is valid but the resource is not found, it returns null, in which
+ // case it will fall back to the MVC default error message.
+ [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "resourceName", Justification = "Temporary")]
+ [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "resourceClassKey", Justification = "Temporary")]
+ [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "actionContext", Justification = "Temporary")]
+ internal static string GetUserResourceString(HttpActionContext actionContext, string resourceName, string resourceClassKey)
+ {
+#if false
+ return (!String.IsNullOrEmpty(resourceClassKey) && (actionContext != null) && (actionContext.HttpContext != null))
+ ? actionContext.HttpContext.GetGlobalResourceObject(resourceClassKey, resourceName, CultureInfo.CurrentUICulture) as string
+ : null;
+#else
+ return null;
+#endif
+ }
+
+ private static string GetValueInvalidResource(HttpActionContext actionContext)
+ {
+ return GetUserResourceString(actionContext, "PropertyValueInvalid") ?? SRResources.ModelBinderConfig_ValueInvalid;
+ }
+
+ private static string GetValueRequiredResource(HttpActionContext actionContext)
+ {
+ return GetUserResourceString(actionContext, "PropertyValueRequired") ?? SRResources.ModelBinderConfig_ValueRequired;
+ }
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/ModelBinderErrorMessageProvider.cs b/src/System.Web.Http/ModelBinding/ModelBinderErrorMessageProvider.cs
new file mode 100644
index 00000000..ae684da7
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/ModelBinderErrorMessageProvider.cs
@@ -0,0 +1,7 @@
+using System.Web.Http.Controllers;
+using System.Web.Http.Metadata;
+
+namespace System.Web.Http.ModelBinding
+{
+ public delegate string ModelBinderErrorMessageProvider(HttpActionContext actionContext, ModelMetadata modelMetadata, object incomingValue);
+}
diff --git a/src/System.Web.Http/ModelBinding/ModelBinderParameterBinding.cs b/src/System.Web.Http/ModelBinding/ModelBinderParameterBinding.cs
new file mode 100644
index 00000000..6806daa8
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/ModelBinderParameterBinding.cs
@@ -0,0 +1,87 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Metadata;
+using System.Web.Http.ValueProviders;
+using System.Web.Http.ValueProviders.Providers;
+
+namespace System.Web.Http.ModelBinding
+{
+ /// <summary>
+ /// Describes a parameter that gets bound via ModelBinding.
+ /// </summary>
+ public class ModelBinderParameterBinding : HttpParameterBinding
+ {
+ private readonly IEnumerable<ValueProviderFactory> _valueProviderFactories;
+ private readonly ModelBinderProvider _modelBinderProvider;
+
+ public ModelBinderParameterBinding(HttpParameterDescriptor descriptor,
+ ModelBinderProvider modelBinderProvider,
+ IEnumerable<ValueProviderFactory> valueProviderFactories)
+ : base(descriptor)
+ {
+ if (modelBinderProvider == null)
+ {
+ throw Error.ArgumentNull("modelBinderProvider");
+ }
+ if (valueProviderFactories == null)
+ {
+ throw Error.ArgumentNull("valueProviderFactories");
+ }
+
+ _modelBinderProvider = modelBinderProvider;
+ _valueProviderFactories = valueProviderFactories;
+ }
+
+ public IEnumerable<ValueProviderFactory> ValueProviderFactories
+ {
+ get { return _valueProviderFactories; }
+ }
+
+ public ModelBinderProvider ModelBinderProvider
+ {
+ get { return _modelBinderProvider; }
+ }
+
+ public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider, HttpActionContext actionContext, CancellationToken cancellationToken)
+ {
+ string name = Descriptor.ParameterName;
+ Type type = Descriptor.ParameterType;
+
+ string prefix = Descriptor.Prefix;
+
+ IValueProvider vp = CreateValueProvider(this._valueProviderFactories, actionContext);
+
+ ModelBindingContext ctx = new ModelBindingContext()
+ {
+ ModelName = prefix ?? name,
+ FallbackToEmptyPrefix = prefix == null, // only fall back if prefix not specified
+ ModelMetadata = metadataProvider.GetMetadataForType(null, type),
+ ModelState = actionContext.ModelState,
+ ValueProvider = vp
+ };
+
+ IModelBinder binder = this._modelBinderProvider.GetBinder(actionContext, ctx);
+
+ bool haveResult = binder.BindModel(actionContext, ctx);
+ object model = haveResult ? ctx.Model : Descriptor.DefaultValue;
+ actionContext.ActionArguments.Add(name, model);
+
+ return TaskHelpers.Completed();
+ }
+
+ // Instantiate the value providers for the given action context.
+ private static IValueProvider CreateValueProvider(IEnumerable<ValueProviderFactory> factories, HttpActionContext actionContext)
+ {
+ List<IValueProvider> providers = factories.Select<ValueProviderFactory, IValueProvider>(f => f.GetValueProvider(actionContext)).ToList();
+ if (providers.Count == 1)
+ {
+ return providers[0];
+ }
+ return new CompositeValueProvider(providers);
+ }
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/ModelBinderProvider.cs b/src/System.Web.Http/ModelBinding/ModelBinderProvider.cs
new file mode 100644
index 00000000..4f8c1c7d
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/ModelBinderProvider.cs
@@ -0,0 +1,9 @@
+using System.Web.Http.Controllers;
+
+namespace System.Web.Http.ModelBinding
+{
+ public abstract class ModelBinderProvider
+ {
+ public abstract IModelBinder GetBinder(HttpActionContext actionContext, ModelBindingContext bindingContext);
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/ModelBindingContext.cs b/src/System.Web.Http/ModelBinding/ModelBindingContext.cs
new file mode 100644
index 00000000..eac46388
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/ModelBindingContext.cs
@@ -0,0 +1,125 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Web.Http.Common;
+using System.Web.Http.Metadata;
+using System.Web.Http.Properties;
+using System.Web.Http.Validation;
+using System.Web.Http.ValueProviders;
+
+namespace System.Web.Http.ModelBinding
+{
+ public class ModelBindingContext
+ {
+ private string _modelName;
+ private ModelStateDictionary _modelState;
+ private Dictionary<string, ModelMetadata> _propertyMetadata;
+ private ModelValidationNode _validationNode;
+
+ public ModelBindingContext()
+ : this(null)
+ {
+ }
+
+ // copies certain values that won't change between parent and child objects,
+ // e.g. ValueProvider, ModelState
+ public ModelBindingContext(ModelBindingContext bindingContext)
+ {
+ if (bindingContext != null)
+ {
+ ModelState = bindingContext.ModelState;
+ ValueProvider = bindingContext.ValueProvider;
+ }
+ }
+
+ public object Model
+ {
+ get
+ {
+ EnsureModelMetadata();
+ return ModelMetadata.Model;
+ }
+ set
+ {
+ EnsureModelMetadata();
+ ModelMetadata.Model = value;
+ }
+ }
+
+ public ModelMetadata ModelMetadata { get; set; }
+
+ public string ModelName
+ {
+ get
+ {
+ if (_modelName == null)
+ {
+ _modelName = String.Empty;
+ }
+ return _modelName;
+ }
+ set { _modelName = value; }
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This is writeable to support unit testing")]
+ public ModelStateDictionary ModelState
+ {
+ get
+ {
+ if (_modelState == null)
+ {
+ _modelState = new ModelStateDictionary();
+ }
+ return _modelState;
+ }
+ set { _modelState = value; }
+ }
+
+ public Type ModelType
+ {
+ get
+ {
+ EnsureModelMetadata();
+ return ModelMetadata.ModelType;
+ }
+ }
+
+ public IDictionary<string, ModelMetadata> PropertyMetadata
+ {
+ get
+ {
+ if (_propertyMetadata == null)
+ {
+ _propertyMetadata = ModelMetadata.Properties.ToDictionary(m => m.PropertyName, StringComparer.OrdinalIgnoreCase);
+ }
+
+ return _propertyMetadata;
+ }
+ }
+
+ public ModelValidationNode ValidationNode
+ {
+ get
+ {
+ if (_validationNode == null)
+ {
+ _validationNode = new ModelValidationNode(ModelMetadata, ModelName);
+ }
+ return _validationNode;
+ }
+ set { _validationNode = value; }
+ }
+
+ public IValueProvider ValueProvider { get; set; }
+
+ public bool FallbackToEmptyPrefix { get; set; }
+
+ private void EnsureModelMetadata()
+ {
+ if (ModelMetadata == null)
+ {
+ throw Error.InvalidOperation(SRResources.ModelBindingContext_ModelMetadataMustBeSet);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/ModelBindingHelper.cs b/src/System.Web.Http/ModelBinding/ModelBindingHelper.cs
new file mode 100644
index 00000000..b7fd2aca
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/ModelBindingHelper.cs
@@ -0,0 +1,193 @@
+using System.Collections;
+using System.Diagnostics.Contracts;
+using System.Globalization;
+using System.Linq;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Internal;
+using System.Web.Http.Metadata;
+using System.Web.Http.ModelBinding.Binders;
+using System.Web.Http.Properties;
+
+namespace System.Web.Http.ModelBinding
+{
+ public static class ModelBindingHelper
+ {
+ internal static TModel CastOrDefault<TModel>(object model)
+ {
+ return (model is TModel) ? (TModel)model : default(TModel);
+ }
+
+ internal static string CreateIndexModelName(string parentName, int index)
+ {
+ return CreateIndexModelName(parentName, index.ToString(CultureInfo.InvariantCulture));
+ }
+
+ internal static string CreateIndexModelName(string parentName, string index)
+ {
+ return (parentName.Length == 0) ? "[" + index + "]" : parentName + "[" + index + "]";
+ }
+
+ internal static string CreatePropertyModelName(string prefix, string propertyName)
+ {
+ if (String.IsNullOrEmpty(prefix))
+ {
+ return propertyName ?? String.Empty;
+ }
+ else if (String.IsNullOrEmpty(propertyName))
+ {
+ return prefix ?? String.Empty;
+ }
+ else
+ {
+ return prefix + "." + propertyName;
+ }
+ }
+
+ internal static IModelBinder GetPossibleBinderInstance(Type closedModelType, Type openModelType, Type openBinderType)
+ {
+ Type[] typeArguments = TypeHelper.GetTypeArgumentsIfMatch(closedModelType, openModelType);
+ return (typeArguments != null) ? (IModelBinder)Activator.CreateInstance(openBinderType.MakeGenericType(typeArguments)) : null;
+ }
+
+ internal static object[] RawValueToObjectArray(object rawValue)
+ {
+ // precondition: rawValue is not null
+
+ // Need to special-case String so it's not caught by the IEnumerable check which follows
+ if (rawValue is string)
+ {
+ return new[] { rawValue };
+ }
+
+ object[] rawValueAsObjectArray = rawValue as object[];
+ if (rawValueAsObjectArray != null)
+ {
+ return rawValueAsObjectArray;
+ }
+
+ IEnumerable rawValueAsEnumerable = rawValue as IEnumerable;
+ if (rawValueAsEnumerable != null)
+ {
+ return rawValueAsEnumerable.Cast<object>().ToArray();
+ }
+
+ // fallback
+ return new[] { rawValue };
+ }
+
+ internal static void ReplaceEmptyStringWithNull(ModelMetadata modelMetadata, ref object model)
+ {
+ if (model is string &&
+ modelMetadata.ConvertEmptyStringToNull &&
+ String.IsNullOrWhiteSpace(model as string))
+ {
+ model = null;
+ }
+ }
+
+ // Returns true if the ModelBinderAttribute specifies a binder provider of <TProvider> or binder instance of <TBinder>
+ internal static bool IsModelBinderFor<TProvider, TBinder>(ModelBinderAttribute modelBinderAttribute)
+ {
+ Contract.Assert(modelBinderAttribute != null, "modelBinderAttribute cannot be null");
+ Type binderType = modelBinderAttribute.BinderType;
+ return binderType != null &&
+ (typeof(TProvider).IsAssignableFrom(binderType) ||
+ typeof(TBinder).IsAssignableFrom(binderType));
+ }
+
+ internal static bool TryGetProviderFromAttribute(Type modelType, ModelBinderAttribute modelBinderAttribute, out ModelBinderProvider provider)
+ {
+ Contract.Assert(modelType != null, "modelType cannot be null.");
+ Contract.Assert(modelBinderAttribute != null, "modelBinderAttribute cannot be null");
+
+ if (typeof(ModelBinderProvider).IsAssignableFrom(modelBinderAttribute.BinderType))
+ {
+ // REVIEW: DI?
+ provider = (ModelBinderProvider)Activator.CreateInstance(modelBinderAttribute.BinderType);
+ }
+ else if (typeof(IModelBinder).IsAssignableFrom(modelBinderAttribute.BinderType))
+ {
+ Type closedBinderType =
+ modelBinderAttribute.BinderType.IsGenericTypeDefinition
+ ? modelBinderAttribute.BinderType.MakeGenericType(modelType.GetGenericArguments())
+ : modelBinderAttribute.BinderType;
+
+ IModelBinder binderInstance = (IModelBinder)Activator.CreateInstance(closedBinderType);
+ provider = new SimpleModelBinderProvider(modelType, binderInstance) { SuppressPrefixCheck = modelBinderAttribute.SuppressPrefixCheck };
+ }
+ else
+ {
+ throw Error.InvalidOperation(SRResources.ModelBinderProviderCollection_InvalidBinderType, modelBinderAttribute.BinderType, typeof(ModelBinderProvider), typeof(IModelBinder));
+ }
+
+ return true;
+ }
+
+ internal static bool TryGetProviderFromAttributes(Type modelType, out ModelBinderProvider provider)
+ {
+ ModelBinderAttribute attr = TypeDescriptorHelper.Get(modelType).GetAttributes().OfType<ModelBinderAttribute>().FirstOrDefault();
+ if (attr == null)
+ {
+ provider = null;
+ return false;
+ }
+
+ return TryGetProviderFromAttribute(modelType, attr, out provider);
+ }
+
+ internal static bool TryGetProviderFromAttributes(HttpParameterDescriptor parameterDescriptor, out ModelBinderProvider provider)
+ {
+ Contract.Assert(parameterDescriptor != null, "parameterDescriptor cannot be null.");
+
+ Type modelType = parameterDescriptor.ParameterType;
+
+ ModelBinderAttribute attr = parameterDescriptor.GetCustomAttributes<ModelBinderAttribute>().FirstOrDefault();
+ if (attr == null)
+ {
+ attr = TypeDescriptorHelper.Get(modelType).GetAttributes().OfType<ModelBinderAttribute>().FirstOrDefault();
+ }
+
+ if (attr == null)
+ {
+ provider = null;
+ return false;
+ }
+
+ return TryGetProviderFromAttribute(modelType, attr, out provider);
+ }
+
+ internal static void ValidateBindingContext(ModelBindingContext bindingContext)
+ {
+ if (bindingContext == null)
+ {
+ throw Error.ArgumentNull("bindingContext");
+ }
+
+ if (bindingContext.ModelMetadata == null)
+ {
+ throw Error.Argument("bindingContext", SRResources.ModelBinderUtil_ModelMetadataCannotBeNull);
+ }
+ }
+
+ internal static void ValidateBindingContext(ModelBindingContext bindingContext, Type requiredType, bool allowNullModel)
+ {
+ ValidateBindingContext(bindingContext);
+
+ if (bindingContext.ModelType != requiredType)
+ {
+ throw Error.Argument("bindingContext", SRResources.ModelBinderUtil_ModelTypeIsWrong, bindingContext.ModelType, requiredType);
+ }
+
+ if (!allowNullModel && bindingContext.Model == null)
+ {
+ throw Error.Argument("bindingContext", SRResources.ModelBinderUtil_ModelCannotBeNull, requiredType);
+ }
+
+ if (bindingContext.Model != null && !requiredType.IsInstanceOfType(bindingContext.Model))
+ {
+ throw Error.Argument("bindingContext", SRResources.ModelBinderUtil_ModelInstanceIsWrong, bindingContext.Model.GetType(), requiredType);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/ModelError.cs b/src/System.Web.Http/ModelBinding/ModelError.cs
new file mode 100644
index 00000000..775971c9
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/ModelError.cs
@@ -0,0 +1,33 @@
+using System.Web.Http.Common;
+
+namespace System.Web.Http.ModelBinding
+{
+ [Serializable]
+ public class ModelError
+ {
+ public ModelError(Exception exception)
+ : this(exception, errorMessage: null)
+ {
+ }
+
+ public ModelError(Exception exception, string errorMessage)
+ : this(errorMessage)
+ {
+ if (exception == null)
+ {
+ throw Error.ArgumentNull("exception");
+ }
+
+ Exception = exception;
+ }
+
+ public ModelError(string errorMessage)
+ {
+ ErrorMessage = errorMessage ?? String.Empty;
+ }
+
+ public Exception Exception { get; private set; }
+
+ public string ErrorMessage { get; private set; }
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/ModelErrorCollection.cs b/src/System.Web.Http/ModelBinding/ModelErrorCollection.cs
new file mode 100644
index 00000000..2f91c1d8
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/ModelErrorCollection.cs
@@ -0,0 +1,18 @@
+using System.Collections.ObjectModel;
+
+namespace System.Web.Http.ModelBinding
+{
+ [Serializable]
+ public class ModelErrorCollection : Collection<ModelError>
+ {
+ public void Add(Exception exception)
+ {
+ Add(new ModelError(exception));
+ }
+
+ public void Add(string errorMessage)
+ {
+ Add(new ModelError(errorMessage));
+ }
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/ModelState.cs b/src/System.Web.Http/ModelBinding/ModelState.cs
new file mode 100644
index 00000000..acafc15c
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/ModelState.cs
@@ -0,0 +1,17 @@
+using System.Web.Http.ValueProviders;
+
+namespace System.Web.Http.ModelBinding
+{
+ [Serializable]
+ public class ModelState
+ {
+ private ModelErrorCollection _errors = new ModelErrorCollection();
+
+ public ValueProviderResult Value { get; set; }
+
+ public ModelErrorCollection Errors
+ {
+ get { return _errors; }
+ }
+ }
+}
diff --git a/src/System.Web.Http/ModelBinding/ModelStateDictionary.cs b/src/System.Web.Http/ModelBinding/ModelStateDictionary.cs
new file mode 100644
index 00000000..1ee177f8
--- /dev/null
+++ b/src/System.Web.Http/ModelBinding/ModelStateDictionary.cs
@@ -0,0 +1,182 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Web.Http.Common;
+using System.Web.Http.ValueProviders;
+
+namespace System.Web.Http.ModelBinding
+{
+ [Serializable]
+ public class ModelStateDictionary : IDictionary<string, ModelState>
+ {
+ private readonly Dictionary<string, ModelState> _innerDictionary = new Dictionary<string, ModelState>(StringComparer.OrdinalIgnoreCase);
+
+ public ModelStateDictionary()
+ {
+ }
+
+ public ModelStateDictionary(ModelStateDictionary dictionary)
+ {
+ if (dictionary == null)
+ {
+ throw Error.ArgumentNull("dictionary");
+ }
+
+ foreach (var entry in dictionary)
+ {
+ _innerDictionary.Add(entry.Key, entry.Value);
+ }
+ }
+
+ public int Count
+ {
+ get { return _innerDictionary.Count; }
+ }
+
+ public bool IsReadOnly
+ {
+ get { return ((IDictionary<string, ModelState>)_innerDictionary).IsReadOnly; }
+ }
+
+ public bool IsValid
+ {
+ get { return Values.All(modelState => modelState.Errors.Count == 0); }
+ }
+
+ public ICollection<string> Keys
+ {
+ get { return _innerDictionary.Keys; }
+ }
+
+ public ICollection<ModelState> Values
+ {
+ get { return _innerDictionary.Values; }
+ }
+
+ public ModelState this[string key]
+ {
+ get
+ {
+ ModelState value;
+ _innerDictionary.TryGetValue(key, out value);
+ return value;
+ }
+ set { _innerDictionary[key] = value; }
+ }
+
+ public void Add(KeyValuePair<string, ModelState> item)
+ {
+ ((IDictionary<string, ModelState>)_innerDictionary).Add(item);
+ }
+
+ public void Add(string key, ModelState value)
+ {
+ _innerDictionary.Add(key, value);
+ }
+
+ public void AddModelError(string key, Exception exception)
+ {
+ GetModelStateForKey(key).Errors.Add(exception);
+ }
+
+ public void AddModelError(string key, string errorMessage)
+ {
+ GetModelStateForKey(key).Errors.Add(errorMessage);
+ }
+
+ public void Clear()
+ {
+ _innerDictionary.Clear();
+ }
+
+ public bool Contains(KeyValuePair<string, ModelState> item)
+ {
+ return ((IDictionary<string, ModelState>)_innerDictionary).Contains(item);
+ }
+
+ public bool ContainsKey(string key)
+ {
+ return _innerDictionary.ContainsKey(key);
+ }
+
+ public void CopyTo(KeyValuePair<string, ModelState>[] array, int arrayIndex)
+ {
+ ((IDictionary<string, ModelState>)_innerDictionary).CopyTo(array, arrayIndex);
+ }
+
+ public IEnumerator<KeyValuePair<string, ModelState>> GetEnumerator()
+ {
+ return _innerDictionary.GetEnumerator();
+ }
+
+ private ModelState GetModelStateForKey(string key)
+ {
+ if (key == null)
+ {
+ throw Error.ArgumentNull("key");
+ }
+
+ ModelState modelState;
+ if (!TryGetValue(key, out modelState))
+ {
+ modelState = new ModelState();
+ this[key] = modelState;
+ }
+
+ return modelState;
+ }
+
+ public bool IsValidField(string key)
+ {
+ if (key == null)
+ {
+ throw Error.ArgumentNull("key");
+ }
+
+ // if the key is not found in the dictionary, we just say that it's valid (since there are no errors)
+ return this.FindKeysWithPrefix(key).All(entry => entry.Value.Errors.Count == 0);
+ }
+
+ public void Merge(ModelStateDictionary dictionary)
+ {
+ if (dictionary == null)
+ {
+ return;
+ }
+
+ foreach (var entry in dictionary)
+ {
+ this[entry.Key] = entry.Value;
+ }
+ }
+
+ public bool Remove(KeyValuePair<string, ModelState> item)
+ {
+ return ((IDictionary<string, ModelState>)_innerDictionary).Remove(item);
+ }
+
+ public bool Remove(string key)
+ {
+ return _innerDictionary.Remove(key);
+ }
+
+ public void SetModelValue(string key, ValueProviderResult value)
+ {
+ GetModelStateForKey(key).Value = value;
+ }
+
+ public bool TryGetValue(string key, out ModelState value)
+ {
+ return _innerDictionary.TryGetValue(key, out value);
+ }
+
+ #region IEnumerable Members
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return ((IEnumerable)_innerDictionary).GetEnumerator();
+ }
+
+ #endregion
+ }
+}
diff --git a/src/System.Web.Http/Modelbinding_ClassDiagram.cd b/src/System.Web.Http/Modelbinding_ClassDiagram.cd
new file mode 100644
index 00000000..0acaf0cb
--- /dev/null
+++ b/src/System.Web.Http/Modelbinding_ClassDiagram.cd
@@ -0,0 +1,148 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ClassDiagram MajorVersion="1" MinorVersion="1">
+ <Class Name="System.Web.Http.ValueProviders.ValueProviderAttribute" Collapsed="true">
+ <Position X="4.25" Y="6.25" Width="2" />
+ <Compartments>
+ <Compartment Name="Methods" Collapsed="true" />
+ </Compartments>
+ <TypeIdentifier>
+ <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAA=</HashCode>
+ <FileName>ValueProviders\ValueProviderAttribute.cs</FileName>
+ </TypeIdentifier>
+ </Class>
+ <Class Name="System.Web.Http.ModelBinding.DefaultActionValueBinder" Collapsed="true">
+ <Position X="0.5" Y="0.5" Width="2.25" />
+ <TypeIdentifier>
+ <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAEAAAAAA=</HashCode>
+ <FileName>ModelBinding\DefaultActionValueBinder.cs</FileName>
+ </TypeIdentifier>
+ <Lollipop Position="0.2" />
+ </Class>
+ <Class Name="System.Web.Http.ValueProviders.ValuePrefixAttribute" Collapsed="true">
+ <Position X="2.25" Y="5" Width="1.75" />
+ <TypeIdentifier>
+ <HashCode>AAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAA=</HashCode>
+ <FileName>ValueProviders\ValuePrefixAttribute.cs</FileName>
+ </TypeIdentifier>
+ </Class>
+ <Class Name="System.Web.Http.ModelBinding.Binders.HttpGenericIntrinsicModelBinder" Collapsed="true">
+ <Position X="6.75" Y="3.25" Width="2.5" />
+ <TypeIdentifier>
+ <HashCode>AAAAAAEAAAgAAAAABAAMAAAAAABACIAAAAAAAAAAgAA=</HashCode>
+ <FileName>ModelBinding\Binders\HttpGenericIntrinsicModelBinder.cs</FileName>
+ </TypeIdentifier>
+ <Lollipop Position="0.2" />
+ </Class>
+ <Class Name="System.Web.Http.ModelBinding.ModelBinderProvider" Collapsed="true">
+ <Position X="4" Y="4" Width="2" />
+ <TypeIdentifier>
+ <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAA=</HashCode>
+ <FileName>ModelBinding\ModelBinderProvider.cs</FileName>
+ </TypeIdentifier>
+ </Class>
+ <Class Name="System.Web.Http.ModelBinding.Binders.ActionContextModelBinder" Collapsed="true">
+ <Position X="6.75" Y="2.5" Width="2.5" />
+ <TypeIdentifier>
+ <HashCode>AAAAAAAAAAgCABAAAAAAQAAAAAAAAAAAAAAAAAAAAAA=</HashCode>
+ <FileName>ModelBinding\Binders\ActionContextModelBinder.cs</FileName>
+ </TypeIdentifier>
+ <Lollipop Position="0.2" />
+ </Class>
+ <Class Name="System.Web.Http.ModelBinding.Binders.ValidatingModelBinder" Collapsed="true">
+ <Position X="6.75" Y="4" Width="2.5" />
+ <TypeIdentifier>
+ <HashCode>AAAAAAAAAAgAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode>
+ <FileName>ModelBinding\Binders\ValidatingModelBinder.cs</FileName>
+ </TypeIdentifier>
+ <Lollipop Position="0.2" />
+ </Class>
+ <Class Name="System.Web.Http.FromBodyAttribute" Collapsed="true">
+ <Position X="2.25" Y="6.25" Width="1.75" />
+ <TypeIdentifier>
+ <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode>
+ <FileName>FromBodyAttribute.cs</FileName>
+ </TypeIdentifier>
+ </Class>
+ <Class Name="System.Web.Http.FromUriAttribute" Collapsed="true">
+ <Position X="0.5" Y="6.25" Width="1.5" />
+ <TypeIdentifier>
+ <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode>
+ <FileName>FromUriAttribute.cs</FileName>
+ </TypeIdentifier>
+ </Class>
+ <Class Name="System.Web.Http.ModelBinding.HttpActionBinding" Collapsed="true">
+ <Position X="0.5" Y="1.5" Width="2.5" />
+ <TypeIdentifier>
+ <HashCode>FgiAAAgAAAACAAgAAEAIAAFgIAAAAAAAACCAIwICABQ=</HashCode>
+ <FileName>ModelBinding\HttpActionBinding.cs</FileName>
+ </TypeIdentifier>
+ </Class>
+ <Class Name="System.Web.Http.ModelBinding.HttpParameterBinding" Collapsed="true">
+ <Position X="0.5" Y="2.5" Width="2" />
+ <TypeIdentifier>
+ <HashCode>AAABgAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAA=</HashCode>
+ <FileName>ModelBinding\HttpParameterBinding.cs</FileName>
+ </TypeIdentifier>
+ <ShowAsAssociation>
+ <Property Name="ModelBinderProvider" />
+ <Property Name="ValueProvider" />
+ <Property Name="ParameterDescriptor" />
+ </ShowAsAssociation>
+ </Class>
+ <Class Name="System.Web.Http.Controllers.HttpParameterDescriptor" Collapsed="true">
+ <Position X="4" Y="3.25" Width="2" />
+ <TypeIdentifier>
+ <HashCode>QAABAAAAAABAEAAAAAAAAAASAABCBBABgAEAgAgCAAA=</HashCode>
+ <FileName>Controllers\HttpParameterDescriptor.cs</FileName>
+ </TypeIdentifier>
+ <Lollipop Position="0.2" />
+ </Class>
+ <Class Name="System.Web.Http.ValueProviders.Providers.KeyValueModelValueProvider" Collapsed="true">
+ <Position X="6.75" Y="1.5" Width="2.5" />
+ <TypeIdentifier>
+ <HashCode>AAAAAAEAIAAAAAAAAAAAEAAAAAAAAIAAAAAAAAAAAAA=</HashCode>
+ <FileName>ValueProviders\Providers\KeyValueModelValueProvider.cs</FileName>
+ </TypeIdentifier>
+ <ShowAsAssociation>
+ <Field Name="_innerKeyValueProvider" />
+ </ShowAsAssociation>
+ <Lollipop Position="0.2" />
+ </Class>
+ <Class Name="System.Web.Http.ModelBinding.Binders.CompositeModelBinder" Collapsed="true">
+ <Position X="6.75" Y="4.75" Width="2.5" />
+ <TypeIdentifier>
+ <HashCode>AAAAAAAAAAgEAAAAAAAAAAAAAAAIAIAAAAAAAAEAAAA=</HashCode>
+ <FileName>ModelBinding\Binders\CompositeModelBinder.cs</FileName>
+ </TypeIdentifier>
+ <Lollipop Position="0.2" />
+ </Class>
+ <Class Name="System.Web.Http.ModelBinding.RequestContentReader" Collapsed="true">
+ <Position X="3.5" Y="1.5" Width="2.5" />
+ <TypeIdentifier>
+ <HashCode>IAAAAAAAAAAAAAAACAAAAAAAAAAAAAAYAAAAQAQAAAA=</HashCode>
+ <FileName>ModelBinding\RequestContentReader.cs</FileName>
+ </TypeIdentifier>
+ </Class>
+ <Class Name="System.Web.Http.Controllers.HttpActionContext" Collapsed="true">
+ <Position X="3.5" Y="0.5" Width="2" />
+ <TypeIdentifier>
+ <HashCode>QAAIgAIAAABAAAAAAAAAAAAMAABAAAAIAAAAAAgAAAA=</HashCode>
+ <FileName>Controllers\HttpActionContext.cs</FileName>
+ </TypeIdentifier>
+ <ShowAsAssociation>
+ <Property Name="RequestContentKeyValueModel" />
+ </ShowAsAssociation>
+ </Class>
+ <Interface Name="System.Web.Http.ValueProviders.IValueProvider" Collapsed="true">
+ <Position X="4" Y="2.5" Width="2" />
+ <TypeIdentifier>
+ <HashCode>AAAAAAEAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode>
+ <FileName>ValueProviders\IValueProvider.cs</FileName>
+ </TypeIdentifier>
+ </Interface>
+ <Interface Name="System.Net.Http.Formatting.IKeyValueModel" Collapsed="true">
+ <Position X="7.25" Y="0.5" Width="1.5" />
+ <TypeIdentifier />
+ </Interface>
+ <Font Name="Tahoma" Size="8.25" />
+</ClassDiagram> \ No newline at end of file
diff --git a/src/System.Web.Http/NonActionAttribute.cs b/src/System.Web.Http/NonActionAttribute.cs
new file mode 100644
index 00000000..b575192c
--- /dev/null
+++ b/src/System.Web.Http/NonActionAttribute.cs
@@ -0,0 +1,14 @@
+using System.Reflection;
+using System.Web.Http.Controllers;
+
+namespace System.Web.Http
+{
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+ public sealed class NonActionAttribute : Attribute, IActionMethodSelector
+ {
+ bool IActionMethodSelector.IsValidForRequest(HttpControllerContext controllerContext, MethodInfo methodInfo)
+ {
+ return false;
+ }
+ }
+}
diff --git a/src/System.Web.Http/Properties/AssemblyInfo.cs b/src/System.Web.Http/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..c7e2245b
--- /dev/null
+++ b/src/System.Web.Http/Properties/AssemblyInfo.cs
@@ -0,0 +1,9 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+[assembly: AssemblyTitle("System.Web.Http")]
+[assembly: AssemblyDescription("")]
+[assembly: Guid("70cecdcd-46f5-492b-9e1f-1d9a947f1fd1")]
+[assembly: InternalsVisibleTo("System.Web.Http.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
+[assembly: InternalsVisibleTo("System.Web.Http.Integration.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
diff --git a/src/System.Web.Http/Properties/SRResources.Designer.cs b/src/System.Web.Http/Properties/SRResources.Designer.cs
new file mode 100644
index 00000000..b03cc0b9
--- /dev/null
+++ b/src/System.Web.Http/Properties/SRResources.Designer.cs
@@ -0,0 +1,1190 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.239
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace System.Web.Http.Properties {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class SRResources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal SRResources() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("System.Web.Http.Properties.SRResources", typeof(SRResources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to After calling {0}.OnActionExecuted, the HttpActionExecutedContext properties Result and Exception were both null. At least one of these values must be non-null. To provide a new response, please set the Result object; to indicate an error, please throw an exception..
+ /// </summary>
+ internal static string ActionFilterAttribute_MustSupplyResponseOrException {
+ get {
+ return ResourceManager.GetString("ActionFilterAttribute_MustSupplyResponseOrException", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to {0} on type {1}.
+ /// </summary>
+ internal static string ActionSelector_AmbiguousMatchType {
+ get {
+ return ResourceManager.GetString("ActionSelector_AmbiguousMatchType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Ambiguous invocation of indexer in type &apos;{0}&apos;.
+ /// </summary>
+ internal static string AmbiguousIndexerInvocation {
+ get {
+ return ResourceManager.GetString("AmbiguousIndexerInvocation", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Ambiguous invocation of method &apos;{0}&apos; in type &apos;{1}&apos;.
+ /// </summary>
+ internal static string AmbiguousMethodInvocation {
+ get {
+ return ResourceManager.GetString("AmbiguousMethodInvocation", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to No action was found on the controller &apos;{0}&apos; that matches the name &apos;{1}&apos;..
+ /// </summary>
+ internal static string ApiControllerActionSelector_ActionNameNotFound {
+ get {
+ return ResourceManager.GetString("ApiControllerActionSelector_ActionNameNotFound", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to No action was found on the controller &apos;{0}&apos; that matches the request..
+ /// </summary>
+ internal static string ApiControllerActionSelector_ActionNotFound {
+ get {
+ return ResourceManager.GetString("ApiControllerActionSelector_ActionNotFound", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Multiple actions were found that match the request: {0}.
+ /// </summary>
+ internal static string ApiControllerActionSelector_AmbiguousMatch {
+ get {
+ return ResourceManager.GetString("ApiControllerActionSelector_AmbiguousMatch", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The requested resource does not support http method &apos;{0}&apos;..
+ /// </summary>
+ internal static string ApiControllerActionSelector_HttpMethodNotSupported {
+ get {
+ return ResourceManager.GetString("ApiControllerActionSelector_HttpMethodNotSupported", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Documentation for &apos;{0}&apos;..
+ /// </summary>
+ internal static string ApiExplorer_DefaultDocumentation {
+ get {
+ return ResourceManager.GetString("ApiExplorer_DefaultDocumentation", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A value for &apos;{0}&apos; is required but was not present in the request..
+ /// </summary>
+ internal static string BindingBehavior_ValueNotFound {
+ get {
+ return ResourceManager.GetString("BindingBehavior_ValueNotFound", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Indexing of multi-dimensional arrays is not supported.
+ /// </summary>
+ internal static string CannotIndexMultiDimArray {
+ get {
+ return ResourceManager.GetString("CannotIndexMultiDimArray", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Cannot reuse an &apos;{0}&apos; instance. &apos;{0}&apos; has to be constructed per incoming message. Check your custom &apos;{1}&apos; and make sure that it will not manufacture the same instance..
+ /// </summary>
+ internal static string CannotSupportSingletonInstance {
+ get {
+ return ResourceManager.GetString("CannotSupportSingletonInstance", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The field {0} must be a date..
+ /// </summary>
+ internal static string ClientDataTypeModelValidatorProvider_FieldMustBeDate {
+ get {
+ return ResourceManager.GetString("ClientDataTypeModelValidatorProvider_FieldMustBeDate", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The field {0} must be a number..
+ /// </summary>
+ internal static string ClientDataTypeModelValidatorProvider_FieldMustBeNumeric {
+ get {
+ return ResourceManager.GetString("ClientDataTypeModelValidatorProvider_FieldMustBeNumeric", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &apos;]&apos; or &apos;,&apos; expected.
+ /// </summary>
+ internal static string CloseBracketOrCommaExpected {
+ get {
+ return ResourceManager.GetString("CloseBracketOrCommaExpected", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &apos;)&apos; or &apos;,&apos; expected.
+ /// </summary>
+ internal static string CloseParenOrCommaExpected {
+ get {
+ return ResourceManager.GetString("CloseParenOrCommaExpected", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &apos;)&apos; or operator expected.
+ /// </summary>
+ internal static string CloseParenOrOperatorExpected {
+ get {
+ return ResourceManager.GetString("CloseParenOrOperatorExpected", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The property {0}.{1} could not be found..
+ /// </summary>
+ internal static string Common_PropertyNotFound {
+ get {
+ return ResourceManager.GetString("Common_PropertyNotFound", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The type {0} must derive from {1}..
+ /// </summary>
+ internal static string Common_TypeMustDriveFromType {
+ get {
+ return ResourceManager.GetString("Common_TypeMustDriveFromType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The type &apos;{0}&apos; does not implement the interface &apos;{1}&apos;..
+ /// </summary>
+ internal static string Common_TypeMustImplementInterface {
+ get {
+ return ResourceManager.GetString("Common_TypeMustImplementInterface", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to {0} has a DisplayColumn attribute for {1}, but property {1} does not exist..
+ /// </summary>
+ internal static string DataAnnotationsModelMetadataProvider_UnknownProperty {
+ get {
+ return ResourceManager.GetString("DataAnnotationsModelMetadataProvider_UnknownProperty", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to {0} has a DisplayColumn attribute for {1}, but property {1} does not have a public getter..
+ /// </summary>
+ internal static string DataAnnotationsModelMetadataProvider_UnreadableProperty {
+ get {
+ return ResourceManager.GetString("DataAnnotationsModelMetadataProvider_UnreadableProperty", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The type {0} must have a public constructor which accepts three parameters of types {1}, {2}, and {3}..
+ /// </summary>
+ internal static string DataAnnotationsModelValidatorProvider_ConstructorRequirements {
+ get {
+ return ResourceManager.GetString("DataAnnotationsModelValidatorProvider_ConstructorRequirements", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The type {0} must have a public constructor which accepts two parameters of types {1} and {2}..
+ /// </summary>
+ internal static string DataAnnotationsModelValidatorProvider_ValidatableConstructorRequirements {
+ get {
+ return ResourceManager.GetString("DataAnnotationsModelValidatorProvider_ValidatableConstructorRequirements", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Multiple types were found that match the controller named &apos;{0}&apos;. This can happen if the route that services this request (&apos;{1}&apos;) found multiple controllers defined with the same name but differing namespaces, which is not supported.
+ ///
+ ///The request for &apos;{0}&apos; has found the following matching controllers:{2}.
+ /// </summary>
+ internal static string DefaultControllerFactory_ControllerNameAmbiguous_WithRouteTemplate {
+ get {
+ return ResourceManager.GetString("DefaultControllerFactory_ControllerNameAmbiguous_WithRouteTemplate", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to No type was found that matches the controller named &apos;{0}&apos;..
+ /// </summary>
+ internal static string DefaultControllerFactory_ControllerNameNotFound {
+ get {
+ return ResourceManager.GetString("DefaultControllerFactory_ControllerNameNotFound", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to An error occurred when trying to create a controller of type &apos;{0}&apos;. Make sure that the controller has a parameterless public constructor..
+ /// </summary>
+ internal static string DefaultControllerFactory_ErrorCreatingController {
+ get {
+ return ResourceManager.GetString("DefaultControllerFactory_ErrorCreatingController", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The &apos;{0}&apos; list is invalid because it contains one or more null items..
+ /// </summary>
+ internal static string DelegatingHandlerArrayContainsNullItem {
+ get {
+ return ResourceManager.GetString("DelegatingHandlerArrayContainsNullItem", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The &apos;{0}&apos; list is invalid because the property &apos;{1}&apos; of &apos;{2}&apos; is not null..
+ /// </summary>
+ internal static string DelegatingHandlerArrayHasNonNullInnerHandler {
+ get {
+ return ResourceManager.GetString("DelegatingHandlerArrayHasNonNullInnerHandler", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The type {0} does not appear to implement Microsoft.Practices.ServiceLocation.IServiceLocator..
+ /// </summary>
+ internal static string DependencyResolver_DoesNotImplementICommonServiceLocator {
+ get {
+ return ResourceManager.GetString("DependencyResolver_DoesNotImplementICommonServiceLocator", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to No service registered for type &apos;{0}&apos;..
+ /// </summary>
+ internal static string DependencyResolverNoService {
+ get {
+ return ResourceManager.GetString("DependencyResolverNoService", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The &apos;{0}&apos; must contain an item named &apos;{1}&apos; with a value of type &apos;{2}&apos;..
+ /// </summary>
+ internal static string DictionaryMissingRequiredValue {
+ get {
+ return ResourceManager.GetString("DictionaryMissingRequiredValue", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Digit expected.
+ /// </summary>
+ internal static string DigitExpected {
+ get {
+ return ResourceManager.GetString("DigitExpected", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The identifier &apos;{0}&apos; was defined more than once.
+ /// </summary>
+ internal static string DuplicateIdentifier {
+ get {
+ return ResourceManager.GetString("DuplicateIdentifier", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Expression expected.
+ /// </summary>
+ internal static string ExpressionExpected {
+ get {
+ return ResourceManager.GetString("ExpressionExpected", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Expression of type &apos;{0}&apos; expected.
+ /// </summary>
+ internal static string ExpressionTypeMismatch {
+ get {
+ return ResourceManager.GetString("ExpressionTypeMismatch", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The type &apos;{0}&apos; is not an open generic type..
+ /// </summary>
+ internal static string GenericModelBinderProvider_ParameterMustSpecifyOpenGenericType {
+ get {
+ return ResourceManager.GetString("GenericModelBinderProvider_ParameterMustSpecifyOpenGenericType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The open model type &apos;{0}&apos; has {1} generic type argument(s), but the open binder type &apos;{2}&apos; has {3} generic type argument(s). The binder type must not be an open generic type or must have the same number of generic arguments as the open model type..
+ /// </summary>
+ internal static string GenericModelBinderProvider_TypeArgumentCountMismatch {
+ get {
+ return ResourceManager.GetString("GenericModelBinderProvider_TypeArgumentCountMismatch", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to This &apos;{0}&apos; instance has been disposed and can no longer accept HTTP requests..
+ /// </summary>
+ internal static string HttpMessageHandlerDisposed {
+ get {
+ return ResourceManager.GetString("HttpMessageHandlerDisposed", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The request does not have an associated configuration object or the provided configuration was null..
+ /// </summary>
+ internal static string HttpRequestMessageExtensions_NoConfiguration {
+ get {
+ return ResourceManager.GetString("HttpRequestMessageExtensions_NoConfiguration", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Processing of the HTTP request resulted in an exception. Please see the HTTP response returned by the &apos;{0}&apos; property of this exception for details..
+ /// </summary>
+ internal static string HttpResponseExceptionMessage {
+ get {
+ return ResourceManager.GetString("HttpResponseExceptionMessage", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Identifier expected.
+ /// </summary>
+ internal static string IdentifierExpected {
+ get {
+ return ResourceManager.GetString("IdentifierExpected", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Operator &apos;{0}&apos; incompatible with operand type &apos;{1}&apos;.
+ /// </summary>
+ internal static string IncompatibleOperand {
+ get {
+ return ResourceManager.GetString("IncompatibleOperand", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Operator &apos;{0}&apos; incompatible with operand types &apos;{1}&apos; and &apos;{2}&apos;.
+ /// </summary>
+ internal static string IncompatibleOperands {
+ get {
+ return ResourceManager.GetString("IncompatibleOperands", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Syntax error &apos;{0}&apos;.
+ /// </summary>
+ internal static string InvalidCharacter {
+ get {
+ return ResourceManager.GetString("InvalidCharacter", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Invalid hexadecimal literal..
+ /// </summary>
+ internal static string InvalidHexLiteral {
+ get {
+ return ResourceManager.GetString("InvalidHexLiteral", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Array index must be an integer expression.
+ /// </summary>
+ internal static string InvalidIndex {
+ get {
+ return ResourceManager.GetString("InvalidIndex", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Invalid integer literal &apos;{0}&apos;.
+ /// </summary>
+ internal static string InvalidIntegerLiteral {
+ get {
+ return ResourceManager.GetString("InvalidIntegerLiteral", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Invalid query operator &apos;{0}&apos;..
+ /// </summary>
+ internal static string InvalidQueryOperator {
+ get {
+ return ResourceManager.GetString("InvalidQueryOperator", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Invalid real literal &apos;{0}&apos;.
+ /// </summary>
+ internal static string InvalidRealLiteral {
+ get {
+ return ResourceManager.GetString("InvalidRealLiteral", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Invalid &apos;{0}&apos; type creation expression..
+ /// </summary>
+ internal static string InvalidTypeCreationExpression {
+ get {
+ return ResourceManager.GetString("InvalidTypeCreationExpression", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The key is invalid JQuery syntax because it is missing a closing bracket.
+ /// </summary>
+ internal static string JQuerySyntaxMissingClosingBracket {
+ get {
+ return ResourceManager.GetString("JQuerySyntaxMissingClosingBracket", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Method &apos;{0}&apos; in type &apos;{1}&apos; does not return a value.
+ /// </summary>
+ internal static string MethodIsVoid {
+ get {
+ return ResourceManager.GetString("MethodIsVoid", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The {0} property is required..
+ /// </summary>
+ internal static string MissingRequiredMember {
+ get {
+ return ResourceManager.GetString("MissingRequiredMember", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The value &apos;{0}&apos; is not valid for {1}..
+ /// </summary>
+ internal static string ModelBinderConfig_ValueInvalid {
+ get {
+ return ResourceManager.GetString("ModelBinderConfig_ValueInvalid", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A value is required..
+ /// </summary>
+ internal static string ModelBinderConfig_ValueRequired {
+ get {
+ return ResourceManager.GetString("ModelBinderConfig_ValueRequired", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A binder for type {0} could not be located..
+ /// </summary>
+ internal static string ModelBinderProviderCollection_BinderForTypeNotFound {
+ get {
+ return ResourceManager.GetString("ModelBinderProviderCollection_BinderForTypeNotFound", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The type &apos;{0}&apos; does not subclass {1} or implement the interface {2}..
+ /// </summary>
+ internal static string ModelBinderProviderCollection_InvalidBinderType {
+ get {
+ return ResourceManager.GetString("ModelBinderProviderCollection_InvalidBinderType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The binding context has a null Model, but this binder requires a non-null model of type &apos;{0}&apos;..
+ /// </summary>
+ internal static string ModelBinderUtil_ModelCannotBeNull {
+ get {
+ return ResourceManager.GetString("ModelBinderUtil_ModelCannotBeNull", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The binding context has a Model of type &apos;{0}&apos;, but this binder can only operate on models of type &apos;{1}&apos;..
+ /// </summary>
+ internal static string ModelBinderUtil_ModelInstanceIsWrong {
+ get {
+ return ResourceManager.GetString("ModelBinderUtil_ModelInstanceIsWrong", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The binding context cannot have a null ModelMetadata..
+ /// </summary>
+ internal static string ModelBinderUtil_ModelMetadataCannotBeNull {
+ get {
+ return ResourceManager.GetString("ModelBinderUtil_ModelMetadataCannotBeNull", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The binding context has a ModelType of &apos;{0}&apos;, but this binder can only operate on models of type &apos;{1}&apos;..
+ /// </summary>
+ internal static string ModelBinderUtil_ModelTypeIsWrong {
+ get {
+ return ResourceManager.GetString("ModelBinderUtil_ModelTypeIsWrong", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The ModelMetadata property must be set before accessing this property..
+ /// </summary>
+ internal static string ModelBindingContext_ModelMetadataMustBeSet {
+ get {
+ return ResourceManager.GetString("ModelBindingContext_ModelMetadataMustBeSet", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Unable to determine the type of the content because the type &apos;{0}&apos; has two or more type parameters..
+ /// </summary>
+ internal static string MultipleTypeParametersForHttpContentType {
+ get {
+ return ResourceManager.GetString("MultipleTypeParametersForHttpContentType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to No applicable aggregate method &apos;{0}&apos; exists.
+ /// </summary>
+ internal static string NoApplicableAggregate {
+ get {
+ return ResourceManager.GetString("NoApplicableAggregate", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to No applicable indexer exists in type &apos;{0}&apos;.
+ /// </summary>
+ internal static string NoApplicableIndexer {
+ get {
+ return ResourceManager.GetString("NoApplicableIndexer", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to No applicable method &apos;{0}&apos; exists in type &apos;{1}&apos;.
+ /// </summary>
+ internal static string NoApplicableMethod {
+ get {
+ return ResourceManager.GetString("NoApplicableMethod", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &apos;(&apos; expected.
+ /// </summary>
+ internal static string OpenParenExpected {
+ get {
+ return ResourceManager.GetString("OpenParenExpected", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Can&apos;t bind multiple parameters (&apos;{0}&apos; and &apos;{1}&apos;) to the request&apos;s content..
+ /// </summary>
+ internal static string ParameterBindingCantHaveMultipleBodyParameters {
+ get {
+ return ResourceManager.GetString("ParameterBindingCantHaveMultipleBodyParameters", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Can&apos;t bind parameter &apos;{0}&apos; because it has conflicting attributes on it..
+ /// </summary>
+ internal static string ParameterBindingConflictingAttributes {
+ get {
+ return ResourceManager.GetString("ParameterBindingConflictingAttributes", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Can&apos;t bind parameter &apos;{1}&apos;. Must specify a custom model binder to bind parameters of type &apos;{0}&apos;..
+ /// </summary>
+ internal static string ParameterBindingIllegalType {
+ get {
+ return ResourceManager.GetString("ParameterBindingIllegalType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to {0} (at index {1}).
+ /// </summary>
+ internal static string ParseExceptionFormat {
+ get {
+ return ResourceManager.GetString("ParseExceptionFormat", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The OData query parameter &apos;{0}&apos; has an invalid value. The value should be a positive integer. The provided value was &apos;{1}&apos;.
+ /// </summary>
+ internal static string PositiveIntegerExpectedForODataQueryParameter {
+ get {
+ return ResourceManager.GetString("PositiveIntegerExpectedForODataQueryParameter", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The parameters dictionary contains a null entry for parameter &apos;{0}&apos; of non-nullable type &apos;{1}&apos; for method &apos;{2}&apos; in &apos;{3}&apos;. An optional parameter must be a reference type, a nullable type, or be declared as an optional parameter..
+ /// </summary>
+ internal static string ReflectedActionDescriptor_ParameterCannotBeNull {
+ get {
+ return ResourceManager.GetString("ReflectedActionDescriptor_ParameterCannotBeNull", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The parameters dictionary does not contain an entry for parameter &apos;{0}&apos; of type &apos;{1}&apos; for method &apos;{2}&apos; in &apos;{3}&apos;. The dictionary must contain an entry for each parameter, including parameters that have null values..
+ /// </summary>
+ internal static string ReflectedActionDescriptor_ParameterNotInDictionary {
+ get {
+ return ResourceManager.GetString("ReflectedActionDescriptor_ParameterNotInDictionary", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The parameters dictionary contains an invalid entry for parameter &apos;{0}&apos; for method &apos;{1}&apos; in &apos;{2}&apos;. The dictionary contains a value of type &apos;{3}&apos;, but the parameter requires a value of type &apos;{4}&apos;..
+ /// </summary>
+ internal static string ReflectedActionDescriptor_ParameterValueHasWrongType {
+ get {
+ return ResourceManager.GetString("ReflectedActionDescriptor_ParameterValueHasWrongType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to ResultLimitAttribute cannot be applied to action &apos;{0}&apos; because it&apos;s return type is not IEnumerable..
+ /// </summary>
+ internal static string ResultLimitFilter_InvalidReturnType {
+ get {
+ return ResourceManager.GetString("ResultLimitFilter_InvalidReturnType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The limit specified by the ResultLimitAttribute applied to action &apos;{0}&apos; must be greater than zero..
+ /// </summary>
+ internal static string ResultLimitFilter_OutOfRange {
+ get {
+ return ResourceManager.GetString("ResultLimitFilter_OutOfRange", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Adding or removing items from a &apos;{0}&apos; is not supported. Please use a key when adding and removing items..
+ /// </summary>
+ internal static string Route_AddRemoveWithNoKeyNotSupported {
+ get {
+ return ResourceManager.GetString("Route_AddRemoveWithNoKeyNotSupported", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter..
+ /// </summary>
+ internal static string Route_CannotHaveCatchAllInMultiSegment {
+ get {
+ return ResourceManager.GetString("Route_CannotHaveCatchAllInMultiSegment", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A path segment cannot contain two consecutive parameters. They must be separated by a &apos;/&apos; or by a literal string..
+ /// </summary>
+ internal static string Route_CannotHaveConsecutiveParameters {
+ get {
+ return ResourceManager.GetString("Route_CannotHaveConsecutiveParameters", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The route template separator character &apos;/&apos; cannot appear consecutively. It must be separated by either a parameter or a literal value..
+ /// </summary>
+ internal static string Route_CannotHaveConsecutiveSeparators {
+ get {
+ return ResourceManager.GetString("Route_CannotHaveConsecutiveSeparators", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A catch-all parameter can only appear as the last segment of the route template..
+ /// </summary>
+ internal static string Route_CatchAllMustBeLast {
+ get {
+ return ResourceManager.GetString("Route_CatchAllMustBeLast", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The route parameter name &apos;{0}&apos; is invalid. Route parameter names must be non-empty and cannot contain these characters: &quot;{{&quot;, &quot;}}&quot;, &quot;/&quot;, &quot;?&quot;.
+ /// </summary>
+ internal static string Route_InvalidParameterName {
+ get {
+ return ResourceManager.GetString("Route_InvalidParameterName", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The route template cannot start with a &apos;/&apos; or &apos;~&apos; character and it cannot contain a &apos;?&apos; character..
+ /// </summary>
+ internal static string Route_InvalidRouteTemplate {
+ get {
+ return ResourceManager.GetString("Route_InvalidRouteTemplate", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to There is an incomplete parameter in this path segment: &apos;{0}&apos;. Check that each &apos;{{&apos; character has a matching &apos;}}&apos; character..
+ /// </summary>
+ internal static string Route_MismatchedParameter {
+ get {
+ return ResourceManager.GetString("Route_MismatchedParameter", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The route parameter name &apos;{0}&apos; appears more than one time in the route template..
+ /// </summary>
+ internal static string Route_RepeatedParameter {
+ get {
+ return ResourceManager.GetString("Route_RepeatedParameter", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The constraint entry &apos;{0}&apos; on the route with route template &apos;{1}&apos; must have a string value or be of a type which implements &apos;{2}&apos;..
+ /// </summary>
+ internal static string Route_ValidationMustBeStringOrCustomConstraint {
+ get {
+ return ResourceManager.GetString("Route_ValidationMustBeStringOrCustomConstraint", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A route named &apos;{0}&apos; could not be found in the route collection..
+ /// </summary>
+ internal static string RouteCollection_NameNotFound {
+ get {
+ return ResourceManager.GetString("RouteCollection_NameNotFound", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Syntax error.
+ /// </summary>
+ internal static string SyntaxError {
+ get {
+ return ResourceManager.GetString("SyntaxError", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Action filter for &apos;{0}&apos;.
+ /// </summary>
+ internal static string TraceActionFilterMessage {
+ get {
+ return ResourceManager.GetString("TraceActionFilterMessage", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Action=&apos;{0}&apos;.
+ /// </summary>
+ internal static string TraceActionInvokeMessage {
+ get {
+ return ResourceManager.GetString("TraceActionInvokeMessage", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Action returned &apos;{0}&apos;.
+ /// </summary>
+ internal static string TraceActionReturnValue {
+ get {
+ return ResourceManager.GetString("TraceActionReturnValue", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Selected action &apos;{0}&apos;.
+ /// </summary>
+ internal static string TraceActionSelectedMessage {
+ get {
+ return ResourceManager.GetString("TraceActionSelectedMessage", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Binding parameter &apos;{0}&apos;.
+ /// </summary>
+ internal static string TraceBeginParameterBind {
+ get {
+ return ResourceManager.GetString("TraceBeginParameterBind", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Cancelled.
+ /// </summary>
+ internal static string TraceCancelledMessage {
+ get {
+ return ResourceManager.GetString("TraceCancelledMessage", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Controller name=&apos;{0}&apos;, route=&apos;{1}&apos;.
+ /// </summary>
+ internal static string TraceControllerNameAndRouteMessage {
+ get {
+ return ResourceManager.GetString("TraceControllerNameAndRouteMessage", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Parameter &apos;{0}&apos; bound to the value &apos;{1}&apos;.
+ /// </summary>
+ internal static string TraceEndParameterBind {
+ get {
+ return ResourceManager.GetString("TraceEndParameterBind", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Parameter &apos;{0}&apos; failed to bind..
+ /// </summary>
+ internal static string TraceEndParameterBindNoBind {
+ get {
+ return ResourceManager.GetString("TraceEndParameterBindNoBind", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Will use same &apos;{0}&apos; formatter.
+ /// </summary>
+ internal static string TraceGetPerRequestFormatterEndMessage {
+ get {
+ return ResourceManager.GetString("TraceGetPerRequestFormatterEndMessage", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Will use new &apos;{0}&apos; formatter.
+ /// </summary>
+ internal static string TraceGetPerRequestFormatterEndMessageNew {
+ get {
+ return ResourceManager.GetString("TraceGetPerRequestFormatterEndMessageNew", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Obtaining formatter of type &apos;{0}&apos; for type=&apos;{1}&apos;, mediaType=&apos;{2}&apos;.
+ /// </summary>
+ internal static string TraceGetPerRequestFormatterMessage {
+ get {
+ return ResourceManager.GetString("TraceGetPerRequestFormatterMessage", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Null formatter.
+ /// </summary>
+ internal static string TraceGetPerRequestNullFormatterEndMessage {
+ get {
+ return ResourceManager.GetString("TraceGetPerRequestNullFormatterEndMessage", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Invoking action &apos;{0}&apos;.
+ /// </summary>
+ internal static string TraceInvokingAction {
+ get {
+ return ResourceManager.GetString("TraceInvokingAction", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to {0}: {1}.
+ /// </summary>
+ internal static string TraceModelStateErrorMessage {
+ get {
+ return ResourceManager.GetString("TraceModelStateErrorMessage", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Model state is invalid. {0}.
+ /// </summary>
+ internal static string TraceModelStateInvalidMessage {
+ get {
+ return ResourceManager.GetString("TraceModelStateInvalidMessage", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Type=&apos;{0}&apos;, formatters=[{1}].
+ /// </summary>
+ internal static string TraceNegotiateFormatter {
+ get {
+ return ResourceManager.GetString("TraceNegotiateFormatter", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to none.
+ /// </summary>
+ internal static string TraceNoneObjectMessage {
+ get {
+ return ResourceManager.GetString("TraceNoneObjectMessage", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Type=&apos;{0}&apos;, content-type=&apos;{1}&apos;.
+ /// </summary>
+ internal static string TraceReadFromStreamMessage {
+ get {
+ return ResourceManager.GetString("TraceReadFromStreamMessage", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Value read=&apos;{0}&apos;.
+ /// </summary>
+ internal static string TraceReadFromStreamValueMessage {
+ get {
+ return ResourceManager.GetString("TraceReadFromStreamValueMessage", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Content-type=&apos;{0}&apos;, content-length={1}.
+ /// </summary>
+ internal static string TraceRequestCompleteMessage {
+ get {
+ return ResourceManager.GetString("TraceRequestCompleteMessage", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Selected formatter=&apos;{0}&apos;, content-type=&apos;{1}&apos;.
+ /// </summary>
+ internal static string TraceSelectedFormatter {
+ get {
+ return ResourceManager.GetString("TraceSelectedFormatter", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to unknown.
+ /// </summary>
+ internal static string TraceUnknownMessage {
+ get {
+ return ResourceManager.GetString("TraceUnknownMessage", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Model state is valid. Values: {0}.
+ /// </summary>
+ internal static string TraceValidModelState {
+ get {
+ return ResourceManager.GetString("TraceValidModelState", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Value=&apos;{0}&apos;, type=&apos;{1}&apos;, content-type=&apos;{2}&apos;.
+ /// </summary>
+ internal static string TraceWriteToStreamMessage {
+ get {
+ return ResourceManager.GetString("TraceWriteToStreamMessage", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to This file is automatically generated. Please do not modify the contents of this file..
+ /// </summary>
+ internal static string TypeCache_DoNotModify {
+ get {
+ return ResourceManager.GetString("TypeCache_DoNotModify", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Unknown identifier &apos;{0}&apos;.
+ /// </summary>
+ internal static string UnknownIdentifier {
+ get {
+ return ResourceManager.GetString("UnknownIdentifier", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to No property or field &apos;{0}&apos; exists in type &apos;{1}&apos;.
+ /// </summary>
+ internal static string UnknownPropertyOrField {
+ get {
+ return ResourceManager.GetString("UnknownPropertyOrField", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Unterminated string literal.
+ /// </summary>
+ internal static string UnterminatedStringLiteral {
+ get {
+ return ResourceManager.GetString("UnterminatedStringLiteral", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The query specified in the URI is not valid..
+ /// </summary>
+ internal static string UriQueryStringInvalid {
+ get {
+ return ResourceManager.GetString("UriQueryStringInvalid", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The model object inside the metadata claimed to be compatible with {0}, but was actually {1}..
+ /// </summary>
+ internal static string ValidatableObjectAdapter_IncompatibleType {
+ get {
+ return ResourceManager.GetString("ValidatableObjectAdapter_IncompatibleType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A value is required but was not present in the request..
+ /// </summary>
+ internal static string Validation_ValueNotFound {
+ get {
+ return ResourceManager.GetString("Validation_ValueNotFound", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Could not create a &apos;{0}&apos; from &apos;{1}&apos;. Please ensure it derives from &apos;{0}&apos; and has a public parameterless constructor..
+ /// </summary>
+ internal static string ValueProviderFactory_Cannot_Create {
+ get {
+ return ResourceManager.GetString("ValueProviderFactory_Cannot_Create", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The parameter conversion from type &apos;{0}&apos; to type &apos;{1}&apos; failed. See the inner exception for more information..
+ /// </summary>
+ internal static string ValueProviderResult_ConversionThrew {
+ get {
+ return ResourceManager.GetString("ValueProviderResult_ConversionThrew", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The parameter conversion from type &apos;{0}&apos; to type &apos;{1}&apos; failed because no type converter can convert between these types..
+ /// </summary>
+ internal static string ValueProviderResult_NoConverterExists {
+ get {
+ return ResourceManager.GetString("ValueProviderResult_NoConverterExists", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/Properties/SRResources.resx b/src/System.Web.Http/Properties/SRResources.resx
new file mode 100644
index 00000000..e88d5f6c
--- /dev/null
+++ b/src/System.Web.Http/Properties/SRResources.resx
@@ -0,0 +1,497 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="BindingBehavior_ValueNotFound" xml:space="preserve">
+ <value>A value for '{0}' is required but was not present in the request.</value>
+ </data>
+ <data name="Common_TypeMustImplementInterface" xml:space="preserve">
+ <value>The type '{0}' does not implement the interface '{1}'.</value>
+ </data>
+ <data name="GenericModelBinderProvider_ParameterMustSpecifyOpenGenericType" xml:space="preserve">
+ <value>The type '{0}' is not an open generic type.</value>
+ </data>
+ <data name="GenericModelBinderProvider_TypeArgumentCountMismatch" xml:space="preserve">
+ <value>The open model type '{0}' has {1} generic type argument(s), but the open binder type '{2}' has {3} generic type argument(s). The binder type must not be an open generic type or must have the same number of generic arguments as the open model type.</value>
+ </data>
+ <data name="ModelBinderUtil_ModelCannotBeNull" xml:space="preserve">
+ <value>The binding context has a null Model, but this binder requires a non-null model of type '{0}'.</value>
+ </data>
+ <data name="ModelBinderUtil_ModelInstanceIsWrong" xml:space="preserve">
+ <value>The binding context has a Model of type '{0}', but this binder can only operate on models of type '{1}'.</value>
+ </data>
+ <data name="ModelBinderUtil_ModelMetadataCannotBeNull" xml:space="preserve">
+ <value>The binding context cannot have a null ModelMetadata.</value>
+ </data>
+ <data name="ModelBinderUtil_ModelTypeIsWrong" xml:space="preserve">
+ <value>The binding context has a ModelType of '{0}', but this binder can only operate on models of type '{1}'.</value>
+ </data>
+ <data name="ModelBinderConfig_ValueInvalid" xml:space="preserve">
+ <value>The value '{0}' is not valid for {1}.</value>
+ </data>
+ <data name="ModelBinderConfig_ValueRequired" xml:space="preserve">
+ <value>A value is required.</value>
+ </data>
+ <data name="ModelBindingContext_ModelMetadataMustBeSet" xml:space="preserve">
+ <value>The ModelMetadata property must be set before accessing this property.</value>
+ </data>
+ <data name="Common_PropertyNotFound" xml:space="preserve">
+ <value>The property {0}.{1} could not be found.</value>
+ </data>
+ <data name="DataAnnotationsModelMetadataProvider_UnknownProperty" xml:space="preserve">
+ <value>{0} has a DisplayColumn attribute for {1}, but property {1} does not exist.</value>
+ </data>
+ <data name="DataAnnotationsModelMetadataProvider_UnreadableProperty" xml:space="preserve">
+ <value>{0} has a DisplayColumn attribute for {1}, but property {1} does not have a public getter.</value>
+ </data>
+ <data name="DependencyResolver_DoesNotImplementICommonServiceLocator" xml:space="preserve">
+ <value>The type {0} does not appear to implement Microsoft.Practices.ServiceLocation.IServiceLocator.</value>
+ </data>
+ <data name="ModelBinderProviderCollection_BinderForTypeNotFound" xml:space="preserve">
+ <value>A binder for type {0} could not be located.</value>
+ </data>
+ <data name="ModelBinderProviderCollection_InvalidBinderType" xml:space="preserve">
+ <value>The type '{0}' does not subclass {1} or implement the interface {2}.</value>
+ </data>
+ <data name="ValueProviderResult_ConversionThrew" xml:space="preserve">
+ <value>The parameter conversion from type '{0}' to type '{1}' failed. See the inner exception for more information.</value>
+ </data>
+ <data name="ValueProviderResult_NoConverterExists" xml:space="preserve">
+ <value>The parameter conversion from type '{0}' to type '{1}' failed because no type converter can convert between these types.</value>
+ </data>
+ <data name="ClientDataTypeModelValidatorProvider_FieldMustBeDate" xml:space="preserve">
+ <value>The field {0} must be a date.</value>
+ </data>
+ <data name="ClientDataTypeModelValidatorProvider_FieldMustBeNumeric" xml:space="preserve">
+ <value>The field {0} must be a number.</value>
+ </data>
+ <data name="Common_TypeMustDriveFromType" xml:space="preserve">
+ <value>The type {0} must derive from {1}.</value>
+ </data>
+ <data name="DataAnnotationsModelValidatorProvider_ConstructorRequirements" xml:space="preserve">
+ <value>The type {0} must have a public constructor which accepts three parameters of types {1}, {2}, and {3}.</value>
+ </data>
+ <data name="DataAnnotationsModelValidatorProvider_ValidatableConstructorRequirements" xml:space="preserve">
+ <value>The type {0} must have a public constructor which accepts two parameters of types {1} and {2}.</value>
+ </data>
+ <data name="ValidatableObjectAdapter_IncompatibleType" xml:space="preserve">
+ <value>The model object inside the metadata claimed to be compatible with {0}, but was actually {1}.</value>
+ </data>
+ <data name="DelegatingHandlerArrayContainsNullItem" xml:space="preserve">
+ <value>The '{0}' list is invalid because it contains one or more null items.</value>
+ </data>
+ <data name="DelegatingHandlerArrayHasNonNullInnerHandler" xml:space="preserve">
+ <value>The '{0}' list is invalid because the property '{1}' of '{2}' is not null.</value>
+ </data>
+ <data name="HttpMessageHandlerDisposed" xml:space="preserve">
+ <value>This '{0}' instance has been disposed and can no longer accept HTTP requests.</value>
+ </data>
+ <data name="HttpResponseExceptionMessage" xml:space="preserve">
+ <value>Processing of the HTTP request resulted in an exception. Please see the HTTP response returned by the '{0}' property of this exception for details.</value>
+ </data>
+ <data name="ApiControllerActionSelector_AmbiguousMatch" xml:space="preserve">
+ <value>Multiple actions were found that match the request: {0}</value>
+ </data>
+ <data name="MultipleTypeParametersForHttpContentType" xml:space="preserve">
+ <value>Unable to determine the type of the content because the type '{0}' has two or more type parameters.</value>
+ </data>
+ <data name="DefaultControllerFactory_ControllerNameAmbiguous_WithRouteTemplate" xml:space="preserve">
+ <value>Multiple types were found that match the controller named '{0}'. This can happen if the route that services this request ('{1}') found multiple controllers defined with the same name but differing namespaces, which is not supported.
+
+The request for '{0}' has found the following matching controllers:{2}</value>
+ </data>
+ <data name="DefaultControllerFactory_ControllerNameNotFound" xml:space="preserve">
+ <value>No type was found that matches the controller named '{0}'.</value>
+ </data>
+ <data name="DefaultControllerFactory_ErrorCreatingController" xml:space="preserve">
+ <value>An error occurred when trying to create a controller of type '{0}'. Make sure that the controller has a parameterless public constructor.</value>
+ </data>
+ <data name="TypeCache_DoNotModify" xml:space="preserve">
+ <value>This file is automatically generated. Please do not modify the contents of this file.</value>
+ </data>
+ <data name="DependencyResolverNoService" xml:space="preserve">
+ <value>No service registered for type '{0}'.</value>
+ </data>
+ <data name="ApiControllerActionSelector_ActionNotFound" xml:space="preserve">
+ <value>No action was found on the controller '{0}' that matches the request.</value>
+ </data>
+ <data name="ApiControllerActionSelector_ActionNameNotFound" xml:space="preserve">
+ <value>No action was found on the controller '{0}' that matches the name '{1}'.</value>
+ </data>
+ <data name="ApiControllerActionSelector_HttpMethodNotSupported" xml:space="preserve">
+ <value>The requested resource does not support http method '{0}'.</value>
+ </data>
+ <data name="ActionSelector_AmbiguousMatchType" xml:space="preserve">
+ <value>{0} on type {1}</value>
+ </data>
+ <data name="ReflectedActionDescriptor_ParameterCannotBeNull" xml:space="preserve">
+ <value>The parameters dictionary contains a null entry for parameter '{0}' of non-nullable type '{1}' for method '{2}' in '{3}'. An optional parameter must be a reference type, a nullable type, or be declared as an optional parameter.</value>
+ </data>
+ <data name="ReflectedActionDescriptor_ParameterNotInDictionary" xml:space="preserve">
+ <value>The parameters dictionary does not contain an entry for parameter '{0}' of type '{1}' for method '{2}' in '{3}'. The dictionary must contain an entry for each parameter, including parameters that have null values.</value>
+ </data>
+ <data name="ReflectedActionDescriptor_ParameterValueHasWrongType" xml:space="preserve">
+ <value>The parameters dictionary contains an invalid entry for parameter '{0}' for method '{1}' in '{2}'. The dictionary contains a value of type '{3}', but the parameter requires a value of type '{4}'.</value>
+ </data>
+ <data name="DictionaryMissingRequiredValue" xml:space="preserve">
+ <value>The '{0}' must contain an item named '{1}' with a value of type '{2}'.</value>
+ </data>
+ <data name="Route_CannotHaveCatchAllInMultiSegment" xml:space="preserve">
+ <value>A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter.</value>
+ </data>
+ <data name="Route_CannotHaveConsecutiveParameters" xml:space="preserve">
+ <value>A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string.</value>
+ </data>
+ <data name="Route_CannotHaveConsecutiveSeparators" xml:space="preserve">
+ <value>The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value.</value>
+ </data>
+ <data name="Route_CatchAllMustBeLast" xml:space="preserve">
+ <value>A catch-all parameter can only appear as the last segment of the route template.</value>
+ </data>
+ <data name="Route_InvalidParameterName" xml:space="preserve">
+ <value>The route parameter name '{0}' is invalid. Route parameter names must be non-empty and cannot contain these characters: "{{", "}}", "/", "?"</value>
+ </data>
+ <data name="Route_InvalidRouteTemplate" xml:space="preserve">
+ <value>The route template cannot start with a '/' or '~' character and it cannot contain a '?' character.</value>
+ </data>
+ <data name="Route_MismatchedParameter" xml:space="preserve">
+ <value>There is an incomplete parameter in this path segment: '{0}'. Check that each '{{' character has a matching '}}' character.</value>
+ </data>
+ <data name="Route_RepeatedParameter" xml:space="preserve">
+ <value>The route parameter name '{0}' appears more than one time in the route template.</value>
+ </data>
+ <data name="Route_ValidationMustBeStringOrCustomConstraint" xml:space="preserve">
+ <value>The constraint entry '{0}' on the route with route template '{1}' must have a string value or be of a type which implements '{2}'.</value>
+ </data>
+ <data name="Route_AddRemoveWithNoKeyNotSupported" xml:space="preserve">
+ <value>Adding or removing items from a '{0}' is not supported. Please use a key when adding and removing items.</value>
+ </data>
+ <data name="AmbiguousIndexerInvocation" xml:space="preserve">
+ <value>Ambiguous invocation of indexer in type '{0}'</value>
+ </data>
+ <data name="AmbiguousMethodInvocation" xml:space="preserve">
+ <value>Ambiguous invocation of method '{0}' in type '{1}'</value>
+ </data>
+ <data name="CannotIndexMultiDimArray" xml:space="preserve">
+ <value>Indexing of multi-dimensional arrays is not supported</value>
+ </data>
+ <data name="CloseBracketOrCommaExpected" xml:space="preserve">
+ <value>']' or ',' expected</value>
+ </data>
+ <data name="CloseParenOrCommaExpected" xml:space="preserve">
+ <value>')' or ',' expected</value>
+ </data>
+ <data name="CloseParenOrOperatorExpected" xml:space="preserve">
+ <value>')' or operator expected</value>
+ </data>
+ <data name="DigitExpected" xml:space="preserve">
+ <value>Digit expected</value>
+ </data>
+ <data name="DuplicateIdentifier" xml:space="preserve">
+ <value>The identifier '{0}' was defined more than once</value>
+ </data>
+ <data name="ExpressionExpected" xml:space="preserve">
+ <value>Expression expected</value>
+ </data>
+ <data name="ExpressionTypeMismatch" xml:space="preserve">
+ <value>Expression of type '{0}' expected</value>
+ </data>
+ <data name="IdentifierExpected" xml:space="preserve">
+ <value>Identifier expected</value>
+ </data>
+ <data name="IncompatibleOperand" xml:space="preserve">
+ <value>Operator '{0}' incompatible with operand type '{1}'</value>
+ </data>
+ <data name="IncompatibleOperands" xml:space="preserve">
+ <value>Operator '{0}' incompatible with operand types '{1}' and '{2}'</value>
+ </data>
+ <data name="InvalidCharacter" xml:space="preserve">
+ <value>Syntax error '{0}'</value>
+ </data>
+ <data name="InvalidIndex" xml:space="preserve">
+ <value>Array index must be an integer expression</value>
+ </data>
+ <data name="InvalidIntegerLiteral" xml:space="preserve">
+ <value>Invalid integer literal '{0}'</value>
+ </data>
+ <data name="InvalidQueryOperator" xml:space="preserve">
+ <value>Invalid query operator '{0}'.</value>
+ </data>
+ <data name="InvalidRealLiteral" xml:space="preserve">
+ <value>Invalid real literal '{0}'</value>
+ </data>
+ <data name="MethodIsVoid" xml:space="preserve">
+ <value>Method '{0}' in type '{1}' does not return a value</value>
+ </data>
+ <data name="NoApplicableAggregate" xml:space="preserve">
+ <value>No applicable aggregate method '{0}' exists</value>
+ </data>
+ <data name="NoApplicableIndexer" xml:space="preserve">
+ <value>No applicable indexer exists in type '{0}'</value>
+ </data>
+ <data name="NoApplicableMethod" xml:space="preserve">
+ <value>No applicable method '{0}' exists in type '{1}'</value>
+ </data>
+ <data name="OpenParenExpected" xml:space="preserve">
+ <value>'(' expected</value>
+ </data>
+ <data name="ParseExceptionFormat" xml:space="preserve">
+ <value>{0} (at index {1})</value>
+ </data>
+ <data name="SyntaxError" xml:space="preserve">
+ <value>Syntax error</value>
+ </data>
+ <data name="UnknownIdentifier" xml:space="preserve">
+ <value>Unknown identifier '{0}'</value>
+ </data>
+ <data name="UnknownPropertyOrField" xml:space="preserve">
+ <value>No property or field '{0}' exists in type '{1}'</value>
+ </data>
+ <data name="UnterminatedStringLiteral" xml:space="preserve">
+ <value>Unterminated string literal</value>
+ </data>
+ <data name="UriQueryStringInvalid" xml:space="preserve">
+ <value>The query specified in the URI is not valid.</value>
+ </data>
+ <data name="ValueProviderFactory_Cannot_Create" xml:space="preserve">
+ <value>Could not create a '{0}' from '{1}'. Please ensure it derives from '{0}' and has a public parameterless constructor.</value>
+ </data>
+ <data name="ResultLimitFilter_InvalidReturnType" xml:space="preserve">
+ <value>ResultLimitAttribute cannot be applied to action '{0}' because it's return type is not IEnumerable.</value>
+ </data>
+ <data name="ResultLimitFilter_OutOfRange" xml:space="preserve">
+ <value>The limit specified by the ResultLimitAttribute applied to action '{0}' must be greater than zero.</value>
+ </data>
+ <data name="Validation_ValueNotFound" xml:space="preserve">
+ <value>A value is required but was not present in the request.</value>
+ </data>
+ <data name="InvalidTypeCreationExpression" xml:space="preserve">
+ <value>Invalid '{0}' type creation expression.</value>
+ </data>
+ <data name="InvalidHexLiteral" xml:space="preserve">
+ <value>Invalid hexadecimal literal.</value>
+ </data>
+ <data name="PositiveIntegerExpectedForODataQueryParameter" xml:space="preserve">
+ <value>The OData query parameter '{0}' has an invalid value. The value should be a positive integer. The provided value was '{1}'</value>
+ </data>
+ <data name="CannotSupportSingletonInstance" xml:space="preserve">
+ <value>Cannot reuse an '{0}' instance. '{0}' has to be constructed per incoming message. Check your custom '{1}' and make sure that it will not manufacture the same instance.</value>
+ </data>
+ <data name="ActionFilterAttribute_MustSupplyResponseOrException" xml:space="preserve">
+ <value>After calling {0}.OnActionExecuted, the HttpActionExecutedContext properties Result and Exception were both null. At least one of these values must be non-null. To provide a new response, please set the Result object; to indicate an error, please throw an exception.</value>
+ </data>
+ <data name="TraceActionInvokeMessage" xml:space="preserve">
+ <value>Action='{0}'</value>
+ </data>
+ <data name="TraceActionFilterMessage" xml:space="preserve">
+ <value>Action filter for '{0}'</value>
+ </data>
+ <data name="TraceActionSelectedMessage" xml:space="preserve">
+ <value>Selected action '{0}'</value>
+ </data>
+ <data name="TraceCancelledMessage" xml:space="preserve">
+ <value>Cancelled</value>
+ </data>
+ <data name="TraceControllerNameAndRouteMessage" xml:space="preserve">
+ <value>Controller name='{0}', route='{1}'</value>
+ </data>
+ <data name="TraceModelStateInvalidMessage" xml:space="preserve">
+ <value>Model state is invalid. {0}</value>
+ </data>
+ <data name="TraceNoneObjectMessage" xml:space="preserve">
+ <value>none</value>
+ </data>
+ <data name="TraceReadFromStreamMessage" xml:space="preserve">
+ <value>Type='{0}', content-type='{1}'</value>
+ </data>
+ <data name="TraceReadFromStreamValueMessage" xml:space="preserve">
+ <value>Value read='{0}'</value>
+ </data>
+ <data name="TraceRequestCompleteMessage" xml:space="preserve">
+ <value>Content-type='{0}', content-length={1}</value>
+ </data>
+ <data name="TraceSelectedFormatter" xml:space="preserve">
+ <value>Selected formatter='{0}', content-type='{1}'</value>
+ </data>
+ <data name="TraceNegotiateFormatter" xml:space="preserve">
+ <value>Type='{0}', formatters=[{1}]</value>
+ </data>
+ <data name="TraceUnknownMessage" xml:space="preserve">
+ <value>unknown</value>
+ </data>
+ <data name="TraceWriteToStreamMessage" xml:space="preserve">
+ <value>Value='{0}', type='{1}', content-type='{2}'</value>
+ </data>
+ <data name="HttpRequestMessageExtensions_NoConfiguration" xml:space="preserve">
+ <value>The request does not have an associated configuration object or the provided configuration was null.</value>
+ </data>
+ <data name="TraceGetPerRequestFormatterEndMessage" xml:space="preserve">
+ <value>Will use same '{0}' formatter</value>
+ </data>
+ <data name="TraceGetPerRequestFormatterMessage" xml:space="preserve">
+ <value>Obtaining formatter of type '{0}' for type='{1}', mediaType='{2}'</value>
+ </data>
+ <data name="RouteCollection_NameNotFound" xml:space="preserve">
+ <value>A route named '{0}' could not be found in the route collection.</value>
+ </data>
+ <data name="ApiExplorer_DefaultDocumentation" xml:space="preserve">
+ <value>Documentation for '{0}'.</value>
+ </data>
+ <data name="ParameterBindingCantHaveMultipleBodyParameters" xml:space="preserve">
+ <value>Can't bind multiple parameters ('{0}' and '{1}') to the request's content.</value>
+ </data>
+ <data name="ParameterBindingConflictingAttributes" xml:space="preserve">
+ <value>Can't bind parameter '{0}' because it has conflicting attributes on it.</value>
+ </data>
+ <data name="ParameterBindingIllegalType" xml:space="preserve">
+ <value>Can't bind parameter '{1}'. Must specify a custom model binder to bind parameters of type '{0}'.</value>
+ </data>
+ <data name="MissingRequiredMember" xml:space="preserve">
+ <value>The {0} property is required.</value>
+ </data>
+ <data name="TraceBeginParameterBind" xml:space="preserve">
+ <value>Binding parameter '{0}'</value>
+ </data>
+ <data name="TraceEndParameterBind" xml:space="preserve">
+ <value>Parameter '{0}' bound to the value '{1}'</value>
+ </data>
+ <data name="TraceEndParameterBindNoBind" xml:space="preserve">
+ <value>Parameter '{0}' failed to bind.</value>
+ </data>
+ <data name="TraceInvokingAction" xml:space="preserve">
+ <value>Invoking action '{0}'</value>
+ </data>
+ <data name="TraceActionReturnValue" xml:space="preserve">
+ <value>Action returned '{0}'</value>
+ </data>
+ <data name="TraceModelStateErrorMessage" xml:space="preserve">
+ <value>{0}: {1}</value>
+ </data>
+ <data name="TraceGetPerRequestNullFormatterEndMessage" xml:space="preserve">
+ <value>Null formatter</value>
+ </data>
+ <data name="TraceValidModelState" xml:space="preserve">
+ <value>Model state is valid. Values: {0}</value>
+ </data>
+ <data name="TraceGetPerRequestFormatterEndMessageNew" xml:space="preserve">
+ <value>Will use new '{0}' formatter</value>
+ </data>
+ <data name="JQuerySyntaxMissingClosingBracket" xml:space="preserve">
+ <value>The key is invalid JQuery syntax because it is missing a closing bracket</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/System.Web.Http/Query/DynamicQueryable.cs b/src/System.Web.Http/Query/DynamicQueryable.cs
new file mode 100644
index 00000000..496f9d5f
--- /dev/null
+++ b/src/System.Web.Http/Query/DynamicQueryable.cs
@@ -0,0 +1,2214 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Web.Http.Properties;
+
+namespace System.Web.Http.Query
+{
+ internal static class DynamicQueryable
+ {
+ public static IQueryable Where(this IQueryable source, string predicate, QueryResolver queryResolver)
+ {
+ if (source == null)
+ throw new ArgumentNullException("source");
+ if (predicate == null)
+ throw new ArgumentNullException("predicate");
+ LambdaExpression lambda = DynamicExpression.ParseLambda(source.ElementType, typeof(bool), predicate, queryResolver);
+ return source.Provider.CreateQuery(
+ Expression.Call(
+ typeof(Queryable), "Where",
+ new Type[] { source.ElementType },
+ source.Expression, Expression.Quote(lambda)));
+ }
+
+ public static IQueryable OrderBy(this IQueryable source, string ordering, QueryResolver queryResolver)
+ {
+ if (source == null)
+ throw new ArgumentNullException("source");
+ if (ordering == null)
+ throw new ArgumentNullException("ordering");
+ ParameterExpression[] parameters = new ParameterExpression[] {
+ Expression.Parameter(source.ElementType, "") };
+ ExpressionParser parser = new ExpressionParser(parameters, ordering, queryResolver);
+ IEnumerable<DynamicOrdering> orderings = parser.ParseOrdering();
+ Expression queryExpr = source.Expression;
+ string methodAsc = "OrderBy";
+ string methodDesc = "OrderByDescending";
+ foreach (DynamicOrdering o in orderings)
+ {
+ queryExpr = Expression.Call(
+ typeof(Queryable), o.Ascending ? methodAsc : methodDesc,
+ new Type[] { source.ElementType, o.Selector.Type },
+ queryExpr, Expression.Quote(DynamicExpression.Lambda(o.Selector, parameters)));
+ methodAsc = "ThenBy";
+ methodDesc = "ThenByDescending";
+ }
+ return source.Provider.CreateQuery(queryExpr);
+ }
+
+ public static IQueryable Take(this IQueryable source, int count)
+ {
+ if (source == null)
+ throw new ArgumentNullException("source");
+ return source.Provider.CreateQuery(
+ Expression.Call(
+ typeof(Queryable), "Take",
+ new Type[] { source.ElementType },
+ source.Expression, Expression.Constant(count)));
+ }
+
+ public static IQueryable Skip(this IQueryable source, int count)
+ {
+ if (source == null)
+ throw new ArgumentNullException("source");
+ return source.Provider.CreateQuery(
+ Expression.Call(
+ typeof(Queryable), "Skip",
+ new Type[] { source.ElementType },
+ source.Expression, Expression.Constant(count)));
+ }
+ }
+
+ internal static class DynamicExpression
+ {
+ static readonly Type[] funcTypes = new Type[] {
+ typeof(Func<>),
+ typeof(Func<,>),
+ typeof(Func<,,>),
+ typeof(Func<,,,>),
+ typeof(Func<,,,,>)
+ };
+
+ public static LambdaExpression ParseLambda(Type itType, Type resultType, string expression, QueryResolver queryResolver)
+ {
+ return ParseLambda(new ParameterExpression[] { Expression.Parameter(itType, "") }, resultType, expression, queryResolver);
+ }
+
+ public static LambdaExpression ParseLambda(ParameterExpression[] parameters, Type resultType, string expression, QueryResolver queryResolver)
+ {
+ ExpressionParser parser = new ExpressionParser(parameters, expression, queryResolver);
+ return Lambda(parser.Parse(resultType), parameters);
+ }
+
+ public static LambdaExpression Lambda(Expression body, params ParameterExpression[] parameters)
+ {
+ int paramCount = parameters == null ? 0 : parameters.Length;
+ Type[] typeArgs = new Type[paramCount + 1];
+ for (int i = 0; i < paramCount; i++)
+ typeArgs[i] = parameters[i].Type;
+ typeArgs[paramCount] = body.Type;
+ return Expression.Lambda(GetFuncType(typeArgs), body, parameters);
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Justification = "Arguments are provided internally by the parser's ParserLambda methods.")]
+ public static Type GetFuncType(params Type[] typeArgs)
+ {
+ if (typeArgs == null || typeArgs.Length < 1 || typeArgs.Length > 5)
+ throw new ArgumentException();
+ return funcTypes[typeArgs.Length - 1].MakeGenericType(typeArgs);
+ }
+ }
+
+ internal class DynamicOrdering
+ {
+ public Expression Selector;
+ public bool Ascending;
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1032:ImplementStandardExceptionConstructors", Justification = "Exception is intended to only be used by the dynamic parser.")]
+ [SuppressMessage("Microsoft.Design", "CA1064:ExceptionsShouldBePublic", Justification = "Exception is intended to only be used by the dynamic parser.")]
+ [SuppressMessage("Microsoft.Usage", "CA2237:MarkISerializableTypesWithSerializable", Justification = "Exception is intended to only be used by the dynamic parser.")]
+ internal class ParseException : Exception
+ {
+ public ParseException(string message, int position)
+ : base(string.Format(CultureInfo.InvariantCulture, string.Format(CultureInfo.CurrentCulture, SRResources.ParseExceptionFormat, message, position)))
+ {
+ }
+ }
+
+ internal class ExpressionParser
+ {
+ struct Token
+ {
+ public TokenId id;
+ public string text;
+ public int pos;
+ }
+
+ enum TokenId
+ {
+ Unknown,
+ End,
+ Identifier,
+ StringLiteral,
+ IntegerLiteral,
+ RealLiteral,
+ Exclamation,
+ Percent,
+ Amphersand,
+ OpenParen,
+ CloseParen,
+ Asterisk,
+ Plus,
+ Comma,
+ Minus,
+ Dot,
+ Slash,
+ Colon,
+ LessThan,
+ Equal,
+ GreaterThan,
+ Question,
+ OpenBracket,
+ CloseBracket,
+ Bar,
+ ExclamationEqual,
+ DoubleAmphersand,
+ LessThanEqual,
+ LessGreater,
+ DoubleEqual,
+ GreaterThanEqual,
+ DoubleBar
+ }
+
+ internal class MappedMemberInfo
+ {
+ public MappedMemberInfo(Type mappedType, string memberName, bool isStatic, bool isMethod)
+ {
+ MappedType = mappedType;
+ MemberName = memberName;
+ IsStatic = isStatic;
+ IsMethod = isMethod;
+ }
+
+ public Type MappedType { get; private set; }
+ public string MemberName { get; private set; }
+ public bool IsStatic { get; private set; }
+ public bool IsMethod { get; private set; }
+ public Action<Expression[]> MapParams { get; set; }
+ }
+
+ interface ILogicalSignatures
+ {
+ void F(bool x, bool y);
+ void F(bool? x, bool? y);
+ }
+
+ interface IArithmeticSignatures
+ {
+ void F(int x, int y);
+ void F(uint x, uint y);
+ void F(long x, long y);
+ void F(ulong x, ulong y);
+ void F(float x, float y);
+ void F(double x, double y);
+ void F(decimal x, decimal y);
+ void F(int? x, int? y);
+ [SuppressMessage("Microsoft.MSInternal", "CA908:AvoidTypesThatRequireJitCompilationInPrecompiledAssemblies", Justification = "Legacy code.")]
+ void F(uint? x, uint? y);
+ [SuppressMessage("Microsoft.MSInternal", "CA908:AvoidTypesThatRequireJitCompilationInPrecompiledAssemblies", Justification = "Legacy code.")]
+ void F(long? x, long? y);
+ [SuppressMessage("Microsoft.MSInternal", "CA908:AvoidTypesThatRequireJitCompilationInPrecompiledAssemblies", Justification = "Legacy code.")]
+ void F(ulong? x, ulong? y);
+ void F(float? x, float? y);
+ void F(double? x, double? y);
+ void F(decimal? x, decimal? y);
+ }
+
+ interface IRelationalSignatures : IArithmeticSignatures
+ {
+ void F(string x, string y);
+ void F(char x, char y);
+ void F(DateTime x, DateTime y);
+ void F(TimeSpan x, TimeSpan y);
+ void F(char? x, char? y);
+ void F(DateTime? x, DateTime? y);
+ void F(TimeSpan? x, TimeSpan? y);
+ void F(DateTimeOffset x, DateTimeOffset y);
+ [SuppressMessage("Microsoft.MSInternal", "CA908:AvoidTypesThatRequireJitCompilationInPrecompiledAssemblies", Justification = "Legacy code.")]
+ void F(DateTimeOffset? x, DateTimeOffset? y);
+ }
+
+ interface IEqualitySignatures : IRelationalSignatures
+ {
+ void F(bool x, bool y);
+ void F(bool? x, bool? y);
+ void F(Guid x, Guid y);
+ void F(Guid? x, Guid? y);
+ }
+
+ interface IAddSignatures : IArithmeticSignatures
+ {
+ void F(DateTime x, TimeSpan y);
+ void F(TimeSpan x, TimeSpan y);
+ void F(DateTime? x, TimeSpan? y);
+ void F(TimeSpan? x, TimeSpan? y);
+ void F(DateTimeOffset x, TimeSpan y);
+ [SuppressMessage("Microsoft.MSInternal", "CA908:AvoidTypesThatRequireJitCompilationInPrecompiledAssemblies", Justification = "Legacy code.")]
+ void F(DateTimeOffset? x, TimeSpan? y);
+ }
+
+ interface ISubtractSignatures : IAddSignatures
+ {
+ void F(DateTime x, DateTime y);
+ void F(DateTime? x, DateTime? y);
+ void F(DateTimeOffset x, DateTimeOffset y);
+ [SuppressMessage("Microsoft.MSInternal", "CA908:AvoidTypesThatRequireJitCompilationInPrecompiledAssemblies", Justification = "Legacy code.")]
+ void F(DateTimeOffset? x, DateTimeOffset? y);
+ }
+
+ interface INegationSignatures
+ {
+ void F(int x);
+ void F(long x);
+ void F(float x);
+ void F(double x);
+ void F(decimal x);
+ void F(int? x);
+ void F(long? x);
+ void F(float? x);
+ void F(double? x);
+ void F(decimal? x);
+ }
+
+ interface INotSignatures
+ {
+ void F(bool x);
+ void F(bool? x);
+ }
+
+ interface IEnumerableSignatures
+ {
+ void Where(bool predicate);
+ void Any();
+ void Any(bool predicate);
+ void All(bool predicate);
+ void Count();
+ void Count(bool predicate);
+ void Min(object selector);
+ void Max(object selector);
+ void Sum(int selector);
+ void Sum(int? selector);
+ void Sum(long selector);
+ void Sum(long? selector);
+ void Sum(float selector);
+ void Sum(float? selector);
+ void Sum(double selector);
+ void Sum(double? selector);
+ void Sum(decimal selector);
+ void Sum(decimal? selector);
+ void Average(int selector);
+ void Average(int? selector);
+ void Average(long selector);
+ void Average(long? selector);
+ void Average(float selector);
+ void Average(float? selector);
+ void Average(double selector);
+ void Average(double? selector);
+ void Average(decimal selector);
+ void Average(decimal? selector);
+ }
+
+ static readonly Expression trueLiteral = Expression.Constant(true);
+ static readonly Expression falseLiteral = Expression.Constant(false);
+ static readonly Expression nullLiteral = Expression.Constant(null);
+
+ static Dictionary<string, object> keywords;
+
+ Dictionary<string, object> symbols;
+ Dictionary<Expression, string> literals;
+ ParameterExpression it;
+ string text;
+ int textPos;
+ int textLen;
+ char ch;
+ Token token;
+ QueryResolver queryResolver;
+
+ public ExpressionParser(ParameterExpression[] parameters, string expression, QueryResolver queryResolver)
+ {
+ if (expression == null)
+ throw new ArgumentNullException("expression");
+ if (keywords == null)
+ keywords = CreateKeywords();
+ this.queryResolver = queryResolver;
+ symbols = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
+ literals = new Dictionary<Expression, string>();
+ if (parameters != null)
+ ProcessParameters(parameters);
+ text = expression;
+ textLen = text.Length;
+ SetTextPos(0);
+ NextToken();
+ }
+
+ void ProcessParameters(ParameterExpression[] parameters)
+ {
+ foreach (ParameterExpression pe in parameters)
+ if (!String.IsNullOrEmpty(pe.Name))
+ AddSymbol(pe.Name, pe);
+ if (parameters.Length == 1 && String.IsNullOrEmpty(parameters[0].Name))
+ it = parameters[0];
+ }
+
+ void AddSymbol(string name, object value)
+ {
+ if (symbols.ContainsKey(name))
+ throw ParseError(string.Format(CultureInfo.CurrentCulture, SRResources.DuplicateIdentifier, name));
+ symbols.Add(name, value);
+ }
+
+ public Expression Parse(Type resultType)
+ {
+ int exprPos = token.pos;
+ Expression expr = ParseExpression();
+ if (resultType != null)
+ if ((expr = PromoteExpression(expr, resultType, true)) == null)
+ throw ParseError(exprPos, string.Format(CultureInfo.CurrentCulture, SRResources.ExpressionTypeMismatch, GetTypeName(resultType)));
+ ValidateToken(TokenId.End, SRResources.SyntaxError);
+ return expr;
+ }
+
+#pragma warning disable 0219
+ public IEnumerable<DynamicOrdering> ParseOrdering()
+ {
+ List<DynamicOrdering> orderings = new List<DynamicOrdering>();
+ while (true)
+ {
+ Expression expr = ParseExpression();
+ bool ascending = true;
+ if (TokenIdentifierIs("asc") || TokenIdentifierIs("ascending"))
+ {
+ NextToken();
+ }
+ else if (TokenIdentifierIs("desc") || TokenIdentifierIs("descending"))
+ {
+ NextToken();
+ ascending = false;
+ }
+ orderings.Add(new DynamicOrdering
+ {
+ Selector = expr,
+ Ascending = ascending
+ });
+ if (token.id != TokenId.Comma)
+ break;
+ NextToken();
+ }
+ ValidateToken(TokenId.End, SRResources.SyntaxError);
+ return orderings;
+ }
+#pragma warning restore 0219
+
+ Expression ParseExpression()
+ {
+ Expression expr = ParseLogicalOr();
+ return expr;
+ }
+
+ // ||, or operator
+ Expression ParseLogicalOr()
+ {
+ Expression left = ParseLogicalAnd();
+ while (token.id == TokenId.DoubleBar || TokenIdentifierIs("or"))
+ {
+ Token op = token;
+ NextToken();
+ Expression right = ParseLogicalAnd();
+ CheckAndPromoteOperands(typeof(ILogicalSignatures), op.text, ref left, ref right, op.pos);
+ left = Expression.OrElse(left, right);
+ }
+ return left;
+ }
+
+ // &&, and operator
+ Expression ParseLogicalAnd()
+ {
+ Expression left = ParseComparison();
+ while (token.id == TokenId.DoubleAmphersand || TokenIdentifierIs("and"))
+ {
+ Token op = token;
+ NextToken();
+ Expression right = ParseComparison();
+ CheckAndPromoteOperands(typeof(ILogicalSignatures), op.text, ref left, ref right, op.pos);
+ left = Expression.AndAlso(left, right);
+ }
+ return left;
+ }
+
+ [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Legacy code.")]
+ // =, ==, !=, <>, >, >=, <, <= operators
+ Expression ParseComparison()
+ {
+ Expression left = ParseAdditive();
+ while (token.id == TokenId.Equal || token.id == TokenId.DoubleEqual ||
+ token.id == TokenId.ExclamationEqual || token.id == TokenId.LessGreater ||
+ token.id == TokenId.GreaterThan || token.id == TokenId.GreaterThanEqual ||
+ token.id == TokenId.LessThan || token.id == TokenId.LessThanEqual)
+ {
+ Token op = token;
+ NextToken();
+ Expression right = ParseAdditive();
+ bool isEquality = op.id == TokenId.Equal || op.id == TokenId.DoubleEqual ||
+ op.id == TokenId.ExclamationEqual || op.id == TokenId.LessGreater;
+ if (isEquality && !left.Type.IsValueType && !right.Type.IsValueType)
+ {
+ if (left.Type != right.Type)
+ {
+ if (left.Type.IsAssignableFrom(right.Type))
+ {
+ right = Expression.Convert(right, left.Type);
+ }
+ else if (right.Type.IsAssignableFrom(left.Type))
+ {
+ left = Expression.Convert(left, right.Type);
+ }
+ else
+ {
+ throw IncompatibleOperandsError(op.text, left, right, op.pos);
+ }
+ }
+ }
+ else if (IsEnumType(left.Type) || IsEnumType(right.Type))
+ {
+ // convert enum expressions to their underlying values for comparison
+ left = ConvertEnumExpression(left, right);
+ right = ConvertEnumExpression(right, left);
+
+ CheckAndPromoteOperands(isEquality ? typeof(IEqualitySignatures) : typeof(IRelationalSignatures),
+ op.text, ref left, ref right, op.pos);
+ }
+ else
+ {
+ CheckAndPromoteOperands(isEquality ? typeof(IEqualitySignatures) : typeof(IRelationalSignatures),
+ op.text, ref left, ref right, op.pos);
+ }
+ switch (op.id)
+ {
+ case TokenId.Equal:
+ case TokenId.DoubleEqual:
+ left = GenerateEqual(left, right);
+ break;
+ case TokenId.ExclamationEqual:
+ case TokenId.LessGreater:
+ left = GenerateNotEqual(left, right);
+ break;
+ case TokenId.GreaterThan:
+ left = GenerateGreaterThan(left, right);
+ break;
+ case TokenId.GreaterThanEqual:
+ left = GenerateGreaterThanEqual(left, right);
+ break;
+ case TokenId.LessThan:
+ left = GenerateLessThan(left, right);
+ break;
+ case TokenId.LessThanEqual:
+ left = GenerateLessThanEqual(left, right);
+ break;
+ }
+ }
+ return left;
+ }
+
+ /// <summary>
+ /// We perform comparisons against enums using the underlying type
+ /// because a more complete set of comparisons can be performed.
+ /// </summary>
+ static Expression ConvertEnumExpression(Expression expr, Expression otherExpr)
+ {
+ if (!IsEnumType(expr.Type))
+ {
+ return expr;
+ }
+
+ Type underlyingType;
+ if (IsNullableType(expr.Type) ||
+ (otherExpr.NodeType == ExpressionType.Constant && ((ConstantExpression)otherExpr).Value == null))
+ {
+ // if the enum expression itself is nullable or is being compared against null
+ // we use a nullable type
+ underlyingType = typeof(Nullable<>).MakeGenericType(Enum.GetUnderlyingType(GetNonNullableType(expr.Type)));
+ }
+ else
+ {
+ underlyingType = Enum.GetUnderlyingType(expr.Type);
+ }
+
+ return Expression.Convert(expr, underlyingType);
+ }
+
+ // +, -, & operators
+ Expression ParseAdditive()
+ {
+ Expression left = ParseMultiplicative();
+ while (token.id == TokenId.Plus || token.id == TokenId.Minus ||
+ token.id == TokenId.Amphersand)
+ {
+ Token op = token;
+ NextToken();
+ Expression right = ParseMultiplicative();
+ switch (op.id)
+ {
+ case TokenId.Plus:
+ if (left.Type == typeof(string) || right.Type == typeof(string))
+ goto case TokenId.Amphersand;
+ CheckAndPromoteOperands(typeof(IAddSignatures), op.text, ref left, ref right, op.pos);
+ left = GenerateAdd(left, right);
+ break;
+ case TokenId.Minus:
+ CheckAndPromoteOperands(typeof(ISubtractSignatures), op.text, ref left, ref right, op.pos);
+ left = GenerateSubtract(left, right);
+ break;
+ case TokenId.Amphersand:
+ left = GenerateStringConcat(left, right);
+ break;
+ }
+ }
+ return left;
+ }
+
+ // *, /, %, mod operators
+ Expression ParseMultiplicative()
+ {
+ Expression left = ParseUnary();
+ while (token.id == TokenId.Asterisk || token.id == TokenId.Slash ||
+ token.id == TokenId.Percent || TokenIdentifierIs("mod"))
+ {
+ Token op = token;
+ NextToken();
+ Expression right = ParseUnary();
+ CheckAndPromoteOperands(typeof(IArithmeticSignatures), op.text, ref left, ref right, op.pos);
+ switch (op.id)
+ {
+ case TokenId.Asterisk:
+ left = Expression.Multiply(left, right);
+ break;
+ case TokenId.Slash:
+ left = Expression.Divide(left, right);
+ break;
+ case TokenId.Percent:
+ case TokenId.Identifier:
+ left = Expression.Modulo(left, right);
+ break;
+ }
+ }
+ return left;
+ }
+
+ // -, !, not unary operators
+ Expression ParseUnary()
+ {
+ if (token.id == TokenId.Minus || token.id == TokenId.Exclamation ||
+ TokenIdentifierIs("not"))
+ {
+ Token op = token;
+ NextToken();
+ if (op.id == TokenId.Minus && (token.id == TokenId.IntegerLiteral ||
+ token.id == TokenId.RealLiteral))
+ {
+ token.text = "-" + token.text;
+ token.pos = op.pos;
+ return ParsePrimary();
+ }
+ Expression expr = ParseUnary();
+ if (op.id == TokenId.Minus)
+ {
+ CheckAndPromoteOperand(typeof(INegationSignatures), op.text, ref expr, op.pos);
+ expr = Expression.Negate(expr);
+ }
+ else
+ {
+ CheckAndPromoteOperand(typeof(INotSignatures), op.text, ref expr, op.pos);
+ expr = Expression.Not(expr);
+ }
+ return expr;
+ }
+ return ParsePrimary();
+ }
+
+ Expression ParsePrimary()
+ {
+ Expression expr = ParsePrimaryStart();
+ while (true)
+ {
+ if (token.id == TokenId.Dot)
+ {
+ NextToken();
+ expr = ParseMemberAccess(null, expr);
+ }
+ else if (token.id == TokenId.OpenBracket)
+ {
+ expr = ParseElementAccess(expr);
+ }
+ else
+ {
+ break;
+ }
+ }
+ return expr;
+ }
+
+ Expression ParsePrimaryStart()
+ {
+ switch (token.id)
+ {
+ case TokenId.Identifier:
+ return ParseIdentifier();
+ case TokenId.StringLiteral:
+ return ParseStringLiteral();
+ case TokenId.IntegerLiteral:
+ return ParseIntegerLiteral();
+ case TokenId.RealLiteral:
+ return ParseRealLiteral();
+ case TokenId.OpenParen:
+ return ParseParenExpression();
+ default:
+ throw ParseError(SRResources.ExpressionExpected);
+ }
+ }
+
+ Expression ParseStringLiteral()
+ {
+ ValidateToken(TokenId.StringLiteral);
+ char quote = token.text[0];
+ // Unwrap string (remove surrounding quotes) and unwrap backslashes.
+ string s = token.text.Substring(1, token.text.Length - 2).Replace("\\\\", "\\");
+
+ if (quote == '\'')
+ {
+ // Unwrap single quotes.
+ s = s.Replace("\\\'", "\'");
+ }
+ else
+ {
+ // Unwrap double quotes.
+ s = s.Replace("\\\"", "\"");
+ // TODO : do we need this code anymore?
+ }
+
+ NextToken();
+ return CreateLiteral(s, s);
+ }
+
+ [SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", Justification = "Legacy code.")]
+ Expression ParseIntegerLiteral()
+ {
+ ValidateToken(TokenId.IntegerLiteral);
+ string text = token.text;
+ if (text[0] != '-')
+ {
+ ulong value;
+ if (!UInt64.TryParse(text, NumberStyles.None, CultureInfo.InvariantCulture, out value))
+ throw ParseError(string.Format(CultureInfo.CurrentCulture, SRResources.InvalidIntegerLiteral, text));
+ NextToken();
+ if (token.text == "L" || token.text == "l")
+ {
+ NextToken();
+ return CreateLiteral((long)value, text);
+ }
+ if (value <= (ulong)Int32.MaxValue)
+ return CreateLiteral((int)value, text);
+ if (value <= (ulong)UInt32.MaxValue)
+ return CreateLiteral((uint)value, text);
+ if (value <= (ulong)Int64.MaxValue)
+ return CreateLiteral((long)value, text);
+ return CreateLiteral(value, text);
+ }
+ else
+ {
+ long value;
+ if (!Int64.TryParse(text, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out value))
+ throw ParseError(string.Format(CultureInfo.CurrentCulture, SRResources.InvalidIntegerLiteral, text));
+ NextToken();
+ if (token.text == "L" || token.text == "l")
+ {
+ NextToken();
+ return CreateLiteral((long)value, text);
+ }
+ if (value >= Int32.MinValue && value <= Int32.MaxValue)
+ return CreateLiteral((int)value, text);
+ return CreateLiteral(value, text);
+ }
+ }
+
+ [SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", Justification = "Legacy code.")]
+ Expression ParseRealLiteral()
+ {
+ ValidateToken(TokenId.RealLiteral);
+ string text = token.text;
+ object value = null;
+ char last = text[text.Length - 1];
+ if (last == 'F' || last == 'f')
+ {
+ float f;
+ if (Single.TryParse(text.Substring(0, text.Length - 1), NumberStyles.Number | NumberStyles.AllowExponent, CultureInfo.InvariantCulture, out f))
+ value = f;
+ }
+ else if (last == 'M' || last == 'm')
+ {
+ decimal m;
+ if (Decimal.TryParse(text.Substring(0, text.Length - 1), NumberStyles.Number | NumberStyles.AllowExponent, CultureInfo.InvariantCulture, out m))
+ value = m;
+ }
+ else if (last == 'D' || last == 'd')
+ {
+ double d;
+ if (Double.TryParse(text.Substring(0, text.Length - 1), NumberStyles.Number | NumberStyles.AllowExponent, CultureInfo.InvariantCulture, out d))
+ value = d;
+ }
+ else
+ {
+ double d;
+ if (Double.TryParse(text, NumberStyles.Number | NumberStyles.AllowExponent, CultureInfo.InvariantCulture, out d))
+ value = d;
+ }
+ if (value == null)
+ throw ParseError(string.Format(CultureInfo.CurrentCulture, SRResources.InvalidRealLiteral, text));
+ NextToken();
+ return CreateLiteral(value, text);
+ }
+
+ Expression CreateLiteral(object value, string valueAsString)
+ {
+ ConstantExpression expr = Expression.Constant(value);
+ literals.Add(expr, valueAsString);
+ return expr;
+ }
+
+ Expression ParseParenExpression()
+ {
+ ValidateToken(TokenId.OpenParen, SRResources.OpenParenExpected);
+ NextToken();
+ Expression e = ParseExpression();
+ ValidateToken(TokenId.CloseParen, SRResources.CloseParenOrOperatorExpected);
+ NextToken();
+ return e;
+ }
+
+ [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Legacy code.")]
+ Expression ParseIdentifier()
+ {
+ ValidateToken(TokenId.Identifier);
+ object value;
+ if (keywords.TryGetValue(token.text, out value))
+ {
+ if (value is Type)
+ return ParseTypeConstruction((Type)value);
+ NextToken();
+ return (Expression)value;
+ }
+
+ if (symbols.TryGetValue(token.text, out value))
+ {
+ Expression expr = value as Expression;
+ if (expr == null)
+ {
+ expr = Expression.Constant(value);
+ }
+ NextToken();
+ return expr;
+ }
+
+ // See if the token is a mapped function call
+ MappedMemberInfo mappedFunction = MapFunction(token.text);
+ if (mappedFunction != null)
+ {
+ return ParseMappedFunction(mappedFunction);
+ }
+
+ if (it != null)
+ return ParseMemberAccess(null, it);
+
+ throw ParseError(string.Format(CultureInfo.CurrentCulture, SRResources.UnknownIdentifier, token.text));
+ }
+
+ MappedMemberInfo MapFunction(string functionName)
+ {
+ MappedMemberInfo mappedMember = MapStringFunction(functionName);
+ if (mappedMember != null)
+ {
+ return mappedMember;
+ }
+
+ mappedMember = MapDateFunction(functionName);
+ if (mappedMember != null)
+ {
+ return mappedMember;
+ }
+
+ mappedMember = MapMathFunction(functionName);
+ if (mappedMember != null)
+ {
+ return mappedMember;
+ }
+
+ return null;
+ }
+
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Legacy code.")]
+ MappedMemberInfo MapStringFunction(string functionName)
+ {
+ if (functionName == "startswith")
+ {
+ return new MappedMemberInfo(typeof(string), "StartsWith", false, true);
+ }
+ else if (functionName == "endswith")
+ {
+ return new MappedMemberInfo(typeof(string), "EndsWith", false, true);
+ }
+ else if (functionName == "length")
+ {
+ return new MappedMemberInfo(typeof(string), "Length", false, false);
+ }
+ else if (functionName == "toupper")
+ {
+ return new MappedMemberInfo(typeof(string), "ToUpper", false, true);
+ }
+ else if (functionName == "tolower")
+ {
+ return new MappedMemberInfo(typeof(string), "ToLower", false, true);
+ }
+ else if (functionName == "substringof")
+ {
+ MappedMemberInfo memberInfo = new MappedMemberInfo(typeof(string), "Contains", false, true);
+ memberInfo.MapParams = (args) =>
+ {
+ // reverse the order of arguments for string.Contains
+ Expression tmp = args[0];
+ args[0] = args[1];
+ args[1] = tmp;
+ };
+ return memberInfo;
+ }
+ else if (functionName == "indexof")
+ {
+ return new MappedMemberInfo(typeof(string), "IndexOf", false, true);
+ }
+ else if (functionName == "replace")
+ {
+ return new MappedMemberInfo(typeof(string), "Replace", false, true);
+ }
+ else if (functionName == "substring")
+ {
+ return new MappedMemberInfo(typeof(string), "Substring", false, true);
+ }
+ else if (functionName == "trim")
+ {
+ return new MappedMemberInfo(typeof(string), "Trim", false, true);
+ }
+ else if (functionName == "concat")
+ {
+ return new MappedMemberInfo(typeof(string), "Concat", true, true);
+ }
+
+ return null;
+ }
+
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Legacy code.")]
+ MappedMemberInfo MapDateFunction(string functionName)
+ {
+ // date functions
+ if (functionName == "day")
+ {
+ return new MappedMemberInfo(typeof(DateTime), "Day", false, false);
+ }
+ else if (functionName == "month")
+ {
+ return new MappedMemberInfo(typeof(DateTime), "Month", false, false);
+ }
+ else if (functionName == "year")
+ {
+ return new MappedMemberInfo(typeof(DateTime), "Year", false, false);
+ }
+ else if (functionName == "hour")
+ {
+ return new MappedMemberInfo(typeof(DateTime), "Hour", false, false);
+ }
+ else if (functionName == "minute")
+ {
+ return new MappedMemberInfo(typeof(DateTime), "Minute", false, false);
+ }
+ else if (functionName == "second")
+ {
+ return new MappedMemberInfo(typeof(DateTime), "Second", false, false);
+ }
+
+ return null;
+ }
+
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Legacy code.")]
+ MappedMemberInfo MapMathFunction(string functionName)
+ {
+ if (functionName == "round")
+ {
+ return new MappedMemberInfo(typeof(Math), "Round", true, true);
+ }
+ if (functionName == "floor")
+ {
+ return new MappedMemberInfo(typeof(Math), "Floor", true, true);
+ }
+ if (functionName == "ceiling")
+ {
+ return new MappedMemberInfo(typeof(Math), "Ceiling", true, true);
+ }
+
+ return null;
+ }
+
+ Expression ParseTypeConstruction(Type type)
+ {
+ string typeIdentifier = token.text;
+ int errorPos = token.pos;
+ NextToken();
+ Expression typeExpression = null;
+
+ if (token.id == TokenId.StringLiteral)
+ {
+ errorPos = token.pos;
+ Expression stringExpr = ParseStringLiteral();
+ string literalValue = (string)((ConstantExpression)stringExpr).Value;
+
+ try
+ {
+ if (type == typeof(DateTime))
+ {
+ DateTime dateTime = DateTime.Parse(literalValue, CultureInfo.CurrentCulture);
+ typeExpression = Expression.Constant(dateTime);
+ }
+ else if (type == typeof(Guid))
+ {
+ Guid guid = Guid.Parse(literalValue);
+ typeExpression = Expression.Constant(guid);
+ }
+ else if (type == typeof(DateTimeOffset))
+ {
+ DateTimeOffset dateTimeOffset = DateTimeOffset.Parse(literalValue, CultureInfo.CurrentCulture);
+ typeExpression = Expression.Constant(dateTimeOffset);
+ }
+ else if (type == typeof(byte[]))
+ {
+ if (literalValue.Length % 2 != 0)
+ {
+ // odd hex strings are not supported
+ throw ParseError(errorPos, string.Format(CultureInfo.CurrentCulture, SRResources.InvalidHexLiteral));
+ }
+ byte[] bytes = new byte[literalValue.Length / 2];
+ for (int i = 0, j = 0; i < literalValue.Length; i += 2, j++)
+ {
+ string hexValue = literalValue.Substring(i, 2);
+ bytes[j] = byte.Parse(hexValue, NumberStyles.HexNumber, CultureInfo.InvariantCulture);
+ }
+ typeExpression = Expression.Constant(bytes);
+ }
+ else if (type == typeof(TimeSpan))
+ {
+ TimeSpan timeSpan = TimeSpan.Parse(literalValue, CultureInfo.CurrentCulture);
+ typeExpression = Expression.Constant(timeSpan);
+ }
+ }
+ catch (FormatException ex)
+ {
+ throw ParseError(errorPos, ex.Message);
+ }
+ }
+ else
+ {
+ throw ParseError(errorPos, string.Format(CultureInfo.CurrentCulture, SRResources.InvalidTypeCreationExpression, typeIdentifier));
+ }
+
+ return typeExpression;
+ }
+
+ [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Legacy code.")]
+ Expression ParseMappedFunction(MappedMemberInfo mappedMember)
+ {
+ Type type = mappedMember.MappedType;
+ string mappedMemberName = mappedMember.MemberName;
+
+ int errorPos = token.pos;
+ Expression[] args;
+ Expression instance = null;
+
+ NextToken();
+ if (token.id == TokenId.OpenParen)
+ {
+ args = ParseArgumentList();
+
+ if (mappedMember.MapParams != null)
+ {
+ mappedMember.MapParams(args);
+ }
+
+ // static methods need to include the target
+ if (!mappedMember.IsStatic)
+ {
+ if (args.Length == 0)
+ {
+ throw ParseError(errorPos, SRResources.NoApplicableMethod, mappedMember.MemberName, mappedMember.MappedType);
+ }
+
+ instance = args[0];
+ args = args.Skip(1).ToArray();
+ }
+ else
+ {
+ instance = null;
+ }
+ }
+ else
+ {
+ // If it is a function it should begin with a '('
+ throw ParseError(SRResources.OpenParenExpected);
+ }
+
+ if (mappedMember.IsMethod)
+ {
+ // a mapped function
+ MethodBase mb;
+ switch (FindMethod(type, mappedMemberName, mappedMember.IsStatic, args, out mb))
+ {
+ case 0:
+ throw ParseError(errorPos, string.Format(CultureInfo.CurrentCulture, SRResources.NoApplicableMethod,
+ mappedMemberName, GetTypeName(type)));
+ case 1:
+ MethodInfo method = (MethodInfo)mb;
+ if (method.ReturnType == typeof(void))
+ throw ParseError(errorPos, string.Format(CultureInfo.CurrentCulture, SRResources.MethodIsVoid,
+ mappedMemberName, GetTypeName(method.DeclaringType)));
+ return Expression.Call(instance, (MethodInfo)method, args);
+ default:
+ throw ParseError(errorPos, string.Format(CultureInfo.CurrentCulture, SRResources.AmbiguousMethodInvocation,
+ mappedMemberName, GetTypeName(type)));
+ }
+ }
+ else
+ {
+ // a mapped Property/Field
+ MemberInfo member = FindPropertyOrField(type, mappedMemberName, mappedMember.IsStatic);
+ if (member == null)
+ {
+ if (this.queryResolver != null)
+ {
+ MemberExpression mex = queryResolver.ResolveMember(type, mappedMemberName, instance);
+ if (mex != null)
+ {
+ return mex;
+ }
+ }
+ throw ParseError(errorPos, string.Format(CultureInfo.CurrentCulture, SRResources.UnknownPropertyOrField,
+ mappedMemberName, GetTypeName(type)));
+ }
+
+ return member is PropertyInfo ?
+ Expression.Property(instance, (PropertyInfo)member) :
+ Expression.Field(instance, (FieldInfo)member);
+ }
+ }
+
+ [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Legacy code.")]
+ Expression ParseMemberAccess(Type type, Expression instance)
+ {
+ if (instance != null)
+ type = instance.Type;
+ int errorPos = token.pos;
+ string id = GetIdentifier();
+ NextToken();
+ if (token.id == TokenId.OpenParen)
+ {
+ if (instance != null && type != typeof(string))
+ {
+ Type enumerableType = FindGenericType(typeof(IEnumerable<>), type);
+ if (enumerableType != null)
+ {
+ Type elementType = enumerableType.GetGenericArguments()[0];
+ return ParseAggregate(instance, elementType, id, errorPos);
+ }
+ }
+
+ throw ParseError(errorPos, string.Format(CultureInfo.CurrentCulture, SRResources.UnknownIdentifier, id));
+ }
+ else
+ {
+ MemberInfo member = FindPropertyOrField(type, id, instance == null);
+ if (member == null)
+ {
+ if (this.queryResolver != null)
+ {
+ MemberExpression mex = queryResolver.ResolveMember(type, id, instance);
+ if (mex != null)
+ {
+ return mex;
+ }
+ }
+ throw ParseError(errorPos, string.Format(CultureInfo.CurrentCulture, SRResources.UnknownPropertyOrField,
+ id, GetTypeName(type)));
+ }
+ return member is PropertyInfo ?
+ Expression.Property(instance, (PropertyInfo)member) :
+ Expression.Field(instance, (FieldInfo)member);
+ }
+ }
+
+ static Type FindGenericType(Type generic, Type type)
+ {
+ while (type != null && type != typeof(object))
+ {
+ if (type.IsGenericType && type.GetGenericTypeDefinition() == generic)
+ return type;
+ if (generic.IsInterface)
+ {
+ foreach (Type intfType in type.GetInterfaces())
+ {
+ Type found = FindGenericType(generic, intfType);
+ if (found != null)
+ return found;
+ }
+ }
+ type = type.BaseType;
+ }
+ return null;
+ }
+
+ Expression ParseAggregate(Expression instance, Type elementType, string methodName, int errorPos)
+ {
+ ParameterExpression outerIt = it;
+ ParameterExpression innerIt = Expression.Parameter(elementType, "");
+ it = innerIt;
+ Expression[] args = ParseArgumentList();
+ it = outerIt;
+ MethodBase signature;
+ if (FindMethod(typeof(IEnumerableSignatures), methodName, false, args, out signature) != 1)
+ throw ParseError(errorPos, string.Format(CultureInfo.CurrentCulture, SRResources.NoApplicableAggregate, methodName));
+ Type[] typeArgs;
+ if (signature.Name == "Min" || signature.Name == "Max")
+ {
+ typeArgs = new Type[] { elementType, args[0].Type };
+ }
+ else
+ {
+ typeArgs = new Type[] { elementType };
+ }
+ if (args.Length == 0)
+ {
+ args = new Expression[] { instance };
+ }
+ else
+ {
+ args = new Expression[] { instance, DynamicExpression.Lambda(args[0], innerIt) };
+ }
+ return Expression.Call(typeof(Enumerable), signature.Name, typeArgs, args);
+ }
+
+ Expression[] ParseArgumentList()
+ {
+ ValidateToken(TokenId.OpenParen, SRResources.OpenParenExpected);
+ NextToken();
+ Expression[] args = token.id != TokenId.CloseParen ? ParseArguments() : new Expression[0];
+ ValidateToken(TokenId.CloseParen, SRResources.CloseParenOrCommaExpected);
+ NextToken();
+ return args;
+ }
+
+ Expression[] ParseArguments()
+ {
+ List<Expression> argList = new List<Expression>();
+ while (true)
+ {
+ argList.Add(ParseExpression());
+ if (token.id != TokenId.Comma)
+ break;
+ NextToken();
+ }
+ return argList.ToArray();
+ }
+
+ Expression ParseElementAccess(Expression expr)
+ {
+ int errorPos = token.pos;
+ ValidateToken(TokenId.OpenBracket, SRResources.OpenParenExpected);
+ NextToken();
+ Expression[] args = ParseArguments();
+ ValidateToken(TokenId.CloseBracket, SRResources.CloseBracketOrCommaExpected);
+ NextToken();
+ if (expr.Type.IsArray)
+ {
+ if (expr.Type.GetArrayRank() != 1 || args.Length != 1)
+ throw ParseError(errorPos, SRResources.CannotIndexMultiDimArray);
+ Expression index = PromoteExpression(args[0], typeof(int), true);
+ if (index == null)
+ throw ParseError(errorPos, SRResources.InvalidIndex);
+ return Expression.ArrayIndex(expr, index);
+ }
+ else
+ {
+ MethodBase mb;
+ switch (FindIndexer(expr.Type, args, out mb))
+ {
+ case 0:
+ throw ParseError(errorPos, string.Format(CultureInfo.CurrentCulture, SRResources.NoApplicableIndexer,
+ GetTypeName(expr.Type)));
+ case 1:
+ return Expression.Call(expr, (MethodInfo)mb, args);
+ default:
+ throw ParseError(errorPos, string.Format(CultureInfo.CurrentCulture, SRResources.AmbiguousIndexerInvocation,
+ GetTypeName(expr.Type)));
+ }
+ }
+ }
+
+ static bool IsNullableType(Type type)
+ {
+ return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
+ }
+
+ static Type GetNonNullableType(Type type)
+ {
+ return IsNullableType(type) ? type.GetGenericArguments()[0] : type;
+ }
+
+ internal static string GetTypeName(Type type)
+ {
+ Type baseType = GetNonNullableType(type);
+ string s = baseType.Name;
+ if (type != baseType)
+ s += '?';
+ return s;
+ }
+
+ static bool IsSignedIntegralType(Type type)
+ {
+ return GetNumericTypeKind(type) == 2;
+ }
+
+ static bool IsUnsignedIntegralType(Type type)
+ {
+ return GetNumericTypeKind(type) == 3;
+ }
+
+ static int GetNumericTypeKind(Type type)
+ {
+ type = GetNonNullableType(type);
+ if (type.IsEnum)
+ return 0;
+ switch (Type.GetTypeCode(type))
+ {
+ case TypeCode.Char:
+ case TypeCode.Single:
+ case TypeCode.Double:
+ case TypeCode.Decimal:
+ return 1;
+ case TypeCode.SByte:
+ case TypeCode.Int16:
+ case TypeCode.Int32:
+ case TypeCode.Int64:
+ return 2;
+ case TypeCode.Byte:
+ case TypeCode.UInt16:
+ case TypeCode.UInt32:
+ case TypeCode.UInt64:
+ return 3;
+ default:
+ return 0;
+ }
+ }
+
+ static bool IsEnumType(Type type)
+ {
+ return GetNonNullableType(type).IsEnum;
+ }
+
+ void CheckAndPromoteOperand(Type signatures, string opName, ref Expression expr, int errorPos)
+ {
+ Expression[] args = new Expression[] { expr };
+ MethodBase method;
+ if (FindMethod(signatures, "F", false, args, out method) != 1)
+ throw ParseError(errorPos, string.Format(CultureInfo.CurrentCulture, SRResources.IncompatibleOperand,
+ opName, GetTypeName(args[0].Type)));
+ expr = args[0];
+ }
+
+ void CheckAndPromoteOperands(Type signatures, string opName, ref Expression left, ref Expression right, int errorPos)
+ {
+ Expression[] args = new Expression[] { left, right };
+ MethodBase method;
+ if (FindMethod(signatures, "F", false, args, out method) != 1)
+ throw IncompatibleOperandsError(opName, left, right, errorPos);
+ left = args[0];
+ right = args[1];
+ }
+
+ static Exception IncompatibleOperandsError(string opName, Expression left, Expression right, int pos)
+ {
+ return ParseError(pos, string.Format(CultureInfo.CurrentCulture, SRResources.IncompatibleOperands,
+ opName, GetTypeName(left.Type), GetTypeName(right.Type)));
+ }
+
+ static MemberInfo FindPropertyOrField(Type type, string memberName, bool staticAccess)
+ {
+ BindingFlags flags = BindingFlags.Public | BindingFlags.DeclaredOnly |
+ (staticAccess ? BindingFlags.Static : BindingFlags.Instance);
+ foreach (Type t in SelfAndBaseTypes(type))
+ {
+ MemberInfo[] members = t.FindMembers(MemberTypes.Property | MemberTypes.Field,
+ flags, Type.FilterNameIgnoreCase, memberName);
+ if (members.Length != 0)
+ return members[0];
+ }
+ return null;
+ }
+
+ int FindMethod(Type type, string methodName, bool staticAccess, Expression[] args, out MethodBase method)
+ {
+ BindingFlags flags = BindingFlags.Public | BindingFlags.DeclaredOnly |
+ (staticAccess ? BindingFlags.Static : BindingFlags.Instance);
+ foreach (Type t in SelfAndBaseTypes(type))
+ {
+ MemberInfo[] members = t.FindMembers(MemberTypes.Method,
+ flags, Type.FilterNameIgnoreCase, methodName);
+ int count = FindBestMethod(members.Cast<MethodBase>(), args, out method);
+ if (count != 0)
+ return count;
+ }
+ method = null;
+ return 0;
+ }
+
+ int FindIndexer(Type type, Expression[] args, out MethodBase method)
+ {
+ foreach (Type t in SelfAndBaseTypes(type))
+ {
+ MemberInfo[] members = t.GetDefaultMembers();
+ if (members.Length != 0)
+ {
+ IEnumerable<MethodBase> methods = members.
+ OfType<PropertyInfo>().
+ Select(p => (MethodBase)p.GetGetMethod()).
+ Where(m => m != null);
+ int count = FindBestMethod(methods, args, out method);
+ if (count != 0)
+ return count;
+ }
+ }
+ method = null;
+ return 0;
+ }
+
+ static IEnumerable<Type> SelfAndBaseTypes(Type type)
+ {
+ if (type.IsInterface)
+ {
+ List<Type> types = new List<Type>();
+ AddInterface(types, type);
+ return types;
+ }
+ return SelfAndBaseClasses(type);
+ }
+
+ static IEnumerable<Type> SelfAndBaseClasses(Type type)
+ {
+ while (type != null)
+ {
+ yield return type;
+ type = type.BaseType;
+ }
+ }
+
+ static void AddInterface(List<Type> types, Type type)
+ {
+ if (!types.Contains(type))
+ {
+ types.Add(type);
+ foreach (Type t in type.GetInterfaces())
+ AddInterface(types, t);
+ }
+ }
+
+ class MethodData
+ {
+ public MethodBase MethodBase;
+ public ParameterInfo[] Parameters;
+ public Expression[] Args;
+ }
+
+ int FindBestMethod(IEnumerable<MethodBase> methods, Expression[] args, out MethodBase method)
+ {
+ MethodData[] applicable = methods.
+ Select(m => new MethodData
+ {
+ MethodBase = m,
+ Parameters = m.GetParameters()
+ }).
+ Where(m => IsApplicable(m, args)).
+ ToArray();
+ if (applicable.Length > 1)
+ {
+ applicable = applicable.
+ Where(m => applicable.All(n => m == n || IsBetterThan(args, m, n))).
+ ToArray();
+ }
+ if (applicable.Length == 1)
+ {
+ MethodData md = applicable[0];
+ for (int i = 0; i < args.Length; i++)
+ args[i] = md.Args[i];
+ method = md.MethodBase;
+ }
+ else
+ {
+ method = null;
+ }
+ return applicable.Length;
+ }
+
+ bool IsApplicable(MethodData method, Expression[] args)
+ {
+ if (method.Parameters.Length != args.Length)
+ return false;
+ Expression[] promotedArgs = new Expression[args.Length];
+ for (int i = 0; i < args.Length; i++)
+ {
+ ParameterInfo pi = method.Parameters[i];
+ if (pi.IsOut)
+ return false;
+ Expression promoted = PromoteExpression(args[i], pi.ParameterType, false);
+ if (promoted == null)
+ return false;
+ promotedArgs[i] = promoted;
+ }
+ method.Args = promotedArgs;
+ return true;
+ }
+
+ [SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", Justification = "Legacy code.")]
+ [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Legacy code.")]
+ Expression PromoteExpression(Expression expr, Type type, bool exact)
+ {
+ if (expr.Type == type)
+ return expr;
+ if (expr is ConstantExpression)
+ {
+ ConstantExpression ce = (ConstantExpression)expr;
+ if (ce == nullLiteral)
+ {
+ if (!type.IsValueType || IsNullableType(type))
+ return Expression.Constant(null, type);
+ }
+ else
+ {
+ string text;
+ if (literals.TryGetValue(ce, out text))
+ {
+ Type target = GetNonNullableType(type);
+ Object value = null;
+ switch (Type.GetTypeCode(ce.Type))
+ {
+ case TypeCode.Int32:
+ case TypeCode.UInt32:
+ case TypeCode.Int64:
+ case TypeCode.UInt64:
+ if (target.IsEnum)
+ {
+ // promoting from a number to an enum
+ value = Enum.Parse(target, text);
+ }
+ else if (target == typeof(char))
+ {
+ // promote from a number to a char
+ value = Convert.ToChar(ce.Value, CultureInfo.InvariantCulture);
+ }
+ else
+ {
+ value = ParseNumber(text, target);
+ }
+ break;
+ case TypeCode.Double:
+ if (target == typeof(decimal))
+ value = ParseNumber(text, target);
+ break;
+ case TypeCode.String:
+ value = ParseEnum(text, target);
+ break;
+ }
+ if (value != null)
+ return Expression.Constant(value, type);
+ }
+ }
+ }
+ if (IsCompatibleWith(expr.Type, type))
+ {
+ if (type.IsValueType || exact)
+ return Expression.Convert(expr, type);
+ return expr;
+ }
+ return null;
+ }
+
+ static object ParseNumber(string text, Type type)
+ {
+ switch (Type.GetTypeCode(GetNonNullableType(type)))
+ {
+ case TypeCode.SByte:
+ sbyte sb;
+ if (sbyte.TryParse(text, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out sb))
+ return sb;
+ break;
+ case TypeCode.Byte:
+ byte b;
+ if (byte.TryParse(text, NumberStyles.None, CultureInfo.InvariantCulture, out b))
+ return b;
+ break;
+ case TypeCode.Int16:
+ short s;
+ if (short.TryParse(text, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out s))
+ return s;
+ break;
+ case TypeCode.UInt16:
+ ushort us;
+ if (ushort.TryParse(text, NumberStyles.None, CultureInfo.InvariantCulture, out us))
+ return us;
+ break;
+ case TypeCode.Int32:
+ int i;
+ if (int.TryParse(text, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out i))
+ return i;
+ break;
+ case TypeCode.UInt32:
+ uint ui;
+ if (uint.TryParse(text, NumberStyles.None, CultureInfo.InvariantCulture, out ui))
+ return ui;
+ break;
+ case TypeCode.Int64:
+ long l;
+ if (long.TryParse(text, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out l))
+ return l;
+ break;
+ case TypeCode.UInt64:
+ ulong ul;
+ if (ulong.TryParse(text, NumberStyles.None, CultureInfo.InvariantCulture, out ul))
+ return ul;
+ break;
+ case TypeCode.Single:
+ float f;
+ if (float.TryParse(text, NumberStyles.Number, CultureInfo.InvariantCulture, out f))
+ return f;
+ break;
+ case TypeCode.Double:
+ double d;
+ if (double.TryParse(text, NumberStyles.Number, CultureInfo.InvariantCulture, out d))
+ return d;
+ break;
+ case TypeCode.Decimal:
+ decimal e;
+ if (decimal.TryParse(text, NumberStyles.Number, CultureInfo.InvariantCulture, out e))
+ return e;
+ break;
+ }
+ return null;
+ }
+
+ static object ParseEnum(string name, Type type)
+ {
+ if (type.IsEnum)
+ {
+ MemberInfo[] memberInfos = type.FindMembers(MemberTypes.Field,
+ BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Static,
+ Type.FilterNameIgnoreCase, name);
+ if (memberInfos.Length != 0)
+ return ((FieldInfo)memberInfos[0]).GetValue(null);
+ }
+ return null;
+ }
+
+ [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Legacy code.")]
+ static bool IsCompatibleWith(Type source, Type target)
+ {
+ if (source == target)
+ return true;
+ if (!target.IsValueType)
+ return target.IsAssignableFrom(source);
+ Type st = GetNonNullableType(source);
+ Type tt = GetNonNullableType(target);
+ if (st != source && tt == target)
+ return false;
+ TypeCode sc = st.IsEnum ? TypeCode.Object : Type.GetTypeCode(st);
+ TypeCode tc = tt.IsEnum ? TypeCode.Object : Type.GetTypeCode(tt);
+ switch (sc)
+ {
+ case TypeCode.SByte:
+ switch (tc)
+ {
+ case TypeCode.SByte:
+ case TypeCode.Int16:
+ case TypeCode.Int32:
+ case TypeCode.Int64:
+ case TypeCode.Single:
+ case TypeCode.Double:
+ case TypeCode.Decimal:
+ return true;
+ }
+ break;
+ case TypeCode.Byte:
+ switch (tc)
+ {
+ case TypeCode.Byte:
+ case TypeCode.Int16:
+ case TypeCode.UInt16:
+ case TypeCode.Int32:
+ case TypeCode.UInt32:
+ case TypeCode.Int64:
+ case TypeCode.UInt64:
+ case TypeCode.Single:
+ case TypeCode.Double:
+ case TypeCode.Decimal:
+ return true;
+ }
+ break;
+ case TypeCode.Int16:
+ switch (tc)
+ {
+ case TypeCode.Int16:
+ case TypeCode.Int32:
+ case TypeCode.Int64:
+ case TypeCode.Single:
+ case TypeCode.Double:
+ case TypeCode.Decimal:
+ return true;
+ }
+ break;
+ case TypeCode.UInt16:
+ switch (tc)
+ {
+ case TypeCode.UInt16:
+ case TypeCode.Int32:
+ case TypeCode.UInt32:
+ case TypeCode.Int64:
+ case TypeCode.UInt64:
+ case TypeCode.Single:
+ case TypeCode.Double:
+ case TypeCode.Decimal:
+ return true;
+ }
+ break;
+ case TypeCode.Int32:
+ switch (tc)
+ {
+ case TypeCode.Int32:
+ case TypeCode.Int64:
+ case TypeCode.Single:
+ case TypeCode.Double:
+ case TypeCode.Decimal:
+ return true;
+ }
+ break;
+ case TypeCode.UInt32:
+ switch (tc)
+ {
+ case TypeCode.UInt32:
+ case TypeCode.Int64:
+ case TypeCode.UInt64:
+ case TypeCode.Single:
+ case TypeCode.Double:
+ case TypeCode.Decimal:
+ return true;
+ }
+ break;
+ case TypeCode.Int64:
+ switch (tc)
+ {
+ case TypeCode.Int64:
+ case TypeCode.Single:
+ case TypeCode.Double:
+ case TypeCode.Decimal:
+ return true;
+ }
+ break;
+ case TypeCode.UInt64:
+ switch (tc)
+ {
+ case TypeCode.UInt64:
+ case TypeCode.Single:
+ case TypeCode.Double:
+ case TypeCode.Decimal:
+ return true;
+ }
+ break;
+ case TypeCode.Single:
+ switch (tc)
+ {
+ case TypeCode.Single:
+ case TypeCode.Double:
+ return true;
+ }
+ break;
+ default:
+ if (st == tt)
+ return true;
+ break;
+ }
+ return false;
+ }
+
+ static bool IsBetterThan(Expression[] args, MethodData m1, MethodData m2)
+ {
+ bool better = false;
+ for (int i = 0; i < args.Length; i++)
+ {
+ int c = CompareConversions(args[i].Type,
+ m1.Parameters[i].ParameterType,
+ m2.Parameters[i].ParameterType);
+ if (c < 0)
+ return false;
+ if (c > 0)
+ better = true;
+ }
+ return better;
+ }
+
+ // Return 1 if s -> t1 is a better conversion than s -> t2
+ // Return -1 if s -> t2 is a better conversion than s -> t1
+ // Return 0 if neither conversion is better
+ static int CompareConversions(Type s, Type t1, Type t2)
+ {
+ if (t1 == t2)
+ return 0;
+ if (s == t1)
+ return 1;
+ if (s == t2)
+ return -1;
+ bool t1t2 = IsCompatibleWith(t1, t2);
+ bool t2t1 = IsCompatibleWith(t2, t1);
+ if (t1t2 && !t2t1)
+ return 1;
+ if (t2t1 && !t1t2)
+ return -1;
+ if (IsSignedIntegralType(t1) && IsUnsignedIntegralType(t2))
+ return 1;
+ if (IsSignedIntegralType(t2) && IsUnsignedIntegralType(t1))
+ return -1;
+ return 0;
+ }
+
+ static Expression GenerateEqual(Expression left, Expression right)
+ {
+ return Expression.Equal(left, right);
+ }
+
+ static Expression GenerateNotEqual(Expression left, Expression right)
+ {
+ return Expression.NotEqual(left, right);
+ }
+
+ static Expression GenerateGreaterThan(Expression left, Expression right)
+ {
+ if (left.Type == typeof(string))
+ {
+ return Expression.GreaterThan(
+ GenerateStaticMethodCall("Compare", left, right),
+ Expression.Constant(0)
+ );
+ }
+ return Expression.GreaterThan(left, right);
+ }
+
+ static Expression GenerateGreaterThanEqual(Expression left, Expression right)
+ {
+ if (left.Type == typeof(string))
+ {
+ return Expression.GreaterThanOrEqual(
+ GenerateStaticMethodCall("Compare", left, right),
+ Expression.Constant(0)
+ );
+ }
+ return Expression.GreaterThanOrEqual(left, right);
+ }
+
+ static Expression GenerateLessThan(Expression left, Expression right)
+ {
+ if (left.Type == typeof(string))
+ {
+ return Expression.LessThan(
+ GenerateStaticMethodCall("Compare", left, right),
+ Expression.Constant(0)
+ );
+ }
+ return Expression.LessThan(left, right);
+ }
+
+ static Expression GenerateLessThanEqual(Expression left, Expression right)
+ {
+ if (left.Type == typeof(string))
+ {
+ return Expression.LessThanOrEqual(
+ GenerateStaticMethodCall("Compare", left, right),
+ Expression.Constant(0)
+ );
+ }
+ return Expression.LessThanOrEqual(left, right);
+ }
+
+ static Expression GenerateAdd(Expression left, Expression right)
+ {
+ if (left.Type == typeof(string) && right.Type == typeof(string))
+ {
+ return GenerateStaticMethodCall("Concat", left, right);
+ }
+ return Expression.Add(left, right);
+ }
+
+ static Expression GenerateSubtract(Expression left, Expression right)
+ {
+ return Expression.Subtract(left, right);
+ }
+
+ static Expression GenerateStringConcat(Expression left, Expression right)
+ {
+ if (left.Type.IsValueType)
+ left = Expression.Convert(left, typeof(object));
+ if (right.Type.IsValueType)
+ right = Expression.Convert(right, typeof(object));
+ return Expression.Call(
+ null,
+ typeof(string).GetMethod("Concat", new[] { typeof(object), typeof(object) }),
+ new[] { left, right });
+ }
+
+ static MethodInfo GetStaticMethod(string methodName, Expression left, Expression right)
+ {
+ return left.Type.GetMethod(methodName, new[] { left.Type, right.Type });
+ }
+
+ static Expression GenerateStaticMethodCall(string methodName, Expression left, Expression right)
+ {
+ return Expression.Call(null, GetStaticMethod(methodName, left, right), new[] { left, right });
+ }
+
+ void SetTextPos(int pos)
+ {
+ textPos = pos;
+ ch = textPos < textLen ? text[textPos] : '\0';
+ }
+
+ void NextChar()
+ {
+ if (textPos < textLen)
+ textPos++;
+ ch = textPos < textLen ? text[textPos] : '\0';
+ }
+
+ [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Legacy code.")]
+ void NextToken()
+ {
+ while (Char.IsWhiteSpace(ch))
+ NextChar();
+ TokenId t;
+ int tokenPos = textPos;
+ switch (ch)
+ {
+ case '%':
+ NextChar();
+ t = TokenId.Percent;
+ break;
+ case '&':
+ NextChar();
+ if (ch == '&')
+ {
+ NextChar();
+ t = TokenId.DoubleAmphersand;
+ }
+ else
+ {
+ t = TokenId.Amphersand;
+ }
+ break;
+ case '(':
+ NextChar();
+ t = TokenId.OpenParen;
+ break;
+ case ')':
+ NextChar();
+ t = TokenId.CloseParen;
+ break;
+ case ',':
+ NextChar();
+ t = TokenId.Comma;
+ break;
+ case '-':
+ NextChar();
+ t = TokenId.Minus;
+ break;
+ case '/':
+ NextChar();
+ t = TokenId.Dot;
+ break;
+ case ':':
+ NextChar();
+ t = TokenId.Colon;
+ break;
+ case '?':
+ NextChar();
+ t = TokenId.Question;
+ break;
+ case '[':
+ NextChar();
+ t = TokenId.OpenBracket;
+ break;
+ case ']':
+ NextChar();
+ t = TokenId.CloseBracket;
+ break;
+ case '|':
+ NextChar();
+ if (ch == '|')
+ {
+ NextChar();
+ t = TokenId.DoubleBar;
+ }
+ else
+ {
+ t = TokenId.Bar;
+ }
+ break;
+ case '"':
+ case '\'':
+ char quote = ch;
+ do
+ {
+ NextChar();
+ while (textPos < textLen && ch != quote)
+ {
+ if (ch == '\\')
+ {
+ NextChar();
+ }
+
+ NextChar();
+ }
+
+ if (textPos == textLen)
+ throw ParseError(textPos, SRResources.UnterminatedStringLiteral);
+ NextChar();
+ } while (ch == quote);
+ t = TokenId.StringLiteral;
+ break;
+ default:
+ if (IsIdentifierStart(ch) || ch == '@' || ch == '_')
+ {
+ do
+ {
+ NextChar();
+ } while (IsIdentifierPart(ch) || ch == '_');
+ t = TokenId.Identifier;
+ break;
+ }
+ if (Char.IsDigit(ch))
+ {
+ t = TokenId.IntegerLiteral;
+ do
+ {
+ NextChar();
+ } while (Char.IsDigit(ch));
+ if (ch == '.')
+ {
+ t = TokenId.RealLiteral;
+ NextChar();
+ ValidateDigit();
+ do
+ {
+ NextChar();
+ } while (Char.IsDigit(ch));
+ }
+ if (ch == 'E' || ch == 'e')
+ {
+ t = TokenId.RealLiteral;
+ NextChar();
+ if (ch == '+' || ch == '-')
+ NextChar();
+ ValidateDigit();
+ do
+ {
+ NextChar();
+ } while (Char.IsDigit(ch));
+ }
+ if (ch == 'F' || ch == 'f' || ch == 'M' || ch == 'm' || ch == 'D' || ch == 'd')
+ {
+ t = TokenId.RealLiteral;
+ NextChar();
+ }
+ break;
+ }
+ if (textPos == textLen)
+ {
+ t = TokenId.End;
+ break;
+ }
+ throw ParseError(textPos, string.Format(CultureInfo.CurrentCulture, SRResources.InvalidCharacter, ch));
+ }
+ token.id = t;
+ token.text = text.Substring(tokenPos, textPos - tokenPos);
+ token.pos = tokenPos;
+
+ token.id = ReclassifyToken(token);
+ }
+
+ [SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", Justification = "Legacy code.")]
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Legacy code.")]
+ TokenId ReclassifyToken(Token token)
+ {
+ if (token.id == TokenId.Identifier)
+ {
+ if (token.text == "add")
+ {
+ return TokenId.Plus;
+ }
+ else if (token.text == "and")
+ {
+ return TokenId.DoubleAmphersand;
+ }
+ else if (token.text == "div")
+ {
+ return TokenId.Slash;
+ }
+ else if (token.text == "sub")
+ {
+ return TokenId.Minus;
+ }
+ else if (token.text == "mul")
+ {
+ return TokenId.Asterisk;
+ }
+ else if (token.text == "mod")
+ {
+ return TokenId.Percent;
+ }
+ else if (token.text == "ne")
+ {
+ return TokenId.ExclamationEqual;
+ }
+ else if (token.text == "not")
+ {
+ return TokenId.Exclamation;
+ }
+ else if (token.text == "le")
+ {
+ return TokenId.LessThanEqual;
+ }
+ else if (token.text == "lt")
+ {
+ return TokenId.LessThan;
+ }
+ else if (token.text == "eq")
+ {
+ return TokenId.DoubleEqual;
+ }
+ else if (token.text == "eq")
+ {
+ return TokenId.DoubleEqual;
+ }
+ else if (token.text == "ge")
+ {
+ return TokenId.GreaterThanEqual;
+ }
+ else if (token.text == "gt")
+ {
+ return TokenId.GreaterThan;
+ }
+ }
+
+ return token.id;
+ }
+
+ static bool IsIdentifierStart(char ch)
+ {
+ const int mask =
+ 1 << (int)UnicodeCategory.UppercaseLetter |
+ 1 << (int)UnicodeCategory.LowercaseLetter |
+ 1 << (int)UnicodeCategory.TitlecaseLetter |
+ 1 << (int)UnicodeCategory.ModifierLetter |
+ 1 << (int)UnicodeCategory.OtherLetter |
+ 1 << (int)UnicodeCategory.LetterNumber;
+ return (1 << (int)Char.GetUnicodeCategory(ch) & mask) != 0;
+ }
+
+ static bool IsIdentifierPart(char ch)
+ {
+ const int mask =
+ 1 << (int)UnicodeCategory.UppercaseLetter |
+ 1 << (int)UnicodeCategory.LowercaseLetter |
+ 1 << (int)UnicodeCategory.TitlecaseLetter |
+ 1 << (int)UnicodeCategory.ModifierLetter |
+ 1 << (int)UnicodeCategory.OtherLetter |
+ 1 << (int)UnicodeCategory.LetterNumber |
+ 1 << (int)UnicodeCategory.DecimalDigitNumber |
+ 1 << (int)UnicodeCategory.ConnectorPunctuation |
+ 1 << (int)UnicodeCategory.NonSpacingMark |
+ 1 << (int)UnicodeCategory.SpacingCombiningMark |
+ 1 << (int)UnicodeCategory.Format;
+ return (1 << (int)Char.GetUnicodeCategory(ch) & mask) != 0;
+ }
+
+ bool TokenIdentifierIs(string id)
+ {
+ return token.id == TokenId.Identifier && String.Equals(id, token.text, StringComparison.OrdinalIgnoreCase);
+ }
+
+ string GetIdentifier()
+ {
+ ValidateToken(TokenId.Identifier, SRResources.IdentifierExpected);
+ string id = token.text;
+ if (id.Length > 1 && id[0] == '@')
+ id = id.Substring(1);
+ return id;
+ }
+
+ void ValidateDigit()
+ {
+ if (!Char.IsDigit(ch))
+ throw ParseError(textPos, SRResources.DigitExpected);
+ }
+
+ void ValidateToken(TokenId t, string errorMessage)
+ {
+ if (token.id != t)
+ throw ParseError(errorMessage);
+ }
+
+ void ValidateToken(TokenId t)
+ {
+ if (token.id != t)
+ throw ParseError(SRResources.SyntaxError);
+ }
+
+ Exception ParseError(string format, params object[] args)
+ {
+ return ParseError(token.pos, format, args);
+ }
+
+ static Exception ParseError(int pos, string format, params object[] args)
+ {
+ return new ParseException(string.Format(CultureInfo.CurrentCulture, format, args), pos);
+ }
+
+ static Dictionary<string, object> CreateKeywords()
+ {
+ Dictionary<string, object> d = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
+
+ d.Add("true", trueLiteral);
+ d.Add("false", falseLiteral);
+ d.Add("null", nullLiteral);
+
+ // Type keywords
+ d.Add("binary", typeof(byte[]));
+ d.Add("X", typeof(byte[]));
+ d.Add("time", typeof(TimeSpan));
+ d.Add("datetime", typeof(DateTime));
+ d.Add("datetimeoffset", typeof(DateTimeOffset));
+ d.Add("guid", typeof(Guid));
+
+ return d;
+ }
+ }
+
+}
diff --git a/src/System.Web.Http/Query/ODataQueryDeserializer.cs b/src/System.Web.Http/Query/ODataQueryDeserializer.cs
new file mode 100644
index 00000000..5c52f448
--- /dev/null
+++ b/src/System.Web.Http/Query/ODataQueryDeserializer.cs
@@ -0,0 +1,190 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Linq;
+using System.Web.Http.Common;
+using System.Web.Http.Internal;
+using System.Web.Http.Properties;
+
+namespace System.Web.Http.Query
+{
+ /// <summary>
+ /// Used to deserialize a set of string based query operations into expressions and
+ /// compose them over a specified query.
+ /// </summary>
+ internal static class ODataQueryDeserializer
+ {
+ /// <summary>
+ /// Deserializes the query operations in the specified Uri and applies them
+ /// to the specified IQueryable.
+ /// </summary>
+ /// <param name="query">The root query to compose the deserialized query over.</param>
+ /// <param name="uri">The request Uri containing the query operations.</param>
+ /// <returns>The resulting IQueryable with the deserialized query composed over it.</returns>
+ public static IQueryable Deserialize(IQueryable query, Uri uri)
+ {
+ if (query == null)
+ {
+ throw Error.ArgumentNull("query");
+ }
+
+ if (uri == null)
+ {
+ throw Error.ArgumentNull("uri");
+ }
+
+ ServiceQuery serviceQuery = GetServiceQuery(uri);
+
+ return Deserialize(query, serviceQuery.QueryParts, null);
+ }
+
+ /// <summary>
+ /// Deserializes the query operations in the specified Uri and returns an IQueryable
+ /// with a manufactured query root with those operations applied.
+ /// </summary>
+ /// <typeparam name="T">The element type of the query</typeparam>
+ /// <param name="uri">The request Uri containing the query operations.</param>
+ /// <returns>The resulting IQueryable with the deserialized query composed over it.</returns>
+ public static IQueryable<T> Deserialize<T>(Uri uri)
+ {
+ if (uri == null)
+ {
+ throw Error.ArgumentNull("uri");
+ }
+
+ return (IQueryable<T>)Deserialize(typeof(T), uri);
+ }
+
+ /// <summary>
+ /// Deserializes the query operations in the specified Uri and returns an IQueryable
+ /// with a manufactured query root with those operations applied.
+ /// </summary>
+ /// <param name="elementType">The element type of the query</param>
+ /// <param name="uri">The request Uri containing the query operations.</param>
+ /// <returns>The resulting IQueryable with the deserialized query composed over it.</returns>
+ public static IQueryable Deserialize(Type elementType, Uri uri)
+ {
+ if (elementType == null)
+ {
+ throw Error.ArgumentNull("elementType");
+ }
+
+ if (uri == null)
+ {
+ throw Error.ArgumentNull("uri");
+ }
+
+ ServiceQuery serviceQuery = GetServiceQuery(uri);
+
+ Array array = Array.CreateInstance(elementType, 0);
+ IQueryable baseQuery = ((IEnumerable)array).AsQueryable();
+
+ return Deserialize(baseQuery, serviceQuery.QueryParts, null);
+ }
+
+ internal static IQueryable Deserialize(IQueryable query, IEnumerable<ServiceQueryPart> queryParts)
+ {
+ if (query == null)
+ {
+ throw Error.ArgumentNull("query");
+ }
+
+ if (queryParts == null)
+ {
+ throw Error.ArgumentNull("queryParts");
+ }
+
+ return Deserialize(query, queryParts, null);
+ }
+
+ internal static IQueryable Deserialize(IQueryable query, IEnumerable<ServiceQueryPart> queryParts, QueryResolver queryResolver)
+ {
+ if (query == null)
+ {
+ throw Error.ArgumentNull("query");
+ }
+
+ if (queryParts == null)
+ {
+ throw Error.ArgumentNull("queryParts");
+ }
+
+ foreach (ServiceQueryPart part in queryParts)
+ {
+ switch (part.QueryOperator)
+ {
+ case "filter":
+ query = DynamicQueryable.Where(query, part.Expression, queryResolver);
+ break;
+ case "orderby":
+ query = DynamicQueryable.OrderBy(query, part.Expression, queryResolver);
+ break;
+ case "skip":
+ int skipCount = Convert.ToInt32(part.Expression, System.Globalization.CultureInfo.InvariantCulture);
+ if (skipCount < 0)
+ {
+ throw Error.InvalidOperation(SRResources.PositiveIntegerExpectedForODataQueryParameter, "$skip", part.Expression);
+ }
+ query = DynamicQueryable.Skip(query, skipCount);
+ break;
+ case "top":
+ int topCount = Convert.ToInt32(part.Expression, System.Globalization.CultureInfo.InvariantCulture);
+ if (topCount < 0)
+ {
+ throw Error.InvalidOperation(SRResources.PositiveIntegerExpectedForODataQueryParameter, "$top", part.Expression);
+ }
+ query = DynamicQueryable.Take(query, topCount);
+ break;
+ }
+ }
+
+ return query;
+ }
+
+ internal static ServiceQuery GetServiceQuery(Uri uri)
+ {
+ if (uri == null)
+ {
+ throw Error.ArgumentNull("uri");
+ }
+
+ NameValueCollection queryPartCollection = UriQueryUtility.ParseQueryString(uri.Query);
+
+ List<ServiceQueryPart> serviceQueryParts = new List<ServiceQueryPart>();
+ foreach (string queryPart in queryPartCollection)
+ {
+ if (queryPart == null || !queryPart.StartsWith("$", StringComparison.Ordinal))
+ {
+ // not a special query string
+ continue;
+ }
+
+ foreach (string value in queryPartCollection.GetValues(queryPart))
+ {
+ string queryOperator = queryPart.Substring(1);
+ if (!ServiceQuery.IsSupportedQueryOperator(queryOperator))
+ {
+ // skip any operators we don't support
+ continue;
+ }
+
+ ServiceQueryPart serviceQueryPart = new ServiceQueryPart(queryOperator, value);
+ serviceQueryParts.Add(serviceQueryPart);
+ }
+ }
+
+ // Query parts for OData need to be ordered $filter, $orderby, $skip, $top. For this
+ // set of query operators, they are already in alphabetical order, so it suffices to
+ // order by operator name. In the future if we support other operators, this may need
+ // to be reexamined.
+ serviceQueryParts = serviceQueryParts.OrderBy(p => p.QueryOperator).ToList();
+
+ ServiceQuery serviceQuery = new ServiceQuery()
+ {
+ QueryParts = serviceQueryParts,
+ };
+
+ return serviceQuery;
+ }
+ }
+}
diff --git a/src/System.Web.Http/Query/QueryComposer.cs b/src/System.Web.Http/Query/QueryComposer.cs
new file mode 100644
index 00000000..99d15ca2
--- /dev/null
+++ b/src/System.Web.Http/Query/QueryComposer.cs
@@ -0,0 +1,71 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+
+namespace System.Web.Http.Query
+{
+ // TODO: ability to extend the composer. Bug 322919
+
+ /// <summary>
+ /// Used to compose two separate queries into a single query
+ /// </summary>
+ internal static class QueryComposer
+ {
+ /// <summary>
+ /// Composes the specified query with the source provided.
+ /// </summary>
+ /// <param name="source">The root or source query</param>
+ /// <param name="query">The query to compose</param>
+ /// <returns>The composed query</returns>
+ public static IQueryable Compose(IQueryable source, IQueryable query)
+ {
+ return QueryRebaser.Rebase(source, query);
+ }
+
+ /// <summary>
+ /// Class used to insert a specified query source into another separate
+ /// query, effectively "rebasing" the query source.
+ /// </summary>
+ public class QueryRebaser : ExpressionVisitor
+ {
+ /// <summary>
+ /// Rebase the specified query to the specified source
+ /// </summary>
+ /// <param name="source">The query source</param>
+ /// <param name="query">The query to rebase</param>
+ /// <returns>Returns the edited query.</returns>
+ public static IQueryable Rebase(IQueryable source, IQueryable query)
+ {
+ Visitor v = new Visitor(source.Expression);
+ Expression expr = v.Visit(query.Expression);
+ return source.Provider.CreateQuery(expr);
+ }
+
+ private class Visitor : ExpressionVisitor
+ {
+ private Expression _root;
+
+ public Visitor(Expression root)
+ {
+ _root = root;
+ }
+
+ protected override Expression VisitMethodCall(MethodCallExpression m)
+ {
+ if ((m.Arguments.Count > 0 && m.Arguments[0].NodeType == ExpressionType.Constant) &&
+ (((ConstantExpression)m.Arguments[0]).Value != null) &&
+ (((ConstantExpression)m.Arguments[0]).Value is IQueryable))
+ {
+ // we found the innermost source which we replace with the
+ // specified source
+ List<Expression> exprs = new List<Expression>();
+ exprs.Add(_root);
+ exprs.AddRange(m.Arguments.Skip(1));
+ return Expression.Call(m.Method, exprs.ToArray());
+ }
+ return base.VisitMethodCall(m);
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/Query/QueryResolver.cs b/src/System.Web.Http/Query/QueryResolver.cs
new file mode 100644
index 00000000..a9ac6ce2
--- /dev/null
+++ b/src/System.Web.Http/Query/QueryResolver.cs
@@ -0,0 +1,19 @@
+using System.Linq.Expressions;
+
+namespace System.Web.Http.Query
+{
+ /// <summary>
+ /// Defines a set of methods that can participate in query deserialization.
+ /// </summary>
+ internal abstract class QueryResolver
+ {
+ /// <summary>
+ /// Called to attempt to resolve unresolved member references during query deserialization.
+ /// </summary>
+ /// <param name="type">The Type the member is expected on.</param>
+ /// <param name="member">The member name.</param>
+ /// <param name="instance">The instance to form the MemberExpression on.</param>
+ /// <returns>A MemberExpression if the member can be resolved, null otherwise.</returns>
+ public abstract MemberExpression ResolveMember(Type type, string member, Expression instance);
+ }
+}
diff --git a/src/System.Web.Http/Query/QueryTypeHelper.cs b/src/System.Web.Http/Query/QueryTypeHelper.cs
new file mode 100644
index 00000000..c4a1cf91
--- /dev/null
+++ b/src/System.Web.Http/Query/QueryTypeHelper.cs
@@ -0,0 +1,49 @@
+using System.Diagnostics.Contracts;
+using System.Linq;
+
+namespace System.Web.Http.Query
+{
+ /// <summary>
+ /// A static class that provides Queryable related types and functionality
+ /// to perform checks related on them.
+ /// </summary>
+ internal static class QueryTypeHelper
+ {
+ private static readonly Type _queryableInterfaceGenericType = typeof(IQueryable<>);
+
+ internal static Type GetQueryableInterfaceInnerTypeOrNull(Type type)
+ {
+ if (type == null)
+ {
+ return type;
+ }
+
+ if (IsQueryableInterfaceGenericType(type))
+ {
+ return type.GetGenericArguments()[0];
+ }
+ else if (ImplementsQueryableInterfaceGenericType(type))
+ {
+ return type.GetInterface(_queryableInterfaceGenericType.FullName).GetGenericArguments()[0];
+ }
+
+ return null;
+ }
+
+ private static bool IsQueryableInterfaceGenericType(Type type)
+ {
+ Contract.Assert(type != null);
+
+ return type.IsInterface
+ && type.IsGenericType
+ && type.GetGenericTypeDefinition().Equals(_queryableInterfaceGenericType);
+ }
+
+ private static bool ImplementsQueryableInterfaceGenericType(Type type)
+ {
+ Contract.Assert(type != null);
+
+ return type.GetInterface(_queryableInterfaceGenericType.FullName) != null;
+ }
+ }
+}
diff --git a/src/System.Web.Http/Query/QueryValidator.cs b/src/System.Web.Http/Query/QueryValidator.cs
new file mode 100644
index 00000000..065f6538
--- /dev/null
+++ b/src/System.Web.Http/Query/QueryValidator.cs
@@ -0,0 +1,71 @@
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Runtime.Serialization;
+using System.Web.Http.Common;
+using System.Web.Http.Properties;
+using System.Xml.Serialization;
+
+namespace System.Web.Http.Query
+{
+ /// <summary>
+ /// This class is used to do validation on the query generated by the ODataQueryDeserializer. The default implementation
+ /// validates that the query is not accessing members hidden through attributes like <c ref="XmlIgnoreAttribute"/>, <c ref="IgnoreDataMemberAttribute" /> and
+ /// <c ref="NonSerializedAttribute" />
+ /// </summary>
+ internal class QueryValidator
+ {
+ private static QueryValidator _instance = new QueryValidator();
+
+ protected QueryValidator()
+ {
+ }
+
+ public static QueryValidator Instance
+ {
+ get { return _instance; }
+ }
+
+ public virtual void Validate(IQueryable query)
+ {
+ QueryValidationVisitor.Validate(query);
+ }
+
+ private class QueryValidationVisitor : ExpressionVisitor
+ {
+ public static void Validate(IQueryable query)
+ {
+ QueryValidationVisitor visitor = new QueryValidationVisitor();
+ visitor.Visit(query.Expression);
+ }
+
+ protected override Expression VisitMember(MemberExpression node)
+ {
+ if (!IsVisible(node.Member))
+ {
+ throw Error.InvalidOperation(SRResources.UnknownPropertyOrField, node.Member.Name, node.Member.DeclaringType.Name);
+ }
+
+ return base.VisitMember(node);
+ }
+
+ private static bool IsVisible(MemberInfo member)
+ {
+ // REVIEW: should i cache this ?
+ object[] attributes = member.GetCustomAttributes(inherit: true);
+ object[] parentAttributes = member.ReflectedType.GetCustomAttributes(inherit: true);
+
+ return !(
+ (HasAttribute<DataContractAttribute>(parentAttributes) && !HasAttribute<DataMemberAttribute>(attributes)) // isDataContractAndNotDataMember
+ || HasAttribute<XmlIgnoreAttribute>(attributes)
+ || HasAttribute<IgnoreDataMemberAttribute>(attributes)
+ || HasAttribute<NonSerializedAttribute>(attributes));
+ }
+
+ private static bool HasAttribute<T>(object[] attribs)
+ {
+ return attribs.Length != 0 && attribs.OfType<T>().Any();
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/Query/ServiceQuery.cs b/src/System.Web.Http/Query/ServiceQuery.cs
new file mode 100644
index 00000000..14c4f9a9
--- /dev/null
+++ b/src/System.Web.Http/Query/ServiceQuery.cs
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+
+namespace System.Web.Http.Query
+{
+ /// <summary>
+ /// Represents an <see cref="System.Linq.IQueryable"/>.
+ /// </summary>
+ internal class ServiceQuery
+ {
+ /// <summary>
+ /// Gets or sets a list of query parts.
+ /// </summary>
+ public IEnumerable<ServiceQueryPart> QueryParts { get; set; }
+
+ public static bool IsSupportedQueryOperator(string queryOperator)
+ {
+ return queryOperator == "filter" || queryOperator == "orderby" ||
+ queryOperator == "skip" || queryOperator == "top";
+ }
+ }
+}
diff --git a/src/System.Web.Http/Query/ServiceQueryPart.cs b/src/System.Web.Http/Query/ServiceQueryPart.cs
new file mode 100644
index 00000000..a6bc1d86
--- /dev/null
+++ b/src/System.Web.Http/Query/ServiceQueryPart.cs
@@ -0,0 +1,62 @@
+using System.Web.Http.Common;
+using System.Web.Http.Properties;
+
+namespace System.Web.Http.Query
+{
+ /// <summary>
+ /// Represents a single query operator to be applied to a query
+ /// </summary>
+ internal class ServiceQueryPart
+ {
+ /// <summary>
+ /// Public constructor
+ /// </summary>
+ public ServiceQueryPart()
+ {
+ }
+
+ /// <summary>
+ /// Public constructor
+ /// </summary>
+ /// <param name="queryOperator">The query operator</param>
+ /// <param name="expression">The query expression</param>
+ public ServiceQueryPart(string queryOperator, string expression)
+ {
+ if (queryOperator == null)
+ {
+ throw Error.ArgumentNull("queryOperator");
+ }
+ if (expression == null)
+ {
+ throw Error.ArgumentNull("expression");
+ }
+
+ if (!ServiceQuery.IsSupportedQueryOperator(queryOperator))
+ {
+ throw Error.Argument("queryOperator", SRResources.InvalidQueryOperator, queryOperator);
+ }
+
+ QueryOperator = queryOperator;
+ Expression = expression;
+ }
+
+ /// <summary>
+ /// Gets or sets the query operator. Must be one of the supported operators : "where", "orderby", "skip", or "take".
+ /// </summary>
+ public string QueryOperator { get; set; }
+
+ /// <summary>
+ /// Gets or sets the query expression.
+ /// </summary>
+ public string Expression { get; set; }
+
+ /// <summary>
+ /// Returns a string representation of this <see cref="ServiceQueryPart"/>
+ /// </summary>
+ /// <returns>The string representation of this <see cref="ServiceQueryPart"/></returns>
+ public override string ToString()
+ {
+ return String.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}={1}", QueryOperator, Expression);
+ }
+ }
+}
diff --git a/src/System.Web.Http/ResultLimitAttribute.cs b/src/System.Web.Http/ResultLimitAttribute.cs
new file mode 100644
index 00000000..e34cfc8a
--- /dev/null
+++ b/src/System.Web.Http/ResultLimitAttribute.cs
@@ -0,0 +1,72 @@
+using System.Collections;
+using System.Diagnostics.Contracts;
+using System.Linq;
+using System.Net.Http;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Filters;
+using System.Web.Http.Properties;
+using System.Web.Http.Query;
+
+namespace System.Web.Http
+{
+ /// <summary>
+ /// This result filter indicates that the results returned from an action should
+ /// be limited to the specified ResultLimit.
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+ public sealed class ResultLimitAttribute : ActionFilterAttribute
+ {
+ private int _resultLimit;
+
+ public ResultLimitAttribute(int resultLimit)
+ {
+ _resultLimit = resultLimit;
+ }
+
+ public int ResultLimit
+ {
+ get { return _resultLimit; }
+ }
+
+ public override void OnActionExecuting(HttpActionContext actionContext)
+ {
+ if (actionContext == null)
+ {
+ throw Error.ArgumentNull("actionContext");
+ }
+
+ Contract.Assert(actionContext.ControllerContext.Request != null);
+
+ if (_resultLimit <= 0)
+ {
+ Error.ArgumentOutOfRange("resultLimit", _resultLimit, SRResources.ResultLimitFilter_OutOfRange, actionContext.ActionDescriptor.ActionName);
+ }
+
+ HttpActionDescriptor action = actionContext.ActionDescriptor;
+ if (!typeof(IEnumerable).IsAssignableFrom(action.ReturnType))
+ {
+ Error.InvalidOperation(SRResources.ResultLimitFilter_InvalidReturnType, action.ActionName);
+ }
+ }
+
+ public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
+ {
+ if (actionExecutedContext == null)
+ {
+ throw Error.ArgumentNull("actionExecutedContext");
+ }
+
+ Contract.Assert(actionExecutedContext.Request != null);
+
+ HttpResponseMessage response = actionExecutedContext.Result;
+ IEnumerable results;
+ if (response != null && response.TryGetObjectValue(out results))
+ {
+ // apply the result limit
+ results = results.AsQueryable().Take(_resultLimit);
+ response.TrySetObjectValue(results);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/Routing/BoundRouteTemplate.cs b/src/System.Web.Http/Routing/BoundRouteTemplate.cs
new file mode 100644
index 00000000..b6a14f5e
--- /dev/null
+++ b/src/System.Web.Http/Routing/BoundRouteTemplate.cs
@@ -0,0 +1,12 @@
+namespace System.Web.Http.Routing
+{
+ /// <summary>
+ /// Represents a URI generated from a <see cref="HttpParsedRoute"/>.
+ /// </summary>
+ internal class BoundRouteTemplate
+ {
+ public string BoundTemplate { get; set; }
+
+ public HttpRouteValueDictionary Values { get; set; }
+ }
+}
diff --git a/src/System.Web.Http/Routing/HttpMethodConstraint.cs b/src/System.Web.Http/Routing/HttpMethodConstraint.cs
new file mode 100644
index 00000000..87cbf9be
--- /dev/null
+++ b/src/System.Web.Http/Routing/HttpMethodConstraint.cs
@@ -0,0 +1,85 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Net.Http;
+using System.Web.Http.Common;
+
+namespace System.Web.Http.Routing
+{
+ public class HttpMethodConstraint : IHttpRouteConstraint
+ {
+ public HttpMethodConstraint(params HttpMethod[] allowedMethods)
+ {
+ if (allowedMethods == null)
+ {
+ throw Error.ArgumentNull("allowedMethods");
+ }
+
+ AllowedMethods = new Collection<HttpMethod>(allowedMethods);
+ }
+
+ public Collection<HttpMethod> AllowedMethods { get; private set; }
+
+ protected virtual bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values, HttpRouteDirection routeDirection)
+ {
+ if (request == null)
+ {
+ throw Error.ArgumentNull("request");
+ }
+
+ if (route == null)
+ {
+ throw Error.ArgumentNull("route");
+ }
+
+ if (parameterName == null)
+ {
+ throw Error.ArgumentNull("parameterName");
+ }
+
+ if (values == null)
+ {
+ throw Error.ArgumentNull("values");
+ }
+
+ switch (routeDirection)
+ {
+ case HttpRouteDirection.UriResolution:
+ return AllowedMethods.Contains(request.Method);
+
+ case HttpRouteDirection.UriGeneration:
+ // We need to see if the user specified the HTTP method explicitly. Consider these two routes:
+ //
+ // a) Route: template = "/{foo}", Constraints = { httpMethod = new HttpMethodConstraint("GET") }
+ // b) Route: template = "/{foo}", Constraints = { httpMethod = new HttpMethodConstraint("POST") }
+ //
+ // A user might know ahead of time that a URI he/she is generating might be used with a particular HTTP
+ // method. If a URI will be used for an HTTP POST but we match on (a) while generating the URI, then
+ // the HTTP GET-specific route will be used for URI generation, which might have undesired behavior.
+ // To prevent this, a user might call RouteCollection.GetVirtualPath(..., { httpMethod = "POST" }) to
+ // signal that he is generating a URI that will be used for an HTTP POST, so he wants the URI
+ // generation to be performed by the (b) route instead of the (a) route, consistent with what would
+ // happen on incoming requests.
+ HttpMethod constraint;
+ if (!values.TryGetValue(parameterName, out constraint))
+ {
+ return true;
+ }
+
+ return AllowedMethods.Contains(constraint);
+
+ default:
+ throw Error.InvalidEnumArgument(String.Empty, (int)routeDirection, typeof(HttpRouteDirection));
+ }
+ }
+
+ #region IRouteConstraint Members
+
+ bool IHttpRouteConstraint.Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values, HttpRouteDirection routeDirection)
+ {
+ return Match(request, route, parameterName, values, routeDirection);
+ }
+
+ #endregion
+ }
+}
diff --git a/src/System.Web.Http/Routing/HttpParsedRoute.cs b/src/System.Web.Http/Routing/HttpParsedRoute.cs
new file mode 100644
index 00000000..2a1f7cbc
--- /dev/null
+++ b/src/System.Web.Http/Routing/HttpParsedRoute.cs
@@ -0,0 +1,815 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace System.Web.Http.Routing
+{
+ internal sealed class HttpParsedRoute
+ {
+ private IList<PathSegment> _pathSegments;
+
+ public HttpParsedRoute(IList<PathSegment> pathSegments)
+ {
+ Contract.Assert(pathSegments != null);
+ _pathSegments = pathSegments;
+ }
+
+ [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Not changing original algorithm")]
+ [SuppressMessage("Microsoft.Maintainability", "CA1505:AvoidUnmaintainableCode", Justification = "Not changing original algorithm")]
+ public BoundRouteTemplate Bind(IDictionary<string, object> currentValues, IDictionary<string, object> values, HttpRouteValueDictionary defaultValues, HttpRouteValueDictionary constraints)
+ {
+ if (currentValues == null)
+ {
+ currentValues = new HttpRouteValueDictionary();
+ }
+
+ if (values == null)
+ {
+ values = new HttpRouteValueDictionary();
+ }
+
+ if (defaultValues == null)
+ {
+ defaultValues = new HttpRouteValueDictionary();
+ }
+
+ // The set of values we should be using when generating the URI in this route
+ HttpRouteValueDictionary acceptedValues = new HttpRouteValueDictionary();
+
+ // Keep track of which new values have been used
+ HashSet<string> unusedNewValues = new HashSet<string>(values.Keys, StringComparer.OrdinalIgnoreCase);
+
+ // Step 1: Get the list of values we're going to try to use to match and generate this URI
+
+ // Find out which entries in the URI are valid for the URI we want to generate.
+ // If the URI had ordered parameters a="1", b="2", c="3" and the new values
+ // specified that b="9", then we need to invalidate everything after it. The new
+ // values should then be a="1", b="9", c=<no value>.
+ ForEachParameter(_pathSegments, delegate(PathParameterSubsegment parameterSubsegment)
+ {
+ // If it's a parameter subsegment, examine the current value to see if it matches the new value
+ string parameterName = parameterSubsegment.ParameterName;
+
+ object newParameterValue;
+ bool hasNewParameterValue = values.TryGetValue(parameterName, out newParameterValue);
+ if (hasNewParameterValue)
+ {
+ unusedNewValues.Remove(parameterName);
+ }
+
+ object currentParameterValue;
+ bool hasCurrentParameterValue = currentValues.TryGetValue(parameterName, out currentParameterValue);
+
+ if (hasNewParameterValue && hasCurrentParameterValue)
+ {
+ if (!RoutePartsEqual(currentParameterValue, newParameterValue))
+ {
+ // Stop copying current values when we find one that doesn't match
+ return false;
+ }
+ }
+
+ // If the parameter is a match, add it to the list of values we will use for URI generation
+ if (hasNewParameterValue)
+ {
+ if (IsRoutePartNonEmpty(newParameterValue))
+ {
+ acceptedValues.Add(parameterName, newParameterValue);
+ }
+ }
+ else
+ {
+ if (hasCurrentParameterValue)
+ {
+ acceptedValues.Add(parameterName, currentParameterValue);
+ }
+ }
+ return true;
+ });
+
+ // Add all remaining new values to the list of values we will use for URI generation
+ foreach (var newValue in values)
+ {
+ if (IsRoutePartNonEmpty(newValue.Value))
+ {
+ if (!acceptedValues.ContainsKey(newValue.Key))
+ {
+ acceptedValues.Add(newValue.Key, newValue.Value);
+ }
+ }
+ }
+
+ // Add all current values that aren't in the URI at all
+ foreach (var currentValue in currentValues)
+ {
+ string parameterName = currentValue.Key;
+ if (!acceptedValues.ContainsKey(parameterName))
+ {
+ PathParameterSubsegment parameterSubsegment = GetParameterSubsegment(_pathSegments, parameterName);
+ if (parameterSubsegment == null)
+ {
+ acceptedValues.Add(parameterName, currentValue.Value);
+ }
+ }
+ }
+
+ // Add all remaining default values from the route to the list of values we will use for URI generation
+ ForEachParameter(_pathSegments, delegate(PathParameterSubsegment parameterSubsegment)
+ {
+ if (!acceptedValues.ContainsKey(parameterSubsegment.ParameterName))
+ {
+ object defaultValue;
+ if (!IsParameterRequired(parameterSubsegment, defaultValues, out defaultValue))
+ {
+ // Add the default value only if there isn't already a new value for it and
+ // only if it actually has a default value, which we determine based on whether
+ // the parameter value is required.
+ acceptedValues.Add(parameterSubsegment.ParameterName, defaultValue);
+ }
+ }
+ return true;
+ });
+
+ // All required parameters in this URI must have values from somewhere (i.e. the accepted values)
+ bool hasAllRequiredValues = ForEachParameter(_pathSegments, delegate(PathParameterSubsegment parameterSubsegment)
+ {
+ object defaultValue;
+ if (IsParameterRequired(parameterSubsegment, defaultValues, out defaultValue))
+ {
+ if (!acceptedValues.ContainsKey(parameterSubsegment.ParameterName))
+ {
+ // If the route parameter value is required that means there's
+ // no default value, so if there wasn't a new value for it
+ // either, this route won't match.
+ return false;
+ }
+ }
+ return true;
+ });
+ if (!hasAllRequiredValues)
+ {
+ return null;
+ }
+
+ // All other default values must match if they are explicitly defined in the new values
+ HttpRouteValueDictionary otherDefaultValues = new HttpRouteValueDictionary(defaultValues);
+ ForEachParameter(_pathSegments, delegate(PathParameterSubsegment parameterSubsegment)
+ {
+ otherDefaultValues.Remove(parameterSubsegment.ParameterName);
+ return true;
+ });
+
+ foreach (var defaultValue in otherDefaultValues)
+ {
+ object value;
+ if (values.TryGetValue(defaultValue.Key, out value))
+ {
+ unusedNewValues.Remove(defaultValue.Key);
+ if (!RoutePartsEqual(value, defaultValue.Value))
+ {
+ // If there is a non-parameterized value in the route and there is a
+ // new value for it and it doesn't match, this route won't match.
+ return null;
+ }
+ }
+ }
+
+ // Step 2: If the route is a match generate the appropriate URI
+
+ StringBuilder uri = new StringBuilder();
+ StringBuilder pendingParts = new StringBuilder();
+
+ bool pendingPartsAreAllSafe = false;
+ bool blockAllUriAppends = false;
+
+ for (int i = 0; i < _pathSegments.Count; i++)
+ {
+ PathSegment pathSegment = _pathSegments[i]; // parsedRouteUriPart
+
+ if (pathSegment is PathSeparatorSegment)
+ {
+ if (pendingPartsAreAllSafe)
+ {
+ // Accept
+ if (pendingParts.Length > 0)
+ {
+ if (blockAllUriAppends)
+ {
+ return null;
+ }
+
+ // Append any pending literals to the URI
+ uri.Append(pendingParts.ToString());
+ pendingParts.Length = 0;
+ }
+ }
+ pendingPartsAreAllSafe = false;
+
+ // Guard against appending multiple separators for empty segments
+ if (pendingParts.Length > 0 && pendingParts[pendingParts.Length - 1] == '/')
+ {
+ // Dev10 676725: Route should not be matched if that causes mismatched tokens
+ // Dev11 86819: We will allow empty matches if all subsequent segments are null
+ if (blockAllUriAppends)
+ {
+ return null;
+ }
+
+ // Append any pending literals to the URI (without the trailing slash) and prevent any future appends
+ uri.Append(pendingParts.ToString(0, pendingParts.Length - 1));
+ pendingParts.Length = 0;
+ blockAllUriAppends = true;
+ }
+ else
+ {
+ pendingParts.Append("/");
+ }
+ }
+ else
+ {
+ PathContentSegment contentPathSegment = pathSegment as PathContentSegment;
+ if (contentPathSegment != null)
+ {
+ // Segments are treated as all-or-none. We should never output a partial segment.
+ // If we add any subsegment of this segment to the generated URI, we have to add
+ // the complete match. For example, if the subsegment is "{p1}-{p2}.xml" and we
+ // used a value for {p1}, we have to output the entire segment up to the next "/".
+ // Otherwise we could end up with the partial segment "v1" instead of the entire
+ // segment "v1-v2.xml".
+ bool addedAnySubsegments = false;
+
+ foreach (PathSubsegment subsegment in contentPathSegment.Subsegments)
+ {
+ PathLiteralSubsegment literalSubsegment = subsegment as PathLiteralSubsegment;
+ if (literalSubsegment != null)
+ {
+ // If it's a literal we hold on to it until we are sure we need to add it
+ pendingPartsAreAllSafe = true;
+ pendingParts.Append(literalSubsegment.Literal);
+ }
+ else
+ {
+ PathParameterSubsegment parameterSubsegment = subsegment as PathParameterSubsegment;
+ if (parameterSubsegment != null)
+ {
+ if (pendingPartsAreAllSafe)
+ {
+ // Accept
+ if (pendingParts.Length > 0)
+ {
+ if (blockAllUriAppends)
+ {
+ return null;
+ }
+
+ // Append any pending literals to the URI
+ uri.Append(pendingParts.ToString());
+ pendingParts.Length = 0;
+
+ addedAnySubsegments = true;
+ }
+ }
+ pendingPartsAreAllSafe = false;
+
+ // If it's a parameter, get its value
+ object acceptedParameterValue;
+ bool hasAcceptedParameterValue = acceptedValues.TryGetValue(parameterSubsegment.ParameterName, out acceptedParameterValue);
+ if (hasAcceptedParameterValue)
+ {
+ unusedNewValues.Remove(parameterSubsegment.ParameterName);
+ }
+
+ object defaultParameterValue;
+ defaultValues.TryGetValue(parameterSubsegment.ParameterName, out defaultParameterValue);
+
+ if (RoutePartsEqual(acceptedParameterValue, defaultParameterValue))
+ {
+ // If the accepted value is the same as the default value, mark it as pending since
+ // we won't necessarily add it to the URI we generate.
+ pendingParts.Append(Convert.ToString(acceptedParameterValue, CultureInfo.InvariantCulture));
+ }
+ else
+ {
+ if (blockAllUriAppends)
+ {
+ return null;
+ }
+
+ // Add the new part to the URI as well as any pending parts
+ if (pendingParts.Length > 0)
+ {
+ // Append any pending literals to the URI
+ uri.Append(pendingParts.ToString());
+ pendingParts.Length = 0;
+ }
+ uri.Append(Convert.ToString(acceptedParameterValue, CultureInfo.InvariantCulture));
+
+ addedAnySubsegments = true;
+ }
+ }
+ else
+ {
+ Contract.Assert(false, "Invalid path subsegment type");
+ }
+ }
+ }
+
+ if (addedAnySubsegments)
+ {
+ // See comment above about why we add the pending parts
+ if (pendingParts.Length > 0)
+ {
+ if (blockAllUriAppends)
+ {
+ return null;
+ }
+
+ // Append any pending literals to the URI
+ uri.Append(pendingParts.ToString());
+ pendingParts.Length = 0;
+ }
+ }
+ }
+ else
+ {
+ Contract.Assert(false, "Invalid path segment type");
+ }
+ }
+ }
+
+ if (pendingPartsAreAllSafe)
+ {
+ // Accept
+ if (pendingParts.Length > 0)
+ {
+ if (blockAllUriAppends)
+ {
+ return null;
+ }
+
+ // Append any pending literals to the URI
+ uri.Append(pendingParts.ToString());
+ }
+ }
+
+ // Process constraints keys
+ if (constraints != null)
+ {
+ // If there are any constraints, mark all the keys as being used so that we don't
+ // generate query string items for custom constraints that don't appear as parameters
+ // in the URI format.
+ foreach (var constraintsItem in constraints)
+ {
+ unusedNewValues.Remove(constraintsItem.Key);
+ }
+ }
+
+ // Encode the URI before we append the query string, otherwise we would double encode the query string
+ StringBuilder encodedUri = new StringBuilder();
+ encodedUri.Append(UriEncode(uri.ToString()));
+ uri = encodedUri;
+
+ // Add remaining new values as query string parameters to the URI
+ if (unusedNewValues.Count > 0)
+ {
+ // Generate the query string
+ bool firstParam = true;
+ foreach (string unusedNewValue in unusedNewValues)
+ {
+ object value;
+ if (acceptedValues.TryGetValue(unusedNewValue, out value))
+ {
+ uri.Append(firstParam ? '?' : '&');
+ firstParam = false;
+ uri.Append(Uri.EscapeDataString(unusedNewValue));
+ uri.Append('=');
+ uri.Append(Uri.EscapeDataString(Convert.ToString(value, CultureInfo.InvariantCulture)));
+ }
+ }
+ }
+
+ return new BoundRouteTemplate
+ {
+ BoundTemplate = uri.ToString(),
+ Values = acceptedValues
+ };
+ }
+
+ private static string EscapeReservedCharacters(Match m)
+ {
+ return Uri.HexEscape(m.Value[0]);
+ }
+
+ private static bool ForEachParameter(IList<PathSegment> pathSegments, Func<PathParameterSubsegment, bool> action)
+ {
+ for (int i = 0; i < pathSegments.Count; i++)
+ {
+ PathSegment pathSegment = pathSegments[i];
+
+ if (pathSegment is PathSeparatorSegment)
+ {
+ // We only care about parameter subsegments, so skip this
+ continue;
+ }
+ else
+ {
+ PathContentSegment contentPathSegment = pathSegment as PathContentSegment;
+ if (contentPathSegment != null)
+ {
+ foreach (PathSubsegment subsegment in contentPathSegment.Subsegments)
+ {
+ PathLiteralSubsegment literalSubsegment = subsegment as PathLiteralSubsegment;
+ if (literalSubsegment != null)
+ {
+ // We only care about parameter subsegments, so skip this
+ continue;
+ }
+ else
+ {
+ PathParameterSubsegment parameterSubsegment = subsegment as PathParameterSubsegment;
+ if (parameterSubsegment != null)
+ {
+ if (!action(parameterSubsegment))
+ {
+ return false;
+ }
+ }
+ else
+ {
+ Contract.Assert(false, "Invalid path subsegment type");
+ }
+ }
+ }
+ }
+ else
+ {
+ Contract.Assert(false, "Invalid path segment type");
+ }
+ }
+ }
+
+ return true;
+ }
+
+ private static PathParameterSubsegment GetParameterSubsegment(IList<PathSegment> pathSegments, string parameterName)
+ {
+ PathParameterSubsegment foundParameterSubsegment = null;
+
+ ForEachParameter(pathSegments, delegate(PathParameterSubsegment parameterSubsegment)
+ {
+ if (String.Equals(parameterName, parameterSubsegment.ParameterName, StringComparison.OrdinalIgnoreCase))
+ {
+ foundParameterSubsegment = parameterSubsegment;
+ return false;
+ }
+ else
+ {
+ return true;
+ }
+ });
+
+ return foundParameterSubsegment;
+ }
+
+ private static bool IsParameterRequired(PathParameterSubsegment parameterSubsegment, HttpRouteValueDictionary defaultValues, out object defaultValue)
+ {
+ if (parameterSubsegment.IsCatchAll)
+ {
+ defaultValue = null;
+ return false;
+ }
+
+ return !defaultValues.TryGetValue(parameterSubsegment.ParameterName, out defaultValue);
+ }
+
+ private static bool IsRoutePartNonEmpty(object routePart)
+ {
+ string routePartString = routePart as string;
+ if (routePartString != null)
+ {
+ return routePartString.Length > 0;
+ }
+ return routePart != null;
+ }
+
+ public HttpRouteValueDictionary Match(string virtualPath, HttpRouteValueDictionary defaultValues)
+ {
+ IList<string> requestPathSegments = HttpRouteParser.SplitUriToPathSegmentStrings(virtualPath);
+
+ if (defaultValues == null)
+ {
+ defaultValues = new HttpRouteValueDictionary();
+ }
+
+ HttpRouteValueDictionary matchedValues = new HttpRouteValueDictionary();
+
+ // This flag gets set once all the data in the URI has been parsed through, but
+ // the route we're trying to match against still has more parts. At this point
+ // we'll only continue matching separator characters and parameters that have
+ // default values.
+ bool ranOutOfStuffToParse = false;
+
+ // This value gets set once we start processing a catchall parameter (if there is one
+ // at all). Once we set this value we consume all remaining parts of the URI into its
+ // parameter value.
+ bool usedCatchAllParameter = false;
+
+ for (int i = 0; i < _pathSegments.Count; i++)
+ {
+ PathSegment pathSegment = _pathSegments[i];
+
+ if (requestPathSegments.Count <= i)
+ {
+ ranOutOfStuffToParse = true;
+ }
+
+ string requestPathSegment = ranOutOfStuffToParse ? null : requestPathSegments[i];
+
+ if (pathSegment is PathSeparatorSegment)
+ {
+ if (ranOutOfStuffToParse)
+ {
+ // If we're trying to match a separator in the route but there's no more content, that's OK
+ }
+ else
+ {
+ if (!String.Equals(requestPathSegment, "/", StringComparison.Ordinal))
+ {
+ return null;
+ }
+ }
+ }
+ else
+ {
+ PathContentSegment contentPathSegment = pathSegment as PathContentSegment;
+ if (contentPathSegment != null)
+ {
+ if (contentPathSegment.IsCatchAll)
+ {
+ Contract.Assert(i == (_pathSegments.Count - 1), "If we're processing a catch-all, we should be on the last route segment.");
+ MatchCatchAll(contentPathSegment, requestPathSegments.Skip(i), defaultValues, matchedValues);
+ usedCatchAllParameter = true;
+ }
+ else
+ {
+ if (!MatchContentPathSegment(contentPathSegment, requestPathSegment, defaultValues, matchedValues))
+ {
+ return null;
+ }
+ }
+ }
+ else
+ {
+ Contract.Assert(false, "Invalid path segment type");
+ }
+ }
+ }
+
+ if (!usedCatchAllParameter)
+ {
+ if (_pathSegments.Count < requestPathSegments.Count)
+ {
+ // If we've already gone through all the parts defined in the route but the URI
+ // still contains more content, check that the remaining content is all separators.
+ for (int i = _pathSegments.Count; i < requestPathSegments.Count; i++)
+ {
+ if (!HttpRouteParser.IsSeparator(requestPathSegments[i]))
+ {
+ return null;
+ }
+ }
+ }
+ }
+
+ // Copy all remaining default values to the route data
+ if (defaultValues != null)
+ {
+ foreach (var defaultValue in defaultValues)
+ {
+ if (!matchedValues.ContainsKey(defaultValue.Key))
+ {
+ matchedValues.Add(defaultValue.Key, defaultValue.Value);
+ }
+ }
+ }
+
+ return matchedValues;
+ }
+
+ private static void MatchCatchAll(PathContentSegment contentPathSegment, IEnumerable<string> remainingRequestSegments, HttpRouteValueDictionary defaultValues, HttpRouteValueDictionary matchedValues)
+ {
+ string remainingRequest = String.Join(String.Empty, remainingRequestSegments.ToArray());
+
+ PathParameterSubsegment catchAllSegment = contentPathSegment.Subsegments[0] as PathParameterSubsegment;
+
+ object catchAllValue;
+
+ if (remainingRequest.Length > 0)
+ {
+ catchAllValue = remainingRequest;
+ }
+ else
+ {
+ defaultValues.TryGetValue(catchAllSegment.ParameterName, out catchAllValue);
+ }
+
+ matchedValues.Add(catchAllSegment.ParameterName, catchAllValue);
+ }
+
+ private static bool MatchContentPathSegment(PathContentSegment routeSegment, string requestPathSegment, HttpRouteValueDictionary defaultValues, HttpRouteValueDictionary matchedValues)
+ {
+ if (String.IsNullOrEmpty(requestPathSegment))
+ {
+ // If there's no data to parse, we must have exactly one parameter segment and no other segments - otherwise no match
+
+ if (routeSegment.Subsegments.Count > 1)
+ {
+ return false;
+ }
+
+ PathParameterSubsegment parameterSubsegment = routeSegment.Subsegments[0] as PathParameterSubsegment;
+ if (parameterSubsegment == null)
+ {
+ return false;
+ }
+
+ // We must have a default value since there's no value in the request URI
+ object parameterValue;
+ if (defaultValues.TryGetValue(parameterSubsegment.ParameterName, out parameterValue))
+ {
+ // If there's a default value for this parameter, use that default value
+ matchedValues.Add(parameterSubsegment.ParameterName, parameterValue);
+ return true;
+ }
+ else
+ {
+ // If there's no default value, this segment doesn't match
+ return false;
+ }
+ }
+
+ // Find last literal segment and get its last index in the string
+
+ int lastIndex = requestPathSegment.Length;
+ int indexOfLastSegmentUsed = routeSegment.Subsegments.Count - 1;
+
+ PathParameterSubsegment parameterNeedsValue = null; // Keeps track of a parameter segment that is pending a value
+ PathLiteralSubsegment lastLiteral = null; // Keeps track of the left-most literal we've encountered
+
+ while (indexOfLastSegmentUsed >= 0)
+ {
+ int newLastIndex = lastIndex;
+
+ PathParameterSubsegment parameterSubsegment = routeSegment.Subsegments[indexOfLastSegmentUsed] as PathParameterSubsegment;
+ if (parameterSubsegment != null)
+ {
+ // Hold on to the parameter so that we can fill it in when we locate the next literal
+ parameterNeedsValue = parameterSubsegment;
+ }
+ else
+ {
+ PathLiteralSubsegment literalSubsegment = routeSegment.Subsegments[indexOfLastSegmentUsed] as PathLiteralSubsegment;
+ if (literalSubsegment != null)
+ {
+ lastLiteral = literalSubsegment;
+
+ int startIndex = lastIndex - 1;
+ // If we have a pending parameter subsegment, we must leave at least one character for that
+ if (parameterNeedsValue != null)
+ {
+ startIndex--;
+ }
+
+ if (startIndex < 0)
+ {
+ return false;
+ }
+
+ int indexOfLiteral = requestPathSegment.LastIndexOf(literalSubsegment.Literal, startIndex, StringComparison.OrdinalIgnoreCase);
+ if (indexOfLiteral == -1)
+ {
+ // If we couldn't find this literal index, this segment cannot match
+ return false;
+ }
+
+ // If the first subsegment is a literal, it must match at the right-most extent of the request URI.
+ // Without this check if your route had "/Foo/" we'd match the request URI "/somethingFoo/".
+ // This check is related to the check we do at the very end of this function.
+ if (indexOfLastSegmentUsed == (routeSegment.Subsegments.Count - 1))
+ {
+ if ((indexOfLiteral + literalSubsegment.Literal.Length) != requestPathSegment.Length)
+ {
+ return false;
+ }
+ }
+
+ newLastIndex = indexOfLiteral;
+ }
+ else
+ {
+ Contract.Assert(false, "Invalid path segment type");
+ }
+ }
+
+ if ((parameterNeedsValue != null) && (((lastLiteral != null) && (parameterSubsegment == null)) || (indexOfLastSegmentUsed == 0)))
+ {
+ // If we have a pending parameter that needs a value, grab that value
+
+ int parameterStartIndex;
+ int parameterTextLength;
+
+ if (lastLiteral == null)
+ {
+ if (indexOfLastSegmentUsed == 0)
+ {
+ parameterStartIndex = 0;
+ }
+ else
+ {
+ parameterStartIndex = newLastIndex;
+ Contract.Assert(false, "indexOfLastSegementUsed should always be 0 from the check above");
+ }
+ parameterTextLength = lastIndex;
+ }
+ else
+ {
+ // If we're getting a value for a parameter that is somewhere in the middle of the segment
+ if ((indexOfLastSegmentUsed == 0) && (parameterSubsegment != null))
+ {
+ parameterStartIndex = 0;
+ parameterTextLength = lastIndex;
+ }
+ else
+ {
+ parameterStartIndex = newLastIndex + lastLiteral.Literal.Length;
+ parameterTextLength = lastIndex - parameterStartIndex;
+ }
+ }
+
+ string parameterValueString = requestPathSegment.Substring(parameterStartIndex, parameterTextLength);
+
+ if (String.IsNullOrEmpty(parameterValueString))
+ {
+ // If we're here that means we have a segment that contains multiple sub-segments.
+ // For these segments all parameters must have non-empty values. If the parameter
+ // has an empty value it's not a match.
+ return false;
+ }
+ else
+ {
+ // If there's a value in the segment for this parameter, use the subsegment value
+ matchedValues.Add(parameterNeedsValue.ParameterName, parameterValueString);
+ }
+
+ parameterNeedsValue = null;
+ lastLiteral = null;
+ }
+
+ lastIndex = newLastIndex;
+ indexOfLastSegmentUsed--;
+ }
+
+ // If the last subsegment is a parameter, it's OK that we didn't parse all the way to the left extent of
+ // the string since the parameter will have consumed all the remaining text anyway. If the last subsegment
+ // is a literal then we *must* have consumed the entire text in that literal. Otherwise we end up matching
+ // the route "Foo" to the request URI "somethingFoo". Thus we have to check that we parsed the *entire*
+ // request URI in order for it to be a match.
+ // This check is related to the check we do earlier in this function for LiteralSubsegments.
+ return (lastIndex == 0) || (routeSegment.Subsegments[0] is PathParameterSubsegment);
+ }
+
+ private static bool RoutePartsEqual(object a, object b)
+ {
+ string sa = a as string;
+ string sb = b as string;
+ if (sa != null && sb != null)
+ {
+ // For strings do a case-insensitive comparison
+ return String.Equals(sa, sb, StringComparison.OrdinalIgnoreCase);
+ }
+ else
+ {
+ if (a != null && b != null)
+ {
+ // Explicitly call .Equals() in case it is overridden in the type
+ return a.Equals(b);
+ }
+ else
+ {
+ // At least one of them is null. Return true if they both are
+ return a == b;
+ }
+ }
+ }
+
+ private static string UriEncode(string str)
+ {
+ string escape = Uri.EscapeUriString(str);
+ return Regex.Replace(escape, "([#?])", new MatchEvaluator(EscapeReservedCharacters));
+ }
+ }
+}
diff --git a/src/System.Web.Http/Routing/HttpRoute.cs b/src/System.Web.Http/Routing/HttpRoute.cs
new file mode 100644
index 00000000..5f901589
--- /dev/null
+++ b/src/System.Web.Http/Routing/HttpRoute.cs
@@ -0,0 +1,228 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Net.Http;
+using System.Text.RegularExpressions;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Internal;
+using System.Web.Http.Properties;
+
+namespace System.Web.Http.Routing
+{
+ /// <summary>
+ /// Route class for self-host (i.e. hosted outside of ASP.NET). This class is mostly the
+ /// same as the System.Web.Routing.Route implementation.
+ /// This class has the same URL matching functionality as System.Web.Routing.Route. However,
+ /// in order for this route to match when generating URLs, a special "httproute" key must be
+ /// specified when generating the URL.
+ /// </summary>
+ public class HttpRoute : IHttpRoute
+ {
+ /// <summary>
+ /// Key used to signify that a route URL generation request should include HTTP routes (e.g. Web API).
+ /// If this key is not specified then no HTTP routes will match.
+ /// </summary>
+ internal const string HttpRouteKey = "httproute";
+
+ private const string HttpMethodParameterName = "httpMethod";
+
+ private string _routeTemplate;
+ private HttpParsedRoute _parsedRoute;
+ private HttpRouteValueDictionary _defaults;
+ private HttpRouteValueDictionary _constraints;
+ private HttpRouteValueDictionary _dataTokens;
+
+ public HttpRoute()
+ : this(null, null, null, null)
+ {
+ }
+
+ public HttpRoute(string routeTemplate)
+ : this(routeTemplate, null, null, null)
+ {
+ }
+
+ public HttpRoute(string routeTemplate, HttpRouteValueDictionary defaults)
+ : this(routeTemplate, defaults, null, null)
+ {
+ }
+
+ public HttpRoute(string routeTemplate, HttpRouteValueDictionary defaults, HttpRouteValueDictionary constraints)
+ : this(routeTemplate, defaults, constraints, null)
+ {
+ }
+
+ public HttpRoute(string routeTemplate, HttpRouteValueDictionary defaults, HttpRouteValueDictionary constraints, HttpRouteValueDictionary dataTokens)
+ {
+ _routeTemplate = String.IsNullOrWhiteSpace(routeTemplate) ? String.Empty : routeTemplate;
+ _defaults = defaults ?? new HttpRouteValueDictionary();
+ _constraints = constraints ?? new HttpRouteValueDictionary();
+ _dataTokens = dataTokens ?? new HttpRouteValueDictionary();
+
+ // The parser will throw for invalid routes.
+ _parsedRoute = HttpRouteParser.Parse(_routeTemplate);
+ }
+
+ public IDictionary<string, object> Defaults
+ {
+ get { return _defaults; }
+ }
+
+ public IDictionary<string, object> Constraints
+ {
+ get { return _constraints; }
+ }
+
+ public IDictionary<string, object> DataTokens
+ {
+ get { return _dataTokens; }
+ }
+
+ public string RouteTemplate
+ {
+ get { return _routeTemplate; }
+ }
+
+ public virtual IHttpRouteData GetRouteData(string virtualPathRoot, HttpRequestMessage request)
+ {
+ if (virtualPathRoot == null)
+ {
+ throw Error.ArgumentNull("virtualPathRoot");
+ }
+
+ if (request == null)
+ {
+ throw Error.ArgumentNull("request");
+ }
+
+ // Note: we don't validate host/port as this is expected to be done at the host level
+ string requestPath = request.RequestUri.AbsolutePath;
+ if (!requestPath.StartsWith(virtualPathRoot, StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+
+ string relativeRequestPath = null;
+ int virtualPathLength = virtualPathRoot.Length;
+ if (requestPath.Length > virtualPathLength && requestPath[virtualPathLength] == '/')
+ {
+ relativeRequestPath = requestPath.Substring(virtualPathLength + 1);
+ }
+ else
+ {
+ relativeRequestPath = requestPath.Substring(virtualPathLength);
+ }
+
+ string decodedRelativeRequestPath = UriQueryUtility.UrlDecode(relativeRequestPath);
+ HttpRouteValueDictionary values = _parsedRoute.Match(decodedRelativeRequestPath, _defaults);
+ if (values == null)
+ {
+ // If we got back a null value set, that means the URI did not match
+ return null;
+ }
+
+ // Validate the values
+ if (!ProcessConstraints(request, values, HttpRouteDirection.UriResolution))
+ {
+ return null;
+ }
+
+ return new HttpRouteData(this, values);
+ }
+
+ /// <summary>
+ /// Attempt to generate a URI that represents the values passed in based on current
+ /// values from the <see cref="HttpRouteData"/> and new values using the specified <see cref="HttpRoute"/>.
+ /// </summary>
+ /// <param name="controllerContext">The HTTP execution context.</param>
+ /// <param name="values">The route values.</param>
+ /// <returns>A <see cref="HttpVirtualPathData"/> instance or null if URI cannot be generated.</returns>
+ public virtual IHttpVirtualPathData GetVirtualPath(HttpControllerContext controllerContext, IDictionary<string, object> values)
+ {
+ if (controllerContext == null)
+ {
+ throw Error.ArgumentNull("controllerContext");
+ }
+
+ // Only perform URL generation if the "httproute" key was specified. This allows these
+ // routes to be ignored when a regular MVC app tries to generate URLs. Without this special
+ // key an HTTP route used for Web API would normally take over almost all the routes in a
+ // typical app.
+ if (values != null && !values.Keys.Contains(HttpRouteKey, StringComparer.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+ // Remove the value from the collection so that it doesn't affect the generated URL
+ var newValues = GetRouteDictionaryWithoutHttpRouteKey(values);
+
+ BoundRouteTemplate result = _parsedRoute.Bind(controllerContext.RouteData.Values, newValues, _defaults, _constraints);
+ if (result == null)
+ {
+ return null;
+ }
+
+ // Verify that the route matches the validation rules
+ if (!ProcessConstraints(controllerContext.Request, result.Values, HttpRouteDirection.UriGeneration))
+ {
+ return null;
+ }
+
+ return new HttpVirtualPathData(this, result.BoundTemplate);
+ }
+
+ private static IDictionary<string, object> GetRouteDictionaryWithoutHttpRouteKey(IDictionary<string, object> routeValues)
+ {
+ var newRouteValues = new Dictionary<string, object>();
+ if (routeValues != null)
+ {
+ foreach (var routeValue in routeValues)
+ {
+ if (!String.Equals(routeValue.Key, HttpRouteKey, StringComparison.OrdinalIgnoreCase))
+ {
+ newRouteValues.Add(routeValue.Key, routeValue.Value);
+ }
+ }
+ }
+ return newRouteValues;
+ }
+
+ protected virtual bool ProcessConstraint(HttpRequestMessage request, object constraint, string parameterName, HttpRouteValueDictionary values, HttpRouteDirection routeDirection)
+ {
+ IHttpRouteConstraint customConstraint = constraint as IHttpRouteConstraint;
+ if (customConstraint != null)
+ {
+ return customConstraint.Match(request, this, parameterName, values, routeDirection);
+ }
+
+ // If there was no custom constraint, then treat the constraint as a string which represents a Regex.
+ string constraintsRule = constraint as string;
+ if (constraintsRule == null)
+ {
+ throw Error.InvalidOperation(SRResources.Route_ValidationMustBeStringOrCustomConstraint, parameterName, RouteTemplate, typeof(IHttpRouteConstraint).Name);
+ }
+
+ object parameterValue;
+ values.TryGetValue(parameterName, out parameterValue);
+ string parameterValueString = Convert.ToString(parameterValue, CultureInfo.InvariantCulture);
+ string constraintsRegEx = "^(" + constraintsRule + ")$";
+ return Regex.IsMatch(parameterValueString, constraintsRegEx, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
+ }
+
+ private bool ProcessConstraints(HttpRequestMessage request, HttpRouteValueDictionary values, HttpRouteDirection routeDirection)
+ {
+ if (Constraints != null)
+ {
+ foreach (KeyValuePair<string, object> constraintsItem in Constraints)
+ {
+ if (!ProcessConstraint(request, constraintsItem.Value, constraintsItem.Key, values, routeDirection))
+ {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/src/System.Web.Http/Routing/HttpRouteData.cs b/src/System.Web.Http/Routing/HttpRouteData.cs
new file mode 100644
index 00000000..2c7df78d
--- /dev/null
+++ b/src/System.Web.Http/Routing/HttpRouteData.cs
@@ -0,0 +1,42 @@
+using System.Collections.Generic;
+using System.Web.Http.Common;
+
+namespace System.Web.Http.Routing
+{
+ public class HttpRouteData : IHttpRouteData
+ {
+ private IHttpRoute _route;
+ private IDictionary<string, object> _values;
+
+ public HttpRouteData(IHttpRoute route)
+ : this(route, new HttpRouteValueDictionary())
+ {
+ }
+
+ public HttpRouteData(IHttpRoute route, HttpRouteValueDictionary values)
+ {
+ if (route == null)
+ {
+ throw Error.ArgumentNull("route");
+ }
+
+ if (values == null)
+ {
+ throw Error.ArgumentNull("values");
+ }
+
+ _route = route;
+ _values = values;
+ }
+
+ public IHttpRoute Route
+ {
+ get { return _route; }
+ }
+
+ public IDictionary<string, object> Values
+ {
+ get { return _values; }
+ }
+ }
+}
diff --git a/src/System.Web.Http/Routing/HttpRouteDirection.cs b/src/System.Web.Http/Routing/HttpRouteDirection.cs
new file mode 100644
index 00000000..65310876
--- /dev/null
+++ b/src/System.Web.Http/Routing/HttpRouteDirection.cs
@@ -0,0 +1,8 @@
+namespace System.Web.Http.Routing
+{
+ public enum HttpRouteDirection
+ {
+ UriResolution = 0,
+ UriGeneration
+ }
+}
diff --git a/src/System.Web.Http/Routing/HttpRouteParser.cs b/src/System.Web.Http/Routing/HttpRouteParser.cs
new file mode 100644
index 00000000..93fe6139
--- /dev/null
+++ b/src/System.Web.Http/Routing/HttpRouteParser.cs
@@ -0,0 +1,360 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+using System.Linq;
+using System.Web.Http.Common;
+using System.Web.Http.Properties;
+
+namespace System.Web.Http.Routing
+{
+ internal static class HttpRouteParser
+ {
+ private static string GetLiteral(string segmentLiteral)
+ {
+ // Scan for errant single { and } and convert double {{ to { and double }} to }
+
+ // First we eliminate all escaped braces and then check if any other braces are remaining
+ string newLiteral = segmentLiteral.Replace("{{", String.Empty).Replace("}}", String.Empty);
+ if (newLiteral.Contains("{") || newLiteral.Contains("}"))
+ {
+ return null;
+ }
+
+ // If it's a valid format, we unescape the braces
+ return segmentLiteral.Replace("{{", "{").Replace("}}", "}");
+ }
+
+ private static int IndexOfFirstOpenParameter(string segment, int startIndex)
+ {
+ // Find the first unescaped open brace
+ while (true)
+ {
+ startIndex = segment.IndexOf('{', startIndex);
+ if (startIndex == -1)
+ {
+ // If there are no more open braces, stop
+ return -1;
+ }
+ if ((startIndex + 1 == segment.Length) ||
+ ((startIndex + 1 < segment.Length) && (segment[startIndex + 1] != '{')))
+ {
+ // If we found an open brace that is followed by a non-open brace, it's
+ // a parameter delimiter.
+ // It's also a delimiter if the open brace is the last character - though
+ // it ends up being being called out as invalid later on.
+ return startIndex;
+ }
+ // Increment by two since we want to skip both the open brace that
+ // we're on as well as the subsequent character since we know for
+ // sure that it is part of an escape sequence.
+ startIndex += 2;
+ }
+ }
+
+ internal static bool IsSeparator(string s)
+ {
+ return String.Equals(s, "/", StringComparison.Ordinal);
+ }
+
+ private static bool IsValidParameterName(string parameterName)
+ {
+ if (parameterName.Length == 0)
+ {
+ return false;
+ }
+
+ for (int i = 0; i < parameterName.Length; i++)
+ {
+ char c = parameterName[i];
+ if (c == '/' || c == '{' || c == '}')
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ internal static bool IsInvalidRouteTemplate(string routeTemplate)
+ {
+ return routeTemplate.StartsWith("~", StringComparison.Ordinal) ||
+ routeTemplate.StartsWith("/", StringComparison.Ordinal) ||
+ (routeTemplate.IndexOf('?') != -1);
+ }
+
+ public static HttpParsedRoute Parse(string routeTemplate)
+ {
+ if (routeTemplate == null)
+ {
+ routeTemplate = String.Empty;
+ }
+
+ if (IsInvalidRouteTemplate(routeTemplate))
+ {
+ throw Error.Argument("routeTemplate", SRResources.Route_InvalidRouteTemplate);
+ }
+
+ IList<string> uriParts = SplitUriToPathSegmentStrings(routeTemplate);
+ Exception ex = ValidateUriParts(uriParts);
+ if (ex != null)
+ {
+ throw ex;
+ }
+
+ IList<PathSegment> pathSegments = SplitUriToPathSegments(uriParts);
+
+ Contract.Assert(uriParts.Count == pathSegments.Count, "The number of string segments should be the same as the number of path segments");
+
+ return new HttpParsedRoute(pathSegments);
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly",
+ Justification = "The exceptions are just constructed here, but they are thrown from a method that does have those parameter names.")]
+ private static IList<PathSubsegment> ParseUriSegment(string segment, out Exception exception)
+ {
+ int startIndex = 0;
+
+ List<PathSubsegment> pathSubsegments = new List<PathSubsegment>();
+
+ while (startIndex < segment.Length)
+ {
+ int nextParameterStart = IndexOfFirstOpenParameter(segment, startIndex);
+ if (nextParameterStart == -1)
+ {
+ // If there are no more parameters in the segment, capture the remainder as a literal and stop
+ string lastLiteralPart = GetLiteral(segment.Substring(startIndex));
+ if (lastLiteralPart == null)
+ {
+ exception = Error.Argument("routeTemplate", SRResources.Route_MismatchedParameter, segment);
+ return null;
+ }
+
+ if (lastLiteralPart.Length > 0)
+ {
+ pathSubsegments.Add(new PathLiteralSubsegment(lastLiteralPart));
+ }
+ break;
+ }
+
+ int nextParameterEnd = segment.IndexOf('}', nextParameterStart + 1);
+ if (nextParameterEnd == -1)
+ {
+ exception = Error.Argument("routeTemplate", SRResources.Route_MismatchedParameter, segment);
+ return null;
+ }
+
+ string literalPart = GetLiteral(segment.Substring(startIndex, nextParameterStart - startIndex));
+ if (literalPart == null)
+ {
+ exception = Error.Argument("routeTemplate", SRResources.Route_MismatchedParameter, segment);
+ return null;
+ }
+
+ if (literalPart.Length > 0)
+ {
+ pathSubsegments.Add(new PathLiteralSubsegment(literalPart));
+ }
+
+ string parameterName = segment.Substring(nextParameterStart + 1, nextParameterEnd - nextParameterStart - 1);
+ pathSubsegments.Add(new PathParameterSubsegment(parameterName));
+
+ startIndex = nextParameterEnd + 1;
+ }
+
+ exception = null;
+ return pathSubsegments;
+ }
+
+ private static IList<PathSegment> SplitUriToPathSegments(IList<string> uriParts)
+ {
+ List<PathSegment> pathSegments = new List<PathSegment>();
+
+ foreach (string pathSegment in uriParts)
+ {
+ bool isCurrentPartSeparator = IsSeparator(pathSegment);
+ if (isCurrentPartSeparator)
+ {
+ pathSegments.Add(new PathSeparatorSegment());
+ }
+ else
+ {
+ Exception exception;
+ IList<PathSubsegment> subsegments = ParseUriSegment(pathSegment, out exception);
+ Contract.Assert(exception == null, "This only gets called after the path has been validated, so there should never be an exception here");
+ pathSegments.Add(new PathContentSegment(subsegments));
+ }
+ }
+ return pathSegments;
+ }
+
+ internal static IList<string> SplitUriToPathSegmentStrings(string uri)
+ {
+ List<string> parts = new List<string>();
+
+ if (String.IsNullOrEmpty(uri))
+ {
+ return parts;
+ }
+
+ int currentIndex = 0;
+
+ // Split the incoming URI into individual parts
+ while (currentIndex < uri.Length)
+ {
+ int indexOfNextSeparator = uri.IndexOf('/', currentIndex);
+ if (indexOfNextSeparator == -1)
+ {
+ // If there are no more separators, the rest of the string is the last part
+ string finalPart = uri.Substring(currentIndex);
+ if (finalPart.Length > 0)
+ {
+ parts.Add(finalPart);
+ }
+ break;
+ }
+
+ string nextPart = uri.Substring(currentIndex, indexOfNextSeparator - currentIndex);
+ if (nextPart.Length > 0)
+ {
+ parts.Add(nextPart);
+ }
+
+ Contract.Assert(uri[indexOfNextSeparator] == '/', "The separator char itself should always be a '/'.");
+ parts.Add("/");
+ currentIndex = indexOfNextSeparator + 1;
+ }
+
+ return parts;
+ }
+
+ [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Not changing original algorithm")]
+ [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly",
+ Justification = "The exceptions are just constructed here, but they are thrown from a method that does have those parameter names.")]
+ private static Exception ValidateUriParts(IList<string> pathSegments)
+ {
+ Contract.Assert(pathSegments != null, "The value should always come from SplitUri(), and that function should never return null.");
+
+ HashSet<string> usedParameterNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+ bool? isPreviousPartSeparator = null;
+
+ bool foundCatchAllParameter = false;
+
+ foreach (string pathSegment in pathSegments)
+ {
+ if (foundCatchAllParameter)
+ {
+ // If we ever start an iteration of the loop and we've already found a
+ // catchall parameter then we have an invalid URI format.
+ return Error.Argument("routeTemplate", SRResources.Route_CatchAllMustBeLast, "routeTemplate");
+ }
+
+ bool isCurrentPartSeparator;
+ if (isPreviousPartSeparator == null)
+ {
+ // Prime the loop with the first value
+ isPreviousPartSeparator = IsSeparator(pathSegment);
+ isCurrentPartSeparator = isPreviousPartSeparator.Value;
+ }
+ else
+ {
+ isCurrentPartSeparator = IsSeparator(pathSegment);
+
+ // If both the previous part and the current part are separators, it's invalid
+ if (isCurrentPartSeparator && isPreviousPartSeparator.Value)
+ {
+ return Error.Argument("routeTemplate", SRResources.Route_CannotHaveConsecutiveSeparators);
+ }
+
+ Contract.Assert(isCurrentPartSeparator != isPreviousPartSeparator.Value, "This assert should only happen if both the current and previous parts are non-separators. This should never happen because consecutive non-separators are always parsed as a single part.");
+ isPreviousPartSeparator = isCurrentPartSeparator;
+ }
+
+ // If it's not a separator, parse the segment for parameters and validate it
+ if (!isCurrentPartSeparator)
+ {
+ Exception exception;
+ IList<PathSubsegment> subsegments = ParseUriSegment(pathSegment, out exception);
+ if (exception != null)
+ {
+ return exception;
+ }
+
+ exception = ValidateUriSegment(subsegments, usedParameterNames);
+ if (exception != null)
+ {
+ return exception;
+ }
+
+ foundCatchAllParameter = subsegments.Any<PathSubsegment>(seg => (seg is PathParameterSubsegment) && ((PathParameterSubsegment)seg).IsCatchAll);
+ }
+ }
+ return null;
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly",
+ Justification = "The exceptions are just constructed here, but they are thrown from a method that does have those parameter names.")]
+ private static Exception ValidateUriSegment(IList<PathSubsegment> pathSubsegments, HashSet<string> usedParameterNames)
+ {
+ bool segmentContainsCatchAll = false;
+
+ Type previousSegmentType = null;
+
+ foreach (PathSubsegment subsegment in pathSubsegments)
+ {
+ if (previousSegmentType != null)
+ {
+ if (previousSegmentType == subsegment.GetType())
+ {
+ return Error.Argument("routeTemplate", SRResources.Route_CannotHaveConsecutiveParameters);
+ }
+ }
+ previousSegmentType = subsegment.GetType();
+
+ PathLiteralSubsegment literalSubsegment = subsegment as PathLiteralSubsegment;
+ if (literalSubsegment != null)
+ {
+ // Nothing to validate for literals - everything is valid
+ }
+ else
+ {
+ PathParameterSubsegment parameterSubsegment = subsegment as PathParameterSubsegment;
+ if (parameterSubsegment != null)
+ {
+ string parameterName = parameterSubsegment.ParameterName;
+
+ if (parameterSubsegment.IsCatchAll)
+ {
+ segmentContainsCatchAll = true;
+ }
+
+ // Check for valid characters in the parameter name
+ if (!IsValidParameterName(parameterName))
+ {
+ return Error.Argument("routeTemplate", SRResources.Route_InvalidParameterName, parameterName);
+ }
+
+ if (usedParameterNames.Contains(parameterName))
+ {
+ return Error.Argument("routeTemplate", SRResources.Route_RepeatedParameter, parameterName);
+ }
+ else
+ {
+ usedParameterNames.Add(parameterName);
+ }
+ }
+ else
+ {
+ Contract.Assert(false, "Invalid path subsegment type");
+ }
+ }
+ }
+
+ if (segmentContainsCatchAll && (pathSubsegments.Count != 1))
+ {
+ return Error.Argument("routeTemplate", SRResources.Route_CannotHaveCatchAllInMultiSegment);
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/System.Web.Http/Routing/HttpRouteValueDictionary.cs b/src/System.Web.Http/Routing/HttpRouteValueDictionary.cs
new file mode 100644
index 00000000..66e88b53
--- /dev/null
+++ b/src/System.Web.Http/Routing/HttpRouteValueDictionary.cs
@@ -0,0 +1,37 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Web.Http.Common;
+
+namespace System.Web.Http.Routing
+{
+ [SuppressMessage("Microsoft.Usage", "CA2237:MarkISerializableTypesWithSerializable", Justification = "This class will never be serialized.")]
+ public class HttpRouteValueDictionary : Dictionary<string, object>
+ {
+ public HttpRouteValueDictionary()
+ : base(StringComparer.OrdinalIgnoreCase)
+ {
+ }
+
+ public HttpRouteValueDictionary(IDictionary<string, object> dictionary)
+ : base(dictionary, StringComparer.OrdinalIgnoreCase)
+ {
+ }
+
+ public HttpRouteValueDictionary(object values)
+ : base(StringComparer.OrdinalIgnoreCase)
+ {
+ if (values == null)
+ {
+ throw Error.ArgumentNull("values");
+ }
+
+ PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(values);
+ foreach (PropertyDescriptor prop in properties)
+ {
+ object val = prop.GetValue(values);
+ Add(prop.Name, val);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/Routing/HttpVirtualPathData.cs b/src/System.Web.Http/Routing/HttpVirtualPathData.cs
new file mode 100644
index 00000000..86b444ef
--- /dev/null
+++ b/src/System.Web.Http/Routing/HttpVirtualPathData.cs
@@ -0,0 +1,27 @@
+using System.Web.Http.Common;
+
+namespace System.Web.Http.Routing
+{
+ public class HttpVirtualPathData : IHttpVirtualPathData
+ {
+ public HttpVirtualPathData(IHttpRoute route, string virtualPath)
+ {
+ if (route == null)
+ {
+ throw Error.ArgumentNull("route");
+ }
+
+ if (virtualPath == null)
+ {
+ throw Error.ArgumentNull("virtualPath");
+ }
+
+ Route = route;
+ VirtualPath = virtualPath;
+ }
+
+ public IHttpRoute Route { get; private set; }
+
+ public string VirtualPath { get; private set; }
+ }
+}
diff --git a/src/System.Web.Http/Routing/IHttpRoute.cs b/src/System.Web.Http/Routing/IHttpRoute.cs
new file mode 100644
index 00000000..938b02b5
--- /dev/null
+++ b/src/System.Web.Http/Routing/IHttpRoute.cs
@@ -0,0 +1,49 @@
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Web.Http.Controllers;
+
+namespace System.Web.Http.Routing
+{
+ /// <summary>
+ /// <see cref="IHttpRoute"/> defines the interface for a route expressing how to map an incoming <see cref="HttpRequestMessage"/> to a particular controller
+ /// and action.
+ /// </summary>
+ public interface IHttpRoute
+ {
+ /// <summary>
+ /// Gets the route template describing the URI pattern to match against.
+ /// </summary>
+ string RouteTemplate { get; }
+
+ /// <summary>
+ /// Gets the default values for route parameters if not provided by the incoming <see cref="HttpRequestMessage"/>.
+ /// </summary>
+ IDictionary<string, object> Defaults { get; }
+
+ /// <summary>
+ /// Gets the constraints for the route parameters.
+ /// </summary>
+ IDictionary<string, object> Constraints { get; }
+
+ /// <summary>
+ /// Gets any additional data tokens not used directly to determine whether a route matches an incoming <see cref="HttpRequestMessage"/>.
+ /// </summary>
+ IDictionary<string, object> DataTokens { get; }
+
+ /// <summary>
+ /// Determine whether this route is a match for the incoming request by looking up the <see cref="IHttpRouteData"/> for the route.
+ /// </summary>
+ /// <param name="virtualPathRoot">The virtual path root.</param>
+ /// <param name="request">The request.</param>
+ /// <returns>The <see cref="IHttpRouteData"/> for a route if matches; otherwise <c>null</c>.</returns>
+ IHttpRouteData GetRouteData(string virtualPathRoot, HttpRequestMessage request);
+
+ /// <summary>
+ /// Compute a URI based on the route and the values provided.
+ /// </summary>
+ /// <param name="controllerContext">The controller context.</param>
+ /// <param name="values">The values.</param>
+ /// <returns></returns>
+ IHttpVirtualPathData GetVirtualPath(HttpControllerContext controllerContext, IDictionary<string, object> values);
+ }
+}
diff --git a/src/System.Web.Http/Routing/IHttpRouteConstraint.cs b/src/System.Web.Http/Routing/IHttpRouteConstraint.cs
new file mode 100644
index 00000000..880e18d1
--- /dev/null
+++ b/src/System.Web.Http/Routing/IHttpRouteConstraint.cs
@@ -0,0 +1,10 @@
+using System.Collections.Generic;
+using System.Net.Http;
+
+namespace System.Web.Http.Routing
+{
+ public interface IHttpRouteConstraint
+ {
+ bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values, HttpRouteDirection routeDirection);
+ }
+}
diff --git a/src/System.Web.Http/Routing/IHttpRouteData.cs b/src/System.Web.Http/Routing/IHttpRouteData.cs
new file mode 100644
index 00000000..01db71e5
--- /dev/null
+++ b/src/System.Web.Http/Routing/IHttpRouteData.cs
@@ -0,0 +1,11 @@
+using System.Collections.Generic;
+
+namespace System.Web.Http.Routing
+{
+ public interface IHttpRouteData
+ {
+ IHttpRoute Route { get; }
+
+ IDictionary<string, object> Values { get; }
+ }
+}
diff --git a/src/System.Web.Http/Routing/IHttpVirtualPathData.cs b/src/System.Web.Http/Routing/IHttpVirtualPathData.cs
new file mode 100644
index 00000000..5ddcbd1d
--- /dev/null
+++ b/src/System.Web.Http/Routing/IHttpVirtualPathData.cs
@@ -0,0 +1,9 @@
+namespace System.Web.Http.Routing
+{
+ public interface IHttpVirtualPathData
+ {
+ IHttpRoute Route { get; }
+
+ string VirtualPath { get; }
+ }
+}
diff --git a/src/System.Web.Http/Routing/PathContentSegment.cs b/src/System.Web.Http/Routing/PathContentSegment.cs
new file mode 100644
index 00000000..e6632540
--- /dev/null
+++ b/src/System.Web.Http/Routing/PathContentSegment.cs
@@ -0,0 +1,52 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+
+namespace System.Web.Http.Routing
+{
+ // Represents a segment of a URI that is not a separator. It contains subsegments such as literals and parameters.
+ internal sealed class PathContentSegment : PathSegment
+ {
+ public PathContentSegment(IList<PathSubsegment> subsegments)
+ {
+ Subsegments = subsegments;
+ }
+
+ [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Not changing original algorithm.")]
+ public bool IsCatchAll
+ {
+ get
+ {
+ // TODO: Verify this is correct. Maybe add an assert.
+ return Subsegments.Any<PathSubsegment>(seg => (seg is PathParameterSubsegment) && ((PathParameterSubsegment)seg).IsCatchAll);
+ }
+ }
+
+ public IList<PathSubsegment> Subsegments { get; private set; }
+
+#if ROUTE_DEBUGGING
+ public override string LiteralText
+ {
+ get
+ {
+ List<string> s = new List<string>();
+ foreach (PathSubsegment subsegment in Subsegments)
+ {
+ s.Add(subsegment.LiteralText);
+ }
+ return String.Join(String.Empty, s.ToArray());
+ }
+ }
+
+ public override string ToString()
+ {
+ List<string> s = new List<string>();
+ foreach (PathSubsegment subsegment in Subsegments)
+ {
+ s.Add(subsegment.ToString());
+ }
+ return "[ " + String.Join(", ", s.ToArray()) + " ]";
+ }
+#endif
+ }
+}
diff --git a/src/System.Web.Http/Routing/PathLiteralSubsegment.cs b/src/System.Web.Http/Routing/PathLiteralSubsegment.cs
new file mode 100644
index 00000000..39bdc426
--- /dev/null
+++ b/src/System.Web.Http/Routing/PathLiteralSubsegment.cs
@@ -0,0 +1,28 @@
+namespace System.Web.Http.Routing
+{
+ // Represents a literal subsegment of a ContentPathSegment
+ internal sealed class PathLiteralSubsegment : PathSubsegment
+ {
+ public PathLiteralSubsegment(string literal)
+ {
+ Literal = literal;
+ }
+
+ public string Literal { get; private set; }
+
+#if ROUTE_DEBUGGING
+ public override string LiteralText
+ {
+ get
+ {
+ return Literal;
+ }
+ }
+
+ public override string ToString()
+ {
+ return "\"" + Literal + "\"";
+ }
+#endif
+ }
+}
diff --git a/src/System.Web.Http/Routing/PathParameterSubsegment.cs b/src/System.Web.Http/Routing/PathParameterSubsegment.cs
new file mode 100644
index 00000000..91b179ff
--- /dev/null
+++ b/src/System.Web.Http/Routing/PathParameterSubsegment.cs
@@ -0,0 +1,38 @@
+namespace System.Web.Http.Routing
+{
+ // Represents a parameter subsegment of a ContentPathSegment
+ internal sealed class PathParameterSubsegment : PathSubsegment
+ {
+ public PathParameterSubsegment(string parameterName)
+ {
+ if (parameterName.StartsWith("*", StringComparison.Ordinal))
+ {
+ ParameterName = parameterName.Substring(1);
+ IsCatchAll = true;
+ }
+ else
+ {
+ ParameterName = parameterName;
+ }
+ }
+
+ public bool IsCatchAll { get; private set; }
+
+ public string ParameterName { get; private set; }
+
+#if ROUTE_DEBUGGING
+ public override string LiteralText
+ {
+ get
+ {
+ return "{" + (IsCatchAll ? "*" : String.Empty) + ParameterName + "}";
+ }
+ }
+
+ public override string ToString()
+ {
+ return "{" + (IsCatchAll ? "*" : String.Empty) + ParameterName + "}";
+ }
+#endif
+ }
+}
diff --git a/src/System.Web.Http/Routing/PathSegment.cs b/src/System.Web.Http/Routing/PathSegment.cs
new file mode 100644
index 00000000..89065e6c
--- /dev/null
+++ b/src/System.Web.Http/Routing/PathSegment.cs
@@ -0,0 +1,13 @@
+namespace System.Web.Http.Routing
+{
+ // Represents a segment of a URI such as a separator or content
+ internal abstract class PathSegment
+ {
+#if ROUTE_DEBUGGING
+ public abstract string LiteralText
+ {
+ get;
+ }
+#endif
+ }
+}
diff --git a/src/System.Web.Http/Routing/PathSeparatorSegment.cs b/src/System.Web.Http/Routing/PathSeparatorSegment.cs
new file mode 100644
index 00000000..133989d3
--- /dev/null
+++ b/src/System.Web.Http/Routing/PathSeparatorSegment.cs
@@ -0,0 +1,21 @@
+namespace System.Web.Http.Routing
+{
+ // Represents a "/" separator in a URI
+ internal sealed class PathSeparatorSegment : PathSegment
+ {
+#if ROUTE_DEBUGGING
+ public override string LiteralText
+ {
+ get
+ {
+ return "/";
+ }
+ }
+
+ public override string ToString()
+ {
+ return "\"/\"";
+ }
+#endif
+ }
+}
diff --git a/src/System.Web.Http/Routing/PathSubsegment.cs b/src/System.Web.Http/Routing/PathSubsegment.cs
new file mode 100644
index 00000000..eac4593c
--- /dev/null
+++ b/src/System.Web.Http/Routing/PathSubsegment.cs
@@ -0,0 +1,13 @@
+namespace System.Web.Http.Routing
+{
+ // Represents a subsegment of a ContentPathSegment such as a parameter or a literal.
+ internal abstract class PathSubsegment
+ {
+#if ROUTE_DEBUGGING
+ public abstract string LiteralText
+ {
+ get;
+ }
+#endif
+ }
+}
diff --git a/src/System.Web.Http/Routing/UrlHelper.cs b/src/System.Web.Http/Routing/UrlHelper.cs
new file mode 100644
index 00000000..6e3b88af
--- /dev/null
+++ b/src/System.Web.Http/Routing/UrlHelper.cs
@@ -0,0 +1,86 @@
+using System.Collections.Generic;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+
+namespace System.Web.Http.Routing
+{
+ public class UrlHelper
+ {
+ private HttpControllerContext _controllerContext;
+
+ public UrlHelper(HttpControllerContext controllerContext)
+ {
+ if (controllerContext == null)
+ {
+ throw Error.ArgumentNull("controllerContext");
+ }
+
+ ControllerContext = controllerContext;
+ }
+
+ /// <summary>
+ /// Gets the <see cref="HttpControllerContext"/> of the current <see cref="ApiController"/>.
+ /// The setter is not intended to be used other than for unit testing purpose.
+ /// </summary>
+ public HttpControllerContext ControllerContext
+ {
+ get { return _controllerContext; }
+ set
+ {
+ if (value == null)
+ {
+ throw Error.ArgumentNull("value");
+ }
+
+ _controllerContext = value;
+ }
+ }
+
+ public string Route(string routeName, object routeValues)
+ {
+ return GetHttpRouteHelper(ControllerContext, routeName, routeValues);
+ }
+
+ public string Route(string routeName, IDictionary<string, object> routeValues)
+ {
+ return GetHttpRouteHelper(ControllerContext, routeName, routeValues);
+ }
+
+ private static string GetHttpRouteHelper(HttpControllerContext controllerContext, string routeName, object routeValues)
+ {
+ IDictionary<string, object> routeValuesDictionary = HttpRouteCollection.GetTypeProperties(routeValues);
+ return GetHttpRouteHelper(controllerContext, routeName, routeValuesDictionary);
+ }
+
+ private static string GetHttpRouteHelper(HttpControllerContext controllerContext, string routeName, IDictionary<string, object> routeValues)
+ {
+ if (routeValues == null)
+ {
+ // If no route values were passed in at all we have to create a new dictionary
+ // so that we can add the extra "httproute" key.
+ routeValues = new Dictionary<string, object>();
+ routeValues.Add(HttpRoute.HttpRouteKey, true);
+ }
+ else
+ {
+ if (!routeValues.ContainsKey(HttpRoute.HttpRouteKey))
+ {
+ // Copy the dictionary so that we can add the extra "httproute" key used by all Web API routes to
+ // disambiguate them from other MVC routes.
+ routeValues = new Dictionary<string, object>(routeValues);
+ routeValues.Add(HttpRoute.HttpRouteKey, true);
+ }
+ }
+
+ IHttpVirtualPathData vpd = controllerContext.Configuration.Routes.GetVirtualPath(
+ controllerContext: controllerContext,
+ name: routeName,
+ values: routeValues);
+ if (vpd == null)
+ {
+ return null;
+ }
+ return vpd.VirtualPath;
+ }
+ }
+}
diff --git a/src/System.Web.Http/Services/DefaultServiceResolver.cs b/src/System.Web.Http/Services/DefaultServiceResolver.cs
new file mode 100644
index 00000000..bfc0bbd3
--- /dev/null
+++ b/src/System.Web.Http/Services/DefaultServiceResolver.cs
@@ -0,0 +1,154 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
+using System.Linq;
+using System.Net.Http.Formatting;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Description;
+using System.Web.Http.Dispatcher;
+using System.Web.Http.Filters;
+using System.Web.Http.Metadata;
+using System.Web.Http.Metadata.Providers;
+using System.Web.Http.ModelBinding;
+using System.Web.Http.ModelBinding.Binders;
+using System.Web.Http.Tracing;
+using System.Web.Http.Validation;
+using System.Web.Http.Validation.Providers;
+using System.Web.Http.ValueProviders;
+using System.Web.Http.ValueProviders.Providers;
+
+namespace System.Web.Http.Services
+{
+ /// <summary>
+ /// This class is what <see cref="DependencyResolver"/> will ultimately fall back to.
+ /// It handles built-in dependencies that the runtime expects to be present.
+ /// </summary>
+ internal class DefaultServiceResolver : IDependencyResolver
+ {
+ private readonly IDictionary<Type, object[]> _services = new Dictionary<Type, object[]>();
+
+ // Use activator function instead of just object so that we can avoid eagerly calling the constructor.
+ // This is especially important when the constructors will in turn ask for other objects. In that case, eagerly calling
+ // ctors can result in infinite recursion.
+ private readonly IDictionary<Type, Func<HttpConfiguration, object>> _deferredService = new Dictionary<Type, Func<HttpConfiguration, object>>();
+
+ private readonly HttpConfiguration _configuration;
+
+ [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Class needs references to large number of types.")]
+ public DefaultServiceResolver(HttpConfiguration configuration)
+ {
+ if (configuration == null)
+ {
+ throw Error.ArgumentNull("configuration");
+ }
+
+ _configuration = configuration;
+
+ Add<IBuildManager>(new DefaultBuildManager());
+
+ Add<IHttpControllerFactory>(config => new DefaultHttpControllerFactory(config));
+
+ Add<IHttpControllerActivator>(config => new DefaultHttpControllerActivator(config));
+
+ Add<IHttpActionSelector>(config => new ApiControllerActionSelector());
+
+ Add<IHttpActionInvoker>(config => new ApiControllerActionInvoker());
+
+ Add<ModelMetadataProvider>(new EmptyModelMetadataProvider());
+
+ Add<IContentNegotiator>(new DefaultContentNegotiator());
+
+ Add<IActionValueBinder>(new DefaultActionValueBinder());
+
+ Add<IApiExplorer>(new ApiExplorer(configuration));
+
+ Add<IBodyModelValidator>(new DefaultBodyModelValidator());
+
+ AddRange<IFilterProvider>(
+ new ConfigurationFilterProvider(),
+ new ActionDescriptorFilterProvider(),
+ new EnumerableEvaluatorFilterProvider(),
+ new QueryCompositionFilterProvider());
+
+ AddRange<ModelBinderProvider>(
+ new TypeMatchModelBinderProvider(),
+ new BinaryDataModelBinderProvider(),
+ new KeyValuePairModelBinderProvider(),
+ new ComplexModelDtoModelBinderProvider(),
+ new ArrayModelBinderProvider(),
+ new DictionaryModelBinderProvider(),
+ new CollectionModelBinderProvider(),
+ new TypeConverterModelBinderProvider(),
+ new MutableObjectModelBinderProvider());
+
+ AddRange<ModelValidatorProvider>(
+ new DataAnnotationsModelValidatorProvider(),
+ new ClientDataTypeModelValidatorProvider(),
+ new DataMemberModelValidatorProvider());
+
+ AddRange<ValueProviderFactory>(
+ //new FormValueProviderFactory(),
+ //new JsonValueProviderFactory(),
+ new RouteDataValueProviderFactory(),
+ new QueryStringValueProviderFactory());
+
+ Add<ModelMetadataProvider>(new CachedDataAnnotationsModelMetadataProvider());
+
+ Add<ITraceManager>(new TraceManager());
+ }
+
+ // activator creates the instance of TInterface
+ private void Add<TInterface>(Func<HttpConfiguration, object> activator)
+ {
+ Type type = typeof(TInterface);
+ Contract.Assert(type.IsInterface || type.IsAbstract);
+
+ _deferredService[typeof(TInterface)] = activator;
+ }
+
+ // singleton for all requests for TInterface
+ private void Add<TInterface>(TInterface singleton)
+ {
+ Add<TInterface>(_ => singleton); // just return existing instance
+ }
+
+ private void AddRange<T>(params object[] services)
+ {
+ // We'd like this to be T[] services, but you can't cast from T[] to object[].
+ _services[typeof(T)] = services;
+ }
+
+ public object GetService(Type t)
+ {
+ Contract.Assert(t != null);
+
+ Func<HttpConfiguration, object> activator;
+ if (_deferredService.TryGetValue(t, out activator))
+ {
+ return activator(_configuration);
+ }
+
+ IEnumerable<object> results = GetServices(t);
+ if (results == null)
+ {
+ return null;
+ }
+
+ return results.FirstOrDefault();
+ }
+
+ public IEnumerable<object> GetServices(Type t)
+ {
+ Contract.Assert(t != null);
+
+ object[] services;
+ if (!_services.TryGetValue(t, out services))
+ {
+ return Enumerable.Empty<object>();
+ }
+
+ return services;
+ }
+ }
+}
diff --git a/src/System.Web.Http/Services/DependencyResolver.cs b/src/System.Web.Http/Services/DependencyResolver.cs
new file mode 100644
index 00000000..49a68173
--- /dev/null
+++ b/src/System.Web.Http/Services/DependencyResolver.cs
@@ -0,0 +1,255 @@
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Reflection;
+using System.Web.Http.Common;
+using System.Web.Http.Properties;
+
+namespace System.Web.Http.Services
+{
+ public class DependencyResolver
+ {
+ // Allows user to specifically override certain resolution types, without needing to create a new DependencyResolver
+ private ConcurrentDictionary<Type, object[]> _overrides = new ConcurrentDictionary<Type, object[]>();
+
+ // Provides default objects for certain MVC types.
+ private IDependencyResolver _defaultServiceBuiltInResolver;
+
+ // The user supplied resolver. We provide a default implementation in case its null.
+ private IDependencyResolver _userResolver;
+
+ // Cache should always be a new CacheDependencyResolver(_current).
+ private ConcurrentDictionary<Type, object> _cacheSingle = new ConcurrentDictionary<Type, object>();
+ private ConcurrentDictionary<Type, IEnumerable<object>> _cacheMultiple = new ConcurrentDictionary<Type, IEnumerable<object>>();
+
+ public DependencyResolver(HttpConfiguration configuration)
+ : this(configuration, new DefaultServiceResolver(configuration))
+ {
+ }
+
+ public DependencyResolver(HttpConfiguration configuration, IDependencyResolver defaultServiceResolver)
+ {
+ if (configuration == null)
+ {
+ throw Error.ArgumentNull("configuration");
+ }
+
+ SetResolver(new EmptyDependencyResolver());
+
+ _defaultServiceBuiltInResolver = defaultServiceResolver ?? new EmptyDependencyResolver();
+ }
+
+ public void SetService(Type serviceType, object value)
+ {
+ if (serviceType == null)
+ {
+ throw Error.ArgumentNull("serviceType");
+ }
+
+ if (value == null)
+ {
+ object[] ignore;
+ _overrides.TryRemove(serviceType, out ignore);
+ return;
+ }
+
+ SetServices(serviceType, new object[] { value });
+ }
+
+ public void SetServices(Type serviceType, params object[] values)
+ {
+ if (serviceType == null)
+ {
+ throw Error.ArgumentNull("serviceType");
+ }
+
+ _overrides[serviceType] = values;
+ }
+
+ protected object GetCachedService(Type serviceType)
+ {
+ if (serviceType == null)
+ {
+ throw Error.ArgumentNull("serviceType");
+ }
+
+ return _cacheSingle.GetOrAdd(serviceType, (x) => GetService(x));
+ }
+
+ protected IEnumerable<object> GetCachedServices(Type serviceType)
+ {
+ if (serviceType == null)
+ {
+ throw Error.ArgumentNull("serviceType");
+ }
+
+ return _cacheMultiple.GetOrAdd(serviceType, (x) => GetServices(x));
+ }
+
+ public object GetService(Type serviceType)
+ {
+ if (serviceType == null)
+ {
+ throw Error.ArgumentNull("serviceType");
+ }
+
+ object result;
+
+ // First lookup in overrides
+ object[] results;
+ if (_overrides.TryGetValue(serviceType, out results))
+ {
+ if ((results != null) && (results.Length > 0))
+ {
+ return results[0];
+ }
+ }
+
+ // Then ask user
+ result = _userResolver.GetService(serviceType);
+ if (result != null)
+ {
+ return result;
+ }
+
+ // Then try defaults
+ result = _defaultServiceBuiltInResolver.GetService(serviceType);
+ return result;
+ }
+
+ public IEnumerable<object> GetServices(Type serviceType)
+ {
+ if (serviceType == null)
+ {
+ throw Error.ArgumentNull("serviceType");
+ }
+
+ IEnumerable<object> result;
+
+ // First lookup override
+ object[] results;
+ if (_overrides.TryGetValue(serviceType, out results))
+ {
+ if (results != null)
+ {
+ return results;
+ }
+ }
+
+ // Then ask user
+ result = _userResolver.GetServices(serviceType);
+ if (result.FirstOrDefault() != null)
+ {
+ return result;
+ }
+
+ // Then try defaults
+ result = _defaultServiceBuiltInResolver.GetServices(serviceType);
+ return result;
+ }
+
+ public void SetResolver(IDependencyResolver resolver)
+ {
+ if (resolver == null)
+ {
+ throw Error.ArgumentNull("resolver");
+ }
+
+ _userResolver = resolver;
+ ResetCache();
+ }
+
+ public void SetResolver(object commonServiceLocator)
+ {
+ if (commonServiceLocator == null)
+ {
+ throw Error.ArgumentNull("commonServiceLocator");
+ }
+
+ Type locatorType = commonServiceLocator.GetType();
+ MethodInfo getInstance = locatorType.GetMethod("GetInstance", new[] { typeof(Type) });
+ MethodInfo getInstances = locatorType.GetMethod("GetAllInstances", new[] { typeof(Type) });
+
+ if (getInstance == null ||
+ getInstance.ReturnType != typeof(object) ||
+ getInstances == null ||
+ getInstances.ReturnType != typeof(IEnumerable<object>))
+ {
+ throw Error.Argument("commonServiceLocator", SRResources.DependencyResolver_DoesNotImplementICommonServiceLocator, locatorType.FullName);
+ }
+
+ var getService = (Func<Type, object>)Delegate.CreateDelegate(typeof(Func<Type, object>), commonServiceLocator, getInstance);
+ var getServices = (Func<Type, IEnumerable<object>>)Delegate.CreateDelegate(typeof(Func<Type, IEnumerable<object>>), commonServiceLocator, getInstances);
+
+ SetResolver(new DelegateBasedDependencyResolver(getService, getServices));
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types.")]
+ public void SetResolver(Func<Type, object> getService, Func<Type, IEnumerable<object>> getServices)
+ {
+ if (getService == null)
+ {
+ throw Error.ArgumentNull("getService");
+ }
+
+ if (getServices == null)
+ {
+ throw Error.ArgumentNull("getServices");
+ }
+
+ SetResolver(new DelegateBasedDependencyResolver(getService, getServices));
+ }
+
+ private void ResetCache()
+ {
+ _cacheSingle = new ConcurrentDictionary<Type, object>();
+ _cacheMultiple = new ConcurrentDictionary<Type, IEnumerable<object>>();
+ }
+
+ // Helper classes
+
+ private class DelegateBasedDependencyResolver : IDependencyResolver
+ {
+ private Func<Type, object> _getService;
+ private Func<Type, IEnumerable<object>> _getServices;
+
+ public DelegateBasedDependencyResolver(Func<Type, object> getService, Func<Type, IEnumerable<object>> getServices)
+ {
+ _getService = getService;
+ _getServices = getServices;
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "This method might throw exceptions whose type we cannot strongly link against; namely, ActivationException from common service locator")]
+ public object GetService(Type type)
+ {
+ try
+ {
+ return _getService.Invoke(type);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ public IEnumerable<object> GetServices(Type type)
+ {
+ return _getServices(type);
+ }
+ }
+
+ private class EmptyDependencyResolver : IDependencyResolver
+ {
+ public object GetService(Type serviceType)
+ {
+ return null;
+ }
+
+ public IEnumerable<object> GetServices(Type serviceType)
+ {
+ return Enumerable.Empty<object>();
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/Services/IDependencyResolver.cs b/src/System.Web.Http/Services/IDependencyResolver.cs
new file mode 100644
index 00000000..0e1a9ac7
--- /dev/null
+++ b/src/System.Web.Http/Services/IDependencyResolver.cs
@@ -0,0 +1,22 @@
+using System.Collections.Generic;
+
+namespace System.Web.Http.Services
+{
+ public interface IDependencyResolver
+ {
+ /// <summary>
+ /// Try to get a service of the given type.
+ /// </summary>
+ /// <param name="serviceType">Type of service to request.</param>
+ /// <returns>an instance of the service, or null if the service is not found</returns>
+ object GetService(Type serviceType);
+
+ /// <summary>
+ /// Try to get a list of services of the given type.
+ /// </summary>
+ /// <param name="serviceType">Type of services to request.</param>
+ /// <returns>an enumeration (possibly empty) of the service.
+ /// Return an empty enumeration is the service is not found (don't return null)</returns>
+ IEnumerable<object> GetServices(Type serviceType);
+ }
+}
diff --git a/src/System.Web.Http/Settings.StyleCop b/src/System.Web.Http/Settings.StyleCop
new file mode 100644
index 00000000..ddf8ec76
--- /dev/null
+++ b/src/System.Web.Http/Settings.StyleCop
@@ -0,0 +1,11 @@
+<StyleCopSettings Version="105">
+ <SourceFileList>
+ <SourceFile>DynamicQueryable.cs</SourceFile>
+ <SourceFile>UriQueryUtility.cs</SourceFile>
+ <Settings>
+ <GlobalSettings>
+ <BooleanProperty Name="RulesEnabledByDefault">False</BooleanProperty>
+ </GlobalSettings>
+ </Settings>
+ </SourceFileList>
+</StyleCopSettings> \ No newline at end of file
diff --git a/src/System.Web.Http/System.Web.Http.csproj b/src/System.Web.Http/System.Web.Http.csproj
new file mode 100644
index 00000000..7189a4d9
--- /dev/null
+++ b/src/System.Web.Http/System.Web.Http.csproj
@@ -0,0 +1,374 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <CodeAnalysis Condition=" '$(CodeAnalysis)' == '' ">false</CodeAnalysis>
+ <ProductVersion>9.0.30729</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{DDC1CE0C-486E-4E35-BB3B-EAB61F8F9440}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>System.Web.Http</RootNamespace>
+ <AssemblyName>System.Web.Http</AssemblyName>
+ <FileAlignment>512</FileAlignment>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>..\..\bin\Debug\</OutputPath>
+ <DefineConstants>TRACE;DEBUG;ASPNETMVC</DefineConstants>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ <DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
+ <NoWarn>1591</NoWarn>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>..\..\bin\Release\</OutputPath>
+ <DefineConstants>TRACE;ASPNETMVC</DefineConstants>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ <RunCodeAnalysis>$(CodeAnalysis)</RunCodeAnalysis>
+ <DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
+ <NoWarn>1591</NoWarn>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'CodeCoverage|AnyCPU'">
+ <DebugSymbols>true</DebugSymbols>
+ <OutputPath>..\..\bin\CodeCoverage\</OutputPath>
+ <DefineConstants>TRACE;DEBUG;CODE_COVERAGE;ASPNETMVC</DefineConstants>
+ <DebugType>full</DebugType>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="System" />
+ <Reference Include="System.ComponentModel.DataAnnotations" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.Data.Linq" />
+ <Reference Include="System.Net.Http">
+ <HintPath>..\..\packages\Microsoft.Net.Http.2.0.20302.1\lib\net40\System.Net.Http.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Net.Http.WebRequest">
+ <HintPath>..\..\packages\Microsoft.Net.Http.2.0.20302.1\lib\net40\System.Net.Http.WebRequest.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Runtime.Caching" />
+ <Reference Include="System.Runtime.Serialization" />
+ <Reference Include="System.Xml" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="..\AptcaCommonAssemblyInfo.cs">
+ <Link>Properties\AptcaCommonAssemblyInfo.cs</Link>
+ </Compile>
+ <Compile Include="..\CommonAssemblyInfo.cs">
+ <Link>Properties\CommonAssemblyInfo.cs</Link>
+ </Compile>
+ <Compile Include="AcceptVerbsAttribute.cs" />
+ <Compile Include="Controllers\HttpControllerConfigurationAttribute.cs" />
+ <Compile Include="Controllers\HttpActionContext.cs" />
+ <Compile Include="Description\ApiDescription.cs" />
+ <Compile Include="Description\ApiExplorer.cs" />
+ <Compile Include="Description\ApiExplorerSettingsAttribute.cs" />
+ <Compile Include="Description\ApiParameterDescription.cs" />
+ <Compile Include="Description\ApiParameterSource.cs" />
+ <Compile Include="Description\IDocumentationProvider.cs" />
+ <Compile Include="Description\IApiExplorer.cs" />
+ <Compile Include="IncludeErrorDetailPolicy.cs" />
+ <Compile Include="Filters\HttpActionExecutedContext.cs" />
+ <Compile Include="Filters\EnumerableEvaluatorFilter.cs" />
+ <Compile Include="Filters\EnumerableEvaluatorFilterProvider.cs" />
+ <Compile Include="Filters\QueryCompositionFilterProvider.cs" />
+ <Compile Include="Internal\UriQueryUtility.cs" />
+ <Compile Include="ModelBinding\FormatterParameterBinding.cs" />
+ <Compile Include="ModelBinding\CancellationTokenParameterBinding.cs" />
+ <Compile Include="ModelBinding\ErrorParameterBinding.cs" />
+ <Compile Include="ModelBinding\FormDataCollectionExtensions.cs" />
+ <Compile Include="ModelBinding\HttpActionBinding.cs" />
+ <Compile Include="Controllers\HttpActionDescriptor.cs" />
+ <Compile Include="ActionNameAttribute.cs" />
+ <Compile Include="Controllers\ReflectedHttpActionDescriptor.cs" />
+ <Compile Include="AllowAnonymousAttribute.cs" />
+ <Compile Include="Dispatcher\ExceptionSurrogate.cs" />
+ <Compile Include="HttpResponseException.cs" />
+ <Compile Include="ModelBinding\HttpRequestParameterBinding.cs" />
+ <Compile Include="ModelBinding\JQueryMVCFormUrlEncodedFormatter.cs" />
+ <Compile Include="ModelBinding\ModelBinderParameterBinding.cs" />
+ <Compile Include="Query\ServiceQueryPart.cs" />
+ <Compile Include="ResultLimitAttribute.cs" />
+ <Compile Include="Filters\QueryCompositionFilterAttribute.cs" />
+ <Compile Include="HttpResponseMessageExtensions.cs" />
+ <Compile Include="Dispatcher\DefaultHttpControllerActivator.cs" />
+ <Compile Include="Controllers\HttpControllerDescriptor.cs" />
+ <Compile Include="Dispatcher\IHttpControllerActivator.cs" />
+ <Compile Include="HttpRouteCollection.cs" />
+ <Compile Include="Controllers\TaskActionResponseConverter.cs" />
+ <Compile Include="Controllers\HttpContentMessageConverter.cs" />
+ <Compile Include="Controllers\VoidHttpResponseMessageConverter.cs" />
+ <Compile Include="Controllers\HttpResponseMessageConverter.cs" />
+ <Compile Include="Controllers\ActionResponseConverter.cs" />
+ <Compile Include="HttpPutAttribute.cs" />
+ <Compile Include="HttpDeleteAttribute.cs" />
+ <Compile Include="Controllers\IActionHttpMethodProvider.cs" />
+ <Compile Include="Controllers\IActionMethodSelector.cs" />
+ <Compile Include="Controllers\ApiControllerActionInvoker.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="AuthorizeAttribute.cs" />
+ <Compile Include="HttpGetAttribute.cs" />
+ <Compile Include="HttpPostAttribute.cs" />
+ <Compile Include="HttpRequestMessageExtensions.cs" />
+ <Compile Include="Filters\HttpFilterCollection.cs" />
+ <Compile Include="Filters\IFilter.cs" />
+ <Compile Include="HttpServer.cs" />
+ <Compile Include="Internal\TypeActivator.cs" />
+ <Compile Include="ModelBinding\DefaultActionValueBinder.cs" />
+ <Compile Include="Controllers\IHttpActionInvoker.cs" />
+ <Compile Include="Controllers\IActionValueBinder.cs" />
+ <Compile Include="Controllers\IHttpActionSelector.cs" />
+ <Compile Include="Filters\ActionFilterAttribute.cs" />
+ <Compile Include="Filters\AuthorizationFilterAttribute.cs" />
+ <Compile Include="Filters\ExceptionFilterAttribute.cs" />
+ <Compile Include="Filters\ActionDescriptorFilterProvider.cs" />
+ <Compile Include="Filters\FilterInfo.cs" />
+ <Compile Include="Filters\FilterInfoComparer.cs" />
+ <Compile Include="Filters\FilterScope.cs" />
+ <Compile Include="Filters\ConfigurationFilterProvider.cs" />
+ <Compile Include="Filters\IActionFilter.cs" />
+ <Compile Include="Filters\IAuthorizationFilter.cs" />
+ <Compile Include="Filters\IExceptionFilter.cs" />
+ <Compile Include="Filters\IFilterProvider.cs" />
+ <Compile Include="Internal\MemberInfoExtensions.cs" />
+ <Compile Include="ModelBinding\Binders\CompositeModelBinderProvider.cs" />
+ <Compile Include="ModelBinding\Binders\CompositeModelBinder.cs" />
+ <Compile Include="HttpRouteCollectionExtensions.cs" />
+ <Compile Include="Internal\ParameterDescriptorExtensionMethods.cs" />
+ <Compile Include="Query\QueryValidator.cs" />
+ <Compile Include="Routing\BoundRouteTemplate.cs" />
+ <Compile Include="Routing\IHttpVirtualPathData.cs" />
+ <Compile Include="Routing\IHttpRouteData.cs" />
+ <Compile Include="Routing\IHttpRoute.cs" />
+ <Compile Include="Routing\HttpMethodConstraint.cs" />
+ <Compile Include="Routing\HttpParsedRoute.cs" />
+ <Compile Include="Routing\HttpRoute.cs" />
+ <Compile Include="Routing\HttpRouteData.cs" />
+ <Compile Include="Routing\HttpRouteDirection.cs" />
+ <Compile Include="Routing\HttpRouteParser.cs" />
+ <Compile Include="Routing\HttpRouteValueDictionary.cs" />
+ <Compile Include="Routing\HttpVirtualPathData.cs" />
+ <Compile Include="Routing\IHttpRouteConstraint.cs" />
+ <Compile Include="Routing\PathContentSegment.cs" />
+ <Compile Include="Routing\PathLiteralSubsegment.cs" />
+ <Compile Include="Routing\PathParameterSubsegment.cs" />
+ <Compile Include="Routing\PathSegment.cs" />
+ <Compile Include="Routing\PathSeparatorSegment.cs" />
+ <Compile Include="Routing\PathSubsegment.cs" />
+ <Compile Include="NonActionAttribute.cs" />
+ <Compile Include="Routing\UrlHelper.cs" />
+ <Compile Include="Services\DependencyResolver.cs" />
+ <Compile Include="Dispatcher\HttpControllerTypeCache.cs" />
+ <Compile Include="Dispatcher\DefaultHttpControllerFactory.cs" />
+ <Compile Include="Dispatcher\DefaultBuildManager.cs" />
+ <Compile Include="DependencyResolverExtensions.cs" />
+ <Compile Include="Services\IDependencyResolver.cs" />
+ <Compile Include="Dispatcher\IHttpControllerFactory.cs" />
+ <Compile Include="DictionaryExtensions.cs" />
+ <Compile Include="Filters\FilterAttribute.cs" />
+ <Compile Include="Hosting\HttpPropertyKeys.cs" />
+ <Compile Include="Hosting\HttpPipelineFactory.cs" />
+ <Compile Include="Dispatcher\HttpControllerDispatcher.cs" />
+ <Compile Include="Dispatcher\IBuildManager.cs" />
+ <Compile Include="Dispatcher\HttpControllerTypeCacheSerializer.cs" />
+ <Compile Include="Dispatcher\HttpControllerTypeCacheUtil.cs" />
+ <Compile Include="Internal\TypeHelper.cs" />
+ <Compile Include="Internal\ParameterInfoExtensions.cs" />
+ <Compile Include="Controllers\ReflectedHttpParameterDescriptor.cs" />
+ <Compile Include="HttpBindNeverAttribute.cs" />
+ <Compile Include="HttpBindRequiredAttribute.cs" />
+ <Compile Include="Controllers\HttpActionContextExtensions.cs" />
+ <Compile Include="ModelBinding\ModelBindingHelper.cs" />
+ <Compile Include="Controllers\ApiControllerActionSelector.cs" />
+ <Compile Include="ModelBinding\HttpParameterBinding.cs" />
+ <Compile Include="Controllers\HttpParameterDescriptor.cs" />
+ <Compile Include="Services\DefaultServiceResolver.cs" />
+ <Compile Include="GlobalSuppressions.cs" />
+ <Compile Include="Controllers\IHttpController.cs" />
+ <Compile Include="Internal\DataTypeUtil.cs" />
+ <Compile Include="Internal\TypeDescriptorHelper.cs" />
+ <Compile Include="Internal\ValueProviderUtil.cs" />
+ <Compile Include="Internal\CollectionModelBinderUtil.cs" />
+ <Compile Include="Metadata\Providers\CachedAssociatedMetadataProvider.cs" />
+ <Compile Include="Metadata\Providers\CachedDataAnnotationsMetadataAttributes.cs" />
+ <Compile Include="Metadata\Providers\CachedModelMetadata.cs" />
+ <Compile Include="Metadata\IMetadataAware.cs" />
+ <Compile Include="Metadata\ModelMetadata.cs" />
+ <Compile Include="Metadata\ModelMetadataProvider.cs" />
+ <Compile Include="Metadata\Providers\AssociatedMetadataProvider.cs" />
+ <Compile Include="Metadata\Providers\CachedDataAnnotationsModelMetadata.cs" />
+ <Compile Include="Metadata\Providers\CachedDataAnnotationsModelMetadataProvider.cs" />
+ <Compile Include="Metadata\Providers\EmptyMetadataProvider.cs" />
+ <Compile Include="ModelBinding\Binders\ArrayModelBinderProvider.cs" />
+ <Compile Include="ModelBinding\Binders\ArrayModelBinder.cs" />
+ <Compile Include="ModelBinding\Binders\BinaryDataModelBinderProvider.cs" />
+ <Compile Include="HttpBindingBehavior.cs" />
+ <Compile Include="HttpBindingBehaviorAttribute.cs" />
+ <Compile Include="ModelBinding\Binders\CollectionModelBinderProvider.cs" />
+ <Compile Include="ModelBinding\Binders\CollectionModelBinder.cs" />
+ <Compile Include="ModelBinding\Binders\ComplexModelDto.cs" />
+ <Compile Include="ModelBinding\Binders\ComplexModelDtoModelBinder.cs" />
+ <Compile Include="ModelBinding\Binders\ComplexModelDtoModelBinderProvider.cs" />
+ <Compile Include="ModelBinding\Binders\ComplexModelDtoResult.cs" />
+ <Compile Include="ModelBinding\Binders\DictionaryModelBinderProvider.cs" />
+ <Compile Include="ModelBinding\Binders\DictionaryModelBinder.cs" />
+ <Compile Include="ModelBinding\ModelBinderAttribute.cs" />
+ <Compile Include="ModelBinding\CustomModelBinderAttribute.cs" />
+ <Compile Include="ModelBinding\ModelBindingContext.cs" />
+ <Compile Include="ModelBinding\Binders\GenericModelBinderProvider.cs" />
+ <Compile Include="ModelBinding\IModelBinder.cs" />
+ <Compile Include="ModelBinding\Binders\KeyValuePairModelBinderProvider.cs" />
+ <Compile Include="Internal\HttpActionContextExtensions.cs" />
+ <Compile Include="ModelBinding\Binders\KeyValuePairModelBinder.cs" />
+ <Compile Include="ModelBinding\ModelBinderConfig.cs" />
+ <Compile Include="ModelBinding\ModelBinderErrorMessageProvider.cs" />
+ <Compile Include="ModelBinding\ModelBinderProvider.cs" />
+ <Compile Include="Controllers\HttpControllerContext.cs" />
+ <Compile Include="ModelBinding\ModelError.cs" />
+ <Compile Include="ModelBinding\ModelErrorCollection.cs" />
+ <Compile Include="ModelBinding\ModelState.cs" />
+ <Compile Include="ModelBinding\ModelStateDictionary.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="Properties\SRResources.Designer.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>SRResources.resx</DependentUpon>
+ </Compile>
+ <Compile Include="Tracing\HttpRequestMessageExtensions.cs" />
+ <Compile Include="Tracing\IFormatterTracer.cs" />
+ <Compile Include="Tracing\ITraceManager.cs" />
+ <Compile Include="Tracing\ITraceWriter.cs" />
+ <Compile Include="Tracing\ITraceWriterExtensions.cs" />
+ <Compile Include="Tracing\TraceManager.cs" />
+ <Compile Include="Tracing\TraceCategories.cs" />
+ <Compile Include="Tracing\TraceKind.cs" />
+ <Compile Include="Tracing\TraceLevel.cs" />
+ <Compile Include="Tracing\TraceRecord.cs" />
+ <Compile Include="Tracing\Tracers\HttpActionBindingTracer.cs" />
+ <Compile Include="Tracing\Tracers\HttpActionDescriptorTracer.cs" />
+ <Compile Include="Tracing\Tracers\ActionFilterAttributeTracer.cs" />
+ <Compile Include="Tracing\Tracers\ActionFilterTracer.cs" />
+ <Compile Include="Tracing\Tracers\HttpActionInvokerTracer.cs" />
+ <Compile Include="Tracing\Tracers\HttpActionSelectorTracer.cs" />
+ <Compile Include="Tracing\Tracers\ActionValueBinderTracer.cs" />
+ <Compile Include="Tracing\Tracers\HttpControllerTracer.cs" />
+ <Compile Include="Tracing\Tracers\AuthorizationFilterAttributeTracer.cs" />
+ <Compile Include="Tracing\Tracers\AuthorizationFilterTracer.cs" />
+ <Compile Include="Tracing\Tracers\BufferedMediaTypeFormatterTracer.cs" />
+ <Compile Include="Tracing\Tracers\HttpControllerActivatorTracer.cs" />
+ <Compile Include="Tracing\Tracers\HttpControllerFactoryTracer.cs" />
+ <Compile Include="Tracing\Tracers\ExceptionFilterAttributeTracer.cs" />
+ <Compile Include="Tracing\Tracers\ExceptionFilterTracer.cs" />
+ <Compile Include="Tracing\Tracers\FilterTracer.cs" />
+ <Compile Include="Tracing\Tracers\ContentNegotiatorTracer.cs" />
+ <Compile Include="Tracing\Tracers\FormatterParameterBindingTracer.cs" />
+ <Compile Include="Tracing\Tracers\FormUrlEncodedMediaTypeFormatterTracer.cs" />
+ <Compile Include="Tracing\Tracers\HttpParameterBindingTracer.cs" />
+ <Compile Include="Tracing\Tracers\JsonMediaTypeFormatterTracer.cs" />
+ <Compile Include="Tracing\Tracers\MediaTypeFormatterTracer.cs" />
+ <Compile Include="Tracing\Tracers\MessageHandlerTracer.cs" />
+ <Compile Include="Tracing\Tracers\RequestMessageHandlerTracer.cs" />
+ <Compile Include="Tracing\Tracers\XmlMediaTypeFormatterTracer.cs" />
+ <Compile Include="Tracing\FormattingUtilities.cs" />
+ <Compile Include="Validation\ClientRules\ModelClientValidationStringLengthRule.cs" />
+ <Compile Include="Validation\ClientRules\ModelClientValidationRequiredRule.cs" />
+ <Compile Include="Validation\ClientRules\ModelClientValidationRegexRule.cs" />
+ <Compile Include="Validation\ClientRules\ModelClientValidationRangeRule.cs" />
+ <Compile Include="Validation\DefaultBodyModelValidator.cs" />
+ <Compile Include="Validation\IBodyModelValidator.cs" />
+ <Compile Include="Validation\IClientValidatable.cs" />
+ <Compile Include="Validation\ModelClientValidationRule.cs" />
+ <Compile Include="Validation\ModelStateFormatterLogger.cs" />
+ <Compile Include="Validation\ModelValidatedEventArgs.cs" />
+ <Compile Include="Validation\ModelValidatingEventArgs.cs" />
+ <Compile Include="Validation\ModelValidationNode.cs" />
+ <Compile Include="ModelBinding\Binders\MutableObjectModelBinder.cs" />
+ <Compile Include="ModelBinding\Binders\MutableObjectModelBinderProvider.cs" />
+ <Compile Include="ModelBinding\Binders\SimpleModelBinderProvider.cs" />
+ <Compile Include="ModelBinding\Binders\TypeConverterModelBinder.cs" />
+ <Compile Include="ModelBinding\Binders\TypeConverterModelBinderProvider.cs" />
+ <Compile Include="ModelBinding\Binders\TypeMatchModelBinder.cs" />
+ <Compile Include="ModelBinding\Binders\TypeMatchModelBinderProvider.cs" />
+ <Compile Include="Validation\ModelValidationRequiredMemberSelector.cs" />
+ <Compile Include="Validation\ModelValidationResult.cs" />
+ <Compile Include="Validation\ModelValidator.cs" />
+ <Compile Include="Validation\ModelValidatorProvider.cs" />
+ <Compile Include="Validation\Providers\AssociatedValidatorProvider.cs" />
+ <Compile Include="Validation\Providers\ClientDataTypeModelValidatorProvider.cs" />
+ <Compile Include="Validation\Providers\DataAnnotationsModelValidatorProvider.cs" />
+ <Compile Include="Validation\Providers\DataMemberModelValidatorProvider.cs" />
+ <Compile Include="Validation\Validators\RequiredMemberModelValidator.cs" />
+ <Compile Include="Validation\Validators\ValidatableObjectAdapter.cs" />
+ <Compile Include="Validation\Validators\StringLengthAttributeAdapter.cs" />
+ <Compile Include="Validation\Validators\RequiredAttributeAdapter.cs" />
+ <Compile Include="Validation\Validators\RegularExpressionAttributeAdapter.cs" />
+ <Compile Include="Validation\Validators\DataAnnotationsModelValidator.cs" />
+ <Compile Include="Validation\Validators\RangeAttributeAdapter.cs" />
+ <Compile Include="FromBodyAttribute.cs" />
+ <Compile Include="FromUriAttribute.cs" />
+ <Compile Include="ValueProviders\IUriValueProviderFactory.cs" />
+ <Compile Include="ValueProviders\Providers\CompositeValueProvider.cs" />
+ <Compile Include="ValueProviders\Providers\CompositeValueProviderFactory.cs" />
+ <Compile Include="ValueProviders\Providers\NameValueCollectionValueProvider.cs" />
+ <Compile Include="ValueProviders\Providers\ElementalValueProvider.cs" />
+ <Compile Include="ValueProviders\IEnumerableValueProvider.cs" />
+ <Compile Include="ValueProviders\Providers\KeyValueModelValueProvider.cs" />
+ <Compile Include="ValueProviders\Providers\RouteDataValueProvider.cs" />
+ <Compile Include="ValueProviders\Providers\RouteDataValueProviderFactory.cs" />
+ <Compile Include="ValueProviders\SingleObjectKeyValueModel.cs" />
+ <Compile Include="ValueProviders\ValueProviderFactory.cs" />
+ <Compile Include="ValueProviders\Providers\QueryStringValueProvider.cs" />
+ <Compile Include="ValueProviders\Providers\QueryStringValueProviderFactory.cs" />
+ <Compile Include="ValueProviders\ValueProviderAttribute.cs" />
+ <Compile Include="ValueProviders\ValueProviderResult.cs" />
+ <Compile Include="ValueProviders\IValueProvider.cs" />
+ <Compile Include="HttpConfiguration.cs" />
+ <Compile Include="ApiController.cs" />
+ <Compile Include="Query\DynamicQueryable.cs" />
+ <Compile Include="Query\ODataQueryDeserializer.cs" />
+ <Compile Include="Query\QueryComposer.cs" />
+ <Compile Include="Query\QueryResolver.cs" />
+ <Compile Include="Query\QueryTypeHelper.cs" />
+ <Compile Include="Query\ServiceQuery.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <CodeAnalysisDictionary Include="..\CodeAnalysisDictionary.xml" />
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="Modelbinding_ClassDiagram.cd" />
+ <None Include="packages.config" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="Properties\SRResources.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>SRResources.Designer.cs</LastGenOutput>
+ <SubType>Designer</SubType>
+ </EmbeddedResource>
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\System.Json\System.Json.csproj">
+ <Project>{F0441BE9-BDC0-4629-BE5A-8765FFAA2481}</Project>
+ <Name>System.Json</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\System.Net.Http.Formatting\System.Net.Http.Formatting.csproj">
+ <Project>{668E9021-CE84-49D9-98FB-DF125A9FCDB0}</Project>
+ <Name>System.Net.Http.Formatting</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\System.Web.Http.Common\System.Web.Http.Common.csproj">
+ <Project>{03A5E5F2-2E23-48F2-ABCC-6C41BAC9AC02}</Project>
+ <Name>System.Web.Http.Common</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup />
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+</Project> \ No newline at end of file
diff --git a/src/System.Web.Http/Tracing/FormattingUtilities.cs b/src/System.Web.Http/Tracing/FormattingUtilities.cs
new file mode 100644
index 00000000..be85c19b
--- /dev/null
+++ b/src/System.Web.Http/Tracing/FormattingUtilities.cs
@@ -0,0 +1,147 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics.Contracts;
+using System.Globalization;
+using System.Linq;
+using System.Net.Http.Formatting;
+using System.Text;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.ModelBinding;
+using System.Web.Http.ModelBinding.Binders;
+using System.Web.Http.Properties;
+using System.Web.Http.Routing;
+using System.Web.Http.ValueProviders;
+using System.Web.Http.ValueProviders.Providers;
+
+namespace System.Web.Http.Tracing
+{
+ /// <summary>
+ /// General purpose utilities to format strings used in tracing.
+ /// </summary>
+ internal static class FormattingUtilities
+ {
+ public static readonly string NullMessage = "null";
+
+ public static string ActionArgumentsToString(IDictionary<string, object> actionArguments)
+ {
+ Contract.Assert(actionArguments != null);
+ return string.Join(", ",
+ actionArguments.Keys.Select<string, string>(
+ (k) => k + "=" + ValueToString(actionArguments[k], CultureInfo.CurrentCulture)));
+ }
+
+ public static string ActionDescriptorToString(HttpActionDescriptor actionDescriptor)
+ {
+ Contract.Assert(actionDescriptor != null);
+
+ string parameterList = string.Join(", ",
+ actionDescriptor.GetParameters().Select<HttpParameterDescriptor, string>(
+ (p) => p.ParameterType.Name + " " + p.ParameterName));
+
+ return actionDescriptor.ActionName + "(" + parameterList + ")";
+ }
+
+ public static string ActionInvokeToString(HttpActionContext actionContext)
+ {
+ Contract.Assert(actionContext != null);
+ return ActionInvokeToString(actionContext.ActionDescriptor.ActionName, actionContext.ActionArguments);
+ }
+
+ public static string ActionInvokeToString(string actionName, IDictionary<string, object> arguments)
+ {
+ Contract.Assert(actionName != null);
+ Contract.Assert(arguments != null);
+
+ return actionName + "(" + ActionArgumentsToString(arguments) + ")";
+ }
+
+ public static string FormattersToString(IEnumerable<MediaTypeFormatter> formatters)
+ {
+ Contract.Assert(formatters != null);
+
+ return String.Join(", ", formatters.Select<MediaTypeFormatter, string>((f) => f.GetType().Name));
+ }
+
+ public static string ModelBinderToString(ModelBinderProvider provider)
+ {
+ Contract.Assert(provider != null);
+
+ CompositeModelBinderProvider composite = provider as CompositeModelBinderProvider;
+ if (composite == null)
+ {
+ return provider.GetType().Name;
+ }
+
+ string modelBinderList = string.Join(", ", composite.Providers.Select<ModelBinderProvider, string>(ModelBinderToString));
+
+ return provider.GetType().Name + "(" + modelBinderList + ")";
+ }
+
+ public static string ModelStateToString(ModelStateDictionary modelState)
+ {
+ Contract.Assert(modelState != null);
+
+ if (modelState.IsValid)
+ {
+ return String.Empty;
+ }
+
+ StringBuilder modelStateBuilder = new StringBuilder();
+ foreach (string key in modelState.Keys)
+ {
+ ModelState state = modelState[key];
+ if (state.Errors.Count > 0)
+ {
+ foreach (ModelError error in state.Errors)
+ {
+ string errorString = Error.Format(SRResources.TraceModelStateErrorMessage,
+ key,
+ error.ErrorMessage);
+ if (modelStateBuilder.Length > 0)
+ {
+ modelStateBuilder.Append(',');
+ }
+
+ modelStateBuilder.Append(errorString);
+ }
+ }
+ }
+
+ return modelStateBuilder.ToString();
+ }
+
+ public static string RouteToString(IHttpRouteData routeData)
+ {
+ Contract.Assert(routeData != null);
+
+ return String.Join(",", routeData.Values.Select((pair) => Error.Format("{0}:{1}", pair.Key, pair.Value)));
+ }
+
+ public static string ValueProviderToString(IValueProvider provider)
+ {
+ Contract.Assert(provider != null);
+
+ CompositeValueProvider composite = provider as CompositeValueProvider;
+ if (composite == null)
+ {
+ return provider.GetType().Name;
+ }
+
+ string providerList = string.Join(", ", composite.Select<IValueProvider, string>(ValueProviderToString));
+ return provider.GetType().Name + "(" + providerList + ")";
+ }
+
+ public static string ValueToString(object value, CultureInfo cultureInfo)
+ {
+ Contract.Assert(cultureInfo != null);
+
+ if (value == null)
+ {
+ return NullMessage;
+ }
+
+ return Convert.ToString(value, cultureInfo);
+ }
+ }
+}
diff --git a/src/System.Web.Http/Tracing/HttpRequestMessageExtensions.cs b/src/System.Web.Http/Tracing/HttpRequestMessageExtensions.cs
new file mode 100644
index 00000000..c3b16f63
--- /dev/null
+++ b/src/System.Web.Http/Tracing/HttpRequestMessageExtensions.cs
@@ -0,0 +1,30 @@
+using System.Diagnostics.Contracts;
+using System.Net.Http;
+using System.Web.Http.Hosting;
+
+namespace System.Web.Http.Tracing
+{
+ public static class HttpRequestMessageExtensions
+ {
+ /// <summary>
+ /// Retrieves the <see cref="Guid"/> which has been assigned as the
+ /// correlation id associated with the given <paramref name="request"/>.
+ /// The value will be created and set the first time this method is called.
+ /// </summary>
+ /// <param name="request">The <see cref="HttpRequestMessage"/></param>
+ /// <returns>The <see cref="Guid"/> associated with that request.</returns>
+ public static Guid GetCorrelationId(this HttpRequestMessage request)
+ {
+ Contract.Assert(request != null);
+
+ Guid correlationId;
+ if (!request.Properties.TryGetValue<Guid>(HttpPropertyKeys.RequestCorrelationKey, out correlationId))
+ {
+ correlationId = Guid.NewGuid();
+ request.Properties.Add(HttpPropertyKeys.RequestCorrelationKey, correlationId);
+ }
+
+ return correlationId;
+ }
+ }
+}
diff --git a/src/System.Web.Http/Tracing/IFormatterTracer.cs b/src/System.Web.Http/Tracing/IFormatterTracer.cs
new file mode 100644
index 00000000..c2e5f5b1
--- /dev/null
+++ b/src/System.Web.Http/Tracing/IFormatterTracer.cs
@@ -0,0 +1,22 @@
+using System.Net.Http;
+using System.Net.Http.Formatting;
+using System.Web.Http.Tracing.Tracers;
+
+namespace System.Web.Http.Tracing
+{
+ /// <summary>
+ /// Interface used to mark <see cref="MediaTypeFormatterTracer"/> classes.
+ /// </summary>
+ internal interface IFormatterTracer
+ {
+ /// <summary>
+ /// Gets the associated <see cref="HttpRequestMessage"/>.
+ /// </summary>
+ HttpRequestMessage Request { get; }
+
+ /// <summary>
+ /// Gets the inner <see cref="MediaTypeFormatter"/> this tracer is monitoring.
+ /// </summary>
+ MediaTypeFormatter InnerFormatter { get; }
+ }
+}
diff --git a/src/System.Web.Http/Tracing/ITraceManager.cs b/src/System.Web.Http/Tracing/ITraceManager.cs
new file mode 100644
index 00000000..9f5b8079
--- /dev/null
+++ b/src/System.Web.Http/Tracing/ITraceManager.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Web.Http.Services;
+
+namespace System.Web.Http.Tracing
+{
+ /// <summary>
+ /// Interface to initialize the tracing layer.
+ /// </summary>
+ /// <remarks>
+ /// This is an extensibility interface that may be inserted into the
+ /// <see cref="DependencyResolver"/> to provide a replacement for the
+ /// entire tracing layer.
+ /// </remarks>
+ public interface ITraceManager
+ {
+ void Initialize(HttpConfiguration configuration);
+ }
+}
diff --git a/src/System.Web.Http/Tracing/ITraceWriter.cs b/src/System.Web.Http/Tracing/ITraceWriter.cs
new file mode 100644
index 00000000..f0c4af4c
--- /dev/null
+++ b/src/System.Web.Http/Tracing/ITraceWriter.cs
@@ -0,0 +1,41 @@
+using System.Net.Http;
+
+namespace System.Web.Http.Tracing
+{
+ /// <summary>
+ /// Interface to write <see cref="TraceRecord"/> instances.
+ /// </summary>
+ public interface ITraceWriter
+ {
+ /// <summary>
+ /// Determines whether tracing is currently enabled for the given <paramref name="category"/>
+ /// and <paramref name="level"/>.
+ /// </summary>
+ /// <param name="category">The trace category.</param>
+ /// <param name="level">The <see cref="TraceLevel"/></param>
+ /// <returns>Returns <c>true</c> if tracing is currently enabled for the category and level,
+ /// otherwise returns <c>false</c>.</returns>
+ bool IsEnabled(string category, TraceLevel level);
+
+ /// <summary>
+ /// Invokes the specified <paramref name="traceAction"/> to allow setting values in
+ /// a new <see cref="TraceRecord"/> if and only if tracing is permitted at the given
+ /// <paramref name="category"/> and <paramref name="level"/>.
+ /// </summary>
+ /// <remarks>
+ /// If tracing is permitted at the given category and level, the <see cref="ITraceWriter"/>
+ /// will construct a <see cref="TraceRecord"/> and invoke the caller's action to allow
+ /// it to set values in the <see cref="TraceRecord"/> provided to it.
+ /// When the caller's action returns, the <see cref="TraceRecord"/>
+ /// will be recorded. If tracing is not enabled, <paramref name="traceAction"/> will not be called.
+ /// </remarks>
+ /// <param name="request">The current <see cref="HttpRequestMessage"/>.
+ /// It may be <c>null</c> but doing so will prevent subsequent trace analysis
+ /// from correlating the trace to a particular request.</param>
+ /// <param name="category">The logical category for the trace. Users can define their own.</param>
+ /// <param name="level">The <see cref="TraceLevel"/> at which to write this trace.</param>
+ /// <param name="traceAction">The action to invoke if tracing is enabled. The caller is expected
+ /// to fill in the fields of the given <see cref="TraceRecord"/> in this action.</param>
+ void Trace(HttpRequestMessage request, string category, TraceLevel level, Action<TraceRecord> traceAction);
+ }
+}
diff --git a/src/System.Web.Http/Tracing/ITraceWriterExtensions.cs b/src/System.Web.Http/Tracing/ITraceWriterExtensions.cs
new file mode 100644
index 00000000..86b4b6f5
--- /dev/null
+++ b/src/System.Web.Http/Tracing/ITraceWriterExtensions.cs
@@ -0,0 +1,746 @@
+using System.Globalization;
+using System.Net.Http;
+using System.Threading.Tasks;
+using System.Web.Http.Properties;
+
+namespace System.Web.Http.Tracing
+{
+ /// <summary>
+ /// Extension methods for <see cref="ITraceWriter"/>.
+ /// </summary>
+ public static class ITraceWriterExtensions
+ {
+ /// <summary>
+ /// Writes a <see cref="TraceRecord"/> at <see cref="TraceLevel.Debug"/> with the given message.
+ /// </summary>
+ /// <param name="traceWriter">The <see cref="ITraceWriter"/></param>
+ /// <param name="request">The <see cref="HttpRequestMessage"/> with which to correlate the request.
+ /// It may be null, but if so will not be correlated with any request.</param>
+ /// <param name="category">The category for the trace.</param>
+ /// <param name="messageFormat">The string to use to format a message. It may not be null.</param>
+ /// <param name="messageArguments">Optional list of arguments for the <paramref name="messageFormat"/>.</param>
+ public static void Debug(this ITraceWriter traceWriter, HttpRequestMessage request, string category, string messageFormat, params object[] messageArguments)
+ {
+ Trace(traceWriter, request, category, TraceLevel.Debug, messageFormat, messageArguments);
+ }
+
+ /// <summary>
+ /// Writes a <see cref="TraceRecord"/> at <see cref="TraceLevel.Debug"/> with the given <paramref name="exception"/>.
+ /// </summary>
+ /// <param name="traceWriter">The <see cref="ITraceWriter"/></param>
+ /// <param name="request">The <see cref="HttpRequestMessage"/> with which to correlate the request.
+ /// It may be null, but if so will not be correlated with any request.</param>
+ /// <param name="category">The category for the trace.</param>
+ /// <param name="exception">The exception to trace</param>
+ public static void Debug(this ITraceWriter traceWriter, HttpRequestMessage request, string category, Exception exception)
+ {
+ Trace(traceWriter, request, category, TraceLevel.Debug, exception);
+ }
+
+ /// <summary>
+ /// Writes a <see cref="TraceRecord"/> at <see cref="TraceLevel.Debug"/> with the given message and exception.
+ /// </summary>
+ /// <param name="traceWriter">The <see cref="ITraceWriter"/></param>
+ /// <param name="request">The <see cref="HttpRequestMessage"/> with which to correlate the request.
+ /// It may be null, but if so will not be correlated with any request.</param>
+ /// <param name="category">The category for the trace.</param>
+ /// <param name="exception">The exception to trace</param>
+ /// <param name="messageFormat">The string to use to format a message. It may not be null.</param>
+ /// <param name="messageArguments">Optional list of arguments for the <paramref name="messageFormat"/>.</param>
+ public static void Debug(this ITraceWriter traceWriter, HttpRequestMessage request, string category, Exception exception, string messageFormat, params object[] messageArguments)
+ {
+ Trace(traceWriter, request, category, TraceLevel.Debug, exception, messageFormat, messageArguments);
+ }
+
+ /// <summary>
+ /// Writes a <see cref="TraceRecord"/> at <see cref="TraceLevel.Error"/> with the given message.
+ /// </summary>
+ /// <param name="traceWriter">The <see cref="ITraceWriter"/></param>
+ /// <param name="request">The <see cref="HttpRequestMessage"/> with which to correlate the request.
+ /// It may be null, but if so will not be correlated with any request.</param>
+ /// <param name="category">The category for the trace.</param>
+ /// <param name="messageFormat">The string to use to format a message. It may not be null.</param>
+ /// <param name="messageArguments">Optional list of arguments for the <paramref name="messageFormat"/>.</param>
+ public static void Error(this ITraceWriter traceWriter, HttpRequestMessage request, string category, string messageFormat, params object[] messageArguments)
+ {
+ Trace(traceWriter, request, category, TraceLevel.Error, messageFormat, messageArguments);
+ }
+
+ /// <summary>
+ /// Writes a <see cref="TraceRecord"/> at <see cref="TraceLevel.Error"/> with the given <paramref name="exception"/>.
+ /// </summary>
+ /// <param name="traceWriter">The <see cref="ITraceWriter"/></param>
+ /// <param name="request">The <see cref="HttpRequestMessage"/> with which to correlate the request.
+ /// It may be null, but if so will not be correlated with any request.</param>
+ /// <param name="category">The category for the trace.</param>
+ /// <param name="exception">The exception to trace</param>
+ public static void Error(this ITraceWriter traceWriter, HttpRequestMessage request, string category, Exception exception)
+ {
+ Trace(traceWriter, request, category, TraceLevel.Error, exception);
+ }
+
+ /// <summary>
+ /// Writes a <see cref="TraceRecord"/> at <see cref="TraceLevel.Error"/> with the given message and exception.
+ /// </summary>
+ /// <param name="traceWriter">The <see cref="ITraceWriter"/></param>
+ /// <param name="request">The <see cref="HttpRequestMessage"/> with which to correlate the request.
+ /// It may be null, but if so will not be correlated with any request.</param>
+ /// <param name="category">The category for the trace.</param>
+ /// <param name="exception">The exception to trace</param>
+ /// <param name="messageFormat">The string to use to format a message. It may not be null.</param>
+ /// <param name="messageArguments">Optional list of arguments for the <paramref name="messageFormat"/>.</param>
+ public static void Error(this ITraceWriter traceWriter, HttpRequestMessage request, string category, Exception exception, string messageFormat, params object[] messageArguments)
+ {
+ Trace(traceWriter, request, category, TraceLevel.Error, exception, messageFormat, messageArguments);
+ }
+
+ /// <summary>
+ /// Writes a <see cref="TraceRecord"/> at <see cref="TraceLevel.Fatal"/> with the given message.
+ /// </summary>
+ /// <param name="traceWriter">The <see cref="ITraceWriter"/></param>
+ /// <param name="request">The <see cref="HttpRequestMessage"/> with which to correlate the request.
+ /// It may be null, but if so will not be correlated with any request.</param>
+ /// <param name="category">The category for the trace.</param>
+ /// <param name="messageFormat">The string to use to format a message. It may not be null.</param>
+ /// <param name="messageArguments">Optional list of arguments for the <paramref name="messageFormat"/>.</param>
+ public static void Fatal(this ITraceWriter traceWriter, HttpRequestMessage request, string category, string messageFormat, params object[] messageArguments)
+ {
+ Trace(traceWriter, request, category, TraceLevel.Fatal, messageFormat, messageArguments);
+ }
+
+ /// <summary>
+ /// Writes a <see cref="TraceRecord"/> at <see cref="TraceLevel.Fatal"/> with the given <paramref name="exception"/>.
+ /// </summary>
+ /// <param name="traceWriter">The <see cref="ITraceWriter"/></param>
+ /// <param name="request">The <see cref="HttpRequestMessage"/> with which to correlate the request.
+ /// It may be null, but if so will not be correlated with any request.</param>
+ /// <param name="category">The category for the trace.</param>
+ /// <param name="exception">The exception to trace</param>
+ public static void Fatal(this ITraceWriter traceWriter, HttpRequestMessage request, string category, Exception exception)
+ {
+ Trace(traceWriter, request, category, TraceLevel.Fatal, exception);
+ }
+
+ /// <summary>
+ /// Writes a <see cref="TraceRecord"/> at <see cref="TraceLevel.Fatal"/> with the given message and exception.
+ /// </summary>
+ /// <param name="traceWriter">The <see cref="ITraceWriter"/></param>
+ /// <param name="request">The <see cref="HttpRequestMessage"/> with which to correlate the request.
+ /// It may be null, but if so will not be correlated with any request.</param>
+ /// <param name="category">The category for the trace.</param>
+ /// <param name="exception">The exception to trace</param>
+ /// <param name="messageFormat">The string to use to format a message. It may not be null.</param>
+ /// <param name="messageArguments">Optional list of arguments for the <paramref name="messageFormat"/>.</param>
+ public static void Fatal(this ITraceWriter traceWriter, HttpRequestMessage request, string category, Exception exception, string messageFormat, params object[] messageArguments)
+ {
+ Trace(traceWriter, request, category, TraceLevel.Fatal, exception, messageFormat, messageArguments);
+ }
+
+ /// <summary>
+ /// Writes a <see cref="TraceRecord"/> at <see cref="TraceLevel.Info"/> with the given message.
+ /// </summary>
+ /// <param name="traceWriter">The <see cref="ITraceWriter"/></param>
+ /// <param name="request">The <see cref="HttpRequestMessage"/> with which to correlate the request.
+ /// It may be null, but if so will not be correlated with any request.</param>
+ /// <param name="category">The category for the trace.</param>
+ /// <param name="messageFormat">The string to use to format a message. It may not be null.</param>
+ /// <param name="messageArguments">Optional list of arguments for the <paramref name="messageFormat"/>.</param>
+ public static void Info(this ITraceWriter traceWriter, HttpRequestMessage request, string category, string messageFormat, params object[] messageArguments)
+ {
+ Trace(traceWriter, request, category, TraceLevel.Info, messageFormat, messageArguments);
+ }
+
+ /// <summary>
+ /// Writes a <see cref="TraceRecord"/> at <see cref="TraceLevel.Info"/> with the given <paramref name="exception"/>.
+ /// </summary>
+ /// <param name="traceWriter">The <see cref="ITraceWriter"/></param>
+ /// <param name="request">The <see cref="HttpRequestMessage"/> with which to correlate the request.
+ /// It may be null, but if so will not be correlated with any request.</param>
+ /// <param name="category">The category for the trace.</param>
+ /// <param name="exception">The exception to trace</param>
+ public static void Info(this ITraceWriter traceWriter, HttpRequestMessage request, string category, Exception exception)
+ {
+ Trace(traceWriter, request, category, TraceLevel.Info, exception);
+ }
+
+ /// <summary>
+ /// Writes a <see cref="TraceRecord"/> at <see cref="TraceLevel.Info"/> with the given message and exception.
+ /// </summary>
+ /// <param name="traceWriter">The <see cref="ITraceWriter"/></param>
+ /// <param name="request">The <see cref="HttpRequestMessage"/> with which to correlate the request.
+ /// It may be null, but if so will not be correlated with any request.</param>
+ /// <param name="category">The category for the trace.</param>
+ /// <param name="exception">The exception to trace</param>
+ /// <param name="messageFormat">The string to use to format a message. It may not be null.</param>
+ /// <param name="messageArguments">Optional list of arguments for the <paramref name="messageFormat"/>.</param>
+ public static void Info(this ITraceWriter traceWriter, HttpRequestMessage request, string category, Exception exception, string messageFormat, params object[] messageArguments)
+ {
+ Trace(traceWriter, request, category, TraceLevel.Info, exception, messageFormat, messageArguments);
+ }
+
+ /// <summary>
+ /// Writes a single <see cref="TraceRecord"/> to the given <see cref="ITraceWriter"/> if the trace writer
+ /// is enabled for the given <paramref name="category"/> and <paramref name="level"/>.
+ /// </summary>
+ /// <param name="traceWriter">The <see cref="ITraceWriter"/></param>
+ /// <param name="request">The <see cref="HttpRequestMessage"/> with which to correlate the request.
+ /// It may be null, but if so cannot be correlated with any request.</param>
+ /// <param name="category">The category for the trace.</param>
+ /// <param name="level">The <see cref="TraceLevel"/> for the trace.</param>
+ /// <param name="exception">The <see cref="Exception"/> to trace. It may not be null.</param>
+ public static void Trace(this ITraceWriter traceWriter, HttpRequestMessage request, string category, TraceLevel level, Exception exception)
+ {
+ if (traceWriter == null)
+ {
+ throw System.Web.Http.Common.Error.ArgumentNull("traceWriter");
+ }
+
+ if (exception == null)
+ {
+ throw System.Web.Http.Common.Error.ArgumentNull("exception");
+ }
+
+ traceWriter.Trace(
+ request,
+ category,
+ level,
+ (TraceRecord traceRecord) =>
+ {
+ traceRecord.Exception = exception;
+ });
+ }
+
+ /// <summary>
+ /// Writes a single <see cref="TraceRecord"/> to the given <see cref="ITraceWriter"/> if the trace writer
+ /// is enabled for the given <paramref name="category"/> and <paramref name="level"/>.
+ /// </summary>
+ /// <param name="traceWriter">The <see cref="ITraceWriter"/></param>
+ /// <param name="request">The <see cref="HttpRequestMessage"/> with which to correlate the request.
+ /// It may be null, but if so will not be correlated with any request.</param>
+ /// <param name="category">The category for the trace.</param>
+ /// <param name="level">The <see cref="TraceLevel"/> for the trace.</param>
+ /// <param name="exception">The <see cref="Exception"/> to trace. It may not be null.</param>
+ /// <param name="messageFormat">The string to use to format a message. It may not be null.</param>
+ /// <param name="messageArguments">Optional list of arguments for the <paramref name="messageFormat"/>.</param>
+ public static void Trace(this ITraceWriter traceWriter, HttpRequestMessage request, string category, TraceLevel level, Exception exception, string messageFormat, params object[] messageArguments)
+ {
+ if (traceWriter == null)
+ {
+ throw System.Web.Http.Common.Error.ArgumentNull("traceWriter");
+ }
+
+ if (exception == null)
+ {
+ throw System.Web.Http.Common.Error.ArgumentNull("exception");
+ }
+
+ if (messageFormat == null)
+ {
+ throw System.Web.Http.Common.Error.ArgumentNull("messageFormat");
+ }
+
+ traceWriter.Trace(
+ request,
+ category,
+ level,
+ (TraceRecord traceRecord) =>
+ {
+ traceRecord.Exception = exception;
+ traceRecord.Message = System.Web.Http.Common.Error.Format(messageFormat, messageArguments);
+ });
+ }
+
+ /// <summary>
+ /// Writes a single <see cref="TraceRecord"/> to the given <see cref="ITraceWriter"/> if the trace writer
+ /// is enabled for the given <paramref name="category"/> and <paramref name="level"/>.
+ /// </summary>
+ /// <param name="traceWriter">The <see cref="ITraceWriter"/></param>
+ /// <param name="request">The <see cref="HttpRequestMessage"/> with which to correlate the request.
+ /// It may be null, but if so will not be correlated with any request.</param>
+ /// <param name="category">The category for the trace.</param>
+ /// <param name="level">The <see cref="TraceLevel"/> for the trace.</param>
+ /// <param name="messageFormat">The string to use to format a message. It may not be null.</param>
+ /// <param name="messageArguments">Optional list of arguments for the <paramref name="messageFormat"/>.</param>
+ public static void Trace(this ITraceWriter traceWriter, HttpRequestMessage request, string category, TraceLevel level, string messageFormat, params object[] messageArguments)
+ {
+ if (traceWriter == null)
+ {
+ throw System.Web.Http.Common.Error.ArgumentNull("traceWriter");
+ }
+
+ if (messageFormat == null)
+ {
+ throw System.Web.Http.Common.Error.ArgumentNull("messageFormat");
+ }
+
+ traceWriter.Trace(
+ request,
+ category,
+ level,
+ (TraceRecord traceRecord) =>
+ {
+ traceRecord.Message = System.Web.Http.Common.Error.Format(messageFormat, messageArguments);
+ });
+ }
+
+ /// <summary>
+ /// Traces both a begin and an end trace around a specified operation.
+ /// </summary>
+ /// <param name="traceWriter">The <see cref="ITraceWriter"/>.</param>
+ /// <param name="request">The <see cref="HttpRequestMessage"/> with which to associate the trace. It may be null.</param>
+ /// <param name="category">The logical category of the trace.</param>
+ /// <param name="level">The <see cref="TraceLevel"/> of the trace.</param>
+ /// <param name="operatorName">The name of the object performing the operation. It may be null.</param>
+ /// <param name="operationName">The name of the operation being performaed. It may be null.</param>
+ /// <param name="beginTrace">The <see cref="Action"/> to invoke prior to performing the operation,
+ /// allowing the given <see cref="TraceRecord"/> to be filled in. It may be null.</param>
+ /// <param name="execute">An <see cref="Action"/> that performs the operation.</param>
+ /// <param name="endTrace">The <see cref="Action"/> to invoke after successfully performing the operation,
+ /// allowing the given <see cref="TraceRecord"/> to be filled in. It may be null.</param>
+ /// <param name="errorTrace">The <see cref="Action"/> to invoke if an error was encountered performing the operation,
+ /// allowing the given <see cref="TraceRecord"/> to be filled in. It may be null.</param>
+ public static void TraceBeginEnd(this ITraceWriter traceWriter,
+ HttpRequestMessage request,
+ string category,
+ TraceLevel level,
+ string operatorName,
+ string operationName,
+ Action<TraceRecord> beginTrace,
+ Action execute,
+ Action<TraceRecord> endTrace,
+ Action<TraceRecord> errorTrace)
+ {
+ if (traceWriter == null)
+ {
+ throw System.Web.Http.Common.Error.ArgumentNull("traceWriter");
+ }
+
+ if (execute == null)
+ {
+ throw System.Web.Http.Common.Error.ArgumentNull("execute");
+ }
+
+ bool isTracing = false;
+
+ traceWriter.Trace(
+ request,
+ category,
+ level,
+ (TraceRecord traceRecord) =>
+ {
+ isTracing = true;
+ traceRecord.Kind = TraceKind.Begin;
+ traceRecord.Operator = operatorName;
+ traceRecord.Operation = operationName;
+ if (beginTrace != null)
+ {
+ beginTrace(traceRecord);
+ }
+ });
+ try
+ {
+ execute();
+
+ if (isTracing)
+ {
+ traceWriter.Trace(
+ request,
+ category,
+ level,
+ (TraceRecord traceRecord) =>
+ {
+ traceRecord.Kind = TraceKind.End;
+ traceRecord.Operator = operatorName;
+ traceRecord.Operation = operationName;
+ if (endTrace != null)
+ {
+ endTrace(traceRecord);
+ }
+ });
+ }
+ }
+ catch (Exception ex)
+ {
+ if (isTracing)
+ {
+ traceWriter.Trace(
+ request,
+ category,
+ TraceLevel.Error,
+ (TraceRecord traceRecord) =>
+ {
+ traceRecord.Kind = TraceKind.End;
+ traceRecord.Operator = operatorName;
+ traceRecord.Operation = operationName;
+ traceRecord.Exception = ex;
+ if (errorTrace != null)
+ {
+ errorTrace(traceRecord);
+ }
+ });
+ }
+ throw;
+ }
+ }
+
+ /// <summary>
+ /// Traces both a begin and an end trace around a specified asynchronous operation.
+ /// </summary>
+ /// <remarks>The end trace will occur when the asynchronous operation completes, either success or failure.</remarks>
+ /// <typeparam name="TResult">The type of result produced by the <see cref="Task"/>.</typeparam>
+ /// /// <param name="traceWriter">The <see cref="ITraceWriter"/>.</param>
+ /// <param name="request">The <see cref="HttpRequestMessage"/> with which to associate the trace. It may be null.</param>
+ /// <param name="category">The logical category of the trace.</param>
+ /// <param name="level">The <see cref="TraceLevel"/> of the trace.</param>
+ /// <param name="operatorName">The name of the object performing the operation. It may be null.</param>
+ /// <param name="operationName">The name of the operation being performed. It may be null.</param>
+ /// <param name="beginTrace">The <see cref="Action"/> to invoke prior to performing the operation,
+ /// allowing the given <see cref="TraceRecord"/> to be filled in. It may be null.</param>
+ /// <param name="execute">An <see cref="Func{Task}"/> that returns the <see cref="Task"/> that will perform the operation.</param>
+ /// <param name="endTrace">The <see cref="Action"/> to invoke after successfully performing the operation,
+ /// allowing the given <see cref="TraceRecord"/> to be filled in. The result of the completed task will also
+ /// be passed to this action. This action may be null.</param>
+ /// <param name="errorTrace">The <see cref="Action"/> to invoke if an error was encountered performing the operation,
+ /// allowing the given <see cref="TraceRecord"/> to be filled in. It may be null.</param>
+ /// <returns>The <see cref="Task"/> returned by the operation.</returns>
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Nested generic required for this method.")]
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "ContinueWith is necessary to observe all completion paths")]
+ public static Task<TResult> TraceBeginEndAsync<TResult>(this ITraceWriter traceWriter,
+ HttpRequestMessage request,
+ string category,
+ TraceLevel level,
+ string operatorName,
+ string operationName,
+ Action<TraceRecord> beginTrace,
+ Func<Task<TResult>> execute,
+ Action<TraceRecord, TResult> endTrace,
+ Action<TraceRecord> errorTrace)
+ {
+ if (traceWriter == null)
+ {
+ throw System.Web.Http.Common.Error.ArgumentNull("traceWriter");
+ }
+
+ if (execute == null)
+ {
+ throw System.Web.Http.Common.Error.ArgumentNull("execute");
+ }
+
+ bool isTracing = false;
+
+ traceWriter.Trace(
+ request,
+ category,
+ level,
+ (TraceRecord traceRecord) =>
+ {
+ isTracing = true;
+ traceRecord.Kind = TraceKind.Begin;
+ traceRecord.Operator = operatorName;
+ traceRecord.Operation = operationName;
+ if (beginTrace != null)
+ {
+ beginTrace(traceRecord);
+ }
+ });
+ try
+ {
+ Task<TResult> task = execute();
+
+ // If we are not tracing, there is no need to ContinueWith
+ if (!isTracing || task == null)
+ {
+ return task;
+ }
+
+ // Task<Task> so that we return the original task, preserving its completion state
+ Task<Task<TResult>> returnTask = task.ContinueWith<Task<TResult>>((t) =>
+ {
+ if (t.IsCanceled)
+ {
+ traceWriter.Trace(
+ request,
+ category,
+ TraceLevel.Warn,
+ (TraceRecord traceRecord) =>
+ {
+ traceRecord.Kind = TraceKind.End;
+ traceRecord.Operator = operatorName;
+ traceRecord.Operation = operationName;
+ traceRecord.Message = SRResources.TraceCancelledMessage;
+ if (errorTrace != null)
+ {
+ errorTrace(traceRecord);
+ }
+ });
+
+ return TaskHelpers.Canceled<TResult>();
+ }
+
+ if (t.IsFaulted)
+ {
+ traceWriter.Trace(
+ request,
+ category,
+ TraceLevel.Error,
+ (TraceRecord traceRecord) =>
+ {
+ traceRecord.Kind = TraceKind.End;
+ traceRecord.Exception = t.Exception.GetBaseException();
+ traceRecord.Operator = operatorName;
+ traceRecord.Operation = operationName;
+ if (errorTrace != null)
+ {
+ errorTrace(traceRecord);
+ }
+ });
+
+ return TaskHelpers.FromErrors<TResult>(t.Exception.InnerExceptions);
+ }
+
+ TaskCompletionSource<TResult> tcs = new TaskCompletionSource<TResult>();
+ TResult result = t.Result;
+
+ traceWriter.Trace(
+ request,
+ category,
+ level,
+ (TraceRecord traceRecord) =>
+ {
+ traceRecord.Kind = TraceKind.End;
+ traceRecord.Operator = operatorName;
+ traceRecord.Operation = operationName;
+ if (endTrace != null)
+ {
+ endTrace(traceRecord, result);
+ }
+ });
+
+ tcs.TrySetResult(result);
+ return tcs.Task;
+ });
+
+ return returnTask.FastUnwrap();
+ }
+ catch (Exception ex)
+ {
+ if (isTracing)
+ {
+ traceWriter.Trace(
+ request,
+ category,
+ TraceLevel.Error,
+ (TraceRecord traceRecord) =>
+ {
+ traceRecord.Kind = TraceKind.End;
+ traceRecord.Operator = operatorName;
+ traceRecord.Operation = operationName;
+ traceRecord.Exception = ex;
+ if (errorTrace != null)
+ {
+ errorTrace(traceRecord);
+ }
+ });
+ }
+ throw;
+ }
+ }
+
+ /// <summary>
+ /// Traces both a begin and an end trace around a specified asynchronous operation.
+ /// </summary>
+ /// <remarks>The end trace will occur when the asynchronous operation completes, either success or failure.</remarks>
+ /// <param name="traceWriter">The <see cref="ITraceWriter"/>.</param>
+ /// <param name="request">The <see cref="HttpRequestMessage"/> with which to associate the trace. It may be null.</param>
+ /// <param name="category">The logical category of the trace.</param>
+ /// <param name="level">The <see cref="TraceLevel"/> of the trace.</param>
+ /// <param name="operatorName">The name of the object performing the operation. It may be null.</param>
+ /// <param name="operationName">The name of the operation being performed. It may be null.</param>
+ /// <param name="beginTrace">The <see cref="Action"/> to invoke prior to performing the operation,
+ /// allowing the given <see cref="TraceRecord"/> to be filled in. It may be null.</param>
+ /// <param name="execute">An <see cref="Func{Task}"/> that returns the <see cref="Task"/> that will perform the operation.</param>
+ /// <param name="endTrace">The <see cref="Action"/> to invoke after successfully performing the operation,
+ /// allowing the given <see cref="TraceRecord"/> to be filled in. It may be null.</param>
+ /// <param name="errorTrace">The <see cref="Action"/> to invoke if an error was encountered performing the operation,
+ /// allowing the given <see cref="TraceRecord"/> to be filled in. It may be null.</param>
+ /// <returns>The <see cref="Task"/> returned by the operation.</returns>
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "ContinueWith is necessary to observe all completion paths")]
+ public static Task TraceBeginEndAsync(this ITraceWriter traceWriter,
+ HttpRequestMessage request,
+ string category,
+ TraceLevel level,
+ string operatorName,
+ string operationName,
+ Action<TraceRecord> beginTrace,
+ Func<Task> execute,
+ Action<TraceRecord> endTrace,
+ Action<TraceRecord> errorTrace)
+ {
+ if (traceWriter == null)
+ {
+ throw System.Web.Http.Common.Error.ArgumentNull("traceWriter");
+ }
+
+ if (execute == null)
+ {
+ throw System.Web.Http.Common.Error.ArgumentNull("execute");
+ }
+
+ bool isTracing = false;
+
+ traceWriter.Trace(
+ request,
+ category,
+ level,
+ (TraceRecord traceRecord) =>
+ {
+ isTracing = true;
+ traceRecord.Kind = TraceKind.Begin;
+ traceRecord.Operator = operatorName;
+ traceRecord.Operation = operationName;
+ if (beginTrace != null)
+ {
+ beginTrace(traceRecord);
+ }
+ });
+ try
+ {
+ Task task = execute();
+
+ // If we are not tracing, there is no need to ContinueWith
+ if (!isTracing || task == null)
+ {
+ return task;
+ }
+
+ Task<Task> returnTask = task.ContinueWith<Task>((t) =>
+ {
+ if (t.IsCanceled)
+ {
+ traceWriter.Trace(
+ request,
+ category,
+ TraceLevel.Warn,
+ (TraceRecord traceRecord) =>
+ {
+ traceRecord.Kind = TraceKind.End;
+ traceRecord.Operator = operatorName;
+ traceRecord.Operation = operationName;
+ traceRecord.Message = SRResources.TraceCancelledMessage;
+ if (errorTrace != null)
+ {
+ errorTrace(traceRecord);
+ }
+ });
+
+ return TaskHelpers.Canceled();
+ }
+
+ if (t.IsFaulted)
+ {
+ traceWriter.Trace(
+ request,
+ category,
+ TraceLevel.Error,
+ (TraceRecord traceRecord) =>
+ {
+ traceRecord.Kind = TraceKind.End;
+ traceRecord.Exception = t.Exception.GetBaseException();
+ traceRecord.Operator = operatorName;
+ traceRecord.Operation = operationName;
+ if (errorTrace != null)
+ {
+ errorTrace(traceRecord);
+ }
+ });
+
+ return TaskHelpers.FromErrors(t.Exception.InnerExceptions);
+ }
+
+ traceWriter.Trace(
+ request,
+ category,
+ level,
+ (TraceRecord traceRecord) =>
+ {
+ traceRecord.Kind = TraceKind.End;
+ traceRecord.Operator = operatorName;
+ traceRecord.Operation = operationName;
+ if (endTrace != null)
+ {
+ endTrace(traceRecord);
+ }
+ });
+
+ return TaskHelpers.Completed();
+ });
+
+ return returnTask.FastUnwrap();
+ }
+ catch (Exception ex)
+ {
+ if (isTracing)
+ {
+ traceWriter.Trace(
+ request,
+ category,
+ TraceLevel.Error,
+ (TraceRecord traceRecord) =>
+ {
+ traceRecord.Kind = TraceKind.End;
+ traceRecord.Operator = operatorName;
+ traceRecord.Operation = operationName;
+ traceRecord.Exception = ex;
+ if (errorTrace != null)
+ {
+ errorTrace(traceRecord);
+ }
+ });
+ }
+ throw;
+ }
+ }
+
+ /// <summary>
+ /// Writes a <see cref="TraceRecord"/> at <see cref="TraceLevel.Warn"/> with the given message.
+ /// </summary>
+ /// <param name="traceWriter">The <see cref="ITraceWriter"/></param>
+ /// <param name="request">The <see cref="HttpRequestMessage"/> with which to correlate the request.
+ /// It may be null, but if so will not be correlated with any request.</param>
+ /// <param name="category">The category for the trace.</param>
+ /// <param name="messageFormat">The string to use to format a message. It may not be null.</param>
+ /// <param name="messageArguments">Optional list of arguments for the <paramref name="messageFormat"/>.</param>
+ public static void Warn(this ITraceWriter traceWriter, HttpRequestMessage request, string category, string messageFormat, params object[] messageArguments)
+ {
+ Trace(traceWriter, request, category, TraceLevel.Warn, messageFormat, messageArguments);
+ }
+
+ /// <summary>
+ /// Writes a <see cref="TraceRecord"/> at <see cref="TraceLevel.Warn"/> with the given <paramref name="exception"/>.
+ /// </summary>
+ /// <param name="traceWriter">The <see cref="ITraceWriter"/></param>
+ /// <param name="request">The <see cref="HttpRequestMessage"/> with which to correlate the request.
+ /// It may be null, but if so will not be correlated with any request.</param>
+ /// <param name="category">The category for the trace.</param>
+ /// <param name="exception">The exception to trace</param>
+ public static void Warn(this ITraceWriter traceWriter, HttpRequestMessage request, string category, Exception exception)
+ {
+ Trace(traceWriter, request, category, TraceLevel.Warn, exception);
+ }
+
+ /// <summary>
+ /// Writes a <see cref="TraceRecord"/> at <see cref="TraceLevel.Warn"/> with the given message and exception.
+ /// </summary>
+ /// <param name="traceWriter">The <see cref="ITraceWriter"/></param>
+ /// <param name="request">The <see cref="HttpRequestMessage"/> with which to correlate the request.
+ /// It may be null, but if so will not be correlated with any request.</param>
+ /// <param name="category">The category for the trace.</param>
+ /// <param name="exception">The exception to trace</param>
+ /// <param name="messageFormat">The string to use to format a message. It may not be null.</param>
+ /// <param name="messageArguments">Optional list of arguments for the <paramref name="messageFormat"/>.</param>
+ public static void Warn(this ITraceWriter traceWriter, HttpRequestMessage request, string category, Exception exception, string messageFormat, params object[] messageArguments)
+ {
+ Trace(traceWriter, request, category, TraceLevel.Warn, exception, messageFormat, messageArguments);
+ }
+ }
+}
diff --git a/src/System.Web.Http/Tracing/TraceCategories.cs b/src/System.Web.Http/Tracing/TraceCategories.cs
new file mode 100644
index 00000000..d2b31034
--- /dev/null
+++ b/src/System.Web.Http/Tracing/TraceCategories.cs
@@ -0,0 +1,25 @@
+using System.Collections.Generic;
+
+namespace System.Web.Http.Tracing
+{
+ /// <summary>
+ /// Category names traced by the default tracing implementation.
+ /// </summary>
+ /// <remarks>
+ /// The list of permitted category names is open-ended, and users may define their own.
+ /// It is recommended that category names reflect the namespace of their
+ /// respective area. This prevents name conflicts and allows external
+ /// logging tools to enable or disable tracing by namespace.
+ /// </remarks>
+ public static class TraceCategories
+ {
+ public static readonly string ActionCategory = "System.Web.Http.Action";
+ public static readonly string ControllersCategory = "System.Web.Http.Controllers";
+ public static readonly string FiltersCategory = "System.Web.Http.Filters";
+ public static readonly string FormattingCategory = "System.Net.Http.Formatting";
+ public static readonly string MessageHandlersCategory = "System.Web.Http.MessageHandlers";
+ public static readonly string ModelBindingCategory = "System.Web.Http.ModelBinding";
+ public static readonly string RequestCategory = "System.Web.Http.Request";
+ public static readonly string RoutingCategory = "System.Web.Http.Routing";
+ }
+}
diff --git a/src/System.Web.Http/Tracing/TraceKind.cs b/src/System.Web.Http/Tracing/TraceKind.cs
new file mode 100644
index 00000000..9edb6914
--- /dev/null
+++ b/src/System.Web.Http/Tracing/TraceKind.cs
@@ -0,0 +1,23 @@
+namespace System.Web.Http.Tracing
+{
+ /// <summary>
+ /// Describes the kind of <see cref="TraceRecord"/> for an individual trace operation.
+ /// </summary>
+ public enum TraceKind
+ {
+ /// <summary>
+ /// Single trace, not part of a Begin/End trace pair
+ /// </summary>
+ Trace,
+
+ /// <summary>
+ /// Trace marking the beginning of some operation.
+ /// </summary>
+ Begin,
+
+ /// <summary>
+ /// Trace marking the end of some operation.
+ /// </summary>
+ End,
+ }
+}
diff --git a/src/System.Web.Http/Tracing/TraceLevel.cs b/src/System.Web.Http/Tracing/TraceLevel.cs
new file mode 100644
index 00000000..180978ae
--- /dev/null
+++ b/src/System.Web.Http/Tracing/TraceLevel.cs
@@ -0,0 +1,45 @@
+namespace System.Web.Http.Tracing
+{
+ /// <summary>
+ /// Available trace levels.
+ /// </summary>
+ /// <remarks>
+ /// The interpretation of these levels is the responsibility of the
+ /// <see cref="ITraceWriter"/> implementation. The general convention is that
+ /// enabling a particular trace level also enables all levels greater than or
+ /// equal to it. For example, tracing at <see cref="Warn"/> level would
+ /// generally trace if the trace writer was enabled to trace at level <see cref="Info"/>.
+ /// </remarks>
+ public enum TraceLevel
+ {
+ /// <summary>
+ /// Tracing is disabled
+ /// </summary>
+ Off = 0,
+
+ /// <summary>
+ /// Trace level for debugging traces
+ /// </summary>
+ Debug = 1,
+
+ /// <summary>
+ /// Trace level for informational traces
+ /// </summary>
+ Info = 2,
+
+ /// <summary>
+ /// Trace level for warning traces
+ /// </summary>
+ Warn = 3,
+
+ /// <summary>
+ /// Trace level for error traces
+ /// </summary>
+ Error = 4,
+
+ /// <summary>
+ /// Trace level for fatal traces
+ /// </summary>
+ Fatal = 5
+ }
+}
diff --git a/src/System.Web.Http/Tracing/TraceManager.cs b/src/System.Web.Http/Tracing/TraceManager.cs
new file mode 100644
index 00000000..66355d74
--- /dev/null
+++ b/src/System.Web.Http/Tracing/TraceManager.cs
@@ -0,0 +1,101 @@
+using System.Net.Http;
+using System.Net.Http.Formatting;
+using System.Web.Http.Controllers;
+using System.Web.Http.Dispatcher;
+using System.Web.Http.Tracing.Tracers;
+
+namespace System.Web.Http.Tracing
+{
+ internal class TraceManager : ITraceManager
+ {
+ public void Initialize(HttpConfiguration configuration)
+ {
+ ITraceWriter traceWriter = configuration.ServiceResolver.GetTraceWriter();
+ if (traceWriter != null)
+ {
+ // Install tracers only when a custom trace writer has been registered
+ CreateAllTracers(configuration, traceWriter);
+ }
+ }
+
+ private static void CreateAllTracers(HttpConfiguration configuration, ITraceWriter traceWriter)
+ {
+ CreateActionInvokerTracer(configuration, traceWriter);
+ CreateActionSelectorTracer(configuration, traceWriter);
+ CreateActionValueBinderTracer(configuration, traceWriter);
+ CreateContentNegotiatorTracer(configuration, traceWriter);
+ CreateControllerActivatorTracer(configuration, traceWriter);
+ CreateControllerFactoryTracer(configuration, traceWriter);
+ CreateMessageHandlerTracers(configuration, traceWriter);
+ CreateMediaTypeFormatterTracers(configuration, traceWriter);
+ }
+
+ private static void CreateActionInvokerTracer(HttpConfiguration configuration, ITraceWriter traceWriter)
+ {
+ IHttpActionInvoker invoker = configuration.ServiceResolver.GetService(typeof(IHttpActionInvoker)) as IHttpActionInvoker;
+ HttpActionInvokerTracer tracer = new HttpActionInvokerTracer(invoker, traceWriter);
+ configuration.ServiceResolver.SetService(typeof(IHttpActionInvoker), tracer);
+ }
+
+ private static void CreateActionSelectorTracer(HttpConfiguration configuration, ITraceWriter traceWriter)
+ {
+ IHttpActionSelector selector = configuration.ServiceResolver.GetService(typeof(IHttpActionSelector)) as IHttpActionSelector;
+ HttpActionSelectorTracer tracer = new HttpActionSelectorTracer(selector, traceWriter);
+ configuration.ServiceResolver.SetService(typeof(IHttpActionSelector), tracer);
+ }
+
+ private static void CreateActionValueBinderTracer(HttpConfiguration configuration, ITraceWriter traceWriter)
+ {
+ IActionValueBinder binder = configuration.ServiceResolver.GetService(typeof(IActionValueBinder)) as IActionValueBinder;
+ ActionValueBinderTracer tracer = new ActionValueBinderTracer(binder, traceWriter);
+ configuration.ServiceResolver.SetService(typeof(IActionValueBinder), tracer);
+ }
+
+ private static void CreateContentNegotiatorTracer(HttpConfiguration configuration, ITraceWriter traceWriter)
+ {
+ IContentNegotiator negotiator = configuration.ServiceResolver.GetService(typeof(IContentNegotiator)) as IContentNegotiator;
+ ContentNegotiatorTracer tracer = new ContentNegotiatorTracer(negotiator, traceWriter);
+ configuration.ServiceResolver.SetService(typeof(IContentNegotiator), tracer);
+ }
+
+ private static void CreateControllerActivatorTracer(HttpConfiguration configuration, ITraceWriter traceWriter)
+ {
+ IHttpControllerActivator activator = configuration.ServiceResolver.GetService(typeof(IHttpControllerActivator)) as IHttpControllerActivator;
+ HttpControllerActivatorTracer tracer = new HttpControllerActivatorTracer(activator, traceWriter);
+ configuration.ServiceResolver.SetService(typeof(IHttpControllerActivator), tracer);
+ }
+
+ private static void CreateControllerFactoryTracer(HttpConfiguration configuration, ITraceWriter traceWriter)
+ {
+ IHttpControllerFactory factory = configuration.ServiceResolver.GetService(typeof(IHttpControllerFactory)) as IHttpControllerFactory;
+ HttpControllerFactoryTracer tracer = new HttpControllerFactoryTracer(factory, traceWriter);
+ configuration.ServiceResolver.SetService(typeof(IHttpControllerFactory), tracer);
+ }
+
+ private static void CreateMediaTypeFormatterTracers(HttpConfiguration configuration, ITraceWriter traceWriter)
+ {
+ for (int i = 0; i < configuration.Formatters.Count; i++)
+ {
+ configuration.Formatters[i] = MediaTypeFormatterTracer.CreateTracer(
+ configuration.Formatters[i],
+ traceWriter,
+ request: null);
+ }
+ }
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Will be disposed when pipeline is disposed.")]
+ private static void CreateMessageHandlerTracers(HttpConfiguration configuration, ITraceWriter traceWriter)
+ {
+ // Insert a tracing handler before each existing message handler (in execution order)
+ int handlerCount = configuration.MessageHandlers.Count;
+ for (int i = 0; i < handlerCount * 2; i += 2)
+ {
+ DelegatingHandler innerHandler = configuration.MessageHandlers[i];
+ DelegatingHandler handlerTracer = new MessageHandlerTracer(innerHandler, traceWriter);
+ configuration.MessageHandlers.Insert(i + 1, handlerTracer);
+ }
+
+ configuration.MessageHandlers.Add(new RequestMessageHandlerTracer(traceWriter));
+ }
+ }
+}
diff --git a/src/System.Web.Http/Tracing/TraceRecord.cs b/src/System.Web.Http/Tracing/TraceRecord.cs
new file mode 100644
index 00000000..00a333c9
--- /dev/null
+++ b/src/System.Web.Http/Tracing/TraceRecord.cs
@@ -0,0 +1,87 @@
+using System.Collections.Generic;
+using System.Net;
+using System.Net.Http;
+
+namespace System.Web.Http.Tracing
+{
+ /// <summary>
+ /// Data object used by <see cref="ITraceWriter"/> to record traces.
+ /// </summary>
+ public class TraceRecord
+ {
+ private Lazy<Dictionary<object, object>> _properties = new Lazy<Dictionary<object, object>>(
+ () => new Dictionary<object, object>());
+
+ public TraceRecord(HttpRequestMessage request, string category, TraceLevel level)
+ {
+ Timestamp = DateTime.UtcNow;
+ Request = request;
+ RequestId = request != null ? request.GetCorrelationId() : Guid.Empty;
+ Category = category;
+ Level = level;
+ }
+
+ /// <summary>
+ /// Gets or sets the tracing category.
+ /// </summary>
+ public string Category { get; set; }
+
+ /// <summary>
+ /// Gets or sets the exception.
+ /// </summary>
+ public Exception Exception { get; set; }
+
+ /// <summary>
+ /// Gets or sets the kind of trace.
+ /// </summary>
+ public TraceKind Kind { get; set; }
+
+ /// <summary>
+ /// Gets or sets the tracing level.
+ /// </summary>
+ public TraceLevel Level { get; set; }
+
+ /// <summary>
+ /// Gets or sets the message.
+ /// </summary>
+ public string Message { get; set; }
+
+ /// <summary>
+ /// Gets or sets the logical operation name being performed.
+ /// </summary>
+ public string Operation { get; set; }
+
+ /// <summary>
+ /// Gets or sets the logical name of the object performing the operation
+ /// </summary>
+ public string Operator { get; set; }
+
+ /// <summary>
+ /// Optional user-defined property bag.
+ /// </summary>
+ public Dictionary<object, object> Properties
+ {
+ get { return _properties.Value; }
+ }
+
+ /// <summary>
+ /// Gets the <see cref="HttpRequestMessage"/>
+ /// </summary>
+ public HttpRequestMessage Request { get; private set; }
+
+ /// <summary>
+ /// Gets the correlation ID from the <see cref="Request"/>.
+ /// </summary>
+ public Guid RequestId { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the <see cref="HttpStatusCode"/> associated with the <see cref="HttpResponseMessage"/>.
+ /// </summary>
+ public HttpStatusCode Status { get; set; }
+
+ /// <summary>
+ /// Gets the <see cref="DateTime"/> of this trace (via <see cref="DateTime.UtcNow"/>)
+ /// </summary>
+ public DateTime Timestamp { get; private set; }
+ }
+}
diff --git a/src/System.Web.Http/Tracing/Tracers/ActionFilterAttributeTracer.cs b/src/System.Web.Http/Tracing/Tracers/ActionFilterAttributeTracer.cs
new file mode 100644
index 00000000..76e7d1a0
--- /dev/null
+++ b/src/System.Web.Http/Tracing/Tracers/ActionFilterAttributeTracer.cs
@@ -0,0 +1,113 @@
+using System.Net.Http;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Filters;
+using System.Web.Http.Properties;
+
+namespace System.Web.Http.Tracing
+{
+ /// <summary>
+ /// Tracer for <see cref="ActionFilterAttribute"/>.
+ /// </summary>
+ internal sealed class ActionFilterAttributeTracer : ActionFilterAttribute
+ {
+ private const string ActionExecutedMethodName = "ActionExecuted";
+ private const string ActionExecutingMethodName = "ActionExecuting";
+
+ private readonly ActionFilterAttribute _innerFilter;
+ private readonly ITraceWriter _traceWriter;
+
+ public ActionFilterAttributeTracer(ActionFilterAttribute innerFilter, ITraceWriter traceWriter)
+ {
+ _innerFilter = innerFilter;
+ _traceWriter = traceWriter;
+ }
+
+ public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
+ {
+ _traceWriter.TraceBeginEnd(
+ actionExecutedContext.Request,
+ TraceCategories.FiltersCategory,
+ TraceLevel.Info,
+ _innerFilter.GetType().Name,
+ ActionExecutedMethodName,
+ beginTrace: (tr) =>
+ {
+ tr.Message = Error.Format(
+ SRResources.TraceActionFilterMessage,
+ FormattingUtilities.ActionDescriptorToString(
+ actionExecutedContext.ActionContext.ActionDescriptor));
+ tr.Exception = actionExecutedContext.Exception;
+ HttpResponseMessage response = actionExecutedContext.Result;
+ if (response != null)
+ {
+ tr.Status = response.StatusCode;
+ }
+ },
+ execute: () =>
+ {
+ _innerFilter.OnActionExecuted(actionExecutedContext);
+ },
+ endTrace: (tr) =>
+ {
+ tr.Exception = actionExecutedContext.Exception;
+ HttpResponseMessage response = actionExecutedContext.Result;
+ if (response != null)
+ {
+ tr.Status = response.StatusCode;
+ }
+ },
+ errorTrace: (tr) =>
+ {
+ HttpResponseMessage response = actionExecutedContext.Result;
+ if (response != null)
+ {
+ tr.Status = response.StatusCode;
+ }
+ });
+ }
+
+ public override void OnActionExecuting(HttpActionContext actionContext)
+ {
+ _traceWriter.TraceBeginEnd(
+ actionContext.Request,
+ TraceCategories.FiltersCategory,
+ TraceLevel.Info,
+ _innerFilter.GetType().Name,
+ ActionExecutingMethodName,
+ beginTrace: (tr) =>
+ {
+ tr.Message = Error.Format(
+ SRResources.TraceActionFilterMessage,
+ FormattingUtilities.ActionDescriptorToString(
+ actionContext.ActionDescriptor));
+
+ HttpResponseMessage response = actionContext.Response;
+ if (response != null)
+ {
+ tr.Status = response.StatusCode;
+ }
+ },
+ execute: () =>
+ {
+ _innerFilter.OnActionExecuting(actionContext);
+ },
+ endTrace: (tr) =>
+ {
+ HttpResponseMessage response = actionContext.Response;
+ if (response != null)
+ {
+ tr.Status = response.StatusCode;
+ }
+ },
+ errorTrace: (tr) =>
+ {
+ HttpResponseMessage response = actionContext.Response;
+ if (response != null)
+ {
+ tr.Status = response.StatusCode;
+ }
+ });
+ }
+ }
+}
diff --git a/src/System.Web.Http/Tracing/Tracers/ActionFilterTracer.cs b/src/System.Web.Http/Tracing/Tracers/ActionFilterTracer.cs
new file mode 100644
index 00000000..467229ef
--- /dev/null
+++ b/src/System.Web.Http/Tracing/Tracers/ActionFilterTracer.cs
@@ -0,0 +1,48 @@
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Controllers;
+using System.Web.Http.Filters;
+
+namespace System.Web.Http.Tracing
+{
+ /// <summary>
+ /// Tracer for <see cref="IActionFilter"/>.
+ /// </summary>
+ internal class ActionFilterTracer : FilterTracer, IActionFilter
+ {
+ private const string ExecuteActionFilterAsyncMethodName = "ExecuteActionFilterAsync";
+
+ public ActionFilterTracer(IActionFilter innerFilter, ITraceWriter traceWriter)
+ : base(innerFilter, traceWriter)
+ {
+ }
+
+ private IActionFilter InnerActionFilter
+ {
+ get { return InnerFilter as IActionFilter; }
+ }
+
+ Task<HttpResponseMessage> IActionFilter.ExecuteActionFilterAsync(HttpActionContext actionContext,
+ CancellationToken cancellationToken,
+ Func<Task<HttpResponseMessage>> continuation)
+ {
+ return TraceWriter.TraceBeginEndAsync<HttpResponseMessage>(
+ actionContext.Request,
+ TraceCategories.FiltersCategory,
+ TraceLevel.Info,
+ InnerActionFilter.GetType().Name,
+ ExecuteActionFilterAsyncMethodName,
+ beginTrace: null,
+ execute: () => InnerActionFilter.ExecuteActionFilterAsync(actionContext, cancellationToken, continuation),
+ endTrace: (tr, response) =>
+ {
+ if (response != null)
+ {
+ tr.Status = response.StatusCode;
+ }
+ },
+ errorTrace: null);
+ }
+ }
+}
diff --git a/src/System.Web.Http/Tracing/Tracers/ActionValueBinderTracer.cs b/src/System.Web.Http/Tracing/Tracers/ActionValueBinderTracer.cs
new file mode 100644
index 00000000..67781cfd
--- /dev/null
+++ b/src/System.Web.Http/Tracing/Tracers/ActionValueBinderTracer.cs
@@ -0,0 +1,41 @@
+using System.Web.Http.Controllers;
+using System.Web.Http.ModelBinding;
+
+namespace System.Web.Http.Tracing.Tracers
+{
+ /// <summary>
+ /// Tracer for <see cref="IActionValueBinder"/>
+ /// </summary>
+ internal class ActionValueBinderTracer : IActionValueBinder
+ {
+ private readonly IActionValueBinder _innerBinder;
+ private readonly ITraceWriter _traceWriter;
+
+ public ActionValueBinderTracer(IActionValueBinder innerBinder, ITraceWriter traceWriter)
+ {
+ _innerBinder = innerBinder;
+ _traceWriter = traceWriter;
+ }
+
+ // Creates wrapping tracers for all HttpParameterBindings
+ HttpActionBinding IActionValueBinder.GetBinding(HttpActionDescriptor actionDescriptor)
+ {
+ HttpActionBinding actionBinding = _innerBinder.GetBinding(actionDescriptor);
+ HttpParameterBinding[] parameterBindings = actionBinding.ParameterBindings;
+ HttpParameterBinding[] newParameterBindings = new HttpParameterBinding[parameterBindings.Length];
+ for (int i = 0; i < newParameterBindings.Length; i++)
+ {
+ HttpParameterBinding parameterBinding = parameterBindings[i];
+
+ // Itercept FormatterParameterBinding to replace its formatters
+ FormatterParameterBinding formatterParameterBinding = parameterBinding as FormatterParameterBinding;
+ newParameterBindings[i] = formatterParameterBinding != null
+ ? (HttpParameterBinding)new FormatterParameterBindingTracer(formatterParameterBinding, _traceWriter)
+ : (HttpParameterBinding)new HttpParameterBindingTracer(parameterBinding, _traceWriter);
+ }
+
+ HttpActionBinding newActionBinding = new HttpActionBinding(actionDescriptor, newParameterBindings);
+ return newActionBinding;
+ }
+ }
+}
diff --git a/src/System.Web.Http/Tracing/Tracers/AuthorizationFilterAttributeTracer.cs b/src/System.Web.Http/Tracing/Tracers/AuthorizationFilterAttributeTracer.cs
new file mode 100644
index 00000000..7c372479
--- /dev/null
+++ b/src/System.Web.Http/Tracing/Tracers/AuthorizationFilterAttributeTracer.cs
@@ -0,0 +1,58 @@
+using System.Net.Http;
+using System.Web.Http.Controllers;
+using System.Web.Http.Filters;
+
+namespace System.Web.Http.Tracing
+{
+ /// <summary>
+ /// Tracer for <see cref="AuthorizationFilterAttribute"/>
+ /// </summary>
+ internal sealed class AuthorizationFilterAttributeTracer : AuthorizationFilterAttribute
+ {
+ private const string OnAuthorizationMethodName = "OnAuthorization";
+
+ private readonly AuthorizationFilterAttribute _innerFilter;
+ private readonly ITraceWriter _traceStore;
+
+ public AuthorizationFilterAttributeTracer(AuthorizationFilterAttribute innerFilter, ITraceWriter traceStore)
+ {
+ _innerFilter = innerFilter;
+ _traceStore = traceStore;
+ }
+
+ public override void OnAuthorization(HttpActionContext actionContext)
+ {
+ _traceStore.TraceBeginEnd(
+ actionContext.ControllerContext.Request,
+ TraceCategories.FiltersCategory,
+ TraceLevel.Info,
+ _innerFilter.GetType().Name,
+ OnAuthorizationMethodName,
+ beginTrace: (tr) =>
+ {
+ HttpResponseMessage response = actionContext.Response;
+ if (response != null)
+ {
+ tr.Status = response.StatusCode;
+ }
+ },
+ execute: () => { _innerFilter.OnAuthorization(actionContext); },
+ endTrace: (tr) =>
+ {
+ HttpResponseMessage response = actionContext.Response;
+ if (response != null)
+ {
+ tr.Status = response.StatusCode;
+ }
+ },
+ errorTrace: (tr) =>
+ {
+ HttpResponseMessage response = actionContext.Response;
+ if (response != null)
+ {
+ tr.Status = response.StatusCode;
+ }
+ });
+ }
+ }
+}
diff --git a/src/System.Web.Http/Tracing/Tracers/AuthorizationFilterTracer.cs b/src/System.Web.Http/Tracing/Tracers/AuthorizationFilterTracer.cs
new file mode 100644
index 00000000..5b451811
--- /dev/null
+++ b/src/System.Web.Http/Tracing/Tracers/AuthorizationFilterTracer.cs
@@ -0,0 +1,48 @@
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Controllers;
+using System.Web.Http.Filters;
+
+namespace System.Web.Http.Tracing
+{
+ /// <summary>
+ /// Tracer for <see cref="IAuthorizationFilter"/>.
+ /// </summary>
+ internal class AuthorizationFilterTracer : FilterTracer, IAuthorizationFilter
+ {
+ private const string ExecuteAuthorizationFilterAsyncMethodName = "ExecuteAuthorizationFilterAsync";
+
+ public AuthorizationFilterTracer(IAuthorizationFilter innerFilter, ITraceWriter traceWriter)
+ : base(innerFilter, traceWriter)
+ {
+ }
+
+ private IAuthorizationFilter InnerAuthorizationFilter
+ {
+ get { return InnerFilter as IAuthorizationFilter; }
+ }
+
+ public Task<HttpResponseMessage> ExecuteAuthorizationFilterAsync(HttpActionContext actionContext,
+ CancellationToken cancellationToken,
+ Func<Task<HttpResponseMessage>> continuation)
+ {
+ return TraceWriter.TraceBeginEndAsync<HttpResponseMessage>(
+ actionContext.Request,
+ TraceCategories.FiltersCategory,
+ TraceLevel.Info,
+ InnerAuthorizationFilter.GetType().Name,
+ ExecuteAuthorizationFilterAsyncMethodName,
+ beginTrace: null,
+ execute: () => InnerAuthorizationFilter.ExecuteAuthorizationFilterAsync(actionContext, cancellationToken, continuation),
+ endTrace: (tr, response) =>
+ {
+ if (response != null)
+ {
+ tr.Status = response.StatusCode;
+ }
+ },
+ errorTrace: null);
+ }
+ }
+}
diff --git a/src/System.Web.Http/Tracing/Tracers/BufferedMediaTypeFormatterTracer.cs b/src/System.Web.Http/Tracing/Tracers/BufferedMediaTypeFormatterTracer.cs
new file mode 100644
index 00000000..e16aa137
--- /dev/null
+++ b/src/System.Web.Http/Tracing/Tracers/BufferedMediaTypeFormatterTracer.cs
@@ -0,0 +1,123 @@
+using System.Globalization;
+using System.IO;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Formatting;
+using System.Net.Http.Headers;
+using System.Web.Http.Common;
+using System.Web.Http.Properties;
+
+namespace System.Web.Http.Tracing.Tracers
+{
+ internal class BufferedMediaTypeFormatterTracer : BufferedMediaTypeFormatter, IFormatterTracer
+ {
+ private const string OnReadFromStreamMethodName = "OnReadFromStream";
+ private const string OnWriteToStreamMethodName = "OnWriteToStream";
+
+ private MediaTypeFormatterTracer _innerTracer;
+
+ public BufferedMediaTypeFormatterTracer(MediaTypeFormatter innerFormatter, ITraceWriter traceWriter, HttpRequestMessage request)
+ {
+ _innerTracer = new MediaTypeFormatterTracer(innerFormatter, traceWriter, request);
+ }
+
+ private BufferedMediaTypeFormatter InnerBufferedFormatter
+ {
+ get { return _innerTracer.InnerFormatter as BufferedMediaTypeFormatter; }
+ }
+
+ HttpRequestMessage IFormatterTracer.Request
+ {
+ get { return _innerTracer.Request; }
+ }
+
+ MediaTypeFormatter IFormatterTracer.InnerFormatter
+ {
+ get { return _innerTracer.InnerFormatter; }
+ }
+
+ public override bool CanReadType(Type type)
+ {
+ return _innerTracer.CanReadType(type);
+ }
+
+ public override bool CanWriteType(Type type)
+ {
+ return _innerTracer.CanWriteType(type);
+ }
+
+ public override MediaTypeFormatter GetPerRequestFormatterInstance(Type type, HttpRequestMessage request, MediaTypeHeaderValue mediaType)
+ {
+ return _innerTracer.GetPerRequestFormatterInstance(type, request, mediaType);
+ }
+
+ public override void SetDefaultContentHeaders(Type type, HttpContentHeaders headers, string mediaType)
+ {
+ _innerTracer.SetDefaultContentHeaders(type, headers, mediaType);
+ }
+
+ public override object OnReadFromStream(Type type, Stream stream, HttpContentHeaders contentHeaders, IFormatterLogger formatterLogger)
+ {
+ BufferedMediaTypeFormatter innerFormatter = InnerBufferedFormatter;
+ MediaTypeHeaderValue contentType = contentHeaders == null ? null : contentHeaders.ContentType;
+ object value = null;
+
+ _innerTracer.TraceWriter.TraceBeginEnd(
+ _innerTracer.Request,
+ TraceCategories.FormattingCategory,
+ TraceLevel.Info,
+ _innerTracer.InnerFormatter.GetType().Name,
+ OnReadFromStreamMethodName,
+ beginTrace: (tr) =>
+ {
+ tr.Message = Error.Format(
+ SRResources.TraceReadFromStreamMessage,
+ type.Name,
+ contentType == null ? SRResources.TraceNoneObjectMessage : contentType.ToString());
+ },
+ execute: () =>
+ {
+ value = innerFormatter.OnReadFromStream(type, stream, contentHeaders, formatterLogger);
+ },
+ endTrace: (tr) =>
+ {
+ tr.Message = Error.Format(
+ SRResources.TraceReadFromStreamValueMessage,
+ FormattingUtilities.ValueToString(value, CultureInfo.CurrentCulture));
+ },
+ errorTrace: null);
+
+ return value;
+ }
+
+ public override void OnWriteToStream(Type type, object value, Stream stream, HttpContentHeaders contentHeaders, TransportContext transportContext)
+ {
+ BufferedMediaTypeFormatter innerFormatter = InnerBufferedFormatter;
+
+ MediaTypeHeaderValue contentType = contentHeaders == null
+ ? null
+ : contentHeaders.ContentType;
+
+ _innerTracer.TraceWriter.TraceBeginEnd(
+ _innerTracer.Request,
+ TraceCategories.FormattingCategory,
+ TraceLevel.Info,
+ _innerTracer.InnerFormatter.GetType().Name,
+ OnWriteToStreamMethodName,
+ beginTrace: (tr) =>
+ {
+ tr.Message = Error.Format(
+ SRResources.TraceWriteToStreamMessage,
+ FormattingUtilities.ValueToString(value, CultureInfo.CurrentCulture),
+ type.Name,
+ contentType == null ? SRResources.TraceNoneObjectMessage : contentType.ToString());
+ },
+ execute: () =>
+ {
+ innerFormatter.OnWriteToStream(type, value, stream, contentHeaders, transportContext);
+ },
+ endTrace: null,
+ errorTrace: null);
+ }
+ }
+}
diff --git a/src/System.Web.Http/Tracing/Tracers/ContentNegotiatorTracer.cs b/src/System.Web.Http/Tracing/Tracers/ContentNegotiatorTracer.cs
new file mode 100644
index 00000000..5053105d
--- /dev/null
+++ b/src/System.Web.Http/Tracing/Tracers/ContentNegotiatorTracer.cs
@@ -0,0 +1,77 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.Net.Http;
+using System.Net.Http.Formatting;
+using System.Net.Http.Headers;
+using System.Web.Http.Common;
+using System.Web.Http.Properties;
+
+namespace System.Web.Http.Tracing.Tracers
+{
+ /// <summary>
+ /// Tracer for <see cref="IContentNegotiator"/>.
+ /// </summary>
+ internal class ContentNegotiatorTracer : IContentNegotiator
+ {
+ private const string NegotiateMethodName = "Negotiate";
+
+ private readonly IContentNegotiator _innerNegotiator;
+ private readonly ITraceWriter _traceWriter;
+
+ public ContentNegotiatorTracer(IContentNegotiator innerNegotiator, ITraceWriter traceWriter)
+ {
+ _innerNegotiator = innerNegotiator;
+ _traceWriter = traceWriter;
+ }
+
+ public MediaTypeFormatter Negotiate(Type type, HttpRequestMessage request, IEnumerable<MediaTypeFormatter> formatters, out MediaTypeHeaderValue mediaType)
+ {
+ mediaType = null;
+ MediaTypeHeaderValue selectedMediaType = null;
+ MediaTypeFormatter selectedFormatter = null;
+
+ _traceWriter.TraceBeginEnd(
+ request,
+ TraceCategories.FormattingCategory,
+ TraceLevel.Info,
+ _innerNegotiator.GetType().Name,
+ NegotiateMethodName,
+
+ beginTrace: (tr) =>
+ {
+ tr.Message = Error.Format(
+ SRResources.TraceNegotiateFormatter,
+ type.Name,
+ FormattingUtilities.FormattersToString(formatters));
+ },
+
+ execute: () =>
+ {
+ selectedFormatter = _innerNegotiator.Negotiate(type, request, formatters, out selectedMediaType);
+ },
+
+ endTrace: (tr) =>
+ {
+ tr.Message = Error.Format(
+ SRResources.TraceSelectedFormatter,
+ selectedFormatter == null
+ ? SRResources.TraceNoneObjectMessage
+ : MediaTypeFormatterTracer.ActualMediaTypeFormatter(selectedFormatter).GetType().Name,
+ selectedMediaType == null
+ ? SRResources.TraceNoneObjectMessage
+ : selectedMediaType.ToString());
+ },
+
+ errorTrace: null);
+
+ mediaType = selectedMediaType;
+
+ if (selectedFormatter != null)
+ {
+ selectedFormatter = MediaTypeFormatterTracer.CreateTracer(selectedFormatter, _traceWriter, request);
+ }
+
+ return selectedFormatter;
+ }
+ }
+}
diff --git a/src/System.Web.Http/Tracing/Tracers/ExceptionFilterAttributeTracer.cs b/src/System.Web.Http/Tracing/Tracers/ExceptionFilterAttributeTracer.cs
new file mode 100644
index 00000000..4b31ac9e
--- /dev/null
+++ b/src/System.Web.Http/Tracing/Tracers/ExceptionFilterAttributeTracer.cs
@@ -0,0 +1,63 @@
+using System.Net.Http;
+using System.Web.Http.Filters;
+
+namespace System.Web.Http.Tracing
+{
+ /// <summary>
+ /// Tracer for <see cref="ExceptionFilterAttribute"/>.
+ /// </summary>
+ internal sealed class ExceptionFilterAttributeTracer : ExceptionFilterAttribute
+ {
+ private const string OnExceptionMethodName = "OnException";
+
+ private readonly ExceptionFilterAttribute _innerFilter;
+ private readonly ITraceWriter _traceStore;
+
+ public ExceptionFilterAttributeTracer(ExceptionFilterAttribute innerFilter, ITraceWriter traceStore)
+ {
+ _innerFilter = innerFilter;
+ _traceStore = traceStore;
+ }
+
+ public override void OnException(HttpActionExecutedContext actionExecutedContext)
+ {
+ _traceStore.TraceBeginEnd(
+ actionExecutedContext.Request,
+ TraceCategories.FiltersCategory,
+ TraceLevel.Info,
+ _innerFilter.GetType().Name,
+ OnExceptionMethodName,
+ beginTrace: (tr) =>
+ {
+ HttpResponseMessage response = actionExecutedContext.Result;
+ if (response != null)
+ {
+ tr.Status = response.StatusCode;
+ }
+ },
+ execute: () =>
+ {
+ _innerFilter.OnException(actionExecutedContext);
+ },
+ endTrace: (tr) =>
+ {
+ Exception returnedException = actionExecutedContext.Exception;
+ tr.Level = returnedException == null ? TraceLevel.Info : TraceLevel.Error;
+ tr.Exception = returnedException;
+ HttpResponseMessage response = actionExecutedContext.Result;
+ if (response != null)
+ {
+ tr.Status = response.StatusCode;
+ }
+ },
+ errorTrace: (tr) =>
+ {
+ HttpResponseMessage response = actionExecutedContext.Result;
+ if (response != null)
+ {
+ tr.Status = response.StatusCode;
+ }
+ });
+ }
+ }
+}
diff --git a/src/System.Web.Http/Tracing/Tracers/ExceptionFilterTracer.cs b/src/System.Web.Http/Tracing/Tracers/ExceptionFilterTracer.cs
new file mode 100644
index 00000000..b9c67c8f
--- /dev/null
+++ b/src/System.Web.Http/Tracing/Tracers/ExceptionFilterTracer.cs
@@ -0,0 +1,49 @@
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Filters;
+
+namespace System.Web.Http.Tracing
+{
+ /// <summary>
+ /// Tracer for <see cref="IExceptionFilter"/>.
+ /// </summary>
+ internal class ExceptionFilterTracer : FilterTracer, IExceptionFilter
+ {
+ private const string ExecuteExceptionFilterAsyncMethodName = "ExecuteExceptionFilterAsync";
+
+ public ExceptionFilterTracer(IExceptionFilter innerFilter, ITraceWriter traceWriter)
+ : base(innerFilter, traceWriter)
+ {
+ }
+
+ public IExceptionFilter InnerExceptionFilter
+ {
+ get { return InnerFilter as IExceptionFilter; }
+ }
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "This layer needs to observe all completion paths")]
+ public Task ExecuteExceptionFilterAsync(HttpActionExecutedContext actionExecutedContext,
+ CancellationToken cancellationToken)
+ {
+ return TraceWriter.TraceBeginEndAsync(
+ actionExecutedContext.Request,
+ TraceCategories.FiltersCategory,
+ TraceLevel.Info,
+ InnerExceptionFilter.GetType().Name,
+ ExecuteExceptionFilterAsyncMethodName,
+ beginTrace: (tr) =>
+ {
+ tr.Exception = actionExecutedContext.Exception;
+ },
+
+ execute: () => InnerExceptionFilter.ExecuteExceptionFilterAsync(actionExecutedContext, cancellationToken),
+
+ endTrace: (tr) =>
+ {
+ tr.Exception = actionExecutedContext.Exception;
+ },
+
+ errorTrace: null);
+ }
+ }
+}
diff --git a/src/System.Web.Http/Tracing/Tracers/FilterTracer.cs b/src/System.Web.Http/Tracing/Tracers/FilterTracer.cs
new file mode 100644
index 00000000..4eecf1e2
--- /dev/null
+++ b/src/System.Web.Http/Tracing/Tracers/FilterTracer.cs
@@ -0,0 +1,104 @@
+using System.Collections.Generic;
+using System.Web.Http.Filters;
+
+namespace System.Web.Http.Tracing
+{
+ /// <summary>
+ /// Base class and helper for the creation of filter tracers.
+ /// </summary>
+ internal class FilterTracer : IFilter
+ {
+ public FilterTracer(IFilter innerFilter, ITraceWriter traceWriter)
+ {
+ InnerFilter = innerFilter;
+ TraceWriter = traceWriter;
+ }
+
+ public IFilter InnerFilter { get; set; }
+
+ public ITraceWriter TraceWriter { get; set; }
+
+ public bool AllowMultiple
+ {
+ get { return InnerFilter.AllowMultiple; }
+ }
+
+ public static IEnumerable<IFilter> CreateFilterTracers(IFilter filter, ITraceWriter traceWriter)
+ {
+ List<IFilter> filters = new List<IFilter>();
+ bool addedActionAttributeTracer = false;
+ bool addedAuthorizationAttributeTracer = false;
+ bool addedExceptionAttributeTracer = false;
+
+ ActionFilterAttribute actionFilterAttribute = filter as ActionFilterAttribute;
+ if (actionFilterAttribute != null)
+ {
+ filters.Add(new ActionFilterAttributeTracer(actionFilterAttribute, traceWriter));
+ addedActionAttributeTracer = true;
+ }
+
+ AuthorizationFilterAttribute authorizationFilterAttribute = filter as AuthorizationFilterAttribute;
+ if (authorizationFilterAttribute != null)
+ {
+ filters.Add(new AuthorizationFilterAttributeTracer(authorizationFilterAttribute, traceWriter));
+ addedAuthorizationAttributeTracer = true;
+ }
+
+ ExceptionFilterAttribute exceptionFilterAttribute = filter as ExceptionFilterAttribute;
+ if (exceptionFilterAttribute != null)
+ {
+ filters.Add(new ExceptionFilterAttributeTracer(exceptionFilterAttribute, traceWriter));
+ addedExceptionAttributeTracer = true;
+ }
+
+ // Do not add an IActionFilter tracer if we already added an ActionFilterAttribute tracer
+ IActionFilter actionFilter = filter as IActionFilter;
+ if (actionFilter != null && !addedActionAttributeTracer)
+ {
+ filters.Add(new ActionFilterTracer(actionFilter, traceWriter));
+ }
+
+ // Do not add an IAuthorizationFilter tracer if we already added an AuthorizationFilterAttribute tracer
+ IAuthorizationFilter authorizationFilter = filter as IAuthorizationFilter;
+ if (authorizationFilter != null && !addedAuthorizationAttributeTracer)
+ {
+ filters.Add(new AuthorizationFilterTracer(authorizationFilter, traceWriter));
+ }
+
+ // Do not add an IExceptionFilter tracer if we already added an ExceptoinFilterAttribute tracer
+ IExceptionFilter exceptionFilter = filter as IExceptionFilter;
+ if (exceptionFilter != null && !addedExceptionAttributeTracer)
+ {
+ filters.Add(new ExceptionFilterTracer(exceptionFilter, traceWriter));
+ }
+
+ if (filters.Count == 0)
+ {
+ filters.Add(new FilterTracer(filter, traceWriter));
+ }
+
+ return filters;
+ }
+
+ public static IEnumerable<FilterInfo> CreateFilterTracers(FilterInfo filter, ITraceWriter traceWriter)
+ {
+ IFilter filterInstance = filter.Instance;
+ IEnumerable<IFilter> filterTracers = CreateFilterTracers(filterInstance, traceWriter);
+ List<FilterInfo> filters = new List<FilterInfo>();
+ foreach (IFilter filterTracer in filterTracers)
+ {
+ filters.Add(new FilterInfo(filterTracer, filter.Scope));
+ }
+
+ return filters;
+ }
+
+ public static bool IsFilterTracer(IFilter filter)
+ {
+ return filter is FilterTracer ||
+ filter is ActionFilterAttributeTracer ||
+ filter is AuthorizationFilterAttributeTracer ||
+ filter is ExceptionFilterAttributeTracer;
+ }
+ }
+}
diff --git a/src/System.Web.Http/Tracing/Tracers/FormUrlEncodedMediaTypeFormatterTracer.cs b/src/System.Web.Http/Tracing/Tracers/FormUrlEncodedMediaTypeFormatterTracer.cs
new file mode 100644
index 00000000..0aec7de0
--- /dev/null
+++ b/src/System.Web.Http/Tracing/Tracers/FormUrlEncodedMediaTypeFormatterTracer.cs
@@ -0,0 +1,62 @@
+using System.IO;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Formatting;
+using System.Net.Http.Headers;
+using System.Threading.Tasks;
+
+namespace System.Web.Http.Tracing.Tracers
+{
+ /// <summary>
+ /// Tracer for <see cref="FormUrlEncodedMediaTypeFormatter"/>.
+ /// It is required because users can select formatters by this type.
+ /// </summary>
+ internal class FormUrlEncodedMediaTypeFormatterTracer : FormUrlEncodedMediaTypeFormatter, IFormatterTracer
+ {
+ private MediaTypeFormatterTracer _innerTracer;
+ public FormUrlEncodedMediaTypeFormatterTracer(MediaTypeFormatter innerFormatter, ITraceWriter traceWriter, HttpRequestMessage request)
+ {
+ _innerTracer = new MediaTypeFormatterTracer(innerFormatter, traceWriter, request);
+ }
+
+ HttpRequestMessage IFormatterTracer.Request
+ {
+ get { return _innerTracer.Request; }
+ }
+
+ MediaTypeFormatter IFormatterTracer.InnerFormatter
+ {
+ get { return _innerTracer.InnerFormatter; }
+ }
+
+ public override bool CanReadType(Type type)
+ {
+ return _innerTracer.CanReadType(type);
+ }
+
+ public override bool CanWriteType(Type type)
+ {
+ return _innerTracer.CanWriteType(type);
+ }
+
+ public override MediaTypeFormatter GetPerRequestFormatterInstance(Type type, HttpRequestMessage request, MediaTypeHeaderValue mediaType)
+ {
+ return _innerTracer.GetPerRequestFormatterInstance(type, request, mediaType);
+ }
+
+ public override Task<object> ReadFromStreamAsync(Type type, Stream stream, HttpContentHeaders contentHeaders, IFormatterLogger formatterLogger)
+ {
+ return _innerTracer.ReadFromStreamAsync(type, stream, contentHeaders, formatterLogger);
+ }
+
+ public override Task WriteToStreamAsync(Type type, object value, Stream stream, HttpContentHeaders contentHeaders, TransportContext transportContext)
+ {
+ return _innerTracer.WriteToStreamAsync(type, value, stream, contentHeaders, transportContext);
+ }
+
+ public override void SetDefaultContentHeaders(Type type, HttpContentHeaders headers, string mediaType)
+ {
+ _innerTracer.SetDefaultContentHeaders(type, headers, mediaType);
+ }
+ }
+}
diff --git a/src/System.Web.Http/Tracing/Tracers/FormatterParameterBindingTracer.cs b/src/System.Web.Http/Tracing/Tracers/FormatterParameterBindingTracer.cs
new file mode 100644
index 00000000..66141fa6
--- /dev/null
+++ b/src/System.Web.Http/Tracing/Tracers/FormatterParameterBindingTracer.cs
@@ -0,0 +1,77 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.Net.Http;
+using System.Net.Http.Formatting;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Metadata;
+using System.Web.Http.ModelBinding;
+using System.Web.Http.Properties;
+
+namespace System.Web.Http.Tracing.Tracers
+{
+ /// <summary>
+ /// Tracer to wrap a <see cref="FormatterParameterBinding"/>.
+ /// Its primary purpose is to intercept binding requests so that it can create tracers for the formatters.
+ /// </summary>
+ internal class FormatterParameterBindingTracer : FormatterParameterBinding
+ {
+ private const string ExecuteBindingAsyncMethodName = "ExecuteBindingAsync";
+
+ private FormatterParameterBinding _innerBinding;
+ private ITraceWriter _traceWriter;
+
+ public FormatterParameterBindingTracer(FormatterParameterBinding innerBinding, ITraceWriter traceWriter) : base(innerBinding.Descriptor, innerBinding.Formatters, innerBinding.BodyModelValidator)
+ {
+ _innerBinding = innerBinding;
+ _traceWriter = traceWriter;
+ }
+
+ protected override Task<object> ReadContentAsync(HttpRequestMessage request, Type type, IEnumerable<MediaTypeFormatter> formatters, IFormatterLogger formatterLogger)
+ {
+ // Intercept this method solely to wrap request-knowledgable formatter tracers
+ return base.ReadContentAsync(request, type, CreateFormatterTracers(request, formatters), formatterLogger);
+ }
+
+ public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider, HttpActionContext actionContext, CancellationToken cancellationToken)
+ {
+ return _traceWriter.TraceBeginEndAsync(
+ actionContext.Request,
+ TraceCategories.ModelBindingCategory,
+ TraceLevel.Info,
+ _innerBinding.GetType().Name,
+ ExecuteBindingAsyncMethodName,
+ beginTrace: (tr) =>
+ {
+ tr.Message = Error.Format(SRResources.TraceBeginParameterBind,
+ _innerBinding.Descriptor.ParameterName);
+ },
+
+ execute: () => _innerBinding.ExecuteBindingAsync(metadataProvider, actionContext, cancellationToken),
+
+ endTrace: (tr) =>
+ {
+ string parameterName = _innerBinding.Descriptor.ParameterName;
+ tr.Message = actionContext.ActionArguments.ContainsKey(parameterName)
+ ? Error.Format(SRResources.TraceEndParameterBind, parameterName,
+ FormattingUtilities.ValueToString(actionContext.ActionArguments[parameterName], CultureInfo.CurrentCulture))
+ : Error.Format(SRResources.TraceEndParameterBindNoBind,
+ parameterName);
+ },
+ errorTrace: null);
+ }
+
+ private IEnumerable<MediaTypeFormatter> CreateFormatterTracers(HttpRequestMessage request, IEnumerable<MediaTypeFormatter> formatters)
+ {
+ List<MediaTypeFormatter> formatterTracers = new List<MediaTypeFormatter>();
+ foreach (MediaTypeFormatter formatter in formatters)
+ {
+ formatterTracers.Add(MediaTypeFormatterTracer.CreateTracer(formatter, _traceWriter, request));
+ }
+
+ return formatterTracers;
+ }
+ }
+}
diff --git a/src/System.Web.Http/Tracing/Tracers/HttpActionBindingTracer.cs b/src/System.Web.Http/Tracing/Tracers/HttpActionBindingTracer.cs
new file mode 100644
index 00000000..03fcf2a3
--- /dev/null
+++ b/src/System.Web.Http/Tracing/Tracers/HttpActionBindingTracer.cs
@@ -0,0 +1,58 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Common;
+using System.Web.Http.ModelBinding;
+using System.Web.Http.Properties;
+
+namespace System.Web.Http.Tracing.Tracers
+{
+ public class HttpActionBindingTracer : HttpActionBinding
+ {
+ private const string ExecuteBindingAsyncMethodName = "ExecuteBindingAsync";
+
+ private readonly HttpActionBinding _innerBinding;
+ private readonly ITraceWriter _traceWriter;
+
+ public HttpActionBindingTracer(HttpActionBinding innerBinding, ITraceWriter traceWriter)
+ {
+ _innerBinding = innerBinding;
+ _traceWriter = traceWriter;
+ }
+
+ public override Task ExecuteBindingAsync(Controllers.HttpActionContext actionContext, CancellationToken cancellationToken)
+ {
+ return _traceWriter.TraceBeginEndAsync(
+ actionContext.ControllerContext.Request,
+ TraceCategories.ModelBindingCategory,
+ TraceLevel.Info,
+ _innerBinding.GetType().Name,
+ ExecuteBindingAsyncMethodName,
+ beginTrace: null,
+ execute: () => _innerBinding.ExecuteBindingAsync(actionContext, cancellationToken),
+ endTrace: (tr) =>
+ {
+ if (!actionContext.ModelState.IsValid)
+ {
+ tr.Message = Error.Format(SRResources.TraceModelStateInvalidMessage,
+ FormattingUtilities.ModelStateToString(
+ actionContext.ModelState));
+ }
+ else
+ {
+ if (actionContext.ActionDescriptor.GetParameters().Count > 0)
+ {
+ tr.Message = Error.Format(SRResources.TraceValidModelState,
+ FormattingUtilities.ActionArgumentsToString(
+ actionContext.ActionArguments));
+ }
+ }
+ },
+ errorTrace: null);
+ }
+ }
+}
diff --git a/src/System.Web.Http/Tracing/Tracers/HttpActionDescriptorTracer.cs b/src/System.Web.Http/Tracing/Tracers/HttpActionDescriptorTracer.cs
new file mode 100644
index 00000000..f7eee337
--- /dev/null
+++ b/src/System.Web.Http/Tracing/Tracers/HttpActionDescriptorTracer.cs
@@ -0,0 +1,123 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Globalization;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Filters;
+using System.Web.Http.Properties;
+
+namespace System.Web.Http.Tracing.Tracers
+{
+ /// <summary>
+ /// Tracer for <see cref="HttpActionDescriptor"/>.
+ /// </summary>
+ internal class HttpActionDescriptorTracer : HttpActionDescriptor
+ {
+ private const string ExecuteMethodName = "Execute";
+
+ private readonly HttpActionDescriptor _innerDescriptor;
+ private readonly ITraceWriter _traceWriter;
+
+ public HttpActionDescriptorTracer(HttpControllerContext controllerContext, HttpActionDescriptor innerDescriptor, ITraceWriter traceWriter) : base(controllerContext.ControllerDescriptor)
+ {
+ _innerDescriptor = innerDescriptor;
+ _traceWriter = traceWriter;
+ }
+
+ public override string ActionName
+ {
+ get { return _innerDescriptor.ActionName; }
+ }
+
+ public override Type ReturnType
+ {
+ get { return _innerDescriptor.ReturnType; }
+ }
+
+ public override object Execute(HttpControllerContext controllerContext, IDictionary<string, object> arguments)
+ {
+ object result = null;
+
+ _traceWriter.TraceBeginEnd(
+ controllerContext.Request,
+ TraceCategories.ActionCategory,
+ TraceLevel.Info,
+ _innerDescriptor.GetType().Name,
+ ExecuteMethodName,
+ beginTrace: (tr) =>
+ {
+ tr.Message = Error.Format(SRResources.TraceInvokingAction,
+ FormattingUtilities.ActionInvokeToString(this.ActionName, arguments));
+ },
+ execute: () =>
+ {
+ result = _innerDescriptor.Execute(controllerContext, arguments);
+ },
+ endTrace: (tr) =>
+ {
+ tr.Message = Error.Format(SRResources.TraceActionReturnValue,
+ FormattingUtilities.ValueToString(result, CultureInfo.CurrentCulture));
+ },
+ errorTrace: null);
+
+ return result;
+ }
+
+ public override IEnumerable<T> GetCustomAttributes<T>()
+ {
+ return _innerDescriptor.GetCustomAttributes<T>();
+ }
+
+ public override IEnumerable<IFilter> GetFilters()
+ {
+ List<IFilter> filters = new List<IFilter>(_innerDescriptor.GetFilters());
+ List<IFilter> returnFilters = new List<IFilter>(filters.Count);
+ for (int i = 0; i < filters.Count; i++)
+ {
+ if (FilterTracer.IsFilterTracer(filters[i]))
+ {
+ returnFilters.Add(filters[i]);
+ }
+ else
+ {
+ IEnumerable<IFilter> filterTracers = FilterTracer.CreateFilterTracers(filters[i], _traceWriter);
+ foreach (IFilter filterTracer in filterTracers)
+ {
+ returnFilters.Add(filterTracer);
+ }
+ }
+ }
+
+ return returnFilters;
+ }
+
+ public override Collection<FilterInfo> GetFilterPipeline()
+ {
+ List<FilterInfo> filters = new List<FilterInfo>(_innerDescriptor.GetFilterPipeline());
+ List<FilterInfo> returnFilters = new List<FilterInfo>(filters.Count);
+ for (int i = 0; i < filters.Count; i++)
+ {
+ // If this filter has been wrapped already, use as is
+ if (FilterTracer.IsFilterTracer(filters[i].Instance))
+ {
+ returnFilters.Add(filters[i]);
+ }
+ else
+ {
+ IEnumerable<FilterInfo> filterTracers = FilterTracer.CreateFilterTracers(filters[i], _traceWriter);
+ foreach (FilterInfo filterTracer in filterTracers)
+ {
+ returnFilters.Add(filterTracer);
+ }
+ }
+ }
+
+ return new Collection<FilterInfo>(returnFilters);
+ }
+
+ public override Collection<HttpParameterDescriptor> GetParameters()
+ {
+ return _innerDescriptor.GetParameters();
+ }
+ }
+}
diff --git a/src/System.Web.Http/Tracing/Tracers/HttpActionInvokerTracer.cs b/src/System.Web.Http/Tracing/Tracers/HttpActionInvokerTracer.cs
new file mode 100644
index 00000000..346f58a8
--- /dev/null
+++ b/src/System.Web.Http/Tracing/Tracers/HttpActionInvokerTracer.cs
@@ -0,0 +1,62 @@
+using System.Globalization;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Properties;
+
+namespace System.Web.Http.Tracing.Tracers
+{
+ /// <summary>
+ /// Tracer for <see cref="IHttpActionInvoker"/>.
+ /// </summary>
+ internal class HttpActionInvokerTracer : IHttpActionInvoker
+ {
+ private const string InvokeActionAsyncMethodName = "InvokeActionAsync";
+
+ private readonly IHttpActionInvoker _innerInvoker;
+ private readonly ITraceWriter _traceWriter;
+
+ public HttpActionInvokerTracer(IHttpActionInvoker innerInvoker, ITraceWriter traceWriter)
+ {
+ _innerInvoker = innerInvoker;
+ _traceWriter = traceWriter;
+ }
+
+ Task<HttpResponseMessage> IHttpActionInvoker.InvokeActionAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
+ {
+ if (actionContext == null)
+ {
+ throw new ArgumentNullException("actionContext");
+ }
+
+ return _traceWriter.TraceBeginEndAsync<HttpResponseMessage>(
+ actionContext.ControllerContext.Request,
+ TraceCategories.ActionCategory,
+ TraceLevel.Info,
+ _innerInvoker.GetType().Name,
+ InvokeActionAsyncMethodName,
+
+ beginTrace: (tr) =>
+ {
+ tr.Message = Error.Format(
+ SRResources.TraceActionInvokeMessage,
+ FormattingUtilities.ActionInvokeToString(actionContext));
+ },
+
+ execute: () => (Task<HttpResponseMessage>)_innerInvoker.InvokeActionAsync(actionContext, cancellationToken),
+
+ endTrace: (tr, result) =>
+ {
+ HttpResponseMessage response = result;
+ if (response != null)
+ {
+ tr.Status = response.StatusCode;
+ }
+ },
+
+ errorTrace: null);
+ }
+ }
+}
diff --git a/src/System.Web.Http/Tracing/Tracers/HttpActionSelectorTracer.cs b/src/System.Web.Http/Tracing/Tracers/HttpActionSelectorTracer.cs
new file mode 100644
index 00000000..2f836012
--- /dev/null
+++ b/src/System.Web.Http/Tracing/Tracers/HttpActionSelectorTracer.cs
@@ -0,0 +1,55 @@
+using System.Globalization;
+using System.Linq;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Properties;
+
+namespace System.Web.Http.Tracing.Tracers
+{
+ /// <summary>
+ /// Tracer for <see cref="IHttpActionSelector"/>.
+ /// </summary>
+ internal class HttpActionSelectorTracer : IHttpActionSelector
+ {
+ private const string SelectActionMethodName = "SelectAction";
+
+ private readonly IHttpActionSelector _innerSelector;
+ private readonly ITraceWriter _traceWriter;
+
+ public HttpActionSelectorTracer(IHttpActionSelector innerSelector, ITraceWriter traceWriter)
+ {
+ _innerSelector = innerSelector;
+ _traceWriter = traceWriter;
+ }
+
+ public ILookup<string, HttpActionDescriptor> GetActionMapping(HttpControllerDescriptor controllerDescriptor)
+ {
+ return _innerSelector.GetActionMapping(controllerDescriptor);
+ }
+
+ HttpActionDescriptor IHttpActionSelector.SelectAction(HttpControllerContext controllerContext)
+ {
+ HttpActionDescriptor actionDescriptor = null;
+
+ _traceWriter.TraceBeginEnd(
+ controllerContext.Request,
+ TraceCategories.ActionCategory,
+ TraceLevel.Info,
+ _innerSelector.GetType().Name,
+ SelectActionMethodName,
+ beginTrace: null,
+ execute: () => { actionDescriptor = _innerSelector.SelectAction(controllerContext); },
+ endTrace: (tr) =>
+ {
+ tr.Message = Error.Format(
+ SRResources.TraceActionSelectedMessage,
+ FormattingUtilities.ActionDescriptorToString(actionDescriptor));
+ },
+
+ errorTrace: null);
+
+ // Intercept returned HttpActionDescriptor with a tracing version
+ return actionDescriptor == null ? null : new HttpActionDescriptorTracer(controllerContext, actionDescriptor, _traceWriter);
+ }
+ }
+}
diff --git a/src/System.Web.Http/Tracing/Tracers/HttpControllerActivatorTracer.cs b/src/System.Web.Http/Tracing/Tracers/HttpControllerActivatorTracer.cs
new file mode 100644
index 00000000..56e41691
--- /dev/null
+++ b/src/System.Web.Http/Tracing/Tracers/HttpControllerActivatorTracer.cs
@@ -0,0 +1,53 @@
+using System.Web.Http.Controllers;
+using System.Web.Http.Dispatcher;
+using System.Web.Http.Properties;
+
+namespace System.Web.Http.Tracing.Tracers
+{
+ /// <summary>
+ /// Tracer for <see cref="IHttpControllerActivator"/>.
+ /// </summary>
+ internal class HttpControllerActivatorTracer : IHttpControllerActivator
+ {
+ private const string CreateMethodName = "Create";
+
+ private readonly IHttpControllerActivator _innerActivator;
+ private readonly ITraceWriter _traceWriter;
+
+ public HttpControllerActivatorTracer(IHttpControllerActivator innerActivator, ITraceWriter traceWriter)
+ {
+ _innerActivator = innerActivator;
+ _traceWriter = traceWriter;
+ }
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "disposable controller is later released in ReleaseController")]
+ IHttpController IHttpControllerActivator.Create(HttpControllerContext controllerContext, Type controllerType)
+ {
+ IHttpController controller = null;
+
+ _traceWriter.TraceBeginEnd(
+ controllerContext.Request,
+ TraceCategories.ControllersCategory,
+ TraceLevel.Info,
+ _innerActivator.GetType().Name,
+ CreateMethodName,
+ beginTrace: null,
+ execute: () =>
+ {
+ controller = _innerActivator.Create(controllerContext, controllerType);
+ },
+ endTrace: (tr) =>
+ {
+ tr.Message = controller == null ? SRResources.TraceNoneObjectMessage : controller.GetType().FullName;
+ },
+ errorTrace: null);
+
+ if (controller != null && !(controller is HttpControllerTracer))
+ {
+ controller = new HttpControllerTracer(controller, _traceWriter);
+ }
+
+ return controller;
+ }
+ }
+}
diff --git a/src/System.Web.Http/Tracing/Tracers/HttpControllerFactoryTracer.cs b/src/System.Web.Http/Tracing/Tracers/HttpControllerFactoryTracer.cs
new file mode 100644
index 00000000..56aa2c23
--- /dev/null
+++ b/src/System.Web.Http/Tracing/Tracers/HttpControllerFactoryTracer.cs
@@ -0,0 +1,90 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Dispatcher;
+using System.Web.Http.Properties;
+
+namespace System.Web.Http.Tracing.Tracers
+{
+ /// <summary>
+ /// Tracer for <see cref="IHttpControllerFactory"/>.
+ /// </summary>
+ internal class HttpControllerFactoryTracer : IHttpControllerFactory
+ {
+ private const string CreateControllerMethodName = "CreateController";
+ private const string ReleaseControllerMethodName = "ReleaseController";
+
+ private readonly IHttpControllerFactory _innerFactory;
+ private readonly ITraceWriter _traceWriter;
+
+ public HttpControllerFactoryTracer(IHttpControllerFactory innerFactory, ITraceWriter traceWriter)
+ {
+ _innerFactory = innerFactory;
+ _traceWriter = traceWriter;
+ }
+
+ IHttpController IHttpControllerFactory.CreateController(HttpControllerContext controllerContext, string controllerName)
+ {
+ IHttpController controller = null;
+
+ _traceWriter.TraceBeginEnd(
+ controllerContext.Request,
+ TraceCategories.ControllersCategory,
+ TraceLevel.Info,
+ _innerFactory.GetType().Name,
+ CreateControllerMethodName,
+ beginTrace: (tr) =>
+ {
+ tr.Message = Error.Format(
+ SRResources.TraceControllerNameAndRouteMessage,
+ controllerName,
+ FormattingUtilities.RouteToString(controllerContext.RouteData));
+ },
+ execute: () =>
+ {
+ controller = _innerFactory.CreateController(controllerContext, controllerName);
+ },
+ endTrace: (tr) =>
+ {
+ tr.Message = controller == null
+ ? SRResources.TraceNoneObjectMessage
+ : HttpControllerTracer.ActualControllerType(controller).FullName;
+ },
+ errorTrace: null);
+
+ if (controller != null && !(controller is HttpControllerTracer))
+ {
+ controller = new HttpControllerTracer(controller, _traceWriter);
+ }
+
+ return controller;
+ }
+
+ IDictionary<string, HttpControllerDescriptor> IHttpControllerFactory.GetControllerMapping()
+ {
+ return _innerFactory.GetControllerMapping();
+ }
+
+ void IHttpControllerFactory.ReleaseController(HttpControllerContext controllerContext, IHttpController controller)
+ {
+ _traceWriter.TraceBeginEnd(
+ controllerContext.Request,
+ TraceCategories.ControllersCategory,
+ TraceLevel.Info,
+ _innerFactory.GetType().Name,
+ ReleaseControllerMethodName,
+ beginTrace: (tr) =>
+ {
+ tr.Message = HttpControllerTracer.ActualControllerType(controller).FullName;
+ },
+ execute: () =>
+ {
+ IHttpController actualController = HttpControllerTracer.ActualController(controller);
+ _innerFactory.ReleaseController(controllerContext, actualController);
+ },
+ endTrace: null,
+ errorTrace: null);
+ }
+ }
+}
diff --git a/src/System.Web.Http/Tracing/Tracers/HttpControllerTracer.cs b/src/System.Web.Http/Tracing/Tracers/HttpControllerTracer.cs
new file mode 100644
index 00000000..a714750b
--- /dev/null
+++ b/src/System.Web.Http/Tracing/Tracers/HttpControllerTracer.cs
@@ -0,0 +1,60 @@
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Controllers;
+
+namespace System.Web.Http.Tracing.Tracers
+{
+ /// <summary>
+ /// Tracer for <see cref="IHttpController"/>.
+ /// </summary>
+ internal class HttpControllerTracer : IHttpController
+ {
+ private const string ExecuteAsyncMethodName = "ExecuteAsync";
+
+ private readonly IHttpController _innerController;
+ private readonly ITraceWriter _traceWriter;
+
+ public HttpControllerTracer(IHttpController innerController, ITraceWriter traceWriter)
+ {
+ _innerController = innerController;
+ _traceWriter = traceWriter;
+ }
+
+ Task<HttpResponseMessage> IHttpController.ExecuteAsync(HttpControllerContext controllerContext, CancellationToken cancellationToken)
+ {
+ return _traceWriter.TraceBeginEndAsync<HttpResponseMessage>(
+ controllerContext.Request,
+ TraceCategories.ControllersCategory,
+ TraceLevel.Info,
+ _innerController.GetType().Name,
+ ExecuteAsyncMethodName,
+ beginTrace: null,
+ execute: () =>
+ {
+ // Critical to allow wrapped controller to have itself in ControllerContext
+ controllerContext.Controller = ActualController(controllerContext.Controller);
+ return _innerController.ExecuteAsync(controllerContext, cancellationToken);
+ },
+ endTrace: (tr, response) =>
+ {
+ if (response != null)
+ {
+ tr.Status = response.StatusCode;
+ }
+ },
+ errorTrace: null);
+ }
+
+ public static IHttpController ActualController(IHttpController controller)
+ {
+ HttpControllerTracer tracer = controller as HttpControllerTracer;
+ return tracer == null ? controller : tracer._innerController;
+ }
+
+ public static Type ActualControllerType(IHttpController controller)
+ {
+ return ActualController(controller).GetType();
+ }
+ }
+}
diff --git a/src/System.Web.Http/Tracing/Tracers/HttpParameterBindingTracer.cs b/src/System.Web.Http/Tracing/Tracers/HttpParameterBindingTracer.cs
new file mode 100644
index 00000000..3ea8fabc
--- /dev/null
+++ b/src/System.Web.Http/Tracing/Tracers/HttpParameterBindingTracer.cs
@@ -0,0 +1,87 @@
+using System.Globalization;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Metadata;
+using System.Web.Http.ModelBinding;
+using System.Web.Http.Properties;
+
+namespace System.Web.Http.Tracing.Tracers
+{
+ /// <summary>
+ /// Tracer to wrap an <see cref="HttpParameterBinding"/>.
+ /// Its primary purpose is to monitor <see cref="ExecuteBindingAsync"/>.
+ /// </summary>
+ internal class HttpParameterBindingTracer : HttpParameterBinding
+ {
+ private const string ExecuteBindingAsyncMethodName = "ExecuteBindingAsync";
+
+ public HttpParameterBindingTracer(HttpParameterBinding innerBinding, ITraceWriter traceWriter) : base(innerBinding.Descriptor)
+ {
+ InnerBinding = innerBinding;
+ TraceWriter = traceWriter;
+ }
+
+ protected HttpParameterBinding InnerBinding { get; private set; }
+
+ protected ITraceWriter TraceWriter { get; private set; }
+
+ public override string ErrorMessage
+ {
+ get
+ {
+ return InnerBinding.ErrorMessage;
+ }
+ }
+
+ public override bool WillReadBody
+ {
+ get
+ {
+ return InnerBinding.WillReadBody;
+ }
+ }
+
+ public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider, HttpActionContext actionContext, CancellationToken cancellationToken)
+ {
+ return TraceWriter.TraceBeginEndAsync(
+ actionContext.Request,
+ TraceCategories.ModelBindingCategory,
+ TraceLevel.Info,
+ InnerBinding.GetType().Name,
+ ExecuteBindingAsyncMethodName,
+ beginTrace: (tr) =>
+ {
+ tr.Message = Error.Format(SRResources.TraceBeginParameterBind,
+ InnerBinding.Descriptor.ParameterName);
+ },
+
+ execute: () => InnerBinding.ExecuteBindingAsync(metadataProvider, actionContext, cancellationToken),
+
+ endTrace: (tr) =>
+ {
+ string parameterName = InnerBinding.Descriptor.ParameterName;
+
+ // Model binding error for this parameter shows the error
+ if (!actionContext.ModelState.IsValid && actionContext.ModelState.ContainsKey(parameterName))
+ {
+ tr.Message = Error.Format(SRResources.TraceModelStateInvalidMessage,
+ FormattingUtilities.ModelStateToString(
+ actionContext.ModelState));
+ }
+ else
+ {
+ tr.Message = actionContext.ActionArguments.ContainsKey(parameterName)
+ ? Error.Format(SRResources.TraceEndParameterBind, parameterName,
+ FormattingUtilities.ValueToString(
+ actionContext.ActionArguments[parameterName],
+ CultureInfo.CurrentCulture))
+ : Error.Format(SRResources.TraceEndParameterBindNoBind,
+ parameterName);
+ }
+ },
+ errorTrace: null);
+ }
+ }
+}
diff --git a/src/System.Web.Http/Tracing/Tracers/JsonMediaTypeFormatterTracer.cs b/src/System.Web.Http/Tracing/Tracers/JsonMediaTypeFormatterTracer.cs
new file mode 100644
index 00000000..5b29798f
--- /dev/null
+++ b/src/System.Web.Http/Tracing/Tracers/JsonMediaTypeFormatterTracer.cs
@@ -0,0 +1,62 @@
+using System.IO;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Formatting;
+using System.Net.Http.Headers;
+using System.Threading.Tasks;
+
+namespace System.Web.Http.Tracing.Tracers
+{
+ /// <summary>
+ /// Tracer for <see cref="JsonMediaTypeFormatter"/>.
+ /// It is required because users can select formatters by this type.
+ /// </summary>
+ internal class JsonMediaTypeFormatterTracer : JsonMediaTypeFormatter, IFormatterTracer
+ {
+ private MediaTypeFormatterTracer _innerTracer;
+ public JsonMediaTypeFormatterTracer(MediaTypeFormatter innerFormatter, ITraceWriter traceWriter, HttpRequestMessage request)
+ {
+ _innerTracer = new MediaTypeFormatterTracer(innerFormatter, traceWriter, request);
+ }
+
+ HttpRequestMessage IFormatterTracer.Request
+ {
+ get { return _innerTracer.Request; }
+ }
+
+ MediaTypeFormatter IFormatterTracer.InnerFormatter
+ {
+ get { return _innerTracer.InnerFormatter; }
+ }
+
+ public override bool CanReadType(Type type)
+ {
+ return _innerTracer.CanReadType(type);
+ }
+
+ public override bool CanWriteType(Type type)
+ {
+ return _innerTracer.CanWriteType(type);
+ }
+
+ public override MediaTypeFormatter GetPerRequestFormatterInstance(Type type, HttpRequestMessage request, MediaTypeHeaderValue mediaType)
+ {
+ return _innerTracer.GetPerRequestFormatterInstance(type, request, mediaType);
+ }
+
+ public override Task<object> ReadFromStreamAsync(Type type, Stream stream, HttpContentHeaders contentHeaders, IFormatterLogger formatterLogger)
+ {
+ return _innerTracer.ReadFromStreamAsync(type, stream, contentHeaders, formatterLogger);
+ }
+
+ public override Task WriteToStreamAsync(Type type, object value, Stream stream, HttpContentHeaders contentHeaders, TransportContext transportContext)
+ {
+ return _innerTracer.WriteToStreamAsync(type, value, stream, contentHeaders, transportContext);
+ }
+
+ public override void SetDefaultContentHeaders(Type type, HttpContentHeaders headers, string mediaType)
+ {
+ _innerTracer.SetDefaultContentHeaders(type, headers, mediaType);
+ }
+ }
+}
diff --git a/src/System.Web.Http/Tracing/Tracers/MediaTypeFormatterTracer.cs b/src/System.Web.Http/Tracing/Tracers/MediaTypeFormatterTracer.cs
new file mode 100644
index 00000000..f874c5db
--- /dev/null
+++ b/src/System.Web.Http/Tracing/Tracers/MediaTypeFormatterTracer.cs
@@ -0,0 +1,233 @@
+using System.Globalization;
+using System.IO;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Formatting;
+using System.Net.Http.Headers;
+using System.Threading.Tasks;
+using System.Web.Http.Common;
+using System.Web.Http.Properties;
+
+namespace System.Web.Http.Tracing.Tracers
+{
+ /// <summary>
+ /// Tracer to monitor <see cref="MediaTypeFormatter"/> instances.
+ /// </summary>
+ internal class MediaTypeFormatterTracer : MediaTypeFormatter, IFormatterTracer
+ {
+ private const string ReadFromStreamAsyncMethodName = "ReadFromStreamAsync";
+ private const string WriteToStreamAsyncMethodName = "WriteToStreamAsync";
+ private const string GetPerRequestFormatterInstanceMethodName = "GetPerRequestFormatterInstance";
+
+ public MediaTypeFormatterTracer(MediaTypeFormatter innerFormatter, ITraceWriter traceWriter, HttpRequestMessage request)
+ {
+ InnerFormatter = innerFormatter;
+ TraceWriter = traceWriter;
+ Request = request;
+ }
+
+ public MediaTypeFormatter InnerFormatter { get; private set; }
+
+ public ITraceWriter TraceWriter { get; set; }
+
+ public HttpRequestMessage Request { get; set; }
+
+ public static MediaTypeFormatter ActualMediaTypeFormatter(MediaTypeFormatter formatter)
+ {
+ IFormatterTracer tracer = formatter as IFormatterTracer;
+ return tracer == null ? formatter : tracer.InnerFormatter;
+ }
+
+ public static MediaTypeFormatter CreateTracer(MediaTypeFormatter formatter, ITraceWriter traceWriter, HttpRequestMessage request)
+ {
+ // If we have been asked to wrap a tracer around a formatter, it could be
+ // already wrapped, and there is nothing to do. But if we see it is a tracer
+ // that is not associated with a request, we wrap it into a new tracer that
+ // does have a request. The only formatter tracers without requests are the
+ // ones in the default MediaTypeFormatterCollection in the HttpConfiguration.
+ IFormatterTracer formatterTracer = formatter as IFormatterTracer;
+ if (formatterTracer != null)
+ {
+ if (formatterTracer.Request == request)
+ {
+ return formatter;
+ }
+
+ formatter = formatterTracer.InnerFormatter;
+ }
+
+ MediaTypeFormatter tracer = null;
+
+ // We special-case Xml, Json and FormUrlEncoded formatters because we expect to be able
+ // to find them with IsAssignableFrom in the MediaTypeFormatterCollection.
+ if (formatter is XmlMediaTypeFormatter)
+ {
+ tracer = new XmlMediaTypeFormatterTracer(formatter, traceWriter, request);
+ }
+ else if (formatter is JsonMediaTypeFormatter)
+ {
+ tracer = new JsonMediaTypeFormatterTracer(formatter, traceWriter, request);
+ }
+ else if (formatter is FormUrlEncodedMediaTypeFormatter)
+ {
+ tracer = new FormUrlEncodedMediaTypeFormatterTracer(formatter, traceWriter, request);
+ }
+ else if (formatter is BufferedMediaTypeFormatter)
+ {
+ tracer = new BufferedMediaTypeFormatterTracer(formatter, traceWriter, request);
+ }
+ else
+ {
+ tracer = new MediaTypeFormatterTracer(formatter, traceWriter, request);
+ }
+
+ // Copy SupportedMediaTypes and MediaTypeMappings because they are publically visible
+ tracer.SupportedMediaTypes.Clear();
+ foreach (MediaTypeHeaderValue mediaType in formatter.SupportedMediaTypes)
+ {
+ tracer.SupportedMediaTypes.Add(mediaType);
+ }
+
+ tracer.MediaTypeMappings.Clear();
+ foreach (MediaTypeMapping mapping in formatter.MediaTypeMappings)
+ {
+ tracer.MediaTypeMappings.Add(mapping);
+ }
+
+ return tracer;
+ }
+
+ public override MediaTypeFormatter GetPerRequestFormatterInstance(Type type, HttpRequestMessage request, MediaTypeHeaderValue mediaType)
+ {
+ MediaTypeFormatter formatter = null;
+
+ TraceWriter.TraceBeginEnd(
+ request,
+ TraceCategories.FormattingCategory,
+ TraceLevel.Info,
+ InnerFormatter.GetType().Name,
+ GetPerRequestFormatterInstanceMethodName,
+ beginTrace: (tr) =>
+ {
+ tr.Message = Error.Format(
+ SRResources.TraceGetPerRequestFormatterMessage,
+ InnerFormatter.GetType().Name,
+ type.Name,
+ mediaType);
+ },
+ execute: () => { formatter = InnerFormatter.GetPerRequestFormatterInstance(type, request, mediaType); },
+ endTrace: (tr) =>
+ {
+ if (formatter == null)
+ {
+ tr.Message = SRResources.TraceGetPerRequestNullFormatterEndMessage;
+ }
+ else
+ {
+ string formatMessage =
+ Object.ReferenceEquals(MediaTypeFormatterTracer.ActualMediaTypeFormatter(formatter),
+ InnerFormatter)
+ ? SRResources.TraceGetPerRequestFormatterEndMessage
+ : SRResources.TraceGetPerRequestFormatterEndMessageNew;
+
+ tr.Message = Error.Format(formatMessage, formatter.GetType().Name);
+ }
+ },
+ errorTrace: null);
+
+ if (formatter != null && !(formatter is IFormatterTracer))
+ {
+ formatter = MediaTypeFormatterTracer.CreateTracer(formatter, TraceWriter, request);
+ }
+
+ return formatter;
+ }
+
+ public override bool CanReadType(Type type)
+ {
+ return InnerFormatter.CanReadType(type);
+ }
+
+ public override bool CanWriteType(Type type)
+ {
+ return InnerFormatter.CanWriteType(type);
+ }
+
+ public override bool Equals(object obj)
+ {
+ return InnerFormatter.Equals(obj);
+ }
+
+ public override int GetHashCode()
+ {
+ return InnerFormatter.GetHashCode();
+ }
+
+ public override void SetDefaultContentHeaders(Type type, HttpContentHeaders headers, string mediaType)
+ {
+ InnerFormatter.SetDefaultContentHeaders(type, headers, mediaType);
+ }
+
+ public override string ToString()
+ {
+ return InnerFormatter.ToString();
+ }
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "Tracing layer needs to observer all Task completion paths")]
+ public override Task<object> ReadFromStreamAsync(Type type, Stream stream, HttpContentHeaders contentHeaders, IFormatterLogger formatterLogger)
+ {
+ MediaTypeHeaderValue contentType = contentHeaders == null ? null : contentHeaders.ContentType;
+
+ return TraceWriter.TraceBeginEndAsync<object>(
+ Request,
+ TraceCategories.FormattingCategory,
+ TraceLevel.Info,
+ InnerFormatter.GetType().Name,
+ ReadFromStreamAsyncMethodName,
+ beginTrace: (tr) =>
+ {
+ tr.Message = Error.Format(
+ SRResources.TraceReadFromStreamMessage,
+ type.Name,
+ contentType == null ? SRResources.TraceNoneObjectMessage : contentType.ToString());
+ },
+
+ execute: () => InnerFormatter.ReadFromStreamAsync(type, stream, contentHeaders, formatterLogger),
+
+ endTrace: (tr, value) =>
+ {
+ tr.Message = Error.Format(
+ SRResources.TraceReadFromStreamValueMessage,
+ FormattingUtilities.ValueToString(value, CultureInfo.CurrentCulture));
+ },
+
+ errorTrace: null);
+ }
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "Tracing layer needs to observer all Task completion paths")]
+ public override Task WriteToStreamAsync(Type type, object value, Stream stream, HttpContentHeaders contentHeaders, TransportContext transportContext)
+ {
+ MediaTypeHeaderValue contentType = contentHeaders == null
+ ? null
+ : contentHeaders.ContentType;
+
+ return TraceWriter.TraceBeginEndAsync(
+ Request,
+ TraceCategories.FormattingCategory,
+ TraceLevel.Info,
+ InnerFormatter.GetType().Name,
+ WriteToStreamAsyncMethodName,
+ beginTrace: (tr) =>
+ {
+ tr.Message = Error.Format(
+ SRResources.TraceWriteToStreamMessage,
+ FormattingUtilities.ValueToString(value, CultureInfo.CurrentCulture),
+ type.Name,
+ contentType == null ? SRResources.TraceNoneObjectMessage : contentType.ToString());
+ },
+ execute: () => InnerFormatter.WriteToStreamAsync(type, value, stream, contentHeaders, transportContext),
+ endTrace: null,
+ errorTrace: null);
+ }
+ }
+}
diff --git a/src/System.Web.Http/Tracing/Tracers/MessageHandlerTracer.cs b/src/System.Web.Http/Tracing/Tracers/MessageHandlerTracer.cs
new file mode 100644
index 00000000..963519ff
--- /dev/null
+++ b/src/System.Web.Http/Tracing/Tracers/MessageHandlerTracer.cs
@@ -0,0 +1,44 @@
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace System.Web.Http.Tracing.Tracers
+{
+ /// <summary>
+ /// Tracer to wrap a <see cref="DelegatingHandler"/>.
+ /// </summary>
+ internal class MessageHandlerTracer : DelegatingHandler
+ {
+ private const string SendAsyncMethodName = "SendAsync";
+
+ private readonly DelegatingHandler _innerHandler;
+ private readonly ITraceWriter _traceWriter;
+
+ public MessageHandlerTracer(DelegatingHandler innerHandler, ITraceWriter traceWriter)
+ {
+ _innerHandler = innerHandler;
+ _traceWriter = traceWriter;
+ }
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "Tracing layer needs to observer all Task completion paths")]
+ protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ return _traceWriter.TraceBeginEndAsync<HttpResponseMessage>(
+ request,
+ TraceCategories.MessageHandlersCategory,
+ TraceLevel.Info,
+ _innerHandler.GetType().Name,
+ SendAsyncMethodName,
+ beginTrace: null,
+ execute: () => base.SendAsync(request, cancellationToken),
+ endTrace: (tr, response) =>
+ {
+ if (response != null)
+ {
+ tr.Status = response.StatusCode;
+ }
+ },
+ errorTrace: null);
+ }
+ }
+}
diff --git a/src/System.Web.Http/Tracing/Tracers/RequestMessageHandlerTracer.cs b/src/System.Web.Http/Tracing/Tracers/RequestMessageHandlerTracer.cs
new file mode 100644
index 00000000..c7a85d7f
--- /dev/null
+++ b/src/System.Web.Http/Tracing/Tracers/RequestMessageHandlerTracer.cs
@@ -0,0 +1,71 @@
+using System.Globalization;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Common;
+using System.Web.Http.Properties;
+
+namespace System.Web.Http.Tracing.Tracers
+{
+ /// <summary>
+ /// Internal <see cref="DelegatingHandler"/> that executes before and after all of the installed message handlers.
+ /// The begin trace of this handler is the first trace for the request.
+ /// </summary>
+ internal class RequestMessageHandlerTracer : DelegatingHandler
+ {
+ private readonly ITraceWriter _traceWriter;
+
+ public RequestMessageHandlerTracer(ITraceWriter traceWriter)
+ {
+ _traceWriter = traceWriter;
+ }
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "Tracing layer needs to observer all Task completion paths")]
+ protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ return _traceWriter.TraceBeginEndAsync<HttpResponseMessage>(
+ request,
+ TraceCategories.RequestCategory,
+ TraceLevel.Info,
+ string.Empty,
+ string.Empty,
+ beginTrace: (tr) =>
+ {
+ tr.Message = request.RequestUri == null ? SRResources.TraceNoneObjectMessage : request.RequestUri.ToString();
+ },
+
+ execute: () => base.SendAsync(request, cancellationToken),
+
+ endTrace: (tr, response) =>
+ {
+ MediaTypeHeaderValue contentType = response == null
+ ? null
+ : response.Content == null
+ ? null
+ : response.Content.Headers.ContentType;
+
+ long? contentLength = response == null
+ ? null
+ : response.Content == null
+ ? null
+ : response.Content.Headers.ContentLength;
+
+ if (response != null)
+ {
+ tr.Status = response.StatusCode;
+ }
+
+ tr.Message =
+ Error.Format(SRResources.TraceRequestCompleteMessage,
+ contentType == null
+ ? SRResources.TraceNoneObjectMessage
+ : contentType.ToString(),
+ contentLength.HasValue
+ ? contentLength.Value.ToString(CultureInfo.CurrentCulture)
+ : SRResources.TraceUnknownMessage);
+ },
+ errorTrace: null);
+ }
+ }
+}
diff --git a/src/System.Web.Http/Tracing/Tracers/XmlMediaTypeFormatterTracer.cs b/src/System.Web.Http/Tracing/Tracers/XmlMediaTypeFormatterTracer.cs
new file mode 100644
index 00000000..857dd4c9
--- /dev/null
+++ b/src/System.Web.Http/Tracing/Tracers/XmlMediaTypeFormatterTracer.cs
@@ -0,0 +1,63 @@
+using System.IO;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Formatting;
+using System.Net.Http.Headers;
+using System.Threading.Tasks;
+
+namespace System.Web.Http.Tracing.Tracers
+{
+ /// <summary>
+ /// Tracer for <see cref="XmlMediaTypeFormatter"/>.
+ /// It is required because users can select formatters by this type.
+ /// </summary>
+ internal class XmlMediaTypeFormatterTracer : XmlMediaTypeFormatter, IFormatterTracer
+ {
+ private MediaTypeFormatterTracer _innerTracer;
+
+ public XmlMediaTypeFormatterTracer(MediaTypeFormatter innerFormatter, ITraceWriter traceWriter, HttpRequestMessage request)
+ {
+ _innerTracer = new MediaTypeFormatterTracer(innerFormatter, traceWriter, request);
+ }
+
+ HttpRequestMessage IFormatterTracer.Request
+ {
+ get { return _innerTracer.Request; }
+ }
+
+ MediaTypeFormatter IFormatterTracer.InnerFormatter
+ {
+ get { return _innerTracer.InnerFormatter; }
+ }
+
+ public override bool CanReadType(Type type)
+ {
+ return _innerTracer.CanReadType(type);
+ }
+
+ public override bool CanWriteType(Type type)
+ {
+ return _innerTracer.CanWriteType(type);
+ }
+
+ public override MediaTypeFormatter GetPerRequestFormatterInstance(Type type, HttpRequestMessage request, MediaTypeHeaderValue mediaType)
+ {
+ return _innerTracer.GetPerRequestFormatterInstance(type, request, mediaType);
+ }
+
+ public override Task<object> ReadFromStreamAsync(Type type, Stream stream, HttpContentHeaders contentHeaders, IFormatterLogger formatterLogger)
+ {
+ return _innerTracer.ReadFromStreamAsync(type, stream, contentHeaders, formatterLogger);
+ }
+
+ public override Task WriteToStreamAsync(Type type, object value, Stream stream, HttpContentHeaders contentHeaders, TransportContext transportContext)
+ {
+ return _innerTracer.WriteToStreamAsync(type, value, stream, contentHeaders, transportContext);
+ }
+
+ public override void SetDefaultContentHeaders(Type type, HttpContentHeaders headers, string mediaType)
+ {
+ _innerTracer.SetDefaultContentHeaders(type, headers, mediaType);
+ }
+ }
+}
diff --git a/src/System.Web.Http/Validation/ClientRules/ModelClientValidationRangeRule.cs b/src/System.Web.Http/Validation/ClientRules/ModelClientValidationRangeRule.cs
new file mode 100644
index 00000000..29270cbb
--- /dev/null
+++ b/src/System.Web.Http/Validation/ClientRules/ModelClientValidationRangeRule.cs
@@ -0,0 +1,13 @@
+namespace System.Web.Http.Validation.ClientRules
+{
+ public class ModelClientValidationRangeRule : ModelClientValidationRule
+ {
+ public ModelClientValidationRangeRule(string errorMessage, object minValue, object maxValue)
+ {
+ ErrorMessage = errorMessage;
+ ValidationType = "range";
+ ValidationParameters["min"] = minValue;
+ ValidationParameters["max"] = maxValue;
+ }
+ }
+}
diff --git a/src/System.Web.Http/Validation/ClientRules/ModelClientValidationRegexRule.cs b/src/System.Web.Http/Validation/ClientRules/ModelClientValidationRegexRule.cs
new file mode 100644
index 00000000..f41eecce
--- /dev/null
+++ b/src/System.Web.Http/Validation/ClientRules/ModelClientValidationRegexRule.cs
@@ -0,0 +1,12 @@
+namespace System.Web.Http.Validation.ClientRules
+{
+ public class ModelClientValidationRegexRule : ModelClientValidationRule
+ {
+ public ModelClientValidationRegexRule(string errorMessage, string pattern)
+ {
+ ErrorMessage = errorMessage;
+ ValidationType = "regex";
+ ValidationParameters.Add("pattern", pattern);
+ }
+ }
+}
diff --git a/src/System.Web.Http/Validation/ClientRules/ModelClientValidationRequiredRule.cs b/src/System.Web.Http/Validation/ClientRules/ModelClientValidationRequiredRule.cs
new file mode 100644
index 00000000..48002146
--- /dev/null
+++ b/src/System.Web.Http/Validation/ClientRules/ModelClientValidationRequiredRule.cs
@@ -0,0 +1,11 @@
+namespace System.Web.Http.Validation.ClientRules
+{
+ public class ModelClientValidationRequiredRule : ModelClientValidationRule
+ {
+ public ModelClientValidationRequiredRule(string errorMessage)
+ {
+ ErrorMessage = errorMessage;
+ ValidationType = "required";
+ }
+ }
+}
diff --git a/src/System.Web.Http/Validation/ClientRules/ModelClientValidationStringLengthRule.cs b/src/System.Web.Http/Validation/ClientRules/ModelClientValidationStringLengthRule.cs
new file mode 100644
index 00000000..68351c56
--- /dev/null
+++ b/src/System.Web.Http/Validation/ClientRules/ModelClientValidationStringLengthRule.cs
@@ -0,0 +1,21 @@
+namespace System.Web.Http.Validation.ClientRules
+{
+ public class ModelClientValidationStringLengthRule : ModelClientValidationRule
+ {
+ public ModelClientValidationStringLengthRule(string errorMessage, int minimumLength, int maximumLength)
+ {
+ ErrorMessage = errorMessage;
+ ValidationType = "length";
+
+ if (minimumLength != 0)
+ {
+ ValidationParameters["min"] = minimumLength;
+ }
+
+ if (maximumLength != Int32.MaxValue)
+ {
+ ValidationParameters["max"] = maximumLength;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/Validation/DefaultBodyModelValidator.cs b/src/System.Web.Http/Validation/DefaultBodyModelValidator.cs
new file mode 100644
index 00000000..a62d8b73
--- /dev/null
+++ b/src/System.Web.Http/Validation/DefaultBodyModelValidator.cs
@@ -0,0 +1,161 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics.Contracts;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Internal;
+using System.Web.Http.Metadata;
+using System.Web.Http.ModelBinding;
+
+namespace System.Web.Http.Validation
+{
+ /// <summary>
+ /// Recursively validate an object.
+ /// </summary>
+ public class DefaultBodyModelValidator : IBodyModelValidator
+ {
+ /// <summary>
+ /// Determines whether the <paramref name="model"/> is valid and adds any validation errors to the <paramref name="actionContext"/>'s <see cref="ModelStateDictionary"/>
+ /// </summary>
+ /// <param name="model">The model to be validated.</param>
+ /// <param name="type">The <see cref="Type"/> to use for validation.</param>
+ /// <param name="metadataProvider">The <see cref="ModelMetadataProvider"/> used to provide the model metadata.</param>
+ /// <param name="actionContext">The <see cref="HttpActionContext"/> within which the model is being validated.</param>
+ /// <param name="keyPrefix">The <see cref="string"/> to append to the key for any validation errors.</param>
+ /// <returns><c>true</c>if <paramref name="model"/> is valid, <c>false</c> otherwise.</returns>
+ public bool Validate(object model, Type type, ModelMetadataProvider metadataProvider, HttpActionContext actionContext, string keyPrefix)
+ {
+ if (type == null)
+ {
+ throw Error.ArgumentNull("type");
+ }
+
+ if (metadataProvider == null)
+ {
+ throw Error.ArgumentNull("metadataProvider");
+ }
+
+ if (actionContext == null)
+ {
+ throw Error.ArgumentNull("actionContext");
+ }
+
+ ModelMetadata metadata = metadataProvider.GetMetadataForType(() => model, type);
+ ValidationContext validationContext = new ValidationContext() { ActionContext = actionContext, MetadataProvider = metadataProvider, Visited = new HashSet<object>() };
+ return ValidateNodeAndChildren(metadata, validationContext, container: null, prefix: keyPrefix);
+ }
+
+ private static bool ValidateNodeAndChildren(ModelMetadata metadata, ValidationContext validationContext, object container, string prefix)
+ {
+ object model = metadata.Model;
+ bool isValid = true;
+
+ if (model == null)
+ {
+ return ShallowValidate(metadata, validationContext.ActionContext, container, prefix);
+ }
+
+ // Check to avoid infinite recursion. This can happen with cycles in an object graph.
+ if (validationContext.Visited.Contains(model))
+ {
+ return true;
+ }
+ validationContext.Visited.Add(model);
+
+ // Validate the children first - depth-first traversal
+ IEnumerable enumerableModel = TypeHelper.GetAsEnumerable(model);
+ if (enumerableModel == null)
+ {
+ isValid = ValidateProperties(metadata, validationContext, prefix);
+ }
+ else
+ {
+ isValid = ValidateElements(enumerableModel, validationContext, prefix);
+ }
+ if (isValid)
+ {
+ // Don't bother to validate this node if children failed.
+ isValid = ShallowValidate(metadata, validationContext.ActionContext, container, prefix);
+ }
+
+ // Pop the object so that it can be validated again in a different path
+ validationContext.Visited.Remove(model);
+
+ return isValid;
+ }
+
+ private static bool ValidateProperties(ModelMetadata metadata, ValidationContext validationContext, string prefix)
+ {
+ bool isValid = true;
+ foreach (ModelMetadata childMetadata in metadata.Properties)
+ {
+ string childPrefix = ModelBindingHelper.CreatePropertyModelName(prefix, childMetadata.PropertyName);
+ if (!ValidateNodeAndChildren(childMetadata, validationContext, metadata.Model, childPrefix))
+ {
+ isValid = false;
+ }
+ }
+ return isValid;
+ }
+
+ private static bool ValidateElements(IEnumerable model, ValidationContext validationContext, string prefix)
+ {
+ bool isValid = true;
+ int index = 0;
+ Type elementType = GetElementType(model.GetType());
+
+ foreach (object element in model)
+ {
+ string elementPrefix = ModelBindingHelper.CreateIndexModelName(prefix, index++);
+ ModelMetadata elementMetadata = validationContext.MetadataProvider.GetMetadataForType(() => element, elementType);
+ if (!ValidateNodeAndChildren(elementMetadata, validationContext, model, elementPrefix))
+ {
+ isValid = false;
+ }
+ }
+ return isValid;
+ }
+
+ // Validates a single node (not including children)
+ // Returns true if validation passes successfully
+ private static bool ShallowValidate(ModelMetadata metadata, HttpActionContext actionContext, object container, string key)
+ {
+ bool isValid = true;
+ foreach (ModelValidator validator in metadata.GetValidators(actionContext.GetValidatorProviders()))
+ {
+ foreach (ModelValidationResult error in validator.Validate(container))
+ {
+ actionContext.ModelState.AddModelError(key, error.Message);
+ isValid = false;
+ }
+ }
+ return isValid;
+ }
+
+ private static Type GetElementType(Type type)
+ {
+ Contract.Assert(typeof(IEnumerable).IsAssignableFrom(type));
+ if (type.IsArray)
+ {
+ return type.GetElementType();
+ }
+
+ foreach (Type implementedInterface in type.GetInterfaces())
+ {
+ if (implementedInterface.IsGenericType && implementedInterface.GetGenericTypeDefinition() == typeof(IEnumerable<>))
+ {
+ return implementedInterface.GetGenericArguments()[0];
+ }
+ }
+
+ return typeof(object);
+ }
+
+ private class ValidationContext
+ {
+ public ModelMetadataProvider MetadataProvider { get; set; }
+ public HttpActionContext ActionContext { get; set; }
+ public HashSet<object> Visited { get; set; }
+ }
+ }
+}
diff --git a/src/System.Web.Http/Validation/IBodyModelValidator.cs b/src/System.Web.Http/Validation/IBodyModelValidator.cs
new file mode 100644
index 00000000..a36c0f80
--- /dev/null
+++ b/src/System.Web.Http/Validation/IBodyModelValidator.cs
@@ -0,0 +1,24 @@
+using System.Net.Http.Formatting;
+using System.Web.Http.Controllers;
+using System.Web.Http.Metadata;
+using System.Web.Http.ModelBinding;
+
+namespace System.Web.Http.Validation
+{
+ /// <summary>
+ /// Validates the body parameter of an action after the parameter has been read by the <see cref="MediaTypeFormatter"/>.
+ /// </summary>
+ public interface IBodyModelValidator
+ {
+ /// <summary>
+ /// Determines whether the <paramref name="model"/> is valid and adds any validation errors to the <paramref name="actionContext"/>'s <see cref="ModelStateDictionary"/>
+ /// </summary>
+ /// <param name="model">The model to be validated.</param>
+ /// <param name="type">The <see cref="Type"/> to use for validation.</param>
+ /// <param name="metadataProvider">The <see cref="ModelMetadataProvider"/> used to provide the model metadata.</param>
+ /// <param name="actionContext">The <see cref="HttpActionContext"/> within which the model is being validated.</param>
+ /// <param name="keyPrefix">The <see cref="string"/> to append to the key for any validation errors.</param>
+ /// <returns><c>true</c>if <paramref name="model"/> is valid, <c>false</c> otherwise.</returns>
+ bool Validate(object model, Type type, ModelMetadataProvider metadataProvider, HttpActionContext actionContext, string keyPrefix);
+ }
+}
diff --git a/src/System.Web.Http/Validation/IClientValidatable.cs b/src/System.Web.Http/Validation/IClientValidatable.cs
new file mode 100644
index 00000000..eed61462
--- /dev/null
+++ b/src/System.Web.Http/Validation/IClientValidatable.cs
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+using System.Web.Http.Controllers;
+using System.Web.Http.Metadata;
+
+namespace System.Web.Http.Validation
+{
+ // The purpose of this interface is to make something as supporting client-side
+ // validation, which could be discovered at runtime by whatever validation
+ // framework you're using. Because this interface is designed to be independent
+ // of underlying implementation details, where you apply this interface will
+ // depend on your specific validation framework.
+ //
+ // For DataAnnotations, you'll apply this interface to your validation attribute
+ // (the class which derives from ValidationAttribute). When you've implemented
+ // this interface, it will alleviate the need of writing a validator and registering
+ // it with the DataAnnotationsModelValidatorProvider.
+ public interface IClientValidatable
+ {
+ IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, IEnumerable<ModelValidatorProvider> validatorProviders);
+ }
+}
diff --git a/src/System.Web.Http/Validation/ModelClientValidationRule.cs b/src/System.Web.Http/Validation/ModelClientValidationRule.cs
new file mode 100644
index 00000000..93707e32
--- /dev/null
+++ b/src/System.Web.Http/Validation/ModelClientValidationRule.cs
@@ -0,0 +1,23 @@
+using System.Collections.Generic;
+
+namespace System.Web.Http.Validation
+{
+ public class ModelClientValidationRule
+ {
+ private readonly Dictionary<string, object> _validationParameters = new Dictionary<string, object>(StringComparer.Ordinal);
+ private string _validationType;
+
+ public string ErrorMessage { get; set; }
+
+ public IDictionary<string, object> ValidationParameters
+ {
+ get { return _validationParameters; }
+ }
+
+ public string ValidationType
+ {
+ get { return _validationType ?? String.Empty; }
+ set { _validationType = value; }
+ }
+ }
+}
diff --git a/src/System.Web.Http/Validation/ModelStateFormatterLogger.cs b/src/System.Web.Http/Validation/ModelStateFormatterLogger.cs
new file mode 100644
index 00000000..5fa171a1
--- /dev/null
+++ b/src/System.Web.Http/Validation/ModelStateFormatterLogger.cs
@@ -0,0 +1,38 @@
+using System.Net.Http.Formatting;
+using System.Web.Http.Common;
+using System.Web.Http.ModelBinding;
+
+namespace System.Web.Http.Validation
+{
+ /// <summary>
+ /// This <see cref="IFormatterLogger"/> logs formatter errors to the provided <see cref="ModelStateDictionary"/>.
+ /// </summary>
+ public class ModelStateFormatterLogger : IFormatterLogger
+ {
+ private readonly ModelStateDictionary _modelState;
+
+ public ModelStateFormatterLogger(ModelStateDictionary modelState)
+ {
+ if (modelState == null)
+ {
+ throw Error.ArgumentNull("modelState");
+ }
+
+ _modelState = modelState;
+ }
+
+ public void LogError(string errorPath, string errorMessage)
+ {
+ if (errorPath == null)
+ {
+ throw Error.ArgumentNull("errorPath");
+ }
+ if (errorMessage == null)
+ {
+ throw Error.ArgumentNull("errorMessage");
+ }
+
+ _modelState.AddModelError(errorPath, errorMessage);
+ }
+ }
+}
diff --git a/src/System.Web.Http/Validation/ModelValidatedEventArgs.cs b/src/System.Web.Http/Validation/ModelValidatedEventArgs.cs
new file mode 100644
index 00000000..139ca10d
--- /dev/null
+++ b/src/System.Web.Http/Validation/ModelValidatedEventArgs.cs
@@ -0,0 +1,23 @@
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+
+namespace System.Web.Http.Validation
+{
+ public sealed class ModelValidatedEventArgs : EventArgs
+ {
+ public ModelValidatedEventArgs(HttpActionContext actionContext, ModelValidationNode parentNode)
+ {
+ if (actionContext == null)
+ {
+ throw Error.ArgumentNull("actionContext");
+ }
+
+ ActionContext = actionContext;
+ ParentNode = parentNode;
+ }
+
+ public HttpActionContext ActionContext { get; private set; }
+
+ public ModelValidationNode ParentNode { get; private set; }
+ }
+}
diff --git a/src/System.Web.Http/Validation/ModelValidatingEventArgs.cs b/src/System.Web.Http/Validation/ModelValidatingEventArgs.cs
new file mode 100644
index 00000000..7c83cea4
--- /dev/null
+++ b/src/System.Web.Http/Validation/ModelValidatingEventArgs.cs
@@ -0,0 +1,24 @@
+using System.ComponentModel;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+
+namespace System.Web.Http.Validation
+{
+ public sealed class ModelValidatingEventArgs : CancelEventArgs
+ {
+ public ModelValidatingEventArgs(HttpActionContext actionContext, ModelValidationNode parentNode)
+ {
+ if (actionContext == null)
+ {
+ throw Error.ArgumentNull("actionContext");
+ }
+
+ ActionContext = actionContext;
+ ParentNode = parentNode;
+ }
+
+ public HttpActionContext ActionContext { get; private set; }
+
+ public ModelValidationNode ParentNode { get; private set; }
+ }
+}
diff --git a/src/System.Web.Http/Validation/ModelValidationNode.cs b/src/System.Web.Http/Validation/ModelValidationNode.cs
new file mode 100644
index 00000000..cc0378b5
--- /dev/null
+++ b/src/System.Web.Http/Validation/ModelValidationNode.cs
@@ -0,0 +1,206 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Metadata;
+using System.Web.Http.ModelBinding;
+using System.Web.Http.Properties;
+
+namespace System.Web.Http.Validation
+{
+ public sealed class ModelValidationNode
+ {
+ public ModelValidationNode(ModelMetadata modelMetadata, string modelStateKey)
+ : this(modelMetadata, modelStateKey, null)
+ {
+ }
+
+ public ModelValidationNode(ModelMetadata modelMetadata, string modelStateKey, IEnumerable<ModelValidationNode> childNodes)
+ {
+ if (modelMetadata == null)
+ {
+ throw Error.ArgumentNull("modelMetadata");
+ }
+ if (modelStateKey == null)
+ {
+ throw Error.ArgumentNull("modelStateKey");
+ }
+
+ ModelMetadata = modelMetadata;
+ ModelStateKey = modelStateKey;
+ ChildNodes = (childNodes != null) ? childNodes.ToList() : new List<ModelValidationNode>();
+ }
+
+ public event EventHandler<ModelValidatedEventArgs> Validated;
+
+ public event EventHandler<ModelValidatingEventArgs> Validating;
+
+ public ICollection<ModelValidationNode> ChildNodes { get; private set; }
+
+ public ModelMetadata ModelMetadata { get; private set; }
+
+ public string ModelStateKey { get; private set; }
+
+ public bool ValidateAllProperties { get; set; }
+
+ public bool SuppressValidation { get; set; }
+
+ public void CombineWith(ModelValidationNode otherNode)
+ {
+ if (otherNode != null && !otherNode.SuppressValidation)
+ {
+ Validated += otherNode.Validated;
+ Validating += otherNode.Validating;
+ foreach (ModelValidationNode childNode in otherNode.ChildNodes)
+ {
+ ChildNodes.Add(childNode);
+ }
+ }
+ }
+
+ private void OnValidated(ModelValidatedEventArgs e)
+ {
+ EventHandler<ModelValidatedEventArgs> handler = Validated;
+ if (handler != null)
+ {
+ handler(this, e);
+ }
+ }
+
+ private void OnValidating(ModelValidatingEventArgs e)
+ {
+ EventHandler<ModelValidatingEventArgs> handler = Validating;
+ if (handler != null)
+ {
+ handler(this, e);
+ }
+ }
+
+ private object TryConvertContainerToMetadataType(ModelValidationNode parentNode)
+ {
+ if (parentNode != null)
+ {
+ object containerInstance = parentNode.ModelMetadata.Model;
+ if (containerInstance != null)
+ {
+ Type expectedContainerType = ModelMetadata.ContainerType;
+ if (expectedContainerType != null)
+ {
+ if (expectedContainerType.IsInstanceOfType(containerInstance))
+ {
+ return containerInstance;
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public void Validate(HttpActionContext actionContext)
+ {
+ Validate(actionContext, null /* parentNode */);
+ }
+
+ public void Validate(HttpActionContext actionContext, ModelValidationNode parentNode)
+ {
+ if (actionContext == null)
+ {
+ throw Error.ArgumentNull("actionContext");
+ }
+
+ if (SuppressValidation)
+ {
+ // no-op
+ return;
+ }
+
+ // pre-validation steps
+ ModelValidatingEventArgs validatingEventArgs = new ModelValidatingEventArgs(actionContext, parentNode);
+ OnValidating(validatingEventArgs);
+ if (validatingEventArgs.Cancel)
+ {
+ return;
+ }
+
+ ValidateChildren(actionContext);
+ ValidateThis(actionContext, parentNode);
+
+ // post-validation steps
+ ModelValidatedEventArgs validatedEventArgs = new ModelValidatedEventArgs(actionContext, parentNode);
+ OnValidated(validatedEventArgs);
+ }
+
+ private void ValidateChildren(HttpActionContext actionContext)
+ {
+ foreach (ModelValidationNode child in ChildNodes)
+ {
+ child.Validate(actionContext, this);
+ }
+
+ if (ValidateAllProperties)
+ {
+ ValidateProperties(actionContext);
+ }
+ }
+
+ private void ValidateProperties(HttpActionContext actionContext)
+ {
+ // Based off CompositeModelValidator.
+ ModelStateDictionary modelState = actionContext.ModelState;
+
+ // DevDiv Bugs #227802 - Caching problem in ModelMetadata requires us to manually regenerate
+ // the ModelMetadata.
+ object model = ModelMetadata.Model;
+ ModelMetadata updatedMetadata = actionContext.GetMetadataProvider().GetMetadataForType(() => model, ModelMetadata.ModelType);
+
+ foreach (ModelMetadata propertyMetadata in updatedMetadata.Properties)
+ {
+ // Only want to add errors to ModelState if something doesn't already exist for the property node,
+ // else we could end up with duplicate or irrelevant error messages.
+ string propertyKeyRoot = ModelBindingHelper.CreatePropertyModelName(ModelStateKey, propertyMetadata.PropertyName);
+
+ if (modelState.IsValidField(propertyKeyRoot))
+ {
+ foreach (ModelValidator propertyValidator in propertyMetadata.GetValidators(actionContext.GetValidatorProviders()))
+ {
+ foreach (ModelValidationResult propertyResult in propertyValidator.Validate(model))
+ {
+ string thisErrorKey = ModelBindingHelper.CreatePropertyModelName(propertyKeyRoot, propertyResult.MemberName);
+ modelState.AddModelError(thisErrorKey, propertyResult.Message);
+ }
+ }
+ }
+ }
+ }
+
+ private void ValidateThis(HttpActionContext actionContext, ModelValidationNode parentNode)
+ {
+ ModelStateDictionary modelState = actionContext.ModelState;
+ if (!modelState.IsValidField(ModelStateKey))
+ {
+ return; // short-circuit
+ }
+
+ // If 'this' is null and there is no parent, we cannot validate, and
+ // the DataAnnotationsModelValidator will throw. So we intercept here
+ // to provide a catch-all value-required validation error
+ if (parentNode == null && ModelMetadata.Model == null)
+ {
+ string trueModelStateKey = ModelBindingHelper.CreatePropertyModelName(ModelStateKey, ModelMetadata.DisplayName);
+ modelState.AddModelError(trueModelStateKey, SRResources.Validation_ValueNotFound);
+ return;
+ }
+
+ object container = TryConvertContainerToMetadataType(parentNode);
+ foreach (ModelValidator validator in ModelMetadata.GetValidators(actionContext.GetValidatorProviders()))
+ {
+ foreach (ModelValidationResult validationResult in validator.Validate(container))
+ {
+ string trueModelStateKey = ModelBindingHelper.CreatePropertyModelName(ModelStateKey, validationResult.MemberName);
+ modelState.AddModelError(trueModelStateKey, validationResult.Message);
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/Validation/ModelValidationRequiredMemberSelector.cs b/src/System.Web.Http/Validation/ModelValidationRequiredMemberSelector.cs
new file mode 100644
index 00000000..e39195af
--- /dev/null
+++ b/src/System.Web.Http/Validation/ModelValidationRequiredMemberSelector.cs
@@ -0,0 +1,48 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http.Formatting;
+using System.Reflection;
+using System.Web.Http.Common;
+using System.Web.Http.Metadata;
+
+namespace System.Web.Http.Validation
+{
+ /// <summary>
+ /// This <see cref="IRequiredMemberSelector"/> selects required members by checking for any
+ /// required ModelValidators associated with the member. This is the default implementation used by
+ /// <see cref="HttpConfiguration"/>.
+ /// </summary>
+ public class ModelValidationRequiredMemberSelector : IRequiredMemberSelector
+ {
+ private readonly HttpConfiguration _configuration;
+
+ public ModelValidationRequiredMemberSelector(HttpConfiguration configuration)
+ {
+ if (configuration == null)
+ {
+ throw Error.ArgumentNull("dependencyResolver");
+ }
+
+ _configuration = configuration;
+ }
+
+ public bool IsRequiredMember(MemberInfo member)
+ {
+ if (member == null)
+ {
+ throw Error.ArgumentNull("member");
+ }
+
+ if (!(member is PropertyInfo))
+ {
+ return false;
+ }
+ ModelMetadataProvider metadataProvider = _configuration.ServiceResolver.GetModelMetadataProvider();
+ IEnumerable<ModelValidatorProvider> validatorProviders = _configuration.ServiceResolver.GetModelValidatorProviders();
+
+ ModelMetadata metadata = metadataProvider.GetMetadataForProperty(() => null, member.DeclaringType, member.Name);
+ IEnumerable<ModelValidator> validators = metadata.GetValidators(validatorProviders);
+ return validators.Any(validator => validator.IsRequired);
+ }
+ }
+}
diff --git a/src/System.Web.Http/Validation/ModelValidationResult.cs b/src/System.Web.Http/Validation/ModelValidationResult.cs
new file mode 100644
index 00000000..2a515a63
--- /dev/null
+++ b/src/System.Web.Http/Validation/ModelValidationResult.cs
@@ -0,0 +1,20 @@
+namespace System.Web.Http.Validation
+{
+ public class ModelValidationResult
+ {
+ private string _memberName;
+ private string _message;
+
+ public string MemberName
+ {
+ get { return _memberName ?? String.Empty; }
+ set { _memberName = value; }
+ }
+
+ public string Message
+ {
+ get { return _message ?? String.Empty; }
+ set { _message = value; }
+ }
+ }
+}
diff --git a/src/System.Web.Http/Validation/ModelValidator.cs b/src/System.Web.Http/Validation/ModelValidator.cs
new file mode 100644
index 00000000..23c135a7
--- /dev/null
+++ b/src/System.Web.Http/Validation/ModelValidator.cs
@@ -0,0 +1,90 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Metadata;
+using System.Web.Http.ModelBinding;
+
+namespace System.Web.Http.Validation
+{
+ public abstract class ModelValidator
+ {
+ protected ModelValidator(ModelMetadata metadata, IEnumerable<ModelValidatorProvider> validatorProviders)
+ {
+ if (metadata == null)
+ {
+ throw Error.ArgumentNull("metadata");
+ }
+ if (validatorProviders == null)
+ {
+ throw Error.ArgumentNull("validatorProviders");
+ }
+
+ Metadata = metadata;
+ ValidatorProviders = validatorProviders;
+ }
+
+ protected internal IEnumerable<ModelValidatorProvider> ValidatorProviders { get; private set; }
+
+ public virtual bool IsRequired
+ {
+ get { return false; }
+ }
+
+ protected internal ModelMetadata Metadata { get; private set; }
+
+ [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This method may perform non-trivial work.")]
+ public virtual IEnumerable<ModelClientValidationRule> GetClientValidationRules()
+ {
+ return Enumerable.Empty<ModelClientValidationRule>();
+ }
+
+ public static ModelValidator GetModelValidator(ModelMetadata metadata, IEnumerable<ModelValidatorProvider> validatorProviders)
+ {
+ return new CompositeModelValidator(metadata, validatorProviders);
+ }
+
+ public abstract IEnumerable<ModelValidationResult> Validate(object container);
+
+ private class CompositeModelValidator : ModelValidator
+ {
+ public CompositeModelValidator(ModelMetadata metadata, IEnumerable<ModelValidatorProvider> validatorProviders)
+ : base(metadata, validatorProviders)
+ {
+ }
+
+ public override IEnumerable<ModelValidationResult> Validate(object container)
+ {
+ bool propertiesValid = true;
+
+ foreach (ModelMetadata propertyMetadata in Metadata.Properties)
+ {
+ foreach (ModelValidator propertyValidator in propertyMetadata.GetValidators(ValidatorProviders))
+ {
+ foreach (ModelValidationResult propertyResult in propertyValidator.Validate(Metadata.Model))
+ {
+ propertiesValid = false;
+ yield return new ModelValidationResult
+ {
+ MemberName = ModelBindingHelper.CreatePropertyModelName(propertyMetadata.PropertyName, propertyResult.MemberName),
+ Message = propertyResult.Message
+ };
+ }
+ }
+ }
+
+ if (propertiesValid)
+ {
+ foreach (ModelValidator typeValidator in Metadata.GetValidators(ValidatorProviders))
+ {
+ foreach (ModelValidationResult typeResult in typeValidator.Validate(container))
+ {
+ yield return typeResult;
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/Validation/ModelValidatorProvider.cs b/src/System.Web.Http/Validation/ModelValidatorProvider.cs
new file mode 100644
index 00000000..c0612b31
--- /dev/null
+++ b/src/System.Web.Http/Validation/ModelValidatorProvider.cs
@@ -0,0 +1,11 @@
+using System.Collections.Generic;
+using System.Web.Http.Controllers;
+using System.Web.Http.Metadata;
+
+namespace System.Web.Http.Validation
+{
+ public abstract class ModelValidatorProvider
+ {
+ public abstract IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, IEnumerable<ModelValidatorProvider> validatorProviders);
+ }
+}
diff --git a/src/System.Web.Http/Validation/Providers/AssociatedValidatorProvider.cs b/src/System.Web.Http/Validation/Providers/AssociatedValidatorProvider.cs
new file mode 100644
index 00000000..0cb3462d
--- /dev/null
+++ b/src/System.Web.Http/Validation/Providers/AssociatedValidatorProvider.cs
@@ -0,0 +1,57 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Internal;
+using System.Web.Http.Metadata;
+using System.Web.Http.Properties;
+
+namespace System.Web.Http.Validation.Providers
+{
+ public abstract class AssociatedValidatorProvider : ModelValidatorProvider
+ {
+ protected virtual ICustomTypeDescriptor GetTypeDescriptor(Type type)
+ {
+ return TypeDescriptorHelper.Get(type);
+ }
+
+ public sealed override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, IEnumerable<ModelValidatorProvider> validatorProviders)
+ {
+ if (metadata == null)
+ {
+ throw Error.ArgumentNull("metadata");
+ }
+ if (validatorProviders == null)
+ {
+ throw Error.ArgumentNull("validatorProviders");
+ }
+
+ if (metadata.ContainerType != null && !String.IsNullOrEmpty(metadata.PropertyName))
+ {
+ return GetValidatorsForProperty(metadata, validatorProviders);
+ }
+
+ return GetValidatorsForType(metadata, validatorProviders);
+ }
+
+ protected abstract IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, IEnumerable<ModelValidatorProvider> validatorProviders, IEnumerable<Attribute> attributes);
+
+ private IEnumerable<ModelValidator> GetValidatorsForProperty(ModelMetadata metadata, IEnumerable<ModelValidatorProvider> validatorProviders)
+ {
+ ICustomTypeDescriptor typeDescriptor = GetTypeDescriptor(metadata.ContainerType);
+ PropertyDescriptor property = typeDescriptor.GetProperties().Find(metadata.PropertyName, true);
+ if (property == null)
+ {
+ throw Error.Argument("metadata", SRResources.Common_PropertyNotFound, metadata.ContainerType, metadata.PropertyName);
+ }
+
+ return GetValidators(metadata, validatorProviders, property.Attributes.OfType<Attribute>());
+ }
+
+ private IEnumerable<ModelValidator> GetValidatorsForType(ModelMetadata metadata, IEnumerable<ModelValidatorProvider> validatorProviders)
+ {
+ return GetValidators(metadata, validatorProviders, GetTypeDescriptor(metadata.ModelType).GetAttributes().Cast<Attribute>());
+ }
+ }
+}
diff --git a/src/System.Web.Http/Validation/Providers/ClientDataTypeModelValidatorProvider.cs b/src/System.Web.Http/Validation/Providers/ClientDataTypeModelValidatorProvider.cs
new file mode 100644
index 00000000..db188fdc
--- /dev/null
+++ b/src/System.Web.Http/Validation/Providers/ClientDataTypeModelValidatorProvider.cs
@@ -0,0 +1,164 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Metadata;
+using System.Web.Http.Properties;
+
+namespace System.Web.Http.Validation.Providers
+{
+ public class ClientDataTypeModelValidatorProvider : ModelValidatorProvider
+ {
+ private static readonly HashSet<Type> _numericTypes = new HashSet<Type>(new Type[]
+ {
+ typeof(byte), typeof(sbyte),
+ typeof(short), typeof(ushort),
+ typeof(int), typeof(uint),
+ typeof(long), typeof(ulong),
+ typeof(float), typeof(double), typeof(decimal)
+ });
+
+ private static string _resourceClassKey;
+
+ public static string ResourceClassKey
+ {
+ get { return _resourceClassKey ?? String.Empty; }
+ set { _resourceClassKey = value; }
+ }
+
+ public override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, IEnumerable<ModelValidatorProvider> validatorProviders)
+ {
+ if (metadata == null)
+ {
+ throw Error.ArgumentNull("metadata");
+ }
+ if (validatorProviders == null)
+ {
+ throw Error.ArgumentNull("validatorProviders");
+ }
+
+ return GetValidatorsImpl(metadata, validatorProviders);
+ }
+
+ private static IEnumerable<ModelValidator> GetValidatorsImpl(ModelMetadata metadata, IEnumerable<ModelValidatorProvider> validatorProviders)
+ {
+ Type type = metadata.ModelType;
+
+ if (IsDateTimeType(type))
+ {
+ yield return new DateModelValidator(metadata, validatorProviders);
+ }
+
+ if (IsNumericType(type))
+ {
+ yield return new NumericModelValidator(metadata, validatorProviders);
+ }
+ }
+
+ private static bool IsNumericType(Type type)
+ {
+ return _numericTypes.Contains(GetTypeToValidate(type));
+ }
+
+ private static bool IsDateTimeType(Type type)
+ {
+ return typeof(DateTime) == GetTypeToValidate(type);
+ }
+
+ private static Type GetTypeToValidate(Type type)
+ {
+ return Nullable.GetUnderlyingType(type) ?? type; // strip off the Nullable<>
+ }
+
+ // If the user specified a ResourceClassKey try to load the resource they specified.
+ // If the class key is invalid, an exception will be thrown.
+ // If the class key is valid but the resource is not found, it returns null, in which
+ // case it will fall back to the MVC default error message.
+ [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "resourceName", Justification = "This is temporary")]
+ [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "actionContext", Justification = "This is temporary")]
+ private static string GetUserResourceString(string resourceName)
+ {
+ string result = null;
+ // REVIEW: Removed user-settable resource option
+#if false
+ if (!String.IsNullOrEmpty(ResourceClassKey) && (HttpExecutionContext != null) && (HttpExecutionContext.HttpContext != null))
+ {
+ result = HttpExecutionContext.HttpContext.GetGlobalResourceObject(ResourceClassKey, resourceName, CultureInfo.CurrentUICulture) as string;
+ }
+#endif
+ return result;
+ }
+
+ private static string GetFieldMustBeNumericResource()
+ {
+ return GetUserResourceString("FieldMustBeNumeric") ?? SRResources.ClientDataTypeModelValidatorProvider_FieldMustBeNumeric;
+ }
+
+ private static string GetFieldMustBeDateResource()
+ {
+ return GetUserResourceString("FieldMustBeDate") ?? SRResources.ClientDataTypeModelValidatorProvider_FieldMustBeDate;
+ }
+
+ internal class ClientModelValidator : ModelValidator
+ {
+ private string _errorMessage;
+ private string _validationType;
+
+ public ClientModelValidator(ModelMetadata metadata, IEnumerable<ModelValidatorProvider> validatorProviders, string validationType, string errorMessage)
+ : base(metadata, validatorProviders)
+ {
+ if (String.IsNullOrEmpty(validationType))
+ {
+ throw Error.ArgumentNullOrEmpty("validationType");
+ }
+ if (String.IsNullOrEmpty(errorMessage))
+ {
+ throw Error.ArgumentNullOrEmpty("errorMessage");
+ }
+
+ _validationType = validationType;
+ _errorMessage = errorMessage;
+ }
+
+ public sealed override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
+ {
+ ModelClientValidationRule rule = new ModelClientValidationRule()
+ {
+ ValidationType = _validationType,
+ ErrorMessage = FormatErrorMessage(Metadata.GetDisplayName())
+ };
+
+ return new ModelClientValidationRule[] { rule };
+ }
+
+ private string FormatErrorMessage(string displayName)
+ {
+ // use CurrentCulture since this message is intended for the site visitor
+ return Error.Format(_errorMessage, displayName);
+ }
+
+ public sealed override IEnumerable<ModelValidationResult> Validate(object container)
+ {
+ // this is not a server-side validator
+ return Enumerable.Empty<ModelValidationResult>();
+ }
+ }
+
+ internal sealed class DateModelValidator : ClientModelValidator
+ {
+ public DateModelValidator(ModelMetadata metadata, IEnumerable<ModelValidatorProvider> validatorProviders)
+ : base(metadata, validatorProviders, "date", GetFieldMustBeDateResource())
+ {
+ }
+ }
+
+ internal sealed class NumericModelValidator : ClientModelValidator
+ {
+ public NumericModelValidator(ModelMetadata metadata, IEnumerable<ModelValidatorProvider> validatorProviders)
+ : base(metadata, validatorProviders, "number", GetFieldMustBeNumericResource())
+ {
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/Validation/Providers/DataAnnotationsModelValidatorProvider.cs b/src/System.Web.Http/Validation/Providers/DataAnnotationsModelValidatorProvider.cs
new file mode 100644
index 00000000..da5b2d72
--- /dev/null
+++ b/src/System.Web.Http/Validation/Providers/DataAnnotationsModelValidatorProvider.cs
@@ -0,0 +1,345 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Reflection;
+using System.Security;
+using System.Threading;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Metadata;
+using System.Web.Http.Properties;
+using System.Web.Http.Validation.Validators;
+
+namespace System.Web.Http.Validation.Providers
+{
+ // A factory for validators based on ValidationAttribute
+ public delegate ModelValidator DataAnnotationsModelValidationFactory(ModelMetadata metadata, IEnumerable<ModelValidatorProvider> validatorProviders, ValidationAttribute attribute);
+
+ // A factory for validators based on IValidatableObject
+ public delegate ModelValidator DataAnnotationsValidatableObjectAdapterFactory(ModelMetadata metadata, IEnumerable<ModelValidatorProvider> validatorProviders);
+
+ /// <summary>
+ /// An implementation of <see cref="ModelValidatorProvider"/> which providers validators
+ /// for attributes which derive from <see cref="ValidationAttribute"/>. It also provides
+ /// a validator for types which implement <see cref="IValidatableObject"/>. To support
+ /// client side validation, you can either register adapters through the static methods
+ /// on this class, or by having your validation attributes implement
+ /// <see cref="IClientValidatable"/>. The logic to support IClientValidatable
+ /// is implemented in <see cref="DataAnnotationsModelValidator"/>.
+ /// </summary>
+ // [SecuritySafeCritical] because class constructor accesses DataAnnotations types
+ [SecuritySafeCritical]
+ public class DataAnnotationsModelValidatorProvider : AssociatedValidatorProvider
+ {
+ private static bool _addImplicitRequiredAttributeForValueTypes = true;
+ private static ReaderWriterLockSlim _adaptersLock = new ReaderWriterLockSlim();
+
+ // Factories for validation attributes
+
+ internal static DataAnnotationsModelValidationFactory DefaultAttributeFactory =
+ (metadata, validationProviders, attribute) => new DataAnnotationsModelValidator(metadata, validationProviders, attribute);
+
+ internal static readonly Dictionary<Type, DataAnnotationsModelValidationFactory> AttributeFactories = new Dictionary<Type, DataAnnotationsModelValidationFactory>()
+ {
+ {
+ typeof(RangeAttribute),
+ (metadata, validatorProviders, attribute) => new RangeAttributeAdapter(metadata, validatorProviders, (RangeAttribute)attribute)
+ },
+ {
+ typeof(RegularExpressionAttribute),
+ (metadata, validatorProviders, attribute) => new RegularExpressionAttributeAdapter(metadata, validatorProviders, (RegularExpressionAttribute)attribute)
+ },
+ {
+ typeof(RequiredAttribute),
+ (metadata, validatorProviders, attribute) => new RequiredAttributeAdapter(metadata, validatorProviders, (RequiredAttribute)attribute)
+ },
+ {
+ typeof(StringLengthAttribute),
+ (metadata, validatorProviders, attribute) => new StringLengthAttributeAdapter(metadata, validatorProviders, (StringLengthAttribute)attribute)
+ },
+ };
+
+ // Factories for IValidatableObject models
+ internal static DataAnnotationsValidatableObjectAdapterFactory DefaultValidatableFactory =
+ (metadata, validationProviders) => new ValidatableObjectAdapter(metadata, validationProviders);
+
+ internal static readonly Dictionary<Type, DataAnnotationsValidatableObjectAdapterFactory> ValidatableFactories = new Dictionary<Type, DataAnnotationsValidatableObjectAdapterFactory>();
+
+ public static bool AddImplicitRequiredAttributeForValueTypes
+ {
+ get { return _addImplicitRequiredAttributeForValueTypes; }
+ set { _addImplicitRequiredAttributeForValueTypes = value; }
+ }
+
+ // [SecuritySafeCritical] because it uses DataAnnotations type ValidationAttribute and IValidatableObject
+ [SecuritySafeCritical]
+ protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, IEnumerable<ModelValidatorProvider> validatorProviders, IEnumerable<Attribute> attributes)
+ {
+ _adaptersLock.EnterReadLock();
+
+ try
+ {
+ List<ModelValidator> results = new List<ModelValidator>();
+
+ // Add an implied [Required] attribute for any non-nullable value type,
+ // unless they've configured us not to do that.
+ if (AddImplicitRequiredAttributeForValueTypes &&
+ metadata.IsRequired &&
+ !attributes.Any(a => a is RequiredAttribute))
+ {
+ attributes = attributes.Concat(new[] { new RequiredAttribute() });
+ }
+
+ // Produce a validator for each validation attribute we find
+ foreach (ValidationAttribute attribute in attributes.OfType<ValidationAttribute>())
+ {
+ DataAnnotationsModelValidationFactory factory;
+ if (!AttributeFactories.TryGetValue(attribute.GetType(), out factory))
+ {
+ factory = DefaultAttributeFactory;
+ }
+ results.Add(factory(metadata, validatorProviders, attribute));
+ }
+
+ // Produce a validator if the type supports IValidatableObject
+ if (typeof(IValidatableObject).IsAssignableFrom(metadata.ModelType))
+ {
+ DataAnnotationsValidatableObjectAdapterFactory factory;
+ if (!ValidatableFactories.TryGetValue(metadata.ModelType, out factory))
+ {
+ factory = DefaultValidatableFactory;
+ }
+ results.Add(factory(metadata, validatorProviders));
+ }
+
+ return results;
+ }
+ finally
+ {
+ _adaptersLock.ExitReadLock();
+ }
+ }
+
+ #region Validation attribute adapter registration
+
+ public static void RegisterAdapter(Type attributeType, Type adapterType)
+ {
+ ValidateAttributeType(attributeType);
+ ValidateAttributeAdapterType(adapterType);
+ ConstructorInfo constructor = GetAttributeAdapterConstructor(attributeType, adapterType);
+
+ _adaptersLock.EnterWriteLock();
+
+ try
+ {
+ AttributeFactories[attributeType] = (metadata, context, attribute) => (ModelValidator)constructor.Invoke(new object[] { metadata, context, attribute });
+ }
+ finally
+ {
+ _adaptersLock.ExitWriteLock();
+ }
+ }
+
+ public static void RegisterAdapterFactory(Type attributeType, DataAnnotationsModelValidationFactory factory)
+ {
+ ValidateAttributeType(attributeType);
+ ValidateAttributeFactory(factory);
+
+ _adaptersLock.EnterWriteLock();
+
+ try
+ {
+ AttributeFactories[attributeType] = factory;
+ }
+ finally
+ {
+ _adaptersLock.ExitWriteLock();
+ }
+ }
+
+ public static void RegisterDefaultAdapter(Type adapterType)
+ {
+ ValidateAttributeAdapterType(adapterType);
+ ConstructorInfo constructor = GetAttributeAdapterConstructor(typeof(ValidationAttribute), adapterType);
+
+ DefaultAttributeFactory = (metadata, context, attribute) => (ModelValidator)constructor.Invoke(new object[] { metadata, context, attribute });
+ }
+
+ public static void RegisterDefaultAdapterFactory(DataAnnotationsModelValidationFactory factory)
+ {
+ ValidateAttributeFactory(factory);
+
+ DefaultAttributeFactory = factory;
+ }
+
+ // Helpers
+
+ private static ConstructorInfo GetAttributeAdapterConstructor(Type attributeType, Type adapterType)
+ {
+ ConstructorInfo constructor = adapterType.GetConstructor(new[] { typeof(ModelMetadata), typeof(HttpActionContext), attributeType });
+ if (constructor == null)
+ {
+ throw Error.Argument("adapterType", SRResources.DataAnnotationsModelValidatorProvider_ConstructorRequirements, adapterType, attributeType);
+ }
+
+ return constructor;
+ }
+
+ private static void ValidateAttributeAdapterType(Type adapterType)
+ {
+ if (adapterType == null)
+ {
+ throw Error.ArgumentNull("adapterType");
+ }
+
+ if (!typeof(ModelValidator).IsAssignableFrom(adapterType))
+ {
+ throw Error.Argument("adapterType", SRResources.Common_TypeMustDriveFromType, adapterType, typeof(ModelValidator));
+ }
+ }
+
+ private static void ValidateAttributeType(Type attributeType)
+ {
+ if (attributeType == null)
+ {
+ throw Error.ArgumentNull("attributeType");
+ }
+
+ if (!typeof(ValidationAttribute).IsAssignableFrom(attributeType))
+ {
+ throw Error.Argument("attributeType", SRResources.Common_TypeMustDriveFromType, attributeType, typeof(ModelValidator));
+ }
+ }
+
+ private static void ValidateAttributeFactory(DataAnnotationsModelValidationFactory factory)
+ {
+ if (factory == null)
+ {
+ throw Error.ArgumentNull("factory");
+ }
+ }
+
+ #endregion
+
+ #region IValidatableObject adapter registration
+
+ /// <summary>
+ /// Registers an adapter type for the given <paramref name="modelType"/>, which must
+ /// implement <see cref="IValidatableObject"/>. The adapter type must derive from
+ /// <see cref="ModelValidator"/> and it must contain a public constructor
+ /// which takes two parameters of types <see cref="ModelMetadata"/> and
+ /// <see cref="HttpActionContext"/>.
+ /// </summary>
+ public static void RegisterValidatableObjectAdapter(Type modelType, Type adapterType)
+ {
+ ValidateValidatableModelType(modelType);
+ ValidateValidatableAdapterType(adapterType);
+ ConstructorInfo constructor = GetValidatableAdapterConstructor(adapterType);
+
+ _adaptersLock.EnterWriteLock();
+
+ try
+ {
+ ValidatableFactories[modelType] = (metadata, context) => (ModelValidator)constructor.Invoke(new object[] { metadata, context });
+ }
+ finally
+ {
+ _adaptersLock.ExitWriteLock();
+ }
+ }
+
+ /// <summary>
+ /// Registers an adapter factory for the given <paramref name="modelType"/>, which must
+ /// implement <see cref="IValidatableObject"/>.
+ /// </summary>
+ public static void RegisterValidatableObjectAdapterFactory(Type modelType, DataAnnotationsValidatableObjectAdapterFactory factory)
+ {
+ ValidateValidatableModelType(modelType);
+ ValidateValidatableFactory(factory);
+
+ _adaptersLock.EnterWriteLock();
+
+ try
+ {
+ ValidatableFactories[modelType] = factory;
+ }
+ finally
+ {
+ _adaptersLock.ExitWriteLock();
+ }
+ }
+
+ /// <summary>
+ /// Registers the default adapter type for objects which implement
+ /// <see cref="IValidatableObject"/>. The adapter type must derive from
+ /// <see cref="ModelValidator"/> and it must contain a public constructor
+ /// which takes two parameters of types <see cref="ModelMetadata"/> and
+ /// <see cref="HttpActionContext"/>.
+ /// </summary>
+ public static void RegisterDefaultValidatableObjectAdapter(Type adapterType)
+ {
+ ValidateValidatableAdapterType(adapterType);
+ ConstructorInfo constructor = GetValidatableAdapterConstructor(adapterType);
+
+ DefaultValidatableFactory = (metadata, context) => (ModelValidator)constructor.Invoke(new object[] { metadata, context });
+ }
+
+ /// <summary>
+ /// Registers the default adapter factory for objects which implement
+ /// <see cref="IValidatableObject"/>.
+ /// </summary>
+ public static void RegisterDefaultValidatableObjectAdapterFactory(DataAnnotationsValidatableObjectAdapterFactory factory)
+ {
+ ValidateValidatableFactory(factory);
+
+ DefaultValidatableFactory = factory;
+ }
+
+ // Helpers
+
+ private static ConstructorInfo GetValidatableAdapterConstructor(Type adapterType)
+ {
+ ConstructorInfo constructor = adapterType.GetConstructor(new[] { typeof(ModelMetadata), typeof(HttpActionContext) });
+ if (constructor == null)
+ {
+ throw Error.Argument("adapterType", SRResources.DataAnnotationsModelValidatorProvider_ValidatableConstructorRequirements, adapterType);
+ }
+
+ return constructor;
+ }
+
+ private static void ValidateValidatableAdapterType(Type adapterType)
+ {
+ if (adapterType == null)
+ {
+ throw Error.ArgumentNull("adapterType");
+ }
+ if (!typeof(ModelValidator).IsAssignableFrom(adapterType))
+ {
+ throw Error.Argument("adapterType", SRResources.Common_TypeMustDriveFromType, adapterType, typeof(ModelValidator));
+ }
+ }
+
+ private static void ValidateValidatableModelType(Type modelType)
+ {
+ if (modelType == null)
+ {
+ throw Error.ArgumentNull("modelType");
+ }
+ if (!typeof(IValidatableObject).IsAssignableFrom(modelType))
+ {
+ throw Error.Argument("modelType", SRResources.Common_TypeMustDriveFromType, modelType, typeof(ModelValidator));
+ }
+ }
+
+ private static void ValidateValidatableFactory(DataAnnotationsValidatableObjectAdapterFactory factory)
+ {
+ if (factory == null)
+ {
+ throw Error.ArgumentNull("factory");
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/System.Web.Http/Validation/Providers/DataMemberModelValidatorProvider.cs b/src/System.Web.Http/Validation/Providers/DataMemberModelValidatorProvider.cs
new file mode 100644
index 00000000..fb66b9fb
--- /dev/null
+++ b/src/System.Web.Http/Validation/Providers/DataMemberModelValidatorProvider.cs
@@ -0,0 +1,29 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.Serialization;
+using System.Web.Http.Metadata;
+using System.Web.Http.Validation.Validators;
+
+namespace System.Web.Http.Validation.Providers
+{
+ /// <summary>
+ /// This <see cref="ModelValidatorProvider"/> provides a required ModelValidator for members marked as [DataMember(IsRequired=true)].
+ /// </summary>
+ public class DataMemberModelValidatorProvider : AssociatedValidatorProvider
+ {
+ protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, IEnumerable<ModelValidatorProvider> validatorProviders, IEnumerable<Attribute> attributes)
+ {
+ DataMemberAttribute dataMemberAttribute = attributes.OfType<DataMemberAttribute>().FirstOrDefault();
+ if (dataMemberAttribute != null)
+ {
+ // isDataContract == true iff the containter type has at least one DataContractAttribute
+ bool isDataContract = GetTypeDescriptor(metadata.ContainerType).GetAttributes().OfType<DataContractAttribute>().Any();
+ if (isDataContract && dataMemberAttribute.IsRequired)
+ {
+ return new[] { new RequiredMemberModelValidator(metadata, validatorProviders, metadata.PropertyName) };
+ }
+ }
+ return new ModelValidator[0];
+ }
+ }
+}
diff --git a/src/System.Web.Http/Validation/Validators/DataAnnotationsModelValidator.cs b/src/System.Web.Http/Validation/Validators/DataAnnotationsModelValidator.cs
new file mode 100644
index 00000000..17b9831b
--- /dev/null
+++ b/src/System.Web.Http/Validation/Validators/DataAnnotationsModelValidator.cs
@@ -0,0 +1,93 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Security;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Metadata;
+
+namespace System.Web.Http.Validation.Validators
+{
+ // [SecuritySafeCritical] because it has ctor and properties exposing DataAnnotations types
+ [SecuritySafeCritical]
+ public class DataAnnotationsModelValidator : ModelValidator
+ {
+ public DataAnnotationsModelValidator(ModelMetadata metadata, IEnumerable<ModelValidatorProvider> validatorProviders, ValidationAttribute attribute)
+ : base(metadata, validatorProviders)
+ {
+ if (attribute == null)
+ {
+ throw Error.ArgumentNull("attribute");
+ }
+
+ Attribute = attribute;
+ }
+
+ protected internal ValidationAttribute Attribute { get; private set; }
+
+ protected internal string ErrorMessage
+ {
+ get { return Attribute.FormatErrorMessage(Metadata.GetDisplayName()); }
+ }
+
+ public override bool IsRequired
+ {
+ // [SecuritySafeCritical] because it uses DataAnnotations type RequiredAttribute
+ [SecuritySafeCritical]
+ get { return Attribute is RequiredAttribute; }
+ }
+
+ internal static ModelValidator Create(ModelMetadata metadata, IEnumerable<ModelValidatorProvider> validatorProviders, ValidationAttribute attribute)
+ {
+ return new DataAnnotationsModelValidator(metadata, validatorProviders, attribute);
+ }
+
+ public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
+ {
+ IEnumerable<ModelClientValidationRule> results = base.GetClientValidationRules();
+
+ IClientValidatable clientValidatable = Attribute as IClientValidatable;
+ if (clientValidatable != null)
+ {
+ results = results.Concat(clientValidatable.GetClientValidationRules(Metadata, ValidatorProviders));
+ }
+
+ return results;
+ }
+
+ // [SecuritySafeCritical] because is uses DataAnnotations type ValidationContext
+ [SecuritySafeCritical]
+ public override IEnumerable<ModelValidationResult> Validate(object container)
+ {
+ // Per the WCF RIA Services team, instance can never be null (if you have
+ // no parent, you pass yourself for the "instance" parameter).
+ ValidationContext context = new ValidationContext(container ?? Metadata.Model, null, null);
+ context.DisplayName = Metadata.GetDisplayName();
+
+ ValidationResult result = Attribute.GetValidationResult(Metadata.Model, context);
+
+ if (result != ValidationResult.Success)
+ {
+ return new ModelValidationResult[] { new ModelValidationResult { Message = result.ErrorMessage } };
+ }
+
+ return new ModelValidationResult[0];
+ }
+ }
+
+ // [SecuritySafeCritical] to allow derivation from DataAnnotationsModelValidator and to permit closed generic type subclasses
+ [SecuritySafeCritical]
+ public class DataAnnotationsModelValidator<TAttribute> : DataAnnotationsModelValidator
+ where TAttribute : ValidationAttribute
+ {
+ public DataAnnotationsModelValidator(ModelMetadata metadata, IEnumerable<ModelValidatorProvider> validatorProviders, TAttribute attribute)
+ : base(metadata, validatorProviders, attribute)
+ {
+ }
+
+ protected new TAttribute Attribute
+ {
+ get { return (TAttribute)base.Attribute; }
+ }
+ }
+}
diff --git a/src/System.Web.Http/Validation/Validators/RangeAttributeAdapter.cs b/src/System.Web.Http/Validation/Validators/RangeAttributeAdapter.cs
new file mode 100644
index 00000000..ab835826
--- /dev/null
+++ b/src/System.Web.Http/Validation/Validators/RangeAttributeAdapter.cs
@@ -0,0 +1,25 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Security;
+using System.Web.Http.Controllers;
+using System.Web.Http.Metadata;
+using System.Web.Http.Validation.ClientRules;
+
+namespace System.Web.Http.Validation.Validators
+{
+ // [SecuritySafeCritical] to allow derivation from DataAnnotationsModelValidator<T>
+ [SecuritySafeCritical]
+ public class RangeAttributeAdapter : DataAnnotationsModelValidator<RangeAttribute>
+ {
+ public RangeAttributeAdapter(ModelMetadata metadata, IEnumerable<ModelValidatorProvider> validatorProviders, RangeAttribute attribute)
+ : base(metadata, validatorProviders, attribute)
+ {
+ }
+
+ public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
+ {
+ string errorMessage = ErrorMessage; // Per Dev10 Bug #923283, need to make sure ErrorMessage is called before Minimum/Maximum
+ return new[] { new ModelClientValidationRangeRule(errorMessage, Attribute.Minimum, Attribute.Maximum) };
+ }
+ }
+}
diff --git a/src/System.Web.Http/Validation/Validators/RegularExpressionAttributeAdapter.cs b/src/System.Web.Http/Validation/Validators/RegularExpressionAttributeAdapter.cs
new file mode 100644
index 00000000..39d8c39c
--- /dev/null
+++ b/src/System.Web.Http/Validation/Validators/RegularExpressionAttributeAdapter.cs
@@ -0,0 +1,24 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Security;
+using System.Web.Http.Controllers;
+using System.Web.Http.Metadata;
+using System.Web.Http.Validation.ClientRules;
+
+namespace System.Web.Http.Validation.Validators
+{
+ // [SecuritySafeCritical] to allow derivation from DataAnnotationsModelValidator<T>
+ [SecuritySafeCritical]
+ public class RegularExpressionAttributeAdapter : DataAnnotationsModelValidator<RegularExpressionAttribute>
+ {
+ public RegularExpressionAttributeAdapter(ModelMetadata metadata, IEnumerable<ModelValidatorProvider> validatorProviders, RegularExpressionAttribute attribute)
+ : base(metadata, validatorProviders, attribute)
+ {
+ }
+
+ public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
+ {
+ return new[] { new ModelClientValidationRegexRule(ErrorMessage, Attribute.Pattern) };
+ }
+ }
+}
diff --git a/src/System.Web.Http/Validation/Validators/RequiredAttributeAdapter.cs b/src/System.Web.Http/Validation/Validators/RequiredAttributeAdapter.cs
new file mode 100644
index 00000000..f02c4a4c
--- /dev/null
+++ b/src/System.Web.Http/Validation/Validators/RequiredAttributeAdapter.cs
@@ -0,0 +1,24 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Security;
+using System.Web.Http.Controllers;
+using System.Web.Http.Metadata;
+using System.Web.Http.Validation.ClientRules;
+
+namespace System.Web.Http.Validation.Validators
+{
+ // [SecuritySafeCritical] to allow derivation from DataAnnotationsModelValidator<T>
+ [SecuritySafeCritical]
+ public class RequiredAttributeAdapter : DataAnnotationsModelValidator<RequiredAttribute>
+ {
+ public RequiredAttributeAdapter(ModelMetadata metadata, IEnumerable<ModelValidatorProvider> validatorProviders, RequiredAttribute attribute)
+ : base(metadata, validatorProviders, attribute)
+ {
+ }
+
+ public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
+ {
+ return new[] { new ModelClientValidationRequiredRule(ErrorMessage) };
+ }
+ }
+}
diff --git a/src/System.Web.Http/Validation/Validators/RequiredMemberModelValidator.cs b/src/System.Web.Http/Validation/Validators/RequiredMemberModelValidator.cs
new file mode 100644
index 00000000..53afef2e
--- /dev/null
+++ b/src/System.Web.Http/Validation/Validators/RequiredMemberModelValidator.cs
@@ -0,0 +1,47 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.Web.Http.Common;
+using System.Web.Http.Metadata;
+using System.Web.Http.Properties;
+using System.Web.Http.Validation.ClientRules;
+
+namespace System.Web.Http.Validation.Validators
+{
+ /// <summary>
+ /// <see cref="ModelValidator"/> for required members.
+ /// </summary>
+ public class RequiredMemberModelValidator : ModelValidator
+ {
+ private readonly string _memberName;
+
+ public RequiredMemberModelValidator(ModelMetadata metadata, IEnumerable<ModelValidatorProvider> validatorProviders, string memberName)
+ : base(metadata, validatorProviders)
+ {
+ if (memberName == null)
+ {
+ throw Error.ArgumentNull("memberName");
+ }
+
+ _memberName = memberName;
+ }
+
+ public override bool IsRequired
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ public override IEnumerable<ModelValidationResult> Validate(object container)
+ {
+ return new ModelValidationResult[0];
+ }
+
+ public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
+ {
+ string message = Error.Format(SRResources.MissingRequiredMember, _memberName);
+ return new[] { new ModelClientValidationRequiredRule(message) };
+ }
+ }
+}
diff --git a/src/System.Web.Http/Validation/Validators/StringLengthAttributeAdapter.cs b/src/System.Web.Http/Validation/Validators/StringLengthAttributeAdapter.cs
new file mode 100644
index 00000000..bb8407da
--- /dev/null
+++ b/src/System.Web.Http/Validation/Validators/StringLengthAttributeAdapter.cs
@@ -0,0 +1,24 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Security;
+using System.Web.Http.Controllers;
+using System.Web.Http.Metadata;
+using System.Web.Http.Validation.ClientRules;
+
+namespace System.Web.Http.Validation.Validators
+{
+ // [SecuritySafeCritical] to allow derivation from DataAnnotationsModelValidator<T>
+ [SecuritySafeCritical]
+ public class StringLengthAttributeAdapter : DataAnnotationsModelValidator<StringLengthAttribute>
+ {
+ public StringLengthAttributeAdapter(ModelMetadata metadata, IEnumerable<ModelValidatorProvider> validatorProviders, StringLengthAttribute attribute)
+ : base(metadata, validatorProviders, attribute)
+ {
+ }
+
+ public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
+ {
+ return new[] { new ModelClientValidationStringLengthRule(ErrorMessage, Attribute.MinimumLength, Attribute.MaximumLength) };
+ }
+ }
+}
diff --git a/src/System.Web.Http/Validation/Validators/ValidatableObjectAdapter.cs b/src/System.Web.Http/Validation/Validators/ValidatableObjectAdapter.cs
new file mode 100644
index 00000000..1fa75c67
--- /dev/null
+++ b/src/System.Web.Http/Validation/Validators/ValidatableObjectAdapter.cs
@@ -0,0 +1,60 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Metadata;
+using System.Web.Http.Properties;
+
+namespace System.Web.Http.Validation.Validators
+{
+ public class ValidatableObjectAdapter : ModelValidator
+ {
+ public ValidatableObjectAdapter(ModelMetadata metadata, IEnumerable<ModelValidatorProvider> validatorProviders)
+ : base(metadata, validatorProviders)
+ {
+ }
+
+ public override IEnumerable<ModelValidationResult> Validate(object container)
+ {
+ // NOTE: Container is never used here, because IValidatableObject doesn't give you
+ // any way to get access to your container.
+
+ object model = Metadata.Model;
+ if (model == null)
+ {
+ return Enumerable.Empty<ModelValidationResult>();
+ }
+
+ IValidatableObject validatable = model as IValidatableObject;
+ if (validatable == null)
+ {
+ throw Error.InvalidOperation(SRResources.ValidatableObjectAdapter_IncompatibleType, model.GetType());
+ }
+
+ ValidationContext validationContext = new ValidationContext(validatable, null, null);
+ return ConvertResults(validatable.Validate(validationContext));
+ }
+
+ private IEnumerable<ModelValidationResult> ConvertResults(IEnumerable<ValidationResult> results)
+ {
+ foreach (ValidationResult result in results)
+ {
+ if (result != ValidationResult.Success)
+ {
+ if (result.MemberNames == null || !result.MemberNames.Any())
+ {
+ yield return new ModelValidationResult { Message = result.ErrorMessage };
+ }
+ else
+ {
+ foreach (string memberName in result.MemberNames)
+ {
+ yield return new ModelValidationResult { Message = result.ErrorMessage, MemberName = memberName };
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Http/ValueProviders/IEnumerableValueProvider.cs b/src/System.Web.Http/ValueProviders/IEnumerableValueProvider.cs
new file mode 100644
index 00000000..f1bb5699
--- /dev/null
+++ b/src/System.Web.Http/ValueProviders/IEnumerableValueProvider.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace System.Web.Http.ValueProviders
+{
+ public interface IEnumerableValueProvider : IValueProvider
+ {
+ IDictionary<string, string> GetKeysFromPrefix(string prefix);
+ }
+}
diff --git a/src/System.Web.Http/ValueProviders/IUriValueProviderFactory.cs b/src/System.Web.Http/ValueProviders/IUriValueProviderFactory.cs
new file mode 100644
index 00000000..683dcd31
--- /dev/null
+++ b/src/System.Web.Http/ValueProviders/IUriValueProviderFactory.cs
@@ -0,0 +1,14 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Web.Http.ValueProviders
+{
+ /// <summary>
+ /// This interface is implemented by any <see cref="ValueProviderFactory"/> that supports
+ /// the creation of a <see cref="IValueProvider"/> to access the <see cref="T:System.Uri"/> of
+ /// an incoming <see cref="T:System.Net.Http.HttpRequestMessage"/>.
+ /// </summary>
+ [SuppressMessage("Microsoft.Design", "CA1040:AvoidEmptyInterfaces", Justification = "Tagging interface is intentional to allow Linq TypeOf")]
+ public interface IUriValueProviderFactory
+ {
+ }
+}
diff --git a/src/System.Web.Http/ValueProviders/IValueProvider.cs b/src/System.Web.Http/ValueProviders/IValueProvider.cs
new file mode 100644
index 00000000..7926352b
--- /dev/null
+++ b/src/System.Web.Http/ValueProviders/IValueProvider.cs
@@ -0,0 +1,8 @@
+namespace System.Web.Http.ValueProviders
+{
+ public interface IValueProvider
+ {
+ bool ContainsPrefix(string prefix);
+ ValueProviderResult GetValue(string key);
+ }
+}
diff --git a/src/System.Web.Http/ValueProviders/Providers/CompositeValueProvider.cs b/src/System.Web.Http/ValueProviders/Providers/CompositeValueProvider.cs
new file mode 100644
index 00000000..11e1255c
--- /dev/null
+++ b/src/System.Web.Http/ValueProviders/Providers/CompositeValueProvider.cs
@@ -0,0 +1,66 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Web.Http.Common;
+
+namespace System.Web.Http.ValueProviders.Providers
+{
+ [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix", Justification = "It is more fundamentally a value provider than a collection")]
+ public class CompositeValueProvider : Collection<IValueProvider>, IValueProvider, IEnumerableValueProvider
+ {
+ public CompositeValueProvider()
+ {
+ }
+
+ public CompositeValueProvider(IList<IValueProvider> list)
+ : base(list)
+ {
+ }
+
+ public virtual bool ContainsPrefix(string prefix)
+ {
+ return this.Any(vp => vp.ContainsPrefix(prefix));
+ }
+
+ public virtual ValueProviderResult GetValue(string key)
+ {
+ return (from provider in this
+ let result = provider.GetValue(key)
+ where result != null
+ select result).FirstOrDefault();
+ }
+
+ public virtual IDictionary<string, string> GetKeysFromPrefix(string prefix)
+ {
+ return (from provider in this
+ let result = GetKeysFromPrefixFromProvider(provider, prefix)
+ where result != null && result.Any()
+ select result).FirstOrDefault() ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ }
+
+ internal static IDictionary<string, string> GetKeysFromPrefixFromProvider(IValueProvider provider, string prefix)
+ {
+ IEnumerableValueProvider enumeratedProvider = provider as IEnumerableValueProvider;
+ return (enumeratedProvider != null) ? enumeratedProvider.GetKeysFromPrefix(prefix) : null;
+ }
+
+ protected override void InsertItem(int index, IValueProvider item)
+ {
+ if (item == null)
+ {
+ throw Error.ArgumentNull("item");
+ }
+ base.InsertItem(index, item);
+ }
+
+ protected override void SetItem(int index, IValueProvider item)
+ {
+ if (item == null)
+ {
+ throw Error.ArgumentNull("item");
+ }
+ base.SetItem(index, item);
+ }
+ }
+}
diff --git a/src/System.Web.Http/ValueProviders/Providers/CompositeValueProviderFactory.cs b/src/System.Web.Http/ValueProviders/Providers/CompositeValueProviderFactory.cs
new file mode 100644
index 00000000..dcf89404
--- /dev/null
+++ b/src/System.Web.Http/ValueProviders/Providers/CompositeValueProviderFactory.cs
@@ -0,0 +1,22 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Web.Http.Controllers;
+
+namespace System.Web.Http.ValueProviders.Providers
+{
+ public class CompositeValueProviderFactory : ValueProviderFactory
+ {
+ private ValueProviderFactory[] _factories;
+
+ public CompositeValueProviderFactory(IEnumerable<ValueProviderFactory> factories)
+ {
+ _factories = factories.ToArray();
+ }
+
+ public override IValueProvider GetValueProvider(HttpActionContext actionContext)
+ {
+ List<IValueProvider> providers = _factories.Select<ValueProviderFactory, IValueProvider>((f) => f.GetValueProvider(actionContext)).Where((vp) => vp != null).ToList();
+ return new CompositeValueProvider(providers);
+ }
+ }
+}
diff --git a/src/System.Web.Http/ValueProviders/Providers/ElementalValueProvider.cs b/src/System.Web.Http/ValueProviders/Providers/ElementalValueProvider.cs
new file mode 100644
index 00000000..9821cf3c
--- /dev/null
+++ b/src/System.Web.Http/ValueProviders/Providers/ElementalValueProvider.cs
@@ -0,0 +1,34 @@
+using System.Globalization;
+using System.Web.Http.Internal;
+
+namespace System.Web.Http.ValueProviders.Providers
+{
+ // Represents a value provider that contains a single value.
+ internal sealed class ElementalValueProvider : IValueProvider
+ {
+ public ElementalValueProvider(string name, object rawValue, CultureInfo culture)
+ {
+ Name = name;
+ RawValue = rawValue;
+ Culture = culture;
+ }
+
+ public CultureInfo Culture { get; private set; }
+
+ public string Name { get; private set; }
+
+ public object RawValue { get; private set; }
+
+ public bool ContainsPrefix(string prefix)
+ {
+ return ValueProviderUtil.IsPrefixMatch(Name, prefix);
+ }
+
+ public ValueProviderResult GetValue(string key)
+ {
+ return String.Equals(key, Name, StringComparison.OrdinalIgnoreCase)
+ ? new ValueProviderResult(RawValue, Convert.ToString(RawValue, Culture), Culture)
+ : null;
+ }
+ }
+}
diff --git a/src/System.Web.Http/ValueProviders/Providers/KeyValueModelValueProvider.cs b/src/System.Web.Http/ValueProviders/Providers/KeyValueModelValueProvider.cs
new file mode 100644
index 00000000..469c8e7a
--- /dev/null
+++ b/src/System.Web.Http/ValueProviders/Providers/KeyValueModelValueProvider.cs
@@ -0,0 +1,98 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Net.Http.Formatting;
+using System.Web.Http.Common;
+using System.Web.Http.Internal;
+
+namespace System.Web.Http.ValueProviders.Providers
+{
+ /// <summary>
+ /// This class provides a <see cref="IValueProvider"/> facade over a
+ /// <see cref="IKeyValueModel"/> instance.
+ /// </summary>
+ public class KeyValueModelValueProvider : IValueProvider
+ {
+ private readonly Lazy<HashSet<string>> _prefixes;
+ private IKeyValueModel _innerKeyValueProvider;
+ private CultureInfo _culture;
+
+ /// <summary>
+ /// Creates a new instance of the <see cref="KeyValueModelValueProvider"/> class.
+ /// </summary>
+ /// <param name="provider">The inner <see cref="IKeyValueModel"/> to use. It may be null.</param>
+ /// <param name="culture">The culture to use. It cannot be null.</param>
+ public KeyValueModelValueProvider(IKeyValueModel provider, CultureInfo culture)
+ {
+ _innerKeyValueProvider = provider;
+ _culture = culture;
+ _prefixes = new Lazy<HashSet<string>>(CalculatePrefixes, isThreadSafe: true);
+ }
+
+ public virtual bool ContainsPrefix(string prefix)
+ {
+ if (prefix == null)
+ {
+ throw Error.ArgumentNull("prefix");
+ }
+
+ return _prefixes.Value.Contains(prefix);
+ }
+
+ public virtual IDictionary<string, string> GetKeysFromPrefix(string prefix)
+ {
+ if (prefix == null)
+ {
+ throw Error.ArgumentNull("prefix");
+ }
+
+ return ValueProviderUtil.GetKeysFromPrefix(_prefixes.Value, prefix);
+ }
+
+ public virtual ValueProviderResult GetValue(string key)
+ {
+ if (key == null)
+ {
+ throw Error.ArgumentNull("key");
+ }
+
+ if (_innerKeyValueProvider == null)
+ {
+ return null;
+ }
+
+ object value;
+ if (!_innerKeyValueProvider.TryGetValue(key, out value))
+ {
+ return null;
+ }
+
+ return new ValueProviderResult(value, value == null ? String.Empty : value.ToString(), _culture);
+ }
+
+ private HashSet<string> CalculatePrefixes()
+ {
+ HashSet<string> result = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+
+ if (_innerKeyValueProvider != null)
+ {
+ IEnumerable<string> keys = _innerKeyValueProvider.Keys;
+
+ if (keys.Any())
+ {
+ result.Add(String.Empty);
+ }
+
+ foreach (string key in keys)
+ {
+ if (key != null)
+ {
+ result.UnionWith(ValueProviderUtil.GetPrefixes(key));
+ }
+ }
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/src/System.Web.Http/ValueProviders/Providers/NameValueCollectionValueProvider.cs b/src/System.Web.Http/ValueProviders/Providers/NameValueCollectionValueProvider.cs
new file mode 100644
index 00000000..93f9bcad
--- /dev/null
+++ b/src/System.Web.Http/ValueProviders/Providers/NameValueCollectionValueProvider.cs
@@ -0,0 +1,95 @@
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Globalization;
+using System.Web.Http.Common;
+using System.Web.Http.Internal;
+
+namespace System.Web.Http.ValueProviders.Providers
+{
+ public class NameValueCollectionValueProvider : IEnumerableValueProvider
+ {
+ private readonly CultureInfo _culture;
+ private readonly Lazy<HashSet<string>> _prefixes;
+ private readonly Lazy<NameValueCollection> _values;
+
+ public NameValueCollectionValueProvider(NameValueCollection values, CultureInfo culture)
+ {
+ if (values == null)
+ {
+ throw Error.ArgumentNull("values");
+ }
+
+ _values = new Lazy<NameValueCollection>(() => values, isThreadSafe: true);
+ _culture = culture;
+ _prefixes = new Lazy<HashSet<string>>(CalculatePrefixes, isThreadSafe: true);
+ }
+
+ public NameValueCollectionValueProvider(Func<NameValueCollection> valuesFactory, CultureInfo culture)
+ {
+ if (valuesFactory == null)
+ {
+ throw Error.ArgumentNull("valuesFactory");
+ }
+
+ _values = new Lazy<NameValueCollection>(valuesFactory, isThreadSafe: true);
+ _culture = culture;
+ _prefixes = new Lazy<HashSet<string>>(CalculatePrefixes, isThreadSafe: true);
+ }
+
+ private HashSet<string> CalculatePrefixes()
+ {
+ HashSet<string> result = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+
+ if (_values.Value.Count > 0)
+ {
+ result.Add(String.Empty);
+ }
+
+ foreach (string key in _values.Value)
+ {
+ if (key != null)
+ {
+ result.UnionWith(ValueProviderUtil.GetPrefixes(key));
+ }
+ }
+
+ return result;
+ }
+
+ public virtual bool ContainsPrefix(string prefix)
+ {
+ if (prefix == null)
+ {
+ throw Error.ArgumentNull("prefix");
+ }
+
+ return _prefixes.Value.Contains(prefix);
+ }
+
+ public virtual IDictionary<string, string> GetKeysFromPrefix(string prefix)
+ {
+ if (prefix == null)
+ {
+ throw Error.ArgumentNull("prefix");
+ }
+
+ return ValueProviderUtil.GetKeysFromPrefix(_prefixes.Value, prefix);
+ }
+
+ public virtual ValueProviderResult GetValue(string key)
+ {
+ if (key == null)
+ {
+ throw Error.ArgumentNull("key");
+ }
+
+ string[] values = _values.Value.GetValues(key);
+ if (values == null)
+ {
+ return null;
+ }
+
+ return new ValueProviderResult(values, _values.Value[key], _culture);
+ }
+ }
+}
diff --git a/src/System.Web.Http/ValueProviders/Providers/QueryStringValueProvider.cs b/src/System.Web.Http/ValueProviders/Providers/QueryStringValueProvider.cs
new file mode 100644
index 00000000..06115748
--- /dev/null
+++ b/src/System.Web.Http/ValueProviders/Providers/QueryStringValueProvider.cs
@@ -0,0 +1,31 @@
+using System.Collections.Specialized;
+using System.Globalization;
+using System.Net.Http.Formatting;
+using System.Web.Http.Controllers;
+using System.Web.Http.Internal;
+using System.Web.Http.ModelBinding;
+
+namespace System.Web.Http.ValueProviders.Providers
+{
+ public class QueryStringValueProvider : NameValueCollectionValueProvider
+ {
+ public QueryStringValueProvider(HttpActionContext actionContext, CultureInfo culture)
+ : base(() => ParseQueryString(actionContext.ControllerContext.Request.RequestUri), culture)
+ {
+ }
+
+ internal static NameValueCollection ParseQueryString(Uri uri)
+ {
+ // Unit tests may not always provide a Uri in the request
+ if (uri == null)
+ {
+ return new NameValueCollection();
+ }
+
+ // Uri --> FormData --> NVC
+ FormDataCollection formData = new FormDataCollection(uri);
+ NameValueCollection nvc = formData.GetJQueryValueNameValueCollection();
+ return nvc;
+ }
+ }
+}
diff --git a/src/System.Web.Http/ValueProviders/Providers/QueryStringValueProviderFactory.cs b/src/System.Web.Http/ValueProviders/Providers/QueryStringValueProviderFactory.cs
new file mode 100644
index 00000000..503fb729
--- /dev/null
+++ b/src/System.Web.Http/ValueProviders/Providers/QueryStringValueProviderFactory.cs
@@ -0,0 +1,13 @@
+using System.Globalization;
+using System.Web.Http.Controllers;
+
+namespace System.Web.Http.ValueProviders.Providers
+{
+ public class QueryStringValueProviderFactory : ValueProviderFactory, IUriValueProviderFactory
+ {
+ public override IValueProvider GetValueProvider(HttpActionContext actionContext)
+ {
+ return new QueryStringValueProvider(actionContext, CultureInfo.CurrentCulture);
+ }
+ }
+}
diff --git a/src/System.Web.Http/ValueProviders/Providers/RouteDataValueProvider.cs b/src/System.Web.Http/ValueProviders/Providers/RouteDataValueProvider.cs
new file mode 100644
index 00000000..56d19002
--- /dev/null
+++ b/src/System.Web.Http/ValueProviders/Providers/RouteDataValueProvider.cs
@@ -0,0 +1,28 @@
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Globalization;
+using System.Web.Http.Controllers;
+using System.Web.Http.Routing;
+
+namespace System.Web.Http.ValueProviders.Providers
+{
+ public class RouteDataValueProvider : NameValueCollectionValueProvider
+ {
+ public RouteDataValueProvider(HttpActionContext actionContext, CultureInfo culture)
+ : base(() => GetRoutes(actionContext.ControllerContext.RouteData), culture)
+ {
+ }
+
+ internal static NameValueCollection GetRoutes(IHttpRouteData routeData)
+ {
+ //// REVIEW: better way to map KeyValuePairs into NameValueCollection
+ NameValueCollection nameValueCollection = new NameValueCollection();
+ foreach (KeyValuePair<string, object> pair in routeData.Values)
+ {
+ nameValueCollection.Add(pair.Key, pair.Value.ToString());
+ }
+
+ return nameValueCollection;
+ }
+ }
+}
diff --git a/src/System.Web.Http/ValueProviders/Providers/RouteDataValueProviderFactory.cs b/src/System.Web.Http/ValueProviders/Providers/RouteDataValueProviderFactory.cs
new file mode 100644
index 00000000..c99bb035
--- /dev/null
+++ b/src/System.Web.Http/ValueProviders/Providers/RouteDataValueProviderFactory.cs
@@ -0,0 +1,17 @@
+using System.Globalization;
+using System.Web.Http.Controllers;
+
+namespace System.Web.Http.ValueProviders.Providers
+{
+ public class RouteDataValueProviderFactory : ValueProviderFactory, IUriValueProviderFactory
+ {
+ public RouteDataValueProviderFactory()
+ {
+ }
+
+ public override IValueProvider GetValueProvider(HttpActionContext actionContext)
+ {
+ return new RouteDataValueProvider(actionContext, CultureInfo.InvariantCulture);
+ }
+ }
+}
diff --git a/src/System.Web.Http/ValueProviders/SingleObjectKeyValueModel.cs b/src/System.Web.Http/ValueProviders/SingleObjectKeyValueModel.cs
new file mode 100644
index 00000000..6ede8bca
--- /dev/null
+++ b/src/System.Web.Http/ValueProviders/SingleObjectKeyValueModel.cs
@@ -0,0 +1,49 @@
+using System.Collections.Generic;
+using System.Net.Http.Formatting;
+using System.Web.Http.Common;
+
+namespace System.Web.Http.ValueProviders
+{
+ /// <summary>
+ /// This internal class exposes a single named value via a <see cref="IKeyValueModel"/> facade.
+ /// It exists primarily to expose the request body content as a single value.
+ /// </summary>
+ internal class SingleObjectKeyValueModel : IKeyValueModel
+ {
+ private string _name;
+ private object _value;
+
+ public SingleObjectKeyValueModel(string name, object value)
+ {
+ if (name == null)
+ {
+ throw Error.ArgumentNull("name");
+ }
+
+ _name = name;
+ _value = value;
+ }
+
+ public IEnumerable<string> Keys
+ {
+ get { return new string[] { _name }; }
+ }
+
+ public bool TryGetValue(string key, out object value)
+ {
+ if (key == null)
+ {
+ throw Error.ArgumentNull("key");
+ }
+
+ if (String.Equals(key, _name, StringComparison.OrdinalIgnoreCase))
+ {
+ value = _value;
+ return true;
+ }
+
+ value = null;
+ return false;
+ }
+ }
+}
diff --git a/src/System.Web.Http/ValueProviders/ValueProviderAttribute.cs b/src/System.Web.Http/ValueProviders/ValueProviderAttribute.cs
new file mode 100644
index 00000000..f95d5c84
--- /dev/null
+++ b/src/System.Web.Http/ValueProviders/ValueProviderAttribute.cs
@@ -0,0 +1,56 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Web.Http.Common;
+using System.Web.Http.ModelBinding;
+using System.Web.Http.Properties;
+
+namespace System.Web.Http.ValueProviders
+{
+ /// <summary>
+ /// This attribute is used to specify a custom <see cref="ValueProviderFactory"/>.
+ /// </summary>
+ [SuppressMessage("Microsoft.Design", "CA1019:DefineAccessorsForAttributeArguments", Justification = "property already exposed in plural form")]
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Parameter, Inherited = true, AllowMultiple = false)]
+ public sealed class ValueProviderAttribute : ModelBinderAttribute
+ {
+ private readonly Type[] _valueProviderFactoryTypes;
+
+ // Provide CLS compliant overload
+ public ValueProviderAttribute(Type valueProviderFactory)
+ : this(new Type[] { valueProviderFactory })
+ {
+ }
+
+ // Convenience for multiple types. This is not cls-compliant.
+ public ValueProviderAttribute(params Type[] valueProviderFactories)
+ {
+ _valueProviderFactoryTypes = valueProviderFactories;
+ }
+
+ public IEnumerable<Type> ValueProviderFactoryTypes
+ {
+ get { return _valueProviderFactoryTypes; }
+ }
+
+ public override IEnumerable<ValueProviderFactory> GetValueProviderFactories(HttpConfiguration configuration)
+ {
+ // By default, just get all registered value provider factories
+ return Array.ConvertAll(_valueProviderFactoryTypes, Instantiate);
+ }
+
+ private static ValueProviderFactory Instantiate(Type factoryType)
+ {
+ if (factoryType == null)
+ {
+ throw new ArgumentNullException("factoryType");
+ }
+
+ if (!typeof(ValueProviderFactory).IsAssignableFrom(factoryType))
+ {
+ throw Error.InvalidOperation(SRResources.ValueProviderFactory_Cannot_Create, typeof(ValueProviderFactory), factoryType);
+ }
+
+ return (ValueProviderFactory)Activator.CreateInstance(factoryType);
+ }
+ }
+}
diff --git a/src/System.Web.Http/ValueProviders/ValueProviderFactory.cs b/src/System.Web.Http/ValueProviders/ValueProviderFactory.cs
new file mode 100644
index 00000000..d6a5cbc2
--- /dev/null
+++ b/src/System.Web.Http/ValueProviders/ValueProviderFactory.cs
@@ -0,0 +1,9 @@
+using System.Web.Http.Controllers;
+
+namespace System.Web.Http.ValueProviders
+{
+ public abstract class ValueProviderFactory
+ {
+ public abstract IValueProvider GetValueProvider(HttpActionContext actionContext);
+ }
+}
diff --git a/src/System.Web.Http/ValueProviders/ValueProviderResult.cs b/src/System.Web.Http/ValueProviders/ValueProviderResult.cs
new file mode 100644
index 00000000..77f38591
--- /dev/null
+++ b/src/System.Web.Http/ValueProviders/ValueProviderResult.cs
@@ -0,0 +1,160 @@
+using System.Collections;
+using System.ComponentModel;
+using System.Globalization;
+using System.Web.Http.Common;
+using System.Web.Http.Properties;
+
+namespace System.Web.Http.ValueProviders
+{
+ [Serializable]
+ public class ValueProviderResult
+ {
+ private static readonly CultureInfo _staticCulture = CultureInfo.InvariantCulture;
+ private CultureInfo _instanceCulture;
+
+ // default constructor so that subclassed types can set the properties themselves
+ protected ValueProviderResult()
+ {
+ }
+
+ public ValueProviderResult(object rawValue, string attemptedValue, CultureInfo culture)
+ {
+ RawValue = rawValue;
+ AttemptedValue = attemptedValue;
+ Culture = culture;
+ }
+
+ public string AttemptedValue { get; protected set; }
+
+ public CultureInfo Culture
+ {
+ get
+ {
+ if (_instanceCulture == null)
+ {
+ _instanceCulture = _staticCulture;
+ }
+ return _instanceCulture;
+ }
+ protected set { _instanceCulture = value; }
+ }
+
+ public object RawValue { get; protected set; }
+
+ private static object ConvertSimpleType(CultureInfo culture, object value, Type destinationType)
+ {
+ if (value == null || destinationType.IsInstanceOfType(value))
+ {
+ return value;
+ }
+
+ // if this is a user-input value but the user didn't type anything, return no value
+ string valueAsString = value as string;
+ if (valueAsString != null && valueAsString.Trim().Length == 0)
+ {
+ return null;
+ }
+
+ TypeConverter converter = TypeDescriptor.GetConverter(destinationType);
+ bool canConvertFrom = converter.CanConvertFrom(value.GetType());
+ if (!canConvertFrom)
+ {
+ converter = TypeDescriptor.GetConverter(value.GetType());
+ }
+ if (!(canConvertFrom || converter.CanConvertTo(destinationType)))
+ {
+ // EnumConverter cannot convert integer, so we verify manually
+ if (destinationType.IsEnum && value is int)
+ {
+ return Enum.ToObject(destinationType, (int)value);
+ }
+
+ // In case of a Nullable object, we try again with its underlying type.
+ Type underlyingType = Nullable.GetUnderlyingType(destinationType);
+ if (underlyingType != null)
+ {
+ return ConvertSimpleType(culture, value, underlyingType);
+ }
+
+ throw Error.InvalidOperation(SRResources.ValueProviderResult_NoConverterExists, value.GetType(), destinationType);
+ }
+
+ try
+ {
+ return canConvertFrom
+ ? converter.ConvertFrom(null, culture, value)
+ : converter.ConvertTo(null, culture, value, destinationType);
+ }
+ catch (Exception ex)
+ {
+ throw Error.InvalidOperation(ex, SRResources.ValueProviderResult_ConversionThrew, value.GetType(), destinationType);
+ }
+ }
+
+ public object ConvertTo(Type type)
+ {
+ return ConvertTo(type, null /* culture */);
+ }
+
+ public virtual object ConvertTo(Type type, CultureInfo culture)
+ {
+ if (type == null)
+ {
+ throw Error.ArgumentNull("type");
+ }
+
+ CultureInfo cultureToUse = culture ?? Culture;
+ return UnwrapPossibleArrayType(cultureToUse, RawValue, type);
+ }
+
+ private static object UnwrapPossibleArrayType(CultureInfo culture, object value, Type destinationType)
+ {
+ if (value == null || destinationType.IsInstanceOfType(value))
+ {
+ return value;
+ }
+
+ // array conversion results in four cases, as below
+ Array valueAsArray = value as Array;
+ if (destinationType.IsArray)
+ {
+ Type destinationElementType = destinationType.GetElementType();
+ if (valueAsArray != null)
+ {
+ // case 1: both destination + source type are arrays, so convert each element
+ IList converted = Array.CreateInstance(destinationElementType, valueAsArray.Length);
+ for (int i = 0; i < valueAsArray.Length; i++)
+ {
+ converted[i] = ConvertSimpleType(culture, valueAsArray.GetValue(i), destinationElementType);
+ }
+ return converted;
+ }
+ else
+ {
+ // case 2: destination type is array but source is single element, so wrap element in array + convert
+ object element = ConvertSimpleType(culture, value, destinationElementType);
+ IList converted = Array.CreateInstance(destinationElementType, 1);
+ converted[0] = element;
+ return converted;
+ }
+ }
+ else if (valueAsArray != null)
+ {
+ // case 3: destination type is single element but source is array, so extract first element + convert
+ if (valueAsArray.Length > 0)
+ {
+ value = valueAsArray.GetValue(0);
+ return ConvertSimpleType(culture, value, destinationType);
+ }
+ else
+ {
+ // case 3(a): source is empty array, so can't perform conversion
+ return null;
+ }
+ }
+
+ // case 4: both destination + source type are single elements, so convert
+ return ConvertSimpleType(culture, value, destinationType);
+ }
+ }
+}
diff --git a/src/System.Web.Http/packages.config b/src/System.Web.Http/packages.config
new file mode 100644
index 00000000..18eb4fa0
--- /dev/null
+++ b/src/System.Web.Http/packages.config
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="Microsoft.Net.Http" version="2.0.20302.1" />
+ <package id="Microsoft.Web.Infrastructure" version="1.0.0.0" />
+</packages> \ No newline at end of file
diff --git a/src/System.Web.Mvc/AcceptVerbsAttribute.cs b/src/System.Web.Mvc/AcceptVerbsAttribute.cs
new file mode 100644
index 00000000..1783cdfe
--- /dev/null
+++ b/src/System.Web.Mvc/AcceptVerbsAttribute.cs
@@ -0,0 +1,64 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Reflection;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc
+{
+ [SuppressMessage("Microsoft.Design", "CA1019:DefineAccessorsForAttributeArguments", Justification = "The accessor is exposed as an ICollection<string>.")]
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+ public sealed class AcceptVerbsAttribute : ActionMethodSelectorAttribute
+ {
+ public AcceptVerbsAttribute(HttpVerbs verbs)
+ : this(EnumToArray(verbs))
+ {
+ }
+
+ public AcceptVerbsAttribute(params string[] verbs)
+ {
+ if (verbs == null || verbs.Length == 0)
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "verbs");
+ }
+
+ Verbs = new ReadOnlyCollection<string>(verbs);
+ }
+
+ public ICollection<string> Verbs { get; private set; }
+
+ private static void AddEntryToList(HttpVerbs verbs, HttpVerbs match, List<string> verbList, string entryText)
+ {
+ if ((verbs & match) != 0)
+ {
+ verbList.Add(entryText);
+ }
+ }
+
+ internal static string[] EnumToArray(HttpVerbs verbs)
+ {
+ List<string> verbList = new List<string>();
+
+ AddEntryToList(verbs, HttpVerbs.Get, verbList, "GET");
+ AddEntryToList(verbs, HttpVerbs.Post, verbList, "POST");
+ AddEntryToList(verbs, HttpVerbs.Put, verbList, "PUT");
+ AddEntryToList(verbs, HttpVerbs.Delete, verbList, "DELETE");
+ AddEntryToList(verbs, HttpVerbs.Head, verbList, "HEAD");
+
+ return verbList.ToArray();
+ }
+
+ public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
+ {
+ if (controllerContext == null)
+ {
+ throw new ArgumentNullException("controllerContext");
+ }
+
+ string incomingVerb = controllerContext.HttpContext.Request.GetHttpMethodOverride();
+
+ return Verbs.Contains(incomingVerb, StringComparer.OrdinalIgnoreCase);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ActionDescriptor.cs b/src/System.Web.Mvc/ActionDescriptor.cs
new file mode 100644
index 00000000..7175bdc4
--- /dev/null
+++ b/src/System.Web.Mvc/ActionDescriptor.cs
@@ -0,0 +1,196 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using System.Reflection;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc
+{
+ public abstract class ActionDescriptor : ICustomAttributeProvider, IUniquelyIdentifiable
+ {
+ private static readonly ActionMethodDispatcherCache _staticDispatcherCache = new ActionMethodDispatcherCache();
+
+ private static readonly ActionSelector[] _emptySelectors = new ActionSelector[0];
+ private readonly Lazy<string> _uniqueId;
+ private ActionMethodDispatcherCache _instanceDispatcherCache;
+
+ protected ActionDescriptor()
+ {
+ _uniqueId = new Lazy<string>(CreateUniqueId);
+ }
+
+ public abstract string ActionName { get; }
+
+ public abstract ControllerDescriptor ControllerDescriptor { get; }
+
+ internal ActionMethodDispatcherCache DispatcherCache
+ {
+ get
+ {
+ if (_instanceDispatcherCache == null)
+ {
+ _instanceDispatcherCache = _staticDispatcherCache;
+ }
+ return _instanceDispatcherCache;
+ }
+ set { _instanceDispatcherCache = value; }
+ }
+
+ [SuppressMessage("Microsoft.Security", "CA2119:SealMethodsThatSatisfyPrivateInterfaces", Justification = "This is overridden elsewhere in System.Web.Mvc")]
+ public virtual string UniqueId
+ {
+ get { return _uniqueId.Value; }
+ }
+
+ private string CreateUniqueId()
+ {
+ return DescriptorUtil.CreateUniqueId(GetType(), ControllerDescriptor, ActionName);
+ }
+
+ public abstract object Execute(ControllerContext controllerContext, IDictionary<string, object> parameters);
+
+ internal static object ExtractParameterFromDictionary(ParameterInfo parameterInfo, IDictionary<string, object> parameters, MethodInfo methodInfo)
+ {
+ object value;
+
+ if (!parameters.TryGetValue(parameterInfo.Name, out value))
+ {
+ // the key should always be present, even if the parameter value is null
+ string message = String.Format(CultureInfo.CurrentCulture, MvcResources.ReflectedActionDescriptor_ParameterNotInDictionary,
+ parameterInfo.Name, parameterInfo.ParameterType, methodInfo, methodInfo.DeclaringType);
+ throw new ArgumentException(message, "parameters");
+ }
+
+ if (value == null && !TypeHelpers.TypeAllowsNullValue(parameterInfo.ParameterType))
+ {
+ // tried to pass a null value for a non-nullable parameter type
+ string message = String.Format(CultureInfo.CurrentCulture, MvcResources.ReflectedActionDescriptor_ParameterCannotBeNull,
+ parameterInfo.Name, parameterInfo.ParameterType, methodInfo, methodInfo.DeclaringType);
+ throw new ArgumentException(message, "parameters");
+ }
+
+ if (value != null && !parameterInfo.ParameterType.IsInstanceOfType(value))
+ {
+ // value was supplied but is not of the proper type
+ string message = String.Format(CultureInfo.CurrentCulture, MvcResources.ReflectedActionDescriptor_ParameterValueHasWrongType,
+ parameterInfo.Name, methodInfo, methodInfo.DeclaringType, value.GetType(), parameterInfo.ParameterType);
+ throw new ArgumentException(message, "parameters");
+ }
+
+ return value;
+ }
+
+ internal static object ExtractParameterOrDefaultFromDictionary(ParameterInfo parameterInfo, IDictionary<string, object> parameters)
+ {
+ Type parameterType = parameterInfo.ParameterType;
+
+ object value;
+ parameters.TryGetValue(parameterInfo.Name, out value);
+
+ // if wrong type, replace with default instance
+ if (parameterType.IsInstanceOfType(value))
+ {
+ return value;
+ }
+ else
+ {
+ object defaultValue;
+ if (ParameterInfoUtil.TryGetDefaultValue(parameterInfo, out defaultValue))
+ {
+ return defaultValue;
+ }
+ else
+ {
+ return TypeHelpers.GetDefaultValue(parameterType);
+ }
+ }
+ }
+
+ public virtual object[] GetCustomAttributes(bool inherit)
+ {
+ return GetCustomAttributes(typeof(object), inherit);
+ }
+
+ public virtual object[] GetCustomAttributes(Type attributeType, bool inherit)
+ {
+ if (attributeType == null)
+ {
+ throw new ArgumentNullException("attributeType");
+ }
+
+ return (object[])Array.CreateInstance(attributeType, 0);
+ }
+
+ public virtual IEnumerable<FilterAttribute> GetFilterAttributes(bool useCache)
+ {
+ return GetCustomAttributes(typeof(FilterAttribute), inherit: true).Cast<FilterAttribute>();
+ }
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ [Obsolete("Please call System.Web.Mvc.FilterProviders.Providers.GetFilters() now.", true)]
+ public virtual FilterInfo GetFilters()
+ {
+ return new FilterInfo();
+ }
+
+ public abstract ParameterDescriptor[] GetParameters();
+
+ [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This method may perform non-trivial work.")]
+ public virtual ICollection<ActionSelector> GetSelectors()
+ {
+ return _emptySelectors;
+ }
+
+ public virtual bool IsDefined(Type attributeType, bool inherit)
+ {
+ if (attributeType == null)
+ {
+ throw new ArgumentNullException("attributeType");
+ }
+
+ return false;
+ }
+
+ internal static string VerifyActionMethodIsCallable(MethodInfo methodInfo)
+ {
+ // we can't call static methods
+ if (methodInfo.IsStatic)
+ {
+ return String.Format(CultureInfo.CurrentCulture,
+ MvcResources.ReflectedActionDescriptor_CannotCallStaticMethod,
+ methodInfo,
+ methodInfo.ReflectedType.FullName);
+ }
+
+ // we can't call instance methods where the 'this' parameter is a type other than ControllerBase
+ if (!typeof(ControllerBase).IsAssignableFrom(methodInfo.ReflectedType))
+ {
+ return String.Format(CultureInfo.CurrentCulture, MvcResources.ReflectedActionDescriptor_CannotCallInstanceMethodOnNonControllerType,
+ methodInfo, methodInfo.ReflectedType.FullName);
+ }
+
+ // we can't call methods with open generic type parameters
+ if (methodInfo.ContainsGenericParameters)
+ {
+ return String.Format(CultureInfo.CurrentCulture, MvcResources.ReflectedActionDescriptor_CannotCallOpenGenericMethods,
+ methodInfo, methodInfo.ReflectedType.FullName);
+ }
+
+ // we can't call methods with ref/out parameters
+ ParameterInfo[] parameterInfos = methodInfo.GetParameters();
+ foreach (ParameterInfo parameterInfo in parameterInfos)
+ {
+ if (parameterInfo.IsOut || parameterInfo.ParameterType.IsByRef)
+ {
+ return String.Format(CultureInfo.CurrentCulture, MvcResources.ReflectedActionDescriptor_CannotCallMethodsWithOutOrRefParameters,
+ methodInfo, methodInfo.ReflectedType.FullName, parameterInfo);
+ }
+ }
+
+ // we can call this method
+ return null;
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ActionDescriptorHelper.cs b/src/System.Web.Mvc/ActionDescriptorHelper.cs
new file mode 100644
index 00000000..1d659c0e
--- /dev/null
+++ b/src/System.Web.Mvc/ActionDescriptorHelper.cs
@@ -0,0 +1,46 @@
+using System.Collections.Generic;
+using System.Reflection;
+
+namespace System.Web.Mvc
+{
+ internal static class ActionDescriptorHelper
+ {
+ public static ICollection<ActionSelector> GetSelectors(MethodInfo methodInfo)
+ {
+ ActionMethodSelectorAttribute[] attrs = (ActionMethodSelectorAttribute[])methodInfo.GetCustomAttributes(typeof(ActionMethodSelectorAttribute), inherit: true);
+ ActionSelector[] selectors = Array.ConvertAll(attrs, attr => (ActionSelector)(controllerContext => attr.IsValidForRequest(controllerContext, methodInfo)));
+ return selectors;
+ }
+
+ public static bool IsDefined(MemberInfo methodInfo, Type attributeType, bool inherit)
+ {
+ return methodInfo.IsDefined(attributeType, inherit);
+ }
+
+ public static object[] GetCustomAttributes(MemberInfo methodInfo, bool inherit)
+ {
+ return methodInfo.GetCustomAttributes(inherit);
+ }
+
+ public static object[] GetCustomAttributes(MemberInfo methodInfo, Type attributeType, bool inherit)
+ {
+ return methodInfo.GetCustomAttributes(attributeType, inherit);
+ }
+
+ public static ParameterDescriptor[] GetParameters(ActionDescriptor actionDescriptor, MethodInfo methodInfo, ref ParameterDescriptor[] parametersCache)
+ {
+ ParameterDescriptor[] parameters = LazilyFetchParametersCollection(actionDescriptor, methodInfo, ref parametersCache);
+
+ // need to clone array so that user modifications aren't accidentally stored
+ return (ParameterDescriptor[])parameters.Clone();
+ }
+
+ private static ParameterDescriptor[] LazilyFetchParametersCollection(ActionDescriptor actionDescriptor, MethodInfo methodInfo, ref ParameterDescriptor[] parametersCache)
+ {
+ return DescriptorUtil.LazilyFetchOrCreateDescriptors<ParameterInfo, ParameterDescriptor>(
+ cacheLocation: ref parametersCache,
+ initializer: methodInfo.GetParameters,
+ converter: parameterInfo => new ReflectedParameterDescriptor(parameterInfo, actionDescriptor));
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ActionExecutedContext.cs b/src/System.Web.Mvc/ActionExecutedContext.cs
new file mode 100644
index 00000000..0ed6b178
--- /dev/null
+++ b/src/System.Web.Mvc/ActionExecutedContext.cs
@@ -0,0 +1,42 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Web.Mvc
+{
+ public class ActionExecutedContext : ControllerContext
+ {
+ private ActionResult _result;
+
+ // parameterless constructor used for mocking
+ public ActionExecutedContext()
+ {
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "The virtual property setters are only to support mocking frameworks, in which case this constructor shouldn't be called anyway.")]
+ public ActionExecutedContext(ControllerContext controllerContext, ActionDescriptor actionDescriptor, bool canceled, Exception exception)
+ : base(controllerContext)
+ {
+ if (actionDescriptor == null)
+ {
+ throw new ArgumentNullException("actionDescriptor");
+ }
+
+ ActionDescriptor = actionDescriptor;
+ Canceled = canceled;
+ Exception = exception;
+ }
+
+ public virtual ActionDescriptor ActionDescriptor { get; set; }
+
+ public virtual bool Canceled { get; set; }
+
+ public virtual Exception Exception { get; set; }
+
+ public bool ExceptionHandled { get; set; }
+
+ public ActionResult Result
+ {
+ get { return _result ?? EmptyResult.Instance; }
+ set { _result = value; }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ActionExecutingContext.cs b/src/System.Web.Mvc/ActionExecutingContext.cs
new file mode 100644
index 00000000..1a66c62c
--- /dev/null
+++ b/src/System.Web.Mvc/ActionExecutingContext.cs
@@ -0,0 +1,37 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Web.Mvc
+{
+ public class ActionExecutingContext : ControllerContext
+ {
+ // parameterless constructor used for mocking
+ public ActionExecutingContext()
+ {
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "The virtual property setters are only to support mocking frameworks, in which case this constructor shouldn't be called anyway.")]
+ public ActionExecutingContext(ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary<string, object> actionParameters)
+ : base(controllerContext)
+ {
+ if (actionDescriptor == null)
+ {
+ throw new ArgumentNullException("actionDescriptor");
+ }
+ if (actionParameters == null)
+ {
+ throw new ArgumentNullException("actionParameters");
+ }
+
+ ActionDescriptor = actionDescriptor;
+ ActionParameters = actionParameters;
+ }
+
+ public virtual ActionDescriptor ActionDescriptor { get; set; }
+
+ [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "The property setter is only here to support mocking this type and should not be called at runtime.")]
+ public virtual IDictionary<string, object> ActionParameters { get; set; }
+
+ public ActionResult Result { get; set; }
+ }
+}
diff --git a/src/System.Web.Mvc/ActionFilterAttribute.cs b/src/System.Web.Mvc/ActionFilterAttribute.cs
new file mode 100644
index 00000000..a27c5fa3
--- /dev/null
+++ b/src/System.Web.Mvc/ActionFilterAttribute.cs
@@ -0,0 +1,25 @@
+namespace System.Web.Mvc
+{
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
+ public abstract class ActionFilterAttribute : FilterAttribute, IActionFilter, IResultFilter
+ {
+ // The OnXxx() methods are virtual rather than abstract so that a developer need override
+ // only the ones that interest him.
+
+ public virtual void OnActionExecuting(ActionExecutingContext filterContext)
+ {
+ }
+
+ public virtual void OnActionExecuted(ActionExecutedContext filterContext)
+ {
+ }
+
+ public virtual void OnResultExecuting(ResultExecutingContext filterContext)
+ {
+ }
+
+ public virtual void OnResultExecuted(ResultExecutedContext filterContext)
+ {
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ActionMethodDispatcher.cs b/src/System.Web.Mvc/ActionMethodDispatcher.cs
new file mode 100644
index 00000000..3fc3a0b9
--- /dev/null
+++ b/src/System.Web.Mvc/ActionMethodDispatcher.cs
@@ -0,0 +1,79 @@
+using System.Collections.Generic;
+using System.Linq.Expressions;
+using System.Reflection;
+
+namespace System.Web.Mvc
+{
+ // The methods in this class don't perform error checking; that is the responsibility of the
+ // caller.
+ internal sealed class ActionMethodDispatcher
+ {
+ private ActionExecutor _executor;
+
+ public ActionMethodDispatcher(MethodInfo methodInfo)
+ {
+ _executor = GetExecutor(methodInfo);
+ MethodInfo = methodInfo;
+ }
+
+ private delegate object ActionExecutor(ControllerBase controller, object[] parameters);
+
+ private delegate void VoidActionExecutor(ControllerBase controller, object[] parameters);
+
+ public MethodInfo MethodInfo { get; private set; }
+
+ public object Execute(ControllerBase controller, object[] parameters)
+ {
+ return _executor(controller, parameters);
+ }
+
+ private static ActionExecutor GetExecutor(MethodInfo methodInfo)
+ {
+ // Parameters to executor
+ ParameterExpression controllerParameter = Expression.Parameter(typeof(ControllerBase), "controller");
+ ParameterExpression parametersParameter = Expression.Parameter(typeof(object[]), "parameters");
+
+ // Build parameter list
+ List<Expression> parameters = new List<Expression>();
+ ParameterInfo[] paramInfos = methodInfo.GetParameters();
+ for (int i = 0; i < paramInfos.Length; i++)
+ {
+ ParameterInfo paramInfo = paramInfos[i];
+ BinaryExpression valueObj = Expression.ArrayIndex(parametersParameter, Expression.Constant(i));
+ UnaryExpression valueCast = Expression.Convert(valueObj, paramInfo.ParameterType);
+
+ // valueCast is "(Ti) parameters[i]"
+ parameters.Add(valueCast);
+ }
+
+ // Call method
+ UnaryExpression instanceCast = (!methodInfo.IsStatic) ? Expression.Convert(controllerParameter, methodInfo.ReflectedType) : null;
+ MethodCallExpression methodCall = methodCall = Expression.Call(instanceCast, methodInfo, parameters);
+
+ // methodCall is "((TController) controller) method((T0) parameters[0], (T1) parameters[1], ...)"
+ // Create function
+ if (methodCall.Type == typeof(void))
+ {
+ Expression<VoidActionExecutor> lambda = Expression.Lambda<VoidActionExecutor>(methodCall, controllerParameter, parametersParameter);
+ VoidActionExecutor voidExecutor = lambda.Compile();
+ return WrapVoidAction(voidExecutor);
+ }
+ else
+ {
+ // must coerce methodCall to match ActionExecutor signature
+ UnaryExpression castMethodCall = Expression.Convert(methodCall, typeof(object));
+ Expression<ActionExecutor> lambda = Expression.Lambda<ActionExecutor>(castMethodCall, controllerParameter, parametersParameter);
+ return lambda.Compile();
+ }
+ }
+
+ private static ActionExecutor WrapVoidAction(VoidActionExecutor executor)
+ {
+ return delegate(ControllerBase controller, object[] parameters)
+ {
+ executor(controller, parameters);
+ return null;
+ };
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ActionMethodDispatcherCache.cs b/src/System.Web.Mvc/ActionMethodDispatcherCache.cs
new file mode 100644
index 00000000..cdfd57af
--- /dev/null
+++ b/src/System.Web.Mvc/ActionMethodDispatcherCache.cs
@@ -0,0 +1,16 @@
+using System.Reflection;
+
+namespace System.Web.Mvc
+{
+ internal sealed class ActionMethodDispatcherCache : ReaderWriterCache<MethodInfo, ActionMethodDispatcher>
+ {
+ public ActionMethodDispatcherCache()
+ {
+ }
+
+ public ActionMethodDispatcher GetDispatcher(MethodInfo methodInfo)
+ {
+ return FetchOrCreateItem(methodInfo, () => new ActionMethodDispatcher(methodInfo));
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ActionMethodSelector.cs b/src/System.Web.Mvc/ActionMethodSelector.cs
new file mode 100644
index 00000000..834b296a
--- /dev/null
+++ b/src/System.Web.Mvc/ActionMethodSelector.cs
@@ -0,0 +1,116 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc
+{
+ internal sealed class ActionMethodSelector
+ {
+ public ActionMethodSelector(Type controllerType)
+ {
+ ControllerType = controllerType;
+ PopulateLookupTables();
+ }
+
+ public Type ControllerType { get; private set; }
+
+ public MethodInfo[] AliasedMethods { get; private set; }
+
+ public ILookup<string, MethodInfo> NonAliasedMethods { get; private set; }
+
+ private AmbiguousMatchException CreateAmbiguousMatchException(List<MethodInfo> ambiguousMethods, string actionName)
+ {
+ StringBuilder exceptionMessageBuilder = new StringBuilder();
+ foreach (MethodInfo methodInfo in ambiguousMethods)
+ {
+ string controllerAction = Convert.ToString(methodInfo, CultureInfo.CurrentCulture);
+ string controllerType = methodInfo.DeclaringType.FullName;
+ exceptionMessageBuilder.AppendLine();
+ exceptionMessageBuilder.AppendFormat(CultureInfo.CurrentCulture, MvcResources.ActionMethodSelector_AmbiguousMatchType, controllerAction, controllerType);
+ }
+ string message = String.Format(CultureInfo.CurrentCulture, MvcResources.ActionMethodSelector_AmbiguousMatch,
+ actionName, ControllerType.Name, exceptionMessageBuilder);
+ return new AmbiguousMatchException(message);
+ }
+
+ public MethodInfo FindActionMethod(ControllerContext controllerContext, string actionName)
+ {
+ List<MethodInfo> methodsMatchingName = GetMatchingAliasedMethods(controllerContext, actionName);
+ methodsMatchingName.AddRange(NonAliasedMethods[actionName]);
+ List<MethodInfo> finalMethods = RunSelectionFilters(controllerContext, methodsMatchingName);
+
+ switch (finalMethods.Count)
+ {
+ case 0:
+ return null;
+
+ case 1:
+ return finalMethods[0];
+
+ default:
+ throw CreateAmbiguousMatchException(finalMethods, actionName);
+ }
+ }
+
+ internal List<MethodInfo> GetMatchingAliasedMethods(ControllerContext controllerContext, string actionName)
+ {
+ // find all aliased methods which are opting in to this request
+ // to opt in, all attributes defined on the method must return true
+
+ var methods = from methodInfo in AliasedMethods
+ let attrs = ReflectedAttributeCache.GetActionNameSelectorAttributes(methodInfo)
+ where attrs.All(attr => attr.IsValidName(controllerContext, actionName, methodInfo))
+ select methodInfo;
+ return methods.ToList();
+ }
+
+ private static bool IsMethodDecoratedWithAliasingAttribute(MethodInfo methodInfo)
+ {
+ return methodInfo.IsDefined(typeof(ActionNameSelectorAttribute), true /* inherit */);
+ }
+
+ private static bool IsValidActionMethod(MethodInfo methodInfo)
+ {
+ return !(methodInfo.IsSpecialName ||
+ methodInfo.GetBaseDefinition().DeclaringType.IsAssignableFrom(typeof(Controller)));
+ }
+
+ private void PopulateLookupTables()
+ {
+ MethodInfo[] allMethods = ControllerType.GetMethods(BindingFlags.InvokeMethod | BindingFlags.Instance | BindingFlags.Public);
+ MethodInfo[] actionMethods = Array.FindAll(allMethods, IsValidActionMethod);
+
+ AliasedMethods = Array.FindAll(actionMethods, IsMethodDecoratedWithAliasingAttribute);
+ NonAliasedMethods = actionMethods.Except(AliasedMethods).ToLookup(method => method.Name, StringComparer.OrdinalIgnoreCase);
+ }
+
+ private static List<MethodInfo> RunSelectionFilters(ControllerContext controllerContext, List<MethodInfo> methodInfos)
+ {
+ // remove all methods which are opting out of this request
+ // to opt out, at least one attribute defined on the method must return false
+
+ List<MethodInfo> matchesWithSelectionAttributes = new List<MethodInfo>();
+ List<MethodInfo> matchesWithoutSelectionAttributes = new List<MethodInfo>();
+
+ foreach (MethodInfo methodInfo in methodInfos)
+ {
+ ICollection<ActionMethodSelectorAttribute> attrs = ReflectedAttributeCache.GetActionMethodSelectorAttributes(methodInfo);
+ if (attrs.Count == 0)
+ {
+ matchesWithoutSelectionAttributes.Add(methodInfo);
+ }
+ else if (attrs.All(attr => attr.IsValidForRequest(controllerContext, methodInfo)))
+ {
+ matchesWithSelectionAttributes.Add(methodInfo);
+ }
+ }
+
+ // if a matching action method had a selection attribute, consider it more specific than a matching action method
+ // without a selection attribute
+ return (matchesWithSelectionAttributes.Count > 0) ? matchesWithSelectionAttributes : matchesWithoutSelectionAttributes;
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ActionMethodSelectorAttribute.cs b/src/System.Web.Mvc/ActionMethodSelectorAttribute.cs
new file mode 100644
index 00000000..9dc3c8c2
--- /dev/null
+++ b/src/System.Web.Mvc/ActionMethodSelectorAttribute.cs
@@ -0,0 +1,10 @@
+using System.Reflection;
+
+namespace System.Web.Mvc
+{
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+ public abstract class ActionMethodSelectorAttribute : Attribute
+ {
+ public abstract bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo);
+ }
+}
diff --git a/src/System.Web.Mvc/ActionNameAttribute.cs b/src/System.Web.Mvc/ActionNameAttribute.cs
new file mode 100644
index 00000000..087ef06e
--- /dev/null
+++ b/src/System.Web.Mvc/ActionNameAttribute.cs
@@ -0,0 +1,26 @@
+using System.Reflection;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc
+{
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+ public sealed class ActionNameAttribute : ActionNameSelectorAttribute
+ {
+ public ActionNameAttribute(string name)
+ {
+ if (String.IsNullOrEmpty(name))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "name");
+ }
+
+ Name = name;
+ }
+
+ public string Name { get; private set; }
+
+ public override bool IsValidName(ControllerContext controllerContext, string actionName, MethodInfo methodInfo)
+ {
+ return String.Equals(actionName, Name, StringComparison.OrdinalIgnoreCase);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ActionNameSelectorAttribute.cs b/src/System.Web.Mvc/ActionNameSelectorAttribute.cs
new file mode 100644
index 00000000..8b2952fc
--- /dev/null
+++ b/src/System.Web.Mvc/ActionNameSelectorAttribute.cs
@@ -0,0 +1,10 @@
+using System.Reflection;
+
+namespace System.Web.Mvc
+{
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+ public abstract class ActionNameSelectorAttribute : Attribute
+ {
+ public abstract bool IsValidName(ControllerContext controllerContext, string actionName, MethodInfo methodInfo);
+ }
+}
diff --git a/src/System.Web.Mvc/ActionResult.cs b/src/System.Web.Mvc/ActionResult.cs
new file mode 100644
index 00000000..2dc190c0
--- /dev/null
+++ b/src/System.Web.Mvc/ActionResult.cs
@@ -0,0 +1,7 @@
+namespace System.Web.Mvc
+{
+ public abstract class ActionResult
+ {
+ public abstract void ExecuteResult(ControllerContext context);
+ }
+}
diff --git a/src/System.Web.Mvc/ActionSelector.cs b/src/System.Web.Mvc/ActionSelector.cs
new file mode 100644
index 00000000..ee790b43
--- /dev/null
+++ b/src/System.Web.Mvc/ActionSelector.cs
@@ -0,0 +1,4 @@
+namespace System.Web.Mvc
+{
+ public delegate bool ActionSelector(ControllerContext controllerContext);
+}
diff --git a/src/System.Web.Mvc/AdditionalMetaDataAttribute.cs b/src/System.Web.Mvc/AdditionalMetaDataAttribute.cs
new file mode 100644
index 00000000..0d131f83
--- /dev/null
+++ b/src/System.Web.Mvc/AdditionalMetaDataAttribute.cs
@@ -0,0 +1,38 @@
+namespace System.Web.Mvc
+{
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Property, AllowMultiple = true)]
+ public sealed class AdditionalMetadataAttribute : Attribute, IMetadataAware
+ {
+ private object _typeId = new object();
+
+ public AdditionalMetadataAttribute(string name, object value)
+ {
+ if (name == null)
+ {
+ throw new ArgumentNullException("name");
+ }
+
+ Name = name;
+ Value = value;
+ }
+
+ public override object TypeId
+ {
+ get { return _typeId; }
+ }
+
+ public string Name { get; private set; }
+
+ public object Value { get; private set; }
+
+ public void OnMetadataCreated(ModelMetadata metadata)
+ {
+ if (metadata == null)
+ {
+ throw new ArgumentNullException("metadata");
+ }
+
+ metadata.AdditionalValues[Name] = Value;
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Ajax/AjaxExtensions.cs b/src/System.Web.Mvc/Ajax/AjaxExtensions.cs
new file mode 100644
index 00000000..d2889fa9
--- /dev/null
+++ b/src/System.Web.Mvc/Ajax/AjaxExtensions.cs
@@ -0,0 +1,358 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Web.Mvc.Html;
+using System.Web.Mvc.Properties;
+using System.Web.Routing;
+
+namespace System.Web.Mvc.Ajax
+{
+ public static class AjaxExtensions
+ {
+ private const string LinkOnClickFormat = "Sys.Mvc.AsyncHyperlink.handleClick(this, new Sys.UI.DomEvent(event), {0});";
+ private const string FormOnClickValue = "Sys.Mvc.AsyncForm.handleClick(this, new Sys.UI.DomEvent(event));";
+ private const string FormOnSubmitFormat = "Sys.Mvc.AsyncForm.handleSubmit(this, new Sys.UI.DomEvent(event), {0});";
+
+ public static MvcHtmlString ActionLink(this AjaxHelper ajaxHelper, string linkText, string actionName, AjaxOptions ajaxOptions)
+ {
+ return ActionLink(ajaxHelper, linkText, actionName, (string)null /* controllerName */, ajaxOptions);
+ }
+
+ public static MvcHtmlString ActionLink(this AjaxHelper ajaxHelper, string linkText, string actionName, object routeValues, AjaxOptions ajaxOptions)
+ {
+ return ActionLink(ajaxHelper, linkText, actionName, (string)null /* controllerName */, routeValues, ajaxOptions);
+ }
+
+ public static MvcHtmlString ActionLink(this AjaxHelper ajaxHelper, string linkText, string actionName, object routeValues, AjaxOptions ajaxOptions, object htmlAttributes)
+ {
+ return ActionLink(ajaxHelper, linkText, actionName, (string)null /* controllerName */, routeValues, ajaxOptions, htmlAttributes);
+ }
+
+ public static MvcHtmlString ActionLink(this AjaxHelper ajaxHelper, string linkText, string actionName, RouteValueDictionary routeValues, AjaxOptions ajaxOptions)
+ {
+ return ActionLink(ajaxHelper, linkText, actionName, (string)null /* controllerName */, routeValues, ajaxOptions);
+ }
+
+ public static MvcHtmlString ActionLink(this AjaxHelper ajaxHelper, string linkText, string actionName, RouteValueDictionary routeValues, AjaxOptions ajaxOptions, IDictionary<string, object> htmlAttributes)
+ {
+ return ActionLink(ajaxHelper, linkText, actionName, (string)null /* controllerName */, routeValues, ajaxOptions, htmlAttributes);
+ }
+
+ public static MvcHtmlString ActionLink(this AjaxHelper ajaxHelper, string linkText, string actionName, string controllerName, AjaxOptions ajaxOptions)
+ {
+ return ActionLink(ajaxHelper, linkText, actionName, controllerName, null /* values */, ajaxOptions, null /* htmlAttributes */);
+ }
+
+ public static MvcHtmlString ActionLink(this AjaxHelper ajaxHelper, string linkText, string actionName, string controllerName, object routeValues, AjaxOptions ajaxOptions)
+ {
+ return ActionLink(ajaxHelper, linkText, actionName, controllerName, routeValues, ajaxOptions, null /* htmlAttributes */);
+ }
+
+ public static MvcHtmlString ActionLink(this AjaxHelper ajaxHelper, string linkText, string actionName, string controllerName, object routeValues, AjaxOptions ajaxOptions, object htmlAttributes)
+ {
+ RouteValueDictionary newValues = new RouteValueDictionary(routeValues);
+ RouteValueDictionary newAttributes = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);
+ return ActionLink(ajaxHelper, linkText, actionName, controllerName, newValues, ajaxOptions, newAttributes);
+ }
+
+ public static MvcHtmlString ActionLink(this AjaxHelper ajaxHelper, string linkText, string actionName, string controllerName, RouteValueDictionary routeValues, AjaxOptions ajaxOptions)
+ {
+ return ActionLink(ajaxHelper, linkText, actionName, controllerName, routeValues, ajaxOptions, null /* htmlAttributes */);
+ }
+
+ public static MvcHtmlString ActionLink(this AjaxHelper ajaxHelper, string linkText, string actionName, string controllerName, RouteValueDictionary routeValues, AjaxOptions ajaxOptions, IDictionary<string, object> htmlAttributes)
+ {
+ if (String.IsNullOrEmpty(linkText))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "linkText");
+ }
+
+ string targetUrl = UrlHelper.GenerateUrl(null, actionName, controllerName, routeValues, ajaxHelper.RouteCollection, ajaxHelper.ViewContext.RequestContext, true /* includeImplicitMvcValues */);
+
+ return MvcHtmlString.Create(GenerateLink(ajaxHelper, linkText, targetUrl, GetAjaxOptions(ajaxOptions), htmlAttributes));
+ }
+
+ public static MvcHtmlString ActionLink(this AjaxHelper ajaxHelper, string linkText, string actionName, string controllerName, string protocol, string hostName, string fragment, object routeValues, AjaxOptions ajaxOptions, object htmlAttributes)
+ {
+ RouteValueDictionary newValues = new RouteValueDictionary(routeValues);
+ RouteValueDictionary newAttributes = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);
+ return ActionLink(ajaxHelper, linkText, actionName, controllerName, protocol, hostName, fragment, newValues, ajaxOptions, newAttributes);
+ }
+
+ public static MvcHtmlString ActionLink(this AjaxHelper ajaxHelper, string linkText, string actionName, string controllerName, string protocol, string hostName, string fragment, RouteValueDictionary routeValues, AjaxOptions ajaxOptions, IDictionary<string, object> htmlAttributes)
+ {
+ if (String.IsNullOrEmpty(linkText))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "linkText");
+ }
+
+ string targetUrl = UrlHelper.GenerateUrl(null /* routeName */, actionName, controllerName, protocol, hostName, fragment, routeValues, ajaxHelper.RouteCollection, ajaxHelper.ViewContext.RequestContext, true /* includeImplicitMvcValues */);
+
+ return MvcHtmlString.Create(GenerateLink(ajaxHelper, linkText, targetUrl, ajaxOptions, htmlAttributes));
+ }
+
+ public static MvcForm BeginForm(this AjaxHelper ajaxHelper, AjaxOptions ajaxOptions)
+ {
+ string formAction = ajaxHelper.ViewContext.HttpContext.Request.RawUrl;
+ return FormHelper(ajaxHelper, formAction, ajaxOptions, new RouteValueDictionary());
+ }
+
+ public static MvcForm BeginForm(this AjaxHelper ajaxHelper, string actionName, AjaxOptions ajaxOptions)
+ {
+ return BeginForm(ajaxHelper, actionName, (string)null /* controllerName */, ajaxOptions);
+ }
+
+ public static MvcForm BeginForm(this AjaxHelper ajaxHelper, string actionName, object routeValues, AjaxOptions ajaxOptions)
+ {
+ return BeginForm(ajaxHelper, actionName, (string)null /* controllerName */, routeValues, ajaxOptions);
+ }
+
+ public static MvcForm BeginForm(this AjaxHelper ajaxHelper, string actionName, object routeValues, AjaxOptions ajaxOptions, object htmlAttributes)
+ {
+ return BeginForm(ajaxHelper, actionName, (string)null /* controllerName */, routeValues, ajaxOptions, htmlAttributes);
+ }
+
+ public static MvcForm BeginForm(this AjaxHelper ajaxHelper, string actionName, RouteValueDictionary routeValues, AjaxOptions ajaxOptions)
+ {
+ return BeginForm(ajaxHelper, actionName, (string)null /* controllerName */, routeValues, ajaxOptions);
+ }
+
+ public static MvcForm BeginForm(this AjaxHelper ajaxHelper, string actionName, RouteValueDictionary routeValues, AjaxOptions ajaxOptions, IDictionary<string, object> htmlAttributes)
+ {
+ return BeginForm(ajaxHelper, actionName, (string)null /* controllerName */, routeValues, ajaxOptions, htmlAttributes);
+ }
+
+ public static MvcForm BeginForm(this AjaxHelper ajaxHelper, string actionName, string controllerName, AjaxOptions ajaxOptions)
+ {
+ return BeginForm(ajaxHelper, actionName, controllerName, null /* values */, ajaxOptions, null /* htmlAttributes */);
+ }
+
+ public static MvcForm BeginForm(this AjaxHelper ajaxHelper, string actionName, string controllerName, object routeValues, AjaxOptions ajaxOptions)
+ {
+ return BeginForm(ajaxHelper, actionName, controllerName, routeValues, ajaxOptions, null /* htmlAttributes */);
+ }
+
+ public static MvcForm BeginForm(this AjaxHelper ajaxHelper, string actionName, string controllerName, object routeValues, AjaxOptions ajaxOptions, object htmlAttributes)
+ {
+ RouteValueDictionary newValues = new RouteValueDictionary(routeValues);
+ RouteValueDictionary newAttributes = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);
+ return BeginForm(ajaxHelper, actionName, controllerName, newValues, ajaxOptions, newAttributes);
+ }
+
+ public static MvcForm BeginForm(this AjaxHelper ajaxHelper, string actionName, string controllerName, RouteValueDictionary routeValues, AjaxOptions ajaxOptions)
+ {
+ return BeginForm(ajaxHelper, actionName, controllerName, routeValues, ajaxOptions, null /* htmlAttributes */);
+ }
+
+ public static MvcForm BeginForm(this AjaxHelper ajaxHelper, string actionName, string controllerName, RouteValueDictionary routeValues, AjaxOptions ajaxOptions, IDictionary<string, object> htmlAttributes)
+ {
+ // get target URL
+ string formAction = UrlHelper.GenerateUrl(null, actionName, controllerName, routeValues ?? new RouteValueDictionary(), ajaxHelper.RouteCollection, ajaxHelper.ViewContext.RequestContext, true /* includeImplicitMvcValues */);
+ return FormHelper(ajaxHelper, formAction, ajaxOptions, htmlAttributes);
+ }
+
+ public static MvcForm BeginRouteForm(this AjaxHelper ajaxHelper, string routeName, AjaxOptions ajaxOptions)
+ {
+ return BeginRouteForm(ajaxHelper, routeName, null /* routeValues */, ajaxOptions, null /* htmlAttributes */);
+ }
+
+ public static MvcForm BeginRouteForm(this AjaxHelper ajaxHelper, string routeName, object routeValues, AjaxOptions ajaxOptions)
+ {
+ return BeginRouteForm(ajaxHelper, routeName, (object)routeValues, ajaxOptions, null /* htmlAttributes */);
+ }
+
+ public static MvcForm BeginRouteForm(this AjaxHelper ajaxHelper, string routeName, object routeValues, AjaxOptions ajaxOptions, object htmlAttributes)
+ {
+ RouteValueDictionary newAttributes = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);
+ return BeginRouteForm(ajaxHelper, routeName, new RouteValueDictionary(routeValues), ajaxOptions, newAttributes);
+ }
+
+ public static MvcForm BeginRouteForm(this AjaxHelper ajaxHelper, string routeName, RouteValueDictionary routeValues, AjaxOptions ajaxOptions)
+ {
+ return BeginRouteForm(ajaxHelper, routeName, routeValues, ajaxOptions, null /* htmlAttributes */);
+ }
+
+ public static MvcForm BeginRouteForm(this AjaxHelper ajaxHelper, string routeName, RouteValueDictionary routeValues, AjaxOptions ajaxOptions, IDictionary<string, object> htmlAttributes)
+ {
+ string formAction = UrlHelper.GenerateUrl(routeName, null /* actionName */, null /* controllerName */, routeValues ?? new RouteValueDictionary(), ajaxHelper.RouteCollection, ajaxHelper.ViewContext.RequestContext, false /* includeImplicitMvcValues */);
+ return FormHelper(ajaxHelper, formAction, ajaxOptions, htmlAttributes);
+ }
+
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "You don't want to dispose of this object unless you intend to write to the response")]
+ private static MvcForm FormHelper(this AjaxHelper ajaxHelper, string formAction, AjaxOptions ajaxOptions, IDictionary<string, object> htmlAttributes)
+ {
+ TagBuilder builder = new TagBuilder("form");
+ builder.MergeAttributes(htmlAttributes);
+ builder.MergeAttribute("action", formAction);
+ builder.MergeAttribute("method", "post");
+
+ ajaxOptions = GetAjaxOptions(ajaxOptions);
+
+ if (ajaxHelper.ViewContext.UnobtrusiveJavaScriptEnabled)
+ {
+ builder.MergeAttributes(ajaxOptions.ToUnobtrusiveHtmlAttributes());
+ }
+ else
+ {
+ builder.MergeAttribute("onclick", FormOnClickValue);
+ builder.MergeAttribute("onsubmit", GenerateAjaxScript(ajaxOptions, FormOnSubmitFormat));
+ }
+
+ if (ajaxHelper.ViewContext.ClientValidationEnabled)
+ {
+ // forms must have an ID for client validation
+ builder.GenerateId(ajaxHelper.ViewContext.FormIdGenerator());
+ }
+
+ ajaxHelper.ViewContext.Writer.Write(builder.ToString(TagRenderMode.StartTag));
+ MvcForm theForm = new MvcForm(ajaxHelper.ViewContext);
+
+ if (ajaxHelper.ViewContext.ClientValidationEnabled)
+ {
+ ajaxHelper.ViewContext.FormContext.FormId = builder.Attributes["id"];
+ }
+
+ return theForm;
+ }
+
+ public static MvcHtmlString GlobalizationScript(this AjaxHelper ajaxHelper)
+ {
+ return GlobalizationScript(ajaxHelper, CultureInfo.CurrentCulture);
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "ajaxHelper", Justification = "This is an extension method")]
+ public static MvcHtmlString GlobalizationScript(this AjaxHelper ajaxHelper, CultureInfo cultureInfo)
+ {
+ return GlobalizationScriptHelper(AjaxHelper.GlobalizationScriptPath, cultureInfo);
+ }
+
+ internal static MvcHtmlString GlobalizationScriptHelper(string scriptPath, CultureInfo cultureInfo)
+ {
+ if (cultureInfo == null)
+ {
+ throw new ArgumentNullException("cultureInfo");
+ }
+
+ TagBuilder tagBuilder = new TagBuilder("script");
+ tagBuilder.MergeAttribute("type", "text/javascript");
+
+ string src = VirtualPathUtility.AppendTrailingSlash(scriptPath) + HttpUtility.UrlEncode(cultureInfo.Name) + ".js";
+ tagBuilder.MergeAttribute("src", src);
+
+ return tagBuilder.ToMvcHtmlString(TagRenderMode.Normal);
+ }
+
+ public static MvcHtmlString RouteLink(this AjaxHelper ajaxHelper, string linkText, object routeValues, AjaxOptions ajaxOptions)
+ {
+ return RouteLink(ajaxHelper, linkText, null /* routeName */, new RouteValueDictionary(routeValues), ajaxOptions,
+ new Dictionary<string, object>());
+ }
+
+ public static MvcHtmlString RouteLink(this AjaxHelper ajaxHelper, string linkText, object routeValues, AjaxOptions ajaxOptions, object htmlAttributes)
+ {
+ return RouteLink(ajaxHelper, linkText, null /* routeName */, new RouteValueDictionary(routeValues), ajaxOptions,
+ HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ public static MvcHtmlString RouteLink(this AjaxHelper ajaxHelper, string linkText, RouteValueDictionary routeValues, AjaxOptions ajaxOptions)
+ {
+ return RouteLink(ajaxHelper, linkText, null /* routeName */, routeValues, ajaxOptions,
+ new Dictionary<string, object>());
+ }
+
+ public static MvcHtmlString RouteLink(this AjaxHelper ajaxHelper, string linkText, RouteValueDictionary routeValues, AjaxOptions ajaxOptions, IDictionary<string, object> htmlAttributes)
+ {
+ return RouteLink(ajaxHelper, linkText, null /* routeName */, routeValues, ajaxOptions, htmlAttributes);
+ }
+
+ public static MvcHtmlString RouteLink(this AjaxHelper ajaxHelper, string linkText, string routeName, AjaxOptions ajaxOptions)
+ {
+ return RouteLink(ajaxHelper, linkText, routeName, new RouteValueDictionary(), ajaxOptions,
+ new Dictionary<string, object>());
+ }
+
+ public static MvcHtmlString RouteLink(this AjaxHelper ajaxHelper, string linkText, string routeName, AjaxOptions ajaxOptions, object htmlAttributes)
+ {
+ return RouteLink(ajaxHelper, linkText, routeName, new RouteValueDictionary(), ajaxOptions, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ public static MvcHtmlString RouteLink(this AjaxHelper ajaxHelper, string linkText, string routeName, AjaxOptions ajaxOptions, IDictionary<string, object> htmlAttributes)
+ {
+ return RouteLink(ajaxHelper, linkText, routeName, new RouteValueDictionary(), ajaxOptions, htmlAttributes);
+ }
+
+ public static MvcHtmlString RouteLink(this AjaxHelper ajaxHelper, string linkText, string routeName, object routeValues, AjaxOptions ajaxOptions)
+ {
+ return RouteLink(ajaxHelper, linkText, routeName, new RouteValueDictionary(routeValues), ajaxOptions,
+ new Dictionary<string, object>());
+ }
+
+ public static MvcHtmlString RouteLink(this AjaxHelper ajaxHelper, string linkText, string routeName, object routeValues, AjaxOptions ajaxOptions, object htmlAttributes)
+ {
+ return RouteLink(ajaxHelper, linkText, routeName, new RouteValueDictionary(routeValues), ajaxOptions,
+ HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ public static MvcHtmlString RouteLink(this AjaxHelper ajaxHelper, string linkText, string routeName, RouteValueDictionary routeValues, AjaxOptions ajaxOptions)
+ {
+ return RouteLink(ajaxHelper, linkText, routeName, routeValues, ajaxOptions, new Dictionary<string, object>());
+ }
+
+ public static MvcHtmlString RouteLink(this AjaxHelper ajaxHelper, string linkText, string routeName, RouteValueDictionary routeValues, AjaxOptions ajaxOptions, IDictionary<string, object> htmlAttributes)
+ {
+ if (String.IsNullOrEmpty(linkText))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "linkText");
+ }
+
+ string targetUrl = UrlHelper.GenerateUrl(routeName, null /* actionName */, null /* controllerName */, routeValues ?? new RouteValueDictionary(), ajaxHelper.RouteCollection, ajaxHelper.ViewContext.RequestContext, false /* includeImplicitMvcValues */);
+
+ return MvcHtmlString.Create(GenerateLink(ajaxHelper, linkText, targetUrl, GetAjaxOptions(ajaxOptions), htmlAttributes));
+ }
+
+ public static MvcHtmlString RouteLink(this AjaxHelper ajaxHelper, string linkText, string routeName, string protocol, string hostName, string fragment, RouteValueDictionary routeValues, AjaxOptions ajaxOptions, IDictionary<string, object> htmlAttributes)
+ {
+ if (String.IsNullOrEmpty(linkText))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "linkText");
+ }
+
+ string targetUrl = UrlHelper.GenerateUrl(routeName, null /* actionName */, null /* controllerName */, protocol, hostName, fragment, routeValues ?? new RouteValueDictionary(), ajaxHelper.RouteCollection, ajaxHelper.ViewContext.RequestContext, false /* includeImplicitMvcValues */);
+
+ return MvcHtmlString.Create(GenerateLink(ajaxHelper, linkText, targetUrl, GetAjaxOptions(ajaxOptions), htmlAttributes));
+ }
+
+ private static string GenerateLink(AjaxHelper ajaxHelper, string linkText, string targetUrl, AjaxOptions ajaxOptions, IDictionary<string, object> htmlAttributes)
+ {
+ TagBuilder tag = new TagBuilder("a")
+ {
+ InnerHtml = HttpUtility.HtmlEncode(linkText)
+ };
+
+ tag.MergeAttributes(htmlAttributes);
+ tag.MergeAttribute("href", targetUrl);
+
+ if (ajaxHelper.ViewContext.UnobtrusiveJavaScriptEnabled)
+ {
+ tag.MergeAttributes(ajaxOptions.ToUnobtrusiveHtmlAttributes());
+ }
+ else
+ {
+ tag.MergeAttribute("onclick", GenerateAjaxScript(ajaxOptions, LinkOnClickFormat));
+ }
+
+ return tag.ToString(TagRenderMode.Normal);
+ }
+
+ private static string GenerateAjaxScript(AjaxOptions ajaxOptions, string scriptFormat)
+ {
+ string optionsString = ajaxOptions.ToJavascriptString();
+ return String.Format(CultureInfo.InvariantCulture, scriptFormat, optionsString);
+ }
+
+ private static AjaxOptions GetAjaxOptions(AjaxOptions ajaxOptions)
+ {
+ return (ajaxOptions != null) ? ajaxOptions : new AjaxOptions();
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Ajax/AjaxOptions.cs b/src/System.Web.Mvc/Ajax/AjaxOptions.cs
new file mode 100644
index 00000000..73b8f712
--- /dev/null
+++ b/src/System.Web.Mvc/Ajax/AjaxOptions.cs
@@ -0,0 +1,216 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Text;
+
+namespace System.Web.Mvc.Ajax
+{
+ public class AjaxOptions
+ {
+ private string _confirm;
+ private string _httpMethod;
+ private InsertionMode _insertionMode = InsertionMode.Replace;
+ private string _loadingElementId;
+ private string _onBegin;
+ private string _onComplete;
+ private string _onFailure;
+ private string _onSuccess;
+ private string _updateTargetId;
+ private string _url;
+
+ public string Confirm
+ {
+ get { return _confirm ?? String.Empty; }
+ set { _confirm = value; }
+ }
+
+ public string HttpMethod
+ {
+ get { return _httpMethod ?? String.Empty; }
+ set { _httpMethod = value; }
+ }
+
+ public InsertionMode InsertionMode
+ {
+ get { return _insertionMode; }
+ set
+ {
+ switch (value)
+ {
+ case InsertionMode.Replace:
+ case InsertionMode.InsertAfter:
+ case InsertionMode.InsertBefore:
+ _insertionMode = value;
+ return;
+
+ default:
+ throw new ArgumentOutOfRangeException("value");
+ }
+ }
+ }
+
+ internal string InsertionModeString
+ {
+ get
+ {
+ switch (InsertionMode)
+ {
+ case InsertionMode.Replace:
+ return "Sys.Mvc.InsertionMode.replace";
+ case InsertionMode.InsertBefore:
+ return "Sys.Mvc.InsertionMode.insertBefore";
+ case InsertionMode.InsertAfter:
+ return "Sys.Mvc.InsertionMode.insertAfter";
+ default:
+ return ((int)InsertionMode).ToString(CultureInfo.InvariantCulture);
+ }
+ }
+ }
+
+ internal string InsertionModeUnobtrusive
+ {
+ get
+ {
+ switch (InsertionMode)
+ {
+ case InsertionMode.Replace:
+ return "replace";
+ case InsertionMode.InsertBefore:
+ return "before";
+ case InsertionMode.InsertAfter:
+ return "after";
+ default:
+ return ((int)InsertionMode).ToString(CultureInfo.InvariantCulture);
+ }
+ }
+ }
+
+ public int LoadingElementDuration { get; set; }
+
+ public string LoadingElementId
+ {
+ get { return _loadingElementId ?? String.Empty; }
+ set { _loadingElementId = value; }
+ }
+
+ public string OnBegin
+ {
+ get { return _onBegin ?? String.Empty; }
+ set { _onBegin = value; }
+ }
+
+ public string OnComplete
+ {
+ get { return _onComplete ?? String.Empty; }
+ set { _onComplete = value; }
+ }
+
+ public string OnFailure
+ {
+ get { return _onFailure ?? String.Empty; }
+ set { _onFailure = value; }
+ }
+
+ public string OnSuccess
+ {
+ get { return _onSuccess ?? String.Empty; }
+ set { _onSuccess = value; }
+ }
+
+ public string UpdateTargetId
+ {
+ get { return _updateTargetId ?? String.Empty; }
+ set { _updateTargetId = value; }
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "This property is used by the optionsBuilder which always accepts a string.")]
+ public string Url
+ {
+ get { return _url ?? String.Empty; }
+ set { _url = value; }
+ }
+
+ internal string ToJavascriptString()
+ {
+ // creates a string of the form { key1: value1, key2 : value2, ... }
+ StringBuilder optionsBuilder = new StringBuilder("{");
+ optionsBuilder.Append(String.Format(CultureInfo.InvariantCulture, " insertionMode: {0},", InsertionModeString));
+ optionsBuilder.Append(PropertyStringIfSpecified("confirm", Confirm));
+ optionsBuilder.Append(PropertyStringIfSpecified("httpMethod", HttpMethod));
+ optionsBuilder.Append(PropertyStringIfSpecified("loadingElementId", LoadingElementId));
+ optionsBuilder.Append(PropertyStringIfSpecified("updateTargetId", UpdateTargetId));
+ optionsBuilder.Append(PropertyStringIfSpecified("url", Url));
+ optionsBuilder.Append(EventStringIfSpecified("onBegin", OnBegin));
+ optionsBuilder.Append(EventStringIfSpecified("onComplete", OnComplete));
+ optionsBuilder.Append(EventStringIfSpecified("onFailure", OnFailure));
+ optionsBuilder.Append(EventStringIfSpecified("onSuccess", OnSuccess));
+ optionsBuilder.Length--;
+ optionsBuilder.Append(" }");
+ return optionsBuilder.ToString();
+ }
+
+ public IDictionary<string, object> ToUnobtrusiveHtmlAttributes()
+ {
+ var result = new Dictionary<string, object>
+ {
+ { "data-ajax", "true" },
+ };
+
+ AddToDictionaryIfSpecified(result, "data-ajax-url", Url);
+ AddToDictionaryIfSpecified(result, "data-ajax-method", HttpMethod);
+ AddToDictionaryIfSpecified(result, "data-ajax-confirm", Confirm);
+
+ AddToDictionaryIfSpecified(result, "data-ajax-begin", OnBegin);
+ AddToDictionaryIfSpecified(result, "data-ajax-complete", OnComplete);
+ AddToDictionaryIfSpecified(result, "data-ajax-failure", OnFailure);
+ AddToDictionaryIfSpecified(result, "data-ajax-success", OnSuccess);
+
+ if (!String.IsNullOrWhiteSpace(LoadingElementId))
+ {
+ result.Add("data-ajax-loading", "#" + LoadingElementId);
+
+ if (LoadingElementDuration > 0)
+ {
+ result.Add("data-ajax-loading-duration", LoadingElementDuration);
+ }
+ }
+
+ if (!String.IsNullOrWhiteSpace(UpdateTargetId))
+ {
+ result.Add("data-ajax-update", "#" + UpdateTargetId);
+ result.Add("data-ajax-mode", InsertionModeUnobtrusive);
+ }
+
+ return result;
+ }
+
+ // Helpers
+
+ private static void AddToDictionaryIfSpecified(IDictionary<string, object> dictionary, string name, string value)
+ {
+ if (!String.IsNullOrWhiteSpace(value))
+ {
+ dictionary.Add(name, value);
+ }
+ }
+
+ private static string EventStringIfSpecified(string propertyName, string handler)
+ {
+ if (!String.IsNullOrEmpty(handler))
+ {
+ return String.Format(CultureInfo.InvariantCulture, " {0}: Function.createDelegate(this, {1}),", propertyName, handler.ToString());
+ }
+ return String.Empty;
+ }
+
+ private static string PropertyStringIfSpecified(string propertyName, string propertyValue)
+ {
+ if (!String.IsNullOrEmpty(propertyValue))
+ {
+ string escapedPropertyValue = propertyValue.Replace("'", @"\'");
+ return String.Format(CultureInfo.InvariantCulture, " {0}: '{1}',", propertyName, escapedPropertyValue);
+ }
+ return String.Empty;
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Ajax/InsertionMode.cs b/src/System.Web.Mvc/Ajax/InsertionMode.cs
new file mode 100644
index 00000000..3e0affc9
--- /dev/null
+++ b/src/System.Web.Mvc/Ajax/InsertionMode.cs
@@ -0,0 +1,9 @@
+namespace System.Web.Mvc.Ajax
+{
+ public enum InsertionMode
+ {
+ Replace = 0,
+ InsertBefore = 1,
+ InsertAfter = 2
+ }
+}
diff --git a/src/System.Web.Mvc/AjaxHelper.cs b/src/System.Web.Mvc/AjaxHelper.cs
new file mode 100644
index 00000000..ab2d82cb
--- /dev/null
+++ b/src/System.Web.Mvc/AjaxHelper.cs
@@ -0,0 +1,83 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Web.Routing;
+
+namespace System.Web.Mvc
+{
+ public class AjaxHelper
+ {
+ private static string _globalizationScriptPath;
+
+ private DynamicViewDataDictionary _dynamicViewDataDictionary;
+
+ public AjaxHelper(ViewContext viewContext, IViewDataContainer viewDataContainer)
+ : this(viewContext, viewDataContainer, RouteTable.Routes)
+ {
+ }
+
+ public AjaxHelper(ViewContext viewContext, IViewDataContainer viewDataContainer, RouteCollection routeCollection)
+ {
+ if (viewContext == null)
+ {
+ throw new ArgumentNullException("viewContext");
+ }
+ if (viewDataContainer == null)
+ {
+ throw new ArgumentNullException("viewDataContainer");
+ }
+ if (routeCollection == null)
+ {
+ throw new ArgumentNullException("routeCollection");
+ }
+ ViewContext = viewContext;
+ ViewDataContainer = viewDataContainer;
+ RouteCollection = routeCollection;
+ }
+
+ public static string GlobalizationScriptPath
+ {
+ get
+ {
+ if (String.IsNullOrEmpty(_globalizationScriptPath))
+ {
+ _globalizationScriptPath = "~/Scripts/Globalization";
+ }
+ return _globalizationScriptPath;
+ }
+ set { _globalizationScriptPath = value; }
+ }
+
+ public RouteCollection RouteCollection { get; private set; }
+
+ public dynamic ViewBag
+ {
+ get
+ {
+ if (_dynamicViewDataDictionary == null)
+ {
+ _dynamicViewDataDictionary = new DynamicViewDataDictionary(() => ViewData);
+ }
+ return _dynamicViewDataDictionary;
+ }
+ }
+
+ public ViewContext ViewContext { get; private set; }
+
+ public ViewDataDictionary ViewData
+ {
+ get { return ViewDataContainer.ViewData; }
+ }
+
+ public IViewDataContainer ViewDataContainer { get; internal set; }
+
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Instance method for consistency with other helpers.")]
+ public string JavaScriptStringEncode(string message)
+ {
+ if (String.IsNullOrEmpty(message))
+ {
+ return message;
+ }
+
+ return HttpUtility.JavaScriptStringEncode(message);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/AjaxHelper`1.cs b/src/System.Web.Mvc/AjaxHelper`1.cs
new file mode 100644
index 00000000..f579b9f1
--- /dev/null
+++ b/src/System.Web.Mvc/AjaxHelper`1.cs
@@ -0,0 +1,39 @@
+using System.Web.Routing;
+
+namespace System.Web.Mvc
+{
+ public class AjaxHelper<TModel> : AjaxHelper
+ {
+ private DynamicViewDataDictionary _dynamicViewDataDictionary;
+ private ViewDataDictionary<TModel> _viewData;
+
+ public AjaxHelper(ViewContext viewContext, IViewDataContainer viewDataContainer)
+ : this(viewContext, viewDataContainer, RouteTable.Routes)
+ {
+ }
+
+ public AjaxHelper(ViewContext viewContext, IViewDataContainer viewDataContainer, RouteCollection routeCollection)
+ : base(viewContext, viewDataContainer, routeCollection)
+ {
+ _viewData = new ViewDataDictionary<TModel>(viewDataContainer.ViewData);
+ }
+
+ public new dynamic ViewBag
+ {
+ get
+ {
+ if (_dynamicViewDataDictionary == null)
+ {
+ _dynamicViewDataDictionary = new DynamicViewDataDictionary(() => ViewData);
+ }
+
+ return _dynamicViewDataDictionary;
+ }
+ }
+
+ public new ViewDataDictionary<TModel> ViewData
+ {
+ get { return _viewData; }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/AjaxRequestExtensions.cs b/src/System.Web.Mvc/AjaxRequestExtensions.cs
new file mode 100644
index 00000000..e890fdcc
--- /dev/null
+++ b/src/System.Web.Mvc/AjaxRequestExtensions.cs
@@ -0,0 +1,15 @@
+namespace System.Web.Mvc
+{
+ public static class AjaxRequestExtensions
+ {
+ public static bool IsAjaxRequest(this HttpRequestBase request)
+ {
+ if (request == null)
+ {
+ throw new ArgumentNullException("request");
+ }
+
+ return (request["X-Requested-With"] == "XMLHttpRequest") || ((request.Headers != null) && (request.Headers["X-Requested-With"] == "XMLHttpRequest"));
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/AllowAnonymousAttribute.cs b/src/System.Web.Mvc/AllowAnonymousAttribute.cs
new file mode 100644
index 00000000..780e7c51
--- /dev/null
+++ b/src/System.Web.Mvc/AllowAnonymousAttribute.cs
@@ -0,0 +1,11 @@
+namespace System.Web.Mvc
+{
+ /// <summary>
+ /// Actions and controllers with the AllowAnonymous attribute are skipped by the Authorize attribute
+ /// on authorization. See AccountController.cs in the project template for an example.
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+ public sealed class AllowAnonymousAttribute : Attribute
+ {
+ }
+}
diff --git a/src/System.Web.Mvc/AllowHtmlAttribute.cs b/src/System.Web.Mvc/AllowHtmlAttribute.cs
new file mode 100644
index 00000000..66abb499
--- /dev/null
+++ b/src/System.Web.Mvc/AllowHtmlAttribute.cs
@@ -0,0 +1,19 @@
+namespace System.Web.Mvc
+{
+ // This attribute can be applied to a model property to specify that the particular property to
+ // which it is applied should not go through request validation.
+
+ [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
+ public sealed class AllowHtmlAttribute : Attribute, IMetadataAware
+ {
+ public void OnMetadataCreated(ModelMetadata metadata)
+ {
+ if (metadata == null)
+ {
+ throw new ArgumentNullException("metadata");
+ }
+
+ metadata.RequestValidationEnabled = false;
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/AreaHelpers.cs b/src/System.Web.Mvc/AreaHelpers.cs
new file mode 100644
index 00000000..e8deaed0
--- /dev/null
+++ b/src/System.Web.Mvc/AreaHelpers.cs
@@ -0,0 +1,35 @@
+using System.Web.Routing;
+
+namespace System.Web.Mvc
+{
+ internal static class AreaHelpers
+ {
+ public static string GetAreaName(RouteBase route)
+ {
+ IRouteWithArea routeWithArea = route as IRouteWithArea;
+ if (routeWithArea != null)
+ {
+ return routeWithArea.Area;
+ }
+
+ Route castRoute = route as Route;
+ if (castRoute != null && castRoute.DataTokens != null)
+ {
+ return castRoute.DataTokens["area"] as string;
+ }
+
+ return null;
+ }
+
+ public static string GetAreaName(RouteData routeData)
+ {
+ object area;
+ if (routeData.DataTokens.TryGetValue("area", out area))
+ {
+ return area as string;
+ }
+
+ return GetAreaName(routeData.Route);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/AreaRegistration.cs b/src/System.Web.Mvc/AreaRegistration.cs
new file mode 100644
index 00000000..6b80fe7e
--- /dev/null
+++ b/src/System.Web.Mvc/AreaRegistration.cs
@@ -0,0 +1,54 @@
+using System.Collections.Generic;
+using System.Web.Routing;
+
+namespace System.Web.Mvc
+{
+ public abstract class AreaRegistration
+ {
+ private const string TypeCacheName = "MVC-AreaRegistrationTypeCache.xml";
+
+ public abstract string AreaName { get; }
+
+ internal void CreateContextAndRegister(RouteCollection routes, object state)
+ {
+ AreaRegistrationContext context = new AreaRegistrationContext(AreaName, routes, state);
+
+ string thisNamespace = GetType().Namespace;
+ if (thisNamespace != null)
+ {
+ context.Namespaces.Add(thisNamespace + ".*");
+ }
+
+ RegisterArea(context);
+ }
+
+ private static bool IsAreaRegistrationType(Type type)
+ {
+ return
+ typeof(AreaRegistration).IsAssignableFrom(type) &&
+ type.GetConstructor(Type.EmptyTypes) != null;
+ }
+
+ public static void RegisterAllAreas()
+ {
+ RegisterAllAreas(null);
+ }
+
+ public static void RegisterAllAreas(object state)
+ {
+ RegisterAllAreas(RouteTable.Routes, new BuildManagerWrapper(), state);
+ }
+
+ internal static void RegisterAllAreas(RouteCollection routes, IBuildManager buildManager, object state)
+ {
+ List<Type> areaRegistrationTypes = TypeCacheUtil.GetFilteredTypesFromAssemblies(TypeCacheName, IsAreaRegistrationType, buildManager);
+ foreach (Type areaRegistrationType in areaRegistrationTypes)
+ {
+ AreaRegistration registration = (AreaRegistration)Activator.CreateInstance(areaRegistrationType);
+ registration.CreateContextAndRegister(routes, state);
+ }
+ }
+
+ public abstract void RegisterArea(AreaRegistrationContext context);
+ }
+}
diff --git a/src/System.Web.Mvc/AreaRegistrationContext.cs b/src/System.Web.Mvc/AreaRegistrationContext.cs
new file mode 100644
index 00000000..44a9a44f
--- /dev/null
+++ b/src/System.Web.Mvc/AreaRegistrationContext.cs
@@ -0,0 +1,93 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Web.Routing;
+
+namespace System.Web.Mvc
+{
+ public class AreaRegistrationContext
+ {
+ private readonly HashSet<string> _namespaces = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+
+ public AreaRegistrationContext(string areaName, RouteCollection routes)
+ : this(areaName, routes, null)
+ {
+ }
+
+ public AreaRegistrationContext(string areaName, RouteCollection routes, object state)
+ {
+ if (String.IsNullOrEmpty(areaName))
+ {
+ throw Error.ParameterCannotBeNullOrEmpty("areaName");
+ }
+ if (routes == null)
+ {
+ throw new ArgumentNullException("routes");
+ }
+
+ AreaName = areaName;
+ Routes = routes;
+ State = state;
+ }
+
+ public string AreaName { get; private set; }
+
+ public ICollection<string> Namespaces
+ {
+ get { return _namespaces; }
+ }
+
+ public RouteCollection Routes { get; private set; }
+
+ public object State { get; private set; }
+
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "This is not a regular URL as it may contain special routing characters.")]
+ public Route MapRoute(string name, string url)
+ {
+ return MapRoute(name, url, (object)null /* defaults */);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "This is not a regular URL as it may contain special routing characters.")]
+ public Route MapRoute(string name, string url, object defaults)
+ {
+ return MapRoute(name, url, defaults, (object)null /* constraints */);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "This is not a regular URL as it may contain special routing characters.")]
+ public Route MapRoute(string name, string url, object defaults, object constraints)
+ {
+ return MapRoute(name, url, defaults, constraints, null /* namespaces */);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "This is not a regular URL as it may contain special routing characters.")]
+ public Route MapRoute(string name, string url, string[] namespaces)
+ {
+ return MapRoute(name, url, (object)null /* defaults */, namespaces);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "This is not a regular URL as it may contain special routing characters.")]
+ public Route MapRoute(string name, string url, object defaults, string[] namespaces)
+ {
+ return MapRoute(name, url, defaults, null /* constraints */, namespaces);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "This is not a regular URL as it may contain special routing characters.")]
+ public Route MapRoute(string name, string url, object defaults, object constraints, string[] namespaces)
+ {
+ if (namespaces == null && Namespaces != null)
+ {
+ namespaces = Namespaces.ToArray();
+ }
+
+ Route route = Routes.MapRoute(name, url, defaults, constraints, namespaces);
+ route.DataTokens["area"] = AreaName;
+
+ // disabling the namespace lookup fallback mechanism keeps this areas from accidentally picking up
+ // controllers belonging to other areas
+ bool useNamespaceFallback = (namespaces == null || namespaces.Length == 0);
+ route.DataTokens["UseNamespaceFallback"] = useNamespaceFallback;
+
+ return route;
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/AssociatedMetadataProvider.cs b/src/System.Web.Mvc/AssociatedMetadataProvider.cs
new file mode 100644
index 00000000..1d1b7617
--- /dev/null
+++ b/src/System.Web.Mvc/AssociatedMetadataProvider.cs
@@ -0,0 +1,110 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Globalization;
+using System.Linq;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc
+{
+ // This class provides a good implementation of ModelMetadataProvider for people who will be
+ // using traditional classes with properties. It uses the buddy class support from
+ // DataAnnotations, and consolidates the three operations down to a single override
+ // for reading the attribute values and creating the metadata class.
+ public abstract class AssociatedMetadataProvider : ModelMetadataProvider
+ {
+ private static void ApplyMetadataAwareAttributes(IEnumerable<Attribute> attributes, ModelMetadata result)
+ {
+ foreach (IMetadataAware awareAttribute in attributes.OfType<IMetadataAware>())
+ {
+ awareAttribute.OnMetadataCreated(result);
+ }
+ }
+
+ protected abstract ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName);
+
+ protected virtual IEnumerable<Attribute> FilterAttributes(Type containerType, PropertyDescriptor propertyDescriptor, IEnumerable<Attribute> attributes)
+ {
+ if (typeof(ViewPage).IsAssignableFrom(containerType) || typeof(ViewUserControl).IsAssignableFrom(containerType))
+ {
+ return attributes.Where(a => !(a is ReadOnlyAttribute));
+ }
+
+ return attributes;
+ }
+
+ public override IEnumerable<ModelMetadata> GetMetadataForProperties(object container, Type containerType)
+ {
+ if (containerType == null)
+ {
+ throw new ArgumentNullException("containerType");
+ }
+
+ return GetMetadataForPropertiesImpl(container, containerType);
+ }
+
+ private IEnumerable<ModelMetadata> GetMetadataForPropertiesImpl(object container, Type containerType)
+ {
+ foreach (PropertyDescriptor property in GetTypeDescriptor(containerType).GetProperties())
+ {
+ Func<object> modelAccessor = container == null ? null : GetPropertyValueAccessor(container, property);
+ yield return GetMetadataForProperty(modelAccessor, containerType, property);
+ }
+ }
+
+ public override ModelMetadata GetMetadataForProperty(Func<object> modelAccessor, Type containerType, string propertyName)
+ {
+ if (containerType == null)
+ {
+ throw new ArgumentNullException("containerType");
+ }
+ if (String.IsNullOrEmpty(propertyName))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "propertyName");
+ }
+
+ ICustomTypeDescriptor typeDescriptor = GetTypeDescriptor(containerType);
+ PropertyDescriptor property = typeDescriptor.GetProperties().Find(propertyName, true);
+ if (property == null)
+ {
+ throw new ArgumentException(
+ String.Format(
+ CultureInfo.CurrentCulture,
+ MvcResources.Common_PropertyNotFound,
+ containerType.FullName, propertyName));
+ }
+
+ return GetMetadataForProperty(modelAccessor, containerType, property);
+ }
+
+ protected virtual ModelMetadata GetMetadataForProperty(Func<object> modelAccessor, Type containerType, PropertyDescriptor propertyDescriptor)
+ {
+ IEnumerable<Attribute> attributes = FilterAttributes(containerType, propertyDescriptor, propertyDescriptor.Attributes.Cast<Attribute>());
+ ModelMetadata result = CreateMetadata(attributes, containerType, modelAccessor, propertyDescriptor.PropertyType, propertyDescriptor.Name);
+ ApplyMetadataAwareAttributes(attributes, result);
+ return result;
+ }
+
+ public override ModelMetadata GetMetadataForType(Func<object> modelAccessor, Type modelType)
+ {
+ if (modelType == null)
+ {
+ throw new ArgumentNullException("modelType");
+ }
+
+ IEnumerable<Attribute> attributes = GetTypeDescriptor(modelType).GetAttributes().Cast<Attribute>();
+ ModelMetadata result = CreateMetadata(attributes, null /* containerType */, modelAccessor, modelType, null /* propertyName */);
+ ApplyMetadataAwareAttributes(attributes, result);
+ return result;
+ }
+
+ private static Func<object> GetPropertyValueAccessor(object container, PropertyDescriptor property)
+ {
+ return () => property.GetValue(container);
+ }
+
+ protected virtual ICustomTypeDescriptor GetTypeDescriptor(Type type)
+ {
+ return TypeDescriptorHelper.Get(type);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/AssociatedValidatorProvider.cs b/src/System.Web.Mvc/AssociatedValidatorProvider.cs
new file mode 100644
index 00000000..b91fade1
--- /dev/null
+++ b/src/System.Web.Mvc/AssociatedValidatorProvider.cs
@@ -0,0 +1,59 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Globalization;
+using System.Linq;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc
+{
+ public abstract class AssociatedValidatorProvider : ModelValidatorProvider
+ {
+ protected virtual ICustomTypeDescriptor GetTypeDescriptor(Type type)
+ {
+ return TypeDescriptorHelper.Get(type);
+ }
+
+ public sealed override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context)
+ {
+ if (metadata == null)
+ {
+ throw new ArgumentNullException("metadata");
+ }
+ if (context == null)
+ {
+ throw new ArgumentNullException("context");
+ }
+
+ if (metadata.ContainerType != null && !String.IsNullOrEmpty(metadata.PropertyName))
+ {
+ return GetValidatorsForProperty(metadata, context);
+ }
+
+ return GetValidatorsForType(metadata, context);
+ }
+
+ protected abstract IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes);
+
+ private IEnumerable<ModelValidator> GetValidatorsForProperty(ModelMetadata metadata, ControllerContext context)
+ {
+ ICustomTypeDescriptor typeDescriptor = GetTypeDescriptor(metadata.ContainerType);
+ PropertyDescriptor property = typeDescriptor.GetProperties().Find(metadata.PropertyName, true);
+ if (property == null)
+ {
+ throw new ArgumentException(
+ String.Format(
+ CultureInfo.CurrentCulture,
+ MvcResources.Common_PropertyNotFound,
+ metadata.ContainerType.FullName, metadata.PropertyName),
+ "metadata");
+ }
+
+ return GetValidators(metadata, context, property.Attributes.OfType<Attribute>());
+ }
+
+ private IEnumerable<ModelValidator> GetValidatorsForType(ModelMetadata metadata, ControllerContext context)
+ {
+ return GetValidators(metadata, context, GetTypeDescriptor(metadata.ModelType).GetAttributes().Cast<Attribute>());
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Async/ActionDescriptorCreator.cs b/src/System.Web.Mvc/Async/ActionDescriptorCreator.cs
new file mode 100644
index 00000000..0ecd2fc1
--- /dev/null
+++ b/src/System.Web.Mvc/Async/ActionDescriptorCreator.cs
@@ -0,0 +1,4 @@
+namespace System.Web.Mvc.Async
+{
+ internal delegate ActionDescriptor ActionDescriptorCreator(string actionName, ControllerDescriptor controllerDescriptor);
+}
diff --git a/src/System.Web.Mvc/Async/AsyncActionDescriptor.cs b/src/System.Web.Mvc/Async/AsyncActionDescriptor.cs
new file mode 100644
index 00000000..8fd7567e
--- /dev/null
+++ b/src/System.Web.Mvc/Async/AsyncActionDescriptor.cs
@@ -0,0 +1,32 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc.Async
+{
+ public abstract class AsyncActionDescriptor : ActionDescriptor
+ {
+ public abstract IAsyncResult BeginExecute(ControllerContext controllerContext, IDictionary<string, object> parameters, AsyncCallback callback, object state);
+
+ public abstract object EndExecute(IAsyncResult asyncResult);
+
+ public override object Execute(ControllerContext controllerContext, IDictionary<string, object> parameters)
+ {
+ string errorMessage = String.Format(CultureInfo.CurrentCulture, MvcResources.AsyncActionDescriptor_CannotExecuteSynchronously,
+ ActionName);
+
+ throw new InvalidOperationException(errorMessage);
+ }
+
+ internal static AsyncManager GetAsyncManager(ControllerBase controller)
+ {
+ IAsyncManagerContainer helperContainer = controller as IAsyncManagerContainer;
+ if (helperContainer == null)
+ {
+ throw Error.AsyncCommon_ControllerMustImplementIAsyncManagerContainer(controller.GetType());
+ }
+
+ return helperContainer.AsyncManager;
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Async/AsyncActionMethodSelector.cs b/src/System.Web.Mvc/Async/AsyncActionMethodSelector.cs
new file mode 100644
index 00000000..0d5d0d55
--- /dev/null
+++ b/src/System.Web.Mvc/Async/AsyncActionMethodSelector.cs
@@ -0,0 +1,227 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc.Async
+{
+ internal sealed class AsyncActionMethodSelector
+ {
+ // This flag controls async action binding for backwards compat since Controller now supports async.
+ // Set to true for classes that derive from AsyncController. In this case, FooAsync/FooCompleted is
+ // bound as a single async action pair "Foo". If false, they're bound as 2 separate sync actions.
+ // Practically, if this is false, then IsAsyncSuffixedMethod and IsCompeltedSuffixedMethod return false.
+ private bool _allowLegacyAsyncActions;
+
+ public AsyncActionMethodSelector(Type controllerType, bool allowLegacyAsyncActions = true)
+ {
+ _allowLegacyAsyncActions = allowLegacyAsyncActions;
+ ControllerType = controllerType;
+ PopulateLookupTables();
+ }
+
+ public Type ControllerType { get; private set; }
+
+ public MethodInfo[] AliasedMethods { get; private set; }
+
+ public ILookup<string, MethodInfo> NonAliasedMethods { get; private set; }
+
+ private AmbiguousMatchException CreateAmbiguousActionMatchException(IEnumerable<MethodInfo> ambiguousMethods, string actionName)
+ {
+ string ambiguityList = CreateAmbiguousMatchList(ambiguousMethods);
+ string message = String.Format(CultureInfo.CurrentCulture, MvcResources.ActionMethodSelector_AmbiguousMatch,
+ actionName, ControllerType.Name, ambiguityList);
+ return new AmbiguousMatchException(message);
+ }
+
+ private AmbiguousMatchException CreateAmbiguousMethodMatchException(IEnumerable<MethodInfo> ambiguousMethods, string methodName)
+ {
+ string ambiguityList = CreateAmbiguousMatchList(ambiguousMethods);
+ string message = String.Format(CultureInfo.CurrentCulture, MvcResources.AsyncActionMethodSelector_AmbiguousMethodMatch,
+ methodName, ControllerType.Name, ambiguityList);
+ return new AmbiguousMatchException(message);
+ }
+
+ private static string CreateAmbiguousMatchList(IEnumerable<MethodInfo> ambiguousMethods)
+ {
+ StringBuilder exceptionMessageBuilder = new StringBuilder();
+ foreach (MethodInfo methodInfo in ambiguousMethods)
+ {
+ exceptionMessageBuilder.AppendLine();
+ exceptionMessageBuilder.AppendFormat(CultureInfo.CurrentCulture, MvcResources.ActionMethodSelector_AmbiguousMatchType, methodInfo, methodInfo.DeclaringType.FullName);
+ }
+
+ return exceptionMessageBuilder.ToString();
+ }
+
+ public ActionDescriptorCreator FindAction(ControllerContext controllerContext, string actionName)
+ {
+ List<MethodInfo> methodsMatchingName = GetMatchingAliasedMethods(controllerContext, actionName);
+ methodsMatchingName.AddRange(NonAliasedMethods[actionName]);
+ List<MethodInfo> finalMethods = RunSelectionFilters(controllerContext, methodsMatchingName);
+
+ switch (finalMethods.Count)
+ {
+ case 0:
+ return null;
+
+ case 1:
+ MethodInfo entryMethod = finalMethods[0];
+ return GetActionDescriptorDelegate(entryMethod);
+
+ default:
+ throw CreateAmbiguousActionMatchException(finalMethods, actionName);
+ }
+ }
+
+ private ActionDescriptorCreator GetActionDescriptorDelegate(MethodInfo entryMethod)
+ {
+ // Does the action return a Task?
+ if (entryMethod.ReturnType != null && typeof(Task).IsAssignableFrom(entryMethod.ReturnType))
+ {
+ return (actionName, controllerDescriptor) => new TaskAsyncActionDescriptor(entryMethod, actionName, controllerDescriptor);
+ }
+
+ // Is this the FooAsync() / FooCompleted() pattern?
+ if (IsAsyncSuffixedMethod(entryMethod))
+ {
+ string completionMethodName = entryMethod.Name.Substring(0, entryMethod.Name.Length - "Async".Length) + "Completed";
+ MethodInfo completionMethod = GetMethodByName(completionMethodName);
+ if (completionMethod != null)
+ {
+ return (actionName, controllerDescriptor) => new ReflectedAsyncActionDescriptor(entryMethod, completionMethod, actionName, controllerDescriptor);
+ }
+ else
+ {
+ throw Error.AsyncActionMethodSelector_CouldNotFindMethod(completionMethodName, ControllerType);
+ }
+ }
+
+ // Fallback to synchronous method
+ return (actionName, controllerDescriptor) => new ReflectedActionDescriptor(entryMethod, actionName, controllerDescriptor);
+ }
+
+ private string GetCanonicalMethodName(MethodInfo methodInfo)
+ {
+ string methodName = methodInfo.Name;
+ return (IsAsyncSuffixedMethod(methodInfo))
+ ? methodName.Substring(0, methodName.Length - "Async".Length)
+ : methodName;
+ }
+
+ internal List<MethodInfo> GetMatchingAliasedMethods(ControllerContext controllerContext, string actionName)
+ {
+ // find all aliased methods which are opting in to this request
+ // to opt in, all attributes defined on the method must return true
+
+ var methods = from methodInfo in AliasedMethods
+ let attrs = ReflectedAttributeCache.GetActionNameSelectorAttributes(methodInfo)
+ where attrs.All(attr => attr.IsValidName(controllerContext, actionName, methodInfo))
+ select methodInfo;
+ return methods.ToList();
+ }
+
+ private bool IsAsyncSuffixedMethod(MethodInfo methodInfo)
+ {
+ return _allowLegacyAsyncActions && methodInfo.Name.EndsWith("Async", StringComparison.OrdinalIgnoreCase);
+ }
+
+ private bool IsCompletedSuffixedMethod(MethodInfo methodInfo)
+ {
+ return _allowLegacyAsyncActions && methodInfo.Name.EndsWith("Completed", StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static bool IsMethodDecoratedWithAliasingAttribute(MethodInfo methodInfo)
+ {
+ return methodInfo.IsDefined(typeof(ActionNameSelectorAttribute), true /* inherit */);
+ }
+
+ private MethodInfo GetMethodByName(string methodName)
+ {
+ List<MethodInfo> methods = (from MethodInfo methodInfo in ControllerType.GetMember(methodName, MemberTypes.Method, BindingFlags.Instance | BindingFlags.Public | BindingFlags.InvokeMethod | BindingFlags.IgnoreCase)
+ where IsValidActionMethod(methodInfo, false /* stripInfrastructureMethods */)
+ select methodInfo).ToList();
+
+ switch (methods.Count)
+ {
+ case 0:
+ return null;
+
+ case 1:
+ return methods[0];
+
+ default:
+ throw CreateAmbiguousMethodMatchException(methods, methodName);
+ }
+ }
+
+ private bool IsValidActionMethod(MethodInfo methodInfo)
+ {
+ return IsValidActionMethod(methodInfo, true /* stripInfrastructureMethods */);
+ }
+
+ private bool IsValidActionMethod(MethodInfo methodInfo, bool stripInfrastructureMethods)
+ {
+ if (methodInfo.IsSpecialName)
+ {
+ // not a normal method, e.g. a constructor or an event
+ return false;
+ }
+
+ if (methodInfo.GetBaseDefinition().DeclaringType.IsAssignableFrom(typeof(AsyncController)))
+ {
+ // is a method on Object, ControllerBase, Controller, or AsyncController
+ return false;
+ }
+
+ if (stripInfrastructureMethods)
+ {
+ if (IsCompletedSuffixedMethod(methodInfo))
+ {
+ // do not match FooCompleted() methods, as these are infrastructure methods
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private void PopulateLookupTables()
+ {
+ MethodInfo[] allMethods = ControllerType.GetMethods(BindingFlags.InvokeMethod | BindingFlags.Instance | BindingFlags.Public);
+ MethodInfo[] actionMethods = Array.FindAll(allMethods, IsValidActionMethod);
+
+ AliasedMethods = Array.FindAll(actionMethods, IsMethodDecoratedWithAliasingAttribute);
+ NonAliasedMethods = actionMethods.Except(AliasedMethods).ToLookup(GetCanonicalMethodName, StringComparer.OrdinalIgnoreCase);
+ }
+
+ private static List<MethodInfo> RunSelectionFilters(ControllerContext controllerContext, List<MethodInfo> methodInfos)
+ {
+ // remove all methods which are opting out of this request
+ // to opt out, at least one attribute defined on the method must return false
+
+ List<MethodInfo> matchesWithSelectionAttributes = new List<MethodInfo>();
+ List<MethodInfo> matchesWithoutSelectionAttributes = new List<MethodInfo>();
+
+ foreach (MethodInfo methodInfo in methodInfos)
+ {
+ ICollection<ActionMethodSelectorAttribute> attrs = ReflectedAttributeCache.GetActionMethodSelectorAttributes(methodInfo);
+ if (attrs.Count == 0)
+ {
+ matchesWithoutSelectionAttributes.Add(methodInfo);
+ }
+ else if (attrs.All(attr => attr.IsValidForRequest(controllerContext, methodInfo)))
+ {
+ matchesWithSelectionAttributes.Add(methodInfo);
+ }
+ }
+
+ // if a matching action method had a selection attribute, consider it more specific than a matching action method
+ // without a selection attribute
+ return (matchesWithSelectionAttributes.Count > 0) ? matchesWithSelectionAttributes : matchesWithoutSelectionAttributes;
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Async/AsyncControllerActionInvoker.cs b/src/System.Web.Mvc/Async/AsyncControllerActionInvoker.cs
new file mode 100644
index 00000000..0d62065c
--- /dev/null
+++ b/src/System.Web.Mvc/Async/AsyncControllerActionInvoker.cs
@@ -0,0 +1,324 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+
+namespace System.Web.Mvc.Async
+{
+ public class AsyncControllerActionInvoker : ControllerActionInvoker, IAsyncActionInvoker
+ {
+ private static readonly object _invokeActionTag = new object();
+ private static readonly object _invokeActionMethodTag = new object();
+ private static readonly object _invokeActionMethodWithFiltersTag = new object();
+
+ public virtual IAsyncResult BeginInvokeAction(ControllerContext controllerContext, string actionName, AsyncCallback callback, object state)
+ {
+ if (controllerContext == null)
+ {
+ throw new ArgumentNullException("controllerContext");
+ }
+ if (String.IsNullOrEmpty(actionName))
+ {
+ throw Error.ParameterCannotBeNullOrEmpty("actionName");
+ }
+
+ ControllerDescriptor controllerDescriptor = GetControllerDescriptor(controllerContext);
+ ActionDescriptor actionDescriptor = FindAction(controllerContext, controllerDescriptor, actionName);
+ if (actionDescriptor != null)
+ {
+ FilterInfo filterInfo = GetFilters(controllerContext, actionDescriptor);
+ Action continuation = null;
+
+ BeginInvokeDelegate beginDelegate = delegate(AsyncCallback asyncCallback, object asyncState)
+ {
+ try
+ {
+ AuthorizationContext authContext = InvokeAuthorizationFilters(controllerContext, filterInfo.AuthorizationFilters, actionDescriptor);
+ if (authContext.Result != null)
+ {
+ // the auth filter signaled that we should let it short-circuit the request
+ continuation = () => InvokeActionResult(controllerContext, authContext.Result);
+ }
+ else
+ {
+ if (controllerContext.Controller.ValidateRequest)
+ {
+ ValidateRequest(controllerContext);
+ }
+
+ IDictionary<string, object> parameters = GetParameterValues(controllerContext, actionDescriptor);
+ IAsyncResult asyncResult = BeginInvokeActionMethodWithFilters(controllerContext, filterInfo.ActionFilters, actionDescriptor, parameters, asyncCallback, asyncState);
+ continuation = () =>
+ {
+ ActionExecutedContext postActionContext = EndInvokeActionMethodWithFilters(asyncResult);
+ InvokeActionResultWithFilters(controllerContext, filterInfo.ResultFilters, postActionContext.Result);
+ };
+ return asyncResult;
+ }
+ }
+ catch (ThreadAbortException)
+ {
+ // This type of exception occurs as a result of Response.Redirect(), but we special-case so that
+ // the filters don't see this as an error.
+ throw;
+ }
+ catch (Exception ex)
+ {
+ // something blew up, so execute the exception filters
+ ExceptionContext exceptionContext = InvokeExceptionFilters(controllerContext, filterInfo.ExceptionFilters, ex);
+ if (!exceptionContext.ExceptionHandled)
+ {
+ throw;
+ }
+
+ continuation = () => InvokeActionResult(controllerContext, exceptionContext.Result);
+ }
+
+ return BeginInvokeAction_MakeSynchronousAsyncResult(asyncCallback, asyncState);
+ };
+
+ EndInvokeDelegate<bool> endDelegate = delegate(IAsyncResult asyncResult)
+ {
+ try
+ {
+ continuation();
+ }
+ catch (ThreadAbortException)
+ {
+ // This type of exception occurs as a result of Response.Redirect(), but we special-case so that
+ // the filters don't see this as an error.
+ throw;
+ }
+ catch (Exception ex)
+ {
+ // something blew up, so execute the exception filters
+ ExceptionContext exceptionContext = InvokeExceptionFilters(controllerContext, filterInfo.ExceptionFilters, ex);
+ if (!exceptionContext.ExceptionHandled)
+ {
+ throw;
+ }
+ InvokeActionResult(controllerContext, exceptionContext.Result);
+ }
+
+ return true;
+ };
+
+ return AsyncResultWrapper.Begin(callback, state, beginDelegate, endDelegate, _invokeActionTag);
+ }
+ else
+ {
+ // Notify the controller that no action was found.
+ return BeginInvokeAction_ActionNotFound(callback, state);
+ }
+ }
+
+ private static IAsyncResult BeginInvokeAction_ActionNotFound(AsyncCallback callback, object state)
+ {
+ BeginInvokeDelegate beginDelegate = BeginInvokeAction_MakeSynchronousAsyncResult;
+
+ EndInvokeDelegate<bool> endDelegate = delegate(IAsyncResult asyncResult)
+ {
+ return false;
+ };
+
+ return AsyncResultWrapper.Begin(callback, state, beginDelegate, endDelegate, _invokeActionTag);
+ }
+
+ private static IAsyncResult BeginInvokeAction_MakeSynchronousAsyncResult(AsyncCallback callback, object state)
+ {
+ SimpleAsyncResult asyncResult = new SimpleAsyncResult(state);
+ asyncResult.MarkCompleted(true /* completedSynchronously */, callback);
+ return asyncResult;
+ }
+
+ protected internal virtual IAsyncResult BeginInvokeActionMethod(ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary<string, object> parameters, AsyncCallback callback, object state)
+ {
+ AsyncActionDescriptor asyncActionDescriptor = actionDescriptor as AsyncActionDescriptor;
+ if (asyncActionDescriptor != null)
+ {
+ return BeginInvokeAsynchronousActionMethod(controllerContext, asyncActionDescriptor, parameters, callback, state);
+ }
+ else
+ {
+ return BeginInvokeSynchronousActionMethod(controllerContext, actionDescriptor, parameters, callback, state);
+ }
+ }
+
+ protected internal virtual IAsyncResult BeginInvokeActionMethodWithFilters(ControllerContext controllerContext, IList<IActionFilter> filters, ActionDescriptor actionDescriptor, IDictionary<string, object> parameters, AsyncCallback callback, object state)
+ {
+ Func<ActionExecutedContext> endContinuation = null;
+
+ BeginInvokeDelegate beginDelegate = delegate(AsyncCallback asyncCallback, object asyncState)
+ {
+ ActionExecutingContext preContext = new ActionExecutingContext(controllerContext, actionDescriptor, parameters);
+ IAsyncResult innerAsyncResult = null;
+
+ Func<Func<ActionExecutedContext>> beginContinuation = () =>
+ {
+ innerAsyncResult = BeginInvokeActionMethod(controllerContext, actionDescriptor, parameters, asyncCallback, asyncState);
+ return () =>
+ new ActionExecutedContext(controllerContext, actionDescriptor, false /* canceled */, null /* exception */)
+ {
+ Result = EndInvokeActionMethod(innerAsyncResult)
+ };
+ };
+
+ // need to reverse the filter list because the continuations are built up backward
+ Func<Func<ActionExecutedContext>> thunk = filters.Reverse().Aggregate(beginContinuation,
+ (next, filter) => () => InvokeActionMethodFilterAsynchronously(filter, preContext, next));
+ endContinuation = thunk();
+
+ if (innerAsyncResult != null)
+ {
+ // we're just waiting for the inner result to complete
+ return innerAsyncResult;
+ }
+ else
+ {
+ // something was short-circuited and the action was not called, so this was a synchronous operation
+ SimpleAsyncResult newAsyncResult = new SimpleAsyncResult(asyncState);
+ newAsyncResult.MarkCompleted(true /* completedSynchronously */, asyncCallback);
+ return newAsyncResult;
+ }
+ };
+
+ EndInvokeDelegate<ActionExecutedContext> endDelegate = delegate(IAsyncResult asyncResult)
+ {
+ return endContinuation();
+ };
+
+ return AsyncResultWrapper.Begin(callback, state, beginDelegate, endDelegate, _invokeActionMethodWithFiltersTag);
+ }
+
+ private IAsyncResult BeginInvokeAsynchronousActionMethod(ControllerContext controllerContext, AsyncActionDescriptor actionDescriptor, IDictionary<string, object> parameters, AsyncCallback callback, object state)
+ {
+ BeginInvokeDelegate beginDelegate = delegate(AsyncCallback asyncCallback, object asyncState)
+ {
+ return actionDescriptor.BeginExecute(controllerContext, parameters, asyncCallback, asyncState);
+ };
+
+ EndInvokeDelegate<ActionResult> endDelegate = delegate(IAsyncResult asyncResult)
+ {
+ object returnValue = actionDescriptor.EndExecute(asyncResult);
+ ActionResult result = CreateActionResult(controllerContext, actionDescriptor, returnValue);
+ return result;
+ };
+
+ return AsyncResultWrapper.Begin(callback, state, beginDelegate, endDelegate, _invokeActionMethodTag);
+ }
+
+ private IAsyncResult BeginInvokeSynchronousActionMethod(ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary<string, object> parameters, AsyncCallback callback, object state)
+ {
+ return AsyncResultWrapper.BeginSynchronous(callback, state,
+ () => InvokeSynchronousActionMethod(controllerContext, actionDescriptor, parameters),
+ _invokeActionMethodTag);
+ }
+
+ public virtual bool EndInvokeAction(IAsyncResult asyncResult)
+ {
+ return AsyncResultWrapper.End<bool>(asyncResult, _invokeActionTag);
+ }
+
+ protected internal virtual ActionResult EndInvokeActionMethod(IAsyncResult asyncResult)
+ {
+ return AsyncResultWrapper.End<ActionResult>(asyncResult, _invokeActionMethodTag);
+ }
+
+ protected internal virtual ActionExecutedContext EndInvokeActionMethodWithFilters(IAsyncResult asyncResult)
+ {
+ return AsyncResultWrapper.End<ActionExecutedContext>(asyncResult, _invokeActionMethodWithFiltersTag);
+ }
+
+ protected override ControllerDescriptor GetControllerDescriptor(ControllerContext controllerContext)
+ {
+ Type controllerType = controllerContext.Controller.GetType();
+ ControllerDescriptor controllerDescriptor = DescriptorCache.GetDescriptor(controllerType, () => new ReflectedAsyncControllerDescriptor(controllerType));
+ return controllerDescriptor;
+ }
+
+ internal static Func<ActionExecutedContext> InvokeActionMethodFilterAsynchronously(IActionFilter filter, ActionExecutingContext preContext, Func<Func<ActionExecutedContext>> nextInChain)
+ {
+ filter.OnActionExecuting(preContext);
+ if (preContext.Result != null)
+ {
+ ActionExecutedContext shortCircuitedPostContext = new ActionExecutedContext(preContext, preContext.ActionDescriptor, true /* canceled */, null /* exception */)
+ {
+ Result = preContext.Result
+ };
+ return () => shortCircuitedPostContext;
+ }
+
+ // There is a nested try / catch block here that contains much the same logic as the outer block.
+ // Since an exception can occur on either side of the asynchronous invocation, we need guards on
+ // on both sides. In the code below, the second side is represented by the nested delegate. This
+ // is really just a parallel of the synchronous ControllerActionInvoker.InvokeActionMethodFilter()
+ // method.
+
+ try
+ {
+ Func<ActionExecutedContext> continuation = nextInChain();
+
+ // add our own continuation, then return the new function
+ return () =>
+ {
+ ActionExecutedContext postContext;
+ bool wasError = true;
+
+ try
+ {
+ postContext = continuation();
+ wasError = false;
+ }
+ catch (ThreadAbortException)
+ {
+ // This type of exception occurs as a result of Response.Redirect(), but we special-case so that
+ // the filters don't see this as an error.
+ postContext = new ActionExecutedContext(preContext, preContext.ActionDescriptor, false /* canceled */, null /* exception */);
+ filter.OnActionExecuted(postContext);
+ throw;
+ }
+ catch (Exception ex)
+ {
+ postContext = new ActionExecutedContext(preContext, preContext.ActionDescriptor, false /* canceled */, ex);
+ filter.OnActionExecuted(postContext);
+ if (!postContext.ExceptionHandled)
+ {
+ throw;
+ }
+ }
+ if (!wasError)
+ {
+ filter.OnActionExecuted(postContext);
+ }
+
+ return postContext;
+ };
+ }
+ catch (ThreadAbortException)
+ {
+ // This type of exception occurs as a result of Response.Redirect(), but we special-case so that
+ // the filters don't see this as an error.
+ ActionExecutedContext postContext = new ActionExecutedContext(preContext, preContext.ActionDescriptor, false /* canceled */, null /* exception */);
+ filter.OnActionExecuted(postContext);
+ throw;
+ }
+ catch (Exception ex)
+ {
+ ActionExecutedContext postContext = new ActionExecutedContext(preContext, preContext.ActionDescriptor, false /* canceled */, ex);
+ filter.OnActionExecuted(postContext);
+ if (postContext.ExceptionHandled)
+ {
+ return () => postContext;
+ }
+ else
+ {
+ throw;
+ }
+ }
+ }
+
+ private ActionResult InvokeSynchronousActionMethod(ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary<string, object> parameters)
+ {
+ return InvokeActionMethod(controllerContext, actionDescriptor, parameters);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Async/AsyncManager.cs b/src/System.Web.Mvc/Async/AsyncManager.cs
new file mode 100644
index 00000000..0e4869aa
--- /dev/null
+++ b/src/System.Web.Mvc/Async/AsyncManager.cs
@@ -0,0 +1,80 @@
+using System.Collections.Generic;
+using System.Threading;
+
+namespace System.Web.Mvc.Async
+{
+ public class AsyncManager
+ {
+ private readonly SynchronizationContext _syncContext;
+
+ /// <summary>
+ /// default timeout is 45 sec
+ /// </summary>
+ /// <remarks>
+ /// from: http://msdn.microsoft.com/en-us/library/system.web.ui.page.asynctimeout.aspx
+ /// </remarks>
+ private int _timeout = 45 * 1000;
+
+ public AsyncManager()
+ : this(null /* syncContext */)
+ {
+ }
+
+ public AsyncManager(SynchronizationContext syncContext)
+ {
+ _syncContext = syncContext ?? SynchronizationContextUtil.GetSynchronizationContext();
+
+ OutstandingOperations = new OperationCounter();
+ OutstandingOperations.Completed += delegate
+ {
+ Finish();
+ };
+
+ Parameters = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
+ }
+
+ public event EventHandler Finished;
+
+ public OperationCounter OutstandingOperations { get; private set; }
+
+ public IDictionary<string, object> Parameters { get; private set; }
+
+ /// <summary>
+ /// Measured in milliseconds, Timeout.Infinite means 'no timeout'
+ /// </summary>
+ public int Timeout
+ {
+ get { return _timeout; }
+ set
+ {
+ if (value < -1)
+ {
+ throw Error.AsyncCommon_InvalidTimeout("value");
+ }
+ _timeout = value;
+ }
+ }
+
+ /// <summary>
+ /// The developer may call this function to signal that all operations are complete instead of
+ /// waiting for the operation counter to reach zero.
+ /// </summary>
+ public virtual void Finish()
+ {
+ EventHandler handler = Finished;
+ if (handler != null)
+ {
+ handler(this, EventArgs.Empty);
+ }
+ }
+
+ /// <summary>
+ /// Executes a callback in the current synchronization context, which gives access to HttpContext and related items.
+ /// </summary>
+ /// <param name="action"></param>
+ public virtual void Sync(Action action)
+ {
+ _syncContext.Sync(action);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Async/AsyncResultWrapper.cs b/src/System.Web.Mvc/Async/AsyncResultWrapper.cs
new file mode 100644
index 00000000..8cc54b7c
--- /dev/null
+++ b/src/System.Web.Mvc/Async/AsyncResultWrapper.cs
@@ -0,0 +1,303 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Threading;
+
+namespace System.Web.Mvc.Async
+{
+ // This class is used for the following pattern:
+
+ // public IAsyncResult BeginInner(..., callback, state);
+ // public TInnerResult EndInner(asyncResult);
+ // public IAsyncResult BeginOuter(..., callback, state);
+ // public TOuterResult EndOuter(asyncResult);
+
+ // That is, Begin/EndOuter() wrap Begin/EndInner(), potentially with pre- and post-processing.
+
+ [DebuggerNonUserCode]
+ internal static class AsyncResultWrapper
+ {
+ // helper methods
+
+ private static Func<AsyncVoid> MakeVoidDelegate(Action action)
+ {
+ return () =>
+ {
+ action();
+ return default(AsyncVoid);
+ };
+ }
+
+ private static EndInvokeDelegate<AsyncVoid> MakeVoidDelegate(EndInvokeDelegate endDelegate)
+ {
+ return ar =>
+ {
+ endDelegate(ar);
+ return default(AsyncVoid);
+ };
+ }
+
+ // kicks off an asynchronous operation
+
+ public static IAsyncResult Begin<TResult>(AsyncCallback callback, object state, BeginInvokeDelegate beginDelegate, EndInvokeDelegate<TResult> endDelegate)
+ {
+ return Begin<TResult>(callback, state, beginDelegate, endDelegate, tag: null);
+ }
+
+ public static IAsyncResult Begin<TResult>(AsyncCallback callback, object state, BeginInvokeDelegate beginDelegate, EndInvokeDelegate<TResult> endDelegate, object tag)
+ {
+ return Begin<TResult>(callback, state, beginDelegate, endDelegate, tag, Timeout.Infinite);
+ }
+
+ public static IAsyncResult Begin<TResult>(AsyncCallback callback, object state, BeginInvokeDelegate beginDelegate, EndInvokeDelegate<TResult> endDelegate, object tag, int timeout)
+ {
+ WrappedAsyncResult<TResult> asyncResult = new WrappedAsyncResult<TResult>(beginDelegate, endDelegate, tag);
+ asyncResult.Begin(callback, state, timeout);
+ return asyncResult;
+ }
+
+ public static IAsyncResult Begin(AsyncCallback callback, object state, BeginInvokeDelegate beginDelegate, EndInvokeDelegate endDelegate)
+ {
+ return Begin(callback, state, beginDelegate, endDelegate, tag: null);
+ }
+
+ public static IAsyncResult Begin(AsyncCallback callback, object state, BeginInvokeDelegate beginDelegate, EndInvokeDelegate endDelegate, object tag)
+ {
+ return Begin(callback, state, beginDelegate, endDelegate, tag, Timeout.Infinite);
+ }
+
+ public static IAsyncResult Begin(AsyncCallback callback, object state, BeginInvokeDelegate beginDelegate, EndInvokeDelegate endDelegate, object tag, int timeout)
+ {
+ return Begin<AsyncVoid>(callback, state, beginDelegate, MakeVoidDelegate(endDelegate), tag, timeout);
+ }
+
+ // wraps a synchronous operation in an asynchronous wrapper, but still completes synchronously
+
+ public static IAsyncResult BeginSynchronous<TResult>(AsyncCallback callback, object state, Func<TResult> func)
+ {
+ return BeginSynchronous<TResult>(callback, state, func, tag: null);
+ }
+
+ public static IAsyncResult BeginSynchronous<TResult>(AsyncCallback callback, object state, Func<TResult> func, object tag)
+ {
+ // Begin() doesn't perform any work on its own and returns immediately.
+ BeginInvokeDelegate beginDelegate = (asyncCallback, asyncState) =>
+ {
+ SimpleAsyncResult innerAsyncResult = new SimpleAsyncResult(asyncState);
+ innerAsyncResult.MarkCompleted(completedSynchronously: true, callback: asyncCallback);
+ return innerAsyncResult;
+ };
+
+ // The End() method blocks.
+ EndInvokeDelegate<TResult> endDelegate = _ =>
+ {
+ return func();
+ };
+
+ WrappedAsyncResult<TResult> asyncResult = new WrappedAsyncResult<TResult>(beginDelegate, endDelegate, tag);
+ asyncResult.Begin(callback, state, Timeout.Infinite);
+ return asyncResult;
+ }
+
+ public static IAsyncResult BeginSynchronous(AsyncCallback callback, object state, Action action)
+ {
+ return BeginSynchronous(callback, state, action, tag: null);
+ }
+
+ public static IAsyncResult BeginSynchronous(AsyncCallback callback, object state, Action action, object tag)
+ {
+ return BeginSynchronous<AsyncVoid>(callback, state, MakeVoidDelegate(action), tag);
+ }
+
+ // completes an asynchronous operation
+
+ public static TResult End<TResult>(IAsyncResult asyncResult)
+ {
+ return End<TResult>(asyncResult, tag: null);
+ }
+
+ public static TResult End<TResult>(IAsyncResult asyncResult, object tag)
+ {
+ return WrappedAsyncResult<TResult>.Cast(asyncResult, tag).End();
+ }
+
+ public static void End(IAsyncResult asyncResult)
+ {
+ End(asyncResult, tag: null);
+ }
+
+ public static void End(IAsyncResult asyncResult, object tag)
+ {
+ End<AsyncVoid>(asyncResult, tag);
+ }
+
+ [DebuggerNonUserCode]
+ [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "The Timer will be disposed of either when it fires or when the operation completes successfully.")]
+ private sealed class WrappedAsyncResult<TResult> : IAsyncResult
+ {
+ private const int AsyncStateNone = 0;
+ private const int AsyncStateBeginUnwound = 1;
+ private const int AsyncStateCallbackFired = 2;
+
+ private int _asyncState;
+ private readonly BeginInvokeDelegate _beginDelegate;
+ private readonly object _beginDelegateLockObj = new object();
+ private readonly EndInvokeDelegate<TResult> _endDelegate;
+ private readonly SingleEntryGate _endExecutedGate = new SingleEntryGate(); // prevent End() from being called twice
+ private readonly SingleEntryGate _handleCallbackGate = new SingleEntryGate(); // prevent callback from being handled multiple times
+ private readonly object _tag; // prevent an instance of this type from being passed to the wrong End() method
+ private IAsyncResult _innerAsyncResult;
+ private AsyncCallback _originalCallback;
+ private volatile bool _timedOut;
+ private Timer _timer;
+
+ public WrappedAsyncResult(BeginInvokeDelegate beginDelegate, EndInvokeDelegate<TResult> endDelegate, object tag)
+ {
+ _beginDelegate = beginDelegate;
+ _endDelegate = endDelegate;
+ _tag = tag;
+ }
+
+ public object AsyncState
+ {
+ get { return _innerAsyncResult.AsyncState; }
+ }
+
+ public WaitHandle AsyncWaitHandle
+ {
+ get { return _innerAsyncResult.AsyncWaitHandle; }
+ }
+
+ public bool CompletedSynchronously { get; private set; }
+
+ public bool IsCompleted
+ {
+ get { return _innerAsyncResult.IsCompleted; }
+ }
+
+ // kicks off the process, instantiates a timer if requested
+ public void Begin(AsyncCallback callback, object state, int timeout)
+ {
+ _originalCallback = callback;
+
+ // Force the target Begin() operation to complete before the callback can continue,
+ // since the target operation might perform post-processing of the data.
+ lock (_beginDelegateLockObj)
+ {
+ _innerAsyncResult = _beginDelegate(HandleAsynchronousCompletion, state);
+
+ // If the callback has already fired, then the completion routine has no-oped and we
+ // can just treat this as if it were a normal synchronous completion.
+ int originalState = Interlocked.Exchange(ref _asyncState, AsyncStateBeginUnwound);
+ bool callbackAlreadyFired = (originalState == AsyncStateCallbackFired);
+
+ CompletedSynchronously = callbackAlreadyFired || _innerAsyncResult.CompletedSynchronously;
+
+ if (!CompletedSynchronously)
+ {
+ if (timeout > Timeout.Infinite)
+ {
+ CreateTimer(timeout);
+ }
+ }
+ }
+
+ if (CompletedSynchronously)
+ {
+ if (callback != null)
+ {
+ callback(this);
+ }
+ }
+ }
+
+ public static WrappedAsyncResult<TResult> Cast(IAsyncResult asyncResult, object tag)
+ {
+ if (asyncResult == null)
+ {
+ throw new ArgumentNullException("asyncResult");
+ }
+
+ WrappedAsyncResult<TResult> castResult = asyncResult as WrappedAsyncResult<TResult>;
+ if (castResult != null && Equals(castResult._tag, tag))
+ {
+ return castResult;
+ }
+ else
+ {
+ throw Error.AsyncCommon_InvalidAsyncResult("asyncResult");
+ }
+ }
+
+ private void CreateTimer(int timeout)
+ {
+ // this method should be called within a lock(_beginDelegateLockObj)
+ _timer = new Timer(HandleTimeout, null, timeout, Timeout.Infinite /* disable periodic signaling */);
+ }
+
+ public TResult End()
+ {
+ if (!_endExecutedGate.TryEnter())
+ {
+ throw Error.AsyncCommon_AsyncResultAlreadyConsumed();
+ }
+
+ if (_timedOut)
+ {
+ throw new TimeoutException();
+ }
+ WaitForBeginToCompleteAndDestroyTimer();
+
+ return _endDelegate(_innerAsyncResult);
+ }
+
+ private void ExecuteAsynchronousCallback(bool timedOut)
+ {
+ WaitForBeginToCompleteAndDestroyTimer();
+
+ if (_handleCallbackGate.TryEnter())
+ {
+ _timedOut = timedOut;
+ if (_originalCallback != null)
+ {
+ _originalCallback(this);
+ }
+ }
+ }
+
+ private void HandleAsynchronousCompletion(IAsyncResult asyncResult)
+ {
+ // Transition the async state to CALLBACK_FIRED. If the Begin* method hasn't yet unwound,
+ // then we can no-op here since the Begin method will query the _asyncState field and
+ // treat this as a regular synchronous completion.
+ int originalState = Interlocked.Exchange(ref _asyncState, AsyncStateCallbackFired);
+ if (originalState != AsyncStateBeginUnwound)
+ {
+ return;
+ }
+
+ ExecuteAsynchronousCallback(timedOut: false);
+ }
+
+ private void HandleTimeout(object state)
+ {
+ ExecuteAsynchronousCallback(timedOut: true);
+ }
+
+ private void WaitForBeginToCompleteAndDestroyTimer()
+ {
+ lock (_beginDelegateLockObj)
+ {
+ // Wait for the target Begin() method to complete, as it might be performing
+ // post-processing. This also forces a memory barrier, so _innerAsyncResult
+ // is guaranteed to be non-null at this point.
+
+ if (_timer != null)
+ {
+ _timer.Dispose();
+ }
+ _timer = null;
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Async/AsyncUtil.cs b/src/System.Web.Mvc/Async/AsyncUtil.cs
new file mode 100644
index 00000000..4c75be0b
--- /dev/null
+++ b/src/System.Web.Mvc/Async/AsyncUtil.cs
@@ -0,0 +1,31 @@
+using System.Threading;
+
+namespace System.Web.Mvc.Async
+{
+ internal static class AsyncUtil
+ {
+ public static AsyncCallback WrapCallbackForSynchronizedExecution(AsyncCallback callback, SynchronizationContext syncContext)
+ {
+ if (callback == null || syncContext == null)
+ {
+ return callback;
+ }
+
+ AsyncCallback newCallback = delegate(IAsyncResult asyncResult)
+ {
+ if (asyncResult.CompletedSynchronously)
+ {
+ callback(asyncResult);
+ }
+ else
+ {
+ // Only take the application lock if this request completed asynchronously,
+ // else we might end up in a deadlock situation.
+ syncContext.Sync(() => callback(asyncResult));
+ }
+ };
+
+ return newCallback;
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Async/AsyncVoid.cs b/src/System.Web.Mvc/Async/AsyncVoid.cs
new file mode 100644
index 00000000..697438cc
--- /dev/null
+++ b/src/System.Web.Mvc/Async/AsyncVoid.cs
@@ -0,0 +1,7 @@
+namespace System.Web.Mvc.Async
+{
+ // Dummy type used for passing something resembling 'void' to the async delegate functions
+ internal struct AsyncVoid
+ {
+ }
+}
diff --git a/src/System.Web.Mvc/Async/BeginInvokeDelegate.cs b/src/System.Web.Mvc/Async/BeginInvokeDelegate.cs
new file mode 100644
index 00000000..4f119f4a
--- /dev/null
+++ b/src/System.Web.Mvc/Async/BeginInvokeDelegate.cs
@@ -0,0 +1,4 @@
+namespace System.Web.Mvc.Async
+{
+ internal delegate IAsyncResult BeginInvokeDelegate(AsyncCallback callback, object state);
+}
diff --git a/src/System.Web.Mvc/Async/EndInvokeDelegate.cs b/src/System.Web.Mvc/Async/EndInvokeDelegate.cs
new file mode 100644
index 00000000..fd037258
--- /dev/null
+++ b/src/System.Web.Mvc/Async/EndInvokeDelegate.cs
@@ -0,0 +1,4 @@
+namespace System.Web.Mvc.Async
+{
+ internal delegate void EndInvokeDelegate(IAsyncResult asyncResult);
+}
diff --git a/src/System.Web.Mvc/Async/EndInvokeDelegate`1.cs b/src/System.Web.Mvc/Async/EndInvokeDelegate`1.cs
new file mode 100644
index 00000000..e2650680
--- /dev/null
+++ b/src/System.Web.Mvc/Async/EndInvokeDelegate`1.cs
@@ -0,0 +1,4 @@
+namespace System.Web.Mvc.Async
+{
+ internal delegate TResult EndInvokeDelegate<TResult>(IAsyncResult asyncResult);
+}
diff --git a/src/System.Web.Mvc/Async/IAsyncActionInvoker.cs b/src/System.Web.Mvc/Async/IAsyncActionInvoker.cs
new file mode 100644
index 00000000..c9ee72c3
--- /dev/null
+++ b/src/System.Web.Mvc/Async/IAsyncActionInvoker.cs
@@ -0,0 +1,8 @@
+namespace System.Web.Mvc.Async
+{
+ public interface IAsyncActionInvoker : IActionInvoker
+ {
+ IAsyncResult BeginInvokeAction(ControllerContext controllerContext, string actionName, AsyncCallback callback, object state);
+ bool EndInvokeAction(IAsyncResult asyncResult);
+ }
+}
diff --git a/src/System.Web.Mvc/Async/IAsyncController.cs b/src/System.Web.Mvc/Async/IAsyncController.cs
new file mode 100644
index 00000000..ce46fce4
--- /dev/null
+++ b/src/System.Web.Mvc/Async/IAsyncController.cs
@@ -0,0 +1,10 @@
+using System.Web.Routing;
+
+namespace System.Web.Mvc.Async
+{
+ public interface IAsyncController : IController
+ {
+ IAsyncResult BeginExecute(RequestContext requestContext, AsyncCallback callback, object state);
+ void EndExecute(IAsyncResult asyncResult);
+ }
+}
diff --git a/src/System.Web.Mvc/Async/IAsyncManagerContainer.cs b/src/System.Web.Mvc/Async/IAsyncManagerContainer.cs
new file mode 100644
index 00000000..9e53fd12
--- /dev/null
+++ b/src/System.Web.Mvc/Async/IAsyncManagerContainer.cs
@@ -0,0 +1,7 @@
+namespace System.Web.Mvc.Async
+{
+ public interface IAsyncManagerContainer
+ {
+ AsyncManager AsyncManager { get; }
+ }
+}
diff --git a/src/System.Web.Mvc/Async/OperationCounter.cs b/src/System.Web.Mvc/Async/OperationCounter.cs
new file mode 100644
index 00000000..9dbb63b5
--- /dev/null
+++ b/src/System.Web.Mvc/Async/OperationCounter.cs
@@ -0,0 +1,56 @@
+using System.Threading;
+
+namespace System.Web.Mvc.Async
+{
+ public sealed class OperationCounter
+ {
+ private int _count;
+
+ public event EventHandler Completed;
+
+ public int Count
+ {
+ get { return Thread.VolatileRead(ref _count); }
+ }
+
+ private int AddAndExecuteCallbackIfCompleted(int value)
+ {
+ int newCount = Interlocked.Add(ref _count, value);
+ if (newCount == 0)
+ {
+ OnCompleted();
+ }
+
+ return newCount;
+ }
+
+ public int Decrement()
+ {
+ return AddAndExecuteCallbackIfCompleted(-1);
+ }
+
+ public int Decrement(int value)
+ {
+ return AddAndExecuteCallbackIfCompleted(-value);
+ }
+
+ public int Increment()
+ {
+ return AddAndExecuteCallbackIfCompleted(1);
+ }
+
+ public int Increment(int value)
+ {
+ return AddAndExecuteCallbackIfCompleted(value);
+ }
+
+ private void OnCompleted()
+ {
+ EventHandler handler = Completed;
+ if (handler != null)
+ {
+ handler(this, EventArgs.Empty);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Async/ReflectedAsyncActionDescriptor.cs b/src/System.Web.Mvc/Async/ReflectedAsyncActionDescriptor.cs
new file mode 100644
index 00000000..b1380e84
--- /dev/null
+++ b/src/System.Web.Mvc/Async/ReflectedAsyncActionDescriptor.cs
@@ -0,0 +1,188 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Threading;
+
+namespace System.Web.Mvc.Async
+{
+ public class ReflectedAsyncActionDescriptor : AsyncActionDescriptor
+ {
+ private readonly object _executeTag = new object();
+
+ private readonly string _actionName;
+ private readonly ControllerDescriptor _controllerDescriptor;
+ private readonly Lazy<string> _uniqueId;
+ private ParameterDescriptor[] _parametersCache;
+
+ public ReflectedAsyncActionDescriptor(MethodInfo asyncMethodInfo, MethodInfo completedMethodInfo, string actionName, ControllerDescriptor controllerDescriptor)
+ : this(asyncMethodInfo, completedMethodInfo, actionName, controllerDescriptor, true /* validateMethods */)
+ {
+ }
+
+ internal ReflectedAsyncActionDescriptor(MethodInfo asyncMethodInfo, MethodInfo completedMethodInfo, string actionName, ControllerDescriptor controllerDescriptor, bool validateMethods)
+ {
+ if (asyncMethodInfo == null)
+ {
+ throw new ArgumentNullException("asyncMethodInfo");
+ }
+ if (completedMethodInfo == null)
+ {
+ throw new ArgumentNullException("completedMethodInfo");
+ }
+ if (String.IsNullOrEmpty(actionName))
+ {
+ throw Error.ParameterCannotBeNullOrEmpty("actionName");
+ }
+ if (controllerDescriptor == null)
+ {
+ throw new ArgumentNullException("controllerDescriptor");
+ }
+
+ if (validateMethods)
+ {
+ string asyncFailedMessage = VerifyActionMethodIsCallable(asyncMethodInfo);
+ if (asyncFailedMessage != null)
+ {
+ throw new ArgumentException(asyncFailedMessage, "asyncMethodInfo");
+ }
+
+ string completedFailedMessage = VerifyActionMethodIsCallable(completedMethodInfo);
+ if (completedFailedMessage != null)
+ {
+ throw new ArgumentException(completedFailedMessage, "completedMethodInfo");
+ }
+ }
+
+ AsyncMethodInfo = asyncMethodInfo;
+ CompletedMethodInfo = completedMethodInfo;
+ _actionName = actionName;
+ _controllerDescriptor = controllerDescriptor;
+ _uniqueId = new Lazy<string>(CreateUniqueId);
+ }
+
+ public override string ActionName
+ {
+ get { return _actionName; }
+ }
+
+ public MethodInfo AsyncMethodInfo { get; private set; }
+
+ public MethodInfo CompletedMethodInfo { get; private set; }
+
+ public override ControllerDescriptor ControllerDescriptor
+ {
+ get { return _controllerDescriptor; }
+ }
+
+ public override string UniqueId
+ {
+ get { return _uniqueId.Value; }
+ }
+
+ public override IAsyncResult BeginExecute(ControllerContext controllerContext, IDictionary<string, object> parameters, AsyncCallback callback, object state)
+ {
+ if (controllerContext == null)
+ {
+ throw new ArgumentNullException("controllerContext");
+ }
+ if (parameters == null)
+ {
+ throw new ArgumentNullException("parameters");
+ }
+
+ AsyncManager asyncManager = GetAsyncManager(controllerContext.Controller);
+
+ BeginInvokeDelegate beginDelegate = delegate(AsyncCallback asyncCallback, object asyncState)
+ {
+ // call the XxxAsync() method
+ ParameterInfo[] parameterInfos = AsyncMethodInfo.GetParameters();
+ var rawParameterValues = from parameterInfo in parameterInfos
+ select ExtractParameterFromDictionary(parameterInfo, parameters, AsyncMethodInfo);
+ object[] parametersArray = rawParameterValues.ToArray();
+
+ TriggerListener listener = new TriggerListener();
+ SimpleAsyncResult asyncResult = new SimpleAsyncResult(asyncState);
+
+ // hook the Finished event to notify us upon completion
+ Trigger finishTrigger = listener.CreateTrigger();
+ asyncManager.Finished += delegate
+ {
+ finishTrigger.Fire();
+ };
+ asyncManager.OutstandingOperations.Increment();
+
+ // to simplify the logic, force the rest of the pipeline to execute in an asynchronous callback
+ listener.SetContinuation(() => ThreadPool.QueueUserWorkItem(_ => asyncResult.MarkCompleted(false /* completedSynchronously */, asyncCallback)));
+
+ // the inner operation might complete synchronously, so all setup work has to be done before this point
+ ActionMethodDispatcher dispatcher = DispatcherCache.GetDispatcher(AsyncMethodInfo);
+ dispatcher.Execute(controllerContext.Controller, parametersArray); // ignore return value from this method
+
+ // now that the XxxAsync() method has completed, kick off any pending operations
+ asyncManager.OutstandingOperations.Decrement();
+ listener.Activate();
+ return asyncResult;
+ };
+
+ EndInvokeDelegate<object> endDelegate = delegate(IAsyncResult asyncResult)
+ {
+ // call the XxxCompleted() method
+ ParameterInfo[] completionParametersInfos = CompletedMethodInfo.GetParameters();
+ var rawCompletionParameterValues = from parameterInfo in completionParametersInfos
+ select ExtractParameterOrDefaultFromDictionary(parameterInfo, asyncManager.Parameters);
+ object[] completionParametersArray = rawCompletionParameterValues.ToArray();
+
+ ActionMethodDispatcher dispatcher = DispatcherCache.GetDispatcher(CompletedMethodInfo);
+ object actionReturnValue = dispatcher.Execute(controllerContext.Controller, completionParametersArray);
+ return actionReturnValue;
+ };
+
+ return AsyncResultWrapper.Begin(callback, state, beginDelegate, endDelegate, _executeTag, asyncManager.Timeout);
+ }
+
+ private string CreateUniqueId()
+ {
+ return base.UniqueId + DescriptorUtil.CreateUniqueId(AsyncMethodInfo, CompletedMethodInfo);
+ }
+
+ public override object EndExecute(IAsyncResult asyncResult)
+ {
+ return AsyncResultWrapper.End<object>(asyncResult, _executeTag);
+ }
+
+ public override object[] GetCustomAttributes(bool inherit)
+ {
+ return ActionDescriptorHelper.GetCustomAttributes(AsyncMethodInfo, inherit);
+ }
+
+ public override object[] GetCustomAttributes(Type attributeType, bool inherit)
+ {
+ return ActionDescriptorHelper.GetCustomAttributes(AsyncMethodInfo, attributeType, inherit);
+ }
+
+ public override IEnumerable<FilterAttribute> GetFilterAttributes(bool useCache)
+ {
+ if (useCache && GetType() == typeof(ReflectedAsyncActionDescriptor))
+ {
+ // Do not look at cache in types derived from this type because they might incorrectly implement GetCustomAttributes
+ return ReflectedAttributeCache.GetMethodFilterAttributes(AsyncMethodInfo);
+ }
+ return base.GetFilterAttributes(useCache);
+ }
+
+ public override ParameterDescriptor[] GetParameters()
+ {
+ return ActionDescriptorHelper.GetParameters(this, AsyncMethodInfo, ref _parametersCache);
+ }
+
+ public override ICollection<ActionSelector> GetSelectors()
+ {
+ return ActionDescriptorHelper.GetSelectors(AsyncMethodInfo);
+ }
+
+ public override bool IsDefined(Type attributeType, bool inherit)
+ {
+ return ActionDescriptorHelper.IsDefined(AsyncMethodInfo, attributeType, inherit);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Async/ReflectedAsyncControllerDescriptor.cs b/src/System.Web.Mvc/Async/ReflectedAsyncControllerDescriptor.cs
new file mode 100644
index 00000000..de60cdbf
--- /dev/null
+++ b/src/System.Web.Mvc/Async/ReflectedAsyncControllerDescriptor.cs
@@ -0,0 +1,104 @@
+using System.Collections.Generic;
+
+namespace System.Web.Mvc.Async
+{
+ public class ReflectedAsyncControllerDescriptor : ControllerDescriptor
+ {
+ private static readonly ActionDescriptor[] _emptyCanonicalActions = new ActionDescriptor[0];
+
+ private readonly Type _controllerType;
+ private readonly AsyncActionMethodSelector _selector;
+
+ public ReflectedAsyncControllerDescriptor(Type controllerType)
+ {
+ if (controllerType == null)
+ {
+ throw new ArgumentNullException("controllerType");
+ }
+
+ _controllerType = controllerType;
+ bool allowLegacyAsyncActions = AllowLegacyAsyncActions(_controllerType);
+ _selector = new AsyncActionMethodSelector(_controllerType, allowLegacyAsyncActions);
+ }
+
+ public sealed override Type ControllerType
+ {
+ get { return _controllerType; }
+ }
+
+ /// <summary>
+ /// Determines if we should bind "Foo" to FooAsync/FooCompleted pattern.
+ /// </summary>
+ /// <param name="controllerType"></param>
+ /// <returns></returns>
+ private static bool AllowLegacyAsyncActions(Type controllerType)
+ {
+ if (typeof(AsyncController).IsAssignableFrom(controllerType))
+ {
+ return true;
+ }
+ if (typeof(Controller).IsAssignableFrom(controllerType))
+ {
+ // for backwards compat. Controller now supports IAsyncController,
+ // but still use synchronous bindings patterns.
+ return false;
+ }
+ if (!typeof(IAsyncController).IsAssignableFrom(controllerType))
+ {
+ return false;
+ }
+ return true;
+ }
+
+ public override ActionDescriptor FindAction(ControllerContext controllerContext, string actionName)
+ {
+ if (controllerContext == null)
+ {
+ throw new ArgumentNullException("controllerContext");
+ }
+ if (String.IsNullOrEmpty(actionName))
+ {
+ throw Error.ParameterCannotBeNullOrEmpty("actionName");
+ }
+
+ ActionDescriptorCreator creator = _selector.FindAction(controllerContext, actionName);
+ if (creator == null)
+ {
+ return null;
+ }
+
+ return creator(actionName, this);
+ }
+
+ public override ActionDescriptor[] GetCanonicalActions()
+ {
+ // everything is looked up dymanically, so there are no 'canonical' actions
+ return _emptyCanonicalActions;
+ }
+
+ public override object[] GetCustomAttributes(bool inherit)
+ {
+ return ControllerType.GetCustomAttributes(inherit);
+ }
+
+ public override object[] GetCustomAttributes(Type attributeType, bool inherit)
+ {
+ return ControllerType.GetCustomAttributes(attributeType, inherit);
+ }
+
+ public override IEnumerable<FilterAttribute> GetFilterAttributes(bool useCache)
+ {
+ if (useCache && GetType() == typeof(ReflectedAsyncControllerDescriptor))
+ {
+ // Do not look at cache in types derived from this type because they might incorrectly implement GetCustomAttributes
+ return ReflectedAttributeCache.GetTypeFilterAttributes(ControllerType);
+ }
+ return base.GetFilterAttributes(useCache);
+ }
+
+ public override bool IsDefined(Type attributeType, bool inherit)
+ {
+ return ControllerType.IsDefined(attributeType, inherit);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Async/SimpleAsyncResult.cs b/src/System.Web.Mvc/Async/SimpleAsyncResult.cs
new file mode 100644
index 00000000..3e4df09a
--- /dev/null
+++ b/src/System.Web.Mvc/Async/SimpleAsyncResult.cs
@@ -0,0 +1,53 @@
+using System.Threading;
+
+namespace System.Web.Mvc.Async
+{
+ internal sealed class SimpleAsyncResult : IAsyncResult
+ {
+ private readonly object _asyncState;
+ private bool _completedSynchronously;
+ private volatile bool _isCompleted;
+
+ public SimpleAsyncResult(object asyncState)
+ {
+ _asyncState = asyncState;
+ }
+
+ public object AsyncState
+ {
+ get { return _asyncState; }
+ }
+
+ // ASP.NET IAsyncResult objects should never expose a WaitHandle due to potential deadlocking
+ public WaitHandle AsyncWaitHandle
+ {
+ get { return null; }
+ }
+
+ public bool CompletedSynchronously
+ {
+ get { return _completedSynchronously; }
+ }
+
+ public bool IsCompleted
+ {
+ get { return _isCompleted; }
+ }
+
+ // Proper order of execution:
+ // 1. Set the CompletedSynchronously property to the correct value
+ // 2. Set the IsCompleted flag
+ // 3. Execute the callback
+ // 4. Signal the WaitHandle (which we don't have)
+ public void MarkCompleted(bool completedSynchronously, AsyncCallback callback)
+ {
+ _completedSynchronously = completedSynchronously;
+ _isCompleted = true;
+
+ if (callback != null)
+ {
+ callback(this);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Async/SingleEntryGate.cs b/src/System.Web.Mvc/Async/SingleEntryGate.cs
new file mode 100644
index 00000000..746593b4
--- /dev/null
+++ b/src/System.Web.Mvc/Async/SingleEntryGate.cs
@@ -0,0 +1,20 @@
+using System.Threading;
+
+namespace System.Web.Mvc.Async
+{
+ // used to synchronize access to a single-use consumable resource
+ internal sealed class SingleEntryGate
+ {
+ private const int NotEntered = 0;
+ private const int Entered = 1;
+
+ private int _status;
+
+ // returns true if this is the first call to TryEnter(), false otherwise
+ public bool TryEnter()
+ {
+ int oldStatus = Interlocked.Exchange(ref _status, Entered);
+ return (oldStatus == NotEntered);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Async/SynchronizationContextUtil.cs b/src/System.Web.Mvc/Async/SynchronizationContextUtil.cs
new file mode 100644
index 00000000..f64a9f6b
--- /dev/null
+++ b/src/System.Web.Mvc/Async/SynchronizationContextUtil.cs
@@ -0,0 +1,52 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Threading;
+
+namespace System.Web.Mvc.Async
+{
+ internal static class SynchronizationContextUtil
+ {
+ public static SynchronizationContext GetSynchronizationContext()
+ {
+ // In a runtime environment, SynchronizationContext.Current will be set to an instance
+ // of AspNetSynchronizationContext. In a unit test environment, the Current property
+ // won't be set and we have to create one on the fly.
+ return SynchronizationContext.Current ?? new SynchronizationContext();
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "The exception is swallowed and immediately re-thrown")]
+ public static T Sync<T>(this SynchronizationContext syncContext, Func<T> func)
+ {
+ T theValue = default(T);
+ Exception thrownException = null;
+
+ syncContext.Send(o =>
+ {
+ try
+ {
+ theValue = func();
+ }
+ catch (Exception ex)
+ {
+ // by default, the AspNetSynchronizationContext type will swallow thrown exceptions,
+ // so we need to save and propagate them
+ thrownException = ex;
+ }
+ }, null);
+
+ if (thrownException != null)
+ {
+ throw Error.SynchronizationContextUtil_ExceptionThrown(thrownException);
+ }
+ return theValue;
+ }
+
+ public static void Sync(this SynchronizationContext syncContext, Action action)
+ {
+ Sync<AsyncVoid>(syncContext, () =>
+ {
+ action();
+ return default(AsyncVoid);
+ });
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Async/SynchronousOperationException.cs b/src/System.Web.Mvc/Async/SynchronousOperationException.cs
new file mode 100644
index 00000000..b7ac88e1
--- /dev/null
+++ b/src/System.Web.Mvc/Async/SynchronousOperationException.cs
@@ -0,0 +1,30 @@
+using System.Runtime.Serialization;
+
+namespace System.Web.Mvc.Async
+{
+ // This exception type is thrown by the SynchronizationContextUtil helper class since the AspNetSynchronizationContext
+ // type swallows exceptions. The inner exception contains the data the user cares about.
+
+ [Serializable]
+ public sealed class SynchronousOperationException : HttpException
+ {
+ public SynchronousOperationException()
+ {
+ }
+
+ private SynchronousOperationException(SerializationInfo info, StreamingContext context)
+ : base(info, context)
+ {
+ }
+
+ public SynchronousOperationException(string message)
+ : base(message)
+ {
+ }
+
+ public SynchronousOperationException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Async/TaskAsyncActionDescriptor.cs b/src/System.Web.Mvc/Async/TaskAsyncActionDescriptor.cs
new file mode 100644
index 00000000..22e8e8b1
--- /dev/null
+++ b/src/System.Web.Mvc/Async/TaskAsyncActionDescriptor.cs
@@ -0,0 +1,264 @@
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc.Async
+{
+ /// <summary>
+ /// When an action method returns either Task or Task{T} the TaskAsyncActionDescriptor provides information about the action.
+ /// </summary>
+ public class TaskAsyncActionDescriptor : AsyncActionDescriptor
+ {
+ /// <summary>
+ /// dictionary to hold methods that can read Task{T}.Result
+ /// </summary>
+ private static readonly ConcurrentDictionary<Type, Func<object, object>> _taskValueExtractors = new ConcurrentDictionary<Type, Func<object, object>>();
+ private readonly string _actionName;
+ private readonly ControllerDescriptor _controllerDescriptor;
+ private readonly Lazy<string> _uniqueId;
+ private ParameterDescriptor[] _parametersCache;
+
+ public TaskAsyncActionDescriptor(MethodInfo taskMethodInfo, string actionName, ControllerDescriptor controllerDescriptor)
+ : this(taskMethodInfo, actionName, controllerDescriptor, validateMethod: true)
+ {
+ }
+
+ internal TaskAsyncActionDescriptor(MethodInfo taskMethodInfo, string actionName, ControllerDescriptor controllerDescriptor, bool validateMethod)
+ {
+ if (taskMethodInfo == null)
+ {
+ throw new ArgumentNullException("taskMethodInfo");
+ }
+ if (String.IsNullOrEmpty(actionName))
+ {
+ throw Error.ParameterCannotBeNullOrEmpty("actionName");
+ }
+ if (controllerDescriptor == null)
+ {
+ throw new ArgumentNullException("controllerDescriptor");
+ }
+
+ if (validateMethod)
+ {
+ string taskFailedMessage = VerifyActionMethodIsCallable(taskMethodInfo);
+ if (taskFailedMessage != null)
+ {
+ throw new ArgumentException(taskFailedMessage, "taskMethodInfo");
+ }
+ }
+
+ TaskMethodInfo = taskMethodInfo;
+ _actionName = actionName;
+ _controllerDescriptor = controllerDescriptor;
+ _uniqueId = new Lazy<string>(CreateUniqueId);
+ }
+
+ public override string ActionName
+ {
+ get { return _actionName; }
+ }
+
+ public MethodInfo TaskMethodInfo { get; private set; }
+
+ public override ControllerDescriptor ControllerDescriptor
+ {
+ get { return _controllerDescriptor; }
+ }
+
+ public override string UniqueId
+ {
+ get { return _uniqueId.Value; }
+ }
+
+ private string CreateUniqueId()
+ {
+ return base.UniqueId + DescriptorUtil.CreateUniqueId(TaskMethodInfo);
+ }
+
+ public override IAsyncResult BeginExecute(ControllerContext controllerContext, IDictionary<string, object> parameters, AsyncCallback callback, object state)
+ {
+ if (controllerContext == null)
+ {
+ throw new ArgumentNullException("controllerContext");
+ }
+ if (parameters == null)
+ {
+ throw new ArgumentNullException("parameters");
+ }
+
+ ParameterInfo[] parameterInfos = TaskMethodInfo.GetParameters();
+ var rawParameterValues = from parameterInfo in parameterInfos
+ select ExtractParameterFromDictionary(parameterInfo, parameters, TaskMethodInfo);
+ object[] parametersArray = rawParameterValues.ToArray();
+
+ CancellationTokenSource tokenSource = null;
+ bool disposedTimer = false;
+ Timer taskCancelledTimer = null;
+ bool taskCancelledTimerRequired = false;
+
+ int timeout = GetAsyncManager(controllerContext.Controller).Timeout;
+
+ for (int i = 0; i < parametersArray.Length; i++)
+ {
+ if (default(CancellationToken).Equals(parametersArray[i]))
+ {
+ tokenSource = new CancellationTokenSource();
+ parametersArray[i] = tokenSource.Token;
+
+ // If there is a timeout we will create a timer to cancel the task when the
+ // timeout expires.
+ taskCancelledTimerRequired = timeout > Timeout.Infinite;
+ break;
+ }
+ }
+
+ ActionMethodDispatcher dispatcher = DispatcherCache.GetDispatcher(TaskMethodInfo);
+
+ if (taskCancelledTimerRequired)
+ {
+ taskCancelledTimer = new Timer(_ =>
+ {
+ lock (tokenSource)
+ {
+ if (!disposedTimer)
+ {
+ tokenSource.Cancel();
+ }
+ }
+ },
+ state: null, dueTime: timeout, period: Timeout.Infinite);
+ }
+
+ Task taskUser = dispatcher.Execute(controllerContext.Controller, parametersArray) as Task;
+ Action cleanupAtEndExecute = () =>
+ {
+ // Cleanup code that's run in EndExecute, after we've waited on the task value.
+
+ if (taskCancelledTimer != null)
+ {
+ // Timer callback may still fire after Dispose is called.
+ taskCancelledTimer.Dispose();
+ }
+
+ if (tokenSource != null)
+ {
+ lock (tokenSource)
+ {
+ disposedTimer = true;
+ tokenSource.Dispose();
+ if (tokenSource.IsCancellationRequested)
+ {
+ // Give Timeout exceptions higher priority over other exceptions, mainly OperationCancelled exceptions
+ // that were signaled with out timeout token.
+ throw new TimeoutException();
+ }
+ }
+ }
+ };
+
+ TaskWrapperAsyncResult result = new TaskWrapperAsyncResult(taskUser, state, cleanupAtEndExecute);
+
+ // if user supplied a callback, invoke that when their task has finished running.
+ if (callback != null)
+ {
+ taskUser.Finally(() =>
+ {
+ callback(result);
+ });
+ }
+
+ return result;
+ }
+
+ public override object Execute(ControllerContext controllerContext, IDictionary<string, object> parameters)
+ {
+ string errorMessage = String.Format(CultureInfo.CurrentCulture, MvcResources.TaskAsyncActionDescriptor_CannotExecuteSynchronously,
+ ActionName);
+
+ throw new InvalidOperationException(errorMessage);
+ }
+
+ public override object EndExecute(IAsyncResult asyncResult)
+ {
+ TaskWrapperAsyncResult wrapperResult = (TaskWrapperAsyncResult)asyncResult;
+
+ // Throw an exception with the correct call stack
+ try
+ {
+ wrapperResult.Task.ThrowIfFaulted();
+ }
+ finally
+ {
+ if (wrapperResult.CleanupThunk != null)
+ {
+ wrapperResult.CleanupThunk();
+ }
+ }
+
+ // Extract the result of the task if there is a result
+ return _taskValueExtractors.GetOrAdd(TaskMethodInfo.ReturnType, CreateTaskValueExtractor)(wrapperResult.Task);
+ }
+
+ private static Func<object, object> CreateTaskValueExtractor(Type taskType)
+ {
+ // Task<T>?
+ if (taskType.IsGenericType && taskType.GetGenericTypeDefinition() == typeof(Task<>))
+ {
+ // lambda = arg => (object)(((Task<T>)arg).Result)
+ var arg = Expression.Parameter(typeof(object));
+ var castArg = Expression.Convert(arg, taskType);
+ var fieldAccess = Expression.Property(castArg, "Result");
+ var castResult = Expression.Convert(fieldAccess, typeof(object));
+ var lambda = Expression.Lambda<Func<object, object>>(castResult, arg);
+ return lambda.Compile();
+ }
+
+ // Any exceptions should be thrown before getting the task value so just return null.
+ return theTask =>
+ {
+ return null;
+ };
+ }
+
+ public override object[] GetCustomAttributes(bool inherit)
+ {
+ return ActionDescriptorHelper.GetCustomAttributes(TaskMethodInfo, inherit);
+ }
+
+ public override object[] GetCustomAttributes(Type attributeType, bool inherit)
+ {
+ return ActionDescriptorHelper.GetCustomAttributes(TaskMethodInfo, attributeType, inherit);
+ }
+
+ public override ParameterDescriptor[] GetParameters()
+ {
+ return ActionDescriptorHelper.GetParameters(this, TaskMethodInfo, ref _parametersCache);
+ }
+
+ public override ICollection<ActionSelector> GetSelectors()
+ {
+ return ActionDescriptorHelper.GetSelectors(TaskMethodInfo);
+ }
+
+ public override bool IsDefined(Type attributeType, bool inherit)
+ {
+ return ActionDescriptorHelper.IsDefined(TaskMethodInfo, attributeType, inherit);
+ }
+
+ public override IEnumerable<FilterAttribute> GetFilterAttributes(bool useCache)
+ {
+ if (useCache && GetType() == typeof(TaskAsyncActionDescriptor))
+ {
+ // Do not look at cache in types derived from this type because they might incorrectly implement GetCustomAttributes
+ return ReflectedAttributeCache.GetMethodFilterAttributes(TaskMethodInfo);
+ }
+ return base.GetFilterAttributes(useCache);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Async/TaskWrapperAsyncResult.cs b/src/System.Web.Mvc/Async/TaskWrapperAsyncResult.cs
new file mode 100644
index 00000000..a9d31a94
--- /dev/null
+++ b/src/System.Web.Mvc/Async/TaskWrapperAsyncResult.cs
@@ -0,0 +1,43 @@
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace System.Web.Mvc.Async
+{
+ /// <summary>
+ /// Wraps a <see cref="Task"/> class, optionally overriding the State object (since the Task Asynchronous Pattern doesn't normally use it).
+ /// Copied from System.Web.
+ /// </summary>
+ internal sealed class TaskWrapperAsyncResult : IAsyncResult
+ {
+ internal TaskWrapperAsyncResult(Task task, object asyncState, Action cleanupThunk = null)
+ {
+ Task = task;
+ AsyncState = asyncState;
+ CleanupThunk = cleanupThunk;
+ }
+
+ public object AsyncState { get; private set; }
+
+ public WaitHandle AsyncWaitHandle
+ {
+ get { return ((IAsyncResult)Task).AsyncWaitHandle; }
+ }
+
+ /// <summary>
+ /// Cleanup logic to run after Task is finished
+ /// </summary>
+ public Action CleanupThunk { get; private set; }
+
+ public bool CompletedSynchronously
+ {
+ get { return ((IAsyncResult)Task).CompletedSynchronously; }
+ }
+
+ public bool IsCompleted
+ {
+ get { return ((IAsyncResult)Task).IsCompleted; }
+ }
+
+ internal Task Task { get; private set; }
+ }
+}
diff --git a/src/System.Web.Mvc/Async/Trigger.cs b/src/System.Web.Mvc/Async/Trigger.cs
new file mode 100644
index 00000000..53efafbb
--- /dev/null
+++ b/src/System.Web.Mvc/Async/Trigger.cs
@@ -0,0 +1,20 @@
+namespace System.Web.Mvc.Async
+{
+ // Provides a trigger for the TriggerListener class.
+
+ internal sealed class Trigger
+ {
+ private readonly Action _fireAction;
+
+ // Constructor should only be called by TriggerListener.
+ internal Trigger(Action fireAction)
+ {
+ _fireAction = fireAction;
+ }
+
+ public void Fire()
+ {
+ _fireAction();
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Async/TriggerListener.cs b/src/System.Web.Mvc/Async/TriggerListener.cs
new file mode 100644
index 00000000..84b9a578
--- /dev/null
+++ b/src/System.Web.Mvc/Async/TriggerListener.cs
@@ -0,0 +1,65 @@
+using System.Threading;
+
+namespace System.Web.Mvc.Async
+{
+ // This class is used to wait for triggers and a continuation. When the continuation has been provded
+ // and all triggers have been fired, the continuation is called. Similar to WaitHandle.WaitAll().
+ // New instances of this type are initially in the inactive state; activation is enabled by a call
+ // to Activate().
+
+ // This class is thread-safe.
+
+ internal sealed class TriggerListener
+ {
+ private readonly Trigger _activateTrigger;
+ private readonly SingleEntryGate _continuationFiredGate = new SingleEntryGate();
+ private readonly Trigger _setContinuationTrigger;
+ private volatile Action _continuation;
+ private int _outstandingTriggers;
+
+ public TriggerListener()
+ {
+ _activateTrigger = CreateTrigger();
+ _setContinuationTrigger = CreateTrigger();
+ }
+
+ public void Activate()
+ {
+ _activateTrigger.Fire();
+ }
+
+ public Trigger CreateTrigger()
+ {
+ Interlocked.Increment(ref _outstandingTriggers);
+
+ SingleEntryGate triggerFiredGate = new SingleEntryGate();
+ return new Trigger(() =>
+ {
+ if (triggerFiredGate.TryEnter())
+ {
+ HandleTriggerFired();
+ }
+ });
+ }
+
+ private void HandleTriggerFired()
+ {
+ if (Interlocked.Decrement(ref _outstandingTriggers) == 0)
+ {
+ if (_continuationFiredGate.TryEnter())
+ {
+ _continuation();
+ }
+ }
+ }
+
+ public void SetContinuation(Action continuation)
+ {
+ if (continuation != null)
+ {
+ _continuation = continuation;
+ _setContinuationTrigger.Fire();
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/AsyncController.cs b/src/System.Web.Mvc/AsyncController.cs
new file mode 100644
index 00000000..91e6c165
--- /dev/null
+++ b/src/System.Web.Mvc/AsyncController.cs
@@ -0,0 +1,10 @@
+namespace System.Web.Mvc
+{
+ // Controller now supports asynchronous operations.
+ // This class only exists
+ // a) for backwards compat for callers that derive from it,
+ // b) ActionMethodSelector can detect it to bind to ActionAsync/ActionCompleted patterns.
+ public abstract class AsyncController : Controller
+ {
+ }
+}
diff --git a/src/System.Web.Mvc/AsyncTimeoutAttribute.cs b/src/System.Web.Mvc/AsyncTimeoutAttribute.cs
new file mode 100644
index 00000000..4933269b
--- /dev/null
+++ b/src/System.Web.Mvc/AsyncTimeoutAttribute.cs
@@ -0,0 +1,41 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Web.Mvc.Async;
+
+namespace System.Web.Mvc
+{
+ [SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", Justification = "Unsealed so that subclassed types can set properties in the default constructor.")]
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
+ public class AsyncTimeoutAttribute : ActionFilterAttribute
+ {
+ // duration is specified in milliseconds
+ public AsyncTimeoutAttribute(int duration)
+ {
+ if (duration < -1)
+ {
+ throw Error.AsyncCommon_InvalidTimeout("duration");
+ }
+
+ Duration = duration;
+ }
+
+ public int Duration { get; private set; }
+
+ public override void OnActionExecuting(ActionExecutingContext filterContext)
+ {
+ if (filterContext == null)
+ {
+ throw new ArgumentNullException("filterContext");
+ }
+
+ IAsyncManagerContainer container = filterContext.Controller as IAsyncManagerContainer;
+ if (container == null)
+ {
+ throw Error.AsyncCommon_ControllerMustImplementIAsyncManagerContainer(filterContext.Controller.GetType());
+ }
+
+ container.AsyncManager.Timeout = Duration;
+
+ base.OnActionExecuting(filterContext);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/AuthorizationContext.cs b/src/System.Web.Mvc/AuthorizationContext.cs
new file mode 100644
index 00000000..a6546b5b
--- /dev/null
+++ b/src/System.Web.Mvc/AuthorizationContext.cs
@@ -0,0 +1,34 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Web.Mvc
+{
+ public class AuthorizationContext : ControllerContext
+ {
+ // parameterless constructor used for mocking
+ public AuthorizationContext()
+ {
+ }
+
+ [Obsolete("The recommended alternative is the constructor AuthorizationContext(ControllerContext controllerContext, ActionDescriptor actionDescriptor).")]
+ public AuthorizationContext(ControllerContext controllerContext)
+ : base(controllerContext)
+ {
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "The virtual property setters are only to support mocking frameworks, in which case this constructor shouldn't be called anyway.")]
+ public AuthorizationContext(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
+ : base(controllerContext)
+ {
+ if (actionDescriptor == null)
+ {
+ throw new ArgumentNullException("actionDescriptor");
+ }
+
+ ActionDescriptor = actionDescriptor;
+ }
+
+ public virtual ActionDescriptor ActionDescriptor { get; set; }
+
+ public ActionResult Result { get; set; }
+ }
+}
diff --git a/src/System.Web.Mvc/AuthorizeAttribute.cs b/src/System.Web.Mvc/AuthorizeAttribute.cs
new file mode 100644
index 00000000..434b94e1
--- /dev/null
+++ b/src/System.Web.Mvc/AuthorizeAttribute.cs
@@ -0,0 +1,152 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Security.Principal;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc
+{
+ [SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", Justification = "Unsealed so that subclassed types can set properties in the default constructor or override our behavior.")]
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
+ public class AuthorizeAttribute : FilterAttribute, IAuthorizationFilter
+ {
+ private readonly object _typeId = new object();
+
+ private string _roles;
+ private string[] _rolesSplit = new string[0];
+ private string _users;
+ private string[] _usersSplit = new string[0];
+
+ public string Roles
+ {
+ get { return _roles ?? String.Empty; }
+ set
+ {
+ _roles = value;
+ _rolesSplit = SplitString(value);
+ }
+ }
+
+ public override object TypeId
+ {
+ get { return _typeId; }
+ }
+
+ public string Users
+ {
+ get { return _users ?? String.Empty; }
+ set
+ {
+ _users = value;
+ _usersSplit = SplitString(value);
+ }
+ }
+
+ // This method must be thread-safe since it is called by the thread-safe OnCacheAuthorization() method.
+ protected virtual bool AuthorizeCore(HttpContextBase httpContext)
+ {
+ if (httpContext == null)
+ {
+ throw new ArgumentNullException("httpContext");
+ }
+
+ IPrincipal user = httpContext.User;
+ if (!user.Identity.IsAuthenticated)
+ {
+ return false;
+ }
+
+ if (_usersSplit.Length > 0 && !_usersSplit.Contains(user.Identity.Name, StringComparer.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ if (_rolesSplit.Length > 0 && !_rolesSplit.Any(user.IsInRole))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ private void CacheValidateHandler(HttpContext context, object data, ref HttpValidationStatus validationStatus)
+ {
+ validationStatus = OnCacheAuthorization(new HttpContextWrapper(context));
+ }
+
+ public virtual void OnAuthorization(AuthorizationContext filterContext)
+ {
+ if (filterContext == null)
+ {
+ throw new ArgumentNullException("filterContext");
+ }
+
+ if (OutputCacheAttribute.IsChildActionCacheActive(filterContext))
+ {
+ // If a child action cache block is active, we need to fail immediately, even if authorization
+ // would have succeeded. The reason is that there's no way to hook a callback to rerun
+ // authorization before the fragment is served from the cache, so we can't guarantee that this
+ // filter will be re-run on subsequent requests.
+ throw new InvalidOperationException(MvcResources.AuthorizeAttribute_CannotUseWithinChildActionCache);
+ }
+
+ bool skipAuthorization = filterContext.ActionDescriptor.IsDefined(typeof(AllowAnonymousAttribute), inherit: true)
+ || filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(AllowAnonymousAttribute), inherit: true);
+
+ if (skipAuthorization)
+ {
+ return;
+ }
+
+ if (AuthorizeCore(filterContext.HttpContext))
+ {
+ // ** IMPORTANT **
+ // Since we're performing authorization at the action level, the authorization code runs
+ // after the output caching module. In the worst case this could allow an authorized user
+ // to cause the page to be cached, then an unauthorized user would later be served the
+ // cached page. We work around this by telling proxies not to cache the sensitive page,
+ // then we hook our custom authorization code into the caching mechanism so that we have
+ // the final say on whether a page should be served from the cache.
+
+ HttpCachePolicyBase cachePolicy = filterContext.HttpContext.Response.Cache;
+ cachePolicy.SetProxyMaxAge(new TimeSpan(0));
+ cachePolicy.AddValidationCallback(CacheValidateHandler, null /* data */);
+ }
+ else
+ {
+ HandleUnauthorizedRequest(filterContext);
+ }
+ }
+
+ protected virtual void HandleUnauthorizedRequest(AuthorizationContext filterContext)
+ {
+ // Returns HTTP 401 - see comment in HttpUnauthorizedResult.cs.
+ filterContext.Result = new HttpUnauthorizedResult();
+ }
+
+ // This method must be thread-safe since it is called by the caching module.
+ protected virtual HttpValidationStatus OnCacheAuthorization(HttpContextBase httpContext)
+ {
+ if (httpContext == null)
+ {
+ throw new ArgumentNullException("httpContext");
+ }
+
+ bool isAuthorized = AuthorizeCore(httpContext);
+ return (isAuthorized) ? HttpValidationStatus.Valid : HttpValidationStatus.IgnoreThisRequest;
+ }
+
+ internal static string[] SplitString(string original)
+ {
+ if (String.IsNullOrEmpty(original))
+ {
+ return new string[0];
+ }
+
+ var split = from piece in original.Split(',')
+ let trimmed = piece.Trim()
+ where !String.IsNullOrEmpty(trimmed)
+ select trimmed;
+ return split.ToArray();
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/BindAttribute.cs b/src/System.Web.Mvc/BindAttribute.cs
new file mode 100644
index 00000000..55165e42
--- /dev/null
+++ b/src/System.Web.Mvc/BindAttribute.cs
@@ -0,0 +1,50 @@
+using System.Linq;
+
+namespace System.Web.Mvc
+{
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
+ public sealed class BindAttribute : Attribute
+ {
+ private string _exclude;
+ private string[] _excludeSplit = new string[0];
+ private string _include;
+ private string[] _includeSplit = new string[0];
+
+ public string Exclude
+ {
+ get { return _exclude ?? String.Empty; }
+ set
+ {
+ _exclude = value;
+ _excludeSplit = AuthorizeAttribute.SplitString(value);
+ }
+ }
+
+ public string Include
+ {
+ get { return _include ?? String.Empty; }
+ set
+ {
+ _include = value;
+ _includeSplit = AuthorizeAttribute.SplitString(value);
+ }
+ }
+
+ public string Prefix { get; set; }
+
+ internal static bool IsPropertyAllowed(string propertyName, string[] includeProperties, string[] excludeProperties)
+ {
+ // We allow a property to be bound if its both in the include list AND not in the exclude list.
+ // An empty include list implies all properties are allowed.
+ // An empty exclude list implies no properties are disallowed.
+ bool includeProperty = (includeProperties == null) || (includeProperties.Length == 0) || includeProperties.Contains(propertyName, StringComparer.OrdinalIgnoreCase);
+ bool excludeProperty = (excludeProperties != null) && excludeProperties.Contains(propertyName, StringComparer.OrdinalIgnoreCase);
+ return includeProperty && !excludeProperty;
+ }
+
+ public bool IsPropertyAllowed(string propertyName)
+ {
+ return IsPropertyAllowed(propertyName, _includeSplit, _excludeSplit);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/BuildManagerCompiledView.cs b/src/System.Web.Mvc/BuildManagerCompiledView.cs
new file mode 100644
index 00000000..a7edef36
--- /dev/null
+++ b/src/System.Web.Mvc/BuildManagerCompiledView.cs
@@ -0,0 +1,85 @@
+using System.Globalization;
+using System.IO;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc
+{
+ public abstract class BuildManagerCompiledView : IView
+ {
+ internal IViewPageActivator ViewPageActivator;
+ private IBuildManager _buildManager;
+ private ControllerContext _controllerContext;
+
+ protected BuildManagerCompiledView(ControllerContext controllerContext, string viewPath)
+ : this(controllerContext, viewPath, null)
+ {
+ }
+
+ protected BuildManagerCompiledView(ControllerContext controllerContext, string viewPath, IViewPageActivator viewPageActivator)
+ : this(controllerContext, viewPath, viewPageActivator, null)
+ {
+ }
+
+ internal BuildManagerCompiledView(ControllerContext controllerContext, string viewPath, IViewPageActivator viewPageActivator, IDependencyResolver dependencyResolver)
+ {
+ if (controllerContext == null)
+ {
+ throw new ArgumentNullException("controllerContext");
+ }
+ if (String.IsNullOrEmpty(viewPath))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "viewPath");
+ }
+
+ _controllerContext = controllerContext;
+
+ ViewPath = viewPath;
+
+ ViewPageActivator = viewPageActivator ?? new BuildManagerViewEngine.DefaultViewPageActivator(dependencyResolver);
+ }
+
+ internal IBuildManager BuildManager
+ {
+ get
+ {
+ if (_buildManager == null)
+ {
+ _buildManager = new BuildManagerWrapper();
+ }
+ return _buildManager;
+ }
+ set { _buildManager = value; }
+ }
+
+ public string ViewPath { get; protected set; }
+
+ public void Render(ViewContext viewContext, TextWriter writer)
+ {
+ if (viewContext == null)
+ {
+ throw new ArgumentNullException("viewContext");
+ }
+
+ object instance = null;
+
+ Type type = BuildManager.GetCompiledType(ViewPath);
+ if (type != null)
+ {
+ instance = ViewPageActivator.Create(_controllerContext, type);
+ }
+
+ if (instance == null)
+ {
+ throw new InvalidOperationException(
+ String.Format(
+ CultureInfo.CurrentCulture,
+ MvcResources.CshtmlView_ViewCouldNotBeCreated,
+ ViewPath));
+ }
+
+ RenderView(viewContext, writer, instance);
+ }
+
+ protected abstract void RenderView(ViewContext viewContext, TextWriter writer, object instance);
+ }
+}
diff --git a/src/System.Web.Mvc/BuildManagerViewEngine.cs b/src/System.Web.Mvc/BuildManagerViewEngine.cs
new file mode 100644
index 00000000..cbc4efc3
--- /dev/null
+++ b/src/System.Web.Mvc/BuildManagerViewEngine.cs
@@ -0,0 +1,92 @@
+namespace System.Web.Mvc
+{
+ public abstract class BuildManagerViewEngine : VirtualPathProviderViewEngine
+ {
+ private IBuildManager _buildManager;
+ private IViewPageActivator _viewPageActivator;
+ private IResolver<IViewPageActivator> _activatorResolver;
+
+ protected BuildManagerViewEngine()
+ : this(null, null, null)
+ {
+ }
+
+ protected BuildManagerViewEngine(IViewPageActivator viewPageActivator)
+ : this(viewPageActivator, null, null)
+ {
+ }
+
+ internal BuildManagerViewEngine(IViewPageActivator viewPageActivator, IResolver<IViewPageActivator> activatorResolver, IDependencyResolver dependencyResolver)
+ {
+ if (viewPageActivator != null)
+ {
+ _viewPageActivator = viewPageActivator;
+ }
+ else
+ {
+ _activatorResolver = activatorResolver ?? new SingleServiceResolver<IViewPageActivator>(
+ () => null,
+ new DefaultViewPageActivator(dependencyResolver),
+ "BuildManagerViewEngine constructor");
+ }
+ }
+
+ internal IBuildManager BuildManager
+ {
+ get
+ {
+ if (_buildManager == null)
+ {
+ _buildManager = new BuildManagerWrapper();
+ }
+ return _buildManager;
+ }
+ set { _buildManager = value; }
+ }
+
+ protected IViewPageActivator ViewPageActivator
+ {
+ get
+ {
+ if (_viewPageActivator != null)
+ {
+ return _viewPageActivator;
+ }
+ _viewPageActivator = _activatorResolver.Current;
+ return _viewPageActivator;
+ }
+ }
+
+ protected override bool FileExists(ControllerContext controllerContext, string virtualPath)
+ {
+ return BuildManager.FileExists(virtualPath);
+ }
+
+ internal class DefaultViewPageActivator : IViewPageActivator
+ {
+ private Func<IDependencyResolver> _resolverThunk;
+
+ public DefaultViewPageActivator()
+ : this(null)
+ {
+ }
+
+ public DefaultViewPageActivator(IDependencyResolver resolver)
+ {
+ if (resolver == null)
+ {
+ _resolverThunk = () => DependencyResolver.Current;
+ }
+ else
+ {
+ _resolverThunk = () => resolver;
+ }
+ }
+
+ public object Create(ControllerContext controllerContext, Type type)
+ {
+ return _resolverThunk().GetService(type) ?? Activator.CreateInstance(type);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/BuildManagerWrapper.cs b/src/System.Web.Mvc/BuildManagerWrapper.cs
new file mode 100644
index 00000000..13f2e94c
--- /dev/null
+++ b/src/System.Web.Mvc/BuildManagerWrapper.cs
@@ -0,0 +1,34 @@
+using System.Collections;
+using System.IO;
+using System.Web.Compilation;
+
+namespace System.Web.Mvc
+{
+ internal sealed class BuildManagerWrapper : IBuildManager
+ {
+ bool IBuildManager.FileExists(string virtualPath)
+ {
+ return BuildManager.GetObjectFactory(virtualPath, false) != null;
+ }
+
+ Type IBuildManager.GetCompiledType(string virtualPath)
+ {
+ return BuildManager.GetCompiledType(virtualPath);
+ }
+
+ ICollection IBuildManager.GetReferencedAssemblies()
+ {
+ return BuildManager.GetReferencedAssemblies();
+ }
+
+ Stream IBuildManager.ReadCachedFile(string fileName)
+ {
+ return BuildManager.ReadCachedFile(fileName);
+ }
+
+ Stream IBuildManager.CreateCachedFile(string fileName)
+ {
+ return BuildManager.CreateCachedFile(fileName);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ByteArrayModelBinder.cs b/src/System.Web.Mvc/ByteArrayModelBinder.cs
new file mode 100644
index 00000000..df14ddb0
--- /dev/null
+++ b/src/System.Web.Mvc/ByteArrayModelBinder.cs
@@ -0,0 +1,34 @@
+namespace System.Web.Mvc
+{
+ public class ByteArrayModelBinder : IModelBinder
+ {
+ public virtual object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
+ {
+ if (bindingContext == null)
+ {
+ throw new ArgumentNullException("bindingContext");
+ }
+
+ ValueProviderResult valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
+
+ // case 1: there was no <input ... /> element containing this data
+ if (valueResult == null)
+ {
+ return null;
+ }
+
+ string value = valueResult.AttemptedValue;
+
+ // case 2: there was an <input ... /> element but it was left blank
+ if (String.IsNullOrEmpty(value))
+ {
+ return null;
+ }
+
+ // Future proofing. If the byte array is actually an instance of System.Data.Linq.Binary
+ // then we need to remove these quotes put in place by the ToString() method.
+ string realValue = value.Replace("\"", String.Empty);
+ return Convert.FromBase64String(realValue);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/CachedAssociatedMetadataProvider`1.cs b/src/System.Web.Mvc/CachedAssociatedMetadataProvider`1.cs
new file mode 100644
index 00000000..b0a00cb2
--- /dev/null
+++ b/src/System.Web.Mvc/CachedAssociatedMetadataProvider`1.cs
@@ -0,0 +1,94 @@
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Runtime.Caching;
+
+namespace System.Web.Mvc
+{
+ public abstract class CachedAssociatedMetadataProvider<TModelMetadata> : AssociatedMetadataProvider
+ where TModelMetadata : ModelMetadata
+ {
+ private static ConcurrentDictionary<Type, string> _typeIds = new ConcurrentDictionary<Type, string>();
+ private string _cacheKeyPrefix;
+ private CacheItemPolicy _cacheItemPolicy = new CacheItemPolicy { SlidingExpiration = TimeSpan.FromMinutes(20) };
+ private ObjectCache _prototypeCache;
+
+ protected internal CacheItemPolicy CacheItemPolicy
+ {
+ get { return _cacheItemPolicy; }
+ set { _cacheItemPolicy = value; }
+ }
+
+ protected string CacheKeyPrefix
+ {
+ get
+ {
+ if (_cacheKeyPrefix == null)
+ {
+ _cacheKeyPrefix = "MetadataPrototypes::" + GetType().GUID.ToString("B");
+ }
+ return _cacheKeyPrefix;
+ }
+ }
+
+ protected internal ObjectCache PrototypeCache
+ {
+ get { return _prototypeCache ?? MemoryCache.Default; }
+ set { _prototypeCache = value; }
+ }
+
+ protected sealed override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
+ {
+ // If metadata is being created for a property then containerType != null && propertyName != null
+ // If metadata is being created for a type then containerType == null && propertyName == null, so we have to use modelType for the cache key.
+ Type typeForCache = containerType ?? modelType;
+ string cacheKey = GetCacheKey(typeForCache, propertyName);
+ TModelMetadata prototype = PrototypeCache.Get(cacheKey) as TModelMetadata;
+ if (prototype == null)
+ {
+ prototype = CreateMetadataPrototype(attributes, containerType, modelType, propertyName);
+ PrototypeCache.Add(cacheKey, prototype, CacheItemPolicy);
+ }
+
+ return CreateMetadataFromPrototype(prototype, modelAccessor);
+ }
+
+ // New override for creating the prototype metadata (without the accessor)
+ protected abstract TModelMetadata CreateMetadataPrototype(IEnumerable<Attribute> attributes, Type containerType, Type modelType, string propertyName);
+
+ // New override for applying the prototype + modelAccess to yield the final metadata
+ protected abstract TModelMetadata CreateMetadataFromPrototype(TModelMetadata prototype, Func<object> modelAccessor);
+
+ internal string GetCacheKey(Type type, string propertyName = null)
+ {
+ propertyName = propertyName ?? String.Empty;
+ return CacheKeyPrefix + GetTypeId(type) + propertyName;
+ }
+
+ public sealed override ModelMetadata GetMetadataForProperty(Func<object> modelAccessor, Type containerType, string propertyName)
+ {
+ return base.GetMetadataForProperty(modelAccessor, containerType, propertyName);
+ }
+
+ protected sealed override ModelMetadata GetMetadataForProperty(Func<object> modelAccessor, Type containerType, PropertyDescriptor propertyDescriptor)
+ {
+ return base.GetMetadataForProperty(modelAccessor, containerType, propertyDescriptor);
+ }
+
+ public sealed override IEnumerable<ModelMetadata> GetMetadataForProperties(object container, Type containerType)
+ {
+ return base.GetMetadataForProperties(container, containerType);
+ }
+
+ public sealed override ModelMetadata GetMetadataForType(Func<object> modelAccessor, Type modelType)
+ {
+ return base.GetMetadataForType(modelAccessor, modelType);
+ }
+
+ private static string GetTypeId(Type type)
+ {
+ // It's fine using a random Guid since we store the mapping for types to guids.
+ return _typeIds.GetOrAdd(type, _ => Guid.NewGuid().ToString("B"));
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/CachedDataAnnotationsMetadataAttributes.cs b/src/System.Web.Mvc/CachedDataAnnotationsMetadataAttributes.cs
new file mode 100644
index 00000000..94a092ef
--- /dev/null
+++ b/src/System.Web.Mvc/CachedDataAnnotationsMetadataAttributes.cs
@@ -0,0 +1,54 @@
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+
+namespace System.Web.Mvc
+{
+ public class CachedDataAnnotationsMetadataAttributes
+ {
+ public CachedDataAnnotationsMetadataAttributes(Attribute[] attributes)
+ {
+ DataType = attributes.OfType<DataTypeAttribute>().FirstOrDefault();
+ Display = attributes.OfType<DisplayAttribute>().FirstOrDefault();
+ DisplayColumn = attributes.OfType<DisplayColumnAttribute>().FirstOrDefault();
+ DisplayFormat = attributes.OfType<DisplayFormatAttribute>().FirstOrDefault();
+ DisplayName = attributes.OfType<DisplayNameAttribute>().FirstOrDefault();
+ Editable = attributes.OfType<EditableAttribute>().FirstOrDefault();
+ HiddenInput = attributes.OfType<HiddenInputAttribute>().FirstOrDefault();
+ ReadOnly = attributes.OfType<ReadOnlyAttribute>().FirstOrDefault();
+ Required = attributes.OfType<RequiredAttribute>().FirstOrDefault();
+ ScaffoldColumn = attributes.OfType<ScaffoldColumnAttribute>().FirstOrDefault();
+
+ var uiHintAttributes = attributes.OfType<UIHintAttribute>();
+ UIHint = uiHintAttributes.FirstOrDefault(a => String.Equals(a.PresentationLayer, "MVC", StringComparison.OrdinalIgnoreCase))
+ ?? uiHintAttributes.FirstOrDefault(a => String.IsNullOrEmpty(a.PresentationLayer));
+
+ if (DisplayFormat == null && DataType != null)
+ {
+ DisplayFormat = DataType.DisplayFormat;
+ }
+ }
+
+ public DataTypeAttribute DataType { get; protected set; }
+
+ public DisplayAttribute Display { get; protected set; }
+
+ public DisplayColumnAttribute DisplayColumn { get; protected set; }
+
+ public DisplayFormatAttribute DisplayFormat { get; protected set; }
+
+ public DisplayNameAttribute DisplayName { get; protected set; }
+
+ public EditableAttribute Editable { get; protected set; }
+
+ public HiddenInputAttribute HiddenInput { get; protected set; }
+
+ public ReadOnlyAttribute ReadOnly { get; protected set; }
+
+ public RequiredAttribute Required { get; protected set; }
+
+ public ScaffoldColumnAttribute ScaffoldColumn { get; protected set; }
+
+ public UIHintAttribute UIHint { get; protected set; }
+ }
+}
diff --git a/src/System.Web.Mvc/CachedDataAnnotationsModelMetadata.cs b/src/System.Web.Mvc/CachedDataAnnotationsModelMetadata.cs
new file mode 100644
index 00000000..399b1481
--- /dev/null
+++ b/src/System.Web.Mvc/CachedDataAnnotationsModelMetadata.cs
@@ -0,0 +1,216 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Globalization;
+using System.Linq;
+using System.Reflection;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc
+{
+ public class CachedDataAnnotationsModelMetadata : CachedModelMetadata<CachedDataAnnotationsMetadataAttributes>
+ {
+ public CachedDataAnnotationsModelMetadata(CachedDataAnnotationsModelMetadata prototype, Func<object> modelAccessor)
+ : base(prototype, modelAccessor)
+ {
+ }
+
+ public CachedDataAnnotationsModelMetadata(CachedDataAnnotationsModelMetadataProvider provider, Type containerType, Type modelType, string propertyName, IEnumerable<Attribute> attributes)
+ : base(provider, containerType, modelType, propertyName, new CachedDataAnnotationsMetadataAttributes(attributes.ToArray()))
+ {
+ }
+
+ protected override bool ComputeConvertEmptyStringToNull()
+ {
+ return PrototypeCache.DisplayFormat != null
+ ? PrototypeCache.DisplayFormat.ConvertEmptyStringToNull
+ : base.ComputeConvertEmptyStringToNull();
+ }
+
+ protected override string ComputeDataTypeName()
+ {
+ if (PrototypeCache.DataType != null)
+ {
+ return PrototypeCache.DataType.ToDataTypeName();
+ }
+
+ if (PrototypeCache.DisplayFormat != null && !PrototypeCache.DisplayFormat.HtmlEncode)
+ {
+ return DataTypeUtil.HtmlTypeName;
+ }
+
+ return base.ComputeDataTypeName();
+ }
+
+ protected override string ComputeDescription()
+ {
+ return PrototypeCache.Display != null
+ ? PrototypeCache.Display.GetDescription()
+ : base.ComputeDescription();
+ }
+
+ protected override string ComputeDisplayFormatString()
+ {
+ return PrototypeCache.DisplayFormat != null
+ ? PrototypeCache.DisplayFormat.DataFormatString
+ : base.ComputeDisplayFormatString();
+ }
+
+ protected override string ComputeDisplayName()
+ {
+ string result = null;
+
+ if (PrototypeCache.Display != null)
+ {
+ result = PrototypeCache.Display.GetName();
+ }
+
+ if (result == null && PrototypeCache.DisplayName != null)
+ {
+ result = PrototypeCache.DisplayName.DisplayName;
+ }
+
+ return result ?? base.ComputeDisplayName();
+ }
+
+ protected override string ComputeEditFormatString()
+ {
+ if (PrototypeCache.DisplayFormat != null && PrototypeCache.DisplayFormat.ApplyFormatInEditMode)
+ {
+ return PrototypeCache.DisplayFormat.DataFormatString;
+ }
+
+ return base.ComputeEditFormatString();
+ }
+
+ protected override bool ComputeHideSurroundingHtml()
+ {
+ return PrototypeCache.HiddenInput != null
+ ? !PrototypeCache.HiddenInput.DisplayValue
+ : base.ComputeHideSurroundingHtml();
+ }
+
+ protected override bool ComputeIsReadOnly()
+ {
+ if (PrototypeCache.Editable != null)
+ {
+ return !PrototypeCache.Editable.AllowEdit;
+ }
+
+ if (PrototypeCache.ReadOnly != null)
+ {
+ return PrototypeCache.ReadOnly.IsReadOnly;
+ }
+
+ return base.ComputeIsReadOnly();
+ }
+
+ protected override bool ComputeIsRequired()
+ {
+ return PrototypeCache.Required != null
+ ? true
+ : base.ComputeIsRequired();
+ }
+
+ protected override string ComputeNullDisplayText()
+ {
+ return PrototypeCache.DisplayFormat != null
+ ? PrototypeCache.DisplayFormat.NullDisplayText
+ : base.ComputeNullDisplayText();
+ }
+
+ protected override int ComputeOrder()
+ {
+ int? result = null;
+
+ if (PrototypeCache.Display != null)
+ {
+ result = PrototypeCache.Display.GetOrder();
+ }
+
+ return result ?? base.ComputeOrder();
+ }
+
+ protected override string ComputeShortDisplayName()
+ {
+ return PrototypeCache.Display != null
+ ? PrototypeCache.Display.GetShortName()
+ : base.ComputeShortDisplayName();
+ }
+
+ protected override bool ComputeShowForDisplay()
+ {
+ return PrototypeCache.ScaffoldColumn != null
+ ? PrototypeCache.ScaffoldColumn.Scaffold
+ : base.ComputeShowForDisplay();
+ }
+
+ protected override bool ComputeShowForEdit()
+ {
+ return PrototypeCache.ScaffoldColumn != null
+ ? PrototypeCache.ScaffoldColumn.Scaffold
+ : base.ComputeShowForEdit();
+ }
+
+ protected override string ComputeSimpleDisplayText()
+ {
+ if (Model != null)
+ {
+ if (PrototypeCache.DisplayColumn != null && !String.IsNullOrEmpty(PrototypeCache.DisplayColumn.DisplayColumn))
+ {
+ PropertyInfo displayColumnProperty = ModelType.GetProperty(PrototypeCache.DisplayColumn.DisplayColumn, BindingFlags.Public | BindingFlags.IgnoreCase | BindingFlags.Instance);
+ ValidateDisplayColumnAttribute(PrototypeCache.DisplayColumn, displayColumnProperty, ModelType);
+
+ object simpleDisplayTextValue = displayColumnProperty.GetValue(Model, new object[0]);
+ if (simpleDisplayTextValue != null)
+ {
+ return simpleDisplayTextValue.ToString();
+ }
+ }
+ }
+
+ return base.ComputeSimpleDisplayText();
+ }
+
+ protected override string ComputeTemplateHint()
+ {
+ if (PrototypeCache.UIHint != null)
+ {
+ return PrototypeCache.UIHint.UIHint;
+ }
+
+ if (PrototypeCache.HiddenInput != null)
+ {
+ return "HiddenInput";
+ }
+
+ return base.ComputeTemplateHint();
+ }
+
+ protected override string ComputeWatermark()
+ {
+ return PrototypeCache.Display != null
+ ? PrototypeCache.Display.GetPrompt()
+ : base.ComputeWatermark();
+ }
+
+ private static void ValidateDisplayColumnAttribute(DisplayColumnAttribute displayColumnAttribute, PropertyInfo displayColumnProperty, Type modelType)
+ {
+ if (displayColumnProperty == null)
+ {
+ throw new InvalidOperationException(
+ String.Format(
+ CultureInfo.CurrentCulture,
+ MvcResources.DataAnnotationsModelMetadataProvider_UnknownProperty,
+ modelType.FullName, displayColumnAttribute.DisplayColumn));
+ }
+ if (displayColumnProperty.GetGetMethod() == null)
+ {
+ throw new InvalidOperationException(
+ String.Format(
+ CultureInfo.CurrentCulture,
+ MvcResources.DataAnnotationsModelMetadataProvider_UnreadableProperty,
+ modelType.FullName, displayColumnAttribute.DisplayColumn));
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/CachedDataAnnotationsModelMetadataProvider.cs b/src/System.Web.Mvc/CachedDataAnnotationsModelMetadataProvider.cs
new file mode 100644
index 00000000..fab73542
--- /dev/null
+++ b/src/System.Web.Mvc/CachedDataAnnotationsModelMetadataProvider.cs
@@ -0,0 +1,17 @@
+using System.Collections.Generic;
+
+namespace System.Web.Mvc
+{
+ public class CachedDataAnnotationsModelMetadataProvider : CachedAssociatedMetadataProvider<CachedDataAnnotationsModelMetadata>
+ {
+ protected override CachedDataAnnotationsModelMetadata CreateMetadataPrototype(IEnumerable<Attribute> attributes, Type containerType, Type modelType, string propertyName)
+ {
+ return new CachedDataAnnotationsModelMetadata(this, containerType, modelType, propertyName, attributes);
+ }
+
+ protected override CachedDataAnnotationsModelMetadata CreateMetadataFromPrototype(CachedDataAnnotationsModelMetadata prototype, Func<object> modelAccessor)
+ {
+ return new CachedDataAnnotationsModelMetadata(prototype, modelAccessor);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/CachedModelMetadata`1.cs b/src/System.Web.Mvc/CachedModelMetadata`1.cs
new file mode 100644
index 00000000..bcc723e5
--- /dev/null
+++ b/src/System.Web.Mvc/CachedModelMetadata`1.cs
@@ -0,0 +1,415 @@
+namespace System.Web.Mvc
+{
+ // This class assumes that model metadata is expensive to create, and allows the user to
+ // stash a cache object that can be copied around as a prototype to make creation and
+ // computation quicker. It delegates the retrieval of values to getter methods, the results
+ // of which are cached on a per-metadata-instance basis.
+ //
+ // This allows flexible caching strategies: either caching the source of information across
+ // instances or caching of the actual information itself, depending on what the developer
+ // decides to put into the prototype cache.
+ public abstract class CachedModelMetadata<TPrototypeCache> : ModelMetadata
+ {
+ private bool _convertEmptyStringToNull;
+ private string _dataTypeName;
+ private string _description;
+ private string _displayFormatString;
+ private string _displayName;
+ private string _editFormatString;
+ private bool _hideSurroundingHtml;
+ private bool _isReadOnly;
+ private bool _isRequired;
+ private string _nullDisplayText;
+ private int _order;
+ private string _shortDisplayName;
+ private bool _showForDisplay;
+ private bool _showForEdit;
+ private string _templateHint;
+ private string _watermark;
+
+ private bool _convertEmptyStringToNullComputed;
+ private bool _dataTypeNameComputed;
+ private bool _descriptionComputed;
+ private bool _displayFormatStringComputed;
+ private bool _displayNameComputed;
+ private bool _editFormatStringComputed;
+ private bool _hideSurroundingHtmlComputed;
+ private bool _isReadOnlyComputed;
+ private bool _isRequiredComputed;
+ private bool _nullDisplayTextComputed;
+ private bool _orderComputed;
+ private bool _shortDisplayNameComputed;
+ private bool _showForDisplayComputed;
+ private bool _showForEditComputed;
+ private bool _templateHintComputed;
+ private bool _watermarkComputed;
+
+ // Constructor for creating real instances of the metadata class based on a prototype
+ protected CachedModelMetadata(CachedModelMetadata<TPrototypeCache> prototype, Func<object> modelAccessor)
+ : base(prototype.Provider, prototype.ContainerType, modelAccessor, prototype.ModelType, prototype.PropertyName)
+ {
+ PrototypeCache = prototype.PrototypeCache;
+ }
+
+ // Constructor for creating the prototype instances of the metadata class
+ protected CachedModelMetadata(CachedDataAnnotationsModelMetadataProvider provider, Type containerType, Type modelType, string propertyName, TPrototypeCache prototypeCache)
+ : base(provider, containerType, null /* modelAccessor */, modelType, propertyName)
+ {
+ PrototypeCache = prototypeCache;
+ }
+
+ public sealed override bool ConvertEmptyStringToNull
+ {
+ get
+ {
+ return CacheOrCompute(ComputeConvertEmptyStringToNull,
+ ref _convertEmptyStringToNull,
+ ref _convertEmptyStringToNullComputed);
+ }
+ set
+ {
+ _convertEmptyStringToNull = value;
+ _convertEmptyStringToNullComputed = true;
+ }
+ }
+
+ public sealed override string DataTypeName
+ {
+ get
+ {
+ return CacheOrCompute(ComputeDataTypeName,
+ ref _dataTypeName,
+ ref _dataTypeNameComputed);
+ }
+ set
+ {
+ _dataTypeName = value;
+ _dataTypeNameComputed = true;
+ }
+ }
+
+ public sealed override string Description
+ {
+ get
+ {
+ return CacheOrCompute(ComputeDescription,
+ ref _description,
+ ref _descriptionComputed);
+ }
+ set
+ {
+ _description = value;
+ _descriptionComputed = true;
+ }
+ }
+
+ public sealed override string DisplayFormatString
+ {
+ get
+ {
+ return CacheOrCompute(ComputeDisplayFormatString,
+ ref _displayFormatString,
+ ref _displayFormatStringComputed);
+ }
+ set
+ {
+ _displayFormatString = value;
+ _displayFormatStringComputed = true;
+ }
+ }
+
+ public sealed override string DisplayName
+ {
+ get
+ {
+ return CacheOrCompute(ComputeDisplayName,
+ ref _displayName,
+ ref _displayNameComputed);
+ }
+ set
+ {
+ _displayName = value;
+ _displayNameComputed = true;
+ }
+ }
+
+ public sealed override string EditFormatString
+ {
+ get
+ {
+ return CacheOrCompute(ComputeEditFormatString,
+ ref _editFormatString,
+ ref _editFormatStringComputed);
+ }
+ set
+ {
+ _editFormatString = value;
+ _editFormatStringComputed = true;
+ }
+ }
+
+ public sealed override bool HideSurroundingHtml
+ {
+ get
+ {
+ return CacheOrCompute(ComputeHideSurroundingHtml,
+ ref _hideSurroundingHtml,
+ ref _hideSurroundingHtmlComputed);
+ }
+ set
+ {
+ _hideSurroundingHtml = value;
+ _hideSurroundingHtmlComputed = true;
+ }
+ }
+
+ public sealed override bool IsReadOnly
+ {
+ get
+ {
+ return CacheOrCompute(ComputeIsReadOnly,
+ ref _isReadOnly,
+ ref _isReadOnlyComputed);
+ }
+ set
+ {
+ _isReadOnly = value;
+ _isReadOnlyComputed = true;
+ }
+ }
+
+ public sealed override bool IsRequired
+ {
+ get
+ {
+ return CacheOrCompute(ComputeIsRequired,
+ ref _isRequired,
+ ref _isRequiredComputed);
+ }
+ set
+ {
+ _isRequired = value;
+ _isRequiredComputed = true;
+ }
+ }
+
+ public sealed override string NullDisplayText
+ {
+ get
+ {
+ return CacheOrCompute(ComputeNullDisplayText,
+ ref _nullDisplayText,
+ ref _nullDisplayTextComputed);
+ }
+ set
+ {
+ _nullDisplayText = value;
+ _nullDisplayTextComputed = true;
+ }
+ }
+
+ public sealed override int Order
+ {
+ get
+ {
+ return CacheOrCompute(ComputeOrder,
+ ref _order,
+ ref _orderComputed);
+ }
+ set
+ {
+ _order = value;
+ _orderComputed = true;
+ }
+ }
+
+ protected TPrototypeCache PrototypeCache { get; set; }
+
+ public sealed override string ShortDisplayName
+ {
+ get
+ {
+ return CacheOrCompute(ComputeShortDisplayName,
+ ref _shortDisplayName,
+ ref _shortDisplayNameComputed);
+ }
+ set
+ {
+ _shortDisplayName = value;
+ _shortDisplayNameComputed = true;
+ }
+ }
+
+ public sealed override bool ShowForDisplay
+ {
+ get
+ {
+ return CacheOrCompute(ComputeShowForDisplay,
+ ref _showForDisplay,
+ ref _showForDisplayComputed);
+ }
+ set
+ {
+ _showForDisplay = value;
+ _showForDisplayComputed = true;
+ }
+ }
+
+ public sealed override bool ShowForEdit
+ {
+ get
+ {
+ return CacheOrCompute(ComputeShowForEdit,
+ ref _showForEdit,
+ ref _showForEditComputed);
+ }
+ set
+ {
+ _showForEdit = value;
+ _showForEditComputed = true;
+ }
+ }
+
+ public sealed override string SimpleDisplayText
+ {
+ get
+ {
+ // This is already cached in the base class with an appropriate override available
+ return base.SimpleDisplayText;
+ }
+ set { base.SimpleDisplayText = value; }
+ }
+
+ public sealed override string TemplateHint
+ {
+ get
+ {
+ return CacheOrCompute(ComputeTemplateHint,
+ ref _templateHint,
+ ref _templateHintComputed);
+ }
+ set
+ {
+ _templateHint = value;
+ _templateHintComputed = true;
+ }
+ }
+
+ public sealed override string Watermark
+ {
+ get
+ {
+ return CacheOrCompute(ComputeWatermark,
+ ref _watermark,
+ ref _watermarkComputed);
+ }
+ set
+ {
+ _watermark = value;
+ _watermarkComputed = true;
+ }
+ }
+
+ private static TResult CacheOrCompute<TResult>(Func<TResult> computeThunk, ref TResult value, ref bool computed)
+ {
+ if (!computed)
+ {
+ value = computeThunk();
+ computed = true;
+ }
+
+ return value;
+ }
+
+ protected virtual bool ComputeConvertEmptyStringToNull()
+ {
+ return base.ConvertEmptyStringToNull;
+ }
+
+ protected virtual string ComputeDataTypeName()
+ {
+ return base.DataTypeName;
+ }
+
+ protected virtual string ComputeDescription()
+ {
+ return base.Description;
+ }
+
+ protected virtual string ComputeDisplayFormatString()
+ {
+ return base.DisplayFormatString;
+ }
+
+ protected virtual string ComputeDisplayName()
+ {
+ return base.DisplayName;
+ }
+
+ protected virtual string ComputeEditFormatString()
+ {
+ return base.EditFormatString;
+ }
+
+ protected virtual bool ComputeHideSurroundingHtml()
+ {
+ return base.HideSurroundingHtml;
+ }
+
+ protected virtual bool ComputeIsReadOnly()
+ {
+ return base.IsReadOnly;
+ }
+
+ protected virtual bool ComputeIsRequired()
+ {
+ return base.IsRequired;
+ }
+
+ protected virtual string ComputeNullDisplayText()
+ {
+ return base.NullDisplayText;
+ }
+
+ protected virtual int ComputeOrder()
+ {
+ return base.Order;
+ }
+
+ protected virtual string ComputeShortDisplayName()
+ {
+ return base.ShortDisplayName;
+ }
+
+ protected virtual bool ComputeShowForDisplay()
+ {
+ return base.ShowForDisplay;
+ }
+
+ protected virtual bool ComputeShowForEdit()
+ {
+ return base.ShowForEdit;
+ }
+
+ protected virtual string ComputeSimpleDisplayText()
+ {
+ return base.GetSimpleDisplayText();
+ }
+
+ protected virtual string ComputeTemplateHint()
+ {
+ return base.TemplateHint;
+ }
+
+ protected virtual string ComputeWatermark()
+ {
+ return base.Watermark;
+ }
+
+ protected sealed override string GetSimpleDisplayText()
+ {
+ // Rename for consistency
+ return ComputeSimpleDisplayText();
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/CancellationTokenModelBinder.cs b/src/System.Web.Mvc/CancellationTokenModelBinder.cs
new file mode 100644
index 00000000..24522296
--- /dev/null
+++ b/src/System.Web.Mvc/CancellationTokenModelBinder.cs
@@ -0,0 +1,12 @@
+using System.Threading;
+
+namespace System.Web.Mvc
+{
+ public class CancellationTokenModelBinder : IModelBinder
+ {
+ public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
+ {
+ return default(CancellationToken);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ChildActionOnlyAttribute.cs b/src/System.Web.Mvc/ChildActionOnlyAttribute.cs
new file mode 100644
index 00000000..16647f4a
--- /dev/null
+++ b/src/System.Web.Mvc/ChildActionOnlyAttribute.cs
@@ -0,0 +1,19 @@
+namespace System.Web.Mvc
+{
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+ public sealed class ChildActionOnlyAttribute : FilterAttribute, IAuthorizationFilter
+ {
+ public void OnAuthorization(AuthorizationContext filterContext)
+ {
+ if (filterContext == null)
+ {
+ throw new ArgumentNullException("filterContext");
+ }
+
+ if (!filterContext.IsChildAction)
+ {
+ throw Error.ChildActionOnlyAttribute_MustBeInChildRequest(filterContext.ActionDescriptor);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ChildActionValueProvider.cs b/src/System.Web.Mvc/ChildActionValueProvider.cs
new file mode 100644
index 00000000..ec1d3528
--- /dev/null
+++ b/src/System.Web.Mvc/ChildActionValueProvider.cs
@@ -0,0 +1,39 @@
+using System.Globalization;
+
+namespace System.Web.Mvc
+{
+ public sealed class ChildActionValueProvider : DictionaryValueProvider<object>
+ {
+ private static string _childActionValuesKey = Guid.NewGuid().ToString();
+
+ public ChildActionValueProvider(ControllerContext controllerContext)
+ : base(controllerContext.RouteData.Values, CultureInfo.InvariantCulture)
+ {
+ }
+
+ internal static string ChildActionValuesKey
+ {
+ get { return _childActionValuesKey; }
+ }
+
+ public override ValueProviderResult GetValue(string key)
+ {
+ if (key == null)
+ {
+ throw new ArgumentNullException("key");
+ }
+
+ ValueProviderResult explicitValues = base.GetValue(ChildActionValuesKey);
+ if (explicitValues != null)
+ {
+ DictionaryValueProvider<object> rawExplicitValues = explicitValues.RawValue as DictionaryValueProvider<object>;
+ if (rawExplicitValues != null)
+ {
+ return rawExplicitValues.GetValue(key);
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ChildActionValueProviderFactory.cs b/src/System.Web.Mvc/ChildActionValueProviderFactory.cs
new file mode 100644
index 00000000..1231a381
--- /dev/null
+++ b/src/System.Web.Mvc/ChildActionValueProviderFactory.cs
@@ -0,0 +1,15 @@
+namespace System.Web.Mvc
+{
+ public sealed class ChildActionValueProviderFactory : ValueProviderFactory
+ {
+ public override IValueProvider GetValueProvider(ControllerContext controllerContext)
+ {
+ if (controllerContext == null)
+ {
+ throw new ArgumentNullException("controllerContext");
+ }
+
+ return new ChildActionValueProvider(controllerContext);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ClientDataTypeModelValidatorProvider.cs b/src/System.Web.Mvc/ClientDataTypeModelValidatorProvider.cs
new file mode 100644
index 00000000..080520b7
--- /dev/null
+++ b/src/System.Web.Mvc/ClientDataTypeModelValidatorProvider.cs
@@ -0,0 +1,159 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc
+{
+ public class ClientDataTypeModelValidatorProvider : ModelValidatorProvider
+ {
+ private static readonly HashSet<Type> _numericTypes = new HashSet<Type>(new Type[]
+ {
+ typeof(byte), typeof(sbyte),
+ typeof(short), typeof(ushort),
+ typeof(int), typeof(uint),
+ typeof(long), typeof(ulong),
+ typeof(float), typeof(double), typeof(decimal)
+ });
+
+ private static string _resourceClassKey;
+
+ public static string ResourceClassKey
+ {
+ get { return _resourceClassKey ?? String.Empty; }
+ set { _resourceClassKey = value; }
+ }
+
+ public override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context)
+ {
+ if (metadata == null)
+ {
+ throw new ArgumentNullException("metadata");
+ }
+ if (context == null)
+ {
+ throw new ArgumentNullException("context");
+ }
+
+ return GetValidatorsImpl(metadata, context);
+ }
+
+ private static IEnumerable<ModelValidator> GetValidatorsImpl(ModelMetadata metadata, ControllerContext context)
+ {
+ Type type = metadata.ModelType;
+
+ if (IsDateTimeType(type))
+ {
+ yield return new DateModelValidator(metadata, context);
+ }
+
+ if (IsNumericType(type))
+ {
+ yield return new NumericModelValidator(metadata, context);
+ }
+ }
+
+ private static bool IsNumericType(Type type)
+ {
+ return _numericTypes.Contains(GetTypeToValidate(type));
+ }
+
+ private static bool IsDateTimeType(Type type)
+ {
+ return typeof(DateTime) == GetTypeToValidate(type);
+ }
+
+ private static Type GetTypeToValidate(Type type)
+ {
+ return Nullable.GetUnderlyingType(type) ?? type; // strip off the Nullable<>
+ }
+
+ // If the user specified a ResourceClassKey try to load the resource they specified.
+ // If the class key is invalid, an exception will be thrown.
+ // If the class key is valid but the resource is not found, it returns null, in which
+ // case it will fall back to the MVC default error message.
+ private static string GetUserResourceString(ControllerContext controllerContext, string resourceName)
+ {
+ string result = null;
+
+ if (!String.IsNullOrEmpty(ResourceClassKey) && (controllerContext != null) && (controllerContext.HttpContext != null))
+ {
+ result = controllerContext.HttpContext.GetGlobalResourceObject(ResourceClassKey, resourceName, CultureInfo.CurrentUICulture) as string;
+ }
+
+ return result;
+ }
+
+ private static string GetFieldMustBeNumericResource(ControllerContext controllerContext)
+ {
+ return GetUserResourceString(controllerContext, "FieldMustBeNumeric") ?? MvcResources.ClientDataTypeModelValidatorProvider_FieldMustBeNumeric;
+ }
+
+ private static string GetFieldMustBeDateResource(ControllerContext controllerContext)
+ {
+ return GetUserResourceString(controllerContext, "FieldMustBeDate") ?? MvcResources.ClientDataTypeModelValidatorProvider_FieldMustBeDate;
+ }
+
+ internal class ClientModelValidator : ModelValidator
+ {
+ private string _errorMessage;
+ private string _validationType;
+
+ public ClientModelValidator(ModelMetadata metadata, ControllerContext controllerContext, string validationType, string errorMessage)
+ : base(metadata, controllerContext)
+ {
+ if (String.IsNullOrEmpty(validationType))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "validationType");
+ }
+
+ if (String.IsNullOrEmpty(errorMessage))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "errorMessage");
+ }
+
+ _validationType = validationType;
+ _errorMessage = errorMessage;
+ }
+
+ public sealed override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
+ {
+ ModelClientValidationRule rule = new ModelClientValidationRule()
+ {
+ ValidationType = _validationType,
+ ErrorMessage = FormatErrorMessage(Metadata.GetDisplayName())
+ };
+
+ return new ModelClientValidationRule[] { rule };
+ }
+
+ private string FormatErrorMessage(string displayName)
+ {
+ // use CurrentCulture since this message is intended for the site visitor
+ return String.Format(CultureInfo.CurrentCulture, _errorMessage, displayName);
+ }
+
+ public sealed override IEnumerable<ModelValidationResult> Validate(object container)
+ {
+ // this is not a server-side validator
+ return Enumerable.Empty<ModelValidationResult>();
+ }
+ }
+
+ internal sealed class DateModelValidator : ClientModelValidator
+ {
+ public DateModelValidator(ModelMetadata metadata, ControllerContext controllerContext)
+ : base(metadata, controllerContext, "date", GetFieldMustBeDateResource(controllerContext))
+ {
+ }
+ }
+
+ internal sealed class NumericModelValidator : ClientModelValidator
+ {
+ public NumericModelValidator(ModelMetadata metadata, ControllerContext controllerContext)
+ : base(metadata, controllerContext, "number", GetFieldMustBeNumericResource(controllerContext))
+ {
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/CompareAttribute.cs b/src/System.Web.Mvc/CompareAttribute.cs
new file mode 100644
index 00000000..dc80ed05
--- /dev/null
+++ b/src/System.Web.Mvc/CompareAttribute.cs
@@ -0,0 +1,74 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Reflection;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc
+{
+ [AttributeUsage(AttributeTargets.Property)]
+ [SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", Justification = "This attribute is designed to be a base class for other attributes.")]
+ public class CompareAttribute : ValidationAttribute, IClientValidatable
+ {
+ public CompareAttribute(string otherProperty)
+ : base(MvcResources.CompareAttribute_MustMatch)
+ {
+ if (otherProperty == null)
+ {
+ throw new ArgumentNullException("otherProperty");
+ }
+ OtherProperty = otherProperty;
+ }
+
+ public string OtherProperty { get; private set; }
+
+ public string OtherPropertyDisplayName { get; internal set; }
+
+ public override string FormatErrorMessage(string name)
+ {
+ return String.Format(CultureInfo.CurrentCulture, ErrorMessageString, name, OtherPropertyDisplayName ?? OtherProperty);
+ }
+
+ protected override ValidationResult IsValid(object value, ValidationContext validationContext)
+ {
+ PropertyInfo otherPropertyInfo = validationContext.ObjectType.GetProperty(OtherProperty);
+ if (otherPropertyInfo == null)
+ {
+ return new ValidationResult(String.Format(CultureInfo.CurrentCulture, MvcResources.CompareAttribute_UnknownProperty, OtherProperty));
+ }
+
+ object otherPropertyValue = otherPropertyInfo.GetValue(validationContext.ObjectInstance, null);
+ if (!Equals(value, otherPropertyValue))
+ {
+ if (OtherPropertyDisplayName == null)
+ {
+ OtherPropertyDisplayName = ModelMetadataProviders.Current.GetMetadataForProperty(() => validationContext.ObjectInstance, validationContext.ObjectType, OtherProperty).GetDisplayName();
+ }
+ return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
+ }
+ return null;
+ }
+
+ public static string FormatPropertyForClientValidation(string property)
+ {
+ if (property == null)
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "property");
+ }
+ return "*." + property;
+ }
+
+ public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
+ {
+ if (metadata.ContainerType != null)
+ {
+ if (OtherPropertyDisplayName == null)
+ {
+ OtherPropertyDisplayName = ModelMetadataProviders.Current.GetMetadataForProperty(() => metadata.Model, metadata.ContainerType, OtherProperty).GetDisplayName();
+ }
+ }
+ yield return new ModelClientValidationEqualToRule(FormatErrorMessage(metadata.GetDisplayName()), FormatPropertyForClientValidation(OtherProperty));
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ContentResult.cs b/src/System.Web.Mvc/ContentResult.cs
new file mode 100644
index 00000000..18812c36
--- /dev/null
+++ b/src/System.Web.Mvc/ContentResult.cs
@@ -0,0 +1,36 @@
+using System.Text;
+
+namespace System.Web.Mvc
+{
+ public class ContentResult : ActionResult
+ {
+ public string Content { get; set; }
+
+ public Encoding ContentEncoding { get; set; }
+
+ public string ContentType { get; set; }
+
+ public override void ExecuteResult(ControllerContext context)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException("context");
+ }
+
+ HttpResponseBase response = context.HttpContext.Response;
+
+ if (!String.IsNullOrEmpty(ContentType))
+ {
+ response.ContentType = ContentType;
+ }
+ if (ContentEncoding != null)
+ {
+ response.ContentEncoding = ContentEncoding;
+ }
+ if (Content != null)
+ {
+ response.Write(Content);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Controller.cs b/src/System.Web.Mvc/Controller.cs
new file mode 100644
index 00000000..e654c650
--- /dev/null
+++ b/src/System.Web.Mvc/Controller.cs
@@ -0,0 +1,920 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.IO;
+using System.Security.Principal;
+using System.Text;
+using System.Web.Mvc.Async;
+using System.Web.Mvc.Properties;
+using System.Web.Profile;
+using System.Web.Routing;
+
+namespace System.Web.Mvc
+{
+ [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Class complexity dictated by public surface area")]
+ public abstract class Controller : ControllerBase, IActionFilter, IAuthorizationFilter, IDisposable, IExceptionFilter, IResultFilter, IAsyncController, IAsyncManagerContainer
+ {
+ private static readonly object _executeTag = new object();
+ private static readonly object _executeCoreTag = new object();
+
+ private readonly AsyncManager _asyncManager = new AsyncManager();
+ private IActionInvoker _actionInvoker;
+ private ModelBinderDictionary _binders;
+ private RouteCollection _routeCollection;
+ private ITempDataProvider _tempDataProvider;
+ private ViewEngineCollection _viewEngineCollection;
+
+ private IDependencyResolver _resolver;
+
+ // By default, use the global resolver with caching.
+ // Or we can override to supply this instance with its own cache.
+ internal IDependencyResolver Resolver
+ {
+ get { return _resolver ?? DependencyResolver.CurrentCache; }
+ set { _resolver = value; }
+ }
+
+ public AsyncManager AsyncManager
+ {
+ get { return _asyncManager; }
+ }
+
+ /// <summary>
+ /// This is for backwards compat. MVC 4.0 starts allowing Controller to support asynchronous patterns.
+ /// This means ExecuteCore doesn't get called on derived classes. Derived classes can override this
+ /// flag and set to true if they still need ExecuteCore to be called.
+ /// </summary>
+ protected virtual bool DisableAsyncSupport
+ {
+ get { return false; }
+ }
+
+ public IActionInvoker ActionInvoker
+ {
+ get
+ {
+ if (_actionInvoker == null)
+ {
+ _actionInvoker = CreateActionInvoker();
+ }
+ return _actionInvoker;
+ }
+ set { _actionInvoker = value; }
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "Property is settable so that the dictionary can be provided for unit testing purposes.")]
+ protected internal ModelBinderDictionary Binders
+ {
+ get
+ {
+ if (_binders == null)
+ {
+ _binders = ModelBinders.Binders;
+ }
+ return _binders;
+ }
+ set { _binders = value; }
+ }
+
+ public HttpContextBase HttpContext
+ {
+ get { return ControllerContext == null ? null : ControllerContext.HttpContext; }
+ }
+
+ public ModelStateDictionary ModelState
+ {
+ get { return ViewData.ModelState; }
+ }
+
+ public ProfileBase Profile
+ {
+ get { return HttpContext == null ? null : HttpContext.Profile; }
+ }
+
+ public HttpRequestBase Request
+ {
+ get { return HttpContext == null ? null : HttpContext.Request; }
+ }
+
+ public HttpResponseBase Response
+ {
+ get { return HttpContext == null ? null : HttpContext.Response; }
+ }
+
+ internal RouteCollection RouteCollection
+ {
+ get
+ {
+ if (_routeCollection == null)
+ {
+ _routeCollection = RouteTable.Routes;
+ }
+ return _routeCollection;
+ }
+ set { _routeCollection = value; }
+ }
+
+ public RouteData RouteData
+ {
+ get { return ControllerContext == null ? null : ControllerContext.RouteData; }
+ }
+
+ public HttpServerUtilityBase Server
+ {
+ get { return HttpContext == null ? null : HttpContext.Server; }
+ }
+
+ public HttpSessionStateBase Session
+ {
+ get { return HttpContext == null ? null : HttpContext.Session; }
+ }
+
+ public ITempDataProvider TempDataProvider
+ {
+ get
+ {
+ if (_tempDataProvider == null)
+ {
+ _tempDataProvider = CreateTempDataProvider();
+ }
+ return _tempDataProvider;
+ }
+ set { _tempDataProvider = value; }
+ }
+
+ public UrlHelper Url { get; set; }
+
+ public IPrincipal User
+ {
+ get { return HttpContext == null ? null : HttpContext.User; }
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This entire type is meant to be mutable.")]
+ public ViewEngineCollection ViewEngineCollection
+ {
+ get { return _viewEngineCollection ?? ViewEngines.Engines; }
+ set { _viewEngineCollection = value; }
+ }
+
+ [SuppressMessage("Microsoft.Naming", "CA1719:ParameterNamesShouldNotMatchMemberNames", MessageId = "0#", Justification = "'Content' refers to ContentResult type; 'content' refers to ContentResult.Content property.")]
+ protected internal ContentResult Content(string content)
+ {
+ return Content(content, null /* contentType */);
+ }
+
+ [SuppressMessage("Microsoft.Naming", "CA1719:ParameterNamesShouldNotMatchMemberNames", MessageId = "0#", Justification = "'Content' refers to ContentResult type; 'content' refers to ContentResult.Content property.")]
+ protected internal ContentResult Content(string content, string contentType)
+ {
+ return Content(content, contentType, null /* contentEncoding */);
+ }
+
+ [SuppressMessage("Microsoft.Naming", "CA1719:ParameterNamesShouldNotMatchMemberNames", MessageId = "0#", Justification = "'Content' refers to ContentResult type; 'content' refers to ContentResult.Content property.")]
+ protected internal virtual ContentResult Content(string content, string contentType, Encoding contentEncoding)
+ {
+ return new ContentResult
+ {
+ Content = content,
+ ContentType = contentType,
+ ContentEncoding = contentEncoding
+ };
+ }
+
+ protected virtual IActionInvoker CreateActionInvoker()
+ {
+ // Controller supports asynchronous operations by default.
+ return Resolver.GetService<IAsyncActionInvoker>() ?? Resolver.GetService<IActionInvoker>() ?? new AsyncControllerActionInvoker();
+ }
+
+ protected virtual ITempDataProvider CreateTempDataProvider()
+ {
+ return Resolver.GetService<ITempDataProvider>() ?? new SessionStateTempDataProvider();
+ }
+
+ // The default invoker will never match methods defined on the Controller type, so
+ // the Dispose() method is not web-callable. However, in general, since implicitly-
+ // implemented interface methods are public, they are web-callable unless decorated with
+ // [NonAction].
+ public void Dispose()
+ {
+ Dispose(true /* disposing */);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ }
+
+ protected override void ExecuteCore()
+ {
+ // If code in this method needs to be updated, please also check the BeginExecuteCore() and
+ // EndExecuteCore() methods of AsyncController to see if that code also must be updated.
+
+ PossiblyLoadTempData();
+ try
+ {
+ string actionName = RouteData.GetRequiredString("action");
+ if (!ActionInvoker.InvokeAction(ControllerContext, actionName))
+ {
+ HandleUnknownAction(actionName);
+ }
+ }
+ finally
+ {
+ PossiblySaveTempData();
+ }
+ }
+
+ protected internal FileContentResult File(byte[] fileContents, string contentType)
+ {
+ return File(fileContents, contentType, null /* fileDownloadName */);
+ }
+
+ protected internal virtual FileContentResult File(byte[] fileContents, string contentType, string fileDownloadName)
+ {
+ return new FileContentResult(fileContents, contentType) { FileDownloadName = fileDownloadName };
+ }
+
+ protected internal FileStreamResult File(Stream fileStream, string contentType)
+ {
+ return File(fileStream, contentType, null /* fileDownloadName */);
+ }
+
+ protected internal virtual FileStreamResult File(Stream fileStream, string contentType, string fileDownloadName)
+ {
+ return new FileStreamResult(fileStream, contentType) { FileDownloadName = fileDownloadName };
+ }
+
+ protected internal FilePathResult File(string fileName, string contentType)
+ {
+ return File(fileName, contentType, null /* fileDownloadName */);
+ }
+
+ protected internal virtual FilePathResult File(string fileName, string contentType, string fileDownloadName)
+ {
+ return new FilePathResult(fileName, contentType) { FileDownloadName = fileDownloadName };
+ }
+
+ protected virtual void HandleUnknownAction(string actionName)
+ {
+ throw new HttpException(404, String.Format(CultureInfo.CurrentCulture,
+ MvcResources.Controller_UnknownAction, actionName, GetType().FullName));
+ }
+
+ protected internal HttpNotFoundResult HttpNotFound()
+ {
+ return HttpNotFound(null);
+ }
+
+ protected internal virtual HttpNotFoundResult HttpNotFound(string statusDescription)
+ {
+ return new HttpNotFoundResult(statusDescription);
+ }
+
+ protected internal virtual JavaScriptResult JavaScript(string script)
+ {
+ return new JavaScriptResult { Script = script };
+ }
+
+ protected internal JsonResult Json(object data)
+ {
+ return Json(data, null /* contentType */, null /* contentEncoding */, JsonRequestBehavior.DenyGet);
+ }
+
+ protected internal JsonResult Json(object data, string contentType)
+ {
+ return Json(data, contentType, null /* contentEncoding */, JsonRequestBehavior.DenyGet);
+ }
+
+ protected internal virtual JsonResult Json(object data, string contentType, Encoding contentEncoding)
+ {
+ return Json(data, contentType, contentEncoding, JsonRequestBehavior.DenyGet);
+ }
+
+ protected internal JsonResult Json(object data, JsonRequestBehavior behavior)
+ {
+ return Json(data, null /* contentType */, null /* contentEncoding */, behavior);
+ }
+
+ protected internal JsonResult Json(object data, string contentType, JsonRequestBehavior behavior)
+ {
+ return Json(data, contentType, null /* contentEncoding */, behavior);
+ }
+
+ protected internal virtual JsonResult Json(object data, string contentType, Encoding contentEncoding, JsonRequestBehavior behavior)
+ {
+ return new JsonResult
+ {
+ Data = data,
+ ContentType = contentType,
+ ContentEncoding = contentEncoding,
+ JsonRequestBehavior = behavior
+ };
+ }
+
+ protected override void Initialize(RequestContext requestContext)
+ {
+ base.Initialize(requestContext);
+ Url = new UrlHelper(requestContext);
+ }
+
+ protected virtual void OnActionExecuting(ActionExecutingContext filterContext)
+ {
+ }
+
+ protected virtual void OnActionExecuted(ActionExecutedContext filterContext)
+ {
+ }
+
+ protected virtual void OnAuthorization(AuthorizationContext filterContext)
+ {
+ }
+
+ protected virtual void OnException(ExceptionContext filterContext)
+ {
+ }
+
+ protected virtual void OnResultExecuted(ResultExecutedContext filterContext)
+ {
+ }
+
+ protected virtual void OnResultExecuting(ResultExecutingContext filterContext)
+ {
+ }
+
+ protected internal PartialViewResult PartialView()
+ {
+ return PartialView(null /* viewName */, null /* model */);
+ }
+
+ protected internal PartialViewResult PartialView(object model)
+ {
+ return PartialView(null /* viewName */, model);
+ }
+
+ protected internal PartialViewResult PartialView(string viewName)
+ {
+ return PartialView(viewName, null /* model */);
+ }
+
+ protected internal virtual PartialViewResult PartialView(string viewName, object model)
+ {
+ if (model != null)
+ {
+ ViewData.Model = model;
+ }
+
+ return new PartialViewResult
+ {
+ ViewName = viewName,
+ ViewData = ViewData,
+ TempData = TempData,
+ ViewEngineCollection = ViewEngineCollection
+ };
+ }
+
+ internal void PossiblyLoadTempData()
+ {
+ if (!ControllerContext.IsChildAction)
+ {
+ TempData.Load(ControllerContext, TempDataProvider);
+ }
+ }
+
+ internal void PossiblySaveTempData()
+ {
+ if (!ControllerContext.IsChildAction)
+ {
+ TempData.Save(ControllerContext, TempDataProvider);
+ }
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "0#", Justification = "Response.Redirect() takes its URI as a string parameter.")]
+ protected internal virtual RedirectResult Redirect(string url)
+ {
+ if (String.IsNullOrEmpty(url))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "url");
+ }
+
+ return new RedirectResult(url);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "0#", Justification = "Response.RedirectPermanent() takes its URI as a string parameter.")]
+ protected internal virtual RedirectResult RedirectPermanent(string url)
+ {
+ if (String.IsNullOrEmpty(url))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "url");
+ }
+
+ return new RedirectResult(url, permanent: true);
+ }
+
+ protected internal RedirectToRouteResult RedirectToAction(string actionName)
+ {
+ return RedirectToAction(actionName, (RouteValueDictionary)null);
+ }
+
+ protected internal RedirectToRouteResult RedirectToAction(string actionName, object routeValues)
+ {
+ return RedirectToAction(actionName, new RouteValueDictionary(routeValues));
+ }
+
+ protected internal RedirectToRouteResult RedirectToAction(string actionName, RouteValueDictionary routeValues)
+ {
+ return RedirectToAction(actionName, null /* controllerName */, routeValues);
+ }
+
+ protected internal RedirectToRouteResult RedirectToAction(string actionName, string controllerName)
+ {
+ return RedirectToAction(actionName, controllerName, (RouteValueDictionary)null);
+ }
+
+ protected internal RedirectToRouteResult RedirectToAction(string actionName, string controllerName, object routeValues)
+ {
+ return RedirectToAction(actionName, controllerName, new RouteValueDictionary(routeValues));
+ }
+
+ protected internal virtual RedirectToRouteResult RedirectToAction(string actionName, string controllerName, RouteValueDictionary routeValues)
+ {
+ RouteValueDictionary mergedRouteValues;
+
+ if (RouteData == null)
+ {
+ mergedRouteValues = RouteValuesHelpers.MergeRouteValues(actionName, controllerName, null, routeValues, includeImplicitMvcValues: true);
+ }
+ else
+ {
+ mergedRouteValues = RouteValuesHelpers.MergeRouteValues(actionName, controllerName, RouteData.Values, routeValues, includeImplicitMvcValues: true);
+ }
+
+ return new RedirectToRouteResult(mergedRouteValues);
+ }
+
+ protected internal RedirectToRouteResult RedirectToActionPermanent(string actionName)
+ {
+ return RedirectToActionPermanent(actionName, (RouteValueDictionary)null);
+ }
+
+ protected internal RedirectToRouteResult RedirectToActionPermanent(string actionName, object routeValues)
+ {
+ return RedirectToActionPermanent(actionName, new RouteValueDictionary(routeValues));
+ }
+
+ protected internal RedirectToRouteResult RedirectToActionPermanent(string actionName, RouteValueDictionary routeValues)
+ {
+ return RedirectToActionPermanent(actionName, null /* controllerName */, routeValues);
+ }
+
+ protected internal RedirectToRouteResult RedirectToActionPermanent(string actionName, string controllerName)
+ {
+ return RedirectToActionPermanent(actionName, controllerName, (RouteValueDictionary)null);
+ }
+
+ protected internal RedirectToRouteResult RedirectToActionPermanent(string actionName, string controllerName, object routeValues)
+ {
+ return RedirectToActionPermanent(actionName, controllerName, new RouteValueDictionary(routeValues));
+ }
+
+ protected internal virtual RedirectToRouteResult RedirectToActionPermanent(string actionName, string controllerName, RouteValueDictionary routeValues)
+ {
+ RouteValueDictionary implicitRouteValues = (RouteData != null) ? RouteData.Values : null;
+
+ RouteValueDictionary mergedRouteValues =
+ RouteValuesHelpers.MergeRouteValues(actionName, controllerName, implicitRouteValues, routeValues, includeImplicitMvcValues: true);
+
+ return new RedirectToRouteResult(null, mergedRouteValues, permanent: true);
+ }
+
+ protected internal RedirectToRouteResult RedirectToRoute(object routeValues)
+ {
+ return RedirectToRoute(new RouteValueDictionary(routeValues));
+ }
+
+ protected internal RedirectToRouteResult RedirectToRoute(RouteValueDictionary routeValues)
+ {
+ return RedirectToRoute(null /* routeName */, routeValues);
+ }
+
+ protected internal RedirectToRouteResult RedirectToRoute(string routeName)
+ {
+ return RedirectToRoute(routeName, (RouteValueDictionary)null);
+ }
+
+ protected internal RedirectToRouteResult RedirectToRoute(string routeName, object routeValues)
+ {
+ return RedirectToRoute(routeName, new RouteValueDictionary(routeValues));
+ }
+
+ protected internal virtual RedirectToRouteResult RedirectToRoute(string routeName, RouteValueDictionary routeValues)
+ {
+ return new RedirectToRouteResult(routeName, RouteValuesHelpers.GetRouteValues(routeValues));
+ }
+
+ protected internal RedirectToRouteResult RedirectToRoutePermanent(object routeValues)
+ {
+ return RedirectToRoutePermanent(new RouteValueDictionary(routeValues));
+ }
+
+ protected internal RedirectToRouteResult RedirectToRoutePermanent(RouteValueDictionary routeValues)
+ {
+ return RedirectToRoutePermanent(null /* routeName */, routeValues);
+ }
+
+ protected internal RedirectToRouteResult RedirectToRoutePermanent(string routeName)
+ {
+ return RedirectToRoutePermanent(routeName, (RouteValueDictionary)null);
+ }
+
+ protected internal RedirectToRouteResult RedirectToRoutePermanent(string routeName, object routeValues)
+ {
+ return RedirectToRoutePermanent(routeName, new RouteValueDictionary(routeValues));
+ }
+
+ protected internal virtual RedirectToRouteResult RedirectToRoutePermanent(string routeName, RouteValueDictionary routeValues)
+ {
+ return new RedirectToRouteResult(routeName, RouteValuesHelpers.GetRouteValues(routeValues), permanent: true);
+ }
+
+ protected internal bool TryUpdateModel<TModel>(TModel model) where TModel : class
+ {
+ return TryUpdateModel(model, null, null, null, ValueProvider);
+ }
+
+ protected internal bool TryUpdateModel<TModel>(TModel model, string prefix) where TModel : class
+ {
+ return TryUpdateModel(model, prefix, null, null, ValueProvider);
+ }
+
+ protected internal bool TryUpdateModel<TModel>(TModel model, string[] includeProperties) where TModel : class
+ {
+ return TryUpdateModel(model, null, includeProperties, null, ValueProvider);
+ }
+
+ protected internal bool TryUpdateModel<TModel>(TModel model, string prefix, string[] includeProperties) where TModel : class
+ {
+ return TryUpdateModel(model, prefix, includeProperties, null, ValueProvider);
+ }
+
+ protected internal bool TryUpdateModel<TModel>(TModel model, string prefix, string[] includeProperties, string[] excludeProperties) where TModel : class
+ {
+ return TryUpdateModel(model, prefix, includeProperties, excludeProperties, ValueProvider);
+ }
+
+ protected internal bool TryUpdateModel<TModel>(TModel model, IValueProvider valueProvider) where TModel : class
+ {
+ return TryUpdateModel(model, null, null, null, valueProvider);
+ }
+
+ protected internal bool TryUpdateModel<TModel>(TModel model, string prefix, IValueProvider valueProvider) where TModel : class
+ {
+ return TryUpdateModel(model, prefix, null, null, valueProvider);
+ }
+
+ protected internal bool TryUpdateModel<TModel>(TModel model, string[] includeProperties, IValueProvider valueProvider) where TModel : class
+ {
+ return TryUpdateModel(model, null, includeProperties, null, valueProvider);
+ }
+
+ protected internal bool TryUpdateModel<TModel>(TModel model, string prefix, string[] includeProperties, IValueProvider valueProvider) where TModel : class
+ {
+ return TryUpdateModel(model, prefix, includeProperties, null, valueProvider);
+ }
+
+ protected internal bool TryUpdateModel<TModel>(TModel model, string prefix, string[] includeProperties, string[] excludeProperties, IValueProvider valueProvider) where TModel : class
+ {
+ if (model == null)
+ {
+ throw new ArgumentNullException("model");
+ }
+ if (valueProvider == null)
+ {
+ throw new ArgumentNullException("valueProvider");
+ }
+
+ Predicate<string> propertyFilter = propertyName => BindAttribute.IsPropertyAllowed(propertyName, includeProperties, excludeProperties);
+ IModelBinder binder = Binders.GetBinder(typeof(TModel));
+
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, typeof(TModel)),
+ ModelName = prefix,
+ ModelState = ModelState,
+ PropertyFilter = propertyFilter,
+ ValueProvider = valueProvider
+ };
+ binder.BindModel(ControllerContext, bindingContext);
+ return ModelState.IsValid;
+ }
+
+ protected internal bool TryValidateModel(object model)
+ {
+ return TryValidateModel(model, null /* prefix */);
+ }
+
+ protected internal bool TryValidateModel(object model, string prefix)
+ {
+ if (model == null)
+ {
+ throw new ArgumentNullException("model");
+ }
+
+ ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, model.GetType());
+
+ foreach (ModelValidationResult validationResult in ModelValidator.GetModelValidator(metadata, ControllerContext).Validate(null))
+ {
+ ModelState.AddModelError(DefaultModelBinder.CreateSubPropertyName(prefix, validationResult.MemberName), validationResult.Message);
+ }
+
+ return ModelState.IsValid;
+ }
+
+ protected internal void UpdateModel<TModel>(TModel model) where TModel : class
+ {
+ UpdateModel(model, null, null, null, ValueProvider);
+ }
+
+ protected internal void UpdateModel<TModel>(TModel model, string prefix) where TModel : class
+ {
+ UpdateModel(model, prefix, null, null, ValueProvider);
+ }
+
+ protected internal void UpdateModel<TModel>(TModel model, string[] includeProperties) where TModel : class
+ {
+ UpdateModel(model, null, includeProperties, null, ValueProvider);
+ }
+
+ protected internal void UpdateModel<TModel>(TModel model, string prefix, string[] includeProperties) where TModel : class
+ {
+ UpdateModel(model, prefix, includeProperties, null, ValueProvider);
+ }
+
+ protected internal void UpdateModel<TModel>(TModel model, string prefix, string[] includeProperties, string[] excludeProperties) where TModel : class
+ {
+ UpdateModel(model, prefix, includeProperties, excludeProperties, ValueProvider);
+ }
+
+ protected internal void UpdateModel<TModel>(TModel model, IValueProvider valueProvider) where TModel : class
+ {
+ UpdateModel(model, null, null, null, valueProvider);
+ }
+
+ protected internal void UpdateModel<TModel>(TModel model, string prefix, IValueProvider valueProvider) where TModel : class
+ {
+ UpdateModel(model, prefix, null, null, valueProvider);
+ }
+
+ protected internal void UpdateModel<TModel>(TModel model, string[] includeProperties, IValueProvider valueProvider) where TModel : class
+ {
+ UpdateModel(model, null, includeProperties, null, valueProvider);
+ }
+
+ protected internal void UpdateModel<TModel>(TModel model, string prefix, string[] includeProperties, IValueProvider valueProvider) where TModel : class
+ {
+ UpdateModel(model, prefix, includeProperties, null, valueProvider);
+ }
+
+ protected internal void UpdateModel<TModel>(TModel model, string prefix, string[] includeProperties, string[] excludeProperties, IValueProvider valueProvider) where TModel : class
+ {
+ bool success = TryUpdateModel(model, prefix, includeProperties, excludeProperties, valueProvider);
+ if (!success)
+ {
+ string message = String.Format(CultureInfo.CurrentCulture, MvcResources.Controller_UpdateModel_UpdateUnsuccessful,
+ typeof(TModel).FullName);
+ throw new InvalidOperationException(message);
+ }
+ }
+
+ protected internal void ValidateModel(object model)
+ {
+ ValidateModel(model, null /* prefix */);
+ }
+
+ protected internal void ValidateModel(object model, string prefix)
+ {
+ if (!TryValidateModel(model, prefix))
+ {
+ throw new InvalidOperationException(
+ String.Format(
+ CultureInfo.CurrentCulture,
+ MvcResources.Controller_Validate_ValidationFailed,
+ model.GetType().FullName));
+ }
+ }
+
+ protected internal ViewResult View()
+ {
+ return View(viewName: null, masterName: null, model: null);
+ }
+
+ protected internal ViewResult View(object model)
+ {
+ return View(null /* viewName */, null /* masterName */, model);
+ }
+
+ protected internal ViewResult View(string viewName)
+ {
+ return View(viewName, masterName: null, model: null);
+ }
+
+ protected internal ViewResult View(string viewName, string masterName)
+ {
+ return View(viewName, masterName, null /* model */);
+ }
+
+ protected internal ViewResult View(string viewName, object model)
+ {
+ return View(viewName, null /* masterName */, model);
+ }
+
+ protected internal virtual ViewResult View(string viewName, string masterName, object model)
+ {
+ if (model != null)
+ {
+ ViewData.Model = model;
+ }
+
+ return new ViewResult
+ {
+ ViewName = viewName,
+ MasterName = masterName,
+ ViewData = ViewData,
+ TempData = TempData,
+ ViewEngineCollection = ViewEngineCollection
+ };
+ }
+
+ [SuppressMessage("Microsoft.Naming", "CA1719:ParameterNamesShouldNotMatchMemberNames", MessageId = "0#", Justification = "The method name 'View' is a convenient shorthand for 'CreateViewResult'.")]
+ protected internal ViewResult View(IView view)
+ {
+ return View(view, null /* model */);
+ }
+
+ [SuppressMessage("Microsoft.Naming", "CA1719:ParameterNamesShouldNotMatchMemberNames", MessageId = "0#", Justification = "The method name 'View' is a convenient shorthand for 'CreateViewResult'.")]
+ protected internal virtual ViewResult View(IView view, object model)
+ {
+ if (model != null)
+ {
+ ViewData.Model = model;
+ }
+
+ return new ViewResult
+ {
+ View = view,
+ ViewData = ViewData,
+ TempData = TempData
+ };
+ }
+
+ IAsyncResult IAsyncController.BeginExecute(RequestContext requestContext, AsyncCallback callback, object state)
+ {
+ return BeginExecute(requestContext, callback, state);
+ }
+
+ void IAsyncController.EndExecute(IAsyncResult asyncResult)
+ {
+ EndExecute(asyncResult);
+ }
+
+ protected virtual IAsyncResult BeginExecute(RequestContext requestContext, AsyncCallback callback, object state)
+ {
+ if (DisableAsyncSupport)
+ {
+ // For backwards compat, we can disallow async support and just chain to the sync Execute() function.
+ Action action = () =>
+ {
+ Execute(requestContext);
+ };
+
+ return AsyncResultWrapper.BeginSynchronous(callback, state, action, _executeTag);
+ }
+ else
+ {
+ if (requestContext == null)
+ {
+ throw new ArgumentNullException("requestContext");
+ }
+
+ // Support Asynchronous behavior.
+ // Execute/ExecuteCore are no longer called.
+
+ VerifyExecuteCalledOnce();
+ Initialize(requestContext);
+ return AsyncResultWrapper.Begin(callback, state, BeginExecuteCore, EndExecuteCore, _executeTag);
+ }
+ }
+
+ protected virtual IAsyncResult BeginExecuteCore(AsyncCallback callback, object state)
+ {
+ // If code in this method needs to be updated, please also check the ExecuteCore() method
+ // of Controller to see if that code also must be updated.
+ PossiblyLoadTempData();
+ try
+ {
+ string actionName = RouteData.GetRequiredString("action");
+ IActionInvoker invoker = ActionInvoker;
+ IAsyncActionInvoker asyncInvoker = invoker as IAsyncActionInvoker;
+ if (asyncInvoker != null)
+ {
+ // asynchronous invocation
+ BeginInvokeDelegate beginDelegate = delegate(AsyncCallback asyncCallback, object asyncState)
+ {
+ return asyncInvoker.BeginInvokeAction(ControllerContext, actionName, asyncCallback, asyncState);
+ };
+
+ EndInvokeDelegate endDelegate = delegate(IAsyncResult asyncResult)
+ {
+ if (!asyncInvoker.EndInvokeAction(asyncResult))
+ {
+ HandleUnknownAction(actionName);
+ }
+ };
+
+ return AsyncResultWrapper.Begin(callback, state, beginDelegate, endDelegate, _executeCoreTag);
+ }
+ else
+ {
+ // synchronous invocation
+ Action action = () =>
+ {
+ if (!invoker.InvokeAction(ControllerContext, actionName))
+ {
+ HandleUnknownAction(actionName);
+ }
+ };
+ return AsyncResultWrapper.BeginSynchronous(callback, state, action, _executeCoreTag);
+ }
+ }
+ catch
+ {
+ PossiblySaveTempData();
+ throw;
+ }
+ }
+
+ protected virtual void EndExecute(IAsyncResult asyncResult)
+ {
+ AsyncResultWrapper.End(asyncResult, _executeTag);
+ }
+
+ protected virtual void EndExecuteCore(IAsyncResult asyncResult)
+ {
+ // If code in this method needs to be updated, please also check the ExecuteCore() method
+ // of Controller to see if that code also must be updated.
+
+ try
+ {
+ AsyncResultWrapper.End(asyncResult, _executeCoreTag);
+ }
+ finally
+ {
+ PossiblySaveTempData();
+ }
+ }
+
+ #region IActionFilter Members
+
+ void IActionFilter.OnActionExecuting(ActionExecutingContext filterContext)
+ {
+ OnActionExecuting(filterContext);
+ }
+
+ void IActionFilter.OnActionExecuted(ActionExecutedContext filterContext)
+ {
+ OnActionExecuted(filterContext);
+ }
+
+ #endregion
+
+ #region IAuthorizationFilter Members
+
+ void IAuthorizationFilter.OnAuthorization(AuthorizationContext filterContext)
+ {
+ OnAuthorization(filterContext);
+ }
+
+ #endregion
+
+ #region IExceptionFilter Members
+
+ void IExceptionFilter.OnException(ExceptionContext filterContext)
+ {
+ OnException(filterContext);
+ }
+
+ #endregion
+
+ #region IResultFilter Members
+
+ void IResultFilter.OnResultExecuting(ResultExecutingContext filterContext)
+ {
+ OnResultExecuting(filterContext);
+ }
+
+ void IResultFilter.OnResultExecuted(ResultExecutedContext filterContext)
+ {
+ OnResultExecuted(filterContext);
+ }
+
+ #endregion
+ }
+}
diff --git a/src/System.Web.Mvc/ControllerActionInvoker.cs b/src/System.Web.Mvc/ControllerActionInvoker.cs
new file mode 100644
index 00000000..64310b72
--- /dev/null
+++ b/src/System.Web.Mvc/ControllerActionInvoker.cs
@@ -0,0 +1,372 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using System.Threading;
+using System.Web.Mvc.Properties;
+using Microsoft.Web.Infrastructure.DynamicValidationHelper;
+
+namespace System.Web.Mvc
+{
+ public class ControllerActionInvoker : IActionInvoker
+ {
+ private static readonly ControllerDescriptorCache _staticDescriptorCache = new ControllerDescriptorCache();
+
+ private ModelBinderDictionary _binders;
+ private Func<ControllerContext, ActionDescriptor, IEnumerable<Filter>> _getFiltersThunk = FilterProviders.Providers.GetFilters;
+ private ControllerDescriptorCache _instanceDescriptorCache;
+
+ public ControllerActionInvoker()
+ {
+ }
+
+ internal ControllerActionInvoker(params object[] filters)
+ : this()
+ {
+ if (filters != null)
+ {
+ _getFiltersThunk = (cc, ad) => filters.Select(f => new Filter(f, FilterScope.Action, null));
+ }
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "Property is settable so that the dictionary can be provided for unit testing purposes.")]
+ protected internal ModelBinderDictionary Binders
+ {
+ get
+ {
+ if (_binders == null)
+ {
+ _binders = ModelBinders.Binders;
+ }
+ return _binders;
+ }
+ set { _binders = value; }
+ }
+
+ internal ControllerDescriptorCache DescriptorCache
+ {
+ get
+ {
+ if (_instanceDescriptorCache == null)
+ {
+ _instanceDescriptorCache = _staticDescriptorCache;
+ }
+ return _instanceDescriptorCache;
+ }
+ set { _instanceDescriptorCache = value; }
+ }
+
+ protected virtual ActionResult CreateActionResult(ControllerContext controllerContext, ActionDescriptor actionDescriptor, object actionReturnValue)
+ {
+ if (actionReturnValue == null)
+ {
+ return new EmptyResult();
+ }
+
+ ActionResult actionResult = (actionReturnValue as ActionResult) ??
+ new ContentResult { Content = Convert.ToString(actionReturnValue, CultureInfo.InvariantCulture) };
+ return actionResult;
+ }
+
+ protected virtual ControllerDescriptor GetControllerDescriptor(ControllerContext controllerContext)
+ {
+ Type controllerType = controllerContext.Controller.GetType();
+ ControllerDescriptor controllerDescriptor = DescriptorCache.GetDescriptor(controllerType, () => new ReflectedControllerDescriptor(controllerType));
+ return controllerDescriptor;
+ }
+
+ protected virtual ActionDescriptor FindAction(ControllerContext controllerContext, ControllerDescriptor controllerDescriptor, string actionName)
+ {
+ ActionDescriptor actionDescriptor = controllerDescriptor.FindAction(controllerContext, actionName);
+ return actionDescriptor;
+ }
+
+ protected virtual FilterInfo GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
+ {
+ return new FilterInfo(_getFiltersThunk(controllerContext, actionDescriptor));
+ }
+
+ private IModelBinder GetModelBinder(ParameterDescriptor parameterDescriptor)
+ {
+ // look on the parameter itself, then look in the global table
+ return parameterDescriptor.BindingInfo.Binder ?? Binders.GetBinder(parameterDescriptor.ParameterType);
+ }
+
+ protected virtual object GetParameterValue(ControllerContext controllerContext, ParameterDescriptor parameterDescriptor)
+ {
+ // collect all of the necessary binding properties
+ Type parameterType = parameterDescriptor.ParameterType;
+ IModelBinder binder = GetModelBinder(parameterDescriptor);
+ IValueProvider valueProvider = controllerContext.Controller.ValueProvider;
+ string parameterName = parameterDescriptor.BindingInfo.Prefix ?? parameterDescriptor.ParameterName;
+ Predicate<string> propertyFilter = GetPropertyFilter(parameterDescriptor);
+
+ // finally, call into the binder
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ FallbackToEmptyPrefix = (parameterDescriptor.BindingInfo.Prefix == null), // only fall back if prefix not specified
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, parameterType),
+ ModelName = parameterName,
+ ModelState = controllerContext.Controller.ViewData.ModelState,
+ PropertyFilter = propertyFilter,
+ ValueProvider = valueProvider
+ };
+
+ object result = binder.BindModel(controllerContext, bindingContext);
+ return result ?? parameterDescriptor.DefaultValue;
+ }
+
+ protected virtual IDictionary<string, object> GetParameterValues(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
+ {
+ Dictionary<string, object> parametersDict = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
+ ParameterDescriptor[] parameterDescriptors = actionDescriptor.GetParameters();
+
+ foreach (ParameterDescriptor parameterDescriptor in parameterDescriptors)
+ {
+ parametersDict[parameterDescriptor.ParameterName] = GetParameterValue(controllerContext, parameterDescriptor);
+ }
+ return parametersDict;
+ }
+
+ private static Predicate<string> GetPropertyFilter(ParameterDescriptor parameterDescriptor)
+ {
+ ParameterBindingInfo bindingInfo = parameterDescriptor.BindingInfo;
+ return propertyName => BindAttribute.IsPropertyAllowed(propertyName, bindingInfo.Include.ToArray(), bindingInfo.Exclude.ToArray());
+ }
+
+ public virtual bool InvokeAction(ControllerContext controllerContext, string actionName)
+ {
+ if (controllerContext == null)
+ {
+ throw new ArgumentNullException("controllerContext");
+ }
+ if (String.IsNullOrEmpty(actionName))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "actionName");
+ }
+
+ ControllerDescriptor controllerDescriptor = GetControllerDescriptor(controllerContext);
+ ActionDescriptor actionDescriptor = FindAction(controllerContext, controllerDescriptor, actionName);
+ if (actionDescriptor != null)
+ {
+ FilterInfo filterInfo = GetFilters(controllerContext, actionDescriptor);
+
+ try
+ {
+ AuthorizationContext authContext = InvokeAuthorizationFilters(controllerContext, filterInfo.AuthorizationFilters, actionDescriptor);
+ if (authContext.Result != null)
+ {
+ // the auth filter signaled that we should let it short-circuit the request
+ InvokeActionResult(controllerContext, authContext.Result);
+ }
+ else
+ {
+ if (controllerContext.Controller.ValidateRequest)
+ {
+ ValidateRequest(controllerContext);
+ }
+
+ IDictionary<string, object> parameters = GetParameterValues(controllerContext, actionDescriptor);
+ ActionExecutedContext postActionContext = InvokeActionMethodWithFilters(controllerContext, filterInfo.ActionFilters, actionDescriptor, parameters);
+ InvokeActionResultWithFilters(controllerContext, filterInfo.ResultFilters, postActionContext.Result);
+ }
+ }
+ catch (ThreadAbortException)
+ {
+ // This type of exception occurs as a result of Response.Redirect(), but we special-case so that
+ // the filters don't see this as an error.
+ throw;
+ }
+ catch (Exception ex)
+ {
+ // something blew up, so execute the exception filters
+ ExceptionContext exceptionContext = InvokeExceptionFilters(controllerContext, filterInfo.ExceptionFilters, ex);
+ if (!exceptionContext.ExceptionHandled)
+ {
+ throw;
+ }
+ InvokeActionResult(controllerContext, exceptionContext.Result);
+ }
+
+ return true;
+ }
+
+ // notify controller that no method matched
+ return false;
+ }
+
+ protected virtual ActionResult InvokeActionMethod(ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary<string, object> parameters)
+ {
+ object returnValue = actionDescriptor.Execute(controllerContext, parameters);
+ ActionResult result = CreateActionResult(controllerContext, actionDescriptor, returnValue);
+ return result;
+ }
+
+ internal static ActionExecutedContext InvokeActionMethodFilter(IActionFilter filter, ActionExecutingContext preContext, Func<ActionExecutedContext> continuation)
+ {
+ filter.OnActionExecuting(preContext);
+ if (preContext.Result != null)
+ {
+ return new ActionExecutedContext(preContext, preContext.ActionDescriptor, true /* canceled */, null /* exception */)
+ {
+ Result = preContext.Result
+ };
+ }
+
+ bool wasError = false;
+ ActionExecutedContext postContext = null;
+ try
+ {
+ postContext = continuation();
+ }
+ catch (ThreadAbortException)
+ {
+ // This type of exception occurs as a result of Response.Redirect(), but we special-case so that
+ // the filters don't see this as an error.
+ postContext = new ActionExecutedContext(preContext, preContext.ActionDescriptor, false /* canceled */, null /* exception */);
+ filter.OnActionExecuted(postContext);
+ throw;
+ }
+ catch (Exception ex)
+ {
+ wasError = true;
+ postContext = new ActionExecutedContext(preContext, preContext.ActionDescriptor, false /* canceled */, ex);
+ filter.OnActionExecuted(postContext);
+ if (!postContext.ExceptionHandled)
+ {
+ throw;
+ }
+ }
+ if (!wasError)
+ {
+ filter.OnActionExecuted(postContext);
+ }
+ return postContext;
+ }
+
+ protected virtual ActionExecutedContext InvokeActionMethodWithFilters(ControllerContext controllerContext, IList<IActionFilter> filters, ActionDescriptor actionDescriptor, IDictionary<string, object> parameters)
+ {
+ ActionExecutingContext preContext = new ActionExecutingContext(controllerContext, actionDescriptor, parameters);
+ Func<ActionExecutedContext> continuation = () =>
+ new ActionExecutedContext(controllerContext, actionDescriptor, false /* canceled */, null /* exception */)
+ {
+ Result = InvokeActionMethod(controllerContext, actionDescriptor, parameters)
+ };
+
+ // need to reverse the filter list because the continuations are built up backward
+ Func<ActionExecutedContext> thunk = filters.Reverse().Aggregate(continuation,
+ (next, filter) => () => InvokeActionMethodFilter(filter, preContext, next));
+ return thunk();
+ }
+
+ protected virtual void InvokeActionResult(ControllerContext controllerContext, ActionResult actionResult)
+ {
+ actionResult.ExecuteResult(controllerContext);
+ }
+
+ internal static ResultExecutedContext InvokeActionResultFilter(IResultFilter filter, ResultExecutingContext preContext, Func<ResultExecutedContext> continuation)
+ {
+ filter.OnResultExecuting(preContext);
+ if (preContext.Cancel)
+ {
+ return new ResultExecutedContext(preContext, preContext.Result, true /* canceled */, null /* exception */);
+ }
+
+ bool wasError = false;
+ ResultExecutedContext postContext = null;
+ try
+ {
+ postContext = continuation();
+ }
+ catch (ThreadAbortException)
+ {
+ // This type of exception occurs as a result of Response.Redirect(), but we special-case so that
+ // the filters don't see this as an error.
+ postContext = new ResultExecutedContext(preContext, preContext.Result, false /* canceled */, null /* exception */);
+ filter.OnResultExecuted(postContext);
+ throw;
+ }
+ catch (Exception ex)
+ {
+ wasError = true;
+ postContext = new ResultExecutedContext(preContext, preContext.Result, false /* canceled */, ex);
+ filter.OnResultExecuted(postContext);
+ if (!postContext.ExceptionHandled)
+ {
+ throw;
+ }
+ }
+ if (!wasError)
+ {
+ filter.OnResultExecuted(postContext);
+ }
+ return postContext;
+ }
+
+ protected virtual ResultExecutedContext InvokeActionResultWithFilters(ControllerContext controllerContext, IList<IResultFilter> filters, ActionResult actionResult)
+ {
+ ResultExecutingContext preContext = new ResultExecutingContext(controllerContext, actionResult);
+ Func<ResultExecutedContext> continuation = delegate
+ {
+ InvokeActionResult(controllerContext, actionResult);
+ return new ResultExecutedContext(controllerContext, actionResult, false /* canceled */, null /* exception */);
+ };
+
+ // need to reverse the filter list because the continuations are built up backward
+ Func<ResultExecutedContext> thunk = filters.Reverse().Aggregate(continuation,
+ (next, filter) => () => InvokeActionResultFilter(filter, preContext, next));
+ return thunk();
+ }
+
+ protected virtual AuthorizationContext InvokeAuthorizationFilters(ControllerContext controllerContext, IList<IAuthorizationFilter> filters, ActionDescriptor actionDescriptor)
+ {
+ AuthorizationContext context = new AuthorizationContext(controllerContext, actionDescriptor);
+ foreach (IAuthorizationFilter filter in filters)
+ {
+ filter.OnAuthorization(context);
+ // short-circuit evaluation
+ if (context.Result != null)
+ {
+ break;
+ }
+ }
+
+ return context;
+ }
+
+ protected virtual ExceptionContext InvokeExceptionFilters(ControllerContext controllerContext, IList<IExceptionFilter> filters, Exception exception)
+ {
+ ExceptionContext context = new ExceptionContext(controllerContext, exception);
+ foreach (IExceptionFilter filter in filters.Reverse())
+ {
+ filter.OnException(context);
+ }
+
+ return context;
+ }
+
+ internal static void ValidateRequest(ControllerContext controllerContext)
+ {
+ if (controllerContext.IsChildAction)
+ {
+ return;
+ }
+
+ // DevDiv 214040: Enable Request Validation by default for all controller requests
+ //
+ // Earlier versions of this method dereferenced Request.RawUrl to force validation of
+ // that field. This was necessary for Routing before ASP.NET v4, which read the incoming
+ // path from RawUrl. Request validation has been moved earlier in the pipeline by default and
+ // routing no longer consumes this property, so we don't have to either.
+
+ // Tolerate null HttpContext for testing
+ HttpContext currentContext = HttpContext.Current;
+ if (currentContext != null)
+ {
+ ValidationUtility.EnableDynamicValidation(currentContext);
+ }
+
+ controllerContext.HttpContext.Request.ValidateInput();
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ControllerBase.cs b/src/System.Web.Mvc/ControllerBase.cs
new file mode 100644
index 00000000..f58696c6
--- /dev/null
+++ b/src/System.Web.Mvc/ControllerBase.cs
@@ -0,0 +1,130 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Web.Mvc.Async;
+using System.Web.Mvc.Properties;
+using System.Web.Routing;
+using System.Web.WebPages.Scope;
+
+namespace System.Web.Mvc
+{
+ public abstract class ControllerBase : IController
+ {
+ private readonly SingleEntryGate _executeWasCalledGate = new SingleEntryGate();
+
+ private DynamicViewDataDictionary _dynamicViewDataDictionary;
+ private TempDataDictionary _tempDataDictionary;
+ private bool _validateRequest = true;
+ private IValueProvider _valueProvider;
+ private ViewDataDictionary _viewDataDictionary;
+
+ public ControllerContext ControllerContext { get; set; }
+
+ [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This property is settable so that unit tests can provide mock implementations.")]
+ public TempDataDictionary TempData
+ {
+ get
+ {
+ if (ControllerContext != null && ControllerContext.IsChildAction)
+ {
+ return ControllerContext.ParentActionViewContext.TempData;
+ }
+ if (_tempDataDictionary == null)
+ {
+ _tempDataDictionary = new TempDataDictionary();
+ }
+ return _tempDataDictionary;
+ }
+ set { _tempDataDictionary = value; }
+ }
+
+ public bool ValidateRequest
+ {
+ get { return _validateRequest; }
+ set { _validateRequest = value; }
+ }
+
+ public IValueProvider ValueProvider
+ {
+ get
+ {
+ if (_valueProvider == null)
+ {
+ _valueProvider = ValueProviderFactories.Factories.GetValueProvider(ControllerContext);
+ }
+ return _valueProvider;
+ }
+ set { _valueProvider = value; }
+ }
+
+ public dynamic ViewBag
+ {
+ get
+ {
+ if (_dynamicViewDataDictionary == null)
+ {
+ _dynamicViewDataDictionary = new DynamicViewDataDictionary(() => ViewData);
+ }
+ return _dynamicViewDataDictionary;
+ }
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This property is settable so that unit tests can provide mock implementations.")]
+ public ViewDataDictionary ViewData
+ {
+ get
+ {
+ if (_viewDataDictionary == null)
+ {
+ _viewDataDictionary = new ViewDataDictionary();
+ }
+ return _viewDataDictionary;
+ }
+ set { _viewDataDictionary = value; }
+ }
+
+ protected virtual void Execute(RequestContext requestContext)
+ {
+ if (requestContext == null)
+ {
+ throw new ArgumentNullException("requestContext");
+ }
+ if (requestContext.HttpContext == null)
+ {
+ throw new ArgumentException(MvcResources.ControllerBase_CannotExecuteWithNullHttpContext, "requestContext");
+ }
+
+ VerifyExecuteCalledOnce();
+ Initialize(requestContext);
+
+ using (ScopeStorage.CreateTransientScope())
+ {
+ ExecuteCore();
+ }
+ }
+
+ protected abstract void ExecuteCore();
+
+ protected virtual void Initialize(RequestContext requestContext)
+ {
+ ControllerContext = new ControllerContext(requestContext, this);
+ }
+
+ internal void VerifyExecuteCalledOnce()
+ {
+ if (!_executeWasCalledGate.TryEnter())
+ {
+ string message = String.Format(CultureInfo.CurrentCulture, MvcResources.ControllerBase_CannotHandleMultipleRequests, GetType());
+ throw new InvalidOperationException(message);
+ }
+ }
+
+ #region IController Members
+
+ void IController.Execute(RequestContext requestContext)
+ {
+ Execute(requestContext);
+ }
+
+ #endregion
+ }
+}
diff --git a/src/System.Web.Mvc/ControllerBuilder.cs b/src/System.Web.Mvc/ControllerBuilder.cs
new file mode 100644
index 00000000..004f5361
--- /dev/null
+++ b/src/System.Web.Mvc/ControllerBuilder.cs
@@ -0,0 +1,88 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc
+{
+ public class ControllerBuilder
+ {
+ private static ControllerBuilder _instance = new ControllerBuilder();
+ private Func<IControllerFactory> _factoryThunk = () => null;
+ private HashSet<string> _namespaces = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+ private IResolver<IControllerFactory> _serviceResolver;
+
+ public ControllerBuilder()
+ : this(null)
+ {
+ }
+
+ internal ControllerBuilder(IResolver<IControllerFactory> serviceResolver)
+ {
+ _serviceResolver = serviceResolver ?? new SingleServiceResolver<IControllerFactory>(
+ () => _factoryThunk(),
+ new DefaultControllerFactory { ControllerBuilder = this },
+ "ControllerBuilder.GetControllerFactory");
+ }
+
+ public static ControllerBuilder Current
+ {
+ get { return _instance; }
+ }
+
+ public HashSet<string> DefaultNamespaces
+ {
+ get { return _namespaces; }
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Calling method multiple times might return different objects.")]
+ public IControllerFactory GetControllerFactory()
+ {
+ return _serviceResolver.Current;
+ }
+
+ public void SetControllerFactory(IControllerFactory controllerFactory)
+ {
+ if (controllerFactory == null)
+ {
+ throw new ArgumentNullException("controllerFactory");
+ }
+
+ _factoryThunk = () => controllerFactory;
+ }
+
+ public void SetControllerFactory(Type controllerFactoryType)
+ {
+ if (controllerFactoryType == null)
+ {
+ throw new ArgumentNullException("controllerFactoryType");
+ }
+ if (!typeof(IControllerFactory).IsAssignableFrom(controllerFactoryType))
+ {
+ throw new ArgumentException(
+ String.Format(
+ CultureInfo.CurrentCulture,
+ MvcResources.ControllerBuilder_MissingIControllerFactory,
+ controllerFactoryType),
+ "controllerFactoryType");
+ }
+
+ _factoryThunk = delegate
+ {
+ try
+ {
+ return (IControllerFactory)Activator.CreateInstance(controllerFactoryType);
+ }
+ catch (Exception ex)
+ {
+ throw new InvalidOperationException(
+ String.Format(
+ CultureInfo.CurrentCulture,
+ MvcResources.ControllerBuilder_ErrorCreatingControllerFactory,
+ controllerFactoryType),
+ ex);
+ }
+ };
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ControllerContext.cs b/src/System.Web.Mvc/ControllerContext.cs
new file mode 100644
index 00000000..54bfcd46
--- /dev/null
+++ b/src/System.Web.Mvc/ControllerContext.cs
@@ -0,0 +1,133 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Web.Routing;
+using System.Web.WebPages;
+
+namespace System.Web.Mvc
+{
+ // Though many of the properties on ControllerContext and its subclassed types are virtual, there are still sealed
+ // properties (like ControllerContext.RequestContext, ActionExecutingContext.Result, etc.). If these properties
+ // were virtual, a mocking framework might override them with incorrect behavior (property getters would return
+ // null, property setters would be no-ops). By sealing these properties, we are forcing them to have the default
+ // "get or store a value" semantics that they were intended to have.
+
+ public class ControllerContext
+ {
+ internal const string ParentActionViewContextToken = "ParentActionViewContext";
+ private HttpContextBase _httpContext;
+ private RequestContext _requestContext;
+ private RouteData _routeData;
+
+ // parameterless constructor used for mocking
+ public ControllerContext()
+ {
+ }
+
+ // copy constructor - allows for subclassed types to take an existing ControllerContext as a parameter
+ // and we'll automatically set the appropriate properties
+ [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "The virtual property setters are only to support mocking frameworks, in which case this constructor shouldn't be called anyway.")]
+ protected ControllerContext(ControllerContext controllerContext)
+ {
+ if (controllerContext == null)
+ {
+ throw new ArgumentNullException("controllerContext");
+ }
+
+ Controller = controllerContext.Controller;
+ RequestContext = controllerContext.RequestContext;
+ }
+
+ public ControllerContext(HttpContextBase httpContext, RouteData routeData, ControllerBase controller)
+ : this(new RequestContext(httpContext, routeData), controller)
+ {
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "The virtual property setters are only to support mocking frameworks, in which case this constructor shouldn't be called anyway.")]
+ public ControllerContext(RequestContext requestContext, ControllerBase controller)
+ {
+ if (requestContext == null)
+ {
+ throw new ArgumentNullException("requestContext");
+ }
+ if (controller == null)
+ {
+ throw new ArgumentNullException("controller");
+ }
+
+ RequestContext = requestContext;
+ Controller = controller;
+ }
+
+ public virtual ControllerBase Controller { get; set; }
+
+ public IDisplayMode DisplayMode
+ {
+ get { return DisplayModeProvider.GetDisplayMode(HttpContext); }
+ set { DisplayModeProvider.SetDisplayMode(HttpContext, value); }
+ }
+
+ public virtual HttpContextBase HttpContext
+ {
+ get
+ {
+ if (_httpContext == null)
+ {
+ _httpContext = (_requestContext != null) ? _requestContext.HttpContext : new EmptyHttpContext();
+ }
+ return _httpContext;
+ }
+ set { _httpContext = value; }
+ }
+
+ public virtual bool IsChildAction
+ {
+ get
+ {
+ RouteData routeData = RouteData;
+ if (routeData == null)
+ {
+ return false;
+ }
+ return routeData.DataTokens.ContainsKey(ParentActionViewContextToken);
+ }
+ }
+
+ public ViewContext ParentActionViewContext
+ {
+ get { return RouteData.DataTokens[ParentActionViewContextToken] as ViewContext; }
+ }
+
+ public RequestContext RequestContext
+ {
+ get
+ {
+ if (_requestContext == null)
+ {
+ // still need explicit calls to constructors since the property getters are virtual and might return null
+ HttpContextBase httpContext = HttpContext ?? new EmptyHttpContext();
+ RouteData routeData = RouteData ?? new RouteData();
+
+ _requestContext = new RequestContext(httpContext, routeData);
+ }
+ return _requestContext;
+ }
+ set { _requestContext = value; }
+ }
+
+ public virtual RouteData RouteData
+ {
+ get
+ {
+ if (_routeData == null)
+ {
+ _routeData = (_requestContext != null) ? _requestContext.RouteData : new RouteData();
+ }
+ return _routeData;
+ }
+ set { _routeData = value; }
+ }
+
+ private sealed class EmptyHttpContext : HttpContextBase
+ {
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ControllerDescriptor.cs b/src/System.Web.Mvc/ControllerDescriptor.cs
new file mode 100644
index 00000000..45c0af9b
--- /dev/null
+++ b/src/System.Web.Mvc/ControllerDescriptor.cs
@@ -0,0 +1,78 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Reflection;
+
+namespace System.Web.Mvc
+{
+ public abstract class ControllerDescriptor : ICustomAttributeProvider, IUniquelyIdentifiable
+ {
+ private readonly Lazy<string> _uniqueId;
+
+ protected ControllerDescriptor()
+ {
+ _uniqueId = new Lazy<string>(CreateUniqueId);
+ }
+
+ public virtual string ControllerName
+ {
+ get
+ {
+ string typeName = ControllerType.Name;
+ if (typeName.EndsWith("Controller", StringComparison.OrdinalIgnoreCase))
+ {
+ return typeName.Substring(0, typeName.Length - "Controller".Length);
+ }
+
+ return typeName;
+ }
+ }
+
+ public abstract Type ControllerType { get; }
+
+ [SuppressMessage("Microsoft.Security", "CA2119:SealMethodsThatSatisfyPrivateInterfaces", Justification = "This is overridden elsewhere in System.Web.Mvc")]
+ public virtual string UniqueId
+ {
+ get { return _uniqueId.Value; }
+ }
+
+ private string CreateUniqueId()
+ {
+ return DescriptorUtil.CreateUniqueId(GetType(), ControllerName, ControllerType);
+ }
+
+ public abstract ActionDescriptor FindAction(ControllerContext controllerContext, string actionName);
+
+ public abstract ActionDescriptor[] GetCanonicalActions();
+
+ public virtual object[] GetCustomAttributes(bool inherit)
+ {
+ return GetCustomAttributes(typeof(object), inherit);
+ }
+
+ public virtual object[] GetCustomAttributes(Type attributeType, bool inherit)
+ {
+ if (attributeType == null)
+ {
+ throw new ArgumentNullException("attributeType");
+ }
+
+ return (object[])Array.CreateInstance(attributeType, 0);
+ }
+
+ public virtual IEnumerable<FilterAttribute> GetFilterAttributes(bool useCache)
+ {
+ return GetCustomAttributes(typeof(FilterAttribute), inherit: true).Cast<FilterAttribute>();
+ }
+
+ public virtual bool IsDefined(Type attributeType, bool inherit)
+ {
+ if (attributeType == null)
+ {
+ throw new ArgumentNullException("attributeType");
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ControllerDescriptorCache.cs b/src/System.Web.Mvc/ControllerDescriptorCache.cs
new file mode 100644
index 00000000..538e03a0
--- /dev/null
+++ b/src/System.Web.Mvc/ControllerDescriptorCache.cs
@@ -0,0 +1,14 @@
+namespace System.Web.Mvc
+{
+ internal sealed class ControllerDescriptorCache : ReaderWriterCache<Type, ControllerDescriptor>
+ {
+ public ControllerDescriptorCache()
+ {
+ }
+
+ public ControllerDescriptor GetDescriptor(Type controllerType, Func<ControllerDescriptor> creator)
+ {
+ return FetchOrCreateItem(controllerType, creator);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ControllerInstanceFilterProvider.cs b/src/System.Web.Mvc/ControllerInstanceFilterProvider.cs
new file mode 100644
index 00000000..89ee3350
--- /dev/null
+++ b/src/System.Web.Mvc/ControllerInstanceFilterProvider.cs
@@ -0,0 +1,16 @@
+using System.Collections.Generic;
+
+namespace System.Web.Mvc
+{
+ public class ControllerInstanceFilterProvider : IFilterProvider
+ {
+ public IEnumerable<Filter> GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
+ {
+ if (controllerContext.Controller != null)
+ {
+ // Use FilterScope.First and Order of Int32.MinValue to ensure controller instance methods always run first
+ yield return new Filter(controllerContext.Controller, FilterScope.First, Int32.MinValue);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ControllerTypeCache.cs b/src/System.Web.Mvc/ControllerTypeCache.cs
new file mode 100644
index 00000000..7b560f4a
--- /dev/null
+++ b/src/System.Web.Mvc/ControllerTypeCache.cs
@@ -0,0 +1,138 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace System.Web.Mvc
+{
+ internal sealed class ControllerTypeCache
+ {
+ private const string TypeCacheName = "MVC-ControllerTypeCache.xml";
+
+ private Dictionary<string, ILookup<string, Type>> _cache;
+ private object _lockObj = new object();
+
+ internal int Count
+ {
+ get
+ {
+ int count = 0;
+ foreach (var lookup in _cache.Values)
+ {
+ foreach (var grouping in lookup)
+ {
+ count += grouping.Count();
+ }
+ }
+ return count;
+ }
+ }
+
+ public void EnsureInitialized(IBuildManager buildManager)
+ {
+ if (_cache == null)
+ {
+ lock (_lockObj)
+ {
+ if (_cache == null)
+ {
+ List<Type> controllerTypes = TypeCacheUtil.GetFilteredTypesFromAssemblies(TypeCacheName, IsControllerType, buildManager);
+ var groupedByName = controllerTypes.GroupBy(
+ t => t.Name.Substring(0, t.Name.Length - "Controller".Length),
+ StringComparer.OrdinalIgnoreCase);
+ _cache = groupedByName.ToDictionary(
+ g => g.Key,
+ g => g.ToLookup(t => t.Namespace ?? String.Empty, StringComparer.OrdinalIgnoreCase),
+ StringComparer.OrdinalIgnoreCase);
+ }
+ }
+ }
+ }
+
+ public ICollection<Type> GetControllerTypes(string controllerName, HashSet<string> namespaces)
+ {
+ HashSet<Type> matchingTypes = new HashSet<Type>();
+
+ ILookup<string, Type> namespaceLookup;
+ if (_cache.TryGetValue(controllerName, out namespaceLookup))
+ {
+ // this friendly name was located in the cache, now cycle through namespaces
+ if (namespaces != null)
+ {
+ foreach (string requestedNamespace in namespaces)
+ {
+ foreach (var targetNamespaceGrouping in namespaceLookup)
+ {
+ if (IsNamespaceMatch(requestedNamespace, targetNamespaceGrouping.Key))
+ {
+ matchingTypes.UnionWith(targetNamespaceGrouping);
+ }
+ }
+ }
+ }
+ else
+ {
+ // if the namespaces parameter is null, search *every* namespace
+ foreach (var namespaceGroup in namespaceLookup)
+ {
+ matchingTypes.UnionWith(namespaceGroup);
+ }
+ }
+ }
+
+ return matchingTypes;
+ }
+
+ internal static bool IsControllerType(Type t)
+ {
+ return
+ t != null &&
+ t.IsPublic &&
+ t.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) &&
+ !t.IsAbstract &&
+ typeof(IController).IsAssignableFrom(t);
+ }
+
+ internal static bool IsNamespaceMatch(string requestedNamespace, string targetNamespace)
+ {
+ // degenerate cases
+ if (requestedNamespace == null)
+ {
+ return false;
+ }
+ else if (requestedNamespace.Length == 0)
+ {
+ return true;
+ }
+
+ if (!requestedNamespace.EndsWith(".*", StringComparison.OrdinalIgnoreCase))
+ {
+ // looking for exact namespace match
+ return String.Equals(requestedNamespace, targetNamespace, StringComparison.OrdinalIgnoreCase);
+ }
+ else
+ {
+ // looking for exact or sub-namespace match
+ requestedNamespace = requestedNamespace.Substring(0, requestedNamespace.Length - ".*".Length);
+ if (!targetNamespace.StartsWith(requestedNamespace, StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ if (requestedNamespace.Length == targetNamespace.Length)
+ {
+ // exact match
+ return true;
+ }
+ else if (targetNamespace[requestedNamespace.Length] == '.')
+ {
+ // good prefix match, e.g. requestedNamespace = "Foo.Bar" and targetNamespace = "Foo.Bar.Baz"
+ return true;
+ }
+ else
+ {
+ // bad prefix match, e.g. requestedNamespace = "Foo.Bar" and targetNamespace = "Foo.Bar2"
+ return false;
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/CustomModelBinderAttribute.cs b/src/System.Web.Mvc/CustomModelBinderAttribute.cs
new file mode 100644
index 00000000..c2e676f9
--- /dev/null
+++ b/src/System.Web.Mvc/CustomModelBinderAttribute.cs
@@ -0,0 +1,13 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Web.Mvc
+{
+ [AttributeUsage(ValidTargets, AllowMultiple = false, Inherited = false)]
+ public abstract class CustomModelBinderAttribute : Attribute
+ {
+ internal const AttributeTargets ValidTargets = AttributeTargets.Class | AttributeTargets.Enum | AttributeTargets.Interface | AttributeTargets.Parameter | AttributeTargets.Struct;
+
+ [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This method can potentially perform a non-trivial amount of work.")]
+ public abstract IModelBinder GetBinder();
+ }
+}
diff --git a/src/System.Web.Mvc/DataAnnotationsModelMetadata.cs b/src/System.Web.Mvc/DataAnnotationsModelMetadata.cs
new file mode 100644
index 00000000..9b8156e3
--- /dev/null
+++ b/src/System.Web.Mvc/DataAnnotationsModelMetadata.cs
@@ -0,0 +1,60 @@
+using System.ComponentModel.DataAnnotations;
+using System.Globalization;
+using System.Reflection;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc
+{
+ public class DataAnnotationsModelMetadata : ModelMetadata
+ {
+ private DisplayColumnAttribute _displayColumnAttribute;
+
+ public DataAnnotationsModelMetadata(DataAnnotationsModelMetadataProvider provider, Type containerType,
+ Func<object> modelAccessor, Type modelType, string propertyName,
+ DisplayColumnAttribute displayColumnAttribute)
+ : base(provider, containerType, modelAccessor, modelType, propertyName)
+ {
+ _displayColumnAttribute = displayColumnAttribute;
+ }
+
+ protected override string GetSimpleDisplayText()
+ {
+ if (Model != null)
+ {
+ if (_displayColumnAttribute != null && !String.IsNullOrEmpty(_displayColumnAttribute.DisplayColumn))
+ {
+ PropertyInfo displayColumnProperty = ModelType.GetProperty(_displayColumnAttribute.DisplayColumn, BindingFlags.Public | BindingFlags.IgnoreCase | BindingFlags.Instance);
+ ValidateDisplayColumnAttribute(_displayColumnAttribute, displayColumnProperty, ModelType);
+
+ object simpleDisplayTextValue = displayColumnProperty.GetValue(Model, new object[0]);
+ if (simpleDisplayTextValue != null)
+ {
+ return simpleDisplayTextValue.ToString();
+ }
+ }
+ }
+
+ return base.GetSimpleDisplayText();
+ }
+
+ private static void ValidateDisplayColumnAttribute(DisplayColumnAttribute displayColumnAttribute, PropertyInfo displayColumnProperty, Type modelType)
+ {
+ if (displayColumnProperty == null)
+ {
+ throw new InvalidOperationException(
+ String.Format(
+ CultureInfo.CurrentCulture,
+ MvcResources.DataAnnotationsModelMetadataProvider_UnknownProperty,
+ modelType.FullName, displayColumnAttribute.DisplayColumn));
+ }
+ if (displayColumnProperty.GetGetMethod() == null)
+ {
+ throw new InvalidOperationException(
+ String.Format(
+ CultureInfo.CurrentCulture,
+ MvcResources.DataAnnotationsModelMetadataProvider_UnreadableProperty,
+ modelType.FullName, displayColumnAttribute.DisplayColumn));
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/DataAnnotationsModelMetadataProvider.cs b/src/System.Web.Mvc/DataAnnotationsModelMetadataProvider.cs
new file mode 100644
index 00000000..7ffef9c7
--- /dev/null
+++ b/src/System.Web.Mvc/DataAnnotationsModelMetadataProvider.cs
@@ -0,0 +1,115 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+
+namespace System.Web.Mvc
+{
+ public class DataAnnotationsModelMetadataProvider : AssociatedMetadataProvider
+ {
+ protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
+ {
+ List<Attribute> attributeList = new List<Attribute>(attributes);
+ DisplayColumnAttribute displayColumnAttribute = attributeList.OfType<DisplayColumnAttribute>().FirstOrDefault();
+ DataAnnotationsModelMetadata result = new DataAnnotationsModelMetadata(this, containerType, modelAccessor, modelType, propertyName, displayColumnAttribute);
+
+ // Do [HiddenInput] before [UIHint], so you can override the template hint
+ HiddenInputAttribute hiddenInputAttribute = attributeList.OfType<HiddenInputAttribute>().FirstOrDefault();
+ if (hiddenInputAttribute != null)
+ {
+ result.TemplateHint = "HiddenInput";
+ result.HideSurroundingHtml = !hiddenInputAttribute.DisplayValue;
+ }
+
+ // We prefer [UIHint("...", PresentationLayer = "MVC")] but will fall back to [UIHint("...")]
+ IEnumerable<UIHintAttribute> uiHintAttributes = attributeList.OfType<UIHintAttribute>();
+ UIHintAttribute uiHintAttribute = uiHintAttributes.FirstOrDefault(a => String.Equals(a.PresentationLayer, "MVC", StringComparison.OrdinalIgnoreCase))
+ ?? uiHintAttributes.FirstOrDefault(a => String.IsNullOrEmpty(a.PresentationLayer));
+ if (uiHintAttribute != null)
+ {
+ result.TemplateHint = uiHintAttribute.UIHint;
+ }
+
+ DataTypeAttribute dataTypeAttribute = attributeList.OfType<DataTypeAttribute>().FirstOrDefault();
+ if (dataTypeAttribute != null)
+ {
+ result.DataTypeName = dataTypeAttribute.ToDataTypeName();
+ }
+
+ EditableAttribute editable = attributes.OfType<EditableAttribute>().FirstOrDefault();
+ if (editable != null)
+ {
+ result.IsReadOnly = !editable.AllowEdit;
+ }
+ else
+ {
+ ReadOnlyAttribute readOnlyAttribute = attributeList.OfType<ReadOnlyAttribute>().FirstOrDefault();
+ if (readOnlyAttribute != null)
+ {
+ result.IsReadOnly = readOnlyAttribute.IsReadOnly;
+ }
+ }
+
+ DisplayFormatAttribute displayFormatAttribute = attributeList.OfType<DisplayFormatAttribute>().FirstOrDefault();
+ if (displayFormatAttribute == null && dataTypeAttribute != null)
+ {
+ displayFormatAttribute = dataTypeAttribute.DisplayFormat;
+ }
+ if (displayFormatAttribute != null)
+ {
+ result.NullDisplayText = displayFormatAttribute.NullDisplayText;
+ result.DisplayFormatString = displayFormatAttribute.DataFormatString;
+ result.ConvertEmptyStringToNull = displayFormatAttribute.ConvertEmptyStringToNull;
+
+ if (displayFormatAttribute.ApplyFormatInEditMode)
+ {
+ result.EditFormatString = displayFormatAttribute.DataFormatString;
+ }
+
+ if (!displayFormatAttribute.HtmlEncode && String.IsNullOrWhiteSpace(result.DataTypeName))
+ {
+ result.DataTypeName = DataTypeUtil.HtmlTypeName;
+ }
+ }
+
+ ScaffoldColumnAttribute scaffoldColumnAttribute = attributeList.OfType<ScaffoldColumnAttribute>().FirstOrDefault();
+ if (scaffoldColumnAttribute != null)
+ {
+ result.ShowForDisplay = result.ShowForEdit = scaffoldColumnAttribute.Scaffold;
+ }
+
+ DisplayAttribute display = attributes.OfType<DisplayAttribute>().FirstOrDefault();
+ string name = null;
+ if (display != null)
+ {
+ result.Description = display.GetDescription();
+ result.ShortDisplayName = display.GetShortName();
+ result.Watermark = display.GetPrompt();
+ result.Order = display.GetOrder() ?? ModelMetadata.DefaultOrder;
+
+ name = display.GetName();
+ }
+
+ if (name != null)
+ {
+ result.DisplayName = name;
+ }
+ else
+ {
+ DisplayNameAttribute displayNameAttribute = attributeList.OfType<DisplayNameAttribute>().FirstOrDefault();
+ if (displayNameAttribute != null)
+ {
+ result.DisplayName = displayNameAttribute.DisplayName;
+ }
+ }
+
+ RequiredAttribute requiredAttribute = attributeList.OfType<RequiredAttribute>().FirstOrDefault();
+ if (requiredAttribute != null)
+ {
+ result.IsRequired = true;
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/DataAnnotationsModelValidator.cs b/src/System.Web.Mvc/DataAnnotationsModelValidator.cs
new file mode 100644
index 00000000..1bb8952b
--- /dev/null
+++ b/src/System.Web.Mvc/DataAnnotationsModelValidator.cs
@@ -0,0 +1,67 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+
+namespace System.Web.Mvc
+{
+ public class DataAnnotationsModelValidator : ModelValidator
+ {
+ public DataAnnotationsModelValidator(ModelMetadata metadata, ControllerContext context, ValidationAttribute attribute)
+ : base(metadata, context)
+ {
+ if (attribute == null)
+ {
+ throw new ArgumentNullException("attribute");
+ }
+
+ Attribute = attribute;
+ }
+
+ protected internal ValidationAttribute Attribute { get; private set; }
+
+ protected internal string ErrorMessage
+ {
+ get { return Attribute.FormatErrorMessage(Metadata.GetDisplayName()); }
+ }
+
+ public override bool IsRequired
+ {
+ get { return Attribute is RequiredAttribute; }
+ }
+
+ internal static ModelValidator Create(ModelMetadata metadata, ControllerContext context, ValidationAttribute attribute)
+ {
+ return new DataAnnotationsModelValidator(metadata, context, attribute);
+ }
+
+ public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
+ {
+ IEnumerable<ModelClientValidationRule> results = base.GetClientValidationRules();
+
+ IClientValidatable clientValidatable = Attribute as IClientValidatable;
+ if (clientValidatable != null)
+ {
+ results = results.Concat(clientValidatable.GetClientValidationRules(Metadata, ControllerContext));
+ }
+
+ return results;
+ }
+
+ public override IEnumerable<ModelValidationResult> Validate(object container)
+ {
+ // Per the WCF RIA Services team, instance can never be null (if you have
+ // no parent, you pass yourself for the "instance" parameter).
+ ValidationContext context = new ValidationContext(container ?? Metadata.Model, null, null);
+ context.DisplayName = Metadata.GetDisplayName();
+
+ ValidationResult result = Attribute.GetValidationResult(Metadata.Model, context);
+ if (result != ValidationResult.Success)
+ {
+ yield return new ModelValidationResult
+ {
+ Message = result.ErrorMessage
+ };
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/DataAnnotationsModelValidatorProvider.cs b/src/System.Web.Mvc/DataAnnotationsModelValidatorProvider.cs
new file mode 100644
index 00000000..26d80c96
--- /dev/null
+++ b/src/System.Web.Mvc/DataAnnotationsModelValidatorProvider.cs
@@ -0,0 +1,375 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Globalization;
+using System.Linq;
+using System.Reflection;
+using System.Threading;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc
+{
+ // A factory for validators based on ValidationAttribute
+ public delegate ModelValidator DataAnnotationsModelValidationFactory(ModelMetadata metadata, ControllerContext context, ValidationAttribute attribute);
+
+ // A factory for validators based on IValidatableObject
+ public delegate ModelValidator DataAnnotationsValidatableObjectAdapterFactory(ModelMetadata metadata, ControllerContext context);
+
+ /// <summary>
+ /// An implementation of <see cref="ModelValidatorProvider"/> which providers validators
+ /// for attributes which derive from <see cref="ValidationAttribute"/>. It also provides
+ /// a validator for types which implement <see cref="IValidatableObject"/>. To support
+ /// client side validation, you can either register adapters through the static methods
+ /// on this class, or by having your validation attributes implement
+ /// <see cref="IClientValidatable"/>. The logic to support IClientValidatable
+ /// is implemented in <see cref="DataAnnotationsModelValidator"/>.
+ /// </summary>
+ public class DataAnnotationsModelValidatorProvider : AssociatedValidatorProvider
+ {
+ private static bool _addImplicitRequiredAttributeForValueTypes = true;
+ private static ReaderWriterLockSlim _adaptersLock = new ReaderWriterLockSlim();
+
+ // Factories for validation attributes
+
+ internal static DataAnnotationsModelValidationFactory DefaultAttributeFactory =
+ (metadata, context, attribute) => new DataAnnotationsModelValidator(metadata, context, attribute);
+
+ internal static Dictionary<Type, DataAnnotationsModelValidationFactory> AttributeFactories = new Dictionary<Type, DataAnnotationsModelValidationFactory>()
+ {
+ {
+ typeof(RangeAttribute),
+ (metadata, context, attribute) => new RangeAttributeAdapter(metadata, context, (RangeAttribute)attribute)
+ },
+ {
+ typeof(RegularExpressionAttribute),
+ (metadata, context, attribute) => new RegularExpressionAttributeAdapter(metadata, context, (RegularExpressionAttribute)attribute)
+ },
+ {
+ typeof(RequiredAttribute),
+ (metadata, context, attribute) => new RequiredAttributeAdapter(metadata, context, (RequiredAttribute)attribute)
+ },
+ {
+ typeof(StringLengthAttribute),
+ (metadata, context, attribute) => new StringLengthAttributeAdapter(metadata, context, (StringLengthAttribute)attribute)
+ },
+ };
+
+ // Factories for IValidatableObject models
+
+ internal static DataAnnotationsValidatableObjectAdapterFactory DefaultValidatableFactory =
+ (metadata, context) => new ValidatableObjectAdapter(metadata, context);
+
+ internal static Dictionary<Type, DataAnnotationsValidatableObjectAdapterFactory> ValidatableFactories = new Dictionary<Type, DataAnnotationsValidatableObjectAdapterFactory>();
+
+ public static bool AddImplicitRequiredAttributeForValueTypes
+ {
+ get { return _addImplicitRequiredAttributeForValueTypes; }
+ set { _addImplicitRequiredAttributeForValueTypes = value; }
+ }
+
+ protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes)
+ {
+ _adaptersLock.EnterReadLock();
+
+ try
+ {
+ List<ModelValidator> results = new List<ModelValidator>();
+
+ // Add an implied [Required] attribute for any non-nullable value type,
+ // unless they've configured us not to do that.
+ if (AddImplicitRequiredAttributeForValueTypes &&
+ metadata.IsRequired &&
+ !attributes.Any(a => a is RequiredAttribute))
+ {
+ attributes = attributes.Concat(new[] { new RequiredAttribute() });
+ }
+
+ // Produce a validator for each validation attribute we find
+ foreach (ValidationAttribute attribute in attributes.OfType<ValidationAttribute>())
+ {
+ DataAnnotationsModelValidationFactory factory;
+ if (!AttributeFactories.TryGetValue(attribute.GetType(), out factory))
+ {
+ factory = DefaultAttributeFactory;
+ }
+ results.Add(factory(metadata, context, attribute));
+ }
+
+ // Produce a validator if the type supports IValidatableObject
+ if (typeof(IValidatableObject).IsAssignableFrom(metadata.ModelType))
+ {
+ DataAnnotationsValidatableObjectAdapterFactory factory;
+ if (!ValidatableFactories.TryGetValue(metadata.ModelType, out factory))
+ {
+ factory = DefaultValidatableFactory;
+ }
+ results.Add(factory(metadata, context));
+ }
+
+ return results;
+ }
+ finally
+ {
+ _adaptersLock.ExitReadLock();
+ }
+ }
+
+ #region Validation attribute adapter registration
+
+ public static void RegisterAdapter(Type attributeType, Type adapterType)
+ {
+ ValidateAttributeType(attributeType);
+ ValidateAttributeAdapterType(adapterType);
+ ConstructorInfo constructor = GetAttributeAdapterConstructor(attributeType, adapterType);
+
+ _adaptersLock.EnterWriteLock();
+
+ try
+ {
+ AttributeFactories[attributeType] = (metadata, context, attribute) => (ModelValidator)constructor.Invoke(new object[] { metadata, context, attribute });
+ }
+ finally
+ {
+ _adaptersLock.ExitWriteLock();
+ }
+ }
+
+ public static void RegisterAdapterFactory(Type attributeType, DataAnnotationsModelValidationFactory factory)
+ {
+ ValidateAttributeType(attributeType);
+ ValidateAttributeFactory(factory);
+
+ _adaptersLock.EnterWriteLock();
+
+ try
+ {
+ AttributeFactories[attributeType] = factory;
+ }
+ finally
+ {
+ _adaptersLock.ExitWriteLock();
+ }
+ }
+
+ public static void RegisterDefaultAdapter(Type adapterType)
+ {
+ ValidateAttributeAdapterType(adapterType);
+ ConstructorInfo constructor = GetAttributeAdapterConstructor(typeof(ValidationAttribute), adapterType);
+
+ DefaultAttributeFactory = (metadata, context, attribute) => (ModelValidator)constructor.Invoke(new object[] { metadata, context, attribute });
+ }
+
+ public static void RegisterDefaultAdapterFactory(DataAnnotationsModelValidationFactory factory)
+ {
+ ValidateAttributeFactory(factory);
+
+ DefaultAttributeFactory = factory;
+ }
+
+ // Helpers
+
+ private static ConstructorInfo GetAttributeAdapterConstructor(Type attributeType, Type adapterType)
+ {
+ ConstructorInfo constructor = adapterType.GetConstructor(new[] { typeof(ModelMetadata), typeof(ControllerContext), attributeType });
+ if (constructor == null)
+ {
+ throw new ArgumentException(
+ String.Format(
+ CultureInfo.CurrentCulture,
+ MvcResources.DataAnnotationsModelValidatorProvider_ConstructorRequirements,
+ adapterType.FullName,
+ typeof(ModelMetadata).FullName,
+ typeof(ControllerContext).FullName,
+ attributeType.FullName),
+ "adapterType");
+ }
+
+ return constructor;
+ }
+
+ private static void ValidateAttributeAdapterType(Type adapterType)
+ {
+ if (adapterType == null)
+ {
+ throw new ArgumentNullException("adapterType");
+ }
+ if (!typeof(ModelValidator).IsAssignableFrom(adapterType))
+ {
+ throw new ArgumentException(
+ String.Format(
+ CultureInfo.CurrentCulture,
+ MvcResources.Common_TypeMustDriveFromType,
+ adapterType.FullName,
+ typeof(ModelValidator).FullName),
+ "adapterType");
+ }
+ }
+
+ private static void ValidateAttributeType(Type attributeType)
+ {
+ if (attributeType == null)
+ {
+ throw new ArgumentNullException("attributeType");
+ }
+ if (!typeof(ValidationAttribute).IsAssignableFrom(attributeType))
+ {
+ throw new ArgumentException(
+ String.Format(
+ CultureInfo.CurrentCulture,
+ MvcResources.Common_TypeMustDriveFromType,
+ attributeType.FullName,
+ typeof(ValidationAttribute).FullName),
+ "attributeType");
+ }
+ }
+
+ private static void ValidateAttributeFactory(DataAnnotationsModelValidationFactory factory)
+ {
+ if (factory == null)
+ {
+ throw new ArgumentNullException("factory");
+ }
+ }
+
+ #endregion
+
+ #region IValidatableObject adapter registration
+
+ /// <summary>
+ /// Registers an adapter type for the given <paramref name="modelType"/>, which must
+ /// implement <see cref="IValidatableObject"/>. The adapter type must derive from
+ /// <see cref="ModelValidator"/> and it must contain a public constructor
+ /// which takes two parameters of types <see cref="ModelMetadata"/> and
+ /// <see cref="ControllerContext"/>.
+ /// </summary>
+ public static void RegisterValidatableObjectAdapter(Type modelType, Type adapterType)
+ {
+ ValidateValidatableModelType(modelType);
+ ValidateValidatableAdapterType(adapterType);
+ ConstructorInfo constructor = GetValidatableAdapterConstructor(adapterType);
+
+ _adaptersLock.EnterWriteLock();
+
+ try
+ {
+ ValidatableFactories[modelType] = (metadata, context) => (ModelValidator)constructor.Invoke(new object[] { metadata, context });
+ }
+ finally
+ {
+ _adaptersLock.ExitWriteLock();
+ }
+ }
+
+ /// <summary>
+ /// Registers an adapter factory for the given <paramref name="modelType"/>, which must
+ /// implement <see cref="IValidatableObject"/>.
+ /// </summary>
+ public static void RegisterValidatableObjectAdapterFactory(Type modelType, DataAnnotationsValidatableObjectAdapterFactory factory)
+ {
+ ValidateValidatableModelType(modelType);
+ ValidateValidatableFactory(factory);
+
+ _adaptersLock.EnterWriteLock();
+
+ try
+ {
+ ValidatableFactories[modelType] = factory;
+ }
+ finally
+ {
+ _adaptersLock.ExitWriteLock();
+ }
+ }
+
+ /// <summary>
+ /// Registers the default adapter type for objects which implement
+ /// <see cref="IValidatableObject"/>. The adapter type must derive from
+ /// <see cref="ModelValidator"/> and it must contain a public constructor
+ /// which takes two parameters of types <see cref="ModelMetadata"/> and
+ /// <see cref="ControllerContext"/>.
+ /// </summary>
+ public static void RegisterDefaultValidatableObjectAdapter(Type adapterType)
+ {
+ ValidateValidatableAdapterType(adapterType);
+ ConstructorInfo constructor = GetValidatableAdapterConstructor(adapterType);
+
+ DefaultValidatableFactory = (metadata, context) => (ModelValidator)constructor.Invoke(new object[] { metadata, context });
+ }
+
+ /// <summary>
+ /// Registers the default adapter factory for objects which implement
+ /// <see cref="IValidatableObject"/>.
+ /// </summary>
+ public static void RegisterDefaultValidatableObjectAdapterFactory(DataAnnotationsValidatableObjectAdapterFactory factory)
+ {
+ ValidateValidatableFactory(factory);
+
+ DefaultValidatableFactory = factory;
+ }
+
+ // Helpers
+
+ private static ConstructorInfo GetValidatableAdapterConstructor(Type adapterType)
+ {
+ ConstructorInfo constructor = adapterType.GetConstructor(new[] { typeof(ModelMetadata), typeof(ControllerContext) });
+ if (constructor == null)
+ {
+ throw new ArgumentException(
+ String.Format(
+ CultureInfo.CurrentCulture,
+ MvcResources.DataAnnotationsModelValidatorProvider_ValidatableConstructorRequirements,
+ adapterType.FullName,
+ typeof(ModelMetadata).FullName,
+ typeof(ControllerContext).FullName),
+ "adapterType");
+ }
+
+ return constructor;
+ }
+
+ private static void ValidateValidatableAdapterType(Type adapterType)
+ {
+ if (adapterType == null)
+ {
+ throw new ArgumentNullException("adapterType");
+ }
+ if (!typeof(ModelValidator).IsAssignableFrom(adapterType))
+ {
+ throw new ArgumentException(
+ String.Format(
+ CultureInfo.CurrentCulture,
+ MvcResources.Common_TypeMustDriveFromType,
+ adapterType.FullName,
+ typeof(ModelValidator).FullName),
+ "adapterType");
+ }
+ }
+
+ private static void ValidateValidatableModelType(Type modelType)
+ {
+ if (modelType == null)
+ {
+ throw new ArgumentNullException("modelType");
+ }
+ if (!typeof(IValidatableObject).IsAssignableFrom(modelType))
+ {
+ throw new ArgumentException(
+ String.Format(
+ CultureInfo.CurrentCulture,
+ MvcResources.Common_TypeMustDriveFromType,
+ modelType.FullName,
+ typeof(IValidatableObject).FullName),
+ "modelType");
+ }
+ }
+
+ private static void ValidateValidatableFactory(DataAnnotationsValidatableObjectAdapterFactory factory)
+ {
+ if (factory == null)
+ {
+ throw new ArgumentNullException("factory");
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/System.Web.Mvc/DataAnnotationsModelValidator`1.cs b/src/System.Web.Mvc/DataAnnotationsModelValidator`1.cs
new file mode 100644
index 00000000..19a55969
--- /dev/null
+++ b/src/System.Web.Mvc/DataAnnotationsModelValidator`1.cs
@@ -0,0 +1,18 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace System.Web.Mvc
+{
+ public class DataAnnotationsModelValidator<TAttribute> : DataAnnotationsModelValidator
+ where TAttribute : ValidationAttribute
+ {
+ public DataAnnotationsModelValidator(ModelMetadata metadata, ControllerContext context, TAttribute attribute)
+ : base(metadata, context, attribute)
+ {
+ }
+
+ protected new TAttribute Attribute
+ {
+ get { return (TAttribute)base.Attribute; }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/DataErrorInfoModelValidatorProvider.cs b/src/System.Web.Mvc/DataErrorInfoModelValidatorProvider.cs
new file mode 100644
index 00000000..cdfc72dc
--- /dev/null
+++ b/src/System.Web.Mvc/DataErrorInfoModelValidatorProvider.cs
@@ -0,0 +1,95 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+
+namespace System.Web.Mvc
+{
+ public class DataErrorInfoModelValidatorProvider : ModelValidatorProvider
+ {
+ public override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context)
+ {
+ if (metadata == null)
+ {
+ throw new ArgumentNullException("metadata");
+ }
+ if (context == null)
+ {
+ throw new ArgumentNullException("context");
+ }
+
+ return GetValidatorsImpl(metadata, context);
+ }
+
+ private static IEnumerable<ModelValidator> GetValidatorsImpl(ModelMetadata metadata, ControllerContext context)
+ {
+ // If the metadata describes a model that implements IDataErrorInfo, we should call its
+ // Error property at the appropriate time.
+ if (TypeImplementsIDataErrorInfo(metadata.ModelType))
+ {
+ yield return new DataErrorInfoClassModelValidator(metadata, context);
+ }
+
+ // If the metadata describes a property of a container that implements IDataErrorInfo,
+ // we should call its Item indexer at the appropriate time.
+ if (TypeImplementsIDataErrorInfo(metadata.ContainerType))
+ {
+ yield return new DataErrorInfoPropertyModelValidator(metadata, context);
+ }
+ }
+
+ private static bool TypeImplementsIDataErrorInfo(Type type)
+ {
+ return typeof(IDataErrorInfo).IsAssignableFrom(type);
+ }
+
+ internal sealed class DataErrorInfoClassModelValidator : ModelValidator
+ {
+ public DataErrorInfoClassModelValidator(ModelMetadata metadata, ControllerContext controllerContext)
+ : base(metadata, controllerContext)
+ {
+ }
+
+ public override IEnumerable<ModelValidationResult> Validate(object container)
+ {
+ IDataErrorInfo castModel = Metadata.Model as IDataErrorInfo;
+ if (castModel != null)
+ {
+ string errorMessage = castModel.Error;
+ if (!String.IsNullOrEmpty(errorMessage))
+ {
+ return new ModelValidationResult[]
+ {
+ new ModelValidationResult() { Message = errorMessage }
+ };
+ }
+ }
+ return Enumerable.Empty<ModelValidationResult>();
+ }
+ }
+
+ internal sealed class DataErrorInfoPropertyModelValidator : ModelValidator
+ {
+ public DataErrorInfoPropertyModelValidator(ModelMetadata metadata, ControllerContext controllerContext)
+ : base(metadata, controllerContext)
+ {
+ }
+
+ public override IEnumerable<ModelValidationResult> Validate(object container)
+ {
+ IDataErrorInfo castContainer = container as IDataErrorInfo;
+ if (castContainer != null && !String.Equals(Metadata.PropertyName, "error", StringComparison.OrdinalIgnoreCase))
+ {
+ string errorMessage = castContainer[Metadata.PropertyName];
+ if (!String.IsNullOrEmpty(errorMessage))
+ {
+ return new ModelValidationResult[]
+ {
+ new ModelValidationResult() { Message = errorMessage }
+ };
+ }
+ }
+ return Enumerable.Empty<ModelValidationResult>();
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/DataTypeUtil.cs b/src/System.Web.Mvc/DataTypeUtil.cs
new file mode 100644
index 00000000..79eb9ab6
--- /dev/null
+++ b/src/System.Web.Mvc/DataTypeUtil.cs
@@ -0,0 +1,109 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+
+namespace System.Web.Mvc
+{
+ internal static class DataTypeUtil
+ {
+ internal static readonly string CurrencyTypeName = DataType.Currency.ToString();
+ internal static readonly string DateTypeName = DataType.Date.ToString();
+ internal static readonly string DateTimeTypeName = DataType.DateTime.ToString();
+ internal static readonly string DurationTypeName = DataType.Duration.ToString();
+ internal static readonly string EmailAddressTypeName = DataType.EmailAddress.ToString();
+ internal static readonly string HtmlTypeName = DataType.Html.ToString();
+ internal static readonly string ImageUrlTypeName = DataType.ImageUrl.ToString();
+ internal static readonly string MultiLineTextTypeName = DataType.MultilineText.ToString();
+ internal static readonly string PasswordTypeName = DataType.Password.ToString();
+ internal static readonly string PhoneNumberTypeName = DataType.PhoneNumber.ToString();
+ internal static readonly string TextTypeName = DataType.Text.ToString();
+ internal static readonly string TimeTypeName = DataType.Time.ToString();
+ internal static readonly string UrlTypeName = DataType.Url.ToString();
+
+ private static readonly Lazy<Dictionary<object, string>> _dataTypeToName = new Lazy<Dictionary<object, string>>(CreateDataTypeToName, isThreadSafe: true);
+
+ // This is a faster version of GetDataTypeName(). It internally calls ToString() on the enum
+ // value, which can be quite slow because of value verification.
+ internal static string ToDataTypeName(this DataTypeAttribute attribute, Func<DataTypeAttribute, Boolean> isDataType = null)
+ {
+ if (isDataType == null)
+ {
+ isDataType = t => t.GetType().Equals(typeof(DataTypeAttribute));
+ }
+
+ // GetDataTypeName is virtual, so this is only safe if they haven't derived from DataTypeAttribute.
+ // However, if they derive from DataTypeAttribute, they can help their own perf by overriding GetDataTypeName
+ // and returning an appropriate string without invoking the ToString() on the enum.
+ if (isDataType(attribute))
+ {
+ // Statically known dataTypes are handled separately for performance
+ string name = KnownDataTypeToString(attribute.DataType);
+ if (name == null)
+ {
+ // Unknown types fallback to a dictionary lookup.
+ // 4.0 will not enter this code for statically known data types.
+ // 4.5 will enter this code for the new data types added to 4.5.
+ _dataTypeToName.Value.TryGetValue(attribute.DataType, out name);
+ }
+
+ if (name != null)
+ {
+ return name;
+ }
+ }
+
+ return attribute.GetDataTypeName();
+ }
+
+ private static string KnownDataTypeToString(DataType dataType)
+ {
+ switch (dataType)
+ {
+ case DataType.Currency:
+ return CurrencyTypeName;
+ case DataType.Date:
+ return DateTypeName;
+ case DataType.DateTime:
+ return DateTimeTypeName;
+ case DataType.Duration:
+ return DurationTypeName;
+ case DataType.EmailAddress:
+ return EmailAddressTypeName;
+ case DataType.Html:
+ return HtmlTypeName;
+ case DataType.ImageUrl:
+ return ImageUrlTypeName;
+ case DataType.MultilineText:
+ return MultiLineTextTypeName;
+ case DataType.Password:
+ return PasswordTypeName;
+ case DataType.PhoneNumber:
+ return PhoneNumberTypeName;
+ case DataType.Text:
+ return TextTypeName;
+ case DataType.Time:
+ return TimeTypeName;
+ case DataType.Url:
+ return UrlTypeName;
+ }
+
+ return null;
+ }
+
+ private static Dictionary<object, string> CreateDataTypeToName()
+ {
+ Dictionary<object, string> dataTypeToName = new Dictionary<object, string>();
+ foreach (DataType dataTypeValue in Enum.GetValues(typeof(DataType)))
+ {
+ // Don't add to the dictionary any of the statically known types.
+ // This is a workingset size optimization.
+ if (dataTypeValue != DataType.Custom && KnownDataTypeToString(dataTypeValue) == null)
+ {
+ string name = Enum.GetName(typeof(DataType), dataTypeValue);
+ dataTypeToName[dataTypeValue] = name;
+ }
+ }
+
+ return dataTypeToName;
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/DefaultControllerFactory.cs b/src/System.Web.Mvc/DefaultControllerFactory.cs
new file mode 100644
index 00000000..8f67ca52
--- /dev/null
+++ b/src/System.Web.Mvc/DefaultControllerFactory.cs
@@ -0,0 +1,294 @@
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Web.Mvc.Properties;
+using System.Web.Routing;
+using System.Web.SessionState;
+
+namespace System.Web.Mvc
+{
+ public class DefaultControllerFactory : IControllerFactory
+ {
+ private static readonly ConcurrentDictionary<Type, SessionStateBehavior> _sessionStateCache = new ConcurrentDictionary<Type, SessionStateBehavior>();
+ private static ControllerTypeCache _staticControllerTypeCache = new ControllerTypeCache();
+ private IBuildManager _buildManager;
+ private IResolver<IControllerActivator> _activatorResolver;
+ private IControllerActivator _controllerActivator;
+ private ControllerBuilder _controllerBuilder;
+ private ControllerTypeCache _instanceControllerTypeCache;
+
+ public DefaultControllerFactory()
+ : this(null, null, null)
+ {
+ }
+
+ public DefaultControllerFactory(IControllerActivator controllerActivator)
+ : this(controllerActivator, null, null)
+ {
+ }
+
+ internal DefaultControllerFactory(IControllerActivator controllerActivator, IResolver<IControllerActivator> activatorResolver, IDependencyResolver dependencyResolver)
+ {
+ if (controllerActivator != null)
+ {
+ _controllerActivator = controllerActivator;
+ }
+ else
+ {
+ _activatorResolver = activatorResolver ?? new SingleServiceResolver<IControllerActivator>(
+ () => null,
+ new DefaultControllerActivator(dependencyResolver),
+ "DefaultControllerFactory constructor");
+ }
+ }
+
+ private IControllerActivator ControllerActivator
+ {
+ get
+ {
+ if (_controllerActivator != null)
+ {
+ return _controllerActivator;
+ }
+ _controllerActivator = _activatorResolver.Current;
+ return _controllerActivator;
+ }
+ }
+
+ internal IBuildManager BuildManager
+ {
+ get
+ {
+ if (_buildManager == null)
+ {
+ _buildManager = new BuildManagerWrapper();
+ }
+ return _buildManager;
+ }
+ set { _buildManager = value; }
+ }
+
+ internal ControllerBuilder ControllerBuilder
+ {
+ get { return _controllerBuilder ?? ControllerBuilder.Current; }
+ set { _controllerBuilder = value; }
+ }
+
+ internal ControllerTypeCache ControllerTypeCache
+ {
+ get { return _instanceControllerTypeCache ?? _staticControllerTypeCache; }
+ set { _instanceControllerTypeCache = value; }
+ }
+
+ internal static InvalidOperationException CreateAmbiguousControllerException(RouteBase route, string controllerName, ICollection<Type> matchingTypes)
+ {
+ // we need to generate an exception containing all the controller types
+ StringBuilder typeList = new StringBuilder();
+ foreach (Type matchedType in matchingTypes)
+ {
+ typeList.AppendLine();
+ typeList.Append(matchedType.FullName);
+ }
+
+ string errorText;
+ Route castRoute = route as Route;
+ if (castRoute != null)
+ {
+ errorText = String.Format(CultureInfo.CurrentCulture, MvcResources.DefaultControllerFactory_ControllerNameAmbiguous_WithRouteUrl,
+ controllerName, castRoute.Url, typeList);
+ }
+ else
+ {
+ errorText = String.Format(CultureInfo.CurrentCulture, MvcResources.DefaultControllerFactory_ControllerNameAmbiguous_WithoutRouteUrl,
+ controllerName, typeList);
+ }
+
+ return new InvalidOperationException(errorText);
+ }
+
+ public virtual IController CreateController(RequestContext requestContext, string controllerName)
+ {
+ if (requestContext == null)
+ {
+ throw new ArgumentNullException("requestContext");
+ }
+ if (String.IsNullOrEmpty(controllerName))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "controllerName");
+ }
+ Type controllerType = GetControllerType(requestContext, controllerName);
+ IController controller = GetControllerInstance(requestContext, controllerType);
+ return controller;
+ }
+
+ protected internal virtual IController GetControllerInstance(RequestContext requestContext, Type controllerType)
+ {
+ if (controllerType == null)
+ {
+ throw new HttpException(404,
+ String.Format(
+ CultureInfo.CurrentCulture,
+ MvcResources.DefaultControllerFactory_NoControllerFound,
+ requestContext.HttpContext.Request.Path));
+ }
+ if (!typeof(IController).IsAssignableFrom(controllerType))
+ {
+ throw new ArgumentException(
+ String.Format(
+ CultureInfo.CurrentCulture,
+ MvcResources.DefaultControllerFactory_TypeDoesNotSubclassControllerBase,
+ controllerType),
+ "controllerType");
+ }
+ return ControllerActivator.Create(requestContext, controllerType);
+ }
+
+ protected internal virtual SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, Type controllerType)
+ {
+ if (controllerType == null)
+ {
+ return SessionStateBehavior.Default;
+ }
+
+ return _sessionStateCache.GetOrAdd(
+ controllerType,
+ type =>
+ {
+ var attr = type.GetCustomAttributes(typeof(SessionStateAttribute), inherit: true)
+ .OfType<SessionStateAttribute>()
+ .FirstOrDefault();
+
+ return (attr != null) ? attr.Behavior : SessionStateBehavior.Default;
+ });
+ }
+
+ protected internal virtual Type GetControllerType(RequestContext requestContext, string controllerName)
+ {
+ if (String.IsNullOrEmpty(controllerName))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "controllerName");
+ }
+
+ // first search in the current route's namespace collection
+ object routeNamespacesObj;
+ Type match;
+ if (requestContext != null && requestContext.RouteData.DataTokens.TryGetValue("Namespaces", out routeNamespacesObj))
+ {
+ IEnumerable<string> routeNamespaces = routeNamespacesObj as IEnumerable<string>;
+ if (routeNamespaces != null && routeNamespaces.Any())
+ {
+ HashSet<string> namespaceHash = new HashSet<string>(routeNamespaces, StringComparer.OrdinalIgnoreCase);
+ match = GetControllerTypeWithinNamespaces(requestContext.RouteData.Route, controllerName, namespaceHash);
+
+ // the UseNamespaceFallback key might not exist, in which case its value is implicitly "true"
+ if (match != null || false.Equals(requestContext.RouteData.DataTokens["UseNamespaceFallback"]))
+ {
+ // got a match or the route requested we stop looking
+ return match;
+ }
+ }
+ }
+
+ // then search in the application's default namespace collection
+ if (ControllerBuilder.DefaultNamespaces.Count > 0)
+ {
+ HashSet<string> namespaceDefaults = new HashSet<string>(ControllerBuilder.DefaultNamespaces, StringComparer.OrdinalIgnoreCase);
+ match = GetControllerTypeWithinNamespaces(requestContext.RouteData.Route, controllerName, namespaceDefaults);
+ if (match != null)
+ {
+ return match;
+ }
+ }
+
+ // if all else fails, search every namespace
+ return GetControllerTypeWithinNamespaces(requestContext.RouteData.Route, controllerName, null /* namespaces */);
+ }
+
+ private Type GetControllerTypeWithinNamespaces(RouteBase route, string controllerName, HashSet<string> namespaces)
+ {
+ // Once the master list of controllers has been created we can quickly index into it
+ ControllerTypeCache.EnsureInitialized(BuildManager);
+
+ ICollection<Type> matchingTypes = ControllerTypeCache.GetControllerTypes(controllerName, namespaces);
+ switch (matchingTypes.Count)
+ {
+ case 0:
+ // no matching types
+ return null;
+
+ case 1:
+ // single matching type
+ return matchingTypes.First();
+
+ default:
+ // multiple matching types
+ throw CreateAmbiguousControllerException(route, controllerName, matchingTypes);
+ }
+ }
+
+ public virtual void ReleaseController(IController controller)
+ {
+ IDisposable disposable = controller as IDisposable;
+ if (disposable != null)
+ {
+ disposable.Dispose();
+ }
+ }
+
+ SessionStateBehavior IControllerFactory.GetControllerSessionBehavior(RequestContext requestContext, string controllerName)
+ {
+ if (requestContext == null)
+ {
+ throw new ArgumentNullException("requestContext");
+ }
+ if (String.IsNullOrEmpty(controllerName))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "controllerName");
+ }
+
+ Type controllerType = GetControllerType(requestContext, controllerName);
+ return GetControllerSessionBehavior(requestContext, controllerType);
+ }
+
+ private class DefaultControllerActivator : IControllerActivator
+ {
+ private Func<IDependencyResolver> _resolverThunk;
+
+ public DefaultControllerActivator()
+ : this(null)
+ {
+ }
+
+ public DefaultControllerActivator(IDependencyResolver resolver)
+ {
+ if (resolver == null)
+ {
+ _resolverThunk = () => DependencyResolver.Current;
+ }
+ else
+ {
+ _resolverThunk = () => resolver;
+ }
+ }
+
+ public IController Create(RequestContext requestContext, Type controllerType)
+ {
+ try
+ {
+ return (IController)(_resolverThunk().GetService(controllerType) ?? Activator.CreateInstance(controllerType));
+ }
+ catch (Exception ex)
+ {
+ throw new InvalidOperationException(
+ String.Format(
+ CultureInfo.CurrentCulture,
+ MvcResources.DefaultControllerFactory_ErrorCreatingController,
+ controllerType),
+ ex);
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/DefaultModelBinder.cs b/src/System.Web.Mvc/DefaultModelBinder.cs
new file mode 100644
index 00000000..6a0377e9
--- /dev/null
+++ b/src/System.Web.Mvc/DefaultModelBinder.cs
@@ -0,0 +1,840 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc
+{
+ public class DefaultModelBinder : IModelBinder
+ {
+ private static string _resourceClassKey;
+ private ModelBinderDictionary _binders;
+
+ [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "Property is settable so that the dictionary can be provided for unit testing purposes.")]
+ protected internal ModelBinderDictionary Binders
+ {
+ get
+ {
+ if (_binders == null)
+ {
+ _binders = ModelBinders.Binders;
+ }
+ return _binders;
+ }
+ set { _binders = value; }
+ }
+
+ public static string ResourceClassKey
+ {
+ get { return _resourceClassKey ?? String.Empty; }
+ set { _resourceClassKey = value; }
+ }
+
+ private static void AddValueRequiredMessageToModelState(ControllerContext controllerContext, ModelStateDictionary modelState, string modelStateKey, Type elementType, object value)
+ {
+ if (value == null && !TypeHelpers.TypeAllowsNullValue(elementType) && modelState.IsValidField(modelStateKey))
+ {
+ modelState.AddModelError(modelStateKey, GetValueRequiredResource(controllerContext));
+ }
+ }
+
+ internal void BindComplexElementalModel(ControllerContext controllerContext, ModelBindingContext bindingContext, object model)
+ {
+ // need to replace the property filter + model object and create an inner binding context
+ ModelBindingContext newBindingContext = CreateComplexElementalModelBindingContext(controllerContext, bindingContext, model);
+
+ // validation
+ if (OnModelUpdating(controllerContext, newBindingContext))
+ {
+ BindProperties(controllerContext, newBindingContext);
+ OnModelUpdated(controllerContext, newBindingContext);
+ }
+ }
+
+ internal object BindComplexModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
+ {
+ object model = bindingContext.Model;
+ Type modelType = bindingContext.ModelType;
+
+ // if we're being asked to create an array, create a list instead, then coerce to an array after the list is created
+ if (model == null && modelType.IsArray)
+ {
+ Type elementType = modelType.GetElementType();
+ Type listType = typeof(List<>).MakeGenericType(elementType);
+ object collection = CreateModel(controllerContext, bindingContext, listType);
+
+ ModelBindingContext arrayBindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => collection, listType),
+ ModelName = bindingContext.ModelName,
+ ModelState = bindingContext.ModelState,
+ PropertyFilter = bindingContext.PropertyFilter,
+ ValueProvider = bindingContext.ValueProvider
+ };
+ IList list = (IList)UpdateCollection(controllerContext, arrayBindingContext, elementType);
+
+ if (list == null)
+ {
+ return null;
+ }
+
+ Array array = Array.CreateInstance(elementType, list.Count);
+ list.CopyTo(array, 0);
+ return array;
+ }
+
+ if (model == null)
+ {
+ model = CreateModel(controllerContext, bindingContext, modelType);
+ }
+
+ // special-case IDictionary<,> and ICollection<>
+ Type dictionaryType = TypeHelpers.ExtractGenericInterface(modelType, typeof(IDictionary<,>));
+ if (dictionaryType != null)
+ {
+ Type[] genericArguments = dictionaryType.GetGenericArguments();
+ Type keyType = genericArguments[0];
+ Type valueType = genericArguments[1];
+
+ ModelBindingContext dictionaryBindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, modelType),
+ ModelName = bindingContext.ModelName,
+ ModelState = bindingContext.ModelState,
+ PropertyFilter = bindingContext.PropertyFilter,
+ ValueProvider = bindingContext.ValueProvider
+ };
+ object dictionary = UpdateDictionary(controllerContext, dictionaryBindingContext, keyType, valueType);
+ return dictionary;
+ }
+
+ Type enumerableType = TypeHelpers.ExtractGenericInterface(modelType, typeof(IEnumerable<>));
+ if (enumerableType != null)
+ {
+ Type elementType = enumerableType.GetGenericArguments()[0];
+
+ Type collectionType = typeof(ICollection<>).MakeGenericType(elementType);
+ if (collectionType.IsInstanceOfType(model))
+ {
+ ModelBindingContext collectionBindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, modelType),
+ ModelName = bindingContext.ModelName,
+ ModelState = bindingContext.ModelState,
+ PropertyFilter = bindingContext.PropertyFilter,
+ ValueProvider = bindingContext.ValueProvider
+ };
+ object collection = UpdateCollection(controllerContext, collectionBindingContext, elementType);
+ return collection;
+ }
+ }
+
+ // otherwise, just update the properties on the complex type
+ BindComplexElementalModel(controllerContext, bindingContext, model);
+ return model;
+ }
+
+ public virtual object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
+ {
+ if (bindingContext == null)
+ {
+ throw new ArgumentNullException("bindingContext");
+ }
+
+ bool performedFallback = false;
+
+ if (!String.IsNullOrEmpty(bindingContext.ModelName) && !bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName))
+ {
+ // We couldn't find any entry that began with the prefix. If this is the top-level element, fall back
+ // to the empty prefix.
+ if (bindingContext.FallbackToEmptyPrefix)
+ {
+ bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = bindingContext.ModelMetadata,
+ ModelState = bindingContext.ModelState,
+ PropertyFilter = bindingContext.PropertyFilter,
+ ValueProvider = bindingContext.ValueProvider
+ };
+ performedFallback = true;
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ // Simple model = int, string, etc.; determined by calling TypeConverter.CanConvertFrom(typeof(string))
+ // or by seeing if a value in the request exactly matches the name of the model we're binding.
+ // Complex type = everything else.
+ if (!performedFallback)
+ {
+ bool performRequestValidation = ShouldPerformRequestValidation(controllerContext, bindingContext);
+ ValueProviderResult valueProviderResult = bindingContext.UnvalidatedValueProvider.GetValue(bindingContext.ModelName, skipValidation: !performRequestValidation);
+ if (valueProviderResult != null)
+ {
+ return BindSimpleModel(controllerContext, bindingContext, valueProviderResult);
+ }
+ }
+ if (!bindingContext.ModelMetadata.IsComplexType)
+ {
+ return null;
+ }
+
+ return BindComplexModel(controllerContext, bindingContext);
+ }
+
+ private void BindProperties(ControllerContext controllerContext, ModelBindingContext bindingContext)
+ {
+ IEnumerable<PropertyDescriptor> properties = GetFilteredModelProperties(controllerContext, bindingContext);
+ foreach (PropertyDescriptor property in properties)
+ {
+ BindProperty(controllerContext, bindingContext, property);
+ }
+ }
+
+ protected virtual void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor)
+ {
+ // need to skip properties that aren't part of the request, else we might hit a StackOverflowException
+ string fullPropertyKey = CreateSubPropertyName(bindingContext.ModelName, propertyDescriptor.Name);
+ if (!bindingContext.ValueProvider.ContainsPrefix(fullPropertyKey))
+ {
+ return;
+ }
+
+ // call into the property's model binder
+ IModelBinder propertyBinder = Binders.GetBinder(propertyDescriptor.PropertyType);
+ object originalPropertyValue = propertyDescriptor.GetValue(bindingContext.Model);
+ ModelMetadata propertyMetadata = bindingContext.PropertyMetadata[propertyDescriptor.Name];
+ propertyMetadata.Model = originalPropertyValue;
+ ModelBindingContext innerBindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = propertyMetadata,
+ ModelName = fullPropertyKey,
+ ModelState = bindingContext.ModelState,
+ ValueProvider = bindingContext.ValueProvider
+ };
+ object newPropertyValue = GetPropertyValue(controllerContext, innerBindingContext, propertyDescriptor, propertyBinder);
+ propertyMetadata.Model = newPropertyValue;
+
+ // validation
+ ModelState modelState = bindingContext.ModelState[fullPropertyKey];
+ if (modelState == null || modelState.Errors.Count == 0)
+ {
+ if (OnPropertyValidating(controllerContext, bindingContext, propertyDescriptor, newPropertyValue))
+ {
+ SetProperty(controllerContext, bindingContext, propertyDescriptor, newPropertyValue);
+ OnPropertyValidated(controllerContext, bindingContext, propertyDescriptor, newPropertyValue);
+ }
+ }
+ else
+ {
+ SetProperty(controllerContext, bindingContext, propertyDescriptor, newPropertyValue);
+
+ // Convert FormatExceptions (type conversion failures) into InvalidValue messages
+ foreach (ModelError error in modelState.Errors.Where(err => String.IsNullOrEmpty(err.ErrorMessage) && err.Exception != null).ToList())
+ {
+ for (Exception exception = error.Exception; exception != null; exception = exception.InnerException)
+ {
+ if (exception is FormatException)
+ {
+ string displayName = propertyMetadata.GetDisplayName();
+ string errorMessageTemplate = GetValueInvalidResource(controllerContext);
+ string errorMessage = String.Format(CultureInfo.CurrentCulture, errorMessageTemplate, modelState.Value.AttemptedValue, displayName);
+ modelState.Errors.Remove(error);
+ modelState.Errors.Add(errorMessage);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ internal object BindSimpleModel(ControllerContext controllerContext, ModelBindingContext bindingContext, ValueProviderResult valueProviderResult)
+ {
+ bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
+
+ // if the value provider returns an instance of the requested data type, we can just short-circuit
+ // the evaluation and return that instance
+ if (bindingContext.ModelType.IsInstanceOfType(valueProviderResult.RawValue))
+ {
+ return valueProviderResult.RawValue;
+ }
+
+ // since a string is an IEnumerable<char>, we want it to skip the two checks immediately following
+ if (bindingContext.ModelType != typeof(string))
+ {
+ // conversion results in 3 cases, as below
+ if (bindingContext.ModelType.IsArray)
+ {
+ // case 1: user asked for an array
+ // ValueProviderResult.ConvertTo() understands array types, so pass in the array type directly
+ object modelArray = ConvertProviderResult(bindingContext.ModelState, bindingContext.ModelName, valueProviderResult, bindingContext.ModelType);
+ return modelArray;
+ }
+
+ Type enumerableType = TypeHelpers.ExtractGenericInterface(bindingContext.ModelType, typeof(IEnumerable<>));
+ if (enumerableType != null)
+ {
+ // case 2: user asked for a collection rather than an array
+ // need to call ConvertTo() on the array type, then copy the array to the collection
+ object modelCollection = CreateModel(controllerContext, bindingContext, bindingContext.ModelType);
+ Type elementType = enumerableType.GetGenericArguments()[0];
+ Type arrayType = elementType.MakeArrayType();
+ object modelArray = ConvertProviderResult(bindingContext.ModelState, bindingContext.ModelName, valueProviderResult, arrayType);
+
+ Type collectionType = typeof(ICollection<>).MakeGenericType(elementType);
+ if (collectionType.IsInstanceOfType(modelCollection))
+ {
+ CollectionHelpers.ReplaceCollection(elementType, modelCollection, modelArray);
+ }
+ return modelCollection;
+ }
+ }
+
+ // case 3: user asked for an individual element
+ object model = ConvertProviderResult(bindingContext.ModelState, bindingContext.ModelName, valueProviderResult, bindingContext.ModelType);
+ return model;
+ }
+
+ private static bool CanUpdateReadonlyTypedReference(Type type)
+ {
+ // value types aren't strictly immutable, but because they have copy-by-value semantics
+ // we can't update a value type that is marked readonly
+ if (type.IsValueType)
+ {
+ return false;
+ }
+
+ // arrays are mutable, but because we can't change their length we shouldn't try
+ // to update an array that is referenced readonly
+ if (type.IsArray)
+ {
+ return false;
+ }
+
+ // special-case known common immutable types
+ if (type == typeof(string))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ [SuppressMessage("Microsoft.Globalization", "CA1304:SpecifyCultureInfo", MessageId = "System.Web.Mvc.ValueProviderResult.ConvertTo(System.Type)", Justification = "The target object should make the correct culture determination, not this method.")]
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We're recording this exception so that we can act on it later.")]
+ private static object ConvertProviderResult(ModelStateDictionary modelState, string modelStateKey, ValueProviderResult valueProviderResult, Type destinationType)
+ {
+ try
+ {
+ object convertedValue = valueProviderResult.ConvertTo(destinationType);
+ return convertedValue;
+ }
+ catch (Exception ex)
+ {
+ modelState.AddModelError(modelStateKey, ex);
+ return null;
+ }
+ }
+
+ internal ModelBindingContext CreateComplexElementalModelBindingContext(ControllerContext controllerContext, ModelBindingContext bindingContext, object model)
+ {
+ BindAttribute bindAttr = (BindAttribute)GetTypeDescriptor(controllerContext, bindingContext).GetAttributes()[typeof(BindAttribute)];
+ Predicate<string> newPropertyFilter = (bindAttr != null)
+ ? propertyName => bindAttr.IsPropertyAllowed(propertyName) && bindingContext.PropertyFilter(propertyName)
+ : bindingContext.PropertyFilter;
+
+ ModelBindingContext newBindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, bindingContext.ModelType),
+ ModelName = bindingContext.ModelName,
+ ModelState = bindingContext.ModelState,
+ PropertyFilter = newPropertyFilter,
+ ValueProvider = bindingContext.ValueProvider
+ };
+
+ return newBindingContext;
+ }
+
+ protected virtual object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
+ {
+ Type typeToCreate = modelType;
+
+ // we can understand some collection interfaces, e.g. IList<>, IDictionary<,>
+ if (modelType.IsGenericType)
+ {
+ Type genericTypeDefinition = modelType.GetGenericTypeDefinition();
+ if (genericTypeDefinition == typeof(IDictionary<,>))
+ {
+ typeToCreate = typeof(Dictionary<,>).MakeGenericType(modelType.GetGenericArguments());
+ }
+ else if (genericTypeDefinition == typeof(IEnumerable<>) || genericTypeDefinition == typeof(ICollection<>) || genericTypeDefinition == typeof(IList<>))
+ {
+ typeToCreate = typeof(List<>).MakeGenericType(modelType.GetGenericArguments());
+ }
+ }
+
+ // fallback to the type's default constructor
+ return Activator.CreateInstance(typeToCreate);
+ }
+
+ protected static string CreateSubIndexName(string prefix, int index)
+ {
+ return String.Format(CultureInfo.InvariantCulture, "{0}[{1}]", prefix, index);
+ }
+
+ protected static string CreateSubIndexName(string prefix, string index)
+ {
+ return String.Format(CultureInfo.InvariantCulture, "{0}[{1}]", prefix, index);
+ }
+
+ protected internal static string CreateSubPropertyName(string prefix, string propertyName)
+ {
+ if (String.IsNullOrEmpty(prefix))
+ {
+ return propertyName;
+ }
+ else if (String.IsNullOrEmpty(propertyName))
+ {
+ return prefix;
+ }
+ else
+ {
+ return prefix + "." + propertyName;
+ }
+ }
+
+ protected IEnumerable<PropertyDescriptor> GetFilteredModelProperties(ControllerContext controllerContext, ModelBindingContext bindingContext)
+ {
+ PropertyDescriptorCollection properties = GetModelProperties(controllerContext, bindingContext);
+ Predicate<string> propertyFilter = bindingContext.PropertyFilter;
+
+ return from PropertyDescriptor property in properties
+ where ShouldUpdateProperty(property, propertyFilter)
+ select property;
+ }
+
+ [SuppressMessage("Microsoft.Globalization", "CA1304:SpecifyCultureInfo", MessageId = "System.Web.Mvc.ValueProviderResult.ConvertTo(System.Type)", Justification = "ValueProviderResult already handles culture conversion appropriately.")]
+ private static void GetIndexes(ModelBindingContext bindingContext, out bool stopOnIndexNotFound, out IEnumerable<string> indexes)
+ {
+ string indexKey = CreateSubPropertyName(bindingContext.ModelName, "index");
+ ValueProviderResult valueProviderResult = bindingContext.ValueProvider.GetValue(indexKey);
+
+ if (valueProviderResult != null)
+ {
+ string[] indexesArray = valueProviderResult.ConvertTo(typeof(string[])) as string[];
+ if (indexesArray != null)
+ {
+ stopOnIndexNotFound = false;
+ indexes = indexesArray;
+ return;
+ }
+ }
+
+ // just use a simple zero-based system
+ stopOnIndexNotFound = true;
+ indexes = GetZeroBasedIndexes();
+ }
+
+ protected virtual PropertyDescriptorCollection GetModelProperties(ControllerContext controllerContext, ModelBindingContext bindingContext)
+ {
+ return GetTypeDescriptor(controllerContext, bindingContext).GetProperties();
+ }
+
+ protected virtual object GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder)
+ {
+ object value = propertyBinder.BindModel(controllerContext, bindingContext);
+
+ if (bindingContext.ModelMetadata.ConvertEmptyStringToNull && Equals(value, String.Empty))
+ {
+ return null;
+ }
+
+ return value;
+ }
+
+ protected virtual ICustomTypeDescriptor GetTypeDescriptor(ControllerContext controllerContext, ModelBindingContext bindingContext)
+ {
+ return TypeDescriptorHelper.Get(bindingContext.ModelType);
+ }
+
+ // If the user specified a ResourceClassKey try to load the resource they specified.
+ // If the class key is invalid, an exception will be thrown.
+ // If the class key is valid but the resource is not found, it returns null, in which
+ // case it will fall back to the MVC default error message.
+ private static string GetUserResourceString(ControllerContext controllerContext, string resourceName)
+ {
+ string result = null;
+
+ if (!String.IsNullOrEmpty(ResourceClassKey) && (controllerContext != null) && (controllerContext.HttpContext != null))
+ {
+ result = controllerContext.HttpContext.GetGlobalResourceObject(ResourceClassKey, resourceName, CultureInfo.CurrentUICulture) as string;
+ }
+
+ return result;
+ }
+
+ private static string GetValueInvalidResource(ControllerContext controllerContext)
+ {
+ return GetUserResourceString(controllerContext, "PropertyValueInvalid") ?? MvcResources.DefaultModelBinder_ValueInvalid;
+ }
+
+ private static string GetValueRequiredResource(ControllerContext controllerContext)
+ {
+ return GetUserResourceString(controllerContext, "PropertyValueRequired") ?? MvcResources.DefaultModelBinder_ValueRequired;
+ }
+
+ private static IEnumerable<string> GetZeroBasedIndexes()
+ {
+ int i = 0;
+ while (true)
+ {
+ yield return i.ToString(CultureInfo.InvariantCulture);
+ i++;
+ }
+ }
+
+ protected static bool IsModelValid(ModelBindingContext bindingContext)
+ {
+ if (bindingContext == null)
+ {
+ throw new ArgumentNullException("bindingContext");
+ }
+ if (String.IsNullOrEmpty(bindingContext.ModelName))
+ {
+ return bindingContext.ModelState.IsValid;
+ }
+ return bindingContext.ModelState.IsValidField(bindingContext.ModelName);
+ }
+
+ protected virtual void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
+ {
+ Dictionary<string, bool> startedValid = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
+
+ foreach (ModelValidationResult validationResult in ModelValidator.GetModelValidator(bindingContext.ModelMetadata, controllerContext).Validate(null))
+ {
+ string subPropertyName = CreateSubPropertyName(bindingContext.ModelName, validationResult.MemberName);
+
+ if (!startedValid.ContainsKey(subPropertyName))
+ {
+ startedValid[subPropertyName] = bindingContext.ModelState.IsValidField(subPropertyName);
+ }
+
+ if (startedValid[subPropertyName])
+ {
+ bindingContext.ModelState.AddModelError(subPropertyName, validationResult.Message);
+ }
+ }
+ }
+
+ protected virtual bool OnModelUpdating(ControllerContext controllerContext, ModelBindingContext bindingContext)
+ {
+ // default implementation does nothing
+ return true;
+ }
+
+ protected virtual void OnPropertyValidated(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value)
+ {
+ // default implementation does nothing
+ }
+
+ protected virtual bool OnPropertyValidating(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value)
+ {
+ // default implementation does nothing
+ return true;
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We're recording this exception so that we can act on it later.")]
+ protected virtual void SetProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value)
+ {
+ ModelMetadata propertyMetadata = bindingContext.PropertyMetadata[propertyDescriptor.Name];
+ propertyMetadata.Model = value;
+ string modelStateKey = CreateSubPropertyName(bindingContext.ModelName, propertyMetadata.PropertyName);
+
+ // If the value is null, and the validation system can find a Required validator for
+ // us, we'd prefer to run it before we attempt to set the value; otherwise, property
+ // setters which throw on null (f.e., Entity Framework properties which are backed by
+ // non-nullable strings in the DB) will get their error message in ahead of us.
+ //
+ // We are effectively using the special validator -- Required -- as a helper to the
+ // binding system, which is why this code is here instead of in the Validating/Validated
+ // methods, which are really the old-school validation hooks.
+ if (value == null && bindingContext.ModelState.IsValidField(modelStateKey))
+ {
+ ModelValidator requiredValidator = ModelValidatorProviders.Providers.GetValidators(propertyMetadata, controllerContext).Where(v => v.IsRequired).FirstOrDefault();
+ if (requiredValidator != null)
+ {
+ foreach (ModelValidationResult validationResult in requiredValidator.Validate(bindingContext.Model))
+ {
+ bindingContext.ModelState.AddModelError(modelStateKey, validationResult.Message);
+ }
+ }
+ }
+
+ bool isNullValueOnNonNullableType =
+ value == null &&
+ !TypeHelpers.TypeAllowsNullValue(propertyDescriptor.PropertyType);
+
+ // Try to set a value into the property unless we know it will fail (read-only
+ // properties and null values with non-nullable types)
+ if (!propertyDescriptor.IsReadOnly && !isNullValueOnNonNullableType)
+ {
+ try
+ {
+ propertyDescriptor.SetValue(bindingContext.Model, value);
+ }
+ catch (Exception ex)
+ {
+ // Only add if we're not already invalid
+ if (bindingContext.ModelState.IsValidField(modelStateKey))
+ {
+ bindingContext.ModelState.AddModelError(modelStateKey, ex);
+ }
+ }
+ }
+
+ // Last chance for an error on null values with non-nullable types, we'll use
+ // the default "A value is required." message.
+ if (isNullValueOnNonNullableType && bindingContext.ModelState.IsValidField(modelStateKey))
+ {
+ bindingContext.ModelState.AddModelError(modelStateKey, GetValueRequiredResource(controllerContext));
+ }
+ }
+
+ private static bool ShouldPerformRequestValidation(ControllerContext controllerContext, ModelBindingContext bindingContext)
+ {
+ if (controllerContext == null || controllerContext.Controller == null || bindingContext == null || bindingContext.ModelMetadata == null)
+ {
+ // To make unit testing easier, if the caller hasn't specified enough contextual information we just default
+ // to always pulling the data from a collection that goes through request validation.
+ return true;
+ }
+
+ // We should perform request validation only if both the controller and the model ask for it. This is the
+ // default behavior for both. If either the controller (via [ValidateInput(false)]) or the model (via [AllowHtml])
+ // opts out, we don't validate.
+ return (controllerContext.Controller.ValidateRequest && bindingContext.ModelMetadata.RequestValidationEnabled);
+ }
+
+ private static bool ShouldUpdateProperty(PropertyDescriptor property, Predicate<string> propertyFilter)
+ {
+ if (property.IsReadOnly && !CanUpdateReadonlyTypedReference(property.PropertyType))
+ {
+ return false;
+ }
+
+ // if this property is rejected by the filter, move on
+ if (!propertyFilter(property.Name))
+ {
+ return false;
+ }
+
+ // otherwise, allow
+ return true;
+ }
+
+ internal object UpdateCollection(ControllerContext controllerContext, ModelBindingContext bindingContext, Type elementType)
+ {
+ bool stopOnIndexNotFound;
+ IEnumerable<string> indexes;
+ GetIndexes(bindingContext, out stopOnIndexNotFound, out indexes);
+ IModelBinder elementBinder = Binders.GetBinder(elementType);
+
+ // build up a list of items from the request
+ List<object> modelList = new List<object>();
+ foreach (string currentIndex in indexes)
+ {
+ string subIndexKey = CreateSubIndexName(bindingContext.ModelName, currentIndex);
+ if (!bindingContext.ValueProvider.ContainsPrefix(subIndexKey))
+ {
+ if (stopOnIndexNotFound)
+ {
+ // we ran out of elements to pull
+ break;
+ }
+ else
+ {
+ continue;
+ }
+ }
+
+ ModelBindingContext innerContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, elementType),
+ ModelName = subIndexKey,
+ ModelState = bindingContext.ModelState,
+ PropertyFilter = bindingContext.PropertyFilter,
+ ValueProvider = bindingContext.ValueProvider
+ };
+ object thisElement = elementBinder.BindModel(controllerContext, innerContext);
+
+ // we need to merge model errors up
+ AddValueRequiredMessageToModelState(controllerContext, bindingContext.ModelState, subIndexKey, elementType, thisElement);
+ modelList.Add(thisElement);
+ }
+
+ // if there weren't any elements at all in the request, just return
+ if (modelList.Count == 0)
+ {
+ return null;
+ }
+
+ // replace the original collection
+ object collection = bindingContext.Model;
+ CollectionHelpers.ReplaceCollection(elementType, collection, modelList);
+ return collection;
+ }
+
+ internal object UpdateDictionary(ControllerContext controllerContext, ModelBindingContext bindingContext, Type keyType, Type valueType)
+ {
+ bool stopOnIndexNotFound;
+ IEnumerable<string> indexes;
+ GetIndexes(bindingContext, out stopOnIndexNotFound, out indexes);
+
+ IModelBinder keyBinder = Binders.GetBinder(keyType);
+ IModelBinder valueBinder = Binders.GetBinder(valueType);
+
+ // build up a list of items from the request
+ List<KeyValuePair<object, object>> modelList = new List<KeyValuePair<object, object>>();
+ foreach (string currentIndex in indexes)
+ {
+ string subIndexKey = CreateSubIndexName(bindingContext.ModelName, currentIndex);
+ string keyFieldKey = CreateSubPropertyName(subIndexKey, "key");
+ string valueFieldKey = CreateSubPropertyName(subIndexKey, "value");
+
+ if (!(bindingContext.ValueProvider.ContainsPrefix(keyFieldKey) && bindingContext.ValueProvider.ContainsPrefix(valueFieldKey)))
+ {
+ if (stopOnIndexNotFound)
+ {
+ // we ran out of elements to pull
+ break;
+ }
+ else
+ {
+ continue;
+ }
+ }
+
+ // bind the key
+ ModelBindingContext keyBindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, keyType),
+ ModelName = keyFieldKey,
+ ModelState = bindingContext.ModelState,
+ ValueProvider = bindingContext.ValueProvider
+ };
+ object thisKey = keyBinder.BindModel(controllerContext, keyBindingContext);
+
+ // we need to merge model errors up
+ AddValueRequiredMessageToModelState(controllerContext, bindingContext.ModelState, keyFieldKey, keyType, thisKey);
+ if (!keyType.IsInstanceOfType(thisKey))
+ {
+ // we can't add an invalid key, so just move on
+ continue;
+ }
+
+ // bind the value
+ modelList.Add(CreateEntryForModel(controllerContext, bindingContext, valueType, valueBinder, valueFieldKey, thisKey));
+ }
+
+ // Let's try another method
+ if (modelList.Count == 0)
+ {
+ IEnumerableValueProvider enumerableValueProvider = bindingContext.ValueProvider as IEnumerableValueProvider;
+ if (enumerableValueProvider != null)
+ {
+ IDictionary<string, string> keys = enumerableValueProvider.GetKeysFromPrefix(bindingContext.ModelName);
+ foreach (var thisKey in keys)
+ {
+ modelList.Add(CreateEntryForModel(controllerContext, bindingContext, valueType, valueBinder, thisKey.Value, thisKey.Key));
+ }
+ }
+ }
+
+ // if there weren't any elements at all in the request, just return
+ if (modelList.Count == 0)
+ {
+ return null;
+ }
+
+ // replace the original collection
+ object dictionary = bindingContext.Model;
+ CollectionHelpers.ReplaceDictionary(keyType, valueType, dictionary, modelList);
+ return dictionary;
+ }
+
+ private static KeyValuePair<object, object> CreateEntryForModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type valueType, IModelBinder valueBinder, string modelName, object modelKey)
+ {
+ ModelBindingContext valueBindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, valueType),
+ ModelName = modelName,
+ ModelState = bindingContext.ModelState,
+ PropertyFilter = bindingContext.PropertyFilter,
+ ValueProvider = bindingContext.ValueProvider
+ };
+ object thisValue = valueBinder.BindModel(controllerContext, valueBindingContext);
+ AddValueRequiredMessageToModelState(controllerContext, bindingContext.ModelState, modelName, valueType, thisValue);
+ return new KeyValuePair<object, object>(modelKey, thisValue);
+ }
+
+ // This helper type is used because we're working with strongly-typed collections, but we don't know the Ts
+ // ahead of time. By using the generic methods below, we can consolidate the collection-specific code in a
+ // single helper type rather than having reflection-based calls spread throughout the DefaultModelBinder type.
+ // There is a single point of entry to each of the methods below, so they're fairly simple to maintain.
+
+ private static class CollectionHelpers
+ {
+ private static readonly MethodInfo _replaceCollectionMethod = typeof(CollectionHelpers).GetMethod("ReplaceCollectionImpl", BindingFlags.Static | BindingFlags.NonPublic);
+ private static readonly MethodInfo _replaceDictionaryMethod = typeof(CollectionHelpers).GetMethod("ReplaceDictionaryImpl", BindingFlags.Static | BindingFlags.NonPublic);
+
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
+ public static void ReplaceCollection(Type collectionType, object collection, object newContents)
+ {
+ MethodInfo targetMethod = _replaceCollectionMethod.MakeGenericMethod(collectionType);
+ targetMethod.Invoke(null, new object[] { collection, newContents });
+ }
+
+ private static void ReplaceCollectionImpl<T>(ICollection<T> collection, IEnumerable newContents)
+ {
+ collection.Clear();
+ if (newContents != null)
+ {
+ foreach (object item in newContents)
+ {
+ // if the item was not a T, some conversion failed. the error message will be propagated,
+ // but in the meanwhile we need to make a placeholder element in the array.
+ T castItem = (item is T) ? (T)item : default(T);
+ collection.Add(castItem);
+ }
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
+ public static void ReplaceDictionary(Type keyType, Type valueType, object dictionary, object newContents)
+ {
+ MethodInfo targetMethod = _replaceDictionaryMethod.MakeGenericMethod(keyType, valueType);
+ targetMethod.Invoke(null, new object[] { dictionary, newContents });
+ }
+
+ private static void ReplaceDictionaryImpl<TKey, TValue>(IDictionary<TKey, TValue> dictionary, IEnumerable<KeyValuePair<object, object>> newContents)
+ {
+ dictionary.Clear();
+ foreach (KeyValuePair<object, object> item in newContents)
+ {
+ // if the item was not a T, some conversion failed. the error message will be propagated,
+ // but in the meanwhile we need to make a placeholder element in the dictionary.
+ TKey castKey = (TKey)item.Key; // this cast shouldn't fail
+ TValue castValue = (item.Value is TValue) ? (TValue)item.Value : default(TValue);
+ dictionary[castKey] = castValue;
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/DefaultViewLocationCache.cs b/src/System.Web.Mvc/DefaultViewLocationCache.cs
new file mode 100644
index 00000000..3e6090f6
--- /dev/null
+++ b/src/System.Web.Mvc/DefaultViewLocationCache.cs
@@ -0,0 +1,52 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Web.Caching;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc
+{
+ public class DefaultViewLocationCache : IViewLocationCache
+ {
+ private static readonly TimeSpan _defaultTimeSpan = new TimeSpan(0, 15, 0);
+
+ [SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", Justification = "The reference type is immutable. ")]
+ public static readonly IViewLocationCache Null = new NullViewLocationCache();
+
+ public DefaultViewLocationCache()
+ : this(_defaultTimeSpan)
+ {
+ }
+
+ public DefaultViewLocationCache(TimeSpan timeSpan)
+ {
+ if (timeSpan.Ticks < 0)
+ {
+ throw new InvalidOperationException(MvcResources.DefaultViewLocationCache_NegativeTimeSpan);
+ }
+ TimeSpan = timeSpan;
+ }
+
+ public TimeSpan TimeSpan { get; private set; }
+
+ #region IViewLocationCache Members
+
+ public string GetViewLocation(HttpContextBase httpContext, string key)
+ {
+ if (httpContext == null)
+ {
+ throw new ArgumentNullException("httpContext");
+ }
+ return (string)httpContext.Cache[key];
+ }
+
+ public void InsertViewLocation(HttpContextBase httpContext, string key, string virtualPath)
+ {
+ if (httpContext == null)
+ {
+ throw new ArgumentNullException("httpContext");
+ }
+ httpContext.Cache.Insert(key, virtualPath, null /* dependencies */, Cache.NoAbsoluteExpiration, TimeSpan);
+ }
+
+ #endregion
+ }
+}
diff --git a/src/System.Web.Mvc/DependencyResolver.cs b/src/System.Web.Mvc/DependencyResolver.cs
new file mode 100644
index 00000000..6fdb0807
--- /dev/null
+++ b/src/System.Web.Mvc/DependencyResolver.cs
@@ -0,0 +1,210 @@
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using System.Reflection;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc
+{
+ public class DependencyResolver
+ {
+ private static DependencyResolver _instance = new DependencyResolver();
+
+ private IDependencyResolver _current;
+
+ /// <summary>
+ /// Cache should always be a new CacheDependencyResolver(_current).
+ /// </summary>
+ private CacheDependencyResolver _currentCache;
+
+ public DependencyResolver()
+ {
+ InnerSetResolver(new DefaultDependencyResolver());
+ }
+
+ public static IDependencyResolver Current
+ {
+ get { return _instance.InnerCurrent; }
+ }
+
+ internal static IDependencyResolver CurrentCache
+ {
+ get { return _instance.InnerCurrentCache; }
+ }
+
+ public IDependencyResolver InnerCurrent
+ {
+ get { return _current; }
+ }
+
+ /// <summary>
+ /// Provides caching over results returned by Current.
+ /// </summary>
+ internal IDependencyResolver InnerCurrentCache
+ {
+ get { return _currentCache; }
+ }
+
+ public static void SetResolver(IDependencyResolver resolver)
+ {
+ _instance.InnerSetResolver(resolver);
+ }
+
+ public static void SetResolver(object commonServiceLocator)
+ {
+ _instance.InnerSetResolver(commonServiceLocator);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types.")]
+ public static void SetResolver(Func<Type, object> getService, Func<Type, IEnumerable<object>> getServices)
+ {
+ _instance.InnerSetResolver(getService, getServices);
+ }
+
+ public void InnerSetResolver(IDependencyResolver resolver)
+ {
+ if (resolver == null)
+ {
+ throw new ArgumentNullException("resolver");
+ }
+
+ _current = resolver;
+ _currentCache = new CacheDependencyResolver(_current);
+ }
+
+ public void InnerSetResolver(object commonServiceLocator)
+ {
+ if (commonServiceLocator == null)
+ {
+ throw new ArgumentNullException("commonServiceLocator");
+ }
+
+ Type locatorType = commonServiceLocator.GetType();
+ MethodInfo getInstance = locatorType.GetMethod("GetInstance", new[] { typeof(Type) });
+ MethodInfo getInstances = locatorType.GetMethod("GetAllInstances", new[] { typeof(Type) });
+
+ if (getInstance == null ||
+ getInstance.ReturnType != typeof(object) ||
+ getInstances == null ||
+ getInstances.ReturnType != typeof(IEnumerable<object>))
+ {
+ throw new ArgumentException(
+ String.Format(
+ CultureInfo.CurrentCulture,
+ MvcResources.DependencyResolver_DoesNotImplementICommonServiceLocator,
+ locatorType.FullName),
+ "commonServiceLocator");
+ }
+
+ var getService = (Func<Type, object>)Delegate.CreateDelegate(typeof(Func<Type, object>), commonServiceLocator, getInstance);
+ var getServices = (Func<Type, IEnumerable<object>>)Delegate.CreateDelegate(typeof(Func<Type, IEnumerable<object>>), commonServiceLocator, getInstances);
+
+ InnerSetResolver(new DelegateBasedDependencyResolver(getService, getServices));
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types.")]
+ public void InnerSetResolver(Func<Type, object> getService, Func<Type, IEnumerable<object>> getServices)
+ {
+ if (getService == null)
+ {
+ throw new ArgumentNullException("getService");
+ }
+ if (getServices == null)
+ {
+ throw new ArgumentNullException("getServices");
+ }
+
+ InnerSetResolver(new DelegateBasedDependencyResolver(getService, getServices));
+ }
+
+ /// <summary>
+ /// Wraps an IDependencyResolver and ensures single instance per-type.
+ /// </summary>
+ /// <remarks>
+ /// Note it's possible for multiple threads to race and call the _resolver service multiple times.
+ /// We'll pick one winner and ignore the others and still guarantee a unique instance.
+ /// </remarks>
+ private sealed class CacheDependencyResolver : IDependencyResolver
+ {
+ private readonly ConcurrentDictionary<Type, object> _cache = new ConcurrentDictionary<Type, object>();
+ private readonly ConcurrentDictionary<Type, IEnumerable<object>> _cacheMultiple = new ConcurrentDictionary<Type, IEnumerable<object>>();
+
+ private readonly IDependencyResolver _resolver;
+
+ public CacheDependencyResolver(IDependencyResolver resolver)
+ {
+ _resolver = resolver;
+ }
+
+ public object GetService(Type serviceType)
+ {
+ return _cache.GetOrAdd(serviceType, _resolver.GetService);
+ }
+
+ public IEnumerable<object> GetServices(Type serviceType)
+ {
+ return _cacheMultiple.GetOrAdd(serviceType, _resolver.GetServices);
+ }
+ }
+
+ private class DefaultDependencyResolver : IDependencyResolver
+ {
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "This method might throw exceptions whose type we cannot strongly link against; namely, ActivationException from common service locator")]
+ public object GetService(Type serviceType)
+ {
+ // Since attempting to create an instance of an interface or an abstract type results in an exception, immediately return null
+ // to improve performance and the debugging experience with first-chance exceptions enabled.
+ if (serviceType.IsInterface || serviceType.IsAbstract)
+ {
+ return null;
+ }
+
+ try
+ {
+ return Activator.CreateInstance(serviceType);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ public IEnumerable<object> GetServices(Type serviceType)
+ {
+ return Enumerable.Empty<object>();
+ }
+ }
+
+ private class DelegateBasedDependencyResolver : IDependencyResolver
+ {
+ private Func<Type, object> _getService;
+ private Func<Type, IEnumerable<object>> _getServices;
+
+ public DelegateBasedDependencyResolver(Func<Type, object> getService, Func<Type, IEnumerable<object>> getServices)
+ {
+ _getService = getService;
+ _getServices = getServices;
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "This method might throw exceptions whose type we cannot strongly link against; namely, ActivationException from common service locator")]
+ public object GetService(Type type)
+ {
+ try
+ {
+ return _getService.Invoke(type);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ public IEnumerable<object> GetServices(Type type)
+ {
+ return _getServices(type);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/DependencyResolverExtensions.cs b/src/System.Web.Mvc/DependencyResolverExtensions.cs
new file mode 100644
index 00000000..db9c5129
--- /dev/null
+++ b/src/System.Web.Mvc/DependencyResolverExtensions.cs
@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace System.Web.Mvc
+{
+ public static class DependencyResolverExtensions
+ {
+ public static TService GetService<TService>(this IDependencyResolver resolver)
+ {
+ return (TService)resolver.GetService(typeof(TService));
+ }
+
+ public static IEnumerable<TService> GetServices<TService>(this IDependencyResolver resolver)
+ {
+ return resolver.GetServices(typeof(TService)).Cast<TService>();
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/DescriptorUtil.cs b/src/System.Web.Mvc/DescriptorUtil.cs
new file mode 100644
index 00000000..8e7905fb
--- /dev/null
+++ b/src/System.Web.Mvc/DescriptorUtil.cs
@@ -0,0 +1,86 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.Reflection;
+using System.Text;
+using System.Threading;
+
+namespace System.Web.Mvc
+{
+ internal static class DescriptorUtil
+ {
+ private static void AppendPartToUniqueIdBuilder(StringBuilder builder, object part)
+ {
+ if (part == null)
+ {
+ builder.Append("[-1]");
+ }
+ else
+ {
+ string partString = Convert.ToString(part, CultureInfo.InvariantCulture);
+ builder.AppendFormat("[{0}]{1}", partString.Length, partString);
+ }
+ }
+
+ public static string CreateUniqueId(params object[] parts)
+ {
+ return CreateUniqueId((IEnumerable<object>)parts);
+ }
+
+ public static string CreateUniqueId(IEnumerable<object> parts)
+ {
+ // returns a unique string made up of the pieces passed in
+ StringBuilder builder = new StringBuilder();
+ foreach (object part in parts)
+ {
+ // We can special-case certain part types
+
+ MemberInfo memberInfo = part as MemberInfo;
+ if (memberInfo != null)
+ {
+ AppendPartToUniqueIdBuilder(builder, memberInfo.Module.ModuleVersionId);
+ AppendPartToUniqueIdBuilder(builder, memberInfo.MetadataToken);
+ continue;
+ }
+
+ IUniquelyIdentifiable uniquelyIdentifiable = part as IUniquelyIdentifiable;
+ if (uniquelyIdentifiable != null)
+ {
+ AppendPartToUniqueIdBuilder(builder, uniquelyIdentifiable.UniqueId);
+ continue;
+ }
+
+ AppendPartToUniqueIdBuilder(builder, part);
+ }
+
+ return builder.ToString();
+ }
+
+ public static TDescriptor[] LazilyFetchOrCreateDescriptors<TReflection, TDescriptor>(ref TDescriptor[] cacheLocation, Func<TReflection[]> initializer, Func<TReflection, TDescriptor> converter)
+ {
+ // did we already calculate this once?
+ TDescriptor[] existingCache = Interlocked.CompareExchange(ref cacheLocation, null, null);
+ if (existingCache != null)
+ {
+ return existingCache;
+ }
+
+ // Note: since this code operates on arrays it is more efficient to call simple array operations
+ // instead of LINQ-y extension methods such as Select and Where. DO NOT attempt to simplify this
+ // without testing the performance impact.
+ TReflection[] memberInfos = initializer();
+ List<TDescriptor> descriptorsList = new List<TDescriptor>(memberInfos.Length);
+ for (int i = 0; i < memberInfos.Length; i++)
+ {
+ TDescriptor descriptor = converter(memberInfos[i]);
+ if (descriptor != null)
+ {
+ descriptorsList.Add(descriptor);
+ }
+ }
+ TDescriptor[] descriptors = descriptorsList.ToArray();
+
+ TDescriptor[] updatedCache = Interlocked.CompareExchange(ref cacheLocation, descriptors, null);
+ return updatedCache ?? descriptors;
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/DictionaryHelpers.cs b/src/System.Web.Mvc/DictionaryHelpers.cs
new file mode 100644
index 00000000..7f917971
--- /dev/null
+++ b/src/System.Web.Mvc/DictionaryHelpers.cs
@@ -0,0 +1,56 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace System.Web.Mvc
+{
+ internal static class DictionaryHelpers
+ {
+ public static IEnumerable<KeyValuePair<string, TValue>> FindKeysWithPrefix<TValue>(IDictionary<string, TValue> dictionary, string prefix)
+ {
+ TValue exactMatchValue;
+ if (dictionary.TryGetValue(prefix, out exactMatchValue))
+ {
+ yield return new KeyValuePair<string, TValue>(prefix, exactMatchValue);
+ }
+
+ foreach (var entry in dictionary)
+ {
+ string key = entry.Key;
+
+ if (key.Length <= prefix.Length)
+ {
+ continue;
+ }
+
+ if (!key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ char charAfterPrefix = key[prefix.Length];
+ switch (charAfterPrefix)
+ {
+ case '[':
+ case '.':
+ yield return entry;
+ break;
+ }
+ }
+ }
+
+ public static bool DoesAnyKeyHavePrefix<TValue>(IDictionary<string, TValue> dictionary, string prefix)
+ {
+ return FindKeysWithPrefix(dictionary, prefix).Any();
+ }
+
+ public static TValue GetOrDefault<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key, TValue @default)
+ {
+ TValue value;
+ if (dict.TryGetValue(key, out value))
+ {
+ return value;
+ }
+ return @default;
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/DictionaryValueProvider`1.cs b/src/System.Web.Mvc/DictionaryValueProvider`1.cs
new file mode 100644
index 00000000..d1abfa94
--- /dev/null
+++ b/src/System.Web.Mvc/DictionaryValueProvider`1.cs
@@ -0,0 +1,65 @@
+using System.Collections.Generic;
+using System.Globalization;
+
+namespace System.Web.Mvc
+{
+ public class DictionaryValueProvider<TValue> : IValueProvider, IEnumerableValueProvider
+ {
+ private readonly HashSet<string> _prefixes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+ private readonly Dictionary<string, ValueProviderResult> _values = new Dictionary<string, ValueProviderResult>(StringComparer.OrdinalIgnoreCase);
+
+ public DictionaryValueProvider(IDictionary<string, TValue> dictionary, CultureInfo culture)
+ {
+ if (dictionary == null)
+ {
+ throw new ArgumentNullException("dictionary");
+ }
+
+ AddValues(dictionary, culture);
+ }
+
+ private void AddValues(IDictionary<string, TValue> dictionary, CultureInfo culture)
+ {
+ if (dictionary.Count > 0)
+ {
+ _prefixes.Add(String.Empty);
+ }
+
+ foreach (var entry in dictionary)
+ {
+ _prefixes.UnionWith(ValueProviderUtil.GetPrefixes(entry.Key));
+
+ object rawValue = entry.Value;
+ string attemptedValue = Convert.ToString(rawValue, culture);
+ _values[entry.Key] = new ValueProviderResult(rawValue, attemptedValue, culture);
+ }
+ }
+
+ public virtual bool ContainsPrefix(string prefix)
+ {
+ if (prefix == null)
+ {
+ throw new ArgumentNullException("prefix");
+ }
+
+ return _prefixes.Contains(prefix);
+ }
+
+ public virtual ValueProviderResult GetValue(string key)
+ {
+ if (key == null)
+ {
+ throw new ArgumentNullException("key");
+ }
+
+ ValueProviderResult valueProviderResult;
+ _values.TryGetValue(key, out valueProviderResult);
+ return valueProviderResult;
+ }
+
+ public virtual IDictionary<string, string> GetKeysFromPrefix(string prefix)
+ {
+ return ValueProviderUtil.GetKeysFromPrefix(_prefixes, prefix);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/DynamicViewDataDictionary.cs b/src/System.Web.Mvc/DynamicViewDataDictionary.cs
new file mode 100644
index 00000000..b7b36b75
--- /dev/null
+++ b/src/System.Web.Mvc/DynamicViewDataDictionary.cs
@@ -0,0 +1,47 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Dynamic;
+
+namespace System.Web.Mvc
+{
+ internal sealed class DynamicViewDataDictionary : DynamicObject
+ {
+ private readonly Func<ViewDataDictionary> _viewDataThunk;
+
+ public DynamicViewDataDictionary(Func<ViewDataDictionary> viewDataThunk)
+ {
+ _viewDataThunk = viewDataThunk;
+ }
+
+ private ViewDataDictionary ViewData
+ {
+ get
+ {
+ ViewDataDictionary viewData = _viewDataThunk();
+ Debug.Assert(viewData != null);
+ return viewData;
+ }
+ }
+
+ // Implementing this function improves the debugging experience as it provides the debugger with the list of all
+ // the properties currently defined on the object
+ public override IEnumerable<string> GetDynamicMemberNames()
+ {
+ return ViewData.Keys;
+ }
+
+ public override bool TryGetMember(GetMemberBinder binder, out object result)
+ {
+ result = ViewData[binder.Name];
+ // since ViewDataDictionary always returns a result even if the key does not exist, always return true
+ return true;
+ }
+
+ public override bool TrySetMember(SetMemberBinder binder, object value)
+ {
+ ViewData[binder.Name] = value;
+ // you can always set a key in the dictionary so return true
+ return true;
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/EmptyModelMetadataProvider.cs b/src/System.Web.Mvc/EmptyModelMetadataProvider.cs
new file mode 100644
index 00000000..000f334c
--- /dev/null
+++ b/src/System.Web.Mvc/EmptyModelMetadataProvider.cs
@@ -0,0 +1,12 @@
+using System.Collections.Generic;
+
+namespace System.Web.Mvc
+{
+ public class EmptyModelMetadataProvider : AssociatedMetadataProvider
+ {
+ protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
+ {
+ return new ModelMetadata(this, containerType, modelAccessor, modelType, propertyName);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/EmptyModelValidatorProvider.cs b/src/System.Web.Mvc/EmptyModelValidatorProvider.cs
new file mode 100644
index 00000000..0bd617c4
--- /dev/null
+++ b/src/System.Web.Mvc/EmptyModelValidatorProvider.cs
@@ -0,0 +1,13 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace System.Web.Mvc
+{
+ public class EmptyModelValidatorProvider : ModelValidatorProvider
+ {
+ public override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context)
+ {
+ return Enumerable.Empty<ModelValidator>();
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/EmptyResult.cs b/src/System.Web.Mvc/EmptyResult.cs
new file mode 100644
index 00000000..f67ab681
--- /dev/null
+++ b/src/System.Web.Mvc/EmptyResult.cs
@@ -0,0 +1,17 @@
+namespace System.Web.Mvc
+{
+ // represents a result that doesn't do anything, like a controller action returning null
+ public class EmptyResult : ActionResult
+ {
+ private static readonly EmptyResult _singleton = new EmptyResult();
+
+ internal static EmptyResult Instance
+ {
+ get { return _singleton; }
+ }
+
+ public override void ExecuteResult(ControllerContext context)
+ {
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Error.cs b/src/System.Web.Mvc/Error.cs
new file mode 100644
index 00000000..173de8ca
--- /dev/null
+++ b/src/System.Web.Mvc/Error.cs
@@ -0,0 +1,76 @@
+using System.Globalization;
+using System.Web.Mvc.Async;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc
+{
+ internal static class Error
+ {
+ public static InvalidOperationException AsyncActionMethodSelector_CouldNotFindMethod(string methodName, Type controllerType)
+ {
+ string message = String.Format(CultureInfo.CurrentCulture, MvcResources.AsyncActionMethodSelector_CouldNotFindMethod,
+ methodName, controllerType);
+ return new InvalidOperationException(message);
+ }
+
+ public static InvalidOperationException AsyncCommon_AsyncResultAlreadyConsumed()
+ {
+ return new InvalidOperationException(MvcResources.AsyncCommon_AsyncResultAlreadyConsumed);
+ }
+
+ public static InvalidOperationException AsyncCommon_ControllerMustImplementIAsyncManagerContainer(Type actualControllerType)
+ {
+ string message = String.Format(CultureInfo.CurrentCulture, MvcResources.AsyncCommon_ControllerMustImplementIAsyncManagerContainer,
+ actualControllerType);
+ return new InvalidOperationException(message);
+ }
+
+ public static ArgumentException AsyncCommon_InvalidAsyncResult(string parameterName)
+ {
+ return new ArgumentException(MvcResources.AsyncCommon_InvalidAsyncResult, parameterName);
+ }
+
+ public static ArgumentOutOfRangeException AsyncCommon_InvalidTimeout(string parameterName)
+ {
+ return new ArgumentOutOfRangeException(parameterName, MvcResources.AsyncCommon_InvalidTimeout);
+ }
+
+ public static InvalidOperationException ChildActionOnlyAttribute_MustBeInChildRequest(ActionDescriptor actionDescriptor)
+ {
+ string message = String.Format(CultureInfo.CurrentCulture, MvcResources.ChildActionOnlyAttribute_MustBeInChildRequest,
+ actionDescriptor.ActionName);
+ return new InvalidOperationException(message);
+ }
+
+ public static ArgumentException ParameterCannotBeNullOrEmpty(string parameterName)
+ {
+ return new ArgumentException(MvcResources.Common_NullOrEmpty, parameterName);
+ }
+
+ public static InvalidOperationException PropertyCannotBeNullOrEmpty(string propertyName)
+ {
+ string message = String.Format(CultureInfo.CurrentCulture, MvcResources.Common_PropertyCannotBeNullOrEmpty,
+ propertyName);
+ return new InvalidOperationException(message);
+ }
+
+ public static SynchronousOperationException SynchronizationContextUtil_ExceptionThrown(Exception innerException)
+ {
+ return new SynchronousOperationException(MvcResources.SynchronizationContextUtil_ExceptionThrown, innerException);
+ }
+
+ public static InvalidOperationException ViewDataDictionary_WrongTModelType(Type valueType, Type modelType)
+ {
+ string message = String.Format(CultureInfo.CurrentCulture, MvcResources.ViewDataDictionary_WrongTModelType,
+ valueType, modelType);
+ return new InvalidOperationException(message);
+ }
+
+ public static InvalidOperationException ViewDataDictionary_ModelCannotBeNull(Type modelType)
+ {
+ string message = String.Format(CultureInfo.CurrentCulture, MvcResources.ViewDataDictionary_ModelCannotBeNull,
+ modelType);
+ return new InvalidOperationException(message);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ExceptionContext.cs b/src/System.Web.Mvc/ExceptionContext.cs
new file mode 100644
index 00000000..29cbf6df
--- /dev/null
+++ b/src/System.Web.Mvc/ExceptionContext.cs
@@ -0,0 +1,36 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Web.Mvc
+{
+ public class ExceptionContext : ControllerContext
+ {
+ private ActionResult _result;
+
+ // parameterless constructor used for mocking
+ public ExceptionContext()
+ {
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "The virtual property setters are only to support mocking frameworks, in which case this constructor shouldn't be called anyway.")]
+ public ExceptionContext(ControllerContext controllerContext, Exception exception)
+ : base(controllerContext)
+ {
+ if (exception == null)
+ {
+ throw new ArgumentNullException("exception");
+ }
+
+ Exception = exception;
+ }
+
+ public virtual Exception Exception { get; set; }
+
+ public bool ExceptionHandled { get; set; }
+
+ public ActionResult Result
+ {
+ get { return _result ?? EmptyResult.Instance; }
+ set { _result = value; }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ExpressionHelper.cs b/src/System.Web.Mvc/ExpressionHelper.cs
new file mode 100644
index 00000000..69432305
--- /dev/null
+++ b/src/System.Web.Mvc/ExpressionHelper.cs
@@ -0,0 +1,131 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Web.Mvc.ExpressionUtil;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc
+{
+ public static class ExpressionHelper
+ {
+ public static string GetExpressionText(string expression)
+ {
+ return
+ String.Equals(expression, "model", StringComparison.OrdinalIgnoreCase)
+ ? String.Empty // If it's exactly "model", then give them an empty string, to replicate the lambda behavior
+ : expression;
+ }
+
+ public static string GetExpressionText(LambdaExpression expression)
+ {
+ // Split apart the expression string for property/field accessors to create its name
+ Stack<string> nameParts = new Stack<string>();
+ Expression part = expression.Body;
+
+ while (part != null)
+ {
+ if (part.NodeType == ExpressionType.Call)
+ {
+ MethodCallExpression methodExpression = (MethodCallExpression)part;
+
+ if (!IsSingleArgumentIndexer(methodExpression))
+ {
+ break;
+ }
+
+ nameParts.Push(
+ GetIndexerInvocation(
+ methodExpression.Arguments.Single(),
+ expression.Parameters.ToArray()));
+
+ part = methodExpression.Object;
+ }
+ else if (part.NodeType == ExpressionType.ArrayIndex)
+ {
+ BinaryExpression binaryExpression = (BinaryExpression)part;
+
+ nameParts.Push(
+ GetIndexerInvocation(
+ binaryExpression.Right,
+ expression.Parameters.ToArray()));
+
+ part = binaryExpression.Left;
+ }
+ else if (part.NodeType == ExpressionType.MemberAccess)
+ {
+ MemberExpression memberExpressionPart = (MemberExpression)part;
+ nameParts.Push("." + memberExpressionPart.Member.Name);
+ part = memberExpressionPart.Expression;
+ }
+ else if (part.NodeType == ExpressionType.Parameter)
+ {
+ // Dev10 Bug #907611
+ // When the expression is parameter based (m => m.Something...), we'll push an empty
+ // string onto the stack and stop evaluating. The extra empty string makes sure that
+ // we don't accidentally cut off too much of m => m.Model.
+ nameParts.Push(String.Empty);
+ part = null;
+ }
+ else
+ {
+ break;
+ }
+ }
+
+ // If it starts with "model", then strip that away
+ if (nameParts.Count > 0 && String.Equals(nameParts.Peek(), ".model", StringComparison.OrdinalIgnoreCase))
+ {
+ nameParts.Pop();
+ }
+
+ if (nameParts.Count > 0)
+ {
+ return nameParts.Aggregate((left, right) => left + right).TrimStart('.');
+ }
+
+ return String.Empty;
+ }
+
+ private static string GetIndexerInvocation(Expression expression, ParameterExpression[] parameters)
+ {
+ Expression converted = Expression.Convert(expression, typeof(object));
+ ParameterExpression fakeParameter = Expression.Parameter(typeof(object), null);
+ Expression<Func<object, object>> lambda = Expression.Lambda<Func<object, object>>(converted, fakeParameter);
+ Func<object, object> func;
+
+ try
+ {
+ func = CachedExpressionCompiler.Process(lambda);
+ }
+ catch (InvalidOperationException ex)
+ {
+ throw new InvalidOperationException(
+ String.Format(
+ CultureInfo.CurrentCulture,
+ MvcResources.ExpressionHelper_InvalidIndexerExpression,
+ expression,
+ parameters[0].Name),
+ ex);
+ }
+
+ return "[" + Convert.ToString(func(null), CultureInfo.InvariantCulture) + "]";
+ }
+
+ internal static bool IsSingleArgumentIndexer(Expression expression)
+ {
+ MethodCallExpression methodExpression = expression as MethodCallExpression;
+ if (methodExpression == null || methodExpression.Arguments.Count != 1)
+ {
+ return false;
+ }
+
+ return methodExpression.Method
+ .DeclaringType
+ .GetDefaultMembers()
+ .OfType<PropertyInfo>()
+ .Any(p => p.GetGetMethod() == methodExpression.Method);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ExpressionUtil/BinaryExpressionFingerprint.cs b/src/System.Web.Mvc/ExpressionUtil/BinaryExpressionFingerprint.cs
new file mode 100644
index 00000000..e1c50e74
--- /dev/null
+++ b/src/System.Web.Mvc/ExpressionUtil/BinaryExpressionFingerprint.cs
@@ -0,0 +1,41 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Linq.Expressions;
+using System.Reflection;
+
+#pragma warning disable 659 // overrides AddToHashCodeCombiner instead
+
+namespace System.Web.Mvc.ExpressionUtil
+{
+ // BinaryExpression fingerprint class
+ // Useful for things like array[index]
+
+ [SuppressMessage("Microsoft.Usage", "CA2218:OverrideGetHashCodeOnOverridingEquals", Justification = "Overrides AddToHashCodeCombiner() instead.")]
+ internal sealed class BinaryExpressionFingerprint : ExpressionFingerprint
+ {
+ public BinaryExpressionFingerprint(ExpressionType nodeType, Type type, MethodInfo method)
+ : base(nodeType, type)
+ {
+ // Other properties on BinaryExpression (like IsLifted / IsLiftedToNull) are simply derived
+ // from Type and NodeType, so they're not necessary for inclusion in the fingerprint.
+
+ Method = method;
+ }
+
+ // http://msdn.microsoft.com/en-us/library/system.linq.expressions.binaryexpression.method.aspx
+ public MethodInfo Method { get; private set; }
+
+ public override bool Equals(object obj)
+ {
+ BinaryExpressionFingerprint other = obj as BinaryExpressionFingerprint;
+ return (other != null)
+ && Equals(this.Method, other.Method)
+ && this.Equals(other);
+ }
+
+ internal override void AddToHashCodeCombiner(HashCodeCombiner combiner)
+ {
+ combiner.AddObject(Method);
+ base.AddToHashCodeCombiner(combiner);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ExpressionUtil/CachedExpressionCompiler.cs b/src/System.Web.Mvc/ExpressionUtil/CachedExpressionCompiler.cs
new file mode 100644
index 00000000..9d6d3fba
--- /dev/null
+++ b/src/System.Web.Mvc/ExpressionUtil/CachedExpressionCompiler.cs
@@ -0,0 +1,142 @@
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq.Expressions;
+using System.Reflection;
+
+namespace System.Web.Mvc.ExpressionUtil
+{
+ internal static class CachedExpressionCompiler
+ {
+ // This is the entry point to the cached expression compilation system. The system
+ // will try to turn the expression into an actual delegate as quickly as possible,
+ // relying on cache lookups and other techniques to save time if appropriate.
+ // If the provided expression is particularly obscure and the system doesn't know
+ // how to handle it, we'll just compile the expression as normal.
+ public static Func<TModel, TValue> Process<TModel, TValue>(Expression<Func<TModel, TValue>> lambdaExpression)
+ {
+ return Compiler<TModel, TValue>.Compile(lambdaExpression);
+ }
+
+ private static class Compiler<TIn, TOut>
+ {
+ private static Func<TIn, TOut> _identityFunc;
+
+ private static readonly ConcurrentDictionary<MemberInfo, Func<TIn, TOut>> _simpleMemberAccessDict =
+ new ConcurrentDictionary<MemberInfo, Func<TIn, TOut>>();
+
+ private static readonly ConcurrentDictionary<MemberInfo, Func<object, TOut>> _constMemberAccessDict =
+ new ConcurrentDictionary<MemberInfo, Func<object, TOut>>();
+
+ private static readonly ConcurrentDictionary<ExpressionFingerprintChain, Hoisted<TIn, TOut>> _fingerprintedCache =
+ new ConcurrentDictionary<ExpressionFingerprintChain, Hoisted<TIn, TOut>>();
+
+ public static Func<TIn, TOut> Compile(Expression<Func<TIn, TOut>> expr)
+ {
+ return CompileFromIdentityFunc(expr)
+ ?? CompileFromConstLookup(expr)
+ ?? CompileFromMemberAccess(expr)
+ ?? CompileFromFingerprint(expr)
+ ?? CompileSlow(expr);
+ }
+
+ private static Func<TIn, TOut> CompileFromConstLookup(Expression<Func<TIn, TOut>> expr)
+ {
+ ConstantExpression constExpr = expr.Body as ConstantExpression;
+ if (constExpr != null)
+ {
+ // model => {const}
+
+ TOut constantValue = (TOut)constExpr.Value;
+ return _ => constantValue;
+ }
+
+ return null;
+ }
+
+ private static Func<TIn, TOut> CompileFromIdentityFunc(Expression<Func<TIn, TOut>> expr)
+ {
+ if (expr.Body == expr.Parameters[0])
+ {
+ // model => model
+
+ // don't need to lock, as all identity funcs are identical
+ if (_identityFunc == null)
+ {
+ _identityFunc = expr.Compile();
+ }
+
+ return _identityFunc;
+ }
+
+ return null;
+ }
+
+ private static Func<TIn, TOut> CompileFromFingerprint(Expression<Func<TIn, TOut>> expr)
+ {
+ List<object> capturedConstants;
+ ExpressionFingerprintChain fingerprint = FingerprintingExpressionVisitor.GetFingerprintChain(expr, out capturedConstants);
+
+ if (fingerprint != null)
+ {
+ var del = _fingerprintedCache.GetOrAdd(fingerprint, _ =>
+ {
+ // Fingerprinting succeeded, but there was a cache miss. Rewrite the expression
+ // and add the rewritten expression to the cache.
+
+ var hoistedExpr = HoistingExpressionVisitor<TIn, TOut>.Hoist(expr);
+ return hoistedExpr.Compile();
+ });
+ return model => del(model, capturedConstants);
+ }
+
+ // couldn't be fingerprinted
+ return null;
+ }
+
+ private static Func<TIn, TOut> CompileFromMemberAccess(Expression<Func<TIn, TOut>> expr)
+ {
+ // Performance tests show that on the x64 platform, special-casing static member and
+ // captured local variable accesses is faster than letting the fingerprinting system
+ // handle them. On the x86 platform, the fingerprinting system is faster, but only
+ // by around one microsecond, so it's not worth it to complicate the logic here with
+ // an architecture check.
+
+ MemberExpression memberExpr = expr.Body as MemberExpression;
+ if (memberExpr != null)
+ {
+ if (memberExpr.Expression == expr.Parameters[0] || memberExpr.Expression == null)
+ {
+ // model => model.Member or model => StaticMember
+ return _simpleMemberAccessDict.GetOrAdd(memberExpr.Member, _ => expr.Compile());
+ }
+
+ ConstantExpression constExpr = memberExpr.Expression as ConstantExpression;
+ if (constExpr != null)
+ {
+ // model => {const}.Member (captured local variable)
+ var del = _constMemberAccessDict.GetOrAdd(memberExpr.Member, _ =>
+ {
+ // rewrite as capturedLocal => ((TDeclaringType)capturedLocal).Member
+ var constParamExpr = Expression.Parameter(typeof(object), "capturedLocal");
+ var constCastExpr = Expression.Convert(constParamExpr, memberExpr.Member.DeclaringType);
+ var newMemberAccessExpr = memberExpr.Update(constCastExpr);
+ var newLambdaExpr = Expression.Lambda<Func<object, TOut>>(newMemberAccessExpr, constParamExpr);
+ return newLambdaExpr.Compile();
+ });
+
+ object capturedLocal = constExpr.Value;
+ return _ => del(capturedLocal);
+ }
+ }
+
+ return null;
+ }
+
+ private static Func<TIn, TOut> CompileSlow(Expression<Func<TIn, TOut>> expr)
+ {
+ // fallback compilation system - just compile the expression directly
+ return expr.Compile();
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ExpressionUtil/ConditionalExpressionFingerprint.cs b/src/System.Web.Mvc/ExpressionUtil/ConditionalExpressionFingerprint.cs
new file mode 100644
index 00000000..ceb7d327
--- /dev/null
+++ b/src/System.Web.Mvc/ExpressionUtil/ConditionalExpressionFingerprint.cs
@@ -0,0 +1,28 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Linq.Expressions;
+
+#pragma warning disable 659 // overrides AddToHashCodeCombiner instead
+
+namespace System.Web.Mvc.ExpressionUtil
+{
+ // ConditionalExpression fingerprint class
+ // Expression of form (test) ? ifTrue : ifFalse
+
+ [SuppressMessage("Microsoft.Usage", "CA2218:OverrideGetHashCodeOnOverridingEquals", Justification = "Overrides AddToHashCodeCombiner() instead.")]
+ internal sealed class ConditionalExpressionFingerprint : ExpressionFingerprint
+ {
+ public ConditionalExpressionFingerprint(ExpressionType nodeType, Type type)
+ : base(nodeType, type)
+ {
+ // There are no properties on ConditionalExpression that are worth including in
+ // the fingerprint.
+ }
+
+ public override bool Equals(object obj)
+ {
+ ConditionalExpressionFingerprint other = obj as ConditionalExpressionFingerprint;
+ return (other != null)
+ && this.Equals(other);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ExpressionUtil/ConstantExpressionFingerprint.cs b/src/System.Web.Mvc/ExpressionUtil/ConstantExpressionFingerprint.cs
new file mode 100644
index 00000000..484e7976
--- /dev/null
+++ b/src/System.Web.Mvc/ExpressionUtil/ConstantExpressionFingerprint.cs
@@ -0,0 +1,32 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Linq.Expressions;
+
+#pragma warning disable 659 // overrides AddToHashCodeCombiner instead
+
+namespace System.Web.Mvc.ExpressionUtil
+{
+ // ConstantExpression fingerprint class
+ //
+ // A ConstantExpression might represent a captured local variable, so we can't compile
+ // the value directly into the cached function. Instead, a placeholder is generated
+ // and the value is hoisted into a local variables array. This placeholder can then
+ // be compiled and cached, and the array lookup happens at runtime.
+
+ [SuppressMessage("Microsoft.Usage", "CA2218:OverrideGetHashCodeOnOverridingEquals", Justification = "Overrides AddToHashCodeCombiner() instead.")]
+ internal sealed class ConstantExpressionFingerprint : ExpressionFingerprint
+ {
+ public ConstantExpressionFingerprint(ExpressionType nodeType, Type type)
+ : base(nodeType, type)
+ {
+ // There are no properties on ConstantExpression that are worth including in
+ // the fingerprint.
+ }
+
+ public override bool Equals(object obj)
+ {
+ ConstantExpressionFingerprint other = obj as ConstantExpressionFingerprint;
+ return (other != null)
+ && this.Equals(other);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ExpressionUtil/DefaultExpressionFingerprint.cs b/src/System.Web.Mvc/ExpressionUtil/DefaultExpressionFingerprint.cs
new file mode 100644
index 00000000..e8503774
--- /dev/null
+++ b/src/System.Web.Mvc/ExpressionUtil/DefaultExpressionFingerprint.cs
@@ -0,0 +1,28 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Linq.Expressions;
+
+#pragma warning disable 659 // overrides AddToHashCodeCombiner instead
+
+namespace System.Web.Mvc.ExpressionUtil
+{
+ // DefaultExpression fingerprint class
+ // Expression of form default(T)
+
+ [SuppressMessage("Microsoft.Usage", "CA2218:OverrideGetHashCodeOnOverridingEquals", Justification = "Overrides AddToHashCodeCombiner() instead.")]
+ internal sealed class DefaultExpressionFingerprint : ExpressionFingerprint
+ {
+ public DefaultExpressionFingerprint(ExpressionType nodeType, Type type)
+ : base(nodeType, type)
+ {
+ // There are no properties on DefaultExpression that are worth including in
+ // the fingerprint.
+ }
+
+ public override bool Equals(object obj)
+ {
+ DefaultExpressionFingerprint other = obj as DefaultExpressionFingerprint;
+ return (other != null)
+ && this.Equals(other);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ExpressionUtil/ExpressionFingerprint.cs b/src/System.Web.Mvc/ExpressionUtil/ExpressionFingerprint.cs
new file mode 100644
index 00000000..1d9cf5e2
--- /dev/null
+++ b/src/System.Web.Mvc/ExpressionUtil/ExpressionFingerprint.cs
@@ -0,0 +1,47 @@
+using System.Linq.Expressions;
+
+namespace System.Web.Mvc.ExpressionUtil
+{
+ // Serves as the base class for all expression fingerprints. Provides a default implementation
+ // of GetHashCode().
+
+ internal abstract class ExpressionFingerprint
+ {
+ protected ExpressionFingerprint(ExpressionType nodeType, Type type)
+ {
+ NodeType = nodeType;
+ Type = type;
+ }
+
+ // the type of expression node, e.g. OP_ADD, MEMBER_ACCESS, etc.
+ public ExpressionType NodeType { get; private set; }
+
+ // the CLR type resulting from this expression, e.g. int, string, etc.
+ public Type Type { get; private set; }
+
+ internal virtual void AddToHashCodeCombiner(HashCodeCombiner combiner)
+ {
+ combiner.AddInt32((int)NodeType);
+ combiner.AddObject(Type);
+ }
+
+ protected bool Equals(ExpressionFingerprint other)
+ {
+ return (other != null)
+ && (this.NodeType == other.NodeType)
+ && Equals(this.Type, other.Type);
+ }
+
+ public override bool Equals(object obj)
+ {
+ return Equals(obj as ExpressionFingerprint);
+ }
+
+ public override int GetHashCode()
+ {
+ HashCodeCombiner combiner = new HashCodeCombiner();
+ AddToHashCodeCombiner(combiner);
+ return combiner.CombinedHash;
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ExpressionUtil/ExpressionFingerprintChain.cs b/src/System.Web.Mvc/ExpressionUtil/ExpressionFingerprintChain.cs
new file mode 100644
index 00000000..3399dcee
--- /dev/null
+++ b/src/System.Web.Mvc/ExpressionUtil/ExpressionFingerprintChain.cs
@@ -0,0 +1,85 @@
+using System.Collections.Generic;
+
+namespace System.Web.Mvc.ExpressionUtil
+{
+ // Expression fingerprint chain class
+ // Contains information used for generalizing, comparing, and recreating Expression instances
+ //
+ // Since Expression objects are immutable and are recreated for every invocation of an expression
+ // helper method, they can't be compared directly. Fingerprinting Expression objects allows
+ // information about them to be abstracted away, and the fingerprints can be directly compared.
+ // Consider the process of fingerprinting that all values (parameters, constants, etc.) are hoisted
+ // and replaced with dummies. What remains can be decomposed into a sequence of operations on specific
+ // types and specific inputs.
+ //
+ // Some sample fingerprints chains:
+ //
+ // 2 + 4 -> OP_ADD, CONST:int, NULL, CONST:int
+ // 2 + 8 -> OP_ADD, CONST:int, NULL, CONST:int
+ // 2.0 + 4.0 -> OP_ADD, CONST:double, NULL, CONST:double
+ //
+ // 2 + 4 and 2 + 8 have the same fingerprint, but 2.0 + 4.0 has a different fingerprint since its
+ // underlying types differ. Note that this looks a bit like prefix notation and is a side effect
+ // of how the ExpressionVisitor class recurses into expressions. (Occasionally there will be a NULL
+ // in the fingerprint chain, which depending on context can denote a static member, a null Conversion
+ // in a BinaryExpression, and so forth.)
+ //
+ // "Hello " + "world" -> OP_ADD, CONST:string, NULL, CONST:string
+ // "Hello " + {model} -> OP_ADD, CONST:string, NULL, PARAM_0:string
+ //
+ // These string concatenations have different fingerprints since the inputs are provided differently:
+ // one is a constant, the other is a parameter.
+ //
+ // ({model} ?? "sample").Length -> MEMBER_ACCESS(String.Length), OP_COALESCE, PARAM_0:string, NULL, CONST:string
+ // ({model} ?? "other sample").Length -> MEMBER_ACCESS(String.Length), OP_COALESCE, PARAM_0:string, NULL, CONST:string
+ //
+ // These expressions have the same fingerprint since all constants of the same underlying type are
+ // treated equally.
+ //
+ // It's also important that the fingerprints don't reference the actual Expression objects that were
+ // used to generate them, as the fingerprints will be cached, and caching a fingerprint that references
+ // an Expression will root the Expression (and any objects it references).
+
+ internal sealed class ExpressionFingerprintChain : IEquatable<ExpressionFingerprintChain>
+ {
+ public readonly List<ExpressionFingerprint> Elements = new List<ExpressionFingerprint>();
+
+ public bool Equals(ExpressionFingerprintChain other)
+ {
+ // Two chains are considered equal if two elements appearing in the same index in
+ // each chain are equal (value equality, not referential equality).
+
+ if (other == null)
+ {
+ return false;
+ }
+
+ if (this.Elements.Count != other.Elements.Count)
+ {
+ return false;
+ }
+
+ for (int i = 0; i < this.Elements.Count; i++)
+ {
+ if (!Equals(this.Elements[i], other.Elements[i]))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public override bool Equals(object obj)
+ {
+ return Equals(obj as ExpressionFingerprintChain);
+ }
+
+ public override int GetHashCode()
+ {
+ HashCodeCombiner combiner = new HashCodeCombiner();
+ Elements.ForEach(combiner.AddFingerprint);
+ return combiner.CombinedHash;
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ExpressionUtil/FingerprintingExpressionVisitor.cs b/src/System.Web.Mvc/ExpressionUtil/FingerprintingExpressionVisitor.cs
new file mode 100644
index 00000000..ceaa6987
--- /dev/null
+++ b/src/System.Web.Mvc/ExpressionUtil/FingerprintingExpressionVisitor.cs
@@ -0,0 +1,296 @@
+using System.Collections.Generic;
+using System.Linq.Expressions;
+
+namespace System.Web.Mvc.ExpressionUtil
+{
+ // This is a visitor which produces a fingerprint of an expression. It doesn't
+ // rewrite the expression in a form which can be compiled and cached.
+
+ internal sealed class FingerprintingExpressionVisitor : ExpressionVisitor
+ {
+ private readonly List<object> _seenConstants = new List<object>();
+ private readonly List<ParameterExpression> _seenParameters = new List<ParameterExpression>();
+ private readonly ExpressionFingerprintChain _currentChain = new ExpressionFingerprintChain();
+ private bool _gaveUp;
+
+ private FingerprintingExpressionVisitor()
+ {
+ }
+
+ private T GiveUp<T>(T node)
+ {
+ // We don't understand this node, so just quit.
+
+ _gaveUp = true;
+ return node;
+ }
+
+ // Returns the fingerprint chain + captured constants list for this expression, or null
+ // if the expression couldn't be fingerprinted.
+ public static ExpressionFingerprintChain GetFingerprintChain(Expression expr, out List<object> capturedConstants)
+ {
+ FingerprintingExpressionVisitor visitor = new FingerprintingExpressionVisitor();
+ visitor.Visit(expr);
+
+ if (visitor._gaveUp)
+ {
+ capturedConstants = null;
+ return null;
+ }
+ else
+ {
+ capturedConstants = visitor._seenConstants;
+ return visitor._currentChain;
+ }
+ }
+
+ public override Expression Visit(Expression node)
+ {
+ if (node == null)
+ {
+ _currentChain.Elements.Add(null);
+ return null;
+ }
+ else
+ {
+ return base.Visit(node);
+ }
+ }
+
+ protected override Expression VisitBinary(BinaryExpression node)
+ {
+ if (_gaveUp)
+ {
+ return node;
+ }
+ _currentChain.Elements.Add(new BinaryExpressionFingerprint(node.NodeType, node.Type, node.Method));
+ return base.VisitBinary(node);
+ }
+
+ protected override Expression VisitBlock(BlockExpression node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override CatchBlock VisitCatchBlock(CatchBlock node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override Expression VisitConditional(ConditionalExpression node)
+ {
+ if (_gaveUp)
+ {
+ return node;
+ }
+ _currentChain.Elements.Add(new ConditionalExpressionFingerprint(node.NodeType, node.Type));
+ return base.VisitConditional(node);
+ }
+
+ protected override Expression VisitConstant(ConstantExpression node)
+ {
+ if (_gaveUp)
+ {
+ return node;
+ }
+
+ _seenConstants.Add(node.Value);
+ _currentChain.Elements.Add(new ConstantExpressionFingerprint(node.NodeType, node.Type));
+ return base.VisitConstant(node);
+ }
+
+ protected override Expression VisitDebugInfo(DebugInfoExpression node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override Expression VisitDefault(DefaultExpression node)
+ {
+ if (_gaveUp)
+ {
+ return node;
+ }
+ _currentChain.Elements.Add(new DefaultExpressionFingerprint(node.NodeType, node.Type));
+ return base.VisitDefault(node);
+ }
+
+ protected override Expression VisitDynamic(DynamicExpression node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override ElementInit VisitElementInit(ElementInit node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override Expression VisitExtension(Expression node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override Expression VisitGoto(GotoExpression node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override Expression VisitIndex(IndexExpression node)
+ {
+ if (_gaveUp)
+ {
+ return node;
+ }
+ _currentChain.Elements.Add(new IndexExpressionFingerprint(node.NodeType, node.Type, node.Indexer));
+ return base.VisitIndex(node);
+ }
+
+ protected override Expression VisitInvocation(InvocationExpression node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override Expression VisitLabel(LabelExpression node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override LabelTarget VisitLabelTarget(LabelTarget node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override Expression VisitLambda<T>(Expression<T> node)
+ {
+ if (_gaveUp)
+ {
+ return node;
+ }
+ _currentChain.Elements.Add(new LambdaExpressionFingerprint(node.NodeType, node.Type));
+ return base.VisitLambda<T>(node);
+ }
+
+ protected override Expression VisitListInit(ListInitExpression node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override Expression VisitLoop(LoopExpression node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override Expression VisitMember(MemberExpression node)
+ {
+ if (_gaveUp)
+ {
+ return node;
+ }
+ _currentChain.Elements.Add(new MemberExpressionFingerprint(node.NodeType, node.Type, node.Member));
+ return base.VisitMember(node);
+ }
+
+ protected override MemberAssignment VisitMemberAssignment(MemberAssignment node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override MemberBinding VisitMemberBinding(MemberBinding node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override Expression VisitMemberInit(MemberInitExpression node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override MemberListBinding VisitMemberListBinding(MemberListBinding node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override MemberMemberBinding VisitMemberMemberBinding(MemberMemberBinding node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override Expression VisitMethodCall(MethodCallExpression node)
+ {
+ if (_gaveUp)
+ {
+ return node;
+ }
+ _currentChain.Elements.Add(new MethodCallExpressionFingerprint(node.NodeType, node.Type, node.Method));
+ return base.VisitMethodCall(node);
+ }
+
+ protected override Expression VisitNew(NewExpression node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override Expression VisitNewArray(NewArrayExpression node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override Expression VisitParameter(ParameterExpression node)
+ {
+ if (_gaveUp)
+ {
+ return node;
+ }
+
+ int parameterIndex = _seenParameters.IndexOf(node);
+ if (parameterIndex < 0)
+ {
+ // first time seeing this parameter
+ parameterIndex = _seenParameters.Count;
+ _seenParameters.Add(node);
+ }
+
+ _currentChain.Elements.Add(new ParameterExpressionFingerprint(node.NodeType, node.Type, parameterIndex));
+ return base.VisitParameter(node);
+ }
+
+ protected override Expression VisitRuntimeVariables(RuntimeVariablesExpression node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override Expression VisitSwitch(SwitchExpression node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override SwitchCase VisitSwitchCase(SwitchCase node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override Expression VisitTry(TryExpression node)
+ {
+ return GiveUp(node);
+ }
+
+ protected override Expression VisitTypeBinary(TypeBinaryExpression node)
+ {
+ if (_gaveUp)
+ {
+ return node;
+ }
+ _currentChain.Elements.Add(new TypeBinaryExpressionFingerprint(node.NodeType, node.Type, node.TypeOperand));
+ return base.VisitTypeBinary(node);
+ }
+
+ protected override Expression VisitUnary(UnaryExpression node)
+ {
+ if (_gaveUp)
+ {
+ return node;
+ }
+ _currentChain.Elements.Add(new UnaryExpressionFingerprint(node.NodeType, node.Type, node.Method));
+ return base.VisitUnary(node);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ExpressionUtil/HashCodeCombiner.cs b/src/System.Web.Mvc/ExpressionUtil/HashCodeCombiner.cs
new file mode 100644
index 00000000..37349bfc
--- /dev/null
+++ b/src/System.Web.Mvc/ExpressionUtil/HashCodeCombiner.cs
@@ -0,0 +1,56 @@
+using System.Collections;
+
+namespace System.Web.Mvc.ExpressionUtil
+{
+ // based on System.Web.Util.HashCodeCombiner
+ internal class HashCodeCombiner
+ {
+ private long _combinedHash64 = 0x1505L;
+
+ public int CombinedHash
+ {
+ get { return _combinedHash64.GetHashCode(); }
+ }
+
+ public void AddFingerprint(ExpressionFingerprint fingerprint)
+ {
+ if (fingerprint != null)
+ {
+ fingerprint.AddToHashCodeCombiner(this);
+ }
+ else
+ {
+ AddInt32(0);
+ }
+ }
+
+ public void AddEnumerable(IEnumerable e)
+ {
+ if (e == null)
+ {
+ AddInt32(0);
+ }
+ else
+ {
+ int count = 0;
+ foreach (object o in e)
+ {
+ AddObject(o);
+ count++;
+ }
+ AddInt32(count);
+ }
+ }
+
+ public void AddInt32(int i)
+ {
+ _combinedHash64 = ((_combinedHash64 << 5) + _combinedHash64) ^ i;
+ }
+
+ public void AddObject(object o)
+ {
+ int hashCode = (o != null) ? o.GetHashCode() : 0;
+ AddInt32(hashCode);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ExpressionUtil/Hoisted`2.cs b/src/System.Web.Mvc/ExpressionUtil/Hoisted`2.cs
new file mode 100644
index 00000000..1785e627
--- /dev/null
+++ b/src/System.Web.Mvc/ExpressionUtil/Hoisted`2.cs
@@ -0,0 +1,6 @@
+using System.Collections.Generic;
+
+namespace System.Web.Mvc.ExpressionUtil
+{
+ internal delegate TValue Hoisted<TModel, TValue>(TModel model, List<object> capturedConstants);
+}
diff --git a/src/System.Web.Mvc/ExpressionUtil/HoistingExpressionVisitor.cs b/src/System.Web.Mvc/ExpressionUtil/HoistingExpressionVisitor.cs
new file mode 100644
index 00000000..40772cbc
--- /dev/null
+++ b/src/System.Web.Mvc/ExpressionUtil/HoistingExpressionVisitor.cs
@@ -0,0 +1,35 @@
+using System.Collections.Generic;
+using System.Linq.Expressions;
+
+namespace System.Web.Mvc.ExpressionUtil
+{
+ // This is a visitor which rewrites constant expressions as parameter lookups. It's meant
+ // to produce an expression which can be cached safely.
+
+ internal sealed class HoistingExpressionVisitor<TIn, TOut> : ExpressionVisitor
+ {
+ private static readonly ParameterExpression _hoistedConstantsParamExpr = Expression.Parameter(typeof(List<object>), "hoistedConstants");
+ private int _numConstantsProcessed;
+
+ // factory will create instance
+ private HoistingExpressionVisitor()
+ {
+ }
+
+ public static Expression<Hoisted<TIn, TOut>> Hoist(Expression<Func<TIn, TOut>> expr)
+ {
+ // rewrite Expression<Func<TIn, TOut>> as Expression<Hoisted<TIn, TOut>>
+
+ var visitor = new HoistingExpressionVisitor<TIn, TOut>();
+ var rewrittenBodyExpr = visitor.Visit(expr.Body);
+ var rewrittenLambdaExpr = Expression.Lambda<Hoisted<TIn, TOut>>(rewrittenBodyExpr, expr.Parameters[0], _hoistedConstantsParamExpr);
+ return rewrittenLambdaExpr;
+ }
+
+ protected override Expression VisitConstant(ConstantExpression node)
+ {
+ // rewrite the constant expression as (TConst)hoistedConstants[i];
+ return Expression.Convert(Expression.Property(_hoistedConstantsParamExpr, "Item", Expression.Constant(_numConstantsProcessed++)), node.Type);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ExpressionUtil/IndexExpressionFingerprint.cs b/src/System.Web.Mvc/ExpressionUtil/IndexExpressionFingerprint.cs
new file mode 100644
index 00000000..038e2f53
--- /dev/null
+++ b/src/System.Web.Mvc/ExpressionUtil/IndexExpressionFingerprint.cs
@@ -0,0 +1,41 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Linq.Expressions;
+using System.Reflection;
+
+#pragma warning disable 659 // overrides AddToHashCodeCombiner instead
+
+namespace System.Web.Mvc.ExpressionUtil
+{
+ // IndexExpression fingerprint class
+ // Represents certain forms of array access or indexer property access
+
+ [SuppressMessage("Microsoft.Usage", "CA2218:OverrideGetHashCodeOnOverridingEquals", Justification = "Overrides AddToHashCodeCombiner() instead.")]
+ internal sealed class IndexExpressionFingerprint : ExpressionFingerprint
+ {
+ public IndexExpressionFingerprint(ExpressionType nodeType, Type type, PropertyInfo indexer)
+ : base(nodeType, type)
+ {
+ // Other properties on IndexExpression (like the argument count) are simply derived
+ // from Type and Indexer, so they're not necessary for inclusion in the fingerprint.
+
+ Indexer = indexer;
+ }
+
+ // http://msdn.microsoft.com/en-us/library/system.linq.expressions.indexexpression.indexer.aspx
+ public PropertyInfo Indexer { get; private set; }
+
+ public override bool Equals(object obj)
+ {
+ IndexExpressionFingerprint other = obj as IndexExpressionFingerprint;
+ return (other != null)
+ && Equals(this.Indexer, other.Indexer)
+ && this.Equals(other);
+ }
+
+ internal override void AddToHashCodeCombiner(HashCodeCombiner combiner)
+ {
+ combiner.AddObject(Indexer);
+ base.AddToHashCodeCombiner(combiner);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ExpressionUtil/LambdaExpressionFingerprint.cs b/src/System.Web.Mvc/ExpressionUtil/LambdaExpressionFingerprint.cs
new file mode 100644
index 00000000..1bcb58bf
--- /dev/null
+++ b/src/System.Web.Mvc/ExpressionUtil/LambdaExpressionFingerprint.cs
@@ -0,0 +1,28 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Linq.Expressions;
+
+#pragma warning disable 659 // overrides AddToHashCodeCombiner instead
+
+namespace System.Web.Mvc.ExpressionUtil
+{
+ // LambdaExpression fingerprint class
+ // Represents a lambda expression (root element in Expression<T>)
+
+ [SuppressMessage("Microsoft.Usage", "CA2218:OverrideGetHashCodeOnOverridingEquals", Justification = "Overrides AddToHashCodeCombiner() instead.")]
+ internal sealed class LambdaExpressionFingerprint : ExpressionFingerprint
+ {
+ public LambdaExpressionFingerprint(ExpressionType nodeType, Type type)
+ : base(nodeType, type)
+ {
+ // There are no properties on LambdaExpression that are worth including in
+ // the fingerprint.
+ }
+
+ public override bool Equals(object obj)
+ {
+ LambdaExpressionFingerprint other = obj as LambdaExpressionFingerprint;
+ return (other != null)
+ && this.Equals(other);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ExpressionUtil/MemberExpressionFingerprint.cs b/src/System.Web.Mvc/ExpressionUtil/MemberExpressionFingerprint.cs
new file mode 100644
index 00000000..309dedca
--- /dev/null
+++ b/src/System.Web.Mvc/ExpressionUtil/MemberExpressionFingerprint.cs
@@ -0,0 +1,38 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Linq.Expressions;
+using System.Reflection;
+
+#pragma warning disable 659 // overrides AddToHashCodeCombiner instead
+
+namespace System.Web.Mvc.ExpressionUtil
+{
+ // MemberExpression fingerprint class
+ // Expression of form xxx.FieldOrProperty
+
+ [SuppressMessage("Microsoft.Usage", "CA2218:OverrideGetHashCodeOnOverridingEquals", Justification = "Overrides AddToHashCodeCombiner() instead.")]
+ internal sealed class MemberExpressionFingerprint : ExpressionFingerprint
+ {
+ public MemberExpressionFingerprint(ExpressionType nodeType, Type type, MemberInfo member)
+ : base(nodeType, type)
+ {
+ Member = member;
+ }
+
+ // http://msdn.microsoft.com/en-us/library/system.linq.expressions.memberexpression.member.aspx
+ public MemberInfo Member { get; private set; }
+
+ public override bool Equals(object obj)
+ {
+ MemberExpressionFingerprint other = obj as MemberExpressionFingerprint;
+ return (other != null)
+ && Equals(this.Member, other.Member)
+ && this.Equals(other);
+ }
+
+ internal override void AddToHashCodeCombiner(HashCodeCombiner combiner)
+ {
+ combiner.AddObject(Member);
+ base.AddToHashCodeCombiner(combiner);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ExpressionUtil/MethodCallExpressionFingerprint.cs b/src/System.Web.Mvc/ExpressionUtil/MethodCallExpressionFingerprint.cs
new file mode 100644
index 00000000..082541a8
--- /dev/null
+++ b/src/System.Web.Mvc/ExpressionUtil/MethodCallExpressionFingerprint.cs
@@ -0,0 +1,41 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Linq.Expressions;
+using System.Reflection;
+
+#pragma warning disable 659 // overrides AddToHashCodeCombiner instead
+
+namespace System.Web.Mvc.ExpressionUtil
+{
+ // MethodCallExpression fingerprint class
+ // Expression of form xxx.Foo(...), xxx[...] (get_Item()), etc.
+
+ [SuppressMessage("Microsoft.Usage", "CA2218:OverrideGetHashCodeOnOverridingEquals", Justification = "Overrides AddToHashCodeCombiner() instead.")]
+ internal sealed class MethodCallExpressionFingerprint : ExpressionFingerprint
+ {
+ public MethodCallExpressionFingerprint(ExpressionType nodeType, Type type, MethodInfo method)
+ : base(nodeType, type)
+ {
+ // Other properties on MethodCallExpression (like the argument count) are simply derived
+ // from Type and Indexer, so they're not necessary for inclusion in the fingerprint.
+
+ Method = method;
+ }
+
+ // http://msdn.microsoft.com/en-us/library/system.linq.expressions.methodcallexpression.method.aspx
+ public MethodInfo Method { get; private set; }
+
+ public override bool Equals(object obj)
+ {
+ MethodCallExpressionFingerprint other = obj as MethodCallExpressionFingerprint;
+ return (other != null)
+ && Equals(this.Method, other.Method)
+ && this.Equals(other);
+ }
+
+ internal override void AddToHashCodeCombiner(HashCodeCombiner combiner)
+ {
+ combiner.AddObject(Method);
+ base.AddToHashCodeCombiner(combiner);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ExpressionUtil/ParameterExpressionFingerprint.cs b/src/System.Web.Mvc/ExpressionUtil/ParameterExpressionFingerprint.cs
new file mode 100644
index 00000000..5799d064
--- /dev/null
+++ b/src/System.Web.Mvc/ExpressionUtil/ParameterExpressionFingerprint.cs
@@ -0,0 +1,37 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Linq.Expressions;
+
+#pragma warning disable 659 // overrides AddToHashCodeCombiner instead
+
+namespace System.Web.Mvc.ExpressionUtil
+{
+ // ParameterExpression fingerprint class
+ // Can represent the model parameter or an inner parameter in an open lambda expression
+
+ [SuppressMessage("Microsoft.Usage", "CA2218:OverrideGetHashCodeOnOverridingEquals", Justification = "Overrides AddToHashCodeCombiner() instead.")]
+ internal sealed class ParameterExpressionFingerprint : ExpressionFingerprint
+ {
+ public ParameterExpressionFingerprint(ExpressionType nodeType, Type type, int parameterIndex)
+ : base(nodeType, type)
+ {
+ ParameterIndex = parameterIndex;
+ }
+
+ // Parameter position within the overall expression, used to maintain alpha equivalence.
+ public int ParameterIndex { get; private set; }
+
+ public override bool Equals(object obj)
+ {
+ ParameterExpressionFingerprint other = obj as ParameterExpressionFingerprint;
+ return (other != null)
+ && (this.ParameterIndex == other.ParameterIndex)
+ && this.Equals(other);
+ }
+
+ internal override void AddToHashCodeCombiner(HashCodeCombiner combiner)
+ {
+ combiner.AddInt32(ParameterIndex);
+ base.AddToHashCodeCombiner(combiner);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ExpressionUtil/TypeBinaryExpressionFingerprint.cs b/src/System.Web.Mvc/ExpressionUtil/TypeBinaryExpressionFingerprint.cs
new file mode 100644
index 00000000..6e60efd4
--- /dev/null
+++ b/src/System.Web.Mvc/ExpressionUtil/TypeBinaryExpressionFingerprint.cs
@@ -0,0 +1,37 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Linq.Expressions;
+
+#pragma warning disable 659 // overrides AddToHashCodeCombiner instead
+
+namespace System.Web.Mvc.ExpressionUtil
+{
+ // TypeBinary fingerprint class
+ // Expression of form "obj is T"
+
+ [SuppressMessage("Microsoft.Usage", "CA2218:OverrideGetHashCodeOnOverridingEquals", Justification = "Overrides AddToHashCodeCombiner() instead.")]
+ internal sealed class TypeBinaryExpressionFingerprint : ExpressionFingerprint
+ {
+ public TypeBinaryExpressionFingerprint(ExpressionType nodeType, Type type, Type typeOperand)
+ : base(nodeType, type)
+ {
+ TypeOperand = typeOperand;
+ }
+
+ // http://msdn.microsoft.com/en-us/library/system.linq.expressions.typebinaryexpression.typeoperand.aspx
+ public Type TypeOperand { get; private set; }
+
+ public override bool Equals(object obj)
+ {
+ TypeBinaryExpressionFingerprint other = obj as TypeBinaryExpressionFingerprint;
+ return (other != null)
+ && Equals(this.TypeOperand, other.TypeOperand)
+ && this.Equals(other);
+ }
+
+ internal override void AddToHashCodeCombiner(HashCodeCombiner combiner)
+ {
+ combiner.AddObject(TypeOperand);
+ base.AddToHashCodeCombiner(combiner);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ExpressionUtil/UnaryExpressionFingerprint.cs b/src/System.Web.Mvc/ExpressionUtil/UnaryExpressionFingerprint.cs
new file mode 100644
index 00000000..2df90a8a
--- /dev/null
+++ b/src/System.Web.Mvc/ExpressionUtil/UnaryExpressionFingerprint.cs
@@ -0,0 +1,41 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Linq.Expressions;
+using System.Reflection;
+
+#pragma warning disable 659 // overrides AddToHashCodeCombiner instead
+
+namespace System.Web.Mvc.ExpressionUtil
+{
+ // UnaryExpression fingerprint class
+ // The most common appearance of a UnaryExpression is a cast or other conversion operator
+
+ [SuppressMessage("Microsoft.Usage", "CA2218:OverrideGetHashCodeOnOverridingEquals", Justification = "Overrides AddToHashCodeCombiner() instead.")]
+ internal sealed class UnaryExpressionFingerprint : ExpressionFingerprint
+ {
+ public UnaryExpressionFingerprint(ExpressionType nodeType, Type type, MethodInfo method)
+ : base(nodeType, type)
+ {
+ // Other properties on UnaryExpression (like IsLifted / IsLiftedToNull) are simply derived
+ // from Type and NodeType, so they're not necessary for inclusion in the fingerprint.
+
+ Method = method;
+ }
+
+ // http://msdn.microsoft.com/en-us/library/system.linq.expressions.unaryexpression.method.aspx
+ public MethodInfo Method { get; private set; }
+
+ public override bool Equals(object obj)
+ {
+ UnaryExpressionFingerprint other = obj as UnaryExpressionFingerprint;
+ return (other != null)
+ && Equals(this.Method, other.Method)
+ && this.Equals(other);
+ }
+
+ internal override void AddToHashCodeCombiner(HashCodeCombiner combiner)
+ {
+ combiner.AddObject(Method);
+ base.AddToHashCodeCombiner(combiner);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/FieldValidationMetadata.cs b/src/System.Web.Mvc/FieldValidationMetadata.cs
new file mode 100644
index 00000000..e025e6ac
--- /dev/null
+++ b/src/System.Web.Mvc/FieldValidationMetadata.cs
@@ -0,0 +1,26 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+
+namespace System.Web.Mvc
+{
+ public class FieldValidationMetadata
+ {
+ private readonly Collection<ModelClientValidationRule> _validationRules = new Collection<ModelClientValidationRule>();
+ private string _fieldName;
+
+ public string FieldName
+ {
+ get { return _fieldName ?? String.Empty; }
+ set { _fieldName = value; }
+ }
+
+ public bool ReplaceValidationMessageContents { get; set; }
+
+ public string ValidationMessageId { get; set; }
+
+ public ICollection<ModelClientValidationRule> ValidationRules
+ {
+ get { return _validationRules; }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/FileContentResult.cs b/src/System.Web.Mvc/FileContentResult.cs
new file mode 100644
index 00000000..ab28d0ba
--- /dev/null
+++ b/src/System.Web.Mvc/FileContentResult.cs
@@ -0,0 +1,26 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Web.Mvc
+{
+ public class FileContentResult : FileResult
+ {
+ public FileContentResult(byte[] fileContents, string contentType)
+ : base(contentType)
+ {
+ if (fileContents == null)
+ {
+ throw new ArgumentNullException("fileContents");
+ }
+
+ FileContents = fileContents;
+ }
+
+ [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "There's no reason to tamper-proof this array since it's supplied to the type's constructor.")]
+ public byte[] FileContents { get; private set; }
+
+ protected override void WriteFile(HttpResponseBase response)
+ {
+ response.OutputStream.Write(FileContents, 0, FileContents.Length);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/FilePathResult.cs b/src/System.Web.Mvc/FilePathResult.cs
new file mode 100644
index 00000000..fd2507a6
--- /dev/null
+++ b/src/System.Web.Mvc/FilePathResult.cs
@@ -0,0 +1,25 @@
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc
+{
+ public class FilePathResult : FileResult
+ {
+ public FilePathResult(string fileName, string contentType)
+ : base(contentType)
+ {
+ if (String.IsNullOrEmpty(fileName))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "fileName");
+ }
+
+ FileName = fileName;
+ }
+
+ public string FileName { get; private set; }
+
+ protected override void WriteFile(HttpResponseBase response)
+ {
+ response.TransmitFile(FileName);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/FileResult.cs b/src/System.Web.Mvc/FileResult.cs
new file mode 100644
index 00000000..dcaab762
--- /dev/null
+++ b/src/System.Web.Mvc/FileResult.cs
@@ -0,0 +1,143 @@
+using System.Net.Mime;
+using System.Text;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc
+{
+ public abstract class FileResult : ActionResult
+ {
+ private string _fileDownloadName;
+
+ protected FileResult(string contentType)
+ {
+ if (String.IsNullOrEmpty(contentType))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "contentType");
+ }
+
+ ContentType = contentType;
+ }
+
+ public string ContentType { get; private set; }
+
+ public string FileDownloadName
+ {
+ get { return _fileDownloadName ?? String.Empty; }
+ set { _fileDownloadName = value; }
+ }
+
+ public override void ExecuteResult(ControllerContext context)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException("context");
+ }
+
+ HttpResponseBase response = context.HttpContext.Response;
+ response.ContentType = ContentType;
+
+ if (!String.IsNullOrEmpty(FileDownloadName))
+ {
+ // From RFC 2183, Sec. 2.3:
+ // The sender may want to suggest a filename to be used if the entity is
+ // detached and stored in a separate file. If the receiving MUA writes
+ // the entity to a file, the suggested filename should be used as a
+ // basis for the actual filename, where possible.
+ string headerValue = ContentDispositionUtil.GetHeaderValue(FileDownloadName);
+ context.HttpContext.Response.AddHeader("Content-Disposition", headerValue);
+ }
+
+ WriteFile(response);
+ }
+
+ protected abstract void WriteFile(HttpResponseBase response);
+
+ private static class ContentDispositionUtil
+ {
+ private const string HexDigits = "0123456789ABCDEF";
+
+ private static void AddByteToStringBuilder(byte b, StringBuilder builder)
+ {
+ builder.Append('%');
+
+ int i = b;
+ AddHexDigitToStringBuilder(i >> 4, builder);
+ AddHexDigitToStringBuilder(i % 16, builder);
+ }
+
+ private static void AddHexDigitToStringBuilder(int digit, StringBuilder builder)
+ {
+ builder.Append(HexDigits[digit]);
+ }
+
+ private static string CreateRfc2231HeaderValue(string filename)
+ {
+ StringBuilder builder = new StringBuilder("attachment; filename*=UTF-8''");
+
+ byte[] filenameBytes = Encoding.UTF8.GetBytes(filename);
+ foreach (byte b in filenameBytes)
+ {
+ if (IsByteValidHeaderValueCharacter(b))
+ {
+ builder.Append((char)b);
+ }
+ else
+ {
+ AddByteToStringBuilder(b, builder);
+ }
+ }
+
+ return builder.ToString();
+ }
+
+ public static string GetHeaderValue(string fileName)
+ {
+ try
+ {
+ // first, try using the .NET built-in generator
+ ContentDisposition disposition = new ContentDisposition() { FileName = fileName };
+ return disposition.ToString();
+ }
+ catch (FormatException)
+ {
+ // otherwise, fall back to RFC 2231 extensions generator
+ return CreateRfc2231HeaderValue(fileName);
+ }
+ }
+
+ // Application of RFC 2231 Encoding to Hypertext Transfer Protocol (HTTP) Header Fields, sec. 3.2
+ // http://greenbytes.de/tech/webdav/draft-reschke-rfc2231-in-http-latest.html
+ private static bool IsByteValidHeaderValueCharacter(byte b)
+ {
+ if ((byte)'0' <= b && b <= (byte)'9')
+ {
+ return true; // is digit
+ }
+ if ((byte)'a' <= b && b <= (byte)'z')
+ {
+ return true; // lowercase letter
+ }
+ if ((byte)'A' <= b && b <= (byte)'Z')
+ {
+ return true; // uppercase letter
+ }
+
+ switch (b)
+ {
+ case (byte)'-':
+ case (byte)'.':
+ case (byte)'_':
+ case (byte)'~':
+ case (byte)':':
+ case (byte)'!':
+ case (byte)'$':
+ case (byte)'&':
+ case (byte)'+':
+ return true;
+ }
+
+ return false;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/FileStreamResult.cs b/src/System.Web.Mvc/FileStreamResult.cs
new file mode 100644
index 00000000..2fc5a132
--- /dev/null
+++ b/src/System.Web.Mvc/FileStreamResult.cs
@@ -0,0 +1,45 @@
+using System.IO;
+
+namespace System.Web.Mvc
+{
+ public class FileStreamResult : FileResult
+ {
+ // default buffer size as defined in BufferedStream type
+ private const int BufferSize = 0x1000;
+
+ public FileStreamResult(Stream fileStream, string contentType)
+ : base(contentType)
+ {
+ if (fileStream == null)
+ {
+ throw new ArgumentNullException("fileStream");
+ }
+
+ FileStream = fileStream;
+ }
+
+ public Stream FileStream { get; private set; }
+
+ protected override void WriteFile(HttpResponseBase response)
+ {
+ // grab chunks of data and write to the output stream
+ Stream outputStream = response.OutputStream;
+ using (FileStream)
+ {
+ byte[] buffer = new byte[BufferSize];
+
+ while (true)
+ {
+ int bytesRead = FileStream.Read(buffer, 0, BufferSize);
+ if (bytesRead == 0)
+ {
+ // no more data
+ break;
+ }
+
+ outputStream.Write(buffer, 0, bytesRead);
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Filter.cs b/src/System.Web.Mvc/Filter.cs
new file mode 100644
index 00000000..e027dbef
--- /dev/null
+++ b/src/System.Web.Mvc/Filter.cs
@@ -0,0 +1,34 @@
+namespace System.Web.Mvc
+{
+ public class Filter
+ {
+ public const int DefaultOrder = -1;
+
+ public Filter(object instance, FilterScope scope, int? order)
+ {
+ if (instance == null)
+ {
+ throw new ArgumentNullException("instance");
+ }
+
+ if (order == null)
+ {
+ IMvcFilter mvcFilter = instance as IMvcFilter;
+ if (mvcFilter != null)
+ {
+ order = mvcFilter.Order;
+ }
+ }
+
+ Instance = instance;
+ Order = order ?? DefaultOrder;
+ Scope = scope;
+ }
+
+ public object Instance { get; protected set; }
+
+ public int Order { get; protected set; }
+
+ public FilterScope Scope { get; protected set; }
+ }
+}
diff --git a/src/System.Web.Mvc/FilterAttribute.cs b/src/System.Web.Mvc/FilterAttribute.cs
new file mode 100644
index 00000000..88fcdaf4
--- /dev/null
+++ b/src/System.Web.Mvc/FilterAttribute.cs
@@ -0,0 +1,41 @@
+using System.Collections.Concurrent;
+using System.Linq;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc
+{
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
+ public abstract class FilterAttribute : Attribute, IMvcFilter
+ {
+ private static readonly ConcurrentDictionary<Type, bool> _multiuseAttributeCache = new ConcurrentDictionary<Type, bool>();
+ private int _order = Filter.DefaultOrder;
+
+ public bool AllowMultiple
+ {
+ get { return AllowsMultiple(GetType()); }
+ }
+
+ public int Order
+ {
+ get { return _order; }
+ set
+ {
+ if (value < Filter.DefaultOrder)
+ {
+ throw new ArgumentOutOfRangeException("value", MvcResources.FilterAttribute_OrderOutOfRange);
+ }
+ _order = value;
+ }
+ }
+
+ private static bool AllowsMultiple(Type attributeType)
+ {
+ return _multiuseAttributeCache.GetOrAdd(
+ attributeType,
+ type => type.GetCustomAttributes(typeof(AttributeUsageAttribute), true)
+ .Cast<AttributeUsageAttribute>()
+ .First()
+ .AllowMultiple);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/FilterAttributeFilterProvider.cs b/src/System.Web.Mvc/FilterAttributeFilterProvider.cs
new file mode 100644
index 00000000..74022795
--- /dev/null
+++ b/src/System.Web.Mvc/FilterAttributeFilterProvider.cs
@@ -0,0 +1,46 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace System.Web.Mvc
+{
+ public class FilterAttributeFilterProvider : IFilterProvider
+ {
+ private readonly bool _cacheAttributeInstances;
+
+ public FilterAttributeFilterProvider()
+ : this(true)
+ {
+ }
+
+ public FilterAttributeFilterProvider(bool cacheAttributeInstances)
+ {
+ _cacheAttributeInstances = cacheAttributeInstances;
+ }
+
+ protected virtual IEnumerable<FilterAttribute> GetActionAttributes(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
+ {
+ return actionDescriptor.GetFilterAttributes(_cacheAttributeInstances);
+ }
+
+ protected virtual IEnumerable<FilterAttribute> GetControllerAttributes(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
+ {
+ return actionDescriptor.ControllerDescriptor.GetFilterAttributes(_cacheAttributeInstances);
+ }
+
+ public virtual IEnumerable<Filter> GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
+ {
+ ControllerBase controller = controllerContext.Controller;
+ if (controller == null)
+ {
+ return Enumerable.Empty<Filter>();
+ }
+
+ var typeFilters = GetControllerAttributes(controllerContext, actionDescriptor)
+ .Select(attr => new Filter(attr, FilterScope.Controller, null));
+ var methodFilters = GetActionAttributes(controllerContext, actionDescriptor)
+ .Select(attr => new Filter(attr, FilterScope.Action, null));
+
+ return typeFilters.Concat(methodFilters).ToList();
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/FilterInfo.cs b/src/System.Web.Mvc/FilterInfo.cs
new file mode 100644
index 00000000..1d14ce79
--- /dev/null
+++ b/src/System.Web.Mvc/FilterInfo.cs
@@ -0,0 +1,48 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace System.Web.Mvc
+{
+ public class FilterInfo
+ {
+ private List<IActionFilter> _actionFilters = new List<IActionFilter>();
+ private List<IAuthorizationFilter> _authorizationFilters = new List<IAuthorizationFilter>();
+ private List<IExceptionFilter> _exceptionFilters = new List<IExceptionFilter>();
+ private List<IResultFilter> _resultFilters = new List<IResultFilter>();
+
+ public FilterInfo()
+ {
+ }
+
+ public FilterInfo(IEnumerable<Filter> filters)
+ {
+ // evaluate the 'filters' enumerable only once since the operation can be quite expensive
+ var filterInstances = filters.Select(f => f.Instance).ToList();
+
+ _actionFilters.AddRange(filterInstances.OfType<IActionFilter>());
+ _authorizationFilters.AddRange(filterInstances.OfType<IAuthorizationFilter>());
+ _exceptionFilters.AddRange(filterInstances.OfType<IExceptionFilter>());
+ _resultFilters.AddRange(filterInstances.OfType<IResultFilter>());
+ }
+
+ public IList<IActionFilter> ActionFilters
+ {
+ get { return _actionFilters; }
+ }
+
+ public IList<IAuthorizationFilter> AuthorizationFilters
+ {
+ get { return _authorizationFilters; }
+ }
+
+ public IList<IExceptionFilter> ExceptionFilters
+ {
+ get { return _exceptionFilters; }
+ }
+
+ public IList<IResultFilter> ResultFilters
+ {
+ get { return _resultFilters; }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/FilterProviderCollection.cs b/src/System.Web.Mvc/FilterProviderCollection.cs
new file mode 100644
index 00000000..854733d7
--- /dev/null
+++ b/src/System.Web.Mvc/FilterProviderCollection.cs
@@ -0,0 +1,125 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+
+namespace System.Web.Mvc
+{
+ public class FilterProviderCollection : Collection<IFilterProvider>
+ {
+ private static FilterComparer _filterComparer = new FilterComparer();
+ private IResolver<IEnumerable<IFilterProvider>> _serviceResolver;
+
+ public FilterProviderCollection()
+ {
+ _serviceResolver = new MultiServiceResolver<IFilterProvider>(() => Items);
+ }
+
+ public FilterProviderCollection(IList<IFilterProvider> providers)
+ : base(providers)
+ {
+ _serviceResolver = new MultiServiceResolver<IFilterProvider>(() => Items);
+ }
+
+ internal FilterProviderCollection(IResolver<IEnumerable<IFilterProvider>> serviceResolver, params IFilterProvider[] providers)
+ : base(providers)
+ {
+ _serviceResolver = serviceResolver ?? new MultiServiceResolver<IFilterProvider>(() => Items);
+ }
+
+ private IEnumerable<IFilterProvider> CombinedItems
+ {
+ get { return _serviceResolver.Current; }
+ }
+
+ private static bool AllowMultiple(object filterInstance)
+ {
+ IMvcFilter mvcFilter = filterInstance as IMvcFilter;
+ if (mvcFilter == null)
+ {
+ return true;
+ }
+
+ return mvcFilter.AllowMultiple;
+ }
+
+ public IEnumerable<Filter> GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
+ {
+ if (controllerContext == null)
+ {
+ throw new ArgumentNullException("controllerContext");
+ }
+ if (actionDescriptor == null)
+ {
+ throw new ArgumentNullException("actionDescriptor");
+ }
+
+ IEnumerable<Filter> combinedFilters =
+ CombinedItems.SelectMany(fp => fp.GetFilters(controllerContext, actionDescriptor))
+ .OrderBy(filter => filter, _filterComparer);
+
+ // Remove duplicates from the back forward
+ return RemoveDuplicates(combinedFilters.Reverse()).Reverse();
+ }
+
+ private IEnumerable<Filter> RemoveDuplicates(IEnumerable<Filter> filters)
+ {
+ HashSet<Type> visitedTypes = new HashSet<Type>();
+
+ foreach (Filter filter in filters)
+ {
+ object filterInstance = filter.Instance;
+ Type filterInstanceType = filterInstance.GetType();
+
+ if (!visitedTypes.Contains(filterInstanceType) || AllowMultiple(filterInstance))
+ {
+ yield return filter;
+ visitedTypes.Add(filterInstanceType);
+ }
+ }
+ }
+
+ private class FilterComparer : IComparer<Filter>
+ {
+ public int Compare(Filter x, Filter y)
+ {
+ // Nulls always have to be less than non-nulls
+ if (x == null && y == null)
+ {
+ return 0;
+ }
+ if (x == null)
+ {
+ return -1;
+ }
+ if (y == null)
+ {
+ return 1;
+ }
+
+ // Sort first by order...
+
+ if (x.Order < y.Order)
+ {
+ return -1;
+ }
+ if (x.Order > y.Order)
+ {
+ return 1;
+ }
+
+ // ...then by scope
+
+ if (x.Scope < y.Scope)
+ {
+ return -1;
+ }
+ if (x.Scope > y.Scope)
+ {
+ return 1;
+ }
+
+ return 0;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/FilterProviders.cs b/src/System.Web.Mvc/FilterProviders.cs
new file mode 100644
index 00000000..7ff3db9a
--- /dev/null
+++ b/src/System.Web.Mvc/FilterProviders.cs
@@ -0,0 +1,15 @@
+namespace System.Web.Mvc
+{
+ public static class FilterProviders
+ {
+ static FilterProviders()
+ {
+ Providers = new FilterProviderCollection();
+ Providers.Add(GlobalFilters.Filters);
+ Providers.Add(new FilterAttributeFilterProvider());
+ Providers.Add(new ControllerInstanceFilterProvider());
+ }
+
+ public static FilterProviderCollection Providers { get; private set; }
+ }
+}
diff --git a/src/System.Web.Mvc/FilterScope.cs b/src/System.Web.Mvc/FilterScope.cs
new file mode 100644
index 00000000..ca315e00
--- /dev/null
+++ b/src/System.Web.Mvc/FilterScope.cs
@@ -0,0 +1,11 @@
+namespace System.Web.Mvc
+{
+ public enum FilterScope
+ {
+ First = 0,
+ Global = 10,
+ Controller = 20,
+ Action = 30,
+ Last = 100,
+ }
+}
diff --git a/src/System.Web.Mvc/FormCollection.cs b/src/System.Web.Mvc/FormCollection.cs
new file mode 100644
index 00000000..4adc8b42
--- /dev/null
+++ b/src/System.Web.Mvc/FormCollection.cs
@@ -0,0 +1,96 @@
+using System.Collections.Specialized;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Web.Helpers;
+
+namespace System.Web.Mvc
+{
+ [SuppressMessage("Microsoft.Usage", "CA2237:MarkISerializableTypesWithSerializable", Justification = "It is not anticipated that users will need to serialize this type.")]
+ [SuppressMessage("Microsoft.Design", "CA1035:ICollectionImplementationsHaveStronglyTypedMembers", Justification = "It is not anticipated that users will call FormCollection.CopyTo().")]
+ [FormCollectionBinder]
+ public sealed class FormCollection : NameValueCollection, IValueProvider
+ {
+ public FormCollection()
+ {
+ }
+
+ public FormCollection(NameValueCollection collection)
+ {
+ if (collection == null)
+ {
+ throw new ArgumentNullException("collection");
+ }
+
+ Add(collection);
+ }
+
+ internal FormCollection(ControllerBase controller, Func<NameValueCollection> validatedValuesThunk, Func<NameValueCollection> unvalidatedValuesThunk)
+ {
+ Add(controller == null || controller.ValidateRequest ? validatedValuesThunk() : unvalidatedValuesThunk());
+ }
+
+ public ValueProviderResult GetValue(string name)
+ {
+ if (name == null)
+ {
+ throw new ArgumentNullException("name");
+ }
+
+ string[] rawValue = GetValues(name);
+ if (rawValue == null)
+ {
+ return null;
+ }
+
+ string attemptedValue = this[name];
+ return new ValueProviderResult(rawValue, attemptedValue, CultureInfo.CurrentCulture);
+ }
+
+ public IValueProvider ToValueProvider()
+ {
+ return this;
+ }
+
+ #region IValueProvider Members
+
+ bool IValueProvider.ContainsPrefix(string prefix)
+ {
+ return ValueProviderUtil.CollectionContainsPrefix(AllKeys, prefix);
+ }
+
+ ValueProviderResult IValueProvider.GetValue(string key)
+ {
+ return GetValue(key);
+ }
+
+ #endregion
+
+ private sealed class FormCollectionBinderAttribute : CustomModelBinderAttribute
+ {
+ // since the FormCollectionModelBinder.BindModel() method is thread-safe, we only need to keep
+ // a single instance of the binder around
+ private static readonly FormCollectionModelBinder _binder = new FormCollectionModelBinder();
+
+ public override IModelBinder GetBinder()
+ {
+ return _binder;
+ }
+
+ // this class is used for generating a FormCollection object
+ private sealed class FormCollectionModelBinder : IModelBinder
+ {
+ public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
+ {
+ if (controllerContext == null)
+ {
+ throw new ArgumentNullException("controllerContext");
+ }
+
+ return new FormCollection(controllerContext.Controller,
+ () => controllerContext.HttpContext.Request.Form,
+ () => controllerContext.HttpContext.Request.Unvalidated().Form);
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/FormContext.cs b/src/System.Web.Mvc/FormContext.cs
new file mode 100644
index 00000000..b117c2b2
--- /dev/null
+++ b/src/System.Web.Mvc/FormContext.cs
@@ -0,0 +1,81 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Web.Script.Serialization;
+
+namespace System.Web.Mvc
+{
+ public class FormContext
+ {
+ private readonly Dictionary<string, FieldValidationMetadata> _fieldValidators = new Dictionary<string, FieldValidationMetadata>();
+ private readonly Dictionary<string, bool> _renderedFields = new Dictionary<string, bool>();
+
+ public IDictionary<string, FieldValidationMetadata> FieldValidators
+ {
+ get { return _fieldValidators; }
+ }
+
+ public string FormId { get; set; }
+
+ public bool ReplaceValidationSummary { get; set; }
+
+ public string ValidationSummaryId { get; set; }
+
+ [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Performs a potentially time-consuming conversion.")]
+ public string GetJsonValidationMetadata()
+ {
+ JavaScriptSerializer serializer = new JavaScriptSerializer();
+
+ SortedDictionary<string, object> dict = new SortedDictionary<string, object>()
+ {
+ { "Fields", FieldValidators.Values },
+ { "FormId", FormId }
+ };
+ if (!String.IsNullOrEmpty(ValidationSummaryId))
+ {
+ dict["ValidationSummaryId"] = ValidationSummaryId;
+ }
+ dict["ReplaceValidationSummary"] = ReplaceValidationSummary;
+
+ return serializer.Serialize(dict);
+ }
+
+ public FieldValidationMetadata GetValidationMetadataForField(string fieldName)
+ {
+ return GetValidationMetadataForField(fieldName, false /* createIfNotFound */);
+ }
+
+ public FieldValidationMetadata GetValidationMetadataForField(string fieldName, bool createIfNotFound)
+ {
+ if (String.IsNullOrEmpty(fieldName))
+ {
+ throw Error.ParameterCannotBeNullOrEmpty("fieldName");
+ }
+
+ FieldValidationMetadata metadata;
+ if (!FieldValidators.TryGetValue(fieldName, out metadata))
+ {
+ if (createIfNotFound)
+ {
+ metadata = new FieldValidationMetadata()
+ {
+ FieldName = fieldName
+ };
+ FieldValidators[fieldName] = metadata;
+ }
+ }
+ return metadata;
+ }
+
+ public bool RenderedField(string fieldName)
+ {
+ bool result;
+ _renderedFields.TryGetValue(fieldName, out result);
+ return result;
+ }
+
+ public void RenderedField(string fieldName, bool value)
+ {
+ _renderedFields[fieldName] = value;
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/FormMethod.cs b/src/System.Web.Mvc/FormMethod.cs
new file mode 100644
index 00000000..5e49bb3b
--- /dev/null
+++ b/src/System.Web.Mvc/FormMethod.cs
@@ -0,0 +1,8 @@
+namespace System.Web.Mvc
+{
+ public enum FormMethod
+ {
+ Get,
+ Post
+ }
+}
diff --git a/src/System.Web.Mvc/FormValueProvider.cs b/src/System.Web.Mvc/FormValueProvider.cs
new file mode 100644
index 00000000..5b9bf1e3
--- /dev/null
+++ b/src/System.Web.Mvc/FormValueProvider.cs
@@ -0,0 +1,19 @@
+using System.Globalization;
+using System.Web.Helpers;
+
+namespace System.Web.Mvc
+{
+ public sealed class FormValueProvider : NameValueCollectionValueProvider
+ {
+ public FormValueProvider(ControllerContext controllerContext)
+ : this(controllerContext, new UnvalidatedRequestValuesWrapper(controllerContext.HttpContext.Request.Unvalidated()))
+ {
+ }
+
+ // For unit testing
+ internal FormValueProvider(ControllerContext controllerContext, IUnvalidatedRequestValues unvalidatedValues)
+ : base(controllerContext.HttpContext.Request.Form, unvalidatedValues.Form, CultureInfo.CurrentCulture)
+ {
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/FormValueProviderFactory.cs b/src/System.Web.Mvc/FormValueProviderFactory.cs
new file mode 100644
index 00000000..d6b28452
--- /dev/null
+++ b/src/System.Web.Mvc/FormValueProviderFactory.cs
@@ -0,0 +1,30 @@
+using System.Web.Helpers;
+
+namespace System.Web.Mvc
+{
+ public sealed class FormValueProviderFactory : ValueProviderFactory
+ {
+ private readonly UnvalidatedRequestValuesAccessor _unvalidatedValuesAccessor;
+
+ public FormValueProviderFactory()
+ : this(null)
+ {
+ }
+
+ // For unit testing
+ internal FormValueProviderFactory(UnvalidatedRequestValuesAccessor unvalidatedValuesAccessor)
+ {
+ _unvalidatedValuesAccessor = unvalidatedValuesAccessor ?? (cc => new UnvalidatedRequestValuesWrapper(cc.HttpContext.Request.Unvalidated()));
+ }
+
+ public override IValueProvider GetValueProvider(ControllerContext controllerContext)
+ {
+ if (controllerContext == null)
+ {
+ throw new ArgumentNullException("controllerContext");
+ }
+
+ return new FormValueProvider(controllerContext, _unvalidatedValuesAccessor(controllerContext));
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/GlobalFilterCollection.cs b/src/System.Web.Mvc/GlobalFilterCollection.cs
new file mode 100644
index 00000000..8dfee88f
--- /dev/null
+++ b/src/System.Web.Mvc/GlobalFilterCollection.cs
@@ -0,0 +1,61 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace System.Web.Mvc
+{
+ public sealed class GlobalFilterCollection : IEnumerable<Filter>, IFilterProvider
+ {
+ private List<Filter> _filters = new List<Filter>();
+
+ public int Count
+ {
+ get { return _filters.Count; }
+ }
+
+ public void Add(object filter)
+ {
+ AddInternal(filter, order: null);
+ }
+
+ public void Add(object filter, int order)
+ {
+ AddInternal(filter, order);
+ }
+
+ private void AddInternal(object filter, int? order)
+ {
+ _filters.Add(new Filter(filter, FilterScope.Global, order));
+ }
+
+ public void Clear()
+ {
+ _filters.Clear();
+ }
+
+ public bool Contains(object filter)
+ {
+ return _filters.Any(f => f.Instance == filter);
+ }
+
+ public IEnumerator<Filter> GetEnumerator()
+ {
+ return _filters.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return _filters.GetEnumerator();
+ }
+
+ IEnumerable<Filter> IFilterProvider.GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
+ {
+ return this;
+ }
+
+ public void Remove(object filter)
+ {
+ _filters.RemoveAll(f => f.Instance == filter);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/GlobalFilters.cs b/src/System.Web.Mvc/GlobalFilters.cs
new file mode 100644
index 00000000..926e4fcd
--- /dev/null
+++ b/src/System.Web.Mvc/GlobalFilters.cs
@@ -0,0 +1,12 @@
+namespace System.Web.Mvc
+{
+ public static class GlobalFilters
+ {
+ static GlobalFilters()
+ {
+ Filters = new GlobalFilterCollection();
+ }
+
+ public static GlobalFilterCollection Filters { get; private set; }
+ }
+}
diff --git a/src/System.Web.Mvc/GlobalSuppressions.cs b/src/System.Web.Mvc/GlobalSuppressions.cs
new file mode 100644
index 00000000..f7a121ee
--- /dev/null
+++ b/src/System.Web.Mvc/GlobalSuppressions.cs
@@ -0,0 +1,19 @@
+// This file is used by Code Analysis to maintain SuppressMessage
+// attributes that are applied to this project.
+// Project-level suppressions either have no target or are given
+// a specific target and scoped to a namespace, type, member, etc.
+//
+// To add a suppression to this file, right-click the message in the
+// Error List, point to "Suppress Message(s)", and click
+// "In Project Suppression File".
+// You do not need to add suppressions to this file manually.
+
+using System.Diagnostics.CodeAnalysis;
+
+[assembly: SuppressMessage("Microsoft.Design", "CA2210:AssembliesShouldHaveValidStrongNames", Justification = "Assembly is delay-signed.")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "System.Web.Mvc.Ajax", Justification = "Helpers reside within a separate namespace to support alternate helper classes.")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Scope = "member", Target = "System.Web.Mvc.TempDataDictionary.#System.Collections.Generic.ICollection`1<System.Collections.Generic.KeyValuePair`2<System.String,System.Object>>.Contains(System.Collections.Generic.KeyValuePair`2<System.String,System.Object>)", Justification = "There are no defined scenarios for wanting to derive from this class, but we don't want to prevent it either.")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Scope = "member", Target = "System.Web.Mvc.TempDataDictionary.#System.Collections.Generic.ICollection`1<System.Collections.Generic.KeyValuePair`2<System.String,System.Object>>.CopyTo(System.Collections.Generic.KeyValuePair`2<System.String,System.Object>[],System.Int32)", Justification = "There are no defined scenarios for wanting to derive from this class, but we don't want to prevent it either.")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Scope = "member", Target = "System.Web.Mvc.TempDataDictionary.#System.Collections.Generic.ICollection`1<System.Collections.Generic.KeyValuePair`2<System.String,System.Object>>.IsReadOnly", Justification = "There are no defined scenarios for wanting to derive from this class, but we don't want to prevent it either.")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId = "Param", Scope = "resource", Target = "System.Web.Mvc.Properties.MvcResources.resources", Justification = "This is the name that matches ASP.NET")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "System.Web.Mvc.Razor", Justification = "This is a grouping of functionally similar components, thus a namespace is a valid way to group them.")]
diff --git a/src/System.Web.Mvc/HandleErrorAttribute.cs b/src/System.Web.Mvc/HandleErrorAttribute.cs
new file mode 100644
index 00000000..c4b61341
--- /dev/null
+++ b/src/System.Web.Mvc/HandleErrorAttribute.cs
@@ -0,0 +1,107 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc
+{
+ [SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", Justification = "This attribute is AllowMultiple = true and users might want to override behavior.")]
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
+ public class HandleErrorAttribute : FilterAttribute, IExceptionFilter
+ {
+ private const string DefaultView = "Error";
+
+ private readonly object _typeId = new object();
+
+ private Type _exceptionType = typeof(Exception);
+ private string _master;
+ private string _view;
+
+ public Type ExceptionType
+ {
+ get { return _exceptionType; }
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException("value");
+ }
+ if (!typeof(Exception).IsAssignableFrom(value))
+ {
+ throw new ArgumentException(String.Format(CultureInfo.CurrentCulture,
+ MvcResources.ExceptionViewAttribute_NonExceptionType, value.FullName));
+ }
+
+ _exceptionType = value;
+ }
+ }
+
+ public string Master
+ {
+ get { return _master ?? String.Empty; }
+ set { _master = value; }
+ }
+
+ public override object TypeId
+ {
+ get { return _typeId; }
+ }
+
+ public string View
+ {
+ get { return (!String.IsNullOrEmpty(_view)) ? _view : DefaultView; }
+ set { _view = value; }
+ }
+
+ public virtual void OnException(ExceptionContext filterContext)
+ {
+ if (filterContext == null)
+ {
+ throw new ArgumentNullException("filterContext");
+ }
+ if (filterContext.IsChildAction)
+ {
+ return;
+ }
+
+ // If custom errors are disabled, we need to let the normal ASP.NET exception handler
+ // execute so that the user can see useful debugging information.
+ if (filterContext.ExceptionHandled || !filterContext.HttpContext.IsCustomErrorEnabled)
+ {
+ return;
+ }
+
+ Exception exception = filterContext.Exception;
+
+ // If this is not an HTTP 500 (for example, if somebody throws an HTTP 404 from an action method),
+ // ignore it.
+ if (new HttpException(null, exception).GetHttpCode() != 500)
+ {
+ return;
+ }
+
+ if (!ExceptionType.IsInstanceOfType(exception))
+ {
+ return;
+ }
+
+ string controllerName = (string)filterContext.RouteData.Values["controller"];
+ string actionName = (string)filterContext.RouteData.Values["action"];
+ HandleErrorInfo model = new HandleErrorInfo(filterContext.Exception, controllerName, actionName);
+ filterContext.Result = new ViewResult
+ {
+ ViewName = View,
+ MasterName = Master,
+ ViewData = new ViewDataDictionary<HandleErrorInfo>(model),
+ TempData = filterContext.Controller.TempData
+ };
+ filterContext.ExceptionHandled = true;
+ filterContext.HttpContext.Response.Clear();
+ filterContext.HttpContext.Response.StatusCode = 500;
+
+ // Certain versions of IIS will sometimes use their own error page when
+ // they detect a server error. Setting this property indicates that we
+ // want it to try to render ASP.NET MVC's error page instead.
+ filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/HandleErrorInfo.cs b/src/System.Web.Mvc/HandleErrorInfo.cs
new file mode 100644
index 00000000..7b708c60
--- /dev/null
+++ b/src/System.Web.Mvc/HandleErrorInfo.cs
@@ -0,0 +1,33 @@
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc
+{
+ public class HandleErrorInfo
+ {
+ public HandleErrorInfo(Exception exception, string controllerName, string actionName)
+ {
+ if (exception == null)
+ {
+ throw new ArgumentNullException("exception");
+ }
+ if (String.IsNullOrEmpty(controllerName))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "controllerName");
+ }
+ if (String.IsNullOrEmpty(actionName))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "actionName");
+ }
+
+ Exception = exception;
+ ControllerName = controllerName;
+ ActionName = actionName;
+ }
+
+ public string ActionName { get; private set; }
+
+ public string ControllerName { get; private set; }
+
+ public Exception Exception { get; private set; }
+ }
+}
diff --git a/src/System.Web.Mvc/HiddenInputAttribute.cs b/src/System.Web.Mvc/HiddenInputAttribute.cs
new file mode 100644
index 00000000..1692a6b0
--- /dev/null
+++ b/src/System.Web.Mvc/HiddenInputAttribute.cs
@@ -0,0 +1,13 @@
+namespace System.Web.Mvc
+{
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
+ public sealed class HiddenInputAttribute : Attribute
+ {
+ public HiddenInputAttribute()
+ {
+ DisplayValue = true;
+ }
+
+ public bool DisplayValue { get; set; }
+ }
+}
diff --git a/src/System.Web.Mvc/Html/ChildActionExtensions.cs b/src/System.Web.Mvc/Html/ChildActionExtensions.cs
new file mode 100644
index 00000000..4c74a763
--- /dev/null
+++ b/src/System.Web.Mvc/Html/ChildActionExtensions.cs
@@ -0,0 +1,181 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Web.Mvc.Properties;
+using System.Web.Routing;
+
+namespace System.Web.Mvc.Html
+{
+ public static class ChildActionExtensions
+ {
+ // Action
+
+ public static MvcHtmlString Action(this HtmlHelper htmlHelper, string actionName)
+ {
+ return Action(htmlHelper, actionName, null /* controllerName */, null /* routeValues */);
+ }
+
+ public static MvcHtmlString Action(this HtmlHelper htmlHelper, string actionName, object routeValues)
+ {
+ return Action(htmlHelper, actionName, null /* controllerName */, new RouteValueDictionary(routeValues));
+ }
+
+ public static MvcHtmlString Action(this HtmlHelper htmlHelper, string actionName, RouteValueDictionary routeValues)
+ {
+ return Action(htmlHelper, actionName, null /* controllerName */, routeValues);
+ }
+
+ public static MvcHtmlString Action(this HtmlHelper htmlHelper, string actionName, string controllerName)
+ {
+ return Action(htmlHelper, actionName, controllerName, null /* routeValues */);
+ }
+
+ public static MvcHtmlString Action(this HtmlHelper htmlHelper, string actionName, string controllerName, object routeValues)
+ {
+ return Action(htmlHelper, actionName, controllerName, new RouteValueDictionary(routeValues));
+ }
+
+ public static MvcHtmlString Action(this HtmlHelper htmlHelper, string actionName, string controllerName, RouteValueDictionary routeValues)
+ {
+ using (StringWriter writer = new StringWriter(CultureInfo.CurrentCulture))
+ {
+ ActionHelper(htmlHelper, actionName, controllerName, routeValues, writer);
+ return MvcHtmlString.Create(writer.ToString());
+ }
+ }
+
+ // RenderAction
+
+ public static void RenderAction(this HtmlHelper htmlHelper, string actionName)
+ {
+ RenderAction(htmlHelper, actionName, null /* controllerName */, null /* routeValues */);
+ }
+
+ public static void RenderAction(this HtmlHelper htmlHelper, string actionName, object routeValues)
+ {
+ RenderAction(htmlHelper, actionName, null /* controllerName */, new RouteValueDictionary(routeValues));
+ }
+
+ public static void RenderAction(this HtmlHelper htmlHelper, string actionName, RouteValueDictionary routeValues)
+ {
+ RenderAction(htmlHelper, actionName, null /* controllerName */, routeValues);
+ }
+
+ public static void RenderAction(this HtmlHelper htmlHelper, string actionName, string controllerName)
+ {
+ RenderAction(htmlHelper, actionName, controllerName, null /* routeValues */);
+ }
+
+ public static void RenderAction(this HtmlHelper htmlHelper, string actionName, string controllerName, object routeValues)
+ {
+ RenderAction(htmlHelper, actionName, controllerName, new RouteValueDictionary(routeValues));
+ }
+
+ public static void RenderAction(this HtmlHelper htmlHelper, string actionName, string controllerName, RouteValueDictionary routeValues)
+ {
+ ActionHelper(htmlHelper, actionName, controllerName, routeValues, htmlHelper.ViewContext.Writer);
+ }
+
+ // Helpers
+
+ internal static void ActionHelper(HtmlHelper htmlHelper, string actionName, string controllerName, RouteValueDictionary routeValues, TextWriter textWriter)
+ {
+ if (htmlHelper == null)
+ {
+ throw new ArgumentNullException("htmlHelper");
+ }
+ if (String.IsNullOrEmpty(actionName))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "actionName");
+ }
+
+ RouteValueDictionary additionalRouteValues = routeValues;
+ routeValues = MergeDictionaries(routeValues, htmlHelper.ViewContext.RouteData.Values);
+
+ routeValues["action"] = actionName;
+ if (!String.IsNullOrEmpty(controllerName))
+ {
+ routeValues["controller"] = controllerName;
+ }
+
+ bool usingAreas;
+ VirtualPathData vpd = htmlHelper.RouteCollection.GetVirtualPathForArea(htmlHelper.ViewContext.RequestContext, null /* name */, routeValues, out usingAreas);
+ if (vpd == null)
+ {
+ throw new InvalidOperationException(MvcResources.Common_NoRouteMatched);
+ }
+
+ if (usingAreas)
+ {
+ routeValues.Remove("area");
+ if (additionalRouteValues != null)
+ {
+ additionalRouteValues.Remove("area");
+ }
+ }
+
+ if (additionalRouteValues != null)
+ {
+ routeValues[ChildActionValueProvider.ChildActionValuesKey] = new DictionaryValueProvider<object>(additionalRouteValues, CultureInfo.InvariantCulture);
+ }
+
+ RouteData routeData = CreateRouteData(vpd.Route, routeValues, vpd.DataTokens, htmlHelper.ViewContext);
+ HttpContextBase httpContext = htmlHelper.ViewContext.HttpContext;
+ RequestContext requestContext = new RequestContext(httpContext, routeData);
+ ChildActionMvcHandler handler = new ChildActionMvcHandler(requestContext);
+ httpContext.Server.Execute(HttpHandlerUtil.WrapForServerExecute(handler), textWriter, true /* preserveForm */);
+ }
+
+ private static RouteData CreateRouteData(RouteBase route, RouteValueDictionary routeValues, RouteValueDictionary dataTokens, ViewContext parentViewContext)
+ {
+ RouteData routeData = new RouteData();
+
+ foreach (KeyValuePair<string, object> kvp in routeValues)
+ {
+ routeData.Values.Add(kvp.Key, kvp.Value);
+ }
+
+ foreach (KeyValuePair<string, object> kvp in dataTokens)
+ {
+ routeData.DataTokens.Add(kvp.Key, kvp.Value);
+ }
+
+ routeData.Route = route;
+ routeData.DataTokens[ControllerContext.ParentActionViewContextToken] = parentViewContext;
+ return routeData;
+ }
+
+ private static RouteValueDictionary MergeDictionaries(params RouteValueDictionary[] dictionaries)
+ {
+ // Merge existing route values with the user provided values
+ var result = new RouteValueDictionary();
+
+ foreach (RouteValueDictionary dictionary in dictionaries.Where(d => d != null))
+ {
+ foreach (KeyValuePair<string, object> kvp in dictionary)
+ {
+ if (!result.ContainsKey(kvp.Key))
+ {
+ result.Add(kvp.Key, kvp.Value);
+ }
+ }
+ }
+
+ return result;
+ }
+
+ internal class ChildActionMvcHandler : MvcHandler
+ {
+ public ChildActionMvcHandler(RequestContext context)
+ : base(context)
+ {
+ }
+
+ protected internal override void AddVersionHeader(HttpContextBase httpContext)
+ {
+ // No version header for child actions
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Html/DefaultDisplayTemplates.cs b/src/System.Web.Mvc/Html/DefaultDisplayTemplates.cs
new file mode 100644
index 00000000..cd329237
--- /dev/null
+++ b/src/System.Web.Mvc/Html/DefaultDisplayTemplates.cs
@@ -0,0 +1,225 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Data;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Web.Mvc.Properties;
+using System.Web.UI.WebControls;
+
+namespace System.Web.Mvc.Html
+{
+ internal static class DefaultDisplayTemplates
+ {
+ internal static string BooleanTemplate(HtmlHelper html)
+ {
+ bool? value = null;
+ if (html.ViewContext.ViewData.Model != null)
+ {
+ value = Convert.ToBoolean(html.ViewContext.ViewData.Model, CultureInfo.InvariantCulture);
+ }
+
+ return html.ViewContext.ViewData.ModelMetadata.IsNullableValueType
+ ? BooleanTemplateDropDownList(value)
+ : BooleanTemplateCheckbox(value ?? false);
+ }
+
+ private static string BooleanTemplateCheckbox(bool value)
+ {
+ TagBuilder inputTag = new TagBuilder("input");
+ inputTag.AddCssClass("check-box");
+ inputTag.Attributes["disabled"] = "disabled";
+ inputTag.Attributes["type"] = "checkbox";
+ if (value)
+ {
+ inputTag.Attributes["checked"] = "checked";
+ }
+
+ return inputTag.ToString(TagRenderMode.SelfClosing);
+ }
+
+ private static string BooleanTemplateDropDownList(bool? value)
+ {
+ StringBuilder builder = new StringBuilder();
+
+ TagBuilder selectTag = new TagBuilder("select");
+ selectTag.AddCssClass("list-box");
+ selectTag.AddCssClass("tri-state");
+ selectTag.Attributes["disabled"] = "disabled";
+ builder.Append(selectTag.ToString(TagRenderMode.StartTag));
+
+ foreach (SelectListItem item in DefaultEditorTemplates.TriStateValues(value))
+ {
+ builder.Append(SelectExtensions.ListItemToOption(item));
+ }
+
+ builder.Append(selectTag.ToString(TagRenderMode.EndTag));
+ return builder.ToString();
+ }
+
+ internal static string CollectionTemplate(HtmlHelper html)
+ {
+ return CollectionTemplate(html, TemplateHelpers.TemplateHelper);
+ }
+
+ internal static string CollectionTemplate(HtmlHelper html, TemplateHelpers.TemplateHelperDelegate templateHelper)
+ {
+ object model = html.ViewContext.ViewData.ModelMetadata.Model;
+ if (model == null)
+ {
+ return String.Empty;
+ }
+
+ IEnumerable collection = model as IEnumerable;
+ if (collection == null)
+ {
+ throw new InvalidOperationException(
+ String.Format(
+ CultureInfo.CurrentCulture,
+ MvcResources.Templates_TypeMustImplementIEnumerable,
+ model.GetType().FullName));
+ }
+
+ Type typeInCollection = typeof(string);
+ Type genericEnumerableType = TypeHelpers.ExtractGenericInterface(collection.GetType(), typeof(IEnumerable<>));
+ if (genericEnumerableType != null)
+ {
+ typeInCollection = genericEnumerableType.GetGenericArguments()[0];
+ }
+ bool typeInCollectionIsNullableValueType = TypeHelpers.IsNullableValueType(typeInCollection);
+
+ string oldPrefix = html.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix;
+
+ try
+ {
+ html.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = String.Empty;
+
+ string fieldNameBase = oldPrefix;
+ StringBuilder result = new StringBuilder();
+ int index = 0;
+
+ foreach (object item in collection)
+ {
+ Type itemType = typeInCollection;
+ if (item != null && !typeInCollectionIsNullableValueType)
+ {
+ itemType = item.GetType();
+ }
+ ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForType(() => item, itemType);
+ string fieldName = String.Format(CultureInfo.InvariantCulture, "{0}[{1}]", fieldNameBase, index++);
+ string output = templateHelper(html, metadata, fieldName, null /* templateName */, DataBoundControlMode.ReadOnly, null /* additionalViewData */);
+ result.Append(output);
+ }
+
+ return result.ToString();
+ }
+ finally
+ {
+ html.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = oldPrefix;
+ }
+ }
+
+ internal static string DecimalTemplate(HtmlHelper html)
+ {
+ if (html.ViewContext.ViewData.TemplateInfo.FormattedModelValue == html.ViewContext.ViewData.ModelMetadata.Model)
+ {
+ html.ViewContext.ViewData.TemplateInfo.FormattedModelValue = String.Format(CultureInfo.CurrentCulture, "{0:0.00}", html.ViewContext.ViewData.ModelMetadata.Model);
+ }
+
+ return StringTemplate(html);
+ }
+
+ internal static string EmailAddressTemplate(HtmlHelper html)
+ {
+ return String.Format(CultureInfo.InvariantCulture,
+ "<a href=\"mailto:{0}\">{1}</a>",
+ html.AttributeEncode(html.ViewContext.ViewData.Model),
+ html.Encode(html.ViewContext.ViewData.TemplateInfo.FormattedModelValue));
+ }
+
+ internal static string HiddenInputTemplate(HtmlHelper html)
+ {
+ if (html.ViewContext.ViewData.ModelMetadata.HideSurroundingHtml)
+ {
+ return String.Empty;
+ }
+ return StringTemplate(html);
+ }
+
+ internal static string HtmlTemplate(HtmlHelper html)
+ {
+ return html.ViewContext.ViewData.TemplateInfo.FormattedModelValue.ToString();
+ }
+
+ internal static string ObjectTemplate(HtmlHelper html)
+ {
+ return ObjectTemplate(html, TemplateHelpers.TemplateHelper);
+ }
+
+ internal static string ObjectTemplate(HtmlHelper html, TemplateHelpers.TemplateHelperDelegate templateHelper)
+ {
+ ViewDataDictionary viewData = html.ViewContext.ViewData;
+ TemplateInfo templateInfo = viewData.TemplateInfo;
+ ModelMetadata modelMetadata = viewData.ModelMetadata;
+ StringBuilder builder = new StringBuilder();
+
+ if (modelMetadata.Model == null)
+ {
+ // DDB #225237
+ return modelMetadata.NullDisplayText;
+ }
+
+ if (templateInfo.TemplateDepth > 1)
+ {
+ // DDB #224751
+ return modelMetadata.SimpleDisplayText;
+ }
+
+ foreach (ModelMetadata propertyMetadata in modelMetadata.Properties.Where(pm => ShouldShow(pm, templateInfo)))
+ {
+ if (!propertyMetadata.HideSurroundingHtml)
+ {
+ string label = propertyMetadata.GetDisplayName();
+ if (!String.IsNullOrEmpty(label))
+ {
+ builder.AppendFormat(CultureInfo.InvariantCulture, "<div class=\"display-label\">{0}</div>", label);
+ builder.AppendLine();
+ }
+
+ builder.Append("<div class=\"display-field\">");
+ }
+
+ builder.Append(templateHelper(html, propertyMetadata, propertyMetadata.PropertyName, null /* templateName */, DataBoundControlMode.ReadOnly, null /* additionalViewData */));
+
+ if (!propertyMetadata.HideSurroundingHtml)
+ {
+ builder.AppendLine("</div>");
+ }
+ }
+
+ return builder.ToString();
+ }
+
+ private static bool ShouldShow(ModelMetadata metadata, TemplateInfo templateInfo)
+ {
+ return
+ metadata.ShowForDisplay
+ && metadata.ModelType != typeof(EntityState)
+ && !metadata.IsComplexType
+ && !templateInfo.Visited(metadata);
+ }
+
+ internal static string StringTemplate(HtmlHelper html)
+ {
+ return html.Encode(html.ViewContext.ViewData.TemplateInfo.FormattedModelValue);
+ }
+
+ internal static string UrlTemplate(HtmlHelper html)
+ {
+ return String.Format(CultureInfo.InvariantCulture,
+ "<a href=\"{0}\">{1}</a>",
+ html.AttributeEncode(html.ViewContext.ViewData.Model),
+ html.Encode(html.ViewContext.ViewData.TemplateInfo.FormattedModelValue));
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Html/DefaultEditorTemplates.cs b/src/System.Web.Mvc/Html/DefaultEditorTemplates.cs
new file mode 100644
index 00000000..c19b9986
--- /dev/null
+++ b/src/System.Web.Mvc/Html/DefaultEditorTemplates.cs
@@ -0,0 +1,236 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Data;
+using System.Data.Linq;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Web.Mvc.Properties;
+using System.Web.UI.WebControls;
+
+namespace System.Web.Mvc.Html
+{
+ internal static class DefaultEditorTemplates
+ {
+ internal static string BooleanTemplate(HtmlHelper html)
+ {
+ bool? value = null;
+ if (html.ViewContext.ViewData.Model != null)
+ {
+ value = Convert.ToBoolean(html.ViewContext.ViewData.Model, CultureInfo.InvariantCulture);
+ }
+
+ return html.ViewContext.ViewData.ModelMetadata.IsNullableValueType
+ ? BooleanTemplateDropDownList(html, value)
+ : BooleanTemplateCheckbox(html, value ?? false);
+ }
+
+ private static string BooleanTemplateCheckbox(HtmlHelper html, bool value)
+ {
+ return html.CheckBox(String.Empty, value, CreateHtmlAttributes("check-box")).ToHtmlString();
+ }
+
+ private static string BooleanTemplateDropDownList(HtmlHelper html, bool? value)
+ {
+ return html.DropDownList(String.Empty, TriStateValues(value), CreateHtmlAttributes("list-box tri-state")).ToHtmlString();
+ }
+
+ internal static string CollectionTemplate(HtmlHelper html)
+ {
+ return CollectionTemplate(html, TemplateHelpers.TemplateHelper);
+ }
+
+ internal static string CollectionTemplate(HtmlHelper html, TemplateHelpers.TemplateHelperDelegate templateHelper)
+ {
+ object model = html.ViewContext.ViewData.ModelMetadata.Model;
+ if (model == null)
+ {
+ return String.Empty;
+ }
+
+ IEnumerable collection = model as IEnumerable;
+ if (collection == null)
+ {
+ throw new InvalidOperationException(
+ String.Format(
+ CultureInfo.CurrentCulture,
+ MvcResources.Templates_TypeMustImplementIEnumerable,
+ model.GetType().FullName));
+ }
+
+ Type typeInCollection = typeof(string);
+ Type genericEnumerableType = TypeHelpers.ExtractGenericInterface(collection.GetType(), typeof(IEnumerable<>));
+ if (genericEnumerableType != null)
+ {
+ typeInCollection = genericEnumerableType.GetGenericArguments()[0];
+ }
+ bool typeInCollectionIsNullableValueType = TypeHelpers.IsNullableValueType(typeInCollection);
+
+ string oldPrefix = html.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix;
+
+ try
+ {
+ html.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = String.Empty;
+
+ string fieldNameBase = oldPrefix;
+ StringBuilder result = new StringBuilder();
+ int index = 0;
+
+ foreach (object item in collection)
+ {
+ Type itemType = typeInCollection;
+ if (item != null && !typeInCollectionIsNullableValueType)
+ {
+ itemType = item.GetType();
+ }
+ ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForType(() => item, itemType);
+ string fieldName = String.Format(CultureInfo.InvariantCulture, "{0}[{1}]", fieldNameBase, index++);
+ string output = templateHelper(html, metadata, fieldName, null /* templateName */, DataBoundControlMode.Edit, null /* additionalViewData */);
+ result.Append(output);
+ }
+
+ return result.ToString();
+ }
+ finally
+ {
+ html.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = oldPrefix;
+ }
+ }
+
+ internal static string DecimalTemplate(HtmlHelper html)
+ {
+ if (html.ViewContext.ViewData.TemplateInfo.FormattedModelValue == html.ViewContext.ViewData.ModelMetadata.Model)
+ {
+ html.ViewContext.ViewData.TemplateInfo.FormattedModelValue = String.Format(CultureInfo.CurrentCulture, "{0:0.00}", html.ViewContext.ViewData.ModelMetadata.Model);
+ }
+
+ return StringTemplate(html);
+ }
+
+ internal static string HiddenInputTemplate(HtmlHelper html)
+ {
+ string result;
+
+ if (html.ViewContext.ViewData.ModelMetadata.HideSurroundingHtml)
+ {
+ result = String.Empty;
+ }
+ else
+ {
+ result = DefaultDisplayTemplates.StringTemplate(html);
+ }
+
+ object model = html.ViewContext.ViewData.Model;
+
+ Binary modelAsBinary = model as Binary;
+ if (modelAsBinary != null)
+ {
+ model = Convert.ToBase64String(modelAsBinary.ToArray());
+ }
+ else
+ {
+ byte[] modelAsByteArray = model as byte[];
+ if (modelAsByteArray != null)
+ {
+ model = Convert.ToBase64String(modelAsByteArray);
+ }
+ }
+
+ result += html.Hidden(String.Empty, model).ToHtmlString();
+ return result;
+ }
+
+ internal static string MultilineTextTemplate(HtmlHelper html)
+ {
+ return html.TextArea(String.Empty,
+ html.ViewContext.ViewData.TemplateInfo.FormattedModelValue.ToString(),
+ 0 /* rows */, 0 /* columns */,
+ CreateHtmlAttributes("text-box multi-line")).ToHtmlString();
+ }
+
+ private static IDictionary<string, object> CreateHtmlAttributes(string className)
+ {
+ return new Dictionary<string, object>()
+ {
+ { "class", className }
+ };
+ }
+
+ internal static string ObjectTemplate(HtmlHelper html)
+ {
+ return ObjectTemplate(html, TemplateHelpers.TemplateHelper);
+ }
+
+ internal static string ObjectTemplate(HtmlHelper html, TemplateHelpers.TemplateHelperDelegate templateHelper)
+ {
+ ViewDataDictionary viewData = html.ViewContext.ViewData;
+ TemplateInfo templateInfo = viewData.TemplateInfo;
+ ModelMetadata modelMetadata = viewData.ModelMetadata;
+ StringBuilder builder = new StringBuilder();
+
+ if (templateInfo.TemplateDepth > 1)
+ {
+ // DDB #224751
+ return modelMetadata.Model == null ? modelMetadata.NullDisplayText : modelMetadata.SimpleDisplayText;
+ }
+
+ foreach (ModelMetadata propertyMetadata in modelMetadata.Properties.Where(pm => ShouldShow(pm, templateInfo)))
+ {
+ if (!propertyMetadata.HideSurroundingHtml)
+ {
+ string label = LabelExtensions.LabelHelper(html, propertyMetadata, propertyMetadata.PropertyName).ToHtmlString();
+ if (!String.IsNullOrEmpty(label))
+ {
+ builder.AppendFormat(CultureInfo.InvariantCulture, "<div class=\"editor-label\">{0}</div>\r\n", label);
+ }
+
+ builder.Append("<div class=\"editor-field\">");
+ }
+
+ builder.Append(templateHelper(html, propertyMetadata, propertyMetadata.PropertyName, null /* templateName */, DataBoundControlMode.Edit, null /* additionalViewData */));
+
+ if (!propertyMetadata.HideSurroundingHtml)
+ {
+ builder.Append(" ");
+ builder.Append(html.ValidationMessage(propertyMetadata.PropertyName));
+ builder.Append("</div>\r\n");
+ }
+ }
+
+ return builder.ToString();
+ }
+
+ internal static string PasswordTemplate(HtmlHelper html)
+ {
+ return html.Password(String.Empty,
+ html.ViewContext.ViewData.TemplateInfo.FormattedModelValue,
+ CreateHtmlAttributes("text-box single-line password")).ToHtmlString();
+ }
+
+ private static bool ShouldShow(ModelMetadata metadata, TemplateInfo templateInfo)
+ {
+ return
+ metadata.ShowForEdit
+ && metadata.ModelType != typeof(EntityState)
+ && !metadata.IsComplexType
+ && !templateInfo.Visited(metadata);
+ }
+
+ internal static string StringTemplate(HtmlHelper html)
+ {
+ return html.TextBox(String.Empty,
+ html.ViewContext.ViewData.TemplateInfo.FormattedModelValue,
+ CreateHtmlAttributes("text-box single-line")).ToHtmlString();
+ }
+
+ internal static List<SelectListItem> TriStateValues(bool? value)
+ {
+ return new List<SelectListItem>
+ {
+ new SelectListItem { Text = MvcResources.Common_TriState_NotSet, Value = String.Empty, Selected = !value.HasValue },
+ new SelectListItem { Text = MvcResources.Common_TriState_True, Value = "true", Selected = value.HasValue && value.Value },
+ new SelectListItem { Text = MvcResources.Common_TriState_False, Value = "false", Selected = value.HasValue && !value.Value },
+ };
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Html/DisplayExtensions.cs b/src/System.Web.Mvc/Html/DisplayExtensions.cs
new file mode 100644
index 00000000..4b5eaa0a
--- /dev/null
+++ b/src/System.Web.Mvc/Html/DisplayExtensions.cs
@@ -0,0 +1,105 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Linq.Expressions;
+using System.Web.UI.WebControls;
+
+namespace System.Web.Mvc.Html
+{
+ public static class DisplayExtensions
+ {
+ public static MvcHtmlString Display(this HtmlHelper html, string expression)
+ {
+ return TemplateHelpers.Template(html, expression, null /* templateName */, null /* htmlFieldName */, DataBoundControlMode.ReadOnly, null /* additionalViewData */);
+ }
+
+ public static MvcHtmlString Display(this HtmlHelper html, string expression, object additionalViewData)
+ {
+ return TemplateHelpers.Template(html, expression, null /* templateName */, null /* htmlFieldName */, DataBoundControlMode.ReadOnly, additionalViewData);
+ }
+
+ public static MvcHtmlString Display(this HtmlHelper html, string expression, string templateName)
+ {
+ return TemplateHelpers.Template(html, expression, templateName, null /* htmlFieldName */, DataBoundControlMode.ReadOnly, null /* additionalViewData */);
+ }
+
+ public static MvcHtmlString Display(this HtmlHelper html, string expression, string templateName, object additionalViewData)
+ {
+ return TemplateHelpers.Template(html, expression, templateName, null /* htmlFieldName */, DataBoundControlMode.ReadOnly, additionalViewData);
+ }
+
+ public static MvcHtmlString Display(this HtmlHelper html, string expression, string templateName, string htmlFieldName)
+ {
+ return TemplateHelpers.Template(html, expression, templateName, htmlFieldName, DataBoundControlMode.ReadOnly, null /* additionalViewData */);
+ }
+
+ public static MvcHtmlString Display(this HtmlHelper html, string expression, string templateName, string htmlFieldName, object additionalViewData)
+ {
+ return TemplateHelpers.Template(html, expression, templateName, htmlFieldName, DataBoundControlMode.ReadOnly, additionalViewData);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString DisplayFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression)
+ {
+ return TemplateHelpers.TemplateFor(html, expression, null /* templateName */, null /* htmlFieldName */, DataBoundControlMode.ReadOnly, null /* additionalViewData */);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString DisplayFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, object additionalViewData)
+ {
+ return TemplateHelpers.TemplateFor(html, expression, null /* templateName */, null /* htmlFieldName */, DataBoundControlMode.ReadOnly, additionalViewData);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString DisplayFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, string templateName)
+ {
+ return TemplateHelpers.TemplateFor(html, expression, templateName, null /* htmlFieldName */, DataBoundControlMode.ReadOnly, null /* additionalViewData */);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString DisplayFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, string templateName, object additionalViewData)
+ {
+ return TemplateHelpers.TemplateFor(html, expression, templateName, null /* htmlFieldName */, DataBoundControlMode.ReadOnly, additionalViewData);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString DisplayFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, string templateName, string htmlFieldName)
+ {
+ return TemplateHelpers.TemplateFor(html, expression, templateName, htmlFieldName, DataBoundControlMode.ReadOnly, null /* additionalViewData */);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString DisplayFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, string templateName, string htmlFieldName, object additionalViewData)
+ {
+ return TemplateHelpers.TemplateFor(html, expression, templateName, htmlFieldName, DataBoundControlMode.ReadOnly, additionalViewData);
+ }
+
+ public static MvcHtmlString DisplayForModel(this HtmlHelper html)
+ {
+ return MvcHtmlString.Create(TemplateHelpers.TemplateHelper(html, html.ViewData.ModelMetadata, String.Empty, null /* templateName */, DataBoundControlMode.ReadOnly, null /* additionalViewData */));
+ }
+
+ public static MvcHtmlString DisplayForModel(this HtmlHelper html, object additionalViewData)
+ {
+ return MvcHtmlString.Create(TemplateHelpers.TemplateHelper(html, html.ViewData.ModelMetadata, String.Empty, null /* templateName */, DataBoundControlMode.ReadOnly, additionalViewData));
+ }
+
+ public static MvcHtmlString DisplayForModel(this HtmlHelper html, string templateName)
+ {
+ return MvcHtmlString.Create(TemplateHelpers.TemplateHelper(html, html.ViewData.ModelMetadata, String.Empty, templateName, DataBoundControlMode.ReadOnly, null /* additionalViewData */));
+ }
+
+ public static MvcHtmlString DisplayForModel(this HtmlHelper html, string templateName, object additionalViewData)
+ {
+ return MvcHtmlString.Create(TemplateHelpers.TemplateHelper(html, html.ViewData.ModelMetadata, String.Empty, templateName, DataBoundControlMode.ReadOnly, additionalViewData));
+ }
+
+ public static MvcHtmlString DisplayForModel(this HtmlHelper html, string templateName, string htmlFieldName)
+ {
+ return MvcHtmlString.Create(TemplateHelpers.TemplateHelper(html, html.ViewData.ModelMetadata, htmlFieldName, templateName, DataBoundControlMode.ReadOnly, null /* additionalViewData */));
+ }
+
+ public static MvcHtmlString DisplayForModel(this HtmlHelper html, string templateName, string htmlFieldName, object additionalViewData)
+ {
+ return MvcHtmlString.Create(TemplateHelpers.TemplateHelper(html, html.ViewData.ModelMetadata, htmlFieldName, templateName, DataBoundControlMode.ReadOnly, additionalViewData));
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Html/DisplayNameExtensions.cs b/src/System.Web.Mvc/Html/DisplayNameExtensions.cs
new file mode 100644
index 00000000..b683aef2
--- /dev/null
+++ b/src/System.Web.Mvc/Html/DisplayNameExtensions.cs
@@ -0,0 +1,60 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Linq.Expressions;
+
+namespace System.Web.Mvc.Html
+{
+ public static class DisplayNameExtensions
+ {
+ public static MvcHtmlString DisplayName(this HtmlHelper html, string expression)
+ {
+ return DisplayNameInternal(html, expression, metadataProvider: null);
+ }
+
+ internal static MvcHtmlString DisplayNameInternal(this HtmlHelper html, string expression, ModelMetadataProvider metadataProvider)
+ {
+ return DisplayNameHelper(ModelMetadata.FromStringExpression(expression, html.ViewData, metadataProvider),
+ expression);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString DisplayNameFor<TModel, TValue>(this HtmlHelper<IEnumerable<TModel>> html, Expression<Func<TModel, TValue>> expression)
+ {
+ return DisplayNameForInternal(html, expression, metadataProvider: null);
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", Justification = "This is an extension method")]
+ internal static MvcHtmlString DisplayNameForInternal<TModel, TValue>(this HtmlHelper<IEnumerable<TModel>> html, Expression<Func<TModel, TValue>> expression, ModelMetadataProvider metadataProvider)
+ {
+ return DisplayNameHelper(ModelMetadata.FromLambdaExpression(expression, new ViewDataDictionary<TModel>(), metadataProvider),
+ ExpressionHelper.GetExpressionText(expression));
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString DisplayNameFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression)
+ {
+ return DisplayNameForInternal(html, expression, metadataProvider: null);
+ }
+
+ internal static MvcHtmlString DisplayNameForInternal<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, ModelMetadataProvider metadataProvider)
+ {
+ return DisplayNameHelper(ModelMetadata.FromLambdaExpression(expression, html.ViewData, metadataProvider),
+ ExpressionHelper.GetExpressionText(expression));
+ }
+
+ public static MvcHtmlString DisplayNameForModel(this HtmlHelper html)
+ {
+ return DisplayNameHelper(html.ViewData.ModelMetadata, String.Empty);
+ }
+
+ internal static MvcHtmlString DisplayNameHelper(ModelMetadata metadata, string htmlFieldName)
+ {
+ // We don't call ModelMetadata.GetDisplayName here because we want to fall back to the field name rather than the ModelType.
+ // This is similar to how the LabelHelpers get the text of a label.
+ string resolvedDisplayName = metadata.DisplayName ?? metadata.PropertyName ?? htmlFieldName.Split('.').Last();
+
+ return new MvcHtmlString(HttpUtility.HtmlEncode(resolvedDisplayName));
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Html/DisplayTextExtensions.cs b/src/System.Web.Mvc/Html/DisplayTextExtensions.cs
new file mode 100644
index 00000000..92142125
--- /dev/null
+++ b/src/System.Web.Mvc/Html/DisplayTextExtensions.cs
@@ -0,0 +1,24 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Linq.Expressions;
+
+namespace System.Web.Mvc.Html
+{
+ public static class DisplayTextExtensions
+ {
+ public static MvcHtmlString DisplayText(this HtmlHelper html, string name)
+ {
+ return DisplayTextHelper(ModelMetadata.FromStringExpression(name, html.ViewContext.ViewData));
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString DisplayTextFor<TModel, TResult>(this HtmlHelper<TModel> html, Expression<Func<TModel, TResult>> expression)
+ {
+ return DisplayTextHelper(ModelMetadata.FromLambdaExpression(expression, html.ViewData));
+ }
+
+ private static MvcHtmlString DisplayTextHelper(ModelMetadata metadata)
+ {
+ return MvcHtmlString.Create(metadata.SimpleDisplayText);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Html/EditorExtensions.cs b/src/System.Web.Mvc/Html/EditorExtensions.cs
new file mode 100644
index 00000000..7caa4887
--- /dev/null
+++ b/src/System.Web.Mvc/Html/EditorExtensions.cs
@@ -0,0 +1,105 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Linq.Expressions;
+using System.Web.UI.WebControls;
+
+namespace System.Web.Mvc.Html
+{
+ public static class EditorExtensions
+ {
+ public static MvcHtmlString Editor(this HtmlHelper html, string expression)
+ {
+ return TemplateHelpers.Template(html, expression, null /* templateName */, null /* htmlFieldName */, DataBoundControlMode.Edit, null /* additionalViewData */);
+ }
+
+ public static MvcHtmlString Editor(this HtmlHelper html, string expression, object additionalViewData)
+ {
+ return TemplateHelpers.Template(html, expression, null /* templateName */, null /* htmlFieldName */, DataBoundControlMode.Edit, additionalViewData);
+ }
+
+ public static MvcHtmlString Editor(this HtmlHelper html, string expression, string templateName)
+ {
+ return TemplateHelpers.Template(html, expression, templateName, null /* htmlFieldName */, DataBoundControlMode.Edit, null /* additionalViewData */);
+ }
+
+ public static MvcHtmlString Editor(this HtmlHelper html, string expression, string templateName, object additionalViewData)
+ {
+ return TemplateHelpers.Template(html, expression, templateName, null /* htmlFieldName */, DataBoundControlMode.Edit, additionalViewData);
+ }
+
+ public static MvcHtmlString Editor(this HtmlHelper html, string expression, string templateName, string htmlFieldName)
+ {
+ return TemplateHelpers.Template(html, expression, templateName, htmlFieldName, DataBoundControlMode.Edit, null /* additionalViewData */);
+ }
+
+ public static MvcHtmlString Editor(this HtmlHelper html, string expression, string templateName, string htmlFieldName, object additionalViewData)
+ {
+ return TemplateHelpers.Template(html, expression, templateName, htmlFieldName, DataBoundControlMode.Edit, additionalViewData);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString EditorFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression)
+ {
+ return TemplateHelpers.TemplateFor(html, expression, null /* templateName */, null /* htmlFieldName */, DataBoundControlMode.Edit, null /* additionalViewData */);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString EditorFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, object additionalViewData)
+ {
+ return TemplateHelpers.TemplateFor(html, expression, null /* templateName */, null /* htmlFieldName */, DataBoundControlMode.Edit, additionalViewData);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString EditorFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, string templateName)
+ {
+ return TemplateHelpers.TemplateFor(html, expression, templateName, null /* htmlFieldName */, DataBoundControlMode.Edit, null /* additionalViewData */);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString EditorFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, string templateName, object additionalViewData)
+ {
+ return TemplateHelpers.TemplateFor(html, expression, templateName, null /* htmlFieldName */, DataBoundControlMode.Edit, additionalViewData);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString EditorFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, string templateName, string htmlFieldName)
+ {
+ return TemplateHelpers.TemplateFor(html, expression, templateName, htmlFieldName, DataBoundControlMode.Edit, null /* additionalViewData */);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString EditorFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, string templateName, string htmlFieldName, object additionalViewData)
+ {
+ return TemplateHelpers.TemplateFor(html, expression, templateName, htmlFieldName, DataBoundControlMode.Edit, additionalViewData);
+ }
+
+ public static MvcHtmlString EditorForModel(this HtmlHelper html)
+ {
+ return MvcHtmlString.Create(TemplateHelpers.TemplateHelper(html, html.ViewData.ModelMetadata, String.Empty, null /* templateName */, DataBoundControlMode.Edit, null /* additionalViewData */));
+ }
+
+ public static MvcHtmlString EditorForModel(this HtmlHelper html, object additionalViewData)
+ {
+ return MvcHtmlString.Create(TemplateHelpers.TemplateHelper(html, html.ViewData.ModelMetadata, String.Empty, null /* templateName */, DataBoundControlMode.Edit, additionalViewData));
+ }
+
+ public static MvcHtmlString EditorForModel(this HtmlHelper html, string templateName)
+ {
+ return MvcHtmlString.Create(TemplateHelpers.TemplateHelper(html, html.ViewData.ModelMetadata, String.Empty, templateName, DataBoundControlMode.Edit, null /* additionalViewData */));
+ }
+
+ public static MvcHtmlString EditorForModel(this HtmlHelper html, string templateName, object additionalViewData)
+ {
+ return MvcHtmlString.Create(TemplateHelpers.TemplateHelper(html, html.ViewData.ModelMetadata, String.Empty, templateName, DataBoundControlMode.Edit, additionalViewData));
+ }
+
+ public static MvcHtmlString EditorForModel(this HtmlHelper html, string templateName, string htmlFieldName)
+ {
+ return MvcHtmlString.Create(TemplateHelpers.TemplateHelper(html, html.ViewData.ModelMetadata, htmlFieldName, templateName, DataBoundControlMode.Edit, null /* additionalViewData */));
+ }
+
+ public static MvcHtmlString EditorForModel(this HtmlHelper html, string templateName, string htmlFieldName, object additionalViewData)
+ {
+ return MvcHtmlString.Create(TemplateHelpers.TemplateHelper(html, html.ViewData.ModelMetadata, htmlFieldName, templateName, DataBoundControlMode.Edit, additionalViewData));
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Html/FormExtensions.cs b/src/System.Web.Mvc/Html/FormExtensions.cs
new file mode 100644
index 00000000..4a6780cb
--- /dev/null
+++ b/src/System.Web.Mvc/Html/FormExtensions.cs
@@ -0,0 +1,180 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Web.Routing;
+
+namespace System.Web.Mvc.Html
+{
+ public static class FormExtensions
+ {
+ public static MvcForm BeginForm(this HtmlHelper htmlHelper)
+ {
+ // generates <form action="{current url}" method="post">...</form>
+ string formAction = htmlHelper.ViewContext.HttpContext.Request.RawUrl;
+ return FormHelper(htmlHelper, formAction, FormMethod.Post, new RouteValueDictionary());
+ }
+
+ public static MvcForm BeginForm(this HtmlHelper htmlHelper, object routeValues)
+ {
+ return BeginForm(htmlHelper, null, null, new RouteValueDictionary(routeValues), FormMethod.Post, new RouteValueDictionary());
+ }
+
+ public static MvcForm BeginForm(this HtmlHelper htmlHelper, RouteValueDictionary routeValues)
+ {
+ return BeginForm(htmlHelper, null, null, routeValues, FormMethod.Post, new RouteValueDictionary());
+ }
+
+ public static MvcForm BeginForm(this HtmlHelper htmlHelper, string actionName, string controllerName)
+ {
+ return BeginForm(htmlHelper, actionName, controllerName, new RouteValueDictionary(), FormMethod.Post, new RouteValueDictionary());
+ }
+
+ public static MvcForm BeginForm(this HtmlHelper htmlHelper, string actionName, string controllerName, object routeValues)
+ {
+ return BeginForm(htmlHelper, actionName, controllerName, new RouteValueDictionary(routeValues), FormMethod.Post, new RouteValueDictionary());
+ }
+
+ public static MvcForm BeginForm(this HtmlHelper htmlHelper, string actionName, string controllerName, RouteValueDictionary routeValues)
+ {
+ return BeginForm(htmlHelper, actionName, controllerName, routeValues, FormMethod.Post, new RouteValueDictionary());
+ }
+
+ public static MvcForm BeginForm(this HtmlHelper htmlHelper, string actionName, string controllerName, FormMethod method)
+ {
+ return BeginForm(htmlHelper, actionName, controllerName, new RouteValueDictionary(), method, new RouteValueDictionary());
+ }
+
+ public static MvcForm BeginForm(this HtmlHelper htmlHelper, string actionName, string controllerName, object routeValues, FormMethod method)
+ {
+ return BeginForm(htmlHelper, actionName, controllerName, new RouteValueDictionary(routeValues), method, new RouteValueDictionary());
+ }
+
+ public static MvcForm BeginForm(this HtmlHelper htmlHelper, string actionName, string controllerName, RouteValueDictionary routeValues, FormMethod method)
+ {
+ return BeginForm(htmlHelper, actionName, controllerName, routeValues, method, new RouteValueDictionary());
+ }
+
+ public static MvcForm BeginForm(this HtmlHelper htmlHelper, string actionName, string controllerName, FormMethod method, object htmlAttributes)
+ {
+ return BeginForm(htmlHelper, actionName, controllerName, new RouteValueDictionary(), method, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ public static MvcForm BeginForm(this HtmlHelper htmlHelper, string actionName, string controllerName, FormMethod method, IDictionary<string, object> htmlAttributes)
+ {
+ return BeginForm(htmlHelper, actionName, controllerName, new RouteValueDictionary(), method, htmlAttributes);
+ }
+
+ public static MvcForm BeginForm(this HtmlHelper htmlHelper, string actionName, string controllerName, object routeValues, FormMethod method, object htmlAttributes)
+ {
+ return BeginForm(htmlHelper, actionName, controllerName, new RouteValueDictionary(routeValues), method, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ public static MvcForm BeginForm(this HtmlHelper htmlHelper, string actionName, string controllerName, RouteValueDictionary routeValues, FormMethod method, IDictionary<string, object> htmlAttributes)
+ {
+ string formAction = UrlHelper.GenerateUrl(null /* routeName */, actionName, controllerName, routeValues, htmlHelper.RouteCollection, htmlHelper.ViewContext.RequestContext, true /* includeImplicitMvcValues */);
+ return FormHelper(htmlHelper, formAction, method, htmlAttributes);
+ }
+
+ public static MvcForm BeginRouteForm(this HtmlHelper htmlHelper, object routeValues)
+ {
+ return BeginRouteForm(htmlHelper, null /* routeName */, new RouteValueDictionary(routeValues), FormMethod.Post, new RouteValueDictionary());
+ }
+
+ public static MvcForm BeginRouteForm(this HtmlHelper htmlHelper, RouteValueDictionary routeValues)
+ {
+ return BeginRouteForm(htmlHelper, null /* routeName */, routeValues, FormMethod.Post, new RouteValueDictionary());
+ }
+
+ public static MvcForm BeginRouteForm(this HtmlHelper htmlHelper, string routeName)
+ {
+ return BeginRouteForm(htmlHelper, routeName, new RouteValueDictionary(), FormMethod.Post, new RouteValueDictionary());
+ }
+
+ public static MvcForm BeginRouteForm(this HtmlHelper htmlHelper, string routeName, object routeValues)
+ {
+ return BeginRouteForm(htmlHelper, routeName, new RouteValueDictionary(routeValues), FormMethod.Post, new RouteValueDictionary());
+ }
+
+ public static MvcForm BeginRouteForm(this HtmlHelper htmlHelper, string routeName, RouteValueDictionary routeValues)
+ {
+ return BeginRouteForm(htmlHelper, routeName, routeValues, FormMethod.Post, new RouteValueDictionary());
+ }
+
+ public static MvcForm BeginRouteForm(this HtmlHelper htmlHelper, string routeName, FormMethod method)
+ {
+ return BeginRouteForm(htmlHelper, routeName, new RouteValueDictionary(), method, new RouteValueDictionary());
+ }
+
+ public static MvcForm BeginRouteForm(this HtmlHelper htmlHelper, string routeName, object routeValues, FormMethod method)
+ {
+ return BeginRouteForm(htmlHelper, routeName, new RouteValueDictionary(routeValues), method, new RouteValueDictionary());
+ }
+
+ public static MvcForm BeginRouteForm(this HtmlHelper htmlHelper, string routeName, RouteValueDictionary routeValues, FormMethod method)
+ {
+ return BeginRouteForm(htmlHelper, routeName, routeValues, method, new RouteValueDictionary());
+ }
+
+ public static MvcForm BeginRouteForm(this HtmlHelper htmlHelper, string routeName, FormMethod method, object htmlAttributes)
+ {
+ return BeginRouteForm(htmlHelper, routeName, new RouteValueDictionary(), method, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ public static MvcForm BeginRouteForm(this HtmlHelper htmlHelper, string routeName, FormMethod method, IDictionary<string, object> htmlAttributes)
+ {
+ return BeginRouteForm(htmlHelper, routeName, new RouteValueDictionary(), method, htmlAttributes);
+ }
+
+ public static MvcForm BeginRouteForm(this HtmlHelper htmlHelper, string routeName, object routeValues, FormMethod method, object htmlAttributes)
+ {
+ return BeginRouteForm(htmlHelper, routeName, new RouteValueDictionary(routeValues), method, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ public static MvcForm BeginRouteForm(this HtmlHelper htmlHelper, string routeName, RouteValueDictionary routeValues, FormMethod method, IDictionary<string, object> htmlAttributes)
+ {
+ string formAction = UrlHelper.GenerateUrl(routeName, null, null, routeValues, htmlHelper.RouteCollection, htmlHelper.ViewContext.RequestContext, false /* includeImplicitMvcValues */);
+ return FormHelper(htmlHelper, formAction, method, htmlAttributes);
+ }
+
+ public static void EndForm(this HtmlHelper htmlHelper)
+ {
+ EndForm(htmlHelper.ViewContext);
+ }
+
+ internal static void EndForm(ViewContext viewContext)
+ {
+ viewContext.Writer.Write("</form>");
+ viewContext.OutputClientValidation();
+ viewContext.FormContext = null;
+ }
+
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Because disposing the object would write to the response stream, you don't want to prematurely dispose of this object.")]
+ private static MvcForm FormHelper(this HtmlHelper htmlHelper, string formAction, FormMethod method, IDictionary<string, object> htmlAttributes)
+ {
+ TagBuilder tagBuilder = new TagBuilder("form");
+ tagBuilder.MergeAttributes(htmlAttributes);
+ // action is implicitly generated, so htmlAttributes take precedence.
+ tagBuilder.MergeAttribute("action", formAction);
+ // method is an explicit parameter, so it takes precedence over the htmlAttributes.
+ tagBuilder.MergeAttribute("method", HtmlHelper.GetFormMethodString(method), true);
+
+ bool traditionalJavascriptEnabled = htmlHelper.ViewContext.ClientValidationEnabled
+ && !htmlHelper.ViewContext.UnobtrusiveJavaScriptEnabled;
+
+ if (traditionalJavascriptEnabled)
+ {
+ // forms must have an ID for client validation
+ tagBuilder.GenerateId(htmlHelper.ViewContext.FormIdGenerator());
+ }
+
+ htmlHelper.ViewContext.Writer.Write(tagBuilder.ToString(TagRenderMode.StartTag));
+ MvcForm theForm = new MvcForm(htmlHelper.ViewContext);
+
+ if (traditionalJavascriptEnabled)
+ {
+ htmlHelper.ViewContext.FormContext.FormId = tagBuilder.Attributes["id"];
+ }
+
+ return theForm;
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Html/InputExtensions.cs b/src/System.Web.Mvc/Html/InputExtensions.cs
new file mode 100644
index 00000000..fa3d7e9c
--- /dev/null
+++ b/src/System.Web.Mvc/Html/InputExtensions.cs
@@ -0,0 +1,581 @@
+using System.Collections.Generic;
+using System.Data.Linq;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq.Expressions;
+using System.Text;
+using System.Web.Mvc.Properties;
+using System.Web.Routing;
+
+namespace System.Web.Mvc.Html
+{
+ public static class InputExtensions
+ {
+ // CheckBox
+
+ public static MvcHtmlString CheckBox(this HtmlHelper htmlHelper, string name)
+ {
+ return CheckBox(htmlHelper, name, htmlAttributes: (object)null);
+ }
+
+ public static MvcHtmlString CheckBox(this HtmlHelper htmlHelper, string name, bool isChecked)
+ {
+ return CheckBox(htmlHelper, name, isChecked, htmlAttributes: (object)null);
+ }
+
+ public static MvcHtmlString CheckBox(this HtmlHelper htmlHelper, string name, bool isChecked, object htmlAttributes)
+ {
+ return CheckBox(htmlHelper, name, isChecked, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ public static MvcHtmlString CheckBox(this HtmlHelper htmlHelper, string name, object htmlAttributes)
+ {
+ return CheckBox(htmlHelper, name, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ public static MvcHtmlString CheckBox(this HtmlHelper htmlHelper, string name, IDictionary<string, object> htmlAttributes)
+ {
+ return CheckBoxHelper(htmlHelper, metadata: null, name: name, isChecked: null, htmlAttributes: htmlAttributes);
+ }
+
+ public static MvcHtmlString CheckBox(this HtmlHelper htmlHelper, string name, bool isChecked, IDictionary<string, object> htmlAttributes)
+ {
+ return CheckBoxHelper(htmlHelper, metadata: null, name: name, isChecked: isChecked, htmlAttributes: htmlAttributes);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString CheckBoxFor<TModel>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, bool>> expression)
+ {
+ return CheckBoxFor(htmlHelper, expression, htmlAttributes: null);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString CheckBoxFor<TModel>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, bool>> expression, object htmlAttributes)
+ {
+ return CheckBoxFor(htmlHelper, expression, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString CheckBoxFor<TModel>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, bool>> expression, IDictionary<string, object> htmlAttributes)
+ {
+ if (expression == null)
+ {
+ throw new ArgumentNullException("expression");
+ }
+
+ ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
+ bool? isChecked = null;
+ if (metadata.Model != null)
+ {
+ bool modelChecked;
+ if (Boolean.TryParse(metadata.Model.ToString(), out modelChecked))
+ {
+ isChecked = modelChecked;
+ }
+ }
+
+ return CheckBoxHelper(htmlHelper, metadata, ExpressionHelper.GetExpressionText(expression), isChecked, htmlAttributes);
+ }
+
+ private static MvcHtmlString CheckBoxHelper(HtmlHelper htmlHelper, ModelMetadata metadata, string name, bool? isChecked, IDictionary<string, object> htmlAttributes)
+ {
+ RouteValueDictionary attributes = ToRouteValueDictionary(htmlAttributes);
+
+ bool explicitValue = isChecked.HasValue;
+ if (explicitValue)
+ {
+ attributes.Remove("checked"); // Explicit value must override dictionary
+ }
+
+ return InputHelper(htmlHelper,
+ InputType.CheckBox,
+ metadata,
+ name,
+ value: "true",
+ useViewData: !explicitValue,
+ isChecked: isChecked ?? false,
+ setId: true,
+ isExplicitValue: false,
+ format: null,
+ htmlAttributes: attributes);
+ }
+
+ // Hidden
+
+ public static MvcHtmlString Hidden(this HtmlHelper htmlHelper, string name)
+ {
+ return Hidden(htmlHelper, name, value: null, htmlAttributes: null);
+ }
+
+ public static MvcHtmlString Hidden(this HtmlHelper htmlHelper, string name, object value)
+ {
+ return Hidden(htmlHelper, name, value, htmlAttributes: null);
+ }
+
+ public static MvcHtmlString Hidden(this HtmlHelper htmlHelper, string name, object value, object htmlAttributes)
+ {
+ return Hidden(htmlHelper, name, value, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ public static MvcHtmlString Hidden(this HtmlHelper htmlHelper, string name, object value, IDictionary<string, object> htmlAttributes)
+ {
+ return HiddenHelper(htmlHelper,
+ metadata: null,
+ value: value,
+ useViewData: value == null,
+ expression: name,
+ htmlAttributes: htmlAttributes);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString HiddenFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression)
+ {
+ return HiddenFor(htmlHelper, expression, (IDictionary<string, object>)null);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString HiddenFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, object htmlAttributes)
+ {
+ return HiddenFor(htmlHelper, expression, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString HiddenFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IDictionary<string, object> htmlAttributes)
+ {
+ ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
+ return HiddenHelper(htmlHelper,
+ metadata,
+ metadata.Model,
+ false,
+ ExpressionHelper.GetExpressionText(expression),
+ htmlAttributes);
+ }
+
+ private static MvcHtmlString HiddenHelper(HtmlHelper htmlHelper, ModelMetadata metadata, object value, bool useViewData, string expression, IDictionary<string, object> htmlAttributes)
+ {
+ Binary binaryValue = value as Binary;
+ if (binaryValue != null)
+ {
+ value = binaryValue.ToArray();
+ }
+
+ byte[] byteArrayValue = value as byte[];
+ if (byteArrayValue != null)
+ {
+ value = Convert.ToBase64String(byteArrayValue);
+ }
+
+ return InputHelper(htmlHelper,
+ InputType.Hidden,
+ metadata,
+ expression,
+ value,
+ useViewData,
+ isChecked: false,
+ setId: true,
+ isExplicitValue: true,
+ format: null,
+ htmlAttributes: htmlAttributes);
+ }
+
+ // Password
+
+ public static MvcHtmlString Password(this HtmlHelper htmlHelper, string name)
+ {
+ return Password(htmlHelper, name, value: null);
+ }
+
+ public static MvcHtmlString Password(this HtmlHelper htmlHelper, string name, object value)
+ {
+ return Password(htmlHelper, name, value, htmlAttributes: null);
+ }
+
+ public static MvcHtmlString Password(this HtmlHelper htmlHelper, string name, object value, object htmlAttributes)
+ {
+ return Password(htmlHelper, name, value, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ public static MvcHtmlString Password(this HtmlHelper htmlHelper, string name, object value, IDictionary<string, object> htmlAttributes)
+ {
+ return PasswordHelper(htmlHelper, metadata: null, name: name, value: value, htmlAttributes: htmlAttributes);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString PasswordFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression)
+ {
+ return PasswordFor(htmlHelper, expression, htmlAttributes: null);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString PasswordFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, object htmlAttributes)
+ {
+ return PasswordFor(htmlHelper, expression, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Users cannot use anonymous methods with the LambdaExpression type")]
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString PasswordFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IDictionary<string, object> htmlAttributes)
+ {
+ if (expression == null)
+ {
+ throw new ArgumentNullException("expression");
+ }
+
+ return PasswordHelper(htmlHelper,
+ ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData),
+ ExpressionHelper.GetExpressionText(expression),
+ value: null,
+ htmlAttributes: htmlAttributes);
+ }
+
+ private static MvcHtmlString PasswordHelper(HtmlHelper htmlHelper, ModelMetadata metadata, string name, object value, IDictionary<string, object> htmlAttributes)
+ {
+ return InputHelper(htmlHelper,
+ InputType.Password,
+ metadata,
+ name,
+ value,
+ useViewData: false,
+ isChecked: false,
+ setId: true,
+ isExplicitValue: true,
+ format: null,
+ htmlAttributes: htmlAttributes);
+ }
+
+ // RadioButton
+
+ public static MvcHtmlString RadioButton(this HtmlHelper htmlHelper, string name, object value)
+ {
+ return RadioButton(htmlHelper, name, value, htmlAttributes: (object)null);
+ }
+
+ public static MvcHtmlString RadioButton(this HtmlHelper htmlHelper, string name, object value, object htmlAttributes)
+ {
+ return RadioButton(htmlHelper, name, value, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ public static MvcHtmlString RadioButton(this HtmlHelper htmlHelper, string name, object value, IDictionary<string, object> htmlAttributes)
+ {
+ // Determine whether or not to render the checked attribute based on the contents of ViewData.
+ string valueString = Convert.ToString(value, CultureInfo.CurrentCulture);
+ bool isChecked = (!String.IsNullOrEmpty(name)) && (String.Equals(htmlHelper.EvalString(name), valueString, StringComparison.OrdinalIgnoreCase));
+ // checked attributes is implicit, so we need to ensure that the dictionary takes precedence.
+ RouteValueDictionary attributes = ToRouteValueDictionary(htmlAttributes);
+ if (attributes.ContainsKey("checked"))
+ {
+ return InputHelper(htmlHelper,
+ InputType.Radio,
+ metadata: null,
+ name: name,
+ value: value,
+ useViewData: false,
+ isChecked: false,
+ setId: true,
+ isExplicitValue: true,
+ format: null,
+ htmlAttributes: attributes);
+ }
+
+ return RadioButton(htmlHelper, name, value, isChecked, htmlAttributes);
+ }
+
+ public static MvcHtmlString RadioButton(this HtmlHelper htmlHelper, string name, object value, bool isChecked)
+ {
+ return RadioButton(htmlHelper, name, value, isChecked, htmlAttributes: (object)null);
+ }
+
+ public static MvcHtmlString RadioButton(this HtmlHelper htmlHelper, string name, object value, bool isChecked, object htmlAttributes)
+ {
+ return RadioButton(htmlHelper, name, value, isChecked, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ public static MvcHtmlString RadioButton(this HtmlHelper htmlHelper, string name, object value, bool isChecked, IDictionary<string, object> htmlAttributes)
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException("value");
+ }
+ // checked attribute is an explicit parameter so it takes precedence.
+ RouteValueDictionary attributes = ToRouteValueDictionary(htmlAttributes);
+ attributes.Remove("checked");
+ return InputHelper(htmlHelper,
+ InputType.Radio,
+ metadata: null,
+ name: name,
+ value: value,
+ useViewData: false,
+ isChecked: isChecked,
+ setId: true,
+ isExplicitValue: true,
+ format: null,
+ htmlAttributes: attributes);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString RadioButtonFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, object value)
+ {
+ return RadioButtonFor(htmlHelper, expression, value, htmlAttributes: null);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString RadioButtonFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, object value, object htmlAttributes)
+ {
+ return RadioButtonFor(htmlHelper, expression, value, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString RadioButtonFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, object value, IDictionary<string, object> htmlAttributes)
+ {
+ ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
+ return RadioButtonHelper(htmlHelper,
+ metadata,
+ metadata.Model,
+ ExpressionHelper.GetExpressionText(expression),
+ value,
+ null /* isChecked */,
+ htmlAttributes);
+ }
+
+ private static MvcHtmlString RadioButtonHelper(HtmlHelper htmlHelper, ModelMetadata metadata, object model, string name, object value, bool? isChecked, IDictionary<string, object> htmlAttributes)
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException("value");
+ }
+
+ RouteValueDictionary attributes = ToRouteValueDictionary(htmlAttributes);
+
+ bool explicitValue = isChecked.HasValue;
+ if (explicitValue)
+ {
+ attributes.Remove("checked"); // Explicit value must override dictionary
+ }
+ else
+ {
+ string valueString = Convert.ToString(value, CultureInfo.CurrentCulture);
+ isChecked = model != null &&
+ !String.IsNullOrEmpty(name) &&
+ String.Equals(model.ToString(), valueString, StringComparison.OrdinalIgnoreCase);
+ }
+
+ return InputHelper(htmlHelper,
+ InputType.Radio,
+ metadata,
+ name,
+ value,
+ useViewData: false,
+ isChecked: isChecked ?? false,
+ setId: true,
+ isExplicitValue: true,
+ format: null,
+ htmlAttributes: attributes);
+ }
+
+ // TextBox
+
+ public static MvcHtmlString TextBox(this HtmlHelper htmlHelper, string name)
+ {
+ return TextBox(htmlHelper, name, value: null);
+ }
+
+ public static MvcHtmlString TextBox(this HtmlHelper htmlHelper, string name, object value)
+ {
+ return TextBox(htmlHelper, name, value, format: null);
+ }
+
+ public static MvcHtmlString TextBox(this HtmlHelper htmlHelper, string name, object value, string format)
+ {
+ return TextBox(htmlHelper, name, value, format, htmlAttributes: (object)null);
+ }
+
+ public static MvcHtmlString TextBox(this HtmlHelper htmlHelper, string name, object value, object htmlAttributes)
+ {
+ return TextBox(htmlHelper, name, value, format: null, htmlAttributes: htmlAttributes);
+ }
+
+ public static MvcHtmlString TextBox(this HtmlHelper htmlHelper, string name, object value, string format, object htmlAttributes)
+ {
+ return TextBox(htmlHelper, name, value, format, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ public static MvcHtmlString TextBox(this HtmlHelper htmlHelper, string name, object value, IDictionary<string, object> htmlAttributes)
+ {
+ return TextBox(htmlHelper, name, value, format: null, htmlAttributes: htmlAttributes);
+ }
+
+ public static MvcHtmlString TextBox(this HtmlHelper htmlHelper, string name, object value, string format, IDictionary<string, object> htmlAttributes)
+ {
+ return InputHelper(htmlHelper,
+ InputType.Text,
+ metadata: null,
+ name: name,
+ value: value,
+ useViewData: (value == null),
+ isChecked: false,
+ setId: true,
+ isExplicitValue: true,
+ format: format,
+ htmlAttributes: htmlAttributes);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString TextBoxFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression)
+ {
+ return htmlHelper.TextBoxFor(expression, format: null);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString TextBoxFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, string format)
+ {
+ return htmlHelper.TextBoxFor(expression, format, (IDictionary<string, object>)null);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString TextBoxFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, object htmlAttributes)
+ {
+ return htmlHelper.TextBoxFor(expression, format: null, htmlAttributes: htmlAttributes);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString TextBoxFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, string format, object htmlAttributes)
+ {
+ return htmlHelper.TextBoxFor(expression, format: format, htmlAttributes: HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString TextBoxFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IDictionary<string, object> htmlAttributes)
+ {
+ return htmlHelper.TextBoxFor(expression, format: null, htmlAttributes: htmlAttributes);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString TextBoxFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, string format, IDictionary<string, object> htmlAttributes)
+ {
+ ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
+ return TextBoxHelper(htmlHelper,
+ metadata,
+ metadata.Model,
+ ExpressionHelper.GetExpressionText(expression),
+ format,
+ htmlAttributes);
+ }
+
+ private static MvcHtmlString TextBoxHelper(this HtmlHelper htmlHelper, ModelMetadata metadata, object model, string expression, string format, IDictionary<string, object> htmlAttributes)
+ {
+ return InputHelper(htmlHelper,
+ InputType.Text,
+ metadata,
+ expression,
+ model,
+ useViewData: false,
+ isChecked: false,
+ setId: true,
+ isExplicitValue: true,
+ format: format,
+ htmlAttributes: htmlAttributes);
+ }
+
+ // Helper methods
+
+ private static MvcHtmlString InputHelper(HtmlHelper htmlHelper, InputType inputType, ModelMetadata metadata, string name, object value, bool useViewData, bool isChecked, bool setId, bool isExplicitValue, string format, IDictionary<string, object> htmlAttributes)
+ {
+ string fullName = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);
+ if (String.IsNullOrEmpty(fullName))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "name");
+ }
+
+ TagBuilder tagBuilder = new TagBuilder("input");
+ tagBuilder.MergeAttributes(htmlAttributes);
+ tagBuilder.MergeAttribute("type", HtmlHelper.GetInputTypeString(inputType));
+ tagBuilder.MergeAttribute("name", fullName, true);
+
+ string valueParameter = htmlHelper.FormatValue(value, format);
+ bool usedModelState = false;
+
+ switch (inputType)
+ {
+ case InputType.CheckBox:
+ bool? modelStateWasChecked = htmlHelper.GetModelStateValue(fullName, typeof(bool)) as bool?;
+ if (modelStateWasChecked.HasValue)
+ {
+ isChecked = modelStateWasChecked.Value;
+ usedModelState = true;
+ }
+ goto case InputType.Radio;
+ case InputType.Radio:
+ if (!usedModelState)
+ {
+ string modelStateValue = htmlHelper.GetModelStateValue(fullName, typeof(string)) as string;
+ if (modelStateValue != null)
+ {
+ isChecked = String.Equals(modelStateValue, valueParameter, StringComparison.Ordinal);
+ usedModelState = true;
+ }
+ }
+ if (!usedModelState && useViewData)
+ {
+ isChecked = htmlHelper.EvalBoolean(fullName);
+ }
+ if (isChecked)
+ {
+ tagBuilder.MergeAttribute("checked", "checked");
+ }
+ tagBuilder.MergeAttribute("value", valueParameter, isExplicitValue);
+ break;
+ case InputType.Password:
+ if (value != null)
+ {
+ tagBuilder.MergeAttribute("value", valueParameter, isExplicitValue);
+ }
+ break;
+ default:
+ string attemptedValue = (string)htmlHelper.GetModelStateValue(fullName, typeof(string));
+ tagBuilder.MergeAttribute("value", attemptedValue ?? ((useViewData) ? htmlHelper.EvalString(fullName, format) : valueParameter), isExplicitValue);
+ break;
+ }
+
+ if (setId)
+ {
+ tagBuilder.GenerateId(fullName);
+ }
+
+ // If there are any errors for a named field, we add the css attribute.
+ ModelState modelState;
+ if (htmlHelper.ViewData.ModelState.TryGetValue(fullName, out modelState))
+ {
+ if (modelState.Errors.Count > 0)
+ {
+ tagBuilder.AddCssClass(HtmlHelper.ValidationInputCssClassName);
+ }
+ }
+
+ tagBuilder.MergeAttributes(htmlHelper.GetUnobtrusiveValidationAttributes(name, metadata));
+
+ if (inputType == InputType.CheckBox)
+ {
+ // Render an additional <input type="hidden".../> for checkboxes. This
+ // addresses scenarios where unchecked checkboxes are not sent in the request.
+ // Sending a hidden input makes it possible to know that the checkbox was present
+ // on the page when the request was submitted.
+ StringBuilder inputItemBuilder = new StringBuilder();
+ inputItemBuilder.Append(tagBuilder.ToString(TagRenderMode.SelfClosing));
+
+ TagBuilder hiddenInput = new TagBuilder("input");
+ hiddenInput.MergeAttribute("type", HtmlHelper.GetInputTypeString(InputType.Hidden));
+ hiddenInput.MergeAttribute("name", fullName);
+ hiddenInput.MergeAttribute("value", "false");
+ inputItemBuilder.Append(hiddenInput.ToString(TagRenderMode.SelfClosing));
+ return MvcHtmlString.Create(inputItemBuilder.ToString());
+ }
+
+ return tagBuilder.ToMvcHtmlString(TagRenderMode.SelfClosing);
+ }
+
+ private static RouteValueDictionary ToRouteValueDictionary(IDictionary<string, object> dictionary)
+ {
+ return dictionary == null ? new RouteValueDictionary() : new RouteValueDictionary(dictionary);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Html/LabelExtensions.cs b/src/System.Web.Mvc/Html/LabelExtensions.cs
new file mode 100644
index 00000000..96c06b2e
--- /dev/null
+++ b/src/System.Web.Mvc/Html/LabelExtensions.cs
@@ -0,0 +1,159 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Linq.Expressions;
+
+namespace System.Web.Mvc.Html
+{
+ public static class LabelExtensions
+ {
+ public static MvcHtmlString Label(this HtmlHelper html, string expression)
+ {
+ return Label(html,
+ expression,
+ labelText: null);
+ }
+
+ public static MvcHtmlString Label(this HtmlHelper html, string expression, string labelText)
+ {
+ return Label(html, expression, labelText, htmlAttributes: null, metadataProvider: null);
+ }
+
+ public static MvcHtmlString Label(this HtmlHelper html, string expression, object htmlAttributes)
+ {
+ return Label(html, expression, labelText: null, htmlAttributes: htmlAttributes, metadataProvider: null);
+ }
+
+ public static MvcHtmlString Label(this HtmlHelper html, string expression, IDictionary<string, object> htmlAttributes)
+ {
+ return Label(html, expression, labelText: null, htmlAttributes: htmlAttributes, metadataProvider: null);
+ }
+
+ public static MvcHtmlString Label(this HtmlHelper html, string expression, string labelText, object htmlAttributes)
+ {
+ return Label(html, expression, labelText, htmlAttributes, metadataProvider: null);
+ }
+
+ public static MvcHtmlString Label(this HtmlHelper html, string expression, string labelText, IDictionary<string, object> htmlAttributes)
+ {
+ return Label(html, expression, labelText, htmlAttributes, metadataProvider: null);
+ }
+
+ internal static MvcHtmlString Label(this HtmlHelper html, string expression, string labelText, object htmlAttributes, ModelMetadataProvider metadataProvider)
+ {
+ return Label(html,
+ expression,
+ labelText,
+ HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes),
+ metadataProvider);
+ }
+
+ internal static MvcHtmlString Label(this HtmlHelper html, string expression, string labelText, IDictionary<string, object> htmlAttributes, ModelMetadataProvider metadataProvider)
+ {
+ return LabelHelper(html,
+ ModelMetadata.FromStringExpression(expression, html.ViewData, metadataProvider),
+ expression,
+ labelText,
+ htmlAttributes);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString LabelFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression)
+ {
+ return LabelFor<TModel, TValue>(html, expression, labelText: null);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString LabelFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, string labelText)
+ {
+ return LabelFor(html, expression, labelText, htmlAttributes: null, metadataProvider: null);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString LabelFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, object htmlAttributes)
+ {
+ return LabelFor(html, expression, labelText: null, htmlAttributes: htmlAttributes, metadataProvider: null);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString LabelFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, IDictionary<string, object> htmlAttributes)
+ {
+ return LabelFor(html, expression, labelText: null, htmlAttributes: htmlAttributes, metadataProvider: null);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString LabelFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, string labelText, object htmlAttributes)
+ {
+ return LabelFor(html, expression, labelText, htmlAttributes, metadataProvider: null);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString LabelFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, string labelText, IDictionary<string, object> htmlAttributes)
+ {
+ return LabelFor(html, expression, labelText, htmlAttributes, metadataProvider: null);
+ }
+
+ internal static MvcHtmlString LabelFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, string labelText, object htmlAttributes, ModelMetadataProvider metadataProvider)
+ {
+ return LabelFor(html,
+ expression,
+ labelText,
+ HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes),
+ metadataProvider);
+ }
+
+ internal static MvcHtmlString LabelFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, string labelText, IDictionary<string, object> htmlAttributes, ModelMetadataProvider metadataProvider)
+ {
+ return LabelHelper(html,
+ ModelMetadata.FromLambdaExpression(expression, html.ViewData, metadataProvider),
+ ExpressionHelper.GetExpressionText(expression),
+ labelText,
+ htmlAttributes);
+ }
+
+ public static MvcHtmlString LabelForModel(this HtmlHelper html)
+ {
+ return LabelForModel(html, labelText: null);
+ }
+
+ public static MvcHtmlString LabelForModel(this HtmlHelper html, string labelText)
+ {
+ return LabelHelper(html, html.ViewData.ModelMetadata, String.Empty, labelText);
+ }
+
+ public static MvcHtmlString LabelForModel(this HtmlHelper html, object htmlAttributes)
+ {
+ return LabelHelper(html, html.ViewData.ModelMetadata, String.Empty, labelText: null, htmlAttributes: HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ public static MvcHtmlString LabelForModel(this HtmlHelper html, IDictionary<string, object> htmlAttributes)
+ {
+ return LabelHelper(html, html.ViewData.ModelMetadata, String.Empty, labelText: null, htmlAttributes: htmlAttributes);
+ }
+
+ public static MvcHtmlString LabelForModel(this HtmlHelper html, string labelText, object htmlAttributes)
+ {
+ return LabelHelper(html, html.ViewData.ModelMetadata, String.Empty, labelText, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ public static MvcHtmlString LabelForModel(this HtmlHelper html, string labelText, IDictionary<string, object> htmlAttributes)
+ {
+ return LabelHelper(html, html.ViewData.ModelMetadata, String.Empty, labelText, htmlAttributes);
+ }
+
+ internal static MvcHtmlString LabelHelper(HtmlHelper html, ModelMetadata metadata, string htmlFieldName, string labelText = null, IDictionary<string, object> htmlAttributes = null)
+ {
+ string resolvedLabelText = labelText ?? metadata.DisplayName ?? metadata.PropertyName ?? htmlFieldName.Split('.').Last();
+ if (String.IsNullOrEmpty(resolvedLabelText))
+ {
+ return MvcHtmlString.Empty;
+ }
+
+ TagBuilder tag = new TagBuilder("label");
+ tag.Attributes.Add("for", TagBuilder.CreateSanitizedId(html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(htmlFieldName)));
+ tag.SetInnerText(resolvedLabelText);
+ tag.MergeAttributes(htmlAttributes, replaceExisting: true);
+ return tag.ToMvcHtmlString(TagRenderMode.Normal);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Html/LinkExtensions.cs b/src/System.Web.Mvc/Html/LinkExtensions.cs
new file mode 100644
index 00000000..cde4318c
--- /dev/null
+++ b/src/System.Web.Mvc/Html/LinkExtensions.cs
@@ -0,0 +1,130 @@
+using System.Collections.Generic;
+using System.Web.Mvc.Properties;
+using System.Web.Routing;
+
+namespace System.Web.Mvc.Html
+{
+ public static class LinkExtensions
+ {
+ public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName)
+ {
+ return ActionLink(htmlHelper, linkText, actionName, null /* controllerName */, new RouteValueDictionary(), new RouteValueDictionary());
+ }
+
+ public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, object routeValues)
+ {
+ return ActionLink(htmlHelper, linkText, actionName, null /* controllerName */, new RouteValueDictionary(routeValues), new RouteValueDictionary());
+ }
+
+ public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, object routeValues, object htmlAttributes)
+ {
+ return ActionLink(htmlHelper, linkText, actionName, null /* controllerName */, new RouteValueDictionary(routeValues), HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, RouteValueDictionary routeValues)
+ {
+ return ActionLink(htmlHelper, linkText, actionName, null /* controllerName */, routeValues, new RouteValueDictionary());
+ }
+
+ public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes)
+ {
+ return ActionLink(htmlHelper, linkText, actionName, null /* controllerName */, routeValues, htmlAttributes);
+ }
+
+ public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName)
+ {
+ return ActionLink(htmlHelper, linkText, actionName, controllerName, new RouteValueDictionary(), new RouteValueDictionary());
+ }
+
+ public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, object routeValues, object htmlAttributes)
+ {
+ return ActionLink(htmlHelper, linkText, actionName, controllerName, new RouteValueDictionary(routeValues), HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes)
+ {
+ if (String.IsNullOrEmpty(linkText))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "linkText");
+ }
+ return MvcHtmlString.Create(HtmlHelper.GenerateLink(htmlHelper.ViewContext.RequestContext, htmlHelper.RouteCollection, linkText, null /* routeName */, actionName, controllerName, routeValues, htmlAttributes));
+ }
+
+ public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, string protocol, string hostName, string fragment, object routeValues, object htmlAttributes)
+ {
+ return ActionLink(htmlHelper, linkText, actionName, controllerName, protocol, hostName, fragment, new RouteValueDictionary(routeValues), HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, string protocol, string hostName, string fragment, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes)
+ {
+ if (String.IsNullOrEmpty(linkText))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "linkText");
+ }
+ return MvcHtmlString.Create(HtmlHelper.GenerateLink(htmlHelper.ViewContext.RequestContext, htmlHelper.RouteCollection, linkText, null /* routeName */, actionName, controllerName, protocol, hostName, fragment, routeValues, htmlAttributes));
+ }
+
+ public static MvcHtmlString RouteLink(this HtmlHelper htmlHelper, string linkText, object routeValues)
+ {
+ return RouteLink(htmlHelper, linkText, new RouteValueDictionary(routeValues));
+ }
+
+ public static MvcHtmlString RouteLink(this HtmlHelper htmlHelper, string linkText, RouteValueDictionary routeValues)
+ {
+ return RouteLink(htmlHelper, linkText, routeValues, new RouteValueDictionary());
+ }
+
+ public static MvcHtmlString RouteLink(this HtmlHelper htmlHelper, string linkText, string routeName)
+ {
+ return RouteLink(htmlHelper, linkText, routeName, (object)null /* routeValues */);
+ }
+
+ public static MvcHtmlString RouteLink(this HtmlHelper htmlHelper, string linkText, string routeName, object routeValues)
+ {
+ return RouteLink(htmlHelper, linkText, routeName, new RouteValueDictionary(routeValues));
+ }
+
+ public static MvcHtmlString RouteLink(this HtmlHelper htmlHelper, string linkText, string routeName, RouteValueDictionary routeValues)
+ {
+ return RouteLink(htmlHelper, linkText, routeName, routeValues, new RouteValueDictionary());
+ }
+
+ public static MvcHtmlString RouteLink(this HtmlHelper htmlHelper, string linkText, object routeValues, object htmlAttributes)
+ {
+ return RouteLink(htmlHelper, linkText, new RouteValueDictionary(routeValues), HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ public static MvcHtmlString RouteLink(this HtmlHelper htmlHelper, string linkText, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes)
+ {
+ return RouteLink(htmlHelper, linkText, null /* routeName */, routeValues, htmlAttributes);
+ }
+
+ public static MvcHtmlString RouteLink(this HtmlHelper htmlHelper, string linkText, string routeName, object routeValues, object htmlAttributes)
+ {
+ return RouteLink(htmlHelper, linkText, routeName, new RouteValueDictionary(routeValues), HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ public static MvcHtmlString RouteLink(this HtmlHelper htmlHelper, string linkText, string routeName, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes)
+ {
+ if (String.IsNullOrEmpty(linkText))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "linkText");
+ }
+ return MvcHtmlString.Create(HtmlHelper.GenerateRouteLink(htmlHelper.ViewContext.RequestContext, htmlHelper.RouteCollection, linkText, routeName, routeValues, htmlAttributes));
+ }
+
+ public static MvcHtmlString RouteLink(this HtmlHelper htmlHelper, string linkText, string routeName, string protocol, string hostName, string fragment, object routeValues, object htmlAttributes)
+ {
+ return RouteLink(htmlHelper, linkText, routeName, protocol, hostName, fragment, new RouteValueDictionary(routeValues), HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ public static MvcHtmlString RouteLink(this HtmlHelper htmlHelper, string linkText, string routeName, string protocol, string hostName, string fragment, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes)
+ {
+ if (String.IsNullOrEmpty(linkText))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "linkText");
+ }
+ return MvcHtmlString.Create(HtmlHelper.GenerateRouteLink(htmlHelper.ViewContext.RequestContext, htmlHelper.RouteCollection, linkText, routeName, protocol, hostName, fragment, routeValues, htmlAttributes));
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Html/MvcForm.cs b/src/System.Web.Mvc/Html/MvcForm.cs
new file mode 100644
index 00000000..1d700468
--- /dev/null
+++ b/src/System.Web.Mvc/Html/MvcForm.cs
@@ -0,0 +1,51 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc.Html
+{
+ public class MvcForm : IDisposable
+ {
+ private readonly ViewContext _viewContext;
+ private bool _disposed;
+
+ [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "httpResponse", Justification = "This method existed in MVC 1.0 and has been deprecated.")]
+ [Obsolete("This constructor is obsolete, because its functionality has been moved to MvcForm(ViewContext) now.", true /* error */)]
+ public MvcForm(HttpResponseBase httpResponse)
+ {
+ throw new InvalidOperationException(MvcResources.MvcForm_ConstructorObsolete);
+ }
+
+ public MvcForm(ViewContext viewContext)
+ {
+ if (viewContext == null)
+ {
+ throw new ArgumentNullException("viewContext");
+ }
+
+ _viewContext = viewContext;
+
+ // push the new FormContext
+ _viewContext.FormContext = new FormContext();
+ }
+
+ public void Dispose()
+ {
+ Dispose(true /* disposing */);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_disposed)
+ {
+ _disposed = true;
+ FormExtensions.EndForm(_viewContext);
+ }
+ }
+
+ public void EndForm()
+ {
+ Dispose(true);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Html/NameExtensions.cs b/src/System.Web.Mvc/Html/NameExtensions.cs
new file mode 100644
index 00000000..9d5f9b16
--- /dev/null
+++ b/src/System.Web.Mvc/Html/NameExtensions.cs
@@ -0,0 +1,43 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Linq.Expressions;
+
+namespace System.Web.Mvc.Html
+{
+ public static class NameExtensions
+ {
+ public static MvcHtmlString Id(this HtmlHelper html, string name)
+ {
+ return MvcHtmlString.Create(html.AttributeEncode(html.ViewData.TemplateInfo.GetFullHtmlFieldId(name)));
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Users cannot use anonymous methods with the LambdaExpression type")]
+ public static MvcHtmlString IdFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression)
+ {
+ return Id(html, ExpressionHelper.GetExpressionText(expression));
+ }
+
+ public static MvcHtmlString IdForModel(this HtmlHelper html)
+ {
+ return Id(html, String.Empty);
+ }
+
+ [SuppressMessage("Microsoft.Naming", "CA1719:ParameterNamesShouldNotMatchMemberNames", MessageId = "1#", Justification = "This is a shipped API.")]
+ public static MvcHtmlString Name(this HtmlHelper html, string name)
+ {
+ return MvcHtmlString.Create(html.AttributeEncode(html.ViewData.TemplateInfo.GetFullHtmlFieldName(name)));
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Users cannot use anonymous methods with the LambdaExpression type")]
+ public static MvcHtmlString NameFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression)
+ {
+ return Name(html, ExpressionHelper.GetExpressionText(expression));
+ }
+
+ public static MvcHtmlString NameForModel(this HtmlHelper html)
+ {
+ return Name(html, String.Empty);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Html/PartialExtensions.cs b/src/System.Web.Mvc/Html/PartialExtensions.cs
new file mode 100644
index 00000000..ee3dcc7d
--- /dev/null
+++ b/src/System.Web.Mvc/Html/PartialExtensions.cs
@@ -0,0 +1,32 @@
+using System.Globalization;
+using System.IO;
+
+namespace System.Web.Mvc.Html
+{
+ public static class PartialExtensions
+ {
+ public static MvcHtmlString Partial(this HtmlHelper htmlHelper, string partialViewName)
+ {
+ return Partial(htmlHelper, partialViewName, null /* model */, htmlHelper.ViewData);
+ }
+
+ public static MvcHtmlString Partial(this HtmlHelper htmlHelper, string partialViewName, ViewDataDictionary viewData)
+ {
+ return Partial(htmlHelper, partialViewName, null /* model */, viewData);
+ }
+
+ public static MvcHtmlString Partial(this HtmlHelper htmlHelper, string partialViewName, object model)
+ {
+ return Partial(htmlHelper, partialViewName, model, htmlHelper.ViewData);
+ }
+
+ public static MvcHtmlString Partial(this HtmlHelper htmlHelper, string partialViewName, object model, ViewDataDictionary viewData)
+ {
+ using (StringWriter writer = new StringWriter(CultureInfo.CurrentCulture))
+ {
+ htmlHelper.RenderPartialInternal(partialViewName, viewData, model, writer, ViewEngines.Engines);
+ return MvcHtmlString.Create(writer.ToString());
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Html/RenderPartialExtensions.cs b/src/System.Web.Mvc/Html/RenderPartialExtensions.cs
new file mode 100644
index 00000000..751c5dd9
--- /dev/null
+++ b/src/System.Web.Mvc/Html/RenderPartialExtensions.cs
@@ -0,0 +1,29 @@
+namespace System.Web.Mvc.Html
+{
+ public static class RenderPartialExtensions
+ {
+ // Renders the partial view with the parent's view data and model
+ public static void RenderPartial(this HtmlHelper htmlHelper, string partialViewName)
+ {
+ htmlHelper.RenderPartialInternal(partialViewName, htmlHelper.ViewData, null /* model */, htmlHelper.ViewContext.Writer, ViewEngines.Engines);
+ }
+
+ // Renders the partial view with the given view data and, implicitly, the given view data's model
+ public static void RenderPartial(this HtmlHelper htmlHelper, string partialViewName, ViewDataDictionary viewData)
+ {
+ htmlHelper.RenderPartialInternal(partialViewName, viewData, null /* model */, htmlHelper.ViewContext.Writer, ViewEngines.Engines);
+ }
+
+ // Renders the partial view with an empty view data and the given model
+ public static void RenderPartial(this HtmlHelper htmlHelper, string partialViewName, object model)
+ {
+ htmlHelper.RenderPartialInternal(partialViewName, htmlHelper.ViewData, model, htmlHelper.ViewContext.Writer, ViewEngines.Engines);
+ }
+
+ // Renders the partial view with a copy of the given view data plus the given model
+ public static void RenderPartial(this HtmlHelper htmlHelper, string partialViewName, object model, ViewDataDictionary viewData)
+ {
+ htmlHelper.RenderPartialInternal(partialViewName, viewData, model, htmlHelper.ViewContext.Writer, ViewEngines.Engines);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Html/SelectExtensions.cs b/src/System.Web.Mvc/Html/SelectExtensions.cs
new file mode 100644
index 00000000..98a93f47
--- /dev/null
+++ b/src/System.Web.Mvc/Html/SelectExtensions.cs
@@ -0,0 +1,317 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Text;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc.Html
+{
+ public static class SelectExtensions
+ {
+ // DropDownList
+
+ public static MvcHtmlString DropDownList(this HtmlHelper htmlHelper, string name)
+ {
+ return DropDownList(htmlHelper, name, null /* selectList */, null /* optionLabel */, null /* htmlAttributes */);
+ }
+
+ public static MvcHtmlString DropDownList(this HtmlHelper htmlHelper, string name, string optionLabel)
+ {
+ return DropDownList(htmlHelper, name, null /* selectList */, optionLabel, null /* htmlAttributes */);
+ }
+
+ public static MvcHtmlString DropDownList(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList)
+ {
+ return DropDownList(htmlHelper, name, selectList, null /* optionLabel */, null /* htmlAttributes */);
+ }
+
+ public static MvcHtmlString DropDownList(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, object htmlAttributes)
+ {
+ return DropDownList(htmlHelper, name, selectList, null /* optionLabel */, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ public static MvcHtmlString DropDownList(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, IDictionary<string, object> htmlAttributes)
+ {
+ return DropDownList(htmlHelper, name, selectList, null /* optionLabel */, htmlAttributes);
+ }
+
+ public static MvcHtmlString DropDownList(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, string optionLabel)
+ {
+ return DropDownList(htmlHelper, name, selectList, optionLabel, null /* htmlAttributes */);
+ }
+
+ public static MvcHtmlString DropDownList(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, string optionLabel, object htmlAttributes)
+ {
+ return DropDownList(htmlHelper, name, selectList, optionLabel, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ public static MvcHtmlString DropDownList(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, string optionLabel, IDictionary<string, object> htmlAttributes)
+ {
+ return DropDownListHelper(htmlHelper, metadata: null, expression: name, selectList: selectList, optionLabel: optionLabel, htmlAttributes: htmlAttributes);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString DropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList)
+ {
+ return DropDownListFor(htmlHelper, expression, selectList, null /* optionLabel */, null /* htmlAttributes */);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString DropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, object htmlAttributes)
+ {
+ return DropDownListFor(htmlHelper, expression, selectList, null /* optionLabel */, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString DropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, IDictionary<string, object> htmlAttributes)
+ {
+ return DropDownListFor(htmlHelper, expression, selectList, null /* optionLabel */, htmlAttributes);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString DropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, string optionLabel)
+ {
+ return DropDownListFor(htmlHelper, expression, selectList, optionLabel, null /* htmlAttributes */);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString DropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, string optionLabel, object htmlAttributes)
+ {
+ return DropDownListFor(htmlHelper, expression, selectList, optionLabel, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Users cannot use anonymous methods with the LambdaExpression type")]
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString DropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, string optionLabel, IDictionary<string, object> htmlAttributes)
+ {
+ if (expression == null)
+ {
+ throw new ArgumentNullException("expression");
+ }
+
+ ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
+
+ return DropDownListHelper(htmlHelper, metadata, ExpressionHelper.GetExpressionText(expression), selectList, optionLabel, htmlAttributes);
+ }
+
+ private static MvcHtmlString DropDownListHelper(HtmlHelper htmlHelper, ModelMetadata metadata, string expression, IEnumerable<SelectListItem> selectList, string optionLabel, IDictionary<string, object> htmlAttributes)
+ {
+ return SelectInternal(htmlHelper, metadata, optionLabel, expression, selectList, allowMultiple: false, htmlAttributes: htmlAttributes);
+ }
+
+ // ListBox
+
+ public static MvcHtmlString ListBox(this HtmlHelper htmlHelper, string name)
+ {
+ return ListBox(htmlHelper, name, null /* selectList */, null /* htmlAttributes */);
+ }
+
+ public static MvcHtmlString ListBox(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList)
+ {
+ return ListBox(htmlHelper, name, selectList, (IDictionary<string, object>)null);
+ }
+
+ public static MvcHtmlString ListBox(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, object htmlAttributes)
+ {
+ return ListBox(htmlHelper, name, selectList, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ public static MvcHtmlString ListBox(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, IDictionary<string, object> htmlAttributes)
+ {
+ return ListBoxHelper(htmlHelper, metadata: null, name: name, selectList: selectList, htmlAttributes: htmlAttributes);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString ListBoxFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList)
+ {
+ return ListBoxFor(htmlHelper, expression, selectList, null /* htmlAttributes */);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString ListBoxFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, object htmlAttributes)
+ {
+ return ListBoxFor(htmlHelper, expression, selectList, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Users cannot use anonymous methods with the LambdaExpression type")]
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString ListBoxFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, IDictionary<string, object> htmlAttributes)
+ {
+ if (expression == null)
+ {
+ throw new ArgumentNullException("expression");
+ }
+
+ ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
+
+ return ListBoxHelper(htmlHelper,
+ metadata,
+ ExpressionHelper.GetExpressionText(expression),
+ selectList,
+ htmlAttributes);
+ }
+
+ private static MvcHtmlString ListBoxHelper(HtmlHelper htmlHelper, ModelMetadata metadata, string name, IEnumerable<SelectListItem> selectList, IDictionary<string, object> htmlAttributes)
+ {
+ return SelectInternal(htmlHelper, metadata, optionLabel: null, name: name, selectList: selectList, allowMultiple: true, htmlAttributes: htmlAttributes);
+ }
+
+ // Helper methods
+
+ private static IEnumerable<SelectListItem> GetSelectData(this HtmlHelper htmlHelper, string name)
+ {
+ object o = null;
+ if (htmlHelper.ViewData != null)
+ {
+ o = htmlHelper.ViewData.Eval(name);
+ }
+ if (o == null)
+ {
+ throw new InvalidOperationException(
+ String.Format(
+ CultureInfo.CurrentCulture,
+ MvcResources.HtmlHelper_MissingSelectData,
+ name,
+ "IEnumerable<SelectListItem>"));
+ }
+ IEnumerable<SelectListItem> selectList = o as IEnumerable<SelectListItem>;
+ if (selectList == null)
+ {
+ throw new InvalidOperationException(
+ String.Format(
+ CultureInfo.CurrentCulture,
+ MvcResources.HtmlHelper_WrongSelectDataType,
+ name,
+ o.GetType().FullName,
+ "IEnumerable<SelectListItem>"));
+ }
+ return selectList;
+ }
+
+ internal static string ListItemToOption(SelectListItem item)
+ {
+ TagBuilder builder = new TagBuilder("option")
+ {
+ InnerHtml = HttpUtility.HtmlEncode(item.Text)
+ };
+ if (item.Value != null)
+ {
+ builder.Attributes["value"] = item.Value;
+ }
+ if (item.Selected)
+ {
+ builder.Attributes["selected"] = "selected";
+ }
+ return builder.ToString(TagRenderMode.Normal);
+ }
+
+ private static IEnumerable<SelectListItem> GetSelectListWithDefaultValue(IEnumerable<SelectListItem> selectList, object defaultValue, bool allowMultiple)
+ {
+ IEnumerable defaultValues;
+
+ if (allowMultiple)
+ {
+ defaultValues = defaultValue as IEnumerable;
+ if (defaultValues == null || defaultValues is string)
+ {
+ throw new InvalidOperationException(
+ String.Format(
+ CultureInfo.CurrentCulture,
+ MvcResources.HtmlHelper_SelectExpressionNotEnumerable,
+ "expression"));
+ }
+ }
+ else
+ {
+ defaultValues = new[] { defaultValue };
+ }
+
+ IEnumerable<string> values = from object value in defaultValues
+ select Convert.ToString(value, CultureInfo.CurrentCulture);
+ HashSet<string> selectedValues = new HashSet<string>(values, StringComparer.OrdinalIgnoreCase);
+ List<SelectListItem> newSelectList = new List<SelectListItem>();
+
+ foreach (SelectListItem item in selectList)
+ {
+ item.Selected = (item.Value != null) ? selectedValues.Contains(item.Value) : selectedValues.Contains(item.Text);
+ newSelectList.Add(item);
+ }
+ return newSelectList;
+ }
+
+ private static MvcHtmlString SelectInternal(this HtmlHelper htmlHelper, ModelMetadata metadata, string optionLabel, string name, IEnumerable<SelectListItem> selectList, bool allowMultiple, IDictionary<string, object> htmlAttributes)
+ {
+ string fullName = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);
+ if (String.IsNullOrEmpty(fullName))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "name");
+ }
+
+ bool usedViewData = false;
+
+ // If we got a null selectList, try to use ViewData to get the list of items.
+ if (selectList == null)
+ {
+ selectList = htmlHelper.GetSelectData(name);
+ usedViewData = true;
+ }
+
+ object defaultValue = (allowMultiple) ? htmlHelper.GetModelStateValue(fullName, typeof(string[])) : htmlHelper.GetModelStateValue(fullName, typeof(string));
+
+ // If we haven't already used ViewData to get the entire list of items then we need to
+ // use the ViewData-supplied value before using the parameter-supplied value.
+ if (!usedViewData && defaultValue == null && !String.IsNullOrEmpty(name))
+ {
+ defaultValue = htmlHelper.ViewData.Eval(name);
+ }
+
+ if (defaultValue != null)
+ {
+ selectList = GetSelectListWithDefaultValue(selectList, defaultValue, allowMultiple);
+ }
+
+ // Convert each ListItem to an <option> tag
+ StringBuilder listItemBuilder = new StringBuilder();
+
+ // Make optionLabel the first item that gets rendered.
+ if (optionLabel != null)
+ {
+ listItemBuilder.AppendLine(ListItemToOption(new SelectListItem() { Text = optionLabel, Value = String.Empty, Selected = false }));
+ }
+
+ foreach (SelectListItem item in selectList)
+ {
+ listItemBuilder.AppendLine(ListItemToOption(item));
+ }
+
+ TagBuilder tagBuilder = new TagBuilder("select")
+ {
+ InnerHtml = listItemBuilder.ToString()
+ };
+ tagBuilder.MergeAttributes(htmlAttributes);
+ tagBuilder.MergeAttribute("name", fullName, true /* replaceExisting */);
+ tagBuilder.GenerateId(fullName);
+ if (allowMultiple)
+ {
+ tagBuilder.MergeAttribute("multiple", "multiple");
+ }
+
+ // If there are any errors for a named field, we add the css attribute.
+ ModelState modelState;
+ if (htmlHelper.ViewData.ModelState.TryGetValue(fullName, out modelState))
+ {
+ if (modelState.Errors.Count > 0)
+ {
+ tagBuilder.AddCssClass(HtmlHelper.ValidationInputCssClassName);
+ }
+ }
+
+ tagBuilder.MergeAttributes(htmlHelper.GetUnobtrusiveValidationAttributes(name, metadata));
+
+ return tagBuilder.ToMvcHtmlString(TagRenderMode.Normal);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Html/TemplateHelpers.cs b/src/System.Web.Mvc/Html/TemplateHelpers.cs
new file mode 100644
index 00000000..26290b8c
--- /dev/null
+++ b/src/System.Web.Mvc/Html/TemplateHelpers.cs
@@ -0,0 +1,333 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Web.Mvc.Properties;
+using System.Web.Routing;
+using System.Web.UI.WebControls;
+
+namespace System.Web.Mvc.Html
+{
+ internal static class TemplateHelpers
+ {
+ private static readonly Dictionary<DataBoundControlMode, string> _modeViewPaths =
+ new Dictionary<DataBoundControlMode, string>
+ {
+ { DataBoundControlMode.ReadOnly, "DisplayTemplates" },
+ { DataBoundControlMode.Edit, "EditorTemplates" }
+ };
+
+ private static readonly Dictionary<string, Func<HtmlHelper, string>> _defaultDisplayActions =
+ new Dictionary<string, Func<HtmlHelper, string>>(StringComparer.OrdinalIgnoreCase)
+ {
+ { "EmailAddress", DefaultDisplayTemplates.EmailAddressTemplate },
+ { "HiddenInput", DefaultDisplayTemplates.HiddenInputTemplate },
+ { "Html", DefaultDisplayTemplates.HtmlTemplate },
+ { "Text", DefaultDisplayTemplates.StringTemplate },
+ { "Url", DefaultDisplayTemplates.UrlTemplate },
+ { "Collection", DefaultDisplayTemplates.CollectionTemplate },
+ { typeof(bool).Name, DefaultDisplayTemplates.BooleanTemplate },
+ { typeof(decimal).Name, DefaultDisplayTemplates.DecimalTemplate },
+ { typeof(string).Name, DefaultDisplayTemplates.StringTemplate },
+ { typeof(object).Name, DefaultDisplayTemplates.ObjectTemplate },
+ };
+
+ private static readonly Dictionary<string, Func<HtmlHelper, string>> _defaultEditorActions =
+ new Dictionary<string, Func<HtmlHelper, string>>(StringComparer.OrdinalIgnoreCase)
+ {
+ { "HiddenInput", DefaultEditorTemplates.HiddenInputTemplate },
+ { "MultilineText", DefaultEditorTemplates.MultilineTextTemplate },
+ { "Password", DefaultEditorTemplates.PasswordTemplate },
+ { "Text", DefaultEditorTemplates.StringTemplate },
+ { "Collection", DefaultEditorTemplates.CollectionTemplate },
+ { typeof(bool).Name, DefaultEditorTemplates.BooleanTemplate },
+ { typeof(decimal).Name, DefaultEditorTemplates.DecimalTemplate },
+ { typeof(string).Name, DefaultEditorTemplates.StringTemplate },
+ { typeof(object).Name, DefaultEditorTemplates.ObjectTemplate },
+ };
+
+ internal static string CacheItemId = Guid.NewGuid().ToString();
+
+ internal delegate string ExecuteTemplateDelegate(HtmlHelper html, ViewDataDictionary viewData, string templateName,
+ DataBoundControlMode mode, GetViewNamesDelegate getViewNames,
+ GetDefaultActionsDelegate getDefaultActions);
+
+ internal delegate Dictionary<string, Func<HtmlHelper, string>> GetDefaultActionsDelegate(DataBoundControlMode mode);
+
+ internal delegate IEnumerable<string> GetViewNamesDelegate(ModelMetadata metadata, params string[] templateHints);
+
+ internal delegate string TemplateHelperDelegate(HtmlHelper html, ModelMetadata metadata, string htmlFieldName,
+ string templateName, DataBoundControlMode mode, object additionalViewData);
+
+ internal static string ExecuteTemplate(HtmlHelper html, ViewDataDictionary viewData, string templateName, DataBoundControlMode mode, GetViewNamesDelegate getViewNames, GetDefaultActionsDelegate getDefaultActions)
+ {
+ Dictionary<string, ActionCacheItem> actionCache = GetActionCache(html);
+ Dictionary<string, Func<HtmlHelper, string>> defaultActions = getDefaultActions(mode);
+ string modeViewPath = _modeViewPaths[mode];
+
+ foreach (string viewName in getViewNames(viewData.ModelMetadata, templateName, viewData.ModelMetadata.TemplateHint, viewData.ModelMetadata.DataTypeName))
+ {
+ string fullViewName = modeViewPath + "/" + viewName;
+ ActionCacheItem cacheItem;
+
+ if (actionCache.TryGetValue(fullViewName, out cacheItem))
+ {
+ if (cacheItem != null)
+ {
+ return cacheItem.Execute(html, viewData);
+ }
+ }
+ else
+ {
+ ViewEngineResult viewEngineResult = ViewEngines.Engines.FindPartialView(html.ViewContext, fullViewName);
+ if (viewEngineResult.View != null)
+ {
+ actionCache[fullViewName] = new ActionCacheViewItem { ViewName = fullViewName };
+
+ using (StringWriter writer = new StringWriter(CultureInfo.InvariantCulture))
+ {
+ viewEngineResult.View.Render(new ViewContext(html.ViewContext, viewEngineResult.View, viewData, html.ViewContext.TempData, writer), writer);
+ return writer.ToString();
+ }
+ }
+
+ Func<HtmlHelper, string> defaultAction;
+ if (defaultActions.TryGetValue(viewName, out defaultAction))
+ {
+ actionCache[fullViewName] = new ActionCacheCodeItem { Action = defaultAction };
+ return defaultAction(MakeHtmlHelper(html, viewData));
+ }
+
+ actionCache[fullViewName] = null;
+ }
+ }
+
+ throw new InvalidOperationException(
+ String.Format(
+ CultureInfo.CurrentCulture,
+ MvcResources.TemplateHelpers_NoTemplate,
+ viewData.ModelMetadata.RealModelType.FullName));
+ }
+
+ internal static Dictionary<string, ActionCacheItem> GetActionCache(HtmlHelper html)
+ {
+ HttpContextBase context = html.ViewContext.HttpContext;
+ Dictionary<string, ActionCacheItem> result;
+
+ if (!context.Items.Contains(CacheItemId))
+ {
+ result = new Dictionary<string, ActionCacheItem>();
+ context.Items[CacheItemId] = result;
+ }
+ else
+ {
+ result = (Dictionary<string, ActionCacheItem>)context.Items[CacheItemId];
+ }
+
+ return result;
+ }
+
+ internal static Dictionary<string, Func<HtmlHelper, string>> GetDefaultActions(DataBoundControlMode mode)
+ {
+ return mode == DataBoundControlMode.ReadOnly ? _defaultDisplayActions : _defaultEditorActions;
+ }
+
+ internal static IEnumerable<string> GetViewNames(ModelMetadata metadata, params string[] templateHints)
+ {
+ foreach (string templateHint in templateHints.Where(s => !String.IsNullOrEmpty(s)))
+ {
+ yield return templateHint;
+ }
+
+ // We don't want to search for Nullable<T>, we want to search for T (which should handle both T and Nullable<T>)
+ Type fieldType = Nullable.GetUnderlyingType(metadata.RealModelType) ?? metadata.RealModelType;
+
+ // TODO: Make better string names for generic types
+ yield return fieldType.Name;
+
+ if (!metadata.IsComplexType)
+ {
+ yield return "String";
+ }
+ else if (fieldType.IsInterface)
+ {
+ if (typeof(IEnumerable).IsAssignableFrom(fieldType))
+ {
+ yield return "Collection";
+ }
+
+ yield return "Object";
+ }
+ else
+ {
+ bool isEnumerable = typeof(IEnumerable).IsAssignableFrom(fieldType);
+
+ while (true)
+ {
+ fieldType = fieldType.BaseType;
+ if (fieldType == null)
+ {
+ break;
+ }
+
+ if (isEnumerable && fieldType == typeof(Object))
+ {
+ yield return "Collection";
+ }
+
+ yield return fieldType.Name;
+ }
+ }
+ }
+
+ internal static MvcHtmlString Template(HtmlHelper html, string expression, string templateName, string htmlFieldName, DataBoundControlMode mode, object additionalViewData)
+ {
+ return MvcHtmlString.Create(Template(html, expression, templateName, htmlFieldName, mode, additionalViewData, TemplateHelper));
+ }
+
+ // Unit testing version
+ internal static string Template(HtmlHelper html, string expression, string templateName, string htmlFieldName,
+ DataBoundControlMode mode, object additionalViewData, TemplateHelperDelegate templateHelper)
+ {
+ return templateHelper(html,
+ ModelMetadata.FromStringExpression(expression, html.ViewData),
+ htmlFieldName ?? ExpressionHelper.GetExpressionText(expression),
+ templateName,
+ mode,
+ additionalViewData);
+ }
+
+ internal static MvcHtmlString TemplateFor<TContainer, TValue>(this HtmlHelper<TContainer> html, Expression<Func<TContainer, TValue>> expression,
+ string templateName, string htmlFieldName, DataBoundControlMode mode,
+ object additionalViewData)
+ {
+ return MvcHtmlString.Create(TemplateFor(html, expression, templateName, htmlFieldName, mode, additionalViewData, TemplateHelper));
+ }
+
+ // Unit testing version
+ internal static string TemplateFor<TContainer, TValue>(this HtmlHelper<TContainer> html, Expression<Func<TContainer, TValue>> expression,
+ string templateName, string htmlFieldName, DataBoundControlMode mode,
+ object additionalViewData, TemplateHelperDelegate templateHelper)
+ {
+ return templateHelper(html,
+ ModelMetadata.FromLambdaExpression(expression, html.ViewData),
+ htmlFieldName ?? ExpressionHelper.GetExpressionText(expression),
+ templateName,
+ mode,
+ additionalViewData);
+ }
+
+ internal static string TemplateHelper(HtmlHelper html, ModelMetadata metadata, string htmlFieldName, string templateName, DataBoundControlMode mode, object additionalViewData)
+ {
+ return TemplateHelper(html, metadata, htmlFieldName, templateName, mode, additionalViewData, ExecuteTemplate);
+ }
+
+ internal static string TemplateHelper(HtmlHelper html, ModelMetadata metadata, string htmlFieldName, string templateName, DataBoundControlMode mode, object additionalViewData, ExecuteTemplateDelegate executeTemplate)
+ {
+ // TODO: Convert Editor into Display if model.IsReadOnly is true? Need to be careful about this because
+ // the Model property on the ViewPage/ViewUserControl is get-only, so the type descriptor automatically
+ // decorates it with a [ReadOnly] attribute...
+
+ if (metadata.ConvertEmptyStringToNull && String.Empty.Equals(metadata.Model))
+ {
+ metadata.Model = null;
+ }
+
+ object formattedModelValue = metadata.Model;
+ if (metadata.Model == null && mode == DataBoundControlMode.ReadOnly)
+ {
+ formattedModelValue = metadata.NullDisplayText;
+ }
+
+ string formatString = mode == DataBoundControlMode.ReadOnly ? metadata.DisplayFormatString : metadata.EditFormatString;
+ if (metadata.Model != null && !String.IsNullOrEmpty(formatString))
+ {
+ formattedModelValue = String.Format(CultureInfo.CurrentCulture, formatString, metadata.Model);
+ }
+
+ // Normally this shouldn't happen, unless someone writes their own custom Object templates which
+ // don't check to make sure that the object hasn't already been displayed
+ object visitedObjectsKey = metadata.Model ?? metadata.RealModelType;
+ if (html.ViewDataContainer.ViewData.TemplateInfo.VisitedObjects.Contains(visitedObjectsKey))
+ {
+ // DDB #224750
+ return String.Empty;
+ }
+
+ ViewDataDictionary viewData = new ViewDataDictionary(html.ViewDataContainer.ViewData)
+ {
+ Model = metadata.Model,
+ ModelMetadata = metadata,
+ TemplateInfo = new TemplateInfo
+ {
+ FormattedModelValue = formattedModelValue,
+ HtmlFieldPrefix = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(htmlFieldName),
+ VisitedObjects = new HashSet<object>(html.ViewContext.ViewData.TemplateInfo.VisitedObjects), // DDB #224750
+ }
+ };
+
+ if (additionalViewData != null)
+ {
+ foreach (KeyValuePair<string, object> kvp in new RouteValueDictionary(additionalViewData))
+ {
+ viewData[kvp.Key] = kvp.Value;
+ }
+ }
+
+ viewData.TemplateInfo.VisitedObjects.Add(visitedObjectsKey); // DDB #224750
+
+ return executeTemplate(html, viewData, templateName, mode, GetViewNames, GetDefaultActions);
+ }
+
+ // Helpers
+
+ private static HtmlHelper MakeHtmlHelper(HtmlHelper html, ViewDataDictionary viewData)
+ {
+ return new HtmlHelper(
+ new ViewContext(html.ViewContext, html.ViewContext.View, viewData, html.ViewContext.TempData, html.ViewContext.Writer),
+ new ViewDataContainer(viewData));
+ }
+
+ internal class ActionCacheCodeItem : ActionCacheItem
+ {
+ public Func<HtmlHelper, string> Action { get; set; }
+
+ public override string Execute(HtmlHelper html, ViewDataDictionary viewData)
+ {
+ return Action(MakeHtmlHelper(html, viewData));
+ }
+ }
+
+ internal abstract class ActionCacheItem
+ {
+ public abstract string Execute(HtmlHelper html, ViewDataDictionary viewData);
+ }
+
+ internal class ActionCacheViewItem : ActionCacheItem
+ {
+ public string ViewName { get; set; }
+
+ public override string Execute(HtmlHelper html, ViewDataDictionary viewData)
+ {
+ ViewEngineResult viewEngineResult = ViewEngines.Engines.FindPartialView(html.ViewContext, ViewName);
+ using (StringWriter writer = new StringWriter(CultureInfo.InvariantCulture))
+ {
+ viewEngineResult.View.Render(new ViewContext(html.ViewContext, viewEngineResult.View, viewData, html.ViewContext.TempData, writer), writer);
+ return writer.ToString();
+ }
+ }
+ }
+
+ private class ViewDataContainer : IViewDataContainer
+ {
+ public ViewDataContainer(ViewDataDictionary viewData)
+ {
+ ViewData = viewData;
+ }
+
+ public ViewDataDictionary ViewData { get; set; }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Html/TextAreaExtensions.cs b/src/System.Web.Mvc/Html/TextAreaExtensions.cs
new file mode 100644
index 00000000..490a76e7
--- /dev/null
+++ b/src/System.Web.Mvc/Html/TextAreaExtensions.cs
@@ -0,0 +1,192 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq.Expressions;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc.Html
+{
+ public static class TextAreaExtensions
+ {
+ // These values are similar to the defaults used by WebForms
+ // when using <asp:TextBox TextMode="MultiLine"> without specifying
+ // the Rows and Columns attributes.
+ private const int TextAreaRows = 2;
+ private const int TextAreaColumns = 20;
+
+ private static Dictionary<string, object> implicitRowsAndColumns = new Dictionary<string, object>
+ {
+ { "rows", TextAreaRows.ToString(CultureInfo.InvariantCulture) },
+ { "cols", TextAreaColumns.ToString(CultureInfo.InvariantCulture) },
+ };
+
+ private static Dictionary<string, object> GetRowsAndColumnsDictionary(int rows, int columns)
+ {
+ if (rows < 0)
+ {
+ throw new ArgumentOutOfRangeException("rows", MvcResources.HtmlHelper_TextAreaParameterOutOfRange);
+ }
+ if (columns < 0)
+ {
+ throw new ArgumentOutOfRangeException("columns", MvcResources.HtmlHelper_TextAreaParameterOutOfRange);
+ }
+
+ Dictionary<string, object> result = new Dictionary<string, object>();
+ if (rows > 0)
+ {
+ result.Add("rows", rows.ToString(CultureInfo.InvariantCulture));
+ }
+ if (columns > 0)
+ {
+ result.Add("cols", columns.ToString(CultureInfo.InvariantCulture));
+ }
+
+ return result;
+ }
+
+ public static MvcHtmlString TextArea(this HtmlHelper htmlHelper, string name)
+ {
+ return TextArea(htmlHelper, name, null /* value */, null /* htmlAttributes */);
+ }
+
+ public static MvcHtmlString TextArea(this HtmlHelper htmlHelper, string name, object htmlAttributes)
+ {
+ return TextArea(htmlHelper, name, null /* value */, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ public static MvcHtmlString TextArea(this HtmlHelper htmlHelper, string name, IDictionary<string, object> htmlAttributes)
+ {
+ return TextArea(htmlHelper, name, null /* value */, htmlAttributes);
+ }
+
+ public static MvcHtmlString TextArea(this HtmlHelper htmlHelper, string name, string value)
+ {
+ return TextArea(htmlHelper, name, value, null /* htmlAttributes */);
+ }
+
+ public static MvcHtmlString TextArea(this HtmlHelper htmlHelper, string name, string value, object htmlAttributes)
+ {
+ return TextArea(htmlHelper, name, value, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ public static MvcHtmlString TextArea(this HtmlHelper htmlHelper, string name, string value, IDictionary<string, object> htmlAttributes)
+ {
+ ModelMetadata metadata = ModelMetadata.FromStringExpression(name, htmlHelper.ViewContext.ViewData);
+ if (value != null)
+ {
+ metadata.Model = value;
+ }
+
+ return TextAreaHelper(htmlHelper, metadata, name, implicitRowsAndColumns, htmlAttributes);
+ }
+
+ public static MvcHtmlString TextArea(this HtmlHelper htmlHelper, string name, string value, int rows, int columns, object htmlAttributes)
+ {
+ return TextArea(htmlHelper, name, value, rows, columns, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ public static MvcHtmlString TextArea(this HtmlHelper htmlHelper, string name, string value, int rows, int columns, IDictionary<string, object> htmlAttributes)
+ {
+ ModelMetadata metadata = ModelMetadata.FromStringExpression(name, htmlHelper.ViewContext.ViewData);
+ if (value != null)
+ {
+ metadata.Model = value;
+ }
+
+ return TextAreaHelper(htmlHelper, metadata, name, GetRowsAndColumnsDictionary(rows, columns), htmlAttributes);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString TextAreaFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression)
+ {
+ return TextAreaFor(htmlHelper, expression, (IDictionary<string, object>)null);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString TextAreaFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, object htmlAttributes)
+ {
+ return TextAreaFor(htmlHelper, expression, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString TextAreaFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IDictionary<string, object> htmlAttributes)
+ {
+ if (expression == null)
+ {
+ throw new ArgumentNullException("expression");
+ }
+
+ return TextAreaHelper(htmlHelper,
+ ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData),
+ ExpressionHelper.GetExpressionText(expression),
+ implicitRowsAndColumns,
+ htmlAttributes);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString TextAreaFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, int rows, int columns, object htmlAttributes)
+ {
+ return TextAreaFor(htmlHelper, expression, rows, columns, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString TextAreaFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, int rows, int columns, IDictionary<string, object> htmlAttributes)
+ {
+ if (expression == null)
+ {
+ throw new ArgumentNullException("expression");
+ }
+
+ return TextAreaHelper(htmlHelper,
+ ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData),
+ ExpressionHelper.GetExpressionText(expression),
+ GetRowsAndColumnsDictionary(rows, columns),
+ htmlAttributes);
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Justification = "If this fails, it is because the string-based version had an empty 'name' parameter")]
+ internal static MvcHtmlString TextAreaHelper(HtmlHelper htmlHelper, ModelMetadata modelMetadata, string name, IDictionary<string, object> rowsAndColumns, IDictionary<string, object> htmlAttributes, string innerHtmlPrefix = null)
+ {
+ string fullName = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);
+ if (String.IsNullOrEmpty(fullName))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "name");
+ }
+
+ TagBuilder tagBuilder = new TagBuilder("textarea");
+ tagBuilder.GenerateId(fullName);
+ tagBuilder.MergeAttributes(htmlAttributes, true);
+ tagBuilder.MergeAttributes(rowsAndColumns, rowsAndColumns != implicitRowsAndColumns); // Only force explicit rows/cols
+ tagBuilder.MergeAttribute("name", fullName, true);
+
+ // If there are any errors for a named field, we add the CSS attribute.
+ ModelState modelState;
+ if (htmlHelper.ViewData.ModelState.TryGetValue(fullName, out modelState) && modelState.Errors.Count > 0)
+ {
+ tagBuilder.AddCssClass(HtmlHelper.ValidationInputCssClassName);
+ }
+
+ tagBuilder.MergeAttributes(htmlHelper.GetUnobtrusiveValidationAttributes(name));
+
+ string value;
+ if (modelState != null && modelState.Value != null)
+ {
+ value = modelState.Value.AttemptedValue;
+ }
+ else if (modelMetadata.Model != null)
+ {
+ value = modelMetadata.Model.ToString();
+ }
+ else
+ {
+ value = String.Empty;
+ }
+
+ // The first newline is always trimmed when a TextArea is rendered, so we add an extra one
+ // in case the value being rendered is something like "\r\nHello".
+ tagBuilder.InnerHtml = (innerHtmlPrefix ?? Environment.NewLine) + HttpUtility.HtmlEncode(value);
+
+ return tagBuilder.ToMvcHtmlString(TagRenderMode.Normal);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Html/ValidationExtensions.cs b/src/System.Web.Mvc/Html/ValidationExtensions.cs
new file mode 100644
index 00000000..b225110e
--- /dev/null
+++ b/src/System.Web.Mvc/Html/ValidationExtensions.cs
@@ -0,0 +1,390 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Text;
+using System.Web.Mvc.Properties;
+using System.Web.Routing;
+
+namespace System.Web.Mvc.Html
+{
+ public static class ValidationExtensions
+ {
+ private const string HiddenListItem = @"<li style=""display:none""></li>";
+ private static string _resourceClassKey;
+
+ public static string ResourceClassKey
+ {
+ get { return _resourceClassKey ?? String.Empty; }
+ set { _resourceClassKey = value; }
+ }
+
+ private static FieldValidationMetadata ApplyFieldValidationMetadata(HtmlHelper htmlHelper, ModelMetadata modelMetadata, string modelName)
+ {
+ FormContext formContext = htmlHelper.ViewContext.FormContext;
+ FieldValidationMetadata fieldMetadata = formContext.GetValidationMetadataForField(modelName, true /* createIfNotFound */);
+
+ // write rules to context object
+ IEnumerable<ModelValidator> validators = ModelValidatorProviders.Providers.GetValidators(modelMetadata, htmlHelper.ViewContext);
+ foreach (ModelClientValidationRule rule in validators.SelectMany(v => v.GetClientValidationRules()))
+ {
+ fieldMetadata.ValidationRules.Add(rule);
+ }
+
+ return fieldMetadata;
+ }
+
+ private static string GetInvalidPropertyValueResource(HttpContextBase httpContext)
+ {
+ string resourceValue = null;
+ if (!String.IsNullOrEmpty(ResourceClassKey) && (httpContext != null))
+ {
+ // If the user specified a ResourceClassKey try to load the resource they specified.
+ // If the class key is invalid, an exception will be thrown.
+ // If the class key is valid but the resource is not found, it returns null, in which
+ // case it will fall back to the MVC default error message.
+ resourceValue = httpContext.GetGlobalResourceObject(ResourceClassKey, "InvalidPropertyValue", CultureInfo.CurrentUICulture) as string;
+ }
+ return resourceValue ?? MvcResources.Common_ValueNotValidForProperty;
+ }
+
+ private static string GetUserErrorMessageOrDefault(HttpContextBase httpContext, ModelError error, ModelState modelState)
+ {
+ if (!String.IsNullOrEmpty(error.ErrorMessage))
+ {
+ return error.ErrorMessage;
+ }
+ if (modelState == null)
+ {
+ return null;
+ }
+
+ string attemptedValue = (modelState.Value != null) ? modelState.Value.AttemptedValue : null;
+ return String.Format(CultureInfo.CurrentCulture, GetInvalidPropertyValueResource(httpContext), attemptedValue);
+ }
+
+ // Validate
+
+ public static void Validate(this HtmlHelper htmlHelper, string modelName)
+ {
+ if (modelName == null)
+ {
+ throw new ArgumentNullException("modelName");
+ }
+
+ ValidateHelper(htmlHelper,
+ ModelMetadata.FromStringExpression(modelName, htmlHelper.ViewContext.ViewData),
+ modelName);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static void ValidateFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression)
+ {
+ ValidateHelper(htmlHelper,
+ ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData),
+ ExpressionHelper.GetExpressionText(expression));
+ }
+
+ private static void ValidateHelper(HtmlHelper htmlHelper, ModelMetadata modelMetadata, string expression)
+ {
+ FormContext formContext = htmlHelper.ViewContext.GetFormContextForClientValidation();
+ if (formContext == null || htmlHelper.ViewContext.UnobtrusiveJavaScriptEnabled)
+ {
+ return; // nothing to do
+ }
+
+ string modelName = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(expression);
+ ApplyFieldValidationMetadata(htmlHelper, modelMetadata, modelName);
+ }
+
+ // ValidationMessage
+
+ public static MvcHtmlString ValidationMessage(this HtmlHelper htmlHelper, string modelName)
+ {
+ return ValidationMessage(htmlHelper, modelName, null /* validationMessage */, new RouteValueDictionary());
+ }
+
+ public static MvcHtmlString ValidationMessage(this HtmlHelper htmlHelper, string modelName, object htmlAttributes)
+ {
+ return ValidationMessage(htmlHelper, modelName, null /* validationMessage */, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ [SuppressMessage("Microsoft.Naming", "CA1719:ParameterNamesShouldNotMatchMemberNames", Justification = "'validationMessage' refers to the message that will be rendered by the ValidationMessage helper.")]
+ public static MvcHtmlString ValidationMessage(this HtmlHelper htmlHelper, string modelName, string validationMessage)
+ {
+ return ValidationMessage(htmlHelper, modelName, validationMessage, new RouteValueDictionary());
+ }
+
+ [SuppressMessage("Microsoft.Naming", "CA1719:ParameterNamesShouldNotMatchMemberNames", Justification = "'validationMessage' refers to the message that will be rendered by the ValidationMessage helper.")]
+ public static MvcHtmlString ValidationMessage(this HtmlHelper htmlHelper, string modelName, string validationMessage, object htmlAttributes)
+ {
+ return ValidationMessage(htmlHelper, modelName, validationMessage, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ public static MvcHtmlString ValidationMessage(this HtmlHelper htmlHelper, string modelName, IDictionary<string, object> htmlAttributes)
+ {
+ return ValidationMessage(htmlHelper, modelName, null /* validationMessage */, htmlAttributes);
+ }
+
+ [SuppressMessage("Microsoft.Naming", "CA1719:ParameterNamesShouldNotMatchMemberNames", Justification = "'validationMessage' refers to the message that will be rendered by the ValidationMessage helper.")]
+ public static MvcHtmlString ValidationMessage(this HtmlHelper htmlHelper, string modelName, string validationMessage, IDictionary<string, object> htmlAttributes)
+ {
+ if (modelName == null)
+ {
+ throw new ArgumentNullException("modelName");
+ }
+
+ return ValidationMessageHelper(htmlHelper,
+ ModelMetadata.FromStringExpression(modelName, htmlHelper.ViewContext.ViewData),
+ modelName,
+ validationMessage,
+ htmlAttributes);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString ValidationMessageFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression)
+ {
+ return ValidationMessageFor(htmlHelper, expression, null /* validationMessage */, new RouteValueDictionary());
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString ValidationMessageFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, string validationMessage)
+ {
+ return ValidationMessageFor(htmlHelper, expression, validationMessage, new RouteValueDictionary());
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString ValidationMessageFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, string validationMessage, object htmlAttributes)
+ {
+ return ValidationMessageFor(htmlHelper, expression, validationMessage, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString ValidationMessageFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, string validationMessage, IDictionary<string, object> htmlAttributes)
+ {
+ return ValidationMessageHelper(htmlHelper,
+ ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData),
+ ExpressionHelper.GetExpressionText(expression),
+ validationMessage,
+ htmlAttributes);
+ }
+
+ [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "Normalization to lowercase is a common requirement for JavaScript and HTML values")]
+ private static MvcHtmlString ValidationMessageHelper(this HtmlHelper htmlHelper, ModelMetadata modelMetadata, string expression, string validationMessage, IDictionary<string, object> htmlAttributes)
+ {
+ string modelName = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(expression);
+ FormContext formContext = htmlHelper.ViewContext.GetFormContextForClientValidation();
+
+ if (!htmlHelper.ViewData.ModelState.ContainsKey(modelName) && formContext == null)
+ {
+ return null;
+ }
+
+ ModelState modelState = htmlHelper.ViewData.ModelState[modelName];
+ ModelErrorCollection modelErrors = (modelState == null) ? null : modelState.Errors;
+ ModelError modelError = (((modelErrors == null) || (modelErrors.Count == 0)) ? null : modelErrors.FirstOrDefault(m => !String.IsNullOrEmpty(m.ErrorMessage)) ?? modelErrors[0]);
+
+ if (modelError == null && formContext == null)
+ {
+ return null;
+ }
+
+ TagBuilder builder = new TagBuilder("span");
+ builder.MergeAttributes(htmlAttributes);
+ builder.AddCssClass((modelError != null) ? HtmlHelper.ValidationMessageCssClassName : HtmlHelper.ValidationMessageValidCssClassName);
+
+ if (!String.IsNullOrEmpty(validationMessage))
+ {
+ builder.SetInnerText(validationMessage);
+ }
+ else if (modelError != null)
+ {
+ builder.SetInnerText(GetUserErrorMessageOrDefault(htmlHelper.ViewContext.HttpContext, modelError, modelState));
+ }
+
+ if (formContext != null)
+ {
+ bool replaceValidationMessageContents = String.IsNullOrEmpty(validationMessage);
+
+ if (htmlHelper.ViewContext.UnobtrusiveJavaScriptEnabled)
+ {
+ builder.MergeAttribute("data-valmsg-for", modelName);
+ builder.MergeAttribute("data-valmsg-replace", replaceValidationMessageContents.ToString().ToLowerInvariant());
+ }
+ else
+ {
+ FieldValidationMetadata fieldMetadata = ApplyFieldValidationMetadata(htmlHelper, modelMetadata, modelName);
+ // rules will already have been written to the metadata object
+ fieldMetadata.ReplaceValidationMessageContents = replaceValidationMessageContents; // only replace contents if no explicit message was specified
+
+ // client validation always requires an ID
+ builder.GenerateId(modelName + "_validationMessage");
+ fieldMetadata.ValidationMessageId = builder.Attributes["id"];
+ }
+ }
+
+ return builder.ToMvcHtmlString(TagRenderMode.Normal);
+ }
+
+ // ValidationSummary
+
+ public static MvcHtmlString ValidationSummary(this HtmlHelper htmlHelper)
+ {
+ return ValidationSummary(htmlHelper, false /* excludePropertyErrors */);
+ }
+
+ public static MvcHtmlString ValidationSummary(this HtmlHelper htmlHelper, bool excludePropertyErrors)
+ {
+ return ValidationSummary(htmlHelper, excludePropertyErrors, null /* message */);
+ }
+
+ public static MvcHtmlString ValidationSummary(this HtmlHelper htmlHelper, string message)
+ {
+ return ValidationSummary(htmlHelper, false /* excludePropertyErrors */, message, (object)null /* htmlAttributes */);
+ }
+
+ public static MvcHtmlString ValidationSummary(this HtmlHelper htmlHelper, bool excludePropertyErrors, string message)
+ {
+ return ValidationSummary(htmlHelper, excludePropertyErrors, message, (object)null /* htmlAttributes */);
+ }
+
+ public static MvcHtmlString ValidationSummary(this HtmlHelper htmlHelper, string message, object htmlAttributes)
+ {
+ return ValidationSummary(htmlHelper, false /* excludePropertyErrors */, message, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ public static MvcHtmlString ValidationSummary(this HtmlHelper htmlHelper, bool excludePropertyErrors, string message, object htmlAttributes)
+ {
+ return ValidationSummary(htmlHelper, excludePropertyErrors, message, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ }
+
+ public static MvcHtmlString ValidationSummary(this HtmlHelper htmlHelper, string message, IDictionary<string, object> htmlAttributes)
+ {
+ return ValidationSummary(htmlHelper, false /* excludePropertyErrors */, message, htmlAttributes);
+ }
+
+ public static MvcHtmlString ValidationSummary(this HtmlHelper htmlHelper, bool excludePropertyErrors, string message, IDictionary<string, object> htmlAttributes)
+ {
+ if (htmlHelper == null)
+ {
+ throw new ArgumentNullException("htmlHelper");
+ }
+
+ FormContext formContext = htmlHelper.ViewContext.GetFormContextForClientValidation();
+ if (htmlHelper.ViewData.ModelState.IsValid)
+ {
+ if (formContext == null)
+ {
+ // No client side validation
+ return null;
+ }
+ // TODO: This isn't really about unobtrusive; can we fix up non-unobtrusive to get rid of this, too?
+ if (htmlHelper.ViewContext.UnobtrusiveJavaScriptEnabled && excludePropertyErrors)
+ {
+ // No client-side updates
+ return null;
+ }
+ }
+
+ string messageSpan;
+ if (!String.IsNullOrEmpty(message))
+ {
+ TagBuilder spanTag = new TagBuilder("span");
+ spanTag.SetInnerText(message);
+ messageSpan = spanTag.ToString(TagRenderMode.Normal) + Environment.NewLine;
+ }
+ else
+ {
+ messageSpan = null;
+ }
+
+ StringBuilder htmlSummary = new StringBuilder();
+ TagBuilder unorderedList = new TagBuilder("ul");
+
+ IEnumerable<ModelState> modelStates = GetModelStateList(htmlHelper, excludePropertyErrors);
+
+ foreach (ModelState modelState in modelStates)
+ {
+ foreach (ModelError modelError in modelState.Errors)
+ {
+ string errorText = GetUserErrorMessageOrDefault(htmlHelper.ViewContext.HttpContext, modelError, null /* modelState */);
+ if (!String.IsNullOrEmpty(errorText))
+ {
+ TagBuilder listItem = new TagBuilder("li");
+ listItem.SetInnerText(errorText);
+ htmlSummary.AppendLine(listItem.ToString(TagRenderMode.Normal));
+ }
+ }
+ }
+
+ if (htmlSummary.Length == 0)
+ {
+ htmlSummary.AppendLine(HiddenListItem);
+ }
+
+ unorderedList.InnerHtml = htmlSummary.ToString();
+
+ TagBuilder divBuilder = new TagBuilder("div");
+ divBuilder.MergeAttributes(htmlAttributes);
+ divBuilder.AddCssClass((htmlHelper.ViewData.ModelState.IsValid) ? HtmlHelper.ValidationSummaryValidCssClassName : HtmlHelper.ValidationSummaryCssClassName);
+ divBuilder.InnerHtml = messageSpan + unorderedList.ToString(TagRenderMode.Normal);
+
+ if (formContext != null)
+ {
+ if (htmlHelper.ViewContext.UnobtrusiveJavaScriptEnabled)
+ {
+ if (!excludePropertyErrors)
+ {
+ // Only put errors in the validation summary if they're supposed to be included there
+ divBuilder.MergeAttribute("data-valmsg-summary", "true");
+ }
+ }
+ else
+ {
+ // client val summaries need an ID
+ divBuilder.GenerateId("validationSummary");
+ formContext.ValidationSummaryId = divBuilder.Attributes["id"];
+ formContext.ReplaceValidationSummary = !excludePropertyErrors;
+ }
+ }
+ return divBuilder.ToMvcHtmlString(TagRenderMode.Normal);
+ }
+
+ // Returns non-null list of model states, which caller will render in order provided.
+ private static IEnumerable<ModelState> GetModelStateList(HtmlHelper htmlHelper, bool excludePropertyErrors)
+ {
+ if (excludePropertyErrors)
+ {
+ ModelState ms;
+ htmlHelper.ViewData.ModelState.TryGetValue(htmlHelper.ViewData.TemplateInfo.HtmlFieldPrefix, out ms);
+ if (ms != null)
+ {
+ return new ModelState[] { ms };
+ }
+
+ return new ModelState[0];
+ }
+ else
+ {
+ // Sort modelStates to respect the ordering in the metadata.
+ // ModelState doesn't refer to ModelMetadata, but we can correlate via the property name.
+ Dictionary<string, int> ordering = new Dictionary<string, int>();
+
+ var metadata = htmlHelper.ViewData.ModelMetadata;
+ if (metadata != null)
+ {
+ foreach (ModelMetadata m in metadata.Properties)
+ {
+ ordering[m.PropertyName] = m.Order;
+ }
+ }
+
+ return from kv in htmlHelper.ViewData.ModelState
+ let name = kv.Key
+ orderby ordering.GetOrDefault(name, ModelMetadata.DefaultOrder)
+ select kv.Value;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Html/ValueExtensions.cs b/src/System.Web.Mvc/Html/ValueExtensions.cs
new file mode 100644
index 00000000..53e297c7
--- /dev/null
+++ b/src/System.Web.Mvc/Html/ValueExtensions.cs
@@ -0,0 +1,80 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Linq.Expressions;
+
+namespace System.Web.Mvc.Html
+{
+ public static class ValueExtensions
+ {
+ public static MvcHtmlString Value(this HtmlHelper html, string name)
+ {
+ return Value(html, name, format: null);
+ }
+
+ public static MvcHtmlString Value(this HtmlHelper html, string name, string format)
+ {
+ if (name == null)
+ {
+ throw new ArgumentNullException("name");
+ }
+
+ return ValueForHelper(html, name, value: null, format: format, useViewData: true);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString ValueFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression)
+ {
+ return ValueFor(html, expression, format: null);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static MvcHtmlString ValueFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression, string format)
+ {
+ ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData);
+ return ValueForHelper(html, ExpressionHelper.GetExpressionText(expression), metadata.Model, format, useViewData: false);
+ }
+
+ public static MvcHtmlString ValueForModel(this HtmlHelper html)
+ {
+ return ValueForModel(html, format: null);
+ }
+
+ public static MvcHtmlString ValueForModel(this HtmlHelper html, string format)
+ {
+ return Value(html, String.Empty, format);
+ }
+
+ internal static MvcHtmlString ValueForHelper(HtmlHelper html, string name, object value, string format, bool useViewData)
+ {
+ string fullName = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);
+ string attemptedValue = (string)html.GetModelStateValue(fullName, typeof(string));
+ string resolvedValue;
+
+ if (attemptedValue != null)
+ {
+ // case 1: if ModelState has a value then it's already formatted so ignore format string
+ resolvedValue = attemptedValue;
+ }
+ else if (useViewData)
+ {
+ if (name.Length == 0)
+ {
+ // case 2(a): format the value from ModelMetadata for the current model
+ ModelMetadata metadata = ModelMetadata.FromStringExpression(String.Empty, html.ViewContext.ViewData);
+ resolvedValue = html.FormatValue(metadata.Model, format);
+ }
+ else
+ {
+ // case 2(b): format the value from ViewData
+ resolvedValue = html.EvalString(name, format);
+ }
+ }
+ else
+ {
+ // case 3: format the explicit value from ModelMetadata
+ resolvedValue = html.FormatValue(value, format);
+ }
+
+ return MvcHtmlString.Create(html.AttributeEncode(resolvedValue));
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/HtmlHelper.cs b/src/System.Web.Mvc/HtmlHelper.cs
new file mode 100644
index 00000000..c6ab7021
--- /dev/null
+++ b/src/System.Web.Mvc/HtmlHelper.cs
@@ -0,0 +1,451 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Web.Helpers;
+using System.Web.Mvc.Properties;
+using System.Web.Routing;
+
+namespace System.Web.Mvc
+{
+ public class HtmlHelper
+ {
+ public static readonly string ValidationInputCssClassName = "input-validation-error";
+ public static readonly string ValidationInputValidCssClassName = "input-validation-valid";
+ public static readonly string ValidationMessageCssClassName = "field-validation-error";
+ public static readonly string ValidationMessageValidCssClassName = "field-validation-valid";
+ public static readonly string ValidationSummaryCssClassName = "validation-summary-errors";
+ public static readonly string ValidationSummaryValidCssClassName = "validation-summary-valid";
+
+ private DynamicViewDataDictionary _dynamicViewDataDictionary;
+
+ public HtmlHelper(ViewContext viewContext, IViewDataContainer viewDataContainer)
+ : this(viewContext, viewDataContainer, RouteTable.Routes)
+ {
+ }
+
+ public HtmlHelper(ViewContext viewContext, IViewDataContainer viewDataContainer, RouteCollection routeCollection)
+ {
+ if (viewContext == null)
+ {
+ throw new ArgumentNullException("viewContext");
+ }
+ if (viewDataContainer == null)
+ {
+ throw new ArgumentNullException("viewDataContainer");
+ }
+ if (routeCollection == null)
+ {
+ throw new ArgumentNullException("routeCollection");
+ }
+
+ ViewContext = viewContext;
+ ViewDataContainer = viewDataContainer;
+ RouteCollection = routeCollection;
+ ClientValidationRuleFactory = (name, metadata) => ModelValidatorProviders.Providers.GetValidators(metadata ?? ModelMetadata.FromStringExpression(name, ViewData), ViewContext).SelectMany(v => v.GetClientValidationRules());
+ }
+
+ public static bool ClientValidationEnabled
+ {
+ get { return ViewContext.GetClientValidationEnabled(); }
+ set { ViewContext.SetClientValidationEnabled(value); }
+ }
+
+ public static string IdAttributeDotReplacement
+ {
+ get { return WebPages.Html.HtmlHelper.IdAttributeDotReplacement; }
+ set { WebPages.Html.HtmlHelper.IdAttributeDotReplacement = value; }
+ }
+
+ internal Func<string, ModelMetadata, IEnumerable<ModelClientValidationRule>> ClientValidationRuleFactory { get; set; }
+
+ public RouteCollection RouteCollection { get; private set; }
+
+ public static bool UnobtrusiveJavaScriptEnabled
+ {
+ get { return ViewContext.GetUnobtrusiveJavaScriptEnabled(); }
+ set { ViewContext.SetUnobtrusiveJavaScriptEnabled(value); }
+ }
+
+ public dynamic ViewBag
+ {
+ get
+ {
+ if (_dynamicViewDataDictionary == null)
+ {
+ _dynamicViewDataDictionary = new DynamicViewDataDictionary(() => ViewData);
+ }
+ return _dynamicViewDataDictionary;
+ }
+ }
+
+ public ViewContext ViewContext { get; private set; }
+
+ public ViewDataDictionary ViewData
+ {
+ get { return ViewDataContainer.ViewData; }
+ }
+
+ public IViewDataContainer ViewDataContainer { get; internal set; }
+
+ public static RouteValueDictionary AnonymousObjectToHtmlAttributes(object htmlAttributes)
+ {
+ RouteValueDictionary result = new RouteValueDictionary();
+
+ if (htmlAttributes != null)
+ {
+ foreach (PropertyDescriptor property in TypeDescriptor.GetProperties(htmlAttributes))
+ {
+ result.Add(property.Name.Replace('_', '-'), property.GetValue(htmlAttributes));
+ }
+ }
+
+ return result;
+ }
+
+ public MvcHtmlString AntiForgeryToken()
+ {
+ return AntiForgeryToken(salt: null);
+ }
+
+ public MvcHtmlString AntiForgeryToken(string salt)
+ {
+ return AntiForgeryToken(salt, domain: null, path: null);
+ }
+
+ public MvcHtmlString AntiForgeryToken(string salt, string domain, string path)
+ {
+ return new MvcHtmlString(AntiForgery.GetHtml(ViewContext.HttpContext, salt, domain, path).ToString());
+ }
+
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "For consistency, all helpers are instance methods.")]
+ public string AttributeEncode(string value)
+ {
+ return (!String.IsNullOrEmpty(value)) ? HttpUtility.HtmlAttributeEncode(value) : String.Empty;
+ }
+
+ public string AttributeEncode(object value)
+ {
+ return AttributeEncode(Convert.ToString(value, CultureInfo.InvariantCulture));
+ }
+
+ public void EnableClientValidation()
+ {
+ EnableClientValidation(enabled: true);
+ }
+
+ public void EnableClientValidation(bool enabled)
+ {
+ ViewContext.ClientValidationEnabled = enabled;
+ }
+
+ public void EnableUnobtrusiveJavaScript()
+ {
+ EnableUnobtrusiveJavaScript(enabled: true);
+ }
+
+ public void EnableUnobtrusiveJavaScript(bool enabled)
+ {
+ ViewContext.UnobtrusiveJavaScriptEnabled = enabled;
+ }
+
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "For consistency, all helpers are instance methods.")]
+ public string Encode(string value)
+ {
+ return (!String.IsNullOrEmpty(value)) ? HttpUtility.HtmlEncode(value) : String.Empty;
+ }
+
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "For consistency, all helpers are instance methods.")]
+ public string Encode(object value)
+ {
+ return value != null ? HttpUtility.HtmlEncode(value) : String.Empty;
+ }
+
+ internal string EvalString(string key)
+ {
+ return Convert.ToString(ViewData.Eval(key), CultureInfo.CurrentCulture);
+ }
+
+ internal string EvalString(string key, string format)
+ {
+ return Convert.ToString(ViewData.Eval(key, format), CultureInfo.CurrentCulture);
+ }
+
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "For consistency, all helpers are instance methods.")]
+ public string FormatValue(object value, string format)
+ {
+ return ViewDataDictionary.FormatValueInternal(value, format);
+ }
+
+ internal bool EvalBoolean(string key)
+ {
+ return Convert.ToBoolean(ViewData.Eval(key), CultureInfo.InvariantCulture);
+ }
+
+ internal static IView FindPartialView(ViewContext viewContext, string partialViewName, ViewEngineCollection viewEngineCollection)
+ {
+ ViewEngineResult result = viewEngineCollection.FindPartialView(viewContext, partialViewName);
+ if (result.View != null)
+ {
+ return result.View;
+ }
+
+ StringBuilder locationsText = new StringBuilder();
+ foreach (string location in result.SearchedLocations)
+ {
+ locationsText.AppendLine();
+ locationsText.Append(location);
+ }
+
+ throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture,
+ MvcResources.Common_PartialViewNotFound, partialViewName, locationsText));
+ }
+
+ public static string GenerateIdFromName(string name)
+ {
+ return GenerateIdFromName(name, IdAttributeDotReplacement);
+ }
+
+ public static string GenerateIdFromName(string name, string idAttributeDotReplacement)
+ {
+ if (name == null)
+ {
+ throw new ArgumentNullException("name");
+ }
+
+ if (idAttributeDotReplacement == null)
+ {
+ throw new ArgumentNullException("idAttributeDotReplacement");
+ }
+
+ // TagBuilder.CreateSanitizedId returns null for empty strings, return String.Empty instead to avoid breaking change
+ if (name.Length == 0)
+ {
+ return String.Empty;
+ }
+
+ return TagBuilder.CreateSanitizedId(name, idAttributeDotReplacement);
+ }
+
+ public static string GenerateLink(RequestContext requestContext, RouteCollection routeCollection, string linkText, string routeName, string actionName, string controllerName, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes)
+ {
+ return GenerateLink(requestContext, routeCollection, linkText, routeName, actionName, controllerName, null /* protocol */, null /* hostName */, null /* fragment */, routeValues, htmlAttributes);
+ }
+
+ public static string GenerateLink(RequestContext requestContext, RouteCollection routeCollection, string linkText, string routeName, string actionName, string controllerName, string protocol, string hostName, string fragment, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes)
+ {
+ return GenerateLinkInternal(requestContext, routeCollection, linkText, routeName, actionName, controllerName, protocol, hostName, fragment, routeValues, htmlAttributes, true /* includeImplicitMvcValues */);
+ }
+
+ private static string GenerateLinkInternal(RequestContext requestContext, RouteCollection routeCollection, string linkText, string routeName, string actionName, string controllerName, string protocol, string hostName, string fragment, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes, bool includeImplicitMvcValues)
+ {
+ string url = UrlHelper.GenerateUrl(routeName, actionName, controllerName, protocol, hostName, fragment, routeValues, routeCollection, requestContext, includeImplicitMvcValues);
+ TagBuilder tagBuilder = new TagBuilder("a")
+ {
+ InnerHtml = (!String.IsNullOrEmpty(linkText)) ? HttpUtility.HtmlEncode(linkText) : String.Empty
+ };
+ tagBuilder.MergeAttributes(htmlAttributes);
+ tagBuilder.MergeAttribute("href", url);
+ return tagBuilder.ToString(TagRenderMode.Normal);
+ }
+
+ public static string GenerateRouteLink(RequestContext requestContext, RouteCollection routeCollection, string linkText, string routeName, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes)
+ {
+ return GenerateRouteLink(requestContext, routeCollection, linkText, routeName, null /* protocol */, null /* hostName */, null /* fragment */, routeValues, htmlAttributes);
+ }
+
+ public static string GenerateRouteLink(RequestContext requestContext, RouteCollection routeCollection, string linkText, string routeName, string protocol, string hostName, string fragment, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes)
+ {
+ return GenerateLinkInternal(requestContext, routeCollection, linkText, routeName, null /* actionName */, null /* controllerName */, protocol, hostName, fragment, routeValues, htmlAttributes, false /* includeImplicitMvcValues */);
+ }
+
+ public static string GetFormMethodString(FormMethod method)
+ {
+ switch (method)
+ {
+ case FormMethod.Get:
+ return "get";
+ case FormMethod.Post:
+ return "post";
+ default:
+ return "post";
+ }
+ }
+
+ public static string GetInputTypeString(InputType inputType)
+ {
+ switch (inputType)
+ {
+ case InputType.CheckBox:
+ return "checkbox";
+ case InputType.Hidden:
+ return "hidden";
+ case InputType.Password:
+ return "password";
+ case InputType.Radio:
+ return "radio";
+ case InputType.Text:
+ return "text";
+ default:
+ return "text";
+ }
+ }
+
+ internal object GetModelStateValue(string key, Type destinationType)
+ {
+ ModelState modelState;
+ if (ViewData.ModelState.TryGetValue(key, out modelState))
+ {
+ if (modelState.Value != null)
+ {
+ return modelState.Value.ConvertTo(destinationType, null /* culture */);
+ }
+ }
+ return null;
+ }
+
+ public IDictionary<string, object> GetUnobtrusiveValidationAttributes(string name)
+ {
+ return GetUnobtrusiveValidationAttributes(name, metadata: null);
+ }
+
+ // Only render attributes if unobtrusive client-side validation is enabled, and then only if we've
+ // never rendered validation for a field with this name in this form. Also, if there's no form context,
+ // then we can't render the attributes (we'd have no <form> to attach them to).
+ public IDictionary<string, object> GetUnobtrusiveValidationAttributes(string name, ModelMetadata metadata)
+ {
+ Dictionary<string, object> results = new Dictionary<string, object>();
+
+ // The ordering of these 3 checks (and the early exits) is for performance reasons.
+ if (!ViewContext.UnobtrusiveJavaScriptEnabled)
+ {
+ return results;
+ }
+
+ FormContext formContext = ViewContext.GetFormContextForClientValidation();
+ if (formContext == null)
+ {
+ return results;
+ }
+
+ string fullName = ViewData.TemplateInfo.GetFullHtmlFieldName(name);
+ if (formContext.RenderedField(fullName))
+ {
+ return results;
+ }
+
+ formContext.RenderedField(fullName, true);
+
+ IEnumerable<ModelClientValidationRule> clientRules = ClientValidationRuleFactory(name, metadata);
+ UnobtrusiveValidationAttributesGenerator.GetValidationAttributes(clientRules, results);
+
+ return results;
+ }
+
+ public MvcHtmlString HttpMethodOverride(HttpVerbs httpVerb)
+ {
+ string httpMethod;
+ switch (httpVerb)
+ {
+ case HttpVerbs.Delete:
+ httpMethod = "DELETE";
+ break;
+ case HttpVerbs.Head:
+ httpMethod = "HEAD";
+ break;
+ case HttpVerbs.Put:
+ httpMethod = "PUT";
+ break;
+ default:
+ throw new ArgumentException(MvcResources.HtmlHelper_InvalidHttpVerb, "httpVerb");
+ }
+
+ return HttpMethodOverride(httpMethod);
+ }
+
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "For consistency, all helpers are instance methods.")]
+ public MvcHtmlString HttpMethodOverride(string httpMethod)
+ {
+ if (String.IsNullOrEmpty(httpMethod))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "httpMethod");
+ }
+ if (String.Equals(httpMethod, "GET", StringComparison.OrdinalIgnoreCase) ||
+ String.Equals(httpMethod, "POST", StringComparison.OrdinalIgnoreCase))
+ {
+ throw new ArgumentException(MvcResources.HtmlHelper_InvalidHttpMethod, "httpMethod");
+ }
+
+ TagBuilder tagBuilder = new TagBuilder("input");
+ tagBuilder.Attributes["type"] = "hidden";
+ tagBuilder.Attributes["name"] = HttpRequestExtensions.XHttpMethodOverrideKey;
+ tagBuilder.Attributes["value"] = httpMethod;
+
+ return tagBuilder.ToMvcHtmlString(TagRenderMode.SelfClosing);
+ }
+
+ /// <summary>
+ /// Wraps HTML markup in an IHtmlString, which will enable HTML markup to be
+ /// rendered to the output without getting HTML encoded.
+ /// </summary>
+ /// <param name="value">HTML markup string.</param>
+ /// <returns>An IHtmlString that represents HTML markup.</returns>
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "For consistency, all helpers are instance methods.")]
+ public IHtmlString Raw(string value)
+ {
+ return new HtmlString(value);
+ }
+
+ /// <summary>
+ /// Wraps HTML markup from the string representation of an object in an IHtmlString,
+ /// which will enable HTML markup to be rendered to the output without getting HTML encoded.
+ /// </summary>
+ /// <param name="value">object with string representation as HTML markup</param>
+ /// <returns>An IHtmlString that represents HTML markup.</returns>
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "For consistency, all helpers are instance methods.")]
+ public IHtmlString Raw(object value)
+ {
+ return new HtmlString(value == null ? null : value.ToString());
+ }
+
+ internal virtual void RenderPartialInternal(string partialViewName, ViewDataDictionary viewData, object model, TextWriter writer, ViewEngineCollection viewEngineCollection)
+ {
+ if (String.IsNullOrEmpty(partialViewName))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "partialViewName");
+ }
+
+ ViewDataDictionary newViewData = null;
+
+ if (model == null)
+ {
+ if (viewData == null)
+ {
+ newViewData = new ViewDataDictionary(ViewData);
+ }
+ else
+ {
+ newViewData = new ViewDataDictionary(viewData);
+ }
+ }
+ else
+ {
+ if (viewData == null)
+ {
+ newViewData = new ViewDataDictionary(model);
+ }
+ else
+ {
+ newViewData = new ViewDataDictionary(viewData) { Model = model };
+ }
+ }
+
+ ViewContext newViewContext = new ViewContext(ViewContext, ViewContext.View, newViewData, ViewContext.TempData, writer);
+ IView view = FindPartialView(newViewContext, partialViewName, viewEngineCollection);
+ view.Render(newViewContext, writer);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/HtmlHelper`1.cs b/src/System.Web.Mvc/HtmlHelper`1.cs
new file mode 100644
index 00000000..77eb44a7
--- /dev/null
+++ b/src/System.Web.Mvc/HtmlHelper`1.cs
@@ -0,0 +1,39 @@
+using System.Web.Routing;
+
+namespace System.Web.Mvc
+{
+ public class HtmlHelper<TModel> : HtmlHelper
+ {
+ private DynamicViewDataDictionary _dynamicViewDataDictionary;
+ private ViewDataDictionary<TModel> _viewData;
+
+ public HtmlHelper(ViewContext viewContext, IViewDataContainer viewDataContainer)
+ : this(viewContext, viewDataContainer, RouteTable.Routes)
+ {
+ }
+
+ public HtmlHelper(ViewContext viewContext, IViewDataContainer viewDataContainer, RouteCollection routeCollection)
+ : base(viewContext, viewDataContainer, routeCollection)
+ {
+ _viewData = new ViewDataDictionary<TModel>(viewDataContainer.ViewData);
+ }
+
+ public new dynamic ViewBag
+ {
+ get
+ {
+ if (_dynamicViewDataDictionary == null)
+ {
+ _dynamicViewDataDictionary = new DynamicViewDataDictionary(() => ViewData);
+ }
+
+ return _dynamicViewDataDictionary;
+ }
+ }
+
+ public new ViewDataDictionary<TModel> ViewData
+ {
+ get { return _viewData; }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/HttpDeleteAttribute.cs b/src/System.Web.Mvc/HttpDeleteAttribute.cs
new file mode 100644
index 00000000..5e661dfa
--- /dev/null
+++ b/src/System.Web.Mvc/HttpDeleteAttribute.cs
@@ -0,0 +1,15 @@
+using System.Reflection;
+
+namespace System.Web.Mvc
+{
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+ public sealed class HttpDeleteAttribute : ActionMethodSelectorAttribute
+ {
+ private static readonly AcceptVerbsAttribute _innerAttribute = new AcceptVerbsAttribute(HttpVerbs.Delete);
+
+ public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
+ {
+ return _innerAttribute.IsValidForRequest(controllerContext, methodInfo);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/HttpFileCollectionValueProvider.cs b/src/System.Web.Mvc/HttpFileCollectionValueProvider.cs
new file mode 100644
index 00000000..3c3760eb
--- /dev/null
+++ b/src/System.Web.Mvc/HttpFileCollectionValueProvider.cs
@@ -0,0 +1,44 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+
+namespace System.Web.Mvc
+{
+ public sealed class HttpFileCollectionValueProvider : DictionaryValueProvider<HttpPostedFileBase[]>
+ {
+ private static readonly Dictionary<string, HttpPostedFileBase[]> _emptyDictionary = new Dictionary<string, HttpPostedFileBase[]>();
+
+ public HttpFileCollectionValueProvider(ControllerContext controllerContext)
+ : base(GetHttpPostedFileDictionary(controllerContext), CultureInfo.InvariantCulture)
+ {
+ }
+
+ private static Dictionary<string, HttpPostedFileBase[]> GetHttpPostedFileDictionary(ControllerContext controllerContext)
+ {
+ HttpFileCollectionBase files = controllerContext.HttpContext.Request.Files;
+
+ // fast-track common case of no files
+ if (files.Count == 0)
+ {
+ return _emptyDictionary;
+ }
+
+ // build up the 1:many file mapping
+ List<KeyValuePair<string, HttpPostedFileBase>> mapping = new List<KeyValuePair<string, HttpPostedFileBase>>();
+ string[] allKeys = files.AllKeys;
+ for (int i = 0; i < files.Count; i++)
+ {
+ string key = allKeys[i];
+ if (key != null)
+ {
+ HttpPostedFileBase file = HttpPostedFileBaseModelBinder.ChooseFileOrNull(files[i]);
+ mapping.Add(new KeyValuePair<string, HttpPostedFileBase>(key, file));
+ }
+ }
+
+ // turn the mapping into a 1:many dictionary
+ var grouped = mapping.GroupBy(el => el.Key, el => el.Value, StringComparer.OrdinalIgnoreCase);
+ return grouped.ToDictionary(g => g.Key, g => g.ToArray(), StringComparer.OrdinalIgnoreCase);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/HttpFileCollectionValueProviderFactory.cs b/src/System.Web.Mvc/HttpFileCollectionValueProviderFactory.cs
new file mode 100644
index 00000000..0c93d4dd
--- /dev/null
+++ b/src/System.Web.Mvc/HttpFileCollectionValueProviderFactory.cs
@@ -0,0 +1,15 @@
+namespace System.Web.Mvc
+{
+ public sealed class HttpFileCollectionValueProviderFactory : ValueProviderFactory
+ {
+ public override IValueProvider GetValueProvider(ControllerContext controllerContext)
+ {
+ if (controllerContext == null)
+ {
+ throw new ArgumentNullException("controllerContext");
+ }
+
+ return new HttpFileCollectionValueProvider(controllerContext);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/HttpGetAttribute.cs b/src/System.Web.Mvc/HttpGetAttribute.cs
new file mode 100644
index 00000000..41029ceb
--- /dev/null
+++ b/src/System.Web.Mvc/HttpGetAttribute.cs
@@ -0,0 +1,15 @@
+using System.Reflection;
+
+namespace System.Web.Mvc
+{
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+ public sealed class HttpGetAttribute : ActionMethodSelectorAttribute
+ {
+ private static readonly AcceptVerbsAttribute _innerAttribute = new AcceptVerbsAttribute(HttpVerbs.Get);
+
+ public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
+ {
+ return _innerAttribute.IsValidForRequest(controllerContext, methodInfo);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/HttpHandlerUtil.cs b/src/System.Web.Mvc/HttpHandlerUtil.cs
new file mode 100644
index 00000000..89d1981b
--- /dev/null
+++ b/src/System.Web.Mvc/HttpHandlerUtil.cs
@@ -0,0 +1,91 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Web.Mvc.Properties;
+using System.Web.UI;
+
+namespace System.Web.Mvc
+{
+ internal static class HttpHandlerUtil
+ {
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "The Dispose on Page doesn't do anything by default, and we control both of these internal types.")]
+ public static IHttpHandler WrapForServerExecute(IHttpHandler httpHandler)
+ {
+ // Since Server.Execute() doesn't propagate HttpExceptions where the status code is
+ // anything other than 500, we need to wrap these exceptions ourselves.
+ IHttpAsyncHandler asyncHandler = httpHandler as IHttpAsyncHandler;
+ return (asyncHandler != null) ? new ServerExecuteHttpHandlerAsyncWrapper(asyncHandler) : new ServerExecuteHttpHandlerWrapper(httpHandler);
+ }
+
+ private sealed class ServerExecuteHttpHandlerAsyncWrapper : ServerExecuteHttpHandlerWrapper, IHttpAsyncHandler
+ {
+ private readonly IHttpAsyncHandler _httpHandler;
+
+ public ServerExecuteHttpHandlerAsyncWrapper(IHttpAsyncHandler httpHandler)
+ : base(httpHandler)
+ {
+ _httpHandler = httpHandler;
+ }
+
+ public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)
+ {
+ return Wrap(() => _httpHandler.BeginProcessRequest(context, cb, extraData));
+ }
+
+ public void EndProcessRequest(IAsyncResult result)
+ {
+ Wrap(() => _httpHandler.EndProcessRequest(result));
+ }
+ }
+
+ /// <remarks>
+ /// Server.Execute() requires that the provided IHttpHandler subclass Page.
+ /// </remarks>
+ internal class ServerExecuteHttpHandlerWrapper : Page
+ {
+ private readonly IHttpHandler _httpHandler;
+
+ public ServerExecuteHttpHandlerWrapper(IHttpHandler httpHandler)
+ {
+ _httpHandler = httpHandler;
+ }
+
+ internal IHttpHandler InnerHandler
+ {
+ get { return _httpHandler; }
+ }
+
+ public override void ProcessRequest(HttpContext context)
+ {
+ Wrap(() => _httpHandler.ProcessRequest(context));
+ }
+
+ protected static void Wrap(Action action)
+ {
+ Wrap(delegate
+ {
+ action();
+ return (object)null;
+ });
+ }
+
+ protected static TResult Wrap<TResult>(Func<TResult> func)
+ {
+ try
+ {
+ return func();
+ }
+ catch (HttpException he)
+ {
+ if (he.GetHttpCode() == 500)
+ {
+ throw; // doesn't need to be wrapped
+ }
+ else
+ {
+ HttpException newHe = new HttpException(500, MvcResources.ViewPageHttpHandlerWrapper_ExceptionOccurred, he);
+ throw newHe;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/HttpNotFoundResult.cs b/src/System.Web.Mvc/HttpNotFoundResult.cs
new file mode 100644
index 00000000..9b749ace
--- /dev/null
+++ b/src/System.Web.Mvc/HttpNotFoundResult.cs
@@ -0,0 +1,18 @@
+using System.Net;
+
+namespace System.Web.Mvc
+{
+ public class HttpNotFoundResult : HttpStatusCodeResult
+ {
+ public HttpNotFoundResult()
+ : this(null)
+ {
+ }
+
+ // NotFound is equivalent to HTTP status 404.
+ public HttpNotFoundResult(string statusDescription)
+ : base(HttpStatusCode.NotFound, statusDescription)
+ {
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/HttpPostAttribute.cs b/src/System.Web.Mvc/HttpPostAttribute.cs
new file mode 100644
index 00000000..da4e10cc
--- /dev/null
+++ b/src/System.Web.Mvc/HttpPostAttribute.cs
@@ -0,0 +1,15 @@
+using System.Reflection;
+
+namespace System.Web.Mvc
+{
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+ public sealed class HttpPostAttribute : ActionMethodSelectorAttribute
+ {
+ private static readonly AcceptVerbsAttribute _innerAttribute = new AcceptVerbsAttribute(HttpVerbs.Post);
+
+ public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
+ {
+ return _innerAttribute.IsValidForRequest(controllerContext, methodInfo);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/HttpPostedFileBaseModelBinder.cs b/src/System.Web.Mvc/HttpPostedFileBaseModelBinder.cs
new file mode 100644
index 00000000..43545acc
--- /dev/null
+++ b/src/System.Web.Mvc/HttpPostedFileBaseModelBinder.cs
@@ -0,0 +1,39 @@
+namespace System.Web.Mvc
+{
+ public class HttpPostedFileBaseModelBinder : IModelBinder
+ {
+ public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
+ {
+ if (controllerContext == null)
+ {
+ throw new ArgumentNullException("controllerContext");
+ }
+ if (bindingContext == null)
+ {
+ throw new ArgumentNullException("bindingContext");
+ }
+
+ HttpPostedFileBase theFile = controllerContext.HttpContext.Request.Files[bindingContext.ModelName];
+ return ChooseFileOrNull(theFile);
+ }
+
+ // helper that returns the original file if there was content uploaded, null if empty
+ internal static HttpPostedFileBase ChooseFileOrNull(HttpPostedFileBase rawFile)
+ {
+ // case 1: there was no <input type="file" ... /> element in the post
+ if (rawFile == null)
+ {
+ return null;
+ }
+
+ // case 2: there was an <input type="file" ... /> element in the post, but it was left blank
+ if (rawFile.ContentLength == 0 && String.IsNullOrEmpty(rawFile.FileName))
+ {
+ return null;
+ }
+
+ // case 3: the file was posted
+ return rawFile;
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/HttpPutAttribute.cs b/src/System.Web.Mvc/HttpPutAttribute.cs
new file mode 100644
index 00000000..7f0fe8e3
--- /dev/null
+++ b/src/System.Web.Mvc/HttpPutAttribute.cs
@@ -0,0 +1,15 @@
+using System.Reflection;
+
+namespace System.Web.Mvc
+{
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+ public sealed class HttpPutAttribute : ActionMethodSelectorAttribute
+ {
+ private static readonly AcceptVerbsAttribute _innerAttribute = new AcceptVerbsAttribute(HttpVerbs.Put);
+
+ public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
+ {
+ return _innerAttribute.IsValidForRequest(controllerContext, methodInfo);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/HttpRequestExtensions.cs b/src/System.Web.Mvc/HttpRequestExtensions.cs
new file mode 100644
index 00000000..d1719da6
--- /dev/null
+++ b/src/System.Web.Mvc/HttpRequestExtensions.cs
@@ -0,0 +1,54 @@
+namespace System.Web.Mvc
+{
+ public static class HttpRequestExtensions
+ {
+ internal const string XHttpMethodOverrideKey = "X-HTTP-Method-Override";
+
+ public static string GetHttpMethodOverride(this HttpRequestBase request)
+ {
+ if (request == null)
+ {
+ throw new ArgumentNullException("request");
+ }
+
+ string incomingVerb = request.HttpMethod;
+
+ if (!String.Equals(incomingVerb, "POST", StringComparison.OrdinalIgnoreCase))
+ {
+ return incomingVerb;
+ }
+
+ string verbOverride = null;
+ string headerOverrideValue = request.Headers[XHttpMethodOverrideKey];
+ if (!String.IsNullOrEmpty(headerOverrideValue))
+ {
+ verbOverride = headerOverrideValue;
+ }
+ else
+ {
+ string formOverrideValue = request.Form[XHttpMethodOverrideKey];
+ if (!String.IsNullOrEmpty(formOverrideValue))
+ {
+ verbOverride = formOverrideValue;
+ }
+ else
+ {
+ string queryStringOverrideValue = request.QueryString[XHttpMethodOverrideKey];
+ if (!String.IsNullOrEmpty(queryStringOverrideValue))
+ {
+ verbOverride = queryStringOverrideValue;
+ }
+ }
+ }
+ if (verbOverride != null)
+ {
+ if (!String.Equals(verbOverride, "GET", StringComparison.OrdinalIgnoreCase) &&
+ !String.Equals(verbOverride, "POST", StringComparison.OrdinalIgnoreCase))
+ {
+ incomingVerb = verbOverride;
+ }
+ }
+ return incomingVerb;
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/HttpStatusCodeResult.cs b/src/System.Web.Mvc/HttpStatusCodeResult.cs
new file mode 100644
index 00000000..785e1eae
--- /dev/null
+++ b/src/System.Web.Mvc/HttpStatusCodeResult.cs
@@ -0,0 +1,46 @@
+using System.Net;
+
+namespace System.Web.Mvc
+{
+ public class HttpStatusCodeResult : ActionResult
+ {
+ public HttpStatusCodeResult(int statusCode)
+ : this(statusCode, null)
+ {
+ }
+
+ public HttpStatusCodeResult(HttpStatusCode statusCode)
+ : this(statusCode, null)
+ {
+ }
+
+ public HttpStatusCodeResult(HttpStatusCode statusCode, string statusDescription)
+ : this((int)statusCode, statusDescription)
+ {
+ }
+
+ public HttpStatusCodeResult(int statusCode, string statusDescription)
+ {
+ StatusCode = statusCode;
+ StatusDescription = statusDescription;
+ }
+
+ public int StatusCode { get; private set; }
+
+ public string StatusDescription { get; private set; }
+
+ public override void ExecuteResult(ControllerContext context)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException("context");
+ }
+
+ context.HttpContext.Response.StatusCode = StatusCode;
+ if (StatusDescription != null)
+ {
+ context.HttpContext.Response.StatusDescription = StatusDescription;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/HttpUnauthorizedResult.cs b/src/System.Web.Mvc/HttpUnauthorizedResult.cs
new file mode 100644
index 00000000..fe672422
--- /dev/null
+++ b/src/System.Web.Mvc/HttpUnauthorizedResult.cs
@@ -0,0 +1,21 @@
+using System.Net;
+
+namespace System.Web.Mvc
+{
+ public class HttpUnauthorizedResult : HttpStatusCodeResult
+ {
+ public HttpUnauthorizedResult()
+ : this(null)
+ {
+ }
+
+ // Unauthorized is equivalent to HTTP status 401, the status code for unauthorized
+ // access. Other code might intercept this and perform some special logic. For
+ // example, the FormsAuthenticationModule looks for 401 responses and instead
+ // redirects the user to the login page.
+ public HttpUnauthorizedResult(string statusDescription)
+ : base(HttpStatusCode.Unauthorized, statusDescription)
+ {
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/HttpVerbs.cs b/src/System.Web.Mvc/HttpVerbs.cs
new file mode 100644
index 00000000..22049977
--- /dev/null
+++ b/src/System.Web.Mvc/HttpVerbs.cs
@@ -0,0 +1,12 @@
+namespace System.Web.Mvc
+{
+ [Flags]
+ public enum HttpVerbs
+ {
+ Get = 1 << 0,
+ Post = 1 << 1,
+ Put = 1 << 2,
+ Delete = 1 << 3,
+ Head = 1 << 4
+ }
+}
diff --git a/src/System.Web.Mvc/IActionFilter.cs b/src/System.Web.Mvc/IActionFilter.cs
new file mode 100644
index 00000000..8ebbbca5
--- /dev/null
+++ b/src/System.Web.Mvc/IActionFilter.cs
@@ -0,0 +1,8 @@
+namespace System.Web.Mvc
+{
+ public interface IActionFilter
+ {
+ void OnActionExecuting(ActionExecutingContext filterContext);
+ void OnActionExecuted(ActionExecutedContext filterContext);
+ }
+}
diff --git a/src/System.Web.Mvc/IActionInvoker.cs b/src/System.Web.Mvc/IActionInvoker.cs
new file mode 100644
index 00000000..0cec2baf
--- /dev/null
+++ b/src/System.Web.Mvc/IActionInvoker.cs
@@ -0,0 +1,7 @@
+namespace System.Web.Mvc
+{
+ public interface IActionInvoker
+ {
+ bool InvokeAction(ControllerContext controllerContext, string actionName);
+ }
+}
diff --git a/src/System.Web.Mvc/IAuthorizationFilter.cs b/src/System.Web.Mvc/IAuthorizationFilter.cs
new file mode 100644
index 00000000..e0f01b26
--- /dev/null
+++ b/src/System.Web.Mvc/IAuthorizationFilter.cs
@@ -0,0 +1,7 @@
+namespace System.Web.Mvc
+{
+ public interface IAuthorizationFilter
+ {
+ void OnAuthorization(AuthorizationContext filterContext);
+ }
+}
diff --git a/src/System.Web.Mvc/IBuildManager.cs b/src/System.Web.Mvc/IBuildManager.cs
new file mode 100644
index 00000000..5782fbd6
--- /dev/null
+++ b/src/System.Web.Mvc/IBuildManager.cs
@@ -0,0 +1,14 @@
+using System.Collections;
+using System.IO;
+
+namespace System.Web.Mvc
+{
+ internal interface IBuildManager
+ {
+ bool FileExists(string virtualPath);
+ Type GetCompiledType(string virtualPath);
+ ICollection GetReferencedAssemblies();
+ Stream ReadCachedFile(string fileName);
+ Stream CreateCachedFile(string fileName);
+ }
+}
diff --git a/src/System.Web.Mvc/IClientValidatable.cs b/src/System.Web.Mvc/IClientValidatable.cs
new file mode 100644
index 00000000..1eeee7c4
--- /dev/null
+++ b/src/System.Web.Mvc/IClientValidatable.cs
@@ -0,0 +1,19 @@
+using System.Collections.Generic;
+
+namespace System.Web.Mvc
+{
+ // The purpose of this interface is to make something as supporting client-side
+ // validation, which could be discovered at runtime by whatever validation
+ // framework you're using. Because this interface is designed to be independent
+ // of underlying implementation details, where you apply this interface will
+ // depend on your specific validation framework.
+ //
+ // For DataAnnotations, you'll apply this interface to your validation attribute
+ // (the class which derives from ValidationAttribute). When you've implemented
+ // this interface, it will alleviate the need of writing a validator and registering
+ // it with the DataAnnotationsModelValidatorProvider.
+ public interface IClientValidatable
+ {
+ IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context);
+ }
+}
diff --git a/src/System.Web.Mvc/IController.cs b/src/System.Web.Mvc/IController.cs
new file mode 100644
index 00000000..751f8d29
--- /dev/null
+++ b/src/System.Web.Mvc/IController.cs
@@ -0,0 +1,9 @@
+using System.Web.Routing;
+
+namespace System.Web.Mvc
+{
+ public interface IController
+ {
+ void Execute(RequestContext requestContext);
+ }
+}
diff --git a/src/System.Web.Mvc/IControllerActivator.cs b/src/System.Web.Mvc/IControllerActivator.cs
new file mode 100644
index 00000000..38bece0e
--- /dev/null
+++ b/src/System.Web.Mvc/IControllerActivator.cs
@@ -0,0 +1,9 @@
+using System.Web.Routing;
+
+namespace System.Web.Mvc
+{
+ public interface IControllerActivator
+ {
+ IController Create(RequestContext requestContext, Type controllerType);
+ }
+}
diff --git a/src/System.Web.Mvc/IControllerFactory.cs b/src/System.Web.Mvc/IControllerFactory.cs
new file mode 100644
index 00000000..55166ba7
--- /dev/null
+++ b/src/System.Web.Mvc/IControllerFactory.cs
@@ -0,0 +1,12 @@
+using System.Web.Routing;
+using System.Web.SessionState;
+
+namespace System.Web.Mvc
+{
+ public interface IControllerFactory
+ {
+ IController CreateController(RequestContext requestContext, string controllerName);
+ SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName);
+ void ReleaseController(IController controller);
+ }
+}
diff --git a/src/System.Web.Mvc/IDependencyResolver.cs b/src/System.Web.Mvc/IDependencyResolver.cs
new file mode 100644
index 00000000..fe5a6cbd
--- /dev/null
+++ b/src/System.Web.Mvc/IDependencyResolver.cs
@@ -0,0 +1,10 @@
+using System.Collections.Generic;
+
+namespace System.Web.Mvc
+{
+ public interface IDependencyResolver
+ {
+ object GetService(Type serviceType);
+ IEnumerable<object> GetServices(Type serviceType);
+ }
+}
diff --git a/src/System.Web.Mvc/IEnumerableValueProvider.cs b/src/System.Web.Mvc/IEnumerableValueProvider.cs
new file mode 100644
index 00000000..e4fe1bde
--- /dev/null
+++ b/src/System.Web.Mvc/IEnumerableValueProvider.cs
@@ -0,0 +1,10 @@
+using System.Collections.Generic;
+
+namespace System.Web.Mvc
+{
+ // Represents a special IValueProvider that has the ability to be enumerable.
+ public interface IEnumerableValueProvider : IValueProvider
+ {
+ IDictionary<string, string> GetKeysFromPrefix(string prefix);
+ }
+}
diff --git a/src/System.Web.Mvc/IExceptionFilter.cs b/src/System.Web.Mvc/IExceptionFilter.cs
new file mode 100644
index 00000000..545187e6
--- /dev/null
+++ b/src/System.Web.Mvc/IExceptionFilter.cs
@@ -0,0 +1,7 @@
+namespace System.Web.Mvc
+{
+ public interface IExceptionFilter
+ {
+ void OnException(ExceptionContext filterContext);
+ }
+}
diff --git a/src/System.Web.Mvc/IFilterProvider.cs b/src/System.Web.Mvc/IFilterProvider.cs
new file mode 100644
index 00000000..edae298e
--- /dev/null
+++ b/src/System.Web.Mvc/IFilterProvider.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace System.Web.Mvc
+{
+ public interface IFilterProvider
+ {
+ IEnumerable<Filter> GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor);
+ }
+}
diff --git a/src/System.Web.Mvc/IMetadataAware.cs b/src/System.Web.Mvc/IMetadataAware.cs
new file mode 100644
index 00000000..c1e9df24
--- /dev/null
+++ b/src/System.Web.Mvc/IMetadataAware.cs
@@ -0,0 +1,12 @@
+namespace System.Web.Mvc
+{
+ // This interface is implemented by attributes which wish to contribute to the
+ // ModelMetadata creation process without needing to write a custom metadata
+ // provider. It is consumed by AssociatedMetadataProvider, so this behavior is
+ // automatically inherited by all classes which derive from it (notably, the
+ // DataAnnotationsModelMetadataProvider).
+ public interface IMetadataAware
+ {
+ void OnMetadataCreated(ModelMetadata metadata);
+ }
+}
diff --git a/src/System.Web.Mvc/IModelBinder.cs b/src/System.Web.Mvc/IModelBinder.cs
new file mode 100644
index 00000000..41a488d2
--- /dev/null
+++ b/src/System.Web.Mvc/IModelBinder.cs
@@ -0,0 +1,7 @@
+namespace System.Web.Mvc
+{
+ public interface IModelBinder
+ {
+ object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext);
+ }
+}
diff --git a/src/System.Web.Mvc/IModelBinderProvider.cs b/src/System.Web.Mvc/IModelBinderProvider.cs
new file mode 100644
index 00000000..eac700a1
--- /dev/null
+++ b/src/System.Web.Mvc/IModelBinderProvider.cs
@@ -0,0 +1,7 @@
+namespace System.Web.Mvc
+{
+ public interface IModelBinderProvider
+ {
+ IModelBinder GetBinder(Type modelType);
+ }
+}
diff --git a/src/System.Web.Mvc/IMvcControlBuilder.cs b/src/System.Web.Mvc/IMvcControlBuilder.cs
new file mode 100644
index 00000000..2f7e2064
--- /dev/null
+++ b/src/System.Web.Mvc/IMvcControlBuilder.cs
@@ -0,0 +1,7 @@
+namespace System.Web.Mvc
+{
+ internal interface IMvcControlBuilder
+ {
+ string Inherits { set; }
+ }
+}
diff --git a/src/System.Web.Mvc/IMvcFilter.cs b/src/System.Web.Mvc/IMvcFilter.cs
new file mode 100644
index 00000000..05aa035f
--- /dev/null
+++ b/src/System.Web.Mvc/IMvcFilter.cs
@@ -0,0 +1,8 @@
+namespace System.Web.Mvc
+{
+ public interface IMvcFilter
+ {
+ bool AllowMultiple { get; }
+ int Order { get; }
+ }
+}
diff --git a/src/System.Web.Mvc/IResolver.cs b/src/System.Web.Mvc/IResolver.cs
new file mode 100644
index 00000000..0d0a01d6
--- /dev/null
+++ b/src/System.Web.Mvc/IResolver.cs
@@ -0,0 +1,7 @@
+namespace System.Web.Mvc
+{
+ internal interface IResolver<T>
+ {
+ T Current { get; }
+ }
+}
diff --git a/src/System.Web.Mvc/IResultFilter.cs b/src/System.Web.Mvc/IResultFilter.cs
new file mode 100644
index 00000000..892489cb
--- /dev/null
+++ b/src/System.Web.Mvc/IResultFilter.cs
@@ -0,0 +1,8 @@
+namespace System.Web.Mvc
+{
+ public interface IResultFilter
+ {
+ void OnResultExecuting(ResultExecutingContext filterContext);
+ void OnResultExecuted(ResultExecutedContext filterContext);
+ }
+}
diff --git a/src/System.Web.Mvc/IRouteWithArea.cs b/src/System.Web.Mvc/IRouteWithArea.cs
new file mode 100644
index 00000000..83fb00fc
--- /dev/null
+++ b/src/System.Web.Mvc/IRouteWithArea.cs
@@ -0,0 +1,7 @@
+namespace System.Web.Mvc
+{
+ public interface IRouteWithArea
+ {
+ string Area { get; }
+ }
+}
diff --git a/src/System.Web.Mvc/ITempDataProvider.cs b/src/System.Web.Mvc/ITempDataProvider.cs
new file mode 100644
index 00000000..7f40896c
--- /dev/null
+++ b/src/System.Web.Mvc/ITempDataProvider.cs
@@ -0,0 +1,10 @@
+using System.Collections.Generic;
+
+namespace System.Web.Mvc
+{
+ public interface ITempDataProvider
+ {
+ IDictionary<string, object> LoadTempData(ControllerContext controllerContext);
+ void SaveTempData(ControllerContext controllerContext, IDictionary<string, object> values);
+ }
+}
diff --git a/src/System.Web.Mvc/IUniquelyIdentifiable.cs b/src/System.Web.Mvc/IUniquelyIdentifiable.cs
new file mode 100644
index 00000000..a310c93a
--- /dev/null
+++ b/src/System.Web.Mvc/IUniquelyIdentifiable.cs
@@ -0,0 +1,7 @@
+namespace System.Web.Mvc
+{
+ internal interface IUniquelyIdentifiable
+ {
+ string UniqueId { get; }
+ }
+}
diff --git a/src/System.Web.Mvc/IUnvalidatedRequestValues.cs b/src/System.Web.Mvc/IUnvalidatedRequestValues.cs
new file mode 100644
index 00000000..1f5c67bc
--- /dev/null
+++ b/src/System.Web.Mvc/IUnvalidatedRequestValues.cs
@@ -0,0 +1,13 @@
+using System.Collections.Specialized;
+
+namespace System.Web.Mvc
+{
+ // Used for mocking the UnvalidatedRequestValues type in System.Web.WebPages
+
+ internal interface IUnvalidatedRequestValues
+ {
+ NameValueCollection Form { get; }
+ NameValueCollection QueryString { get; }
+ string this[string key] { get; }
+ }
+}
diff --git a/src/System.Web.Mvc/IUnvalidatedValueProvider.cs b/src/System.Web.Mvc/IUnvalidatedValueProvider.cs
new file mode 100644
index 00000000..91882c3a
--- /dev/null
+++ b/src/System.Web.Mvc/IUnvalidatedValueProvider.cs
@@ -0,0 +1,8 @@
+namespace System.Web.Mvc
+{
+ // Represents a special IValueProvider that has the ability to skip request validation.
+ public interface IUnvalidatedValueProvider : IValueProvider
+ {
+ ValueProviderResult GetValue(string key, bool skipValidation);
+ }
+}
diff --git a/src/System.Web.Mvc/IValueProvider.cs b/src/System.Web.Mvc/IValueProvider.cs
new file mode 100644
index 00000000..37b83a14
--- /dev/null
+++ b/src/System.Web.Mvc/IValueProvider.cs
@@ -0,0 +1,8 @@
+namespace System.Web.Mvc
+{
+ public interface IValueProvider
+ {
+ bool ContainsPrefix(string prefix);
+ ValueProviderResult GetValue(string key);
+ }
+}
diff --git a/src/System.Web.Mvc/IView.cs b/src/System.Web.Mvc/IView.cs
new file mode 100644
index 00000000..2d48cf70
--- /dev/null
+++ b/src/System.Web.Mvc/IView.cs
@@ -0,0 +1,9 @@
+using System.IO;
+
+namespace System.Web.Mvc
+{
+ public interface IView
+ {
+ void Render(ViewContext viewContext, TextWriter writer);
+ }
+}
diff --git a/src/System.Web.Mvc/IViewDataContainer.cs b/src/System.Web.Mvc/IViewDataContainer.cs
new file mode 100644
index 00000000..34063148
--- /dev/null
+++ b/src/System.Web.Mvc/IViewDataContainer.cs
@@ -0,0 +1,10 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Web.Mvc
+{
+ public interface IViewDataContainer
+ {
+ [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This is the mechanism by which the ViewPage / ViewUserControl get their ViewDataDictionary objects.")]
+ ViewDataDictionary ViewData { get; set; }
+ }
+}
diff --git a/src/System.Web.Mvc/IViewEngine.cs b/src/System.Web.Mvc/IViewEngine.cs
new file mode 100644
index 00000000..cda8075f
--- /dev/null
+++ b/src/System.Web.Mvc/IViewEngine.cs
@@ -0,0 +1,9 @@
+namespace System.Web.Mvc
+{
+ public interface IViewEngine
+ {
+ ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache);
+ ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache);
+ void ReleaseView(ControllerContext controllerContext, IView view);
+ }
+}
diff --git a/src/System.Web.Mvc/IViewLocationCache.cs b/src/System.Web.Mvc/IViewLocationCache.cs
new file mode 100644
index 00000000..a8661349
--- /dev/null
+++ b/src/System.Web.Mvc/IViewLocationCache.cs
@@ -0,0 +1,8 @@
+namespace System.Web.Mvc
+{
+ public interface IViewLocationCache
+ {
+ string GetViewLocation(HttpContextBase httpContext, string key);
+ void InsertViewLocation(HttpContextBase httpContext, string key, string virtualPath);
+ }
+}
diff --git a/src/System.Web.Mvc/IViewPageActivator.cs b/src/System.Web.Mvc/IViewPageActivator.cs
new file mode 100644
index 00000000..40420ff8
--- /dev/null
+++ b/src/System.Web.Mvc/IViewPageActivator.cs
@@ -0,0 +1,7 @@
+namespace System.Web.Mvc
+{
+ public interface IViewPageActivator
+ {
+ object Create(ControllerContext controllerContext, Type type);
+ }
+}
diff --git a/src/System.Web.Mvc/IViewStartPageChild.cs b/src/System.Web.Mvc/IViewStartPageChild.cs
new file mode 100644
index 00000000..b97d02ed
--- /dev/null
+++ b/src/System.Web.Mvc/IViewStartPageChild.cs
@@ -0,0 +1,9 @@
+namespace System.Web.Mvc
+{
+ internal interface IViewStartPageChild
+ {
+ HtmlHelper<object> Html { get; }
+ UrlHelper Url { get; }
+ ViewContext ViewContext { get; }
+ }
+}
diff --git a/src/System.Web.Mvc/InputType.cs b/src/System.Web.Mvc/InputType.cs
new file mode 100644
index 00000000..d3c3b204
--- /dev/null
+++ b/src/System.Web.Mvc/InputType.cs
@@ -0,0 +1,11 @@
+namespace System.Web.Mvc
+{
+ public enum InputType
+ {
+ CheckBox,
+ Hidden,
+ Password,
+ Radio,
+ Text
+ }
+}
diff --git a/src/System.Web.Mvc/JavaScriptResult.cs b/src/System.Web.Mvc/JavaScriptResult.cs
new file mode 100644
index 00000000..e692648d
--- /dev/null
+++ b/src/System.Web.Mvc/JavaScriptResult.cs
@@ -0,0 +1,23 @@
+namespace System.Web.Mvc
+{
+ public class JavaScriptResult : ActionResult
+ {
+ public string Script { get; set; }
+
+ public override void ExecuteResult(ControllerContext context)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException("context");
+ }
+
+ HttpResponseBase response = context.HttpContext.Response;
+ response.ContentType = "application/x-javascript";
+
+ if (Script != null)
+ {
+ response.Write(Script);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/JsonRequestBehavior.cs b/src/System.Web.Mvc/JsonRequestBehavior.cs
new file mode 100644
index 00000000..438d3cb3
--- /dev/null
+++ b/src/System.Web.Mvc/JsonRequestBehavior.cs
@@ -0,0 +1,8 @@
+namespace System.Web.Mvc
+{
+ public enum JsonRequestBehavior
+ {
+ AllowGet,
+ DenyGet,
+ }
+}
diff --git a/src/System.Web.Mvc/JsonResult.cs b/src/System.Web.Mvc/JsonResult.cs
new file mode 100644
index 00000000..762a787c
--- /dev/null
+++ b/src/System.Web.Mvc/JsonResult.cs
@@ -0,0 +1,73 @@
+using System.Text;
+using System.Web.Mvc.Properties;
+using System.Web.Script.Serialization;
+
+namespace System.Web.Mvc
+{
+ public class JsonResult : ActionResult
+ {
+ public JsonResult()
+ {
+ JsonRequestBehavior = JsonRequestBehavior.DenyGet;
+ }
+
+ public Encoding ContentEncoding { get; set; }
+
+ public string ContentType { get; set; }
+
+ public object Data { get; set; }
+
+ public JsonRequestBehavior JsonRequestBehavior { get; set; }
+
+ /// <summary>
+ /// When set MaxJsonLength passed to the JavaScriptSerializer.
+ /// </summary>
+ public int? MaxJsonLength { get; set; }
+
+ /// <summary>
+ /// When set RecursionLimit passed to the JavaScriptSerializer.
+ /// </summary>
+ public int? RecursionLimit { get; set; }
+
+ public override void ExecuteResult(ControllerContext context)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException("context");
+ }
+ if (JsonRequestBehavior == JsonRequestBehavior.DenyGet &&
+ String.Equals(context.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))
+ {
+ throw new InvalidOperationException(MvcResources.JsonRequest_GetNotAllowed);
+ }
+
+ HttpResponseBase response = context.HttpContext.Response;
+
+ if (!String.IsNullOrEmpty(ContentType))
+ {
+ response.ContentType = ContentType;
+ }
+ else
+ {
+ response.ContentType = "application/json";
+ }
+ if (ContentEncoding != null)
+ {
+ response.ContentEncoding = ContentEncoding;
+ }
+ if (Data != null)
+ {
+ JavaScriptSerializer serializer = new JavaScriptSerializer();
+ if (MaxJsonLength.HasValue)
+ {
+ serializer.MaxJsonLength = MaxJsonLength.Value;
+ }
+ if (RecursionLimit.HasValue)
+ {
+ serializer.RecursionLimit = RecursionLimit.Value;
+ }
+ response.Write(serializer.Serialize(Data));
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/JsonValueProviderFactory.cs b/src/System.Web.Mvc/JsonValueProviderFactory.cs
new file mode 100644
index 00000000..31be9845
--- /dev/null
+++ b/src/System.Web.Mvc/JsonValueProviderFactory.cs
@@ -0,0 +1,131 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Configuration;
+using System.Globalization;
+using System.IO;
+using System.Web.Mvc.Properties;
+using System.Web.Script.Serialization;
+
+namespace System.Web.Mvc
+{
+ public sealed class JsonValueProviderFactory : ValueProviderFactory
+ {
+ private static void AddToBackingStore(EntryLimitedDictionary backingStore, string prefix, object value)
+ {
+ IDictionary<string, object> d = value as IDictionary<string, object>;
+ if (d != null)
+ {
+ foreach (KeyValuePair<string, object> entry in d)
+ {
+ AddToBackingStore(backingStore, MakePropertyKey(prefix, entry.Key), entry.Value);
+ }
+ return;
+ }
+
+ IList l = value as IList;
+ if (l != null)
+ {
+ for (int i = 0; i < l.Count; i++)
+ {
+ AddToBackingStore(backingStore, MakeArrayKey(prefix, i), l[i]);
+ }
+ return;
+ }
+
+ // primitive
+ backingStore.Add(prefix, value);
+ }
+
+ private static object GetDeserializedObject(ControllerContext controllerContext)
+ {
+ if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
+ {
+ // not JSON request
+ return null;
+ }
+
+ StreamReader reader = new StreamReader(controllerContext.HttpContext.Request.InputStream);
+ string bodyText = reader.ReadToEnd();
+ if (String.IsNullOrEmpty(bodyText))
+ {
+ // no JSON data
+ return null;
+ }
+
+ JavaScriptSerializer serializer = new JavaScriptSerializer();
+ object jsonData = serializer.DeserializeObject(bodyText);
+ return jsonData;
+ }
+
+ public override IValueProvider GetValueProvider(ControllerContext controllerContext)
+ {
+ if (controllerContext == null)
+ {
+ throw new ArgumentNullException("controllerContext");
+ }
+
+ object jsonData = GetDeserializedObject(controllerContext);
+ if (jsonData == null)
+ {
+ return null;
+ }
+
+ Dictionary<string, object> backingStore = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
+ EntryLimitedDictionary backingStoreWrapper = new EntryLimitedDictionary(backingStore);
+ AddToBackingStore(backingStoreWrapper, String.Empty, jsonData);
+ return new DictionaryValueProvider<object>(backingStore, CultureInfo.CurrentCulture);
+ }
+
+ private static string MakeArrayKey(string prefix, int index)
+ {
+ return prefix + "[" + index.ToString(CultureInfo.InvariantCulture) + "]";
+ }
+
+ private static string MakePropertyKey(string prefix, string propertyName)
+ {
+ return (String.IsNullOrEmpty(prefix)) ? propertyName : prefix + "." + propertyName;
+ }
+
+ private class EntryLimitedDictionary
+ {
+ private static int _maximumDepth = GetMaximumDepth();
+ private readonly IDictionary<string, object> _innerDictionary;
+ private int _itemCount = 0;
+
+ public EntryLimitedDictionary(IDictionary<string, object> innerDictionary)
+ {
+ _innerDictionary = innerDictionary;
+ }
+
+ public void Add(string key, object value)
+ {
+ if (++_itemCount > _maximumDepth)
+ {
+ throw new InvalidOperationException(MvcResources.JsonValueProviderFactory_RequestTooLarge);
+ }
+
+ _innerDictionary.Add(key, value);
+ }
+
+ private static int GetMaximumDepth()
+ {
+ NameValueCollection appSettings = ConfigurationManager.AppSettings;
+ if (appSettings != null)
+ {
+ string[] valueArray = appSettings.GetValues("aspnet:MaxJsonDeserializerMembers");
+ if (valueArray != null && valueArray.Length > 0)
+ {
+ int result;
+ if (Int32.TryParse(valueArray[0], out result))
+ {
+ return result;
+ }
+ }
+ }
+
+ return 1000; // Fallback default
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/LinqBinaryModelBinder.cs b/src/System.Web.Mvc/LinqBinaryModelBinder.cs
new file mode 100644
index 00000000..005a36df
--- /dev/null
+++ b/src/System.Web.Mvc/LinqBinaryModelBinder.cs
@@ -0,0 +1,18 @@
+using System.Data.Linq;
+
+namespace System.Web.Mvc
+{
+ public class LinqBinaryModelBinder : ByteArrayModelBinder
+ {
+ public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
+ {
+ byte[] byteValue = (byte[])base.BindModel(controllerContext, bindingContext);
+ if (byteValue == null)
+ {
+ return null;
+ }
+
+ return new Binary(byteValue);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ModelBinderAttribute.cs b/src/System.Web.Mvc/ModelBinderAttribute.cs
new file mode 100644
index 00000000..4e517d3c
--- /dev/null
+++ b/src/System.Web.Mvc/ModelBinderAttribute.cs
@@ -0,0 +1,44 @@
+using System.Globalization;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc
+{
+ [AttributeUsage(ValidTargets, AllowMultiple = false, Inherited = false)]
+ public sealed class ModelBinderAttribute : CustomModelBinderAttribute
+ {
+ public ModelBinderAttribute(Type binderType)
+ {
+ if (binderType == null)
+ {
+ throw new ArgumentNullException("binderType");
+ }
+ if (!typeof(IModelBinder).IsAssignableFrom(binderType))
+ {
+ string message = String.Format(CultureInfo.CurrentCulture,
+ MvcResources.ModelBinderAttribute_TypeNotIModelBinder, binderType.FullName);
+ throw new ArgumentException(message, "binderType");
+ }
+
+ BinderType = binderType;
+ }
+
+ public Type BinderType { get; private set; }
+
+ public override IModelBinder GetBinder()
+ {
+ try
+ {
+ return (IModelBinder)Activator.CreateInstance(BinderType);
+ }
+ catch (Exception ex)
+ {
+ throw new InvalidOperationException(
+ String.Format(
+ CultureInfo.CurrentCulture,
+ MvcResources.ModelBinderAttribute_ErrorCreatingModelBinder,
+ BinderType.FullName),
+ ex);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ModelBinderDictionary.cs b/src/System.Web.Mvc/ModelBinderDictionary.cs
new file mode 100644
index 00000000..273787e5
--- /dev/null
+++ b/src/System.Web.Mvc/ModelBinderDictionary.cs
@@ -0,0 +1,167 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc
+{
+ public class ModelBinderDictionary : IDictionary<Type, IModelBinder>
+ {
+ private readonly Dictionary<Type, IModelBinder> _innerDictionary = new Dictionary<Type, IModelBinder>();
+ private IModelBinder _defaultBinder;
+ private ModelBinderProviderCollection _modelBinderProviders;
+
+ public ModelBinderDictionary()
+ : this(ModelBinderProviders.BinderProviders)
+ {
+ }
+
+ internal ModelBinderDictionary(ModelBinderProviderCollection modelBinderProviders)
+ {
+ _modelBinderProviders = modelBinderProviders;
+ }
+
+ public int Count
+ {
+ get { return _innerDictionary.Count; }
+ }
+
+ public IModelBinder DefaultBinder
+ {
+ get
+ {
+ if (_defaultBinder == null)
+ {
+ _defaultBinder = new DefaultModelBinder();
+ }
+ return _defaultBinder;
+ }
+ set { _defaultBinder = value; }
+ }
+
+ public bool IsReadOnly
+ {
+ get { return ((IDictionary<Type, IModelBinder>)_innerDictionary).IsReadOnly; }
+ }
+
+ public ICollection<Type> Keys
+ {
+ get { return _innerDictionary.Keys; }
+ }
+
+ public ICollection<IModelBinder> Values
+ {
+ get { return _innerDictionary.Values; }
+ }
+
+ public IModelBinder this[Type key]
+ {
+ get
+ {
+ IModelBinder binder;
+ _innerDictionary.TryGetValue(key, out binder);
+ return binder;
+ }
+ set { _innerDictionary[key] = value; }
+ }
+
+ public void Add(KeyValuePair<Type, IModelBinder> item)
+ {
+ ((IDictionary<Type, IModelBinder>)_innerDictionary).Add(item);
+ }
+
+ public void Add(Type key, IModelBinder value)
+ {
+ _innerDictionary.Add(key, value);
+ }
+
+ public void Clear()
+ {
+ _innerDictionary.Clear();
+ }
+
+ public bool Contains(KeyValuePair<Type, IModelBinder> item)
+ {
+ return ((IDictionary<Type, IModelBinder>)_innerDictionary).Contains(item);
+ }
+
+ public bool ContainsKey(Type key)
+ {
+ return _innerDictionary.ContainsKey(key);
+ }
+
+ public void CopyTo(KeyValuePair<Type, IModelBinder>[] array, int arrayIndex)
+ {
+ ((IDictionary<Type, IModelBinder>)_innerDictionary).CopyTo(array, arrayIndex);
+ }
+
+ public IModelBinder GetBinder(Type modelType)
+ {
+ return GetBinder(modelType, true /* fallbackToDefault */);
+ }
+
+ public virtual IModelBinder GetBinder(Type modelType, bool fallbackToDefault)
+ {
+ if (modelType == null)
+ {
+ throw new ArgumentNullException("modelType");
+ }
+
+ return GetBinder(modelType, (fallbackToDefault) ? DefaultBinder : null);
+ }
+
+ private IModelBinder GetBinder(Type modelType, IModelBinder fallbackBinder)
+ {
+ // Try to look up a binder for this type. We use this order of precedence:
+ // 1. Binder returned from provider
+ // 2. Binder registered in the global table
+ // 3. Binder attribute defined on the type
+ // 4. Supplied fallback binder
+
+ IModelBinder binder = _modelBinderProviders.GetBinder(modelType);
+ if (binder != null)
+ {
+ return binder;
+ }
+
+ if (_innerDictionary.TryGetValue(modelType, out binder))
+ {
+ return binder;
+ }
+
+ binder = ModelBinders.GetBinderFromAttributes(modelType,
+ () => String.Format(CultureInfo.CurrentCulture, MvcResources.ModelBinderDictionary_MultipleAttributes, modelType.FullName));
+
+ return binder ?? fallbackBinder;
+ }
+
+ public IEnumerator<KeyValuePair<Type, IModelBinder>> GetEnumerator()
+ {
+ return _innerDictionary.GetEnumerator();
+ }
+
+ public bool Remove(KeyValuePair<Type, IModelBinder> item)
+ {
+ return ((IDictionary<Type, IModelBinder>)_innerDictionary).Remove(item);
+ }
+
+ public bool Remove(Type key)
+ {
+ return _innerDictionary.Remove(key);
+ }
+
+ public bool TryGetValue(Type key, out IModelBinder value)
+ {
+ return _innerDictionary.TryGetValue(key, out value);
+ }
+
+ #region IEnumerable Members
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return ((IEnumerable)_innerDictionary).GetEnumerator();
+ }
+
+ #endregion
+ }
+}
diff --git a/src/System.Web.Mvc/ModelBinderProviderCollection.cs b/src/System.Web.Mvc/ModelBinderProviderCollection.cs
new file mode 100644
index 00000000..eb3117a3
--- /dev/null
+++ b/src/System.Web.Mvc/ModelBinderProviderCollection.cs
@@ -0,0 +1,66 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+
+namespace System.Web.Mvc
+{
+ public class ModelBinderProviderCollection : Collection<IModelBinderProvider>
+ {
+ private IResolver<IEnumerable<IModelBinderProvider>> _serviceResolver;
+
+ public ModelBinderProviderCollection()
+ {
+ _serviceResolver = new MultiServiceResolver<IModelBinderProvider>(() => Items);
+ }
+
+ public ModelBinderProviderCollection(IList<IModelBinderProvider> list)
+ : base(list)
+ {
+ _serviceResolver = new MultiServiceResolver<IModelBinderProvider>(() => Items);
+ }
+
+ internal ModelBinderProviderCollection(IResolver<IEnumerable<IModelBinderProvider>> resolver, params IModelBinderProvider[] providers)
+ : base(providers)
+ {
+ _serviceResolver = resolver ?? new MultiServiceResolver<IModelBinderProvider>(() => Items);
+ }
+
+ private IEnumerable<IModelBinderProvider> CombinedItems
+ {
+ get { return _serviceResolver.Current; }
+ }
+
+ protected override void InsertItem(int index, IModelBinderProvider item)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+ base.InsertItem(index, item);
+ }
+
+ protected override void SetItem(int index, IModelBinderProvider item)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+ base.SetItem(index, item);
+ }
+
+ public IModelBinder GetBinder(Type modelType)
+ {
+ if (modelType == null)
+ {
+ throw new ArgumentNullException("modelType");
+ }
+
+ var modelBinders = from providers in CombinedItems
+ let modelBinder = providers.GetBinder(modelType)
+ where modelBinder != null
+ select modelBinder;
+
+ return modelBinders.FirstOrDefault();
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ModelBinderProviders.cs b/src/System.Web.Mvc/ModelBinderProviders.cs
new file mode 100644
index 00000000..dcbed64e
--- /dev/null
+++ b/src/System.Web.Mvc/ModelBinderProviders.cs
@@ -0,0 +1,14 @@
+namespace System.Web.Mvc
+{
+ public static class ModelBinderProviders
+ {
+ private static readonly ModelBinderProviderCollection _binderProviders = new ModelBinderProviderCollection
+ {
+ };
+
+ public static ModelBinderProviderCollection BinderProviders
+ {
+ get { return _binderProviders; }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ModelBinders.cs b/src/System.Web.Mvc/ModelBinders.cs
new file mode 100644
index 00000000..da986a42
--- /dev/null
+++ b/src/System.Web.Mvc/ModelBinders.cs
@@ -0,0 +1,71 @@
+using System.ComponentModel;
+using System.Data.Linq;
+using System.Linq;
+using System.Reflection;
+using System.Threading;
+
+namespace System.Web.Mvc
+{
+ public static class ModelBinders
+ {
+ private static readonly ModelBinderDictionary _binders = CreateDefaultBinderDictionary();
+
+ public static ModelBinderDictionary Binders
+ {
+ get { return _binders; }
+ }
+
+ internal static IModelBinder GetBinderFromAttributes(Type type, Func<string> errorMessageAccessor)
+ {
+ AttributeCollection allAttrs = TypeDescriptorHelper.Get(type).GetAttributes();
+ CustomModelBinderAttribute[] filteredAttrs = allAttrs.OfType<CustomModelBinderAttribute>().ToArray();
+ return GetBinderFromAttributesImpl(filteredAttrs, errorMessageAccessor);
+ }
+
+ internal static IModelBinder GetBinderFromAttributes(ICustomAttributeProvider element, Func<string> errorMessageAccessor)
+ {
+ CustomModelBinderAttribute[] attrs = (CustomModelBinderAttribute[])element.GetCustomAttributes(typeof(CustomModelBinderAttribute), true /* inherit */);
+ return GetBinderFromAttributesImpl(attrs, errorMessageAccessor);
+ }
+
+ private static IModelBinder GetBinderFromAttributesImpl(CustomModelBinderAttribute[] attrs, Func<string> errorMessageAccessor)
+ {
+ // this method is used to get a custom binder based on the attributes of the element passed to it.
+ // it will return null if a binder cannot be detected based on the attributes alone.
+
+ if (attrs == null)
+ {
+ return null;
+ }
+
+ switch (attrs.Length)
+ {
+ case 0:
+ return null;
+
+ case 1:
+ IModelBinder binder = attrs[0].GetBinder();
+ return binder;
+
+ default:
+ string errorMessage = errorMessageAccessor();
+ throw new InvalidOperationException(errorMessage);
+ }
+ }
+
+ private static ModelBinderDictionary CreateDefaultBinderDictionary()
+ {
+ // We can't add a binder to the HttpPostedFileBase type as an attribute, so we'll just
+ // prepopulate the dictionary as a convenience to users.
+
+ ModelBinderDictionary binders = new ModelBinderDictionary()
+ {
+ { typeof(HttpPostedFileBase), new HttpPostedFileBaseModelBinder() },
+ { typeof(byte[]), new ByteArrayModelBinder() },
+ { typeof(Binary), new LinqBinaryModelBinder() },
+ { typeof(CancellationToken), new CancellationTokenModelBinder() }
+ };
+ return binders;
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ModelBindingContext.cs b/src/System.Web.Mvc/ModelBindingContext.cs
new file mode 100644
index 00000000..e57be059
--- /dev/null
+++ b/src/System.Web.Mvc/ModelBindingContext.cs
@@ -0,0 +1,138 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc
+{
+ public class ModelBindingContext
+ {
+ private static readonly Predicate<string> _defaultPropertyFilter = _ => true;
+
+ private string _modelName;
+ private ModelStateDictionary _modelState;
+ private Predicate<string> _propertyFilter;
+ private Dictionary<string, ModelMetadata> _propertyMetadata;
+
+ public ModelBindingContext()
+ : this(null)
+ {
+ }
+
+ // copies certain values that won't change between parent and child objects,
+ // e.g. ValueProvider, ModelState
+ public ModelBindingContext(ModelBindingContext bindingContext)
+ {
+ if (bindingContext != null)
+ {
+ ModelState = bindingContext.ModelState;
+ ValueProvider = bindingContext.ValueProvider;
+ }
+ }
+
+ public bool FallbackToEmptyPrefix { get; set; }
+
+ [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "value", Justification = "Cannot remove setter as that's a breaking change")]
+ public object Model
+ {
+ get { return ModelMetadata.Model; }
+ set { throw new InvalidOperationException(MvcResources.ModelMetadata_PropertyNotSettable); }
+ }
+
+ public ModelMetadata ModelMetadata { get; set; }
+
+ public string ModelName
+ {
+ get
+ {
+ if (_modelName == null)
+ {
+ _modelName = String.Empty;
+ }
+ return _modelName;
+ }
+ set { _modelName = value; }
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "The containing type is mutable.")]
+ public ModelStateDictionary ModelState
+ {
+ get
+ {
+ if (_modelState == null)
+ {
+ _modelState = new ModelStateDictionary();
+ }
+ return _modelState;
+ }
+ set { _modelState = value; }
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "value", Justification = "Cannot remove setter as that's a breaking change")]
+ public Type ModelType
+ {
+ get { return ModelMetadata.ModelType; }
+ set { throw new InvalidOperationException(MvcResources.ModelMetadata_PropertyNotSettable); }
+ }
+
+ public Predicate<string> PropertyFilter
+ {
+ get
+ {
+ if (_propertyFilter == null)
+ {
+ _propertyFilter = _defaultPropertyFilter;
+ }
+ return _propertyFilter;
+ }
+ set { _propertyFilter = value; }
+ }
+
+ public IDictionary<string, ModelMetadata> PropertyMetadata
+ {
+ get
+ {
+ if (_propertyMetadata == null)
+ {
+ _propertyMetadata = ModelMetadata.Properties.ToDictionary(m => m.PropertyName, StringComparer.OrdinalIgnoreCase);
+ }
+
+ return _propertyMetadata;
+ }
+ }
+
+ public IValueProvider ValueProvider { get; set; }
+
+ internal IUnvalidatedValueProvider UnvalidatedValueProvider
+ {
+ get { return (ValueProvider as IUnvalidatedValueProvider) ?? new UnvalidatedValueProviderWrapper(ValueProvider); }
+ }
+
+ // Used to wrap an IValueProvider in an IUnvalidatedValueProvider
+ private sealed class UnvalidatedValueProviderWrapper : IValueProvider, IUnvalidatedValueProvider
+ {
+ private readonly IValueProvider _backingProvider;
+
+ public UnvalidatedValueProviderWrapper(IValueProvider backingProvider)
+ {
+ _backingProvider = backingProvider;
+ }
+
+ public ValueProviderResult GetValue(string key, bool skipValidation)
+ {
+ // 'skipValidation' isn't understood by the backing provider and can be ignored
+ return GetValue(key);
+ }
+
+ public bool ContainsPrefix(string prefix)
+ {
+ return _backingProvider.ContainsPrefix(prefix);
+ }
+
+ public ValueProviderResult GetValue(string key)
+ {
+ return _backingProvider.GetValue(key);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ModelError.cs b/src/System.Web.Mvc/ModelError.cs
new file mode 100644
index 00000000..d05f2509
--- /dev/null
+++ b/src/System.Web.Mvc/ModelError.cs
@@ -0,0 +1,31 @@
+namespace System.Web.Mvc
+{
+ [Serializable]
+ public class ModelError
+ {
+ public ModelError(Exception exception)
+ : this(exception, null /* errorMessage */)
+ {
+ }
+
+ public ModelError(Exception exception, string errorMessage)
+ : this(errorMessage)
+ {
+ if (exception == null)
+ {
+ throw new ArgumentNullException("exception");
+ }
+
+ Exception = exception;
+ }
+
+ public ModelError(string errorMessage)
+ {
+ ErrorMessage = errorMessage ?? String.Empty;
+ }
+
+ public Exception Exception { get; private set; }
+
+ public string ErrorMessage { get; private set; }
+ }
+}
diff --git a/src/System.Web.Mvc/ModelErrorCollection.cs b/src/System.Web.Mvc/ModelErrorCollection.cs
new file mode 100644
index 00000000..e6a7867d
--- /dev/null
+++ b/src/System.Web.Mvc/ModelErrorCollection.cs
@@ -0,0 +1,18 @@
+using System.Collections.ObjectModel;
+
+namespace System.Web.Mvc
+{
+ [Serializable]
+ public class ModelErrorCollection : Collection<ModelError>
+ {
+ public void Add(Exception exception)
+ {
+ Add(new ModelError(exception));
+ }
+
+ public void Add(string errorMessage)
+ {
+ Add(new ModelError(errorMessage));
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ModelMetadata.cs b/src/System.Web.Mvc/ModelMetadata.cs
new file mode 100644
index 00000000..4a2ad50c
--- /dev/null
+++ b/src/System.Web.Mvc/ModelMetadata.cs
@@ -0,0 +1,407 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Web.Mvc.ExpressionUtil;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc
+{
+ public class ModelMetadata
+ {
+ public const int DefaultOrder = 10000;
+
+ private readonly Type _containerType;
+ private readonly Type _modelType;
+ private readonly string _propertyName;
+
+ /// <summary>
+ /// Explicit backing store for the things we want initialized by default, so don't have to call
+ /// the protected virtual setters of an auto-generated property
+ /// </summary>
+ private Dictionary<string, object> _additionalValues = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
+ private bool _convertEmptyStringToNull = true;
+ private bool _isRequired;
+ private object _model;
+ private Func<object> _modelAccessor;
+ private int _order = DefaultOrder;
+ private IEnumerable<ModelMetadata> _properties;
+ private Type _realModelType;
+ private bool _requestValidationEnabled = true;
+ private bool _showForDisplay = true;
+ private bool _showForEdit = true;
+ private string _simpleDisplayText;
+
+ public ModelMetadata(ModelMetadataProvider provider, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
+ {
+ if (provider == null)
+ {
+ throw new ArgumentNullException("provider");
+ }
+ if (modelType == null)
+ {
+ throw new ArgumentNullException("modelType");
+ }
+
+ Provider = provider;
+
+ _containerType = containerType;
+ _isRequired = !TypeHelpers.TypeAllowsNullValue(modelType);
+ _modelAccessor = modelAccessor;
+ _modelType = modelType;
+ _propertyName = propertyName;
+ }
+
+ public virtual Dictionary<string, object> AdditionalValues
+ {
+ get { return _additionalValues; }
+ }
+
+ public Type ContainerType
+ {
+ get { return _containerType; }
+ }
+
+ public virtual bool ConvertEmptyStringToNull
+ {
+ get { return _convertEmptyStringToNull; }
+ set { _convertEmptyStringToNull = value; }
+ }
+
+ public virtual string DataTypeName { get; set; }
+
+ public virtual string Description { get; set; }
+
+ public virtual string DisplayFormatString { get; set; }
+
+ [SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods", Justification = "The method is a delegating helper to choose among multiple property values")]
+ public virtual string DisplayName { get; set; }
+
+ public virtual string EditFormatString { get; set; }
+
+ public virtual bool HideSurroundingHtml { get; set; }
+
+ public virtual bool IsComplexType
+ {
+ get { return !(TypeDescriptor.GetConverter(ModelType).CanConvertFrom(typeof(string))); }
+ }
+
+ public bool IsNullableValueType
+ {
+ get { return TypeHelpers.IsNullableValueType(ModelType); }
+ }
+
+ public virtual bool IsReadOnly { get; set; }
+
+ public virtual bool IsRequired
+ {
+ get { return _isRequired; }
+ set { _isRequired = value; }
+ }
+
+ public object Model
+ {
+ get
+ {
+ if (_modelAccessor != null)
+ {
+ _model = _modelAccessor();
+ _modelAccessor = null;
+ }
+ return _model;
+ }
+ set
+ {
+ _model = value;
+ _modelAccessor = null;
+ _properties = null;
+ _realModelType = null;
+ }
+ }
+
+ public Type ModelType
+ {
+ get { return _modelType; }
+ }
+
+ public virtual string NullDisplayText { get; set; }
+
+ public virtual int Order
+ {
+ get { return _order; }
+ set { _order = value; }
+ }
+
+ public virtual IEnumerable<ModelMetadata> Properties
+ {
+ get
+ {
+ if (_properties == null)
+ {
+ _properties = Provider.GetMetadataForProperties(Model, RealModelType).OrderBy(m => m.Order);
+ }
+ return _properties;
+ }
+ }
+
+ public string PropertyName
+ {
+ get { return _propertyName; }
+ }
+
+ protected ModelMetadataProvider Provider { get; set; }
+
+ internal Type RealModelType
+ {
+ get
+ {
+ if (_realModelType == null)
+ {
+ _realModelType = ModelType;
+
+ // Don't call GetType() if the model is Nullable<T>, because it will
+ // turn Nullable<T> into T for non-null values
+ if (Model != null && !TypeHelpers.IsNullableValueType(ModelType))
+ {
+ _realModelType = Model.GetType();
+ }
+ }
+
+ return _realModelType;
+ }
+ }
+
+ public virtual bool RequestValidationEnabled
+ {
+ get { return _requestValidationEnabled; }
+ set { _requestValidationEnabled = value; }
+ }
+
+ public virtual string ShortDisplayName { get; set; }
+
+ public virtual bool ShowForDisplay
+ {
+ get { return _showForDisplay; }
+ set { _showForDisplay = value; }
+ }
+
+ public virtual bool ShowForEdit
+ {
+ get { return _showForEdit; }
+ set { _showForEdit = value; }
+ }
+
+ [SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods", Justification = "This property delegates to the method when the user has not yet set a simple display text value.")]
+ public virtual string SimpleDisplayText
+ {
+ get
+ {
+ if (_simpleDisplayText == null)
+ {
+ _simpleDisplayText = GetSimpleDisplayText();
+ }
+ return _simpleDisplayText;
+ }
+ set { _simpleDisplayText = value; }
+ }
+
+ public virtual string TemplateHint { get; set; }
+
+ public virtual string Watermark { get; set; }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
+ public static ModelMetadata FromLambdaExpression<TParameter, TValue>(Expression<Func<TParameter, TValue>> expression,
+ ViewDataDictionary<TParameter> viewData)
+ {
+ return FromLambdaExpression(expression, viewData, metadataProvider: null);
+ }
+
+ internal static ModelMetadata FromLambdaExpression<TParameter, TValue>(Expression<Func<TParameter, TValue>> expression,
+ ViewDataDictionary<TParameter> viewData,
+ ModelMetadataProvider metadataProvider)
+ {
+ if (expression == null)
+ {
+ throw new ArgumentNullException("expression");
+ }
+ if (viewData == null)
+ {
+ throw new ArgumentNullException("viewData");
+ }
+
+ string propertyName = null;
+ Type containerType = null;
+ bool legalExpression = false;
+
+ // Need to verify the expression is valid; it needs to at least end in something
+ // that we can convert to a meaningful string for model binding purposes
+
+ switch (expression.Body.NodeType)
+ {
+ case ExpressionType.ArrayIndex:
+ // ArrayIndex always means a single-dimensional indexer; multi-dimensional indexer is a method call to Get()
+ legalExpression = true;
+ break;
+
+ case ExpressionType.Call:
+ // Only legal method call is a single argument indexer/DefaultMember call
+ legalExpression = ExpressionHelper.IsSingleArgumentIndexer(expression.Body);
+ break;
+
+ case ExpressionType.MemberAccess:
+ // Property/field access is always legal
+ MemberExpression memberExpression = (MemberExpression)expression.Body;
+ propertyName = memberExpression.Member is PropertyInfo ? memberExpression.Member.Name : null;
+ containerType = memberExpression.Expression.Type;
+ legalExpression = true;
+ break;
+
+ case ExpressionType.Parameter:
+ // Parameter expression means "model => model", so we delegate to FromModel
+ return FromModel(viewData, metadataProvider);
+ }
+
+ if (!legalExpression)
+ {
+ throw new InvalidOperationException(MvcResources.TemplateHelpers_TemplateLimitations);
+ }
+
+ TParameter container = viewData.Model;
+ Func<object> modelAccessor = () =>
+ {
+ try
+ {
+ return CachedExpressionCompiler.Process(expression)(container);
+ }
+ catch (NullReferenceException)
+ {
+ return null;
+ }
+ };
+
+ return GetMetadataFromProvider(modelAccessor, typeof(TValue), propertyName, containerType, metadataProvider);
+ }
+
+ private static ModelMetadata FromModel(ViewDataDictionary viewData, ModelMetadataProvider metadataProvider)
+ {
+ return viewData.ModelMetadata ?? GetMetadataFromProvider(null, typeof(string), null, null, metadataProvider);
+ }
+
+ public static ModelMetadata FromStringExpression(string expression, ViewDataDictionary viewData)
+ {
+ return FromStringExpression(expression, viewData, metadataProvider: null);
+ }
+
+ internal static ModelMetadata FromStringExpression(string expression, ViewDataDictionary viewData, ModelMetadataProvider metadataProvider)
+ {
+ if (expression == null)
+ {
+ throw new ArgumentNullException("expression");
+ }
+ if (viewData == null)
+ {
+ throw new ArgumentNullException("viewData");
+ }
+ if (expression.Length == 0)
+ {
+ // Empty string really means "model metadata for the current model"
+ return FromModel(viewData, metadataProvider);
+ }
+
+ ViewDataInfo vdi = viewData.GetViewDataInfo(expression);
+ Type containerType = null;
+ Type modelType = null;
+ Func<object> modelAccessor = null;
+ string propertyName = null;
+
+ if (vdi != null)
+ {
+ if (vdi.Container != null)
+ {
+ containerType = vdi.Container.GetType();
+ }
+
+ modelAccessor = () => vdi.Value;
+
+ if (vdi.PropertyDescriptor != null)
+ {
+ propertyName = vdi.PropertyDescriptor.Name;
+ modelType = vdi.PropertyDescriptor.PropertyType;
+ }
+ else if (vdi.Value != null)
+ {
+ // We only need to delay accessing properties (for LINQ to SQL)
+ modelType = vdi.Value.GetType();
+ }
+ }
+ else if (viewData.ModelMetadata != null)
+ {
+ // Try getting a property from ModelMetadata if we couldn't find an answer in ViewData
+ ModelMetadata propertyMetadata = viewData.ModelMetadata.Properties.Where(p => p.PropertyName == expression).FirstOrDefault();
+ if (propertyMetadata != null)
+ {
+ return propertyMetadata;
+ }
+ }
+
+ return GetMetadataFromProvider(modelAccessor, modelType ?? typeof(string), propertyName, containerType, metadataProvider);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "The method is a delegating helper to choose among multiple property values")]
+ public string GetDisplayName()
+ {
+ return DisplayName ?? PropertyName ?? ModelType.Name;
+ }
+
+ private static ModelMetadata GetMetadataFromProvider(Func<object> modelAccessor, Type modelType, string propertyName, Type containerType, ModelMetadataProvider metadataProvider)
+ {
+ metadataProvider = metadataProvider ?? ModelMetadataProviders.Current;
+ if (containerType != null && !String.IsNullOrEmpty(propertyName))
+ {
+ return metadataProvider.GetMetadataForProperty(modelAccessor, containerType, propertyName);
+ }
+ return metadataProvider.GetMetadataForType(modelAccessor, modelType);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This method is used to resolve the simple display text when it was not explicitly set through other means.")]
+ protected virtual string GetSimpleDisplayText()
+ {
+ if (Model == null)
+ {
+ return NullDisplayText;
+ }
+
+ string toStringResult = Convert.ToString(Model, CultureInfo.CurrentCulture);
+ if (toStringResult == null)
+ {
+ return String.Empty;
+ }
+
+ if (!toStringResult.Equals(Model.GetType().FullName, StringComparison.Ordinal))
+ {
+ return toStringResult;
+ }
+
+ ModelMetadata firstProperty = Properties.FirstOrDefault();
+ if (firstProperty == null)
+ {
+ return String.Empty;
+ }
+
+ if (firstProperty.Model == null)
+ {
+ return firstProperty.NullDisplayText;
+ }
+
+ return Convert.ToString(firstProperty.Model, CultureInfo.CurrentCulture);
+ }
+
+ public virtual IEnumerable<ModelValidator> GetValidators(ControllerContext context)
+ {
+ return ModelValidatorProviders.Providers.GetValidators(this, context);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ModelMetadataProvider.cs b/src/System.Web.Mvc/ModelMetadataProvider.cs
new file mode 100644
index 00000000..0db73fba
--- /dev/null
+++ b/src/System.Web.Mvc/ModelMetadataProvider.cs
@@ -0,0 +1,13 @@
+using System.Collections.Generic;
+
+namespace System.Web.Mvc
+{
+ public abstract class ModelMetadataProvider
+ {
+ public abstract IEnumerable<ModelMetadata> GetMetadataForProperties(object container, Type containerType);
+
+ public abstract ModelMetadata GetMetadataForProperty(Func<object> modelAccessor, Type containerType, string propertyName);
+
+ public abstract ModelMetadata GetMetadataForType(Func<object> modelAccessor, Type modelType);
+ }
+}
diff --git a/src/System.Web.Mvc/ModelMetadataProviders.cs b/src/System.Web.Mvc/ModelMetadataProviders.cs
new file mode 100644
index 00000000..d9cd8b92
--- /dev/null
+++ b/src/System.Web.Mvc/ModelMetadataProviders.cs
@@ -0,0 +1,29 @@
+namespace System.Web.Mvc
+{
+ public class ModelMetadataProviders
+ {
+ private static ModelMetadataProviders _instance = new ModelMetadataProviders();
+ private ModelMetadataProvider _currentProvider;
+ private IResolver<ModelMetadataProvider> _resolver;
+
+ internal ModelMetadataProviders(IResolver<ModelMetadataProvider> resolver = null)
+ {
+ _resolver = resolver ?? new SingleServiceResolver<ModelMetadataProvider>(
+ () => _currentProvider,
+ new CachedDataAnnotationsModelMetadataProvider(),
+ "ModelMetadataProviders.Current");
+ }
+
+ public static ModelMetadataProvider Current
+ {
+ get { return _instance.CurrentInternal; }
+ set { _instance.CurrentInternal = value; }
+ }
+
+ internal ModelMetadataProvider CurrentInternal
+ {
+ get { return _resolver.Current; }
+ set { _currentProvider = value ?? new EmptyModelMetadataProvider(); }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ModelState.cs b/src/System.Web.Mvc/ModelState.cs
new file mode 100644
index 00000000..ae1d28d4
--- /dev/null
+++ b/src/System.Web.Mvc/ModelState.cs
@@ -0,0 +1,15 @@
+namespace System.Web.Mvc
+{
+ [Serializable]
+ public class ModelState
+ {
+ private ModelErrorCollection _errors = new ModelErrorCollection();
+
+ public ValueProviderResult Value { get; set; }
+
+ public ModelErrorCollection Errors
+ {
+ get { return _errors; }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ModelStateDictionary.cs b/src/System.Web.Mvc/ModelStateDictionary.cs
new file mode 100644
index 00000000..f1b149f2
--- /dev/null
+++ b/src/System.Web.Mvc/ModelStateDictionary.cs
@@ -0,0 +1,180 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace System.Web.Mvc
+{
+ [Serializable]
+ public class ModelStateDictionary : IDictionary<string, ModelState>
+ {
+ private readonly Dictionary<string, ModelState> _innerDictionary = new Dictionary<string, ModelState>(StringComparer.OrdinalIgnoreCase);
+
+ public ModelStateDictionary()
+ {
+ }
+
+ public ModelStateDictionary(ModelStateDictionary dictionary)
+ {
+ if (dictionary == null)
+ {
+ throw new ArgumentNullException("dictionary");
+ }
+
+ foreach (var entry in dictionary)
+ {
+ _innerDictionary.Add(entry.Key, entry.Value);
+ }
+ }
+
+ public int Count
+ {
+ get { return _innerDictionary.Count; }
+ }
+
+ public bool IsReadOnly
+ {
+ get { return ((IDictionary<string, ModelState>)_innerDictionary).IsReadOnly; }
+ }
+
+ public bool IsValid
+ {
+ get { return Values.All(modelState => modelState.Errors.Count == 0); }
+ }
+
+ public ICollection<string> Keys
+ {
+ get { return _innerDictionary.Keys; }
+ }
+
+ public ICollection<ModelState> Values
+ {
+ get { return _innerDictionary.Values; }
+ }
+
+ public ModelState this[string key]
+ {
+ get
+ {
+ ModelState value;
+ _innerDictionary.TryGetValue(key, out value);
+ return value;
+ }
+ set { _innerDictionary[key] = value; }
+ }
+
+ public void Add(KeyValuePair<string, ModelState> item)
+ {
+ ((IDictionary<string, ModelState>)_innerDictionary).Add(item);
+ }
+
+ public void Add(string key, ModelState value)
+ {
+ _innerDictionary.Add(key, value);
+ }
+
+ public void AddModelError(string key, Exception exception)
+ {
+ GetModelStateForKey(key).Errors.Add(exception);
+ }
+
+ public void AddModelError(string key, string errorMessage)
+ {
+ GetModelStateForKey(key).Errors.Add(errorMessage);
+ }
+
+ public void Clear()
+ {
+ _innerDictionary.Clear();
+ }
+
+ public bool Contains(KeyValuePair<string, ModelState> item)
+ {
+ return ((IDictionary<string, ModelState>)_innerDictionary).Contains(item);
+ }
+
+ public bool ContainsKey(string key)
+ {
+ return _innerDictionary.ContainsKey(key);
+ }
+
+ public void CopyTo(KeyValuePair<string, ModelState>[] array, int arrayIndex)
+ {
+ ((IDictionary<string, ModelState>)_innerDictionary).CopyTo(array, arrayIndex);
+ }
+
+ public IEnumerator<KeyValuePair<string, ModelState>> GetEnumerator()
+ {
+ return _innerDictionary.GetEnumerator();
+ }
+
+ private ModelState GetModelStateForKey(string key)
+ {
+ if (key == null)
+ {
+ throw new ArgumentNullException("key");
+ }
+
+ ModelState modelState;
+ if (!TryGetValue(key, out modelState))
+ {
+ modelState = new ModelState();
+ this[key] = modelState;
+ }
+
+ return modelState;
+ }
+
+ public bool IsValidField(string key)
+ {
+ if (key == null)
+ {
+ throw new ArgumentNullException("key");
+ }
+
+ // if the key is not found in the dictionary, we just say that it's valid (since there are no errors)
+ return DictionaryHelpers.FindKeysWithPrefix(this, key).All(entry => entry.Value.Errors.Count == 0);
+ }
+
+ public void Merge(ModelStateDictionary dictionary)
+ {
+ if (dictionary == null)
+ {
+ return;
+ }
+
+ foreach (var entry in dictionary)
+ {
+ this[entry.Key] = entry.Value;
+ }
+ }
+
+ public bool Remove(KeyValuePair<string, ModelState> item)
+ {
+ return ((IDictionary<string, ModelState>)_innerDictionary).Remove(item);
+ }
+
+ public bool Remove(string key)
+ {
+ return _innerDictionary.Remove(key);
+ }
+
+ public void SetModelValue(string key, ValueProviderResult value)
+ {
+ GetModelStateForKey(key).Value = value;
+ }
+
+ public bool TryGetValue(string key, out ModelState value)
+ {
+ return _innerDictionary.TryGetValue(key, out value);
+ }
+
+ #region IEnumerable Members
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return ((IEnumerable)_innerDictionary).GetEnumerator();
+ }
+
+ #endregion
+ }
+}
diff --git a/src/System.Web.Mvc/ModelValidationResult.cs b/src/System.Web.Mvc/ModelValidationResult.cs
new file mode 100644
index 00000000..f7815371
--- /dev/null
+++ b/src/System.Web.Mvc/ModelValidationResult.cs
@@ -0,0 +1,20 @@
+namespace System.Web.Mvc
+{
+ public class ModelValidationResult
+ {
+ private string _memberName;
+ private string _message;
+
+ public string MemberName
+ {
+ get { return _memberName ?? String.Empty; }
+ set { _memberName = value; }
+ }
+
+ public string Message
+ {
+ get { return _message ?? String.Empty; }
+ set { _message = value; }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ModelValidator.cs b/src/System.Web.Mvc/ModelValidator.cs
new file mode 100644
index 00000000..e5c9774c
--- /dev/null
+++ b/src/System.Web.Mvc/ModelValidator.cs
@@ -0,0 +1,86 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+
+namespace System.Web.Mvc
+{
+ public abstract class ModelValidator
+ {
+ protected ModelValidator(ModelMetadata metadata, ControllerContext controllerContext)
+ {
+ if (metadata == null)
+ {
+ throw new ArgumentNullException("metadata");
+ }
+ if (controllerContext == null)
+ {
+ throw new ArgumentNullException("controllerContext");
+ }
+
+ Metadata = metadata;
+ ControllerContext = controllerContext;
+ }
+
+ protected internal ControllerContext ControllerContext { get; private set; }
+
+ public virtual bool IsRequired
+ {
+ get { return false; }
+ }
+
+ protected internal ModelMetadata Metadata { get; private set; }
+
+ [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This method may perform non-trivial work.")]
+ public virtual IEnumerable<ModelClientValidationRule> GetClientValidationRules()
+ {
+ return Enumerable.Empty<ModelClientValidationRule>();
+ }
+
+ public static ModelValidator GetModelValidator(ModelMetadata metadata, ControllerContext context)
+ {
+ return new CompositeModelValidator(metadata, context);
+ }
+
+ public abstract IEnumerable<ModelValidationResult> Validate(object container);
+
+ private class CompositeModelValidator : ModelValidator
+ {
+ public CompositeModelValidator(ModelMetadata metadata, ControllerContext controllerContext)
+ : base(metadata, controllerContext)
+ {
+ }
+
+ public override IEnumerable<ModelValidationResult> Validate(object container)
+ {
+ bool propertiesValid = true;
+
+ foreach (ModelMetadata propertyMetadata in Metadata.Properties)
+ {
+ foreach (ModelValidator propertyValidator in propertyMetadata.GetValidators(ControllerContext))
+ {
+ foreach (ModelValidationResult propertyResult in propertyValidator.Validate(Metadata.Model))
+ {
+ propertiesValid = false;
+ yield return new ModelValidationResult
+ {
+ MemberName = DefaultModelBinder.CreateSubPropertyName(propertyMetadata.PropertyName, propertyResult.MemberName),
+ Message = propertyResult.Message
+ };
+ }
+ }
+ }
+
+ if (propertiesValid)
+ {
+ foreach (ModelValidator typeValidator in Metadata.GetValidators(ControllerContext))
+ {
+ foreach (ModelValidationResult typeResult in typeValidator.Validate(container))
+ {
+ yield return typeResult;
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ModelValidatorProvider.cs b/src/System.Web.Mvc/ModelValidatorProvider.cs
new file mode 100644
index 00000000..932a091f
--- /dev/null
+++ b/src/System.Web.Mvc/ModelValidatorProvider.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace System.Web.Mvc
+{
+ public abstract class ModelValidatorProvider
+ {
+ public abstract IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context);
+ }
+}
diff --git a/src/System.Web.Mvc/ModelValidatorProviderCollection.cs b/src/System.Web.Mvc/ModelValidatorProviderCollection.cs
new file mode 100644
index 00000000..666fdbf0
--- /dev/null
+++ b/src/System.Web.Mvc/ModelValidatorProviderCollection.cs
@@ -0,0 +1,56 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+
+namespace System.Web.Mvc
+{
+ public class ModelValidatorProviderCollection : Collection<ModelValidatorProvider>
+ {
+ private IResolver<IEnumerable<ModelValidatorProvider>> _serviceResolver;
+
+ public ModelValidatorProviderCollection()
+ {
+ _serviceResolver = new MultiServiceResolver<ModelValidatorProvider>(() => Items);
+ }
+
+ public ModelValidatorProviderCollection(IList<ModelValidatorProvider> list)
+ : base(list)
+ {
+ _serviceResolver = new MultiServiceResolver<ModelValidatorProvider>(() => Items);
+ }
+
+ internal ModelValidatorProviderCollection(IResolver<IEnumerable<ModelValidatorProvider>> serviceResolver, params ModelValidatorProvider[] validatorProvidors)
+ : base(validatorProvidors)
+ {
+ _serviceResolver = serviceResolver ?? new MultiServiceResolver<ModelValidatorProvider>(() => Items);
+ }
+
+ private IEnumerable<ModelValidatorProvider> CombinedItems
+ {
+ get { return _serviceResolver.Current; }
+ }
+
+ protected override void InsertItem(int index, ModelValidatorProvider item)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+ base.InsertItem(index, item);
+ }
+
+ protected override void SetItem(int index, ModelValidatorProvider item)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+ base.SetItem(index, item);
+ }
+
+ public IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context)
+ {
+ return CombinedItems.SelectMany(provider => provider.GetValidators(metadata, context));
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ModelValidatorProviders.cs b/src/System.Web.Mvc/ModelValidatorProviders.cs
new file mode 100644
index 00000000..66852578
--- /dev/null
+++ b/src/System.Web.Mvc/ModelValidatorProviders.cs
@@ -0,0 +1,17 @@
+namespace System.Web.Mvc
+{
+ public static class ModelValidatorProviders
+ {
+ private static readonly ModelValidatorProviderCollection _providers = new ModelValidatorProviderCollection()
+ {
+ new DataAnnotationsModelValidatorProvider(),
+ new DataErrorInfoModelValidatorProvider(),
+ new ClientDataTypeModelValidatorProvider()
+ };
+
+ public static ModelValidatorProviderCollection Providers
+ {
+ get { return _providers; }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/MultiSelectList.cs b/src/System.Web.Mvc/MultiSelectList.cs
new file mode 100644
index 00000000..eedb0100
--- /dev/null
+++ b/src/System.Web.Mvc/MultiSelectList.cs
@@ -0,0 +1,118 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using System.Web.UI;
+
+namespace System.Web.Mvc
+{
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Multi", Justification = "FxCop won't accept this in the custom dictionary, so we're suppressing it in source")]
+ [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix", Justification = "This is a shipped API")]
+ public class MultiSelectList : IEnumerable<SelectListItem>
+ {
+ public MultiSelectList(IEnumerable items)
+ : this(items, null /* selectedValues */)
+ {
+ }
+
+ public MultiSelectList(IEnumerable items, IEnumerable selectedValues)
+ : this(items, null /* dataValuefield */, null /* dataTextField */, selectedValues)
+ {
+ }
+
+ public MultiSelectList(IEnumerable items, string dataValueField, string dataTextField)
+ : this(items, dataValueField, dataTextField, null /* selectedValues */)
+ {
+ }
+
+ public MultiSelectList(IEnumerable items, string dataValueField, string dataTextField, IEnumerable selectedValues)
+ {
+ if (items == null)
+ {
+ throw new ArgumentNullException("items");
+ }
+
+ Items = items;
+ DataValueField = dataValueField;
+ DataTextField = dataTextField;
+ SelectedValues = selectedValues;
+ }
+
+ public string DataTextField { get; private set; }
+
+ public string DataValueField { get; private set; }
+
+ public IEnumerable Items { get; private set; }
+
+ public IEnumerable SelectedValues { get; private set; }
+
+ public virtual IEnumerator<SelectListItem> GetEnumerator()
+ {
+ return GetListItems().GetEnumerator();
+ }
+
+ internal IList<SelectListItem> GetListItems()
+ {
+ return (!String.IsNullOrEmpty(DataValueField))
+ ? GetListItemsWithValueField()
+ : GetListItemsWithoutValueField();
+ }
+
+ private IList<SelectListItem> GetListItemsWithValueField()
+ {
+ HashSet<string> selectedValues = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+ if (SelectedValues != null)
+ {
+ selectedValues.UnionWith(from object value in SelectedValues
+ select Convert.ToString(value, CultureInfo.CurrentCulture));
+ }
+
+ var listItems = from object item in Items
+ let value = Eval(item, DataValueField)
+ select new SelectListItem
+ {
+ Value = value,
+ Text = Eval(item, DataTextField),
+ Selected = selectedValues.Contains(value)
+ };
+ return listItems.ToList();
+ }
+
+ private IList<SelectListItem> GetListItemsWithoutValueField()
+ {
+ HashSet<object> selectedValues = new HashSet<object>();
+ if (SelectedValues != null)
+ {
+ selectedValues.UnionWith(SelectedValues.Cast<object>());
+ }
+
+ var listItems = from object item in Items
+ select new SelectListItem
+ {
+ Text = Eval(item, DataTextField),
+ Selected = selectedValues.Contains(item)
+ };
+ return listItems.ToList();
+ }
+
+ private static string Eval(object container, string expression)
+ {
+ object value = container;
+ if (!String.IsNullOrEmpty(expression))
+ {
+ value = DataBinder.Eval(container, expression);
+ }
+ return Convert.ToString(value, CultureInfo.CurrentCulture);
+ }
+
+ #region IEnumerable Members
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ #endregion
+ }
+}
diff --git a/src/System.Web.Mvc/MultiServiceResolver.cs b/src/System.Web.Mvc/MultiServiceResolver.cs
new file mode 100644
index 00000000..d3506fa2
--- /dev/null
+++ b/src/System.Web.Mvc/MultiServiceResolver.cs
@@ -0,0 +1,51 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace System.Web.Mvc
+{
+ internal class MultiServiceResolver<TService> : IResolver<IEnumerable<TService>>
+ where TService : class
+ {
+ private IEnumerable<TService> _itemsFromService;
+ private Func<IEnumerable<TService>> _itemsThunk;
+ private Func<IDependencyResolver> _resolverThunk;
+
+ public MultiServiceResolver(Func<IEnumerable<TService>> itemsThunk)
+ {
+ if (itemsThunk == null)
+ {
+ throw new ArgumentNullException("itemsThunk");
+ }
+
+ _itemsThunk = itemsThunk;
+ _resolverThunk = () => DependencyResolver.Current;
+ }
+
+ internal MultiServiceResolver(Func<IEnumerable<TService>> itemsThunk, IDependencyResolver resolver)
+ : this(itemsThunk)
+ {
+ if (resolver != null)
+ {
+ _resolverThunk = () => resolver;
+ }
+ }
+
+ public IEnumerable<TService> Current
+ {
+ get
+ {
+ if (_itemsFromService == null)
+ {
+ lock (_itemsThunk)
+ {
+ if (_itemsFromService == null)
+ {
+ _itemsFromService = _resolverThunk().GetServices<TService>();
+ }
+ }
+ }
+ return _itemsFromService.Concat(_itemsThunk());
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/MvcFilter.cs b/src/System.Web.Mvc/MvcFilter.cs
new file mode 100644
index 00000000..480b66ac
--- /dev/null
+++ b/src/System.Web.Mvc/MvcFilter.cs
@@ -0,0 +1,19 @@
+namespace System.Web.Mvc
+{
+ public abstract class MvcFilter : IMvcFilter
+ {
+ protected MvcFilter()
+ {
+ }
+
+ protected MvcFilter(bool allowMultiple, int order)
+ {
+ AllowMultiple = allowMultiple;
+ Order = order;
+ }
+
+ public bool AllowMultiple { get; private set; }
+
+ public int Order { get; private set; }
+ }
+}
diff --git a/src/System.Web.Mvc/MvcHandler.cs b/src/System.Web.Mvc/MvcHandler.cs
new file mode 100644
index 00000000..de4a6d8b
--- /dev/null
+++ b/src/System.Web.Mvc/MvcHandler.cs
@@ -0,0 +1,248 @@
+using System.Globalization;
+using System.Linq;
+using System.Reflection;
+using System.Threading;
+using System.Web.Mvc.Async;
+using System.Web.Mvc.Properties;
+using System.Web.Routing;
+using System.Web.SessionState;
+using Microsoft.Web.Infrastructure.DynamicValidationHelper;
+
+namespace System.Web.Mvc
+{
+ public class MvcHandler : IHttpAsyncHandler, IHttpHandler, IRequiresSessionState
+ {
+ private static readonly object _processRequestTag = new object();
+
+ internal static readonly string MvcVersion = GetMvcVersionString();
+ public static readonly string MvcVersionHeaderName = "X-AspNetMvc-Version";
+ private ControllerBuilder _controllerBuilder;
+
+ public MvcHandler(RequestContext requestContext)
+ {
+ if (requestContext == null)
+ {
+ throw new ArgumentNullException("requestContext");
+ }
+
+ RequestContext = requestContext;
+ }
+
+ internal ControllerBuilder ControllerBuilder
+ {
+ get
+ {
+ if (_controllerBuilder == null)
+ {
+ _controllerBuilder = ControllerBuilder.Current;
+ }
+ return _controllerBuilder;
+ }
+ set { _controllerBuilder = value; }
+ }
+
+ public static bool DisableMvcResponseHeader { get; set; }
+
+ protected virtual bool IsReusable
+ {
+ get { return false; }
+ }
+
+ public RequestContext RequestContext { get; private set; }
+
+ protected internal virtual void AddVersionHeader(HttpContextBase httpContext)
+ {
+ if (!DisableMvcResponseHeader)
+ {
+ httpContext.Response.AppendHeader(MvcVersionHeaderName, MvcVersion);
+ }
+ }
+
+ protected virtual IAsyncResult BeginProcessRequest(HttpContext httpContext, AsyncCallback callback, object state)
+ {
+ HttpContextBase httpContextBase = new HttpContextWrapper(httpContext);
+ return BeginProcessRequest(httpContextBase, callback, state);
+ }
+
+ protected internal virtual IAsyncResult BeginProcessRequest(HttpContextBase httpContext, AsyncCallback callback, object state)
+ {
+ return SecurityUtil.ProcessInApplicationTrust(() =>
+ {
+ IController controller;
+ IControllerFactory factory;
+ ProcessRequestInit(httpContext, out controller, out factory);
+
+ IAsyncController asyncController = controller as IAsyncController;
+ if (asyncController != null)
+ {
+ // asynchronous controller
+ BeginInvokeDelegate beginDelegate = delegate(AsyncCallback asyncCallback, object asyncState)
+ {
+ try
+ {
+ return asyncController.BeginExecute(RequestContext, asyncCallback, asyncState);
+ }
+ catch
+ {
+ factory.ReleaseController(asyncController);
+ throw;
+ }
+ };
+
+ EndInvokeDelegate endDelegate = delegate(IAsyncResult asyncResult)
+ {
+ try
+ {
+ asyncController.EndExecute(asyncResult);
+ }
+ finally
+ {
+ factory.ReleaseController(asyncController);
+ }
+ };
+
+ SynchronizationContext syncContext = SynchronizationContextUtil.GetSynchronizationContext();
+ AsyncCallback newCallback = AsyncUtil.WrapCallbackForSynchronizedExecution(callback, syncContext);
+ return AsyncResultWrapper.Begin(newCallback, state, beginDelegate, endDelegate, _processRequestTag);
+ }
+ else
+ {
+ // synchronous controller
+ Action action = delegate
+ {
+ try
+ {
+ controller.Execute(RequestContext);
+ }
+ finally
+ {
+ factory.ReleaseController(controller);
+ }
+ };
+
+ return AsyncResultWrapper.BeginSynchronous(callback, state, action, _processRequestTag);
+ }
+ });
+ }
+
+ protected internal virtual void EndProcessRequest(IAsyncResult asyncResult)
+ {
+ SecurityUtil.ProcessInApplicationTrust(() =>
+ {
+ AsyncResultWrapper.End(asyncResult, _processRequestTag);
+ });
+ }
+
+ private static string GetMvcVersionString()
+ {
+ // DevDiv 216459:
+ // This code originally used Assembly.GetName(), but that requires FileIOPermission, which isn't granted in
+ // medium trust. However, Assembly.FullName *is* accessible in medium trust.
+ return new AssemblyName(typeof(MvcHandler).Assembly.FullName).Version.ToString(2);
+ }
+
+ protected virtual void ProcessRequest(HttpContext httpContext)
+ {
+ HttpContextBase httpContextBase = new HttpContextWrapper(httpContext);
+ ProcessRequest(httpContextBase);
+ }
+
+ protected internal virtual void ProcessRequest(HttpContextBase httpContext)
+ {
+ SecurityUtil.ProcessInApplicationTrust(() =>
+ {
+ IController controller;
+ IControllerFactory factory;
+ ProcessRequestInit(httpContext, out controller, out factory);
+
+ try
+ {
+ controller.Execute(RequestContext);
+ }
+ finally
+ {
+ factory.ReleaseController(controller);
+ }
+ });
+ }
+
+ private void ProcessRequestInit(HttpContextBase httpContext, out IController controller, out IControllerFactory factory)
+ {
+ // If request validation has already been enabled, make it lazy. This allows attributes like [HttpPost] (which looks
+ // at Request.Form) to work correctly without triggering full validation.
+ // Tolerate null HttpContext for testing.
+ HttpContext currentContext = HttpContext.Current;
+ if (currentContext != null)
+ {
+ bool? isRequestValidationEnabled = ValidationUtility.IsValidationEnabled(currentContext);
+ if (isRequestValidationEnabled == true)
+ {
+ ValidationUtility.EnableDynamicValidation(currentContext);
+ }
+ }
+
+ AddVersionHeader(httpContext);
+ RemoveOptionalRoutingParameters();
+
+ // Get the controller type
+ string controllerName = RequestContext.RouteData.GetRequiredString("controller");
+
+ // Instantiate the controller and call Execute
+ factory = ControllerBuilder.GetControllerFactory();
+ controller = factory.CreateController(RequestContext, controllerName);
+ if (controller == null)
+ {
+ throw new InvalidOperationException(
+ String.Format(
+ CultureInfo.CurrentCulture,
+ MvcResources.ControllerBuilder_FactoryReturnedNull,
+ factory.GetType(),
+ controllerName));
+ }
+ }
+
+ private void RemoveOptionalRoutingParameters()
+ {
+ RouteValueDictionary rvd = RequestContext.RouteData.Values;
+
+ // Get all keys for which the corresponding value is 'Optional'.
+ // ToArray() necessary so that we don't manipulate the dictionary while enumerating.
+ string[] matchingKeys = (from entry in rvd
+ where entry.Value == UrlParameter.Optional
+ select entry.Key).ToArray();
+
+ foreach (string key in matchingKeys)
+ {
+ rvd.Remove(key);
+ }
+ }
+
+ #region IHttpHandler Members
+
+ bool IHttpHandler.IsReusable
+ {
+ get { return IsReusable; }
+ }
+
+ void IHttpHandler.ProcessRequest(HttpContext httpContext)
+ {
+ ProcessRequest(httpContext);
+ }
+
+ #endregion
+
+ #region IHttpAsyncHandler Members
+
+ IAsyncResult IHttpAsyncHandler.BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)
+ {
+ return BeginProcessRequest(context, cb, extraData);
+ }
+
+ void IHttpAsyncHandler.EndProcessRequest(IAsyncResult result)
+ {
+ EndProcessRequest(result);
+ }
+
+ #endregion
+ }
+}
diff --git a/src/System.Web.Mvc/MvcHtmlString.cs b/src/System.Web.Mvc/MvcHtmlString.cs
new file mode 100644
index 00000000..8905cd04
--- /dev/null
+++ b/src/System.Web.Mvc/MvcHtmlString.cs
@@ -0,0 +1,28 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Web.Mvc
+{
+ public sealed class MvcHtmlString : HtmlString
+ {
+ [SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", Justification = "MvcHtmlString is immutable")]
+ public static readonly MvcHtmlString Empty = Create(String.Empty);
+
+ private readonly string _value;
+
+ public MvcHtmlString(string value)
+ : base(value ?? String.Empty)
+ {
+ _value = value ?? String.Empty;
+ }
+
+ public static MvcHtmlString Create(string value)
+ {
+ return new MvcHtmlString(value);
+ }
+
+ public static bool IsNullOrEmpty(MvcHtmlString value)
+ {
+ return (value == null || value._value.Length == 0);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/MvcHttpHandler.cs b/src/System.Web.Mvc/MvcHttpHandler.cs
new file mode 100644
index 00000000..a64fb272
--- /dev/null
+++ b/src/System.Web.Mvc/MvcHttpHandler.cs
@@ -0,0 +1,105 @@
+using System.Web.Mvc.Async;
+using System.Web.Routing;
+using System.Web.SessionState;
+
+namespace System.Web.Mvc
+{
+ public class MvcHttpHandler : UrlRoutingHandler, IHttpAsyncHandler, IRequiresSessionState
+ {
+ private static readonly object _processRequestTag = new object();
+
+ protected virtual IAsyncResult BeginProcessRequest(HttpContext httpContext, AsyncCallback callback, object state)
+ {
+ HttpContextBase httpContextBase = new HttpContextWrapper(httpContext);
+ return BeginProcessRequest(httpContextBase, callback, state);
+ }
+
+ protected internal virtual IAsyncResult BeginProcessRequest(HttpContextBase httpContext, AsyncCallback callback, object state)
+ {
+ IHttpHandler httpHandler = GetHttpHandler(httpContext);
+ IHttpAsyncHandler httpAsyncHandler = httpHandler as IHttpAsyncHandler;
+
+ if (httpAsyncHandler != null)
+ {
+ // asynchronous handler
+ BeginInvokeDelegate beginDelegate = delegate(AsyncCallback asyncCallback, object asyncState)
+ {
+ return httpAsyncHandler.BeginProcessRequest(HttpContext.Current, asyncCallback, asyncState);
+ };
+ EndInvokeDelegate endDelegate = delegate(IAsyncResult asyncResult)
+ {
+ httpAsyncHandler.EndProcessRequest(asyncResult);
+ };
+ return AsyncResultWrapper.Begin(callback, state, beginDelegate, endDelegate, _processRequestTag);
+ }
+ else
+ {
+ // synchronous handler
+ Action action = delegate
+ {
+ httpHandler.ProcessRequest(HttpContext.Current);
+ };
+ return AsyncResultWrapper.BeginSynchronous(callback, state, action, _processRequestTag);
+ }
+ }
+
+ protected internal virtual void EndProcessRequest(IAsyncResult asyncResult)
+ {
+ AsyncResultWrapper.End(asyncResult, _processRequestTag);
+ }
+
+ private static IHttpHandler GetHttpHandler(HttpContextBase httpContext)
+ {
+ DummyHttpHandler dummyHandler = new DummyHttpHandler();
+ dummyHandler.PublicProcessRequest(httpContext);
+ return dummyHandler.HttpHandler;
+ }
+
+ // synchronous code
+ protected override void VerifyAndProcessRequest(IHttpHandler httpHandler, HttpContextBase httpContext)
+ {
+ if (httpHandler == null)
+ {
+ throw new ArgumentNullException("httpHandler");
+ }
+
+ httpHandler.ProcessRequest(HttpContext.Current);
+ }
+
+ #region IHttpAsyncHandler Members
+
+ IAsyncResult IHttpAsyncHandler.BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)
+ {
+ return BeginProcessRequest(context, cb, extraData);
+ }
+
+ void IHttpAsyncHandler.EndProcessRequest(IAsyncResult result)
+ {
+ EndProcessRequest(result);
+ }
+
+ #endregion
+
+ // Since UrlRoutingHandler.ProcessRequest() does the heavy lifting of looking at the RouteCollection for
+ // a matching route, we need to call into it. However, that method is also responsible for kicking off
+ // the synchronous request, and we can't allow it to do that. The purpose of this dummy class is to run
+ // only the lookup portion of UrlRoutingHandler.ProcessRequest(), then intercept the handler it returns
+ // and execute it asynchronously.
+
+ private sealed class DummyHttpHandler : UrlRoutingHandler
+ {
+ public IHttpHandler HttpHandler;
+
+ public void PublicProcessRequest(HttpContextBase httpContext)
+ {
+ ProcessRequest(httpContext);
+ }
+
+ protected override void VerifyAndProcessRequest(IHttpHandler httpHandler, HttpContextBase httpContext)
+ {
+ // don't process the request, just store a reference to it
+ HttpHandler = httpHandler;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/MvcRouteHandler.cs b/src/System.Web.Mvc/MvcRouteHandler.cs
new file mode 100644
index 00000000..4b898b2c
--- /dev/null
+++ b/src/System.Web.Mvc/MvcRouteHandler.cs
@@ -0,0 +1,47 @@
+using System.Web.Mvc.Properties;
+using System.Web.Routing;
+using System.Web.SessionState;
+
+namespace System.Web.Mvc
+{
+ public class MvcRouteHandler : IRouteHandler
+ {
+ private IControllerFactory _controllerFactory;
+
+ public MvcRouteHandler()
+ {
+ }
+
+ public MvcRouteHandler(IControllerFactory controllerFactory)
+ {
+ _controllerFactory = controllerFactory;
+ }
+
+ protected virtual IHttpHandler GetHttpHandler(RequestContext requestContext)
+ {
+ requestContext.HttpContext.SetSessionStateBehavior(GetSessionStateBehavior(requestContext));
+ return new MvcHandler(requestContext);
+ }
+
+ protected virtual SessionStateBehavior GetSessionStateBehavior(RequestContext requestContext)
+ {
+ string controllerName = (string)requestContext.RouteData.Values["controller"];
+ if (String.IsNullOrWhiteSpace(controllerName))
+ {
+ throw new InvalidOperationException(MvcResources.MvcRouteHandler_RouteValuesHasNoController);
+ }
+
+ IControllerFactory controllerFactory = _controllerFactory ?? ControllerBuilder.Current.GetControllerFactory();
+ return controllerFactory.GetControllerSessionBehavior(requestContext, controllerName);
+ }
+
+ #region IRouteHandler Members
+
+ IHttpHandler IRouteHandler.GetHttpHandler(RequestContext requestContext)
+ {
+ return GetHttpHandler(requestContext);
+ }
+
+ #endregion
+ }
+}
diff --git a/src/System.Web.Mvc/MvcWebRazorHostFactory.cs b/src/System.Web.Mvc/MvcWebRazorHostFactory.cs
new file mode 100644
index 00000000..f26f01ac
--- /dev/null
+++ b/src/System.Web.Mvc/MvcWebRazorHostFactory.cs
@@ -0,0 +1,20 @@
+using System.Web.Mvc.Razor;
+using System.Web.WebPages.Razor;
+
+namespace System.Web.Mvc
+{
+ public class MvcWebRazorHostFactory : WebRazorHostFactory
+ {
+ public override WebPageRazorHost CreateHost(string virtualPath, string physicalPath)
+ {
+ WebPageRazorHost host = base.CreateHost(virtualPath, physicalPath);
+
+ if (!host.IsSpecialPage)
+ {
+ return new MvcWebPageRazorHost(virtualPath, physicalPath);
+ }
+
+ return host;
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/NameValueCollectionExtensions.cs b/src/System.Web.Mvc/NameValueCollectionExtensions.cs
new file mode 100644
index 00000000..31bdd4f3
--- /dev/null
+++ b/src/System.Web.Mvc/NameValueCollectionExtensions.cs
@@ -0,0 +1,33 @@
+using System.Collections.Generic;
+using System.Collections.Specialized;
+
+namespace System.Web.Mvc
+{
+ public static class NameValueCollectionExtensions
+ {
+ public static void CopyTo(this NameValueCollection collection, IDictionary<string, object> destination)
+ {
+ CopyTo(collection, destination, false /* replaceEntries */);
+ }
+
+ public static void CopyTo(this NameValueCollection collection, IDictionary<string, object> destination, bool replaceEntries)
+ {
+ if (collection == null)
+ {
+ throw new ArgumentNullException("collection");
+ }
+ if (destination == null)
+ {
+ throw new ArgumentNullException("destination");
+ }
+
+ foreach (string key in collection.Keys)
+ {
+ if (replaceEntries || !destination.ContainsKey(key))
+ {
+ destination[key] = collection[key];
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/NameValueCollectionValueProvider.cs b/src/System.Web.Mvc/NameValueCollectionValueProvider.cs
new file mode 100644
index 00000000..0db898e2
--- /dev/null
+++ b/src/System.Web.Mvc/NameValueCollectionValueProvider.cs
@@ -0,0 +1,121 @@
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Globalization;
+using System.Threading;
+
+namespace System.Web.Mvc
+{
+ public class NameValueCollectionValueProvider : IValueProvider, IUnvalidatedValueProvider, IEnumerableValueProvider
+ {
+ private readonly HashSet<string> _prefixes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+ private readonly Dictionary<string, ValueProviderResultPlaceholder> _values = new Dictionary<string, ValueProviderResultPlaceholder>(StringComparer.OrdinalIgnoreCase);
+
+ public NameValueCollectionValueProvider(NameValueCollection collection, CultureInfo culture)
+ : this(collection, null /* unvalidatedCollection */, culture)
+ {
+ }
+
+ public NameValueCollectionValueProvider(NameValueCollection collection, NameValueCollection unvalidatedCollection, CultureInfo culture)
+ {
+ if (collection == null)
+ {
+ throw new ArgumentNullException("collection");
+ }
+
+ AddValues(collection, unvalidatedCollection ?? collection, culture);
+ }
+
+ private void AddValues(NameValueCollection validatedCollection, NameValueCollection unvalidatedCollection, CultureInfo culture)
+ {
+ // Need to read keys from the unvalidated collection, as M.W.I's granular request validation is a bit touchy
+ // and validated entries at the time the key or value is looked at. For example, GetKey() will throw if the
+ // value fails request validation, even though the value's not being looked at (M.W.I can't tell the difference).
+
+ if (unvalidatedCollection.Count > 0)
+ {
+ _prefixes.Add(String.Empty);
+ }
+
+ foreach (string key in unvalidatedCollection)
+ {
+ if (key != null)
+ {
+ _prefixes.UnionWith(ValueProviderUtil.GetPrefixes(key));
+
+ // need to look up values lazily, as eagerly looking at the collection might trigger validation
+ _values[key] = new ValueProviderResultPlaceholder(key, validatedCollection, unvalidatedCollection, culture);
+ }
+ }
+ }
+
+ public virtual bool ContainsPrefix(string prefix)
+ {
+ if (prefix == null)
+ {
+ throw new ArgumentNullException("prefix");
+ }
+
+ return _prefixes.Contains(prefix);
+ }
+
+ public virtual ValueProviderResult GetValue(string key)
+ {
+ return GetValue(key, skipValidation: false);
+ }
+
+ public virtual ValueProviderResult GetValue(string key, bool skipValidation)
+ {
+ if (key == null)
+ {
+ throw new ArgumentNullException("key");
+ }
+
+ ValueProviderResultPlaceholder placeholder;
+ _values.TryGetValue(key, out placeholder);
+ if (placeholder == null)
+ {
+ return null;
+ }
+ else
+ {
+ return (skipValidation) ? placeholder.UnvalidatedResult : placeholder.ValidatedResult;
+ }
+ }
+
+ public virtual IDictionary<string, string> GetKeysFromPrefix(string prefix)
+ {
+ return ValueProviderUtil.GetKeysFromPrefix(_prefixes, prefix);
+ }
+
+ // Placeholder that can store a validated (in relation to request validation) or unvalidated
+ // ValueProviderResult for a given key.
+ private sealed class ValueProviderResultPlaceholder
+ {
+ private readonly Lazy<ValueProviderResult> _validatedResultPlaceholder;
+ private readonly Lazy<ValueProviderResult> _unvalidatedResultPlaceholder;
+
+ public ValueProviderResultPlaceholder(string key, NameValueCollection validatedCollection, NameValueCollection unvalidatedCollection, CultureInfo culture)
+ {
+ _validatedResultPlaceholder = new Lazy<ValueProviderResult>(() => GetResultFromCollection(key, validatedCollection, culture), LazyThreadSafetyMode.None);
+ _unvalidatedResultPlaceholder = new Lazy<ValueProviderResult>(() => GetResultFromCollection(key, unvalidatedCollection, culture), LazyThreadSafetyMode.None);
+ }
+
+ public ValueProviderResult ValidatedResult
+ {
+ get { return _validatedResultPlaceholder.Value; }
+ }
+
+ public ValueProviderResult UnvalidatedResult
+ {
+ get { return _unvalidatedResultPlaceholder.Value; }
+ }
+
+ private static ValueProviderResult GetResultFromCollection(string key, NameValueCollection collection, CultureInfo culture)
+ {
+ string[] rawValue = collection.GetValues(key);
+ string attemptedValue = collection[key];
+ return new ValueProviderResult(rawValue, attemptedValue, culture);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/NoAsyncTimeoutAttribute.cs b/src/System.Web.Mvc/NoAsyncTimeoutAttribute.cs
new file mode 100644
index 00000000..05da36c4
--- /dev/null
+++ b/src/System.Web.Mvc/NoAsyncTimeoutAttribute.cs
@@ -0,0 +1,13 @@
+using System.Threading;
+
+namespace System.Web.Mvc
+{
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
+ public sealed class NoAsyncTimeoutAttribute : AsyncTimeoutAttribute
+ {
+ public NoAsyncTimeoutAttribute()
+ : base(Timeout.Infinite)
+ {
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/NonActionAttribute.cs b/src/System.Web.Mvc/NonActionAttribute.cs
new file mode 100644
index 00000000..be4e1d47
--- /dev/null
+++ b/src/System.Web.Mvc/NonActionAttribute.cs
@@ -0,0 +1,13 @@
+using System.Reflection;
+
+namespace System.Web.Mvc
+{
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+ public sealed class NonActionAttribute : ActionMethodSelectorAttribute
+ {
+ public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
+ {
+ return false;
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/NullViewLocationCache.cs b/src/System.Web.Mvc/NullViewLocationCache.cs
new file mode 100644
index 00000000..681e7e1e
--- /dev/null
+++ b/src/System.Web.Mvc/NullViewLocationCache.cs
@@ -0,0 +1,18 @@
+namespace System.Web.Mvc
+{
+ internal sealed class NullViewLocationCache : IViewLocationCache
+ {
+ #region IViewLocationCache Members
+
+ public string GetViewLocation(HttpContextBase httpContext, string key)
+ {
+ return null;
+ }
+
+ public void InsertViewLocation(HttpContextBase httpContext, string key, string virtualPath)
+ {
+ }
+
+ #endregion
+ }
+}
diff --git a/src/System.Web.Mvc/OutputCacheAttribute.cs b/src/System.Web.Mvc/OutputCacheAttribute.cs
new file mode 100644
index 00000000..f205f49a
--- /dev/null
+++ b/src/System.Web.Mvc/OutputCacheAttribute.cs
@@ -0,0 +1,355 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Runtime.Caching;
+using System.Security.Cryptography;
+using System.Text;
+using System.Web.Mvc.Properties;
+using System.Web.UI;
+
+namespace System.Web.Mvc
+{
+ [SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", Justification = "Unsealed so that subclassed types can set properties in the default constructor.")]
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
+ public class OutputCacheAttribute : ActionFilterAttribute, IExceptionFilter
+ {
+ private const string CacheKeyPrefix = "_MvcChildActionCache_";
+ private static ObjectCache _childActionCache;
+ private static object _childActionFilterFinishCallbackKey = new object();
+ private OutputCacheParameters _cacheSettings = new OutputCacheParameters { VaryByParam = "*" };
+ private Func<ObjectCache> _childActionCacheThunk = () => ChildActionCache;
+ private bool _locationWasSet;
+ private bool _noStoreWasSet;
+
+ public OutputCacheAttribute()
+ {
+ }
+
+ internal OutputCacheAttribute(ObjectCache childActionCache)
+ {
+ _childActionCacheThunk = () => childActionCache;
+ }
+
+ public string CacheProfile
+ {
+ get { return _cacheSettings.CacheProfile ?? String.Empty; }
+ set { _cacheSettings.CacheProfile = value; }
+ }
+
+ internal OutputCacheParameters CacheSettings
+ {
+ get { return _cacheSettings; }
+ }
+
+ public static ObjectCache ChildActionCache
+ {
+ get { return _childActionCache ?? MemoryCache.Default; }
+ set { _childActionCache = value; }
+ }
+
+ private ObjectCache ChildActionCacheInternal
+ {
+ get { return _childActionCacheThunk(); }
+ }
+
+ public int Duration
+ {
+ get { return _cacheSettings.Duration; }
+ set { _cacheSettings.Duration = value; }
+ }
+
+ public OutputCacheLocation Location
+ {
+ get { return _cacheSettings.Location; }
+ set
+ {
+ _cacheSettings.Location = value;
+ _locationWasSet = true;
+ }
+ }
+
+ public bool NoStore
+ {
+ get { return _cacheSettings.NoStore; }
+ set
+ {
+ _cacheSettings.NoStore = value;
+ _noStoreWasSet = true;
+ }
+ }
+
+ public string SqlDependency
+ {
+ get { return _cacheSettings.SqlDependency ?? String.Empty; }
+ set { _cacheSettings.SqlDependency = value; }
+ }
+
+ public string VaryByContentEncoding
+ {
+ get { return _cacheSettings.VaryByContentEncoding ?? String.Empty; }
+ set { _cacheSettings.VaryByContentEncoding = value; }
+ }
+
+ public string VaryByCustom
+ {
+ get { return _cacheSettings.VaryByCustom ?? String.Empty; }
+ set { _cacheSettings.VaryByCustom = value; }
+ }
+
+ public string VaryByHeader
+ {
+ get { return _cacheSettings.VaryByHeader ?? String.Empty; }
+ set { _cacheSettings.VaryByHeader = value; }
+ }
+
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Param", Justification = "Matches the @ OutputCache page directive. Suppressed in source because this is a special case suppression.")]
+ public string VaryByParam
+ {
+ get { return _cacheSettings.VaryByParam ?? String.Empty; }
+ set { _cacheSettings.VaryByParam = value; }
+ }
+
+ private static void ClearChildActionFilterFinishCallback(ControllerContext controllerContext)
+ {
+ controllerContext.HttpContext.Items.Remove(_childActionFilterFinishCallbackKey);
+ }
+
+ private static void CompleteChildAction(ControllerContext filterContext, bool wasException)
+ {
+ Action<bool> callback = GetChildActionFilterFinishCallback(filterContext);
+
+ if (callback != null)
+ {
+ ClearChildActionFilterFinishCallback(filterContext);
+ callback(wasException);
+ }
+ }
+
+ private static Action<bool> GetChildActionFilterFinishCallback(ControllerContext controllerContext)
+ {
+ return controllerContext.HttpContext.Items[_childActionFilterFinishCallbackKey] as Action<bool>;
+ }
+
+ internal string GetChildActionUniqueId(ActionExecutingContext filterContext)
+ {
+ StringBuilder uniqueIdBuilder = new StringBuilder();
+
+ // Start with a prefix, presuming that we share the cache with other users
+ uniqueIdBuilder.Append(CacheKeyPrefix);
+
+ // Unique ID of the action description
+ uniqueIdBuilder.Append(filterContext.ActionDescriptor.UniqueId);
+
+ // Unique ID from the VaryByCustom settings, if any
+ uniqueIdBuilder.Append(DescriptorUtil.CreateUniqueId(VaryByCustom));
+ if (!String.IsNullOrEmpty(VaryByCustom))
+ {
+ string varyByCustomResult = filterContext.HttpContext.ApplicationInstance.GetVaryByCustomString(HttpContext.Current, VaryByCustom);
+ uniqueIdBuilder.Append(varyByCustomResult);
+ }
+
+ // Unique ID from the VaryByParam settings, if any
+ uniqueIdBuilder.Append(GetUniqueIdFromActionParameters(filterContext, SplitVaryByParam(VaryByParam)));
+
+ // The key is typically too long to be useful, so we use a cryptographic hash
+ // as the actual key (better randomization and key distribution, so small vary
+ // values will generate dramtically different keys).
+ using (SHA256 sha = SHA256.Create())
+ {
+ return Convert.ToBase64String(sha.ComputeHash(Encoding.UTF8.GetBytes(uniqueIdBuilder.ToString())));
+ }
+ }
+
+ private static string GetUniqueIdFromActionParameters(ActionExecutingContext filterContext, IEnumerable<string> keys)
+ {
+ // Generate a unique ID of normalized key names + key values
+ var keyValues = new Dictionary<string, object>(filterContext.ActionParameters, StringComparer.OrdinalIgnoreCase);
+ keys = (keys ?? keyValues.Keys).Select(key => key.ToUpperInvariant())
+ .OrderBy(key => key, StringComparer.Ordinal);
+
+ return DescriptorUtil.CreateUniqueId(keys.Concat(keys.Select(key => keyValues.ContainsKey(key) ? keyValues[key] : null)));
+ }
+
+ public static bool IsChildActionCacheActive(ControllerContext controllerContext)
+ {
+ return GetChildActionFilterFinishCallback(controllerContext) != null;
+ }
+
+ public override void OnActionExecuted(ActionExecutedContext filterContext)
+ {
+ if (filterContext == null)
+ {
+ throw new ArgumentNullException("filterContext");
+ }
+
+ // Complete the request if the child action threw an exception
+ if (filterContext.IsChildAction && filterContext.Exception != null)
+ {
+ CompleteChildAction(filterContext, wasException: true);
+ }
+ }
+
+ public override void OnActionExecuting(ActionExecutingContext filterContext)
+ {
+ if (filterContext == null)
+ {
+ throw new ArgumentNullException("filterContext");
+ }
+
+ if (filterContext.IsChildAction)
+ {
+ ValidateChildActionConfiguration();
+
+ // Already actively being captured? (i.e., cached child action inside of cached child action)
+ // Realistically, this needs write substitution to do properly (including things like authentication)
+ if (GetChildActionFilterFinishCallback(filterContext) != null)
+ {
+ throw new InvalidOperationException(MvcResources.OutputCacheAttribute_CannotNestChildCache);
+ }
+
+ // Already cached?
+ string uniqueId = GetChildActionUniqueId(filterContext);
+ string cachedValue = ChildActionCacheInternal.Get(uniqueId) as string;
+ if (cachedValue != null)
+ {
+ filterContext.Result = new ContentResult() { Content = cachedValue };
+ return;
+ }
+
+ // Swap in a new TextWriter so we can capture the output
+ StringWriter cachingWriter = new StringWriter(CultureInfo.InvariantCulture);
+ TextWriter originalWriter = filterContext.HttpContext.Response.Output;
+ filterContext.HttpContext.Response.Output = cachingWriter;
+
+ // Set a finish callback to clean up
+ SetChildActionFilterFinishCallback(filterContext, wasException =>
+ {
+ // Restore original writer
+ filterContext.HttpContext.Response.Output = originalWriter;
+
+ // Grab output and write it
+ string capturedText = cachingWriter.ToString();
+ filterContext.HttpContext.Response.Write(capturedText);
+
+ // Only cache output if this wasn't an error
+ if (!wasException)
+ {
+ ChildActionCacheInternal.Add(uniqueId, capturedText, DateTimeOffset.UtcNow.AddSeconds(Duration));
+ }
+ });
+ }
+ }
+
+ public void OnException(ExceptionContext filterContext)
+ {
+ if (filterContext == null)
+ {
+ throw new ArgumentNullException("filterContext");
+ }
+
+ if (filterContext.IsChildAction)
+ {
+ CompleteChildAction(filterContext, wasException: true);
+ }
+ }
+
+ public override void OnResultExecuting(ResultExecutingContext filterContext)
+ {
+ if (filterContext == null)
+ {
+ throw new ArgumentNullException("filterContext");
+ }
+
+ if (!filterContext.IsChildAction)
+ {
+ // we need to call ProcessRequest() since there's no other way to set the Page.Response intrinsic
+ using (OutputCachedPage page = new OutputCachedPage(_cacheSettings))
+ {
+ page.ProcessRequest(HttpContext.Current);
+ }
+ }
+ }
+
+ public override void OnResultExecuted(ResultExecutedContext filterContext)
+ {
+ if (filterContext == null)
+ {
+ throw new ArgumentNullException("filterContext");
+ }
+
+ if (filterContext.IsChildAction)
+ {
+ CompleteChildAction(filterContext, wasException: filterContext.Exception != null);
+ }
+ }
+
+ private static void SetChildActionFilterFinishCallback(ControllerContext controllerContext, Action<bool> callback)
+ {
+ controllerContext.HttpContext.Items[_childActionFilterFinishCallbackKey] = callback;
+ }
+
+ private static IEnumerable<string> SplitVaryByParam(string varyByParam)
+ {
+ if (String.Equals(varyByParam, "none", StringComparison.OrdinalIgnoreCase))
+ {
+ // Vary by nothing
+ return Enumerable.Empty<string>();
+ }
+
+ if (String.Equals(varyByParam, "*", StringComparison.OrdinalIgnoreCase))
+ {
+ // Vary by everything
+ return null;
+ }
+
+ return from part in varyByParam.Split(';') // Vary by specific parameters
+ let trimmed = part.Trim()
+ where !String.IsNullOrEmpty(trimmed)
+ select trimmed;
+ }
+
+ private void ValidateChildActionConfiguration()
+ {
+ if (Duration <= 0)
+ {
+ throw new InvalidOperationException(MvcResources.OutputCacheAttribute_InvalidDuration);
+ }
+
+ if (String.IsNullOrWhiteSpace(VaryByParam))
+ {
+ throw new InvalidOperationException(MvcResources.OutputCacheAttribute_InvalidVaryByParam);
+ }
+
+ if (!String.IsNullOrWhiteSpace(CacheProfile) ||
+ !String.IsNullOrWhiteSpace(SqlDependency) ||
+ !String.IsNullOrWhiteSpace(VaryByContentEncoding) ||
+ !String.IsNullOrWhiteSpace(VaryByHeader) ||
+ _locationWasSet || _noStoreWasSet)
+ {
+ throw new InvalidOperationException(MvcResources.OutputCacheAttribute_ChildAction_UnsupportedSetting);
+ }
+ }
+
+ [SuppressMessage("ASP.NET.Security", "CA5328:ValidateRequestShouldBeEnabled", Justification = "Instances of this type are not created in response to direct user input.")]
+ private sealed class OutputCachedPage : Page
+ {
+ private OutputCacheParameters _cacheSettings;
+
+ public OutputCachedPage(OutputCacheParameters cacheSettings)
+ {
+ // Tracing requires Page IDs to be unique.
+ ID = Guid.NewGuid().ToString();
+ _cacheSettings = cacheSettings;
+ }
+
+ protected override void FrameworkInitialize()
+ {
+ // when you put the <%@ OutputCache %> directive on a page, the generated code calls InitOutputCache() from here
+ base.FrameworkInitialize();
+ InitOutputCache(_cacheSettings);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ParameterBindingInfo.cs b/src/System.Web.Mvc/ParameterBindingInfo.cs
new file mode 100644
index 00000000..a005900c
--- /dev/null
+++ b/src/System.Web.Mvc/ParameterBindingInfo.cs
@@ -0,0 +1,27 @@
+using System.Collections.Generic;
+
+namespace System.Web.Mvc
+{
+ public abstract class ParameterBindingInfo
+ {
+ public virtual IModelBinder Binder
+ {
+ get { return null; }
+ }
+
+ public virtual ICollection<string> Exclude
+ {
+ get { return new string[0]; }
+ }
+
+ public virtual ICollection<string> Include
+ {
+ get { return new string[0]; }
+ }
+
+ public virtual string Prefix
+ {
+ get { return null; }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ParameterDescriptor.cs b/src/System.Web.Mvc/ParameterDescriptor.cs
new file mode 100644
index 00000000..fbed437f
--- /dev/null
+++ b/src/System.Web.Mvc/ParameterDescriptor.cs
@@ -0,0 +1,54 @@
+using System.Reflection;
+
+namespace System.Web.Mvc
+{
+ public abstract class ParameterDescriptor : ICustomAttributeProvider
+ {
+ private static readonly EmptyParameterBindingInfo _emptyBindingInfo = new EmptyParameterBindingInfo();
+
+ public abstract ActionDescriptor ActionDescriptor { get; }
+
+ public virtual ParameterBindingInfo BindingInfo
+ {
+ get { return _emptyBindingInfo; }
+ }
+
+ public virtual object DefaultValue
+ {
+ get { return null; }
+ }
+
+ public abstract string ParameterName { get; }
+
+ public abstract Type ParameterType { get; }
+
+ public virtual object[] GetCustomAttributes(bool inherit)
+ {
+ return GetCustomAttributes(typeof(object), inherit);
+ }
+
+ public virtual object[] GetCustomAttributes(Type attributeType, bool inherit)
+ {
+ if (attributeType == null)
+ {
+ throw new ArgumentNullException("attributeType");
+ }
+
+ return (object[])Array.CreateInstance(attributeType, 0);
+ }
+
+ public virtual bool IsDefined(Type attributeType, bool inherit)
+ {
+ if (attributeType == null)
+ {
+ throw new ArgumentNullException("attributeType");
+ }
+
+ return false;
+ }
+
+ private sealed class EmptyParameterBindingInfo : ParameterBindingInfo
+ {
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ParameterInfoUtil.cs b/src/System.Web.Mvc/ParameterInfoUtil.cs
new file mode 100644
index 00000000..e7343cdf
--- /dev/null
+++ b/src/System.Web.Mvc/ParameterInfoUtil.cs
@@ -0,0 +1,33 @@
+using System.ComponentModel;
+using System.Reflection;
+
+namespace System.Web.Mvc
+{
+ internal static class ParameterInfoUtil
+ {
+ public static bool TryGetDefaultValue(ParameterInfo parameterInfo, out object value)
+ {
+ // this will get the default value as seen by the VB / C# compilers
+ // if no value was baked in, RawDefaultValue returns DBNull.Value
+ object defaultValue = parameterInfo.DefaultValue;
+ if (defaultValue != DBNull.Value)
+ {
+ value = defaultValue;
+ return true;
+ }
+
+ // if the compiler did not bake in a default value, check the [DefaultValue] attribute
+ DefaultValueAttribute[] attrs = (DefaultValueAttribute[])parameterInfo.GetCustomAttributes(typeof(DefaultValueAttribute), false);
+ if (attrs == null || attrs.Length == 0)
+ {
+ value = default(object);
+ return false;
+ }
+ else
+ {
+ value = attrs[0].Value;
+ return true;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/PartialViewResult.cs b/src/System.Web.Mvc/PartialViewResult.cs
new file mode 100644
index 00000000..19e08795
--- /dev/null
+++ b/src/System.Web.Mvc/PartialViewResult.cs
@@ -0,0 +1,28 @@
+using System.Globalization;
+using System.Text;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc
+{
+ public class PartialViewResult : ViewResultBase
+ {
+ protected override ViewEngineResult FindView(ControllerContext context)
+ {
+ ViewEngineResult result = ViewEngineCollection.FindPartialView(context, ViewName);
+ if (result.View != null)
+ {
+ return result;
+ }
+
+ // we need to generate an exception containing all the locations we searched
+ StringBuilder locationsText = new StringBuilder();
+ foreach (string location in result.SearchedLocations)
+ {
+ locationsText.AppendLine();
+ locationsText.Append(location);
+ }
+ throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture,
+ MvcResources.Common_PartialViewNotFound, ViewName, locationsText));
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/PathHelpers.cs b/src/System.Web.Mvc/PathHelpers.cs
new file mode 100644
index 00000000..b6d25b09
--- /dev/null
+++ b/src/System.Web.Mvc/PathHelpers.cs
@@ -0,0 +1,97 @@
+namespace System.Web.Mvc
+{
+ internal static class PathHelpers
+ {
+ private static UrlRewriterHelper _urlRewriterHelper = new UrlRewriterHelper();
+
+ // this method can accept an app-relative path or an absolute path for contentPath
+ public static string GenerateClientUrl(HttpContextBase httpContext, string contentPath)
+ {
+ if (String.IsNullOrEmpty(contentPath))
+ {
+ return contentPath;
+ }
+
+ // many of the methods we call internally can't handle query strings properly, so just strip it out for
+ // the time being
+ string query;
+ contentPath = StripQuery(contentPath, out query);
+
+ return GenerateClientUrlInternal(httpContext, contentPath) + query;
+ }
+
+ private static string GenerateClientUrlInternal(HttpContextBase httpContext, string contentPath)
+ {
+ if (String.IsNullOrEmpty(contentPath))
+ {
+ return contentPath;
+ }
+
+ // can't call VirtualPathUtility.IsAppRelative since it throws on some inputs
+ bool isAppRelative = contentPath[0] == '~';
+ if (isAppRelative)
+ {
+ string absoluteContentPath = VirtualPathUtility.ToAbsolute(contentPath, httpContext.Request.ApplicationPath);
+ string modifiedAbsoluteContentPath = httpContext.Response.ApplyAppPathModifier(absoluteContentPath);
+ return GenerateClientUrlInternal(httpContext, modifiedAbsoluteContentPath);
+ }
+
+ // we only want to manipulate the path if URL rewriting is active for this request, else we risk breaking the generated URL
+ bool wasRequestRewritten = _urlRewriterHelper.WasRequestRewritten(httpContext);
+ if (!wasRequestRewritten)
+ {
+ return contentPath;
+ }
+
+ // Since the rawUrl represents what the user sees in his browser, it is what we want to use as the base
+ // of our absolute paths. For example, consider mysite.example.com/foo, which is internally
+ // rewritten to content.example.com/mysite/foo. When we want to generate a link to ~/bar, we want to
+ // base it from / instead of /foo, otherwise the user ends up seeing mysite.example.com/foo/bar,
+ // which is incorrect.
+ string relativeUrlToDestination = MakeRelative(httpContext.Request.Path, contentPath);
+ string absoluteUrlToDestination = MakeAbsolute(httpContext.Request.RawUrl, relativeUrlToDestination);
+ return absoluteUrlToDestination;
+ }
+
+ public static string MakeAbsolute(string basePath, string relativePath)
+ {
+ // The Combine() method can't handle query strings on the base path, so we trim it off.
+ string query;
+ basePath = StripQuery(basePath, out query);
+ return VirtualPathUtility.Combine(basePath, relativePath);
+ }
+
+ public static string MakeRelative(string fromPath, string toPath)
+ {
+ string relativeUrl = VirtualPathUtility.MakeRelative(fromPath, toPath);
+ if (String.IsNullOrEmpty(relativeUrl) || relativeUrl[0] == '?')
+ {
+ // Sometimes VirtualPathUtility.MakeRelative() will return an empty string when it meant to return '.',
+ // but links to {empty string} are browser dependent. We replace it with an explicit path to force
+ // consistency across browsers.
+ relativeUrl = "./" + relativeUrl;
+ }
+ return relativeUrl;
+ }
+
+ private static string StripQuery(string path, out string query)
+ {
+ int queryIndex = path.IndexOf('?');
+ if (queryIndex >= 0)
+ {
+ query = path.Substring(queryIndex);
+ return path.Substring(0, queryIndex);
+ }
+ else
+ {
+ query = null;
+ return path;
+ }
+ }
+
+ internal static void ResetUrlRewriterHelper()
+ {
+ _urlRewriterHelper = new UrlRewriterHelper();
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/PreApplicationStartCode.cs b/src/System.Web.Mvc/PreApplicationStartCode.cs
new file mode 100644
index 00000000..05da00ac
--- /dev/null
+++ b/src/System.Web.Mvc/PreApplicationStartCode.cs
@@ -0,0 +1,26 @@
+using System.ComponentModel;
+using System.Web.WebPages.Scope;
+
+namespace System.Web.Mvc
+{
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static class PreApplicationStartCode
+ {
+ private static bool _startWasCalled;
+
+ public static void Start()
+ {
+ // Guard against multiple calls. All Start calls are made on same thread, so no lock needed here
+ if (_startWasCalled)
+ {
+ return;
+ }
+ _startWasCalled = true;
+
+ WebPages.Razor.PreApplicationStartCode.Start();
+ WebPages.PreApplicationStartCode.Start();
+
+ ViewContext.GlobalScopeThunk = () => ScopeStorage.CurrentScope;
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Properties/AssemblyInfo.cs b/src/System.Web.Mvc/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..939b03c0
--- /dev/null
+++ b/src/System.Web.Mvc/Properties/AssemblyInfo.cs
@@ -0,0 +1,27 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Security;
+using System.Web;
+using System.Web.Mvc;
+
+[assembly: AssemblyTitle("System.Web.Mvc.dll")]
+[assembly: AssemblyDescription("System.Web.Mvc.dll")]
+[assembly: Guid("4b5f4208-c6b0-4c37-9a41-63325ffa52ad")]
+
+#if !CODE_COVERAGE
+[assembly: AllowPartiallyTrustedCallers]
+#endif
+
+[assembly: InternalsVisibleTo("System.Web.Mvc.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
+[assembly: PreApplicationStartMethod(typeof(PreApplicationStartCode), "Start")]
+[assembly: TypeForwardedTo(typeof(TagBuilder))]
+[assembly: TypeForwardedTo(typeof(TagRenderMode))]
+[assembly: TypeForwardedTo(typeof(HttpAntiForgeryException))]
+[assembly: TypeForwardedTo(typeof(ModelClientValidationEqualToRule))]
+[assembly: TypeForwardedTo(typeof(ModelClientValidationRangeRule))]
+[assembly: TypeForwardedTo(typeof(ModelClientValidationRegexRule))]
+[assembly: TypeForwardedTo(typeof(ModelClientValidationRemoteRule))]
+[assembly: TypeForwardedTo(typeof(ModelClientValidationRequiredRule))]
+[assembly: TypeForwardedTo(typeof(ModelClientValidationRule))]
+[assembly: TypeForwardedTo(typeof(ModelClientValidationStringLengthRule))]
diff --git a/src/System.Web.Mvc/Properties/MvcResources.Designer.cs b/src/System.Web.Mvc/Properties/MvcResources.Designer.cs
new file mode 100644
index 00000000..0618068e
--- /dev/null
+++ b/src/System.Web.Mvc/Properties/MvcResources.Designer.cs
@@ -0,0 +1,1012 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.17369
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace System.Web.Mvc.Properties {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class MvcResources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal MvcResources() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("System.Web.Mvc.Properties.MvcResources", typeof(MvcResources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The current request for action &apos;{0}&apos; on controller type &apos;{1}&apos; is ambiguous between the following action methods:{2}.
+ /// </summary>
+ internal static string ActionMethodSelector_AmbiguousMatch {
+ get {
+ return ResourceManager.GetString("ActionMethodSelector_AmbiguousMatch", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to {0} on type {1}.
+ /// </summary>
+ internal static string ActionMethodSelector_AmbiguousMatchType {
+ get {
+ return ResourceManager.GetString("ActionMethodSelector_AmbiguousMatchType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The asynchronous action method &apos;{0}&apos; cannot be executed synchronously..
+ /// </summary>
+ internal static string AsyncActionDescriptor_CannotExecuteSynchronously {
+ get {
+ return ResourceManager.GetString("AsyncActionDescriptor_CannotExecuteSynchronously", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Lookup for method &apos;{0}&apos; on controller type &apos;{1}&apos; failed because of an ambiguity between the following methods:{2}.
+ /// </summary>
+ internal static string AsyncActionMethodSelector_AmbiguousMethodMatch {
+ get {
+ return ResourceManager.GetString("AsyncActionMethodSelector_AmbiguousMethodMatch", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Could not locate a method named &apos;{0}&apos; on controller type {1}..
+ /// </summary>
+ internal static string AsyncActionMethodSelector_CouldNotFindMethod {
+ get {
+ return ResourceManager.GetString("AsyncActionMethodSelector_CouldNotFindMethod", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The provided IAsyncResult has already been consumed..
+ /// </summary>
+ internal static string AsyncCommon_AsyncResultAlreadyConsumed {
+ get {
+ return ResourceManager.GetString("AsyncCommon_AsyncResultAlreadyConsumed", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The controller of type &apos;{0}&apos; must subclass AsyncController or implement the IAsyncManagerContainer interface..
+ /// </summary>
+ internal static string AsyncCommon_ControllerMustImplementIAsyncManagerContainer {
+ get {
+ return ResourceManager.GetString("AsyncCommon_ControllerMustImplementIAsyncManagerContainer", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The provided IAsyncResult is not valid for this method..
+ /// </summary>
+ internal static string AsyncCommon_InvalidAsyncResult {
+ get {
+ return ResourceManager.GetString("AsyncCommon_InvalidAsyncResult", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The timeout value must be non-negative or Timeout.Infinite..
+ /// </summary>
+ internal static string AsyncCommon_InvalidTimeout {
+ get {
+ return ResourceManager.GetString("AsyncCommon_InvalidTimeout", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to AuthorizeAttribute cannot be used within a child action caching block..
+ /// </summary>
+ internal static string AuthorizeAttribute_CannotUseWithinChildActionCache {
+ get {
+ return ResourceManager.GetString("AuthorizeAttribute_CannotUseWithinChildActionCache", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The action &apos;{0}&apos; is accessible only by a child request..
+ /// </summary>
+ internal static string ChildActionOnlyAttribute_MustBeInChildRequest {
+ get {
+ return ResourceManager.GetString("ChildActionOnlyAttribute_MustBeInChildRequest", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The field {0} must be a date..
+ /// </summary>
+ internal static string ClientDataTypeModelValidatorProvider_FieldMustBeDate {
+ get {
+ return ResourceManager.GetString("ClientDataTypeModelValidatorProvider_FieldMustBeDate", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The field {0} must be a number..
+ /// </summary>
+ internal static string ClientDataTypeModelValidatorProvider_FieldMustBeNumeric {
+ get {
+ return ResourceManager.GetString("ClientDataTypeModelValidatorProvider_FieldMustBeNumeric", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to No route in the route table matches the supplied values..
+ /// </summary>
+ internal static string Common_NoRouteMatched {
+ get {
+ return ResourceManager.GetString("Common_NoRouteMatched", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Value cannot be null or empty..
+ /// </summary>
+ internal static string Common_NullOrEmpty {
+ get {
+ return ResourceManager.GetString("Common_NullOrEmpty", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The partial view &apos;{0}&apos; was not found or no view engine supports the searched locations. The following locations were searched:{1}.
+ /// </summary>
+ internal static string Common_PartialViewNotFound {
+ get {
+ return ResourceManager.GetString("Common_PartialViewNotFound", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The property &apos;{0}&apos; cannot be null or empty..
+ /// </summary>
+ internal static string Common_PropertyCannotBeNullOrEmpty {
+ get {
+ return ResourceManager.GetString("Common_PropertyCannotBeNullOrEmpty", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The property {0}.{1} could not be found..
+ /// </summary>
+ internal static string Common_PropertyNotFound {
+ get {
+ return ResourceManager.GetString("Common_PropertyNotFound", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to False.
+ /// </summary>
+ internal static string Common_TriState_False {
+ get {
+ return ResourceManager.GetString("Common_TriState_False", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Not Set.
+ /// </summary>
+ internal static string Common_TriState_NotSet {
+ get {
+ return ResourceManager.GetString("Common_TriState_NotSet", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to True.
+ /// </summary>
+ internal static string Common_TriState_True {
+ get {
+ return ResourceManager.GetString("Common_TriState_True", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The type {0} must derive from {1}.
+ /// </summary>
+ internal static string Common_TypeMustDriveFromType {
+ get {
+ return ResourceManager.GetString("Common_TypeMustDriveFromType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The value &apos;{0}&apos; is invalid..
+ /// </summary>
+ internal static string Common_ValueNotValidForProperty {
+ get {
+ return ResourceManager.GetString("Common_ValueNotValidForProperty", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The view &apos;{0}&apos; or its master was not found or no view engine supports the searched locations. The following locations were searched:{1}.
+ /// </summary>
+ internal static string Common_ViewNotFound {
+ get {
+ return ResourceManager.GetString("Common_ViewNotFound", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &apos;{0}&apos; and &apos;{1}&apos; do not match..
+ /// </summary>
+ internal static string CompareAttribute_MustMatch {
+ get {
+ return ResourceManager.GetString("CompareAttribute_MustMatch", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Could not find a property named {0}..
+ /// </summary>
+ internal static string CompareAttribute_UnknownProperty {
+ get {
+ return ResourceManager.GetString("CompareAttribute_UnknownProperty", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A public action method &apos;{0}&apos; was not found on controller &apos;{1}&apos;..
+ /// </summary>
+ internal static string Controller_UnknownAction {
+ get {
+ return ResourceManager.GetString("Controller_UnknownAction", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The model of type &apos;{0}&apos; could not be updated..
+ /// </summary>
+ internal static string Controller_UpdateModel_UpdateUnsuccessful {
+ get {
+ return ResourceManager.GetString("Controller_UpdateModel_UpdateUnsuccessful", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The model of type &apos;{0}&apos; is not valid..
+ /// </summary>
+ internal static string Controller_Validate_ValidationFailed {
+ get {
+ return ResourceManager.GetString("Controller_Validate_ValidationFailed", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Cannot execute Controller with a null HttpContext..
+ /// </summary>
+ internal static string ControllerBase_CannotExecuteWithNullHttpContext {
+ get {
+ return ResourceManager.GetString("ControllerBase_CannotExecuteWithNullHttpContext", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A single instance of controller &apos;{0}&apos; cannot be used to handle multiple requests. If a custom controller factory is in use, make sure that it creates a new instance of the controller for each request..
+ /// </summary>
+ internal static string ControllerBase_CannotHandleMultipleRequests {
+ get {
+ return ResourceManager.GetString("ControllerBase_CannotHandleMultipleRequests", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to An error occurred when trying to create the IControllerFactory &apos;{0}&apos;. Make sure that the controller factory has a public parameterless constructor..
+ /// </summary>
+ internal static string ControllerBuilder_ErrorCreatingControllerFactory {
+ get {
+ return ResourceManager.GetString("ControllerBuilder_ErrorCreatingControllerFactory", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The IControllerFactory &apos;{0}&apos; did not return a controller for the name &apos;{1}&apos;..
+ /// </summary>
+ internal static string ControllerBuilder_FactoryReturnedNull {
+ get {
+ return ResourceManager.GetString("ControllerBuilder_FactoryReturnedNull", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The controller factory type &apos;{0}&apos; must implement the IControllerFactory interface..
+ /// </summary>
+ internal static string ControllerBuilder_MissingIControllerFactory {
+ get {
+ return ResourceManager.GetString("ControllerBuilder_MissingIControllerFactory", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The view found at &apos;{0}&apos; was not created..
+ /// </summary>
+ internal static string CshtmlView_ViewCouldNotBeCreated {
+ get {
+ return ResourceManager.GetString("CshtmlView_ViewCouldNotBeCreated", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The view at &apos;{0}&apos; must derive from WebViewPage, or WebViewPage&lt;TModel&gt;..
+ /// </summary>
+ internal static string CshtmlView_WrongViewBase {
+ get {
+ return ResourceManager.GetString("CshtmlView_WrongViewBase", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to {0} has a DisplayColumn attribute for {1}, but property {1} does not exist..
+ /// </summary>
+ internal static string DataAnnotationsModelMetadataProvider_UnknownProperty {
+ get {
+ return ResourceManager.GetString("DataAnnotationsModelMetadataProvider_UnknownProperty", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to {0} has a DisplayColumn attribute for {1}, but property {1} does not have a public getter..
+ /// </summary>
+ internal static string DataAnnotationsModelMetadataProvider_UnreadableProperty {
+ get {
+ return ResourceManager.GetString("DataAnnotationsModelMetadataProvider_UnreadableProperty", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The type {0} must have a public constructor which accepts three parameters of types {1}, {2}, and {3}.
+ /// </summary>
+ internal static string DataAnnotationsModelValidatorProvider_ConstructorRequirements {
+ get {
+ return ResourceManager.GetString("DataAnnotationsModelValidatorProvider_ConstructorRequirements", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The type {0} must have a public constructor which accepts two parameters of types {1} and {2}..
+ /// </summary>
+ internal static string DataAnnotationsModelValidatorProvider_ValidatableConstructorRequirements {
+ get {
+ return ResourceManager.GetString("DataAnnotationsModelValidatorProvider_ValidatableConstructorRequirements", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Multiple types were found that match the controller named &apos;{0}&apos;. This can happen if the route that services this request does not specify namespaces to search for a controller that matches the request. If this is the case, register this route by calling an overload of the &apos;MapRoute&apos; method that takes a &apos;namespaces&apos; parameter.
+ ///
+ ///The request for &apos;{0}&apos; has found the following matching controllers:{1}.
+ /// </summary>
+ internal static string DefaultControllerFactory_ControllerNameAmbiguous_WithoutRouteUrl {
+ get {
+ return ResourceManager.GetString("DefaultControllerFactory_ControllerNameAmbiguous_WithoutRouteUrl", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Multiple types were found that match the controller named &apos;{0}&apos;. This can happen if the route that services this request (&apos;{1}&apos;) does not specify namespaces to search for a controller that matches the request. If this is the case, register this route by calling an overload of the &apos;MapRoute&apos; method that takes a &apos;namespaces&apos; parameter.
+ ///
+ ///The request for &apos;{0}&apos; has found the following matching controllers:{2}.
+ /// </summary>
+ internal static string DefaultControllerFactory_ControllerNameAmbiguous_WithRouteUrl {
+ get {
+ return ResourceManager.GetString("DefaultControllerFactory_ControllerNameAmbiguous_WithRouteUrl", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to An error occurred when trying to create a controller of type &apos;{0}&apos;. Make sure that the controller has a parameterless public constructor..
+ /// </summary>
+ internal static string DefaultControllerFactory_ErrorCreatingController {
+ get {
+ return ResourceManager.GetString("DefaultControllerFactory_ErrorCreatingController", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The controller for path &apos;{0}&apos; was not found or does not implement IController..
+ /// </summary>
+ internal static string DefaultControllerFactory_NoControllerFound {
+ get {
+ return ResourceManager.GetString("DefaultControllerFactory_NoControllerFound", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The controller type &apos;{0}&apos; must implement IController..
+ /// </summary>
+ internal static string DefaultControllerFactory_TypeDoesNotSubclassControllerBase {
+ get {
+ return ResourceManager.GetString("DefaultControllerFactory_TypeDoesNotSubclassControllerBase", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The value &apos;{0}&apos; is not valid for {1}..
+ /// </summary>
+ internal static string DefaultModelBinder_ValueInvalid {
+ get {
+ return ResourceManager.GetString("DefaultModelBinder_ValueInvalid", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A value is required..
+ /// </summary>
+ internal static string DefaultModelBinder_ValueRequired {
+ get {
+ return ResourceManager.GetString("DefaultModelBinder_ValueRequired", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The number of ticks for the TimeSpan value must be greater than or equal to 0..
+ /// </summary>
+ internal static string DefaultViewLocationCache_NegativeTimeSpan {
+ get {
+ return ResourceManager.GetString("DefaultViewLocationCache_NegativeTimeSpan", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The type {0} does not appear to implement Microsoft.Practices.ServiceLocation.IServiceLocator..
+ /// </summary>
+ internal static string DependencyResolver_DoesNotImplementICommonServiceLocator {
+ get {
+ return ResourceManager.GetString("DependencyResolver_DoesNotImplementICommonServiceLocator", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The type &apos;{0}&apos; does not inherit from Exception..
+ /// </summary>
+ internal static string ExceptionViewAttribute_NonExceptionType {
+ get {
+ return ResourceManager.GetString("ExceptionViewAttribute_NonExceptionType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The expression compiler was unable to evaluate the indexer expression &apos;{0}&apos; because it references the model parameter &apos;{1}&apos; which is unavailable..
+ /// </summary>
+ internal static string ExpressionHelper_InvalidIndexerExpression {
+ get {
+ return ResourceManager.GetString("ExpressionHelper_InvalidIndexerExpression", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Order must be greater than or equal to -1..
+ /// </summary>
+ internal static string FilterAttribute_OrderOutOfRange {
+ get {
+ return ResourceManager.GetString("FilterAttribute_OrderOutOfRange", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The GET and POST HTTP methods are not supported..
+ /// </summary>
+ internal static string HtmlHelper_InvalidHttpMethod {
+ get {
+ return ResourceManager.GetString("HtmlHelper_InvalidHttpMethod", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The specified HttpVerbs value is not supported. The supported values are Delete, Head, and Put..
+ /// </summary>
+ internal static string HtmlHelper_InvalidHttpVerb {
+ get {
+ return ResourceManager.GetString("HtmlHelper_InvalidHttpVerb", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to There is no ViewData item of type &apos;{1}&apos; that has the key &apos;{0}&apos;..
+ /// </summary>
+ internal static string HtmlHelper_MissingSelectData {
+ get {
+ return ResourceManager.GetString("HtmlHelper_MissingSelectData", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The parameter &apos;{0}&apos; must evaluate to an IEnumerable when multiple selection is allowed..
+ /// </summary>
+ internal static string HtmlHelper_SelectExpressionNotEnumerable {
+ get {
+ return ResourceManager.GetString("HtmlHelper_SelectExpressionNotEnumerable", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The value must be greater than or equal to zero..
+ /// </summary>
+ internal static string HtmlHelper_TextAreaParameterOutOfRange {
+ get {
+ return ResourceManager.GetString("HtmlHelper_TextAreaParameterOutOfRange", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The ViewData item that has the key &apos;{0}&apos; is of type &apos;{1}&apos; but must be of type &apos;{2}&apos;..
+ /// </summary>
+ internal static string HtmlHelper_WrongSelectDataType {
+ get {
+ return ResourceManager.GetString("HtmlHelper_WrongSelectDataType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to This request has been blocked because sensitive information could be disclosed to third party web sites when this is used in a GET request. To allow GET requests, set JsonRequestBehavior to AllowGet..
+ /// </summary>
+ internal static string JsonRequest_GetNotAllowed {
+ get {
+ return ResourceManager.GetString("JsonRequest_GetNotAllowed", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The JSON request was too large to be deserialized..
+ /// </summary>
+ internal static string JsonValueProviderFactory_RequestTooLarge {
+ get {
+ return ResourceManager.GetString("JsonValueProviderFactory_RequestTooLarge", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to An error occurred when trying to create the IModelBinder &apos;{0}&apos;. Make sure that the binder has a public parameterless constructor..
+ /// </summary>
+ internal static string ModelBinderAttribute_ErrorCreatingModelBinder {
+ get {
+ return ResourceManager.GetString("ModelBinderAttribute_ErrorCreatingModelBinder", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The type &apos;{0}&apos; does not implement the IModelBinder interface..
+ /// </summary>
+ internal static string ModelBinderAttribute_TypeNotIModelBinder {
+ get {
+ return ResourceManager.GetString("ModelBinderAttribute_TypeNotIModelBinder", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The type &apos;{0}&apos; contains multiple attributes that inherit from CustomModelBinderAttribute..
+ /// </summary>
+ internal static string ModelBinderDictionary_MultipleAttributes {
+ get {
+ return ResourceManager.GetString("ModelBinderDictionary_MultipleAttributes", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to This property setter is obsolete, because its value is derived from ModelMetadata.Model now..
+ /// </summary>
+ internal static string ModelMetadata_PropertyNotSettable {
+ get {
+ return ResourceManager.GetString("ModelMetadata_PropertyNotSettable", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to This constructor is obsolete, because its functionality has been moved to MvcForm(ViewContext) now..
+ /// </summary>
+ internal static string MvcForm_ConstructorObsolete {
+ get {
+ return ResourceManager.GetString("MvcForm_ConstructorObsolete", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The &apos;inherits&apos; keyword is not allowed when a &apos;{0}&apos; keyword is used..
+ /// </summary>
+ internal static string MvcRazorCodeParser_CannotHaveModelAndInheritsKeyword {
+ get {
+ return ResourceManager.GetString("MvcRazorCodeParser_CannotHaveModelAndInheritsKeyword", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The &apos;{0}&apos; keyword must be followed by a type name on the same line..
+ /// </summary>
+ internal static string MvcRazorCodeParser_ModelKeywordMustBeFollowedByTypeName {
+ get {
+ return ResourceManager.GetString("MvcRazorCodeParser_ModelKeywordMustBeFollowedByTypeName", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Only one &apos;{0}&apos; statement is allowed in a file..
+ /// </summary>
+ internal static string MvcRazorCodeParser_OnlyOneModelStatementIsAllowed {
+ get {
+ return ResourceManager.GetString("MvcRazorCodeParser_OnlyOneModelStatementIsAllowed", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The matched route does not include a &apos;controller&apos; route value, which is required..
+ /// </summary>
+ internal static string MvcRouteHandler_RouteValuesHasNoController {
+ get {
+ return ResourceManager.GetString("MvcRouteHandler_RouteValuesHasNoController", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to OutputCacheAttribute is not allowed on child actions which are children of an already cached child action..
+ /// </summary>
+ internal static string OutputCacheAttribute_CannotNestChildCache {
+ get {
+ return ResourceManager.GetString("OutputCacheAttribute_CannotNestChildCache", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to OutputCacheAttribute for child actions only supports Duration, VaryByCustom, and VaryByParam values. Please do not set CacheProfile, Location, NoStore, SqlDependency, VaryByContentEncoding, or VaryByHeader values for child actions..
+ /// </summary>
+ internal static string OutputCacheAttribute_ChildAction_UnsupportedSetting {
+ get {
+ return ResourceManager.GetString("OutputCacheAttribute_ChildAction_UnsupportedSetting", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Duration must be a positive number..
+ /// </summary>
+ internal static string OutputCacheAttribute_InvalidDuration {
+ get {
+ return ResourceManager.GetString("OutputCacheAttribute_InvalidDuration", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to VaryByParam must be &apos;*&apos;, &apos;none&apos;, or a semicolon-delimited list of keys..
+ /// </summary>
+ internal static string OutputCacheAttribute_InvalidVaryByParam {
+ get {
+ return ResourceManager.GetString("OutputCacheAttribute_InvalidVaryByParam", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Child actions are not allowed to perform redirect actions..
+ /// </summary>
+ internal static string RedirectAction_CannotRedirectInChildAction {
+ get {
+ return ResourceManager.GetString("RedirectAction_CannotRedirectInChildAction", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Cannot create a descriptor for instance method &apos;{0}&apos; on type &apos;{1}&apos; because the type does not derive from ControllerBase..
+ /// </summary>
+ internal static string ReflectedActionDescriptor_CannotCallInstanceMethodOnNonControllerType {
+ get {
+ return ResourceManager.GetString("ReflectedActionDescriptor_CannotCallInstanceMethodOnNonControllerType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Cannot call action method &apos;{0}&apos; on controller &apos;{1}&apos; because the parameter &apos;{2}&apos; is passed by reference..
+ /// </summary>
+ internal static string ReflectedActionDescriptor_CannotCallMethodsWithOutOrRefParameters {
+ get {
+ return ResourceManager.GetString("ReflectedActionDescriptor_CannotCallMethodsWithOutOrRefParameters", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Cannot call action method &apos;{0}&apos; on controller &apos;{1}&apos; because the action method is a generic method..
+ /// </summary>
+ internal static string ReflectedActionDescriptor_CannotCallOpenGenericMethods {
+ get {
+ return ResourceManager.GetString("ReflectedActionDescriptor_CannotCallOpenGenericMethods", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Cannot call action method &apos;{0}&apos; on controller &apos;{1}&apos; because the action method is a static method..
+ /// </summary>
+ internal static string ReflectedActionDescriptor_CannotCallStaticMethod {
+ get {
+ return ResourceManager.GetString("ReflectedActionDescriptor_CannotCallStaticMethod", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The parameters dictionary contains a null entry for parameter &apos;{0}&apos; of non-nullable type &apos;{1}&apos; for method &apos;{2}&apos; in &apos;{3}&apos;. An optional parameter must be a reference type, a nullable type, or be declared as an optional parameter..
+ /// </summary>
+ internal static string ReflectedActionDescriptor_ParameterCannotBeNull {
+ get {
+ return ResourceManager.GetString("ReflectedActionDescriptor_ParameterCannotBeNull", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The parameters dictionary does not contain an entry for parameter &apos;{0}&apos; of type &apos;{1}&apos; for method &apos;{2}&apos; in &apos;{3}&apos;. The dictionary must contain an entry for each parameter, including parameters that have null values..
+ /// </summary>
+ internal static string ReflectedActionDescriptor_ParameterNotInDictionary {
+ get {
+ return ResourceManager.GetString("ReflectedActionDescriptor_ParameterNotInDictionary", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The parameters dictionary contains an invalid entry for parameter &apos;{0}&apos; for method &apos;{1}&apos; in &apos;{2}&apos;. The dictionary contains a value of type &apos;{3}&apos;, but the parameter requires a value of type &apos;{4}&apos;..
+ /// </summary>
+ internal static string ReflectedActionDescriptor_ParameterValueHasWrongType {
+ get {
+ return ResourceManager.GetString("ReflectedActionDescriptor_ParameterValueHasWrongType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The parameter &apos;{0}&apos; on method &apos;{1}&apos; contains multiple attributes that inherit from CustomModelBinderAttribute..
+ /// </summary>
+ internal static string ReflectedParameterBindingInfo_MultipleConverterAttributes {
+ get {
+ return ResourceManager.GetString("ReflectedParameterBindingInfo_MultipleConverterAttributes", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to No url for remote validation could be found..
+ /// </summary>
+ internal static string RemoteAttribute_NoUrlFound {
+ get {
+ return ResourceManager.GetString("RemoteAttribute_NoUrlFound", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &apos;{0}&apos; is invalid..
+ /// </summary>
+ internal static string RemoteAttribute_RemoteValidationFailed {
+ get {
+ return ResourceManager.GetString("RemoteAttribute_RemoteValidationFailed", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The requested resource can only be accessed via SSL..
+ /// </summary>
+ internal static string RequireHttpsAttribute_MustUseSsl {
+ get {
+ return ResourceManager.GetString("RequireHttpsAttribute_MustUseSsl", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The SessionStateTempDataProvider class requires session state to be enabled..
+ /// </summary>
+ internal static string SessionStateTempDataProvider_SessionStateDisabled {
+ get {
+ return ResourceManager.GetString("SessionStateTempDataProvider_SessionStateDisabled", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to An instance of {0} was found in the resolver as well as a custom registered provider in {1}. Please set only one or the other..
+ /// </summary>
+ internal static string SingleServiceResolver_CannotRegisterTwoInstances {
+ get {
+ return ResourceManager.GetString("SingleServiceResolver_CannotRegisterTwoInstances", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to An operation that crossed a synchronization context failed. See the inner exception for more information..
+ /// </summary>
+ internal static string SynchronizationContextUtil_ExceptionThrown {
+ get {
+ return ResourceManager.GetString("SynchronizationContextUtil_ExceptionThrown", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The asynchronous action method &apos;{0}&apos; returns a Task, which cannot be executed synchronously..
+ /// </summary>
+ internal static string TaskAsyncActionDescriptor_CannotExecuteSynchronously {
+ get {
+ return ResourceManager.GetString("TaskAsyncActionDescriptor_CannotExecuteSynchronously", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Unable to locate an appropriate template for type {0}..
+ /// </summary>
+ internal static string TemplateHelpers_NoTemplate {
+ get {
+ return ResourceManager.GetString("TemplateHelpers_NoTemplate", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Templates can be used only with field access, property access, single-dimension array index, or single-parameter custom indexer expressions..
+ /// </summary>
+ internal static string TemplateHelpers_TemplateLimitations {
+ get {
+ return ResourceManager.GetString("TemplateHelpers_TemplateLimitations", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The Collection template was used with an object of type &apos;{0}&apos;, which does not implement System.IEnumerable..
+ /// </summary>
+ internal static string Templates_TypeMustImplementIEnumerable {
+ get {
+ return ResourceManager.GetString("Templates_TypeMustImplementIEnumerable", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to This file is automatically generated. Please do not modify the contents of this file..
+ /// </summary>
+ internal static string TypeCache_DoNotModify {
+ get {
+ return ResourceManager.GetString("TypeCache_DoNotModify", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The model object inside the metadata claimed to be compatible with {0}, but was actually {1}..
+ /// </summary>
+ internal static string ValidatableObjectAdapter_IncompatibleType {
+ get {
+ return ResourceManager.GetString("ValidatableObjectAdapter_IncompatibleType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The parameter conversion from type &apos;{0}&apos; to type &apos;{1}&apos; failed. See the inner exception for more information..
+ /// </summary>
+ internal static string ValueProviderResult_ConversionThrew {
+ get {
+ return ResourceManager.GetString("ValueProviderResult_ConversionThrew", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The parameter conversion from type &apos;{0}&apos; to type &apos;{1}&apos; failed because no type converter can convert between these types..
+ /// </summary>
+ internal static string ValueProviderResult_NoConverterExists {
+ get {
+ return ResourceManager.GetString("ValueProviderResult_NoConverterExists", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The model item passed into the dictionary is null, but this dictionary requires a non-null model item of type &apos;{0}&apos;..
+ /// </summary>
+ internal static string ViewDataDictionary_ModelCannotBeNull {
+ get {
+ return ResourceManager.GetString("ViewDataDictionary_ModelCannotBeNull", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The model item passed into the dictionary is of type &apos;{0}&apos;, but this dictionary requires a model item of type &apos;{1}&apos;..
+ /// </summary>
+ internal static string ViewDataDictionary_WrongTModelType {
+ get {
+ return ResourceManager.GetString("ViewDataDictionary_WrongTModelType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A ViewMasterPage can be used only with content pages that derive from ViewPage or ViewPage&lt;TModel&gt;..
+ /// </summary>
+ internal static string ViewMasterPage_RequiresViewPage {
+ get {
+ return ResourceManager.GetString("ViewMasterPage_RequiresViewPage", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Execution of the child request failed. Please examine the InnerException for more information..
+ /// </summary>
+ internal static string ViewPageHttpHandlerWrapper_ExceptionOccurred {
+ get {
+ return ResourceManager.GetString("ViewPageHttpHandlerWrapper_ExceptionOccurred", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A ViewStartPage can be used only with with a page that derives from WebViewPage or another ViewStartPage..
+ /// </summary>
+ internal static string ViewStartPage_RequiresMvcRazorView {
+ get {
+ return ResourceManager.GetString("ViewStartPage_RequiresMvcRazorView", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The ViewUserControl &apos;{0}&apos; cannot find an IViewDataContainer object. The ViewUserControl must be inside a ViewPage, a ViewMasterPage, or another ViewUserControl..
+ /// </summary>
+ internal static string ViewUserControl_RequiresViewDataProvider {
+ get {
+ return ResourceManager.GetString("ViewUserControl_RequiresViewDataProvider", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A ViewUserControl can be used only in pages that derive from ViewPage or ViewPage&lt;TModel&gt;..
+ /// </summary>
+ internal static string ViewUserControl_RequiresViewPage {
+ get {
+ return ResourceManager.GetString("ViewUserControl_RequiresViewPage", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A master name cannot be specified when the view is a ViewUserControl..
+ /// </summary>
+ internal static string WebFormViewEngine_UserControlCannotHaveMaster {
+ get {
+ return ResourceManager.GetString("WebFormViewEngine_UserControlCannotHaveMaster", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The view at &apos;{0}&apos; must derive from ViewPage, ViewPage&lt;TModel&gt;, ViewUserControl, or ViewUserControl&lt;TModel&gt;..
+ /// </summary>
+ internal static string WebFormViewEngine_WrongViewBase {
+ get {
+ return ResourceManager.GetString("WebFormViewEngine_WrongViewBase", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Properties/MvcResources.resx b/src/System.Web.Mvc/Properties/MvcResources.resx
new file mode 100644
index 00000000..5bcc68f5
--- /dev/null
+++ b/src/System.Web.Mvc/Properties/MvcResources.resx
@@ -0,0 +1,439 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="ActionMethodSelector_AmbiguousMatch" xml:space="preserve">
+ <value>The current request for action '{0}' on controller type '{1}' is ambiguous between the following action methods:{2}</value>
+ </data>
+ <data name="Common_NoRouteMatched" xml:space="preserve">
+ <value>No route in the route table matches the supplied values.</value>
+ </data>
+ <data name="Common_NullOrEmpty" xml:space="preserve">
+ <value>Value cannot be null or empty.</value>
+ </data>
+ <data name="Common_PartialViewNotFound" xml:space="preserve">
+ <value>The partial view '{0}' was not found or no view engine supports the searched locations. The following locations were searched:{1}</value>
+ </data>
+ <data name="Common_PropertyCannotBeNullOrEmpty" xml:space="preserve">
+ <value>The property '{0}' cannot be null or empty.</value>
+ </data>
+ <data name="Common_ViewNotFound" xml:space="preserve">
+ <value>The view '{0}' or its master was not found or no view engine supports the searched locations. The following locations were searched:{1}</value>
+ </data>
+ <data name="ControllerBuilder_ErrorCreatingControllerFactory" xml:space="preserve">
+ <value>An error occurred when trying to create the IControllerFactory '{0}'. Make sure that the controller factory has a public parameterless constructor.</value>
+ </data>
+ <data name="ControllerBuilder_FactoryReturnedNull" xml:space="preserve">
+ <value>The IControllerFactory '{0}' did not return a controller for the name '{1}'.</value>
+ </data>
+ <data name="ControllerBuilder_MissingIControllerFactory" xml:space="preserve">
+ <value>The controller factory type '{0}' must implement the IControllerFactory interface.</value>
+ </data>
+ <data name="Controller_UnknownAction" xml:space="preserve">
+ <value>A public action method '{0}' was not found on controller '{1}'.</value>
+ </data>
+ <data name="DefaultControllerFactory_ErrorCreatingController" xml:space="preserve">
+ <value>An error occurred when trying to create a controller of type '{0}'. Make sure that the controller has a parameterless public constructor.</value>
+ </data>
+ <data name="DefaultControllerFactory_NoControllerFound" xml:space="preserve">
+ <value>The controller for path '{0}' was not found or does not implement IController.</value>
+ </data>
+ <data name="DefaultControllerFactory_TypeDoesNotSubclassControllerBase" xml:space="preserve">
+ <value>The controller type '{0}' must implement IController.</value>
+ </data>
+ <data name="ValueProviderResult_ConversionThrew" xml:space="preserve">
+ <value>The parameter conversion from type '{0}' to type '{1}' failed. See the inner exception for more information.</value>
+ </data>
+ <data name="ValueProviderResult_NoConverterExists" xml:space="preserve">
+ <value>The parameter conversion from type '{0}' to type '{1}' failed because no type converter can convert between these types.</value>
+ </data>
+ <data name="ExceptionViewAttribute_NonExceptionType" xml:space="preserve">
+ <value>The type '{0}' does not inherit from Exception.</value>
+ </data>
+ <data name="FilterAttribute_OrderOutOfRange" xml:space="preserve">
+ <value>Order must be greater than or equal to -1.</value>
+ </data>
+ <data name="HtmlHelper_MissingSelectData" xml:space="preserve">
+ <value>There is no ViewData item of type '{1}' that has the key '{0}'.</value>
+ </data>
+ <data name="HtmlHelper_TextAreaParameterOutOfRange" xml:space="preserve">
+ <value>The value must be greater than or equal to zero.</value>
+ </data>
+ <data name="HtmlHelper_WrongSelectDataType" xml:space="preserve">
+ <value>The ViewData item that has the key '{0}' is of type '{1}' but must be of type '{2}'.</value>
+ </data>
+ <data name="ModelBinderAttribute_ErrorCreatingModelBinder" xml:space="preserve">
+ <value>An error occurred when trying to create the IModelBinder '{0}'. Make sure that the binder has a public parameterless constructor.</value>
+ </data>
+ <data name="ModelBinderAttribute_TypeNotIModelBinder" xml:space="preserve">
+ <value>The type '{0}' does not implement the IModelBinder interface.</value>
+ </data>
+ <data name="ModelBinderDictionary_MultipleAttributes" xml:space="preserve">
+ <value>The type '{0}' contains multiple attributes that inherit from CustomModelBinderAttribute.</value>
+ </data>
+ <data name="SessionStateTempDataProvider_SessionStateDisabled" xml:space="preserve">
+ <value>The SessionStateTempDataProvider class requires session state to be enabled.</value>
+ </data>
+ <data name="ViewDataDictionary_WrongTModelType" xml:space="preserve">
+ <value>The model item passed into the dictionary is of type '{0}', but this dictionary requires a model item of type '{1}'.</value>
+ </data>
+ <data name="ViewMasterPage_RequiresViewPage" xml:space="preserve">
+ <value>A ViewMasterPage can be used only with content pages that derive from ViewPage or ViewPage&lt;TModel&gt;.</value>
+ </data>
+ <data name="ViewUserControl_RequiresViewDataProvider" xml:space="preserve">
+ <value>The ViewUserControl '{0}' cannot find an IViewDataContainer object. The ViewUserControl must be inside a ViewPage, a ViewMasterPage, or another ViewUserControl.</value>
+ </data>
+ <data name="ViewUserControl_RequiresViewPage" xml:space="preserve">
+ <value>A ViewUserControl can be used only in pages that derive from ViewPage or ViewPage&lt;TModel&gt;.</value>
+ </data>
+ <data name="WebFormViewEngine_UserControlCannotHaveMaster" xml:space="preserve">
+ <value>A master name cannot be specified when the view is a ViewUserControl.</value>
+ </data>
+ <data name="WebFormViewEngine_WrongViewBase" xml:space="preserve">
+ <value>The view at '{0}' must derive from ViewPage, ViewPage&lt;TModel&gt;, ViewUserControl, or ViewUserControl&lt;TModel&gt;.</value>
+ </data>
+ <data name="Common_ValueNotValidForProperty" xml:space="preserve">
+ <value>The value '{0}' is invalid.</value>
+ </data>
+ <data name="ActionMethodSelector_AmbiguousMatchType" xml:space="preserve">
+ <value>{0} on type {1}</value>
+ </data>
+ <data name="Controller_UpdateModel_UpdateUnsuccessful" xml:space="preserve">
+ <value>The model of type '{0}' could not be updated.</value>
+ </data>
+ <data name="DefaultModelBinder_ValueRequired" xml:space="preserve">
+ <value>A value is required.</value>
+ </data>
+ <data name="ReflectedActionDescriptor_ParameterCannotBeNull" xml:space="preserve">
+ <value>The parameters dictionary contains a null entry for parameter '{0}' of non-nullable type '{1}' for method '{2}' in '{3}'. An optional parameter must be a reference type, a nullable type, or be declared as an optional parameter.</value>
+ </data>
+ <data name="ReflectedActionDescriptor_ParameterNotInDictionary" xml:space="preserve">
+ <value>The parameters dictionary does not contain an entry for parameter '{0}' of type '{1}' for method '{2}' in '{3}'. The dictionary must contain an entry for each parameter, including parameters that have null values.</value>
+ </data>
+ <data name="ReflectedActionDescriptor_ParameterValueHasWrongType" xml:space="preserve">
+ <value>The parameters dictionary contains an invalid entry for parameter '{0}' for method '{1}' in '{2}'. The dictionary contains a value of type '{3}', but the parameter requires a value of type '{4}'.</value>
+ </data>
+ <data name="ReflectedParameterBindingInfo_MultipleConverterAttributes" xml:space="preserve">
+ <value>The parameter '{0}' on method '{1}' contains multiple attributes that inherit from CustomModelBinderAttribute.</value>
+ </data>
+ <data name="ReflectedActionDescriptor_CannotCallInstanceMethodOnNonControllerType" xml:space="preserve">
+ <value>Cannot create a descriptor for instance method '{0}' on type '{1}' because the type does not derive from ControllerBase.</value>
+ </data>
+ <data name="ReflectedActionDescriptor_CannotCallMethodsWithOutOrRefParameters" xml:space="preserve">
+ <value>Cannot call action method '{0}' on controller '{1}' because the parameter '{2}' is passed by reference.</value>
+ </data>
+ <data name="ReflectedActionDescriptor_CannotCallOpenGenericMethods" xml:space="preserve">
+ <value>Cannot call action method '{0}' on controller '{1}' because the action method is a generic method.</value>
+ </data>
+ <data name="DefaultViewLocationCache_NegativeTimeSpan" xml:space="preserve">
+ <value>The number of ticks for the TimeSpan value must be greater than or equal to 0.</value>
+ </data>
+ <data name="DefaultModelBinder_ValueInvalid" xml:space="preserve">
+ <value>The value '{0}' is not valid for {1}.</value>
+ </data>
+ <data name="TemplateHelpers_TemplateLimitations" xml:space="preserve">
+ <value>Templates can be used only with field access, property access, single-dimension array index, or single-parameter custom indexer expressions.</value>
+ </data>
+ <data name="Common_TriState_False" xml:space="preserve">
+ <value>False</value>
+ </data>
+ <data name="Common_TriState_NotSet" xml:space="preserve">
+ <value>Not Set</value>
+ </data>
+ <data name="Common_TriState_True" xml:space="preserve">
+ <value>True</value>
+ </data>
+ <data name="ControllerBase_CannotHandleMultipleRequests" xml:space="preserve">
+ <value>A single instance of controller '{0}' cannot be used to handle multiple requests. If a custom controller factory is in use, make sure that it creates a new instance of the controller for each request.</value>
+ </data>
+ <data name="Common_PropertyNotFound" xml:space="preserve">
+ <value>The property {0}.{1} could not be found.</value>
+ </data>
+ <data name="DataAnnotationsModelMetadataProvider_UnknownProperty" xml:space="preserve">
+ <value>{0} has a DisplayColumn attribute for {1}, but property {1} does not exist.</value>
+ </data>
+ <data name="DataAnnotationsModelMetadataProvider_UnreadableProperty" xml:space="preserve">
+ <value>{0} has a DisplayColumn attribute for {1}, but property {1} does not have a public getter.</value>
+ </data>
+ <data name="TemplateHelpers_NoTemplate" xml:space="preserve">
+ <value>Unable to locate an appropriate template for type {0}.</value>
+ </data>
+ <data name="RequireHttpsAttribute_MustUseSsl" xml:space="preserve">
+ <value>The requested resource can only be accessed via SSL.</value>
+ </data>
+ <data name="HtmlHelper_InvalidHttpVerb" xml:space="preserve">
+ <value>The specified HttpVerbs value is not supported. The supported values are Delete, Head, and Put.</value>
+ </data>
+ <data name="HtmlHelper_InvalidHttpMethod" xml:space="preserve">
+ <value>The GET and POST HTTP methods are not supported.</value>
+ </data>
+ <data name="JsonRequest_GetNotAllowed" xml:space="preserve">
+ <value>This request has been blocked because sensitive information could be disclosed to third party web sites when this is used in a GET request. To allow GET requests, set JsonRequestBehavior to AllowGet.</value>
+ </data>
+ <data name="ModelMetadata_PropertyNotSettable" xml:space="preserve">
+ <value>This property setter is obsolete, because its value is derived from ModelMetadata.Model now.</value>
+ </data>
+ <data name="ViewDataDictionary_ModelCannotBeNull" xml:space="preserve">
+ <value>The model item passed into the dictionary is null, but this dictionary requires a non-null model item of type '{0}'.</value>
+ </data>
+ <data name="Common_TypeMustDriveFromType" xml:space="preserve">
+ <value>The type {0} must derive from {1}</value>
+ </data>
+ <data name="DataAnnotationsModelValidatorProvider_ConstructorRequirements" xml:space="preserve">
+ <value>The type {0} must have a public constructor which accepts three parameters of types {1}, {2}, and {3}</value>
+ </data>
+ <data name="ViewPageHttpHandlerWrapper_ExceptionOccurred" xml:space="preserve">
+ <value>Execution of the child request failed. Please examine the InnerException for more information.</value>
+ </data>
+ <data name="RedirectAction_CannotRedirectInChildAction" xml:space="preserve">
+ <value>Child actions are not allowed to perform redirect actions.</value>
+ </data>
+ <data name="AsyncCommon_AsyncResultAlreadyConsumed" xml:space="preserve">
+ <value>The provided IAsyncResult has already been consumed.</value>
+ </data>
+ <data name="AsyncCommon_InvalidAsyncResult" xml:space="preserve">
+ <value>The provided IAsyncResult is not valid for this method.</value>
+ </data>
+ <data name="SynchronizationContextUtil_ExceptionThrown" xml:space="preserve">
+ <value>An operation that crossed a synchronization context failed. See the inner exception for more information.</value>
+ </data>
+ <data name="AsyncCommon_ControllerMustImplementIAsyncManagerContainer" xml:space="preserve">
+ <value>The controller of type '{0}' must subclass AsyncController or implement the IAsyncManagerContainer interface.</value>
+ </data>
+ <data name="AsyncCommon_InvalidTimeout" xml:space="preserve">
+ <value>The timeout value must be non-negative or Timeout.Infinite.</value>
+ </data>
+ <data name="AsyncActionMethodSelector_AmbiguousMethodMatch" xml:space="preserve">
+ <value>Lookup for method '{0}' on controller type '{1}' failed because of an ambiguity between the following methods:{2}</value>
+ </data>
+ <data name="AsyncActionMethodSelector_CouldNotFindMethod" xml:space="preserve">
+ <value>Could not locate a method named '{0}' on controller type {1}.</value>
+ </data>
+ <data name="ChildActionOnlyAttribute_MustBeInChildRequest" xml:space="preserve">
+ <value>The action '{0}' is accessible only by a child request.</value>
+ </data>
+ <data name="Templates_TypeMustImplementIEnumerable" xml:space="preserve">
+ <value>The Collection template was used with an object of type '{0}', which does not implement System.IEnumerable.</value>
+ </data>
+ <data name="TypeCache_DoNotModify" xml:space="preserve">
+ <value>This file is automatically generated. Please do not modify the contents of this file.</value>
+ </data>
+ <data name="ClientDataTypeModelValidatorProvider_FieldMustBeNumeric" xml:space="preserve">
+ <value>The field {0} must be a number.</value>
+ </data>
+ <data name="ExpressionHelper_InvalidIndexerExpression" xml:space="preserve">
+ <value>The expression compiler was unable to evaluate the indexer expression '{0}' because it references the model parameter '{1}' which is unavailable.</value>
+ </data>
+ <data name="Controller_Validate_ValidationFailed" xml:space="preserve">
+ <value>The model of type '{0}' is not valid.</value>
+ </data>
+ <data name="DefaultControllerFactory_ControllerNameAmbiguous_WithoutRouteUrl" xml:space="preserve">
+ <value>Multiple types were found that match the controller named '{0}'. This can happen if the route that services this request does not specify namespaces to search for a controller that matches the request. If this is the case, register this route by calling an overload of the 'MapRoute' method that takes a 'namespaces' parameter.
+
+The request for '{0}' has found the following matching controllers:{1}</value>
+ </data>
+ <data name="DefaultControllerFactory_ControllerNameAmbiguous_WithRouteUrl" xml:space="preserve">
+ <value>Multiple types were found that match the controller named '{0}'. This can happen if the route that services this request ('{1}') does not specify namespaces to search for a controller that matches the request. If this is the case, register this route by calling an overload of the 'MapRoute' method that takes a 'namespaces' parameter.
+
+The request for '{0}' has found the following matching controllers:{2}</value>
+ </data>
+ <data name="DataAnnotationsModelValidatorProvider_ValidatableConstructorRequirements" xml:space="preserve">
+ <value>The type {0} must have a public constructor which accepts two parameters of types {1} and {2}.</value>
+ </data>
+ <data name="ValidatableObjectAdapter_IncompatibleType" xml:space="preserve">
+ <value>The model object inside the metadata claimed to be compatible with {0}, but was actually {1}.</value>
+ </data>
+ <data name="CshtmlView_ViewCouldNotBeCreated" xml:space="preserve">
+ <value>The view found at '{0}' was not created.</value>
+ </data>
+ <data name="CshtmlView_WrongViewBase" xml:space="preserve">
+ <value>The view at '{0}' must derive from WebViewPage, or WebViewPage&lt;TModel&gt;.</value>
+ </data>
+ <data name="ReflectedActionDescriptor_CannotCallStaticMethod" xml:space="preserve">
+ <value>Cannot call action method '{0}' on controller '{1}' because the action method is a static method.</value>
+ </data>
+ <data name="MvcRazorCodeParser_ModelKeywordMustBeFollowedByTypeName" xml:space="preserve">
+ <value>The '{0}' keyword must be followed by a type name on the same line.</value>
+ </data>
+ <data name="MvcRazorCodeParser_CannotHaveModelAndInheritsKeyword" xml:space="preserve">
+ <value>The 'inherits' keyword is not allowed when a '{0}' keyword is used.</value>
+ </data>
+ <data name="MvcRazorCodeParser_OnlyOneModelStatementIsAllowed" xml:space="preserve">
+ <value>Only one '{0}' statement is allowed in a file.</value>
+ </data>
+ <data name="SingleServiceResolver_CannotRegisterTwoInstances" xml:space="preserve">
+ <value>An instance of {0} was found in the resolver as well as a custom registered provider in {1}. Please set only one or the other.</value>
+ </data>
+ <data name="DependencyResolver_DoesNotImplementICommonServiceLocator" xml:space="preserve">
+ <value>The type {0} does not appear to implement Microsoft.Practices.ServiceLocation.IServiceLocator.</value>
+ </data>
+ <data name="ViewStartPage_RequiresMvcRazorView" xml:space="preserve">
+ <value>A ViewStartPage can be used only with with a page that derives from WebViewPage or another ViewStartPage.</value>
+ </data>
+ <data name="ControllerBase_CannotExecuteWithNullHttpContext" xml:space="preserve">
+ <value>Cannot execute Controller with a null HttpContext.</value>
+ </data>
+ <data name="CompareAttribute_MustMatch" xml:space="preserve">
+ <value>'{0}' and '{1}' do not match.</value>
+ </data>
+ <data name="RemoteAttribute_RemoteValidationFailed" xml:space="preserve">
+ <value>'{0}' is invalid.</value>
+ </data>
+ <data name="RemoteAttribute_NoUrlFound" xml:space="preserve">
+ <value>No url for remote validation could be found.</value>
+ </data>
+ <data name="AuthorizeAttribute_CannotUseWithinChildActionCache" xml:space="preserve">
+ <value>AuthorizeAttribute cannot be used within a child action caching block.</value>
+ </data>
+ <data name="OutputCacheAttribute_InvalidDuration" xml:space="preserve">
+ <value>Duration must be a positive number.</value>
+ </data>
+ <data name="OutputCacheAttribute_InvalidVaryByParam" xml:space="preserve">
+ <value>VaryByParam must be '*', 'none', or a semicolon-delimited list of keys.</value>
+ </data>
+ <data name="OutputCacheAttribute_ChildAction_UnsupportedSetting" xml:space="preserve">
+ <value>OutputCacheAttribute for child actions only supports Duration, VaryByCustom, and VaryByParam values. Please do not set CacheProfile, Location, NoStore, SqlDependency, VaryByContentEncoding, or VaryByHeader values for child actions.</value>
+ </data>
+ <data name="OutputCacheAttribute_CannotNestChildCache" xml:space="preserve">
+ <value>OutputCacheAttribute is not allowed on child actions which are children of an already cached child action.</value>
+ </data>
+ <data name="CompareAttribute_UnknownProperty" xml:space="preserve">
+ <value>Could not find a property named {0}.</value>
+ </data>
+ <data name="HtmlHelper_SelectExpressionNotEnumerable" xml:space="preserve">
+ <value>The parameter '{0}' must evaluate to an IEnumerable when multiple selection is allowed.</value>
+ </data>
+ <data name="MvcRouteHandler_RouteValuesHasNoController" xml:space="preserve">
+ <value>The matched route does not include a 'controller' route value, which is required.</value>
+ </data>
+ <data name="ClientDataTypeModelValidatorProvider_FieldMustBeDate" xml:space="preserve">
+ <value>The field {0} must be a date.</value>
+ </data>
+ <data name="TaskAsyncActionDescriptor_CannotExecuteSynchronously" xml:space="preserve">
+ <value>The asynchronous action method '{0}' returns a Task, which cannot be executed synchronously.</value>
+ </data>
+ <data name="AsyncActionDescriptor_CannotExecuteSynchronously" xml:space="preserve">
+ <value>The asynchronous action method '{0}' cannot be executed synchronously.</value>
+ </data>
+ <data name="MvcForm_ConstructorObsolete" xml:space="preserve">
+ <value>This constructor is obsolete, because its functionality has been moved to MvcForm(ViewContext) now.</value>
+ </data>
+ <data name="JsonValueProviderFactory_RequestTooLarge" xml:space="preserve">
+ <value>The JSON request was too large to be deserialized.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/System.Web.Mvc/QueryStringValueProvider.cs b/src/System.Web.Mvc/QueryStringValueProvider.cs
new file mode 100644
index 00000000..3a2d34ba
--- /dev/null
+++ b/src/System.Web.Mvc/QueryStringValueProvider.cs
@@ -0,0 +1,21 @@
+using System.Globalization;
+using System.Web.Helpers;
+
+namespace System.Web.Mvc
+{
+ public sealed class QueryStringValueProvider : NameValueCollectionValueProvider
+ {
+ // QueryString should use the invariant culture since it's part of the URL, and the URL should be
+ // interpreted in a uniform fashion regardless of the origin of a particular request.
+ public QueryStringValueProvider(ControllerContext controllerContext)
+ : this(controllerContext, new UnvalidatedRequestValuesWrapper(controllerContext.HttpContext.Request.Unvalidated()))
+ {
+ }
+
+ // For unit testing
+ internal QueryStringValueProvider(ControllerContext controllerContext, IUnvalidatedRequestValues unvalidatedValues)
+ : base(controllerContext.HttpContext.Request.QueryString, unvalidatedValues.QueryString, CultureInfo.InvariantCulture)
+ {
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/QueryStringValueProviderFactory.cs b/src/System.Web.Mvc/QueryStringValueProviderFactory.cs
new file mode 100644
index 00000000..eaa0f696
--- /dev/null
+++ b/src/System.Web.Mvc/QueryStringValueProviderFactory.cs
@@ -0,0 +1,30 @@
+using System.Web.Helpers;
+
+namespace System.Web.Mvc
+{
+ public sealed class QueryStringValueProviderFactory : ValueProviderFactory
+ {
+ private readonly UnvalidatedRequestValuesAccessor _unvalidatedValuesAccessor;
+
+ public QueryStringValueProviderFactory()
+ : this(null)
+ {
+ }
+
+ // For unit testing
+ internal QueryStringValueProviderFactory(UnvalidatedRequestValuesAccessor unvalidatedValuesAccessor)
+ {
+ _unvalidatedValuesAccessor = unvalidatedValuesAccessor ?? (cc => new UnvalidatedRequestValuesWrapper(cc.HttpContext.Request.Unvalidated()));
+ }
+
+ public override IValueProvider GetValueProvider(ControllerContext controllerContext)
+ {
+ if (controllerContext == null)
+ {
+ throw new ArgumentNullException("controllerContext");
+ }
+
+ return new QueryStringValueProvider(controllerContext, _unvalidatedValuesAccessor(controllerContext));
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/RangeAttributeAdapter.cs b/src/System.Web.Mvc/RangeAttributeAdapter.cs
new file mode 100644
index 00000000..d0b41660
--- /dev/null
+++ b/src/System.Web.Mvc/RangeAttributeAdapter.cs
@@ -0,0 +1,19 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+
+namespace System.Web.Mvc
+{
+ public class RangeAttributeAdapter : DataAnnotationsModelValidator<RangeAttribute>
+ {
+ public RangeAttributeAdapter(ModelMetadata metadata, ControllerContext context, RangeAttribute attribute)
+ : base(metadata, context, attribute)
+ {
+ }
+
+ public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
+ {
+ string errorMessage = ErrorMessage; // Per Dev10 Bug #923283, need to make sure ErrorMessage is called before Minimum/Maximum
+ return new[] { new ModelClientValidationRangeRule(errorMessage, Attribute.Minimum, Attribute.Maximum) };
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Razor/MvcCSharpRazorCodeGenerator.cs b/src/System.Web.Mvc/Razor/MvcCSharpRazorCodeGenerator.cs
new file mode 100644
index 00000000..b09a95a9
--- /dev/null
+++ b/src/System.Web.Mvc/Razor/MvcCSharpRazorCodeGenerator.cs
@@ -0,0 +1,30 @@
+using System.CodeDom;
+using System.Web.Razor;
+using System.Web.Razor.Generator;
+
+namespace System.Web.Mvc.Razor
+{
+ internal class MvcCSharpRazorCodeGenerator : CSharpRazorCodeGenerator
+ {
+ private const string DefaultModelTypeName = "dynamic";
+
+ public MvcCSharpRazorCodeGenerator(string className, string rootNamespaceName, string sourceFileName, RazorEngineHost host)
+ : base(className, rootNamespaceName, sourceFileName, host)
+ {
+ var mvcHost = host as MvcWebPageRazorHost;
+ if (mvcHost != null && !mvcHost.IsSpecialPage)
+ {
+ // set the default model type to "dynamic" (Dev10 bug 935656)
+ // don't set it for "special" pages (such as "_viewStart.cshtml")
+ SetBaseType(DefaultModelTypeName);
+ }
+ }
+
+ private void SetBaseType(string modelTypeName)
+ {
+ var baseType = new CodeTypeReference(Context.Host.DefaultBaseClass + "<" + modelTypeName + ">");
+ Context.GeneratedClass.BaseTypes.Clear();
+ Context.GeneratedClass.BaseTypes.Add(baseType);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Razor/MvcCSharpRazorCodeParser.cs b/src/System.Web.Mvc/Razor/MvcCSharpRazorCodeParser.cs
new file mode 100644
index 00000000..e2d30351
--- /dev/null
+++ b/src/System.Web.Mvc/Razor/MvcCSharpRazorCodeParser.cs
@@ -0,0 +1,68 @@
+using System.Globalization;
+using System.Web.Mvc.Properties;
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Text;
+
+namespace System.Web.Mvc.Razor
+{
+ public class MvcCSharpRazorCodeParser : CSharpCodeParser
+ {
+ private const string ModelKeyword = "model";
+ private const string GenericTypeFormatString = "{0}<{1}>";
+ private SourceLocation? _endInheritsLocation;
+ private bool _modelStatementFound;
+
+ public MvcCSharpRazorCodeParser()
+ {
+ MapDirectives(ModelDirective, ModelKeyword);
+ }
+
+ protected override void InheritsDirective()
+ {
+ // Verify we're on the right keyword and accept
+ AssertDirective(SyntaxConstants.CSharp.InheritsKeyword);
+ AcceptAndMoveNext();
+ _endInheritsLocation = CurrentLocation;
+
+ InheritsDirectiveCore();
+ CheckForInheritsAndModelStatements();
+ }
+
+ private void CheckForInheritsAndModelStatements()
+ {
+ if (_modelStatementFound && _endInheritsLocation.HasValue)
+ {
+ Context.OnError(_endInheritsLocation.Value, String.Format(CultureInfo.CurrentCulture, MvcResources.MvcRazorCodeParser_CannotHaveModelAndInheritsKeyword, ModelKeyword));
+ }
+ }
+
+ protected virtual void ModelDirective()
+ {
+ // Verify we're on the right keyword and accept
+ AssertDirective(ModelKeyword);
+ AcceptAndMoveNext();
+
+ SourceLocation endModelLocation = CurrentLocation;
+
+ BaseTypeDirective(
+ String.Format(CultureInfo.CurrentCulture,
+ MvcResources.MvcRazorCodeParser_ModelKeywordMustBeFollowedByTypeName, ModelKeyword),
+ CreateModelCodeGenerator);
+
+ if (_modelStatementFound)
+ {
+ Context.OnError(endModelLocation, String.Format(CultureInfo.CurrentCulture, MvcResources.MvcRazorCodeParser_OnlyOneModelStatementIsAllowed, ModelKeyword));
+ }
+
+ _modelStatementFound = true;
+
+ CheckForInheritsAndModelStatements();
+ }
+
+ private SpanCodeGenerator CreateModelCodeGenerator(string model)
+ {
+ return new SetModelTypeCodeGenerator(model, GenericTypeFormatString);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Razor/MvcVBRazorCodeParser.cs b/src/System.Web.Mvc/Razor/MvcVBRazorCodeParser.cs
new file mode 100644
index 00000000..d2e33479
--- /dev/null
+++ b/src/System.Web.Mvc/Razor/MvcVBRazorCodeParser.cs
@@ -0,0 +1,93 @@
+using System.Globalization;
+using System.Linq;
+using System.Web.Mvc.Properties;
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Text;
+using System.Web.Razor.Tokenizer.Symbols;
+
+namespace System.Web.Mvc.Razor
+{
+ public class MvcVBRazorCodeParser : VBCodeParser
+ {
+ internal const string ModelTypeKeyword = "ModelType";
+ private const string GenericTypeFormatString = "{0}(Of {1})";
+ private SourceLocation? _endInheritsLocation;
+ private bool _modelStatementFound;
+
+ public MvcVBRazorCodeParser()
+ {
+ MapDirective(ModelTypeKeyword, ModelTypeDirective);
+ }
+
+ protected override bool InheritsStatement()
+ {
+ // Verify we're on the right keyword and accept
+ Assert(VBKeyword.Inherits);
+ VBSymbol inherits = CurrentSymbol;
+ NextToken();
+ _endInheritsLocation = CurrentLocation;
+ PutCurrentBack();
+ PutBack(inherits);
+ EnsureCurrent();
+
+ bool result = base.InheritsStatement();
+ CheckForInheritsAndModelStatements();
+ return result;
+ }
+
+ private void CheckForInheritsAndModelStatements()
+ {
+ if (_modelStatementFound && _endInheritsLocation.HasValue)
+ {
+ Context.OnError(_endInheritsLocation.Value, String.Format(CultureInfo.CurrentCulture, MvcResources.MvcRazorCodeParser_CannotHaveModelAndInheritsKeyword, ModelTypeKeyword));
+ }
+ }
+
+ protected virtual bool ModelTypeDirective()
+ {
+ AssertDirective(ModelTypeKeyword);
+
+ Span.CodeGenerator = SpanCodeGenerator.Null;
+ Context.CurrentBlock.Type = BlockType.Directive;
+
+ AcceptAndMoveNext();
+ SourceLocation endModelLocation = CurrentLocation;
+
+ if (At(VBSymbolType.WhiteSpace))
+ {
+ Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
+ }
+
+ AcceptWhile(VBSymbolType.WhiteSpace);
+ Output(SpanKind.MetaCode);
+
+ if (_modelStatementFound)
+ {
+ Context.OnError(endModelLocation, String.Format(CultureInfo.CurrentCulture, MvcResources.MvcRazorCodeParser_OnlyOneModelStatementIsAllowed, ModelTypeKeyword));
+ }
+ _modelStatementFound = true;
+
+ if (EndOfFile || At(VBSymbolType.WhiteSpace) || At(VBSymbolType.NewLine))
+ {
+ Context.OnError(endModelLocation, MvcResources.MvcRazorCodeParser_ModelKeywordMustBeFollowedByTypeName, ModelTypeKeyword);
+ }
+
+ // Just accept to a newline
+ AcceptUntil(VBSymbolType.NewLine);
+ if (!Context.DesignTimeMode)
+ {
+ // We want the newline to be treated as code, but it causes issues at design-time.
+ Optional(VBSymbolType.NewLine);
+ }
+
+ string baseType = String.Concat(Span.Symbols.Select(s => s.Content)).Trim();
+ Span.CodeGenerator = new SetModelTypeCodeGenerator(baseType, GenericTypeFormatString);
+
+ CheckForInheritsAndModelStatements();
+ Output(SpanKind.Code);
+ return false;
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Razor/MvcWebPageRazorHost.cs b/src/System.Web.Mvc/Razor/MvcWebPageRazorHost.cs
new file mode 100644
index 00000000..69f058f7
--- /dev/null
+++ b/src/System.Web.Mvc/Razor/MvcWebPageRazorHost.cs
@@ -0,0 +1,64 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser;
+using System.Web.WebPages.Razor;
+
+namespace System.Web.Mvc.Razor
+{
+ [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "WebPage", Justification = "The class name is derived from the name of the base type")]
+ public class MvcWebPageRazorHost : WebPageRazorHost
+ {
+ [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "The NamespaceImports property should not be virtual. This is a temporary fix.")]
+ public MvcWebPageRazorHost(string virtualPath, string physicalPath)
+ : base(virtualPath, physicalPath)
+ {
+ RegisterSpecialFile(RazorViewEngine.ViewStartFileName, typeof(ViewStartPage));
+
+ DefaultPageBaseClass = typeof(WebViewPage).FullName;
+
+ // REVIEW get rid of the namespace import to not force additional references in default MVC projects
+ GetRidOfNamespace("System.Web.WebPages.Html");
+ }
+
+ public override RazorCodeGenerator DecorateCodeGenerator(RazorCodeGenerator incomingCodeGenerator)
+ {
+ if (incomingCodeGenerator is CSharpRazorCodeGenerator)
+ {
+ return new MvcCSharpRazorCodeGenerator(incomingCodeGenerator.ClassName,
+ incomingCodeGenerator.RootNamespaceName,
+ incomingCodeGenerator.SourceFileName,
+ incomingCodeGenerator.Host);
+ }
+ else
+ {
+ return base.DecorateCodeGenerator(incomingCodeGenerator);
+ }
+ }
+
+ public override ParserBase DecorateCodeParser(ParserBase incomingCodeParser)
+ {
+ if (incomingCodeParser is CSharpCodeParser)
+ {
+ return new MvcCSharpRazorCodeParser();
+ }
+ else if (incomingCodeParser is VBCodeParser)
+ {
+ return new MvcVBRazorCodeParser();
+ }
+ else
+ {
+ return base.DecorateCodeParser(incomingCodeParser);
+ }
+ }
+
+ private void GetRidOfNamespace(string ns)
+ {
+ Debug.Assert(NamespaceImports.Contains(ns), ns + " is not a default namespace anymore");
+ if (NamespaceImports.Contains(ns))
+ {
+ NamespaceImports.Remove(ns);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Razor/SetModelTypeCodeGenerator.cs b/src/System.Web.Mvc/Razor/SetModelTypeCodeGenerator.cs
new file mode 100644
index 00000000..e8640b20
--- /dev/null
+++ b/src/System.Web.Mvc/Razor/SetModelTypeCodeGenerator.cs
@@ -0,0 +1,47 @@
+using System.Globalization;
+using System.Web.Mvc.ExpressionUtil;
+using System.Web.Razor.Generator;
+
+namespace System.Web.Mvc.Razor
+{
+ internal class SetModelTypeCodeGenerator : SetBaseTypeCodeGenerator
+ {
+ private string _genericTypeFormat;
+
+ public SetModelTypeCodeGenerator(string modelType, string genericTypeFormat)
+ : base(modelType)
+ {
+ _genericTypeFormat = genericTypeFormat;
+ }
+
+ protected override string ResolveType(CodeGeneratorContext context, string baseType)
+ {
+ return String.Format(
+ CultureInfo.InvariantCulture,
+ _genericTypeFormat,
+ context.Host.DefaultBaseClass,
+ baseType);
+ }
+
+ public override bool Equals(object obj)
+ {
+ SetModelTypeCodeGenerator other = obj as SetModelTypeCodeGenerator;
+ return other != null &&
+ base.Equals(obj) &&
+ String.Equals(_genericTypeFormat, other._genericTypeFormat, StringComparison.Ordinal);
+ }
+
+ public override int GetHashCode()
+ {
+ var combiner = new HashCodeCombiner();
+ combiner.AddInt32(base.GetHashCode());
+ combiner.AddObject(_genericTypeFormat);
+ return combiner.CombinedHash;
+ }
+
+ public override string ToString()
+ {
+ return "Model:" + BaseType;
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/Razor/StartPageLookupDelegate.cs b/src/System.Web.Mvc/Razor/StartPageLookupDelegate.cs
new file mode 100644
index 00000000..07197610
--- /dev/null
+++ b/src/System.Web.Mvc/Razor/StartPageLookupDelegate.cs
@@ -0,0 +1,7 @@
+using System.Collections.Generic;
+using System.Web.WebPages;
+
+namespace System.Web.Mvc.Razor
+{
+ internal delegate WebPageRenderingBase StartPageLookupDelegate(WebPageRenderingBase page, string fileName, IEnumerable<string> supportedExtensions);
+}
diff --git a/src/System.Web.Mvc/RazorView.cs b/src/System.Web.Mvc/RazorView.cs
new file mode 100644
index 00000000..70b96cb3
--- /dev/null
+++ b/src/System.Web.Mvc/RazorView.cs
@@ -0,0 +1,82 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Web.Mvc.Properties;
+using System.Web.Mvc.Razor;
+using System.Web.WebPages;
+
+namespace System.Web.Mvc
+{
+ public class RazorView : BuildManagerCompiledView
+ {
+ public RazorView(ControllerContext controllerContext, string viewPath, string layoutPath, bool runViewStartPages, IEnumerable<string> viewStartFileExtensions)
+ : this(controllerContext, viewPath, layoutPath, runViewStartPages, viewStartFileExtensions, null)
+ {
+ }
+
+ public RazorView(ControllerContext controllerContext, string viewPath, string layoutPath, bool runViewStartPages, IEnumerable<string> viewStartFileExtensions, IViewPageActivator viewPageActivator)
+ : base(controllerContext, viewPath, viewPageActivator)
+ {
+ LayoutPath = layoutPath ?? String.Empty;
+ RunViewStartPages = runViewStartPages;
+ StartPageLookup = StartPage.GetStartPage;
+ ViewStartFileExtensions = viewStartFileExtensions ?? Enumerable.Empty<string>();
+ }
+
+ public string LayoutPath { get; private set; }
+
+ public bool RunViewStartPages { get; private set; }
+
+ internal StartPageLookupDelegate StartPageLookup { get; set; }
+
+ internal IVirtualPathFactory VirtualPathFactory { get; set; }
+
+ internal DisplayModeProvider DisplayModeProvider { get; set; }
+
+ public IEnumerable<string> ViewStartFileExtensions { get; private set; }
+
+ protected override void RenderView(ViewContext viewContext, TextWriter writer, object instance)
+ {
+ if (writer == null)
+ {
+ throw new ArgumentNullException("writer");
+ }
+
+ WebViewPage webViewPage = instance as WebViewPage;
+ if (webViewPage == null)
+ {
+ throw new InvalidOperationException(
+ String.Format(
+ CultureInfo.CurrentCulture,
+ MvcResources.CshtmlView_WrongViewBase,
+ ViewPath));
+ }
+
+ // An overriden master layout might have been specified when the ViewActionResult got returned.
+ // We need to hold on to it so that we can set it on the inner page once it has executed.
+ webViewPage.OverridenLayoutPath = LayoutPath;
+ webViewPage.VirtualPath = ViewPath;
+ webViewPage.ViewContext = viewContext;
+ webViewPage.ViewData = viewContext.ViewData;
+
+ webViewPage.InitHelpers();
+
+ if (VirtualPathFactory != null)
+ {
+ webViewPage.VirtualPathFactory = VirtualPathFactory;
+ }
+ if (DisplayModeProvider != null)
+ {
+ webViewPage.DisplayModeProvider = DisplayModeProvider;
+ }
+
+ WebPageRenderingBase startPage = null;
+ if (RunViewStartPages)
+ {
+ startPage = StartPageLookup(webViewPage, RazorViewEngine.ViewStartFileName, ViewStartFileExtensions);
+ }
+ webViewPage.ExecutePageHierarchy(new WebPageContext(context: viewContext.HttpContext, page: null, model: null), writer, startPage);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/RazorViewEngine.cs b/src/System.Web.Mvc/RazorViewEngine.cs
new file mode 100644
index 00000000..9e903be6
--- /dev/null
+++ b/src/System.Web.Mvc/RazorViewEngine.cs
@@ -0,0 +1,85 @@
+namespace System.Web.Mvc
+{
+ public class RazorViewEngine : BuildManagerViewEngine
+ {
+ internal static readonly string ViewStartFileName = "_ViewStart";
+
+ public RazorViewEngine()
+ : this(null)
+ {
+ }
+
+ public RazorViewEngine(IViewPageActivator viewPageActivator)
+ : base(viewPageActivator)
+ {
+ AreaViewLocationFormats = new[]
+ {
+ "~/Areas/{2}/Views/{1}/{0}.cshtml",
+ "~/Areas/{2}/Views/{1}/{0}.vbhtml",
+ "~/Areas/{2}/Views/Shared/{0}.cshtml",
+ "~/Areas/{2}/Views/Shared/{0}.vbhtml"
+ };
+ AreaMasterLocationFormats = new[]
+ {
+ "~/Areas/{2}/Views/{1}/{0}.cshtml",
+ "~/Areas/{2}/Views/{1}/{0}.vbhtml",
+ "~/Areas/{2}/Views/Shared/{0}.cshtml",
+ "~/Areas/{2}/Views/Shared/{0}.vbhtml"
+ };
+ AreaPartialViewLocationFormats = new[]
+ {
+ "~/Areas/{2}/Views/{1}/{0}.cshtml",
+ "~/Areas/{2}/Views/{1}/{0}.vbhtml",
+ "~/Areas/{2}/Views/Shared/{0}.cshtml",
+ "~/Areas/{2}/Views/Shared/{0}.vbhtml"
+ };
+
+ ViewLocationFormats = new[]
+ {
+ "~/Views/{1}/{0}.cshtml",
+ "~/Views/{1}/{0}.vbhtml",
+ "~/Views/Shared/{0}.cshtml",
+ "~/Views/Shared/{0}.vbhtml"
+ };
+ MasterLocationFormats = new[]
+ {
+ "~/Views/{1}/{0}.cshtml",
+ "~/Views/{1}/{0}.vbhtml",
+ "~/Views/Shared/{0}.cshtml",
+ "~/Views/Shared/{0}.vbhtml"
+ };
+ PartialViewLocationFormats = new[]
+ {
+ "~/Views/{1}/{0}.cshtml",
+ "~/Views/{1}/{0}.vbhtml",
+ "~/Views/Shared/{0}.cshtml",
+ "~/Views/Shared/{0}.vbhtml"
+ };
+
+ FileExtensions = new[]
+ {
+ "cshtml",
+ "vbhtml",
+ };
+ }
+
+ protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
+ {
+ return new RazorView(controllerContext, partialPath,
+ layoutPath: null, runViewStartPages: false, viewStartFileExtensions: FileExtensions, viewPageActivator: ViewPageActivator)
+ {
+ DisplayModeProvider = DisplayModeProvider
+ };
+ }
+
+ protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
+ {
+ var view = new RazorView(controllerContext, viewPath,
+ layoutPath: masterPath, runViewStartPages: true, viewStartFileExtensions: FileExtensions, viewPageActivator: ViewPageActivator)
+ {
+ DisplayModeProvider = DisplayModeProvider
+ };
+ return view;
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ReaderWriterCache`2.cs b/src/System.Web.Mvc/ReaderWriterCache`2.cs
new file mode 100644
index 00000000..a02b4740
--- /dev/null
+++ b/src/System.Web.Mvc/ReaderWriterCache`2.cs
@@ -0,0 +1,66 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Threading;
+
+namespace System.Web.Mvc
+{
+ [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "Instances of this type are meant to be singletons.")]
+ internal abstract class ReaderWriterCache<TKey, TValue>
+ {
+ private readonly Dictionary<TKey, TValue> _cache;
+ private readonly ReaderWriterLockSlim _readerWriterLock = new ReaderWriterLockSlim();
+
+ protected ReaderWriterCache()
+ : this(null)
+ {
+ }
+
+ protected ReaderWriterCache(IEqualityComparer<TKey> comparer)
+ {
+ _cache = new Dictionary<TKey, TValue>(comparer);
+ }
+
+ protected Dictionary<TKey, TValue> Cache
+ {
+ get { return _cache; }
+ }
+
+ protected TValue FetchOrCreateItem(TKey key, Func<TValue> creator)
+ {
+ // first, see if the item already exists in the cache
+ _readerWriterLock.EnterReadLock();
+ try
+ {
+ TValue existingEntry;
+ if (_cache.TryGetValue(key, out existingEntry))
+ {
+ return existingEntry;
+ }
+ }
+ finally
+ {
+ _readerWriterLock.ExitReadLock();
+ }
+
+ // insert the new item into the cache
+ TValue newEntry = creator();
+ _readerWriterLock.EnterWriteLock();
+ try
+ {
+ TValue existingEntry;
+ if (_cache.TryGetValue(key, out existingEntry))
+ {
+ // another thread already inserted an item, so use that one
+ return existingEntry;
+ }
+
+ _cache[key] = newEntry;
+ return newEntry;
+ }
+ finally
+ {
+ _readerWriterLock.ExitWriteLock();
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/RedirectResult.cs b/src/System.Web.Mvc/RedirectResult.cs
new file mode 100644
index 00000000..0dfad042
--- /dev/null
+++ b/src/System.Web.Mvc/RedirectResult.cs
@@ -0,0 +1,56 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc
+{
+ // represents a result that performs a redirection given some URI
+ public class RedirectResult : ActionResult
+ {
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "0#", Justification = "Response.Redirect() takes its URI as a string parameter.")]
+ public RedirectResult(string url)
+ : this(url, permanent: false)
+ {
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "0#", Justification = "Response.Redirect() takes its URI as a string parameter.")]
+ public RedirectResult(string url, bool permanent)
+ {
+ if (String.IsNullOrEmpty(url))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "url");
+ }
+
+ Permanent = permanent;
+ Url = url;
+ }
+
+ public bool Permanent { get; private set; }
+
+ [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "Response.Redirect() takes its URI as a string parameter.")]
+ public string Url { get; private set; }
+
+ public override void ExecuteResult(ControllerContext context)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException("context");
+ }
+ if (context.IsChildAction)
+ {
+ throw new InvalidOperationException(MvcResources.RedirectAction_CannotRedirectInChildAction);
+ }
+
+ string destinationUrl = UrlHelper.GenerateContentUrl(Url, context.HttpContext);
+ context.Controller.TempData.Keep();
+
+ if (Permanent)
+ {
+ context.HttpContext.Response.RedirectPermanent(destinationUrl, endResponse: false);
+ }
+ else
+ {
+ context.HttpContext.Response.Redirect(destinationUrl, endResponse: false);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/RedirectToRouteResult.cs b/src/System.Web.Mvc/RedirectToRouteResult.cs
new file mode 100644
index 00000000..c35846d0
--- /dev/null
+++ b/src/System.Web.Mvc/RedirectToRouteResult.cs
@@ -0,0 +1,77 @@
+using System.Web.Mvc.Properties;
+using System.Web.Routing;
+
+namespace System.Web.Mvc
+{
+ // represents a result that performs a redirection given some values dictionary
+ public class RedirectToRouteResult : ActionResult
+ {
+ private RouteCollection _routes;
+
+ public RedirectToRouteResult(RouteValueDictionary routeValues)
+ :
+ this(null, routeValues)
+ {
+ }
+
+ public RedirectToRouteResult(string routeName, RouteValueDictionary routeValues)
+ : this(routeName, routeValues, permanent: false)
+ {
+ }
+
+ public RedirectToRouteResult(string routeName, RouteValueDictionary routeValues, bool permanent)
+ {
+ Permanent = permanent;
+ RouteName = routeName ?? String.Empty;
+ RouteValues = routeValues ?? new RouteValueDictionary();
+ }
+
+ public bool Permanent { get; private set; }
+
+ public string RouteName { get; private set; }
+
+ public RouteValueDictionary RouteValues { get; private set; }
+
+ internal RouteCollection Routes
+ {
+ get
+ {
+ if (_routes == null)
+ {
+ _routes = RouteTable.Routes;
+ }
+ return _routes;
+ }
+ set { _routes = value; }
+ }
+
+ public override void ExecuteResult(ControllerContext context)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException("context");
+ }
+ if (context.IsChildAction)
+ {
+ throw new InvalidOperationException(MvcResources.RedirectAction_CannotRedirectInChildAction);
+ }
+
+ string destinationUrl = UrlHelper.GenerateUrl(RouteName, null /* actionName */, null /* controllerName */, RouteValues, Routes, context.RequestContext, false /* includeImplicitMvcValues */);
+ if (String.IsNullOrEmpty(destinationUrl))
+ {
+ throw new InvalidOperationException(MvcResources.Common_NoRouteMatched);
+ }
+
+ context.Controller.TempData.Keep();
+
+ if (Permanent)
+ {
+ context.HttpContext.Response.RedirectPermanent(destinationUrl, endResponse: false);
+ }
+ else
+ {
+ context.HttpContext.Response.Redirect(destinationUrl, endResponse: false);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ReflectedActionDescriptor.cs b/src/System.Web.Mvc/ReflectedActionDescriptor.cs
new file mode 100644
index 00000000..707521ec
--- /dev/null
+++ b/src/System.Web.Mvc/ReflectedActionDescriptor.cs
@@ -0,0 +1,135 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc
+{
+ public class ReflectedActionDescriptor : ActionDescriptor
+ {
+ private readonly string _actionName;
+ private readonly ControllerDescriptor _controllerDescriptor;
+ private readonly Lazy<string> _uniqueId;
+ private ParameterDescriptor[] _parametersCache;
+
+ public ReflectedActionDescriptor(MethodInfo methodInfo, string actionName, ControllerDescriptor controllerDescriptor)
+ : this(methodInfo, actionName, controllerDescriptor, true /* validateMethod */)
+ {
+ }
+
+ internal ReflectedActionDescriptor(MethodInfo methodInfo, string actionName, ControllerDescriptor controllerDescriptor, bool validateMethod)
+ {
+ if (methodInfo == null)
+ {
+ throw new ArgumentNullException("methodInfo");
+ }
+ if (String.IsNullOrEmpty(actionName))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "actionName");
+ }
+ if (controllerDescriptor == null)
+ {
+ throw new ArgumentNullException("controllerDescriptor");
+ }
+
+ if (validateMethod)
+ {
+ string failedMessage = VerifyActionMethodIsCallable(methodInfo);
+ if (failedMessage != null)
+ {
+ throw new ArgumentException(failedMessage, "methodInfo");
+ }
+ }
+
+ MethodInfo = methodInfo;
+ _actionName = actionName;
+ _controllerDescriptor = controllerDescriptor;
+ _uniqueId = new Lazy<string>(CreateUniqueId);
+ }
+
+ public override string ActionName
+ {
+ get { return _actionName; }
+ }
+
+ public override ControllerDescriptor ControllerDescriptor
+ {
+ get { return _controllerDescriptor; }
+ }
+
+ public MethodInfo MethodInfo { get; private set; }
+
+ public override string UniqueId
+ {
+ get { return _uniqueId.Value; }
+ }
+
+ private string CreateUniqueId()
+ {
+ return base.UniqueId + DescriptorUtil.CreateUniqueId(MethodInfo);
+ }
+
+ public override object Execute(ControllerContext controllerContext, IDictionary<string, object> parameters)
+ {
+ if (controllerContext == null)
+ {
+ throw new ArgumentNullException("controllerContext");
+ }
+ if (parameters == null)
+ {
+ throw new ArgumentNullException("parameters");
+ }
+
+ ParameterInfo[] parameterInfos = MethodInfo.GetParameters();
+ var rawParameterValues = from parameterInfo in parameterInfos
+ select ExtractParameterFromDictionary(parameterInfo, parameters, MethodInfo);
+ object[] parametersArray = rawParameterValues.ToArray();
+
+ ActionMethodDispatcher dispatcher = DispatcherCache.GetDispatcher(MethodInfo);
+ object actionReturnValue = dispatcher.Execute(controllerContext.Controller, parametersArray);
+ return actionReturnValue;
+ }
+
+ public override object[] GetCustomAttributes(bool inherit)
+ {
+ return ActionDescriptorHelper.GetCustomAttributes(MethodInfo, inherit);
+ }
+
+ public override object[] GetCustomAttributes(Type attributeType, bool inherit)
+ {
+ return ActionDescriptorHelper.GetCustomAttributes(MethodInfo, attributeType, inherit);
+ }
+
+ public override IEnumerable<FilterAttribute> GetFilterAttributes(bool useCache)
+ {
+ if (useCache && GetType() == typeof(ReflectedActionDescriptor))
+ {
+ // Do not look at cache in types derived from this type because they might incorrectly implement GetCustomAttributes
+ return ReflectedAttributeCache.GetMethodFilterAttributes(MethodInfo);
+ }
+ return base.GetFilterAttributes(useCache);
+ }
+
+ public override ParameterDescriptor[] GetParameters()
+ {
+ return ActionDescriptorHelper.GetParameters(this, MethodInfo, ref _parametersCache);
+ }
+
+ public override ICollection<ActionSelector> GetSelectors()
+ {
+ return ActionDescriptorHelper.GetSelectors(MethodInfo);
+ }
+
+ public override bool IsDefined(Type attributeType, bool inherit)
+ {
+ return ActionDescriptorHelper.IsDefined(MethodInfo, attributeType, inherit);
+ }
+
+ internal static ReflectedActionDescriptor TryCreateDescriptor(MethodInfo methodInfo, string name, ControllerDescriptor controllerDescriptor)
+ {
+ ReflectedActionDescriptor descriptor = new ReflectedActionDescriptor(methodInfo, name, controllerDescriptor, false /* validateMethod */);
+ string failedMessage = VerifyActionMethodIsCallable(methodInfo);
+ return (failedMessage == null) ? descriptor : null;
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ReflectedAttributeCache.cs b/src/System.Web.Mvc/ReflectedAttributeCache.cs
new file mode 100644
index 00000000..98b3b159
--- /dev/null
+++ b/src/System.Web.Mvc/ReflectedAttributeCache.cs
@@ -0,0 +1,46 @@
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.Reflection;
+
+namespace System.Web.Mvc
+{
+ internal static class ReflectedAttributeCache
+ {
+ private static readonly ConcurrentDictionary<MethodInfo, ReadOnlyCollection<ActionMethodSelectorAttribute>> _actionMethodSelectorAttributeCache = new ConcurrentDictionary<MethodInfo, ReadOnlyCollection<ActionMethodSelectorAttribute>>();
+ private static readonly ConcurrentDictionary<MethodInfo, ReadOnlyCollection<ActionNameSelectorAttribute>> _actionNameSelectorAttributeCache = new ConcurrentDictionary<MethodInfo, ReadOnlyCollection<ActionNameSelectorAttribute>>();
+ private static readonly ConcurrentDictionary<MethodInfo, ReadOnlyCollection<FilterAttribute>> _methodFilterAttributeCache = new ConcurrentDictionary<MethodInfo, ReadOnlyCollection<FilterAttribute>>();
+
+ private static readonly ConcurrentDictionary<Type, ReadOnlyCollection<FilterAttribute>> _typeFilterAttributeCache = new ConcurrentDictionary<Type, ReadOnlyCollection<FilterAttribute>>();
+
+ public static ICollection<FilterAttribute> GetTypeFilterAttributes(Type type)
+ {
+ return GetAttributes(_typeFilterAttributeCache, type);
+ }
+
+ public static ICollection<FilterAttribute> GetMethodFilterAttributes(MethodInfo methodInfo)
+ {
+ return GetAttributes(_methodFilterAttributeCache, methodInfo);
+ }
+
+ public static ICollection<ActionMethodSelectorAttribute> GetActionMethodSelectorAttributes(MethodInfo methodInfo)
+ {
+ return GetAttributes(_actionMethodSelectorAttributeCache, methodInfo);
+ }
+
+ public static ICollection<ActionNameSelectorAttribute> GetActionNameSelectorAttributes(MethodInfo methodInfo)
+ {
+ return GetAttributes(_actionNameSelectorAttributeCache, methodInfo);
+ }
+
+ private static ReadOnlyCollection<TAttribute> GetAttributes<TMemberInfo, TAttribute>(ConcurrentDictionary<TMemberInfo, ReadOnlyCollection<TAttribute>> lookup, TMemberInfo memberInfo)
+ where TAttribute : Attribute
+ where TMemberInfo : MemberInfo
+ {
+ Debug.Assert(memberInfo != null);
+ Debug.Assert(lookup != null);
+ return lookup.GetOrAdd(memberInfo, mi => new ReadOnlyCollection<TAttribute>((TAttribute[])memberInfo.GetCustomAttributes(typeof(TAttribute), inherit: true)));
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ReflectedControllerDescriptor.cs b/src/System.Web.Mvc/ReflectedControllerDescriptor.cs
new file mode 100644
index 00000000..bdda9050
--- /dev/null
+++ b/src/System.Web.Mvc/ReflectedControllerDescriptor.cs
@@ -0,0 +1,99 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc
+{
+ public class ReflectedControllerDescriptor : ControllerDescriptor
+ {
+ private readonly Type _controllerType;
+ private readonly ActionMethodSelector _selector;
+ private ActionDescriptor[] _canonicalActionsCache;
+
+ public ReflectedControllerDescriptor(Type controllerType)
+ {
+ if (controllerType == null)
+ {
+ throw new ArgumentNullException("controllerType");
+ }
+
+ _controllerType = controllerType;
+ _selector = new ActionMethodSelector(_controllerType);
+ }
+
+ public sealed override Type ControllerType
+ {
+ get { return _controllerType; }
+ }
+
+ public override ActionDescriptor FindAction(ControllerContext controllerContext, string actionName)
+ {
+ if (controllerContext == null)
+ {
+ throw new ArgumentNullException("controllerContext");
+ }
+ if (String.IsNullOrEmpty(actionName))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "actionName");
+ }
+
+ MethodInfo matched = _selector.FindActionMethod(controllerContext, actionName);
+ if (matched == null)
+ {
+ return null;
+ }
+
+ return new ReflectedActionDescriptor(matched, actionName, this);
+ }
+
+ private MethodInfo[] GetAllActionMethodsFromSelector()
+ {
+ List<MethodInfo> allValidMethods = new List<MethodInfo>();
+ allValidMethods.AddRange(_selector.AliasedMethods);
+ allValidMethods.AddRange(_selector.NonAliasedMethods.SelectMany(g => g));
+ return allValidMethods.ToArray();
+ }
+
+ public override ActionDescriptor[] GetCanonicalActions()
+ {
+ ActionDescriptor[] actions = LazilyFetchCanonicalActionsCollection();
+
+ // need to clone array so that user modifications aren't accidentally stored
+ return (ActionDescriptor[])actions.Clone();
+ }
+
+ public override object[] GetCustomAttributes(bool inherit)
+ {
+ return ControllerType.GetCustomAttributes(inherit);
+ }
+
+ public override object[] GetCustomAttributes(Type attributeType, bool inherit)
+ {
+ return ControllerType.GetCustomAttributes(attributeType, inherit);
+ }
+
+ public override IEnumerable<FilterAttribute> GetFilterAttributes(bool useCache)
+ {
+ if (useCache && GetType() == typeof(ReflectedControllerDescriptor))
+ {
+ // Do not look at cache in types derived from this type because they might incorrectly implement GetCustomAttributes
+ return ReflectedAttributeCache.GetTypeFilterAttributes(ControllerType);
+ }
+ return base.GetFilterAttributes(useCache);
+ }
+
+ public override bool IsDefined(Type attributeType, bool inherit)
+ {
+ return ControllerType.IsDefined(attributeType, inherit);
+ }
+
+ private ActionDescriptor[] LazilyFetchCanonicalActionsCollection()
+ {
+ return DescriptorUtil.LazilyFetchOrCreateDescriptors<MethodInfo, ActionDescriptor>(
+ ref _canonicalActionsCache /* cacheLocation */,
+ GetAllActionMethodsFromSelector /* initializer */,
+ methodInfo => ReflectedActionDescriptor.TryCreateDescriptor(methodInfo, methodInfo.Name, this) /* converter */);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ReflectedParameterBindingInfo.cs b/src/System.Web.Mvc/ReflectedParameterBindingInfo.cs
new file mode 100644
index 00000000..27254f2d
--- /dev/null
+++ b/src/System.Web.Mvc/ReflectedParameterBindingInfo.cs
@@ -0,0 +1,62 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Globalization;
+using System.Reflection;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc
+{
+ internal class ReflectedParameterBindingInfo : ParameterBindingInfo
+ {
+ private readonly ParameterInfo _parameterInfo;
+ private ICollection<string> _exclude = new string[0];
+ private ICollection<string> _include = new string[0];
+ private string _prefix;
+
+ public ReflectedParameterBindingInfo(ParameterInfo parameterInfo)
+ {
+ _parameterInfo = parameterInfo;
+ ReadSettingsFromBindAttribute();
+ }
+
+ public override IModelBinder Binder
+ {
+ get
+ {
+ IModelBinder binder = ModelBinders.GetBinderFromAttributes(_parameterInfo,
+ () => String.Format(CultureInfo.CurrentCulture, MvcResources.ReflectedParameterBindingInfo_MultipleConverterAttributes,
+ _parameterInfo.Name, _parameterInfo.Member));
+
+ return binder;
+ }
+ }
+
+ public override ICollection<string> Exclude
+ {
+ get { return _exclude; }
+ }
+
+ public override ICollection<string> Include
+ {
+ get { return _include; }
+ }
+
+ public override string Prefix
+ {
+ get { return _prefix; }
+ }
+
+ private void ReadSettingsFromBindAttribute()
+ {
+ BindAttribute attr = (BindAttribute)Attribute.GetCustomAttribute(_parameterInfo, typeof(BindAttribute));
+ if (attr == null)
+ {
+ return;
+ }
+
+ _exclude = new ReadOnlyCollection<string>(AuthorizeAttribute.SplitString(attr.Exclude));
+ _include = new ReadOnlyCollection<string>(AuthorizeAttribute.SplitString(attr.Include));
+ _prefix = attr.Prefix;
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ReflectedParameterDescriptor.cs b/src/System.Web.Mvc/ReflectedParameterDescriptor.cs
new file mode 100644
index 00000000..28f653df
--- /dev/null
+++ b/src/System.Web.Mvc/ReflectedParameterDescriptor.cs
@@ -0,0 +1,79 @@
+using System.Reflection;
+
+namespace System.Web.Mvc
+{
+ public class ReflectedParameterDescriptor : ParameterDescriptor
+ {
+ private readonly ActionDescriptor _actionDescriptor;
+ private readonly ReflectedParameterBindingInfo _bindingInfo;
+
+ public ReflectedParameterDescriptor(ParameterInfo parameterInfo, ActionDescriptor actionDescriptor)
+ {
+ if (parameterInfo == null)
+ {
+ throw new ArgumentNullException("parameterInfo");
+ }
+ if (actionDescriptor == null)
+ {
+ throw new ArgumentNullException("actionDescriptor");
+ }
+
+ ParameterInfo = parameterInfo;
+ _actionDescriptor = actionDescriptor;
+ _bindingInfo = new ReflectedParameterBindingInfo(parameterInfo);
+ }
+
+ public override ActionDescriptor ActionDescriptor
+ {
+ get { return _actionDescriptor; }
+ }
+
+ public override ParameterBindingInfo BindingInfo
+ {
+ get { return _bindingInfo; }
+ }
+
+ public override object DefaultValue
+ {
+ get
+ {
+ object value;
+ if (ParameterInfoUtil.TryGetDefaultValue(ParameterInfo, out value))
+ {
+ return value;
+ }
+ else
+ {
+ return base.DefaultValue;
+ }
+ }
+ }
+
+ public ParameterInfo ParameterInfo { get; private set; }
+
+ public override string ParameterName
+ {
+ get { return ParameterInfo.Name; }
+ }
+
+ public override Type ParameterType
+ {
+ get { return ParameterInfo.ParameterType; }
+ }
+
+ public override object[] GetCustomAttributes(bool inherit)
+ {
+ return ParameterInfo.GetCustomAttributes(inherit);
+ }
+
+ public override object[] GetCustomAttributes(Type attributeType, bool inherit)
+ {
+ return ParameterInfo.GetCustomAttributes(attributeType, inherit);
+ }
+
+ public override bool IsDefined(Type attributeType, bool inherit)
+ {
+ return ParameterInfo.IsDefined(attributeType, inherit);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/RegularExpressionAttributeAdapter.cs b/src/System.Web.Mvc/RegularExpressionAttributeAdapter.cs
new file mode 100644
index 00000000..18617497
--- /dev/null
+++ b/src/System.Web.Mvc/RegularExpressionAttributeAdapter.cs
@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+
+namespace System.Web.Mvc
+{
+ public class RegularExpressionAttributeAdapter : DataAnnotationsModelValidator<RegularExpressionAttribute>
+ {
+ public RegularExpressionAttributeAdapter(ModelMetadata metadata, ControllerContext context, RegularExpressionAttribute attribute)
+ : base(metadata, context, attribute)
+ {
+ }
+
+ public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
+ {
+ return new[] { new ModelClientValidationRegexRule(ErrorMessage, Attribute.Pattern) };
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/RemoteAttribute.cs b/src/System.Web.Mvc/RemoteAttribute.cs
new file mode 100644
index 00000000..68230a46
--- /dev/null
+++ b/src/System.Web.Mvc/RemoteAttribute.cs
@@ -0,0 +1,139 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Web.Mvc.Properties;
+using System.Web.Routing;
+
+namespace System.Web.Mvc
+{
+ [AttributeUsage(AttributeTargets.Property)]
+ [SuppressMessage("Microsoft.Design", "CA1019:DefineAccessorsForAttributeArguments", Justification = "The constructor parameters are used to feed RouteData, which is public.")]
+ [SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", Justification = "This attribute is designed to be a base class for other attributes.")]
+ public class RemoteAttribute : ValidationAttribute, IClientValidatable
+ {
+ private string _additionalFields;
+ private string[] _additonalFieldsSplit = new string[0];
+
+ protected RemoteAttribute()
+ : base(MvcResources.RemoteAttribute_RemoteValidationFailed)
+ {
+ RouteData = new RouteValueDictionary();
+ }
+
+ public RemoteAttribute(string routeName)
+ : this()
+ {
+ if (String.IsNullOrWhiteSpace(routeName))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "routeName");
+ }
+
+ RouteName = routeName;
+ }
+
+ public RemoteAttribute(string action, string controller)
+ :
+ this(action, controller, null /* areaName */)
+ {
+ }
+
+ public RemoteAttribute(string action, string controller, string areaName)
+ : this()
+ {
+ if (String.IsNullOrWhiteSpace(action))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "action");
+ }
+ if (String.IsNullOrWhiteSpace(controller))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "controller");
+ }
+
+ RouteData["controller"] = controller;
+ RouteData["action"] = action;
+
+ if (!String.IsNullOrWhiteSpace(areaName))
+ {
+ RouteData["area"] = areaName;
+ }
+ }
+
+ public string HttpMethod { get; set; }
+
+ public string AdditionalFields
+ {
+ get { return _additionalFields ?? String.Empty; }
+ set
+ {
+ _additionalFields = value;
+ _additonalFieldsSplit = AuthorizeAttribute.SplitString(value);
+ }
+ }
+
+ protected RouteValueDictionary RouteData { get; private set; }
+
+ protected string RouteName { get; set; }
+
+ protected virtual RouteCollection Routes
+ {
+ get { return RouteTable.Routes; }
+ }
+
+ public string FormatAdditionalFieldsForClientValidation(string property)
+ {
+ if (String.IsNullOrEmpty(property))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "property");
+ }
+
+ string delimitedAdditionalFields = FormatPropertyForClientValidation(property);
+
+ foreach (string field in _additonalFieldsSplit)
+ {
+ delimitedAdditionalFields += "," + FormatPropertyForClientValidation(field);
+ }
+
+ return delimitedAdditionalFields;
+ }
+
+ public static string FormatPropertyForClientValidation(string property)
+ {
+ if (String.IsNullOrEmpty(property))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "property");
+ }
+ return "*." + property;
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings", Justification = "The value is a not a regular URL since it may contain ~/ ASP.NET-specific characters")]
+ protected virtual string GetUrl(ControllerContext controllerContext)
+ {
+ var pathData = Routes.GetVirtualPathForArea(controllerContext.RequestContext,
+ RouteName,
+ RouteData);
+
+ if (pathData == null)
+ {
+ throw new InvalidOperationException(MvcResources.RemoteAttribute_NoUrlFound);
+ }
+
+ return pathData.VirtualPath;
+ }
+
+ public override string FormatErrorMessage(string name)
+ {
+ return String.Format(CultureInfo.CurrentCulture, ErrorMessageString, name);
+ }
+
+ public override bool IsValid(object value)
+ {
+ return true;
+ }
+
+ public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
+ {
+ yield return new ModelClientValidationRemoteRule(FormatErrorMessage(metadata.GetDisplayName()), GetUrl(context), HttpMethod, FormatAdditionalFieldsForClientValidation(metadata.PropertyName));
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/RequireHttpsAttribute.cs b/src/System.Web.Mvc/RequireHttpsAttribute.cs
new file mode 100644
index 00000000..f39b7865
--- /dev/null
+++ b/src/System.Web.Mvc/RequireHttpsAttribute.cs
@@ -0,0 +1,38 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc
+{
+ [SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", Justification = "Unsealed because type contains virtual extensibility points.")]
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
+ public class RequireHttpsAttribute : FilterAttribute, IAuthorizationFilter
+ {
+ public virtual void OnAuthorization(AuthorizationContext filterContext)
+ {
+ if (filterContext == null)
+ {
+ throw new ArgumentNullException("filterContext");
+ }
+
+ if (!filterContext.HttpContext.Request.IsSecureConnection)
+ {
+ HandleNonHttpsRequest(filterContext);
+ }
+ }
+
+ protected virtual void HandleNonHttpsRequest(AuthorizationContext filterContext)
+ {
+ // only redirect for GET requests, otherwise the browser might not propagate the verb and request
+ // body correctly.
+
+ if (!String.Equals(filterContext.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))
+ {
+ throw new InvalidOperationException(MvcResources.RequireHttpsAttribute_MustUseSsl);
+ }
+
+ // redirect to HTTPS version of page
+ string url = "https://" + filterContext.HttpContext.Request.Url.Host + filterContext.HttpContext.Request.RawUrl;
+ filterContext.Result = new RedirectResult(url);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/RequiredAttributeAdapter.cs b/src/System.Web.Mvc/RequiredAttributeAdapter.cs
new file mode 100644
index 00000000..bfdce5fc
--- /dev/null
+++ b/src/System.Web.Mvc/RequiredAttributeAdapter.cs
@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+
+namespace System.Web.Mvc
+{
+ public class RequiredAttributeAdapter : DataAnnotationsModelValidator<RequiredAttribute>
+ {
+ public RequiredAttributeAdapter(ModelMetadata metadata, ControllerContext context, RequiredAttribute attribute)
+ : base(metadata, context, attribute)
+ {
+ }
+
+ public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
+ {
+ return new[] { new ModelClientValidationRequiredRule(ErrorMessage) };
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ResultExecutedContext.cs b/src/System.Web.Mvc/ResultExecutedContext.cs
new file mode 100644
index 00000000..c5e70127
--- /dev/null
+++ b/src/System.Web.Mvc/ResultExecutedContext.cs
@@ -0,0 +1,34 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Web.Mvc
+{
+ public class ResultExecutedContext : ControllerContext
+ {
+ // parameterless constructor used for mocking
+ public ResultExecutedContext()
+ {
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "The virtual property setters are only to support mocking frameworks, in which case this constructor shouldn't be called anyway.")]
+ public ResultExecutedContext(ControllerContext controllerContext, ActionResult result, bool canceled, Exception exception)
+ : base(controllerContext)
+ {
+ if (result == null)
+ {
+ throw new ArgumentNullException("result");
+ }
+
+ Result = result;
+ Canceled = canceled;
+ Exception = exception;
+ }
+
+ public virtual bool Canceled { get; set; }
+
+ public virtual Exception Exception { get; set; }
+
+ public bool ExceptionHandled { get; set; }
+
+ public virtual ActionResult Result { get; set; }
+ }
+}
diff --git a/src/System.Web.Mvc/ResultExecutingContext.cs b/src/System.Web.Mvc/ResultExecutingContext.cs
new file mode 100644
index 00000000..8d1910db
--- /dev/null
+++ b/src/System.Web.Mvc/ResultExecutingContext.cs
@@ -0,0 +1,28 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Web.Mvc
+{
+ public class ResultExecutingContext : ControllerContext
+ {
+ // parameterless constructor used for mocking
+ public ResultExecutingContext()
+ {
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "The virtual property setters are only to support mocking frameworks, in which case this constructor shouldn't be called anyway.")]
+ public ResultExecutingContext(ControllerContext controllerContext, ActionResult result)
+ : base(controllerContext)
+ {
+ if (result == null)
+ {
+ throw new ArgumentNullException("result");
+ }
+
+ Result = result;
+ }
+
+ public bool Cancel { get; set; }
+
+ public virtual ActionResult Result { get; set; }
+ }
+}
diff --git a/src/System.Web.Mvc/RouteCollectionExtensions.cs b/src/System.Web.Mvc/RouteCollectionExtensions.cs
new file mode 100644
index 00000000..95626bbc
--- /dev/null
+++ b/src/System.Web.Mvc/RouteCollectionExtensions.cs
@@ -0,0 +1,193 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Web.Routing;
+
+namespace System.Web.Mvc
+{
+ public static class RouteCollectionExtensions
+ {
+ // This method returns a new RouteCollection containing only routes that matched a particular area.
+ // The Boolean out parameter is just a flag specifying whether any registered routes were area-aware.
+ private static RouteCollection FilterRouteCollectionByArea(RouteCollection routes, string areaName, out bool usingAreas)
+ {
+ if (areaName == null)
+ {
+ areaName = String.Empty;
+ }
+
+ usingAreas = false;
+ RouteCollection filteredRoutes = new RouteCollection();
+
+ using (routes.GetReadLock())
+ {
+ foreach (RouteBase route in routes)
+ {
+ string thisAreaName = AreaHelpers.GetAreaName(route) ?? String.Empty;
+ usingAreas |= (thisAreaName.Length > 0);
+ if (String.Equals(thisAreaName, areaName, StringComparison.OrdinalIgnoreCase))
+ {
+ filteredRoutes.Add(route);
+ }
+ }
+ }
+
+ // if areas are not in use, the filtered route collection might be incorrect
+ return (usingAreas) ? filteredRoutes : routes;
+ }
+
+ public static VirtualPathData GetVirtualPathForArea(this RouteCollection routes, RequestContext requestContext, RouteValueDictionary values)
+ {
+ return GetVirtualPathForArea(routes, requestContext, null /* name */, values);
+ }
+
+ public static VirtualPathData GetVirtualPathForArea(this RouteCollection routes, RequestContext requestContext, string name, RouteValueDictionary values)
+ {
+ bool usingAreas; // don't care about this value
+ return GetVirtualPathForArea(routes, requestContext, name, values, out usingAreas);
+ }
+
+ internal static VirtualPathData GetVirtualPathForArea(this RouteCollection routes, RequestContext requestContext, string name, RouteValueDictionary values, out bool usingAreas)
+ {
+ if (routes == null)
+ {
+ throw new ArgumentNullException("routes");
+ }
+
+ if (!String.IsNullOrEmpty(name))
+ {
+ // the route name is a stronger qualifier than the area name, so just pipe it through
+ usingAreas = false;
+ return routes.GetVirtualPath(requestContext, name, values);
+ }
+
+ string targetArea = null;
+ if (values != null)
+ {
+ object targetAreaRawValue;
+ if (values.TryGetValue("area", out targetAreaRawValue))
+ {
+ targetArea = targetAreaRawValue as string;
+ }
+ else
+ {
+ // set target area to current area
+ if (requestContext != null)
+ {
+ targetArea = AreaHelpers.GetAreaName(requestContext.RouteData);
+ }
+ }
+ }
+
+ // need to apply a correction to the RVD if areas are in use
+ RouteValueDictionary correctedValues = values;
+ RouteCollection filteredRoutes = FilterRouteCollectionByArea(routes, targetArea, out usingAreas);
+ if (usingAreas)
+ {
+ correctedValues = new RouteValueDictionary(values);
+ correctedValues.Remove("area");
+ }
+
+ VirtualPathData vpd = filteredRoutes.GetVirtualPath(requestContext, correctedValues);
+ return vpd;
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "This is not a regular URL as it may contain special routing characters.")]
+ public static void IgnoreRoute(this RouteCollection routes, string url)
+ {
+ IgnoreRoute(routes, url, null /* constraints */);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "This is not a regular URL as it may contain special routing characters.")]
+ public static void IgnoreRoute(this RouteCollection routes, string url, object constraints)
+ {
+ if (routes == null)
+ {
+ throw new ArgumentNullException("routes");
+ }
+ if (url == null)
+ {
+ throw new ArgumentNullException("url");
+ }
+
+ IgnoreRouteInternal route = new IgnoreRouteInternal(url)
+ {
+ Constraints = new RouteValueDictionary(constraints)
+ };
+
+ routes.Add(route);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "2#", Justification = "This is not a regular URL as it may contain special routing characters.")]
+ public static Route MapRoute(this RouteCollection routes, string name, string url)
+ {
+ return MapRoute(routes, name, url, null /* defaults */, (object)null /* constraints */);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "2#", Justification = "This is not a regular URL as it may contain special routing characters.")]
+ public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults)
+ {
+ return MapRoute(routes, name, url, defaults, (object)null /* constraints */);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "2#", Justification = "This is not a regular URL as it may contain special routing characters.")]
+ public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints)
+ {
+ return MapRoute(routes, name, url, defaults, constraints, null /* namespaces */);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "2#", Justification = "This is not a regular URL as it may contain special routing characters.")]
+ public static Route MapRoute(this RouteCollection routes, string name, string url, string[] namespaces)
+ {
+ return MapRoute(routes, name, url, null /* defaults */, null /* constraints */, namespaces);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "2#", Justification = "This is not a regular URL as it may contain special routing characters.")]
+ public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, string[] namespaces)
+ {
+ return MapRoute(routes, name, url, defaults, null /* constraints */, namespaces);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "2#", Justification = "This is not a regular URL as it may contain special routing characters.")]
+ public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints, string[] namespaces)
+ {
+ if (routes == null)
+ {
+ throw new ArgumentNullException("routes");
+ }
+ if (url == null)
+ {
+ throw new ArgumentNullException("url");
+ }
+
+ Route route = new Route(url, new MvcRouteHandler())
+ {
+ Defaults = new RouteValueDictionary(defaults),
+ Constraints = new RouteValueDictionary(constraints),
+ DataTokens = new RouteValueDictionary()
+ };
+
+ if ((namespaces != null) && (namespaces.Length > 0))
+ {
+ route.DataTokens["Namespaces"] = namespaces;
+ }
+
+ routes.Add(name, route);
+
+ return route;
+ }
+
+ private sealed class IgnoreRouteInternal : Route
+ {
+ public IgnoreRouteInternal(string url)
+ : base(url, new StopRoutingHandler())
+ {
+ }
+
+ public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary routeValues)
+ {
+ // Never match during route generation. This avoids the scenario where an IgnoreRoute with
+ // fairly relaxed constraints ends up eagerly matching all generated URLs.
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/RouteDataValueProvider.cs b/src/System.Web.Mvc/RouteDataValueProvider.cs
new file mode 100644
index 00000000..f31ec75a
--- /dev/null
+++ b/src/System.Web.Mvc/RouteDataValueProvider.cs
@@ -0,0 +1,14 @@
+using System.Globalization;
+
+namespace System.Web.Mvc
+{
+ public sealed class RouteDataValueProvider : DictionaryValueProvider<object>
+ {
+ // RouteData should use the invariant culture since it's part of the URL, and the URL should be
+ // interpreted in a uniform fashion regardless of the origin of a particular request.
+ public RouteDataValueProvider(ControllerContext controllerContext)
+ : base(controllerContext.RouteData.Values, CultureInfo.InvariantCulture)
+ {
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/RouteDataValueProviderFactory.cs b/src/System.Web.Mvc/RouteDataValueProviderFactory.cs
new file mode 100644
index 00000000..b2054158
--- /dev/null
+++ b/src/System.Web.Mvc/RouteDataValueProviderFactory.cs
@@ -0,0 +1,15 @@
+namespace System.Web.Mvc
+{
+ public sealed class RouteDataValueProviderFactory : ValueProviderFactory
+ {
+ public override IValueProvider GetValueProvider(ControllerContext controllerContext)
+ {
+ if (controllerContext == null)
+ {
+ throw new ArgumentNullException("controllerContext");
+ }
+
+ return new RouteDataValueProvider(controllerContext);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/RouteValuesHelpers.cs b/src/System.Web.Mvc/RouteValuesHelpers.cs
new file mode 100644
index 00000000..4e4b6904
--- /dev/null
+++ b/src/System.Web.Mvc/RouteValuesHelpers.cs
@@ -0,0 +1,60 @@
+using System.Collections.Generic;
+using System.Web.Routing;
+
+namespace System.Web.Mvc
+{
+ internal static class RouteValuesHelpers
+ {
+ public static RouteValueDictionary GetRouteValues(RouteValueDictionary routeValues)
+ {
+ return (routeValues != null) ? new RouteValueDictionary(routeValues) : new RouteValueDictionary();
+ }
+
+ public static RouteValueDictionary MergeRouteValues(string actionName, string controllerName, RouteValueDictionary implicitRouteValues, RouteValueDictionary routeValues, bool includeImplicitMvcValues)
+ {
+ // Create a new dictionary containing implicit and auto-generated values
+ RouteValueDictionary mergedRouteValues = new RouteValueDictionary();
+
+ if (includeImplicitMvcValues)
+ {
+ // We only include MVC-specific values like 'controller' and 'action' if we are generating an action link.
+ // If we are generating a route link [as to MapRoute("Foo", "any/url", new { controller = ... })], including
+ // the current controller name will cause the route match to fail if the current controller is not the same
+ // as the destination controller.
+
+ object implicitValue;
+ if (implicitRouteValues != null && implicitRouteValues.TryGetValue("action", out implicitValue))
+ {
+ mergedRouteValues["action"] = implicitValue;
+ }
+
+ if (implicitRouteValues != null && implicitRouteValues.TryGetValue("controller", out implicitValue))
+ {
+ mergedRouteValues["controller"] = implicitValue;
+ }
+ }
+
+ // Merge values from the user's dictionary/object
+ if (routeValues != null)
+ {
+ foreach (KeyValuePair<string, object> routeElement in GetRouteValues(routeValues))
+ {
+ mergedRouteValues[routeElement.Key] = routeElement.Value;
+ }
+ }
+
+ // Merge explicit parameters when not null
+ if (actionName != null)
+ {
+ mergedRouteValues["action"] = actionName;
+ }
+
+ if (controllerName != null)
+ {
+ mergedRouteValues["controller"] = controllerName;
+ }
+
+ return mergedRouteValues;
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/SecurityUtil.cs b/src/System.Web.Mvc/SecurityUtil.cs
new file mode 100644
index 00000000..becded41
--- /dev/null
+++ b/src/System.Web.Mvc/SecurityUtil.cs
@@ -0,0 +1,79 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+using System.Security;
+
+namespace System.Web.Mvc
+{
+ internal static class SecurityUtil
+ {
+ private static Action<Action> _callInAppTrustThunk;
+
+ // !! IMPORTANT !!
+ // Do not try to optimize this method or perform any extra caching; doing so could lead to MVC not operating
+ // correctly until the AppDomain is restarted.
+ [SuppressMessage("Microsoft.Security", "CA2107:ReviewDenyAndPermitOnlyUsage",
+ Justification = "This is essentially the same logic as Page.ProcessRequest.")]
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes",
+ Justification = "If an exception is thrown, assume we're running in same trust level as the application itself, so we don't need to do anything special.")]
+ private static Action<Action> GetCallInAppTrustThunk()
+ {
+ // do we need to create the thunk?
+ if (_callInAppTrustThunk == null)
+ {
+ try
+ {
+ if (!typeof(SecurityUtil).Assembly.IsFullyTrusted /* bin-deployed */
+ || AppDomain.CurrentDomain.IsHomogenous /* .NET 4 CAS model */)
+ {
+ // we're already running in the application's trust level, so nothing to do
+ _callInAppTrustThunk = f => f();
+ }
+ else
+ {
+ // legacy CAS model - need to lower own permission level to be compatible with legacy systems
+ // This is essentially the same logic as Page.ProcessRequest(HttpContext)
+ NamedPermissionSet namedPermissionSet = (NamedPermissionSet)typeof(HttpRuntime).GetProperty("NamedPermissionSet", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static).GetValue(null, null);
+ bool disableProcessRequestInApplicationTrust = (bool)typeof(HttpRuntime).GetProperty("DisableProcessRequestInApplicationTrust", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static).GetValue(null, null);
+ if (namedPermissionSet != null && !disableProcessRequestInApplicationTrust)
+ {
+ _callInAppTrustThunk = f =>
+ {
+ // lower permissions
+ namedPermissionSet.PermitOnly();
+ f();
+ };
+ }
+ else
+ {
+ // application's trust level is FullTrust, so nothing to do
+ _callInAppTrustThunk = f => f();
+ }
+ }
+ }
+ catch
+ {
+ // MVC assembly is already running in application trust, so swallow exceptions
+ }
+ }
+
+ // if there was an error, just process transparently
+ return _callInAppTrustThunk ?? (Action<Action>)(f => f());
+ }
+
+ public static TResult ProcessInApplicationTrust<TResult>(Func<TResult> func)
+ {
+ TResult result = default(TResult);
+ ProcessInApplicationTrust(delegate
+ {
+ result = func();
+ });
+ return result;
+ }
+
+ public static void ProcessInApplicationTrust(Action action)
+ {
+ Action<Action> executor = GetCallInAppTrustThunk();
+ executor(action);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/SelectList.cs b/src/System.Web.Mvc/SelectList.cs
new file mode 100644
index 00000000..2c605386
--- /dev/null
+++ b/src/System.Web.Mvc/SelectList.cs
@@ -0,0 +1,37 @@
+using System.Collections;
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Web.Mvc
+{
+ [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix", Justification = "This is a shipped API")]
+ public class SelectList : MultiSelectList
+ {
+ public SelectList(IEnumerable items)
+ : this(items, null /* selectedValue */)
+ {
+ }
+
+ public SelectList(IEnumerable items, object selectedValue)
+ : this(items, null /* dataValuefield */, null /* dataTextField */, selectedValue)
+ {
+ }
+
+ public SelectList(IEnumerable items, string dataValueField, string dataTextField)
+ : this(items, dataValueField, dataTextField, null /* selectedValue */)
+ {
+ }
+
+ public SelectList(IEnumerable items, string dataValueField, string dataTextField, object selectedValue)
+ : base(items, dataValueField, dataTextField, ToEnumerable(selectedValue))
+ {
+ SelectedValue = selectedValue;
+ }
+
+ public object SelectedValue { get; private set; }
+
+ private static IEnumerable ToEnumerable(object selectedValue)
+ {
+ return (selectedValue != null) ? new object[] { selectedValue } : null;
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/SelectListItem.cs b/src/System.Web.Mvc/SelectListItem.cs
new file mode 100644
index 00000000..650540d9
--- /dev/null
+++ b/src/System.Web.Mvc/SelectListItem.cs
@@ -0,0 +1,11 @@
+namespace System.Web.Mvc
+{
+ public class SelectListItem
+ {
+ public bool Selected { get; set; }
+
+ public string Text { get; set; }
+
+ public string Value { get; set; }
+ }
+}
diff --git a/src/System.Web.Mvc/SessionStateAttribute.cs b/src/System.Web.Mvc/SessionStateAttribute.cs
new file mode 100644
index 00000000..c675bf60
--- /dev/null
+++ b/src/System.Web.Mvc/SessionStateAttribute.cs
@@ -0,0 +1,15 @@
+using System.Web.SessionState;
+
+namespace System.Web.Mvc
+{
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
+ public sealed class SessionStateAttribute : Attribute
+ {
+ public SessionStateAttribute(SessionStateBehavior behavior)
+ {
+ Behavior = behavior;
+ }
+
+ public SessionStateBehavior Behavior { get; private set; }
+ }
+}
diff --git a/src/System.Web.Mvc/SessionStateTempDataProvider.cs b/src/System.Web.Mvc/SessionStateTempDataProvider.cs
new file mode 100644
index 00000000..01f83f08
--- /dev/null
+++ b/src/System.Web.Mvc/SessionStateTempDataProvider.cs
@@ -0,0 +1,64 @@
+using System.Collections.Generic;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc
+{
+ public class SessionStateTempDataProvider : ITempDataProvider
+ {
+ internal const string TempDataSessionStateKey = "__ControllerTempData";
+
+ public virtual IDictionary<string, object> LoadTempData(ControllerContext controllerContext)
+ {
+ HttpSessionStateBase session = controllerContext.HttpContext.Session;
+
+ if (session != null)
+ {
+ Dictionary<string, object> tempDataDictionary = session[TempDataSessionStateKey] as Dictionary<string, object>;
+
+ if (tempDataDictionary != null)
+ {
+ // If we got it from Session, remove it so that no other request gets it
+ session.Remove(TempDataSessionStateKey);
+ return tempDataDictionary;
+ }
+ }
+
+ return new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
+ }
+
+ public virtual void SaveTempData(ControllerContext controllerContext, IDictionary<string, object> values)
+ {
+ if (controllerContext == null)
+ {
+ throw new ArgumentNullException("controllerContext");
+ }
+
+ HttpSessionStateBase session = controllerContext.HttpContext.Session;
+ bool isDirty = (values != null && values.Count > 0);
+
+ if (session == null)
+ {
+ if (isDirty)
+ {
+ throw new InvalidOperationException(MvcResources.SessionStateTempDataProvider_SessionStateDisabled);
+ }
+ }
+ else
+ {
+ if (isDirty)
+ {
+ session[TempDataSessionStateKey] = values;
+ }
+ else
+ {
+ // Since the default implementation of Remove() (from SessionStateItemCollection) dirties the
+ // collection, we shouldn't call it unless we really do need to remove the existing key.
+ if (session[TempDataSessionStateKey] != null)
+ {
+ session.Remove(TempDataSessionStateKey);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/SingleServiceResolver.cs b/src/System.Web.Mvc/SingleServiceResolver.cs
new file mode 100644
index 00000000..b1f5c344
--- /dev/null
+++ b/src/System.Web.Mvc/SingleServiceResolver.cs
@@ -0,0 +1,65 @@
+using System.Globalization;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc
+{
+ internal class SingleServiceResolver<TService> : IResolver<TService>
+ where TService : class
+ {
+ private TService _currentValueFromResolver;
+ private Func<TService> _currentValueThunk;
+ private TService _defaultValue;
+ private Func<IDependencyResolver> _resolverThunk;
+ private string _callerMethodName;
+
+ public SingleServiceResolver(Func<TService> currentValueThunk, TService defaultValue, string callerMethodName)
+ {
+ if (currentValueThunk == null)
+ {
+ throw new ArgumentNullException("currentValueThunk");
+ }
+ if (defaultValue == null)
+ {
+ throw new ArgumentNullException("defaultValue");
+ }
+
+ _resolverThunk = () => DependencyResolver.Current;
+ _currentValueThunk = currentValueThunk;
+ _defaultValue = defaultValue;
+ _callerMethodName = callerMethodName;
+ }
+
+ internal SingleServiceResolver(Func<TService> staticAccessor, TService defaultValue, IDependencyResolver resolver, string callerMethodName)
+ : this(staticAccessor, defaultValue, callerMethodName)
+ {
+ if (resolver != null)
+ {
+ _resolverThunk = () => resolver;
+ }
+ }
+
+ public TService Current
+ {
+ get
+ {
+ if (_resolverThunk != null)
+ {
+ lock (_currentValueThunk)
+ {
+ if (_resolverThunk != null)
+ {
+ _currentValueFromResolver = _resolverThunk().GetService<TService>();
+ _resolverThunk = null;
+
+ if (_currentValueFromResolver != null && _currentValueThunk() != null)
+ {
+ throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, MvcResources.SingleServiceResolver_CannotRegisterTwoInstances, typeof(TService).Name.ToString(), _callerMethodName));
+ }
+ }
+ }
+ }
+ return _currentValueFromResolver ?? _currentValueThunk() ?? _defaultValue;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/StringLengthAttributeAdapter.cs b/src/System.Web.Mvc/StringLengthAttributeAdapter.cs
new file mode 100644
index 00000000..ad301038
--- /dev/null
+++ b/src/System.Web.Mvc/StringLengthAttributeAdapter.cs
@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+
+namespace System.Web.Mvc
+{
+ public class StringLengthAttributeAdapter : DataAnnotationsModelValidator<StringLengthAttribute>
+ {
+ public StringLengthAttributeAdapter(ModelMetadata metadata, ControllerContext context, StringLengthAttribute attribute)
+ : base(metadata, context, attribute)
+ {
+ }
+
+ public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
+ {
+ return new[] { new ModelClientValidationStringLengthRule(ErrorMessage, Attribute.MinimumLength, Attribute.MaximumLength) };
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/System.Web.Mvc.csproj b/src/System.Web.Mvc/System.Web.Mvc.csproj
new file mode 100644
index 00000000..646cce61
--- /dev/null
+++ b/src/System.Web.Mvc/System.Web.Mvc.csproj
@@ -0,0 +1,475 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <CodeAnalysis Condition=" '$(CodeAnalysis)' == '' ">false</CodeAnalysis>
+ <ProductVersion>9.0.30729</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{3D3FFD8A-624D-4E9B-954B-E1C105507975}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>System.Web.Mvc</RootNamespace>
+ <AssemblyName>System.Web.Mvc</AssemblyName>
+ <FileAlignment>512</FileAlignment>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ <BaseAddress>1609891840</BaseAddress>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>..\..\bin\Debug\</OutputPath>
+ <DefineConstants>TRACE;DEBUG;ASPNETMVC</DefineConstants>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ <DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
+ <NoWarn>1591</NoWarn>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>..\..\bin\Release\</OutputPath>
+ <DefineConstants>TRACE;ASPNETMVC</DefineConstants>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ <RunCodeAnalysis>$(CodeAnalysis)</RunCodeAnalysis>
+ <DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
+ <NoWarn>1591</NoWarn>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'CodeCoverage|AnyCPU'">
+ <DebugSymbols>true</DebugSymbols>
+ <OutputPath>..\..\bin\CodeCoverage\</OutputPath>
+ <DefineConstants>TRACE;DEBUG;CODE_COVERAGE;ASPNETMVC</DefineConstants>
+ <DebugType>full</DebugType>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="Microsoft.Web.Infrastructure, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+ <HintPath>..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll</HintPath>
+ </Reference>
+ <Reference Include="System" />
+ <Reference Include="System.ComponentModel.DataAnnotations" />
+ <Reference Include="System.configuration" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.Data.Entity" />
+ <Reference Include="System.Data.Linq" />
+ <Reference Include="System.Runtime.Caching" />
+ <Reference Include="System.Web" />
+ <Reference Include="System.Data" />
+ <Reference Include="System.Web.Abstractions" />
+ <Reference Include="System.Web.Extensions" />
+ <Reference Include="System.Web.Routing" />
+ <Reference Include="System.Xml" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="..\CommonAssemblyInfo.cs">
+ <Link>Properties\CommonAssemblyInfo.cs</Link>
+ </Compile>
+ <Compile Include="..\System.Web.Http.Common\TaskHelpers.cs">
+ <Link>Async\TaskHelpers.cs</Link>
+ </Compile>
+ <Compile Include="..\System.Web.Http.Common\TaskHelpersExtensions.cs">
+ <Link>Async\TaskHelpersExtensions.cs</Link>
+ </Compile>
+ <Compile Include="..\TransparentCommonAssemblyInfo.cs">
+ <Link>Properties\TransparentCommonAssemblyInfo.cs</Link>
+ </Compile>
+ <Compile Include="AdditionalMetaDataAttribute.cs" />
+ <Compile Include="AllowAnonymousAttribute.cs" />
+ <Compile Include="ActionDescriptorHelper.cs" />
+ <Compile Include="Async\TaskAsyncActionDescriptor.cs" />
+ <Compile Include="Async\TaskWrapperAsyncResult.cs" />
+ <Compile Include="BuildManagerCompiledView.cs" />
+ <Compile Include="BuildManagerViewEngine.cs" />
+ <Compile Include="CachedAssociatedMetadataProvider`1.cs" />
+ <Compile Include="CachedDataAnnotationsMetadataAttributes.cs" />
+ <Compile Include="CachedDataAnnotationsModelMetadata.cs" />
+ <Compile Include="CachedDataAnnotationsModelMetadataProvider.cs" />
+ <Compile Include="CachedModelMetadata`1.cs" />
+ <Compile Include="CancellationTokenModelBinder.cs" />
+ <Compile Include="CompareAttribute.cs" />
+ <Compile Include="ChildActionValueProvider.cs" />
+ <Compile Include="ChildActionValueProviderFactory.cs" />
+ <Compile Include="IEnumerableValueProvider.cs" />
+ <Compile Include="DataTypeUtil.cs" />
+ <Compile Include="Html\DisplayNameExtensions.cs" />
+ <Compile Include="Html\NameExtensions.cs" />
+ <Compile Include="Html\ValueExtensions.cs" />
+ <Compile Include="Razor\MvcCSharpRazorCodeGenerator.cs" />
+ <Compile Include="Razor\SetModelTypeCodeGenerator.cs" />
+ <Compile Include="ReflectedAttributeCache.cs" />
+ <Compile Include="SessionStateAttribute.cs" />
+ <Compile Include="AllowHtmlAttribute.cs" />
+ <Compile Include="UnvalidatedRequestValuesAccessor.cs" />
+ <Compile Include="UnvalidatedRequestValuesWrapper.cs" />
+ <Compile Include="IUnvalidatedRequestValues.cs" />
+ <Compile Include="IUnvalidatedValueProvider.cs" />
+ <Compile Include="DependencyResolverExtensions.cs" />
+ <Compile Include="ExpressionUtil\BinaryExpressionFingerprint.cs" />
+ <Compile Include="ExpressionUtil\CachedExpressionCompiler.cs" />
+ <Compile Include="ExpressionUtil\ConditionalExpressionFingerprint.cs" />
+ <Compile Include="ExpressionUtil\ConstantExpressionFingerprint.cs" />
+ <Compile Include="ExpressionUtil\DefaultExpressionFingerprint.cs" />
+ <Compile Include="ExpressionUtil\ExpressionFingerprint.cs" />
+ <Compile Include="ExpressionUtil\ExpressionFingerprintChain.cs" />
+ <Compile Include="ExpressionUtil\FingerprintingExpressionVisitor.cs" />
+ <Compile Include="ExpressionUtil\HashCodeCombiner.cs" />
+ <Compile Include="ExpressionUtil\Hoisted`2.cs" />
+ <Compile Include="ExpressionUtil\HoistingExpressionVisitor.cs" />
+ <Compile Include="ExpressionUtil\IndexExpressionFingerprint.cs" />
+ <Compile Include="ExpressionUtil\LambdaExpressionFingerprint.cs" />
+ <Compile Include="ExpressionUtil\MemberExpressionFingerprint.cs" />
+ <Compile Include="ExpressionUtil\MethodCallExpressionFingerprint.cs" />
+ <Compile Include="ExpressionUtil\ParameterExpressionFingerprint.cs" />
+ <Compile Include="ExpressionUtil\TypeBinaryExpressionFingerprint.cs" />
+ <Compile Include="ExpressionUtil\UnaryExpressionFingerprint.cs" />
+ <Compile Include="IControllerActivator.cs" />
+ <Compile Include="IModelBinderProvider.cs" />
+ <Compile Include="IUniquelyIdentifiable.cs" />
+ <Compile Include="IViewStartPageChild.cs" />
+ <Compile Include="IResolver.cs" />
+ <Compile Include="ControllerInstanceFilterProvider.cs" />
+ <Compile Include="RazorView.cs" />
+ <Compile Include="RazorViewEngine.cs" />
+ <Compile Include="DynamicViewDataDictionary.cs" />
+ <Compile Include="Filter.cs" />
+ <Compile Include="FilterAttributeFilterProvider.cs" />
+ <Compile Include="FilterProviderCollection.cs" />
+ <Compile Include="FilterProviders.cs" />
+ <Compile Include="FilterScope.cs" />
+ <Compile Include="GlobalFilterCollection.cs" />
+ <Compile Include="GlobalFilters.cs" />
+ <Compile Include="IFilterProvider.cs" />
+ <Compile Include="IMvcFilter.cs" />
+ <Compile Include="IViewPageActivator.cs" />
+ <Compile Include="ModelBinderProviderCollection.cs" />
+ <Compile Include="ModelBinderProviders.cs" />
+ <Compile Include="MultiServiceResolver.cs" />
+ <Compile Include="Razor\MvcCSharpRazorCodeParser.cs" />
+ <Compile Include="MvcFilter.cs" />
+ <Compile Include="Razor\MvcVBRazorCodeParser.cs" />
+ <Compile Include="Razor\MvcWebPageRazorHost.cs" />
+ <Compile Include="MvcWebRazorHostFactory.cs" />
+ <Compile Include="PreApplicationStartCode.cs" />
+ <Compile Include="RemoteAttribute.cs" />
+ <Compile Include="SecurityUtil.cs" />
+ <Compile Include="SingleServiceResolver.cs" />
+ <Compile Include="Razor\StartPageLookupDelegate.cs" />
+ <Compile Include="TagBuilderExtensions.cs" />
+ <Compile Include="UrlRewriterHelper.cs" />
+ <Compile Include="ViewStartPage.cs" />
+ <Compile Include="WebViewPage.cs" />
+ <Compile Include="WebViewPage`1.cs" />
+ <Compile Include="HttpNotFoundResult.cs" />
+ <Compile Include="HttpStatusCodeResult.cs" />
+ <Compile Include="IMvcControlBuilder.cs" />
+ <Compile Include="AssociatedMetadataProvider.cs" />
+ <Compile Include="ActionExecutedContext.cs" />
+ <Compile Include="ActionExecutingContext.cs" />
+ <Compile Include="ClientDataTypeModelValidatorProvider.cs" />
+ <Compile Include="AssociatedValidatorProvider.cs" />
+ <Compile Include="Async\ActionDescriptorCreator.cs" />
+ <Compile Include="Async\AsyncActionDescriptor.cs" />
+ <Compile Include="Async\AsyncActionMethodSelector.cs" />
+ <Compile Include="Async\AsyncControllerActionInvoker.cs" />
+ <Compile Include="Async\SynchronousOperationException.cs" />
+ <Compile Include="Async\AsyncManager.cs" />
+ <Compile Include="AsyncTimeoutAttribute.cs" />
+ <Compile Include="Async\BeginInvokeDelegate.cs" />
+ <Compile Include="Async\AsyncResultWrapper.cs" />
+ <Compile Include="Async\AsyncVoid.cs" />
+ <Compile Include="AsyncController.cs" />
+ <Compile Include="Async\AsyncUtil.cs" />
+ <Compile Include="Async\IAsyncController.cs" />
+ <Compile Include="Async\IAsyncActionInvoker.cs" />
+ <Compile Include="Async\IAsyncManagerContainer.cs" />
+ <Compile Include="IClientValidatable.cs" />
+ <Compile Include="IMetadataAware.cs" />
+ <Compile Include="IDependencyResolver.cs" />
+ <Compile Include="JsonValueProviderFactory.cs" />
+ <Compile Include="DependencyResolver.cs" />
+ <Compile Include="UrlParameter.cs" />
+ <Compile Include="FormValueProvider.cs" />
+ <Compile Include="FormValueProviderFactory.cs" />
+ <Compile Include="HttpFileCollectionValueProvider.cs" />
+ <Compile Include="HttpFileCollectionValueProviderFactory.cs" />
+ <Compile Include="QueryStringValueProvider.cs" />
+ <Compile Include="QueryStringValueProviderFactory.cs" />
+ <Compile Include="RangeAttributeAdapter.cs" />
+ <Compile Include="RegularExpressionAttributeAdapter.cs" />
+ <Compile Include="RequiredAttributeAdapter.cs" />
+ <Compile Include="RouteDataValueProvider.cs" />
+ <Compile Include="RouteDataValueProviderFactory.cs" />
+ <Compile Include="StringLengthAttributeAdapter.cs" />
+ <Compile Include="TypeCacheUtil.cs" />
+ <Compile Include="TypeCacheSerializer.cs" />
+ <Compile Include="Html\DisplayTextExtensions.cs" />
+ <Compile Include="NoAsyncTimeoutAttribute.cs" />
+ <Compile Include="Async\OperationCounter.cs" />
+ <Compile Include="Async\ReflectedAsyncActionDescriptor.cs" />
+ <Compile Include="Async\ReflectedAsyncControllerDescriptor.cs" />
+ <Compile Include="Async\Trigger.cs" />
+ <Compile Include="Async\TriggerListener.cs" />
+ <Compile Include="Async\SimpleAsyncResult.cs" />
+ <Compile Include="Async\EndInvokeDelegate.cs" />
+ <Compile Include="Async\EndInvokeDelegate`1.cs" />
+ <Compile Include="Async\SynchronizationContextUtil.cs" />
+ <Compile Include="AuthorizationContext.cs" />
+ <Compile Include="ByteArrayModelBinder.cs" />
+ <Compile Include="ControllerContext.cs" />
+ <Compile Include="Html\ChildActionExtensions.cs" />
+ <Compile Include="ParameterInfoUtil.cs" />
+ <Compile Include="HttpHandlerUtil.cs" />
+ <Compile Include="ChildActionOnlyAttribute.cs" />
+ <Compile Include="TypeDescriptorHelper.cs" />
+ <Compile Include="ValidatableObjectAdapter.cs" />
+ <Compile Include="ValueProviderFactories.cs" />
+ <Compile Include="ValueProviderFactory.cs" />
+ <Compile Include="ValueProviderFactoryCollection.cs" />
+ <Compile Include="ValueProviderCollection.cs" />
+ <Compile Include="DictionaryValueProvider`1.cs" />
+ <Compile Include="NameValueCollectionValueProvider.cs" />
+ <Compile Include="ValueProviderUtil.cs" />
+ <Compile Include="IValueProvider.cs" />
+ <Compile Include="DataErrorInfoModelValidatorProvider.cs" />
+ <Compile Include="ModelValidatorProviderCollection.cs" />
+ <Compile Include="DataAnnotationsModelMetadata.cs" />
+ <Compile Include="HiddenInputAttribute.cs" />
+ <Compile Include="HttpGetAttribute.cs" />
+ <Compile Include="HttpPutAttribute.cs" />
+ <Compile Include="HttpDeleteAttribute.cs" />
+ <Compile Include="MvcHtmlString.cs" />
+ <Compile Include="DataAnnotationsModelValidator.cs" />
+ <Compile Include="DataAnnotationsModelValidatorProvider.cs" />
+ <Compile Include="DataAnnotationsModelValidator`1.cs" />
+ <Compile Include="EmptyModelValidatorProvider.cs" />
+ <Compile Include="ExpressionHelper.cs" />
+ <Compile Include="FieldValidationMetadata.cs" />
+ <Compile Include="FormContext.cs" />
+ <Compile Include="JsonRequestBehavior.cs" />
+ <Compile Include="ModelValidationResult.cs" />
+ <Compile Include="ModelValidator.cs" />
+ <Compile Include="ModelValidatorProvider.cs" />
+ <Compile Include="ModelValidatorProviders.cs" />
+ <Compile Include="RequireHttpsAttribute.cs" />
+ <Compile Include="HttpRequestExtensions.cs" />
+ <Compile Include="DataAnnotationsModelMetadataProvider.cs" />
+ <Compile Include="EmptyModelMetadataProvider.cs" />
+ <Compile Include="ModelMetadata.cs" />
+ <Compile Include="ModelMetadataProvider.cs" />
+ <Compile Include="ModelMetadataProviders.cs" />
+ <Compile Include="AreaHelpers.cs" />
+ <Compile Include="AreaRegistration.cs" />
+ <Compile Include="AreaRegistrationContext.cs" />
+ <Compile Include="Error.cs" />
+ <Compile Include="IRouteWithArea.cs" />
+ <Compile Include="Async\SingleEntryGate.cs" />
+ <Compile Include="Html\PartialExtensions.cs" />
+ <Compile Include="LinqBinaryModelBinder.cs" />
+ <Compile Include="TryGetValueDelegate.cs" />
+ <Compile Include="ViewDataInfo.cs" />
+ <Compile Include="Html\DefaultDisplayTemplates.cs" />
+ <Compile Include="Html\DefaultEditorTemplates.cs" />
+ <Compile Include="Html\DisplayExtensions.cs" />
+ <Compile Include="Html\EditorExtensions.cs" />
+ <Compile Include="Html\LabelExtensions.cs" />
+ <Compile Include="Html\TemplateHelpers.cs" />
+ <Compile Include="HttpPostAttribute.cs" />
+ <Compile Include="PathHelpers.cs" />
+ <Compile Include="ExceptionContext.cs" />
+ <Compile Include="ResultExecutedContext.cs" />
+ <Compile Include="ResultExecutingContext.cs" />
+ <Compile Include="TemplateInfo.cs" />
+ <Compile Include="ValidateAntiForgeryTokenAttribute.cs" />
+ <Compile Include="JavaScriptResult.cs" />
+ <Compile Include="ActionDescriptor.cs" />
+ <Compile Include="ActionMethodDispatcher.cs" />
+ <Compile Include="ActionMethodSelector.cs" />
+ <Compile Include="ActionMethodSelectorAttribute.cs" />
+ <Compile Include="ActionNameSelectorAttribute.cs" />
+ <Compile Include="AuthorizeAttribute.cs" />
+ <Compile Include="Ajax\AjaxOptions.cs" />
+ <Compile Include="Ajax\AjaxExtensions.cs" />
+ <Compile Include="ActionMethodDispatcherCache.cs" />
+ <Compile Include="BindAttribute.cs" />
+ <Compile Include="ControllerBase.cs" />
+ <Compile Include="ActionNameAttribute.cs" />
+ <Compile Include="AcceptVerbsAttribute.cs" />
+ <Compile Include="AjaxHelper`1.cs" />
+ <Compile Include="HtmlHelper`1.cs" />
+ <Compile Include="DictionaryHelpers.cs" />
+ <Compile Include="AjaxRequestExtensions.cs" />
+ <Compile Include="ModelBinderDictionary.cs" />
+ <Compile Include="ValueProviderDictionary.cs" />
+ <Compile Include="ViewContext.cs" />
+ <Compile Include="ViewMasterPageControlBuilder.cs" />
+ <Compile Include="ViewTemplateUserControl.cs">
+ <SubType>ASPXCodeBehind</SubType>
+ </Compile>
+ <Compile Include="ViewTemplateUserControl`1.cs">
+ <SubType>ASPXCodeBehind</SubType>
+ </Compile>
+ <Compile Include="ViewType.cs" />
+ <Compile Include="ViewTypeControlBuilder.cs" />
+ <Compile Include="ViewUserControlControlBuilder.cs" />
+ <Compile Include="ViewPageControlBuilder.cs" />
+ <Compile Include="ViewTypeParserFilter.cs" />
+ <Compile Include="DefaultViewLocationCache.cs" />
+ <Compile Include="FormCollection.cs" />
+ <Compile Include="HttpPostedFileBaseModelBinder.cs" />
+ <Compile Include="NullViewLocationCache.cs" />
+ <Compile Include="ValidateInputAttribute.cs" />
+ <Compile Include="FileContentResult.cs" />
+ <Compile Include="FilePathResult.cs" />
+ <Compile Include="FileResult.cs" />
+ <Compile Include="FileStreamResult.cs" />
+ <Compile Include="InputType.cs" />
+ <Compile Include="ControllerDescriptorCache.cs" />
+ <Compile Include="ReflectedParameterBindingInfo.cs" />
+ <Compile Include="ParameterBindingInfo.cs" />
+ <Compile Include="ReaderWriterCache`2.cs" />
+ <Compile Include="DescriptorUtil.cs" />
+ <Compile Include="ReflectedControllerDescriptor.cs" />
+ <Compile Include="ControllerDescriptor.cs" />
+ <Compile Include="ActionSelector.cs" />
+ <Compile Include="ReflectedActionDescriptor.cs" />
+ <Compile Include="Html\MvcForm.cs" />
+ <Compile Include="HttpVerbs.cs" />
+ <Compile Include="DefaultModelBinder.cs" />
+ <Compile Include="ModelBindingContext.cs" />
+ <Compile Include="ParameterDescriptor.cs" />
+ <Compile Include="RouteValuesHelpers.cs" />
+ <Compile Include="SelectListItem.cs" />
+ <Compile Include="ReflectedParameterDescriptor.cs" />
+ <Compile Include="ValueProviderResult.cs" />
+ <Compile Include="CustomModelBinderAttribute.cs" />
+ <Compile Include="FormMethod.cs" />
+ <Compile Include="Html\FormExtensions.cs" />
+ <Compile Include="Html\InputExtensions.cs" />
+ <Compile Include="Html\RenderPartialExtensions.cs" />
+ <Compile Include="Html\SelectExtensions.cs" />
+ <Compile Include="Html\TextAreaExtensions.cs" />
+ <Compile Include="Html\ValidationExtensions.cs" />
+ <Compile Include="IModelBinder.cs" />
+ <Compile Include="Html\LinkExtensions.cs" />
+ <Compile Include="ModelBinderAttribute.cs" />
+ <Compile Include="ModelBinders.cs" />
+ <Compile Include="ModelStateDictionary.cs" />
+ <Compile Include="ModelState.cs" />
+ <Compile Include="ModelErrorCollection.cs" />
+ <Compile Include="ModelError.cs" />
+ <Compile Include="Ajax\InsertionMode.cs" />
+ <Compile Include="HandleErrorAttribute.cs" />
+ <Compile Include="HandleErrorInfo.cs" />
+ <Compile Include="HttpUnauthorizedResult.cs" />
+ <Compile Include="IActionInvoker.cs" />
+ <Compile Include="IView.cs" />
+ <Compile Include="IViewLocationCache.cs" />
+ <Compile Include="MvcHttpHandler.cs" />
+ <Compile Include="PartialViewResult.cs" />
+ <Compile Include="SessionStateTempDataProvider.cs" />
+ <Compile Include="ITempDataProvider.cs" />
+ <Compile Include="OutputCacheAttribute.cs" />
+ <Compile Include="FilterInfo.cs" />
+ <Compile Include="GlobalSuppressions.cs" />
+ <Compile Include="ActionFilterAttribute.cs" />
+ <Compile Include="ActionResult.cs" />
+ <Compile Include="AjaxHelper.cs" />
+ <Compile Include="BuildManagerWrapper.cs" />
+ <Compile Include="Controller.cs" />
+ <Compile Include="ControllerActionInvoker.cs" />
+ <Compile Include="ControllerBuilder.cs" />
+ <Compile Include="ControllerTypeCache.cs" />
+ <Compile Include="ContentResult.cs" />
+ <Compile Include="FilterAttribute.cs" />
+ <Compile Include="IResultFilter.cs" />
+ <Compile Include="IExceptionFilter.cs" />
+ <Compile Include="IAuthorizationFilter.cs" />
+ <Compile Include="JsonResult.cs" />
+ <Compile Include="NameValueCollectionExtensions.cs" />
+ <Compile Include="ViewDataDictionary`1.cs" />
+ <Compile Include="EmptyResult.cs" />
+ <Compile Include="MultiSelectList.cs" />
+ <Compile Include="RedirectResult.cs" />
+ <Compile Include="RedirectToRouteResult.cs" />
+ <Compile Include="DefaultControllerFactory.cs" />
+ <Compile Include="HtmlHelper.cs" />
+ <Compile Include="IActionFilter.cs" />
+ <Compile Include="IBuildManager.cs" />
+ <Compile Include="IController.cs" />
+ <Compile Include="IControllerFactory.cs" />
+ <Compile Include="IViewDataContainer.cs" />
+ <Compile Include="IViewEngine.cs" />
+ <Compile Include="MvcHandler.cs" />
+ <Compile Include="MvcRouteHandler.cs" />
+ <Compile Include="NonActionAttribute.cs" />
+ <Compile Include="RouteCollectionExtensions.cs" />
+ <Compile Include="SelectList.cs" />
+ <Compile Include="TempDataDictionary.cs" />
+ <Compile Include="TypeHelpers.cs" />
+ <Compile Include="UrlHelper.cs" />
+ <Compile Include="ViewDataDictionary.cs" />
+ <Compile Include="ViewEngineCollection.cs" />
+ <Compile Include="ViewEngineResult.cs" />
+ <Compile Include="ViewEngines.cs" />
+ <Compile Include="ViewMasterPage.cs">
+ <SubType>ASPXCodeBehind</SubType>
+ </Compile>
+ <Compile Include="ViewMasterPage`1.cs">
+ <SubType>ASPXCodeBehind</SubType>
+ </Compile>
+ <Compile Include="ViewPage.cs">
+ <SubType>ASPXCodeBehind</SubType>
+ </Compile>
+ <Compile Include="ViewPage`1.cs">
+ <SubType>ASPXCodeBehind</SubType>
+ </Compile>
+ <Compile Include="ViewResult.cs" />
+ <Compile Include="ViewResultBase.cs" />
+ <Compile Include="ViewUserControl.cs">
+ <SubType>ASPXCodeBehind</SubType>
+ </Compile>
+ <Compile Include="ViewUserControl`1.cs">
+ <SubType>ASPXCodeBehind</SubType>
+ </Compile>
+ <Compile Include="VirtualPathProviderViewEngine.cs" />
+ <Compile Include="WebFormView.cs" />
+ <Compile Include="WebFormViewEngine.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="Properties\MvcResources.Designer.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>MvcResources.resx</DependentUpon>
+ </Compile>
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="Properties\MvcResources.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>MvcResources.Designer.cs</LastGenOutput>
+ <SubType>Designer</SubType>
+ </EmbeddedResource>
+ </ItemGroup>
+ <ItemGroup>
+ <CodeAnalysisDictionary Include="..\CodeAnalysisDictionary.xml" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\System.Web.Razor\System.Web.Razor.csproj">
+ <Project>{8F18041B-9410-4C36-A9C5-067813DF5F31}</Project>
+ <Name>System.Web.Razor</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\System.Web.WebPages.Razor\System.Web.WebPages.Razor.csproj">
+ <Project>{0939B11A-FE4E-4BA1-8AD6-D97741EE314F}</Project>
+ <Name>System.Web.WebPages.Razor</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\System.Web.WebPages\System.Web.WebPages.csproj">
+ <Project>{76EFA9C5-8D7E-4FDF-B710-E20F8B6B00D2}</Project>
+ <Name>System.Web.WebPages</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="packages.config" />
+ </ItemGroup>
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+</Project> \ No newline at end of file
diff --git a/src/System.Web.Mvc/TagBuilderExtensions.cs b/src/System.Web.Mvc/TagBuilderExtensions.cs
new file mode 100644
index 00000000..cd458f59
--- /dev/null
+++ b/src/System.Web.Mvc/TagBuilderExtensions.cs
@@ -0,0 +1,13 @@
+using System.Diagnostics;
+
+namespace System.Web.Mvc
+{
+ internal static class TagBuilderExtensions
+ {
+ internal static MvcHtmlString ToMvcHtmlString(this TagBuilder tagBuilder, TagRenderMode renderMode)
+ {
+ Debug.Assert(tagBuilder != null);
+ return new MvcHtmlString(tagBuilder.ToString(renderMode));
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/TempDataDictionary.cs b/src/System.Web.Mvc/TempDataDictionary.cs
new file mode 100644
index 00000000..76bb4b2c
--- /dev/null
+++ b/src/System.Web.Mvc/TempDataDictionary.cs
@@ -0,0 +1,208 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace System.Web.Mvc
+{
+ public class TempDataDictionary : IDictionary<string, object>
+ {
+ internal const string TempDataSerializationKey = "__tempData";
+
+ private Dictionary<string, object> _data;
+ private HashSet<string> _initialKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+ private HashSet<string> _retainedKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+
+ public TempDataDictionary()
+ {
+ _data = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
+ }
+
+ public int Count
+ {
+ get { return _data.Count; }
+ }
+
+ public ICollection<string> Keys
+ {
+ get { return _data.Keys; }
+ }
+
+ public ICollection<object> Values
+ {
+ get { return _data.Values; }
+ }
+
+ bool ICollection<KeyValuePair<string, object>>.IsReadOnly
+ {
+ get { return ((ICollection<KeyValuePair<string, object>>)_data).IsReadOnly; }
+ }
+
+ public object this[string key]
+ {
+ get
+ {
+ object value;
+ if (TryGetValue(key, out value))
+ {
+ _initialKeys.Remove(key);
+ return value;
+ }
+ return null;
+ }
+ set
+ {
+ _data[key] = value;
+ _initialKeys.Add(key);
+ }
+ }
+
+ public void Keep()
+ {
+ _retainedKeys.Clear();
+ _retainedKeys.UnionWith(_data.Keys);
+ }
+
+ public void Keep(string key)
+ {
+ _retainedKeys.Add(key);
+ }
+
+ public void Load(ControllerContext controllerContext, ITempDataProvider tempDataProvider)
+ {
+ IDictionary<string, object> providerDictionary = tempDataProvider.LoadTempData(controllerContext);
+ _data = (providerDictionary != null)
+ ? new Dictionary<string, object>(providerDictionary, StringComparer.OrdinalIgnoreCase)
+ : new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
+ _initialKeys = new HashSet<string>(_data.Keys, StringComparer.OrdinalIgnoreCase);
+ _retainedKeys.Clear();
+ }
+
+ public object Peek(string key)
+ {
+ object value;
+ _data.TryGetValue(key, out value);
+ return value;
+ }
+
+ public void Save(ControllerContext controllerContext, ITempDataProvider tempDataProvider)
+ {
+ string[] keysToKeep = _initialKeys.Union(_retainedKeys, StringComparer.OrdinalIgnoreCase).ToArray();
+ string[] keysToRemove = _data.Keys.Except(keysToKeep, StringComparer.OrdinalIgnoreCase).ToArray();
+ foreach (string key in keysToRemove)
+ {
+ _data.Remove(key);
+ }
+ tempDataProvider.SaveTempData(controllerContext, _data);
+ }
+
+ public void Add(string key, object value)
+ {
+ _data.Add(key, value);
+ _initialKeys.Add(key);
+ }
+
+ public void Clear()
+ {
+ _data.Clear();
+ _retainedKeys.Clear();
+ _initialKeys.Clear();
+ }
+
+ public bool ContainsKey(string key)
+ {
+ return _data.ContainsKey(key);
+ }
+
+ public bool ContainsValue(object value)
+ {
+ return _data.ContainsValue(value);
+ }
+
+ public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
+ {
+ return new TempDataDictionaryEnumerator(this);
+ }
+
+ public bool Remove(string key)
+ {
+ _retainedKeys.Remove(key);
+ _initialKeys.Remove(key);
+ return _data.Remove(key);
+ }
+
+ public bool TryGetValue(string key, out object value)
+ {
+ _initialKeys.Remove(key);
+ return _data.TryGetValue(key, out value);
+ }
+
+ void ICollection<KeyValuePair<string, object>>.CopyTo(KeyValuePair<string, object>[] array, int index)
+ {
+ ((ICollection<KeyValuePair<string, object>>)_data).CopyTo(array, index);
+ }
+
+ void ICollection<KeyValuePair<string, object>>.Add(KeyValuePair<string, object> keyValuePair)
+ {
+ _initialKeys.Add(keyValuePair.Key);
+ ((ICollection<KeyValuePair<string, object>>)_data).Add(keyValuePair);
+ }
+
+ bool ICollection<KeyValuePair<string, object>>.Contains(KeyValuePair<string, object> keyValuePair)
+ {
+ return ((ICollection<KeyValuePair<string, object>>)_data).Contains(keyValuePair);
+ }
+
+ bool ICollection<KeyValuePair<string, object>>.Remove(KeyValuePair<string, object> keyValuePair)
+ {
+ _initialKeys.Remove(keyValuePair.Key);
+ return ((ICollection<KeyValuePair<string, object>>)_data).Remove(keyValuePair);
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return new TempDataDictionaryEnumerator(this);
+ }
+
+ private sealed class TempDataDictionaryEnumerator : IEnumerator<KeyValuePair<string, object>>
+ {
+ private IEnumerator<KeyValuePair<string, object>> _enumerator;
+ private TempDataDictionary _tempData;
+
+ public TempDataDictionaryEnumerator(TempDataDictionary tempData)
+ {
+ _tempData = tempData;
+ _enumerator = _tempData._data.GetEnumerator();
+ }
+
+ public KeyValuePair<string, object> Current
+ {
+ get
+ {
+ KeyValuePair<string, object> kvp = _enumerator.Current;
+ _tempData._initialKeys.Remove(kvp.Key);
+ return kvp;
+ }
+ }
+
+ object IEnumerator.Current
+ {
+ get { return Current; }
+ }
+
+ public bool MoveNext()
+ {
+ return _enumerator.MoveNext();
+ }
+
+ public void Reset()
+ {
+ _enumerator.Reset();
+ }
+
+ void IDisposable.Dispose()
+ {
+ _enumerator.Dispose();
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/TemplateInfo.cs b/src/System.Web.Mvc/TemplateInfo.cs
new file mode 100644
index 00000000..0f05f1ca
--- /dev/null
+++ b/src/System.Web.Mvc/TemplateInfo.cs
@@ -0,0 +1,58 @@
+using System.Collections.Generic;
+
+namespace System.Web.Mvc
+{
+ public class TemplateInfo
+ {
+ private string _htmlFieldPrefix;
+ private object _formattedModelValue;
+ private HashSet<object> _visitedObjects;
+
+ public object FormattedModelValue
+ {
+ get { return _formattedModelValue ?? String.Empty; }
+ set { _formattedModelValue = value; }
+ }
+
+ public string HtmlFieldPrefix
+ {
+ get { return _htmlFieldPrefix ?? String.Empty; }
+ set { _htmlFieldPrefix = value; }
+ }
+
+ public int TemplateDepth
+ {
+ get { return VisitedObjects.Count; }
+ }
+
+ // DDB #224750 - Keep a collection of visited objects to prevent infinite recursion
+ internal HashSet<object> VisitedObjects
+ {
+ get
+ {
+ if (_visitedObjects == null)
+ {
+ _visitedObjects = new HashSet<object>();
+ }
+ return _visitedObjects;
+ }
+ set { _visitedObjects = value; }
+ }
+
+ public string GetFullHtmlFieldId(string partialFieldName)
+ {
+ return HtmlHelper.GenerateIdFromName(GetFullHtmlFieldName(partialFieldName));
+ }
+
+ public string GetFullHtmlFieldName(string partialFieldName)
+ {
+ // This uses "combine and trim" because either or both of these values might be empty
+ return (HtmlFieldPrefix + "." + (partialFieldName ?? String.Empty)).Trim('.');
+ }
+
+ public bool Visited(ModelMetadata metadata)
+ {
+ return VisitedObjects.Contains(metadata.Model ?? metadata.ModelType);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/TryGetValueDelegate.cs b/src/System.Web.Mvc/TryGetValueDelegate.cs
new file mode 100644
index 00000000..2092b9e0
--- /dev/null
+++ b/src/System.Web.Mvc/TryGetValueDelegate.cs
@@ -0,0 +1,4 @@
+namespace System.Web.Mvc
+{
+ internal delegate bool TryGetValueDelegate(object dictionary, string key, out object value);
+}
diff --git a/src/System.Web.Mvc/TypeCacheSerializer.cs b/src/System.Web.Mvc/TypeCacheSerializer.cs
new file mode 100644
index 00000000..9067f0d8
--- /dev/null
+++ b/src/System.Web.Mvc/TypeCacheSerializer.cs
@@ -0,0 +1,122 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Web.Mvc.Properties;
+using System.Xml;
+
+namespace System.Web.Mvc
+{
+ // Processes files with this format:
+ //
+ // <typeCache lastModified=... mvcVersionId=...>
+ // <assembly name=...>
+ // <module versionId=...>
+ // <type>...</type>
+ // </module>
+ // </assembly>
+ // </typeCache>
+ //
+ // This is used to store caches of files between AppDomain resets, leading to improved cold boot time
+ // and more efficient use of memory.
+
+ internal sealed class TypeCacheSerializer
+ {
+ private static readonly Guid _mvcVersionId = typeof(TypeCacheSerializer).Module.ModuleVersionId;
+
+ // used for unit testing
+
+ private DateTime CurrentDate
+ {
+ get { return CurrentDateOverride ?? DateTime.Now; }
+ }
+
+ internal DateTime? CurrentDateOverride { get; set; }
+
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This is an instance method for consistency with the SerializeTypes() method.")]
+ public List<Type> DeserializeTypes(TextReader input)
+ {
+ XmlDocument doc = new XmlDocument();
+ doc.Load(input);
+ XmlElement rootElement = doc.DocumentElement;
+
+ Guid readMvcVersionId = new Guid(rootElement.Attributes["mvcVersionId"].Value);
+ if (readMvcVersionId != _mvcVersionId)
+ {
+ // The cache is outdated because the cache file was produced by a different version
+ // of MVC.
+ return null;
+ }
+
+ List<Type> deserializedTypes = new List<Type>();
+ foreach (XmlNode assemblyNode in rootElement.ChildNodes)
+ {
+ string assemblyName = assemblyNode.Attributes["name"].Value;
+ Assembly assembly = Assembly.Load(assemblyName);
+
+ foreach (XmlNode moduleNode in assemblyNode.ChildNodes)
+ {
+ Guid moduleVersionId = new Guid(moduleNode.Attributes["versionId"].Value);
+
+ foreach (XmlNode typeNode in moduleNode.ChildNodes)
+ {
+ string typeName = typeNode.InnerText;
+ Type type = assembly.GetType(typeName);
+ if (type == null || type.Module.ModuleVersionId != moduleVersionId)
+ {
+ // The cache is outdated because we couldn't find a previously recorded
+ // type or the type's containing module was modified.
+ return null;
+ }
+ else
+ {
+ deserializedTypes.Add(type);
+ }
+ }
+ }
+ }
+
+ return deserializedTypes;
+ }
+
+ public void SerializeTypes(IEnumerable<Type> types, TextWriter output)
+ {
+ var groupedByAssembly = from type in types
+ group type by type.Module
+ into groupedByModule
+ group groupedByModule by groupedByModule.Key.Assembly;
+
+ XmlDocument doc = new XmlDocument();
+ doc.AppendChild(doc.CreateComment(MvcResources.TypeCache_DoNotModify));
+
+ XmlElement typeCacheElement = doc.CreateElement("typeCache");
+ doc.AppendChild(typeCacheElement);
+ typeCacheElement.SetAttribute("lastModified", CurrentDate.ToString());
+ typeCacheElement.SetAttribute("mvcVersionId", _mvcVersionId.ToString());
+
+ foreach (var assemblyGroup in groupedByAssembly)
+ {
+ XmlElement assemblyElement = doc.CreateElement("assembly");
+ typeCacheElement.AppendChild(assemblyElement);
+ assemblyElement.SetAttribute("name", assemblyGroup.Key.FullName);
+
+ foreach (var moduleGroup in assemblyGroup)
+ {
+ XmlElement moduleElement = doc.CreateElement("module");
+ assemblyElement.AppendChild(moduleElement);
+ moduleElement.SetAttribute("versionId", moduleGroup.Key.ModuleVersionId.ToString());
+
+ foreach (Type type in moduleGroup)
+ {
+ XmlElement typeElement = doc.CreateElement("type");
+ moduleElement.AppendChild(typeElement);
+ typeElement.AppendChild(doc.CreateTextNode(type.FullName));
+ }
+ }
+ }
+
+ doc.Save(output);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/TypeCacheUtil.cs b/src/System.Web.Mvc/TypeCacheUtil.cs
new file mode 100644
index 00000000..2f9fed55
--- /dev/null
+++ b/src/System.Web.Mvc/TypeCacheUtil.cs
@@ -0,0 +1,104 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+
+namespace System.Web.Mvc
+{
+ internal static class TypeCacheUtil
+ {
+ private static IEnumerable<Type> FilterTypesInAssemblies(IBuildManager buildManager, Predicate<Type> predicate)
+ {
+ // Go through all assemblies referenced by the application and search for types matching a predicate
+ IEnumerable<Type> typesSoFar = Type.EmptyTypes;
+
+ ICollection assemblies = buildManager.GetReferencedAssemblies();
+ foreach (Assembly assembly in assemblies)
+ {
+ Type[] typesInAsm;
+ try
+ {
+ typesInAsm = assembly.GetTypes();
+ }
+ catch (ReflectionTypeLoadException ex)
+ {
+ typesInAsm = ex.Types;
+ }
+ typesSoFar = typesSoFar.Concat(typesInAsm);
+ }
+ return typesSoFar.Where(type => TypeIsPublicClass(type) && predicate(type));
+ }
+
+ public static List<Type> GetFilteredTypesFromAssemblies(string cacheName, Predicate<Type> predicate, IBuildManager buildManager)
+ {
+ TypeCacheSerializer serializer = new TypeCacheSerializer();
+
+ // first, try reading from the cache on disk
+ List<Type> matchingTypes = ReadTypesFromCache(cacheName, predicate, buildManager, serializer);
+ if (matchingTypes != null)
+ {
+ return matchingTypes;
+ }
+
+ // if reading from the cache failed, enumerate over every assembly looking for a matching type
+ matchingTypes = FilterTypesInAssemblies(buildManager, predicate).ToList();
+
+ // finally, save the cache back to disk
+ SaveTypesToCache(cacheName, matchingTypes, buildManager, serializer);
+
+ return matchingTypes;
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Cache failures are not fatal, and the code should continue executing normally.")]
+ internal static List<Type> ReadTypesFromCache(string cacheName, Predicate<Type> predicate, IBuildManager buildManager, TypeCacheSerializer serializer)
+ {
+ try
+ {
+ Stream stream = buildManager.ReadCachedFile(cacheName);
+ if (stream != null)
+ {
+ using (StreamReader reader = new StreamReader(stream))
+ {
+ List<Type> deserializedTypes = serializer.DeserializeTypes(reader);
+ if (deserializedTypes != null && deserializedTypes.All(type => TypeIsPublicClass(type) && predicate(type)))
+ {
+ // If all read types still match the predicate, success!
+ return deserializedTypes;
+ }
+ }
+ }
+ }
+ catch
+ {
+ }
+
+ return null;
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Cache failures are not fatal, and the code should continue executing normally.")]
+ internal static void SaveTypesToCache(string cacheName, IList<Type> matchingTypes, IBuildManager buildManager, TypeCacheSerializer serializer)
+ {
+ try
+ {
+ Stream stream = buildManager.CreateCachedFile(cacheName);
+ if (stream != null)
+ {
+ using (StreamWriter writer = new StreamWriter(stream))
+ {
+ serializer.SerializeTypes(matchingTypes, writer);
+ }
+ }
+ }
+ catch
+ {
+ }
+ }
+
+ private static bool TypeIsPublicClass(Type type)
+ {
+ return (type != null && type.IsPublic && type.IsClass && !type.IsAbstract);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/TypeDescriptorHelper.cs b/src/System.Web.Mvc/TypeDescriptorHelper.cs
new file mode 100644
index 00000000..48d9bcfb
--- /dev/null
+++ b/src/System.Web.Mvc/TypeDescriptorHelper.cs
@@ -0,0 +1,13 @@
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+
+namespace System.Web.Mvc
+{
+ internal static class TypeDescriptorHelper
+ {
+ public static ICustomTypeDescriptor Get(Type type)
+ {
+ return new AssociatedMetadataTypeTypeDescriptionProvider(type).GetTypeDescriptor(type);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/TypeHelpers.cs b/src/System.Web.Mvc/TypeHelpers.cs
new file mode 100644
index 00000000..5309b4df
--- /dev/null
+++ b/src/System.Web.Mvc/TypeHelpers.cs
@@ -0,0 +1,146 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Threading;
+
+namespace System.Web.Mvc
+{
+ internal static class TypeHelpers
+ {
+ private static readonly Dictionary<Type, TryGetValueDelegate> _tryGetValueDelegateCache = new Dictionary<Type, TryGetValueDelegate>();
+ private static readonly ReaderWriterLockSlim _tryGetValueDelegateCacheLock = new ReaderWriterLockSlim();
+
+ private static readonly MethodInfo _strongTryGetValueImplInfo = typeof(TypeHelpers).GetMethod("StrongTryGetValueImpl", BindingFlags.NonPublic | BindingFlags.Static);
+
+ public static readonly Assembly MsCorLibAssembly = typeof(string).Assembly;
+ public static readonly Assembly MvcAssembly = typeof(Controller).Assembly;
+ public static readonly Assembly SystemWebAssembly = typeof(HttpContext).Assembly;
+
+ // method is used primarily for lighting up new .NET Framework features even if MVC targets the previous version
+ // thisParameter is the 'this' parameter if target method is instance method, should be null for static method
+ public static TDelegate CreateDelegate<TDelegate>(Assembly assembly, string typeName, string methodName, object thisParameter) where TDelegate : class
+ {
+ // ensure target type exists
+ Type targetType = assembly.GetType(typeName, false /* throwOnError */);
+ if (targetType == null)
+ {
+ return null;
+ }
+
+ return CreateDelegate<TDelegate>(targetType, methodName, thisParameter);
+ }
+
+ public static TDelegate CreateDelegate<TDelegate>(Type targetType, string methodName, object thisParameter) where TDelegate : class
+ {
+ // ensure target method exists
+ ParameterInfo[] delegateParameters = typeof(TDelegate).GetMethod("Invoke").GetParameters();
+ Type[] argumentTypes = Array.ConvertAll(delegateParameters, pInfo => pInfo.ParameterType);
+ MethodInfo targetMethod = targetType.GetMethod(methodName, argumentTypes);
+ if (targetMethod == null)
+ {
+ return null;
+ }
+
+ TDelegate d = Delegate.CreateDelegate(typeof(TDelegate), thisParameter, targetMethod, false /* throwOnBindFailure */) as TDelegate;
+ return d;
+ }
+
+ public static TryGetValueDelegate CreateTryGetValueDelegate(Type targetType)
+ {
+ TryGetValueDelegate result;
+
+ _tryGetValueDelegateCacheLock.EnterReadLock();
+ try
+ {
+ if (_tryGetValueDelegateCache.TryGetValue(targetType, out result))
+ {
+ return result;
+ }
+ }
+ finally
+ {
+ _tryGetValueDelegateCacheLock.ExitReadLock();
+ }
+
+ Type dictionaryType = ExtractGenericInterface(targetType, typeof(IDictionary<,>));
+
+ // just wrap a call to the underlying IDictionary<TKey, TValue>.TryGetValue() where string can be cast to TKey
+ if (dictionaryType != null)
+ {
+ Type[] typeArguments = dictionaryType.GetGenericArguments();
+ Type keyType = typeArguments[0];
+ Type returnType = typeArguments[1];
+
+ if (keyType.IsAssignableFrom(typeof(string)))
+ {
+ MethodInfo strongImplInfo = _strongTryGetValueImplInfo.MakeGenericMethod(keyType, returnType);
+ result = (TryGetValueDelegate)Delegate.CreateDelegate(typeof(TryGetValueDelegate), strongImplInfo);
+ }
+ }
+
+ // wrap a call to the underlying IDictionary.Item()
+ if (result == null && typeof(IDictionary).IsAssignableFrom(targetType))
+ {
+ result = TryGetValueFromNonGenericDictionary;
+ }
+
+ _tryGetValueDelegateCacheLock.EnterWriteLock();
+ try
+ {
+ _tryGetValueDelegateCache[targetType] = result;
+ }
+ finally
+ {
+ _tryGetValueDelegateCacheLock.ExitWriteLock();
+ }
+
+ return result;
+ }
+
+ public static Type ExtractGenericInterface(Type queryType, Type interfaceType)
+ {
+ Func<Type, bool> matchesInterface = t => t.IsGenericType && t.GetGenericTypeDefinition() == interfaceType;
+ return (matchesInterface(queryType)) ? queryType : queryType.GetInterfaces().FirstOrDefault(matchesInterface);
+ }
+
+ public static object GetDefaultValue(Type type)
+ {
+ return (TypeAllowsNullValue(type)) ? null : Activator.CreateInstance(type);
+ }
+
+ public static bool IsCompatibleObject<T>(object value)
+ {
+ return (value is T || (value == null && TypeAllowsNullValue(typeof(T))));
+ }
+
+ public static bool IsNullableValueType(Type type)
+ {
+ return Nullable.GetUnderlyingType(type) != null;
+ }
+
+ private static bool StrongTryGetValueImpl<TKey, TValue>(object dictionary, string key, out object value)
+ {
+ IDictionary<TKey, TValue> strongDict = (IDictionary<TKey, TValue>)dictionary;
+
+ TValue strongValue;
+ bool retVal = strongDict.TryGetValue((TKey)(object)key, out strongValue);
+ value = strongValue;
+ return retVal;
+ }
+
+ private static bool TryGetValueFromNonGenericDictionary(object dictionary, string key, out object value)
+ {
+ IDictionary weakDict = (IDictionary)dictionary;
+
+ bool containsKey = weakDict.Contains(key);
+ value = (containsKey) ? weakDict[key] : null;
+ return containsKey;
+ }
+
+ public static bool TypeAllowsNullValue(Type type)
+ {
+ return (!type.IsValueType || IsNullableValueType(type));
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/UnvalidatedRequestValuesAccessor.cs b/src/System.Web.Mvc/UnvalidatedRequestValuesAccessor.cs
new file mode 100644
index 00000000..0c4f775e
--- /dev/null
+++ b/src/System.Web.Mvc/UnvalidatedRequestValuesAccessor.cs
@@ -0,0 +1,4 @@
+namespace System.Web.Mvc
+{
+ internal delegate IUnvalidatedRequestValues UnvalidatedRequestValuesAccessor(ControllerContext controllerContext);
+}
diff --git a/src/System.Web.Mvc/UnvalidatedRequestValuesWrapper.cs b/src/System.Web.Mvc/UnvalidatedRequestValuesWrapper.cs
new file mode 100644
index 00000000..260093a7
--- /dev/null
+++ b/src/System.Web.Mvc/UnvalidatedRequestValuesWrapper.cs
@@ -0,0 +1,32 @@
+using System.Collections.Specialized;
+using System.Web.Helpers;
+
+namespace System.Web.Mvc
+{
+ // Concrete implementation for the IUnvalidatedRequestValues helper interface
+
+ internal sealed class UnvalidatedRequestValuesWrapper : IUnvalidatedRequestValues
+ {
+ private readonly UnvalidatedRequestValues _unvalidatedValues;
+
+ public UnvalidatedRequestValuesWrapper(UnvalidatedRequestValues unvalidatedValues)
+ {
+ _unvalidatedValues = unvalidatedValues;
+ }
+
+ public NameValueCollection Form
+ {
+ get { return _unvalidatedValues.Form; }
+ }
+
+ public NameValueCollection QueryString
+ {
+ get { return _unvalidatedValues.QueryString; }
+ }
+
+ public string this[string key]
+ {
+ get { return _unvalidatedValues[key]; }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/UrlHelper.cs b/src/System.Web.Mvc/UrlHelper.cs
new file mode 100644
index 00000000..804879f3
--- /dev/null
+++ b/src/System.Web.Mvc/UrlHelper.cs
@@ -0,0 +1,222 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Web.Mvc.Properties;
+using System.Web.Routing;
+using System.Web.WebPages;
+
+namespace System.Web.Mvc
+{
+ public class UrlHelper
+ {
+ public UrlHelper(RequestContext requestContext)
+ : this(requestContext, RouteTable.Routes)
+ {
+ }
+
+ public UrlHelper(RequestContext requestContext, RouteCollection routeCollection)
+ {
+ if (requestContext == null)
+ {
+ throw new ArgumentNullException("requestContext");
+ }
+ if (routeCollection == null)
+ {
+ throw new ArgumentNullException("routeCollection");
+ }
+ RequestContext = requestContext;
+ RouteCollection = routeCollection;
+ }
+
+ public RequestContext RequestContext { get; private set; }
+
+ public RouteCollection RouteCollection { get; private set; }
+
+ public string Action(string actionName)
+ {
+ return GenerateUrl(null /* routeName */, actionName, null, (RouteValueDictionary)null /* routeValues */);
+ }
+
+ public string Action(string actionName, object routeValues)
+ {
+ return GenerateUrl(null /* routeName */, actionName, null /* controllerName */, new RouteValueDictionary(routeValues));
+ }
+
+ public string Action(string actionName, RouteValueDictionary routeValues)
+ {
+ return GenerateUrl(null /* routeName */, actionName, null /* controllerName */, routeValues);
+ }
+
+ public string Action(string actionName, string controllerName)
+ {
+ return GenerateUrl(null /* routeName */, actionName, controllerName, (RouteValueDictionary)null /* routeValues */);
+ }
+
+ public string Action(string actionName, string controllerName, object routeValues)
+ {
+ return GenerateUrl(null /* routeName */, actionName, controllerName, new RouteValueDictionary(routeValues));
+ }
+
+ public string Action(string actionName, string controllerName, RouteValueDictionary routeValues)
+ {
+ return GenerateUrl(null /* routeName */, actionName, controllerName, routeValues);
+ }
+
+ public string Action(string actionName, string controllerName, object routeValues, string protocol)
+ {
+ return GenerateUrl(null /* routeName */, actionName, controllerName, protocol, null /* hostName */, null /* fragment */, new RouteValueDictionary(routeValues), RouteCollection, RequestContext, true /* includeImplicitMvcValues */);
+ }
+
+ public string Action(string actionName, string controllerName, RouteValueDictionary routeValues, string protocol, string hostName)
+ {
+ return GenerateUrl(null /* routeName */, actionName, controllerName, protocol, hostName, null /* fragment */, routeValues, RouteCollection, RequestContext, true /* includeImplicitMvcValues */);
+ }
+
+ public string Content(string contentPath)
+ {
+ return GenerateContentUrl(contentPath, RequestContext.HttpContext);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings", Justification = "As the return value will used only for rendering, string return value is more appropriate.")]
+ public static string GenerateContentUrl(string contentPath, HttpContextBase httpContext)
+ {
+ if (String.IsNullOrEmpty(contentPath))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "contentPath");
+ }
+
+ if (httpContext == null)
+ {
+ throw new ArgumentNullException("httpContext");
+ }
+
+ if (contentPath[0] == '~')
+ {
+ return PathHelpers.GenerateClientUrl(httpContext, contentPath);
+ }
+ else
+ {
+ return contentPath;
+ }
+ }
+
+ //REVIEW: Should we have an overload that takes Uri?
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", Justification = "Needs to take same parameters as HttpUtility.UrlEncode()")]
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "For consistency, all helpers are instance methods.")]
+ public string Encode(string url)
+ {
+ return HttpUtility.UrlEncode(url);
+ }
+
+ private string GenerateUrl(string routeName, string actionName, string controllerName, RouteValueDictionary routeValues)
+ {
+ return GenerateUrl(routeName, actionName, controllerName, routeValues, RouteCollection, RequestContext, true /* includeImplicitMvcValues */);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings", Justification = "As the return value will used only for rendering, string return value is more appropriate.")]
+ public static string GenerateUrl(string routeName, string actionName, string controllerName, string protocol, string hostName, string fragment, RouteValueDictionary routeValues, RouteCollection routeCollection, RequestContext requestContext, bool includeImplicitMvcValues)
+ {
+ string url = GenerateUrl(routeName, actionName, controllerName, routeValues, routeCollection, requestContext, includeImplicitMvcValues);
+
+ if (url != null)
+ {
+ if (!String.IsNullOrEmpty(fragment))
+ {
+ url = url + "#" + fragment;
+ }
+
+ if (!String.IsNullOrEmpty(protocol) || !String.IsNullOrEmpty(hostName))
+ {
+ Uri requestUrl = requestContext.HttpContext.Request.Url;
+ protocol = (!String.IsNullOrEmpty(protocol)) ? protocol : Uri.UriSchemeHttp;
+ hostName = (!String.IsNullOrEmpty(hostName)) ? hostName : requestUrl.Host;
+
+ string port = String.Empty;
+ string requestProtocol = requestUrl.Scheme;
+
+ if (String.Equals(protocol, requestProtocol, StringComparison.OrdinalIgnoreCase))
+ {
+ port = requestUrl.IsDefaultPort ? String.Empty : (":" + Convert.ToString(requestUrl.Port, CultureInfo.InvariantCulture));
+ }
+
+ url = protocol + Uri.SchemeDelimiter + hostName + port + url;
+ }
+ }
+
+ return url;
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings", Justification = "As the return value will used only for rendering, string return value is more appropriate.")]
+ public static string GenerateUrl(string routeName, string actionName, string controllerName, RouteValueDictionary routeValues, RouteCollection routeCollection, RequestContext requestContext, bool includeImplicitMvcValues)
+ {
+ if (routeCollection == null)
+ {
+ throw new ArgumentNullException("routeCollection");
+ }
+
+ if (requestContext == null)
+ {
+ throw new ArgumentNullException("requestContext");
+ }
+
+ RouteValueDictionary mergedRouteValues = RouteValuesHelpers.MergeRouteValues(actionName, controllerName, requestContext.RouteData.Values, routeValues, includeImplicitMvcValues);
+
+ VirtualPathData vpd = routeCollection.GetVirtualPathForArea(requestContext, routeName, mergedRouteValues);
+ if (vpd == null)
+ {
+ return null;
+ }
+
+ string modifiedUrl = PathHelpers.GenerateClientUrl(requestContext.HttpContext, vpd.VirtualPath);
+ return modifiedUrl;
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "0#", Justification = "Response.Redirect() takes its URI as a string parameter.")]
+ public bool IsLocalUrl(string url)
+ {
+ // TODO this should call the System.Web.dll API once it gets added to the framework and MVC takes a dependency on it.
+ return RequestExtensions.IsUrlLocalToHost(RequestContext.HttpContext.Request, url);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings", Justification = "As the return value will used only for rendering, string return value is more appropriate.")]
+ public string RouteUrl(object routeValues)
+ {
+ return RouteUrl(null /* routeName */, routeValues);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings", Justification = "As the return value will used only for rendering, string return value is more appropriate.")]
+ public string RouteUrl(RouteValueDictionary routeValues)
+ {
+ return RouteUrl(null /* routeName */, routeValues);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings", Justification = "As the return value will used only for rendering, string return value is more appropriate.")]
+ public string RouteUrl(string routeName)
+ {
+ return RouteUrl(routeName, (object)null /* routeValues */);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings", Justification = "As the return value will used only for rendering, string return value is more appropriate.")]
+ public string RouteUrl(string routeName, object routeValues)
+ {
+ return RouteUrl(routeName, routeValues, null /* protocol */);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings", Justification = "As the return value will used only for rendering, string return value is more appropriate.")]
+ public string RouteUrl(string routeName, RouteValueDictionary routeValues)
+ {
+ return RouteUrl(routeName, routeValues, null /* protocol */, null /* hostName */);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings", Justification = "As the return value will used only for rendering, string return value is more appropriate.")]
+ public string RouteUrl(string routeName, object routeValues, string protocol)
+ {
+ return GenerateUrl(routeName, null /* actionName */, null /* controllerName */, protocol, null /* hostName */, null /* fragment */, new RouteValueDictionary(routeValues), RouteCollection, RequestContext, false /* includeImplicitMvcValues */);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings", Justification = "As the return value will used only for rendering, string return value is more appropriate.")]
+ public string RouteUrl(string routeName, RouteValueDictionary routeValues, string protocol, string hostName)
+ {
+ return GenerateUrl(routeName, null /* actionName */, null /* controllerName */, protocol, hostName, null /* fragment */, routeValues, RouteCollection, RequestContext, false /* includeImplicitMvcValues */);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/UrlParameter.cs b/src/System.Web.Mvc/UrlParameter.cs
new file mode 100644
index 00000000..0b567667
--- /dev/null
+++ b/src/System.Web.Mvc/UrlParameter.cs
@@ -0,0 +1,20 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Web.Mvc
+{
+ public sealed class UrlParameter
+ {
+ [SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", Justification = "This type is immutable.")]
+ public static readonly UrlParameter Optional = new UrlParameter();
+
+ // singleton constructor
+ private UrlParameter()
+ {
+ }
+
+ public override string ToString()
+ {
+ return String.Empty;
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/UrlRewriterHelper.cs b/src/System.Web.Mvc/UrlRewriterHelper.cs
new file mode 100644
index 00000000..265f7202
--- /dev/null
+++ b/src/System.Web.Mvc/UrlRewriterHelper.cs
@@ -0,0 +1,45 @@
+using System.Collections.Specialized;
+
+namespace System.Web.Mvc
+{
+ internal class UrlRewriterHelper
+ {
+ private const string UrlWasRewrittenServerVar = "IIS_WasUrlRewritten";
+ private const string UrlRewriterEnabledServerVar = "IIS_UrlRewriteModule";
+
+ private object _lockObject = new object();
+ private bool _urlRewriterIsTurnedOnValue;
+ private bool _urlRewriterIsTurnedOnCalculated = false;
+
+ private static bool WasThisRequestRewritten(HttpContextBase httpContext)
+ {
+ NameValueCollection serverVars = httpContext.Request.ServerVariables;
+ bool requestWasRewritten = (serverVars != null && serverVars[UrlWasRewrittenServerVar] != null);
+ return requestWasRewritten;
+ }
+
+ private bool IsUrlRewriterTurnedOn(HttpContextBase httpContext)
+ {
+ // Need to do double-check locking because a single instance of this class is shared in the entire app domain (see PathHelpers)
+ if (!_urlRewriterIsTurnedOnCalculated)
+ {
+ lock (_lockObject)
+ {
+ if (!_urlRewriterIsTurnedOnCalculated)
+ {
+ NameValueCollection serverVars = httpContext.Request.ServerVariables;
+ bool urlRewriterIsEnabled = (serverVars != null && serverVars[UrlRewriterEnabledServerVar] != null);
+ _urlRewriterIsTurnedOnValue = urlRewriterIsEnabled;
+ _urlRewriterIsTurnedOnCalculated = true;
+ }
+ }
+ }
+ return _urlRewriterIsTurnedOnValue;
+ }
+
+ public virtual bool WasRequestRewritten(HttpContextBase httpContext)
+ {
+ return IsUrlRewriterTurnedOn(httpContext) && WasThisRequestRewritten(httpContext);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ValidatableObjectAdapter.cs b/src/System.Web.Mvc/ValidatableObjectAdapter.cs
new file mode 100644
index 00000000..88c0137e
--- /dev/null
+++ b/src/System.Web.Mvc/ValidatableObjectAdapter.cs
@@ -0,0 +1,63 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Globalization;
+using System.Linq;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc
+{
+ public class ValidatableObjectAdapter : ModelValidator
+ {
+ public ValidatableObjectAdapter(ModelMetadata metadata, ControllerContext context)
+ : base(metadata, context)
+ {
+ }
+
+ public override IEnumerable<ModelValidationResult> Validate(object container)
+ {
+ // NOTE: Container is never used here, because IValidatableObject doesn't give you
+ // any way to get access to your container.
+
+ object model = Metadata.Model;
+ if (model == null)
+ {
+ return Enumerable.Empty<ModelValidationResult>();
+ }
+
+ IValidatableObject validatable = model as IValidatableObject;
+ if (validatable == null)
+ {
+ throw new InvalidOperationException(
+ String.Format(
+ CultureInfo.CurrentCulture,
+ MvcResources.ValidatableObjectAdapter_IncompatibleType,
+ typeof(IValidatableObject).FullName,
+ model.GetType().FullName));
+ }
+
+ ValidationContext validationContext = new ValidationContext(validatable, null, null);
+ return ConvertResults(validatable.Validate(validationContext));
+ }
+
+ private IEnumerable<ModelValidationResult> ConvertResults(IEnumerable<ValidationResult> results)
+ {
+ foreach (ValidationResult result in results)
+ {
+ if (result != ValidationResult.Success)
+ {
+ if (result.MemberNames == null || !result.MemberNames.Any())
+ {
+ yield return new ModelValidationResult { Message = result.ErrorMessage };
+ }
+ else
+ {
+ foreach (string memberName in result.MemberNames)
+ {
+ yield return new ModelValidationResult { Message = result.ErrorMessage, MemberName = memberName };
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ValidateAntiForgeryTokenAttribute.cs b/src/System.Web.Mvc/ValidateAntiForgeryTokenAttribute.cs
new file mode 100644
index 00000000..b948a0b0
--- /dev/null
+++ b/src/System.Web.Mvc/ValidateAntiForgeryTokenAttribute.cs
@@ -0,0 +1,40 @@
+using System.Diagnostics;
+using System.Web.Helpers;
+
+namespace System.Web.Mvc
+{
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+ public sealed class ValidateAntiForgeryTokenAttribute : FilterAttribute, IAuthorizationFilter
+ {
+ private string _salt;
+
+ public ValidateAntiForgeryTokenAttribute()
+ : this(AntiForgery.Validate)
+ {
+ }
+
+ internal ValidateAntiForgeryTokenAttribute(Action<HttpContextBase, string> validateAction)
+ {
+ Debug.Assert(validateAction != null);
+ ValidateAction = validateAction;
+ }
+
+ public string Salt
+ {
+ get { return _salt ?? String.Empty; }
+ set { _salt = value; }
+ }
+
+ internal Action<HttpContextBase, string> ValidateAction { get; private set; }
+
+ public void OnAuthorization(AuthorizationContext filterContext)
+ {
+ if (filterContext == null)
+ {
+ throw new ArgumentNullException("filterContext");
+ }
+
+ ValidateAction(filterContext.HttpContext, Salt);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ValidateInputAttribute.cs b/src/System.Web.Mvc/ValidateInputAttribute.cs
new file mode 100644
index 00000000..67d43afe
--- /dev/null
+++ b/src/System.Web.Mvc/ValidateInputAttribute.cs
@@ -0,0 +1,26 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Web.Mvc
+{
+ [SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", Justification = "No compelling performance reason to seal this type.")]
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
+ public class ValidateInputAttribute : FilterAttribute, IAuthorizationFilter
+ {
+ public ValidateInputAttribute(bool enableValidation)
+ {
+ EnableValidation = enableValidation;
+ }
+
+ public bool EnableValidation { get; private set; }
+
+ public virtual void OnAuthorization(AuthorizationContext filterContext)
+ {
+ if (filterContext == null)
+ {
+ throw new ArgumentNullException("filterContext");
+ }
+
+ filterContext.Controller.ValidateRequest = EnableValidation;
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ValueProviderCollection.cs b/src/System.Web.Mvc/ValueProviderCollection.cs
new file mode 100644
index 00000000..2cb85e92
--- /dev/null
+++ b/src/System.Web.Mvc/ValueProviderCollection.cs
@@ -0,0 +1,78 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+
+namespace System.Web.Mvc
+{
+ public class ValueProviderCollection : Collection<IValueProvider>, IValueProvider, IUnvalidatedValueProvider, IEnumerableValueProvider
+ {
+ public ValueProviderCollection()
+ {
+ }
+
+ public ValueProviderCollection(IList<IValueProvider> list)
+ : base(list)
+ {
+ }
+
+ public virtual bool ContainsPrefix(string prefix)
+ {
+ return this.Any(vp => vp.ContainsPrefix(prefix));
+ }
+
+ public virtual ValueProviderResult GetValue(string key)
+ {
+ return GetValue(key, skipValidation: false);
+ }
+
+ public virtual ValueProviderResult GetValue(string key, bool skipValidation)
+ {
+ return (from provider in this
+ let result = GetValueFromProvider(provider, key, skipValidation)
+ where result != null
+ select result).FirstOrDefault();
+ }
+
+ public virtual IDictionary<string, string> GetKeysFromPrefix(string prefix)
+ {
+ return (from provider in this
+ let result = GetKeysFromPrefixFromProvider(provider, prefix)
+ where result != null && result.Any()
+ select result).FirstOrDefault() ?? new Dictionary<string, string>();
+ }
+
+ internal static ValueProviderResult GetValueFromProvider(IValueProvider provider, string key, bool skipValidation)
+ {
+ // Since IUnvalidatedValueProvider is a superset of IValueProvider, it's always OK to use the
+ // IUnvalidatedValueProvider-supplied members if they're present. Otherwise just call the
+ // normal IValueProvider members.
+
+ IUnvalidatedValueProvider unvalidatedProvider = provider as IUnvalidatedValueProvider;
+ return (unvalidatedProvider != null) ? unvalidatedProvider.GetValue(key, skipValidation) : provider.GetValue(key);
+ }
+
+ internal static IDictionary<string, string> GetKeysFromPrefixFromProvider(IValueProvider provider, string prefix)
+ {
+ IEnumerableValueProvider enumeratedProvider = provider as IEnumerableValueProvider;
+ return (enumeratedProvider != null) ? enumeratedProvider.GetKeysFromPrefix(prefix) : null;
+ }
+
+ protected override void InsertItem(int index, IValueProvider item)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+ base.InsertItem(index, item);
+ }
+
+ protected override void SetItem(int index, IValueProvider item)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+ base.SetItem(index, item);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ValueProviderDictionary.cs b/src/System.Web.Mvc/ValueProviderDictionary.cs
new file mode 100644
index 00000000..6ea7b6d8
--- /dev/null
+++ b/src/System.Web.Mvc/ValueProviderDictionary.cs
@@ -0,0 +1,217 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Web.Routing;
+
+namespace System.Web.Mvc
+{
+ [Obsolete("The recommended alternative is to use one of the specific ValueProvider types, such as FormValueProvider.")]
+ public class ValueProviderDictionary : IDictionary<string, ValueProviderResult>, IValueProvider
+ {
+ private readonly Dictionary<string, ValueProviderResult> _dictionary = new Dictionary<string, ValueProviderResult>(StringComparer.OrdinalIgnoreCase);
+
+ public ValueProviderDictionary(ControllerContext controllerContext)
+ {
+ ControllerContext = controllerContext;
+ if (controllerContext != null)
+ {
+ PopulateDictionary();
+ }
+ }
+
+ public ControllerContext ControllerContext { get; private set; }
+
+ public int Count
+ {
+ get { return ((ICollection<KeyValuePair<string, ValueProviderResult>>)Dictionary).Count; }
+ }
+
+ internal Dictionary<string, ValueProviderResult> Dictionary
+ {
+ get { return _dictionary; }
+ }
+
+ public bool IsReadOnly
+ {
+ get { return ((ICollection<KeyValuePair<string, ValueProviderResult>>)Dictionary).IsReadOnly; }
+ }
+
+ public ICollection<string> Keys
+ {
+ get { return Dictionary.Keys; }
+ }
+
+ public ValueProviderResult this[string key]
+ {
+ get
+ {
+ ValueProviderResult result;
+ Dictionary.TryGetValue(key, out result);
+ return result;
+ }
+ set { Dictionary[key] = value; }
+ }
+
+ public ICollection<ValueProviderResult> Values
+ {
+ get { return Dictionary.Values; }
+ }
+
+ public void Add(KeyValuePair<string, ValueProviderResult> item)
+ {
+ ((ICollection<KeyValuePair<string, ValueProviderResult>>)Dictionary).Add(item);
+ }
+
+ public void Add(string key, object value)
+ {
+ string attemptedValue = Convert.ToString(value, CultureInfo.InvariantCulture);
+ ValueProviderResult valueProviderResult = new ValueProviderResult(value, attemptedValue, CultureInfo.InvariantCulture);
+ Add(key, valueProviderResult);
+ }
+
+ public void Add(string key, ValueProviderResult value)
+ {
+ Dictionary.Add(key, value);
+ }
+
+ private void AddToDictionaryIfNotPresent(string key, ValueProviderResult result)
+ {
+ if (!String.IsNullOrEmpty(key))
+ {
+ if (!Dictionary.ContainsKey(key))
+ {
+ Dictionary.Add(key, result);
+ }
+ }
+ }
+
+ public void Clear()
+ {
+ ((ICollection<KeyValuePair<string, ValueProviderResult>>)Dictionary).Clear();
+ }
+
+ public bool Contains(KeyValuePair<string, ValueProviderResult> item)
+ {
+ return ((ICollection<KeyValuePair<string, ValueProviderResult>>)Dictionary).Contains(item);
+ }
+
+ public bool ContainsKey(string key)
+ {
+ return Dictionary.ContainsKey(key);
+ }
+
+ public void CopyTo(KeyValuePair<string, ValueProviderResult>[] array, int arrayIndex)
+ {
+ ((ICollection<KeyValuePair<string, ValueProviderResult>>)Dictionary).CopyTo(array, arrayIndex);
+ }
+
+ public IEnumerator<KeyValuePair<string, ValueProviderResult>> GetEnumerator()
+ {
+ return ((IEnumerable<KeyValuePair<string, ValueProviderResult>>)Dictionary).GetEnumerator();
+ }
+
+ private void PopulateDictionary()
+ {
+ CultureInfo currentCulture = CultureInfo.CurrentCulture;
+ CultureInfo invariantCulture = CultureInfo.InvariantCulture;
+
+ // We use this order of precedence to populate the dictionary:
+ // 1. Request form submission (should be culture-aware)
+ // 2. Values from the RouteData (could be from the typed-in URL or from the route's default values)
+ // 3. URI query string
+
+ NameValueCollection form = ControllerContext.HttpContext.Request.Form;
+ if (form != null)
+ {
+ string[] keys = form.AllKeys;
+ foreach (string key in keys)
+ {
+ string[] rawValue = form.GetValues(key);
+ string attemptedValue = form[key];
+ ValueProviderResult result = new ValueProviderResult(rawValue, attemptedValue, currentCulture);
+ AddToDictionaryIfNotPresent(key, result);
+ }
+ }
+
+ RouteValueDictionary routeValues = ControllerContext.RouteData.Values;
+ if (routeValues != null)
+ {
+ foreach (var kvp in routeValues)
+ {
+ string key = kvp.Key;
+ object rawValue = kvp.Value;
+ string attemptedValue = Convert.ToString(rawValue, invariantCulture);
+ ValueProviderResult result = new ValueProviderResult(rawValue, attemptedValue, invariantCulture);
+ AddToDictionaryIfNotPresent(key, result);
+ }
+ }
+
+ NameValueCollection queryString = ControllerContext.HttpContext.Request.QueryString;
+ if (queryString != null)
+ {
+ string[] keys = queryString.AllKeys;
+ foreach (string key in keys)
+ {
+ string[] rawValue = queryString.GetValues(key);
+ string attemptedValue = queryString[key];
+ ValueProviderResult result = new ValueProviderResult(rawValue, attemptedValue, invariantCulture);
+ AddToDictionaryIfNotPresent(key, result);
+ }
+ }
+ }
+
+ public bool Remove(KeyValuePair<string, ValueProviderResult> item)
+ {
+ return ((ICollection<KeyValuePair<string, ValueProviderResult>>)Dictionary).Remove(item);
+ }
+
+ public bool Remove(string key)
+ {
+ return Dictionary.Remove(key);
+ }
+
+ public bool TryGetValue(string key, out ValueProviderResult value)
+ {
+ return Dictionary.TryGetValue(key, out value);
+ }
+
+ #region IEnumerable Members
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return ((IEnumerable)Dictionary).GetEnumerator();
+ }
+
+ #endregion
+
+ #region IValueProvider Members
+
+ [SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Justification = "The declaring type is obsolete, so there is little benefit to exposing this as a virtual method.")]
+ bool IValueProvider.ContainsPrefix(string prefix)
+ {
+ if (prefix == null)
+ {
+ throw new ArgumentNullException("prefix");
+ }
+
+ return ValueProviderUtil.CollectionContainsPrefix(Keys, prefix);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Justification = "The declaring type is obsolete, so there is little benefit to exposing this as a virtual method.")]
+ ValueProviderResult IValueProvider.GetValue(string key)
+ {
+ if (key == null)
+ {
+ throw new ArgumentNullException("key");
+ }
+
+ ValueProviderResult valueProviderResult;
+ TryGetValue(key, out valueProviderResult);
+ return valueProviderResult;
+ }
+
+ #endregion
+ }
+}
diff --git a/src/System.Web.Mvc/ValueProviderFactories.cs b/src/System.Web.Mvc/ValueProviderFactories.cs
new file mode 100644
index 00000000..ee98658a
--- /dev/null
+++ b/src/System.Web.Mvc/ValueProviderFactories.cs
@@ -0,0 +1,20 @@
+namespace System.Web.Mvc
+{
+ public static class ValueProviderFactories
+ {
+ private static readonly ValueProviderFactoryCollection _factories = new ValueProviderFactoryCollection()
+ {
+ new ChildActionValueProviderFactory(),
+ new FormValueProviderFactory(),
+ new JsonValueProviderFactory(),
+ new RouteDataValueProviderFactory(),
+ new QueryStringValueProviderFactory(),
+ new HttpFileCollectionValueProviderFactory(),
+ };
+
+ public static ValueProviderFactoryCollection Factories
+ {
+ get { return _factories; }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ValueProviderFactory.cs b/src/System.Web.Mvc/ValueProviderFactory.cs
new file mode 100644
index 00000000..332b4ae0
--- /dev/null
+++ b/src/System.Web.Mvc/ValueProviderFactory.cs
@@ -0,0 +1,7 @@
+namespace System.Web.Mvc
+{
+ public abstract class ValueProviderFactory
+ {
+ public abstract IValueProvider GetValueProvider(ControllerContext controllerContext);
+ }
+}
diff --git a/src/System.Web.Mvc/ValueProviderFactoryCollection.cs b/src/System.Web.Mvc/ValueProviderFactoryCollection.cs
new file mode 100644
index 00000000..84b57759
--- /dev/null
+++ b/src/System.Web.Mvc/ValueProviderFactoryCollection.cs
@@ -0,0 +1,56 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+
+namespace System.Web.Mvc
+{
+ public class ValueProviderFactoryCollection : Collection<ValueProviderFactory>
+ {
+ private IResolver<IEnumerable<ValueProviderFactory>> _serviceResolver;
+
+ public ValueProviderFactoryCollection()
+ {
+ _serviceResolver = new MultiServiceResolver<ValueProviderFactory>(() => Items);
+ }
+
+ public ValueProviderFactoryCollection(IList<ValueProviderFactory> list)
+ : base(list)
+ {
+ _serviceResolver = new MultiServiceResolver<ValueProviderFactory>(() => Items);
+ }
+
+ internal ValueProviderFactoryCollection(IResolver<IEnumerable<ValueProviderFactory>> serviceResolver, params ValueProviderFactory[] valueProviderFactories)
+ : base(valueProviderFactories)
+ {
+ _serviceResolver = serviceResolver ?? new MultiServiceResolver<ValueProviderFactory>(() => Items);
+ }
+
+ public IValueProvider GetValueProvider(ControllerContext controllerContext)
+ {
+ var valueProviders = from factory in _serviceResolver.Current
+ let valueProvider = factory.GetValueProvider(controllerContext)
+ where valueProvider != null
+ select valueProvider;
+
+ return new ValueProviderCollection(valueProviders.ToList());
+ }
+
+ protected override void InsertItem(int index, ValueProviderFactory item)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+ base.InsertItem(index, item);
+ }
+
+ protected override void SetItem(int index, ValueProviderFactory item)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+ base.SetItem(index, item);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ValueProviderResult.cs b/src/System.Web.Mvc/ValueProviderResult.cs
new file mode 100644
index 00000000..3a15980b
--- /dev/null
+++ b/src/System.Web.Mvc/ValueProviderResult.cs
@@ -0,0 +1,163 @@
+using System.Collections;
+using System.ComponentModel;
+using System.Globalization;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc
+{
+ [Serializable]
+ public class ValueProviderResult
+ {
+ private static readonly CultureInfo _staticCulture = CultureInfo.InvariantCulture;
+ private CultureInfo _instanceCulture;
+
+ // default constructor so that subclassed types can set the properties themselves
+ protected ValueProviderResult()
+ {
+ }
+
+ public ValueProviderResult(object rawValue, string attemptedValue, CultureInfo culture)
+ {
+ RawValue = rawValue;
+ AttemptedValue = attemptedValue;
+ Culture = culture;
+ }
+
+ public string AttemptedValue { get; protected set; }
+
+ public CultureInfo Culture
+ {
+ get
+ {
+ if (_instanceCulture == null)
+ {
+ _instanceCulture = _staticCulture;
+ }
+ return _instanceCulture;
+ }
+ protected set { _instanceCulture = value; }
+ }
+
+ public object RawValue { get; protected set; }
+
+ private static object ConvertSimpleType(CultureInfo culture, object value, Type destinationType)
+ {
+ if (value == null || destinationType.IsInstanceOfType(value))
+ {
+ return value;
+ }
+
+ // if this is a user-input value but the user didn't type anything, return no value
+ string valueAsString = value as string;
+ if (valueAsString != null && valueAsString.Trim().Length == 0)
+ {
+ return null;
+ }
+
+ TypeConverter converter = TypeDescriptor.GetConverter(destinationType);
+ bool canConvertFrom = converter.CanConvertFrom(value.GetType());
+ if (!canConvertFrom)
+ {
+ converter = TypeDescriptor.GetConverter(value.GetType());
+ }
+ if (!(canConvertFrom || converter.CanConvertTo(destinationType)))
+ {
+ // EnumConverter cannot convert integer, so we verify manually
+ if (destinationType.IsEnum && value is int)
+ {
+ return Enum.ToObject(destinationType, (int)value);
+ }
+
+ // In case of a Nullable object, we try again with its underlying type.
+ Type underlyingType = Nullable.GetUnderlyingType(destinationType);
+ if (underlyingType != null)
+ {
+ return ConvertSimpleType(culture, value, underlyingType);
+ }
+
+ string message = String.Format(CultureInfo.CurrentCulture, MvcResources.ValueProviderResult_NoConverterExists,
+ value.GetType().FullName, destinationType.FullName);
+ throw new InvalidOperationException(message);
+ }
+
+ try
+ {
+ object convertedValue = (canConvertFrom)
+ ? converter.ConvertFrom(null /* context */, culture, value)
+ : converter.ConvertTo(null /* context */, culture, value, destinationType);
+ return convertedValue;
+ }
+ catch (Exception ex)
+ {
+ string message = String.Format(CultureInfo.CurrentCulture, MvcResources.ValueProviderResult_ConversionThrew,
+ value.GetType().FullName, destinationType.FullName);
+ throw new InvalidOperationException(message, ex);
+ }
+ }
+
+ public object ConvertTo(Type type)
+ {
+ return ConvertTo(type, null /* culture */);
+ }
+
+ public virtual object ConvertTo(Type type, CultureInfo culture)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+
+ CultureInfo cultureToUse = culture ?? Culture;
+ return UnwrapPossibleArrayType(cultureToUse, RawValue, type);
+ }
+
+ private static object UnwrapPossibleArrayType(CultureInfo culture, object value, Type destinationType)
+ {
+ if (value == null || destinationType.IsInstanceOfType(value))
+ {
+ return value;
+ }
+
+ // array conversion results in four cases, as below
+ Array valueAsArray = value as Array;
+ if (destinationType.IsArray)
+ {
+ Type destinationElementType = destinationType.GetElementType();
+ if (valueAsArray != null)
+ {
+ // case 1: both destination + source type are arrays, so convert each element
+ IList converted = Array.CreateInstance(destinationElementType, valueAsArray.Length);
+ for (int i = 0; i < valueAsArray.Length; i++)
+ {
+ converted[i] = ConvertSimpleType(culture, valueAsArray.GetValue(i), destinationElementType);
+ }
+ return converted;
+ }
+ else
+ {
+ // case 2: destination type is array but source is single element, so wrap element in array + convert
+ object element = ConvertSimpleType(culture, value, destinationElementType);
+ IList converted = Array.CreateInstance(destinationElementType, 1);
+ converted[0] = element;
+ return converted;
+ }
+ }
+ else if (valueAsArray != null)
+ {
+ // case 3: destination type is single element but source is array, so extract first element + convert
+ if (valueAsArray.Length > 0)
+ {
+ value = valueAsArray.GetValue(0);
+ return ConvertSimpleType(culture, value, destinationType);
+ }
+ else
+ {
+ // case 3(a): source is empty array, so can't perform conversion
+ return null;
+ }
+ }
+ // case 4: both destination + source type are single elements, so convert
+ return ConvertSimpleType(culture, value, destinationType);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ValueProviderUtil.cs b/src/System.Web.Mvc/ValueProviderUtil.cs
new file mode 100644
index 00000000..4db98104
--- /dev/null
+++ b/src/System.Web.Mvc/ValueProviderUtil.cs
@@ -0,0 +1,119 @@
+using System.Collections.Generic;
+
+namespace System.Web.Mvc
+{
+ internal static class ValueProviderUtil
+ {
+ // Given "foo.bar[baz].quux", this method will return:
+ // - "foo.bar[baz].quux"
+ // - "foo.bar[baz]"
+ // - "foo.bar"
+ // - "foo"
+ public static IEnumerable<string> GetPrefixes(string key)
+ {
+ yield return key;
+ for (int i = key.Length - 1; i >= 0; i--)
+ {
+ switch (key[i])
+ {
+ case '.':
+ case '[':
+ yield return key.Substring(0, i);
+ break;
+ }
+ }
+ }
+
+ public static bool CollectionContainsPrefix(IEnumerable<string> collection, string prefix)
+ {
+ foreach (string key in collection)
+ {
+ if (key != null)
+ {
+ if (prefix.Length == 0)
+ {
+ return true; // shortcut - non-null key matches empty prefix
+ }
+
+ if (key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+ {
+ if (key.Length == prefix.Length)
+ {
+ return true; // exact match
+ }
+ else
+ {
+ switch (key[prefix.Length])
+ {
+ case '.': // known separator characters
+ case '[':
+ return true;
+ }
+ }
+ }
+ }
+ }
+
+ return false; // nothing found
+ }
+
+ // Given "foo.bar", "foo.hello", "something.other", foo[abc].baz and asking for prefix "foo" will return:
+ // - "bar"/"foo.bar"
+ // - "hello"/"foo.hello"
+ // - "abc"/"foo[abc]"
+ public static IDictionary<string, string> GetKeysFromPrefix(IEnumerable<string> collection, string prefix)
+ {
+ IDictionary<string, string> keys = new Dictionary<string, string>();
+ foreach (var entry in collection)
+ {
+ if (entry != null)
+ {
+ string key = null;
+ string fullName = null;
+
+ if (entry.Length == prefix.Length)
+ {
+ // No key in this entry
+ continue;
+ }
+
+ if (entry.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+ {
+ int keyPosition = prefix.Length + 1;
+ switch (entry[prefix.Length])
+ {
+ case '.':
+ int dotPosition = entry.IndexOf('.', keyPosition);
+ if (dotPosition == -1)
+ {
+ dotPosition = entry.Length;
+ }
+
+ key = entry.Substring(keyPosition, dotPosition - keyPosition);
+ fullName = entry.Substring(0, dotPosition);
+ break;
+ case '[':
+ int bracketPosition = entry.IndexOf(']', keyPosition);
+ if (bracketPosition == -1)
+ {
+ // Malformed for dictionary
+ continue;
+ }
+
+ key = entry.Substring(keyPosition, bracketPosition - keyPosition);
+ fullName = entry.Substring(0, bracketPosition + 1);
+ break;
+ }
+
+ if (!keys.ContainsKey(key))
+ {
+ keys.Add(key, fullName);
+ }
+ }
+ }
+ }
+
+ return keys;
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ViewContext.cs b/src/System.Web.Mvc/ViewContext.cs
new file mode 100644
index 00000000..87e5200e
--- /dev/null
+++ b/src/System.Web.Mvc/ViewContext.cs
@@ -0,0 +1,281 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.IO;
+using System.Web.WebPages.Scope;
+
+namespace System.Web.Mvc
+{
+ public class ViewContext : ControllerContext
+ {
+ private const string ClientValidationScript = @"<script type=""text/javascript"">
+//<![CDATA[
+if (!window.mvcClientValidationMetadata) {{ window.mvcClientValidationMetadata = []; }}
+window.mvcClientValidationMetadata.push({0});
+//]]>
+</script>";
+
+ internal static readonly string ClientValidationKeyName = "ClientValidationEnabled";
+ internal static readonly string UnobtrusiveJavaScriptKeyName = "UnobtrusiveJavaScriptEnabled";
+
+ // Some values have to be stored in HttpContext.Items in order to be propagated between calls
+ // to RenderPartial(), RenderAction(), etc.
+ private static readonly object _formContextKey = new object();
+ private static readonly object _lastFormNumKey = new object();
+
+ private Func<IDictionary<object, object>> _scopeThunk;
+ private IDictionary<object, object> _transientScope;
+
+ private DynamicViewDataDictionary _dynamicViewDataDictionary;
+ private Func<string> _formIdGenerator;
+
+ // We need a default FormContext if the user uses html <form> instead of an MvcForm
+ private FormContext _defaultFormContext = new FormContext();
+
+ // parameterless constructor used for mocking
+ public ViewContext()
+ {
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "The virtual property setters are only to support mocking frameworks, in which case this constructor shouldn't be called anyway.")]
+ public ViewContext(ControllerContext controllerContext, IView view, ViewDataDictionary viewData, TempDataDictionary tempData, TextWriter writer)
+ : base(controllerContext)
+ {
+ if (controllerContext == null)
+ {
+ throw new ArgumentNullException("controllerContext");
+ }
+ if (view == null)
+ {
+ throw new ArgumentNullException("view");
+ }
+ if (viewData == null)
+ {
+ throw new ArgumentNullException("viewData");
+ }
+ if (tempData == null)
+ {
+ throw new ArgumentNullException("tempData");
+ }
+ if (writer == null)
+ {
+ throw new ArgumentNullException("writer");
+ }
+
+ View = view;
+ ViewData = viewData;
+ Writer = writer;
+ TempData = tempData;
+ }
+
+ public virtual bool ClientValidationEnabled
+ {
+ get { return GetClientValidationEnabled(Scope, HttpContext); }
+ set { SetClientValidationEnabled(value, Scope, HttpContext); }
+ }
+
+ public virtual FormContext FormContext
+ {
+ get
+ {
+ // Never return a null form context, this is important for validation purposes
+ return HttpContext.Items[_formContextKey] as FormContext ?? _defaultFormContext;
+ }
+ set { HttpContext.Items[_formContextKey] = value; }
+ }
+
+ internal Func<string> FormIdGenerator
+ {
+ get
+ {
+ if (_formIdGenerator == null)
+ {
+ _formIdGenerator = DefaultFormIdGenerator;
+ }
+ return _formIdGenerator;
+ }
+ set { _formIdGenerator = value; }
+ }
+
+ internal static Func<IDictionary<object, object>> GlobalScopeThunk { get; set; }
+
+ private IDictionary<object, object> Scope
+ {
+ get
+ {
+ if (ScopeThunk != null)
+ {
+ return ScopeThunk();
+ }
+ if (_transientScope == null)
+ {
+ _transientScope = new Dictionary<object, object>();
+ }
+ return _transientScope;
+ }
+ }
+
+ internal Func<IDictionary<object, object>> ScopeThunk
+ {
+ get { return _scopeThunk ?? GlobalScopeThunk; }
+ set { _scopeThunk = value; }
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "The property setter is only here to support mocking this type and should not be called at runtime.")]
+ public virtual TempDataDictionary TempData { get; set; }
+
+ public virtual bool UnobtrusiveJavaScriptEnabled
+ {
+ get { return GetUnobtrusiveJavaScriptEnabled(Scope, HttpContext); }
+ set { SetUnobtrusiveJavaScriptEnabled(value, Scope, HttpContext); }
+ }
+
+ public virtual IView View { get; set; }
+
+ public dynamic ViewBag
+ {
+ get
+ {
+ if (_dynamicViewDataDictionary == null)
+ {
+ _dynamicViewDataDictionary = new DynamicViewDataDictionary(() => ViewData);
+ }
+ return _dynamicViewDataDictionary;
+ }
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "The property setter is only here to support mocking this type and should not be called at runtime.")]
+ public virtual ViewDataDictionary ViewData { get; set; }
+
+ public virtual TextWriter Writer { get; set; }
+
+ private string DefaultFormIdGenerator()
+ {
+ int formNum = IncrementFormCount(HttpContext.Items);
+ return String.Format(CultureInfo.InvariantCulture, "form{0}", formNum);
+ }
+
+ internal static bool GetClientValidationEnabled(IDictionary<object, object> scope = null, HttpContextBase httpContext = null)
+ {
+ return ScopeCache.Get(scope, httpContext).ClientValidationEnabled;
+ }
+
+ internal FormContext GetFormContextForClientValidation()
+ {
+ return (ClientValidationEnabled) ? FormContext : null;
+ }
+
+ internal static bool GetUnobtrusiveJavaScriptEnabled(IDictionary<object, object> scope = null, HttpContextBase httpContext = null)
+ {
+ return ScopeCache.Get(scope, httpContext).UnobtrusiveJavaScriptEnabled;
+ }
+
+ private static int IncrementFormCount(IDictionary items)
+ {
+ object lastFormNum = items[_lastFormNumKey];
+ int newFormNum = (lastFormNum != null) ? ((int)lastFormNum) + 1 : 0;
+ items[_lastFormNumKey] = newFormNum;
+ return newFormNum;
+ }
+
+ public void OutputClientValidation()
+ {
+ FormContext formContext = GetFormContextForClientValidation();
+ if (formContext == null || UnobtrusiveJavaScriptEnabled)
+ {
+ return; // do nothing
+ }
+
+ string scriptWithCorrectNewLines = ClientValidationScript.Replace("\r\n", Environment.NewLine);
+ string validationJson = formContext.GetJsonValidationMetadata();
+ string formatted = String.Format(CultureInfo.InvariantCulture, scriptWithCorrectNewLines, validationJson);
+
+ Writer.Write(formatted);
+ }
+
+ internal static void SetClientValidationEnabled(bool enabled, IDictionary<object, object> scope = null, HttpContextBase httpContext = null)
+ {
+ ScopeCache.Get(scope, httpContext).ClientValidationEnabled = enabled;
+ }
+
+ internal static void SetUnobtrusiveJavaScriptEnabled(bool enabled, IDictionary<object, object> scope = null, HttpContextBase httpContext = null)
+ {
+ ScopeCache.Get(scope, httpContext).UnobtrusiveJavaScriptEnabled = enabled;
+ }
+
+ private static TValue ScopeGet<TValue>(IDictionary<object, object> scope, string name, TValue defaultValue = default(TValue))
+ {
+ object result;
+ if (scope.TryGetValue(name, out result))
+ {
+ return (TValue)Convert.ChangeType(result, typeof(TValue), CultureInfo.InvariantCulture);
+ }
+ return defaultValue;
+ }
+
+ private sealed class ScopeCache
+ {
+ private static readonly object _cacheKey = new object();
+ private bool _clientValidationEnabled;
+ private IDictionary<object, object> _scope;
+ private bool _unobtrusiveJavaScriptEnabled;
+
+ private ScopeCache(IDictionary<object, object> scope)
+ {
+ _scope = scope;
+
+ _clientValidationEnabled = ScopeGet(scope, ClientValidationKeyName, false);
+ _unobtrusiveJavaScriptEnabled = ScopeGet(scope, UnobtrusiveJavaScriptKeyName, false);
+ }
+
+ public bool ClientValidationEnabled
+ {
+ get { return _clientValidationEnabled; }
+ set
+ {
+ _clientValidationEnabled = value;
+ _scope[ClientValidationKeyName] = value;
+ }
+ }
+
+ public bool UnobtrusiveJavaScriptEnabled
+ {
+ get { return _unobtrusiveJavaScriptEnabled; }
+ set
+ {
+ _unobtrusiveJavaScriptEnabled = value;
+ _scope[UnobtrusiveJavaScriptKeyName] = value;
+ }
+ }
+
+ public static ScopeCache Get(IDictionary<object, object> scope, HttpContextBase httpContext)
+ {
+ if (httpContext == null && Web.HttpContext.Current != null)
+ {
+ httpContext = new HttpContextWrapper(Web.HttpContext.Current);
+ }
+
+ ScopeCache result = null;
+ scope = scope ?? ScopeStorage.CurrentScope;
+
+ if (httpContext != null)
+ {
+ result = httpContext.Items[_cacheKey] as ScopeCache;
+ }
+
+ if (result == null || result._scope != scope)
+ {
+ result = new ScopeCache(scope);
+
+ if (httpContext != null)
+ {
+ httpContext.Items[_cacheKey] = result;
+ }
+ }
+
+ return result;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ViewDataDictionary.cs b/src/System.Web.Mvc/ViewDataDictionary.cs
new file mode 100644
index 00000000..d104b33f
--- /dev/null
+++ b/src/System.Web.Mvc/ViewDataDictionary.cs
@@ -0,0 +1,387 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc
+{
+ // TODO: Unit test ModelState interaction with VDD
+
+ public class ViewDataDictionary : IDictionary<string, object>
+ {
+ private readonly Dictionary<string, object> _innerDictionary = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
+ private readonly ModelStateDictionary _modelState = new ModelStateDictionary();
+ private object _model;
+ private ModelMetadata _modelMetadata;
+ private TemplateInfo _templateMetadata;
+
+ public ViewDataDictionary()
+ : this((object)null)
+ {
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "See note on SetModel() method.")]
+ public ViewDataDictionary(object model)
+ {
+ Model = model;
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "See note on SetModel() method.")]
+ public ViewDataDictionary(ViewDataDictionary dictionary)
+ {
+ if (dictionary == null)
+ {
+ throw new ArgumentNullException("dictionary");
+ }
+
+ foreach (var entry in dictionary)
+ {
+ _innerDictionary.Add(entry.Key, entry.Value);
+ }
+ foreach (var entry in dictionary.ModelState)
+ {
+ ModelState.Add(entry.Key, entry.Value);
+ }
+
+ Model = dictionary.Model;
+ TemplateInfo = dictionary.TemplateInfo;
+
+ // PERF: Don't unnecessarily instantiate the model metadata
+ _modelMetadata = dictionary._modelMetadata;
+ }
+
+ public int Count
+ {
+ get { return _innerDictionary.Count; }
+ }
+
+ public bool IsReadOnly
+ {
+ get { return ((IDictionary<string, object>)_innerDictionary).IsReadOnly; }
+ }
+
+ public ICollection<string> Keys
+ {
+ get { return _innerDictionary.Keys; }
+ }
+
+ public object Model
+ {
+ get { return _model; }
+ set
+ {
+ _modelMetadata = null;
+ SetModel(value);
+ }
+ }
+
+ public virtual ModelMetadata ModelMetadata
+ {
+ get
+ {
+ if (_modelMetadata == null && _model != null)
+ {
+ _modelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => _model, _model.GetType());
+ }
+ return _modelMetadata;
+ }
+ set { _modelMetadata = value; }
+ }
+
+ public ModelStateDictionary ModelState
+ {
+ get { return _modelState; }
+ }
+
+ public TemplateInfo TemplateInfo
+ {
+ get
+ {
+ if (_templateMetadata == null)
+ {
+ _templateMetadata = new TemplateInfo();
+ }
+ return _templateMetadata;
+ }
+ set { _templateMetadata = value; }
+ }
+
+ public ICollection<object> Values
+ {
+ get { return _innerDictionary.Values; }
+ }
+
+ public object this[string key]
+ {
+ get
+ {
+ object value;
+ _innerDictionary.TryGetValue(key, out value);
+ return value;
+ }
+ set { _innerDictionary[key] = value; }
+ }
+
+ public void Add(KeyValuePair<string, object> item)
+ {
+ ((IDictionary<string, object>)_innerDictionary).Add(item);
+ }
+
+ public void Add(string key, object value)
+ {
+ _innerDictionary.Add(key, value);
+ }
+
+ public void Clear()
+ {
+ _innerDictionary.Clear();
+ }
+
+ public bool Contains(KeyValuePair<string, object> item)
+ {
+ return ((IDictionary<string, object>)_innerDictionary).Contains(item);
+ }
+
+ public bool ContainsKey(string key)
+ {
+ return _innerDictionary.ContainsKey(key);
+ }
+
+ public void CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
+ {
+ ((IDictionary<string, object>)_innerDictionary).CopyTo(array, arrayIndex);
+ }
+
+ public object Eval(string expression)
+ {
+ ViewDataInfo info = GetViewDataInfo(expression);
+ return (info != null) ? info.Value : null;
+ }
+
+ public string Eval(string expression, string format)
+ {
+ object value = Eval(expression);
+ return FormatValueInternal(value, format);
+ }
+
+ internal static string FormatValueInternal(object value, string format)
+ {
+ if (value == null)
+ {
+ return String.Empty;
+ }
+
+ if (String.IsNullOrEmpty(format))
+ {
+ return Convert.ToString(value, CultureInfo.CurrentCulture);
+ }
+ else
+ {
+ return String.Format(CultureInfo.CurrentCulture, format, value);
+ }
+ }
+
+ public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
+ {
+ return _innerDictionary.GetEnumerator();
+ }
+
+ public ViewDataInfo GetViewDataInfo(string expression)
+ {
+ if (String.IsNullOrEmpty(expression))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "expression");
+ }
+
+ return ViewDataEvaluator.Eval(this, expression);
+ }
+
+ public bool Remove(KeyValuePair<string, object> item)
+ {
+ return ((IDictionary<string, object>)_innerDictionary).Remove(item);
+ }
+
+ public bool Remove(string key)
+ {
+ return _innerDictionary.Remove(key);
+ }
+
+ // This method will execute before the derived type's instance constructor executes. Derived types must
+ // be aware of this and should plan accordingly. For example, the logic in SetModel() should be simple
+ // enough so as not to depend on the "this" pointer referencing a fully constructed object.
+ protected virtual void SetModel(object value)
+ {
+ _model = value;
+ }
+
+ public bool TryGetValue(string key, out object value)
+ {
+ return _innerDictionary.TryGetValue(key, out value);
+ }
+
+ internal static class ViewDataEvaluator
+ {
+ public static ViewDataInfo Eval(ViewDataDictionary vdd, string expression)
+ {
+ //Given an expression "foo.bar.baz" we look up the following (pseudocode):
+ // this["foo.bar.baz.quux"]
+ // this["foo.bar.baz"]["quux"]
+ // this["foo.bar"]["baz.quux]
+ // this["foo.bar"]["baz"]["quux"]
+ // this["foo"]["bar.baz.quux"]
+ // this["foo"]["bar.baz"]["quux"]
+ // this["foo"]["bar"]["baz.quux"]
+ // this["foo"]["bar"]["baz"]["quux"]
+
+ ViewDataInfo evaluated = EvalComplexExpression(vdd, expression);
+ return evaluated;
+ }
+
+ private static ViewDataInfo EvalComplexExpression(object indexableObject, string expression)
+ {
+ foreach (ExpressionPair expressionPair in GetRightToLeftExpressions(expression))
+ {
+ string subExpression = expressionPair.Left;
+ string postExpression = expressionPair.Right;
+
+ ViewDataInfo subTargetInfo = GetPropertyValue(indexableObject, subExpression);
+ if (subTargetInfo != null)
+ {
+ if (String.IsNullOrEmpty(postExpression))
+ {
+ return subTargetInfo;
+ }
+
+ if (subTargetInfo.Value != null)
+ {
+ ViewDataInfo potential = EvalComplexExpression(subTargetInfo.Value, postExpression);
+ if (potential != null)
+ {
+ return potential;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ private static IEnumerable<ExpressionPair> GetRightToLeftExpressions(string expression)
+ {
+ // Produces an enumeration of all the combinations of complex property names
+ // given a complex expression. See the list above for an example of the result
+ // of the enumeration.
+
+ yield return new ExpressionPair(expression, String.Empty);
+
+ int lastDot = expression.LastIndexOf('.');
+
+ string subExpression = expression;
+ string postExpression = String.Empty;
+
+ while (lastDot > -1)
+ {
+ subExpression = expression.Substring(0, lastDot);
+ postExpression = expression.Substring(lastDot + 1);
+ yield return new ExpressionPair(subExpression, postExpression);
+
+ lastDot = subExpression.LastIndexOf('.');
+ }
+ }
+
+ private static ViewDataInfo GetIndexedPropertyValue(object indexableObject, string key)
+ {
+ IDictionary<string, object> dict = indexableObject as IDictionary<string, object>;
+ object value = null;
+ bool success = false;
+
+ if (dict != null)
+ {
+ success = dict.TryGetValue(key, out value);
+ }
+ else
+ {
+ TryGetValueDelegate tgvDel = TypeHelpers.CreateTryGetValueDelegate(indexableObject.GetType());
+ if (tgvDel != null)
+ {
+ success = tgvDel(indexableObject, key, out value);
+ }
+ }
+
+ if (success)
+ {
+ return new ViewDataInfo()
+ {
+ Container = indexableObject,
+ Value = value
+ };
+ }
+
+ return null;
+ }
+
+ private static ViewDataInfo GetPropertyValue(object container, string propertyName)
+ {
+ // This method handles one "segment" of a complex property expression
+
+ // First, we try to evaluate the property based on its indexer
+ ViewDataInfo value = GetIndexedPropertyValue(container, propertyName);
+ if (value != null)
+ {
+ return value;
+ }
+
+ // If the indexer didn't return anything useful, continue...
+
+ // If the container is a ViewDataDictionary then treat its Model property
+ // as the container instead of the ViewDataDictionary itself.
+ ViewDataDictionary vdd = container as ViewDataDictionary;
+ if (vdd != null)
+ {
+ container = vdd.Model;
+ }
+
+ // If the container is null, we're out of options
+ if (container == null)
+ {
+ return null;
+ }
+
+ // Second, we try to use PropertyDescriptors and treat the expression as a property name
+ PropertyDescriptor descriptor = TypeDescriptor.GetProperties(container).Find(propertyName, true);
+ if (descriptor == null)
+ {
+ return null;
+ }
+
+ return new ViewDataInfo(() => descriptor.GetValue(container))
+ {
+ Container = container,
+ PropertyDescriptor = descriptor
+ };
+ }
+
+ private struct ExpressionPair
+ {
+ public readonly string Left;
+ public readonly string Right;
+
+ public ExpressionPair(string left, string right)
+ {
+ Left = left;
+ Right = right;
+ }
+ }
+ }
+
+ #region IEnumerable Members
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return ((IEnumerable)_innerDictionary).GetEnumerator();
+ }
+
+ #endregion
+ }
+}
diff --git a/src/System.Web.Mvc/ViewDataDictionary`1.cs b/src/System.Web.Mvc/ViewDataDictionary`1.cs
new file mode 100644
index 00000000..966cc77b
--- /dev/null
+++ b/src/System.Web.Mvc/ViewDataDictionary`1.cs
@@ -0,0 +1,60 @@
+namespace System.Web.Mvc
+{
+ public class ViewDataDictionary<TModel> : ViewDataDictionary
+ {
+ public ViewDataDictionary()
+ :
+ base(default(TModel))
+ {
+ }
+
+ public ViewDataDictionary(TModel model)
+ :
+ base(model)
+ {
+ }
+
+ public ViewDataDictionary(ViewDataDictionary viewDataDictionary)
+ :
+ base(viewDataDictionary)
+ {
+ }
+
+ public new TModel Model
+ {
+ get { return (TModel)base.Model; }
+ set { SetModel(value); }
+ }
+
+ public override ModelMetadata ModelMetadata
+ {
+ get
+ {
+ ModelMetadata result = base.ModelMetadata;
+ if (result == null)
+ {
+ result = base.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(TModel));
+ }
+ return result;
+ }
+ set { base.ModelMetadata = value; }
+ }
+
+ protected override void SetModel(object value)
+ {
+ bool castWillSucceed = TypeHelpers.IsCompatibleObject<TModel>(value);
+
+ if (castWillSucceed)
+ {
+ base.SetModel((TModel)value);
+ }
+ else
+ {
+ InvalidOperationException exception = (value != null)
+ ? Error.ViewDataDictionary_WrongTModelType(value.GetType(), typeof(TModel))
+ : Error.ViewDataDictionary_ModelCannotBeNull(typeof(TModel));
+ throw exception;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ViewDataInfo.cs b/src/System.Web.Mvc/ViewDataInfo.cs
new file mode 100644
index 00000000..c508ffda
--- /dev/null
+++ b/src/System.Web.Mvc/ViewDataInfo.cs
@@ -0,0 +1,42 @@
+using System.ComponentModel;
+
+namespace System.Web.Mvc
+{
+ public class ViewDataInfo
+ {
+ private object _value;
+ private Func<object> _valueAccessor;
+
+ public ViewDataInfo()
+ {
+ }
+
+ public ViewDataInfo(Func<object> valueAccessor)
+ {
+ _valueAccessor = valueAccessor;
+ }
+
+ public object Container { get; set; }
+
+ public PropertyDescriptor PropertyDescriptor { get; set; }
+
+ public object Value
+ {
+ get
+ {
+ if (_valueAccessor != null)
+ {
+ _value = _valueAccessor();
+ _valueAccessor = null;
+ }
+
+ return _value;
+ }
+ set
+ {
+ _value = value;
+ _valueAccessor = null;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ViewEngineCollection.cs b/src/System.Web.Mvc/ViewEngineCollection.cs
new file mode 100644
index 00000000..6dcd254b
--- /dev/null
+++ b/src/System.Web.Mvc/ViewEngineCollection.cs
@@ -0,0 +1,133 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc
+{
+ public class ViewEngineCollection : Collection<IViewEngine>
+ {
+ private IResolver<IEnumerable<IViewEngine>> _serviceResolver;
+
+ public ViewEngineCollection()
+ {
+ _serviceResolver = new MultiServiceResolver<IViewEngine>(() => Items);
+ }
+
+ public ViewEngineCollection(IList<IViewEngine> list)
+ : base(list)
+ {
+ _serviceResolver = new MultiServiceResolver<IViewEngine>(() => Items);
+ }
+
+ internal ViewEngineCollection(IResolver<IEnumerable<IViewEngine>> serviceResolver, params IViewEngine[] engines)
+ : base(engines)
+ {
+ _serviceResolver = serviceResolver ?? new MultiServiceResolver<IViewEngine>(() => Items);
+ }
+
+ private IEnumerable<IViewEngine> CombinedItems
+ {
+ get { return _serviceResolver.Current; }
+ }
+
+ protected override void InsertItem(int index, IViewEngine item)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+ base.InsertItem(index, item);
+ }
+
+ protected override void SetItem(int index, IViewEngine item)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+ base.SetItem(index, item);
+ }
+
+ private ViewEngineResult Find(Func<IViewEngine, ViewEngineResult> cacheLocator, Func<IViewEngine, ViewEngineResult> locator)
+ {
+ // First, look up using the cacheLocator and do not track the searched paths in non-matching view engines
+ // Then, look up using the normal locator and track the searched paths so that an error view engine can be returned
+ return Find(cacheLocator, trackSearchedPaths: false)
+ ?? Find(locator, trackSearchedPaths: true);
+ }
+
+ private ViewEngineResult Find(Func<IViewEngine, ViewEngineResult> lookup, bool trackSearchedPaths)
+ {
+ // Returns
+ // 1st result
+ // OR list of searched paths (if trackSearchedPaths == true)
+ // OR null
+ ViewEngineResult result;
+
+ List<string> searched = null;
+ if (trackSearchedPaths)
+ {
+ searched = new List<string>();
+ }
+
+ foreach (IViewEngine engine in CombinedItems)
+ {
+ if (engine != null)
+ {
+ result = lookup(engine);
+
+ if (result.View != null)
+ {
+ return result;
+ }
+
+ if (trackSearchedPaths)
+ {
+ searched.AddRange(result.SearchedLocations);
+ }
+ }
+ }
+
+ if (trackSearchedPaths)
+ {
+ // Remove duplicate search paths since multiple view engines could have potentially looked at the same path
+ return new ViewEngineResult(searched.Distinct().ToList());
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ public virtual ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName)
+ {
+ if (controllerContext == null)
+ {
+ throw new ArgumentNullException("controllerContext");
+ }
+ if (String.IsNullOrEmpty(partialViewName))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "partialViewName");
+ }
+
+ return Find(e => e.FindPartialView(controllerContext, partialViewName, true),
+ e => e.FindPartialView(controllerContext, partialViewName, false));
+ }
+
+ public virtual ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName)
+ {
+ if (controllerContext == null)
+ {
+ throw new ArgumentNullException("controllerContext");
+ }
+ if (String.IsNullOrEmpty(viewName))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "viewName");
+ }
+
+ return Find(e => e.FindView(controllerContext, viewName, masterName, true),
+ e => e.FindView(controllerContext, viewName, masterName, false));
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ViewEngineResult.cs b/src/System.Web.Mvc/ViewEngineResult.cs
new file mode 100644
index 00000000..7e9e081a
--- /dev/null
+++ b/src/System.Web.Mvc/ViewEngineResult.cs
@@ -0,0 +1,38 @@
+using System.Collections.Generic;
+
+namespace System.Web.Mvc
+{
+ public class ViewEngineResult
+ {
+ public ViewEngineResult(IEnumerable<string> searchedLocations)
+ {
+ if (searchedLocations == null)
+ {
+ throw new ArgumentNullException("searchedLocations");
+ }
+
+ SearchedLocations = searchedLocations;
+ }
+
+ public ViewEngineResult(IView view, IViewEngine viewEngine)
+ {
+ if (view == null)
+ {
+ throw new ArgumentNullException("view");
+ }
+ if (viewEngine == null)
+ {
+ throw new ArgumentNullException("viewEngine");
+ }
+
+ View = view;
+ ViewEngine = viewEngine;
+ }
+
+ public IEnumerable<string> SearchedLocations { get; private set; }
+
+ public IView View { get; private set; }
+
+ public IViewEngine ViewEngine { get; private set; }
+ }
+}
diff --git a/src/System.Web.Mvc/ViewEngines.cs b/src/System.Web.Mvc/ViewEngines.cs
new file mode 100644
index 00000000..090f20bc
--- /dev/null
+++ b/src/System.Web.Mvc/ViewEngines.cs
@@ -0,0 +1,16 @@
+namespace System.Web.Mvc
+{
+ public static class ViewEngines
+ {
+ private static readonly ViewEngineCollection _engines = new ViewEngineCollection
+ {
+ new WebFormViewEngine(),
+ new RazorViewEngine(),
+ };
+
+ public static ViewEngineCollection Engines
+ {
+ get { return _engines; }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ViewMasterPage.cs b/src/System.Web.Mvc/ViewMasterPage.cs
new file mode 100644
index 00000000..c3d7c66b
--- /dev/null
+++ b/src/System.Web.Mvc/ViewMasterPage.cs
@@ -0,0 +1,68 @@
+using System.Globalization;
+using System.Web.Mvc.Properties;
+using System.Web.UI;
+
+namespace System.Web.Mvc
+{
+ [FileLevelControlBuilder(typeof(ViewMasterPageControlBuilder))]
+ public class ViewMasterPage : MasterPage
+ {
+ public AjaxHelper<object> Ajax
+ {
+ get { return ViewPage.Ajax; }
+ }
+
+ public HtmlHelper<object> Html
+ {
+ get { return ViewPage.Html; }
+ }
+
+ public object Model
+ {
+ get { return ViewData.Model; }
+ }
+
+ public TempDataDictionary TempData
+ {
+ get { return ViewPage.TempData; }
+ }
+
+ public UrlHelper Url
+ {
+ get { return ViewPage.Url; }
+ }
+
+ public dynamic ViewBag
+ {
+ get { return ViewPage.ViewBag; }
+ }
+
+ public ViewContext ViewContext
+ {
+ get { return ViewPage.ViewContext; }
+ }
+
+ public ViewDataDictionary ViewData
+ {
+ get { return ViewPage.ViewData; }
+ }
+
+ internal ViewPage ViewPage
+ {
+ get
+ {
+ ViewPage viewPage = Page as ViewPage;
+ if (viewPage == null)
+ {
+ throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, MvcResources.ViewMasterPage_RequiresViewPage));
+ }
+ return viewPage;
+ }
+ }
+
+ public HtmlTextWriter Writer
+ {
+ get { return ViewPage.Writer; }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ViewMasterPageControlBuilder.cs b/src/System.Web.Mvc/ViewMasterPageControlBuilder.cs
new file mode 100644
index 00000000..a41556ef
--- /dev/null
+++ b/src/System.Web.Mvc/ViewMasterPageControlBuilder.cs
@@ -0,0 +1,18 @@
+using System.CodeDom;
+using System.Web.UI;
+
+namespace System.Web.Mvc
+{
+ internal sealed class ViewMasterPageControlBuilder : FileLevelMasterPageControlBuilder, IMvcControlBuilder
+ {
+ public string Inherits { get; set; }
+
+ public override void ProcessGeneratedCode(CodeCompileUnit codeCompileUnit, CodeTypeDeclaration baseType, CodeTypeDeclaration derivedType, CodeMemberMethod buildMethod, CodeMemberMethod dataBindingMethod)
+ {
+ if (!String.IsNullOrWhiteSpace(Inherits))
+ {
+ derivedType.BaseTypes[0] = new CodeTypeReference(Inherits);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ViewMasterPage`1.cs b/src/System.Web.Mvc/ViewMasterPage`1.cs
new file mode 100644
index 00000000..d514c7d7
--- /dev/null
+++ b/src/System.Web.Mvc/ViewMasterPage`1.cs
@@ -0,0 +1,50 @@
+namespace System.Web.Mvc
+{
+ public class ViewMasterPage<TModel> : ViewMasterPage
+ {
+ private AjaxHelper<TModel> _ajaxHelper;
+ private HtmlHelper<TModel> _htmlHelper;
+ private ViewDataDictionary<TModel> _viewData;
+
+ public new AjaxHelper<TModel> Ajax
+ {
+ get
+ {
+ if (_ajaxHelper == null)
+ {
+ _ajaxHelper = new AjaxHelper<TModel>(ViewContext, ViewPage);
+ }
+ return _ajaxHelper;
+ }
+ }
+
+ public new HtmlHelper<TModel> Html
+ {
+ get
+ {
+ if (_htmlHelper == null)
+ {
+ _htmlHelper = new HtmlHelper<TModel>(ViewContext, ViewPage);
+ }
+ return _htmlHelper;
+ }
+ }
+
+ public new TModel Model
+ {
+ get { return ViewData.Model; }
+ }
+
+ public new ViewDataDictionary<TModel> ViewData
+ {
+ get
+ {
+ if (_viewData == null)
+ {
+ _viewData = new ViewDataDictionary<TModel>(ViewPage.ViewData);
+ }
+ return _viewData;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ViewPage.cs b/src/System.Web.Mvc/ViewPage.cs
new file mode 100644
index 00000000..79a4fec7
--- /dev/null
+++ b/src/System.Web.Mvc/ViewPage.cs
@@ -0,0 +1,427 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.IO;
+using System.Text;
+using System.Web.UI;
+
+namespace System.Web.Mvc
+{
+ [FileLevelControlBuilder(typeof(ViewPageControlBuilder))]
+ public class ViewPage : Page, IViewDataContainer
+ {
+ [ThreadStatic]
+ private static int _nextId;
+
+ private DynamicViewDataDictionary _dynamicViewData;
+ private string _masterLocation;
+
+ private ViewDataDictionary _viewData;
+
+ public AjaxHelper<object> Ajax { get; set; }
+
+ public HtmlHelper<object> Html { get; set; }
+
+ public string MasterLocation
+ {
+ get { return _masterLocation ?? String.Empty; }
+ set { _masterLocation = value; }
+ }
+
+ public object Model
+ {
+ get { return ViewData.Model; }
+ }
+
+ public TempDataDictionary TempData
+ {
+ get { return ViewContext.TempData; }
+ }
+
+ public UrlHelper Url { get; set; }
+
+ public dynamic ViewBag
+ {
+ get
+ {
+ if (_dynamicViewData == null)
+ {
+ _dynamicViewData = new DynamicViewDataDictionary(() => ViewData);
+ }
+ return _dynamicViewData;
+ }
+ }
+
+ public ViewContext ViewContext { get; set; }
+
+ [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This is the mechanism by which the ViewPage gets its ViewDataDictionary object.")]
+ public ViewDataDictionary ViewData
+ {
+ get
+ {
+ if (_viewData == null)
+ {
+ SetViewData(new ViewDataDictionary());
+ }
+ return _viewData;
+ }
+ set { SetViewData(value); }
+ }
+
+ public HtmlTextWriter Writer { get; private set; }
+
+ public virtual void InitHelpers()
+ {
+ Ajax = new AjaxHelper<object>(ViewContext, this);
+ Html = new HtmlHelper<object>(ViewContext, this);
+ Url = new UrlHelper(ViewContext.RequestContext);
+ }
+
+ internal static string NextId()
+ {
+ return (++_nextId).ToString(CultureInfo.InvariantCulture);
+ }
+
+ protected override void OnPreInit(EventArgs e)
+ {
+ base.OnPreInit(e);
+
+ if (!String.IsNullOrEmpty(MasterLocation))
+ {
+ MasterPageFile = MasterLocation;
+ }
+ }
+
+ public override void ProcessRequest(HttpContext context)
+ {
+ // Tracing requires IDs to be unique.
+ ID = NextId();
+
+ base.ProcessRequest(context);
+ }
+
+ protected override void Render(HtmlTextWriter writer)
+ {
+ Writer = writer;
+ try
+ {
+ base.Render(writer);
+ }
+ finally
+ {
+ Writer = null;
+ }
+ }
+
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "The object is disposed in the finally block of the method")]
+ public virtual void RenderView(ViewContext viewContext)
+ {
+ ViewContext = viewContext;
+ InitHelpers();
+
+ bool createdSwitchWriter = false;
+ SwitchWriter switchWriter = viewContext.HttpContext.Response.Output as SwitchWriter;
+
+ try
+ {
+ if (switchWriter == null)
+ {
+ switchWriter = new SwitchWriter();
+ createdSwitchWriter = true;
+ }
+
+ using (switchWriter.Scope(viewContext.Writer))
+ {
+ if (createdSwitchWriter)
+ {
+ // It's safe to reset the _nextId within a Server.Execute() since it pushes a new TraceContext onto
+ // the stack, so there won't be an ID conflict.
+ int originalNextId = _nextId;
+ try
+ {
+ _nextId = 0;
+ viewContext.HttpContext.Server.Execute(HttpHandlerUtil.WrapForServerExecute(this), switchWriter, true /* preserveForm */);
+ }
+ finally
+ {
+ // Restore the original _nextId in case this isn't actually the outermost view, since resetting
+ // the _nextId may now cause trace ID conflicts in the outer view.
+ _nextId = originalNextId;
+ }
+ }
+ else
+ {
+ ProcessRequest(HttpContext.Current);
+ }
+ }
+ }
+ finally
+ {
+ if (createdSwitchWriter)
+ {
+ switchWriter.Dispose();
+ }
+ }
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "textWriter", Justification = "This method existed in MVC 1.0 and has been deprecated.")]
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This method existed in MVC 1.0 and has been deprecated.")]
+ [Obsolete("The TextWriter is now provided by the ViewContext object passed to the RenderView method.", true /* error */)]
+ public void SetTextWriter(TextWriter textWriter)
+ {
+ // this is now a no-op
+ }
+
+ protected virtual void SetViewData(ViewDataDictionary viewData)
+ {
+ _viewData = viewData;
+ }
+
+ internal class SwitchWriter : TextWriter
+ {
+ public SwitchWriter()
+ : base(CultureInfo.CurrentCulture)
+ {
+ }
+
+ public override Encoding Encoding
+ {
+ get { return InnerWriter.Encoding; }
+ }
+
+ public override IFormatProvider FormatProvider
+ {
+ get { return InnerWriter.FormatProvider; }
+ }
+
+ internal TextWriter InnerWriter { get; set; }
+
+ public override string NewLine
+ {
+ get { return InnerWriter.NewLine; }
+ set { InnerWriter.NewLine = value; }
+ }
+
+ public override void Close()
+ {
+ InnerWriter.Close();
+ }
+
+ public override void Flush()
+ {
+ InnerWriter.Flush();
+ }
+
+ public IDisposable Scope(TextWriter writer)
+ {
+ WriterScope scope = new WriterScope(this, InnerWriter);
+
+ try
+ {
+ if (writer != this)
+ {
+ InnerWriter = writer;
+ }
+
+ return scope;
+ }
+ catch
+ {
+ scope.Dispose();
+ throw;
+ }
+ }
+
+ public override void Write(bool value)
+ {
+ InnerWriter.Write(value);
+ }
+
+ public override void Write(char value)
+ {
+ InnerWriter.Write(value);
+ }
+
+ public override void Write(char[] buffer)
+ {
+ InnerWriter.Write(buffer);
+ }
+
+ public override void Write(char[] buffer, int index, int count)
+ {
+ InnerWriter.Write(buffer, index, count);
+ }
+
+ public override void Write(decimal value)
+ {
+ InnerWriter.Write(value);
+ }
+
+ public override void Write(double value)
+ {
+ InnerWriter.Write(value);
+ }
+
+ public override void Write(float value)
+ {
+ InnerWriter.Write(value);
+ }
+
+ public override void Write(int value)
+ {
+ InnerWriter.Write(value);
+ }
+
+ public override void Write(long value)
+ {
+ InnerWriter.Write(value);
+ }
+
+ public override void Write(object value)
+ {
+ InnerWriter.Write(value);
+ }
+
+ public override void Write(string format, object arg0)
+ {
+ InnerWriter.Write(format, arg0);
+ }
+
+ public override void Write(string format, object arg0, object arg1)
+ {
+ InnerWriter.Write(format, arg0, arg1);
+ }
+
+ public override void Write(string format, object arg0, object arg1, object arg2)
+ {
+ InnerWriter.Write(format, arg0, arg1, arg2);
+ }
+
+ public override void Write(string format, params object[] arg)
+ {
+ InnerWriter.Write(format, arg);
+ }
+
+ public override void Write(string value)
+ {
+ InnerWriter.Write(value);
+ }
+
+ public override void Write(uint value)
+ {
+ InnerWriter.Write(value);
+ }
+
+ public override void Write(ulong value)
+ {
+ InnerWriter.Write(value);
+ }
+
+ public override void WriteLine()
+ {
+ InnerWriter.WriteLine();
+ }
+
+ public override void WriteLine(bool value)
+ {
+ InnerWriter.WriteLine(value);
+ }
+
+ public override void WriteLine(char value)
+ {
+ InnerWriter.WriteLine(value);
+ }
+
+ public override void WriteLine(char[] buffer)
+ {
+ InnerWriter.WriteLine(buffer);
+ }
+
+ public override void WriteLine(char[] buffer, int index, int count)
+ {
+ InnerWriter.WriteLine(buffer, index, count);
+ }
+
+ public override void WriteLine(decimal value)
+ {
+ InnerWriter.WriteLine(value);
+ }
+
+ public override void WriteLine(double value)
+ {
+ InnerWriter.WriteLine(value);
+ }
+
+ public override void WriteLine(float value)
+ {
+ InnerWriter.WriteLine(value);
+ }
+
+ public override void WriteLine(int value)
+ {
+ InnerWriter.WriteLine(value);
+ }
+
+ public override void WriteLine(long value)
+ {
+ InnerWriter.WriteLine(value);
+ }
+
+ public override void WriteLine(object value)
+ {
+ InnerWriter.WriteLine(value);
+ }
+
+ public override void WriteLine(string format, object arg0)
+ {
+ InnerWriter.WriteLine(format, arg0);
+ }
+
+ public override void WriteLine(string format, object arg0, object arg1)
+ {
+ InnerWriter.WriteLine(format, arg0, arg1);
+ }
+
+ public override void WriteLine(string format, object arg0, object arg1, object arg2)
+ {
+ InnerWriter.WriteLine(format, arg0, arg1, arg2);
+ }
+
+ public override void WriteLine(string format, params object[] arg)
+ {
+ InnerWriter.WriteLine(format, arg);
+ }
+
+ public override void WriteLine(string value)
+ {
+ InnerWriter.WriteLine(value);
+ }
+
+ public override void WriteLine(uint value)
+ {
+ InnerWriter.WriteLine(value);
+ }
+
+ public override void WriteLine(ulong value)
+ {
+ InnerWriter.WriteLine(value);
+ }
+
+ private sealed class WriterScope : IDisposable
+ {
+ private SwitchWriter _switchWriter;
+ private TextWriter _writerToRestore;
+
+ public WriterScope(SwitchWriter switchWriter, TextWriter writerToRestore)
+ {
+ _switchWriter = switchWriter;
+ _writerToRestore = writerToRestore;
+ }
+
+ public void Dispose()
+ {
+ _switchWriter.InnerWriter = _writerToRestore;
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ViewPageControlBuilder.cs b/src/System.Web.Mvc/ViewPageControlBuilder.cs
new file mode 100644
index 00000000..0dd309a3
--- /dev/null
+++ b/src/System.Web.Mvc/ViewPageControlBuilder.cs
@@ -0,0 +1,18 @@
+using System.CodeDom;
+using System.Web.UI;
+
+namespace System.Web.Mvc
+{
+ internal sealed class ViewPageControlBuilder : FileLevelPageControlBuilder, IMvcControlBuilder
+ {
+ public string Inherits { get; set; }
+
+ public override void ProcessGeneratedCode(CodeCompileUnit codeCompileUnit, CodeTypeDeclaration baseType, CodeTypeDeclaration derivedType, CodeMemberMethod buildMethod, CodeMemberMethod dataBindingMethod)
+ {
+ if (!String.IsNullOrWhiteSpace(Inherits))
+ {
+ derivedType.BaseTypes[0] = new CodeTypeReference(Inherits);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ViewPage`1.cs b/src/System.Web.Mvc/ViewPage`1.cs
new file mode 100644
index 00000000..2e5be343
--- /dev/null
+++ b/src/System.Web.Mvc/ViewPage`1.cs
@@ -0,0 +1,47 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Web.Mvc
+{
+ public class ViewPage<TModel> : ViewPage
+ {
+ private ViewDataDictionary<TModel> _viewData;
+
+ public new AjaxHelper<TModel> Ajax { get; set; }
+
+ public new HtmlHelper<TModel> Html { get; set; }
+
+ public new TModel Model
+ {
+ get { return ViewData.Model; }
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This is settable for unit testing purposes")]
+ public new ViewDataDictionary<TModel> ViewData
+ {
+ get
+ {
+ if (_viewData == null)
+ {
+ SetViewData(new ViewDataDictionary<TModel>());
+ }
+ return _viewData;
+ }
+ set { SetViewData(value); }
+ }
+
+ public override void InitHelpers()
+ {
+ base.InitHelpers();
+
+ Ajax = new AjaxHelper<TModel>(ViewContext, this);
+ Html = new HtmlHelper<TModel>(ViewContext, this);
+ }
+
+ protected override void SetViewData(ViewDataDictionary viewData)
+ {
+ _viewData = new ViewDataDictionary<TModel>(viewData);
+
+ base.SetViewData(_viewData);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ViewResult.cs b/src/System.Web.Mvc/ViewResult.cs
new file mode 100644
index 00000000..c462d6df
--- /dev/null
+++ b/src/System.Web.Mvc/ViewResult.cs
@@ -0,0 +1,36 @@
+using System.Globalization;
+using System.Text;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc
+{
+ public class ViewResult : ViewResultBase
+ {
+ private string _masterName;
+
+ public string MasterName
+ {
+ get { return _masterName ?? String.Empty; }
+ set { _masterName = value; }
+ }
+
+ protected override ViewEngineResult FindView(ControllerContext context)
+ {
+ ViewEngineResult result = ViewEngineCollection.FindView(context, ViewName, MasterName);
+ if (result.View != null)
+ {
+ return result;
+ }
+
+ // we need to generate an exception containing all the locations we searched
+ StringBuilder locationsText = new StringBuilder();
+ foreach (string location in result.SearchedLocations)
+ {
+ locationsText.AppendLine();
+ locationsText.Append(location);
+ }
+ throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture,
+ MvcResources.Common_ViewNotFound, ViewName, locationsText));
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ViewResultBase.cs b/src/System.Web.Mvc/ViewResultBase.cs
new file mode 100644
index 00000000..d7650aa8
--- /dev/null
+++ b/src/System.Web.Mvc/ViewResultBase.cs
@@ -0,0 +1,105 @@
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+
+namespace System.Web.Mvc
+{
+ public abstract class ViewResultBase : ActionResult
+ {
+ private DynamicViewDataDictionary _dynamicViewData;
+ private TempDataDictionary _tempData;
+ private ViewDataDictionary _viewData;
+ private ViewEngineCollection _viewEngineCollection;
+ private string _viewName;
+
+ public object Model
+ {
+ get { return ViewData.Model; }
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This entire type is meant to be mutable.")]
+ public TempDataDictionary TempData
+ {
+ get
+ {
+ if (_tempData == null)
+ {
+ _tempData = new TempDataDictionary();
+ }
+ return _tempData;
+ }
+ set { _tempData = value; }
+ }
+
+ public IView View { get; set; }
+
+ public dynamic ViewBag
+ {
+ get
+ {
+ if (_dynamicViewData == null)
+ {
+ _dynamicViewData = new DynamicViewDataDictionary(() => ViewData);
+ }
+ return _dynamicViewData;
+ }
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This entire type is meant to be mutable.")]
+ public ViewDataDictionary ViewData
+ {
+ get
+ {
+ if (_viewData == null)
+ {
+ _viewData = new ViewDataDictionary();
+ }
+ return _viewData;
+ }
+ set { _viewData = value; }
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This entire type is meant to be mutable.")]
+ public ViewEngineCollection ViewEngineCollection
+ {
+ get { return _viewEngineCollection ?? ViewEngines.Engines; }
+ set { _viewEngineCollection = value; }
+ }
+
+ public string ViewName
+ {
+ get { return _viewName ?? String.Empty; }
+ set { _viewName = value; }
+ }
+
+ public override void ExecuteResult(ControllerContext context)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException("context");
+ }
+ if (String.IsNullOrEmpty(ViewName))
+ {
+ ViewName = context.RouteData.GetRequiredString("action");
+ }
+
+ ViewEngineResult result = null;
+
+ if (View == null)
+ {
+ result = FindView(context);
+ View = result.View;
+ }
+
+ TextWriter writer = context.HttpContext.Response.Output;
+ ViewContext viewContext = new ViewContext(context, View, ViewData, TempData, writer);
+ View.Render(viewContext, writer);
+
+ if (result != null)
+ {
+ result.ViewEngine.ReleaseView(context, View);
+ }
+ }
+
+ protected abstract ViewEngineResult FindView(ControllerContext context);
+ }
+}
diff --git a/src/System.Web.Mvc/ViewStartPage.cs b/src/System.Web.Mvc/ViewStartPage.cs
new file mode 100644
index 00000000..437943c1
--- /dev/null
+++ b/src/System.Web.Mvc/ViewStartPage.cs
@@ -0,0 +1,43 @@
+using System.Web.Mvc.Properties;
+using System.Web.WebPages;
+
+namespace System.Web.Mvc
+{
+ public abstract class ViewStartPage : StartPage, IViewStartPageChild
+ {
+ private IViewStartPageChild _viewStartPageChild;
+
+ public HtmlHelper<object> Html
+ {
+ get { return ViewStartPageChild.Html; }
+ }
+
+ public UrlHelper Url
+ {
+ get { return ViewStartPageChild.Url; }
+ }
+
+ public ViewContext ViewContext
+ {
+ get { return ViewStartPageChild.ViewContext; }
+ }
+
+ internal IViewStartPageChild ViewStartPageChild
+ {
+ get
+ {
+ if (_viewStartPageChild == null)
+ {
+ IViewStartPageChild child = ChildPage as IViewStartPageChild;
+ if (child == null)
+ {
+ throw new InvalidOperationException(MvcResources.ViewStartPage_RequiresMvcRazorView);
+ }
+ _viewStartPageChild = child;
+ }
+
+ return _viewStartPageChild;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ViewTemplateUserControl.cs b/src/System.Web.Mvc/ViewTemplateUserControl.cs
new file mode 100644
index 00000000..fa6f0096
--- /dev/null
+++ b/src/System.Web.Mvc/ViewTemplateUserControl.cs
@@ -0,0 +1,6 @@
+namespace System.Web.Mvc
+{
+ public class ViewTemplateUserControl : ViewTemplateUserControl<object>
+ {
+ }
+}
diff --git a/src/System.Web.Mvc/ViewTemplateUserControl`1.cs b/src/System.Web.Mvc/ViewTemplateUserControl`1.cs
new file mode 100644
index 00000000..5f3243fd
--- /dev/null
+++ b/src/System.Web.Mvc/ViewTemplateUserControl`1.cs
@@ -0,0 +1,10 @@
+namespace System.Web.Mvc
+{
+ public class ViewTemplateUserControl<TModel> : ViewUserControl<TModel>
+ {
+ protected string FormattedModelValue
+ {
+ get { return ViewData.TemplateInfo.FormattedModelValue.ToString(); }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ViewType.cs b/src/System.Web.Mvc/ViewType.cs
new file mode 100644
index 00000000..2bf9c01a
--- /dev/null
+++ b/src/System.Web.Mvc/ViewType.cs
@@ -0,0 +1,19 @@
+using System.ComponentModel;
+using System.Web.UI;
+
+namespace System.Web.Mvc
+{
+ [ControlBuilder(typeof(ViewTypeControlBuilder))]
+ [NonVisualControl]
+ public class ViewType : Control
+ {
+ private string _typeName;
+
+ [DefaultValue("")]
+ public string TypeName
+ {
+ get { return _typeName ?? String.Empty; }
+ set { _typeName = value; }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ViewTypeControlBuilder.cs b/src/System.Web.Mvc/ViewTypeControlBuilder.cs
new file mode 100644
index 00000000..01a0a2aa
--- /dev/null
+++ b/src/System.Web.Mvc/ViewTypeControlBuilder.cs
@@ -0,0 +1,29 @@
+using System.CodeDom;
+using System.Collections;
+using System.Web.UI;
+
+namespace System.Web.Mvc
+{
+ internal sealed class ViewTypeControlBuilder : ControlBuilder
+ {
+ private string _typeName;
+
+ public override void Init(TemplateParser parser, ControlBuilder parentBuilder, Type type, string tagName, string id, IDictionary attribs)
+ {
+ base.Init(parser, parentBuilder, type, tagName, id, attribs);
+
+ _typeName = (string)attribs["typename"];
+ }
+
+ public override void ProcessGeneratedCode(
+ CodeCompileUnit codeCompileUnit,
+ CodeTypeDeclaration baseType,
+ CodeTypeDeclaration derivedType,
+ CodeMemberMethod buildMethod,
+ CodeMemberMethod dataBindingMethod)
+ {
+ // Override the view's base type with the explicit base type
+ derivedType.BaseTypes[0] = new CodeTypeReference(_typeName);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ViewTypeParserFilter.cs b/src/System.Web.Mvc/ViewTypeParserFilter.cs
new file mode 100644
index 00000000..3d1caecf
--- /dev/null
+++ b/src/System.Web.Mvc/ViewTypeParserFilter.cs
@@ -0,0 +1,109 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Web.UI;
+
+namespace System.Web.Mvc
+{
+ internal class ViewTypeParserFilter : PageParserFilter
+ {
+ private static Dictionary<string, Type> _directiveBaseTypeMappings = new Dictionary<string, Type>
+ {
+ { "page", typeof(ViewPage) },
+ { "control", typeof(ViewUserControl) },
+ { "master", typeof(ViewMasterPage) },
+ };
+
+ private string _inherits;
+
+ [SuppressMessage("Microsoft.Security", "CA2141:TransparentMethodsMustNotSatisfyLinkDemandsFxCopRule", Justification = "System.Web.Mvc is SecurityTransparent and requires medium trust to run, so this downstream link demand is fine")]
+ public ViewTypeParserFilter()
+ {
+ }
+
+ public override bool AllowCode
+ {
+ get { return true; }
+ }
+
+ public override int NumberOfControlsAllowed
+ {
+ get { return -1; }
+ }
+
+ public override int NumberOfDirectDependenciesAllowed
+ {
+ get { return -1; }
+ }
+
+ public override int TotalNumberOfDependenciesAllowed
+ {
+ get { return -1; }
+ }
+
+ [SuppressMessage("Microsoft.Security", "CA2141:TransparentMethodsMustNotSatisfyLinkDemandsFxCopRule", Justification = "System.Web.Mvc is SecurityTransparent and requires medium trust to run, so this downstream link demand is fine")]
+ public override void PreprocessDirective(string directiveName, IDictionary attributes)
+ {
+ base.PreprocessDirective(directiveName, attributes);
+
+ Type baseType;
+ if (_directiveBaseTypeMappings.TryGetValue(directiveName, out baseType))
+ {
+ string inheritsAttribute = attributes["inherits"] as string;
+
+ // Since the ASP.NET page parser doesn't understand native generic syntax, we
+ // need to swap out whatever the user provided with the default base type for
+ // the given directive (page vs. control vs. master). We stash the old value
+ // and swap it back in inside the control builder. Our "is this generic?"
+ // check here really only works for C# and VB.NET, since we're checking for
+ // < or ( in the type name.
+ //
+ // We only change generic directives, because doing so breaks back-compat
+ // for property setters on @Page, @Control, and @Master directives. The user
+ // can work around this breaking behavior by using a non-generic inherits
+ // directive, or by using the CLR syntax for generic type names.
+
+ if (inheritsAttribute != null && inheritsAttribute.IndexOfAny(new[] { '<', '(' }) > 0)
+ {
+ attributes["inherits"] = baseType.FullName;
+ _inherits = inheritsAttribute;
+ }
+ }
+ }
+
+ [SuppressMessage("Microsoft.Security", "CA2141:TransparentMethodsMustNotSatisfyLinkDemandsFxCopRule", Justification = "System.Web.Mvc is SecurityTransparent and requires medium trust to run, so this downstream link demand is fine")]
+ public override void ParseComplete(ControlBuilder rootBuilder)
+ {
+ base.ParseComplete(rootBuilder);
+
+ IMvcControlBuilder builder = rootBuilder as IMvcControlBuilder;
+ if (builder != null)
+ {
+ builder.Inherits = _inherits;
+ }
+ }
+
+ // Everything else in this class is unrelated to our 'inherits' handling.
+ // Since PageParserFilter blocks everything by default, we need to unblock it
+
+ public override bool AllowBaseType(Type baseType)
+ {
+ return true;
+ }
+
+ public override bool AllowControl(Type controlType, ControlBuilder builder)
+ {
+ return true;
+ }
+
+ public override bool AllowVirtualReference(string referenceVirtualPath, VirtualReferenceType referenceType)
+ {
+ return true;
+ }
+
+ public override bool AllowServerSideInclude(string includeVirtualPath)
+ {
+ return true;
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ViewUserControl.cs b/src/System.Web.Mvc/ViewUserControl.cs
new file mode 100644
index 00000000..a5f7e075
--- /dev/null
+++ b/src/System.Web.Mvc/ViewUserControl.cs
@@ -0,0 +1,213 @@
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.IO;
+using System.Web.Mvc.Properties;
+using System.Web.UI;
+
+namespace System.Web.Mvc
+{
+ [FileLevelControlBuilder(typeof(ViewUserControlControlBuilder))]
+ public class ViewUserControl : UserControl, IViewDataContainer
+ {
+ private AjaxHelper<object> _ajaxHelper;
+ private DynamicViewDataDictionary _dynamicViewData;
+ private HtmlHelper<object> _htmlHelper;
+ private ViewContext _viewContext;
+ private ViewDataDictionary _viewData;
+ private string _viewDataKey;
+
+ public AjaxHelper<object> Ajax
+ {
+ get
+ {
+ if (_ajaxHelper == null)
+ {
+ _ajaxHelper = new AjaxHelper<object>(ViewContext, this);
+ }
+ return _ajaxHelper;
+ }
+ }
+
+ public HtmlHelper<object> Html
+ {
+ get
+ {
+ if (_htmlHelper == null)
+ {
+ _htmlHelper = new HtmlHelper<object>(ViewContext, this);
+ }
+ return _htmlHelper;
+ }
+ }
+
+ public object Model
+ {
+ get { return ViewData.Model; }
+ }
+
+ public TempDataDictionary TempData
+ {
+ get { return ViewPage.TempData; }
+ }
+
+ public UrlHelper Url
+ {
+ get { return ViewPage.Url; }
+ }
+
+ public dynamic ViewBag
+ {
+ get
+ {
+ if (_dynamicViewData == null)
+ {
+ _dynamicViewData = new DynamicViewDataDictionary(() => ViewData);
+ }
+ return _dynamicViewData;
+ }
+ }
+
+ [Browsable(false)]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public ViewContext ViewContext
+ {
+ get { return _viewContext ?? ViewPage.ViewContext; }
+ set { _viewContext = value; }
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This is the mechanism by which the ViewUserControl gets its ViewDataDictionary object.")]
+ [Browsable(false)]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public ViewDataDictionary ViewData
+ {
+ get
+ {
+ EnsureViewData();
+ return _viewData;
+ }
+ set { SetViewData(value); }
+ }
+
+ [DefaultValue("")]
+ public string ViewDataKey
+ {
+ get { return _viewDataKey ?? String.Empty; }
+ set { _viewDataKey = value; }
+ }
+
+ internal ViewPage ViewPage
+ {
+ get
+ {
+ ViewPage viewPage = Page as ViewPage;
+ if (viewPage == null)
+ {
+ throw new InvalidOperationException(MvcResources.ViewUserControl_RequiresViewPage);
+ }
+ return viewPage;
+ }
+ }
+
+ public HtmlTextWriter Writer
+ {
+ get { return ViewPage.Writer; }
+ }
+
+ protected virtual void SetViewData(ViewDataDictionary viewData)
+ {
+ _viewData = viewData;
+ }
+
+ protected void EnsureViewData()
+ {
+ if (_viewData != null)
+ {
+ return;
+ }
+
+ // Get the ViewData for this ViewUserControl, optionally using the specified ViewDataKey
+ IViewDataContainer vdc = GetViewDataContainer(this);
+ if (vdc == null)
+ {
+ throw new InvalidOperationException(
+ String.Format(
+ CultureInfo.CurrentCulture,
+ MvcResources.ViewUserControl_RequiresViewDataProvider,
+ AppRelativeVirtualPath));
+ }
+
+ ViewDataDictionary myViewData = vdc.ViewData;
+
+ // If we have a ViewDataKey, try to extract the ViewData from the dictionary, otherwise
+ // return the container's ViewData.
+ if (!String.IsNullOrEmpty(ViewDataKey))
+ {
+ object target = myViewData.Eval(ViewDataKey);
+ myViewData = target as ViewDataDictionary ?? new ViewDataDictionary(myViewData) { Model = target };
+ }
+
+ SetViewData(myViewData);
+ }
+
+ private static IViewDataContainer GetViewDataContainer(Control control)
+ {
+ // Walk up the control hierarchy until we find someone that implements IViewDataContainer
+ while (control != null)
+ {
+ control = control.Parent;
+ IViewDataContainer vdc = control as IViewDataContainer;
+ if (vdc != null)
+ {
+ return vdc;
+ }
+ }
+ return null;
+ }
+
+ public virtual void RenderView(ViewContext viewContext)
+ {
+ using (ViewUserControlContainerPage containerPage = new ViewUserControlContainerPage(this))
+ {
+ RenderViewAndRestoreContentType(containerPage, viewContext);
+ }
+ }
+
+ internal static void RenderViewAndRestoreContentType(ViewPage containerPage, ViewContext viewContext)
+ {
+ // We need to restore the Content-Type since Page.SetIntrinsics() will reset it. It's not possible
+ // to work around the call to SetIntrinsics() since the control's render method requires the
+ // containing page's Response property to be non-null, and SetIntrinsics() is the only way to set
+ // this.
+ string savedContentType = viewContext.HttpContext.Response.ContentType;
+ containerPage.RenderView(viewContext);
+ viewContext.HttpContext.Response.ContentType = savedContentType;
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "textWriter", Justification = "This method existed in MVC 1.0 and has been deprecated.")]
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This method existed in MVC 1.0 and has been deprecated.")]
+ [Obsolete("The TextWriter is now provided by the ViewContext object passed to the RenderView method.", true /* error */)]
+ public void SetTextWriter(TextWriter textWriter)
+ {
+ // this is now a no-op
+ }
+
+ private sealed class ViewUserControlContainerPage : ViewPage
+ {
+ private readonly ViewUserControl _userControl;
+
+ public ViewUserControlContainerPage(ViewUserControl userControl)
+ {
+ _userControl = userControl;
+ }
+
+ public override void ProcessRequest(HttpContext context)
+ {
+ _userControl.ID = NextId();
+ Controls.Add(_userControl);
+
+ base.ProcessRequest(context);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ViewUserControlControlBuilder.cs b/src/System.Web.Mvc/ViewUserControlControlBuilder.cs
new file mode 100644
index 00000000..e09e2982
--- /dev/null
+++ b/src/System.Web.Mvc/ViewUserControlControlBuilder.cs
@@ -0,0 +1,18 @@
+using System.CodeDom;
+using System.Web.UI;
+
+namespace System.Web.Mvc
+{
+ internal sealed class ViewUserControlControlBuilder : FileLevelUserControlBuilder, IMvcControlBuilder
+ {
+ public string Inherits { get; set; }
+
+ public override void ProcessGeneratedCode(CodeCompileUnit codeCompileUnit, CodeTypeDeclaration baseType, CodeTypeDeclaration derivedType, CodeMemberMethod buildMethod, CodeMemberMethod dataBindingMethod)
+ {
+ if (!String.IsNullOrWhiteSpace(Inherits))
+ {
+ derivedType.BaseTypes[0] = new CodeTypeReference(Inherits);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/ViewUserControl`1.cs b/src/System.Web.Mvc/ViewUserControl`1.cs
new file mode 100644
index 00000000..357888f4
--- /dev/null
+++ b/src/System.Web.Mvc/ViewUserControl`1.cs
@@ -0,0 +1,58 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Web.Mvc
+{
+ public class ViewUserControl<TModel> : ViewUserControl
+ {
+ private AjaxHelper<TModel> _ajaxHelper;
+ private HtmlHelper<TModel> _htmlHelper;
+ private ViewDataDictionary<TModel> _viewData;
+
+ public new AjaxHelper<TModel> Ajax
+ {
+ get
+ {
+ if (_ajaxHelper == null)
+ {
+ _ajaxHelper = new AjaxHelper<TModel>(ViewContext, this);
+ }
+ return _ajaxHelper;
+ }
+ }
+
+ public new HtmlHelper<TModel> Html
+ {
+ get
+ {
+ if (_htmlHelper == null)
+ {
+ _htmlHelper = new HtmlHelper<TModel>(ViewContext, this);
+ }
+ return _htmlHelper;
+ }
+ }
+
+ public new TModel Model
+ {
+ get { return ViewData.Model; }
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This is settable for unit testing purposes")]
+ public new ViewDataDictionary<TModel> ViewData
+ {
+ get
+ {
+ EnsureViewData();
+ return _viewData;
+ }
+ set { SetViewData(value); }
+ }
+
+ protected override void SetViewData(ViewDataDictionary viewData)
+ {
+ _viewData = new ViewDataDictionary<TModel>(viewData);
+
+ base.SetViewData(_viewData);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/VirtualPathProviderViewEngine.cs b/src/System.Web.Mvc/VirtualPathProviderViewEngine.cs
new file mode 100644
index 00000000..9cf5d4d4
--- /dev/null
+++ b/src/System.Web.Mvc/VirtualPathProviderViewEngine.cs
@@ -0,0 +1,345 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using System.Web.Hosting;
+using System.Web.Mvc.Properties;
+using System.Web.WebPages;
+
+namespace System.Web.Mvc
+{
+ public abstract class VirtualPathProviderViewEngine : IViewEngine
+ {
+ // format is ":ViewCacheEntry:{cacheType}:{prefix}:{name}:{controllerName}:{areaName}:"
+ private const string CacheKeyFormat = ":ViewCacheEntry:{0}:{1}:{2}:{3}:{4}:";
+ private const string CacheKeyPrefixMaster = "Master";
+ private const string CacheKeyPrefixPartial = "Partial";
+ private const string CacheKeyPrefixView = "View";
+ private static readonly string[] _emptyLocations = new string[0];
+ private DisplayModeProvider _displayModeProvider;
+
+ private VirtualPathProvider _vpp;
+ internal Func<string, string> GetExtensionThunk = VirtualPathUtility.GetExtension;
+
+ protected VirtualPathProviderViewEngine()
+ {
+ if (HttpContext.Current == null || HttpContext.Current.IsDebuggingEnabled)
+ {
+ ViewLocationCache = DefaultViewLocationCache.Null;
+ }
+ else
+ {
+ ViewLocationCache = new DefaultViewLocationCache();
+ }
+ }
+
+ [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "This is a shipped API")]
+ public string[] AreaMasterLocationFormats { get; set; }
+
+ [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "This is a shipped API")]
+ public string[] AreaPartialViewLocationFormats { get; set; }
+
+ [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "This is a shipped API")]
+ public string[] AreaViewLocationFormats { get; set; }
+
+ [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "This is a shipped API")]
+ public string[] FileExtensions { get; set; }
+
+ [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "This is a shipped API")]
+ public string[] MasterLocationFormats { get; set; }
+
+ [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "This is a shipped API")]
+ public string[] PartialViewLocationFormats { get; set; }
+
+ public IViewLocationCache ViewLocationCache { get; set; }
+
+ [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "This is a shipped API")]
+ public string[] ViewLocationFormats { get; set; }
+
+ protected VirtualPathProvider VirtualPathProvider
+ {
+ get
+ {
+ if (_vpp == null)
+ {
+ _vpp = HostingEnvironment.VirtualPathProvider;
+ }
+ return _vpp;
+ }
+ set { _vpp = value; }
+ }
+
+ protected internal DisplayModeProvider DisplayModeProvider
+ {
+ get { return _displayModeProvider ?? DisplayModeProvider.Instance; }
+ set { _displayModeProvider = value; }
+ }
+
+ private string CreateCacheKey(string prefix, string name, string controllerName, string areaName)
+ {
+ return String.Format(CultureInfo.InvariantCulture, CacheKeyFormat,
+ GetType().AssemblyQualifiedName, prefix, name, controllerName, areaName);
+ }
+
+ internal static string AppendDisplayModeToCacheKey(string cacheKey, string displayMode)
+ {
+ // key format is ":ViewCacheEntry:{cacheType}:{prefix}:{name}:{controllerName}:{areaName}:"
+ // so append "{displayMode}:" to the key
+ return cacheKey + displayMode + ":";
+ }
+
+ protected abstract IView CreatePartialView(ControllerContext controllerContext, string partialPath);
+
+ protected abstract IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath);
+
+ protected virtual bool FileExists(ControllerContext controllerContext, string virtualPath)
+ {
+ return VirtualPathProvider.FileExists(virtualPath);
+ }
+
+ public virtual ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
+ {
+ if (controllerContext == null)
+ {
+ throw new ArgumentNullException("controllerContext");
+ }
+ if (String.IsNullOrEmpty(partialViewName))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "partialViewName");
+ }
+
+ string[] searched;
+ string controllerName = controllerContext.RouteData.GetRequiredString("controller");
+ string partialPath = GetPath(controllerContext, PartialViewLocationFormats, AreaPartialViewLocationFormats, "PartialViewLocationFormats", partialViewName, controllerName, CacheKeyPrefixPartial, useCache, out searched);
+
+ if (String.IsNullOrEmpty(partialPath))
+ {
+ return new ViewEngineResult(searched);
+ }
+
+ return new ViewEngineResult(CreatePartialView(controllerContext, partialPath), this);
+ }
+
+ public virtual ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
+ {
+ if (controllerContext == null)
+ {
+ throw new ArgumentNullException("controllerContext");
+ }
+ if (String.IsNullOrEmpty(viewName))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "viewName");
+ }
+
+ string[] viewLocationsSearched;
+ string[] masterLocationsSearched;
+
+ string controllerName = controllerContext.RouteData.GetRequiredString("controller");
+ string viewPath = GetPath(controllerContext, ViewLocationFormats, AreaViewLocationFormats, "ViewLocationFormats", viewName, controllerName, CacheKeyPrefixView, useCache, out viewLocationsSearched);
+ string masterPath = GetPath(controllerContext, MasterLocationFormats, AreaMasterLocationFormats, "MasterLocationFormats", masterName, controllerName, CacheKeyPrefixMaster, useCache, out masterLocationsSearched);
+
+ if (String.IsNullOrEmpty(viewPath) || (String.IsNullOrEmpty(masterPath) && !String.IsNullOrEmpty(masterName)))
+ {
+ return new ViewEngineResult(viewLocationsSearched.Union(masterLocationsSearched));
+ }
+
+ return new ViewEngineResult(CreateView(controllerContext, viewPath, masterPath), this);
+ }
+
+ private string GetPath(ControllerContext controllerContext, string[] locations, string[] areaLocations, string locationsPropertyName, string name, string controllerName, string cacheKeyPrefix, bool useCache, out string[] searchedLocations)
+ {
+ searchedLocations = _emptyLocations;
+
+ if (String.IsNullOrEmpty(name))
+ {
+ return String.Empty;
+ }
+
+ string areaName = AreaHelpers.GetAreaName(controllerContext.RouteData);
+ bool usingAreas = !String.IsNullOrEmpty(areaName);
+ List<ViewLocation> viewLocations = GetViewLocations(locations, (usingAreas) ? areaLocations : null);
+
+ if (viewLocations.Count == 0)
+ {
+ throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture,
+ MvcResources.Common_PropertyCannotBeNullOrEmpty, locationsPropertyName));
+ }
+
+ bool nameRepresentsPath = IsSpecificPath(name);
+ string cacheKey = CreateCacheKey(cacheKeyPrefix, name, (nameRepresentsPath) ? String.Empty : controllerName, areaName);
+
+ if (useCache)
+ {
+ // Only look at cached display modes that can handle the context.
+ IEnumerable<IDisplayMode> possibleDisplayModes = DisplayModeProvider.GetAvailableDisplayModesForContext(controllerContext.HttpContext, controllerContext.DisplayMode);
+ foreach (IDisplayMode displayMode in possibleDisplayModes)
+ {
+ string cachedLocation = ViewLocationCache.GetViewLocation(controllerContext.HttpContext, AppendDisplayModeToCacheKey(cacheKey, displayMode.DisplayModeId));
+
+ if (cachedLocation != null)
+ {
+ if (controllerContext.DisplayMode == null)
+ {
+ controllerContext.DisplayMode = displayMode;
+ }
+
+ return cachedLocation;
+ }
+ }
+
+ // GetPath is called again without using the cache.
+ return null;
+ }
+ else
+ {
+ return nameRepresentsPath
+ ? GetPathFromSpecificName(controllerContext, name, cacheKey, ref searchedLocations)
+ : GetPathFromGeneralName(controllerContext, viewLocations, name, controllerName, areaName, cacheKey, ref searchedLocations);
+ }
+ }
+
+ private string GetPathFromGeneralName(ControllerContext controllerContext, List<ViewLocation> locations, string name, string controllerName, string areaName, string cacheKey, ref string[] searchedLocations)
+ {
+ string result = String.Empty;
+ searchedLocations = new string[locations.Count];
+
+ for (int i = 0; i < locations.Count; i++)
+ {
+ ViewLocation location = locations[i];
+ string virtualPath = location.Format(name, controllerName, areaName);
+ DisplayInfo virtualPathDisplayInfo = DisplayModeProvider.GetDisplayInfoForVirtualPath(virtualPath, controllerContext.HttpContext, path => FileExists(controllerContext, path), controllerContext.DisplayMode);
+
+ if (virtualPathDisplayInfo != null)
+ {
+ string resolvedVirtualPath = virtualPathDisplayInfo.FilePath;
+
+ searchedLocations = _emptyLocations;
+ result = resolvedVirtualPath;
+ ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, AppendDisplayModeToCacheKey(cacheKey, virtualPathDisplayInfo.DisplayMode.DisplayModeId), result);
+
+ if (controllerContext.DisplayMode == null)
+ {
+ controllerContext.DisplayMode = virtualPathDisplayInfo.DisplayMode;
+ }
+
+ // Populate the cache with the existing paths returned by all display modes.
+ // Since we currently don't keep track of cache misses, if we cache view.aspx on a request from a standard browser
+ // we don't want a cache hit for view.aspx from a mobile browser so we populate the cache with view.Mobile.aspx.
+ IEnumerable<IDisplayMode> allDisplayModes = DisplayModeProvider.Modes;
+ foreach (IDisplayMode displayMode in allDisplayModes)
+ {
+ if (displayMode.DisplayModeId != virtualPathDisplayInfo.DisplayMode.DisplayModeId)
+ {
+ DisplayInfo displayInfoToCache = displayMode.GetDisplayInfo(controllerContext.HttpContext, virtualPath, virtualPathExists: path => FileExists(controllerContext, path));
+
+ if (displayInfoToCache != null && displayInfoToCache.FilePath != null)
+ {
+ ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, AppendDisplayModeToCacheKey(cacheKey, displayInfoToCache.DisplayMode.DisplayModeId), displayInfoToCache.FilePath);
+ }
+ }
+ }
+ break;
+ }
+
+ searchedLocations[i] = virtualPath;
+ }
+
+ return result;
+ }
+
+ private string GetPathFromSpecificName(ControllerContext controllerContext, string name, string cacheKey, ref string[] searchedLocations)
+ {
+ string result = name;
+
+ if (!(FilePathIsSupported(name) && FileExists(controllerContext, name)))
+ {
+ result = String.Empty;
+ searchedLocations = new[] { name };
+ }
+
+ ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, cacheKey, result);
+ return result;
+ }
+
+ private bool FilePathIsSupported(string virtualPath)
+ {
+ if (FileExtensions == null)
+ {
+ // legacy behavior for custom ViewEngine that might not set the FileExtensions property
+ return true;
+ }
+ else
+ {
+ // get rid of the '.' because the FileExtensions property expects extensions withouth a dot.
+ string extension = GetExtensionThunk(virtualPath).TrimStart('.');
+ return FileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
+ }
+ }
+
+ private static List<ViewLocation> GetViewLocations(string[] viewLocationFormats, string[] areaViewLocationFormats)
+ {
+ List<ViewLocation> allLocations = new List<ViewLocation>();
+
+ if (areaViewLocationFormats != null)
+ {
+ foreach (string areaViewLocationFormat in areaViewLocationFormats)
+ {
+ allLocations.Add(new AreaAwareViewLocation(areaViewLocationFormat));
+ }
+ }
+
+ if (viewLocationFormats != null)
+ {
+ foreach (string viewLocationFormat in viewLocationFormats)
+ {
+ allLocations.Add(new ViewLocation(viewLocationFormat));
+ }
+ }
+
+ return allLocations;
+ }
+
+ private static bool IsSpecificPath(string name)
+ {
+ char c = name[0];
+ return (c == '~' || c == '/');
+ }
+
+ public virtual void ReleaseView(ControllerContext controllerContext, IView view)
+ {
+ IDisposable disposable = view as IDisposable;
+ if (disposable != null)
+ {
+ disposable.Dispose();
+ }
+ }
+
+ private class AreaAwareViewLocation : ViewLocation
+ {
+ public AreaAwareViewLocation(string virtualPathFormatString)
+ : base(virtualPathFormatString)
+ {
+ }
+
+ public override string Format(string viewName, string controllerName, string areaName)
+ {
+ return String.Format(CultureInfo.InvariantCulture, _virtualPathFormatString, viewName, controllerName, areaName);
+ }
+ }
+
+ private class ViewLocation
+ {
+ protected string _virtualPathFormatString;
+
+ public ViewLocation(string virtualPathFormatString)
+ {
+ _virtualPathFormatString = virtualPathFormatString;
+ }
+
+ public virtual string Format(string viewName, string controllerName, string areaName)
+ {
+ return String.Format(CultureInfo.InvariantCulture, _virtualPathFormatString, viewName, controllerName);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/WebFormView.cs b/src/System.Web.Mvc/WebFormView.cs
new file mode 100644
index 00000000..d8651656
--- /dev/null
+++ b/src/System.Web.Mvc/WebFormView.cs
@@ -0,0 +1,72 @@
+using System.Globalization;
+using System.IO;
+using System.Web.Mvc.Properties;
+
+namespace System.Web.Mvc
+{
+ public class WebFormView : BuildManagerCompiledView
+ {
+ public WebFormView(ControllerContext controllerContext, string viewPath)
+ : this(controllerContext, viewPath, null, null)
+ {
+ }
+
+ public WebFormView(ControllerContext controllerContext, string viewPath, string masterPath)
+ : this(controllerContext, viewPath, masterPath, null)
+ {
+ }
+
+ public WebFormView(ControllerContext controllerContext, string viewPath, string masterPath, IViewPageActivator viewPageActivator)
+ : base(controllerContext, viewPath, viewPageActivator)
+ {
+ MasterPath = masterPath ?? String.Empty;
+ }
+
+ public string MasterPath { get; private set; }
+
+ protected override void RenderView(ViewContext viewContext, TextWriter writer, object instance)
+ {
+ ViewPage viewPage = instance as ViewPage;
+ if (viewPage != null)
+ {
+ RenderViewPage(viewContext, viewPage);
+ return;
+ }
+
+ ViewUserControl viewUserControl = instance as ViewUserControl;
+ if (viewUserControl != null)
+ {
+ RenderViewUserControl(viewContext, viewUserControl);
+ return;
+ }
+
+ throw new InvalidOperationException(
+ String.Format(
+ CultureInfo.CurrentCulture,
+ MvcResources.WebFormViewEngine_WrongViewBase,
+ ViewPath));
+ }
+
+ private void RenderViewPage(ViewContext context, ViewPage page)
+ {
+ if (!String.IsNullOrEmpty(MasterPath))
+ {
+ page.MasterLocation = MasterPath;
+ }
+
+ page.ViewData = context.ViewData;
+ page.RenderView(context);
+ }
+
+ private void RenderViewUserControl(ViewContext context, ViewUserControl control)
+ {
+ if (!String.IsNullOrEmpty(MasterPath))
+ {
+ throw new InvalidOperationException(MvcResources.WebFormViewEngine_UserControlCannotHaveMaster);
+ }
+
+ control.ViewData = context.ViewData;
+ control.RenderView(context);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/WebFormViewEngine.cs b/src/System.Web.Mvc/WebFormViewEngine.cs
new file mode 100644
index 00000000..2827e736
--- /dev/null
+++ b/src/System.Web.Mvc/WebFormViewEngine.cs
@@ -0,0 +1,62 @@
+namespace System.Web.Mvc
+{
+ public class WebFormViewEngine : BuildManagerViewEngine
+ {
+ public WebFormViewEngine()
+ : this(null)
+ {
+ }
+
+ public WebFormViewEngine(IViewPageActivator viewPageActivator)
+ : base(viewPageActivator)
+ {
+ MasterLocationFormats = new[]
+ {
+ "~/Views/{1}/{0}.master",
+ "~/Views/Shared/{0}.master"
+ };
+
+ AreaMasterLocationFormats = new[]
+ {
+ "~/Areas/{2}/Views/{1}/{0}.master",
+ "~/Areas/{2}/Views/Shared/{0}.master",
+ };
+
+ ViewLocationFormats = new[]
+ {
+ "~/Views/{1}/{0}.aspx",
+ "~/Views/{1}/{0}.ascx",
+ "~/Views/Shared/{0}.aspx",
+ "~/Views/Shared/{0}.ascx"
+ };
+
+ AreaViewLocationFormats = new[]
+ {
+ "~/Areas/{2}/Views/{1}/{0}.aspx",
+ "~/Areas/{2}/Views/{1}/{0}.ascx",
+ "~/Areas/{2}/Views/Shared/{0}.aspx",
+ "~/Areas/{2}/Views/Shared/{0}.ascx",
+ };
+
+ PartialViewLocationFormats = ViewLocationFormats;
+ AreaPartialViewLocationFormats = AreaViewLocationFormats;
+
+ FileExtensions = new[]
+ {
+ "aspx",
+ "ascx",
+ "master",
+ };
+ }
+
+ protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
+ {
+ return new WebFormView(controllerContext, partialPath, null, ViewPageActivator);
+ }
+
+ protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
+ {
+ return new WebFormView(controllerContext, viewPath, masterPath, ViewPageActivator);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/WebViewPage.cs b/src/System.Web.Mvc/WebViewPage.cs
new file mode 100644
index 00000000..3799c602
--- /dev/null
+++ b/src/System.Web.Mvc/WebViewPage.cs
@@ -0,0 +1,115 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.IO;
+using System.Web.Mvc.Properties;
+using System.Web.WebPages;
+
+namespace System.Web.Mvc
+{
+ public abstract class WebViewPage : WebPageBase, IViewDataContainer, IViewStartPageChild
+ {
+ private ViewDataDictionary _viewData;
+ private DynamicViewDataDictionary _dynamicViewData;
+ private HttpContextBase _context;
+
+ public AjaxHelper<object> Ajax { get; set; }
+
+ public override HttpContextBase Context
+ {
+ // REVIEW why are we forced to override this?
+ get { return _context ?? ViewContext.HttpContext; }
+ set { _context = value; }
+ }
+
+ public HtmlHelper<object> Html { get; set; }
+
+ public object Model
+ {
+ get { return ViewData.Model; }
+ }
+
+ internal string OverridenLayoutPath { get; set; }
+
+ public TempDataDictionary TempData
+ {
+ get { return ViewContext.TempData; }
+ }
+
+ public UrlHelper Url { get; set; }
+
+ public dynamic ViewBag
+ {
+ get
+ {
+ if (_dynamicViewData == null)
+ {
+ _dynamicViewData = new DynamicViewDataDictionary(() => ViewData);
+ }
+ return _dynamicViewData;
+ }
+ }
+
+ public ViewContext ViewContext { get; set; }
+
+ [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This is the mechanism by which the ViewPage gets its ViewDataDictionary object.")]
+ public ViewDataDictionary ViewData
+ {
+ get
+ {
+ if (_viewData == null)
+ {
+ SetViewData(new ViewDataDictionary());
+ }
+ return _viewData;
+ }
+ set { SetViewData(value); }
+ }
+
+ protected override void ConfigurePage(WebPageBase parentPage)
+ {
+ var baseViewPage = parentPage as WebViewPage;
+ if (baseViewPage == null)
+ {
+ // TODO : review if this check is even necessary.
+ // When this method is called by the framework parentPage should already be an instance of WebViewPage
+ // Need to review what happens if this method gets called in Plan9 pointing at an MVC view
+ throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, MvcResources.CshtmlView_WrongViewBase, parentPage.VirtualPath));
+ }
+
+ // Set ViewContext and ViewData here so that the layout page inherits ViewData from the main page
+ ViewContext = baseViewPage.ViewContext;
+ ViewData = baseViewPage.ViewData;
+ InitHelpers();
+ }
+
+ public override void ExecutePageHierarchy()
+ {
+ // Change the Writer so that things like Html.BeginForm work correctly
+ TextWriter oldWriter = ViewContext.Writer;
+ ViewContext.Writer = Output;
+
+ base.ExecutePageHierarchy();
+
+ // Overwrite LayoutPage so that returning a view with a custom master page works.
+ if (!String.IsNullOrEmpty(OverridenLayoutPath))
+ {
+ Layout = OverridenLayoutPath;
+ }
+
+ // Restore the old View Context Writer
+ ViewContext.Writer = oldWriter;
+ }
+
+ public virtual void InitHelpers()
+ {
+ Ajax = new AjaxHelper<object>(ViewContext, this);
+ Html = new HtmlHelper<object>(ViewContext, this);
+ Url = new UrlHelper(ViewContext.RequestContext);
+ }
+
+ protected virtual void SetViewData(ViewDataDictionary viewData)
+ {
+ _viewData = viewData;
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/WebViewPage`1.cs b/src/System.Web.Mvc/WebViewPage`1.cs
new file mode 100644
index 00000000..1b25f6b4
--- /dev/null
+++ b/src/System.Web.Mvc/WebViewPage`1.cs
@@ -0,0 +1,47 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Web.Mvc
+{
+ public abstract class WebViewPage<TModel> : WebViewPage
+ {
+ private ViewDataDictionary<TModel> _viewData;
+
+ public new AjaxHelper<TModel> Ajax { get; set; }
+
+ public new HtmlHelper<TModel> Html { get; set; }
+
+ public new TModel Model
+ {
+ get { return ViewData.Model; }
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This is the mechanism by which the ViewPage gets its ViewDataDictionary object.")]
+ public new ViewDataDictionary<TModel> ViewData
+ {
+ get
+ {
+ if (_viewData == null)
+ {
+ SetViewData(new ViewDataDictionary<TModel>());
+ }
+ return _viewData;
+ }
+ set { SetViewData(value); }
+ }
+
+ public override void InitHelpers()
+ {
+ base.InitHelpers();
+
+ Ajax = new AjaxHelper<TModel>(ViewContext, this);
+ Html = new HtmlHelper<TModel>(ViewContext, this);
+ }
+
+ protected override void SetViewData(ViewDataDictionary viewData)
+ {
+ _viewData = new ViewDataDictionary<TModel>(viewData);
+
+ base.SetViewData(_viewData);
+ }
+ }
+}
diff --git a/src/System.Web.Mvc/packages.config b/src/System.Web.Mvc/packages.config
new file mode 100644
index 00000000..f143a04f
--- /dev/null
+++ b/src/System.Web.Mvc/packages.config
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="Microsoft.Web.Infrastructure" version="1.0.0.0" />
+</packages> \ No newline at end of file
diff --git a/src/System.Web.Razor/CSharpRazorCodeLanguage.cs b/src/System.Web.Razor/CSharpRazorCodeLanguage.cs
new file mode 100644
index 00000000..592a1839
--- /dev/null
+++ b/src/System.Web.Razor/CSharpRazorCodeLanguage.cs
@@ -0,0 +1,46 @@
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser;
+using Microsoft.CSharp;
+
+namespace System.Web.Razor
+{
+ /// <summary>
+ /// Defines the C# Code Language for Razor
+ /// </summary>
+ public class CSharpRazorCodeLanguage : RazorCodeLanguage
+ {
+ private const string CSharpLanguageName = "csharp";
+
+ /// <summary>
+ /// Returns the name of the language: "csharp"
+ /// </summary>
+ public override string LanguageName
+ {
+ get { return CSharpLanguageName; }
+ }
+
+ /// <summary>
+ /// Returns the type of the CodeDOM provider for this language
+ /// </summary>
+ public override Type CodeDomProviderType
+ {
+ get { return typeof(CSharpCodeProvider); }
+ }
+
+ /// <summary>
+ /// Constructs a new instance of the code parser for this language
+ /// </summary>
+ public override ParserBase CreateCodeParser()
+ {
+ return new CSharpCodeParser();
+ }
+
+ /// <summary>
+ /// Constructs a new instance of the code generator for this language with the specified settings
+ /// </summary>
+ public override RazorCodeGenerator CreateCodeGenerator(string className, string rootNamespaceName, string sourceFileName, RazorEngineHost host)
+ {
+ return new CSharpRazorCodeGenerator(className, rootNamespaceName, sourceFileName, host);
+ }
+ }
+}
diff --git a/src/System.Web.Razor/DocumentParseCompleteEventArgs.cs b/src/System.Web.Razor/DocumentParseCompleteEventArgs.cs
new file mode 100644
index 00000000..e9caf922
--- /dev/null
+++ b/src/System.Web.Razor/DocumentParseCompleteEventArgs.cs
@@ -0,0 +1,25 @@
+using System.Web.Razor.Text;
+
+namespace System.Web.Razor
+{
+ /// <summary>
+ /// Arguments for the DocumentParseComplete event in RazorEditorParser
+ /// </summary>
+ public class DocumentParseCompleteEventArgs : EventArgs
+ {
+ /// <summary>
+ /// Indicates if the tree structure has actually changed since the previous reparse.
+ /// </summary>
+ public bool TreeStructureChanged { get; set; }
+
+ /// <summary>
+ /// The results of the code generation and parsing
+ /// </summary>
+ public GeneratorResults GeneratorResults { get; set; }
+
+ /// <summary>
+ /// The TextChange which triggered the reparse
+ /// </summary>
+ public TextChange SourceChange { get; set; }
+ }
+}
diff --git a/src/System.Web.Razor/Editor/AutoCompleteEditHandler.cs b/src/System.Web.Razor/Editor/AutoCompleteEditHandler.cs
new file mode 100644
index 00000000..a2315fd6
--- /dev/null
+++ b/src/System.Web.Razor/Editor/AutoCompleteEditHandler.cs
@@ -0,0 +1,61 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Web.Razor.Editor;
+using System.Web.Razor.Text;
+using System.Web.Razor.Tokenizer.Symbols;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.Razor.Parser.SyntaxTree
+{
+ public class AutoCompleteEditHandler : SpanEditHandler
+ {
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Func<T> is the recommended delegate type and requires this level of nesting.")]
+ public AutoCompleteEditHandler(Func<string, IEnumerable<ISymbol>> tokenizer)
+ : base(tokenizer)
+ {
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Func<T> is the recommended delegate type and requires this level of nesting.")]
+ public AutoCompleteEditHandler(Func<string, IEnumerable<ISymbol>> tokenizer, AcceptedCharacters accepted)
+ : base(tokenizer, accepted)
+ {
+ }
+
+ public bool AutoCompleteAtEndOfSpan { get; set; }
+ public string AutoCompleteString { get; set; }
+
+ protected override PartialParseResult CanAcceptChange(Span target, TextChange normalizedChange)
+ {
+ if (((AutoCompleteAtEndOfSpan && IsAtEndOfSpan(target, normalizedChange)) || IsAtEndOfFirstLine(target, normalizedChange)) &&
+ normalizedChange.IsInsert &&
+ ParserHelpers.IsNewLine(normalizedChange.NewText) &&
+ AutoCompleteString != null)
+ {
+ return PartialParseResult.Rejected | PartialParseResult.AutoCompleteBlock;
+ }
+ return PartialParseResult.Rejected;
+ }
+
+ public override string ToString()
+ {
+ return base.ToString() + ",AutoComplete:[" + (AutoCompleteString ?? "<null>") + "]" + (AutoCompleteAtEndOfSpan ? ";AtEnd" : ";AtEOL");
+ }
+
+ public override bool Equals(object obj)
+ {
+ AutoCompleteEditHandler other = obj as AutoCompleteEditHandler;
+ return base.Equals(obj) &&
+ other != null &&
+ String.Equals(other.AutoCompleteString, AutoCompleteString, StringComparison.Ordinal) &&
+ AutoCompleteAtEndOfSpan == other.AutoCompleteAtEndOfSpan;
+ }
+
+ public override int GetHashCode()
+ {
+ return HashCodeCombiner.Start()
+ .Add(base.GetHashCode())
+ .Add(AutoCompleteString)
+ .CombinedHash;
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Editor/BackgroundParseTask.cs b/src/System.Web.Razor/Editor/BackgroundParseTask.cs
new file mode 100644
index 00000000..de583259
--- /dev/null
+++ b/src/System.Web.Razor/Editor/BackgroundParseTask.cs
@@ -0,0 +1,110 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Razor.Text;
+
+namespace System.Web.Razor.Editor
+{
+ internal class BackgroundParseTask : IDisposable
+ {
+ private CancellationTokenSource _cancelSource = new CancellationTokenSource();
+ private GeneratorResults _results;
+
+ [SuppressMessage("Microsoft.WebAPI", "CR4002:DoNotConstructTaskInstances", Justification = "This rule is not applicable to this assembly.")]
+ private BackgroundParseTask(RazorTemplateEngine engine, string sourceFileName, TextChange change)
+ {
+ Change = change;
+ Engine = engine;
+ SourceFileName = sourceFileName;
+ InnerTask = new Task(() => Run(_cancelSource.Token), _cancelSource.Token);
+ }
+
+ public Task InnerTask { get; private set; }
+ public TextChange Change { get; private set; }
+ public RazorTemplateEngine Engine { get; private set; }
+ public string SourceFileName { get; private set; }
+
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "It is the caller's responsibility to dispose this object, which will dispose all members of this object")]
+ public static BackgroundParseTask StartNew(RazorTemplateEngine engine, string sourceFileName, TextChange change)
+ {
+ BackgroundParseTask task = new BackgroundParseTask(engine, sourceFileName, change);
+ task.Start();
+ return task;
+ }
+
+ public void Cancel()
+ {
+ _cancelSource.Cancel();
+ }
+
+ [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "This rule is not applicable to this assembly.")]
+ public void Start()
+ {
+ InnerTask.Start();
+ }
+
+ [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "This rule is not applicable to this assembly.")]
+ public BackgroundParseTask ContinueWith(Action<GeneratorResults, BackgroundParseTask> continuation)
+ {
+ InnerTask.ContinueWith(t => RunContinuation(t, continuation));
+ return this;
+ }
+
+ private void RunContinuation(Task completed, Action<GeneratorResults, BackgroundParseTask> continuation)
+ {
+ if (!completed.IsCanceled)
+ {
+ continuation(_results, this);
+ }
+ }
+
+ internal virtual void Run(CancellationToken cancelToken)
+ {
+ if (!cancelToken.IsCancellationRequested)
+ {
+ // Seek the buffer to the beginning
+ Change.NewBuffer.Position = 0;
+
+ try
+ {
+ _results = Engine.GenerateCode(Change.NewBuffer, className: null, rootNamespace: null, sourceFileName: SourceFileName, cancelToken: cancelToken);
+ }
+ catch (OperationCanceledException ex)
+ {
+ if (ex.CancellationToken == cancelToken)
+ {
+ // We've been cancelled, so just return.
+ return;
+ }
+ else
+ {
+ // Exception was thrown for some other reason...
+ throw;
+ }
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ }
+
+ [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "This rule is not applicable to this assembly.")]
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ if (InnerTask != null)
+ {
+ InnerTask.Dispose();
+ InnerTask = null;
+ }
+ if (_cancelSource != null)
+ {
+ _cancelSource.Dispose();
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Editor/EditResult.cs b/src/System.Web.Razor/Editor/EditResult.cs
new file mode 100644
index 00000000..8a563758
--- /dev/null
+++ b/src/System.Web.Razor/Editor/EditResult.cs
@@ -0,0 +1,16 @@
+using System.Web.Razor.Parser.SyntaxTree;
+
+namespace System.Web.Razor.Editor
+{
+ public class EditResult
+ {
+ public EditResult(PartialParseResult result, SpanBuilder editedSpan)
+ {
+ Result = result;
+ EditedSpan = editedSpan;
+ }
+
+ public PartialParseResult Result { get; set; }
+ public SpanBuilder EditedSpan { get; set; }
+ }
+}
diff --git a/src/System.Web.Razor/Editor/EditorHints.cs b/src/System.Web.Razor/Editor/EditorHints.cs
new file mode 100644
index 00000000..e78e3ec8
--- /dev/null
+++ b/src/System.Web.Razor/Editor/EditorHints.cs
@@ -0,0 +1,27 @@
+namespace System.Web.Razor.Editor
+{
+ /// <summary>
+ /// Used within <see cref="F:SpanEditHandler.EditorHints"/>.
+ /// </summary>
+ [Flags]
+ public enum EditorHints
+ {
+ /// <summary>
+ /// The default (Markup or Code) editor behavior for Statement completion should be used.
+ /// Editors can always use the default behavior, even if the span is labeled with a different CompletionType.
+ /// </summary>
+ None = 0, // 0000 0000
+
+ /// <summary>
+ /// Indicates that Virtual Path completion should be used for this span if the editor supports it.
+ /// Editors need not support this mode of completion, and will use the default (<see cref="F:EditorHints.None"/>) behavior
+ /// if they do not support it.
+ /// </summary>
+ VirtualPath = 1, // 0000 0001
+
+ /// <summary>
+ /// Indicates that this span's content contains the path to the layout page for this document.
+ /// </summary>
+ LayoutPage = 2, // 0000 0010
+ }
+}
diff --git a/src/System.Web.Razor/Editor/ImplicitExpressionEditHandler.cs b/src/System.Web.Razor/Editor/ImplicitExpressionEditHandler.cs
new file mode 100644
index 00000000..5eef4b2c
--- /dev/null
+++ b/src/System.Web.Razor/Editor/ImplicitExpressionEditHandler.cs
@@ -0,0 +1,234 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Text;
+using System.Web.Razor.Tokenizer.Symbols;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.Razor.Editor
+{
+ public class ImplicitExpressionEditHandler : SpanEditHandler
+ {
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Func<T> is the recommended delegate type and requires this level of nesting.")]
+ public ImplicitExpressionEditHandler(Func<string, IEnumerable<ISymbol>> tokenizer, ISet<string> keywords, bool acceptTrailingDot)
+ : base(tokenizer)
+ {
+ Initialize(keywords, acceptTrailingDot);
+ }
+
+ public bool AcceptTrailingDot { get; private set; }
+ public ISet<string> Keywords { get; private set; }
+
+ public override string ToString()
+ {
+ return String.Format(CultureInfo.InvariantCulture, "{0};ImplicitExpression[{1}];K{2}", base.ToString(), AcceptTrailingDot ? "ATD" : "RTD", Keywords.Count);
+ }
+
+ public override bool Equals(object obj)
+ {
+ ImplicitExpressionEditHandler other = obj as ImplicitExpressionEditHandler;
+ return other != null &&
+ base.Equals(other) &&
+ Keywords.SetEquals(other.Keywords) &&
+ AcceptTrailingDot == other.AcceptTrailingDot;
+ }
+
+ public override int GetHashCode()
+ {
+ return HashCodeCombiner.Start()
+ .Add(base.GetHashCode())
+ .Add(AcceptTrailingDot)
+ .Add(Keywords)
+ .CombinedHash;
+ }
+
+ protected override PartialParseResult CanAcceptChange(Span target, TextChange normalizedChange)
+ {
+ if (AcceptedCharacters == AcceptedCharacters.Any)
+ {
+ return PartialParseResult.Rejected;
+ }
+
+ if (IsAcceptableReplace(target, normalizedChange))
+ {
+ return HandleReplacement(target, normalizedChange);
+ }
+ int changeRelativePosition = normalizedChange.OldPosition - target.Start.AbsoluteIndex;
+
+ // Get the edit context
+ char? lastChar = null;
+ if (changeRelativePosition > 0 && target.Content.Length > 0)
+ {
+ lastChar = target.Content[changeRelativePosition - 1];
+ }
+
+ // Don't support 0->1 length edits
+ if (lastChar == null)
+ {
+ return PartialParseResult.Rejected;
+ }
+
+ // Only support insertions at the end of the span
+ if (IsAcceptableInsertion(target, normalizedChange))
+ {
+ // Handle the insertion
+ return HandleInsertion(target, lastChar.Value, normalizedChange);
+ }
+
+ if (IsAcceptableDeletion(target, normalizedChange))
+ {
+ return HandleDeletion(target, lastChar.Value, normalizedChange);
+ }
+
+ return PartialParseResult.Rejected;
+ }
+
+ private void Initialize(ISet<string> keywords, bool acceptTrailingDot)
+ {
+ Keywords = keywords ?? new HashSet<string>();
+ AcceptTrailingDot = acceptTrailingDot;
+ }
+
+ private static bool IsAcceptableReplace(Span target, TextChange change)
+ {
+ return IsEndReplace(target, change) ||
+ (change.IsReplace && RemainingIsWhitespace(target, change));
+ }
+
+ private static bool IsAcceptableDeletion(Span target, TextChange change)
+ {
+ return IsEndDeletion(target, change) ||
+ (change.IsDelete && RemainingIsWhitespace(target, change));
+ }
+
+ private static bool IsAcceptableInsertion(Span target, TextChange change)
+ {
+ return IsEndInsertion(target, change) ||
+ (change.IsInsert && RemainingIsWhitespace(target, change));
+ }
+
+ private static bool RemainingIsWhitespace(Span target, TextChange change)
+ {
+ int offset = (change.OldPosition - target.Start.AbsoluteIndex) + change.OldLength;
+ return String.IsNullOrWhiteSpace(target.Content.Substring(offset));
+ }
+
+ private PartialParseResult HandleReplacement(Span target, TextChange change)
+ {
+ // Special Case for IntelliSense commits.
+ // When IntelliSense commits, we get two changes (for example user typed "Date", then committed "DateTime" by pressing ".")
+ // 1. Insert "." at the end of this span
+ // 2. Replace the "Date." at the end of the span with "DateTime."
+ // We need partial parsing to accept case #2.
+ string oldText = GetOldText(target, change);
+
+ PartialParseResult result = PartialParseResult.Rejected;
+ if (EndsWithDot(oldText) && EndsWithDot(change.NewText))
+ {
+ result = PartialParseResult.Accepted;
+ if (!AcceptTrailingDot)
+ {
+ result |= PartialParseResult.Provisional;
+ }
+ }
+ return result;
+ }
+
+ private PartialParseResult HandleDeletion(Span target, char previousChar, TextChange change)
+ {
+ // What's left after deleting?
+ if (previousChar == '.')
+ {
+ return TryAcceptChange(target, change, PartialParseResult.Accepted | PartialParseResult.Provisional);
+ }
+ else if (ParserHelpers.IsIdentifierPart(previousChar))
+ {
+ return TryAcceptChange(target, change);
+ }
+ else
+ {
+ return PartialParseResult.Rejected;
+ }
+ }
+
+ private PartialParseResult HandleInsertion(Span target, char previousChar, TextChange change)
+ {
+ // What are we inserting after?
+ if (previousChar == '.')
+ {
+ return HandleInsertionAfterDot(target, change);
+ }
+ else if (ParserHelpers.IsIdentifierPart(previousChar) || previousChar == ')' || previousChar == ']')
+ {
+ return HandleInsertionAfterIdPart(target, change);
+ }
+ else
+ {
+ return PartialParseResult.Rejected;
+ }
+ }
+
+ private PartialParseResult HandleInsertionAfterIdPart(Span target, TextChange change)
+ {
+ // If the insertion is a full identifier part, accept it
+ if (ParserHelpers.IsIdentifier(change.NewText, requireIdentifierStart: false))
+ {
+ return TryAcceptChange(target, change);
+ }
+ else if (EndsWithDot(change.NewText))
+ {
+ // Accept it, possibly provisionally
+ PartialParseResult result = PartialParseResult.Accepted;
+ if (!AcceptTrailingDot)
+ {
+ result |= PartialParseResult.Provisional;
+ }
+ return TryAcceptChange(target, change, result);
+ }
+ else
+ {
+ return PartialParseResult.Rejected;
+ }
+ }
+
+ private static bool EndsWithDot(string content)
+ {
+ return (content.Length == 1 && content[0] == '.') ||
+ (content[content.Length - 1] == '.' &&
+ content.Take(content.Length - 1).All(ParserHelpers.IsIdentifierPart));
+ }
+
+ private PartialParseResult HandleInsertionAfterDot(Span target, TextChange change)
+ {
+ // If the insertion is a full identifier, accept it
+ if (ParserHelpers.IsIdentifier(change.NewText))
+ {
+ return TryAcceptChange(target, change);
+ }
+ return PartialParseResult.Rejected;
+ }
+
+ private PartialParseResult TryAcceptChange(Span target, TextChange change, PartialParseResult acceptResult = PartialParseResult.Accepted)
+ {
+ string content = change.ApplyChange(target);
+ if (StartsWithKeyword(content))
+ {
+ return PartialParseResult.Rejected | PartialParseResult.SpanContextChanged;
+ }
+
+ return acceptResult;
+ }
+
+ private bool StartsWithKeyword(string newContent)
+ {
+ using (StringReader reader = new StringReader(newContent))
+ {
+ return Keywords.Contains(reader.ReadWhile(ParserHelpers.IsIdentifierPart));
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Editor/SingleLineMarkupEditHandler.cs b/src/System.Web.Razor/Editor/SingleLineMarkupEditHandler.cs
new file mode 100644
index 00000000..5293f368
--- /dev/null
+++ b/src/System.Web.Razor/Editor/SingleLineMarkupEditHandler.cs
@@ -0,0 +1,22 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Tokenizer.Symbols;
+
+namespace System.Web.Razor.Editor
+{
+ public class SingleLineMarkupEditHandler : SpanEditHandler
+ {
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Func<T> is the recommended delegate type and requires this level of nesting.")]
+ public SingleLineMarkupEditHandler(Func<string, IEnumerable<ISymbol>> tokenizer)
+ : base(tokenizer)
+ {
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Func<T> is the recommended delegate type and requires this level of nesting.")]
+ public SingleLineMarkupEditHandler(Func<string, IEnumerable<ISymbol>> tokenizer, AcceptedCharacters accepted)
+ : base(tokenizer, accepted)
+ {
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Editor/SpanEditHandler.cs b/src/System.Web.Razor/Editor/SpanEditHandler.cs
new file mode 100644
index 00000000..d91edfea
--- /dev/null
+++ b/src/System.Web.Razor/Editor/SpanEditHandler.cs
@@ -0,0 +1,181 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Text;
+using System.Web.Razor.Tokenizer.Symbols;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.Razor.Editor
+{
+ // Manages edits to a span
+ public class SpanEditHandler
+ {
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Func<T> is the recommended delegate type and requires this level of nesting.")]
+ public SpanEditHandler(Func<string, IEnumerable<ISymbol>> tokenizer)
+ : this(tokenizer, AcceptedCharacters.Any)
+ {
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Func<T> is the recommended delegate type and requires this level of nesting.")]
+ public SpanEditHandler(Func<string, IEnumerable<ISymbol>> tokenizer, AcceptedCharacters accepted)
+ {
+ AcceptedCharacters = accepted;
+ Tokenizer = tokenizer;
+ }
+
+ public AcceptedCharacters AcceptedCharacters { get; set; }
+
+ /// <summary>
+ /// Provides a set of hints to editors which may be manipulating the document in which this span is located.
+ /// </summary>
+ public EditorHints EditorHints { get; set; }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Func<T> is the recommended delegate type and requires this level of nesting.")]
+ public Func<string, IEnumerable<ISymbol>> Tokenizer { get; set; }
+
+ public static SpanEditHandler CreateDefault()
+ {
+ return CreateDefault(s => Enumerable.Empty<ISymbol>());
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Func<T> is the recommended delegate type and requires this level of nesting.")]
+ public static SpanEditHandler CreateDefault(Func<string, IEnumerable<ISymbol>> tokenizer)
+ {
+ return new SpanEditHandler(tokenizer);
+ }
+
+ public virtual EditResult ApplyChange(Span target, TextChange change)
+ {
+ return ApplyChange(target, change, force: false);
+ }
+
+ public virtual EditResult ApplyChange(Span target, TextChange change, bool force)
+ {
+ PartialParseResult result = PartialParseResult.Accepted;
+ TextChange normalized = change.Normalize();
+ if (!force)
+ {
+ result = CanAcceptChange(target, normalized);
+ }
+
+ // If the change is accepted then apply the change
+ if (result.HasFlag(PartialParseResult.Accepted))
+ {
+ return new EditResult(result, UpdateSpan(target, normalized));
+ }
+ return new EditResult(result, new SpanBuilder(target));
+ }
+
+ public virtual bool OwnsChange(Span target, TextChange change)
+ {
+ int end = target.Start.AbsoluteIndex + target.Length;
+ int changeOldEnd = change.OldPosition + change.OldLength;
+ return change.OldPosition >= target.Start.AbsoluteIndex &&
+ (changeOldEnd < end || (changeOldEnd == end && AcceptedCharacters != AcceptedCharacters.None));
+ }
+
+ protected virtual PartialParseResult CanAcceptChange(Span target, TextChange normalizedChange)
+ {
+ return PartialParseResult.Rejected;
+ }
+
+ protected virtual SpanBuilder UpdateSpan(Span target, TextChange normalizedChange)
+ {
+ string newContent = normalizedChange.ApplyChange(target);
+ SpanBuilder newSpan = new SpanBuilder(target);
+ newSpan.ClearSymbols();
+ foreach (ISymbol sym in Tokenizer(newContent))
+ {
+ sym.OffsetStart(target.Start);
+ newSpan.Accept(sym);
+ }
+ if (target.Next != null)
+ {
+ SourceLocation newEnd = SourceLocationTracker.CalculateNewLocation(target.Start, newContent);
+ target.Next.ChangeStart(newEnd);
+ }
+ return newSpan;
+ }
+
+ protected internal static bool IsAtEndOfFirstLine(Span target, TextChange change)
+ {
+ int endOfFirstLine = target.Content.IndexOfAny(new char[] { (char)0x000d, (char)0x000a, (char)0x2028, (char)0x2029 });
+ return (endOfFirstLine == -1 || (change.OldPosition - target.Start.AbsoluteIndex) <= endOfFirstLine);
+ }
+
+ /// <summary>
+ /// Returns true if the specified change is an insertion of text at the end of this span.
+ /// </summary>
+ protected internal static bool IsEndInsertion(Span target, TextChange change)
+ {
+ return change.IsInsert && IsAtEndOfSpan(target, change);
+ }
+
+ /// <summary>
+ /// Returns true if the specified change is an insertion of text at the end of this span.
+ /// </summary>
+ protected internal static bool IsEndDeletion(Span target, TextChange change)
+ {
+ return change.IsDelete && IsAtEndOfSpan(target, change);
+ }
+
+ /// <summary>
+ /// Returns true if the specified change is a replacement of text at the end of this span.
+ /// </summary>
+ protected internal static bool IsEndReplace(Span target, TextChange change)
+ {
+ return change.IsReplace && IsAtEndOfSpan(target, change);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "This method should only be used on Spans")]
+ protected internal static bool IsAtEndOfSpan(Span target, TextChange change)
+ {
+ return (change.OldPosition + change.OldLength) == (target.Start.AbsoluteIndex + target.Length);
+ }
+
+ /// <summary>
+ /// Returns the old text referenced by the change.
+ /// </summary>
+ /// <remarks>
+ /// If the content has already been updated by applying the change, this data will be _invalid_
+ /// </remarks>
+ protected internal static string GetOldText(Span target, TextChange change)
+ {
+ return target.Content.Substring(change.OldPosition - target.Start.AbsoluteIndex, change.OldLength);
+ }
+
+ // Is the specified span to the right of this span and immediately adjacent?
+ internal static bool IsAdjacentOnRight(Span target, Span other)
+ {
+ return target.Start.AbsoluteIndex < other.Start.AbsoluteIndex && target.Start.AbsoluteIndex + target.Length == other.Start.AbsoluteIndex;
+ }
+
+ // Is the specified span to the left of this span and immediately adjacent?
+ internal static bool IsAdjacentOnLeft(Span target, Span other)
+ {
+ return other.Start.AbsoluteIndex < target.Start.AbsoluteIndex && other.Start.AbsoluteIndex + other.Length == target.Start.AbsoluteIndex;
+ }
+
+ public override string ToString()
+ {
+ return GetType().Name + ";Accepts:" + AcceptedCharacters + ((EditorHints == EditorHints.None) ? String.Empty : (";Hints: " + EditorHints.ToString()));
+ }
+
+ public override bool Equals(object obj)
+ {
+ SpanEditHandler other = obj as SpanEditHandler;
+ return other != null &&
+ AcceptedCharacters == other.AcceptedCharacters &&
+ EditorHints == other.EditorHints;
+ }
+
+ public override int GetHashCode()
+ {
+ return HashCodeCombiner.Start()
+ .Add(AcceptedCharacters)
+ .Add(EditorHints)
+ .CombinedHash;
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Generator/AddImportCodeGenerator.cs b/src/System.Web.Razor/Generator/AddImportCodeGenerator.cs
new file mode 100644
index 00000000..d39bae7b
--- /dev/null
+++ b/src/System.Web.Razor/Generator/AddImportCodeGenerator.cs
@@ -0,0 +1,66 @@
+using System.CodeDom;
+using System.Linq;
+using System.Web.Razor.Parser.SyntaxTree;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.Razor.Generator
+{
+ public class AddImportCodeGenerator : SpanCodeGenerator
+ {
+ public AddImportCodeGenerator(string ns, int namespaceKeywordLength)
+ {
+ Namespace = ns;
+ NamespaceKeywordLength = namespaceKeywordLength;
+ }
+
+ public string Namespace { get; private set; }
+ public int NamespaceKeywordLength { get; set; }
+
+ public override void GenerateCode(Span target, CodeGeneratorContext context)
+ {
+ // Try to find the namespace in the existing imports
+ string ns = Namespace;
+ if (!String.IsNullOrEmpty(ns) && Char.IsWhiteSpace(ns[0]))
+ {
+ ns = ns.Substring(1);
+ }
+
+ CodeNamespaceImport import = context.Namespace
+ .Imports
+ .OfType<CodeNamespaceImport>()
+ .Where(i => String.Equals(i.Namespace, ns.Trim(), StringComparison.Ordinal))
+ .FirstOrDefault();
+
+ if (import == null)
+ {
+ // It doesn't exist, create it
+ import = new CodeNamespaceImport(ns);
+ context.Namespace.Imports.Add(import);
+ }
+
+ // Attach our info to the existing/new import.
+ import.LinePragma = context.GenerateLinePragma(target);
+ }
+
+ public override string ToString()
+ {
+ return "Import:" + Namespace + ";KwdLen:" + NamespaceKeywordLength;
+ }
+
+ public override bool Equals(object obj)
+ {
+ AddImportCodeGenerator other = obj as AddImportCodeGenerator;
+ return other != null &&
+ String.Equals(Namespace, other.Namespace, StringComparison.Ordinal) &&
+ NamespaceKeywordLength == other.NamespaceKeywordLength;
+ }
+
+ public override int GetHashCode()
+ {
+ return HashCodeCombiner.Start()
+ .Add(Namespace)
+ .Add(NamespaceKeywordLength)
+ .CombinedHash;
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Generator/AttributeBlockCodeGenerator.cs b/src/System.Web.Razor/Generator/AttributeBlockCodeGenerator.cs
new file mode 100644
index 00000000..5ea1cd58
--- /dev/null
+++ b/src/System.Web.Razor/Generator/AttributeBlockCodeGenerator.cs
@@ -0,0 +1,88 @@
+using System.Globalization;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Text;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.Razor.Generator
+{
+ public class AttributeBlockCodeGenerator : BlockCodeGenerator
+ {
+ public AttributeBlockCodeGenerator(string name, LocationTagged<string> prefix, LocationTagged<string> suffix)
+ {
+ Name = name;
+ Prefix = prefix;
+ Suffix = suffix;
+ }
+
+ public string Name { get; private set; }
+ public LocationTagged<string> Prefix { get; private set; }
+ public LocationTagged<string> Suffix { get; private set; }
+
+ public override void GenerateStartBlockCode(Block target, CodeGeneratorContext context)
+ {
+ if (context.Host.DesignTimeMode)
+ {
+ return; // Don't generate anything!
+ }
+ context.FlushBufferedStatement();
+ context.AddStatement(context.BuildCodeString(cw =>
+ {
+ if (!String.IsNullOrEmpty(context.TargetWriterName))
+ {
+ cw.WriteStartMethodInvoke(context.Host.GeneratedClassContext.WriteAttributeToMethodName);
+ cw.WriteSnippet(context.TargetWriterName);
+ cw.WriteParameterSeparator();
+ }
+ else
+ {
+ cw.WriteStartMethodInvoke(context.Host.GeneratedClassContext.WriteAttributeMethodName);
+ }
+ cw.WriteStringLiteral(Name);
+ cw.WriteParameterSeparator();
+ cw.WriteLocationTaggedString(Prefix);
+ cw.WriteParameterSeparator();
+ cw.WriteLocationTaggedString(Suffix);
+
+ // In VB, we need a line continuation
+ cw.WriteLineContinuation();
+ }));
+ }
+
+ public override void GenerateEndBlockCode(Block target, CodeGeneratorContext context)
+ {
+ if (context.Host.DesignTimeMode)
+ {
+ return; // Don't generate anything!
+ }
+ context.FlushBufferedStatement();
+ context.AddStatement(context.BuildCodeString(cw =>
+ {
+ cw.WriteEndMethodInvoke();
+ cw.WriteEndStatement();
+ }));
+ }
+
+ public override string ToString()
+ {
+ return String.Format(CultureInfo.CurrentCulture, "Attr:{0},{1:F},{2:F}", Name, Prefix, Suffix);
+ }
+
+ public override bool Equals(object obj)
+ {
+ AttributeBlockCodeGenerator other = obj as AttributeBlockCodeGenerator;
+ return other != null &&
+ String.Equals(other.Name, Name, StringComparison.Ordinal) &&
+ Equals(other.Prefix, Prefix) &&
+ Equals(other.Suffix, Suffix);
+ }
+
+ public override int GetHashCode()
+ {
+ return HashCodeCombiner.Start()
+ .Add(Name)
+ .Add(Prefix)
+ .Add(Suffix)
+ .CombinedHash;
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Generator/BaseCodeWriter.cs b/src/System.Web.Razor/Generator/BaseCodeWriter.cs
new file mode 100644
index 00000000..1825d0b8
--- /dev/null
+++ b/src/System.Web.Razor/Generator/BaseCodeWriter.cs
@@ -0,0 +1,74 @@
+namespace System.Web.Razor.Generator
+{
+ internal abstract class BaseCodeWriter : CodeWriter
+ {
+ public override void WriteSnippet(string snippet)
+ {
+ InnerWriter.Write(snippet);
+ }
+
+ protected internal override void EmitStartMethodInvoke(string methodName)
+ {
+ EmitStartMethodInvoke(methodName, new string[0]);
+ }
+
+ protected internal override void EmitStartMethodInvoke(string methodName, params string[] genericArguments)
+ {
+ InnerWriter.Write(methodName);
+ if (genericArguments != null && genericArguments.Length > 0)
+ {
+ WriteStartGenerics();
+ for (int i = 0; i < genericArguments.Length; i++)
+ {
+ if (i > 0)
+ {
+ WriteParameterSeparator();
+ }
+ WriteSnippet(genericArguments[i]);
+ }
+ WriteEndGenerics();
+ }
+
+ InnerWriter.Write("(");
+ }
+
+ protected internal override void EmitEndMethodInvoke()
+ {
+ InnerWriter.Write(")");
+ }
+
+ protected internal override void EmitEndConstructor()
+ {
+ InnerWriter.Write(")");
+ }
+
+ protected internal override void EmitEndLambdaExpression()
+ {
+ }
+
+ public override void WriteParameterSeparator()
+ {
+ InnerWriter.Write(", ");
+ }
+
+ protected internal void WriteCommaSeparatedList<T>(T[] items, Action<T> writeItemAction)
+ {
+ for (int i = 0; i < items.Length; i++)
+ {
+ if (i > 0)
+ {
+ InnerWriter.Write(", ");
+ }
+ writeItemAction(items[i]);
+ }
+ }
+
+ protected internal virtual void WriteStartGenerics()
+ {
+ }
+
+ protected internal virtual void WriteEndGenerics()
+ {
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Generator/BlockCodeGenerator.cs b/src/System.Web.Razor/Generator/BlockCodeGenerator.cs
new file mode 100644
index 00000000..339334cd
--- /dev/null
+++ b/src/System.Web.Razor/Generator/BlockCodeGenerator.cs
@@ -0,0 +1,45 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Web.Razor.Parser.SyntaxTree;
+
+namespace System.Web.Razor.Generator
+{
+ public abstract class BlockCodeGenerator : CodeGeneratorBase, IBlockCodeGenerator
+ {
+ [SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", Justification = "This class has no instance state")]
+ public static readonly IBlockCodeGenerator Null = new NullBlockCodeGenerator();
+
+ public virtual void GenerateStartBlockCode(Block target, CodeGeneratorContext context)
+ {
+ }
+
+ public virtual void GenerateEndBlockCode(Block target, CodeGeneratorContext context)
+ {
+ }
+
+ public override bool Equals(object obj)
+ {
+ return (obj as IBlockCodeGenerator) != null;
+ }
+
+ public override int GetHashCode()
+ {
+ return base.GetHashCode();
+ }
+
+ private class NullBlockCodeGenerator : IBlockCodeGenerator
+ {
+ public void GenerateStartBlockCode(Block target, CodeGeneratorContext context)
+ {
+ }
+
+ public void GenerateEndBlockCode(Block target, CodeGeneratorContext context)
+ {
+ }
+
+ public override string ToString()
+ {
+ return "None";
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Generator/CSharpCodeWriter.cs b/src/System.Web.Razor/Generator/CSharpCodeWriter.cs
new file mode 100644
index 00000000..6a5809ee
--- /dev/null
+++ b/src/System.Web.Razor/Generator/CSharpCodeWriter.cs
@@ -0,0 +1,247 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+
+namespace System.Web.Razor.Generator
+{
+ internal class CSharpCodeWriter : BaseCodeWriter
+ {
+ protected internal override void WriteStartGenerics()
+ {
+ InnerWriter.Write("<");
+ }
+
+ protected internal override void WriteEndGenerics()
+ {
+ InnerWriter.Write(">");
+ }
+
+ public override int WriteVariableDeclaration(string type, string name, string value)
+ {
+ InnerWriter.Write(type);
+ InnerWriter.Write(" ");
+ InnerWriter.Write(name);
+ if (!String.IsNullOrEmpty(value))
+ {
+ InnerWriter.Write(" = ");
+ InnerWriter.Write(value);
+ }
+ else
+ {
+ InnerWriter.Write(" = null");
+ }
+ return 0;
+ }
+
+ public override void WriteDisableUnusedFieldWarningPragma()
+ {
+ InnerWriter.Write("#pragma warning disable 219");
+ }
+
+ public override void WriteRestoreUnusedFieldWarningPragma()
+ {
+ InnerWriter.Write("#pragma warning restore 219");
+ }
+
+ public override void WriteStringLiteral(string literal)
+ {
+ if (literal == null)
+ {
+ throw new ArgumentNullException("literal");
+ }
+
+ // From CSharpCodeProvider in CodeDOM
+ // If the string is short, use C style quoting (e.g "\r\n")
+ // Also do it if it is too long to fit in one line
+ // If the string contains '\0', verbatim style won't work.
+ if (literal.Length >= 256 && literal.Length <= 1500 && literal.IndexOf('\0') == -1)
+ {
+ WriteVerbatimStringLiteral(literal);
+ }
+ else
+ {
+ WriteCStyleStringLiteral(literal);
+ }
+ }
+
+ private void WriteVerbatimStringLiteral(string literal)
+ {
+ // From CSharpCodeGenerator.QuoteSnippetStringVerbatim in CodeDOM
+ InnerWriter.Write("@\"");
+ for (int i = 0; i < literal.Length; i++)
+ {
+ if (literal[i] == '\"')
+ {
+ InnerWriter.Write("\"\"");
+ }
+ else
+ {
+ InnerWriter.Write(literal[i]);
+ }
+ }
+ InnerWriter.Write("\"");
+ }
+
+ private void WriteCStyleStringLiteral(string literal)
+ {
+ // From CSharpCodeGenerator.QuoteSnippetStringCStyle in CodeDOM
+ InnerWriter.Write("\"");
+ for (int i = 0; i < literal.Length; i++)
+ {
+ switch (literal[i])
+ {
+ case '\r':
+ InnerWriter.Write("\\r");
+ break;
+ case '\t':
+ InnerWriter.Write("\\t");
+ break;
+ case '\"':
+ InnerWriter.Write("\\\"");
+ break;
+ case '\'':
+ InnerWriter.Write("\\\'");
+ break;
+ case '\\':
+ InnerWriter.Write("\\\\");
+ break;
+ case '\0':
+ InnerWriter.Write("\\\0");
+ break;
+ case '\n':
+ InnerWriter.Write("\\n");
+ break;
+ case '\u2028':
+ case '\u2029':
+ // Inlined CSharpCodeGenerator.AppendEscapedChar
+ InnerWriter.Write("\\u");
+ InnerWriter.Write(((int)literal[i]).ToString("X4", CultureInfo.InvariantCulture));
+ break;
+ default:
+ InnerWriter.Write(literal[i]);
+ break;
+ }
+ if (i > 0 && i % 80 == 0)
+ {
+ // If current character is a high surrogate and the following
+ // character is a low surrogate, don't break them.
+ // Otherwise when we write the string to a file, we might lose
+ // the characters.
+ if (Char.IsHighSurrogate(literal[i])
+ && (i < literal.Length - 1)
+ && Char.IsLowSurrogate(literal[i + 1]))
+ {
+ InnerWriter.Write(literal[++i]);
+ }
+
+ InnerWriter.Write("\" +");
+ InnerWriter.Write(Environment.NewLine);
+ InnerWriter.Write('\"');
+ }
+ }
+ InnerWriter.Write("\"");
+ }
+
+ public override void WriteEndStatement()
+ {
+ InnerWriter.WriteLine(";");
+ }
+
+ public override void WriteIdentifier(string identifier)
+ {
+ InnerWriter.Write("@" + identifier);
+ }
+
+ [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "Lowercase is intended here. C# boolean literals are all lowercase")]
+ public override void WriteBooleanLiteral(bool value)
+ {
+ WriteSnippet(value.ToString().ToLowerInvariant());
+ }
+
+ protected internal override void EmitStartLambdaExpression(string[] parameterNames)
+ {
+ if (parameterNames == null)
+ {
+ throw new ArgumentNullException("parameterNames");
+ }
+
+ if (parameterNames.Length == 0 || parameterNames.Length > 1)
+ {
+ InnerWriter.Write("(");
+ }
+ WriteCommaSeparatedList(parameterNames, InnerWriter.Write);
+ if (parameterNames.Length == 0 || parameterNames.Length > 1)
+ {
+ InnerWriter.Write(")");
+ }
+ InnerWriter.Write(" => ");
+ }
+
+ protected internal override void EmitStartLambdaDelegate(string[] parameterNames)
+ {
+ if (parameterNames == null)
+ {
+ throw new ArgumentNullException("parameterNames");
+ }
+
+ EmitStartLambdaExpression(parameterNames);
+ InnerWriter.WriteLine("{");
+ }
+
+ protected internal override void EmitEndLambdaDelegate()
+ {
+ InnerWriter.Write("}");
+ }
+
+ protected internal override void EmitStartConstructor(string typeName)
+ {
+ if (typeName == null)
+ {
+ throw new ArgumentNullException("typeName");
+ }
+
+ InnerWriter.Write("new ");
+ InnerWriter.Write(typeName);
+ InnerWriter.Write("(");
+ }
+
+ public override void WriteReturn()
+ {
+ InnerWriter.Write("return ");
+ }
+
+ public override void WriteLinePragma(int? lineNumber, string fileName)
+ {
+ InnerWriter.WriteLine();
+ if (lineNumber != null)
+ {
+ InnerWriter.Write("#line ");
+ InnerWriter.Write(lineNumber);
+ InnerWriter.Write(" \"");
+ InnerWriter.Write(fileName);
+ InnerWriter.Write("\"");
+ InnerWriter.WriteLine();
+ }
+ else
+ {
+ InnerWriter.WriteLine("#line default");
+ InnerWriter.WriteLine("#line hidden");
+ }
+ }
+
+ public override void WriteHiddenLinePragma()
+ {
+ InnerWriter.WriteLine("#line hidden");
+ }
+
+ public override void WriteHelperHeaderPrefix(string templateTypeName, bool isStatic)
+ {
+ InnerWriter.Write("public ");
+ if (isStatic)
+ {
+ InnerWriter.Write("static ");
+ }
+ InnerWriter.Write(templateTypeName);
+ InnerWriter.Write(" ");
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Generator/CSharpRazorCodeGenerator.cs b/src/System.Web.Razor/Generator/CSharpRazorCodeGenerator.cs
new file mode 100644
index 00000000..339aed7f
--- /dev/null
+++ b/src/System.Web.Razor/Generator/CSharpRazorCodeGenerator.cs
@@ -0,0 +1,28 @@
+using System.CodeDom;
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Web.Razor.Generator
+{
+ public class CSharpRazorCodeGenerator : RazorCodeGenerator
+ {
+ private const string HiddenLinePragma = "#line hidden";
+
+ public CSharpRazorCodeGenerator(string className, string rootNamespaceName, string sourceFileName, RazorEngineHost host)
+ : base(className, rootNamespaceName, sourceFileName, host)
+ {
+ }
+
+ internal override Func<CodeWriter> CodeWriterFactory
+ {
+ get { return () => new CSharpCodeWriter(); }
+ }
+
+ [SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "System.CodeDom.CodeSnippetTypeMember.#ctor(System.String)", Justification = "Value is never to be localized")]
+ protected override void Initialize(CodeGeneratorContext context)
+ {
+ base.Initialize(context);
+
+ context.GeneratedClass.Members.Insert(0, new CodeSnippetTypeMember(HiddenLinePragma));
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Generator/CodeGenerationCompleteEventArgs.cs b/src/System.Web.Razor/Generator/CodeGenerationCompleteEventArgs.cs
new file mode 100644
index 00000000..c0f64c3e
--- /dev/null
+++ b/src/System.Web.Razor/Generator/CodeGenerationCompleteEventArgs.cs
@@ -0,0 +1,27 @@
+using System.CodeDom;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.Razor.Generator
+{
+ public class CodeGenerationCompleteEventArgs : EventArgs
+ {
+ public CodeGenerationCompleteEventArgs(string virtualPath, string physicalPath, CodeCompileUnit generatedCode)
+ {
+ if (String.IsNullOrEmpty(virtualPath))
+ {
+ throw ExceptionHelper.CreateArgumentNullOrEmptyException("virtualPath");
+ }
+ if (generatedCode == null)
+ {
+ throw new ArgumentNullException("generatedCode");
+ }
+ VirtualPath = virtualPath;
+ PhysicalPath = physicalPath;
+ GeneratedCode = generatedCode;
+ }
+
+ public CodeCompileUnit GeneratedCode { get; private set; }
+ public string VirtualPath { get; private set; }
+ public string PhysicalPath { get; private set; }
+ }
+}
diff --git a/src/System.Web.Razor/Generator/CodeGeneratorBase.cs b/src/System.Web.Razor/Generator/CodeGeneratorBase.cs
new file mode 100644
index 00000000..7c184ed0
--- /dev/null
+++ b/src/System.Web.Razor/Generator/CodeGeneratorBase.cs
@@ -0,0 +1,35 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Web.Razor.Parser.SyntaxTree;
+
+namespace System.Web.Razor.Generator
+{
+ public abstract class CodeGeneratorBase
+ {
+ // Helpers
+ protected internal static int CalculatePadding(Span target)
+ {
+ return CalculatePadding(target, generatedStart: 0);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "This method should only be used on Spans")]
+ protected internal static int CalculatePadding(Span target, int generatedStart)
+ {
+ int padding = target.Start.CharacterIndex - generatedStart;
+ if (padding < 0)
+ {
+ padding = 0;
+ }
+ return padding;
+ }
+
+ protected internal static string Pad(string code, Span target)
+ {
+ return Pad(code, target, 0);
+ }
+
+ protected internal static string Pad(string code, Span target, int generatedStart)
+ {
+ return code.PadLeft(CalculatePadding(target, generatedStart) + code.Length, ' ');
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Generator/CodeGeneratorContext.cs b/src/System.Web.Razor/Generator/CodeGeneratorContext.cs
new file mode 100644
index 00000000..7025ed68
--- /dev/null
+++ b/src/System.Web.Razor/Generator/CodeGeneratorContext.cs
@@ -0,0 +1,327 @@
+using System.CodeDom;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Text;
+using System.Web.Razor.Utils;
+
+namespace System.Web.Razor.Generator
+{
+ public class CodeGeneratorContext
+ {
+ private const string DesignTimeHelperMethodName = "__RazorDesignTimeHelpers__";
+
+ private int _nextDesignTimePragmaId = 1;
+ private bool _expressionHelperVariableWriten;
+ private CodeMemberMethod _designTimeHelperMethod;
+ private StatementBuffer _currentBuffer = new StatementBuffer();
+
+ private CodeGeneratorContext()
+ {
+ ExpressionRenderingMode = ExpressionRenderingMode.WriteToOutput;
+ }
+
+ // Internal/Private state. Technically consumers might want to use some of these but they can implement them independently if necessary.
+ // It's way safer to make them internal for now, especially with the code generator stuff in a bit of flux.
+ internal ExpressionRenderingMode ExpressionRenderingMode { get; set; }
+ private Action<string, CodeLinePragma> StatementCollector { get; set; }
+ private Func<CodeWriter> CodeWriterFactory { get; set; }
+
+ public string SourceFile { get; internal set; }
+ public CodeCompileUnit CompileUnit { get; internal set; }
+ public CodeNamespace Namespace { get; internal set; }
+ public CodeTypeDeclaration GeneratedClass { get; internal set; }
+ public RazorEngineHost Host { get; private set; }
+ public IDictionary<int, GeneratedCodeMapping> CodeMappings { get; private set; }
+ public string TargetWriterName { get; set; }
+ public CodeMemberMethod TargetMethod { get; set; }
+
+ public string CurrentBufferedStatement
+ {
+ get { return _currentBuffer == null ? String.Empty : _currentBuffer.Builder.ToString(); }
+ }
+
+ public static CodeGeneratorContext Create(RazorEngineHost host, string className, string rootNamespace, string sourceFile, bool shouldGenerateLinePragmas)
+ {
+ return Create(host, null, className, rootNamespace, sourceFile, shouldGenerateLinePragmas);
+ }
+
+ internal static CodeGeneratorContext Create(RazorEngineHost host, Func<CodeWriter> writerFactory, string className, string rootNamespace, string sourceFile, bool shouldGenerateLinePragmas)
+ {
+ CodeGeneratorContext context = new CodeGeneratorContext()
+ {
+ Host = host,
+ CodeWriterFactory = writerFactory,
+ SourceFile = shouldGenerateLinePragmas ? sourceFile : null,
+ CompileUnit = new CodeCompileUnit(),
+ Namespace = new CodeNamespace(rootNamespace),
+ GeneratedClass = new CodeTypeDeclaration(className)
+ {
+ IsClass = true
+ },
+ TargetMethod = new CodeMemberMethod()
+ {
+ Name = host.GeneratedClassContext.ExecuteMethodName,
+ Attributes = MemberAttributes.Override | MemberAttributes.Public
+ },
+ CodeMappings = new Dictionary<int, GeneratedCodeMapping>()
+ };
+ context.CompileUnit.Namespaces.Add(context.Namespace);
+ context.Namespace.Types.Add(context.GeneratedClass);
+ context.GeneratedClass.Members.Add(context.TargetMethod);
+
+ context.Namespace.Imports.AddRange(host.NamespaceImports
+ .Select(s => new CodeNamespaceImport(s))
+ .ToArray());
+
+ return context;
+ }
+
+ public void AddDesignTimeHelperStatement(CodeSnippetStatement statement)
+ {
+ if (_designTimeHelperMethod == null)
+ {
+ _designTimeHelperMethod = new CodeMemberMethod()
+ {
+ Name = DesignTimeHelperMethodName,
+ Attributes = MemberAttributes.Private
+ };
+ _designTimeHelperMethod.Statements.Add(
+ new CodeSnippetStatement(BuildCodeString(cw => cw.WriteDisableUnusedFieldWarningPragma())));
+ _designTimeHelperMethod.Statements.Add(
+ new CodeSnippetStatement(BuildCodeString(cw => cw.WriteRestoreUnusedFieldWarningPragma())));
+ GeneratedClass.Members.Insert(0, _designTimeHelperMethod);
+ }
+ _designTimeHelperMethod.Statements.Insert(_designTimeHelperMethod.Statements.Count - 1, statement);
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2233:OperationsShouldNotOverflow", MessageId = "generatedCodeStart+1", Justification = "There is no risk of overflow in this case")]
+ public int AddCodeMapping(SourceLocation sourceLocation, int generatedCodeStart, int generatedCodeLength)
+ {
+ if (generatedCodeStart == Int32.MaxValue)
+ {
+ throw new ArgumentOutOfRangeException("generatedCodeStart");
+ }
+
+ GeneratedCodeMapping mapping = new GeneratedCodeMapping(
+ startOffset: sourceLocation.AbsoluteIndex,
+ startLine: sourceLocation.LineIndex + 1,
+ startColumn: sourceLocation.CharacterIndex + 1,
+ startGeneratedColumn: generatedCodeStart + 1,
+ codeLength: generatedCodeLength);
+
+ int id = _nextDesignTimePragmaId++;
+ CodeMappings[id] = mapping;
+ return id;
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "This method requires that a Span be provided")]
+ public CodeLinePragma GenerateLinePragma(Span target)
+ {
+ return GenerateLinePragma(target, 0);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "This method requires that a Span be provided")]
+ public CodeLinePragma GenerateLinePragma(Span target, int generatedCodeStart)
+ {
+ return GenerateLinePragma(target, generatedCodeStart, target.Content.Length);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "This method requires that a Span be provided")]
+ public CodeLinePragma GenerateLinePragma(Span target, int generatedCodeStart, int codeLength)
+ {
+ return GenerateLinePragma(target.Start, generatedCodeStart, codeLength);
+ }
+
+ public CodeLinePragma GenerateLinePragma(SourceLocation start, int generatedCodeStart, int codeLength)
+ {
+ if (!String.IsNullOrEmpty(SourceFile))
+ {
+ if (Host.DesignTimeMode)
+ {
+ int mappingId = AddCodeMapping(start, generatedCodeStart, codeLength);
+ return new CodeLinePragma(SourceFile, mappingId);
+ }
+ return new CodeLinePragma(SourceFile, start.LineIndex + 1);
+ }
+ return null;
+ }
+
+ public void BufferStatementFragment(Span sourceSpan)
+ {
+ BufferStatementFragment(sourceSpan.Content, sourceSpan);
+ }
+
+ public void BufferStatementFragment(string fragment)
+ {
+ BufferStatementFragment(fragment, null);
+ }
+
+ public void BufferStatementFragment(string fragment, Span sourceSpan)
+ {
+ if (sourceSpan != null && _currentBuffer.LinePragmaSpan == null)
+ {
+ _currentBuffer.LinePragmaSpan = sourceSpan;
+
+ // Pad the output as necessary
+ int start = _currentBuffer.Builder.Length;
+ if (_currentBuffer.GeneratedCodeStart != null)
+ {
+ start = _currentBuffer.GeneratedCodeStart.Value;
+ }
+ string padded = CodeGeneratorBase.Pad(_currentBuffer.Builder.ToString(), sourceSpan, start);
+ _currentBuffer.GeneratedCodeStart = start + (padded.Length - _currentBuffer.Builder.Length);
+ _currentBuffer.Builder.Clear();
+ _currentBuffer.Builder.Append(padded);
+ }
+ _currentBuffer.Builder.Append(fragment);
+ }
+
+ public void MarkStartOfGeneratedCode()
+ {
+ _currentBuffer.MarkStart();
+ }
+
+ public void MarkEndOfGeneratedCode()
+ {
+ _currentBuffer.MarkEnd();
+ }
+
+ public void FlushBufferedStatement()
+ {
+ if (_currentBuffer.Builder.Length > 0)
+ {
+ CodeLinePragma pragma = null;
+ if (_currentBuffer.LinePragmaSpan != null)
+ {
+ int start = _currentBuffer.Builder.Length;
+ if (_currentBuffer.GeneratedCodeStart != null)
+ {
+ start = _currentBuffer.GeneratedCodeStart.Value;
+ }
+ int len = _currentBuffer.Builder.Length - start;
+ if (_currentBuffer.CodeLength != null)
+ {
+ len = _currentBuffer.CodeLength.Value;
+ }
+ pragma = GenerateLinePragma(_currentBuffer.LinePragmaSpan, start, len);
+ }
+ AddStatement(_currentBuffer.Builder.ToString(), pragma);
+ _currentBuffer.Reset();
+ }
+ }
+
+ public void AddStatement(string generatedCode)
+ {
+ AddStatement(generatedCode, null);
+ }
+
+ public void AddStatement(string body, CodeLinePragma pragma)
+ {
+ if (StatementCollector == null)
+ {
+ TargetMethod.Statements.Add(new CodeSnippetStatement(body) { LinePragma = pragma });
+ }
+ else
+ {
+ StatementCollector(body, pragma);
+ }
+ }
+
+ public void EnsureExpressionHelperVariable()
+ {
+ if (!_expressionHelperVariableWriten)
+ {
+ GeneratedClass.Members.Insert(0,
+ new CodeMemberField(typeof(object), "__o")
+ {
+ Attributes = MemberAttributes.Private | MemberAttributes.Static
+ });
+ _expressionHelperVariableWriten = true;
+ }
+ }
+
+ public IDisposable ChangeStatementCollector(Action<string, CodeLinePragma> collector)
+ {
+ Action<string, CodeLinePragma> oldCollector = StatementCollector;
+ StatementCollector = collector;
+ return new DisposableAction(() =>
+ {
+ StatementCollector = oldCollector;
+ });
+ }
+
+ [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "We explicitly want the lower-case string here")]
+ public void AddContextCall(Span contentSpan, string methodName, bool isLiteral)
+ {
+ AddStatement(BuildCodeString(cw =>
+ {
+ cw.WriteStartMethodInvoke(methodName);
+ if (!String.IsNullOrEmpty(TargetWriterName))
+ {
+ cw.WriteSnippet(TargetWriterName);
+ cw.WriteParameterSeparator();
+ }
+ cw.WriteStringLiteral(Host.InstrumentedSourceFilePath);
+ cw.WriteParameterSeparator();
+ cw.WriteSnippet(contentSpan.Start.AbsoluteIndex.ToString(CultureInfo.InvariantCulture));
+ cw.WriteParameterSeparator();
+ cw.WriteSnippet(contentSpan.Content.Length.ToString(CultureInfo.InvariantCulture));
+ cw.WriteParameterSeparator();
+ cw.WriteSnippet(isLiteral.ToString().ToLowerInvariant());
+ cw.WriteEndMethodInvoke();
+ cw.WriteEndStatement();
+ }));
+ }
+
+ internal CodeWriter CreateCodeWriter()
+ {
+ Debug.Assert(CodeWriterFactory != null);
+ if (CodeWriterFactory == null)
+ {
+ throw new InvalidOperationException(RazorResources.CreateCodeWriter_NoCodeWriter);
+ }
+ return CodeWriterFactory();
+ }
+
+ internal string BuildCodeString(Action<CodeWriter> action)
+ {
+ using (CodeWriter cw = CodeWriterFactory())
+ {
+ action(cw);
+ return cw.Content;
+ }
+ }
+
+ private class StatementBuffer
+ {
+ public StringBuilder Builder = new StringBuilder();
+ public int? GeneratedCodeStart;
+ public int? CodeLength;
+ public Span LinePragmaSpan;
+
+ public void Reset()
+ {
+ Builder.Clear();
+ GeneratedCodeStart = null;
+ CodeLength = null;
+ LinePragmaSpan = null;
+ }
+
+ public void MarkStart()
+ {
+ GeneratedCodeStart = Builder.Length;
+ }
+
+ public void MarkEnd()
+ {
+ CodeLength = Builder.Length - GeneratedCodeStart;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Generator/CodeWriter.cs b/src/System.Web.Razor/Generator/CodeWriter.cs
new file mode 100644
index 00000000..afec9cbf
--- /dev/null
+++ b/src/System.Web.Razor/Generator/CodeWriter.cs
@@ -0,0 +1,207 @@
+using System.CodeDom;
+using System.Globalization;
+using System.IO;
+
+namespace System.Web.Razor.Generator
+{
+ // Utility class which helps write code snippets
+ internal abstract class CodeWriter : IDisposable
+ {
+ private StringWriter _writer;
+
+ protected CodeWriter()
+ {
+ }
+
+ private enum WriterMode
+ {
+ Constructor,
+ MethodCall,
+ LambdaDelegate,
+ LambdaExpression
+ }
+
+ public string Content
+ {
+ get { return InnerWriter.ToString(); }
+ }
+
+ public StringWriter InnerWriter
+ {
+ get
+ {
+ if (_writer == null)
+ {
+ _writer = new StringWriter(CultureInfo.InvariantCulture);
+ }
+ return _writer;
+ }
+ }
+
+ public virtual bool SupportsMidStatementLinePragmas
+ {
+ get { return true; }
+ }
+
+ public abstract void WriteParameterSeparator();
+ public abstract void WriteReturn();
+ public abstract void WriteLinePragma(int? lineNumber, string fileName);
+ public abstract void WriteHelperHeaderPrefix(string templateTypeName, bool isStatic);
+ public abstract void WriteSnippet(string snippet);
+ public abstract void WriteStringLiteral(string literal);
+ public abstract int WriteVariableDeclaration(string type, string name, string value);
+
+ public virtual void WriteLinePragma()
+ {
+ WriteLinePragma(null);
+ }
+
+ public virtual void WriteLinePragma(CodeLinePragma pragma)
+ {
+ if (pragma == null)
+ {
+ WriteLinePragma(null, null);
+ }
+ else
+ {
+ WriteLinePragma(pragma.LineNumber, pragma.FileName);
+ }
+ }
+
+ public virtual void WriteHiddenLinePragma()
+ {
+ }
+
+ public virtual void WriteDisableUnusedFieldWarningPragma()
+ {
+ }
+
+ public virtual void WriteRestoreUnusedFieldWarningPragma()
+ {
+ }
+
+ public virtual void WriteIdentifier(string identifier)
+ {
+ InnerWriter.Write(identifier);
+ }
+
+ public virtual void WriteHelperHeaderSuffix(string templateTypeName)
+ {
+ }
+
+ public virtual void WriteHelperTrailer()
+ {
+ }
+
+ public void WriteStartMethodInvoke(string methodName)
+ {
+ EmitStartMethodInvoke(methodName);
+ }
+
+ public void WriteStartMethodInvoke(string methodName, params string[] genericArguments)
+ {
+ EmitStartMethodInvoke(methodName, genericArguments);
+ }
+
+ public void WriteEndMethodInvoke()
+ {
+ EmitEndMethodInvoke();
+ }
+
+ public virtual void WriteEndStatement()
+ {
+ }
+
+ public virtual void WriteStartAssignment(string variableName)
+ {
+ InnerWriter.Write(variableName);
+ InnerWriter.Write(" = ");
+ }
+
+ public void WriteStartLambdaExpression(params string[] parameterNames)
+ {
+ EmitStartLambdaExpression(parameterNames);
+ }
+
+ public void WriteStartConstructor(string typeName)
+ {
+ EmitStartConstructor(typeName);
+ }
+
+ public void WriteStartLambdaDelegate(params string[] parameterNames)
+ {
+ EmitStartLambdaDelegate(parameterNames);
+ }
+
+ public void WriteEndLambdaExpression()
+ {
+ EmitEndLambdaExpression();
+ }
+
+ public void WriteEndConstructor()
+ {
+ EmitEndConstructor();
+ }
+
+ public void WriteEndLambdaDelegate()
+ {
+ EmitEndLambdaDelegate();
+ }
+
+ public virtual void WriteLineContinuation()
+ {
+ }
+
+ public virtual void WriteBooleanLiteral(bool value)
+ {
+ WriteSnippet(value.ToString(CultureInfo.InvariantCulture));
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ public void Clear()
+ {
+ if (InnerWriter != null)
+ {
+ InnerWriter.GetStringBuilder().Clear();
+ }
+ }
+
+ public CodeSnippetStatement ToStatement()
+ {
+ return new CodeSnippetStatement(Content);
+ }
+
+ public CodeSnippetTypeMember ToTypeMember()
+ {
+ return new CodeSnippetTypeMember(Content);
+ }
+
+ protected internal abstract void EmitStartLambdaDelegate(string[] parameterNames);
+ protected internal abstract void EmitStartLambdaExpression(string[] parameterNames);
+ protected internal abstract void EmitStartConstructor(string typeName);
+ protected internal abstract void EmitStartMethodInvoke(string methodName);
+
+ protected internal virtual void EmitStartMethodInvoke(string methodName, params string[] genericArguments)
+ {
+ EmitStartMethodInvoke(methodName);
+ }
+
+ protected internal abstract void EmitEndLambdaDelegate();
+ protected internal abstract void EmitEndLambdaExpression();
+ protected internal abstract void EmitEndConstructor();
+ protected internal abstract void EmitEndMethodInvoke();
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing && _writer != null)
+ {
+ _writer.Dispose();
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Generator/CodeWriterExtensions.cs b/src/System.Web.Razor/Generator/CodeWriterExtensions.cs
new file mode 100644
index 00000000..bb55bfff
--- /dev/null
+++ b/src/System.Web.Razor/Generator/CodeWriterExtensions.cs
@@ -0,0 +1,17 @@
+using System.Globalization;
+using System.Web.Razor.Text;
+
+namespace System.Web.Razor.Generator
+{
+ internal static class CodeWriterExtensions
+ {
+ public static void WriteLocationTaggedString(this CodeWriter writer, LocationTagged<string> value)
+ {
+ writer.WriteStartMethodInvoke("Tuple.Create");
+ writer.WriteStringLiteral(value.Value);
+ writer.WriteParameterSeparator();
+ writer.WriteSnippet(value.Location.AbsoluteIndex.ToString(CultureInfo.CurrentCulture));
+ writer.WriteEndMethodInvoke();
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Generator/DynamicAttributeBlockCodeGenerator.cs b/src/System.Web.Razor/Generator/DynamicAttributeBlockCodeGenerator.cs
new file mode 100644
index 00000000..28ea204c
--- /dev/null
+++ b/src/System.Web.Razor/Generator/DynamicAttributeBlockCodeGenerator.cs
@@ -0,0 +1,139 @@
+using System.Globalization;
+using System.Linq;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Text;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.Razor.Generator
+{
+ public class DynamicAttributeBlockCodeGenerator : BlockCodeGenerator
+ {
+ private const string ValueWriterName = "__razor_attribute_value_writer";
+ private string _oldTargetWriter;
+ private bool _isExpression;
+ private ExpressionRenderingMode _oldRenderingMode;
+
+ public DynamicAttributeBlockCodeGenerator(LocationTagged<string> prefix, int offset, int line, int col)
+ : this(prefix, new SourceLocation(offset, line, col))
+ {
+ }
+
+ public DynamicAttributeBlockCodeGenerator(LocationTagged<string> prefix, SourceLocation valueStart)
+ {
+ Prefix = prefix;
+ ValueStart = valueStart;
+ }
+
+ public LocationTagged<string> Prefix { get; private set; }
+ public SourceLocation ValueStart { get; private set; }
+
+ public override void GenerateStartBlockCode(Block target, CodeGeneratorContext context)
+ {
+ if (context.Host.DesignTimeMode)
+ {
+ return; // Don't generate anything!
+ }
+
+ // What kind of block is nested within
+ string generatedCode;
+ Block child = target.Children.Where(n => n.IsBlock).Cast<Block>().FirstOrDefault();
+ if (child != null && child.Type == BlockType.Expression)
+ {
+ _isExpression = true;
+ generatedCode = context.BuildCodeString(cw =>
+ {
+ cw.WriteParameterSeparator();
+ cw.WriteStartMethodInvoke("Tuple.Create");
+ cw.WriteLocationTaggedString(Prefix);
+ cw.WriteParameterSeparator();
+ cw.WriteStartMethodInvoke("Tuple.Create", "System.Object", "System.Int32");
+ });
+
+ _oldRenderingMode = context.ExpressionRenderingMode;
+ context.ExpressionRenderingMode = ExpressionRenderingMode.InjectCode;
+ }
+ else
+ {
+ generatedCode = context.BuildCodeString(cw =>
+ {
+ cw.WriteParameterSeparator();
+ cw.WriteStartMethodInvoke("Tuple.Create");
+ cw.WriteLocationTaggedString(Prefix);
+ cw.WriteParameterSeparator();
+ cw.WriteStartMethodInvoke("Tuple.Create", "System.Object", "System.Int32");
+ cw.WriteStartConstructor(context.Host.GeneratedClassContext.TemplateTypeName);
+ cw.WriteStartLambdaDelegate(ValueWriterName);
+ });
+ }
+
+ context.MarkEndOfGeneratedCode();
+ context.BufferStatementFragment(generatedCode);
+
+ _oldTargetWriter = context.TargetWriterName;
+ context.TargetWriterName = ValueWriterName;
+ }
+
+ public override void GenerateEndBlockCode(Block target, CodeGeneratorContext context)
+ {
+ if (context.Host.DesignTimeMode)
+ {
+ return; // Don't generate anything!
+ }
+
+ string generatedCode;
+ if (_isExpression)
+ {
+ generatedCode = context.BuildCodeString(cw =>
+ {
+ cw.WriteParameterSeparator();
+ cw.WriteSnippet(ValueStart.AbsoluteIndex.ToString(CultureInfo.CurrentCulture));
+ cw.WriteEndMethodInvoke();
+ cw.WriteParameterSeparator();
+ // literal: false - This attribute value is not a literal value, it is dynamically generated
+ cw.WriteBooleanLiteral(false);
+ cw.WriteEndMethodInvoke();
+ cw.WriteLineContinuation();
+ });
+ context.ExpressionRenderingMode = _oldRenderingMode;
+ }
+ else
+ {
+ generatedCode = context.BuildCodeString(cw =>
+ {
+ cw.WriteEndLambdaDelegate();
+ cw.WriteEndConstructor();
+ cw.WriteParameterSeparator();
+ cw.WriteSnippet(ValueStart.AbsoluteIndex.ToString(CultureInfo.CurrentCulture));
+ cw.WriteEndMethodInvoke();
+ cw.WriteParameterSeparator();
+ // literal: false - This attribute value is not a literal value, it is dynamically generated
+ cw.WriteBooleanLiteral(false);
+ cw.WriteEndMethodInvoke();
+ cw.WriteLineContinuation();
+ });
+ }
+
+ context.AddStatement(generatedCode);
+ context.TargetWriterName = _oldTargetWriter;
+ }
+
+ public override string ToString()
+ {
+ return String.Format(CultureInfo.CurrentCulture, "DynAttr:{0:F}", Prefix);
+ }
+
+ public override bool Equals(object obj)
+ {
+ DynamicAttributeBlockCodeGenerator other = obj as DynamicAttributeBlockCodeGenerator;
+ return other != null &&
+ Equals(other.Prefix, Prefix);
+ }
+
+ public override int GetHashCode()
+ {
+ return HashCodeCombiner.Start()
+ .Add(Prefix)
+ .CombinedHash;
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Generator/ExpressionCodeGenerator.cs b/src/System.Web.Razor/Generator/ExpressionCodeGenerator.cs
new file mode 100644
index 00000000..e5db54cd
--- /dev/null
+++ b/src/System.Web.Razor/Generator/ExpressionCodeGenerator.cs
@@ -0,0 +1,110 @@
+using System.Linq;
+using System.Web.Razor.Parser.SyntaxTree;
+
+namespace System.Web.Razor.Generator
+{
+ public class ExpressionCodeGenerator : HybridCodeGenerator
+ {
+ public override void GenerateStartBlockCode(Block target, CodeGeneratorContext context)
+ {
+ if (context.Host.EnableInstrumentation && context.ExpressionRenderingMode == ExpressionRenderingMode.WriteToOutput)
+ {
+ Span contentSpan = target.Children
+ .OfType<Span>()
+ .Where(s => s.Kind == SpanKind.Code || s.Kind == SpanKind.Markup)
+ .FirstOrDefault();
+
+ if (contentSpan != null)
+ {
+ context.AddContextCall(contentSpan, context.Host.GeneratedClassContext.BeginContextMethodName, false);
+ }
+ }
+
+ string writeInvocation = context.BuildCodeString(cw =>
+ {
+ if (context.Host.DesignTimeMode)
+ {
+ context.EnsureExpressionHelperVariable();
+ cw.WriteStartAssignment("__o");
+ }
+ else if (context.ExpressionRenderingMode == ExpressionRenderingMode.WriteToOutput)
+ {
+ if (!String.IsNullOrEmpty(context.TargetWriterName))
+ {
+ cw.WriteStartMethodInvoke(context.Host.GeneratedClassContext.WriteToMethodName);
+ cw.WriteSnippet(context.TargetWriterName);
+ cw.WriteParameterSeparator();
+ }
+ else
+ {
+ cw.WriteStartMethodInvoke(context.Host.GeneratedClassContext.WriteMethodName);
+ }
+ }
+ });
+
+ context.BufferStatementFragment(writeInvocation);
+ context.MarkStartOfGeneratedCode();
+ }
+
+ public override void GenerateEndBlockCode(Block target, CodeGeneratorContext context)
+ {
+ string endBlock = context.BuildCodeString(cw =>
+ {
+ if (context.ExpressionRenderingMode == ExpressionRenderingMode.WriteToOutput)
+ {
+ if (!context.Host.DesignTimeMode)
+ {
+ cw.WriteEndMethodInvoke();
+ }
+ cw.WriteEndStatement();
+ }
+ else
+ {
+ cw.WriteLineContinuation();
+ }
+ });
+
+ context.MarkEndOfGeneratedCode();
+ context.BufferStatementFragment(endBlock);
+ context.FlushBufferedStatement();
+
+ if (context.Host.EnableInstrumentation && context.ExpressionRenderingMode == ExpressionRenderingMode.WriteToOutput)
+ {
+ Span contentSpan = target.Children
+ .OfType<Span>()
+ .Where(s => s.Kind == SpanKind.Code || s.Kind == SpanKind.Markup)
+ .FirstOrDefault();
+
+ if (contentSpan != null)
+ {
+ context.AddContextCall(contentSpan, context.Host.GeneratedClassContext.EndContextMethodName, false);
+ }
+ }
+ }
+
+ public override void GenerateCode(Span target, CodeGeneratorContext context)
+ {
+ Span sourceSpan = null;
+ if (context.CreateCodeWriter().SupportsMidStatementLinePragmas || context.ExpressionRenderingMode == ExpressionRenderingMode.WriteToOutput)
+ {
+ sourceSpan = target;
+ }
+ context.BufferStatementFragment(target.Content, sourceSpan);
+ }
+
+ public override string ToString()
+ {
+ return "Expr";
+ }
+
+ public override bool Equals(object obj)
+ {
+ return obj is ExpressionCodeGenerator;
+ }
+
+ public override int GetHashCode()
+ {
+ return base.GetHashCode();
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Generator/ExpressionRenderingMode.cs b/src/System.Web.Razor/Generator/ExpressionRenderingMode.cs
new file mode 100644
index 00000000..755d6549
--- /dev/null
+++ b/src/System.Web.Razor/Generator/ExpressionRenderingMode.cs
@@ -0,0 +1,26 @@
+namespace System.Web.Razor.Generator
+{
+ public enum ExpressionRenderingMode
+ {
+ /// <summary>
+ /// Indicates that expressions should be written to the output stream
+ /// </summary>
+ /// <example>
+ /// If @foo is rendered with WriteToOutput, the code generator would output the following code:
+ ///
+ /// Write(foo);
+ /// </example>
+ WriteToOutput,
+
+ /// <summary>
+ /// Indicates that expressions should simply be placed as-is in the code, and the context in which
+ /// the code exists will be used to render it
+ /// </summary>
+ /// <example>
+ /// If @foo is rendered with InjectCode, the code generator would output the following code:
+ ///
+ /// foo
+ /// </example>
+ InjectCode
+ }
+}
diff --git a/src/System.Web.Razor/Generator/GeneratedClassContext.cs b/src/System.Web.Razor/Generator/GeneratedClassContext.cs
new file mode 100644
index 00000000..3a507622
--- /dev/null
+++ b/src/System.Web.Razor/Generator/GeneratedClassContext.cs
@@ -0,0 +1,174 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.Razor.Generator
+{
+ public struct GeneratedClassContext
+ {
+ public static readonly string DefaultWriteMethodName = "Write";
+ public static readonly string DefaultWriteLiteralMethodName = "WriteLiteral";
+ public static readonly string DefaultExecuteMethodName = "Execute";
+ public static readonly string DefaultLayoutPropertyName = "Layout";
+ public static readonly string DefaultWriteAttributeMethodName = "WriteAttribute";
+ public static readonly string DefaultWriteAttributeToMethodName = "WriteAttributeTo";
+
+ public static readonly GeneratedClassContext Default = new GeneratedClassContext(DefaultExecuteMethodName,
+ DefaultWriteMethodName,
+ DefaultWriteLiteralMethodName);
+
+ public GeneratedClassContext(string executeMethodName, string writeMethodName, string writeLiteralMethodName)
+ : this()
+ {
+ if (String.IsNullOrEmpty(executeMethodName))
+ {
+ throw new ArgumentException(String.Format(CultureInfo.CurrentCulture,
+ CommonResources.Argument_Cannot_Be_Null_Or_Empty,
+ "executeMethodName"),
+ "executeMethodName");
+ }
+ if (String.IsNullOrEmpty(writeMethodName))
+ {
+ throw new ArgumentException(String.Format(CultureInfo.CurrentCulture,
+ CommonResources.Argument_Cannot_Be_Null_Or_Empty,
+ "writeMethodName"),
+ "writeMethodName");
+ }
+ if (String.IsNullOrEmpty(writeLiteralMethodName))
+ {
+ throw new ArgumentException(String.Format(CultureInfo.CurrentCulture,
+ CommonResources.Argument_Cannot_Be_Null_Or_Empty,
+ "writeLiteralMethodName"),
+ "writeLiteralMethodName");
+ }
+
+ WriteMethodName = writeMethodName;
+ WriteLiteralMethodName = writeLiteralMethodName;
+ ExecuteMethodName = executeMethodName;
+
+ WriteToMethodName = null;
+ WriteLiteralToMethodName = null;
+ TemplateTypeName = null;
+ DefineSectionMethodName = null;
+
+ LayoutPropertyName = DefaultLayoutPropertyName;
+ WriteAttributeMethodName = DefaultWriteAttributeMethodName;
+ WriteAttributeToMethodName = DefaultWriteAttributeToMethodName;
+ }
+
+ public GeneratedClassContext(string executeMethodName,
+ string writeMethodName,
+ string writeLiteralMethodName,
+ string writeToMethodName,
+ string writeLiteralToMethodName,
+ string templateTypeName)
+ : this(executeMethodName, writeMethodName, writeLiteralMethodName)
+ {
+ WriteToMethodName = writeToMethodName;
+ WriteLiteralToMethodName = writeLiteralToMethodName;
+ TemplateTypeName = templateTypeName;
+ }
+
+ public GeneratedClassContext(string executeMethodName,
+ string writeMethodName,
+ string writeLiteralMethodName,
+ string writeToMethodName,
+ string writeLiteralToMethodName,
+ string templateTypeName,
+ string defineSectionMethodName)
+ : this(executeMethodName, writeMethodName, writeLiteralMethodName, writeToMethodName, writeLiteralToMethodName, templateTypeName)
+ {
+ DefineSectionMethodName = defineSectionMethodName;
+ }
+
+ public GeneratedClassContext(string executeMethodName,
+ string writeMethodName,
+ string writeLiteralMethodName,
+ string writeToMethodName,
+ string writeLiteralToMethodName,
+ string templateTypeName,
+ string defineSectionMethodName,
+ string beginContextMethodName,
+ string endContextMethodName)
+ : this(executeMethodName, writeMethodName, writeLiteralMethodName, writeToMethodName, writeLiteralToMethodName, templateTypeName, defineSectionMethodName)
+ {
+ BeginContextMethodName = beginContextMethodName;
+ EndContextMethodName = endContextMethodName;
+ }
+
+ public string WriteMethodName { get; private set; }
+ public string WriteLiteralMethodName { get; private set; }
+ public string WriteToMethodName { get; private set; }
+ public string WriteLiteralToMethodName { get; private set; }
+ public string ExecuteMethodName { get; private set; }
+
+ // Optional Items
+ public string BeginContextMethodName { get; set; }
+ public string EndContextMethodName { get; set; }
+ public string LayoutPropertyName { get; set; }
+ public string DefineSectionMethodName { get; set; }
+ public string TemplateTypeName { get; set; }
+ public string WriteAttributeMethodName { get; set; }
+ public string WriteAttributeToMethodName { get; set; }
+
+ [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "Property is not a URL property")]
+ public string ResolveUrlMethodName { get; set; }
+
+ public bool AllowSections
+ {
+ get { return !String.IsNullOrEmpty(DefineSectionMethodName); }
+ }
+
+ public bool AllowTemplates
+ {
+ get { return !String.IsNullOrEmpty(TemplateTypeName); }
+ }
+
+ public bool SupportsInstrumentation
+ {
+ get { return !String.IsNullOrEmpty(BeginContextMethodName) && !String.IsNullOrEmpty(EndContextMethodName); }
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (!(obj is GeneratedClassContext))
+ {
+ return false;
+ }
+ GeneratedClassContext other = (GeneratedClassContext)obj;
+ return String.Equals(DefineSectionMethodName, other.DefineSectionMethodName, StringComparison.Ordinal) &&
+ String.Equals(WriteMethodName, other.WriteMethodName, StringComparison.Ordinal) &&
+ String.Equals(WriteLiteralMethodName, other.WriteLiteralMethodName, StringComparison.Ordinal) &&
+ String.Equals(WriteToMethodName, other.WriteToMethodName, StringComparison.Ordinal) &&
+ String.Equals(WriteLiteralToMethodName, other.WriteLiteralToMethodName, StringComparison.Ordinal) &&
+ String.Equals(ExecuteMethodName, other.ExecuteMethodName, StringComparison.Ordinal) &&
+ String.Equals(TemplateTypeName, other.TemplateTypeName, StringComparison.Ordinal) &&
+ String.Equals(BeginContextMethodName, other.BeginContextMethodName, StringComparison.Ordinal) &&
+ String.Equals(EndContextMethodName, other.EndContextMethodName, StringComparison.Ordinal);
+ }
+
+ public override int GetHashCode()
+ {
+ // TODO: Use HashCodeCombiner
+ return DefineSectionMethodName.GetHashCode() ^
+ WriteMethodName.GetHashCode() ^
+ WriteLiteralMethodName.GetHashCode() ^
+ WriteToMethodName.GetHashCode() ^
+ WriteLiteralToMethodName.GetHashCode() ^
+ ExecuteMethodName.GetHashCode() ^
+ TemplateTypeName.GetHashCode() ^
+ BeginContextMethodName.GetHashCode() ^
+ EndContextMethodName.GetHashCode();
+ }
+
+ public static bool operator ==(GeneratedClassContext left, GeneratedClassContext right)
+ {
+ return left.Equals(right);
+ }
+
+ public static bool operator !=(GeneratedClassContext left, GeneratedClassContext right)
+ {
+ return !left.Equals(right);
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Generator/GeneratedCodeMapping.cs b/src/System.Web.Razor/Generator/GeneratedCodeMapping.cs
new file mode 100644
index 00000000..fb66a8c2
--- /dev/null
+++ b/src/System.Web.Razor/Generator/GeneratedCodeMapping.cs
@@ -0,0 +1,99 @@
+using System.Globalization;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.Razor.Generator
+{
+ public struct GeneratedCodeMapping
+ {
+ public GeneratedCodeMapping(int startLine, int startColumn, int startGeneratedColumn, int codeLength)
+ : this(null, startLine, startColumn, startGeneratedColumn, codeLength)
+ {
+ }
+
+ public GeneratedCodeMapping(int startOffset, int startLine, int startColumn, int startGeneratedColumn, int codeLength)
+ : this((int?)startOffset, startLine, startColumn, startGeneratedColumn, codeLength)
+ {
+ }
+
+ private GeneratedCodeMapping(int? startOffset, int startLine, int startColumn, int startGeneratedColumn, int codeLength)
+ : this()
+ {
+ if (startLine < 0)
+ {
+ throw new ArgumentOutOfRangeException("startLine", String.Format(CultureInfo.CurrentCulture, CommonResources.Argument_Must_Be_GreaterThanOrEqualTo, "startLine", "0"));
+ }
+ if (startColumn < 0)
+ {
+ throw new ArgumentOutOfRangeException("startColumn", String.Format(CultureInfo.CurrentCulture, CommonResources.Argument_Must_Be_GreaterThanOrEqualTo, "startColumn", "0"));
+ }
+ if (startGeneratedColumn < 0)
+ {
+ throw new ArgumentOutOfRangeException("startGeneratedColumn", String.Format(CultureInfo.CurrentCulture, CommonResources.Argument_Must_Be_GreaterThanOrEqualTo, "startGeneratedColumn", "0"));
+ }
+ if (codeLength < 0)
+ {
+ throw new ArgumentOutOfRangeException("codeLength", String.Format(CultureInfo.CurrentCulture, CommonResources.Argument_Must_Be_GreaterThanOrEqualTo, "codeLength", "0"));
+ }
+
+ StartOffset = startOffset;
+ StartLine = startLine;
+ StartColumn = startColumn;
+ StartGeneratedColumn = startGeneratedColumn;
+ CodeLength = codeLength;
+ }
+
+ public int? StartOffset { get; set; }
+ public int CodeLength { get; set; }
+ public int StartColumn { get; set; }
+ public int StartGeneratedColumn { get; set; }
+ public int StartLine { get; set; }
+
+ public override bool Equals(object obj)
+ {
+ if (!(obj is GeneratedCodeMapping))
+ {
+ return false;
+ }
+ GeneratedCodeMapping other = (GeneratedCodeMapping)obj;
+ return CodeLength == other.CodeLength &&
+ StartColumn == other.StartColumn &&
+ StartGeneratedColumn == other.StartGeneratedColumn &&
+ StartLine == other.StartLine &&
+ // Null means it matches the other no matter what.
+ (StartOffset == null || other.StartOffset == null || StartOffset.Equals(other.StartOffset));
+ }
+
+ public override string ToString()
+ {
+ return String.Format(
+ CultureInfo.CurrentCulture,
+ "({0}, {1}, {2}) -> (?, {3}) [{4}]",
+ StartOffset == null ? "?" : StartOffset.Value.ToString(CultureInfo.CurrentCulture),
+ StartLine,
+ StartColumn,
+ StartGeneratedColumn,
+ CodeLength);
+ }
+
+ public override int GetHashCode()
+ {
+ return HashCodeCombiner.Start()
+ .Add(CodeLength)
+ .Add(StartColumn)
+ .Add(StartGeneratedColumn)
+ .Add(StartLine)
+ .Add(StartOffset)
+ .CombinedHash;
+ }
+
+ public static bool operator ==(GeneratedCodeMapping left, GeneratedCodeMapping right)
+ {
+ return left.Equals(right);
+ }
+
+ public static bool operator !=(GeneratedCodeMapping left, GeneratedCodeMapping right)
+ {
+ return !left.Equals(right);
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Generator/HelperCodeGenerator.cs b/src/System.Web.Razor/Generator/HelperCodeGenerator.cs
new file mode 100644
index 00000000..8f955574
--- /dev/null
+++ b/src/System.Web.Razor/Generator/HelperCodeGenerator.cs
@@ -0,0 +1,112 @@
+using System.CodeDom;
+using System.Globalization;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Text;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.Razor.Generator
+{
+ public class HelperCodeGenerator : BlockCodeGenerator
+ {
+ private const string HelperWriterName = "__razor_helper_writer";
+
+ private CodeWriter _writer;
+ private string _oldWriter;
+ private IDisposable _statementCollectorToken;
+
+ public HelperCodeGenerator(LocationTagged<string> signature, bool headerComplete)
+ {
+ Signature = signature;
+ HeaderComplete = headerComplete;
+ }
+
+ public LocationTagged<string> Signature { get; private set; }
+ public LocationTagged<string> Footer { get; set; }
+ public bool HeaderComplete { get; private set; }
+
+ public override void GenerateStartBlockCode(Block target, CodeGeneratorContext context)
+ {
+ _writer = context.CreateCodeWriter();
+
+ string prefix = context.BuildCodeString(
+ cw => cw.WriteHelperHeaderPrefix(context.Host.GeneratedClassContext.TemplateTypeName, context.Host.StaticHelpers));
+
+ _writer.WriteLinePragma(
+ context.GenerateLinePragma(Signature.Location, prefix.Length, Signature.Value.Length));
+ _writer.WriteSnippet(prefix);
+ _writer.WriteSnippet(Signature);
+ if (HeaderComplete)
+ {
+ _writer.WriteHelperHeaderSuffix(context.Host.GeneratedClassContext.TemplateTypeName);
+ }
+ _writer.WriteLinePragma(null);
+ if (HeaderComplete)
+ {
+ _writer.WriteReturn();
+ _writer.WriteStartConstructor(context.Host.GeneratedClassContext.TemplateTypeName);
+ _writer.WriteStartLambdaDelegate(HelperWriterName);
+ }
+
+ _statementCollectorToken = context.ChangeStatementCollector(AddStatementToHelper);
+ _oldWriter = context.TargetWriterName;
+ context.TargetWriterName = HelperWriterName;
+ }
+
+ public override void GenerateEndBlockCode(Block target, CodeGeneratorContext context)
+ {
+ _statementCollectorToken.Dispose();
+ if (HeaderComplete)
+ {
+ _writer.WriteEndLambdaDelegate();
+ _writer.WriteEndConstructor();
+ _writer.WriteEndStatement();
+ }
+ if (Footer != null && !String.IsNullOrEmpty(Footer.Value))
+ {
+ _writer.WriteLinePragma(
+ context.GenerateLinePragma(Footer.Location, 0, Footer.Value.Length));
+ _writer.WriteSnippet(Footer);
+ _writer.WriteLinePragma();
+ }
+ _writer.WriteHelperTrailer();
+
+ context.GeneratedClass.Members.Add(new CodeSnippetTypeMember(_writer.Content));
+ context.TargetWriterName = _oldWriter;
+ }
+
+ public override bool Equals(object obj)
+ {
+ HelperCodeGenerator other = obj as HelperCodeGenerator;
+ return other != null &&
+ base.Equals(other) &&
+ HeaderComplete == other.HeaderComplete &&
+ Equals(Signature, other.Signature);
+ }
+
+ public override int GetHashCode()
+ {
+ return HashCodeCombiner.Start()
+ .Add(base.GetHashCode())
+ .Add(Signature)
+ .CombinedHash;
+ }
+
+ public override string ToString()
+ {
+ return "Helper:" + Signature.ToString("F", CultureInfo.CurrentCulture) + ";" + (HeaderComplete ? "C" : "I");
+ }
+
+ private void AddStatementToHelper(string statement, CodeLinePragma pragma)
+ {
+ if (pragma != null)
+ {
+ _writer.WriteLinePragma(pragma);
+ }
+ _writer.WriteSnippet(statement);
+ if (pragma != null)
+ {
+ _writer.WriteLinePragma();
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Generator/HybridCodeGenerator.cs b/src/System.Web.Razor/Generator/HybridCodeGenerator.cs
new file mode 100644
index 00000000..c53dbf8a
--- /dev/null
+++ b/src/System.Web.Razor/Generator/HybridCodeGenerator.cs
@@ -0,0 +1,19 @@
+using System.Web.Razor.Parser.SyntaxTree;
+
+namespace System.Web.Razor.Generator
+{
+ public abstract class HybridCodeGenerator : CodeGeneratorBase, ISpanCodeGenerator, IBlockCodeGenerator
+ {
+ public virtual void GenerateStartBlockCode(Block target, CodeGeneratorContext context)
+ {
+ }
+
+ public virtual void GenerateEndBlockCode(Block target, CodeGeneratorContext context)
+ {
+ }
+
+ public virtual void GenerateCode(Span target, CodeGeneratorContext context)
+ {
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Generator/IBlockCodeGenerator.cs b/src/System.Web.Razor/Generator/IBlockCodeGenerator.cs
new file mode 100644
index 00000000..d0b67a30
--- /dev/null
+++ b/src/System.Web.Razor/Generator/IBlockCodeGenerator.cs
@@ -0,0 +1,10 @@
+using System.Web.Razor.Parser.SyntaxTree;
+
+namespace System.Web.Razor.Generator
+{
+ public interface IBlockCodeGenerator
+ {
+ void GenerateStartBlockCode(Block target, CodeGeneratorContext context);
+ void GenerateEndBlockCode(Block target, CodeGeneratorContext context);
+ }
+}
diff --git a/src/System.Web.Razor/Generator/ISpanCodeGenerator.cs b/src/System.Web.Razor/Generator/ISpanCodeGenerator.cs
new file mode 100644
index 00000000..3e558a08
--- /dev/null
+++ b/src/System.Web.Razor/Generator/ISpanCodeGenerator.cs
@@ -0,0 +1,9 @@
+using System.Web.Razor.Parser.SyntaxTree;
+
+namespace System.Web.Razor.Generator
+{
+ public interface ISpanCodeGenerator
+ {
+ void GenerateCode(Span target, CodeGeneratorContext context);
+ }
+}
diff --git a/src/System.Web.Razor/Generator/LiteralAttributeCodeGenerator.cs b/src/System.Web.Razor/Generator/LiteralAttributeCodeGenerator.cs
new file mode 100644
index 00000000..07d78ddd
--- /dev/null
+++ b/src/System.Web.Razor/Generator/LiteralAttributeCodeGenerator.cs
@@ -0,0 +1,111 @@
+using System.Globalization;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Text;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.Razor.Generator
+{
+ public class LiteralAttributeCodeGenerator : SpanCodeGenerator
+ {
+ public LiteralAttributeCodeGenerator(LocationTagged<string> prefix, LocationTagged<SpanCodeGenerator> valueGenerator)
+ {
+ Prefix = prefix;
+ ValueGenerator = valueGenerator;
+ }
+
+ public LiteralAttributeCodeGenerator(LocationTagged<string> prefix, LocationTagged<string> value)
+ {
+ Prefix = prefix;
+ Value = value;
+ }
+
+ public LocationTagged<string> Prefix { get; private set; }
+ public LocationTagged<string> Value { get; private set; }
+ public LocationTagged<SpanCodeGenerator> ValueGenerator { get; private set; }
+
+ public override void GenerateCode(Span target, CodeGeneratorContext context)
+ {
+ if (context.Host.DesignTimeMode)
+ {
+ return;
+ }
+ ExpressionRenderingMode oldMode = context.ExpressionRenderingMode;
+ context.BufferStatementFragment(context.BuildCodeString(cw =>
+ {
+ cw.WriteParameterSeparator();
+ cw.WriteStartMethodInvoke("Tuple.Create");
+ cw.WriteLocationTaggedString(Prefix);
+ cw.WriteParameterSeparator();
+ if (ValueGenerator != null)
+ {
+ cw.WriteStartMethodInvoke("Tuple.Create", "System.Object", "System.Int32");
+ context.ExpressionRenderingMode = ExpressionRenderingMode.InjectCode;
+ }
+ else
+ {
+ cw.WriteLocationTaggedString(Value);
+ cw.WriteParameterSeparator();
+ // literal: true - This attribute value is a literal value
+ cw.WriteBooleanLiteral(true);
+ cw.WriteEndMethodInvoke();
+
+ // In VB, we need a line continuation
+ cw.WriteLineContinuation();
+ }
+ }));
+ if (ValueGenerator != null)
+ {
+ ValueGenerator.Value.GenerateCode(target, context);
+ context.FlushBufferedStatement();
+ context.ExpressionRenderingMode = oldMode;
+ context.AddStatement(context.BuildCodeString(cw =>
+ {
+ cw.WriteParameterSeparator();
+ cw.WriteSnippet(ValueGenerator.Location.AbsoluteIndex.ToString(CultureInfo.CurrentCulture));
+ cw.WriteEndMethodInvoke();
+ cw.WriteParameterSeparator();
+ // literal: false - This attribute value is not a literal value, it is dynamically generated
+ cw.WriteBooleanLiteral(false);
+ cw.WriteEndMethodInvoke();
+
+ // In VB, we need a line continuation
+ cw.WriteLineContinuation();
+ }));
+ }
+ else
+ {
+ context.FlushBufferedStatement();
+ }
+ }
+
+ public override string ToString()
+ {
+ if (ValueGenerator == null)
+ {
+ return String.Format(CultureInfo.CurrentCulture, "LitAttr:{0:F},{1:F}", Prefix, Value);
+ }
+ else
+ {
+ return String.Format(CultureInfo.CurrentCulture, "LitAttr:{0:F},<Sub:{1:F}>", Prefix, ValueGenerator);
+ }
+ }
+
+ public override bool Equals(object obj)
+ {
+ LiteralAttributeCodeGenerator other = obj as LiteralAttributeCodeGenerator;
+ return other != null &&
+ Equals(other.Prefix, Prefix) &&
+ Equals(other.Value, Value) &&
+ Equals(other.ValueGenerator, ValueGenerator);
+ }
+
+ public override int GetHashCode()
+ {
+ return HashCodeCombiner.Start()
+ .Add(Prefix)
+ .Add(Value)
+ .Add(ValueGenerator)
+ .CombinedHash;
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Generator/MarkupCodeGenerator.cs b/src/System.Web.Razor/Generator/MarkupCodeGenerator.cs
new file mode 100644
index 00000000..c1195087
--- /dev/null
+++ b/src/System.Web.Razor/Generator/MarkupCodeGenerator.cs
@@ -0,0 +1,61 @@
+using System.Web.Razor.Parser.SyntaxTree;
+
+namespace System.Web.Razor.Generator
+{
+ public class MarkupCodeGenerator : SpanCodeGenerator
+ {
+ public override void GenerateCode(Span target, CodeGeneratorContext context)
+ {
+ if (!context.Host.DesignTimeMode && String.IsNullOrEmpty(target.Content))
+ {
+ return;
+ }
+
+ if (context.Host.EnableInstrumentation)
+ {
+ context.AddContextCall(target, context.Host.GeneratedClassContext.BeginContextMethodName, isLiteral: true);
+ }
+
+ if (!String.IsNullOrEmpty(target.Content) && !context.Host.DesignTimeMode)
+ {
+ string code = context.BuildCodeString(cw =>
+ {
+ if (!String.IsNullOrEmpty(context.TargetWriterName))
+ {
+ cw.WriteStartMethodInvoke(context.Host.GeneratedClassContext.WriteLiteralToMethodName);
+ cw.WriteSnippet(context.TargetWriterName);
+ cw.WriteParameterSeparator();
+ }
+ else
+ {
+ cw.WriteStartMethodInvoke(context.Host.GeneratedClassContext.WriteLiteralMethodName);
+ }
+ cw.WriteStringLiteral(target.Content);
+ cw.WriteEndMethodInvoke();
+ cw.WriteEndStatement();
+ });
+ context.AddStatement(code);
+ }
+
+ if (context.Host.EnableInstrumentation)
+ {
+ context.AddContextCall(target, context.Host.GeneratedClassContext.EndContextMethodName, isLiteral: true);
+ }
+ }
+
+ public override string ToString()
+ {
+ return "Markup";
+ }
+
+ public override bool Equals(object obj)
+ {
+ return obj is MarkupCodeGenerator;
+ }
+
+ public override int GetHashCode()
+ {
+ return base.GetHashCode();
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Generator/RazorCodeGenerator.cs b/src/System.Web.Razor/Generator/RazorCodeGenerator.cs
new file mode 100644
index 00000000..89503199
--- /dev/null
+++ b/src/System.Web.Razor/Generator/RazorCodeGenerator.cs
@@ -0,0 +1,101 @@
+using System.CodeDom;
+using System.Linq;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.Razor.Generator
+{
+ public abstract class RazorCodeGenerator : ParserVisitor
+ {
+ private CodeGeneratorContext _context;
+
+ protected RazorCodeGenerator(string className, string rootNamespaceName, string sourceFileName, RazorEngineHost host)
+ {
+ if (String.IsNullOrEmpty(className))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "className");
+ }
+ if (rootNamespaceName == null)
+ {
+ throw new ArgumentNullException("rootNamespaceName");
+ }
+ if (host == null)
+ {
+ throw new ArgumentNullException("host");
+ }
+
+ ClassName = className;
+ RootNamespaceName = rootNamespaceName;
+ SourceFileName = sourceFileName;
+ GenerateLinePragmas = String.IsNullOrEmpty(SourceFileName) ? false : true;
+ Host = host;
+ }
+
+ // Data pulled from constructor
+ public string ClassName { get; private set; }
+ public string RootNamespaceName { get; private set; }
+ public string SourceFileName { get; private set; }
+ public RazorEngineHost Host { get; private set; }
+
+ // Generation settings
+ public bool GenerateLinePragmas { get; set; }
+ public bool DesignTimeMode { get; set; }
+
+ public CodeGeneratorContext Context
+ {
+ get
+ {
+ EnsureContextInitialized();
+ return _context;
+ }
+ }
+
+ internal virtual Func<CodeWriter> CodeWriterFactory
+ {
+ get { return null; }
+ }
+
+ public override void VisitStartBlock(Block block)
+ {
+ block.CodeGenerator.GenerateStartBlockCode(block, Context);
+ }
+
+ public override void VisitEndBlock(Block block)
+ {
+ block.CodeGenerator.GenerateEndBlockCode(block, Context);
+ }
+
+ public override void VisitSpan(Span span)
+ {
+ span.CodeGenerator.GenerateCode(span, Context);
+ }
+
+ public override void OnComplete()
+ {
+ Context.FlushBufferedStatement();
+ }
+
+ private void EnsureContextInitialized()
+ {
+ if (_context == null)
+ {
+ _context = CodeGeneratorContext.Create(Host, CodeWriterFactory, ClassName, RootNamespaceName, SourceFileName, GenerateLinePragmas);
+ Initialize(_context);
+ }
+ }
+
+ protected virtual void Initialize(CodeGeneratorContext context)
+ {
+ context.Namespace.Imports.AddRange(Host.NamespaceImports.Select(s => new CodeNamespaceImport(s)).ToArray());
+
+ if (!String.IsNullOrEmpty(Host.DefaultBaseClass))
+ {
+ context.GeneratedClass.BaseTypes.Add(new CodeTypeReference(Host.DefaultBaseClass));
+ }
+
+ // Dev10 Bug 937438: Generate explicit Parameter-less constructor on Razor generated class
+ context.GeneratedClass.Members.Add(new CodeConstructor() { Attributes = MemberAttributes.Public });
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Generator/RazorCommentCodeGenerator.cs b/src/System.Web.Razor/Generator/RazorCommentCodeGenerator.cs
new file mode 100644
index 00000000..40ea700b
--- /dev/null
+++ b/src/System.Web.Razor/Generator/RazorCommentCodeGenerator.cs
@@ -0,0 +1,18 @@
+using System.Web.Razor.Parser.SyntaxTree;
+
+namespace System.Web.Razor.Generator
+{
+ public class RazorCommentCodeGenerator : BlockCodeGenerator
+ {
+ public override void GenerateStartBlockCode(Block target, CodeGeneratorContext context)
+ {
+ // Flush the buffered statement since we're interrupting it with a comment.
+ if (!String.IsNullOrEmpty(context.CurrentBufferedStatement))
+ {
+ context.MarkEndOfGeneratedCode();
+ context.BufferStatementFragment(context.BuildCodeString(cw => cw.WriteLineContinuation()));
+ }
+ context.FlushBufferedStatement();
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Generator/RazorDirectiveAttributeCodeGenerator.cs b/src/System.Web.Razor/Generator/RazorDirectiveAttributeCodeGenerator.cs
new file mode 100644
index 00000000..d9a27e92
--- /dev/null
+++ b/src/System.Web.Razor/Generator/RazorDirectiveAttributeCodeGenerator.cs
@@ -0,0 +1,52 @@
+using System.CodeDom;
+using System.Web.Razor.Parser.SyntaxTree;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.Razor.Generator
+{
+ public class RazorDirectiveAttributeCodeGenerator : SpanCodeGenerator
+ {
+ public RazorDirectiveAttributeCodeGenerator(string name, string value)
+ {
+ if (String.IsNullOrEmpty(name))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "name");
+ }
+ Name = name;
+ Value = value ?? String.Empty; // Coerce to empty string if it was null.
+ }
+
+ public string Name { get; private set; }
+
+ public string Value { get; private set; }
+
+ public override void GenerateCode(Span target, CodeGeneratorContext context)
+ {
+ var attributeType = new CodeTypeReference(typeof(RazorDirectiveAttribute));
+ var attributeDeclaration = new CodeAttributeDeclaration(
+ attributeType,
+ new CodeAttributeArgument(new CodePrimitiveExpression(Name)),
+ new CodeAttributeArgument(new CodePrimitiveExpression(Value)));
+ context.GeneratedClass.CustomAttributes.Add(attributeDeclaration);
+ }
+
+ public override string ToString()
+ {
+ return "Directive: " + Name + ", Value: " + Value;
+ }
+
+ public override bool Equals(object obj)
+ {
+ RazorDirectiveAttributeCodeGenerator other = obj as RazorDirectiveAttributeCodeGenerator;
+ return other != null &&
+ Name.Equals(other.Name, StringComparison.OrdinalIgnoreCase) &&
+ Value.Equals(other.Value, StringComparison.OrdinalIgnoreCase);
+ }
+
+ public override int GetHashCode()
+ {
+ return Tuple.Create(Name.ToUpperInvariant(), Value.ToUpperInvariant())
+ .GetHashCode();
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Generator/ResolveUrlCodeGenerator.cs b/src/System.Web.Razor/Generator/ResolveUrlCodeGenerator.cs
new file mode 100644
index 00000000..3060713e
--- /dev/null
+++ b/src/System.Web.Razor/Generator/ResolveUrlCodeGenerator.cs
@@ -0,0 +1,90 @@
+using System.Web.Razor.Parser.SyntaxTree;
+
+namespace System.Web.Razor.Generator
+{
+ public class ResolveUrlCodeGenerator : SpanCodeGenerator
+ {
+ public override void GenerateCode(Span target, CodeGeneratorContext context)
+ {
+ // Check if the host supports it
+ if (String.IsNullOrEmpty(context.Host.GeneratedClassContext.ResolveUrlMethodName))
+ {
+ // Nope, just use the default MarkupCodeGenerator behavior
+ new MarkupCodeGenerator().GenerateCode(target, context);
+ return;
+ }
+
+ if (!context.Host.DesignTimeMode && String.IsNullOrEmpty(target.Content))
+ {
+ return;
+ }
+
+ if (context.Host.EnableInstrumentation && context.ExpressionRenderingMode == ExpressionRenderingMode.WriteToOutput)
+ {
+ // Add a non-literal context call (non-literal because the expanded URL will not match the source character-by-character)
+ context.AddContextCall(target, context.Host.GeneratedClassContext.BeginContextMethodName, isLiteral: false);
+ }
+
+ if (!String.IsNullOrEmpty(target.Content) && !context.Host.DesignTimeMode)
+ {
+ string code = context.BuildCodeString(cw =>
+ {
+ if (context.ExpressionRenderingMode == ExpressionRenderingMode.WriteToOutput)
+ {
+ if (!String.IsNullOrEmpty(context.TargetWriterName))
+ {
+ cw.WriteStartMethodInvoke(context.Host.GeneratedClassContext.WriteLiteralToMethodName);
+ cw.WriteSnippet(context.TargetWriterName);
+ cw.WriteParameterSeparator();
+ }
+ else
+ {
+ cw.WriteStartMethodInvoke(context.Host.GeneratedClassContext.WriteLiteralMethodName);
+ }
+ }
+ cw.WriteStartMethodInvoke(context.Host.GeneratedClassContext.ResolveUrlMethodName);
+ cw.WriteStringLiteral(target.Content);
+ cw.WriteEndMethodInvoke();
+
+ if (context.ExpressionRenderingMode == ExpressionRenderingMode.WriteToOutput)
+ {
+ cw.WriteEndMethodInvoke();
+ cw.WriteEndStatement();
+ }
+ else
+ {
+ cw.WriteLineContinuation();
+ }
+ });
+ if (context.ExpressionRenderingMode == ExpressionRenderingMode.WriteToOutput)
+ {
+ context.AddStatement(code);
+ }
+ else
+ {
+ context.BufferStatementFragment(code);
+ }
+ }
+
+ if (context.Host.EnableInstrumentation && context.ExpressionRenderingMode == ExpressionRenderingMode.WriteToOutput)
+ {
+ context.AddContextCall(target, context.Host.GeneratedClassContext.EndContextMethodName, isLiteral: false);
+ }
+ }
+
+ public override string ToString()
+ {
+ return "VirtualPath";
+ }
+
+ public override bool Equals(object obj)
+ {
+ return obj is ResolveUrlCodeGenerator;
+ }
+
+ public override int GetHashCode()
+ {
+ return base.GetHashCode();
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Generator/SectionCodeGenerator.cs b/src/System.Web.Razor/Generator/SectionCodeGenerator.cs
new file mode 100644
index 00000000..2d53a7fb
--- /dev/null
+++ b/src/System.Web.Razor/Generator/SectionCodeGenerator.cs
@@ -0,0 +1,59 @@
+using System.Web.Razor.Parser.SyntaxTree;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.Razor.Generator
+{
+ public class SectionCodeGenerator : BlockCodeGenerator
+ {
+ public SectionCodeGenerator(string sectionName)
+ {
+ SectionName = sectionName;
+ }
+
+ public string SectionName { get; private set; }
+
+ public override void GenerateStartBlockCode(Block target, CodeGeneratorContext context)
+ {
+ string startBlock = context.BuildCodeString(cw =>
+ {
+ cw.WriteStartMethodInvoke(context.Host.GeneratedClassContext.DefineSectionMethodName);
+ cw.WriteStringLiteral(SectionName);
+ cw.WriteParameterSeparator();
+ cw.WriteStartLambdaDelegate();
+ });
+ context.AddStatement(startBlock);
+ }
+
+ public override void GenerateEndBlockCode(Block target, CodeGeneratorContext context)
+ {
+ string startBlock = context.BuildCodeString(cw =>
+ {
+ cw.WriteEndLambdaDelegate();
+ cw.WriteEndMethodInvoke();
+ cw.WriteEndStatement();
+ });
+ context.AddStatement(startBlock);
+ }
+
+ public override bool Equals(object obj)
+ {
+ SectionCodeGenerator other = obj as SectionCodeGenerator;
+ return other != null &&
+ base.Equals(other) &&
+ String.Equals(SectionName, other.SectionName, StringComparison.Ordinal);
+ }
+
+ public override int GetHashCode()
+ {
+ return HashCodeCombiner.Start()
+ .Add(base.GetHashCode())
+ .Add(SectionName)
+ .CombinedHash;
+ }
+
+ public override string ToString()
+ {
+ return "Section:" + SectionName;
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Generator/SetBaseTypeCodeGenerator.cs b/src/System.Web.Razor/Generator/SetBaseTypeCodeGenerator.cs
new file mode 100644
index 00000000..609e8c60
--- /dev/null
+++ b/src/System.Web.Razor/Generator/SetBaseTypeCodeGenerator.cs
@@ -0,0 +1,62 @@
+using System.CodeDom;
+using System.Web.Razor.Parser.SyntaxTree;
+
+namespace System.Web.Razor.Generator
+{
+ public class SetBaseTypeCodeGenerator : SpanCodeGenerator
+ {
+ public SetBaseTypeCodeGenerator(string baseType)
+ {
+ BaseType = baseType;
+ }
+
+ public string BaseType { get; private set; }
+
+ public override void GenerateCode(Span target, CodeGeneratorContext context)
+ {
+ context.GeneratedClass.BaseTypes.Clear();
+ context.GeneratedClass.BaseTypes.Add(new CodeTypeReference(ResolveType(context, BaseType.Trim())));
+
+ if (context.Host.DesignTimeMode)
+ {
+ int generatedCodeStart = 0;
+ string code = context.BuildCodeString(cw =>
+ {
+ generatedCodeStart = cw.WriteVariableDeclaration(target.Content, "__inheritsHelper", null);
+ cw.WriteEndStatement();
+ });
+
+ int padding = CalculatePadding(target, generatedCodeStart);
+
+ CodeSnippetStatement stmt = new CodeSnippetStatement(
+ Pad(code, target, generatedCodeStart))
+ {
+ LinePragma = context.GenerateLinePragma(target, generatedCodeStart + padding)
+ };
+ context.AddDesignTimeHelperStatement(stmt);
+ }
+ }
+
+ protected virtual string ResolveType(CodeGeneratorContext context, string baseType)
+ {
+ return baseType;
+ }
+
+ public override string ToString()
+ {
+ return "Base:" + BaseType;
+ }
+
+ public override bool Equals(object obj)
+ {
+ SetBaseTypeCodeGenerator other = obj as SetBaseTypeCodeGenerator;
+ return other != null &&
+ String.Equals(BaseType, other.BaseType, StringComparison.Ordinal);
+ }
+
+ public override int GetHashCode()
+ {
+ return BaseType.GetHashCode();
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Generator/SetLayoutCodeGenerator.cs b/src/System.Web.Razor/Generator/SetLayoutCodeGenerator.cs
new file mode 100644
index 00000000..43da0cb5
--- /dev/null
+++ b/src/System.Web.Razor/Generator/SetLayoutCodeGenerator.cs
@@ -0,0 +1,42 @@
+using System.CodeDom;
+using System.Web.Razor.Parser.SyntaxTree;
+
+namespace System.Web.Razor.Generator
+{
+ public class SetLayoutCodeGenerator : SpanCodeGenerator
+ {
+ public SetLayoutCodeGenerator(string layoutPath)
+ {
+ LayoutPath = layoutPath;
+ }
+
+ public string LayoutPath { get; set; }
+
+ public override void GenerateCode(Span target, CodeGeneratorContext context)
+ {
+ if (!context.Host.DesignTimeMode && !String.IsNullOrEmpty(context.Host.GeneratedClassContext.LayoutPropertyName))
+ {
+ context.TargetMethod.Statements.Add(
+ new CodeAssignStatement(
+ new CodePropertyReferenceExpression(null, context.Host.GeneratedClassContext.LayoutPropertyName),
+ new CodePrimitiveExpression(LayoutPath)));
+ }
+ }
+
+ public override string ToString()
+ {
+ return "Layout: " + LayoutPath;
+ }
+
+ public override bool Equals(object obj)
+ {
+ SetLayoutCodeGenerator other = obj as SetLayoutCodeGenerator;
+ return other != null && String.Equals(other.LayoutPath, LayoutPath, StringComparison.Ordinal);
+ }
+
+ public override int GetHashCode()
+ {
+ return LayoutPath.GetHashCode();
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Generator/SetVBOptionCodeGenerator.cs b/src/System.Web.Razor/Generator/SetVBOptionCodeGenerator.cs
new file mode 100644
index 00000000..5c347dc5
--- /dev/null
+++ b/src/System.Web.Razor/Generator/SetVBOptionCodeGenerator.cs
@@ -0,0 +1,41 @@
+using System.Web.Razor.Parser.SyntaxTree;
+
+namespace System.Web.Razor.Generator
+{
+ public class SetVBOptionCodeGenerator : SpanCodeGenerator
+ {
+ public static readonly string StrictCodeDomOptionName = "AllowLateBound";
+ public static readonly string ExplicitCodeDomOptionName = "RequireVariableDeclaration";
+
+ public SetVBOptionCodeGenerator(string optionName, bool value)
+ {
+ OptionName = optionName;
+ Value = value;
+ }
+
+ // CodeDOM Option Name, which is NOT the same as the VB Option Name
+ public string OptionName { get; private set; }
+ public bool Value { get; private set; }
+
+ public static SetVBOptionCodeGenerator Strict(bool onOffValue)
+ {
+ // Strict On = AllowLateBound Off
+ return new SetVBOptionCodeGenerator(StrictCodeDomOptionName, !onOffValue);
+ }
+
+ public static SetVBOptionCodeGenerator Explicit(bool onOffValue)
+ {
+ return new SetVBOptionCodeGenerator(ExplicitCodeDomOptionName, onOffValue);
+ }
+
+ public override void GenerateCode(Span target, CodeGeneratorContext context)
+ {
+ context.CompileUnit.UserData[OptionName] = Value;
+ }
+
+ public override string ToString()
+ {
+ return "Option:" + OptionName + "=" + Value;
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Generator/SpanCodeGenerator.cs b/src/System.Web.Razor/Generator/SpanCodeGenerator.cs
new file mode 100644
index 00000000..cc210119
--- /dev/null
+++ b/src/System.Web.Razor/Generator/SpanCodeGenerator.cs
@@ -0,0 +1,37 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Web.Razor.Parser.SyntaxTree;
+
+namespace System.Web.Razor.Generator
+{
+ public abstract class SpanCodeGenerator : CodeGeneratorBase, ISpanCodeGenerator
+ {
+ [SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", Justification = "This class has no instance state")]
+ public static readonly ISpanCodeGenerator Null = new NullSpanCodeGenerator();
+
+ public virtual void GenerateCode(Span target, CodeGeneratorContext context)
+ {
+ }
+
+ public override bool Equals(object obj)
+ {
+ return (obj as ISpanCodeGenerator) != null;
+ }
+
+ public override int GetHashCode()
+ {
+ return base.GetHashCode();
+ }
+
+ private class NullSpanCodeGenerator : ISpanCodeGenerator
+ {
+ public void GenerateCode(Span target, CodeGeneratorContext context)
+ {
+ }
+
+ public override string ToString()
+ {
+ return "None";
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Generator/StatementCodeGenerator.cs b/src/System.Web.Razor/Generator/StatementCodeGenerator.cs
new file mode 100644
index 00000000..6a409ea5
--- /dev/null
+++ b/src/System.Web.Razor/Generator/StatementCodeGenerator.cs
@@ -0,0 +1,52 @@
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+
+namespace System.Web.Razor.Generator
+{
+ public class StatementCodeGenerator : SpanCodeGenerator
+ {
+ public override void GenerateCode(Span target, CodeGeneratorContext context)
+ {
+ context.FlushBufferedStatement();
+
+ string generatedCode = context.BuildCodeString(cw =>
+ {
+ cw.WriteSnippet(target.Content);
+ });
+
+ int startGeneratedCode = target.Start.CharacterIndex;
+ generatedCode = Pad(generatedCode, target);
+
+ // Is this the span immediately following "@"?
+ if (context.Host.DesignTimeMode &&
+ !String.IsNullOrEmpty(generatedCode) &&
+ Char.IsWhiteSpace(generatedCode[0]) &&
+ target.Previous != null &&
+ target.Previous.Kind == SpanKind.Transition &&
+ String.Equals(target.Previous.Content, SyntaxConstants.TransitionString))
+ {
+ generatedCode = generatedCode.Substring(1);
+ startGeneratedCode--;
+ }
+
+ context.AddStatement(
+ generatedCode,
+ context.GenerateLinePragma(target, startGeneratedCode));
+ }
+
+ public override string ToString()
+ {
+ return "Stmt";
+ }
+
+ public override bool Equals(object obj)
+ {
+ return obj is StatementCodeGenerator;
+ }
+
+ public override int GetHashCode()
+ {
+ return base.GetHashCode();
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Generator/TemplateBlockCodeGenerator.cs b/src/System.Web.Razor/Generator/TemplateBlockCodeGenerator.cs
new file mode 100644
index 00000000..2014ab97
--- /dev/null
+++ b/src/System.Web.Razor/Generator/TemplateBlockCodeGenerator.cs
@@ -0,0 +1,42 @@
+using System.Web.Razor.Parser.SyntaxTree;
+
+namespace System.Web.Razor.Generator
+{
+ public class TemplateBlockCodeGenerator : BlockCodeGenerator
+ {
+ private const string TemplateWriterName = "__razor_template_writer";
+ private const string ItemParameterName = "item";
+
+ private string _oldTargetWriter;
+
+ public override void GenerateStartBlockCode(Block target, CodeGeneratorContext context)
+ {
+ string generatedCode = context.BuildCodeString(cw =>
+ {
+ cw.WriteStartLambdaExpression(ItemParameterName);
+ cw.WriteStartConstructor(context.Host.GeneratedClassContext.TemplateTypeName);
+ cw.WriteStartLambdaDelegate(TemplateWriterName);
+ });
+
+ context.MarkEndOfGeneratedCode();
+ context.BufferStatementFragment(generatedCode);
+ context.FlushBufferedStatement();
+
+ _oldTargetWriter = context.TargetWriterName;
+ context.TargetWriterName = TemplateWriterName;
+ }
+
+ public override void GenerateEndBlockCode(Block target, CodeGeneratorContext context)
+ {
+ string generatedCode = context.BuildCodeString(cw =>
+ {
+ cw.WriteEndLambdaDelegate();
+ cw.WriteEndConstructor();
+ cw.WriteEndLambdaExpression();
+ });
+
+ context.BufferStatementFragment(generatedCode);
+ context.TargetWriterName = _oldTargetWriter;
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Generator/TypeMemberCodeGenerator.cs b/src/System.Web.Razor/Generator/TypeMemberCodeGenerator.cs
new file mode 100644
index 00000000..e7eb6fd6
--- /dev/null
+++ b/src/System.Web.Razor/Generator/TypeMemberCodeGenerator.cs
@@ -0,0 +1,38 @@
+using System.CodeDom;
+using System.Web.Razor.Parser.SyntaxTree;
+
+namespace System.Web.Razor.Generator
+{
+ public class TypeMemberCodeGenerator : SpanCodeGenerator
+ {
+ public override void GenerateCode(Span target, CodeGeneratorContext context)
+ {
+ string generatedCode = context.BuildCodeString(cw =>
+ {
+ cw.WriteSnippet(target.Content);
+ });
+
+ context.GeneratedClass.Members.Add(
+ new CodeSnippetTypeMember(Pad(generatedCode, target))
+ {
+ LinePragma = context.GenerateLinePragma(target, target.Start.CharacterIndex)
+ });
+ }
+
+ public override string ToString()
+ {
+ return "TypeMember";
+ }
+
+ public override bool Equals(object obj)
+ {
+ return obj is TypeMemberCodeGenerator;
+ }
+
+ // C# complains at us if we don't provide an implementation, even one like this
+ public override int GetHashCode()
+ {
+ return base.GetHashCode();
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Generator/VBCodeWriter.cs b/src/System.Web.Razor/Generator/VBCodeWriter.cs
new file mode 100644
index 00000000..f83ed189
--- /dev/null
+++ b/src/System.Web.Razor/Generator/VBCodeWriter.cs
@@ -0,0 +1,198 @@
+namespace System.Web.Razor.Generator
+{
+ internal class VBCodeWriter : BaseCodeWriter
+ {
+ public override bool SupportsMidStatementLinePragmas
+ {
+ get { return false; }
+ }
+
+ protected internal override void WriteStartGenerics()
+ {
+ InnerWriter.Write("(Of ");
+ }
+
+ protected internal override void WriteEndGenerics()
+ {
+ InnerWriter.Write(")");
+ }
+
+ public override void WriteLineContinuation()
+ {
+ InnerWriter.Write(" _");
+ }
+
+ public override int WriteVariableDeclaration(string type, string name, string value)
+ {
+ InnerWriter.Write("Dim ");
+ InnerWriter.Write(name);
+ InnerWriter.Write(" As ");
+ int typePos = InnerWriter.GetStringBuilder().Length;
+ InnerWriter.Write(type);
+ if (!String.IsNullOrEmpty(value))
+ {
+ InnerWriter.Write(" = ");
+ InnerWriter.Write(value);
+ }
+ else
+ {
+ InnerWriter.Write(" = Nothing");
+ }
+ return typePos;
+ }
+
+ public override void WriteStringLiteral(string literal)
+ {
+ bool inQuotes = true;
+ InnerWriter.Write("\"");
+ for (int i = 0; i < literal.Length; i++)
+ {
+ switch (literal[i])
+ {
+ case '\t':
+ case '\n':
+ case '\r':
+ case '\0':
+ case '\u2028':
+ case '\u2029':
+ // Exit quotes
+ EnsureOutOfQuotes(ref inQuotes);
+
+ // Write concat character
+ InnerWriter.Write("&");
+
+ // Write character literal
+ WriteCharLiteral(literal[i]);
+ break;
+ case '"':
+ case '“':
+ case '”':
+ case (char)0xff02:
+ EnsureInQuotes(ref inQuotes);
+ InnerWriter.Write(literal[i]);
+ InnerWriter.Write(literal[i]);
+ break;
+ default:
+ EnsureInQuotes(ref inQuotes);
+ InnerWriter.Write(literal[i]);
+ break;
+ }
+ if (i > 0 && (i % 80) == 0)
+ {
+ if ((Char.IsHighSurrogate(literal[i]) && (i < (literal.Length - 1))) && Char.IsLowSurrogate(literal[i + 1]))
+ {
+ InnerWriter.Write(literal[++i]);
+ }
+ if (inQuotes)
+ {
+ InnerWriter.Write("\"");
+ }
+ inQuotes = true;
+ InnerWriter.Write("& _ ");
+ InnerWriter.Write(Environment.NewLine);
+ InnerWriter.Write('"');
+ }
+ }
+ EnsureOutOfQuotes(ref inQuotes);
+ }
+
+ protected internal override void EmitStartLambdaExpression(string[] parameterNames)
+ {
+ InnerWriter.Write("Function (");
+ WriteCommaSeparatedList(parameterNames, InnerWriter.Write);
+ InnerWriter.Write(") ");
+ }
+
+ protected internal override void EmitStartConstructor(string typeName)
+ {
+ InnerWriter.Write("New ");
+ InnerWriter.Write(typeName);
+ InnerWriter.Write("(");
+ }
+
+ protected internal override void EmitStartLambdaDelegate(string[] parameterNames)
+ {
+ InnerWriter.Write("Sub (");
+ WriteCommaSeparatedList(parameterNames, InnerWriter.Write);
+ InnerWriter.WriteLine(")");
+ }
+
+ protected internal override void EmitEndLambdaDelegate()
+ {
+ InnerWriter.Write("End Sub");
+ }
+
+ private void WriteCharLiteral(char literal)
+ {
+ InnerWriter.Write("Global.Microsoft.VisualBasic.ChrW(");
+ InnerWriter.Write((int)literal);
+ InnerWriter.Write(")");
+ }
+
+ private void EnsureInQuotes(ref bool inQuotes)
+ {
+ if (!inQuotes)
+ {
+ InnerWriter.Write("&\"");
+ inQuotes = true;
+ }
+ }
+
+ private void EnsureOutOfQuotes(ref bool inQuotes)
+ {
+ if (inQuotes)
+ {
+ InnerWriter.Write("\"");
+ inQuotes = false;
+ }
+ }
+
+ public override void WriteReturn()
+ {
+ InnerWriter.Write("Return ");
+ }
+
+ public override void WriteLinePragma(int? lineNumber, string fileName)
+ {
+ InnerWriter.WriteLine();
+ if (lineNumber != null)
+ {
+ InnerWriter.Write("#ExternalSource(\"");
+ InnerWriter.Write(fileName);
+ InnerWriter.Write("\", ");
+ InnerWriter.Write(lineNumber);
+ InnerWriter.WriteLine(")");
+ }
+ else
+ {
+ InnerWriter.WriteLine("#End ExternalSource");
+ }
+ }
+
+ public override void WriteHelperHeaderPrefix(string templateTypeName, bool isStatic)
+ {
+ InnerWriter.Write("Public ");
+ if (isStatic)
+ {
+ InnerWriter.Write("Shared ");
+ }
+ InnerWriter.Write("Function ");
+ }
+
+ public override void WriteHelperHeaderSuffix(string templateTypeName)
+ {
+ InnerWriter.Write(" As ");
+ InnerWriter.WriteLine(templateTypeName);
+ }
+
+ public override void WriteHelperTrailer()
+ {
+ InnerWriter.WriteLine("End Function");
+ }
+
+ public override void WriteEndStatement()
+ {
+ InnerWriter.WriteLine();
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Generator/VBRazorCodeGenerator.cs b/src/System.Web.Razor/Generator/VBRazorCodeGenerator.cs
new file mode 100644
index 00000000..8085dfd1
--- /dev/null
+++ b/src/System.Web.Razor/Generator/VBRazorCodeGenerator.cs
@@ -0,0 +1,15 @@
+namespace System.Web.Razor.Generator
+{
+ public class VBRazorCodeGenerator : RazorCodeGenerator
+ {
+ public VBRazorCodeGenerator(string className, string rootNamespaceName, string sourceFileName, RazorEngineHost host)
+ : base(className, rootNamespaceName, sourceFileName, host)
+ {
+ }
+
+ internal override Func<CodeWriter> CodeWriterFactory
+ {
+ get { return () => new VBCodeWriter(); }
+ }
+ }
+}
diff --git a/src/System.Web.Razor/GeneratorResults.cs b/src/System.Web.Razor/GeneratorResults.cs
new file mode 100644
index 00000000..3d346619
--- /dev/null
+++ b/src/System.Web.Razor/GeneratorResults.cs
@@ -0,0 +1,53 @@
+using System.CodeDom;
+using System.Collections.Generic;
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser.SyntaxTree;
+
+namespace System.Web.Razor
+{
+ /// <summary>
+ /// Represents results from code generation (and parsing, since that is a pre-requisite of code generation)
+ /// </summary>
+ /// <remarks>
+ /// Since this inherits from ParserResults, it has all the data from ParserResults, and simply adds code generation data
+ /// </remarks>
+ public class GeneratorResults : ParserResults
+ {
+ public GeneratorResults(ParserResults parserResults,
+ CodeCompileUnit generatedCode,
+ IDictionary<int, GeneratedCodeMapping> designTimeLineMappings)
+ : this(parserResults.Document, parserResults.ParserErrors, generatedCode, designTimeLineMappings)
+ {
+ }
+
+ public GeneratorResults(Block document,
+ IList<RazorError> parserErrors,
+ CodeCompileUnit generatedCode,
+ IDictionary<int, GeneratedCodeMapping> designTimeLineMappings)
+ : this(parserErrors.Count == 0, document, parserErrors, generatedCode, designTimeLineMappings)
+ {
+ }
+
+ protected GeneratorResults(bool success,
+ Block document,
+ IList<RazorError> parserErrors,
+ CodeCompileUnit generatedCode,
+ IDictionary<int, GeneratedCodeMapping> designTimeLineMappings)
+ : base(success, document, parserErrors)
+ {
+ GeneratedCode = generatedCode;
+ DesignTimeLineMappings = designTimeLineMappings;
+ }
+
+ /// <summary>
+ /// The generated code
+ /// </summary>
+ public CodeCompileUnit GeneratedCode { get; private set; }
+
+ /// <summary>
+ /// If design-time mode was used in the Code Generator, this will contain the dictionary
+ /// of design-time generated code mappings
+ /// </summary>
+ public IDictionary<int, GeneratedCodeMapping> DesignTimeLineMappings { get; private set; }
+ }
+}
diff --git a/src/System.Web.Razor/GlobalSuppressions.cs b/src/System.Web.Razor/GlobalSuppressions.cs
new file mode 100644
index 00000000..1548f4d4
--- /dev/null
+++ b/src/System.Web.Razor/GlobalSuppressions.cs
@@ -0,0 +1,20 @@
+// This file is used by Code Analysis to maintain SuppressMessage
+// attributes that are applied to this project.
+// Project-level suppressions either have no target or are given
+// a specific target and scoped to a namespace, type, member, etc.
+//
+// To add a suppression to this file, right-click the message in the
+// Error List, point to "Suppress Message(s)", and click
+// "In Project Suppression File".
+// You do not need to add suppressions to this file manually.
+
+using System.Diagnostics.CodeAnalysis;
+
+[assembly: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId = "br", Scope = "resource", Target = "System.Web.Razor.Resources.RazorResources.resources", Justification = "Resource is referencing html tag")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "System.Web.Razor.Tokenizer.Symbols", Justification = "These namespaces are design to group classes by function. They will be reviewed to ensure they remain relevant.")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "System.Web.Razor.Tokenizer", Justification = "These namespaces are design to group classes by function. They will be reviewed to ensure they remain relevant.")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "System.Web.Razor.Text", Justification = "These namespaces are design to group classes by function. They will be reviewed to ensure they remain relevant.")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "System.Web.Razor.Parser", Justification = "These namespaces are design to group classes by function. They will be reviewed to ensure they remain relevant.")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "System.Web.Razor.Editor", Justification = "These namespaces are design to group classes by function. They will be reviewed to ensure they remain relevant.")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "System.Web.Razor", Justification = "These namespaces are design to group classes by function. They will be reviewed to ensure they remain relevant.")]
+[assembly: SuppressMessage("Microsoft.Web.FxCop", "MW1000:UnusedResourceUsageRule", Justification = "There are numerous unused resources due to VB being disabled. This rule will be re-run after VB is restored")]
diff --git a/src/System.Web.Razor/Parser/BalancingModes.cs b/src/System.Web.Razor/Parser/BalancingModes.cs
new file mode 100644
index 00000000..c8deec5c
--- /dev/null
+++ b/src/System.Web.Razor/Parser/BalancingModes.cs
@@ -0,0 +1,12 @@
+namespace System.Web.Razor.Parser
+{
+ [Flags]
+ public enum BalancingModes
+ {
+ None = 0,
+ BacktrackOnFailure = 1,
+ NoErrorOnFailure = 2,
+ AllowCommentsAndTemplates = 4,
+ AllowEmbeddedTransitions = 8
+ }
+}
diff --git a/src/System.Web.Razor/Parser/CSharpCodeParser.Directives.cs b/src/System.Web.Razor/Parser/CSharpCodeParser.Directives.cs
new file mode 100644
index 00000000..eaf1d40b
--- /dev/null
+++ b/src/System.Web.Razor/Parser/CSharpCodeParser.Directives.cs
@@ -0,0 +1,502 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using System.Web.Razor.Editor;
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Text;
+using System.Web.Razor.Tokenizer.Symbols;
+
+namespace System.Web.Razor.Parser
+{
+ public partial class CSharpCodeParser
+ {
+ private void SetupDirectives()
+ {
+ MapDirectives(InheritsDirective, SyntaxConstants.CSharp.InheritsKeyword);
+ MapDirectives(FunctionsDirective, SyntaxConstants.CSharp.FunctionsKeyword);
+ MapDirectives(SectionDirective, SyntaxConstants.CSharp.SectionKeyword);
+ MapDirectives(HelperDirective, SyntaxConstants.CSharp.HelperKeyword);
+ MapDirectives(LayoutDirective, SyntaxConstants.CSharp.LayoutKeyword);
+ MapDirectives(SessionStateDirective, SyntaxConstants.CSharp.SessionStateKeyword);
+ }
+
+ protected virtual void LayoutDirective()
+ {
+ AssertDirective(SyntaxConstants.CSharp.LayoutKeyword);
+ AcceptAndMoveNext();
+ Context.CurrentBlock.Type = BlockType.Directive;
+
+ // Accept spaces, but not newlines
+ bool foundSomeWhitespace = At(CSharpSymbolType.WhiteSpace);
+ AcceptWhile(CSharpSymbolType.WhiteSpace);
+ Output(SpanKind.MetaCode, foundSomeWhitespace ? AcceptedCharacters.None : AcceptedCharacters.Any);
+
+ // First non-whitespace character starts the Layout Page, then newline ends it
+ AcceptUntil(CSharpSymbolType.NewLine);
+ Span.CodeGenerator = new SetLayoutCodeGenerator(Span.GetContent());
+ Span.EditHandler.EditorHints = EditorHints.LayoutPage | EditorHints.VirtualPath;
+ bool foundNewline = Optional(CSharpSymbolType.NewLine);
+ AddMarkerSymbolIfNecessary();
+ Output(SpanKind.MetaCode, foundNewline ? AcceptedCharacters.None : AcceptedCharacters.Any);
+ }
+
+ protected virtual void SessionStateDirective()
+ {
+ AssertDirective(SyntaxConstants.CSharp.SessionStateKeyword);
+ AcceptAndMoveNext();
+
+ SessionStateDirectiveCore();
+ }
+
+ protected void SessionStateDirectiveCore()
+ {
+ SessionStateTypeDirective(RazorResources.ParserEror_SessionDirectiveMissingValue, (key, value) => new RazorDirectiveAttributeCodeGenerator(key, value));
+ }
+
+ protected void SessionStateTypeDirective(string noValueError, Func<string, string, SpanCodeGenerator> createCodeGenerator)
+ {
+ // Set the block type
+ Context.CurrentBlock.Type = BlockType.Directive;
+
+ // Accept whitespace
+ CSharpSymbol remainingWs = AcceptSingleWhiteSpaceCharacter();
+
+ if (Span.Symbols.Count > 1)
+ {
+ Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
+ }
+
+ Output(SpanKind.MetaCode);
+
+ if (remainingWs != null)
+ {
+ Accept(remainingWs);
+ }
+ AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true));
+
+ // Parse a Type Name
+ if (!ValidSessionStateValue())
+ {
+ Context.OnError(CurrentLocation, noValueError);
+ }
+
+ // Pull out the type name
+ string sessionStateValue = String.Concat(
+ Span.Symbols
+ .Cast<CSharpSymbol>()
+ .Select(sym => sym.Content)).Trim();
+
+ // Set up code generation
+ Span.CodeGenerator = createCodeGenerator(SyntaxConstants.CSharp.SessionStateKeyword, sessionStateValue);
+
+ // Output the span and finish the block
+ CompleteBlock();
+ Output(SpanKind.Code);
+ }
+
+ protected virtual bool ValidSessionStateValue()
+ {
+ return Optional(CSharpSymbolType.Identifier);
+ }
+
+ [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Coupling will be reviewed at a later date")]
+ [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "C# Keywords are always lower-case")]
+ protected virtual void HelperDirective()
+ {
+ bool nested = Context.IsWithin(BlockType.Helper);
+
+ // Set the block and span type
+ Context.CurrentBlock.Type = BlockType.Helper;
+
+ // Verify we're on "helper" and accept
+ AssertDirective(SyntaxConstants.CSharp.HelperKeyword);
+ Block block = new Block(CurrentSymbol.Content.ToString().ToLowerInvariant(), CurrentLocation);
+ AcceptAndMoveNext();
+
+ if (nested)
+ {
+ Context.OnError(CurrentLocation, RazorResources.ParseError_Helpers_Cannot_Be_Nested);
+ }
+
+ // Accept a single whitespace character if present, if not, we should stop now
+ if (!At(CSharpSymbolType.WhiteSpace))
+ {
+ string error;
+ if (At(CSharpSymbolType.NewLine))
+ {
+ error = RazorResources.ErrorComponent_Newline;
+ }
+ else if (EndOfFile)
+ {
+ error = RazorResources.ErrorComponent_EndOfFile;
+ }
+ else
+ {
+ error = String.Format(CultureInfo.CurrentCulture, RazorResources.ErrorComponent_Character, CurrentSymbol.Content);
+ }
+
+ Context.OnError(
+ CurrentLocation,
+ RazorResources.ParseError_Unexpected_Character_At_Helper_Name_Start,
+ error);
+ PutCurrentBack();
+ Output(SpanKind.MetaCode);
+ return;
+ }
+
+ CSharpSymbol remainingWs = AcceptSingleWhiteSpaceCharacter();
+
+ // Output metacode and continue
+ Output(SpanKind.MetaCode);
+ if (remainingWs != null)
+ {
+ Accept(remainingWs);
+ }
+ AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true)); // Don't accept newlines.
+
+ // Expecting an identifier (helper name)
+ bool errorReported = !Required(CSharpSymbolType.Identifier, errorIfNotFound: true, errorBase: RazorResources.ParseError_Unexpected_Character_At_Helper_Name_Start);
+ if (!errorReported)
+ {
+ Assert(CSharpSymbolType.Identifier);
+ AcceptAndMoveNext();
+ }
+
+ AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true));
+
+ // Expecting parameter list start: "("
+ SourceLocation bracketErrorPos = CurrentLocation;
+ if (!Optional(CSharpSymbolType.LeftParenthesis))
+ {
+ if (!errorReported)
+ {
+ errorReported = true;
+ Context.OnError(
+ CurrentLocation,
+ RazorResources.ParseError_MissingCharAfterHelperName,
+ "(");
+ }
+ }
+ else
+ {
+ SourceLocation bracketStart = CurrentLocation;
+ if (!Balance(BalancingModes.NoErrorOnFailure,
+ CSharpSymbolType.LeftParenthesis,
+ CSharpSymbolType.RightParenthesis,
+ bracketStart))
+ {
+ errorReported = true;
+ Context.OnError(
+ bracketErrorPos,
+ RazorResources.ParseError_UnterminatedHelperParameterList);
+ }
+ Optional(CSharpSymbolType.RightParenthesis);
+ }
+
+ int bookmark = CurrentLocation.AbsoluteIndex;
+ IEnumerable<CSharpSymbol> ws = ReadWhile(IsSpacingToken(includeNewLines: true, includeComments: true));
+
+ // Expecting a "{"
+ SourceLocation errorLocation = CurrentLocation;
+ bool headerComplete = At(CSharpSymbolType.LeftBrace);
+ if (headerComplete)
+ {
+ Accept(ws);
+ AcceptAndMoveNext();
+ }
+ else
+ {
+ Context.Source.Position = bookmark;
+ NextToken();
+ AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true));
+ if (!errorReported)
+ {
+ Context.OnError(
+ errorLocation,
+ RazorResources.ParseError_MissingCharAfterHelperParameters,
+ Language.GetSample(CSharpSymbolType.LeftBrace));
+ }
+ }
+
+ // Grab the signature and build the code generator
+ AddMarkerSymbolIfNecessary();
+ LocationTagged<string> signature = Span.GetContent();
+ HelperCodeGenerator blockGen = new HelperCodeGenerator(signature, headerComplete);
+ Context.CurrentBlock.CodeGenerator = blockGen;
+
+ // The block will generate appropriate code,
+ Span.CodeGenerator = SpanCodeGenerator.Null;
+
+ if (!headerComplete)
+ {
+ CompleteBlock();
+ Output(SpanKind.Code);
+ return;
+ }
+ else
+ {
+ Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
+ Output(SpanKind.Code);
+ }
+
+ // We're valid, so parse the nested block
+ AutoCompleteEditHandler bodyEditHandler = new AutoCompleteEditHandler(Language.TokenizeString);
+ using (PushSpanConfig(DefaultSpanConfig))
+ {
+ using (Context.StartBlock(BlockType.Statement))
+ {
+ Span.EditHandler = bodyEditHandler;
+ CodeBlock(false, block);
+ CompleteBlock(insertMarkerIfNecessary: true);
+ Output(SpanKind.Code);
+ }
+ }
+ Initialize(Span);
+
+ EnsureCurrent();
+
+ Span.CodeGenerator = SpanCodeGenerator.Null; // The block will generate the footer code.
+ if (!Optional(CSharpSymbolType.RightBrace))
+ {
+ // The } is missing, so set the initial signature span to use it as an autocomplete string
+ bodyEditHandler.AutoCompleteString = "}";
+
+ // Need to be able to accept anything to properly handle the autocomplete
+ bodyEditHandler.AcceptedCharacters = AcceptedCharacters.Any;
+ }
+ else
+ {
+ blockGen.Footer = Span.GetContent();
+ Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
+ }
+ CompleteBlock();
+ Output(SpanKind.Code);
+ }
+
+ protected virtual void SectionDirective()
+ {
+ bool nested = Context.IsWithin(BlockType.Section);
+ bool errorReported = false;
+
+ // Set the block and span type
+ Context.CurrentBlock.Type = BlockType.Section;
+
+ // Verify we're on "section" and accept
+ AssertDirective(SyntaxConstants.CSharp.SectionKeyword);
+ AcceptAndMoveNext();
+
+ if (nested)
+ {
+ Context.OnError(CurrentLocation, String.Format(CultureInfo.CurrentCulture, RazorResources.ParseError_Sections_Cannot_Be_Nested, RazorResources.SectionExample_CS));
+ errorReported = true;
+ }
+
+ IEnumerable<CSharpSymbol> ws = ReadWhile(IsSpacingToken(includeNewLines: true, includeComments: false));
+
+ // Get the section name
+ string sectionName = String.Empty;
+ if (!Required(CSharpSymbolType.Identifier,
+ errorIfNotFound: true,
+ errorBase: RazorResources.ParseError_Unexpected_Character_At_Section_Name_Start))
+ {
+ if (!errorReported)
+ {
+ errorReported = true;
+ }
+
+ PutCurrentBack();
+ PutBack(ws);
+ AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: false));
+ }
+ else
+ {
+ Accept(ws);
+ sectionName = CurrentSymbol.Content;
+ AcceptAndMoveNext();
+ }
+ Context.CurrentBlock.CodeGenerator = new SectionCodeGenerator(sectionName);
+
+ SourceLocation errorLocation = CurrentLocation;
+ ws = ReadWhile(IsSpacingToken(includeNewLines: true, includeComments: false));
+
+ // Get the starting brace
+ bool sawStartingBrace = At(CSharpSymbolType.LeftBrace);
+ if (!sawStartingBrace)
+ {
+ if (!errorReported)
+ {
+ errorReported = true;
+ Context.OnError(errorLocation, RazorResources.ParseError_MissingOpenBraceAfterSection);
+ }
+
+ PutCurrentBack();
+ PutBack(ws);
+ AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: false));
+ Optional(CSharpSymbolType.NewLine);
+ Output(SpanKind.MetaCode);
+ CompleteBlock();
+ return;
+ }
+ else
+ {
+ Accept(ws);
+ }
+
+ // Set up edit handler
+ AutoCompleteEditHandler editHandler = new AutoCompleteEditHandler(Language.TokenizeString) { AutoCompleteAtEndOfSpan = true };
+
+ Span.EditHandler = editHandler;
+ Span.Accept(CurrentSymbol);
+
+ // Output Metacode then switch to section parser
+ Output(SpanKind.MetaCode);
+ SectionBlock("{", "}", caseSensitive: true);
+
+ Span.CodeGenerator = SpanCodeGenerator.Null;
+ // Check for the terminating "}"
+ if (!Optional(CSharpSymbolType.RightBrace))
+ {
+ editHandler.AutoCompleteString = "}";
+ Context.OnError(CurrentLocation,
+ RazorResources.ParseError_Expected_X,
+ Language.GetSample(CSharpSymbolType.RightBrace));
+ }
+ else
+ {
+ Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
+ }
+ CompleteBlock(insertMarkerIfNecessary: false, captureWhitespaceToEndOfLine: true);
+ Output(SpanKind.MetaCode);
+ return;
+ }
+
+ protected virtual void FunctionsDirective()
+ {
+ // Set the block type
+ Context.CurrentBlock.Type = BlockType.Functions;
+
+ // Verify we're on "functions" and accept
+ AssertDirective(SyntaxConstants.CSharp.FunctionsKeyword);
+ Block block = new Block(CurrentSymbol);
+ AcceptAndMoveNext();
+
+ AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: false));
+
+ if (!At(CSharpSymbolType.LeftBrace))
+ {
+ Context.OnError(CurrentLocation,
+ RazorResources.ParseError_Expected_X,
+ Language.GetSample(CSharpSymbolType.LeftBrace));
+ CompleteBlock();
+ Output(SpanKind.MetaCode);
+ return;
+ }
+ else
+ {
+ Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
+ }
+
+ // Capture start point and continue
+ SourceLocation blockStart = CurrentLocation;
+ AcceptAndMoveNext();
+
+ // Output what we've seen and continue
+ Output(SpanKind.MetaCode);
+
+ AutoCompleteEditHandler editHandler = new AutoCompleteEditHandler(Language.TokenizeString);
+ Span.EditHandler = editHandler;
+
+ Balance(BalancingModes.NoErrorOnFailure, CSharpSymbolType.LeftBrace, CSharpSymbolType.RightBrace, blockStart);
+ Span.CodeGenerator = new TypeMemberCodeGenerator();
+ if (!At(CSharpSymbolType.RightBrace))
+ {
+ editHandler.AutoCompleteString = "}";
+ Context.OnError(block.Start, RazorResources.ParseError_Expected_EndOfBlock_Before_EOF, block.Name, "}", "{");
+ CompleteBlock();
+ Output(SpanKind.Code);
+ }
+ else
+ {
+ Output(SpanKind.Code);
+ Assert(CSharpSymbolType.RightBrace);
+ Span.CodeGenerator = SpanCodeGenerator.Null;
+ Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
+ AcceptAndMoveNext();
+ CompleteBlock();
+ Output(SpanKind.MetaCode);
+ }
+ }
+
+ protected virtual void InheritsDirective()
+ {
+ // Verify we're on the right keyword and accept
+ AssertDirective(SyntaxConstants.CSharp.InheritsKeyword);
+ AcceptAndMoveNext();
+
+ InheritsDirectiveCore();
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "directive", Justification = "This only occurs in Release builds, where this method is empty by design")]
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This only occurs in Release builds, where this method is empty by design")]
+ [Conditional("DEBUG")]
+ protected void AssertDirective(string directive)
+ {
+ Assert(CSharpSymbolType.Identifier);
+ Debug.Assert(String.Equals(CurrentSymbol.Content, directive, StringComparison.Ordinal));
+ }
+
+ protected void InheritsDirectiveCore()
+ {
+ BaseTypeDirective(RazorResources.ParseError_InheritsKeyword_Must_Be_Followed_By_TypeName, baseType => new SetBaseTypeCodeGenerator(baseType));
+ }
+
+ protected void BaseTypeDirective(string noTypeNameError, Func<string, SpanCodeGenerator> createCodeGenerator)
+ {
+ // Set the block type
+ Context.CurrentBlock.Type = BlockType.Directive;
+
+ // Accept whitespace
+ CSharpSymbol remainingWs = AcceptSingleWhiteSpaceCharacter();
+
+ if (Span.Symbols.Count > 1)
+ {
+ Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
+ }
+
+ Output(SpanKind.MetaCode);
+
+ if (remainingWs != null)
+ {
+ Accept(remainingWs);
+ }
+ AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true));
+
+ if (EndOfFile || At(CSharpSymbolType.WhiteSpace) || At(CSharpSymbolType.NewLine))
+ {
+ Context.OnError(CurrentLocation, noTypeNameError);
+ }
+
+ // Parse to the end of the line
+ AcceptUntil(CSharpSymbolType.NewLine);
+ if (!Context.DesignTimeMode)
+ {
+ // We want the newline to be treated as code, but it causes issues at design-time.
+ Optional(CSharpSymbolType.NewLine);
+ }
+
+ // Pull out the type name
+ string baseType = Span.GetContent();
+
+ // Set up code generation
+ Span.CodeGenerator = createCodeGenerator(baseType.Trim());
+
+ // Output the span and finish the block
+ CompleteBlock();
+ Output(SpanKind.Code);
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Parser/CSharpCodeParser.Statements.cs b/src/System.Web.Razor/Parser/CSharpCodeParser.Statements.cs
new file mode 100644
index 00000000..d2c2ce41
--- /dev/null
+++ b/src/System.Web.Razor/Parser/CSharpCodeParser.Statements.cs
@@ -0,0 +1,672 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Globalization;
+using System.Linq;
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Text;
+using System.Web.Razor.Tokenizer.Symbols;
+
+namespace System.Web.Razor.Parser
+{
+ public partial class CSharpCodeParser
+ {
+ private void SetUpKeywords()
+ {
+ MapKeywords(ConditionalBlock, CSharpKeyword.For, CSharpKeyword.Foreach, CSharpKeyword.While, CSharpKeyword.Switch, CSharpKeyword.Lock);
+ MapKeywords(CaseStatement, false, CSharpKeyword.Case, CSharpKeyword.Default);
+ MapKeywords(IfStatement, CSharpKeyword.If);
+ MapKeywords(TryStatement, CSharpKeyword.Try);
+ MapKeywords(UsingKeyword, CSharpKeyword.Using);
+ MapKeywords(DoStatement, CSharpKeyword.Do);
+ MapKeywords(ReservedDirective, CSharpKeyword.Namespace, CSharpKeyword.Class);
+ }
+
+ protected virtual void ReservedDirective(bool topLevel)
+ {
+ Context.OnError(CurrentLocation, String.Format(CultureInfo.CurrentCulture, RazorResources.ParseError_ReservedWord, CurrentSymbol.Content));
+ AcceptAndMoveNext();
+ Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
+ Span.CodeGenerator = SpanCodeGenerator.Null;
+ Context.CurrentBlock.Type = BlockType.Directive;
+ CompleteBlock();
+ Output(SpanKind.MetaCode);
+ }
+
+ private void KeywordBlock(bool topLevel)
+ {
+ HandleKeyword(topLevel, () =>
+ {
+ Context.CurrentBlock.Type = BlockType.Expression;
+ Context.CurrentBlock.CodeGenerator = new ExpressionCodeGenerator();
+ ImplicitExpression();
+ });
+ }
+
+ private void CaseStatement(bool topLevel)
+ {
+ Assert(CSharpSymbolType.Keyword);
+ Debug.Assert(CurrentSymbol.Keyword != null &&
+ (CurrentSymbol.Keyword.Value == CSharpKeyword.Case ||
+ CurrentSymbol.Keyword.Value == CSharpKeyword.Default));
+ AcceptUntil(CSharpSymbolType.Colon);
+ Optional(CSharpSymbolType.Colon);
+ }
+
+ private void DoStatement(bool topLevel)
+ {
+ Assert(CSharpKeyword.Do);
+ UnconditionalBlock();
+ WhileClause();
+ if (topLevel)
+ {
+ CompleteBlock();
+ }
+ }
+
+ private void WhileClause()
+ {
+ Span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any;
+ IEnumerable<CSharpSymbol> ws = SkipToNextImportantToken();
+
+ if (At(CSharpKeyword.While))
+ {
+ Accept(ws);
+ Assert(CSharpKeyword.While);
+ AcceptAndMoveNext();
+ AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true));
+ if (AcceptCondition() && Optional(CSharpSymbolType.Semicolon))
+ {
+ Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
+ }
+ }
+ else
+ {
+ PutCurrentBack();
+ PutBack(ws);
+ }
+ }
+
+ private void UsingKeyword(bool topLevel)
+ {
+ Assert(CSharpKeyword.Using);
+ Block block = new Block(CurrentSymbol);
+ AcceptAndMoveNext();
+ AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true));
+
+ if (At(CSharpSymbolType.LeftParenthesis))
+ {
+ // using ( ==> Using Statement
+ UsingStatement(block);
+ }
+ else if (At(CSharpSymbolType.Identifier))
+ {
+ // using Identifier ==> Using Declaration
+ if (!topLevel)
+ {
+ Context.OnError(block.Start, RazorResources.ParseError_NamespaceImportAndTypeAlias_Cannot_Exist_Within_CodeBlock);
+ StandardStatement();
+ }
+ else
+ {
+ UsingDeclaration();
+ }
+ }
+
+ if (topLevel)
+ {
+ CompleteBlock();
+ }
+ }
+
+ private void UsingDeclaration()
+ {
+ // Set block type to directive
+ Context.CurrentBlock.Type = BlockType.Directive;
+
+ // Parse a type name
+ Assert(CSharpSymbolType.Identifier);
+ NamespaceOrTypeName();
+ IEnumerable<CSharpSymbol> ws = ReadWhile(IsSpacingToken(includeNewLines: true, includeComments: true));
+ if (At(CSharpSymbolType.Assign))
+ {
+ // Alias
+ Accept(ws);
+ Assert(CSharpSymbolType.Assign);
+ AcceptAndMoveNext();
+
+ AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true));
+
+ // One more namespace or type name
+ NamespaceOrTypeName();
+ }
+ else
+ {
+ PutCurrentBack();
+ PutBack(ws);
+ }
+
+ Span.EditHandler.AcceptedCharacters = AcceptedCharacters.AnyExceptNewline;
+ Span.CodeGenerator = new AddImportCodeGenerator(
+ Span.GetContent(syms => syms.Skip(1)), // Skip "using"
+ SyntaxConstants.CSharp.UsingKeywordLength);
+
+ // Optional ";"
+ if (EnsureCurrent())
+ {
+ Optional(CSharpSymbolType.Semicolon);
+ }
+ }
+
+ private bool NamespaceOrTypeName()
+ {
+ if (Optional(CSharpSymbolType.Identifier) || Optional(CSharpSymbolType.Keyword))
+ {
+ Optional(CSharpSymbolType.QuestionMark); // Nullable
+ if (Optional(CSharpSymbolType.DoubleColon))
+ {
+ if (!Optional(CSharpSymbolType.Identifier))
+ {
+ Optional(CSharpSymbolType.Keyword);
+ }
+ }
+ if (At(CSharpSymbolType.LessThan))
+ {
+ TypeArgumentList();
+ }
+ if (Optional(CSharpSymbolType.Dot))
+ {
+ NamespaceOrTypeName();
+ }
+ while (At(CSharpSymbolType.LeftBracket))
+ {
+ Balance(BalancingModes.None);
+ Optional(CSharpSymbolType.RightBracket);
+ }
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ private void TypeArgumentList()
+ {
+ Assert(CSharpSymbolType.LessThan);
+ Balance(BalancingModes.None);
+ Optional(CSharpSymbolType.GreaterThan);
+ }
+
+ private void UsingStatement(Block block)
+ {
+ Assert(CSharpSymbolType.LeftParenthesis);
+
+ // Parse condition
+ if (AcceptCondition())
+ {
+ AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true));
+
+ // Parse code block
+ ExpectCodeBlock(block);
+ }
+ }
+
+ private void TryStatement(bool topLevel)
+ {
+ Assert(CSharpKeyword.Try);
+ UnconditionalBlock();
+ AfterTryClause();
+ if (topLevel)
+ {
+ CompleteBlock();
+ }
+ }
+
+ private void IfStatement(bool topLevel)
+ {
+ Assert(CSharpKeyword.If);
+ ConditionalBlock(topLevel: false);
+ AfterIfClause();
+ if (topLevel)
+ {
+ CompleteBlock();
+ }
+ }
+
+ private void AfterTryClause()
+ {
+ // Grab whitespace
+ IEnumerable<CSharpSymbol> ws = SkipToNextImportantToken();
+
+ // Check for a catch or finally part
+ if (At(CSharpKeyword.Catch))
+ {
+ Accept(ws);
+ Assert(CSharpKeyword.Catch);
+ ConditionalBlock(topLevel: false);
+ AfterTryClause();
+ }
+ else if (At(CSharpKeyword.Finally))
+ {
+ Accept(ws);
+ Assert(CSharpKeyword.Finally);
+ UnconditionalBlock();
+ }
+ else
+ {
+ // Return whitespace and end the block
+ PutCurrentBack();
+ PutBack(ws);
+ Span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any;
+ }
+ }
+
+ private void AfterIfClause()
+ {
+ // Grab whitespace and razor comments
+ IEnumerable<CSharpSymbol> ws = SkipToNextImportantToken();
+
+ // Check for an else part
+ if (At(CSharpKeyword.Else))
+ {
+ Accept(ws);
+ Assert(CSharpKeyword.Else);
+ ElseClause();
+ }
+ else
+ {
+ // No else, return whitespace
+ PutCurrentBack();
+ PutBack(ws);
+ Span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any;
+ }
+ }
+
+ private void ElseClause()
+ {
+ if (!At(CSharpKeyword.Else))
+ {
+ return;
+ }
+ Block block = new Block(CurrentSymbol);
+
+ AcceptAndMoveNext();
+ AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true));
+ if (At(CSharpKeyword.If))
+ {
+ // ElseIf
+ block.Name = SyntaxConstants.CSharp.ElseIfKeyword;
+ ConditionalBlock(block);
+ AfterIfClause();
+ }
+ else if (!EndOfFile)
+ {
+ // Else
+ ExpectCodeBlock(block);
+ }
+ }
+
+ private void ExpectCodeBlock(Block block)
+ {
+ if (!EndOfFile)
+ {
+ // Check for "{" to make sure we're at a block
+ if (!At(CSharpSymbolType.LeftBrace))
+ {
+ Context.OnError(CurrentLocation,
+ RazorResources.ParseError_SingleLine_ControlFlowStatements_Not_Allowed,
+ Language.GetSample(CSharpSymbolType.LeftBrace),
+ CurrentSymbol.Content);
+ }
+
+ // Parse the statement and then we're done
+ Statement(block);
+ }
+ }
+
+ private void UnconditionalBlock()
+ {
+ Assert(CSharpSymbolType.Keyword);
+ Block block = new Block(CurrentSymbol);
+ AcceptAndMoveNext();
+ AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true));
+ ExpectCodeBlock(block);
+ }
+
+ private void ConditionalBlock(bool topLevel)
+ {
+ Assert(CSharpSymbolType.Keyword);
+ Block block = new Block(CurrentSymbol);
+ ConditionalBlock(block);
+ if (topLevel)
+ {
+ CompleteBlock();
+ }
+ }
+
+ private void ConditionalBlock(Block block)
+ {
+ AcceptAndMoveNext();
+ AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true));
+
+ // Parse the condition, if present (if not present, we'll let the C# compiler complain)
+ if (AcceptCondition())
+ {
+ AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true));
+ ExpectCodeBlock(block);
+ }
+ }
+
+ private bool AcceptCondition()
+ {
+ if (At(CSharpSymbolType.LeftParenthesis))
+ {
+ bool complete = Balance(BalancingModes.BacktrackOnFailure | BalancingModes.AllowCommentsAndTemplates);
+ if (!complete)
+ {
+ AcceptUntil(CSharpSymbolType.NewLine);
+ }
+ else
+ {
+ Optional(CSharpSymbolType.RightParenthesis);
+ }
+ return complete;
+ }
+ return true;
+ }
+
+ private void Statement()
+ {
+ Statement(null);
+ }
+
+ private void Statement(Block block)
+ {
+ Span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any;
+
+ // Accept whitespace but always keep the last whitespace node so we can put it back if necessary
+ CSharpSymbol lastWs = AcceptWhiteSpaceInLines();
+ Debug.Assert(lastWs == null || (lastWs.Start.AbsoluteIndex + lastWs.Content.Length == CurrentLocation.AbsoluteIndex));
+
+ if (EndOfFile)
+ {
+ if (lastWs != null)
+ {
+ Accept(lastWs);
+ }
+ return;
+ }
+
+ CSharpSymbolType type = CurrentSymbol.Type;
+ SourceLocation loc = CurrentLocation;
+
+ bool isSingleLineMarkup = type == CSharpSymbolType.Transition && NextIs(CSharpSymbolType.Colon);
+ bool isMarkup = isSingleLineMarkup ||
+ type == CSharpSymbolType.LessThan ||
+ (type == CSharpSymbolType.Transition && NextIs(CSharpSymbolType.LessThan));
+
+ if (Context.DesignTimeMode || !isMarkup)
+ {
+ // CODE owns whitespace, MARKUP owns it ONLY in DesignTimeMode.
+ if (lastWs != null)
+ {
+ Accept(lastWs);
+ }
+ }
+ else
+ {
+ // MARKUP owns whitespace EXCEPT in DesignTimeMode.
+ PutCurrentBack();
+ PutBack(lastWs);
+ }
+
+ if (isMarkup)
+ {
+ if (type == CSharpSymbolType.Transition && !isSingleLineMarkup)
+ {
+ Context.OnError(loc, RazorResources.ParseError_AtInCode_Must_Be_Followed_By_Colon_Paren_Or_Identifier_Start);
+ }
+
+ // Markup block
+ Output(SpanKind.Code);
+ if (Context.DesignTimeMode && CurrentSymbol != null && (CurrentSymbol.Type == CSharpSymbolType.LessThan || CurrentSymbol.Type == CSharpSymbolType.Transition))
+ {
+ PutCurrentBack();
+ }
+ OtherParserBlock();
+ }
+ else
+ {
+ // What kind of statement is this?
+ HandleStatement(block, type);
+ }
+ }
+
+ private void HandleStatement(Block block, CSharpSymbolType type)
+ {
+ switch (type)
+ {
+ case CSharpSymbolType.RazorCommentTransition:
+ Output(SpanKind.Code);
+ RazorComment();
+ Statement(block);
+ break;
+ case CSharpSymbolType.LeftBrace:
+ // Verbatim Block
+ block = block ?? new Block(RazorResources.BlockName_Code, CurrentLocation);
+ AcceptAndMoveNext();
+ CodeBlock(block);
+ break;
+ case CSharpSymbolType.Keyword:
+ // Keyword block
+ HandleKeyword(false, StandardStatement);
+ break;
+ case CSharpSymbolType.Transition:
+ // Embedded Expression block
+ EmbeddedExpression();
+ break;
+ case CSharpSymbolType.RightBrace:
+ // Possible end of Code Block, just run the continuation
+ break;
+ case CSharpSymbolType.Comment:
+ AcceptAndMoveNext();
+ break;
+ default:
+ // Other statement
+ StandardStatement();
+ break;
+ }
+ }
+
+ private void EmbeddedExpression()
+ {
+ // First, verify the type of the block
+ Assert(CSharpSymbolType.Transition);
+ CSharpSymbol transition = CurrentSymbol;
+ NextToken();
+
+ if (At(CSharpSymbolType.Transition))
+ {
+ // Escaped "@"
+ Output(SpanKind.Code);
+
+ // Output "@" as hidden span
+ Accept(transition);
+ Span.CodeGenerator = SpanCodeGenerator.Null;
+ Output(SpanKind.Code);
+
+ Assert(CSharpSymbolType.Transition);
+ AcceptAndMoveNext();
+ StandardStatement();
+ }
+ else
+ {
+ // Throw errors as necessary, but continue parsing
+ if (At(CSharpSymbolType.Keyword))
+ {
+ Context.OnError(CurrentLocation,
+ RazorResources.ParseError_Unexpected_Keyword_After_At,
+ CSharpLanguageCharacteristics.GetKeyword(CurrentSymbol.Keyword.Value));
+ }
+ else if (At(CSharpSymbolType.LeftBrace))
+ {
+ Context.OnError(CurrentLocation, RazorResources.ParseError_Unexpected_Nested_CodeBlock);
+ }
+
+ // @( or @foo - Nested expression, parse a child block
+ PutCurrentBack();
+ PutBack(transition);
+
+ // Before exiting, add a marker span if necessary
+ AddMarkerSymbolIfNecessary();
+
+ NestedBlock();
+ }
+ }
+
+ private void StandardStatement()
+ {
+ while (!EndOfFile)
+ {
+ int bookmark = CurrentLocation.AbsoluteIndex;
+ IEnumerable<CSharpSymbol> read = ReadWhile(sym => sym.Type != CSharpSymbolType.Semicolon &&
+ sym.Type != CSharpSymbolType.RazorCommentTransition &&
+ sym.Type != CSharpSymbolType.Transition &&
+ sym.Type != CSharpSymbolType.LeftBrace &&
+ sym.Type != CSharpSymbolType.LeftParenthesis &&
+ sym.Type != CSharpSymbolType.LeftBracket &&
+ sym.Type != CSharpSymbolType.RightBrace);
+ if (At(CSharpSymbolType.LeftBrace) || At(CSharpSymbolType.LeftParenthesis) || At(CSharpSymbolType.LeftBracket))
+ {
+ Accept(read);
+ Balance(BalancingModes.AllowCommentsAndTemplates);
+ Optional(CSharpSymbolType.RightBrace);
+ }
+ else if (At(CSharpSymbolType.Transition) && (NextIs(CSharpSymbolType.LessThan, CSharpSymbolType.Colon)))
+ {
+ Accept(read);
+ Output(SpanKind.Code);
+ Template();
+ }
+ else if (At(CSharpSymbolType.RazorCommentTransition))
+ {
+ Accept(read);
+ RazorComment();
+ }
+ else if (At(CSharpSymbolType.Semicolon))
+ {
+ Accept(read);
+ AcceptAndMoveNext();
+ return;
+ }
+ else if (At(CSharpSymbolType.RightBrace))
+ {
+ Accept(read);
+ return;
+ }
+ else
+ {
+ Context.Source.Position = bookmark;
+ NextToken();
+ AcceptUntil(CSharpSymbolType.LessThan, CSharpSymbolType.RightBrace);
+ return;
+ }
+ }
+ }
+
+ private void CodeBlock(Block block)
+ {
+ CodeBlock(true, block);
+ }
+
+ private void CodeBlock(bool acceptTerminatingBrace, Block block)
+ {
+ EnsureCurrent();
+ while (!EndOfFile && !At(CSharpSymbolType.RightBrace))
+ {
+ // Parse a statement, then return here
+ Statement();
+ EnsureCurrent();
+ }
+
+ if (EndOfFile)
+ {
+ Context.OnError(block.Start, RazorResources.ParseError_Expected_EndOfBlock_Before_EOF, block.Name, '}', '{');
+ }
+ else if (acceptTerminatingBrace)
+ {
+ Assert(CSharpSymbolType.RightBrace);
+ Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
+ AcceptAndMoveNext();
+ }
+ }
+
+ private void HandleKeyword(bool topLevel, Action fallback)
+ {
+ Debug.Assert(CurrentSymbol.Type == CSharpSymbolType.Keyword && CurrentSymbol.Keyword != null);
+ Action<bool> handler;
+ if (_keywordParsers.TryGetValue(CurrentSymbol.Keyword.Value, out handler))
+ {
+ handler(topLevel);
+ }
+ else
+ {
+ fallback();
+ }
+ }
+
+ private IEnumerable<CSharpSymbol> SkipToNextImportantToken()
+ {
+ while (!EndOfFile)
+ {
+ IEnumerable<CSharpSymbol> ws = ReadWhile(IsSpacingToken(includeNewLines: true, includeComments: true));
+ if (At(CSharpSymbolType.RazorCommentTransition))
+ {
+ Accept(ws);
+ Span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any;
+ RazorComment();
+ }
+ else
+ {
+ return ws;
+ }
+ }
+ return Enumerable.Empty<CSharpSymbol>();
+ }
+
+ // Common code for Parsers, but FxCop REALLY doesn't like it in the base class.. moving it here for now.
+ protected override void OutputSpanBeforeRazorComment()
+ {
+ AddMarkerSymbolIfNecessary();
+ Output(SpanKind.Code);
+ }
+
+ protected class Block
+ {
+ public Block(string name, SourceLocation start)
+ {
+ Name = name;
+ Start = start;
+ }
+
+ public Block(CSharpSymbol symbol)
+ : this(GetName(symbol), symbol.Start)
+ {
+ }
+
+ public string Name { get; set; }
+ public SourceLocation Start { get; set; }
+
+ private static string GetName(CSharpSymbol sym)
+ {
+ if (sym.Type == CSharpSymbolType.Keyword)
+ {
+ return CSharpLanguageCharacteristics.GetKeyword(sym.Keyword.Value);
+ }
+ return sym.Content;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Parser/CSharpCodeParser.cs b/src/System.Web.Razor/Parser/CSharpCodeParser.cs
new file mode 100644
index 00000000..c022d7af
--- /dev/null
+++ b/src/System.Web.Razor/Parser/CSharpCodeParser.cs
@@ -0,0 +1,568 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Web.Razor.Editor;
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Tokenizer;
+using System.Web.Razor.Tokenizer.Symbols;
+
+namespace System.Web.Razor.Parser
+{
+ public partial class CSharpCodeParser : TokenizerBackedParser<CSharpTokenizer, CSharpSymbol, CSharpSymbolType>
+ {
+ internal static readonly int UsingKeywordLength = 5; // using
+
+ internal static ISet<string> DefaultKeywords = new HashSet<string>()
+ {
+ "if",
+ "do",
+ "try",
+ "for",
+ "foreach",
+ "while",
+ "switch",
+ "lock",
+ "using",
+ "section",
+ "inherits",
+ "helper",
+ "functions",
+ "namespace",
+ "class",
+ "layout",
+ "sessionstate"
+ };
+
+ private Dictionary<string, Action> _directiveParsers = new Dictionary<string, Action>();
+ private Dictionary<CSharpKeyword, Action<bool>> _keywordParsers = new Dictionary<CSharpKeyword, Action<bool>>();
+
+ public CSharpCodeParser()
+ {
+ Keywords = new HashSet<string>();
+ SetUpKeywords();
+ SetupDirectives();
+ }
+
+ protected internal ISet<string> Keywords { get; private set; }
+
+ public bool IsNested { get; set; }
+
+ protected override ParserBase OtherParser
+ {
+ get { return Context.MarkupParser; }
+ }
+
+ protected override LanguageCharacteristics<CSharpTokenizer, CSharpSymbol, CSharpSymbolType> Language
+ {
+ get { return CSharpLanguageCharacteristics.Instance; }
+ }
+
+ protected void MapDirectives(Action handler, params string[] directives)
+ {
+ foreach (string directive in directives)
+ {
+ _directiveParsers.Add(directive, handler);
+ Keywords.Add(directive);
+ }
+ }
+
+ protected bool TryGetDirectiveHandler(string directive, out Action handler)
+ {
+ return _directiveParsers.TryGetValue(directive, out handler);
+ }
+
+ private void MapKeywords(Action<bool> handler, params CSharpKeyword[] keywords)
+ {
+ MapKeywords(handler, topLevel: true, keywords: keywords);
+ }
+
+ private void MapKeywords(Action<bool> handler, bool topLevel, params CSharpKeyword[] keywords)
+ {
+ foreach (CSharpKeyword keyword in keywords)
+ {
+ _keywordParsers.Add(keyword, handler);
+ if (topLevel)
+ {
+ Keywords.Add(CSharpLanguageCharacteristics.GetKeyword(keyword));
+ }
+ }
+ }
+
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This only occurs in Release builds, where this method is empty by design")]
+ [Conditional("DEBUG")]
+ internal void Assert(CSharpKeyword expectedKeyword)
+ {
+ Debug.Assert(CurrentSymbol.Type == CSharpSymbolType.Keyword && CurrentSymbol.Keyword.HasValue && CurrentSymbol.Keyword.Value == expectedKeyword);
+ }
+
+ protected internal bool At(CSharpKeyword keyword)
+ {
+ return At(CSharpSymbolType.Keyword) && CurrentSymbol.Keyword.HasValue && CurrentSymbol.Keyword.Value == keyword;
+ }
+
+ protected internal bool AcceptIf(CSharpKeyword keyword)
+ {
+ if (At(keyword))
+ {
+ AcceptAndMoveNext();
+ return true;
+ }
+ return false;
+ }
+
+ protected static Func<CSharpSymbol, bool> IsSpacingToken(bool includeNewLines, bool includeComments)
+ {
+ return sym => sym.Type == CSharpSymbolType.WhiteSpace ||
+ (includeNewLines && sym.Type == CSharpSymbolType.NewLine) ||
+ (includeComments && sym.Type == CSharpSymbolType.Comment);
+ }
+
+ public override void ParseBlock()
+ {
+ using (PushSpanConfig(DefaultSpanConfig))
+ {
+ if (Context == null)
+ {
+ throw new InvalidOperationException(RazorResources.Parser_Context_Not_Set);
+ }
+
+ // Unless changed, the block is a statement block
+ using (Context.StartBlock(BlockType.Statement))
+ {
+ NextToken();
+
+ AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true));
+
+ CSharpSymbol current = CurrentSymbol;
+ if (At(CSharpSymbolType.StringLiteral) && CurrentSymbol.Content.Length > 0 && CurrentSymbol.Content[0] == SyntaxConstants.TransitionCharacter)
+ {
+ Tuple<CSharpSymbol, CSharpSymbol> split = Language.SplitSymbol(CurrentSymbol, 1, CSharpSymbolType.Transition);
+ current = split.Item1;
+ Context.Source.Position = split.Item2.Start.AbsoluteIndex;
+ NextToken();
+ }
+ else if (At(CSharpSymbolType.Transition))
+ {
+ NextToken();
+ }
+
+ // Accept "@" if we see it, but if we don't, that's OK. We assume we were started for a good reason
+ if (current.Type == CSharpSymbolType.Transition)
+ {
+ if (Span.Symbols.Count > 0)
+ {
+ Output(SpanKind.Code);
+ }
+ AtTransition(current);
+ }
+ else
+ {
+ // No "@" => Jump straight to AfterTransition
+ AfterTransition();
+ }
+ Output(SpanKind.Code);
+ }
+ }
+ }
+
+ private void DefaultSpanConfig(SpanBuilder span)
+ {
+ span.EditHandler = SpanEditHandler.CreateDefault(Language.TokenizeString);
+ span.CodeGenerator = new StatementCodeGenerator();
+ }
+
+ private void AtTransition(CSharpSymbol current)
+ {
+ Debug.Assert(current.Type == CSharpSymbolType.Transition);
+ Accept(current);
+ Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
+ Span.CodeGenerator = SpanCodeGenerator.Null;
+
+ // Output the "@" span and continue here
+ Output(SpanKind.Transition);
+ AfterTransition();
+ }
+
+ private void AfterTransition()
+ {
+ using (PushSpanConfig(DefaultSpanConfig))
+ {
+ EnsureCurrent();
+ // What type of block is this?
+ if (!EndOfFile)
+ {
+ if (CurrentSymbol.Type == CSharpSymbolType.LeftParenthesis)
+ {
+ Context.CurrentBlock.Type = BlockType.Expression;
+ Context.CurrentBlock.CodeGenerator = new ExpressionCodeGenerator();
+ ExplicitExpression();
+ return;
+ }
+ else if (CurrentSymbol.Type == CSharpSymbolType.Identifier)
+ {
+ Action handler;
+ if (TryGetDirectiveHandler(CurrentSymbol.Content, out handler))
+ {
+ Span.CodeGenerator = SpanCodeGenerator.Null;
+ handler();
+ return;
+ }
+ else
+ {
+ Context.CurrentBlock.Type = BlockType.Expression;
+ Context.CurrentBlock.CodeGenerator = new ExpressionCodeGenerator();
+ ImplicitExpression();
+ return;
+ }
+ }
+ else if (CurrentSymbol.Type == CSharpSymbolType.Keyword)
+ {
+ KeywordBlock(topLevel: true);
+ return;
+ }
+ else if (CurrentSymbol.Type == CSharpSymbolType.LeftBrace)
+ {
+ VerbatimBlock();
+ return;
+ }
+ }
+
+ // Invalid character
+ Context.CurrentBlock.Type = BlockType.Expression;
+ Context.CurrentBlock.CodeGenerator = new ExpressionCodeGenerator();
+ AddMarkerSymbolIfNecessary();
+ Span.CodeGenerator = new ExpressionCodeGenerator();
+ Span.EditHandler = new ImplicitExpressionEditHandler(
+ Language.TokenizeString,
+ DefaultKeywords,
+ acceptTrailingDot: IsNested)
+ {
+ AcceptedCharacters = AcceptedCharacters.NonWhiteSpace
+ };
+ if (At(CSharpSymbolType.WhiteSpace) || At(CSharpSymbolType.NewLine))
+ {
+ Context.OnError(CurrentLocation, RazorResources.ParseError_Unexpected_WhiteSpace_At_Start_Of_CodeBlock_CS);
+ }
+ else if (EndOfFile)
+ {
+ Context.OnError(CurrentLocation, RazorResources.ParseError_Unexpected_EndOfFile_At_Start_Of_CodeBlock);
+ }
+ else
+ {
+ Context.OnError(CurrentLocation, RazorResources.ParseError_Unexpected_Character_At_Start_Of_CodeBlock_CS, CurrentSymbol.Content);
+ }
+ PutCurrentBack();
+ }
+ }
+
+ private void VerbatimBlock()
+ {
+ Assert(CSharpSymbolType.LeftBrace);
+ Block block = new Block(RazorResources.BlockName_Code, CurrentLocation);
+ AcceptAndMoveNext();
+
+ // Set up the "{" span and output
+ Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
+ Span.CodeGenerator = SpanCodeGenerator.Null;
+ Output(SpanKind.MetaCode);
+
+ // Set up auto-complete and parse the code block
+ AutoCompleteEditHandler editHandler = new AutoCompleteEditHandler(Language.TokenizeString);
+ Span.EditHandler = editHandler;
+ CodeBlock(false, block);
+
+ Span.CodeGenerator = new StatementCodeGenerator();
+ AddMarkerSymbolIfNecessary();
+ if (!At(CSharpSymbolType.RightBrace))
+ {
+ editHandler.AutoCompleteString = "}";
+ }
+ Output(SpanKind.Code);
+
+ if (Optional(CSharpSymbolType.RightBrace))
+ {
+ // Set up the "}" span
+ Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
+ Span.CodeGenerator = SpanCodeGenerator.Null;
+ }
+
+ if (!At(CSharpSymbolType.WhiteSpace) && !At(CSharpSymbolType.NewLine))
+ {
+ PutCurrentBack();
+ }
+
+ CompleteBlock(insertMarkerIfNecessary: false);
+ Output(SpanKind.MetaCode);
+ }
+
+ private void ImplicitExpression()
+ {
+ Context.CurrentBlock.Type = BlockType.Expression;
+ Context.CurrentBlock.CodeGenerator = new ExpressionCodeGenerator();
+
+ using (PushSpanConfig(span =>
+ {
+ span.EditHandler = new ImplicitExpressionEditHandler(Language.TokenizeString, Keywords, acceptTrailingDot: IsNested);
+ span.EditHandler.AcceptedCharacters = AcceptedCharacters.NonWhiteSpace;
+ span.CodeGenerator = new ExpressionCodeGenerator();
+ }))
+ {
+ do
+ {
+ if (AtIdentifier(allowKeywords: true))
+ {
+ AcceptAndMoveNext();
+ }
+ }
+ while (MethodCallOrArrayIndex());
+
+ PutCurrentBack();
+ Output(SpanKind.Code);
+ }
+ }
+
+ private bool MethodCallOrArrayIndex()
+ {
+ if (!EndOfFile)
+ {
+ if (CurrentSymbol.Type == CSharpSymbolType.LeftParenthesis || CurrentSymbol.Type == CSharpSymbolType.LeftBracket)
+ {
+ // If we end within "(", whitespace is fine
+ Span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any;
+
+ CSharpSymbolType right;
+ bool success;
+
+ using (PushSpanConfig((span, prev) =>
+ {
+ prev(span);
+ span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any;
+ }))
+ {
+ right = Language.FlipBracket(CurrentSymbol.Type);
+ success = Balance(BalancingModes.BacktrackOnFailure | BalancingModes.AllowCommentsAndTemplates);
+ }
+
+ if (!success)
+ {
+ AcceptUntil(CSharpSymbolType.LessThan);
+ }
+ if (At(right))
+ {
+ AcceptAndMoveNext();
+ Span.EditHandler.AcceptedCharacters = AcceptedCharacters.NonWhiteSpace;
+ }
+ return MethodCallOrArrayIndex();
+ }
+ if (CurrentSymbol.Type == CSharpSymbolType.Dot)
+ {
+ CSharpSymbol dot = CurrentSymbol;
+ if (NextToken())
+ {
+ if (At(CSharpSymbolType.Identifier) || At(CSharpSymbolType.Keyword))
+ {
+ // Accept the dot and return to the start
+ Accept(dot);
+ return true; // continue
+ }
+ else
+ {
+ // Put the symbol back
+ PutCurrentBack();
+ }
+ }
+ if (!IsNested)
+ {
+ // Put the "." back
+ PutBack(dot);
+ }
+ else
+ {
+ Accept(dot);
+ }
+ }
+ else if (!At(CSharpSymbolType.WhiteSpace) && !At(CSharpSymbolType.NewLine))
+ {
+ PutCurrentBack();
+ }
+ }
+
+ // Implicit Expression is complete
+ return false;
+ }
+
+ private void CompleteBlock()
+ {
+ CompleteBlock(insertMarkerIfNecessary: true);
+ }
+
+ private void CompleteBlock(bool insertMarkerIfNecessary)
+ {
+ CompleteBlock(insertMarkerIfNecessary, captureWhitespaceToEndOfLine: insertMarkerIfNecessary);
+ }
+
+ private void CompleteBlock(bool insertMarkerIfNecessary, bool captureWhitespaceToEndOfLine)
+ {
+ if (insertMarkerIfNecessary && Context.LastAcceptedCharacters != AcceptedCharacters.Any)
+ {
+ AddMarkerSymbolIfNecessary();
+ }
+
+ EnsureCurrent();
+
+ // Read whitespace, but not newlines
+ // If we're not inserting a marker span, we don't need to capture whitespace
+ if (!Context.WhiteSpaceIsSignificantToAncestorBlock &&
+ Context.CurrentBlock.Type != BlockType.Expression &&
+ captureWhitespaceToEndOfLine &&
+ !Context.DesignTimeMode &&
+ !IsNested)
+ {
+ CaptureWhitespaceAtEndOfCodeOnlyLine();
+ }
+ else
+ {
+ PutCurrentBack();
+ }
+ }
+
+ private void CaptureWhitespaceAtEndOfCodeOnlyLine()
+ {
+ IEnumerable<CSharpSymbol> ws = ReadWhile(sym => sym.Type == CSharpSymbolType.WhiteSpace);
+ if (At(CSharpSymbolType.NewLine))
+ {
+ Accept(ws);
+ AcceptAndMoveNext();
+ PutCurrentBack();
+ }
+ else
+ {
+ PutCurrentBack();
+ PutBack(ws);
+ }
+ }
+
+ private void ConfigureExplicitExpressionSpan(SpanBuilder sb)
+ {
+ sb.EditHandler = SpanEditHandler.CreateDefault(Language.TokenizeString);
+ sb.CodeGenerator = new ExpressionCodeGenerator();
+ }
+
+ private void ExplicitExpression()
+ {
+ Block block = new Block(RazorResources.BlockName_ExplicitExpression, CurrentLocation);
+ Assert(CSharpSymbolType.LeftParenthesis);
+ AcceptAndMoveNext();
+ Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
+ Span.CodeGenerator = SpanCodeGenerator.Null;
+ Output(SpanKind.MetaCode);
+ using (PushSpanConfig(ConfigureExplicitExpressionSpan))
+ {
+ bool success = Balance(
+ BalancingModes.BacktrackOnFailure | BalancingModes.NoErrorOnFailure | BalancingModes.AllowCommentsAndTemplates,
+ CSharpSymbolType.LeftParenthesis,
+ CSharpSymbolType.RightParenthesis,
+ block.Start);
+
+ if (!success)
+ {
+ AcceptUntil(CSharpSymbolType.LessThan);
+ Context.OnError(block.Start, RazorResources.ParseError_Expected_EndOfBlock_Before_EOF, block.Name, ")", "(");
+ }
+
+ // If necessary, put an empty-content marker symbol here
+ if (Span.Symbols.Count == 0)
+ {
+ Accept(new CSharpSymbol(CurrentLocation, String.Empty, CSharpSymbolType.Unknown));
+ }
+
+ // Output the content span and then capture the ")"
+ Output(SpanKind.Code);
+ }
+ Optional(CSharpSymbolType.RightParenthesis);
+ if (!EndOfFile)
+ {
+ PutCurrentBack();
+ }
+ Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
+ Span.CodeGenerator = SpanCodeGenerator.Null;
+ CompleteBlock(insertMarkerIfNecessary: false);
+ Output(SpanKind.MetaCode);
+ }
+
+ private void Template()
+ {
+ if (Context.IsWithin(BlockType.Template))
+ {
+ Context.OnError(CurrentLocation, RazorResources.ParseError_InlineMarkup_Blocks_Cannot_Be_Nested);
+ }
+ Output(SpanKind.Code);
+ using (Context.StartBlock(BlockType.Template))
+ {
+ Context.CurrentBlock.CodeGenerator = new TemplateBlockCodeGenerator();
+ PutCurrentBack();
+ OtherParserBlock();
+ }
+ }
+
+ private void OtherParserBlock()
+ {
+ ParseWithOtherParser(p => p.ParseBlock());
+ }
+
+ private void SectionBlock(string left, string right, bool caseSensitive)
+ {
+ ParseWithOtherParser(p => p.ParseSection(Tuple.Create(left, right), caseSensitive));
+ }
+
+ private void NestedBlock()
+ {
+ Output(SpanKind.Code);
+ bool wasNested = IsNested;
+ IsNested = true;
+ using (PushSpanConfig())
+ {
+ ParseBlock();
+ }
+ Initialize(Span);
+ IsNested = wasNested;
+ NextToken();
+ }
+
+ protected override bool IsAtEmbeddedTransition(bool allowTemplatesAndComments, bool allowTransitions)
+ {
+ // No embedded transitions in C#, so ignore that param
+ return allowTemplatesAndComments
+ && ((Language.IsTransition(CurrentSymbol)
+ && NextIs(CSharpSymbolType.LessThan, CSharpSymbolType.Colon))
+ || Language.IsCommentStart(CurrentSymbol));
+ }
+
+ protected override void HandleEmbeddedTransition()
+ {
+ if (Language.IsTransition(CurrentSymbol))
+ {
+ PutCurrentBack();
+ Template();
+ }
+ else if (Language.IsCommentStart(CurrentSymbol))
+ {
+ RazorComment();
+ }
+ }
+
+ private void ParseWithOtherParser(Action<ParserBase> parseAction)
+ {
+ using (PushSpanConfig())
+ {
+ Context.SwitchActiveParser();
+ parseAction(Context.MarkupParser);
+ Context.SwitchActiveParser();
+ }
+ Initialize(Span);
+ NextToken();
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Parser/CSharpLanguageCharacteristics.cs b/src/System.Web.Razor/Parser/CSharpLanguageCharacteristics.cs
new file mode 100644
index 00000000..b7a462a1
--- /dev/null
+++ b/src/System.Web.Razor/Parser/CSharpLanguageCharacteristics.cs
@@ -0,0 +1,187 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Text;
+using System.Web.Razor.Tokenizer;
+using System.Web.Razor.Tokenizer.Symbols;
+
+namespace System.Web.Razor.Parser
+{
+ public class CSharpLanguageCharacteristics : LanguageCharacteristics<CSharpTokenizer, CSharpSymbol, CSharpSymbolType>
+ {
+ private static readonly CSharpLanguageCharacteristics _instance = new CSharpLanguageCharacteristics();
+
+ private static Dictionary<CSharpSymbolType, string> _symbolSamples = new Dictionary<CSharpSymbolType, string>()
+ {
+ { CSharpSymbolType.Arrow, "->" },
+ { CSharpSymbolType.Minus, "-" },
+ { CSharpSymbolType.Decrement, "--" },
+ { CSharpSymbolType.MinusAssign, "-=" },
+ { CSharpSymbolType.NotEqual, "!=" },
+ { CSharpSymbolType.Not, "!" },
+ { CSharpSymbolType.Modulo, "%" },
+ { CSharpSymbolType.ModuloAssign, "%=" },
+ { CSharpSymbolType.AndAssign, "&=" },
+ { CSharpSymbolType.And, "&" },
+ { CSharpSymbolType.DoubleAnd, "&&" },
+ { CSharpSymbolType.LeftParenthesis, "(" },
+ { CSharpSymbolType.RightParenthesis, ")" },
+ { CSharpSymbolType.Star, "*" },
+ { CSharpSymbolType.MultiplyAssign, "*=" },
+ { CSharpSymbolType.Comma, "," },
+ { CSharpSymbolType.Dot, "." },
+ { CSharpSymbolType.Slash, "/" },
+ { CSharpSymbolType.DivideAssign, "/=" },
+ { CSharpSymbolType.DoubleColon, "::" },
+ { CSharpSymbolType.Colon, ":" },
+ { CSharpSymbolType.Semicolon, ";" },
+ { CSharpSymbolType.QuestionMark, "?" },
+ { CSharpSymbolType.NullCoalesce, "??" },
+ { CSharpSymbolType.RightBracket, "]" },
+ { CSharpSymbolType.LeftBracket, "[" },
+ { CSharpSymbolType.XorAssign, "^=" },
+ { CSharpSymbolType.Xor, "^" },
+ { CSharpSymbolType.LeftBrace, "{" },
+ { CSharpSymbolType.OrAssign, "|=" },
+ { CSharpSymbolType.DoubleOr, "||" },
+ { CSharpSymbolType.Or, "|" },
+ { CSharpSymbolType.RightBrace, "}" },
+ { CSharpSymbolType.Tilde, "~" },
+ { CSharpSymbolType.Plus, "+" },
+ { CSharpSymbolType.PlusAssign, "+=" },
+ { CSharpSymbolType.Increment, "++" },
+ { CSharpSymbolType.LessThan, "<" },
+ { CSharpSymbolType.LessThanEqual, "<=" },
+ { CSharpSymbolType.LeftShift, "<<" },
+ { CSharpSymbolType.LeftShiftAssign, "<<=" },
+ { CSharpSymbolType.Assign, "=" },
+ { CSharpSymbolType.Equals, "==" },
+ { CSharpSymbolType.GreaterThan, ">" },
+ { CSharpSymbolType.GreaterThanEqual, ">=" },
+ { CSharpSymbolType.RightShift, ">>" },
+ { CSharpSymbolType.RightShiftAssign, ">>>" },
+ { CSharpSymbolType.Hash, "#" },
+ { CSharpSymbolType.Transition, "@" },
+ };
+
+ private CSharpLanguageCharacteristics()
+ {
+ }
+
+ public static CSharpLanguageCharacteristics Instance
+ {
+ get { return _instance; }
+ }
+
+ public override CSharpTokenizer CreateTokenizer(ITextDocument source)
+ {
+ return new CSharpTokenizer(source);
+ }
+
+ protected override CSharpSymbol CreateSymbol(SourceLocation location, string content, CSharpSymbolType type, IEnumerable<RazorError> errors)
+ {
+ return new CSharpSymbol(location, content, type, errors);
+ }
+
+ public override string GetSample(CSharpSymbolType type)
+ {
+ return GetSymbolSample(type);
+ }
+
+ public override CSharpSymbol CreateMarkerSymbol(SourceLocation location)
+ {
+ return new CSharpSymbol(location, String.Empty, CSharpSymbolType.Unknown);
+ }
+
+ public override CSharpSymbolType GetKnownSymbolType(KnownSymbolType type)
+ {
+ switch (type)
+ {
+ case KnownSymbolType.Identifier:
+ return CSharpSymbolType.Identifier;
+ case KnownSymbolType.Keyword:
+ return CSharpSymbolType.Keyword;
+ case KnownSymbolType.NewLine:
+ return CSharpSymbolType.NewLine;
+ case KnownSymbolType.WhiteSpace:
+ return CSharpSymbolType.WhiteSpace;
+ case KnownSymbolType.Transition:
+ return CSharpSymbolType.Transition;
+ case KnownSymbolType.CommentStart:
+ return CSharpSymbolType.RazorCommentTransition;
+ case KnownSymbolType.CommentStar:
+ return CSharpSymbolType.RazorCommentStar;
+ case KnownSymbolType.CommentBody:
+ return CSharpSymbolType.RazorComment;
+ default:
+ return CSharpSymbolType.Unknown;
+ }
+ }
+
+ public override CSharpSymbolType FlipBracket(CSharpSymbolType bracket)
+ {
+ switch (bracket)
+ {
+ case CSharpSymbolType.LeftBrace:
+ return CSharpSymbolType.RightBrace;
+ case CSharpSymbolType.LeftBracket:
+ return CSharpSymbolType.RightBracket;
+ case CSharpSymbolType.LeftParenthesis:
+ return CSharpSymbolType.RightParenthesis;
+ case CSharpSymbolType.LessThan:
+ return CSharpSymbolType.GreaterThan;
+ case CSharpSymbolType.RightBrace:
+ return CSharpSymbolType.LeftBrace;
+ case CSharpSymbolType.RightBracket:
+ return CSharpSymbolType.LeftBracket;
+ case CSharpSymbolType.RightParenthesis:
+ return CSharpSymbolType.LeftParenthesis;
+ case CSharpSymbolType.GreaterThan:
+ return CSharpSymbolType.LessThan;
+ default:
+ Debug.Fail("FlipBracket must be called with a bracket character");
+ return CSharpSymbolType.Unknown;
+ }
+ }
+
+ [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "C# Keywords are lower-case")]
+ public static string GetKeyword(CSharpKeyword keyword)
+ {
+ return keyword.ToString().ToLowerInvariant();
+ }
+
+ public static string GetSymbolSample(CSharpSymbolType type)
+ {
+ string sample;
+ if (!_symbolSamples.TryGetValue(type, out sample))
+ {
+ switch (type)
+ {
+ case CSharpSymbolType.Identifier:
+ return RazorResources.CSharpSymbol_Identifier;
+ case CSharpSymbolType.Keyword:
+ return RazorResources.CSharpSymbol_Keyword;
+ case CSharpSymbolType.IntegerLiteral:
+ return RazorResources.CSharpSymbol_IntegerLiteral;
+ case CSharpSymbolType.NewLine:
+ return RazorResources.CSharpSymbol_Newline;
+ case CSharpSymbolType.WhiteSpace:
+ return RazorResources.CSharpSymbol_Whitespace;
+ case CSharpSymbolType.Comment:
+ return RazorResources.CSharpSymbol_Comment;
+ case CSharpSymbolType.RealLiteral:
+ return RazorResources.CSharpSymbol_RealLiteral;
+ case CSharpSymbolType.CharacterLiteral:
+ return RazorResources.CSharpSymbol_CharacterLiteral;
+ case CSharpSymbolType.StringLiteral:
+ return RazorResources.CSharpSymbol_StringLiteral;
+ default:
+ return RazorResources.Symbol_Unknown;
+ }
+ }
+ return sample;
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Parser/CallbackVisitor.cs b/src/System.Web.Razor/Parser/CallbackVisitor.cs
new file mode 100644
index 00000000..527c723e
--- /dev/null
+++ b/src/System.Web.Razor/Parser/CallbackVisitor.cs
@@ -0,0 +1,93 @@
+using System.Threading;
+using System.Web.Razor.Parser.SyntaxTree;
+
+namespace System.Web.Razor.Parser
+{
+ public class CallbackVisitor : ParserVisitor
+ {
+ private Action<Span> _spanCallback;
+ private Action<RazorError> _errorCallback;
+ private Action<BlockType> _endBlockCallback;
+ private Action<BlockType> _startBlockCallback;
+ private Action _completeCallback;
+
+ public CallbackVisitor(Action<Span> spanCallback)
+ : this(spanCallback, _ =>
+ {
+ })
+ {
+ }
+
+ public CallbackVisitor(Action<Span> spanCallback, Action<RazorError> errorCallback)
+ : this(spanCallback, errorCallback, _ =>
+ {
+ }, _ =>
+ {
+ })
+ {
+ }
+
+ public CallbackVisitor(Action<Span> spanCallback, Action<RazorError> errorCallback, Action<BlockType> startBlockCallback, Action<BlockType> endBlockCallback)
+ : this(spanCallback, errorCallback, startBlockCallback, endBlockCallback, () =>
+ {
+ })
+ {
+ }
+
+ public CallbackVisitor(Action<Span> spanCallback, Action<RazorError> errorCallback, Action<BlockType> startBlockCallback, Action<BlockType> endBlockCallback, Action completeCallback)
+ {
+ _spanCallback = spanCallback;
+ _errorCallback = errorCallback;
+ _startBlockCallback = startBlockCallback;
+ _endBlockCallback = endBlockCallback;
+ _completeCallback = completeCallback;
+ }
+
+ public SynchronizationContext SynchronizationContext { get; set; }
+
+ public override void VisitStartBlock(Block block)
+ {
+ base.VisitStartBlock(block);
+ RaiseCallback(SynchronizationContext, block.Type, _startBlockCallback);
+ }
+
+ public override void VisitSpan(Span span)
+ {
+ base.VisitSpan(span);
+ RaiseCallback(SynchronizationContext, span, _spanCallback);
+ }
+
+ public override void VisitEndBlock(Block block)
+ {
+ base.VisitEndBlock(block);
+ RaiseCallback(SynchronizationContext, block.Type, _endBlockCallback);
+ }
+
+ public override void VisitError(RazorError err)
+ {
+ base.VisitError(err);
+ RaiseCallback(SynchronizationContext, err, _errorCallback);
+ }
+
+ public override void OnComplete()
+ {
+ base.OnComplete();
+ RaiseCallback<object>(SynchronizationContext, null, _ => _completeCallback());
+ }
+
+ private static void RaiseCallback<T>(SynchronizationContext syncContext, T param, Action<T> callback)
+ {
+ if (callback != null)
+ {
+ if (syncContext != null)
+ {
+ syncContext.Post(state => callback((T)state), param);
+ }
+ else
+ {
+ callback(param);
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Parser/ConditionalAttributeCollapser.cs b/src/System.Web.Razor/Parser/ConditionalAttributeCollapser.cs
new file mode 100644
index 00000000..f7c610b0
--- /dev/null
+++ b/src/System.Web.Razor/Parser/ConditionalAttributeCollapser.cs
@@ -0,0 +1,49 @@
+using System.Diagnostics;
+using System.Linq;
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Text;
+
+namespace System.Web.Razor.Parser
+{
+ internal class ConditionalAttributeCollapser : MarkupRewriter
+ {
+ public ConditionalAttributeCollapser(Action<SpanBuilder, SourceLocation, string> markupSpanFactory) : base(markupSpanFactory)
+ {
+ }
+
+ protected override bool CanRewrite(Block block)
+ {
+ AttributeBlockCodeGenerator gen = block.CodeGenerator as AttributeBlockCodeGenerator;
+ return gen != null && block.Children.Any() && block.Children.All(IsLiteralAttributeValue);
+ }
+
+ protected override SyntaxTreeNode RewriteBlock(BlockBuilder parent, Block block)
+ {
+ // Collect the content of this node
+ string content = String.Concat(block.Children.Cast<Span>().Select(s => s.Content));
+
+ // Create a new span containing this content
+ SpanBuilder span = new SpanBuilder();
+ FillSpan(span, block.Children.Cast<Span>().First().Start, content);
+ return span.Build();
+ }
+
+ private bool IsLiteralAttributeValue(SyntaxTreeNode node)
+ {
+ if (node.IsBlock)
+ {
+ return false;
+ }
+ Span span = node as Span;
+ Debug.Assert(span != null);
+
+ LiteralAttributeCodeGenerator litGen = span.CodeGenerator as LiteralAttributeCodeGenerator;
+
+ return span != null &&
+ ((litGen != null && litGen.ValueGenerator == null) ||
+ span.CodeGenerator == SpanCodeGenerator.Null ||
+ span.CodeGenerator is MarkupCodeGenerator);
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Parser/HtmlLanguageCharacteristics.cs b/src/System.Web.Razor/Parser/HtmlLanguageCharacteristics.cs
new file mode 100644
index 00000000..d9da49a7
--- /dev/null
+++ b/src/System.Web.Razor/Parser/HtmlLanguageCharacteristics.cs
@@ -0,0 +1,129 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Text;
+using System.Web.Razor.Tokenizer;
+using System.Web.Razor.Tokenizer.Symbols;
+
+namespace System.Web.Razor.Parser
+{
+ public class HtmlLanguageCharacteristics : LanguageCharacteristics<HtmlTokenizer, HtmlSymbol, HtmlSymbolType>
+ {
+ private static readonly HtmlLanguageCharacteristics _instance = new HtmlLanguageCharacteristics();
+
+ private HtmlLanguageCharacteristics()
+ {
+ }
+
+ public static HtmlLanguageCharacteristics Instance
+ {
+ get { return _instance; }
+ }
+
+ public override string GetSample(HtmlSymbolType type)
+ {
+ switch (type)
+ {
+ case HtmlSymbolType.Text:
+ return RazorResources.HtmlSymbol_Text;
+ case HtmlSymbolType.WhiteSpace:
+ return RazorResources.HtmlSymbol_WhiteSpace;
+ case HtmlSymbolType.NewLine:
+ return RazorResources.HtmlSymbol_NewLine;
+ case HtmlSymbolType.OpenAngle:
+ return "<";
+ case HtmlSymbolType.Bang:
+ return "!";
+ case HtmlSymbolType.Solidus:
+ return "/";
+ case HtmlSymbolType.QuestionMark:
+ return "?";
+ case HtmlSymbolType.DoubleHyphen:
+ return "--";
+ case HtmlSymbolType.LeftBracket:
+ return "[";
+ case HtmlSymbolType.CloseAngle:
+ return ">";
+ case HtmlSymbolType.RightBracket:
+ return "]";
+ case HtmlSymbolType.Equals:
+ return "=";
+ case HtmlSymbolType.DoubleQuote:
+ return "\"";
+ case HtmlSymbolType.SingleQuote:
+ return "'";
+ case HtmlSymbolType.Transition:
+ return "@";
+ case HtmlSymbolType.Colon:
+ return ":";
+ case HtmlSymbolType.RazorComment:
+ return RazorResources.HtmlSymbol_RazorComment;
+ case HtmlSymbolType.RazorCommentStar:
+ return "*";
+ case HtmlSymbolType.RazorCommentTransition:
+ return "@";
+ default:
+ return RazorResources.Symbol_Unknown;
+ }
+ }
+
+ public override HtmlTokenizer CreateTokenizer(ITextDocument source)
+ {
+ return new HtmlTokenizer(source);
+ }
+
+ public override HtmlSymbolType FlipBracket(HtmlSymbolType bracket)
+ {
+ switch (bracket)
+ {
+ case HtmlSymbolType.LeftBracket:
+ return HtmlSymbolType.RightBracket;
+ case HtmlSymbolType.OpenAngle:
+ return HtmlSymbolType.CloseAngle;
+ case HtmlSymbolType.RightBracket:
+ return HtmlSymbolType.LeftBracket;
+ case HtmlSymbolType.CloseAngle:
+ return HtmlSymbolType.OpenAngle;
+ default:
+ Debug.Fail("FlipBracket must be called with a bracket character");
+ return HtmlSymbolType.Unknown;
+ }
+ }
+
+ public override HtmlSymbol CreateMarkerSymbol(SourceLocation location)
+ {
+ return new HtmlSymbol(location, String.Empty, HtmlSymbolType.Unknown);
+ }
+
+ public override HtmlSymbolType GetKnownSymbolType(KnownSymbolType type)
+ {
+ switch (type)
+ {
+ case KnownSymbolType.CommentStart:
+ return HtmlSymbolType.RazorCommentTransition;
+ case KnownSymbolType.CommentStar:
+ return HtmlSymbolType.RazorCommentStar;
+ case KnownSymbolType.CommentBody:
+ return HtmlSymbolType.RazorComment;
+ case KnownSymbolType.Identifier:
+ return HtmlSymbolType.Text;
+ case KnownSymbolType.Keyword:
+ return HtmlSymbolType.Text;
+ case KnownSymbolType.NewLine:
+ return HtmlSymbolType.NewLine;
+ case KnownSymbolType.Transition:
+ return HtmlSymbolType.Transition;
+ case KnownSymbolType.WhiteSpace:
+ return HtmlSymbolType.WhiteSpace;
+ default:
+ return HtmlSymbolType.Unknown;
+ }
+ }
+
+ protected override HtmlSymbol CreateSymbol(SourceLocation location, string content, HtmlSymbolType type, IEnumerable<RazorError> errors)
+ {
+ return new HtmlSymbol(location, content, type, errors);
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Parser/HtmlMarkupParser.Block.cs b/src/System.Web.Razor/Parser/HtmlMarkupParser.Block.cs
new file mode 100644
index 00000000..568d0a08
--- /dev/null
+++ b/src/System.Web.Razor/Parser/HtmlMarkupParser.Block.cs
@@ -0,0 +1,770 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Web.Razor.Editor;
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Text;
+using System.Web.Razor.Tokenizer.Symbols;
+
+namespace System.Web.Razor.Parser
+{
+ public partial class HtmlMarkupParser
+ {
+ private SourceLocation _lastTagStart = SourceLocation.Zero;
+ private HtmlSymbol _bufferedOpenAngle;
+
+ public override void ParseBlock()
+ {
+ if (Context == null)
+ {
+ throw new InvalidOperationException(RazorResources.Parser_Context_Not_Set);
+ }
+
+ using (PushSpanConfig(DefaultMarkupSpan))
+ {
+ using (Context.StartBlock(BlockType.Markup))
+ {
+ if (!NextToken())
+ {
+ return;
+ }
+
+ AcceptWhile(IsSpacingToken(includeNewLines: true));
+
+ if (CurrentSymbol.Type == HtmlSymbolType.OpenAngle)
+ {
+ // "<" => Implicit Tag Block
+ TagBlock(new Stack<Tuple<HtmlSymbol, SourceLocation>>());
+ }
+ else if (CurrentSymbol.Type == HtmlSymbolType.Transition)
+ {
+ // "@" => Explicit Tag/Single Line Block OR Template
+ Output(SpanKind.Markup);
+
+ // Definitely have a transition span
+ Assert(HtmlSymbolType.Transition);
+ AcceptAndMoveNext();
+ Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
+ Span.CodeGenerator = SpanCodeGenerator.Null;
+ Output(SpanKind.Transition);
+ if (At(HtmlSymbolType.Transition))
+ {
+ Span.CodeGenerator = SpanCodeGenerator.Null;
+ AcceptAndMoveNext();
+ Output(SpanKind.MetaCode);
+ }
+ AfterTransition();
+ }
+ else
+ {
+ Context.OnError(CurrentSymbol.Start, RazorResources.ParseError_MarkupBlock_Must_Start_With_Tag);
+ }
+ Output(SpanKind.Markup);
+ }
+ }
+ }
+
+ private void DefaultMarkupSpan(SpanBuilder span)
+ {
+ span.CodeGenerator = new MarkupCodeGenerator();
+ span.EditHandler = new SpanEditHandler(Language.TokenizeString, AcceptedCharacters.Any);
+ }
+
+ private void AfterTransition()
+ {
+ // "@:" => Explicit Single Line Block
+ if (CurrentSymbol.Type == HtmlSymbolType.Text && CurrentSymbol.Content.Length > 0 && CurrentSymbol.Content[0] == ':')
+ {
+ // Split the token
+ Tuple<HtmlSymbol, HtmlSymbol> split = Language.SplitSymbol(CurrentSymbol, 1, HtmlSymbolType.Colon);
+
+ // The first part (left) is added to this span and we return a MetaCode span
+ Accept(split.Item1);
+ Span.CodeGenerator = SpanCodeGenerator.Null;
+ Output(SpanKind.MetaCode);
+ if (split.Item2 != null)
+ {
+ Accept(split.Item2);
+ }
+ NextToken();
+ SingleLineMarkup();
+ }
+ else if (CurrentSymbol.Type == HtmlSymbolType.OpenAngle)
+ {
+ TagBlock(new Stack<Tuple<HtmlSymbol, SourceLocation>>());
+ }
+ }
+
+ private void SingleLineMarkup()
+ {
+ // Parse until a newline, it's that simple!
+ // First, signal to code parser that whitespace is significant to us.
+ bool old = Context.WhiteSpaceIsSignificantToAncestorBlock;
+ Context.WhiteSpaceIsSignificantToAncestorBlock = true;
+ Span.EditHandler = new SingleLineMarkupEditHandler(Language.TokenizeString);
+ SkipToAndParseCode(HtmlSymbolType.NewLine);
+ if (!EndOfFile && CurrentSymbol.Type == HtmlSymbolType.NewLine)
+ {
+ AcceptAndMoveNext();
+ Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
+ }
+ PutCurrentBack();
+ Context.WhiteSpaceIsSignificantToAncestorBlock = old;
+ Output(SpanKind.Markup);
+ }
+
+ private void TagBlock(Stack<Tuple<HtmlSymbol, SourceLocation>> tags)
+ {
+ // Skip Whitespace and Text
+ bool complete = false;
+ do
+ {
+ SkipToAndParseCode(HtmlSymbolType.OpenAngle);
+ if (EndOfFile)
+ {
+ EndTagBlock(tags, complete: true);
+ }
+ else
+ {
+ _bufferedOpenAngle = null;
+ _lastTagStart = CurrentLocation;
+ Assert(HtmlSymbolType.OpenAngle);
+ _bufferedOpenAngle = CurrentSymbol;
+ SourceLocation tagStart = CurrentLocation;
+ if (!NextToken())
+ {
+ Accept(_bufferedOpenAngle);
+ EndTagBlock(tags, complete: false);
+ }
+ else
+ {
+ complete = AfterTagStart(tagStart, tags);
+ }
+ }
+ }
+ while (tags.Count > 0);
+
+ EndTagBlock(tags, complete);
+ }
+
+ private bool AfterTagStart(SourceLocation tagStart, Stack<Tuple<HtmlSymbol, SourceLocation>> tags)
+ {
+ if (!EndOfFile)
+ {
+ switch (CurrentSymbol.Type)
+ {
+ case HtmlSymbolType.Solidus:
+ // End Tag
+ return EndTag(tagStart, tags);
+ case HtmlSymbolType.Bang:
+ // Comment
+ Accept(_bufferedOpenAngle);
+ return BangTag();
+ case HtmlSymbolType.QuestionMark:
+ // XML PI
+ Accept(_bufferedOpenAngle);
+ return XmlPI();
+ default:
+ // Start Tag
+ return StartTag(tags);
+ }
+ }
+ if (tags.Count == 0)
+ {
+ Context.OnError(CurrentLocation, RazorResources.ParseError_OuterTagMissingName);
+ }
+ return false;
+ }
+
+ private bool XmlPI()
+ {
+ // Accept "?"
+ Assert(HtmlSymbolType.QuestionMark);
+ AcceptAndMoveNext();
+ return AcceptUntilAll(HtmlSymbolType.QuestionMark, HtmlSymbolType.CloseAngle);
+ }
+
+ private bool BangTag()
+ {
+ // Accept "!"
+ Assert(HtmlSymbolType.Bang);
+ AcceptAndMoveNext();
+ if (CurrentSymbol.Type == HtmlSymbolType.DoubleHyphen)
+ {
+ AcceptAndMoveNext();
+ return AcceptUntilAll(HtmlSymbolType.DoubleHyphen, HtmlSymbolType.CloseAngle);
+ }
+ else if (CurrentSymbol.Type == HtmlSymbolType.LeftBracket)
+ {
+ AcceptAndMoveNext();
+ return CData();
+ }
+ else
+ {
+ AcceptAndMoveNext();
+ return AcceptUntilAll(HtmlSymbolType.CloseAngle);
+ }
+ }
+
+ private bool CData()
+ {
+ if (CurrentSymbol.Type == HtmlSymbolType.Text && String.Equals(CurrentSymbol.Content, "cdata", StringComparison.OrdinalIgnoreCase))
+ {
+ AcceptAndMoveNext();
+ if (CurrentSymbol.Type == HtmlSymbolType.LeftBracket)
+ {
+ return AcceptUntilAll(HtmlSymbolType.RightBracket, HtmlSymbolType.RightBracket, HtmlSymbolType.CloseAngle);
+ }
+ }
+ return false;
+ }
+
+ private bool EndTag(SourceLocation tagStart, Stack<Tuple<HtmlSymbol, SourceLocation>> tags)
+ {
+ // Accept "/" and move next
+ Assert(HtmlSymbolType.Solidus);
+ HtmlSymbol solidus = CurrentSymbol;
+ if (!NextToken())
+ {
+ Accept(_bufferedOpenAngle);
+ Accept(solidus);
+ return false;
+ }
+ else
+ {
+ string tagName = String.Empty;
+ if (At(HtmlSymbolType.Text))
+ {
+ tagName = CurrentSymbol.Content;
+ }
+ bool matched = RemoveTag(tags, tagName, tagStart);
+
+ if (tags.Count == 0 &&
+ String.Equals(tagName, SyntaxConstants.TextTagName, StringComparison.OrdinalIgnoreCase) &&
+ matched)
+ {
+ Output(SpanKind.Markup);
+ return EndTextTag(solidus);
+ }
+ Accept(_bufferedOpenAngle);
+ Accept(solidus);
+
+ AcceptUntil(HtmlSymbolType.CloseAngle);
+
+ // Accept the ">"
+ return Optional(HtmlSymbolType.CloseAngle);
+ }
+ }
+
+ private bool EndTextTag(HtmlSymbol solidus)
+ {
+ SourceLocation start = _bufferedOpenAngle.Start;
+
+ Accept(_bufferedOpenAngle);
+ Accept(solidus);
+
+ Assert(HtmlSymbolType.Text);
+ AcceptAndMoveNext();
+
+ bool seenCloseAngle = Optional(HtmlSymbolType.CloseAngle);
+
+ if (!seenCloseAngle)
+ {
+ Context.OnError(start, RazorResources.ParseError_TextTagCannotContainAttributes);
+ }
+ else
+ {
+ Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
+ }
+
+ Span.CodeGenerator = SpanCodeGenerator.Null;
+ Output(SpanKind.Transition);
+ return seenCloseAngle;
+ }
+
+ private bool IsTagRecoveryStopPoint(HtmlSymbol sym)
+ {
+ return sym.Type == HtmlSymbolType.CloseAngle ||
+ sym.Type == HtmlSymbolType.Solidus ||
+ sym.Type == HtmlSymbolType.OpenAngle ||
+ sym.Type == HtmlSymbolType.SingleQuote ||
+ sym.Type == HtmlSymbolType.DoubleQuote;
+ }
+
+ private void TagContent()
+ {
+ if (!At(HtmlSymbolType.WhiteSpace))
+ {
+ // We should be right after the tag name, so if there's no whitespace, something is wrong
+ RecoverToEndOfTag();
+ }
+ else
+ {
+ // We are here ($): <tag$ foo="bar" biz="~/Baz" />
+ while (!EndOfFile && !IsEndOfTag())
+ {
+ BeforeAttribute();
+ }
+ }
+ }
+
+ private bool IsEndOfTag()
+ {
+ if (At(HtmlSymbolType.Solidus))
+ {
+ if (NextIs(HtmlSymbolType.CloseAngle))
+ {
+ return true;
+ }
+ else
+ {
+ AcceptAndMoveNext();
+ }
+ }
+ return At(HtmlSymbolType.CloseAngle) || At(HtmlSymbolType.OpenAngle);
+ }
+
+ private void BeforeAttribute()
+ {
+ // http://dev.w3.org/html5/spec/tokenization.html#before-attribute-name-state
+ // Capture whitespace
+ var whitespace = ReadWhile(sym => sym.Type == HtmlSymbolType.WhiteSpace || sym.Type == HtmlSymbolType.NewLine);
+
+ if (At(HtmlSymbolType.Transition))
+ {
+ // Transition outside of attribute value => Switch to recovery mode
+ Accept(whitespace);
+ RecoverToEndOfTag();
+ return;
+ }
+
+ // http://dev.w3.org/html5/spec/tokenization.html#attribute-name-state
+ // Read the 'name' (i.e. read until the '=' or whitespace/newline)
+ var name = Enumerable.Empty<HtmlSymbol>();
+ if (At(HtmlSymbolType.Text))
+ {
+ name = ReadWhile(sym =>
+ sym.Type != HtmlSymbolType.WhiteSpace &&
+ sym.Type != HtmlSymbolType.NewLine &&
+ sym.Type != HtmlSymbolType.Equals &&
+ sym.Type != HtmlSymbolType.CloseAngle &&
+ sym.Type != HtmlSymbolType.OpenAngle &&
+ (sym.Type != HtmlSymbolType.Solidus || !NextIs(HtmlSymbolType.CloseAngle)));
+ }
+ else
+ {
+ // Unexpected character in tag, enter recovery
+ Accept(whitespace);
+ RecoverToEndOfTag();
+ return;
+ }
+
+ if (!At(HtmlSymbolType.Equals))
+ {
+ // Saw a space or newline after the name, so just skip this attribute and continue around the loop
+ Accept(whitespace);
+ Accept(name);
+ return;
+ }
+
+ Output(SpanKind.Markup);
+
+ // Start a new markup block for the attribute
+ using (Context.StartBlock(BlockType.Markup))
+ {
+ AttributePrefix(whitespace, name);
+ }
+ }
+
+ private void AttributePrefix(IEnumerable<HtmlSymbol> whitespace, IEnumerable<HtmlSymbol> name)
+ {
+ // Accept the whitespace and name
+ Accept(whitespace);
+ Accept(name);
+ Assert(HtmlSymbolType.Equals); // We should be at "="
+ AcceptAndMoveNext();
+ HtmlSymbolType quote = HtmlSymbolType.Unknown;
+ if (At(HtmlSymbolType.SingleQuote) || At(HtmlSymbolType.DoubleQuote))
+ {
+ quote = CurrentSymbol.Type;
+ AcceptAndMoveNext();
+ }
+
+ // We now have the prefix: (i.e. ' foo="')
+ LocationTagged<string> prefix = Span.GetContent();
+ Span.CodeGenerator = SpanCodeGenerator.Null; // The block code generator will render the prefix
+ Output(SpanKind.Markup);
+
+ // Read the values
+ while (!EndOfFile && !IsEndOfAttributeValue(quote, CurrentSymbol))
+ {
+ AttributeValue(quote);
+ }
+
+ // Capture the suffix
+ LocationTagged<string> suffix = new LocationTagged<string>(String.Empty, CurrentLocation);
+ if (quote != HtmlSymbolType.Unknown && At(quote))
+ {
+ suffix = CurrentSymbol.GetContent();
+ AcceptAndMoveNext();
+ }
+
+ if (Span.Symbols.Count > 0)
+ {
+ Span.CodeGenerator = SpanCodeGenerator.Null; // Again, block code generator will render the suffix
+ Output(SpanKind.Markup);
+ }
+
+ // Create the block code generator
+ Context.CurrentBlock.CodeGenerator = new AttributeBlockCodeGenerator(
+ name.GetContent(Span.Start), prefix, suffix);
+ }
+
+ private void AttributeValue(HtmlSymbolType quote)
+ {
+ SourceLocation prefixStart = CurrentLocation;
+ var prefix = ReadWhile(sym => sym.Type == HtmlSymbolType.WhiteSpace || sym.Type == HtmlSymbolType.NewLine);
+ Accept(prefix);
+
+ if (At(HtmlSymbolType.Transition))
+ {
+ SourceLocation valueStart = CurrentLocation;
+ PutCurrentBack();
+
+ // Output the prefix but as a null-span. DynamicAttributeBlockCodeGenerator will render it
+ Span.CodeGenerator = SpanCodeGenerator.Null;
+
+ // Dynamic value, start a new block and set the code generator
+ using (Context.StartBlock(BlockType.Markup))
+ {
+ Context.CurrentBlock.CodeGenerator = new DynamicAttributeBlockCodeGenerator(prefix.GetContent(prefixStart), valueStart);
+
+ OtherParserBlock();
+ }
+ }
+ else if (At(HtmlSymbolType.Text) && CurrentSymbol.Content.Length > 0 && CurrentSymbol.Content[0] == '~' && NextIs(HtmlSymbolType.Solidus))
+ {
+ // Virtual Path value
+ SourceLocation valueStart = CurrentLocation;
+ VirtualPath();
+ Span.CodeGenerator = new LiteralAttributeCodeGenerator(
+ prefix.GetContent(prefixStart),
+ new LocationTagged<SpanCodeGenerator>(new ResolveUrlCodeGenerator(), valueStart));
+ }
+ else
+ {
+ // Literal value
+ // 'quote' should be "Unknown" if not quoted and symbols coming from the tokenizer should never have "Unknown" type.
+ var value = ReadWhile(sym =>
+ // These three conditions find separators which break the attribute value into portions
+ sym.Type != HtmlSymbolType.WhiteSpace &&
+ sym.Type != HtmlSymbolType.NewLine &&
+ sym.Type != HtmlSymbolType.Transition &&
+ // This condition checks for the end of the attribute value (it repeats some of the checks above but for now that's ok)
+ !IsEndOfAttributeValue(quote, sym));
+ Accept(value);
+ Span.CodeGenerator = new LiteralAttributeCodeGenerator(prefix.GetContent(prefixStart), value.GetContent(prefixStart));
+ }
+ Output(SpanKind.Markup);
+ }
+
+ private bool IsEndOfAttributeValue(HtmlSymbolType quote, HtmlSymbol sym)
+ {
+ return EndOfFile || sym == null ||
+ (quote != HtmlSymbolType.Unknown
+ ? sym.Type == quote // If quoted, just wait for the quote
+ : IsUnquotedEndOfAttributeValue(sym));
+ }
+
+ private bool IsUnquotedEndOfAttributeValue(HtmlSymbol sym)
+ {
+ // If unquoted, we have a larger set of terminating characters:
+ // http://dev.w3.org/html5/spec/tokenization.html#attribute-value-unquoted-state
+ // Also we need to detect "/" and ">"
+ return sym.Type == HtmlSymbolType.DoubleQuote ||
+ sym.Type == HtmlSymbolType.SingleQuote ||
+ sym.Type == HtmlSymbolType.OpenAngle ||
+ sym.Type == HtmlSymbolType.Equals ||
+ (sym.Type == HtmlSymbolType.Solidus && NextIs(HtmlSymbolType.CloseAngle)) ||
+ sym.Type == HtmlSymbolType.CloseAngle ||
+ sym.Type == HtmlSymbolType.WhiteSpace ||
+ sym.Type == HtmlSymbolType.NewLine;
+ }
+
+ private void VirtualPath()
+ {
+ Assert(HtmlSymbolType.Text);
+ Debug.Assert(CurrentSymbol.Content.Length > 0 && CurrentSymbol.Content[0] == '~');
+
+ // Parse until a transition symbol, whitespace, newline or quote. We support only a fairly minimal subset of Virtual Paths
+ AcceptUntil(HtmlSymbolType.Transition, HtmlSymbolType.WhiteSpace, HtmlSymbolType.NewLine, HtmlSymbolType.SingleQuote, HtmlSymbolType.DoubleQuote);
+
+ // Output a Virtual Path span
+ Span.EditHandler.EditorHints = EditorHints.VirtualPath;
+ }
+
+ private void RecoverToEndOfTag()
+ {
+ // Accept until ">", "/" or "<", but parse code
+ while (!EndOfFile)
+ {
+ SkipToAndParseCode(IsTagRecoveryStopPoint);
+ if (!EndOfFile)
+ {
+ EnsureCurrent();
+ switch (CurrentSymbol.Type)
+ {
+ case HtmlSymbolType.SingleQuote:
+ case HtmlSymbolType.DoubleQuote:
+ ParseQuoted();
+ break;
+ case HtmlSymbolType.OpenAngle:
+ // Another "<" means this tag is invalid.
+ case HtmlSymbolType.Solidus:
+ // Empty tag
+ case HtmlSymbolType.CloseAngle:
+ // End of tag
+ return;
+ default:
+ AcceptAndMoveNext();
+ break;
+ }
+ }
+ }
+ }
+
+ private void ParseQuoted()
+ {
+ HtmlSymbolType type = CurrentSymbol.Type;
+ AcceptAndMoveNext();
+ ParseQuoted(type);
+ }
+
+ private void ParseQuoted(HtmlSymbolType type)
+ {
+ SkipToAndParseCode(type);
+ if (!EndOfFile)
+ {
+ Assert(type);
+ AcceptAndMoveNext();
+ }
+ }
+
+ private bool StartTag(Stack<Tuple<HtmlSymbol, SourceLocation>> tags)
+ {
+ // If we're at text, it's the name, otherwise the name is ""
+ HtmlSymbol tagName;
+ if (At(HtmlSymbolType.Text))
+ {
+ tagName = CurrentSymbol;
+ }
+ else
+ {
+ tagName = new HtmlSymbol(CurrentLocation, String.Empty, HtmlSymbolType.Unknown);
+ }
+
+ Tuple<HtmlSymbol, SourceLocation> tag = Tuple.Create(tagName, _lastTagStart);
+
+ if (tags.Count == 0 && String.Equals(tag.Item1.Content, SyntaxConstants.TextTagName, StringComparison.OrdinalIgnoreCase))
+ {
+ Output(SpanKind.Markup);
+ Span.CodeGenerator = SpanCodeGenerator.Null;
+
+ Accept(_bufferedOpenAngle);
+ Assert(HtmlSymbolType.Text);
+
+ AcceptAndMoveNext();
+
+ int bookmark = CurrentLocation.AbsoluteIndex;
+ IEnumerable<HtmlSymbol> tokens = ReadWhile(IsSpacingToken(includeNewLines: true));
+ bool empty = At(HtmlSymbolType.Solidus);
+ if (empty)
+ {
+ Accept(tokens);
+ Assert(HtmlSymbolType.Solidus);
+ AcceptAndMoveNext();
+ bookmark = CurrentLocation.AbsoluteIndex;
+ tokens = ReadWhile(IsSpacingToken(includeNewLines: true));
+ }
+
+ if (!Optional(HtmlSymbolType.CloseAngle))
+ {
+ Context.Source.Position = bookmark;
+ NextToken();
+ Context.OnError(tag.Item2, RazorResources.ParseError_TextTagCannotContainAttributes);
+ }
+ else
+ {
+ Accept(tokens);
+ Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
+ }
+
+ if (!empty)
+ {
+ tags.Push(tag);
+ }
+ Output(SpanKind.Transition);
+ return true;
+ }
+ Accept(_bufferedOpenAngle);
+ Optional(HtmlSymbolType.Text);
+ return RestOfTag(tag, tags);
+ }
+
+ private bool RestOfTag(Tuple<HtmlSymbol, SourceLocation> tag, Stack<Tuple<HtmlSymbol, SourceLocation>> tags)
+ {
+ TagContent();
+
+ // We are now at a possible end of the tag
+ // Found '<', so we just abort this tag.
+ if (At(HtmlSymbolType.OpenAngle))
+ {
+ return false;
+ }
+
+ bool isEmpty = At(HtmlSymbolType.Solidus);
+ // Found a solidus, so don't accept it but DON'T push the tag to the stack
+ if (isEmpty)
+ {
+ AcceptAndMoveNext();
+ }
+
+ // Check for the '>' to determine if the tag is finished
+ bool seenClose = Optional(HtmlSymbolType.CloseAngle);
+ if (!seenClose)
+ {
+ Context.OnError(tag.Item2, RazorResources.ParseError_UnfinishedTag, tag.Item1.Content);
+ }
+ else
+ {
+ if (!isEmpty)
+ {
+ // Is this a void element?
+ string tagName = tag.Item1.Content.Trim();
+ if (VoidElements.Contains(tagName))
+ {
+ // Technically, void elements like "meta" are not allowed to have end tags. Just in case they do,
+ // we need to look ahead at the next set of tokens. If we see "<", "/", tag name, accept it and the ">" following it
+ // Place a bookmark
+ int bookmark = CurrentLocation.AbsoluteIndex;
+
+ // Skip whitespace
+ IEnumerable<HtmlSymbol> ws = ReadWhile(IsSpacingToken(includeNewLines: true));
+
+ // Open Angle
+ if (At(HtmlSymbolType.OpenAngle) && NextIs(HtmlSymbolType.Solidus))
+ {
+ HtmlSymbol openAngle = CurrentSymbol;
+ NextToken();
+ Assert(HtmlSymbolType.Solidus);
+ HtmlSymbol solidus = CurrentSymbol;
+ NextToken();
+ if (At(HtmlSymbolType.Text) && String.Equals(CurrentSymbol.Content, tagName, StringComparison.OrdinalIgnoreCase))
+ {
+ // Accept up to here
+ Accept(ws);
+ Accept(openAngle);
+ Accept(solidus);
+ AcceptAndMoveNext();
+
+ // Accept to '>', '<' or EOF
+ AcceptUntil(HtmlSymbolType.CloseAngle, HtmlSymbolType.OpenAngle);
+ // Accept the '>' if we saw it. And if we do see it, we're complete
+ return Optional(HtmlSymbolType.CloseAngle);
+ } // At(HtmlSymbolType.Text) && String.Equals(CurrentSymbol.Content, tagName, StringComparison.OrdinalIgnoreCase)
+ } // At(HtmlSymbolType.OpenAngle) && NextIs(HtmlSymbolType.Solidus)
+
+ // Go back to the bookmark and just finish this tag at the close angle
+ Context.Source.Position = bookmark;
+ NextToken();
+ }
+ else
+ {
+ // Push the tag on to the stack
+ tags.Push(tag);
+ }
+ }
+ }
+ return seenClose;
+ }
+
+ private bool AcceptUntilAll(params HtmlSymbolType[] endSequence)
+ {
+ while (!EndOfFile)
+ {
+ SkipToAndParseCode(endSequence[0]);
+ if (AcceptAll(endSequence))
+ {
+ return true;
+ }
+ }
+ Debug.Assert(EndOfFile);
+ Span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any;
+ return false;
+ }
+
+ private bool RemoveTag(Stack<Tuple<HtmlSymbol, SourceLocation>> tags, string tagName, SourceLocation tagStart)
+ {
+ Tuple<HtmlSymbol, SourceLocation> currentTag = null;
+ while (tags.Count > 0)
+ {
+ currentTag = tags.Pop();
+ if (String.Equals(tagName, currentTag.Item1.Content, StringComparison.OrdinalIgnoreCase))
+ {
+ // Matched the tag
+ return true;
+ }
+ }
+ if (currentTag != null)
+ {
+ Context.OnError(currentTag.Item2, RazorResources.ParseError_MissingEndTag, currentTag.Item1.Content);
+ }
+ else
+ {
+ Context.OnError(tagStart, RazorResources.ParseError_UnexpectedEndTag, tagName);
+ }
+ return false;
+ }
+
+ private void EndTagBlock(Stack<Tuple<HtmlSymbol, SourceLocation>> tags, bool complete)
+ {
+ if (tags.Count > 0)
+ {
+ // Ended because of EOF, not matching close tag. Throw error for last tag
+ while (tags.Count > 1)
+ {
+ tags.Pop();
+ }
+ Tuple<HtmlSymbol, SourceLocation> tag = tags.Pop();
+ Context.OnError(tag.Item2, RazorResources.ParseError_MissingEndTag, tag.Item1.Content);
+ }
+ else if (complete)
+ {
+ Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
+ }
+ tags.Clear();
+ if (!Context.DesignTimeMode)
+ {
+ AcceptWhile(HtmlSymbolType.WhiteSpace);
+ if (!EndOfFile && CurrentSymbol.Type == HtmlSymbolType.NewLine)
+ {
+ AcceptAndMoveNext();
+ }
+ }
+ else if (Span.EditHandler.AcceptedCharacters == AcceptedCharacters.Any)
+ {
+ AcceptWhile(HtmlSymbolType.WhiteSpace);
+ Optional(HtmlSymbolType.NewLine);
+ }
+ PutCurrentBack();
+
+ if (!complete)
+ {
+ AddMarkerSymbolIfNecessary();
+ }
+ Output(SpanKind.Markup);
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Parser/HtmlMarkupParser.Document.cs b/src/System.Web.Razor/Parser/HtmlMarkupParser.Document.cs
new file mode 100644
index 00000000..5cefa672
--- /dev/null
+++ b/src/System.Web.Razor/Parser/HtmlMarkupParser.Document.cs
@@ -0,0 +1,36 @@
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Tokenizer.Symbols;
+
+namespace System.Web.Razor.Parser
+{
+ public partial class HtmlMarkupParser
+ {
+ public override void ParseDocument()
+ {
+ if (Context == null)
+ {
+ throw new InvalidOperationException(RazorResources.Parser_Context_Not_Set);
+ }
+
+ using (PushSpanConfig(DefaultMarkupSpan))
+ {
+ using (Context.StartBlock(BlockType.Markup))
+ {
+ NextToken();
+ while (!EndOfFile)
+ {
+ SkipToAndParseCode(HtmlSymbolType.OpenAngle);
+ if (Optional(HtmlSymbolType.OpenAngle) && !At(HtmlSymbolType.Solidus))
+ {
+ Optional(HtmlSymbolType.Text); // Tag Name, but we don't care what it is
+ TagContent(); // Parse the tag, don't care about the content
+ }
+ }
+ AddMarkerSymbolIfNecessary();
+ Output(SpanKind.Markup);
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Parser/HtmlMarkupParser.Section.cs b/src/System.Web.Razor/Parser/HtmlMarkupParser.Section.cs
new file mode 100644
index 00000000..a6816c5e
--- /dev/null
+++ b/src/System.Web.Razor/Parser/HtmlMarkupParser.Section.cs
@@ -0,0 +1,202 @@
+using System.Diagnostics;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Tokenizer.Symbols;
+
+namespace System.Web.Razor.Parser
+{
+ public partial class HtmlMarkupParser
+ {
+ private bool CaseSensitive { get; set; }
+
+ private StringComparison Comparison
+ {
+ get { return CaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; }
+ }
+
+ public override void ParseSection(Tuple<string, string> nestingSequences, bool caseSensitive)
+ {
+ if (Context == null)
+ {
+ throw new InvalidOperationException(RazorResources.Parser_Context_Not_Set);
+ }
+
+ using (PushSpanConfig(DefaultMarkupSpan))
+ {
+ using (Context.StartBlock(BlockType.Markup))
+ {
+ NextToken();
+ CaseSensitive = caseSensitive;
+ if (nestingSequences.Item1 == null)
+ {
+ NonNestingSection(nestingSequences.Item2.Split());
+ }
+ else
+ {
+ NestingSection(nestingSequences);
+ }
+ AddMarkerSymbolIfNecessary();
+ Output(SpanKind.Markup);
+ }
+ }
+ }
+
+ private void NonNestingSection(string[] nestingSequenceComponents)
+ {
+ do
+ {
+ SkipToAndParseCode(sym => sym.Type == HtmlSymbolType.OpenAngle || AtEnd(nestingSequenceComponents));
+ if (Optional(HtmlSymbolType.OpenAngle) && !At(HtmlSymbolType.Solidus))
+ {
+ Optional(HtmlSymbolType.Text); // Tag Name, but we don't care what it is
+ TagContent(); // Parse the tag, don't care about the content
+ }
+ else if (!EndOfFile && AtEnd(nestingSequenceComponents))
+ {
+ break;
+ }
+ }
+ while (!EndOfFile);
+
+ PutCurrentBack();
+ }
+
+ private void NestingSection(Tuple<string, string> nestingSequences)
+ {
+ int nesting = 1;
+ while (nesting > 0 && !EndOfFile)
+ {
+ if (At(HtmlSymbolType.Text))
+ {
+ nesting += ProcessTextToken(nestingSequences, nesting);
+ }
+ else if (At(HtmlSymbolType.Transition))
+ {
+ PutCurrentBack();
+ Output(SpanKind.Markup);
+ OtherParserBlock();
+ continue;
+ }
+ else if (At(HtmlSymbolType.RazorCommentTransition))
+ {
+ RazorComment();
+ }
+ else if (Optional(HtmlSymbolType.OpenAngle) && !At(HtmlSymbolType.Solidus))
+ {
+ Optional(HtmlSymbolType.Text); // Tag Name, but we don't care what it is
+ TagContent(); // Parse the tag, don't care about the content
+ continue;
+ }
+
+ if (CurrentSymbol != null)
+ {
+ AcceptAndMoveNext();
+ }
+ else if (nesting > 0)
+ {
+ NextToken();
+ }
+ }
+ }
+
+ private bool AtEnd(string[] nestingSequenceComponents)
+ {
+ EnsureCurrent();
+ if (String.Equals(CurrentSymbol.Content, nestingSequenceComponents[0], Comparison))
+ {
+ int bookmark = CurrentSymbol.Start.AbsoluteIndex;
+ try
+ {
+ foreach (string component in nestingSequenceComponents)
+ {
+ if (!String.Equals(CurrentSymbol.Content, component, Comparison))
+ {
+ return false;
+ }
+ NextToken();
+ while (!EndOfFile && IsSpacingToken(includeNewLines: true)(CurrentSymbol))
+ {
+ NextToken();
+ }
+ }
+ return true;
+ }
+ finally
+ {
+ Context.Source.Position = bookmark;
+ NextToken();
+ }
+ }
+ return false;
+ }
+
+ private int ProcessTextToken(Tuple<string, string> nestingSequences, int currentNesting)
+ {
+ for (int i = 0; i < CurrentSymbol.Content.Length; i++)
+ {
+ int nestingDelta = HandleNestingSequence(nestingSequences.Item1, i, currentNesting, 1);
+ if (nestingDelta == 0)
+ {
+ nestingDelta = HandleNestingSequence(nestingSequences.Item2, i, currentNesting, -1);
+ }
+
+ if (nestingDelta != 0)
+ {
+ return nestingDelta;
+ }
+ }
+ return 0;
+ }
+
+ private int HandleNestingSequence(string sequence, int position, int currentNesting, int retIfMatched)
+ {
+ if (sequence != null &&
+ CurrentSymbol.Content[position] == sequence[0] &&
+ position + sequence.Length <= CurrentSymbol.Content.Length)
+ {
+ string possibleStart = CurrentSymbol.Content.Substring(position, sequence.Length);
+ if (String.Equals(possibleStart, sequence, Comparison))
+ {
+ // Capture the current symbol and "put it back" (really we just want to clear CurrentSymbol)
+ int bookmark = Context.Source.Position;
+ HtmlSymbol sym = CurrentSymbol;
+ PutCurrentBack();
+
+ // Carve up the symbol
+ Tuple<HtmlSymbol, HtmlSymbol> pair = Language.SplitSymbol(sym, position, HtmlSymbolType.Text);
+ HtmlSymbol preSequence = pair.Item1;
+ Debug.Assert(pair.Item2 != null);
+ pair = Language.SplitSymbol(pair.Item2, sequence.Length, HtmlSymbolType.Text);
+ HtmlSymbol sequenceToken = pair.Item1;
+ HtmlSymbol postSequence = pair.Item2;
+
+ // Accept the first chunk (up to the nesting sequence we just saw)
+ if (!String.IsNullOrEmpty(preSequence.Content))
+ {
+ Accept(preSequence);
+ }
+
+ // Accept the sequence if it isn't the last one
+ if (currentNesting + retIfMatched != 0)
+ {
+ Accept(sequenceToken);
+
+ // Position at the start of the postSequence symbol
+ if (postSequence != null)
+ {
+ Context.Source.Position = postSequence.Start.AbsoluteIndex;
+ }
+ else
+ {
+ Context.Source.Position = bookmark;
+ }
+ }
+
+ // Return the value we were asked to return if matched, since we found a nesting sequence
+ return retIfMatched;
+ }
+ }
+ return 0;
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Parser/HtmlMarkupParser.cs b/src/System.Web.Razor/Parser/HtmlMarkupParser.cs
new file mode 100644
index 00000000..97f5fcf5
--- /dev/null
+++ b/src/System.Web.Razor/Parser/HtmlMarkupParser.cs
@@ -0,0 +1,184 @@
+using System.Collections.Generic;
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Text;
+using System.Web.Razor.Tokenizer;
+using System.Web.Razor.Tokenizer.Symbols;
+
+namespace System.Web.Razor.Parser
+{
+ public partial class HtmlMarkupParser : TokenizerBackedParser<HtmlTokenizer, HtmlSymbol, HtmlSymbolType>
+ {
+ //From http://dev.w3.org/html5/spec/Overview.html#elements-0
+ private ISet<string> _voidElements = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+ {
+ "area",
+ "base",
+ "br",
+ "col",
+ "command",
+ "embed",
+ "hr",
+ "img",
+ "input",
+ "keygen",
+ "link",
+ "meta",
+ "param",
+ "source",
+ "track",
+ "wbr"
+ };
+
+ public ISet<string> VoidElements
+ {
+ get { return _voidElements; }
+ }
+
+ protected override ParserBase OtherParser
+ {
+ get { return Context.CodeParser; }
+ }
+
+ protected override LanguageCharacteristics<HtmlTokenizer, HtmlSymbol, HtmlSymbolType> Language
+ {
+ get { return HtmlLanguageCharacteristics.Instance; }
+ }
+
+ public override void BuildSpan(SpanBuilder span, SourceLocation start, string content)
+ {
+ span.Kind = SpanKind.Markup;
+ span.CodeGenerator = new MarkupCodeGenerator();
+ base.BuildSpan(span, start, content);
+ }
+
+ protected override void OutputSpanBeforeRazorComment()
+ {
+ Output(SpanKind.Markup);
+ }
+
+ protected void SkipToAndParseCode(HtmlSymbolType type)
+ {
+ SkipToAndParseCode(sym => sym.Type == type);
+ }
+
+ protected void SkipToAndParseCode(Func<HtmlSymbol, bool> condition)
+ {
+ HtmlSymbol last = null;
+ bool startOfLine = false;
+ while (!EndOfFile && !condition(CurrentSymbol))
+ {
+ if (At(HtmlSymbolType.NewLine))
+ {
+ if (last != null)
+ {
+ Accept(last);
+ }
+
+ // Mark the start of a new line
+ startOfLine = true;
+ last = null;
+ AcceptAndMoveNext();
+ }
+ else if (At(HtmlSymbolType.Transition))
+ {
+ HtmlSymbol transition = CurrentSymbol;
+ NextToken();
+ if (At(HtmlSymbolType.Transition))
+ {
+ if (last != null)
+ {
+ Accept(last);
+ last = null;
+ }
+ Output(SpanKind.Markup);
+ Accept(transition);
+ Span.CodeGenerator = SpanCodeGenerator.Null;
+ Output(SpanKind.Markup);
+ AcceptAndMoveNext();
+ continue; // while
+ }
+ else
+ {
+ if (!EndOfFile)
+ {
+ PutCurrentBack();
+ }
+ PutBack(transition);
+ }
+
+ // Handle whitespace rewriting
+ if (last != null)
+ {
+ if (!Context.DesignTimeMode && last.Type == HtmlSymbolType.WhiteSpace && startOfLine)
+ {
+ // Put the whitespace back too
+ startOfLine = false;
+ PutBack(last);
+ last = null;
+ }
+ else
+ {
+ // Accept last
+ Accept(last);
+ last = null;
+ }
+ }
+
+ OtherParserBlock();
+ }
+ else if (At(HtmlSymbolType.RazorCommentTransition))
+ {
+ if (last != null)
+ {
+ Accept(last);
+ last = null;
+ }
+ AddMarkerSymbolIfNecessary();
+ Output(SpanKind.Markup);
+ RazorComment();
+ }
+ else
+ {
+ // As long as we see whitespace, we're still at the "start" of the line
+ startOfLine &= At(HtmlSymbolType.WhiteSpace);
+
+ // If there's a last token, accept it
+ if (last != null)
+ {
+ Accept(last);
+ last = null;
+ }
+
+ // Advance
+ last = CurrentSymbol;
+ NextToken();
+ }
+ }
+
+ if (last != null)
+ {
+ Accept(last);
+ }
+ }
+
+ protected static Func<HtmlSymbol, bool> IsSpacingToken(bool includeNewLines)
+ {
+ return sym => sym.Type == HtmlSymbolType.WhiteSpace || (includeNewLines && sym.Type == HtmlSymbolType.NewLine);
+ }
+
+ private void OtherParserBlock()
+ {
+ AddMarkerSymbolIfNecessary();
+ Output(SpanKind.Markup);
+ using (PushSpanConfig())
+ {
+ Context.SwitchActiveParser();
+ Context.CodeParser.ParseBlock();
+ Context.SwitchActiveParser();
+ }
+ Initialize(Span);
+ NextToken();
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Parser/ISyntaxTreeRewriter.cs b/src/System.Web.Razor/Parser/ISyntaxTreeRewriter.cs
new file mode 100644
index 00000000..f8756ae2
--- /dev/null
+++ b/src/System.Web.Razor/Parser/ISyntaxTreeRewriter.cs
@@ -0,0 +1,9 @@
+using System.Web.Razor.Parser.SyntaxTree;
+
+namespace System.Web.Razor.Parser
+{
+ internal interface ISyntaxTreeRewriter
+ {
+ Block Rewrite(Block input);
+ }
+}
diff --git a/src/System.Web.Razor/Parser/LanguageCharacteristics.cs b/src/System.Web.Razor/Parser/LanguageCharacteristics.cs
new file mode 100644
index 00000000..12a1c047
--- /dev/null
+++ b/src/System.Web.Razor/Parser/LanguageCharacteristics.cs
@@ -0,0 +1,110 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Text;
+using System.Web.Razor.Tokenizer;
+using System.Web.Razor.Tokenizer.Symbols;
+
+namespace System.Web.Razor.Parser
+{
+ [SuppressMessage("Microsoft.Design", "CA1005:AvoidExcessiveParametersOnGenericTypes", Justification = "All generic type parameters are required")]
+ public abstract class LanguageCharacteristics<TTokenizer, TSymbol, TSymbolType>
+ where TTokenizer : Tokenizer<TSymbol, TSymbolType>
+ where TSymbol : SymbolBase<TSymbolType>
+ {
+ public abstract string GetSample(TSymbolType type);
+ public abstract TTokenizer CreateTokenizer(ITextDocument source);
+ public abstract TSymbolType FlipBracket(TSymbolType bracket);
+ public abstract TSymbol CreateMarkerSymbol(SourceLocation location);
+
+ public virtual IEnumerable<TSymbol> TokenizeString(string content)
+ {
+ return TokenizeString(SourceLocation.Zero, content);
+ }
+
+ public virtual IEnumerable<TSymbol> TokenizeString(SourceLocation start, string input)
+ {
+ using (SeekableTextReader reader = new SeekableTextReader(input))
+ {
+ TTokenizer tok = CreateTokenizer(reader);
+ TSymbol sym;
+ while ((sym = tok.NextSymbol()) != null)
+ {
+ sym.OffsetStart(start);
+ yield return sym;
+ }
+ }
+ }
+
+ public virtual bool IsWhiteSpace(TSymbol symbol)
+ {
+ return IsKnownSymbolType(symbol, KnownSymbolType.WhiteSpace);
+ }
+
+ public virtual bool IsNewLine(TSymbol symbol)
+ {
+ return IsKnownSymbolType(symbol, KnownSymbolType.NewLine);
+ }
+
+ public virtual bool IsIdentifier(TSymbol symbol)
+ {
+ return IsKnownSymbolType(symbol, KnownSymbolType.Identifier);
+ }
+
+ public virtual bool IsKeyword(TSymbol symbol)
+ {
+ return IsKnownSymbolType(symbol, KnownSymbolType.Keyword);
+ }
+
+ public virtual bool IsTransition(TSymbol symbol)
+ {
+ return IsKnownSymbolType(symbol, KnownSymbolType.Transition);
+ }
+
+ public virtual bool IsCommentStart(TSymbol symbol)
+ {
+ return IsKnownSymbolType(symbol, KnownSymbolType.CommentStart);
+ }
+
+ public virtual bool IsCommentStar(TSymbol symbol)
+ {
+ return IsKnownSymbolType(symbol, KnownSymbolType.CommentStar);
+ }
+
+ public virtual bool IsCommentBody(TSymbol symbol)
+ {
+ return IsKnownSymbolType(symbol, KnownSymbolType.CommentBody);
+ }
+
+ public virtual bool IsUnknown(TSymbol symbol)
+ {
+ return IsKnownSymbolType(symbol, KnownSymbolType.Unknown);
+ }
+
+ public virtual bool IsKnownSymbolType(TSymbol symbol, KnownSymbolType type)
+ {
+ return symbol != null && Equals(symbol.Type, GetKnownSymbolType(type));
+ }
+
+ public virtual Tuple<TSymbol, TSymbol> SplitSymbol(TSymbol symbol, int splitAt, TSymbolType leftType)
+ {
+ TSymbol left = CreateSymbol(symbol.Start, symbol.Content.Substring(0, splitAt), leftType, Enumerable.Empty<RazorError>());
+ TSymbol right = null;
+ if (splitAt < symbol.Content.Length)
+ {
+ right = CreateSymbol(SourceLocationTracker.CalculateNewLocation(symbol.Start, left.Content), symbol.Content.Substring(splitAt), symbol.Type, symbol.Errors);
+ }
+ return Tuple.Create(left, right);
+ }
+
+ public abstract TSymbolType GetKnownSymbolType(KnownSymbolType type);
+
+ public virtual bool KnowsSymbolType(KnownSymbolType type)
+ {
+ return type == KnownSymbolType.Unknown || !Equals(GetKnownSymbolType(type), GetKnownSymbolType(KnownSymbolType.Unknown));
+ }
+
+ protected abstract TSymbol CreateSymbol(SourceLocation location, string content, TSymbolType type, IEnumerable<RazorError> errors);
+ }
+}
diff --git a/src/System.Web.Razor/Parser/MarkupCollapser.cs b/src/System.Web.Razor/Parser/MarkupCollapser.cs
new file mode 100644
index 00000000..024043ee
--- /dev/null
+++ b/src/System.Web.Razor/Parser/MarkupCollapser.cs
@@ -0,0 +1,35 @@
+using System.Linq;
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Text;
+
+namespace System.Web.Razor.Parser
+{
+ internal class MarkupCollapser : MarkupRewriter
+ {
+ public MarkupCollapser(Action<SpanBuilder, SourceLocation, string> markupSpanFactory) : base(markupSpanFactory)
+ {
+ }
+
+ protected override bool CanRewrite(Span span)
+ {
+ return span.Kind == SpanKind.Markup && span.CodeGenerator is MarkupCodeGenerator;
+ }
+
+ protected override SyntaxTreeNode RewriteSpan(BlockBuilder parent, Span span)
+ {
+ // Only rewrite if we have a previous that is also markup (CanRewrite does this check for us!)
+ Span previous = parent.Children.LastOrDefault() as Span;
+ if (previous == null || !CanRewrite(previous))
+ {
+ return span;
+ }
+
+ // Merge spans
+ parent.Children.Remove(previous);
+ SpanBuilder merged = new SpanBuilder();
+ FillSpan(merged, previous.Start, previous.Content + span.Content);
+ return merged.Build();
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Parser/MarkupRewriter.cs b/src/System.Web.Razor/Parser/MarkupRewriter.cs
new file mode 100644
index 00000000..69b489cd
--- /dev/null
+++ b/src/System.Web.Razor/Parser/MarkupRewriter.cs
@@ -0,0 +1,102 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Text;
+
+namespace System.Web.Razor.Parser
+{
+ internal abstract class MarkupRewriter : ParserVisitor, ISyntaxTreeRewriter
+ {
+ private Stack<BlockBuilder> _blocks = new Stack<BlockBuilder>();
+ private Action<SpanBuilder, SourceLocation, string> _markupSpanFactory;
+
+ protected MarkupRewriter(Action<SpanBuilder, SourceLocation, string> markupSpanFactory)
+ {
+ if (markupSpanFactory == null)
+ {
+ throw new ArgumentNullException("markupSpanFactory");
+ }
+ _markupSpanFactory = markupSpanFactory;
+ }
+
+ protected BlockBuilder Parent
+ {
+ get { return _blocks.Count > 0 ? _blocks.Peek() : null; }
+ }
+
+ public virtual Block Rewrite(Block input)
+ {
+ input.Accept(this);
+ Debug.Assert(_blocks.Count == 1);
+ return _blocks.Pop().Build();
+ }
+
+ public override void VisitBlock(Block block)
+ {
+ if (CanRewrite(block))
+ {
+ SyntaxTreeNode newNode = RewriteBlock(_blocks.Peek(), block);
+ if (newNode != null)
+ {
+ _blocks.Peek().Children.Add(newNode);
+ }
+ }
+ else
+ {
+ // Not rewritable.
+ BlockBuilder builder = new BlockBuilder(block);
+ builder.Children.Clear();
+ _blocks.Push(builder);
+ base.VisitBlock(block);
+ Debug.Assert(ReferenceEquals(builder, _blocks.Peek()));
+
+ if (_blocks.Count > 1)
+ {
+ _blocks.Pop();
+ _blocks.Peek().Children.Add(builder.Build());
+ }
+ }
+ }
+
+ public override void VisitSpan(Span span)
+ {
+ if (CanRewrite(span))
+ {
+ SyntaxTreeNode newNode = RewriteSpan(_blocks.Peek(), span);
+ if (newNode != null)
+ {
+ _blocks.Peek().Children.Add(newNode);
+ }
+ }
+ else
+ {
+ _blocks.Peek().Children.Add(span);
+ }
+ }
+
+ protected virtual bool CanRewrite(Block block)
+ {
+ return false;
+ }
+
+ protected virtual bool CanRewrite(Span span)
+ {
+ return false;
+ }
+
+ protected virtual SyntaxTreeNode RewriteBlock(BlockBuilder parent, Block block)
+ {
+ throw new NotImplementedException();
+ }
+
+ protected virtual SyntaxTreeNode RewriteSpan(BlockBuilder parent, Span span)
+ {
+ throw new NotImplementedException();
+ }
+
+ protected void FillSpan(SpanBuilder builder, SourceLocation start, string content)
+ {
+ _markupSpanFactory(builder, start, content);
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Parser/ParserBase.cs b/src/System.Web.Razor/Parser/ParserBase.cs
new file mode 100644
index 00000000..aa54cc6d
--- /dev/null
+++ b/src/System.Web.Razor/Parser/ParserBase.cs
@@ -0,0 +1,48 @@
+using System.Diagnostics;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Text;
+
+namespace System.Web.Razor.Parser
+{
+ public abstract class ParserBase
+ {
+ private ParserContext _context;
+
+ public virtual ParserContext Context
+ {
+ get { return _context; }
+ set
+ {
+ Debug.Assert(_context == null, "Context has already been set for this parser!");
+ _context = value;
+ _context.AssertOnOwnerTask();
+ }
+ }
+
+ public virtual bool IsMarkupParser
+ {
+ get { return false; }
+ }
+
+ protected abstract ParserBase OtherParser { get; }
+
+ public abstract void BuildSpan(SpanBuilder span, SourceLocation start, string content);
+
+ public abstract void ParseBlock();
+
+ // Markup Parsers need the ParseDocument and ParseSection methods since the markup parser is the first parser to hit the document
+ // and the logic may be different than the ParseBlock method.
+ public virtual void ParseDocument()
+ {
+ Debug.Assert(IsMarkupParser);
+ throw new NotSupportedException(RazorResources.ParserIsNotAMarkupParser);
+ }
+
+ public virtual void ParseSection(Tuple<string, string> nestingSequences, bool caseSensitive)
+ {
+ Debug.Assert(IsMarkupParser);
+ throw new NotSupportedException(RazorResources.ParserIsNotAMarkupParser);
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Parser/ParserContext.cs b/src/System.Web.Razor/Parser/ParserContext.cs
new file mode 100644
index 00000000..836e3537
--- /dev/null
+++ b/src/System.Web.Razor/Parser/ParserContext.cs
@@ -0,0 +1,309 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Text;
+using System.Web.Razor.Utils;
+
+namespace System.Web.Razor.Parser
+{
+ public partial class ParserContext
+ {
+ private int? _ownerTaskId;
+
+ private bool _terminated = false;
+
+ private Stack<BlockBuilder> _blockStack = new Stack<BlockBuilder>();
+
+ public ParserContext(ITextDocument source, ParserBase codeParser, ParserBase markupParser, ParserBase activeParser)
+ {
+ if (source == null)
+ {
+ throw new ArgumentNullException("source");
+ }
+ if (codeParser == null)
+ {
+ throw new ArgumentNullException("codeParser");
+ }
+ if (markupParser == null)
+ {
+ throw new ArgumentNullException("markupParser");
+ }
+ if (activeParser == null)
+ {
+ throw new ArgumentNullException("activeParser");
+ }
+ if (activeParser != codeParser && activeParser != markupParser)
+ {
+ throw new ArgumentException(RazorResources.ActiveParser_Must_Be_Code_Or_Markup_Parser, "activeParser");
+ }
+
+ CaptureOwnerTask();
+
+ Source = new TextDocumentReader(source);
+ CodeParser = codeParser;
+ MarkupParser = markupParser;
+ ActiveParser = activeParser;
+ Errors = new List<RazorError>();
+ }
+
+ public IList<RazorError> Errors { get; private set; }
+ public TextDocumentReader Source { get; set; }
+ public ParserBase CodeParser { get; private set; }
+ public ParserBase MarkupParser { get; private set; }
+ public ParserBase ActiveParser { get; private set; }
+ public bool DesignTimeMode { get; set; }
+
+ public BlockBuilder CurrentBlock
+ {
+ get { return _blockStack.Peek(); }
+ }
+
+ public Span LastSpan { get; private set; }
+ public bool WhiteSpaceIsSignificantToAncestorBlock { get; set; }
+
+ public AcceptedCharacters LastAcceptedCharacters
+ {
+ get
+ {
+ if (LastSpan == null)
+ {
+ return AcceptedCharacters.None;
+ }
+ return LastSpan.EditHandler.AcceptedCharacters;
+ }
+ }
+
+ internal Stack<BlockBuilder> BlockStack
+ {
+ get { return _blockStack; }
+ }
+
+ public char CurrentCharacter
+ {
+ get
+ {
+ if (_terminated)
+ {
+ return '\0';
+ }
+#if DEBUG
+ if (CheckInfiniteLoop())
+ {
+ return '\0';
+ }
+#endif
+ int ch = Source.Peek();
+ if (ch == -1)
+ {
+ return '\0';
+ }
+ return (char)ch;
+ }
+ }
+
+ public bool EndOfFile
+ {
+ get { return _terminated || Source.Peek() == -1; }
+ }
+
+ public void AddSpan(Span span)
+ {
+ EnusreNotTerminated();
+ if (_blockStack.Count == 0)
+ {
+ throw new InvalidOperationException(RazorResources.ParserContext_NoCurrentBlock);
+ }
+ _blockStack.Peek().Children.Add(span);
+
+ span.Previous = LastSpan;
+ if (LastSpan != null)
+ {
+ LastSpan.Next = span;
+ }
+
+ LastSpan = span;
+ }
+
+ /// <summary>
+ /// Starts a block of the specified type
+ /// </summary>
+ /// <param name="blockType">The type of the block to start</param>
+ public IDisposable StartBlock(BlockType blockType)
+ {
+ EnusreNotTerminated();
+ AssertOnOwnerTask();
+ _blockStack.Push(new BlockBuilder() { Type = blockType });
+ return new DisposableAction(EndBlock);
+ }
+
+ /// <summary>
+ /// Starts a block
+ /// </summary>
+ public IDisposable StartBlock()
+ {
+ EnusreNotTerminated();
+ AssertOnOwnerTask();
+ _blockStack.Push(new BlockBuilder());
+ return new DisposableAction(EndBlock);
+ }
+
+ /// <summary>
+ /// Ends the current block
+ /// </summary>
+ public void EndBlock()
+ {
+ EnusreNotTerminated();
+ AssertOnOwnerTask();
+
+ if (_blockStack.Count == 0)
+ {
+ throw new InvalidOperationException(RazorResources.EndBlock_Called_Without_Matching_StartBlock);
+ }
+ if (_blockStack.Count > 1)
+ {
+ BlockBuilder block = _blockStack.Pop();
+ _blockStack.Peek().Children.Add(block.Build());
+ }
+ else
+ {
+ // If we're at 1, terminate the parser
+ _terminated = true;
+ }
+ }
+
+ /// <summary>
+ /// Gets a boolean indicating if any of the ancestors of the current block is of the specified type
+ /// </summary>
+ public bool IsWithin(BlockType type)
+ {
+ return _blockStack.Any(b => b.Type == type);
+ }
+
+ public void SwitchActiveParser()
+ {
+ EnusreNotTerminated();
+ AssertOnOwnerTask();
+ if (ReferenceEquals(ActiveParser, CodeParser))
+ {
+ ActiveParser = MarkupParser;
+ }
+ else
+ {
+ ActiveParser = CodeParser;
+ }
+ }
+
+ public void OnError(SourceLocation location, string message)
+ {
+ EnusreNotTerminated();
+ AssertOnOwnerTask();
+ Errors.Add(new RazorError(message, location));
+ }
+
+ public void OnError(SourceLocation location, string message, params object[] args)
+ {
+ EnusreNotTerminated();
+ AssertOnOwnerTask();
+ OnError(location, String.Format(CultureInfo.CurrentCulture, message, args));
+ }
+
+ public ParserResults CompleteParse()
+ {
+ if (_blockStack.Count == 0)
+ {
+ throw new InvalidOperationException(RazorResources.ParserContext_CannotCompleteTree_NoRootBlock);
+ }
+ if (_blockStack.Count != 1)
+ {
+ throw new InvalidOperationException(RazorResources.ParserContext_CannotCompleteTree_OutstandingBlocks);
+ }
+ return new ParserResults(_blockStack.Pop().Build(), Errors);
+ }
+
+ [Conditional("DEBUG")]
+ internal void CaptureOwnerTask()
+ {
+ if (Task.CurrentId != null)
+ {
+ _ownerTaskId = Task.CurrentId;
+ }
+ }
+
+ [Conditional("DEBUG")]
+ internal void AssertOnOwnerTask()
+ {
+ if (_ownerTaskId != null)
+ {
+ Debug.Assert(_ownerTaskId == Task.CurrentId);
+ }
+ }
+
+ [Conditional("DEBUG")]
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "The method body is empty in Release builds")]
+ internal void AssertCurrent(char expected)
+ {
+ Debug.Assert(CurrentCharacter == expected);
+ }
+
+ private void EnusreNotTerminated()
+ {
+ if (_terminated)
+ {
+ throw new InvalidOperationException(RazorResources.ParserContext_ParseComplete);
+ }
+ }
+ }
+
+ // Debug Helpers
+
+#if DEBUG
+ [DebuggerDisplay("{Unparsed}")]
+ public partial class ParserContext
+ {
+ private const int InfiniteLoopCountThreshold = 1000;
+ private int _infiniteLoopGuardCount = 0;
+ private SourceLocation? _infiniteLoopGuardLocation = null;
+
+ internal string Unparsed
+ {
+ get
+ {
+ string remaining = Source.ReadToEnd();
+ Source.Position -= remaining.Length;
+ return remaining;
+ }
+ }
+
+ private bool CheckInfiniteLoop()
+ {
+ // Infinite loop guard
+ // Basically, if this property is accessed 1000 times in a row without having advanced the source reader to the next position, we
+ // cause a parser error
+ if (_infiniteLoopGuardLocation != null)
+ {
+ if (Source.Location == _infiniteLoopGuardLocation.Value)
+ {
+ _infiniteLoopGuardCount++;
+ if (_infiniteLoopGuardCount > InfiniteLoopCountThreshold)
+ {
+ Debug.Fail("An internal parser error is causing an infinite loop at this location.");
+ _terminated = true;
+ return true;
+ }
+ }
+ else
+ {
+ _infiniteLoopGuardCount = 0;
+ }
+ }
+ _infiniteLoopGuardLocation = Source.Location;
+ return false;
+ }
+ }
+#endif
+}
diff --git a/src/System.Web.Razor/Parser/ParserHelpers.cs b/src/System.Web.Razor/Parser/ParserHelpers.cs
new file mode 100644
index 00000000..5dbd3ae7
--- /dev/null
+++ b/src/System.Web.Razor/Parser/ParserHelpers.cs
@@ -0,0 +1,143 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+
+namespace System.Web.Razor.Parser
+{
+ public static class ParserHelpers
+ {
+ public static bool IsNewLine(char value)
+ {
+ return value == '\r' // Carriage return
+ || value == '\n' // Linefeed
+ || value == '\u0085' // Next Line
+ || value == '\u2028' // Line separator
+ || value == '\u2029'; // Paragraph separator
+ }
+
+ public static bool IsNewLine(string value)
+ {
+ return (value.Length == 1 && (IsNewLine(value[0]))) ||
+ (String.Equals(value, "\r\n", StringComparison.Ordinal));
+ }
+
+ // Returns true if the character is Whitespace and NOT a newline
+ [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Whitespace", Justification = "This would be a breaking change in a shipping API")]
+ public static bool IsWhitespace(char value)
+ {
+ return value == ' ' ||
+ value == '\f' ||
+ value == '\t' ||
+ value == '\u000B' || // Vertical Tab
+ Char.GetUnicodeCategory(value) == UnicodeCategory.SpaceSeparator;
+ }
+
+ [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Whitespace", Justification = "This would be a breaking change in a shipping API")]
+ public static bool IsWhitespaceOrNewLine(char value)
+ {
+ return IsWhitespace(value) || IsNewLine(value);
+ }
+
+ public static bool IsIdentifier(string value)
+ {
+ return IsIdentifier(value, requireIdentifierStart: true);
+ }
+
+ public static bool IsIdentifier(string value, bool requireIdentifierStart)
+ {
+ IEnumerable<char> identifierPart = value;
+ if (requireIdentifierStart)
+ {
+ identifierPart = identifierPart.Skip(1);
+ }
+ return (!requireIdentifierStart || IsIdentifierStart(value[0])) && identifierPart.All(IsIdentifierPart);
+ }
+
+ public static bool IsHexDigit(char value)
+ {
+ return (value >= '0' && value <= '9') || (value >= 'A' && value <= 'F') || (value >= 'a' && value <= 'f');
+ }
+
+ public static bool IsIdentifierStart(char value)
+ {
+ return value == '_' || IsLetter(value);
+ }
+
+ public static bool IsIdentifierPart(char value)
+ {
+ return IsLetter(value)
+ || IsDecimalDigit(value)
+ || IsConnecting(value)
+ || IsCombining(value)
+ || IsFormatting(value);
+ }
+
+ public static bool IsTerminatingCharToken(char value)
+ {
+ return IsNewLine(value) || value == '\'';
+ }
+
+ public static bool IsTerminatingQuotedStringToken(char value)
+ {
+ return IsNewLine(value) || value == '"';
+ }
+
+ public static bool IsDecimalDigit(char value)
+ {
+ return Char.GetUnicodeCategory(value) == UnicodeCategory.DecimalDigitNumber;
+ }
+
+ public static bool IsLetterOrDecimalDigit(char value)
+ {
+ return IsLetter(value) || IsDecimalDigit(value);
+ }
+
+ public static bool IsLetter(char value)
+ {
+ var cat = Char.GetUnicodeCategory(value);
+ return cat == UnicodeCategory.UppercaseLetter
+ || cat == UnicodeCategory.LowercaseLetter
+ || cat == UnicodeCategory.TitlecaseLetter
+ || cat == UnicodeCategory.ModifierLetter
+ || cat == UnicodeCategory.OtherLetter
+ || cat == UnicodeCategory.LetterNumber;
+ }
+
+ public static bool IsFormatting(char value)
+ {
+ return Char.GetUnicodeCategory(value) == UnicodeCategory.Format;
+ }
+
+ public static bool IsCombining(char value)
+ {
+ var cat = Char.GetUnicodeCategory(value);
+ return cat == UnicodeCategory.SpacingCombiningMark || cat == UnicodeCategory.NonSpacingMark;
+ }
+
+ public static bool IsConnecting(char value)
+ {
+ return Char.GetUnicodeCategory(value) == UnicodeCategory.ConnectorPunctuation;
+ }
+
+ public static string SanitizeClassName(string inputName)
+ {
+ if (!IsIdentifierStart(inputName[0]) && IsIdentifierPart(inputName[0]))
+ {
+ inputName = "_" + inputName;
+ }
+
+ return new String((from value in inputName
+ select IsIdentifierPart(value) ? value : '_')
+ .ToArray());
+ }
+
+ public static bool IsEmailPart(char character)
+ {
+ // Source: http://tools.ietf.org/html/rfc5322#section-3.4.1
+ // We restrict the allowed characters to alpha-numerics and '_' in order to ensure we cover most of the cases where an
+ // email address is intended without restricting the usage of code within JavaScript, CSS, and other contexts.
+ return Char.IsLetter(character) || Char.IsDigit(character) || character == '_';
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Parser/ParserVisitor.cs b/src/System.Web.Razor/Parser/ParserVisitor.cs
new file mode 100644
index 00000000..af0a487b
--- /dev/null
+++ b/src/System.Web.Razor/Parser/ParserVisitor.cs
@@ -0,0 +1,53 @@
+using System.Threading;
+using System.Web.Razor.Parser.SyntaxTree;
+
+namespace System.Web.Razor.Parser
+{
+ public abstract class ParserVisitor
+ {
+ public CancellationToken? CancelToken { get; set; }
+
+ public virtual void VisitBlock(Block block)
+ {
+ VisitStartBlock(block);
+ foreach (SyntaxTreeNode node in block.Children)
+ {
+ node.Accept(this);
+ }
+ VisitEndBlock(block);
+ }
+
+ public virtual void VisitStartBlock(Block block)
+ {
+ ThrowIfCanceled();
+ }
+
+ public virtual void VisitSpan(Span span)
+ {
+ ThrowIfCanceled();
+ }
+
+ public virtual void VisitEndBlock(Block block)
+ {
+ ThrowIfCanceled();
+ }
+
+ public virtual void VisitError(RazorError err)
+ {
+ ThrowIfCanceled();
+ }
+
+ public virtual void OnComplete()
+ {
+ ThrowIfCanceled();
+ }
+
+ public virtual void ThrowIfCanceled()
+ {
+ if (CancelToken != null && CancelToken.Value.IsCancellationRequested)
+ {
+ throw new OperationCanceledException();
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Parser/ParserVisitorExtensions.cs b/src/System.Web.Razor/Parser/ParserVisitorExtensions.cs
new file mode 100644
index 00000000..6c021870
--- /dev/null
+++ b/src/System.Web.Razor/Parser/ParserVisitorExtensions.cs
@@ -0,0 +1,26 @@
+using System.Web.Razor.Parser.SyntaxTree;
+
+namespace System.Web.Razor.Parser
+{
+ public static class ParserVisitorExtensions
+ {
+ public static void Visit(this ParserVisitor self, ParserResults result)
+ {
+ if (self == null)
+ {
+ throw new ArgumentNullException("self");
+ }
+ if (result == null)
+ {
+ throw new ArgumentNullException("result");
+ }
+
+ result.Document.Accept(self);
+ foreach (RazorError error in result.ParserErrors)
+ {
+ self.VisitError(error);
+ }
+ self.OnComplete();
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Parser/RazorParser.cs b/src/System.Web.Razor/Parser/RazorParser.cs
new file mode 100644
index 00000000..1ab81093
--- /dev/null
+++ b/src/System.Web.Razor/Parser/RazorParser.cs
@@ -0,0 +1,145 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Text;
+
+namespace System.Web.Razor.Parser
+{
+ public class RazorParser
+ {
+ public RazorParser(ParserBase codeParser, ParserBase markupParser)
+ {
+ if (codeParser == null)
+ {
+ throw new ArgumentNullException("codeParser");
+ }
+ if (markupParser == null)
+ {
+ throw new ArgumentNullException("markupParser");
+ }
+
+ MarkupParser = markupParser;
+ CodeParser = codeParser;
+ Optimizers = new List<ISyntaxTreeRewriter>()
+ {
+ // Move whitespace from start of expression block to markup
+ new WhiteSpaceRewriter(MarkupParser.BuildSpan),
+ // Collapse conditional attributes where the entire value is literal
+ new ConditionalAttributeCollapser(MarkupParser.BuildSpan),
+ // Collapse sibling Markup Spans
+ // TODO: Fix and restore Markup Collapser post-beta.
+ //new MarkupCollapser(MarkupParser.BuildSpan)
+ };
+ }
+
+ internal ParserBase CodeParser { get; private set; }
+ internal ParserBase MarkupParser { get; private set; }
+ internal IList<ISyntaxTreeRewriter> Optimizers { get; private set; }
+
+ public bool DesignTimeMode { get; set; }
+
+ public virtual void Parse(TextReader input, ParserVisitor visitor)
+ {
+ Parse(new SeekableTextReader(input), visitor);
+ }
+
+ public virtual ParserResults Parse(TextReader input)
+ {
+ return ParseCore(new SeekableTextReader(input));
+ }
+
+ public virtual ParserResults Parse(ITextDocument input)
+ {
+ return ParseCore(input);
+ }
+
+#pragma warning disable 0618
+ [Obsolete("Lookahead-based readers have been deprecated, use overrides which accept a TextReader or ITextDocument instead")]
+ public virtual void Parse(LookaheadTextReader input, ParserVisitor visitor)
+ {
+ ParserResults results = ParseCore(new SeekableTextReader(input));
+
+ // Replay the results on the visitor
+ visitor.Visit(results);
+ }
+
+ [Obsolete("Lookahead-based readers have been deprecated, use overrides which accept a TextReader or ITextDocument instead")]
+ public virtual ParserResults Parse(LookaheadTextReader input)
+ {
+ return ParseCore(new SeekableTextReader(input));
+ }
+#pragma warning restore 0618
+
+ public virtual Task CreateParseTask(TextReader input, Action<Span> spanCallback, Action<RazorError> errorCallback)
+ {
+ return CreateParseTask(input, new CallbackVisitor(spanCallback, errorCallback));
+ }
+
+ public virtual Task CreateParseTask(TextReader input, Action<Span> spanCallback, Action<RazorError> errorCallback, SynchronizationContext context)
+ {
+ return CreateParseTask(input, new CallbackVisitor(spanCallback, errorCallback) { SynchronizationContext = context });
+ }
+
+ public virtual Task CreateParseTask(TextReader input, Action<Span> spanCallback, Action<RazorError> errorCallback, CancellationToken cancelToken)
+ {
+ return CreateParseTask(input, new CallbackVisitor(spanCallback, errorCallback) { CancelToken = cancelToken });
+ }
+
+ public virtual Task CreateParseTask(TextReader input, Action<Span> spanCallback, Action<RazorError> errorCallback, SynchronizationContext context, CancellationToken cancelToken)
+ {
+ return CreateParseTask(input, new CallbackVisitor(spanCallback, errorCallback)
+ {
+ SynchronizationContext = context,
+ CancelToken = cancelToken
+ });
+ }
+
+ [SuppressMessage("Microsoft.WebAPI", "CR4002:DoNotConstructTaskInstances", Justification = "This rule is not applicable to this assembly.")]
+ public virtual Task CreateParseTask(TextReader input,
+ ParserVisitor consumer)
+ {
+ return new Task(() =>
+ {
+ try
+ {
+ Parse(input, consumer);
+ }
+ catch (OperationCanceledException)
+ {
+ return; // Just return if we're cancelled.
+ }
+ });
+ }
+
+ private ParserResults ParseCore(ITextDocument input)
+ {
+ // Setup the parser context
+ ParserContext context = new ParserContext(input, CodeParser, MarkupParser, MarkupParser)
+ {
+ DesignTimeMode = DesignTimeMode
+ };
+
+ MarkupParser.Context = context;
+ CodeParser.Context = context;
+
+ // Execute the parse
+ MarkupParser.ParseDocument();
+
+ // Get the result
+ ParserResults results = context.CompleteParse();
+
+ // Rewrite whitespace if supported
+ Block current = results.Document;
+ foreach (ISyntaxTreeRewriter rewriter in Optimizers)
+ {
+ current = rewriter.Rewrite(current);
+ }
+
+ // Return the new result
+ return new ParserResults(current, results.ParserErrors);
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Parser/SyntaxConstants.cs b/src/System.Web.Razor/Parser/SyntaxConstants.cs
new file mode 100644
index 00000000..573065da
--- /dev/null
+++ b/src/System.Web.Razor/Parser/SyntaxConstants.cs
@@ -0,0 +1,50 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Web.Razor.Parser
+{
+ public static class SyntaxConstants
+ {
+ public static readonly string TextTagName = "text";
+ public static readonly char TransitionCharacter = '@';
+ public static readonly string TransitionString = "@";
+ public static readonly string StartCommentSequence = "@*";
+ public static readonly string EndCommentSequence = "*@";
+
+ [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "Class is nested to provide better organization")]
+ [SuppressMessage("Microsoft.Naming", "CA1724:TypeNamesShouldNotMatchNamespaces", Justification = "This type name should not cause a conflict")]
+ public static class CSharp
+ {
+ public static readonly int UsingKeywordLength = 5;
+ public static readonly string InheritsKeyword = "inherits";
+ public static readonly string FunctionsKeyword = "functions";
+ public static readonly string SectionKeyword = "section";
+ public static readonly string HelperKeyword = "helper";
+ public static readonly string ElseIfKeyword = "else if";
+ public static readonly string NamespaceKeyword = "namespace";
+ public static readonly string ClassKeyword = "class";
+ public static readonly string LayoutKeyword = "layout";
+ public static readonly string SessionStateKeyword = "sessionstate";
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "Class is nested to provide better organization")]
+ public static class VB
+ {
+ public static readonly int ImportsKeywordLength = 7;
+ public static readonly string EndKeyword = "End";
+ public static readonly string CodeKeyword = "Code";
+ public static readonly string FunctionsKeyword = "Functions";
+ public static readonly string SectionKeyword = "Section";
+ public static readonly string StrictKeyword = "Strict";
+ public static readonly string ExplicitKeyword = "Explicit";
+ public static readonly string OffKeyword = "Off";
+ public static readonly string HelperKeyword = "Helper";
+ public static readonly string SelectCaseKeyword = "Select Case";
+ public static readonly string LayoutKeyword = "Layout";
+ public static readonly string EndCodeKeyword = "End Code";
+ public static readonly string EndHelperKeyword = "End Helper";
+ public static readonly string EndFunctionsKeyword = "End Functions";
+ public static readonly string EndSectionKeyword = "End Section";
+ public static readonly string SessionStateKeyword = "SessionState";
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Parser/SyntaxTree/AcceptedCharacters.cs b/src/System.Web.Razor/Parser/SyntaxTree/AcceptedCharacters.cs
new file mode 100644
index 00000000..7b231e15
--- /dev/null
+++ b/src/System.Web.Razor/Parser/SyntaxTree/AcceptedCharacters.cs
@@ -0,0 +1,21 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Web.Razor.Parser.SyntaxTree
+{
+ [Flags]
+ public enum AcceptedCharacters
+ {
+ None = 0,
+ NewLine = 1,
+ WhiteSpace = 2,
+
+ [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "NonWhite", Justification = "This is not a compound word, it is two words")]
+ NonWhiteSpace = 4,
+
+ AllWhiteSpace = NewLine | WhiteSpace,
+ Any = AllWhiteSpace | NonWhiteSpace,
+
+ [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Newline", Justification = "This would be a breaking change to a previous released API")]
+ AnyExceptNewline = NonWhiteSpace | WhiteSpace
+ }
+}
diff --git a/src/System.Web.Razor/Parser/SyntaxTree/Block.cs b/src/System.Web.Razor/Parser/SyntaxTree/Block.cs
new file mode 100644
index 00000000..c8073335
--- /dev/null
+++ b/src/System.Web.Razor/Parser/SyntaxTree/Block.cs
@@ -0,0 +1,198 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using System.Web.Razor.Generator;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Text;
+
+namespace System.Web.Razor.Parser.SyntaxTree
+{
+ public class Block : SyntaxTreeNode
+ {
+ public Block(BlockBuilder source)
+ {
+ if (source.Type == null)
+ {
+ throw new InvalidOperationException(RazorResources.Block_Type_Not_Specified);
+ }
+ Type = source.Type.Value;
+ Children = source.Children;
+ Name = source.Name;
+ CodeGenerator = source.CodeGenerator;
+ source.Reset();
+
+ foreach (SyntaxTreeNode node in Children)
+ {
+ node.Parent = this;
+ }
+ }
+
+ internal Block(BlockType type, IEnumerable<SyntaxTreeNode> contents, IBlockCodeGenerator generator)
+ {
+ Type = type;
+ CodeGenerator = generator;
+ Children = contents;
+ }
+
+ [SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods", Justification = "Type is the most appropriate name for this property and there is little chance of confusion with GetType")]
+ public BlockType Type { get; private set; }
+
+ public IEnumerable<SyntaxTreeNode> Children { get; private set; }
+ public string Name { get; private set; }
+ public IBlockCodeGenerator CodeGenerator { get; private set; }
+
+ public override bool IsBlock
+ {
+ get { return true; }
+ }
+
+ public override SourceLocation Start
+ {
+ get
+ {
+ SyntaxTreeNode child = Children.FirstOrDefault();
+ if (child == null)
+ {
+ return SourceLocation.Zero;
+ }
+ else
+ {
+ return child.Start;
+ }
+ }
+ }
+
+ public override int Length
+ {
+ get { return Children.Sum(child => child.Length); }
+ }
+
+ public Span FindFirstDescendentSpan()
+ {
+ SyntaxTreeNode current = this;
+ while (current != null && current.IsBlock)
+ {
+ current = ((Block)current).Children.FirstOrDefault();
+ }
+ return current as Span;
+ }
+
+ public Span FindLastDescendentSpan()
+ {
+ SyntaxTreeNode current = this;
+ while (current != null && current.IsBlock)
+ {
+ current = ((Block)current).Children.LastOrDefault();
+ }
+ return current as Span;
+ }
+
+ public override void Accept(ParserVisitor visitor)
+ {
+ visitor.VisitBlock(this);
+ }
+
+ public override string ToString()
+ {
+ return String.Format(CultureInfo.CurrentCulture, "{0} Block at {1}::{2} (Gen:{3})", Type, Start, Length, CodeGenerator);
+ }
+
+ public override bool Equals(object obj)
+ {
+ Block other = obj as Block;
+ return other != null &&
+ Type == other.Type &&
+ Equals(CodeGenerator, other.CodeGenerator) &&
+ ChildrenEqual(Children, other.Children);
+ }
+
+ public override int GetHashCode()
+ {
+ return (int)Type;
+ }
+
+ public IEnumerable<Span> Flatten()
+ {
+ // Create an enumerable that flattens the tree for use by syntax highlighters, etc.
+ foreach (SyntaxTreeNode element in Children)
+ {
+ Span span = element as Span;
+ if (span != null)
+ {
+ yield return span;
+ }
+ else
+ {
+ Block block = element as Block;
+ foreach (Span childSpan in block.Flatten())
+ {
+ yield return childSpan;
+ }
+ }
+ }
+ }
+
+ public Span LocateOwner(TextChange change)
+ {
+ // Ask each child recursively
+ Span owner = null;
+ foreach (SyntaxTreeNode element in Children)
+ {
+ Span span = element as Span;
+ if (span == null)
+ {
+ owner = ((Block)element).LocateOwner(change);
+ }
+ else
+ {
+ if (change.OldPosition < span.Start.AbsoluteIndex)
+ {
+ // Early escape for cases where changes overlap multiple spans
+ // In those cases, the span will return false, and we don't want to search the whole tree
+ // So if the current span starts after the change, we know we've searched as far as we need to
+ break;
+ }
+ owner = span.EditHandler.OwnsChange(span, change) ? span : owner;
+ }
+
+ if (owner != null)
+ {
+ break;
+ }
+ }
+ return owner;
+ }
+
+ private static bool ChildrenEqual(IEnumerable<SyntaxTreeNode> left, IEnumerable<SyntaxTreeNode> right)
+ {
+ IEnumerator<SyntaxTreeNode> leftEnum = left.GetEnumerator();
+ IEnumerator<SyntaxTreeNode> rightEnum = right.GetEnumerator();
+ while (leftEnum.MoveNext())
+ {
+ if (!rightEnum.MoveNext() || // More items in left than in right
+ !Equals(leftEnum.Current, rightEnum.Current))
+ {
+ // Nodes are not equal
+ return false;
+ }
+ }
+ if (rightEnum.MoveNext())
+ {
+ // More items in right than left
+ return false;
+ }
+ return true;
+ }
+
+ public override bool EquivalentTo(SyntaxTreeNode node)
+ {
+ Block other = node as Block;
+ if (other == null || other.Type != Type)
+ {
+ return false;
+ }
+ return Enumerable.SequenceEqual(Children, other.Children, new EquivalenceComparer());
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Parser/SyntaxTree/BlockBuilder.cs b/src/System.Web.Razor/Parser/SyntaxTree/BlockBuilder.cs
new file mode 100644
index 00000000..329f0b08
--- /dev/null
+++ b/src/System.Web.Razor/Parser/SyntaxTree/BlockBuilder.cs
@@ -0,0 +1,42 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Web.Razor.Generator;
+
+namespace System.Web.Razor.Parser.SyntaxTree
+{
+ public class BlockBuilder
+ {
+ public BlockBuilder()
+ {
+ Reset();
+ }
+
+ public BlockBuilder(Block original)
+ {
+ Type = original.Type;
+ Children = new List<SyntaxTreeNode>(original.Children);
+ Name = original.Name;
+ CodeGenerator = original.CodeGenerator;
+ }
+
+ [SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods", Justification = "Type is the most appropriate name for this property and there is little chance of confusion with GetType")]
+ public BlockType? Type { get; set; }
+
+ public IList<SyntaxTreeNode> Children { get; private set; }
+ public string Name { get; set; }
+ public IBlockCodeGenerator CodeGenerator { get; set; }
+
+ public Block Build()
+ {
+ return new Block(this);
+ }
+
+ public void Reset()
+ {
+ Type = null;
+ Name = null;
+ Children = new List<SyntaxTreeNode>();
+ CodeGenerator = BlockCodeGenerator.Null;
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Parser/SyntaxTree/BlockType.cs b/src/System.Web.Razor/Parser/SyntaxTree/BlockType.cs
new file mode 100644
index 00000000..3c31cf95
--- /dev/null
+++ b/src/System.Web.Razor/Parser/SyntaxTree/BlockType.cs
@@ -0,0 +1,20 @@
+namespace System.Web.Razor.Parser.SyntaxTree
+{
+ public enum BlockType
+ {
+ // Code
+ Statement,
+ Directive,
+ Functions,
+ Expression,
+ Helper,
+
+ // Markup
+ Markup,
+ Section,
+ Template,
+
+ // Special
+ Comment
+ }
+}
diff --git a/src/System.Web.Razor/Parser/SyntaxTree/EquivalenceComparer.cs b/src/System.Web.Razor/Parser/SyntaxTree/EquivalenceComparer.cs
new file mode 100644
index 00000000..d52e263e
--- /dev/null
+++ b/src/System.Web.Razor/Parser/SyntaxTree/EquivalenceComparer.cs
@@ -0,0 +1,17 @@
+using System.Collections.Generic;
+
+namespace System.Web.Razor.Parser.SyntaxTree
+{
+ internal class EquivalenceComparer : IEqualityComparer<SyntaxTreeNode>
+ {
+ public bool Equals(SyntaxTreeNode x, SyntaxTreeNode y)
+ {
+ return x.EquivalentTo(y);
+ }
+
+ public int GetHashCode(SyntaxTreeNode obj)
+ {
+ return obj.GetHashCode();
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Parser/SyntaxTree/RazorError.cs b/src/System.Web.Razor/Parser/SyntaxTree/RazorError.cs
new file mode 100644
index 00000000..a334dda8
--- /dev/null
+++ b/src/System.Web.Razor/Parser/SyntaxTree/RazorError.cs
@@ -0,0 +1,56 @@
+using System.Globalization;
+using System.Web.Razor.Text;
+
+namespace System.Web.Razor.Parser.SyntaxTree
+{
+ public class RazorError : IEquatable<RazorError>
+ {
+ public RazorError(string message, SourceLocation location)
+ : this(message, location, 1)
+ {
+ }
+
+ public RazorError(string message, int absoluteIndex, int lineIndex, int columnIndex)
+ : this(message, new SourceLocation(absoluteIndex, lineIndex, columnIndex))
+ {
+ }
+
+ public RazorError(string message, SourceLocation location, int length)
+ {
+ Message = message;
+ Location = location;
+ Length = length;
+ }
+
+ public RazorError(string message, int absoluteIndex, int lineIndex, int columnIndex, int length)
+ : this(message, new SourceLocation(absoluteIndex, lineIndex, columnIndex), length)
+ {
+ }
+
+ public string Message { get; private set; }
+ public SourceLocation Location { get; private set; }
+ public int Length { get; private set; }
+
+ public override string ToString()
+ {
+ return String.Format(CultureInfo.CurrentCulture, "Error @ {0}({2}) - [{1}]", Location, Message, Length);
+ }
+
+ public override bool Equals(object obj)
+ {
+ RazorError err = obj as RazorError;
+ return (err != null) && (Equals(err));
+ }
+
+ public override int GetHashCode()
+ {
+ return base.GetHashCode();
+ }
+
+ public bool Equals(RazorError other)
+ {
+ return String.Equals(other.Message, Message, StringComparison.Ordinal) &&
+ Location.Equals(other.Location);
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Parser/SyntaxTree/Span.cs b/src/System.Web.Razor/Parser/SyntaxTree/Span.cs
new file mode 100644
index 00000000..972b3a20
--- /dev/null
+++ b/src/System.Web.Razor/Parser/SyntaxTree/Span.cs
@@ -0,0 +1,150 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+using System.Web.Razor.Editor;
+using System.Web.Razor.Generator;
+using System.Web.Razor.Text;
+using System.Web.Razor.Tokenizer.Symbols;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.Razor.Parser.SyntaxTree
+{
+ public partial class Span : SyntaxTreeNode
+ {
+ private SourceLocation _start;
+
+ public Span(SpanBuilder builder)
+ {
+ ReplaceWith(builder);
+ }
+
+ public SpanKind Kind { get; protected set; }
+ public IEnumerable<ISymbol> Symbols { get; protected set; }
+
+ // Allow test code to re-link spans
+ public Span Previous { get; protected internal set; }
+ public Span Next { get; protected internal set; }
+
+ public SpanEditHandler EditHandler { get; protected set; }
+ public ISpanCodeGenerator CodeGenerator { get; protected set; }
+
+ public override bool IsBlock
+ {
+ get { return false; }
+ }
+
+ public override int Length
+ {
+ get { return Content.Length; }
+ }
+
+ public override SourceLocation Start
+ {
+ get { return _start; }
+ }
+
+ public string Content { get; private set; }
+
+ public void Change(Action<SpanBuilder> changes)
+ {
+ SpanBuilder builder = new SpanBuilder(this);
+ changes(builder);
+ ReplaceWith(builder);
+ }
+
+ public void ReplaceWith(SpanBuilder builder)
+ {
+ Debug.Assert(!builder.Symbols.Any() || builder.Symbols.All(s => s != null));
+
+ Kind = builder.Kind;
+ Symbols = builder.Symbols;
+ EditHandler = builder.EditHandler;
+ CodeGenerator = builder.CodeGenerator ?? SpanCodeGenerator.Null;
+ _start = builder.Start;
+
+ // Since we took references to the values in SpanBuilder, clear its references out
+ builder.Reset();
+
+ // Calculate other properties
+ Content = Symbols.Aggregate(new StringBuilder(), (sb, sym) => sb.Append(sym.Content), sb => sb.ToString());
+ }
+
+ /// <summary>
+ /// Accepts the specified visitor
+ /// </summary>
+ /// <remarks>
+ /// Calls the VisitSpan method on the specified visitor, passing in this
+ /// </remarks>
+ public override void Accept(ParserVisitor visitor)
+ {
+ visitor.VisitSpan(this);
+ }
+
+ public override string ToString()
+ {
+ StringBuilder builder = new StringBuilder();
+ builder.Append(Kind);
+ builder.AppendFormat(" Span at {0}::{1} - [{2}]", Start, Length, Content);
+ builder.Append(" Edit: <");
+ builder.Append(EditHandler.ToString());
+ builder.Append(">");
+ builder.Append(" Gen: <");
+ builder.Append(CodeGenerator.ToString());
+ builder.Append("> {");
+ builder.Append(String.Join(";", Symbols.GroupBy(sym => sym.GetType()).Select(grp => String.Concat(grp.Key.Name, ":", grp.Count()))));
+ builder.Append("}");
+ return builder.ToString();
+ }
+
+ public void ChangeStart(SourceLocation newStart)
+ {
+ _start = newStart;
+ Span current = this;
+ SourceLocationTracker tracker = new SourceLocationTracker(newStart);
+ tracker.UpdateLocation(Content);
+ while ((current = current.Next) != null)
+ {
+ current._start = tracker.CurrentLocation;
+ tracker.UpdateLocation(current.Content);
+ }
+ }
+
+ internal void SetStart(SourceLocation newStart)
+ {
+ _start = newStart;
+ }
+
+ /// <summary>
+ /// Checks that the specified span is equivalent to the other in that it has the same start point and content.
+ /// </summary>
+ public override bool EquivalentTo(SyntaxTreeNode node)
+ {
+ Span other = node as Span;
+ return other != null &&
+ Kind.Equals(other.Kind) &&
+ Start.Equals(other.Start) &&
+ EditHandler.Equals(other.EditHandler) &&
+ String.Equals(other.Content, Content, StringComparison.Ordinal);
+ }
+
+ public override bool Equals(object obj)
+ {
+ Span other = obj as Span;
+ return other != null &&
+ Kind.Equals(other.Kind) &&
+ EditHandler.Equals(other.EditHandler) &&
+ CodeGenerator.Equals(other.CodeGenerator) &&
+ Symbols.SequenceEqual(other.Symbols);
+ }
+
+ public override int GetHashCode()
+ {
+ return HashCodeCombiner.Start()
+ .Add((int)Kind)
+ .Add(Start)
+ .Add(Content)
+ .CombinedHash;
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Parser/SyntaxTree/SpanBuilder.cs b/src/System.Web.Razor/Parser/SyntaxTree/SpanBuilder.cs
new file mode 100644
index 00000000..bf947eb9
--- /dev/null
+++ b/src/System.Web.Razor/Parser/SyntaxTree/SpanBuilder.cs
@@ -0,0 +1,82 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Web.Razor.Editor;
+using System.Web.Razor.Generator;
+using System.Web.Razor.Text;
+using System.Web.Razor.Tokenizer.Symbols;
+
+namespace System.Web.Razor.Parser.SyntaxTree
+{
+ public class SpanBuilder
+ {
+ private IList<ISymbol> _symbols = new List<ISymbol>();
+ private SourceLocationTracker _tracker = new SourceLocationTracker();
+
+ public SpanBuilder(Span original)
+ {
+ Kind = original.Kind;
+ _symbols = new List<ISymbol>(original.Symbols);
+ EditHandler = original.EditHandler;
+ CodeGenerator = original.CodeGenerator;
+ Start = original.Start;
+ }
+
+ public SpanBuilder()
+ {
+ Reset();
+ }
+
+ public SourceLocation Start { get; set; }
+ public SpanKind Kind { get; set; }
+
+ public ReadOnlyCollection<ISymbol> Symbols
+ {
+ get { return new ReadOnlyCollection<ISymbol>(_symbols); }
+ }
+
+ public SpanEditHandler EditHandler { get; set; }
+ public ISpanCodeGenerator CodeGenerator { get; set; }
+
+ public void Reset()
+ {
+ _symbols = new List<ISymbol>();
+ EditHandler = SpanEditHandler.CreateDefault(s => Enumerable.Empty<ISymbol>());
+ CodeGenerator = SpanCodeGenerator.Null;
+ Start = SourceLocation.Zero;
+ }
+
+ public Span Build()
+ {
+ return new Span(this);
+ }
+
+ public void ClearSymbols()
+ {
+ _symbols.Clear();
+ }
+
+ // Short-cut method for adding a symbol
+ public void Accept(ISymbol symbol)
+ {
+ if (symbol == null)
+ {
+ return;
+ }
+
+ if (_symbols.Count == 0)
+ {
+ Start = symbol.Start;
+ symbol.ChangeStart(SourceLocation.Zero);
+ _tracker.CurrentLocation = SourceLocation.Zero;
+ }
+ else
+ {
+ symbol.ChangeStart(_tracker.CurrentLocation);
+ }
+
+ _symbols.Add(symbol);
+ _tracker.UpdateLocation(symbol.Content);
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Parser/SyntaxTree/SpanKind.cs b/src/System.Web.Razor/Parser/SyntaxTree/SpanKind.cs
new file mode 100644
index 00000000..935a2364
--- /dev/null
+++ b/src/System.Web.Razor/Parser/SyntaxTree/SpanKind.cs
@@ -0,0 +1,11 @@
+namespace System.Web.Razor.Parser.SyntaxTree
+{
+ public enum SpanKind
+ {
+ Transition,
+ MetaCode,
+ Comment,
+ Code,
+ Markup
+ }
+}
diff --git a/src/System.Web.Razor/Parser/SyntaxTree/SyntaxTreeNode.cs b/src/System.Web.Razor/Parser/SyntaxTree/SyntaxTreeNode.cs
new file mode 100644
index 00000000..5277af39
--- /dev/null
+++ b/src/System.Web.Razor/Parser/SyntaxTree/SyntaxTreeNode.cs
@@ -0,0 +1,39 @@
+using System.Web.Razor.Text;
+
+namespace System.Web.Razor.Parser.SyntaxTree
+{
+ public abstract class SyntaxTreeNode
+ {
+ public Block Parent { get; internal set; }
+
+ /// <summary>
+ /// Returns true if this element is a block (to avoid casting)
+ /// </summary>
+ public abstract bool IsBlock { get; }
+
+ /// <summary>
+ /// The length of all the content contained in this node
+ /// </summary>
+ public abstract int Length { get; }
+
+ /// <summary>
+ /// The start point of this node
+ /// </summary>
+ public abstract SourceLocation Start { get; }
+
+ /// <summary>
+ /// Accepts a parser visitor, calling the appropriate visit method and passing in this instance
+ /// </summary>
+ /// <param name="visitor">The visitor to accept</param>
+ public abstract void Accept(ParserVisitor visitor);
+
+ /// <summary>
+ /// Determines if the specified node is equivalent to this node
+ /// </summary>
+ /// <param name="node">The node to compare this node with</param>
+ /// <returns>
+ /// true if the provided node has all the same content and metadata, though the specific quantity and type of symbols may be different.
+ /// </returns>
+ public abstract bool EquivalentTo(SyntaxTreeNode node);
+ }
+}
diff --git a/src/System.Web.Razor/Parser/TextReaderExtensions.cs b/src/System.Web.Razor/Parser/TextReaderExtensions.cs
new file mode 100644
index 00000000..acd2d7f6
--- /dev/null
+++ b/src/System.Web.Razor/Parser/TextReaderExtensions.cs
@@ -0,0 +1,106 @@
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace System.Web.Razor.Parser
+{
+ internal static class TextReaderExtensions
+ {
+ public static string ReadUntil(this TextReader reader, char terminator)
+ {
+ return ReadUntil(reader, terminator, inclusive: false);
+ }
+
+ public static string ReadUntil(this TextReader reader, char terminator, bool inclusive)
+ {
+ if (reader == null)
+ {
+ throw new ArgumentNullException("reader");
+ }
+
+ // Rather not allocate an array to use ReadUntil(TextReader, params char[]) so we'll just call the predicate version directly
+ return reader.ReadUntil(c => c == terminator, inclusive);
+ }
+
+ public static string ReadUntil(this TextReader reader, params char[] terminators)
+ {
+ // NOTE: Using named parameters would be difficult here, hence the inline comment
+ return reader.ReadUntil(inclusive: false, terminators: terminators);
+ }
+
+ public static string ReadUntil(this TextReader reader, bool inclusive, params char[] terminators)
+ {
+ if (reader == null)
+ {
+ throw new ArgumentNullException("reader");
+ }
+ if (terminators == null)
+ {
+ throw new ArgumentNullException("terminators");
+ }
+
+ return reader.ReadUntil(c => terminators.Any(tc => tc == c), inclusive: inclusive);
+ }
+
+ public static string ReadUntil(this TextReader reader, Predicate<char> condition)
+ {
+ return reader.ReadUntil(condition, inclusive: false);
+ }
+
+ public static string ReadUntil(this TextReader reader, Predicate<char> condition, bool inclusive)
+ {
+ if (reader == null)
+ {
+ throw new ArgumentNullException("reader");
+ }
+ if (condition == null)
+ {
+ throw new ArgumentNullException("condition");
+ }
+
+ StringBuilder builder = new StringBuilder();
+ int ch = -1;
+ while ((ch = reader.Peek()) != -1 && !condition((char)ch))
+ {
+ reader.Read(); // Advance the reader
+ builder.Append((char)ch);
+ }
+
+ if (inclusive && reader.Peek() != -1)
+ {
+ builder.Append((char)reader.Read());
+ }
+
+ return builder.ToString();
+ }
+
+ public static string ReadWhile(this TextReader reader, Predicate<char> condition)
+ {
+ return reader.ReadWhile(condition, inclusive: false);
+ }
+
+ public static string ReadWhile(this TextReader reader, Predicate<char> condition, bool inclusive)
+ {
+ if (reader == null)
+ {
+ throw new ArgumentNullException("reader");
+ }
+ if (condition == null)
+ {
+ throw new ArgumentNullException("condition");
+ }
+
+ return reader.ReadUntil(ch => !condition(ch), inclusive);
+ }
+
+ public static string ReadWhiteSpace(this TextReader reader)
+ {
+ return reader.ReadWhile(c => Char.IsWhiteSpace(c));
+ }
+
+ public static string ReadUntilWhiteSpace(this TextReader reader)
+ {
+ return reader.ReadUntil(c => Char.IsWhiteSpace(c));
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Parser/TokenizerBackedParser.Helpers.cs b/src/System.Web.Razor/Parser/TokenizerBackedParser.Helpers.cs
new file mode 100644
index 00000000..44c451f8
--- /dev/null
+++ b/src/System.Web.Razor/Parser/TokenizerBackedParser.Helpers.cs
@@ -0,0 +1,547 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using System.Web.Razor.Editor;
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Text;
+using System.Web.Razor.Tokenizer;
+using System.Web.Razor.Tokenizer.Symbols;
+using System.Web.Razor.Utils;
+
+namespace System.Web.Razor.Parser
+{
+ public abstract partial class TokenizerBackedParser<TTokenizer, TSymbol, TSymbolType> : ParserBase
+ where TTokenizer : Tokenizer<TSymbol, TSymbolType>
+ where TSymbol : SymbolBase<TSymbolType>
+ {
+ // Helpers
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This only occurs in Release builds, where this method is empty by design")]
+ [Conditional("DEBUG")]
+ internal void Assert(TSymbolType expectedType)
+ {
+ Debug.Assert(!EndOfFile && Equals(CurrentSymbol.Type, expectedType));
+ }
+
+ protected internal void PutBack(TSymbol symbol)
+ {
+ if (symbol != null)
+ {
+ Tokenizer.PutBack(symbol);
+ }
+ }
+
+ /// <summary>
+ /// Put the specified symbols back in the input stream. The provided list MUST be in the ORDER THE SYMBOLS WERE READ. The
+ /// list WILL be reversed and the Putback(TSymbol) will be called on each item.
+ /// </summary>
+ /// <remarks>
+ /// If a document contains symbols: a, b, c, d, e, f
+ /// and AcceptWhile or AcceptUntil is used to collect until d
+ /// the list returned by AcceptWhile/Until will contain: a, b, c IN THAT ORDER
+ /// that is the correct format for providing to this method. The caller of this method would,
+ /// in that case, want to put c, b and a back into the stream, so "a, b, c" is the CORRECT order
+ /// </remarks>
+ protected internal void PutBack(IEnumerable<TSymbol> symbols)
+ {
+ foreach (TSymbol symbol in symbols.Reverse())
+ {
+ PutBack(symbol);
+ }
+ }
+
+ protected internal void PutCurrentBack()
+ {
+ if (!EndOfFile && CurrentSymbol != null)
+ {
+ PutBack(CurrentSymbol);
+ }
+ }
+
+ protected internal bool Balance(BalancingModes mode)
+ {
+ TSymbolType left = CurrentSymbol.Type;
+ TSymbolType right = Language.FlipBracket(left);
+ SourceLocation start = CurrentLocation;
+ AcceptAndMoveNext();
+ if (EndOfFile && !mode.HasFlag(BalancingModes.NoErrorOnFailure))
+ {
+ Context.OnError(start,
+ RazorResources.ParseError_Expected_CloseBracket_Before_EOF,
+ Language.GetSample(left),
+ Language.GetSample(right));
+ }
+
+ return Balance(mode, left, right, start);
+ }
+
+ protected internal bool Balance(BalancingModes mode, TSymbolType left, TSymbolType right, SourceLocation start)
+ {
+ int startPosition = CurrentLocation.AbsoluteIndex;
+ int nesting = 1;
+ if (!EndOfFile)
+ {
+ IList<TSymbol> syms = new List<TSymbol>();
+ do
+ {
+ if (IsAtEmbeddedTransition(
+ mode.HasFlag(BalancingModes.AllowCommentsAndTemplates),
+ mode.HasFlag(BalancingModes.AllowEmbeddedTransitions)))
+ {
+ Accept(syms);
+ syms.Clear();
+ HandleEmbeddedTransition();
+
+ // Reset backtracking since we've already outputted some spans.
+ startPosition = CurrentLocation.AbsoluteIndex;
+ }
+ if (At(left))
+ {
+ nesting++;
+ }
+ else if (At(right))
+ {
+ nesting--;
+ }
+ if (nesting > 0)
+ {
+ syms.Add(CurrentSymbol);
+ }
+ }
+ while (nesting > 0 && NextToken());
+
+ if (nesting > 0)
+ {
+ if (!mode.HasFlag(BalancingModes.NoErrorOnFailure))
+ {
+ Context.OnError(start,
+ RazorResources.ParseError_Expected_CloseBracket_Before_EOF,
+ Language.GetSample(left),
+ Language.GetSample(right));
+ }
+ if (mode.HasFlag(BalancingModes.BacktrackOnFailure))
+ {
+ Context.Source.Position = startPosition;
+ NextToken();
+ }
+ else
+ {
+ Accept(syms);
+ }
+ }
+ else
+ {
+ // Accept all the symbols we saw
+ Accept(syms);
+ }
+ }
+ return nesting == 0;
+ }
+
+ protected internal bool NextIs(TSymbolType type)
+ {
+ return NextIs(sym => sym != null && Equals(type, sym.Type));
+ }
+
+ protected internal bool NextIs(params TSymbolType[] types)
+ {
+ return NextIs(sym => sym != null && types.Any(t => Equals(t, sym.Type)));
+ }
+
+ protected internal bool NextIs(Func<TSymbol, bool> condition)
+ {
+ TSymbol cur = CurrentSymbol;
+ NextToken();
+ bool result = condition(CurrentSymbol);
+ PutCurrentBack();
+ PutBack(cur);
+ EnsureCurrent();
+ return result;
+ }
+
+ protected internal bool Was(TSymbolType type)
+ {
+ return PreviousSymbol != null && Equals(PreviousSymbol.Type, type);
+ }
+
+ protected internal bool At(TSymbolType type)
+ {
+ return !EndOfFile && CurrentSymbol != null && Equals(CurrentSymbol.Type, type);
+ }
+
+ protected internal bool AcceptAndMoveNext()
+ {
+ Accept(CurrentSymbol);
+ return NextToken();
+ }
+
+ protected TSymbol AcceptSingleWhiteSpaceCharacter()
+ {
+ if (Language.IsWhiteSpace(CurrentSymbol))
+ {
+ Tuple<TSymbol, TSymbol> pair = Language.SplitSymbol(CurrentSymbol, 1, Language.GetKnownSymbolType(KnownSymbolType.WhiteSpace));
+ Accept(pair.Item1);
+ Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
+ NextToken();
+ return pair.Item2;
+ }
+ return null;
+ }
+
+ protected internal void Accept(IEnumerable<TSymbol> symbols)
+ {
+ foreach (TSymbol symbol in symbols)
+ {
+ Accept(symbol);
+ }
+ }
+
+ protected internal void Accept(TSymbol symbol)
+ {
+ if (symbol != null)
+ {
+ foreach (RazorError error in symbol.Errors)
+ {
+ Context.Errors.Add(error);
+ }
+ Span.Accept(symbol);
+ }
+ }
+
+ protected internal bool AcceptAll(params TSymbolType[] types)
+ {
+ foreach (TSymbolType type in types)
+ {
+ if (CurrentSymbol == null || !Equals(CurrentSymbol.Type, type))
+ {
+ return false;
+ }
+ AcceptAndMoveNext();
+ }
+ return true;
+ }
+
+ protected internal void AddMarkerSymbolIfNecessary()
+ {
+ AddMarkerSymbolIfNecessary(CurrentLocation);
+ }
+
+ protected internal void AddMarkerSymbolIfNecessary(SourceLocation location)
+ {
+ if (Span.Symbols.Count == 0 && Context.LastAcceptedCharacters != AcceptedCharacters.Any)
+ {
+ Accept(Language.CreateMarkerSymbol(location));
+ }
+ }
+
+ protected internal void Output(SpanKind kind)
+ {
+ Configure(kind, null);
+ Output();
+ }
+
+ protected internal void Output(SpanKind kind, AcceptedCharacters accepts)
+ {
+ Configure(kind, accepts);
+ Output();
+ }
+
+ protected internal void Output(AcceptedCharacters accepts)
+ {
+ Configure(null, accepts);
+ Output();
+ }
+
+ private void Output()
+ {
+ if (Span.Symbols.Count > 0)
+ {
+ Context.AddSpan(Span.Build());
+ Initialize(Span);
+ }
+ }
+
+ protected IDisposable PushSpanConfig()
+ {
+ return PushSpanConfig(newConfig: (Action<SpanBuilder, Action<SpanBuilder>>)null);
+ }
+
+ protected IDisposable PushSpanConfig(Action<SpanBuilder> newConfig)
+ {
+ return PushSpanConfig(newConfig == null ? (Action<SpanBuilder, Action<SpanBuilder>>)null : (span, _) => newConfig(span));
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "The Action<T> parameters are preferred over custom delegates")]
+ protected IDisposable PushSpanConfig(Action<SpanBuilder, Action<SpanBuilder>> newConfig)
+ {
+ Action<SpanBuilder> old = SpanConfig;
+ ConfigureSpan(newConfig);
+ return new DisposableAction(() => SpanConfig = old);
+ }
+
+ protected void ConfigureSpan(Action<SpanBuilder> config)
+ {
+ SpanConfig = config;
+ Initialize(Span);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "The Action<T> parameters are preferred over custom delegates")]
+ protected void ConfigureSpan(Action<SpanBuilder, Action<SpanBuilder>> config)
+ {
+ Action<SpanBuilder> prev = SpanConfig;
+ if (config == null)
+ {
+ SpanConfig = null;
+ }
+ else
+ {
+ SpanConfig = span => config(span, prev);
+ }
+ Initialize(Span);
+ }
+
+ protected internal void Expected(KnownSymbolType type)
+ {
+ Expected(Language.GetKnownSymbolType(type));
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "types", Justification = "It is used in debug builds")]
+ protected internal void Expected(params TSymbolType[] types)
+ {
+ Debug.Assert(!EndOfFile && CurrentSymbol != null && types.Contains(CurrentSymbol.Type));
+ AcceptAndMoveNext();
+ }
+
+ protected internal bool Optional(KnownSymbolType type)
+ {
+ return Optional(Language.GetKnownSymbolType(type));
+ }
+
+ protected internal bool Optional(TSymbolType type)
+ {
+ if (At(type))
+ {
+ AcceptAndMoveNext();
+ return true;
+ }
+ return false;
+ }
+
+ protected internal bool Required(TSymbolType expected, bool errorIfNotFound, string errorBase)
+ {
+ bool found = At(expected);
+ if (!found && errorIfNotFound)
+ {
+ string error;
+ if (Language.IsNewLine(CurrentSymbol))
+ {
+ error = RazorResources.ErrorComponent_Newline;
+ }
+ else if (Language.IsWhiteSpace(CurrentSymbol))
+ {
+ error = RazorResources.ErrorComponent_Whitespace;
+ }
+ else if (EndOfFile)
+ {
+ error = RazorResources.ErrorComponent_EndOfFile;
+ }
+ else
+ {
+ error = String.Format(CultureInfo.CurrentCulture, RazorResources.ErrorComponent_Character, CurrentSymbol.Content);
+ }
+
+ Context.OnError(
+ CurrentLocation,
+ errorBase,
+ error);
+ }
+ return found;
+ }
+
+ protected bool EnsureCurrent()
+ {
+ if (CurrentSymbol == null)
+ {
+ return NextToken();
+ }
+ return true;
+ }
+
+ protected internal void AcceptWhile(TSymbolType type)
+ {
+ AcceptWhile(sym => Equals(type, sym.Type));
+ }
+
+ // We want to avoid array allocations and enumeration where possible, so we use the same technique as String.Format
+ protected internal void AcceptWhile(TSymbolType type1, TSymbolType type2)
+ {
+ AcceptWhile(sym => Equals(type1, sym.Type) || Equals(type2, sym.Type));
+ }
+
+ protected internal void AcceptWhile(TSymbolType type1, TSymbolType type2, TSymbolType type3)
+ {
+ AcceptWhile(sym => Equals(type1, sym.Type) || Equals(type2, sym.Type) || Equals(type3, sym.Type));
+ }
+
+ protected internal void AcceptWhile(params TSymbolType[] types)
+ {
+ AcceptWhile(sym => types.Any(expected => Equals(expected, sym.Type)));
+ }
+
+ protected internal void AcceptUntil(TSymbolType type)
+ {
+ AcceptWhile(sym => !Equals(type, sym.Type));
+ }
+
+ // We want to avoid array allocations and enumeration where possible, so we use the same technique as String.Format
+ protected internal void AcceptUntil(TSymbolType type1, TSymbolType type2)
+ {
+ AcceptWhile(sym => !Equals(type1, sym.Type) && !Equals(type2, sym.Type));
+ }
+
+ protected internal void AcceptUntil(TSymbolType type1, TSymbolType type2, TSymbolType type3)
+ {
+ AcceptWhile(sym => !Equals(type1, sym.Type) && !Equals(type2, sym.Type) && !Equals(type3, sym.Type));
+ }
+
+ protected internal void AcceptUntil(params TSymbolType[] types)
+ {
+ AcceptWhile(sym => types.All(expected => !Equals(expected, sym.Type)));
+ }
+
+ protected internal void AcceptWhile(Func<TSymbol, bool> condition)
+ {
+ Accept(ReadWhileLazy(condition));
+ }
+
+ protected internal IEnumerable<TSymbol> ReadWhile(Func<TSymbol, bool> condition)
+ {
+ return ReadWhileLazy(condition).ToList();
+ }
+
+ protected TSymbol AcceptWhiteSpaceInLines()
+ {
+ TSymbol lastWs = null;
+ while (Language.IsWhiteSpace(CurrentSymbol) || Language.IsNewLine(CurrentSymbol))
+ {
+ // Capture the previous whitespace node
+ if (lastWs != null)
+ {
+ Accept(lastWs);
+ }
+
+ if (Language.IsWhiteSpace(CurrentSymbol))
+ {
+ lastWs = CurrentSymbol;
+ }
+ else if (Language.IsNewLine(CurrentSymbol))
+ {
+ // Accept newline and reset last whitespace tracker
+ Accept(CurrentSymbol);
+ lastWs = null;
+ }
+
+ Tokenizer.Next();
+ }
+ return lastWs;
+ }
+
+ protected bool AtIdentifier(bool allowKeywords)
+ {
+ return CurrentSymbol != null &&
+ (Language.IsIdentifier(CurrentSymbol) ||
+ (allowKeywords && Language.IsKeyword(CurrentSymbol)));
+ }
+
+ // Don't open this to sub classes because it's lazy but it looks eager.
+ // You have to advance the Enumerable to read the next characters.
+ internal IEnumerable<TSymbol> ReadWhileLazy(Func<TSymbol, bool> condition)
+ {
+ while (EnsureCurrent() && condition(CurrentSymbol))
+ {
+ yield return CurrentSymbol;
+ NextToken();
+ }
+ }
+
+ private void Configure(SpanKind? kind, AcceptedCharacters? accepts)
+ {
+ if (kind != null)
+ {
+ Span.Kind = kind.Value;
+ }
+ if (accepts != null)
+ {
+ Span.EditHandler.AcceptedCharacters = accepts.Value;
+ }
+ }
+
+ protected virtual void OutputSpanBeforeRazorComment()
+ {
+ throw new InvalidOperationException(RazorResources.Language_Does_Not_Support_RazorComment);
+ }
+
+ private void CommentSpanConfig(SpanBuilder span)
+ {
+ span.CodeGenerator = SpanCodeGenerator.Null;
+ span.EditHandler = SpanEditHandler.CreateDefault(Language.TokenizeString);
+ }
+
+ protected void RazorComment()
+ {
+ if (!Language.KnowsSymbolType(KnownSymbolType.CommentStart) ||
+ !Language.KnowsSymbolType(KnownSymbolType.CommentStar) ||
+ !Language.KnowsSymbolType(KnownSymbolType.CommentBody))
+ {
+ throw new InvalidOperationException(RazorResources.Language_Does_Not_Support_RazorComment);
+ }
+ OutputSpanBeforeRazorComment();
+ using (PushSpanConfig(CommentSpanConfig))
+ {
+ using (Context.StartBlock(BlockType.Comment))
+ {
+ Context.CurrentBlock.CodeGenerator = new RazorCommentCodeGenerator();
+ SourceLocation start = CurrentLocation;
+
+ Expected(KnownSymbolType.CommentStart);
+ Output(SpanKind.Transition, AcceptedCharacters.None);
+
+ Expected(KnownSymbolType.CommentStar);
+ Output(SpanKind.MetaCode, AcceptedCharacters.None);
+
+ Optional(KnownSymbolType.CommentBody);
+ AddMarkerSymbolIfNecessary();
+ Output(SpanKind.Comment);
+
+ bool errorReported = false;
+ if (!Optional(KnownSymbolType.CommentStar))
+ {
+ errorReported = true;
+ Context.OnError(start, RazorResources.ParseError_RazorComment_Not_Terminated);
+ }
+ else
+ {
+ Output(SpanKind.MetaCode, AcceptedCharacters.None);
+ }
+
+ if (!Optional(KnownSymbolType.CommentStart))
+ {
+ if (!errorReported)
+ {
+ errorReported = true;
+ Context.OnError(start, RazorResources.ParseError_RazorComment_Not_Terminated);
+ }
+ }
+ else
+ {
+ Output(SpanKind.Transition, AcceptedCharacters.None);
+ }
+ }
+ }
+ Initialize(Span);
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Parser/TokenizerBackedParser.cs b/src/System.Web.Razor/Parser/TokenizerBackedParser.cs
new file mode 100644
index 00000000..7ebb663c
--- /dev/null
+++ b/src/System.Web.Razor/Parser/TokenizerBackedParser.cs
@@ -0,0 +1,85 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Text;
+using System.Web.Razor.Tokenizer;
+using System.Web.Razor.Tokenizer.Symbols;
+
+namespace System.Web.Razor.Parser
+{
+ [SuppressMessage("Microsoft.Design", "CA1005:AvoidExcessiveParametersOnGenericTypes", Justification = "All generic type parameters are required")]
+ public abstract partial class TokenizerBackedParser<TTokenizer, TSymbol, TSymbolType> : ParserBase
+ where TTokenizer : Tokenizer<TSymbol, TSymbolType>
+ where TSymbol : SymbolBase<TSymbolType>
+ {
+ private TokenizerView<TTokenizer, TSymbol, TSymbolType> _tokenizer;
+
+ protected TokenizerBackedParser()
+ {
+ Span = new SpanBuilder();
+ }
+
+ protected SpanBuilder Span { get; set; }
+
+ protected TokenizerView<TTokenizer, TSymbol, TSymbolType> Tokenizer
+ {
+ get { return _tokenizer ?? InitTokenizer(); }
+ }
+
+ protected Action<SpanBuilder> SpanConfig { get; set; }
+
+ protected TSymbol CurrentSymbol
+ {
+ get { return Tokenizer.Current; }
+ }
+
+ protected TSymbol PreviousSymbol { get; private set; }
+
+ protected SourceLocation CurrentLocation
+ {
+ get { return (EndOfFile || CurrentSymbol == null) ? Context.Source.Location : CurrentSymbol.Start; }
+ }
+
+ protected bool EndOfFile
+ {
+ get { return Tokenizer.EndOfFile; }
+ }
+
+ protected abstract LanguageCharacteristics<TTokenizer, TSymbol, TSymbolType> Language { get; }
+
+ protected virtual void HandleEmbeddedTransition()
+ {
+ }
+
+ protected virtual bool IsAtEmbeddedTransition(bool allowTemplatesAndComments, bool allowTransitions)
+ {
+ return false;
+ }
+
+ public override void BuildSpan(SpanBuilder span, SourceLocation start, string content)
+ {
+ foreach (ISymbol sym in Language.TokenizeString(start, content))
+ {
+ span.Accept(sym);
+ }
+ }
+
+ protected void Initialize(SpanBuilder span)
+ {
+ if (SpanConfig != null)
+ {
+ SpanConfig(span);
+ }
+ }
+
+ protected internal bool NextToken()
+ {
+ PreviousSymbol = CurrentSymbol;
+ return Tokenizer.Next();
+ }
+
+ private TokenizerView<TTokenizer, TSymbol, TSymbolType> InitTokenizer()
+ {
+ return _tokenizer = new TokenizerView<TTokenizer, TSymbol, TSymbolType>(Language.CreateTokenizer(Context.Source));
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Parser/VBCodeParser.Directives.cs b/src/System.Web.Razor/Parser/VBCodeParser.Directives.cs
new file mode 100644
index 00000000..35778f2b
--- /dev/null
+++ b/src/System.Web.Razor/Parser/VBCodeParser.Directives.cs
@@ -0,0 +1,408 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using System.Web.Razor.Editor;
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Text;
+using System.Web.Razor.Tokenizer;
+using System.Web.Razor.Tokenizer.Symbols;
+
+namespace System.Web.Razor.Parser
+{
+ public partial class VBCodeParser : TokenizerBackedParser<VBTokenizer, VBSymbol, VBSymbolType>
+ {
+ private void SetUpDirectives()
+ {
+ MapDirective(SyntaxConstants.VB.CodeKeyword, EndTerminatedDirective(SyntaxConstants.VB.CodeKeyword,
+ BlockType.Statement,
+ new StatementCodeGenerator(),
+ allowMarkup: true));
+ MapDirective(SyntaxConstants.VB.FunctionsKeyword, EndTerminatedDirective(SyntaxConstants.VB.FunctionsKeyword,
+ BlockType.Functions,
+ new TypeMemberCodeGenerator(),
+ allowMarkup: false));
+ MapDirective(SyntaxConstants.VB.SectionKeyword, SectionDirective);
+ MapDirective(SyntaxConstants.VB.HelperKeyword, HelperDirective);
+
+ MapDirective(SyntaxConstants.VB.LayoutKeyword, LayoutDirective);
+ MapDirective(SyntaxConstants.VB.SessionStateKeyword, SessionStateDirective);
+ }
+
+ protected virtual bool LayoutDirective()
+ {
+ AssertDirective(SyntaxConstants.VB.LayoutKeyword);
+ AcceptAndMoveNext();
+ Context.CurrentBlock.Type = BlockType.Directive;
+
+ // Accept spaces, but not newlines
+ bool foundSomeWhitespace = At(VBSymbolType.WhiteSpace);
+ AcceptWhile(VBSymbolType.WhiteSpace);
+ Output(SpanKind.MetaCode, foundSomeWhitespace ? AcceptedCharacters.None : AcceptedCharacters.Any);
+
+ // First non-whitespace character starts the Layout Page, then newline ends it
+ AcceptUntil(VBSymbolType.NewLine);
+ Span.CodeGenerator = new SetLayoutCodeGenerator(Span.GetContent());
+ Span.EditHandler.EditorHints = EditorHints.LayoutPage | EditorHints.VirtualPath;
+ bool foundNewline = Optional(VBSymbolType.NewLine);
+ AddMarkerSymbolIfNecessary();
+ Output(SpanKind.MetaCode, foundNewline ? AcceptedCharacters.None : AcceptedCharacters.Any);
+ return true;
+ }
+
+ protected virtual bool SessionStateDirective()
+ {
+ AssertDirective(SyntaxConstants.VB.SessionStateKeyword);
+ AcceptAndMoveNext();
+ Context.CurrentBlock.Type = BlockType.Directive;
+
+ // Accept spaces, but not newlines
+ bool foundSomeWhitespace = At(VBSymbolType.WhiteSpace);
+ AcceptWhile(VBSymbolType.WhiteSpace);
+ Output(SpanKind.MetaCode, foundSomeWhitespace ? AcceptedCharacters.None : AcceptedCharacters.Any);
+
+ // First non-whitespace character starts the session state directive, then newline ends it
+ AcceptUntil(VBSymbolType.NewLine);
+ var value = String.Concat(Span.Symbols.Select(sym => sym.Content));
+ Span.CodeGenerator = new RazorDirectiveAttributeCodeGenerator(SyntaxConstants.VB.SessionStateKeyword, value);
+ bool foundNewline = Optional(VBSymbolType.NewLine);
+ AddMarkerSymbolIfNecessary();
+ Output(SpanKind.MetaCode, foundNewline ? AcceptedCharacters.None : AcceptedCharacters.Any);
+ return true;
+ }
+
+ protected virtual bool HelperDirective()
+ {
+ if (Context.IsWithin(BlockType.Helper))
+ {
+ Context.OnError(CurrentLocation, RazorResources.ParseError_Helpers_Cannot_Be_Nested);
+ }
+
+ Context.CurrentBlock.Type = BlockType.Helper;
+ SourceLocation blockStart = CurrentLocation;
+
+ AssertDirective(SyntaxConstants.VB.HelperKeyword);
+ AcceptAndMoveNext();
+
+ VBSymbolType firstAfterKeyword = VBSymbolType.Unknown;
+ if (CurrentSymbol != null)
+ {
+ firstAfterKeyword = CurrentSymbol.Type;
+ }
+
+ VBSymbol remainingWs = null;
+ if (At(VBSymbolType.NewLine))
+ {
+ // Accept a _single_ new line, we'll be aborting later.
+ AcceptAndMoveNext();
+ }
+ else
+ {
+ remainingWs = AcceptSingleWhiteSpaceCharacter();
+ }
+ if (firstAfterKeyword == VBSymbolType.WhiteSpace || firstAfterKeyword == VBSymbolType.NewLine)
+ {
+ Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
+ }
+ Output(SpanKind.MetaCode);
+ if (firstAfterKeyword != VBSymbolType.WhiteSpace)
+ {
+ string error;
+ if (At(VBSymbolType.NewLine))
+ {
+ error = RazorResources.ErrorComponent_Newline;
+ }
+ else if (EndOfFile)
+ {
+ error = RazorResources.ErrorComponent_EndOfFile;
+ }
+ else
+ {
+ error = String.Format(CultureInfo.CurrentCulture, RazorResources.ErrorComponent_Character, CurrentSymbol.Content);
+ }
+
+ Context.OnError(
+ CurrentLocation,
+ RazorResources.ParseError_Unexpected_Character_At_Helper_Name_Start,
+ error);
+
+ // Bail out.
+ PutCurrentBack();
+ Output(SpanKind.Code);
+ return false;
+ }
+
+ if (remainingWs != null)
+ {
+ Accept(remainingWs);
+ }
+
+ bool errorReported = !Required(VBSymbolType.Identifier, RazorResources.ParseError_Unexpected_Character_At_Helper_Name_Start);
+
+ AcceptWhile(VBSymbolType.WhiteSpace);
+
+ SourceLocation parensStart = CurrentLocation;
+ bool headerComplete = false;
+ if (!Optional(VBSymbolType.LeftParenthesis))
+ {
+ if (!errorReported)
+ {
+ errorReported = true;
+ Context.OnError(CurrentLocation,
+ RazorResources.ParseError_MissingCharAfterHelperName,
+ VBSymbol.GetSample(VBSymbolType.LeftParenthesis));
+ }
+ }
+ else if (!Balance(BalancingModes.NoErrorOnFailure, VBSymbolType.LeftParenthesis, VBSymbolType.RightParenthesis, parensStart))
+ {
+ Context.OnError(parensStart, RazorResources.ParseError_UnterminatedHelperParameterList);
+ }
+ else
+ {
+ Expected(VBSymbolType.RightParenthesis);
+ headerComplete = true;
+ }
+
+ AddMarkerSymbolIfNecessary();
+ Context.CurrentBlock.CodeGenerator = new HelperCodeGenerator(
+ Span.GetContent(),
+ headerComplete);
+ AutoCompleteEditHandler editHandler = new AutoCompleteEditHandler(Language.TokenizeString);
+ Span.EditHandler = editHandler;
+ Output(SpanKind.Code);
+
+ if (headerComplete)
+ {
+ bool old = IsNested;
+ IsNested = true;
+ using (Context.StartBlock(BlockType.Statement))
+ {
+ using (PushSpanConfig(StatementBlockSpanConfiguration(new StatementCodeGenerator())))
+ {
+ try
+ {
+ if (!EndTerminatedDirectiveBody(SyntaxConstants.VB.HelperKeyword, blockStart, allowAllTransitions: true))
+ {
+ if (Context.LastAcceptedCharacters != AcceptedCharacters.Any)
+ {
+ AddMarkerSymbolIfNecessary();
+ }
+
+ editHandler.AutoCompleteString = SyntaxConstants.VB.EndHelperKeyword;
+ return false;
+ }
+ else
+ {
+ return true;
+ }
+ }
+ finally
+ {
+ Output(SpanKind.Code);
+ IsNested = old;
+ }
+ }
+ }
+ }
+ else
+ {
+ Output(SpanKind.Code);
+ }
+ PutCurrentBack();
+ return false;
+ }
+
+ protected virtual bool SectionDirective()
+ {
+ SourceLocation start = CurrentLocation;
+ AssertDirective(SyntaxConstants.VB.SectionKeyword);
+ AcceptAndMoveNext();
+
+ if (Context.IsWithin(BlockType.Section))
+ {
+ Context.OnError(CurrentLocation, RazorResources.ParseError_Sections_Cannot_Be_Nested, RazorResources.SectionExample_VB);
+ }
+
+ if (At(VBSymbolType.NewLine))
+ {
+ AcceptAndMoveNext();
+ }
+ else
+ {
+ AcceptVBSpaces();
+ }
+ string sectionName = null;
+ if (!At(VBSymbolType.Identifier))
+ {
+ Context.OnError(CurrentLocation,
+ RazorResources.ParseError_Unexpected_Character_At_Section_Name_Start,
+ GetCurrentSymbolDisplay());
+ }
+ else
+ {
+ sectionName = CurrentSymbol.Content;
+ AcceptAndMoveNext();
+ }
+ Context.CurrentBlock.Type = BlockType.Section;
+ Context.CurrentBlock.CodeGenerator = new SectionCodeGenerator(sectionName ?? String.Empty);
+
+ AutoCompleteEditHandler editHandler = new AutoCompleteEditHandler(Language.TokenizeString);
+ Span.EditHandler = editHandler;
+
+ PutCurrentBack();
+
+ Output(SpanKind.MetaCode);
+
+ // Parse the section
+ OtherParserBlock(null, SyntaxConstants.VB.EndSectionKeyword);
+
+ Span.CodeGenerator = SpanCodeGenerator.Null;
+ bool complete = false;
+ if (!At(VBKeyword.End))
+ {
+ Context.OnError(start,
+ RazorResources.ParseError_BlockNotTerminated,
+ SyntaxConstants.VB.SectionKeyword,
+ SyntaxConstants.VB.EndSectionKeyword);
+ editHandler.AutoCompleteString = SyntaxConstants.VB.EndSectionKeyword;
+ }
+ else
+ {
+ AcceptAndMoveNext();
+ AcceptWhile(VBSymbolType.WhiteSpace);
+ if (!At(SyntaxConstants.VB.SectionKeyword))
+ {
+ Context.OnError(start,
+ RazorResources.ParseError_BlockNotTerminated,
+ SyntaxConstants.VB.SectionKeyword,
+ SyntaxConstants.VB.EndSectionKeyword);
+ }
+ else
+ {
+ AcceptAndMoveNext();
+ Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
+ complete = true;
+ }
+ }
+ PutCurrentBack();
+ Output(SpanKind.MetaCode);
+ return complete;
+ }
+
+ protected virtual Func<bool> EndTerminatedDirective(string directive, BlockType blockType, SpanCodeGenerator codeGenerator, bool allowMarkup)
+ {
+ return () =>
+ {
+ SourceLocation blockStart = CurrentLocation;
+ Context.CurrentBlock.Type = blockType;
+ AssertDirective(directive);
+ AcceptAndMoveNext();
+
+ Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
+ Span.CodeGenerator = SpanCodeGenerator.Null;
+ Output(SpanKind.MetaCode);
+
+ using (PushSpanConfig(StatementBlockSpanConfiguration(codeGenerator)))
+ {
+ AutoCompleteEditHandler editHandler = new AutoCompleteEditHandler(Language.TokenizeString);
+ Span.EditHandler = editHandler;
+
+ if (!EndTerminatedDirectiveBody(directive, blockStart, allowMarkup))
+ {
+ editHandler.AutoCompleteString = String.Concat(SyntaxConstants.VB.EndKeyword, " ", directive);
+ return false;
+ }
+ return true;
+ }
+ };
+ }
+
+ protected virtual bool EndTerminatedDirectiveBody(string directive, SourceLocation blockStart, bool allowAllTransitions)
+ {
+ while (!EndOfFile)
+ {
+ VBSymbol lastWhitespace = AcceptWhiteSpaceInLines();
+ if (IsAtEmbeddedTransition(allowTemplatesAndComments: allowAllTransitions, allowTransitions: allowAllTransitions))
+ {
+ HandleEmbeddedTransition(lastWhitespace);
+ }
+ else
+ {
+ if (At(VBKeyword.End))
+ {
+ Accept(lastWhitespace);
+ VBSymbol end = CurrentSymbol;
+ NextToken();
+ IEnumerable<VBSymbol> ws = ReadVBSpaces();
+ if (At(directive))
+ {
+ if (Context.LastAcceptedCharacters != AcceptedCharacters.Any)
+ {
+ AddMarkerSymbolIfNecessary(end.Start);
+ }
+ Output(SpanKind.Code);
+ Accept(end);
+ Accept(ws);
+ AcceptAndMoveNext();
+ Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
+ Span.CodeGenerator = SpanCodeGenerator.Null;
+ Output(SpanKind.MetaCode);
+ return true;
+ }
+ else
+ {
+ Accept(end);
+ Accept(ws);
+ AcceptAndMoveNext();
+ }
+ }
+ else
+ {
+ Accept(lastWhitespace);
+ AcceptAndMoveNext();
+ }
+ }
+ }
+
+ // This is a language keyword, so it does not need to be localized
+ Context.OnError(blockStart, RazorResources.ParseError_BlockNotTerminated, directive, String.Concat(SyntaxConstants.VB.EndKeyword, " ", directive));
+ return false;
+ }
+
+ protected bool At(string directive)
+ {
+ return At(VBSymbolType.Identifier) && String.Equals(CurrentSymbol.Content, directive, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "'this' is used in DEBUG builds")]
+ [Conditional("DEBUG")]
+ protected void AssertDirective(string directive)
+ {
+ Assert(VBSymbolType.Identifier);
+ Debug.Assert(String.Equals(directive, CurrentSymbol.Content, StringComparison.OrdinalIgnoreCase));
+ }
+
+ private string GetCurrentSymbolDisplay()
+ {
+ if (EndOfFile)
+ {
+ return RazorResources.ErrorComponent_EndOfFile;
+ }
+ else if (At(VBSymbolType.NewLine))
+ {
+ return RazorResources.ErrorComponent_Newline;
+ }
+ else if (At(VBSymbolType.WhiteSpace))
+ {
+ return RazorResources.ErrorComponent_Whitespace;
+ }
+ else
+ {
+ return String.Format(CultureInfo.CurrentCulture, RazorResources.ErrorComponent_Character, CurrentSymbol.Content);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Parser/VBCodeParser.Statements.cs b/src/System.Web.Razor/Parser/VBCodeParser.Statements.cs
new file mode 100644
index 00000000..fdbe3fc7
--- /dev/null
+++ b/src/System.Web.Razor/Parser/VBCodeParser.Statements.cs
@@ -0,0 +1,312 @@
+using System.Diagnostics;
+using System.Globalization;
+using System.Linq;
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Text;
+using System.Web.Razor.Tokenizer;
+using System.Web.Razor.Tokenizer.Symbols;
+
+namespace System.Web.Razor.Parser
+{
+ public partial class VBCodeParser : TokenizerBackedParser<VBTokenizer, VBSymbol, VBSymbolType>
+ {
+ private void SetUpKeywords()
+ {
+ MapKeyword(VBKeyword.Using, EndTerminatedStatement(VBKeyword.Using, supportsExit: false, supportsContinue: false)); // http://msdn.microsoft.com/en-us/library/htd05whh.aspx
+ MapKeyword(VBKeyword.While, EndTerminatedStatement(VBKeyword.While, supportsExit: true, supportsContinue: true)); // http://msdn.microsoft.com/en-us/library/zh1f56zs.aspx
+ MapKeyword(VBKeyword.If, EndTerminatedStatement(VBKeyword.If, supportsExit: false, supportsContinue: false)); // http://msdn.microsoft.com/en-us/library/752y8abs.aspx
+ MapKeyword(VBKeyword.Select, EndTerminatedStatement(VBKeyword.Select, supportsExit: true, supportsContinue: false, blockName: SyntaxConstants.VB.SelectCaseKeyword)); // http://msdn.microsoft.com/en-us/library/cy37t14y.aspx
+ MapKeyword(VBKeyword.Try, EndTerminatedStatement(VBKeyword.Try, supportsExit: true, supportsContinue: false)); // http://msdn.microsoft.com/en-us/library/fk6t46tz.aspx
+ MapKeyword(VBKeyword.With, EndTerminatedStatement(VBKeyword.With, supportsExit: false, supportsContinue: false)); // http://msdn.microsoft.com/en-us/library/wc500chb.aspx
+ MapKeyword(VBKeyword.SyncLock, EndTerminatedStatement(VBKeyword.SyncLock, supportsExit: false, supportsContinue: false)); // http://msdn.microsoft.com/en-us/library/3a86s51t.aspx
+
+ // http://msdn.microsoft.com/en-us/library/5z06z1kb.aspx
+ // http://msdn.microsoft.com/en-us/library/5ebk1751.aspx
+ MapKeyword(VBKeyword.For, KeywordTerminatedStatement(VBKeyword.For, VBKeyword.Next, supportsExit: true, supportsContinue: true));
+ MapKeyword(VBKeyword.Do, KeywordTerminatedStatement(VBKeyword.Do, VBKeyword.Loop, supportsExit: true, supportsContinue: true)); // http://msdn.microsoft.com/en-us/library/eked04a7.aspx
+
+ MapKeyword(VBKeyword.Imports, ImportsStatement);
+ MapKeyword(VBKeyword.Option, OptionStatement);
+ MapKeyword(VBKeyword.Inherits, InheritsStatement);
+
+ MapKeyword(VBKeyword.Class, ReservedWord);
+ MapKeyword(VBKeyword.Namespace, ReservedWord);
+ }
+
+ protected virtual bool InheritsStatement()
+ {
+ Assert(VBKeyword.Inherits);
+
+ Span.CodeGenerator = SpanCodeGenerator.Null;
+ Context.CurrentBlock.Type = BlockType.Directive;
+
+ AcceptAndMoveNext();
+ SourceLocation endInherits = CurrentLocation;
+
+ if (At(VBSymbolType.WhiteSpace))
+ {
+ Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
+ }
+
+ AcceptWhile(VBSymbolType.WhiteSpace);
+ Output(SpanKind.MetaCode);
+
+ if (EndOfFile || At(VBSymbolType.WhiteSpace) || At(VBSymbolType.NewLine))
+ {
+ Context.OnError(endInherits, RazorResources.ParseError_InheritsKeyword_Must_Be_Followed_By_TypeName);
+ }
+
+ // Just accept to a newline
+ AcceptUntil(VBSymbolType.NewLine);
+ if (!Context.DesignTimeMode)
+ {
+ // We want the newline to be treated as code, but it causes issues at design-time.
+ Optional(VBSymbolType.NewLine);
+ }
+
+ string baseType = Span.GetContent();
+ Span.CodeGenerator = new SetBaseTypeCodeGenerator(baseType.Trim());
+
+ Output(SpanKind.Code);
+ return false;
+ }
+
+ protected virtual bool OptionStatement()
+ {
+ try
+ {
+ Context.CurrentBlock.Type = BlockType.Directive;
+
+ Assert(VBKeyword.Option);
+ AcceptAndMoveNext();
+ AcceptWhile(VBSymbolType.WhiteSpace);
+ if (!At(VBSymbolType.Identifier))
+ {
+ if (CurrentSymbol != null)
+ {
+ Context.OnError(CurrentLocation, String.Format(CultureInfo.CurrentCulture,
+ RazorResources.ParseError_Unexpected,
+ CurrentSymbol.Content));
+ }
+ return false;
+ }
+ SourceLocation optionLoc = CurrentLocation;
+ string option = CurrentSymbol.Content;
+ AcceptAndMoveNext();
+
+ AcceptWhile(VBSymbolType.WhiteSpace);
+ bool boolVal;
+ if (At(VBKeyword.On))
+ {
+ AcceptAndMoveNext();
+ boolVal = true;
+ }
+ else if (At(VBSymbolType.Identifier))
+ {
+ if (String.Equals(CurrentSymbol.Content, SyntaxConstants.VB.OffKeyword, StringComparison.OrdinalIgnoreCase))
+ {
+ AcceptAndMoveNext();
+ boolVal = false;
+ }
+ else
+ {
+ Context.OnError(CurrentLocation, String.Format(CultureInfo.CurrentCulture,
+ RazorResources.ParseError_InvalidOptionValue,
+ option,
+ CurrentSymbol.Content));
+ AcceptAndMoveNext();
+ return false;
+ }
+ }
+ else
+ {
+ if (!EndOfFile)
+ {
+ Context.OnError(CurrentLocation, String.Format(CultureInfo.CurrentCulture,
+ RazorResources.ParseError_Unexpected,
+ CurrentSymbol.Content));
+ AcceptAndMoveNext();
+ }
+ return false;
+ }
+
+ if (String.Equals(option, SyntaxConstants.VB.StrictKeyword, StringComparison.OrdinalIgnoreCase))
+ {
+ Span.CodeGenerator = SetVBOptionCodeGenerator.Strict(boolVal);
+ }
+ else if (String.Equals(option, SyntaxConstants.VB.ExplicitKeyword, StringComparison.OrdinalIgnoreCase))
+ {
+ Span.CodeGenerator = SetVBOptionCodeGenerator.Explicit(boolVal);
+ }
+ else
+ {
+ Span.CodeGenerator = new SetVBOptionCodeGenerator(option, boolVal);
+ Context.OnError(optionLoc, RazorResources.ParseError_UnknownOption, option);
+ }
+ }
+ finally
+ {
+ if (Span.Symbols.Count > 0)
+ {
+ Output(SpanKind.MetaCode);
+ }
+ }
+ return true;
+ }
+
+ protected virtual bool ImportsStatement()
+ {
+ Context.CurrentBlock.Type = BlockType.Directive;
+ Assert(VBKeyword.Imports);
+ AcceptAndMoveNext();
+
+ AcceptVBSpaces();
+ if (At(VBSymbolType.WhiteSpace) || At(VBSymbolType.NewLine))
+ {
+ Context.OnError(CurrentLocation, RazorResources.ParseError_NamespaceOrTypeAliasExpected);
+ }
+
+ // Just accept to a newline
+ AcceptUntil(VBSymbolType.NewLine);
+ Optional(VBSymbolType.NewLine);
+
+ string ns = String.Concat(Span.Symbols.Skip(1).Select(s => s.Content));
+ Span.CodeGenerator = new AddImportCodeGenerator(ns, SyntaxConstants.VB.ImportsKeywordLength);
+
+ Output(SpanKind.MetaCode);
+ return false;
+ }
+
+ protected virtual Func<bool> EndTerminatedStatement(VBKeyword keyword, bool supportsExit, bool supportsContinue)
+ {
+ return EndTerminatedStatement(keyword, supportsExit, supportsContinue, blockName: keyword.ToString());
+ }
+
+ protected virtual Func<bool> EndTerminatedStatement(VBKeyword keyword, bool supportsExit, bool supportsContinue, string blockName)
+ {
+ return () =>
+ {
+ using (PushSpanConfig(StatementBlockSpanConfiguration(new StatementCodeGenerator())))
+ {
+ SourceLocation blockStart = CurrentLocation;
+ Assert(keyword);
+ AcceptAndMoveNext();
+
+ while (!EndOfFile)
+ {
+ VBSymbol lastWhitespace = AcceptWhiteSpaceInLines();
+ if (IsAtEmbeddedTransition(allowTemplatesAndComments: true, allowTransitions: true))
+ {
+ HandleEmbeddedTransition(lastWhitespace);
+ }
+ else
+ {
+ Accept(lastWhitespace);
+
+ if ((supportsExit && At(VBKeyword.Exit)) || (supportsContinue && At(VBKeyword.Continue)))
+ {
+ HandleExitOrContinue(keyword);
+ }
+ else if (At(VBKeyword.End))
+ {
+ AcceptAndMoveNext();
+ AcceptVBSpaces();
+ if (At(keyword))
+ {
+ AcceptAndMoveNext();
+ if (!Context.DesignTimeMode)
+ {
+ Optional(VBSymbolType.NewLine);
+ }
+ Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
+ return false;
+ }
+ }
+ else if (At(keyword))
+ {
+ // Parse nested statement
+ EndTerminatedStatement(keyword, supportsExit, supportsContinue)();
+ }
+ else if (!EndOfFile)
+ {
+ AcceptAndMoveNext();
+ }
+ }
+ }
+
+ Context.OnError(blockStart,
+ RazorResources.ParseError_BlockNotTerminated,
+ blockName,
+ // This is a language keyword, so it does not need to be localized
+ String.Concat(VBKeyword.End, " ", keyword));
+ return false;
+ }
+ };
+ }
+
+ protected virtual Func<bool> KeywordTerminatedStatement(VBKeyword start, VBKeyword terminator, bool supportsExit, bool supportsContinue)
+ {
+ return () =>
+ {
+ using (PushSpanConfig(StatementBlockSpanConfiguration(new StatementCodeGenerator())))
+ {
+ SourceLocation blockStart = CurrentLocation;
+ Assert(start);
+ AcceptAndMoveNext();
+ while (!EndOfFile)
+ {
+ VBSymbol lastWhitespace = AcceptWhiteSpaceInLines();
+ if (IsAtEmbeddedTransition(allowTemplatesAndComments: true, allowTransitions: true))
+ {
+ HandleEmbeddedTransition(lastWhitespace);
+ }
+ else
+ {
+ Accept(lastWhitespace);
+ if ((supportsExit && At(VBKeyword.Exit)) || (supportsContinue && At(VBKeyword.Continue)))
+ {
+ HandleExitOrContinue(start);
+ }
+ else if (At(start))
+ {
+ // Parse nested statement
+ KeywordTerminatedStatement(start, terminator, supportsExit, supportsContinue)();
+ }
+ else if (At(terminator))
+ {
+ AcceptUntil(VBSymbolType.NewLine);
+ Optional(VBSymbolType.NewLine);
+ Span.EditHandler.AcceptedCharacters = AcceptedCharacters.AnyExceptNewline;
+ return false;
+ }
+ else if (!EndOfFile)
+ {
+ AcceptAndMoveNext();
+ }
+ }
+ }
+
+ Context.OnError(blockStart,
+ RazorResources.ParseError_BlockNotTerminated,
+ start, terminator);
+ return false;
+ }
+ };
+ }
+
+ protected void HandleExitOrContinue(VBKeyword keyword)
+ {
+ Assert(VBSymbolType.Keyword);
+ Debug.Assert(CurrentSymbol.Keyword == VBKeyword.Continue || CurrentSymbol.Keyword == VBKeyword.Exit);
+
+ // Accept, read whitespace and look for the next keyword
+ AcceptAndMoveNext();
+ AcceptWhile(VBSymbolType.WhiteSpace);
+
+ // If this is the start keyword, skip it and continue (to avoid starting a nested statement block)
+ Optional(keyword);
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Parser/VBCodeParser.cs b/src/System.Web.Razor/Parser/VBCodeParser.cs
new file mode 100644
index 00000000..865dcc78
--- /dev/null
+++ b/src/System.Web.Razor/Parser/VBCodeParser.cs
@@ -0,0 +1,598 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Web.Razor.Editor;
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Text;
+using System.Web.Razor.Tokenizer;
+using System.Web.Razor.Tokenizer.Symbols;
+
+namespace System.Web.Razor.Parser
+{
+ public partial class VBCodeParser : TokenizerBackedParser<VBTokenizer, VBSymbol, VBSymbolType>
+ {
+ internal static ISet<string> DefaultKeywords = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+ {
+ "functions",
+ "code",
+ "section",
+ "do",
+ "while",
+ "if",
+ "select",
+ "for",
+ "try",
+ "with",
+ "synclock",
+ "using",
+ "imports",
+ "inherits",
+ "option",
+ "helper",
+ "namespace",
+ "class",
+ "layout",
+ "sessionstate"
+ };
+
+ private Dictionary<VBKeyword, Func<bool>> _keywordHandlers = new Dictionary<VBKeyword, Func<bool>>();
+ private Dictionary<string, Func<bool>> _directiveHandlers = new Dictionary<string, Func<bool>>(StringComparer.OrdinalIgnoreCase);
+
+ [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "Necessary state is initialized before calling virtual methods")]
+ public VBCodeParser()
+ {
+ DirectParentIsCode = false;
+ Keywords = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+ SetUpKeywords();
+ SetUpDirectives();
+ }
+
+ protected internal ISet<string> Keywords { get; private set; }
+
+ protected override LanguageCharacteristics<VBTokenizer, VBSymbol, VBSymbolType> Language
+ {
+ get { return VBLanguageCharacteristics.Instance; }
+ }
+
+ protected override ParserBase OtherParser
+ {
+ get { return Context.MarkupParser; }
+ }
+
+ private bool IsNested { get; set; }
+ private bool DirectParentIsCode { get; set; }
+
+ protected override bool IsAtEmbeddedTransition(bool allowTemplatesAndComments, bool allowTransitions)
+ {
+ return (allowTransitions && Language.IsTransition(CurrentSymbol) && !Was(VBSymbolType.Dot)) ||
+ (allowTemplatesAndComments && Language.IsCommentStart(CurrentSymbol)) ||
+ (Language.IsTransition(CurrentSymbol) && NextIs(VBSymbolType.Transition));
+ }
+
+ protected override void HandleEmbeddedTransition()
+ {
+ HandleEmbeddedTransition(null);
+ }
+
+ protected void HandleEmbeddedTransition(VBSymbol lastWhiteSpace)
+ {
+ if (At(VBSymbolType.RazorCommentTransition))
+ {
+ Accept(lastWhiteSpace);
+ RazorComment();
+ }
+ else if ((At(VBSymbolType.Transition) && !Was(VBSymbolType.Dot)))
+ {
+ HandleTransition(lastWhiteSpace);
+ }
+ }
+
+ public override void ParseBlock()
+ {
+ if (Context == null)
+ {
+ throw new InvalidOperationException(RazorResources.Parser_Context_Not_Set);
+ }
+ using (PushSpanConfig())
+ {
+ if (Context == null)
+ {
+ throw new InvalidOperationException(RazorResources.Parser_Context_Not_Set);
+ }
+
+ Initialize(Span);
+ NextToken();
+ using (Context.StartBlock())
+ {
+ IEnumerable<VBSymbol> syms = ReadWhile(sym => sym.Type == VBSymbolType.WhiteSpace);
+ if (At(VBSymbolType.Transition))
+ {
+ Accept(syms);
+ Span.CodeGenerator = new StatementCodeGenerator();
+ Output(SpanKind.Code);
+ }
+ else
+ {
+ PutBack(syms);
+ EnsureCurrent();
+ }
+
+ // Allow a transition span, but don't require it
+ if (Optional(VBSymbolType.Transition))
+ {
+ Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
+ Span.CodeGenerator = SpanCodeGenerator.Null;
+ Output(SpanKind.Transition);
+ }
+
+ Context.CurrentBlock.Type = BlockType.Expression;
+ Context.CurrentBlock.CodeGenerator = new ExpressionCodeGenerator();
+
+ // Determine the type of the block
+ bool isComplete = false;
+ Action<SpanBuilder> config = null;
+ if (!EndOfFile)
+ {
+ switch (CurrentSymbol.Type)
+ {
+ case VBSymbolType.Identifier:
+ if (!TryDirectiveBlock(ref isComplete))
+ {
+ ImplicitExpression();
+ }
+ break;
+ case VBSymbolType.LeftParenthesis:
+ isComplete = ExplicitExpression();
+ break;
+ case VBSymbolType.Keyword:
+ Context.CurrentBlock.Type = BlockType.Statement;
+ Context.CurrentBlock.CodeGenerator = BlockCodeGenerator.Null;
+ isComplete = KeywordBlock();
+ break;
+ case VBSymbolType.WhiteSpace:
+ case VBSymbolType.NewLine:
+ config = ImplictExpressionSpanConfig;
+ Context.OnError(CurrentLocation,
+ RazorResources.ParseError_Unexpected_WhiteSpace_At_Start_Of_CodeBlock_VB);
+ break;
+ default:
+ config = ImplictExpressionSpanConfig;
+ Context.OnError(CurrentLocation,
+ RazorResources.ParseError_Unexpected_Character_At_Start_Of_CodeBlock_VB,
+ CurrentSymbol.Content);
+ break;
+ }
+ }
+ else
+ {
+ config = ImplictExpressionSpanConfig;
+ Context.OnError(CurrentLocation,
+ RazorResources.ParseError_Unexpected_EndOfFile_At_Start_Of_CodeBlock);
+ }
+ using (PushSpanConfig(config))
+ {
+ if (!isComplete && Span.Symbols.Count == 0 && Context.LastAcceptedCharacters != AcceptedCharacters.Any)
+ {
+ AddMarkerSymbolIfNecessary();
+ }
+ Output(SpanKind.Code);
+ PutCurrentBack();
+ }
+ }
+ }
+ }
+
+ private void ImplictExpressionSpanConfig(SpanBuilder span)
+ {
+ span.CodeGenerator = new ExpressionCodeGenerator();
+ span.EditHandler = new ImplicitExpressionEditHandler(
+ Language.TokenizeString,
+ Keywords,
+ acceptTrailingDot: DirectParentIsCode)
+ {
+ AcceptedCharacters = AcceptedCharacters.NonWhiteSpace
+ };
+ }
+
+ private Action<SpanBuilder> StatementBlockSpanConfiguration(SpanCodeGenerator codeGenerator)
+ {
+ return span =>
+ {
+ span.Kind = SpanKind.Code;
+ span.CodeGenerator = codeGenerator;
+ span.EditHandler = SpanEditHandler.CreateDefault(Language.TokenizeString);
+ };
+ }
+
+ // Pass "complete" flag by ref, not out because some paths may not change it.
+ private bool TryDirectiveBlock(ref bool complete)
+ {
+ Assert(VBSymbolType.Identifier);
+ Func<bool> handler;
+ if (_directiveHandlers.TryGetValue(CurrentSymbol.Content, out handler))
+ {
+ Context.CurrentBlock.CodeGenerator = BlockCodeGenerator.Null;
+ complete = handler();
+ return true;
+ }
+ return false;
+ }
+
+ private bool KeywordBlock()
+ {
+ Assert(VBSymbolType.Keyword);
+ Func<bool> handler;
+ if (_keywordHandlers.TryGetValue(CurrentSymbol.Keyword.Value, out handler))
+ {
+ Span.CodeGenerator = new StatementCodeGenerator();
+ Context.CurrentBlock.Type = BlockType.Statement;
+ return handler();
+ }
+ else
+ {
+ ImplicitExpression();
+ return false;
+ }
+ }
+
+ private bool ExplicitExpression()
+ {
+ Context.CurrentBlock.Type = BlockType.Expression;
+ Context.CurrentBlock.CodeGenerator = new ExpressionCodeGenerator();
+ SourceLocation start = CurrentLocation;
+ Expected(VBSymbolType.LeftParenthesis);
+ Span.CodeGenerator = SpanCodeGenerator.Null;
+ Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
+ Output(SpanKind.MetaCode);
+
+ Span.CodeGenerator = new ExpressionCodeGenerator();
+ using (PushSpanConfig(span => span.CodeGenerator = new ExpressionCodeGenerator()))
+ {
+ if (!Balance(BalancingModes.NoErrorOnFailure |
+ BalancingModes.BacktrackOnFailure |
+ BalancingModes.AllowCommentsAndTemplates,
+ VBSymbolType.LeftParenthesis,
+ VBSymbolType.RightParenthesis,
+ start))
+ {
+ Context.OnError(start,
+ RazorResources.ParseError_Expected_EndOfBlock_Before_EOF,
+ RazorResources.BlockName_ExplicitExpression,
+ VBSymbol.GetSample(VBSymbolType.RightParenthesis),
+ VBSymbol.GetSample(VBSymbolType.LeftParenthesis));
+ AcceptUntil(VBSymbolType.NewLine);
+ AddMarkerSymbolIfNecessary();
+ Output(SpanKind.Code);
+ PutCurrentBack();
+ return false;
+ }
+ else
+ {
+ AddMarkerSymbolIfNecessary();
+ Output(SpanKind.Code);
+ Expected(VBSymbolType.RightParenthesis);
+ Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
+ Span.CodeGenerator = SpanCodeGenerator.Null;
+ Output(SpanKind.MetaCode);
+ PutCurrentBack();
+ return true;
+ }
+ }
+ }
+
+ private void ImplicitExpression()
+ {
+ Context.CurrentBlock.Type = BlockType.Expression;
+ Context.CurrentBlock.CodeGenerator = new ExpressionCodeGenerator();
+ using (PushSpanConfig(ImplictExpressionSpanConfig))
+ {
+ Expected(VBSymbolType.Identifier, VBSymbolType.Keyword);
+ Span.CodeGenerator = new ExpressionCodeGenerator();
+ while (!EndOfFile)
+ {
+ switch (CurrentSymbol.Type)
+ {
+ case VBSymbolType.LeftParenthesis:
+ SourceLocation start = CurrentLocation;
+ AcceptAndMoveNext();
+
+ Action<SpanBuilder> oldConfig = SpanConfig;
+ using (PushSpanConfig())
+ {
+ ConfigureSpan(span =>
+ {
+ oldConfig(span);
+ span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any;
+ });
+ Balance(BalancingModes.AllowCommentsAndTemplates,
+ VBSymbolType.LeftParenthesis,
+ VBSymbolType.RightParenthesis,
+ start);
+ }
+ if (Optional(VBSymbolType.RightParenthesis))
+ {
+ Span.EditHandler.AcceptedCharacters = AcceptedCharacters.NonWhiteSpace;
+ }
+ break;
+ case VBSymbolType.Dot:
+ VBSymbol dot = CurrentSymbol;
+ NextToken();
+ if (At(VBSymbolType.Identifier) || At(VBSymbolType.Keyword))
+ {
+ Accept(dot);
+ AcceptAndMoveNext();
+ }
+ else if (At(VBSymbolType.Transition))
+ {
+ VBSymbol at = CurrentSymbol;
+ NextToken();
+ if (At(VBSymbolType.Identifier) || At(VBSymbolType.Keyword))
+ {
+ Accept(dot);
+ Accept(at);
+ AcceptAndMoveNext();
+ }
+ else
+ {
+ PutBack(at);
+ PutBack(dot);
+ }
+ }
+ else
+ {
+ PutCurrentBack();
+ if (IsNested)
+ {
+ Accept(dot);
+ }
+ else
+ {
+ PutBack(dot);
+ }
+ return;
+ }
+ break;
+ default:
+ PutCurrentBack();
+ return;
+ }
+ }
+ }
+ }
+
+ protected void MapKeyword(VBKeyword keyword, Func<bool> action)
+ {
+ _keywordHandlers[keyword] = action;
+ Keywords.Add(keyword.ToString());
+ }
+
+ protected void MapDirective(string directive, Func<bool> action)
+ {
+ _directiveHandlers[directive] = action;
+ Keywords.Add(directive);
+ }
+
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This only occurs in Release builds, where this method is empty by design")]
+ [Conditional("DEBUG")]
+ protected void Assert(VBKeyword keyword)
+ {
+ Debug.Assert(CurrentSymbol.Type == VBSymbolType.Keyword && CurrentSymbol.Keyword == keyword);
+ }
+
+ protected bool At(VBKeyword keyword)
+ {
+ return At(VBSymbolType.Keyword) && CurrentSymbol.Keyword == keyword;
+ }
+
+ protected void OtherParserBlock()
+ {
+ OtherParserBlock(null, null);
+ }
+
+ protected void OtherParserBlock(string startSequence, string endSequence)
+ {
+ using (PushSpanConfig())
+ {
+ if (Span.Symbols.Count > 0)
+ {
+ Output(SpanKind.Code);
+ }
+
+ Context.SwitchActiveParser();
+
+ bool old = DirectParentIsCode;
+ DirectParentIsCode = false;
+
+ Debug.Assert(ReferenceEquals(Context.ActiveParser, Context.MarkupParser));
+ if (!String.IsNullOrEmpty(startSequence) || !String.IsNullOrEmpty(endSequence))
+ {
+ Context.MarkupParser.ParseSection(Tuple.Create(startSequence, endSequence), false);
+ }
+ else
+ {
+ Context.MarkupParser.ParseBlock();
+ }
+
+ DirectParentIsCode = old;
+
+ Context.SwitchActiveParser();
+ EnsureCurrent();
+ }
+ Initialize(Span);
+ }
+
+ protected void HandleTransition(VBSymbol lastWhiteSpace)
+ {
+ if (At(VBSymbolType.RazorCommentTransition))
+ {
+ Accept(lastWhiteSpace);
+ RazorComment();
+ return;
+ }
+
+ // Check the next character
+ VBSymbol transition = CurrentSymbol;
+ NextToken();
+ if (At(VBSymbolType.LessThan) || At(VBSymbolType.Colon))
+ {
+ // Put the transition back
+ PutCurrentBack();
+ PutBack(transition);
+
+ // If we're in design-time mode, accept the whitespace, otherwise put it back
+ if (Context.DesignTimeMode)
+ {
+ Accept(lastWhiteSpace);
+ }
+ else
+ {
+ PutBack(lastWhiteSpace);
+ }
+
+ // Switch to markup
+ OtherParserBlock();
+ }
+ else if (At(VBSymbolType.Transition))
+ {
+ if (Context.IsWithin(BlockType.Template))
+ {
+ Context.OnError(transition.Start, RazorResources.ParseError_InlineMarkup_Blocks_Cannot_Be_Nested);
+ }
+ Accept(lastWhiteSpace);
+ VBSymbol transition2 = CurrentSymbol;
+ NextToken();
+ if (At(VBSymbolType.LessThan) || At(VBSymbolType.Colon))
+ {
+ PutCurrentBack();
+ PutBack(transition2);
+ PutBack(transition);
+ Output(SpanKind.Code);
+
+ // Start a template block and switch to Markup
+ using (Context.StartBlock(BlockType.Template))
+ {
+ Context.CurrentBlock.CodeGenerator = new TemplateBlockCodeGenerator();
+ OtherParserBlock();
+ Initialize(Span);
+ }
+ }
+ else
+ {
+ Accept(transition);
+ Accept(transition2);
+ }
+ }
+ else
+ {
+ Accept(lastWhiteSpace);
+
+ PutCurrentBack();
+ PutBack(transition);
+
+ bool old = IsNested;
+ IsNested = true;
+ NestedBlock();
+ IsNested = old;
+ }
+ }
+
+ protected override void OutputSpanBeforeRazorComment()
+ {
+ Output(SpanKind.Code);
+ }
+
+ protected bool ReservedWord()
+ {
+ Context.CurrentBlock.Type = BlockType.Directive;
+ Context.OnError(CurrentLocation, RazorResources.ParseError_ReservedWord, CurrentSymbol.Content);
+ Span.CodeGenerator = SpanCodeGenerator.Null;
+ AcceptAndMoveNext();
+ Output(SpanKind.MetaCode, AcceptedCharacters.None);
+ return true;
+ }
+
+ protected void NestedBlock()
+ {
+ using (PushSpanConfig())
+ {
+ Output(SpanKind.Code);
+
+ bool old = DirectParentIsCode;
+ DirectParentIsCode = true;
+
+ ParseBlock();
+
+ DirectParentIsCode = old;
+ }
+ Initialize(Span);
+ }
+
+ protected bool Required(VBSymbolType expected, string errorBase)
+ {
+ if (!Optional(expected))
+ {
+ Context.OnError(CurrentLocation, errorBase, GetCurrentSymbolDisplay());
+ return false;
+ }
+ return true;
+ }
+
+ protected bool Optional(VBKeyword keyword)
+ {
+ if (At(keyword))
+ {
+ AcceptAndMoveNext();
+ return true;
+ }
+ return false;
+ }
+
+ protected void AcceptVBSpaces()
+ {
+ Accept(ReadVBSpacesLazy());
+ }
+
+ protected IEnumerable<VBSymbol> ReadVBSpaces()
+ {
+ return ReadVBSpacesLazy().ToList();
+ }
+
+ public bool IsDirectiveDefined(string directive)
+ {
+ return _directiveHandlers.ContainsKey(directive);
+ }
+
+ private IEnumerable<VBSymbol> ReadVBSpacesLazy()
+ {
+ foreach (var symbol in ReadWhileLazy(sym => sym.Type == VBSymbolType.WhiteSpace))
+ {
+ yield return symbol;
+ }
+ while (At(VBSymbolType.LineContinuation))
+ {
+ int bookmark = CurrentLocation.AbsoluteIndex;
+ VBSymbol under = CurrentSymbol;
+ NextToken();
+ if (At(VBSymbolType.NewLine))
+ {
+ yield return under;
+ yield return CurrentSymbol;
+ NextToken();
+ foreach (var symbol in ReadVBSpaces())
+ {
+ yield return symbol;
+ }
+ }
+ else
+ {
+ Context.Source.Position = bookmark;
+ NextToken();
+ yield break;
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Parser/VBLanguageCharacteristics.cs b/src/System.Web.Razor/Parser/VBLanguageCharacteristics.cs
new file mode 100644
index 00000000..70e7a81b
--- /dev/null
+++ b/src/System.Web.Razor/Parser/VBLanguageCharacteristics.cs
@@ -0,0 +1,88 @@
+using System.Collections.Generic;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Text;
+using System.Web.Razor.Tokenizer;
+using System.Web.Razor.Tokenizer.Symbols;
+
+namespace System.Web.Razor.Parser
+{
+ public class VBLanguageCharacteristics : LanguageCharacteristics<VBTokenizer, VBSymbol, VBSymbolType>
+ {
+ private static readonly VBLanguageCharacteristics _instance = new VBLanguageCharacteristics();
+
+ private VBLanguageCharacteristics()
+ {
+ }
+
+ public static VBLanguageCharacteristics Instance
+ {
+ get { return _instance; }
+ }
+
+ public override VBTokenizer CreateTokenizer(ITextDocument source)
+ {
+ return new VBTokenizer(source);
+ }
+
+ public override string GetSample(VBSymbolType type)
+ {
+ return VBSymbol.GetSample(type);
+ }
+
+ public override VBSymbolType FlipBracket(VBSymbolType bracket)
+ {
+ switch (bracket)
+ {
+ case VBSymbolType.LeftBrace:
+ return VBSymbolType.RightBrace;
+ case VBSymbolType.LeftBracket:
+ return VBSymbolType.RightBracket;
+ case VBSymbolType.LeftParenthesis:
+ return VBSymbolType.RightParenthesis;
+ case VBSymbolType.RightBrace:
+ return VBSymbolType.LeftBrace;
+ case VBSymbolType.RightBracket:
+ return VBSymbolType.LeftBracket;
+ case VBSymbolType.RightParenthesis:
+ return VBSymbolType.LeftParenthesis;
+ default:
+ return VBSymbolType.Unknown;
+ }
+ }
+
+ public override VBSymbol CreateMarkerSymbol(SourceLocation location)
+ {
+ return new VBSymbol(location, String.Empty, VBSymbolType.Unknown);
+ }
+
+ public override VBSymbolType GetKnownSymbolType(KnownSymbolType type)
+ {
+ switch (type)
+ {
+ case KnownSymbolType.CommentStart:
+ return VBSymbolType.RazorCommentTransition;
+ case KnownSymbolType.CommentStar:
+ return VBSymbolType.RazorCommentStar;
+ case KnownSymbolType.CommentBody:
+ return VBSymbolType.RazorComment;
+ case KnownSymbolType.Identifier:
+ return VBSymbolType.Identifier;
+ case KnownSymbolType.Keyword:
+ return VBSymbolType.Keyword;
+ case KnownSymbolType.NewLine:
+ return VBSymbolType.NewLine;
+ case KnownSymbolType.Transition:
+ return VBSymbolType.Transition;
+ case KnownSymbolType.WhiteSpace:
+ return VBSymbolType.WhiteSpace;
+ default:
+ return VBSymbolType.Unknown;
+ }
+ }
+
+ protected override VBSymbol CreateSymbol(SourceLocation location, string content, VBSymbolType type, IEnumerable<RazorError> errors)
+ {
+ return new VBSymbol(location, content, type, errors);
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Parser/WhitespaceRewriter.cs b/src/System.Web.Razor/Parser/WhitespaceRewriter.cs
new file mode 100644
index 00000000..4c6306d8
--- /dev/null
+++ b/src/System.Web.Razor/Parser/WhitespaceRewriter.cs
@@ -0,0 +1,74 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Text;
+
+namespace System.Web.Razor.Parser
+{
+ internal class WhiteSpaceRewriter : MarkupRewriter
+ {
+ public WhiteSpaceRewriter(Action<SpanBuilder, SourceLocation, string> markupSpanFactory) : base(markupSpanFactory)
+ {
+ }
+
+ protected override bool CanRewrite(Block block)
+ {
+ return block.Type == BlockType.Expression && Parent != null;
+ }
+
+ //public override void VisitBlock(Block block)
+ //{
+ // BlockBuilder parent = null;
+ // if (_blocks.Count > 0)
+ // {
+ // parent = _blocks.Peek();
+ // }
+ // BlockBuilder newBlock = new BlockBuilder(block);
+ // newBlock.Children.Clear();
+ // _blocks.Push(newBlock);
+ // if (block.Type == BlockType.Expression && parent != null)
+ // {
+ // VisitExpressionBlock(block, parent);
+ // }
+ // else
+ // {
+ // base.VisitBlock(block);
+ // }
+ // if (_blocks.Count > 1)
+ // {
+ // parent.Children.Add(_blocks.Pop().Build());
+ // }
+ //}
+
+ //public override void VisitSpan(Span span)
+ //{
+ // Debug.Assert(_blocks.Count > 0);
+ // _blocks.Peek().Children.Add(span);
+ //}
+
+ protected override SyntaxTreeNode RewriteBlock(BlockBuilder parent, Block block)
+ {
+ BlockBuilder newBlock = new BlockBuilder(block);
+ newBlock.Children.Clear();
+ Span ws = block.Children.FirstOrDefault() as Span;
+ IEnumerable<SyntaxTreeNode> newNodes = block.Children;
+ if (ws.Content.All(Char.IsWhiteSpace))
+ {
+ // Add this node to the parent
+ SpanBuilder builder = new SpanBuilder(ws);
+ builder.ClearSymbols();
+ FillSpan(builder, ws.Start, ws.Content);
+ parent.Children.Add(builder.Build());
+
+ // Remove the old whitespace node
+ newNodes = block.Children.Skip(1);
+ }
+
+ foreach (SyntaxTreeNode node in newNodes)
+ {
+ newBlock.Children.Add(node);
+ }
+ return newBlock.Build();
+ }
+ }
+}
diff --git a/src/System.Web.Razor/ParserResults.cs b/src/System.Web.Razor/ParserResults.cs
new file mode 100644
index 00000000..4c01429b
--- /dev/null
+++ b/src/System.Web.Razor/ParserResults.cs
@@ -0,0 +1,38 @@
+using System.Collections.Generic;
+using System.Web.Razor.Parser.SyntaxTree;
+
+namespace System.Web.Razor
+{
+ /// <summary>
+ /// Represents the results of parsing a Razor document
+ /// </summary>
+ public class ParserResults
+ {
+ public ParserResults(Block document, IList<RazorError> parserErrors)
+ : this(parserErrors == null || parserErrors.Count == 0, document, parserErrors)
+ {
+ }
+
+ protected ParserResults(bool success, Block document, IList<RazorError> errors)
+ {
+ Success = success;
+ Document = document;
+ ParserErrors = errors ?? new List<RazorError>();
+ }
+
+ /// <summary>
+ /// Indicates if parsing was successful (no errors)
+ /// </summary>
+ public bool Success { get; private set; }
+
+ /// <summary>
+ /// The root node in the document's syntax tree
+ /// </summary>
+ public Block Document { get; private set; }
+
+ /// <summary>
+ /// The list of errors which occurred during parsing.
+ /// </summary>
+ public IList<RazorError> ParserErrors { get; private set; }
+ }
+}
diff --git a/src/System.Web.Razor/PartialParseResult.cs b/src/System.Web.Razor/PartialParseResult.cs
new file mode 100644
index 00000000..694fae81
--- /dev/null
+++ b/src/System.Web.Razor/PartialParseResult.cs
@@ -0,0 +1,57 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Web.Razor
+{
+ // Flags:
+ // Provisional, ContextChanged, Accepted, Rejected
+ // 000001 1 - Rejected,
+ // 000010 2 - Accepted
+ // 000100 4 - Provisional
+ // 001000 8 - Context Changed
+ // 010000 16 - Auto Complete Block
+
+ /// <summary>
+ /// The result of attempting an incremental parse
+ /// </summary>
+ /// <remarks>
+ /// Either the Accepted or Rejected flag is ALWAYS set.
+ /// Additionally, Provisional may be set with Accepted and SpanContextChanged may be set with Rejected.
+ /// Provisional may NOT be set with Rejected and SpanContextChanged may NOT be set with Accepted.
+ /// </remarks>
+ [Flags]
+ [SuppressMessage("Microsoft.Naming", "CA1714:FlagsEnumsShouldHavePluralNames", Justification = "The singular name is more appropriate here")]
+ public enum PartialParseResult
+ {
+ /// <summary>
+ /// Indicates that the edit could not be accepted and that a reparse is underway.
+ /// </summary>
+ Rejected = 1,
+
+ /// <summary>
+ /// Indicates that the edit was accepted and has been added to the parse tree
+ /// </summary>
+ Accepted = 2,
+
+ /// <summary>
+ /// Indicates that the edit was accepted, but that a reparse should be forced when idle time is available
+ /// since the edit may be misclassified
+ /// </summary>
+ /// <remarks>
+ /// This generally occurs when a "." is typed in an Implicit Expression, since editors require that this
+ /// be assigned to Code in order to properly support features like IntelliSense. However, if no further edits
+ /// occur following the ".", it should be treated as Markup.
+ /// </remarks>
+ Provisional = 4,
+
+ /// <summary>
+ /// Indicates that the edit caused a change in the span's context and that if any statement completions were active prior to starting this
+ /// partial parse, they should be reinitialized.
+ /// </summary>
+ SpanContextChanged = 8,
+
+ /// <summary>
+ /// Indicates that the edit requires an auto completion to occur
+ /// </summary>
+ AutoCompleteBlock = 16
+ }
+}
diff --git a/src/System.Web.Razor/Properties/AssemblyInfo.cs b/src/System.Web.Razor/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..0e73b1c0
--- /dev/null
+++ b/src/System.Web.Razor/Properties/AssemblyInfo.cs
@@ -0,0 +1,12 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+
+[assembly: AssemblyTitle("System.Web.Razor")]
+[assembly: AssemblyDescription("")]
+[assembly: InternalsVisibleTo("System.Web.Razor.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
+[assembly: InternalsVisibleTo("System.Web.WebPages.Razor.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
+[assembly: InternalsVisibleTo("Microsoft.WebPages.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
diff --git a/src/System.Web.Razor/RazorCodeLanguage.cs b/src/System.Web.Razor/RazorCodeLanguage.cs
new file mode 100644
index 00000000..f3a134a2
--- /dev/null
+++ b/src/System.Web.Razor/RazorCodeLanguage.cs
@@ -0,0 +1,58 @@
+using System.Collections.Generic;
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser;
+
+namespace System.Web.Razor
+{
+ /// <summary>
+ /// Represents a code language in Razor.
+ /// </summary>
+ public abstract class RazorCodeLanguage
+ {
+ private static IDictionary<string, RazorCodeLanguage> _services = new Dictionary<string, RazorCodeLanguage>(StringComparer.OrdinalIgnoreCase)
+ {
+ { "cshtml", new CSharpRazorCodeLanguage() },
+ { "vbhtml", new VBRazorCodeLanguage() }
+ };
+
+ /// <summary>
+ /// Gets the list of registered languages mapped to file extensions (without a ".")
+ /// </summary>
+ public static IDictionary<string, RazorCodeLanguage> Languages
+ {
+ get { return _services; }
+ }
+
+ /// <summary>
+ /// The name of the language (for use in System.Web.Compilation.BuildProvider.GetDefaultCompilerTypeForLanguage)
+ /// </summary>
+ public abstract string LanguageName { get; }
+
+ /// <summary>
+ /// The type of the CodeDOM provider for this language
+ /// </summary>
+ public abstract Type CodeDomProviderType { get; }
+
+ /// <summary>
+ /// Gets the RazorCodeLanguage registered for the specified file extension
+ /// </summary>
+ /// <param name="fileExtension">The extension, with or without a "."</param>
+ /// <returns>The language registered for that extension</returns>
+ public static RazorCodeLanguage GetLanguageByExtension(string fileExtension)
+ {
+ RazorCodeLanguage service = null;
+ Languages.TryGetValue(fileExtension.TrimStart('.'), out service);
+ return service;
+ }
+
+ /// <summary>
+ /// Constructs the code parser. Must return a new instance on EVERY call to ensure thread-safety
+ /// </summary>
+ public abstract ParserBase CreateCodeParser();
+
+ /// <summary>
+ /// Constructs the code generator. Must return a new instance on EVERY call to ensure thread-safety
+ /// </summary>
+ public abstract RazorCodeGenerator CreateCodeGenerator(string className, string rootNamespaceName, string sourceFileName, RazorEngineHost host);
+ }
+}
diff --git a/src/System.Web.Razor/RazorDebugHelpers.cs b/src/System.Web.Razor/RazorDebugHelpers.cs
new file mode 100644
index 00000000..70942949
--- /dev/null
+++ b/src/System.Web.Razor/RazorDebugHelpers.cs
@@ -0,0 +1,197 @@
+#if DEBUG
+
+using System.CodeDom;
+using System.CodeDom.Compiler;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Text;
+
+namespace System.Web.Razor
+{
+ internal static class RazorDebugHelpers
+ {
+ private static bool _outputDebuggingEnabled = IsDebuggingEnabled();
+
+ private static readonly Dictionary<char, string> _printableEscapeChars = new Dictionary<char, string>
+ {
+ { '\0', "\\0" },
+ { '\\', "\\\\" },
+ { '\'', "'" },
+ { '\"', "\\\"" },
+ { '\a', "\\a" },
+ { '\b', "\\b" },
+ { '\f', "\\f" },
+ { '\n', "\\n" },
+ { '\r', "\\r" },
+ { '\t', "\\t" },
+ { '\v', "\\v" },
+ };
+
+ internal static bool OutputDebuggingEnabled
+ {
+ get { return _outputDebuggingEnabled; }
+ }
+
+ [SuppressMessage("Microsoft.Security", "CA2141:TransparentMethodsMustNotSatisfyLinkDemandsFxCopRule", Justification = "This is debug only")]
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "This is debug only")]
+ [SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider", MessageId = "System.IO.StringWriter.#ctor", Justification = "This is debug only")]
+ internal static void WriteGeneratedCode(string sourceFile, CodeCompileUnit codeCompileUnit)
+ {
+ if (!OutputDebuggingEnabled)
+ {
+ return;
+ }
+
+ RunTask(() =>
+ {
+ string extension = Path.GetExtension(sourceFile);
+ RazorCodeLanguage language = RazorCodeLanguage.GetLanguageByExtension(extension);
+ CodeDomProvider provider = CodeDomProvider.CreateProvider(language.LanguageName);
+
+ using (var writer = new StringWriter())
+ {
+ // Trim the html part of cshtml or vbhtml
+ string outputExtension = extension.Substring(0, 3);
+ string outputFileName = Normalize(sourceFile) + "_generated" + outputExtension;
+ string outputPath = Path.Combine(Path.GetDirectoryName(sourceFile), outputFileName);
+
+ // REVIEW: Do these options need to be tweaked?
+ provider.GenerateCodeFromCompileUnit(codeCompileUnit, writer, new CodeGeneratorOptions());
+ File.WriteAllText(outputPath, writer.ToString());
+ }
+ });
+ }
+
+ internal static void WriteDebugTree(string sourceFile, Block document, PartialParseResult result, TextChange change, RazorEditorParser parser, bool treeStructureChanged)
+ {
+ if (!OutputDebuggingEnabled)
+ {
+ return;
+ }
+
+ RunTask(() =>
+ {
+ string outputFileName = Normalize(sourceFile) + "_tree";
+ string outputPath = Path.Combine(Path.GetDirectoryName(sourceFile), outputFileName);
+
+ var treeBuilder = new StringBuilder();
+ WriteTree(document, treeBuilder);
+ treeBuilder.AppendLine();
+ treeBuilder.AppendFormat(CultureInfo.CurrentCulture, "Last Change: {0}", change);
+ treeBuilder.AppendLine();
+ treeBuilder.AppendFormat(CultureInfo.CurrentCulture, "Normalized To: {0}", change.Normalize());
+ treeBuilder.AppendLine();
+ treeBuilder.AppendFormat(CultureInfo.CurrentCulture, "Partial Parse Result: {0}", result);
+ treeBuilder.AppendLine();
+ if (result.HasFlag(PartialParseResult.Rejected))
+ {
+ treeBuilder.AppendFormat(CultureInfo.CurrentCulture, "Tree Structure Changed: {0}", treeStructureChanged);
+ treeBuilder.AppendLine();
+ }
+ if (result.HasFlag(PartialParseResult.AutoCompleteBlock))
+ {
+ treeBuilder.AppendFormat(CultureInfo.CurrentCulture, "Auto Complete Insert String: \"{0}\"", parser.GetAutoCompleteString());
+ treeBuilder.AppendLine();
+ }
+ File.WriteAllText(outputPath, treeBuilder.ToString());
+ });
+ }
+
+ private static void WriteTree(SyntaxTreeNode node, StringBuilder treeBuilder, int depth = 0)
+ {
+ if (node == null)
+ {
+ return;
+ }
+ if (depth > 1)
+ {
+ WriteIndent(treeBuilder, depth);
+ }
+
+ if (depth > 0)
+ {
+ treeBuilder.Append("|-- ");
+ }
+
+ treeBuilder.AppendLine(ConvertEscapseSequences(node.ToString()));
+ if (node.IsBlock)
+ {
+ foreach (SyntaxTreeNode child in ((Block)node).Children)
+ {
+ WriteTree(child, treeBuilder, depth + 1);
+ }
+ }
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "This is debug only")]
+ [SuppressMessage("Microsoft.WebAPI", "CR4000:DoNotUseProblematicTaskTypes", Justification = "This rule is not applicable to this assembly.")]
+ [SuppressMessage("Microsoft.WebAPI", "CR4001:DoNotCallProblematicMethodsOnTask", Justification = "This rule is not applicable to this assembly.")]
+ private static void RunTask(Action action)
+ {
+ Task.Factory.StartNew(() =>
+ {
+ try
+ {
+ action();
+ }
+ catch
+ {
+ // Catch all errors since this is just a debug helper
+ }
+ });
+ }
+
+ private static void WriteIndent(StringBuilder sb, int depth)
+ {
+ for (int i = 0; i < (depth - 1) * 4; ++i)
+ {
+ if (i % 4 == 0)
+ {
+ sb.Append("|");
+ }
+ else
+ {
+ sb.Append(" ");
+ }
+ }
+ }
+
+ private static string Normalize(string path)
+ {
+ return Path.GetFileName(path).Replace('.', '_');
+ }
+
+ private static string ConvertEscapseSequences(string value)
+ {
+ StringBuilder sb = new StringBuilder();
+ foreach (var ch in value)
+ {
+ sb.Append(GetCharValue(ch));
+ }
+ return sb.ToString();
+ }
+
+ private static string GetCharValue(char ch)
+ {
+ string value;
+ if (_printableEscapeChars.TryGetValue(ch, out value))
+ {
+ return value;
+ }
+ return ch.ToString();
+ }
+
+ private static bool IsDebuggingEnabled()
+ {
+ bool enabled;
+ return Boolean.TryParse(Environment.GetEnvironmentVariable("RAZOR_DEBUG"), out enabled) && enabled;
+ }
+ }
+}
+
+#endif
diff --git a/src/System.Web.Razor/RazorDirectiveAttribute.cs b/src/System.Web.Razor/RazorDirectiveAttribute.cs
new file mode 100644
index 00000000..d35ed96e
--- /dev/null
+++ b/src/System.Web.Razor/RazorDirectiveAttribute.cs
@@ -0,0 +1,47 @@
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.Razor
+{
+ /// <summary>
+ /// Specifies a Razor directive that is rendered as an attribute on the generated class.
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
+ public sealed class RazorDirectiveAttribute : Attribute
+ {
+ private readonly object _typeId = new object();
+
+ public RazorDirectiveAttribute(string name, string value)
+ {
+ if (String.IsNullOrEmpty(name))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "name");
+ }
+
+ Name = name;
+ Value = value;
+ }
+
+ public override object TypeId
+ {
+ get { return _typeId; }
+ }
+
+ public string Name { get; private set; }
+
+ public string Value { get; private set; }
+
+ public override bool Equals(object obj)
+ {
+ RazorDirectiveAttribute attribute = obj as RazorDirectiveAttribute;
+ return attribute != null &&
+ Name.Equals(attribute.Name, StringComparison.OrdinalIgnoreCase) &&
+ StringComparer.OrdinalIgnoreCase.Equals(Value, attribute.Value);
+ }
+
+ public override int GetHashCode()
+ {
+ return (StringComparer.OrdinalIgnoreCase.GetHashCode(Name) * 31) +
+ (Value == null ? 0 : StringComparer.OrdinalIgnoreCase.GetHashCode(Value));
+ }
+ }
+}
diff --git a/src/System.Web.Razor/RazorEditorParser.cs b/src/System.Web.Razor/RazorEditorParser.cs
new file mode 100644
index 00000000..816ecaa0
--- /dev/null
+++ b/src/System.Web.Razor/RazorEditorParser.cs
@@ -0,0 +1,366 @@
+using System.CodeDom;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using System.Web.Razor.Editor;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Text;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.Razor
+{
+ /// <summary>
+ /// Parser used by editors to avoid reparsing the entire document on each text change
+ /// </summary>
+ /// <remarks>
+ /// This parser is designed to allow editors to avoid having to worry about incremental parsing.
+ /// The CheckForStructureChanges method can be called with every change made by a user in an editor and
+ /// the parser will provide a result indicating if it was able to incrementally reparse the document.
+ ///
+ /// The general workflow for editors with this parser is:
+ /// 0. User edits document
+ /// 1. Editor builds TextChange structure describing the edit and providing a reference to the _updated_ text buffer
+ /// 2. Editor calls CheckForStructureChanges passing in that change.
+ /// 3. Parser determines if the change can be simply applied to an existing parse tree node
+ /// a. If it can, the Parser updates its parse tree and returns PartialParseResult.Accepted
+ /// b. If it can not, the Parser starts a background parse task and return PartialParseResult.Rejected
+ /// NOTE: Additional flags can be applied to the PartialParseResult, see that enum for more details. However,
+ /// the Accepted or Rejected flags will ALWAYS be present
+ ///
+ /// A change can only be incrementally parsed if a single, unique, Span (see System.Web.Razor.Parser.SyntaxTree) in the syntax tree can
+ /// be identified as owning the entire change. For example, if a change overlaps with multiple spans, the change cannot be
+ /// parsed incrementally and a full reparse is necessary. A Span "owns" a change if the change occurs either a) entirely
+ /// within it's boundaries or b) it is a pure insertion (see TextChange) at the end of a Span whose CanGrow flag (see Span) is
+ /// true.
+ ///
+ /// Even if a single unique Span owner can be identified, it's possible the edit will cause the Span to split or merge with other
+ /// Spans, in which case, a full reparse is necessary to identify the extent of the changes to the tree.
+ ///
+ /// When the RazorEditorParser returns Accepted, it updates CurrentParseTree immediately. However, the editor is expected to
+ /// update it's own data structures independently. It can use CurrentParseTree to do this, as soon as the editor returns from
+ /// CheckForStructureChanges, but it should (ideally) have logic for doing so without needing the new tree.
+ ///
+ /// When Rejected is returned by CheckForStructureChanges, a background parse task has _already_ been started. When that task
+ /// finishes, the DocumentStructureChanged event will be fired containing the new generated code, parse tree and a reference to
+ /// the original TextChange that caused the reparse, to allow the editor to resolve the new tree against any changes made since
+ /// calling CheckForStructureChanges.
+ ///
+ /// If a call to CheckForStructureChanges occurs while a reparse is already in-progress, the reparse is cancelled IMMEDIATELY
+ /// and Rejected is returned without attempting to reparse. This means that if a conusmer calls CheckForStructureChanges, which
+ /// returns Rejected, then calls it again before DocumentParseComplete is fired, it will only recieve one DocumentParseComplete
+ /// event, for the second change.
+ /// </remarks>
+ public class RazorEditorParser : IDisposable
+ {
+ // Lock for this document
+ private object _lock = new object();
+ private Stack<BackgroundParseTask> _outstandingParserTasks = new Stack<BackgroundParseTask>();
+ private Span _lastChangeOwner;
+ private Span _lastAutoCompleteSpan;
+#if DEBUG
+ private CodeCompileUnit _currentCompileUnit;
+#endif
+
+ /// <summary>
+ /// Constructs the editor parser. One instance should be used per active editor. This
+ /// instance _can_ be shared among reparses, but should _never_ be shared between documents.
+ /// </summary>
+ /// <param name="host">The <see cref="RazorEngineHost"/> which defines the environment in which the generated code will live. <see cref="F:RazorEngineHost.DesignTimeMode"/> should be set if design-time code mappings are desired</param>
+ /// <param name="sourceFileName">The physical path to use in line pragmas</param>
+ public RazorEditorParser(RazorEngineHost host, string sourceFileName)
+ {
+ if (host == null)
+ {
+ throw new ArgumentNullException("host");
+ }
+ if (String.IsNullOrEmpty(sourceFileName))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "sourceFileName");
+ }
+
+ Host = host;
+ FileName = sourceFileName;
+ }
+
+ /// <summary>
+ /// Event fired when a full reparse of the document completes
+ /// </summary>
+ public event EventHandler<DocumentParseCompleteEventArgs> DocumentParseComplete;
+
+ public RazorEngineHost Host { get; private set; }
+ public string FileName { get; private set; }
+ public bool LastResultProvisional { get; private set; }
+ public Block CurrentParseTree { get; private set; }
+
+ [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Since this method is heavily affected by side-effects, particularly calls to CheckForStructureChanges, it should not be made into a property")]
+ public virtual string GetAutoCompleteString()
+ {
+ if (_lastAutoCompleteSpan != null)
+ {
+ AutoCompleteEditHandler editHandler = _lastAutoCompleteSpan.EditHandler as AutoCompleteEditHandler;
+ if (editHandler != null)
+ {
+ return editHandler.AutoCompleteString;
+ }
+ }
+ return null;
+ }
+
+ /// <summary>
+ /// Determines if a change will cause a structural change to the document and if not, applies it to the existing tree.
+ /// If a structural change would occur, automatically starts a reparse
+ /// </summary>
+ /// <remarks>
+ /// NOTE: The initial incremental parsing check and actual incremental parsing (if possible) occurs
+ /// on the callers thread. However, if a full reparse is needed, this occurs on a background thread.
+ /// </remarks>
+ /// <param name="change">The change to apply to the parse tree</param>
+ /// <returns>A PartialParseResult value indicating the result of the incremental parse</returns>
+ public virtual PartialParseResult CheckForStructureChanges(TextChange change)
+ {
+ // Validate the change
+ if (change.NewBuffer == null)
+ {
+ throw new ArgumentException(String.Format(CultureInfo.CurrentUICulture,
+ RazorResources.Structure_Member_CannotBeNull,
+ "Buffer",
+ "TextChange"), "change");
+ }
+
+ PartialParseResult result = PartialParseResult.Rejected;
+
+ // Lock the state objects
+ lock (_lock)
+ {
+ // If there isn't already a parse underway, try partial-parsing
+ if (CurrentParseTree != null && _outstandingParserTasks.Count == 0)
+ {
+ result = TryPartialParse(change);
+ }
+
+ // If partial parsing failed or there were outstanding parser tasks, start a full reparse
+ if (result.HasFlag(PartialParseResult.Rejected))
+ {
+ QueueFullReparse(change);
+ }
+#if DEBUG
+ else
+ {
+ if (CurrentParseTree != null)
+ {
+ RazorDebugHelpers.WriteDebugTree(FileName, CurrentParseTree, result, change, this, false);
+ }
+ if (_currentCompileUnit != null)
+ {
+ RazorDebugHelpers.WriteGeneratedCode(FileName, _currentCompileUnit);
+ }
+ }
+#endif
+
+ // Otherwise, remember if this was provisionally accepted for next partial parse
+ LastResultProvisional = result.HasFlag(PartialParseResult.Provisional);
+ }
+ VerifyFlagsAreValid(result);
+ return result;
+ }
+
+ /// <summary>
+ /// Disposes of this parser. Should be called when the editor window is closed and the document is unloaded.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "_cancelTokenSource", Justification = "The cancellation token is owned by the worker thread, so it is disposed there")]
+ [SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "_changeReceived", Justification = "The change received event is owned by the worker thread, so it is disposed there")]
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ lock (_lock)
+ {
+ if (_outstandingParserTasks.Count > 0)
+ {
+ foreach (var task in _outstandingParserTasks)
+ {
+ task.Dispose();
+ }
+ }
+ }
+ }
+ }
+
+ private void QueueFullReparse(TextChange change)
+ {
+ if (_outstandingParserTasks.Count > 0)
+ {
+ _outstandingParserTasks.Peek().Cancel();
+ }
+ _outstandingParserTasks.Push(CreateBackgroundTask(Host, FileName, change).ContinueWith(OnParseCompleted));
+ }
+
+ private PartialParseResult TryPartialParse(TextChange change)
+ {
+ PartialParseResult result = PartialParseResult.Rejected;
+
+ // Try the last change owner
+ if (_lastChangeOwner != null && _lastChangeOwner.EditHandler.OwnsChange(_lastChangeOwner, change))
+ {
+ EditResult editResult = _lastChangeOwner.EditHandler.ApplyChange(_lastChangeOwner, change);
+ result = editResult.Result;
+ if (!editResult.Result.HasFlag(PartialParseResult.Rejected))
+ {
+ _lastChangeOwner.ReplaceWith(editResult.EditedSpan);
+ }
+
+ // If the last change was provisional, then the result of this span's attempt to parse partially goes
+ // Otherwise, accept the change if this span accepted it, but if it didn't, just do the standard search.
+ if (LastResultProvisional || result.HasFlag(PartialParseResult.Accepted))
+ {
+ return result;
+ }
+ }
+
+ // Locate the span responsible for this change
+ _lastChangeOwner = CurrentParseTree.LocateOwner(change);
+
+ if (LastResultProvisional)
+ {
+ // Last change owner couldn't accept this, so we must do a full reparse
+ result = PartialParseResult.Rejected;
+ }
+ else if (_lastChangeOwner != null)
+ {
+ EditResult editRes = _lastChangeOwner.EditHandler.ApplyChange(_lastChangeOwner, change);
+ result = editRes.Result;
+ if (!editRes.Result.HasFlag(PartialParseResult.Rejected))
+ {
+ _lastChangeOwner.ReplaceWith(editRes.EditedSpan);
+ }
+ if (result.HasFlag(PartialParseResult.AutoCompleteBlock))
+ {
+ _lastAutoCompleteSpan = _lastChangeOwner;
+ }
+ else
+ {
+ _lastAutoCompleteSpan = null;
+ }
+ }
+ return result;
+ }
+
+ private void OnParseCompleted(GeneratorResults results, BackgroundParseTask parseTask)
+ {
+ try
+ {
+ // Lock the state objects
+ bool treeStructureChanged = true;
+ TextChange lastChange;
+ lock (_lock)
+ {
+ // Are we still active?
+ if (_outstandingParserTasks.Count == 0 || !ReferenceEquals(parseTask, _outstandingParserTasks.Peek()))
+ {
+ // We aren't, just abort
+ return;
+ }
+
+ // Ok, we're active. Flush out the changes from all the parser tasks and clear the stack of outstanding parse tasks
+ TextChange[] changes = _outstandingParserTasks.Select(t => t.Change).Reverse().ToArray();
+ lastChange = changes.Last();
+ _outstandingParserTasks.Clear();
+
+ // Take the current tree and check for differences
+ treeStructureChanged = CurrentParseTree == null || TreesAreDifferent(CurrentParseTree, results.Document, changes);
+ CurrentParseTree = results.Document;
+#if DEBUG
+ _currentCompileUnit = results.GeneratedCode;
+#endif
+ _lastChangeOwner = null;
+ }
+
+ // Done, now exit the lock and fire the event
+ OnDocumentParseComplete(new DocumentParseCompleteEventArgs()
+ {
+ GeneratorResults = results,
+ SourceChange = lastChange,
+ TreeStructureChanged = treeStructureChanged
+ });
+ }
+ finally
+ {
+ parseTask.Dispose();
+ }
+ }
+
+ internal static bool TreesAreDifferent(Block leftTree, Block rightTree, TextChange[] changes)
+ {
+ // Apply all the pending changes to the original tree
+ // PERF: If this becomes a bottleneck, we can probably do it the other way around,
+ // i.e. visit the tree and find applicable changes for each node.
+ foreach (TextChange change in changes)
+ {
+ Span changeOwner = leftTree.LocateOwner(change);
+
+ // Apply the change to the tree
+ if (changeOwner == null)
+ {
+ return true;
+ }
+ EditResult result = changeOwner.EditHandler.ApplyChange(changeOwner, change, force: true);
+ changeOwner.ReplaceWith(result.EditedSpan);
+ }
+
+ // Now compare the trees
+ bool treesDifferent = !leftTree.EquivalentTo(rightTree);
+#if DEBUG
+ if (RazorDebugHelpers.OutputDebuggingEnabled)
+ {
+ Debug.WriteLine(String.Format(CultureInfo.CurrentCulture, "Processed {0} changes, trees were{1} different", changes.Length, treesDifferent ? String.Empty : " not"));
+ }
+#endif
+ return treesDifferent;
+ }
+
+ private void OnDocumentParseComplete(DocumentParseCompleteEventArgs args)
+ {
+ Debug.Assert(args != null, "Event arguments cannot be null");
+ EventHandler<DocumentParseCompleteEventArgs> handler = DocumentParseComplete;
+ if (handler != null)
+ {
+ handler(this, args);
+ }
+#if DEBUG
+ RazorDebugHelpers.WriteDebugTree(FileName, args.GeneratorResults.Document, PartialParseResult.Rejected, args.SourceChange, this, args.TreeStructureChanged);
+ RazorDebugHelpers.WriteGeneratedCode(FileName, args.GeneratorResults.GeneratedCode);
+#endif
+ }
+
+ [Conditional("DEBUG")]
+ private static void VerifyFlagsAreValid(PartialParseResult result)
+ {
+ Debug.Assert(result.HasFlag(PartialParseResult.Accepted) ||
+ result.HasFlag(PartialParseResult.Rejected),
+ "Partial Parse result does not have either of Accepted or Rejected flags set");
+ Debug.Assert(result.HasFlag(PartialParseResult.Rejected) ||
+ !result.HasFlag(PartialParseResult.SpanContextChanged),
+ "Partial Parse result was Accepted AND had SpanContextChanged flag set");
+ Debug.Assert(result.HasFlag(PartialParseResult.Rejected) ||
+ !result.HasFlag(PartialParseResult.AutoCompleteBlock),
+ "Partial Parse result was Accepted AND had AutoCompleteBlock flag set");
+ Debug.Assert(result.HasFlag(PartialParseResult.Accepted) ||
+ !result.HasFlag(PartialParseResult.Provisional),
+ "Partial Parse result was Rejected AND had Provisional flag set");
+ }
+
+ internal virtual BackgroundParseTask CreateBackgroundTask(RazorEngineHost host, string fileName, TextChange change)
+ {
+ return BackgroundParseTask.StartNew(new RazorTemplateEngine(Host), FileName, change);
+ }
+ }
+}
diff --git a/src/System.Web.Razor/RazorEngineHost.cs b/src/System.Web.Razor/RazorEngineHost.cs
new file mode 100644
index 00000000..0e192b4c
--- /dev/null
+++ b/src/System.Web.Razor/RazorEngineHost.cs
@@ -0,0 +1,213 @@
+using System.CodeDom;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser;
+
+namespace System.Web.Razor
+{
+ /// <summary>
+ /// Defines the environment in which a Razor template will live
+ /// </summary>
+ /// <remarks>
+ /// The host defines the following things:
+ /// * What method names will be used for rendering markup, expressions etc. For example "Write", "WriteLiteral"
+ /// * The namespace imports to be added to every page generated via this host
+ /// * The default Base Class to inherit the generated class from
+ /// * The default Class Name and Namespace for the generated class (can be overridden by parameters in RazorTemplateEngine.GeneratedCode)
+ /// * The language of the code in a Razor page
+ /// * The markup, code parsers and code generators to use (the system will select defaults, but a Host gets a change to augment them)
+ /// ** See DecorateNNN methods
+ /// * Additional code to add to the generated code (see PostProcessGeneratedCode)
+ /// </remarks>
+ public class RazorEngineHost
+ {
+ internal const string InternalDefaultClassName = "__CompiledTemplate";
+ internal const string InternalDefaultNamespace = "Razor";
+
+ private bool _instrumentationActive = false;
+ private Func<ParserBase> _markupParserFactory;
+
+ [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "The code path is safe, it is a property setter and not dependent on other state")]
+ protected RazorEngineHost()
+ {
+ GeneratedClassContext = GeneratedClassContext.Default;
+ NamespaceImports = new HashSet<string>();
+ DesignTimeMode = false;
+ DefaultNamespace = InternalDefaultNamespace;
+ DefaultClassName = InternalDefaultClassName;
+ EnableInstrumentation = false;
+ }
+
+ /// <summary>
+ /// Creates a host which uses the specified code language and the HTML markup language
+ /// </summary>
+ /// <param name="codeLanguage">The code language to use</param>
+ public RazorEngineHost(RazorCodeLanguage codeLanguage)
+ : this(codeLanguage, () => new HtmlMarkupParser())
+ {
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "The code path is safe, it is a property setter and not dependent on other state")]
+ public RazorEngineHost(RazorCodeLanguage codeLanguage, Func<ParserBase> markupParserFactory)
+ : this()
+ {
+ if (codeLanguage == null)
+ {
+ throw new ArgumentNullException("codeLanguage");
+ }
+ if (markupParserFactory == null)
+ {
+ throw new ArgumentNullException("markupParserFactory");
+ }
+
+ CodeLanguage = codeLanguage;
+ _markupParserFactory = markupParserFactory;
+ }
+
+ /// <summary>
+ /// Details about the methods and types that should be used to generate code for Razor constructs
+ /// </summary>
+ public virtual GeneratedClassContext GeneratedClassContext { get; set; }
+
+ /// <summary>
+ /// A list of namespaces to import in the generated file
+ /// </summary>
+ public virtual ISet<string> NamespaceImports { get; private set; }
+
+ /// <summary>
+ /// The base-class of the generated class
+ /// </summary>
+ public virtual string DefaultBaseClass { get; set; }
+
+ /// <summary>
+ /// Indiciates if the parser and code generator should run in design-time mode
+ /// </summary>
+ public virtual bool DesignTimeMode { get; set; }
+
+ /// <summary>
+ /// The name of the generated class
+ /// </summary>
+ public virtual string DefaultClassName { get; set; }
+
+ /// <summary>
+ /// The namespace which will contain the generated class
+ /// </summary>
+ public virtual string DefaultNamespace { get; set; }
+
+ /// <summary>
+ /// Boolean indicating if helper methods should be instance methods or static methods
+ /// </summary>
+ public virtual bool StaticHelpers { get; set; }
+
+ /// <summary>
+ /// The language of the code within the Razor template.
+ /// </summary>
+ public virtual RazorCodeLanguage CodeLanguage { get; protected set; }
+
+ /// <summary>
+ /// Boolean indicating if instrumentation code should be injected into the output page
+ /// </summary>
+ public virtual bool EnableInstrumentation
+ {
+ // Always disable instrumentation in DesignTimeMode.
+ get { return !DesignTimeMode && _instrumentationActive; }
+ set { _instrumentationActive = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets the path to use for this document when generating Instrumentation calls
+ /// </summary>
+ public virtual string InstrumentedSourceFilePath { get; set; }
+
+ /// <summary>
+ /// Constructs the markup parser. Must return a new instance on EVERY call to ensure thread-safety
+ /// </summary>
+ public virtual ParserBase CreateMarkupParser()
+ {
+ if (_markupParserFactory != null)
+ {
+ return _markupParserFactory();
+ }
+ return null;
+ }
+
+ /// <summary>
+ /// Gets an instance of the code parser and is provided an opportunity to decorate or replace it
+ /// </summary>
+ /// <param name="incomingCodeParser">The code parser</param>
+ /// <returns>Either the same code parser, after modifications, or a different code parser</returns>
+ public virtual ParserBase DecorateCodeParser(ParserBase incomingCodeParser)
+ {
+ if (incomingCodeParser == null)
+ {
+ throw new ArgumentNullException("incomingCodeParser");
+ }
+ return incomingCodeParser;
+ }
+
+ /// <summary>
+ /// Gets an instance of the markup parser and is provided an opportunity to decorate or replace it
+ /// </summary>
+ /// <param name="incomingMarkupParser">The markup parser</param>
+ /// <returns>Either the same markup parser, after modifications, or a different markup parser</returns>
+ public virtual ParserBase DecorateMarkupParser(ParserBase incomingMarkupParser)
+ {
+ if (incomingMarkupParser == null)
+ {
+ throw new ArgumentNullException("incomingMarkupParser");
+ }
+ return incomingMarkupParser;
+ }
+
+ /// <summary>
+ /// Gets an instance of the code generator and is provided an opportunity to decorate or replace it
+ /// </summary>
+ /// <param name="incomingCodeGenerator">The code generator</param>
+ /// <returns>Either the same code generator, after modifications, or a different code generator</returns>
+ public virtual RazorCodeGenerator DecorateCodeGenerator(RazorCodeGenerator incomingCodeGenerator)
+ {
+ if (incomingCodeGenerator == null)
+ {
+ throw new ArgumentNullException("incomingCodeGenerator");
+ }
+ return incomingCodeGenerator;
+ }
+
+ /// <summary>
+ /// Gets the important CodeDOM nodes generated by the code generator and has a chance to add to them.
+ /// </summary>
+ /// <remarks>
+ /// All the other parameter values can be located by traversing tree in the codeCompileUnit node, they
+ /// are simply provided for convenience
+ /// </remarks>
+ /// <param name="context">The current <see cref="CodeGeneratorContext"/>.</param>
+ public virtual void PostProcessGeneratedCode(CodeGeneratorContext context)
+ {
+#pragma warning disable 0618
+ PostProcessGeneratedCode(context.CompileUnit, context.Namespace, context.GeneratedClass, context.TargetMethod);
+#pragma warning restore 0618
+ }
+
+ [Obsolete("This method is obsolete, use the override which takes a CodeGeneratorContext instead")]
+ public virtual void PostProcessGeneratedCode(CodeCompileUnit codeCompileUnit, CodeNamespace generatedNamespace, CodeTypeDeclaration generatedClass, CodeMemberMethod executeMethod)
+ {
+ if (codeCompileUnit == null)
+ {
+ throw new ArgumentNullException("codeCompileUnit");
+ }
+ if (generatedNamespace == null)
+ {
+ throw new ArgumentNullException("generatedNamespace");
+ }
+ if (generatedClass == null)
+ {
+ throw new ArgumentNullException("generatedClass");
+ }
+ if (executeMethod == null)
+ {
+ throw new ArgumentNullException("executeMethod");
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Razor/RazorTemplateEngine.cs b/src/System.Web.Razor/RazorTemplateEngine.cs
new file mode 100644
index 00000000..5d669803
--- /dev/null
+++ b/src/System.Web.Razor/RazorTemplateEngine.cs
@@ -0,0 +1,197 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Threading;
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Text;
+
+namespace System.Web.Razor
+{
+ /// <summary>
+ /// Entry-point to the Razor Template Engine
+ /// </summary>
+ public class RazorTemplateEngine
+ {
+ public static readonly string DefaultClassName = "Template";
+ public static readonly string DefaultNamespace = String.Empty;
+
+ /// <summary>
+ /// Constructs a new RazorTemplateEngine with the specified host
+ /// </summary>
+ /// <param name="host">The host which defines the environment in which the generated template code will live</param>
+ public RazorTemplateEngine(RazorEngineHost host)
+ {
+ if (host == null)
+ {
+ throw new ArgumentNullException("host");
+ }
+
+ Host = host;
+ }
+
+ /// <summary>
+ /// The RazorEngineHost which defines the environment in which the generated template code will live
+ /// </summary>
+ public RazorEngineHost Host { get; private set; }
+
+ public ParserResults ParseTemplate(ITextBuffer input)
+ {
+ return ParseTemplate(input, null);
+ }
+
+ /// <summary>
+ /// Parses the template specified by the TextBuffer and returns it's result
+ /// </summary>
+ /// <remarks>
+ /// IMPORTANT: This does NOT need to be called before GeneratedCode! GenerateCode will automatically
+ /// parse the document first.
+ ///
+ /// The cancel token provided can be used to cancel the parse. However, please note
+ /// that the parse occurs _synchronously_, on the callers thread. This parameter is
+ /// provided so that if the caller is in a background thread with a CancellationToken,
+ /// it can pass it along to the parser.
+ /// </remarks>
+ /// <param name="input">The input text to parse</param>
+ /// <param name="cancelToken">A token used to cancel the parser</param>
+ /// <returns>The resulting parse tree</returns>
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Input object would be disposed if we dispose the wrapper. We don't own the input so we don't want to dispose it")]
+ public ParserResults ParseTemplate(ITextBuffer input, CancellationToken? cancelToken)
+ {
+ return ParseTemplateCore(input.ToDocument(), cancelToken);
+ }
+
+ // See ParseTemplate(ITextBuffer, CancellationToken?),
+ // this overload simply wraps a TextReader in a TextBuffer (see ITextBuffer and BufferingTextReader)
+ public ParserResults ParseTemplate(TextReader input)
+ {
+ return ParseTemplate(input, null);
+ }
+
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Input object would be disposed if we dispose the wrapper. We don't own the input so we don't want to dispose it")]
+ public ParserResults ParseTemplate(TextReader input, CancellationToken? cancelToken)
+ {
+ return ParseTemplateCore(new SeekableTextReader(input), cancelToken);
+ }
+
+ protected internal virtual ParserResults ParseTemplateCore(ITextDocument input, CancellationToken? cancelToken)
+ {
+ // Construct the parser
+ RazorParser parser = CreateParser();
+ Debug.Assert(parser != null);
+ return parser.Parse(input);
+ }
+
+ public GeneratorResults GenerateCode(ITextBuffer input)
+ {
+ return GenerateCode(input, null, null, null, null);
+ }
+
+ public GeneratorResults GenerateCode(ITextBuffer input, CancellationToken? cancelToken)
+ {
+ return GenerateCode(input, null, null, null, cancelToken);
+ }
+
+ public GeneratorResults GenerateCode(ITextBuffer input, string className, string rootNamespace, string sourceFileName)
+ {
+ return GenerateCode(input, className, rootNamespace, sourceFileName, null);
+ }
+
+ /// <summary>
+ /// Parses the template specified by the TextBuffer, generates code for it, and returns the constructed CodeDOM tree
+ /// </summary>
+ /// <remarks>
+ /// The cancel token provided can be used to cancel the parse. However, please note
+ /// that the parse occurs _synchronously_, on the callers thread. This parameter is
+ /// provided so that if the caller is in a background thread with a CancellationToken,
+ /// it can pass it along to the parser.
+ ///
+ /// The className, rootNamespace and sourceFileName parameters are optional and override the default
+ /// specified by the Host. For example, the WebPageRazorHost in System.Web.WebPages.Razor configures the
+ /// Class Name, Root Namespace and Source File Name based on the virtual path of the page being compiled.
+ /// However, the built-in RazorEngineHost class uses constant defaults, so the caller will likely want to
+ /// change them using these parameters
+ /// </remarks>
+ /// <param name="input">The input text to parse</param>
+ /// <param name="cancelToken">A token used to cancel the parser</param>
+ /// <param name="className">The name of the generated class, overriding whatever is specified in the Host. The default value (defined in the Host) can be used by providing null for this argument</param>
+ /// <param name="rootNamespace">The namespace in which the generated class will reside, overriding whatever is specified in the Host. The default value (defined in the Host) can be used by providing null for this argument</param>
+ /// <param name="sourceFileName">The file name to use in line pragmas, usually the original Razor file, overriding whatever is specified in the Host. The default value (defined in the Host) can be used by providing null for this argument</param>
+ /// <returns>The resulting parse tree AND generated Code DOM tree</returns>
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Input object would be disposed if we dispose the wrapper. We don't own the input so we don't want to dispose it")]
+ public GeneratorResults GenerateCode(ITextBuffer input, string className, string rootNamespace, string sourceFileName, CancellationToken? cancelToken)
+ {
+ return GenerateCodeCore(input.ToDocument(), className, rootNamespace, sourceFileName, cancelToken);
+ }
+
+ // See GenerateCode override which takes ITextBuffer, and BufferingTextReader for details.
+ public GeneratorResults GenerateCode(TextReader input)
+ {
+ return GenerateCode(input, null, null, null, null);
+ }
+
+ public GeneratorResults GenerateCode(TextReader input, CancellationToken? cancelToken)
+ {
+ return GenerateCode(input, null, null, null, cancelToken);
+ }
+
+ public GeneratorResults GenerateCode(TextReader input, string className, string rootNamespace, string sourceFileName)
+ {
+ return GenerateCode(input, className, rootNamespace, sourceFileName, null);
+ }
+
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Input object would be disposed if we dispose the wrapper. We don't own the input so we don't want to dispose it")]
+ public GeneratorResults GenerateCode(TextReader input, string className, string rootNamespace, string sourceFileName, CancellationToken? cancelToken)
+ {
+ return GenerateCodeCore(new SeekableTextReader(input), className, rootNamespace, sourceFileName, cancelToken);
+ }
+
+ protected internal virtual GeneratorResults GenerateCodeCore(ITextDocument input, string className, string rootNamespace, string sourceFileName, CancellationToken? cancelToken)
+ {
+ className = (className ?? Host.DefaultClassName) ?? DefaultClassName;
+ rootNamespace = (rootNamespace ?? Host.DefaultNamespace) ?? DefaultNamespace;
+
+ // Run the parser
+ RazorParser parser = CreateParser();
+ Debug.Assert(parser != null);
+ ParserResults results = parser.Parse(input);
+
+ // Generate code
+ RazorCodeGenerator generator = CreateCodeGenerator(className, rootNamespace, sourceFileName);
+ generator.DesignTimeMode = Host.DesignTimeMode;
+ generator.Visit(results);
+
+ // Post process code
+ Host.PostProcessGeneratedCode(generator.Context);
+
+ // Extract design-time mappings
+ IDictionary<int, GeneratedCodeMapping> designTimeLineMappings = null;
+ if (Host.DesignTimeMode)
+ {
+ designTimeLineMappings = generator.Context.CodeMappings;
+ }
+
+ // Collect results and return
+ return new GeneratorResults(results, generator.Context.CompileUnit, designTimeLineMappings);
+ }
+
+ protected internal virtual RazorCodeGenerator CreateCodeGenerator(string className, string rootNamespace, string sourceFileName)
+ {
+ return Host.DecorateCodeGenerator(
+ Host.CodeLanguage.CreateCodeGenerator(className, rootNamespace, sourceFileName, Host));
+ }
+
+ protected internal virtual RazorParser CreateParser()
+ {
+ ParserBase codeParser = Host.CodeLanguage.CreateCodeParser();
+ ParserBase markupParser = Host.CreateMarkupParser();
+
+ return new RazorParser(Host.DecorateCodeParser(codeParser),
+ Host.DecorateMarkupParser(markupParser))
+ {
+ DesignTimeMode = Host.DesignTimeMode
+ };
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Resources/RazorResources.Designer.cs b/src/System.Web.Razor/Resources/RazorResources.Designer.cs
new file mode 100644
index 00000000..cdb5ec52
--- /dev/null
+++ b/src/System.Web.Razor/Resources/RazorResources.Designer.cs
@@ -0,0 +1,868 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.239
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace System.Web.Razor.Resources {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class RazorResources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal RazorResources() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("System.Web.Razor.Resources.RazorResources", typeof(RazorResources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The active parser must the same as either the markup or code parser..
+ /// </summary>
+ internal static string ActiveParser_Must_Be_Code_Or_Markup_Parser {
+ get {
+ return ResourceManager.GetString("ActiveParser_Must_Be_Code_Or_Markup_Parser", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Block cannot be built because a Type has not been specified in the BlockBuilder.
+ /// </summary>
+ internal static string Block_Type_Not_Specified {
+ get {
+ return ResourceManager.GetString("Block_Type_Not_Specified", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to code.
+ /// </summary>
+ internal static string BlockName_Code {
+ get {
+ return ResourceManager.GetString("BlockName_Code", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to explicit expression.
+ /// </summary>
+ internal static string BlockName_ExplicitExpression {
+ get {
+ return ResourceManager.GetString("BlockName_ExplicitExpression", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The &quot;CancelBacktrack&quot; method can be called only while in a look-ahead process started with the &quot;BeginLookahead&quot; method..
+ /// </summary>
+ internal static string CancelBacktrack_Must_Be_Called_Within_Lookahead {
+ get {
+ return ResourceManager.GetString("CancelBacktrack_Must_Be_Called_Within_Lookahead", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Cannot call CreateCodeWriter, a CodeWriter was not provided to the Create method.
+ /// </summary>
+ internal static string CreateCodeWriter_NoCodeWriter {
+ get {
+ return ResourceManager.GetString("CreateCodeWriter_NoCodeWriter", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &lt;&lt;character literal&gt;&gt;.
+ /// </summary>
+ internal static string CSharpSymbol_CharacterLiteral {
+ get {
+ return ResourceManager.GetString("CSharpSymbol_CharacterLiteral", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &lt;&lt;comment&gt;&gt;.
+ /// </summary>
+ internal static string CSharpSymbol_Comment {
+ get {
+ return ResourceManager.GetString("CSharpSymbol_Comment", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &lt;&lt;identifier&gt;&gt;.
+ /// </summary>
+ internal static string CSharpSymbol_Identifier {
+ get {
+ return ResourceManager.GetString("CSharpSymbol_Identifier", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &lt;&lt;integer literal&gt;&gt;.
+ /// </summary>
+ internal static string CSharpSymbol_IntegerLiteral {
+ get {
+ return ResourceManager.GetString("CSharpSymbol_IntegerLiteral", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &lt;&lt;keyword&gt;&gt;.
+ /// </summary>
+ internal static string CSharpSymbol_Keyword {
+ get {
+ return ResourceManager.GetString("CSharpSymbol_Keyword", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &lt;&lt;newline sequence&gt;&gt;.
+ /// </summary>
+ internal static string CSharpSymbol_Newline {
+ get {
+ return ResourceManager.GetString("CSharpSymbol_Newline", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &lt;&lt;real literal&gt;&gt;.
+ /// </summary>
+ internal static string CSharpSymbol_RealLiteral {
+ get {
+ return ResourceManager.GetString("CSharpSymbol_RealLiteral", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &lt;&lt;string literal&gt;&gt;.
+ /// </summary>
+ internal static string CSharpSymbol_StringLiteral {
+ get {
+ return ResourceManager.GetString("CSharpSymbol_StringLiteral", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &lt;&lt;white space&gt;&gt;.
+ /// </summary>
+ internal static string CSharpSymbol_Whitespace {
+ get {
+ return ResourceManager.GetString("CSharpSymbol_Whitespace", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &quot;EndBlock&quot; was called without a matching call to &quot;StartBlock&quot;..
+ /// </summary>
+ internal static string EndBlock_Called_Without_Matching_StartBlock {
+ get {
+ return ResourceManager.GetString("EndBlock_Called_Without_Matching_StartBlock", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &quot;{0}&quot; character.
+ /// </summary>
+ internal static string ErrorComponent_Character {
+ get {
+ return ResourceManager.GetString("ErrorComponent_Character", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to end of file.
+ /// </summary>
+ internal static string ErrorComponent_EndOfFile {
+ get {
+ return ResourceManager.GetString("ErrorComponent_EndOfFile", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to line break.
+ /// </summary>
+ internal static string ErrorComponent_Newline {
+ get {
+ return ResourceManager.GetString("ErrorComponent_Newline", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to space or line break.
+ /// </summary>
+ internal static string ErrorComponent_Whitespace {
+ get {
+ return ResourceManager.GetString("ErrorComponent_Whitespace", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &lt;&lt;newline sequence&gt;&gt;.
+ /// </summary>
+ internal static string HtmlSymbol_NewLine {
+ get {
+ return ResourceManager.GetString("HtmlSymbol_NewLine", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &lt;&lt;razor comment&gt;&gt;.
+ /// </summary>
+ internal static string HtmlSymbol_RazorComment {
+ get {
+ return ResourceManager.GetString("HtmlSymbol_RazorComment", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &lt;&lt;text&gt;&gt;.
+ /// </summary>
+ internal static string HtmlSymbol_Text {
+ get {
+ return ResourceManager.GetString("HtmlSymbol_Text", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &lt;&lt;white space&gt;&gt;.
+ /// </summary>
+ internal static string HtmlSymbol_WhiteSpace {
+ get {
+ return ResourceManager.GetString("HtmlSymbol_WhiteSpace", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Cannot use built-in RazorComment handler, language characteristics does not define the CommentStart, CommentStar and CommentBody known symbol types or parser does not override TokenizerBackedParser.OutputSpanBeforeRazorComment.
+ /// </summary>
+ internal static string Language_Does_Not_Support_RazorComment {
+ get {
+ return ResourceManager.GetString("Language_Does_Not_Support_RazorComment", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The &quot;@&quot; character must be followed by a &quot;:&quot;, &quot;(&quot;, or a C# identifier. If you intended to switch to markup, use an HTML start tag, for example:
+ ///
+ ///@if(isLoggedIn) {
+ /// &lt;p&gt;Hello, @user!&lt;/p&gt;
+ ///}.
+ /// </summary>
+ internal static string ParseError_AtInCode_Must_Be_Followed_By_Colon_Paren_Or_Identifier_Start {
+ get {
+ return ResourceManager.GetString("ParseError_AtInCode_Must_Be_Followed_By_Colon_Paren_Or_Identifier_Start", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to End of file was reached before the end of the block comment. All comments started with &quot;/*&quot; sequence must be terminated with a matching &quot;*/&quot; sequence..
+ /// </summary>
+ internal static string ParseError_BlockComment_Not_Terminated {
+ get {
+ return ResourceManager.GetString("ParseError_BlockComment_Not_Terminated", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The &quot;{0}&quot; block was not terminated. All &quot;{0}&quot; statements must be terminated with a matching &quot;{1}&quot;..
+ /// </summary>
+ internal static string ParseError_BlockNotTerminated {
+ get {
+ return ResourceManager.GetString("ParseError_BlockNotTerminated", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to An opening &quot;{0}&quot; is missing the corresponding closing &quot;{1}&quot;..
+ /// </summary>
+ internal static string ParseError_Expected_CloseBracket_Before_EOF {
+ get {
+ return ResourceManager.GetString("ParseError_Expected_CloseBracket_Before_EOF", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The {0} block is missing a closing &quot;{1}&quot; character. Make sure you have a matching &quot;{1}&quot; character for all the &quot;{2}&quot; characters within this block, and that none of the &quot;{1}&quot; characters are being interpreted as markup..
+ /// </summary>
+ internal static string ParseError_Expected_EndOfBlock_Before_EOF {
+ get {
+ return ResourceManager.GetString("ParseError_Expected_EndOfBlock_Before_EOF", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Expected &quot;{0}&quot;..
+ /// </summary>
+ internal static string ParseError_Expected_X {
+ get {
+ return ResourceManager.GetString("ParseError_Expected_X", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Helper blocks cannot be nested within each other..
+ /// </summary>
+ internal static string ParseError_Helpers_Cannot_Be_Nested {
+ get {
+ return ResourceManager.GetString("ParseError_Helpers_Cannot_Be_Nested", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The &apos;inherits&apos; keyword must be followed by a type name on the same line..
+ /// </summary>
+ internal static string ParseError_InheritsKeyword_Must_Be_Followed_By_TypeName {
+ get {
+ return ResourceManager.GetString("ParseError_InheritsKeyword_Must_Be_Followed_By_TypeName", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Inline markup blocks (@&lt;p&gt;Content&lt;/p&gt;) cannot be nested. Only one level of inline markup is allowed..
+ /// </summary>
+ internal static string ParseError_InlineMarkup_Blocks_Cannot_Be_Nested {
+ get {
+ return ResourceManager.GetString("ParseError_InlineMarkup_Blocks_Cannot_Be_Nested", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &quot;{1}&quot; is not a valid value for the &quot;{0}&quot; option. The &quot;Option {0}&quot; statement must be followed by either &quot;On&quot; or &quot;Off&quot;. .
+ /// </summary>
+ internal static string ParseError_InvalidOptionValue {
+ get {
+ return ResourceManager.GetString("ParseError_InvalidOptionValue", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Markup in a code block must start with a tag and all start tags must be matched with end tags. Do not use unclosed tags like &quot;&lt;br&gt;&quot;. Instead use self-closing tags like &quot;&lt;br/&gt;&quot;..
+ /// </summary>
+ internal static string ParseError_MarkupBlock_Must_Start_With_Tag {
+ get {
+ return ResourceManager.GetString("ParseError_MarkupBlock_Must_Start_With_Tag", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Expected a &quot;{0}&quot; after the helper name..
+ /// </summary>
+ internal static string ParseError_MissingCharAfterHelperName {
+ get {
+ return ResourceManager.GetString("ParseError_MissingCharAfterHelperName", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Expected a &quot;{0}&quot; after the helper parameters..
+ /// </summary>
+ internal static string ParseError_MissingCharAfterHelperParameters {
+ get {
+ return ResourceManager.GetString("ParseError_MissingCharAfterHelperParameters", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The &quot;{0}&quot; element was not closed. All elements must be either self-closing or have a matching end tag..
+ /// </summary>
+ internal static string ParseError_MissingEndTag {
+ get {
+ return ResourceManager.GetString("ParseError_MissingEndTag", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Sections cannot be empty. The &quot;@section&quot; keyword must be followed by a block of markup surrounded by &quot;{}&quot;. For example:
+ ///
+ ///@section Sidebar {
+ /// &lt;!-- Markup and text goes here --&gt;
+ ///}.
+ /// </summary>
+ internal static string ParseError_MissingOpenBraceAfterSection {
+ get {
+ return ResourceManager.GetString("ParseError_MissingOpenBraceAfterSection", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Namespace imports and type aliases cannot be placed within code blocks. They must immediately follow an &quot;@&quot; character in markup. It is recommended that you put them at the top of the page, as in the following example:
+ ///
+ ///@using System.Drawing;
+ ///@{
+ /// // OK here to use types from System.Drawing in the page.
+ ///}.
+ /// </summary>
+ internal static string ParseError_NamespaceImportAndTypeAlias_Cannot_Exist_Within_CodeBlock {
+ get {
+ return ResourceManager.GetString("ParseError_NamespaceImportAndTypeAlias_Cannot_Exist_Within_CodeBlock", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The &quot;Imports&quot; keyword must be followed by a namespace or a type alias on the same line..
+ /// </summary>
+ internal static string ParseError_NamespaceOrTypeAliasExpected {
+ get {
+ return ResourceManager.GetString("ParseError_NamespaceOrTypeAliasExpected", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Outer tag is missing a name. The first character of a markup block must be an HTML tag with a valid name..
+ /// </summary>
+ internal static string ParseError_OuterTagMissingName {
+ get {
+ return ResourceManager.GetString("ParseError_OuterTagMissingName", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to End of file was reached before the end of the block comment. All comments that start with the &quot;@*&quot; sequence must be terminated with a matching &quot;*@&quot; sequence..
+ /// </summary>
+ internal static string ParseError_RazorComment_Not_Terminated {
+ get {
+ return ResourceManager.GetString("ParseError_RazorComment_Not_Terminated", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &quot;{0}&quot; is a reserved word and cannot be used in implicit expressions. An explicit expression (&quot;@()&quot;) must be used..
+ /// </summary>
+ internal static string ParseError_ReservedWord {
+ get {
+ return ResourceManager.GetString("ParseError_ReservedWord", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Section blocks (&quot;{0}&quot;) cannot be nested. Only one level of section blocks are allowed..
+ /// </summary>
+ internal static string ParseError_Sections_Cannot_Be_Nested {
+ get {
+ return ResourceManager.GetString("ParseError_Sections_Cannot_Be_Nested", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Expected a &quot;{0}&quot; but found a &quot;{1}&quot;. Block statements must be enclosed in &quot;{{&quot; and &quot;}}&quot;. You cannot use single-statement control-flow statements in CSHTML pages. For example, the following is not allowed:
+ ///
+ ///@if(isLoggedIn)
+ /// &lt;p&gt;Hello, @user&lt;/p&gt;
+ ///
+ ///Instead, wrap the contents of the block in &quot;{{}}&quot;:
+ ///
+ ///@if(isLoggedIn) {{
+ /// &lt;p&gt;Hello, @user&lt;/p&gt;
+ ///}}.
+ /// </summary>
+ internal static string ParseError_SingleLine_ControlFlowStatements_Not_Allowed {
+ get {
+ return ResourceManager.GetString("ParseError_SingleLine_ControlFlowStatements_Not_Allowed", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &quot;&lt;text&gt;&quot; and &quot;&lt;/text&gt;&quot; tags cannot contain attributes..
+ /// </summary>
+ internal static string ParseError_TextTagCannotContainAttributes {
+ get {
+ return ResourceManager.GetString("ParseError_TextTagCannotContainAttributes", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Unexpected &quot;{0}&quot;.
+ /// </summary>
+ internal static string ParseError_Unexpected {
+ get {
+ return ResourceManager.GetString("ParseError_Unexpected", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Unexpected {0} after helper keyword. All helpers must have a name which starts with an &quot;_&quot; or alphabetic character. The remaining characters must be either &quot;_&quot; or alphanumeric..
+ /// </summary>
+ internal static string ParseError_Unexpected_Character_At_Helper_Name_Start {
+ get {
+ return ResourceManager.GetString("ParseError_Unexpected_Character_At_Helper_Name_Start", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Unexpected {0} after section keyword. Section names must start with an &quot;_&quot; or alphabetic character, and the remaining characters must be either &quot;_&quot; or alphanumeric..
+ /// </summary>
+ internal static string ParseError_Unexpected_Character_At_Section_Name_Start {
+ get {
+ return ResourceManager.GetString("ParseError_Unexpected_Character_At_Section_Name_Start", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &quot;{0}&quot; is not valid at the start of a code block. Only identifiers, keywords, comments, &quot;(&quot; and &quot;{{&quot; are valid..
+ /// </summary>
+ internal static string ParseError_Unexpected_Character_At_Start_Of_CodeBlock_CS {
+ get {
+ return ResourceManager.GetString("ParseError_Unexpected_Character_At_Start_Of_CodeBlock_CS", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &quot;{0}&quot; is not valid at the start of a code block. Only identifiers, keywords, comments, and &quot;(&quot; are valid..
+ /// </summary>
+ internal static string ParseError_Unexpected_Character_At_Start_Of_CodeBlock_VB {
+ get {
+ return ResourceManager.GetString("ParseError_Unexpected_Character_At_Start_Of_CodeBlock_VB", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to End-of-file was found after the &quot;@&quot; character. &quot;@&quot; must be followed by a valid code block. If you want to output an &quot;@&quot;, escape it using the sequence: &quot;@@&quot;.
+ /// </summary>
+ internal static string ParseError_Unexpected_EndOfFile_At_Start_Of_CodeBlock {
+ get {
+ return ResourceManager.GetString("ParseError_Unexpected_EndOfFile_At_Start_Of_CodeBlock", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Unexpected &quot;{0}&quot; keyword after &quot;@&quot; character. Once inside code, you do not need to prefix constructs like &quot;{0}&quot; with &quot;@&quot;..
+ /// </summary>
+ internal static string ParseError_Unexpected_Keyword_After_At {
+ get {
+ return ResourceManager.GetString("ParseError_Unexpected_Keyword_After_At", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Unexpected &quot;{&quot; after &quot;@&quot; character. Once inside the body of a code block (@if {}, @{}, etc.) you do not need to use &quot;@{&quot; to switch to code..
+ /// </summary>
+ internal static string ParseError_Unexpected_Nested_CodeBlock {
+ get {
+ return ResourceManager.GetString("ParseError_Unexpected_Nested_CodeBlock", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A space or line break was encountered after the &quot;@&quot; character. Only valid identifiers, keywords, comments, &quot;(&quot; and &quot;{&quot; are valid at the start of a code block and they must occur immediately following &quot;@&quot; with no space in between..
+ /// </summary>
+ internal static string ParseError_Unexpected_WhiteSpace_At_Start_Of_CodeBlock_CS {
+ get {
+ return ResourceManager.GetString("ParseError_Unexpected_WhiteSpace_At_Start_Of_CodeBlock_CS", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A space or line break was encountered after the &quot;@&quot; character. Only valid identifiers, keywords, comments, and &quot;(&quot; are valid at the start of a code block and they must occur immediately following &quot;@&quot; with no space in between..
+ /// </summary>
+ internal static string ParseError_Unexpected_WhiteSpace_At_Start_Of_CodeBlock_VB {
+ get {
+ return ResourceManager.GetString("ParseError_Unexpected_WhiteSpace_At_Start_Of_CodeBlock_VB", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Encountered end tag &quot;{0}&quot; with no matching start tag. Are your start/end tags properly balanced?.
+ /// </summary>
+ internal static string ParseError_UnexpectedEndTag {
+ get {
+ return ResourceManager.GetString("ParseError_UnexpectedEndTag", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to End of file or an unexpected character was reached before the &quot;{0}&quot; tag could be parsed. Elements inside markup blocks must be complete. They must either be self-closing (&quot;&lt;br /&gt;&quot;) or have matching end tags (&quot;&lt;p&gt;Hello&lt;/p&gt;&quot;). If you intended to display a &quot;&lt;&quot; character, use the &quot;&amp;lt;&quot; HTML entity..
+ /// </summary>
+ internal static string ParseError_UnfinishedTag {
+ get {
+ return ResourceManager.GetString("ParseError_UnfinishedTag", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Unknown option: &quot;{0}&quot;..
+ /// </summary>
+ internal static string ParseError_UnknownOption {
+ get {
+ return ResourceManager.GetString("ParseError_UnknownOption", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Unterminated string literal. Strings that start with a quotation mark (&quot;) must be terminated before the end of the line. However, strings that start with @ and a quotation mark (@&quot;) can span multiple lines..
+ /// </summary>
+ internal static string ParseError_Unterminated_String_Literal {
+ get {
+ return ResourceManager.GetString("ParseError_Unterminated_String_Literal", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Helper parameter list is missing a closing &quot;)&quot;..
+ /// </summary>
+ internal static string ParseError_UnterminatedHelperParameterList {
+ get {
+ return ResourceManager.GetString("ParseError_UnterminatedHelperParameterList", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Parser was started with a null Context property. The Context property must be set BEFORE calling any methods on the parser..
+ /// </summary>
+ internal static string Parser_Context_Not_Set {
+ get {
+ return ResourceManager.GetString("Parser_Context_Not_Set", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Cannot complete the tree, StartBlock must be called at least once..
+ /// </summary>
+ internal static string ParserContext_CannotCompleteTree_NoRootBlock {
+ get {
+ return ResourceManager.GetString("ParserContext_CannotCompleteTree_NoRootBlock", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Cannot complete the tree, there are still open blocks..
+ /// </summary>
+ internal static string ParserContext_CannotCompleteTree_OutstandingBlocks {
+ get {
+ return ResourceManager.GetString("ParserContext_CannotCompleteTree_OutstandingBlocks", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Cannot finish span, there is no current block. Call StartBlock at least once before finishing a span.
+ /// </summary>
+ internal static string ParserContext_NoCurrentBlock {
+ get {
+ return ResourceManager.GetString("ParserContext_NoCurrentBlock", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Cannot complete action, the parser has finished. Only CompleteParse can be called to extract the final parser results after the parser has finished.
+ /// </summary>
+ internal static string ParserContext_ParseComplete {
+ get {
+ return ResourceManager.GetString("ParserContext_ParseComplete", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Missing value for session state directive..
+ /// </summary>
+ internal static string ParserEror_SessionDirectiveMissingValue {
+ get {
+ return ResourceManager.GetString("ParserEror_SessionDirectiveMissingValue", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The parser provided to the ParserContext was not a Markup Parser..
+ /// </summary>
+ internal static string ParserIsNotAMarkupParser {
+ get {
+ return ResourceManager.GetString("ParserIsNotAMarkupParser", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to @section Header { ... }.
+ /// </summary>
+ internal static string SectionExample_CS {
+ get {
+ return ResourceManager.GetString("SectionExample_CS", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to @Section Header ... End Section.
+ /// </summary>
+ internal static string SectionExample_VB {
+ get {
+ return ResourceManager.GetString("SectionExample_VB", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The {0} property of the {1} structure cannot be null..
+ /// </summary>
+ internal static string Structure_Member_CannotBeNull {
+ get {
+ return ResourceManager.GetString("Structure_Member_CannotBeNull", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &lt;&lt;unknown&gt;&gt;.
+ /// </summary>
+ internal static string Symbol_Unknown {
+ get {
+ return ResourceManager.GetString("Symbol_Unknown", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Cannot resume this symbol. Only the symbol immediately preceding the current one can be resumed..
+ /// </summary>
+ internal static string Tokenizer_CannotResumeSymbolUnlessIsPrevious {
+ get {
+ return ResourceManager.GetString("Tokenizer_CannotResumeSymbolUnlessIsPrevious", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to In order to put a symbol back, it must have been the symbol which ended at the current position. The specified symbol ends at {0}, but the current position is {1}.
+ /// </summary>
+ internal static string TokenizerView_CannotPutBack {
+ get {
+ return ResourceManager.GetString("TokenizerView_CannotPutBack", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &lt;&lt;character literal&gt;&gt;.
+ /// </summary>
+ internal static string VBSymbol_CharacterLiteral {
+ get {
+ return ResourceManager.GetString("VBSymbol_CharacterLiteral", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &lt;&lt;comment&gt;&gt;.
+ /// </summary>
+ internal static string VBSymbol_Comment {
+ get {
+ return ResourceManager.GetString("VBSymbol_Comment", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &lt;&lt;date literal&gt;&gt;.
+ /// </summary>
+ internal static string VBSymbol_DateLiteral {
+ get {
+ return ResourceManager.GetString("VBSymbol_DateLiteral", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &lt;&lt;floating point literal&gt;&gt;.
+ /// </summary>
+ internal static string VBSymbol_FloatingPointLiteral {
+ get {
+ return ResourceManager.GetString("VBSymbol_FloatingPointLiteral", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &lt;&lt;identifier&gt;&gt;.
+ /// </summary>
+ internal static string VBSymbol_Identifier {
+ get {
+ return ResourceManager.GetString("VBSymbol_Identifier", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &lt;&lt;integer literal&gt;&gt;.
+ /// </summary>
+ internal static string VBSymbol_IntegerLiteral {
+ get {
+ return ResourceManager.GetString("VBSymbol_IntegerLiteral", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &lt;&lt;keyword&gt;&gt;.
+ /// </summary>
+ internal static string VBSymbol_Keyword {
+ get {
+ return ResourceManager.GetString("VBSymbol_Keyword", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &lt;&lt;newline sequence&gt;&gt;.
+ /// </summary>
+ internal static string VBSymbol_NewLine {
+ get {
+ return ResourceManager.GetString("VBSymbol_NewLine", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &lt;&lt;razor comment&gt;&gt;.
+ /// </summary>
+ internal static string VBSymbol_RazorComment {
+ get {
+ return ResourceManager.GetString("VBSymbol_RazorComment", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &lt;&lt;string literal&gt;&gt;.
+ /// </summary>
+ internal static string VBSymbol_StringLiteral {
+ get {
+ return ResourceManager.GetString("VBSymbol_StringLiteral", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &lt;&lt;white space&gt;&gt;.
+ /// </summary>
+ internal static string VBSymbol_WhiteSpace {
+ get {
+ return ResourceManager.GetString("VBSymbol_WhiteSpace", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Resources/RazorResources.resx b/src/System.Web.Razor/Resources/RazorResources.resx
new file mode 100644
index 00000000..a892d055
--- /dev/null
+++ b/src/System.Web.Razor/Resources/RazorResources.resx
@@ -0,0 +1,412 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="ActiveParser_Must_Be_Code_Or_Markup_Parser" xml:space="preserve">
+ <value>The active parser must the same as either the markup or code parser.</value>
+ </data>
+ <data name="BlockName_Code" xml:space="preserve">
+ <value>code</value>
+ <comment>This is a literal used when composing ParserError_* messages. Most blocks are named by the keyword that starts them, for example "if". However, for those without keywords, a (localizable) name must be used. This literal is ALWAYS used mid-sentence, thus should not be capitalized.</comment>
+ </data>
+ <data name="BlockName_ExplicitExpression" xml:space="preserve">
+ <value>explicit expression</value>
+ <comment>This is a literal used when composing ParserError_* messages. Most blocks are named by the keyword that starts them, for example "if". However, for those without keywords, a (localizable) name must be used. This literal is ALWAYS used mid-sentence, thus should not be capitalized.</comment>
+ </data>
+ <data name="CancelBacktrack_Must_Be_Called_Within_Lookahead" xml:space="preserve">
+ <value>The "CancelBacktrack" method can be called only while in a look-ahead process started with the "BeginLookahead" method.</value>
+ </data>
+ <data name="EndBlock_Called_Without_Matching_StartBlock" xml:space="preserve">
+ <value>"EndBlock" was called without a matching call to "StartBlock".</value>
+ </data>
+ <data name="ParseError_AtInCode_Must_Be_Followed_By_Colon_Paren_Or_Identifier_Start" xml:space="preserve">
+ <value>The "@" character must be followed by a ":", "(", or a C# identifier. If you intended to switch to markup, use an HTML start tag, for example:
+
+@if(isLoggedIn) {
+ &lt;p&gt;Hello, @user!&lt;/p&gt;
+}</value>
+ </data>
+ <data name="ParseError_BlockComment_Not_Terminated" xml:space="preserve">
+ <value>End of file was reached before the end of the block comment. All comments started with "/*" sequence must be terminated with a matching "*/" sequence.</value>
+ </data>
+ <data name="ParseError_Expected_CloseBracket_Before_EOF" xml:space="preserve">
+ <value>An opening "{0}" is missing the corresponding closing "{1}".</value>
+ </data>
+ <data name="ParseError_Expected_EndOfBlock_Before_EOF" xml:space="preserve">
+ <value>The {0} block is missing a closing "{1}" character. Make sure you have a matching "{1}" character for all the "{2}" characters within this block, and that none of the "{1}" characters are being interpreted as markup.</value>
+ </data>
+ <data name="ParseError_Expected_X" xml:space="preserve">
+ <value>Expected "{0}".</value>
+ </data>
+ <data name="ParseError_InlineMarkup_Blocks_Cannot_Be_Nested" xml:space="preserve">
+ <value>Inline markup blocks (@&lt;p&gt;Content&lt;/p&gt;) cannot be nested. Only one level of inline markup is allowed.</value>
+ </data>
+ <data name="ParseError_MarkupBlock_Must_Start_With_Tag" xml:space="preserve">
+ <value>Markup in a code block must start with a tag and all start tags must be matched with end tags. Do not use unclosed tags like "&lt;br&gt;". Instead use self-closing tags like "&lt;br/&gt;".</value>
+ </data>
+ <data name="ParseError_MissingEndTag" xml:space="preserve">
+ <value>The "{0}" element was not closed. All elements must be either self-closing or have a matching end tag.</value>
+ </data>
+ <data name="ParseError_MissingOpenBraceAfterSection" xml:space="preserve">
+ <value>Sections cannot be empty. The "@section" keyword must be followed by a block of markup surrounded by "{}". For example:
+
+@section Sidebar {
+ &lt;!-- Markup and text goes here --&gt;
+}</value>
+ </data>
+ <data name="ParseError_NamespaceImportAndTypeAlias_Cannot_Exist_Within_CodeBlock" xml:space="preserve">
+ <value>Namespace imports and type aliases cannot be placed within code blocks. They must immediately follow an "@" character in markup. It is recommended that you put them at the top of the page, as in the following example:
+
+@using System.Drawing;
+@{
+ // OK here to use types from System.Drawing in the page.
+}</value>
+ </data>
+ <data name="ParseError_Sections_Cannot_Be_Nested" xml:space="preserve">
+ <value>Section blocks ("{0}") cannot be nested. Only one level of section blocks are allowed.</value>
+ <comment>{0} is one of SectionExample_VB or SectionExample_CS based on the language.</comment>
+ </data>
+ <data name="ParseError_SingleLine_ControlFlowStatements_Not_Allowed" xml:space="preserve">
+ <value>Expected a "{0}" but found a "{1}". Block statements must be enclosed in "{{" and "}}". You cannot use single-statement control-flow statements in CSHTML pages. For example, the following is not allowed:
+
+@if(isLoggedIn)
+ &lt;p&gt;Hello, @user&lt;/p&gt;
+
+Instead, wrap the contents of the block in "{{}}":
+
+@if(isLoggedIn) {{
+ &lt;p&gt;Hello, @user&lt;/p&gt;
+}}</value>
+ <comment>{0} is only ever a single character</comment>
+ </data>
+ <data name="ParseError_UnexpectedEndTag" xml:space="preserve">
+ <value>Encountered end tag "{0}" with no matching start tag. Are your start/end tags properly balanced?</value>
+ </data>
+ <data name="ParseError_Unexpected_Character_At_Section_Name_Start" xml:space="preserve">
+ <value>Unexpected {0} after section keyword. Section names must start with an "_" or alphabetic character, and the remaining characters must be either "_" or alphanumeric.</value>
+ </data>
+ <data name="ParseError_Unexpected_Character_At_Start_Of_CodeBlock_CS" xml:space="preserve">
+ <value>"{0}" is not valid at the start of a code block. Only identifiers, keywords, comments, "(" and "{{" are valid.</value>
+ <comment>"{{" is an escape sequence for String.Format, when outputted to the user it will be displayed as "{"</comment>
+ </data>
+ <data name="ParseError_UnfinishedTag" xml:space="preserve">
+ <value>End of file or an unexpected character was reached before the "{0}" tag could be parsed. Elements inside markup blocks must be complete. They must either be self-closing ("&lt;br /&gt;") or have matching end tags ("&lt;p&gt;Hello&lt;/p&gt;"). If you intended to display a "&lt;" character, use the "&amp;lt;" HTML entity.</value>
+ </data>
+ <data name="ParseError_Unterminated_String_Literal" xml:space="preserve">
+ <value>Unterminated string literal. Strings that start with a quotation mark (") must be terminated before the end of the line. However, strings that start with @ and a quotation mark (@") can span multiple lines.</value>
+ </data>
+ <data name="ParseError_InvalidOptionValue" xml:space="preserve">
+ <value>"{1}" is not a valid value for the "{0}" option. The "Option {0}" statement must be followed by either "On" or "Off". </value>
+ <comment>{0} is either Strict or Explicit and represent VB's "Option Strict" and "Option Explicit" keywords</comment>
+ </data>
+ <data name="ParseError_UnknownOption" xml:space="preserve">
+ <value>Unknown option: "{0}".</value>
+ </data>
+ <data name="ParseError_BlockNotTerminated" xml:space="preserve">
+ <value>The "{0}" block was not terminated. All "{0}" statements must be terminated with a matching "{1}".</value>
+ </data>
+ <data name="SectionExample_CS" xml:space="preserve">
+ <value>@section Header { ... }</value>
+ <comment>In CSHTML, the @section keyword is case-sensitive and lowercase (as with all C# keywords)</comment>
+ </data>
+ <data name="ParseError_TextTagCannotContainAttributes" xml:space="preserve">
+ <value>"&lt;text&gt;" and "&lt;/text&gt;" tags cannot contain attributes.</value>
+ </data>
+ <data name="ParseError_NamespaceOrTypeAliasExpected" xml:space="preserve">
+ <value>The "Imports" keyword must be followed by a namespace or a type alias on the same line.</value>
+ </data>
+ <data name="ParseError_Unexpected_WhiteSpace_At_Start_Of_CodeBlock_CS" xml:space="preserve">
+ <value>A space or line break was encountered after the "@" character. Only valid identifiers, keywords, comments, "(" and "{" are valid at the start of a code block and they must occur immediately following "@" with no space in between.</value>
+ </data>
+ <data name="ParseError_InheritsKeyword_Must_Be_Followed_By_TypeName" xml:space="preserve">
+ <value>The 'inherits' keyword must be followed by a type name on the same line.</value>
+ </data>
+ <data name="ParseError_OuterTagMissingName" xml:space="preserve">
+ <value>Outer tag is missing a name. The first character of a markup block must be an HTML tag with a valid name.</value>
+ </data>
+ <data name="ParseError_RazorComment_Not_Terminated" xml:space="preserve">
+ <value>End of file was reached before the end of the block comment. All comments that start with the "@*" sequence must be terminated with a matching "*@" sequence.</value>
+ </data>
+ <data name="ErrorComponent_Character" xml:space="preserve">
+ <value>"{0}" character</value>
+ </data>
+ <data name="ErrorComponent_EndOfFile" xml:space="preserve">
+ <value>end of file</value>
+ </data>
+ <data name="ErrorComponent_Whitespace" xml:space="preserve">
+ <value>space or line break</value>
+ </data>
+ <data name="ParseError_Unexpected_Character_At_Start_Of_CodeBlock_VB" xml:space="preserve">
+ <value>"{0}" is not valid at the start of a code block. Only identifiers, keywords, comments, and "(" are valid.</value>
+ </data>
+ <data name="ParseError_Unexpected_EndOfFile_At_Start_Of_CodeBlock" xml:space="preserve">
+ <value>End-of-file was found after the "@" character. "@" must be followed by a valid code block. If you want to output an "@", escape it using the sequence: "@@"</value>
+ </data>
+ <data name="ParseError_Unexpected_WhiteSpace_At_Start_Of_CodeBlock_VB" xml:space="preserve">
+ <value>A space or line break was encountered after the "@" character. Only valid identifiers, keywords, comments, and "(" are valid at the start of a code block and they must occur immediately following "@" with no space in between.</value>
+ </data>
+ <data name="Structure_Member_CannotBeNull" xml:space="preserve">
+ <value>The {0} property of the {1} structure cannot be null.</value>
+ </data>
+ <data name="ParseError_MissingCharAfterHelperParameters" xml:space="preserve">
+ <value>Expected a "{0}" after the helper parameters.</value>
+ </data>
+ <data name="ParseError_MissingCharAfterHelperName" xml:space="preserve">
+ <value>Expected a "{0}" after the helper name.</value>
+ </data>
+ <data name="ParseError_UnterminatedHelperParameterList" xml:space="preserve">
+ <value>Helper parameter list is missing a closing ")".</value>
+ </data>
+ <data name="ParseError_Helpers_Cannot_Be_Nested" xml:space="preserve">
+ <value>Helper blocks cannot be nested within each other.</value>
+ </data>
+ <data name="Parser_Context_Not_Set" xml:space="preserve">
+ <value>Parser was started with a null Context property. The Context property must be set BEFORE calling any methods on the parser.</value>
+ </data>
+ <data name="ParseError_Unexpected_Keyword_After_At" xml:space="preserve">
+ <value>Unexpected "{0}" keyword after "@" character. Once inside code, you do not need to prefix constructs like "{0}" with "@".</value>
+ </data>
+ <data name="ParseError_ReservedWord" xml:space="preserve">
+ <value>"{0}" is a reserved word and cannot be used in implicit expressions. An explicit expression ("@()") must be used.</value>
+ </data>
+ <data name="ParseError_Unexpected_Character_At_Helper_Name_Start" xml:space="preserve">
+ <value>Unexpected {0} after helper keyword. All helpers must have a name which starts with an "_" or alphabetic character. The remaining characters must be either "_" or alphanumeric.</value>
+ </data>
+ <data name="Tokenizer_CannotResumeSymbolUnlessIsPrevious" xml:space="preserve">
+ <value>Cannot resume this symbol. Only the symbol immediately preceding the current one can be resumed.</value>
+ </data>
+ <data name="ParserContext_NoCurrentBlock" xml:space="preserve">
+ <value>Cannot finish span, there is no current block. Call StartBlock at least once before finishing a span</value>
+ </data>
+ <data name="ParserContext_CannotCompleteTree_OutstandingBlocks" xml:space="preserve">
+ <value>Cannot complete the tree, there are still open blocks.</value>
+ </data>
+ <data name="ParserContext_CannotCompleteTree_NoRootBlock" xml:space="preserve">
+ <value>Cannot complete the tree, StartBlock must be called at least once.</value>
+ </data>
+ <data name="ParserContext_ParseComplete" xml:space="preserve">
+ <value>Cannot complete action, the parser has finished. Only CompleteParse can be called to extract the final parser results after the parser has finished</value>
+ </data>
+ <data name="Block_Type_Not_Specified" xml:space="preserve">
+ <value>Block cannot be built because a Type has not been specified in the BlockBuilder</value>
+ </data>
+ <data name="CSharpSymbol_CharacterLiteral" xml:space="preserve">
+ <value>&lt;&lt;character literal&gt;&gt;</value>
+ </data>
+ <data name="CSharpSymbol_Comment" xml:space="preserve">
+ <value>&lt;&lt;comment&gt;&gt;</value>
+ </data>
+ <data name="CSharpSymbol_Identifier" xml:space="preserve">
+ <value>&lt;&lt;identifier&gt;&gt;</value>
+ </data>
+ <data name="CSharpSymbol_IntegerLiteral" xml:space="preserve">
+ <value>&lt;&lt;integer literal&gt;&gt;</value>
+ </data>
+ <data name="CSharpSymbol_Keyword" xml:space="preserve">
+ <value>&lt;&lt;keyword&gt;&gt;</value>
+ </data>
+ <data name="CSharpSymbol_Newline" xml:space="preserve">
+ <value>&lt;&lt;newline sequence&gt;&gt;</value>
+ </data>
+ <data name="CSharpSymbol_RealLiteral" xml:space="preserve">
+ <value>&lt;&lt;real literal&gt;&gt;</value>
+ </data>
+ <data name="CSharpSymbol_StringLiteral" xml:space="preserve">
+ <value>&lt;&lt;string literal&gt;&gt;</value>
+ </data>
+ <data name="CSharpSymbol_Whitespace" xml:space="preserve">
+ <value>&lt;&lt;white space&gt;&gt;</value>
+ </data>
+ <data name="Symbol_Unknown" xml:space="preserve">
+ <value>&lt;&lt;unknown&gt;&gt;</value>
+ </data>
+ <data name="TokenizerView_CannotPutBack" xml:space="preserve">
+ <value>In order to put a symbol back, it must have been the symbol which ended at the current position. The specified symbol ends at {0}, but the current position is {1}</value>
+ </data>
+ <data name="ParseError_Unexpected" xml:space="preserve">
+ <value>Unexpected "{0}"</value>
+ </data>
+ <data name="ParseError_Unexpected_Nested_CodeBlock" xml:space="preserve">
+ <value>Unexpected "{" after "@" character. Once inside the body of a code block (@if {}, @{}, etc.) you do not need to use "@{" to switch to code.</value>
+ </data>
+ <data name="ErrorComponent_Newline" xml:space="preserve">
+ <value>line break</value>
+ </data>
+ <data name="SectionExample_VB" xml:space="preserve">
+ <value>@Section Header ... End Section</value>
+ <comment>In VBHTML, the @Section keyword is case-insensitive (as with all VB keywords), but the standard practice is to Title Case it</comment>
+ </data>
+ <data name="VBSymbol_CharacterLiteral" xml:space="preserve">
+ <value>&lt;&lt;character literal&gt;&gt;</value>
+ </data>
+ <data name="VBSymbol_Comment" xml:space="preserve">
+ <value>&lt;&lt;comment&gt;&gt;</value>
+ </data>
+ <data name="VBSymbol_DateLiteral" xml:space="preserve">
+ <value>&lt;&lt;date literal&gt;&gt;</value>
+ </data>
+ <data name="VBSymbol_FloatingPointLiteral" xml:space="preserve">
+ <value>&lt;&lt;floating point literal&gt;&gt;</value>
+ </data>
+ <data name="VBSymbol_Identifier" xml:space="preserve">
+ <value>&lt;&lt;identifier&gt;&gt;</value>
+ </data>
+ <data name="VBSymbol_IntegerLiteral" xml:space="preserve">
+ <value>&lt;&lt;integer literal&gt;&gt;</value>
+ </data>
+ <data name="VBSymbol_Keyword" xml:space="preserve">
+ <value>&lt;&lt;keyword&gt;&gt;</value>
+ </data>
+ <data name="VBSymbol_NewLine" xml:space="preserve">
+ <value>&lt;&lt;newline sequence&gt;&gt;</value>
+ </data>
+ <data name="VBSymbol_RazorComment" xml:space="preserve">
+ <value>&lt;&lt;razor comment&gt;&gt;</value>
+ </data>
+ <data name="VBSymbol_StringLiteral" xml:space="preserve">
+ <value>&lt;&lt;string literal&gt;&gt;</value>
+ </data>
+ <data name="VBSymbol_WhiteSpace" xml:space="preserve">
+ <value>&lt;&lt;white space&gt;&gt;</value>
+ </data>
+ <data name="HtmlSymbol_NewLine" xml:space="preserve">
+ <value>&lt;&lt;newline sequence&gt;&gt;</value>
+ </data>
+ <data name="HtmlSymbol_RazorComment" xml:space="preserve">
+ <value>&lt;&lt;razor comment&gt;&gt;</value>
+ </data>
+ <data name="HtmlSymbol_Text" xml:space="preserve">
+ <value>&lt;&lt;text&gt;&gt;</value>
+ </data>
+ <data name="HtmlSymbol_WhiteSpace" xml:space="preserve">
+ <value>&lt;&lt;white space&gt;&gt;</value>
+ </data>
+ <data name="ParserIsNotAMarkupParser" xml:space="preserve">
+ <value>The parser provided to the ParserContext was not a Markup Parser.</value>
+ </data>
+ <data name="Language_Does_Not_Support_RazorComment" xml:space="preserve">
+ <value>Cannot use built-in RazorComment handler, language characteristics does not define the CommentStart, CommentStar and CommentBody known symbol types or parser does not override TokenizerBackedParser.OutputSpanBeforeRazorComment</value>
+ </data>
+ <data name="ParserEror_SessionDirectiveMissingValue" xml:space="preserve">
+ <value>Missing value for session state directive.</value>
+ </data>
+ <data name="CreateCodeWriter_NoCodeWriter" xml:space="preserve">
+ <value>Cannot call CreateCodeWriter, a CodeWriter was not provided to the Create method</value>
+ <comment>This error should not be seen by users, it should only appear to internal developers, but I'm putting it in resources just in case</comment>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/System.Web.Razor/StateMachine.cs b/src/System.Web.Razor/StateMachine.cs
new file mode 100644
index 00000000..0bf3ee12
--- /dev/null
+++ b/src/System.Web.Razor/StateMachine.cs
@@ -0,0 +1,103 @@
+namespace System.Web.Razor
+{
+ public abstract class StateMachine<TReturn>
+ {
+ protected delegate StateResult State();
+
+ protected abstract State StartState { get; }
+
+ protected State CurrentState { get; set; }
+
+ protected virtual TReturn Turn()
+ {
+ if (CurrentState != null)
+ {
+ StateResult result;
+ do
+ {
+ // Keep running until we get a null result or output
+ result = CurrentState();
+ CurrentState = result.Next;
+ }
+ while (result != null && !result.HasOutput);
+
+ if (result == null)
+ {
+ return default(TReturn); // Terminated
+ }
+ return result.Output;
+ }
+ return default(TReturn);
+ }
+
+ /// <summary>
+ /// Returns a result indicating that the machine should stop executing and return null output.
+ /// </summary>
+ protected StateResult Stop()
+ {
+ return null;
+ }
+
+ /// <summary>
+ /// Returns a result indicating that this state has no output and the machine should immediately invoke the specified state
+ /// </summary>
+ /// <remarks>
+ /// By returning no output, the state machine will invoke the next state immediately, before returning
+ /// controller to the caller of <see cref="Turn"/>
+ /// </remarks>
+ protected StateResult Transition(State newState)
+ {
+ return new StateResult(newState);
+ }
+
+ /// <summary>
+ /// Returns a result containing the specified output and indicating that the next call to
+ /// <see cref="Turn"/> should invoke the provided state.
+ /// </summary>
+ protected StateResult Transition(TReturn output, State newState)
+ {
+ return new StateResult(output, newState);
+ }
+
+ /// <summary>
+ /// Returns a result indicating that this state has no output and the machine should remain in this state
+ /// </summary>
+ /// <remarks>
+ /// By returning no output, the state machine will re-invoke the current state again before returning
+ /// controller to the caller of <see cref="Turn"/>
+ /// </remarks>
+ protected StateResult Stay()
+ {
+ return new StateResult(CurrentState);
+ }
+
+ /// <summary>
+ /// Returns a result containing the specified output and indicating that the next call to
+ /// <see cref="Turn"/> should re-invoke the current state.
+ /// </summary>
+ protected StateResult Stay(TReturn output)
+ {
+ return new StateResult(output, CurrentState);
+ }
+
+ protected class StateResult
+ {
+ public StateResult(State next)
+ {
+ HasOutput = false;
+ Next = next;
+ }
+
+ public StateResult(TReturn output, State next)
+ {
+ HasOutput = true;
+ Output = output;
+ Next = next;
+ }
+
+ public bool HasOutput { get; set; }
+ public TReturn Output { get; set; }
+ public State Next { get; set; }
+ }
+ }
+}
diff --git a/src/System.Web.Razor/System.Web.Razor.csproj b/src/System.Web.Razor/System.Web.Razor.csproj
new file mode 100644
index 00000000..6389c7f8
--- /dev/null
+++ b/src/System.Web.Razor/System.Web.Razor.csproj
@@ -0,0 +1,236 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <CodeAnalysis Condition=" '$(CodeAnalysis)' == '' ">false</CodeAnalysis>
+ <ProductVersion>9.0.30729</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{8F18041B-9410-4C36-A9C5-067813DF5F31}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <RootNamespace>System.Web.Razor</RootNamespace>
+ <AssemblyName>System.Web.Razor</AssemblyName>
+ <FileAlignment>512</FileAlignment>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>..\..\bin\Debug\</OutputPath>
+ <DefineConstants>TRACE;DEBUG;ASPNETWEBPAGES</DefineConstants>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ <DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
+ <NoWarn>1591</NoWarn>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>..\..\bin\Release\</OutputPath>
+ <DefineConstants>TRACE;ASPNETWEBPAGES</DefineConstants>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ <RunCodeAnalysis>$(CodeAnalysis)</RunCodeAnalysis>
+ <DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
+ <NoWarn>1591</NoWarn>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'CodeCoverage|AnyCPU'">
+ <DebugSymbols>true</DebugSymbols>
+ <OutputPath>..\..\bin\CodeCoverage\</OutputPath>
+ <DefineConstants>TRACE;DEBUG;CODE_COVERAGE;ASPNETWEBPAGES</DefineConstants>
+ <DebugType>full</DebugType>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="System" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="..\CommonAssemblyInfo.cs">
+ <Link>Properties\CommonAssemblyInfo.cs</Link>
+ </Compile>
+ <Compile Include="..\CommonResources.Designer.cs">
+ <Link>Common\CommonResources.Designer.cs</Link>
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>CommonResources.resx</DependentUpon>
+ </Compile>
+ <Compile Include="..\ExceptionHelper.cs">
+ <Link>Common\ExceptionHelper.cs</Link>
+ </Compile>
+ <Compile Include="..\GlobalSuppressions.cs">
+ <Link>Common\GlobalSuppressions.cs</Link>
+ </Compile>
+ <Compile Include="..\HashCodeCombiner.cs">
+ <Link>Common\HashCodeCombiner.cs</Link>
+ </Compile>
+ <Compile Include="..\TransparentCommonAssemblyInfo.cs">
+ <Link>Properties\TransparentCommonAssemblyInfo.cs</Link>
+ </Compile>
+ <Compile Include="DocumentParseCompleteEventArgs.cs" />
+ <Compile Include="Editor\AutoCompleteEditHandler.cs" />
+ <Compile Include="Editor\BackgroundParseTask.cs" />
+ <Compile Include="Generator\AttributeBlockCodeGenerator.cs" />
+ <Compile Include="Generator\CodeWriterExtensions.cs" />
+ <Compile Include="Generator\DynamicAttributeBlockCodeGenerator.cs" />
+ <Compile Include="Generator\ExpressionRenderingMode.cs" />
+ <Compile Include="Generator\LiteralAttributeCodeGenerator.cs" />
+ <Compile Include="Generator\RazorCommentCodeGenerator.cs" />
+ <Compile Include="Generator\SetLayoutCodeGenerator.cs" />
+ <Compile Include="Editor\SingleLineMarkupEditHandler.cs" />
+ <Compile Include="Editor\SpanEditHandler.cs" />
+ <Compile Include="Editor\EditResult.cs" />
+ <Compile Include="Editor\ImplicitExpressionEditHandler.cs" />
+ <Compile Include="Editor\EditorHints.cs" />
+ <Compile Include="Generator\AddImportCodeGenerator.cs" />
+ <Compile Include="Generator\BlockCodeGenerator.cs" />
+ <Compile Include="Generator\CodeGeneratorBase.cs" />
+ <Compile Include="Generator\CodeGeneratorContext.cs" />
+ <Compile Include="Generator\CodeWriter.cs" />
+ <Compile Include="Generator\BaseCodeWriter.cs" />
+ <Compile Include="Generator\RazorDirectiveAttributeCodeGenerator.cs" />
+ <Compile Include="Generator\CSharpCodeWriter.cs" />
+ <Compile Include="Generator\ExpressionCodeGenerator.cs" />
+ <Compile Include="Generator\GeneratedClassContext.cs" />
+ <Compile Include="Generator\HybridCodeGenerator.cs" />
+ <Compile Include="Generator\IBlockCodeGenerator.cs" />
+ <Compile Include="Generator\ISpanCodeGenerator.cs" />
+ <Compile Include="Generator\MarkupCodeGenerator.cs" />
+ <Compile Include="Generator\ResolveUrlCodeGenerator.cs" />
+ <Compile Include="Generator\SetBaseTypeCodeGenerator.cs" />
+ <Compile Include="Generator\HelperCodeGenerator.cs" />
+ <Compile Include="Generator\SectionCodeGenerator.cs" />
+ <Compile Include="Generator\SetVBOptionCodeGenerator.cs" />
+ <Compile Include="Generator\SpanCodeGenerator.cs" />
+ <Compile Include="Generator\StatementCodeGenerator.cs" />
+ <Compile Include="Generator\TemplateBlockCodeGenerator.cs" />
+ <Compile Include="Generator\TypeMemberCodeGenerator.cs" />
+ <Compile Include="Generator\VBCodeWriter.cs" />
+ <Compile Include="Parser\BalancingModes.cs" />
+ <Compile Include="Parser\CallbackVisitor.cs" />
+ <Compile Include="Parser\ConditionalAttributeCollapser.cs" />
+ <Compile Include="Parser\CSharpCodeParser.Directives.cs" />
+ <Compile Include="Parser\CSharpCodeParser.Statements.cs" />
+ <Compile Include="Parser\CSharpCodeParser.cs" />
+ <Compile Include="Parser\CSharpLanguageCharacteristics.cs" />
+ <Compile Include="Parser\HtmlLanguageCharacteristics.cs" />
+ <Compile Include="Parser\HtmlMarkupParser.Block.cs" />
+ <Compile Include="Parser\HtmlMarkupParser.Document.cs" />
+ <Compile Include="Parser\HtmlMarkupParser.cs" />
+ <Compile Include="Parser\HtmlMarkupParser.Section.cs" />
+ <Compile Include="Parser\ISyntaxTreeRewriter.cs" />
+ <Compile Include="Parser\LanguageCharacteristics.cs" />
+ <Compile Include="Parser\MarkupCollapser.cs" />
+ <Compile Include="Parser\MarkupRewriter.cs" />
+ <Compile Include="Parser\ParserHelpers.cs" />
+ <Compile Include="Parser\ParserVisitor.cs" />
+ <Compile Include="Parser\ParserVisitorExtensions.cs" />
+ <Compile Include="Parser\SyntaxConstants.cs" />
+ <Compile Include="Parser\SyntaxTree\AcceptedCharacters.cs" />
+ <Compile Include="Parser\SyntaxTree\BlockBuilder.cs" />
+ <Compile Include="Parser\SyntaxTree\EquivalenceComparer.cs" />
+ <Compile Include="Parser\SyntaxTree\SpanBuilder.cs" />
+ <Compile Include="Parser\TextReaderExtensions.cs" />
+ <Compile Include="Parser\TokenizerBackedParser.cs" />
+ <Compile Include="Parser\TokenizerBackedParser.Helpers.cs" />
+ <Compile Include="Parser\VBCodeParser.cs" />
+ <Compile Include="Parser\VBCodeParser.Directives.cs" />
+ <Compile Include="Parser\VBLanguageCharacteristics.cs" />
+ <Compile Include="Parser\WhitespaceRewriter.cs" />
+ <Compile Include="PartialParseResult.cs" />
+ <Compile Include="RazorDebugHelpers.cs" />
+ <Compile Include="RazorDirectiveAttribute.cs" />
+ <Compile Include="RazorEditorParser.cs" />
+ <Compile Include="Generator\CodeGenerationCompleteEventArgs.cs" />
+ <Compile Include="Generator\GeneratedCodeMapping.cs" />
+ <Compile Include="Generator\RazorCodeGenerator.cs" />
+ <Compile Include="GeneratorResults.cs" />
+ <Compile Include="RazorEngineHost.cs" />
+ <Compile Include="RazorTemplateEngine.cs" />
+ <Compile Include="StateMachine.cs" />
+ <Compile Include="Text\BufferingTextReader.cs" />
+ <Compile Include="Text\LookaheadToken.cs" />
+ <Compile Include="Text\LocationTagged.cs" />
+ <Compile Include="Text\SeekableTextReader.cs" />
+ <Compile Include="Text\LineTrackingStringBuffer.cs" />
+ <Compile Include="Text\LookaheadTextReader.cs" />
+ <Compile Include="Text\TextExtensions.cs" />
+ <Compile Include="Text\TextBufferReader.cs" />
+ <Compile Include="Text\TextDocumentReader.cs" />
+ <Compile Include="Tokenizer\ITokenizer.cs" />
+ <Compile Include="Tokenizer\Symbols\ISymbol.cs" />
+ <Compile Include="Tokenizer\Symbols\KnownSymbolType.cs" />
+ <Compile Include="Tokenizer\Symbols\SymbolExtensions.cs" />
+ <Compile Include="Tokenizer\Symbols\SymbolTypeSuppressions.cs" />
+ <Compile Include="Tokenizer\TokenizerView.cs" />
+ <Compile Include="Tokenizer\VBKeywordDetector.cs" />
+ <Compile Include="Tokenizer\VBHelpers.cs" />
+ <Compile Include="Tokenizer\CSharpHelpers.cs" />
+ <Compile Include="Tokenizer\CSharpKeywordDetector.cs" />
+ <Compile Include="Tokenizer\CSharpTokenizer.cs" />
+ <Compile Include="Tokenizer\HtmlTokenizer.cs" />
+ <Compile Include="Tokenizer\Symbols\VBKeyword.cs" />
+ <Compile Include="Tokenizer\Symbols\VBSymbol.cs" />
+ <Compile Include="Tokenizer\Symbols\VBSymbolType.cs" />
+ <Compile Include="Tokenizer\Symbols\CSharpKeyword.cs" />
+ <Compile Include="Tokenizer\Symbols\CSharpSymbol.cs" />
+ <Compile Include="Tokenizer\Symbols\CSharpSymbolType.cs" />
+ <Compile Include="Tokenizer\Symbols\HtmlSymbol.cs" />
+ <Compile Include="Tokenizer\Symbols\HtmlSymbolType.cs" />
+ <Compile Include="Tokenizer\Symbols\SymbolBase.cs" />
+ <Compile Include="Tokenizer\Tokenizer.cs" />
+ <Compile Include="Tokenizer\VBTokenizer.cs" />
+ <Compile Include="Tokenizer\XmlHelpers.cs" />
+ <Compile Include="Utils\CharUtils.cs" />
+ <Compile Include="VBRazorCodeLanguage.cs" />
+ <Compile Include="Generator\CSharpRazorCodeGenerator.cs" />
+ <Compile Include="Parser\SyntaxTree\BlockType.cs" />
+ <Compile Include="Parser\SyntaxTree\Block.cs" />
+ <Compile Include="Parser\SyntaxTree\RazorError.cs" />
+ <Compile Include="ParserResults.cs" />
+ <Compile Include="Parser\SyntaxTree\SyntaxTreeNode.cs" />
+ <Compile Include="Parser\SyntaxTree\SpanKind.cs" />
+ <Compile Include="Parser\ParserContext.cs" />
+ <Compile Include="Parser\RazorParser.cs" />
+ <Compile Include="Text\ITextBuffer.cs" />
+ <Compile Include="Text\SourceLocationTracker.cs" />
+ <Compile Include="RazorCodeLanguage.cs" />
+ <Compile Include="CSharpRazorCodeLanguage.cs" />
+ <Compile Include="Utils\DisposableAction.cs" />
+ <Compile Include="Utils\EnumUtil.cs" />
+ <Compile Include="Utils\EnumeratorExtensions.cs" />
+ <Compile Include="GlobalSuppressions.cs" />
+ <Compile Include="Parser\ParserBase.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="Resources\RazorResources.Designer.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>RazorResources.resx</DependentUpon>
+ </Compile>
+ <Compile Include="Text\SourceLocation.cs" />
+ <Compile Include="Text\TextChange.cs" />
+ <Compile Include="Text\TextChangeType.cs" />
+ <Compile Include="Parser\SyntaxTree\Span.cs" />
+ <Compile Include="Generator\VBRazorCodeGenerator.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="..\CommonResources.resx">
+ <Link>Common\CommonResources.resx</Link>
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>CommonResources.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Include="Resources\RazorResources.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>RazorResources.Designer.cs</LastGenOutput>
+ <SubType>Designer</SubType>
+ </EmbeddedResource>
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="Parser\VBCodeParser.Statements.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <CodeAnalysisDictionary Include="..\CodeAnalysisDictionary.xml" />
+ </ItemGroup>
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+</Project> \ No newline at end of file
diff --git a/src/System.Web.Razor/Text/BufferingTextReader.cs b/src/System.Web.Razor/Text/BufferingTextReader.cs
new file mode 100644
index 00000000..2b319fef
--- /dev/null
+++ b/src/System.Web.Razor/Text/BufferingTextReader.cs
@@ -0,0 +1,198 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Utils;
+
+namespace System.Web.Razor.Text
+{
+ public class BufferingTextReader : LookaheadTextReader
+ {
+ private Stack<BacktrackContext> _backtrackStack = new Stack<BacktrackContext>();
+ private int _currentBufferPosition;
+
+ private int _currentCharacter;
+ private SourceLocationTracker _locationTracker;
+
+ public BufferingTextReader(TextReader source)
+ {
+ if (source == null)
+ {
+ throw new ArgumentNullException("source");
+ }
+
+ InnerReader = source;
+ _locationTracker = new SourceLocationTracker();
+
+ UpdateCurrentCharacter();
+ }
+
+ internal StringBuilder Buffer { get; set; }
+ internal bool Buffering { get; set; }
+ internal TextReader InnerReader { get; private set; }
+
+ public override SourceLocation CurrentLocation
+ {
+ get { return _locationTracker.CurrentLocation; }
+ }
+
+ protected virtual int CurrentCharacter
+ {
+ get { return _currentCharacter; }
+ }
+
+ public override int Read()
+ {
+ int ch = CurrentCharacter;
+ NextCharacter();
+ return ch;
+ }
+
+ // TODO: Optimize Read(char[],int,int) to copy direct from the buffer where possible
+
+ public override int Peek()
+ {
+ return CurrentCharacter;
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ InnerReader.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ public override IDisposable BeginLookahead()
+ {
+ // Is this our first lookahead?
+ if (Buffer == null)
+ {
+ // Yes, setup the backtrack buffer
+ Buffer = new StringBuilder();
+ }
+
+ if (!Buffering)
+ {
+ // We're not already buffering, so we need to expand the buffer to hold the first character
+ ExpandBuffer();
+ Buffering = true;
+ }
+
+ // Mark the position to return to when we backtrack
+ // Use the closures and the "using" statement rather than an explicit stack
+ BacktrackContext context = new BacktrackContext()
+ {
+ BufferIndex = _currentBufferPosition,
+ Location = CurrentLocation
+ };
+ _backtrackStack.Push(context);
+ return new DisposableAction(() =>
+ {
+ EndLookahead(context);
+ });
+ }
+
+ // REVIEW: This really doesn't sound like the best name for this...
+ public override void CancelBacktrack()
+ {
+ if (_backtrackStack.Count == 0)
+ {
+ throw new InvalidOperationException(RazorResources.CancelBacktrack_Must_Be_Called_Within_Lookahead);
+ }
+ // Just pop the current backtrack context so that when the lookahead ends, it won't be backtracked
+ _backtrackStack.Pop();
+ }
+
+ private void EndLookahead(BacktrackContext context)
+ {
+ // If the specified context is not the one on the stack, it was popped by a call to DoNotBacktrack
+ if (_backtrackStack.Count > 0 && ReferenceEquals(_backtrackStack.Peek(), context))
+ {
+ _backtrackStack.Pop();
+ _currentBufferPosition = context.BufferIndex;
+ _locationTracker.CurrentLocation = context.Location;
+
+ UpdateCurrentCharacter();
+ }
+ }
+
+ protected virtual void NextCharacter()
+ {
+ int prevChar = CurrentCharacter;
+ if (prevChar == -1)
+ {
+ return; // We're at the end of the source
+ }
+
+ if (Buffering)
+ {
+ if (_currentBufferPosition >= Buffer.Length - 1)
+ {
+ // If there are no more lookaheads (thus no need to continue with the buffer) we can just clean up the buffer
+ if (_backtrackStack.Count == 0)
+ {
+ // Reset the buffer
+ Buffer.Length = 0;
+ _currentBufferPosition = 0;
+ Buffering = false;
+ }
+ else if (!ExpandBuffer())
+ {
+ // Failed to expand the buffer, because we're at the end of the source
+ _currentBufferPosition = Buffer.Length; // Force the position past the end of the buffer
+ }
+ }
+ else
+ {
+ // Not at the end yet, just advance the buffer pointer
+ _currentBufferPosition++;
+ }
+ }
+ else
+ {
+ // Just act like normal
+ InnerReader.Read(); // Don't care about the return value, Peek() is used to get characters from the source
+ }
+
+ UpdateCurrentCharacter();
+ _locationTracker.UpdateLocation((char)prevChar, (char)CurrentCharacter);
+ }
+
+ protected bool ExpandBuffer()
+ {
+ // Pull another character into the buffer and update the position
+ int ch = InnerReader.Read();
+
+ // Only append the character to the buffer if there actually is one
+ if (ch != -1)
+ {
+ Buffer.Append((char)ch);
+ _currentBufferPosition = Buffer.Length - 1;
+ return true;
+ }
+ return false;
+ }
+
+ private void UpdateCurrentCharacter()
+ {
+ if (Buffering && _currentBufferPosition < Buffer.Length)
+ {
+ // Read from the buffer
+ _currentCharacter = (int)Buffer[_currentBufferPosition];
+ }
+ else
+ {
+ // No buffer? Peek from the source
+ _currentCharacter = InnerReader.Peek();
+ }
+ }
+
+ private class BacktrackContext
+ {
+ public int BufferIndex { get; set; }
+ public SourceLocation Location { get; set; }
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Text/ITextBuffer.cs b/src/System.Web.Razor/Text/ITextBuffer.cs
new file mode 100644
index 00000000..afe5f92c
--- /dev/null
+++ b/src/System.Web.Razor/Text/ITextBuffer.cs
@@ -0,0 +1,16 @@
+namespace System.Web.Razor.Text
+{
+ public interface ITextBuffer
+ {
+ int Length { get; }
+ int Position { get; set; }
+ int Read();
+ int Peek();
+ }
+
+ // TextBuffer with Location tracking
+ public interface ITextDocument : ITextBuffer
+ {
+ SourceLocation Location { get; }
+ }
+}
diff --git a/src/System.Web.Razor/Text/LineTrackingStringBuffer.cs b/src/System.Web.Razor/Text/LineTrackingStringBuffer.cs
new file mode 100644
index 00000000..45594992
--- /dev/null
+++ b/src/System.Web.Razor/Text/LineTrackingStringBuffer.cs
@@ -0,0 +1,159 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Text;
+using System.Web.Razor.Parser;
+
+namespace System.Web.Razor.Text
+{
+ internal class LineTrackingStringBuffer
+ {
+ private TextLine _currentLine;
+ private TextLine _endLine;
+ private IList<TextLine> _lines;
+
+ public LineTrackingStringBuffer()
+ {
+ _endLine = new TextLine(0, 0);
+ _lines = new List<TextLine>() { _endLine };
+ }
+
+ public int Length
+ {
+ get { return _endLine.End; }
+ }
+
+ public SourceLocation EndLocation
+ {
+ get { return new SourceLocation(Length, _lines.Count - 1, _lines[_lines.Count - 1].Length); }
+ }
+
+ public void Append(string content)
+ {
+ for (int i = 0; i < content.Length; i++)
+ {
+ AppendCore(content[i]);
+
+ // \r on it's own: Start a new line, otherwise wait for \n
+ // Other Newline: Start a new line
+ if ((content[i] == '\r' && (i + 1 == content.Length || content[i + 1] != '\n')) || (content[i] != '\r' && ParserHelpers.IsNewLine(content[i])))
+ {
+ PushNewLine();
+ }
+ }
+ }
+
+ public CharacterReference CharAt(int absoluteIndex)
+ {
+ TextLine line = FindLine(absoluteIndex);
+ if (line == null)
+ {
+ throw new ArgumentOutOfRangeException("absoluteIndex");
+ }
+ int idx = absoluteIndex - line.Start;
+ return new CharacterReference(line.Content[idx], new SourceLocation(absoluteIndex, line.Index, idx));
+ }
+
+ private void PushNewLine()
+ {
+ _endLine = new TextLine(_endLine.End, _endLine.Index + 1);
+ _lines.Add(_endLine);
+ }
+
+ private void AppendCore(char chr)
+ {
+ Debug.Assert(_lines.Count > 0);
+ _lines[_lines.Count - 1].Content.Append(chr);
+ }
+
+ private TextLine FindLine(int absoluteIndex)
+ {
+ TextLine selected = null;
+
+ if (_currentLine != null)
+ {
+ if (_currentLine.Contains(absoluteIndex))
+ {
+ // This index is on the last read line
+ selected = _currentLine;
+ }
+ else if (absoluteIndex > _currentLine.Index && _currentLine.Index + 1 < _lines.Count)
+ {
+ // This index is ahead of the last read line
+ selected = ScanLines(absoluteIndex, _currentLine.Index);
+ }
+ }
+
+ // Have we found a line yet?
+ if (selected == null)
+ {
+ // Scan from line 0
+ selected = ScanLines(absoluteIndex, 0);
+ }
+
+ Debug.Assert(selected == null || selected.Contains(absoluteIndex));
+ _currentLine = selected;
+ return selected;
+ }
+
+ private TextLine ScanLines(int absoluteIndex, int startPos)
+ {
+ for (int i = 0; i < _lines.Count; i++)
+ {
+ int idx = (i + startPos) % _lines.Count;
+ Debug.Assert(idx >= 0 && idx < _lines.Count);
+
+ if (_lines[idx].Contains(absoluteIndex))
+ {
+ return _lines[idx];
+ }
+ }
+ return null;
+ }
+
+ internal class CharacterReference
+ {
+ public CharacterReference(char character, SourceLocation location)
+ {
+ Character = character;
+ Location = location;
+ }
+
+ public char Character { get; private set; }
+ public SourceLocation Location { get; private set; }
+ }
+
+ private class TextLine
+ {
+ private StringBuilder _content = new StringBuilder();
+
+ public TextLine(int start, int index)
+ {
+ Start = start;
+ Index = index;
+ }
+
+ public StringBuilder Content
+ {
+ get { return _content; }
+ }
+
+ public int Length
+ {
+ get { return Content.Length; }
+ }
+
+ public int Start { get; set; }
+ public int Index { get; set; }
+
+ public int End
+ {
+ get { return Start + Length; }
+ }
+
+ public bool Contains(int index)
+ {
+ return index < End && index >= Start;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Text/LocationTagged.cs b/src/System.Web.Razor/Text/LocationTagged.cs
new file mode 100644
index 00000000..17509824
--- /dev/null
+++ b/src/System.Web.Razor/Text/LocationTagged.cs
@@ -0,0 +1,90 @@
+using System.Diagnostics;
+using System.Globalization;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.Razor.Text
+{
+ [DebuggerDisplay("({Location})\"{Value}\"")]
+ public class LocationTagged<T> : IFormattable
+ {
+ private LocationTagged()
+ {
+ Location = SourceLocation.Undefined;
+ Value = default(T);
+ }
+
+ public LocationTagged(T value, int offset, int line, int col)
+ : this(value, new SourceLocation(offset, line, col))
+ {
+ }
+
+ public LocationTagged(T value, SourceLocation location)
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException("value");
+ }
+
+ Location = location;
+ Value = value;
+ }
+
+ public SourceLocation Location { get; private set; }
+ public T Value { get; private set; }
+
+ public override bool Equals(object obj)
+ {
+ LocationTagged<T> other = obj as LocationTagged<T>;
+ return other != null &&
+ Equals(other.Location, Location) &&
+ Equals(other.Value, Value);
+ }
+
+ public override int GetHashCode()
+ {
+ return HashCodeCombiner.Start()
+ .Add(Location)
+ .Add(Value)
+ .CombinedHash;
+ }
+
+ public override string ToString()
+ {
+ return Value.ToString();
+ }
+
+ public string ToString(string format, IFormatProvider formatProvider)
+ {
+ if (String.IsNullOrEmpty(format))
+ {
+ format = "P";
+ }
+ if (formatProvider == null)
+ {
+ formatProvider = CultureInfo.CurrentCulture;
+ }
+ switch (format.ToUpperInvariant())
+ {
+ case "F":
+ return String.Format(formatProvider, "{0}@{1}", Value, Location);
+ default:
+ return Value.ToString();
+ }
+ }
+
+ public static implicit operator T(LocationTagged<T> value)
+ {
+ return value.Value;
+ }
+
+ public static bool operator ==(LocationTagged<T> left, LocationTagged<T> right)
+ {
+ return Equals(left, right);
+ }
+
+ public static bool operator !=(LocationTagged<T> left, LocationTagged<T> right)
+ {
+ return !Equals(left, right);
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Text/LookaheadTextReader.cs b/src/System.Web.Razor/Text/LookaheadTextReader.cs
new file mode 100644
index 00000000..5451657d
--- /dev/null
+++ b/src/System.Web.Razor/Text/LookaheadTextReader.cs
@@ -0,0 +1,11 @@
+using System.IO;
+
+namespace System.Web.Razor.Text
+{
+ public abstract class LookaheadTextReader : TextReader
+ {
+ public abstract SourceLocation CurrentLocation { get; }
+ public abstract IDisposable BeginLookahead();
+ public abstract void CancelBacktrack();
+ }
+}
diff --git a/src/System.Web.Razor/Text/LookaheadToken.cs b/src/System.Web.Razor/Text/LookaheadToken.cs
new file mode 100644
index 00000000..01fe3871
--- /dev/null
+++ b/src/System.Web.Razor/Text/LookaheadToken.cs
@@ -0,0 +1,32 @@
+namespace System.Web.Razor.Text
+{
+ public class LookaheadToken : IDisposable
+ {
+ private Action _cancelAction;
+ private bool _accepted;
+
+ public LookaheadToken(Action cancelAction)
+ {
+ _cancelAction = cancelAction;
+ }
+
+ public void Accept()
+ {
+ _accepted = true;
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_accepted)
+ {
+ _cancelAction();
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Text/SeekableTextReader.cs b/src/System.Web.Razor/Text/SeekableTextReader.cs
new file mode 100644
index 00000000..db67aad6
--- /dev/null
+++ b/src/System.Web.Razor/Text/SeekableTextReader.cs
@@ -0,0 +1,97 @@
+using System.IO;
+
+namespace System.Web.Razor.Text
+{
+ public class SeekableTextReader : TextReader, ITextDocument
+ {
+ private int _position = 0;
+ private LineTrackingStringBuffer _buffer = new LineTrackingStringBuffer();
+ private SourceLocation _location = SourceLocation.Zero;
+ private char? _current;
+
+ public SeekableTextReader(string content)
+ {
+ _buffer.Append(content);
+ UpdateState();
+ }
+
+ public SeekableTextReader(TextReader source)
+ : this(source.ReadToEnd())
+ {
+ }
+
+ public SeekableTextReader(ITextBuffer buffer)
+ : this(buffer.ReadToEnd())
+ {
+ }
+
+ public SourceLocation Location
+ {
+ get { return _location; }
+ }
+
+ public int Length
+ {
+ get { return _buffer.Length; }
+ }
+
+ public int Position
+ {
+ get { return _position; }
+ set
+ {
+ if (_position != value)
+ {
+ _position = value;
+ UpdateState();
+ }
+ }
+ }
+
+ internal LineTrackingStringBuffer Buffer
+ {
+ get { return _buffer; }
+ }
+
+ public override int Read()
+ {
+ if (_current == null)
+ {
+ return -1;
+ }
+ char chr = _current.Value;
+ _position++;
+ UpdateState();
+ return chr;
+ }
+
+ public override int Peek()
+ {
+ if (_current == null)
+ {
+ return -1;
+ }
+ return _current.Value;
+ }
+
+ private void UpdateState()
+ {
+ if (_position < _buffer.Length)
+ {
+ LineTrackingStringBuffer.CharacterReference chr = _buffer.CharAt(_position);
+ _current = chr.Character;
+ _location = chr.Location;
+ }
+ else if (_buffer.Length == 0)
+ {
+ _current = null;
+ _location = SourceLocation.Zero;
+ }
+ else
+ {
+ _current = null;
+ _location = _buffer.EndLocation;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Text/SourceLocation.cs b/src/System.Web.Razor/Text/SourceLocation.cs
new file mode 100644
index 00000000..123baf64
--- /dev/null
+++ b/src/System.Web.Razor/Text/SourceLocation.cs
@@ -0,0 +1,136 @@
+using System.Globalization;
+
+namespace System.Web.Razor.Text
+{
+ [Serializable]
+ public struct SourceLocation : IEquatable<SourceLocation>, IComparable<SourceLocation>
+ {
+ public static readonly SourceLocation Undefined = CreateUndefined();
+ public static readonly SourceLocation Zero = new SourceLocation(0, 0, 0);
+
+ private int _absoluteIndex;
+ private int _lineIndex;
+ private int _characterIndex;
+
+ public SourceLocation(int absoluteIndex, int lineIndex, int characterIndex)
+ {
+ _absoluteIndex = absoluteIndex;
+ _lineIndex = lineIndex;
+ _characterIndex = characterIndex;
+ }
+
+ public int AbsoluteIndex
+ {
+ get { return _absoluteIndex; }
+ }
+
+ public int LineIndex
+ {
+ get { return _lineIndex; }
+ }
+
+ public int CharacterIndex
+ {
+ get { return _characterIndex; }
+ }
+
+ public override string ToString()
+ {
+ return String.Format(CultureInfo.CurrentCulture, "({0}:{1},{2})", AbsoluteIndex, LineIndex, CharacterIndex);
+ }
+
+ public override bool Equals(object obj)
+ {
+ return (obj is SourceLocation) && Equals((SourceLocation)obj);
+ }
+
+ public override int GetHashCode()
+ {
+ // LineIndex and CharacterIndex can be calculated from AbsoluteIndex and the document content.
+ return AbsoluteIndex;
+ }
+
+ public bool Equals(SourceLocation other)
+ {
+ if (other == null)
+ {
+ throw new ArgumentNullException("other");
+ }
+
+ return AbsoluteIndex == other.AbsoluteIndex &&
+ LineIndex == other.LineIndex &&
+ CharacterIndex == other.CharacterIndex;
+ }
+
+ public int CompareTo(SourceLocation other)
+ {
+ return AbsoluteIndex.CompareTo(other.AbsoluteIndex);
+ }
+
+ public static SourceLocation Advance(SourceLocation left, string text)
+ {
+ SourceLocationTracker tracker = new SourceLocationTracker(left);
+ tracker.UpdateLocation(text);
+ return tracker.CurrentLocation;
+ }
+
+ public static SourceLocation Add(SourceLocation left, SourceLocation right)
+ {
+ if (right.LineIndex > 0)
+ {
+ // Column index doesn't matter
+ return new SourceLocation(left.AbsoluteIndex + right.AbsoluteIndex, left.LineIndex + right.LineIndex, right.CharacterIndex);
+ }
+ else
+ {
+ return new SourceLocation(left.AbsoluteIndex + right.AbsoluteIndex, left.LineIndex + right.LineIndex, left.CharacterIndex + right.CharacterIndex);
+ }
+ }
+
+ public static SourceLocation Subtract(SourceLocation left, SourceLocation right)
+ {
+ return new SourceLocation(left.AbsoluteIndex - right.AbsoluteIndex,
+ left.LineIndex - right.LineIndex,
+ left.LineIndex != right.LineIndex ? left.CharacterIndex : left.CharacterIndex - right.CharacterIndex);
+ }
+
+ private static SourceLocation CreateUndefined()
+ {
+ SourceLocation sl = new SourceLocation();
+ sl._absoluteIndex = -1;
+ sl._lineIndex = -1;
+ sl._characterIndex = -1;
+ return sl;
+ }
+
+ public static bool operator <(SourceLocation left, SourceLocation right)
+ {
+ return left.CompareTo(right) < 0;
+ }
+
+ public static bool operator >(SourceLocation left, SourceLocation right)
+ {
+ return left.CompareTo(right) > 0;
+ }
+
+ public static bool operator ==(SourceLocation left, SourceLocation right)
+ {
+ return left.Equals(right);
+ }
+
+ public static bool operator !=(SourceLocation left, SourceLocation right)
+ {
+ return !left.Equals(right);
+ }
+
+ public static SourceLocation operator +(SourceLocation left, SourceLocation right)
+ {
+ return Add(left, right);
+ }
+
+ public static SourceLocation operator -(SourceLocation left, SourceLocation right)
+ {
+ return Subtract(left, right);
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Text/SourceLocationTracker.cs b/src/System.Web.Razor/Text/SourceLocationTracker.cs
new file mode 100644
index 00000000..a6ec4f9d
--- /dev/null
+++ b/src/System.Web.Razor/Text/SourceLocationTracker.cs
@@ -0,0 +1,85 @@
+using System.Web.Razor.Parser;
+
+namespace System.Web.Razor.Text
+{
+ public class SourceLocationTracker
+ {
+ private int _absoluteIndex = 0;
+ private int _characterIndex = 0;
+ private int _lineIndex = 0;
+ private SourceLocation _currentLocation;
+
+ public SourceLocationTracker()
+ : this(SourceLocation.Zero)
+ {
+ }
+
+ public SourceLocationTracker(SourceLocation currentLocation)
+ {
+ CurrentLocation = currentLocation;
+
+ UpdateInternalState();
+ }
+
+ public SourceLocation CurrentLocation
+ {
+ get { return _currentLocation; }
+ set
+ {
+ if (_currentLocation != value)
+ {
+ _currentLocation = value;
+ UpdateInternalState();
+ }
+ }
+ }
+
+ public void UpdateLocation(char characterRead, char nextCharacter)
+ {
+ _absoluteIndex++;
+
+ if (ParserHelpers.IsNewLine(characterRead) && (characterRead != '\r' || nextCharacter != '\n'))
+ {
+ _lineIndex++;
+ _characterIndex = 0;
+ }
+ else
+ {
+ _characterIndex++;
+ }
+
+ UpdateLocation();
+ }
+
+ public SourceLocationTracker UpdateLocation(string content)
+ {
+ for (int i = 0; i < content.Length; i++)
+ {
+ char nextCharacter = '\0';
+ if (i < content.Length - 1)
+ {
+ nextCharacter = content[i + 1];
+ }
+ UpdateLocation(content[i], nextCharacter);
+ }
+ return this;
+ }
+
+ private void UpdateInternalState()
+ {
+ _absoluteIndex = CurrentLocation.AbsoluteIndex;
+ _characterIndex = CurrentLocation.CharacterIndex;
+ _lineIndex = CurrentLocation.LineIndex;
+ }
+
+ private void UpdateLocation()
+ {
+ CurrentLocation = new SourceLocation(_absoluteIndex, _lineIndex, _characterIndex);
+ }
+
+ public static SourceLocation CalculateNewLocation(SourceLocation lastPosition, string newContent)
+ {
+ return new SourceLocationTracker(lastPosition).UpdateLocation(newContent).CurrentLocation;
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Text/TextBufferReader.cs b/src/System.Web.Razor/Text/TextBufferReader.cs
new file mode 100644
index 00000000..78e02bd9
--- /dev/null
+++ b/src/System.Web.Razor/Text/TextBufferReader.cs
@@ -0,0 +1,103 @@
+using System.Collections.Generic;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Utils;
+
+namespace System.Web.Razor.Text
+{
+ public class TextBufferReader : LookaheadTextReader
+ {
+ private Stack<BacktrackContext> _bookmarks = new Stack<BacktrackContext>();
+ private SourceLocationTracker _tracker = new SourceLocationTracker();
+
+ public TextBufferReader(ITextBuffer buffer)
+ {
+ if (buffer == null)
+ {
+ throw new ArgumentNullException("buffer");
+ }
+
+ InnerBuffer = buffer;
+ }
+
+ internal ITextBuffer InnerBuffer { get; private set; }
+
+ public override SourceLocation CurrentLocation
+ {
+ get { return _tracker.CurrentLocation; }
+ }
+
+ public override int Peek()
+ {
+ return InnerBuffer.Peek();
+ }
+
+ public override int Read()
+ {
+ int read = InnerBuffer.Read();
+ if (read != -1)
+ {
+ char nextChar = '\0';
+ int next = Peek();
+ if (next != -1)
+ {
+ nextChar = (char)next;
+ }
+ _tracker.UpdateLocation((char)read, nextChar);
+ }
+ return read;
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ IDisposable disposable = InnerBuffer as IDisposable;
+ if (disposable != null)
+ {
+ disposable.Dispose();
+ }
+ }
+ base.Dispose(disposing);
+ }
+
+ public override IDisposable BeginLookahead()
+ {
+ BacktrackContext context = new BacktrackContext() { Location = CurrentLocation };
+ _bookmarks.Push(context);
+ return new DisposableAction(() =>
+ {
+ EndLookahead(context);
+ });
+ }
+
+ public override void CancelBacktrack()
+ {
+ if (_bookmarks.Count == 0)
+ {
+ throw new InvalidOperationException(RazorResources.CancelBacktrack_Must_Be_Called_Within_Lookahead);
+ }
+ _bookmarks.Pop();
+ }
+
+ private void EndLookahead(BacktrackContext context)
+ {
+ if (_bookmarks.Count > 0 && ReferenceEquals(_bookmarks.Peek(), context))
+ {
+ // Backtrack wasn't cancelled, so pop it
+ _bookmarks.Pop();
+
+ // Set the new current location
+ _tracker.CurrentLocation = context.Location;
+ InnerBuffer.Position = context.Location.AbsoluteIndex;
+ }
+ }
+
+ /// <summary>
+ /// Need a class for reference equality to support cancelling backtrack.
+ /// </summary>
+ private class BacktrackContext
+ {
+ public SourceLocation Location { get; set; }
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Text/TextChange.cs b/src/System.Web.Razor/Text/TextChange.cs
new file mode 100644
index 00000000..7fb28e9c
--- /dev/null
+++ b/src/System.Web.Razor/Text/TextChange.cs
@@ -0,0 +1,208 @@
+using System.Diagnostics;
+using System.Globalization;
+using System.Text;
+using System.Web.Razor.Parser.SyntaxTree;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.Razor.Text
+{
+ public struct TextChange
+ {
+ private string _newText;
+ private string _oldText;
+
+ /// <summary>
+ /// Constructor for changes where the position hasn't moved (primarily for tests)
+ /// </summary>
+ internal TextChange(int position, int oldLength, ITextBuffer oldBuffer, int newLength, ITextBuffer newBuffer)
+ : this(position, oldLength, oldBuffer, position, newLength, newBuffer)
+ {
+ }
+
+ public TextChange(int oldPosition, int oldLength, ITextBuffer oldBuffer, int newPosition, int newLength, ITextBuffer newBuffer)
+ : this()
+ {
+ if (oldPosition < 0)
+ {
+ throw new ArgumentOutOfRangeException("oldPosition", String.Format(CultureInfo.CurrentCulture, CommonResources.Argument_Must_Be_GreaterThanOrEqualTo, "0"));
+ }
+ if (newPosition < 0)
+ {
+ throw new ArgumentOutOfRangeException("newPosition", String.Format(CultureInfo.CurrentCulture, CommonResources.Argument_Must_Be_GreaterThanOrEqualTo, "0"));
+ }
+ if (oldLength < 0)
+ {
+ throw new ArgumentOutOfRangeException("oldLength", String.Format(CultureInfo.CurrentCulture, CommonResources.Argument_Must_Be_GreaterThanOrEqualTo, "0"));
+ }
+ if (newLength < 0)
+ {
+ throw new ArgumentOutOfRangeException("newLength", String.Format(CultureInfo.CurrentCulture, CommonResources.Argument_Must_Be_GreaterThanOrEqualTo, "0"));
+ }
+ if (oldBuffer == null)
+ {
+ throw new ArgumentNullException("oldBuffer");
+ }
+ if (newBuffer == null)
+ {
+ throw new ArgumentNullException("newBuffer");
+ }
+
+ OldPosition = oldPosition;
+ NewPosition = newPosition;
+ OldLength = oldLength;
+ NewLength = newLength;
+ NewBuffer = newBuffer;
+ OldBuffer = oldBuffer;
+ }
+
+ public int OldPosition { get; private set; }
+ public int NewPosition { get; private set; }
+ public int OldLength { get; private set; }
+ public int NewLength { get; private set; }
+ public ITextBuffer NewBuffer { get; private set; }
+ public ITextBuffer OldBuffer { get; private set; }
+
+ public string OldText
+ {
+ get
+ {
+ if (_oldText == null && OldBuffer != null)
+ {
+ _oldText = GetText(OldBuffer, OldPosition, OldLength);
+ }
+ return _oldText;
+ }
+ }
+
+ public string NewText
+ {
+ get
+ {
+ if (_newText == null)
+ {
+ _newText = GetText(NewBuffer, NewPosition, NewLength);
+ }
+ return _newText;
+ }
+ }
+
+ public bool IsInsert
+ {
+ get { return OldLength == 0 && NewLength > 0; }
+ }
+
+ public bool IsDelete
+ {
+ get { return OldLength > 0 && NewLength == 0; }
+ }
+
+ public bool IsReplace
+ {
+ get { return OldLength > 0 && NewLength > 0; }
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (!(obj is TextChange))
+ {
+ return false;
+ }
+ TextChange change = (TextChange)obj;
+ return (change.OldPosition == OldPosition) &&
+ (change.NewPosition == NewPosition) &&
+ (change.OldLength == OldLength) &&
+ (change.NewLength == NewLength) &&
+ OldBuffer.Equals(change.OldBuffer) &&
+ NewBuffer.Equals(change.NewBuffer);
+ }
+
+ public string ApplyChange(string content, int changeOffset)
+ {
+ int changeRelativePosition = OldPosition - changeOffset;
+
+ Debug.Assert(changeRelativePosition >= 0);
+ return content.Remove(changeRelativePosition, OldLength)
+ .Insert(changeRelativePosition, NewText);
+ }
+
+ /// <summary>
+ /// Applies the text change to the content of the span and returns the new content.
+ /// This method doesn't update the span content.
+ /// </summary>
+ public string ApplyChange(Span span)
+ {
+ return ApplyChange(span.Content, span.Start.AbsoluteIndex);
+ }
+
+ public override int GetHashCode()
+ {
+ return OldPosition ^ NewPosition ^ OldLength ^ NewLength ^ NewBuffer.GetHashCode() ^ OldBuffer.GetHashCode();
+ }
+
+ public override string ToString()
+ {
+ return String.Format(CultureInfo.CurrentCulture, "({0}:{1}) \"{3}\" -> ({0}:{2}) \"{4}\"", OldPosition, OldLength, NewLength, OldText, NewText);
+ }
+
+ /// <summary>
+ /// Removes a common prefix from the edit to turn IntelliSense replacements into insertions where possible
+ /// </summary>
+ /// <returns>A normalized text change</returns>
+ public TextChange Normalize()
+ {
+ if (OldBuffer != null && IsReplace && NewLength > OldLength && NewText.StartsWith(OldText, StringComparison.Ordinal) && NewPosition == OldPosition)
+ {
+ // Normalize the change into an insertion of the uncommon suffix (i.e. strip out the common prefix)
+ return new TextChange(oldPosition: OldPosition + OldLength,
+ oldLength: 0,
+ oldBuffer: OldBuffer,
+ newPosition: OldPosition + OldLength,
+ newLength: NewLength - OldLength,
+ newBuffer: NewBuffer);
+ }
+ return this;
+ }
+
+ private string GetText(ITextBuffer buffer, int position, int length)
+ {
+ int oldPosition = buffer.Position;
+ try
+ {
+ buffer.Position = position;
+ // Optimization for the common case of one char inserts
+ if (NewLength == 1)
+ {
+ return ((char)buffer.Read()).ToString();
+ }
+ else
+ {
+ var builder = new StringBuilder();
+ for (int i = 0; i < length; i++)
+ {
+ char c = (char)buffer.Read();
+ builder.Append(c);
+ if (Char.IsHighSurrogate(c))
+ {
+ builder.Append((char)buffer.Read());
+ }
+ }
+ return builder.ToString();
+ }
+ }
+ finally
+ {
+ buffer.Position = oldPosition;
+ }
+ }
+
+ public static bool operator ==(TextChange left, TextChange right)
+ {
+ return left.Equals(right);
+ }
+
+ public static bool operator !=(TextChange left, TextChange right)
+ {
+ return !left.Equals(right);
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Text/TextChangeType.cs b/src/System.Web.Razor/Text/TextChangeType.cs
new file mode 100644
index 00000000..d58772c6
--- /dev/null
+++ b/src/System.Web.Razor/Text/TextChangeType.cs
@@ -0,0 +1,8 @@
+namespace System.Web.Razor.Text
+{
+ public enum TextChangeType
+ {
+ Insert,
+ Remove
+ }
+}
diff --git a/src/System.Web.Razor/Text/TextDocumentReader.cs b/src/System.Web.Razor/Text/TextDocumentReader.cs
new file mode 100644
index 00000000..b90619ba
--- /dev/null
+++ b/src/System.Web.Razor/Text/TextDocumentReader.cs
@@ -0,0 +1,40 @@
+using System.IO;
+
+namespace System.Web.Razor.Text
+{
+ public class TextDocumentReader : TextReader, ITextDocument
+ {
+ public TextDocumentReader(ITextDocument source)
+ {
+ Document = source;
+ }
+
+ internal ITextDocument Document { get; private set; }
+
+ public SourceLocation Location
+ {
+ get { return Document.Location; }
+ }
+
+ public int Length
+ {
+ get { return Document.Length; }
+ }
+
+ public int Position
+ {
+ get { return Document.Position; }
+ set { Document.Position = value; }
+ }
+
+ public override int Read()
+ {
+ return Document.Read();
+ }
+
+ public override int Peek()
+ {
+ return Document.Peek();
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Text/TextExtensions.cs b/src/System.Web.Razor/Text/TextExtensions.cs
new file mode 100644
index 00000000..2b040d18
--- /dev/null
+++ b/src/System.Web.Razor/Text/TextExtensions.cs
@@ -0,0 +1,44 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Text;
+
+namespace System.Web.Razor.Text
+{
+ internal static class TextExtensions
+ {
+ public static void Seek(this ITextBuffer self, int characters)
+ {
+ self.Position += characters;
+ }
+
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "The consumer is expected to dispose this object")]
+ public static ITextDocument ToDocument(this ITextBuffer self)
+ {
+ ITextDocument ret = self as ITextDocument;
+ if (ret == null)
+ {
+ ret = new SeekableTextReader(self);
+ }
+ return ret;
+ }
+
+ public static LookaheadToken BeginLookahead(this ITextBuffer self)
+ {
+ int start = self.Position;
+ return new LookaheadToken(() =>
+ {
+ self.Position = start;
+ });
+ }
+
+ public static string ReadToEnd(this ITextBuffer self)
+ {
+ StringBuilder builder = new StringBuilder();
+ int read;
+ while ((read = self.Read()) != -1)
+ {
+ builder.Append((char)read);
+ }
+ return builder.ToString();
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Tokenizer/CSharpHelpers.cs b/src/System.Web.Razor/Tokenizer/CSharpHelpers.cs
new file mode 100644
index 00000000..8945e241
--- /dev/null
+++ b/src/System.Web.Razor/Tokenizer/CSharpHelpers.cs
@@ -0,0 +1,41 @@
+using System.Globalization;
+
+namespace System.Web.Razor.Tokenizer
+{
+ public static class CSharpHelpers
+ {
+ // CSharp Spec §2.4.2
+ public static bool IsIdentifierStart(char character)
+ {
+ return Char.IsLetter(character) ||
+ character == '_' ||
+ Char.GetUnicodeCategory(character) == UnicodeCategory.LetterNumber; // Ln
+ }
+
+ public static bool IsIdentifierPart(char character)
+ {
+ return Char.IsDigit(character) ||
+ IsIdentifierStart(character) ||
+ IsIdentifierPartByUnicodeCategory(character);
+ }
+
+ public static bool IsRealLiteralSuffix(char character)
+ {
+ return character == 'F' ||
+ character == 'f' ||
+ character == 'D' ||
+ character == 'd' ||
+ character == 'M' ||
+ character == 'm';
+ }
+
+ private static bool IsIdentifierPartByUnicodeCategory(char character)
+ {
+ UnicodeCategory category = Char.GetUnicodeCategory(character);
+ return category == UnicodeCategory.NonSpacingMark || // Mn
+ category == UnicodeCategory.SpacingCombiningMark || // Mc
+ category == UnicodeCategory.ConnectorPunctuation || // Pc
+ category == UnicodeCategory.Format; // Cf
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Tokenizer/CSharpKeywordDetector.cs b/src/System.Web.Razor/Tokenizer/CSharpKeywordDetector.cs
new file mode 100644
index 00000000..32fe988b
--- /dev/null
+++ b/src/System.Web.Razor/Tokenizer/CSharpKeywordDetector.cs
@@ -0,0 +1,99 @@
+using System.Collections.Generic;
+using System.Web.Razor.Tokenizer.Symbols;
+
+namespace System.Web.Razor.Tokenizer
+{
+ internal static class CSharpKeywordDetector
+ {
+ private static readonly Dictionary<string, CSharpKeyword> _keywords = new Dictionary<string, CSharpKeyword>(StringComparer.Ordinal)
+ {
+ { "abstract", CSharpKeyword.Abstract },
+ { "byte", CSharpKeyword.Byte },
+ { "class", CSharpKeyword.Class },
+ { "delegate", CSharpKeyword.Delegate },
+ { "event", CSharpKeyword.Event },
+ { "fixed", CSharpKeyword.Fixed },
+ { "if", CSharpKeyword.If },
+ { "internal", CSharpKeyword.Internal },
+ { "new", CSharpKeyword.New },
+ { "override", CSharpKeyword.Override },
+ { "readonly", CSharpKeyword.Readonly },
+ { "short", CSharpKeyword.Short },
+ { "struct", CSharpKeyword.Struct },
+ { "try", CSharpKeyword.Try },
+ { "unsafe", CSharpKeyword.Unsafe },
+ { "volatile", CSharpKeyword.Volatile },
+ { "as", CSharpKeyword.As },
+ { "do", CSharpKeyword.Do },
+ { "is", CSharpKeyword.Is },
+ { "params", CSharpKeyword.Params },
+ { "ref", CSharpKeyword.Ref },
+ { "switch", CSharpKeyword.Switch },
+ { "ushort", CSharpKeyword.Ushort },
+ { "while", CSharpKeyword.While },
+ { "case", CSharpKeyword.Case },
+ { "const", CSharpKeyword.Const },
+ { "explicit", CSharpKeyword.Explicit },
+ { "float", CSharpKeyword.Float },
+ { "null", CSharpKeyword.Null },
+ { "sizeof", CSharpKeyword.Sizeof },
+ { "typeof", CSharpKeyword.Typeof },
+ { "implicit", CSharpKeyword.Implicit },
+ { "private", CSharpKeyword.Private },
+ { "this", CSharpKeyword.This },
+ { "using", CSharpKeyword.Using },
+ { "extern", CSharpKeyword.Extern },
+ { "return", CSharpKeyword.Return },
+ { "stackalloc", CSharpKeyword.Stackalloc },
+ { "uint", CSharpKeyword.Uint },
+ { "base", CSharpKeyword.Base },
+ { "catch", CSharpKeyword.Catch },
+ { "continue", CSharpKeyword.Continue },
+ { "double", CSharpKeyword.Double },
+ { "for", CSharpKeyword.For },
+ { "in", CSharpKeyword.In },
+ { "lock", CSharpKeyword.Lock },
+ { "object", CSharpKeyword.Object },
+ { "protected", CSharpKeyword.Protected },
+ { "static", CSharpKeyword.Static },
+ { "false", CSharpKeyword.False },
+ { "public", CSharpKeyword.Public },
+ { "sbyte", CSharpKeyword.Sbyte },
+ { "throw", CSharpKeyword.Throw },
+ { "virtual", CSharpKeyword.Virtual },
+ { "decimal", CSharpKeyword.Decimal },
+ { "else", CSharpKeyword.Else },
+ { "operator", CSharpKeyword.Operator },
+ { "string", CSharpKeyword.String },
+ { "ulong", CSharpKeyword.Ulong },
+ { "bool", CSharpKeyword.Bool },
+ { "char", CSharpKeyword.Char },
+ { "default", CSharpKeyword.Default },
+ { "foreach", CSharpKeyword.Foreach },
+ { "long", CSharpKeyword.Long },
+ { "void", CSharpKeyword.Void },
+ { "enum", CSharpKeyword.Enum },
+ { "finally", CSharpKeyword.Finally },
+ { "int", CSharpKeyword.Int },
+ { "out", CSharpKeyword.Out },
+ { "sealed", CSharpKeyword.Sealed },
+ { "true", CSharpKeyword.True },
+ { "goto", CSharpKeyword.Goto },
+ { "unchecked", CSharpKeyword.Unchecked },
+ { "interface", CSharpKeyword.Interface },
+ { "break", CSharpKeyword.Break },
+ { "checked", CSharpKeyword.Checked },
+ { "namespace", CSharpKeyword.Namespace }
+ };
+
+ public static CSharpKeyword? SymbolTypeForIdentifier(string id)
+ {
+ CSharpKeyword type;
+ if (!_keywords.TryGetValue(id, out type))
+ {
+ return null;
+ }
+ return type;
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Tokenizer/CSharpTokenizer.cs b/src/System.Web.Razor/Tokenizer/CSharpTokenizer.cs
new file mode 100644
index 00000000..137fd079
--- /dev/null
+++ b/src/System.Web.Razor/Tokenizer/CSharpTokenizer.cs
@@ -0,0 +1,425 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Text;
+using System.Web.Razor.Tokenizer.Symbols;
+
+namespace System.Web.Razor.Tokenizer
+{
+ public class CSharpTokenizer : Tokenizer<CSharpSymbol, CSharpSymbolType>
+ {
+ private Dictionary<char, Func<CSharpSymbolType>> _operatorHandlers;
+
+ public CSharpTokenizer(ITextDocument source)
+ : base(source)
+ {
+ CurrentState = Data;
+
+ _operatorHandlers = new Dictionary<char, Func<CSharpSymbolType>>()
+ {
+ { '-', MinusOperator },
+ { '<', LessThanOperator },
+ { '>', GreaterThanOperator },
+ { '&', CreateTwoCharOperatorHandler(CSharpSymbolType.And, '=', CSharpSymbolType.AndAssign, '&', CSharpSymbolType.DoubleAnd) },
+ { '|', CreateTwoCharOperatorHandler(CSharpSymbolType.Or, '=', CSharpSymbolType.OrAssign, '|', CSharpSymbolType.DoubleOr) },
+ { '+', CreateTwoCharOperatorHandler(CSharpSymbolType.Plus, '=', CSharpSymbolType.PlusAssign, '+', CSharpSymbolType.Increment) },
+ { '=', CreateTwoCharOperatorHandler(CSharpSymbolType.Assign, '=', CSharpSymbolType.Equals, '>', CSharpSymbolType.GreaterThanEqual) },
+ { '!', CreateTwoCharOperatorHandler(CSharpSymbolType.Not, '=', CSharpSymbolType.NotEqual) },
+ { '%', CreateTwoCharOperatorHandler(CSharpSymbolType.Modulo, '=', CSharpSymbolType.ModuloAssign) },
+ { '*', CreateTwoCharOperatorHandler(CSharpSymbolType.Star, '=', CSharpSymbolType.MultiplyAssign) },
+ { ':', CreateTwoCharOperatorHandler(CSharpSymbolType.Colon, ':', CSharpSymbolType.DoubleColon) },
+ { '?', CreateTwoCharOperatorHandler(CSharpSymbolType.QuestionMark, '?', CSharpSymbolType.NullCoalesce) },
+ { '^', CreateTwoCharOperatorHandler(CSharpSymbolType.Xor, '=', CSharpSymbolType.XorAssign) },
+ { '(', () => CSharpSymbolType.LeftParenthesis },
+ { ')', () => CSharpSymbolType.RightParenthesis },
+ { '{', () => CSharpSymbolType.LeftBrace },
+ { '}', () => CSharpSymbolType.RightBrace },
+ { '[', () => CSharpSymbolType.LeftBracket },
+ { ']', () => CSharpSymbolType.RightBracket },
+ { ',', () => CSharpSymbolType.Comma },
+ { ';', () => CSharpSymbolType.Semicolon },
+ { '~', () => CSharpSymbolType.Tilde },
+ { '#', () => CSharpSymbolType.Hash }
+ };
+ }
+
+ protected override State StartState
+ {
+ get { return Data; }
+ }
+
+ public override CSharpSymbolType RazorCommentType
+ {
+ get { return CSharpSymbolType.RazorComment; }
+ }
+
+ public override CSharpSymbolType RazorCommentTransitionType
+ {
+ get { return CSharpSymbolType.RazorCommentTransition; }
+ }
+
+ public override CSharpSymbolType RazorCommentStarType
+ {
+ get { return CSharpSymbolType.RazorCommentStar; }
+ }
+
+ protected override CSharpSymbol CreateSymbol(SourceLocation start, string content, CSharpSymbolType type, IEnumerable<RazorError> errors)
+ {
+ return new CSharpSymbol(start, content, type, errors);
+ }
+
+ private StateResult Data()
+ {
+ if (ParserHelpers.IsNewLine(CurrentCharacter))
+ {
+ // CSharp Spec §2.3.1
+ bool checkTwoCharNewline = CurrentCharacter == '\r';
+ TakeCurrent();
+ if (checkTwoCharNewline && CurrentCharacter == '\n')
+ {
+ TakeCurrent();
+ }
+ return Stay(EndSymbol(CSharpSymbolType.NewLine));
+ }
+ else if (ParserHelpers.IsWhitespace(CurrentCharacter))
+ {
+ // CSharp Spec §2.3.3
+ TakeUntil(c => !ParserHelpers.IsWhitespace(c));
+ return Stay(EndSymbol(CSharpSymbolType.WhiteSpace));
+ }
+ else if (CSharpHelpers.IsIdentifierStart(CurrentCharacter))
+ {
+ return Identifier();
+ }
+ else if (Char.IsDigit(CurrentCharacter))
+ {
+ return NumericLiteral();
+ }
+ switch (CurrentCharacter)
+ {
+ case '@':
+ return AtSymbol();
+ case '\'':
+ TakeCurrent();
+ return Transition(() => QuotedLiteral('\'', CSharpSymbolType.CharacterLiteral));
+ case '"':
+ TakeCurrent();
+ return Transition(() => QuotedLiteral('"', CSharpSymbolType.StringLiteral));
+ case '.':
+ if (Char.IsDigit(Peek()))
+ {
+ return RealLiteral();
+ }
+ return Stay(Single(CSharpSymbolType.Dot));
+ case '/':
+ TakeCurrent();
+ if (CurrentCharacter == '/')
+ {
+ TakeCurrent();
+ return SingleLineComment();
+ }
+ else if (CurrentCharacter == '*')
+ {
+ TakeCurrent();
+ return Transition(BlockComment);
+ }
+ else if (CurrentCharacter == '=')
+ {
+ TakeCurrent();
+ return Stay(EndSymbol(CSharpSymbolType.DivideAssign));
+ }
+ else
+ {
+ return Stay(EndSymbol(CSharpSymbolType.Slash));
+ }
+ default:
+ return Stay(EndSymbol(Operator()));
+ }
+ }
+
+ private StateResult AtSymbol()
+ {
+ TakeCurrent();
+ if (CurrentCharacter == '"')
+ {
+ TakeCurrent();
+ return Transition(VerbatimStringLiteral);
+ }
+ else if (CurrentCharacter == '*')
+ {
+ return Transition(EndSymbol(CSharpSymbolType.RazorCommentTransition), AfterRazorCommentTransition);
+ }
+ else if (CurrentCharacter == '@')
+ {
+ // Could be escaped comment transition
+ return Transition(EndSymbol(CSharpSymbolType.Transition), () =>
+ {
+ TakeCurrent();
+ return Transition(EndSymbol(CSharpSymbolType.Transition), Data);
+ });
+ }
+ return Stay(EndSymbol(CSharpSymbolType.Transition));
+ }
+
+ private CSharpSymbolType Operator()
+ {
+ char first = CurrentCharacter;
+ TakeCurrent();
+ Func<CSharpSymbolType> handler;
+ if (_operatorHandlers.TryGetValue(first, out handler))
+ {
+ return handler();
+ }
+ return CSharpSymbolType.Unknown;
+ }
+
+ private CSharpSymbolType LessThanOperator()
+ {
+ if (CurrentCharacter == '=')
+ {
+ TakeCurrent();
+ return CSharpSymbolType.LessThanEqual;
+ }
+ return CSharpSymbolType.LessThan;
+ }
+
+ private CSharpSymbolType GreaterThanOperator()
+ {
+ if (CurrentCharacter == '=')
+ {
+ TakeCurrent();
+ return CSharpSymbolType.GreaterThanEqual;
+ }
+ return CSharpSymbolType.GreaterThan;
+ }
+
+ private CSharpSymbolType MinusOperator()
+ {
+ if (CurrentCharacter == '>')
+ {
+ TakeCurrent();
+ return CSharpSymbolType.Arrow;
+ }
+ else if (CurrentCharacter == '-')
+ {
+ TakeCurrent();
+ return CSharpSymbolType.Decrement;
+ }
+ else if (CurrentCharacter == '=')
+ {
+ TakeCurrent();
+ return CSharpSymbolType.MinusAssign;
+ }
+ return CSharpSymbolType.Minus;
+ }
+
+ private Func<CSharpSymbolType> CreateTwoCharOperatorHandler(CSharpSymbolType typeIfOnlyFirst, char second, CSharpSymbolType typeIfBoth)
+ {
+ return () =>
+ {
+ if (CurrentCharacter == second)
+ {
+ TakeCurrent();
+ return typeIfBoth;
+ }
+ return typeIfOnlyFirst;
+ };
+ }
+
+ private Func<CSharpSymbolType> CreateTwoCharOperatorHandler(CSharpSymbolType typeIfOnlyFirst, char option1, CSharpSymbolType typeIfOption1, char option2, CSharpSymbolType typeIfOption2)
+ {
+ return () =>
+ {
+ if (CurrentCharacter == option1)
+ {
+ TakeCurrent();
+ return typeIfOption1;
+ }
+ else if (CurrentCharacter == option2)
+ {
+ TakeCurrent();
+ return typeIfOption2;
+ }
+ return typeIfOnlyFirst;
+ };
+ }
+
+ private StateResult VerbatimStringLiteral()
+ {
+ TakeUntil(c => c == '"');
+ if (CurrentCharacter == '"')
+ {
+ TakeCurrent();
+ if (CurrentCharacter == '"')
+ {
+ TakeCurrent();
+ // Stay in the literal, this is an escaped "
+ return Stay();
+ }
+ }
+ else if (EndOfFile)
+ {
+ CurrentErrors.Add(new RazorError(RazorResources.ParseError_Unterminated_String_Literal, CurrentStart));
+ }
+ return Transition(EndSymbol(CSharpSymbolType.StringLiteral), Data);
+ }
+
+ private StateResult QuotedLiteral(char quote, CSharpSymbolType literalType)
+ {
+ TakeUntil(c => c == '\\' || c == quote || ParserHelpers.IsNewLine(c));
+ if (CurrentCharacter == '\\')
+ {
+ TakeCurrent(); // Take the '\'
+ TakeCurrent(); // Take the next char as well (multi-char escapes don't matter)
+ return Stay();
+ }
+ else if (EndOfFile || ParserHelpers.IsNewLine(CurrentCharacter))
+ {
+ CurrentErrors.Add(new RazorError(RazorResources.ParseError_Unterminated_String_Literal, CurrentStart));
+ }
+ else
+ {
+ TakeCurrent(); // No-op if at EOF
+ }
+ return Transition(EndSymbol(literalType), Data);
+ }
+
+ // CSharp Spec §2.3.2
+ private StateResult BlockComment()
+ {
+ TakeUntil(c => c == '*');
+ if (EndOfFile)
+ {
+ CurrentErrors.Add(new RazorError(RazorResources.ParseError_BlockComment_Not_Terminated, CurrentStart));
+ return Transition(EndSymbol(CSharpSymbolType.Comment), Data);
+ }
+ if (CurrentCharacter == '*')
+ {
+ TakeCurrent();
+ if (CurrentCharacter == '/')
+ {
+ TakeCurrent();
+ return Transition(EndSymbol(CSharpSymbolType.Comment), Data);
+ }
+ }
+ return Stay();
+ }
+
+ // CSharp Spec §2.3.2
+ private StateResult SingleLineComment()
+ {
+ TakeUntil(c => ParserHelpers.IsNewLine(c));
+ return Stay(EndSymbol(CSharpSymbolType.Comment));
+ }
+
+ // CSharp Spec §2.4.4
+ private StateResult NumericLiteral()
+ {
+ if (TakeAll("0x", caseSensitive: true))
+ {
+ return HexLiteral();
+ }
+ else
+ {
+ return DecimalLiteral();
+ }
+ }
+
+ private StateResult HexLiteral()
+ {
+ TakeUntil(c => !ParserHelpers.IsHexDigit(c));
+ TakeIntegerSuffix();
+ return Stay(EndSymbol(CSharpSymbolType.IntegerLiteral));
+ }
+
+ private StateResult DecimalLiteral()
+ {
+ TakeUntil(c => !Char.IsDigit(c));
+ if (CurrentCharacter == '.' && Char.IsDigit(Peek()))
+ {
+ return RealLiteral();
+ }
+ else if (CSharpHelpers.IsRealLiteralSuffix(CurrentCharacter) ||
+ CurrentCharacter == 'E' || CurrentCharacter == 'e')
+ {
+ return RealLiteralExponentPart();
+ }
+ else
+ {
+ TakeIntegerSuffix();
+ return Stay(EndSymbol(CSharpSymbolType.IntegerLiteral));
+ }
+ }
+
+ private StateResult RealLiteralExponentPart()
+ {
+ if (CurrentCharacter == 'E' || CurrentCharacter == 'e')
+ {
+ TakeCurrent();
+ if (CurrentCharacter == '+' || CurrentCharacter == '-')
+ {
+ TakeCurrent();
+ }
+ TakeUntil(c => !Char.IsDigit(c));
+ }
+ if (CSharpHelpers.IsRealLiteralSuffix(CurrentCharacter))
+ {
+ TakeCurrent();
+ }
+ return Stay(EndSymbol(CSharpSymbolType.RealLiteral));
+ }
+
+ // CSharp Spec §2.4.4.3
+ private StateResult RealLiteral()
+ {
+ AssertCurrent('.');
+ TakeCurrent();
+ Debug.Assert(Char.IsDigit(CurrentCharacter));
+ TakeUntil(c => !Char.IsDigit(c));
+ return RealLiteralExponentPart();
+ }
+
+ private void TakeIntegerSuffix()
+ {
+ if (Char.ToLowerInvariant(CurrentCharacter) == 'u')
+ {
+ TakeCurrent();
+ if (Char.ToLowerInvariant(CurrentCharacter) == 'l')
+ {
+ TakeCurrent();
+ }
+ }
+ else if (Char.ToLowerInvariant(CurrentCharacter) == 'l')
+ {
+ TakeCurrent();
+ if (Char.ToLowerInvariant(CurrentCharacter) == 'u')
+ {
+ TakeCurrent();
+ }
+ }
+ }
+
+ // CSharp Spec §2.4.2
+ private StateResult Identifier()
+ {
+ Debug.Assert(CSharpHelpers.IsIdentifierStart(CurrentCharacter));
+ TakeCurrent();
+ TakeUntil(c => !CSharpHelpers.IsIdentifierPart(c));
+ CSharpSymbol sym = null;
+ if (HaveContent)
+ {
+ CSharpKeyword? kwd = CSharpKeywordDetector.SymbolTypeForIdentifier(Buffer.ToString());
+ CSharpSymbolType type = CSharpSymbolType.Identifier;
+ if (kwd != null)
+ {
+ type = CSharpSymbolType.Keyword;
+ }
+ sym = new CSharpSymbol(CurrentStart, Buffer.ToString(), type) { Keyword = kwd };
+ }
+ StartSymbol();
+ return Stay(sym);
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Tokenizer/HtmlTokenizer.cs b/src/System.Web.Razor/Tokenizer/HtmlTokenizer.cs
new file mode 100644
index 00000000..3dc0d34c
--- /dev/null
+++ b/src/System.Web.Razor/Tokenizer/HtmlTokenizer.cs
@@ -0,0 +1,197 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Text;
+using System.Web.Razor.Tokenizer.Symbols;
+
+namespace System.Web.Razor.Tokenizer
+{
+ // Tokenizer _loosely_ based on http://dev.w3.org/html5/spec/Overview.html#tokenization
+ public class HtmlTokenizer : Tokenizer<HtmlSymbol, HtmlSymbolType>
+ {
+ private const char TransitionChar = '@';
+
+ public HtmlTokenizer(ITextDocument source)
+ : base(source)
+ {
+ CurrentState = Data;
+ }
+
+ protected override State StartState
+ {
+ get { return Data; }
+ }
+
+ public override HtmlSymbolType RazorCommentType
+ {
+ get { return HtmlSymbolType.RazorComment; }
+ }
+
+ public override HtmlSymbolType RazorCommentTransitionType
+ {
+ get { return HtmlSymbolType.RazorCommentTransition; }
+ }
+
+ public override HtmlSymbolType RazorCommentStarType
+ {
+ get { return HtmlSymbolType.RazorCommentStar; }
+ }
+
+ internal static IEnumerable<HtmlSymbol> Tokenize(string content)
+ {
+ using (SeekableTextReader reader = new SeekableTextReader(content))
+ {
+ HtmlTokenizer tok = new HtmlTokenizer(reader);
+ HtmlSymbol sym;
+ while ((sym = tok.NextSymbol()) != null)
+ {
+ yield return sym;
+ }
+ }
+ }
+
+ protected override HtmlSymbol CreateSymbol(SourceLocation start, string content, HtmlSymbolType type, IEnumerable<RazorError> errors)
+ {
+ return new HtmlSymbol(start, content, type, errors);
+ }
+
+ // http://dev.w3.org/html5/spec/Overview.html#data-state
+ private StateResult Data()
+ {
+ if (ParserHelpers.IsWhitespace(CurrentCharacter))
+ {
+ return Stay(Whitespace());
+ }
+ else if (ParserHelpers.IsNewLine(CurrentCharacter))
+ {
+ return Stay(Newline());
+ }
+ else if (CurrentCharacter == '@')
+ {
+ TakeCurrent();
+ if (CurrentCharacter == '*')
+ {
+ return Transition(EndSymbol(HtmlSymbolType.RazorCommentTransition), AfterRazorCommentTransition);
+ }
+ else if (CurrentCharacter == '@')
+ {
+ // Could be escaped comment transition
+ return Transition(EndSymbol(HtmlSymbolType.Transition), () =>
+ {
+ TakeCurrent();
+ return Transition(EndSymbol(HtmlSymbolType.Transition), Data);
+ });
+ }
+ return Stay(EndSymbol(HtmlSymbolType.Transition));
+ }
+ else if (AtSymbol())
+ {
+ return Stay(Symbol());
+ }
+ else
+ {
+ return Transition(Text);
+ }
+ }
+
+ private StateResult Text()
+ {
+ char prev = '\0';
+ while (!EndOfFile && !ParserHelpers.IsWhitespaceOrNewLine(CurrentCharacter) && !AtSymbol())
+ {
+ prev = CurrentCharacter;
+ TakeCurrent();
+ }
+
+ if (CurrentCharacter == '@')
+ {
+ char next = Peek();
+ if (ParserHelpers.IsLetterOrDecimalDigit(prev) && ParserHelpers.IsLetterOrDecimalDigit(next))
+ {
+ TakeCurrent(); // Take the "@"
+ return Stay(); // Stay in the Text state
+ }
+ }
+
+ // Output the Text token and return to the Data state to tokenize the next character (if there is one)
+ return Transition(EndSymbol(HtmlSymbolType.Text), Data);
+ }
+
+ private HtmlSymbol Symbol()
+ {
+ Debug.Assert(AtSymbol());
+ char sym = CurrentCharacter;
+ TakeCurrent();
+ switch (sym)
+ {
+ case '<':
+ return EndSymbol(HtmlSymbolType.OpenAngle);
+ case '!':
+ return EndSymbol(HtmlSymbolType.Bang);
+ case '/':
+ return EndSymbol(HtmlSymbolType.Solidus);
+ case '?':
+ return EndSymbol(HtmlSymbolType.QuestionMark);
+ case '[':
+ return EndSymbol(HtmlSymbolType.LeftBracket);
+ case '>':
+ return EndSymbol(HtmlSymbolType.CloseAngle);
+ case ']':
+ return EndSymbol(HtmlSymbolType.RightBracket);
+ case '=':
+ return EndSymbol(HtmlSymbolType.Equals);
+ case '"':
+ return EndSymbol(HtmlSymbolType.DoubleQuote);
+ case '\'':
+ return EndSymbol(HtmlSymbolType.SingleQuote);
+ case '-':
+ Debug.Assert(CurrentCharacter == '-');
+ TakeCurrent();
+ return EndSymbol(HtmlSymbolType.DoubleHyphen);
+ default:
+ Debug.Fail("Unexpected symbol!");
+ return EndSymbol(HtmlSymbolType.Unknown);
+ }
+ }
+
+ private HtmlSymbol Whitespace()
+ {
+ while (ParserHelpers.IsWhitespace(CurrentCharacter))
+ {
+ TakeCurrent();
+ }
+ return EndSymbol(HtmlSymbolType.WhiteSpace);
+ }
+
+ private HtmlSymbol Newline()
+ {
+ Debug.Assert(ParserHelpers.IsNewLine(CurrentCharacter));
+ // CSharp Spec §2.3.1
+ bool checkTwoCharNewline = CurrentCharacter == '\r';
+ TakeCurrent();
+ if (checkTwoCharNewline && CurrentCharacter == '\n')
+ {
+ TakeCurrent();
+ }
+ return EndSymbol(HtmlSymbolType.NewLine);
+ }
+
+ private bool AtSymbol()
+ {
+ return CurrentCharacter == '<' ||
+ CurrentCharacter == '<' ||
+ CurrentCharacter == '!' ||
+ CurrentCharacter == '/' ||
+ CurrentCharacter == '?' ||
+ CurrentCharacter == '[' ||
+ CurrentCharacter == '>' ||
+ CurrentCharacter == ']' ||
+ CurrentCharacter == '=' ||
+ CurrentCharacter == '"' ||
+ CurrentCharacter == '\'' ||
+ CurrentCharacter == '@' ||
+ (CurrentCharacter == '-' && Peek() == '-');
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Tokenizer/ITokenizer.cs b/src/System.Web.Razor/Tokenizer/ITokenizer.cs
new file mode 100644
index 00000000..4ed80f1e
--- /dev/null
+++ b/src/System.Web.Razor/Tokenizer/ITokenizer.cs
@@ -0,0 +1,9 @@
+using System.Web.Razor.Tokenizer.Symbols;
+
+namespace System.Web.Razor.Tokenizer
+{
+ public interface ITokenizer
+ {
+ ISymbol NextSymbol();
+ }
+}
diff --git a/src/System.Web.Razor/Tokenizer/Symbols/CSharpKeyword.cs b/src/System.Web.Razor/Tokenizer/Symbols/CSharpKeyword.cs
new file mode 100644
index 00000000..eb7b4032
--- /dev/null
+++ b/src/System.Web.Razor/Tokenizer/Symbols/CSharpKeyword.cs
@@ -0,0 +1,83 @@
+namespace System.Web.Razor.Tokenizer.Symbols
+{
+ public enum CSharpKeyword
+ {
+ Abstract,
+ Byte,
+ Class,
+ Delegate,
+ Event,
+ Fixed,
+ If,
+ Internal,
+ New,
+ Override,
+ Readonly,
+ Short,
+ Struct,
+ Try,
+ Unsafe,
+ Volatile,
+ As,
+ Do,
+ Is,
+ Params,
+ Ref,
+ Switch,
+ Ushort,
+ While,
+ Case,
+ Const,
+ Explicit,
+ Float,
+ Null,
+ Sizeof,
+ Typeof,
+ Implicit,
+ Private,
+ This,
+ Using,
+ Extern,
+ Return,
+ Stackalloc,
+ Uint,
+ Base,
+ Catch,
+ Continue,
+ Double,
+ For,
+ In,
+ Lock,
+ Object,
+ Protected,
+ Static,
+ False,
+ Public,
+ Sbyte,
+ Throw,
+ Virtual,
+ Decimal,
+ Else,
+ Operator,
+ String,
+ Ulong,
+ Bool,
+ Char,
+ Default,
+ Foreach,
+ Long,
+ Void,
+ Enum,
+ Finally,
+ Int,
+ Out,
+ Sealed,
+ True,
+ Goto,
+ Unchecked,
+ Interface,
+ Break,
+ Checked,
+ Namespace
+ }
+}
diff --git a/src/System.Web.Razor/Tokenizer/Symbols/CSharpSymbol.cs b/src/System.Web.Razor/Tokenizer/Symbols/CSharpSymbol.cs
new file mode 100644
index 00000000..eba1c1bb
--- /dev/null
+++ b/src/System.Web.Razor/Tokenizer/Symbols/CSharpSymbol.cs
@@ -0,0 +1,45 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Text;
+
+namespace System.Web.Razor.Tokenizer.Symbols
+{
+ public class CSharpSymbol : SymbolBase<CSharpSymbolType>
+ {
+ // Helper constructor
+ public CSharpSymbol(int offset, int line, int column, string content, CSharpSymbolType type)
+ : this(new SourceLocation(offset, line, column), content, type, Enumerable.Empty<RazorError>())
+ {
+ }
+
+ public CSharpSymbol(SourceLocation start, string content, CSharpSymbolType type)
+ : this(start, content, type, Enumerable.Empty<RazorError>())
+ {
+ }
+
+ public CSharpSymbol(int offset, int line, int column, string content, CSharpSymbolType type, IEnumerable<RazorError> errors)
+ : base(new SourceLocation(offset, line, column), content, type, errors)
+ {
+ }
+
+ public CSharpSymbol(SourceLocation start, string content, CSharpSymbolType type, IEnumerable<RazorError> errors)
+ : base(start, content, type, errors)
+ {
+ }
+
+ public bool? EscapedIdentifier { get; set; }
+ public CSharpKeyword? Keyword { get; set; }
+
+ public override bool Equals(object obj)
+ {
+ CSharpSymbol other = obj as CSharpSymbol;
+ return base.Equals(obj) && other.Keyword == Keyword;
+ }
+
+ public override int GetHashCode()
+ {
+ return base.GetHashCode() ^ Keyword.GetHashCode();
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Tokenizer/Symbols/CSharpSymbolType.cs b/src/System.Web.Razor/Tokenizer/Symbols/CSharpSymbolType.cs
new file mode 100644
index 00000000..bf9c0286
--- /dev/null
+++ b/src/System.Web.Razor/Tokenizer/Symbols/CSharpSymbolType.cs
@@ -0,0 +1,72 @@
+namespace System.Web.Razor.Tokenizer.Symbols
+{
+ public enum CSharpSymbolType
+ {
+ Unknown,
+ Identifier,
+ Keyword,
+ IntegerLiteral,
+ NewLine,
+ WhiteSpace,
+ Comment,
+ RealLiteral,
+ CharacterLiteral,
+ StringLiteral,
+
+ // Operators
+ Arrow,
+ Minus,
+ Decrement,
+ MinusAssign,
+ NotEqual,
+ Not,
+ Modulo,
+ ModuloAssign,
+ AndAssign,
+ And,
+ DoubleAnd,
+ LeftParenthesis,
+ RightParenthesis,
+ Star,
+ MultiplyAssign,
+ Comma,
+ Dot,
+ Slash,
+ DivideAssign,
+ DoubleColon,
+ Colon,
+ Semicolon,
+ QuestionMark,
+ NullCoalesce,
+ RightBracket,
+ LeftBracket,
+ XorAssign,
+ Xor,
+ LeftBrace,
+ OrAssign,
+ DoubleOr,
+ Or,
+ RightBrace,
+ Tilde,
+ Plus,
+ PlusAssign,
+ Increment,
+ LessThan,
+ LessThanEqual,
+ LeftShift,
+ LeftShiftAssign,
+ Assign,
+ Equals,
+ GreaterThan,
+ GreaterThanEqual,
+ RightShift,
+ RightShiftAssign,
+ Hash,
+ Transition,
+
+ // Razor specific
+ RazorCommentTransition,
+ RazorCommentStar,
+ RazorComment
+ }
+}
diff --git a/src/System.Web.Razor/Tokenizer/Symbols/HtmlSymbol.cs b/src/System.Web.Razor/Tokenizer/Symbols/HtmlSymbol.cs
new file mode 100644
index 00000000..eeacee80
--- /dev/null
+++ b/src/System.Web.Razor/Tokenizer/Symbols/HtmlSymbol.cs
@@ -0,0 +1,31 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Text;
+
+namespace System.Web.Razor.Tokenizer.Symbols
+{
+ public class HtmlSymbol : SymbolBase<HtmlSymbolType>
+ {
+ // Helper constructor
+ public HtmlSymbol(int offset, int line, int column, string content, HtmlSymbolType type)
+ : this(new SourceLocation(offset, line, column), content, type, Enumerable.Empty<RazorError>())
+ {
+ }
+
+ public HtmlSymbol(SourceLocation start, string content, HtmlSymbolType type)
+ : base(start, content, type, Enumerable.Empty<RazorError>())
+ {
+ }
+
+ public HtmlSymbol(int offset, int line, int column, string content, HtmlSymbolType type, IEnumerable<RazorError> errors)
+ : base(new SourceLocation(offset, line, column), content, type, errors)
+ {
+ }
+
+ public HtmlSymbol(SourceLocation start, string content, HtmlSymbolType type, IEnumerable<RazorError> errors)
+ : base(start, content, type, errors)
+ {
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Tokenizer/Symbols/HtmlSymbolType.cs b/src/System.Web.Razor/Tokenizer/Symbols/HtmlSymbolType.cs
new file mode 100644
index 00000000..7c8a9cbd
--- /dev/null
+++ b/src/System.Web.Razor/Tokenizer/Symbols/HtmlSymbolType.cs
@@ -0,0 +1,26 @@
+namespace System.Web.Razor.Tokenizer.Symbols
+{
+ public enum HtmlSymbolType
+ {
+ Unknown,
+ Text, // Text which isn't one of the below
+ WhiteSpace, // Non-newline Whitespace
+ NewLine, // Newline
+ OpenAngle, // <
+ Bang, // !
+ Solidus, // /
+ QuestionMark, // ?
+ DoubleHyphen, // --
+ LeftBracket, // [
+ CloseAngle, // >
+ RightBracket, // ]
+ Equals, // =
+ DoubleQuote, // "
+ SingleQuote, // '
+ Transition, // @
+ Colon,
+ RazorComment,
+ RazorCommentStar,
+ RazorCommentTransition
+ }
+}
diff --git a/src/System.Web.Razor/Tokenizer/Symbols/ISymbol.cs b/src/System.Web.Razor/Tokenizer/Symbols/ISymbol.cs
new file mode 100644
index 00000000..2d86896e
--- /dev/null
+++ b/src/System.Web.Razor/Tokenizer/Symbols/ISymbol.cs
@@ -0,0 +1,13 @@
+using System.Web.Razor.Text;
+
+namespace System.Web.Razor.Tokenizer.Symbols
+{
+ public interface ISymbol
+ {
+ SourceLocation Start { get; }
+ string Content { get; }
+
+ void OffsetStart(SourceLocation documentStart);
+ void ChangeStart(SourceLocation newStart);
+ }
+}
diff --git a/src/System.Web.Razor/Tokenizer/Symbols/KnownSymbolType.cs b/src/System.Web.Razor/Tokenizer/Symbols/KnownSymbolType.cs
new file mode 100644
index 00000000..32d27b79
--- /dev/null
+++ b/src/System.Web.Razor/Tokenizer/Symbols/KnownSymbolType.cs
@@ -0,0 +1,15 @@
+namespace System.Web.Razor.Tokenizer.Symbols
+{
+ public enum KnownSymbolType
+ {
+ WhiteSpace,
+ NewLine,
+ Identifier,
+ Keyword,
+ Transition,
+ Unknown,
+ CommentStart,
+ CommentStar,
+ CommentBody
+ }
+}
diff --git a/src/System.Web.Razor/Tokenizer/Symbols/SymbolBase.cs b/src/System.Web.Razor/Tokenizer/Symbols/SymbolBase.cs
new file mode 100644
index 00000000..75f0a9cb
--- /dev/null
+++ b/src/System.Web.Razor/Tokenizer/Symbols/SymbolBase.cs
@@ -0,0 +1,69 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Text;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.Razor.Tokenizer.Symbols
+{
+ public abstract class SymbolBase<TType> : ISymbol
+ {
+ protected SymbolBase(SourceLocation start, string content, TType type, IEnumerable<RazorError> errors)
+ {
+ if (content == null)
+ {
+ throw new ArgumentNullException("content");
+ }
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+
+ Start = start;
+ Content = content;
+ Type = type;
+ Errors = errors;
+ }
+
+ public SourceLocation Start { get; private set; }
+ public string Content { get; private set; }
+ public IEnumerable<RazorError> Errors { get; private set; }
+
+ [SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods", Justification = "This is the most appropriate name for this property and conflicts are unlikely")]
+ public TType Type { get; private set; }
+
+ public override bool Equals(object obj)
+ {
+ SymbolBase<TType> other = obj as SymbolBase<TType>;
+ return other != null &&
+ Start.Equals(other.Start) &&
+ String.Equals(Content, other.Content, StringComparison.Ordinal) &&
+ Type.Equals(other.Type);
+ }
+
+ public override int GetHashCode()
+ {
+ return HashCodeCombiner.Start()
+ .Add(Start)
+ .Add(Content)
+ .Add(Type)
+ .CombinedHash;
+ }
+
+ public override string ToString()
+ {
+ return String.Format(CultureInfo.InvariantCulture, "{0} {1} - [{2}]", Start, Type, Content);
+ }
+
+ public void OffsetStart(SourceLocation documentStart)
+ {
+ Start = documentStart + Start;
+ }
+
+ public void ChangeStart(SourceLocation newStart)
+ {
+ Start = newStart;
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Tokenizer/Symbols/SymbolExtensions.cs b/src/System.Web.Razor/Tokenizer/Symbols/SymbolExtensions.cs
new file mode 100644
index 00000000..26586b3d
--- /dev/null
+++ b/src/System.Web.Razor/Tokenizer/Symbols/SymbolExtensions.cs
@@ -0,0 +1,39 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Text;
+
+namespace System.Web.Razor.Tokenizer.Symbols
+{
+ public static class SymbolExtensions
+ {
+ public static LocationTagged<string> GetContent(this SpanBuilder span)
+ {
+ return GetContent(span, e => e);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Func<T> is the recommended type for generic delegates and requires this level of nesting")]
+ public static LocationTagged<string> GetContent(this SpanBuilder span, Func<IEnumerable<ISymbol>, IEnumerable<ISymbol>> filter)
+ {
+ return GetContent(filter(span.Symbols), span.Start);
+ }
+
+ public static LocationTagged<string> GetContent(this IEnumerable<ISymbol> symbols, SourceLocation spanStart)
+ {
+ if (symbols.Any())
+ {
+ return new LocationTagged<string>(String.Concat(symbols.Select(s => s.Content)), spanStart + symbols.First().Start);
+ }
+ else
+ {
+ return new LocationTagged<string>(String.Empty, spanStart);
+ }
+ }
+
+ public static LocationTagged<string> GetContent(this ISymbol symbol)
+ {
+ return new LocationTagged<string>(symbol.Content, symbol.Start);
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Tokenizer/Symbols/SymbolTypeSuppressions.cs b/src/System.Web.Razor/Tokenizer/Symbols/SymbolTypeSuppressions.cs
new file mode 100644
index 00000000..a810f3aa
--- /dev/null
+++ b/src/System.Web.Razor/Tokenizer/Symbols/SymbolTypeSuppressions.cs
@@ -0,0 +1,24 @@
+// Centralized all the supressions for the CSharpSymbolType and VBSymbolType enum members here for clarity. They are
+// not in the CodeAnalysisDictionary because they are special case exclusions
+
+using System.Diagnostics.CodeAnalysis;
+
+[assembly: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Foreach", Scope = "member", Target = "System.Web.Razor.Tokenizer.Symbols.CSharpKeyword.#Foreach", Justification = Justifications.SymbolTypeNames)]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Readonly", Scope = "member", Target = "System.Web.Razor.Tokenizer.Symbols.CSharpKeyword.#Readonly", Justification = Justifications.SymbolTypeNames)]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Sbyte", Scope = "member", Target = "System.Web.Razor.Tokenizer.Symbols.CSharpKeyword.#Sbyte", Justification = Justifications.SymbolTypeNames)]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Sizeof", Scope = "member", Target = "System.Web.Razor.Tokenizer.Symbols.CSharpKeyword.#Sizeof", Justification = Justifications.SymbolTypeNames)]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Stackalloc", Scope = "member", Target = "System.Web.Razor.Tokenizer.Symbols.CSharpKeyword.#Stackalloc", Justification = Justifications.SymbolTypeNames)]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Typeof", Scope = "member", Target = "System.Web.Razor.Tokenizer.Symbols.CSharpKeyword.#Typeof", Justification = Justifications.SymbolTypeNames)]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Uint", Scope = "member", Target = "System.Web.Razor.Tokenizer.Symbols.CSharpKeyword.#Uint", Justification = Justifications.SymbolTypeNames)]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Ulong", Scope = "member", Target = "System.Web.Razor.Tokenizer.Symbols.CSharpKeyword.#Ulong", Justification = Justifications.SymbolTypeNames)]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Ushort", Scope = "member", Target = "System.Web.Razor.Tokenizer.Symbols.CSharpKeyword.#Ushort", Justification = Justifications.SymbolTypeNames)]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Val", Scope = "member", Target = "System.Web.Razor.Tokenizer.Symbols.VBKeyword.#ByVal", Justification = Justifications.SymbolTypeNames)]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Sng", Scope = "member", Target = "System.Web.Razor.Tokenizer.Symbols.VBKeyword.#CSng", Justification = Justifications.SymbolTypeNames)]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "ReDim", Scope = "member", Target = "System.Web.Razor.Tokenizer.Symbols.VBKeyword.#ReDim", Justification = Justifications.SymbolTypeNames)]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Re", Scope = "member", Target = "System.Web.Razor.Tokenizer.Symbols.VBKeyword.#ReDim", Justification = Justifications.SymbolTypeNames)]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Str", Scope = "member", Target = "System.Web.Razor.Tokenizer.Symbols.VBKeyword.#CStr", Justification = Justifications.SymbolTypeNames)]
+
+internal static partial class Justifications
+{
+ internal const string SymbolTypeNames = "Symbol Type Names are spelled according to the language keyword or token they represent";
+}
diff --git a/src/System.Web.Razor/Tokenizer/Symbols/VBKeyword.cs b/src/System.Web.Razor/Tokenizer/Symbols/VBKeyword.cs
new file mode 100644
index 00000000..0f0da905
--- /dev/null
+++ b/src/System.Web.Razor/Tokenizer/Symbols/VBKeyword.cs
@@ -0,0 +1,166 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Web.Razor.Tokenizer.Symbols
+{
+ public enum VBKeyword
+ {
+ AddHandler,
+ AndAlso,
+ Byte,
+ Catch,
+ CDate,
+ CInt,
+ Const,
+ CSng,
+
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Lng", Justification = "This is a VB Keyword. Note: Excluded here because it is a specific case")]
+ CULng,
+ Declare,
+ DirectCast,
+ Else,
+ Enum,
+ Exit,
+ Friend,
+ GetXmlNamespace,
+ Handles,
+ In,
+ Is,
+ Like,
+ Mod,
+ MyBase,
+ New,
+ AddressOf,
+ As,
+ ByVal,
+ CBool,
+
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Dbl", Justification = "This is a VB Keyword. Note: Excluded here because it is a specific case")]
+ CDbl,
+ Class,
+ Continue,
+ CStr,
+ CUShort,
+ Default,
+ Do,
+ ElseIf,
+ Erase,
+ False,
+ Function,
+ Global,
+ If,
+ Inherits,
+ IsNot,
+ Long,
+ Module,
+ MyClass,
+ Next,
+ Alias,
+ Boolean,
+ Call,
+ CByte,
+ CDec,
+
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Lng", Justification = "This is a VB Keyword. Note: Excluded here because it is a specific case")]
+ CLng,
+ CSByte,
+ CType,
+ Date,
+ Delegate,
+ Double,
+ End,
+ Error,
+ Finally,
+ Get,
+ GoSub,
+ Implements,
+ Integer,
+ Let,
+ Loop,
+ MustInherit,
+ Namespace,
+ Not,
+ And,
+ ByRef,
+ Case,
+ CChar,
+ Char,
+ CObj,
+ CShort,
+ CUInt,
+ Decimal,
+ Dim,
+ Each,
+ EndIf,
+ Event,
+ For,
+ GetType,
+ GoTo,
+ Imports,
+ Interface,
+ Lib,
+ Me,
+ MustOverride,
+ Narrowing,
+ Nothing,
+ NotInheritable,
+ On,
+ Or,
+ Overrides,
+ Property,
+ ReadOnly,
+ Resume,
+ Set,
+ Single,
+ String,
+ Then,
+ Try,
+ ULong,
+ Wend,
+ With,
+ NotOverridable,
+ Operator,
+ OrElse,
+ ParamArray,
+ Protected,
+ ReDim,
+ Return,
+ Shadows,
+ Static,
+ Structure,
+ Throw,
+ TryCast,
+ UShort,
+ When,
+ WithEvents,
+ Object,
+ Option,
+ Overloads,
+ Partial,
+ Public,
+ Rem,
+ SByte,
+ Shared,
+ Step,
+ Sub,
+ To,
+ TypeOf,
+ Using,
+ While,
+ WriteOnly,
+ Of,
+ Optional,
+ Overridable,
+ Private,
+ RaiseEvent,
+ RemoveHandler,
+ Select,
+ Short,
+ Stop,
+ SyncLock,
+ True,
+ UInteger,
+ Variant,
+ Widening,
+ Xor
+ }
+}
diff --git a/src/System.Web.Razor/Tokenizer/Symbols/VBSymbol.cs b/src/System.Web.Razor/Tokenizer/Symbols/VBSymbol.cs
new file mode 100644
index 00000000..7bd91a09
--- /dev/null
+++ b/src/System.Web.Razor/Tokenizer/Symbols/VBSymbol.cs
@@ -0,0 +1,112 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Text;
+
+namespace System.Web.Razor.Tokenizer.Symbols
+{
+ public class VBSymbol : SymbolBase<VBSymbolType>
+ {
+ // Helper constructor
+ private static Dictionary<VBSymbolType, string> _symbolSamples = new Dictionary<VBSymbolType, string>()
+ {
+ { VBSymbolType.LineContinuation, "_" },
+ { VBSymbolType.LeftParenthesis, "(" },
+ { VBSymbolType.RightParenthesis, ")" },
+ { VBSymbolType.LeftBracket, "[" },
+ { VBSymbolType.RightBracket, "]" },
+ { VBSymbolType.LeftBrace, "{" },
+ { VBSymbolType.RightBrace, "}" },
+ { VBSymbolType.Bang, "!" },
+ { VBSymbolType.Hash, "#" },
+ { VBSymbolType.Comma, "," },
+ { VBSymbolType.Dot, "." },
+ { VBSymbolType.Colon, ":" },
+ { VBSymbolType.QuestionMark, "?" },
+ { VBSymbolType.Concatenation, "&" },
+ { VBSymbolType.Multiply, "*" },
+ { VBSymbolType.Add, "+" },
+ { VBSymbolType.Subtract, "-" },
+ { VBSymbolType.Divide, "/" },
+ { VBSymbolType.IntegerDivide, "\\" },
+ { VBSymbolType.Exponentiation, "^" },
+ { VBSymbolType.Equal, "=" },
+ { VBSymbolType.LessThan, "<" },
+ { VBSymbolType.GreaterThan, ">" },
+ { VBSymbolType.Dollar, "$" },
+ { VBSymbolType.Transition, "@" },
+ { VBSymbolType.RazorCommentTransition, "@" },
+ { VBSymbolType.RazorCommentStar, "*" }
+ };
+
+ public VBSymbol(int offset, int line, int column, string content, VBSymbolType type)
+ : this(new SourceLocation(offset, line, column), content, type, Enumerable.Empty<RazorError>())
+ {
+ }
+
+ public VBSymbol(SourceLocation start, string content, VBSymbolType type)
+ : this(start, content, type, Enumerable.Empty<RazorError>())
+ {
+ }
+
+ public VBSymbol(int offset, int line, int column, string content, VBSymbolType type, IEnumerable<RazorError> errors)
+ : base(new SourceLocation(offset, line, column), content, type, errors)
+ {
+ }
+
+ public VBSymbol(SourceLocation start, string content, VBSymbolType type, IEnumerable<RazorError> errors)
+ : base(start, content, type, errors)
+ {
+ }
+
+ public VBKeyword? Keyword { get; set; }
+
+ public override bool Equals(object obj)
+ {
+ VBSymbol other = obj as VBSymbol;
+ return base.Equals(obj) && other.Keyword == Keyword;
+ }
+
+ public override int GetHashCode()
+ {
+ return base.GetHashCode() ^ Keyword.GetHashCode();
+ }
+
+ public static string GetSample(VBSymbolType type)
+ {
+ string sample;
+ if (!_symbolSamples.TryGetValue(type, out sample))
+ {
+ switch (type)
+ {
+ case VBSymbolType.WhiteSpace:
+ return RazorResources.VBSymbol_WhiteSpace;
+ case VBSymbolType.NewLine:
+ return RazorResources.VBSymbol_NewLine;
+ case VBSymbolType.Comment:
+ return RazorResources.VBSymbol_Comment;
+ case VBSymbolType.Identifier:
+ return RazorResources.VBSymbol_Identifier;
+ case VBSymbolType.Keyword:
+ return RazorResources.VBSymbol_Keyword;
+ case VBSymbolType.IntegerLiteral:
+ return RazorResources.VBSymbol_IntegerLiteral;
+ case VBSymbolType.FloatingPointLiteral:
+ return RazorResources.VBSymbol_FloatingPointLiteral;
+ case VBSymbolType.StringLiteral:
+ return RazorResources.VBSymbol_StringLiteral;
+ case VBSymbolType.CharacterLiteral:
+ return RazorResources.VBSymbol_CharacterLiteral;
+ case VBSymbolType.DateLiteral:
+ return RazorResources.VBSymbol_DateLiteral;
+ case VBSymbolType.RazorComment:
+ return RazorResources.VBSymbol_RazorComment;
+ default:
+ return RazorResources.Symbol_Unknown;
+ }
+ }
+ return sample;
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Tokenizer/Symbols/VBSymbolType.cs b/src/System.Web.Razor/Tokenizer/Symbols/VBSymbolType.cs
new file mode 100644
index 00000000..f6bc7756
--- /dev/null
+++ b/src/System.Web.Razor/Tokenizer/Symbols/VBSymbolType.cs
@@ -0,0 +1,46 @@
+namespace System.Web.Razor.Tokenizer.Symbols
+{
+ public enum VBSymbolType
+ {
+ Unknown,
+ WhiteSpace,
+ NewLine,
+ LineContinuation,
+ Comment,
+ Identifier,
+ Keyword,
+ IntegerLiteral,
+ FloatingPointLiteral,
+ StringLiteral,
+ CharacterLiteral,
+ DateLiteral,
+ LeftParenthesis,
+ RightBrace,
+ LeftBrace,
+ RightParenthesis,
+ Hash,
+ Bang,
+ Comma,
+ Dot,
+ Colon,
+ Concatenation,
+ QuestionMark,
+ Subtract,
+ Multiply,
+ Add,
+ Divide,
+ IntegerDivide,
+ Exponentiation,
+ LessThan,
+ GreaterThan,
+ Equal,
+ RightBracket,
+ LeftBracket,
+ Dollar,
+ Transition,
+
+ RazorCommentTransition,
+ RazorCommentStar,
+ RazorComment
+ }
+}
diff --git a/src/System.Web.Razor/Tokenizer/Tokenizer.cs b/src/System.Web.Razor/Tokenizer/Tokenizer.cs
new file mode 100644
index 00000000..fa95b15c
--- /dev/null
+++ b/src/System.Web.Razor/Tokenizer/Tokenizer.cs
@@ -0,0 +1,352 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Text;
+using System.Web.Razor.Tokenizer.Symbols;
+
+namespace System.Web.Razor.Tokenizer
+{
+ public abstract partial class Tokenizer<TSymbol, TSymbolType> : StateMachine<TSymbol>, ITokenizer
+ where TSymbol : SymbolBase<TSymbolType>
+ {
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "TextDocumentReader does not require disposal")]
+ protected Tokenizer(ITextDocument source)
+ {
+ if (source == null)
+ {
+ throw new ArgumentNullException("source");
+ }
+ Source = new TextDocumentReader(source);
+ Buffer = new StringBuilder();
+ CurrentErrors = new List<RazorError>();
+ StartSymbol();
+ }
+
+ public TextDocumentReader Source { get; private set; }
+
+ protected StringBuilder Buffer { get; private set; }
+
+ protected bool EndOfFile
+ {
+ get { return Source.Peek() == -1; }
+ }
+
+ protected IList<RazorError> CurrentErrors { get; private set; }
+
+ public abstract TSymbolType RazorCommentStarType { get; }
+ public abstract TSymbolType RazorCommentType { get; }
+ public abstract TSymbolType RazorCommentTransitionType { get; }
+
+ protected bool HaveContent
+ {
+ get { return Buffer.Length > 0; }
+ }
+
+ protected char CurrentCharacter
+ {
+ get
+ {
+ int peek = Source.Peek();
+ return peek == -1 ? '\0' : (char)peek;
+ }
+ }
+
+ protected SourceLocation CurrentLocation
+ {
+ get { return Source.Location; }
+ }
+
+ protected SourceLocation CurrentStart { get; private set; }
+
+ public virtual TSymbol NextSymbol()
+ {
+ // Post-Condition: Buffer should be empty at the start of Next()
+ Debug.Assert(Buffer.Length == 0);
+ StartSymbol();
+
+ if (EndOfFile)
+ {
+ return null;
+ }
+ TSymbol sym = Turn();
+
+ // Post-Condition: Buffer should be empty at the end of Next()
+ Debug.Assert(Buffer.Length == 0);
+
+ return sym;
+ }
+
+ public void Reset()
+ {
+ CurrentState = StartState;
+ }
+
+ protected abstract TSymbol CreateSymbol(SourceLocation start, string content, TSymbolType type, IEnumerable<RazorError> errors);
+
+ protected TSymbol Single(TSymbolType type)
+ {
+ TakeCurrent();
+ return EndSymbol(type);
+ }
+
+ protected bool TakeString(string input, bool caseSensitive)
+ {
+ int position = 0;
+ Func<char, char> charFilter = c => c;
+ if (caseSensitive)
+ {
+ charFilter = Char.ToLower;
+ }
+ while (!EndOfFile && position < input.Length && charFilter(CurrentCharacter) == charFilter(input[position++]))
+ {
+ TakeCurrent();
+ }
+ return position == input.Length;
+ }
+
+ protected void StartSymbol()
+ {
+ Buffer.Clear();
+ CurrentStart = CurrentLocation;
+ CurrentErrors.Clear();
+ }
+
+ protected TSymbol EndSymbol(TSymbolType type)
+ {
+ return EndSymbol(CurrentStart, type);
+ }
+
+ protected TSymbol EndSymbol(SourceLocation start, TSymbolType type)
+ {
+ TSymbol sym = null;
+ if (HaveContent)
+ {
+ sym = CreateSymbol(start, Buffer.ToString(), type, CurrentErrors.ToArray());
+ }
+ StartSymbol();
+ return sym;
+ }
+
+ protected void ResumeSymbol(TSymbol previous)
+ {
+ // Verify the symbol can be resumed
+ if (previous.Start.AbsoluteIndex + previous.Content.Length != CurrentStart.AbsoluteIndex)
+ {
+ throw new InvalidOperationException(RazorResources.Tokenizer_CannotResumeSymbolUnlessIsPrevious);
+ }
+
+ // Reset the start point
+ CurrentStart = previous.Start;
+
+ // Capture the current buffer content
+ string newContent = Buffer.ToString();
+
+ // Clear the buffer, then put the old content back and add the new content to the end
+ Buffer.Clear();
+ Buffer.Append(previous.Content);
+ Buffer.Append(newContent);
+ }
+
+ protected bool TakeUntil(Func<char, bool> predicate)
+ {
+ // Take all the characters up to the end character
+ while (!EndOfFile && !predicate(CurrentCharacter))
+ {
+ TakeCurrent();
+ }
+
+ // Why did we end?
+ return !EndOfFile;
+ }
+
+ protected Func<char, bool> CharOrWhiteSpace(char character)
+ {
+ return c => c == character || ParserHelpers.IsWhitespace(c) || ParserHelpers.IsNewLine(c);
+ }
+
+ protected void TakeCurrent()
+ {
+ if (EndOfFile)
+ {
+ return;
+ } // No-op
+ Buffer.Append(CurrentCharacter);
+ MoveNext();
+ }
+
+ protected void MoveNext()
+ {
+#if DEBUG
+ _read.Append(CurrentCharacter);
+#endif
+ Source.Read();
+ }
+
+ protected bool TakeAll(string expected, bool caseSensitive)
+ {
+ return Lookahead(expected, takeIfMatch: true, caseSensitive: caseSensitive);
+ }
+
+ protected bool At(string expected, bool caseSensitive)
+ {
+ return Lookahead(expected, takeIfMatch: false, caseSensitive: caseSensitive);
+ }
+
+ protected char Peek()
+ {
+ using (LookaheadToken lookahead = Source.BeginLookahead())
+ {
+ MoveNext();
+ return CurrentCharacter;
+ }
+ }
+
+ protected StateResult AfterRazorCommentTransition()
+ {
+ if (CurrentCharacter != '*')
+ {
+ // We've been moved since last time we were asked for a symbol... reset the state
+ return Transition(StartState);
+ }
+ AssertCurrent('*');
+ TakeCurrent();
+ return Transition(EndSymbol(RazorCommentStarType), RazorCommentBody);
+ }
+
+ protected StateResult RazorCommentBody()
+ {
+ TakeUntil(c => c == '*');
+ if (CurrentCharacter == '*')
+ {
+ char star = CurrentCharacter;
+ SourceLocation start = CurrentLocation;
+ MoveNext();
+ if (!EndOfFile && CurrentCharacter == '@')
+ {
+ State next = () =>
+ {
+ Buffer.Append(star);
+ return Transition(EndSymbol(start, RazorCommentStarType), () =>
+ {
+ if (CurrentCharacter != '@')
+ {
+ // We've been moved since last time we were asked for a symbol... reset the state
+ return Transition(StartState);
+ }
+ TakeCurrent();
+ return Transition(EndSymbol(RazorCommentTransitionType), StartState);
+ });
+ };
+
+ if (HaveContent)
+ {
+ return Transition(EndSymbol(RazorCommentType), next);
+ }
+ else
+ {
+ return Transition(next);
+ }
+ }
+ else
+ {
+ Buffer.Append(star);
+ return Stay();
+ }
+ }
+ return Transition(EndSymbol(RazorCommentType), StartState);
+ }
+
+ private bool Lookahead(string expected, bool takeIfMatch, bool caseSensitive)
+ {
+ Func<char, char> filter = c => c;
+ if (!caseSensitive)
+ {
+ filter = Char.ToLowerInvariant;
+ }
+
+ if (expected.Length == 0 || filter(CurrentCharacter) != filter(expected[0]))
+ {
+ return false;
+ }
+
+ // Capture the current buffer content in case we have to backtrack
+ string oldBuffer = null;
+ if (takeIfMatch)
+ {
+ Buffer.ToString();
+ }
+
+ using (LookaheadToken lookahead = Source.BeginLookahead())
+ {
+ for (int i = 0; i < expected.Length; i++)
+ {
+ if (filter(CurrentCharacter) != filter(expected[i]))
+ {
+ if (takeIfMatch)
+ {
+ // Clear the buffer and put the old buffer text back
+ Buffer.Clear();
+ Buffer.Append(oldBuffer);
+ }
+ // Return without accepting lookahead (thus rejecting it)
+ return false;
+ }
+ if (takeIfMatch)
+ {
+ TakeCurrent();
+ }
+ else
+ {
+ MoveNext();
+ }
+ }
+ if (takeIfMatch)
+ {
+ lookahead.Accept();
+ }
+ }
+ return true;
+ }
+
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This only occurs in Release builds, where this method is empty by design")]
+ [Conditional("DEBUG")]
+ internal void AssertCurrent(char current)
+ {
+ Debug.Assert(CurrentCharacter == current, "CurrentCharacter Assumption violated", "Assumed that the current character would be {0}, but it is actually {1}", current, CurrentCharacter);
+ }
+
+ ISymbol ITokenizer.NextSymbol()
+ {
+ return (ISymbol)NextSymbol();
+ }
+ }
+
+#if DEBUG
+ [DebuggerDisplay("{DebugDisplay}")]
+ public partial class Tokenizer<TSymbol, TSymbolType>
+ {
+ private StringBuilder _read = new StringBuilder();
+
+ public string DebugDisplay
+ {
+ get { return String.Format(CultureInfo.InvariantCulture, "[{0}] [{1}] [{2}]", _read.ToString(), CurrentCharacter, Remaining); }
+ }
+
+ public string Remaining
+ {
+ get
+ {
+ string remaining = Source.ReadToEnd();
+ Source.Seek(-remaining.Length);
+ return remaining;
+ }
+ }
+ }
+#endif
+}
diff --git a/src/System.Web.Razor/Tokenizer/TokenizerView.cs b/src/System.Web.Razor/Tokenizer/TokenizerView.cs
new file mode 100644
index 00000000..1ce40a86
--- /dev/null
+++ b/src/System.Web.Razor/Tokenizer/TokenizerView.cs
@@ -0,0 +1,54 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Text;
+using System.Web.Razor.Tokenizer.Symbols;
+
+namespace System.Web.Razor.Tokenizer
+{
+ [SuppressMessage("Microsoft.Design", "CA1005:AvoidExcessiveParametersOnGenericTypes", Justification = "All generic parameters are required")]
+ public class TokenizerView<TTokenizer, TSymbol, TSymbolType>
+ where TTokenizer : Tokenizer<TSymbol, TSymbolType>
+ where TSymbol : SymbolBase<TSymbolType>
+ {
+ public TokenizerView(TTokenizer tokenizer)
+ {
+ Tokenizer = tokenizer;
+ }
+
+ public TTokenizer Tokenizer { get; private set; }
+ public bool EndOfFile { get; private set; }
+ public TSymbol Current { get; private set; }
+
+ public ITextDocument Source
+ {
+ get { return Tokenizer.Source; }
+ }
+
+ public bool Next()
+ {
+ Current = Tokenizer.NextSymbol();
+ EndOfFile = (Current == null);
+ return !EndOfFile;
+ }
+
+ public void PutBack(TSymbol symbol)
+ {
+ Debug.Assert(Source.Position == symbol.Start.AbsoluteIndex + symbol.Content.Length);
+ if (Source.Position != symbol.Start.AbsoluteIndex + symbol.Content.Length)
+ {
+ // We've already passed this symbol
+ throw new InvalidOperationException(
+ String.Format(CultureInfo.CurrentCulture,
+ RazorResources.TokenizerView_CannotPutBack,
+ symbol.Start.AbsoluteIndex + symbol.Content.Length,
+ Source.Position));
+ }
+ Source.Position -= symbol.Content.Length;
+ Current = null;
+ EndOfFile = Source.Position >= Source.Length;
+ Tokenizer.Reset();
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Tokenizer/VBHelpers.cs b/src/System.Web.Razor/Tokenizer/VBHelpers.cs
new file mode 100644
index 00000000..d39ba437
--- /dev/null
+++ b/src/System.Web.Razor/Tokenizer/VBHelpers.cs
@@ -0,0 +1,20 @@
+namespace System.Web.Razor.Tokenizer
+{
+ public static class VBHelpers
+ {
+ public static bool IsSingleQuote(char character)
+ {
+ return character == '\'' || character == '‘' || character == '’';
+ }
+
+ public static bool IsDoubleQuote(char character)
+ {
+ return character == '"' || character == '“' || character == '”';
+ }
+
+ public static bool IsOctalDigit(char character)
+ {
+ return character >= '0' && character <= '7';
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Tokenizer/VBKeywordDetector.cs b/src/System.Web.Razor/Tokenizer/VBKeywordDetector.cs
new file mode 100644
index 00000000..43dd9e4e
--- /dev/null
+++ b/src/System.Web.Razor/Tokenizer/VBKeywordDetector.cs
@@ -0,0 +1,174 @@
+using System.Collections.Generic;
+using System.Web.Razor.Tokenizer.Symbols;
+
+namespace System.Web.Razor.Tokenizer
+{
+ internal static class VBKeywordDetector
+ {
+ private static readonly Dictionary<string, VBKeyword> _keywords = new Dictionary<string, VBKeyword>(StringComparer.OrdinalIgnoreCase)
+ {
+ { "addhandler", VBKeyword.AddHandler },
+ { "andalso", VBKeyword.AndAlso },
+ { "byte", VBKeyword.Byte },
+ { "catch", VBKeyword.Catch },
+ { "cdate", VBKeyword.CDate },
+ { "cint", VBKeyword.CInt },
+ { "const", VBKeyword.Const },
+ { "csng", VBKeyword.CSng },
+ { "culng", VBKeyword.CULng },
+ { "declare", VBKeyword.Declare },
+ { "directcast", VBKeyword.DirectCast },
+ { "else", VBKeyword.Else },
+ { "enum", VBKeyword.Enum },
+ { "exit", VBKeyword.Exit },
+ { "friend", VBKeyword.Friend },
+ { "getxmlnamespace", VBKeyword.GetXmlNamespace },
+ { "handles", VBKeyword.Handles },
+ { "in", VBKeyword.In },
+ { "is", VBKeyword.Is },
+ { "like", VBKeyword.Like },
+ { "mod", VBKeyword.Mod },
+ { "mybase", VBKeyword.MyBase },
+ { "new", VBKeyword.New },
+ { "addressof", VBKeyword.AddressOf },
+ { "as", VBKeyword.As },
+ { "byval", VBKeyword.ByVal },
+ { "cbool", VBKeyword.CBool },
+ { "cdbl", VBKeyword.CDbl },
+ { "class", VBKeyword.Class },
+ { "continue", VBKeyword.Continue },
+ { "cstr", VBKeyword.CStr },
+ { "cushort", VBKeyword.CUShort },
+ { "default", VBKeyword.Default },
+ { "do", VBKeyword.Do },
+ { "elseif", VBKeyword.ElseIf },
+ { "erase", VBKeyword.Erase },
+ { "false", VBKeyword.False },
+ { "function", VBKeyword.Function },
+ { "global", VBKeyword.Global },
+ { "if", VBKeyword.If },
+ { "inherits", VBKeyword.Inherits },
+ { "isnot", VBKeyword.IsNot },
+ { "long", VBKeyword.Long },
+ { "module", VBKeyword.Module },
+ { "myclass", VBKeyword.MyClass },
+ { "next", VBKeyword.Next },
+ { "alias", VBKeyword.Alias },
+ { "boolean", VBKeyword.Boolean },
+ { "call", VBKeyword.Call },
+ { "cbyte", VBKeyword.CByte },
+ { "cdec", VBKeyword.CDec },
+ { "clng", VBKeyword.CLng },
+ { "csbyte", VBKeyword.CSByte },
+ { "ctype", VBKeyword.CType },
+ { "date", VBKeyword.Date },
+ { "delegate", VBKeyword.Delegate },
+ { "double", VBKeyword.Double },
+ { "end", VBKeyword.End },
+ { "error", VBKeyword.Error },
+ { "finally", VBKeyword.Finally },
+ { "get", VBKeyword.Get },
+ { "gosub", VBKeyword.GoSub },
+ { "implements", VBKeyword.Implements },
+ { "integer", VBKeyword.Integer },
+ { "let", VBKeyword.Let },
+ { "loop", VBKeyword.Loop },
+ { "mustinherit", VBKeyword.MustInherit },
+ { "namespace", VBKeyword.Namespace },
+ { "not", VBKeyword.Not },
+ { "and", VBKeyword.And },
+ { "byref", VBKeyword.ByRef },
+ { "case", VBKeyword.Case },
+ { "cchar", VBKeyword.CChar },
+ { "char", VBKeyword.Char },
+ { "cobj", VBKeyword.CObj },
+ { "cshort", VBKeyword.CShort },
+ { "cuint", VBKeyword.CUInt },
+ { "decimal", VBKeyword.Decimal },
+ { "dim", VBKeyword.Dim },
+ { "each", VBKeyword.Each },
+ { "endif", VBKeyword.EndIf },
+ { "event", VBKeyword.Event },
+ { "for", VBKeyword.For },
+ { "gettype", VBKeyword.GetType },
+ { "goto", VBKeyword.GoTo },
+ { "imports", VBKeyword.Imports },
+ { "interface", VBKeyword.Interface },
+ { "lib", VBKeyword.Lib },
+ { "me", VBKeyword.Me },
+ { "mustoverride", VBKeyword.MustOverride },
+ { "narrowing", VBKeyword.Narrowing },
+ { "nothing", VBKeyword.Nothing },
+ { "notinheritable", VBKeyword.NotInheritable },
+ { "on", VBKeyword.On },
+ { "or", VBKeyword.Or },
+ { "overrides", VBKeyword.Overrides },
+ { "property", VBKeyword.Property },
+ { "rem", VBKeyword.Rem },
+ { "readonly", VBKeyword.ReadOnly },
+ { "resume", VBKeyword.Resume },
+ { "set", VBKeyword.Set },
+ { "single", VBKeyword.Single },
+ { "string", VBKeyword.String },
+ { "then", VBKeyword.Then },
+ { "try", VBKeyword.Try },
+ { "ulong", VBKeyword.ULong },
+ { "wend", VBKeyword.Wend },
+ { "with", VBKeyword.With },
+ { "notoverridable", VBKeyword.NotOverridable },
+ { "operator", VBKeyword.Operator },
+ { "orelse", VBKeyword.OrElse },
+ { "paramarray", VBKeyword.ParamArray },
+ { "protected", VBKeyword.Protected },
+ { "redim", VBKeyword.ReDim },
+ { "return", VBKeyword.Return },
+ { "shadows", VBKeyword.Shadows },
+ { "static", VBKeyword.Static },
+ { "structure", VBKeyword.Structure },
+ { "throw", VBKeyword.Throw },
+ { "trycast", VBKeyword.TryCast },
+ { "ushort", VBKeyword.UShort },
+ { "when", VBKeyword.When },
+ { "withevents", VBKeyword.WithEvents },
+ { "object", VBKeyword.Object },
+ { "option", VBKeyword.Option },
+ { "overloads", VBKeyword.Overloads },
+ { "partial", VBKeyword.Partial },
+ { "public", VBKeyword.Public },
+ { "sbyte", VBKeyword.SByte },
+ { "shared", VBKeyword.Shared },
+ { "step", VBKeyword.Step },
+ { "sub", VBKeyword.Sub },
+ { "to", VBKeyword.To },
+ { "typeof", VBKeyword.TypeOf },
+ { "using", VBKeyword.Using },
+ { "while", VBKeyword.While },
+ { "writeonly", VBKeyword.WriteOnly },
+ { "of", VBKeyword.Of },
+ { "optional", VBKeyword.Optional },
+ { "overridable", VBKeyword.Overridable },
+ { "private", VBKeyword.Private },
+ { "raiseevent", VBKeyword.RaiseEvent },
+ { "removehandler", VBKeyword.RemoveHandler },
+ { "select", VBKeyword.Select },
+ { "short", VBKeyword.Short },
+ { "stop", VBKeyword.Stop },
+ { "synclock", VBKeyword.SyncLock },
+ { "true", VBKeyword.True },
+ { "uinteger", VBKeyword.UInteger },
+ { "variant", VBKeyword.Variant },
+ { "widening", VBKeyword.Widening },
+ { "xor", VBKeyword.Xor }
+ };
+
+ public static VBKeyword? GetKeyword(string id)
+ {
+ VBKeyword type;
+ if (!_keywords.TryGetValue(id, out type))
+ {
+ return null;
+ }
+ return type;
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Tokenizer/VBTokenizer.cs b/src/System.Web.Razor/Tokenizer/VBTokenizer.cs
new file mode 100644
index 00000000..f744be2d
--- /dev/null
+++ b/src/System.Web.Razor/Tokenizer/VBTokenizer.cs
@@ -0,0 +1,381 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Globalization;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Text;
+using System.Web.Razor.Tokenizer.Symbols;
+
+namespace System.Web.Razor.Tokenizer
+{
+ public class VBTokenizer : Tokenizer<VBSymbol, VBSymbolType>
+ {
+ private static Dictionary<char, VBSymbolType> _operatorTable = new Dictionary<char, VBSymbolType>()
+ {
+ { '_', VBSymbolType.LineContinuation },
+ { '(', VBSymbolType.LeftParenthesis },
+ { ')', VBSymbolType.RightParenthesis },
+ { '[', VBSymbolType.LeftBracket },
+ { ']', VBSymbolType.RightBracket },
+ { '{', VBSymbolType.LeftBrace },
+ { '}', VBSymbolType.RightBrace },
+ { '!', VBSymbolType.Bang },
+ { '#', VBSymbolType.Hash },
+ { ',', VBSymbolType.Comma },
+ { '.', VBSymbolType.Dot },
+ { ':', VBSymbolType.Colon },
+ { '?', VBSymbolType.QuestionMark },
+ { '&', VBSymbolType.Concatenation },
+ { '*', VBSymbolType.Multiply },
+ { '+', VBSymbolType.Add },
+ { '-', VBSymbolType.Subtract },
+ { '/', VBSymbolType.Divide },
+ { '\\', VBSymbolType.IntegerDivide },
+ { '^', VBSymbolType.Exponentiation },
+ { '=', VBSymbolType.Equal },
+ { '<', VBSymbolType.LessThan },
+ { '>', VBSymbolType.GreaterThan },
+ { '$', VBSymbolType.Dollar },
+ };
+
+ public VBTokenizer(ITextDocument source)
+ : base(source)
+ {
+ CurrentState = Data;
+ }
+
+ protected override State StartState
+ {
+ get { return Data; }
+ }
+
+ public override VBSymbolType RazorCommentType
+ {
+ get { return VBSymbolType.RazorComment; }
+ }
+
+ public override VBSymbolType RazorCommentTransitionType
+ {
+ get { return VBSymbolType.RazorCommentTransition; }
+ }
+
+ public override VBSymbolType RazorCommentStarType
+ {
+ get { return VBSymbolType.RazorCommentStar; }
+ }
+
+ internal static IEnumerable<VBSymbol> Tokenize(string content)
+ {
+ using (SeekableTextReader reader = new SeekableTextReader(content))
+ {
+ VBTokenizer tok = new VBTokenizer(reader);
+ VBSymbol sym;
+ while ((sym = tok.NextSymbol()) != null)
+ {
+ yield return sym;
+ }
+ }
+ }
+
+ protected override VBSymbol CreateSymbol(SourceLocation start, string content, VBSymbolType type, IEnumerable<RazorError> errors)
+ {
+ return new VBSymbol(start, content, type, errors);
+ }
+
+ private StateResult Data()
+ {
+ // We are accepting more characters and whitespace/newlines then the VB Spec defines, to simplify things
+ // Since the code must still be compiled by a VB compiler, this will not cause adverse effects.
+ if (ParserHelpers.IsNewLine(CurrentCharacter))
+ {
+ // VB Spec §2.1.1
+ bool checkTwoCharNewline = CurrentCharacter == '\r';
+ TakeCurrent();
+ if (checkTwoCharNewline && CurrentCharacter == '\n')
+ {
+ TakeCurrent();
+ }
+ return Stay(EndSymbol(VBSymbolType.NewLine));
+ }
+ else if (ParserHelpers.IsWhitespace(CurrentCharacter))
+ {
+ // CSharp Spec §2.1.3
+ TakeUntil(c => !ParserHelpers.IsWhitespace(c));
+ return Stay(EndSymbol(VBSymbolType.WhiteSpace));
+ }
+ else if (VBHelpers.IsSingleQuote(CurrentCharacter))
+ {
+ TakeCurrent();
+ return CommentBody();
+ }
+ else if (IsIdentifierStart())
+ {
+ return Identifier();
+ }
+ else if (Char.IsDigit(CurrentCharacter))
+ {
+ return DecimalLiteral();
+ }
+ else if (CurrentCharacter == '&')
+ {
+ char next = Char.ToLower(Peek(), CultureInfo.InvariantCulture);
+ if (next == 'h')
+ {
+ return HexLiteral();
+ }
+ else if (next == 'o')
+ {
+ return OctLiteral();
+ }
+ }
+ else if (CurrentCharacter == '.' && Char.IsDigit(Peek()))
+ {
+ return FloatingPointLiteralEnd();
+ }
+ else if (VBHelpers.IsDoubleQuote(CurrentCharacter))
+ {
+ TakeCurrent();
+ return Transition(QuotedLiteral);
+ }
+ else if (AtDateLiteral())
+ {
+ return DateLiteral();
+ }
+ else if (CurrentCharacter == '@')
+ {
+ TakeCurrent();
+ if (CurrentCharacter == '*')
+ {
+ return Transition(EndSymbol(VBSymbolType.RazorCommentTransition), AfterRazorCommentTransition);
+ }
+ else if (CurrentCharacter == '@')
+ {
+ // Could be escaped comment transition
+ return Transition(EndSymbol(VBSymbolType.Transition), () =>
+ {
+ TakeCurrent();
+ return Transition(EndSymbol(VBSymbolType.Transition), Data);
+ });
+ }
+ else
+ {
+ return Stay(EndSymbol(VBSymbolType.Transition));
+ }
+ }
+ return Stay(EndSymbol(Operator()));
+ }
+
+ private StateResult DateLiteral()
+ {
+ AssertCurrent('#');
+ TakeCurrent();
+ TakeUntil(c => c == '#' || ParserHelpers.IsNewLine(c));
+ if (CurrentCharacter == '#')
+ {
+ TakeCurrent();
+ }
+ return Stay(EndSymbol(VBSymbolType.DateLiteral));
+ }
+
+ private bool AtDateLiteral()
+ {
+ if (CurrentCharacter != '#')
+ {
+ return false;
+ }
+ int start = Source.Position;
+ try
+ {
+ MoveNext();
+ while (ParserHelpers.IsWhitespace(CurrentCharacter))
+ {
+ MoveNext();
+ }
+ return Char.IsDigit(CurrentCharacter);
+ }
+ finally
+ {
+ Source.Position = start;
+ }
+ }
+
+ private StateResult QuotedLiteral()
+ {
+ TakeUntil(c => VBHelpers.IsDoubleQuote(c) || ParserHelpers.IsNewLine(c));
+ if (VBHelpers.IsDoubleQuote(CurrentCharacter))
+ {
+ TakeCurrent();
+ if (VBHelpers.IsDoubleQuote(CurrentCharacter))
+ {
+ // Escape sequence, remain in the string
+ TakeCurrent();
+ return Stay();
+ }
+ }
+
+ VBSymbolType type = VBSymbolType.StringLiteral;
+ if (Char.ToLowerInvariant(CurrentCharacter) == 'c')
+ {
+ TakeCurrent();
+ type = VBSymbolType.CharacterLiteral;
+ }
+ return Transition(EndSymbol(type), Data);
+ }
+
+ private StateResult DecimalLiteral()
+ {
+ TakeUntil(c => !Char.IsDigit(c));
+ char lower = Char.ToLowerInvariant(CurrentCharacter);
+ if (IsFloatTypeSuffix(lower) || lower == '.' || lower == 'e')
+ {
+ return FloatingPointLiteralEnd();
+ }
+ else
+ {
+ TakeIntTypeSuffix();
+ return Stay(EndSymbol(VBSymbolType.IntegerLiteral));
+ }
+ }
+
+ private static bool IsFloatTypeSuffix(char chr)
+ {
+ chr = Char.ToLowerInvariant(chr);
+ return chr == 'f' || chr == 'r' || chr == 'd';
+ }
+
+ private StateResult FloatingPointLiteralEnd()
+ {
+ if (CurrentCharacter == '.')
+ {
+ TakeCurrent();
+ TakeUntil(c => !Char.IsDigit(c));
+ }
+ if (Char.ToLowerInvariant(CurrentCharacter) == 'e')
+ {
+ TakeCurrent();
+ if (CurrentCharacter == '+' || CurrentCharacter == '-')
+ {
+ TakeCurrent();
+ }
+ TakeUntil(c => !Char.IsDigit(c));
+ }
+ if (IsFloatTypeSuffix(CurrentCharacter))
+ {
+ TakeCurrent();
+ }
+ return Stay(EndSymbol(VBSymbolType.FloatingPointLiteral));
+ }
+
+ private StateResult HexLiteral()
+ {
+ AssertCurrent('&');
+ TakeCurrent();
+ Debug.Assert(Char.ToLowerInvariant(CurrentCharacter) == 'h');
+ TakeCurrent();
+ TakeUntil(c => !ParserHelpers.IsHexDigit(c));
+ TakeIntTypeSuffix();
+ return Stay(EndSymbol(VBSymbolType.IntegerLiteral));
+ }
+
+ private StateResult OctLiteral()
+ {
+ AssertCurrent('&');
+ TakeCurrent();
+ Debug.Assert(Char.ToLowerInvariant(CurrentCharacter) == 'o');
+ TakeCurrent();
+ TakeUntil(c => !VBHelpers.IsOctalDigit(c));
+ TakeIntTypeSuffix();
+ return Stay(EndSymbol(VBSymbolType.IntegerLiteral));
+ }
+
+ private VBSymbolType Operator()
+ {
+ char op = CurrentCharacter;
+ TakeCurrent();
+ VBSymbolType ret;
+ if (_operatorTable.TryGetValue(op, out ret))
+ {
+ return ret;
+ }
+ return VBSymbolType.Unknown;
+ }
+
+ private void TakeIntTypeSuffix()
+ {
+ // Take the "U" in US, UI, UL
+ if (Char.ToLowerInvariant(CurrentCharacter) == 'u')
+ {
+ TakeCurrent(); // Unsigned Prefix
+ }
+
+ // Take the S, I or L integer suffix
+ if (IsIntegerSuffix(CurrentCharacter))
+ {
+ TakeCurrent();
+ }
+ }
+
+ private static bool IsIntegerSuffix(char chr)
+ {
+ chr = Char.ToLowerInvariant(chr);
+ return chr == 's' || chr == 'i' || chr == 'l';
+ }
+
+ private StateResult CommentBody()
+ {
+ TakeUntil(ParserHelpers.IsNewLine);
+ return Stay(EndSymbol(VBSymbolType.Comment));
+ }
+
+ private StateResult Identifier()
+ {
+ bool isEscaped = false;
+ if (CurrentCharacter == '[')
+ {
+ TakeCurrent();
+ isEscaped = true;
+ }
+ TakeUntil(c => !ParserHelpers.IsIdentifierPart(c));
+
+ // If we're escaped, take the ']'
+ if (isEscaped)
+ {
+ if (CurrentCharacter == ']')
+ {
+ TakeCurrent();
+ }
+ }
+
+ // Check for Keywords and build the symbol
+ VBKeyword? keyword = VBKeywordDetector.GetKeyword(Buffer.ToString());
+ if (keyword == VBKeyword.Rem)
+ {
+ return CommentBody();
+ }
+
+ VBSymbol sym = new VBSymbol(CurrentStart, Buffer.ToString(), keyword == null ? VBSymbolType.Identifier : VBSymbolType.Keyword)
+ {
+ Keyword = keyword
+ };
+
+ StartSymbol();
+
+ return Stay(sym);
+ }
+
+ private bool IsIdentifierStart()
+ {
+ if (CurrentCharacter == '_')
+ {
+ // VB Spec §2.2:
+ // If an identifier begins with an underscore, it must contain at least one other valid identifier character to disambiguate it from a line continuation.
+ return ParserHelpers.IsIdentifierPart(Peek());
+ }
+ if (CurrentCharacter == '[')
+ {
+ return ParserHelpers.IsIdentifierPart(Peek());
+ }
+ return ParserHelpers.IsIdentifierStart(CurrentCharacter);
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Tokenizer/XmlHelpers.cs b/src/System.Web.Razor/Tokenizer/XmlHelpers.cs
new file mode 100644
index 00000000..6f4b9424
--- /dev/null
+++ b/src/System.Web.Razor/Tokenizer/XmlHelpers.cs
@@ -0,0 +1,47 @@
+namespace System.Web.Razor.Tokenizer
+{
+ internal static class XmlHelpers
+ {
+ public static bool IsXmlNameStartChar(char chr)
+ {
+ // [4] NameStartChar ::= ":" | [A-Z] | "_" | [a-z] | [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x2FF] | [#x370-#x37D] |
+ // [#x37F-#x1FFF] | [#x200C-#x200D] | [#x2070-#x218F] | [#x2C00-#x2FEF] | [#x3001-#xD7FF] |
+ // [#xF900-#xFDCF] | [#xFDF0-#xFFFD] | [#x10000-#xEFFFF]
+ // http://www.w3.org/TR/REC-xml/#NT-Name
+
+ return Char.IsLetter(chr) ||
+ chr == ':' ||
+ chr == '_' ||
+ IsInRange(chr, 0xC0, 0xD6) ||
+ IsInRange(chr, 0xD8, 0xF6) ||
+ IsInRange(chr, 0xF8, 0x2FF) ||
+ IsInRange(chr, 0x370, 0x37D) ||
+ IsInRange(chr, 0x37F, 0x1FFF) ||
+ IsInRange(chr, 0x200C, 0x200D) ||
+ IsInRange(chr, 0x2070, 0x218F) ||
+ IsInRange(chr, 0x2C00, 0x2FEF) ||
+ IsInRange(chr, 0x3001, 0xD7FF) ||
+ IsInRange(chr, 0xF900, 0xFDCF) ||
+ IsInRange(chr, 0xFDF0, 0xFFFD) ||
+ IsInRange(chr, 0x10000, 0xEFFFF);
+ }
+
+ public static bool IsXmlNameChar(char chr)
+ {
+ // [4a] NameChar ::= NameStartChar | "-" | "." | [0-9] | #xB7 | [#x0300-#x036F] | [#x203F-#x2040]
+ // http://www.w3.org/TR/REC-xml/#NT-Name
+ return Char.IsDigit(chr) ||
+ IsXmlNameStartChar(chr) ||
+ chr == '-' ||
+ chr == '.' ||
+ chr == '·' || // (U+00B7 is middle dot: ·)
+ IsInRange(chr, 0x0300, 0x036F) ||
+ IsInRange(chr, 0x203F, 0x2040);
+ }
+
+ public static bool IsInRange(char chr, int low, int high)
+ {
+ return ((int)chr >= low) && ((int)chr <= high);
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Utils/CharUtils.cs b/src/System.Web.Razor/Utils/CharUtils.cs
new file mode 100644
index 00000000..81efd33b
--- /dev/null
+++ b/src/System.Web.Razor/Utils/CharUtils.cs
@@ -0,0 +1,18 @@
+namespace System.Web.Razor.Utils
+{
+ internal static class CharUtils
+ {
+ internal static bool IsNonNewLineWhitespace(char c)
+ {
+ return Char.IsWhiteSpace(c) && !IsNewLine(c);
+ }
+
+ internal static bool IsNewLine(char c)
+ {
+ return c == 0x000d // Carriage return
+ || c == 0x000a // Linefeed
+ || c == 0x2028 // Line separator
+ || c == 0x2029; // Paragraph separator
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Utils/DisposableAction.cs b/src/System.Web.Razor/Utils/DisposableAction.cs
new file mode 100644
index 00000000..e1c4f6df
--- /dev/null
+++ b/src/System.Web.Razor/Utils/DisposableAction.cs
@@ -0,0 +1,31 @@
+namespace System.Web.Razor.Utils
+{
+ internal class DisposableAction : IDisposable
+ {
+ private Action _action;
+
+ public DisposableAction(Action action)
+ {
+ if (action == null)
+ {
+ throw new ArgumentNullException("action");
+ }
+ _action = action;
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ // If we were disposed by the finalizer it's because the user didn't use a "using" block, so don't do anything!
+ if (disposing)
+ {
+ _action();
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Utils/EnumUtil.cs b/src/System.Web.Razor/Utils/EnumUtil.cs
new file mode 100644
index 00000000..9ab07916
--- /dev/null
+++ b/src/System.Web.Razor/Utils/EnumUtil.cs
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+
+namespace System.Web.Razor.Utils
+{
+ internal static class EnumUtil
+ {
+ public static IEnumerable<T> Single<T>(T item)
+ {
+ yield return item;
+ }
+
+ public static IEnumerable<T> Prepend<T>(T item, IEnumerable<T> enumerable)
+ {
+ yield return item;
+ foreach (T t in enumerable)
+ {
+ yield return t;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.Razor/Utils/EnumeratorExtensions.cs b/src/System.Web.Razor/Utils/EnumeratorExtensions.cs
new file mode 100644
index 00000000..d7d4a545
--- /dev/null
+++ b/src/System.Web.Razor/Utils/EnumeratorExtensions.cs
@@ -0,0 +1,13 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace System.Web.Razor.Utils
+{
+ internal static class EnumeratorExtensions
+ {
+ public static IEnumerable<T> Flatten<T>(this IEnumerable<IEnumerable<T>> source)
+ {
+ return source.SelectMany(e => e);
+ }
+ }
+}
diff --git a/src/System.Web.Razor/VBRazorCodeLanguage.cs b/src/System.Web.Razor/VBRazorCodeLanguage.cs
new file mode 100644
index 00000000..ac19b384
--- /dev/null
+++ b/src/System.Web.Razor/VBRazorCodeLanguage.cs
@@ -0,0 +1,46 @@
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser;
+using Microsoft.VisualBasic;
+
+namespace System.Web.Razor
+{
+ /// <summary>
+ /// Defines the Visual Basic Code Language for Razor
+ /// </summary>
+ public class VBRazorCodeLanguage : RazorCodeLanguage
+ {
+ private const string VBLanguageName = "vb";
+
+ /// <summary>
+ /// Returns the name of the language: "vb"
+ /// </summary>
+ public override string LanguageName
+ {
+ get { return VBLanguageName; }
+ }
+
+ /// <summary>
+ /// Returns the type of the CodeDOM provider for this language
+ /// </summary>
+ public override Type CodeDomProviderType
+ {
+ get { return typeof(VBCodeProvider); }
+ }
+
+ /// <summary>
+ /// Constructs a new instance of the code parser for this language
+ /// </summary>
+ public override ParserBase CreateCodeParser()
+ {
+ return new VBCodeParser();
+ }
+
+ /// <summary>
+ /// Constructs a new instance of the code generator for this language with the specified settings
+ /// </summary>
+ public override RazorCodeGenerator CreateCodeGenerator(string className, string rootNamespaceName, string sourceFileName, RazorEngineHost host)
+ {
+ return new VBRazorCodeGenerator(className, rootNamespaceName, sourceFileName, host);
+ }
+ }
+}
diff --git a/src/System.Web.WebPages.Administration/Default.cshtml b/src/System.Web.WebPages.Administration/Default.cshtml
new file mode 100644
index 00000000..eaf27776
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/Default.cshtml
@@ -0,0 +1,39 @@
+@* Generator: WebPage *@
+
+@using System.Web.WebPages.Administration.PackageManager;
+@{
+ Page.Title = AdminResources.Modules;
+ var adminModules = from p in SiteAdmin.Modules
+ where !p.StartPageVirtualPath.Equals(SiteAdmin.AdminVirtualPath, StringComparison.OrdinalIgnoreCase)
+ orderby p.DisplayName
+ select p;
+}
+
+@if (!adminModules.Any() && !PackageManagerModule.Available) {
+ <h3>@AdminResources.NoAdminModulesInstalled</h3>
+}
+else if (PackageManagerModule.Available && !adminModules.Any()) {
+ // If no other module is available, take the user directly to the package manager
+ Response.Redirect(Href("packages"));
+ return;
+}
+else {
+ <ul class="modules">
+ @if (PackageManagerModule.Available) {
+ <li>
+ <a href="@Href("packages")" title="@PackageManagerModule.ModuleName"><strong>@PackageManagerModule.ModuleName</strong></a>
+ <div class="description">@PackageManagerModule.ModuleDescription</div>
+ </li>
+ }
+ @if (adminModules.Any()) {
+ foreach (var module in adminModules) {
+ <li>
+ <a href="@Href(module.StartPageVirtualPath)"><strong>@module.DisplayName</strong></a>
+ <div class="description">
+ @module.Description
+ </div>
+ </li>
+ }
+ }
+ </ul>
+} \ No newline at end of file
diff --git a/src/System.Web.WebPages.Administration/Default.generated.cs b/src/System.Web.WebPages.Administration/Default.generated.cs
new file mode 100644
index 00000000..60cdd2c3
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/Default.generated.cs
@@ -0,0 +1,149 @@
+#pragma warning disable 1591
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.225
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace System.Web.WebPages.Administration
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+ using System.Net;
+ using System.Web;
+ using System.Web.Helpers;
+ using System.Web.Security;
+ using System.Web.UI;
+ using System.Web.WebPages;
+ using System.Web.WebPages.Html;
+ using System.Web.WebPages.Administration.PackageManager;
+
+ [System.Web.WebPages.PageVirtualPathAttribute("~/Default.cshtml")]
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorSingleFileGenerator", "1.0.0.0")]
+ public class Default_cshtml : System.Web.WebPages.WebPage
+ {
+#line hidden
+
+ // Resolve package relative syntax
+ // Also, if it comes from a static embedded resource, change the path accordingly
+ public override string Href(string virtualPath, params object[] pathParts) {
+ virtualPath = ApplicationPart.ProcessVirtualPath(GetType().Assembly, VirtualPath, virtualPath);
+ return base.Href(virtualPath, pathParts);
+ }
+ public Default_cshtml()
+ {
+ }
+ protected System.Web.HttpApplication ApplicationInstance
+ {
+ get
+ {
+ return ((System.Web.HttpApplication)(Context.ApplicationInstance));
+ }
+ }
+ public override void Execute()
+ {
+
+
+WriteLiteral("\r\n\r\n");
+
+
+
+
+ Page.Title = AdminResources.Modules;
+ var adminModules = from p in SiteAdmin.Modules
+ where !p.StartPageVirtualPath.Equals(SiteAdmin.AdminVirtualPath, StringComparison.OrdinalIgnoreCase)
+ orderby p.DisplayName
+ select p;
+
+
+WriteLiteral("\r\n");
+
+
+ if (!adminModules.Any() && !PackageManagerModule.Available) {
+
+WriteLiteral(" <h3>");
+
+
+ Write(AdminResources.NoAdminModulesInstalled);
+
+WriteLiteral("</h3>\r\n");
+
+
+}
+else if (PackageManagerModule.Available && !adminModules.Any()) {
+ // If no other module is available, take the user directly to the package manager
+ Response.Redirect(Href("packages"));
+ return;
+}
+else {
+
+WriteLiteral(" <ul class=\"modules\">\r\n");
+
+
+ if (PackageManagerModule.Available) {
+
+WriteLiteral(" <li> \r\n <a href=\"");
+
+
+ Write(Href("packages"));
+
+WriteLiteral("\" title=\"");
+
+
+ Write(PackageManagerModule.ModuleName);
+
+WriteLiteral("\"><strong>");
+
+
+ Write(PackageManagerModule.ModuleName);
+
+WriteLiteral("</strong></a>\r\n <div class=\"description\">");
+
+
+ Write(PackageManagerModule.ModuleDescription);
+
+WriteLiteral("</div>\r\n </li>\r\n");
+
+
+ }
+
+
+ if (adminModules.Any()) {
+ foreach (var module in adminModules) {
+
+WriteLiteral(" <li>\r\n <a href=\"");
+
+
+ Write(Href(module.StartPageVirtualPath));
+
+WriteLiteral("\"><strong>");
+
+
+ Write(module.DisplayName);
+
+WriteLiteral("</strong></a>\r\n <div class=\"description\">\r\n ");
+
+
+ Write(module.Description);
+
+WriteLiteral("\r\n </div>\r\n </li>\r\n");
+
+
+ }
+ }
+
+WriteLiteral(" </ul>\r\n");
+
+
+}
+
+ }
+ }
+}
+#pragma warning restore 1591
diff --git a/src/System.Web.WebPages.Administration/EnableInstructions.cshtml b/src/System.Web.WebPages.Administration/EnableInstructions.cshtml
new file mode 100644
index 00000000..ca033705
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/EnableInstructions.cshtml
@@ -0,0 +1,21 @@
+@* Generator: WebPage *@
+
+@using System.Globalization;
+
+@{
+ Page.Title = AdminResources.SecurityTitle;
+
+ if(AdminSecurity.HasAdminPassword()) {
+ SiteAdmin.RedirectToHome(Response);
+ return;
+ }
+
+ string url = SiteAdmin.GetRedirectUrl(SiteAdmin.AdminVirtualPath);
+}
+
+@Html.Raw(AdminResources.EnableInstructions)
+<br />
+<p>
+ @Html.Raw(String.Format(CultureInfo.CurrentCulture, AdminResources.ContinueAfterEnableText, Html.Encode(Href(url))))
+</p>
+
diff --git a/src/System.Web.WebPages.Administration/EnableInstructions.generated.cs b/src/System.Web.WebPages.Administration/EnableInstructions.generated.cs
new file mode 100644
index 00000000..7d240422
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/EnableInstructions.generated.cs
@@ -0,0 +1,86 @@
+#pragma warning disable 1591
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.225
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace System.Web.WebPages.Administration
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+ using System.Net;
+ using System.Web;
+ using System.Web.Helpers;
+ using System.Web.Security;
+ using System.Web.UI;
+ using System.Web.WebPages;
+ using System.Web.WebPages.Html;
+ using System.Globalization;
+
+ [System.Web.WebPages.PageVirtualPathAttribute("~/EnableInstructions.cshtml")]
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorSingleFileGenerator", "1.0.0.0")]
+ public class EnableInstructions_cshtml : System.Web.WebPages.WebPage
+ {
+#line hidden
+
+ // Resolve package relative syntax
+ // Also, if it comes from a static embedded resource, change the path accordingly
+ public override string Href(string virtualPath, params object[] pathParts) {
+ virtualPath = ApplicationPart.ProcessVirtualPath(GetType().Assembly, VirtualPath, virtualPath);
+ return base.Href(virtualPath, pathParts);
+ }
+ public EnableInstructions_cshtml()
+ {
+ }
+ protected System.Web.HttpApplication ApplicationInstance
+ {
+ get
+ {
+ return ((System.Web.HttpApplication)(Context.ApplicationInstance));
+ }
+ }
+ public override void Execute()
+ {
+
+
+WriteLiteral("\r\n\r\n");
+
+
+WriteLiteral("\r\n");
+
+
+
+ Page.Title = AdminResources.SecurityTitle;
+
+ if(AdminSecurity.HasAdminPassword()) {
+ SiteAdmin.RedirectToHome(Response);
+ return;
+ }
+
+ string url = SiteAdmin.GetRedirectUrl(SiteAdmin.AdminVirtualPath);
+
+
+WriteLiteral("\r\n");
+
+
+Write(Html.Raw(AdminResources.EnableInstructions));
+
+WriteLiteral("\r\n<br />\r\n<p>\r\n ");
+
+
+Write(Html.Raw(String.Format(CultureInfo.CurrentCulture, AdminResources.ContinueAfterEnableText, Html.Encode(Href(url)))));
+
+WriteLiteral("\r\n</p>\r\n\r\n");
+
+
+ }
+ }
+}
+#pragma warning restore 1591
diff --git a/src/System.Web.WebPages.Administration/Framework/AdminSecurity.cs b/src/System.Web.WebPages.Administration/Framework/AdminSecurity.cs
new file mode 100644
index 00000000..2525e683
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/Framework/AdminSecurity.cs
@@ -0,0 +1,258 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Web.Helpers;
+using System.Web.Hosting;
+using System.Web.Security;
+
+namespace System.Web.WebPages.Administration
+{
+ internal static class AdminSecurity
+ {
+ private const string AuthCookieName = ".ASPXADMINAUTH";
+ private const string AdminUserNameToken = "ADMIN";
+
+ // Bug941370: Renamed the file to .config to prevent IIS6 from serving this files.
+ internal static readonly string AdminPasswordFile = VirtualPathUtility.Combine(SiteAdmin.AdminSettingsFolder, "Password.config");
+ internal static readonly string TemporaryPasswordFile = VirtualPathUtility.Combine(SiteAdmin.AdminSettingsFolder, "_Password.config");
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We catch all exceptions to prevent any decryption failure results from being exposed.")]
+ internal static bool IsAuthenticated(HttpRequestBase request)
+ {
+ HttpCookie authCookie = request.Cookies[AuthCookieName];
+
+ // Not authenticated if there is no cookie
+ if (authCookie == null)
+ {
+ return false;
+ }
+
+ try
+ {
+ return IsValidAuthCookie(authCookie);
+ }
+ catch
+ {
+ // If decryption fails, it may be a bad cookie
+ return false;
+ }
+ }
+
+ private static bool IsValidAuthCookie(HttpCookie authCookie)
+ {
+ // Decrypt the cookie and check the expired flag
+ FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(authCookie.Value);
+
+ // Ensure that the ticket hasn't expired and that the custom UserData string is our AdminUserNameToken
+ return !ticket.Expired && ticket.UserData != null && ticket.UserData.Equals(AdminUserNameToken);
+ }
+
+ internal static void SetAuthCookie(HttpResponseBase response)
+ {
+ // Get an auth admin cookie
+ HttpCookie cookie = GetAuthCookie();
+
+ // Set the name to our auth cookie name
+ cookie.Name = AuthCookieName;
+
+ // Add it to the response
+ response.Cookies.Add(cookie);
+ }
+
+ internal static HttpCookie GetAuthCookie()
+ {
+ // Create a new forms auth ticket for the admin section
+ // Add the admin user name as user data so that we distinguish between a regular
+ // ticket issued by ASP.NET's auth system versus our own
+ var ticket = new FormsAuthenticationTicket(2,
+ AdminUserNameToken,
+ DateTime.Now,
+ DateTime.Now.Add(FormsAuthentication.Timeout),
+ false,
+ AdminUserNameToken,
+ FormsAuthentication.FormsCookiePath);
+
+ // Encrypt the ticket and create the cookie
+ string encryptedValue = FormsAuthentication.Encrypt(ticket);
+ var cookie = new HttpCookie(AuthCookieName, encryptedValue);
+ cookie.HttpOnly = true;
+ cookie.Path = ticket.CookiePath;
+
+ return cookie;
+ }
+
+ internal static void DeleteAuthCookie(HttpResponseBase response)
+ {
+ // Expire the cookie
+ var cookie = new HttpCookie(AuthCookieName);
+ cookie.Expires = DateTime.Now.AddDays(-1);
+ response.Cookies.Add(cookie);
+ }
+
+ internal static bool HasAdminPassword()
+ {
+ return HasAdminPassword(HostingEnvironment.VirtualPathProvider);
+ }
+
+ internal static bool HasAdminPassword(VirtualPathProvider vpp)
+ {
+ // REVIEW: Do we need to check for content as well?
+ return vpp.FileExists(AdminPasswordFile);
+ }
+
+ internal static bool HasTemporaryPassword()
+ {
+ return HasTemporaryPassword(HostingEnvironment.VirtualPathProvider);
+ }
+
+ internal static bool HasTemporaryPassword(VirtualPathProvider vpp)
+ {
+ // REVIEW: Do we need to check for content as well?
+ return vpp.FileExists(TemporaryPasswordFile);
+ }
+
+ internal static bool SaveTemporaryPassword(string password)
+ {
+ // When saving the admin password we store it in a dummy file so that we don't enable to the admin UI by default
+ return SaveTemporaryPassword(password, GetTemporaryPasswordFileStream);
+ }
+
+ private static Stream GetTemporaryPasswordFileStream()
+ {
+ // Get the password directory and file name
+ string passwordFilePath = HostingEnvironment.MapPath(TemporaryPasswordFile);
+ string passwordFileDir = Path.GetDirectoryName(passwordFilePath);
+
+ // Ensure password directory exists
+ Directory.CreateDirectory(passwordFileDir);
+
+ // Return the stream
+ return File.OpenWrite(passwordFilePath);
+ }
+
+ internal static bool SaveTemporaryPassword(string password, Func<Stream> getPasswordFileStream)
+ {
+ Stream stream = null;
+ try
+ {
+ // Get the password file stream
+ stream = getPasswordFileStream();
+ }
+ catch (UnauthorizedAccessException)
+ {
+ // The user doesn't have write access to App_Data or the site root
+ return false;
+ }
+
+ using (var writer = new StreamWriter(stream))
+ {
+ // Write the salty password
+ writer.WriteLine(Crypto.HashPassword(password));
+ }
+
+ return true;
+ }
+
+ internal static bool CheckPassword(string password)
+ {
+ return CheckPassword(password, () =>
+ {
+ VirtualFile passwordFile = HostingEnvironment.VirtualPathProvider.GetFile(AdminPasswordFile);
+ Debug.Assert(passwordFile != null, "password file should not be null");
+ return passwordFile.Open();
+ });
+ }
+
+ internal static bool CheckPassword(string password, Func<Stream> getPasswordFileStream)
+ {
+ string saltyPassword = null;
+
+ Stream stream = getPasswordFileStream();
+ using (StreamReader reader = new StreamReader(stream))
+ {
+ // Read the salted password
+ saltyPassword = reader.ReadLine();
+ }
+
+ return Crypto.VerifyHashedPassword(saltyPassword, password);
+ }
+
+ /// <summary>
+ /// Ensure that the current request is authorized.
+ /// If the request is authenticated, then we skip all other checks.
+ /// </summary>
+ internal static void Authorize(StartPage page)
+ {
+ Authorize(page, HostingEnvironment.VirtualPathProvider, VirtualPathUtility.ToAppRelative);
+ }
+
+ internal static void Authorize(StartPage page, VirtualPathProvider vpp, Func<string, string> makeAppRelative)
+ {
+ if (!IsAuthenticated(page.Request))
+ {
+ if (HasAdminPassword(vpp))
+ {
+ // If there is a password file (Password.config) then we redirect to the login page
+ RedirectSafe(page, SiteAdmin.LoginVirtualPath, makeAppRelative);
+ }
+ else if (HasTemporaryPassword(vpp))
+ {
+ // Dev 10 941521: Admin: Pass through returnurl into page that tells the user to rename _password.config
+ // If there is a disabled password file (_Password.config) then we redirect to the instructions page
+ RedirectSafe(page, SiteAdmin.EnableInstructionsVirtualPath, makeAppRelative);
+ }
+ else
+ {
+ // The user hasn't done anything so redirect to the register page.
+ RedirectSafe(page, SiteAdmin.RegisterVirtualPath, makeAppRelative);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Doesn't do a redirect if the requesting page is itself the same as the virtual path.
+ /// We need to do this since it is called from the _pagestart.cshtml which always runs.
+ /// </summary>
+ private static void RedirectSafe(StartPage page, string virtualPath, Func<string, string> makeAppRelative)
+ {
+ // Make sure we get the virtual path
+ virtualPath = SiteAdmin.GetVirtualPath(virtualPath);
+
+ if (!IsRequestingPage(page, virtualPath))
+ {
+ // Append the redirect url querystring
+ virtualPath = SiteAdmin.GetRedirectUrl(page.Request, virtualPath, makeAppRelative);
+
+ page.Context.Response.Redirect(virtualPath);
+ }
+ }
+
+ /// <summary>
+ /// Determines if the specified virtualPath is being requested. We do this by walking the page hierarchy
+ /// and comparing the virtualPath with the child page's VirtualPath property (which in the case of ApplicationParts are compiled into the assembly
+ /// using the PageVirtualPath attribute).
+ /// </summary>
+ internal static bool IsRequestingPage(this StartPage page, string virtualPath)
+ {
+ WebPageRenderingBase webPage = GetRootPage(page);
+
+ return webPage.VirtualPath.Equals(virtualPath, StringComparison.OrdinalIgnoreCase);
+ }
+
+ /// <summary>
+ /// Walks the page hierarcy to find the requested page.
+ /// </summary>
+ private static WebPageRenderingBase GetRootPage(StartPage page)
+ {
+ WebPageRenderingBase currentPage = null;
+ while (page != null)
+ {
+ currentPage = page.ChildPage;
+ page = currentPage as StartPage;
+ }
+
+ Debug.Assert(currentPage != null, "Should never be null");
+ return currentPage;
+ }
+ }
+}
diff --git a/src/System.Web.WebPages.Administration/Framework/PreApplicationStartCode.cs b/src/System.Web.WebPages.Administration/Framework/PreApplicationStartCode.cs
new file mode 100644
index 00000000..474138fe
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/Framework/PreApplicationStartCode.cs
@@ -0,0 +1,32 @@
+using System.ComponentModel;
+
+namespace System.Web.WebPages.Administration
+{
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static class PreApplicationStartCode
+ {
+ // NOTE: Do not add public fields, methods, or other members to this class.
+ // This class does not show up in Intellisense so members on it will not be
+ // discoverable by users. Place new members on more appropriate classes that
+ // relate to the public API (for example, a LoginUrl property should go on a
+ // membership-related class).
+
+ private static bool _startWasCalled;
+
+ public static void Start()
+ {
+ // Even though ASP.NET will only call each PreAppStart once, we sometimes internally call one PreAppStart from
+ // another PreAppStart to ensure that things get initialized in the right order. ASP.NET does not guarantee the
+ // order so we have to guard against multiple calls.
+ // All Start calls are made on same thread, so no lock needed here.
+ if (_startWasCalled)
+ {
+ return;
+ }
+ _startWasCalled = true;
+
+ // Register the admin module
+ SiteAdmin.RegisterAdminModule();
+ }
+ }
+}
diff --git a/src/System.Web.WebPages.Administration/Framework/SiteAdmin.cs b/src/System.Web.WebPages.Administration/Framework/SiteAdmin.cs
new file mode 100644
index 00000000..1cc568c2
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/Framework/SiteAdmin.cs
@@ -0,0 +1,230 @@
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Configuration;
+using System.Diagnostics;
+using System.Globalization;
+
+namespace System.Web.WebPages.Administration
+{
+ public class SiteAdmin
+ {
+ internal const string DefaultAdminVirtualPath = "~/_Admin/";
+ internal const string AdminSettingsFolder = "~/App_Data/Admin/";
+
+ // Configuration settings
+ private const string AdminVirtualPathAppSettingsKey = "asp:AdminFolderVirtualPath";
+ private const string AdminEnabledAppSettingsKey = "asp:AdminManagerEnabled";
+
+ private const string ReturnUrlQueryString = "ReturnUrl";
+
+ // Virtual paths excluded from security. These urls are the same as the ones in the PageVirtulPath attribute.
+ internal const string LoginVirtualPath = "~/Login.cshtml";
+ internal const string LogoutVirtualPath = "~/Logout.cshtml";
+ internal const string RegisterVirtualPath = "~/Register.cshtml";
+ internal const string EnableInstructionsVirtualPath = "~/EnableInstructions.cshtml";
+
+ private static ConcurrentDictionary<string, SiteAdmin> _adminModules = new ConcurrentDictionary<string, SiteAdmin>();
+
+ // These only needs to be computed once per app domain
+ private static readonly Lazy<bool?> _adminEnabled = new Lazy<bool?>(GetAdminEnabledSetting);
+ private static readonly Lazy<string> _adminVirtualPath = new Lazy<string>(GetDefaultVirtualPath);
+
+ private SiteAdmin(string startPageVirtualPath, string displayName, string description)
+ {
+ Debug.Assert(startPageVirtualPath != null, "startPageVirtualPath can't be null");
+ Debug.Assert(displayName != null, "displayName can't be null");
+
+ StartPageVirtualPath = startPageVirtualPath;
+ DisplayName = displayName;
+ Description = description;
+ }
+
+ public static string AdminVirtualPath
+ {
+ get { return _adminVirtualPath.Value; }
+ }
+
+ public string StartPageVirtualPath { get; private set; }
+
+ public string DisplayName { get; private set; }
+
+ public string Description { get; private set; }
+
+ internal static bool Available
+ {
+ get
+ {
+ HttpContext context = HttpContext.Current;
+ if (context != null && context.Request.IsLocal)
+ {
+ // Check the configuration setting if there is any
+ if (_adminEnabled.Value != null)
+ {
+ return _adminEnabled.Value.Value;
+ }
+
+ // There was no setting so localhost is enough to verify availability
+ return true;
+ }
+ // If we're not on localhost then nothing is available
+ return false;
+ }
+ }
+
+ public static IEnumerable<SiteAdmin> Modules
+ {
+ get { return _adminModules.Values; }
+ }
+
+ internal static void RegisterAdminModule()
+ {
+ // Add a admin module as an application module (precompiled)
+ ApplicationPart.Register(new ApplicationPart(typeof(SiteAdmin).Assembly, AdminVirtualPath));
+ }
+
+ private static bool? GetAdminEnabledSetting()
+ {
+ bool enabled;
+ if (Boolean.TryParse(ConfigurationManager.AppSettings[AdminEnabledAppSettingsKey], out enabled))
+ {
+ return enabled;
+ }
+
+ return null;
+ }
+
+ public static string GetVirtualPath(string virtualPath)
+ {
+ if (virtualPath == null)
+ {
+ throw new ArgumentNullException("virtualPath");
+ }
+
+ // Don't add the virtual path more than once
+ if (virtualPath.StartsWith(AdminVirtualPath, StringComparison.OrdinalIgnoreCase))
+ {
+ return virtualPath;
+ }
+
+ if (virtualPath.StartsWith("~/", StringComparison.OrdinalIgnoreCase))
+ {
+ virtualPath = virtualPath.Substring(2);
+ }
+
+ if (virtualPath.StartsWith("/", StringComparison.OrdinalIgnoreCase))
+ {
+ virtualPath = virtualPath.Substring(1);
+ }
+ return VirtualPathUtility.Combine(AdminVirtualPath, virtualPath);
+ }
+
+ public static void Register(string startPageVirtualPath, string displayName, string description)
+ {
+ if (startPageVirtualPath == null)
+ {
+ throw new ArgumentNullException("startPageVirtualPath");
+ }
+
+ if (displayName == null)
+ {
+ throw new ArgumentNullException("displayName");
+ }
+
+ // Get the virtual path relative to the admin virtual path
+ string virtualPath = GetVirtualPath(startPageVirtualPath);
+
+ // Register that under the admin root
+ Register(new SiteAdmin(virtualPath, displayName, description));
+ }
+
+ private static string GetDefaultVirtualPath()
+ {
+ string virtualPath = ConfigurationManager.AppSettings[AdminVirtualPathAppSettingsKey];
+ if (String.IsNullOrEmpty(virtualPath))
+ {
+ virtualPath = DefaultAdminVirtualPath;
+ }
+
+ if (!virtualPath.EndsWith("/", StringComparison.OrdinalIgnoreCase))
+ {
+ virtualPath += "/";
+ }
+ return virtualPath;
+ }
+
+ internal static void RedirectToLogin(HttpResponseBase response)
+ {
+ response.Redirect(GetVirtualPath(LoginVirtualPath));
+ }
+
+ internal static void RedirectToRegister(HttpResponseBase response)
+ {
+ response.Redirect(GetVirtualPath(RegisterVirtualPath));
+ }
+
+ internal static void RedirectToHome(HttpResponseBase response)
+ {
+ response.Redirect(AdminVirtualPath);
+ }
+
+ internal static void Register(SiteAdmin module)
+ {
+ if (_adminModules.ContainsKey(module.StartPageVirtualPath))
+ {
+ throw new InvalidOperationException(
+ String.Format(CultureInfo.CurrentCulture,
+ AdminResources.ModuleAlreadyRegistered, module.StartPageVirtualPath));
+ }
+
+ // Add to the list of registered modules
+ _adminModules.TryAdd(module.StartPageVirtualPath, module);
+ }
+
+ internal static string GetReturnUrl(HttpRequestBase request)
+ {
+ // REVIEW: FormsAuthentication.GetReturlUrl also checks the form for the return url
+ // do we need that?
+ string returnUrl = request.QueryString[ReturnUrlQueryString];
+
+ // If the return url query string doesn't exist or is empty
+ // return the admin root virtual path
+ if (String.IsNullOrEmpty(returnUrl))
+ {
+ return null;
+ }
+
+ // REVIEW: FormsAuthentication.GetReturnUrl checks if the url is dangerous
+ // i.e it uses an internal helper in System.Web CrossSiteScriptingValidation.IsDangerousUrl.
+ // Should we copy that behavior?
+
+ if (!VirtualPathUtility.IsAppRelative(returnUrl))
+ {
+ // We only put app relative return urls in the query string (i.e starts with ~/)
+ throw new InvalidOperationException(AdminResources.InvalidReturnUrl);
+ }
+
+ return returnUrl;
+ }
+
+ internal static string GetRedirectUrl(string redirectUrl)
+ {
+ var request = new HttpRequestWrapper(HttpContext.Current.Request);
+ return GetRedirectUrl(request, redirectUrl, VirtualPathUtility.ToAppRelative);
+ }
+
+ internal static string GetRedirectUrl(HttpRequestBase request, string redirectUrl, Func<string, string> makeAppRelative)
+ {
+ // If there's already a return url then use it, otherwise the app relative url of the
+ // current request to redirect to after signing in
+ string returnUrl = GetReturnUrl(request) ?? makeAppRelative(request.RawUrl);
+
+ // Get the app relative path to the redirect url
+ redirectUrl = GetVirtualPath(redirectUrl);
+
+ // Get the current page with return url
+ redirectUrl += "?" + ReturnUrlQueryString + "=" + HttpUtility.UrlEncode(returnUrl);
+
+ return redirectUrl;
+ }
+ }
+}
diff --git a/src/System.Web.WebPages.Administration/Framework/packages/IPackagesSourceFile.cs b/src/System.Web.WebPages.Administration/Framework/packages/IPackagesSourceFile.cs
new file mode 100644
index 00000000..8c783d22
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/Framework/packages/IPackagesSourceFile.cs
@@ -0,0 +1,13 @@
+using System.Collections.Generic;
+
+namespace System.Web.WebPages.Administration.PackageManager
+{
+ public interface IPackagesSourceFile
+ {
+ bool Exists();
+
+ void WriteSources(IEnumerable<WebPackageSource> sources);
+
+ IEnumerable<WebPackageSource> ReadSources();
+ }
+}
diff --git a/src/System.Web.WebPages.Administration/Framework/packages/IWebProjectManager.cs b/src/System.Web.WebPages.Administration/Framework/packages/IWebProjectManager.cs
new file mode 100644
index 00000000..d5a0289d
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/Framework/packages/IWebProjectManager.cs
@@ -0,0 +1,74 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using NuGet;
+
+namespace System.Web.WebPages.Administration.PackageManager
+{
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public interface IWebProjectManager
+ {
+ /// <summary>
+ /// The repository where packages are installed.
+ /// </summary>
+ IPackageRepository LocalRepository { get; }
+
+ /// <summary>
+ /// Remote feed to fetch packages from.
+ /// </summary>
+ IPackageRepository SourceRepository { get; }
+
+ /// <summary>
+ /// Gets packages from the SourceRepository
+ /// </summary>
+ /// <param name="searchTerms">One or more terms separated by a whitespace used to filter packages.</param>
+ /// <param name="filterPreferred">Determines if packages are filtered by tag identifying packages for Asp.Net WebPages.</param>
+ IQueryable<IPackage> GetRemotePackages(string searchTerms, bool filterPreferred);
+
+ /// <summary>
+ /// Gets packages from the LocalRepository.
+ /// </summary>
+ /// <param name="searchTerms">One or more terms separated by a whitespace used to filter packages.</param>
+ IQueryable<IPackage> GetInstalledPackages(string searchTerms);
+
+ /// <summary>
+ /// Gets packages from the RemoteRepository that are updates to installed packages.
+ /// </summary>
+ /// <param name="searchTerms">One or more terms separated by a whitespace used to filter packages.</param>
+ /// <param name="filterPreferredPackages"></param>
+ IEnumerable<IPackage> GetPackagesWithUpdates(string searchTerms, bool filterPreferredPackages);
+
+ /// <summary>
+ /// Installs the package to the LocalRepository.
+ /// </summary>
+ /// <param name="package">The package to be installed.</param>
+ /// <param name="appDomain">The AppDomain that is used to determine binding redirects. If null, the current AppDomain would be used.</param>
+ /// <returns>A sequence of errors that occurred during the operation.</returns>
+ IEnumerable<string> InstallPackage(IPackage package, AppDomain appDomain);
+
+ /// <summary>
+ /// Updates the package in the LocalRepository.
+ /// </summary>
+ /// <param name="package">The package to be installed.</param>
+ /// <param name="appDomain">The AppDomain that is used to determine binding redirects. If null, the current AppDomain would be used.</param>
+ /// <returns>A sequence of errors that occurred during the operation.</returns>
+ IEnumerable<string> UpdatePackage(IPackage package, AppDomain appDomain);
+
+ /// <summary>
+ /// Removes the package from the LocalRepository.
+ /// </summary>
+ /// <returns>A sequence of errors that occurred during the operation.</returns>
+ IEnumerable<string> UninstallPackage(IPackage package, bool removeDependencies);
+
+ /// <summary>
+ /// Gets the latest version of the package.
+ /// </summary>
+ /// <param name="package">The package to find updates for.</param>
+ IPackage GetUpdate(IPackage package);
+
+ /// <summary>
+ /// Determines if any version of a package is installed in the LocalRepository.
+ /// </summary>
+ bool IsPackageInstalled(IPackage package);
+ }
+}
diff --git a/src/System.Web.WebPages.Administration/Framework/packages/PackageExtensions.cs b/src/System.Web.WebPages.Administration/Framework/packages/PackageExtensions.cs
new file mode 100644
index 00000000..3388b7cc
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/Framework/packages/PackageExtensions.cs
@@ -0,0 +1,13 @@
+using NuGet;
+
+namespace System.Web.WebPages.Administration.PackageManager
+{
+ internal static class PackageExtensions
+ {
+ public static string GetDisplayName(this IPackage package)
+ {
+ string name = String.IsNullOrEmpty(package.Title) ? package.Id : package.Title;
+ return String.Concat(name, ' ', package.Version);
+ }
+ }
+}
diff --git a/src/System.Web.WebPages.Administration/Framework/packages/PackageManagerModule.cs b/src/System.Web.WebPages.Administration/Framework/packages/PackageManagerModule.cs
new file mode 100644
index 00000000..38507dcc
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/Framework/packages/PackageManagerModule.cs
@@ -0,0 +1,165 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Web.Hosting;
+
+namespace System.Web.WebPages.Administration.PackageManager
+{
+ internal static class PackageManagerModule
+ {
+ private const string DefaultSourceUrl = @"http://go.microsoft.com/fwlink/?LinkID=226946";
+ private const string NuGetSourceUrl = @"http://go.microsoft.com/fwlink/?LinkID=226948";
+ internal static readonly string PackageSourceFilePath = VirtualPathUtility.Combine(SiteAdmin.AdminSettingsFolder, "PackageSources.config");
+ private static readonly object _sourceFileLock = new object();
+
+ private static readonly IEnumerable<WebPackageSource> _defaultSources = new[]
+ {
+ new WebPackageSource(name: PackageManagerResources.DefaultPackageSourceName, source: DefaultSourceUrl) { FilterPreferredPackages = true },
+ new WebPackageSource(name: PackageManagerResources.NuGetFeed, source: NuGetSourceUrl) { FilterPreferredPackages = false }
+ };
+
+ private static readonly PackageSourceFile _sourceFile = new PackageSourceFile(PackageSourceFilePath);
+ private static ISet<WebPackageSource> _packageSources;
+
+ public static IEnumerable<WebPackageSource> PackageSources
+ {
+ get
+ {
+ Debug.Assert(_packageSources != null, "InitFeedsFile must be called before Feeds can be accessed.");
+ return _packageSources.Any() ? _packageSources : _defaultSources;
+ }
+ }
+
+ /// <summary>
+ /// Gets the first available PackageSource
+ /// </summary>
+ public static WebPackageSource ActiveSource
+ {
+ get { return PackageSources.First(); }
+ }
+
+ public static IEnumerable<WebPackageSource> DefaultSources
+ {
+ get { return _defaultSources; }
+ }
+
+ public static string ModuleName
+ {
+ get { return PackageManagerResources.ModuleTitle; }
+ }
+
+ public static string ModuleDescription
+ {
+ get { return PackageManagerResources.ModuleDesc; }
+ }
+
+ internal static string SiteRoot
+ {
+ get { return HostingEnvironment.MapPath("~/"); }
+ }
+
+ internal static bool Available
+ {
+ get { return (HttpContext.Current != null) && HttpContext.Current.Request.IsLocal; }
+ }
+
+ public static bool InitPackageSourceFile()
+ {
+ return InitPackageSourceFile(_sourceFile, ref _packageSources);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Any exception that occurs indicates that the source file cannot be initialized.")]
+ public static bool InitPackageSourceFile(IPackagesSourceFile sourceFile, ref ISet<WebPackageSource> packageSources)
+ {
+ if (packageSources != null)
+ {
+ return true;
+ }
+ try
+ {
+ lock (_sourceFileLock)
+ {
+ // This method is invoked from Page_Start and ensures we have a feed file to read/write.
+ // The call needs to be guarded against multiple simultaneous requests.
+ if (!sourceFile.Exists())
+ {
+ packageSources = new HashSet<WebPackageSource>(_defaultSources);
+ sourceFile.WriteSources(_packageSources);
+ }
+ else
+ {
+ packageSources = new HashSet<WebPackageSource>(sourceFile.ReadSources());
+ }
+ }
+ }
+ catch
+ {
+ return false;
+ }
+ return true;
+ }
+
+ public static WebPackageSource GetSource(string sourceName)
+ {
+ Debug.Assert(_packageSources != null, "InitFeedsFile must be called before this method can be called.");
+ return GetSource(PackageSources, sourceName);
+ }
+
+ public static WebPackageSource GetSource(IEnumerable<WebPackageSource> packageSources, string sourceName)
+ {
+ lock (_sourceFileLock)
+ {
+ return packageSources.Where(source => source.Name.Equals(sourceName, StringComparison.OrdinalIgnoreCase)).FirstOrDefault();
+ }
+ }
+
+ public static bool AddPackageSource(string source, string name)
+ {
+ Debug.Assert(_packageSources != null, "InitFeedsFile must be called before this method can be called.");
+ name = String.IsNullOrEmpty(name) ? source.ToString() : name;
+ var packageSource = new WebPackageSource(source: source, name: name);
+ return AddPackageSource(_sourceFile, _packageSources, packageSource);
+ }
+
+ public static bool AddPackageSource(WebPackageSource packageSource)
+ {
+ Debug.Assert(!String.IsNullOrEmpty(packageSource.Name) && !String.IsNullOrEmpty(packageSource.Source));
+ return AddPackageSource(_sourceFile, _packageSources, packageSource);
+ }
+
+ public static bool AddPackageSource(IPackagesSourceFile sourceFile, ISet<WebPackageSource> packageSources, WebPackageSource packageSource)
+ {
+ if (GetSource(packageSources, packageSource.Name) != null)
+ {
+ return false;
+ }
+ lock (_sourceFileLock)
+ {
+ packageSources.Add(packageSource);
+ sourceFile.WriteSources(packageSources);
+ }
+ return true;
+ }
+
+ public static void RemovePackageSource(string sourceName)
+ {
+ Debug.Assert(_packageSources != null, "InitFeedsFile must be called before this method can be called.");
+ RemovePackageSource(_sourceFile, _packageSources, sourceName);
+ }
+
+ public static void RemovePackageSource(IPackagesSourceFile sourceFile, ISet<WebPackageSource> packageSources, string name)
+ {
+ var packageSource = GetSource(packageSources, name);
+ lock (_sourceFileLock)
+ {
+ if (packageSource == null)
+ {
+ return;
+ }
+ packageSources.Remove(packageSource);
+ sourceFile.WriteSources(packageSources);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.WebPages.Administration/Framework/packages/PackageSourceFile.cs b/src/System.Web.WebPages.Administration/Framework/packages/PackageSourceFile.cs
new file mode 100644
index 00000000..a2669ce3
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/Framework/packages/PackageSourceFile.cs
@@ -0,0 +1,103 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Web.Hosting;
+using System.Xml.Linq;
+
+namespace System.Web.WebPages.Administration.PackageManager
+{
+ /// <summary>
+ /// Provides an abstraction for reading and writing feeds to disk.
+ /// Calls to this file must be externally guarded against multiple threads.
+ /// </summary>
+ internal class PackageSourceFile : IPackagesSourceFile
+ {
+ private const string UrlAttribute = "url";
+ private const string NameAttribute = "displayname";
+ private const string FilterPreferredAttribute = "filterpreferred";
+ private readonly string _fileName;
+
+ public PackageSourceFile(string fileName)
+ {
+ _fileName = fileName;
+ }
+
+ public void WriteSources(IEnumerable<WebPackageSource> sources)
+ {
+ WriteFeeds(sources, () => GetStreamForWrite());
+ }
+
+ public IEnumerable<WebPackageSource> ReadSources()
+ {
+ return ReadFeeds(() => GetStreamForRead());
+ }
+
+ public bool Exists()
+ {
+ return HostingEnvironment.VirtualPathProvider.FileExists(_fileName);
+ }
+
+ internal static IEnumerable<WebPackageSource> ReadFeeds(Func<Stream> getStream)
+ {
+ using (var stream = getStream())
+ {
+ var document = XDocument.Load(stream);
+ var root = document.Root;
+
+ return (from element in root.Elements()
+ select ParsePackageSource(element)).ToList();
+ }
+ }
+
+ internal static void WriteFeeds(IEnumerable<WebPackageSource> sources, Func<Stream> getStream)
+ {
+ var xmlTree = from item in sources
+ select new XElement("source",
+ new XAttribute(UrlAttribute, item.Source),
+ new XAttribute(NameAttribute, item.Name),
+ new XAttribute(FilterPreferredAttribute, item.FilterPreferredPackages));
+
+ using (Stream stream = getStream())
+ {
+ new XDocument(new XElement("sources", xmlTree)).Save(stream);
+ }
+ }
+
+ internal static WebPackageSource ParsePackageSource(XElement element)
+ {
+ var urlAttribute = element.Attribute(UrlAttribute);
+ var nameAttribute = element.Attribute(NameAttribute);
+ var filterPreferredAttribute = element.Attribute(FilterPreferredAttribute);
+
+ // Throw if the file was tampered externally
+ if (urlAttribute == null || nameAttribute == null)
+ {
+ throw new FormatException();
+ }
+ Uri feedUrl;
+ if (!Uri.TryCreate(urlAttribute.Value, UriKind.Absolute, out feedUrl))
+ {
+ throw new FormatException();
+ }
+
+ return new WebPackageSource(feedUrl.OriginalString, nameAttribute.Value) { FilterPreferredPackages = filterPreferredAttribute != null && filterPreferredAttribute.Value.AsBool(false) };
+ }
+
+ private Stream GetStreamForRead()
+ {
+ var vpp = HostingEnvironment.VirtualPathProvider;
+ return vpp.GetFile(_fileName).Open();
+ }
+
+ private Stream GetStreamForWrite()
+ {
+ string mappedPath = HostingEnvironment.MapPath(_fileName);
+ if (!File.Exists(_fileName))
+ {
+ Directory.CreateDirectory(Path.GetDirectoryName(mappedPath));
+ return File.Create(mappedPath);
+ }
+ return File.Open(mappedPath, FileMode.Truncate);
+ }
+ }
+}
diff --git a/src/System.Web.WebPages.Administration/Framework/packages/PageUtils.cs b/src/System.Web.WebPages.Administration/Framework/packages/PageUtils.cs
new file mode 100644
index 00000000..388c403a
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/Framework/packages/PageUtils.cs
@@ -0,0 +1,80 @@
+using System.Collections.Generic;
+using System.Text;
+
+namespace System.Web.WebPages.Administration.PackageManager
+{
+ internal static class PageUtils
+ {
+ private const string WebPagesPreferredTag = " aspnetwebpages ";
+
+ internal static string GetPackagesHome()
+ {
+ return SiteAdmin.GetVirtualPath("~/packages");
+ }
+
+ internal static string GetPageVirtualPath(string page)
+ {
+ return SiteAdmin.GetVirtualPath("~/packages/" + page);
+ }
+
+ internal static WebPackageSource GetPackageSource(string name)
+ {
+ if (String.IsNullOrEmpty(name))
+ {
+ return PackageManagerModule.ActiveSource;
+ }
+ // If no source is found for the specified name, default to the ActiveSource
+ return PackageManagerModule.GetSource(name) ?? PackageManagerModule.ActiveSource;
+ }
+
+ internal static string GetFilterValue(HttpRequestBase request, string cookieName, string key)
+ {
+ var value = request.QueryString[key];
+ if (String.IsNullOrEmpty(value))
+ {
+ var cookie = request.Cookies[cookieName];
+ if (cookie != null)
+ {
+ value = cookie[key];
+ }
+ }
+ return value;
+ }
+
+ internal static void PersistFilter(HttpResponseBase response, string cookieName, IDictionary<string, string> filterItems)
+ {
+ var cookie = response.Cookies[cookieName];
+ if (cookie == null)
+ {
+ cookie = new HttpCookie(cookieName);
+ response.Cookies.Add(cookie);
+ }
+ foreach (var item in filterItems)
+ {
+ cookie[item.Key] = item.Value;
+ }
+ }
+
+ internal static bool IsValidLicenseUrl(Uri licenseUri)
+ {
+ return Uri.UriSchemeHttp.Equals(licenseUri.Scheme, StringComparison.OrdinalIgnoreCase) ||
+ Uri.UriSchemeHttps.Equals(licenseUri.Scheme, StringComparison.OrdinalIgnoreCase);
+ }
+
+ /// <summary>
+ /// Constructs a query string from an IDictionary
+ /// </summary>
+ internal static string BuildQueryString(IDictionary<string, string> parameters)
+ {
+ StringBuilder stringBuilder = new StringBuilder();
+ foreach (var param in parameters)
+ {
+ stringBuilder.Append(stringBuilder.Length == 0 ? '?' : '&')
+ .Append(HttpUtility.UrlEncode(param.Key))
+ .Append('=')
+ .Append(HttpUtility.UrlEncode(param.Value));
+ }
+ return stringBuilder.ToString();
+ }
+ }
+}
diff --git a/src/System.Web.WebPages.Administration/Framework/packages/RemoteAssembly.cs b/src/System.Web.WebPages.Administration/Framework/packages/RemoteAssembly.cs
new file mode 100644
index 00000000..3dace254
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/Framework/packages/RemoteAssembly.cs
@@ -0,0 +1,198 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Web.WebPages.Deployment;
+using NuGet.Runtime;
+
+namespace System.Web.WebPages.Administration.PackageManager
+{
+ // We need to make changes to NuGet.Core once we ship Beta so that this type is removed.
+ internal class RemoteAssembly : MarshalByRefObject, IAssembly, IEquatable<RemoteAssembly>, IComparable<RemoteAssembly>
+ {
+ private readonly List<IAssembly> _referencedAssemblies = new List<IAssembly>();
+
+ internal RemoteAssembly()
+ {
+ }
+
+ internal RemoteAssembly(string name, Version version, string publicKeyToken, string culture)
+ {
+ Name = name;
+ Version = version;
+ PublicKeyToken = publicKeyToken;
+ Culture = culture;
+ }
+
+ public string Name { get; private set; }
+
+ public Version Version { get; private set; }
+
+ public string PublicKeyToken { get; private set; }
+
+ public string Culture { get; private set; }
+
+ public IEnumerable<IAssembly> ReferencedAssemblies
+ {
+ get { return _referencedAssemblies; }
+ }
+
+ public void Load(string assemblyString, bool isPath)
+ {
+ Assembly assembly;
+
+ if (isPath)
+ {
+ assembly = Assembly.ReflectionOnlyLoadFrom(assemblyString);
+ }
+ else
+ {
+ assembly = Assembly.ReflectionOnlyLoad(assemblyString);
+ }
+
+ // Get the assembly name and set the properties on this object
+ CopyAssemblyProperties(assembly.GetName(), this);
+
+ // Do the same for referenced assemblies
+ foreach (AssemblyName referencedAssemblyName in assembly.GetReferencedAssemblies())
+ {
+ // Copy the properties to the referenced assembly
+ var referencedAssembly = new RemoteAssembly();
+ _referencedAssemblies.Add(CopyAssemblyProperties(referencedAssemblyName, referencedAssembly));
+ }
+ }
+
+ private static RemoteAssembly CopyAssemblyProperties(AssemblyName assemblyName, RemoteAssembly assembly)
+ {
+ assembly.Name = assemblyName.Name;
+ assembly.Version = assemblyName.Version;
+ assembly.PublicKeyToken = assemblyName.GetPublicKeyTokenString();
+ string culture = assemblyName.CultureInfo.ToString();
+
+ if (String.IsNullOrEmpty(culture))
+ {
+ assembly.Culture = "neutral";
+ }
+ else
+ {
+ assembly.Culture = culture;
+ }
+
+ return assembly;
+ }
+
+ internal static IAssembly LoadAssembly(string assemblyString, AppDomain domain, bool isPath)
+ {
+ if (domain != AppDomain.CurrentDomain)
+ {
+ var crossDomainAssembly = domain.CreateInstance<RemoteAssembly>();
+ crossDomainAssembly.Load(assemblyString, isPath);
+
+ return crossDomainAssembly;
+ }
+
+ var assembly = new RemoteAssembly();
+ assembly.Load(assemblyString, isPath);
+ return assembly;
+ }
+
+ public static IEnumerable<IAssembly> GetAssembliesForBindingRedirect(AppDomain appDomain, string binDirectoryPath)
+ {
+ return GetAssembliesForBindingRedirect(appDomain, binDirectoryPath, GetBinAssemblies);
+ }
+
+ internal static IEnumerable<IAssembly> GetAssembliesForBindingRedirect(AppDomain appDomain, string binDirectoryPath, Func<AppDomain, string, IEnumerable<IAssembly>> getBinAssemblies)
+ {
+ var binAssemblies = getBinAssemblies(appDomain, binDirectoryPath).ToList();
+ if (!binAssemblies.Any())
+ {
+ return binAssemblies;
+ }
+ var webPagesAssemblies = from asm in WebPagesDeployment.GetWebPagesAssemblies()
+ select LoadAssembly(asm.FullName, appDomain, isPath: false);
+ return webPagesAssemblies.Concat(binAssemblies)
+ .Distinct()
+ .ToList();
+ }
+
+ private static IEnumerable<IAssembly> GetBinAssemblies(AppDomain appDomain, string binDirectoryPath)
+ {
+ if (!Directory.Exists(binDirectoryPath))
+ {
+ yield break;
+ }
+ var extensions = new[] { "*.dll", "*.exe" };
+ foreach (var extension in extensions)
+ {
+ foreach (var path in Directory.EnumerateFiles(binDirectoryPath, extension))
+ {
+ IAssembly assembly = LoadAssemblyFromSafe(appDomain, path);
+ if (assembly != null)
+ {
+ yield return assembly;
+ }
+ }
+ }
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We're loading arbitrary binaries from the bin and some of them might not be native. Catch all to prevent this from throwing.")]
+ private static IAssembly LoadAssemblyFromSafe(AppDomain appDomain, string path)
+ {
+ try
+ {
+ return LoadAssembly(path, appDomain, isPath: true);
+ }
+ catch
+ {
+ // We don't want to throw from this method.
+ }
+ return null;
+ }
+
+ public override bool Equals(object obj)
+ {
+ var otherAssembly = obj as RemoteAssembly;
+ return otherAssembly != null && Equals(otherAssembly);
+ }
+
+ public bool Equals(RemoteAssembly other)
+ {
+ return CompareTo(other) == 0;
+ }
+
+ public override int GetHashCode()
+ {
+ return Name.GetHashCode();
+ }
+
+ public int CompareTo(RemoteAssembly other)
+ {
+ return Compare(this, other);
+ }
+
+ internal static int Compare(IAssembly a, IAssembly b)
+ {
+ if (b == null)
+ {
+ return 1;
+ }
+ var nameDiff = StringComparer.OrdinalIgnoreCase.Compare(a.Name, b.Name);
+ if (nameDiff != 0)
+ {
+ return nameDiff;
+ }
+ var versionDiff = a.Version.CompareTo(b.Version);
+ if (versionDiff != 0)
+ {
+ return versionDiff;
+ }
+ var publicKeyDiff = StringComparer.OrdinalIgnoreCase.Compare(a.PublicKeyToken, b.PublicKeyToken);
+ if (publicKeyDiff != 0)
+ {
+ return publicKeyDiff;
+ }
+ return StringComparer.OrdinalIgnoreCase.Compare(a.Culture, b.Culture);
+ }
+ }
+}
diff --git a/src/System.Web.WebPages.Administration/Framework/packages/WebPackageSource.cs b/src/System.Web.WebPages.Administration/Framework/packages/WebPackageSource.cs
new file mode 100644
index 00000000..82726254
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/Framework/packages/WebPackageSource.cs
@@ -0,0 +1,25 @@
+using NuGet;
+
+namespace System.Web.WebPages.Administration.PackageManager
+{
+ public class WebPackageSource : PackageSource
+ {
+ public WebPackageSource(string source, string name)
+ : base(source, name)
+ {
+ }
+
+ public bool FilterPreferredPackages { get; set; }
+
+ public override bool Equals(object obj)
+ {
+ WebPackageSource other = obj as WebPackageSource;
+ return base.Equals(other) && FilterPreferredPackages == other.FilterPreferredPackages;
+ }
+
+ public override int GetHashCode()
+ {
+ return base.GetHashCode() ^ (FilterPreferredPackages ? 1 : 0);
+ }
+ }
+}
diff --git a/src/System.Web.WebPages.Administration/Framework/packages/WebProjectManager.cs b/src/System.Web.WebPages.Administration/Framework/packages/WebProjectManager.cs
new file mode 100644
index 00000000..6ebd179f
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/Framework/packages/WebProjectManager.cs
@@ -0,0 +1,266 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using Microsoft.Internal.Web.Utils;
+using NuGet;
+using NuGet.Runtime;
+
+namespace System.Web.WebPages.Administration.PackageManager
+{
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public class WebProjectManager : IWebProjectManager
+ {
+ private const string WebPagesPreferredTag = " aspnetwebpages ";
+ private readonly IProjectManager _projectManager;
+ private readonly string _siteRoot;
+
+ public WebProjectManager(string remoteSource, string siteRoot)
+ {
+ if (String.IsNullOrEmpty(remoteSource))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "remoteSource");
+ }
+ if (String.IsNullOrEmpty(siteRoot))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "siteRoot");
+ }
+
+ _siteRoot = siteRoot;
+ string webRepositoryDirectory = GetWebRepositoryDirectory(siteRoot);
+ _projectManager = new ProjectManager(sourceRepository: PackageRepositoryFactory.Default.CreateRepository(remoteSource),
+ pathResolver: new DefaultPackagePathResolver(webRepositoryDirectory),
+ localRepository: PackageRepositoryFactory.Default.CreateRepository(webRepositoryDirectory),
+ project: new WebProjectSystem(siteRoot));
+ }
+
+ internal WebProjectManager(IProjectManager projectManager, string siteRoot)
+ {
+ if (String.IsNullOrEmpty(siteRoot))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "siteRoot");
+ }
+
+ if (projectManager == null)
+ {
+ throw new ArgumentNullException("projectManager");
+ }
+
+ _siteRoot = siteRoot;
+ _projectManager = projectManager;
+ }
+
+ public IPackageRepository LocalRepository
+ {
+ get { return _projectManager.LocalRepository; }
+ }
+
+ public IPackageRepository SourceRepository
+ {
+ get { return _projectManager.SourceRepository; }
+ }
+
+ internal bool DoNotAddBindingRedirects { get; set; }
+
+ [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#",
+ Justification = "We want to ensure we get server-side counts for the IQueryable which can only be performed before we collapse versions.")]
+ public virtual IQueryable<IPackage> GetRemotePackages(string searchTerms, bool filterPreferred)
+ {
+ var packages = GetPackages(SourceRepository, searchTerms);
+ if (filterPreferred)
+ {
+ packages = packages.Where(p => p.Tags.ToLower().Contains(WebPagesPreferredTag));
+ }
+
+ // Order by download count and Id to allow collapsing
+ return packages.OrderByDescending(p => p.DownloadCount)
+ .ThenBy(p => p.Id);
+ }
+
+ public IQueryable<IPackage> GetInstalledPackages(string searchTerms)
+ {
+ return GetPackages(LocalRepository, searchTerms);
+ }
+
+ public IEnumerable<IPackage> GetPackagesWithUpdates(string searchTerms, bool filterPreferredPackages)
+ {
+ var packagesToUpdate = GetPackages(LocalRepository, searchTerms);
+ if (filterPreferredPackages)
+ {
+ packagesToUpdate = packagesToUpdate.Where(p => p.Tags.ToLower().Contains(WebPagesPreferredTag));
+ }
+ return SourceRepository.GetUpdates(packagesToUpdate, includePrerelease: false).AsQueryable();
+ }
+
+ internal IEnumerable<string> InstallPackage(IPackage package)
+ {
+ return InstallPackage(package, AppDomain.CurrentDomain);
+ }
+
+ /// <summary>
+ /// Installs and adds a package reference to the project
+ /// </summary>
+ /// <returns>Warnings encountered when installing the package.</returns>
+ public IEnumerable<string> InstallPackage(IPackage package, AppDomain appDomain)
+ {
+ IEnumerable<string> result = PerformLoggedAction(() =>
+ {
+ _projectManager.AddPackageReference(package.Id, package.Version, ignoreDependencies: false, allowPrereleaseVersions: false);
+ AddBindingRedirects(appDomain);
+ });
+ return result;
+ }
+
+ internal IEnumerable<string> UpdatePackage(IPackage package)
+ {
+ return UpdatePackage(package, AppDomain.CurrentDomain);
+ }
+
+ /// <summary>
+ /// Updates a package reference. Installs the package to the App_Data repository if it does not already exist.
+ /// </summary>
+ /// <returns>Warnings encountered when updating the package.</returns>
+ public IEnumerable<string> UpdatePackage(IPackage package, AppDomain appDomain)
+ {
+ return PerformLoggedAction(() =>
+ {
+ _projectManager.UpdatePackageReference(package.Id, package.Version, updateDependencies: true, allowPrereleaseVersions: false);
+ AddBindingRedirects(appDomain);
+ });
+ }
+
+ /// <summary>
+ /// Removes a package reference and uninstalls the package
+ /// </summary>
+ /// <returns>Warnings encountered when uninstalling the package.</returns>
+ public IEnumerable<string> UninstallPackage(IPackage package, bool removeDependencies)
+ {
+ return PerformLoggedAction(() =>
+ {
+ _projectManager.RemovePackageReference(package.Id, forceRemove: false, removeDependencies: removeDependencies);
+ });
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "It seems more appropriate to deal with IPackages")]
+ public bool IsPackageInstalled(IPackage package)
+ {
+ return LocalRepository.Exists(package);
+ }
+
+ public IPackage GetUpdate(IPackage package)
+ {
+ return SourceRepository.GetUpdates(new[] { package }, includePrerelease: false).SingleOrDefault();
+ }
+
+ private void AddBindingRedirects(AppDomain appDomain)
+ {
+ if (DoNotAddBindingRedirects)
+ {
+ return;
+ }
+ // We can't use HttpRuntime.BinDirectory since there is no runtime when installing via WebMatrix.
+ var binDirectory = Path.Combine(_siteRoot, "bin");
+ var assemblies = RemoteAssembly.GetAssembliesForBindingRedirect(appDomain, binDirectory);
+ var bindingRedirects = BindingRedirectResolver.GetBindingRedirects(assemblies);
+
+ if (bindingRedirects.Any())
+ {
+ // NuGet ends up reading our web.config file regardless of if any bindingRedirects are needed.
+ var bindingRedirectManager = new BindingRedirectManager(_projectManager.Project, "web.config");
+ bindingRedirectManager.AddBindingRedirects(bindingRedirects);
+ }
+ }
+
+ private IEnumerable<string> PerformLoggedAction(Action action)
+ {
+ ErrorLogger logger = new ErrorLogger();
+ _projectManager.Logger = logger;
+ try
+ {
+ action();
+ }
+ finally
+ {
+ _projectManager.Logger = null;
+ }
+ return logger.Errors;
+ }
+
+ /// <remarks>
+ /// Ensure that some form of sorting is applied to the IQueryable before this method is invoked.
+ /// </remarks>
+ /// <returns>A sequence with the most recent version for each package.</returns>
+ public static IEnumerable<IPackage> CollapseVersions(IQueryable<IPackage> packages)
+ {
+ const int BufferSize = 30;
+ return packages.Where(package => package.IsLatestVersion)
+ .AsBufferedEnumerable(BufferSize)
+ .DistinctLast(PackageEqualityComparer.Id, PackageComparer.Version);
+ }
+
+ internal IEnumerable<IPackage> GetPackagesRequiringLicenseAcceptance(IPackage package)
+ {
+ return GetPackagesRequiringLicenseAcceptance(package, localRepository: LocalRepository, sourceRepository: SourceRepository);
+ }
+
+ internal static IEnumerable<IPackage> GetPackagesRequiringLicenseAcceptance(IPackage package, IPackageRepository localRepository, IPackageRepository sourceRepository)
+ {
+ var dependencies = GetPackageDependencies(package, localRepository, sourceRepository);
+
+ return from p in dependencies
+ where p.RequireLicenseAcceptance
+ select p;
+ }
+
+ private static IEnumerable<IPackage> GetPackageDependencies(IPackage package, IPackageRepository localRepository, IPackageRepository sourceRepository)
+ {
+ InstallWalker walker = new InstallWalker(localRepository: localRepository, sourceRepository: sourceRepository, logger: NullLogger.Instance,
+ ignoreDependencies: false, allowPrereleaseVersions: false);
+ IEnumerable<PackageOperation> operations = walker.ResolveOperations(package);
+
+ return from operation in operations
+ where operation.Action == PackageAction.Install
+ select operation.Package;
+ }
+
+ internal static IQueryable<IPackage> GetPackages(IPackageRepository repository, string searchTerm)
+ {
+ return GetPackages(repository.GetPackages(), searchTerm);
+ }
+
+ internal static IQueryable<IPackage> GetPackages(IQueryable<IPackage> packages, string searchTerm)
+ {
+ if (!String.IsNullOrEmpty(searchTerm))
+ {
+ searchTerm = searchTerm.Trim();
+ packages = packages.Find(searchTerm);
+ }
+ return packages;
+ }
+
+ internal static string GetWebRepositoryDirectory(string siteRoot)
+ {
+ return Path.Combine(siteRoot, "App_Data", "packages");
+ }
+
+ private class ErrorLogger : ILogger
+ {
+ private readonly IList<string> _errors = new List<string>();
+
+ public IEnumerable<string> Errors
+ {
+ get { return _errors; }
+ }
+
+ public void Log(MessageLevel level, string message, params object[] args)
+ {
+ if (level == MessageLevel.Warning)
+ {
+ _errors.Add(String.Format(CultureInfo.CurrentCulture, message, args));
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Web.WebPages.Administration/Framework/packages/WebProjectSystem.cs b/src/System.Web.WebPages.Administration/Framework/packages/WebProjectSystem.cs
new file mode 100644
index 00000000..8ac45c0a
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/Framework/packages/WebProjectSystem.cs
@@ -0,0 +1,225 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.Versioning;
+using System.Xml.Linq;
+using NuGet;
+
+namespace System.Web.WebPages.Administration.PackageManager
+{
+ public class WebProjectSystem : PhysicalFileSystem, IProjectSystem
+ {
+ private const string BinDir = "bin";
+ private const string AppCodeFolder = "App_Code";
+ private static readonly string[] _generatedFilesFolder = new[] { "Generated___Files" };
+ private static readonly string[] _sourceFileExtensions = new[] { ".cs", ".vb" };
+
+ /// <summary>
+ /// Keys taken from the 4.0 RedistList.
+ /// </summary>
+ private static readonly string[] _knownPublicKeys = new[] { "b03f5f7f11d50a3a", "b77a5c561934e089", "31bf3856ad364e35" };
+
+ public WebProjectSystem(string root)
+ : base(root)
+ {
+ }
+
+ public string ProjectName
+ {
+ get { return Root; }
+ }
+
+ public FrameworkName TargetFramework
+ {
+ get { return VersionUtility.DefaultTargetFramework; }
+ }
+
+ public void AddReference(string referencePath, Stream stream)
+ {
+ // Copy to bin by default
+ string referenceName = Path.GetFileName(referencePath);
+ string dest = GetFullPath(GetReferencePath(referenceName));
+
+ // Copy the reference over
+ AddFile(dest, stream);
+ }
+
+ public dynamic GetPropertyValue(string propertyName)
+ {
+ if (propertyName == null)
+ {
+ return null;
+ }
+
+ // Return empty string for the root namespace of this project.
+ if (propertyName.Equals("RootNamespace", StringComparison.OrdinalIgnoreCase))
+ {
+ return String.Empty;
+ }
+
+ return null;
+ }
+
+ public bool IsSupportedFile(string path)
+ {
+ return !Path.GetFileName(path).Equals("app.config", StringComparison.OrdinalIgnoreCase);
+ }
+
+ public bool ReferenceExists(string name)
+ {
+ string path = GetReferencePath(name);
+ return FileExists(path);
+ }
+
+ public void RemoveReference(string name)
+ {
+ DeleteFile(GetReferencePath(name));
+
+ // Delete the bin directory if this was the last reference
+ if (!GetFiles(BinDir).Any())
+ {
+ DeleteDirectory(BinDir);
+ }
+ }
+
+ public void AddFrameworkReference(string name)
+ {
+ // Before we add a framework assembly to web.config, verify that it exists in the GAC. This is important because a website would be completely unusable if the assembly reference
+ // does not exist and is added to web.config. Since the assembly name may be a partial name, We use the ResolveAssemblyReference task in Msbuild to identify a full name and if it is
+ // installed in the GAC.
+ var fullName = ResolvePartialAssemblyName(name);
+ if (fullName == null)
+ {
+ throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, PackageManagerResources.UnknownFrameworkReference, name));
+ }
+ AddReferencesToConfig(this, fullName);
+ }
+
+ public override IEnumerable<string> GetDirectories(string path)
+ {
+ if (IsUnderAppCode(path))
+ {
+ // There is an invisible folder called Generated___Files under app code that we want to exclude from our search
+ return base.GetDirectories(path).Except(_generatedFilesFolder, StringComparer.OrdinalIgnoreCase);
+ }
+ return base.GetDirectories(path);
+ }
+
+ public string ResolvePath(string path)
+ {
+ if (RequiresAppCodeRemapping(path))
+ {
+ path = Path.Combine(AppCodeFolder, path);
+ }
+ return path;
+ }
+
+ protected virtual string GetReferencePath(string name)
+ {
+ return Path.Combine(BinDir, name);
+ }
+
+ /// <summary>
+ /// Uses ResolveAssemblyReference to calculate a full name from a partial assembly name.
+ /// </summary>
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes",
+ Justification = "We never want to throw from this method. If Assembly.Load fails, the only message we want to display is that package could not be installed.")]
+ internal static string ResolvePartialAssemblyName(string name)
+ {
+ foreach (var key in _knownPublicKeys)
+ {
+ var assemblyFullName = String.Format(CultureInfo.InvariantCulture, "{0}, Version={1}, Culture=neutral, PublicKeyToken={2}",
+ name, VersionUtility.DefaultTargetFrameworkVersion, key);
+
+ try
+ {
+ Assembly.Load(assemblyFullName);
+ // Assembly.Load throws a FileNotFoundException if the assembly name cannot be resolved. If we managed to successfully locate the assembly, return it.
+ return assemblyFullName;
+ }
+ catch
+ {
+ // Do nothing. We don't want to throw from this method.
+ }
+ }
+ return null;
+ }
+
+ internal static void AddReferencesToConfig(IFileSystem fileSystem, string references)
+ {
+ var webConfigPath = Path.Combine(fileSystem.Root, "web.config");
+ XDocument document;
+ // Read the web.config file from the AppRoot if it exists.
+ if (fileSystem.FileExists(webConfigPath))
+ {
+ using (Stream stream = fileSystem.OpenFile(webConfigPath))
+ {
+ document = XDocument.Load(stream, LoadOptions.PreserveWhitespace);
+ }
+ }
+ else
+ {
+ document = new XDocument(new XElement("configuration"));
+ }
+
+ var assemblies = GetOrCreateChild(document.Root, "system.web/compilation/assemblies");
+
+ // Get the name of the existing references
+ // References are stored in the format <add assembly="System.Web.Abstractions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
+ bool existingAssembly = (from item in assemblies.Elements()
+ where !String.IsNullOrEmpty(item.GetOptionalAttributeValue("assembly"))
+ let assemblyName = new AssemblyName(item.Attribute("assembly").Value).Name
+ where String.Equals(assemblyName, references, StringComparison.OrdinalIgnoreCase)
+ select item).Any();
+
+ if (!existingAssembly)
+ {
+ assemblies.Add(new XElement("add", new XAttribute("assembly", references)));
+ SaveDocument(fileSystem, webConfigPath, document);
+ }
+ }
+
+ private static void SaveDocument(IFileSystem fileSystem, string webConfigPath, XDocument document)
+ {
+ using (MemoryStream stream = new MemoryStream())
+ {
+ document.Save(stream);
+ stream.Seek(0, SeekOrigin.Begin);
+ fileSystem.AddFile(webConfigPath, stream);
+ }
+ }
+
+ private static XElement GetOrCreateChild(XElement element, string childName)
+ {
+ foreach (var item in childName.Split('/'))
+ {
+ XElement child = element.Element(item);
+ if (child == null)
+ {
+ child = new XElement(item);
+ element.Add(child);
+ }
+ element = child;
+ }
+ return element;
+ }
+
+ private static bool RequiresAppCodeRemapping(string path)
+ {
+ return !IsUnderAppCode(path) && IsSourceFile(path);
+ }
+
+ private static bool IsUnderAppCode(string path)
+ {
+ return path.StartsWith(AppCodeFolder + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static bool IsSourceFile(string path)
+ {
+ return _sourceFileExtensions.Contains(Path.GetExtension(path), StringComparer.OrdinalIgnoreCase);
+ }
+ }
+}
diff --git a/src/System.Web.WebPages.Administration/Login.cshtml b/src/System.Web.WebPages.Administration/Login.cshtml
new file mode 100644
index 00000000..9ed1fbf2
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/Login.cshtml
@@ -0,0 +1,65 @@
+@* Generator: WebPage *@
+
+@using System.Globalization;
+@{
+ Page.Title = AdminResources.LoginTitle;
+
+ // No admin password has been registered so redirect
+ if(!AdminSecurity.HasAdminPassword()) {
+ SiteAdmin.RedirectToRegister(Response);
+ return;
+ }
+
+ if (IsPost) {
+ AntiForgery.Validate();
+ var password = Request.Form["password"];
+
+ if (AdminSecurity.CheckPassword(password)) {
+ // Get the return url
+ var returnUrl = SiteAdmin.GetReturnUrl(Request) ?? SiteAdmin.AdminVirtualPath;
+
+ // Set the admin auth cookie
+ AdminSecurity.SetAuthCookie(Response);
+
+ // Redirect to the return url
+ Response.Redirect(returnUrl);
+ }
+ else {
+ ModelState.AddError("password", AdminResources.Validation_PasswordIncorrect);
+ }
+ }
+}
+
+@section Head{
+ <script type="text/javascript">
+ function showForgotPasswordInfo(){
+ document.getElementById('forgotPasswordInfo').style.display = '';
+ }
+ </script>
+}
+
+@Html.ValidationSummary()
+<br />
+
+<form method="post" action="">
+ @AntiForgery.GetHtml()
+ <fieldset>
+ <ol>
+ <li class="password">
+ <label for="password">@AdminResources.Password:</label>
+ @Html.Password("password") @Html.ValidationMessage("password", "*")
+ </ol>
+ <p class="form-actions">
+ <input type="submit" value="@AdminResources.Login" />
+ </p>
+ </fieldset>
+ <p>
+ <a href="#" onclick="showForgotPasswordInfo(); return false;">@AdminResources.ForgotPassword</a>
+ </p>
+</form>
+<br />
+@{
+ var passwordFileLocation = AdminSecurity.AdminPasswordFile.TrimStart('~', '/');
+ var forgotPasswordHelp = String.Format(CultureInfo.CurrentCulture, AdminResources.AdminPasswordChangeInstructions, Html.Encode(passwordFileLocation));
+}
+<span id="forgotPasswordInfo" style="display: none">@Html.Raw(forgotPasswordHelp)</span> \ No newline at end of file
diff --git a/src/System.Web.WebPages.Administration/Login.generated.cs b/src/System.Web.WebPages.Administration/Login.generated.cs
new file mode 100644
index 00000000..7c118eb8
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/Login.generated.cs
@@ -0,0 +1,154 @@
+#pragma warning disable 1591
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.225
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace System.Web.WebPages.Administration
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+ using System.Net;
+ using System.Web;
+ using System.Web.Helpers;
+ using System.Web.Security;
+ using System.Web.UI;
+ using System.Web.WebPages;
+ using System.Web.WebPages.Html;
+ using System.Globalization;
+
+ [System.Web.WebPages.PageVirtualPathAttribute("~/Login.cshtml")]
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorSingleFileGenerator", "1.0.0.0")]
+ public class Login_cshtml : System.Web.WebPages.WebPage
+ {
+#line hidden
+
+ // Resolve package relative syntax
+ // Also, if it comes from a static embedded resource, change the path accordingly
+ public override string Href(string virtualPath, params object[] pathParts) {
+ virtualPath = ApplicationPart.ProcessVirtualPath(GetType().Assembly, VirtualPath, virtualPath);
+ return base.Href(virtualPath, pathParts);
+ }
+ public Login_cshtml()
+ {
+ }
+ protected System.Web.HttpApplication ApplicationInstance
+ {
+ get
+ {
+ return ((System.Web.HttpApplication)(Context.ApplicationInstance));
+ }
+ }
+ public override void Execute()
+ {
+
+
+WriteLiteral("\r\n\r\n");
+
+
+
+
+ Page.Title = AdminResources.LoginTitle;
+
+ // No admin password has been registered so redirect
+ if(!AdminSecurity.HasAdminPassword()) {
+ SiteAdmin.RedirectToRegister(Response);
+ return;
+ }
+
+ if (IsPost) {
+ AntiForgery.Validate();
+ var password = Request.Form["password"];
+
+ if (AdminSecurity.CheckPassword(password)) {
+ // Get the return url
+ var returnUrl = SiteAdmin.GetReturnUrl(Request) ?? SiteAdmin.AdminVirtualPath;
+
+ // Set the admin auth cookie
+ AdminSecurity.SetAuthCookie(Response);
+
+ // Redirect to the return url
+ Response.Redirect(returnUrl);
+ }
+ else {
+ ModelState.AddError("password", AdminResources.Validation_PasswordIncorrect);
+ }
+ }
+
+
+WriteLiteral("\r\n");
+
+
+DefineSection("Head", () => {
+
+WriteLiteral("\r\n <script type=\"text/javascript\">\r\n function showForgotPasswordInfo(){\r\n " +
+" document.getElementById(\'forgotPasswordInfo\').style.display = \'\';\r\n }\r\n" +
+" </script>\r\n");
+
+
+});
+
+WriteLiteral("\r\n\r\n");
+
+
+Write(Html.ValidationSummary());
+
+WriteLiteral("\r\n<br />\r\n\r\n<form method=\"post\" action=\"\">\r\n ");
+
+
+Write(AntiForgery.GetHtml());
+
+WriteLiteral("\r\n <fieldset>\r\n <ol>\r\n <li class=\"password\">\r\n <label for" +
+"=\"password\">");
+
+
+ Write(AdminResources.Password);
+
+WriteLiteral(":</label>\r\n ");
+
+
+ Write(Html.Password("password"));
+
+WriteLiteral(" ");
+
+
+ Write(Html.ValidationMessage("password", "*"));
+
+WriteLiteral("\r\n </ol>\r\n <p class=\"form-actions\">\r\n <input type=\"submit\" value=\"");
+
+
+ Write(AdminResources.Login);
+
+WriteLiteral("\" />\r\n </p>\r\n </fieldset>\r\n <p>\r\n <a href=\"#\" onclick=\"showForgot" +
+"PasswordInfo(); return false;\">");
+
+
+ Write(AdminResources.ForgotPassword);
+
+WriteLiteral("</a>\r\n </p>\r\n</form>\r\n<br />\r\n");
+
+
+
+ var passwordFileLocation = AdminSecurity.AdminPasswordFile.TrimStart('~', '/');
+ var forgotPasswordHelp = String.Format(CultureInfo.CurrentCulture, AdminResources.AdminPasswordChangeInstructions, Html.Encode(passwordFileLocation));
+
+
+WriteLiteral("<span id=\"forgotPasswordInfo\" style=\"display: none\">");
+
+
+ Write(Html.Raw(forgotPasswordHelp));
+
+WriteLiteral("</span>");
+
+
+ }
+ }
+}
+#pragma warning restore 1591
diff --git a/src/System.Web.WebPages.Administration/Logout.cshtml b/src/System.Web.WebPages.Administration/Logout.cshtml
new file mode 100644
index 00000000..ba1de0f7
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/Logout.cshtml
@@ -0,0 +1,9 @@
+@* Generator: WebPage *@
+
+@{
+ // Delete the admin auth cookie
+ AdminSecurity.DeleteAuthCookie(Response);
+
+ // Redirect home
+ SiteAdmin.RedirectToHome(Response);
+}
diff --git a/src/System.Web.WebPages.Administration/Logout.generated.cs b/src/System.Web.WebPages.Administration/Logout.generated.cs
new file mode 100644
index 00000000..d0f467f0
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/Logout.generated.cs
@@ -0,0 +1,67 @@
+#pragma warning disable 1591
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.225
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace System.Web.WebPages.Administration
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+ using System.Net;
+ using System.Web;
+ using System.Web.Helpers;
+ using System.Web.Security;
+ using System.Web.UI;
+ using System.Web.WebPages;
+ using System.Web.WebPages.Html;
+
+ [System.Web.WebPages.PageVirtualPathAttribute("~/Logout.cshtml")]
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorSingleFileGenerator", "1.0.0.0")]
+ public class Logout_cshtml : System.Web.WebPages.WebPage
+ {
+#line hidden
+
+ // Resolve package relative syntax
+ // Also, if it comes from a static embedded resource, change the path accordingly
+ public override string Href(string virtualPath, params object[] pathParts) {
+ virtualPath = ApplicationPart.ProcessVirtualPath(GetType().Assembly, VirtualPath, virtualPath);
+ return base.Href(virtualPath, pathParts);
+ }
+ public Logout_cshtml()
+ {
+ }
+ protected System.Web.HttpApplication ApplicationInstance
+ {
+ get
+ {
+ return ((System.Web.HttpApplication)(Context.ApplicationInstance));
+ }
+ }
+ public override void Execute()
+ {
+
+
+WriteLiteral("\r\n\r\n");
+
+
+
+ // Delete the admin auth cookie
+ AdminSecurity.DeleteAuthCookie(Response);
+
+ // Redirect home
+ SiteAdmin.RedirectToHome(Response);
+
+
+
+ }
+ }
+}
+#pragma warning restore 1591
diff --git a/src/System.Web.WebPages.Administration/Properties/AssemblyInfo.cs b/src/System.Web.WebPages.Administration/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..78745791
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/Properties/AssemblyInfo.cs
@@ -0,0 +1,13 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Web;
+using System.Web.WebPages.Administration;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+
+[assembly: AssemblyTitle("System.Web.WebPages.Administration")]
+[assembly: AssemblyDescription("")]
+[assembly: PreApplicationStartMethod(typeof(PreApplicationStartCode), "Start")]
+[assembly: InternalsVisibleTo("System.Web.WebPages.Administration.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
diff --git a/src/System.Web.WebPages.Administration/Register.cshtml b/src/System.Web.WebPages.Administration/Register.cshtml
new file mode 100644
index 00000000..9f3bc206
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/Register.cshtml
@@ -0,0 +1,66 @@
+@* Generator: WebPage *@
+
+@using System.Globalization;
+@{
+ Page.Title = AdminResources.RegisterTitle;
+ var adminPath = SiteAdmin.AdminVirtualPath.TrimStart('~');
+ Page.Desc = String.Format(CultureInfo.CurrentCulture, AdminResources.RegisterDesc, Html.Encode(adminPath));
+
+ // If the password is already set the redirect to login
+ if(AdminSecurity.HasAdminPassword()) {
+ SiteAdmin.RedirectToLogin(Response);
+ return;
+ }
+
+ if (IsPost) {
+ AntiForgery.Validate();
+
+ var password = Request.Form["password"];
+ var reenteredPassword = Request.Form["repassword"];
+ if (password.IsEmpty()) {
+ ModelState.AddError("password", AdminResources.Validation_PasswordRequired);
+ }
+ else if (password != reenteredPassword) {
+ ModelState.AddError("repassword", AdminResources.Validation_PasswordsDoNotMatch);
+ }
+
+ if (ModelState.IsValid) {
+ // Save the admin password
+ if(AdminSecurity.SaveTemporaryPassword(password)) {
+ // Get the return url
+ var returnUrl = SiteAdmin.GetReturnUrl(Request) ?? SiteAdmin.AdminVirtualPath;
+
+ // Redirect to the return url
+ Response.Redirect(returnUrl);
+ }
+ else {
+ // Add a validation error since creating the password.txt failed
+ ModelState.AddFormError(AdminResources.AdminModuleRequiresAccessToAppData);
+ }
+
+ }
+ }
+}
+
+<br/>
+
+@Html.ValidationSummary()
+
+<form method="post" action="">
+@AntiForgery.GetHtml()
+<fieldset>
+ <ol>
+ <li class="password">
+ <label for="password">@AdminResources.EnterPassword</label>
+ @Html.Password("password") @Html.ValidationMessage("password", "*")
+ </li>
+ <li class="password">
+ <label>@AdminResources.ReenterPassword</label>
+ @Html.Password("repassword") @Html.ValidationMessage("repassword", "*")
+ </li>
+ </ol>
+ <p class="form-actions">
+ <input type="submit" value="@AdminResources.CreatePassword" class="long-input" />
+ </p>
+</fieldset>
+</form>
diff --git a/src/System.Web.WebPages.Administration/Register.generated.cs b/src/System.Web.WebPages.Administration/Register.generated.cs
new file mode 100644
index 00000000..a2cec653
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/Register.generated.cs
@@ -0,0 +1,151 @@
+#pragma warning disable 1591
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.225
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace System.Web.WebPages.Administration
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+ using System.Net;
+ using System.Web;
+ using System.Web.Helpers;
+ using System.Web.Security;
+ using System.Web.UI;
+ using System.Web.WebPages;
+ using System.Web.WebPages.Html;
+ using System.Globalization;
+
+ [System.Web.WebPages.PageVirtualPathAttribute("~/Register.cshtml")]
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorSingleFileGenerator", "1.0.0.0")]
+ public class Register_cshtml : System.Web.WebPages.WebPage
+ {
+#line hidden
+
+ // Resolve package relative syntax
+ // Also, if it comes from a static embedded resource, change the path accordingly
+ public override string Href(string virtualPath, params object[] pathParts) {
+ virtualPath = ApplicationPart.ProcessVirtualPath(GetType().Assembly, VirtualPath, virtualPath);
+ return base.Href(virtualPath, pathParts);
+ }
+ public Register_cshtml()
+ {
+ }
+ protected System.Web.HttpApplication ApplicationInstance
+ {
+ get
+ {
+ return ((System.Web.HttpApplication)(Context.ApplicationInstance));
+ }
+ }
+ public override void Execute()
+ {
+
+
+WriteLiteral("\r\n\r\n");
+
+
+
+
+ Page.Title = AdminResources.RegisterTitle;
+ var adminPath = SiteAdmin.AdminVirtualPath.TrimStart('~');
+ Page.Desc = String.Format(CultureInfo.CurrentCulture, AdminResources.RegisterDesc, Html.Encode(adminPath));
+
+ // If the password is already set the redirect to login
+ if(AdminSecurity.HasAdminPassword()) {
+ SiteAdmin.RedirectToLogin(Response);
+ return;
+ }
+
+ if (IsPost) {
+ AntiForgery.Validate();
+
+ var password = Request.Form["password"];
+ var reenteredPassword = Request.Form["repassword"];
+ if (password.IsEmpty()) {
+ ModelState.AddError("password", AdminResources.Validation_PasswordRequired);
+ }
+ else if (password != reenteredPassword) {
+ ModelState.AddError("repassword", AdminResources.Validation_PasswordsDoNotMatch);
+ }
+
+ if (ModelState.IsValid) {
+ // Save the admin password
+ if(AdminSecurity.SaveTemporaryPassword(password)) {
+ // Get the return url
+ var returnUrl = SiteAdmin.GetReturnUrl(Request) ?? SiteAdmin.AdminVirtualPath;
+
+ // Redirect to the return url
+ Response.Redirect(returnUrl);
+ }
+ else {
+ // Add a validation error since creating the password.txt failed
+ ModelState.AddFormError(AdminResources.AdminModuleRequiresAccessToAppData);
+ }
+
+ }
+ }
+
+
+WriteLiteral("\r\n<br/>\r\n\r\n");
+
+
+Write(Html.ValidationSummary());
+
+WriteLiteral("\r\n\r\n<form method=\"post\" action=\"\">\r\n");
+
+
+Write(AntiForgery.GetHtml());
+
+WriteLiteral("\r\n<fieldset>\r\n <ol>\r\n <li class=\"password\">\r\n <label for=\"pa" +
+"ssword\">");
+
+
+ Write(AdminResources.EnterPassword);
+
+WriteLiteral("</label>\r\n ");
+
+
+ Write(Html.Password("password"));
+
+WriteLiteral(" ");
+
+
+ Write(Html.ValidationMessage("password", "*"));
+
+WriteLiteral("\r\n </li>\r\n <li class=\"password\">\r\n <label>");
+
+
+ Write(AdminResources.ReenterPassword);
+
+WriteLiteral("</label>\r\n ");
+
+
+ Write(Html.Password("repassword"));
+
+WriteLiteral(" ");
+
+
+ Write(Html.ValidationMessage("repassword", "*"));
+
+WriteLiteral("\r\n </li>\r\n </ol>\r\n <p class=\"form-actions\">\r\n <input type=\"su" +
+"bmit\" value=\"");
+
+
+ Write(AdminResources.CreatePassword);
+
+WriteLiteral("\" class=\"long-input\" />\r\n </p>\r\n</fieldset>\r\n</form>\r\n");
+
+
+ }
+ }
+}
+#pragma warning restore 1591
diff --git a/src/System.Web.WebPages.Administration/Resources/AdminResources.Designer.cs b/src/System.Web.WebPages.Administration/Resources/AdminResources.Designer.cs
new file mode 100644
index 00000000..39dc1e4e
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/Resources/AdminResources.Designer.cs
@@ -0,0 +1,297 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.214
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace System.Web.WebPages.Administration {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class AdminResources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal AdminResources() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("System.Web.WebPages.Administration.Resources.AdminResources", typeof(AdminResources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Home.
+ /// </summary>
+ internal static string AdminHome {
+ get {
+ return ResourceManager.GetString("AdminHome", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Web Pages Administration.
+ /// </summary>
+ internal static string AdminModuleDisplayTitle {
+ get {
+ return ResourceManager.GetString("AdminModuleDisplayTitle", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The Admin Module requires write access to ~/App_Data.
+ /// </summary>
+ internal static string AdminModuleRequiresAccessToAppData {
+ get {
+ return ResourceManager.GetString("AdminModuleRequiresAccessToAppData", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to ASP.NET Web Pages Administration.
+ /// </summary>
+ internal static string AdminModuleTitle {
+ get {
+ return ResourceManager.GetString("AdminModuleTitle", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to To reset the password, delete the &lt;strong&gt;{0}&lt;/strong&gt; file and revisit this page..
+ /// </summary>
+ internal static string AdminPasswordChangeInstructions {
+ get {
+ return ResourceManager.GetString("AdminPasswordChangeInstructions", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to &lt;a href=&quot;{0}&quot;&gt;Click here&lt;/a&gt; to continue and verify your password after you have renamed the file to Password.config..
+ /// </summary>
+ internal static string ContinueAfterEnableText {
+ get {
+ return ResourceManager.GetString("ContinueAfterEnableText", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Create Password.
+ /// </summary>
+ internal static string CreatePassword {
+ get {
+ return ResourceManager.GetString("CreatePassword", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to For security reasons your password hash is saved in a file named _Password.config in the /App_Data/Admin/ folder of your website. To fully enable site administration, rename the file to Password.config by removing the underscore (_) character from the file name. If this is the first time you are seeing these instructions and you have not yet created a password, then remove the /App_Data/Admin/_Password.config file. This will remove a previously created password and allow you to create your own password.
+ /// </summary>
+ internal static string EnableInstructions {
+ get {
+ return ResourceManager.GetString("EnableInstructions", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Enter Password.
+ /// </summary>
+ internal static string EnterPassword {
+ get {
+ return ResourceManager.GetString("EnterPassword", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Forgot Password?.
+ /// </summary>
+ internal static string ForgotPassword {
+ get {
+ return ResourceManager.GetString("ForgotPassword", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The return URL specified for request redirection is invalid..
+ /// </summary>
+ internal static string InvalidReturnUrl {
+ get {
+ return ResourceManager.GetString("InvalidReturnUrl", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Login.
+ /// </summary>
+ internal static string Login {
+ get {
+ return ResourceManager.GetString("Login", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Login to ASP.NET Web Pages Administration.
+ /// </summary>
+ internal static string LoginTitle {
+ get {
+ return ResourceManager.GetString("LoginTitle", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Logo.
+ /// </summary>
+ internal static string LogoLabel {
+ get {
+ return ResourceManager.GetString("LogoLabel", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Logout.
+ /// </summary>
+ internal static string Logout {
+ get {
+ return ResourceManager.GetString("Logout", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Module already registered for virtual path &apos;{0}&apos;.
+ /// </summary>
+ internal static string ModuleAlreadyRegistered {
+ get {
+ return ResourceManager.GetString("ModuleAlreadyRegistered", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Modules.
+ /// </summary>
+ internal static string Modules {
+ get {
+ return ResourceManager.GetString("Modules", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to No modules are currently installed..
+ /// </summary>
+ internal static string NoAdminModulesInstalled {
+ get {
+ return ResourceManager.GetString("NoAdminModulesInstalled", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Password.
+ /// </summary>
+ internal static string Password {
+ get {
+ return ResourceManager.GetString("Password", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Confirm Password.
+ /// </summary>
+ internal static string ReenterPassword {
+ get {
+ return ResourceManager.GetString("ReenterPassword", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Accessing pages in ASP.NET Web Pages Package Administration requires a password. You create the password the first time you visit a page in this directory..
+ /// </summary>
+ internal static string RegisterDesc {
+ get {
+ return ResourceManager.GetString("RegisterDesc", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Create a password.
+ /// </summary>
+ internal static string RegisterTitle {
+ get {
+ return ResourceManager.GetString("RegisterTitle", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to ASP.NET Web Pages Administration Security Check.
+ /// </summary>
+ internal static string SecurityTitle {
+ get {
+ return ResourceManager.GetString("SecurityTitle", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The password in incorrect.
+ /// </summary>
+ internal static string Validation_PasswordIncorrect {
+ get {
+ return ResourceManager.GetString("Validation_PasswordIncorrect", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A password is required..
+ /// </summary>
+ internal static string Validation_PasswordRequired {
+ get {
+ return ResourceManager.GetString("Validation_PasswordRequired", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Confirm password does not match password.
+ /// </summary>
+ internal static string Validation_PasswordsDoNotMatch {
+ get {
+ return ResourceManager.GetString("Validation_PasswordsDoNotMatch", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.WebPages.Administration/Resources/AdminResources.resx b/src/System.Web.WebPages.Administration/Resources/AdminResources.resx
new file mode 100644
index 00000000..49edb5b2
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/Resources/AdminResources.resx
@@ -0,0 +1,201 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="AdminModuleTitle" xml:space="preserve">
+ <value>ASP.NET Web Pages Administration</value>
+ </data>
+ <data name="LoginTitle" xml:space="preserve">
+ <value>Login to ASP.NET Web Pages Administration</value>
+ </data>
+ <data name="RegisterTitle" xml:space="preserve">
+ <value>Create a password</value>
+ </data>
+ <data name="ModuleAlreadyRegistered" xml:space="preserve">
+ <value>Module already registered for virtual path '{0}'</value>
+ </data>
+ <data name="NoAdminModulesInstalled" xml:space="preserve">
+ <value>No modules are currently installed.</value>
+ </data>
+ <data name="Validation_PasswordIncorrect" xml:space="preserve">
+ <value>The password in incorrect</value>
+ </data>
+ <data name="Validation_PasswordRequired" xml:space="preserve">
+ <value>A password is required.</value>
+ </data>
+ <data name="Logout" xml:space="preserve">
+ <value>Logout</value>
+ </data>
+ <data name="Login" xml:space="preserve">
+ <value>Login</value>
+ </data>
+ <data name="Password" xml:space="preserve">
+ <value>Password</value>
+ </data>
+ <data name="AdminModuleRequiresAccessToAppData" xml:space="preserve">
+ <value>The Admin Module requires write access to ~/App_Data</value>
+ </data>
+ <data name="CreatePassword" xml:space="preserve">
+ <value>Create Password</value>
+ </data>
+ <data name="InvalidReturnUrl" xml:space="preserve">
+ <value>The return URL specified for request redirection is invalid.</value>
+ </data>
+ <data name="EnterPassword" xml:space="preserve">
+ <value>Enter Password</value>
+ </data>
+ <data name="ReenterPassword" xml:space="preserve">
+ <value>Confirm Password</value>
+ </data>
+ <data name="Validation_PasswordsDoNotMatch" xml:space="preserve">
+ <value>Confirm password does not match password</value>
+ </data>
+ <data name="EnableInstructions" xml:space="preserve">
+ <value>For security reasons your password hash is saved in a file named _Password.config in the /App_Data/Admin/ folder of your website. To fully enable site administration, rename the file to Password.config by removing the underscore (_) character from the file name. If this is the first time you are seeing these instructions and you have not yet created a password, then remove the /App_Data/Admin/_Password.config file. This will remove a previously created password and allow you to create your own password</value>
+ <comment>This resource string will be printed without html encoding. Ensure parameters are html encoded.</comment>
+ </data>
+ <data name="AdminPasswordChangeInstructions" xml:space="preserve">
+ <value>To reset the password, delete the &lt;strong&gt;{0}&lt;/strong&gt; file and revisit this page.</value>
+ <comment>This resource string will be printed without html encoding. Ensure parameters are html encoded.</comment>
+ </data>
+ <data name="ForgotPassword" xml:space="preserve">
+ <value>Forgot Password?</value>
+ </data>
+ <data name="Modules" xml:space="preserve">
+ <value>Modules</value>
+ </data>
+ <data name="RegisterDesc" xml:space="preserve">
+ <value>Accessing pages in ASP.NET Web Pages Package Administration requires a password. You create the password the first time you visit a page in this directory.</value>
+ </data>
+ <data name="AdminHome" xml:space="preserve">
+ <value>Home</value>
+ </data>
+ <data name="AdminModuleDisplayTitle" xml:space="preserve">
+ <value>Web Pages Administration</value>
+ </data>
+ <data name="LogoLabel" xml:space="preserve">
+ <value>Logo</value>
+ </data>
+ <data name="ContinueAfterEnableText" xml:space="preserve">
+ <value>&lt;a href="{0}"&gt;Click here&lt;/a&gt; to continue and verify your password after you have renamed the file to Password.config.</value>
+ <comment>This resource string will be printed without html encoding. Ensure parameters are html encoded.</comment>
+ </data>
+ <data name="SecurityTitle" xml:space="preserve">
+ <value>ASP.NET Web Pages Administration Security Check</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/System.Web.WebPages.Administration/Resources/PackageManagerResources.Designer.cs b/src/System.Web.WebPages.Administration/Resources/PackageManagerResources.Designer.cs
new file mode 100644
index 00000000..58aae8a8
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/Resources/PackageManagerResources.Designer.cs
@@ -0,0 +1,513 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.488
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace System.Web.WebPages.Administration.PackageManager {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class PackageManagerResources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal PackageManagerResources() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("System.Web.WebPages.Administration.Resources.PackageManagerResources", typeof(PackageManagerResources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Add Package Source.
+ /// </summary>
+ internal static string AddPackageSourceLabel {
+ get {
+ return ResourceManager.GetString("AddPackageSourceLabel", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Are you sure you want to uninstall &lt;strong&gt;{0}&lt;/strong&gt;?.
+ /// </summary>
+ internal static string AreYouSureUninstall {
+ get {
+ return ResourceManager.GetString("AreYouSureUninstall", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Author.
+ /// </summary>
+ internal static string AuthorsLabel {
+ get {
+ return ResourceManager.GetString("AuthorsLabel", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Bad Request.
+ /// </summary>
+ internal static string BadRequest {
+ get {
+ return ResourceManager.GetString("BadRequest", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Cancel.
+ /// </summary>
+ internal static string Cancel {
+ get {
+ return ResourceManager.GetString("Cancel", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Clear.
+ /// </summary>
+ internal static string ClearLabel {
+ get {
+ return ResourceManager.GetString("ClearLabel", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Default.
+ /// </summary>
+ internal static string DefaultPackageSourceName {
+ get {
+ return ResourceManager.GetString("DefaultPackageSourceName", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Delete.
+ /// </summary>
+ internal static string DeleteLabel {
+ get {
+ return ResourceManager.GetString("DeleteLabel", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to By clicking &quot;Install&quot; you agree to the license terms for the package(s) above. If you do not agree to the license terms, click &quot;Cancel.&quot; Each package is licensed to you by its owner. Microsoft is not responsible for, nor does it grant any licenses to, third-party packages..
+ /// </summary>
+ internal static string Disclaimer {
+ get {
+ return ResourceManager.GetString("Disclaimer", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Installed.
+ /// </summary>
+ internal static string InstalledLabel {
+ get {
+ return ResourceManager.GetString("InstalledLabel", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Install.
+ /// </summary>
+ internal static string InstallPackage {
+ get {
+ return ResourceManager.GetString("InstallPackage", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Installing package {0}..
+ /// </summary>
+ internal static string InstallPackageDesc {
+ get {
+ return ResourceManager.GetString("InstallPackageDesc", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Package &lt;strong&gt;{0}&lt;/strong&gt; was successfully installed..
+ /// </summary>
+ internal static string InstallSuccess {
+ get {
+ return ResourceManager.GetString("InstallSuccess", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to JavaScript is required to correctly view this page..
+ /// </summary>
+ internal static string JavascriptRequired {
+ get {
+ return ResourceManager.GetString("JavascriptRequired", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Use this page to manage your package sources..
+ /// </summary>
+ internal static string ManageSourcesDesc {
+ get {
+ return ResourceManager.GetString("ManageSourcesDesc", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Manage Package Sources.
+ /// </summary>
+ internal static string ManageSourcesTitle {
+ get {
+ return ResourceManager.GetString("ManageSourcesTitle", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A collection of tools to automate the process of installing, upgrading, configuring, and removing packages from an ASP.NET application..
+ /// </summary>
+ internal static string ModuleDesc {
+ get {
+ return ResourceManager.GetString("ModuleDesc", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Package Manager.
+ /// </summary>
+ internal static string ModuleTitle {
+ get {
+ return ResourceManager.GetString("ModuleTitle", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Next.
+ /// </summary>
+ internal static string NextText {
+ get {
+ return ResourceManager.GetString("NextText", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to No packages found..
+ /// </summary>
+ internal static string NoPackagesFound {
+ get {
+ return ResourceManager.GetString("NoPackagesFound", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to No packages installed. &lt;p&gt;&lt;a href=&quot;{0}&quot;&gt;Install packages from an online feed&lt;/a&gt;&lt;/p&gt;.
+ /// </summary>
+ internal static string NoPackagesInstalled {
+ get {
+ return ResourceManager.GetString("NoPackagesInstalled", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Default (All).
+ /// </summary>
+ internal static string NuGetFeed {
+ get {
+ return ResourceManager.GetString("NuGetFeed", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Online.
+ /// </summary>
+ internal static string OnlineLabel {
+ get {
+ return ResourceManager.GetString("OnlineLabel", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Error installing package &quot;{0}&quot;:.
+ /// </summary>
+ internal static string PackageInstallationError {
+ get {
+ return ResourceManager.GetString("PackageInstallationError", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Unable to read package source file. Ensure that the file &quot;{0}&quot; is writeable and the contents of the file have not been modified externally..
+ /// </summary>
+ internal static string PackageSourceFileInstructions {
+ get {
+ return ResourceManager.GetString("PackageSourceFileInstructions", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Source.
+ /// </summary>
+ internal static string PackageSourceLabel {
+ get {
+ return ResourceManager.GetString("PackageSourceLabel", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Error occurred uninstalling package &quot;{0}&quot;:.
+ /// </summary>
+ internal static string PackageUninstallationError {
+ get {
+ return ResourceManager.GetString("PackageUninstallationError", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Error occurred updating package &quot;{0}&quot;:.
+ /// </summary>
+ internal static string PackageUpdateError {
+ get {
+ return ResourceManager.GetString("PackageUpdateError", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Page.
+ /// </summary>
+ internal static string PageLabel {
+ get {
+ return ResourceManager.GetString("PageLabel", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Previous.
+ /// </summary>
+ internal static string PreviousText {
+ get {
+ return ResourceManager.GetString("PreviousText", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Privacy Statement.
+ /// </summary>
+ internal static string PrivacyStatement {
+ get {
+ return ResourceManager.GetString("PrivacyStatement", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Remove Dependencies?.
+ /// </summary>
+ internal static string RemoveDependencies {
+ get {
+ return ResourceManager.GetString("RemoveDependencies", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Restore Default .
+ /// </summary>
+ internal static string RestoreDefaultSources {
+ get {
+ return ResourceManager.GetString("RestoreDefaultSources", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Search.
+ /// </summary>
+ internal static string SearchLabel {
+ get {
+ return ResourceManager.GetString("SearchLabel", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Packages.
+ /// </summary>
+ internal static string SectionTitle {
+ get {
+ return ResourceManager.GetString("SectionTitle", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Show.
+ /// </summary>
+ internal static string ShowLabel {
+ get {
+ return ResourceManager.GetString("ShowLabel", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Name.
+ /// </summary>
+ internal static string SourceNameLabel {
+ get {
+ return ResourceManager.GetString("SourceNameLabel", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Source.
+ /// </summary>
+ internal static string SourceUrlLabel {
+ get {
+ return ResourceManager.GetString("SourceUrlLabel", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Uninstall.
+ /// </summary>
+ internal static string UninstallPackage {
+ get {
+ return ResourceManager.GetString("UninstallPackage", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Uninstalling package {0}..
+ /// </summary>
+ internal static string UninstallPackageDesc {
+ get {
+ return ResourceManager.GetString("UninstallPackageDesc", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Package &lt;strong&gt;{0}&lt;/strong&gt; was successfully uninstalled..
+ /// </summary>
+ internal static string UninstallSuccess {
+ get {
+ return ResourceManager.GetString("UninstallSuccess", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Unknown framework assembly: &quot;{0}&quot;.
+ /// </summary>
+ internal static string UnknownFrameworkReference {
+ get {
+ return ResourceManager.GetString("UnknownFrameworkReference", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Update.
+ /// </summary>
+ internal static string UpdatePackage {
+ get {
+ return ResourceManager.GetString("UpdatePackage", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Updating package {0} to version {1}..
+ /// </summary>
+ internal static string UpdatePackageDesc {
+ get {
+ return ResourceManager.GetString("UpdatePackageDesc", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Updates.
+ /// </summary>
+ internal static string UpdatesLabel {
+ get {
+ return ResourceManager.GetString("UpdatesLabel", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Package &lt;strong&gt;{0}&lt;/strong&gt; was successfully updated..
+ /// </summary>
+ internal static string UpdateSuccess {
+ get {
+ return ResourceManager.GetString("UpdateSuccess", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Invalid source url..
+ /// </summary>
+ internal static string Validation_InvalidPackageSourceUrl {
+ get {
+ return ResourceManager.GetString("Validation_InvalidPackageSourceUrl", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Specified package source already exists..
+ /// </summary>
+ internal static string Validation_PackageSourceAlreadyExists {
+ get {
+ return ResourceManager.GetString("Validation_PackageSourceAlreadyExists", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to View License Terms.
+ /// </summary>
+ internal static string ViewLicenseTerms {
+ get {
+ return ResourceManager.GetString("ViewLicenseTerms", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to , .
+ /// </summary>
+ internal static string WordSeparator {
+ get {
+ return ResourceManager.GetString("WordSeparator", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.WebPages.Administration/Resources/PackageManagerResources.resx b/src/System.Web.WebPages.Administration/Resources/PackageManagerResources.resx
new file mode 100644
index 00000000..c2089622
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/Resources/PackageManagerResources.resx
@@ -0,0 +1,277 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="PackageSourceLabel" xml:space="preserve">
+ <value>Source</value>
+ </data>
+ <data name="PageLabel" xml:space="preserve">
+ <value>Page</value>
+ </data>
+ <data name="InstallPackage" xml:space="preserve">
+ <value>Install</value>
+ </data>
+ <data name="DefaultPackageSourceName" xml:space="preserve">
+ <value>Default</value>
+ </data>
+ <data name="NoPackagesFound" xml:space="preserve">
+ <value>No packages found.</value>
+ </data>
+ <data name="PackageInstallationError" xml:space="preserve">
+ <value>Error installing package "{0}":</value>
+ </data>
+ <data name="PackageUninstallationError" xml:space="preserve">
+ <value>Error occurred uninstalling package "{0}":</value>
+ </data>
+ <data name="SearchLabel" xml:space="preserve">
+ <value>Search</value>
+ </data>
+ <data name="UninstallPackage" xml:space="preserve">
+ <value>Uninstall</value>
+ </data>
+ <data name="BadRequest" xml:space="preserve">
+ <value>Bad Request</value>
+ </data>
+ <data name="ModuleDesc" xml:space="preserve">
+ <value>A collection of tools to automate the process of installing, upgrading, configuring, and removing packages from an ASP.NET application.</value>
+ </data>
+ <data name="ModuleTitle" xml:space="preserve">
+ <value>Package Manager</value>
+ </data>
+ <data name="NoPackagesInstalled" xml:space="preserve">
+ <value>No packages installed. &lt;p&gt;&lt;a href="{0}"&gt;Install packages from an online feed&lt;/a&gt;&lt;/p&gt;</value>
+ <comment>Not HTML encoding the text. Ensure params are encoded</comment>
+ </data>
+ <data name="AuthorsLabel" xml:space="preserve">
+ <value>Author</value>
+ </data>
+ <data name="WordSeparator" xml:space="preserve">
+ <value>, </value>
+ <comment>String used as glue to join multiple words into a single comma separated string </comment>
+ </data>
+ <data name="Disclaimer" xml:space="preserve">
+ <value>By clicking "Install" you agree to the license terms for the package(s) above. If you do not agree to the license terms, click "Cancel." Each package is licensed to you by its owner. Microsoft is not responsible for, nor does it grant any licenses to, third-party packages.</value>
+ </data>
+ <data name="InstallPackageDesc" xml:space="preserve">
+ <value>Installing package {0}.</value>
+ </data>
+ <data name="UninstallPackageDesc" xml:space="preserve">
+ <value>Uninstalling package {0}.</value>
+ </data>
+ <data name="AreYouSureUninstall" xml:space="preserve">
+ <value>Are you sure you want to uninstall &lt;strong&gt;{0}&lt;/strong&gt;?</value>
+ <comment>Not HTML encoding the text. Ensure params are encoded </comment>
+ </data>
+ <data name="Cancel" xml:space="preserve">
+ <value>Cancel</value>
+ </data>
+ <data name="UninstallSuccess" xml:space="preserve">
+ <value>Package &lt;strong&gt;{0}&lt;/strong&gt; was successfully uninstalled.</value>
+ <comment>Not HTML encoding the text. Ensure params are encoded</comment>
+ </data>
+ <data name="UpdatePackage" xml:space="preserve">
+ <value>Update</value>
+ </data>
+ <data name="JavascriptRequired" xml:space="preserve">
+ <value>JavaScript is required to correctly view this page.</value>
+ </data>
+ <data name="ViewLicenseTerms" xml:space="preserve">
+ <value>View License Terms</value>
+ </data>
+ <data name="InstallSuccess" xml:space="preserve">
+ <value>Package &lt;strong&gt;{0}&lt;/strong&gt; was successfully installed.</value>
+ <comment>Not HTML encoding the text. Ensure params are encoded</comment>
+ </data>
+ <data name="UpdatePackageDesc" xml:space="preserve">
+ <value>Updating package {0} to version {1}.</value>
+ <comment>Updating Foo 1.1 to version 1.3.</comment>
+ </data>
+ <data name="UpdateSuccess" xml:space="preserve">
+ <value>Package &lt;strong&gt;{0}&lt;/strong&gt; was successfully updated.</value>
+ <comment>Not HTML encoding the text. Ensure params are encoded</comment>
+ </data>
+ <data name="ClearLabel" xml:space="preserve">
+ <value>Clear</value>
+ </data>
+ <data name="ManageSourcesDesc" xml:space="preserve">
+ <value>Use this page to manage your package sources.</value>
+ </data>
+ <data name="AddPackageSourceLabel" xml:space="preserve">
+ <value>Add Package Source</value>
+ </data>
+ <data name="DeleteLabel" xml:space="preserve">
+ <value>Delete</value>
+ </data>
+ <data name="SourceNameLabel" xml:space="preserve">
+ <value>Name</value>
+ </data>
+ <data name="SourceUrlLabel" xml:space="preserve">
+ <value>Source</value>
+ </data>
+ <data name="ManageSourcesTitle" xml:space="preserve">
+ <value>Manage Package Sources</value>
+ </data>
+ <data name="Validation_InvalidPackageSourceUrl" xml:space="preserve">
+ <value>Invalid source url.</value>
+ </data>
+ <data name="PackageSourceFileInstructions" xml:space="preserve">
+ <value>Unable to read package source file. Ensure that the file "{0}" is writeable and the contents of the file have not been modified externally.</value>
+ </data>
+ <data name="RestoreDefaultSources" xml:space="preserve">
+ <value>Restore Default </value>
+ </data>
+ <data name="PackageUpdateError" xml:space="preserve">
+ <value>Error occurred updating package "{0}":</value>
+ </data>
+ <data name="RemoveDependencies" xml:space="preserve">
+ <value>Remove Dependencies?</value>
+ </data>
+ <data name="SectionTitle" xml:space="preserve">
+ <value>Packages</value>
+ </data>
+ <data name="Validation_PackageSourceAlreadyExists" xml:space="preserve">
+ <value>Specified package source already exists.</value>
+ </data>
+ <data name="ShowLabel" xml:space="preserve">
+ <value>Show</value>
+ </data>
+ <data name="InstalledLabel" xml:space="preserve">
+ <value>Installed</value>
+ </data>
+ <data name="OnlineLabel" xml:space="preserve">
+ <value>Online</value>
+ </data>
+ <data name="UpdatesLabel" xml:space="preserve">
+ <value>Updates</value>
+ </data>
+ <data name="NuGetFeed" xml:space="preserve">
+ <value>Default (All)</value>
+ </data>
+ <data name="PrivacyStatement" xml:space="preserve">
+ <value>Privacy Statement</value>
+ </data>
+ <data name="UnknownFrameworkReference" xml:space="preserve">
+ <value>Unknown framework assembly: "{0}"</value>
+ </data>
+ <data name="NextText" xml:space="preserve">
+ <value>Next</value>
+ </data>
+ <data name="PreviousText" xml:space="preserve">
+ <value>Previous</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/System.Web.WebPages.Administration/Site.css b/src/System.Web.WebPages.Administration/Site.css
new file mode 100644
index 00000000..b588bebe
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/Site.css
@@ -0,0 +1,589 @@
+/* Reset
+***************************************************************/
+
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, font, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ outline: 0;
+ font-weight: inherit;
+ font-style: inherit;
+ font-size: 100%;
+ font-family: inherit;
+ vertical-align: baseline;
+}
+
+/* Remember focus styles! */
+:focus { outline: 0; }
+
+body { line-height: 1.4em; color: black; background: white; }
+ol, ul { list-style: none; }
+
+/* Tables still need 'cellspacing="0"' in the markup */
+table { border-collapse: separate; border-spacing: 0; }
+caption, td { text-align: left; }
+
+thead th
+{
+ font-weight: bold;
+ text-align: center;
+}
+
+blockquote:before, blockquote:after,
+q:before, q:after { content: ""; }
+blockquote, q { quotes: "" ""; }
+
+/* HTML 5 elements as block */
+header, footer, aside, nav, article { display: block; }
+
+/* Clearing Floats
+***************************************************************/
+
+.group:after
+{
+ content: ".";
+ display: block;
+ height: 0;
+ clear: both;
+ visibility: hidden;
+}
+
+/* General
+***************************************************************/
+
+/* Default font settings.
+The font-size 81.3% sets the base font to 13px
+
+Pixels EMs Percent Points
+6px 0.462em 46.2% 5pt
+7px 0.538em 53.8% 5pt
+8px 0.615em 61.5% 6pt
+9px 0.692em 69.2% 7pt
+10px 0.769em 76.9% 8pt
+11px 0.846em 84.6% 8pt
+12px 0.923em 92.3% 9pt
+13px 1em 100% 10pt
+14px 1.077em 107.7% 11pt
+15px 1.154em 115.4% 11pt
+16px 1.231em 123.1% 12pt
+17px 1.308em 130.8% 13pt
+18px 1.385em 138.5% 14pt
+19px 1.462em 146.2% 14pt
+20px 1.538em 153.8% 15pt
+21px 1.615em 161.5% 16pt
+22px 1.692em 169.2% 17pt
+23px 1.769em 176.9% 17pt
+24px 1.846em 184.6% 18pt
+26px 2em 200% 20pt
+*/
+
+body {
+ background-color: #ececec;
+ font-size: 81.3%;
+ font-family: Segoe UI,Trebuchet,"Helvetica Neue", Arial, Helvetica, sans-serif; color:#333;
+ margin: 0;
+ padding: 0;
+
+/*CSS3 properties*/
+ filter:progid:DXImageTransform.Microsoft.Gradient(GradientType=0, startColorstr='#ffffff', endColorstr='#ececec');
+ background: -webkit-gradient(linear, 0 0, 0 100%, from(#fff), to(#ececec)) fixed;
+ background: -moz-linear-gradient(top, #fff, #ececec) fixed;
+}
+
+a:link {
+ color: #034af3;
+ text-decoration: underline;
+}
+
+a:visited {
+ color: #505abc;
+}
+
+a:hover {
+ color: #1d60ff;
+ text-decoration: none;
+}
+
+a:active {
+ color: #12eb87;
+}
+
+/* Headings and Text Elements
+***************************************************************/
+
+/* Headings */
+h1, h2, h3, h4, h5, h6 { color: #385366; }
+
+h1 { font-size: 184.6%; } /*24px*/
+h2 { font-size: 176.9%; } /*23px*/
+h3 { font-size: 153.8%; } /*20px*/
+h4 { font-size: 138.5%; } /*18px*/
+h5 { font-size: 123.1%; } /*16px*/
+h6 { font-size: 107.7%; } /*14px*/
+
+/* Text elements */
+p
+{
+ line-height: 18px;
+ margin-top: 10px;
+}
+
+.text-small { font-size: 84.6%; /*11px*/ }
+
+strong
+{
+ font-weight: bold;
+}
+
+code
+{
+ display: block;
+ margin: 5px 0px 5px 5px;
+}
+
+em
+{
+ font-style: italic;
+}
+
+/* Layout
+***************************************************************/
+
+#page {
+ width: 90%;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+#header {
+ position: relative;
+ margin-bottom: 0;
+ color: #000;
+ padding: 26px 26px 26px 0;
+}
+
+#main {
+ padding: 30px 30px 15px 30px;
+ background-color: #fff;
+ margin: 0 0 30px 0;
+ border: 1px solid #a6a6a6;
+
+ /*CSS3 properties*/
+ border-radius: 3px;
+ -webkit-border-radius: 3px;
+ -moz-border-radius: 3px;
+ -webkit-box-shadow: 0px 0px 4px rgba(102, 102, 102, 0.3);
+ -moz-box-shadow: 0px 0px 4px rgba(102, 102, 102, 0.3);
+ box-shadow: 0px 0px 4px rgba(102, 102, 102, 0.3);
+ background: -webkit-gradient(linear, 0 0, 0 100%, from(#fff), to(#f3f3f3)) fixed;
+ background: -moz-linear-gradient(top, #fff, #f3f3f3) fixed;
+ filter:progid:DXImageTransform.Microsoft.Gradient(GradientType=0, startColorstr='#ffffff', endColorstr='#f3f3f3');
+}
+
+#footer {
+ padding: 10px 0;
+ text-align: center;
+ line-height: normal;
+ margin: 0;
+}
+
+/* Site Title
+***************************************************************/
+
+#header .site-title {
+ float: left;
+ color: #385366;
+ padding: 5px;
+ margin: 0;
+ border: none;
+}
+
+
+#header .site-title span.aspnet {
+ color:#3c89c8;
+}
+
+/* Login Display
+***************************************************************/
+
+#settings {
+ float: right;
+ font-size: 123.1%; /*16px*/
+ display: block;
+ text-align: right;
+ margin: 14px 0 0 0;
+}
+
+ #settings li {
+ margin: 0;
+ display: inline;
+ list-style: none;
+ padding: 0 0 0 5px;
+ }
+
+
+ #settings a:before
+ {
+ margin-right: 10px;
+ content: "["
+ }
+
+ #settings a:after
+ {
+ margin-left: 10px;
+ content: "]"
+ }
+
+ #settings a:link, #settings a:visited
+ {
+ text-decoration: none;
+ margin: 0 0 0 10px;
+ color:#034AF3;
+ }
+
+ #settings a:hover
+ {
+ text-decoration: underline;
+ }
+
+/* Tab Menu
+***************************************************************/
+
+.nav li { font-size: 123.1%; } /*16px*/
+
+ul#menu {
+ clear: both;
+ border-bottom: 1px #5c87b2 solid;
+ padding: 0 0 2px 0;
+ position: relative;
+ text-align: right;
+}
+
+ ul#menu li {
+ display: inline;
+ list-style: none;
+
+ }
+
+ ul#menu li a {
+ padding: 10px 20px 0 0;
+ font-weight: bold;
+ text-decoration: none;
+ line-height: 2.8em;
+ background-color: #e8eef4;
+ color: #034af3;
+
+ /*CSS3 properties*/
+ border-radius: 4px 4px 0 0;
+ -webkit-border-radius: 4px 4px 0 0;
+ -moz-border-radius: 4px 4px 0 0;
+ }
+
+ ul#menu li a:hover {
+ background-color: #fff;
+ text-decoration: none;
+ }
+
+ ul#menu li a:active {
+ background-color: #a6e2a6;
+ text-decoration: none;
+ }
+
+ ul#menu li.selected a {
+ background-color: #fff;
+ color: #000;
+ }
+
+/* Page Title
+***************************************************************/
+
+ .page-title {
+ border-bottom: 1px solid #e8e6e6;
+ margin: 0 0 26px 0;
+ }
+
+/* Forms
+***************************************************************/
+fieldset {
+ margin: 10px 0;
+ padding: 10px;
+ border: 1px solid #ccc;
+}
+
+ fieldset legend {
+ font-size: 123.1%; /*16px*/
+ font-weight: 600;
+ padding: 2px 4px 8px 4px;
+ }
+
+ fieldset ol {
+ padding: 0;
+ list-style: none;
+ }
+
+ fieldset ol li {
+ padding: 0 0 5px 0;
+ }
+
+ fieldset label {
+ display: block;
+ font-weight: bold;
+ }
+
+ fieldset label.checkbox {
+ display: inline;
+ }
+
+ select, fieldset input[type="text"], input[type="password"] {
+ border: 1px solid #c4c4c4;
+ color: #444;
+ width: 300px;
+ padding: 3px;
+
+ /*CSS3 properties*/
+ border-radius: 3px;
+ -webkit-border-radius: 3px;
+ -moz-border-radius: 3px;
+ }
+
+ select {
+ width: auto;
+}
+
+input[type="submit"],
+input[type="reset"],
+input[type="button"]
+{
+ font-size: 107.7%; /*14px*/
+ color:#333;
+ background:#F5F5F5;
+ border:1px solid #999;
+ cursor:pointer;
+ width:80px;
+ padding: 1px;
+ text-align:center;
+
+ /*CSS3 properties*/
+ filter:progid:DXImageTransform.Microsoft.Gradient(GradientType=0, startColorstr='#f5f5f5', endColorstr='#cbcbcb');
+ background: -webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#cbcbcb));
+ background: -moz-linear-gradient(top, #f5f5f5, #cbcbcb);
+
+ box-shadow: inset 0px 0px 1px rgba(255, 255, 255, 1.0), 1px 1px 1px rgba(102, 102, 102, 0.3);
+ -webkit-box-shadow: inset 0px 0px 1px rgba(255, 255, 255, 1.0), 1px 1px 1px rgba(102, 102, 102, 0.3);
+ -moz-box-shadow: inset 0px 0px 1px rgba(255, 255, 255, 1.0), 1px 1px 1px rgba(102, 102, 102, 0.3);
+ border-radius: 3px;
+ -webkit-border-radius: 3px;
+ -moz-border-radius: 3px;
+ }
+
+
+input[type="submit"]:hover,
+input[type="reset"]:hover,
+input[type="button"]:hover,
+button:hover {
+ text-decoration:none;
+ background: #62a9e2;
+ color:#fff;
+ border:1px solid #2e76b1;
+
+ /*CSS3 properties*/
+ filter:progid:DXImageTransform.Microsoft.Gradient(GradientType=0, startColorstr='#62a9e2', endColorstr='#3c89c8');
+ background: -webkit-gradient(linear, 0 0, 0 100%, from(#62a9e2), to(#3c89c8));
+ background: -moz-linear-gradient(top, #62a9e2, #3c89c8);
+}
+
+input[type="submit"]:active,
+input[type="reset"]:active,
+input[type="button"]:active,
+button:active {
+ text-decoration:none;
+ background: #62a9e2;
+ color:#fff;
+ border:1px solid #093253;
+
+ /*CSS3 properties*/
+ filter:progid:DXImageTransform.Microsoft.Gradient(GradientType=0, startColorstr='#72b8f2', endColorstr='#3078b3');
+ background: -webkit-gradient(linear, 0 0, 0 100%, from(#72b8f2), to(#3078b3));
+ background: -moz-linear-gradient(top, #72b8f2, #3078b3);
+ box-shadow: inset 0px 0px 1px rgba(0, 0, 0, 1.0), 1px 1px 1px rgba(102, 102, 102, 0.3);
+ -moz-box-shadow: inset 0px 0px 1px rgba(0, 0, 0, 1.0), 1px 1px 1px rgba(102, 102, 102, 0.3);
+ -webkit-box-shadow: inset 0px 0px 1px rgba(0, 0, 0, 1.0), 1px 1px 1px rgba(102, 102, 102, 0.3);
+}
+
+input[type="submit"].long-input,
+input[type="reset"].long-input,
+input[type="button"].long-input,
+button.long-input {
+ width: 140px;
+}
+
+input[type="submit"]:focus::-moz-focus-inner, button:focus::-moz-focus-inner {
+ border: 1px dotted transparent;
+}
+
+
+ /* Information and errors
+----------------------------------------------------------*/
+.message {
+ clear: both;
+ border: 1px solid;
+ margin: 10px 0px;
+ padding: 15px;
+ font-weight: bold;
+
+ /*CSS3 properties*/
+ border-radius: 3px;
+ -moz-border-radius: 3px;
+ -webkit-border-radius: 3px;
+
+ -moz-box-shadow: inset 0px 0px 1px rgba(0, 0, 0, 1.0), 1px 1px 1px rgba(102, 102, 102, 0.3);
+ -webkit-box-shadow: inset 0px 0px 1px rgba(0, 0, 0, 1.0), 1px 1px 1px rgba(102, 102, 102, 0.3);
+ box-shadow: inset 0px 0px 1px rgba(0, 0, 0, 1.0), 1px 1px 1px rgba(102, 102, 102, 0.3);
+}
+
+.info {
+ background: #dddddd;
+ color: #00529b;
+}
+
+.error {
+ background: #ffe4e4;
+ color: #d63301;
+}
+
+.success {
+ background: #dff2bf;
+ color: #43750E;
+}
+
+input[type="text"].validation-error, input[type="password"].validation-error {
+ border: solid 1px #d63301;
+ background-color: #ffccba;
+ font-weight: inherit;
+ font-size: inherit;
+ color: inherit;
+}
+
+.validation-error {
+ display: inline;
+ color: #be3e16;
+ font-weight: 600;
+ font-size: 123.1%; /*16px*/
+}
+
+.page-title > h1
+{
+ margin-top: 0;
+}
+
+.page-title > span
+{
+ font-style: italic;
+}
+.modules
+{
+ list-style-type: none;
+ margin: 0 0 0 -15px;
+}
+
+
+.modules > li
+{
+ background: url(images/package.png) no-repeat 0 10%;
+ padding: 0 0 0 30px;
+}
+
+.modules > li > a
+{
+ font-size: 123.1%; /*16px*/
+
+}
+
+#breadcrumbs
+{
+ font-size: 123.1%; /*16px*/
+ margin:0 auto;
+ display: block;
+ height: 44px;
+}
+
+#breadcrumbs ul, #breadcrumbs li
+{
+ float:left;
+ margin:0 8px;
+ height: 100%;
+}
+
+#breadcrumbs ul
+{
+ line-height: 20px;
+ list-style: none outside none;
+ padding: 0;
+}
+
+
+#breadcrumbs li.selected
+{
+ background:url("images/tabOn.gif") no-repeat scroll 45% bottom transparent;
+ bottom: -1px;
+ position: relative;
+}
+
+#breadcrumbs a, #breadcrumbs a:visited, #breadcrumbs a:active
+{
+ text-decoration: none;
+ color:#034AF3;
+}
+
+.error ul
+{
+ padding: 0;
+ margin: 0;
+}
+
+.error ul li
+{
+ list-style-type: none;
+
+}
+
+.page-settings
+{
+ float: right;
+ width: 5%;
+ text-transform: lowercase;
+}
+
+hr
+{
+ border-top: none;
+ border-left: none;
+ border-right: none;
+ border-bottom: 1px solid #E8E6E6;
+}
+
+/* Misc
+***************************************************************/
+
+.clear { clear: both; }
+
+.left { float: left; }
+
+.centered { text-align: center; }
+
+.right { float: right; }
+
+img.inline
+{
+ vertical-align: text-bottom;
+}
+
+
+
diff --git a/src/System.Web.WebPages.Administration/System.Web.WebPages.Administration.csproj b/src/System.Web.WebPages.Administration/System.Web.WebPages.Administration.csproj
new file mode 100644
index 00000000..4363a8be
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/System.Web.WebPages.Administration.csproj
@@ -0,0 +1,329 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <CodeAnalysis Condition=" '$(CodeAnalysis)' == ''">false</CodeAnalysis>
+ <ProductVersion>9.0.30729</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{C23F02FC-4538-43F5-ABBA-38BA069AEA8F}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>System.Web.WebPages.Administration</RootNamespace>
+ <AssemblyName>System.Web.WebPages.Administration</AssemblyName>
+ <FileAlignment>512</FileAlignment>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>..\..\bin\Debug\</OutputPath>
+ <DefineConstants>TRACE;DEBUG;ASPNETWEBPAGES</DefineConstants>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ <DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
+ <NoWarn>1591</NoWarn>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>..\..\bin\Release\</OutputPath>
+ <DefineConstants>TRACE;ASPNETWEBPAGES</DefineConstants>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ <RunCodeAnalysis>$(CodeAnalysis)</RunCodeAnalysis>
+ <DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
+ <NoWarn>1591</NoWarn>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'CodeCoverage|AnyCPU'">
+ <DebugSymbols>true</DebugSymbols>
+ <OutputPath>..\..\bin\CodeCoverage\</OutputPath>
+ <DefineConstants>TRACE;DEBUG;CODE_COVERAGE;ASPNETWEBPAGES</DefineConstants>
+ <DebugType>full</DebugType>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="NuGet.Core">
+ <HintPath>..\..\packages\Nuget.Core.1.6.2\lib\net40\NuGet.Core.dll</HintPath>
+ </Reference>
+ <Reference Include="System" />
+ <Reference Include="System.Configuration" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.Web" />
+ <Reference Include="System.Xml.Linq" />
+ <Reference Include="Microsoft.CSharp" />
+ <Reference Include="System.Xml" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="..\CommonResources.Designer.cs">
+ <Link>Common\CommonResources.Designer.cs</Link>
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>CommonResources.resx</DependentUpon>
+ </Compile>
+ <Compile Include="..\GlobalSuppressions.cs">
+ <Link>Common\GlobalSuppressions.cs</Link>
+ </Compile>
+ <Compile Include="..\TransparentCommonAssemblyInfo.cs">
+ <Link>Properties\TransparentCommonAssemblyInfo.cs</Link>
+ </Compile>
+ <Compile Include="EnableInstructions.generated.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>EnableInstructions.cshtml</DependentUpon>
+ </Compile>
+ <Compile Include="Framework\packages\RemoteAssembly.cs" />
+ <Compile Include="Framework\packages\IPackagesSourceFile.cs" />
+ <Compile Include="Framework\packages\IWebProjectManager.cs" />
+ <Compile Include="Framework\packages\PackageSourceFile.cs" />
+ <Compile Include="Framework\packages\WebProjectManager.cs" />
+ <Compile Include="Framework\packages\WebProjectSystem.cs" />
+ <Compile Include="Framework\SiteAdmin.cs" />
+ <Compile Include="Framework\packages\WebPackageSource.cs" />
+ <Compile Include="Framework\packages\PackageManagerModule.cs" />
+ <Compile Include="Framework\packages\PageUtils.cs" />
+ <Compile Include="Logout.generated.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>Logout.cshtml</DependentUpon>
+ </Compile>
+ <Compile Include="packages\SourceFileInstructions.generated.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>SourceFileInstructions.cshtml</DependentUpon>
+ </Compile>
+ <Compile Include="packages\PackageSources.generated.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>PackageSources.cshtml</DependentUpon>
+ </Compile>
+ <Compile Include="packages\Install.generated.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>Install.cshtml</DependentUpon>
+ </Compile>
+ <Compile Include="packages\Update.generated.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>Update.cshtml</DependentUpon>
+ </Compile>
+ <Compile Include="packages\_PackageDetails.generated.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>_PackageDetails.cshtml</DependentUpon>
+ </Compile>
+ <Compile Include="Resources\PackageManagerResources.Designer.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>PackageManagerResources.resx</DependentUpon>
+ </Compile>
+ <Compile Include="packages\Uninstall.generated.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>Uninstall.cshtml</DependentUpon>
+ </Compile>
+ <Compile Include="packages\_Package.generated.cs">
+ <DependentUpon>_Package.cshtml</DependentUpon>
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ </Compile>
+ <Compile Include="packages\Default.generated.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>Default.cshtml</DependentUpon>
+ </Compile>
+ <Compile Include="packages\_pagestart.generated.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>_pagestart.cshtml</DependentUpon>
+ </Compile>
+ <Compile Include="packages\_Layout.generated.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>_Layout.cshtml</DependentUpon>
+ </Compile>
+ <Compile Include="Resources\AdminResources.Designer.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>AdminResources.resx</DependentUpon>
+ </Compile>
+ <Compile Include="Login.generated.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>Login.cshtml</DependentUpon>
+ </Compile>
+ <Compile Include="Register.generated.cs">
+ <DependentUpon>Register.cshtml</DependentUpon>
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ </Compile>
+ <Compile Include="Framework\AdminSecurity.cs" />
+ <Compile Include="Default.generated.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>Default.cshtml</DependentUpon>
+ </Compile>
+ <Compile Include="Framework\PreApplicationStartCode.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="..\CommonAssemblyInfo.cs">
+ <Link>Properties\CommonAssemblyInfo.cs</Link>
+ </Compile>
+ <Compile Include="_pagestart.generated.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>_pagestart.cshtml</DependentUpon>
+ </Compile>
+ <Compile Include="_Layout.generated.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>_Layout.cshtml</DependentUpon>
+ </Compile>
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="Framework\packages\PackageExtensions.cs" />
+ <None Include="packages.config" />
+ <None Include="_Layout.cshtml">
+ <Generator>RazorGenerator</Generator>
+ <LastGenOutput>_Layout.generated.cs</LastGenOutput>
+ </None>
+ <None Include="Logout.cshtml">
+ <Generator>RazorGenerator</Generator>
+ <LastGenOutput>Logout.generated.cs</LastGenOutput>
+ </None>
+ <None Include="Register.cshtml">
+ <Generator>RazorGenerator</Generator>
+ <LastGenOutput>Register.generated.cs</LastGenOutput>
+ </None>
+ <None Include="Login.cshtml">
+ <Generator>RazorGenerator</Generator>
+ <LastGenOutput>Login.generated.cs</LastGenOutput>
+ </None>
+ <None Include="_pagestart.cshtml">
+ <Generator>RazorGenerator</Generator>
+ <LastGenOutput>_pagestart.generated.cs</LastGenOutput>
+ </None>
+ <None Include="Default.cshtml">
+ <Generator>RazorGenerator</Generator>
+ <LastGenOutput>Default.generated.cs</LastGenOutput>
+ </None>
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="..\CommonResources.resx">
+ <Link>Common\CommonResources.resx</Link>
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>CommonResources.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Include="Resources\AdminResources.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>AdminResources.Designer.cs</LastGenOutput>
+ <CustomToolNamespace>System.Web.WebPages.Administration</CustomToolNamespace>
+ </EmbeddedResource>
+ <EmbeddedResource Include="Resources\PackageManagerResources.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>PackageManagerResources.Designer.cs</LastGenOutput>
+ <CustomToolNamespace>System.Web.WebPages.Administration.PackageManager</CustomToolNamespace>
+ </EmbeddedResource>
+ <EmbeddedResource Include="Site.css" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="images\error.png" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="images\package.png" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\System.Web.WebPages.Deployment\System.Web.WebPages.Deployment.csproj">
+ <Project>{22BABB60-8F02-4027-AFFC-ACF069954536}</Project>
+ <Name>System.Web.WebPages.Deployment</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\WebMatrix.Data\WebMatrix.Data.csproj">
+ <Project>{4D39BAAF-8A96-473E-AB79-C8A341885137}</Project>
+ <Name>WebMatrix.Data</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\System.Web.Helpers\System.Web.Helpers.csproj">
+ <Project>{9B7E3740-6161-4548-833C-4BBCA43B970E}</Project>
+ <Name>System.Web.Helpers</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\System.Web.WebPages\System.Web.WebPages.csproj">
+ <Project>{76EFA9C5-8D7E-4FDF-B710-E20F8B6B00D2}</Project>
+ <Name>System.Web.WebPages</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\WebMatrix.WebData\WebMatrix.WebData.csproj">
+ <Project>{55A15F40-1435-4248-A7F2-2A146BB83586}</Project>
+ <Name>WebMatrix.WebData</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="images\aspLogo.gif" />
+ <EmbeddedResource Include="images\tabon.gif" />
+ <EmbeddedResource Include="images\ok.png" />
+ <EmbeddedResource Include="packages\scripts\PackageAction.js" />
+ <EmbeddedResource Include="packages\scripts\Default.js" />
+ <None Include="EnableInstructions.cshtml">
+ <Generator>RazorGenerator</Generator>
+ <LastGenOutput>EnableInstructions.generated.cs</LastGenOutput>
+ </None>
+ <None Include="packages\SourceFileInstructions.cshtml">
+ <CustomToolNamespace>System.Web.WebPages.Administration.PackageManager</CustomToolNamespace>
+ <Generator>RazorGenerator</Generator>
+ <LastGenOutput>SourceFileInstructions.generated.cs</LastGenOutput>
+ </None>
+ <None Include="packages\PackageSources.cshtml">
+ <CustomToolNamespace>System.Web.WebPages.Administration.PackageManager</CustomToolNamespace>
+ <Generator>RazorGenerator</Generator>
+ <LastGenOutput>PackageSources.generated.cs</LastGenOutput>
+ </None>
+ <None Include="packages\Install.cshtml">
+ <CustomToolNamespace>System.Web.WebPages.Administration.PackageManager</CustomToolNamespace>
+ <Generator>RazorGenerator</Generator>
+ <LastGenOutput>Install.generated.cs</LastGenOutput>
+ </None>
+ <None Include="packages\Update.cshtml">
+ <CustomToolNamespace>System.Web.WebPages.Administration.PackageManager</CustomToolNamespace>
+ <Generator>RazorGenerator</Generator>
+ <LastGenOutput>Update.generated.cs</LastGenOutput>
+ </None>
+ <None Include="packages\_Package.cshtml">
+ <Generator>RazorGenerator</Generator>
+ <LastGenOutput>_Package.generated.cs</LastGenOutput>
+ <CustomToolNamespace>System.Web.WebPages.Administration.PackageManager</CustomToolNamespace>
+ </None>
+ <None Include="packages\Default.cshtml">
+ <Generator>RazorGenerator</Generator>
+ <LastGenOutput>Default.generated.cs</LastGenOutput>
+ <CustomToolNamespace>System.Web.WebPages.Administration.PackageManager</CustomToolNamespace>
+ </None>
+ <EmbeddedResource Include="packages\images\error.png" />
+ <EmbeddedResource Include="packages\images\package.png" />
+ <EmbeddedResource Include="packages\Site.css" />
+ <None Include="packages\Uninstall.cshtml">
+ <Generator>RazorGenerator</Generator>
+ <LastGenOutput>Uninstall.generated.cs</LastGenOutput>
+ <CustomToolNamespace>System.Web.WebPages.Administration.PackageManager</CustomToolNamespace>
+ </None>
+ <None Include="packages\_PackageDetails.cshtml">
+ <Generator>RazorGenerator</Generator>
+ <LastGenOutput>_PackageDetails.generated.cs</LastGenOutput>
+ <CustomToolNamespace>System.Web.WebPages.Administration.PackageManager</CustomToolNamespace>
+ </None>
+ <None Include="packages\_pagestart.cshtml">
+ <Generator>RazorGenerator</Generator>
+ <CustomToolNamespace>System.Web.WebPages.Administration.PackageManager</CustomToolNamespace>
+ <LastGenOutput>_pagestart.generated.cs</LastGenOutput>
+ </None>
+ <None Include="packages\_Layout.cshtml">
+ <Generator>RazorGenerator</Generator>
+ <LastGenOutput>_Layout.generated.cs</LastGenOutput>
+ <CustomToolNamespace>System.Web.WebPages.Administration.PackageManager</CustomToolNamespace>
+ </None>
+ </ItemGroup>
+ <ItemGroup>
+ <Folder Include="packages\Resources\" />
+ </ItemGroup>
+ <ItemGroup>
+ <CodeAnalysisDictionary Include="..\CodeAnalysisDictionary.xml" />
+ </ItemGroup>
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+</Project> \ No newline at end of file
diff --git a/src/System.Web.WebPages.Administration/_Layout.cshtml b/src/System.Web.WebPages.Administration/_Layout.cshtml
new file mode 100644
index 00000000..6d0f84ba
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/_Layout.cshtml
@@ -0,0 +1,73 @@
+@* Generator: WebPage *@
+
+@{
+ string title = Page.Title;
+}
+
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>@AdminResources.AdminModuleTitle@if (!title.IsEmpty()) { <text>- @title </text> }</title>
+ <link href="@Href("Site.css")" rel="stylesheet" type="text/css" />
+ @RenderSection("Head", required: false)
+ </head>
+ <body>
+ <div id="page">
+ <div id="header" class="group">
+ <h1 class="site-title">
+ <img src="@Href("images/aspLogo.gif")" alt="@AdminResources.LogoLabel" />
+ <span class="aspnet">ASP.NET</span>&nbsp;@AdminResources.AdminModuleDisplayTitle
+ </h1>
+ <div id="settings">
+ <span>
+ @RenderSection("PageSettings", required: false)
+ &nbsp;
+ @if (AdminSecurity.IsAuthenticated(Request)) {
+ <a href="@Href("Logout")">@AdminResources.Logout</a>
+ }
+ </span>
+ </div>
+ </div>
+ <div class="clear"></div>
+ <div id="breadcrumbs" class="group">
+ <ul>
+ @{
+ var firstCrumb = true;
+ int current = 0;
+ }
+ @foreach(var item in Page.BreadCrumbs){
+ current++;
+ if(firstCrumb) {
+ firstCrumb = false;
+ }
+ else {
+ <li><span>&gt;</span></li>
+ }
+ <li @((current == Page.BreadCrumbs.Count) ? @Html.Raw("class=\"selected\"") : null)>
+ <a href="@item.Item2" title="@item.Item1">@item.Item1</a>
+ </li>
+ }
+ </ul>
+
+ </div>
+ <div id="main">
+ @{
+ string sectionTitle = Page.SectionTitle ?? title;
+ }
+ @if (!sectionTitle.IsEmpty()) {
+ <div class="page-title">
+ <h1>@sectionTitle</h1>
+ @if (!String.IsNullOrEmpty(Page.Desc)) {
+ <span>@Page.Desc</span>
+ }
+ </div>
+ }
+ @RenderBody()
+ <p />
+ </div>
+ <div id="footer">
+ @RenderSection("Footer", required: false)
+ </div>
+ </div>
+ </body>
+</html> \ No newline at end of file
diff --git a/src/System.Web.WebPages.Administration/_Layout.generated.cs b/src/System.Web.WebPages.Administration/_Layout.generated.cs
new file mode 100644
index 00000000..be9ea3de
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/_Layout.generated.cs
@@ -0,0 +1,232 @@
+#pragma warning disable 1591
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.225
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace System.Web.WebPages.Administration
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+ using System.Net;
+ using System.Web;
+ using System.Web.Helpers;
+ using System.Web.Security;
+ using System.Web.UI;
+ using System.Web.WebPages;
+ using System.Web.WebPages.Html;
+
+ [System.Web.WebPages.PageVirtualPathAttribute("~/_Layout.cshtml")]
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorSingleFileGenerator", "1.0.0.0")]
+ public class Layout_cshtml : System.Web.WebPages.WebPage
+ {
+#line hidden
+
+ // Resolve package relative syntax
+ // Also, if it comes from a static embedded resource, change the path accordingly
+ public override string Href(string virtualPath, params object[] pathParts) {
+ virtualPath = ApplicationPart.ProcessVirtualPath(GetType().Assembly, VirtualPath, virtualPath);
+ return base.Href(virtualPath, pathParts);
+ }
+ public Layout_cshtml()
+ {
+ }
+ protected System.Web.HttpApplication ApplicationInstance
+ {
+ get
+ {
+ return ((System.Web.HttpApplication)(Context.ApplicationInstance));
+ }
+ }
+ public override void Execute()
+ {
+
+
+WriteLiteral("\r\n\r\n");
+
+
+
+ string title = Page.Title;
+
+
+WriteLiteral("\r\n<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/" +
+"xhtml1/DTD/xhtml1-strict.dtd\">\r\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\r\n " +
+"<head>\r\n <title>");
+
+
+ Write(AdminResources.AdminModuleTitle);
+
+
+ if (!title.IsEmpty()) {
+WriteLiteral(" ");
+
+WriteLiteral("- ");
+
+
+ Write(title);
+
+WriteLiteral(" ");
+
+WriteLiteral(" ");
+
+
+ }
+WriteLiteral("</title>\r\n <link href=\"");
+
+
+ Write(Href("Site.css"));
+
+WriteLiteral("\" rel=\"stylesheet\" type=\"text/css\" />\r\n ");
+
+
+ Write(RenderSection("Head", required: false));
+
+WriteLiteral("\r\n </head>\r\n <body>\r\n <div id=\"page\">\r\n <div id=\"header\" class=\"" +
+"group\">\r\n <h1 class=\"site-title\">\r\n <img src=\"");
+
+
+ Write(Href("images/aspLogo.gif"));
+
+WriteLiteral("\" alt=\"");
+
+
+ Write(AdminResources.LogoLabel);
+
+WriteLiteral("\" />\r\n <span class=\"aspnet\">ASP.NET</span>&nbsp;");
+
+
+ Write(AdminResources.AdminModuleDisplayTitle);
+
+WriteLiteral("\r\n </h1>\r\n <div id=\"settings\">\r\n <span>\r\n " +
+" ");
+
+
+ Write(RenderSection("PageSettings", required: false));
+
+WriteLiteral("\r\n &nbsp;\r\n");
+
+
+ if (AdminSecurity.IsAuthenticated(Request)) {
+
+WriteLiteral(" <a href=\"");
+
+
+ Write(Href("Logout"));
+
+WriteLiteral("\">");
+
+
+ Write(AdminResources.Logout);
+
+WriteLiteral("</a>\r\n");
+
+
+ }
+
+WriteLiteral(" </span>\r\n </div>\r\n </div>\r\n <div " +
+"class=\"clear\"></div>\r\n <div id=\"breadcrumbs\" class=\"group\">\r\n " +
+" <ul>\r\n");
+
+
+
+ var firstCrumb = true;
+ int current = 0;
+
+
+
+ foreach(var item in Page.BreadCrumbs){
+ current++;
+ if(firstCrumb) {
+ firstCrumb = false;
+ }
+ else {
+
+WriteLiteral(" <li><span>&gt;</span></li>\r\n");
+
+
+ }
+
+WriteLiteral(" <li ");
+
+
+ Write((current == Page.BreadCrumbs.Count) ? @Html.Raw("class=\"selected\"") : null);
+
+WriteLiteral(">\r\n <a href=\"");
+
+
+ Write(item.Item2);
+
+WriteLiteral("\" title=\"");
+
+
+ Write(item.Item1);
+
+WriteLiteral("\">");
+
+
+ Write(item.Item1);
+
+WriteLiteral("</a>\r\n </li>\r\n");
+
+
+ }
+
+WriteLiteral(" </ul>\r\n\r\n </div>\r\n <div id=\"main\">\r\n");
+
+
+
+ string sectionTitle = Page.SectionTitle ?? title;
+
+
+
+ if (!sectionTitle.IsEmpty()) {
+
+WriteLiteral(" <div class=\"page-title\">\r\n <h1>");
+
+
+ Write(sectionTitle);
+
+WriteLiteral("</h1>\r\n");
+
+
+ if (!String.IsNullOrEmpty(Page.Desc)) {
+
+WriteLiteral(" <span>");
+
+
+ Write(Page.Desc);
+
+WriteLiteral("</span>\r\n");
+
+
+ }
+
+WriteLiteral(" </div>\r\n");
+
+
+ }
+
+WriteLiteral(" ");
+
+
+ Write(RenderBody());
+
+WriteLiteral("\r\n <p />\r\n </div>\r\n <div id=\"footer\">\r\n ");
+
+
+ Write(RenderSection("Footer", required: false));
+
+WriteLiteral("\r\n </div>\r\n </div>\r\n </body>\r\n</html>");
+
+
+ }
+ }
+}
+#pragma warning restore 1591
diff --git a/src/System.Web.WebPages.Administration/_pagestart.cshtml b/src/System.Web.WebPages.Administration/_pagestart.cshtml
new file mode 100644
index 00000000..3506c852
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/_pagestart.cshtml
@@ -0,0 +1,30 @@
+@* Generator: WebPage *@
+
+@{
+ // 404 if the admin page isn't available
+ if (!SiteAdmin.Available) {
+ Response.SetStatus(HttpStatusCode.NotFound);
+ return;
+ }
+
+ AdminSecurity.Authorize(this);
+}
+
+@{
+ // Set up layout values
+ var breadCrumbs = new List<Tuple<string, string>>();
+ if (SiteAdmin.Modules.Any()) {
+ breadCrumbs.Add(Tuple.Create(AdminResources.AdminHome, Href(SiteAdmin.AdminVirtualPath)));
+ }
+ PageData["BreadCrumbs"] = breadCrumbs;
+ Layout = "_Layout.cshtml";
+
+
+ HtmlHelper.ValidationSummaryClass = "message error";
+ HtmlHelper.ValidationInputCssClassName = "validation-error";
+
+ // Force IE9 standards mode rendering
+ Response.AddHeader("X-UA-Compatible", "IE=Edge");
+}
+
+
diff --git a/src/System.Web.WebPages.Administration/_pagestart.generated.cs b/src/System.Web.WebPages.Administration/_pagestart.generated.cs
new file mode 100644
index 00000000..01cbe883
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/_pagestart.generated.cs
@@ -0,0 +1,91 @@
+#pragma warning disable 1591
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.225
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace System.Web.WebPages.Administration
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+ using System.Net;
+ using System.Web;
+ using System.Web.Helpers;
+ using System.Web.Security;
+ using System.Web.UI;
+ using System.Web.WebPages;
+ using System.Web.WebPages.Html;
+
+ [System.Web.WebPages.PageVirtualPathAttribute("~/_pagestart.cshtml")]
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorSingleFileGenerator", "1.0.0.0")]
+ public class pagestart_cshtml : System.Web.WebPages.StartPage
+ {
+#line hidden
+
+ // Resolve package relative syntax
+ // Also, if it comes from a static embedded resource, change the path accordingly
+ public override string Href(string virtualPath, params object[] pathParts) {
+ virtualPath = ApplicationPart.ProcessVirtualPath(GetType().Assembly, VirtualPath, virtualPath);
+ return base.Href(virtualPath, pathParts);
+ }
+ public pagestart_cshtml()
+ {
+ }
+ protected System.Web.HttpApplication ApplicationInstance
+ {
+ get
+ {
+ return ((System.Web.HttpApplication)(Context.ApplicationInstance));
+ }
+ }
+ public override void Execute()
+ {
+
+
+WriteLiteral("\r\n\r\n");
+
+
+
+ // 404 if the admin page isn't available
+ if (!SiteAdmin.Available) {
+ Response.SetStatus(HttpStatusCode.NotFound);
+ return;
+ }
+
+ AdminSecurity.Authorize(this);
+
+
+WriteLiteral("\r\n");
+
+
+
+ // Set up layout values
+ var breadCrumbs = new List<Tuple<string, string>>();
+ if (SiteAdmin.Modules.Any()) {
+ breadCrumbs.Add(Tuple.Create(AdminResources.AdminHome, Href(SiteAdmin.AdminVirtualPath)));
+ }
+ PageData["BreadCrumbs"] = breadCrumbs;
+ Layout = "_Layout.cshtml";
+
+
+ HtmlHelper.ValidationSummaryClass = "message error";
+ HtmlHelper.ValidationInputCssClassName = "validation-error";
+
+ // Force IE9 standards mode rendering
+ Response.AddHeader("X-UA-Compatible", "IE=Edge");
+
+
+WriteLiteral("\r\n\r\n");
+
+
+ }
+ }
+}
+#pragma warning restore 1591
diff --git a/src/System.Web.WebPages.Administration/images/aspLogo.gif b/src/System.Web.WebPages.Administration/images/aspLogo.gif
new file mode 100644
index 00000000..640c6096
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/images/aspLogo.gif
Binary files differ
diff --git a/src/System.Web.WebPages.Administration/images/error.png b/src/System.Web.WebPages.Administration/images/error.png
new file mode 100644
index 00000000..23e6d0e5
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/images/error.png
Binary files differ
diff --git a/src/System.Web.WebPages.Administration/images/ok.png b/src/System.Web.WebPages.Administration/images/ok.png
new file mode 100644
index 00000000..2d567dd7
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/images/ok.png
Binary files differ
diff --git a/src/System.Web.WebPages.Administration/images/package.png b/src/System.Web.WebPages.Administration/images/package.png
new file mode 100644
index 00000000..8ed71ae1
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/images/package.png
Binary files differ
diff --git a/src/System.Web.WebPages.Administration/images/tabon.gif b/src/System.Web.WebPages.Administration/images/tabon.gif
new file mode 100644
index 00000000..a41a9bb4
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/images/tabon.gif
Binary files differ
diff --git a/src/System.Web.WebPages.Administration/packages.config b/src/System.Web.WebPages.Administration/packages.config
new file mode 100644
index 00000000..08dbc9de
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/packages.config
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="NuGet.Core" version="1.6.2" />
+</packages> \ No newline at end of file
diff --git a/src/System.Web.WebPages.Administration/packages/Default.cshtml b/src/System.Web.WebPages.Administration/packages/Default.cshtml
new file mode 100644
index 00000000..1c901a36
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/packages/Default.cshtml
@@ -0,0 +1,218 @@
+@* Generator: WebPage *@
+
+@using System.Globalization;
+@using NuGet;
+
+@section PackageHead{
+ <script type="text/javascript" src="@Href("scripts/Default.js")"></script>
+ <noscript>@PackageManagerResources.JavascriptRequired</noscript>
+}
+@{
+ Page.SectionTitle = PackageManagerResources.SectionTitle;
+ // Page Constants
+ const int PackagesPerPage = 10;
+ const string FilterCookieName = "packagesFilter";
+
+ // Request parameters
+ var view = GetView(PageUtils.GetFilterValue(Request, FilterCookieName, "view"));
+ var searchTerm = Request.QueryString["search"];
+ var packageSourceName = PageUtils.GetFilterValue(Request, FilterCookieName, "source");
+
+ var packageSource = PageUtils.GetPackageSource(packageSourceName);
+
+ PageUtils.PersistFilter(Response, FilterCookieName, new Dictionary<string, string> {
+ { "view", view.ToString() },
+ { "source", packageSourceName },
+ });
+
+ // Add values to ModelState
+ ModelState.SetModelValue("view", view.ToString());
+
+ WebProjectManager projectManager;
+ WebGrid grid;
+ int totalPackages = 0;
+
+ try {
+ // This entire block needs ot be inside a try catch block. This is necessary because exceptions could be fired at two places
+ // 1. When trying to connect to the remote repository. 2. When executing the Linq expression
+
+ IEnumerable<IPackage> packages;
+
+ projectManager = new WebProjectManager(packageSource.Source, PackageManagerModule.SiteRoot);
+ // Read packages
+ switch (view) {
+ case View.Updates:
+ packages = projectManager.GetPackagesWithUpdates(searchTerm, packageSource.FilterPreferredPackages);
+ break;
+ case View.Online:
+ IQueryable<IPackage> remotePackages = projectManager.GetRemotePackages(searchTerm, packageSource.FilterPreferredPackages);
+ totalPackages = remotePackages.Count();
+ packages = WebProjectManager.CollapseVersions(remotePackages);
+ break;
+ default:
+ packages = projectManager.GetInstalledPackages(searchTerm);
+ break;
+ }
+ if (view != View.Online) {
+ totalPackages = packages.Count();
+ }
+
+ grid = new WebGrid(rowsPerPage: PackagesPerPage, pageFieldName: "page");
+
+ packages = packages.Skip(grid.PageIndex * PackagesPerPage)
+ .Take(PackagesPerPage);
+
+ grid.Bind(packages.ToList(), columnNames: Enumerable.Empty<string>(), autoSortAndPage: false, rowCount: totalPackages);
+
+ }
+ catch (Exception exception) {
+ <div class="error message">@exception.Message</div>
+ return;
+ }
+}
+
+@{
+ var completedAction = Request.QueryString["action-completed"];
+ if (!completedAction.IsEmpty()) {
+ var packageName = Html.Encode(Request.QueryString["packageName"]);
+ string message = null;
+ if (completedAction.Equals("Install", StringComparison.OrdinalIgnoreCase)) {
+ message = String.Format(CultureInfo.CurrentCulture, PackageManagerResources.InstallSuccess, packageName);
+ }
+ else if (completedAction.Equals("Uninstall", StringComparison.OrdinalIgnoreCase)) {
+ message = String.Format(CultureInfo.CurrentCulture, PackageManagerResources.UninstallSuccess, packageName);
+ }
+ else if (completedAction.Equals("Update", StringComparison.OrdinalIgnoreCase)) {
+ message = String.Format(CultureInfo.CurrentCulture, PackageManagerResources.UpdateSuccess, packageName);
+ }
+
+ if (message != null) {
+ <div class="success message">
+ <img class="inline" src="@Href(SiteAdmin.GetVirtualPath("~/images/ok.png"))" alt="@Html.Raw(message)" />&nbsp;@Html.Raw(message)
+ </div>
+ <br />
+ }
+ }
+}
+<form method="get" action="" class="group">
+ <div class="left form-actions">
+ <label>@PackageManagerResources.ShowLabel:
+ @Html.DropDownList("view", from v in new[] { View.Installed, View.Online, View.Updates }
+ select new SelectListItem { Text = GetViewName(v), Value = v.ToString() })
+ </label>
+
+ @if (PackageManagerModule.PackageSources.Count() > 1) {
+ <span @if (view == View.Installed) { @Html.Raw("style=\"display: none\"");
+ }>
+ &nbsp;
+ <label>@PackageManagerResources.PackageSourceLabel:</label>
+ @Html.DropDownList("source", from f in PackageManagerModule.PackageSources.OrderBy(p => p.Name)
+ select new SelectListItem {
+ Value = f.Name,
+ Text = f.Name,
+ Selected = f.Name.Equals(packageSourceName, StringComparison.OrdinalIgnoreCase)
+ }
+ )
+ </span>
+ }
+ </div>
+
+ <div class="right">
+ <fieldset class="no-border">
+ <input type="text" id="search" name="search" size="30" value="@searchTerm" />
+ <input type="submit" value="@PackageManagerResources.SearchLabel" />
+ <input type="reset" id="searchReset" value="@PackageManagerResources.ClearLabel" />
+ </fieldset>
+ </div>
+</form>
+
+@if (view != View.Online && !projectManager.LocalRepository.GetPackages().Any()) {
+ var onlineLink = Href(PageUtils.GetPackagesHome()) + "?view=" + View.Online;
+ @Html.Raw(String.Format(CultureInfo.CurrentCulture, PackageManagerResources.NoPackagesInstalled, Html.Encode(onlineLink)))
+ return;
+}
+
+@if (!grid.Rows.Any()) {
+ <h3>@PackageManagerResources.NoPackagesFound</h3>
+}
+else {
+ <ul id="package-list">
+ @{ var dataDictionary = new Dictionary<string, object>(1); }
+ @foreach (var item in grid.Rows) {
+ IPackage package = item.Value;
+ dataDictionary["package"] = package;
+ <li>
+ <div class="column-left">
+ @RenderPage("_Package.cshtml", dataDictionary)
+ </div>
+ <div class="right">
+ <form method="get" action="@Href(GetPostUrl(view, package, projectManager))">
+ <input type="hidden" name="page" value="@(grid.PageIndex + 1)" />
+ <input type="hidden" name="package" value="@package.Id" />
+ <input type="hidden" name="version" value="@package.Version" />
+ <input type="hidden" name="packageName" value="@package.GetDisplayName()" />
+ <input class="formatted" type="submit" value="@GetSubmitText(view, package, projectManager)" />
+ </form>
+ </div>
+ <div class="clear"></div>
+ </li>
+ }
+ </ul>
+}
+@if (totalPackages > PackagesPerPage) {
+ <div class="pager">
+ <strong>@PackageManagerResources.PageLabel: </strong>
+ @grid.Pager(WebGridPagerModes.FirstLast | WebGridPagerModes.NextPrevious,
+ nextText: PackageManagerResources.NextText,
+ previousText: PackageManagerResources.PreviousText)
+ </div>
+}
+
+
+@functions {
+ private enum View { Installed, Updates, Online };
+ private static readonly IDictionary<string, View> _viewMapping = new Dictionary<string, View>(StringComparer.OrdinalIgnoreCase) {
+ { "Installed", View.Installed }, { "Updates", View.Updates }, { "Online", View.Online }
+ };
+
+ private static View GetView(string viewName) {
+ View view;
+ if (String.IsNullOrEmpty(viewName) || !_viewMapping.TryGetValue(viewName, out view)) {
+ view = View.Installed;
+ }
+ return view;
+ }
+
+ private static string GetViewName(View view) {
+ switch (view) {
+ case View.Online:
+ return PackageManagerResources.OnlineLabel;
+ case View.Updates:
+ return PackageManagerResources.UpdatesLabel;
+ default:
+ return PackageManagerResources.InstalledLabel;
+ }
+ }
+
+ private static string GetSubmitText(View view, IPackage package, WebProjectManager projectManager) {
+ switch (view) {
+ case View.Online:
+ return projectManager.IsPackageInstalled(package) ? PackageManagerResources.UninstallPackage : PackageManagerResources.InstallPackage;
+ case View.Updates:
+ return PackageManagerResources.UpdatePackage;
+ default:
+ return PackageManagerResources.UninstallPackage;
+ }
+ }
+
+ private static string GetPostUrl(View view, IPackage package, WebProjectManager projectManager) {
+ switch (view) {
+ case View.Online:
+ return projectManager.IsPackageInstalled(package) ? "Uninstall" : "Install";
+ case View.Updates:
+ return "Update";
+ default:
+ return "Uninstall";
+ }
+ }
+} \ No newline at end of file
diff --git a/src/System.Web.WebPages.Administration/packages/Default.generated.cs b/src/System.Web.WebPages.Administration/packages/Default.generated.cs
new file mode 100644
index 00000000..44e526d5
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/packages/Default.generated.cs
@@ -0,0 +1,420 @@
+#pragma warning disable 1591
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.225
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace System.Web.WebPages.Administration.PackageManager
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+ using System.Net;
+ using System.Web;
+ using System.Web.Helpers;
+ using System.Web.Security;
+ using System.Web.UI;
+ using System.Web.WebPages;
+ using System.Web.WebPages.Html;
+ using System.Globalization;
+ using NuGet;
+
+ [System.Web.WebPages.PageVirtualPathAttribute("~/packages/Default.cshtml")]
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorSingleFileGenerator", "0.6.0.0")]
+ public class packages_Default_cshtml : System.Web.WebPages.WebPage
+ {
+#line hidden
+
+ private enum View { Installed, Updates, Online };
+ private static readonly IDictionary<string, View> _viewMapping = new Dictionary<string, View>(StringComparer.OrdinalIgnoreCase) {
+ { "Installed", View.Installed }, { "Updates", View.Updates }, { "Online", View.Online }
+ };
+
+ private static View GetView(string viewName) {
+ View view;
+ if (String.IsNullOrEmpty(viewName) || !_viewMapping.TryGetValue(viewName, out view)) {
+ view = View.Installed;
+ }
+ return view;
+ }
+
+ private static string GetViewName(View view) {
+ switch (view) {
+ case View.Online:
+ return PackageManagerResources.OnlineLabel;
+ case View.Updates:
+ return PackageManagerResources.UpdatesLabel;
+ default:
+ return PackageManagerResources.InstalledLabel;
+ }
+ }
+
+ private static string GetSubmitText(View view, IPackage package, WebProjectManager projectManager) {
+ switch (view) {
+ case View.Online:
+ return projectManager.IsPackageInstalled(package) ? PackageManagerResources.UninstallPackage : PackageManagerResources.InstallPackage;
+ case View.Updates:
+ return PackageManagerResources.UpdatePackage;
+ default:
+ return PackageManagerResources.UninstallPackage;
+ }
+ }
+
+ private static string GetPostUrl(View view, IPackage package, WebProjectManager projectManager) {
+ switch (view) {
+ case View.Online:
+ return projectManager.IsPackageInstalled(package) ? "Uninstall" : "Install";
+ case View.Updates:
+ return "Update";
+ default:
+ return "Uninstall";
+ }
+ }
+
+ // Resolve package relative syntax
+ // Also, if it comes from a static embedded resource, change the path accordingly
+ public override string Href(string virtualPath, params object[] pathParts) {
+ virtualPath = ApplicationPart.ProcessVirtualPath(GetType().Assembly, VirtualPath, virtualPath);
+ return base.Href(virtualPath, pathParts);
+ }
+ public packages_Default_cshtml()
+ {
+ }
+ protected System.Web.HttpApplication ApplicationInstance
+ {
+ get
+ {
+ return ((System.Web.HttpApplication)(Context.ApplicationInstance));
+ }
+ }
+ public override void Execute()
+ {
+
+
+WriteLiteral("\r\n\r\n");
+
+
+
+WriteLiteral("\r\n");
+
+
+DefineSection("PackageHead", () => {
+
+WriteLiteral("\r\n <script type=\"text/javascript\" src=\"");
+
+
+ Write(Href("scripts/Default.js"));
+
+WriteLiteral("\"></script>\r\n <noscript>");
+
+
+ Write(PackageManagerResources.JavascriptRequired);
+
+WriteLiteral("</noscript>\r\n");
+
+
+});
+
+WriteLiteral("\r\n");
+
+
+
+ Page.SectionTitle = PackageManagerResources.SectionTitle;
+ // Page Constants
+ const int PackagesPerPage = 10;
+ const string FilterCookieName = "packagesFilter";
+
+ // Request parameters
+ var view = GetView(PageUtils.GetFilterValue(Request, FilterCookieName, "view"));
+ var searchTerm = Request.QueryString["search"];
+ var packageSourceName = PageUtils.GetFilterValue(Request, FilterCookieName, "source");
+
+ var packageSource = PageUtils.GetPackageSource(packageSourceName);
+
+ PageUtils.PersistFilter(Response, FilterCookieName, new Dictionary<string, string> {
+ { "view", view.ToString() },
+ { "source", packageSourceName },
+ });
+
+ // Add values to ModelState
+ ModelState.SetModelValue("view", view.ToString());
+
+ WebProjectManager projectManager;
+ WebGrid grid;
+ int totalPackages = 0;
+
+ try {
+ // This entire block needs ot be inside a try catch block. This is necessary because exceptions could be fired at two places
+ // 1. When trying to connect to the remote repository. 2. When executing the Linq expression
+
+ IEnumerable<IPackage> packages;
+
+ projectManager = new WebProjectManager(packageSource.Source, PackageManagerModule.SiteRoot);
+ // Read packages
+ switch (view) {
+ case View.Updates:
+ packages = projectManager.GetPackagesWithUpdates(searchTerm, packageSource.FilterPreferredPackages);
+ break;
+ case View.Online:
+ IQueryable<IPackage> remotePackages = projectManager.GetRemotePackages(searchTerm, packageSource.FilterPreferredPackages);
+ totalPackages = remotePackages.Count();
+ packages = WebProjectManager.CollapseVersions(remotePackages);
+ break;
+ default:
+ packages = projectManager.GetInstalledPackages(searchTerm);
+ break;
+ }
+ if (view != View.Online) {
+ totalPackages = packages.Count();
+ }
+
+ grid = new WebGrid(rowsPerPage: PackagesPerPage, pageFieldName: "page");
+
+ packages = packages.Skip(grid.PageIndex * PackagesPerPage)
+ .Take(PackagesPerPage);
+
+ grid.Bind(packages.ToList(), columnNames: Enumerable.Empty<string>(), autoSortAndPage: false, rowCount: totalPackages);
+
+ }
+ catch (Exception exception) {
+
+WriteLiteral(" <div class=\"error message\">");
+
+
+ Write(exception.Message);
+
+WriteLiteral("</div>\r\n");
+
+
+ return;
+ }
+
+
+WriteLiteral("\r\n");
+
+
+
+ var completedAction = Request.QueryString["action-completed"];
+ if (!completedAction.IsEmpty()) {
+ var packageName = Html.Encode(Request.QueryString["packageName"]);
+ string message = null;
+ if (completedAction.Equals("Install", StringComparison.OrdinalIgnoreCase)) {
+ message = String.Format(CultureInfo.CurrentCulture, PackageManagerResources.InstallSuccess, packageName);
+ }
+ else if (completedAction.Equals("Uninstall", StringComparison.OrdinalIgnoreCase)) {
+ message = String.Format(CultureInfo.CurrentCulture, PackageManagerResources.UninstallSuccess, packageName);
+ }
+ else if (completedAction.Equals("Update", StringComparison.OrdinalIgnoreCase)) {
+ message = String.Format(CultureInfo.CurrentCulture, PackageManagerResources.UpdateSuccess, packageName);
+ }
+
+ if (message != null) {
+
+WriteLiteral(" <div class=\"success message\">\r\n <img class=\"inline\" sr" +
+"c=\"");
+
+
+ Write(Href(SiteAdmin.GetVirtualPath("~/images/ok.png")));
+
+WriteLiteral("\" alt=\"");
+
+
+ Write(Html.Raw(message));
+
+WriteLiteral("\" />&nbsp;");
+
+
+ Write(Html.Raw(message));
+
+WriteLiteral("\r\n </div>\r\n");
+
+
+
+WriteLiteral(" <br />\r\n");
+
+
+ }
+ }
+
+
+WriteLiteral("<form method=\"get\" action=\"\" class=\"group\">\r\n <div class=\"left form-actions\">\r" +
+"\n <label>");
+
+
+ Write(PackageManagerResources.ShowLabel);
+
+WriteLiteral(":\r\n ");
+
+
+ Write(Html.DropDownList("view", from v in new[] { View.Installed, View.Online, View.Updates }
+ select new SelectListItem { Text = GetViewName(v), Value = v.ToString() }));
+
+WriteLiteral("\r\n </label> \r\n \r\n");
+
+
+ if (PackageManagerModule.PackageSources.Count() > 1) {
+
+
+WriteLiteral(" <span ");
+
+ if (view == View.Installed) {
+ Write(Html.Raw("style=\"display: none\""));
+
+ ;
+ }
+WriteLiteral(">\r\n &nbsp;\r\n <label>");
+
+
+ Write(PackageManagerResources.PackageSourceLabel);
+
+WriteLiteral(":</label>\r\n ");
+
+
+ Write(Html.DropDownList("source", from f in PackageManagerModule.PackageSources.OrderBy(p => p.Name)
+ select new SelectListItem {
+ Value = f.Name,
+ Text = f.Name,
+ Selected = f.Name.Equals(packageSourceName, StringComparison.OrdinalIgnoreCase)
+ }
+ ));
+
+WriteLiteral("\r\n </span>\r\n");
+
+
+ }
+
+WriteLiteral(" </div>\r\n\r\n <div class=\"right\">\r\n <fieldset class=\"no-border\">\r\n " +
+" <input type=\"text\" id=\"search\" name=\"search\" size=\"30\" value=\"");
+
+
+ Write(searchTerm);
+
+WriteLiteral("\" />\r\n <input type=\"submit\" value=\"");
+
+
+ Write(PackageManagerResources.SearchLabel);
+
+WriteLiteral("\" />\r\n <input type=\"reset\" id=\"searchReset\" value=\"");
+
+
+ Write(PackageManagerResources.ClearLabel);
+
+WriteLiteral("\" />\r\n </fieldset>\r\n </div>\r\n</form>\r\n\r\n");
+
+
+ if (view != View.Online && !projectManager.LocalRepository.GetPackages().Any()) {
+ var onlineLink = Href(PageUtils.GetPackagesHome()) + "?view=" + View.Online;
+
+Write(Html.Raw(String.Format(CultureInfo.CurrentCulture, PackageManagerResources.NoPackagesInstalled, Html.Encode(onlineLink))));
+
+
+ return;
+}
+
+WriteLiteral("\r\n");
+
+
+ if (!grid.Rows.Any()) {
+
+WriteLiteral(" <h3>");
+
+
+ Write(PackageManagerResources.NoPackagesFound);
+
+WriteLiteral("</h3>\r\n");
+
+
+}
+else {
+
+WriteLiteral(" <ul id=\"package-list\">\r\n");
+
+
+ var dataDictionary = new Dictionary<string, object>(1);
+
+
+ foreach (var item in grid.Rows) {
+ IPackage package = item.Value;
+ dataDictionary["package"] = package;
+
+WriteLiteral(" <li>\r\n <div class=\"column-left\">\r\n ");
+
+
+ Write(RenderPage("_Package.cshtml", dataDictionary));
+
+WriteLiteral("\r\n </div>\r\n <div class=\"right\">\r\n <form meth" +
+"od=\"get\" action=\"");
+
+
+ Write(Href(GetPostUrl(view, package, projectManager)));
+
+WriteLiteral("\">\r\n <input type=\"hidden\" name=\"page\" value=\"");
+
+
+ Write(grid.PageIndex + 1);
+
+WriteLiteral("\" />\r\n <input type=\"hidden\" name=\"package\" value=\"");
+
+
+ Write(package.Id);
+
+WriteLiteral("\" />\r\n <input type=\"hidden\" name=\"version\" value=\"");
+
+
+ Write(package.Version);
+
+WriteLiteral("\" />\r\n <input type=\"hidden\" name=\"packageName\" value=\"");
+
+
+ Write(package.GetDisplayName());
+
+WriteLiteral("\" />\r\n <input class=\"formatted\" type=\"submit\" value=\"");
+
+
+ Write(GetSubmitText(view, package, projectManager));
+
+WriteLiteral("\" />\r\n </form>\r\n </div>\r\n <div class=\"clear\"" +
+"></div>\r\n </li>\r\n");
+
+
+ }
+
+WriteLiteral(" </ul>\r\n");
+
+
+}
+
+
+ if (totalPackages > PackagesPerPage) {
+
+WriteLiteral(" <div class=\"pager\">\r\n <strong>");
+
+
+ Write(PackageManagerResources.PageLabel);
+
+WriteLiteral(": </strong>\r\n ");
+
+
+ Write(grid.Pager(WebGridPagerModes.FirstLast | WebGridPagerModes.NextPrevious,
+ nextText: PackageManagerResources.NextText,
+ previousText: PackageManagerResources.PreviousText));
+
+WriteLiteral("\r\n </div>\r\n");
+
+
+}
+
+WriteLiteral("\r\n\r\n");
+
+
+
+ }
+ }
+}
+#pragma warning restore 1591
diff --git a/src/System.Web.WebPages.Administration/packages/Install.cshtml b/src/System.Web.WebPages.Administration/packages/Install.cshtml
new file mode 100644
index 00000000..028f7075
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/packages/Install.cshtml
@@ -0,0 +1,91 @@
+@* Generator: WebPage *@
+
+@using System.Globalization;
+@using System.IO;
+@using System.Linq;
+@using NuGet;
+
+@section PackageHead {
+ <script type="text/javascript" src="@Href("scripts/PackageAction.js")"></script>
+ <noscript>@PackageManagerResources.JavascriptRequired</noscript>
+}
+@{
+ // Read params from request
+ var sourceName = Request["source"];
+ var packageId = Request["package"];
+ var version = Request["version"];
+
+ var packageSource = PageUtils.GetPackageSource(sourceName);
+
+ WebProjectManager projectManager;
+ try {
+ projectManager = new WebProjectManager(packageSource.Source, PackageManagerModule.SiteRoot);
+ } catch (Exception exception) {
+ <div class="error message">@exception.Message</div>
+ return;
+ }
+ IPackage package = projectManager.SourceRepository.FindPackage(packageId, version != null ? SemanticVersion.Parse(version) : null);
+
+ if (package == null) {
+ ModelState.AddFormError(PackageManagerResources.BadRequest);
+ @Html.ValidationSummary()
+ return;
+ }
+
+ Page.SectionTitle = String.Format(CultureInfo.CurrentCulture, PackageManagerResources.InstallPackageDesc, package.GetDisplayName());
+
+ var packagesHomeUrl = Href(PageUtils.GetPackagesHome(), Request.Url.Query);
+ if (IsPost) {
+ AntiForgery.Validate();
+ try {
+ projectManager.InstallPackage(package);
+ } catch (Exception exception) {
+ ModelState.AddFormError(exception.Message);
+ }
+
+ if (ModelState.IsValid) {
+ Response.Redirect(packagesHomeUrl + "&action-completed=Install");
+ }
+ else {
+ @Html.ValidationSummary(String.Format(CultureInfo.CurrentCulture, PackageManagerResources.PackageInstallationError, package.GetDisplayName()))
+ return;
+ }
+ }
+}
+
+@RenderPage("_PackageDetails.cshtml", new Dictionary<string, object>{ {"Package", package} })
+
+@{
+ var licensePackages = projectManager.GetPackagesRequiringLicenseAcceptance(package);
+ if (licensePackages.Any()) {
+ <hr />
+ <ul>
+ @foreach(var licensePackage in licensePackages.Where(p => PageUtils.IsValidLicenseUrl(p.LicenseUrl))){
+ <li>
+ <strong>@licensePackage.Id @licensePackage.Version</strong>
+ (@PackageManagerResources.AuthorsLabel: <span class="package-author">@String.Join(PackageManagerResources.WordSeparator, licensePackage.Authors)</span>)
+ <br />
+ <a href="@licensePackage.LicenseUrl" target="_blank">@PackageManagerResources.ViewLicenseTerms</a>
+ </li>
+ }
+ </ul>
+ }else {
+ <br />
+ <hr />
+ }
+}
+
+<form method="post" action="" id="submitForm">
+<p>@PackageManagerResources.Disclaimer</p>
+<fieldset class="no-border install">
+ <input type="hidden" name="source" value="@sourceName" />
+ <input type="hidden" name="package" value="@packageId" />
+ <input type="hidden" name="version" value="@version" />
+ @AntiForgery.GetHtml()
+
+ <input type="submit" value="@PackageManagerResources.InstallPackage" />
+ <input type="reset" value="@PackageManagerResources.Cancel" data-returnurl="@packagesHomeUrl" />
+</fieldset>
+
+
+</form> \ No newline at end of file
diff --git a/src/System.Web.WebPages.Administration/packages/Install.generated.cs b/src/System.Web.WebPages.Administration/packages/Install.generated.cs
new file mode 100644
index 00000000..c5f2ec6c
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/packages/Install.generated.cs
@@ -0,0 +1,259 @@
+#pragma warning disable 1591
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.225
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace System.Web.WebPages.Administration.PackageManager
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+ using System.Net;
+ using System.Web;
+ using System.Web.Helpers;
+ using System.Web.Security;
+ using System.Web.UI;
+ using System.Web.WebPages;
+ using System.Web.WebPages.Html;
+ using System.Globalization;
+ using NuGet;
+
+ [System.Web.WebPages.PageVirtualPathAttribute("~/packages/Install.cshtml")]
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorSingleFileGenerator", "0.6.0.0")]
+ public class packages_Install_cshtml : System.Web.WebPages.WebPage
+ {
+
+
+ // Resolve package relative syntax
+ // Also, if it comes from a static embedded resource, change the path accordingly
+ public override string Href(string virtualPath, params object[] pathParts) {
+ virtualPath = ApplicationPart.ProcessVirtualPath(GetType().Assembly, VirtualPath, virtualPath);
+ return base.Href(virtualPath, pathParts);
+ }
+ public packages_Install_cshtml()
+ {
+ }
+ protected System.Web.HttpApplication ApplicationInstance
+ {
+ get
+ {
+ return ((System.Web.HttpApplication)(Context.ApplicationInstance));
+ }
+ }
+ public override void Execute()
+ {
+
+
+WriteLiteral("\r\n\r\n");
+
+
+
+
+
+WriteLiteral("\r\n");
+
+
+DefineSection("PackageHead", () => {
+
+WriteLiteral(" \r\n <script type=\"text/javascript\" src=\"");
+
+
+ Write(Href("scripts/PackageAction.js"));
+
+WriteLiteral("\"></script>\r\n <noscript>");
+
+
+ Write(PackageManagerResources.JavascriptRequired);
+
+WriteLiteral("</noscript>\r\n");
+
+
+});
+
+WriteLiteral("\r\n");
+
+
+
+ // Read params from request
+ var sourceName = Request["source"];
+ var packageId = Request["package"];
+ var version = Request["version"];
+
+ var packageSource = PageUtils.GetPackageSource(sourceName);
+
+ WebProjectManager projectManager;
+ try {
+ projectManager = new WebProjectManager(packageSource.Source, PackageManagerModule.SiteRoot);
+ } catch (Exception exception) {
+
+WriteLiteral(" <div class=\"error message\">");
+
+
+ Write(exception.Message);
+
+WriteLiteral("</div>\r\n");
+
+
+ return;
+ }
+ IPackage package = projectManager.SourceRepository.FindPackage(packageId, version != null ? SemanticVersion.Parse(version) : null);
+
+ if (package == null) {
+ ModelState.AddFormError(PackageManagerResources.BadRequest);
+
+ Write(Html.ValidationSummary());
+
+
+ return;
+ }
+
+ Page.SectionTitle = String.Format(CultureInfo.CurrentCulture, PackageManagerResources.InstallPackageDesc, package.GetDisplayName());
+
+ var packagesHomeUrl = Href(PageUtils.GetPackagesHome(), Request.Url.Query);
+ if (IsPost) {
+ AntiForgery.Validate();
+ try {
+ projectManager.InstallPackage(package);
+ } catch (Exception exception) {
+ ModelState.AddFormError(exception.Message);
+ }
+
+ if (ModelState.IsValid) {
+ Response.Redirect(packagesHomeUrl + "&action-completed=Install");
+ }
+ else {
+
+ Write(Html.ValidationSummary(String.Format(CultureInfo.CurrentCulture, PackageManagerResources.PackageInstallationError, package.GetDisplayName())));
+
+
+ return;
+ }
+ }
+
+
+WriteLiteral("\r\n");
+
+
+Write(RenderPage("_PackageDetails.cshtml", new Dictionary<string, object>{ {"Package", package} }));
+
+WriteLiteral("\r\n\r\n");
+
+
+
+ var licensePackages = projectManager.GetPackagesRequiringLicenseAcceptance(package);
+ if (licensePackages.Any()) {
+
+WriteLiteral(" <hr />\r\n");
+
+
+
+WriteLiteral(" <ul>\r\n");
+
+
+ foreach(var licensePackage in licensePackages.Where(p => PageUtils.IsValidLicenseUrl(p.LicenseUrl))){
+
+WriteLiteral(" <li>\r\n <strong>");
+
+
+ Write(licensePackage.Id);
+
+WriteLiteral(" ");
+
+
+ Write(licensePackage.Version);
+
+WriteLiteral("</strong> \r\n (");
+
+
+ Write(PackageManagerResources.AuthorsLabel);
+
+WriteLiteral(": <span class=\"package-author\">");
+
+
+ Write(String.Join(PackageManagerResources.WordSeparator, licensePackage.Authors));
+
+WriteLiteral("</span>)\r\n <br />\r\n <a href=\"");
+
+
+ Write(licensePackage.LicenseUrl);
+
+WriteLiteral("\" target=\"_blank\">");
+
+
+ Write(PackageManagerResources.ViewLicenseTerms);
+
+WriteLiteral("</a>\r\n </li>\r\n");
+
+
+ }
+
+WriteLiteral(" </ul> \r\n");
+
+
+ }else {
+
+WriteLiteral(" <br />\r\n");
+
+
+
+WriteLiteral(" <hr />\r\n");
+
+
+ }
+
+
+WriteLiteral("\r\n<form method=\"post\" action=\"\" id=\"submitForm\">\r\n<p>");
+
+
+Write(PackageManagerResources.Disclaimer);
+
+WriteLiteral("</p> \r\n<fieldset class=\"no-border install\">\r\n <input type=\"hidden\" name=\"so" +
+"urce\" value=\"");
+
+
+ Write(sourceName);
+
+WriteLiteral("\" />\r\n <input type=\"hidden\" name=\"package\" value=\"");
+
+
+ Write(packageId);
+
+WriteLiteral("\" />\r\n <input type=\"hidden\" name=\"version\" value=\"");
+
+
+ Write(version);
+
+WriteLiteral("\" />\r\n ");
+
+
+Write(AntiForgery.GetHtml());
+
+WriteLiteral("\r\n\r\n <input type=\"submit\" value=\"");
+
+
+ Write(PackageManagerResources.InstallPackage);
+
+WriteLiteral("\" />\r\n <input type=\"reset\" value=\"");
+
+
+ Write(PackageManagerResources.Cancel);
+
+WriteLiteral("\" data-returnurl=\"");
+
+
+ Write(packagesHomeUrl);
+
+WriteLiteral("\" />\r\n</fieldset>\r\n \r\n\r\n</form>");
+
+
+ }
+ }
+}
+#pragma warning restore 1591
diff --git a/src/System.Web.WebPages.Administration/packages/PackageSources.cshtml b/src/System.Web.WebPages.Administration/packages/PackageSources.cshtml
new file mode 100644
index 00000000..aabb95ed
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/packages/PackageSources.cshtml
@@ -0,0 +1,111 @@
+@* Generator: WebPage *@
+
+@using System.Globalization;
+@{
+ // Setup layout
+ var currentPage = Href(PageUtils.GetPageVirtualPath("PackageSources"));
+ PageData["BreadCrumbs"].Add(Tuple.Create(PackageManagerResources.ManageSourcesTitle, currentPage));
+ Page.Desc = PackageManagerResources.ManageSourcesDesc;
+ Page.SectionTitle = PackageManagerResources.ManageSourcesTitle;
+
+ if (IsPost) {
+ AntiForgery.Validate();
+ var action = Request.Form["action"];
+ var sourceUrl = Request.Form["sourceUrl"];
+ var sourceName = Request.Form["sourceName"];
+ try {
+ if (action.Equals(PackageManagerResources.AddPackageSourceLabel, StringComparison.OrdinalIgnoreCase)) {
+
+ ModelState.SetModelValue("sourceName", sourceName);
+ ModelState.SetModelValue("sourceUrl", sourceUrl);
+
+ Uri url;
+ if (!Uri.TryCreate(sourceUrl, UriKind.Absolute, out url)) {
+ ModelState.AddError("sourceUrl", PackageManagerResources.Validation_InvalidPackageSourceUrl);
+ } else if (!PackageManagerModule.AddPackageSource(source: sourceUrl, name: sourceName)) {
+ ModelState.AddError("sourceName", PackageManagerResources.Validation_PackageSourceAlreadyExists);
+ } else {
+ // The feed was successfully added. Clear the model state.
+ ModelState.Clear();
+ }
+ }
+ else if (action.Equals(PackageManagerResources.DeleteLabel, StringComparison.OrdinalIgnoreCase)) {
+ PackageManagerModule.RemovePackageSource(sourceName);
+ }
+ else if (action.Equals(PackageManagerResources.RestoreDefaultSources, StringComparison.OrdinalIgnoreCase)) {
+ foreach(var packageSource in PackageManagerModule.DefaultSources) {
+ PackageManagerModule.AddPackageSource(packageSource);
+ }
+ }
+ } catch (UnauthorizedAccessException) {
+ <div class="message error">
+ @String.Format(CultureInfo.CurrentCulture, PackageManagerResources.PackageSourceFileInstructions, PackageManagerModule.PackageSourceFilePath)
+ </div>
+ }
+ }
+
+ var numSources = PackageManagerModule.PackageSources.Count();
+}
+
+@Html.ValidationSummary(excludeFieldErrors: true)
+
+<table id="feeds">
+<thead>
+ <tr>
+ <th scope="col">@PackageManagerResources.SourceNameLabel</th>
+ <th scope="col">@PackageManagerResources.SourceUrlLabel</th>
+ <th></th>
+ </tr>
+</thead>
+<tbody>
+ @foreach(var source in PackageManagerModule.PackageSources) {
+ <tr>
+ <td>@source.Name</td>
+ <td><a href="@source.Source">@source.Source</a></td>
+ <td>
+ @if(numSources > 1) {
+ <form method="post" action="">
+ <input type="hidden" name="sourceName" value="@source.Name" />
+ <input type="submit" name="action" value="@PackageManagerResources.DeleteLabel" />
+ @AntiForgery.GetHtml()
+ </form>
+ }
+ </td>
+ </tr>
+ }
+</tbody>
+</table>
+<br />
+<form method="post" action="">
+@AntiForgery.GetHtml()
+<fieldset>
+ <legend>@PackageManagerResources.AddPackageSourceLabel</legend>
+ <ol>
+ <li>
+ <label for="feedName">@PackageManagerResources.SourceNameLabel:</label>
+ @Html.TextBox("sourceName") @Html.ValidationMessage("sourceName")
+ </li>
+ <li>
+ <label for="feedUrl">@PackageManagerResources.SourceUrlLabel:</label>
+ @Html.TextBox("sourceUrl") @Html.ValidationMessage("sourceUrl")
+ </li>
+ </ol>
+ <p class="form-actions">
+
+ <input type="submit" name="action" class="long-input" value="@PackageManagerResources.AddPackageSourceLabel" />
+ </p>
+</fieldset>
+</form>
+
+@{
+ if (PackageManagerModule.DefaultSources.Intersect(PackageManagerModule.PackageSources).Count() != PackageManagerModule.DefaultSources.Count()) {
+ <p>
+ <form method="post" action="">
+ @AntiForgery.GetHtml()
+ <fieldset class="no-border">
+ <input type="submit" name="action" class="long-input" value="@PackageManagerResources.RestoreDefaultSources" />
+ </fieldset>
+ </form>
+ </p>
+ }
+}
diff --git a/src/System.Web.WebPages.Administration/packages/PackageSources.generated.cs b/src/System.Web.WebPages.Administration/packages/PackageSources.generated.cs
new file mode 100644
index 00000000..90ebb7fb
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/packages/PackageSources.generated.cs
@@ -0,0 +1,248 @@
+#pragma warning disable 1591
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.225
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace System.Web.WebPages.Administration.PackageManager
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+ using System.Net;
+ using System.Web;
+ using System.Web.Helpers;
+ using System.Web.Security;
+ using System.Web.UI;
+ using System.Web.WebPages;
+ using System.Web.WebPages.Html;
+ using System.Globalization;
+
+ [System.Web.WebPages.PageVirtualPathAttribute("~/packages/PackageSources.cshtml")]
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorSingleFileGenerator", "1.0.0.0")]
+ public class packages_PackageSources_cshtml : System.Web.WebPages.WebPage
+ {
+#line hidden
+
+ // Resolve package relative syntax
+ // Also, if it comes from a static embedded resource, change the path accordingly
+ public override string Href(string virtualPath, params object[] pathParts) {
+ virtualPath = ApplicationPart.ProcessVirtualPath(GetType().Assembly, VirtualPath, virtualPath);
+ return base.Href(virtualPath, pathParts);
+ }
+ public packages_PackageSources_cshtml()
+ {
+ }
+ protected System.Web.HttpApplication ApplicationInstance
+ {
+ get
+ {
+ return ((System.Web.HttpApplication)(Context.ApplicationInstance));
+ }
+ }
+ public override void Execute()
+ {
+
+
+WriteLiteral("\r\n\r\n");
+
+
+
+
+ // Setup layout
+ var currentPage = Href(PageUtils.GetPageVirtualPath("PackageSources"));
+ PageData["BreadCrumbs"].Add(Tuple.Create(PackageManagerResources.ManageSourcesTitle, currentPage));
+ Page.Desc = PackageManagerResources.ManageSourcesDesc;
+ Page.SectionTitle = PackageManagerResources.ManageSourcesTitle;
+
+ if (IsPost) {
+ AntiForgery.Validate();
+ var action = Request.Form["action"];
+ var sourceUrl = Request.Form["sourceUrl"];
+ var sourceName = Request.Form["sourceName"];
+ try {
+ if (action.Equals(PackageManagerResources.AddPackageSourceLabel, StringComparison.OrdinalIgnoreCase)) {
+
+ ModelState.SetModelValue("sourceName", sourceName);
+ ModelState.SetModelValue("sourceUrl", sourceUrl);
+
+ Uri url;
+ if (!Uri.TryCreate(sourceUrl, UriKind.Absolute, out url)) {
+ ModelState.AddError("sourceUrl", PackageManagerResources.Validation_InvalidPackageSourceUrl);
+ } else if (!PackageManagerModule.AddPackageSource(source: sourceUrl, name: sourceName)) {
+ ModelState.AddError("sourceName", PackageManagerResources.Validation_PackageSourceAlreadyExists);
+ } else {
+ // The feed was successfully added. Clear the model state.
+ ModelState.Clear();
+ }
+ }
+ else if (action.Equals(PackageManagerResources.DeleteLabel, StringComparison.OrdinalIgnoreCase)) {
+ PackageManagerModule.RemovePackageSource(sourceName);
+ }
+ else if (action.Equals(PackageManagerResources.RestoreDefaultSources, StringComparison.OrdinalIgnoreCase)) {
+ foreach(var packageSource in PackageManagerModule.DefaultSources) {
+ PackageManagerModule.AddPackageSource(packageSource);
+ }
+ }
+ } catch (UnauthorizedAccessException) {
+
+WriteLiteral(" <div class=\"message error\">\r\n ");
+
+
+ Write(String.Format(CultureInfo.CurrentCulture, PackageManagerResources.PackageSourceFileInstructions, PackageManagerModule.PackageSourceFilePath));
+
+WriteLiteral("\r\n </div>\r\n");
+
+
+ }
+ }
+
+ var numSources = PackageManagerModule.PackageSources.Count();
+
+
+WriteLiteral("\r\n");
+
+
+Write(Html.ValidationSummary(excludeFieldErrors: true));
+
+WriteLiteral("\r\n\r\n<table id=\"feeds\">\r\n<thead>\r\n <tr>\r\n <th scope=\"col\">");
+
+
+ Write(PackageManagerResources.SourceNameLabel);
+
+WriteLiteral("</th>\r\n <th scope=\"col\">");
+
+
+ Write(PackageManagerResources.SourceUrlLabel);
+
+WriteLiteral("</th>\r\n <th></th>\r\n </tr>\r\n</thead>\r\n<tbody> \r\n");
+
+
+ foreach(var source in PackageManagerModule.PackageSources) {
+
+WriteLiteral(" <tr>\r\n <td>");
+
+
+ Write(source.Name);
+
+WriteLiteral("</td>\r\n <td><a href=\"");
+
+
+ Write(source.Source);
+
+WriteLiteral("\">");
+
+
+ Write(source.Source);
+
+WriteLiteral("</a></td>\r\n <td>\r\n");
+
+
+ if(numSources > 1) {
+
+WriteLiteral(" <form method=\"post\" action=\"\">\r\n <input type=\"" +
+"hidden\" name=\"sourceName\" value=\"");
+
+
+ Write(source.Name);
+
+WriteLiteral("\" />\r\n <input type=\"submit\" name=\"action\" value=\"");
+
+
+ Write(PackageManagerResources.DeleteLabel);
+
+WriteLiteral("\" />\r\n ");
+
+
+ Write(AntiForgery.GetHtml());
+
+WriteLiteral("\r\n </form>\r\n");
+
+
+ }
+
+WriteLiteral(" </td>\r\n </tr>\r\n");
+
+
+ }
+
+WriteLiteral("</tbody>\r\n</table>\r\n<br />\r\n<form method=\"post\" action=\"\">\r\n");
+
+
+Write(AntiForgery.GetHtml());
+
+WriteLiteral("\r\n<fieldset>\r\n <legend>");
+
+
+ Write(PackageManagerResources.AddPackageSourceLabel);
+
+WriteLiteral("</legend>\r\n <ol>\r\n <li>\r\n <label for=\"feedName\">");
+
+
+ Write(PackageManagerResources.SourceNameLabel);
+
+WriteLiteral(":</label>\r\n ");
+
+
+ Write(Html.TextBox("sourceName"));
+
+WriteLiteral(" ");
+
+
+ Write(Html.ValidationMessage("sourceName"));
+
+WriteLiteral("\r\n </li>\r\n <li>\r\n <label for=\"feedUrl\">");
+
+
+ Write(PackageManagerResources.SourceUrlLabel);
+
+WriteLiteral(":</label>\r\n ");
+
+
+ Write(Html.TextBox("sourceUrl"));
+
+WriteLiteral(" ");
+
+
+ Write(Html.ValidationMessage("sourceUrl"));
+
+WriteLiteral("\r\n </li>\r\n </ol>\r\n <p class=\"form-actions\">\r\n \r\n <input ty" +
+"pe=\"submit\" name=\"action\" class=\"long-input\" value=\"");
+
+
+ Write(PackageManagerResources.AddPackageSourceLabel);
+
+WriteLiteral("\" />\r\n </p>\r\n</fieldset>\r\n</form>\r\n\r\n");
+
+
+
+ if (PackageManagerModule.DefaultSources.Intersect(PackageManagerModule.PackageSources).Count() != PackageManagerModule.DefaultSources.Count()) {
+
+WriteLiteral(" <p>\r\n <form method=\"post\" action=\"\">\r\n ");
+
+
+ Write(AntiForgery.GetHtml());
+
+WriteLiteral("\r\n <fieldset class=\"no-border\"> \r\n <input type=\"submit" +
+"\" name=\"action\" class=\"long-input\" value=\"");
+
+
+ Write(PackageManagerResources.RestoreDefaultSources);
+
+WriteLiteral("\" />\r\n </fieldset>\r\n </form>\r\n </p>\r\n");
+
+
+ }
+
+
+
+ }
+ }
+}
+#pragma warning restore 1591
diff --git a/src/System.Web.WebPages.Administration/packages/Site.css b/src/System.Web.WebPages.Administration/packages/Site.css
new file mode 100644
index 00000000..8131a2a4
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/packages/Site.css
@@ -0,0 +1,127 @@
+.validation-summary-errors li {
+ list-style-image: url(images/error.png);
+}
+
+.package-description {
+ padding: 0px;
+ margin: 5px 5px 3px 0px;
+}
+
+.package {
+ list-style-image: url(images/package.png);
+ padding-left: 10px;
+ margin-left: 10px;
+ margin-bottom: 5px;
+}
+
+.centered {
+ text-align: center;
+}
+
+.nav li {
+ font-size: 16px;
+}
+
+#package-name {
+ font-weight: bold;
+}
+
+.package-author {
+ font-style: oblique;
+}
+
+#package-list {
+ width: 100%;
+ border-collapse: collapse;
+ list-style-type: none;
+}
+
+#package-list li {
+ margin-bottom: 10px;
+}
+
+#package-list li:last {
+ margin-bottom: 0;
+}
+
+.column-left {
+ width: 90%;
+ float: left;
+}
+
+.column-right {
+ width: 10%;
+ float: left;
+}
+
+.package-info h4 {
+ background: url("images/package.png") no-repeat scroll 0 50% transparent;
+ padding-left: 30px;
+ font-size: 16px;
+ line-height: 24px;
+}
+
+.package-info h4:hover {
+ text-decoration: underline;
+ cursor: pointer;
+}
+
+.pager {
+ font-size: 16px;
+ font-weight: bold;
+ margin: 20px auto;
+}
+
+.pager a, .pager span {
+ margin: 0 2px;
+ padding: 3px;
+}
+
+.pager span {
+ background-color: #699;
+ color: #fff;
+}
+
+.pager a, .pager a:hover {
+ text-decoration: none;
+}
+
+.nav li {
+ margin-bottom: 5px;
+}
+
+fieldset.no-border {
+ border: none;
+ margin: 10px 0;
+ padding: 10px 0;
+}
+
+.message h4 {
+ margin-top: 0;
+}
+
+.package-details .package-info {
+ width: 60%;
+}
+
+.notice {
+ font-size: 0.9em;
+}
+
+#searchForm fieldset {
+ margin: 0;
+ padding: 0;
+}
+
+#searchForm input[type="text"] {
+ width: auto;
+}
+
+fieldset.install {
+ margin: 0;
+ padding: 5px;
+}
+
+table#feed th, td {
+ padding: 4px;
+}
diff --git a/src/System.Web.WebPages.Administration/packages/SourceFileInstructions.cshtml b/src/System.Web.WebPages.Administration/packages/SourceFileInstructions.cshtml
new file mode 100644
index 00000000..58dfe519
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/packages/SourceFileInstructions.cshtml
@@ -0,0 +1,7 @@
+@* Generator: WebPage *@
+
+@using System.Globalization;
+
+<div class="message error">
+ @String.Format(CultureInfo.CurrentCulture, PackageManagerResources.PackageSourceFileInstructions, PackageManagerModule.PackageSourceFilePath)
+</div> \ No newline at end of file
diff --git a/src/System.Web.WebPages.Administration/packages/SourceFileInstructions.generated.cs b/src/System.Web.WebPages.Administration/packages/SourceFileInstructions.generated.cs
new file mode 100644
index 00000000..c7f430df
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/packages/SourceFileInstructions.generated.cs
@@ -0,0 +1,67 @@
+#pragma warning disable 1591
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.225
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace System.Web.WebPages.Administration.PackageManager
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+ using System.Net;
+ using System.Web;
+ using System.Web.Helpers;
+ using System.Web.Security;
+ using System.Web.UI;
+ using System.Web.WebPages;
+ using System.Web.WebPages.Html;
+ using System.Globalization;
+
+ [System.Web.WebPages.PageVirtualPathAttribute("~/packages/SourceFileInstructions.cshtml")]
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorSingleFileGenerator", "1.0.0.0")]
+ public class packages_SourceFileInstructions_cshtml : System.Web.WebPages.WebPage
+ {
+#line hidden
+
+ // Resolve package relative syntax
+ // Also, if it comes from a static embedded resource, change the path accordingly
+ public override string Href(string virtualPath, params object[] pathParts) {
+ virtualPath = ApplicationPart.ProcessVirtualPath(GetType().Assembly, VirtualPath, virtualPath);
+ return base.Href(virtualPath, pathParts);
+ }
+ public packages_SourceFileInstructions_cshtml()
+ {
+ }
+ protected System.Web.HttpApplication ApplicationInstance
+ {
+ get
+ {
+ return ((System.Web.HttpApplication)(Context.ApplicationInstance));
+ }
+ }
+ public override void Execute()
+ {
+
+
+WriteLiteral("\r\n\r\n");
+
+
+WriteLiteral("\r\n<div class=\"message error\">\r\n ");
+
+
+Write(String.Format(CultureInfo.CurrentCulture, PackageManagerResources.PackageSourceFileInstructions, PackageManagerModule.PackageSourceFilePath));
+
+WriteLiteral("\r\n</div>");
+
+
+ }
+ }
+}
+#pragma warning restore 1591
diff --git a/src/System.Web.WebPages.Administration/packages/Uninstall.cshtml b/src/System.Web.WebPages.Administration/packages/Uninstall.cshtml
new file mode 100644
index 00000000..44a2a992
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/packages/Uninstall.cshtml
@@ -0,0 +1,74 @@
+@* Generator: WebPage *@
+
+@using System.Globalization;
+@using System.Text.RegularExpressions;
+@using NuGet;
+
+@section PackageHead {
+ <script type="text/javascript" src="@Href("scripts/PackageAction.js")"></script>
+ <noscript>@PackageManagerResources.JavascriptRequired</noscript>
+}
+
+@{
+ // Read from request
+ var packageId = Request["package"];
+ var version = Request["version"];
+
+ WebProjectManager projectManager;
+ try {
+ projectManager = new WebProjectManager(PackageManagerModule.ActiveSource.Source, PackageManagerModule.SiteRoot);
+ } catch (Exception exception) {
+ <div class="error message">@exception.Message</div>
+ return;
+ }
+
+ IPackage package = projectManager.LocalRepository.FindPackage(packageId, version != null ? SemanticVersion.Parse(version) : null);
+
+ if (package == null) {
+ ModelState.AddFormError(PackageManagerResources.BadRequest);
+ @Html.ValidationSummary()
+ return;
+ }
+
+ // Set up layout values
+ var packagesHomeUrl = Href(PageUtils.GetPackagesHome(), Request.Url.Query);
+ Page.SectionTitle = String.Format(CultureInfo.CurrentCulture, PackageManagerResources.UninstallPackageDesc, package.GetDisplayName());
+
+ if (IsPost) {
+ AntiForgery.Validate();
+ bool removeDependencies = Request.Form["removeDependencies"].AsBool(false);
+ try {
+ projectManager.UninstallPackage(package, removeDependencies: removeDependencies);
+ } catch (Exception exception) {
+ ModelState.AddFormError(exception.Message);
+ }
+
+ if (ModelState.IsValid) {
+ Response.Redirect(packagesHomeUrl + "&action-completed=Uninstall");
+ }
+ else {
+ @Html.ValidationSummary(String.Format(CultureInfo.CurrentCulture, PackageManagerResources.PackageUninstallationError, package.GetDisplayName()))
+ }
+ return;
+ }
+}
+@{
+ var encodedPackageName = Html.Encode(package.GetDisplayName());
+ <h4>@Html.Raw(String.Format(CultureInfo.CurrentCulture, PackageManagerResources.AreYouSureUninstall, encodedPackageName))</h4>
+}
+<form method="post" action="" id="submitForm">
+<fieldset class="no-border">
+ @AntiForgery.GetHtml()
+ <input type="hidden" name="package" value="@packageId" />
+ <input type="hidden" name="version" value="@version" />
+ @if (package.Dependencies.Any()) {
+ <div>
+ <label><input type="checkbox" name="removeDependencies" value="true" checked="checked"/>@PackageManagerResources.RemoveDependencies</label>
+ </div>
+ <br />
+ }
+ <input type="submit" value="@PackageManagerResources.UninstallPackage" />
+ &nbsp;
+ <input type="reset" value="@PackageManagerResources.Cancel" data-returnurl="@packagesHomeUrl" />
+</fieldset>
+</form> \ No newline at end of file
diff --git a/src/System.Web.WebPages.Administration/packages/Uninstall.generated.cs b/src/System.Web.WebPages.Administration/packages/Uninstall.generated.cs
new file mode 100644
index 00000000..71100ee2
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/packages/Uninstall.generated.cs
@@ -0,0 +1,212 @@
+#pragma warning disable 1591
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.225
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace System.Web.WebPages.Administration.PackageManager
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+ using System.Net;
+ using System.Web;
+ using System.Web.Helpers;
+ using System.Web.Security;
+ using System.Web.UI;
+ using System.Web.WebPages;
+ using System.Web.WebPages.Html;
+ using System.Globalization;
+ using System.Text.RegularExpressions;
+ using NuGet;
+
+ [System.Web.WebPages.PageVirtualPathAttribute("~/packages/Uninstall.cshtml")]
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorSingleFileGenerator", "1.0.0.0")]
+ public class packages_Uninstall_cshtml : System.Web.WebPages.WebPage
+ {
+#line hidden
+
+ // Resolve package relative syntax
+ // Also, if it comes from a static embedded resource, change the path accordingly
+ public override string Href(string virtualPath, params object[] pathParts) {
+ virtualPath = ApplicationPart.ProcessVirtualPath(GetType().Assembly, VirtualPath, virtualPath);
+ return base.Href(virtualPath, pathParts);
+ }
+ public packages_Uninstall_cshtml()
+ {
+ }
+ protected System.Web.HttpApplication ApplicationInstance
+ {
+ get
+ {
+ return ((System.Web.HttpApplication)(Context.ApplicationInstance));
+ }
+ }
+ public override void Execute()
+ {
+
+
+WriteLiteral("\r\n\r\n");
+
+
+
+
+WriteLiteral("\r\n");
+
+
+DefineSection("PackageHead", () => {
+
+WriteLiteral(" \r\n <script type=\"text/javascript\" src=\"");
+
+
+ Write(Href("scripts/PackageAction.js"));
+
+WriteLiteral("\"></script>\r\n <noscript>");
+
+
+ Write(PackageManagerResources.JavascriptRequired);
+
+WriteLiteral("</noscript>\r\n");
+
+
+});
+
+WriteLiteral("\r\n\r\n");
+
+
+
+ // Read from request
+ var packageId = Request["package"];
+ var version = Request["version"];
+
+ WebProjectManager projectManager;
+ try {
+ projectManager = new WebProjectManager(PackageManagerModule.ActiveSource.Source, PackageManagerModule.SiteRoot);
+ } catch (Exception exception) {
+
+WriteLiteral(" <div class=\"error message\">");
+
+
+ Write(exception.Message);
+
+WriteLiteral("</div>\r\n");
+
+
+ return;
+ }
+
+ IPackage package = projectManager.LocalRepository.FindPackage(packageId, version != null ? SemanticVersion.Parse(version) : null);
+
+ if (package == null) {
+ ModelState.AddFormError(PackageManagerResources.BadRequest);
+
+ Write(Html.ValidationSummary());
+
+
+ return;
+ }
+
+ // Set up layout values
+ var packagesHomeUrl = Href(PageUtils.GetPackagesHome(), Request.Url.Query);
+ Page.SectionTitle = String.Format(CultureInfo.CurrentCulture, PackageManagerResources.UninstallPackageDesc, package.GetDisplayName());
+
+ if (IsPost) {
+ AntiForgery.Validate();
+ bool removeDependencies = Request.Form["removeDependencies"].AsBool(false);
+ try {
+ projectManager.UninstallPackage(package, removeDependencies: removeDependencies);
+ } catch (Exception exception) {
+ ModelState.AddFormError(exception.Message);
+ }
+
+ if (ModelState.IsValid) {
+ Response.Redirect(packagesHomeUrl + "&action-completed=Uninstall");
+ }
+ else {
+
+ Write(Html.ValidationSummary(String.Format(CultureInfo.CurrentCulture, PackageManagerResources.PackageUninstallationError, package.GetDisplayName())));
+
+
+ }
+ return;
+ }
+
+
+
+
+ var encodedPackageName = Html.Encode(package.GetDisplayName());
+
+WriteLiteral(" <h4>");
+
+
+ Write(Html.Raw(String.Format(CultureInfo.CurrentCulture, PackageManagerResources.AreYouSureUninstall, encodedPackageName)));
+
+WriteLiteral("</h4>\r\n");
+
+
+
+
+WriteLiteral("<form method=\"post\" action=\"\" id=\"submitForm\">\r\n<fieldset class=\"no-border\">\r\n " +
+" ");
+
+
+Write(AntiForgery.GetHtml());
+
+WriteLiteral("\r\n <input type=\"hidden\" name=\"package\" value=\"");
+
+
+ Write(packageId);
+
+WriteLiteral("\" />\r\n <input type=\"hidden\" name=\"version\" value=\"");
+
+
+ Write(version);
+
+WriteLiteral("\" />\r\n");
+
+
+ if (package.Dependencies.Any()) {
+
+WriteLiteral(" <div>\r\n <label><input type=\"checkbox\" name=\"removeDependencies" +
+"\" value=\"true\" checked=\"checked\"/>");
+
+
+ Write(PackageManagerResources.RemoveDependencies);
+
+WriteLiteral("</label>\r\n </div>\r\n");
+
+
+
+WriteLiteral(" <br />\r\n");
+
+
+ }
+
+WriteLiteral(" <input type=\"submit\" value=\"");
+
+
+ Write(PackageManagerResources.UninstallPackage);
+
+WriteLiteral("\" />\r\n &nbsp;\r\n <input type=\"reset\" value=\"");
+
+
+ Write(PackageManagerResources.Cancel);
+
+WriteLiteral("\" data-returnurl=\"");
+
+
+ Write(packagesHomeUrl);
+
+WriteLiteral("\" />\r\n</fieldset>\r\n</form>");
+
+
+ }
+ }
+}
+#pragma warning restore 1591
diff --git a/src/System.Web.WebPages.Administration/packages/Update.cshtml b/src/System.Web.WebPages.Administration/packages/Update.cshtml
new file mode 100644
index 00000000..2ec0c22f
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/packages/Update.cshtml
@@ -0,0 +1,69 @@
+@* Generator: WebPage *@
+
+@using System.Globalization;
+@using System.IO;
+@using NuGet;
+
+@section PackageHead {
+ <script type="text/javascript" src="@Href("scripts/PackageAction.js")"></script>
+ <noscript>@PackageManagerResources.JavascriptRequired</noscript>
+}
+@{
+ // Read params from request
+ var sourceName = Request["source"];
+ var packageId = Request["package"];
+ var versionString = Request["version"];
+ var packageSource = PageUtils.GetPackageSource(sourceName);
+
+ var version = !versionString.IsEmpty() ? SemanticVersion.Parse(versionString) : null;
+
+ WebProjectManager projectManager;
+ try {
+ projectManager = new WebProjectManager(packageSource.Source, PackageManagerModule.SiteRoot);
+ } catch (Exception exception) {
+ <div class="error message">@exception.Message</div>
+ return;
+ }
+ var updatePackage = projectManager.SourceRepository.FindPackage(packageId, version);
+ if (updatePackage == null) {
+ ModelState.AddFormError(PackageManagerResources.BadRequest);
+ @Html.ValidationSummary()
+ return;
+ }
+
+ var package = projectManager.LocalRepository.FindPackage(packageId);
+
+ // Layout
+ Page.SectionTitle = String.Format(CultureInfo.CurrentCulture, PackageManagerResources.UpdatePackageDesc, package.GetDisplayName(), updatePackage.Version);
+ var packagesHomeUrl = Href(PageUtils.GetPackagesHome(), Request.Url.Query);
+
+ if (IsPost) {
+ AntiForgery.Validate();
+ try {
+ projectManager.UpdatePackage(updatePackage);
+ } catch (Exception exception) {
+ ModelState.AddFormError(exception.Message);
+ }
+
+ if (ModelState.IsValid) {
+ Response.Redirect(packagesHomeUrl + "&action-completed=Update");
+ }
+ else {
+ @Html.ValidationSummary(String.Format(CultureInfo.CurrentCulture, PackageManagerResources.PackageUpdateError, package.GetDisplayName()))
+ }
+ return;
+ }
+}
+
+@RenderPage("_PackageDetails.cshtml", new Dictionary<string, object>{ {"Package", updatePackage} })
+<br />
+<form method="post" action="" id="submitForm">
+ @AntiForgery.GetHtml()
+ <input type="hidden" name="source" value="@sourceName" />
+ <input type="hidden" name="package" value="@packageId" />
+ <input type="hidden" name="version" value="@version" />
+
+ <input type="submit" value="@PackageManagerResources.UpdatePackage" />
+ <input type="reset" value="@PackageManagerResources.Cancel" data-returnurl="@packagesHomeUrl" />
+ <br /><br />
+</form> \ No newline at end of file
diff --git a/src/System.Web.WebPages.Administration/packages/Update.generated.cs b/src/System.Web.WebPages.Administration/packages/Update.generated.cs
new file mode 100644
index 00000000..7418c973
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/packages/Update.generated.cs
@@ -0,0 +1,189 @@
+#pragma warning disable 1591
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.225
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace System.Web.WebPages.Administration.PackageManager
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+ using System.Net;
+ using System.Web;
+ using System.Web.Helpers;
+ using System.Web.Security;
+ using System.Web.UI;
+ using System.Web.WebPages;
+ using System.Web.WebPages.Html;
+ using System.Globalization;
+ using NuGet;
+
+ [System.Web.WebPages.PageVirtualPathAttribute("~/packages/Update.cshtml")]
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorSingleFileGenerator", "1.0.0.0")]
+ public class packages_Update_cshtml : System.Web.WebPages.WebPage
+ {
+#line hidden
+
+ // Resolve package relative syntax
+ // Also, if it comes from a static embedded resource, change the path accordingly
+ public override string Href(string virtualPath, params object[] pathParts) {
+ virtualPath = ApplicationPart.ProcessVirtualPath(GetType().Assembly, VirtualPath, virtualPath);
+ return base.Href(virtualPath, pathParts);
+ }
+ public packages_Update_cshtml()
+ {
+ }
+ protected System.Web.HttpApplication ApplicationInstance
+ {
+ get
+ {
+ return ((System.Web.HttpApplication)(Context.ApplicationInstance));
+ }
+ }
+ public override void Execute()
+ {
+
+
+WriteLiteral("\r\n\r\n");
+
+
+
+
+WriteLiteral("\r\n");
+
+
+DefineSection("PackageHead", () => {
+
+WriteLiteral(" \r\n <script type=\"text/javascript\" src=\"");
+
+
+ Write(Href("scripts/PackageAction.js"));
+
+WriteLiteral("\"></script>\r\n <noscript>");
+
+
+ Write(PackageManagerResources.JavascriptRequired);
+
+WriteLiteral("</noscript>\r\n");
+
+
+});
+
+WriteLiteral("\r\n");
+
+
+
+ // Read params from request
+ var sourceName = Request["source"];
+ var packageId = Request["package"];
+ var versionString = Request["version"];
+ var packageSource = PageUtils.GetPackageSource(sourceName);
+
+ var version = !versionString.IsEmpty() ? SemanticVersion.Parse(versionString) : null;
+
+ WebProjectManager projectManager;
+ try {
+ projectManager = new WebProjectManager(packageSource.Source, PackageManagerModule.SiteRoot);
+ } catch (Exception exception) {
+
+WriteLiteral(" <div class=\"error message\">");
+
+
+ Write(exception.Message);
+
+WriteLiteral("</div>\r\n");
+
+
+ return;
+ }
+ var updatePackage = projectManager.SourceRepository.FindPackage(packageId, version);
+ if (updatePackage == null) {
+ ModelState.AddFormError(PackageManagerResources.BadRequest);
+
+ Write(Html.ValidationSummary());
+
+
+ return;
+ }
+
+ var package = projectManager.LocalRepository.FindPackage(packageId);
+
+ // Layout
+ Page.SectionTitle = String.Format(CultureInfo.CurrentCulture, PackageManagerResources.UpdatePackageDesc, package.GetDisplayName(), updatePackage.Version);
+ var packagesHomeUrl = Href(PageUtils.GetPackagesHome(), Request.Url.Query);
+
+ if (IsPost) {
+ AntiForgery.Validate();
+ try {
+ projectManager.UpdatePackage(updatePackage);
+ } catch (Exception exception) {
+ ModelState.AddFormError(exception.Message);
+ }
+
+ if (ModelState.IsValid) {
+ Response.Redirect(packagesHomeUrl + "&action-completed=Update");
+ }
+ else {
+
+ Write(Html.ValidationSummary(String.Format(CultureInfo.CurrentCulture, PackageManagerResources.PackageUpdateError, package.GetDisplayName())));
+
+
+ }
+ return;
+ }
+
+
+WriteLiteral("\r\n");
+
+
+Write(RenderPage("_PackageDetails.cshtml", new Dictionary<string, object>{ {"Package", updatePackage} }));
+
+WriteLiteral("\r\n<br />\r\n<form method=\"post\" action=\"\" id=\"submitForm\">\r\n ");
+
+
+Write(AntiForgery.GetHtml());
+
+WriteLiteral("\r\n <input type=\"hidden\" name=\"source\" value=\"");
+
+
+ Write(sourceName);
+
+WriteLiteral("\" />\r\n <input type=\"hidden\" name=\"package\" value=\"");
+
+
+ Write(packageId);
+
+WriteLiteral("\" />\r\n <input type=\"hidden\" name=\"version\" value=\"");
+
+
+ Write(version);
+
+WriteLiteral("\" />\r\n\r\n <input type=\"submit\" value=\"");
+
+
+ Write(PackageManagerResources.UpdatePackage);
+
+WriteLiteral("\" />\r\n <input type=\"reset\" value=\"");
+
+
+ Write(PackageManagerResources.Cancel);
+
+WriteLiteral("\" data-returnurl=\"");
+
+
+ Write(packagesHomeUrl);
+
+WriteLiteral("\" />\r\n <br /><br />\r\n</form>");
+
+
+ }
+ }
+}
+#pragma warning restore 1591
diff --git a/src/System.Web.WebPages.Administration/packages/_Layout.cshtml b/src/System.Web.WebPages.Administration/packages/_Layout.cshtml
new file mode 100644
index 00000000..e7efa868
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/packages/_Layout.cshtml
@@ -0,0 +1,22 @@
+@* Generator: WebPage *@
+
+@{
+ Layout = "../_Layout.cshtml";
+ Page.Title = PackageManagerResources.ModuleTitle;
+}
+@section Head {
+ <link href="@Href("Site.css")" rel="stylesheet" type="text/css" />
+ <script src="http://ajax.microsoft.com/ajax/jquery/jquery-1.4.2.js" type="text/javascript"></script>
+
+ @RenderSection("PackageHead", required: false)
+}
+
+@section PageSettings {
+ <a href="@Href("PackageSources")">@PackageManagerResources.ManageSourcesTitle</a>
+}
+
+@RenderBody()
+
+@section Footer {
+ <a href="http://go.microsoft.com/fwlink/?LinkID=205205" target="_blank">@PackageManagerResources.PrivacyStatement</a>
+} \ No newline at end of file
diff --git a/src/System.Web.WebPages.Administration/packages/_Layout.generated.cs b/src/System.Web.WebPages.Administration/packages/_Layout.generated.cs
new file mode 100644
index 00000000..f89303b1
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/packages/_Layout.generated.cs
@@ -0,0 +1,123 @@
+#pragma warning disable 1591
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.225
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace System.Web.WebPages.Administration.PackageManager
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+ using System.Net;
+ using System.Web;
+ using System.Web.Helpers;
+ using System.Web.Security;
+ using System.Web.UI;
+ using System.Web.WebPages;
+ using System.Web.WebPages.Html;
+
+ [System.Web.WebPages.PageVirtualPathAttribute("~/packages/_Layout.cshtml")]
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorSingleFileGenerator", "1.0.0.0")]
+ public class packages__Layout_cshtml : System.Web.WebPages.WebPage
+ {
+#line hidden
+
+ // Resolve package relative syntax
+ // Also, if it comes from a static embedded resource, change the path accordingly
+ public override string Href(string virtualPath, params object[] pathParts) {
+ virtualPath = ApplicationPart.ProcessVirtualPath(GetType().Assembly, VirtualPath, virtualPath);
+ return base.Href(virtualPath, pathParts);
+ }
+ public packages__Layout_cshtml()
+ {
+ }
+ protected System.Web.HttpApplication ApplicationInstance
+ {
+ get
+ {
+ return ((System.Web.HttpApplication)(Context.ApplicationInstance));
+ }
+ }
+ public override void Execute()
+ {
+
+
+WriteLiteral("\r\n\r\n");
+
+
+
+ Layout = "../_Layout.cshtml";
+ Page.Title = PackageManagerResources.ModuleTitle;
+
+
+
+DefineSection("Head", () => {
+
+WriteLiteral("\r\n <link href=\"");
+
+
+ Write(Href("Site.css"));
+
+WriteLiteral("\" rel=\"stylesheet\" type=\"text/css\" />\r\n <script src=\"http://ajax.microsoft.com" +
+"/ajax/jquery/jquery-1.4.2.js\" type=\"text/javascript\"></script>\r\n\r\n ");
+
+
+Write(RenderSection("PackageHead", required: false));
+
+WriteLiteral("\r\n");
+
+
+});
+
+WriteLiteral("\r\n\r\n");
+
+
+DefineSection("PageSettings", () => {
+
+WriteLiteral("\r\n <a href=\"");
+
+
+ Write(Href("PackageSources"));
+
+WriteLiteral("\">");
+
+
+ Write(PackageManagerResources.ManageSourcesTitle);
+
+WriteLiteral("</a>\r\n");
+
+
+});
+
+WriteLiteral("\r\n\r\n");
+
+
+Write(RenderBody());
+
+WriteLiteral("\r\n\r\n");
+
+
+DefineSection("Footer", () => {
+
+WriteLiteral("\r\n <a href=\"http://go.microsoft.com/fwlink/?LinkID=205205\" target=\"_blank\">");
+
+
+ Write(PackageManagerResources.PrivacyStatement);
+
+WriteLiteral("</a>\r\n");
+
+
+});
+
+
+ }
+ }
+}
+#pragma warning restore 1591
diff --git a/src/System.Web.WebPages.Administration/packages/_Package.cshtml b/src/System.Web.WebPages.Administration/packages/_Package.cshtml
new file mode 100644
index 00000000..cbcc6a47
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/packages/_Package.cshtml
@@ -0,0 +1,16 @@
+@* Generator: WebPage *@
+
+@using System.Linq;
+@using NuGet;
+@{
+ IPackage package = Page.package;
+}
+
+<div class="package-info">
+ <h4>
+ @package.GetDisplayName()
+ </h4>
+ @if (!String.IsNullOrEmpty(package.Description)) {
+ <p class="package-description">@package.Description</p>
+ }
+</div>
diff --git a/src/System.Web.WebPages.Administration/packages/_Package.generated.cs b/src/System.Web.WebPages.Administration/packages/_Package.generated.cs
new file mode 100644
index 00000000..4a3e03af
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/packages/_Package.generated.cs
@@ -0,0 +1,88 @@
+#pragma warning disable 1591
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.225
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace System.Web.WebPages.Administration.PackageManager
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+ using System.Net;
+ using System.Web;
+ using System.Web.Helpers;
+ using System.Web.Security;
+ using System.Web.UI;
+ using System.Web.WebPages;
+ using System.Web.WebPages.Html;
+ using NuGet;
+
+ [System.Web.WebPages.PageVirtualPathAttribute("~/packages/_Package.cshtml")]
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorSingleFileGenerator", "1.0.0.0")]
+ public class packages__Package_cshtml : System.Web.WebPages.WebPage
+ {
+#line hidden
+
+ // Resolve package relative syntax
+ // Also, if it comes from a static embedded resource, change the path accordingly
+ public override string Href(string virtualPath, params object[] pathParts) {
+ virtualPath = ApplicationPart.ProcessVirtualPath(GetType().Assembly, VirtualPath, virtualPath);
+ return base.Href(virtualPath, pathParts);
+ }
+ public packages__Package_cshtml()
+ {
+ }
+ protected System.Web.HttpApplication ApplicationInstance
+ {
+ get
+ {
+ return ((System.Web.HttpApplication)(Context.ApplicationInstance));
+ }
+ }
+ public override void Execute()
+ {
+
+
+WriteLiteral("\r\n\r\n");
+
+
+
+
+
+ IPackage package = Page.package;
+
+
+WriteLiteral("\r\n<div class=\"package-info\">\r\n <h4>\r\n ");
+
+
+ Write(package.GetDisplayName());
+
+WriteLiteral("\r\n </h4>\r\n");
+
+
+ if (!String.IsNullOrEmpty(package.Description)) {
+
+WriteLiteral(" <p class=\"package-description\">");
+
+
+ Write(package.Description);
+
+WriteLiteral("</p>\r\n");
+
+
+ }
+
+WriteLiteral("</div>\r\n");
+
+
+ }
+ }
+}
+#pragma warning restore 1591
diff --git a/src/System.Web.WebPages.Administration/packages/_PackageDetails.cshtml b/src/System.Web.WebPages.Administration/packages/_PackageDetails.cshtml
new file mode 100644
index 00000000..870269e8
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/packages/_PackageDetails.cshtml
@@ -0,0 +1,23 @@
+@* Generator: WebPage *@
+
+@using NuGet;
+
+@{
+ IPackage package = Page.Package;
+}
+<div class="package-info">
+<h4>
+ @package.GetDisplayName()
+</h4>
+@if (!String.IsNullOrEmpty(package.Description)) {
+ <p class="package-description">@package.Description</p>
+}
+@{
+ var authors = package.Authors as IEnumerable<string>;
+ if (authors.Any()) {
+ <p>
+ <strong>@PackageManagerResources.AuthorsLabel: </strong><span class="package-author">@String.Join(PackageManagerResources.WordSeparator, authors)</span>
+ </p>
+ }
+}
+</div>
diff --git a/src/System.Web.WebPages.Administration/packages/_PackageDetails.generated.cs b/src/System.Web.WebPages.Administration/packages/_PackageDetails.generated.cs
new file mode 100644
index 00000000..dbc49442
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/packages/_PackageDetails.generated.cs
@@ -0,0 +1,110 @@
+#pragma warning disable 1591
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.225
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace System.Web.WebPages.Administration.PackageManager
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+ using System.Net;
+ using System.Web;
+ using System.Web.Helpers;
+ using System.Web.Security;
+ using System.Web.UI;
+ using System.Web.WebPages;
+ using System.Web.WebPages.Html;
+ using NuGet;
+
+ [System.Web.WebPages.PageVirtualPathAttribute("~/packages/_PackageDetails.cshtml")]
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorSingleFileGenerator", "1.0.0.0")]
+ public class packages__PackageDetails_cshtml : System.Web.WebPages.WebPage
+ {
+#line hidden
+
+ // Resolve package relative syntax
+ // Also, if it comes from a static embedded resource, change the path accordingly
+ public override string Href(string virtualPath, params object[] pathParts) {
+ virtualPath = ApplicationPart.ProcessVirtualPath(GetType().Assembly, VirtualPath, virtualPath);
+ return base.Href(virtualPath, pathParts);
+ }
+ public packages__PackageDetails_cshtml()
+ {
+ }
+ protected System.Web.HttpApplication ApplicationInstance
+ {
+ get
+ {
+ return ((System.Web.HttpApplication)(Context.ApplicationInstance));
+ }
+ }
+ public override void Execute()
+ {
+
+
+WriteLiteral("\r\n\r\n");
+
+
+WriteLiteral("\r\n");
+
+
+
+ IPackage package = Page.Package;
+
+
+WriteLiteral("<div class=\"package-info\">\r\n<h4>\r\n ");
+
+
+Write(package.GetDisplayName());
+
+WriteLiteral("\r\n</h4>\r\n");
+
+
+ if (!String.IsNullOrEmpty(package.Description)) {
+
+WriteLiteral(" <p class=\"package-description\">");
+
+
+ Write(package.Description);
+
+WriteLiteral("</p>\r\n");
+
+
+}
+
+
+
+ var authors = package.Authors as IEnumerable<string>;
+ if (authors.Any()) {
+
+WriteLiteral(" <p>\r\n <strong>");
+
+
+ Write(PackageManagerResources.AuthorsLabel);
+
+WriteLiteral(": </strong><span class=\"package-author\">");
+
+
+ Write(String.Join(PackageManagerResources.WordSeparator, authors));
+
+WriteLiteral("</span>\r\n </p>\r\n");
+
+
+ }
+
+
+WriteLiteral("</div>\r\n");
+
+
+ }
+ }
+}
+#pragma warning restore 1591
diff --git a/src/System.Web.WebPages.Administration/packages/_pagestart.cshtml b/src/System.Web.WebPages.Administration/packages/_pagestart.cshtml
new file mode 100644
index 00000000..9dc743f4
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/packages/_pagestart.cshtml
@@ -0,0 +1,15 @@
+@* Generator: WebPage *@
+
+@using System.Web.WebPages.Administration;
+@{
+ if (!PackageManagerModule.InitPackageSourceFile()) {
+ var instructionsPath = VirtualPathUtility.ToAbsolute(PageUtils.GetPageVirtualPath("SourceFileInstructions"));
+ if (!Request.FilePath.Equals(instructionsPath, StringComparison.OrdinalIgnoreCase)) {
+ Response.Redirect(instructionsPath);
+ }
+ }
+ Layout = "_Layout.cshtml";
+ PageData["Title"] = PackageManagerResources.ModuleTitle;
+ string packagesVirutalPath = SiteAdmin.GetVirtualPath("~/packages");
+ PageData["BreadCrumbs"].Add(Tuple.Create(PackageManagerResources.ModuleTitle, Href(packagesVirutalPath)));
+} \ No newline at end of file
diff --git a/src/System.Web.WebPages.Administration/packages/_pagestart.generated.cs b/src/System.Web.WebPages.Administration/packages/_pagestart.generated.cs
new file mode 100644
index 00000000..5b38a27b
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/packages/_pagestart.generated.cs
@@ -0,0 +1,73 @@
+#pragma warning disable 1591
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.225
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace System.Web.WebPages.Administration.PackageManager
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+ using System.Net;
+ using System.Web;
+ using System.Web.Helpers;
+ using System.Web.Security;
+ using System.Web.UI;
+ using System.Web.WebPages;
+ using System.Web.WebPages.Html;
+ using System.Web.WebPages.Administration;
+
+ [System.Web.WebPages.PageVirtualPathAttribute("~/packages/_pagestart.cshtml")]
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorSingleFileGenerator", "1.0.0.0")]
+ public class packages__pagestart_cshtml : System.Web.WebPages.StartPage
+ {
+#line hidden
+
+ // Resolve package relative syntax
+ // Also, if it comes from a static embedded resource, change the path accordingly
+ public override string Href(string virtualPath, params object[] pathParts) {
+ virtualPath = ApplicationPart.ProcessVirtualPath(GetType().Assembly, VirtualPath, virtualPath);
+ return base.Href(virtualPath, pathParts);
+ }
+ public packages__pagestart_cshtml()
+ {
+ }
+ protected System.Web.HttpApplication ApplicationInstance
+ {
+ get
+ {
+ return ((System.Web.HttpApplication)(Context.ApplicationInstance));
+ }
+ }
+ public override void Execute()
+ {
+
+
+WriteLiteral("\r\n\r\n");
+
+
+
+
+ if (!PackageManagerModule.InitPackageSourceFile()) {
+ var instructionsPath = VirtualPathUtility.ToAbsolute(PageUtils.GetPageVirtualPath("SourceFileInstructions"));
+ if (!Request.FilePath.Equals(instructionsPath, StringComparison.OrdinalIgnoreCase)) {
+ Response.Redirect(instructionsPath);
+ }
+ }
+ Layout = "_Layout.cshtml";
+ PageData["Title"] = PackageManagerResources.ModuleTitle;
+ string packagesVirutalPath = SiteAdmin.GetVirtualPath("~/packages");
+ PageData["BreadCrumbs"].Add(Tuple.Create(PackageManagerResources.ModuleTitle, Href(packagesVirutalPath)));
+
+
+ }
+ }
+}
+#pragma warning restore 1591
diff --git a/src/System.Web.WebPages.Administration/packages/images/error.png b/src/System.Web.WebPages.Administration/packages/images/error.png
new file mode 100644
index 00000000..23e6d0e5
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/packages/images/error.png
Binary files differ
diff --git a/src/System.Web.WebPages.Administration/packages/images/package.png b/src/System.Web.WebPages.Administration/packages/images/package.png
new file mode 100644
index 00000000..1a192875
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/packages/images/package.png
Binary files differ
diff --git a/src/System.Web.WebPages.Administration/packages/scripts/Default.js b/src/System.Web.WebPages.Administration/packages/scripts/Default.js
new file mode 100644
index 00000000..a39e54a4
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/packages/scripts/Default.js
@@ -0,0 +1,30 @@
+$(function () {
+ $('#source, #view').change(function () {
+ $(this).closest('form').submit();
+ });
+
+ $('#searchReset').click(function (event) {
+ $('#search').val('');
+ $(this).closest('form').submit();
+ });
+
+ $('#package-list form').submit(function (event) {
+ event.preventDefault();
+ var form = $(event.target);
+
+ var getParams = {
+ source: $('#source').val(),
+ search: $('#search').val(),
+ package: form.find('input[name="package"]').val(),
+ version: form.find('input[name="version"]').val(),
+ page: form.find('input[name="page"]').val(),
+ packageName: form.find('input[name="packageName"]').val()
+ };
+ location.href = form.attr('action') + '?' + $.param(getParams);
+ });
+
+ $('#package-list h4').click(function (event) {
+ var form = $(event.target).closest('li').find('form').submit();
+ });
+});
+
diff --git a/src/System.Web.WebPages.Administration/packages/scripts/PackageAction.js b/src/System.Web.WebPages.Administration/packages/scripts/PackageAction.js
new file mode 100644
index 00000000..fd8ab579
--- /dev/null
+++ b/src/System.Web.WebPages.Administration/packages/scripts/PackageAction.js
@@ -0,0 +1,5 @@
+$(document).ready(function () {
+ $('#submitForm input[type="reset"]').click(function (event) {
+ location.href = $(this).attr('data-returnurl');
+ });
+});
diff --git a/src/System.Web.WebPages.Deployment/AppDomainHelper.cs b/src/System.Web.WebPages.Deployment/AppDomainHelper.cs
new file mode 100644
index 00000000..f9bed368
--- /dev/null
+++ b/src/System.Web.WebPages.Deployment/AppDomainHelper.cs
@@ -0,0 +1,61 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+
+namespace System.Web.WebPages.Deployment
+{
+ internal static class AppDomainHelper
+ {
+ public static IDictionary<string, IEnumerable<string>> GetBinAssemblyReferences(string appPath, string configPath)
+ {
+ string binDirectory = Path.Combine(appPath, "bin");
+ if (!Directory.Exists(binDirectory))
+ {
+ return null;
+ }
+
+ AppDomain appDomain = null;
+ try
+ {
+ var appDomainSetup = new AppDomainSetup
+ {
+ ApplicationBase = appPath,
+ ConfigurationFile = configPath,
+ PrivateBinPath = binDirectory,
+ };
+ appDomain = AppDomain.CreateDomain(typeof(AppDomainHelper).Namespace, AppDomain.CurrentDomain.Evidence, appDomainSetup);
+
+ var type = typeof(RemoteAssemblyLoader);
+ var instance = (RemoteAssemblyLoader)appDomain.CreateInstanceAndUnwrap(type.Assembly.FullName, type.FullName);
+
+ return Directory.EnumerateFiles(binDirectory, "*.dll")
+ .ToDictionary(assemblyPath => assemblyPath,
+ assemblyPath => instance.GetReferences(assemblyPath));
+ }
+ finally
+ {
+ if (appDomain != null)
+ {
+ AppDomain.Unload(appDomain);
+ }
+ }
+ }
+
+ private sealed class RemoteAssemblyLoader : MarshalByRefObject
+ {
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Method needs to be instance level for cross domain invocation"),
+ SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.Reflection.Assembly.LoadFrom",
+ Justification = "We want to load this specific assembly.")]
+ public IEnumerable<string> GetReferences(string assemblyPath)
+ {
+ var assembly = Assembly.LoadFrom(assemblyPath);
+ return assembly.GetReferencedAssemblies()
+ .Select(asmName => Assembly.Load(asmName.FullName).FullName)
+ .Concat(new[] { assembly.FullName })
+ .ToArray();
+ }
+ }
+ }
+}
diff --git a/src/System.Web.WebPages.Deployment/AssemblyUtils.cs b/src/System.Web.WebPages.Deployment/AssemblyUtils.cs
new file mode 100644
index 00000000..8c8b6b2e
--- /dev/null
+++ b/src/System.Web.WebPages.Deployment/AssemblyUtils.cs
@@ -0,0 +1,182 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Security;
+using Microsoft.Internal.Web.Utils;
+using Microsoft.Web.Infrastructure;
+
+namespace System.Web.WebPages.Deployment
+{
+ internal static class AssemblyUtils
+ {
+ // Copied from AssemblyRefs.cs
+ private const string SharedLibPublicKey = "31bf3856ad364e35";
+
+ internal static readonly AssemblyName ThisAssemblyName = new AssemblyName(typeof(AssemblyUtils).Assembly.FullName);
+ private static readonly Version WebPagesV1Version = new Version(1, 0, 0, 0);
+ private static readonly string _binFileName = Path.GetFileName(ThisAssemblyName.Name) + ".dll";
+
+ // Special case MWI because it does not share the same assembly version as the rest of WebPages.
+ private static readonly Version _mwiVersion = new AssemblyName(typeof(InfrastructureHelper).Assembly.FullName).Version;
+
+ private static readonly AssemblyName _mwiAssemblyName = GetFullName("Microsoft.Web.Infrastructure", _mwiVersion);
+
+ private static readonly AssemblyName[] _version1AssemblyList = new[]
+ {
+ _mwiAssemblyName,
+ GetFullName("System.Web.Razor", WebPagesV1Version),
+ GetFullName("System.Web.Helpers", WebPagesV1Version),
+ GetFullName("System.Web.WebPages", WebPagesV1Version),
+ GetFullName("System.Web.WebPages.Administration", WebPagesV1Version),
+ GetFullName("System.Web.WebPages.Razor", WebPagesV1Version),
+ GetFullName("WebMatrix.Data", WebPagesV1Version),
+ GetFullName("WebMatrix.WebData", WebPagesV1Version)
+ };
+
+ private static readonly AssemblyName[] _versionCurrentAssemblyList = new[]
+ {
+ _mwiAssemblyName,
+ GetFullName("System.Web.Razor", ThisAssemblyName.Version),
+ GetFullName("System.Web.Helpers", ThisAssemblyName.Version),
+ GetFullName("System.Web.WebPages", ThisAssemblyName.Version),
+ GetFullName("System.Web.WebPages.Administration", ThisAssemblyName.Version),
+ GetFullName("System.Web.WebPages.Razor", ThisAssemblyName.Version),
+ GetFullName("WebMatrix.Data", ThisAssemblyName.Version),
+ GetFullName("WebMatrix.WebData", ThisAssemblyName.Version)
+ };
+
+ internal static Version GetMaxWebPagesVersion()
+ {
+ return GetMaxWebPagesVersion(GetLoadedAssemblies());
+ }
+
+ internal static Version GetMaxWebPagesVersion(IEnumerable<AssemblyName> loadedAssemblies)
+ {
+ return GetWebPagesAssemblies(loadedAssemblies).Max(c => c.Version);
+ }
+
+ internal static bool IsVersionAvailable(Version version)
+ {
+ return IsVersionAvailable(GetLoadedAssemblies(), version);
+ }
+
+ internal static bool IsVersionAvailable(IEnumerable<AssemblyName> loadedAssemblies, Version version)
+ {
+ return GetWebPagesAssemblies(loadedAssemblies).Any(c => c.Version == version);
+ }
+
+ private static IEnumerable<AssemblyName> GetWebPagesAssemblies(IEnumerable<AssemblyName> loadedAssemblies)
+ {
+ return (from otherName in loadedAssemblies
+ where NamesMatch(ThisAssemblyName, otherName, matchVersion: false)
+ select otherName);
+ }
+
+ /// <summary>
+ /// Returns the version of a System.Web.WebPages.Deployment.dll if it is present in the bin and matches the name and
+ /// public key token of the current assembly.
+ /// </summary>
+ /// <returns>Version from bin if present, null otherwise.</returns>
+ internal static Version GetVersionFromBin(string binDirectory, IFileSystem fileSystem, Func<string, AssemblyName> getAssemblyNameThunk = null)
+ {
+ // If a version of the assembly is present both in the bin and the GAC, the GAC would win.
+ // To work around this, we'll look for a physical file on disk with the same name as the current assembly and load it to determine the version.
+ // Determine if the Deployment assembly is present in the bin
+ var assemblyInBin = Path.Combine(binDirectory, _binFileName);
+ if (fileSystem.FileExists(assemblyInBin))
+ {
+ try
+ {
+ getAssemblyNameThunk = getAssemblyNameThunk ?? AssemblyName.GetAssemblyName;
+ AssemblyName assemblyName = getAssemblyNameThunk(assemblyInBin);
+ if (NamesMatch(ThisAssemblyName, assemblyName, matchVersion: false))
+ {
+ return assemblyName.Version;
+ }
+ }
+ catch (BadImageFormatException)
+ {
+ // Do nothing.
+ }
+ catch (SecurityException)
+ {
+ // Do nothing
+ }
+ catch (FileLoadException)
+ {
+ // Do nothing.
+ }
+ }
+ return null;
+ }
+
+ internal static bool NamesMatch(AssemblyName left, AssemblyName right, bool matchVersion)
+ {
+ return Equals(left.Name, right.Name) &&
+ Equals(left.CultureInfo, right.CultureInfo) &&
+ Enumerable.SequenceEqual(left.GetPublicKeyToken(), right.GetPublicKeyToken()) &&
+ (!matchVersion || Equals(left.Version, right.Version));
+ }
+
+ internal static IEnumerable<AssemblyName> GetLoadedAssemblies()
+ {
+ return AppDomain.CurrentDomain.GetAssemblies()
+ .Select(GetAssemblyName)
+ .ToList();
+ }
+
+ internal static IEnumerable<AssemblyName> GetAssembliesForVersion(Version version)
+ {
+ if (version == WebPagesV1Version)
+ {
+ return _version1AssemblyList;
+ }
+ return _versionCurrentAssemblyList;
+ }
+
+ private static AssemblyName GetAssemblyName(Assembly assembly)
+ {
+ return new AssemblyName(assembly.FullName);
+ }
+
+ private static AssemblyName GetFullName(string name, Version version, string publicKeyToken)
+ {
+ return new AssemblyName(String.Format(CultureInfo.InvariantCulture,
+ "{0}, Version={1}, Culture=neutral, PublicKeyToken={2}",
+ name, version, publicKeyToken));
+ }
+
+ internal static AssemblyName GetFullName(string name, Version version)
+ {
+ return GetFullName(name, version, SharedLibPublicKey);
+ }
+
+ public static IDictionary<string, Version> GetAssembliesMatchingOtherVersions(IDictionary<string, IEnumerable<string>> references)
+ {
+ var webPagesAssemblies = AssemblyUtils.GetAssembliesForVersion(AssemblyUtils.ThisAssemblyName.Version);
+ if (webPagesAssemblies == null || !webPagesAssemblies.Any())
+ {
+ return new Dictionary<string, Version>(0);
+ }
+
+ var matchingVersions = from item in references
+ let matchedVersion = GetMatchingVersion(webPagesAssemblies, item.Value)
+ where matchedVersion != null
+ select new KeyValuePair<string, Version>(item.Key, matchedVersion);
+ return matchingVersions.ToDictionary(k => k.Key, k => k.Value);
+ }
+
+ private static Version GetMatchingVersion(IEnumerable<AssemblyName> webPagesAssemblies, IEnumerable<string> references)
+ {
+ // Return assemblies that match in name but not in version.
+ var matchingVersions = from webPagesAssembly in webPagesAssemblies
+ from referenceName in references
+ let referencedAssembly = new AssemblyName(referenceName)
+ where AssemblyUtils.NamesMatch(webPagesAssembly, referencedAssembly, matchVersion: false) && webPagesAssembly.Version != referencedAssembly.Version
+ select referencedAssembly.Version;
+ return matchingVersions.FirstOrDefault();
+ }
+ }
+}
diff --git a/src/System.Web.WebPages.Deployment/BuildManagerWrapper.cs b/src/System.Web.WebPages.Deployment/BuildManagerWrapper.cs
new file mode 100644
index 00000000..c0509030
--- /dev/null
+++ b/src/System.Web.WebPages.Deployment/BuildManagerWrapper.cs
@@ -0,0 +1,26 @@
+using System.IO;
+using System.Web.Compilation;
+
+namespace System.Web.WebPages.Deployment
+{
+ internal sealed class BuildManagerWrapper : IBuildManager
+ {
+ /// <summary>
+ /// Reads a special cached file from %WindDir%\Microsoft.NET\Framework\vx.x\ASP.NET Temporary Files\&lt;x&gt;\&lt;y&gt;\UserCache that is
+ /// available across AppDomain recycles.
+ /// </summary>
+ public Stream ReadCachedFile(string path)
+ {
+ return BuildManager.ReadCachedFile(path);
+ }
+
+ /// <summary>
+ /// Creates or opens a special cached file that is created under %WindDir%\Microsoft.NET\Framework\vx.x\ASP.NET Temporary Files\&lt;x&gt;\&lt;y&gt;\UserCache that is
+ /// available across AppDomain recycles.
+ /// </summary>
+ public Stream CreateCachedFile(string path)
+ {
+ return BuildManager.CreateCachedFile(path);
+ }
+ }
+}
diff --git a/src/System.Web.WebPages.Deployment/Common/IFileSystem.cs b/src/System.Web.WebPages.Deployment/Common/IFileSystem.cs
new file mode 100644
index 00000000..3d1b9fe8
--- /dev/null
+++ b/src/System.Web.WebPages.Deployment/Common/IFileSystem.cs
@@ -0,0 +1,16 @@
+using System.Collections.Generic;
+using System.IO;
+
+namespace Microsoft.Internal.Web.Utils
+{
+ internal interface IFileSystem
+ {
+ bool FileExists(string path);
+
+ Stream ReadFile(string path);
+
+ Stream OpenFile(string path);
+
+ IEnumerable<string> EnumerateFiles(string root);
+ }
+}
diff --git a/src/System.Web.WebPages.Deployment/Common/PhysicalFileSystem.cs b/src/System.Web.WebPages.Deployment/Common/PhysicalFileSystem.cs
new file mode 100644
index 00000000..b6fe59ec
--- /dev/null
+++ b/src/System.Web.WebPages.Deployment/Common/PhysicalFileSystem.cs
@@ -0,0 +1,38 @@
+using System.Collections.Generic;
+using System.IO;
+
+namespace Microsoft.Internal.Web.Utils
+{
+ internal sealed class PhysicalFileSystem : IFileSystem
+ {
+ public bool FileExists(string path)
+ {
+ return File.Exists(path);
+ }
+
+ public Stream ReadFile(string path)
+ {
+ return File.OpenRead(path);
+ }
+
+ public Stream OpenFile(string path)
+ {
+ string directory = Path.GetDirectoryName(path);
+ EnsureDirectory(directory);
+ return File.OpenWrite(path);
+ }
+
+ public IEnumerable<string> EnumerateFiles(string path)
+ {
+ return Directory.EnumerateFiles(path);
+ }
+
+ private static void EnsureDirectory(string path)
+ {
+ if (!Directory.Exists(path))
+ {
+ Directory.CreateDirectory(path);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.WebPages.Deployment/GlobalSuppressions.cs b/src/System.Web.WebPages.Deployment/GlobalSuppressions.cs
new file mode 100644
index 00000000..8e7ebfe5
--- /dev/null
+++ b/src/System.Web.WebPages.Deployment/GlobalSuppressions.cs
@@ -0,0 +1,13 @@
+// This file is used by Code Analysis to maintain SuppressMessage
+// attributes that are applied to this project.
+// Project-level suppressions either have no target or are given
+// a specific target and scoped to a namespace, type, member, etc.
+//
+// To add a suppression to this file, right-click the message in the
+// Error List, point to "Suppress Message(s)", and click
+// "In Project Suppression File".
+// You do not need to add suppressions to this file manually.
+
+using System.Diagnostics.CodeAnalysis;
+
+[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "System.Web.WebPages.Deployment", Justification = "Namespace is present to match assembly name")]
diff --git a/src/System.Web.WebPages.Deployment/IBuildManager.cs b/src/System.Web.WebPages.Deployment/IBuildManager.cs
new file mode 100644
index 00000000..a71faabb
--- /dev/null
+++ b/src/System.Web.WebPages.Deployment/IBuildManager.cs
@@ -0,0 +1,11 @@
+using System.IO;
+
+namespace System.Web.WebPages.Deployment
+{
+ internal interface IBuildManager
+ {
+ Stream CreateCachedFile(string fileName);
+
+ Stream ReadCachedFile(string fileName);
+ }
+}
diff --git a/src/System.Web.WebPages.Deployment/PreApplicationStartCode.cs b/src/System.Web.WebPages.Deployment/PreApplicationStartCode.cs
new file mode 100644
index 00000000..00607c64
--- /dev/null
+++ b/src/System.Web.WebPages.Deployment/PreApplicationStartCode.cs
@@ -0,0 +1,272 @@
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using System.Reflection;
+using System.Web.Caching;
+using System.Web.Compilation;
+using System.Web.Configuration;
+using System.Web.WebPages.Deployment.Resources;
+using Microsoft.Internal.Web.Utils;
+using Microsoft.Web.Infrastructure;
+
+namespace System.Web.WebPages.Deployment
+{
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static class PreApplicationStartCode
+ {
+ /// <summary>
+ /// Key used to indicate to tooling that the compile exception we throw to refresh the app domain originated from us so that they can deal with it correctly.
+ /// </summary>
+ private const string ToolingIndicatorKey = "WebPages.VersionChange";
+
+ // NOTE: Do not add public fields, methods, or other members to this class.
+ // This class does not show up in Intellisense so members on it will not be
+ // discoverable by users. Place new members on more appropriate classes that
+ // relate to the public API (for example, a LoginUrl property should go on a
+ // membership-related class).
+ private static readonly IFileSystem _physicalFileSystem = new PhysicalFileSystem();
+
+ private static bool _startWasCalled;
+
+ public static void Start()
+ {
+ // Even though ASP.NET will only call each PreAppStart once, we sometimes internally call one PreAppStart from
+ // another PreAppStart to ensure that things get initialized in the right order. ASP.NET does not guarantee the
+ // order so we have to guard against multiple calls.
+ // All Start calls are made on same thread, so no lock needed here.
+
+ if (_startWasCalled)
+ {
+ return;
+ }
+ _startWasCalled = true;
+
+ StartCore();
+ }
+
+ internal static bool StartCore()
+ {
+ var buildManager = new BuildManagerWrapper();
+ NameValueCollection appSettings = WebConfigurationManager.AppSettings;
+ Action<Version> loadWebPages = LoadWebPages;
+ Action registerForChangeNotification = RegisterForChangeNotifications;
+ IEnumerable<AssemblyName> loadedAssemblies = AssemblyUtils.GetLoadedAssemblies();
+
+ return StartCore(_physicalFileSystem, HttpRuntime.AppDomainAppPath, HttpRuntime.BinDirectory, appSettings, loadedAssemblies,
+ buildManager, loadWebPages, registerForChangeNotification);
+ }
+
+ // Adds Parameter for unit tests
+ internal static bool StartCore(IFileSystem fileSystem, string appDomainAppPath, string binDirectory, NameValueCollection appSettings, IEnumerable<AssemblyName> loadedAssemblies,
+ IBuildManager buildManager, Action<Version> loadWebPages, Action registerForChangeNotification, Func<string, AssemblyName> getAssemblyNameThunk = null)
+ {
+ if (WebPagesDeployment.IsExplicitlyDisabled(appSettings))
+ {
+ // If WebPages is explicitly disabled, exit.
+ Debug.WriteLine("WebPages Bootstrapper v{0}: not loading WebPages since it is disabled", AssemblyUtils.ThisAssemblyName.Version);
+ return false;
+ }
+
+ Version maxWebPagesVersion = AssemblyUtils.GetMaxWebPagesVersion(loadedAssemblies);
+ Debug.Assert(maxWebPagesVersion != null, "Function must return some max value.");
+ if (AssemblyUtils.ThisAssemblyName.Version != maxWebPagesVersion)
+ {
+ // Always let the highest version determine what needs to be done. This would make future proofing simpler.
+ Debug.WriteLine("WebPages Bootstrapper v{0}: Higher version v{1} is available.", AssemblyUtils.ThisAssemblyName.Version, maxWebPagesVersion);
+ return false;
+ }
+
+ var webPagesEnabled = WebPagesDeployment.IsEnabled(fileSystem, appDomainAppPath, appSettings);
+ Version binVersion = AssemblyUtils.GetVersionFromBin(binDirectory, fileSystem, getAssemblyNameThunk);
+ Version version = WebPagesDeployment.GetVersionInternal(appSettings, binVersion, defaultVersion: maxWebPagesVersion);
+
+ // Asserts to ensure unit tests are set up correctly. So essentially, we're unit testing the unit tests.
+ Debug.Assert(version != null, "GetVersion always returns a version");
+ Debug.Assert(binVersion == null || binVersion <= maxWebPagesVersion, "binVersion cannot be higher than max version");
+
+ if ((binVersion != null) && (binVersion != version))
+ {
+ // Determine if there's a version conflict. A conflict could occur if there's a version specified in the bin which is different from the version specified in the
+ // config that is different.
+ throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, ConfigurationResources.WebPagesVersionConflict, version, binVersion));
+ }
+ else if (binVersion != null)
+ {
+ // The rest of the code is only meant to be executed if we are executing from the GAC.
+ // If a version is bin deployed, we don't need to do anything special to bootstrap.
+ return false;
+ }
+ else if (!webPagesEnabled)
+ {
+ Debug.WriteLine("WebPages Bootstrapper v{0}: WebPages not enabled, registering for change notifications", AssemblyUtils.ThisAssemblyName.Version);
+ // Register for change notifications under the application root
+ registerForChangeNotification();
+ return false;
+ }
+ else if (!AssemblyUtils.IsVersionAvailable(loadedAssemblies, version))
+ {
+ throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, ConfigurationResources.WebPagesVersionNotFound, version, AssemblyUtils.ThisAssemblyName.Version));
+ }
+
+ Debug.WriteLine("WebPages Bootstrapper v{0}: loading version {1}, loading WebPages", AssemblyUtils.ThisAssemblyName.Version, version);
+ // If the version the application was compiled earlier was different, invalidate compilation results by adding a file to the bin.
+ InvalidateCompilationResultsIfVersionChanged(buildManager, fileSystem, binDirectory, version);
+ loadWebPages(version);
+ return true;
+ }
+
+ /// <summary>
+ /// WebPages stores the version to be compiled against in AppSettings as &gt;add key="webpages:version" value="1.0" /&lt;.
+ /// Changing values AppSettings does not cause recompilation therefore we could run into a state where we have files compiled against v1 but the application is
+ /// currently v2.
+ /// </summary>
+ private static void InvalidateCompilationResultsIfVersionChanged(IBuildManager buildManager, IFileSystem fileSystem, string binDirectory, Version currentVersion)
+ {
+ Version previousVersion = WebPagesDeployment.GetPreviousRuntimeVersion(buildManager);
+
+ // Persist the current version number in BuildManager's cached file
+ WebPagesDeployment.PersistRuntimeVersion(buildManager, currentVersion);
+
+ if (previousVersion == null)
+ {
+ // Do nothing.
+ }
+ else if (previousVersion != currentVersion)
+ {
+ // If the previous runtime version is different, perturb the bin directory so that it forces recompilation.
+ WebPagesDeployment.ForceRecompile(fileSystem, binDirectory);
+ var httpCompileException = new HttpCompileException(ConfigurationResources.WebPagesVersionChanges);
+ // Indicator for tooling
+ httpCompileException.Data[ToolingIndicatorKey] = true;
+ throw httpCompileException;
+ }
+ }
+
+ // Copied from xsp\System\Web\Compilation\BuildManager.cs
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Copied from System.Web.dll")]
+ internal static ICollection<MethodInfo> GetPreStartInitMethodsFromAssemblyCollection(IEnumerable<Assembly> assemblies)
+ {
+ List<MethodInfo> methods = new List<MethodInfo>();
+ foreach (Assembly assembly in assemblies)
+ {
+ PreApplicationStartMethodAttribute[] attributes = null;
+ try
+ {
+ attributes = (PreApplicationStartMethodAttribute[])assembly.GetCustomAttributes(typeof(PreApplicationStartMethodAttribute), inherit: true);
+ }
+ catch
+ {
+ // GetCustomAttributes invokes the constructors of the attributes, so it is possible that they might throw unexpected exceptions.
+ // (Dev10 bug 831981)
+ }
+
+ if (attributes != null && attributes.Length != 0)
+ {
+ Debug.Assert(attributes.Length == 1);
+ PreApplicationStartMethodAttribute attribute = attributes[0];
+ Debug.Assert(attribute != null);
+
+ MethodInfo method = null;
+ // Ensure the Type on the attribute is in the same assembly as the attribute itself
+ if (attribute.Type != null && !String.IsNullOrEmpty(attribute.MethodName) && attribute.Type.Assembly == assembly)
+ {
+ method = FindPreStartInitMethod(attribute.Type, attribute.MethodName);
+ }
+
+ if (method != null)
+ {
+ methods.Add(method);
+ }
+
+ // No-op if the attribute is invalid
+ /*
+ else {
+ throw new HttpException(SR.GetString(SR.Invalid_PreApplicationStartMethodAttribute_value,
+ assembly.FullName,
+ (attribute.Type != null ? attribute.Type.FullName : String.Empty),
+ attribute.MethodName));
+ }
+ */
+ }
+ }
+ return methods;
+ }
+
+ // Copied from xsp\System\Web\Compilation\BuildManager.cs
+ internal static MethodInfo FindPreStartInitMethod(Type type, string methodName)
+ {
+ Debug.Assert(type != null);
+ Debug.Assert(!String.IsNullOrEmpty(methodName));
+ MethodInfo method = null;
+ if (type.IsPublic)
+ {
+ // Verify that type is public to avoid allowing internal code execution. This implementation will not match
+ // nested public types.
+ method = type.GetMethod(methodName, BindingFlags.Public | BindingFlags.Static | BindingFlags.IgnoreCase,
+ binder: null,
+ types: Type.EmptyTypes,
+ modifiers: null);
+ }
+ return method;
+ }
+
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "The cache disposes of the dependency")]
+ private static void RegisterForChangeNotifications()
+ {
+ string physicalPath = HttpRuntime.AppDomainAppPath;
+
+ CacheDependency cacheDependency = new CacheDependency(physicalPath, DateTime.UtcNow);
+ var key = WebPagesDeployment.CacheKeyPrefix + physicalPath;
+
+ HttpRuntime.Cache.Insert(key, physicalPath, cacheDependency,
+ Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration,
+ CacheItemPriority.NotRemovable, new CacheItemRemovedCallback(OnChanged));
+ }
+
+ private static void OnChanged(string key, object value, CacheItemRemovedReason reason)
+ {
+ // Only handle case when the dependency has changed.
+ if (reason != CacheItemRemovedReason.DependencyChanged)
+ {
+ return;
+ }
+
+ // Scan the app root for a webpages file
+ if (WebPagesDeployment.AppRootContainsWebPagesFile(_physicalFileSystem, HttpRuntime.AppDomainAppPath))
+ {
+ // Unload the app domain so we register plan9 when the app restarts
+ InfrastructureHelper.UnloadAppDomain();
+ }
+ else
+ {
+ // We need to re-register since the item was removed from the cache
+ RegisterForChangeNotifications();
+ }
+ }
+
+ private static void LoadWebPages(Version version)
+ {
+ IEnumerable<AssemblyName> assemblyList = AssemblyUtils.GetAssembliesForVersion(version);
+ var assemblies = assemblyList.Select(LoadAssembly);
+
+ foreach (var asm in assemblies)
+ {
+ BuildManager.AddReferencedAssembly(asm);
+ }
+
+ foreach (var m in GetPreStartInitMethodsFromAssemblyCollection(assemblies))
+ {
+ m.Invoke(null, null);
+ }
+ }
+
+ private static Assembly LoadAssembly(AssemblyName name)
+ {
+ return Assembly.Load(name);
+ }
+ }
+}
diff --git a/src/System.Web.WebPages.Deployment/Properties/AssemblyInfo.cs b/src/System.Web.WebPages.Deployment/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..916036ee
--- /dev/null
+++ b/src/System.Web.WebPages.Deployment/Properties/AssemblyInfo.cs
@@ -0,0 +1,13 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Web;
+using System.Web.WebPages.Deployment;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+
+[assembly: AssemblyTitle("System.Web.WebPages.Deployment")]
+[assembly: AssemblyDescription("")]
+[assembly: InternalsVisibleTo("System.Web.WebPages.Deployment.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
+[assembly: PreApplicationStartMethod(typeof(PreApplicationStartCode), "Start")]
diff --git a/src/System.Web.WebPages.Deployment/Resources/ConfigurationResources.Designer.cs b/src/System.Web.WebPages.Deployment/Resources/ConfigurationResources.Designer.cs
new file mode 100644
index 00000000..0be9061d
--- /dev/null
+++ b/src/System.Web.WebPages.Deployment/Resources/ConfigurationResources.Designer.cs
@@ -0,0 +1,108 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.235
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace System.Web.WebPages.Deployment.Resources {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class ConfigurationResources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal ConfigurationResources() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("System.Web.WebPages.Deployment.Resources.ConfigurationResources", typeof(ConfigurationResources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The &quot;InstallPath&quot; name was not found in the Web Pages registry key &quot;{0}&quot;..
+ /// </summary>
+ internal static string InstallPathNotFound {
+ get {
+ return ResourceManager.GetString("InstallPathNotFound", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The Web Pages registry key &quot;{0}&quot; does not exist..
+ /// </summary>
+ internal static string WebPagesRegistryKeyDoesNotExist {
+ get {
+ return ResourceManager.GetString("WebPagesRegistryKeyDoesNotExist", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Changes were detected in the Web Pages runtime version that require your application to be recompiled. Refresh your browser window to continue..
+ /// </summary>
+ internal static string WebPagesVersionChanges {
+ get {
+ return ResourceManager.GetString("WebPagesVersionChanges", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Conflicting versions of ASP.NET Web Pages detected: specified version is &quot;{0}&quot;, but the version in bin is &quot;{1}&quot;. To continue, remove files from the application&apos;s bin directory or remove the version specification in web.config..
+ /// </summary>
+ internal static string WebPagesVersionConflict {
+ get {
+ return ResourceManager.GetString("WebPagesVersionConflict", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Specified Web Pages version &quot;{0}&quot; could not be found. Update your web.config to specify a different version. Current version: &quot;{1}&quot;..
+ /// </summary>
+ internal static string WebPagesVersionNotFound {
+ get {
+ return ResourceManager.GetString("WebPagesVersionNotFound", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.WebPages.Deployment/Resources/ConfigurationResources.resx b/src/System.Web.WebPages.Deployment/Resources/ConfigurationResources.resx
new file mode 100644
index 00000000..35225e14
--- /dev/null
+++ b/src/System.Web.WebPages.Deployment/Resources/ConfigurationResources.resx
@@ -0,0 +1,135 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="InstallPathNotFound" xml:space="preserve">
+ <value>The "InstallPath" name was not found in the Web Pages registry key "{0}".</value>
+ </data>
+ <data name="WebPagesRegistryKeyDoesNotExist" xml:space="preserve">
+ <value>The Web Pages registry key "{0}" does not exist.</value>
+ </data>
+ <data name="WebPagesVersionChanges" xml:space="preserve">
+ <value>Changes were detected in the Web Pages runtime version that require your application to be recompiled. Refresh your browser window to continue.</value>
+ </data>
+ <data name="WebPagesVersionConflict" xml:space="preserve">
+ <value>Conflicting versions of ASP.NET Web Pages detected: specified version is "{0}", but the version in bin is "{1}". To continue, remove files from the application's bin directory or remove the version specification in web.config.</value>
+ </data>
+ <data name="WebPagesVersionNotFound" xml:space="preserve">
+ <value>Specified Web Pages version "{0}" could not be found. Update your web.config to specify a different version. Current version: "{1}".</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/System.Web.WebPages.Deployment/System.Web.WebPages.Deployment.csproj b/src/System.Web.WebPages.Deployment/System.Web.WebPages.Deployment.csproj
new file mode 100644
index 00000000..21677956
--- /dev/null
+++ b/src/System.Web.WebPages.Deployment/System.Web.WebPages.Deployment.csproj
@@ -0,0 +1,111 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <CodeAnalysis Condition=" '$(CodeAnalysis)' == '' ">false</CodeAnalysis>
+ <ProductVersion>9.0.30729</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{22BABB60-8F02-4027-AFFC-ACF069954536}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <RootNamespace>System.Web.WebPages.Deployment</RootNamespace>
+ <AssemblyName>System.Web.WebPages.Deployment</AssemblyName>
+ <FileAlignment>512</FileAlignment>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>..\..\bin\Debug\</OutputPath>
+ <DefineConstants>TRACE;DEBUG;ASPNETWEBPAGES</DefineConstants>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ <DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
+ <NoWarn>1591</NoWarn>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>..\..\bin\Release\</OutputPath>
+ <DefineConstants>TRACE;ASPNETWEBPAGES</DefineConstants>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ <RunCodeAnalysis>$(CodeAnalysis)</RunCodeAnalysis>
+ <DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
+ <NoWarn>1591</NoWarn>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'CodeCoverage|AnyCPU'">
+ <DebugSymbols>true</DebugSymbols>
+ <OutputPath>..\..\bin\CodeCoverage\</OutputPath>
+ <DefineConstants>TRACE;DEBUG;CODE_COVERAGE;ASPNETWEBPAGES</DefineConstants>
+ <DebugType>full</DebugType>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="Microsoft.Web.Infrastructure, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+ <HintPath>..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll</HintPath>
+ </Reference>
+ <Reference Include="System" />
+ <Reference Include="System.configuration" />
+ <Reference Include="System.Web" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="..\CommonAssemblyInfo.cs">
+ <Link>Properties\CommonAssemblyInfo.cs</Link>
+ </Compile>
+ <Compile Include="..\CommonResources.Designer.cs">
+ <Link>Common\CommonResources.Designer.cs</Link>
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>CommonResources.resx</DependentUpon>
+ </Compile>
+ <Compile Include="..\ExceptionHelper.cs">
+ <Link>Common\ExceptionHelper.cs</Link>
+ </Compile>
+ <Compile Include="..\GlobalSuppressions.cs">
+ <Link>Common\GlobalSuppressions.cs</Link>
+ </Compile>
+ <Compile Include="..\TransparentCommonAssemblyInfo.cs">
+ <Link>Properties\TransparentCommonAssemblyInfo.cs</Link>
+ </Compile>
+ <Compile Include="AppDomainHelper.cs" />
+ <Compile Include="AssemblyUtils.cs" />
+ <Compile Include="BuildManagerWrapper.cs" />
+ <Compile Include="Common\IFileSystem.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="Common\PhysicalFileSystem.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="GlobalSuppressions.cs" />
+ <Compile Include="IBuildManager.cs" />
+ <Compile Include="Resources\ConfigurationResources.Designer.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>ConfigurationResources.resx</DependentUpon>
+ </Compile>
+ <Compile Include="WebPagesDeployment.cs" />
+ <Compile Include="PreApplicationStartCode.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="..\CommonResources.resx">
+ <Link>Common\CommonResources.resx</Link>
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>CommonResources.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Include="Resources\ConfigurationResources.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>ConfigurationResources.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ </ItemGroup>
+ <ItemGroup>
+ <CodeAnalysisDictionary Include="..\CodeAnalysisDictionary.xml" />
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="packages.config" />
+ </ItemGroup>
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+</Project> \ No newline at end of file
diff --git a/src/System.Web.WebPages.Deployment/WebPagesDeployment.cs b/src/System.Web.WebPages.Deployment/WebPagesDeployment.cs
new file mode 100644
index 00000000..b6ecec3a
--- /dev/null
+++ b/src/System.Web.WebPages.Deployment/WebPagesDeployment.cs
@@ -0,0 +1,384 @@
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Configuration;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Web.Configuration;
+using System.Web.Hosting;
+using System.Web.WebPages.Deployment.Resources;
+using Microsoft.Internal.Web.Utils;
+using Microsoft.Win32;
+
+namespace System.Web.WebPages.Deployment
+{
+ public static class WebPagesDeployment
+ {
+ private const string AppSettingsVersionKey = "webpages:Version";
+ private const string AppSettingsEnabledKey = "webpages:Enabled";
+
+ /// <summary>
+ /// File name for a temporary file that we drop in bin to force recompilation.
+ /// </summary>
+ private const string ForceRecompilationFile = "WebPagesRecompilation.deleteme";
+
+ private const string WebPagesRegistryKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\ASP.NET Web Pages\v{0}.{1}";
+ internal static readonly string CacheKeyPrefix = "__System.Web.WebPages.Deployment__";
+ private static readonly string[] _webPagesExtensions = new[] { ".cshtml", ".vbhtml" };
+ private static readonly object _installPathNotFound = new object();
+ private static readonly IFileSystem _fileSystem = new PhysicalFileSystem();
+
+ /// <param name="path">Physical or virtual path to a directory where we need to determine the version of WebPages to be used.</param>
+ /// <remarks>
+ /// In a non-hosted scenario, this method would only look at a web.config that is present at the current path. Any config settings at an
+ /// ancestor directory would not be considered.
+ /// </remarks>
+ public static Version GetVersionWithoutEnabledCheck(string path)
+ {
+ if (String.IsNullOrEmpty(path))
+ {
+ throw ExceptionHelper.CreateArgumentNullOrEmptyException("path");
+ }
+
+ var binDirectory = GetBinDirectory(path);
+ var binVersion = AssemblyUtils.GetVersionFromBin(binDirectory, _fileSystem);
+ var maxVersion = AssemblyUtils.GetMaxWebPagesVersion();
+ return GetVersionInternal(GetAppSettings(path), binVersion, maxVersion);
+ }
+
+ [Obsolete("This method is obsolete and is meant for legacy code. Use GetVersionWithoutEnabled instead.")]
+ public static Version GetVersion(string path)
+ {
+ return GetObsoleteVersionInternal(path, GetAppSettings(path), new PhysicalFileSystem(), AssemblyUtils.GetMaxWebPagesVersion);
+ }
+
+ /// <remarks>
+ /// This is meant to test an obsolete method. Don't use this!
+ /// </remarks>
+ internal static Version GetObsoleteVersionInternal(string path, NameValueCollection configuration, IFileSystem fileSystem, Func<Version> getMaxWebPagesVersion)
+ {
+ if (String.IsNullOrEmpty(path))
+ {
+ throw ExceptionHelper.CreateArgumentNullOrEmptyException("path");
+ }
+
+ var binDirectory = GetBinDirectory(path);
+ var binVersion = AssemblyUtils.GetVersionFromBin(binDirectory, _fileSystem);
+ var version = GetVersionInternal(configuration, binVersion, defaultVersion: null);
+
+ if (version != null)
+ {
+ // If a webpages version is available in config or bin, return it.
+ return version;
+ }
+ else if (AppRootContainsWebPagesFile(fileSystem, path))
+ {
+ // If the path points to a WebPages site, return the highest version.
+ return getMaxWebPagesVersion();
+ }
+ return null;
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This operation might be expensive since it has to reflect over Assembly names.")]
+ public static Version GetMaxVersion()
+ {
+ return AssemblyUtils.GetMaxWebPagesVersion();
+ }
+
+ /// <summary>
+ /// Determines if Asp.Net Web Pages is enabled.
+ /// Web Pages is enabled if there's a webPages:Enabled key in AppSettings is set to "true" or if there's a cshtml file in the current path
+ /// and the key is not present.
+ /// </summary>
+ /// <param name="path">The path at which to determine if web pages is enabled.</param>
+ /// <remarks>
+ /// In a non-hosted scenario, this method would only look at a web.config that is present at the current path. Any config settings at an
+ /// ancestor directory would not be considered.
+ /// </remarks>
+ public static bool IsEnabled(string path)
+ {
+ if (String.IsNullOrEmpty(path))
+ {
+ throw ExceptionHelper.CreateArgumentNullOrEmptyException("path");
+ }
+
+ return IsEnabled(_fileSystem, path, GetAppSettings(path));
+ }
+
+ /// <remarks>
+ /// In a non-hosted scenario, this method would only look at a web.config that is present at the current path. Any config settings at an
+ /// ancestor directory would not be considered.
+ /// </remarks>
+ public static bool IsExplicitlyDisabled(string path)
+ {
+ if (String.IsNullOrEmpty(path))
+ {
+ throw ExceptionHelper.CreateArgumentNullOrEmptyException("path");
+ }
+ return IsExplicitlyDisabled(GetAppSettings(path));
+ }
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static IDictionary<string, Version> GetIncompatibleDependencies(string appPath)
+ {
+ if (String.IsNullOrEmpty(appPath))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "appPath");
+ }
+ var configFilePath = Path.Combine(appPath, "web.config");
+
+ var assemblyReferences = AppDomainHelper.GetBinAssemblyReferences(appPath, configFilePath);
+ return AssemblyUtils.GetAssembliesMatchingOtherVersions(assemblyReferences);
+ }
+
+ internal static bool IsExplicitlyDisabled(NameValueCollection appSettings)
+ {
+ bool? enabled = GetEnabled(appSettings);
+ return enabled.HasValue && enabled.Value == false;
+ }
+
+ internal static bool IsEnabled(IFileSystem fileSystem, string path, NameValueCollection appSettings)
+ {
+ bool? enabled = GetEnabled(appSettings);
+ if (!enabled.HasValue)
+ {
+ return AppRootContainsWebPagesFile(fileSystem, path);
+ }
+ return enabled.Value;
+ }
+
+ /// <summary>
+ /// Returns the value for webPages:Enabled AppSetting value in web.config.
+ /// </summary>
+ private static bool? GetEnabled(NameValueCollection appSettings)
+ {
+ string enabledSetting = appSettings.Get(AppSettingsEnabledKey);
+ if (String.IsNullOrEmpty(enabledSetting))
+ {
+ return null;
+ }
+ else
+ {
+ return Boolean.Parse(enabledSetting);
+ }
+ }
+
+ /// <summary>
+ /// Returns the version of WebPages to be used for a specified path.
+ /// </summary>
+ /// <remarks>
+ /// This method would always returns a value regardless of web pages is explicitly disabled (via config) or implicitly disabled (by virtue of not having a cshtml file) at
+ /// the specified path.
+ /// </remarks>
+ internal static Version GetVersionInternal(NameValueCollection appSettings, Version binVersion, Version defaultVersion)
+ {
+ // Return version values with the following precedence:
+ // 1) Version in config
+ // 2) Version in bin
+ // 3) defaultVersion.
+ return GetVersionFromConfig(appSettings) ?? binVersion ?? defaultVersion;
+ }
+
+ /// <summary>
+ /// Gets full path to a folder that contains ASP.NET WebPages assemblies for a given version. Used by
+ /// WebMatrix and Visual Studio so they know what to copy to an app's Bin folder or deploy to a hoster.
+ /// </summary>
+ public static string GetAssemblyPath(Version version)
+ {
+ if (version == null)
+ {
+ throw new ArgumentNullException("version");
+ }
+
+ string webPagesRegistryKey = String.Format(CultureInfo.InvariantCulture, WebPagesRegistryKey, version.Major, version.Minor);
+
+ object installPath = Registry.GetValue(webPagesRegistryKey, "InstallPath", _installPathNotFound);
+
+ if (installPath == null)
+ {
+ throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture,
+ ConfigurationResources.WebPagesRegistryKeyDoesNotExist, webPagesRegistryKey));
+ }
+ else if (installPath == _installPathNotFound)
+ {
+ throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture,
+ ConfigurationResources.InstallPathNotFound, webPagesRegistryKey));
+ }
+
+ return Path.Combine((string)installPath, "Assemblies");
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This operation might be expensive since it has to reflect over Assembly names.")]
+ public static IEnumerable<AssemblyName> GetWebPagesAssemblies()
+ {
+ return AssemblyUtils.GetAssembliesForVersion(AssemblyUtils.ThisAssemblyName.Version);
+ }
+
+ private static NameValueCollection GetAppSettings(string path)
+ {
+ if (path.StartsWith("~/", StringComparison.Ordinal))
+ {
+ // Path is virtual, assume we're hosted
+ return (NameValueCollection)WebConfigurationManager.GetSection("appSettings", path);
+ }
+ else
+ {
+ // Path is physical, map it to an application
+ WebConfigurationFileMap fileMap = new WebConfigurationFileMap();
+ fileMap.VirtualDirectories.Add("/", new VirtualDirectoryMapping(path, true));
+ var config = WebConfigurationManager.OpenMappedWebConfiguration(fileMap, "/");
+
+ var appSettingsSection = config.AppSettings;
+ var appSettings = new NameValueCollection();
+
+ foreach (KeyValueConfigurationElement element in appSettingsSection.Settings)
+ {
+ appSettings.Add(element.Key, element.Value);
+ }
+ return appSettings;
+ }
+ }
+
+ internal static Version GetVersionFromConfig(NameValueCollection appSettings)
+ {
+ string version = appSettings.Get(AppSettingsVersionKey);
+ // Version will be null if the config section is registered but not present in app web.config.
+ if (!String.IsNullOrEmpty(version))
+ {
+ // Build and Revision are optional in config but required by Fusion, so we set them to 0 if unspecified in config.
+ // Valid in config: "1.0", "1.0.0", "1.0.0.0"
+ var fullVersion = new Version(version);
+ if (fullVersion.Build == -1 || fullVersion.Revision == -1)
+ {
+ fullVersion = new Version(fullVersion.Major, fullVersion.Minor,
+ fullVersion.Build == -1 ? 0 : fullVersion.Build,
+ fullVersion.Revision == -1 ? 0 : fullVersion.Revision);
+ }
+ return fullVersion;
+ }
+ return null;
+ }
+
+ internal static bool AppRootContainsWebPagesFile(IFileSystem fileSystem, string path)
+ {
+ var files = fileSystem.EnumerateFiles(path);
+ return files.Any(IsWebPagesFile);
+ }
+
+ private static bool IsWebPagesFile(string file)
+ {
+ var extension = Path.GetExtension(file);
+ return _webPagesExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
+ }
+
+ /// <summary>
+ /// HttpRuntime.BinDirectory is unavailable in design time and throws if we try to access it. To workaround this, if we aren't hosted,
+ /// we will assume that the path that was passed to us is the application root.
+ /// </summary>
+ /// <param name="path"></param>
+ /// <returns></returns>
+ private static string GetBinDirectory(string path)
+ {
+ if (HostingEnvironment.IsHosted)
+ {
+ return HttpRuntime.BinDirectory;
+ }
+ return Path.Combine(path, "bin");
+ }
+
+ /// <summary>
+ /// Reads a previously persisted version number from build manager's cached directory.
+ /// </summary>
+ /// <returns>Null if a previous version number does not exist or is not a valid version number, read version number otherwise.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We never want to throw an exception from this method.")]
+ internal static Version GetPreviousRuntimeVersion(IBuildManager buildManagerFileSystem)
+ {
+ string fileName = GetCachedFileName();
+ try
+ {
+ Stream stream = buildManagerFileSystem.ReadCachedFile(fileName);
+ if (stream == null)
+ {
+ return null;
+ }
+
+ using (StreamReader reader = new StreamReader(stream))
+ {
+ string text = reader.ReadLine();
+ Version version;
+ if (Version.TryParse(text, out version))
+ {
+ return version;
+ }
+ }
+ }
+ catch
+ {
+ }
+ return null;
+ }
+
+ /// <summary>
+ /// Persists the version number in a file under the build manager's cached directory.
+ /// </summary>
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We never want to throw an exception from this method.")]
+ internal static void PersistRuntimeVersion(IBuildManager buildManager, Version version)
+ {
+ string fileName = GetCachedFileName();
+ try
+ {
+ Stream stream = buildManager.CreateCachedFile(fileName);
+ using (var writer = new StreamWriter(stream))
+ {
+ writer.WriteLine(version.ToString());
+ }
+ }
+ catch
+ {
+ }
+ }
+
+ /// <summary>
+ /// Forces recompilation of the application by dropping a file under bin.
+ /// </summary>
+ /// <param name="fileSystem">File system instance used to write a file to bin directory.</param>
+ /// <param name="binDirectory">Path to bin directory of the application</param>
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We never want to throw an exception from this method.")]
+ internal static void ForceRecompile(IFileSystem fileSystem, string binDirectory)
+ {
+ var fileToWrite = Path.Combine(binDirectory, ForceRecompilationFile);
+ try
+ {
+ // Note: We should use BuildManager::ForceRecompile once that method makes it into System.Web.
+ using (var writer = new StreamWriter(fileSystem.OpenFile(fileToWrite)))
+ {
+ writer.WriteLine();
+ }
+ }
+ catch
+ {
+ }
+ }
+
+ /// <summary>
+ /// Name of the the temporary file used by BuildManager.CreateCachedFile / BuildManager.ReadCachedFile where we cache WebPages's version number.
+ /// </summary>
+ /// <returns></returns>
+ private static string GetCachedFileName()
+ {
+ return typeof(WebPagesDeployment).Namespace;
+ }
+
+ private static string RemoveTrailingSlash(string path)
+ {
+ if (!String.IsNullOrEmpty(path))
+ {
+ path = path.TrimEnd(Path.DirectorySeparatorChar);
+ }
+ return path;
+ }
+ }
+}
diff --git a/src/System.Web.WebPages.Deployment/packages.config b/src/System.Web.WebPages.Deployment/packages.config
new file mode 100644
index 00000000..f143a04f
--- /dev/null
+++ b/src/System.Web.WebPages.Deployment/packages.config
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="Microsoft.Web.Infrastructure" version="1.0.0.0" />
+</packages> \ No newline at end of file
diff --git a/src/System.Web.WebPages.Razor/AssemblyBuilderWrapper.cs b/src/System.Web.WebPages.Razor/AssemblyBuilderWrapper.cs
new file mode 100644
index 00000000..2944df45
--- /dev/null
+++ b/src/System.Web.WebPages.Razor/AssemblyBuilderWrapper.cs
@@ -0,0 +1,30 @@
+using System.CodeDom;
+using System.Web.Compilation;
+
+namespace System.Web.WebPages.Razor
+{
+ internal sealed class AssemblyBuilderWrapper : IAssemblyBuilder
+ {
+ public AssemblyBuilderWrapper(AssemblyBuilder builder)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException("builder");
+ }
+
+ InnerBuilder = builder;
+ }
+
+ internal AssemblyBuilder InnerBuilder { get; set; }
+
+ public void AddCodeCompileUnit(BuildProvider buildProvider, CodeCompileUnit compileUnit)
+ {
+ InnerBuilder.AddCodeCompileUnit(buildProvider, compileUnit);
+ }
+
+ public void GenerateTypeFactory(string typeName)
+ {
+ InnerBuilder.GenerateTypeFactory(typeName);
+ }
+ }
+}
diff --git a/src/System.Web.WebPages.Razor/CompilingPathEventArgs.cs b/src/System.Web.WebPages.Razor/CompilingPathEventArgs.cs
new file mode 100644
index 00000000..952d3bbb
--- /dev/null
+++ b/src/System.Web.WebPages.Razor/CompilingPathEventArgs.cs
@@ -0,0 +1,14 @@
+namespace System.Web.WebPages.Razor
+{
+ public class CompilingPathEventArgs : EventArgs
+ {
+ public CompilingPathEventArgs(string virtualPath, WebPageRazorHost host)
+ {
+ VirtualPath = virtualPath;
+ Host = host;
+ }
+
+ public string VirtualPath { get; private set; }
+ public WebPageRazorHost Host { get; set; }
+ }
+}
diff --git a/src/System.Web.WebPages.Razor/Configuration/HostSection.cs b/src/System.Web.WebPages.Razor/Configuration/HostSection.cs
new file mode 100644
index 00000000..9909c6ea
--- /dev/null
+++ b/src/System.Web.WebPages.Razor/Configuration/HostSection.cs
@@ -0,0 +1,29 @@
+using System.Configuration;
+
+namespace System.Web.WebPages.Razor.Configuration
+{
+ public class HostSection : ConfigurationSection
+ {
+ public static readonly string SectionName = RazorWebSectionGroup.GroupName + "/host";
+
+ private static readonly ConfigurationProperty _typeProperty =
+ new ConfigurationProperty("factoryType",
+ typeof(string),
+ null,
+ ConfigurationPropertyOptions.IsRequired);
+
+ private bool _factoryTypeSet = false;
+ private string _factoryType;
+
+ [ConfigurationProperty("factoryType", IsRequired = true, DefaultValue = null)]
+ public string FactoryType
+ {
+ get { return _factoryTypeSet ? _factoryType : (string)this[_typeProperty]; }
+ set
+ {
+ _factoryType = value;
+ _factoryTypeSet = true;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.WebPages.Razor/Configuration/RazorPagesSection.cs b/src/System.Web.WebPages.Razor/Configuration/RazorPagesSection.cs
new file mode 100644
index 00000000..35a883d2
--- /dev/null
+++ b/src/System.Web.WebPages.Razor/Configuration/RazorPagesSection.cs
@@ -0,0 +1,52 @@
+using System.Configuration;
+using System.Diagnostics.CodeAnalysis;
+using System.Web.Configuration;
+
+namespace System.Web.WebPages.Razor.Configuration
+{
+ public class RazorPagesSection : ConfigurationSection
+ {
+ public static readonly string SectionName = RazorWebSectionGroup.GroupName + "/pages";
+
+ private static readonly ConfigurationProperty _pageBaseTypeProperty =
+ new ConfigurationProperty("pageBaseType",
+ typeof(string),
+ null,
+ ConfigurationPropertyOptions.IsRequired);
+
+ private static readonly ConfigurationProperty _namespacesProperty =
+ new ConfigurationProperty("namespaces",
+ typeof(NamespaceCollection),
+ null,
+ ConfigurationPropertyOptions.IsRequired);
+
+ private bool _pageBaseTypeSet = false;
+ private bool _namespacesSet = false;
+
+ private string _pageBaseType;
+ private NamespaceCollection _namespaces;
+
+ [ConfigurationProperty("pageBaseType", IsRequired = true)]
+ public string PageBaseType
+ {
+ get { return _pageBaseTypeSet ? _pageBaseType : (string)this[_pageBaseTypeProperty]; }
+ set
+ {
+ _pageBaseType = value;
+ _pageBaseTypeSet = true;
+ }
+ }
+
+ [ConfigurationProperty("namespaces", IsRequired = true)]
+ [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "Being able to set this property is extremely useful for third-parties who are testing components which interact with the Razor configuration system")]
+ public NamespaceCollection Namespaces
+ {
+ get { return _namespacesSet ? _namespaces : (NamespaceCollection)this[_namespacesProperty]; }
+ set
+ {
+ _namespaces = value;
+ _namespacesSet = true;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.WebPages.Razor/Configuration/RazorWebSectionGroup.cs b/src/System.Web.WebPages.Razor/Configuration/RazorWebSectionGroup.cs
new file mode 100644
index 00000000..6b59d5d1
--- /dev/null
+++ b/src/System.Web.WebPages.Razor/Configuration/RazorWebSectionGroup.cs
@@ -0,0 +1,38 @@
+using System.Configuration;
+
+namespace System.Web.WebPages.Razor.Configuration
+{
+ public class RazorWebSectionGroup : ConfigurationSectionGroup
+ {
+ public static readonly string GroupName = "system.web.webPages.razor";
+
+ // Use flags instead of null values since tests may want to set the property to null
+ private bool _hostSet = false;
+ private bool _pagesSet = false;
+
+ private HostSection _host;
+ private RazorPagesSection _pages;
+
+ [ConfigurationProperty("host", IsRequired = false)]
+ public HostSection Host
+ {
+ get { return _hostSet ? _host : (HostSection)Sections["host"]; }
+ set
+ {
+ _host = value;
+ _hostSet = true;
+ }
+ }
+
+ [ConfigurationProperty("pages", IsRequired = false)]
+ public RazorPagesSection Pages
+ {
+ get { return _pagesSet ? _pages : (RazorPagesSection)Sections["pages"]; }
+ set
+ {
+ _pages = value;
+ _pagesSet = true;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.WebPages.Razor/GlobalSuppressions.cs b/src/System.Web.WebPages.Razor/GlobalSuppressions.cs
new file mode 100644
index 00000000..4e03a177
--- /dev/null
+++ b/src/System.Web.WebPages.Razor/GlobalSuppressions.cs
@@ -0,0 +1,9 @@
+// This file is used by Code Analysis to maintain SuppressMessage
+// attributes that are applied to this project.
+// Project-level suppressions either have no target or are given
+// a specific target and scoped to a namespace, type, member, etc.
+//
+// To add a suppression to this file, right-click the message in the
+// Error List, point to "Suppress Message(s)", and click
+// "In Project Suppression File".
+// You do not need to add suppressions to this file manually.
diff --git a/src/System.Web.WebPages.Razor/HostingEnvironmentWrapper.cs b/src/System.Web.WebPages.Razor/HostingEnvironmentWrapper.cs
new file mode 100644
index 00000000..4179c812
--- /dev/null
+++ b/src/System.Web.WebPages.Razor/HostingEnvironmentWrapper.cs
@@ -0,0 +1,12 @@
+using System.Web.Hosting;
+
+namespace System.Web.WebPages.Razor
+{
+ internal sealed class HostingEnvironmentWrapper : IHostingEnvironment
+ {
+ public string MapPath(string virtualPath)
+ {
+ return HostingEnvironment.MapPath(virtualPath);
+ }
+ }
+}
diff --git a/src/System.Web.WebPages.Razor/IAssemblyBuilder.cs b/src/System.Web.WebPages.Razor/IAssemblyBuilder.cs
new file mode 100644
index 00000000..2a5323d6
--- /dev/null
+++ b/src/System.Web.WebPages.Razor/IAssemblyBuilder.cs
@@ -0,0 +1,11 @@
+using System.CodeDom;
+using System.Web.Compilation;
+
+namespace System.Web.WebPages.Razor
+{
+ internal interface IAssemblyBuilder
+ {
+ void AddCodeCompileUnit(BuildProvider buildProvider, CodeCompileUnit compileUnit);
+ void GenerateTypeFactory(string typeName);
+ }
+}
diff --git a/src/System.Web.WebPages.Razor/IHostingEnvironment.cs b/src/System.Web.WebPages.Razor/IHostingEnvironment.cs
new file mode 100644
index 00000000..71fd9f51
--- /dev/null
+++ b/src/System.Web.WebPages.Razor/IHostingEnvironment.cs
@@ -0,0 +1,7 @@
+namespace System.Web.WebPages.Razor
+{
+ internal interface IHostingEnvironment
+ {
+ string MapPath(string virtualPath);
+ }
+}
diff --git a/src/System.Web.WebPages.Razor/PreApplicationStartCode.cs b/src/System.Web.WebPages.Razor/PreApplicationStartCode.cs
new file mode 100644
index 00000000..e3653e1c
--- /dev/null
+++ b/src/System.Web.WebPages.Razor/PreApplicationStartCode.cs
@@ -0,0 +1,34 @@
+using System.ComponentModel;
+using System.Web.Compilation;
+
+namespace System.Web.WebPages.Razor
+{
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static class PreApplicationStartCode
+ {
+ // NOTE: Do not add public fields, methods, or other members to this class.
+ // This class does not show up in Intellisense so members on it will not be
+ // discoverable by users. Place new members on more appropriate classes that
+ // relate to the public API (for example, a LoginUrl property should go on a
+ // membership-related class).
+
+ private static bool _startWasCalled;
+
+ public static void Start()
+ {
+ // Even though ASP.NET will only call each PreAppStart once, we sometimes internally call one PreAppStart from
+ // another PreAppStart to ensure that things get initialized in the right order. ASP.NET does not guarantee the
+ // order so we have to guard against multiple calls.
+ // All Start calls are made on same thread, so no lock needed here.
+
+ if (_startWasCalled)
+ {
+ return;
+ }
+ _startWasCalled = true;
+
+ BuildProvider.RegisterBuildProvider(".cshtml", typeof(RazorBuildProvider));
+ BuildProvider.RegisterBuildProvider(".vbhtml", typeof(RazorBuildProvider));
+ }
+ }
+}
diff --git a/src/System.Web.WebPages.Razor/Properties/AssemblyInfo.cs b/src/System.Web.WebPages.Razor/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..264c4660
--- /dev/null
+++ b/src/System.Web.WebPages.Razor/Properties/AssemblyInfo.cs
@@ -0,0 +1,13 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Web;
+using System.Web.WebPages.Razor;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+
+[assembly: AssemblyTitle("System.Web.WebPages.Razor")]
+[assembly: AssemblyDescription("")]
+[assembly: InternalsVisibleTo("System.Web.WebPages.Razor.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
+[assembly: PreApplicationStartMethod(typeof(PreApplicationStartCode), "Start")]
diff --git a/src/System.Web.WebPages.Razor/RazorBuildProvider.cs b/src/System.Web.WebPages.Razor/RazorBuildProvider.cs
new file mode 100644
index 00000000..17dda19f
--- /dev/null
+++ b/src/System.Web.WebPages.Razor/RazorBuildProvider.cs
@@ -0,0 +1,252 @@
+using System.CodeDom;
+using System.CodeDom.Compiler;
+using System.Collections;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Security;
+using System.Web.Compilation;
+using System.Web.Razor;
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser.SyntaxTree;
+
+namespace System.Web.WebPages.Razor
+{
+ [BuildProviderAppliesTo(BuildProviderAppliesTo.Web | BuildProviderAppliesTo.Code)]
+ public class RazorBuildProvider : BuildProvider
+ {
+ private static bool? _isFullTrust;
+ private CodeCompileUnit _generatedCode = null;
+ private WebPageRazorHost _host = null;
+ private IList _virtualPathDependencies;
+ private IAssemblyBuilder _assemblyBuilder;
+ public static event EventHandler<CodeGenerationCompleteEventArgs> CodeGenerationCompleted;
+
+ internal event EventHandler<CodeGenerationCompleteEventArgs> CodeGenerationCompletedInternal
+ {
+ add { _codeGenerationCompletedInternal += value; }
+ remove { _codeGenerationCompletedInternal -= value; }
+ }
+
+ public static event EventHandler CodeGenerationStarted;
+
+ /// <summary>
+ /// For unit testing.
+ /// </summary>
+ internal event EventHandler CodeGenerationStartedInternal
+ {
+ add { _codeGenerationStartedInternal += value; }
+ remove { _codeGenerationStartedInternal -= value; }
+ }
+
+ public static event EventHandler<CompilingPathEventArgs> CompilingPath;
+
+ /// <summary>
+ /// For unit testing
+ /// </summary>
+ private event EventHandler<CodeGenerationCompleteEventArgs> _codeGenerationCompletedInternal;
+ private event EventHandler _codeGenerationStartedInternal;
+
+ internal WebPageRazorHost Host
+ {
+ get
+ {
+ if (_host == null)
+ {
+ _host = CreateHost();
+ }
+ return _host;
+ }
+ set { _host = value; }
+ }
+
+ // Returns the base dependencies and any dependencies added via AddVirtualPathDependencies
+ public override ICollection VirtualPathDependencies
+ {
+ get
+ {
+ if (_virtualPathDependencies != null)
+ {
+ // Return a readonly wrapper so as to prevent users from modifying the collection directly.
+ return ArrayList.ReadOnly(_virtualPathDependencies);
+ }
+ else
+ {
+ return base.VirtualPathDependencies;
+ }
+ }
+ }
+
+ public new string VirtualPath
+ {
+ get { return base.VirtualPath; }
+ }
+
+ public AssemblyBuilder AssemblyBuilder
+ {
+ get
+ {
+ var wrapper = _assemblyBuilder as AssemblyBuilderWrapper;
+ if (wrapper != null)
+ {
+ return wrapper.InnerBuilder;
+ }
+ else
+ {
+ return null;
+ }
+ }
+ }
+
+ // For unit testing
+ internal IAssemblyBuilder AssemblyBuilderInternal
+ {
+ get { return _assemblyBuilder; }
+ }
+
+ internal CodeCompileUnit GeneratedCode
+ {
+ get
+ {
+ EnsureGeneratedCode();
+ return _generatedCode;
+ }
+ set { _generatedCode = value; }
+ }
+
+ public override CompilerType CodeCompilerType
+ {
+ get
+ {
+ EnsureGeneratedCode();
+ CompilerType compilerType = GetDefaultCompilerTypeForLanguage(Host.CodeLanguage.LanguageName);
+ if (_isFullTrust != false && Host.DefaultDebugCompilation)
+ {
+ try
+ {
+ SetIncludeDebugInfoFlag(compilerType);
+ _isFullTrust = true;
+ }
+ catch (SecurityException)
+ {
+ _isFullTrust = false;
+ }
+ }
+ return compilerType;
+ }
+ }
+
+ public void AddVirtualPathDependency(string dependency)
+ {
+ if (_virtualPathDependencies == null)
+ {
+ // Initialize the collection containing the base dependencies
+ _virtualPathDependencies = new ArrayList(base.VirtualPathDependencies);
+ }
+
+ _virtualPathDependencies.Add(dependency);
+ }
+
+ public override Type GetGeneratedType(CompilerResults results)
+ {
+ return results.CompiledAssembly.GetType(String.Format(CultureInfo.CurrentCulture, "{0}.{1}", Host.DefaultNamespace, Host.DefaultClassName));
+ }
+
+ public override void GenerateCode(AssemblyBuilder assemblyBuilder)
+ {
+ GenerateCodeCore(new AssemblyBuilderWrapper(assemblyBuilder));
+ }
+
+ internal virtual void GenerateCodeCore(IAssemblyBuilder assemblyBuilder)
+ {
+ OnCodeGenerationStarted(assemblyBuilder);
+ assemblyBuilder.AddCodeCompileUnit(this, GeneratedCode);
+ assemblyBuilder.GenerateTypeFactory(String.Format(CultureInfo.InvariantCulture, "{0}.{1}", Host.DefaultNamespace, Host.DefaultClassName));
+ }
+
+ protected internal virtual TextReader InternalOpenReader()
+ {
+ return OpenReader();
+ }
+
+ protected internal virtual WebPageRazorHost CreateHost()
+ {
+ // Get the host from config
+ WebPageRazorHost configuredHost = GetHostFromConfig();
+
+ // Fire the event
+ CompilingPathEventArgs args = new CompilingPathEventArgs(VirtualPath, configuredHost);
+ OnBeforeCompilePath(args);
+
+ // Return the host provided in the args, which may have been changed by the handler
+ return args.Host;
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This method performs significant work and a property would not be appropriate")]
+ protected internal virtual WebPageRazorHost GetHostFromConfig()
+ {
+ return WebRazorHostFactory.CreateHostFromConfig(VirtualPath);
+ }
+
+ protected virtual void OnBeforeCompilePath(CompilingPathEventArgs args)
+ {
+ EventHandler<CompilingPathEventArgs> handler = CompilingPath;
+ if (handler != null)
+ {
+ handler(this, args);
+ }
+ }
+
+ private void OnCodeGenerationStarted(IAssemblyBuilder assemblyBuilder)
+ {
+ _assemblyBuilder = assemblyBuilder;
+ EventHandler handler = _codeGenerationStartedInternal ?? CodeGenerationStarted;
+ if (handler != null)
+ {
+ handler(this, null);
+ }
+ }
+
+ private void OnCodeGenerationCompleted(CodeCompileUnit generatedCode)
+ {
+ EventHandler<CodeGenerationCompleteEventArgs> handler = _codeGenerationCompletedInternal ?? CodeGenerationCompleted;
+ if (handler != null)
+ {
+ handler(this, new CodeGenerationCompleteEventArgs(Host.VirtualPath, Host.PhysicalPath, generatedCode));
+ }
+ }
+
+ private void EnsureGeneratedCode()
+ {
+ if (_generatedCode == null)
+ {
+ RazorTemplateEngine engine = new RazorTemplateEngine(Host);
+ GeneratorResults results = null;
+ using (TextReader reader = InternalOpenReader())
+ {
+ results = engine.GenerateCode(reader, className: null, rootNamespace: null, sourceFileName: Host.PhysicalPath);
+ }
+ if (!results.Success)
+ {
+ throw CreateExceptionFromParserError(results.ParserErrors.Last(), VirtualPath);
+ }
+ _generatedCode = results.GeneratedCode;
+
+ // Run the code gen complete event
+ OnCodeGenerationCompleted(_generatedCode);
+ }
+ }
+
+ private static HttpParseException CreateExceptionFromParserError(RazorError error, string virtualPath)
+ {
+ return new HttpParseException(error.Message + Environment.NewLine, null, virtualPath, null, error.Location.LineIndex + 1);
+ }
+
+ [SuppressMessage("Microsoft.Security", "CA2141:TransparentMethodsMustNotSatisfyLinkDemandsFxCopRule", Justification = "We are catching the SecurityException to detect medium trust")]
+ private static void SetIncludeDebugInfoFlag(CompilerType compilerType)
+ {
+ compilerType.CompilerParameters.IncludeDebugInformation = true;
+ }
+ }
+}
diff --git a/src/System.Web.WebPages.Razor/Resources/RazorWebResources.Designer.cs b/src/System.Web.WebPages.Razor/Resources/RazorWebResources.Designer.cs
new file mode 100644
index 00000000..e6a1a76f
--- /dev/null
+++ b/src/System.Web.WebPages.Razor/Resources/RazorWebResources.Designer.cs
@@ -0,0 +1,81 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.1
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace System.Web.WebPages.Razor.Resources {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class RazorWebResources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal RazorWebResources() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("System.Web.WebPages.Razor.Resources.RazorWebResources", typeof(RazorWebResources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Could not determine the code language for &quot;{0}&quot;..
+ /// </summary>
+ internal static string BuildProvider_No_CodeLanguageService_For_Path {
+ get {
+ return ResourceManager.GetString("BuildProvider_No_CodeLanguageService_For_Path", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Could not locate Razor Host Factory type: {0}.
+ /// </summary>
+ internal static string Could_Not_Locate_FactoryType {
+ get {
+ return ResourceManager.GetString("Could_Not_Locate_FactoryType", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.WebPages.Razor/Resources/RazorWebResources.resx b/src/System.Web.WebPages.Razor/Resources/RazorWebResources.resx
new file mode 100644
index 00000000..9ecd60cd
--- /dev/null
+++ b/src/System.Web.WebPages.Razor/Resources/RazorWebResources.resx
@@ -0,0 +1,126 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="BuildProvider_No_CodeLanguageService_For_Path" xml:space="preserve">
+ <value>Could not determine the code language for "{0}".</value>
+ </data>
+ <data name="Could_Not_Locate_FactoryType" xml:space="preserve">
+ <value>Could not locate Razor Host Factory type: {0}</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/System.Web.WebPages.Razor/System.Web.WebPages.Razor.csproj b/src/System.Web.WebPages.Razor/System.Web.WebPages.Razor.csproj
new file mode 100644
index 00000000..3192a9d0
--- /dev/null
+++ b/src/System.Web.WebPages.Razor/System.Web.WebPages.Razor.csproj
@@ -0,0 +1,113 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <CodeAnalysis Condition=" '$(CodeAnalysis)' == '' ">false</CodeAnalysis>
+ <ProductVersion>9.0.30729</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{0939B11A-FE4E-4BA1-8AD6-D97741EE314F}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <RootNamespace>System.Web.WebPages.Razor</RootNamespace>
+ <AssemblyName>System.Web.WebPages.Razor</AssemblyName>
+ <FileAlignment>512</FileAlignment>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>..\..\bin\Debug\</OutputPath>
+ <DefineConstants>TRACE;DEBUG;ASPNETWEBPAGES</DefineConstants>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ <DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
+ <NoWarn>1591</NoWarn>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>..\..\bin\Release\</OutputPath>
+ <DefineConstants>TRACE;ASPNETWEBPAGES</DefineConstants>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ <RunCodeAnalysis>$(CodeAnalysis)</RunCodeAnalysis>
+ <DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
+ <NoWarn>1591</NoWarn>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'CodeCoverage|AnyCPU'">
+ <DebugSymbols>true</DebugSymbols>
+ <OutputPath>..\..\bin\CodeCoverage\</OutputPath>
+ <DefineConstants>TRACE;DEBUG;CODE_COVERAGE;ASPNETWEBPAGES</DefineConstants>
+ <DebugType>full</DebugType>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="System" />
+ <Reference Include="System.Configuration" />
+ <Reference Include="System.Web" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="..\CommonAssemblyInfo.cs">
+ <Link>Properties\CommonAssemblyInfo.cs</Link>
+ </Compile>
+ <Compile Include="..\CommonResources.Designer.cs">
+ <Link>Common\CommonResources.Designer.cs</Link>
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>CommonResources.resx</DependentUpon>
+ </Compile>
+ <Compile Include="..\GlobalSuppressions.cs">
+ <Link>Common\GlobalSuppressions.cs</Link>
+ </Compile>
+ <Compile Include="..\TransparentCommonAssemblyInfo.cs">
+ <Link>Properties\TransparentCommonAssemblyInfo.cs</Link>
+ </Compile>
+ <Compile Include="AssemblyBuilderWrapper.cs" />
+ <Compile Include="CompilingPathEventArgs.cs" />
+ <Compile Include="Configuration\HostSection.cs" />
+ <Compile Include="Configuration\RazorPagesSection.cs" />
+ <Compile Include="Configuration\RazorWebSectionGroup.cs" />
+ <Compile Include="GlobalSuppressions.cs" />
+ <Compile Include="HostingEnvironmentWrapper.cs" />
+ <Compile Include="IAssemblyBuilder.cs" />
+ <Compile Include="IHostingEnvironment.cs" />
+ <Compile Include="PreApplicationStartCode.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="RazorBuildProvider.cs" />
+ <Compile Include="WebRazorHostFactory.cs" />
+ <Compile Include="WebCodeRazorHost.cs" />
+ <Compile Include="WebPageRazorHost.cs" />
+ <Compile Include="Resources\RazorWebResources.Designer.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>RazorWebResources.resx</DependentUpon>
+ </Compile>
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\System.Web.WebPages\System.Web.WebPages.csproj">
+ <Project>{76EFA9C5-8D7E-4FDF-B710-E20F8B6B00D2}</Project>
+ <Name>System.Web.WebPages</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\System.Web.Razor\System.Web.Razor.csproj">
+ <Project>{8F18041B-9410-4C36-A9C5-067813DF5F31}</Project>
+ <Name>System.Web.Razor</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="..\CommonResources.resx">
+ <Link>Common\CommonResources.resx</Link>
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>CommonResources.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Include="Resources\RazorWebResources.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>RazorWebResources.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ </ItemGroup>
+ <ItemGroup>
+ <CodeAnalysisDictionary Include="..\CodeAnalysisDictionary.xml" />
+ </ItemGroup>
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+</Project> \ No newline at end of file
diff --git a/src/System.Web.WebPages.Razor/WebCodeRazorHost.cs b/src/System.Web.WebPages.Razor/WebCodeRazorHost.cs
new file mode 100644
index 00000000..3d155c73
--- /dev/null
+++ b/src/System.Web.WebPages.Razor/WebCodeRazorHost.cs
@@ -0,0 +1,99 @@
+using System.CodeDom;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Linq;
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser;
+
+namespace System.Web.WebPages.Razor
+{
+ public class WebCodeRazorHost : WebPageRazorHost
+ {
+ private const string AppCodeDir = "App_Code";
+ private const string HttpContextAccessorName = "Context";
+ private static readonly string _helperPageBaseType = typeof(HelperPage).FullName;
+
+ [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "The code path is safe, it is a property setter and not dependent on other state")]
+ public WebCodeRazorHost(string virtualPath)
+ : base(virtualPath)
+ {
+ DefaultBaseClass = _helperPageBaseType;
+ DefaultNamespace = DetermineNamespace(virtualPath);
+ DefaultDebugCompilation = false;
+ StaticHelpers = true;
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "The code path is safe, it is a property setter and not dependent on other state")]
+ public WebCodeRazorHost(string virtualPath, string physicalPath)
+ : base(virtualPath, physicalPath)
+ {
+ DefaultBaseClass = _helperPageBaseType;
+ DefaultNamespace = DetermineNamespace(virtualPath);
+ DefaultDebugCompilation = false;
+ StaticHelpers = true;
+ }
+
+ public override void PostProcessGeneratedCode(CodeGeneratorContext context)
+ {
+ base.PostProcessGeneratedCode(context);
+
+ // Yank out the execute method (ignored in Razor Web Code pages)
+ context.GeneratedClass.Members.Remove(context.TargetMethod);
+
+ // Make ApplicationInstance static
+ CodeMemberProperty appInstanceProperty =
+ context.GeneratedClass.Members
+ .OfType<CodeMemberProperty>()
+ .Where(p => ApplicationInstancePropertyName
+ .Equals(p.Name))
+ .SingleOrDefault();
+
+ if (appInstanceProperty != null)
+ {
+ appInstanceProperty.Attributes |= MemberAttributes.Static;
+ }
+ }
+
+ protected override string GetClassName(string virtualPath)
+ {
+ return ParserHelpers.SanitizeClassName(Path.GetFileNameWithoutExtension(virtualPath));
+ }
+
+ private static string DetermineNamespace(string virtualPath)
+ {
+ // Normailzize the virtual path
+ virtualPath = virtualPath.Replace(Path.DirectorySeparatorChar, '/');
+
+ // Get the directory
+ virtualPath = GetDirectory(virtualPath);
+
+ // Skip the App_Code segment if any
+ int appCodeIndex = virtualPath.IndexOf(AppCodeDir, StringComparison.OrdinalIgnoreCase);
+ if (appCodeIndex != -1)
+ {
+ virtualPath = virtualPath.Substring(appCodeIndex + AppCodeDir.Length);
+ }
+
+ // Get the segments removing any empty entries
+ IEnumerable<string> segments = virtualPath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
+
+ if (!segments.Any())
+ {
+ return WebDefaultNamespace;
+ }
+
+ return WebDefaultNamespace + "." + String.Join(".", segments);
+ }
+
+ private static string GetDirectory(string virtualPath)
+ {
+ int lastSlash = virtualPath.LastIndexOf('/');
+ if (lastSlash != -1)
+ {
+ return virtualPath.Substring(0, lastSlash);
+ }
+ return String.Empty;
+ }
+ }
+}
diff --git a/src/System.Web.WebPages.Razor/WebPageRazorHost.cs b/src/System.Web.WebPages.Razor/WebPageRazorHost.cs
new file mode 100644
index 00000000..b5bb5c91
--- /dev/null
+++ b/src/System.Web.WebPages.Razor/WebPageRazorHost.cs
@@ -0,0 +1,351 @@
+using System.CodeDom;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Web.Compilation;
+using System.Web.Hosting;
+using System.Web.Razor;
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser;
+using System.Web.WebPages.Instrumentation;
+using System.Web.WebPages.Razor.Resources;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.WebPages.Razor
+{
+ public class WebPageRazorHost : RazorEngineHost
+ {
+ // DevDiv Bug 941404 - Add a prefix and folder name to class names
+ internal const string PageClassNamePrefix = "_Page_";
+ internal const string ApplicationInstancePropertyName = "ApplicationInstance";
+ internal const string ContextPropertyName = "Context";
+ internal const string DefineSectionMethodName = "DefineSection";
+ internal const string WebDefaultNamespace = "ASP";
+ internal const string WriteToMethodName = "WriteTo";
+ internal const string WriteLiteralToMethodName = "WriteLiteralTo";
+ internal const string BeginContextMethodName = "BeginContext";
+ internal const string EndContextMethodName = "EndContext";
+ internal const string ResolveUrlMethodName = "Href";
+
+ private const string ApplicationStartFileName = "_AppStart";
+ private const string PageStartFileName = "_PageStart";
+
+ internal static readonly string FallbackApplicationTypeName = typeof(HttpApplication).FullName;
+ internal static readonly string PageBaseClass = typeof(WebPage).FullName;
+ internal static readonly string TemplateTypeName = typeof(HelperResult).FullName;
+
+ private static ConcurrentDictionary<string, object> _importedNamespaces = new ConcurrentDictionary<string, object>();
+ private readonly Dictionary<string, string> _specialFileBaseTypes = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+
+ private string _className;
+ private RazorCodeLanguage _codeLanguage;
+ private string _globalAsaxTypeName;
+ private bool? _isSpecialPage;
+ private string _physicalPath = null;
+ private string _specialFileBaseClass;
+
+ [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "The code path is safe, it is a property setter and not dependent on other state")]
+ private WebPageRazorHost()
+ {
+ NamespaceImports.Add("System");
+ NamespaceImports.Add("System.Collections.Generic");
+ NamespaceImports.Add("System.IO");
+ NamespaceImports.Add("System.Linq");
+ NamespaceImports.Add("System.Net");
+ NamespaceImports.Add("System.Web");
+ NamespaceImports.Add("System.Web.Helpers");
+ NamespaceImports.Add("System.Web.Security");
+ NamespaceImports.Add("System.Web.UI");
+ NamespaceImports.Add("System.Web.WebPages");
+ NamespaceImports.Add("System.Web.WebPages.Html");
+
+ RegisterSpecialFile(ApplicationStartFileName, typeof(ApplicationStartPage));
+ RegisterSpecialFile(PageStartFileName, typeof(StartPage));
+ DefaultNamespace = WebDefaultNamespace;
+ GeneratedClassContext = new GeneratedClassContext(GeneratedClassContext.DefaultExecuteMethodName,
+ GeneratedClassContext.DefaultWriteMethodName,
+ GeneratedClassContext.DefaultWriteLiteralMethodName,
+ WriteToMethodName,
+ WriteLiteralToMethodName,
+ TemplateTypeName,
+ DefineSectionMethodName,
+ BeginContextMethodName,
+ EndContextMethodName)
+ {
+ ResolveUrlMethodName = ResolveUrlMethodName
+ };
+ DefaultPageBaseClass = PageBaseClass;
+ DefaultDebugCompilation = true;
+ EnableInstrumentation = false;
+ }
+
+ public WebPageRazorHost(string virtualPath)
+ : this(virtualPath, null)
+ {
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "The code path is safe, it is a property setter and not dependent on other state")]
+ public WebPageRazorHost(string virtualPath, string physicalPath)
+ : this()
+ {
+ if (String.IsNullOrEmpty(virtualPath))
+ {
+ throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, CommonResources.Argument_Cannot_Be_Null_Or_Empty, "virtualPath"), "virtualPath");
+ }
+
+ VirtualPath = virtualPath;
+
+ PhysicalPath = physicalPath;
+ DefaultClassName = GetClassName(VirtualPath);
+ CodeLanguage = GetCodeLanguage();
+ EnableInstrumentation = new InstrumentationService().IsAvailable;
+ }
+
+ public override RazorCodeLanguage CodeLanguage
+ {
+ get
+ {
+ if (_codeLanguage == null)
+ {
+ _codeLanguage = GetCodeLanguage();
+ }
+ return _codeLanguage;
+ }
+ protected set { _codeLanguage = value; }
+ }
+
+ public override string DefaultBaseClass
+ {
+ get
+ {
+ if (base.DefaultBaseClass != null)
+ {
+ return base.DefaultBaseClass;
+ }
+ if (IsSpecialPage)
+ {
+ return SpecialPageBaseClass;
+ }
+ else
+ {
+ return DefaultPageBaseClass;
+ }
+ }
+ set { base.DefaultBaseClass = value; }
+ }
+
+ public override string DefaultClassName
+ {
+ get
+ {
+ if (_className == null)
+ {
+ _className = GetClassName(VirtualPath);
+ }
+ return _className;
+ }
+ set { _className = value; }
+ }
+
+ public bool DefaultDebugCompilation { get; set; }
+
+ public string DefaultPageBaseClass { get; set; }
+
+ internal string GlobalAsaxTypeName
+ {
+ get { return _globalAsaxTypeName ?? (HostingEnvironment.IsHosted ? BuildManager.GetGlobalAsaxType().FullName : FallbackApplicationTypeName); }
+ set { _globalAsaxTypeName = value; }
+ }
+
+ public bool IsSpecialPage
+ {
+ get
+ {
+ CheckForSpecialPage();
+ return _isSpecialPage.Value;
+ }
+ }
+
+ public string PhysicalPath
+ {
+ get
+ {
+ MapPhysicalPath();
+ return _physicalPath;
+ }
+ set { _physicalPath = value; }
+ }
+
+ public override string InstrumentedSourceFilePath
+ {
+ get { return VirtualPath; }
+ set { VirtualPath = value; }
+ }
+
+ private string SpecialPageBaseClass
+ {
+ get
+ {
+ CheckForSpecialPage();
+ return _specialFileBaseClass;
+ }
+ }
+
+ public string VirtualPath { get; private set; }
+
+ public static void AddGlobalImport(string ns)
+ {
+ if (String.IsNullOrEmpty(ns))
+ {
+ throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, CommonResources.Argument_Cannot_Be_Null_Or_Empty, "ns"), "ns");
+ }
+
+ _importedNamespaces.TryAdd(ns, null);
+ }
+
+ private void CheckForSpecialPage()
+ {
+ if (!_isSpecialPage.HasValue)
+ {
+ string fileName = Path.GetFileNameWithoutExtension(VirtualPath);
+ string baseType;
+ if (_specialFileBaseTypes.TryGetValue(fileName, out baseType))
+ {
+ _isSpecialPage = true;
+ _specialFileBaseClass = baseType;
+ }
+ else
+ {
+ _isSpecialPage = false;
+ }
+ }
+ }
+
+ public override ParserBase CreateMarkupParser()
+ {
+ return new HtmlMarkupParser();
+ }
+
+ private static RazorCodeLanguage DetermineCodeLanguage(string fileName)
+ {
+ string extension = Path.GetExtension(fileName);
+
+ // Use an if rather than else-if just in case Path.GetExtension returns null for some reason
+ if (String.IsNullOrEmpty(extension))
+ {
+ return null;
+ }
+ if (extension[0] == '.')
+ {
+ extension = extension.Substring(1); // Trim off the dot
+ }
+
+ // Look up the language
+ // At the moment this only deals with code languages: cs, vb, etc., but in theory we could have MarkupLanguageServices which allow for
+ // interesting combinations like: vbcss, csxml, etc.
+ RazorCodeLanguage language = GetLanguageByExtension(extension);
+ return language;
+ }
+
+ protected virtual string GetClassName(string virtualPath)
+ {
+ // Remove "~/" and run through our santizer
+ // For example, for ~/Foo/Bar/Baz.cshtml, the class name is _Page_Foo_Bar_Baz_cshtml
+ return ParserHelpers.SanitizeClassName(PageClassNamePrefix + virtualPath.TrimStart('~', '/'));
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Method involves significant processing and should not be a property")]
+ protected virtual RazorCodeLanguage GetCodeLanguage()
+ {
+ RazorCodeLanguage language = DetermineCodeLanguage(VirtualPath);
+ if (language == null && !String.IsNullOrEmpty(PhysicalPath))
+ {
+ language = DetermineCodeLanguage(PhysicalPath);
+ }
+
+ if (language == null)
+ {
+ throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, RazorWebResources.BuildProvider_No_CodeLanguageService_For_Path, VirtualPath));
+ }
+
+ return language;
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This method involves copying memory, so a property is not appropriate")]
+ public static IEnumerable<string> GetGlobalImports()
+ {
+ return _importedNamespaces.ToArray().Select(pair => pair.Key);
+ }
+
+ private static RazorCodeLanguage GetLanguageByExtension(string extension)
+ {
+ return RazorCodeLanguage.GetLanguageByExtension(extension);
+ }
+
+ private void MapPhysicalPath()
+ {
+ if (_physicalPath == null && HostingEnvironment.IsHosted)
+ {
+ string path = HostingEnvironment.MapPath(VirtualPath);
+ if (!String.IsNullOrEmpty(path) && File.Exists(path))
+ {
+ _physicalPath = path;
+ }
+ }
+ }
+
+ public override void PostProcessGeneratedCode(CodeGeneratorContext context)
+ {
+ base.PostProcessGeneratedCode(context);
+
+ // Add additional global imports
+ context.Namespace.Imports.AddRange(GetGlobalImports().Select(s => new CodeNamespaceImport(s)).ToArray());
+
+ // Create ApplicationInstance property
+ CodeMemberProperty prop = new CodeMemberProperty()
+ {
+ Name = ApplicationInstancePropertyName,
+ Type = new CodeTypeReference(GlobalAsaxTypeName),
+ HasGet = true,
+ HasSet = false,
+ Attributes = MemberAttributes.Family | MemberAttributes.Final
+ };
+ prop.GetStatements.Add(
+ new CodeMethodReturnStatement(
+ new CodeCastExpression(
+ new CodeTypeReference(GlobalAsaxTypeName),
+ new CodePropertyReferenceExpression(
+ new CodePropertyReferenceExpression(
+ null,
+ ContextPropertyName),
+ ApplicationInstancePropertyName))));
+ context.GeneratedClass.Members.Insert(0, prop);
+ }
+
+ protected void RegisterSpecialFile(string fileName, Type baseType)
+ {
+ if (baseType == null)
+ {
+ throw new ArgumentNullException("baseType");
+ }
+ RegisterSpecialFile(fileName, baseType.FullName);
+ }
+
+ protected void RegisterSpecialFile(string fileName, string baseTypeName)
+ {
+ if (String.IsNullOrEmpty(fileName))
+ {
+ throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, CommonResources.Argument_Cannot_Be_Null_Or_Empty, "fileName"), "fileName");
+ }
+ if (String.IsNullOrEmpty(baseTypeName))
+ {
+ throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, CommonResources.Argument_Cannot_Be_Null_Or_Empty, "baseTypeName"), "baseTypeName");
+ }
+
+ _specialFileBaseTypes[fileName] = baseTypeName;
+ }
+ }
+}
diff --git a/src/System.Web.WebPages.Razor/WebRazorHostFactory.cs b/src/System.Web.WebPages.Razor/WebRazorHostFactory.cs
new file mode 100644
index 00000000..9623d719
--- /dev/null
+++ b/src/System.Web.WebPages.Razor/WebRazorHostFactory.cs
@@ -0,0 +1,175 @@
+using System.Collections.Concurrent;
+using System.Diagnostics;
+using System.Globalization;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Web.Compilation;
+using System.Web.Configuration;
+using System.Web.Hosting;
+using System.Web.WebPages.Razor.Configuration;
+using System.Web.WebPages.Razor.Resources;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.WebPages.Razor
+{
+ public class WebRazorHostFactory
+ {
+ private static ConcurrentDictionary<string, Func<WebRazorHostFactory>> _factories =
+ new ConcurrentDictionary<string, Func<WebRazorHostFactory>>(StringComparer.OrdinalIgnoreCase);
+
+ internal static Func<string, Type> TypeFactory = DefaultTypeFactory;
+
+ public static WebPageRazorHost CreateDefaultHost(string virtualPath)
+ {
+ return CreateDefaultHost(virtualPath, null);
+ }
+
+ public static WebPageRazorHost CreateDefaultHost(string virtualPath, string physicalPath)
+ {
+ return CreateHostFromConfigCore(null, virtualPath, physicalPath);
+ }
+
+ public static WebPageRazorHost CreateHostFromConfig(string virtualPath)
+ {
+ return CreateHostFromConfig(virtualPath, null);
+ }
+
+ public static WebPageRazorHost CreateHostFromConfig(string virtualPath, string physicalPath)
+ {
+ if (String.IsNullOrEmpty(virtualPath))
+ {
+ throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, CommonResources.Argument_Cannot_Be_Null_Or_Empty, "virtualPath"), "virtualPath");
+ }
+
+ return CreateHostFromConfigCore(GetRazorSection(virtualPath), virtualPath, physicalPath);
+ }
+
+ public static WebPageRazorHost CreateHostFromConfig(RazorWebSectionGroup config, string virtualPath)
+ {
+ return CreateHostFromConfig(config, virtualPath, null);
+ }
+
+ public static WebPageRazorHost CreateHostFromConfig(RazorWebSectionGroup config, string virtualPath, string physicalPath)
+ {
+ if (config == null)
+ {
+ throw new ArgumentNullException("config");
+ }
+ if (String.IsNullOrEmpty(virtualPath))
+ {
+ throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, CommonResources.Argument_Cannot_Be_Null_Or_Empty, "virtualPath"), "virtualPath");
+ }
+
+ return CreateHostFromConfigCore(config, virtualPath, physicalPath);
+ }
+
+ internal static WebPageRazorHost CreateHostFromConfigCore(RazorWebSectionGroup config, string virtualPath, string physicalPath)
+ {
+ // Use the virtual path to select a host environment for the generated code
+ // Do this check here because the App_Code host can't be overridden.
+
+ // Make the path app relative
+ virtualPath = EnsureAppRelative(virtualPath);
+
+ WebPageRazorHost host;
+ if (virtualPath.StartsWith("~/App_Code", StringComparison.OrdinalIgnoreCase))
+ {
+ // Under App_Code => It's a Web Code file
+ host = new WebCodeRazorHost(virtualPath, physicalPath);
+ }
+ else
+ {
+ WebRazorHostFactory factory = null;
+ if (config != null && config.Host != null && !String.IsNullOrEmpty(config.Host.FactoryType))
+ {
+ Func<WebRazorHostFactory> factoryCreator = _factories.GetOrAdd(config.Host.FactoryType, CreateFactory);
+ Debug.Assert(factoryCreator != null); // CreateFactory should throw if there's an error creating the factory
+ factory = factoryCreator();
+ }
+
+ host = (factory ?? new WebRazorHostFactory()).CreateHost(virtualPath, physicalPath);
+
+ if (config != null && config.Pages != null)
+ {
+ ApplyConfigurationToHost(config.Pages, host);
+ }
+ }
+
+ return host;
+ }
+
+ private static Func<WebRazorHostFactory> CreateFactory(string typeName)
+ {
+ Type factoryType = TypeFactory(typeName);
+ if (factoryType == null)
+ {
+ throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture,
+ RazorWebResources.Could_Not_Locate_FactoryType,
+ typeName));
+ }
+ return Expression.Lambda<Func<WebRazorHostFactory>>(Expression.New(factoryType))
+ .Compile();
+ }
+
+ public static void ApplyConfigurationToHost(RazorPagesSection config, WebPageRazorHost host)
+ {
+ host.DefaultPageBaseClass = config.PageBaseType;
+
+ // Add imports
+ foreach (string import in config.Namespaces.OfType<NamespaceInfo>().Select(ns => ns.Namespace))
+ {
+ host.NamespaceImports.Add(import);
+ }
+ }
+
+ public virtual WebPageRazorHost CreateHost(string virtualPath, string physicalPath)
+ {
+ return new WebPageRazorHost(virtualPath, physicalPath);
+ }
+
+ internal static RazorWebSectionGroup GetRazorSection(string virtualPath)
+ {
+ // Get the individual sections (we can only use GetSection in medium trust) and then reconstruct the section group
+ return new RazorWebSectionGroup()
+ {
+ Host = (HostSection)WebConfigurationManager.GetSection(HostSection.SectionName, virtualPath),
+ Pages = (RazorPagesSection)WebConfigurationManager.GetSection(RazorPagesSection.SectionName, virtualPath)
+ };
+ }
+
+#if CODE_COVERAGE
+ [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ // JUSTIFICATION: VirtualPathUtility.ToAppRelative is only available in ASP.Net environment
+#endif
+
+ private static string EnsureAppRelative(string virtualPath)
+ {
+ if (HostingEnvironment.IsHosted)
+ {
+ virtualPath = VirtualPathUtility.ToAppRelative(virtualPath);
+ }
+ else
+ {
+ if (virtualPath.StartsWith("/", StringComparison.Ordinal))
+ {
+ virtualPath = "~" + virtualPath;
+ }
+ else if (!virtualPath.StartsWith("~/", StringComparison.Ordinal))
+ {
+ virtualPath = "~/" + virtualPath;
+ }
+ }
+ return virtualPath;
+ }
+
+#if CODE_COVERAGE
+ [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ // JUSTIFICATION: BuildManager.GetType is only available in ASP.Net environment
+#endif
+
+ private static Type DefaultTypeFactory(string typeName)
+ {
+ return BuildManager.GetType(typeName, false, false);
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/ApplicationPart.cs b/src/System.Web.WebPages/ApplicationPart.cs
new file mode 100644
index 00000000..791eb661
--- /dev/null
+++ b/src/System.Web.WebPages/ApplicationPart.cs
@@ -0,0 +1,226 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Web.Routing;
+using System.Web.WebPages.ApplicationParts;
+using System.Web.WebPages.Resources;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.WebPages
+{
+ public class ApplicationPart
+ {
+ private const string ModuleRootSyntax = "@/";
+ private const string ResourceVirtualPathRoot = "~/r.ashx/";
+ private const string ResourceRoute = "r.ashx/{module}/{*path}";
+ private static readonly LazyAction _initApplicationPart = new LazyAction(InitApplicationParts);
+ private static ApplicationPartRegistry _partRegistry;
+ private readonly Lazy<IDictionary<string, string>> _applicationPartResources;
+ private readonly Lazy<string> _applicationPartName;
+
+ public ApplicationPart(Assembly assembly, string rootVirtualPath)
+ : this(new ResourceAssembly(assembly), rootVirtualPath)
+ {
+ }
+
+ internal ApplicationPart(IResourceAssembly assembly, string rootVirtualPath)
+ {
+ if (String.IsNullOrEmpty(rootVirtualPath))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "rootVirtualPath");
+ }
+
+ // Make sure the root path ends with a slash
+ if (!rootVirtualPath.EndsWith("/", StringComparison.Ordinal))
+ {
+ rootVirtualPath += "/";
+ }
+
+ Assembly = assembly;
+ RootVirtualPath = rootVirtualPath;
+ _applicationPartResources = new Lazy<IDictionary<string, string>>(() => Assembly.GetManifestResourceNames().ToDictionary(key => key, key => key, StringComparer.OrdinalIgnoreCase));
+ _applicationPartName = new Lazy<string>(() => Assembly.Name);
+ }
+
+ internal IResourceAssembly Assembly { get; private set; }
+
+ internal string RootVirtualPath { get; private set; }
+
+ internal string Name
+ {
+ get { return _applicationPartName.Value; }
+ }
+
+ internal IDictionary<string, string> ApplicationPartResources
+ {
+ get { return _applicationPartResources.Value; }
+ }
+
+ // REVIEW: Do we need an Unregister?
+ // Register an assembly as an application module, which makes its compiled web pages
+ // and embedded resources available
+ public static void Register(ApplicationPart applicationPart)
+ {
+ // Ensure the registry is ready and the route handlers are set up
+ _initApplicationPart.EnsurePerformed();
+ Debug.Assert(_partRegistry != null, "Part registry should be initialized");
+
+ _partRegistry.Register(applicationPart);
+ }
+
+ public static string ProcessVirtualPath(Assembly assembly, string baseVirtualPath, string virtualPath)
+ {
+ if (_partRegistry == null)
+ {
+ // This was called without registering a part.
+ throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, WebPageResources.ApplicationPart_ModuleNotRegistered, assembly));
+ }
+
+ ApplicationPart applicationPart = _partRegistry[new ResourceAssembly(assembly)];
+ if (applicationPart == null)
+ {
+ throw new InvalidOperationException(
+ String.Format(CultureInfo.CurrentCulture,
+ WebPageResources.ApplicationPart_ModuleNotRegistered,
+ assembly));
+ }
+
+ return applicationPart.ProcessVirtualPath(baseVirtualPath, virtualPath);
+ }
+
+ internal static IEnumerable<ApplicationPart> GetRegisteredParts()
+ {
+ _initApplicationPart.EnsurePerformed();
+ return _partRegistry.RegisteredParts;
+ }
+
+ private string ProcessVirtualPath(string baseVirtualPath, string virtualPath)
+ {
+ virtualPath = ResolveVirtualPath(RootVirtualPath, baseVirtualPath, virtualPath);
+ if (!virtualPath.StartsWith(RootVirtualPath, StringComparison.OrdinalIgnoreCase))
+ {
+ return virtualPath;
+ }
+
+ // Remove the root package path from the path, since the resource name doesn't use it
+ // e.g. ~/admin/Debugger/Sub Folder/foo.jpg ==> ~/Sub Folder/foo.jpg
+ string packageVirtualPath = "~/" + virtualPath.Substring(RootVirtualPath.Length);
+
+ string resourceName = GetResourceNameFromVirtualPath(packageVirtualPath);
+
+ // If the assembly doesn't contains that resource, don't change the path
+ if (!ApplicationPartResources.ContainsKey(resourceName))
+ {
+ return virtualPath;
+ }
+
+ // The resource exists, so return a special path that will be handled by the resource handler
+ return GetResourceVirtualPath(virtualPath);
+ }
+
+ /// <summary>
+ /// Expands a virtual path by replacing a leading "@" with the application part root
+ /// or combining it with the specified baseVirtualPath
+ /// </summary>
+ internal static string ResolveVirtualPath(string applicationRoot, string baseVirtualPath, string virtualPath)
+ {
+ // If it starts with @/, replace that with the package root
+ // e.g. @/Sub Folder/foo.jpg ==> ~/admin/Debugger/Sub Folder/foo.jpg
+ if (virtualPath.StartsWith(ModuleRootSyntax, StringComparison.OrdinalIgnoreCase))
+ {
+ return applicationRoot + virtualPath.Substring(ModuleRootSyntax.Length);
+ }
+ else
+ {
+ // Resolve if relative to the base
+ return VirtualPathUtility.Combine(baseVirtualPath, virtualPath);
+ }
+ }
+
+ internal Stream GetResourceStream(string virtualPath)
+ {
+ string resourceName = GetResourceNameFromVirtualPath(virtualPath);
+ string normalizedResourceName;
+ if (ApplicationPartResources.TryGetValue(resourceName, out normalizedResourceName))
+ {
+ // Return the resource stream
+ return Assembly.GetManifestResourceStream(normalizedResourceName);
+ }
+ return null;
+ }
+
+ // Get the name of an embedded resource based on a virtual path
+ private string GetResourceNameFromVirtualPath(string virtualPath)
+ {
+ return GetResourceNameFromVirtualPath(Name, virtualPath);
+ }
+
+ internal static string GetResourceNameFromVirtualPath(string moduleName, string virtualPath)
+ {
+ // Make sure path starts with ~/
+ if (!virtualPath.StartsWith("~/", StringComparison.Ordinal))
+ {
+ virtualPath = "~/" + virtualPath;
+ }
+
+ // Get the directory part of the path
+ // e.g. ~/Sub Folder/foo.jpg ==> ~/Sub Folder/
+ string dir = VirtualPathUtility.GetDirectory(virtualPath);
+
+ // Get rid of the starting ~/
+ // e.g. ~/Sub Folder/ ==> Sub Folder/
+ if (dir.Length >= 2)
+ {
+ dir = dir.Substring(2);
+ }
+
+ // Replace / with . and spaces with _
+ // TODO: other special chars need to be replaced by _ as well
+ // e.g. Sub Folder/ ==> Sub_Folder.
+ dir = dir.Replace('/', '.');
+ dir = dir.Replace(' ', '_');
+
+ // Get the file name part. That part of the resource names is the same as in the virtual path,
+ // so no replacements are needed
+ // e.g. ~/Sub Folder/foo.jpg ==> foo.jpg
+ string fileName = Path.GetFileName(virtualPath);
+
+ // Put them back together, and prepend the assembly name
+ // e.g. DebuggerAssembly.Sub_Folder.foo.jpg
+ return moduleName + "." + dir + fileName;
+ }
+
+ // Get a virtual path that uses the resource handler from a regular virtual path
+ private string GetResourceVirtualPath(string virtualPath)
+ {
+ return GetResourceVirtualPath(Name, RootVirtualPath, virtualPath);
+ }
+
+ internal static string GetResourceVirtualPath(string moduleName, string moduleRoot, string virtualPath)
+ {
+ // The path should always start with the root of the module. Skip it.
+ Debug.Assert(virtualPath.StartsWith(moduleRoot, StringComparison.OrdinalIgnoreCase));
+ virtualPath = virtualPath.Substring(moduleRoot.Length).TrimStart('/');
+
+ // Make a path to the resource through our resource route, e.g. ~/r.ashx/sub/foo.jpg
+ // e.g. ~/admin/Debugger/Sub Folder/foo.jpg ==> ~/r.ashx/DebuggerPackageName/Sub Folder/foo.jpg
+ return ResourceVirtualPathRoot + HttpUtility.UrlPathEncode(moduleName) + "/" + virtualPath;
+ }
+
+ private static void InitApplicationParts()
+ {
+ // Register the virtual path factory
+ var virtualPathFactory = new DictionaryBasedVirtualPathFactory();
+ VirtualPathFactoryManager.RegisterVirtualPathFactory(virtualPathFactory);
+
+ // Intantiate the part registry
+ _partRegistry = new ApplicationPartRegistry(virtualPathFactory);
+
+ // Register the resource route
+ RouteTable.Routes.Add(new Route(ResourceRoute, new ResourceRouteHandler(_partRegistry)));
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/ApplicationParts/ApplicationPartRegistry.cs b/src/System.Web.WebPages/ApplicationParts/ApplicationPartRegistry.cs
new file mode 100644
index 00000000..34dac99b
--- /dev/null
+++ b/src/System.Web.WebPages/ApplicationParts/ApplicationPartRegistry.cs
@@ -0,0 +1,142 @@
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Web.WebPages.Resources;
+
+namespace System.Web.WebPages.ApplicationParts
+{
+ internal class ApplicationPartRegistry
+ {
+ // Page types that we could serve
+ private static readonly Type _webPageType = typeof(WebPageRenderingBase);
+ private readonly DictionaryBasedVirtualPathFactory _virtualPathFactory;
+ private readonly ConcurrentDictionary<string, bool> _registeredVirtualPaths;
+ private readonly ConcurrentDictionary<IResourceAssembly, ApplicationPart> _applicationParts;
+
+ public ApplicationPartRegistry(DictionaryBasedVirtualPathFactory pathFactory)
+ {
+ _applicationParts = new ConcurrentDictionary<IResourceAssembly, ApplicationPart>();
+ _registeredVirtualPaths = new ConcurrentDictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
+ _virtualPathFactory = pathFactory;
+ }
+
+ public IEnumerable<ApplicationPart> RegisteredParts
+ {
+ get { return _applicationParts.Values; }
+ }
+
+ public ApplicationPart this[string name]
+ {
+ get { return _applicationParts.Values.FirstOrDefault(appPart => appPart.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); }
+ }
+
+ public ApplicationPart this[IResourceAssembly assembly]
+ {
+ get
+ {
+ ApplicationPart part;
+ if (!_applicationParts.TryGetValue(assembly, out part))
+ {
+ part = null;
+ }
+ return part;
+ }
+ }
+
+ // Register an assembly as an application module, which makes its compiled web pages
+ // and embedded resources available
+ public void Register(ApplicationPart applicationPart)
+ {
+ Register(applicationPart, registerPageAction: null); // Use default action which creates a new page
+ }
+
+ // Register an assembly as an application module, which makes its compiled web pages
+ // and embedded resources available
+ internal void Register(ApplicationPart applicationPart, Func<object> registerPageAction)
+ {
+ // Throw if this assembly has been registered
+ if (_applicationParts.ContainsKey(applicationPart.Assembly))
+ {
+ throw new InvalidOperationException(
+ String.Format(CultureInfo.CurrentCulture,
+ WebPageResources.ApplicationPart_ModuleAlreadyRegistered, applicationPart.Assembly));
+ }
+
+ // Throw if the virtual path is already in use
+ if (_registeredVirtualPaths.ContainsKey(applicationPart.RootVirtualPath))
+ {
+ throw new InvalidOperationException(
+ String.Format(CultureInfo.CurrentCulture,
+ WebPageResources.ApplicationPart_ModuleAlreadyRegisteredForVirtualPath, applicationPart.RootVirtualPath));
+ }
+
+ // REVIEW: Should we register the app-part after we scan the assembly for webpages?
+ // Add the part to the list
+ if (_applicationParts.TryAdd(applicationPart.Assembly, applicationPart))
+ {
+ // We don't really care about the value
+ _registeredVirtualPaths.TryAdd(applicationPart.RootVirtualPath, true);
+
+ // Get all of the web page types
+ var webPageTypes = from type in applicationPart.Assembly.GetTypes()
+ where type.IsSubclassOf(_webPageType)
+ select type;
+
+ // Register each of page with the plan9
+ foreach (Type webPageType in webPageTypes)
+ {
+ RegisterWebPage(applicationPart, webPageType, registerPageAction);
+ }
+ }
+ }
+
+ private void RegisterWebPage(ApplicationPart module, Type webPageType, Func<object> registerPageAction)
+ {
+ var virtualPathAttribute = webPageType.GetCustomAttributes(typeof(PageVirtualPathAttribute), false)
+ .Cast<PageVirtualPathAttribute>()
+ .SingleOrDefault();
+
+ // Ignore it if it doesn't have a PageVirtualPathAttribute
+ if (virtualPathAttribute == null)
+ {
+ return;
+ }
+
+ // Get the path of the page relative to the module root
+ string virtualPath = GetRootRelativeVirtualPath(module.RootVirtualPath, virtualPathAttribute.VirtualPath);
+
+ // Create a factory for the page type
+ Func<object> pageFactory = registerPageAction ?? NewTypeInstance(webPageType);
+
+ // Register a page factory for it
+ _virtualPathFactory.RegisterPath(virtualPath, pageFactory);
+ }
+
+ private static Func<object> NewTypeInstance(Type type)
+ {
+ return Expression.Lambda<Func<object>>(Expression.New(type)).Compile();
+ }
+
+ internal static string GetRootRelativeVirtualPath(string rootVirtualPath, string pageVirtualPath)
+ {
+ string virtualPath = pageVirtualPath;
+
+ // Trim the ~/ prefix, since we want it to be relative to the module base path
+ if (virtualPath.StartsWith("~/", StringComparison.Ordinal))
+ {
+ virtualPath = virtualPath.Substring(2);
+ }
+
+ if (!rootVirtualPath.EndsWith("/", StringComparison.OrdinalIgnoreCase))
+ {
+ rootVirtualPath += "/";
+ }
+
+ // Combine it with the root
+ virtualPath = VirtualPathUtility.Combine(rootVirtualPath, virtualPath);
+ return virtualPath;
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/ApplicationParts/DictionaryBasedVirtualPathFactory.cs b/src/System.Web.WebPages/ApplicationParts/DictionaryBasedVirtualPathFactory.cs
new file mode 100644
index 00000000..41bbb2e7
--- /dev/null
+++ b/src/System.Web.WebPages/ApplicationParts/DictionaryBasedVirtualPathFactory.cs
@@ -0,0 +1,29 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+
+namespace System.Web.WebPages.ApplicationParts
+{
+ // IVirtualPathFactory that keeps track of a mapping from virtual paths to handler factories
+ internal class DictionaryBasedVirtualPathFactory : IVirtualPathFactory
+ {
+ private Dictionary<string, Func<object>> _factories = new Dictionary<string, Func<object>>(StringComparer.OrdinalIgnoreCase);
+
+ internal void RegisterPath(string virtualPath, Func<object> factory)
+ {
+ _factories[virtualPath] = factory;
+ }
+
+ public bool Exists(string virtualPath)
+ {
+ return _factories.ContainsKey(virtualPath);
+ }
+
+ public object CreateInstance(string virtualPath)
+ {
+ // Instantiate an object for the path
+ // Note that this fails if it doesn't exist. PathExists is assumed to have been called first
+ Debug.Assert(Exists(virtualPath));
+ return _factories[virtualPath]();
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/ApplicationParts/IResourceAssembly.cs b/src/System.Web.WebPages/ApplicationParts/IResourceAssembly.cs
new file mode 100644
index 00000000..565257d5
--- /dev/null
+++ b/src/System.Web.WebPages/ApplicationParts/IResourceAssembly.cs
@@ -0,0 +1,14 @@
+using System.Collections.Generic;
+using System.IO;
+
+namespace System.Web.WebPages.ApplicationParts
+{
+ // For unit testing purpose since Assembly is not Moqable
+ internal interface IResourceAssembly
+ {
+ string Name { get; }
+ Stream GetManifestResourceStream(string name);
+ IEnumerable<string> GetManifestResourceNames();
+ IEnumerable<Type> GetTypes();
+ }
+}
diff --git a/src/System.Web.WebPages/ApplicationParts/LazyAction.cs b/src/System.Web.WebPages/ApplicationParts/LazyAction.cs
new file mode 100644
index 00000000..f6f4156f
--- /dev/null
+++ b/src/System.Web.WebPages/ApplicationParts/LazyAction.cs
@@ -0,0 +1,29 @@
+using System.Diagnostics;
+
+namespace System.Web.WebPages.ApplicationParts
+{
+ internal class LazyAction
+ {
+ private Lazy<object> _lazyAction;
+
+ public LazyAction(Action action)
+ {
+ Debug.Assert(action != null, "action should not be null");
+ // Setup the lazy object to run our action and just return null
+ // since we don't care about the value
+ _lazyAction = new Lazy<object>(() =>
+ {
+ action();
+ return null;
+ });
+ }
+
+ public object EnsurePerformed()
+ {
+ // REVIEW: This isn't used we're just exploiting the use of Lazy<T> to execute
+ // our action once in a thread safe way
+ // It would be nice if the framework had Unit (i.e a better void type so we could type Func<Unit> -> Action)
+ return _lazyAction.Value;
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/ApplicationParts/ResourceAssembly.cs b/src/System.Web.WebPages/ApplicationParts/ResourceAssembly.cs
new file mode 100644
index 00000000..bab6f9d6
--- /dev/null
+++ b/src/System.Web.WebPages/ApplicationParts/ResourceAssembly.cs
@@ -0,0 +1,65 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Reflection;
+
+namespace System.Web.WebPages.ApplicationParts
+{
+ // Implementation of IResourceAssembly over a standard assembly
+ internal class ResourceAssembly : IResourceAssembly
+ {
+ private readonly Assembly _assembly;
+
+ public ResourceAssembly(Assembly assembly)
+ {
+ if (assembly == null)
+ {
+ throw new ArgumentNullException("assembly");
+ }
+ _assembly = assembly;
+ }
+
+ public string Name
+ {
+ get
+ {
+ // Need this for medium trust
+ AssemblyName assemblyName = new AssemblyName(_assembly.FullName);
+ Debug.Assert(assemblyName != null, "Assembly name should not be null");
+ // Use the assembly short name
+ return assemblyName.Name;
+ }
+ }
+
+ public Stream GetManifestResourceStream(string name)
+ {
+ return _assembly.GetManifestResourceStream(name);
+ }
+
+ public IEnumerable<string> GetManifestResourceNames()
+ {
+ return _assembly.GetManifestResourceNames();
+ }
+
+ public IEnumerable<Type> GetTypes()
+ {
+ return _assembly.GetExportedTypes();
+ }
+
+ public override bool Equals(object obj)
+ {
+ var otherAssembly = obj as ResourceAssembly;
+ return otherAssembly != null && otherAssembly._assembly.Equals(_assembly);
+ }
+
+ public override int GetHashCode()
+ {
+ return _assembly.GetHashCode();
+ }
+
+ public override String ToString()
+ {
+ return _assembly.ToString();
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/ApplicationParts/ResourceHandler.cs b/src/System.Web.WebPages/ApplicationParts/ResourceHandler.cs
new file mode 100644
index 00000000..356edcca
--- /dev/null
+++ b/src/System.Web.WebPages/ApplicationParts/ResourceHandler.cs
@@ -0,0 +1,67 @@
+using System.Globalization;
+using System.Web.WebPages.Resources;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.WebPages.ApplicationParts
+{
+ // Used to serve static resource files (e.g. .jpg, .css, .js) that live inside appliaction modules
+ internal class ResourceHandler : IHttpHandler
+ {
+ private readonly string _path;
+ private readonly ApplicationPart _applicationPart;
+
+ public ResourceHandler(ApplicationPart applicationPart, string path)
+ {
+ if (applicationPart == null)
+ {
+ throw new ArgumentNullException("applicationPart");
+ }
+
+ if (String.IsNullOrEmpty(path))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "path");
+ }
+
+ _applicationPart = applicationPart;
+ _path = path;
+ }
+
+ public bool IsReusable
+ {
+ get { return true; }
+ }
+
+ public void ProcessRequest(HttpContext context)
+ {
+ ProcessRequest(new HttpResponseWrapper(context.Response));
+ }
+
+ internal void ProcessRequest(HttpResponseBase response)
+ {
+ string virtualPath = _path;
+
+ // Make sure it starts with ~/
+ if (!virtualPath.StartsWith("~/", StringComparison.Ordinal))
+ {
+ virtualPath = "~/" + virtualPath;
+ }
+
+ // Get the resource stream for this virtual path
+ using (var stream = _applicationPart.GetResourceStream(virtualPath))
+ {
+ if (stream == null)
+ {
+ throw new HttpException(404, String.Format(
+ CultureInfo.CurrentCulture,
+ WebPageResources.ApplicationPart_ResourceNotFound, _path));
+ }
+
+ // Set the mime type based on the file extension
+ response.ContentType = MimeMapping.GetMimeMapping(virtualPath);
+
+ // Copy it to the response
+ stream.CopyTo(response.OutputStream);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/ApplicationParts/ResourceRouteHandler.cs b/src/System.Web.WebPages/ApplicationParts/ResourceRouteHandler.cs
new file mode 100644
index 00000000..72b3e453
--- /dev/null
+++ b/src/System.Web.WebPages/ApplicationParts/ResourceRouteHandler.cs
@@ -0,0 +1,39 @@
+using System.Globalization;
+using System.Web.Routing;
+using System.Web.WebPages.Resources;
+
+namespace System.Web.WebPages.ApplicationParts
+{
+ internal class ResourceRouteHandler : IRouteHandler
+ {
+ private ApplicationPartRegistry _partRegistry;
+
+ public ResourceRouteHandler(ApplicationPartRegistry partRegistry)
+ {
+ _partRegistry = partRegistry;
+ }
+
+ public IHttpHandler GetHttpHandler(RequestContext requestContext)
+ {
+ // Get the package name and static resource path from the route
+ string partName = (string)requestContext.RouteData.GetRequiredString("module");
+
+ // Try to find an application module by this name
+ ApplicationPart module = _partRegistry[partName];
+
+ // Throw an exception if we can't find the module by name
+ if (module == null)
+ {
+ throw new InvalidOperationException(
+ String.Format(CultureInfo.CurrentCulture,
+ WebPageResources.ApplicationPart_ModuleCannotBeFound, partName));
+ }
+
+ // Get the resource path
+ string path = (string)requestContext.RouteData.GetRequiredString("path");
+
+ // Return the resource handler for this module and path
+ return new ResourceHandler(module, path);
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/ApplicationStartPage.cs b/src/System.Web.WebPages/ApplicationStartPage.cs
new file mode 100644
index 00000000..8a5f96d5
--- /dev/null
+++ b/src/System.Web.WebPages/ApplicationStartPage.cs
@@ -0,0 +1,173 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Reflection;
+using System.Threading;
+using System.Web.Caching;
+using System.Web.Hosting;
+using Microsoft.Web.Infrastructure;
+
+namespace System.Web.WebPages
+{
+ public abstract class ApplicationStartPage : WebPageExecutingBase
+ {
+ private static readonly Action<Action> _safeExecuteStartPageThunk = GetSafeExecuteStartPageThunk();
+ public static readonly string StartPageVirtualPath = "~/_appstart.";
+ public static readonly string CacheKeyPrefix = "__AppStartPage__";
+
+ public HttpApplication Application { get; internal set; }
+
+ public override HttpContextBase Context
+ {
+ get { return new HttpContextWrapper(Application.Context); }
+ }
+
+ public static HtmlString Markup { get; private set; }
+
+ internal static Exception Exception { get; private set; }
+
+ public TextWriter Output { get; internal set; }
+
+ public override string VirtualPath
+ {
+ get { return StartPageVirtualPath; }
+ set
+ {
+ // The virtual path for the start page is fixed for now.
+ throw new NotSupportedException();
+ }
+ }
+
+ internal void ExecuteInternal()
+ {
+ // See comments in GetSafeExecuteStartPageThunk().
+ _safeExecuteStartPageThunk(() =>
+ {
+ Output = new StringWriter(CultureInfo.InvariantCulture);
+ Execute();
+ Markup = new HtmlString(Output.ToString());
+ });
+ }
+
+ internal static void ExecuteStartPage(HttpApplication application)
+ {
+ ExecuteStartPage(application,
+ vpath => MonitorFile(vpath),
+ VirtualPathFactoryManager.Instance,
+ WebPageHttpHandler.GetRegisteredExtensions());
+ }
+
+ internal static void ExecuteStartPage(HttpApplication application, Action<string> monitorFile, IVirtualPathFactory virtualPathFactory, IEnumerable<string> supportedExtensions)
+ {
+ try
+ {
+ ExecuteStartPageInternal(application, monitorFile, virtualPathFactory, supportedExtensions);
+ }
+ catch (Exception e)
+ {
+ // Throw it as a HttpException so as to
+ // display the original stack trace information.
+ Exception = e;
+ throw new HttpException(null, e);
+ }
+ }
+
+ internal static void ExecuteStartPageInternal(HttpApplication application, Action<string> monitorFile, IVirtualPathFactory virtualPathFactory, IEnumerable<string> supportedExtensions)
+ {
+ ApplicationStartPage startPage = null;
+
+ foreach (var extension in supportedExtensions)
+ {
+ var vpath = StartPageVirtualPath + extension;
+
+ // We need to monitor regardless of existence because the user could add/remove the
+ // file at any time.
+ monitorFile(vpath);
+ if (!virtualPathFactory.Exists(vpath))
+ {
+ continue;
+ }
+
+ if (startPage == null)
+ {
+ startPage = virtualPathFactory.CreateInstance<ApplicationStartPage>(vpath);
+ startPage.Application = application;
+ startPage.VirtualPathFactory = virtualPathFactory;
+ startPage.ExecuteInternal();
+ }
+ }
+ }
+
+ private static Action<Action> GetSafeExecuteStartPageThunk()
+ {
+ // Programmatically detect if this version of System.Web.dll suffers from a bug in
+ // which HttpUtility.HtmlEncode can't be called from Application_Start, and if so
+ // set the current HttpContext to null to work around it.
+ //
+ // See Dev10 #906296 and Dev10 #898600 for more information.
+
+ if (typeof(HttpResponse).GetProperty("DisableCustomHttpEncoder", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly) != null)
+ {
+ // this version suffers from the bug
+ return HttpContextHelper.ExecuteInNullContext;
+ }
+ else
+ {
+ // this version does not suffer from the bug
+ return action => action();
+ }
+ }
+
+ private static void InitiateShutdown(string key, object value, CacheItemRemovedReason reason)
+ {
+ // Only handle case when the dependency has changed.
+ if (reason != CacheItemRemovedReason.DependencyChanged)
+ {
+ return;
+ }
+
+ ThreadPool.QueueUserWorkItem(new WaitCallback(ShutdownCallBack));
+ }
+
+ private static void MonitorFile(string virtualPath)
+ {
+ var virtualPathDependencies = new List<string>();
+ virtualPathDependencies.Add(virtualPath);
+ CacheDependency cacheDependency = HostingEnvironment.VirtualPathProvider.GetCacheDependency(
+ virtualPath, virtualPathDependencies, DateTime.UtcNow);
+ var key = CacheKeyPrefix + virtualPath;
+
+ HttpRuntime.Cache.Insert(key, virtualPath, cacheDependency,
+ Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration,
+ CacheItemPriority.NotRemovable, new CacheItemRemovedCallback(InitiateShutdown));
+ }
+
+ private static void ShutdownCallBack(object state)
+ {
+ InfrastructureHelper.UnloadAppDomain();
+ }
+
+ public override void Write(HelperResult result)
+ {
+ if (result != null)
+ {
+ result.WriteTo(Output);
+ }
+ }
+
+ public override void WriteLiteral(object value)
+ {
+ Output.Write(value);
+ }
+
+ public override void Write(object value)
+ {
+ Output.Write(HttpUtility.HtmlEncode(value));
+ }
+
+ protected internal override TextWriter GetOutputWriter()
+ {
+ return Output;
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/AttributeValue.cs b/src/System.Web.WebPages/AttributeValue.cs
new file mode 100644
index 00000000..6cfb6fe9
--- /dev/null
+++ b/src/System.Web.WebPages/AttributeValue.cs
@@ -0,0 +1,43 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Web.WebPages.Instrumentation;
+
+namespace System.Web.WebPages
+{
+ public class AttributeValue
+ {
+ public AttributeValue(PositionTagged<string> prefix, PositionTagged<object> value, bool literal)
+ {
+ Prefix = prefix;
+ Value = value;
+ Literal = literal;
+ }
+
+ public PositionTagged<string> Prefix { get; private set; }
+ public PositionTagged<object> Value { get; private set; }
+ public bool Literal { get; private set; }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "We are using tuples here to avoid dependencies from Razor to WebPages")]
+ public static AttributeValue FromTuple(Tuple<Tuple<string, int>, Tuple<object, int>, bool> value)
+ {
+ return new AttributeValue(value.Item1, value.Item2, value.Item3);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "We are using tuples here to avoid dependencies from Razor to WebPages")]
+ public static AttributeValue FromTuple(Tuple<Tuple<string, int>, Tuple<string, int>, bool> value)
+ {
+ return new AttributeValue(value.Item1, new PositionTagged<object>(value.Item2.Item1, value.Item2.Item2), value.Item3);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "We are using tuples here to avoid dependencies from Razor to WebPages")]
+ public static implicit operator AttributeValue(Tuple<Tuple<string, int>, Tuple<object, int>, bool> value)
+ {
+ return FromTuple(value);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "We are using tuples here to avoid dependencies from Razor to WebPages")]
+ public static implicit operator AttributeValue(Tuple<Tuple<string, int>, Tuple<string, int>, bool> value)
+ {
+ return FromTuple(value);
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/BrowserHelpers.cs b/src/System.Web.WebPages/BrowserHelpers.cs
new file mode 100644
index 00000000..5b431bd5
--- /dev/null
+++ b/src/System.Web.WebPages/BrowserHelpers.cs
@@ -0,0 +1,168 @@
+using System.Web.Hosting;
+
+namespace System.Web.WebPages
+{
+ /// <summary>
+ /// Extension methods used to determine what browser a visitor wants to be seen as using.
+ /// </summary>
+ public static class BrowserHelpers
+ {
+ /// <summary>
+ /// Stock IE6 user agent string
+ /// </summary>
+ private const string DesktopUserAgent = "Mozilla/4.0 (compatible; MSIE 6.1; Windows XP)";
+
+ /// <summary>
+ /// Stock Windows Mobile 6.0 user agent string
+ /// </summary>
+ private const string MobileUserAgent = "Mozilla/4.0 (compatible; MSIE 6.0; Windows CE; IEMobile 8.12; MSIEMobile 6.0)";
+
+ private static readonly object _browserOverrideKey = new object();
+ private static readonly object _userAgentKey = new object();
+
+ /// <summary>
+ /// Clears the set browser for the request. After clearing the browser the overridden browser will be the browser for the request.
+ /// </summary>
+ public static void ClearOverriddenBrowser(this HttpContextBase httpContext)
+ {
+ SetOverriddenBrowser(httpContext, userAgent: null);
+ }
+
+ // Default implementation to generate an HttpBrowserCapabilities object using the current HttpCapabilitiesProvider
+ private static HttpBrowserCapabilitiesBase CreateOverriddenBrowser(string userAgent)
+ {
+ HttpBrowserCapabilities overriddenBrowser = new HttpContext(new UserAgentWorkerRequest(userAgent)).Request.Browser;
+ return new HttpBrowserCapabilitiesWrapper(overriddenBrowser);
+ }
+
+ /// <summary>
+ /// Gets the overridden browser for the request based on the overridden user agent.
+ /// If no overridden user agent is set, returns the browser for the request.
+ /// </summary>
+ public static HttpBrowserCapabilitiesBase GetOverriddenBrowser(this HttpContextBase httpContext)
+ {
+ return GetOverriddenBrowser(httpContext, CreateOverriddenBrowser);
+ }
+
+ internal static HttpBrowserCapabilitiesBase GetOverriddenBrowser(this HttpContextBase httpContext, Func<string, HttpBrowserCapabilitiesBase> createBrowser)
+ {
+ HttpBrowserCapabilitiesBase overriddenBrowser = (HttpBrowserCapabilitiesBase)httpContext.Items[_browserOverrideKey];
+
+ if (overriddenBrowser == null)
+ {
+ string overriddenUserAgent = GetOverriddenUserAgent(httpContext);
+
+ if (!String.Equals(overriddenUserAgent, httpContext.Request.UserAgent))
+ {
+ overriddenBrowser = createBrowser(overriddenUserAgent);
+ }
+ else
+ {
+ overriddenBrowser = httpContext.Request.Browser;
+ }
+
+ httpContext.Items[_browserOverrideKey] = overriddenBrowser;
+ }
+
+ return overriddenBrowser;
+ }
+
+ /// <summary>
+ /// Gets the overridden user agent for the request. If no overridden user agent is set, returns the user agent for the request.
+ /// </summary>
+ public static string GetOverriddenUserAgent(this HttpContextBase httpContext)
+ {
+ return (string)httpContext.Items[_userAgentKey] ??
+ BrowserOverrideStores.Current.GetOverriddenUserAgent(httpContext) ??
+ httpContext.Request.UserAgent;
+ }
+
+ /// <summary>
+ /// Gets a string that varies based upon the type of the browser. Can be used to override
+ /// System.Web.HttpApplication.GetVaryByCustomString to differentiate cache keys based on
+ /// the overridden browser.
+ /// </summary>
+ public static string GetVaryByCustomStringForOverriddenBrowser(this HttpContext httpContext)
+ {
+ return GetVaryByCustomStringForOverriddenBrowser(new HttpContextWrapper(httpContext));
+ }
+
+ /// <summary>
+ /// Gets a string that varies based upon the type of the browser. Can be used to override
+ /// System.Web.HttpApplication.GetVaryByCustomString to differentiate cache keys based on
+ /// the overridden browser.
+ /// </summary>
+ public static string GetVaryByCustomStringForOverriddenBrowser(this HttpContextBase httpContext)
+ {
+ return GetVaryByCustomStringForOverriddenBrowser(httpContext, userAgent => CreateOverriddenBrowser(userAgent));
+ }
+
+ internal static string GetVaryByCustomStringForOverriddenBrowser(this HttpContextBase httpContext, Func<string, HttpBrowserCapabilitiesBase> generateBrowser)
+ {
+ return GetOverriddenBrowser(httpContext, generateBrowser).Type;
+ }
+
+ /// <summary>
+ /// Sets the overridden user agent for the request using a BrowserOverride.
+ /// </summary>
+ public static void SetOverriddenBrowser(this HttpContextBase httpContext, BrowserOverride browserOverride)
+ {
+ string userAgent = null;
+
+ switch (browserOverride)
+ {
+ case BrowserOverride.Desktop:
+ // bug:262389 override only if the request was not made from a browser or the browser is not of a desktop device
+ if (httpContext.Request.Browser == null || httpContext.Request.Browser.IsMobileDevice)
+ {
+ userAgent = DesktopUserAgent;
+ }
+ break;
+ case BrowserOverride.Mobile:
+ if (httpContext.Request.Browser == null || !httpContext.Request.Browser.IsMobileDevice)
+ {
+ userAgent = MobileUserAgent;
+ }
+ break;
+ }
+
+ if (userAgent != null)
+ {
+ SetOverriddenBrowser(httpContext, userAgent);
+ }
+ else
+ {
+ ClearOverriddenBrowser(httpContext);
+ }
+ }
+
+ /// <summary>
+ /// Sets the overridden user agent for the request using a string
+ /// </summary>
+ public static void SetOverriddenBrowser(this HttpContextBase httpContext, string userAgent)
+ {
+ // Set the overridden user agent and clear the overridden browser
+ // so that it can be generated from the new overridden user agent.
+ httpContext.Items[_userAgentKey] = userAgent;
+ httpContext.Items[_browserOverrideKey] = null;
+
+ BrowserOverrideStores.Current.SetOverriddenUserAgent(httpContext, userAgent);
+ }
+
+ private sealed class UserAgentWorkerRequest : SimpleWorkerRequest
+ {
+ private readonly string _userAgent;
+
+ public UserAgentWorkerRequest(string userAgent)
+ : base(String.Empty, String.Empty, output: null)
+ {
+ _userAgent = userAgent;
+ }
+
+ public override string GetKnownRequestHeader(int index)
+ {
+ return index == HeaderUserAgent ? _userAgent : null;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/BrowserOverride.cs b/src/System.Web.WebPages/BrowserOverride.cs
new file mode 100644
index 00000000..a002ba19
--- /dev/null
+++ b/src/System.Web.WebPages/BrowserOverride.cs
@@ -0,0 +1,11 @@
+namespace System.Web.WebPages
+{
+ /// <summary>
+ /// BrowserOverrides can be used by BrowserHelpers to override the browser for a particular request.
+ /// </summary>
+ public enum BrowserOverride
+ {
+ Desktop,
+ Mobile
+ }
+}
diff --git a/src/System.Web.WebPages/BrowserOverrideStore.cs b/src/System.Web.WebPages/BrowserOverrideStore.cs
new file mode 100644
index 00000000..b820f447
--- /dev/null
+++ b/src/System.Web.WebPages/BrowserOverrideStore.cs
@@ -0,0 +1,12 @@
+namespace System.Web.WebPages
+{
+ /// <summary>
+ /// The current BrowserOverrideStore is used to get and set the user agent of a request.
+ /// For an example see CookieBasedBrowserOverrideStore.
+ /// </summary>
+ public abstract class BrowserOverrideStore
+ {
+ public abstract string GetOverriddenUserAgent(HttpContextBase httpContext);
+ public abstract void SetOverriddenUserAgent(HttpContextBase httpContext, string userAgent);
+ }
+}
diff --git a/src/System.Web.WebPages/BrowserOverrideStores.cs b/src/System.Web.WebPages/BrowserOverrideStores.cs
new file mode 100644
index 00000000..77829c68
--- /dev/null
+++ b/src/System.Web.WebPages/BrowserOverrideStores.cs
@@ -0,0 +1,23 @@
+namespace System.Web.WebPages
+{
+ public class BrowserOverrideStores
+ {
+ private static BrowserOverrideStores _instance = new BrowserOverrideStores();
+ private BrowserOverrideStore _currentOverrideStore = new CookieBrowserOverrideStore();
+
+ /// <summary>
+ /// The current BrowserOverrideStore
+ /// </summary>
+ public static BrowserOverrideStore Current
+ {
+ get { return _instance.CurrentInternal; }
+ set { _instance.CurrentInternal = value; }
+ }
+
+ internal BrowserOverrideStore CurrentInternal
+ {
+ get { return _currentOverrideStore; }
+ set { _currentOverrideStore = value ?? new RequestBrowserOverrideStore(); }
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/BuildManagerWrapper.cs b/src/System.Web.WebPages/BuildManagerWrapper.cs
new file mode 100644
index 00000000..53d5f474
--- /dev/null
+++ b/src/System.Web.WebPages/BuildManagerWrapper.cs
@@ -0,0 +1,202 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Web.Caching;
+using System.Web.Compilation;
+using System.Web.Hosting;
+using System.Web.Util;
+using System.Xml.Linq;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.WebPages
+{
+ /// <summary>
+ /// Wraps the caching and instantiation of paths of the BuildManager.
+ /// In case of precompiled non-updateable sites, the only way to verify if a file exists is to call BuildManager.GetObjectFactory. However this method is less performant than
+ /// VirtualPathProvider.FileExists which is used for all other scenarios. In this class, we optimize for the first scenario by storing the results of GetObjectFactory for a
+ /// long duration.
+ /// </summary>
+ internal sealed class BuildManagerWrapper : IVirtualPathFactory
+ {
+ internal static readonly Guid KeyGuid = Guid.NewGuid();
+ private static readonly TimeSpan _objectFactoryCacheDuration = TimeSpan.FromMinutes(1);
+ private readonly IVirtualPathUtility _virtualPathUtility;
+ private readonly VirtualPathProvider _vpp;
+ private readonly bool _isPrecompiled;
+ private readonly FileExistenceCache _vppCache;
+ private IEnumerable<string> _supportedExtensions;
+
+ public BuildManagerWrapper()
+ : this(HostingEnvironment.VirtualPathProvider, new VirtualPathUtilityWrapper())
+ {
+ }
+
+ public BuildManagerWrapper(VirtualPathProvider vpp, IVirtualPathUtility virtualPathUtility)
+ {
+ _vpp = vpp;
+ _virtualPathUtility = virtualPathUtility;
+ _isPrecompiled = IsNonUpdatablePrecompiledApp();
+ if (!_isPrecompiled)
+ {
+ _vppCache = new FileExistenceCache(vpp);
+ }
+ }
+
+ public IEnumerable<string> SupportedExtensions
+ {
+ get { return _supportedExtensions ?? WebPageHttpHandler.GetRegisteredExtensions(); }
+ set { _supportedExtensions = value; }
+ }
+
+ /// <summary>
+ /// Determines if a page exists in the website.
+ /// This method switches between a long duration cache or a short duration FileExistenceCache depending on whether the site is precompiled.
+ /// This is an optimization because BuildManager.GetObjectFactory is comparably slower than performing VirtualPathFactory.Exists
+ /// </summary>
+ public bool Exists(string virtualPath)
+ {
+ if (_isPrecompiled)
+ {
+ return ExistsInPrecompiledSite(virtualPath);
+ }
+ return ExistsInVpp(virtualPath);
+ }
+
+ /// <summary>
+ /// An app's is precompiled for our purposes if
+ /// (a) it has a PreCompiledApp.config file in the site root,
+ /// (b) The PreCompiledApp.config says that the app is not Updatable.
+ /// </summary>
+ /// <remarks>
+ /// This code is based on System.Web.DynamicData.Misc.IsNonUpdatablePrecompiledAppNoCache (DynamicData)
+ /// </remarks>
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes",
+ Justification = "We want to replicate the behavior of BuildManager which catches all exceptions.")]
+ internal bool IsNonUpdatablePrecompiledApp()
+ {
+ if (_vpp == null)
+ {
+ return false;
+ }
+ var virtualPath = _virtualPathUtility.ToAbsolute("~/PrecompiledApp.config");
+ if (!_vpp.FileExists(virtualPath))
+ {
+ return false;
+ }
+
+ XDocument document;
+ using (var stream = _vpp.GetFile(virtualPath).Open())
+ {
+ try
+ {
+ document = XDocument.Load(_vpp.GetFile(virtualPath).Open());
+ }
+ catch
+ {
+ // If we are unable to load the file, ignore it. The BuildManager behaves identically.
+ return false;
+ }
+ }
+
+ if (document.Root == null || !document.Root.Name.LocalName.Equals("precompiledApp", StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+ var updatableAttribute = document.Root.Attribute("updatable");
+ if (updatableAttribute != null)
+ {
+ bool result;
+ return Boolean.TryParse(updatableAttribute.Value, out result) && (result == false);
+ }
+ return false;
+ }
+
+ private bool ExistsInPrecompiledSite(string virtualPath)
+ {
+ var key = GetKeyFromVirtualPath(virtualPath);
+
+ // We assume that the key is unique enough to avoid collisions.
+ var buildManagerResult = (BuildManagerResult)HttpRuntime.Cache.Get(key);
+ if (buildManagerResult == null)
+ {
+ // For precompiled apps, we cache the ObjectFactory and use it in the CreateInstance method.
+ var objectFactory = GetObjectFactory(virtualPath);
+ buildManagerResult = new BuildManagerResult { ObjectFactory = objectFactory, Exists = objectFactory != null };
+ // Cache the result with a sliding expiration for a long duration.
+ HttpRuntime.Cache.Add(key, buildManagerResult, null, Cache.NoAbsoluteExpiration, _objectFactoryCacheDuration, CacheItemPriority.Low, null);
+ }
+ return buildManagerResult.Exists;
+ }
+
+ /// <summary>
+ /// Determines if a site exists in the VirtualPathProvider.
+ /// Results of hits are cached for a very short amount of time in the FileExistenceCache.
+ /// </summary>
+ private bool ExistsInVpp(string virtualPath)
+ {
+ Debug.Assert(_vppCache != null);
+ return _vppCache.FileExists(virtualPath);
+ }
+
+ /// <summary>
+ /// Determines if an ObjectFactory exists for the virtualPath.
+ /// The BuildManager complains if we pass in extensions that aren't registered for compilation. So we ensure that the virtual path is not
+ /// extensionless and that it is one of the extension
+ /// </summary>
+ private IWebObjectFactory GetObjectFactory(string virtualPath)
+ {
+ if (IsPathExtensionSupported(virtualPath))
+ {
+ return BuildManager.GetObjectFactory(virtualPath, throwIfNotFound: false);
+ }
+ return null;
+ }
+
+ public object CreateInstance(string virtualPath)
+ {
+ return CreateInstanceOfType<object>(virtualPath);
+ }
+
+ public T CreateInstanceOfType<T>(string virtualPath) where T : class
+ {
+ if (_isPrecompiled)
+ {
+ var buildManagerResult = (BuildManagerResult)HttpRuntime.Cache.Get(GetKeyFromVirtualPath(virtualPath));
+ // The cache could have evicted our results. In this case, we'll simply fall through to CreateInstanceFromVirtualPath
+ if (buildManagerResult != null)
+ {
+ Debug.Assert(buildManagerResult.Exists && buildManagerResult.ObjectFactory != null, "This method must only be called if the file exists.");
+ return buildManagerResult.ObjectFactory.CreateInstance() as T;
+ }
+ }
+
+ return (T)BuildManager.CreateInstanceFromVirtualPath(virtualPath, typeof(T));
+ }
+
+ /// <summary>
+ /// Determines if the extension is one of the extensions registered with WebPageHttpHandler.
+ /// </summary>
+ public bool IsPathExtensionSupported(string virtualPath)
+ {
+ string extension = PathUtil.GetExtension(virtualPath);
+ return !String.IsNullOrEmpty(extension)
+ && SupportedExtensions.Contains(extension.Substring(1), StringComparer.OrdinalIgnoreCase);
+ }
+
+ /// <summary>
+ /// Creates a reasonably unique key for a given virtual path by concatenating it with a Guid.
+ /// </summary>
+ private static string GetKeyFromVirtualPath(string virtualPath)
+ {
+ return KeyGuid.ToString() + "_" + virtualPath;
+ }
+
+ private class BuildManagerResult
+ {
+ public bool Exists { get; set; }
+
+ public IWebObjectFactory ObjectFactory { get; set; }
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Common/DisposableAction.cs b/src/System.Web.WebPages/Common/DisposableAction.cs
new file mode 100644
index 00000000..60492f9c
--- /dev/null
+++ b/src/System.Web.WebPages/Common/DisposableAction.cs
@@ -0,0 +1,39 @@
+namespace System.Web.WebPages
+{
+ internal class DisposableAction : IDisposable
+ {
+ private Action _action;
+ private bool _hasDisposed;
+
+ public DisposableAction(Action action)
+ {
+ if (action == null)
+ {
+ throw new ArgumentNullException("action");
+ }
+ _action = action;
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ // If we were disposed by the finalizer it's because the user didn't use a "using" block, so don't do anything!
+ if (disposing)
+ {
+ lock (this)
+ {
+ if (!_hasDisposed)
+ {
+ _hasDisposed = true;
+ _action();
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/CookieBrowserOverrideStore.cs b/src/System.Web.WebPages/CookieBrowserOverrideStore.cs
new file mode 100644
index 00000000..e7b0573e
--- /dev/null
+++ b/src/System.Web.WebPages/CookieBrowserOverrideStore.cs
@@ -0,0 +1,95 @@
+namespace System.Web.WebPages
+{
+ /// <summary>
+ /// The default BrowserOverrideStore. Gets overridden user agent for a request from a cookie.
+ /// Creates a cookie to set the overridden user agent.
+ /// </summary>
+ public class CookieBrowserOverrideStore : BrowserOverrideStore
+ {
+ internal static readonly string BrowserOverrideCookieName = ".ASPXBrowserOverride";
+ private readonly int _daysToExpire;
+
+ /// <summary>
+ /// Creates the BrowserOverrideStore setting any browser override cookie to expire in 7 days.
+ /// </summary>
+ public CookieBrowserOverrideStore()
+ : this(daysToExpire: 7)
+ {
+ }
+
+ /// <summary>
+ /// Constructor to control the expiration of the browser override cookie.
+ /// </summary>
+ public CookieBrowserOverrideStore(int daysToExpire)
+ {
+ _daysToExpire = daysToExpire;
+ }
+
+ /// <summary>
+ /// Looks for a user agent by searching for the browser override cookie. If no cookie is found
+ /// returns null.
+ /// </summary>
+ public override string GetOverriddenUserAgent(HttpContextBase httpContext)
+ {
+ // Check the response to see if the cookie has been set somewhere in the current request.
+ HttpCookieCollection responseCookies = httpContext.Response.Cookies;
+ // NOTE: Only look for the key (via AllKeys) so a new cookie is not automatically created.
+ string[] cookieNames = responseCookies.AllKeys;
+ // NOTE: use a simple for loop since it performs an order of magnitude faster than .Contains()
+ // and this is a hot path that gets executed for every request.
+ for (int i = 0; i < cookieNames.Length; i++)
+ {
+ // HttpCookieCollection uses OrdinalIgnoreCase comparison for its keys
+ if (String.Equals(cookieNames[i], BrowserOverrideCookieName, StringComparison.OrdinalIgnoreCase))
+ {
+ HttpCookie currentOverriddenBrowserCookie = responseCookies[BrowserOverrideCookieName];
+
+ if (currentOverriddenBrowserCookie.Value != null)
+ {
+ return currentOverriddenBrowserCookie.Value;
+ }
+ else
+ {
+ // The cookie has no value. It was cleared on the current request so return null.
+ return null;
+ }
+ }
+ }
+
+ // If there was no cookie found in the response check the request.
+ var requestOverrideCookie = httpContext.Request.Cookies[BrowserOverrideCookieName];
+ if (requestOverrideCookie != null)
+ {
+ return requestOverrideCookie.Value;
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Adds a browser override cookie with the set user agent to the response of the current request.
+ /// If the user agent is null the browser override cookie is set to expire, otherwise its expiration is set
+ /// to daysToExpire, specified when CookieBasedOverrideStore is created.
+ /// </summary>
+ public override void SetOverriddenUserAgent(HttpContextBase httpContext, string userAgent)
+ {
+ HttpCookie browserOverrideCookie = new HttpCookie(BrowserOverrideCookieName, HttpUtility.UrlEncode(userAgent));
+
+ if (userAgent == null)
+ {
+ browserOverrideCookie.Expires = DateTime.Now.AddDays(-1);
+ }
+ else
+ {
+ // Only set expiration if the cookie should live longer than the current session
+ if (_daysToExpire > 0)
+ {
+ browserOverrideCookie.Expires = DateTime.Now.AddDays(_daysToExpire);
+ }
+ }
+
+ httpContext.Response.Cookies.Remove(BrowserOverrideCookieName);
+ httpContext.Response.Cookies.Add(browserOverrideCookie);
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/DefaultDisplayMode.cs b/src/System.Web.WebPages/DefaultDisplayMode.cs
new file mode 100644
index 00000000..919d08df
--- /dev/null
+++ b/src/System.Web.WebPages/DefaultDisplayMode.cs
@@ -0,0 +1,71 @@
+using System.IO;
+
+namespace System.Web.WebPages
+{
+ /// <summary>
+ /// The <see cref="DefaultDisplayMode"/> can take any suffix and determine if there is a corresponding
+ /// file that exists given a path and request by transforming the path to contain the suffix.
+ /// Add a new DefaultDisplayMode to the Modes collection to handle a new suffix or inherit from
+ /// DefaultDisplayMode to provide custom logic to transform paths with a suffix.
+ /// </summary>
+ public class DefaultDisplayMode : IDisplayMode
+ {
+ private readonly string _suffix;
+
+ public DefaultDisplayMode()
+ : this(DisplayModeProvider.DefaultDisplayModeId)
+ {
+ }
+
+ public DefaultDisplayMode(string suffix)
+ {
+ _suffix = suffix ?? String.Empty;
+ }
+
+ /// <summary>
+ /// When set, the <see cref="DefaultDisplayMode"/> will only be available to return Display Info for a request
+ /// if the ContextCondition evaluates to true.
+ /// </summary>
+ public Func<HttpContextBase, bool> ContextCondition { get; set; }
+
+ public virtual string DisplayModeId
+ {
+ get { return _suffix; }
+ }
+
+ public bool CanHandleContext(HttpContextBase httpContext)
+ {
+ return ContextCondition == null || ContextCondition(httpContext);
+ }
+
+ /// <summary>
+ /// Returns DisplayInfo with the transformed path if it exists.
+ /// </summary>
+ public virtual DisplayInfo GetDisplayInfo(HttpContextBase httpContext, string virtualPath, Func<string, bool> virtualPathExists)
+ {
+ string transformedFilename = TransformPath(virtualPath, _suffix);
+ if (transformedFilename != null && virtualPathExists(transformedFilename))
+ {
+ return new DisplayInfo(transformedFilename, this);
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Transforms paths according to the following rules:
+ /// \some\path.blah\file.txt.zip -> \some\path.blah\file.txt.suffix.zip
+ /// \some\path.blah\file -> \some\path.blah\file.suffix
+ /// </summary>
+ protected virtual string TransformPath(string virtualPath, string suffix)
+ {
+ if (String.IsNullOrEmpty(suffix))
+ {
+ return virtualPath;
+ }
+
+ string extension = Path.GetExtension(virtualPath);
+ return Path.ChangeExtension(virtualPath, suffix + extension);
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/DisplayInfo.cs b/src/System.Web.WebPages/DisplayInfo.cs
new file mode 100644
index 00000000..62b4bd3e
--- /dev/null
+++ b/src/System.Web.WebPages/DisplayInfo.cs
@@ -0,0 +1,35 @@
+namespace System.Web.WebPages
+{
+ /// <summary>
+ /// DisplayInfo wraps the resolved file path and IDisplayMode for a request and path.
+ /// The returned IDisplayMode can be used to resolve other page elements for the request.
+ /// </summary>
+ public class DisplayInfo
+ {
+ public DisplayInfo(string filePath, IDisplayMode displayMode)
+ {
+ if (filePath == null)
+ {
+ throw new ArgumentNullException("filePath");
+ }
+
+ if (displayMode == null)
+ {
+ throw new ArgumentNullException("displayMode");
+ }
+
+ FilePath = filePath;
+ DisplayMode = displayMode;
+ }
+
+ /// <summary>
+ /// The Display Mode used to resolve a virtual path.
+ /// </summary>
+ public IDisplayMode DisplayMode { get; private set; }
+
+ /// <summary>
+ /// Resolved path of a file that exists.
+ /// </summary>
+ public string FilePath { get; private set; }
+ }
+}
diff --git a/src/System.Web.WebPages/DisplayModeProvider.cs b/src/System.Web.WebPages/DisplayModeProvider.cs
new file mode 100644
index 00000000..7f23a8b3
--- /dev/null
+++ b/src/System.Web.WebPages/DisplayModeProvider.cs
@@ -0,0 +1,92 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace System.Web.WebPages
+{
+ public sealed class DisplayModeProvider
+ {
+ public static readonly string MobileDisplayModeId = "Mobile";
+ public static readonly string DefaultDisplayModeId = String.Empty;
+ private static readonly object _displayModeKey = new object();
+ private static readonly DisplayModeProvider _instance = new DisplayModeProvider();
+
+ private readonly List<IDisplayMode> _displayModes = new List<IDisplayMode>
+ {
+ new DefaultDisplayMode(MobileDisplayModeId)
+ {
+ ContextCondition = context => context.GetOverriddenBrowser().IsMobileDevice
+ },
+ new DefaultDisplayMode()
+ };
+
+ internal DisplayModeProvider()
+ {
+ // The type is a psuedo-singleton. A user would gain nothing from constructing it since we won't use anything but DisplayModeProvider.Instance internally.
+ }
+
+ /// <summary>
+ /// Restricts the search for Display Info to Display Modes either equal to or following the current
+ /// Display Mode in Modes. For example, a page being rendered in the Default Display Mode will not
+ /// display Mobile partial views in order to achieve a consistent look and feel.
+ /// </summary>
+ public bool RequireConsistentDisplayMode { get; set; }
+
+ public static DisplayModeProvider Instance
+ {
+ get { return _instance; }
+ }
+
+ /// <summary>
+ /// All Display Modes that are available to handle a request.
+ /// </summary>
+ public IList<IDisplayMode> Modes
+ {
+ get { return _displayModes; }
+ }
+
+ /// <summary>
+ /// Returns any IDisplayMode that can handle the given request.
+ /// </summary>
+ public IEnumerable<IDisplayMode> GetAvailableDisplayModesForContext(HttpContextBase httpContext, IDisplayMode currentDisplayMode)
+ {
+ return GetAvailableDisplayModesForContext(httpContext, currentDisplayMode, RequireConsistentDisplayMode).ToList();
+ }
+
+ internal IEnumerable<IDisplayMode> GetAvailableDisplayModesForContext(HttpContextBase httpContext, IDisplayMode currentDisplayMode, bool requireConsistentDisplayMode)
+ {
+ IEnumerable<IDisplayMode> possibleDisplayModes = (requireConsistentDisplayMode && currentDisplayMode != null) ? Modes.SkipWhile(mode => mode != currentDisplayMode) : Modes;
+ return possibleDisplayModes.Where(mode => mode.CanHandleContext(httpContext));
+ }
+
+ /// <summary>
+ /// Returns DisplayInfo from the first IDisplayMode in Modes that can handle the given request and locate the virtual path.
+ /// If currentDisplayMode is not null and RequireConsistentDisplayMode is set to true the search for DisplayInfo will only
+ /// start with the currentDisplayMode.
+ /// </summary>
+ public DisplayInfo GetDisplayInfoForVirtualPath(string virtualPath, HttpContextBase httpContext, Func<string, bool> virtualPathExists, IDisplayMode currentDisplayMode)
+ {
+ return GetDisplayInfoForVirtualPath(virtualPath, httpContext, virtualPathExists, currentDisplayMode, RequireConsistentDisplayMode);
+ }
+
+ internal DisplayInfo GetDisplayInfoForVirtualPath(string virtualPath, HttpContextBase httpContext, Func<string, bool> virtualPathExists, IDisplayMode currentDisplayMode,
+ bool requireConsistentDisplayMode)
+ {
+ IEnumerable<IDisplayMode> possibleDisplayModes = GetAvailableDisplayModesForContext(httpContext, currentDisplayMode, requireConsistentDisplayMode);
+ return possibleDisplayModes.Select(mode => mode.GetDisplayInfo(httpContext, virtualPath, virtualPathExists))
+ .FirstOrDefault(info => info != null);
+ }
+
+ internal static IDisplayMode GetDisplayMode(HttpContextBase context)
+ {
+ return context != null ? context.Items[_displayModeKey] as IDisplayMode : null;
+ }
+
+ internal static void SetDisplayMode(HttpContextBase context, IDisplayMode displayMode)
+ {
+ if (context != null)
+ {
+ context.Items[_displayModeKey] = displayMode;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/DynamicHttpApplicationState.cs b/src/System.Web.WebPages/DynamicHttpApplicationState.cs
new file mode 100644
index 00000000..9c28dafb
--- /dev/null
+++ b/src/System.Web.WebPages/DynamicHttpApplicationState.cs
@@ -0,0 +1,78 @@
+using System.Dynamic;
+using System.Web.WebPages.Resources;
+
+namespace System.Web.WebPages
+{
+ internal class DynamicHttpApplicationState : DynamicObject
+ {
+ private HttpApplicationStateBase _state;
+
+ public DynamicHttpApplicationState(HttpApplicationStateBase state)
+ {
+ _state = state;
+ }
+
+ public override bool TryGetMember(GetMemberBinder binder, out object result)
+ {
+ result = _state[binder.Name];
+ // We return true here because HttpApplicationState returns null if the key is not
+ // in the dictionary, so we simply pass on the returned value.
+ return true;
+ }
+
+ public override bool TrySetMember(SetMemberBinder binder, object value)
+ {
+ _state[binder.Name] = value;
+ return true;
+ }
+
+ public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result)
+ {
+ if (indexes == null || indexes.Length != 1)
+ {
+ throw new ArgumentException(WebPageResources.DynamicDictionary_InvalidNumberOfIndexes);
+ }
+
+ result = null;
+ string key = indexes[0] as string;
+ if (key != null)
+ {
+ result = _state[key];
+ }
+ else if (indexes[0] is int)
+ {
+ result = _state[(int)indexes[0]];
+ }
+ else
+ {
+ // HttpApplicationState only supports keys of type string and int when getting values, so any attempt
+ // to use other types will result in an error. We throw an exception here to explain to the user what is wrong.
+ // Returning false will instead cause a runtime binder exception which might be confusing to the user.
+ throw new ArgumentException(WebPageResources.DynamicHttpApplicationState_UseOnlyStringOrIntToGet);
+ }
+ return true;
+ }
+
+ public override bool TrySetIndex(SetIndexBinder binder, object[] indexes, object value)
+ {
+ if (indexes == null || indexes.Length != 1)
+ {
+ throw new ArgumentException(WebPageResources.DynamicDictionary_InvalidNumberOfIndexes);
+ }
+
+ string key = indexes[0] as string;
+ if (key != null)
+ {
+ _state[key] = value;
+ return true;
+ }
+ else
+ {
+ // HttpApplicationState only supports keys of type string when setting values, so any attempt
+ // to use other types will result in an error. We throw an exception here to explain to the user what is wrong.
+ // Returning false will instead cause a runtime binder error which might be confusing to the user.
+ throw new ArgumentException(WebPageResources.DynamicHttpApplicationState_UseOnlyStringToSet);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/DynamicPageDataDictionary.cs b/src/System.Web.WebPages/DynamicPageDataDictionary.cs
new file mode 100644
index 00000000..0cb8c653
--- /dev/null
+++ b/src/System.Web.WebPages/DynamicPageDataDictionary.cs
@@ -0,0 +1,149 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Dynamic;
+using System.Web.WebPages.Resources;
+
+namespace System.Web.WebPages
+{
+ /// <summary>
+ /// This is a wrapper around PageDataDictionary[[dynamic]] which allows dynamic
+ /// access (e.g. dict.Foo). Like PageDataDictionary, it returns null if the key is not found,
+ /// instead of throwing an exception.
+ /// This class is intended to be used as DynamicPageDataDictionary[[dynamic]]
+ /// </summary>
+ // This is a generic type because C# does not allow implementing an interface
+ // involving dynamic types (implementing IDictionary<object, dynamic> causes
+ // a compile error
+ // http://blogs.msdn.com/cburrows/archive/2009/02/04/c-dynamic-part-vii.aspx).
+ internal class DynamicPageDataDictionary<TValue> : DynamicObject, IDictionary<object, TValue>
+ {
+ private PageDataDictionary<TValue> _data;
+
+ public DynamicPageDataDictionary(PageDataDictionary<TValue> dictionary)
+ {
+ _data = dictionary;
+ }
+
+ public ICollection<object> Keys
+ {
+ get { return _data.Keys; }
+ }
+
+ public ICollection<TValue> Values
+ {
+ get { return _data.Values; }
+ }
+
+ public int Count
+ {
+ get { return _data.Count; }
+ }
+
+ public bool IsReadOnly
+ {
+ get { return _data.IsReadOnly; }
+ }
+
+ public TValue this[object key]
+ {
+ get { return _data[key]; }
+ set { _data[key] = value; }
+ }
+
+ public override bool TryGetMember(GetMemberBinder binder, out object result)
+ {
+ result = _data[binder.Name];
+ // We return true here because PageDataDictionary returns null if the key is not
+ // in the dictionary, so we simply pass on the returned value.
+ return true;
+ }
+
+ public override bool TrySetMember(SetMemberBinder binder, object value)
+ {
+ // This cast should always succeed assuming TValue is dynamic.
+ TValue v = (TValue)value;
+ _data[binder.Name] = v;
+ return true;
+ }
+
+ public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result)
+ {
+ if (indexes == null || indexes.Length != 1)
+ {
+ throw new ArgumentException(WebPageResources.DynamicDictionary_InvalidNumberOfIndexes);
+ }
+
+ result = _data[indexes[0]];
+ // We return true here because PageDataDictionary returns null if the key is not
+ // in the dictionary, so we simply pass on the returned value.
+ return true;
+ }
+
+ public override bool TrySetIndex(SetIndexBinder binder, object[] indexes, object value)
+ {
+ if (indexes == null || indexes.Length != 1)
+ {
+ throw new ArgumentException(WebPageResources.DynamicDictionary_InvalidNumberOfIndexes);
+ }
+
+ // This cast should always succeed assuming TValue is dynamic.
+ _data[indexes[0]] = (TValue)value;
+ return true;
+ }
+
+ public void Add(object key, TValue value)
+ {
+ _data.Add(key, value);
+ }
+
+ public bool ContainsKey(object key)
+ {
+ return _data.ContainsKey(key);
+ }
+
+ public bool Remove(object key)
+ {
+ return _data.Remove(key);
+ }
+
+ public bool TryGetValue(object key, out TValue value)
+ {
+ return _data.TryGetValue(key, out value);
+ }
+
+ public void Add(KeyValuePair<object, TValue> item)
+ {
+ _data.Add(item);
+ }
+
+ public void Clear()
+ {
+ _data.Clear();
+ }
+
+ public bool Contains(KeyValuePair<object, TValue> item)
+ {
+ return _data.Contains(item);
+ }
+
+ public void CopyTo(KeyValuePair<object, TValue>[] array, int arrayIndex)
+ {
+ _data.CopyTo(array, arrayIndex);
+ }
+
+ public bool Remove(KeyValuePair<object, TValue> item)
+ {
+ return _data.Remove(item.Key);
+ }
+
+ public IEnumerator<KeyValuePair<object, TValue>> GetEnumerator()
+ {
+ return _data.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return _data.GetEnumerator();
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/FileExistenceCache.cs b/src/System.Web.WebPages/FileExistenceCache.cs
new file mode 100644
index 00000000..4718d00c
--- /dev/null
+++ b/src/System.Web.WebPages/FileExistenceCache.cs
@@ -0,0 +1,77 @@
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Threading;
+using System.Web.Hosting;
+
+namespace System.Web.WebPages
+{
+ /// <summary>
+ /// This class caches the result of VirtualPathProvider.FileExists for a short
+ /// period of time, and recomputes it if necessary.
+ ///
+ /// The default VPP MapPathBasedVirtualPathProvider caches the result of
+ /// the FileExists call with the appropriate dependencies, so it is less
+ /// expensive on subsequent calls, but it still needs to do MapPath which can
+ /// take quite some time.
+ /// </summary>
+ internal class FileExistenceCache
+ {
+ private const int TickPerMiliseconds = 10000;
+ private readonly VirtualPathProvider _virtualPathProvider;
+ private ConcurrentDictionary<string, bool> _cache;
+ private long _creationTick;
+ private int _ticksBeforeReset;
+
+ public FileExistenceCache(VirtualPathProvider virtualPathProvider, int milliSecondsBeforeReset = 1000)
+ {
+ _virtualPathProvider = virtualPathProvider;
+ _ticksBeforeReset = milliSecondsBeforeReset * TickPerMiliseconds;
+ Reset();
+ }
+
+ // Use the VPP returned by the HostingEnvironment unless a custom vpp is passed in (mainly for testing purposes)
+ public VirtualPathProvider VirtualPathProvider
+ {
+ get { return _virtualPathProvider; }
+ }
+
+ public int MilliSecondsBeforeReset
+ {
+ get { return _ticksBeforeReset / TickPerMiliseconds; }
+ internal set { _ticksBeforeReset = value * TickPerMiliseconds; }
+ }
+
+ internal IDictionary<string, bool> CacheInternal
+ {
+ get { return _cache; }
+ }
+
+ public bool TimeExceeded
+ {
+ get { return (DateTime.UtcNow.Ticks - Interlocked.Read(ref _creationTick)) > _ticksBeforeReset; }
+ }
+
+ public void Reset()
+ {
+ _cache = new ConcurrentDictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
+
+ DateTime now = DateTime.UtcNow;
+ long tick = now.Ticks;
+
+ Interlocked.Exchange(ref _creationTick, tick);
+ }
+
+ public bool FileExists(string virtualPath)
+ {
+ if (TimeExceeded)
+ {
+ Reset();
+ }
+ // The right way to do this is to verify in the constructor that the VirtualPathProvider argument is not null.
+ // However when unit testing this, we often new up instances when not running under Asp.Net when HostingEnvironment.VirtualPathProvider is null.
+ Debug.Assert(_virtualPathProvider != null);
+ return _cache.GetOrAdd(virtualPath, _virtualPathProvider.FileExists);
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/GlobalSuppressions.cs b/src/System.Web.WebPages/GlobalSuppressions.cs
new file mode 100644
index 00000000..07747b70
--- /dev/null
+++ b/src/System.Web.WebPages/GlobalSuppressions.cs
@@ -0,0 +1,15 @@
+// This file is used by Code Analysis to maintain SuppressMessage
+// attributes that are applied to this project.
+// Project-level suppressions either have no target or are given
+// a specific target and scoped to a namespace, type, member, etc.
+//
+// To add a suppression to this file, right-click the message in the
+// Error List, point to "Suppress Message(s)", and click
+// "In Project Suppression File".
+// You do not need to add suppressions to this file manually.
+
+using System.Diagnostics.CodeAnalysis;
+
+[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "System.Web.WebPages.Html", Justification = "The namespace contains types specific to Razor. It allows a way for MVC Razor host to identify and remove the namespace")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "System.Web.Mvc", Justification = "This namespace contains TagBuilder and other types forwarded from System.Web.Mvc. The namespace must stay the way it is for type forwarding to work")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "System.Web.WebPages.Instrumentation", Justification = "This namespace contains Instrumentation types and represents an isolated set of functionality.")]
diff --git a/src/System.Web.WebPages/HelperPage.cs b/src/System.Web.WebPages/HelperPage.cs
new file mode 100644
index 00000000..5d6a5ea6
--- /dev/null
+++ b/src/System.Web.WebPages/HelperPage.cs
@@ -0,0 +1,266 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Security.Principal;
+using System.Web.Caching;
+using System.Web.WebPages.Html;
+using System.Web.WebPages.Instrumentation;
+
+namespace System.Web.WebPages
+{
+ [SuppressMessage("Microsoft.Design", "CA1053:StaticHolderTypesShouldNotHaveConstructors", Justification = "Even though this is essentially a static class, we need helper classes to inherit from it to get their static methods")]
+ public class HelperPage
+ {
+ private static WebPageContext _pageContext;
+ private static InstrumentationService _instrumentationService = null;
+
+ private static InstrumentationService InstrumentationService
+ {
+ get
+ {
+ if (_instrumentationService == null)
+ {
+ _instrumentationService = new InstrumentationService();
+ }
+ return _instrumentationService;
+ }
+ }
+
+ public static HttpContextBase Context
+ {
+ get { return new HttpContextWrapper(HttpContext.Current); }
+ }
+
+ public static WebPageRenderingBase CurrentPage
+ {
+ get { return PageContext.Page; }
+ }
+
+ public static dynamic Page
+ {
+ get { return CurrentPage.Page; }
+ }
+
+ public static dynamic Model
+ {
+ get
+ {
+ WebPage currentWebPage = CurrentPage as WebPage;
+ if (currentWebPage == null)
+ {
+ return null;
+ }
+ return currentWebPage.Model;
+ }
+ }
+
+ public static ModelStateDictionary ModelState
+ {
+ get
+ {
+ WebPage currentWebPage = CurrentPage as WebPage;
+ if (currentWebPage == null)
+ {
+ return null;
+ }
+ return currentWebPage.ModelState;
+ }
+ }
+
+ public static HtmlHelper Html
+ {
+ get
+ {
+ WebPage currentWebPage = CurrentPage as WebPage;
+ if (currentWebPage == null)
+ {
+ return null;
+ }
+ return currentWebPage.Html;
+ }
+ }
+
+ public static WebPageContext PageContext
+ {
+ get { return _pageContext ?? WebPageContext.Current; }
+ set { _pageContext = value; }
+ }
+
+ public static HttpApplicationStateBase AppState
+ {
+ get
+ {
+ if (Context != null)
+ {
+ return Context.Application;
+ }
+ return null;
+ }
+ }
+
+ public static dynamic App
+ {
+ get { return CurrentPage.App; }
+ }
+
+ public static string VirtualPath
+ {
+ get { return PageContext.Page.VirtualPath; }
+ }
+
+ public static Cache Cache
+ {
+ get
+ {
+ if (Context != null)
+ {
+ return Context.Cache;
+ }
+ return null;
+ }
+ }
+
+ public static HttpRequestBase Request
+ {
+ get
+ {
+ if (Context != null)
+ {
+ return Context.Request;
+ }
+ return null;
+ }
+ }
+
+ public static HttpResponseBase Response
+ {
+ get
+ {
+ if (Context != null)
+ {
+ return Context.Response;
+ }
+ return null;
+ }
+ }
+
+ public static HttpServerUtilityBase Server
+ {
+ get
+ {
+ if (Context != null)
+ {
+ return Context.Server;
+ }
+ return null;
+ }
+ }
+
+ public static HttpSessionStateBase Session
+ {
+ get
+ {
+ if (Context != null)
+ {
+ return Context.Session;
+ }
+ return null;
+ }
+ }
+
+ public static IList<string> UrlData
+ {
+ get { return CurrentPage.UrlData; }
+ }
+
+ public static IPrincipal User
+ {
+ get { return CurrentPage.User; }
+ }
+
+ public static bool IsPost
+ {
+ get { return CurrentPage.IsPost; }
+ }
+
+ public static bool IsAjax
+ {
+ get { return CurrentPage.IsAjax; }
+ }
+
+ public static IDictionary<object, dynamic> PageData
+ {
+ get { return PageContext.PageData; }
+ }
+
+ protected static string HelperVirtualPath { get; set; }
+
+ public static string Href(string path, params object[] pathParts)
+ {
+ return CurrentPage.Href(path, pathParts);
+ }
+
+ public static void WriteTo(TextWriter writer, object value)
+ {
+ WebPageBase.WriteTo(writer, value);
+ }
+
+ public static void WriteLiteralTo(TextWriter writer, object value)
+ {
+ WebPageBase.WriteLiteralTo(writer, value);
+ }
+
+ public static void WriteTo(TextWriter writer, HelperResult value)
+ {
+ WebPageBase.WriteTo(writer, value);
+ }
+
+ public static void WriteLiteralTo(TextWriter writer, HelperResult value)
+ {
+ WebPageBase.WriteLiteralTo(writer, value);
+ }
+
+ public static void WriteAttributeTo(TextWriter writer, string name, PositionTagged<string> prefix, PositionTagged<string> suffix, params AttributeValue[] values)
+ {
+ CurrentPage.WriteAttributeTo(VirtualPath, writer, name, prefix, suffix, values);
+ }
+
+ public static void BeginContext(string virtualPath, int startPosition, int length, bool isLiteral)
+ {
+ BeginContext(PageContext.Page.GetOutputWriter(), virtualPath, startPosition, length, isLiteral);
+ }
+
+ public static void BeginContext(TextWriter writer, string virtualPath, int startPosition, int length, bool isLiteral)
+ {
+ // Double check that the instrumentation service is active because WriteAttribute always calls this
+ if (InstrumentationService.IsAvailable)
+ {
+ InstrumentationService.BeginContext(Context,
+ virtualPath,
+ writer,
+ startPosition,
+ length,
+ isLiteral);
+ }
+ }
+
+ public static void EndContext(string virtualPath, int startPosition, int length, bool isLiteral)
+ {
+ EndContext(PageContext.Page.GetOutputWriter(), virtualPath, startPosition, length, isLiteral);
+ }
+
+ public static void EndContext(TextWriter writer, string virtualPath, int startPosition, int length, bool isLiteral)
+ {
+ // Double check that the instrumentation service is active because WriteAttribute always calls this
+ if (InstrumentationService.IsAvailable)
+ {
+ InstrumentationService.EndContext(Context,
+ virtualPath,
+ writer,
+ startPosition,
+ length,
+ isLiteral);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/HelperResult.cs b/src/System.Web.WebPages/HelperResult.cs
new file mode 100644
index 00000000..b29fe97d
--- /dev/null
+++ b/src/System.Web.WebPages/HelperResult.cs
@@ -0,0 +1,38 @@
+using System.Globalization;
+using System.IO;
+
+namespace System.Web.WebPages
+{
+ public class HelperResult : IHtmlString
+ {
+ private readonly Action<TextWriter> _action;
+
+ public HelperResult(Action<TextWriter> action)
+ {
+ if (action == null)
+ {
+ throw new ArgumentNullException("action");
+ }
+ _action = action;
+ }
+
+ public string ToHtmlString()
+ {
+ return ToString();
+ }
+
+ public override string ToString()
+ {
+ using (var writer = new StringWriter(CultureInfo.InvariantCulture))
+ {
+ _action(writer);
+ return writer.ToString();
+ }
+ }
+
+ public void WriteTo(TextWriter writer)
+ {
+ _action(writer);
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Helpers/AntiForgery.cs b/src/System.Web.WebPages/Helpers/AntiForgery.cs
new file mode 100644
index 00000000..afd827f9
--- /dev/null
+++ b/src/System.Web.WebPages/Helpers/AntiForgery.cs
@@ -0,0 +1,48 @@
+using System.Web.WebPages.Resources;
+
+namespace System.Web.Helpers
+{
+ public static class AntiForgery
+ {
+ private static readonly AntiForgeryWorker _worker = new AntiForgeryWorker();
+
+ public static HtmlString GetHtml()
+ {
+ if (HttpContext.Current == null)
+ {
+ throw new ArgumentException(WebPageResources.HttpContextUnavailable);
+ }
+
+ return GetHtml(new HttpContextWrapper(HttpContext.Current), salt: null, domain: null, path: null);
+ }
+
+ public static HtmlString GetHtml(HttpContextBase httpContext, string salt, string domain, string path)
+ {
+ if (httpContext == null)
+ {
+ throw new ArgumentNullException("httpContext");
+ }
+
+ return _worker.GetHtml(httpContext, salt, domain, path);
+ }
+
+ public static void Validate()
+ {
+ if (HttpContext.Current == null)
+ {
+ throw new ArgumentException(WebPageResources.HttpContextUnavailable);
+ }
+ Validate(new HttpContextWrapper(HttpContext.Current), salt: null);
+ }
+
+ public static void Validate(HttpContextBase httpContext, string salt)
+ {
+ if (httpContext == null)
+ {
+ throw new ArgumentNullException("httpContext");
+ }
+
+ _worker.Validate(httpContext, salt);
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Helpers/AntiForgeryData.cs b/src/System.Web.WebPages/Helpers/AntiForgeryData.cs
new file mode 100644
index 00000000..5af09b02
--- /dev/null
+++ b/src/System.Web.WebPages/Helpers/AntiForgeryData.cs
@@ -0,0 +1,117 @@
+using System.Security.Cryptography;
+using System.Security.Principal;
+using System.Text;
+
+namespace System.Web.Helpers
+{
+ internal sealed class AntiForgeryData
+ {
+ private const string AntiForgeryTokenFieldName = "__RequestVerificationToken";
+
+ private const int TokenLength = 128 / 8;
+ private static readonly RNGCryptoServiceProvider _prng = new RNGCryptoServiceProvider();
+
+ private DateTime _creationDate = DateTime.UtcNow;
+ private string _salt;
+ private string _username;
+ private string _value;
+
+ public AntiForgeryData()
+ {
+ }
+
+ // copy constructor
+ public AntiForgeryData(AntiForgeryData token)
+ {
+ if (token == null)
+ {
+ throw new ArgumentNullException("token");
+ }
+
+ CreationDate = token.CreationDate;
+ Salt = token.Salt;
+ Username = token.Username;
+ Value = token.Value;
+ }
+
+ public DateTime CreationDate
+ {
+ get { return _creationDate; }
+ set { _creationDate = value; }
+ }
+
+ public string Salt
+ {
+ get { return _salt ?? String.Empty; }
+ set { _salt = value; }
+ }
+
+ public string Username
+ {
+ get { return _username ?? String.Empty; }
+ set { _username = value; }
+ }
+
+ public string Value
+ {
+ get { return _value ?? String.Empty; }
+ set { _value = value; }
+ }
+
+ private static string Base64EncodeForCookieName(string s)
+ {
+ byte[] rawBytes = Encoding.UTF8.GetBytes(s);
+ string base64String = Convert.ToBase64String(rawBytes);
+
+ // replace base64-specific characters with characters that are safe for a cookie name
+ return base64String.Replace('+', '.').Replace('/', '-').Replace('=', '_');
+ }
+
+ private static string GenerateRandomTokenString()
+ {
+ byte[] tokenBytes = new byte[TokenLength];
+ _prng.GetBytes(tokenBytes);
+
+ string token = Convert.ToBase64String(tokenBytes);
+ return token;
+ }
+
+ // If the app path is provided, we're generating a cookie name rather than a field name, and the cookie names should
+ // be unique so that a development server cookie and an IIS cookie - both running on localhost - don't stomp on
+ // each other.
+ internal static string GetAntiForgeryTokenName(string appPath)
+ {
+ if (String.IsNullOrEmpty(appPath))
+ {
+ return AntiForgeryTokenFieldName;
+ }
+ else
+ {
+ return AntiForgeryTokenFieldName + "_" + Base64EncodeForCookieName(appPath);
+ }
+ }
+
+ internal static string GetUsername(IPrincipal user)
+ {
+ if (user != null)
+ {
+ IIdentity identity = user.Identity;
+ if (identity != null && identity.IsAuthenticated)
+ {
+ return identity.Name;
+ }
+ }
+
+ return String.Empty;
+ }
+
+ public static AntiForgeryData NewToken()
+ {
+ string tokenString = GenerateRandomTokenString();
+ return new AntiForgeryData()
+ {
+ Value = tokenString
+ };
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Helpers/AntiForgeryDataSerializer.cs b/src/System.Web.WebPages/Helpers/AntiForgeryDataSerializer.cs
new file mode 100644
index 00000000..f8df2564
--- /dev/null
+++ b/src/System.Web.WebPages/Helpers/AntiForgeryDataSerializer.cs
@@ -0,0 +1,109 @@
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Text;
+using System.Web.Mvc;
+using System.Web.Security;
+using System.Web.WebPages.Resources;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.Helpers
+{
+ internal class AntiForgeryDataSerializer
+ {
+ // Testing hooks
+
+ internal Func<string, byte[]> Decoder =
+ (value) => MachineKey.Decode(Base64ToHex(value), MachineKeyProtection.All);
+
+ internal Func<byte[], string> Encoder =
+ (bytes) => HexToBase64(MachineKey.Encode(bytes, MachineKeyProtection.All).ToUpperInvariant());
+
+ [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "MemoryStream is resilient to double-Dispose")]
+ public virtual AntiForgeryData Deserialize(string serializedToken)
+ {
+ if (String.IsNullOrEmpty(serializedToken))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "serializedToken");
+ }
+
+ try
+ {
+ using (MemoryStream stream = new MemoryStream(Decoder(serializedToken)))
+ {
+ using (BinaryReader reader = new BinaryReader(stream))
+ {
+ return new AntiForgeryData
+ {
+ Salt = reader.ReadString(),
+ Value = reader.ReadString(),
+ CreationDate = new DateTime(reader.ReadInt64()),
+ Username = reader.ReadString()
+ };
+ }
+ }
+ }
+ catch
+ {
+ throw new HttpAntiForgeryException(WebPageResources.AntiForgeryToken_ValidationFailed);
+ }
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "MemoryStream is resilient to double-Dispose")]
+ public virtual string Serialize(AntiForgeryData token)
+ {
+ if (token == null)
+ {
+ throw new ArgumentNullException("token");
+ }
+
+ using (MemoryStream stream = new MemoryStream())
+ {
+ using (BinaryWriter writer = new BinaryWriter(stream))
+ {
+ writer.Write(token.Salt);
+ writer.Write(token.Value);
+ writer.Write(token.CreationDate.Ticks);
+ writer.Write(token.Username);
+
+ return Encoder(stream.ToArray());
+ }
+ }
+ }
+
+ // String transformation helpers
+
+ private static string Base64ToHex(string base64)
+ {
+ StringBuilder builder = new StringBuilder(base64.Length * 4);
+ foreach (byte b in Convert.FromBase64String(base64))
+ {
+ builder.Append(HexDigit(b >> 4));
+ builder.Append(HexDigit(b & 0x0F));
+ }
+ string result = builder.ToString();
+ return result;
+ }
+
+ internal static char HexDigit(int value)
+ {
+ return (char)(value > 9 ? value + '7' : value + '0');
+ }
+
+ internal static int HexValue(char digit)
+ {
+ return digit > '9' ? digit - '7' : digit - '0';
+ }
+
+ private static string HexToBase64(string hex)
+ {
+ int size = hex.Length / 2;
+ byte[] bytes = new byte[size];
+ for (int idx = 0; idx < size; idx++)
+ {
+ bytes[idx] = (byte)((HexValue(hex[idx * 2]) << 4) + HexValue(hex[(idx * 2) + 1]));
+ }
+ string result = Convert.ToBase64String(bytes);
+ return result;
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Helpers/AntiForgeryWorker.cs b/src/System.Web.WebPages/Helpers/AntiForgeryWorker.cs
new file mode 100644
index 00000000..314921b8
--- /dev/null
+++ b/src/System.Web.WebPages/Helpers/AntiForgeryWorker.cs
@@ -0,0 +1,117 @@
+using System.Diagnostics;
+using System.Web.Mvc;
+using System.Web.WebPages.Resources;
+
+namespace System.Web.Helpers
+{
+ internal class AntiForgeryWorker
+ {
+ public AntiForgeryWorker()
+ {
+ Serializer = new AntiForgeryDataSerializer();
+ }
+
+ internal AntiForgeryDataSerializer Serializer { get; set; }
+
+ private static HttpAntiForgeryException CreateValidationException()
+ {
+ return new HttpAntiForgeryException(WebPageResources.AntiForgeryToken_ValidationFailed);
+ }
+
+ public HtmlString GetHtml(HttpContextBase httpContext, string salt, string domain, string path)
+ {
+ Debug.Assert(httpContext != null);
+
+ string formValue = GetAntiForgeryTokenAndSetCookie(httpContext, salt, domain, path);
+ string fieldName = AntiForgeryData.GetAntiForgeryTokenName(null);
+
+ TagBuilder builder = new TagBuilder("input");
+ builder.Attributes["type"] = "hidden";
+ builder.Attributes["name"] = fieldName;
+ builder.Attributes["value"] = formValue;
+ return new HtmlString(builder.ToString(TagRenderMode.SelfClosing));
+ }
+
+ private string GetAntiForgeryTokenAndSetCookie(HttpContextBase httpContext, string salt, string domain, string path)
+ {
+ string cookieName = AntiForgeryData.GetAntiForgeryTokenName(httpContext.Request.ApplicationPath);
+
+ AntiForgeryData cookieToken = null;
+ HttpCookie cookie = httpContext.Request.Cookies[cookieName];
+ if (cookie != null)
+ {
+ try
+ {
+ cookieToken = Serializer.Deserialize(cookie.Value);
+ }
+ catch (HttpAntiForgeryException)
+ {
+ }
+ }
+
+ if (cookieToken == null)
+ {
+ cookieToken = AntiForgeryData.NewToken();
+ string cookieValue = Serializer.Serialize(cookieToken);
+
+ HttpCookie newCookie = new HttpCookie(cookieName, cookieValue) { HttpOnly = true, Domain = domain };
+ if (!String.IsNullOrEmpty(path))
+ {
+ newCookie.Path = path;
+ }
+ httpContext.Response.Cookies.Set(newCookie);
+ }
+
+ AntiForgeryData formToken = new AntiForgeryData(cookieToken)
+ {
+ Salt = salt,
+ Username = AntiForgeryData.GetUsername(httpContext.User)
+ };
+ return Serializer.Serialize(formToken);
+ }
+
+ public void Validate(HttpContextBase context, string salt)
+ {
+ Debug.Assert(context != null);
+
+ string fieldName = AntiForgeryData.GetAntiForgeryTokenName(null);
+ string cookieName = AntiForgeryData.GetAntiForgeryTokenName(context.Request.ApplicationPath);
+
+ HttpCookie cookie = context.Request.Cookies[cookieName];
+ if (cookie == null || String.IsNullOrEmpty(cookie.Value))
+ {
+ // error: cookie token is missing
+ throw CreateValidationException();
+ }
+ AntiForgeryData cookieToken = Serializer.Deserialize(cookie.Value);
+
+ string formValue = context.Request.Form[fieldName];
+ if (String.IsNullOrEmpty(formValue))
+ {
+ // error: form token is missing
+ throw CreateValidationException();
+ }
+ AntiForgeryData formToken = Serializer.Deserialize(formValue);
+
+ if (!String.Equals(cookieToken.Value, formToken.Value, StringComparison.Ordinal))
+ {
+ // error: form token does not match cookie token
+ throw CreateValidationException();
+ }
+
+ string currentUsername = AntiForgeryData.GetUsername(context.User);
+ if (!String.Equals(formToken.Username, currentUsername, StringComparison.OrdinalIgnoreCase))
+ {
+ // error: form token is not valid for this user
+ // (don't care about cookie token)
+ throw CreateValidationException();
+ }
+
+ if (!String.Equals(salt ?? String.Empty, formToken.Salt, StringComparison.Ordinal))
+ {
+ // error: custom validation failed
+ throw CreateValidationException();
+ }
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Helpers/UnvalidatedRequestValues.cs b/src/System.Web.WebPages/Helpers/UnvalidatedRequestValues.cs
new file mode 100644
index 00000000..9fe271fe
--- /dev/null
+++ b/src/System.Web.WebPages/Helpers/UnvalidatedRequestValues.cs
@@ -0,0 +1,62 @@
+using System.Collections.Specialized;
+
+namespace System.Web.Helpers
+{
+ // Provides access to Request.* collections, except that these have not gone through request validation.
+ public sealed class UnvalidatedRequestValues
+ {
+ private readonly HttpRequestBase _request;
+ private readonly Func<NameValueCollection> _formGetter;
+ private readonly Func<NameValueCollection> _queryStringGetter;
+
+ internal UnvalidatedRequestValues(HttpRequestBase request, Func<NameValueCollection> formGetter, Func<NameValueCollection> queryStringGetter)
+ {
+ _request = request;
+ _formGetter = formGetter;
+ _queryStringGetter = queryStringGetter;
+ }
+
+ public NameValueCollection Form
+ {
+ get { return _formGetter(); }
+ }
+
+ public NameValueCollection QueryString
+ {
+ get { return _queryStringGetter(); }
+ }
+
+ // this item getter follows the same logic as HttpRequest.get_Item
+ public string this[string key]
+ {
+ get
+ {
+ string queryStringValue = QueryString[key];
+ if (queryStringValue != null)
+ {
+ return queryStringValue;
+ }
+
+ string formValue = Form[key];
+ if (formValue != null)
+ {
+ return formValue;
+ }
+
+ HttpCookie cookie = _request.Cookies[key];
+ if (cookie != null)
+ {
+ return cookie.Value;
+ }
+
+ string serverVarValue = _request.ServerVariables[key];
+ if (serverVarValue != null)
+ {
+ return serverVarValue;
+ }
+
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Helpers/Validation.cs b/src/System.Web.WebPages/Helpers/Validation.cs
new file mode 100644
index 00000000..63fc2827
--- /dev/null
+++ b/src/System.Web.WebPages/Helpers/Validation.cs
@@ -0,0 +1,39 @@
+using System.Collections.Specialized;
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.Web.Infrastructure.DynamicValidationHelper;
+
+namespace System.Web.Helpers
+{
+ public static class Validation
+ {
+ [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "request",
+ Justification = "Parameter is only meant for making this show up as 'Request.Unvalidated()', which closely resembles FX45 syntax.")]
+ public static UnvalidatedRequestValues Unvalidated(this HttpRequestBase request)
+ {
+ return Unvalidated((HttpRequest)null);
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "request",
+ Justification = "Parameter is only meant for making this show up as 'Request.Unvalidated()', which closely resembles FX45 syntax.")]
+ public static UnvalidatedRequestValues Unvalidated(this HttpRequest request)
+ {
+ // We don't actually need the request object; we'll get HttpContext.Current directly.
+ HttpContext context = HttpContext.Current;
+ Func<NameValueCollection> formGetter;
+ Func<NameValueCollection> queryStringGetter;
+ ValidationUtility.GetUnvalidatedCollections(context, out formGetter, out queryStringGetter);
+
+ return new UnvalidatedRequestValues(new HttpRequestWrapper(context.Request), formGetter, queryStringGetter);
+ }
+
+ public static string Unvalidated(this HttpRequestBase request, string key)
+ {
+ return Unvalidated(request)[key];
+ }
+
+ public static string Unvalidated(this HttpRequest request, string key)
+ {
+ return Unvalidated(request)[key];
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Html/HtmlHelper.Checkbox.cs b/src/System.Web.WebPages/Html/HtmlHelper.Checkbox.cs
new file mode 100644
index 00000000..954b0afa
--- /dev/null
+++ b/src/System.Web.WebPages/Html/HtmlHelper.Checkbox.cs
@@ -0,0 +1,84 @@
+using System.Collections.Generic;
+using System.Web.Mvc;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.WebPages.Html
+{
+ public partial class HtmlHelper
+ {
+ public IHtmlString CheckBox(string name)
+ {
+ return CheckBox(name, htmlAttributes: (IDictionary<string, object>)null);
+ }
+
+ public IHtmlString CheckBox(string name, object htmlAttributes)
+ {
+ return CheckBox(name, TypeHelper.ObjectToDictionary(htmlAttributes));
+ }
+
+ public IHtmlString CheckBox(string name, IDictionary<string, object> htmlAttributes)
+ {
+ if (String.IsNullOrEmpty(name))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "name");
+ }
+
+ return BuildCheckBox(name, null, htmlAttributes);
+ }
+
+ public IHtmlString CheckBox(string name, bool isChecked)
+ {
+ return CheckBox(name, isChecked, (IDictionary<string, object>)null);
+ }
+
+ public IHtmlString CheckBox(string name, bool isChecked, object htmlAttributes)
+ {
+ return CheckBox(name, isChecked, TypeHelper.ObjectToDictionary(htmlAttributes));
+ }
+
+ public IHtmlString CheckBox(string name, bool isChecked, IDictionary<string, object> htmlAttributes)
+ {
+ if (String.IsNullOrEmpty(name))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "name");
+ }
+ return BuildCheckBox(name, isChecked, htmlAttributes);
+ }
+
+ private IHtmlString BuildCheckBox(string name, bool? isChecked, IDictionary<string, object> attributes)
+ {
+ TagBuilder builder = new TagBuilder("input");
+ builder.MergeAttribute("type", "checkbox", replaceExisting: true);
+ builder.GenerateId(name);
+ builder.MergeAttributes(attributes, replaceExisting: true);
+ builder.MergeAttribute("name", name, replaceExisting: true);
+
+ if (UnobtrusiveJavaScriptEnabled)
+ {
+ var validationAttributes = _validationHelper.GetUnobtrusiveValidationAttributes(name);
+ builder.MergeAttributes(validationAttributes, replaceExisting: false);
+ }
+
+ var model = ModelState[name];
+ if (model != null && model.Value != null)
+ {
+ bool modelValue = (bool)ConvertTo(model.Value, typeof(bool));
+ isChecked = isChecked ?? modelValue;
+ }
+ if (isChecked.HasValue)
+ {
+ if (isChecked.Value == true)
+ {
+ builder.MergeAttribute("checked", "checked", replaceExisting: true);
+ }
+ else
+ {
+ builder.Attributes.Remove("checked");
+ }
+ }
+
+ AddErrorClass(builder, name);
+ return builder.ToHtmlString(TagRenderMode.SelfClosing);
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Html/HtmlHelper.Input.cs b/src/System.Web.WebPages/Html/HtmlHelper.Input.cs
new file mode 100644
index 00000000..a4554d8b
--- /dev/null
+++ b/src/System.Web.WebPages/Html/HtmlHelper.Input.cs
@@ -0,0 +1,176 @@
+using System.Collections.Generic;
+using System.Data.Linq;
+using System.Diagnostics.CodeAnalysis;
+using System.Web.Mvc;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.WebPages.Html
+{
+ public partial class HtmlHelper
+ {
+ private enum InputType
+ {
+ Text,
+ Password,
+ Hidden
+ }
+
+ public IHtmlString TextBox(string name)
+ {
+ if (String.IsNullOrEmpty(name))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "name");
+ }
+
+ return BuildInputField(name, InputType.Text, value: null, isExplicitValue: false,
+ attributes: (IDictionary<string, object>)null);
+ }
+
+ public IHtmlString TextBox(string name, object value)
+ {
+ return TextBox(name, value, htmlAttributes: (IDictionary<string, object>)null);
+ }
+
+ public IHtmlString TextBox(string name, object value, object htmlAttributes)
+ {
+ return TextBox(name, value, TypeHelper.ObjectToDictionary(htmlAttributes));
+ }
+
+ public IHtmlString TextBox(string name, object value, IDictionary<string, object> htmlAttributes)
+ {
+ if (String.IsNullOrEmpty(name))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "name");
+ }
+
+ return BuildInputField(name, InputType.Text, value, isExplicitValue: true, attributes: htmlAttributes);
+ }
+
+ public IHtmlString Hidden(string name)
+ {
+ if (String.IsNullOrEmpty(name))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "name");
+ }
+
+ return BuildInputField(name, InputType.Hidden, value: null, isExplicitValue: false,
+ attributes: (IDictionary<string, object>)null);
+ }
+
+ public IHtmlString Hidden(string name, object value)
+ {
+ return Hidden(name, value, htmlAttributes: (IDictionary<string, object>)null);
+ }
+
+ public IHtmlString Hidden(string name, object value, object htmlAttributes)
+ {
+ return Hidden(name, value, TypeHelper.ObjectToDictionary(htmlAttributes));
+ }
+
+ public IHtmlString Hidden(string name, object value, IDictionary<string, object> htmlAttributes)
+ {
+ if (String.IsNullOrEmpty(name))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "name");
+ }
+
+ return BuildInputField(name, InputType.Hidden, GetHiddenFieldValue(value), isExplicitValue: true,
+ attributes: htmlAttributes);
+ }
+
+ private static object GetHiddenFieldValue(object value)
+ {
+ Binary binaryValue = value as Binary;
+ if (binaryValue != null)
+ {
+ value = binaryValue.ToArray();
+ }
+
+ byte[] byteArrayValue = value as byte[];
+ if (byteArrayValue != null)
+ {
+ value = Convert.ToBase64String(byteArrayValue);
+ }
+
+ return value;
+ }
+
+ public IHtmlString Password(string name)
+ {
+ if (String.IsNullOrEmpty(name))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "name");
+ }
+
+ return BuildInputField(name, InputType.Password, null, isExplicitValue: false,
+ attributes: (IDictionary<string, object>)null);
+ }
+
+ public IHtmlString Password(string name, object value)
+ {
+ return Password(name, value, htmlAttributes: (IDictionary<string, object>)null);
+ }
+
+ public IHtmlString Password(string name, object value, object htmlAttributes)
+ {
+ return Password(name, value, TypeHelper.ObjectToDictionary(htmlAttributes));
+ }
+
+ public IHtmlString Password(string name, object value, IDictionary<string, object> htmlAttributes)
+ {
+ if (String.IsNullOrEmpty(name))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "name");
+ }
+
+ return BuildInputField(name, InputType.Password, value, isExplicitValue: true, attributes: htmlAttributes);
+ }
+
+ private IHtmlString BuildInputField(string name, InputType type, object value, bool isExplicitValue,
+ IDictionary<string, object> attributes)
+ {
+ TagBuilder tagBuilder = new TagBuilder("input");
+ // Implicit parameters
+ tagBuilder.MergeAttribute("type", GetInputTypeString(type));
+ tagBuilder.GenerateId(name);
+
+ // Overwrite implicit
+ tagBuilder.MergeAttributes(attributes, replaceExisting: true);
+
+ if (UnobtrusiveJavaScriptEnabled)
+ {
+ // Add validation attributes
+ var validationAttributes = _validationHelper.GetUnobtrusiveValidationAttributes(name);
+ tagBuilder.MergeAttributes(validationAttributes, replaceExisting: false);
+ }
+
+ // Function arguments
+ tagBuilder.MergeAttribute("name", name, replaceExisting: true);
+ var modelState = ModelState[name];
+ if ((type != InputType.Password) && modelState != null)
+ {
+ // Don't use model values for passwords
+ value = value ?? modelState.Value ?? String.Empty;
+ }
+
+ if ((type != InputType.Password) || ((type == InputType.Password) && (value != null)))
+ {
+ // Review: Do we really need to be this pedantic about sticking to mvc?
+ tagBuilder.MergeAttribute("value", (string)ConvertTo(value, typeof(string)), replaceExisting: isExplicitValue);
+ }
+
+ AddErrorClass(tagBuilder, name);
+ return tagBuilder.ToHtmlString(TagRenderMode.SelfClosing);
+ }
+
+ [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "Input types are specified in lower case")]
+ private static string GetInputTypeString(InputType inputType)
+ {
+ if (!Enum.IsDefined(typeof(InputType), inputType))
+ {
+ inputType = InputType.Text;
+ }
+ return inputType.ToString().ToLowerInvariant();
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Html/HtmlHelper.Internal.cs b/src/System.Web.WebPages/Html/HtmlHelper.Internal.cs
new file mode 100644
index 00000000..b98dbe1d
--- /dev/null
+++ b/src/System.Web.WebPages/Html/HtmlHelper.Internal.cs
@@ -0,0 +1,117 @@
+using System.Collections;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Globalization;
+using System.Web.Mvc;
+using System.Web.WebPages.Resources;
+
+namespace System.Web.WebPages.Html
+{
+ public partial class HtmlHelper
+ {
+ private void AddErrorClass(TagBuilder tagBuilder, string name)
+ {
+ if (!ModelState.IsValidField(name))
+ {
+ tagBuilder.AddCssClass(ValidationInputCssClassName);
+ }
+ }
+
+ private static object ConvertTo(object value, Type type)
+ {
+ Debug.Assert(type != null);
+ return UnwrapPossibleArrayType(value, type, CultureInfo.InvariantCulture);
+ }
+
+ private static object UnwrapPossibleArrayType(object value, Type destinationType, CultureInfo culture)
+ {
+ if (value == null || destinationType.IsInstanceOfType(value))
+ {
+ return value;
+ }
+
+ // array conversion results in four cases, as below
+ Array valueAsArray = value as Array;
+ if (destinationType.IsArray)
+ {
+ Type destinationElementType = destinationType.GetElementType();
+ if (valueAsArray != null)
+ {
+ // case 1: both destination + source type are arrays, so convert each element
+ IList converted = Array.CreateInstance(destinationElementType, valueAsArray.Length);
+ for (int i = 0; i < valueAsArray.Length; i++)
+ {
+ converted[i] = ConvertSimpleType(valueAsArray.GetValue(i), destinationElementType, culture);
+ }
+ return converted;
+ }
+ else
+ {
+ // case 2: destination type is array but source is single element, so wrap element in array + convert
+ object element = ConvertSimpleType(value, destinationElementType, culture);
+ IList converted = Array.CreateInstance(destinationElementType, 1);
+ converted[0] = element;
+ return converted;
+ }
+ }
+ else if (valueAsArray != null)
+ {
+ // case 3: destination type is single element but source is array, so extract first element + convert
+ if (valueAsArray.Length > 0)
+ {
+ value = valueAsArray.GetValue(0);
+ return ConvertSimpleType(value, destinationType, culture);
+ }
+ else
+ {
+ // case 3(a): source is empty array, so can't perform conversion
+ return null;
+ }
+ }
+ // case 4: both destination + source type are single elements, so convert
+ return ConvertSimpleType(value, destinationType, culture);
+ }
+
+ private static object ConvertSimpleType(object value, Type destinationType, CultureInfo culture)
+ {
+ if (value == null || destinationType.IsInstanceOfType(value))
+ {
+ return value;
+ }
+
+ // if this is a user-input value but the user didn't type anything, return no value
+ string valueAsString = value as string;
+ if (valueAsString != null && valueAsString.Trim().Length == 0)
+ {
+ return null;
+ }
+
+ TypeConverter converter = TypeDescriptor.GetConverter(destinationType);
+ bool canConvertFrom = converter.CanConvertFrom(value.GetType());
+ if (!canConvertFrom)
+ {
+ converter = TypeDescriptor.GetConverter(value.GetType());
+ }
+ if (!(canConvertFrom || converter.CanConvertTo(destinationType)))
+ {
+ string message = String.Format(CultureInfo.CurrentCulture, WebPageResources.HtmlHelper_NoConverterExists,
+ value.GetType().FullName, destinationType.FullName);
+ throw new InvalidOperationException(message);
+ }
+
+ try
+ {
+ object convertedValue = (canConvertFrom)
+ ? converter.ConvertFrom(context: null, culture: culture, value: value)
+ : converter.ConvertTo(context: null, culture: culture, value: value, destinationType: destinationType);
+ return convertedValue;
+ }
+ catch (Exception ex)
+ {
+ string message = String.Format(CultureInfo.CurrentUICulture, WebPageResources.HtmlHelper_ConversionThrew,
+ value.GetType().FullName, destinationType.FullName);
+ throw new InvalidOperationException(message, ex);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Html/HtmlHelper.Label.cs b/src/System.Web.WebPages/Html/HtmlHelper.Label.cs
new file mode 100644
index 00000000..5d4048c9
--- /dev/null
+++ b/src/System.Web.WebPages/Html/HtmlHelper.Label.cs
@@ -0,0 +1,49 @@
+using System.Collections.Generic;
+using System.Web.Mvc;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.WebPages.Html
+{
+ public partial class HtmlHelper
+ {
+ public IHtmlString Label(string labelText)
+ {
+ return Label(labelText, null, (IDictionary<string, object>)null);
+ }
+
+ public IHtmlString Label(string labelText, string labelFor)
+ {
+ return Label(labelText, labelFor, (IDictionary<string, object>)null);
+ }
+
+ public IHtmlString Label(string labelText, object attributes)
+ {
+ return Label(labelText, null, TypeHelper.ObjectToDictionary(attributes));
+ }
+
+ public IHtmlString Label(string labelText, string labelFor, object attributes)
+ {
+ return Label(labelText, labelFor, TypeHelper.ObjectToDictionary(attributes));
+ }
+
+ public IHtmlString Label(string labelText, string labelFor, IDictionary<string, object> attributes)
+ {
+ if (String.IsNullOrEmpty(labelText))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "labelText");
+ }
+
+ labelFor = labelFor ?? labelText;
+
+ TagBuilder tag = new TagBuilder("label") { InnerHtml = Encode(labelText) };
+
+ if (!String.IsNullOrEmpty(labelFor))
+ {
+ tag.MergeAttribute("for", labelFor);
+ }
+ tag.MergeAttributes(attributes, false);
+
+ return tag.ToHtmlString(TagRenderMode.Normal);
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Html/HtmlHelper.Radio.cs b/src/System.Web.WebPages/Html/HtmlHelper.Radio.cs
new file mode 100644
index 00000000..317d80cc
--- /dev/null
+++ b/src/System.Web.WebPages/Html/HtmlHelper.Radio.cs
@@ -0,0 +1,93 @@
+using System.Collections.Generic;
+using System.Web.Mvc;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.WebPages.Html
+{
+ public partial class HtmlHelper
+ {
+ public IHtmlString RadioButton(string name, object value)
+ {
+ return RadioButton(name, value, htmlAttributes: (IDictionary<string, object>)null);
+ }
+
+ public IHtmlString RadioButton(string name, object value, object htmlAttributes)
+ {
+ return RadioButton(name, value, TypeHelper.ObjectToDictionary(htmlAttributes));
+ }
+
+ public IHtmlString RadioButton(string name, object value, IDictionary<string, object> htmlAttributes)
+ {
+ if (String.IsNullOrEmpty(name))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "name");
+ }
+
+ return BuildRadioButton(name, value, isChecked: null, attributes: htmlAttributes);
+ }
+
+ public IHtmlString RadioButton(string name, object value, bool isChecked)
+ {
+ return RadioButton(name, value, isChecked, htmlAttributes: (IDictionary<string, object>)null);
+ }
+
+ public IHtmlString RadioButton(string name, object value, bool isChecked, object htmlAttributes)
+ {
+ return RadioButton(name, value, isChecked, TypeHelper.ObjectToDictionary(htmlAttributes));
+ }
+
+ public IHtmlString RadioButton(string name, object value, bool isChecked, IDictionary<string, object> htmlAttributes)
+ {
+ if (name == null)
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "name");
+ }
+ return BuildRadioButton(name, value, isChecked, htmlAttributes);
+ }
+
+ private IHtmlString BuildRadioButton(string name, object value, bool? isChecked, IDictionary<string, object> attributes)
+ {
+ string valueString = ConvertTo(value, typeof(string)) as string;
+
+ TagBuilder builder = new TagBuilder("input");
+ builder.MergeAttribute("type", "radio", true);
+ builder.GenerateId(name);
+ builder.MergeAttributes(attributes, replaceExisting: true);
+
+ builder.MergeAttribute("value", valueString, replaceExisting: true);
+ builder.MergeAttribute("name", name, replaceExisting: true);
+
+ if (UnobtrusiveJavaScriptEnabled)
+ {
+ // Add validation attributes
+ var validationAttributes = _validationHelper.GetUnobtrusiveValidationAttributes(name);
+ builder.MergeAttributes(validationAttributes, replaceExisting: false);
+ }
+
+ var modelState = ModelState[name];
+ string modelValue = null;
+ if (modelState != null)
+ {
+ modelValue = ConvertTo(modelState.Value, typeof(string)) as string;
+ isChecked = isChecked ?? String.Equals(modelValue, valueString, StringComparison.OrdinalIgnoreCase);
+ }
+
+ if (isChecked.HasValue)
+ {
+ // Overrides attribute values
+ if (isChecked.Value)
+ {
+ builder.MergeAttribute("checked", "checked", true);
+ }
+ else
+ {
+ builder.Attributes.Remove("checked");
+ }
+ }
+
+ AddErrorClass(builder, name);
+
+ return builder.ToHtmlString(TagRenderMode.SelfClosing);
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Html/HtmlHelper.Select.cs b/src/System.Web.WebPages/Html/HtmlHelper.Select.cs
new file mode 100644
index 00000000..f31b826b
--- /dev/null
+++ b/src/System.Web.WebPages/Html/HtmlHelper.Select.cs
@@ -0,0 +1,288 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Web.Mvc;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.WebPages.Html
+{
+ public partial class HtmlHelper
+ {
+ public IHtmlString ListBox(string name, IEnumerable<SelectListItem> selectList)
+ {
+ return ListBox(name, defaultOption: null, selectList: selectList, htmlAttributes: (IDictionary<string, object>)null);
+ }
+
+ public IHtmlString ListBox(string name, string defaultOption, IEnumerable<SelectListItem> selectList)
+ {
+ return ListBox(name, defaultOption: defaultOption, selectList: selectList, selectedValues: null,
+ htmlAttributes: (IDictionary<string, object>)null);
+ }
+
+ public IHtmlString ListBox(string name, IEnumerable<SelectListItem> selectList, object htmlAttributes)
+ {
+ return ListBox(name, defaultOption: null, selectList: selectList, selectedValues: null, htmlAttributes: htmlAttributes);
+ }
+
+ public IHtmlString ListBox(string name, IEnumerable<SelectListItem> selectList, IDictionary<string, object> htmlAttributes)
+ {
+ return ListBox(name, defaultOption: null, selectList: selectList, selectedValues: null, htmlAttributes: htmlAttributes);
+ }
+
+ public IHtmlString ListBox(string name, string defaultOption, IEnumerable<SelectListItem> selectList,
+ IDictionary<string, object> htmlAttributes)
+ {
+ return ListBox(name, defaultOption, selectList, selectedValues: null, htmlAttributes: htmlAttributes);
+ }
+
+ public IHtmlString ListBox(string name, string defaultOption, IEnumerable<SelectListItem> selectList, object htmlAttributes)
+ {
+ return ListBox(name, defaultOption: defaultOption, selectList: selectList, selectedValues: null, htmlAttributes: htmlAttributes);
+ }
+
+ public IHtmlString ListBox(string name, string defaultOption, IEnumerable<SelectListItem> selectList, object selectedValues,
+ IDictionary<string, object> htmlAttributes)
+ {
+ if (String.IsNullOrEmpty(name))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "name");
+ }
+ return BuildListBox(name, defaultOption: defaultOption, selectList: selectList,
+ selectedValues: selectedValues, size: null, allowMultiple: false, htmlAttributes: htmlAttributes);
+ }
+
+ public IHtmlString ListBox(string name, string defaultOption, IEnumerable<SelectListItem> selectList, object selectedValues,
+ object htmlAttributes)
+ {
+ return ListBox(name, defaultOption: defaultOption, selectList: selectList,
+ selectedValues: selectedValues, htmlAttributes: TypeHelper.ObjectToDictionary(htmlAttributes));
+ }
+
+ public IHtmlString ListBox(string name, IEnumerable<SelectListItem> selectList,
+ object selectedValues, int size, bool allowMultiple)
+ {
+ return ListBox(name, defaultOption: null, selectList: selectList, selectedValues: selectedValues, size: size,
+ allowMultiple: allowMultiple, htmlAttributes: (IDictionary<string, object>)null);
+ }
+
+ public IHtmlString ListBox(string name, string defaultOption, IEnumerable<SelectListItem> selectList,
+ object selectedValues, int size, bool allowMultiple)
+ {
+ return ListBox(name, defaultOption: defaultOption, selectList: selectList, selectedValues: selectedValues,
+ size: size, allowMultiple: allowMultiple, htmlAttributes: (IDictionary<string, object>)null);
+ }
+
+ public IHtmlString ListBox(string name, string defaultOption, IEnumerable<SelectListItem> selectList,
+ object selectedValues, int size, bool allowMultiple, IDictionary<string, object> htmlAttributes)
+ {
+ if (String.IsNullOrEmpty(name))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "name");
+ }
+
+ return BuildListBox(name, defaultOption, selectList, selectedValues, size, allowMultiple, htmlAttributes);
+ }
+
+ public IHtmlString ListBox(string name, string defaultOption, IEnumerable<SelectListItem> selectList,
+ object selectedValues, int size, bool allowMultiple, object htmlAttributes)
+ {
+ return ListBox(name, defaultOption, selectList, selectedValues, size, allowMultiple, TypeHelper.ObjectToDictionary(htmlAttributes));
+ }
+
+ private IHtmlString BuildListBox(string name, string defaultOption, IEnumerable<SelectListItem> selectList,
+ object selectedValues, int? size, bool allowMultiple, IDictionary<string, object> htmlAttributes)
+ {
+ var modelState = ModelState[name];
+ if (modelState != null)
+ {
+ selectedValues = selectedValues ?? ModelState[name].Value;
+ }
+
+ if (selectedValues != null)
+ {
+ IEnumerable values = (allowMultiple) ? ConvertTo(selectedValues, typeof(string[])) as string[]
+ : new[] { ConvertTo(selectedValues, typeof(string)) };
+
+ HashSet<string> selectedValueSet = new HashSet<string>(from object value in values
+ select Convert.ToString(value, CultureInfo.CurrentCulture),
+ StringComparer.OrdinalIgnoreCase);
+ List<SelectListItem> newSelectList = new List<SelectListItem>();
+
+ bool previousSelected = false;
+ foreach (SelectListItem item in selectList)
+ {
+ bool selected = false;
+ // If the user's specified allowed multiple to be false
+ // only pick up the first item that was selected.
+ if (allowMultiple || !previousSelected)
+ {
+ selected = item.Selected || selectedValueSet.Contains(item.Value ?? item.Text);
+ }
+ previousSelected = previousSelected | selected;
+
+ newSelectList.Add(new SelectListItem(item) { Selected = selected });
+ }
+ selectList = newSelectList;
+ }
+
+ TagBuilder tagBuilder = new TagBuilder("select")
+ {
+ InnerHtml = BuildListOptions(selectList, defaultOption)
+ };
+
+ if (UnobtrusiveJavaScriptEnabled)
+ {
+ // Add validation attributes
+ var validationAttributes = _validationHelper.GetUnobtrusiveValidationAttributes(name);
+ tagBuilder.MergeAttributes(validationAttributes, replaceExisting: false);
+ }
+
+ tagBuilder.GenerateId(name);
+ tagBuilder.MergeAttributes(htmlAttributes);
+
+ tagBuilder.MergeAttribute("name", name, replaceExisting: true);
+ if (size.HasValue)
+ {
+ tagBuilder.MergeAttribute("size", size.ToString(), true);
+ }
+ if (allowMultiple)
+ {
+ tagBuilder.MergeAttribute("multiple", "multiple");
+ }
+ else if (tagBuilder.Attributes.ContainsKey("multiple"))
+ {
+ tagBuilder.Attributes.Remove("multiple");
+ }
+
+ // If there are any errors for a named field, we add the css attribute.
+ AddErrorClass(tagBuilder, name);
+
+ return tagBuilder.ToHtmlString(TagRenderMode.Normal);
+ }
+
+ public IHtmlString DropDownList(string name, IEnumerable<SelectListItem> selectList)
+ {
+ return DropDownList(name, defaultOption: null, selectList: selectList, htmlAttributes: (IDictionary<string, object>)null);
+ }
+
+ public IHtmlString DropDownList(string name, IEnumerable<SelectListItem> selectList, object htmlAttributes)
+ {
+ return DropDownList(name, defaultOption: null, selectList: selectList, selectedValue: null, htmlAttributes: htmlAttributes);
+ }
+
+ public IHtmlString DropDownList(string name, IEnumerable<SelectListItem> selectList, IDictionary<string, object> htmlAttributes)
+ {
+ return DropDownList(name, defaultOption: null, selectList: selectList, selectedValue: null, htmlAttributes: htmlAttributes);
+ }
+
+ public IHtmlString DropDownList(string name, string defaultOption, IEnumerable<SelectListItem> selectList)
+ {
+ return DropDownList(name, defaultOption, selectList, selectedValue: null, htmlAttributes: (IDictionary<string, object>)null);
+ }
+
+ public IHtmlString DropDownList(string name, string defaultOption, IEnumerable<SelectListItem> selectList,
+ IDictionary<string, object> htmlAttributes)
+ {
+ return DropDownList(name, defaultOption, selectList, selectedValue: null, htmlAttributes: htmlAttributes);
+ }
+
+ public IHtmlString DropDownList(string name, string defaultOption, IEnumerable<SelectListItem> selectList, object htmlAttributes)
+ {
+ return DropDownList(name, defaultOption: defaultOption, selectList: selectList, selectedValue: null, htmlAttributes: htmlAttributes);
+ }
+
+ public IHtmlString DropDownList(string name, string defaultOption, IEnumerable<SelectListItem> selectList, object selectedValue,
+ IDictionary<string, object> htmlAttributes)
+ {
+ if (String.IsNullOrEmpty(name))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "name");
+ }
+ return BuildDropDownList(name, defaultOption, selectList, selectedValue, htmlAttributes: htmlAttributes);
+ }
+
+ public IHtmlString DropDownList(string name, string defaultOption, IEnumerable<SelectListItem> selectList, object selectedValue,
+ object htmlAttributes)
+ {
+ return DropDownList(name, defaultOption, selectList, selectedValue, TypeHelper.ObjectToDictionary(htmlAttributes));
+ }
+
+ private IHtmlString BuildDropDownList(string name, string defaultOption, IEnumerable<SelectListItem> selectList,
+ object selectedValue, IDictionary<string, object> htmlAttributes)
+ {
+ var modelState = ModelState[name];
+ if (modelState != null)
+ {
+ selectedValue = selectedValue ?? ModelState[name].Value;
+ }
+ selectedValue = ConvertTo(selectedValue, typeof(string));
+
+ if (selectedValue != null)
+ {
+ var newSelectList = new List<SelectListItem>(from item in selectList
+ select new SelectListItem(item));
+ var comparer = StringComparer.InvariantCultureIgnoreCase;
+ var selectedItem = newSelectList.FirstOrDefault(item => item.Selected || comparer.Equals(item.Value ?? item.Text, selectedValue));
+ if (selectedItem != default(SelectListItem))
+ {
+ selectedItem.Selected = true;
+ selectList = newSelectList;
+ }
+ }
+
+ TagBuilder tagBuilder = new TagBuilder("select")
+ {
+ InnerHtml = BuildListOptions(selectList, defaultOption)
+ };
+ tagBuilder.MergeAttributes(htmlAttributes);
+ tagBuilder.MergeAttribute("name", name, replaceExisting: true);
+ tagBuilder.GenerateId(name);
+ if (UnobtrusiveJavaScriptEnabled)
+ {
+ var validationAttributes = _validationHelper.GetUnobtrusiveValidationAttributes(name);
+ tagBuilder.MergeAttributes(validationAttributes, replaceExisting: false);
+ }
+
+ // If there are any errors for a named field, we add the css attribute.
+ AddErrorClass(tagBuilder, name);
+
+ return tagBuilder.ToHtmlString(TagRenderMode.Normal);
+ }
+
+ private static string BuildListOptions(IEnumerable<SelectListItem> selectList, string optionText)
+ {
+ StringBuilder builder = new StringBuilder().AppendLine();
+ if (optionText != null)
+ {
+ builder.AppendLine(ListItemToOption(new SelectListItem { Text = optionText, Value = String.Empty }));
+ }
+ if (selectList != null)
+ {
+ foreach (var item in selectList)
+ {
+ builder.AppendLine(ListItemToOption(item));
+ }
+ }
+ return builder.ToString();
+ }
+
+ private static string ListItemToOption(SelectListItem item)
+ {
+ TagBuilder builder = new TagBuilder("option")
+ {
+ InnerHtml = HttpUtility.HtmlEncode(item.Text)
+ };
+ if (item.Value != null)
+ {
+ builder.Attributes["value"] = item.Value;
+ }
+ if (item.Selected)
+ {
+ builder.Attributes["selected"] = "selected";
+ }
+ return builder.ToString(TagRenderMode.Normal);
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Html/HtmlHelper.TextArea.cs b/src/System.Web.WebPages/Html/HtmlHelper.TextArea.cs
new file mode 100644
index 00000000..f5ac7e90
--- /dev/null
+++ b/src/System.Web.WebPages/Html/HtmlHelper.TextArea.cs
@@ -0,0 +1,119 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.Web.Mvc;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.WebPages.Html
+{
+ public partial class HtmlHelper
+ {
+ // Values from mvc
+ private const int TextAreaRows = 2;
+ private const int TextAreaColumns = 20;
+
+ private static readonly IDictionary<string, object> _implicitRowsAndColumns = new Dictionary<string, object>
+ {
+ { "rows", TextAreaRows.ToString(CultureInfo.InvariantCulture) },
+ { "cols", TextAreaColumns.ToString(CultureInfo.InvariantCulture) },
+ };
+
+ private static IDictionary<string, object> GetRowsAndColumnsDictionary(int rows, int columns)
+ {
+ Dictionary<string, object> result = new Dictionary<string, object>();
+ if (rows > 0)
+ {
+ result.Add("rows", rows.ToString(CultureInfo.InvariantCulture));
+ }
+ if (columns > 0)
+ {
+ result.Add("cols", columns.ToString(CultureInfo.InvariantCulture));
+ }
+ return result;
+ }
+
+ public IHtmlString TextArea(string name)
+ {
+ return TextArea(name, value: null, htmlAttributes: (IDictionary<string, object>)null);
+ }
+
+ public IHtmlString TextArea(string name, object htmlAttributes)
+ {
+ return TextArea(name, value: null, htmlAttributes: TypeHelper.ObjectToDictionary(htmlAttributes));
+ }
+
+ public IHtmlString TextArea(string name, IDictionary<string, object> htmlAttributes)
+ {
+ return TextArea(name, value: null, htmlAttributes: htmlAttributes);
+ }
+
+ public IHtmlString TextArea(string name, string value)
+ {
+ return TextArea(name, value, (IDictionary<string, object>)null);
+ }
+
+ public IHtmlString TextArea(string name, string value, object htmlAttributes)
+ {
+ return TextArea(name, value, TypeHelper.ObjectToDictionary(htmlAttributes));
+ }
+
+ public IHtmlString TextArea(string name, string value, IDictionary<string, object> htmlAttributes)
+ {
+ if (String.IsNullOrEmpty(name))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "name");
+ }
+
+ return BuildTextArea(name, value, _implicitRowsAndColumns, htmlAttributes);
+ }
+
+ public IHtmlString TextArea(string name, string value, int rows, int columns,
+ object htmlAttributes)
+ {
+ return TextArea(name, value, rows, columns, TypeHelper.ObjectToDictionary(htmlAttributes));
+ }
+
+ public IHtmlString TextArea(string name, string value, int rows, int columns,
+ IDictionary<string, object> htmlAttributes)
+ {
+ if (String.IsNullOrEmpty(name))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "name");
+ }
+ return BuildTextArea(name, value, GetRowsAndColumnsDictionary(rows, columns), htmlAttributes);
+ }
+
+ private IHtmlString BuildTextArea(string name, string value, IDictionary<string, object> rowsAndColumnsDictionary,
+ IDictionary<string, object> htmlAttributes)
+ {
+ TagBuilder tagBuilder = new TagBuilder("textarea");
+
+ if (UnobtrusiveJavaScriptEnabled)
+ {
+ // Add validation attributes
+ var validationAttributes = _validationHelper.GetUnobtrusiveValidationAttributes(name);
+ tagBuilder.MergeAttributes(validationAttributes, replaceExisting: false);
+ }
+
+ // Add user specified htmlAttributes
+ tagBuilder.MergeAttributes(htmlAttributes);
+
+ tagBuilder.MergeAttributes(rowsAndColumnsDictionary, rowsAndColumnsDictionary != _implicitRowsAndColumns);
+
+ // Value becomes the inner html of the textarea element
+ var modelState = ModelState[name];
+ if (modelState != null)
+ {
+ value = value ?? Convert.ToString(ModelState[name].Value, CultureInfo.CurrentCulture);
+ }
+ tagBuilder.InnerHtml = Encode(value);
+
+ //Assign name and id
+ tagBuilder.MergeAttribute("name", name);
+ tagBuilder.GenerateId(name);
+
+ AddErrorClass(tagBuilder, name);
+
+ return tagBuilder.ToHtmlString(TagRenderMode.Normal);
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Html/HtmlHelper.Validation.cs b/src/System.Web.WebPages/Html/HtmlHelper.Validation.cs
new file mode 100644
index 00000000..57146002
--- /dev/null
+++ b/src/System.Web.WebPages/Html/HtmlHelper.Validation.cs
@@ -0,0 +1,182 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Text;
+using System.Web.Mvc;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.WebPages.Html
+{
+ public partial class HtmlHelper
+ {
+ public IHtmlString ValidationMessage(string name)
+ {
+ return ValidationMessage(name, null, null);
+ }
+
+ public IHtmlString ValidationMessage(string name, string message)
+ {
+ return ValidationMessage(name, message, (IDictionary<string, object>)null);
+ }
+
+ public IHtmlString ValidationMessage(string name, object htmlAttributes)
+ {
+ return ValidationMessage(name, null, TypeHelper.ObjectToDictionary(htmlAttributes));
+ }
+
+ public IHtmlString ValidationMessage(string name, IDictionary<string, object> htmlAttributes)
+ {
+ return ValidationMessage(name, null, htmlAttributes);
+ }
+
+ public IHtmlString ValidationMessage(string name, string message, object htmlAttributes)
+ {
+ return ValidationMessage(name, message, TypeHelper.ObjectToDictionary(htmlAttributes));
+ }
+
+ public IHtmlString ValidationMessage(string name, string message, IDictionary<string, object> htmlAttributes)
+ {
+ if (String.IsNullOrEmpty(name))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "name");
+ }
+ return BuildValidationMessage(name, message, htmlAttributes);
+ }
+
+ [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase",
+ Justification = "Normalization to lowercase is a common requirement for JavaScript and HTML values")]
+ private IHtmlString BuildValidationMessage(string name, string message, IDictionary<string, object> htmlAttributes)
+ {
+ var modelState = ModelState[name];
+ IEnumerable<string> errors = null;
+ if (modelState != null)
+ {
+ errors = modelState.Errors;
+ }
+ bool hasError = errors != null && errors.Any();
+ if (!hasError && !UnobtrusiveJavaScriptEnabled)
+ {
+ // If unobtrusive validation is enabled, we need to generate an empty span with the "val-for" attribute"
+ return null;
+ }
+ else
+ {
+ string error = null;
+ if (hasError)
+ {
+ error = message ?? errors.First();
+ }
+
+ TagBuilder tagBuilder = new TagBuilder("span") { InnerHtml = Encode(error) };
+ tagBuilder.MergeAttributes(htmlAttributes);
+ if (UnobtrusiveJavaScriptEnabled)
+ {
+ bool replaceValidationMessageContents = String.IsNullOrEmpty(message);
+ tagBuilder.MergeAttribute("data-valmsg-for", name);
+ tagBuilder.MergeAttribute("data-valmsg-replace", replaceValidationMessageContents.ToString().ToLowerInvariant());
+ }
+ tagBuilder.AddCssClass(hasError ? ValidationMessageCssClassName : ValidationMessageValidCssClassName);
+ return tagBuilder.ToHtmlString(TagRenderMode.Normal);
+ }
+ }
+
+ public IHtmlString ValidationSummary()
+ {
+ return BuildValidationSummary(message: null, excludeFieldErrors: false, htmlAttributes: (IDictionary<string, object>)null);
+ }
+
+ public IHtmlString ValidationSummary(string message)
+ {
+ return BuildValidationSummary(message: message, excludeFieldErrors: false, htmlAttributes: (IDictionary<string, object>)null);
+ }
+
+ public IHtmlString ValidationSummary(bool excludeFieldErrors)
+ {
+ return ValidationSummary(message: null, excludeFieldErrors: excludeFieldErrors, htmlAttributes: (IDictionary<string, object>)null);
+ }
+
+ public IHtmlString ValidationSummary(object htmlAttributes)
+ {
+ return ValidationSummary(message: null, excludeFieldErrors: false, htmlAttributes: htmlAttributes);
+ }
+
+ public IHtmlString ValidationSummary(IDictionary<string, object> htmlAttributes)
+ {
+ return ValidationSummary(message: null, excludeFieldErrors: false, htmlAttributes: htmlAttributes);
+ }
+
+ public IHtmlString ValidationSummary(string message, object htmlAttributes)
+ {
+ return ValidationSummary(message, excludeFieldErrors: false, htmlAttributes: htmlAttributes);
+ }
+
+ public IHtmlString ValidationSummary(string message, IDictionary<string, object> htmlAttributes)
+ {
+ return ValidationSummary(message, excludeFieldErrors: false, htmlAttributes: htmlAttributes);
+ }
+
+ public IHtmlString ValidationSummary(string message, bool excludeFieldErrors, object htmlAttributes)
+ {
+ return ValidationSummary(message, excludeFieldErrors, TypeHelper.ObjectToDictionary(htmlAttributes));
+ }
+
+ public IHtmlString ValidationSummary(string message, bool excludeFieldErrors, IDictionary<string, object> htmlAttributes)
+ {
+ return BuildValidationSummary(message, excludeFieldErrors, htmlAttributes);
+ }
+
+ private IHtmlString BuildValidationSummary(string message, bool excludeFieldErrors, IDictionary<string, object> htmlAttributes)
+ {
+ IEnumerable<string> errors = null;
+ if (excludeFieldErrors)
+ {
+ // Review: Is there a better way to share the form field name between this and ModelStateDictionary?
+ var formModelState = ModelState[ModelStateDictionary.FormFieldKey];
+ if (formModelState != null)
+ {
+ errors = formModelState.Errors;
+ }
+ }
+ else
+ {
+ errors = ModelState.SelectMany(c => c.Value.Errors);
+ }
+
+ bool hasErrors = errors != null && errors.Any();
+ if (!hasErrors && (!UnobtrusiveJavaScriptEnabled || excludeFieldErrors))
+ {
+ // If no errors are found and we do not have unobtrusive validation enabled or if the summary is not meant to display field errors, don't generate the summary.
+ return null;
+ }
+ else
+ {
+ TagBuilder tagBuilder = new TagBuilder("div");
+ tagBuilder.MergeAttributes(htmlAttributes);
+ tagBuilder.AddCssClass(hasErrors ? ValidationSummaryClass : ValidationSummaryValidClass);
+ if (UnobtrusiveJavaScriptEnabled && !excludeFieldErrors)
+ {
+ tagBuilder.MergeAttribute("data-valmsg-summary", "true");
+ }
+
+ StringBuilder builder = new StringBuilder();
+ if (message != null)
+ {
+ builder.Append("<span>");
+ builder.Append(Encode(message));
+ builder.AppendLine("</span>");
+ }
+ builder.AppendLine("<ul>");
+ foreach (var error in errors)
+ {
+ builder.Append("<li>");
+ builder.Append(Encode(error));
+ builder.AppendLine("</li>");
+ }
+ builder.Append("</ul>");
+
+ tagBuilder.InnerHtml = builder.ToString();
+ return tagBuilder.ToHtmlString(TagRenderMode.Normal);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Html/HtmlHelper.cs b/src/System.Web.WebPages/Html/HtmlHelper.cs
new file mode 100644
index 00000000..0eea4936
--- /dev/null
+++ b/src/System.Web.WebPages/Html/HtmlHelper.cs
@@ -0,0 +1,200 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Web.WebPages.Scope;
+
+namespace System.Web.WebPages.Html
+{
+ public partial class HtmlHelper
+ {
+ internal const string DefaultValidationInputErrorCssClass = "input-validation-error";
+ private const string DefaultValidationInputValidCssClass = "input-validation-valid";
+ private const string DefaultValidationMessageErrorCssClass = "field-validation-error";
+ private const string DefaultValidationMessageValidCssClass = "field-validation-valid";
+ private const string DefaultValidationSummaryErrorCssClass = "validation-summary-errors";
+ private const string DefaultValidationSummaryValidCssClassName = "validation-summary-valid";
+ private static readonly object _validationMesssageErrorClassKey = new object();
+ private static readonly object _validationMessageValidClassKey = new object();
+ private static readonly object _validationInputErrorClassKey = new object();
+ private static readonly object _validationInputValidClassKey = new object();
+ private static readonly object _validationSummaryClassKey = new object();
+ private static readonly object _validationSummaryValidClassKey = new object();
+ private static readonly object _unobtrusiveValidationKey = new object();
+ private static string _idAttributeDotReplacement;
+ private readonly ValidationHelper _validationHelper;
+
+ internal HtmlHelper(ModelStateDictionary modelState, ValidationHelper validationHelper)
+ {
+ ModelState = modelState;
+ _validationHelper = validationHelper;
+ }
+
+ // This property got copied from MVC's HtmlHelper along with TagBuilder.
+ // It was a global property in MVC so it should not have scoped semantics here either.
+ public static string IdAttributeDotReplacement
+ {
+ get
+ {
+ if (String.IsNullOrEmpty(_idAttributeDotReplacement))
+ {
+ _idAttributeDotReplacement = "_";
+ }
+ return _idAttributeDotReplacement;
+ }
+ set { _idAttributeDotReplacement = value; }
+ }
+
+ public static string ValidationInputValidCssClassName
+ {
+ get { return ScopeStorage.CurrentScope[_validationInputValidClassKey] as string ?? DefaultValidationInputValidCssClass; }
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException("value");
+ }
+ ScopeStorage.CurrentScope[_validationInputValidClassKey] = value;
+ }
+ }
+
+ public static string ValidationInputCssClassName
+ {
+ get { return ScopeStorage.CurrentScope[_validationInputErrorClassKey] as string ?? DefaultValidationInputErrorCssClass; }
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException("value");
+ }
+ ScopeStorage.CurrentScope[_validationInputErrorClassKey] = value;
+ }
+ }
+
+ public static string ValidationMessageValidCssClassName
+ {
+ get { return ScopeStorage.CurrentScope[_validationMessageValidClassKey] as string ?? DefaultValidationMessageValidCssClass; }
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException("value");
+ }
+ ScopeStorage.CurrentScope[_validationMessageValidClassKey] = value;
+ }
+ }
+
+ public static string ValidationMessageCssClassName
+ {
+ get { return ScopeStorage.CurrentScope[_validationMesssageErrorClassKey] as string ?? DefaultValidationMessageErrorCssClass; }
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException("value");
+ }
+ ScopeStorage.CurrentScope[_validationMesssageErrorClassKey] = value;
+ }
+ }
+
+ public static string ValidationSummaryClass
+ {
+ get { return ScopeStorage.CurrentScope[_validationSummaryClassKey] as string ?? DefaultValidationSummaryErrorCssClass; }
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException("value");
+ }
+ ScopeStorage.CurrentScope[_validationSummaryClassKey] = value;
+ }
+ }
+
+ public static string ValidationSummaryValidClass
+ {
+ get { return ScopeStorage.CurrentScope[_validationSummaryValidClassKey] as string ?? DefaultValidationSummaryValidCssClassName; }
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException("value");
+ }
+ ScopeStorage.CurrentScope[_validationSummaryValidClassKey] = value;
+ }
+ }
+
+ public static bool UnobtrusiveJavaScriptEnabled
+ {
+ get
+ {
+ bool? value = (bool?)ScopeStorage.CurrentScope[_unobtrusiveValidationKey];
+ return value ?? true;
+ }
+ set { ScopeStorage.CurrentScope[_unobtrusiveValidationKey] = value; }
+ }
+
+ private ModelStateDictionary ModelState { get; set; }
+
+ public string AttributeEncode(object value)
+ {
+ return AttributeEncode(Convert.ToString(value, CultureInfo.InvariantCulture));
+ }
+
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic",
+ Justification = "For consistency, all helpers are instance methods.")]
+ public string AttributeEncode(string value)
+ {
+ if (String.IsNullOrEmpty(value))
+ {
+ return String.Empty;
+ }
+ else
+ {
+ return HttpUtility.HtmlAttributeEncode(value);
+ }
+ }
+
+ public string Encode(object value)
+ {
+ return Encode(Convert.ToString(value, CultureInfo.InvariantCulture));
+ }
+
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic",
+ Justification = "For consistency, all helpers are instance methods.")]
+ public string Encode(string value)
+ {
+ if (String.IsNullOrEmpty(value))
+ {
+ return String.Empty;
+ }
+ else
+ {
+ return HttpUtility.HtmlEncode(value);
+ }
+ }
+
+ /// <summary>
+ /// Wraps HTML markup in an IHtmlString, which will enable HTML markup to be
+ /// rendered to the output without getting HTML encoded.
+ /// </summary>
+ /// <param name="value">HTML markup string.</param>
+ /// <returns>An IHtmlString that represents HTML markup.</returns>
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic",
+ Justification = "For consistency, all helpers are instance methods.")]
+ public IHtmlString Raw(string value)
+ {
+ return new HtmlString(value);
+ }
+
+ /// <summary>
+ /// Wraps HTML markup from the string representation of an object in an IHtmlString,
+ /// which will enable HTML markup to be rendered to the output without getting HTML encoded.
+ /// </summary>
+ /// <param name="value">object with string representation as HTML markup</param>
+ /// <returns>An IHtmlString that represents HTML markup.</returns>
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic",
+ Justification = "For consistency, all helpers are instance methods.")]
+ public IHtmlString Raw(object value)
+ {
+ return new HtmlString(value == null ? null : value.ToString());
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Html/ModelState.cs b/src/System.Web.WebPages/Html/ModelState.cs
new file mode 100644
index 00000000..95e28e09
--- /dev/null
+++ b/src/System.Web.WebPages/Html/ModelState.cs
@@ -0,0 +1,16 @@
+using System.Collections.Generic;
+
+namespace System.Web.WebPages.Html
+{
+ public class ModelState
+ {
+ private List<string> _errors = new List<string>();
+
+ public IList<string> Errors
+ {
+ get { return _errors; }
+ }
+
+ public object Value { get; set; }
+ }
+}
diff --git a/src/System.Web.WebPages/Html/ModelStateDictionary.cs b/src/System.Web.WebPages/Html/ModelStateDictionary.cs
new file mode 100644
index 00000000..9501d7ad
--- /dev/null
+++ b/src/System.Web.WebPages/Html/ModelStateDictionary.cs
@@ -0,0 +1,183 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace System.Web.WebPages.Html
+{
+ // Most of the code for this class is copied from MVC 2.0
+ public class ModelStateDictionary : IDictionary<string, ModelState>
+ {
+ internal const string FormFieldKey = "_FORM";
+ private readonly Dictionary<string, ModelState> _innerDictionary = new Dictionary<string, ModelState>(StringComparer.OrdinalIgnoreCase);
+
+ public ModelStateDictionary()
+ {
+ }
+
+ public ModelStateDictionary(ModelStateDictionary dictionary)
+ {
+ if (dictionary == null)
+ {
+ throw new ArgumentNullException("dictionary");
+ }
+
+ foreach (var entry in dictionary)
+ {
+ _innerDictionary.Add(entry.Key, entry.Value);
+ }
+ }
+
+ public int Count
+ {
+ get { return _innerDictionary.Count; }
+ }
+
+ public bool IsReadOnly
+ {
+ get { return ((IDictionary<string, ModelState>)_innerDictionary).IsReadOnly; }
+ }
+
+ public bool IsValid
+ {
+ get { return !Values.SelectMany(modelState => modelState.Errors).Any(); }
+ }
+
+ public ICollection<string> Keys
+ {
+ get { return _innerDictionary.Keys; }
+ }
+
+ public ICollection<ModelState> Values
+ {
+ get { return _innerDictionary.Values; }
+ }
+
+ public ModelState this[string key]
+ {
+ get
+ {
+ ModelState value;
+ _innerDictionary.TryGetValue(key, out value);
+ return value;
+ }
+ set { _innerDictionary[key] = value; }
+ }
+
+ public void Add(KeyValuePair<string, ModelState> item)
+ {
+ ((IDictionary<string, ModelState>)_innerDictionary).Add(item);
+ }
+
+ public void Add(string key, ModelState value)
+ {
+ _innerDictionary.Add(key, value);
+ }
+
+ public void AddError(string key, string errorMessage)
+ {
+ GetModelStateForKey(key).Errors.Add(errorMessage);
+ }
+
+ public void AddFormError(string errorMessage)
+ {
+ GetModelStateForKey(FormFieldKey).Errors.Add(errorMessage);
+ }
+
+ public void Clear()
+ {
+ _innerDictionary.Clear();
+ }
+
+ public bool Contains(KeyValuePair<string, ModelState> item)
+ {
+ return ((IDictionary<string, ModelState>)_innerDictionary).Contains(item);
+ }
+
+ public bool ContainsKey(string key)
+ {
+ return _innerDictionary.ContainsKey(key);
+ }
+
+ public void CopyTo(KeyValuePair<string, ModelState>[] array, int arrayIndex)
+ {
+ ((IDictionary<string, ModelState>)_innerDictionary).CopyTo(array, arrayIndex);
+ }
+
+ public IEnumerator<KeyValuePair<string, ModelState>> GetEnumerator()
+ {
+ return _innerDictionary.GetEnumerator();
+ }
+
+ private ModelState GetModelStateForKey(string key)
+ {
+ if (key == null)
+ {
+ throw new ArgumentNullException("key");
+ }
+
+ ModelState modelState;
+ if (!TryGetValue(key, out modelState))
+ {
+ modelState = new ModelState();
+ _innerDictionary[key] = modelState;
+ }
+
+ return modelState;
+ }
+
+ public bool IsValidField(string key)
+ {
+ if (key == null)
+ {
+ throw new ArgumentNullException("key");
+ }
+
+ // if the key is not found in the dictionary, we just say that it's valid (since there are no errors)
+ ModelState modelState = this[key];
+ return (modelState == null) || !modelState.Errors.Any();
+ }
+
+ public void Merge(ModelStateDictionary dictionary)
+ {
+ if (dictionary == null)
+ {
+ return;
+ }
+
+ foreach (var entry in dictionary)
+ {
+ this[entry.Key] = entry.Value;
+ }
+ }
+
+ public bool Remove(KeyValuePair<string, ModelState> item)
+ {
+ return ((IDictionary<string, ModelState>)_innerDictionary).Remove(item);
+ }
+
+ public bool Remove(string key)
+ {
+ return _innerDictionary.Remove(key);
+ }
+
+ public bool TryGetValue(string key, out ModelState value)
+ {
+ return _innerDictionary.TryGetValue(key, out value);
+ }
+
+ public void SetModelValue(string key, object value)
+ {
+ ModelState state = GetModelStateForKey(key);
+ state.Value = value;
+ }
+
+ #region IEnumerable Members
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return ((IEnumerable)_innerDictionary).GetEnumerator();
+ }
+
+ #endregion
+ }
+}
diff --git a/src/System.Web.WebPages/Html/SelectListItem.cs b/src/System.Web.WebPages/Html/SelectListItem.cs
new file mode 100644
index 00000000..c73ec6e6
--- /dev/null
+++ b/src/System.Web.WebPages/Html/SelectListItem.cs
@@ -0,0 +1,22 @@
+namespace System.Web.WebPages.Html
+{
+ public class SelectListItem
+ {
+ public SelectListItem()
+ {
+ }
+
+ public SelectListItem(SelectListItem item)
+ {
+ Text = item.Text;
+ Value = item.Value;
+ Selected = item.Selected;
+ }
+
+ public string Text { get; set; }
+
+ public string Value { get; set; }
+
+ public bool Selected { get; set; }
+ }
+}
diff --git a/src/System.Web.WebPages/HttpContextExtensions.cs b/src/System.Web.WebPages/HttpContextExtensions.cs
new file mode 100644
index 00000000..91550e4f
--- /dev/null
+++ b/src/System.Web.WebPages/HttpContextExtensions.cs
@@ -0,0 +1,29 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Web.WebPages
+{
+ public static class HttpContextExtensions
+ {
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "Response.Redirect() takes its URI as a string parameter.")]
+ public static void RedirectLocal(this HttpContextBase context, string url)
+ {
+ if (context.Request.IsUrlLocalToHost(url))
+ {
+ context.Response.Redirect(url);
+ }
+ else
+ {
+ context.Response.Redirect("~/");
+ }
+ }
+
+ public static void RegisterForDispose(this HttpContextBase context, IDisposable resource)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException("context");
+ }
+ RequestResourceTracker.RegisterForDispose(context, resource);
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/IDisplayMode.cs b/src/System.Web.WebPages/IDisplayMode.cs
new file mode 100644
index 00000000..e891b013
--- /dev/null
+++ b/src/System.Web.WebPages/IDisplayMode.cs
@@ -0,0 +1,16 @@
+namespace System.Web.WebPages
+{
+ /// <summary>
+ /// An interface that provides DisplayInfo for a virtual path and request. An IDisplayMode may modify the virtual path before checking
+ /// if it exists. CanHandleContext is called to determine if the Display Mode is available to return display info for the request.
+ /// GetDisplayInfo should return null if the virtual path does not exist. For an example implementation, see DefaultDisplayMode.
+ /// DisplayModeId is used to cache the non-null result of a call to GetDisplayInfo and should be unique for each Display Mode. See
+ /// DisplayModes for the built-in Display Modes and their ids.
+ /// </summary>
+ public interface IDisplayMode
+ {
+ string DisplayModeId { get; }
+ bool CanHandleContext(HttpContextBase httpContext);
+ DisplayInfo GetDisplayInfo(HttpContextBase httpContext, string virtualPath, Func<string, bool> virtualPathExists);
+ }
+}
diff --git a/src/System.Web.WebPages/ITemplateFile.cs b/src/System.Web.WebPages/ITemplateFile.cs
new file mode 100644
index 00000000..e7a628c1
--- /dev/null
+++ b/src/System.Web.WebPages/ITemplateFile.cs
@@ -0,0 +1,12 @@
+namespace System.Web.WebPages
+{
+ /// <summary>
+ /// An interface that provides information about the current executing file.
+ /// WebPageRenderingBase implements this type so that all pages excluding AppStart pages could be queried to identify the
+ /// current executing file.
+ /// </summary>
+ public interface ITemplateFile
+ {
+ TemplateFileInfo TemplateInfo { get; }
+ }
+}
diff --git a/src/System.Web.WebPages/IVirtualPathFactory.cs b/src/System.Web.WebPages/IVirtualPathFactory.cs
new file mode 100644
index 00000000..9c36185d
--- /dev/null
+++ b/src/System.Web.WebPages/IVirtualPathFactory.cs
@@ -0,0 +1,10 @@
+namespace System.Web.WebPages
+{
+ // Implemented by classes that can create object instances from virtual path.
+ // Those implementations can completely bypass the BuildManager (e.g. for dynamic language pages)
+ public interface IVirtualPathFactory
+ {
+ bool Exists(string virtualPath);
+ object CreateInstance(string virtualPath);
+ }
+}
diff --git a/src/System.Web.WebPages/IWebPageRequestExecutor.cs b/src/System.Web.WebPages/IWebPageRequestExecutor.cs
new file mode 100644
index 00000000..e5ab936b
--- /dev/null
+++ b/src/System.Web.WebPages/IWebPageRequestExecutor.cs
@@ -0,0 +1,9 @@
+namespace System.Web.WebPages
+{
+ // An executor is a class that can take over the execution of a WebPage. This can be used to
+ // implement features like AJAX callback methods on the page (like WebForms Page Methods)
+ public interface IWebPageRequestExecutor
+ {
+ bool Execute(WebPage page);
+ }
+}
diff --git a/src/System.Web.WebPages/Instrumentation/HttpContextAdapter.Availability.cs b/src/System.Web.WebPages/Instrumentation/HttpContextAdapter.Availability.cs
new file mode 100644
index 00000000..4e1b92a4
--- /dev/null
+++ b/src/System.Web.WebPages/Instrumentation/HttpContextAdapter.Availability.cs
@@ -0,0 +1,14 @@
+using System.Reflection;
+
+namespace System.Web.WebPages.Instrumentation
+{
+ internal partial class HttpContextAdapter
+ {
+ private static readonly bool _isInstrumentationAvailable = typeof(HttpContext).GetProperty("PageInstrumentation", BindingFlags.Instance | BindingFlags.Public) != null;
+
+ internal static bool IsInstrumentationAvailable
+ {
+ get { return _isInstrumentationAvailable; }
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Instrumentation/HttpContextAdapter.generated.cs b/src/System.Web.WebPages/Instrumentation/HttpContextAdapter.generated.cs
new file mode 100644
index 00000000..6489548a
--- /dev/null
+++ b/src/System.Web.WebPages/Instrumentation/HttpContextAdapter.generated.cs
@@ -0,0 +1,24 @@
+using System.CodeDom.Compiler;
+
+namespace System.Web.WebPages.Instrumentation
+{
+ [GeneratedCode("Microsoft.Web.CodeGen.DynamicCallerGenerator", "1.0.0.0")]
+ internal partial class HttpContextAdapter
+ {
+ internal PageInstrumentationServiceAdapter PageInstrumentation
+ {
+ get { return new PageInstrumentationServiceAdapter((object)Adaptee.PageInstrumentation); }
+ }
+
+ // BEGIN Adaptor Infrastructure Code
+ private static readonly Type _TargetType = typeof(HttpContext);
+ internal dynamic Adaptee { get; private set; }
+
+ internal HttpContextAdapter(object existing)
+ {
+ Adaptee = existing;
+ }
+
+ // END Adaptor Infrastructure Code
+ }
+}
diff --git a/src/System.Web.WebPages/Instrumentation/HttpContextAdapter.tt b/src/System.Web.WebPages/Instrumentation/HttpContextAdapter.tt
new file mode 100644
index 00000000..32cbebe9
--- /dev/null
+++ b/src/System.Web.WebPages/Instrumentation/HttpContextAdapter.tt
@@ -0,0 +1,21 @@
+<#@ template debug="false" hostspecific="true" language="C#" inherits="Microsoft.Web.CodeGen.DynamicCallerGenerator" #>
+<#@ import namespace="System.Reflection" #>
+<#@ assembly name="$(SolutionDir)\tools\Microsoft.Web.CodeGen\Microsoft.Web.CodeGen.dll" #>
+<#@ output extension=".cs" #>
+<#
+TargetNamespace = "System.Web.WebPages.Instrumentation";
+
+Include(MemberTypes.Property, "PageInstrumentation");
+
+Map(
+ "System.Web.Instrumentation.PageInstrumentationService",
+ "System.Web.WebPages.Instrumentation.PageInstrumentationServiceAdapter",
+ s => {#><#=s#>.Adaptee<#},
+ s => {#>new PageInstrumentationServiceAdapter(<#=s#>)<#}
+);
+
+WriteAdaptor(
+ "System.Web.HttpContext",
+ Host.ResolvePath(@"..\..\..\ReferenceAssemblies\NetFx\Dev11\System.Web.dll")
+);
+#> \ No newline at end of file
diff --git a/src/System.Web.WebPages/Instrumentation/InstrumentationService.cs b/src/System.Web.WebPages/Instrumentation/InstrumentationService.cs
new file mode 100644
index 00000000..fd7d8f5a
--- /dev/null
+++ b/src/System.Web.WebPages/Instrumentation/InstrumentationService.cs
@@ -0,0 +1,81 @@
+using System.IO;
+
+namespace System.Web.WebPages.Instrumentation
+{
+ public class InstrumentationService
+ {
+ private static readonly bool _isAvailable = HttpContextAdapter.IsInstrumentationAvailable;
+
+ private bool _localIsAvailable = _isAvailable && PageInstrumentationServiceAdapter.IsEnabled;
+
+ public InstrumentationService()
+ {
+ ExtractInstrumentationService = GetInstrumentationService;
+ CreateContext = CreateSystemWebContext;
+ }
+
+ public bool IsAvailable
+ {
+ get { return _localIsAvailable; }
+ internal set { _localIsAvailable = value; }
+ }
+
+ internal Func<HttpContextBase, PageInstrumentationServiceAdapter> ExtractInstrumentationService { get; set; }
+ internal Func<string, TextWriter, int, int, bool, PageExecutionContextAdapter> CreateContext { get; set; }
+
+ public void BeginContext(HttpContextBase context, string virtualPath, TextWriter writer, int startPosition, int length, bool isLiteral)
+ {
+ RunOnListeners(context,
+ listener => listener.BeginContext(CreateContext(
+ virtualPath,
+ writer,
+ startPosition,
+ length,
+ isLiteral)));
+ }
+
+ public void EndContext(HttpContextBase context, string virtualPath, TextWriter writer, int startPosition, int length, bool isLiteral)
+ {
+ RunOnListeners(context,
+ listener => listener.EndContext(CreateContext(
+ virtualPath,
+ writer,
+ startPosition,
+ length,
+ isLiteral)));
+ }
+
+ private PageExecutionContextAdapter CreateSystemWebContext(string virtualPath, TextWriter writer, int startPosition, int length, bool isLiteral)
+ {
+ return new PageExecutionContextAdapter()
+ {
+ VirtualPath = virtualPath,
+ TextWriter = writer,
+ StartPosition = startPosition,
+ Length = length,
+ IsLiteral = isLiteral
+ };
+ }
+
+ private PageInstrumentationServiceAdapter GetInstrumentationService(HttpContextBase context)
+ {
+ HttpContextAdapter ctx = new HttpContextAdapter(context);
+ return ctx.PageInstrumentation;
+ }
+
+ private void RunOnListeners(HttpContextBase context, Action<PageExecutionListenerAdapter> act)
+ {
+ if (IsAvailable)
+ {
+ PageInstrumentationServiceAdapter instSvc = ExtractInstrumentationService(context);
+ if (instSvc != null)
+ {
+ foreach (PageExecutionListenerAdapter listener in instSvc.ExecutionListeners)
+ {
+ act(listener);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Instrumentation/PageExecutionContextAdapter.generated.cs b/src/System.Web.WebPages/Instrumentation/PageExecutionContextAdapter.generated.cs
new file mode 100644
index 00000000..a1418de4
--- /dev/null
+++ b/src/System.Web.WebPages/Instrumentation/PageExecutionContextAdapter.generated.cs
@@ -0,0 +1,68 @@
+using System.CodeDom.Compiler;
+using System.IO;
+using System.Linq.Expressions;
+
+namespace System.Web.WebPages.Instrumentation
+{
+ [GeneratedCode("Microsoft.Web.CodeGen.DynamicCallerGenerator", "1.0.0.0")]
+ internal partial class PageExecutionContextAdapter
+ {
+ internal bool IsLiteral
+ {
+ get { return Adaptee.IsLiteral; }
+ set { Adaptee.IsLiteral = value; }
+ }
+
+ internal int Length
+ {
+ get { return Adaptee.Length; }
+ set { Adaptee.Length = value; }
+ }
+
+ internal int StartPosition
+ {
+ get { return Adaptee.StartPosition; }
+ set { Adaptee.StartPosition = value; }
+ }
+
+ internal TextWriter TextWriter
+ {
+ get { return Adaptee.TextWriter; }
+ set { Adaptee.TextWriter = value; }
+ }
+
+ internal string VirtualPath
+ {
+ get { return Adaptee.VirtualPath; }
+ set { Adaptee.VirtualPath = value; }
+ }
+
+ private static class _CallSite_ctor_1
+ {
+ public static Func<object> Site;
+
+ static _CallSite_ctor_1()
+ {
+ Site = Expression.Lambda<Func<object>>(
+ Expression.New(_TargetType.GetConstructor(new Type[] { })))
+ .Compile();
+ }
+ }
+
+ internal PageExecutionContextAdapter()
+ {
+ Adaptee = _CallSite_ctor_1.Site();
+ }
+
+ // BEGIN Adaptor Infrastructure Code
+ private static readonly Type _TargetType = typeof(HttpContext).Assembly.GetType("System.Web.Instrumentation.PageExecutionContext");
+ internal dynamic Adaptee { get; private set; }
+
+ internal PageExecutionContextAdapter(object existing)
+ {
+ Adaptee = existing;
+ }
+
+ // END Adaptor Infrastructure Code
+ }
+}
diff --git a/src/System.Web.WebPages/Instrumentation/PageExecutionContextAdapter.tt b/src/System.Web.WebPages/Instrumentation/PageExecutionContextAdapter.tt
new file mode 100644
index 00000000..6165e91a
--- /dev/null
+++ b/src/System.Web.WebPages/Instrumentation/PageExecutionContextAdapter.tt
@@ -0,0 +1,9 @@
+<#@ template debug="false" hostspecific="true" language="C#" inherits="Microsoft.Web.CodeGen.DynamicCallerGenerator" #>
+<#@ assembly name="$(SolutionDir)\tools\Microsoft.Web.CodeGen\Microsoft.Web.CodeGen.dll" #>
+<#@ output extension=".cs" #>
+<#
+TargetNamespace = "System.Web.WebPages.Instrumentation";
+WriteAdaptor(
+ "System.Web.Instrumentation.PageExecutionContext",
+ Host.ResolvePath(@"..\..\..\ReferenceAssemblies\NetFx\Dev11\System.Web.dll")
+); #> \ No newline at end of file
diff --git a/src/System.Web.WebPages/Instrumentation/PageExecutionListenerAdapter.generated.cs b/src/System.Web.WebPages/Instrumentation/PageExecutionListenerAdapter.generated.cs
new file mode 100644
index 00000000..0f99ec5d
--- /dev/null
+++ b/src/System.Web.WebPages/Instrumentation/PageExecutionListenerAdapter.generated.cs
@@ -0,0 +1,29 @@
+using System.CodeDom.Compiler;
+
+namespace System.Web.WebPages.Instrumentation
+{
+ [GeneratedCode("Microsoft.Web.CodeGen.DynamicCallerGenerator", "1.0.0.0")]
+ internal partial class PageExecutionListenerAdapter
+ {
+ internal void BeginContext(PageExecutionContextAdapter context)
+ {
+ Adaptee.BeginContext(context.Adaptee);
+ }
+
+ internal void EndContext(PageExecutionContextAdapter context)
+ {
+ Adaptee.EndContext(context.Adaptee);
+ }
+
+ // BEGIN Adaptor Infrastructure Code
+ private static readonly Type _TargetType = typeof(HttpContext).Assembly.GetType("System.Web.Instrumentation.PageExecutionListener");
+ internal dynamic Adaptee { get; private set; }
+
+ internal PageExecutionListenerAdapter(object existing)
+ {
+ Adaptee = existing;
+ }
+
+ // END Adaptor Infrastructure Code
+ }
+}
diff --git a/src/System.Web.WebPages/Instrumentation/PageExecutionListenerAdapter.tt b/src/System.Web.WebPages/Instrumentation/PageExecutionListenerAdapter.tt
new file mode 100644
index 00000000..eef34eff
--- /dev/null
+++ b/src/System.Web.WebPages/Instrumentation/PageExecutionListenerAdapter.tt
@@ -0,0 +1,14 @@
+<#@ template debug="false" hostspecific="true" language="C#" inherits="Microsoft.Web.CodeGen.DynamicCallerGenerator" #>
+<#@ assembly name="$(SolutionDir)\tools\Microsoft.Web.CodeGen\Microsoft.Web.CodeGen.dll" #>
+<#@ output extension=".cs" #>
+<#
+TargetNamespace = "System.Web.WebPages.Instrumentation";
+Map(
+ "System.Web.Instrumentation.PageExecutionContext",
+ "System.Web.WebPages.Instrumentation.PageExecutionContextAdapter",
+ s => {#><#=s#>.Adaptee<#}
+);
+WriteAdaptor(
+ "System.Web.Instrumentation.PageExecutionListener",
+ Host.ResolvePath(@"..\..\..\ReferenceAssemblies\NetFx\Dev11\System.Web.dll")
+); #> \ No newline at end of file
diff --git a/src/System.Web.WebPages/Instrumentation/PageInstrumentationServiceAdapter.cs b/src/System.Web.WebPages/Instrumentation/PageInstrumentationServiceAdapter.cs
new file mode 100644
index 00000000..3f1636a0
--- /dev/null
+++ b/src/System.Web.WebPages/Instrumentation/PageInstrumentationServiceAdapter.cs
@@ -0,0 +1,95 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+
+namespace System.Web.WebPages.Instrumentation
+{
+ internal partial class PageInstrumentationServiceAdapter
+ {
+ private static readonly Type _targetType = typeof(HttpContext).Assembly.GetType("System.Web.Instrumentation.PageInstrumentationService");
+
+ internal PageInstrumentationServiceAdapter()
+ {
+ Adaptee = _CallSite_ctor_2.Site();
+ }
+
+ internal PageInstrumentationServiceAdapter(object existing)
+ {
+ Adaptee = existing;
+ }
+
+ internal IEnumerable<PageExecutionListenerAdapter> ExecutionListeners
+ {
+ get
+ {
+ IEnumerable<dynamic> inner = Adaptee.ExecutionListeners;
+ // Bug 235916: If we pass the type as an object, the callsite is limited to wherever the object is assigned to dynamic which avoids private reflection issues in
+ // partial trust.
+ return inner.Select(listener => new PageExecutionListenerAdapter((object)listener));
+ }
+ }
+
+ internal static bool IsEnabled
+ {
+ get { return _CallSite_IsEnabled_1.Getter(); }
+ set { _CallSite_IsEnabled_1.Setter(value); }
+ }
+
+ internal dynamic Adaptee { get; private set; }
+
+ private static class _CallSite_IsEnabled_1
+ {
+ public static Func<bool> Getter;
+ public static Action<bool> Setter;
+
+ [SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline", Justification = "Fields cannot be initialized at declaration")]
+ static _CallSite_IsEnabled_1()
+ {
+ PropertyInfo prop = null;
+ if (_targetType != null)
+ {
+ prop = _targetType.GetProperty("IsEnabled", BindingFlags.Static | BindingFlags.Public, Type.DefaultBinder, typeof(bool), Type.EmptyTypes, new ParameterModifier[0]);
+ }
+ if (prop != null)
+ {
+ Getter = Expression.Lambda<Func<bool>>(Expression.Property(null, prop)).Compile();
+ ParameterExpression value = Expression.Parameter(typeof(bool));
+ Setter = Expression.Lambda<Action<bool>>(
+ Expression.Assign(Expression.Property(null, prop), value), value).Compile();
+ }
+ else
+ {
+ Getter = () => false;
+ Setter = _ =>
+ {
+ };
+ }
+ }
+ }
+
+ private static class _CallSite_ctor_2
+ {
+ public static Func<object> Site;
+
+ [SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline", Justification = "Fields cannot be initialized at declaration")]
+ static _CallSite_ctor_2()
+ {
+ if (_targetType != null)
+ {
+ Site = Expression.Lambda<Func<object>>(
+ Expression.New(
+ _targetType.GetConstructor(new Type[] { })))
+ .Compile();
+ }
+ else
+ {
+ Site = () => null;
+ }
+ }
+ }
+
+ // END Adaptor Infrastructure Code
+ }
+}
diff --git a/src/System.Web.WebPages/Instrumentation/PositionTagged.cs b/src/System.Web.WebPages/Instrumentation/PositionTagged.cs
new file mode 100644
index 00000000..3edcc95f
--- /dev/null
+++ b/src/System.Web.WebPages/Instrumentation/PositionTagged.cs
@@ -0,0 +1,65 @@
+using System.Diagnostics;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.WebPages.Instrumentation
+{
+ [DebuggerDisplay("({Position})\"{Value}\"")]
+ public class PositionTagged<T>
+ {
+ private PositionTagged()
+ {
+ Position = 0;
+ Value = default(T);
+ }
+
+ public PositionTagged(T value, int offset)
+ {
+ Position = offset;
+ Value = value;
+ }
+
+ public int Position { get; private set; }
+ public T Value { get; private set; }
+
+ public override bool Equals(object obj)
+ {
+ PositionTagged<T> other = obj as PositionTagged<T>;
+ return other != null &&
+ other.Position == Position &&
+ Equals(other.Value, Value);
+ }
+
+ public override int GetHashCode()
+ {
+ return HashCodeCombiner.Start()
+ .Add(Position)
+ .Add(Value)
+ .CombinedHash;
+ }
+
+ public override string ToString()
+ {
+ return Value.ToString();
+ }
+
+ public static implicit operator T(PositionTagged<T> value)
+ {
+ return value.Value;
+ }
+
+ public static implicit operator PositionTagged<T>(Tuple<T, int> value)
+ {
+ return new PositionTagged<T>(value.Item1, value.Item2);
+ }
+
+ public static bool operator ==(PositionTagged<T> left, PositionTagged<T> right)
+ {
+ return Equals(left, right);
+ }
+
+ public static bool operator !=(PositionTagged<T> left, PositionTagged<T> right)
+ {
+ return !Equals(left, right);
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Mvc/HttpAntiForgeryException.cs b/src/System.Web.WebPages/Mvc/HttpAntiForgeryException.cs
new file mode 100644
index 00000000..4d987bb4
--- /dev/null
+++ b/src/System.Web.WebPages/Mvc/HttpAntiForgeryException.cs
@@ -0,0 +1,29 @@
+using System.Runtime.CompilerServices;
+using System.Runtime.Serialization;
+
+namespace System.Web.Mvc
+{
+ [Serializable]
+ [TypeForwardedFrom("System.Web.Mvc, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35")]
+ public sealed class HttpAntiForgeryException : HttpException
+ {
+ public HttpAntiForgeryException()
+ {
+ }
+
+ private HttpAntiForgeryException(SerializationInfo info, StreamingContext context)
+ : base(info, context)
+ {
+ }
+
+ public HttpAntiForgeryException(string message)
+ : base(message)
+ {
+ }
+
+ public HttpAntiForgeryException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Mvc/ModelClientValidationEqualToRule.cs b/src/System.Web.WebPages/Mvc/ModelClientValidationEqualToRule.cs
new file mode 100644
index 00000000..6572841f
--- /dev/null
+++ b/src/System.Web.WebPages/Mvc/ModelClientValidationEqualToRule.cs
@@ -0,0 +1,15 @@
+using System.Runtime.CompilerServices;
+
+namespace System.Web.Mvc
+{
+ [TypeForwardedFrom("System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35")]
+ public class ModelClientValidationEqualToRule : ModelClientValidationRule
+ {
+ public ModelClientValidationEqualToRule(string errorMessage, object other)
+ {
+ ErrorMessage = errorMessage;
+ ValidationType = "equalto";
+ ValidationParameters["other"] = other;
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Mvc/ModelClientValidationRangeRule.cs b/src/System.Web.WebPages/Mvc/ModelClientValidationRangeRule.cs
new file mode 100644
index 00000000..a4e5b1d6
--- /dev/null
+++ b/src/System.Web.WebPages/Mvc/ModelClientValidationRangeRule.cs
@@ -0,0 +1,16 @@
+using System.Runtime.CompilerServices;
+
+namespace System.Web.Mvc
+{
+ [TypeForwardedFrom("System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35")]
+ public class ModelClientValidationRangeRule : ModelClientValidationRule
+ {
+ public ModelClientValidationRangeRule(string errorMessage, object minValue, object maxValue)
+ {
+ ErrorMessage = errorMessage;
+ ValidationType = "range";
+ ValidationParameters["min"] = minValue;
+ ValidationParameters["max"] = maxValue;
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Mvc/ModelClientValidationRegexRule.cs b/src/System.Web.WebPages/Mvc/ModelClientValidationRegexRule.cs
new file mode 100644
index 00000000..1bc92d4b
--- /dev/null
+++ b/src/System.Web.WebPages/Mvc/ModelClientValidationRegexRule.cs
@@ -0,0 +1,15 @@
+using System.Runtime.CompilerServices;
+
+namespace System.Web.Mvc
+{
+ [TypeForwardedFrom("System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35")]
+ public class ModelClientValidationRegexRule : ModelClientValidationRule
+ {
+ public ModelClientValidationRegexRule(string errorMessage, string pattern)
+ {
+ ErrorMessage = errorMessage;
+ ValidationType = "regex";
+ ValidationParameters.Add("pattern", pattern);
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Mvc/ModelClientValidationRemoteRule.cs b/src/System.Web.WebPages/Mvc/ModelClientValidationRemoteRule.cs
new file mode 100644
index 00000000..943d99a2
--- /dev/null
+++ b/src/System.Web.WebPages/Mvc/ModelClientValidationRemoteRule.cs
@@ -0,0 +1,24 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+
+namespace System.Web.Mvc
+{
+ [TypeForwardedFrom("System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35")]
+ public class ModelClientValidationRemoteRule : ModelClientValidationRule
+ {
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", Justification = "The value is a not a regular URL since it may contain ~/ ASP.NET-specific characters")]
+ public ModelClientValidationRemoteRule(string errorMessage, string url, string httpMethod, string additionalFields)
+ {
+ ErrorMessage = errorMessage;
+ ValidationType = "remote";
+ ValidationParameters["url"] = url;
+
+ if (!String.IsNullOrEmpty(httpMethod))
+ {
+ ValidationParameters["type"] = httpMethod;
+ }
+
+ ValidationParameters["additionalfields"] = additionalFields;
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Mvc/ModelClientValidationRequiredRule.cs b/src/System.Web.WebPages/Mvc/ModelClientValidationRequiredRule.cs
new file mode 100644
index 00000000..989ecdeb
--- /dev/null
+++ b/src/System.Web.WebPages/Mvc/ModelClientValidationRequiredRule.cs
@@ -0,0 +1,14 @@
+using System.Runtime.CompilerServices;
+
+namespace System.Web.Mvc
+{
+ [TypeForwardedFrom("System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35")]
+ public class ModelClientValidationRequiredRule : ModelClientValidationRule
+ {
+ public ModelClientValidationRequiredRule(string errorMessage)
+ {
+ ErrorMessage = errorMessage;
+ ValidationType = "required";
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Mvc/ModelClientValidationRule.cs b/src/System.Web.WebPages/Mvc/ModelClientValidationRule.cs
new file mode 100644
index 00000000..1551e553
--- /dev/null
+++ b/src/System.Web.WebPages/Mvc/ModelClientValidationRule.cs
@@ -0,0 +1,25 @@
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+
+namespace System.Web.Mvc
+{
+ [TypeForwardedFrom("System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35")]
+ public class ModelClientValidationRule
+ {
+ private readonly Dictionary<string, object> _validationParameters = new Dictionary<string, object>();
+ private string _validationType;
+
+ public string ErrorMessage { get; set; }
+
+ public IDictionary<string, object> ValidationParameters
+ {
+ get { return _validationParameters; }
+ }
+
+ public string ValidationType
+ {
+ get { return _validationType ?? String.Empty; }
+ set { _validationType = value; }
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Mvc/ModelClientValidationStringLengthRule.cs b/src/System.Web.WebPages/Mvc/ModelClientValidationStringLengthRule.cs
new file mode 100644
index 00000000..7c741e6e
--- /dev/null
+++ b/src/System.Web.WebPages/Mvc/ModelClientValidationStringLengthRule.cs
@@ -0,0 +1,24 @@
+using System.Runtime.CompilerServices;
+
+namespace System.Web.Mvc
+{
+ [TypeForwardedFrom("System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35")]
+ public class ModelClientValidationStringLengthRule : ModelClientValidationRule
+ {
+ public ModelClientValidationStringLengthRule(string errorMessage, int minimumLength, int maximumLength)
+ {
+ ErrorMessage = errorMessage;
+ ValidationType = "length";
+
+ if (minimumLength != 0)
+ {
+ ValidationParameters["min"] = minimumLength;
+ }
+
+ if (maximumLength != Int32.MaxValue)
+ {
+ ValidationParameters["max"] = maximumLength;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Mvc/TagBuilder.cs b/src/System.Web.WebPages/Mvc/TagBuilder.cs
new file mode 100644
index 00000000..47bd8c29
--- /dev/null
+++ b/src/System.Web.WebPages/Mvc/TagBuilder.cs
@@ -0,0 +1,259 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Web.WebPages.Html;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.Mvc
+{
+ [TypeForwardedFrom("System.Web.Mvc, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35")]
+ public class TagBuilder
+ {
+ private string _idAttributeDotReplacement;
+
+ private string _innerHtml;
+
+ public TagBuilder(string tagName)
+ {
+ if (String.IsNullOrEmpty(tagName))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "tagName");
+ }
+
+ TagName = tagName;
+ Attributes = new SortedDictionary<string, string>(StringComparer.Ordinal);
+ }
+
+ public IDictionary<string, string> Attributes { get; private set; }
+
+ public string IdAttributeDotReplacement
+ {
+ get
+ {
+ if (String.IsNullOrEmpty(_idAttributeDotReplacement))
+ {
+ _idAttributeDotReplacement = HtmlHelper.IdAttributeDotReplacement;
+ }
+ return _idAttributeDotReplacement;
+ }
+ set { _idAttributeDotReplacement = value; }
+ }
+
+ public string InnerHtml
+ {
+ get { return _innerHtml ?? String.Empty; }
+ set { _innerHtml = value; }
+ }
+
+ public string TagName { get; private set; }
+
+ public void AddCssClass(string value)
+ {
+ string currentValue;
+
+ if (Attributes.TryGetValue("class", out currentValue))
+ {
+ Attributes["class"] = value + " " + currentValue;
+ }
+ else
+ {
+ Attributes["class"] = value;
+ }
+ }
+
+ public static string CreateSanitizedId(string originalId)
+ {
+ return CreateSanitizedId(originalId, HtmlHelper.IdAttributeDotReplacement);
+ }
+
+ public static string CreateSanitizedId(string originalId, string invalidCharReplacement)
+ {
+ if (String.IsNullOrEmpty(originalId))
+ {
+ return null;
+ }
+
+ if (invalidCharReplacement == null)
+ {
+ throw new ArgumentNullException("invalidCharReplacement");
+ }
+
+ char firstChar = originalId[0];
+ if (!Html401IdUtil.IsLetter(firstChar))
+ {
+ // the first character must be a letter
+ return null;
+ }
+
+ StringBuilder sb = new StringBuilder(originalId.Length);
+ sb.Append(firstChar);
+
+ for (int i = 1; i < originalId.Length; i++)
+ {
+ char thisChar = originalId[i];
+ if (Html401IdUtil.IsValidIdCharacter(thisChar))
+ {
+ sb.Append(thisChar);
+ }
+ else
+ {
+ sb.Append(invalidCharReplacement);
+ }
+ }
+
+ return sb.ToString();
+ }
+
+ public void GenerateId(string name)
+ {
+ if (!Attributes.ContainsKey("id"))
+ {
+ string sanitizedId = CreateSanitizedId(name, IdAttributeDotReplacement);
+ if (!String.IsNullOrEmpty(sanitizedId))
+ {
+ Attributes["id"] = sanitizedId;
+ }
+ }
+ }
+
+ private void AppendAttributes(StringBuilder sb)
+ {
+ foreach (var attribute in Attributes)
+ {
+ string key = attribute.Key;
+ if (String.Equals(key, "id", StringComparison.Ordinal /* case-sensitive */) && String.IsNullOrEmpty(attribute.Value))
+ {
+ continue; // DevDiv Bugs #227595: don't output empty IDs
+ }
+ string value = HttpUtility.HtmlAttributeEncode(attribute.Value);
+ sb.Append(' ')
+ .Append(key)
+ .Append("=\"")
+ .Append(value)
+ .Append('"');
+ }
+ }
+
+ public void MergeAttribute(string key, string value)
+ {
+ MergeAttribute(key, value, replaceExisting: false);
+ }
+
+ public void MergeAttribute(string key, string value, bool replaceExisting)
+ {
+ if (String.IsNullOrEmpty(key))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "key");
+ }
+
+ if (replaceExisting || !Attributes.ContainsKey(key))
+ {
+ Attributes[key] = value;
+ }
+ }
+
+ public void MergeAttributes<TKey, TValue>(IDictionary<TKey, TValue> attributes)
+ {
+ MergeAttributes(attributes, replaceExisting: false);
+ }
+
+ public void MergeAttributes<TKey, TValue>(IDictionary<TKey, TValue> attributes, bool replaceExisting)
+ {
+ if (attributes != null)
+ {
+ foreach (var entry in attributes)
+ {
+ string key = Convert.ToString(entry.Key, CultureInfo.InvariantCulture);
+ string value = Convert.ToString(entry.Value, CultureInfo.InvariantCulture);
+ MergeAttribute(key, value, replaceExisting);
+ }
+ }
+ }
+
+ public void SetInnerText(string innerText)
+ {
+ InnerHtml = HttpUtility.HtmlEncode(innerText);
+ }
+
+ internal IHtmlString ToHtmlString(TagRenderMode renderMode)
+ {
+ return new HtmlString(ToString(renderMode));
+ }
+
+ public override string ToString()
+ {
+ return ToString(TagRenderMode.Normal);
+ }
+
+ public string ToString(TagRenderMode renderMode)
+ {
+ StringBuilder sb = new StringBuilder();
+ switch (renderMode)
+ {
+ case TagRenderMode.StartTag:
+ sb.Append('<')
+ .Append(TagName);
+ AppendAttributes(sb);
+ sb.Append('>');
+ break;
+ case TagRenderMode.EndTag:
+ sb.Append("</")
+ .Append(TagName)
+ .Append('>');
+ break;
+ case TagRenderMode.SelfClosing:
+ sb.Append('<')
+ .Append(TagName);
+ AppendAttributes(sb);
+ sb.Append(" />");
+ break;
+ default:
+ sb.Append('<')
+ .Append(TagName);
+ AppendAttributes(sb);
+ sb.Append('>')
+ .Append(InnerHtml)
+ .Append("</")
+ .Append(TagName)
+ .Append('>');
+ break;
+ }
+ return sb.ToString();
+ }
+
+ // Valid IDs are defined in http://www.w3.org/TR/html401/types.html#type-id
+ private static class Html401IdUtil
+ {
+ private static bool IsAllowableSpecialCharacter(char c)
+ {
+ switch (c)
+ {
+ case '-':
+ case '_':
+ case ':':
+ // note that we're specifically excluding the '.' character
+ return true;
+
+ default:
+ return false;
+ }
+ }
+
+ private static bool IsDigit(char c)
+ {
+ return ('0' <= c && c <= '9');
+ }
+
+ public static bool IsLetter(char c)
+ {
+ return (('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z'));
+ }
+
+ public static bool IsValidIdCharacter(char c)
+ {
+ return (IsLetter(c) || IsDigit(c) || IsAllowableSpecialCharacter(c));
+ }
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Mvc/TagRenderMode.cs b/src/System.Web.WebPages/Mvc/TagRenderMode.cs
new file mode 100644
index 00000000..b4082701
--- /dev/null
+++ b/src/System.Web.WebPages/Mvc/TagRenderMode.cs
@@ -0,0 +1,13 @@
+using System.Runtime.CompilerServices;
+
+namespace System.Web.Mvc
+{
+ [TypeForwardedFrom("System.Web.Mvc, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35")]
+ public enum TagRenderMode
+ {
+ Normal,
+ StartTag,
+ EndTag,
+ SelfClosing
+ }
+}
diff --git a/src/System.Web.WebPages/Mvc/UnobtrusiveValidationAttributesGenerator.cs b/src/System.Web.WebPages/Mvc/UnobtrusiveValidationAttributesGenerator.cs
new file mode 100644
index 00000000..52445300
--- /dev/null
+++ b/src/System.Web.WebPages/Mvc/UnobtrusiveValidationAttributesGenerator.cs
@@ -0,0 +1,96 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Web.WebPages.Resources;
+
+namespace System.Web.Mvc
+{
+ public static class UnobtrusiveValidationAttributesGenerator
+ {
+ public static void GetValidationAttributes(IEnumerable<ModelClientValidationRule> clientRules, IDictionary<string, object> results)
+ {
+ if (clientRules == null)
+ {
+ throw new ArgumentNullException("clientRules");
+ }
+ if (results == null)
+ {
+ throw new ArgumentNullException("results");
+ }
+
+ bool renderedRules = false;
+
+ foreach (ModelClientValidationRule rule in clientRules)
+ {
+ renderedRules = true;
+ string ruleName = "data-val-" + rule.ValidationType;
+
+ ValidateUnobtrusiveValidationRule(rule, results, ruleName);
+
+ results.Add(ruleName, HttpUtility.HtmlEncode(rule.ErrorMessage ?? String.Empty));
+ ruleName += "-";
+
+ foreach (var kvp in rule.ValidationParameters)
+ {
+ results.Add(ruleName + kvp.Key, HttpUtility.HtmlEncode(kvp.Value ?? String.Empty));
+ }
+ }
+
+ if (renderedRules)
+ {
+ results.Add("data-val", "true");
+ }
+ }
+
+ private static void ValidateUnobtrusiveValidationRule(ModelClientValidationRule rule, IDictionary<string, object> resultsDictionary, string dictionaryKey)
+ {
+ if (String.IsNullOrWhiteSpace(rule.ValidationType))
+ {
+ throw new InvalidOperationException(
+ String.Format(
+ CultureInfo.CurrentCulture,
+ WebPageResources.UnobtrusiveJavascript_ValidationTypeCannotBeEmpty,
+ rule.GetType().FullName));
+ }
+
+ if (resultsDictionary.ContainsKey(dictionaryKey))
+ {
+ throw new InvalidOperationException(
+ String.Format(
+ CultureInfo.CurrentCulture,
+ WebPageResources.UnobtrusiveJavascript_ValidationTypeMustBeUnique,
+ rule.ValidationType));
+ }
+
+ if (rule.ValidationType.Any(c => !Char.IsLower(c)))
+ {
+ throw new InvalidOperationException(
+ String.Format(CultureInfo.CurrentCulture, WebPageResources.UnobtrusiveJavascript_ValidationTypeMustBeLegal,
+ rule.ValidationType,
+ rule.GetType().FullName));
+ }
+
+ foreach (var key in rule.ValidationParameters.Keys)
+ {
+ if (String.IsNullOrWhiteSpace(key))
+ {
+ throw new InvalidOperationException(
+ String.Format(
+ CultureInfo.CurrentCulture,
+ WebPageResources.UnobtrusiveJavascript_ValidationParameterCannotBeEmpty,
+ rule.GetType().FullName));
+ }
+
+ if (!Char.IsLower(key.First()) || key.Any(c => !Char.IsLower(c) && !Char.IsDigit(c)))
+ {
+ throw new InvalidOperationException(
+ String.Format(
+ CultureInfo.CurrentCulture,
+ WebPageResources.UnobtrusiveJavascript_ValidationParameterMustBeLegal,
+ key,
+ rule.GetType().FullName));
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/PageDataDictionary.cs b/src/System.Web.WebPages/PageDataDictionary.cs
new file mode 100644
index 00000000..a0c3316a
--- /dev/null
+++ b/src/System.Web.WebPages/PageDataDictionary.cs
@@ -0,0 +1,332 @@
+using System.Collections;
+using System.Collections.Generic;
+
+namespace System.Web.WebPages
+{
+ /// <summary>
+ /// This is a wrapper around Dictionary so that using PageData[key] returns null
+ /// if the key is not found, instead of throwing an exception.
+ /// </summary>
+ // This is a generic type because C# does not allow implementing an interface
+ // involving dynamic types (implementing IDictionary<object, dynamic> causes
+ // a compile error
+ // http://blogs.msdn.com/cburrows/archive/2009/02/04/c-dynamic-part-vii.aspx).
+ internal class PageDataDictionary<TValue> : IDictionary<object, TValue>
+ {
+ private IDictionary<object, TValue> _data = new Dictionary<object, TValue>(new PageDataComparer());
+
+ private IDictionary<string, TValue> _stringDictionary = new Dictionary<string, TValue>(StringComparer.OrdinalIgnoreCase);
+
+ private IList<TValue> _indexedValues = new List<TValue>();
+
+ internal IDictionary<object, TValue> Data
+ {
+ get { return _data; }
+ }
+
+ internal IDictionary<string, TValue> StringDictionary
+ {
+ get { return _stringDictionary; }
+ }
+
+ internal IList<TValue> IndexedValues
+ {
+ get { return _indexedValues; }
+ }
+
+ public ICollection<object> Keys
+ {
+ get
+ {
+ List<object> keys = new List<object>();
+ keys.AddRange(_stringDictionary.Keys);
+ for (int i = 0; i < _indexedValues.Count; i++)
+ {
+ keys.Add(i);
+ }
+ foreach (var key in _data.Keys)
+ {
+ if (!ContainsIndex(key) && !ContainsStringKey(key))
+ {
+ keys.Add(key);
+ }
+ }
+ return keys;
+ }
+ }
+
+ public ICollection<TValue> Values
+ {
+ get
+ {
+ List<TValue> values = new List<TValue>();
+ foreach (var key in Keys)
+ {
+ values.Add(this[key]);
+ }
+ return values;
+ }
+ }
+
+ internal ICollection<KeyValuePair<object, TValue>> Items
+ {
+ get
+ {
+ var items = new List<KeyValuePair<object, TValue>>();
+ foreach (var key in Keys)
+ {
+ var value = this[key];
+ var kvp = new KeyValuePair<object, TValue>(key, value);
+ items.Add(kvp);
+ }
+ return items;
+ }
+ }
+
+ public int Count
+ {
+ get { return Items.Count; }
+ }
+
+ public bool IsReadOnly
+ {
+ get { return false; }
+ }
+
+ public TValue this[object key]
+ {
+ get
+ {
+ TValue v = default(TValue);
+ TryGetValue(key, out v);
+ return v;
+ }
+
+ // Note that this affects and updates the string dictionary and indexed list
+ // only for existing keys found in these collections.
+ // Otherwise, the key/value goes into the _data dictionary.
+ set
+ {
+ if (ContainsStringKey(key))
+ {
+ _stringDictionary[(string)key] = value;
+ }
+ else if (ContainsIndex(key))
+ {
+ _indexedValues[(int)key] = value;
+ }
+ else
+ {
+ _data[key] = value;
+ }
+ }
+ }
+
+ public void Add(object key, TValue value)
+ {
+ _data.Add(key, value);
+ }
+
+ internal bool ContainsIndex(object o)
+ {
+ if (o is int)
+ {
+ return ContainsIndex((int)o);
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ internal bool ContainsIndex(int index)
+ {
+ return _indexedValues.Count > index && index >= 0;
+ }
+
+ internal bool ContainsStringKey(object o)
+ {
+ string s = o as string;
+ if (s != null)
+ {
+ return ContainsStringKey(s);
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ internal bool ContainsStringKey(string key)
+ {
+ return _stringDictionary.ContainsKey(key);
+ }
+
+ public bool ContainsKey(object key)
+ {
+ if (ContainsIndex(key))
+ {
+ return true;
+ }
+ else if (ContainsStringKey(key))
+ {
+ return true;
+ }
+ else if (_data.ContainsKey(key))
+ {
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ // Note that if the key exists in more than one place, then only
+ // the string dictionary and the indexed list will be updated.
+ public bool Remove(object key)
+ {
+ if (ContainsStringKey(key))
+ {
+ return _stringDictionary.Remove((string)key);
+ }
+ else if (ContainsIndex(key))
+ {
+ return _indexedValues.Remove(_indexedValues[(int)key]);
+ }
+ else
+ {
+ return _data.Remove(key);
+ }
+ }
+
+ public bool TryGetValue(object key, out TValue value)
+ {
+ if (ContainsStringKey(key))
+ {
+ return _stringDictionary.TryGetValue((string)key, out value);
+ }
+ else if (ContainsIndex(key))
+ {
+ value = _indexedValues[(int)key];
+ return true;
+ }
+ else
+ {
+ return _data.TryGetValue(key, out value);
+ }
+ }
+
+ public void Add(KeyValuePair<object, TValue> item)
+ {
+ this[item.Key] = item.Value;
+ }
+
+ public void Clear()
+ {
+ _stringDictionary.Clear();
+ _indexedValues.Clear();
+ _data.Clear();
+ }
+
+ public bool Contains(KeyValuePair<object, TValue> item)
+ {
+ return ContainsKey(item.Key) && Values.Contains(item.Value);
+ }
+
+ public void CopyTo(KeyValuePair<object, TValue>[] array, int arrayIndex)
+ {
+ Items.CopyTo(array, arrayIndex);
+ }
+
+ public bool Remove(KeyValuePair<object, TValue> item)
+ {
+ if (Contains(item))
+ {
+ return Remove((object)item.Key);
+ }
+ return false;
+ }
+
+ public IEnumerator<KeyValuePair<object, TValue>> GetEnumerator()
+ {
+ return Items.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return Items.GetEnumerator();
+ }
+
+ // Creates a new PageData dictionary using only the original items from the pageData (excluding the string dictionary and indexed list),
+ // and adding the parameters.
+ internal static IDictionary<object, dynamic> CreatePageDataFromParameters(IDictionary<object, dynamic> previousPageData, params object[] data)
+ {
+ var oldPageData = previousPageData as PageDataDictionary<dynamic>;
+
+ // Add the original items
+ var pageData = new PageDataDictionary<dynamic>();
+ foreach (var kvp in oldPageData.Data)
+ {
+ pageData.Data.Add(kvp);
+ }
+
+ if (data != null && data.Length > 0)
+ {
+ // Add items to the indexed list
+ for (int i = 0; i < data.Length; i++)
+ {
+ pageData.IndexedValues.Add(data[i]);
+ }
+
+ // Check for anonymous types, and add to the string dictionary
+ object first = data[0];
+ Type type = first.GetType();
+ if (TypeHelper.IsAnonymousType(type))
+ {
+ // Anonymous type
+ TypeHelper.AddAnonymousObjectToDictionary(pageData.StringDictionary, first);
+ }
+
+ // Check if the first element is of type IDictionary<string, object>
+ if (typeof(IDictionary<string, object>).IsAssignableFrom(type))
+ {
+ // Dictionary
+ var stringDictionary = first as IDictionary<string, object>;
+ foreach (var kvp in stringDictionary)
+ {
+ pageData.StringDictionary.Add(kvp);
+ }
+ }
+ }
+
+ return pageData;
+ }
+
+ // This comparer treats only strings as case-insensitive, but still handles objects
+ // of other types as well.
+ private sealed class PageDataComparer : IEqualityComparer<object>
+ {
+ bool IEqualityComparer<object>.Equals(object x, object y)
+ {
+ var s1 = x as string;
+ var s2 = y as string;
+ if (s1 != null && s2 != null)
+ {
+ return String.Equals(s1, s2, StringComparison.OrdinalIgnoreCase);
+ }
+ return Equals(x, y);
+ }
+
+ int IEqualityComparer<object>.GetHashCode(object obj)
+ {
+ var s = obj as string;
+ if (s != null)
+ {
+ return s.ToUpperInvariant().GetHashCode();
+ }
+ return obj.GetHashCode();
+ }
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/PageVirtualPathAttribute.cs b/src/System.Web.WebPages/PageVirtualPathAttribute.cs
new file mode 100644
index 00000000..ee5be9b1
--- /dev/null
+++ b/src/System.Web.WebPages/PageVirtualPathAttribute.cs
@@ -0,0 +1,16 @@
+namespace System.Web.WebPages
+{
+ // Attribute placed on a WebPage derived class that indicates the virtual path that it's associated with
+ // This is used to support scenarios where pages are compiled ahead of time in external class libraries
+ // Specifically, this is used by the RazorSingleFileGenerator.
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
+ public sealed class PageVirtualPathAttribute : Attribute
+ {
+ public PageVirtualPathAttribute(string virtualPath)
+ {
+ VirtualPath = virtualPath;
+ }
+
+ public string VirtualPath { get; private set; }
+ }
+}
diff --git a/src/System.Web.WebPages/PreApplicationStartCode.cs b/src/System.Web.WebPages/PreApplicationStartCode.cs
new file mode 100644
index 00000000..513b5f20
--- /dev/null
+++ b/src/System.Web.WebPages/PreApplicationStartCode.cs
@@ -0,0 +1,43 @@
+using System.ComponentModel;
+using System.Web.UI;
+using System.Web.WebPages.Scope;
+using Microsoft.Web.Infrastructure.DynamicModuleHelper;
+
+namespace System.Web.WebPages
+{
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static class PreApplicationStartCode
+ {
+ // NOTE: Do not add public fields, methods, or other members to this class.
+ // This class does not show up in Intellisense so members on it will not be
+ // discoverable by users. Place new members on more appropriate classes that
+ // relate to the public API (for example, a LoginUrl property should go on a
+ // membership-related class).
+
+ private static bool _startWasCalled;
+
+ public static void Start()
+ {
+ // Even though ASP.NET will only call each PreAppStart once, we sometimes internally call one PreAppStart from
+ // another PreAppStart to ensure that things get initialized in the right order. ASP.NET does not guarantee the
+ // order so we have to guard against multiple calls.
+ // All Start calls are made on same thread, so no lock needed here.
+
+ if (_startWasCalled)
+ {
+ return;
+ }
+ _startWasCalled = true;
+
+ WebPageHttpHandler.RegisterExtension("cshtml");
+ WebPageHttpHandler.RegisterExtension("vbhtml");
+
+ // Turn off the string resource behavior which would not work in our simple base page
+ PageParser.EnableLongStringsAsResources = false;
+
+ DynamicModuleUtility.RegisterModule(typeof(WebPageHttpModule));
+
+ ScopeStorage.CurrentProvider = new AspNetRequestScopeStorageProvider();
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Properties/AssemblyInfo.cs b/src/System.Web.WebPages/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..ae1f748b
--- /dev/null
+++ b/src/System.Web.WebPages/Properties/AssemblyInfo.cs
@@ -0,0 +1,19 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Web;
+using System.Web.WebPages;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+
+[assembly: AssemblyTitle("System.Web.WebPages")]
+[assembly: AssemblyDescription("")]
+[assembly: InternalsVisibleTo("System.Web.Mvc, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
+[assembly: InternalsVisibleTo("System.Web.Helpers, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
+[assembly: InternalsVisibleTo("System.Web.WebPages.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
+[assembly: InternalsVisibleTo("System.Web.Helpers.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
+[assembly: InternalsVisibleTo("System.Web.WebPages.Administration.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
+[assembly: PreApplicationStartMethod(typeof(PreApplicationStartCode), "Start")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "System.Web.Helpers", Justification = "Namespace has more types in System.Web.Helpers.dll.")]
diff --git a/src/System.Web.WebPages/ReflectionDynamicObject.cs b/src/System.Web.WebPages/ReflectionDynamicObject.cs
new file mode 100644
index 00000000..5f2edc9d
--- /dev/null
+++ b/src/System.Web.WebPages/ReflectionDynamicObject.cs
@@ -0,0 +1,82 @@
+using System.Dynamic;
+using System.Globalization;
+using System.Reflection;
+
+namespace System.Web.WebPages
+{
+ // Allows dynamic access over a CLR object via private reflection
+ internal sealed class ReflectionDynamicObject : DynamicObject
+ {
+ private object RealObject { get; set; }
+
+ public static object WrapObjectIfInternal(object o)
+ {
+ // If it's null, don't try to wrap it
+ if (o == null)
+ {
+ return null;
+ }
+
+ // If it's public, leave it alone since the standard dynamic binder will work. Well, it won't work for
+ // internal properties, but we're mostly concerned about supporting anonymous objects, which are never public
+ if (o.GetType().IsPublic)
+ {
+ return o;
+ }
+
+ return new ReflectionDynamicObject() { RealObject = o };
+ }
+
+ // Called when a property is accessed
+ public override bool TryGetMember(GetMemberBinder binder, out object result)
+ {
+ // Get the property info
+ PropertyInfo propInfo = RealObject.GetType().GetProperty(
+ binder.Name,
+ BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.Public);
+
+ if (propInfo == null)
+ {
+ // If there is no such property, return null instead of failing. This allows optional parameters
+ result = null;
+ }
+ else
+ {
+ // Get the property value
+ result = propInfo.GetValue(RealObject, null);
+
+ // Wrap the sub object if necessary. This allows nested anonymous objects to work.
+ result = WrapObjectIfInternal(result);
+ }
+
+ return true;
+ }
+
+ // Called when a method is called
+ public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
+ {
+ result = RealObject.GetType().InvokeMember(
+ binder.Name,
+ BindingFlags.InvokeMethod | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
+ null,
+ RealObject,
+ args,
+ CultureInfo.InvariantCulture);
+
+ return true;
+ }
+
+ // Called when the dynamic object needs to be converted to a non dynamic object
+ public override bool TryConvert(ConvertBinder binder, out object result)
+ {
+ result = RealObject;
+ return true;
+ }
+
+ public override string ToString()
+ {
+ // Just return the original object's display string
+ return RealObject.ToString();
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/RequestBrowserOverrideStore.cs b/src/System.Web.WebPages/RequestBrowserOverrideStore.cs
new file mode 100644
index 00000000..47a171b7
--- /dev/null
+++ b/src/System.Web.WebPages/RequestBrowserOverrideStore.cs
@@ -0,0 +1,18 @@
+namespace System.Web.WebPages
+{
+ /// <summary>
+ /// RequestBrowserOverrideStore simply returns the user agent of the current request.
+ /// </summary>
+ internal sealed class RequestBrowserOverrideStore : BrowserOverrideStore
+ {
+ public override string GetOverriddenUserAgent(HttpContextBase httpContext)
+ {
+ return httpContext.Request.UserAgent;
+ }
+
+ public override void SetOverriddenUserAgent(HttpContextBase httpContext, string userAgent)
+ {
+ return;
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/RequestExtensions.cs b/src/System.Web.WebPages/RequestExtensions.cs
new file mode 100644
index 00000000..cde6c9ba
--- /dev/null
+++ b/src/System.Web.WebPages/RequestExtensions.cs
@@ -0,0 +1,16 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Web.WebPages
+{
+ public static class RequestExtensions
+ {
+ [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "The request parameter is no longer being used but we do not want to break legacy callers.")]
+ [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "Response.Redirect() takes its URI as a string parameter.")]
+ public static bool IsUrlLocalToHost(this HttpRequestBase request, string url)
+ {
+ return !url.IsEmpty() &&
+ ((url[0] == '/' && (url.Length == 1 || (url[1] != '/' && url[1] != '\\'))) || // "/" or "/foo" but not "//" or "/\"
+ (url.Length > 1 && url[0] == '~' && url[1] == '/')); // "~/" or "~/foo"
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/RequestResourceTracker.cs b/src/System.Web.WebPages/RequestResourceTracker.cs
new file mode 100644
index 00000000..9e8cbea1
--- /dev/null
+++ b/src/System.Web.WebPages/RequestResourceTracker.cs
@@ -0,0 +1,68 @@
+using System.Collections.Generic;
+
+namespace System.Web.WebPages
+{
+ internal static class RequestResourceTracker
+ {
+ private static readonly object _resourcesKey = new object();
+
+ private static List<SecureWeakReference> GetResources(HttpContextBase context)
+ {
+ var resources = (List<SecureWeakReference>)context.Items[_resourcesKey];
+ if (resources == null)
+ {
+ resources = new List<SecureWeakReference>();
+ context.Items[_resourcesKey] = resources;
+ }
+
+ return resources;
+ }
+
+ internal static void DisposeResources(HttpContextBase context)
+ {
+ var resources = GetResources(context);
+ if (resources != null)
+ {
+ resources.ForEach(resource => resource.Dispose());
+ resources.Clear();
+ }
+ }
+
+ internal static void RegisterForDispose(HttpContextBase context, IDisposable resource)
+ {
+ var resources = GetResources(context);
+ if (resources != null)
+ {
+ resources.Add(new SecureWeakReference(resource));
+ }
+ }
+
+ internal static void RegisterForDispose(IDisposable resource)
+ {
+ var context = HttpContext.Current;
+ if (context != null)
+ {
+ RegisterForDispose(new HttpContextWrapper(context), resource);
+ }
+ }
+
+ private sealed class SecureWeakReference
+ {
+ private readonly WeakReference _reference;
+
+ public SecureWeakReference(IDisposable reference)
+ {
+ _reference = new WeakReference(reference);
+ }
+
+ internal void Dispose()
+ {
+ var disposable = (IDisposable)_reference.Target;
+ if (disposable != null)
+ {
+ disposable.Dispose();
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Resources/WebPageResources.Designer.cs b/src/System.Web.WebPages/Resources/WebPageResources.Designer.cs
new file mode 100644
index 00000000..f374f093
--- /dev/null
+++ b/src/System.Web.WebPages/Resources/WebPageResources.Designer.cs
@@ -0,0 +1,441 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.239
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace System.Web.WebPages.Resources {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class WebPageResources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal WebPageResources() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("System.Web.WebPages.Resources.WebPageResources", typeof(WebPageResources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A required anti-forgery token was not supplied or was invalid..
+ /// </summary>
+ internal static string AntiForgeryToken_ValidationFailed {
+ get {
+ return ResourceManager.GetString("AntiForgeryToken_ValidationFailed", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The assembly &quot;{0}&quot; is already registered..
+ /// </summary>
+ internal static string ApplicationPart_ModuleAlreadyRegistered {
+ get {
+ return ResourceManager.GetString("ApplicationPart_ModuleAlreadyRegistered", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to An application module is already registered for virtual path &quot;{0}&quot;..
+ /// </summary>
+ internal static string ApplicationPart_ModuleAlreadyRegisteredForVirtualPath {
+ get {
+ return ResourceManager.GetString("ApplicationPart_ModuleAlreadyRegisteredForVirtualPath", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Unable to find an application module with the name &quot;{0}&quot;..
+ /// </summary>
+ internal static string ApplicationPart_ModuleCannotBeFound {
+ get {
+ return ResourceManager.GetString("ApplicationPart_ModuleCannotBeFound", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The assembly &quot;{0}&quot; is not a registered application module..
+ /// </summary>
+ internal static string ApplicationPart_ModuleNotRegistered {
+ get {
+ return ResourceManager.GetString("ApplicationPart_ModuleNotRegistered", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The resource file &quot;{0}&quot; could not be found..
+ /// </summary>
+ internal static string ApplicationPart_ResourceNotFound {
+ get {
+ return ResourceManager.GetString("ApplicationPart_ResourceNotFound", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Index length must be exactly one..
+ /// </summary>
+ internal static string DynamicDictionary_InvalidNumberOfIndexes {
+ get {
+ return ResourceManager.GetString("DynamicDictionary_InvalidNumberOfIndexes", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Index must be of type string or int when getting a value..
+ /// </summary>
+ internal static string DynamicHttpApplicationState_UseOnlyStringOrIntToGet {
+ get {
+ return ResourceManager.GetString("DynamicHttpApplicationState_UseOnlyStringOrIntToGet", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Index must be of type string when setting a value..
+ /// </summary>
+ internal static string DynamicHttpApplicationState_UseOnlyStringToSet {
+ get {
+ return ResourceManager.GetString("DynamicHttpApplicationState_UseOnlyStringToSet", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The parameter conversion from type &quot;{0}&quot; to type &quot;{1}&quot; failed. See the inner exception for more information..
+ /// </summary>
+ internal static string HtmlHelper_ConversionThrew {
+ get {
+ return ResourceManager.GetString("HtmlHelper_ConversionThrew", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The parameter conversion from type &quot;{0}&quot; to type &quot;{1}&quot; failed because no type converter can convert between these types..
+ /// </summary>
+ internal static string HtmlHelper_NoConverterExists {
+ get {
+ return ResourceManager.GetString("HtmlHelper_NoConverterExists", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to An HttpContext is required to perform this operation. Check that this operation is being performed during a web request..
+ /// </summary>
+ internal static string HttpContextUnavailable {
+ get {
+ return ResourceManager.GetString("HttpContextUnavailable", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Value &quot;{0}&quot; specified in &quot;{1}&quot; is an invalid value for the SessionState directive. Possible values are: &quot;{2}&quot;..
+ /// </summary>
+ internal static string SessionState_InvalidValue {
+ get {
+ return ResourceManager.GetString("SessionState_InvalidValue", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to At most one SessionState value can be declared per page..
+ /// </summary>
+ internal static string SessionState_TooManyValues {
+ get {
+ return ResourceManager.GetString("SessionState_TooManyValues", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to RequestScope cannot be created when _AppStart is executing..
+ /// </summary>
+ internal static string StateStorage_RequestScopeNotAvailable {
+ get {
+ return ResourceManager.GetString("StateStorage_RequestScopeNotAvailable", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Storage scope is read only..
+ /// </summary>
+ internal static string StateStorage_ScopeIsReadOnly {
+ get {
+ return ResourceManager.GetString("StateStorage_ScopeIsReadOnly", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Storage scopes cannot be created when _AppStart is executing..
+ /// </summary>
+ internal static string StateStorage_StorageScopesCannotBeCreated {
+ get {
+ return ResourceManager.GetString("StateStorage_StorageScopesCannotBeCreated", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Validation parameter names in unobtrusive client validation rules cannot be empty. Client rule type: {0}.
+ /// </summary>
+ internal static string UnobtrusiveJavascript_ValidationParameterCannotBeEmpty {
+ get {
+ return ResourceManager.GetString("UnobtrusiveJavascript_ValidationParameterCannotBeEmpty", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Validation parameter names in unobtrusive client validation rules must start with a lowercase letter and consist of only lowercase letters or digits. Validation parameter name: {0}, client rule type: {1}.
+ /// </summary>
+ internal static string UnobtrusiveJavascript_ValidationParameterMustBeLegal {
+ get {
+ return ResourceManager.GetString("UnobtrusiveJavascript_ValidationParameterMustBeLegal", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Validation type names in unobtrusive client validation rules cannot be empty. Client rule type: {0}.
+ /// </summary>
+ internal static string UnobtrusiveJavascript_ValidationTypeCannotBeEmpty {
+ get {
+ return ResourceManager.GetString("UnobtrusiveJavascript_ValidationTypeCannotBeEmpty", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Validation type names in unobtrusive client validation rules must consist of only lowercase letters. Invalid name: &quot;{0}&quot;, client rule type: {1}.
+ /// </summary>
+ internal static string UnobtrusiveJavascript_ValidationTypeMustBeLegal {
+ get {
+ return ResourceManager.GetString("UnobtrusiveJavascript_ValidationTypeMustBeLegal", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Validation type names in unobtrusive client validation rules must be unique. The following validation type was seen more than once: {0}.
+ /// </summary>
+ internal static string UnobtrusiveJavascript_ValidationTypeMustBeUnique {
+ get {
+ return ResourceManager.GetString("UnobtrusiveJavascript_ValidationTypeMustBeUnique", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The UrlData collection is read-only..
+ /// </summary>
+ internal static string UrlData_ReadOnly {
+ get {
+ return ResourceManager.GetString("UrlData_ReadOnly", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Input format is invalid..
+ /// </summary>
+ internal static string ValidationDefault_DataType {
+ get {
+ return ResourceManager.GetString("ValidationDefault_DataType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Values do not match..
+ /// </summary>
+ internal static string ValidationDefault_EqualsTo {
+ get {
+ return ResourceManager.GetString("ValidationDefault_EqualsTo", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Value must be a decimal between {0} and {1}..
+ /// </summary>
+ internal static string ValidationDefault_FloatRange {
+ get {
+ return ResourceManager.GetString("ValidationDefault_FloatRange", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Value must be an integer between {0} and {1}..
+ /// </summary>
+ internal static string ValidationDefault_IntegerRange {
+ get {
+ return ResourceManager.GetString("ValidationDefault_IntegerRange", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Value is invalid..
+ /// </summary>
+ internal static string ValidationDefault_Regex {
+ get {
+ return ResourceManager.GetString("ValidationDefault_Regex", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to This field is required..
+ /// </summary>
+ internal static string ValidationDefault_Required {
+ get {
+ return ResourceManager.GetString("ValidationDefault_Required", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Max length: {0}..
+ /// </summary>
+ internal static string ValidationDefault_StringLength {
+ get {
+ return ResourceManager.GetString("ValidationDefault_StringLength", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to String must be between {0} and {1} characters..
+ /// </summary>
+ internal static string ValidationDefault_StringLengthRange {
+ get {
+ return ResourceManager.GetString("ValidationDefault_StringLengthRange", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The file &quot;{0}&quot; cannot be requested directly because it calls the &quot;{1}&quot; method..
+ /// </summary>
+ internal static string WebPage_CannotRequestDirectly {
+ get {
+ return ResourceManager.GetString("WebPage_CannotRequestDirectly", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The following file could not be rendered because its extension &quot;{0}&quot; might not be supported: &quot;{1}&quot;..
+ /// </summary>
+ internal static string WebPage_FileNotSupported {
+ get {
+ return ResourceManager.GetString("WebPage_FileNotSupported", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The file &quot;{0}&quot; could not be rendered, because it does not exist or is not a valid page..
+ /// </summary>
+ internal static string WebPage_InvalidPageType {
+ get {
+ return ResourceManager.GetString("WebPage_InvalidPageType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The layout page &quot;{0}&quot; could not be found at the following path: &quot;{1}&quot;..
+ /// </summary>
+ internal static string WebPage_LayoutPageNotFound {
+ get {
+ return ResourceManager.GetString("WebPage_LayoutPageNotFound", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The &quot;RenderBody&quot; method has already been called..
+ /// </summary>
+ internal static string WebPage_RenderBodyAlreadyCalled {
+ get {
+ return ResourceManager.GetString("WebPage_RenderBodyAlreadyCalled", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The &quot;RenderBody&quot; method has not been called for layout page &quot;{0}&quot;..
+ /// </summary>
+ internal static string WebPage_RenderBodyNotCalled {
+ get {
+ return ResourceManager.GetString("WebPage_RenderBodyNotCalled", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Section already defined: &quot;{0}&quot;..
+ /// </summary>
+ internal static string WebPage_SectionAleadyDefined {
+ get {
+ return ResourceManager.GetString("WebPage_SectionAleadyDefined", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The &quot;RenderSection&quot; method has already been called for the section named &quot;{0}&quot;..
+ /// </summary>
+ internal static string WebPage_SectionAleadyRendered {
+ get {
+ return ResourceManager.GetString("WebPage_SectionAleadyRendered", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Section not defined: &quot;{0}&quot;..
+ /// </summary>
+ internal static string WebPage_SectionNotDefined {
+ get {
+ return ResourceManager.GetString("WebPage_SectionNotDefined", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The following sections have been defined but have not been rendered for the layout page &quot;{0}&quot;: &quot;{1}&quot;..
+ /// </summary>
+ internal static string WebPage_SectionsNotRendered {
+ get {
+ return ResourceManager.GetString("WebPage_SectionsNotRendered", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Files with leading underscores (&quot;_&quot;) cannot be served..
+ /// </summary>
+ internal static string WebPageRoute_UnderscoreBlocked {
+ get {
+ return ResourceManager.GetString("WebPageRoute_UnderscoreBlocked", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Resources/WebPageResources.resx b/src/System.Web.WebPages/Resources/WebPageResources.resx
new file mode 100644
index 00000000..e88eb921
--- /dev/null
+++ b/src/System.Web.WebPages/Resources/WebPageResources.resx
@@ -0,0 +1,246 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="AntiForgeryToken_ValidationFailed" xml:space="preserve">
+ <value>A required anti-forgery token was not supplied or was invalid.</value>
+ </data>
+ <data name="ApplicationPart_ModuleAlreadyRegistered" xml:space="preserve">
+ <value>The assembly "{0}" is already registered.</value>
+ </data>
+ <data name="ApplicationPart_ModuleAlreadyRegisteredForVirtualPath" xml:space="preserve">
+ <value>An application module is already registered for virtual path "{0}".</value>
+ </data>
+ <data name="ApplicationPart_ModuleCannotBeFound" xml:space="preserve">
+ <value>Unable to find an application module with the name "{0}".</value>
+ </data>
+ <data name="ApplicationPart_ModuleNotRegistered" xml:space="preserve">
+ <value>The assembly "{0}" is not a registered application module.</value>
+ </data>
+ <data name="ApplicationPart_ResourceNotFound" xml:space="preserve">
+ <value>The resource file "{0}" could not be found.</value>
+ </data>
+ <data name="DynamicDictionary_InvalidNumberOfIndexes" xml:space="preserve">
+ <value>Index length must be exactly one.</value>
+ </data>
+ <data name="DynamicHttpApplicationState_UseOnlyStringOrIntToGet" xml:space="preserve">
+ <value>Index must be of type string or int when getting a value.</value>
+ </data>
+ <data name="DynamicHttpApplicationState_UseOnlyStringToSet" xml:space="preserve">
+ <value>Index must be of type string when setting a value.</value>
+ </data>
+ <data name="HtmlHelper_ConversionThrew" xml:space="preserve">
+ <value>The parameter conversion from type "{0}" to type "{1}" failed. See the inner exception for more information.</value>
+ </data>
+ <data name="HtmlHelper_NoConverterExists" xml:space="preserve">
+ <value>The parameter conversion from type "{0}" to type "{1}" failed because no type converter can convert between these types.</value>
+ </data>
+ <data name="HttpContextUnavailable" xml:space="preserve">
+ <value>An HttpContext is required to perform this operation. Check that this operation is being performed during a web request.</value>
+ </data>
+ <data name="SessionState_InvalidValue" xml:space="preserve">
+ <value>Value "{0}" specified in "{1}" is an invalid value for the SessionState directive. Possible values are: "{2}".</value>
+ </data>
+ <data name="SessionState_TooManyValues" xml:space="preserve">
+ <value>At most one SessionState value can be declared per page.</value>
+ </data>
+ <data name="StateStorage_RequestScopeNotAvailable" xml:space="preserve">
+ <value>RequestScope cannot be created when _AppStart is executing.</value>
+ </data>
+ <data name="StateStorage_ScopeIsReadOnly" xml:space="preserve">
+ <value>Storage scope is read only.</value>
+ </data>
+ <data name="StateStorage_StorageScopesCannotBeCreated" xml:space="preserve">
+ <value>Storage scopes cannot be created when _AppStart is executing.</value>
+ </data>
+ <data name="UnobtrusiveJavascript_ValidationParameterCannotBeEmpty" xml:space="preserve">
+ <value>Validation parameter names in unobtrusive client validation rules cannot be empty. Client rule type: {0}</value>
+ </data>
+ <data name="UnobtrusiveJavascript_ValidationParameterMustBeLegal" xml:space="preserve">
+ <value>Validation parameter names in unobtrusive client validation rules must start with a lowercase letter and consist of only lowercase letters or digits. Validation parameter name: {0}, client rule type: {1}</value>
+ </data>
+ <data name="UnobtrusiveJavascript_ValidationTypeCannotBeEmpty" xml:space="preserve">
+ <value>Validation type names in unobtrusive client validation rules cannot be empty. Client rule type: {0}</value>
+ </data>
+ <data name="UnobtrusiveJavascript_ValidationTypeMustBeLegal" xml:space="preserve">
+ <value>Validation type names in unobtrusive client validation rules must consist of only lowercase letters. Invalid name: "{0}", client rule type: {1}</value>
+ </data>
+ <data name="UnobtrusiveJavascript_ValidationTypeMustBeUnique" xml:space="preserve">
+ <value>Validation type names in unobtrusive client validation rules must be unique. The following validation type was seen more than once: {0}</value>
+ </data>
+ <data name="UrlData_ReadOnly" xml:space="preserve">
+ <value>The UrlData collection is read-only.</value>
+ </data>
+ <data name="ValidationDefault_DataType" xml:space="preserve">
+ <value>Input format is invalid.</value>
+ </data>
+ <data name="ValidationDefault_EqualsTo" xml:space="preserve">
+ <value>Values do not match.</value>
+ </data>
+ <data name="ValidationDefault_FloatRange" xml:space="preserve">
+ <value>Value must be a decimal between {0} and {1}.</value>
+ </data>
+ <data name="ValidationDefault_IntegerRange" xml:space="preserve">
+ <value>Value must be an integer between {0} and {1}.</value>
+ </data>
+ <data name="ValidationDefault_Regex" xml:space="preserve">
+ <value>Value is invalid.</value>
+ </data>
+ <data name="ValidationDefault_Required" xml:space="preserve">
+ <value>This field is required.</value>
+ </data>
+ <data name="ValidationDefault_StringLength" xml:space="preserve">
+ <value>Max length: {0}.</value>
+ </data>
+ <data name="ValidationDefault_StringLengthRange" xml:space="preserve">
+ <value>String must be between {0} and {1} characters.</value>
+ </data>
+ <data name="WebPageRoute_UnderscoreBlocked" xml:space="preserve">
+ <value>Files with leading underscores ("_") cannot be served.</value>
+ </data>
+ <data name="WebPage_CannotRequestDirectly" xml:space="preserve">
+ <value>The file "{0}" cannot be requested directly because it calls the "{1}" method.</value>
+ </data>
+ <data name="WebPage_FileNotSupported" xml:space="preserve">
+ <value>The following file could not be rendered because its extension "{0}" might not be supported: "{1}".</value>
+ </data>
+ <data name="WebPage_InvalidPageType" xml:space="preserve">
+ <value>The file "{0}" could not be rendered, because it does not exist or is not a valid page.</value>
+ </data>
+ <data name="WebPage_LayoutPageNotFound" xml:space="preserve">
+ <value>The layout page "{0}" could not be found at the following path: "{1}".</value>
+ </data>
+ <data name="WebPage_RenderBodyAlreadyCalled" xml:space="preserve">
+ <value>The "RenderBody" method has already been called.</value>
+ </data>
+ <data name="WebPage_RenderBodyNotCalled" xml:space="preserve">
+ <value>The "RenderBody" method has not been called for layout page "{0}".</value>
+ </data>
+ <data name="WebPage_SectionAleadyDefined" xml:space="preserve">
+ <value>Section already defined: "{0}".</value>
+ </data>
+ <data name="WebPage_SectionAleadyRendered" xml:space="preserve">
+ <value>The "RenderSection" method has already been called for the section named "{0}".</value>
+ </data>
+ <data name="WebPage_SectionNotDefined" xml:space="preserve">
+ <value>Section not defined: "{0}".</value>
+ </data>
+ <data name="WebPage_SectionsNotRendered" xml:space="preserve">
+ <value>The following sections have been defined but have not been rendered for the layout page "{0}": "{1}".</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/System.Web.WebPages/ResponseExtensions.cs b/src/System.Web.WebPages/ResponseExtensions.cs
new file mode 100644
index 00000000..66be8154
--- /dev/null
+++ b/src/System.Web.WebPages/ResponseExtensions.cs
@@ -0,0 +1,87 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Net;
+
+namespace System.Web.WebPages
+{
+ public static class ResponseExtensions
+ {
+ public static void SetStatus(this HttpResponseBase response, HttpStatusCode httpStatusCode)
+ {
+ SetStatus(response, (int)httpStatusCode);
+ }
+
+ public static void SetStatus(this HttpResponseBase response, int httpStatusCode)
+ {
+ response.StatusCode = httpStatusCode;
+ response.End();
+ }
+
+ public static void WriteBinary(this HttpResponseBase response, byte[] data, string mimeType)
+ {
+ response.ContentType = mimeType;
+ WriteBinary(response, data);
+ }
+
+ public static void WriteBinary(this HttpResponseBase response, byte[] data)
+ {
+ response.OutputStream.Write(data, 0, data.Length);
+ }
+
+ // REVIEW: See what this is actually calling that's needed
+ // Configure output caching for the request
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "We are not removing optional parameters from helpers")]
+ public static void OutputCache(this HttpResponseBase response,
+ int numberOfSeconds,
+ bool sliding = false,
+ IEnumerable<string> varyByParams = null,
+ IEnumerable<string> varyByHeaders = null,
+ IEnumerable<string> varyByContentEncodings = null,
+ HttpCacheability cacheability = HttpCacheability.Public)
+ {
+ OutputCache(new HttpContextWrapper(HttpContext.Current), response.Cache, numberOfSeconds, sliding, varyByParams, varyByHeaders, varyByContentEncodings,
+ cacheability);
+ }
+
+ internal static void OutputCache(HttpContextBase httpContext,
+ HttpCachePolicyBase cache,
+ int numberOfSeconds,
+ bool sliding,
+ IEnumerable<string> varyByParams,
+ IEnumerable<string> varyByHeaders,
+ IEnumerable<string> varyByContentEncodings,
+ HttpCacheability cacheability)
+ {
+ cache.SetCacheability(cacheability);
+ cache.SetExpires(httpContext.Timestamp.AddSeconds(numberOfSeconds));
+ cache.SetMaxAge(new TimeSpan(0, 0, numberOfSeconds));
+ cache.SetValidUntilExpires(true);
+ cache.SetLastModified(httpContext.Timestamp);
+ cache.SetSlidingExpiration(sliding);
+
+ if (varyByParams != null)
+ {
+ foreach (var p in varyByParams)
+ {
+ cache.VaryByParams[p] = true;
+ }
+ }
+
+ if (varyByHeaders != null)
+ {
+ foreach (var headerName in varyByHeaders)
+ {
+ cache.VaryByHeaders[headerName] = true;
+ }
+ }
+
+ if (varyByContentEncodings != null)
+ {
+ foreach (var contentEncoding in varyByContentEncodings)
+ {
+ cache.VaryByContentEncodings[contentEncoding] = true;
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Scope/ApplicationScopeStorageDictionary.cs b/src/System.Web.WebPages/Scope/ApplicationScopeStorageDictionary.cs
new file mode 100644
index 00000000..bda47296
--- /dev/null
+++ b/src/System.Web.WebPages/Scope/ApplicationScopeStorageDictionary.cs
@@ -0,0 +1,24 @@
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+
+namespace System.Web.WebPages.Scope
+{
+ /// <summary>
+ /// The application level storage context that uses a static dictionary as a backing store.
+ /// </summary>
+ internal class ApplicationScopeStorageDictionary : ScopeStorageDictionary
+ {
+ private static readonly IDictionary<object, object> _innerDictionary =
+ new ConcurrentDictionary<object, object>(ScopeStorageComparer.Instance);
+
+ public ApplicationScopeStorageDictionary()
+ : this(new WebConfigScopeDictionary())
+ {
+ }
+
+ public ApplicationScopeStorageDictionary(WebConfigScopeDictionary webConfigState)
+ : base(baseScope: webConfigState, backingStore: _innerDictionary)
+ {
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Scope/AspNetRequestScopeStorageProvider.cs b/src/System.Web.WebPages/Scope/AspNetRequestScopeStorageProvider.cs
new file mode 100644
index 00000000..f24864ea
--- /dev/null
+++ b/src/System.Web.WebPages/Scope/AspNetRequestScopeStorageProvider.cs
@@ -0,0 +1,108 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Web.WebPages.Resources;
+
+namespace System.Web.WebPages.Scope
+{
+ public class AspNetRequestScopeStorageProvider : IScopeStorageProvider
+ {
+ private static readonly object _pageScopeKey = new object();
+ private static readonly object _requestScopeKey = new object();
+ private readonly HttpContextBase _httpContext;
+ private readonly Func<bool> _appStartExecuted;
+
+ public AspNetRequestScopeStorageProvider()
+ : this(httpContext: null, appStartExecuted: () => WebPageHttpModule.AppStartExecuteCompleted)
+ {
+ }
+
+ internal AspNetRequestScopeStorageProvider(HttpContextBase httpContext, Func<bool> appStartExecuted)
+ {
+ _httpContext = httpContext;
+ _appStartExecuted = appStartExecuted;
+ ApplicationScope = new ApplicationScopeStorageDictionary();
+ }
+
+ [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "The state storage API is designed to allow contexts to be set")]
+ public IDictionary<object, object> CurrentScope
+ {
+ get { return PageScope ?? RequestScopeInternal ?? ApplicationScope; }
+ set
+ {
+ if (!_appStartExecuted())
+ {
+ // Disallow creating new contexts before the start page is executed.
+ // This makes sense because our provider is scoped to a request.
+ throw new InvalidOperationException(WebPageResources.StateStorage_StorageScopesCannotBeCreated);
+ }
+ PageScope = value;
+ }
+ }
+
+ public IDictionary<object, object> GlobalScope
+ {
+ get { return ApplicationScope; }
+ }
+
+ public IDictionary<object, object> ApplicationScope { get; private set; }
+
+ public IDictionary<object, object> RequestScope
+ {
+ get
+ {
+ var requestContext = RequestScopeInternal;
+ if (requestContext == null)
+ {
+ throw new InvalidOperationException(WebPageResources.StateStorage_RequestScopeNotAvailable);
+ }
+ return requestContext;
+ }
+ }
+
+ private HttpContextBase HttpContext
+ {
+ get
+ {
+ // If a http context is specifically provided, use that. Else return the value from System.Web.HttpContext.Current if its available.
+ var currentHttpContext = Web.HttpContext.Current;
+ return _httpContext ?? (currentHttpContext == null ? null : new HttpContextWrapper(currentHttpContext));
+ }
+ }
+
+ private IDictionary<object, object> RequestScopeInternal
+ {
+ get
+ {
+ if (_appStartExecuted())
+ {
+ var requestContext = (IDictionary<object, object>)HttpContext.Items[_requestScopeKey];
+ if (requestContext == null)
+ {
+ HttpContext.Items[_requestScopeKey] = requestContext = new ScopeStorageDictionary(ApplicationScope);
+ }
+ return requestContext;
+ }
+ return null;
+ }
+ }
+
+ private IDictionary<object, object> PageScope
+ {
+ get
+ {
+ if (HttpContext == null)
+ {
+ return null;
+ }
+ return (IDictionary<object, object>)HttpContext.Items[_pageScopeKey];
+ }
+ set
+ {
+ // This call would be guarded by the CurrentContext setter.
+ Debug.Assert(HttpContext != null);
+ HttpContext.Items[_pageScopeKey] = value;
+ }
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Scope/IScopeStorageProvider.cs b/src/System.Web.WebPages/Scope/IScopeStorageProvider.cs
new file mode 100644
index 00000000..61a4276f
--- /dev/null
+++ b/src/System.Web.WebPages/Scope/IScopeStorageProvider.cs
@@ -0,0 +1,13 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Web.WebPages.Scope
+{
+ public interface IScopeStorageProvider
+ {
+ [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "The state storage API is designed to allow contexts to be set")]
+ IDictionary<object, object> CurrentScope { get; set; }
+
+ IDictionary<object, object> GlobalScope { get; }
+ }
+}
diff --git a/src/System.Web.WebPages/Scope/ScopeStorage.cs b/src/System.Web.WebPages/Scope/ScopeStorage.cs
new file mode 100644
index 00000000..ef89718d
--- /dev/null
+++ b/src/System.Web.WebPages/Scope/ScopeStorage.cs
@@ -0,0 +1,38 @@
+using System.Collections.Generic;
+
+namespace System.Web.WebPages.Scope
+{
+ public static class ScopeStorage
+ {
+ private static readonly IScopeStorageProvider _defaultStorageProvider = new StaticScopeStorageProvider();
+ private static IScopeStorageProvider _stateStorageProvider;
+
+ public static IScopeStorageProvider CurrentProvider
+ {
+ get { return _stateStorageProvider ?? _defaultStorageProvider; }
+ set { _stateStorageProvider = value; }
+ }
+
+ public static IDictionary<object, object> CurrentScope
+ {
+ get { return CurrentProvider.CurrentScope; }
+ }
+
+ public static IDictionary<object, object> GlobalScope
+ {
+ get { return CurrentProvider.GlobalScope; }
+ }
+
+ public static IDisposable CreateTransientScope(IDictionary<object, object> context)
+ {
+ var currentContext = CurrentScope;
+ CurrentProvider.CurrentScope = context;
+ return new DisposableAction(() => CurrentProvider.CurrentScope = currentContext); // Return an IDisposable that pops the item back off
+ }
+
+ public static IDisposable CreateTransientScope()
+ {
+ return CreateTransientScope(new ScopeStorageDictionary(baseScope: CurrentScope));
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Scope/ScopeStorageComparer.cs b/src/System.Web.WebPages/Scope/ScopeStorageComparer.cs
new file mode 100644
index 00000000..cdaa9fa2
--- /dev/null
+++ b/src/System.Web.WebPages/Scope/ScopeStorageComparer.cs
@@ -0,0 +1,60 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Web.WebPages.Scope
+{
+ /// <summary>
+ /// Custom comparer for the context dictionaries
+ /// The comparer treats strings as a special case, performing case insesitive comparison.
+ /// This guaratees that we remain consistent throughout the chain of contexts since PageData dictionary
+ /// behaves in this manner.
+ /// </summary>
+ internal class ScopeStorageComparer : IEqualityComparer<object>
+ {
+ private static IEqualityComparer<object> _instance;
+ private readonly IEqualityComparer<object> _defaultComparer = EqualityComparer<object>.Default;
+ private readonly IEqualityComparer<string> _stringComparer = StringComparer.OrdinalIgnoreCase;
+
+ private ScopeStorageComparer()
+ {
+ }
+
+ public static IEqualityComparer<object> Instance
+ {
+ get
+ {
+ if (_instance == null)
+ {
+ _instance = new ScopeStorageComparer();
+ }
+ return _instance;
+ }
+ }
+
+ [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", Target = "xString, yString",
+ Justification = "These names make most sense.")]
+ public new bool Equals(object x, object y)
+ {
+ string xString = x as string;
+ string yString = y as string;
+
+ if ((xString != null) && (yString != null))
+ {
+ return _stringComparer.Equals(xString, yString);
+ }
+
+ return _defaultComparer.Equals(x, y);
+ }
+
+ public int GetHashCode(object obj)
+ {
+ string objString = obj as string;
+ if (objString != null)
+ {
+ return _stringComparer.GetHashCode(objString);
+ }
+
+ return _defaultComparer.GetHashCode(obj);
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Scope/ScopeStorageDictionary.cs b/src/System.Web.WebPages/Scope/ScopeStorageDictionary.cs
new file mode 100644
index 00000000..e305c105
--- /dev/null
+++ b/src/System.Web.WebPages/Scope/ScopeStorageDictionary.cs
@@ -0,0 +1,165 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+
+namespace System.Web.WebPages.Scope
+{
+ public class ScopeStorageDictionary : IDictionary<object, object>
+ {
+ private static readonly StateStorageKeyValueComparer _keyValueComparer = new StateStorageKeyValueComparer();
+ private readonly IDictionary<object, object> _baseScope;
+ private readonly IDictionary<object, object> _backingStore;
+
+ public ScopeStorageDictionary()
+ : this(baseScope: null)
+ {
+ }
+
+ public ScopeStorageDictionary(IDictionary<object, object> baseScope)
+ : this(baseScope: baseScope, backingStore: new Dictionary<object, object>(ScopeStorageComparer.Instance))
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ScopeStorageDictionary"/> class.
+ /// </summary>
+ /// <param name="baseScope">The base scope.</param>
+ /// <param name="backingStore">
+ /// The dictionary to use as a storage. Since the dictionary would be used as-is, we expect the implementer to
+ /// use the same key-value comparison logic as we do here.
+ /// </param>
+ internal ScopeStorageDictionary(IDictionary<object, object> baseScope, IDictionary<object, object> backingStore)
+ {
+ _baseScope = baseScope;
+ _backingStore = backingStore;
+ }
+
+ protected IDictionary<object, object> BackingStore
+ {
+ get { return _backingStore; }
+ }
+
+ protected IDictionary<object, object> BaseScope
+ {
+ get { return _baseScope; }
+ }
+
+ public virtual ICollection<object> Keys
+ {
+ get { return GetItems().Select(item => item.Key).ToList(); }
+ }
+
+ public virtual ICollection<object> Values
+ {
+ get { return GetItems().Select(item => item.Value).ToList(); }
+ }
+
+ public virtual int Count
+ {
+ get { return GetItems().Count(); }
+ }
+
+ public virtual bool IsReadOnly
+ {
+ get { return false; }
+ }
+
+ public object this[object key]
+ {
+ get
+ {
+ object value;
+ TryGetValue(key, out value);
+ return value;
+ }
+ set { SetValue(key, value); }
+ }
+
+ public virtual void SetValue(object key, object value)
+ {
+ _backingStore[key] = value;
+ }
+
+ public virtual bool TryGetValue(object key, out object value)
+ {
+ return _backingStore.TryGetValue(key, out value) || (_baseScope != null && _baseScope.TryGetValue(key, out value));
+ }
+
+ public virtual bool Remove(object key)
+ {
+ return _backingStore.Remove(key);
+ }
+
+ public virtual IEnumerator<KeyValuePair<object, object>> GetEnumerator()
+ {
+ return GetItems().GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ public virtual void Add(object key, object value)
+ {
+ SetValue(key, value);
+ }
+
+ public virtual bool ContainsKey(object key)
+ {
+ return _backingStore.ContainsKey(key) || (_baseScope != null && _baseScope.ContainsKey(key));
+ }
+
+ public virtual void Add(KeyValuePair<object, object> item)
+ {
+ SetValue(item.Key, item.Value);
+ }
+
+ public virtual void Clear()
+ {
+ _backingStore.Clear();
+ }
+
+ public virtual bool Contains(KeyValuePair<object, object> item)
+ {
+ return _backingStore.Contains(item) || (_baseScope != null && _baseScope.Contains(item));
+ }
+
+ public virtual void CopyTo(KeyValuePair<object, object>[] array, int arrayIndex)
+ {
+ GetItems().ToList().CopyTo(array, arrayIndex);
+ }
+
+ public virtual bool Remove(KeyValuePair<object, object> item)
+ {
+ return _backingStore.Remove(item);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This call might be expensive depending on how long the chain of contexts is")]
+ [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This method is implementation specific and is not meant to be exposed as a public API.")]
+ protected virtual IEnumerable<KeyValuePair<object, object>> GetItems()
+ {
+ if (_baseScope == null)
+ {
+ return _backingStore;
+ }
+ return Enumerable.Concat(_backingStore, _baseScope).Distinct(_keyValueComparer);
+ }
+
+ private class StateStorageKeyValueComparer : IEqualityComparer<KeyValuePair<object, object>>
+ {
+ private IEqualityComparer<object> _stateStorageComparer = ScopeStorageComparer.Instance;
+
+ public bool Equals(KeyValuePair<object, object> x, KeyValuePair<object, object> y)
+ {
+ return _stateStorageComparer.Equals(x.Key, y.Key);
+ }
+
+ public int GetHashCode(KeyValuePair<object, object> obj)
+ {
+ return _stateStorageComparer.GetHashCode(obj.Key);
+ }
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Scope/StaticScopeStorageProvider.cs b/src/System.Web.WebPages/Scope/StaticScopeStorageProvider.cs
new file mode 100644
index 00000000..7d368436
--- /dev/null
+++ b/src/System.Web.WebPages/Scope/StaticScopeStorageProvider.cs
@@ -0,0 +1,26 @@
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Web.WebPages.Scope
+{
+ public class StaticScopeStorageProvider : IScopeStorageProvider
+ {
+ private static readonly IDictionary<object, object> _defaultContext =
+ new ScopeStorageDictionary(null, new ConcurrentDictionary<object, object>(ScopeStorageComparer.Instance));
+
+ private IDictionary<object, object> _currentContext;
+
+ [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "The state storage API is designed to allow contexts to be set")]
+ public IDictionary<object, object> CurrentScope
+ {
+ get { return _currentContext ?? _defaultContext; }
+ set { _currentContext = value; }
+ }
+
+ public IDictionary<object, object> GlobalScope
+ {
+ get { return _defaultContext; }
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Scope/WebConfigScopeStorageDictionary.cs b/src/System.Web.WebPages/Scope/WebConfigScopeStorageDictionary.cs
new file mode 100644
index 00000000..7ca79b93
--- /dev/null
+++ b/src/System.Web.WebPages/Scope/WebConfigScopeStorageDictionary.cs
@@ -0,0 +1,115 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Linq;
+using System.Web.Configuration;
+using System.Web.WebPages.Resources;
+
+namespace System.Web.WebPages.Scope
+{
+ internal class WebConfigScopeDictionary : IDictionary<object, object>
+ {
+ private readonly Lazy<Dictionary<object, object>> _items;
+
+ public WebConfigScopeDictionary()
+ : this(WebConfigurationManager.AppSettings)
+ {
+ }
+
+ public WebConfigScopeDictionary(NameValueCollection appSettings)
+ {
+ _items = new Lazy<Dictionary<object, object>>(() => appSettings.AllKeys.ToDictionary(key => key, key => (object)appSettings[key], ScopeStorageComparer.Instance));
+ }
+
+ private IDictionary<object, object> Items
+ {
+ get { return _items.Value; }
+ }
+
+ public ICollection<object> Keys
+ {
+ get { return Items.Keys; }
+ }
+
+ public ICollection<object> Values
+ {
+ get { return Items.Values; }
+ }
+
+ public int Count
+ {
+ get { return Items.Count; }
+ }
+
+ public bool IsReadOnly
+ {
+ get { return true; }
+ }
+
+ public object this[object key]
+ {
+ get
+ {
+ object value;
+ TryGetValue(key, out value);
+ return value;
+ }
+ set { throw new NotSupportedException(WebPageResources.StateStorage_ScopeIsReadOnly); }
+ }
+
+ public bool TryGetValue(object key, out object value)
+ {
+ return Items.TryGetValue(key, out value);
+ }
+
+ public IEnumerator<KeyValuePair<object, object>> GetEnumerator()
+ {
+ return Items.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ public void Add(object key, object value)
+ {
+ throw new NotSupportedException(WebPageResources.StateStorage_ScopeIsReadOnly);
+ }
+
+ public bool ContainsKey(object key)
+ {
+ return Items.ContainsKey(key);
+ }
+
+ public bool Remove(object key)
+ {
+ throw new NotSupportedException(WebPageResources.StateStorage_ScopeIsReadOnly);
+ }
+
+ public void Add(KeyValuePair<object, object> item)
+ {
+ throw new NotSupportedException(WebPageResources.StateStorage_ScopeIsReadOnly);
+ }
+
+ public void Clear()
+ {
+ throw new NotSupportedException(WebPageResources.StateStorage_ScopeIsReadOnly);
+ }
+
+ public bool Contains(KeyValuePair<object, object> item)
+ {
+ return Items.Contains(item);
+ }
+
+ public void CopyTo(KeyValuePair<object, object>[] array, int arrayIndex)
+ {
+ Items.CopyTo(array, arrayIndex);
+ }
+
+ public bool Remove(KeyValuePair<object, object> item)
+ {
+ throw new NotSupportedException(WebPageResources.StateStorage_ScopeIsReadOnly);
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/SectionWriter.cs b/src/System.Web.WebPages/SectionWriter.cs
new file mode 100644
index 00000000..a607d107
--- /dev/null
+++ b/src/System.Web.WebPages/SectionWriter.cs
@@ -0,0 +1,4 @@
+namespace System.Web.WebPages
+{
+ public delegate void SectionWriter();
+}
diff --git a/src/System.Web.WebPages/SecurityUtil.cs b/src/System.Web.WebPages/SecurityUtil.cs
new file mode 100644
index 00000000..cce36be8
--- /dev/null
+++ b/src/System.Web.WebPages/SecurityUtil.cs
@@ -0,0 +1,80 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+using System.Security;
+
+namespace System.Web.WebPages
+{
+ // This code is copied from MVC.
+ internal static class SecurityUtil
+ {
+ private static Action<Action> _callInAppTrustThunk;
+
+ // !! IMPORTANT !!
+ // Do not try to optimize this method or perform any extra caching; doing so could lead to MVC not operating
+ // correctly until the AppDomain is restarted.
+ [SuppressMessage("Microsoft.Security", "CA2107:ReviewDenyAndPermitOnlyUsage",
+ Justification = "This is essentially the same logic as Page.ProcessRequest.")]
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes",
+ Justification = "If an exception is thrown, assume we're running in same trust level as the application itself, so we don't need to do anything special.")]
+ private static Action<Action> GetCallInAppTrustThunk()
+ {
+ // do we need to create the thunk?
+ if (_callInAppTrustThunk == null)
+ {
+ try
+ {
+ if (!typeof(SecurityUtil).Assembly.IsFullyTrusted /* bin-deployed */
+ || AppDomain.CurrentDomain.IsHomogenous /* .NET 4 CAS model */)
+ {
+ // we're already running in the application's trust level, so nothing to do
+ _callInAppTrustThunk = f => f();
+ }
+ else
+ {
+ // legacy CAS model - need to lower own permission level to be compatible with legacy systems
+ // This is essentially the same logic as Page.ProcessRequest(HttpContext)
+ NamedPermissionSet namedPermissionSet = (NamedPermissionSet)typeof(HttpRuntime).GetProperty("NamedPermissionSet", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static).GetValue(null, null);
+ bool disableProcessRequestInApplicationTrust = (bool)typeof(HttpRuntime).GetProperty("DisableProcessRequestInApplicationTrust", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static).GetValue(null, null);
+ if (namedPermissionSet != null && !disableProcessRequestInApplicationTrust)
+ {
+ _callInAppTrustThunk = f =>
+ {
+ // lower permissions
+ namedPermissionSet.PermitOnly();
+ f();
+ };
+ }
+ else
+ {
+ // application's trust level is FullTrust, so nothing to do
+ _callInAppTrustThunk = f => f();
+ }
+ }
+ }
+ catch
+ {
+ // MVC assembly is already running in application trust, so swallow exceptions
+ }
+ }
+
+ // if there was an error, just process transparently
+ return _callInAppTrustThunk ?? (Action<Action>)(f => f());
+ }
+
+ public static TResult ProcessInApplicationTrust<TResult>(Func<TResult> func)
+ {
+ TResult result = default(TResult);
+ ProcessInApplicationTrust(delegate
+ {
+ result = func();
+ });
+ return result;
+ }
+
+ public static void ProcessInApplicationTrust(Action action)
+ {
+ Action<Action> executor = GetCallInAppTrustThunk();
+ executor(action);
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/StartPage.cs b/src/System.Web.WebPages/StartPage.cs
new file mode 100644
index 00000000..d11f00fa
--- /dev/null
+++ b/src/System.Web.WebPages/StartPage.cs
@@ -0,0 +1,172 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.IO;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.WebPages
+{
+ /// <summary>
+ /// Wrapper class to be used by _pagestart.cshtml files to call into
+ /// the actual page.
+ /// Most of the properties and methods just delegate the call to ChildPage.XXX
+ /// </summary>
+ public abstract class StartPage : WebPageRenderingBase
+ {
+ public WebPageRenderingBase ChildPage { get; set; }
+
+ public override HttpContextBase Context
+ {
+ get { return ChildPage.Context; }
+ set { ChildPage.Context = value; }
+ }
+
+ public override string Layout
+ {
+ get { return ChildPage.Layout; }
+ set
+ {
+ if (value == null)
+ {
+ ChildPage.Layout = null;
+ }
+ else
+ {
+ ChildPage.Layout = NormalizeLayoutPagePath(value);
+ }
+ }
+ }
+
+ public override IDictionary<object, dynamic> PageData
+ {
+ get { return ChildPage.PageData; }
+ }
+
+ public override dynamic Page
+ {
+ get { return ChildPage.Page; }
+ }
+
+ internal bool RunPageCalled { get; set; }
+
+ public override void ExecutePageHierarchy()
+ {
+ // Push the current pagestart on the stack.
+ TemplateStack.Push(Context, this);
+ try
+ {
+ // Execute the developer-written code of the InitPage
+ Execute();
+
+ // If the child page wasn't explicitly run by the developer of the InitPage, then run it now.
+ // The child page is either the next InitPage, or the final WebPage.
+ if (!RunPageCalled)
+ {
+ RunPage();
+ }
+ }
+ finally
+ {
+ TemplateStack.Pop(Context);
+ }
+ }
+
+ /// <summary>
+ /// Returns either the root-most init page, or the provided page itself if no init page is found
+ /// </summary>
+ [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters",
+ Justification = "Start Pages are instances of WebPageRenderingBase. It might be possible to have WebPageExecuting bases that are not in the same inheritance tree as StartPages")]
+ public static WebPageRenderingBase GetStartPage(WebPageRenderingBase page, string fileName, IEnumerable<string> supportedExtensions)
+ {
+ if (page == null)
+ {
+ throw new ArgumentNullException("page");
+ }
+ if (String.IsNullOrEmpty(fileName))
+ {
+ throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, CommonResources.Argument_Cannot_Be_Null_Or_Empty, "fileName"), "fileName");
+ }
+ if (supportedExtensions == null)
+ {
+ throw new ArgumentNullException("supportedExtensions");
+ }
+
+ // Use the page's VirtualPathFactory if available
+ return GetStartPage(page, page.VirtualPathFactory ?? VirtualPathFactoryManager.Instance,
+ HttpRuntime.AppDomainAppVirtualPath, fileName, supportedExtensions);
+ }
+
+ internal static WebPageRenderingBase GetStartPage(WebPageRenderingBase page, IVirtualPathFactory virtualPathFactory, string appDomainAppVirtualPath,
+ string fileName, IEnumerable<string> supportedExtensions)
+ {
+ // Build up a list of pages to execute, such as one of the following:
+ // ~/somepage.cshtml
+ // ~/_pageStart.cshtml --> ~/somepage.cshtml
+ // ~/_pageStart.cshtml --> ~/sub/_pageStart.cshtml --> ~/sub/somepage.cshtml
+ WebPageRenderingBase currentPage = page;
+ var pageDirectory = VirtualPathUtility.GetDirectory(page.VirtualPath);
+
+ // Start with the requested page's directory, find the init page,
+ // and then traverse up the hierarchy to find init pages all the
+ // way up to the root of the app.
+ while (!String.IsNullOrEmpty(pageDirectory) && pageDirectory != "/" && PathUtil.IsWithinAppRoot(appDomainAppVirtualPath, pageDirectory))
+ {
+ // Go through the list of supported extensions
+ foreach (var extension in supportedExtensions)
+ {
+ var virtualPath = VirtualPathUtility.Combine(pageDirectory, fileName + "." + extension);
+
+ // Can we build a file from the current path?
+ if (virtualPathFactory.Exists(virtualPath))
+ {
+ var parentStartPage = virtualPathFactory.CreateInstance<StartPage>(virtualPath);
+ parentStartPage.VirtualPath = virtualPath;
+ parentStartPage.ChildPage = currentPage;
+ parentStartPage.VirtualPathFactory = virtualPathFactory;
+ currentPage = parentStartPage;
+
+ break;
+ }
+ }
+
+ pageDirectory = currentPage.GetDirectory(pageDirectory);
+ }
+
+ // At this point 'currentPage' is the root-most StartPage (if there were
+ // any StartPages at all) or it is the requested page itself.
+ return currentPage;
+ }
+
+ public override HelperResult RenderPage(string path, params object[] data)
+ {
+ return ChildPage.RenderPage(NormalizePath(path), data);
+ }
+
+ public void RunPage()
+ {
+ RunPageCalled = true;
+ //ChildPage.PageContext = PageContext;
+ ChildPage.ExecutePageHierarchy();
+ }
+
+ public override void Write(HelperResult result)
+ {
+ ChildPage.Write(result);
+ }
+
+ public override void WriteLiteral(object value)
+ {
+ ChildPage.WriteLiteral(value);
+ }
+
+ public override void Write(object value)
+ {
+ ChildPage.Write(value);
+ }
+
+ protected internal override TextWriter GetOutputWriter()
+ {
+ return ChildPage.GetOutputWriter();
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/StringExtensions.cs b/src/System.Web.WebPages/StringExtensions.cs
new file mode 100644
index 00000000..d5d609f4
--- /dev/null
+++ b/src/System.Web.WebPages/StringExtensions.cs
@@ -0,0 +1,161 @@
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+
+namespace System.Web.WebPages
+{
+ public static class StringExtensions
+ {
+ public static bool IsEmpty(this string value)
+ {
+ return String.IsNullOrEmpty(value);
+ }
+
+ [SuppressMessage("Microsoft.Naming", "CA1720:IdentifiersShouldNotContainTypeNames", MessageId = "int", Justification = "We specificaly want type names")]
+ public static int AsInt(this string value)
+ {
+ return AsInt(value, 0);
+ }
+
+ [SuppressMessage("Microsoft.Naming", "CA1720:IdentifiersShouldNotContainTypeNames", MessageId = "int", Justification = "We specificaly want type names")]
+ public static int AsInt(this string value, int defaultValue)
+ {
+ int result;
+ return Int32.TryParse(value, out result) ? result : defaultValue;
+ }
+
+ public static decimal AsDecimal(this string value)
+ {
+ // Decimal.TryParse does not work consistently for some locales. For instance for lt-LT, it accepts but ignores decimal values so "12.12" is parsed as 1212.
+ return As<Decimal>(value);
+ }
+
+ public static decimal AsDecimal(this string value, decimal defaultValue)
+ {
+ return As<Decimal>(value, defaultValue);
+ }
+
+ [SuppressMessage("Microsoft.Naming", "CA1720:IdentifiersShouldNotContainTypeNames", MessageId = "float", Justification = "We specificaly want type names")]
+ public static float AsFloat(this string value)
+ {
+ return AsFloat(value, default(float));
+ }
+
+ [SuppressMessage("Microsoft.Naming", "CA1720:IdentifiersShouldNotContainTypeNames", MessageId = "float", Justification = "We specificaly want type names")]
+ public static float AsFloat(this string value, float defaultValue)
+ {
+ float result;
+ return Single.TryParse(value, out result) ? result : defaultValue;
+ }
+
+ public static DateTime AsDateTime(this string value)
+ {
+ return AsDateTime(value, default(DateTime));
+ }
+
+ public static DateTime AsDateTime(this string value, DateTime defaultValue)
+ {
+ DateTime result;
+ return DateTime.TryParse(value, out result) ? result : defaultValue;
+ }
+
+ public static TValue As<TValue>(this string value)
+ {
+ return As<TValue>(value, default(TValue));
+ }
+
+ [SuppressMessage("Microsoft.Naming", "CA1720:IdentifiersShouldNotContainTypeNames", MessageId = "bool", Justification = "We specificaly want type names")]
+ public static bool AsBool(this string value)
+ {
+ return AsBool(value, default(bool));
+ }
+
+ [SuppressMessage("Microsoft.Naming", "CA1720:IdentifiersShouldNotContainTypeNames", MessageId = "bool", Justification = "We specificaly want type names")]
+ public static bool AsBool(this string value, bool defaultValue)
+ {
+ bool result;
+ return Boolean.TryParse(value, out result) ? result : defaultValue;
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We want to make this user friendly and return the default value on all failures")]
+ public static TValue As<TValue>(this string value, TValue defaultValue)
+ {
+ try
+ {
+ TypeConverter converter = TypeDescriptor.GetConverter(typeof(TValue));
+ if (converter.CanConvertFrom(typeof(string)))
+ {
+ return (TValue)converter.ConvertFrom(value);
+ }
+ // try the other direction
+ converter = TypeDescriptor.GetConverter(typeof(string));
+ if (converter.CanConvertTo(typeof(TValue)))
+ {
+ return (TValue)converter.ConvertTo(value, typeof(TValue));
+ }
+ }
+ catch
+ {
+ // eat all exceptions and return the defaultValue, assumption is that its always a parse/format exception
+ }
+ return defaultValue;
+ }
+
+ [SuppressMessage("Microsoft.Naming", "CA1720:IdentifiersShouldNotContainTypeNames", MessageId = "bool", Justification = "We specificaly want type names")]
+ public static bool IsBool(this string value)
+ {
+ bool result;
+ return Boolean.TryParse(value, out result);
+ }
+
+ [SuppressMessage("Microsoft.Naming", "CA1720:IdentifiersShouldNotContainTypeNames", MessageId = "int", Justification = "We specificaly want type names")]
+ public static bool IsInt(this string value)
+ {
+ int result;
+ return Int32.TryParse(value, out result);
+ }
+
+ public static bool IsDecimal(this string value)
+ {
+ // For some reason, Decimal.TryParse incorrectly parses floating point values as decimal value for some cultures.
+ // For example, 12.5 is parsed as 125 in lt-LT.
+ return Is<Decimal>(value);
+ }
+
+ [SuppressMessage("Microsoft.Naming", "CA1720:IdentifiersShouldNotContainTypeNames", MessageId = "float", Justification = "We specificaly want type names")]
+ public static bool IsFloat(this string value)
+ {
+ float result;
+ return Single.TryParse(value, out result);
+ }
+
+ public static bool IsDateTime(this string value)
+ {
+ DateTime result;
+ return DateTime.TryParse(value, out result);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "This is the identical to the way it is done in TypeConverter.IsValid"),
+ SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "This is the signature we want")]
+ public static bool Is<TValue>(this string value)
+ {
+ TypeConverter converter = TypeDescriptor.GetConverter(typeof(TValue));
+ if (converter != null)
+ {
+ try
+ {
+ if ((value == null) || converter.CanConvertFrom(null, value.GetType()))
+ {
+ // TypeConverter.IsValid essentially does this - a try catch - but uses InvariantCulture to convert.
+ converter.ConvertFrom(null, CultureInfo.CurrentCulture, value);
+ return true;
+ }
+ }
+ catch
+ {
+ }
+ }
+ return false;
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/System.Web.WebPages.csproj b/src/System.Web.WebPages/System.Web.WebPages.csproj
new file mode 100644
index 00000000..571e2c87
--- /dev/null
+++ b/src/System.Web.WebPages/System.Web.WebPages.csproj
@@ -0,0 +1,269 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <CodeAnalysis Condition=" '$(CodeAnalysis)' == '' ">false</CodeAnalysis>
+ <ProductVersion>9.0.30729</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{76EFA9C5-8D7E-4FDF-B710-E20F8B6B00D2}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>System.Web.WebPages</RootNamespace>
+ <AssemblyName>System.Web.WebPages</AssemblyName>
+ <FileAlignment>512</FileAlignment>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>..\..\bin\Debug\</OutputPath>
+ <DefineConstants>TRACE;DEBUG;ASPNETWEBPAGES</DefineConstants>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ <DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
+ <NoWarn>1591</NoWarn>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>..\..\bin\Release\</OutputPath>
+ <DefineConstants>TRACE;ASPNETWEBPAGES</DefineConstants>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ <RunCodeAnalysis>$(CodeAnalysis)</RunCodeAnalysis>
+ <DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
+ <NoWarn>1591</NoWarn>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'CodeCoverage|AnyCPU'">
+ <DebugSymbols>true</DebugSymbols>
+ <OutputPath>..\..\bin\CodeCoverage\</OutputPath>
+ <DefineConstants>TRACE;DEBUG;CODE_COVERAGE;ASPNETWEBPAGES</DefineConstants>
+ <DebugType>full</DebugType>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="Microsoft.CSharp" />
+ <Reference Include="Microsoft.Web.Infrastructure, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+ <HintPath>..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll</HintPath>
+ </Reference>
+ <Reference Include="System" />
+ <Reference Include="System.ComponentModel.DataAnnotations" />
+ <Reference Include="System.Configuration" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.Data.Linq" />
+ <Reference Include="System.Web" />
+ <Reference Include="System.XML" />
+ <Reference Include="System.Xml.Linq" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="..\CommonAssemblyInfo.cs">
+ <Link>Properties\CommonAssemblyInfo.cs</Link>
+ </Compile>
+ <Compile Include="..\CommonResources.Designer.cs">
+ <Link>Common\CommonResources.Designer.cs</Link>
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>CommonResources.resx</DependentUpon>
+ </Compile>
+ <Compile Include="..\ExceptionHelper.cs">
+ <Link>Common\ExceptionHelper.cs</Link>
+ </Compile>
+ <Compile Include="..\GlobalSuppressions.cs">
+ <Link>Common\GlobalSuppressions.cs</Link>
+ </Compile>
+ <Compile Include="..\HashCodeCombiner.cs">
+ <Link>Common\HashCodeCombiner.cs</Link>
+ </Compile>
+ <Compile Include="..\TransparentCommonAssemblyInfo.cs">
+ <Link>Properties\TransparentCommonAssemblyInfo.cs</Link>
+ </Compile>
+ <Compile Include="ApplicationPart.cs" />
+ <Compile Include="ApplicationParts\ApplicationPartRegistry.cs" />
+ <Compile Include="ApplicationParts\DictionaryBasedVirtualPathFactory.cs" />
+ <Compile Include="ApplicationParts\IResourceAssembly.cs" />
+ <Compile Include="ApplicationParts\LazyAction.cs" />
+ <Compile Include="..\MimeMapping.cs">
+ <Link>Common\MimeMapping.cs</Link>
+ </Compile>
+ <Compile Include="ApplicationParts\ResourceAssembly.cs" />
+ <Compile Include="ApplicationParts\ResourceHandler.cs" />
+ <Compile Include="ApplicationParts\ResourceRouteHandler.cs" />
+ <Compile Include="ApplicationStartPage.cs" />
+ <Compile Include="AttributeValue.cs" />
+ <Compile Include="BrowserOverrideStore.cs" />
+ <Compile Include="BrowserOverrideStores.cs" />
+ <Compile Include="BrowserOverride.cs" />
+ <Compile Include="BuildManagerWrapper.cs" />
+ <Compile Include="CookieBrowserOverrideStore.cs" />
+ <Compile Include="DisplayInfo.cs" />
+ <Compile Include="DisplayModeProvider.cs" />
+ <Compile Include="BrowserHelpers.cs" />
+ <Compile Include="IDisplayMode.cs" />
+ <Compile Include="DefaultDisplayMode.cs" />
+ <Compile Include="FileExistenceCache.cs" />
+ <Compile Include="Helpers\AntiForgeryData.cs" />
+ <Compile Include="Helpers\AntiForgeryDataSerializer.cs" />
+ <Compile Include="Helpers\AntiForgery.cs" />
+ <Compile Include="Helpers\AntiForgeryWorker.cs" />
+ <Compile Include="HttpContextExtensions.cs" />
+ <Compile Include="Instrumentation\HttpContextAdapter.Availability.cs" />
+ <Compile Include="Instrumentation\HttpContextAdapter.generated.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>HttpContextAdapter.tt</DependentUpon>
+ </Compile>
+ <Compile Include="Instrumentation\InstrumentationService.cs" />
+ <Compile Include="Instrumentation\PageExecutionContextAdapter.generated.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>PageExecutionContextAdapter.tt</DependentUpon>
+ </Compile>
+ <Compile Include="Instrumentation\PageExecutionListenerAdapter.generated.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>PageExecutionListenerAdapter.tt</DependentUpon>
+ </Compile>
+ <Compile Include="Instrumentation\PageInstrumentationServiceAdapter.cs" />
+ <Compile Include="Instrumentation\PositionTagged.cs" />
+ <Compile Include="Mvc\ModelClientValidationEqualToRule.cs" />
+ <Compile Include="Mvc\ModelClientValidationRangeRule.cs" />
+ <Compile Include="Mvc\ModelClientValidationRegexRule.cs" />
+ <Compile Include="Mvc\ModelClientValidationRemoteRule.cs" />
+ <Compile Include="Mvc\ModelClientValidationRequiredRule.cs" />
+ <Compile Include="Mvc\ModelClientValidationRule.cs" />
+ <Compile Include="Mvc\ModelClientValidationStringLengthRule.cs" />
+ <Compile Include="Mvc\UnobtrusiveValidationAttributesGenerator.cs" />
+ <Compile Include="RequestBrowserOverrideStore.cs" />
+ <Compile Include="Utils\SessionStateUtil.cs" />
+ <Compile Include="Utils\TypeHelper.cs" />
+ <Compile Include="Utils\UrlUtil.cs" />
+ <Compile Include="Validation\CompareValidator.cs" />
+ <Compile Include="Validation\DataTypeValidator.cs" />
+ <Compile Include="Validation\IValidator.cs" />
+ <Compile Include="Validation\RequestFieldValidatorBase.cs" />
+ <Compile Include="Validation\ValidationAttributeAdapter.cs" />
+ <Compile Include="Validation\ValidationHelper.cs" />
+ <Compile Include="Validation\Validator.cs" />
+ <Compile Include="VirtualPathFactoryExtensions.cs" />
+ <Compile Include="Mvc\HttpAntiForgeryException.cs" />
+ <Compile Include="RequestExtensions.cs" />
+ <Compile Include="Helpers\UnvalidatedRequestValues.cs" />
+ <Compile Include="Helpers\Validation.cs" />
+ <Compile Include="ITemplateFile.cs" />
+ <Compile Include="Mvc\TagBuilder.cs" />
+ <Compile Include="Mvc\TagRenderMode.cs" />
+ <Compile Include="StartPage.cs" />
+ <Compile Include="RequestResourceTracker.cs" />
+ <Compile Include="SecurityUtil.cs" />
+ <Compile Include="DynamicHttpApplicationState.cs" />
+ <Compile Include="DynamicPageDataDictionary.cs" />
+ <Compile Include="Html\HtmlHelper.Checkbox.cs" />
+ <Compile Include="Html\HtmlHelper.cs" />
+ <Compile Include="Html\HtmlHelper.Input.cs" />
+ <Compile Include="Html\HtmlHelper.Internal.cs" />
+ <Compile Include="Html\HtmlHelper.Label.cs" />
+ <Compile Include="Html\HtmlHelper.Radio.cs" />
+ <Compile Include="Html\HtmlHelper.Select.cs" />
+ <Compile Include="Html\HtmlHelper.TextArea.cs" />
+ <Compile Include="Html\HtmlHelper.Validation.cs" />
+ <Compile Include="Html\SelectListItem.cs" />
+ <Compile Include="IVirtualPathFactory.cs" />
+ <Compile Include="Scope\ApplicationScopeStorageDictionary.cs" />
+ <Compile Include="Scope\AspNetRequestScopeStorageProvider.cs" />
+ <Compile Include="Common\DisposableAction.cs" />
+ <Compile Include="Scope\IScopeStorageProvider.cs" />
+ <Compile Include="Scope\ScopeStorage.cs" />
+ <Compile Include="Scope\ScopeStorageComparer.cs" />
+ <Compile Include="Scope\ScopeStorageDictionary.cs" />
+ <Compile Include="Scope\StaticScopeStorageProvider.cs" />
+ <Compile Include="Scope\WebConfigScopeStorageDictionary.cs" />
+ <Compile Include="TemplateFileInfo.cs" />
+ <Compile Include="TemplateStack.cs" />
+ <Compile Include="Utils\CultureUtil.cs" />
+ <Compile Include="Utils\PathUtil.cs" />
+ <Compile Include="WebPageHttpModule.cs" />
+ <Compile Include="HelperPage.cs" />
+ <Compile Include="HelperResult.cs" />
+ <Compile Include="IWebPageRequestExecutor.cs" />
+ <Compile Include="Html\ModelState.cs" />
+ <Compile Include="Html\ModelStateDictionary.cs" />
+ <Compile Include="WebPageRoute.cs" />
+ <Compile Include="WebPageContext.cs" />
+ <Compile Include="SectionWriter.cs" />
+ <Compile Include="WebPageExecutingBase.cs" />
+ <Compile Include="WebPageRenderingBase.cs" />
+ <Compile Include="ReflectionDynamicObject.cs" />
+ <Compile Include="ResponseExtensions.cs" />
+ <Compile Include="PageDataDictionary.cs" />
+ <Compile Include="PageVirtualPathAttribute.cs" />
+ <Compile Include="UrlDataList.cs" />
+ <Compile Include="StringExtensions.cs" />
+ <Compile Include="GlobalSuppressions.cs" />
+ <Compile Include="Resources\WebPageResources.Designer.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>WebPageResources.resx</DependentUpon>
+ </Compile>
+ <Compile Include="WebPageMatch.cs" />
+ <Compile Include="VirtualPathFactoryManager.cs" />
+ <Compile Include="WebPageBase.cs" />
+ <Compile Include="WebPageHttpHandler.cs" />
+ <Compile Include="PreApplicationStartCode.cs" />
+ <Compile Include="Utils\BuildManagerExceptionUtil.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="WebPage.cs" />
+ <Compile Include="..\IVirtualPathUtility.cs">
+ <Link>Common\IVirtualPathUtility.cs</Link>
+ </Compile>
+ <Compile Include="..\VirtualPathUtilityWrapper.cs">
+ <Link>Common\VirtualPathUtilityWrapper.cs</Link>
+ </Compile>
+ </ItemGroup>
+ <ItemGroup>
+ <Service Include="{508349B6-6B84-4DF5-91F0-309BEEBAD82D}" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="..\CommonResources.resx">
+ <Link>Common\CommonResources.resx</Link>
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>CommonResources.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Include="Resources\WebPageResources.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>WebPageResources.Designer.cs</LastGenOutput>
+ <SubType>Designer</SubType>
+ </EmbeddedResource>
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\System.Web.Razor\System.Web.Razor.csproj">
+ <Project>{8F18041B-9410-4C36-A9C5-067813DF5F31}</Project>
+ <Name>System.Web.Razor</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\System.Web.WebPages.Deployment\System.Web.WebPages.Deployment.csproj">
+ <Project>{22BABB60-8F02-4027-AFFC-ACF069954536}</Project>
+ <Name>System.Web.WebPages.Deployment</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="Instrumentation\HttpContextAdapter.tt">
+ <Generator>TextTemplatingFileGenerator</Generator>
+ <LastGenOutput>HttpContextAdapter.generated.cs</LastGenOutput>
+ </None>
+ <None Include="Instrumentation\PageExecutionContextAdapter.tt">
+ <Generator>TextTemplatingFileGenerator</Generator>
+ <LastGenOutput>PageExecutionContextAdapter.generated.cs</LastGenOutput>
+ </None>
+ <None Include="Instrumentation\PageExecutionListenerAdapter.tt">
+ <Generator>TextTemplatingFileGenerator</Generator>
+ <LastGenOutput>PageExecutionListenerAdapter.generated.cs</LastGenOutput>
+ </None>
+ <None Include="packages.config" />
+ </ItemGroup>
+ <ItemGroup>
+ <CodeAnalysisDictionary Include="..\CodeAnalysisDictionary.xml" />
+ </ItemGroup>
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+</Project> \ No newline at end of file
diff --git a/src/System.Web.WebPages/TemplateFileInfo.cs b/src/System.Web.WebPages/TemplateFileInfo.cs
new file mode 100644
index 00000000..a6c55bef
--- /dev/null
+++ b/src/System.Web.WebPages/TemplateFileInfo.cs
@@ -0,0 +1,21 @@
+namespace System.Web.WebPages
+{
+ /// <summary>
+ /// TemplateFileInfo specifies properties of a template such as VirtualPath.
+ /// This type allows us to modify the behavior of ITemplateFile between releases without changing the interface.
+ /// </summary>
+ public class TemplateFileInfo
+ {
+ private readonly string _virtualPath;
+
+ public TemplateFileInfo(string virtualPath)
+ {
+ _virtualPath = virtualPath;
+ }
+
+ public string VirtualPath
+ {
+ get { return _virtualPath; }
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/TemplateStack.cs b/src/System.Web.WebPages/TemplateStack.cs
new file mode 100644
index 00000000..9d1fb29b
--- /dev/null
+++ b/src/System.Web.WebPages/TemplateStack.cs
@@ -0,0 +1,59 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+
+namespace System.Web.WebPages
+{
+ /// <summary>
+ /// Template stacks store a stack of template files. WebPageExecutingBase implements this type, so when executing Plan9 or Mvc WebViewPage,
+ /// the stack would contain instances of the page.
+ /// The stack can be queried to identify properties of the current executing file such as the virtual path of the file.
+ /// </summary>
+ [SuppressMessage("Microsoft.Naming", "CA1711:IdentifiersShouldNotHaveIncorrectSuffix", Justification = "TemplateStack is a stack")]
+ public static class TemplateStack
+ {
+ private static readonly object _contextKey = new object();
+
+ public static ITemplateFile GetCurrentTemplate(HttpContextBase httpContext)
+ {
+ if (httpContext == null)
+ {
+ throw new ArgumentNullException("httpContext");
+ }
+ return GetStack(httpContext).FirstOrDefault();
+ }
+
+ public static ITemplateFile Pop(HttpContextBase httpContext)
+ {
+ if (httpContext == null)
+ {
+ throw new ArgumentNullException("httpContext");
+ }
+ return GetStack(httpContext).Pop();
+ }
+
+ public static void Push(HttpContextBase httpContext, ITemplateFile templateFile)
+ {
+ if (templateFile == null)
+ {
+ throw new ArgumentNullException("templateFile");
+ }
+ if (httpContext == null)
+ {
+ throw new ArgumentNullException("httpContext");
+ }
+ GetStack(httpContext).Push(templateFile);
+ }
+
+ private static Stack<ITemplateFile> GetStack(HttpContextBase httpContext)
+ {
+ var stack = httpContext.Items[_contextKey] as Stack<ITemplateFile>;
+ if (stack == null)
+ {
+ stack = new Stack<ITemplateFile>();
+ httpContext.Items[_contextKey] = stack;
+ }
+ return stack;
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/UrlDataList.cs b/src/System.Web.WebPages/UrlDataList.cs
new file mode 100644
index 00000000..fa65ef10
--- /dev/null
+++ b/src/System.Web.WebPages/UrlDataList.cs
@@ -0,0 +1,99 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Web.WebPages.Resources;
+
+namespace System.Web.WebPages
+{
+ // Wrapper for list that lets us return empty string for non existant pieces of the Url
+ internal class UrlDataList : IList<string>
+ {
+ private List<string> _urlData;
+
+ public UrlDataList(string pathInfo)
+ {
+ if (String.IsNullOrEmpty(pathInfo))
+ {
+ _urlData = new List<string>();
+ }
+ else
+ {
+ _urlData = pathInfo.Split(new char[] { '/' }).ToList();
+ }
+ }
+
+ public int Count
+ {
+ get { return _urlData.Count; }
+ }
+
+ public bool IsReadOnly
+ {
+ get { return true; }
+ }
+
+ public string this[int index]
+ {
+ get
+ {
+ // REVIEW: what about index < 0
+ if (index >= _urlData.Count)
+ {
+ return String.Empty;
+ }
+ return _urlData[index];
+ }
+ set { throw new NotSupportedException(WebPageResources.UrlData_ReadOnly); }
+ }
+
+ public int IndexOf(string item)
+ {
+ return _urlData.IndexOf(item);
+ }
+
+ public void Insert(int index, string item)
+ {
+ throw new NotSupportedException(WebPageResources.UrlData_ReadOnly);
+ }
+
+ public void RemoveAt(int index)
+ {
+ throw new NotSupportedException(WebPageResources.UrlData_ReadOnly);
+ }
+
+ public void Add(string item)
+ {
+ throw new NotSupportedException(WebPageResources.UrlData_ReadOnly);
+ }
+
+ public void Clear()
+ {
+ throw new NotSupportedException(WebPageResources.UrlData_ReadOnly);
+ }
+
+ public bool Contains(string item)
+ {
+ return _urlData.Contains(item);
+ }
+
+ public void CopyTo(string[] array, int arrayIndex)
+ {
+ _urlData.CopyTo(array, arrayIndex);
+ }
+
+ public bool Remove(string item)
+ {
+ throw new NotSupportedException(WebPageResources.UrlData_ReadOnly);
+ }
+
+ public IEnumerator<string> GetEnumerator()
+ {
+ return _urlData.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return _urlData.GetEnumerator();
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Utils/BuildManagerExceptionUtil.cs b/src/System.Web.WebPages/Utils/BuildManagerExceptionUtil.cs
new file mode 100644
index 00000000..0d256364
--- /dev/null
+++ b/src/System.Web.WebPages/Utils/BuildManagerExceptionUtil.cs
@@ -0,0 +1,51 @@
+using System.Globalization;
+using System.IO;
+using System.Web.WebPages.Resources;
+using Microsoft.Web.Infrastructure;
+
+namespace System.Web.WebPages
+{
+ internal static class BuildManagerExceptionUtil
+ {
+ // Checks the exception to see if it is from CompilationUtil.GetBuildProviderTypeFromExtension, which will throw
+ // an exception about an unsupported extension.
+ // Actual error format: There is no build provider registered for the extension '.txt'. You can register one in the <compilation><buildProviders> section in machine.config or web.config. Make sure is has a BuildProviderAppliesToAttribute attribute which includes the value 'Web' or 'All'.
+ internal static bool IsUnsupportedExtensionError(HttpException e)
+ {
+ Exception exception = e;
+
+ // Go through the layers of exceptions to find if any of them is from GetBuildProviderTypeFromExtension
+ while (exception != null)
+ {
+ var site = exception.TargetSite;
+ if (site != null && site.Name == "GetBuildProviderTypeFromExtension" && site.DeclaringType != null && site.DeclaringType.Name == "CompilationUtil")
+ {
+ return true;
+ }
+ exception = exception.InnerException;
+ }
+ return false;
+ }
+
+ internal static void ThrowIfUnsupportedExtension(string virtualPath, HttpException e)
+ {
+ if (IsUnsupportedExtensionError(e))
+ {
+ var extension = Path.GetExtension(virtualPath);
+ throw new HttpException(String.Format(CultureInfo.CurrentCulture, WebPageResources.WebPage_FileNotSupported, extension, virtualPath));
+ }
+ }
+
+ internal static void ThrowIfCodeDomDefinedExtension(string virtualPath, HttpException e)
+ {
+ if (e is HttpCompileException)
+ {
+ var extension = Path.GetExtension(virtualPath);
+ if (InfrastructureHelper.IsCodeDomDefinedExtension(extension))
+ {
+ throw new HttpException(String.Format(CultureInfo.CurrentCulture, WebPageResources.WebPage_FileNotSupported, extension, virtualPath));
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Utils/CultureUtil.cs b/src/System.Web.WebPages/Utils/CultureUtil.cs
new file mode 100644
index 00000000..28c38399
--- /dev/null
+++ b/src/System.Web.WebPages/Utils/CultureUtil.cs
@@ -0,0 +1,73 @@
+using System.Diagnostics;
+using System.Globalization;
+using System.Linq;
+using System.Threading;
+
+namespace System.Web.WebPages
+{
+ internal static class CultureUtil
+ {
+ internal static void SetCulture(Thread thread, HttpContextBase context, string cultureName)
+ {
+ Debug.Assert(!String.IsNullOrEmpty(cultureName));
+ CultureInfo cultureInfo = GetCulture(context, cultureName);
+ if (cultureInfo != null)
+ {
+ thread.CurrentCulture = cultureInfo;
+ }
+ }
+
+ internal static void SetUICulture(Thread thread, HttpContextBase context, string cultureName)
+ {
+ Debug.Assert(!String.IsNullOrEmpty(cultureName));
+ CultureInfo cultureInfo = GetCulture(context, cultureName);
+ if (cultureInfo != null)
+ {
+ thread.CurrentUICulture = cultureInfo;
+ }
+ }
+
+ private static CultureInfo GetCulture(HttpContextBase context, string cultureName)
+ {
+ if (cultureName.Equals("auto", StringComparison.OrdinalIgnoreCase))
+ {
+ return DetermineAutoCulture(context);
+ }
+ else
+ {
+ return CultureInfo.GetCultureInfo(cultureName);
+ }
+ }
+
+ private static CultureInfo DetermineAutoCulture(HttpContextBase context)
+ {
+ HttpRequestBase request = context.Request;
+ Debug.Assert(request != null); //This call is made from a WebPageExecutingBase. Request can never be null when inside a page.
+ CultureInfo culture = null;
+
+ if (request.UserLanguages != null)
+ {
+ string userLanguageEntry = request.UserLanguages.FirstOrDefault();
+ if (!String.IsNullOrWhiteSpace(userLanguageEntry))
+ {
+ // Check if user language has q parameter. E.g. something like this: "as-IN;q=0.3"
+ int index = userLanguageEntry.IndexOf(';');
+ if (index != -1)
+ {
+ userLanguageEntry = userLanguageEntry.Substring(0, index);
+ }
+
+ try
+ {
+ culture = new CultureInfo(userLanguageEntry);
+ }
+ catch (CultureNotFoundException)
+ {
+ // There is no easy way to ask if a given culture is invalid so we have to handle exception.
+ }
+ }
+ }
+ return culture;
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Utils/PathUtil.cs b/src/System.Web.WebPages/Utils/PathUtil.cs
new file mode 100644
index 00000000..913ece6b
--- /dev/null
+++ b/src/System.Web.WebPages/Utils/PathUtil.cs
@@ -0,0 +1,73 @@
+using System.IO;
+
+namespace System.Web.WebPages
+{
+ internal static class PathUtil
+ {
+ /// <summary>
+ /// Path.GetExtension performs a CheckInvalidPathChars(path) which blows up for paths that do not translate to valid physical paths but are valid paths in ASP.NET
+ /// This method is a near clone of Path.GetExtension without a call to CheckInvalidPathChars(path);
+ /// </summary>
+ internal static string GetExtension(string path)
+ {
+ if (String.IsNullOrEmpty(path))
+ {
+ return path;
+ }
+ int current = path.Length;
+ while (--current >= 0)
+ {
+ char ch = path[current];
+ if (ch == '.')
+ {
+ if (current == path.Length - 1)
+ {
+ break;
+ }
+ return path.Substring(current);
+ }
+ if (ch == Path.DirectorySeparatorChar || ch == Path.AltDirectorySeparatorChar)
+ {
+ break;
+ }
+ }
+ return String.Empty;
+ }
+
+ internal static bool IsWithinAppRoot(string appDomainAppVirtualPath, string virtualPath)
+ {
+ if (appDomainAppVirtualPath == null)
+ {
+ // If the runtime has not been initialized, just return true.
+ return true;
+ }
+
+ var absPath = virtualPath;
+ if (!VirtualPathUtility.IsAbsolute(absPath))
+ {
+ absPath = VirtualPathUtility.ToAbsolute(absPath);
+ }
+ // We need to call this overload because it returns null if the path is not within the application root.
+ // The overload calls into MakeVirtualPathAppRelative(string virtualPath, string applicationPath, bool nullIfNotInApp), with
+ // nullIfNotInApp set to true.
+ return VirtualPathUtility.ToAppRelative(absPath, appDomainAppVirtualPath) != null;
+ }
+
+ /// <summary>
+ /// Determines true if the path is simply "MyPath", and not app-relative "~/MyPath" or absolute "/MyApp/MyPath" or relative "../Test/MyPath"
+ /// </summary>
+ /// <returns>True if it is a not app-relative, absolute or relative.</returns>
+ internal static bool IsSimpleName(string path)
+ {
+ if (VirtualPathUtility.IsAbsolute(path) || VirtualPathUtility.IsAppRelative(path))
+ {
+ return false;
+ }
+ if (path.StartsWith(".", StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+ return true;
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Utils/SessionStateUtil.cs b/src/System.Web.WebPages/Utils/SessionStateUtil.cs
new file mode 100644
index 00000000..bb3cfe18
--- /dev/null
+++ b/src/System.Web.WebPages/Utils/SessionStateUtil.cs
@@ -0,0 +1,81 @@
+using System.Collections.Concurrent;
+using System.Diagnostics;
+using System.Globalization;
+using System.Linq;
+using System.Web.Razor;
+using System.Web.SessionState;
+using System.Web.WebPages.Resources;
+
+namespace System.Web.WebPages
+{
+ internal static class SessionStateUtil
+ {
+ private static readonly ConcurrentDictionary<Type, SessionStateBehavior?> _sessionStateBehaviorCache = new ConcurrentDictionary<Type, SessionStateBehavior?>();
+
+ internal static void SetUpSessionState(HttpContextBase context, IHttpHandler handler)
+ {
+ SetUpSessionState(context, handler, _sessionStateBehaviorCache);
+ }
+
+ internal static void SetUpSessionState(HttpContextBase context, IHttpHandler handler, ConcurrentDictionary<Type, SessionStateBehavior?> cache)
+ {
+ WebPageHttpHandler webPageHandler = handler as WebPageHttpHandler;
+ Debug.Assert(handler != null);
+ SessionStateBehavior? sessionState = GetSessionStateBehavior(webPageHandler.RequestedPage, cache);
+
+ if (sessionState != null)
+ {
+ // If the page explicitly specifies a session state value, return since it has the most priority.
+ context.SetSessionStateBehavior(sessionState.Value);
+ return;
+ }
+
+ WebPageRenderingBase page = webPageHandler.StartPage;
+ StartPage startPage = null;
+ do
+ {
+ // Drill down _AppStart and _PageStart.
+ startPage = page as StartPage;
+ if (startPage != null)
+ {
+ sessionState = GetSessionStateBehavior(page, cache);
+ page = startPage.ChildPage;
+ }
+ }
+ while (startPage != null);
+
+ if (sessionState != null)
+ {
+ context.SetSessionStateBehavior(sessionState.Value);
+ }
+ }
+
+ private static SessionStateBehavior? GetSessionStateBehavior(WebPageExecutingBase page, ConcurrentDictionary<Type, SessionStateBehavior?> cache)
+ {
+ return cache.GetOrAdd(page.GetType(), type =>
+ {
+ SessionStateBehavior sessionStateBehavior = SessionStateBehavior.Default;
+ var attributes = (RazorDirectiveAttribute[])type.GetCustomAttributes(typeof(RazorDirectiveAttribute), inherit: false);
+ var directiveAttributes = attributes.Where(attr => StringComparer.OrdinalIgnoreCase.Equals("sessionstate", attr.Name))
+ .ToList();
+
+ if (!directiveAttributes.Any())
+ {
+ return null;
+ }
+ if (directiveAttributes.Count > 1)
+ {
+ throw new InvalidOperationException(WebPageResources.SessionState_TooManyValues);
+ }
+ var directiveAttribute = directiveAttributes[0];
+ if (!Enum.TryParse<SessionStateBehavior>(directiveAttribute.Value, ignoreCase: true, result: out sessionStateBehavior))
+ {
+ var values = Enum.GetValues(typeof(SessionStateBehavior)).Cast<SessionStateBehavior>().Select(s => s.ToString());
+ throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, WebPageResources.SessionState_InvalidValue,
+ directiveAttribute.Value, page.VirtualPath, String.Join(", ", values)));
+ }
+ return sessionStateBehavior;
+ });
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Utils/TypeHelper.cs b/src/System.Web.WebPages/Utils/TypeHelper.cs
new file mode 100644
index 00000000..2106e5a0
--- /dev/null
+++ b/src/System.Web.WebPages/Utils/TypeHelper.cs
@@ -0,0 +1,45 @@
+using System.Collections.Generic;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Web.Routing;
+
+namespace System.Web.WebPages
+{
+ internal static class TypeHelper
+ {
+ /// <summary>
+ /// Given an object of anonymous type, add each property as a key and associated with its value to a dictionary.
+ /// </summary>
+ internal static IDictionary<string, object> ObjectToDictionary(object value)
+ {
+ return new RouteValueDictionary(value);
+ }
+
+ /// <summary>
+ /// Given an object of anonymous type, add each property as a key and associated with its value to the given dictionary.
+ /// </summary>
+ internal static void AddAnonymousObjectToDictionary(IDictionary<string, object> dictionary, object value)
+ {
+ var values = ObjectToDictionary(value);
+ foreach (var item in values)
+ {
+ dictionary.Add(item);
+ }
+ }
+
+ /// <remarks>This code is copied from http://www.liensberger.it/web/blog/?p=191 </remarks>
+ internal static bool IsAnonymousType(Type type)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+
+ // TODO: The only way to detect anonymous types right now.
+ return Attribute.IsDefined(type, typeof(CompilerGeneratedAttribute), false)
+ && type.IsGenericType && type.Name.Contains("AnonymousType")
+ && (type.Name.StartsWith("<>", StringComparison.OrdinalIgnoreCase) || type.Name.StartsWith("VB$", StringComparison.OrdinalIgnoreCase))
+ && (type.Attributes & TypeAttributes.NotPublic) == TypeAttributes.NotPublic;
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Utils/UrlUtil.cs b/src/System.Web.WebPages/Utils/UrlUtil.cs
new file mode 100644
index 00000000..676a670d
--- /dev/null
+++ b/src/System.Web.WebPages/Utils/UrlUtil.cs
@@ -0,0 +1,69 @@
+using System.Globalization;
+using System.Text;
+using System.Web.Routing;
+
+namespace System.Web.WebPages
+{
+ internal static class UrlUtil
+ {
+ internal static string Url(string basePath, string path, params object[] pathParts)
+ {
+ if (basePath != null)
+ {
+ path = VirtualPathUtility.Combine(basePath, path);
+ }
+
+ // Make sure it's not a ~/ path, which the client couldn't handle
+ path = VirtualPathUtility.ToAbsolute(path);
+
+ return BuildUrl(path, pathParts);
+ }
+
+ internal static string BuildUrl(string path, params object[] pathParts)
+ {
+ path = HttpUtility.UrlPathEncode(path);
+ StringBuilder queryString = new StringBuilder();
+
+ foreach (var pathPart in pathParts)
+ {
+ Type partType = pathPart.GetType();
+ if (IsDisplayableType(partType))
+ {
+ var displayablePath = Convert.ToString(pathPart, CultureInfo.InvariantCulture);
+ path += "/" + HttpUtility.UrlPathEncode(displayablePath);
+ }
+ else
+ {
+ // If it smells like an anonymous object, treat it as query string name/value pairs instead of path info parts
+ // REVIEW: this is hacky!
+ var dictionary = new RouteValueDictionary(pathPart);
+ foreach (var item in dictionary)
+ {
+ if (queryString.Length == 0)
+ {
+ queryString.Append('?');
+ }
+ else
+ {
+ queryString.Append('&');
+ }
+
+ string stringValue = Convert.ToString(item.Value, CultureInfo.InvariantCulture);
+
+ queryString.Append(HttpUtility.UrlEncode(item.Key))
+ .Append('=')
+ .Append(HttpUtility.UrlEncode(stringValue));
+ }
+ }
+ }
+ return path + queryString;
+ }
+
+ private static bool IsDisplayableType(Type t)
+ {
+ // If it doesn't support any interfaces (e.g. IFormattable), we probably can't display it. It's likely an anonymous type.
+ // REVIEW: this is hacky!
+ return t.GetInterfaces().Length > 0;
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Validation/CompareValidator.cs b/src/System.Web.WebPages/Validation/CompareValidator.cs
new file mode 100644
index 00000000..7a371c40
--- /dev/null
+++ b/src/System.Web.WebPages/Validation/CompareValidator.cs
@@ -0,0 +1,30 @@
+using System.Diagnostics;
+using System.Web.Mvc;
+
+namespace System.Web.WebPages
+{
+ internal class CompareValidator : RequestFieldValidatorBase
+ {
+ private readonly string _otherField;
+ private readonly ModelClientValidationEqualToRule _clientValidationRule;
+
+ public CompareValidator(string otherField, string errorMessage)
+ : base(errorMessage)
+ {
+ Debug.Assert(!String.IsNullOrEmpty(otherField));
+ _otherField = otherField;
+ _clientValidationRule = new ModelClientValidationEqualToRule(errorMessage, otherField);
+ }
+
+ public override ModelClientValidationRule ClientValidationRule
+ {
+ get { return _clientValidationRule; }
+ }
+
+ protected override bool IsValid(HttpContextBase httpContext, string value)
+ {
+ string otherValue = GetRequestValue(httpContext.Request, _otherField);
+ return String.Equals(value, otherValue, StringComparison.CurrentCulture);
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Validation/DataTypeValidator.cs b/src/System.Web.WebPages/Validation/DataTypeValidator.cs
new file mode 100644
index 00000000..b91319b9
--- /dev/null
+++ b/src/System.Web.WebPages/Validation/DataTypeValidator.cs
@@ -0,0 +1,45 @@
+namespace System.Web.WebPages
+{
+ internal class DataTypeValidator : RequestFieldValidatorBase
+ {
+ private readonly SupportedValidationDataType _dataType;
+
+ public DataTypeValidator(SupportedValidationDataType type, string errorMessage = null)
+ : base(errorMessage)
+ {
+ _dataType = type;
+ }
+
+ public enum SupportedValidationDataType
+ {
+ DateTime,
+ Decimal,
+ Url,
+ Integer,
+ Float
+ }
+
+ protected override bool IsValid(HttpContextBase httpContext, string value)
+ {
+ if (String.IsNullOrEmpty(value))
+ {
+ return true;
+ }
+
+ switch (_dataType)
+ {
+ case SupportedValidationDataType.DateTime:
+ return value.IsDateTime();
+ case SupportedValidationDataType.Float:
+ return value.IsFloat();
+ case SupportedValidationDataType.Decimal:
+ return value.IsDecimal();
+ case SupportedValidationDataType.Integer:
+ return value.IsInt();
+ case SupportedValidationDataType.Url:
+ return Uri.IsWellFormedUriString(value, UriKind.Absolute);
+ }
+ return true;
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Validation/IValidator.cs b/src/System.Web.WebPages/Validation/IValidator.cs
new file mode 100644
index 00000000..979c8c27
--- /dev/null
+++ b/src/System.Web.WebPages/Validation/IValidator.cs
@@ -0,0 +1,11 @@
+using System.ComponentModel.DataAnnotations;
+using System.Web.Mvc;
+
+namespace System.Web.WebPages
+{
+ public interface IValidator
+ {
+ ModelClientValidationRule ClientValidationRule { get; }
+ ValidationResult Validate(ValidationContext validationContext);
+ }
+}
diff --git a/src/System.Web.WebPages/Validation/RequestFieldValidatorBase.cs b/src/System.Web.WebPages/Validation/RequestFieldValidatorBase.cs
new file mode 100644
index 00000000..d77107d5
--- /dev/null
+++ b/src/System.Web.WebPages/Validation/RequestFieldValidatorBase.cs
@@ -0,0 +1,72 @@
+using System.ComponentModel.DataAnnotations;
+using System.Diagnostics;
+using System.Web.Helpers;
+using System.Web.Mvc;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.WebPages
+{
+ public abstract class RequestFieldValidatorBase : IValidator
+ {
+ private readonly string _errorMessage;
+ private readonly bool _useUnvalidatedValues;
+
+ protected RequestFieldValidatorBase(string errorMessage)
+ : this(errorMessage, useUnvalidatedValues: false)
+ {
+ }
+
+ protected RequestFieldValidatorBase(string errorMessage, bool useUnvalidatedValues)
+ {
+ if (String.IsNullOrEmpty(errorMessage))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "errorMessage");
+ }
+
+ _errorMessage = errorMessage;
+ _useUnvalidatedValues = useUnvalidatedValues;
+ }
+
+ public virtual ModelClientValidationRule ClientValidationRule
+ {
+ get { return null; }
+ }
+
+ /// <summary>
+ /// Meant for unit tests that causes RequestFieldValidatorBase to basically ignore the unvalidated field requirement.
+ /// </summary>
+ internal static bool IgnoreUseUnvalidatedValues { get; set; }
+
+ protected abstract bool IsValid(HttpContextBase httpContext, string value);
+
+ public virtual ValidationResult Validate(ValidationContext validationContext)
+ {
+ var httpContext = GetHttpContext(validationContext);
+ var field = validationContext.MemberName;
+ var fieldValue = GetRequestValue(httpContext.Request, field);
+
+ if (IsValid(httpContext, fieldValue))
+ {
+ return ValidationResult.Success;
+ }
+ return new ValidationResult(_errorMessage, memberNames: new[] { field });
+ }
+
+ protected static HttpContextBase GetHttpContext(ValidationContext validationContext)
+ {
+ Debug.Assert(validationContext.ObjectInstance is HttpContextBase, "For our validation context, ObjectInstance must be an HttpContextBase instance.");
+ return (HttpContextBase)validationContext.ObjectInstance;
+ }
+
+ protected string GetRequestValue(HttpRequestBase request, string field)
+ {
+ if (IgnoreUseUnvalidatedValues)
+ {
+ // Make sure we do not set this when we are hosted since this is only meant for unit test scenarios.
+ Debug.Assert(HttpContext.Current == null, "This flag should not be set when we are hosted.");
+ return request.Form[field];
+ }
+ return _useUnvalidatedValues ? request.Unvalidated(field) : request.Form[field];
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Validation/ValidationAttributeAdapter.cs b/src/System.Web.WebPages/Validation/ValidationAttributeAdapter.cs
new file mode 100644
index 00000000..97ebcb46
--- /dev/null
+++ b/src/System.Web.WebPages/Validation/ValidationAttributeAdapter.cs
@@ -0,0 +1,39 @@
+using System.ComponentModel.DataAnnotations;
+using System.Web.Mvc;
+
+namespace System.Web.WebPages
+{
+ internal class ValidationAttributeAdapter : RequestFieldValidatorBase
+ {
+ private readonly ValidationAttribute _attribute;
+ private readonly ModelClientValidationRule _clientValidationRule;
+
+ public ValidationAttributeAdapter(ValidationAttribute attribute, string errorMessage, ModelClientValidationRule clientValidationRule)
+ :
+ this(attribute, errorMessage, clientValidationRule, useUnvalidatedValues: false)
+ {
+ }
+
+ public ValidationAttributeAdapter(ValidationAttribute attribute, string errorMessage, ModelClientValidationRule clientValidationRule, bool useUnvalidatedValues)
+ : base(errorMessage, useUnvalidatedValues)
+ {
+ _attribute = attribute;
+ _clientValidationRule = clientValidationRule;
+ }
+
+ public ValidationAttribute Attribute
+ {
+ get { return _attribute; }
+ }
+
+ public override ModelClientValidationRule ClientValidationRule
+ {
+ get { return _clientValidationRule; }
+ }
+
+ protected override bool IsValid(HttpContextBase httpContext, string value)
+ {
+ return _attribute.IsValid(value);
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Validation/ValidationHelper.cs b/src/System.Web.WebPages/Validation/ValidationHelper.cs
new file mode 100644
index 00000000..f87d8a19
--- /dev/null
+++ b/src/System.Web.WebPages/Validation/ValidationHelper.cs
@@ -0,0 +1,293 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Web.Mvc;
+using System.Web.WebPages.Html;
+using System.Web.WebPages.Scope;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.WebPages
+{
+ public sealed class ValidationHelper
+ {
+ private static readonly object _invalidCssClassKey = new object();
+ private static readonly object _validCssClassKey = new object();
+ private static IDictionary<object, object> _scopeOverride;
+
+ private readonly Dictionary<string, List<IValidator>> _validators = new Dictionary<string, List<IValidator>>(StringComparer.OrdinalIgnoreCase);
+ private readonly HttpContextBase _httpContext;
+ private readonly ModelStateDictionary _modelStateDictionary;
+
+ internal ValidationHelper(HttpContextBase httpContext, ModelStateDictionary modelStateDictionary)
+ {
+ Debug.Assert(httpContext != null);
+ Debug.Assert(modelStateDictionary != null);
+
+ _httpContext = httpContext;
+ _modelStateDictionary = modelStateDictionary;
+ }
+
+ public static string ValidCssClass
+ {
+ get
+ {
+ object value;
+ if (!Scope.TryGetValue(_validCssClassKey, out value))
+ {
+ return null;
+ }
+ return value as string;
+ }
+ set { Scope[_validCssClassKey] = value; }
+ }
+
+ public static string InvalidCssClass
+ {
+ get
+ {
+ object value;
+ if (!Scope.TryGetValue(_invalidCssClassKey, out value))
+ {
+ return HtmlHelper.DefaultValidationInputErrorCssClass;
+ }
+ return value as string;
+ }
+ set { Scope[_invalidCssClassKey] = value; }
+ }
+
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This makes it easier for a user to read this value without knowing of this type.")]
+ public string FormField
+ {
+ get { return ModelStateDictionary.FormFieldKey; }
+ }
+
+ internal static IDictionary<object, object> Scope
+ {
+ get { return _scopeOverride ?? ScopeStorage.CurrentScope; }
+ }
+
+ public void RequireField(string field)
+ {
+ RequireField(field, errorMessage: null);
+ }
+
+ public void RequireField(string field, string errorMessage)
+ {
+ if (String.IsNullOrEmpty(field))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "field");
+ }
+ Add(field, Validator.Required(errorMessage: errorMessage));
+ }
+
+ public void RequireFields(params string[] fields)
+ {
+ if (fields == null)
+ {
+ throw new ArgumentNullException("fields");
+ }
+ foreach (var field in fields)
+ {
+ RequireField(field);
+ }
+ }
+
+ public void Add(string field, params IValidator[] validators)
+ {
+ if (String.IsNullOrEmpty(field))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "field");
+ }
+ if ((validators == null) || validators.Any(v => v == null))
+ {
+ throw new ArgumentNullException("validators");
+ }
+
+ AddFieldValidators(field, validators);
+ }
+
+ public void Add(IEnumerable<string> fields, params IValidator[] validators)
+ {
+ if (fields == null)
+ {
+ throw new ArgumentNullException("fields");
+ }
+ if (validators == null)
+ {
+ throw new ArgumentNullException("validators");
+ }
+ foreach (var field in fields)
+ {
+ Add(field, validators);
+ }
+ }
+
+ public void AddFormError(string errorMessage)
+ {
+ _modelStateDictionary.AddFormError(errorMessage);
+ }
+
+ public bool IsValid(params string[] fields)
+ {
+ // Don't need to validate fields as we treat empty fields as all in Validate.
+ return !Validate(fields).Any();
+ }
+
+ public IEnumerable<ValidationResult> Validate(params string[] fields)
+ {
+ IEnumerable<string> keys = fields;
+ if (fields == null || !fields.Any())
+ {
+ // If no fields are present, validate all of them.
+ keys = _validators.Keys.Concat(new[] { FormField });
+ }
+ return ValidateFieldsAndUpdateModelState(keys);
+ }
+
+ public IEnumerable<string> GetErrors(params string[] fields)
+ {
+ // Don't need to validate fields as we treat empty fields as all in Validate.
+ return Validate(fields).Select(r => r.ErrorMessage);
+ }
+
+ public HtmlString For(string field)
+ {
+ if (String.IsNullOrEmpty(field))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "field");
+ }
+
+ var clientRules = GetClientValidationRules(field);
+ return GenerateHtmlFromClientValidationRules(clientRules);
+ }
+
+ public HtmlString ClassFor(string field)
+ {
+ if (_httpContext != null && String.Equals("POST", _httpContext.Request.HttpMethod, StringComparison.OrdinalIgnoreCase))
+ {
+ string cssClass = IsValid(field) ? ValidationHelper.ValidCssClass : ValidationHelper.InvalidCssClass;
+ return cssClass == null ? null : new HtmlString(cssClass);
+ }
+ return null;
+ }
+
+ internal static IDisposable OverrideScope()
+ {
+ _scopeOverride = new Dictionary<object, object>();
+ return new DisposableAction(() => _scopeOverride = null);
+ }
+
+ internal IDictionary<string, object> GetUnobtrusiveValidationAttributes(string field)
+ {
+ var clientRules = GetClientValidationRules(field);
+ var attributes = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
+ UnobtrusiveValidationAttributesGenerator.GetValidationAttributes(clientRules, attributes);
+ return attributes;
+ }
+
+ private IEnumerable<ValidationResult> ValidateFieldsAndUpdateModelState(IEnumerable<string> fields)
+ {
+ var validationContext = new ValidationContext(_httpContext, serviceProvider: null, items: null);
+ var validationResults = new List<ValidationResult>();
+ foreach (var field in fields)
+ {
+ IEnumerable<ValidationResult> fieldResults = ValidateField(field, validationContext);
+ IEnumerable<string> errors = fieldResults.Select(c => c.ErrorMessage);
+ ModelState modelState = _modelStateDictionary[field];
+ if (modelState != null && modelState.Errors.Any())
+ {
+ errors = errors.Except(modelState.Errors, StringComparer.OrdinalIgnoreCase);
+
+ // If there were other validation errors that were added via ModelState, add them to the collection.
+ fieldResults = fieldResults.Concat(modelState.Errors.Select(e => new ValidationResult(e, new[] { field })));
+ }
+
+ foreach (var errorMessage in errors)
+ {
+ // Only add errors that haven't been encountered before. This is to prevent from the same error message being duplicated
+ // if a call is made multiple times
+ _modelStateDictionary.AddError(field, errorMessage);
+ }
+
+ validationResults.AddRange(fieldResults);
+ }
+ return validationResults;
+ }
+
+ private void AddFieldValidators(string field, params IValidator[] validators)
+ {
+ List<IValidator> fieldValidators = null;
+ if (!_validators.TryGetValue(field, out fieldValidators))
+ {
+ fieldValidators = new List<IValidator>();
+ _validators[field] = fieldValidators;
+ }
+ foreach (var validator in validators)
+ {
+ fieldValidators.Add(validator);
+ }
+ }
+
+ private IEnumerable<ValidationResult> ValidateField(string field, ValidationContext context)
+ {
+ List<IValidator> fieldValidators;
+ if (!_validators.TryGetValue(field, out fieldValidators))
+ {
+ return Enumerable.Empty<ValidationResult>();
+ }
+ context.MemberName = field;
+ return fieldValidators.Select(f => f.Validate(context))
+ .Where(result => result != ValidationResult.Success);
+ }
+
+ private IEnumerable<ModelClientValidationRule> GetClientValidationRules(string field)
+ {
+ List<IValidator> fieldValidators = null;
+ if (!_validators.TryGetValue(field, out fieldValidators))
+ {
+ return Enumerable.Empty<ModelClientValidationRule>();
+ }
+
+ return from item in fieldValidators
+ let clientRule = item.ClientValidationRule
+ where clientRule != null
+ select clientRule;
+ }
+
+ internal static HtmlString GenerateHtmlFromClientValidationRules(IEnumerable<ModelClientValidationRule> clientRules)
+ {
+ if (!clientRules.Any())
+ {
+ return null;
+ }
+
+ var attributes = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
+ UnobtrusiveValidationAttributesGenerator.GetValidationAttributes(clientRules, attributes);
+
+ var stringBuilder = new StringBuilder();
+ foreach (var attribute in attributes)
+ {
+ string key = attribute.Key;
+ // Values are already html encoded.
+ string value = Convert.ToString(attribute.Value, CultureInfo.InvariantCulture);
+ stringBuilder.Append(key)
+ .Append("=\"")
+ .Append(value)
+ .Append('"')
+ .Append(' ');
+ }
+
+ // Trim trailing whitespace
+ if (stringBuilder.Length > 0)
+ {
+ stringBuilder.Length--;
+ }
+
+ return new HtmlString(stringBuilder.ToString());
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/Validation/Validator.cs b/src/System.Web.WebPages/Validation/Validator.cs
new file mode 100644
index 00000000..433e9325
--- /dev/null
+++ b/src/System.Web.WebPages/Validation/Validator.cs
@@ -0,0 +1,125 @@
+using System.ComponentModel.DataAnnotations;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Web.Mvc;
+using System.Web.WebPages.Resources;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.WebPages
+{
+ public abstract class Validator
+ {
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "We are ok using default parameters for helpers")]
+ public static IValidator Required(string errorMessage = null)
+ {
+ errorMessage = DefaultIfEmpty(errorMessage, WebPageResources.ValidationDefault_Required);
+ var clientAttributes = new ModelClientValidationRequiredRule(errorMessage);
+ // We don't care if the value is unsafe when verifying that it is required.
+ return new ValidationAttributeAdapter(new RequiredAttribute(), errorMessage, clientAttributes, useUnvalidatedValues: true);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "We are ok using default parameters for helpers")]
+ public static IValidator Range(int minValue, int maxValue, string errorMessage = null)
+ {
+ errorMessage = String.Format(CultureInfo.CurrentCulture, DefaultIfEmpty(errorMessage, WebPageResources.ValidationDefault_IntegerRange), minValue, maxValue);
+ var clientAttributes = new ModelClientValidationRangeRule(errorMessage, minValue, maxValue);
+ return new ValidationAttributeAdapter(new RangeAttribute(minValue, maxValue), errorMessage, clientAttributes);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "We are ok using default parameters for helpers")]
+ public static IValidator Range(double minValue, double maxValue, string errorMessage = null)
+ {
+ errorMessage = String.Format(CultureInfo.CurrentCulture, DefaultIfEmpty(errorMessage, WebPageResources.ValidationDefault_FloatRange), minValue, maxValue);
+ var clientAttributes = new ModelClientValidationRangeRule(errorMessage, minValue, maxValue);
+ return new ValidationAttributeAdapter(new RangeAttribute(minValue, maxValue), errorMessage, clientAttributes);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "We are ok using default parameters for helpers")]
+ public static IValidator StringLength(int maxLength, int minLength = 0, string errorMessage = null)
+ {
+ if (minLength == 0)
+ {
+ errorMessage = String.Format(CultureInfo.CurrentCulture, DefaultIfEmpty(errorMessage, WebPageResources.ValidationDefault_StringLength), maxLength);
+ }
+ else
+ {
+ errorMessage = DefaultIfEmpty(errorMessage, WebPageResources.ValidationDefault_StringLengthRange);
+ errorMessage = String.Format(CultureInfo.CurrentCulture, errorMessage, minLength, maxLength);
+ }
+ var clientAttributes = new ModelClientValidationStringLengthRule(errorMessage, minLength, maxLength);
+
+ // We don't care if the value is unsafe when checking the length of the request field passed to us.
+ return new ValidationAttributeAdapter(new StringLengthAttribute(maxLength) { MinimumLength = minLength }, errorMessage, clientAttributes,
+ useUnvalidatedValues: true);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "We are ok using default parameters for helpers")]
+ public static IValidator Regex(string pattern, string errorMessage = null)
+ {
+ if (String.IsNullOrEmpty(pattern))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "pattern");
+ }
+
+ errorMessage = DefaultIfEmpty(errorMessage, WebPageResources.ValidationDefault_Regex);
+ var clientAttributes = new ModelClientValidationRegexRule(errorMessage, pattern);
+ return new ValidationAttributeAdapter(new RegularExpressionAttribute(pattern), errorMessage, clientAttributes);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "We are ok using default parameters for helpers")]
+ public static IValidator EqualsTo(string otherFieldName, string errorMessage = null)
+ {
+ if (String.IsNullOrEmpty(otherFieldName))
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "otherFieldName");
+ }
+
+ errorMessage = DefaultIfEmpty(errorMessage, WebPageResources.ValidationDefault_EqualsTo);
+ return new CompareValidator(otherFieldName, errorMessage);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "We are ok using default parameters for helpers")]
+ public static IValidator DateTime(string errorMessage = null)
+ {
+ errorMessage = DefaultIfEmpty(errorMessage, WebPageResources.ValidationDefault_DataType);
+ return new DataTypeValidator(DataTypeValidator.SupportedValidationDataType.DateTime, errorMessage);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "We are ok using default parameters for helpers")]
+ public static IValidator Decimal(string errorMessage = null)
+ {
+ errorMessage = DefaultIfEmpty(errorMessage, WebPageResources.ValidationDefault_DataType);
+ return new DataTypeValidator(DataTypeValidator.SupportedValidationDataType.Decimal, errorMessage);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "We are ok using default parameters for helpers")]
+ public static IValidator Integer(string errorMessage = null)
+ {
+ errorMessage = DefaultIfEmpty(errorMessage, WebPageResources.ValidationDefault_DataType);
+ return new DataTypeValidator(DataTypeValidator.SupportedValidationDataType.Integer, errorMessage);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "We are ok using default parameters for helpers")]
+ public static IValidator Url(string errorMessage = null)
+ {
+ errorMessage = DefaultIfEmpty(errorMessage, WebPageResources.ValidationDefault_DataType);
+ return new DataTypeValidator(DataTypeValidator.SupportedValidationDataType.Url, errorMessage);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "We are ok using default parameters for helpers")]
+ public static IValidator Float(string errorMessage = null)
+ {
+ errorMessage = DefaultIfEmpty(errorMessage, WebPageResources.ValidationDefault_DataType);
+ return new DataTypeValidator(DataTypeValidator.SupportedValidationDataType.Float, errorMessage);
+ }
+
+ private static string DefaultIfEmpty(string errorMessage, string defaultErrorMessage)
+ {
+ if (String.IsNullOrEmpty(errorMessage))
+ {
+ return defaultErrorMessage;
+ }
+ return errorMessage;
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/VirtualPathFactoryExtensions.cs b/src/System.Web.WebPages/VirtualPathFactoryExtensions.cs
new file mode 100644
index 00000000..852bdf1d
--- /dev/null
+++ b/src/System.Web.WebPages/VirtualPathFactoryExtensions.cs
@@ -0,0 +1,21 @@
+namespace System.Web.WebPages
+{
+ internal static class VirtualPathFactoryExtensions
+ {
+ public static T CreateInstance<T>(this IVirtualPathFactory factory, string virtualPath) where T : class
+ {
+ var virtualPathFactoryManager = factory as VirtualPathFactoryManager;
+ if (virtualPathFactoryManager != null)
+ {
+ return virtualPathFactoryManager.CreateInstanceOfType<T>(virtualPath);
+ }
+ var buildManagerFactory = factory as BuildManagerWrapper;
+ if (buildManagerFactory != null)
+ {
+ return buildManagerFactory.CreateInstanceOfType<T>(virtualPath);
+ }
+
+ return factory.CreateInstance(virtualPath) as T;
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/VirtualPathFactoryManager.cs b/src/System.Web.WebPages/VirtualPathFactoryManager.cs
new file mode 100644
index 00000000..2ad071b3
--- /dev/null
+++ b/src/System.Web.WebPages/VirtualPathFactoryManager.cs
@@ -0,0 +1,59 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace System.Web.WebPages
+{
+ // This class encapsulates the creation of objects from virtual paths. The creation is either performed via BuildBanager API's, or
+ // by using explicitly registered factories (which happens through ApplicationPart.Register).
+ public class VirtualPathFactoryManager : IVirtualPathFactory
+ {
+ private static readonly Lazy<VirtualPathFactoryManager> _instance = new Lazy<VirtualPathFactoryManager>(() => new VirtualPathFactoryManager(new BuildManagerWrapper()));
+ private readonly LinkedList<IVirtualPathFactory> _virtualPathFactories = new LinkedList<IVirtualPathFactory>();
+
+ internal VirtualPathFactoryManager(IVirtualPathFactory defaultFactory)
+ {
+ _virtualPathFactories.AddFirst(defaultFactory);
+ }
+
+ // Get the VirtualPathFactoryManager singleton instance
+ internal static VirtualPathFactoryManager Instance
+ {
+ get { return _instance.Value; }
+ }
+
+ internal IEnumerable<IVirtualPathFactory> RegisteredFactories
+ {
+ get { return _virtualPathFactories; }
+ }
+
+ public static void RegisterVirtualPathFactory(IVirtualPathFactory virtualPathFactory)
+ {
+ Instance.RegisterVirtualPathFactoryInternal(virtualPathFactory);
+ }
+
+ internal void RegisterVirtualPathFactoryInternal(IVirtualPathFactory virtualPathFactory)
+ {
+ _virtualPathFactories.AddBefore(_virtualPathFactories.Last, virtualPathFactory);
+ }
+
+ public bool Exists(string virtualPath)
+ {
+ return _virtualPathFactories.Any(factory => factory.Exists(virtualPath));
+ }
+
+ public object CreateInstance(string virtualPath)
+ {
+ return CreateInstanceOfType<object>(virtualPath);
+ }
+
+ internal T CreateInstanceOfType<T>(string virtualPath) where T : class
+ {
+ var virtualPathFactory = _virtualPathFactories.FirstOrDefault(f => f.Exists(virtualPath));
+ if (virtualPathFactory != null)
+ {
+ return virtualPathFactory.CreateInstance<T>(virtualPath);
+ }
+ return null;
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/WebPage.cs b/src/System.Web.WebPages/WebPage.cs
new file mode 100644
index 00000000..31993433
--- /dev/null
+++ b/src/System.Web.WebPages/WebPage.cs
@@ -0,0 +1,98 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Web.WebPages.Html;
+using System.Web.WebPages.Scope;
+
+namespace System.Web.WebPages
+{
+ [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "This is a core class which needs to have references to many other classes")]
+ public abstract class WebPage : WebPageBase
+ {
+ private static readonly List<IWebPageRequestExecutor> _executors = new List<IWebPageRequestExecutor>();
+
+ private HttpContextBase _context;
+ // Expose the model as dynamic
+ private dynamic _model;
+
+ // True if this is a 'top level' page (URL addressable), vs a 'satellite' page like a user control or master
+ internal bool TopLevelPage { get; set; }
+
+ public override HttpContextBase Context
+ {
+ get
+ {
+ if (_context == null)
+ {
+ return PageContext.HttpContext;
+ }
+ return _context;
+ }
+ set { _context = value; }
+ }
+
+ public HtmlHelper Html { get; private set; }
+
+ public ValidationHelper Validation
+ {
+ get { return PageContext.Validation; }
+ }
+
+ public dynamic Model
+ {
+ get
+ {
+ if (_model == null)
+ {
+ // Instead of directly returning the model, we wrap it in our own custom DynamicObject.
+ // This allows it to perform private reflection, which would normally fail. This is useful
+ // when dealing with anonymous objects, which are always internal
+ _model = ReflectionDynamicObject.WrapObjectIfInternal(PageContext.Model);
+ }
+ return _model;
+ }
+ }
+
+ public ModelStateDictionary ModelState
+ {
+ get { return PageContext.ModelState; }
+ }
+
+ public static void RegisterPageExecutor(IWebPageRequestExecutor executor)
+ {
+ _executors.Add(executor);
+ }
+
+ public override void ExecutePageHierarchy()
+ {
+ using (ScopeStorage.CreateTransientScope(new ScopeStorageDictionary(ScopeStorage.CurrentScope, PageData)))
+ {
+ ExecutePageHierarchy(_executors);
+ }
+ }
+
+ internal void ExecutePageHierarchy(IEnumerable<IWebPageRequestExecutor> executors)
+ {
+ // Call all the executors until we find one that wants to handle it. This is used to implement features
+ // such as AJAX Page methods without having to bake them into the framework.
+ // Note that we only do this for 'top level' pages, as these are request-level executors that should not run for each user control/master
+ if (!TopLevelPage || !executors.Any(executor => executor.Execute(this)))
+ {
+ // No executor handled the request, so use normal processing
+ base.ExecutePageHierarchy();
+ }
+ }
+
+ public override HelperResult RenderPage(string path, params object[] data)
+ {
+ return base.RenderPage(path, data);
+ }
+
+ protected override void InitializePage()
+ {
+ base.InitializePage();
+
+ Html = new HtmlHelper(ModelState, Validation);
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/WebPageBase.cs b/src/System.Web.WebPages/WebPageBase.cs
new file mode 100644
index 00000000..e599dd79
--- /dev/null
+++ b/src/System.Web.WebPages/WebPageBase.cs
@@ -0,0 +1,491 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.IO;
+using System.Text;
+using System.Web.WebPages.Resources;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.WebPages
+{
+ // TODO(elipton): Clean this up and remove the suppression
+ [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "This is temporary (elipton)")]
+ public abstract class WebPageBase : WebPageRenderingBase
+ {
+ // Keep track of which sections RenderSection has already been called on
+ private HashSet<string> _renderedSections = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+ // Keep track of whether RenderBody has been called
+ private bool _renderedBody = false;
+ // Action for rendering the body within a layout page
+ private Action<TextWriter> _body;
+
+ // TODO(elipton): Figure out if we still need these two writers
+ private TextWriter _tempWriter;
+ private TextWriter _currentWriter;
+
+ private DynamicPageDataDictionary<dynamic> _dynamicPageData;
+
+ public override string Layout { get; set; }
+
+ public TextWriter Output
+ {
+ get { return OutputStack.Peek(); }
+ }
+
+ public Stack<TextWriter> OutputStack
+ {
+ get { return PageContext.OutputStack; }
+ }
+
+ public override IDictionary<object, dynamic> PageData
+ {
+ get { return PageContext.PageData; }
+ }
+
+ public override dynamic Page
+ {
+ get
+ {
+ if (_dynamicPageData == null)
+ {
+ _dynamicPageData = new DynamicPageDataDictionary<dynamic>((PageDataDictionary<dynamic>)PageData);
+ }
+ return _dynamicPageData;
+ }
+ }
+
+ // Retrieves the sections defined in the calling page. If this is null, that means
+ // this page has been requested directly.
+ private Dictionary<string, SectionWriter> PreviousSectionWriters
+ {
+ get
+ {
+ var top = SectionWritersStack.Pop();
+ var previous = SectionWritersStack.Count > 0 ? SectionWritersStack.Peek() : null;
+ SectionWritersStack.Push(top);
+ return previous;
+ }
+ }
+
+ // Retrieves the current Dictionary of sectionWriters on the stack without poping it.
+ // There should be at least one on the stack which is added when the Render(ViewData,TextWriter)
+ // is called.
+ private Dictionary<string, SectionWriter> SectionWriters
+ {
+ get { return SectionWritersStack.Peek(); }
+ }
+
+ private Stack<Dictionary<string, SectionWriter>> SectionWritersStack
+ {
+ get { return PageContext.SectionWritersStack; }
+ }
+
+ protected virtual void ConfigurePage(WebPageBase parentPage)
+ {
+ }
+
+ public static WebPageBase CreateInstanceFromVirtualPath(string virtualPath)
+ {
+ return CreateInstanceFromVirtualPath(virtualPath, VirtualPathFactoryManager.Instance);
+ }
+
+ internal static WebPageBase CreateInstanceFromVirtualPath(string virtualPath, IVirtualPathFactory virtualPathFactory)
+ {
+ // Get the compiled object
+ try
+ {
+ WebPageBase webPage = virtualPathFactory.CreateInstance<WebPageBase>(virtualPath);
+
+ // Give it its virtual path
+ webPage.VirtualPath = virtualPath;
+
+ // Assign it the VirtualPathFactory
+ webPage.VirtualPathFactory = virtualPathFactory;
+
+ return webPage;
+ }
+ catch (HttpException e)
+ {
+ BuildManagerExceptionUtil.ThrowIfUnsupportedExtension(virtualPath, e);
+ throw;
+ }
+ }
+
+ /// <summary>
+ /// Attempts to create a WebPageBase instance from a virtualPath and wraps complex compiler exceptions with simpler messages
+ /// </summary>
+ private WebPageBase CreatePageFromVirtualPath(string virtualPath, HttpContextBase httpContext, Func<string, bool> virtualPathExists, DisplayModeProvider displayModeProvider, IDisplayMode displayMode)
+ {
+ try
+ {
+ DisplayInfo resolvedDisplayInfo = displayModeProvider.GetDisplayInfoForVirtualPath(virtualPath, httpContext, virtualPathExists, displayMode);
+
+ if (resolvedDisplayInfo != null)
+ {
+ var webPage = VirtualPathFactory.CreateInstance<WebPageBase>(resolvedDisplayInfo.FilePath);
+
+ if (webPage != null)
+ {
+ // Give it its virtual path
+ webPage.VirtualPath = virtualPath;
+ webPage.VirtualPathFactory = VirtualPathFactory;
+ webPage.DisplayModeProvider = DisplayModeProvider;
+
+ return webPage;
+ }
+ }
+ }
+ catch (HttpException e)
+ {
+ // If the path uses an unregistered extension, such as Foo.txt,
+ // then an error regarding build providers will be thrown.
+ // Check if this is the case and throw a simpler error.
+ BuildManagerExceptionUtil.ThrowIfUnsupportedExtension(virtualPath, e);
+
+ // If the path uses an extension registered with codedom, such as Foo.js,
+ // then an unfriendly compilation error might get thrown by the underlying compiler.
+ // Check if this is the case and throw a simpler error.
+ BuildManagerExceptionUtil.ThrowIfCodeDomDefinedExtension(virtualPath, e);
+
+ // Rethrow any errors
+ throw;
+ }
+ // The page is missing, could not be compiled or is of an invalid type.
+ throw new HttpException(String.Format(CultureInfo.CurrentCulture, WebPageResources.WebPage_InvalidPageType, virtualPath));
+ }
+
+ private WebPageContext CreatePageContextFromParameters(bool isLayoutPage, params object[] data)
+ {
+ object first = null;
+ if (data != null && data.Length > 0)
+ {
+ first = data[0];
+ }
+
+ var pageData = PageDataDictionary<dynamic>.CreatePageDataFromParameters(PageData, data);
+
+ return WebPageContext.CreateNestedPageContext(PageContext, pageData, first, isLayoutPage);
+ }
+
+ public void DefineSection(string name, SectionWriter action)
+ {
+ if (SectionWriters.ContainsKey(name))
+ {
+ throw new HttpException(String.Format(CultureInfo.InvariantCulture, WebPageResources.WebPage_SectionAleadyDefined, name));
+ }
+ SectionWriters[name] = action;
+ }
+
+ internal void EnsurePageCanBeRequestedDirectly(string methodName)
+ {
+ if (PreviousSectionWriters == null)
+ {
+ throw new HttpException(String.Format(CultureInfo.CurrentCulture, WebPageResources.WebPage_CannotRequestDirectly, VirtualPath, methodName));
+ }
+ }
+
+ public void ExecutePageHierarchy(WebPageContext pageContext, TextWriter writer)
+ {
+ ExecutePageHierarchy(pageContext, writer, startPage: null);
+ }
+
+ // This method is only used by WebPageBase to allow passing in the view context and writer.
+ public void ExecutePageHierarchy(WebPageContext pageContext, TextWriter writer, WebPageRenderingBase startPage)
+ {
+ PushContext(pageContext, writer);
+
+ if (startPage != null)
+ {
+ if (startPage != this)
+ {
+ var startPageContext = WebPageContext.CreateNestedPageContext<object>(parentContext: pageContext, pageData: null, model: null, isLayoutPage: false);
+ startPageContext.Page = startPage;
+ startPage.PageContext = startPageContext;
+ }
+ startPage.ExecutePageHierarchy();
+ }
+ else
+ {
+ ExecutePageHierarchy();
+ }
+ PopContext();
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We really don't care if SourceHeader fails, and we don't want it to fail any real requests ever")]
+ public override void ExecutePageHierarchy()
+ {
+ // Unlike InitPages, for a WebPage there is no hierarchy - it is always
+ // the last file to execute in the chain. There can still be layout pages
+ // and partial pages, but they are never part of the hierarchy.
+
+ // (add server header for falcon debugging)
+ // call to MapPath() is expensive. If we are not emiting source files to header,
+ // don't bother to populate the SourceFiles collection. This saves perf significantly.
+ if (WebPageHttpHandler.ShouldGenerateSourceHeader(Context))
+ {
+ try
+ {
+ string vp = VirtualPath;
+ if (vp != null)
+ {
+ string path = Context.Request.MapPath(vp);
+ if (!path.IsEmpty())
+ {
+ PageContext.SourceFiles.Add(path);
+ }
+ }
+ }
+ catch
+ {
+ // we really don't care if this ever fails, so we swallow all exceptions
+ }
+ }
+
+ TemplateStack.Push(Context, this);
+ try
+ {
+ // Execute the developer-written code of the WebPage
+ Execute();
+ }
+ finally
+ {
+ TemplateStack.Pop(Context);
+ }
+ }
+
+ protected virtual void InitializePage()
+ {
+ }
+
+ public bool IsSectionDefined(string name)
+ {
+ EnsurePageCanBeRequestedDirectly("IsSectionDefined");
+ return PreviousSectionWriters.ContainsKey(name);
+ }
+
+ public void PopContext()
+ {
+ string renderedContent = _tempWriter.ToString();
+ OutputStack.Pop();
+
+ if (!String.IsNullOrEmpty(Layout))
+ {
+ string layoutPagePath = NormalizeLayoutPagePath(Layout);
+ // If a layout file was specified, render it passing our page content
+ OutputStack.Push(_currentWriter);
+ RenderSurrounding(
+ layoutPagePath,
+ w => w.Write(renderedContent));
+ OutputStack.Pop();
+ }
+ else
+ {
+ // Otherwise, just render the page
+ _currentWriter.Write(renderedContent);
+ }
+
+ VerifyRenderedBodyOrSections();
+ SectionWritersStack.Pop();
+ }
+
+ public void PushContext(WebPageContext pageContext, TextWriter writer)
+ {
+ _currentWriter = writer;
+ PageContext = pageContext;
+ pageContext.Page = this;
+
+ InitializePage();
+
+ // Create a temporary writer
+ _tempWriter = new StringWriter(CultureInfo.InvariantCulture);
+
+ // Render the page into it
+ OutputStack.Push(_tempWriter);
+ SectionWritersStack.Push(new Dictionary<string, SectionWriter>(StringComparer.OrdinalIgnoreCase));
+
+ // If the body is defined in the ViewData, remove it and store it on the instance
+ // so that it won't affect rendering of partial pages when they call VerifyRenderedBodyOrSections
+ if (PageContext.BodyAction != null)
+ {
+ _body = PageContext.BodyAction;
+ PageContext.BodyAction = null;
+ }
+ }
+
+ public HelperResult RenderBody()
+ {
+ EnsurePageCanBeRequestedDirectly("RenderBody");
+
+ if (_renderedBody)
+ {
+ throw new HttpException(WebPageResources.WebPage_RenderBodyAlreadyCalled);
+ }
+ _renderedBody = true;
+
+ // _body should have previously been set in Render(ViewContext,TextWriter) if it
+ // was available in the ViewData.
+ if (_body != null)
+ {
+ return new HelperResult(tw => _body(tw));
+ }
+ else
+ {
+ throw new HttpException(String.Format(CultureInfo.CurrentCulture, WebPageResources.WebPage_CannotRequestDirectly, VirtualPath, "RenderBody"));
+ }
+ }
+
+ public override HelperResult RenderPage(string path, params object[] data)
+ {
+ return RenderPageCore(path, isLayoutPage: false, data: data);
+ }
+
+ private HelperResult RenderPageCore(string path, bool isLayoutPage, object[] data)
+ {
+ if (String.IsNullOrEmpty(path))
+ {
+ throw ExceptionHelper.CreateArgumentNullOrEmptyException("path");
+ }
+
+ return new HelperResult(writer =>
+ {
+ path = NormalizePath(path);
+ WebPageBase subPage = CreatePageFromVirtualPath(path, Context, VirtualPathFactory.Exists, DisplayModeProvider, DisplayMode);
+ var pageContext = CreatePageContextFromParameters(isLayoutPage, data);
+
+ subPage.ConfigurePage(this);
+ subPage.ExecutePageHierarchy(pageContext, writer);
+ });
+ }
+
+ public HelperResult RenderSection(string name)
+ {
+ return RenderSection(name, required: true);
+ }
+
+ public HelperResult RenderSection(string name, bool required)
+ {
+ EnsurePageCanBeRequestedDirectly("RenderSection");
+
+ if (PreviousSectionWriters.ContainsKey(name))
+ {
+ var result = new HelperResult(tw =>
+ {
+ if (_renderedSections.Contains(name))
+ {
+ throw new HttpException(String.Format(CultureInfo.InvariantCulture, WebPageResources.WebPage_SectionAleadyRendered, name));
+ }
+ var body = PreviousSectionWriters[name];
+ // Since the body can also call RenderSection, we need to temporarily remove
+ // the current sections from the stack.
+ var top = SectionWritersStack.Pop();
+
+ bool pushed = false;
+ try
+ {
+ if (Output != tw)
+ {
+ OutputStack.Push(tw);
+ pushed = true;
+ }
+
+ body();
+ }
+ finally
+ {
+ if (pushed)
+ {
+ OutputStack.Pop();
+ }
+ }
+ SectionWritersStack.Push(top);
+ _renderedSections.Add(name);
+ });
+ return result;
+ }
+ else if (required)
+ {
+ // If the section is not found, and it is not optional, throw an error.
+ throw new HttpException(String.Format(CultureInfo.InvariantCulture, WebPageResources.WebPage_SectionNotDefined, name));
+ }
+ else
+ {
+ // If the section is optional and not found, then don't do anything.
+ return null;
+ }
+ }
+
+ private void RenderSurrounding(string partialViewName, Action<TextWriter> body)
+ {
+ // Save the previous body action and set ours instead.
+ // This value will be retrieved by the sub-page being rendered when it runs
+ // Render(ViewData, TextWriter).
+ var priorValue = PageContext.BodyAction;
+ PageContext.BodyAction = body;
+
+ // Render the layout file
+ Write(RenderPageCore(partialViewName, isLayoutPage: true, data: new object[0]));
+
+ // Restore the state
+ PageContext.BodyAction = priorValue;
+ }
+
+ // Verifies that RenderBody is called, or that RenderSection is called for all sections
+ private void VerifyRenderedBodyOrSections()
+ {
+ // The _body will be set within a layout page because PageContext.BodyAction was set by RenderSurrounding,
+ // which is only called in the case of rendering layout pages.
+ // Using RenderPage will not result in a _body being set in a partial page, thus the following checks for
+ // sections should not apply when RenderPage is called.
+ // Dev10 bug 928341
+ if (_body != null)
+ {
+ if (SectionWritersStack.Count > 1 && PreviousSectionWriters != null && PreviousSectionWriters.Count > 0)
+ {
+ // There are sections defined. Check that all sections have been rendered.
+ StringBuilder sectionsNotRendered = new StringBuilder();
+ foreach (var name in PreviousSectionWriters.Keys)
+ {
+ if (!_renderedSections.Contains(name))
+ {
+ if (sectionsNotRendered.Length > 0)
+ {
+ sectionsNotRendered.Append("; ");
+ }
+ sectionsNotRendered.Append(name);
+ }
+ }
+ if (sectionsNotRendered.Length > 0)
+ {
+ throw new HttpException(String.Format(CultureInfo.CurrentCulture, WebPageResources.WebPage_SectionsNotRendered, VirtualPath, sectionsNotRendered.ToString()));
+ }
+ }
+ else if (!_renderedBody)
+ {
+ // There are no sections defined, but RenderBody was NOT called.
+ // If a body was defined, then RenderBody should have been called.
+ throw new HttpException(String.Format(CultureInfo.CurrentCulture, WebPageResources.WebPage_RenderBodyNotCalled, VirtualPath));
+ }
+ }
+ }
+
+ public override void Write(HelperResult result)
+ {
+ WriteTo(Output, result);
+ }
+
+ public override void Write(object value)
+ {
+ WriteTo(Output, value);
+ }
+
+ public override void WriteLiteral(object value)
+ {
+ Output.Write(value);
+ }
+
+ protected internal override TextWriter GetOutputWriter()
+ {
+ return Output;
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/WebPageContext.cs b/src/System.Web.WebPages/WebPageContext.cs
new file mode 100644
index 00000000..df329bb7
--- /dev/null
+++ b/src/System.Web.WebPages/WebPageContext.cs
@@ -0,0 +1,155 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Web.WebPages.Html;
+
+namespace System.Web.WebPages
+{
+ // Class for containing various pieces of data required by a WebPage
+ public class WebPageContext
+ {
+ private static readonly object _sourceFileKey = new object();
+ private Stack<TextWriter> _outputStack;
+ private Stack<Dictionary<string, SectionWriter>> _sectionWritersStack;
+ private IDictionary<object, dynamic> _pageData;
+ private ValidationHelper _validation;
+ private ModelStateDictionary _modelStateDictionary;
+
+ public WebPageContext()
+ : this(context: null, page: null, model: null)
+ {
+ }
+
+ public WebPageContext(HttpContextBase context, WebPageRenderingBase page, object model)
+ {
+ HttpContext = context;
+ Page = page;
+ Model = model;
+ }
+
+ public static WebPageContext Current
+ {
+ get
+ {
+ // The TemplateStack stores instances of WebPageRenderingBase.
+ // Retrieve the top-most item from the stack and cast it to WebPageBase.
+ var httpContext = Web.HttpContext.Current;
+ if (httpContext != null)
+ {
+ var contextWrapper = new HttpContextWrapper(httpContext);
+ var currentTemplate = TemplateStack.GetCurrentTemplate(contextWrapper);
+ var currentPage = (currentTemplate as WebPageRenderingBase);
+
+ return (currentPage == null) ? null : currentPage.PageContext;
+ }
+ return null;
+ }
+ }
+
+ internal HttpContextBase HttpContext { get; set; }
+
+ public object Model { get; internal set; }
+
+ internal ModelStateDictionary ModelState
+ {
+ get
+ {
+ if (_modelStateDictionary == null)
+ {
+ _modelStateDictionary = new ModelStateDictionary();
+ }
+ return _modelStateDictionary;
+ }
+ }
+
+ internal ValidationHelper Validation
+ {
+ get
+ {
+ if (_validation == null)
+ {
+ Debug.Assert(HttpContext != null, "HttpContext must be initalized for Validation to work.");
+ _validation = new ValidationHelper(HttpContext, ModelState);
+ }
+ return _validation;
+ }
+ private set { _validation = value; }
+ }
+
+ internal Action<TextWriter> BodyAction { get; set; }
+
+ internal Stack<TextWriter> OutputStack
+ {
+ get
+ {
+ if (_outputStack == null)
+ {
+ _outputStack = new Stack<TextWriter>();
+ }
+ return _outputStack;
+ }
+ set { _outputStack = value; }
+ }
+
+ public WebPageRenderingBase Page { get; internal set; }
+
+ public IDictionary<object, dynamic> PageData
+ {
+ get
+ {
+ if (_pageData == null)
+ {
+ _pageData = new PageDataDictionary<dynamic>();
+ }
+ return _pageData;
+ }
+ internal set { _pageData = value; }
+ }
+
+ internal Stack<Dictionary<string, SectionWriter>> SectionWritersStack
+ {
+ get
+ {
+ if (_sectionWritersStack == null)
+ {
+ _sectionWritersStack = new Stack<Dictionary<string, SectionWriter>>();
+ }
+ return _sectionWritersStack;
+ }
+ set { _sectionWritersStack = value; }
+ }
+
+ // NOTE: We use a hashset because order doesn't matter and we want to eliminate duplicates
+ internal HashSet<string> SourceFiles
+ {
+ get
+ {
+ HashSet<string> sourceFiles = HttpContext.Items[_sourceFileKey] as HashSet<string>;
+ if (sourceFiles == null)
+ {
+ sourceFiles = new HashSet<string>();
+ HttpContext.Items[_sourceFileKey] = sourceFiles;
+ }
+ return sourceFiles;
+ }
+ }
+
+ internal static WebPageContext CreateNestedPageContext<TModel>(WebPageContext parentContext, IDictionary<object, dynamic> pageData, TModel model, bool isLayoutPage)
+ {
+ var nestedContext = new WebPageContext
+ {
+ HttpContext = parentContext.HttpContext,
+ OutputStack = parentContext.OutputStack,
+ Validation = parentContext.Validation,
+ PageData = pageData,
+ Model = model,
+ };
+ if (isLayoutPage)
+ {
+ nestedContext.BodyAction = parentContext.BodyAction;
+ nestedContext.SectionWritersStack = parentContext.SectionWritersStack;
+ }
+ return nestedContext;
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/WebPageExecutingBase.cs b/src/System.Web.WebPages/WebPageExecutingBase.cs
new file mode 100644
index 00000000..e32ee914
--- /dev/null
+++ b/src/System.Web.WebPages/WebPageExecutingBase.cs
@@ -0,0 +1,288 @@
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.IO;
+using System.Web.WebPages.Instrumentation;
+using System.Web.WebPages.Resources;
+
+/*
+WebPage class hierarchy
+
+WebPageExecutingBase The base class for all Plan9 files (_pagestart, _appstart, and regular pages)
+ ApplicationStartPage Used for _appstart.cshtml
+ WebPageRenderingBase
+ StartPage Used for _pagestart.cshtml
+ WebPageBase
+ WebPage Plan9Pages
+ ViewWebPage? MVC Views
+HelperPage Base class for Web Pages in App_Code.
+*/
+
+namespace System.Web.WebPages
+{
+ // The base class for all CSHTML files (_pagestart, _appstart, and regular pages)
+ public abstract class WebPageExecutingBase
+ {
+ private IVirtualPathFactory _virtualPathFactory;
+ private DynamicHttpApplicationState _dynamicAppState;
+ private InstrumentationService _instrumentationService = null;
+
+ internal InstrumentationService InstrumentationService
+ {
+ get
+ {
+ if (_instrumentationService == null)
+ {
+ _instrumentationService = new InstrumentationService();
+ }
+ return _instrumentationService;
+ }
+ set { _instrumentationService = value; }
+ }
+
+ public virtual HttpApplicationStateBase AppState
+ {
+ get
+ {
+ if (Context != null)
+ {
+ return Context.Application;
+ }
+ return null;
+ }
+ }
+
+ public virtual dynamic App
+ {
+ get
+ {
+ if (_dynamicAppState == null && AppState != null)
+ {
+ _dynamicAppState = new DynamicHttpApplicationState(AppState);
+ }
+ return _dynamicAppState;
+ }
+ }
+
+ public virtual HttpContextBase Context { get; set; }
+
+ public virtual string VirtualPath { get; set; }
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public virtual IVirtualPathFactory VirtualPathFactory
+ {
+ get { return _virtualPathFactory ?? VirtualPathFactoryManager.Instance; }
+ set { _virtualPathFactory = value; }
+ }
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public abstract void Execute();
+
+ public virtual string Href(string path, params object[] pathParts)
+ {
+ return UrlUtil.Url(VirtualPath, path, pathParts);
+ }
+
+ protected internal void BeginContext(int startPosition, int length, bool isLiteral)
+ {
+ BeginContext(GetOutputWriter(), VirtualPath, startPosition, length, isLiteral);
+ }
+
+ protected internal void BeginContext(string virtualPath, int startPosition, int length, bool isLiteral)
+ {
+ BeginContext(GetOutputWriter(), virtualPath, startPosition, length, isLiteral);
+ }
+
+ protected internal void BeginContext(TextWriter writer, int startPosition, int length, bool isLiteral)
+ {
+ BeginContext(writer, VirtualPath, startPosition, length, isLiteral);
+ }
+
+ protected internal void BeginContext(TextWriter writer, string virtualPath, int startPosition, int length, bool isLiteral)
+ {
+ // Double check that the instrumentation service is active because WriteAttribute always calls this
+ if (InstrumentationService.IsAvailable)
+ {
+ InstrumentationService.BeginContext(Context,
+ virtualPath,
+ writer,
+ startPosition,
+ length,
+ isLiteral);
+ }
+ }
+
+ protected internal void EndContext(int startPosition, int length, bool isLiteral)
+ {
+ EndContext(GetOutputWriter(), VirtualPath, startPosition, length, isLiteral);
+ }
+
+ protected internal void EndContext(string virtualPath, int startPosition, int length, bool isLiteral)
+ {
+ EndContext(GetOutputWriter(), virtualPath, startPosition, length, isLiteral);
+ }
+
+ protected internal void EndContext(TextWriter writer, int startPosition, int length, bool isLiteral)
+ {
+ EndContext(writer, VirtualPath, startPosition, length, isLiteral);
+ }
+
+ protected internal void EndContext(TextWriter writer, string virtualPath, int startPosition, int length, bool isLiteral)
+ {
+ // Double check that the instrumentation service is active because WriteAttribute always calls this
+ if (InstrumentationService.IsAvailable)
+ {
+ InstrumentationService.EndContext(Context,
+ virtualPath,
+ writer,
+ startPosition,
+ length,
+ isLiteral);
+ }
+ }
+
+ internal virtual string GetDirectory(string virtualPath)
+ {
+ return VirtualPathUtility.GetDirectory(virtualPath);
+ }
+
+ /// <summary>
+ /// Normalizes path relative to the current virtual path and throws if a file does not exist at the location.
+ /// </summary>
+ internal string NormalizeLayoutPagePath(string layoutPagePath)
+ {
+ var virtualPath = NormalizePath(layoutPagePath);
+ // Look for it as specified, either absolute, relative or same folder
+ if (VirtualPathFactory.Exists(virtualPath))
+ {
+ return virtualPath;
+ }
+ throw new HttpException(String.Format(CultureInfo.CurrentCulture, WebPageResources.WebPage_LayoutPageNotFound, layoutPagePath, virtualPath));
+ }
+
+ public virtual string NormalizePath(string path)
+ {
+ // If it's relative, resolve it
+ return VirtualPathUtility.Combine(VirtualPath, path);
+ }
+
+ public abstract void Write(HelperResult result);
+
+ public abstract void Write(object value);
+
+ public abstract void WriteLiteral(object value);
+
+ public virtual void WriteAttribute(string name, PositionTagged<string> prefix, PositionTagged<string> suffix, params AttributeValue[] values)
+ {
+ WriteAttributeTo(GetOutputWriter(), name, prefix, suffix, values);
+ }
+
+ public virtual void WriteAttributeTo(TextWriter writer, string name, PositionTagged<string> prefix, PositionTagged<string> suffix, params AttributeValue[] values)
+ {
+ WriteAttributeTo(VirtualPath, writer, name, prefix, suffix, values);
+ }
+
+ protected internal virtual void WriteAttributeTo(string pageVirtualPath, TextWriter writer, string name, PositionTagged<string> prefix, PositionTagged<string> suffix, params AttributeValue[] values)
+ {
+ bool first = true;
+ bool wroteSomething = false;
+ if (values.Length == 0)
+ {
+ // Explicitly empty attribute, so write the prefix and suffix
+ WritePositionTaggedLiteral(writer, pageVirtualPath, prefix);
+ WritePositionTaggedLiteral(writer, pageVirtualPath, suffix);
+ }
+ else
+ {
+ foreach (AttributeValue attrVal in values)
+ {
+ PositionTagged<object> val = attrVal.Value;
+ bool? boolVal = null;
+ if (val.Value is bool)
+ {
+ boolVal = (bool)val.Value;
+ }
+
+ if (val.Value != null && (boolVal == null || boolVal.Value))
+ {
+ string valStr = val.Value as string;
+ if (valStr == null)
+ {
+ valStr = val.Value.ToString();
+ }
+ if (boolVal != null)
+ {
+ Debug.Assert(boolVal.Value);
+ valStr = name;
+ }
+
+ if (first)
+ {
+ WritePositionTaggedLiteral(writer, pageVirtualPath, prefix);
+ first = false;
+ }
+ else
+ {
+ WritePositionTaggedLiteral(writer, pageVirtualPath, attrVal.Prefix);
+ }
+ BeginContext(writer, pageVirtualPath, attrVal.Value.Position, valStr.Length, isLiteral: attrVal.Literal);
+ if (attrVal.Literal)
+ {
+ WriteLiteralTo(writer, valStr);
+ }
+ else
+ {
+ WriteTo(writer, valStr); // Write value
+ }
+ EndContext(writer, pageVirtualPath, attrVal.Value.Position, valStr.Length, isLiteral: attrVal.Literal);
+ wroteSomething = true;
+ }
+ }
+ if (wroteSomething)
+ {
+ WritePositionTaggedLiteral(writer, pageVirtualPath, suffix);
+ }
+ }
+ }
+
+ private void WritePositionTaggedLiteral(TextWriter writer, string pageVirtualPath, string value, int position)
+ {
+ BeginContext(writer, pageVirtualPath, position, value.Length, isLiteral: true);
+ WriteLiteralTo(writer, value);
+ EndContext(writer, pageVirtualPath, position, value.Length, isLiteral: true);
+ }
+
+ private void WritePositionTaggedLiteral(TextWriter writer, string pageVirtualPath, PositionTagged<string> value)
+ {
+ WritePositionTaggedLiteral(writer, pageVirtualPath, value.Value, value.Position);
+ }
+
+ // This method is called by generated code and needs to stay in sync with the parser
+ public static void WriteTo(TextWriter writer, HelperResult content)
+ {
+ if (content != null)
+ {
+ content.WriteTo(writer);
+ }
+ }
+
+ // This method is called by generated code and needs to stay in sync with the parser
+ public static void WriteTo(TextWriter writer, object content)
+ {
+ writer.Write(HttpUtility.HtmlEncode(content));
+ }
+
+ // This method is called by generated code and needs to stay in sync with the parser
+ public static void WriteLiteralTo(TextWriter writer, object content)
+ {
+ writer.Write(content);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "A method is more appropriate in this case since a property likely already exists to hold this value")]
+ protected internal virtual TextWriter GetOutputWriter()
+ {
+ return TextWriter.Null;
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/WebPageHttpHandler.cs b/src/System.Web.WebPages/WebPageHttpHandler.cs
new file mode 100644
index 00000000..7975b87d
--- /dev/null
+++ b/src/System.Web.WebPages/WebPageHttpHandler.cs
@@ -0,0 +1,190 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Reflection;
+using System.Security;
+using System.Text;
+using System.Web.SessionState;
+using Microsoft.Web.Infrastructure.DynamicValidationHelper;
+
+namespace System.Web.WebPages
+{
+ public class WebPageHttpHandler : IHttpHandler, IRequiresSessionState
+ {
+ internal const string StartPageFileName = "_PageStart";
+ public static readonly string WebPagesVersionHeaderName = "X-AspNetWebPages-Version";
+ private static readonly List<string> _supportedExtensions = new List<string>();
+ internal static readonly string WebPagesVersion = GetVersionString();
+ private readonly WebPage _webPage;
+ private readonly Lazy<WebPageRenderingBase> _startPage;
+
+ public WebPageHttpHandler(WebPage webPage)
+ : this(webPage, new Lazy<WebPageRenderingBase>(() => System.Web.WebPages.StartPage.GetStartPage(webPage, StartPageFileName, GetRegisteredExtensions())))
+ {
+ }
+
+ internal WebPageHttpHandler(WebPage webPage, Lazy<WebPageRenderingBase> startPage)
+ {
+ if (webPage == null)
+ {
+ throw new ArgumentNullException("webPage");
+ }
+ _webPage = webPage;
+ _startPage = startPage;
+ }
+
+ public static bool DisableWebPagesResponseHeader { get; set; }
+
+ public virtual bool IsReusable
+ {
+ get { return false; }
+ }
+
+ internal WebPage RequestedPage
+ {
+ get { return _webPage; }
+ }
+
+ internal WebPageRenderingBase StartPage
+ {
+ get { return _startPage.Value; }
+ }
+
+ internal static void AddVersionHeader(HttpContextBase httpContext)
+ {
+ if (!DisableWebPagesResponseHeader)
+ {
+ httpContext.Response.AppendHeader(WebPagesVersionHeaderName, WebPagesVersion);
+ }
+ }
+
+ public static IHttpHandler CreateFromVirtualPath(string virtualPath)
+ {
+ return CreateFromVirtualPath(virtualPath, VirtualPathFactoryManager.Instance);
+ }
+
+ internal static IHttpHandler CreateFromVirtualPath(string virtualPath, IVirtualPathFactory virtualPathFactory)
+ {
+ // We will try to create a WebPage from our factory. If this fails, we assume that the virtual path maps to an IHttpHandler.
+ // Instantiate the page from the virtual path
+ WebPage page = virtualPathFactory.CreateInstance<WebPage>(virtualPath);
+
+ // If it's not a page, assume it's a regular handler
+ if (page == null)
+ {
+ return virtualPathFactory.CreateInstance<IHttpHandler>(virtualPath);
+ }
+
+ // Mark it as a 'top level' page (as opposed to a user control or master)
+ page.TopLevelPage = true;
+
+ // Give it its virtual path
+ page.VirtualPath = virtualPath;
+
+ // Assign it the object factory
+ page.VirtualPathFactory = virtualPathFactory;
+
+ // Return a handler over it
+ return new WebPageHttpHandler(page);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "We don't want a property")]
+ public static ReadOnlyCollection<string> GetRegisteredExtensions()
+ {
+ return new ReadOnlyCollection<string>(_supportedExtensions);
+ }
+
+ private static string GetVersionString()
+ {
+ // DevDiv 216459:
+ // This code originally used Assembly.GetName(), but that requires FileIOPermission, which isn't granted in
+ // medium trust. However, Assembly.FullName *is* accessible in medium trust.
+ return new AssemblyName(typeof(WebPageHttpHandler).Assembly.FullName).Version.ToString(2);
+ }
+
+ private static bool HandleError(Exception e)
+ {
+ // This method is similar to System.Web.UI.Page.HandleError
+
+ // Don't touch security exception
+ if (e is SecurityException)
+ {
+ return false;
+ }
+
+ throw new HttpUnhandledException(null, e);
+ }
+
+ internal static void GenerateSourceFilesHeader(WebPageContext context)
+ {
+ if (context.SourceFiles.Any())
+ {
+ var files = String.Join("|", context.SourceFiles);
+ // Since the characters in the value are files that may include characters outside of the ASCII set, header encoding as specified in RFC2047.
+ // =?<charset>?<encoding>?...?=
+ // In the following case, UTF8 is used with base64 encoding
+ var encodedText = "=?UTF-8?B?" + Convert.ToBase64String(Encoding.UTF8.GetBytes(files)) + "?=";
+ context.HttpContext.Response.AddHeader("X-SourceFiles", encodedText);
+ }
+ }
+
+ public virtual void ProcessRequest(HttpContext context)
+ {
+ // Dev10 bug 921943 - Plan9 should lower its permissions if operating in legacy CAS
+ SecurityUtil.ProcessInApplicationTrust(() =>
+ {
+ ProcessRequestInternal(context);
+ });
+ }
+
+ internal void ProcessRequestInternal(HttpContext context)
+ {
+ // enable dynamic validation for this request
+ ValidationUtility.EnableDynamicValidation(context);
+ context.Request.ValidateInput();
+
+ HttpContextBase contextBase = new HttpContextWrapper(context);
+ ProcessRequestInternal(contextBase);
+ }
+
+ internal void ProcessRequestInternal(HttpContextBase httpContext)
+ {
+ try
+ {
+ //WebSecurity.Context = contextBase;
+ AddVersionHeader(httpContext);
+
+ // This is also the point where a Plan9 request truly begins execution
+
+ // We call ExecutePageHierarchy on the requested page, passing in the possible initPage, so that
+ // the requested page can take care of the necessary push/pop context and trigger the call to
+ // the initPage.
+ _webPage.ExecutePageHierarchy(new WebPageContext { HttpContext = httpContext }, httpContext.Response.Output, StartPage);
+
+ if (ShouldGenerateSourceHeader(httpContext))
+ {
+ GenerateSourceFilesHeader(_webPage.PageContext);
+ }
+ }
+ catch (Exception e)
+ {
+ if (!HandleError(e))
+ {
+ throw;
+ }
+ }
+ }
+
+ public static void RegisterExtension(string extension)
+ {
+ // Note: we don't lock or check for duplicates because we only expect this method to be called during PreAppStart
+ _supportedExtensions.Add(extension);
+ }
+
+ internal static bool ShouldGenerateSourceHeader(HttpContextBase context)
+ {
+ return context.Request.IsLocal;
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/WebPageHttpModule.cs b/src/System.Web.WebPages/WebPageHttpModule.cs
new file mode 100644
index 00000000..9988b010
--- /dev/null
+++ b/src/System.Web.WebPages/WebPageHttpModule.cs
@@ -0,0 +1,115 @@
+namespace System.Web.WebPages
+{
+ internal class WebPageHttpModule : IHttpModule
+ {
+ internal static EventHandler Initialize;
+ internal static EventHandler ApplicationStart;
+ internal static EventHandler BeginRequest;
+ internal static EventHandler EndRequest;
+ private static bool _appStartExecuted = false;
+ private static readonly object _appStartExecutedLock = new object();
+ private static readonly object _hasBeenRegisteredKey = new object();
+
+ internal static bool AppStartExecuteCompleted { get; set; }
+
+ public void Dispose()
+ {
+ }
+
+ public void Init(HttpApplication application)
+ {
+ if (application.Context.Items[_hasBeenRegisteredKey] != null)
+ {
+ // registration for this module has already run for this HttpApplication instance
+ return;
+ }
+
+ application.Context.Items[_hasBeenRegisteredKey] = true;
+
+ InitApplication(application);
+ }
+
+ internal static void InitApplication(HttpApplication application)
+ {
+ // We need to run StartApplication first, so that any exception thrown during execution of the StartPage gets
+ // recorded on StartPage.Exception
+ StartApplication(application);
+ InitializeApplication(application);
+ }
+
+ internal static void InitializeApplication(HttpApplication application)
+ {
+ InitializeApplication(application, OnApplicationPostResolveRequestCache, Initialize);
+ }
+
+ internal static void InitializeApplication(HttpApplication application, EventHandler onApplicationPostResolveRequestCache, EventHandler initialize)
+ {
+ if (initialize != null)
+ {
+ initialize(application, EventArgs.Empty);
+ }
+ application.PostResolveRequestCache += onApplicationPostResolveRequestCache;
+ if (ApplicationStartPage.Exception != null || BeginRequest != null)
+ {
+ application.BeginRequest += OnBeginRequest;
+ }
+
+ application.EndRequest += OnEndRequest;
+ }
+
+ internal static void StartApplication(HttpApplication application)
+ {
+ StartApplication(application, ApplicationStartPage.ExecuteStartPage, ApplicationStart);
+ }
+
+ internal static void StartApplication(HttpApplication application, Action<HttpApplication> executeStartPage, EventHandler applicationStart)
+ {
+ // Application start events should happen only once per application life time.
+ lock (_appStartExecutedLock)
+ {
+ if (!_appStartExecuted)
+ {
+ _appStartExecuted = true;
+
+ executeStartPage(application);
+ AppStartExecuteCompleted = true;
+ if (applicationStart != null)
+ {
+ applicationStart(application, EventArgs.Empty);
+ }
+ }
+ }
+ }
+
+ internal static void OnApplicationPostResolveRequestCache(object sender, EventArgs e)
+ {
+ HttpContextBase context = new HttpContextWrapper(((HttpApplication)sender).Context);
+ new WebPageRoute().DoPostResolveRequestCache(context);
+ }
+
+ internal static void OnBeginRequest(object sender, EventArgs e)
+ {
+ if (ApplicationStartPage.Exception != null)
+ {
+ // Throw it as a HttpException so as to
+ // display the original stack trace information.
+ throw new HttpException(null, ApplicationStartPage.Exception);
+ }
+ if (BeginRequest != null)
+ {
+ BeginRequest(sender, e);
+ }
+ }
+
+ internal static void OnEndRequest(object sender, EventArgs e)
+ {
+ if (EndRequest != null)
+ {
+ EndRequest(sender, e);
+ }
+
+ var app = (HttpApplication)sender;
+ RequestResourceTracker.DisposeResources(new HttpContextWrapper(app.Context));
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/WebPageMatch.cs b/src/System.Web.WebPages/WebPageMatch.cs
new file mode 100644
index 00000000..f81ca07b
--- /dev/null
+++ b/src/System.Web.WebPages/WebPageMatch.cs
@@ -0,0 +1,15 @@
+namespace System.Web.WebPages
+{
+ internal sealed class WebPageMatch
+ {
+ public WebPageMatch(string matchedPath, string pathInfo)
+ {
+ MatchedPath = matchedPath;
+ PathInfo = pathInfo;
+ }
+
+ public string MatchedPath { get; private set; }
+
+ public string PathInfo { get; private set; }
+ }
+}
diff --git a/src/System.Web.WebPages/WebPageRenderingBase.cs b/src/System.Web.WebPages/WebPageRenderingBase.cs
new file mode 100644
index 00000000..f36f3f5a
--- /dev/null
+++ b/src/System.Web.WebPages/WebPageRenderingBase.cs
@@ -0,0 +1,207 @@
+using System.Collections.Generic;
+using System.Security.Principal;
+using System.Threading;
+using System.Web.Caching;
+using System.Web.Profile;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.WebPages
+{
+ public abstract class WebPageRenderingBase : WebPageExecutingBase, ITemplateFile
+ {
+ private IPrincipal _user;
+ private UrlDataList _urlData;
+ private TemplateFileInfo _templateFileInfo;
+ private DisplayModeProvider _displayModeProvider;
+
+ public virtual Cache Cache
+ {
+ get
+ {
+ if (Context != null)
+ {
+ return Context.Cache;
+ }
+ return null;
+ }
+ }
+
+ internal DisplayModeProvider DisplayModeProvider
+ {
+ get { return _displayModeProvider ?? DisplayModeProvider.Instance; }
+
+ set { _displayModeProvider = value; }
+ }
+
+ protected internal IDisplayMode DisplayMode
+ {
+ get { return DisplayModeProvider.GetDisplayMode(Context); }
+ }
+
+ public abstract string Layout { get; set; }
+
+ public abstract IDictionary<object, dynamic> PageData { get; }
+
+ public abstract dynamic Page { get; }
+
+ public WebPageContext PageContext { get; internal set; }
+
+ public ProfileBase Profile
+ {
+ get
+ {
+ if (Context != null)
+ {
+ return Context.Profile;
+ }
+ return null;
+ }
+ }
+
+ public virtual HttpRequestBase Request
+ {
+ get
+ {
+ if (Context != null)
+ {
+ return Context.Request;
+ }
+ return null;
+ }
+ }
+
+ public virtual HttpResponseBase Response
+ {
+ get
+ {
+ if (Context != null)
+ {
+ return Context.Response;
+ }
+ return null;
+ }
+ }
+
+ public virtual HttpServerUtilityBase Server
+ {
+ get
+ {
+ if (Context != null)
+ {
+ return Context.Server;
+ }
+ return null;
+ }
+ }
+
+ public virtual HttpSessionStateBase Session
+ {
+ get
+ {
+ if (Context != null)
+ {
+ return Context.Session;
+ }
+ return null;
+ }
+ }
+
+ public virtual IList<string> UrlData
+ {
+ get
+ {
+ if (_urlData == null)
+ {
+ WebPageMatch match = WebPageRoute.GetWebPageMatch(Context);
+ if (match != null)
+ {
+ _urlData = new UrlDataList(match.PathInfo);
+ }
+ else
+ {
+ // REVIEW: Can there ever be no route match?
+ _urlData = new UrlDataList(null);
+ }
+ }
+ return _urlData;
+ }
+ }
+
+ public virtual IPrincipal User
+ {
+ get
+ {
+ if (_user == null)
+ {
+ return Context.User;
+ }
+ return _user;
+ }
+ internal set { _user = value; }
+ }
+
+ public virtual TemplateFileInfo TemplateInfo
+ {
+ get
+ {
+ if (_templateFileInfo == null)
+ {
+ _templateFileInfo = new TemplateFileInfo(VirtualPath);
+ }
+ return _templateFileInfo;
+ }
+ }
+
+ public virtual bool IsPost
+ {
+ get { return Request.HttpMethod == "POST"; }
+ }
+
+ public virtual bool IsAjax
+ {
+ get
+ {
+ var request = Request;
+ if (request == null)
+ {
+ return false;
+ }
+ return (request["X-Requested-With"] == "XMLHttpRequest") || ((request.Headers != null) && (request.Headers["X-Requested-With"] == "XMLHttpRequest"));
+ }
+ }
+
+ public string Culture
+ {
+ get { return Thread.CurrentThread.CurrentCulture.Name; }
+ set
+ {
+ if (String.IsNullOrEmpty(value))
+ {
+ // GetCultureInfo accepts empty strings but throws for null strings. To maintain consistency in our string handling behavior, throw
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "value");
+ }
+ CultureUtil.SetCulture(Thread.CurrentThread, Context, value);
+ }
+ }
+
+ public string UICulture
+ {
+ get { return Thread.CurrentThread.CurrentUICulture.Name; }
+ set
+ {
+ if (String.IsNullOrEmpty(value))
+ {
+ // GetCultureInfo accepts empty strings but throws for null strings. To maintain consistency in our string handling behavior, throw
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "value");
+ }
+ CultureUtil.SetUICulture(Thread.CurrentThread, Context, value);
+ }
+ }
+
+ // Calls the Execute() method, and calls RunPage() if the page is an InitPage but
+ // did not call RunPage().
+ public abstract void ExecutePageHierarchy();
+
+ public abstract HelperResult RenderPage(string path, params object[] data);
+ }
+}
diff --git a/src/System.Web.WebPages/WebPageRoute.cs b/src/System.Web.WebPages/WebPageRoute.cs
new file mode 100644
index 00000000..6c629902
--- /dev/null
+++ b/src/System.Web.WebPages/WebPageRoute.cs
@@ -0,0 +1,222 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Web.WebPages.Deployment;
+using System.Web.WebPages.Resources;
+
+namespace System.Web.WebPages
+{
+ internal sealed class WebPageRoute
+ {
+ private static readonly Lazy<bool> _isRootExplicitlyDisabled = new Lazy<bool>(() => WebPagesDeployment.IsExplicitlyDisabled("~/"));
+ private IVirtualPathFactory _virtualPathFactory;
+ private bool? _isExplicitlyDisabled;
+
+ internal IVirtualPathFactory VirtualPathFactory
+ {
+ get { return _virtualPathFactory ?? VirtualPathFactoryManager.Instance; }
+ set { _virtualPathFactory = value; }
+ }
+
+ internal bool IsExplicitlyDisabled
+ {
+ get { return _isExplicitlyDisabled ?? _isRootExplicitlyDisabled.Value; }
+ set { _isExplicitlyDisabled = value; }
+ }
+
+ internal void DoPostResolveRequestCache(HttpContextBase context)
+ {
+ if (IsExplicitlyDisabled)
+ {
+ // If the root config is explicitly disabled, do not process the request.
+ return;
+ }
+
+ // Parse incoming URL (we trim off the first two chars since they're always "~/")
+ string requestPath = context.Request.AppRelativeCurrentExecutionFilePath.Substring(2) + context.Request.PathInfo;
+ var registeredExtensions = WebPageHttpHandler.GetRegisteredExtensions();
+
+ // Check if this request matches a file in the app
+ WebPageMatch webpageRouteMatch = MatchRequest(requestPath, registeredExtensions, VirtualPathFactory, context, DisplayModeProvider.Instance);
+ if (webpageRouteMatch != null)
+ {
+ // If it matches then save some data for the WebPage's UrlData
+ context.Items[typeof(WebPageMatch)] = webpageRouteMatch;
+
+ string virtualPath = "~/" + webpageRouteMatch.MatchedPath;
+
+ // Verify that this path is enabled before remapping
+ if (!WebPagesDeployment.IsExplicitlyDisabled(virtualPath))
+ {
+ IHttpHandler handler = WebPageHttpHandler.CreateFromVirtualPath(virtualPath);
+ if (handler != null)
+ {
+ SessionStateUtil.SetUpSessionState(context, handler);
+ // Remap to our handler
+ context.RemapHandler(handler);
+ }
+ }
+ }
+ else
+ {
+ // Bug:904704 If its not a match, but to a supported extension, we want to return a 404 instead of a 403
+ string extension = PathUtil.GetExtension(requestPath);
+ foreach (string supportedExt in registeredExtensions)
+ {
+ if (String.Equals("." + supportedExt, extension, StringComparison.OrdinalIgnoreCase))
+ {
+ throw new HttpException(404, null);
+ }
+ }
+ }
+ }
+
+ private static bool FileExists(string virtualPath, IVirtualPathFactory virtualPathFactory)
+ {
+ var path = "~/" + virtualPath;
+ return virtualPathFactory.Exists(path);
+ }
+
+ internal static WebPageMatch GetWebPageMatch(HttpContextBase context)
+ {
+ WebPageMatch webPageMatch = (WebPageMatch)context.Items[typeof(WebPageMatch)];
+ return webPageMatch;
+ }
+
+ private static string GetRouteLevelMatch(string pathValue, IEnumerable<string> supportedExtensions, IVirtualPathFactory virtualPathFactory, HttpContextBase context, DisplayModeProvider displayModeProvider)
+ {
+ foreach (string supportedExtension in supportedExtensions)
+ {
+ string virtualPath = "~/" + pathValue;
+
+ // Only add the extension if it's not already there
+ if (!virtualPath.EndsWith("." + supportedExtension, StringComparison.OrdinalIgnoreCase))
+ {
+ virtualPath += "." + supportedExtension;
+ }
+ DisplayInfo virtualPathDisplayInfo = displayModeProvider.GetDisplayInfoForVirtualPath(virtualPath, context, virtualPathFactory.Exists, currentDisplayMode: null);
+
+ if (virtualPathDisplayInfo != null)
+ {
+ // If there's an exact match on disk, return it unless it starts with an underscore
+ if (Path.GetFileName(virtualPathDisplayInfo.FilePath).StartsWith("_", StringComparison.OrdinalIgnoreCase))
+ {
+ throw new HttpException(404, WebPageResources.WebPageRoute_UnderscoreBlocked);
+ }
+
+ string resolvedVirtualPath = virtualPathDisplayInfo.FilePath;
+
+ // Matches are not expected to be virtual paths so remove the ~/ from the match
+ if (resolvedVirtualPath.StartsWith("~/", StringComparison.OrdinalIgnoreCase))
+ {
+ resolvedVirtualPath = resolvedVirtualPath.Remove(0, 2);
+ }
+
+ DisplayModeProvider.SetDisplayMode(context, virtualPathDisplayInfo.DisplayMode);
+
+ return resolvedVirtualPath;
+ }
+ }
+
+ return null;
+ }
+
+ internal static WebPageMatch MatchRequest(string pathValue, IEnumerable<string> supportedExtensions, IVirtualPathFactory virtualPathFactory, HttpContextBase context, DisplayModeProvider displayModes)
+ {
+ string currentLevel = String.Empty;
+ string currentPathInfo = pathValue;
+
+ // We can skip the file exists check and normal lookup for empty paths, but we still need to look for default pages
+ if (!String.IsNullOrEmpty(pathValue))
+ {
+ // If the file exists and its not a supported extension, let the request go through
+ if (FileExists(pathValue, virtualPathFactory))
+ {
+ // TODO: Look into switching to RawURL to eliminate the need for this issue
+ bool foundSupportedExtension = false;
+ foreach (string supportedExtension in supportedExtensions)
+ {
+ if (pathValue.EndsWith("." + supportedExtension, StringComparison.OrdinalIgnoreCase))
+ {
+ foundSupportedExtension = true;
+ break;
+ }
+ }
+
+ if (!foundSupportedExtension)
+ {
+ return null;
+ }
+ }
+
+ // For each trimmed part of the path try to add a known extension and
+ // check if it matches a file in the application.
+ currentLevel = pathValue;
+ currentPathInfo = String.Empty;
+ while (true)
+ {
+ // Does the current route level patch any supported extension?
+ string routeLevelMatch = GetRouteLevelMatch(currentLevel, supportedExtensions, virtualPathFactory, context, displayModes);
+ if (routeLevelMatch != null)
+ {
+ return new WebPageMatch(routeLevelMatch, currentPathInfo);
+ }
+
+ // Try to remove the last path segment (e.g. go from /foo/bar to /foo)
+ int indexOfLastSlash = currentLevel.LastIndexOf('/');
+ if (indexOfLastSlash == -1)
+ {
+ // If there are no more slashes, we're done
+ break;
+ }
+ else
+ {
+ // Chop off the last path segment to get to the next one
+ currentLevel = currentLevel.Substring(0, indexOfLastSlash);
+
+ // And save the path info in case there is a match
+ currentPathInfo = pathValue.Substring(indexOfLastSlash + 1);
+ }
+ }
+ }
+
+ return MatchDefaultFiles(pathValue, supportedExtensions, virtualPathFactory, context, displayModes, currentLevel);
+ }
+
+ private static WebPageMatch MatchDefaultFiles(string pathValue, IEnumerable<string> supportedExtensions, IVirtualPathFactory virtualPathFactory, HttpContextBase context, DisplayModeProvider displayModes, string currentLevel)
+ {
+ // If we haven't found anything yet, now try looking for default.* or index.* at the current url
+ currentLevel = pathValue;
+ string currentLevelDefault;
+ string currentLevelIndex;
+ if (String.IsNullOrEmpty(currentLevel))
+ {
+ currentLevelDefault = "default";
+ currentLevelIndex = "index";
+ }
+ else
+ {
+ if (currentLevel[currentLevel.Length - 1] != '/')
+ {
+ currentLevel += "/";
+ }
+ currentLevelDefault = currentLevel + "default";
+ currentLevelIndex = currentLevel + "index";
+ }
+
+ // Does the current route level match any supported extension?
+ string defaultMatch = GetRouteLevelMatch(currentLevelDefault, supportedExtensions, virtualPathFactory, context, displayModes);
+ if (defaultMatch != null)
+ {
+ return new WebPageMatch(defaultMatch, String.Empty);
+ }
+
+ string indexMatch = GetRouteLevelMatch(currentLevelIndex, supportedExtensions, virtualPathFactory, context, displayModes);
+ if (indexMatch != null)
+ {
+ return new WebPageMatch(indexMatch, String.Empty);
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/System.Web.WebPages/packages.config b/src/System.Web.WebPages/packages.config
new file mode 100644
index 00000000..f143a04f
--- /dev/null
+++ b/src/System.Web.WebPages/packages.config
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="Microsoft.Web.Infrastructure" version="1.0.0.0" />
+</packages> \ No newline at end of file
diff --git a/src/TransparentCommonAssemblyInfo.cs b/src/TransparentCommonAssemblyInfo.cs
new file mode 100644
index 00000000..e243a36c
--- /dev/null
+++ b/src/TransparentCommonAssemblyInfo.cs
@@ -0,0 +1,3 @@
+using System.Security;
+
+[assembly: SecurityTransparent]
diff --git a/src/VirtualPathUtilityWrapper.cs b/src/VirtualPathUtilityWrapper.cs
new file mode 100644
index 00000000..8fc4e146
--- /dev/null
+++ b/src/VirtualPathUtilityWrapper.cs
@@ -0,0 +1,17 @@
+using System.Web;
+
+namespace Microsoft.Internal.Web.Utils
+{
+ internal sealed class VirtualPathUtilityWrapper : IVirtualPathUtility
+ {
+ public string Combine(string basePath, string relativePath)
+ {
+ return VirtualPathUtility.Combine(basePath, relativePath);
+ }
+
+ public string ToAbsolute(string virtualPath)
+ {
+ return VirtualPathUtility.ToAbsolute(virtualPath);
+ }
+ }
+}
diff --git a/src/WebHelpers.ruleset b/src/WebHelpers.ruleset
new file mode 100644
index 00000000..ba165b53
--- /dev/null
+++ b/src/WebHelpers.ruleset
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RuleSet Name="FxCop rules for Web Helpers" ToolsVersion="10.0">
+ <Include Path="strict.ruleset" Action="Default" />
+ <Rules AnalyzerId="Microsoft.Analyzers.ManagedCodeAnalysis" RuleNamespace="Microsoft.Rules.Managed">
+ <Rule Id="CA1026" Action="None" />
+ </Rules>
+</RuleSet> \ No newline at end of file
diff --git a/src/WebMatrix.Data/ConfigurationManagerWrapper.cs b/src/WebMatrix.Data/ConfigurationManagerWrapper.cs
new file mode 100644
index 00000000..73e51f51
--- /dev/null
+++ b/src/WebMatrix.Data/ConfigurationManagerWrapper.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Collections.Generic;
+using System.Configuration;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+
+namespace WebMatrix.Data
+{
+ internal class ConfigurationManagerWrapper : IConfigurationManager
+ {
+ private readonly string _dataDirectory = null;
+ private IDictionary<string, string> _appSettings;
+ private IDictionary<string, IDbFileHandler> _handlers;
+
+ public ConfigurationManagerWrapper(IDictionary<string, IDbFileHandler> handlers, string dataDirectory = null)
+ {
+ Debug.Assert(handlers != null, "handlers should not be null");
+ _dataDirectory = dataDirectory ?? Database.DataDirectory;
+ _handlers = handlers;
+ }
+
+ public IDictionary<string, string> AppSettings
+ {
+ get
+ {
+ if (_appSettings == null)
+ {
+ _appSettings = (from string key in ConfigurationManager.AppSettings
+ select key).ToDictionary(key => key, key => ConfigurationManager.AppSettings[key]);
+ }
+ return _appSettings;
+ }
+ }
+
+ private static IConnectionConfiguration GetConnectionConfigurationFromConfig(string name)
+ {
+ ConnectionStringSettings setting = ConfigurationManager.ConnectionStrings[name];
+ if (setting != null)
+ {
+ return new ConnectionConfiguration(setting.ProviderName, setting.ConnectionString);
+ }
+ return null;
+ }
+
+ public IConnectionConfiguration GetConnection(string name)
+ {
+ return GetConnection(name, GetConnectionConfigurationFromConfig, File.Exists);
+ }
+
+ // For unit testing
+ internal IConnectionConfiguration GetConnection(string name, Func<string, IConnectionConfiguration> getConfigConnection, Func<string, bool> fileExists)
+ {
+ // First try config
+ IConnectionConfiguration configuraitonConfig = getConfigConnection(name);
+ if (configuraitonConfig != null)
+ {
+ return configuraitonConfig;
+ }
+
+ // Then try files under the |DataDirectory| with the supported extensions
+ // REVIEW: We sort because we want to process mdf before sdf (we only have 2 entries)
+ foreach (var handler in _handlers.OrderBy(h => h.Key))
+ {
+ string fileName = Path.Combine(_dataDirectory, name + handler.Key);
+ if (fileExists(fileName))
+ {
+ return handler.Value.GetConnectionConfiguration(fileName);
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/WebMatrix.Data/ConnectionConfiguration.cs b/src/WebMatrix.Data/ConnectionConfiguration.cs
new file mode 100644
index 00000000..527ff413
--- /dev/null
+++ b/src/WebMatrix.Data/ConnectionConfiguration.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Diagnostics;
+
+namespace WebMatrix.Data
+{
+ internal class ConnectionConfiguration : IConnectionConfiguration
+ {
+ internal ConnectionConfiguration(string providerName, string connectionString)
+ : this(new DbProviderFactoryWrapper(providerName), connectionString)
+ {
+ }
+
+ internal ConnectionConfiguration(IDbProviderFactory providerFactory, string connectionString)
+ {
+ Debug.Assert(!String.IsNullOrEmpty(connectionString), "connectionString should not be null");
+
+ ProviderFactory = providerFactory;
+ ConnectionString = connectionString;
+ }
+
+ public IDbProviderFactory ProviderFactory { get; private set; }
+
+ public string ConnectionString { get; private set; }
+ }
+}
diff --git a/src/WebMatrix.Data/ConnectionEventArgs.cs b/src/WebMatrix.Data/ConnectionEventArgs.cs
new file mode 100644
index 00000000..8720d85a
--- /dev/null
+++ b/src/WebMatrix.Data/ConnectionEventArgs.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Data.Common;
+
+namespace WebMatrix.Data
+{
+ public class ConnectionEventArgs : EventArgs
+ {
+ public ConnectionEventArgs(DbConnection connection)
+ {
+ Connection = connection;
+ }
+
+ public DbConnection Connection { get; private set; }
+ }
+}
diff --git a/src/WebMatrix.Data/Database.cs b/src/WebMatrix.Data/Database.cs
new file mode 100644
index 00000000..22e32857
--- /dev/null
+++ b/src/WebMatrix.Data/Database.cs
@@ -0,0 +1,302 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Data.Common;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using Microsoft.Internal.Web.Utils;
+using WebMatrix.Data.Resources;
+
+namespace WebMatrix.Data
+{
+ public class Database : IDisposable
+ {
+ internal const string SqlCeProviderName = "System.Data.SqlServerCe.4.0";
+ internal const string SqlServerProviderName = "System.Data.SqlClient";
+
+ private const string DefaultDataProviderAppSetting = "systemData:defaultProvider";
+ internal static string DataDirectory = (string)AppDomain.CurrentDomain.GetData("DataDirectory") ?? Directory.GetCurrentDirectory();
+
+ private static readonly IDictionary<string, IDbFileHandler> _databaseFileHandlers = new Dictionary<string, IDbFileHandler>(StringComparer.OrdinalIgnoreCase)
+ {
+ { ".sdf", new SqlCeDbFileHandler() },
+ { ".mdf", new SqlServerDbFileHandler() }
+ };
+
+ private static readonly IConfigurationManager _configurationManager = new ConfigurationManagerWrapper(_databaseFileHandlers);
+ private Func<DbConnection> _connectionFactory;
+ private DbConnection _connection;
+
+ internal Database(Func<DbConnection> connectionFactory)
+ {
+ _connectionFactory = connectionFactory;
+ }
+
+ public static event EventHandler<ConnectionEventArgs> ConnectionOpened
+ {
+ add { _connectionOpened += value; }
+ remove { _connectionOpened -= value; }
+ }
+
+ private static event EventHandler<ConnectionEventArgs> _connectionOpened;
+
+ public DbConnection Connection
+ {
+ get
+ {
+ if (_connection == null)
+ {
+ _connection = _connectionFactory();
+ }
+ return _connection;
+ }
+ }
+
+ public void Close()
+ {
+ Dispose();
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ if (_connection != null)
+ {
+ _connection.Close();
+ _connection = null;
+ }
+ }
+ }
+
+ public dynamic QuerySingle(string commandText, params object[] args)
+ {
+ if (String.IsNullOrEmpty(commandText))
+ {
+ throw ExceptionHelper.CreateArgumentNullOrEmptyException("commandText");
+ }
+
+ return QueryInternal(commandText, args).FirstOrDefault();
+ }
+
+ public IEnumerable<dynamic> Query(string commandText, params object[] parameters)
+ {
+ if (String.IsNullOrEmpty(commandText))
+ {
+ throw ExceptionHelper.CreateArgumentNullOrEmptyException("commandText");
+ }
+ // Return a readonly collection
+ return QueryInternal(commandText, parameters).ToList().AsReadOnly();
+ }
+
+ [SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities", Justification = "Users are responsible for ensuring the inputs to this method are SQL Injection sanitized")]
+ private IEnumerable<dynamic> QueryInternal(string commandText, params object[] parameters)
+ {
+ EnsureConnectionOpen();
+
+ DbCommand command = Connection.CreateCommand();
+ command.CommandText = commandText;
+
+ AddParameters(command, parameters);
+ using (command)
+ {
+ IEnumerable<string> columnNames = null;
+ using (DbDataReader reader = command.ExecuteReader())
+ {
+ foreach (DbDataRecord record in reader)
+ {
+ if (columnNames == null)
+ {
+ columnNames = GetColumnNames(record);
+ }
+ yield return new DynamicRecord(columnNames, record);
+ }
+ }
+ }
+ }
+
+ private static IEnumerable<string> GetColumnNames(DbDataRecord record)
+ {
+ // Get all of the column names for this query
+ for (int i = 0; i < record.FieldCount; i++)
+ {
+ yield return record.GetName(i);
+ }
+ }
+
+ [SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities", Justification = "Users are responsible for ensuring the inputs to this method are SQL Injection sanitized")]
+ public int Execute(string commandText, params object[] args)
+ {
+ if (String.IsNullOrEmpty(commandText))
+ {
+ throw ExceptionHelper.CreateArgumentNullOrEmptyException("commandText");
+ }
+
+ EnsureConnectionOpen();
+
+ DbCommand command = Connection.CreateCommand();
+ command.CommandText = commandText;
+
+ AddParameters(command, args);
+ using (command)
+ {
+ return command.ExecuteNonQuery();
+ }
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This makes a database request")]
+ public dynamic GetLastInsertId()
+ {
+ // This method only support sql ce and sql server for now
+ return QueryValue("SELECT @@Identity");
+ }
+
+ [SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities", Justification = "Users are responsible for ensuring the inputs to this method are SQL Injection sanitized")]
+ public dynamic QueryValue(string commandText, params object[] args)
+ {
+ if (String.IsNullOrEmpty(commandText))
+ {
+ throw ExceptionHelper.CreateArgumentNullOrEmptyException("commandText");
+ }
+
+ EnsureConnectionOpen();
+
+ DbCommand command = Connection.CreateCommand();
+ command.CommandText = commandText;
+
+ AddParameters(command, args);
+ using (command)
+ {
+ return command.ExecuteScalar();
+ }
+ }
+
+ private void EnsureConnectionOpen()
+ {
+ // If the connection isn't open then open it
+ if (Connection.State != ConnectionState.Open)
+ {
+ Connection.Open();
+
+ // Raise the connection opened event
+ OnConnectionOpened();
+ }
+ }
+
+ private void OnConnectionOpened()
+ {
+ if (_connectionOpened != null)
+ {
+ _connectionOpened(this, new ConnectionEventArgs(Connection));
+ }
+ }
+
+ private static void AddParameters(DbCommand command, object[] args)
+ {
+ if (args == null)
+ {
+ return;
+ }
+
+ // Create numbered parameters
+ IEnumerable<DbParameter> parameters = args.Select((o, index) =>
+ {
+ var parameter = command.CreateParameter();
+ parameter.ParameterName = index.ToString(CultureInfo.InvariantCulture);
+ parameter.Value = o ?? DBNull.Value;
+ return parameter;
+ });
+
+ foreach (var p in parameters)
+ {
+ command.Parameters.Add(p);
+ }
+ }
+
+ public static Database OpenConnectionString(string connectionString)
+ {
+ return OpenConnectionString(connectionString, providerName: null);
+ }
+
+ public static Database OpenConnectionString(string connectionString, string providerName)
+ {
+ if (String.IsNullOrEmpty(connectionString))
+ {
+ throw ExceptionHelper.CreateArgumentNullOrEmptyException("connectionString");
+ }
+
+ return OpenConnectionStringInternal(providerName, connectionString);
+ }
+
+ public static Database Open(string name)
+ {
+ if (String.IsNullOrEmpty(name))
+ {
+ throw ExceptionHelper.CreateArgumentNullOrEmptyException("name");
+ }
+ return OpenNamedConnection(name, _configurationManager);
+ }
+
+ internal static IConnectionConfiguration GetConnectionConfiguration(string fileName, IDictionary<string, IDbFileHandler> handlers)
+ {
+ string extension = Path.GetExtension(fileName);
+ IDbFileHandler handler;
+ if (handlers.TryGetValue(extension, out handler))
+ {
+ return handler.GetConnectionConfiguration(fileName);
+ }
+
+ throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture,
+ DataResources.UnableToDetermineDatabase, fileName));
+ }
+
+ private static Database OpenConnectionStringInternal(string providerName, string connectionString)
+ {
+ return OpenConnectionStringInternal(new DbProviderFactoryWrapper(providerName), connectionString);
+ }
+
+ private static Database OpenConnectionInternal(IConnectionConfiguration connectionConfig)
+ {
+ return OpenConnectionStringInternal(connectionConfig.ProviderFactory, connectionConfig.ConnectionString);
+ }
+
+ internal static Database OpenConnectionStringInternal(IDbProviderFactory providerFactory, string connectionString)
+ {
+ return new Database(() => providerFactory.CreateConnection(connectionString));
+ }
+
+ internal static Database OpenNamedConnection(string name, IConfigurationManager configurationManager)
+ {
+ // Opens a connection using the connection string setting with the specified name
+ IConnectionConfiguration configuration = configurationManager.GetConnection(name);
+ if (configuration != null)
+ {
+ // We've found one in the connection string setting in config so use it
+ return OpenConnectionInternal(configuration);
+ }
+
+ throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture,
+ DataResources.ConnectionStringNotFound, name));
+ }
+
+ internal static string GetDefaultProviderName()
+ {
+ string providerName;
+ // Get the default provider name from config if there is any
+ if (!_configurationManager.AppSettings.TryGetValue(DefaultDataProviderAppSetting, out providerName))
+ {
+ providerName = SqlCeProviderName;
+ }
+
+ return providerName;
+ }
+ }
+}
diff --git a/src/WebMatrix.Data/DbProviderFactoryWrapper.cs b/src/WebMatrix.Data/DbProviderFactoryWrapper.cs
new file mode 100644
index 00000000..d9a19823
--- /dev/null
+++ b/src/WebMatrix.Data/DbProviderFactoryWrapper.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Data.Common;
+
+namespace WebMatrix.Data
+{
+ internal class DbProviderFactoryWrapper : IDbProviderFactory
+ {
+ private string _providerName;
+ private DbProviderFactory _providerFactory;
+
+ public DbProviderFactoryWrapper(string providerName)
+ {
+ _providerName = providerName;
+ }
+
+ public DbConnection CreateConnection(string connectionString)
+ {
+ if (String.IsNullOrEmpty(_providerName))
+ {
+ // If the provider name is null or empty then use the default
+ _providerName = Database.GetDefaultProviderName();
+ }
+
+ if (_providerFactory == null)
+ {
+ _providerFactory = DbProviderFactories.GetFactory(_providerName);
+ }
+
+ DbConnection connection = _providerFactory.CreateConnection();
+ connection.ConnectionString = connectionString;
+ return connection;
+ }
+ }
+}
diff --git a/src/WebMatrix.Data/DynamicRecord.cs b/src/WebMatrix.Data/DynamicRecord.cs
new file mode 100644
index 00000000..7d8ff128
--- /dev/null
+++ b/src/WebMatrix.Data/DynamicRecord.cs
@@ -0,0 +1,197 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Data;
+using System.Diagnostics;
+using System.Dynamic;
+using System.Globalization;
+using System.Linq;
+using WebMatrix.Data.Resources;
+
+namespace WebMatrix.Data
+{
+ public sealed class DynamicRecord : DynamicObject, ICustomTypeDescriptor
+ {
+ internal DynamicRecord(IEnumerable<string> columnNames, IDataRecord record)
+ {
+ Debug.Assert(record != null, "record should not be null");
+ Debug.Assert(columnNames != null, "columnNames should not be null");
+
+ Columns = columnNames.ToList();
+ Record = record;
+ }
+
+ public IList<string> Columns { get; private set; }
+
+ private IDataRecord Record { get; set; }
+
+ public object this[string name]
+ {
+ get
+ {
+ VerifyColumn(name);
+ return GetValue(Record[name]);
+ }
+ }
+
+ public object this[int index]
+ {
+ get { return GetValue(Record[index]); }
+ }
+
+ public override bool TryGetMember(GetMemberBinder binder, out object result)
+ {
+ result = this[binder.Name];
+ return true;
+ }
+
+ private static object GetValue(object value)
+ {
+ return DBNull.Value == value ? null : value;
+ }
+
+ public override IEnumerable<string> GetDynamicMemberNames()
+ {
+ return Columns;
+ }
+
+ private void VerifyColumn(string name)
+ {
+ // REVIEW: Perf
+ if (!Columns.Contains(name, StringComparer.OrdinalIgnoreCase))
+ {
+ throw new InvalidOperationException(
+ String.Format(CultureInfo.CurrentCulture,
+ DataResources.InvalidColumnName, name));
+ }
+ }
+
+ AttributeCollection ICustomTypeDescriptor.GetAttributes()
+ {
+ return AttributeCollection.Empty;
+ }
+
+ string ICustomTypeDescriptor.GetClassName()
+ {
+ return null;
+ }
+
+ string ICustomTypeDescriptor.GetComponentName()
+ {
+ return null;
+ }
+
+ TypeConverter ICustomTypeDescriptor.GetConverter()
+ {
+ return null;
+ }
+
+ EventDescriptor ICustomTypeDescriptor.GetDefaultEvent()
+ {
+ return null;
+ }
+
+ PropertyDescriptor ICustomTypeDescriptor.GetDefaultProperty()
+ {
+ return null;
+ }
+
+ object ICustomTypeDescriptor.GetEditor(Type editorBaseType)
+ {
+ return null;
+ }
+
+ EventDescriptorCollection ICustomTypeDescriptor.GetEvents(Attribute[] attributes)
+ {
+ return EventDescriptorCollection.Empty;
+ }
+
+ EventDescriptorCollection ICustomTypeDescriptor.GetEvents()
+ {
+ return EventDescriptorCollection.Empty;
+ }
+
+ PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties(Attribute[] attributes)
+ {
+ return ((ICustomTypeDescriptor)this).GetProperties();
+ }
+
+ PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties()
+ {
+ // Get the name and type for each column name
+ var properties = from columnName in Columns
+ let columnIndex = Record.GetOrdinal(columnName)
+ let type = Record.GetFieldType(columnIndex)
+ select new DynamicPropertyDescriptor(columnName, type);
+
+ return new PropertyDescriptorCollection(properties.ToArray(), readOnly: true);
+ }
+
+ object ICustomTypeDescriptor.GetPropertyOwner(PropertyDescriptor pd)
+ {
+ return this;
+ }
+
+ private class DynamicPropertyDescriptor : PropertyDescriptor
+ {
+ private static readonly Attribute[] _empty = new Attribute[0];
+ private readonly Type _type;
+
+ public DynamicPropertyDescriptor(string name, Type type)
+ : base(name, _empty)
+ {
+ _type = type;
+ }
+
+ public override Type ComponentType
+ {
+ get { return typeof(DynamicRecord); }
+ }
+
+ public override bool IsReadOnly
+ {
+ get { return true; }
+ }
+
+ public override Type PropertyType
+ {
+ get { return _type; }
+ }
+
+ public override bool CanResetValue(object component)
+ {
+ return false;
+ }
+
+ public override object GetValue(object component)
+ {
+ DynamicRecord record = component as DynamicRecord;
+ // REVIEW: Should we throw if the wrong object was passed in?
+ if (record != null)
+ {
+ return record[Name];
+ }
+ return null;
+ }
+
+ public override void ResetValue(object component)
+ {
+ throw new InvalidOperationException(
+ String.Format(CultureInfo.CurrentCulture,
+ DataResources.RecordIsReadOnly, Name));
+ }
+
+ public override void SetValue(object component, object value)
+ {
+ throw new InvalidOperationException(
+ String.Format(CultureInfo.CurrentCulture,
+ DataResources.RecordIsReadOnly, Name));
+ }
+
+ public override bool ShouldSerializeValue(object component)
+ {
+ return false;
+ }
+ }
+ }
+}
diff --git a/src/WebMatrix.Data/GlobalSuppressions.cs b/src/WebMatrix.Data/GlobalSuppressions.cs
new file mode 100644
index 00000000..365dd7e9
--- /dev/null
+++ b/src/WebMatrix.Data/GlobalSuppressions.cs
@@ -0,0 +1,13 @@
+// This file is used by Code Analysis to maintain SuppressMessage
+// attributes that are applied to this project.
+// Project-level suppressions either have no target or are given
+// a specific target and scoped to a namespace, type, member, etc.
+//
+// To add a suppression to this file, right-click the message in the
+// Error List, point to "Suppress Message(s)", and click
+// "In Project Suppression File".
+// You do not need to add suppressions to this file manually.
+
+using System.Diagnostics.CodeAnalysis;
+
+[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "WebMatrix.Data", Justification = "WebMatrix.Data is a logical grouping of data-related helpers, and thus it belongs in its own namespace")]
diff --git a/src/WebMatrix.Data/IConfigurationManager.cs b/src/WebMatrix.Data/IConfigurationManager.cs
new file mode 100644
index 00000000..40d57f50
--- /dev/null
+++ b/src/WebMatrix.Data/IConfigurationManager.cs
@@ -0,0 +1,10 @@
+using System.Collections.Generic;
+
+namespace WebMatrix.Data
+{
+ internal interface IConfigurationManager
+ {
+ IDictionary<string, string> AppSettings { get; }
+ IConnectionConfiguration GetConnection(string name);
+ }
+}
diff --git a/src/WebMatrix.Data/IConnectionConfiguration.cs b/src/WebMatrix.Data/IConnectionConfiguration.cs
new file mode 100644
index 00000000..4d47eed9
--- /dev/null
+++ b/src/WebMatrix.Data/IConnectionConfiguration.cs
@@ -0,0 +1,8 @@
+namespace WebMatrix.Data
+{
+ internal interface IConnectionConfiguration
+ {
+ string ConnectionString { get; }
+ IDbProviderFactory ProviderFactory { get; }
+ }
+}
diff --git a/src/WebMatrix.Data/IDbFileHandler.cs b/src/WebMatrix.Data/IDbFileHandler.cs
new file mode 100644
index 00000000..b9302965
--- /dev/null
+++ b/src/WebMatrix.Data/IDbFileHandler.cs
@@ -0,0 +1,7 @@
+namespace WebMatrix.Data
+{
+ internal interface IDbFileHandler
+ {
+ IConnectionConfiguration GetConnectionConfiguration(string fileName);
+ }
+}
diff --git a/src/WebMatrix.Data/IDbProviderFactory.cs b/src/WebMatrix.Data/IDbProviderFactory.cs
new file mode 100644
index 00000000..07ef1882
--- /dev/null
+++ b/src/WebMatrix.Data/IDbProviderFactory.cs
@@ -0,0 +1,9 @@
+using System.Data.Common;
+
+namespace WebMatrix.Data
+{
+ internal interface IDbProviderFactory
+ {
+ DbConnection CreateConnection(string connectionString);
+ }
+}
diff --git a/src/WebMatrix.Data/Properties/AssemblyInfo.cs b/src/WebMatrix.Data/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..8c5e4450
--- /dev/null
+++ b/src/WebMatrix.Data/Properties/AssemblyInfo.cs
@@ -0,0 +1,11 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+
+[assembly: AssemblyTitle("WebMatrix.Data")]
+[assembly: AssemblyDescription("")]
+[assembly: InternalsVisibleTo("WebMatrix.Data.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
+[assembly: InternalsVisibleTo("WebMatrix.WebData.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
diff --git a/src/WebMatrix.Data/Resources/DataResources.Designer.cs b/src/WebMatrix.Data/Resources/DataResources.Designer.cs
new file mode 100644
index 00000000..a3124f79
--- /dev/null
+++ b/src/WebMatrix.Data/Resources/DataResources.Designer.cs
@@ -0,0 +1,99 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.1
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace WebMatrix.Data.Resources {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class DataResources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal DataResources() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("WebMatrix.Data.Resources.DataResources", typeof(DataResources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Connection string &quot;{0}&quot; was not found..
+ /// </summary>
+ internal static string ConnectionStringNotFound {
+ get {
+ return ResourceManager.GetString("ConnectionStringNotFound", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Invalid column name &quot;{0}&quot;..
+ /// </summary>
+ internal static string InvalidColumnName {
+ get {
+ return ResourceManager.GetString("InvalidColumnName", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Unable to modify the value of column &quot;{0}&quot; because the record is read only..
+ /// </summary>
+ internal static string RecordIsReadOnly {
+ get {
+ return ResourceManager.GetString("RecordIsReadOnly", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Unable to determine the provider for the database file &quot;{0}&quot;..
+ /// </summary>
+ internal static string UnableToDetermineDatabase {
+ get {
+ return ResourceManager.GetString("UnableToDetermineDatabase", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/WebMatrix.Data/Resources/DataResources.resx b/src/WebMatrix.Data/Resources/DataResources.resx
new file mode 100644
index 00000000..0a4bcca1
--- /dev/null
+++ b/src/WebMatrix.Data/Resources/DataResources.resx
@@ -0,0 +1,132 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="ConnectionStringNotFound" xml:space="preserve">
+ <value>Connection string "{0}" was not found.</value>
+ </data>
+ <data name="InvalidColumnName" xml:space="preserve">
+ <value>Invalid column name "{0}".</value>
+ </data>
+ <data name="RecordIsReadOnly" xml:space="preserve">
+ <value>Unable to modify the value of column "{0}" because the record is read only.</value>
+ </data>
+ <data name="UnableToDetermineDatabase" xml:space="preserve">
+ <value>Unable to determine the provider for the database file "{0}".</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/WebMatrix.Data/SqlCeDbFileHandler.cs b/src/WebMatrix.Data/SqlCeDbFileHandler.cs
new file mode 100644
index 00000000..a5c3a146
--- /dev/null
+++ b/src/WebMatrix.Data/SqlCeDbFileHandler.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Diagnostics;
+using System.Globalization;
+using System.IO;
+
+namespace WebMatrix.Data
+{
+ internal class SqlCeDbFileHandler : IDbFileHandler
+ {
+ private const string SqlCeConnectionStringFormat = @"Data Source={0};File Access Retry Timeout=10";
+
+ public IConnectionConfiguration GetConnectionConfiguration(string fileName)
+ {
+ // Get the default provider name
+ string providerName = Database.GetDefaultProviderName();
+ Debug.Assert(!String.IsNullOrEmpty(providerName), "Provider name should not be null or empty");
+
+ string connectionString = GetConnectionString(fileName);
+ return new ConnectionConfiguration(providerName, connectionString);
+ }
+
+ public static string GetConnectionString(string fileName)
+ {
+ if (Path.IsPathRooted(fileName))
+ {
+ return String.Format(CultureInfo.InvariantCulture, SqlCeConnectionStringFormat, fileName);
+ }
+
+ // Use |DataDirectory| if the path isn't rooted
+ string dataSource = @"|DataDirectory|\" + Path.GetFileName(fileName);
+ return String.Format(CultureInfo.InvariantCulture, SqlCeConnectionStringFormat, dataSource);
+ }
+ }
+}
diff --git a/src/WebMatrix.Data/SqlServerDbFileHandler.cs b/src/WebMatrix.Data/SqlServerDbFileHandler.cs
new file mode 100644
index 00000000..9c9ccba1
--- /dev/null
+++ b/src/WebMatrix.Data/SqlServerDbFileHandler.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Globalization;
+using System.IO;
+
+namespace WebMatrix.Data
+{
+ internal class SqlServerDbFileHandler : IDbFileHandler
+ {
+ private const string SqlServerConnectionStringFormat = @"Data Source=.\SQLEXPRESS;AttachDbFilename={0};Initial Catalog={1};Integrated Security=True;User Instance=True;MultipleActiveResultSets=True";
+ private const string SqlServerProviderName = "System.Data.SqlClient";
+
+ public IConnectionConfiguration GetConnectionConfiguration(string fileName)
+ {
+ return new ConnectionConfiguration(SqlServerProviderName, GetConnectionString(fileName, Database.DataDirectory));
+ }
+
+ internal static string GetConnectionString(string fileName, string dataDirectory)
+ {
+ if (Path.IsPathRooted(fileName))
+ {
+ // Attach the db as the file name if it is rooted
+ return String.Format(CultureInfo.InvariantCulture, SqlServerConnectionStringFormat, fileName, fileName);
+ }
+
+ // Use |DataDirectory| if the path isn't rooted
+ string dataSource = @"|DataDirectory|\" + Path.GetFileName(fileName);
+ // Set the full path for the initial catalog so we attach as that
+ string initialCatalog = Path.Combine(dataDirectory, Path.GetFileName(fileName));
+ return String.Format(CultureInfo.InvariantCulture, SqlServerConnectionStringFormat, dataSource, initialCatalog);
+ }
+ }
+}
diff --git a/src/WebMatrix.Data/WebMatrix.Data.csproj b/src/WebMatrix.Data/WebMatrix.Data.csproj
new file mode 100644
index 00000000..9dbd2e15
--- /dev/null
+++ b/src/WebMatrix.Data/WebMatrix.Data.csproj
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <CodeAnalysis Condition=" '$(CodeAnalysis)' == '' ">false</CodeAnalysis>
+ <ProductVersion>9.0.30729</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{4D39BAAF-8A96-473E-AB79-C8A341885137}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <RootNamespace>WebMatrix.Data</RootNamespace>
+ <AssemblyName>WebMatrix.Data</AssemblyName>
+ <FileAlignment>512</FileAlignment>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>..\..\bin\Debug\</OutputPath>
+ <DefineConstants>TRACE;DEBUG;ASPNETWEBPAGES</DefineConstants>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ <DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
+ <NoWarn>1591</NoWarn>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>..\..\bin\Release\</OutputPath>
+ <DefineConstants>TRACE;ASPNETWEBPAGES</DefineConstants>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ <RunCodeAnalysis>$(CodeAnalysis)</RunCodeAnalysis>
+ <DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
+ <NoWarn>1591</NoWarn>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'CodeCoverage|AnyCPU'">
+ <DebugSymbols>true</DebugSymbols>
+ <OutputPath>..\..\bin\CodeCoverage\</OutputPath>
+ <DefineConstants>TRACE;DEBUG;CODE_COVERAGE;ASPNETWEBPAGES</DefineConstants>
+ <DebugType>full</DebugType>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="System" />
+ <Reference Include="System.configuration" />
+ <Reference Include="System.Core" />
+ <Reference Include="Microsoft.CSharp" />
+ <Reference Include="System.Data" />
+ <Reference Include="System.Xml" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="..\CommonAssemblyInfo.cs">
+ <Link>Properties\CommonAssemblyInfo.cs</Link>
+ </Compile>
+ <Compile Include="..\CommonResources.Designer.cs">
+ <Link>Common\CommonResources.Designer.cs</Link>
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>CommonResources.resx</DependentUpon>
+ </Compile>
+ <Compile Include="..\ExceptionHelper.cs">
+ <Link>Common\ExceptionHelper.cs</Link>
+ </Compile>
+ <Compile Include="..\GlobalSuppressions.cs">
+ <Link>Common\GlobalSuppressions.cs</Link>
+ </Compile>
+ <Compile Include="..\TransparentCommonAssemblyInfo.cs">
+ <Link>Properties\TransparentCommonAssemblyInfo.cs</Link>
+ </Compile>
+ <Compile Include="ConfigurationManagerWrapper.cs" />
+ <Compile Include="ConnectionConfiguration.cs" />
+ <Compile Include="ConnectionEventArgs.cs" />
+ <Compile Include="Database.cs" />
+ <Compile Include="DbProviderFactoryWrapper.cs" />
+ <Compile Include="DynamicRecord.cs" />
+ <Compile Include="GlobalSuppressions.cs" />
+ <Compile Include="IConfigurationManager.cs" />
+ <Compile Include="IConnectionConfiguration.cs" />
+ <Compile Include="IDbFileHandler.cs" />
+ <Compile Include="IDbProviderFactory.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="Resources\DataResources.Designer.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>DataResources.resx</DependentUpon>
+ </Compile>
+ <Compile Include="SqlCeDbFileHandler.cs" />
+ <Compile Include="SqlServerDbFileHandler.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="..\CommonResources.resx">
+ <Link>Common\CommonResources.resx</Link>
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>CommonResources.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Include="Resources\DataResources.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>DataResources.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ </ItemGroup>
+ <ItemGroup>
+ <CodeAnalysisDictionary Include="..\CodeAnalysisDictionary.xml" />
+ </ItemGroup>
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+</Project> \ No newline at end of file
diff --git a/src/WebMatrix.WebData/ConfigUtil.cs b/src/WebMatrix.WebData/ConfigUtil.cs
new file mode 100644
index 00000000..97e62cf4
--- /dev/null
+++ b/src/WebMatrix.WebData/ConfigUtil.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Configuration;
+using System.Web.Security;
+
+namespace WebMatrix.WebData
+{
+ internal static class ConfigUtil
+ {
+ private static bool _simpleMembershipEnabled = IsSimpleMembershipEnabled();
+ private static string _loginUrl = GetLoginUrl();
+
+ public static bool SimpleMembershipEnabled
+ {
+ get { return _simpleMembershipEnabled; }
+ }
+
+ public static string LoginUrl
+ {
+ get { return _loginUrl; }
+ }
+
+ private static string GetLoginUrl()
+ {
+ return ConfigurationManager.AppSettings[FormsAuthenticationSettings.LoginUrlKey] ??
+ (ShouldPreserveLoginUrl() ? FormsAuthentication.LoginUrl : FormsAuthenticationSettings.DefaultLoginUrl);
+ }
+
+ private static bool IsSimpleMembershipEnabled()
+ {
+ string settingValue = ConfigurationManager.AppSettings[WebSecurity.EnableSimpleMembershipKey];
+ bool enabled;
+ if (!String.IsNullOrEmpty(settingValue) && Boolean.TryParse(settingValue, out enabled))
+ {
+ return enabled;
+ }
+ // Simple Membership is enabled by default, but attempts to delegate to the current provider if not initialized.
+ return true;
+ }
+
+ private static bool ShouldPreserveLoginUrl()
+ {
+ string settingValue = ConfigurationManager.AppSettings[FormsAuthenticationSettings.PreserveLoginUrlKey];
+ bool preserveLoginUrl;
+ if (!String.IsNullOrEmpty(settingValue) && Boolean.TryParse(settingValue, out preserveLoginUrl))
+ {
+ return preserveLoginUrl;
+ }
+
+ // For backwards compatible with WebPages 1.0, we override the loginUrl value if
+ // the PreserveLoginUrl key is not present.
+ return false;
+ }
+ }
+}
diff --git a/src/WebMatrix.WebData/DatabaseConnectionInfo.cs b/src/WebMatrix.WebData/DatabaseConnectionInfo.cs
new file mode 100644
index 00000000..b9581033
--- /dev/null
+++ b/src/WebMatrix.WebData/DatabaseConnectionInfo.cs
@@ -0,0 +1,53 @@
+using WebMatrix.Data;
+
+namespace WebMatrix.WebData
+{
+ internal class DatabaseConnectionInfo
+ {
+ private string _connectionStringName;
+ private string _connectionString;
+
+ private enum ConnectionType
+ {
+ ConnectionStringName = 0,
+ ConnectionString = 1
+ }
+
+ public string ConnectionString
+ {
+ get { return _connectionString; }
+ set
+ {
+ _connectionString = value;
+ Type = ConnectionType.ConnectionString;
+ }
+ }
+
+ public string ConnectionStringName
+ {
+ get { return _connectionStringName; }
+ set
+ {
+ _connectionStringName = value;
+ Type = ConnectionType.ConnectionStringName;
+ }
+ }
+
+ public string ProviderName { get; set; }
+
+ private ConnectionType Type { get; set; }
+
+ public Database Connect()
+ {
+ switch (Type)
+ {
+ case ConnectionType.ConnectionString:
+ return Database.OpenConnectionString(ConnectionString, ProviderName);
+ case ConnectionType.ConnectionStringName:
+ return Database.Open(ConnectionStringName);
+ default:
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/WebMatrix.WebData/DatabaseWrapper.cs b/src/WebMatrix.WebData/DatabaseWrapper.cs
new file mode 100644
index 00000000..65e53f4c
--- /dev/null
+++ b/src/WebMatrix.WebData/DatabaseWrapper.cs
@@ -0,0 +1,40 @@
+using System.Collections.Generic;
+using WebMatrix.Data;
+
+namespace WebMatrix.WebData
+{
+ internal class DatabaseWrapper : IDatabase
+ {
+ private readonly Database _database;
+
+ public DatabaseWrapper(Database database)
+ {
+ _database = database;
+ }
+
+ public dynamic QuerySingle(string commandText, params object[] parameters)
+ {
+ return _database.QuerySingle(commandText, parameters);
+ }
+
+ public IEnumerable<dynamic> Query(string commandText, params object[] parameters)
+ {
+ return _database.Query(commandText, parameters);
+ }
+
+ public dynamic QueryValue(string commandText, params object[] parameters)
+ {
+ return _database.QueryValue(commandText, parameters);
+ }
+
+ public int Execute(string commandText, params object[] parameters)
+ {
+ return _database.Execute(commandText, parameters);
+ }
+
+ public void Dispose()
+ {
+ _database.Dispose();
+ }
+ }
+}
diff --git a/src/WebMatrix.WebData/ExtendedMembershipProvider.cs b/src/WebMatrix.WebData/ExtendedMembershipProvider.cs
new file mode 100644
index 00000000..1064caaf
--- /dev/null
+++ b/src/WebMatrix.WebData/ExtendedMembershipProvider.cs
@@ -0,0 +1,93 @@
+using System;
+using System.Collections.Generic;
+using System.Web.Security;
+
+namespace WebMatrix.WebData
+{
+ public abstract class ExtendedMembershipProvider : MembershipProvider
+ {
+ private const int OneDayInMinutes = 24 * 60;
+
+ /// <summary>
+ /// Deletes the OAuth and OpenID account with the specified provider name and provider user id.
+ /// </summary>
+ /// <param name="provider">The provider.</param>
+ /// <param name="providerUserId">The provider user id.</param>
+ public abstract void DeleteOAuthAccount(string provider, string providerUserId);
+
+ /// <summary>
+ /// Creates a new OAuth account with the specified data or update an existing one if it already exists.
+ /// </summary>
+ /// <param name="provider">The provider.</param>
+ /// <param name="providerUserId">The provider userid.</param>
+ /// <param name="userName">The username.</param>
+ public abstract void CreateOrUpdateOAuthAccount(string provider, string providerUserId, string userName);
+
+ /// <summary>
+ /// Gets the id of the user with the specified provider name and provider user id.
+ /// </summary>
+ /// <param name="provider">The provider.</param>
+ /// <param name="providerUserId">The provider user id.</param>
+ /// <returns></returns>
+ public abstract int GetUserIdFromOAuth(string provider, string providerUserId);
+
+ /// <summary>
+ /// Gets the username of a user with the given id
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <returns></returns>
+ public abstract string GetUserNameFromId(int userId);
+
+ /// <summary>
+ /// Gets all OAuth accounts associated with the specified username
+ /// </summary>
+ /// <param name="userName">Name of the user.</param>
+ /// <returns></returns>
+ public abstract ICollection<OAuthAccountData> GetAccountsForUser(string userName);
+
+ public virtual string CreateUserAndAccount(string userName, string password)
+ {
+ return CreateUserAndAccount(userName, password, requireConfirmation: false, values: null);
+ }
+
+ public virtual string CreateUserAndAccount(string userName, string password, bool requireConfirmation)
+ {
+ return CreateUserAndAccount(userName, password, requireConfirmation, values: null);
+ }
+
+ public virtual string CreateUserAndAccount(string userName, string password, IDictionary<string, object> values)
+ {
+ return CreateUserAndAccount(userName, password, requireConfirmation: false, values: values);
+ }
+
+ public abstract string CreateUserAndAccount(string userName, string password, bool requireConfirmation, IDictionary<string, object> values);
+
+ public virtual string CreateAccount(string userName, string password)
+ {
+ return CreateAccount(userName, password, requireConfirmationToken: false);
+ }
+
+ public abstract string CreateAccount(string userName, string password, bool requireConfirmationToken);
+ public abstract bool ConfirmAccount(string userName, string accountConfirmationToken);
+ public abstract bool ConfirmAccount(string accountConfirmationToken);
+ public abstract bool DeleteAccount(string userName);
+
+ public virtual string GeneratePasswordResetToken(string userName)
+ {
+ return GeneratePasswordResetToken(userName, tokenExpirationInMinutesFromNow: OneDayInMinutes);
+ }
+
+ public abstract string GeneratePasswordResetToken(string userName, int tokenExpirationInMinutesFromNow);
+ public abstract int GetUserIdFromPasswordResetToken(string token);
+ public abstract bool IsConfirmed(string userName);
+ public abstract bool ResetPasswordWithToken(string token, string newPassword);
+ public abstract int GetPasswordFailuresSinceLastSuccess(string userName);
+ public abstract DateTime GetCreateDate(string userName);
+ public abstract DateTime GetPasswordChangedDate(string userName);
+ public abstract DateTime GetLastPasswordFailureDate(string userName);
+
+ internal virtual void VerifyInitialized()
+ {
+ }
+ }
+}
diff --git a/src/WebMatrix.WebData/FormsAuthenticationSettings.cs b/src/WebMatrix.WebData/FormsAuthenticationSettings.cs
new file mode 100644
index 00000000..c09108f4
--- /dev/null
+++ b/src/WebMatrix.WebData/FormsAuthenticationSettings.cs
@@ -0,0 +1,19 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace WebMatrix.WebData
+{
+ /// <summary>
+ /// Defines key names for use in a web.config &lt;appSettings&gt; section to override default settings.
+ /// </summary>
+ public static class FormsAuthenticationSettings
+ {
+ [SuppressMessage("Microsoft.Naming", "CA1726:UsePreferredTerms", MessageId = "Login", Justification = "The term Login is used more frequently in ASP.Net")]
+ public static readonly string LoginUrlKey = "loginUrl";
+
+ [SuppressMessage("Microsoft.Naming", "CA1726:UsePreferredTerms", MessageId = "Login", Justification = "The term Login is used more frequently in ASP.Net")]
+ public static readonly string DefaultLoginUrl = "~/Account/Login";
+
+ [SuppressMessage("Microsoft.Naming", "CA1726:UsePreferredTerms", MessageId = "Login", Justification = "The term Login is used more frequently in ASP.Net")]
+ public static readonly string PreserveLoginUrlKey = "PreserveLoginUrl";
+ }
+}
diff --git a/src/WebMatrix.WebData/GlobalSuppressions.cs b/src/WebMatrix.WebData/GlobalSuppressions.cs
new file mode 100644
index 00000000..f126993c
--- /dev/null
+++ b/src/WebMatrix.WebData/GlobalSuppressions.cs
@@ -0,0 +1,15 @@
+// This file is used by Code Analysis to maintain SuppressMessage
+// attributes that are applied to this project.
+// Project-level suppressions either have no target or are given
+// a specific target and scoped to a namespace, type, member, etc.
+//
+// To add a suppression to this file, right-click the message in the
+// Error List, point to "Suppress Message(s)", and click
+// "In Project Suppression File".
+// You do not need to add suppressions to this file manually.
+
+using System.Diagnostics.CodeAnalysis;
+
+[assembly: SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Username", Scope = "member", Target = "WebMatrix.WebData.ExtendedMembershipProvider.#GetUsernameFromId(System.Int32)", Justification = "Username is one word.")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "username", Scope = "member", Target = "WebMatrix.WebData.ExtendedMembershipProvider.#GetAccountsForUser(System.String)", Justification = "Username is one word.")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "username", Scope = "member", Target = "WebMatrix.WebData.ExtendedMembershipProvider.#CreateOrUpdateOAuthAccount(System.String,System.String,System.String)", Justification = "Username is one word.")]
diff --git a/src/WebMatrix.WebData/IDatabase.cs b/src/WebMatrix.WebData/IDatabase.cs
new file mode 100644
index 00000000..d62ff737
--- /dev/null
+++ b/src/WebMatrix.WebData/IDatabase.cs
@@ -0,0 +1,16 @@
+using System;
+using System.Collections.Generic;
+
+namespace WebMatrix.WebData
+{
+ internal interface IDatabase : IDisposable
+ {
+ dynamic QuerySingle(string commandText, params object[] args);
+
+ IEnumerable<dynamic> Query(string commandText, params object[] parameters);
+
+ dynamic QueryValue(string commandText, params object[] parameters);
+
+ int Execute(string commandText, params object[] args);
+ }
+}
diff --git a/src/WebMatrix.WebData/OAuthAccountData.cs b/src/WebMatrix.WebData/OAuthAccountData.cs
new file mode 100644
index 00000000..c17f1a63
--- /dev/null
+++ b/src/WebMatrix.WebData/OAuthAccountData.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Globalization;
+using Microsoft.Internal.Web.Utils;
+
+namespace WebMatrix.WebData
+{
+ /// <summary>
+ /// Represents an OpenAuth and OpenID account.
+ /// </summary>
+ public class OAuthAccountData
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OAuthAccountData"/> class.
+ /// </summary>
+ /// <param name="provider">The provider.</param>
+ /// <param name="providerUserId">The provider user id.</param>
+ public OAuthAccountData(string provider, string providerUserId)
+ {
+ if (String.IsNullOrEmpty(provider))
+ {
+ throw new ArgumentException(
+ String.Format(CultureInfo.CurrentCulture, CommonResources.Argument_Cannot_Be_Null_Or_Empty, "provider"),
+ "provider");
+ }
+
+ if (String.IsNullOrEmpty(providerUserId))
+ {
+ throw new ArgumentException(
+ String.Format(CultureInfo.CurrentCulture, CommonResources.Argument_Cannot_Be_Null_Or_Empty, "providerUserId"),
+ "providerUserId");
+ }
+
+ Provider = provider;
+ ProviderUserId = providerUserId;
+ }
+
+ /// <summary>
+ /// Gets the provider name.
+ /// </summary>
+ public string Provider { get; private set; }
+
+ /// <summary>
+ /// Gets the provider user id.
+ /// </summary>
+ public string ProviderUserId { get; private set; }
+ }
+}
diff --git a/src/WebMatrix.WebData/PreApplicationStartCode.cs b/src/WebMatrix.WebData/PreApplicationStartCode.cs
new file mode 100644
index 00000000..e2ebe967
--- /dev/null
+++ b/src/WebMatrix.WebData/PreApplicationStartCode.cs
@@ -0,0 +1,81 @@
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Web;
+using System.Web.Security;
+using System.Web.WebPages;
+using System.Web.WebPages.Razor;
+using WebMatrix.Data;
+
+namespace WebMatrix.WebData
+{
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static class PreApplicationStartCode
+ {
+ // NOTE: Do not add public fields, methods, or other members to this class.
+ // This class does not show up in Intellisense so members on it will not be
+ // discoverable by users. Place new members on more appropriate classes that
+ // relate to the public API (for example, a LoginUrl property should go on a
+ // membership-related class).
+
+ private static bool _startWasCalled;
+
+ public static void Start()
+ {
+ // Even though ASP.NET will only call each PreAppStart once, we sometimes internally call one PreAppStart from
+ // another PreAppStart to ensure that things get initialized in the right order. ASP.NET does not guarantee the
+ // order so we have to guard against multiple calls.
+ // All Start calls are made on same thread, so no lock needed here.
+
+ if (_startWasCalled)
+ {
+ return;
+ }
+ _startWasCalled = true;
+
+ // Summary of Simple Membership startup behavior:
+ // 1. If the appSetting enabledSimpleMembership is present and equal to "false", NEITHER SimpleMembership NOR AutoFormsAuth are activated
+ // 2. If the appSetting is true, a non-boolean string or not present, BOTH may be activated
+ // a. SimpleMembership ONLY replaces the AspNetSqlMemberhipProvider, but it does replace it even if it isn't the default. This
+ // means that anything accessing this provider by name will get Simple Membership, but if this provider is no longer the default
+ // then SimpleMembership does not affect the default
+ // b. SimpleMembership delegates to the previous default provider UNLESS WebSecurity.InitializeDatabaseConnection is called.
+
+ // Initialize membership provider
+ WebSecurity.PreAppStartInit();
+
+ // Initialize Forms Authentication default configuration
+ SetUpFormsAuthentication();
+
+ // Wire up WebMatrix.Data's Database object to the ASP.NET Web Pages resource tracker
+ Database.ConnectionOpened += OnConnectionOpened;
+
+ // Auto import the WebMatrix.Data and WebMatrix.WebData namespaces to all apps that are executing.
+ WebPageRazorHost.AddGlobalImport("WebMatrix.Data");
+ WebPageRazorHost.AddGlobalImport("WebMatrix.WebData");
+ }
+
+ private static void OnConnectionOpened(object sender, ConnectionEventArgs e)
+ {
+ // Register all open connections for disposing at the end of the request
+ HttpContext httpContext = HttpContext.Current;
+ if (httpContext != null)
+ {
+ HttpContextWrapper httpContextWrapper = new HttpContextWrapper(httpContext);
+ httpContextWrapper.RegisterForDispose(e.Connection);
+ }
+ }
+
+ private static void SetUpFormsAuthentication()
+ {
+ if (ConfigUtil.SimpleMembershipEnabled)
+ {
+ // Allow use of <add key="loginUrl" value="~/MyPath/LogOn" /> as a shortcut to specify
+ // a custom log in url
+ FormsAuthentication.EnableFormsAuthentication(new NameValueCollection()
+ {
+ { "loginUrl", ConfigUtil.LoginUrl }
+ });
+ }
+ }
+ }
+}
diff --git a/src/WebMatrix.WebData/Properties/AssemblyInfo.cs b/src/WebMatrix.WebData/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..54c7fbed
--- /dev/null
+++ b/src/WebMatrix.WebData/Properties/AssemblyInfo.cs
@@ -0,0 +1,14 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Web;
+using WebMatrix.WebData;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+
+[assembly: AssemblyTitle("WebMatrix.WebData")]
+[assembly: AssemblyDescription("")]
+[assembly: InternalsVisibleTo("WebPages.Functional.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
+[assembly: InternalsVisibleTo("WebMatrix.WebData.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
+[assembly: PreApplicationStartMethod(typeof(PreApplicationStartCode), "Start")]
diff --git a/src/WebMatrix.WebData/Resources/WebDataResources.Designer.cs b/src/WebMatrix.WebData/Resources/WebDataResources.Designer.cs
new file mode 100644
index 00000000..055daac3
--- /dev/null
+++ b/src/WebMatrix.WebData/Resources/WebDataResources.Designer.cs
@@ -0,0 +1,189 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.214
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace WebMatrix.WebData.Resources {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class WebDataResources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal WebDataResources() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("WebMatrix.WebData.Resources.WebDataResources", typeof(WebDataResources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Database operation failed..
+ /// </summary>
+ internal static string Security_DbFailure {
+ get {
+ return ResourceManager.GetString("Security_DbFailure", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to No user table found that has the name &quot;{0}&quot;..
+ /// </summary>
+ internal static string Security_FailedToFindUserTable {
+ get {
+ return ResourceManager.GetString("Security_FailedToFindUserTable", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The &quot;WebSecurity.InitializeDatabaseConnection&quot; method can be called only once..
+ /// </summary>
+ internal static string Security_InitializeAlreadyCalled {
+ get {
+ return ResourceManager.GetString("Security_InitializeAlreadyCalled", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to You must call the &quot;WebSecurity.InitializeDatabaseConnection&quot; method before you call any other method of the &quot;WebSecurity&quot; class. This call should be placed in an _AppStart.cshtml file in the root of your site..
+ /// </summary>
+ internal static string Security_InitializeMustBeCalledFirst {
+ get {
+ return ResourceManager.GetString("Security_InitializeMustBeCalledFirst", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to No account exists for &quot;{0}&quot;..
+ /// </summary>
+ internal static string Security_NoAccountFound {
+ get {
+ return ResourceManager.GetString("Security_NoAccountFound", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to To call this method, the &quot;Membership.Provider&quot; property must be an instance of &quot;ExtendedMembershipProvider&quot;..
+ /// </summary>
+ internal static string Security_NoExtendedMembershipProvider {
+ get {
+ return ResourceManager.GetString("Security_NoExtendedMembershipProvider", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to No user found was found that has the name &quot;{0}&quot;..
+ /// </summary>
+ internal static string Security_NoUserFound {
+ get {
+ return ResourceManager.GetString("Security_NoUserFound", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The membership password is too long. (Maximum length is 128 characters)..
+ /// </summary>
+ internal static string SimpleMembership_PasswordTooLong {
+ get {
+ return ResourceManager.GetString("SimpleMembership_PasswordTooLong", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Provider unrecognized attribute: &quot;{0}&quot;..
+ /// </summary>
+ internal static string SimpleMembership_ProviderUnrecognizedAttribute {
+ get {
+ return ResourceManager.GetString("SimpleMembership_ProviderUnrecognizedAttribute", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The role &quot;{0}&quot; cannot be deleted because there are still users in the role..
+ /// </summary>
+ internal static string SimpleRoleProvder_RolePopulated {
+ get {
+ return ResourceManager.GetString("SimpleRoleProvder_RolePopulated", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to User &quot;{0}&quot; is already in role &quot;{1}&quot;..
+ /// </summary>
+ internal static string SimpleRoleProvder_UserAlreadyInRole {
+ get {
+ return ResourceManager.GetString("SimpleRoleProvder_UserAlreadyInRole", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to User &quot;{0}&quot; is not in role &quot;{1}&quot;..
+ /// </summary>
+ internal static string SimpleRoleProvder_UserNotInRole {
+ get {
+ return ResourceManager.GetString("SimpleRoleProvder_UserNotInRole", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to No role found that has the name &quot;{0}&quot;..
+ /// </summary>
+ internal static string SimpleRoleProvider_NoRoleFound {
+ get {
+ return ResourceManager.GetString("SimpleRoleProvider_NoRoleFound", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Role &quot;{0}&quot; already exists..
+ /// </summary>
+ internal static string SimpleRoleProvider_RoleExists {
+ get {
+ return ResourceManager.GetString("SimpleRoleProvider_RoleExists", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/WebMatrix.WebData/Resources/WebDataResources.resx b/src/WebMatrix.WebData/Resources/WebDataResources.resx
new file mode 100644
index 00000000..02db7600
--- /dev/null
+++ b/src/WebMatrix.WebData/Resources/WebDataResources.resx
@@ -0,0 +1,162 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="Security_DbFailure" xml:space="preserve">
+ <value>Database operation failed.</value>
+ </data>
+ <data name="Security_FailedToFindUserTable" xml:space="preserve">
+ <value>No user table found that has the name "{0}".</value>
+ </data>
+ <data name="Security_InitializeAlreadyCalled" xml:space="preserve">
+ <value>The "WebSecurity.InitializeDatabaseConnection" method can be called only once.</value>
+ </data>
+ <data name="Security_InitializeMustBeCalledFirst" xml:space="preserve">
+ <value>You must call the "WebSecurity.InitializeDatabaseConnection" method before you call any other method of the "WebSecurity" class. This call should be placed in an _AppStart.cshtml file in the root of your site.</value>
+ </data>
+ <data name="Security_NoAccountFound" xml:space="preserve">
+ <value>No account exists for "{0}".</value>
+ </data>
+ <data name="Security_NoExtendedMembershipProvider" xml:space="preserve">
+ <value>To call this method, the "Membership.Provider" property must be an instance of "ExtendedMembershipProvider".</value>
+ </data>
+ <data name="Security_NoUserFound" xml:space="preserve">
+ <value>No user found was found that has the name "{0}".</value>
+ </data>
+ <data name="SimpleMembership_PasswordTooLong" xml:space="preserve">
+ <value>The membership password is too long. (Maximum length is 128 characters).</value>
+ </data>
+ <data name="SimpleMembership_ProviderUnrecognizedAttribute" xml:space="preserve">
+ <value>Provider unrecognized attribute: "{0}".</value>
+ </data>
+ <data name="SimpleRoleProvder_RolePopulated" xml:space="preserve">
+ <value>The role "{0}" cannot be deleted because there are still users in the role.</value>
+ </data>
+ <data name="SimpleRoleProvder_UserAlreadyInRole" xml:space="preserve">
+ <value>User "{0}" is already in role "{1}".</value>
+ </data>
+ <data name="SimpleRoleProvder_UserNotInRole" xml:space="preserve">
+ <value>User "{0}" is not in role "{1}".</value>
+ </data>
+ <data name="SimpleRoleProvider_NoRoleFound" xml:space="preserve">
+ <value>No role found that has the name "{0}".</value>
+ </data>
+ <data name="SimpleRoleProvider_RoleExists" xml:space="preserve">
+ <value>Role "{0}" already exists.</value>
+ </data>
+</root> \ No newline at end of file
diff --git a/src/WebMatrix.WebData/SimpleMembershipProvider.cs b/src/WebMatrix.WebData/SimpleMembershipProvider.cs
new file mode 100644
index 00000000..2783ef1c
--- /dev/null
+++ b/src/WebMatrix.WebData/SimpleMembershipProvider.cs
@@ -0,0 +1,1135 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Configuration.Provider;
+using System.Diagnostics;
+using System.Globalization;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using System.Web;
+using System.Web.Helpers;
+using System.Web.Security;
+using System.Web.WebPages;
+using Microsoft.Internal.Web.Utils;
+using WebMatrix.Data;
+using WebMatrix.WebData.Resources;
+
+namespace WebMatrix.WebData
+{
+ public class SimpleMembershipProvider : ExtendedMembershipProvider
+ {
+ private const int TokenSizeInBytes = 16;
+ private readonly MembershipProvider _previousProvider;
+
+ public SimpleMembershipProvider()
+ : this(null)
+ {
+ }
+
+ public SimpleMembershipProvider(MembershipProvider previousProvider)
+ {
+ _previousProvider = previousProvider;
+ if (_previousProvider != null)
+ {
+ _previousProvider.ValidatingPassword += (sender, args) =>
+ {
+ if (!InitializeCalled)
+ {
+ OnValidatingPassword(args);
+ }
+ };
+ }
+ }
+
+ private MembershipProvider PreviousProvider
+ {
+ get
+ {
+ if (_previousProvider == null)
+ {
+ throw new InvalidOperationException(WebDataResources.Security_InitializeMustBeCalledFirst);
+ }
+ else
+ {
+ return _previousProvider;
+ }
+ }
+ }
+
+ // Public properties
+ // Inherited from MembershipProvider ==> Forwarded to previous provider if this provider hasn't been initialized
+ public override bool EnablePasswordRetrieval
+ {
+ get { return InitializeCalled ? false : PreviousProvider.EnablePasswordRetrieval; }
+ }
+
+ // Inherited from MembershipProvider ==> Forwarded to previous provider if this provider hasn't been initialized
+ public override bool EnablePasswordReset
+ {
+ get { return InitializeCalled ? false : PreviousProvider.EnablePasswordReset; }
+ }
+
+ // Inherited from MembershipProvider ==> Forwarded to previous provider if this provider hasn't been initialized
+ public override bool RequiresQuestionAndAnswer
+ {
+ get { return InitializeCalled ? false : PreviousProvider.RequiresQuestionAndAnswer; }
+ }
+
+ // Inherited from MembershipProvider ==> Forwarded to previous provider if this provider hasn't been initialized
+ public override bool RequiresUniqueEmail
+ {
+ get { return InitializeCalled ? false : PreviousProvider.RequiresUniqueEmail; }
+ }
+
+ // Inherited from MembershipProvider ==> Forwarded to previous provider if this provider hasn't been initialized
+ public override MembershipPasswordFormat PasswordFormat
+ {
+ get { return InitializeCalled ? MembershipPasswordFormat.Hashed : PreviousProvider.PasswordFormat; }
+ }
+
+ // Inherited from MembershipProvider ==> Forwarded to previous provider if this provider hasn't been initialized
+ public override int MaxInvalidPasswordAttempts
+ {
+ get { return InitializeCalled ? Int32.MaxValue : PreviousProvider.MaxInvalidPasswordAttempts; }
+ }
+
+ // Inherited from MembershipProvider ==> Forwarded to previous provider if this provider hasn't been initialized
+ public override int PasswordAttemptWindow
+ {
+ get { return InitializeCalled ? Int32.MaxValue : PreviousProvider.PasswordAttemptWindow; }
+ }
+
+ // Inherited from MembershipProvider ==> Forwarded to previous provider if this provider hasn't been initialized
+ public override int MinRequiredPasswordLength
+ {
+ get { return InitializeCalled ? 0 : PreviousProvider.MinRequiredPasswordLength; }
+ }
+
+ // Inherited from MembershipProvider ==> Forwarded to previous provider if this provider hasn't been initialized
+ public override int MinRequiredNonAlphanumericCharacters
+ {
+ get { return InitializeCalled ? 0 : PreviousProvider.MinRequiredNonAlphanumericCharacters; }
+ }
+
+ // Inherited from MembershipProvider ==> Forwarded to previous provider if this provider hasn't been initialized
+ public override string PasswordStrengthRegularExpression
+ {
+ get { return InitializeCalled ? String.Empty : PreviousProvider.PasswordStrengthRegularExpression; }
+ }
+
+ // Inherited from MembershipProvider ==> Forwarded to previous provider if this provider hasn't been initialized
+ public override string ApplicationName
+ {
+ get
+ {
+ if (InitializeCalled)
+ {
+ throw new NotSupportedException();
+ }
+ else
+ {
+ return PreviousProvider.ApplicationName;
+ }
+ }
+ set
+ {
+ if (InitializeCalled)
+ {
+ throw new NotSupportedException();
+ }
+ else
+ {
+ PreviousProvider.ApplicationName = value;
+ }
+ }
+ }
+
+ internal static string MembershipTableName
+ {
+ get { return "webpages_Membership"; }
+ }
+
+ internal static string OAuthMembershipTableName
+ {
+ get { return "webpages_OAuthMembership"; }
+ }
+
+ private string SafeUserTableName
+ {
+ get { return "[" + UserTableName + "]"; }
+ }
+
+ private string SafeUserIdColumn
+ {
+ get { return "[" + UserIdColumn + "]"; }
+ }
+
+ private string SafeUserNameColumn
+ {
+ get { return "[" + UserNameColumn + "]"; }
+ }
+
+ // represents the User table for the app
+ public string UserTableName { get; set; }
+
+ // represents the User created UserName column, i.e. Email
+ public string UserNameColumn { get; set; }
+
+ // Represents the User created id column, i.e. ID;
+ // REVIEW: we could get this from the primary key of UserTable in the future
+ public string UserIdColumn { get; set; }
+
+ internal DatabaseConnectionInfo ConnectionInfo { get; set; }
+ internal bool InitializeCalled { get; set; }
+
+ internal override void VerifyInitialized()
+ {
+ if (!InitializeCalled)
+ {
+ throw new InvalidOperationException(WebDataResources.Security_InitializeMustBeCalledFirst);
+ }
+ }
+
+ // Inherited from ProviderBase - The "previous provider" we get has already been initialized by the Config system,
+ // so we shouldn't forward this call
+ public override void Initialize(string name, NameValueCollection config)
+ {
+ if (config == null)
+ {
+ throw new ArgumentNullException("config");
+ }
+ if (String.IsNullOrEmpty(name))
+ {
+ name = "SimpleMembershipProvider";
+ }
+ if (String.IsNullOrEmpty(config["description"]))
+ {
+ config.Remove("description");
+ config.Add("description", "Simple Membership Provider");
+ }
+ base.Initialize(name, config);
+
+ config.Remove("connectionStringName");
+ config.Remove("enablePasswordRetrieval");
+ config.Remove("enablePasswordReset");
+ config.Remove("requiresQuestionAndAnswer");
+ config.Remove("applicationName");
+ config.Remove("requiresUniqueEmail");
+ config.Remove("maxInvalidPasswordAttempts");
+ config.Remove("passwordAttemptWindow");
+ config.Remove("passwordFormat");
+ config.Remove("name");
+ config.Remove("description");
+ config.Remove("minRequiredPasswordLength");
+ config.Remove("minRequiredNonalphanumericCharacters");
+ config.Remove("passwordStrengthRegularExpression");
+ config.Remove("hashAlgorithmType");
+
+ if (config.Count > 0)
+ {
+ string attribUnrecognized = config.GetKey(0);
+ if (!String.IsNullOrEmpty(attribUnrecognized))
+ {
+ throw new ProviderException(String.Format(CultureInfo.CurrentCulture, WebDataResources.SimpleMembership_ProviderUnrecognizedAttribute, attribUnrecognized));
+ }
+ }
+ }
+
+ internal static bool CheckTableExists(IDatabase db, string tableName)
+ {
+ var query = db.QuerySingle(@"SELECT * from INFORMATION_SCHEMA.TABLES where TABLE_NAME = @0", tableName);
+ return query != null;
+ }
+
+ internal void CreateTablesIfNeeded()
+ {
+ using (var db = ConnectToDatabase())
+ {
+ if (!CheckTableExists(db, UserTableName))
+ {
+ db.Execute(@"CREATE TABLE " + SafeUserTableName + "(" + SafeUserIdColumn + " int NOT NULL PRIMARY KEY IDENTITY, " + SafeUserNameColumn + " nvarchar(56) NOT NULL UNIQUE)");
+ }
+
+ if (!CheckTableExists(db, OAuthMembershipTableName))
+ {
+ db.Execute(@"CREATE TABLE " + OAuthMembershipTableName + "(Provider nvarchar(30) NOT NULL, ProviderUserId nvarchar(100) NOT NULL, UserId int NOT NULL, Primary Key (Provider, ProviderUserId))");
+ }
+
+ if (!CheckTableExists(db, MembershipTableName))
+ {
+ db.Execute(@"CREATE TABLE " + MembershipTableName + @" (
+ UserId int NOT NULL PRIMARY KEY,
+ CreateDate datetime ,
+ ConfirmationToken nvarchar(128) ,
+ IsConfirmed bit DEFAULT 0,
+ LastPasswordFailureDate datetime ,
+ PasswordFailuresSinceLastSuccess int NOT NULL DEFAULT 0,
+ Password nvarchar(128) NOT NULL,
+ PasswordChangedDate datetime ,
+ PasswordSalt nvarchar(128) NOT NULL,
+ PasswordVerificationToken nvarchar(128) ,
+ PasswordVerificationTokenExpirationDate datetime)");
+ // TODO: Do we want to add FK constraint to user table too?
+ // CONSTRAINT fk_UserId FOREIGN KEY (UserId) REFERENCES "+UserTableName+"("+UserIdColumn+"))");
+ }
+ }
+ }
+
+ // Not an override ==> Simple Membership MUST be enabled to use this method
+ public int GetUserId(string userName)
+ {
+ VerifyInitialized();
+ using (var db = ConnectToDatabase())
+ {
+ return GetUserId(db, SafeUserTableName, SafeUserNameColumn, SafeUserIdColumn, userName);
+ }
+ }
+
+ internal static int GetUserId(IDatabase db, string userTableName, string userNameColumn, string userIdColumn, string userName)
+ {
+ var result = db.QueryValue(@"SELECT " + userIdColumn + " FROM " + userTableName + " WHERE (UPPER(" + userNameColumn + ") = @0)", userName.ToUpperInvariant());
+ if (result != null)
+ {
+ return (int)result;
+ }
+ return -1;
+ }
+
+ // Inherited from ExtendedMembershipProvider ==> Simple Membership MUST be enabled to use this method
+ public override int GetUserIdFromPasswordResetToken(string token)
+ {
+ VerifyInitialized();
+ using (var db = ConnectToDatabase())
+ {
+ var result = db.QuerySingle(@"SELECT UserId FROM " + MembershipTableName + " WHERE (PasswordVerificationToken = @0)", token);
+ if (result != null && result[0] != null)
+ {
+ return (int)result[0];
+ }
+ return -1;
+ }
+ }
+
+ // Inherited from MembershipProvider ==> Forwarded to previous provider if this provider hasn't been initialized
+ public override bool ChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer)
+ {
+ if (!InitializeCalled)
+ {
+ return PreviousProvider.ChangePasswordQuestionAndAnswer(username, password, newPasswordQuestion, newPasswordAnswer);
+ }
+ throw new NotSupportedException();
+ }
+
+ /// <summary>
+ /// Sets the confirmed flag for the username if it is correct.
+ /// </summary>
+ /// <returns>True if the account could be successfully confirmed. False if the username was not found or the confirmation token is invalid.</returns>
+ /// <remarks>Inherited from ExtendedMembershipProvider ==> Simple Membership MUST be enabled to use this method</remarks>
+ public override bool ConfirmAccount(string userName, string accountConfirmationToken)
+ {
+ VerifyInitialized();
+ using (var db = ConnectToDatabase())
+ {
+ // We need to compare the token using a case insensitive comparison however it seems tricky to do this uniformly across databases when representing the token as a string.
+ // Therefore verify the case on the client
+ var row = db.QuerySingle("SELECT m.[UserId], m.[ConfirmationToken] FROM " + MembershipTableName + " m JOIN " + SafeUserTableName + " u"
+ + " ON m.[UserId] = u." + SafeUserIdColumn
+ + " WHERE m.[ConfirmationToken] = @0 AND"
+ + " u." + SafeUserNameColumn + " = @1", accountConfirmationToken, userName);
+ if (row == null)
+ {
+ return false;
+ }
+ int userId = row[0];
+ string expectedToken = row[1];
+
+ if (String.Equals(accountConfirmationToken, expectedToken, StringComparison.Ordinal))
+ {
+ int affectedRows = db.Execute("UPDATE " + MembershipTableName + " SET [IsConfirmed] = 1 WHERE [UserId] = @0", userId);
+ return affectedRows > 0;
+ }
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// Sets the confirmed flag for the username if it is correct.
+ /// </summary>
+ /// <returns>True if the account could be successfully confirmed. False if the username was not found or the confirmation token is invalid.</returns>
+ /// <remarks>Inherited from ExtendedMembershipProvider ==> Simple Membership MUST be enabled to use this method.
+ /// There is a tiny possibility where this method fails to work correctly. Two or more users could be assigned the same token but specified using different cases.
+ /// A workaround for this would be to use the overload that accepts both the user name and confirmation token.
+ /// </remarks>
+ public override bool ConfirmAccount(string accountConfirmationToken)
+ {
+ VerifyInitialized();
+ using (var db = ConnectToDatabase())
+ {
+ // We need to compare the token using a case insensitive comparison however it seems tricky to do this uniformly across databases when representing the token as a string.
+ // Therefore verify the case on the client
+ var rows = db.Query("SELECT [UserId], [ConfirmationToken] FROM " + MembershipTableName + " WHERE [ConfirmationToken] = @0", accountConfirmationToken)
+ .Where(r => ((string)r[1]).Equals(accountConfirmationToken, StringComparison.Ordinal))
+ .ToList();
+ Debug.Assert(rows.Count < 2, "By virtue of the fact that the ConfirmationToken is random and unique, we can never have two tokens that are identical.");
+ if (!rows.Any())
+ {
+ return false;
+ }
+ var row = rows.First();
+ int userId = row[0];
+ int affectedRows = db.Execute("UPDATE " + MembershipTableName + " SET [IsConfirmed] = 1 WHERE [UserId] = @0", userId);
+ return affectedRows > 0;
+ }
+ }
+
+ internal virtual IDatabase ConnectToDatabase()
+ {
+ return new DatabaseWrapper(ConnectionInfo.Connect());
+ }
+
+ // Inherited from ExtendedMembershipProvider ==> Simple Membership MUST be enabled to use this method
+ public override string CreateAccount(string userName, string password, bool requireConfirmationToken)
+ {
+ VerifyInitialized();
+
+ if (password.IsEmpty())
+ {
+ throw new MembershipCreateUserException(MembershipCreateStatus.InvalidPassword);
+ }
+
+ string hashedPassword = Crypto.HashPassword(password);
+ if (hashedPassword.Length > 128)
+ {
+ throw new MembershipCreateUserException(MembershipCreateStatus.InvalidPassword);
+ }
+
+ if (userName.IsEmpty())
+ {
+ throw new MembershipCreateUserException(MembershipCreateStatus.InvalidUserName);
+ }
+
+ using (var db = ConnectToDatabase())
+ {
+ // Step 1: Check if the user exists in the Users table
+ int uid = GetUserId(db, SafeUserTableName, SafeUserNameColumn, SafeUserIdColumn, userName);
+ if (uid == -1)
+ {
+ // User not found
+ throw new MembershipCreateUserException(MembershipCreateStatus.ProviderError);
+ }
+
+ // Step 2: Check if the user exists in the Membership table: Error if yes.
+ var result = db.QuerySingle(@"SELECT COUNT(*) FROM [" + MembershipTableName + "] WHERE UserId = @0", uid);
+ if (result[0] > 0)
+ {
+ throw new MembershipCreateUserException(MembershipCreateStatus.DuplicateUserName);
+ }
+
+ // Step 3: Create user in Membership table
+ string token = null;
+ object dbtoken = DBNull.Value;
+ if (requireConfirmationToken)
+ {
+ token = GenerateToken();
+ dbtoken = token;
+ }
+ int defaultNumPasswordFailures = 0;
+
+ int insert = db.Execute(@"INSERT INTO [" + MembershipTableName + "] (UserId, [Password], PasswordSalt, IsConfirmed, ConfirmationToken, CreateDate, PasswordChangedDate, PasswordFailuresSinceLastSuccess)"
+ + " VALUES (@0, @1, @2, @3, @4, @5, @5, @6)", uid, hashedPassword, String.Empty /* salt column is unused */, !requireConfirmationToken, dbtoken, DateTime.UtcNow, defaultNumPasswordFailures);
+ if (insert != 1)
+ {
+ throw new MembershipCreateUserException(MembershipCreateStatus.ProviderError);
+ }
+ return token;
+ }
+ }
+
+ // Inherited from MembershipProvider ==> Forwarded to previous provider if this provider hasn't been initialized
+ public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status)
+ {
+ if (!InitializeCalled)
+ {
+ return PreviousProvider.CreateUser(username, password, email, passwordQuestion, passwordAnswer, isApproved, providerUserKey, out status);
+ }
+ throw new NotSupportedException();
+ }
+
+ private void CreateUserRow(IDatabase db, string userName, IDictionary<string, object> values)
+ {
+ // Make sure user doesn't exist
+ int userId = GetUserId(db, SafeUserTableName, SafeUserNameColumn, SafeUserIdColumn, userName);
+ if (userId != -1)
+ {
+ throw new MembershipCreateUserException(MembershipCreateStatus.DuplicateUserName);
+ }
+
+ StringBuilder columnString = new StringBuilder();
+ columnString.Append(SafeUserNameColumn);
+ StringBuilder argsString = new StringBuilder();
+ argsString.Append("@0");
+ List<object> argsArray = new List<object>();
+ argsArray.Add(userName);
+ if (values != null)
+ {
+ int index = 1;
+ foreach (string key in values.Keys)
+ {
+ // Skip the user name column since we always generate that
+ if (String.Equals(UserNameColumn, key, StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+ columnString.Append(",").Append(key);
+ argsString.Append(",@").Append(index++);
+ object value = values[key];
+ if (value == null)
+ {
+ value = DBNull.Value;
+ }
+ argsArray.Add(value);
+ }
+ }
+
+ int rows = db.Execute("INSERT INTO " + SafeUserTableName + " (" + columnString.ToString() + ") VALUES (" + argsString.ToString() + ")", argsArray.ToArray());
+ if (rows != 1)
+ {
+ throw new MembershipCreateUserException(MembershipCreateStatus.ProviderError);
+ }
+ }
+
+ // Inherited from ExtendedMembershipProvider ==> Simple Membership MUST be enabled to use this method
+ public override string CreateUserAndAccount(string userName, string password, bool requireConfirmation, IDictionary<string, object> values)
+ {
+ VerifyInitialized();
+
+ using (var db = ConnectToDatabase())
+ {
+ CreateUserRow(db, userName, values);
+ return CreateAccount(userName, password, requireConfirmation);
+ }
+ }
+
+ // Inherited from MembershipProvider ==> Forwarded to previous provider if this provider hasn't been initialized
+ public override string GetPassword(string username, string answer)
+ {
+ if (!InitializeCalled)
+ {
+ return PreviousProvider.GetPassword(username, answer);
+ }
+ throw new NotSupportedException();
+ }
+
+ private static bool SetPassword(IDatabase db, int userId, string newPassword)
+ {
+ string hashedPassword = Crypto.HashPassword(newPassword);
+ if (hashedPassword.Length > 128)
+ {
+ throw new ArgumentException(WebDataResources.SimpleMembership_PasswordTooLong);
+ }
+
+ // Update new password
+ int result = db.Execute(@"UPDATE " + MembershipTableName + " SET Password=@0, PasswordSalt=@1, PasswordChangedDate=@2 WHERE UserId = @3", hashedPassword, String.Empty /* salt column is unused */, DateTime.UtcNow, userId);
+ return result > 0;
+ }
+
+ // Inherited from MembershipProvider ==> Forwarded to previous provider if this provider hasn't been initialized
+ public override bool ChangePassword(string username, string oldPassword, string newPassword)
+ {
+ if (!InitializeCalled)
+ {
+ return PreviousProvider.ChangePassword(username, oldPassword, newPassword);
+ }
+
+ // REVIEW: are commas special in the password?
+ if (username.IsEmpty())
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "username");
+ }
+ if (oldPassword.IsEmpty())
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "oldPassword");
+ }
+ if (newPassword.IsEmpty())
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "newPassword");
+ }
+
+ using (var db = ConnectToDatabase())
+ {
+ int userId = GetUserId(db, SafeUserTableName, SafeUserNameColumn, SafeUserIdColumn, username);
+ if (userId == -1)
+ {
+ return false; // User not found
+ }
+
+ // First check that the old credentials match
+ if (!CheckPassword(db, userId, oldPassword))
+ {
+ return false;
+ }
+
+ return SetPassword(db, userId, newPassword);
+ }
+ }
+
+ // Inherited from MembershipProvider ==> Forwarded to previous provider if this provider hasn't been initialized
+ public override string ResetPassword(string username, string answer)
+ {
+ if (!InitializeCalled)
+ {
+ return PreviousProvider.ResetPassword(username, answer);
+ }
+ throw new NotSupportedException();
+ }
+
+ // Inherited from MembershipProvider ==> Forwarded to previous provider if this provider hasn't been initialized
+ public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
+ {
+ if (!InitializeCalled)
+ {
+ return PreviousProvider.GetUser(providerUserKey, userIsOnline);
+ }
+ throw new NotSupportedException();
+ }
+
+ // Inherited from MembershipProvider ==> Forwarded to previous provider if this provider hasn't been initialized
+ public override MembershipUser GetUser(string username, bool userIsOnline)
+ {
+ if (!InitializeCalled)
+ {
+ return PreviousProvider.GetUser(username, userIsOnline);
+ }
+
+ // Due to a bug in v1, GetUser allows passing null / empty values.
+ using (var db = ConnectToDatabase())
+ {
+ int userId = GetUserId(db, SafeUserTableName, SafeUserNameColumn, SafeUserIdColumn, username);
+ if (userId == -1)
+ {
+ return null; // User not found
+ }
+
+ return new MembershipUser(Membership.Provider.Name, username, userId, null, null, null, true, false, DateTime.MinValue, DateTime.MinValue, DateTime.MinValue, DateTime.MinValue, DateTime.MinValue);
+ }
+ }
+
+ // Inherited from MembershipProvider ==> Forwarded to previous provider if this provider hasn't been initialized
+ public override string GetUserNameByEmail(string email)
+ {
+ if (!InitializeCalled)
+ {
+ return PreviousProvider.GetUserNameByEmail(email);
+ }
+ throw new NotSupportedException();
+ }
+
+ // Inherited from ExtendedMembershipProvider ==> Simple Membership MUST be enabled to use this method
+ public override bool DeleteAccount(string userName)
+ {
+ VerifyInitialized();
+
+ using (var db = ConnectToDatabase())
+ {
+ int userId = GetUserId(db, SafeUserTableName, SafeUserNameColumn, SafeUserIdColumn, userName);
+ if (userId == -1)
+ {
+ return false; // User not found
+ }
+
+ int deleted = db.Execute(@"DELETE FROM " + MembershipTableName + " WHERE UserId = @0", userId);
+ return (deleted == 1);
+ }
+ }
+
+ // Inherited from MembershipProvider ==> Forwarded to previous provider if this provider hasn't been initialized
+ public override bool DeleteUser(string username, bool deleteAllRelatedData)
+ {
+ if (!InitializeCalled)
+ {
+ return PreviousProvider.DeleteUser(username, deleteAllRelatedData);
+ }
+
+ using (var db = ConnectToDatabase())
+ {
+ int userId = GetUserId(db, SafeUserTableName, SafeUserNameColumn, SafeUserIdColumn, username);
+ if (userId == -1)
+ {
+ return false; // User not found
+ }
+
+ int deleted = db.Execute(@"DELETE FROM " + SafeUserTableName + " WHERE " + SafeUserIdColumn + " = @0", userId);
+ bool returnValue = (deleted == 1);
+
+ //if (deleteAllRelatedData) {
+ // REVIEW: do we really want to delete from the user table?
+ //}
+ return returnValue;
+ }
+ }
+
+ internal bool DeleteUserAndAccountInternal(string userName)
+ {
+ return (DeleteAccount(userName) && DeleteUser(userName, false));
+ }
+
+ // Inherited from MembershipProvider ==> Forwarded to previous provider if this provider hasn't been initialized
+ public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords)
+ {
+ if (!InitializeCalled)
+ {
+ return PreviousProvider.GetAllUsers(pageIndex, pageSize, out totalRecords);
+ }
+ throw new NotSupportedException();
+ }
+
+ // Inherited from MembershipProvider ==> Forwarded to previous provider if this provider hasn't been initialized
+ public override int GetNumberOfUsersOnline()
+ {
+ if (!InitializeCalled)
+ {
+ return PreviousProvider.GetNumberOfUsersOnline();
+ }
+ throw new NotSupportedException();
+ }
+
+ // Inherited from MembershipProvider ==> Forwarded to previous provider if this provider hasn't been initialized
+ public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
+ {
+ if (!InitializeCalled)
+ {
+ return PreviousProvider.FindUsersByName(usernameToMatch, pageIndex, pageSize, out totalRecords);
+ }
+ throw new NotSupportedException();
+ }
+
+ // Inherited from MembershipProvider ==> Forwarded to previous provider if this provider hasn't been initialized
+ public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
+ {
+ if (!InitializeCalled)
+ {
+ return PreviousProvider.FindUsersByEmail(emailToMatch, pageIndex, pageSize, out totalRecords);
+ }
+ throw new NotSupportedException();
+ }
+
+ private static int GetPasswordFailuresSinceLastSuccess(IDatabase db, int userId)
+ {
+ var failure = db.QueryValue(@"SELECT PasswordFailuresSinceLastSuccess FROM " + MembershipTableName + " WHERE (UserId = @0)", userId);
+ if (failure != null)
+ {
+ return failure;
+ }
+ return -1;
+ }
+
+ // Inherited from ExtendedMembershipProvider ==> Simple Membership MUST be enabled to use this method
+ public override int GetPasswordFailuresSinceLastSuccess(string userName)
+ {
+ using (var db = ConnectToDatabase())
+ {
+ int userId = GetUserId(db, SafeUserTableName, SafeUserNameColumn, SafeUserIdColumn, userName);
+ if (userId == -1)
+ {
+ throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, WebDataResources.Security_NoUserFound, userName));
+ }
+
+ return GetPasswordFailuresSinceLastSuccess(db, userId);
+ }
+ }
+
+ // Inherited from ExtendedMembershipProvider ==> Simple Membership MUST be enabled to use this method
+ public override DateTime GetCreateDate(string userName)
+ {
+ using (var db = ConnectToDatabase())
+ {
+ int userId = GetUserId(db, SafeUserTableName, SafeUserNameColumn, SafeUserIdColumn, userName);
+ if (userId == -1)
+ {
+ throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, WebDataResources.Security_NoUserFound, userName));
+ }
+
+ var createDate = db.QueryValue(@"SELECT CreateDate FROM " + MembershipTableName + " WHERE (UserId = @0)", userId);
+ if (createDate != null)
+ {
+ return createDate;
+ }
+ return DateTime.MinValue;
+ }
+ }
+
+ // Inherited from ExtendedMembershipProvider ==> Simple Membership MUST be enabled to use this method
+ public override DateTime GetPasswordChangedDate(string userName)
+ {
+ using (var db = ConnectToDatabase())
+ {
+ int userId = GetUserId(db, SafeUserTableName, SafeUserNameColumn, SafeUserIdColumn, userName);
+ if (userId == -1)
+ {
+ throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, WebDataResources.Security_NoUserFound, userName));
+ }
+
+ var pwdChangeDate = db.QuerySingle(@"SELECT PasswordChangedDate FROM " + MembershipTableName + " WHERE (UserId = @0)", userId);
+ if (pwdChangeDate != null && pwdChangeDate[0] != null)
+ {
+ return (DateTime)pwdChangeDate[0];
+ }
+ return DateTime.MinValue;
+ }
+ }
+
+ // Inherited from ExtendedMembershipProvider ==> Simple Membership MUST be enabled to use this method
+ public override DateTime GetLastPasswordFailureDate(string userName)
+ {
+ using (var db = ConnectToDatabase())
+ {
+ int userId = GetUserId(db, SafeUserTableName, SafeUserNameColumn, SafeUserIdColumn, userName);
+ if (userId == -1)
+ {
+ throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, WebDataResources.Security_NoUserFound, userName));
+ }
+
+ var failureDate = db.QuerySingle(@"SELECT LastPasswordFailureDate FROM " + MembershipTableName + " WHERE (UserId = @0)", userId);
+ if (failureDate != null && failureDate[0] != null)
+ {
+ return (DateTime)failureDate[0];
+ }
+ return DateTime.MinValue;
+ }
+ }
+
+ private bool CheckPassword(IDatabase db, int userId, string password)
+ {
+ string hashedPassword = GetHashedPassword(db, userId);
+ bool verificationSucceeded = (hashedPassword != null && Crypto.VerifyHashedPassword(hashedPassword, password));
+ if (verificationSucceeded)
+ {
+ // Reset password failure count on successful credential check
+ db.Execute(@"UPDATE " + MembershipTableName + " SET PasswordFailuresSinceLastSuccess = 0 WHERE (UserId = @0)", userId);
+ }
+ else
+ {
+ int failures = GetPasswordFailuresSinceLastSuccess(db, userId);
+ if (failures != -1)
+ {
+ db.Execute(@"UPDATE " + MembershipTableName + " SET PasswordFailuresSinceLastSuccess = @1, LastPasswordFailureDate = @2 WHERE (UserId = @0)", userId, failures + 1, DateTime.UtcNow);
+ }
+ }
+ return verificationSucceeded;
+ }
+
+ private string GetHashedPassword(IDatabase db, int userId)
+ {
+ var pwdQuery = db.Query(@"SELECT m.[Password] " +
+ @"FROM " + MembershipTableName + " m, " + SafeUserTableName + " u " +
+ @"WHERE m.UserId = " + userId + " AND m.UserId = u." + SafeUserIdColumn).ToList();
+ // REVIEW: Should get exactly one match, should we throw if we get > 1?
+ if (pwdQuery.Count != 1)
+ {
+ return null;
+ }
+ return pwdQuery[0].Password;
+ }
+
+ // Ensures the user exists in the accounts table
+ private int VerifyUserNameHasConfirmedAccount(IDatabase db, string username, bool throwException)
+ {
+ int userId = GetUserId(db, SafeUserTableName, SafeUserNameColumn, SafeUserIdColumn, username);
+ if (userId == -1)
+ {
+ if (throwException)
+ {
+ throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, WebDataResources.Security_NoUserFound, username));
+ }
+ else
+ {
+ return -1;
+ }
+ }
+
+ int result = db.QueryValue(@"SELECT COUNT(*) FROM " + MembershipTableName + " WHERE (UserId = @0 AND IsConfirmed = 1)", userId);
+ if (result == 0)
+ {
+ if (throwException)
+ {
+ throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, WebDataResources.Security_NoAccountFound, username));
+ }
+ else
+ {
+ return -1;
+ }
+ }
+ return userId;
+ }
+
+ private static string GenerateToken()
+ {
+ using (var prng = new RNGCryptoServiceProvider())
+ {
+ return GenerateToken(prng);
+ }
+ }
+
+ internal static string GenerateToken(RandomNumberGenerator generator)
+ {
+ byte[] tokenBytes = new byte[TokenSizeInBytes];
+ generator.GetBytes(tokenBytes);
+ return HttpServerUtility.UrlTokenEncode(tokenBytes);
+ }
+
+ // Inherited from ExtendedMembershipProvider ==> Simple Membership MUST be enabled to use this method
+ public override string GeneratePasswordResetToken(string userName, int tokenExpirationInMinutesFromNow)
+ {
+ VerifyInitialized();
+ if (userName.IsEmpty())
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "userName");
+ }
+ using (var db = ConnectToDatabase())
+ {
+ int userId = VerifyUserNameHasConfirmedAccount(db, userName, throwException: true);
+
+ string token = db.QueryValue(@"SELECT PasswordVerificationToken FROM " + MembershipTableName + " WHERE (UserId = @0 AND PasswordVerificationTokenExpirationDate > @1)", userId, DateTime.UtcNow);
+ if (token == null)
+ {
+ token = GenerateToken();
+
+ int rows = db.Execute(@"UPDATE " + MembershipTableName + " SET PasswordVerificationToken = @0, PasswordVerificationTokenExpirationDate = @1 WHERE (UserId = @2)", token, DateTime.UtcNow.AddMinutes(tokenExpirationInMinutesFromNow), userId);
+ if (rows != 1)
+ {
+ throw new ProviderException(WebDataResources.Security_DbFailure);
+ }
+ }
+ else
+ {
+ // TODO: should we update expiry again?
+ }
+ return token;
+ }
+ }
+
+ // Inherited from ExtendedMembershipProvider ==> Simple Membership MUST be enabled to use this method
+ public override bool IsConfirmed(string userName)
+ {
+ VerifyInitialized();
+ if (userName.IsEmpty())
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "userName");
+ }
+
+ using (var db = ConnectToDatabase())
+ {
+ int userId = VerifyUserNameHasConfirmedAccount(db, userName, throwException: false);
+ return (userId != -1);
+ }
+ }
+
+ // Inherited from ExtendedMembershipProvider ==> Simple Membership MUST be enabled to use this method
+ public override bool ResetPasswordWithToken(string token, string newPassword)
+ {
+ VerifyInitialized();
+ if (newPassword.IsEmpty())
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "newPassword");
+ }
+ using (var db = ConnectToDatabase())
+ {
+ int? userId = db.QueryValue(@"SELECT UserId FROM " + MembershipTableName + " WHERE (PasswordVerificationToken = @0 AND PasswordVerificationTokenExpirationDate > @1)", token, DateTime.UtcNow);
+ if (userId != null)
+ {
+ bool success = SetPassword(db, userId.Value, newPassword);
+ if (success)
+ {
+ // Clear the Token on success
+ int rows = db.Execute(@"UPDATE " + MembershipTableName + " SET PasswordVerificationToken = NULL, PasswordVerificationTokenExpirationDate = NULL WHERE (UserId = @0)", userId);
+ if (rows != 1)
+ {
+ throw new ProviderException(WebDataResources.Security_DbFailure);
+ }
+ }
+ return success;
+ }
+ else
+ {
+ return false;
+ }
+ }
+ }
+
+ // Inherited from MembershipProvider ==> Forwarded to previous provider if this provider hasn't been initialized
+ public override void UpdateUser(MembershipUser user)
+ {
+ if (!InitializeCalled)
+ {
+ PreviousProvider.UpdateUser(user);
+ }
+ else
+ {
+ throw new NotSupportedException();
+ }
+ }
+
+ // Inherited from MembershipProvider ==> Forwarded to previous provider if this provider hasn't been initialized
+ public override bool UnlockUser(string userName)
+ {
+ if (!InitializeCalled)
+ {
+ return PreviousProvider.UnlockUser(userName);
+ }
+ throw new NotSupportedException();
+ }
+
+ internal void ValidateUserTable()
+ {
+ using (var db = ConnectToDatabase())
+ {
+ // GetUser will fail with an exception if the user table isn't set up properly
+ try
+ {
+ GetUserId(db, SafeUserTableName, SafeUserNameColumn, SafeUserIdColumn, "z");
+ }
+ catch (Exception e)
+ {
+ throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, WebDataResources.Security_FailedToFindUserTable, UserTableName), e);
+ }
+ }
+ }
+
+ // Inherited from MembershipProvider ==> Forwarded to previous provider if this provider hasn't been initialized
+ public override bool ValidateUser(string username, string password)
+ {
+ if (!InitializeCalled)
+ {
+ return PreviousProvider.ValidateUser(username, password);
+ }
+ if (username.IsEmpty())
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "username");
+ }
+ if (password.IsEmpty())
+ {
+ throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "password");
+ }
+
+ using (var db = ConnectToDatabase())
+ {
+ int userId = VerifyUserNameHasConfirmedAccount(db, username, throwException: false);
+ if (userId == -1)
+ {
+ return false;
+ }
+ else
+ {
+ return CheckPassword(db, userId, password);
+ }
+ }
+ }
+
+ public override string GetUserNameFromId(int userId)
+ {
+ VerifyInitialized();
+
+ using (var db = ConnectToDatabase())
+ {
+ dynamic username = db.QueryValue("SELECT " + SafeUserNameColumn + " FROM " + SafeUserTableName + " WHERE (" + SafeUserIdColumn + "=@0)", userId);
+ return (string)username;
+ }
+ }
+
+ public override void CreateOrUpdateOAuthAccount(string provider, string providerUserId, string userName)
+ {
+ VerifyInitialized();
+
+ if (userName.IsEmpty())
+ {
+ throw new MembershipCreateUserException(MembershipCreateStatus.ProviderError);
+ }
+
+ int userId = GetUserId(userName);
+ if (userId == -1)
+ {
+ throw new MembershipCreateUserException(MembershipCreateStatus.InvalidUserName);
+ }
+
+ var oldUserId = GetUserIdFromOAuth(provider, providerUserId);
+ using (var db = ConnectToDatabase())
+ {
+ if (oldUserId == -1)
+ {
+ // account doesn't exist. create a new one.
+ int insert = db.Execute(@"INSERT INTO [" + OAuthMembershipTableName + "] (Provider, ProviderUserId, UserId) VALUES (@0, @1, @2)", provider, providerUserId, userId);
+ if (insert != 1)
+ {
+ throw new MembershipCreateUserException(MembershipCreateStatus.ProviderError);
+ }
+ }
+ else
+ {
+ // account already exist. update it
+ int insert = db.Execute(@"UPDATE [" + OAuthMembershipTableName + "] SET UserId = @2 WHERE UPPER(Provider)=@0 AND UPPER(ProviderUserId)=@1", provider, providerUserId, userId);
+ if (insert != 1)
+ {
+ throw new MembershipCreateUserException(MembershipCreateStatus.ProviderError);
+ }
+ }
+ }
+ }
+
+ public override void DeleteOAuthAccount(string provider, string providerUserId)
+ {
+ VerifyInitialized();
+
+ using (var db = ConnectToDatabase())
+ {
+ // account doesn't exist. create a new one.
+ int insert = db.Execute(@"DELETE FROM [" + OAuthMembershipTableName + "] WHERE UPPER(Provider)=@0 AND UPPER(ProviderUserId)=@1", provider, providerUserId);
+ if (insert != 1)
+ {
+ throw new MembershipCreateUserException(MembershipCreateStatus.ProviderError);
+ }
+ }
+ }
+
+ public override int GetUserIdFromOAuth(string provider, string providerUserId)
+ {
+ VerifyInitialized();
+
+ using (var db = ConnectToDatabase())
+ {
+ dynamic id = db.QueryValue(@"SELECT UserId FROM [" + OAuthMembershipTableName + "] WHERE UPPER(Provider)=@0 AND UPPER(ProviderUserId)=@1", provider.ToUpperInvariant(), providerUserId.ToUpperInvariant());
+ if (id != null)
+ {
+ return (int)id;
+ }
+
+ return -1;
+ }
+ }
+
+ public override ICollection<OAuthAccountData> GetAccountsForUser(string userName)
+ {
+ VerifyInitialized();
+
+ int userId = GetUserId(userName);
+ if (userId != -1)
+ {
+ using (var db = ConnectToDatabase())
+ {
+ IEnumerable<dynamic> records = db.Query(@"SELECT Provider, ProviderUserId FROM [" + OAuthMembershipTableName + "] WHERE UserId=@0", userId);
+ if (records != null)
+ {
+ var accounts = new List<OAuthAccountData>();
+ foreach (DynamicRecord row in records)
+ {
+ accounts.Add(new OAuthAccountData((string)row["Provider"], (string)row["ProviderUserId"]));
+ }
+ return accounts;
+ }
+ }
+ }
+
+ return new OAuthAccountData[0];
+ }
+ }
+}
diff --git a/src/WebMatrix.WebData/SimpleRoleProvider.cs b/src/WebMatrix.WebData/SimpleRoleProvider.cs
new file mode 100644
index 00000000..d2ee8ced
--- /dev/null
+++ b/src/WebMatrix.WebData/SimpleRoleProvider.cs
@@ -0,0 +1,419 @@
+using System;
+using System.Collections.Generic;
+using System.Configuration.Provider;
+using System.Globalization;
+using System.Linq;
+using System.Web.Security;
+using WebMatrix.WebData.Resources;
+
+namespace WebMatrix.WebData
+{
+ public class SimpleRoleProvider : RoleProvider
+ {
+ private RoleProvider _previousProvider;
+
+ public SimpleRoleProvider()
+ : this(null)
+ {
+ }
+
+ public SimpleRoleProvider(RoleProvider previousProvider)
+ {
+ _previousProvider = previousProvider;
+ }
+
+ private RoleProvider PreviousProvider
+ {
+ get
+ {
+ if (_previousProvider == null)
+ {
+ throw new InvalidOperationException(WebDataResources.Security_InitializeMustBeCalledFirst);
+ }
+ else
+ {
+ return _previousProvider;
+ }
+ }
+ }
+
+ private string SafeUserTableName
+ {
+ get { return "[" + UserTableName + "]"; }
+ }
+
+ private string SafeUserNameColumn
+ {
+ get { return "[" + UserNameColumn + "]"; }
+ }
+
+ private string SafeUserIdColumn
+ {
+ get { return "[" + UserIdColumn + "]"; }
+ }
+
+ internal static string RoleTableName
+ {
+ get { return "webpages_Roles"; }
+ }
+
+ internal static string UsersInRoleTableName
+ {
+ get { return "webpages_UsersInRoles"; }
+ }
+
+ // represents the User table for the app
+ public string UserTableName { get; set; }
+
+ // represents the User created UserName column, i.e. Email
+ public string UserNameColumn { get; set; }
+
+ // Represents the User created id column, i.e. ID;
+ // REVIEW: we could get this from the primary key of UserTable in the future
+ public string UserIdColumn { get; set; }
+
+ internal DatabaseConnectionInfo ConnectionInfo { get; set; }
+ internal bool InitializeCalled { get; set; }
+
+ // Inherited from RoleProvider ==> Forwarded to previous provider if this provider hasn't been initialized
+ public override string ApplicationName
+ {
+ get
+ {
+ if (InitializeCalled)
+ {
+ throw new NotSupportedException();
+ }
+ else
+ {
+ return PreviousProvider.ApplicationName;
+ }
+ }
+ set
+ {
+ if (InitializeCalled)
+ {
+ throw new NotSupportedException();
+ }
+ else
+ {
+ PreviousProvider.ApplicationName = value;
+ }
+ }
+ }
+
+ private void VerifyInitialized()
+ {
+ if (!InitializeCalled)
+ {
+ throw new InvalidOperationException(WebDataResources.Security_InitializeMustBeCalledFirst);
+ }
+ }
+
+ private IDatabase ConnectToDatabase()
+ {
+ return new DatabaseWrapper(ConnectionInfo.Connect());
+ }
+
+ internal void CreateTablesIfNeeded()
+ {
+ using (var db = ConnectToDatabase())
+ {
+ if (!SimpleMembershipProvider.CheckTableExists(db, RoleTableName))
+ {
+ db.Execute(@"CREATE TABLE " + RoleTableName + @" (
+ RoleId int NOT NULL PRIMARY KEY IDENTITY,
+ RoleName nvarchar(256) NOT NULL UNIQUE)");
+
+ db.Execute(@"CREATE TABLE " + UsersInRoleTableName + @" (
+ UserId int NOT NULL,
+ RoleId int NOT NULL,
+ PRIMARY KEY (UserId, RoleId),
+ CONSTRAINT fk_UserId FOREIGN KEY (UserId) REFERENCES " + SafeUserTableName + "(" + SafeUserIdColumn + @"),
+ CONSTRAINT fk_RoleId FOREIGN KEY (RoleId) REFERENCES " + RoleTableName + "(RoleId) )");
+ }
+ }
+ }
+
+ private List<int> GetUserIdsFromNames(IDatabase db, string[] usernames)
+ {
+ List<int> userIds = new List<int>(usernames.Length);
+ foreach (string username in usernames)
+ {
+ int id = SimpleMembershipProvider.GetUserId(db, SafeUserTableName, SafeUserNameColumn, SafeUserIdColumn, username);
+ if (id == -1)
+ {
+ throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, WebDataResources.Security_NoUserFound, username));
+ }
+ userIds.Add(id);
+ }
+ return userIds;
+ }
+
+ private static List<int> GetRoleIdsFromNames(IDatabase db, string[] roleNames)
+ {
+ List<int> roleIds = new List<int>(roleNames.Length);
+ foreach (string role in roleNames)
+ {
+ int id = FindRoleId(db, role);
+ if (id == -1)
+ {
+ throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, WebDataResources.SimpleRoleProvider_NoRoleFound, role));
+ }
+ roleIds.Add(id);
+ }
+ return roleIds;
+ }
+
+ // Inherited from RoleProvider ==> Forwarded to previous provider if this provider hasn't been initialized
+ public override void AddUsersToRoles(string[] usernames, string[] roleNames)
+ {
+ if (!InitializeCalled)
+ {
+ PreviousProvider.AddUsersToRoles(usernames, roleNames);
+ }
+ else
+ {
+ using (var db = ConnectToDatabase())
+ {
+ int userCount = usernames.Length;
+ int roleCount = roleNames.Length;
+ List<int> userIds = GetUserIdsFromNames(db, usernames);
+ List<int> roleIds = GetRoleIdsFromNames(db, roleNames);
+
+ // Generate a INSERT INTO for each userid/rowid combination, where userIds are the first params, and roleIds follow
+ for (int uId = 0; uId < userCount; uId++)
+ {
+ for (int rId = 0; rId < roleCount; rId++)
+ {
+ if (IsUserInRole(usernames[uId], roleNames[rId]))
+ {
+ throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, WebDataResources.SimpleRoleProvder_UserAlreadyInRole, usernames[uId], roleNames[rId]));
+ }
+
+ // REVIEW: is there a way to batch up these inserts?
+ int rows = db.Execute("INSERT INTO " + UsersInRoleTableName + " VALUES (" + userIds[uId] + "," + roleIds[rId] + "); ");
+ if (rows != 1)
+ {
+ throw new ProviderException(WebDataResources.Security_DbFailure);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Inherited from RoleProvider ==> Forwarded to previous provider if this provider hasn't been initialized
+ public override void CreateRole(string roleName)
+ {
+ if (!InitializeCalled)
+ {
+ PreviousProvider.CreateRole(roleName);
+ }
+ else
+ {
+ using (var db = ConnectToDatabase())
+ {
+ int roleId = FindRoleId(db, roleName);
+ if (roleId != -1)
+ {
+ throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, WebDataResources.SimpleRoleProvider_RoleExists, roleName));
+ }
+
+ int rows = db.Execute("INSERT INTO " + RoleTableName + " (RoleName) VALUES (@0)", roleName);
+ if (rows != 1)
+ {
+ throw new ProviderException(WebDataResources.Security_DbFailure);
+ }
+ }
+ }
+ }
+
+ // Inherited from RoleProvider ==> Forwarded to previous provider if this provider hasn't been initialized
+ public override bool DeleteRole(string roleName, bool throwOnPopulatedRole)
+ {
+ if (!InitializeCalled)
+ {
+ return PreviousProvider.DeleteRole(roleName, throwOnPopulatedRole);
+ }
+ using (var db = ConnectToDatabase())
+ {
+ int roleId = FindRoleId(db, roleName);
+ if (roleId == -1)
+ {
+ return false;
+ }
+
+ if (throwOnPopulatedRole)
+ {
+ int usersInRole = db.Query(@"SELECT * FROM " + UsersInRoleTableName + " WHERE (RoleId = @0)", roleId).Count();
+ if (usersInRole > 0)
+ {
+ throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, WebDataResources.SimpleRoleProvder_RolePopulated, roleName));
+ }
+ }
+ else
+ {
+ // Delete any users in this role first
+ db.Execute(@"DELETE FROM " + UsersInRoleTableName + " WHERE (RoleId = @0)", roleId);
+ }
+
+ int rows = db.Execute(@"DELETE FROM " + RoleTableName + " WHERE (RoleId = @0)", roleId);
+ return (rows == 1); // REVIEW: should this ever be > 1?
+ }
+ }
+
+ // Inherited from RoleProvider ==> Forwarded to previous provider if this provider hasn't been initialized
+ public override string[] FindUsersInRole(string roleName, string usernameToMatch)
+ {
+ if (!InitializeCalled)
+ {
+ return PreviousProvider.FindUsersInRole(roleName, usernameToMatch);
+ }
+ using (var db = ConnectToDatabase())
+ {
+ // REVIEW: Is there any way to directly get out a string[]?
+ List<dynamic> userNames = db.Query(@"SELECT u." + SafeUserNameColumn + " FROM " + SafeUserTableName + " u, " + UsersInRoleTableName + " ur, " + RoleTableName + " r Where (r.RoleName = @0 and ur.RoleId = r.RoleId and ur.UserId = u." + SafeUserIdColumn + " and u." + SafeUserNameColumn + " LIKE @1)", new object[] { roleName, usernameToMatch }).ToList();
+ string[] users = new string[userNames.Count];
+ for (int i = 0; i < userNames.Count; i++)
+ {
+ users[i] = (string)userNames[i][0];
+ }
+ return users;
+ }
+ }
+
+ // Inherited from RoleProvider ==> Forwarded to previous provider if this provider hasn't been initialized
+ public override string[] GetAllRoles()
+ {
+ if (!InitializeCalled)
+ {
+ return PreviousProvider.GetAllRoles();
+ }
+ using (var db = ConnectToDatabase())
+ {
+ return db.Query(@"SELECT RoleName FROM " + RoleTableName).Select<dynamic, string>(d => (string)d[0]).ToArray();
+ }
+ }
+
+ // Inherited from RoleProvider ==> Forwarded to previous provider if this provider hasn't been initialized
+ public override string[] GetRolesForUser(string username)
+ {
+ if (!InitializeCalled)
+ {
+ return PreviousProvider.GetRolesForUser(username);
+ }
+ using (var db = ConnectToDatabase())
+ {
+ int userId = SimpleMembershipProvider.GetUserId(db, SafeUserTableName, SafeUserNameColumn, SafeUserIdColumn, username);
+ if (userId == -1)
+ {
+ throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, WebDataResources.Security_NoUserFound, username));
+ }
+
+ string query = @"SELECT r.RoleName FROM " + UsersInRoleTableName + " u, " + RoleTableName + " r Where (u.UserId = @0 and u.RoleId = r.RoleId) GROUP BY RoleName";
+ return db.Query(query, new object[] { userId }).Select<dynamic, string>(d => (string)d[0]).ToArray();
+ }
+ }
+
+ // Inherited from RoleProvider ==> Forwarded to previous provider if this provider hasn't been initialized
+ public override string[] GetUsersInRole(string roleName)
+ {
+ if (!InitializeCalled)
+ {
+ return PreviousProvider.GetUsersInRole(roleName);
+ }
+ using (var db = ConnectToDatabase())
+ {
+ string query = @"SELECT u." + SafeUserNameColumn + " FROM " + SafeUserTableName + " u, " + UsersInRoleTableName + " ur, " + RoleTableName + " r Where (r.RoleName = @0 and ur.RoleId = r.RoleId and ur.UserId = u." + SafeUserIdColumn + ")";
+ return db.Query(query, new object[] { roleName }).Select<dynamic, string>(d => (string)d[0]).ToArray();
+ }
+ }
+
+ // Inherited from RoleProvider ==> Forwarded to previous provider if this provider hasn't been initialized
+ public override bool IsUserInRole(string username, string roleName)
+ {
+ if (!InitializeCalled)
+ {
+ return PreviousProvider.IsUserInRole(username, roleName);
+ }
+ using (var db = ConnectToDatabase())
+ {
+ var count = db.QuerySingle("SELECT COUNT(*) FROM " + SafeUserTableName + " u, " + UsersInRoleTableName + " ur, " + RoleTableName + " r Where (u." + SafeUserNameColumn + " = @0 and r.RoleName = @1 and ur.RoleId = r.RoleId and ur.UserId = u." + SafeUserIdColumn + ")", username, roleName);
+ return (count[0] == 1);
+ }
+ }
+
+ // Inherited from RoleProvider ==> Forwarded to previous provider if this provider hasn't been initialized
+ public override void RemoveUsersFromRoles(string[] usernames, string[] roleNames)
+ {
+ if (!InitializeCalled)
+ {
+ PreviousProvider.RemoveUsersFromRoles(usernames, roleNames);
+ }
+ else
+ {
+ foreach (string rolename in roleNames)
+ {
+ if (!RoleExists(rolename))
+ {
+ throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, WebDataResources.SimpleRoleProvider_NoRoleFound, rolename));
+ }
+ }
+
+ foreach (string username in usernames)
+ {
+ foreach (string rolename in roleNames)
+ {
+ if (!IsUserInRole(username, rolename))
+ {
+ throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, WebDataResources.SimpleRoleProvder_UserNotInRole, username, rolename));
+ }
+ }
+ }
+
+ using (var db = ConnectToDatabase())
+ {
+ List<int> userIds = GetUserIdsFromNames(db, usernames);
+ List<int> roleIds = GetRoleIdsFromNames(db, roleNames);
+
+ foreach (int userId in userIds)
+ {
+ foreach (int roleId in roleIds)
+ {
+ // Review: Is there a way to do these all in one query?
+ int rows = db.Execute("DELETE FROM " + UsersInRoleTableName + " WHERE UserId = " + userId + " and RoleId = " + roleId);
+ if (rows != 1)
+ {
+ throw new ProviderException(WebDataResources.Security_DbFailure);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private static int FindRoleId(IDatabase db, string roleName)
+ {
+ var result = db.QuerySingle(@"SELECT RoleId FROM " + RoleTableName + " WHERE (RoleName = @0)", roleName);
+ if (result == null)
+ {
+ return -1;
+ }
+ return (int)result[0];
+ }
+
+ // Inherited from RoleProvider ==> Forwarded to previous provider if this provider hasn't been initialized
+ public override bool RoleExists(string roleName)
+ {
+ if (!InitializeCalled)
+ {
+ return PreviousProvider.RoleExists(roleName);
+ }
+ using (var db = ConnectToDatabase())
+ {
+ return (FindRoleId(db, roleName) != -1);
+ }
+ }
+ }
+}
diff --git a/src/WebMatrix.WebData/WebMatrix.WebData.csproj b/src/WebMatrix.WebData/WebMatrix.WebData.csproj
new file mode 100644
index 00000000..0f84c1fb
--- /dev/null
+++ b/src/WebMatrix.WebData/WebMatrix.WebData.csproj
@@ -0,0 +1,130 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <CodeAnalysis Condition=" '$(CodeAnalysis)' == '' ">false</CodeAnalysis>
+ <ProductVersion>9.0.30729</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{55A15F40-1435-4248-A7F2-2A146BB83586}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <RootNamespace>WebMatrix.WebData</RootNamespace>
+ <AssemblyName>WebMatrix.WebData</AssemblyName>
+ <FileAlignment>512</FileAlignment>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>..\..\bin\Debug\</OutputPath>
+ <DefineConstants>TRACE;DEBUG;ASPNETWEBPAGES</DefineConstants>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ <DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
+ <NoWarn>1591</NoWarn>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>..\..\bin\Release\</OutputPath>
+ <DefineConstants>TRACE;ASPNETWEBPAGES</DefineConstants>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ <RunCodeAnalysis>$(CodeAnalysis)</RunCodeAnalysis>
+ <DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
+ <NoWarn>1591</NoWarn>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'CodeCoverage|AnyCPU'">
+ <DebugSymbols>true</DebugSymbols>
+ <OutputPath>..\..\bin\CodeCoverage\</OutputPath>
+ <DefineConstants>TRACE;DEBUG;CODE_COVERAGE;ASPNETWEBPAGES</DefineConstants>
+ <DebugType>full</DebugType>
+ <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="System" />
+ <Reference Include="System.configuration" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.Web" />
+ <Reference Include="System.Web.ApplicationServices" />
+ <Reference Include="Microsoft.CSharp" />
+ <Reference Include="System.Data" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="..\CommonAssemblyInfo.cs">
+ <Link>Properties\CommonAssemblyInfo.cs</Link>
+ </Compile>
+ <Compile Include="..\CommonResources.Designer.cs">
+ <Link>Common\CommonResources.Designer.cs</Link>
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>CommonResources.resx</DependentUpon>
+ </Compile>
+ <Compile Include="..\ExceptionHelper.cs">
+ <Link>ExceptionHelper.cs</Link>
+ </Compile>
+ <Compile Include="..\GlobalSuppressions.cs">
+ <Link>Common\GlobalSuppressions.cs</Link>
+ </Compile>
+ <Compile Include="..\TransparentCommonAssemblyInfo.cs">
+ <Link>Properties\TransparentCommonAssemblyInfo.cs</Link>
+ </Compile>
+ <Compile Include="ConfigUtil.cs" />
+ <Compile Include="DatabaseConnectionInfo.cs" />
+ <Compile Include="DatabaseWrapper.cs" />
+ <Compile Include="ExtendedMembershipProvider.cs" />
+ <Compile Include="FormsAuthenticationSettings.cs" />
+ <Compile Include="GlobalSuppressions.cs" />
+ <Compile Include="IDatabase.cs" />
+ <Compile Include="OAuthAccountData.cs" />
+ <Compile Include="PreApplicationStartCode.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="Resources\WebDataResources.Designer.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>WebDataResources.resx</DependentUpon>
+ </Compile>
+ <Compile Include="SimpleMembershipProvider.cs" />
+ <Compile Include="SimpleRoleProvider.cs" />
+ <Compile Include="WebSecurity.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\System.Web.Helpers\System.Web.Helpers.csproj">
+ <Project>{9B7E3740-6161-4548-833C-4BBCA43B970E}</Project>
+ <Name>System.Web.Helpers</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\System.Web.Razor\System.Web.Razor.csproj">
+ <Project>{8F18041B-9410-4C36-A9C5-067813DF5F31}</Project>
+ <Name>System.Web.Razor</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\System.Web.WebPages.Razor\System.Web.WebPages.Razor.csproj">
+ <Project>{0939B11A-FE4E-4BA1-8AD6-D97741EE314F}</Project>
+ <Name>System.Web.WebPages.Razor</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\System.Web.WebPages\System.Web.WebPages.csproj">
+ <Project>{76EFA9C5-8D7E-4FDF-B710-E20F8B6B00D2}</Project>
+ <Name>System.Web.WebPages</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\WebMatrix.Data\WebMatrix.Data.csproj">
+ <Project>{4D39BAAF-8A96-473E-AB79-C8A341885137}</Project>
+ <Name>WebMatrix.Data</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="..\CommonResources.resx">
+ <Link>Common\CommonResources.resx</Link>
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>CommonResources.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Include="Resources\WebDataResources.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>WebDataResources.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ </ItemGroup>
+ <ItemGroup>
+ <CodeAnalysisDictionary Include="..\CodeAnalysisDictionary.xml" />
+ </ItemGroup>
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+</Project> \ No newline at end of file
diff --git a/src/WebMatrix.WebData/WebSecurity.cs b/src/WebMatrix.WebData/WebSecurity.cs
new file mode 100644
index 00000000..ca93ffc0
--- /dev/null
+++ b/src/WebMatrix.WebData/WebSecurity.cs
@@ -0,0 +1,423 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Net;
+using System.Web;
+using System.Web.Routing;
+using System.Web.Security;
+using System.Web.WebPages;
+using WebMatrix.WebData.Resources;
+
+namespace WebMatrix.WebData
+{
+ public static class WebSecurity
+ {
+ public static readonly string EnableSimpleMembershipKey = "enableSimpleMembership";
+
+ /// <summary>
+ /// Gets a value indicating whether the <see cref="M:InitializeDatabaseConnection"/> method has been initialized.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if initialized; otherwise, <c>false</c>.
+ /// </value>
+ public static bool Initialized { get; private set; }
+
+ public static int CurrentUserId
+ {
+ get { return GetUserId(CurrentUserName); }
+ }
+
+ public static string CurrentUserName
+ {
+ get { return Context.User.Identity.Name; }
+ }
+
+ public static bool HasUserId
+ {
+ get { return CurrentUserId != -1; }
+ }
+
+ public static bool IsAuthenticated
+ {
+ get { return Request.IsAuthenticated; }
+ }
+
+ internal static HttpContextBase Context
+ {
+ get { return new HttpContextWrapper(HttpContext.Current); }
+ }
+
+ internal static HttpRequestBase Request
+ {
+ get { return Context.Request; }
+ }
+
+ internal static HttpResponseBase Response
+ {
+ get { return Context.Response; }
+ }
+
+ internal static void PreAppStartInit()
+ {
+ // Allow use of <add key="EnableSimpleMembershipKey" value="false" /> to disable registration of membership/role providers as default.
+ if (ConfigUtil.SimpleMembershipEnabled)
+ {
+ // called during PreAppStart, should also hook up the config for MembershipProviders?
+ // Replace the AspNetSqlMembershipProvider (which is the default that is registered in root web.config)
+ const string BuiltInMembershipProviderName = "AspNetSqlMembershipProvider";
+ var builtInMembership = Membership.Providers[BuiltInMembershipProviderName];
+ if (builtInMembership != null)
+ {
+ var simpleMembership = CreateDefaultSimpleMembershipProvider(BuiltInMembershipProviderName, currentDefault: builtInMembership);
+ Membership.Providers.Remove(BuiltInMembershipProviderName);
+ Membership.Providers.Add(simpleMembership);
+ }
+
+ Roles.Enabled = true;
+ const string BuiltInRolesProviderName = "AspNetSqlRoleProvider";
+ var builtInRoles = Roles.Providers[BuiltInRolesProviderName];
+ if (builtInRoles != null)
+ {
+ var simpleRoles = CreateDefaultSimpleRoleProvider(BuiltInRolesProviderName, currentDefault: builtInRoles);
+ Roles.Providers.Remove(BuiltInRolesProviderName);
+ Roles.Providers.Add(simpleRoles);
+ }
+ }
+ }
+
+ private static ExtendedMembershipProvider VerifyProvider()
+ {
+ ExtendedMembershipProvider provider = Membership.Provider as ExtendedMembershipProvider;
+ if (provider == null)
+ {
+ throw new InvalidOperationException(WebDataResources.Security_NoExtendedMembershipProvider);
+ }
+ provider.VerifyInitialized(); // Have the provider verify that it's initialized (only our SimpleMembershipProvider does anything here)
+ return provider;
+ }
+
+ public static void InitializeDatabaseConnection(string connectionStringName, string userTableName, string userIdColumn, string userNameColumn, bool autoCreateTables)
+ {
+ DatabaseConnectionInfo connect = new DatabaseConnectionInfo();
+ connect.ConnectionStringName = connectionStringName;
+ InitializeProviders(connect, userTableName, userIdColumn, userNameColumn, autoCreateTables);
+ }
+
+ public static void InitializeDatabaseConnection(string connectionString, string providerName, string userTableName, string userIdColumn, string userNameColumn, bool autoCreateTables)
+ {
+ DatabaseConnectionInfo connect = new DatabaseConnectionInfo();
+ connect.ConnectionString = connectionString;
+ connect.ProviderName = providerName;
+ InitializeProviders(connect, userTableName, userIdColumn, userNameColumn, autoCreateTables);
+ }
+
+ private static void InitializeProviders(DatabaseConnectionInfo connect, string userTableName, string userIdColumn, string userNameColumn, bool autoCreateTables)
+ {
+ SimpleMembershipProvider simpleMembership = Membership.Provider as SimpleMembershipProvider;
+ if (simpleMembership != null)
+ {
+ InitializeMembershipProvider(simpleMembership, connect, userTableName, userIdColumn, userNameColumn, autoCreateTables);
+ }
+
+ SimpleRoleProvider simpleRoles = Roles.Provider as SimpleRoleProvider;
+ if (simpleRoles != null)
+ {
+ InitializeRoleProvider(simpleRoles, connect, userTableName, userIdColumn, userNameColumn, autoCreateTables);
+ }
+
+ Initialized = true;
+ }
+
+ internal static void InitializeMembershipProvider(SimpleMembershipProvider simpleMembership, DatabaseConnectionInfo connect, string userTableName, string userIdColumn, string userNameColumn, bool createTables)
+ {
+ if (simpleMembership.InitializeCalled)
+ {
+ throw new InvalidOperationException(WebDataResources.Security_InitializeAlreadyCalled);
+ }
+ simpleMembership.ConnectionInfo = connect;
+ simpleMembership.UserIdColumn = userIdColumn;
+ simpleMembership.UserNameColumn = userNameColumn;
+ simpleMembership.UserTableName = userTableName;
+ if (createTables)
+ {
+ simpleMembership.CreateTablesIfNeeded();
+ }
+ else
+ {
+ // We want to validate the user table if we aren't creating them
+ simpleMembership.ValidateUserTable();
+ }
+ simpleMembership.InitializeCalled = true;
+ }
+
+ internal static void InitializeRoleProvider(SimpleRoleProvider simpleRoles, DatabaseConnectionInfo connect, string userTableName, string userIdColumn, string userNameColumn, bool createTables)
+ {
+ if (simpleRoles.InitializeCalled)
+ {
+ throw new InvalidOperationException(WebDataResources.Security_InitializeAlreadyCalled);
+ }
+ simpleRoles.ConnectionInfo = connect;
+ simpleRoles.UserTableName = userTableName;
+ simpleRoles.UserIdColumn = userIdColumn;
+ simpleRoles.UserNameColumn = userNameColumn;
+ if (createTables)
+ {
+ simpleRoles.CreateTablesIfNeeded();
+ }
+ simpleRoles.InitializeCalled = true;
+ }
+
+ private static SimpleMembershipProvider CreateDefaultSimpleMembershipProvider(string name, MembershipProvider currentDefault)
+ {
+ var membership = new SimpleMembershipProvider(previousProvider: currentDefault);
+ NameValueCollection config = new NameValueCollection();
+ membership.Initialize(name, config);
+ return membership;
+ }
+
+ private static SimpleRoleProvider CreateDefaultSimpleRoleProvider(string name, RoleProvider currentDefault)
+ {
+ var roleProvider = new SimpleRoleProvider(previousProvider: currentDefault);
+ NameValueCollection config = new NameValueCollection();
+ roleProvider.Initialize(name, config);
+ return roleProvider;
+ }
+
+ [SuppressMessage("Microsoft.Naming", "CA1726:UsePreferredTerms", MessageId = "Login", Justification = "Login is used more consistently in ASP.Net")]
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "This is a helper class, and we are not removing optional parameters from methods in helper classes")]
+ public static bool Login(string userName, string password, bool persistCookie = false)
+ {
+ VerifyProvider();
+ bool success = Membership.ValidateUser(userName, password);
+ if (success)
+ {
+ FormsAuthentication.SetAuthCookie(userName, persistCookie);
+ }
+ return success;
+ }
+
+ [SuppressMessage("Microsoft.Naming", "CA1726:UsePreferredTerms", MessageId = "Logout", Justification = "Login is used more consistently in ASP.Net")]
+ public static void Logout()
+ {
+ VerifyProvider();
+ FormsAuthentication.SignOut();
+ }
+
+ public static bool ChangePassword(string userName, string currentPassword, string newPassword)
+ {
+ VerifyProvider();
+ bool success = false;
+ try
+ {
+ var currentUser = Membership.GetUser(userName, true /* userIsOnline */);
+ success = currentUser.ChangePassword(currentPassword, newPassword);
+ }
+ catch (ArgumentException)
+ {
+ // An argument exception is thrown if the new password does not meet the provider's requirements
+ }
+
+ return success;
+ }
+
+ public static bool ConfirmAccount(string accountConfirmationToken)
+ {
+ ExtendedMembershipProvider provider = VerifyProvider();
+ Debug.Assert(provider != null); // VerifyProvider checks this
+ return provider.ConfirmAccount(accountConfirmationToken);
+ }
+
+ public static bool ConfirmAccount(string userName, string accountConfirmationToken)
+ {
+ ExtendedMembershipProvider provider = VerifyProvider();
+ Debug.Assert(provider != null); // VerifyProvider checks this
+ return provider.ConfirmAccount(userName, accountConfirmationToken);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "This is a helper class, and we are not removing optional parameters from methods in helper classes")]
+ public static string CreateAccount(string userName, string password, bool requireConfirmationToken = false)
+ {
+ ExtendedMembershipProvider provider = VerifyProvider();
+ Debug.Assert(provider != null); // VerifyProvider checks this
+
+ return provider.CreateAccount(userName, password, requireConfirmationToken);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "This is a helper class, and we are not removing optional parameters from methods in helper classes")]
+ public static string CreateUserAndAccount(string userName, string password, object propertyValues = null, bool requireConfirmationToken = false)
+ {
+ ExtendedMembershipProvider provider = VerifyProvider();
+ Debug.Assert(provider != null); // VerifyProvider checks this
+
+ IDictionary<string, object> values = null;
+ if (propertyValues != null)
+ {
+ values = new RouteValueDictionary(propertyValues);
+ }
+
+ return provider.CreateUserAndAccount(userName, password, requireConfirmationToken, values);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "This is a helper class, and we are not removing optional parameters from methods in helper classes")]
+ public static string GeneratePasswordResetToken(string userName, int tokenExpirationInMinutesFromNow = 1440)
+ {
+ ExtendedMembershipProvider provider = VerifyProvider();
+ Debug.Assert(provider != null); // VerifyProvider checks this
+
+ return provider.GeneratePasswordResetToken(userName, tokenExpirationInMinutesFromNow);
+ }
+
+ public static bool UserExists(string userName)
+ {
+ VerifyProvider();
+ return Membership.GetUser(userName) != null;
+ }
+
+ public static int GetUserId(string userName)
+ {
+ VerifyProvider();
+ MembershipUser user = Membership.GetUser(userName);
+ if (user == null)
+ {
+ return -1;
+ }
+
+ // REVIEW: This cast is breaking the abstraction for the membershipprovider, we basically assume that userids are ints
+ return (int)user.ProviderUserKey;
+ }
+
+ public static int GetUserIdFromPasswordResetToken(string token)
+ {
+ ExtendedMembershipProvider provider = VerifyProvider();
+ Debug.Assert(provider != null); // VerifyProvider checks this
+
+ return provider.GetUserIdFromPasswordResetToken(token);
+ }
+
+ public static bool IsCurrentUser(string userName)
+ {
+ VerifyProvider();
+ return String.Equals(CurrentUserName, userName, StringComparison.OrdinalIgnoreCase);
+ }
+
+ public static bool IsConfirmed(string userName)
+ {
+ ExtendedMembershipProvider provider = VerifyProvider();
+ Debug.Assert(provider != null); // VerifyProvider checks this
+
+ return provider.IsConfirmed(userName);
+ }
+
+ // Make sure the logged on user is same as the one specified by the id
+ private static bool IsUserLoggedOn(int userId)
+ {
+ VerifyProvider();
+ return CurrentUserId == userId;
+ }
+
+ // Make sure the user was authenticated
+ public static void RequireAuthenticatedUser()
+ {
+ VerifyProvider();
+ var user = Context.User;
+ if (user == null || !user.Identity.IsAuthenticated)
+ {
+ Response.SetStatus(HttpStatusCode.Unauthorized);
+ }
+ }
+
+ // Make sure the user was authenticated
+ public static void RequireUser(int userId)
+ {
+ VerifyProvider();
+ if (!IsUserLoggedOn(userId))
+ {
+ Response.SetStatus(HttpStatusCode.Unauthorized);
+ }
+ }
+
+ public static void RequireUser(string userName)
+ {
+ VerifyProvider();
+ if (!String.Equals(CurrentUserName, userName, StringComparison.OrdinalIgnoreCase))
+ {
+ Response.SetStatus(HttpStatusCode.Unauthorized);
+ }
+ }
+
+ public static void RequireRoles(params string[] roles)
+ {
+ VerifyProvider();
+ foreach (string role in roles)
+ {
+ if (!Roles.IsUserInRole(CurrentUserName, role))
+ {
+ Response.SetStatus(HttpStatusCode.Unauthorized);
+ return;
+ }
+ }
+ }
+
+ public static bool ResetPassword(string passwordResetToken, string newPassword)
+ {
+ ExtendedMembershipProvider provider = VerifyProvider();
+ Debug.Assert(provider != null); // VerifyProvider checks this
+ return provider.ResetPasswordWithToken(passwordResetToken, newPassword);
+ }
+
+ public static bool IsAccountLockedOut(string userName, int allowedPasswordAttempts, int intervalInSeconds)
+ {
+ VerifyProvider();
+ return IsAccountLockedOut(userName, allowedPasswordAttempts, TimeSpan.FromSeconds(intervalInSeconds));
+ }
+
+ public static bool IsAccountLockedOut(string userName, int allowedPasswordAttempts, TimeSpan interval)
+ {
+ ExtendedMembershipProvider provider = VerifyProvider();
+ Debug.Assert(provider != null); // VerifyProvider checks this
+
+ return IsAccountLockedOutInternal(provider, userName, allowedPasswordAttempts, interval);
+ }
+
+ internal static bool IsAccountLockedOutInternal(ExtendedMembershipProvider provider, string userName, int allowedPasswordAttempts, TimeSpan interval)
+ {
+ return (provider.GetUser(userName, false) != null &&
+ provider.GetPasswordFailuresSinceLastSuccess(userName) > allowedPasswordAttempts &&
+ provider.GetLastPasswordFailureDate(userName).Add(interval) > DateTime.UtcNow);
+ }
+
+ public static int GetPasswordFailuresSinceLastSuccess(string userName)
+ {
+ ExtendedMembershipProvider provider = VerifyProvider();
+ Debug.Assert(provider != null); // VerifyProvider checks this
+
+ return provider.GetPasswordFailuresSinceLastSuccess(userName);
+ }
+
+ public static DateTime GetCreateDate(string userName)
+ {
+ ExtendedMembershipProvider provider = VerifyProvider();
+ Debug.Assert(provider != null); // VerifyProvider checks this
+
+ return provider.GetCreateDate(userName);
+ }
+
+ public static DateTime GetPasswordChangedDate(string userName)
+ {
+ ExtendedMembershipProvider provider = VerifyProvider();
+ Debug.Assert(provider != null); // VerifyProvider checks this
+
+ return provider.GetPasswordChangedDate(userName);
+ }
+
+ public static DateTime GetLastPasswordFailureDate(string userName)
+ {
+ ExtendedMembershipProvider provider = VerifyProvider();
+ Debug.Assert(provider != null); // VerifyProvider checks this
+
+ return provider.GetLastPasswordFailureDate(userName);
+ }
+ }
+}
diff --git a/test/Microsoft.TestCommon/AppDomainUtils.cs b/test/Microsoft.TestCommon/AppDomainUtils.cs
new file mode 100644
index 00000000..addbcee1
--- /dev/null
+++ b/test/Microsoft.TestCommon/AppDomainUtils.cs
@@ -0,0 +1,71 @@
+using System.IO;
+using System.Reflection;
+using System.Web.Compilation;
+using System.Web.Hosting;
+
+namespace System.Web.WebPages.TestUtils
+{
+ public static class AppDomainUtils
+ {
+ // Allow a test to modify static fields in an independent appdomain so that
+ // other tests will not be affected.
+ public static void RunInSeparateAppDomain(Action action)
+ {
+ RunInSeparateAppDomain(new AppDomainSetup(), action);
+ }
+
+ public static void RunInSeparateAppDomain(AppDomainSetup setup, Action action)
+ {
+ var dir = Path.GetDirectoryName(typeof(AppDomainUtils).Assembly.CodeBase).Replace("file:\\", "");
+ setup.PrivateBinPath = dir;
+ setup.ApplicationBase = dir;
+ setup.ApplicationName = Guid.NewGuid().ToString();
+ setup.ShadowCopyFiles = "true";
+ setup.ShadowCopyDirectories = setup.ApplicationBase;
+ setup.CachePath = Path.Combine(Path.GetTempPath(), setup.ApplicationName);
+
+ AppDomain appDomain = null;
+ try
+ {
+ appDomain = AppDomain.CreateDomain(setup.ApplicationName, null, setup);
+ AppDomainHelper helper = appDomain.CreateInstanceAndUnwrap(typeof(AppDomainUtils).Assembly.FullName, typeof(AppDomainHelper).FullName) as AppDomainHelper;
+ helper.Run(action);
+ }
+ finally
+ {
+ if (appDomain != null)
+ {
+ AppDomain.Unload(appDomain);
+ }
+ }
+ }
+
+ public class AppDomainHelper : MarshalByRefObject
+ {
+ public void Run(Action action)
+ {
+ action();
+ }
+ }
+
+ public static void SetPreAppStartStage()
+ {
+ var stage = typeof(BuildManager).GetProperty("PreStartInitStage", BindingFlags.Static | BindingFlags.NonPublic);
+ var value = ((FieldInfo)typeof(BuildManager).Assembly.GetType("System.Web.Compilation.PreStartInitStage").GetMember("DuringPreStartInit")[0]).GetValue(null);
+ stage.SetValue(null, value, new object[] { });
+ SetAppData();
+ var env = new HostingEnvironment();
+ }
+
+ public static void SetAppData()
+ {
+ var appdomain = AppDomain.CurrentDomain;
+ // Set some dummy values to make the appdomain seem more like a ASP.NET hosted one
+ appdomain.SetData(".appDomain", "*");
+ appdomain.SetData(".appId", "appId");
+ appdomain.SetData(".appPath", "appPath");
+ appdomain.SetData(".appVPath", "/WebSite1");
+ appdomain.SetData(".domainId", "1");
+ }
+ }
+}
diff --git a/test/Microsoft.TestCommon/AssertEx.cs b/test/Microsoft.TestCommon/AssertEx.cs
new file mode 100644
index 00000000..6b1a227c
--- /dev/null
+++ b/test/Microsoft.TestCommon/AssertEx.cs
@@ -0,0 +1,31 @@
+using System;
+using Xunit;
+
+namespace Microsoft.TestCommon
+{
+ // This extends xUnit.net's Assert class, and makes it partial so that we can
+ // organize the extension points by logical functionality (rather than dumping them
+ // all into this single file).
+ //
+ // See files named XxxAssertions for root extensions to AssertEx.
+ public partial class AssertEx : Assert
+ {
+ public static readonly ReflectionAssert Reflection = new ReflectionAssert();
+
+ public static readonly TypeAssert Type = new TypeAssert();
+
+ public static readonly HttpAssert Http = new HttpAssert();
+
+ public static readonly MediaTypeAssert MediaType = new MediaTypeAssert();
+
+ public static readonly GenericTypeAssert GenericType = new GenericTypeAssert();
+
+ public static readonly SerializerAssert Serializer = new SerializerAssert();
+
+ public static readonly StreamAssert Stream = new StreamAssert();
+
+ public static readonly TaskAssert Task = new TaskAssert();
+
+ public static readonly XmlAssert Xml = new XmlAssert();
+ }
+}
diff --git a/test/Microsoft.TestCommon/CultureReplacer.cs b/test/Microsoft.TestCommon/CultureReplacer.cs
new file mode 100644
index 00000000..2ef7a1ed
--- /dev/null
+++ b/test/Microsoft.TestCommon/CultureReplacer.cs
@@ -0,0 +1,34 @@
+using System.Globalization;
+using System.Threading;
+using Xunit;
+
+namespace System.Web.TestUtil
+{
+ public class CultureReplacer : IDisposable
+ {
+ private readonly CultureInfo _originalCulture;
+ private readonly long _threadId;
+
+ public CultureReplacer(string culture = "en-us")
+ {
+ _originalCulture = Thread.CurrentThread.CurrentCulture;
+ _threadId = Thread.CurrentThread.ManagedThreadId;
+ Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo(culture);
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ Assert.True(Thread.CurrentThread.ManagedThreadId == _threadId, "The current thread is not the same as the thread invoking the constructor. This should never happen.");
+ Thread.CurrentThread.CurrentCulture = _originalCulture;
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.TestCommon/DefaultTimeoutFactAttribute.cs b/test/Microsoft.TestCommon/DefaultTimeoutFactAttribute.cs
new file mode 100644
index 00000000..d9708552
--- /dev/null
+++ b/test/Microsoft.TestCommon/DefaultTimeoutFactAttribute.cs
@@ -0,0 +1,17 @@
+using System;
+using Xunit;
+
+namespace Microsoft.TestCommon
+{
+ /// <summary>
+ /// An override of <see cref="FactAttribute"/> that provides a default timeout.
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
+ public class DefaultTimeoutFactAttribute : FactAttribute
+ {
+ public DefaultTimeoutFactAttribute()
+ {
+ Timeout = TimeoutConstant.DefaultTimeout;
+ }
+ }
+}
diff --git a/test/Microsoft.TestCommon/DefaultTimeoutTheoryAttribute.cs b/test/Microsoft.TestCommon/DefaultTimeoutTheoryAttribute.cs
new file mode 100644
index 00000000..dbfa12b3
--- /dev/null
+++ b/test/Microsoft.TestCommon/DefaultTimeoutTheoryAttribute.cs
@@ -0,0 +1,14 @@
+using System;
+using Xunit.Extensions;
+
+namespace Microsoft.TestCommon
+{
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
+ public class DefaultTimeoutTheoryAttribute : TheoryAttribute
+ {
+ public DefaultTimeoutTheoryAttribute()
+ {
+ Timeout = TimeoutConstant.DefaultTimeout;
+ }
+ }
+}
diff --git a/test/Microsoft.TestCommon/DictionaryEqualityComparer.cs b/test/Microsoft.TestCommon/DictionaryEqualityComparer.cs
new file mode 100644
index 00000000..34955d58
--- /dev/null
+++ b/test/Microsoft.TestCommon/DictionaryEqualityComparer.cs
@@ -0,0 +1,47 @@
+using System.Collections.Generic;
+
+namespace Microsoft.TestCommon
+{
+ public class DictionaryEqualityComparer : IEqualityComparer<IDictionary<string, object>>
+ {
+ public bool Equals(IDictionary<string, object> x, IDictionary<string, object> y)
+ {
+ if (x.Count != y.Count)
+ {
+ return false;
+ }
+
+ foreach (string key in x.Keys)
+ {
+ object xVal = x[key];
+ object yVal;
+ if (!y.TryGetValue(key, out yVal))
+ {
+ return false;
+ }
+
+ if (xVal == null)
+ {
+ if (yVal == null)
+ {
+ continue;
+ }
+
+ return false;
+ }
+
+ if (!xVal.Equals(yVal))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public int GetHashCode(IDictionary<string, object> obj)
+ {
+ return 1;
+ }
+ }
+}
diff --git a/test/Microsoft.TestCommon/ExceptionAssertions.cs b/test/Microsoft.TestCommon/ExceptionAssertions.cs
new file mode 100644
index 00000000..b16e84d6
--- /dev/null
+++ b/test/Microsoft.TestCommon/ExceptionAssertions.cs
@@ -0,0 +1,515 @@
+using System;
+using System.ComponentModel;
+using System.Globalization;
+using System.Reflection;
+using System.Web;
+
+namespace Microsoft.TestCommon
+{
+ public partial class AssertEx
+ {
+ /// <summary>
+ /// Determines if your thread's current culture and current UI culture is English.
+ /// </summary>
+ public static bool CurrentCultureIsEnglish
+ {
+ get
+ {
+ return String.Equals(CultureInfo.CurrentCulture.TwoLetterISOLanguageName, "en", StringComparison.OrdinalIgnoreCase)
+ && String.Equals(CultureInfo.CurrentUICulture.TwoLetterISOLanguageName, "en", StringComparison.OrdinalIgnoreCase);
+ }
+ }
+
+ /// <summary>
+ /// Determines whether the specified exception is of the given type (or optionally of a derived type).
+ /// The exception is not allowed to be null;
+ /// </summary>
+ /// <param name="exceptionType">The type of the exception to test for.</param>
+ /// <param name="exception">The exception to be tested.</param>
+ /// <param name="expectedMessage">The expected exception message (only verified on US English OSes).</param>
+ /// <param name="allowDerivedExceptions">Pass true to allow exceptions which derive from TException; pass false, otherwise</param>
+ public static void IsException(Type exceptionType, Exception exception, string expectedMessage = null, bool allowDerivedExceptions = false)
+ {
+ exception = UnwrapException(exception);
+ NotNull(exception);
+
+ if (allowDerivedExceptions)
+ IsAssignableFrom(exceptionType, exception);
+ else
+ IsType(exceptionType, exception);
+
+ VerifyExceptionMessage(exception, expectedMessage, partialMatch: false);
+ }
+
+ /// <summary>
+ /// Determines whether the specified exception is of the given type (or optionally of a derived type).
+ /// The exception is not allowed to be null;
+ /// </summary>
+ /// <typeparam name="TException">The type of the exception to test for.</typeparam>
+ /// <param name="exception">The exception to be tested.</param>
+ /// <param name="expectedMessage">The expected exception message (only verified on US English OSes).</param>
+ /// <param name="allowDerivedExceptions">Pass true to allow exceptions which derive from TException; pass false, otherwise</param>
+ /// <returns>The exception cast to TException.</returns>
+ public static TException IsException<TException>(Exception exception, string expectedMessage = null, bool allowDerivedExceptions = false)
+ where TException : Exception
+ {
+ TException result;
+
+ exception = UnwrapException(exception);
+ NotNull(exception);
+
+ if (allowDerivedExceptions)
+ result = IsAssignableFrom<TException>(exception);
+ else
+ result = IsType<TException>(exception);
+
+ VerifyExceptionMessage(exception, expectedMessage, partialMatch: false);
+ return result;
+ }
+
+ // We've re-implemented all the xUnit.net Throws code so that we can get this
+ // updated implementation of RecordException which silently unwraps any instances
+ // of AggregateException. This lets our tests better simulate what "await" would do
+ // and thus makes them easier to port to .NET 4.5.
+ private static Exception RecordException(Action testCode)
+ {
+ try
+ {
+ testCode();
+ return null;
+ }
+ catch (Exception exception)
+ {
+ return UnwrapException(exception);
+ }
+ }
+
+ /// <summary>
+ /// Verifies that the exact exception is thrown (and not a derived exception type).
+ /// </summary>
+ /// <typeparam name="T">The type of the exception expected to be thrown</typeparam>
+ /// <param name="testCode">A delegate to the code to be tested</param>
+ /// <returns>The exception that was thrown, when successful</returns>
+ /// <exception cref="ThrowsException">Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown</exception>
+ public static T Throws<T>(Action testCode)
+ where T : Exception
+ {
+ return (T)Throws(typeof(T), testCode);
+ }
+
+ /// <summary>
+ /// Verifies that the exact exception is thrown (and not a derived exception type).
+ /// Generally used to test property accessors.
+ /// </summary>
+ /// <typeparam name="T">The type of the exception expected to be thrown</typeparam>
+ /// <param name="testCode">A delegate to the code to be tested</param>
+ /// <returns>The exception that was thrown, when successful</returns>
+ /// <exception cref="ThrowsException">Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown</exception>
+ public static T Throws<T>(Func<object> testCode)
+ where T : Exception
+ {
+ return (T)Throws(typeof(T), testCode);
+ }
+
+ /// <summary>
+ /// Verifies that the exact exception is thrown (and not a derived exception type).
+ /// </summary>
+ /// <param name="exceptionType">The type of the exception expected to be thrown</param>
+ /// <param name="testCode">A delegate to the code to be tested</param>
+ /// <returns>The exception that was thrown, when successful</returns>
+ /// <exception cref="ThrowsException">Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown</exception>
+ public static Exception Throws(Type exceptionType, Action testCode)
+ {
+ Exception exception = RecordException(testCode);
+
+ if (exception == null)
+ throw new ThrowsException(exceptionType);
+
+ if (!exceptionType.Equals(exception.GetType()))
+ throw new ThrowsException(exceptionType, exception);
+
+ return exception;
+ }
+
+ /// <summary>
+ /// Verifies that the exact exception is thrown (and not a derived exception type).
+ /// Generally used to test property accessors.
+ /// </summary>
+ /// <param name="exceptionType">The type of the exception expected to be thrown</param>
+ /// <param name="testCode">A delegate to the code to be tested</param>
+ /// <returns>The exception that was thrown, when successful</returns>
+ /// <exception cref="ThrowsException">Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown</exception>
+ public static Exception Throws(Type exceptionType, Func<object> testCode)
+ {
+ return Throws(exceptionType, () => { object unused = testCode(); });
+ }
+
+ /// <summary>
+ /// Verifies that an exception of the given type (or optionally a derived type) is thrown.
+ /// </summary>
+ /// <typeparam name="TException">The type of the exception expected to be thrown</typeparam>
+ /// <param name="testCode">A delegate to the code to be tested</param>
+ /// <param name="allowDerivedExceptions">Pass true to allow exceptions which derive from TException; pass false, otherwise</param>
+ /// <returns>The exception that was thrown, when successful</returns>
+ /// <exception cref="ThrowsException">Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown</exception>
+ public static TException Throws<TException>(Action testCode, bool allowDerivedExceptions)
+ where TException : Exception
+ {
+ Type exceptionType = typeof(TException);
+ Exception exception = RecordException(testCode);
+
+ TargetInvocationException tie = exception as TargetInvocationException;
+ if (tie != null)
+ {
+ exception = tie.InnerException;
+ }
+
+ if (exception == null)
+ {
+ throw new ThrowsException(exceptionType);
+ }
+
+ var typedException = exception as TException;
+ if (typedException == null || (!allowDerivedExceptions && typedException.GetType() != typeof(TException)))
+ {
+ throw new ThrowsException(exceptionType, exception);
+ }
+
+ return typedException;
+ }
+
+ /// <summary>
+ /// Verifies that an exception of the given type (or optionally a derived type) is thrown.
+ /// Generally used to test property accessors.
+ /// </summary>
+ /// <typeparam name="TException">The type of the exception expected to be thrown</typeparam>
+ /// <param name="testCode">A delegate to the code to be tested</param>
+ /// <param name="allowDerivedExceptions">Pass true to allow exceptions which derive from TException; pass false, otherwise</param>
+ /// <returns>The exception that was thrown, when successful</returns>
+ /// <exception cref="ThrowsException">Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown</exception>
+ public static TException Throws<TException>(Func<object> testCode, bool allowDerivedExceptions)
+ where TException : Exception
+ {
+ return Throws<TException>(() => { testCode(); }, allowDerivedExceptions);
+ }
+
+ /// <summary>
+ /// Verifies that an exception of the given type (or optionally a derived type) is thrown.
+ /// Also verified that the exception message matches if the current thread locale is English.
+ /// </summary>
+ /// <typeparam name="TException">The type of the exception expected to be thrown</typeparam>
+ /// <param name="testCode">A delegate to the code to be tested</param>
+ /// <param name="exceptionMessage">The exception message to verify</param>
+ /// <param name="allowDerivedExceptions">Pass true to allow exceptions which derive from TException; pass false, otherwise</param>
+ /// <returns>The exception that was thrown, when successful</returns>
+ /// <exception cref="ThrowsException">Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown</exception>
+ public static TException Throws<TException>(Action testCode, string exceptionMessage, bool allowDerivedExceptions = false)
+ where TException : Exception
+ {
+ var ex = Throws<TException>(testCode, allowDerivedExceptions);
+ VerifyExceptionMessage(ex, exceptionMessage);
+ return ex;
+ }
+
+ /// <summary>
+ /// Verifies that an exception of the given type (or optionally a derived type) is thrown.
+ /// Also verified that the exception message matches if the current thread locale is English.
+ /// </summary>
+ /// <typeparam name="TException">The type of the exception expected to be thrown</typeparam>
+ /// <param name="testCode">A delegate to the code to be tested</param>
+ /// <param name="exceptionMessage">The exception message to verify</param>
+ /// <param name="allowDerivedExceptions">Pass true to allow exceptions which derive from TException; pass false, otherwise</param>
+ /// <returns>The exception that was thrown, when successful</returns>
+ /// <exception cref="ThrowsException">Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown</exception>
+ public static TException Throws<TException>(Func<object> testCode, string exceptionMessage, bool allowDerivedExceptions = false)
+ where TException : Exception
+ {
+ return Throws<TException>(() => { testCode(); }, exceptionMessage, allowDerivedExceptions);
+ }
+
+ /// <summary>
+ /// Verifies that the code throws an <see cref="ArgumentException"/> (or optionally any exception which derives from it).
+ /// </summary>
+ /// <param name="testCode">A delegate to the code to be tested</param>
+ /// <param name="paramName">The name of the parameter that should throw the exception</param>
+ /// <param name="allowDerivedExceptions">Pass true to allow exceptions which derive from TException; pass false, otherwise</param>
+ /// <returns>The exception that was thrown, when successful</returns>
+ /// <exception cref="ThrowsException">Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown</exception>
+ public static ArgumentException ThrowsArgument(Action testCode, string paramName, bool allowDerivedExceptions = false)
+ {
+ var ex = Throws<ArgumentException>(testCode, allowDerivedExceptions);
+
+ if (paramName != null)
+ {
+ Equal(paramName, ex.ParamName);
+ }
+
+ return ex;
+ }
+
+ /// <summary>
+ /// Verifies that the code throws an <see cref="ArgumentException"/> (or optionally any exception which derives from it).
+ /// </summary>
+ /// <param name="testCode">A delegate to the code to be tested</param>
+ /// <param name="paramName">The name of the parameter that should throw the exception</param>
+ /// <param name="exceptionMessage">The exception message to verify</param>
+ /// <param name="allowDerivedExceptions">Pass true to allow exceptions which derive from TException; pass false, otherwise</param>
+ /// <returns>The exception that was thrown, when successful</returns>
+ /// <exception cref="ThrowsException">Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown</exception>
+ public static ArgumentException ThrowsArgument(Action testCode, string paramName, string exceptionMessage, bool allowDerivedExceptions = false)
+ {
+ var ex = Throws<ArgumentException>(testCode, allowDerivedExceptions);
+
+ if (paramName != null)
+ {
+ Equal(paramName, ex.ParamName);
+ }
+
+ VerifyExceptionMessage(ex, exceptionMessage, partialMatch: true);
+
+ return ex;
+ }
+
+ /// <summary>
+ /// Verifies that the code throws an ArgumentException (or optionally any exception which derives from it).
+ /// </summary>
+ /// <param name="testCode">A delegate to the code to be tested</param>
+ /// <param name="paramName">The name of the parameter that should throw the exception</param>
+ /// <param name="allowDerivedExceptions">Pass true to allow exceptions which derive from TException; pass false, otherwise</param>
+ /// <returns>The exception that was thrown, when successful</returns>
+ /// <exception cref="ThrowsException">Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown</exception>
+ public static ArgumentException ThrowsArgument(Func<object> testCode, string paramName, bool allowDerivedExceptions = false)
+ {
+ var ex = Throws<ArgumentException>(testCode, allowDerivedExceptions);
+
+ if (paramName != null)
+ {
+ Equal(paramName, ex.ParamName);
+ }
+
+ return ex;
+ }
+
+ /// <summary>
+ /// Verifies that the code throws an ArgumentNullException (or optionally any exception which derives from it).
+ /// </summary>
+ /// <param name="testCode">A delegate to the code to be tested</param>
+ /// <param name="paramName">The name of the parameter that should throw the exception</param>
+ /// <returns>The exception that was thrown, when successful</returns>
+ /// <exception cref="ThrowsException">Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown</exception>
+ public static ArgumentNullException ThrowsArgumentNull(Action testCode, string paramName)
+ {
+ var ex = Throws<ArgumentNullException>(testCode, allowDerivedExceptions: false);
+
+ if (paramName != null)
+ {
+ Equal(paramName, ex.ParamName);
+ }
+
+ return ex;
+ }
+
+ /// <summary>
+ /// Verifies that the code throws an ArgumentNullException with the expected message that indicates that the value cannot
+ /// be null or empty.
+ /// </summary>
+ /// <param name="testCode">A delegate to the code to be tested</param>
+ /// <param name="paramName">The name of the parameter that should throw the exception</param>
+ /// <returns>The exception that was thrown, when successful</returns>
+ /// <exception cref="ThrowsException">Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown</exception>
+ public static ArgumentException ThrowsArgumentNullOrEmpty(Action testCode, string paramName)
+ {
+ return Throws<ArgumentException>(testCode, "Value cannot be null or empty.\r\nParameter name: " + paramName, allowDerivedExceptions: false);
+ }
+
+ /// <summary>
+ /// Verifies that the code throws an ArgumentNullException with the expected message that indicates that the value cannot
+ /// be null or empty string.
+ /// </summary>
+ /// <param name="testCode">A delegate to the code to be tested</param>
+ /// <param name="paramName">The name of the parameter that should throw the exception</param>
+ /// <returns>The exception that was thrown, when successful</returns>
+ /// <exception cref="ThrowsException">Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown</exception>
+ public static ArgumentException ThrowsArgumentNullOrEmptyString(Action testCode, string paramName)
+ {
+ return ThrowsArgument(testCode, paramName, "Value cannot be null or an empty string.", allowDerivedExceptions: true);
+ }
+
+ /// <summary>
+ /// Verifies that the code throws an ArgumentOutOfRangeException (or optionally any exception which derives from it).
+ /// </summary>
+ /// <param name="testCode">A delegate to the code to be tested</param>
+ /// <param name="paramName">The name of the parameter that should throw the exception</param>
+ /// <param name="exceptionMessage">The exception message to verify</param>
+ /// <param name="allowDerivedExceptions">Pass true to allow exceptions which derive from TException; pass false, otherwise</param>
+ /// <returns>The exception that was thrown, when successful</returns>
+ /// <exception cref="ThrowsException">Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown</exception>
+ public static ArgumentOutOfRangeException ThrowsArgumentOutOfRange(Action testCode, string paramName, string exceptionMessage, bool allowDerivedExceptions = false)
+ {
+ exceptionMessage = exceptionMessage != null
+ ? exceptionMessage + "\r\nParameter name: " + paramName
+ : exceptionMessage;
+ var ex = Throws<ArgumentOutOfRangeException>(testCode, exceptionMessage, allowDerivedExceptions);
+
+ if (paramName != null)
+ {
+ Equal(paramName, ex.ParamName);
+ }
+
+ return ex;
+ }
+
+ /// <summary>
+ /// Verifies that the code throws an <see cref="ArgumentOutOfRangeException"/> with the expected message that indicates that
+ /// the value must be greater than the given value.
+ /// </summary>
+ /// <param name="testCode">A delegate to the code to be tested</param>
+ /// <param name="paramName">The name of the parameter that should throw the exception</param>
+ /// <param name="value">The expected limit value.</param>
+ /// <returns>The exception that was thrown, when successful</returns>
+ /// <exception cref="ThrowsException">Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown</exception>
+ public static ArgumentOutOfRangeException ThrowsArgumentGreaterThan(Action testCode, string paramName, string value)
+ {
+ return ThrowsArgumentOutOfRange(testCode, paramName, String.Format("Value must be greater than {0}.", value));
+ }
+
+ /// <summary>
+ /// Verifies that the code throws an <see cref="ArgumentOutOfRangeException"/> with the expected message that indicates that
+ /// the value must be greater than or equal to the given value.
+ /// </summary>
+ /// <param name="testCode">A delegate to the code to be tested</param>
+ /// <param name="paramName">The name of the parameter that should throw the exception</param>
+ /// <param name="value">The expected limit value.</param>
+ /// <returns>The exception that was thrown, when successful</returns>
+ /// <exception cref="ThrowsException">Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown</exception>
+ public static ArgumentOutOfRangeException ThrowsArgumentGreaterThanOrEqualTo(Action testCode, string paramName, string value)
+ {
+ return ThrowsArgumentOutOfRange(testCode, paramName, String.Format("Value must be greater than or equal to {0}.", value));
+ }
+
+ /// <summary>
+ /// Verifies that the code throws an <see cref="ArgumentOutOfRangeException"/> with the expected message that indicates that
+ /// the value must be less than the given value.
+ /// </summary>
+ /// <param name="testCode">A delegate to the code to be tested</param>
+ /// <param name="paramName">The name of the parameter that should throw the exception</param>
+ /// <param name="value">The expected limit value.</param>
+ /// <returns>The exception that was thrown, when successful</returns>
+ /// <exception cref="ThrowsException">Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown</exception>
+ public static ArgumentOutOfRangeException ThrowsArgumentLessThan(Action testCode, string paramName, string value)
+ {
+ return ThrowsArgumentOutOfRange(testCode, paramName, String.Format("Value must be less than {0}.", value));
+ }
+
+ /// <summary>
+ /// Verifies that the code throws an <see cref="ArgumentOutOfRangeException"/> with the expected message that indicates that
+ /// the value must be less than or equal to the given value.
+ /// </summary>
+ /// <param name="testCode">A delegate to the code to be tested</param>
+ /// <param name="paramName">The name of the parameter that should throw the exception</param>
+ /// <param name="value">The expected limit value.</param>
+ /// <returns>The exception that was thrown, when successful</returns>
+ /// <exception cref="ThrowsException">Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown</exception>
+ public static ArgumentOutOfRangeException ThrowsArgumentLessThanOrEqualTo(Action testCode, string paramName, string value)
+ {
+ return ThrowsArgumentOutOfRange(testCode, paramName, String.Format("Value must be less than or equal to {0}.", value));
+ }
+
+ /// <summary>
+ /// Verifies that the code throws an HttpException (or optionally any exception which derives from it).
+ /// </summary>
+ /// <param name="testCode">A delegate to the code to be tested</param>
+ /// <param name="exceptionMessage">The exception message to verify</param>
+ /// <param name="httpCode">The expected HTTP status code of the exception</param>
+ /// <param name="allowDerivedExceptions">Pass true to allow exceptions which derive from TException; pass false, otherwise</param>
+ /// <returns>The exception that was thrown, when successful</returns>
+ /// <exception cref="ThrowsException">Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown</exception>
+ public static HttpException ThrowsHttpException(Action testCode, string exceptionMessage, int httpCode, bool allowDerivedExceptions = false)
+ {
+ var ex = Throws<HttpException>(testCode, exceptionMessage, allowDerivedExceptions);
+ Equal(httpCode, ex.GetHttpCode());
+ return ex;
+ }
+
+ /// <summary>
+ /// Verifies that the code throws an InvalidEnumArgumentException (or optionally any exception which derives from it).
+ /// </summary>
+ /// <param name="testCode">A delegate to the code to be tested</param>
+ /// <param name="paramName">The name of the parameter that should throw the exception</param>
+ /// <param name="invalidValue">The expected invalid value that should appear in the message</param>
+ /// <param name="enumType">The type of the enumeration</param>
+ /// <param name="allowDerivedExceptions">Pass true to allow exceptions which derive from TException; pass false, otherwise</param>
+ /// <returns>The exception that was thrown, when successful</returns>
+ /// <exception cref="ThrowsException">Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown</exception>
+ public static InvalidEnumArgumentException ThrowsInvalidEnumArgument(Action testCode, string paramName, int invalidValue, Type enumType, bool allowDerivedExceptions = false)
+ {
+ return Throws<InvalidEnumArgumentException>(
+ testCode,
+ String.Format("The value of argument '{0}' ({1}) is invalid for Enum type '{2}'.{3}Parameter name: {0}", paramName, invalidValue, enumType.Name, Environment.NewLine),
+ allowDerivedExceptions
+ );
+ }
+
+ /// <summary>
+ /// Verifies that the code throws an HttpException (or optionally any exception which derives from it).
+ /// </summary>
+ /// <param name="testCode">A delegate to the code to be tested</param>
+ /// <param name="objectName">The name of the object that was dispose</param>
+ /// <param name="allowDerivedExceptions">Pass true to allow exceptions which derive from TException; pass false, otherwise</param>
+ /// <returns>The exception that was thrown, when successful</returns>
+ /// <exception cref="ThrowsException">Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown</exception>
+ public static ObjectDisposedException ThrowsObjectDisposed(Action testCode, string objectName, bool allowDerivedExceptions = false)
+ {
+ var ex = Throws<ObjectDisposedException>(testCode, allowDerivedExceptions);
+
+ if (objectName != null)
+ {
+ Equal(objectName, ex.ObjectName);
+ }
+
+ return ex;
+ }
+
+ private static Exception UnwrapException(Exception exception)
+ {
+ AggregateException aggEx;
+ while ((aggEx = exception as AggregateException) != null)
+ exception = aggEx.GetBaseException();
+
+ return exception;
+ }
+
+ private static void VerifyExceptionMessage(Exception exception, string expectedMessage, bool partialMatch = false)
+ {
+ if (expectedMessage != null && CurrentCultureIsEnglish)
+ {
+ if (!partialMatch)
+ {
+ Equal(expectedMessage, exception.Message);
+ }
+ else
+ {
+ Contains(expectedMessage, exception.Message);
+ }
+ }
+ }
+
+ // Custom ThrowsException so we can filter the stack trace.
+ private class ThrowsException : Xunit.Sdk.ThrowsException
+ {
+ public ThrowsException(Type type) : base(type) { }
+
+ public ThrowsException(Type type, Exception ex) : base(type, ex) { }
+
+ protected override bool ExcludeStackFrame(string stackFrame)
+ {
+ if (stackFrame.StartsWith("at Microsoft.TestCommon.AssertEx.", StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+
+ return base.ExcludeStackFrame(stackFrame);
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.TestCommon/MemberHelper.cs b/test/Microsoft.TestCommon/MemberHelper.cs
new file mode 100644
index 00000000..c09c4278
--- /dev/null
+++ b/test/Microsoft.TestCommon/MemberHelper.cs
@@ -0,0 +1,381 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Reflection;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.TestUtil
+{
+ public static class MemberHelper
+ {
+ private static ConstructorInfo GetConstructorInfo(object instance, Type[] parameterTypes)
+ {
+ if (instance == null)
+ {
+ throw new ArgumentNullException("instance");
+ }
+ ConstructorInfo constructorInfo = instance.GetType().GetConstructor(parameterTypes);
+ if (constructorInfo == null)
+ {
+ throw new ArgumentException(String.Format(
+ "A matching constructor on type '{0}' could not be found.",
+ instance.GetType().FullName));
+ }
+ return constructorInfo;
+ }
+
+ private static EventInfo GetEventInfo(object instance, string eventName)
+ {
+ if (instance == null)
+ {
+ throw new ArgumentNullException("instance");
+ }
+ if (String.IsNullOrEmpty(eventName))
+ {
+ throw new ArgumentException("An event must be specified.", "eventName");
+ }
+ EventInfo eventInfo = instance.GetType().GetEvent(eventName);
+ if (eventInfo == null)
+ {
+ throw new ArgumentException(String.Format(
+ "An event named '{0}' on type '{1}' could not be found.",
+ eventName, instance.GetType().FullName));
+ }
+ return eventInfo;
+ }
+
+ private static MethodInfo GetMethodInfo(object instance, string methodName, Type[] types = null, MethodAttributes attrs = MethodAttributes.Public)
+ {
+ if (instance == null)
+ {
+ throw new ArgumentNullException("instance");
+ }
+ if (String.IsNullOrEmpty(methodName))
+ {
+ throw new ArgumentException("A method must be specified.", "methodName");
+ }
+
+ MethodInfo methodInfo;
+ if (types != null)
+ {
+ methodInfo = instance.GetType().GetMethod(methodName,
+ BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
+ null,
+ types,
+ null);
+ }
+ else
+ {
+ methodInfo = instance.GetType().GetMethod(methodName,
+ BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
+ }
+
+ if (methodInfo == null)
+ {
+ throw new ArgumentException(String.Format(
+ "A method named '{0}' on type '{1}' could not be found.",
+ methodName, instance.GetType().FullName));
+ }
+
+ if ((methodInfo.Attributes & attrs) != attrs)
+ {
+ throw new ArgumentException(String.Format(
+ "Method '{0}' on type '{1}' with attributes '{2}' does not match the attributes '{3}'.",
+ methodName, instance.GetType().FullName, methodInfo.Attributes, attrs));
+ }
+
+ return methodInfo;
+ }
+
+ private static PropertyInfo GetPropertyInfo(object instance, string propertyName)
+ {
+ if (instance == null)
+ {
+ throw new ArgumentNullException("instance");
+ }
+ if (String.IsNullOrEmpty(propertyName))
+ {
+ throw new ArgumentException("A property must be specified.", "propertyName");
+ }
+ PropertyInfo propInfo = instance.GetType().GetProperty(propertyName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
+ if (propInfo == null)
+ {
+ throw new ArgumentException(String.Format(
+ "A property named '{0}' on type '{1}' could not be found.",
+ propertyName, instance.GetType().FullName));
+ }
+ return propInfo;
+ }
+
+ private static void TestAttribute<TAttribute>(MemberInfo memberInfo, TAttribute attributeValue)
+ where TAttribute : Attribute
+ {
+ object[] attrs = memberInfo.GetCustomAttributes(typeof(TAttribute), true);
+
+ if (attributeValue == null)
+ {
+ Assert.True(attrs.Length == 0, "Member should not have an attribute of type " + typeof(TAttribute));
+ }
+ else
+ {
+ Assert.True(attrs != null && attrs.Length > 0,
+ "Member does not have an attribute of type " + typeof(TAttribute));
+ Assert.Equal(attributeValue, attrs[0]);
+ }
+ }
+
+ public static void TestBooleanProperty(object instance, string propertyName, bool initialValue, bool testDefaultValue)
+ {
+ // Assert initial value
+ TestGetPropertyValue(instance, propertyName, initialValue);
+
+ if (testDefaultValue)
+ {
+ // Assert DefaultValue attribute matches inital value
+ TestDefaultValue(instance, propertyName);
+ }
+
+ TestPropertyValue(instance, propertyName, true);
+ TestPropertyValue(instance, propertyName, false);
+ }
+
+ public static void TestDefaultValue(object instance, string propertyName)
+ {
+ PropertyInfo propInfo = GetPropertyInfo(instance, propertyName);
+
+ object initialValue = propInfo.GetValue(instance, null);
+ TestAttribute(propInfo, new DefaultValueAttribute(initialValue));
+ }
+
+ public static void TestEvent<TEventArgs>(object instance, string eventName, TEventArgs eventArgs) where TEventArgs : EventArgs
+ {
+ EventInfo eventInfo = GetEventInfo(instance, eventName);
+
+ // Assert category "Action"
+ TestAttribute(eventInfo, new CategoryAttribute("Action"));
+
+ // Call protected method with no event handlers, assert no error
+ MethodInfo methodInfo = GetMethodInfo(instance, "On" + eventName, attrs: MethodAttributes.Family | MethodAttributes.Virtual);
+ methodInfo.Invoke(instance, new object[] { eventArgs });
+
+ // Attach handler, call method, assert fires once
+ List<object> eventHandlerArgs = new List<object>();
+ EventHandler<TEventArgs> handler = new EventHandler<TEventArgs>(delegate(object sender, TEventArgs t)
+ {
+ eventHandlerArgs.Add(sender);
+ eventHandlerArgs.Add(t);
+ });
+ eventInfo.AddEventHandler(instance, handler);
+ methodInfo.Invoke(instance, new object[] { eventArgs });
+ Assert.Equal(new[] { instance, eventArgs }, eventHandlerArgs.ToArray());
+
+ // Detach handler, call method, assert not fired
+ eventHandlerArgs = new List<object>();
+ eventInfo.RemoveEventHandler(instance, handler);
+ methodInfo.Invoke(instance, new object[] { eventArgs });
+ Assert.Empty(eventHandlerArgs);
+ }
+
+ public static void TestGetPropertyValue(object instance, string propertyName, object valueToCheck)
+ {
+ PropertyInfo propInfo = GetPropertyInfo(instance, propertyName);
+ object value = propInfo.GetValue(instance, null);
+ Assert.Equal(valueToCheck, value);
+ }
+
+ public static void TestEnumProperty<TEnumValue>(object instance, string propertyName, TEnumValue initialValue, bool testDefaultValue)
+ {
+ // Assert initial value
+ TestGetPropertyValue(instance, propertyName, initialValue);
+
+ if (testDefaultValue)
+ {
+ // Assert DefaultValue attribute matches inital value
+ TestDefaultValue(instance, propertyName);
+ }
+
+ PropertyInfo propInfo = GetPropertyInfo(instance, propertyName);
+
+ // Values are sorted numerically
+ TEnumValue[] values = (TEnumValue[])Enum.GetValues(propInfo.PropertyType);
+
+ // Assert get/set works for all valid enum values
+ foreach (TEnumValue value in values)
+ {
+ TestPropertyValue(instance, propertyName, value);
+ }
+
+ // Assert ArgumentOutOfRangeException is thrown for value one less than smallest
+ // enum value, and one more than largest enum value
+ var targetException = Assert.Throws<TargetInvocationException>(() => propInfo.SetValue(instance, Convert.ToInt32(values[0]) - 1, null));
+ Assert.IsType<ArgumentOutOfRangeException>(targetException.InnerException);
+
+ targetException = Assert.Throws<TargetInvocationException>(() => propInfo.SetValue(instance, Convert.ToInt32(values[values.Length - 1]) + 1, null));
+ Assert.IsType<ArgumentOutOfRangeException>(targetException.InnerException);
+ }
+
+ public static void TestInt32Property(object instance, string propertyName, int value1, int value2)
+ {
+ TestPropertyValue(instance, propertyName, value1);
+ TestPropertyValue(instance, propertyName, value2);
+ }
+
+ public static void TestPropertyWithDefaultInstance(object instance, string propertyName, object valueToSet)
+ {
+ PropertyInfo propInfo = GetPropertyInfo(instance, propertyName);
+
+ // Set to explicit property
+ propInfo.SetValue(instance, valueToSet, null);
+ object value = propInfo.GetValue(instance, null);
+ Assert.Equal(valueToSet, value);
+
+ // Set to null
+ propInfo.SetValue(instance, null, null);
+ value = propInfo.GetValue(instance, null);
+ Assert.IsAssignableFrom(propInfo.PropertyType, value);
+ }
+
+ public static void TestPropertyWithDefaultInstance(object instance, string propertyName, object valueToSet, object defaultValue)
+ {
+ PropertyInfo propInfo = GetPropertyInfo(instance, propertyName);
+
+ // Set to explicit property
+ propInfo.SetValue(instance, valueToSet, null);
+ object value = propInfo.GetValue(instance, null);
+ Assert.Same(valueToSet, value);
+
+ // Set to null
+ propInfo.SetValue(instance, null, null);
+ value = propInfo.GetValue(instance, null);
+ Assert.Equal(defaultValue, value);
+ }
+
+ public static void TestPropertyValue(object instance, string propertyName, object value)
+ {
+ TestPropertyValue(instance, propertyName, value, value);
+ }
+
+ public static void TestPropertyValue(object instance, string propertyName, object valueToSet, object valueToCheck)
+ {
+ PropertyInfo propInfo = GetPropertyInfo(instance, propertyName);
+ propInfo.SetValue(instance, valueToSet, null);
+ object value = propInfo.GetValue(instance, null);
+ Assert.Equal(valueToCheck, value);
+ }
+
+ public static void TestStringParams(object instance, Type[] constructorParameterTypes, object[] parameters)
+ {
+ ConstructorInfo ctor = GetConstructorInfo(instance, constructorParameterTypes);
+ TestStringParams(ctor, instance, parameters);
+ }
+
+ public static void TestStringParams(object instance, string methodName, object[] parameters)
+ {
+ TestStringParams(instance, methodName, null, parameters);
+ }
+
+ public static void TestStringParams(object instance, string methodName, Type[] types, object[] parameters)
+ {
+ MethodInfo method = GetMethodInfo(instance, methodName, types);
+ TestStringParams(method, instance, parameters);
+ }
+
+ private static void TestStringParams(MethodBase method, object instance, object[] parameters)
+ {
+ ParameterInfo[] parameterInfos = method.GetParameters();
+ foreach (ParameterInfo parameterInfo in parameterInfos)
+ {
+ if (parameterInfo.ParameterType == typeof(String))
+ {
+ object originalParameter = parameters[parameterInfo.Position];
+
+ parameters[parameterInfo.Position] = null;
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate()
+ {
+ try
+ {
+ method.Invoke(instance, parameters);
+ }
+ catch (TargetInvocationException e)
+ {
+ throw e.InnerException;
+ }
+ },
+ parameterInfo.Name);
+
+ parameters[parameterInfo.Position] = String.Empty;
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate()
+ {
+ try
+ {
+ method.Invoke(instance, parameters);
+ }
+ catch (TargetInvocationException e)
+ {
+ throw e.InnerException;
+ }
+ },
+ parameterInfo.Name);
+
+ parameters[parameterInfo.Position] = originalParameter;
+ }
+ }
+ }
+
+ public static void TestStringProperty(object instance, string propertyName, string initialValue,
+ bool testDefaultValueAttribute = false, bool allowNullAndEmpty = true,
+ string nullAndEmptyReturnValue = "")
+ {
+ // Assert initial value
+ TestGetPropertyValue(instance, propertyName, initialValue);
+
+ if (testDefaultValueAttribute)
+ {
+ // Assert DefaultValue attribute matches inital value
+ TestDefaultValue(instance, propertyName);
+ }
+
+ if (allowNullAndEmpty)
+ {
+ // Assert get/set works for null
+ TestPropertyValue(instance, propertyName, null, nullAndEmptyReturnValue);
+
+ // Assert get/set works for String.Empty
+ TestPropertyValue(instance, propertyName, String.Empty, nullAndEmptyReturnValue);
+ }
+ else
+ {
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate()
+ {
+ try
+ {
+ TestPropertyValue(instance, propertyName, null);
+ }
+ catch (TargetInvocationException e)
+ {
+ throw e.InnerException;
+ }
+ },
+ "value");
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate()
+ {
+ try
+ {
+ TestPropertyValue(instance, propertyName, String.Empty);
+ }
+ catch (TargetInvocationException e)
+ {
+ throw e.InnerException;
+ }
+ },
+ "value");
+ }
+
+ // Assert get/set works for arbitrary value
+ TestPropertyValue(instance, propertyName, "TestValue");
+ }
+ }
+}
diff --git a/test/Microsoft.TestCommon/Microsoft.TestCommon.csproj b/test/Microsoft.TestCommon/Microsoft.TestCommon.csproj
new file mode 100644
index 00000000..41cc12c7
--- /dev/null
+++ b/test/Microsoft.TestCommon/Microsoft.TestCommon.csproj
@@ -0,0 +1,116 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProductVersion>9.0.30729</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>Microsoft.TestCommon</RootNamespace>
+ <AssemblyName>Microsoft.TestCommon</AssemblyName>
+ <FileAlignment>512</FileAlignment>
+ <ProjectTypeGuids>{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Debug\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Release\Test\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'CodeCoverage' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\CodeCoverage\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="System" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.Net.Http">
+ <HintPath>..\..\packages\Microsoft.Net.Http.2.0.20302.1\lib\net40\System.Net.Http.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Net.Http.WebRequest">
+ <HintPath>..\..\packages\Microsoft.Net.Http.2.0.20302.1\lib\net40\System.Net.Http.WebRequest.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Runtime.Serialization" />
+ <Reference Include="System.Web" />
+ <Reference Include="System.Xml" />
+ <Reference Include="System.Xml.Linq" />
+ <Reference Include="xunit">
+ <HintPath>..\..\packages\xunit.1.9.0.1566\lib\xunit.dll</HintPath>
+ </Reference>
+ <Reference Include="xunit.extensions">
+ <HintPath>..\..\packages\xunit.extensions.1.9.0.1566\lib\xunit.extensions.dll</HintPath>
+ </Reference>
+ <Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="AppDomainUtils.cs" />
+ <Compile Include="AssertEx.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="CultureReplacer.cs" />
+ <Compile Include="DefaultTimeoutFactAttribute.cs" />
+ <Compile Include="DefaultTimeoutTheoryAttribute.cs" />
+ <Compile Include="DictionaryEqualityComparer.cs" />
+ <Compile Include="ExceptionAssertions.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="MemberHelper.cs" />
+ <Compile Include="Microsoft\TestCommon\DataSets\RefTypeTestData.cs" />
+ <Compile Include="Microsoft\TestCommon\DataSets\TestDataVariations.cs" />
+ <Compile Include="Microsoft\TestCommon\DataSets\ValueTypeTestData.cs" />
+ <Compile Include="Microsoft\TestCommon\GenericTypeAssert.cs" />
+ <Compile Include="Microsoft\TestCommon\HttpAssert.cs" />
+ <Compile Include="Microsoft\TestCommon\MediaTypeAssert.cs" />
+ <Compile Include="Microsoft\TestCommon\MediaTypeHeaderValueComparer.cs" />
+ <Compile Include="Microsoft\TestCommon\ParsedMediaTypeHeaderValue.cs" />
+ <Compile Include="Microsoft\TestCommon\RegexReplacement.cs" />
+ <Compile Include="Microsoft\TestCommon\RuntimeEnvironment.cs" />
+ <Compile Include="Microsoft\TestCommon\SerializerAssert.cs" />
+ <Compile Include="Microsoft\TestCommon\StreamAssert.cs" />
+ <Compile Include="Microsoft\TestCommon\TaskAssert.cs" />
+ <Compile Include="Microsoft\TestCommon\DataSets\TestData.cs" />
+ <Compile Include="Microsoft\TestCommon\TestDataSetAttribute.cs" />
+ <Compile Include="Microsoft\TestCommon\TimeoutConstant.cs" />
+ <Compile Include="Microsoft\TestCommon\TypeAssert.cs" />
+ <Compile Include="Microsoft\TestCommon\Types\FlagsEnum.cs" />
+ <Compile Include="Microsoft\TestCommon\Types\INameAndIdContainer.cs" />
+ <Compile Include="Microsoft\TestCommon\Types\ISerializableType.cs" />
+ <Compile Include="Microsoft\TestCommon\Types\LongEnum.cs" />
+ <Compile Include="Microsoft\TestCommon\Types\SimpleEnum.cs" />
+ <Compile Include="Microsoft\TestCommon\DataSets\CommonUnitTestDataSets.cs" />
+ <Compile Include="Microsoft\TestCommon\XmlAssert.cs" />
+ <Compile Include="PreAppStartTestHelper.cs" />
+ <Compile Include="PreserveSyncContextAttribute.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="ReflectionAssert.cs" />
+ <Compile Include="TaskExtensions.cs" />
+ <Compile Include="TestFile.cs" />
+ <Compile Include="TestHelper.cs" />
+ <Compile Include="TheoryDataSet.cs" />
+ <Compile Include="ThreadPoolSyncContext.cs" />
+ <Compile Include="WebUtils.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="packages.config" />
+ </ItemGroup>
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+</Project> \ No newline at end of file
diff --git a/test/Microsoft.TestCommon/Microsoft/TestCommon/DataSets/CommonUnitTestDataSets.cs b/test/Microsoft.TestCommon/Microsoft/TestCommon/DataSets/CommonUnitTestDataSets.cs
new file mode 100644
index 00000000..e3c19e43
--- /dev/null
+++ b/test/Microsoft.TestCommon/Microsoft/TestCommon/DataSets/CommonUnitTestDataSets.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Collections.ObjectModel;
+using Microsoft.TestCommon.Types;
+
+namespace Microsoft.TestCommon
+{
+ public class CommonUnitTestDataSets
+ {
+ public static ValueTypeTestData<char> Chars { get { return TestData.CharTestData; } }
+ public static ValueTypeTestData<int> Ints { get { return TestData.IntTestData; } }
+ public static ValueTypeTestData<uint> Uints { get { return TestData.UintTestData; } }
+ public static ValueTypeTestData<short> Shorts { get { return TestData.ShortTestData; } }
+ public static ValueTypeTestData<ushort> Ushorts { get { return TestData.UshortTestData; } }
+ public static ValueTypeTestData<long> Longs { get { return TestData.LongTestData; } }
+ public static ValueTypeTestData<ulong> Ulongs { get { return TestData.UlongTestData; } }
+ public static ValueTypeTestData<byte> Bytes { get { return TestData.ByteTestData; } }
+ public static ValueTypeTestData<sbyte> SBytes { get { return TestData.SByteTestData; } }
+ public static ValueTypeTestData<bool> Bools { get { return TestData.BoolTestData; } }
+ public static ValueTypeTestData<double> Doubles { get { return TestData.DoubleTestData; } }
+ public static ValueTypeTestData<float> Floats { get { return TestData.FloatTestData; } }
+ public static ValueTypeTestData<DateTime> DateTimes { get { return TestData.DateTimeTestData; } }
+ public static ValueTypeTestData<Decimal> Decimals { get { return TestData.DecimalTestData; } }
+ public static ValueTypeTestData<TimeSpan> TimeSpans { get { return TestData.TimeSpanTestData; } }
+ public static ValueTypeTestData<Guid> Guids { get { return TestData.GuidTestData; } }
+ public static ValueTypeTestData<DateTimeOffset> DateTimeOffsets { get { return TestData.DateTimeOffsetTestData; } }
+ public static ValueTypeTestData<SimpleEnum> SimpleEnums { get { return TestData.SimpleEnumTestData; } }
+ public static ValueTypeTestData<LongEnum> LongEnums { get { return TestData.LongEnumTestData; } }
+ public static ValueTypeTestData<FlagsEnum> FlagsEnums { get { return TestData.FlagsEnumTestData; } }
+ public static TestData<string> EmptyStrings { get { return TestData.EmptyStrings; } }
+ public static RefTypeTestData<string> Strings { get { return TestData.StringTestData; } }
+ public static TestData<string> NonNullEmptyStrings { get { return TestData.NonNullEmptyStrings; } }
+ public static RefTypeTestData<ISerializableType> ISerializableTypes { get { return TestData.ISerializableTypeTestData; } }
+ public static ReadOnlyCollection<TestData> ValueTypeTestDataCollection { get { return TestData.ValueTypeTestDataCollection; } }
+ public static ReadOnlyCollection<TestData> RefTypeTestDataCollection { get { return TestData.RefTypeTestDataCollection; } }
+ public static ReadOnlyCollection<TestData> ValueAndRefTypeTestDataCollection { get { return TestData.ValueTypeTestDataCollection; } }
+ public static ReadOnlyCollection<TestData> RepresentativeValueAndRefTypeTestDataCollection { get { return TestData.RepresentativeValueAndRefTypeTestDataCollection; } }
+ }
+} \ No newline at end of file
diff --git a/test/Microsoft.TestCommon/Microsoft/TestCommon/DataSets/RefTypeTestData.cs b/test/Microsoft.TestCommon/Microsoft/TestCommon/DataSets/RefTypeTestData.cs
new file mode 100644
index 00000000..21a3dc6f
--- /dev/null
+++ b/test/Microsoft.TestCommon/Microsoft/TestCommon/DataSets/RefTypeTestData.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Microsoft.TestCommon
+{
+ public class RefTypeTestData<T> : TestData<T> where T : class
+ {
+ private Func<IEnumerable<T>> testDataProvider;
+ private Func<IEnumerable<T>> derivedTypeTestDataProvider;
+ private Func<IEnumerable<T>> knownTypeTestDataProvider;
+
+ public RefTypeTestData(Func<IEnumerable<T>> testDataProvider)
+ {
+ if (testDataProvider == null)
+ {
+ throw new ArgumentNullException("testDataProvider");
+ }
+
+ this.testDataProvider = testDataProvider;
+ }
+
+ public RefTypeTestData(
+ Func<IEnumerable<T>> testDataProvider,
+ Func<IEnumerable<T>> derivedTypeTestDataProvider,
+ Func<IEnumerable<T>> knownTypeTestDataProvider)
+ : this(testDataProvider)
+ {
+ this.derivedTypeTestDataProvider = derivedTypeTestDataProvider;
+ if (this.derivedTypeTestDataProvider != null)
+ {
+ this.RegisterTestDataVariation(TestDataVariations.AsDerivedType, this.Type, this.GetTestDataAsDerivedType);
+ }
+
+ this.knownTypeTestDataProvider = knownTypeTestDataProvider;
+ if (this.knownTypeTestDataProvider != null)
+ {
+ this.RegisterTestDataVariation(TestDataVariations.AsKnownType, this.Type, this.GetTestDataAsDerivedKnownType);
+ }
+ }
+
+ public IEnumerable<T> GetTestDataAsDerivedType()
+ {
+ if (this.derivedTypeTestDataProvider != null)
+ {
+ return this.derivedTypeTestDataProvider();
+ }
+
+ return Enumerable.Empty<T>();
+ }
+
+ public IEnumerable<T> GetTestDataAsDerivedKnownType()
+ {
+ if (this.knownTypeTestDataProvider != null)
+ {
+ return this.knownTypeTestDataProvider();
+ }
+
+ return Enumerable.Empty<T>();
+ }
+
+ protected override IEnumerable<T> GetTypedTestData()
+ {
+ return this.testDataProvider();
+ }
+ }
+}
diff --git a/test/Microsoft.TestCommon/Microsoft/TestCommon/DataSets/TestData.cs b/test/Microsoft.TestCommon/Microsoft/TestCommon/DataSets/TestData.cs
new file mode 100644
index 00000000..4af24f48
--- /dev/null
+++ b/test/Microsoft.TestCommon/Microsoft/TestCommon/DataSets/TestData.cs
@@ -0,0 +1,444 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using Microsoft.TestCommon.Types;
+
+namespace Microsoft.TestCommon
+{
+ /// <summary>
+ /// A base class for test data. A <see cref="TestData"/> instance is associated with a given type, and the <see cref="TestData"/> instance can
+ /// provide instances of the given type to use as data in tests. The same <see cref="TestData"/> instance can also provide instances
+ /// of types related to the given type, such as a <see cref="List<>"/> of the type. See the <see cref="TestDataVariations"/> enum for all the
+ /// variations of test data that a <see cref="TestData"/> instance can provide.
+ /// </summary>
+ public abstract class TestData
+ {
+ /// <summary>
+ /// Common <see cref="TestData"/> for a <see cref="char"/>.
+ /// </summary>
+ public static readonly ValueTypeTestData<char> CharTestData = new ValueTypeTestData<char>('a', Char.MinValue, Char.MaxValue);
+
+ /// <summary>
+ /// Common <see cref="TestData"/> for a <see cref="int"/>.
+ /// </summary>
+ public static readonly ValueTypeTestData<int> IntTestData = new ValueTypeTestData<int>(-1, 0, 1, Int32.MinValue, Int32.MaxValue);
+
+ /// <summary>
+ /// Common <see cref="TestData"/> for a <see cref="uint"/>.
+ /// </summary>
+ public static readonly ValueTypeTestData<uint> UintTestData = new ValueTypeTestData<uint>(0, 1, UInt32.MinValue, UInt32.MaxValue);
+
+ /// <summary>
+ /// Common <see cref="TestData"/> for a <see cref="short"/>.
+ /// </summary>
+ public static readonly ValueTypeTestData<short> ShortTestData = new ValueTypeTestData<short>(-1, 0, 1, Int16.MinValue, Int16.MaxValue);
+
+ /// <summary>
+ /// Common <see cref="TestData"/> for a <see cref="ushort"/>.
+ /// </summary>
+ public static readonly ValueTypeTestData<ushort> UshortTestData = new ValueTypeTestData<ushort>(0, 1, UInt16.MinValue, UInt16.MaxValue);
+
+ /// <summary>
+ /// Common <see cref="TestData"/> for a <see cref="long"/>.
+ /// </summary>
+ public static readonly ValueTypeTestData<long> LongTestData = new ValueTypeTestData<long>(-1, 0, 1, Int64.MinValue, Int64.MaxValue);
+
+ /// <summary>
+ /// Common <see cref="TestData"/> for a <see cref="ulong"/>.
+ /// </summary>
+ public static readonly ValueTypeTestData<ulong> UlongTestData = new ValueTypeTestData<ulong>(0, 1, UInt64.MinValue, UInt64.MaxValue);
+
+ /// <summary>
+ /// Common <see cref="TestData"/> for a <see cref="byte"/>.
+ /// </summary>
+ public static readonly ValueTypeTestData<byte> ByteTestData = new ValueTypeTestData<byte>(0, 1, Byte.MinValue, Byte.MaxValue);
+
+ /// <summary>
+ /// Common <see cref="TestData"/> for a <see cref="sbyte"/>.
+ /// </summary>
+ public static readonly ValueTypeTestData<sbyte> SByteTestData = new ValueTypeTestData<sbyte>(-1, 0, 1, SByte.MinValue, SByte.MaxValue);
+
+ /// <summary>
+ /// Common <see cref="TestData"/> for a <see cref="bool"/>.
+ /// </summary>
+ public static readonly ValueTypeTestData<bool> BoolTestData = new ValueTypeTestData<bool>(true, false);
+
+ /// <summary>
+ /// Common <see cref="TestData"/> for a <see cref="double"/>.
+ /// </summary>
+ public static readonly ValueTypeTestData<double> DoubleTestData = new ValueTypeTestData<double>(
+ -1.0,
+ 0.0,
+ 1.0,
+ double.MinValue,
+ double.MaxValue,
+ double.PositiveInfinity,
+ double.NegativeInfinity);
+
+ /// <summary>
+ /// Common <see cref="TestData"/> for a <see cref="float"/>.
+ /// </summary>
+ public static readonly ValueTypeTestData<float> FloatTestData = new ValueTypeTestData<float>(
+ -1.0f,
+ 0.0f,
+ 1.0f,
+ float.MinValue,
+ float.MaxValue,
+ float.PositiveInfinity,
+ float.NegativeInfinity);
+
+ /// <summary>
+ /// Common <see cref="TestData"/> for a <see cref="decimal"/>.
+ /// </summary>
+ public static readonly ValueTypeTestData<decimal> DecimalTestData = new ValueTypeTestData<decimal>(
+ -1M,
+ 0M,
+ 1M,
+ decimal.MinValue,
+ decimal.MaxValue);
+
+ /// <summary>
+ /// Common <see cref="TestData"/> for a <see cref="DateTime"/>.
+ /// </summary>
+ public static readonly ValueTypeTestData<DateTime> DateTimeTestData = new ValueTypeTestData<DateTime>(
+ DateTime.Now,
+ DateTime.UtcNow,
+ DateTime.MaxValue,
+ DateTime.MinValue);
+
+ /// <summary>
+ /// Common <see cref="TestData"/> for a <see cref="TimeSpan"/>.
+ /// </summary>
+ public static readonly ValueTypeTestData<TimeSpan> TimeSpanTestData = new ValueTypeTestData<TimeSpan>(
+ TimeSpan.MinValue,
+ TimeSpan.MaxValue);
+
+ /// <summary>
+ /// Common <see cref="TestData"/> for a <see cref="Guid"/>.
+ /// </summary>
+ public static readonly ValueTypeTestData<Guid> GuidTestData = new ValueTypeTestData<Guid>(
+ Guid.NewGuid(),
+ Guid.Empty);
+
+ /// <summary>
+ /// Common <see cref="TestData"/> for a <see cref="DateTimeOffset"/>.
+ /// </summary>
+ public static readonly ValueTypeTestData<DateTimeOffset> DateTimeOffsetTestData = new ValueTypeTestData<DateTimeOffset>(
+ DateTimeOffset.MaxValue,
+ DateTimeOffset.MinValue,
+ new DateTimeOffset(DateTime.Now));
+
+ /// <summary>
+ /// Common <see cref="TestData"/> for an <c>enum</c>.
+ /// </summary>
+ public static readonly ValueTypeTestData<SimpleEnum> SimpleEnumTestData = new ValueTypeTestData<SimpleEnum>(
+ SimpleEnum.First,
+ SimpleEnum.Second,
+ SimpleEnum.Third);
+
+ /// <summary>
+ /// Common <see cref="TestData"/> for an <c>enum</c> implemented with a <see cref="long"/>.
+ /// </summary>
+ public static readonly ValueTypeTestData<LongEnum> LongEnumTestData = new ValueTypeTestData<LongEnum>(
+ LongEnum.FirstLong,
+ LongEnum.SecondLong,
+ LongEnum.ThirdLong);
+
+ /// <summary>
+ /// Common <see cref="TestData"/> for an <c>enum</c> decorated with a <see cref="FlagsAttribtute"/>.
+ /// </summary>
+ public static readonly ValueTypeTestData<FlagsEnum> FlagsEnumTestData = new ValueTypeTestData<FlagsEnum>(
+ FlagsEnum.One,
+ FlagsEnum.Two,
+ FlagsEnum.Four);
+
+ /// <summary>
+ /// Expected permutations of non supported file paths.
+ /// </summary>
+ public static readonly TestData<string> NotSupportedFilePaths = new RefTypeTestData<string>(() => new List<string>() {
+ "cc:\\a\\b",
+ });
+
+ /// <summary>
+ /// Expected permutations of invalid file paths.
+ /// </summary>
+ public static readonly TestData<string> InvalidNonNullFilePaths = new RefTypeTestData<string>(() => new List<string>() {
+ String.Empty,
+ "",
+ " ",
+ " ",
+ "\t\t \n ",
+ "c:\\a<b",
+ "c:\\a>b",
+ "c:\\a\"b",
+ "c:\\a\tb",
+ "c:\\a|b",
+ "c:\\a\bb",
+ "c:\\a\0b",
+ });
+
+ /// <summary>
+ /// All expected permutations of an empty string.
+ /// </summary>
+ public static readonly TestData<string> NonNullEmptyStrings = new RefTypeTestData<string>(() => new List<string>() { String.Empty, "", " ", "\t\r\n" });
+
+ /// <summary>
+ /// All expected permutations of an empty string.
+ /// </summary>
+ public static readonly TestData<string> EmptyStrings = new RefTypeTestData<string>(() => new List<string>() { null, String.Empty, "", " ", "\t\r\n" });
+
+ /// <summary>
+ /// Common <see cref="TestData"/> for a <see cref="string"/>.
+ /// </summary>
+ public static readonly RefTypeTestData<string> StringTestData = new RefTypeTestData<string>(() => new List<string>() {
+ "",
+ " ", // one space
+ " ", // multiple spaces
+ " data ", // leading and trailing whitespace
+ "\t\t \n ",
+ "Some String!"});
+
+ /// <summary>
+ /// Common <see cref="TestData"/> for a class that implements <see cref="ISerializable"/>.
+ /// </summary>
+ public static readonly RefTypeTestData<ISerializableType> ISerializableTypeTestData = new RefTypeTestData<ISerializableType>(
+ ISerializableType.GetTestData);
+
+ /// <summary>
+ /// A read-only collection of value type test data.
+ /// </summary>
+ public static readonly ReadOnlyCollection<TestData> ValueTypeTestDataCollection = new ReadOnlyCollection<TestData>(new TestData[] {
+ CharTestData,
+ IntTestData,
+ UintTestData,
+ ShortTestData,
+ UshortTestData,
+ LongTestData,
+ UlongTestData,
+ ByteTestData,
+ SByteTestData,
+ BoolTestData,
+ DoubleTestData,
+ FloatTestData,
+ DecimalTestData,
+ TimeSpanTestData,
+ GuidTestData,
+ DateTimeOffsetTestData,
+ SimpleEnumTestData,
+ LongEnumTestData,
+ FlagsEnumTestData});
+
+ /// <summary>
+ /// A read-only collection of reference type test data.
+ /// </summary>
+ public static readonly ReadOnlyCollection<TestData> RefTypeTestDataCollection = new ReadOnlyCollection<TestData>(new TestData[] {
+ StringTestData,
+ ISerializableTypeTestData});
+
+ /// <summary>
+ /// A read-only collection of value and reference type test data.
+ /// </summary>
+ public static readonly ReadOnlyCollection<TestData> ValueAndRefTypeTestDataCollection = new ReadOnlyCollection<TestData>(
+ ValueTypeTestDataCollection.Concat(RefTypeTestDataCollection).ToList());
+
+ /// <summary>
+ /// A read-only collection of representative values and reference type test data.
+ /// Uses where exhaustive coverage is not required.
+ /// </summary>
+ public static readonly ReadOnlyCollection<TestData> RepresentativeValueAndRefTypeTestDataCollection = new ReadOnlyCollection<TestData>(new TestData[] {
+ IntTestData,
+ BoolTestData,
+ SimpleEnumTestData,
+ StringTestData,
+ });
+
+ private Dictionary<TestDataVariations, TestDataVariationProvider> registeredTestDataVariations;
+
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="TestData"/> class.
+ /// </summary>
+ /// <param name="type">The type associated with the <see cref="TestData"/> instance.</param>
+ protected TestData(Type type)
+ {
+ if (type.ContainsGenericParameters)
+ {
+ throw new InvalidOperationException("Only closed generic types are supported.");
+ }
+
+ this.Type = type;
+ this.registeredTestDataVariations = new Dictionary<TestDataVariations, TestDataVariationProvider>();
+ }
+
+ /// <summary>
+ /// Gets the type associated with the <see cref="TestData"/> instance.
+ /// </summary>
+ public Type Type { get; private set; }
+
+
+ /// <summary>
+ /// Gets the supported test data variations.
+ /// </summary>
+ /// <returns></returns>
+ public IEnumerable<TestDataVariations> GetSupportedTestDataVariations()
+ {
+ return this.registeredTestDataVariations.Keys;
+ }
+
+ /// <summary>
+ /// Gets the related type for the given test data variation or returns null if the <see cref="TestData"/> instance
+ /// doesn't support the given variation.
+ /// </summary>
+ /// <param name="variation">The test data variation with which to create the related <see cref="Type"/>.</param>
+ /// <returns>The related <see cref="Type"/> for the <see cref="TestData.Type"/> as given by the test data variation.</returns>
+ /// <example>
+ /// For example, if the given <see cref="TestData"/> was created for <see cref="string"/> test data and the varation parameter
+ /// was <see cref="TestDataVariations.AsList"/> then the returned type would be <see cref="List<string>"/>.
+ /// </example>
+ public Type GetAsTypeOrNull(TestDataVariations variation)
+ {
+ TestDataVariationProvider testDataVariation = null;
+ if (this.registeredTestDataVariations.TryGetValue(variation, out testDataVariation))
+ {
+ return testDataVariation.Type;
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Gets test data for the given test data variation or returns null if the <see cref="TestData"/> instance
+ /// doesn't support the given variation.
+ /// </summary>
+ /// <param name="variation">The test data variation with which to create the related test data.</param>
+ /// <returns>Test data of the type specified by the <see cref="TestData.GetAsTypeOrNull"/> method.</returns>
+ public object GetAsTestDataOrNull(TestDataVariations variation)
+ {
+ TestDataVariationProvider testDataVariation = null;
+ if (this.registeredTestDataVariations.TryGetValue(variation, out testDataVariation))
+ {
+ return testDataVariation.TestDataProvider();
+ }
+
+ return null;
+ }
+
+
+ /// <summary>
+ /// Allows derived classes to register a <paramref name="testDataProvider "/> <see cref="Func<>"/> that will
+ /// provide test data for a given variation.
+ /// </summary>
+ /// <param name="variation">The variation with which to register the <paramref name="testDataProvider "/>r.</param>
+ /// <param name="type">The type of the test data created by the <paramref name="testDataProvider "/></param>
+ /// <param name="testDataProvider">A <see cref="Func<>"/> that will provide test data.</param>
+ protected void RegisterTestDataVariation(TestDataVariations variation, Type type, Func<object> testDataProvider)
+ {
+ this.registeredTestDataVariations.Add(variation, new TestDataVariationProvider(type, testDataProvider));
+ }
+
+ private class TestDataVariationProvider
+ {
+ public TestDataVariationProvider(Type type, Func<object> testDataProvider)
+ {
+ this.Type = type;
+ this.TestDataProvider = testDataProvider;
+ }
+
+
+ public Func<object> TestDataProvider { get; private set; }
+
+ public Type Type { get; private set; }
+ }
+ }
+
+
+ /// <summary>
+ /// A generic base class for test data.
+ /// </summary>
+ /// <typeparam name="T">The type associated with the test data.</typeparam>
+ public abstract class TestData<T> : TestData, IEnumerable<T>
+ {
+ private static readonly Type OpenIEnumerableType = typeof(IEnumerable<>);
+ private static readonly Type OpenListType = typeof(List<>);
+ private static readonly Type OpenIQueryableType = typeof(IQueryable<>);
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="TestData&lt;T&gt;"/> class.
+ /// </summary>
+ protected TestData()
+ : base(typeof(T))
+ {
+ Type[] typeParams = new Type[] { this.Type };
+
+ Type arrayType = this.Type.MakeArrayType();
+ Type listType = OpenListType.MakeGenericType(typeParams);
+ Type iEnumerableType = OpenIEnumerableType.MakeGenericType(typeParams);
+ Type iQueryableType = OpenIQueryableType.MakeGenericType(typeParams);
+
+ Type[] typeArrayParams = new Type[] { arrayType };
+ Type[] typeListParams = new Type[] { listType };
+ Type[] typeIEnumerableParams = new Type[] { iEnumerableType };
+ Type[] typeIQueryableParams = new Type[] { iQueryableType };
+
+ this.RegisterTestDataVariation(TestDataVariations.AsInstance, this.Type, () => GetTypedTestData());
+ this.RegisterTestDataVariation(TestDataVariations.AsArray, arrayType, GetTestDataAsArray);
+ this.RegisterTestDataVariation(TestDataVariations.AsIEnumerable, iEnumerableType, GetTestDataAsIEnumerable);
+ this.RegisterTestDataVariation(TestDataVariations.AsIQueryable, iQueryableType, GetTestDataAsIQueryable);
+ this.RegisterTestDataVariation(TestDataVariations.AsList, listType, GetTestDataAsList);
+ }
+
+ public IEnumerator<T> GetEnumerator()
+ {
+ return (IEnumerator<T>)this.GetTypedTestData().ToList().GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return (IEnumerator)this.GetTypedTestData().ToList().GetEnumerator();
+ }
+
+ /// <summary>
+ /// Gets the test data as an array.
+ /// </summary>
+ /// <returns>An array of test data of the given type.</returns>
+ public T[] GetTestDataAsArray()
+ {
+ return this.GetTypedTestData().ToArray();
+ }
+
+ /// <summary>
+ /// Gets the test data as a <see cref="List<>"/>.
+ /// </summary>
+ /// <returns>A <see cref="List<>"/> of test data of the given type.</returns>
+ public List<T> GetTestDataAsList()
+ {
+ return this.GetTypedTestData().ToList();
+ }
+
+ /// <summary>
+ /// Gets the test data as an <see cref="IEnumerable<>"/>.
+ /// </summary>
+ /// <returns>An <see cref="IEnumerable<>"/> of test data of the given type.</returns>
+ public IEnumerable<T> GetTestDataAsIEnumerable()
+ {
+ return this.GetTypedTestData().AsEnumerable();
+ }
+
+ /// <summary>
+ /// Gets the test data as an <see cref="IQueryable<>"/>.
+ /// </summary>
+ /// <returns>An <see cref="IQueryable<>"/> of test data of the given type.</returns>
+ public IQueryable<T> GetTestDataAsIQueryable()
+ {
+ return this.GetTypedTestData().AsQueryable();
+ }
+
+ /// <summary>
+ /// Must be implemented by derived types to return test data of the given type.
+ /// </summary>
+ /// <returns>Test data of the given type.</returns>
+ protected abstract IEnumerable<T> GetTypedTestData();
+ }
+}
diff --git a/test/Microsoft.TestCommon/Microsoft/TestCommon/DataSets/TestDataVariations.cs b/test/Microsoft.TestCommon/Microsoft/TestCommon/DataSets/TestDataVariations.cs
new file mode 100644
index 00000000..9bd61c2e
--- /dev/null
+++ b/test/Microsoft.TestCommon/Microsoft/TestCommon/DataSets/TestDataVariations.cs
@@ -0,0 +1,95 @@
+using System;
+
+namespace Microsoft.TestCommon
+{
+ /// <summary>
+ /// An flags enum that can be used to indicate different variations of a given
+ /// <see cref="TestData"/> instance.
+ /// </summary>
+ [Flags]
+ public enum TestDataVariations
+ {
+ /// <summary>
+ /// An individual instance of a given <see cref="TestData"/> type.
+ /// </summary>
+ AsInstance = 0x1,
+
+ /// <summary>
+ /// An individual instance of a type that derives from a given <see cref="TestData"/> type.
+ /// </summary>
+ AsDerivedType = 0x2,
+
+ /// <summary>
+ /// An individual instance of a given <see cref="TestData"/> type that has a property value
+ /// that is a known type of the declared property type.
+ /// </summary>
+ AsKnownType = 0x4,
+
+ /// <summary>
+ /// A <see cref="Nullable<>"/> instance of a given <see cref="TestData"/> type. Only applies to
+ /// instances of <see cref="ValueTypeTestData"/>.
+ /// </summary>
+ AsNullable = 0x8,
+
+ /// <summary>
+ /// An instance of a <see cref="System.Collections.Generic.List<>"/> of a given <see cref="TestData"/> type.
+ /// </summary>
+ AsList = 0x10,
+
+ /// <summary>
+ /// An instance of a array of the <see cref="TestData"/> type.
+ /// </summary>
+ AsArray = 0x20,
+
+ /// <summary>
+ /// An instance of an <see cref="System.Collections.Generic.IEnumerable<>"/> of a given <see cref="TestData"/> type.
+ /// </summary>
+ AsIEnumerable = 0x40,
+
+ /// <summary>
+ /// An instance of an <see cref="System.Linq.IQueryable<>"/> of a given <see cref="TestData"/> type.
+ /// </summary>
+ AsIQueryable = 0x80,
+
+ /// <summary>
+ /// An instance of a DataContract type in which a given <see cref="TestData"/> type is a member.
+ /// </summary>
+ AsDataMember = 0x100,
+
+ /// <summary>
+ /// An instance of a type in which a given <see cref="TestData"/> type is decorated with a
+ /// <see cref="System.Xml.Serialization.XmlElementAttribute"/>.
+ /// </summary>
+ AsXmlElementProperty = 0x200,
+
+ /// <summary>
+ /// All of the flags for single instance variations of a given <see cref="TestData"/> type.
+ /// </summary>
+ AllSingleInstances = AsInstance | AsDerivedType | AsKnownType | AsNullable,
+
+ /// <summary>
+ /// All of the flags for collection variations of a given <see cref="TestData"/> type.
+ /// </summary>
+ AllCollections = AsList | AsArray | AsIEnumerable | AsIQueryable,
+
+ /// <summary>
+ /// All of the flags for variations in which a given <see cref="TestData"/> type is a property on another type.
+ /// </summary>
+ AllProperties = AsDataMember | AsXmlElementProperty,
+
+ /// <summary>
+ /// All of the flags for interface collection variations of a given <see cref="TestData"/> type.
+ /// </summary>
+ AllInterfaces = AsIEnumerable | AsIQueryable,
+
+ /// <summary>
+ /// All of the flags except for the interface collection variations of a given <see cref="TestData"/> type.
+ /// </summary>
+ AllNonInterfaces = All & ~AllInterfaces,
+
+ /// <summary>
+ /// All of the flags for all of the variations of a given <see cref="TestData"/> type.
+ /// </summary>
+ All = AllSingleInstances | AllCollections | AllProperties
+ }
+}
diff --git a/test/Microsoft.TestCommon/Microsoft/TestCommon/DataSets/ValueTypeTestData.cs b/test/Microsoft.TestCommon/Microsoft/TestCommon/DataSets/ValueTypeTestData.cs
new file mode 100644
index 00000000..6d730e4b
--- /dev/null
+++ b/test/Microsoft.TestCommon/Microsoft/TestCommon/DataSets/ValueTypeTestData.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Microsoft.TestCommon
+{
+ public class ValueTypeTestData<T> : TestData<T> where T : struct
+ {
+ private static readonly Type OpenNullableType = typeof(Nullable<>);
+ private T[] testData;
+
+ public ValueTypeTestData(params T[] testData)
+ : base()
+ {
+ this.testData = testData;
+
+ Type[] typeParams = new Type[] { this.Type };
+ this.RegisterTestDataVariation(TestDataVariations.AsNullable, OpenNullableType.MakeGenericType(typeParams), GetTestDataAsNullable);
+ }
+
+ public IEnumerable<Nullable<T>> GetTestDataAsNullable()
+ {
+ return this.GetTypedTestData().Select(d => new Nullable<T>(d));
+ }
+
+ protected override IEnumerable<T> GetTypedTestData()
+ {
+ return this.testData;
+ }
+ }
+}
diff --git a/test/Microsoft.TestCommon/Microsoft/TestCommon/GenericTypeAssert.cs b/test/Microsoft.TestCommon/Microsoft/TestCommon/GenericTypeAssert.cs
new file mode 100644
index 00000000..45990138
--- /dev/null
+++ b/test/Microsoft.TestCommon/Microsoft/TestCommon/GenericTypeAssert.cs
@@ -0,0 +1,489 @@
+using System;
+using System.Linq;
+using System.Reflection;
+using Xunit;
+
+namespace Microsoft.TestCommon
+{
+ /// <summary>
+ /// MSTest assertion class to provide convenience and assert methods for generic types
+ /// whose type parameters are not known at compile time.
+ /// </summary>
+ public class GenericTypeAssert
+ {
+ private static readonly GenericTypeAssert singleton = new GenericTypeAssert();
+
+ public static GenericTypeAssert Singleton { get { return singleton; } }
+
+ /// <summary>
+ /// Asserts the given <paramref name="genericBaseType"/> is a generic type and creates a new
+ /// bound generic type using <paramref name="genericParameterType"/>. It then asserts there
+ /// is a constructor that will accept <paramref name="parameterTypes"/> and returns it.
+ /// </summary>
+ /// <param name="genericBaseType">The unbound generic base type.</param>
+ /// <param name="genericParameterType">The type of the single generic parameter to apply to create a bound generic type.</param>
+ /// <param name="parameterTypes">The list of parameter types for a constructor that must exist.</param>
+ /// <returns>The <see cref="ConstructorInfo"/> of that constructor which may be invoked to create that new generic type.</returns>
+ public ConstructorInfo GetConstructor(Type genericBaseType, Type genericParameterType, params Type[] parameterTypes)
+ {
+ Assert.NotNull(genericBaseType);
+ Assert.True(genericBaseType.IsGenericTypeDefinition);
+ Assert.NotNull(genericParameterType);
+ Assert.NotNull(parameterTypes);
+
+ Type genericType = genericBaseType.MakeGenericType(new Type[] { genericParameterType });
+ ConstructorInfo ctor = genericType.GetConstructor(parameterTypes);
+ Assert.True(ctor != null, String.Format("Test error: failed to locate generic ctor for type '{0}<{1}>',", genericBaseType.Name, genericParameterType.Name));
+ return ctor;
+ }
+
+ /// <summary>
+ /// Asserts the given <paramref name="genericBaseType"/> is a generic type and creates a new
+ /// bound generic type using <paramref name="genericParameterType"/>. It then asserts there
+ /// is a constructor that will accept <paramref name="parameterTypes"/> and returns it.
+ /// </summary>
+ /// <param name="genericBaseType">The unbound generic base type.</param>
+ /// <param name="genericParameterTypes">The types of the generic parameters to apply to create a bound generic type.</param>
+ /// <param name="parameterTypes">The list of parameter types for a constructor that must exist.</param>
+ /// <returns>The <see cref="ConstructorInfo"/> of that constructor which may be invoked to create that new generic type.</returns>
+ public ConstructorInfo GetConstructor(Type genericBaseType, Type[] genericParameterTypes, params Type[] parameterTypes)
+ {
+ Assert.NotNull(genericBaseType);
+ Assert.True(genericBaseType.IsGenericTypeDefinition);
+ Assert.NotNull(genericParameterTypes);
+ Assert.NotNull(parameterTypes);
+
+ Type genericType = genericBaseType.MakeGenericType(genericParameterTypes);
+ ConstructorInfo ctor = genericType.GetConstructor(parameterTypes);
+ Assert.True(ctor != null, String.Format("Test error: failed to locate generic ctor for type '{0}<>',", genericBaseType.Name));
+ return ctor;
+ }
+
+ /// <summary>
+ /// Creates a new bound generic type and invokes the constructor matched from <see cref="parameterTypes"/>.
+ /// </summary>
+ /// <param name="genericBaseType">The unbound generic base type.</param>
+ /// <param name="genericParameterType">The type of the single generic parameter to apply to create a bound generic type.</param>
+ /// <param name="parameterTypes">The list of parameter types for a constructor that must exist.</param>
+ /// <param name="parameterValues">The list of values to supply to the constructor</param>
+ /// <returns>The instance created by calling that constructor.</returns>
+ public object InvokeConstructor(Type genericBaseType, Type genericParameterType, Type[] parameterTypes, object[] parameterValues)
+ {
+ ConstructorInfo ctor = GetConstructor(genericBaseType, genericParameterType, parameterTypes);
+ Assert.NotNull(parameterValues);
+ Assert.Equal(parameterTypes.Length, parameterValues.Length);
+ return ctor.Invoke(parameterValues);
+ }
+
+ /// <summary>
+ /// Creates a new bound generic type and invokes the constructor matched from <see cref="parameterTypes"/>.
+ /// </summary>
+ /// <param name="genericBaseType">The unbound generic base type.</param>
+ /// <param name="genericParameterTypse">The types of the generic parameters to apply to create a bound generic type.</param>
+ /// <param name="parameterTypes">The list of parameter types for a constructor that must exist.</param>
+ /// <param name="parameterValues">The list of values to supply to the constructor</param>
+ /// <returns>The instance created by calling that constructor.</returns>
+ public object InvokeConstructor(Type genericBaseType, Type[] genericParameterTypes, Type[] parameterTypes, object[] parameterValues)
+ {
+ ConstructorInfo ctor = GetConstructor(genericBaseType, genericParameterTypes, parameterTypes);
+ Assert.NotNull(parameterValues);
+ Assert.Equal(parameterTypes.Length, parameterValues.Length);
+ return ctor.Invoke(parameterValues);
+ }
+
+ /// <summary>
+ /// Creates a new bound generic type and invokes the constructor matched from the types of <paramref name="parameterValues"/>.
+ /// </summary>
+ /// <param name="genericBaseType">The unbound generic base type.</param>
+ /// <param name="genericParameterType">The type of the single generic parameter to apply to create a bound generic type.</param>
+ /// <param name="parameterValues">The list of values to supply to the constructor. It must be possible to determine the</param>
+ /// <returns>The instance created by calling that constructor.</returns>
+ public object InvokeConstructor(Type genericBaseType, Type genericParameterType, params object[] parameterValues)
+ {
+ Assert.NotNull(genericBaseType);
+ Assert.True(genericBaseType.IsGenericTypeDefinition);
+ Assert.NotNull(genericParameterType);
+
+ Type genericType = genericBaseType.MakeGenericType(new Type[] { genericParameterType });
+
+ ConstructorInfo ctor = FindConstructor(genericType, parameterValues);
+ Assert.True(ctor != null, String.Format("Test error: failed to locate generic ctor for type '{0}<{1}>',", genericBaseType.Name, genericParameterType.Name));
+ return ctor.Invoke(parameterValues);
+ }
+
+ /// <summary>
+ /// Creates a new bound generic type and invokes the constructor matched from the types of <paramref name="parameterValues"/>.
+ /// </summary>
+ /// <param name="genericBaseType">The unbound generic base type.</param>
+ /// <param name="genericParameterTypes">The types of the generic parameters to apply to create a bound generic type.</param>
+ /// <param name="parameterValues">The list of values to supply to the constructor. It must be possible to determine the</param>
+ /// <returns>The instance created by calling that constructor.</returns>
+ public object InvokeConstructor(Type genericBaseType, Type[] genericParameterTypes, params object[] parameterValues)
+ {
+ Assert.NotNull(genericBaseType);
+ Assert.True(genericBaseType.IsGenericTypeDefinition);
+ Assert.NotNull(genericParameterTypes);
+
+ Type genericType = genericBaseType.MakeGenericType(genericParameterTypes);
+
+ ConstructorInfo ctor = FindConstructor(genericType, parameterValues);
+ Assert.True(ctor != null, String.Format("Test error: failed to locate generic ctor for type '{0}<>',", genericBaseType.Name));
+ return ctor.Invoke(parameterValues);
+ }
+
+ /// <summary>
+ /// Creates a new bound generic type and invokes the constructor matched from <see cref="parameterTypes"/>.
+ /// </summary>
+ /// <typeparam name="T">The type of object the constuctor is expected to yield.</typeparam>
+ /// <param name="genericBaseType">The unbound generic base type.</param>
+ /// <param name="genericParameterType">The type of the single generic parameter to apply to create a bound generic type.</param>
+ /// <param name="parameterTypes">The list of parameter types for a constructor that must exist.</param>
+ /// <param name="parameterValues">The list of values to supply to the constructor</param>
+ /// <returns>An instance of type <typeparamref name="T"/>.</returns>
+ public T InvokeConstructor<T>(Type genericBaseType, Type genericParameterType, Type[] parameterTypes, object[] parameterValues)
+ {
+ ConstructorInfo ctor = GetConstructor(genericBaseType, genericParameterType, parameterTypes);
+ Assert.NotNull(parameterValues);
+ Assert.Equal(parameterTypes.Length, parameterValues.Length);
+ return (T)ctor.Invoke(parameterValues);
+ }
+
+ /// <summary>
+ /// Creates a new bound generic type and invokes the constructor matched from <see cref="parameterTypes"/>.
+ /// </summary>
+ /// <typeparam name="T">The type of object the constuctor is expected to yield.</typeparam>
+ /// <param name="genericBaseType">The unbound generic base type.</param>
+ /// <param name="genericParameterTypes">The types of the generic parameters to apply to create a bound generic type.</param>
+ /// <param name="parameterTypes">The list of parameter types for a constructor that must exist.</param>
+ /// <param name="parameterValues">The list of values to supply to the constructor</param>
+ /// <returns>An instance of type <typeparamref name="T"/>.</returns>
+ public T InvokeConstructor<T>(Type genericBaseType, Type[] genericParameterTypes, Type[] parameterTypes, object[] parameterValues)
+ {
+ ConstructorInfo ctor = GetConstructor(genericBaseType, genericParameterTypes, parameterTypes);
+ Assert.NotNull(parameterValues);
+ Assert.Equal(parameterTypes.Length, parameterValues.Length);
+ return (T)ctor.Invoke(parameterValues);
+ }
+
+ /// <summary>
+ /// Creates a new bound generic type and invokes the constructor matched from <see cref="parameterTypes"/>.
+ /// </summary>
+ /// <typeparam name="T">The type of object the constuctor is expected to yield.</typeparam>
+ /// <param name="genericBaseType">The unbound generic base type.</param>
+ /// <param name="genericParameterType">The type of the single generic parameter to apply to create a bound generic type.</param>
+ /// <param name="parameterValues">The list of values to supply to the constructor. It must be possible to determine the</param>
+ /// <returns>The instance created by calling that constructor.</returns>
+ /// <returns>An instance of type <typeparamref name="T"/>.</returns>
+ public T InvokeConstructor<T>(Type genericBaseType, Type genericParameterType, params object[] parameterValues)
+ {
+ Assert.NotNull(genericBaseType == null);
+ Assert.True(genericBaseType.IsGenericTypeDefinition);
+ Assert.NotNull(genericParameterType);
+
+ Type genericType = genericBaseType.MakeGenericType(new Type[] { genericParameterType });
+
+ ConstructorInfo ctor = FindConstructor(genericType, parameterValues);
+ Assert.True(ctor != null, String.Format("Test error: failed to locate generic ctor for type '{0}<{1}>',", genericBaseType.Name, genericParameterType.Name));
+ return (T)ctor.Invoke(parameterValues);
+ }
+
+ /// <summary>
+ /// Creates a new bound generic type and invokes the constructor matched from <see cref="parameterTypes"/>.
+ /// </summary>
+ /// <typeparam name="T">The type of object the constuctor is expected to yield.</typeparam>
+ /// <param name="genericBaseType">The unbound generic base type.</param>
+ /// <param name="genericParameterTypes">The types of the generic parameters to apply to create a bound generic type.</param>
+ /// <param name="parameterValues">The list of values to supply to the constructor. It must be possible to determine the</param>
+ /// <returns>The instance created by calling that constructor.</returns>
+ /// <returns>An instance of type <typeparamref name="T"/>.</returns>
+ public T InvokeConstructor<T>(Type genericBaseType, Type[] genericParameterTypes, params object[] parameterValues)
+ {
+ Assert.NotNull(genericBaseType);
+ Assert.True(genericBaseType.IsGenericTypeDefinition);
+ Assert.NotNull(genericParameterTypes);
+
+ Type genericType = genericBaseType.MakeGenericType(genericParameterTypes);
+
+ ConstructorInfo ctor = FindConstructor(genericType, parameterValues);
+ Assert.True(ctor != null, String.Format("Test error: failed to locate generic ctor for type '{0}<>',", genericBaseType.Name));
+ return (T)ctor.Invoke(parameterValues);
+ }
+
+ /// <summary>
+ /// Asserts the given instance is one from a generic type of the specified parameter type.
+ /// </summary>
+ /// <typeparam name="T">The type of instance.</typeparam>
+ /// <param name="instance">The instance to test.</param>
+ /// <param name="genericTypeParameter">The type of the generic parameter to which the instance's generic type should have been bound.</param>
+ public void IsCorrectGenericType<T>(T instance, Type genericTypeParameter)
+ {
+ Assert.NotNull(instance);
+ Assert.NotNull(genericTypeParameter);
+ Assert.True(instance.GetType().IsGenericType);
+ Type[] genericArguments = instance.GetType().GetGenericArguments();
+ Assert.Equal(1, genericArguments.Length);
+ Assert.Equal(genericTypeParameter, genericArguments[0]);
+ }
+
+ /// <summary>
+ /// Invokes via Reflection the method on the given instance.
+ /// </summary>
+ /// <param name="instance">The instance to use.</param>
+ /// <param name="methodName">The name of the method to call.</param>
+ /// <param name="parameterTypes">The types of the parameters to the method.</param>
+ /// <param name="parameterValues">The values to supply to the method.</param>
+ /// <returns>The results of the method.</returns>
+ public object InvokeMethod(object instance, string methodName, Type[] parameterTypes, object[] parameterValues)
+ {
+ Assert.NotNull(instance);
+ Assert.NotNull(parameterTypes);
+ Assert.NotNull(parameterValues);
+ Assert.Equal(parameterTypes.Length, parameterValues.Length);
+ MethodInfo methodInfo = instance.GetType().GetMethod(methodName, parameterTypes);
+ Assert.NotNull(methodInfo);
+ return methodInfo.Invoke(instance, parameterValues);
+ }
+
+ /// <summary>
+ /// Invokes via Reflection the static method on the given type.
+ /// </summary>
+ /// <param name="type">The type containing the method.</param>
+ /// <param name="methodName">The name of the method to call.</param>
+ /// <param name="parameterTypes">The types of the parameters to the method.</param>
+ /// <param name="parameterValues">The values to supply to the method.</param>
+ /// <returns>The results of the method.</returns>
+ public object InvokeMethod(Type type, string methodName, Type[] parameterTypes, object[] parameterValues)
+ {
+ Assert.NotNull(type);
+ Assert.NotNull(parameterTypes);
+ Assert.NotNull(parameterValues);
+ Assert.Equal(parameterTypes.Length, parameterValues.Length);
+ MethodInfo methodInfo = type.GetMethod(methodName, parameterTypes);
+ Assert.NotNull(methodInfo);
+ return methodInfo.Invoke(null, parameterValues);
+ }
+
+ /// <summary>
+ /// Invokes via Reflection the static method on the given type.
+ /// </summary>
+ /// <param name="type">The type containing the method.</param>
+ /// <param name="methodName">The name of the method to call.</param>
+ /// <param name="genericParameterType">The generic parameter type of the method.</param>
+ /// <param name="parameterTypes">The types of the parameters to the method.</param>
+ /// <param name="parameterValues">The values to supply to the method.</param>
+ /// <returns>The results of the method.</returns>
+ public MethodInfo CreateGenericMethod(Type type, string methodName, Type genericParameterType, Type[] parameterTypes)
+ {
+ Assert.NotNull(type);
+ Assert.NotNull(parameterTypes);
+ Assert.NotNull(genericParameterType);
+ //MethodInfo methodInfo = type.GetMethod(methodName, parameterTypes);
+ MethodInfo methodInfo = type.GetMethods().Where((m) => m.Name.Equals(methodName, StringComparison.OrdinalIgnoreCase) && m.IsGenericMethod && AreAssignableFrom(m.GetParameters(), parameterTypes)).FirstOrDefault();
+ Assert.NotNull(methodInfo);
+ Assert.True(methodInfo.IsGenericMethod);
+ MethodInfo genericMethod = methodInfo.MakeGenericMethod(genericParameterType);
+ Assert.NotNull(genericMethod);
+ return genericMethod;
+ }
+
+ /// <summary>
+ /// Invokes via Reflection the static generic method on the given type.
+ /// </summary>
+ /// <param name="type">The type containing the method.</param>
+ /// <param name="methodName">The name of the method to call.</param>
+ /// <param name="genericParameterType">The generic parameter type of the method.</param>
+ /// <param name="parameterTypes">The types of the parameters to the method.</param>
+ /// <param name="parameterValues">The values to supply to the method.</param>
+ /// <returns>The results of the method.</returns>
+ public object InvokeGenericMethod(Type type, string methodName, Type genericParameterType, Type[] parameterTypes, object[] parameterValues)
+ {
+ MethodInfo methodInfo = CreateGenericMethod(type, methodName, genericParameterType, parameterTypes);
+ Assert.Equal(parameterTypes.Length, parameterValues.Length);
+ return methodInfo.Invoke(null, parameterValues);
+ }
+
+ /// <summary>
+ /// Invokes via Reflection the generic method on the given instance.
+ /// </summary>
+ /// <param name="instance">The instance on which to invoke the method.</param>
+ /// <param name="methodName">The name of the method to call.</param>
+ /// <param name="genericParameterType">The generic parameter type of the method.</param>
+ /// <param name="parameterTypes">The types of the parameters to the method.</param>
+ /// <param name="parameterValues">The values to supply to the method.</param>
+ /// <returns>The results of the method.</returns>
+ public object InvokeGenericMethod(object instance, string methodName, Type genericParameterType, Type[] parameterTypes, object[] parameterValues)
+ {
+ Assert.NotNull(instance);
+ MethodInfo methodInfo = CreateGenericMethod(instance.GetType(), methodName, genericParameterType, parameterTypes);
+ Assert.Equal(parameterTypes.Length, parameterValues.Length);
+ return methodInfo.Invoke(instance, parameterValues);
+ }
+
+ /// <summary>
+ /// Invokes via Reflection the generic method on the given instance.
+ /// </summary>
+ /// <typeparam name="T">The type of the return value from the method.</typeparam>
+ /// <param name="instance">The instance on which to invoke the method.</param>
+ /// <param name="methodName">The name of the method to call.</param>
+ /// <param name="genericParameterType">The generic parameter type of the method.</param>
+ /// <param name="parameterTypes">The types of the parameters to the method.</param>
+ /// <param name="parameterValues">The values to supply to the method.</param>
+ /// <returns>The results of the method.</returns>
+ public T InvokeGenericMethod<T>(object instance, string methodName, Type genericParameterType, Type[] parameterTypes, object[] parameterValues)
+ {
+ return (T)InvokeGenericMethod(instance, methodName, genericParameterType, parameterTypes, parameterValues);
+ }
+
+ /// <summary>
+ /// Invokes via Reflection the method on the given instance.
+ /// </summary>
+ /// <param name="instance">The instance to use.</param>
+ /// <param name="methodName">The name of the method to call.</param>
+ /// <param name="parameterValues">The values to supply to the method.</param>
+ /// <returns>The results of the method.</returns>
+ public object InvokeMethod(object instance, string methodName, params object[] parameterValues)
+ {
+ Assert.NotNull(instance);
+ MethodInfo methodInfo = FindMethod(instance.GetType(), methodName, parameterValues);
+ Assert.NotNull(methodInfo);
+ return methodInfo.Invoke(instance, parameterValues);
+ }
+
+ /// <summary>
+ /// Invokes via Reflection the static method on the given type.
+ /// </summary>
+ /// <param name="instance">The instance to use.</param>
+ /// <param name="methodName">The name of the method to call.</param>
+ /// <param name="parameterValues">The values to supply to the method.</param>
+ /// <returns>The results of the method.</returns>
+ public object InvokeMethod(Type type, string methodName, params object[] parameterValues)
+ {
+ Assert.NotNull(type);
+ MethodInfo methodInfo = FindMethod(type, methodName, parameterValues);
+ Assert.NotNull(methodInfo);
+ return methodInfo.Invoke(null, parameterValues);
+ }
+
+ /// <summary>
+ /// Invokes via Reflection the method on the given instance.
+ /// </summary>
+ /// <param name="instance">The instance to use.</param>
+ /// <param name="methodName">The name of the method to call.</param>
+ /// <param name="genericParameterType">The type of the generic parameter.</param>
+ /// <param name="parameterValues">The values to supply to the method.</param>
+ /// <returns>The results of the method.</returns>
+ public object InvokeGenericMethod(object instance, string methodName, Type genericParameterType, params object[] parameterValues)
+ {
+ Assert.NotNull(instance);
+ Assert.NotNull(genericParameterType);
+ MethodInfo methodInfo = FindMethod(instance.GetType(), methodName, parameterValues);
+ Assert.NotNull(methodInfo);
+ Assert.True(methodInfo.IsGenericMethod);
+ MethodInfo genericMethod = methodInfo.MakeGenericMethod(genericParameterType);
+ return genericMethod.Invoke(instance, parameterValues);
+ }
+
+ /// <summary>
+ /// Invokes via Reflection the method on the given instance.
+ /// </summary>
+ /// <param name="instance">The instance to use.</param>
+ /// <param name="methodName">The name of the method to call.</param>
+ /// <param name="genericParameterType">The type of the generic parameter.</param>
+ /// <param name="parameterValues">The values to supply to the method.</param>
+ /// <returns>The results of the method.</returns>
+ public object InvokeGenericMethod(Type type, string methodName, Type genericParameterType, params object[] parameterValues)
+ {
+ Assert.NotNull(type);
+ Assert.NotNull(genericParameterType);
+ MethodInfo methodInfo = FindMethod(type, methodName, parameterValues);
+ Assert.NotNull(methodInfo);
+ Assert.True(methodInfo.IsGenericMethod);
+ MethodInfo genericMethod = methodInfo.MakeGenericMethod(genericParameterType);
+ return genericMethod.Invoke(null, parameterValues);
+ }
+
+ /// <summary>
+ /// Retrieves the value from the specified property.
+ /// </summary>
+ /// <param name="instance">The instance containing the property value.</param>
+ /// <param name="propertyName">The name of the property.</param>
+ /// <param name="failureMessage">The error message to prefix any test assertions.</param>
+ /// <returns>The value returned from the property.</returns>
+ public object GetProperty(object instance, string propertyName, string failureMessage)
+ {
+ PropertyInfo propertyInfo = instance.GetType().GetProperty(propertyName);
+ Assert.NotNull(propertyInfo);
+ return propertyInfo.GetValue(instance, null);
+ }
+
+ private static bool AreAssignableFrom(Type[] parameterTypes, params object[] parameterValues)
+ {
+ Assert.NotNull(parameterTypes);
+ Assert.NotNull(parameterValues);
+ if (parameterTypes.Length != parameterValues.Length)
+ {
+ return false;
+ }
+
+ for (int i = 0; i < parameterTypes.Length; ++i)
+ {
+ if (!parameterTypes[i].IsInstanceOfType(parameterValues[i]))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static bool AreAssignableFrom(ParameterInfo[] parameterInfos, params Type[] parameterTypes)
+ {
+ Assert.NotNull(parameterInfos);
+ Assert.NotNull(parameterTypes);
+ Type[] parameterInfoTypes = parameterInfos.Select<ParameterInfo, Type>((info) => info.ParameterType).ToArray();
+ if (parameterInfoTypes.Length != parameterTypes.Length)
+ {
+ return false;
+ }
+
+ for (int i = 0; i < parameterInfoTypes.Length; ++i)
+ {
+ // Generic parameters are assumed to be assignable
+ if (parameterInfoTypes[i].IsGenericParameter)
+ {
+ continue;
+ }
+
+ if (!parameterInfoTypes[i].IsAssignableFrom(parameterTypes[i]))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static bool AreAssignableFrom(ParameterInfo[] parameterInfos, params object[] parameterValues)
+ {
+ Assert.NotNull(parameterInfos);
+ Assert.NotNull(parameterValues);
+ Type[] parameterTypes = parameterInfos.Select<ParameterInfo, Type>((info) => info.ParameterType).ToArray();
+ return AreAssignableFrom(parameterTypes, parameterValues);
+ }
+
+ private static ConstructorInfo FindConstructor(Type type, params object[] parameterValues)
+ {
+ Assert.NotNull(type);
+ Assert.NotNull(parameterValues);
+ return type.GetConstructors().FirstOrDefault((c) => AreAssignableFrom(c.GetParameters(), parameterValues));
+ }
+
+ private static MethodInfo FindMethod(Type type, string methodName, params object[] parameterValues)
+ {
+ Assert.NotNull(type);
+ Assert.False(String.IsNullOrWhiteSpace(methodName));
+ Assert.NotNull(parameterValues);
+ return type.GetMethods().FirstOrDefault((m) => String.Equals(m.Name, methodName, StringComparison.Ordinal) && AreAssignableFrom(m.GetParameters(), parameterValues));
+ }
+ }
+}
diff --git a/test/Microsoft.TestCommon/Microsoft/TestCommon/HttpAssert.cs b/test/Microsoft.TestCommon/Microsoft/TestCommon/HttpAssert.cs
new file mode 100644
index 00000000..645be48a
--- /dev/null
+++ b/test/Microsoft.TestCommon/Microsoft/TestCommon/HttpAssert.cs
@@ -0,0 +1,252 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Text.RegularExpressions;
+using Xunit;
+using MsTest = Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.TestCommon
+{
+ /// <summary>
+ /// MSTest utility for testing <see cref="HttpResponseMessage"/> instances.
+ /// </summary>
+ public class HttpAssert
+ {
+ private const string CommaSeperator = ", ";
+ private static readonly HttpAssert singleton = new HttpAssert();
+
+ public static HttpAssert Singleton { get { return singleton; } }
+
+ /// <summary>
+ /// Asserts that the expected <see cref="HttpRequestMessage"/> is equal to the actual <see cref="HttpRequestMessage"/>.
+ /// </summary>
+ /// <param name="expected">The expected <see cref="HttpRequestMessage"/>. Should not be <c>null</c>.</param>
+ /// <param name="actual">The actual <see cref="HttpRequestMessage"/>. Should not be <c>null</c>.</param>
+ public void Equal(HttpRequestMessage expected, HttpRequestMessage actual)
+ {
+ Assert.NotNull(expected);
+ Assert.NotNull(actual);
+
+ Assert.Equal(expected.Version, actual.Version);
+ Equal(expected.Headers, actual.Headers);
+
+ if (expected.Content == null)
+ {
+ Assert.Null(actual.Content);
+ }
+ else
+ {
+ string expectedContent = CleanContentString(expected.Content.ReadAsStringAsync().Result);
+ string actualContent = CleanContentString(actual.Content.ReadAsStringAsync().Result);
+ Assert.Equal(expectedContent, actualContent);
+ Equal(expected.Content.Headers, actual.Content.Headers);
+ }
+ }
+
+ /// <summary>
+ /// Asserts that the expected <see cref="HttpResponseMessage"/> is equal to the actual <see cref="HttpResponseMessage"/>.
+ /// </summary>
+ /// <param name="expected">The expected <see cref="HttpResponseMessage"/>. Should not be <c>null</c>.</param>
+ /// <param name="actual">The actual <see cref="HttpResponseMessage"/>. Should not be <c>null</c>.</param>
+ public void Equal(HttpResponseMessage expected, HttpResponseMessage actual)
+ {
+ Equal(expected, actual, null);
+ }
+
+ /// <summary>
+ /// Asserts that the expected <see cref="HttpResponseMessage"/> is equal to the actual <see cref="HttpResponseMessage"/>.
+ /// </summary>
+ /// <param name="expected">The expected <see cref="HttpResponseMessage"/>. Should not be <c>null</c>.</param>
+ /// <param name="actual">The actual <see cref="HttpResponseMessage"/>. Should not be <c>null</c>.</param>
+ /// <param name="verifyContentCallback">The callback to verify the Content string. If it is null, Assert.Equal will be used. </param>
+ public void Equal(HttpResponseMessage expected, HttpResponseMessage actual, Action<string, string> verifyContentStringCallback)
+ {
+ Assert.NotNull(expected);
+ Assert.NotNull(actual);
+
+ Assert.Equal(expected.StatusCode, actual.StatusCode);
+ Assert.Equal(expected.ReasonPhrase, actual.ReasonPhrase);
+ Assert.Equal(expected.Version, actual.Version);
+ Equal(expected.Headers, actual.Headers);
+
+ if (expected.Content == null)
+ {
+ Assert.Null(actual.Content);
+ }
+ else
+ {
+ string expectedContent = CleanContentString(expected.Content.ReadAsStringAsync().Result);
+ string actualContent = CleanContentString(actual.Content.ReadAsStringAsync().Result);
+ if (verifyContentStringCallback != null)
+ {
+ verifyContentStringCallback(expectedContent, actualContent);
+ }
+ else
+ {
+ Assert.Equal(expectedContent, actualContent);
+ }
+ Equal(expected.Content.Headers, actual.Content.Headers);
+ }
+ }
+
+ /// <summary>
+ /// Asserts that the expected <see cref="HttpHeaders"/> instance is equal to the actual <see cref="actualHeaders"/> instance.
+ /// </summary>
+ /// <param name="expectedHeaders">The expected <see cref="HttpHeaders"/> instance. Should not be <c>null</c>.</param>
+ /// <param name="actualHeaders">The actual <see cref="HttpHeaders"/> instance. Should not be <c>null</c>.</param>
+ public void Equal(HttpHeaders expectedHeaders, HttpHeaders actualHeaders)
+ {
+ Assert.NotNull(expectedHeaders);
+ Assert.NotNull(actualHeaders);
+
+ Assert.Equal(expectedHeaders.Count(), actualHeaders.Count());
+
+ foreach (KeyValuePair<string, IEnumerable<string>> expectedHeader in expectedHeaders)
+ {
+ KeyValuePair<string, IEnumerable<string>> actualHeader = actualHeaders.FirstOrDefault(h => h.Key == expectedHeader.Key);
+ Assert.NotNull(actualHeader);
+
+ if (expectedHeader.Key == "Date")
+ {
+ HandleDateHeader(expectedHeader.Value.ToArray(), actualHeader.Value.ToArray());
+ }
+ else
+ {
+ string expectedHeaderStr = string.Join(CommaSeperator, expectedHeader.Value);
+ string actualHeaderStr = string.Join(CommaSeperator, actualHeader.Value);
+ Assert.Equal(expectedHeaderStr, actualHeaderStr);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Asserts the given <see cref="HttpHeaders"/> contain the given <paramref name="values"/>
+ /// for the given <paramref name="name"/>.
+ /// </summary>
+ /// <param name="headers">The <see cref="HttpHeaders"/> to examine. It cannot be <c>null</c>.</param>
+ /// <param name="name">The name of the header. It cannot be empty.</param>
+ /// <param name="values">The values that must all be present. It cannot be null.</param>
+ public void Contains(HttpHeaders headers, string name, params string[] values)
+ {
+ Assert.NotNull(headers);
+ Assert.False(String.IsNullOrWhiteSpace(name), "Test error: name cannot be empty.");
+ Assert.NotNull(values);
+
+ IEnumerable<string> headerValues = null;
+ bool foundIt = headers.TryGetValues(name, out headerValues);
+ Assert.True(foundIt);
+ MsTest.CollectionAssert.IsSubsetOf(values.ToList(), headerValues.ToList(), "Headers did not contain any or all of the expected headers.");
+ }
+
+ public bool IsKnownUnserializableType(Type type, Func<Type, bool> isTypeUnserializableCallback)
+ {
+ if (isTypeUnserializableCallback != null && isTypeUnserializableCallback(type))
+ {
+ return true;
+ }
+
+ if (type.IsGenericType)
+ {
+ if (typeof(IEnumerable).IsAssignableFrom(type))
+ {
+ if (type.GetMethod("Add") == null)
+ {
+ return true;
+ }
+ }
+
+ // Generic type -- recursively analyze generic arguments
+ return IsKnownUnserializableType(type.GetGenericArguments()[0], isTypeUnserializableCallback);
+ }
+
+ if (type.HasElementType && IsKnownUnserializableType(type.GetElementType(), isTypeUnserializableCallback))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ public bool IsKnownUnserializable(Type type, object obj, Func<Type, bool> isTypeUnserializableCallback)
+ {
+ if (IsKnownUnserializableType(type, isTypeUnserializableCallback))
+ {
+ return true;
+ }
+
+ return obj != null && IsKnownUnserializableType(obj.GetType(), isTypeUnserializableCallback);
+ }
+
+ public bool IsKnownUnserializable(Type type, object obj)
+ {
+ return IsKnownUnserializable(type, obj, null);
+ }
+
+ public bool CanRoundTrip(Type type)
+ {
+ if (typeof(TimeSpan).IsAssignableFrom(type))
+ {
+ return false;
+ }
+
+ if (typeof(DateTimeOffset).IsAssignableFrom(type))
+ {
+ return false;
+ }
+
+ if (type.IsGenericType)
+ {
+ foreach (Type genericParameterType in type.GetGenericArguments())
+ {
+ if (!CanRoundTrip(genericParameterType))
+ {
+ return false;
+ }
+ }
+ }
+
+ if (type.HasElementType)
+ {
+ return CanRoundTrip(type.GetElementType());
+ }
+
+ return true;
+ }
+
+ private static void HandleDateHeader(string[] expectedDateHeaderValues, string[] actualDateHeaderValues)
+ {
+ Assert.Equal(expectedDateHeaderValues.Length, actualDateHeaderValues.Length);
+
+ for (int i = 0; i < expectedDateHeaderValues.Length; i++)
+ {
+ DateTime expectedDateTime = DateTime.Parse(expectedDateHeaderValues[i]);
+ DateTime actualDateTime = DateTime.Parse(actualDateHeaderValues[i]);
+
+ Assert.Equal(expectedDateTime.Year, actualDateTime.Year);
+ Assert.Equal(expectedDateTime.Month, actualDateTime.Month);
+ Assert.Equal(expectedDateTime.Day, actualDateTime.Day);
+
+ int hourDifference = Math.Abs(actualDateTime.Hour - expectedDateTime.Hour);
+ Assert.True(hourDifference <= 1);
+
+ int minuteDifference = Math.Abs(actualDateTime.Minute - expectedDateTime.Minute);
+ Assert.True(minuteDifference <= 1);
+ }
+ }
+
+ private static string CleanContentString(string content)
+ {
+ Assert.Null(content);
+
+ string cleanedContent = null;
+
+ // remove any port numbers from Uri's
+ cleanedContent = Regex.Replace(content, ":\\d+", "");
+
+ return cleanedContent;
+ }
+ }
+}
diff --git a/test/Microsoft.TestCommon/Microsoft/TestCommon/MediaTypeAssert.cs b/test/Microsoft.TestCommon/Microsoft/TestCommon/MediaTypeAssert.cs
new file mode 100644
index 00000000..39841350
--- /dev/null
+++ b/test/Microsoft.TestCommon/Microsoft/TestCommon/MediaTypeAssert.cs
@@ -0,0 +1,53 @@
+using System;
+using System.Net.Http.Headers;
+using Xunit;
+
+namespace Microsoft.TestCommon
+{
+ public class MediaTypeAssert
+ {
+ private static readonly MediaTypeAssert singleton = new MediaTypeAssert();
+
+ public static MediaTypeAssert Singleton { get { return singleton; } }
+
+ public void AreEqual(MediaTypeHeaderValue expected, MediaTypeHeaderValue actual, string errorMessage)
+ {
+ if (expected != null || actual != null)
+ {
+ Assert.NotNull(expected);
+ Assert.Equal(0, new MediaTypeHeaderValueComparer().Compare(expected, actual));
+ }
+ }
+
+ public void AreEqual(MediaTypeHeaderValue expected, string actual, string errorMessage)
+ {
+ if (expected != null || !String.IsNullOrEmpty(actual))
+ {
+ MediaTypeHeaderValue actualMediaType = new MediaTypeHeaderValue(actual);
+ Assert.NotNull(expected);
+ Assert.Equal(0, new MediaTypeHeaderValueComparer().Compare(expected, actualMediaType));
+ }
+ }
+
+ public void AreEqual(string expected, string actual, string errorMessage)
+ {
+ if (!String.IsNullOrEmpty(expected) || !String.IsNullOrEmpty(actual))
+ {
+ Assert.NotNull(expected);
+ MediaTypeHeaderValue expectedMediaType = new MediaTypeHeaderValue(expected);
+ MediaTypeHeaderValue actualMediaType = new MediaTypeHeaderValue(actual);
+ Assert.Equal(0, new MediaTypeHeaderValueComparer().Compare(expectedMediaType, actualMediaType));
+ }
+ }
+
+ public void AreEqual(string expected, MediaTypeHeaderValue actual, string errorMessage)
+ {
+ if (!String.IsNullOrEmpty(expected) || actual != null)
+ {
+ Assert.NotNull(expected);
+ MediaTypeHeaderValue expectedMediaType = new MediaTypeHeaderValue(expected); ;
+ Assert.Equal(0, new MediaTypeHeaderValueComparer().Compare(expectedMediaType, actual));
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.TestCommon/Microsoft/TestCommon/MediaTypeHeaderValueComparer.cs b/test/Microsoft.TestCommon/Microsoft/TestCommon/MediaTypeHeaderValueComparer.cs
new file mode 100644
index 00000000..8da097f5
--- /dev/null
+++ b/test/Microsoft.TestCommon/Microsoft/TestCommon/MediaTypeHeaderValueComparer.cs
@@ -0,0 +1,88 @@
+using System;
+using System.Collections.Generic;
+using System.Net.Http.Headers;
+
+namespace Microsoft.TestCommon
+{
+ public class MediaTypeHeaderValueComparer : IComparer<MediaTypeHeaderValue>
+ {
+ private static readonly MediaTypeHeaderValueComparer mediaTypeComparer = new MediaTypeHeaderValueComparer();
+
+ public MediaTypeHeaderValueComparer()
+ {
+ }
+
+ public static MediaTypeHeaderValueComparer Comparer
+ {
+ get
+ {
+ return mediaTypeComparer;
+ }
+ }
+
+ public int Compare(MediaTypeHeaderValue mediaType1, MediaTypeHeaderValue mediaType2)
+ {
+ ParsedMediaTypeHeaderValue parsedMediaType1 = new ParsedMediaTypeHeaderValue(mediaType1);
+ ParsedMediaTypeHeaderValue parsedMediaType2 = new ParsedMediaTypeHeaderValue(mediaType2);
+
+ int returnValue = CompareBasedOnQualityFactor(parsedMediaType1, parsedMediaType2);
+
+ if (returnValue == 0)
+ {
+ if (!String.Equals(parsedMediaType1.Type, parsedMediaType2.Type, StringComparison.OrdinalIgnoreCase))
+ {
+ if (parsedMediaType1.IsAllMediaRange)
+ {
+ return 1;
+ }
+ else if (parsedMediaType2.IsAllMediaRange)
+ {
+ return -1;
+ }
+ }
+ else if (!String.Equals(parsedMediaType1.SubType, parsedMediaType2.SubType, StringComparison.OrdinalIgnoreCase))
+ {
+ if (parsedMediaType1.IsSubTypeMediaRange)
+ {
+ return 1;
+ }
+ else if (parsedMediaType2.IsSubTypeMediaRange)
+ {
+ return -1;
+ }
+ }
+ else
+ {
+ if (!parsedMediaType1.HasNonQualityFactorParameter)
+ {
+ if (parsedMediaType2.HasNonQualityFactorParameter)
+ {
+ return 1;
+ }
+ }
+ else if (!parsedMediaType2.HasNonQualityFactorParameter)
+ {
+ return -1;
+ }
+ }
+ }
+
+ return returnValue;
+ }
+
+ private static int CompareBasedOnQualityFactor(ParsedMediaTypeHeaderValue parsedMediaType1, ParsedMediaTypeHeaderValue parsedMediaType2)
+ {
+ double qualityDifference = parsedMediaType1.QualityFactor - parsedMediaType2.QualityFactor;
+ if (qualityDifference < 0)
+ {
+ return 1;
+ }
+ else if (qualityDifference > 0)
+ {
+ return -1;
+ }
+
+ return 0;
+ }
+ }
+} \ No newline at end of file
diff --git a/test/Microsoft.TestCommon/Microsoft/TestCommon/ParsedMediaTypeHeaderValue.cs b/test/Microsoft.TestCommon/Microsoft/TestCommon/ParsedMediaTypeHeaderValue.cs
new file mode 100644
index 00000000..95fd2a69
--- /dev/null
+++ b/test/Microsoft.TestCommon/Microsoft/TestCommon/ParsedMediaTypeHeaderValue.cs
@@ -0,0 +1,109 @@
+using System;
+using System.Net.Http.Headers;
+
+namespace Microsoft.TestCommon
+{
+ internal class ParsedMediaTypeHeaderValue
+ {
+ private const string MediaRangeAsterisk = "*";
+ private const char MediaTypeSubTypeDelimiter = '/';
+ private const string QualityFactorParameterName = "q";
+ private const double DefaultQualityFactor = 1.0;
+
+ private MediaTypeHeaderValue mediaType;
+ private string type;
+ private string subType;
+ private bool? hasNonQualityFactorParameter;
+ private double? qualityFactor;
+
+ public ParsedMediaTypeHeaderValue(MediaTypeHeaderValue mediaType)
+ {
+ this.mediaType = mediaType;
+ string[] splitMediaType = mediaType.MediaType.Split(MediaTypeSubTypeDelimiter);
+ this.type = splitMediaType[0];
+ this.subType = splitMediaType[1];
+ }
+
+ public string Type
+ {
+ get
+ {
+ return this.type;
+ }
+ }
+
+ public string SubType
+ {
+ get
+ {
+ return this.subType;
+ }
+ }
+
+ public bool IsAllMediaRange
+ {
+ get
+ {
+ return this.IsSubTypeMediaRange && String.Equals(MediaRangeAsterisk, this.Type, StringComparison.Ordinal);
+ }
+ }
+
+ public bool IsSubTypeMediaRange
+ {
+ get
+ {
+ return String.Equals(MediaRangeAsterisk, this.SubType, StringComparison.Ordinal);
+ }
+ }
+
+ public bool HasNonQualityFactorParameter
+ {
+ get
+ {
+ if (!this.hasNonQualityFactorParameter.HasValue)
+ {
+ this.hasNonQualityFactorParameter = false;
+ foreach (NameValueHeaderValue param in this.mediaType.Parameters)
+ {
+ if (!String.Equals(QualityFactorParameterName, param.Name, StringComparison.Ordinal))
+ {
+ this.hasNonQualityFactorParameter = true;
+ }
+ }
+ }
+
+ return this.hasNonQualityFactorParameter.Value;
+ }
+ }
+
+ public string CharSet
+ {
+ get
+ {
+ return this.mediaType.CharSet;
+ }
+ }
+
+ public double QualityFactor
+ {
+ get
+ {
+ if (!this.qualityFactor.HasValue)
+ {
+ MediaTypeWithQualityHeaderValue mediaTypeWithQuality = this.mediaType as MediaTypeWithQualityHeaderValue;
+ if (mediaTypeWithQuality != null)
+ {
+ this.qualityFactor = mediaTypeWithQuality.Quality;
+ }
+
+ if (!this.qualityFactor.HasValue)
+ {
+ this.qualityFactor = DefaultQualityFactor;
+ }
+ }
+
+ return this.qualityFactor.Value;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/test/Microsoft.TestCommon/Microsoft/TestCommon/RegexReplacement.cs b/test/Microsoft.TestCommon/Microsoft/TestCommon/RegexReplacement.cs
new file mode 100644
index 00000000..46737f2f
--- /dev/null
+++ b/test/Microsoft.TestCommon/Microsoft/TestCommon/RegexReplacement.cs
@@ -0,0 +1,38 @@
+using System.Text.RegularExpressions;
+
+namespace Microsoft.TestCommon
+{
+ public class RegexReplacement
+ {
+ Regex regex;
+ string replacement;
+
+ public RegexReplacement(Regex regex, string replacement)
+ {
+ this.regex = regex;
+ this.replacement = replacement;
+ }
+
+ public RegexReplacement(string regex, string replacement)
+ {
+ this.regex = new Regex(regex);
+ this.replacement = replacement;
+ }
+
+ public Regex Regex
+ {
+ get
+ {
+ return this.regex;
+ }
+ }
+
+ public string Replacement
+ {
+ get
+ {
+ return this.replacement;
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.TestCommon/Microsoft/TestCommon/RuntimeEnvironment.cs b/test/Microsoft.TestCommon/Microsoft/TestCommon/RuntimeEnvironment.cs
new file mode 100644
index 00000000..4a3b523d
--- /dev/null
+++ b/test/Microsoft.TestCommon/Microsoft/TestCommon/RuntimeEnvironment.cs
@@ -0,0 +1,31 @@
+using System;
+using Microsoft.Win32;
+
+namespace Microsoft.TestCommon
+{
+ public static class RuntimeEnvironment
+ {
+ private const string NetFx40FullSubKey = @"SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full";
+ private const string Version = "Version";
+
+ static RuntimeEnvironment()
+ {
+ object runtimeVersion = Registry.LocalMachine.OpenSubKey(RuntimeEnvironment.NetFx40FullSubKey).GetValue(RuntimeEnvironment.Version);
+ string versionFor40String = runtimeVersion as string;
+ if (versionFor40String != null)
+ {
+ VersionFor40 = new Version(versionFor40String);
+ }
+ }
+
+ private static Version VersionFor40;
+
+ public static bool IsVersion45Installed
+ {
+ get
+ {
+ return VersionFor40.Major > 4 || (VersionFor40.Major == 4 && VersionFor40.Minor >= 5);
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.TestCommon/Microsoft/TestCommon/SerializerAssert.cs b/test/Microsoft.TestCommon/Microsoft/TestCommon/SerializerAssert.cs
new file mode 100644
index 00000000..316017b5
--- /dev/null
+++ b/test/Microsoft.TestCommon/Microsoft/TestCommon/SerializerAssert.cs
@@ -0,0 +1,150 @@
+using System;
+using System.IO;
+using System.Runtime.Serialization;
+using System.Runtime.Serialization.Json;
+using System.Xml.Serialization;
+
+namespace Microsoft.TestCommon
+{
+ /// <summary>
+ /// MSTest utility for testing code operating against a stream.
+ /// </summary>
+ public class SerializerAssert
+ {
+ private static SerializerAssert singleton = new SerializerAssert();
+
+ public static SerializerAssert Singleton { get { return singleton; } }
+
+ /// <summary>
+ /// Creates a <see cref="Stream"/>, serializes <paramref name="objectInstance"/> to it using
+ /// <see cref="XmlSerializer"/>, rewinds the stream and calls <see cref="codeThatChecks"/>.
+ /// </summary>
+ /// <param name="type">The type to serialize. It cannot be <c>null</c>.</param>
+ /// <param name="objectInstance">The value to serialize.</param>
+ /// <param name="codeThatChecks">Code to check the contents of the stream.</param>
+ public void UsingXmlSerializer(Type type, object objectInstance, Action<Stream> codeThatChecks)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+
+ if (codeThatChecks == null)
+ {
+ throw new ArgumentNullException("codeThatChecks");
+ }
+
+ XmlSerializer serializer = new XmlSerializer(type);
+
+ using (MemoryStream stream = new MemoryStream())
+ {
+ serializer.Serialize(stream, objectInstance);
+
+ stream.Flush();
+ stream.Seek(0L, SeekOrigin.Begin);
+
+ codeThatChecks(stream);
+ }
+ }
+
+ /// <summary>
+ /// Creates a <see cref="Stream"/>, serializes <paramref name="objectInstance"/> to it using
+ /// <see cref="XmlSerializer"/>, rewinds the stream and calls <see cref="codeThatChecks"/>.
+ /// </summary>
+ /// <typeparam name="T">The type to serialize.</typeparam>
+ /// <param name="objectInstance">The value to serialize.</param>
+ /// <param name="codeThatChecks">Code to check the contents of the stream.</param>
+ public void UsingXmlSerializer<T>(T objectInstance, Action<Stream> codeThatChecks)
+ {
+ UsingXmlSerializer(typeof(T), objectInstance, codeThatChecks);
+ }
+
+ /// <summary>
+ /// Creates a <see cref="Stream"/>, serializes <paramref name="objectInstance"/> to it using
+ /// <see cref="DataContractSerializer"/>, rewinds the stream and calls <see cref="codeThatChecks"/>.
+ /// </summary>
+ /// <param name="type">The type to serialize. It cannot be <c>null</c>.</param>
+ /// <param name="objectInstance">The value to serialize.</param>
+ /// <param name="codeThatChecks">Code to check the contents of the stream.</param>
+ public void UsingDataContractSerializer(Type type, object objectInstance, Action<Stream> codeThatChecks)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+
+ if (codeThatChecks == null)
+ {
+ throw new ArgumentNullException("codeThatChecks");
+ }
+
+ DataContractSerializer serializer = new DataContractSerializer(type);
+
+ using (MemoryStream stream = new MemoryStream())
+ {
+ serializer.WriteObject(stream, objectInstance);
+
+ stream.Flush();
+ stream.Seek(0L, SeekOrigin.Begin);
+
+ codeThatChecks(stream);
+ }
+ }
+
+ /// <summary>
+ /// Creates a <see cref="Stream"/>, serializes <paramref name="objectInstance"/> to it using
+ /// <see cref="DataContractSerializer"/>, rewinds the stream and calls <see cref="codeThatChecks"/>.
+ /// </summary>
+ /// <typeparam name="T">The type to serialize.</typeparam>
+ /// <param name="objectInstance">The value to serialize.</param>
+ /// <param name="codeThatChecks">Code to check the contents of the stream.</param>
+ public void UsingDataContractSerializer<T>(T objectInstance, Action<Stream> codeThatChecks)
+ {
+ UsingDataContractSerializer(typeof(T), objectInstance, codeThatChecks);
+ }
+
+ /// <summary>
+ /// Creates a <see cref="Stream"/>, serializes <paramref name="objectInstance"/> to it using
+ /// <see cref="DataContractJsonSerializer"/>, rewinds the stream and calls <see cref="codeThatChecks"/>.
+ /// </summary>
+ /// <param name="type">The type to serialize. It cannot be <c>null</c>.</param>
+ /// <param name="objectInstance">The value to serialize.</param>
+ /// <param name="codeThatChecks">Code to check the contents of the stream.</param>
+ public static void UsingDataContractJsonSerializer(Type type, object objectInstance, Action<Stream> codeThatChecks)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+
+ if (codeThatChecks == null)
+ {
+ throw new ArgumentNullException("codeThatChecks");
+ }
+
+ DataContractJsonSerializer serializer = new DataContractJsonSerializer(type);
+
+ using (MemoryStream stream = new MemoryStream())
+ {
+ serializer.WriteObject(stream, objectInstance);
+
+ stream.Flush();
+ stream.Seek(0L, SeekOrigin.Begin);
+
+ codeThatChecks(stream);
+ }
+ }
+
+ /// <summary>
+ /// Creates a <see cref="Stream"/>, serializes <paramref name="objectInstance"/> to it using
+ /// <see cref="DataContractJsonSerializer"/>, rewinds the stream and calls <see cref="codeThatChecks"/>.
+ /// </summary>
+ /// <typeparam name="T">The type to serialize.</typeparam>
+ /// <param name="objectInstance">The value to serialize.</param>
+ /// <param name="codeThatChecks">Code to check the contents of the stream.</param>
+ public void UsingDataContractJsonSerializer<T>(T objectInstance, Action<Stream> codeThatChecks)
+ {
+ UsingDataContractJsonSerializer(typeof(T), objectInstance, codeThatChecks);
+ }
+ }
+}
diff --git a/test/Microsoft.TestCommon/Microsoft/TestCommon/StreamAssert.cs b/test/Microsoft.TestCommon/Microsoft/TestCommon/StreamAssert.cs
new file mode 100644
index 00000000..5f86df06
--- /dev/null
+++ b/test/Microsoft.TestCommon/Microsoft/TestCommon/StreamAssert.cs
@@ -0,0 +1,94 @@
+using System;
+using System.IO;
+
+namespace Microsoft.TestCommon
+{
+ //// TODO RONCAIN using System.Runtime.Serialization.Json;
+
+ /// <summary>
+ /// MSTest utility for testing code operating against a stream.
+ /// </summary>
+ public class StreamAssert
+ {
+ private static StreamAssert singleton = new StreamAssert();
+
+ public static StreamAssert Singleton { get { return singleton; } }
+
+ /// <summary>
+ /// Creates a <see cref="Stream"/>, invokes <paramref name="codeThatWrites"/> to write to it,
+ /// rewinds the stream to the beginning and invokes <paramref name="codeThatReads"/>.
+ /// </summary>
+ /// <param name="codeThatWrites">Code to write to the stream. It cannot be <c>null</c>.</param>
+ /// <param name="codeThatReads">Code that reads from the stream. It cannot be <c>null</c>.</param>
+ public void WriteAndRead(Action<Stream> codeThatWrites, Action<Stream> codeThatReads)
+ {
+ if (codeThatWrites == null)
+ {
+ throw new ArgumentNullException("codeThatWrites");
+ }
+
+ if (codeThatReads == null)
+ {
+ throw new ArgumentNullException("codeThatReads");
+ }
+
+ using (MemoryStream stream = new MemoryStream())
+ {
+ codeThatWrites(stream);
+
+ stream.Flush();
+ stream.Seek(0L, SeekOrigin.Begin);
+
+ codeThatReads(stream);
+ }
+ }
+
+ /// <summary>
+ /// Creates a <see cref="Stream"/>, invokes <paramref name="codeThatWrites"/> to write to it,
+ /// rewinds the stream to the beginning and invokes <paramref name="codeThatReads"/> to obtain
+ /// the result to return from this method.
+ /// </summary>
+ /// <param name="codeThatWrites">Code to write to the stream. It cannot be <c>null</c>.</param>
+ /// <param name="codeThatReads">Code that reads from the stream and returns the result. It cannot be <c>null</c>.</param>
+ /// <returns>The value returned from <paramref name="codeThatReads"/>.</returns>
+ public static object WriteAndReadResult(Action<Stream> codeThatWrites, Func<Stream, object> codeThatReads)
+ {
+ if (codeThatWrites == null)
+ {
+ throw new ArgumentNullException("codeThatWrites");
+ }
+
+ if (codeThatReads == null)
+ {
+ throw new ArgumentNullException("codeThatReads");
+ }
+
+ object result = null;
+ using (MemoryStream stream = new MemoryStream())
+ {
+ codeThatWrites(stream);
+
+ stream.Flush();
+ stream.Seek(0L, SeekOrigin.Begin);
+
+ result = codeThatReads(stream);
+ }
+
+ return result;
+ }
+
+ /// <summary>
+ /// Creates a <see cref="Stream"/>, invokes <paramref name="codeThatWrites"/> to write to it,
+ /// rewinds the stream to the beginning and invokes <paramref name="codeThatReads"/> to obtain
+ /// the result to return from this method.
+ /// </summary>
+ /// <typeparam name="T">The type of the result expected.</typeparam>
+ /// <param name="codeThatWrites">Code to write to the stream. It cannot be <c>null</c>.</param>
+ /// <param name="codeThatReads">Code that reads from the stream and returns the result. It cannot be <c>null</c>.</param>
+ /// <returns>The value returned from <paramref name="codeThatReads"/>.</returns>
+ public T WriteAndReadResult<T>(Action<Stream> codeThatWrites, Func<Stream, object> codeThatReads)
+ {
+ return (T)WriteAndReadResult(codeThatWrites, codeThatReads);
+ }
+ }
+}
diff --git a/test/Microsoft.TestCommon/Microsoft/TestCommon/TaskAssert.cs b/test/Microsoft.TestCommon/Microsoft/TestCommon/TaskAssert.cs
new file mode 100644
index 00000000..fc3f872a
--- /dev/null
+++ b/test/Microsoft.TestCommon/Microsoft/TestCommon/TaskAssert.cs
@@ -0,0 +1,99 @@
+using System;
+using System.Reflection;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Microsoft.TestCommon
+{
+ /// <summary>
+ /// MSTest assert class to make assertions about tests using <see cref="Task"/>.
+ /// </summary>
+ public class TaskAssert
+ {
+ private static int timeOutMs = System.Diagnostics.Debugger.IsAttached ? TimeoutConstant.DefaultTimeout : TimeoutConstant.DefaultTimeout * 10;
+ private static TaskAssert singleton = new TaskAssert();
+
+ public static TaskAssert Singleton { get { return singleton; } }
+
+ /// <summary>
+ /// Asserts the given task has been started. TAP guidelines are that all
+ /// <see cref="Task"/> objects returned from public API's have been started.
+ /// </summary>
+ /// <param name="task">The <see cref="Task"/> to test.</param>
+ public void IsStarted(Task task)
+ {
+ Assert.NotNull(task);
+ Assert.True(task.Status != TaskStatus.Created);
+ }
+
+ /// <summary>
+ /// Asserts the given task completes successfully. This method will block the
+ /// current thread waiting for the task, but will timeout if it does not complete.
+ /// </summary>
+ /// <param name="task">The <see cref="Task"/> to test.</param>
+ public void Succeeds(Task task)
+ {
+ IsStarted(task);
+ task.Wait(timeOutMs);
+ AggregateException aggregateException = task.Exception;
+ Exception innerException = aggregateException == null ? null : aggregateException.InnerException;
+ Assert.Null(innerException);
+ }
+
+ /// <summary>
+ /// Asserts the given task completes successfully and returns a result.
+ /// Use this overload for a generic <see cref="Task"/> whose generic parameter is not known at compile time.
+ /// This method will block the current thread waiting for the task, but will timeout if it does not complete.
+ /// </summary>
+ /// <param name="task">The <see cref="Task"/> to test.</param>
+ /// <returns>The result from that task.</returns>
+ public object SucceedsWithResult(Task task)
+ {
+ Succeeds(task);
+ Assert.True(task.GetType().IsGenericType);
+ Type[] genericArguments = task.GetType().GetGenericArguments();
+ Assert.Equal(1, genericArguments.Length);
+ PropertyInfo resultProperty = task.GetType().GetProperty("Result");
+ Assert.NotNull(resultProperty);
+ return resultProperty.GetValue(task, null);
+ }
+
+ /// <summary>
+ /// Asserts the given task completes successfully and returns a <typeparamref name="T"/> result.
+ /// This method will block the current thread waiting for the task, but will timeout if it does not complete.
+ /// </summary>
+ /// <typeparam name="T">The result of the <see cref="Task"/>.</typeparam>
+ /// <param name="task">The <see cref="Task"/> to test.</param>
+ /// <returns>The result from that task.</returns>
+ public T SucceedsWithResult<T>(Task<T> task)
+ {
+ Succeeds(task);
+ return task.Result;
+ }
+
+ /// <summary>
+ /// Asserts the given <see cref="Task"/> completes successfully and yields
+ /// the expected result.
+ /// </summary>
+ /// <param name="task">The <see cref="Task"/> to test.</param>
+ /// <param name="expectedObj">The expected result.</param>
+ public void ResultEquals(Task task, object expectedObj)
+ {
+ object actualObj = SucceedsWithResult(task);
+ Assert.Equal(expectedObj, actualObj);
+ }
+
+ /// <summary>
+ /// Asserts the given <see cref="Task"/> completes successfully and yields
+ /// the expected result.
+ /// </summary>
+ /// <typeparam name="T">The type the task will return.</typeparam>
+ /// <param name="task">The task to test.</param>
+ /// <param name="expectedObj">The expected result.</param>
+ public void ResultEquals<T>(Task<T> task, T expectedObj)
+ {
+ T actualObj = SucceedsWithResult<T>(task);
+ Assert.Equal(expectedObj, actualObj);
+ }
+ }
+}
diff --git a/test/Microsoft.TestCommon/Microsoft/TestCommon/TestDataSetAttribute.cs b/test/Microsoft.TestCommon/Microsoft/TestCommon/TestDataSetAttribute.cs
new file mode 100644
index 00000000..3490a3ca
--- /dev/null
+++ b/test/Microsoft.TestCommon/Microsoft/TestCommon/TestDataSetAttribute.cs
@@ -0,0 +1,193 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using Xunit.Extensions;
+
+namespace Microsoft.TestCommon
+{
+ public class TestDataSetAttribute : DataAttribute
+ {
+ public Type DeclaringType { get; set; }
+
+ public string PropertyName { get; set; }
+
+ public TestDataVariations TestDataVariations { get; set; }
+
+ private IEnumerable<Tuple<Type, string>> ExtraDataSets { get; set; }
+
+ public TestDataSetAttribute(Type declaringType, string propertyName, TestDataVariations testDataVariations = TestCommon.TestDataVariations.All)
+ {
+ DeclaringType = declaringType;
+ PropertyName = propertyName;
+ TestDataVariations = testDataVariations;
+ ExtraDataSets = new List<Tuple<Type, string>>();
+ }
+
+ public TestDataSetAttribute(Type declaringType, string propertyName,
+ Type declaringType1, string propertyName1,
+ TestDataVariations testDataVariations = TestCommon.TestDataVariations.All)
+ : this(declaringType, propertyName, testDataVariations)
+ {
+ ExtraDataSets = new List<Tuple<Type, string>> { Tuple.Create(declaringType1, propertyName1) };
+ }
+
+ public TestDataSetAttribute(Type declaringType, string propertyName,
+ Type declaringType1, string propertyName1,
+ Type declaringType2, string propertyName2,
+ TestDataVariations testDataVariations = TestCommon.TestDataVariations.All)
+ : this(declaringType, propertyName, testDataVariations)
+ {
+ ExtraDataSets = new List<Tuple<Type, string>> { Tuple.Create(declaringType1, propertyName1), Tuple.Create(declaringType2, propertyName2) };
+ }
+
+ public TestDataSetAttribute(Type declaringType, string propertyName,
+ Type declaringType1, string propertyName1,
+ Type declaringType2, string propertyName2,
+ Type declaringType3, string propertyName3,
+ TestDataVariations testDataVariations = TestCommon.TestDataVariations.All)
+ : this(declaringType, propertyName, testDataVariations)
+ {
+ ExtraDataSets = new List<Tuple<Type, string>> { Tuple.Create(declaringType1, propertyName1), Tuple.Create(declaringType2, propertyName2), Tuple.Create(declaringType3, propertyName3) };
+ }
+
+ public TestDataSetAttribute(Type declaringType, string propertyName,
+ Type declaringType1, string propertyName1,
+ Type declaringType2, string propertyName2,
+ Type declaringType3, string propertyName3,
+ Type declaringType4, string propertyName4,
+ TestDataVariations testDataVariations = TestCommon.TestDataVariations.All)
+ : this(declaringType, propertyName, testDataVariations)
+ {
+ ExtraDataSets = new List<Tuple<Type, string>>
+ {
+ Tuple.Create(declaringType1, propertyName1), Tuple.Create(declaringType2, propertyName2),
+ Tuple.Create(declaringType3, propertyName3), Tuple.Create(declaringType4, propertyName4)
+ };
+ }
+
+ public override IEnumerable<object[]> GetData(MethodInfo methodUnderTest, Type[] parameterTypes)
+ {
+ IEnumerable<object[]> baseDataSet = GetBaseDataSet(DeclaringType, PropertyName, TestDataVariations);
+ IEnumerable<IEnumerable<object[]>> extraDataSets = GetExtraDataSets();
+
+ IEnumerable<IEnumerable<object[]>> finalDataSets = (new[] { baseDataSet }).Concat(extraDataSets);
+
+ var datasets = CrossProduct(finalDataSets);
+
+ return datasets;
+ }
+
+ private static IEnumerable<object[]> CrossProduct(IEnumerable<IEnumerable<object[]>> datasets)
+ {
+ if (datasets.Count() == 1)
+ {
+ foreach (var dataset in datasets.First())
+ {
+ yield return dataset;
+ }
+ }
+ else
+ {
+ IEnumerable<object[]> datasetLeft = datasets.First();
+ IEnumerable<object[]> datasetRight = CrossProduct(datasets.Skip(1));
+
+ foreach (var dataLeft in datasetLeft)
+ {
+ foreach (var dataRight in datasetRight)
+ {
+ yield return dataLeft.Concat(dataRight).ToArray();
+ }
+ }
+ }
+ }
+
+ // The base data set(first one) can either be a TestDataSet or a TestDataSetCollection
+ private static IEnumerable<object[]> GetBaseDataSet(Type declaringType, string propertyName, TestDataVariations variations)
+ {
+ return TryGetDataSetFromTestDataCollection(declaringType, propertyName, variations) ?? GetDataSet(declaringType, propertyName);
+ }
+
+ private IEnumerable<IEnumerable<object[]>> GetExtraDataSets()
+ {
+ foreach (var tuple in ExtraDataSets)
+ {
+ yield return GetDataSet(tuple.Item1, tuple.Item2);
+ }
+ }
+
+ private static object GetTestDataPropertyValue(Type declaringType, string propertyName)
+ {
+ PropertyInfo property = declaringType.GetProperty(propertyName, BindingFlags.Static | BindingFlags.Public);
+
+ if (property == null)
+ {
+ throw new ArgumentException(string.Format("Could not find public static property {0} on {1}", propertyName, declaringType.FullName));
+ }
+ else
+ {
+ return property.GetValue(null, null);
+ }
+ }
+
+ private static IEnumerable<object[]> GetDataSet(Type declaringType, string propertyName)
+ {
+ object propertyValue = GetTestDataPropertyValue(declaringType, propertyName);
+
+ // box the dataset items if the property is not a RefTypeTestData
+ IEnumerable<object> value = (propertyValue as IEnumerable<object>) ?? (propertyValue as IEnumerable).Cast<object>();
+ if (value == null)
+ {
+ throw new InvalidOperationException(string.Format("{0}.{1} is either null or does not implement IEnumerable", declaringType.FullName, propertyName));
+ }
+
+ return value.Select((data) => new object[] { data });
+ }
+
+ private static IEnumerable<object[]> TryGetDataSetFromTestDataCollection(Type declaringType, string propertyName, TestDataVariations variations)
+ {
+ object propertyValue = GetTestDataPropertyValue(declaringType, propertyName);
+
+ IEnumerable<TestData> testDataCollection = propertyValue as IEnumerable<TestData>;
+
+ return testDataCollection == null ? null : GetDataSetFromTestDataCollection(testDataCollection, variations);
+ }
+
+ private static IEnumerable<object[]> GetDataSetFromTestDataCollection(IEnumerable<TestData> testDataCollection, TestDataVariations variations)
+ {
+ foreach (TestData testdataInstance in testDataCollection)
+ {
+ foreach (TestDataVariations variation in testdataInstance.GetSupportedTestDataVariations())
+ {
+ if ((variation & variations) == variation)
+ {
+ Type variationType = testdataInstance.GetAsTypeOrNull(variation);
+ object testData = testdataInstance.GetAsTestDataOrNull(variation);
+ if (AsSingleInstances(variation))
+ {
+ foreach (object obj in (IEnumerable)testData)
+ {
+ yield return new object[] { variationType, obj };
+ }
+ }
+ else
+ {
+ yield return new object[] { variationType, testData };
+ }
+ }
+ }
+ }
+ }
+
+ private static bool AsSingleInstances(TestDataVariations variation)
+ {
+ return variation == TestDataVariations.AsInstance ||
+ variation == TestDataVariations.AsNullable ||
+ variation == TestDataVariations.AsDerivedType ||
+ variation == TestDataVariations.AsKnownType ||
+ variation == TestDataVariations.AsDataMember ||
+ variation == TestDataVariations.AsXmlElementProperty;
+ }
+ }
+}
diff --git a/test/Microsoft.TestCommon/Microsoft/TestCommon/TimeoutConstant.cs b/test/Microsoft.TestCommon/Microsoft/TestCommon/TimeoutConstant.cs
new file mode 100644
index 00000000..a1ad512b
--- /dev/null
+++ b/test/Microsoft.TestCommon/Microsoft/TestCommon/TimeoutConstant.cs
@@ -0,0 +1,20 @@
+namespace Microsoft.TestCommon
+{
+ /// <summary>
+ /// MSTest timeout constants for use with the <see cref="Microsoft.VisualStudio.TestTools.UnitTesting.TimeoutAttribute"/>.
+ /// </summary>
+ public class TimeoutConstant
+ {
+ private const int seconds = 1000;
+
+ /// <summary>
+ /// The default timeout for test methods.
+ /// </summary>
+ public const int DefaultTimeout = 30 * seconds;
+
+ /// <summary>
+ /// An extendedn timeout for longer running test methods.
+ /// </summary>
+ public const int ExtendedTimeout = 240 * seconds;
+ }
+}
diff --git a/test/Microsoft.TestCommon/Microsoft/TestCommon/TypeAssert.cs b/test/Microsoft.TestCommon/Microsoft/TestCommon/TypeAssert.cs
new file mode 100644
index 00000000..9c869849
--- /dev/null
+++ b/test/Microsoft.TestCommon/Microsoft/TestCommon/TypeAssert.cs
@@ -0,0 +1,162 @@
+using System;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace Microsoft.TestCommon
+{
+ /// <summary>
+ /// MSTest utility for testing that a given type has the expected properties such as being public, sealed, etc.
+ /// </summary>
+ public class TypeAssert
+ {
+ /// <summary>
+ /// Specifies a set of type properties to test for using the <see cref="CheckProperty"/> method.
+ /// This enumeration has a <see cref="FlagsAttribute"/> attribute that allows a bitwise combination of its member values.
+ /// </summary>
+ [Flags]
+ public enum TypeProperties
+ {
+ /// <summary>
+ /// Indicates that the type must be abstract.
+ /// </summary>
+ IsAbstract = 0x1,
+
+ /// <summary>
+ /// Indicates that the type must be a class.
+ /// </summary>
+ IsClass = 0x2,
+
+ /// <summary>
+ /// Indicates that the type must be a COM object.
+ /// </summary>
+ IsComObject = 0x4,
+
+ /// <summary>
+ /// Indicates that the type must be disposable.
+ /// </summary>
+ IsDisposable = 0x8,
+
+ /// <summary>
+ /// Indicates that the type must be an enum.
+ /// </summary>
+ IsEnum = 0x10,
+
+ /// <summary>
+ /// Indicates that the type must be a generic type.
+ /// </summary>
+ IsGenericType = 0x20,
+
+ /// <summary>
+ /// Indicates that the type must be a generic type definition.
+ /// </summary>
+ IsGenericTypeDefinition = 0x40,
+
+ /// <summary>
+ /// Indicates that the type must be an interface.
+ /// </summary>
+ IsInterface = 0x80,
+
+ /// <summary>
+ /// Indicates that the type must be nested and declared private.
+ /// </summary>
+ IsNestedPrivate = 0x100,
+
+ /// <summary>
+ /// Indicates that the type must be nested and declared public.
+ /// </summary>
+ IsNestedPublic = 0x200,
+
+ /// <summary>
+ /// Indicates that the type must be public.
+ /// </summary>
+ IsPublic = 0x400,
+
+ /// <summary>
+ /// Indicates that the type must be sealed.
+ /// </summary>
+ IsSealed = 0x800,
+
+ /// <summary>
+ /// Indicates that the type must be visible outside the assembly.
+ /// </summary>
+ IsVisible = 0x1000,
+
+ /// <summary>
+ /// Indicates that the type must be static.
+ /// </summary>
+ IsStatic = TypeAssert.TypeProperties.IsAbstract | TypeAssert.TypeProperties.IsSealed,
+
+ /// <summary>
+ /// Indicates that the type must be a public, visible class.
+ /// </summary>
+ IsPublicVisibleClass = TypeAssert.TypeProperties.IsClass | TypeAssert.TypeProperties.IsPublic | TypeAssert.TypeProperties.IsVisible
+ }
+
+ private static void CheckProperty(Type type, bool expected, bool actual, string property)
+ {
+ Assert.NotNull(type);
+ Assert.True(expected == actual, String.Format("Type '{0}' should{1} be {2}.", type.FullName, expected ? "" : " NOT", property));
+ }
+
+ /// <summary>
+ /// Determines whether the specified type has a given set of properties such as being public, sealed, etc.
+ /// The method asserts if one or more of the properties are not satisfied.
+ /// </summary>
+ /// <typeparam name="T">The type to test for properties.</typeparam>
+ /// <param name="typeProperties">The set of type properties to test for.</param>
+ public void HasProperties<T>(TypeProperties typeProperties)
+ {
+ HasProperties(typeof(T), typeProperties);
+ }
+
+ /// <summary>
+ /// Determines whether the specified type has a given set of properties such as being public, sealed, etc.
+ /// The method asserts if one or more of the properties are not satisfied.
+ /// </summary>
+ /// <typeparam name="T">The type to test for properties.</typeparam>
+ /// <typeparam name="TIsAssignableFrom">Verify that the type to test is assignable from this type.</typeparam>
+ /// <param name="typeProperties">The set of type properties to test for.</param>
+ public void HasProperties<T, TIsAssignableFrom>(TypeProperties typeProperties)
+ {
+ HasProperties(typeof(T), typeProperties, typeof(TIsAssignableFrom));
+ }
+
+ /// <summary>
+ /// Determines whether the specified type has a given set of properties such as being public, sealed, etc.
+ /// The method asserts if one or more of the properties are not satisfied.
+ /// </summary>
+ /// <param name="type">The type to test for properties.</param>
+ /// <param name="typeProperties">The set of type properties to test for.</param>
+ public void HasProperties(Type type, TypeProperties typeProperties)
+ {
+ HasProperties(type, typeProperties, null);
+ }
+
+ /// <summary>
+ /// Determines whether the specified type has a given set of properties such as being public, sealed, etc.
+ /// The method asserts if one or more of the properties are not satisfied.
+ /// </summary>
+ /// <param name="type">The type to test for properties.</param>
+ /// <param name="typeProperties">The set of type properties to test for.</param>
+ /// <param name="isAssignableFrom">Verify that the type to test is assignable from this type.</param>
+ public void HasProperties(Type type, TypeProperties typeProperties, Type isAssignableFrom)
+ {
+ TypeAssert.CheckProperty(type, (typeProperties & TypeProperties.IsAbstract) > 0, type.IsAbstract, "abstract");
+ TypeAssert.CheckProperty(type, (typeProperties & TypeProperties.IsClass) > 0, type.IsClass, "a class");
+ TypeAssert.CheckProperty(type, (typeProperties & TypeProperties.IsComObject) > 0, type.IsCOMObject, "a COM object");
+ TypeAssert.CheckProperty(type, (typeProperties & TypeProperties.IsDisposable) > 0, typeof(IDisposable).IsAssignableFrom(type), "disposable");
+ TypeAssert.CheckProperty(type, (typeProperties & TypeProperties.IsEnum) > 0, type.IsEnum, "an enum");
+ TypeAssert.CheckProperty(type, (typeProperties & TypeProperties.IsGenericType) > 0, type.IsGenericType, "a generic type");
+ TypeAssert.CheckProperty(type, (typeProperties & TypeProperties.IsGenericTypeDefinition) > 0, type.IsGenericTypeDefinition, "a generic type definition");
+ TypeAssert.CheckProperty(type, (typeProperties & TypeProperties.IsInterface) > 0, type.IsInterface, "an interface");
+ TypeAssert.CheckProperty(type, (typeProperties & TypeProperties.IsNestedPrivate) > 0, type.IsNestedPrivate, "nested private");
+ TypeAssert.CheckProperty(type, (typeProperties & TypeProperties.IsNestedPublic) > 0, type.IsNestedPublic, "nested public");
+ TypeAssert.CheckProperty(type, (typeProperties & TypeProperties.IsPublic) > 0, type.IsPublic, "public");
+ TypeAssert.CheckProperty(type, (typeProperties & TypeProperties.IsSealed) > 0, type.IsSealed, "sealed");
+ TypeAssert.CheckProperty(type, (typeProperties & TypeProperties.IsVisible) > 0, type.IsVisible, "visible");
+ if (isAssignableFrom != null)
+ {
+ TypeAssert.CheckProperty(type, true, isAssignableFrom.IsAssignableFrom(type), String.Format("assignable from {0}", isAssignableFrom.FullName));
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/test/Microsoft.TestCommon/Microsoft/TestCommon/Types/FlagsEnum.cs b/test/Microsoft.TestCommon/Microsoft/TestCommon/Types/FlagsEnum.cs
new file mode 100644
index 00000000..b2e7548d
--- /dev/null
+++ b/test/Microsoft.TestCommon/Microsoft/TestCommon/Types/FlagsEnum.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace Microsoft.TestCommon.Types
+{
+ [Flags]
+ public enum FlagsEnum
+ {
+ One = 0x1,
+ Two = 0x2,
+ Four = 0x4
+ }
+}
diff --git a/test/Microsoft.TestCommon/Microsoft/TestCommon/Types/INameAndIdContainer.cs b/test/Microsoft.TestCommon/Microsoft/TestCommon/Types/INameAndIdContainer.cs
new file mode 100644
index 00000000..708daa78
--- /dev/null
+++ b/test/Microsoft.TestCommon/Microsoft/TestCommon/Types/INameAndIdContainer.cs
@@ -0,0 +1,12 @@
+namespace Microsoft.TestCommon.Types
+{
+ /// <summary>
+ /// Tagging interface to assist comparing instances of these types.
+ /// </summary>
+ public interface INameAndIdContainer
+ {
+ string Name { get; set; }
+
+ int Id { get; set; }
+ }
+}
diff --git a/test/Microsoft.TestCommon/Microsoft/TestCommon/Types/ISerializableType.cs b/test/Microsoft.TestCommon/Microsoft/TestCommon/Types/ISerializableType.cs
new file mode 100644
index 00000000..e749ed94
--- /dev/null
+++ b/test/Microsoft.TestCommon/Microsoft/TestCommon/Types/ISerializableType.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.Serialization;
+
+namespace Microsoft.TestCommon.Types
+{
+ [Serializable]
+ public class ISerializableType : ISerializable, INameAndIdContainer
+ {
+ private int id;
+ private string name;
+
+ public ISerializableType()
+ {
+ }
+
+ public ISerializableType(int id, string name)
+ {
+ this.id = id;
+ this.name = name;
+ }
+
+ public ISerializableType(SerializationInfo information, StreamingContext context)
+ {
+ this.id = information.GetInt32("Id");
+ this.name = information.GetString("Name");
+ }
+
+ public int Id
+ {
+ get
+ {
+ return this.id;
+ }
+
+ set
+ {
+ this.IdSet = true;
+ this.id = value;
+ }
+ }
+
+ public string Name
+ {
+ get
+ {
+ return this.name;
+ }
+
+ set
+ {
+ this.NameSet = true;
+ this.name = value;
+ }
+
+ }
+
+ public bool IdSet { get; private set; }
+
+ public bool NameSet { get; private set; }
+
+ public void GetObjectData(SerializationInfo info, StreamingContext context)
+ {
+ info.AddValue("Id", this.Id);
+ info.AddValue("Name", this.Name);
+ }
+
+ public static IEnumerable<ISerializableType> GetTestData()
+ {
+ return new ISerializableType[] { new ISerializableType(), new ISerializableType(1, "SomeName") };
+ }
+ }
+}
diff --git a/test/Microsoft.TestCommon/Microsoft/TestCommon/Types/LongEnum.cs b/test/Microsoft.TestCommon/Microsoft/TestCommon/Types/LongEnum.cs
new file mode 100644
index 00000000..df94d84e
--- /dev/null
+++ b/test/Microsoft.TestCommon/Microsoft/TestCommon/Types/LongEnum.cs
@@ -0,0 +1,9 @@
+namespace Microsoft.TestCommon.Types
+{
+ public enum LongEnum : long
+ {
+ FirstLong,
+ SecondLong,
+ ThirdLong
+ }
+}
diff --git a/test/Microsoft.TestCommon/Microsoft/TestCommon/Types/SimpleEnum.cs b/test/Microsoft.TestCommon/Microsoft/TestCommon/Types/SimpleEnum.cs
new file mode 100644
index 00000000..8bff4843
--- /dev/null
+++ b/test/Microsoft.TestCommon/Microsoft/TestCommon/Types/SimpleEnum.cs
@@ -0,0 +1,9 @@
+namespace Microsoft.TestCommon.Types
+{
+ public enum SimpleEnum
+ {
+ First,
+ Second,
+ Third
+ }
+}
diff --git a/test/Microsoft.TestCommon/Microsoft/TestCommon/XmlAssert.cs b/test/Microsoft.TestCommon/Microsoft/TestCommon/XmlAssert.cs
new file mode 100644
index 00000000..52d8fe9c
--- /dev/null
+++ b/test/Microsoft.TestCommon/Microsoft/TestCommon/XmlAssert.cs
@@ -0,0 +1,77 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Web;
+using System.Xml.Linq;
+using Xunit;
+
+namespace Microsoft.TestCommon
+{
+ /// <summary>
+ /// Assert class that compares two XML strings for equality. Namespaces are ignored during comparison
+ /// </summary>
+ public class XmlAssert
+ {
+ public void Equal(string expected, string actual, params RegexReplacement[] regexReplacements)
+ {
+ if (regexReplacements != null)
+ {
+ for (int i = 0; i < regexReplacements.Length; i++)
+ {
+ actual = regexReplacements[i].Regex.Replace(actual, regexReplacements[i].Replacement);
+ }
+ }
+
+ Equal(XElement.Parse(expected), XElement.Parse(actual));
+ }
+
+ public void Equal(XElement expected, XElement actual)
+ {
+ Assert.Equal(Normalize(expected).ToString(), Normalize(actual).ToString());
+ }
+
+ private static XElement Normalize(XElement element)
+ {
+ if (element.HasElements)
+ {
+ return new XElement(
+ Encode(element.Name),
+ Normalize(element.Attributes()),
+ Normalize(element.Elements()));
+ }
+
+ if (element.IsEmpty)
+ {
+ return new XElement(
+ Encode(element.Name),
+ Normalize(element.Attributes()));
+ }
+ else
+ {
+ return new XElement(
+ Encode(element.Name),
+ Normalize(element.Attributes()),
+ element.Value);
+ }
+ }
+
+ private static IEnumerable<XAttribute> Normalize(IEnumerable<XAttribute> attributes)
+ {
+ return attributes
+ .Where((attrib) => !attrib.IsNamespaceDeclaration)
+ .Select((attrib) => new XAttribute(Encode(attrib.Name), attrib.Value))
+ .OrderBy(a => a.Name.ToString());
+ }
+
+ private static IEnumerable<XElement> Normalize(IEnumerable<XElement> elements)
+ {
+ return elements
+ .Select(e => Normalize(e))
+ .OrderBy(a => a.ToString());
+ }
+
+ private static string Encode(XName name)
+ {
+ return string.Format("{0}_{1}", HttpUtility.UrlEncode(name.NamespaceName).Replace('%', '_'), name.LocalName);
+ }
+ }
+} \ No newline at end of file
diff --git a/test/Microsoft.TestCommon/PreAppStartTestHelper.cs b/test/Microsoft.TestCommon/PreAppStartTestHelper.cs
new file mode 100644
index 00000000..79f90320
--- /dev/null
+++ b/test/Microsoft.TestCommon/PreAppStartTestHelper.cs
@@ -0,0 +1,25 @@
+using System.ComponentModel;
+using System.Reflection;
+using Xunit;
+
+namespace System.Web.WebPages.TestUtils
+{
+ public static class PreAppStartTestHelper
+ {
+ public static void TestPreAppStartClass(Type preAppStartType)
+ {
+ string typeMessage = String.Format("The type '{0}' must be static, public, and named 'PreApplicationStartCode'.", preAppStartType.FullName);
+ Assert.True(preAppStartType.IsSealed && preAppStartType.IsAbstract && preAppStartType.IsPublic && preAppStartType.Name == "PreApplicationStartCode", typeMessage);
+
+ string editorBrowsableMessage = String.Format("The only attribute on type '{0}' must be [EditorBrowsable(EditorBrowsableState.Never)].", preAppStartType.FullName);
+ object[] attrs = preAppStartType.GetCustomAttributes(typeof(EditorBrowsableAttribute), true);
+ Assert.True(attrs.Length == 1 && ((EditorBrowsableAttribute)attrs[0]).State == EditorBrowsableState.Never, editorBrowsableMessage);
+
+ string startMethodMessage = String.Format("The only public member on type '{0}' must be a method called Start().", preAppStartType.FullName);
+ MemberInfo[] publicMembers = preAppStartType.GetMembers(BindingFlags.Public | BindingFlags.Static);
+ Assert.True(publicMembers.Length == 1, startMethodMessage);
+ Assert.True(publicMembers[0].MemberType == MemberTypes.Method, startMethodMessage);
+ Assert.True(publicMembers[0].Name == "Start", startMethodMessage);
+ }
+ }
+}
diff --git a/test/Microsoft.TestCommon/PreserveSyncContextAttribute.cs b/test/Microsoft.TestCommon/PreserveSyncContextAttribute.cs
new file mode 100644
index 00000000..f47e9b62
--- /dev/null
+++ b/test/Microsoft.TestCommon/PreserveSyncContextAttribute.cs
@@ -0,0 +1,24 @@
+using System.Threading;
+using Xunit;
+
+namespace Microsoft.TestCommon
+{
+ /// <summary>
+ /// Preserves the current <see cref="SynchronizationContext"/>. Use this attribute on
+ /// tests which modify the current <see cref="SynchronizationContext"/>.
+ /// </summary>
+ public class PreserveSyncContextAttribute : BeforeAfterTestAttribute
+ {
+ private SynchronizationContext _syncContext;
+
+ public override void Before(System.Reflection.MethodInfo methodUnderTest)
+ {
+ _syncContext = SynchronizationContext.Current;
+ }
+
+ public override void After(System.Reflection.MethodInfo methodUnderTest)
+ {
+ SynchronizationContext.SetSynchronizationContext(_syncContext);
+ }
+ }
+}
diff --git a/test/Microsoft.TestCommon/Properties/AssemblyInfo.cs b/test/Microsoft.TestCommon/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..46cecb32
--- /dev/null
+++ b/test/Microsoft.TestCommon/Properties/AssemblyInfo.cs
@@ -0,0 +1,35 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("Microsoft.TestCommon")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("Microsoft")]
+[assembly: AssemblyProduct("Microsoft.TestCommon")]
+[assembly: AssemblyCopyright("Copyright © Microsoft 2011")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("d128ebbb-a536-472f-8f8a-0fcb0966624e")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/test/Microsoft.TestCommon/ReflectionAssert.cs b/test/Microsoft.TestCommon/ReflectionAssert.cs
new file mode 100644
index 00000000..a2b3eb59
--- /dev/null
+++ b/test/Microsoft.TestCommon/ReflectionAssert.cs
@@ -0,0 +1,115 @@
+using System;
+using System.Linq.Expressions;
+using System.Reflection;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace Microsoft.TestCommon
+{
+ public class ReflectionAssert
+ {
+ private static PropertyInfo GetPropertyInfo<T, TProp>(Expression<Func<T, TProp>> property)
+ {
+ if (property.Body is MemberExpression)
+ {
+ return (PropertyInfo)((MemberExpression)property.Body).Member;
+ }
+ else if (property.Body is UnaryExpression && property.Body.NodeType == ExpressionType.Convert)
+ {
+ return (PropertyInfo)((MemberExpression)((UnaryExpression)property.Body).Operand).Member;
+ }
+ else
+ {
+ throw new InvalidOperationException("Could not determine property from lambda expression.");
+ }
+ }
+
+ private static void TestPropertyValue<TInstance, TValue>(TInstance instance, Func<TInstance, TValue> getFunc, Action<TInstance, TValue> setFunc, TValue valueToSet, TValue valueToCheck)
+ {
+ setFunc(instance, valueToSet);
+ TValue newValue = getFunc(instance);
+ Assert.Equal(valueToCheck, newValue);
+ }
+
+ private static void TestPropertyValue<TInstance, TValue>(TInstance instance, Func<TInstance, TValue> getFunc, Action<TInstance, TValue> setFunc, TValue value)
+ {
+ TestPropertyValue(instance, getFunc, setFunc, value, value);
+ }
+
+ public void Property<T, TResult>(T instance, Expression<Func<T, TResult>> propertyGetter, TResult expectedDefaultValue, bool allowNull = false, TResult roundTripTestValue = null) where TResult : class
+ {
+ PropertyInfo property = GetPropertyInfo(propertyGetter);
+ Func<T, TResult> getFunc = (obj) => (TResult)property.GetValue(obj, index: null);
+ Action<T, TResult> setFunc = (obj, value) => property.SetValue(obj, value, index: null);
+
+ Assert.Equal(expectedDefaultValue, getFunc(instance));
+
+ if (allowNull)
+ {
+ TestPropertyValue(instance, getFunc, setFunc, null);
+ }
+ else
+ {
+ Assert.ThrowsArgumentNull(() =>
+ {
+ setFunc(instance, null);
+ }, "value");
+ }
+
+ if (roundTripTestValue != null)
+ {
+ TestPropertyValue(instance, getFunc, setFunc, roundTripTestValue);
+ }
+ }
+
+ public void StringProperty<T>(T instance, Expression<Func<T, string>> propertyGetter, string expectedDefaultValue,
+ bool allowNullAndEmpty = true, string nullAndEmptyReturnValue = "")
+ {
+ PropertyInfo property = GetPropertyInfo(propertyGetter);
+ Func<T, string> getFunc = (obj) => (string)property.GetValue(obj, index: null);
+ Action<T, string> setFunc = (obj, value) => property.SetValue(obj, value, index: null);
+
+ Assert.Equal(expectedDefaultValue, getFunc(instance));
+
+ if (allowNullAndEmpty)
+ {
+ // Assert get/set works for null
+ TestPropertyValue(instance, getFunc, setFunc, null, nullAndEmptyReturnValue);
+
+ // Assert get/set works for String.Empty
+ TestPropertyValue(instance, getFunc, setFunc, String.Empty, nullAndEmptyReturnValue);
+ }
+ else
+ {
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate()
+ {
+ try
+ {
+ TestPropertyValue(instance, getFunc, setFunc, null);
+ }
+ catch (TargetInvocationException e)
+ {
+ throw e.InnerException;
+ }
+ },
+ "value");
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate()
+ {
+ try
+ {
+ TestPropertyValue(instance, getFunc, setFunc, String.Empty);
+ }
+ catch (TargetInvocationException e)
+ {
+ throw e.InnerException;
+ }
+ },
+ "value");
+ }
+
+ // Assert get/set works for arbitrary value
+ TestPropertyValue(instance, getFunc, setFunc, "TestValue");
+ }
+ }
+}
diff --git a/test/Microsoft.TestCommon/TaskExtensions.cs b/test/Microsoft.TestCommon/TaskExtensions.cs
new file mode 100644
index 00000000..b0cf4aa9
--- /dev/null
+++ b/test/Microsoft.TestCommon/TaskExtensions.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Threading.Tasks;
+
+// No namespace so that these extensions are available for all test classes
+
+public static class TaskExtensions
+{
+ /// <summary>
+ /// Waits until the given task finishes executing and completes in any of the 3 states.
+ /// </summary>
+ /// <param name="task">A task</param>
+ public static void WaitUntilCompleted(this Task task)
+ {
+ if (task == null) throw new ArgumentNullException("task");
+ task.ContinueWith(prev =>
+ {
+ if (prev.IsFaulted)
+ {
+ // Observe the exception in the faulted case to avoid an unobserved exception leaking and
+ // killing the thread finalizer.
+ var e = prev.Exception;
+ }
+ }, TaskContinuationOptions.ExecuteSynchronously).Wait();
+ }
+}
diff --git a/test/Microsoft.TestCommon/TestFile.cs b/test/Microsoft.TestCommon/TestFile.cs
new file mode 100644
index 00000000..b77236db
--- /dev/null
+++ b/test/Microsoft.TestCommon/TestFile.cs
@@ -0,0 +1,73 @@
+using System.IO;
+using System.Reflection;
+using Xunit;
+
+namespace System.Web.WebPages.TestUtils
+{
+ public class TestFile
+ {
+ public const string ResourceNameFormat = "{0}.TestFiles.{1}";
+
+ public string ResourceName { get; set; }
+ public Assembly Assembly { get; set; }
+
+ public TestFile(string resName, Assembly asm)
+ {
+ ResourceName = resName;
+ Assembly = asm;
+ }
+
+ public static TestFile Create(string localResourceName)
+ {
+ return new TestFile(String.Format(ResourceNameFormat, Assembly.GetCallingAssembly().GetName().Name, localResourceName), Assembly.GetCallingAssembly());
+ }
+
+ public Stream OpenRead()
+ {
+ Stream strm = Assembly.GetManifestResourceStream(ResourceName);
+ if (strm == null)
+ {
+ Assert.True(false, String.Format("Manifest resource: {0} not found", ResourceName));
+ }
+ return strm;
+ }
+
+ public byte[] ReadAllBytes()
+ {
+ using (Stream stream = OpenRead())
+ {
+ byte[] buffer = new byte[stream.Length];
+ stream.Read(buffer, 0, buffer.Length);
+ return buffer;
+ }
+ }
+
+ public string ReadAllText()
+ {
+ using (StreamReader reader = new StreamReader(OpenRead()))
+ {
+ return reader.ReadToEnd();
+ }
+ }
+
+ /// <summary>
+ /// Saves the file to the specified path.
+ /// </summary>
+ public void Save(string filePath)
+ {
+ var directory = Path.GetDirectoryName(filePath);
+ if (!Directory.Exists(directory))
+ {
+ Directory.CreateDirectory(directory);
+ }
+
+ using (Stream outStream = File.Create(filePath))
+ {
+ using (Stream inStream = OpenRead())
+ {
+ inStream.CopyTo(outStream);
+ }
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.TestCommon/TestHelper.cs b/test/Microsoft.TestCommon/TestHelper.cs
new file mode 100644
index 00000000..7a59d370
--- /dev/null
+++ b/test/Microsoft.TestCommon/TestHelper.cs
@@ -0,0 +1,29 @@
+using System.Globalization;
+using System.Linq;
+using Xunit;
+
+namespace System.Web.TestUtil
+{
+ public static class UnitTestHelper
+ {
+ public static bool EnglishBuildAndOS
+ {
+ get
+ {
+ bool englishBuild = String.Equals(CultureInfo.CurrentCulture.TwoLetterISOLanguageName, "en",
+ StringComparison.OrdinalIgnoreCase);
+ bool englishOS = String.Equals(CultureInfo.CurrentCulture.TwoLetterISOLanguageName, "en",
+ StringComparison.OrdinalIgnoreCase);
+ return englishBuild && englishOS;
+ }
+ }
+
+ public static void AssertEqualsIgnoreWhitespace(string expected, string actual)
+ {
+ expected = new String(expected.Where(c => !Char.IsWhiteSpace(c)).ToArray());
+ actual = new String(actual.Where(c => !Char.IsWhiteSpace(c)).ToArray());
+
+ Assert.Equal(expected, actual, StringComparer.OrdinalIgnoreCase);
+ }
+ }
+}
diff --git a/test/Microsoft.TestCommon/TheoryDataSet.cs b/test/Microsoft.TestCommon/TheoryDataSet.cs
new file mode 100644
index 00000000..2fa4b34a
--- /dev/null
+++ b/test/Microsoft.TestCommon/TheoryDataSet.cs
@@ -0,0 +1,86 @@
+using System.Collections;
+using System.Collections.Generic;
+
+namespace Microsoft.TestCommon
+{
+ /// <summary>
+ /// Helper class for generating test data for XUnit's <see cref="Xunit.Extensions.TheoryAttribute"/>-based tests.
+ /// Should be used in combination with <see cref="Xunit.Extensions.PropertyDataAttribute"/>.
+ /// </summary>
+ /// <typeparam name="TParam">First parameter type</typeparam>
+ public class TheoryDataSet<TParam> : TheoryDataSet
+ {
+ public void Add(TParam p)
+ {
+ AddItem(p);
+ }
+ }
+
+ /// <summary>
+ /// Helper class for generating test data for XUnit's <see cref="Xunit.Extensions.TheoryAttribute"/>-based tests.
+ /// Should be used in combination with <see cref="Xunit.Extensions.PropertyDataAttribute"/>.
+ /// </summary>
+ /// <typeparam name="TParam1">First parameter type</typeparam>
+ /// <typeparam name="TParam2">Second parameter type</typeparam>
+ public class TheoryDataSet<TParam1, TParam2> : TheoryDataSet
+ {
+ public void Add(TParam1 p1, TParam2 p2)
+ {
+ AddItem(p1, p2);
+ }
+ }
+
+ /// <summary>
+ /// Helper class for generating test data for XUnit's <see cref="Xunit.Extensions.TheoryAttribute"/>-based tests.
+ /// Should be used in combination with <see cref="Xunit.Extensions.PropertyDataAttribute"/>.
+ /// </summary>
+ /// <typeparam name="TParam1">First parameter type</typeparam>
+ /// <typeparam name="TParam2">Second parameter type</typeparam>
+ /// <typeparam name="TParam3">Third parameter type</typeparam>
+ public class TheoryDataSet<TParam1, TParam2, TParam3> : TheoryDataSet
+ {
+ public void Add(TParam1 p1, TParam2 p2, TParam3 p3)
+ {
+ AddItem(p1, p2, p3);
+ }
+ }
+
+ /// <summary>
+ /// Helper class for generating test data for XUnit's <see cref="Xunit.Extensions.TheoryAttribute"/>-based tests.
+ /// Should be used in combination with <see cref="Xunit.Extensions.PropertyDataAttribute"/>.
+ /// </summary>
+ /// <typeparam name="TParam1">First parameter type</typeparam>
+ /// <typeparam name="TParam2">Second parameter type</typeparam>
+ /// <typeparam name="TParam3">Third parameter type</typeparam>
+ /// <typeparam name="TParam4">Fourth parameter type</typeparam>
+ public class TheoryDataSet<TParam1, TParam2, TParam3, TParam4> : TheoryDataSet
+ {
+ public void Add(TParam1 p1, TParam2 p2, TParam3 p3, TParam4 p4)
+ {
+ AddItem(p1, p2, p3, p4);
+ }
+ }
+
+ /// <summary>
+ /// Base class for <c>TheoryDataSet</c> classes.
+ /// </summary>
+ public abstract class TheoryDataSet : IEnumerable<object[]>
+ {
+ private readonly List<object[]> data = new List<object[]>();
+
+ protected void AddItem(params object[] values)
+ {
+ data.Add(values);
+ }
+
+ public IEnumerator<object[]> GetEnumerator()
+ {
+ return data.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+ }
+}
diff --git a/test/Microsoft.TestCommon/ThreadPoolSyncContext.cs b/test/Microsoft.TestCommon/ThreadPoolSyncContext.cs
new file mode 100644
index 00000000..63552323
--- /dev/null
+++ b/test/Microsoft.TestCommon/ThreadPoolSyncContext.cs
@@ -0,0 +1,31 @@
+using System.Threading;
+
+namespace Microsoft.TestCommon
+{
+ /// <summary>
+ /// This is an implementation of SynchronizationContext that not only queues things on the thread pool for
+ /// later work, but also ensures that it sets itself back as the synchronization context (something that the
+ /// default implementatation of SynchronizationContext does not do).
+ /// </summary>
+ public class ThreadPoolSyncContext : SynchronizationContext
+ {
+ public override void Post(SendOrPostCallback d, object state)
+ {
+ ThreadPool.QueueUserWorkItem(_ =>
+ {
+ SynchronizationContext oldContext = SynchronizationContext.Current;
+ SynchronizationContext.SetSynchronizationContext(this);
+ d.Invoke(state);
+ SynchronizationContext.SetSynchronizationContext(oldContext);
+ }, null);
+ }
+
+ public override void Send(SendOrPostCallback d, object state)
+ {
+ SynchronizationContext oldContext = SynchronizationContext.Current;
+ SynchronizationContext.SetSynchronizationContext(this);
+ d.Invoke(state);
+ SynchronizationContext.SetSynchronizationContext(oldContext);
+ }
+ }
+}
diff --git a/test/Microsoft.TestCommon/WebUtils.cs b/test/Microsoft.TestCommon/WebUtils.cs
new file mode 100644
index 00000000..070fe219
--- /dev/null
+++ b/test/Microsoft.TestCommon/WebUtils.cs
@@ -0,0 +1,87 @@
+using System.IO;
+using System.Reflection;
+using System.Web.UI;
+
+namespace System.Web.WebPages.TestUtils
+{
+ public static class WebUtils
+ {
+ /// <summary>
+ /// Creates an instance of HttpRuntime and assigns it (using magic) to the singleton instance of HttpRuntime.
+ /// Ensure that the returned value is disposed at the end of the test.
+ /// </summary>
+ /// <returns>Returns an IDisposable that restores the original HttpRuntime.</returns>
+ public static IDisposable CreateHttpRuntime(string appVPath, string appPath = null)
+ {
+ var runtime = new HttpRuntime();
+ var appDomainAppVPathField = typeof(HttpRuntime).GetField("_appDomainAppVPath", BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance);
+ appDomainAppVPathField.SetValue(runtime, CreateVirtualPath(appVPath));
+
+ if (appPath != null)
+ {
+ var appDomainAppPathField = typeof(HttpRuntime).GetField("_appDomainAppPath", BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance);
+ appDomainAppPathField.SetValue(runtime, Path.GetFullPath(appPath));
+ }
+
+ GetTheRuntime().SetValue(null, runtime);
+ var appDomainIdField = typeof(HttpRuntime).GetField("_appDomainId", BindingFlags.NonPublic | BindingFlags.Instance);
+ appDomainIdField.SetValue(runtime, "test");
+
+ return new DisposableAction(RestoreHttpRuntime);
+ }
+
+ internal static FieldInfo GetTheRuntime()
+ {
+ return typeof(HttpRuntime).GetField("_theRuntime", BindingFlags.NonPublic | BindingFlags.Static);
+ }
+
+ internal static void RestoreHttpRuntime()
+ {
+ GetTheRuntime().SetValue(null, null);
+ }
+
+ internal static object CreateVirtualPath(string path)
+ {
+ var vPath = typeof(Page).Assembly.GetType("System.Web.VirtualPath");
+ var method = vPath.GetMethod("CreateNonRelativeTrailingSlash", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
+ return method.Invoke(null, new object[] { path });
+ }
+
+ private class DisposableAction : IDisposable
+ {
+ private Action _action;
+ private bool _hasDisposed;
+
+ public DisposableAction(Action action)
+ {
+ if (action == null)
+ {
+ throw new ArgumentNullException("action");
+ }
+ _action = action;
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ // If we were disposed by the finalizer it's because the user didn't use a "using" block, so don't do anything!
+ if (disposing)
+ {
+ lock (this)
+ {
+ if (!_hasDisposed)
+ {
+ _hasDisposed = true;
+ _action();
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.TestCommon/packages.config b/test/Microsoft.TestCommon/packages.config
new file mode 100644
index 00000000..684e330f
--- /dev/null
+++ b/test/Microsoft.TestCommon/packages.config
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="Microsoft.Net.Http" version="2.0.20302.1" />
+ <package id="xunit" version="1.9.0.1566" />
+ <package id="xunit.extensions" version="1.9.0.1566" />
+</packages> \ No newline at end of file
diff --git a/test/Microsoft.Web.Helpers.Test/AnalyticsTest.cs b/test/Microsoft.Web.Helpers.Test/AnalyticsTest.cs
new file mode 100644
index 00000000..92c738fd
--- /dev/null
+++ b/test/Microsoft.Web.Helpers.Test/AnalyticsTest.cs
@@ -0,0 +1,133 @@
+using System.Web.TestUtil;
+using Xunit;
+
+namespace Microsoft.Web.Helpers.Test
+{
+ /// <summary>
+ ///This is a test class for AnalyticsTest and is intended
+ ///to contain all AnalyticsTest Unit Tests
+ ///</summary>
+ public class AnalyticsTest
+ {
+ /// <summary>
+ ///A test for GetYahooAnalyticsHtml
+ ///</summary>
+ [Fact]
+ public void GetYahooAnalyticsHtmlTest()
+ {
+ string account = "My_yahoo_account";
+ string actual = Analytics.GetYahooHtml(account).ToString();
+ Assert.True(actual.Contains(".yahoo.com") && actual.Contains("My_yahoo_account"));
+ }
+
+ /// <summary>
+ ///A test for GetStatCounterAnalyticsHtml
+ ///</summary>
+ [Fact]
+ public void GetStatCounterAnalyticsHtmlTest()
+ {
+ int project = 31553;
+ string security = "stat_security";
+ string actual = Analytics.GetStatCounterHtml(project, security).ToString();
+ Assert.True(actual.Contains("statcounter.com/counter/counter_xhtml.js") &&
+ actual.Contains(project.ToString()) && actual.Contains(security));
+ }
+
+ /// <summary>
+ ///A test for GetGoogleAnalyticsHtml
+ ///</summary>
+ [Fact]
+ public void GetGoogleAnalyticsHtmlTest()
+ {
+ string account = "My_google_account";
+ string actual = Analytics.GetGoogleHtml(account).ToString();
+ Assert.True(actual.Contains("google-analytics.com/ga.js") && actual.Contains("My_google_account"));
+ }
+
+ [Fact]
+ public void GetGoogleAnalyticsEscapesJavascript()
+ {
+ string account = "My_\"google_account";
+ string actual = Analytics.GetGoogleHtml(account).ToString();
+ string expected = "<script type=\"text/javascript\">\n" +
+ "var gaJsHost = ((\"https:\" == document.location.protocol) ? \"https://ssl.\" : \"http://www.\");\n" +
+ "document.write(unescape(\"%3Cscript src='\" + gaJsHost + \"google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E\"));\n" +
+ "</script>\n" +
+ "<script type=\"text/javascript\">\n" +
+ "try{\n" +
+ "var pageTracker = _gat._getTracker(\"My_\\\"google_account\");\n" +
+ "pageTracker._trackPageview();\n" +
+ "} catch(err) {}\n" +
+ "</script>\n";
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(expected, actual);
+ }
+
+ [Fact]
+ public void GetGoogleAnalyticsAsyncHtmlTest()
+ {
+ string account = "My_google_account";
+ string actual = Analytics.GetGoogleAsyncHtml(account).ToString();
+ Assert.True(actual.Contains("google-analytics.com/ga.js") && actual.Contains("My_google_account"));
+ }
+
+ [Fact]
+ public void GetGoogleAnalyticsAsyncHtmlEscapesJavaScript()
+ {
+ string account = "My_\"google_account";
+ string actual = Analytics.GetGoogleAsyncHtml(account).ToString();
+ string expected = "<script type=\"text/javascript\">\n" +
+ "var _gaq = _gaq || [];\n" +
+ "_gaq.push(['_setAccount', 'My_\\\"google_account']);\n" +
+ "_gaq.push(['_trackPageview']);\n" +
+ "(function() {\n" +
+ "var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;\n" +
+ "ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';\n" +
+ "var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);\n" +
+ "})();\n" +
+ "</script>\n";
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(expected, actual);
+ }
+
+ [Fact]
+ public void GetYahooAnalyticsEscapesJavascript()
+ {
+ string account = "My_\"yahoo_account";
+ string actual = Analytics.GetYahooHtml(account).ToString();
+ string expected = "<script type=\"text/javascript\">\n" +
+ "window.ysm_customData = new Object();\n" +
+ "window.ysm_customData.conversion = \"transId=,currency=,amount=\";\n" +
+ "var ysm_accountid = \"My_\\\"yahoo_account\";\n" +
+ "document.write(\"<SCR\" + \"IPT language='JavaScript' type='text/javascript' \"\n" +
+ "+ \"SRC=//\" + \"srv3.wa.marketingsolutions.yahoo.com\" + \"/script/ScriptServlet\" + \"?aid=\" + ysm_accountid\n" +
+ "+ \"></SCR\" + \"IPT>\");\n" +
+ "</script>\n";
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(expected, actual);
+ }
+
+ [Fact]
+ public void GetStatCounterAnalyticsEscapesCorrectly()
+ {
+ string account = "My_\"stat_account";
+ string actual = Analytics.GetStatCounterHtml(2, account).ToString();
+ string expected = "<script type=\"text/javascript\">\n" +
+ "var sc_project=2;\n" +
+ "var sc_invisible=1;\n" +
+ "var sc_security=\"My_\\\"stat_account\";\n" +
+ "var sc_text=2;\n" +
+ "var sc_https=1;\n" +
+ "var scJsHost = ((\"https:\" == document.location.protocol) ? \"https://secure.\" : \"http://www.\");\n" +
+ "document.write(\"<sc\" + \"ript type='text/javascript' src='\" + " +
+ "scJsHost + \"statcounter.com/counter/counter_xhtml.js'></\" + \"script>\");\n" +
+ "</script>\n\n" +
+ "<noscript>" +
+ "<div class=\"statcounter\">" +
+ "<a title=\"tumblrstatistics\" class=\"statcounter\" href=\"http://www.statcounter.com/tumblr/\">" +
+ "<img class=\"statcounter\" src=\"https://c.statcounter.com/2/0/My_&quot;stat_account/1/\" alt=\"tumblr statistics\"/>" +
+ "</a>" +
+ "</div>" +
+ "</noscript>";
+
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(expected, actual);
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Helpers.Test/BingTest.cs b/test/Microsoft.Web.Helpers.Test/BingTest.cs
new file mode 100644
index 00000000..1d2bce96
--- /dev/null
+++ b/test/Microsoft.Web.Helpers.Test/BingTest.cs
@@ -0,0 +1,252 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Text;
+using System.Web;
+using System.Web.Helpers.Test;
+using System.Web.TestUtil;
+using System.Web.WebPages.Scope;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace Microsoft.Web.Helpers.Test
+{
+ public class BingTest
+ {
+ private static readonly IDictionary<object, object> _emptyStateStorage = new Dictionary<object, object>();
+
+ private const string BasicBingSearchTemplate = @"<form action=""http://www.bing.com/search"" class=""BingSearch"" method=""get"" target=""_blank"">"
+ + @"<input name=""FORM"" type=""hidden"" value=""FREESS"" /><input name=""cp"" type=""hidden"" value=""{0}"" />"
+ + @"<table cellpadding=""0"" cellspacing=""0"" style=""width:{1}px;""><tr style=""height: 32px"">"
+ + @"<td style=""width: 100%; border:solid 1px #ccc; border-right-style:none; padding-left:10px; padding-right:10px; vertical-align:middle;"">"
+ + @"<input name=""q"" style=""background-image:url(http://www.bing.com/siteowner/s/siteowner/searchbox_background_k.png); background-position:right; background-repeat:no-repeat; font-family:Arial; font-size:14px; color:#000; width:100%; border:none 0 transparent;"" title=""Search Bing"" type=""text"" />"
+ + @"</td><td style=""border:solid 1px #ccc; border-left-style:none; padding-left:0px; padding-right:3px;"">"
+ + @"<input alt=""Search"" src=""http://www.bing.com/siteowner/s/siteowner/searchbutton_normal_k.gif"" style=""border:none 0 transparent; height:24px; width:24px; vertical-align:top;"" type=""image"" />"
+ + @"</td></tr>";
+
+ private const string BasicBingSearchFooter = "</table></form>";
+
+ private const string BasicBingSearchLocalSiteSearch = @"<tr><td colspan=""2"" style=""font-size: small""><label><input checked=""checked"" name=""q1"" type=""radio"" value=""site:{0}"" />{1}</label>&nbsp;<label><input name=""q1"" type=""radio"" value="""" />Search Web</label></td></tr>";
+
+ [Fact]
+ public void SiteTitleThrowsWhenSetToNull()
+ {
+ Assert.ThrowsArgumentNull(() => Bing.SiteTitle = null, "SiteTitle");
+ }
+
+ [Fact]
+ public void SiteTitleUsesScopeStorage()
+ {
+ // Arrange
+ var value = "value";
+
+ // Act
+ Bing.SiteTitle = value;
+
+ // Assert
+ Assert.Equal(Bing.SiteTitle, value);
+ Assert.Equal(ScopeStorage.CurrentScope[Bing._siteTitleKey], value);
+ }
+
+ [Fact]
+ public void SiteUrlThrowsWhenSetToNull()
+ {
+ Assert.ThrowsArgumentNull(() => Bing.SiteUrl = null, "SiteUrl");
+ }
+
+ [Fact]
+ public void SiteUrlUsesScopeStorage()
+ {
+ // Arrange
+ var value = "value";
+
+ // Act
+ Bing.SiteUrl = value;
+
+ // Assert
+ Assert.Equal(Bing.SiteUrl, value);
+ Assert.Equal(ScopeStorage.CurrentScope[Bing._siteUrlKey], value);
+ }
+
+ [Fact]
+ public void SearchBoxGeneratesValidHtml()
+ {
+ // Act & Assert
+ XhtmlAssert.Validate1_0(
+ Bing._SearchBox("322px", null, null, GetContextForSearchBox(), _emptyStateStorage), true
+ );
+ }
+
+ [Fact]
+ public void SearchBoxDoesNotContainSearchLocalWhenSiteUrlIsNull()
+ {
+ // Arrange
+ var encoding = Encoding.UTF8;
+ var expectedHtml = String.Format(CultureInfo.InvariantCulture, BasicBingSearchTemplate, encoding.CodePage, 322) + BasicBingSearchFooter;
+
+ // Act
+ var actualHtml = Bing._SearchBox("322px", null, null, GetContextForSearchBox(encoding), _emptyStateStorage).ToString();
+
+ // Assert
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(expectedHtml, actualHtml);
+ }
+
+ [Fact]
+ public void SearchBoxDoesNotContainSearchLocalWhenSiteUrlIsEmpty()
+ {
+ // Arrange
+ var encoding = Encoding.UTF8;
+ var expectedHtml = String.Format(CultureInfo.InvariantCulture, BasicBingSearchTemplate, encoding.CodePage, 322) + BasicBingSearchFooter;
+
+ // Act
+ var actualHtml = Bing._SearchBox("322px", String.Empty, String.Empty, GetContextForSearchBox(encoding), _emptyStateStorage).ToString();
+
+ // Assert
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(expectedHtml, actualHtml);
+ }
+
+ [Fact]
+ public void SearchBoxUsesResponseEncodingToDetermineCodePage()
+ {
+ // Arrange
+ var encoding = Encoding.GetEncoding(51932); //euc-jp
+ var expectedHtml = String.Format(CultureInfo.InvariantCulture, BasicBingSearchTemplate, encoding.CodePage, 322) + BasicBingSearchFooter;
+
+ // Act
+ var actualHtml = Bing._SearchBox("322px", String.Empty, String.Empty, GetContextForSearchBox(encoding), _emptyStateStorage).ToString();
+
+ // Assert
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(expectedHtml, actualHtml);
+ }
+
+ [Fact]
+ public void SearchBoxUsesWidthToSetBingSearchTableSize()
+ {
+ // Arrange
+ var encoding = Encoding.UTF8;
+ var expectedHtml = String.Format(CultureInfo.InvariantCulture, BasicBingSearchTemplate, encoding.CodePage, 609) + BasicBingSearchFooter;
+
+ // Act
+ var actualHtml = Bing._SearchBox("609px", String.Empty, String.Empty, GetContextForSearchBox(encoding), _emptyStateStorage).ToString();
+
+ // Assert
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(expectedHtml, actualHtml);
+ }
+
+ [Fact]
+ public void SearchBoxUsesWithSiteUrlProducesLocalSearchOptions()
+ {
+ // Arrange
+ var encoding = Encoding.Default;
+ var expectedHtml = String.Format(CultureInfo.InvariantCulture, BasicBingSearchTemplate, encoding.CodePage, 322) +
+ String.Format(CultureInfo.InvariantCulture, BasicBingSearchLocalSiteSearch, "www.asp.net", "Search Site") + BasicBingSearchFooter;
+
+ // Act
+ var actualHtml = Bing._SearchBox("322px", "www.asp.net", String.Empty, GetContextForSearchBox(encoding), _emptyStateStorage).ToString();
+
+ // Assert
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(expectedHtml, actualHtml);
+ }
+
+ [Fact]
+ public void SearchBoxUsesWithSiteUrlAndSiteTitleProducesLocalSearchOptions()
+ {
+ // Arrange
+ var encoding = Encoding.Default;
+ var expectedHtml = String.Format(CultureInfo.InvariantCulture, BasicBingSearchTemplate, encoding.CodePage, 322) +
+ String.Format(CultureInfo.InvariantCulture, BasicBingSearchLocalSiteSearch, "www.microsoft.com", "Custom Search") + BasicBingSearchFooter;
+
+ // Act
+ var actualHtml = Bing._SearchBox("322px", "www.microsoft.com", "Custom Search", GetContextForSearchBox(encoding), _emptyStateStorage).ToString();
+
+ // Assert
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(expectedHtml, actualHtml);
+ }
+
+ [Fact]
+ public void SearchBoxWithLocalSiteOptionUsesResponseEncoding()
+ {
+ // Arrange
+ var encoding = Encoding.GetEncoding(1258); //windows-1258
+ var expectedHtml = String.Format(CultureInfo.InvariantCulture, BasicBingSearchTemplate, encoding.CodePage, 322) +
+ String.Format(CultureInfo.InvariantCulture, BasicBingSearchLocalSiteSearch, "www.asp.net", "Search Site") + BasicBingSearchFooter;
+
+ // Act
+ var actualHtml = Bing._SearchBox("322px", "www.asp.net", String.Empty, GetContextForSearchBox(encoding), _emptyStateStorage).ToString();
+
+ // Assert
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(expectedHtml, actualHtml);
+ }
+
+ [Fact]
+ public void SearchBoxUsesScopeStorageIfSiteTitleParameterIsNull()
+ {
+ // Arrange
+ var encoding = Encoding.GetEncoding(1258); //windows-1258
+ var expectedHtml = String.Format(CultureInfo.InvariantCulture, BasicBingSearchTemplate, encoding.CodePage, 322) +
+ String.Format(CultureInfo.InvariantCulture, BasicBingSearchLocalSiteSearch, "www.asp.net", "foobar") + BasicBingSearchFooter;
+
+ // Act
+ var actualHtml = Bing._SearchBox("322px", "www.asp.net", null, GetContextForSearchBox(encoding), new Dictionary<object, object> { { Bing._siteTitleKey, "foobar" } }).ToString();
+
+ // Assert
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(expectedHtml, actualHtml);
+ }
+
+ [Fact]
+ public void SearchBoxUsesScopeStorageIfSiteTitleParameterIsEmpty()
+ {
+ // Arrange
+ var encoding = Encoding.GetEncoding(1258); //windows-1258
+ var expectedHtml = String.Format(CultureInfo.InvariantCulture, BasicBingSearchTemplate, encoding.CodePage, 322) +
+ String.Format(CultureInfo.InvariantCulture, BasicBingSearchLocalSiteSearch, "www.asptest.net", "bazbiz") + BasicBingSearchFooter;
+
+ // Act
+ var actualHtml = Bing._SearchBox("322px", "www.asptest.net", String.Empty, GetContextForSearchBox(encoding), new Dictionary<object, object> { { Bing._siteTitleKey, "bazbiz" } }).ToString();
+
+ // Assert
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(expectedHtml, actualHtml);
+ }
+
+ [Fact]
+ public void SearchBoxUsesScopeStorageIfSiteUrlParameterIsNull()
+ {
+ // Arrange
+ var encoding = Encoding.GetEncoding(1258); //windows-1258
+ var expectedHtml = String.Format(CultureInfo.InvariantCulture, BasicBingSearchTemplate, encoding.CodePage, 322) +
+ String.Format(CultureInfo.InvariantCulture, BasicBingSearchLocalSiteSearch, "www.myawesomesite.net", "my-test-string") + BasicBingSearchFooter;
+
+ // Act
+ var actualHtml = Bing._SearchBox("322px", null, "my-test-string", GetContextForSearchBox(encoding), new Dictionary<object, object> { { Bing._siteUrlKey, "www.myawesomesite.net" } }).ToString();
+
+ // Assert
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(expectedHtml, actualHtml);
+ }
+
+ [Fact]
+ public void SearchBoxUsesScopeStorageIfSiteUrlParameterIsEmpty()
+ {
+ // Arrange
+ var encoding = Encoding.GetEncoding(1258); //windows-1258
+ var expectedHtml = String.Format(CultureInfo.InvariantCulture, BasicBingSearchTemplate, encoding.CodePage, 322) +
+ String.Format(CultureInfo.InvariantCulture, BasicBingSearchLocalSiteSearch, "www.myawesomesite.net", "my-test-string") + BasicBingSearchFooter;
+
+ // Act
+ var actualHtml = Bing._SearchBox("322px", String.Empty, "my-test-string", GetContextForSearchBox(encoding), new Dictionary<object, object> { { Bing._siteUrlKey, "www.myawesomesite.net" } }).ToString();
+
+ // Assert
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(expectedHtml, actualHtml);
+ }
+
+ private HttpContextBase GetContextForSearchBox(Encoding contentEncoding = null)
+ {
+ Mock<HttpContextBase> context = new Mock<HttpContextBase>();
+ Mock<HttpResponseBase> response = new Mock<HttpResponseBase>();
+ response.Setup(c => c.ContentEncoding).Returns(contentEncoding ?? Encoding.Default);
+ context.Setup(c => c.Response).Returns(response.Object);
+
+ return context.Object;
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Helpers.Test/FacebookTest.cs b/test/Microsoft.Web.Helpers.Test/FacebookTest.cs
new file mode 100644
index 00000000..b5b7a1f6
--- /dev/null
+++ b/test/Microsoft.Web.Helpers.Test/FacebookTest.cs
@@ -0,0 +1,270 @@
+using System;
+using System.Collections.Generic;
+using System.Web;
+using System.Web.TestUtil;
+using Microsoft.TestCommon;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace Microsoft.Web.Helpers.Test
+{
+ public class FacebookTest
+ {
+ public FacebookTest()
+ {
+ Facebook.AppId = "myapp'";
+ Facebook.AppSecret = "myappsecret";
+ Facebook.Language = "french";
+ }
+
+ [Fact]
+ public void GetFacebookCookieInfoReturnsEmptyStringIfCookieIsNotPresent()
+ {
+ // Arrange
+ var context = CreateHttpContext();
+
+ // Act
+ var info = Facebook.GetFacebookCookieInfo(context, "foo");
+
+ // Assert
+ Assert.Equal("", info);
+ }
+
+ [Fact]
+ public void GetFacebookCookieInfoThrowsIfCookieIsNotValid()
+ {
+ // Arrange
+
+ var context = CreateHttpContext(new Dictionary<string, string>
+ {
+ {"fbs_myapp'", "sig=malformed-signature&name=foo&val=bar&uid=MyFacebookName"},
+ {"fbs_uid", "MyFacebookName"},
+ });
+
+ // Act and Assert
+ Assert.Throws<InvalidOperationException>(() => Facebook.GetFacebookCookieInfo(context, "uid"), "Invalid Facebook cookie.");
+ }
+
+ [Fact]
+ public void GetFacebookCookieReturnsUserIdIfCookieIsValid()
+ {
+ // Arrange
+ var context = CreateHttpContext(new Dictionary<string, string>
+ {
+ {"fbs_myapp'", "sig=B2E6B3A21D0C9FA72E612BD6C3084807&name=foo&val=bar&uid=MyFacebookName"},
+ });
+
+ // Act
+ var info = Facebook.GetFacebookCookieInfo(context, "uid");
+
+ // Assert
+ Assert.Equal("MyFacebookName", info);
+ }
+
+ [Fact]
+ public void GetInitScriptsJSEncodesParameters()
+ {
+ // Arrange
+ var expectedText = @"
+ <div id=""fb-root""></div>
+ <script type=""text/javascript"">
+ window.fbAsyncInit = function () {
+ FB.init({ appId: 'MyApp\u0027', status: true, cookie: true, xfbml: true });
+ };
+ (function () {
+ var e = document.createElement('script'); e.async = true;
+ e.src = document.location.protocol +
+ '//connect.facebook.net/French/all.js';
+ document.getElementById('fb-root').appendChild(e);
+ } ());
+
+ function loginRedirect(url) { window.location = url; }
+ </script>
+ ";
+
+ // Act
+ var actualText = Facebook.GetInitializationScripts();
+
+ // Assert
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(expectedText, actualText.ToString());
+ }
+
+ [Fact]
+ public void LoginButtonTest()
+ {
+ // Arrange
+ var expected40 = @"<fb:login-button autologoutlink=""True"" size=""extra-small"" length=""extra-long"" onlogin=""loginRedirect(&#39;http://www.test.com/facebook/?registerUrl=http%3a%2f%2fwww.test.com%2f&amp;returnUrl=http%3a%2f%2fww.test.com%2fLogin.cshtml&#39;)"" show-faces=""True"" perms=""email,none&quot;"">Awesome &quot;button text&quot;</fb:login-button>";
+ var expected45 = @"<fb:login-button autologoutlink=""True"" size=""extra-small"" length=""extra-long"" onlogin=""loginRedirect(&#39;http://www.test.com/facebook/?registerUrl=http%3a%2f%2fwww.test.com%2f\u0026returnUrl=http%3a%2f%2fww.test.com%2fLogin.cshtml&#39;)"" show-faces=""True"" perms=""email,none&quot;"">Awesome &quot;button text&quot;</fb:login-button>";
+
+ // Act
+ var actualText = Facebook.LoginButton("http://www.test.com", "http://ww.test.com/Login.cshtml", "http://www.test.com/facebook/", "Awesome \"button text\"", true, "extra-small", "extra-long", true, "none\"");
+
+ // Assert
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(RuntimeEnvironment.IsVersion45Installed ? expected45 : expected40, actualText.ToString());
+ }
+
+ [Fact]
+ public void LoginButtonOnlyTagTest()
+ {
+ // Arrange
+ var expectedText = @"<fb:login-button autologoutlink=""True"" size=""small"" length=""medium"" onlogin=""foobar();"" show-faces=""True"" perms=""none&quot;"">&quot;Awesome button text&quot;</fb:login-button>";
+
+ // Act
+ var actualText = Facebook.LoginButtonTagOnly("\"Awesome button text\"", true, "small", "medium", "foobar();", true, "none\"");
+
+ // Assert
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(expectedText, actualText.ToString());
+ }
+
+ [Fact]
+ public void LikeButtonTest()
+ {
+ // Arrange
+ var expectedText = @"<iframe src=""http://www.facebook.com/plugins/like.php?href=http%3a%2f%2fsomewebsite&amp;layout=modern&amp;show_faces=false&amp;width=300&amp;action=hop&amp;colorscheme=lighter&amp;height=30&amp;font=Comic+Sans&amp;locale=French&amp;ref=foo+bar"" scrolling=""no"" frameborder=""0"" style=""border:none; overflow:hidden; width:300px; height:30px;"" allowTransparency=""true""></iframe>";
+
+ // Act
+ var actualText = Facebook.LikeButton("http://somewebsite", "modern", false, 300, 30, "hop", "Comic Sans", "lighter", "foo bar");
+
+ // Assert
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(expectedText, actualText.ToString());
+ }
+
+ [Fact]
+ public void CommentsWithNoXidTest()
+ {
+ // Arrange
+ var expectedText = @"<fb:comments numposts=""3"" width=""300"" reverse=""true"" simple=""true"" ></fb:comments>";
+
+ // Act
+ var actualText = Facebook.Comments(null, 300, 3, true, true);
+
+ // Assert
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(expectedText, actualText.ToString());
+ }
+
+ [Fact]
+ public void CommentsWithXidTest()
+ {
+ // Arrange
+ var expectedText = @"<fb:comments xid=""bar"" numposts=""3"" width=""300"" reverse=""true"" simple=""true"" ></fb:comments>";
+
+ // Act
+ var actualText = Facebook.Comments("bar", 300, 3, true, true);
+
+ // Assert
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(expectedText, actualText.ToString());
+ }
+
+ [Fact]
+ public void RecommendationsTest()
+ {
+ // Arrange
+ var expectedText = @"<iframe src=""http://www.facebook.com/plugins/recommendations.php?site=http%3a%2f%2fsomesite&amp;width=100&amp;height=200&amp;header=False&amp;colorscheme=blue&amp;font=none&amp;border_color=black&amp;filter=All+posts&amp;ref=ref+label&amp;locale=french"" scrolling=""no"" frameborder=""0"" style=""border:none; overflow:hidden; width:100px; height:200px;"" allowTransparency=""true""></iframe>";
+
+ // Act
+ var actualText = Facebook.Recommendations("http://somesite", 100, 200, false, "blue", "none", "black", "All posts", "ref label");
+
+ // Assert
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(expectedText, actualText.ToString());
+ }
+
+ [Fact]
+ public void LikeBoxTest()
+ {
+ // Arrange
+ var expectedText = @"<iframe src=""http://www.facebook.com/plugins/recommendations.php?href=http%3a%2f%2fsomesite&amp;width=100&amp;height=200&amp;header=False&amp;colorscheme=blue&amp;connections=5&amp;stream=True&amp;header=False&amp;locale=french"" scrolling=""no"" frameborder=""0"" style=""border:none; overflow:hidden; width:100px; height:200px;"" allowTransparency=""true""></iframe>";
+
+ // Act
+ var actualText = Facebook.LikeBox("http://somesite", 100, 200, "blue", 5, true, false);
+
+ // Assert
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(expectedText, actualText.ToString());
+ }
+
+ [Fact]
+ public void FacepileTest()
+ {
+ // Arrange
+ var expectedText = @"<fb:facepile max-rows=""3"" width=""100""></fb:facepile>";
+
+ // Act
+ var actualText = Facebook.Facepile(3, 100);
+
+ // Assert
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(expectedText, actualText.ToString());
+ }
+
+ [Fact]
+ public void LiveStreamWithEmptyXidTest()
+ {
+ // Arrange
+ var expectedText = @"<iframe src=""http://www.facebook.com/plugins/live_stream_box.php?app_id=myapp%27&amp;width=100&amp;height=100&amp;always_post_to_friends=True&amp;locale=french"" scrolling=""no"" frameborder=""0"" style=""border:none; overflow:hidden; width:100px; height:100px;"" allowTransparency=""true""></iframe>";
+
+ // Act
+ var actualText = Facebook.LiveStream(100, 100, "", "", true);
+
+ // Assert
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(expectedText, actualText.ToString());
+ }
+
+ [Fact]
+ public void LiveStreamWithXidValueTest()
+ {
+ // Arrange
+ var expectedText = @"<iframe src=""http://www.facebook.com/plugins/live_stream_box.php?app_id=myapp%27&amp;width=100&amp;height=100&amp;always_post_to_friends=True&amp;locale=french&amp;xid=some-val&amp;via_url=http%3a%2f%2fmysite"" scrolling=""no"" frameborder=""0"" style=""border:none; overflow:hidden; width:100px; height:100px;"" allowTransparency=""true""></iframe>";
+
+ // Act
+ var actualText = Facebook.LiveStream(100, 100, "some-val", "http://mysite", true);
+
+ // Assert
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(expectedText, actualText.ToString());
+ }
+
+ [Fact]
+ public void ActivityStreamTest()
+ {
+ // Arrange
+ var expectedText = @"<iframe src=""http://www.facebook.com/plugins/activity.php?site=http%3a%2f%2fmysite&amp;width=100&amp;height=120&amp;header=False&amp;colorscheme=gray&amp;font=Arial&amp;border_color=blue&amp;recommendations=True&amp;locale=french"" scrolling=""no"" frameborder=""0"" style=""border:none; overflow:hidden; width:300px; height:300px;"" allowTransparency=""true""></iframe>";
+
+ // Act
+ var actualText = Facebook.ActivityFeed("http://mysite", 100, 120, false, "gray", "Arial", "blue", true);
+
+ // Assert
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(expectedText, actualText.ToString());
+ }
+
+ [Fact]
+ public void FbmlNamespacesTest()
+ {
+ // Arrange
+ var expectedText = @"xmlns:fb=""http://www.facebook.com/2008/fbml"" xmlns:og=""http://opengraphprotocol.org/schema/""";
+
+ // Act
+ var actualText = Facebook.FbmlNamespaces();
+
+ // Assert
+ Assert.Equal(expectedText, actualText.ToString());
+ }
+
+ private static HttpContextBase CreateHttpContext(IDictionary<string, string> cookieValues = null)
+ {
+ var context = new Mock<HttpContextBase>();
+ var httpRequest = new Mock<HttpRequestBase>();
+ var cookies = new HttpCookieCollection();
+ httpRequest.Setup(c => c.Cookies).Returns(cookies);
+
+ context.Setup(c => c.Request).Returns(httpRequest.Object);
+
+ if (cookieValues != null)
+ {
+ foreach (var item in cookieValues)
+ {
+ cookies.Add(new HttpCookie(item.Key, item.Value));
+ }
+ }
+
+ return context.Object;
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Helpers.Test/FileUploadTest.cs b/test/Microsoft.Web.Helpers.Test/FileUploadTest.cs
new file mode 100644
index 00000000..c343eaf3
--- /dev/null
+++ b/test/Microsoft.Web.Helpers.Test/FileUploadTest.cs
@@ -0,0 +1,199 @@
+using System.Collections;
+using System.Web;
+using System.Web.Helpers.Test;
+using System.Web.TestUtil;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace Microsoft.Web.Helpers.Test
+{
+ public class FileUploadTest
+ {
+ private const string _fileUploadScript = "<script type=\"text/javascript\"> if (!window[\"FileUploadHelper\"]) window[\"FileUploadHelper\"] = {}; FileUploadHelper.addInputElement = function(index, name) { var inputElem = document.createElement(\"input\"); inputElem.type = \"file\"; inputElem.name = name; var divElem = document.createElement(\"div\"); divElem.appendChild(inputElem.cloneNode(false)); var inputs = document.getElementById(\"file-upload-\" + index); inputs.appendChild(divElem); } </script>";
+
+ [Fact]
+ public void RenderThrowsWhenNumberOfFilesIsLessThanZero()
+ {
+ // Act and Assert
+ Assert.ThrowsArgumentGreaterThanOrEqualTo(
+ () => FileUpload._GetHtml(GetContext(), name: null, initialNumberOfFiles: -2, allowMoreFilesToBeAdded: false, includeFormTag: false, addText: "", uploadText: "").ToString(),
+ "initialNumberOfFiles", "0");
+ }
+
+ [Fact]
+ public void ResultIncludesFormTagAndSubmitButtonWhenRequested()
+ {
+ // Arrange
+ string expectedResult = @"<form action="""" enctype=""multipart/form-data"" method=""post""><div class=""file-upload"" id=""file-upload-0"">"
+ + @"<div><input name=""fileUpload"" type=""file"" /></div></div>"
+ + @"<div class=""file-upload-buttons""><input type=""submit"" value=""Upload"" /></div></form>";
+
+ // Act
+ var actualResult = FileUpload._GetHtml(GetContext(), name: null, initialNumberOfFiles: 1, allowMoreFilesToBeAdded: false, includeFormTag: true, addText: null, uploadText: null);
+
+ // Assert
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(expectedResult, actualResult.ToString());
+ }
+
+ [Fact]
+ public void ResultDoesNotIncludeFormTagAndSubmitButtonWhenNotRequested()
+ {
+ // Arrange
+ string expectedResult = @"<div class=""file-upload"" id=""file-upload-0""><div><input name=""fileUpload"" type=""file"" /></div></div>";
+
+ // Act
+ var actualResult = FileUpload._GetHtml(GetContext(), name: null, initialNumberOfFiles: 1, allowMoreFilesToBeAdded: false, includeFormTag: false, addText: null, uploadText: null);
+
+ // Assert
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(expectedResult, actualResult.ToString());
+ }
+
+ [Fact]
+ public void ResultIncludesCorrectNumberOfInputFields()
+ {
+ // Arrange
+ string expectedResult = @"<div class=""file-upload"" id=""file-upload-0""><div><input name=""fileUpload"" type=""file"" /></div><div><input name=""fileUpload"" type=""file"" /></div>"
+ + @"<div><input name=""fileUpload"" type=""file"" /></div></div>";
+
+ // Act
+ var actualResult = FileUpload._GetHtml(GetContext(), name: null, initialNumberOfFiles: 3, allowMoreFilesToBeAdded: false, includeFormTag: false, addText: null, uploadText: null);
+
+ // Assert
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(expectedResult, actualResult.ToString());
+ }
+
+ [Fact]
+ public void ResultIncludesAnchorTagWithCorrectAddText()
+ {
+ // Arrange
+ string customAddText = "Add More";
+ string expectedResult = _fileUploadScript
+ + @"<div class=""file-upload"" id=""file-upload-0""><div><input name=""fileUpload"" type=""file"" /></div></div>"
+ + @"<div class=""file-upload-buttons""><a href=""#"" onclick=""FileUploadHelper.addInputElement(0, &quot;fileUpload&quot;); return false;"">" + customAddText + "</a></div>";
+
+ // Act
+ var result = FileUpload._GetHtml(GetContext(), name: null, allowMoreFilesToBeAdded: true, includeFormTag: false, addText: customAddText, initialNumberOfFiles: 1, uploadText: null);
+
+ // Assert
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(expectedResult, result.ToString());
+ }
+
+ [Fact]
+ public void ResultDoesNotIncludeAnchorTagNorAddTextWhenNotRequested()
+ {
+ // Arrange
+ string customAddText = "Add More";
+ string expectedResult = @"<div class=""file-upload"" id=""file-upload-0""><div><input name=""fileUpload"" type=""file"" /></div></div>";
+
+ // Act
+ var result = FileUpload._GetHtml(GetContext(), name: null, allowMoreFilesToBeAdded: false, includeFormTag: false, addText: customAddText, uploadText: null, initialNumberOfFiles: 1);
+
+ // Assert
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(expectedResult, result.ToString());
+ }
+
+ [Fact]
+ public void ResultIncludesSubmitInputTagWithCustomUploadText()
+ {
+ // Arrange
+ string customUploadText = "Now!";
+ string expectedResult = @"<form action="""" enctype=""multipart/form-data"" method=""post""><div class=""file-upload"" id=""file-upload-0"">"
+ + @"<div><input name=""fileUpload"" type=""file"" /></div></div>"
+ + @"<div class=""file-upload-buttons""><input type=""submit"" value=""" + customUploadText + @""" /></div></form>";
+
+ // Act
+ var result = FileUpload._GetHtml(GetContext(), name: null, includeFormTag: true, uploadText: customUploadText, allowMoreFilesToBeAdded: false, initialNumberOfFiles: 1, addText: null);
+
+ // Assert
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(expectedResult, result.ToString());
+ }
+
+ [Fact]
+ public void FileUploadGeneratesUniqueIdsForMultipleCallsForCommonRequest()
+ {
+ // Arrange
+ var context = GetContext();
+ string expectedResult1 = @"<div class=""file-upload"" id=""file-upload-0""><div><input name=""fileUpload"" type=""file"" /></div><div><input name=""fileUpload"" type=""file"" /></div>"
+ + @"<div><input name=""fileUpload"" type=""file"" /></div></div>";
+ string expectedResult2 = @"<form action="""" enctype=""multipart/form-data"" method=""post""><div class=""file-upload"" id=""file-upload-1""><div><input name=""fileUpload"" type=""file"" /></div>"
+ + @"<div><input name=""fileUpload"" type=""file"" /></div></div><div class=""file-upload-buttons""><input type=""submit"" value=""Upload"" /></div></form>";
+
+ // Act
+ var result1 = FileUpload._GetHtml(context, name: null, initialNumberOfFiles: 3, allowMoreFilesToBeAdded: false, includeFormTag: false, addText: null, uploadText: null);
+ var result2 = FileUpload._GetHtml(context, name: null, initialNumberOfFiles: 2, allowMoreFilesToBeAdded: false, includeFormTag: true, addText: null, uploadText: null);
+
+ // Assert
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(expectedResult1, result1.ToString());
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(expectedResult2, result2.ToString());
+ }
+
+ [Fact]
+ public void FileUploadGeneratesScriptOncePerRequest()
+ {
+ // Arrange
+ var context = GetContext();
+ string expectedResult1 = _fileUploadScript
+ + @"<div class=""file-upload"" id=""file-upload-0""><div><input name=""fileUpload"" type=""file"" /></div></div>"
+ + @"<div class=""file-upload-buttons""><a href=""#"" onclick=""FileUploadHelper.addInputElement(0, &quot;fileUpload&quot;); return false;"">Add more files</a></div>";
+ string expectedResult2 = @"<form action="""" enctype=""multipart/form-data"" method=""post""><div class=""file-upload"" id=""file-upload-1""><div><input name=""fileUpload"" type=""file"" /></div></div>"
+ + @"<div class=""file-upload-buttons""><a href=""#"" onclick=""FileUploadHelper.addInputElement(1, &quot;fileUpload&quot;); return false;"">Add more files</a><input type=""submit"" value=""Upload"" /></div></form>";
+ string expectedResult3 = @"<form action="""" enctype=""multipart/form-data"" method=""post""><div class=""file-upload"" id=""file-upload-2"">"
+ + @"<div><input name=""fileUpload"" type=""file"" /></div></div>"
+ + @"<div class=""file-upload-buttons""><input type=""submit"" value=""Upload"" /></div></form>";
+
+ // Act
+ var result1 = FileUpload._GetHtml(context, name: null, initialNumberOfFiles: 1, allowMoreFilesToBeAdded: true, includeFormTag: false, addText: null, uploadText: null);
+ var result2 = FileUpload._GetHtml(context, name: null, initialNumberOfFiles: 1, allowMoreFilesToBeAdded: true, includeFormTag: true, addText: null, uploadText: null);
+ var result3 = FileUpload._GetHtml(context, name: null, initialNumberOfFiles: 1, allowMoreFilesToBeAdded: false, includeFormTag: true, addText: null, uploadText: null);
+
+ // Assert
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(expectedResult1, result1.ToString());
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(expectedResult2, result2.ToString());
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(expectedResult3, result3.ToString());
+ }
+
+ [Fact]
+ public void FileUploadUsesNamePropertyInJavascript()
+ {
+ // Arrange
+ var context = GetContext();
+ string name = "foobar";
+ string expectedResult = _fileUploadScript
+ + @"<form action="""" enctype=""multipart/form-data"" method=""post""><div class=""file-upload"" id=""file-upload-0""><div><input name=""foobar"" type=""file"" /></div></div>"
+ + @"<div class=""file-upload-buttons""><a href=""#"" onclick=""FileUploadHelper.addInputElement(0, &quot;foobar&quot;); return false;"">Add more files</a><input type=""submit"" value=""Upload"" /></div></form>";
+
+ // Act
+ var result = FileUpload._GetHtml(context, name: name, initialNumberOfFiles: 1, allowMoreFilesToBeAdded: true, includeFormTag: true, addText: null, uploadText: null);
+
+ // Assert
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(expectedResult, result.ToString());
+ }
+
+ [Fact]
+ public void _GetHtmlWithDefaultArgumentsProducesValidXhtml()
+ {
+ // Act
+ var result = FileUpload._GetHtml(GetContext(), name: null, initialNumberOfFiles: 1, includeFormTag: false, allowMoreFilesToBeAdded: false, addText: null, uploadText: null);
+
+ // Assert
+ XhtmlAssert.Validate1_1(result, "div");
+ }
+
+ [Fact]
+ public void _GetHtmlWithoutFormTagProducesValidXhtml()
+ {
+ // Act
+ var result = FileUpload._GetHtml(GetContext(), name: null, includeFormTag: false, initialNumberOfFiles: 1, allowMoreFilesToBeAdded: false, addText: null, uploadText: null);
+
+ XhtmlAssert.Validate1_1(result, "div");
+ }
+
+ private HttpContextBase GetContext()
+ {
+ var context = new Mock<HttpContextBase>();
+ context.Setup(c => c.Items).Returns(new Hashtable());
+ return context.Object;
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Helpers.Test/GamerCardTest.cs b/test/Microsoft.Web.Helpers.Test/GamerCardTest.cs
new file mode 100644
index 00000000..5923a78c
--- /dev/null
+++ b/test/Microsoft.Web.Helpers.Test/GamerCardTest.cs
@@ -0,0 +1,59 @@
+using System;
+using System.Web.Helpers.Test;
+using System.Web.TestUtil;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace Microsoft.Web.Helpers.Test
+{
+ public class GamerCardTest
+ {
+ [Fact]
+ public void RenderThrowsWhenGamertagIsEmpty()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmptyString(() => { GamerCard.GetHtml(String.Empty).ToString(); }, "gamerTag");
+ }
+
+ [Fact]
+ public void RenderThrowsWhenGamertagIsNull()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmptyString(() => { GamerCard.GetHtml(null).ToString(); }, "gamerTag");
+ }
+
+ [Fact]
+ public void RenderGeneratesProperMarkupWithSimpleGamertag()
+ {
+ // Arrange
+ string expectedHtml = "<iframe frameborder=\"0\" height=\"140\" scrolling=\"no\" src=\"http://gamercard.xbox.com/osbornm.card\" width=\"204\">osbornm</iframe>";
+
+ // Act
+ string html = GamerCard.GetHtml("osbornm").ToHtmlString();
+
+ // Assert
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(expectedHtml, html);
+ }
+
+ [Fact]
+ public void RenderGeneratesProperMarkupWithComplexGamertag()
+ {
+ // Arrange
+ string expectedHtml = "<iframe frameborder=\"0\" height=\"140\" scrolling=\"no\" src=\"http://gamercard.xbox.com/matthew%20osborn&#39;s.card\" width=\"204\">matthew osborn&#39;s</iframe>";
+
+ // Act
+ string html = GamerCard.GetHtml("matthew osborn's").ToHtmlString();
+
+ // Assert
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(expectedHtml, html);
+ }
+
+ [Fact]
+ public void RenderGeneratesValidXhtml()
+ {
+ XhtmlAssert.Validate1_0(
+ GamerCard.GetHtml("osbornm")
+ );
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Helpers.Test/GravatarTest.cs b/test/Microsoft.Web.Helpers.Test/GravatarTest.cs
new file mode 100644
index 00000000..af2e039f
--- /dev/null
+++ b/test/Microsoft.Web.Helpers.Test/GravatarTest.cs
@@ -0,0 +1,142 @@
+using System;
+using System.Web.Helpers.Test;
+using System.Web.WebPages.TestUtils;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace Microsoft.Web.Helpers.Test
+{
+ public class GravatarTest
+ {
+ [Fact]
+ public void GetUrlDefaults()
+ {
+ string url = Gravatar.GetUrl("foo@bar.com");
+ Assert.Equal("http://www.gravatar.com/avatar/f3ada405ce890b6f8204094deb12d8a8?s=80", url);
+ }
+
+ [Fact]
+ public void RenderEncodesDefaultImageUrl()
+ {
+ string render = Gravatar.GetHtml("foo@bar.com", defaultImage: "http://example.com/images/example.jpg").ToString();
+ Assert.Equal(
+ "<img src=\"http://www.gravatar.com/avatar/f3ada405ce890b6f8204094deb12d8a8?s=80&amp;d=http%3a%2f%2fexample.com%2fimages%2fexample.jpg\" alt=\"gravatar\" />",
+ render);
+ }
+
+ [Fact]
+ public void RenderLowerCasesEmail()
+ {
+ string render = Gravatar.GetHtml("FOO@BAR.COM").ToString();
+ Assert.Equal(
+ "<img src=\"http://www.gravatar.com/avatar/f3ada405ce890b6f8204094deb12d8a8?s=80\" alt=\"gravatar\" />",
+ render);
+ }
+
+ [Fact]
+ public void RendersValidXhtml()
+ {
+ XhtmlAssert.Validate1_1(Gravatar.GetHtml("foo@bar.com"));
+ }
+
+ [Fact]
+ public void RenderThrowsWhenEmailIsEmpty()
+ {
+ Assert.ThrowsArgumentNullOrEmptyString(() => { Gravatar.GetHtml(String.Empty); }, "email");
+ }
+
+ [Fact]
+ public void RenderThrowsWhenEmailIsNull()
+ {
+ Assert.ThrowsArgumentNullOrEmptyString(() => { Gravatar.GetHtml(null); }, "email");
+ }
+
+ [Fact]
+ public void RenderThrowsWhenImageSizeIsLessThanZero()
+ {
+ Assert.ThrowsArgument(() => { Gravatar.GetHtml("foo@bar.com", imageSize: -1); }, "imageSize", "The Gravatar image size must be between 1 and 512 pixels.");
+ }
+
+ [Fact]
+ public void RenderThrowsWhenImageSizeIsZero()
+ {
+ Assert.ThrowsArgument(() => { Gravatar.GetHtml("foo@bar.com", imageSize: 0); }, "imageSize", "The Gravatar image size must be between 1 and 512 pixels.");
+ }
+
+ [Fact]
+ public void RenderThrowsWhenImageSizeIsGreaterThan512()
+ {
+ Assert.ThrowsArgument(() => { Gravatar.GetHtml("foo@bar.com", imageSize: 513); }, "imageSize", "The Gravatar image size must be between 1 and 512 pixels.");
+ }
+
+ [Fact]
+ public void RenderTrimsEmail()
+ {
+ string render = Gravatar.GetHtml(" \t foo@bar.com\t\r\n").ToString();
+ Assert.Equal(
+ "<img src=\"http://www.gravatar.com/avatar/f3ada405ce890b6f8204094deb12d8a8?s=80\" alt=\"gravatar\" />",
+ render);
+ }
+
+ [Fact]
+ public void RenderUsesDefaultImage()
+ {
+ string render = Gravatar.GetHtml("foo@bar.com", defaultImage: "wavatar").ToString();
+ Assert.Equal(
+ "<img src=\"http://www.gravatar.com/avatar/f3ada405ce890b6f8204094deb12d8a8?s=80&amp;d=wavatar\" alt=\"gravatar\" />",
+ render);
+ }
+
+ [Fact]
+ public void RenderUsesImageSize()
+ {
+ string render = Gravatar.GetHtml("foo@bar.com", imageSize: 512).ToString();
+ Assert.Equal(
+ "<img src=\"http://www.gravatar.com/avatar/f3ada405ce890b6f8204094deb12d8a8?s=512\" alt=\"gravatar\" />",
+ render);
+ }
+
+ [Fact]
+ public void RenderUsesRating()
+ {
+ string render = Gravatar.GetHtml("foo@bar.com", rating: GravatarRating.G).ToString();
+ Assert.Equal(
+ "<img src=\"http://www.gravatar.com/avatar/f3ada405ce890b6f8204094deb12d8a8?s=80&amp;r=g\" alt=\"gravatar\" />",
+ render);
+ }
+
+ [Fact]
+ public void RenderWithAttributes()
+ {
+ string render = Gravatar.GetHtml("foo@bar.com",
+ attributes: new { id = "gravatar", alT = "<b>foo@bar.com</b>", srC = "ignored" }).ToString();
+ // beware of attributes ordering in tests
+ Assert.Equal(
+ "<img src=\"http://www.gravatar.com/avatar/f3ada405ce890b6f8204094deb12d8a8?s=80\" alT=\"&lt;b>foo@bar.com&lt;/b>\" id=\"gravatar\" />",
+ render);
+ }
+
+ [Fact]
+ public void RenderWithDefaults()
+ {
+ string render = Gravatar.GetHtml("foo@bar.com").ToString();
+ Assert.Equal(
+ "<img src=\"http://www.gravatar.com/avatar/f3ada405ce890b6f8204094deb12d8a8?s=80\" alt=\"gravatar\" />",
+ render);
+ }
+
+ [Fact]
+ public void RenderWithExtension()
+ {
+ string render = Gravatar.GetHtml("foo@bar.com", imageExtension: ".png").ToString();
+ Assert.Equal(
+ "<img src=\"http://www.gravatar.com/avatar/f3ada405ce890b6f8204094deb12d8a8.png?s=80\" alt=\"gravatar\" />",
+ render);
+
+ render = Gravatar.GetHtml("foo@bar.com", imageExtension: "xyz").ToString();
+ Assert.Equal(
+ "<img src=\"http://www.gravatar.com/avatar/f3ada405ce890b6f8204094deb12d8a8.xyz?s=80\" alt=\"gravatar\" />",
+ render);
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Helpers.Test/LinkShareTest.cs b/test/Microsoft.Web.Helpers.Test/LinkShareTest.cs
new file mode 100644
index 00000000..2d9bc4a2
--- /dev/null
+++ b/test/Microsoft.Web.Helpers.Test/LinkShareTest.cs
@@ -0,0 +1,188 @@
+using System;
+using System.Linq;
+using System.Web;
+using System.Web.Helpers.Test;
+using System.Web.TestUtil;
+using System.Web.WebPages.Scope;
+using System.Web.WebPages.TestUtils;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace Microsoft.Web.Helpers.Test
+{
+ public class LinkShareTest
+ {
+ private static LinkShareSite[] _allLinkShareSites = new[]
+ {
+ LinkShareSite.Delicious, LinkShareSite.Digg, LinkShareSite.GoogleBuzz,
+ LinkShareSite.Facebook, LinkShareSite.Reddit, LinkShareSite.StumbleUpon, LinkShareSite.Twitter
+ };
+
+ [Fact]
+ public void RenderWithFacebookFirst_ReturnsHtmlWithFacebookAndThenOthersTest()
+ {
+ string pageTitle = "page1";
+ string pageLinkBack = "page link back";
+ string twitterUserName = String.Empty;
+ string twitterTag = String.Empty;
+ string actual;
+ actual = LinkShare.GetHtml(pageTitle, pageLinkBack, twitterUserName, twitterTag, LinkShareSite.Facebook, LinkShareSite.All).ToString();
+ Assert.True(actual.Contains("twitter.com"));
+ int pos = actual.IndexOf("facebook.com");
+ Assert.True(pos > 0);
+ int pos2 = actual.IndexOf("reddit.com");
+ Assert.True(pos2 > pos);
+ pos2 = actual.IndexOf("digg.com");
+ Assert.True(pos2 > pos);
+ }
+
+ [Fact]
+ public void BitlyApiKeyThrowsWhenSetToNull()
+ {
+ Assert.ThrowsArgumentNull(() => LinkShare.BitlyApiKey = null, "value");
+ }
+
+ [Fact]
+ public void BitlyApiKeyUsesScopeStorage()
+ {
+ // Arrange
+ var value = "value";
+
+ // Act
+ LinkShare.BitlyApiKey = value;
+
+ // Assert
+ Assert.Equal(LinkShare.BitlyApiKey, value);
+ Assert.Equal(ScopeStorage.CurrentScope[LinkShare._bitlyApiKey], value);
+ }
+
+ [Fact]
+ public void BitlyLoginThrowsWhenSetToNull()
+ {
+ Assert.ThrowsArgumentNull(() => LinkShare.BitlyLogin = null, "value");
+ }
+
+ [Fact]
+ public void BitlyLoginUsesScopeStorage()
+ {
+ // Arrange
+ var value = "value";
+
+ // Act
+ LinkShare.BitlyLogin = value;
+
+ // Assert
+ Assert.Equal(LinkShare.BitlyLogin, value);
+ Assert.Equal(ScopeStorage.CurrentScope[LinkShare._bitlyLogin], value);
+ }
+
+ [Fact]
+ public void RenderWithNullPageTitle_ThrowsException()
+ {
+ Assert.ThrowsArgumentNullOrEmptyString(
+ () => LinkShare.GetHtml(null).ToString(),
+ "pageTitle");
+ }
+
+ [Fact]
+ public void Render_WithFacebook_Works()
+ {
+ string actualHTML = LinkShare.GetHtml("page-title", "www.foo.com", linkSites: LinkShareSite.Facebook).ToString();
+ string expectedHTML =
+ "<a href=\"http://www.facebook.com/sharer.php?u=www.foo.com&amp;t=page-title\" target=\"_blank\" title=\"Share on Facebook\"><img alt=\"Share on Facebook\" src=\"http://www.facebook.com/favicon.ico\" style=\"border:0; height:16px; width:16px; margin:0 1px;\" title=\"Share on Facebook\" /></a>";
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(actualHTML, expectedHTML);
+ }
+
+ [Fact]
+ public void Render_WithFacebookAndDigg_Works()
+ {
+ string actualHTML = LinkShare.GetHtml("page-title", "www.foo.com", linkSites: new[] { LinkShareSite.Facebook, LinkShareSite.Digg }).ToString();
+ string expectedHTML =
+ "<a href=\"http://www.facebook.com/sharer.php?u=www.foo.com&amp;t=page-title\" target=\"_blank\" title=\"Share on Facebook\"><img alt=\"Share on Facebook\" src=\"http://www.facebook.com/favicon.ico\" style=\"border:0; height:16px; width:16px; margin:0 1px;\" title=\"Share on Facebook\" /></a><a href=\"http://digg.com/submit?url=www.foo.com&amp;title=page-title\" target=\"_blank\" title=\"Digg!\"><img alt=\"Digg!\" src=\"http://digg.com/img/badges/16x16-digg-guy.gif\" style=\"border:0; height:16px; width:16px; margin:0 1px;\" title=\"Digg!\" /></a>";
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(actualHTML, expectedHTML);
+ }
+
+ [Fact]
+ public void Render_WithFacebook_RendersAnchorTitle()
+ {
+ string actualHTML = LinkShare.GetHtml("page-title", "www.foo.com", linkSites: LinkShareSite.Facebook).ToString();
+ string expectedHtml = @"<a href=""http://www.facebook.com/sharer.php?u=www.foo.com&amp;t=page-title"" target=""_blank"" title=""Share on Facebook"">
+ <img alt=""Share on Facebook"" src=""http://www.facebook.com/favicon.ico"" style=""border:0; height:16px; width:16px; margin:0 1px;"" title=""Share on Facebook"" />
+ </a>";
+
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(expectedHtml, actualHTML);
+ }
+
+ [Fact]
+ public void LinkShare_GetSitesInOrderReturnsAllSitesWhenArgumentIsNull()
+ {
+ // Act and Assert
+ var result = LinkShare.GetSitesInOrder(linkSites: null);
+
+ Assert.Equal(_allLinkShareSites, result.ToArray());
+ }
+
+ [Fact]
+ public void LinkShare_GetSitesInOrderReturnsAllSitesWhenArgumentIEmpty()
+ {
+ // Act
+ var result = LinkShare.GetSitesInOrder(linkSites: new LinkShareSite[] { });
+
+ // Assert
+ Assert.Equal(_allLinkShareSites, result.ToArray());
+ }
+
+ [Fact]
+ public void LinkShare_GetSitesInOrderReturnsAllSitesWhenAllIsFirstItem()
+ {
+ // Act
+ var result = LinkShare.GetSitesInOrder(linkSites: new[] { LinkShareSite.All, LinkShareSite.Reddit });
+
+ // Assert
+ Assert.Equal(_allLinkShareSites, result.ToArray());
+ }
+
+ [Fact]
+ public void LinkShare_GetSitesInOrderReturnsSitesInOrderWhenAllIsNotFirstItem()
+ {
+ // Act
+ var result = LinkShare.GetSitesInOrder(linkSites: new[] { LinkShareSite.Reddit, LinkShareSite.Facebook, LinkShareSite.All });
+
+ // Assert
+ Assert.Equal(new[]
+ {
+ LinkShareSite.Reddit, LinkShareSite.Facebook, LinkShareSite.Delicious, LinkShareSite.Digg,
+ LinkShareSite.GoogleBuzz, LinkShareSite.StumbleUpon, LinkShareSite.Twitter
+ }, result.ToArray());
+ }
+
+ [Fact]
+ public void LinkShare_EncodesParameters()
+ {
+ // Arrange
+ var expectedHtml =
+ @"<a href=""http://reddit.com/submit?url=www.foo.com&amp;title=%26%26"" target=""_blank"" title=""Reddit!"">
+ <img alt=""Reddit!"" src=""http://www.Reddit.com/favicon.ico"" style=""border:0; height:16px; width:16px; margin:0 1px;"" title=""Reddit!"" />
+ </a>
+ <a href=""http://twitter.com/home/?status=%26%26%3a+www.foo.com%2c+(via+%40%40%3cTweeter+Bot%3e)+I+%3c3+Tweets"" target=""_blank"" title=""Share on Twitter"">
+ <img alt=""Share on Twitter"" src=""http://twitter.com/favicon.ico"" style=""border:0; height:16px; width:16px; margin:0 1px;"" title=""Share on Twitter"" />
+ </a>";
+
+ // Act
+ var actualHtml = LinkShare.GetHtml("&&", "www.foo.com", "<Tweeter Bot>", "I <3 Tweets", LinkShareSite.Reddit, LinkShareSite.Twitter).ToString();
+
+ // Assert
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(expectedHtml, actualHtml);
+ }
+
+ [Fact]
+ public void LinkshareRendersValidXhtml()
+ {
+ string result = "<html> <head> \n <title> </title> \n </head> \n <body> <div> \n" +
+ LinkShare.GetHtml("any<>title", "my test page <>") +
+ "\n </div> </body> \n </html>";
+ HtmlString htmlResult = new HtmlString(result);
+ XhtmlAssert.Validate1_0(htmlResult);
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Helpers.Test/MapsTest.cs b/test/Microsoft.Web.Helpers.Test/MapsTest.cs
new file mode 100644
index 00000000..ec9aacf0
--- /dev/null
+++ b/test/Microsoft.Web.Helpers.Test/MapsTest.cs
@@ -0,0 +1,55 @@
+using System;
+using System.Linq;
+using Xunit;
+
+namespace Microsoft.Web.Helpers.Test
+{
+ public class MapsTest
+ {
+ [Fact]
+ public void GetDirectionsQueryReturnsLocationIfNotEmpty()
+ {
+ // Arrange
+ var location = "a%";
+ var latitude = "12.34";
+ var longitude = "-56.78";
+
+ // Act
+ string result = Maps.GetDirectionsQuery(location, latitude, longitude);
+
+ // Assert
+ Assert.Equal("a%25", result);
+ }
+
+ [Fact]
+ public void GetDirectionsQueryReturnsLatitudeLongitudeIfLocationIsEmpty()
+ {
+ // Arrange
+ var location = "";
+ var latitude = "12.34%";
+ var longitude = "-&56.78";
+
+ // Act
+ string result = Maps.GetDirectionsQuery(location, latitude, longitude);
+
+ // Assert
+ Assert.Equal("12.34%25%2c-%2656.78", result);
+ }
+
+ [Fact]
+ public void GetDirectionsQueryUsesSpecifiedEncoder()
+ {
+ // Arrange
+ var location = "24 gnidliuB tfosorciM";
+ var latitude = "12.34%";
+ var longitude = "-&56.78";
+ Func<string, string> encoder = k => new String(k.Reverse().ToArray());
+
+ // Act
+ string result = Maps.GetDirectionsQuery(location, latitude, longitude, encoder);
+
+ // Assert
+ Assert.Equal("Microsoft Building 42", result);
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Helpers.Test/Microsoft.Web.Helpers.Test.csproj b/test/Microsoft.Web.Helpers.Test/Microsoft.Web.Helpers.Test.csproj
new file mode 100644
index 00000000..a7b7feb9
--- /dev/null
+++ b/test/Microsoft.Web.Helpers.Test/Microsoft.Web.Helpers.Test.csproj
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProductVersion>9.0.30729</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{2C653A66-8159-4A41-954F-A67915DFDA87}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>Microsoft.Web.Helpers.Test</RootNamespace>
+ <AssemblyName>Microsoft.Web.Helpers.Test</AssemblyName>
+ <FileAlignment>512</FileAlignment>
+ <ProjectTypeGuids>{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Debug\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Release\Test\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'CodeCoverage' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\CodeCoverage\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="Microsoft.CSharp" />
+ <Reference Include="Moq, Version=4.0.10827.0, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL">
+ <HintPath>..\..\packages\Moq.4.0.10827\lib\NET40\Moq.dll</HintPath>
+ </Reference>
+ <Reference Include="System" />
+ <Reference Include="System.Web" />
+ <Reference Include="xunit">
+ <HintPath>..\..\packages\xunit.1.9.0.1566\lib\xunit.dll</HintPath>
+ </Reference>
+ <Reference Include="xunit.extensions">
+ <HintPath>..\..\packages\xunit.extensions.1.9.0.1566\lib\xunit.extensions.dll</HintPath>
+ </Reference>
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="AnalyticsTest.cs" />
+ <Compile Include="BingTest.cs" />
+ <Compile Include="FacebookTest.cs" />
+ <Compile Include="FileUploadTest.cs" />
+ <Compile Include="GamerCardTest.cs" />
+ <Compile Include="GravatarTest.cs" />
+ <Compile Include="LinkShareTest.cs" />
+ <Compile Include="MapsTest.cs" />
+ <Compile Include="PreAppStartCodeTest.cs" />
+ <Compile Include="ThemesTest.cs" />
+ <Compile Include="TwitterTest.cs" />
+ <Compile Include="VideoTest.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="ReCaptchaTest.cs" />
+ <Compile Include="UrlBuilderTest.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.Web.Helpers\Microsoft.Web.Helpers.csproj">
+ <Project>{0C7CE809-0F72-4C19-8C64-D6573E4D9521}</Project>
+ <Name>Microsoft.Web.Helpers</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\src\System.Web.Helpers\System.Web.Helpers.csproj">
+ <Project>{9B7E3740-6161-4548-833C-4BBCA43B970E}</Project>
+ <Name>System.Web.Helpers</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\src\System.Web.Razor\System.Web.Razor.csproj">
+ <Project>{8F18041B-9410-4C36-A9C5-067813DF5F31}</Project>
+ <Name>System.Web.Razor</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\src\System.Web.WebPages.Razor\System.Web.WebPages.Razor.csproj">
+ <Project>{0939B11A-FE4E-4BA1-8AD6-D97741EE314F}</Project>
+ <Name>System.Web.WebPages.Razor</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\src\System.Web.WebPages\System.Web.WebPages.csproj">
+ <Project>{76EFA9C5-8D7E-4FDF-B710-E20F8B6B00D2}</Project>
+ <Name>System.Web.WebPages</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\Microsoft.TestCommon\Microsoft.TestCommon.csproj">
+ <Project>{FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0}</Project>
+ <Name>Microsoft.TestCommon</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\System.Web.Helpers.Test\System.Web.Helpers.Test.csproj">
+ <Project>{D3313BDF-8071-4AC8-9D98-ABF7F9E88A57}</Project>
+ <Name>System.Web.Helpers.Test</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="packages.config" />
+ </ItemGroup>
+ <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+</Project> \ No newline at end of file
diff --git a/test/Microsoft.Web.Helpers.Test/PreAppStartCodeTest.cs b/test/Microsoft.Web.Helpers.Test/PreAppStartCodeTest.cs
new file mode 100644
index 00000000..3f79b448
--- /dev/null
+++ b/test/Microsoft.Web.Helpers.Test/PreAppStartCodeTest.cs
@@ -0,0 +1,31 @@
+using System.Linq;
+using System.Web.WebPages.Razor;
+using System.Web.WebPages.TestUtils;
+using Xunit;
+
+namespace Microsoft.Web.Helpers.Test
+{
+ public class PreApplicationStartCodeTest
+ {
+ [Fact]
+ public void StartTest()
+ {
+ AppDomainUtils.RunInSeparateAppDomain(() =>
+ {
+ // Act
+ AppDomainUtils.SetPreAppStartStage();
+ PreApplicationStartCode.Start();
+
+ // Assert
+ var imports = WebPageRazorHost.GetGlobalImports();
+ Assert.True(imports.Any(ns => ns.Equals("Microsoft.Web.Helpers")));
+ });
+ }
+
+ [Fact]
+ public void TestPreAppStartClass()
+ {
+ PreAppStartTestHelper.TestPreAppStartClass(typeof(PreApplicationStartCode));
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Helpers.Test/Properties/AssemblyInfo.cs b/test/Microsoft.Web.Helpers.Test/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..1b4a4e5a
--- /dev/null
+++ b/test/Microsoft.Web.Helpers.Test/Properties/AssemblyInfo.cs
@@ -0,0 +1,34 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+
+[assembly: AssemblyTitle("Microsoft.Web.Helpers.Test")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("MSIT")]
+[assembly: AssemblyProduct("Microsoft.Web.Helpers.Test")]
+[assembly: AssemblyCopyright("Copyright MSIT 2010")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+
+[assembly: ComVisible(false)]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/test/Microsoft.Web.Helpers.Test/ReCaptchaTest.cs b/test/Microsoft.Web.Helpers.Test/ReCaptchaTest.cs
new file mode 100644
index 00000000..dbcc3dad
--- /dev/null
+++ b/test/Microsoft.Web.Helpers.Test/ReCaptchaTest.cs
@@ -0,0 +1,251 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Web;
+using System.Web.Helpers.Test;
+using System.Web.TestUtil;
+using System.Web.WebPages.Scope;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace Microsoft.Web.Helpers.Test
+{
+ public class ReCaptchaTest
+ {
+ [Fact]
+ public void ReCaptchaOptionsMissingWhenNoOptionsAndDefaultRendering()
+ {
+ var html = ReCaptcha.GetHtml(GetContext(), "PUBLIC_KEY");
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(
+ @"<script src=""http://www.google.com/recaptcha/api/challenge?k=PUBLIC_KEY"" type=""text/javascript""></script>" +
+ @"<noscript>" +
+ @"<iframe frameborder=""0"" height=""300px"" src=""http://www.google.com/recaptcha/api/noscript?k=PUBLIC_KEY"" width=""500px""></iframe><br/><br/>" +
+ @"<textarea cols=""40"" name=""recaptcha_challenge_field"" rows=""3""></textarea>" +
+ @"<input name=""recaptcha_response_field"" type=""hidden"" value=""manual_challenge""/>" +
+ @"</noscript>",
+ html.ToString());
+ XhtmlAssert.Validate1_0(html, addRoot: true);
+ }
+
+ [Fact]
+ public void ReCaptchaOptionsWhenOneOptionAndDefaultRendering()
+ {
+ var html = ReCaptcha.GetHtml(GetContext(), "PUBLIC_KEY", options: new { theme = "white" });
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(
+ @"<script type=""text/javascript"">var RecaptchaOptions={""theme"":""white""};</script>" +
+ @"<script src=""http://www.google.com/recaptcha/api/challenge?k=PUBLIC_KEY"" type=""text/javascript""></script>" +
+ @"<noscript>" +
+ @"<iframe frameborder=""0"" height=""300px"" src=""http://www.google.com/recaptcha/api/noscript?k=PUBLIC_KEY"" width=""500px""></iframe><br/><br/>" +
+ @"<textarea cols=""40"" name=""recaptcha_challenge_field"" rows=""3""></textarea>" +
+ @"<input name=""recaptcha_response_field"" type=""hidden"" value=""manual_challenge""/>" +
+ @"</noscript>",
+ html.ToString());
+ XhtmlAssert.Validate1_0(html, addRoot: true);
+ }
+
+ [Fact]
+ public void ReCaptchaOptionsWhenMultipleOptionsAndDefaultRendering()
+ {
+ var html = ReCaptcha.GetHtml(GetContext(), "PUBLIC_KEY", options: new { theme = "white", tabindex = 5 });
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(
+ @"<script type=""text/javascript"">var RecaptchaOptions={""theme"":""white"",""tabindex"":5};</script>" +
+ @"<script src=""http://www.google.com/recaptcha/api/challenge?k=PUBLIC_KEY"" type=""text/javascript""></script>" +
+ @"<noscript>" +
+ @"<iframe frameborder=""0"" height=""300px"" src=""http://www.google.com/recaptcha/api/noscript?k=PUBLIC_KEY"" width=""500px""></iframe><br/><br/>" +
+ @"<textarea cols=""40"" name=""recaptcha_challenge_field"" rows=""3""></textarea>" +
+ @"<input name=""recaptcha_response_field"" type=""hidden"" value=""manual_challenge""/>" +
+ @"</noscript>",
+ html.ToString());
+ XhtmlAssert.Validate1_0(html, addRoot: true);
+ }
+
+ [Fact]
+ public void ReCaptchaOptionsWhenMultipleOptionsFromDictionaryAndDefaultRendering()
+ {
+ // verifies that a dictionary will serialize the same as a projection
+ var options = new Dictionary<string, object> { { "theme", "white" }, { "tabindex", 5 } };
+ var html = ReCaptcha.GetHtml(GetContext(), "PUBLIC_KEY", options: options);
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(
+ @"<script type=""text/javascript"">var RecaptchaOptions={""theme"":""white"",""tabindex"":5};</script>" +
+ @"<script src=""http://www.google.com/recaptcha/api/challenge?k=PUBLIC_KEY"" type=""text/javascript""></script>" +
+ @"<noscript>" +
+ @"<iframe frameborder=""0"" height=""300px"" src=""http://www.google.com/recaptcha/api/noscript?k=PUBLIC_KEY"" width=""500px""></iframe><br/><br/>" +
+ @"<textarea cols=""40"" name=""recaptcha_challenge_field"" rows=""3""></textarea>" +
+ @"<input name=""recaptcha_response_field"" type=""hidden"" value=""manual_challenge""/>" +
+ @"</noscript>",
+ html.ToString());
+ XhtmlAssert.Validate1_0(html, addRoot: true);
+ }
+
+ [Fact]
+ public void RenderUsesLastError()
+ {
+ HttpContextBase context = GetContext();
+ ReCaptcha.HandleValidateResponse(context, "false\nincorrect-captcha-sol");
+ var html = ReCaptcha.GetHtml(context, "PUBLIC_KEY");
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(
+ @"<script src=""http://www.google.com/recaptcha/api/challenge?k=PUBLIC_KEY&amp;error=incorrect-captcha-sol"" type=""text/javascript""></script>" +
+ @"<noscript>" +
+ @"<iframe frameborder=""0"" height=""300px"" src=""http://www.google.com/recaptcha/api/noscript?k=PUBLIC_KEY"" width=""500px""></iframe><br/><br/>" +
+ @"<textarea cols=""40"" name=""recaptcha_challenge_field"" rows=""3""></textarea>" +
+ @"<input name=""recaptcha_response_field"" type=""hidden"" value=""manual_challenge""/>" +
+ @"</noscript>",
+ html.ToString());
+ XhtmlAssert.Validate1_0(html, addRoot: true);
+ }
+
+ [Fact]
+ public void RenderWhenConnectionIsSecure()
+ {
+ var html = ReCaptcha.GetHtml(GetContext(isSecure: true), "PUBLIC_KEY");
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(
+ @"<script src=""https://www.google.com/recaptcha/api/challenge?k=PUBLIC_KEY"" type=""text/javascript""></script>" +
+ @"<noscript>" +
+ @"<iframe frameborder=""0"" height=""300px"" src=""https://www.google.com/recaptcha/api/noscript?k=PUBLIC_KEY"" width=""500px""></iframe><br/><br/>" +
+ @"<textarea cols=""40"" name=""recaptcha_challenge_field"" rows=""3""></textarea>" +
+ @"<input name=""recaptcha_response_field"" type=""hidden"" value=""manual_challenge""/>" +
+ @"</noscript>",
+ html.ToString());
+ XhtmlAssert.Validate1_0(html, addRoot: true);
+ }
+
+ [Fact]
+ public void ValidateThrowsWhenRemoteAddressNotAvailable()
+ {
+ HttpContextBase context = GetContext();
+ VirtualPathUtilityBase virtualPathUtility = GetVirtualPathUtility();
+ context.Request.Form["recaptcha_challenge_field"] = "CHALLENGE";
+ context.Request.Form["recaptcha_response_field"] = "RESPONSE";
+
+ Assert.Throws<InvalidOperationException>(() => { ReCaptcha.Validate(context, privateKey: "PRIVATE_KEY", virtualPathUtility: virtualPathUtility).ToString(); }, "The captcha cannot be validated because the remote address was not found in the request.");
+ }
+
+ [Fact]
+ public void ValidateReturnsFalseWhenChallengeNotPosted()
+ {
+ HttpContextBase context = GetContext();
+ VirtualPathUtilityBase virtualPathUtility = GetVirtualPathUtility();
+ context.Request.ServerVariables["REMOTE_ADDR"] = "127.0.0.1";
+
+ Assert.False(ReCaptcha.Validate(context, privateKey: "PRIVATE_KEY", virtualPathUtility: virtualPathUtility));
+ }
+
+ [Fact]
+ public void ValidatePostData()
+ {
+ HttpContextBase context = GetContext();
+ VirtualPathUtilityBase virtualPathUtility = GetVirtualPathUtility();
+ context.Request.ServerVariables["REMOTE_ADDR"] = "127.0.0.1";
+ context.Request.Form["recaptcha_challenge_field"] = "CHALLENGE";
+ context.Request.Form["recaptcha_response_field"] = "RESPONSE";
+
+ Assert.Equal("privatekey=PRIVATE_KEY&remoteip=127.0.0.1&challenge=CHALLENGE&response=RESPONSE",
+ ReCaptcha.GetValidatePostData(context, "PRIVATE_KEY", virtualPathUtility));
+ }
+
+ [Fact]
+ public void ValidatePostDataWhenNoResponse()
+ {
+ // Arrange
+ HttpContextBase context = GetContext();
+ VirtualPathUtilityBase virtualPathUtility = GetVirtualPathUtility();
+ context.Request.ServerVariables["REMOTE_ADDR"] = "127.0.0.1";
+ context.Request.Form["recaptcha_challenge_field"] = "CHALLENGE";
+
+ // Act
+ var validatePostData = ReCaptcha.GetValidatePostData(context, "PRIVATE_KEY", virtualPathUtility);
+
+ // Assert
+ Assert.Equal("privatekey=PRIVATE_KEY&remoteip=127.0.0.1&challenge=CHALLENGE&response=", validatePostData);
+ }
+
+ [Fact]
+ public void ValidateResponseReturnsFalseOnEmptyReCaptchaResponse()
+ {
+ HttpContextBase context = GetContext();
+ Assert.False(ReCaptcha.HandleValidateResponse(context, ""));
+ Assert.Equal(String.Empty, ReCaptcha.GetLastError(context));
+ }
+
+ [Fact]
+ public void ValidateResponseReturnsTrueOnSuccess()
+ {
+ HttpContextBase context = GetContext();
+ Assert.True(ReCaptcha.HandleValidateResponse(context, "true\nsuccess"));
+ Assert.Equal(String.Empty, ReCaptcha.GetLastError(context));
+ }
+
+ [Fact]
+ public void ValidateResponseReturnsFalseOnError()
+ {
+ HttpContextBase context = GetContext();
+ Assert.False(ReCaptcha.HandleValidateResponse(context, "false\nincorrect-captcha-sol"));
+ Assert.Equal("incorrect-captcha-sol", ReCaptcha.GetLastError(context));
+ }
+
+ [Fact]
+ public void ReCaptchaPrivateKeyThowsWhenSetToNull()
+ {
+ Assert.ThrowsArgumentNull(() => ReCaptcha.PrivateKey = null, "value");
+ }
+
+ [Fact]
+ public void ReCaptchaPrivateKeyUsesScopeStorage()
+ {
+ // Arrange
+ var value = "value";
+
+ // Act
+ ReCaptcha.PrivateKey = value;
+
+ // Assert
+ Assert.Equal(ReCaptcha.PrivateKey, value);
+ Assert.Equal(ScopeStorage.CurrentScope[ReCaptcha._privateKey], value);
+ }
+
+ [Fact]
+ public void PublicKeyThowsWhenSetToNull()
+ {
+ Assert.ThrowsArgumentNull(() => ReCaptcha.PublicKey = null, "value");
+ }
+
+ [Fact]
+ public void ReCaptchaPublicKeyUsesScopeStorage()
+ {
+ // Arrange
+ var value = "value";
+
+ // Act
+ ReCaptcha.PublicKey = value;
+
+ // Assert
+ Assert.Equal(ReCaptcha.PublicKey, value);
+ Assert.Equal(ScopeStorage.CurrentScope[ReCaptcha._publicKey], value);
+ }
+
+ private HttpContextBase GetContext(bool isSecure = false)
+ {
+ // mock HttpRequest
+ Mock<HttpRequestBase> requestMock = new Mock<HttpRequestBase>();
+ requestMock.Setup(request => request.IsSecureConnection).Returns(isSecure);
+ requestMock.Setup(request => request.Form).Returns(new NameValueCollection());
+ requestMock.Setup(request => request.ServerVariables).Returns(new NameValueCollection());
+
+ // mock HttpContext
+ Mock<HttpContextBase> contextMock = new Mock<HttpContextBase>();
+ contextMock.Setup(context => context.Items).Returns(new Hashtable());
+ contextMock.Setup(context => context.Request).Returns(requestMock.Object);
+ return contextMock.Object;
+ }
+
+ private static VirtualPathUtilityBase GetVirtualPathUtility()
+ {
+ var virtualPathUtility = new Mock<VirtualPathUtilityBase>();
+ virtualPathUtility.Setup(c => c.ToAbsolute(It.IsAny<string>())).Returns<string>(_ => _);
+
+ return virtualPathUtility.Object;
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Helpers.Test/ThemesTest.cs b/test/Microsoft.Web.Helpers.Test/ThemesTest.cs
new file mode 100644
index 00000000..d89a1d82
--- /dev/null
+++ b/test/Microsoft.Web.Helpers.Test/ThemesTest.cs
@@ -0,0 +1,373 @@
+using System;
+using System.Collections.Generic;
+using System.Web.Hosting;
+using System.Web.WebPages.Scope;
+using System.Web.WebPages.TestUtils;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace Microsoft.Web.Helpers.Test
+{
+ public class ThemesTest
+ {
+ [Fact]
+ public void Initialize_WithBadParams_Throws()
+ {
+ // Arrange
+ var mockVpp = new Mock<VirtualPathProvider>().Object;
+ var scope = new ScopeStorageDictionary();
+
+ // Act and Assert
+ Assert.ThrowsArgumentNullOrEmptyString(() => new ThemesImplementation(mockVpp, scope).Initialize(null, "foo"), "themeDirectory");
+ Assert.ThrowsArgumentNullOrEmptyString(() => new ThemesImplementation(mockVpp, scope).Initialize("", "foo"), "themeDirectory");
+
+ Assert.ThrowsArgumentNullOrEmptyString(() => new ThemesImplementation(mockVpp, scope).Initialize("~/folder", null), "defaultTheme");
+ Assert.ThrowsArgumentNullOrEmptyString(() => new ThemesImplementation(mockVpp, scope).Initialize("~/folder", ""), "defaultTheme");
+ }
+
+ [Fact]
+ public void CurrentThemeThrowsIfAssignedNullOrEmpty()
+ {
+ // Arrange
+ var mockVpp = new Mock<VirtualPathProvider>().Object;
+ var scope = new ScopeStorageDictionary();
+ var themesImpl = new ThemesImplementation(mockVpp, scope);
+
+ // Act and Assert
+ Assert.ThrowsArgumentNullOrEmptyString(() => { themesImpl.CurrentTheme = null; }, "value");
+ Assert.ThrowsArgumentNullOrEmptyString(() => { themesImpl.CurrentTheme = String.Empty; }, "value");
+ }
+
+ [Fact]
+ public void InvokingPropertiesAndMethodsBeforeInitializationThrows()
+ {
+ // Arrange
+ var mockVpp = new Mock<VirtualPathProvider>().Object;
+ var scope = new ScopeStorageDictionary();
+ var themesImpl = new ThemesImplementation(mockVpp, scope);
+
+ // Act and Assert
+ Assert.Throws<InvalidOperationException>(() => themesImpl.CurrentTheme = "Foo",
+ @"You must call the ""Themes.Initialize"" method before you call any other method of the ""Themes"" class.");
+
+ Assert.Throws<InvalidOperationException>(() => { var x = themesImpl.CurrentTheme; },
+ @"You must call the ""Themes.Initialize"" method before you call any other method of the ""Themes"" class.");
+
+ Assert.Throws<InvalidOperationException>(() => { var x = themesImpl.AvailableThemes; },
+ @"You must call the ""Themes.Initialize"" method before you call any other method of the ""Themes"" class.");
+
+ Assert.Throws<InvalidOperationException>(() => { var x = themesImpl.DefaultTheme; },
+ @"You must call the ""Themes.Initialize"" method before you call any other method of the ""Themes"" class.");
+
+ Assert.Throws<InvalidOperationException>(() => { var x = themesImpl.GetResourcePath("baz"); },
+ @"You must call the ""Themes.Initialize"" method before you call any other method of the ""Themes"" class.");
+
+ Assert.Throws<InvalidOperationException>(() => { var x = themesImpl.GetResourcePath("baz", "some-file"); },
+ @"You must call the ""Themes.Initialize"" method before you call any other method of the ""Themes"" class.");
+ }
+
+ [Fact]
+ public void InitializeThrowsIfDefaultThemeDirectoryDoesNotExist()
+ {
+ // Arrange
+ var defaultTheme = "default-theme";
+ var themeDirectory = "theme-directory";
+
+ var scope = new ScopeStorageDictionary();
+ var themesImpl = new ThemesImplementation(GetVirtualPathProvider(themeDirectory, new Dir("not-default-theme")), scope);
+
+ // Act And Assert
+ Assert.ThrowsArgument(
+ () => themesImpl.Initialize(themeDirectory: themeDirectory, defaultTheme: defaultTheme),
+ "defaultTheme",
+ "Unknown theme 'default-theme'. Ensure that a directory labeled 'default-theme' exists under the theme directory.");
+ }
+
+ [Fact]
+ public void ThemesImplUsesScopeStorageToStoreProperties()
+ {
+ // Arrange
+ var defaultTheme = "default-theme";
+ var themeDirectory = "theme-directory";
+
+ var scope = new ScopeStorageDictionary();
+ var themesImpl = new ThemesImplementation(GetVirtualPathProvider(themeDirectory, new Dir(defaultTheme)), scope);
+
+ // Act
+ themesImpl.Initialize(themeDirectory: themeDirectory, defaultTheme: defaultTheme);
+
+ // Ensure Theme use scope storage to store properties
+ Assert.Equal(scope[ThemesImplementation.ThemesInitializedKey], true);
+ Assert.Equal(scope[ThemesImplementation.ThemeDirectoryKey], themeDirectory);
+ Assert.Equal(scope[ThemesImplementation.DefaultThemeKey], defaultTheme);
+ }
+
+ [Fact]
+ public void ThemesImplUsesDefaultThemeWhenNoCurrentThemeIsSpecified()
+ {
+ // Arrange
+ var defaultTheme = "default-theme";
+ var themeDirectory = "theme-directory";
+
+ var scope = new ScopeStorageDictionary();
+ var themesImpl = new ThemesImplementation(GetVirtualPathProvider(themeDirectory, new Dir(defaultTheme)), scope);
+ themesImpl.Initialize(themeDirectory, defaultTheme);
+
+ // Act and Assert
+ // CurrentTheme falls back to default theme when null
+ Assert.Equal(themesImpl.CurrentTheme, defaultTheme);
+ }
+
+ [Fact]
+ public void ThemesImplThrowsIfCurrentThemeIsInvalid()
+ {
+ // Arrange
+ var defaultTheme = "default-theme";
+ var themeDirectory = "theme-directory";
+ var themesImpl = new ThemesImplementation(GetVirtualPathProvider(themeDirectory, new Dir(defaultTheme), new Dir("not-a-random-value")), new ScopeStorageDictionary());
+ themesImpl.Initialize(themeDirectory, defaultTheme);
+
+ // Act and Assert
+ Assert.ThrowsArgument(() => themesImpl.CurrentTheme = "random-value",
+ "value",
+ "Unknown theme 'random-value'. Ensure that a directory labeled 'random-value' exists under the theme directory.");
+ }
+
+ [Fact]
+ public void ThemesImplUsesScopeStorageToStoreCurrentTheme()
+ {
+ // Arrange
+ var defaultTheme = "default-theme";
+ var themeDirectory = "theme-directory";
+ var currentThemeDir = "custom-theme-dir";
+ var scope = new ScopeStorageDictionary();
+ var themesImpl = new ThemesImplementation(GetVirtualPathProvider(themeDirectory, new Dir(defaultTheme), new Dir("custom-theme-dir")), scope);
+
+ // Act
+ themesImpl.Initialize(themeDirectory, defaultTheme);
+ themesImpl.CurrentTheme = currentThemeDir;
+
+ // Assert
+ Assert.Equal(scope[ThemesImplementation.CurrentThemeKey], currentThemeDir);
+ }
+
+ [Fact]
+ public void GetResourcePathThrowsIfCurrentDirectoryIsNull()
+ {
+ // Arrange
+ var themesImpl = new ThemesImplementation(scopeStorage: new ScopeStorageDictionary(),
+ vpp: GetVirtualPathProvider("themes", new Dir("default"), new Dir("mobile"), new Dir(@"mobile", "wp7.css")));
+ themesImpl.Initialize("themes", "default");
+
+ // Act and Assert
+ Assert.ThrowsArgumentNull(() => themesImpl.GetResourcePath(folder: null, fileName: "wp7.css"), "folder");
+ }
+
+ [Fact]
+ public void GetResourcePathThrowsIfFileNameIsNullOrEmpty()
+ {
+ // Arrange
+ var themesImpl = new ThemesImplementation(scopeStorage: new ScopeStorageDictionary(),
+ vpp: GetVirtualPathProvider("themes", new Dir("default"), new Dir("mobile"), new Dir(@"mobile", "wp7.css")));
+ themesImpl.Initialize("themes", "default");
+
+ // Act and Assert
+ Assert.ThrowsArgumentNullOrEmptyString(() => themesImpl.GetResourcePath(folder: String.Empty, fileName: null), "fileName");
+ Assert.ThrowsArgumentNullOrEmptyString(() => themesImpl.GetResourcePath(folder: String.Empty, fileName: String.Empty), "fileName");
+ }
+
+ [Fact]
+ public void GetResourcePathReturnsItemFromThemeRootIfAvailable()
+ {
+ // Arrange
+ var themesImpl = new ThemesImplementation(scopeStorage: new ScopeStorageDictionary(),
+ vpp: GetVirtualPathProvider("themes", new Dir("default"), new Dir("mobile"), new Dir(@"mobile", "wp7.css")));
+ themesImpl.Initialize("themes", "default");
+
+ // Act
+ themesImpl.CurrentTheme = "mobile";
+ var themePath = themesImpl.GetResourcePath(fileName: "wp7.css");
+
+ // Assert
+ Assert.Equal(themePath, @"themes/mobile/wp7.css");
+ }
+
+ [Fact]
+ public void GetResourcePathReturnsItemFromCurrentThemeDirectoryIfAvailable()
+ {
+ // Arrange
+ var themesImpl = new ThemesImplementation(scopeStorage: new ScopeStorageDictionary(),
+ vpp: GetVirtualPathProvider("themes", new Dir("default"), new Dir("mobile"), new Dir(@"mobile\styles", "wp7.css"), new Dir(@"default\styles", "main.css")));
+ themesImpl.Initialize("themes", "default");
+
+ // Act
+ themesImpl.CurrentTheme = "mobile";
+ var themePath = themesImpl.GetResourcePath(folder: "styles", fileName: "wp7.css");
+
+ // Assert
+ Assert.Equal(themePath, @"themes/mobile/styles/wp7.css");
+ }
+
+ [Fact]
+ public void GetResourcePathReturnsItemFromDefaultThemeDirectoryIfNotFoundInCurrentThemeDirectory()
+ {
+ // Arrange
+ var themesImpl = new ThemesImplementation(scopeStorage: new ScopeStorageDictionary(),
+ vpp: GetVirtualPathProvider("themes", new Dir("default"), new Dir("mobile"), new Dir(@"mobile\styles", "wp7.css"), new Dir(@"default\styles", "main.css")));
+ themesImpl.Initialize("themes", "default");
+
+ // Act
+ themesImpl.CurrentTheme = "mobile";
+ var themePath = themesImpl.GetResourcePath(folder: "styles", fileName: "main.css");
+
+ // Assert
+ Assert.Equal(themePath, @"themes/default/styles/main.css");
+ }
+
+ [Fact]
+ public void GetResourcePathReturnsNullIfDirectoryDoesNotExist()
+ {
+ // Arrange
+ var themesImpl = new ThemesImplementation(scopeStorage: new ScopeStorageDictionary(),
+ vpp: GetVirtualPathProvider("themes", new Dir("default"), new Dir("mobile"), new Dir(@"mobile\styles", "wp7.css"), new Dir(@"default\styles", "main.css")));
+ themesImpl.Initialize("themes", "default");
+
+ // Act
+ themesImpl.CurrentTheme = "mobile";
+ var themePath = themesImpl.GetResourcePath(folder: "does-not-exist", fileName: "main.css");
+
+ // Assert
+ Assert.Null(themePath);
+ }
+
+ [Fact]
+ public void GetResourcePathReturnsNullIfItemNotFoundInCurrentAndDefaultThemeDirectories()
+ {
+ // Arrange
+ var themesImpl = new ThemesImplementation(scopeStorage: new ScopeStorageDictionary(),
+ vpp: GetVirtualPathProvider("themes", new Dir("default"), new Dir("mobile"), new Dir(@"mobile\styles", "wp7.css"), new Dir(@"default\styles", "main.css")));
+ themesImpl.Initialize("themes", "default");
+
+ // Act
+ themesImpl.CurrentTheme = "mobile";
+ var themePath = themesImpl.GetResourcePath(folder: "styles", fileName: "awesome-blinking-text.css");
+
+ // Assert
+ Assert.Null(themePath);
+ }
+
+ [Fact]
+ public void AvaliableThemesReturnsTopLevelDirectoriesUnderThemeDirectory()
+ {
+ // Arrange
+ var themesImpl = new ThemesImplementation(scopeStorage: new ScopeStorageDictionary(),
+ vpp: GetVirtualPathProvider("themes", new Dir("default"), new Dir("mobile"), new Dir("rotary-phone")));
+ // Act
+ themesImpl.Initialize("themes", "default");
+ var themes = themesImpl.AvailableThemes;
+
+ // Assert
+ Assert.Equal(3, themes.Count);
+ Assert.Equal(themes[0], "default");
+ Assert.Equal(themes[1], "mobile");
+ Assert.Equal(themes[2], "rotary-phone");
+ }
+
+ /// <remarks>
+ /// // folder structure:
+ /// // /root
+ /// // /foo
+ /// // /bar.cs
+ /// // testing that a file specified as foo/bar in folder root will return null
+ /// </remarks>
+ [Fact]
+ public void FileWithSlash_ReturnsNull()
+ {
+ // Arrange
+ var themesImpl = new ThemesImplementation(scopeStorage: new ScopeStorageDictionary(),
+ vpp: GetVirtualPathProvider("themes", new Dir("default"), new Dir("root"), new Dir(@"root\foo", "wp7.css"), new Dir(@"default\styles", "main.css")));
+
+ // Act
+ var actual = themesImpl.FindMatchingFile("root", "foo/bar.cs");
+
+ // Assert
+ Assert.Null(actual);
+ }
+
+ [Fact]
+ public void DirectoryWithNoFilesReturnsNull()
+ {
+ // Arrange
+ var themesImpl = new ThemesImplementation(scopeStorage: new ScopeStorageDictionary(),
+ vpp: GetVirtualPathProvider("themes", new Dir("default"), new Dir("empty-dir")));
+
+ // Act
+ var actual = themesImpl.FindMatchingFile(@"themes\empty-dir", "main.css");
+
+ // Assert
+ Assert.Null(actual);
+ }
+
+ [Fact]
+ public void MatchingFiles_ReturnsCorrectFile()
+ {
+ // Arrange
+ var themesImpl = new ThemesImplementation(scopeStorage: new ScopeStorageDictionary(),
+ vpp: GetVirtualPathProvider("themes", new Dir(@"nomatchingfiles", "foo.cs")));
+
+ // Act
+ var bar = themesImpl.FindMatchingFile(@"themes\nomatchingfiles", "bar.cs");
+ var foo = themesImpl.FindMatchingFile(@"themes\nomatchingfiles", "foo.cs");
+
+ // Assert
+ Assert.Null(bar);
+ Assert.Equal(@"themes/nomatchingfiles/foo.cs", foo);
+ }
+
+ private static VirtualPathProvider GetVirtualPathProvider(string themeRoot, params Dir[] fileSystem)
+ {
+ var mockVpp = new Mock<VirtualPathProvider>();
+ var dirRoot = new Mock<VirtualDirectory>(themeRoot);
+
+ var themeDirectories = new List<VirtualDirectory>();
+ foreach (var directory in fileSystem)
+ {
+ var dir = new Mock<VirtualDirectory>(directory.Name);
+ var directoryPath = themeRoot + '\\' + directory.Name;
+ dir.SetupGet(d => d.Name).Returns(directory.Name);
+ mockVpp.Setup(c => c.GetDirectory(It.Is<string>(p => p.Equals(directoryPath, StringComparison.OrdinalIgnoreCase)))).Returns(dir.Object);
+
+ var fileList = new List<VirtualFile>();
+ foreach (var item in directory.Files)
+ {
+ var filePath = directoryPath + '\\' + item;
+ var file = new Mock<VirtualFile>(filePath);
+ file.SetupGet(f => f.Name).Returns(item);
+ fileList.Add(file.Object);
+ }
+
+ dir.SetupGet(c => c.Files).Returns(fileList);
+ themeDirectories.Add(dir.Object);
+ }
+
+ dirRoot.SetupGet(c => c.Directories).Returns(themeDirectories);
+ mockVpp.Setup(c => c.GetDirectory(themeRoot)).Returns(dirRoot.Object);
+
+ return mockVpp.Object;
+ }
+
+ private class Dir
+ {
+ public Dir(string name, params string[] files)
+ {
+ Name = name;
+ Files = files;
+ }
+
+ public string Name { get; private set; }
+
+ public IEnumerable<string> Files { get; private set; }
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Helpers.Test/TwitterTest.cs b/test/Microsoft.Web.Helpers.Test/TwitterTest.cs
new file mode 100644
index 00000000..ca0d0d24
--- /dev/null
+++ b/test/Microsoft.Web.Helpers.Test/TwitterTest.cs
@@ -0,0 +1,481 @@
+using System.Web;
+using System.Web.Helpers;
+using System.Web.Helpers.Test;
+using System.Web.TestUtil;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace Microsoft.Web.Helpers.Test
+{
+ public class TwitterTest
+ {
+ /// <summary>
+ ///A test for Profile
+ ///</summary>
+ [Fact]
+ public void Profile_ReturnsValidData()
+ {
+ // Arrange
+ string twitterUserName = "my-userName";
+ int width = 100;
+ int height = 100;
+ string backgroundShellColor = "my-backgroundShellColor";
+ string shellColor = "my-shellColor";
+ string tweetsBackgroundColor = "tweetsBackgroundColor";
+ string tweetsColor = "tweets-color";
+ string tweetsLinksColor = "tweets Linkcolor";
+ bool scrollBar = false;
+ bool loop = false;
+ bool live = false;
+ bool hashTags = false;
+ bool timestamp = false;
+ bool avatars = false;
+ var behavior = "all";
+ int searchInterval = 10;
+
+ // Act
+ string actual = Twitter.Profile(twitterUserName, width, height, backgroundShellColor, shellColor, tweetsBackgroundColor, tweetsColor, tweetsLinksColor,
+ 5, scrollBar, loop, live, hashTags, timestamp, avatars, behavior, searchInterval).ToString();
+
+ // Assert
+ var json = GetTwitterJSObject(actual);
+ Assert.Equal(json.type, "profile");
+ Assert.Equal(json.interval, 1000 * searchInterval);
+ Assert.Equal(json.width.ToString(), width.ToString());
+ Assert.Equal(json.height.ToString(), height.ToString());
+ Assert.Equal(json.theme.shell.background, backgroundShellColor);
+ Assert.Equal(json.theme.shell.color, shellColor);
+ Assert.Equal(json.theme.tweets.background, tweetsBackgroundColor);
+ Assert.Equal(json.theme.tweets.color, tweetsColor);
+ Assert.Equal(json.theme.tweets.links, tweetsLinksColor);
+ Assert.Equal(json.features.scrollbar, scrollBar);
+ Assert.Equal(json.features.loop, loop);
+ Assert.Equal(json.features.live, live);
+ Assert.Equal(json.features.hashtags, hashTags);
+ Assert.Equal(json.features.avatars, avatars);
+ Assert.Equal(json.features.behavior, behavior.ToLowerInvariant());
+ Assert.True(actual.Contains(".setUser('my-userName')"));
+ }
+
+ [Fact]
+ public void ProfileJSEncodesParameters()
+ {
+ // Arrange
+ string twitterUserName = "\"my-userName\'";
+ string backgroundShellColor = "\\foo";
+ string shellColor = "<shellColor>\\";
+ string tweetsBackgroundColor = "<tweetsBackgroundColor";
+ string tweetsColor = "<tweetsColor>";
+ string tweetsLinksColor = "<tweetsLinkColor>";
+
+ // Act
+ string actual = Twitter.Profile(twitterUserName, 100, 100, backgroundShellColor, shellColor, tweetsBackgroundColor, tweetsColor, tweetsLinksColor).ToString();
+
+ // Assert
+ Assert.True(actual.Contains("background: '\\\\foo"));
+ Assert.True(actual.Contains("color: '\\u003cshellColor\\u003e\\\\"));
+ Assert.True(actual.Contains("background: '\\u003ctweetsBackgroundColor"));
+ Assert.True(actual.Contains("color: '\\u003ctweetsColor\\u003e"));
+ Assert.True(actual.Contains("links: '\\u003ctweetsLinkColor\\u003e"));
+ Assert.True(actual.Contains(".setUser('\\\"my-userName\\u0027')"));
+ }
+
+ [Fact]
+ public void Search_ReturnsValidData()
+ {
+ // Arrange
+ string search = "awesome-search-term";
+ int width = 100;
+ int height = 100;
+ string title = "cust-title";
+ string caption = "some caption";
+ string backgroundShellColor = "background-shell-color";
+ string shellColor = "shellColorValue";
+ string tweetsBackgroundColor = "tweetsBackgroundColor";
+ string tweetsColor = "tweetsColorVal";
+ string tweetsLinksColor = "tweetsLinkColor";
+ bool scrollBar = false;
+ bool loop = false;
+ bool live = true;
+ bool hashTags = false;
+ bool timestamp = true;
+ bool avatars = true;
+ bool topTweets = true;
+ var behavior = "default";
+ int searchInterval = 10;
+
+ // Act
+ string actual = Twitter.Search(search, width, height, title, caption, backgroundShellColor, shellColor, tweetsBackgroundColor, tweetsColor, tweetsLinksColor, scrollBar,
+ loop, live, hashTags, timestamp, avatars, topTweets, behavior, searchInterval).ToString();
+
+ // Assert
+ var json = GetTwitterJSObject(actual);
+ Assert.Equal(json.type, "search");
+ Assert.Equal(json.search, search);
+ Assert.Equal(json.interval, 1000 * searchInterval);
+ Assert.Equal(json.title, title);
+ Assert.Equal(json.subject, caption);
+ Assert.Equal(json.width.ToString(), width.ToString());
+ Assert.Equal(json.height.ToString(), height.ToString());
+ Assert.Equal(json.theme.shell.background, backgroundShellColor);
+ Assert.Equal(json.theme.shell.color, shellColor);
+ Assert.Equal(json.theme.tweets.background, tweetsBackgroundColor);
+ Assert.Equal(json.theme.tweets.color, tweetsColor);
+ Assert.Equal(json.theme.tweets.links, tweetsLinksColor);
+ Assert.Equal(json.features.scrollbar, scrollBar);
+ Assert.Equal(json.features.loop, loop);
+ Assert.Equal(json.features.live, live);
+ Assert.Equal(json.features.hashtags, hashTags);
+ Assert.Equal(json.features.avatars, avatars);
+ Assert.Equal(json.features.toptweets, topTweets);
+ Assert.Equal(json.features.behavior, behavior.ToLowerInvariant());
+ }
+
+ [Fact]
+ public void SearchJavascriptEncodesParameters()
+ {
+ // Arrange
+ string search = "<script>";
+ int width = 100;
+ int height = 100;
+ string title = "'title'";
+ string caption = "<caption>";
+ string backgroundShellColor = "\\foo";
+ string shellColor = "<shellColor>\\";
+ string tweetsBackgroundColor = "<tweetsBackgroundColor";
+ string tweetsColor = "<tweetsColor>";
+ string tweetsLinksColor = "<tweetsLinkColor>";
+
+ // Act
+ string actual = Twitter.Search(search, width, height, title, caption, backgroundShellColor, shellColor, tweetsBackgroundColor, tweetsColor, tweetsLinksColor).ToString();
+
+ // Assert
+ Assert.True(actual.Contains("search: '\\u003cscript\\u003e'"));
+ Assert.True(actual.Contains("title: '\\u0027title\\u0027'"));
+ Assert.True(actual.Contains("subject: '\\u003ccaption\\u003e'"));
+ Assert.True(actual.Contains("background: '\\\\foo"));
+ Assert.True(actual.Contains("color: '\\u003cshellColor\\u003e\\\\"));
+ Assert.True(actual.Contains("background: '\\u003ctweetsBackgroundColor"));
+ Assert.True(actual.Contains("color: '\\u003ctweetsColor\\u003e"));
+ Assert.True(actual.Contains("links: '\\u003ctweetsLinkColor\\u003e"));
+ }
+
+ [Fact]
+ public void SearchWithInvalidArgs_ThrowsArgumentException()
+ {
+ Assert.ThrowsArgumentNullOrEmptyString(() => Twitter.Search(null).ToString(), "searchQuery");
+ }
+
+ [Fact]
+ public void ProfileWithInvalidArgs_ThrowsArgumentException()
+ {
+ Assert.ThrowsArgumentNullOrEmptyString(() => Twitter.Profile(null).ToString(), "userName");
+ }
+
+ [Fact]
+ public void SearchRendersRendersValidXhtml()
+ {
+ string result = "<html> <head> \n <title> </title> \n </head> \n <body> \n" +
+ Twitter.Search("any<>term") +
+ "\n </body> \n </html>";
+ HtmlString htmlResult = new HtmlString(result);
+ XhtmlAssert.Validate1_1(htmlResult);
+ }
+
+ [Fact]
+ public void SearchEncodesSearchTerms()
+ {
+ // Act
+ string result = Twitter.Search("'any term'", backgroundShellColor: "\"bad-color").ToString();
+
+ // Assert
+ Assert.True(result.Contains(@"background: '\""bad-color',"));
+ //Assert.True(result.Contains(@"search: @"'\u0027any term\u0027'","));
+ }
+
+ [Fact]
+ public void ProfileRendersRendersValidXhtml()
+ {
+ string result = "<html> <head> \n <title> </title> \n </head> \n <body> \n" +
+ Twitter.Profile("any<>Name") +
+ "\n </body> \n </html>";
+ HtmlString htmlResult = new HtmlString(result);
+ XhtmlAssert.Validate1_1(htmlResult);
+ }
+
+ [Fact]
+ public void ProfileEncodesSearchTerms()
+ {
+ // Act
+ string result = Twitter.Profile("'some user'", backgroundShellColor: "\"malformed-color").ToString();
+
+ // Assert
+ Assert.True(result.Contains(@"background: '\""malformed-color'"));
+ Assert.True(result.Contains("setUser('\\u0027some user\\u0027')"));
+ }
+
+ [Fact]
+ public void TweetButtonWithDefaultAttributes()
+ {
+ // Arrange
+ string expected = @"<a href=""http://twitter.com/share"" class=""twitter-share-button"" data-count=""vertical"">Tweet</a><script type=""text/javascript"" src=""http://platform.twitter.com/widgets.js""></script>";
+
+ // Act
+ string result = Twitter.TweetButton().ToString();
+
+ // Assert
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(result, expected);
+ }
+
+ [Fact]
+ public void TweetButtonWithSpeicifedAttributes()
+ {
+ // Arrange
+ string expected = @"<a href=""http://twitter.com/share"" class=""twitter-share-button"" data-text=""tweet-text"" data-url=""http://www.microsoft.com"" data-via=""userName"" data-related=""related-userName:rel-desc"" data-count=""none"">my-share-text</a><script type=""text/javascript"" src=""http://platform.twitter.com/widgets.js""></script>";
+
+ // Act
+ string result = Twitter.TweetButton("none", shareText: "my-share-text", tweetText: "tweet-text", url: "http://www.microsoft.com", language: "en", userName: "userName", relatedUserName: "related-userName", relatedUserDescription: "rel-desc").ToString();
+
+ // Assert
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(result, expected);
+ }
+
+ [Fact]
+ public void TweetButtonHtmlEncodesValues()
+ {
+ // Arrange
+ string expected = @"<a href=""http://twitter.com/share"" class=""twitter-share-button"" data-text=""&lt;tweet-text>"" data-url=""&lt;http://www.microsoft.com>"" data-via=""&lt;userName>"" data-related=""&lt;related-userName>:&lt;rel-desc>"" data-count=""none"">&lt;Tweet</a><script type=""text/javascript"" src=""http://platform.twitter.com/widgets.js""></script>";
+ // Act
+ string result = Twitter.TweetButton("none", shareText: "<Tweet", tweetText: "<tweet-text>", url: "<http://www.microsoft.com>", language: "en", userName: "<userName>", relatedUserName: "<related-userName>", relatedUserDescription: "<rel-desc>").ToString();
+
+ // Assert
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(result, expected);
+ }
+
+ [Fact]
+ public void FollowButtonWithDefaultParameters()
+ {
+ // Arrange
+ string expected = @"<a href=""http://www.twitter.com/my-twitter-userName""><img src=""http://twitter-badges.s3.amazonaws.com/follow_me-a.png"" alt=""Follow my-twitter-userName on Twitter""/></a>";
+
+ // Act
+ string result = Twitter.FollowButton("my-twitter-userName").ToString();
+
+ // Assert
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(result, expected);
+ }
+
+ [Fact]
+ public void FollowButtonWithSpecifiedParmeters()
+ {
+ // Arrange
+ string expected = @"<a href=""http://www.twitter.com/my-twitter-userName""><img src=""http://twitter-badges.s3.amazonaws.com/t_logo-b.png"" alt=""Follow my-twitter-userName on Twitter""/></a>";
+
+ // Act
+ string result = Twitter.FollowButton("my-twitter-userName", "t_logo", "b").ToString();
+
+ // Assert
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(result, expected);
+ }
+
+ [Fact]
+ public void FollowButtonEncodesParameters()
+ {
+ // Arrange
+ string expected = @"<a href=""http://www.twitter.com/http%3a%2f%2fmy-twitter-userName%3cscript""><img src=""http://twitter-badges.s3.amazonaws.com/t_logo-b.png"" alt=""Follow http://my-twitter-userName&lt;script on Twitter""/></a>";
+
+ // Act
+ string result = Twitter.FollowButton("http://my-twitter-userName<script", "t_logo", "b").ToString();
+
+ // Assert
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(result, expected);
+ }
+
+ [Fact]
+ public void FavesReturnsValidData()
+ {
+ // Arrange
+ string twitterUserName = "my-userName";
+ string title = "my-title";
+ string caption = "my-caption";
+ int width = 100;
+ int height = 100;
+ string backgroundShellColor = "my-backgroundShellColor";
+ string shellColor = "my-shellColor";
+ string tweetsBackgroundColor = "tweetsBackgroundColor";
+ string tweetsColor = "tweets-color";
+ string tweetsLinksColor = "tweets Linkcolor";
+ int numTweets = 4;
+ bool scrollBar = false;
+ bool loop = false;
+ bool live = false;
+ bool hashTags = false;
+ bool timestamp = false;
+ bool avatars = false;
+ var behavior = "all";
+ int searchInterval = 10;
+
+ // Act
+ string actual = Twitter.Faves(twitterUserName, width, height, title, caption, backgroundShellColor, shellColor, tweetsBackgroundColor, tweetsColor, tweetsLinksColor,
+ numTweets, scrollBar, loop, live, hashTags, timestamp, avatars, "all", searchInterval).ToString();
+
+ // Assert
+ var json = GetTwitterJSObject(actual);
+ Assert.Equal(json.type, "faves");
+ Assert.Equal(json.interval, 1000 * searchInterval);
+ Assert.Equal(json.width.ToString(), width.ToString());
+ Assert.Equal(json.height.ToString(), height.ToString());
+ Assert.Equal(json.title, title);
+ Assert.Equal(json.subject, caption);
+ Assert.Equal(json.theme.shell.background, backgroundShellColor);
+ Assert.Equal(json.theme.shell.color, shellColor);
+ Assert.Equal(json.theme.tweets.background, tweetsBackgroundColor);
+ Assert.Equal(json.theme.tweets.color, tweetsColor);
+ Assert.Equal(json.theme.tweets.links, tweetsLinksColor);
+ Assert.Equal(json.features.scrollbar, scrollBar);
+ Assert.Equal(json.features.loop, loop);
+ Assert.Equal(json.features.live, live);
+ Assert.Equal(json.features.hashtags, hashTags);
+ Assert.Equal(json.features.avatars, avatars);
+ Assert.Equal(json.features.behavior, behavior.ToLowerInvariant());
+ Assert.True(actual.Contains(".setUser('my-userName')"));
+ }
+
+ [Fact]
+ public void FavesJavascriptEncodesParameters()
+ {
+ // Arrange
+ string twitterUserName = "<my-userName>";
+ string title = "<my-title>";
+ string caption = "<my-caption>";
+ int width = 100;
+ int height = 100;
+ string backgroundShellColor = "<my-backgroundShellColor>";
+ string shellColor = "<my-shellColor>";
+ string tweetsBackgroundColor = "<tweetsBackgroundColor>";
+ string tweetsColor = "<tweets-color>";
+ string tweetsLinksColor = "<tweets Linkcolor>";
+
+ // Act
+ string actual = Twitter.Faves(twitterUserName, width, height, title, caption, backgroundShellColor, shellColor, tweetsBackgroundColor, tweetsColor, tweetsLinksColor).ToString();
+
+ // Assert
+ Assert.True(actual.Contains("title: '\\u003cmy-title\\u003e'"));
+ Assert.True(actual.Contains("subject: '\\u003cmy-caption\\u003e'"));
+ Assert.True(actual.Contains("background: '\\u003cmy-backgroundShellColor\\u003e'"));
+ Assert.True(actual.Contains("color: '\\u003cmy-shellColor\\u003e'"));
+ Assert.True(actual.Contains("background: '\\u003ctweetsBackgroundColor\\u003e'"));
+ Assert.True(actual.Contains("color: '\\u003ctweets-color\\u003e"));
+ Assert.True(actual.Contains("links: '\\u003ctweets Linkcolor\\u003e'"));
+ Assert.True(actual.Contains(".setUser('\\u003cmy-userName\\u003e')"));
+ }
+
+ [Fact]
+ public void FavesRendersRendersValidXhtml()
+ {
+ string result = "<html> <head> \n <title> </title> \n </head> \n <body> \n" +
+ Twitter.Faves("any<>Name") +
+ "\n </body> \n </html>";
+ HtmlString htmlResult = new HtmlString(result);
+ XhtmlAssert.Validate1_1(htmlResult);
+ }
+
+ [Fact]
+ public void ListReturnsValidData()
+ {
+ // Arrange
+ string twitterUserName = "my-userName";
+ string list = "my-list";
+ string title = "my-title";
+ string caption = "my-caption";
+ int width = 100;
+ int height = 100;
+ string backgroundShellColor = "my-backgroundShellColor";
+ string shellColor = "my-shellColor";
+ string tweetsBackgroundColor = "tweetsBackgroundColor";
+ string tweetsColor = "tweets-color";
+ string tweetsLinksColor = "tweets Linkcolor";
+ int numTweets = 4;
+ bool scrollBar = false;
+ bool loop = false;
+ bool live = false;
+ bool hashTags = false;
+ bool timestamp = false;
+ bool avatars = false;
+ var behavior = "all";
+ int searchInterval = 10;
+
+ // Act
+ string actual = Twitter.List(twitterUserName, list, width, height, title, caption, backgroundShellColor, shellColor, tweetsBackgroundColor, tweetsColor, tweetsLinksColor,
+ numTweets, scrollBar, loop, live, hashTags, timestamp, avatars, "all", searchInterval).ToString();
+
+ // Assert
+ var json = GetTwitterJSObject(actual);
+ Assert.Equal(json.type, "list");
+ Assert.Equal(json.interval, 1000 * searchInterval);
+ Assert.Equal(json.width.ToString(), width.ToString());
+ Assert.Equal(json.height.ToString(), height.ToString());
+ Assert.Equal(json.title, title);
+ Assert.Equal(json.subject, caption);
+ Assert.Equal(json.theme.shell.background, backgroundShellColor);
+ Assert.Equal(json.theme.shell.color, shellColor);
+ Assert.Equal(json.theme.tweets.background, tweetsBackgroundColor);
+ Assert.Equal(json.theme.tweets.color, tweetsColor);
+ Assert.Equal(json.theme.tweets.links, tweetsLinksColor);
+ Assert.Equal(json.features.scrollbar, scrollBar);
+ Assert.Equal(json.features.loop, loop);
+ Assert.Equal(json.features.live, live);
+ Assert.Equal(json.features.hashtags, hashTags);
+ Assert.Equal(json.features.avatars, avatars);
+ Assert.Equal(json.features.behavior, behavior.ToLowerInvariant());
+ Assert.True(actual.Contains(".setList('my-userName', 'my-list')"));
+ }
+
+ [Fact]
+ public void ListJavascriptEncodesParameters()
+ {
+ // Arrange
+ string twitterUserName = "<my-userName>";
+ string list = "<my-list>";
+ string title = "<my-title>";
+ string caption = "<my-caption>";
+ int width = 100;
+ int height = 100;
+ string backgroundShellColor = "<my-backgroundShellColor>";
+ string shellColor = "<my-shellColor>";
+ string tweetsBackgroundColor = "<tweetsBackgroundColor>";
+ string tweetsColor = "<tweets-color>";
+ string tweetsLinksColor = "<tweets Linkcolor>";
+
+ // Act
+ string actual = Twitter.List(twitterUserName, list, width, height, title, caption, backgroundShellColor, shellColor, tweetsBackgroundColor, tweetsColor, tweetsLinksColor).ToString();
+
+ // Assert
+ Assert.True(actual.Contains("title: '\\u003cmy-title\\u003e'"));
+ Assert.True(actual.Contains("subject: '\\u003cmy-caption\\u003e'"));
+ Assert.True(actual.Contains("background: '\\u003cmy-backgroundShellColor\\u003e'"));
+ Assert.True(actual.Contains("color: '\\u003cmy-shellColor\\u003e'"));
+ Assert.True(actual.Contains("background: '\\u003ctweetsBackgroundColor\\u003e'"));
+ Assert.True(actual.Contains("color: '\\u003ctweets-color\\u003e"));
+ Assert.True(actual.Contains("links: '\\u003ctweets Linkcolor\\u003e'"));
+ Assert.True(actual.Contains(".setList('\\u003cmy-userName\\u003e', '\\u003cmy-list\\u003e')"));
+ }
+
+ [Fact]
+ public void ListRendersRendersValidXhtml()
+ {
+ string result = "<html> <head> \n <title> </title> \n </head> \n <body> \n" +
+ Twitter.List("any<>Name", "my-list") +
+ "\n </body> \n </html>";
+ HtmlString htmlResult = new HtmlString(result);
+ XhtmlAssert.Validate1_1(htmlResult);
+ }
+
+ private static dynamic GetTwitterJSObject(string twitterOutput)
+ {
+ const string startString = "Widget(";
+ int start = twitterOutput.IndexOf(startString) + startString.Length, end = twitterOutput.IndexOf(')');
+ return Json.Decode(twitterOutput.Substring(start, end - start));
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Helpers.Test/UrlBuilderTest.cs b/test/Microsoft.Web.Helpers.Test/UrlBuilderTest.cs
new file mode 100644
index 00000000..80240253
--- /dev/null
+++ b/test/Microsoft.Web.Helpers.Test/UrlBuilderTest.cs
@@ -0,0 +1,413 @@
+using System;
+using System.Collections;
+using System.IO;
+using System.Reflection;
+using System.Text;
+using System.Web;
+using System.Web.UI;
+using System.Web.WebPages;
+using Moq;
+using Xunit;
+
+namespace Microsoft.Web.Helpers.Test
+{
+ public class UrlBuilderTest
+ {
+ private static TestVirtualPathUtility _virtualPathUtility = new TestVirtualPathUtility();
+
+ [Fact]
+ public void UrlBuilderUsesPathAsIsIfPathIsAValidUri()
+ {
+ // Arrange
+ var pagePath = "http://www.test.com/page-path";
+
+ // Act
+ var builder = new UrlBuilder(GetContext(), _virtualPathUtility, pagePath, null);
+
+ // Assert
+ Assert.Equal("http://www.test.com/page-path", builder.Path);
+ }
+
+ [Fact]
+ public void UrlBuilderUsesQueryComponentsIfPathIsAValidUri()
+ {
+ // Arrange
+ var pagePath = "http://www.test.com/page-path.vbhtml?param=value&baz=biz";
+
+ // Act
+ var builder = new UrlBuilder(GetContext(), _virtualPathUtility, pagePath, null);
+
+ // Assert
+ Assert.Equal("http://www.test.com/page-path.vbhtml", builder.Path);
+ Assert.Equal("?param=value&baz=biz", builder.QueryString);
+ }
+
+ [Fact]
+ public void UrlBuilderUsesUsesObjectParameterAsQueryString()
+ {
+ // Arrange
+ var pagePath = "http://www.test.com/page-path.vbhtml?param=value&baz=biz";
+
+ // Act
+ var builder = new UrlBuilder(GetContext(), _virtualPathUtility, pagePath, null);
+
+ // Assert
+ Assert.Equal("http://www.test.com/page-path.vbhtml", builder.Path);
+ Assert.Equal("?param=value&baz=biz", builder.QueryString);
+ }
+
+ [Fact]
+ public void UrlBuilderAppendsObjectParametersToPathWithQueryString()
+ {
+ // Arrange
+ var pagePath = "http://www.test.com/page-path.vbhtml?param=value&baz=biz";
+
+ // Act
+ var builder = new UrlBuilder(GetContext(), _virtualPathUtility, pagePath, new { param2 = "param2val" });
+
+ // Assert
+ Assert.Equal("http://www.test.com/page-path.vbhtml", builder.Path);
+ Assert.Equal("?param=value&baz=biz&param2=param2val", builder.QueryString);
+ }
+
+ [Fact]
+ public void UrlBuilderWithVirtualPathUsesVirtualPathUtility()
+ {
+ // Arrange
+ var pagePath = "~/page";
+
+ // Act
+ var builder = new UrlBuilder(GetContext(), _virtualPathUtility, pagePath, null);
+
+ // Assert
+ Assert.Equal("page", builder.Path);
+ }
+
+ [Fact]
+ public void UrlBuilderDoesNotResolvePathIfContextIsNull()
+ {
+ // Arrange
+ var pagePath = "~/foo/bar";
+
+ // Act
+ var builder = new UrlBuilder(null, _virtualPathUtility, pagePath, null);
+
+ // Assert
+ Assert.Equal(pagePath, builder.Path);
+ }
+
+ [Fact]
+ public void UrlBuilderSetsPathToNullIfContextIsNullAndPagePathIsNotSpecified()
+ {
+ // Act
+ var builder = new UrlBuilder(null, null, null, null);
+
+ // Assert
+ Assert.Null(builder.Path);
+ Assert.True(String.IsNullOrEmpty(builder.ToString()));
+ }
+
+ [Fact]
+ public void UrlBuilderWithVirtualPathAppendsObjectAsQueryString()
+ {
+ // Arrange
+ var pagePath = "~/page";
+
+ // Act
+ var builder = new UrlBuilder(GetContext(), _virtualPathUtility, pagePath, new { Foo = "bar", baz = "qux" });
+
+ // Assert
+ Assert.Equal("page", builder.Path);
+ Assert.Equal("?Foo=bar&baz=qux", builder.QueryString);
+ }
+
+ [Fact]
+ public void UrlBuilderWithVirtualPathExtractsQueryStringParameterFromPath()
+ {
+ // Arrange
+ var pagePath = "~/dir/page?someparam=value";
+
+ // Act
+ var builder = new UrlBuilder(GetContext(), _virtualPathUtility, pagePath, null);
+
+ // Assert
+ Assert.Equal("dir/page", builder.Path);
+ Assert.Equal("?someparam=value", builder.QueryString);
+ }
+
+ [Fact]
+ public void UrlBuilderWithVirtualPathAndQueryStringAppendsObjectAsQueryStringParams()
+ {
+ // Arrange
+ var pagePath = "~/dir/page?someparam=value";
+
+ // Act
+ var builder = new UrlBuilder(GetContext(), _virtualPathUtility, pagePath, new { someotherparam = "value2" });
+
+ // Assert
+ Assert.Equal("dir/page", builder.Path);
+ Assert.Equal("?someparam=value&someotherparam=value2", builder.QueryString);
+ }
+
+ [Fact]
+ public void AddPathAddsPathPortionToRelativeUrl()
+ {
+ // Arrange
+ var pagePath = "~/dir/page?someparam=value";
+
+ // Act
+ var builder = new UrlBuilder(GetContext(), _virtualPathUtility, pagePath, null);
+ builder.AddPath("foo").AddPath("bar/baz");
+
+ // Assert
+ Assert.Equal("dir/page/foo/bar/baz", builder.Path);
+ Assert.Equal("?someparam=value", builder.QueryString);
+ }
+
+ [Fact]
+ public void AddPathEncodesPathParams()
+ {
+ // Arrange
+ var pagePath = "~/dir/page?someparam=value";
+
+ // Act
+ var builder = new UrlBuilder(GetContext(), _virtualPathUtility, pagePath, null);
+ builder.AddPath("foo bar").AddPath("baz biz", "qux");
+
+ // Assert
+ Assert.Equal("dir/page/foo%20bar/baz%20biz/qux", builder.Path);
+ }
+
+ [Fact]
+ public void AddPathAddsPathPortionToAbsoluteUrl()
+ {
+ // Arrange
+ var pagePath = "http://some-site/dir/page?someparam=value";
+
+ // Act
+ var builder = new UrlBuilder(GetContext(), _virtualPathUtility, pagePath, null);
+ builder.AddPath("foo").AddPath("bar/baz");
+
+ // Assert
+ Assert.Equal("http://some-site/dir/page/foo/bar/baz", builder.Path);
+ Assert.Equal("?someparam=value", builder.QueryString);
+ }
+
+ [Fact]
+ public void AddPathWithParamsArrayAddsPathPortionToRelativeUrl()
+ {
+ // Arrange
+ var pagePath = "~/dir/page/?someparam=value";
+
+ // Act
+ var builder = new UrlBuilder(GetContext(), _virtualPathUtility, pagePath, null);
+ builder.AddPath("foo", "bar", "baz").AddPath("qux");
+
+ // Assert
+ Assert.Equal("dir/page/foo/bar/baz/qux", builder.Path);
+ Assert.Equal("?someparam=value", builder.QueryString);
+ }
+
+ [Fact]
+ public void AddPathEnsuresSlashesAreNotRepeated()
+ {
+ // Arrange
+ var pagePath = "~/dir/page/";
+
+ // Act
+ var builder = new UrlBuilder(GetContext(), _virtualPathUtility, pagePath, null);
+ builder.AddPath("foo").AddPath("/bar/").AddPath("/baz");
+
+ // Assert
+ Assert.Equal("dir/page/foo/bar/baz", builder.Path);
+ }
+
+ [Fact]
+ public void AddPathWithParamsEnsuresSlashAreNotRepeated()
+ {
+ // Arrange
+ var pagePath = "~/dir/page/";
+
+ // Act
+ var builder = new UrlBuilder(GetContext(), _virtualPathUtility, pagePath, null);
+ builder.AddPath("foo", "/bar/", "/baz").AddPath("qux/");
+
+ // Assert
+ Assert.Equal("dir/page/foo/bar/baz/qux/", builder.Path);
+ }
+
+ [Fact]
+ public void AddPathWithParamsArrayAddsPathPortionToAbsoluteUrl()
+ {
+ // Arrange
+ var pagePath = "http://www.test.com/dir/page/?someparam=value";
+
+ // Act
+ var builder = new UrlBuilder(GetContext(), _virtualPathUtility, pagePath, null);
+ builder.AddPath("foo", "bar", "baz").AddPath("qux");
+
+ // Assert
+ Assert.Equal("http://www.test.com/dir/page/foo/bar/baz/qux", builder.Path);
+ Assert.Equal("?someparam=value", builder.QueryString);
+ }
+
+ [Fact]
+ public void UrlBuilderEncodesParameters()
+ {
+ // Arrange
+ var pagePath = "~/dir/page/?someparam=value";
+
+ // Act
+ var builder = new UrlBuilder(GetContext(), _virtualPathUtility, pagePath, new { Λ = "λ" });
+ builder.AddParam(new { π = "is not a lie" }).AddParam("Π", "maybe a lie");
+ // Assert
+ Assert.Equal("?someparam=value&%ce%9b=%ce%bb&%cf%80=is+not+a+lie&%ce%a0=maybe+a+lie", builder.QueryString);
+ }
+
+ [Fact]
+ public void AddParamAddsParamToQueryString()
+ {
+ // Arrange
+ var pagePath = "http://www.test.com/dir/page/?someparam=value";
+
+ // Act
+ var builder = new UrlBuilder(GetContext(), _virtualPathUtility, pagePath, null);
+ builder.AddParam("foo", "bar");
+
+ // Assert
+ Assert.Equal("?someparam=value&foo=bar", builder.QueryString);
+ }
+
+ [Fact]
+ public void AddParamAddsQuestionMarkToQueryStringIfFirstParam()
+ {
+ // Arrange
+ var pagePath = "~/dir/page";
+
+ // Act
+ var builder = new UrlBuilder(GetContext(), _virtualPathUtility, pagePath, null);
+ builder.AddParam("foo", "bar").AddParam(new { baz = "qux", biz = "quark" });
+
+ // Assert
+ Assert.Equal("?foo=bar&baz=qux&biz=quark", builder.QueryString);
+ }
+
+ [Fact]
+ public void AddParamIgnoresParametersWithEmptyKey()
+ {
+ // Arrange
+ var pagePath = "~/dir/page";
+
+ // Act
+ var builder = new UrlBuilder(GetContext(), _virtualPathUtility, pagePath, null);
+ builder.AddParam("", "bar").AddParam(new { baz = "", biz = "quark" }).AddParam("qux", null).AddParam(null, "somevalue");
+
+ // Assert
+ Assert.Equal("?baz=&biz=quark&qux=", builder.QueryString);
+ }
+
+ [Fact]
+ public void ToStringConcatsPathAndQueryString()
+ {
+ // Arrange
+ var pagePath = "~/dir/page";
+
+ // Act
+ var builder = new UrlBuilder(GetContext(), _virtualPathUtility, pagePath, null);
+ builder.AddParam("foo", "bar").AddParam(new { baz = "qux", biz = "quark" });
+
+ // Assert
+ Assert.Equal("dir/page?foo=bar&baz=qux&biz=quark", builder.ToString());
+ }
+
+ [Fact]
+ public void UrlBuilderWithRealVirtualPathUtilityTest()
+ {
+ // Arrange
+ var pagePath = "~/world/test.aspx";
+ try
+ {
+ // Act
+ CreateHttpContext("default.aspx", "http://localhost/WebSite1/subfolder1/default.aspx");
+ CreateHttpRuntime("/WebSite1/");
+ var builder = new UrlBuilder(pagePath, null);
+
+ // Assert
+ Assert.Equal(@"/WebSite1/world/test.aspx", builder.Path);
+ }
+ finally
+ {
+ RestoreHttpRuntime();
+ }
+ }
+
+ private static HttpContextBase GetContext(params string[] virtualPaths)
+ {
+ var httpContext = new Mock<HttpContextBase>();
+ var table = new Hashtable();
+ httpContext.SetupGet(c => c.Items).Returns(table);
+
+ foreach (var item in virtualPaths)
+ {
+ var page = new Mock<ITemplateFile>();
+ page.SetupGet(p => p.TemplateInfo).Returns(new TemplateFileInfo(item));
+ TemplateStack.Push(httpContext.Object, page.Object);
+ }
+
+ return httpContext.Object;
+ }
+
+ private class TestVirtualPathUtility : VirtualPathUtilityBase
+ {
+ public override string Combine(string basePath, string relativePath)
+ {
+ return basePath + '/' + relativePath.TrimStart('~', '/');
+ }
+
+ public override string ToAbsolute(string virtualPath)
+ {
+ return virtualPath.TrimStart('~', '/');
+ }
+ }
+
+ internal static void CreateHttpRuntime(string appVPath)
+ {
+ var runtime = new HttpRuntime();
+ var appDomainAppVPathField = typeof(HttpRuntime).GetField("_appDomainAppVPath", BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance);
+ appDomainAppVPathField.SetValue(runtime, CreateVirtualPath(appVPath));
+ GetTheRuntime().SetValue(null, runtime);
+ var appDomainIdField = typeof(HttpRuntime).GetField("_appDomainId", BindingFlags.NonPublic | BindingFlags.Instance);
+ appDomainIdField.SetValue(runtime, "test");
+ }
+
+ internal static FieldInfo GetTheRuntime()
+ {
+ return typeof(HttpRuntime).GetField("_theRuntime", BindingFlags.NonPublic | BindingFlags.Static);
+ }
+
+ internal static void RestoreHttpRuntime()
+ {
+ GetTheRuntime().SetValue(null, null);
+ }
+
+ // E.g. "default.aspx", "http://localhost/WebSite1/subfolder1/default.aspx"
+ internal static void CreateHttpContext(string filename, string url)
+ {
+ var request = new HttpRequest(filename, url, null);
+ var httpContext = new HttpContext(request, new HttpResponse(new StringWriter(new StringBuilder())));
+ HttpContext.Current = httpContext;
+ }
+
+ internal static void RestoreHttpContext()
+ {
+ HttpContext.Current = null;
+ }
+
+ internal static object CreateVirtualPath(string path)
+ {
+ var vPath = typeof(Page).Assembly.GetType("System.Web.VirtualPath");
+ var method = vPath.GetMethod("CreateNonRelativeTrailingSlash", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
+ return method.Invoke(null, new object[] { path });
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Helpers.Test/VideoTest.cs b/test/Microsoft.Web.Helpers.Test/VideoTest.cs
new file mode 100644
index 00000000..0a266c1a
--- /dev/null
+++ b/test/Microsoft.Web.Helpers.Test/VideoTest.cs
@@ -0,0 +1,300 @@
+using System;
+using System.Reflection;
+using System.Text.RegularExpressions;
+using System.Web;
+using System.Web.WebPages.TestUtils;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace Microsoft.Web.Helpers.Test
+{
+ public class VideoTest
+ {
+ private VirtualPathUtilityWrapper _pathUtility = new VirtualPathUtilityWrapper();
+
+ [Fact]
+ public void FlashCannotOverrideHtmlAttributes()
+ {
+ Assert.ThrowsArgument(() => { Video.Flash(GetContext(), _pathUtility, "http://foo.bar.com/foo.swf", htmlAttributes: new { cLASSid = "CanNotOverride" }); }, "htmlAttributes", "Property \"cLASSid\" cannot be set through this argument.");
+ }
+
+ [Fact]
+ public void FlashDefaults()
+ {
+ string html = Video.Flash(GetContext(), _pathUtility, "http://foo.bar.com/foo.swf").ToString().Replace("\r\n", "");
+ Assert.True(html.StartsWith(
+ "<object classid=\"clsid:d27cdb6e-ae6d-11cf-96b8-444553540000\" " +
+ "codebase=\"http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab\" type=\"application/x-oleobject\" >"
+ ));
+ Assert.True(html.Contains("<param name=\"movie\" value=\"http://foo.bar.com/foo.swf\" />"));
+ Assert.True(html.Contains("<embed src=\"http://foo.bar.com/foo.swf\" type=\"application/x-shockwave-flash\" />"));
+ Assert.True(html.EndsWith("</object>"));
+ }
+
+ [Fact]
+ public void FlashThrowsWhenPathIsEmpty()
+ {
+ Assert.ThrowsArgumentNullOrEmptyString(() => { Video.Flash(GetContext(), _pathUtility, String.Empty); }, "path");
+ }
+
+ [Fact]
+ public void FlashThrowsWhenPathIsNull()
+ {
+ Assert.ThrowsArgumentNullOrEmptyString(() => { Video.Flash(GetContext(), _pathUtility, null); }, "path");
+ }
+
+ [Fact]
+ public void FlashWithExposedOptions()
+ {
+ string html = Video.Flash(GetContext(), _pathUtility, "http://foo.bar.com/foo.swf", width: "100px", height: "100px",
+ play: false, loop: false, menu: false, backgroundColor: "#000", quality: "Q", scale: "S", windowMode: "WM",
+ baseUrl: "http://foo.bar.com/", version: "1.0.0.0", htmlAttributes: new { id = "fl" }, embedName: "efl").ToString().Replace("\r\n", "");
+
+ Assert.True(html.StartsWith(
+ "<object classid=\"clsid:d27cdb6e-ae6d-11cf-96b8-444553540000\" " +
+ "codebase=\"http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=1,0,0,0\" " +
+ "height=\"100px\" id=\"fl\" type=\"application/x-oleobject\" width=\"100px\" >"
+ ));
+ Assert.True(html.Contains("<param name=\"play\" value=\"False\" />"));
+ Assert.True(html.Contains("<param name=\"loop\" value=\"False\" />"));
+ Assert.True(html.Contains("<param name=\"menu\" value=\"False\" />"));
+ Assert.True(html.Contains("<param name=\"bgColor\" value=\"#000\" />"));
+ Assert.True(html.Contains("<param name=\"quality\" value=\"Q\" />"));
+ Assert.True(html.Contains("<param name=\"scale\" value=\"S\" />"));
+ Assert.True(html.Contains("<param name=\"wmode\" value=\"WM\" />"));
+ Assert.True(html.Contains("<param name=\"base\" value=\"http://foo.bar.com/\" />"));
+
+ var embed = new Regex("<embed.*/>").Match(html);
+ Assert.True(embed.Success);
+ Assert.True(embed.Value.StartsWith("<embed src=\"http://foo.bar.com/foo.swf\" width=\"100px\" height=\"100px\" name=\"efl\" type=\"application/x-shockwave-flash\" "));
+ Assert.True(embed.Value.Contains("play=\"False\""));
+ Assert.True(embed.Value.Contains("loop=\"False\""));
+ Assert.True(embed.Value.Contains("menu=\"False\""));
+ Assert.True(embed.Value.Contains("bgColor=\"#000\""));
+ Assert.True(embed.Value.Contains("quality=\"Q\""));
+ Assert.True(embed.Value.Contains("scale=\"S\""));
+ Assert.True(embed.Value.Contains("wmode=\"WM\""));
+ Assert.True(embed.Value.Contains("base=\"http://foo.bar.com/\""));
+ }
+
+ [Fact]
+ public void FlashWithUnexposedOptions()
+ {
+ string html = Video.Flash(GetContext(), _pathUtility, "http://foo.bar.com/foo.swf", options: new { X = "Y", Z = 123 }).ToString().Replace("\r\n", "");
+ Assert.True(html.Contains("<param name=\"X\" value=\"Y\" />"));
+ Assert.True(html.Contains("<param name=\"Z\" value=\"123\" />"));
+ // note - can't guarantee order of optional params:
+ Assert.True(
+ html.Contains("<embed src=\"http://foo.bar.com/foo.swf\" type=\"application/x-shockwave-flash\" X=\"Y\" Z=\"123\" />") ||
+ html.Contains("<embed src=\"http://foo.bar.com/foo.swf\" type=\"application/x-shockwave-flash\" Z=\"123\" X=\"Y\" />")
+ );
+ }
+
+ [Fact]
+ public void MediaPlayerCannotOverrideHtmlAttributes()
+ {
+ Assert.ThrowsArgument(() => { Video.MediaPlayer(GetContext(), _pathUtility, "http://foo.bar.com/foo.wmv", htmlAttributes: new { cODEbase = "CanNotOverride" }); }, "htmlAttributes", "Property \"cODEbase\" cannot be set through this argument.");
+ }
+
+ [Fact]
+ public void MediaPlayerDefaults()
+ {
+ string html = Video.MediaPlayer(GetContext(), _pathUtility, "http://foo.bar.com/foo.wmv").ToString().Replace("\r\n", "");
+ Assert.True(html.StartsWith(
+ "<object classid=\"clsid:6BF52A52-394A-11D3-B153-00C04F79FAA6\" >"
+ ));
+ Assert.True(html.Contains("<param name=\"URL\" value=\"http://foo.bar.com/foo.wmv\" />"));
+ Assert.True(html.Contains("<embed src=\"http://foo.bar.com/foo.wmv\" type=\"application/x-mplayer2\" />"));
+ Assert.True(html.EndsWith("</object>"));
+ }
+
+ [Fact]
+ public void MediaPlayerThrowsWhenPathIsEmpty()
+ {
+ Assert.ThrowsArgumentNullOrEmptyString(() => { Video.MediaPlayer(GetContext(), _pathUtility, String.Empty); }, "path");
+ }
+
+ [Fact]
+ public void MediaPlayerThrowsWhenPathIsNull()
+ {
+ Assert.ThrowsArgumentNullOrEmptyString(() => { Video.MediaPlayer(GetContext(), _pathUtility, null); }, "path");
+ }
+
+ [Fact]
+ public void MediaPlayerWithExposedOptions()
+ {
+ string html = Video.MediaPlayer(GetContext(), _pathUtility, "http://foo.bar.com/foo.wmv", width: "100px", height: "100px",
+ autoStart: false, playCount: 2, uiMode: "UIMODE", stretchToFit: true, enableContextMenu: false, mute: true,
+ volume: 1, baseUrl: "http://foo.bar.com/", htmlAttributes: new { id = "mp" }, embedName: "emp").ToString().Replace("\r\n", "");
+ Assert.True(html.StartsWith(
+ "<object classid=\"clsid:6BF52A52-394A-11D3-B153-00C04F79FAA6\" height=\"100px\" id=\"mp\" width=\"100px\" >"
+ ));
+ Assert.True(html.Contains("<param name=\"URL\" value=\"http://foo.bar.com/foo.wmv\" />"));
+ Assert.True(html.Contains("<param name=\"autoStart\" value=\"False\" />"));
+ Assert.True(html.Contains("<param name=\"playCount\" value=\"2\" />"));
+ Assert.True(html.Contains("<param name=\"uiMode\" value=\"UIMODE\" />"));
+ Assert.True(html.Contains("<param name=\"stretchToFit\" value=\"True\" />"));
+ Assert.True(html.Contains("<param name=\"enableContextMenu\" value=\"False\" />"));
+ Assert.True(html.Contains("<param name=\"mute\" value=\"True\" />"));
+ Assert.True(html.Contains("<param name=\"volume\" value=\"1\" />"));
+ Assert.True(html.Contains("<param name=\"baseURL\" value=\"http://foo.bar.com/\" />"));
+
+ var embed = new Regex("<embed.*/>").Match(html);
+ Assert.True(embed.Success);
+ Assert.True(embed.Value.StartsWith("<embed src=\"http://foo.bar.com/foo.wmv\" width=\"100px\" height=\"100px\" name=\"emp\" type=\"application/x-mplayer2\" "));
+ Assert.True(embed.Value.Contains("autoStart=\"False\""));
+ Assert.True(embed.Value.Contains("playCount=\"2\""));
+ Assert.True(embed.Value.Contains("uiMode=\"UIMODE\""));
+ Assert.True(embed.Value.Contains("stretchToFit=\"True\""));
+ Assert.True(embed.Value.Contains("enableContextMenu=\"False\""));
+ Assert.True(embed.Value.Contains("mute=\"True\""));
+ Assert.True(embed.Value.Contains("volume=\"1\""));
+ Assert.True(embed.Value.Contains("baseURL=\"http://foo.bar.com/\""));
+ }
+
+ [Fact]
+ public void MediaPlayerWithUnexposedOptions()
+ {
+ string html = Video.MediaPlayer(GetContext(), _pathUtility, "http://foo.bar.com/foo.wmv", options: new { X = "Y", Z = 123 }).ToString().Replace("\r\n", "");
+ Assert.True(html.Contains("<param name=\"X\" value=\"Y\" />"));
+ Assert.True(html.Contains("<param name=\"Z\" value=\"123\" />"));
+ Assert.True(
+ html.Contains("<embed src=\"http://foo.bar.com/foo.wmv\" type=\"application/x-mplayer2\" X=\"Y\" Z=\"123\" />") ||
+ html.Contains("<embed src=\"http://foo.bar.com/foo.wmv\" type=\"application/x-mplayer2\" Z=\"123\" X=\"Y\" />")
+ );
+ }
+
+ [Fact]
+ public void SilverlightCannotOverrideHtmlAttributes()
+ {
+ Assert.ThrowsArgument(() =>
+ {
+ Video.Silverlight(GetContext(), _pathUtility, "http://foo.bar.com/foo.xap", "100px", "100px",
+ htmlAttributes: new { WIDTH = "CanNotOverride" });
+ }, "htmlAttributes", "Property \"WIDTH\" cannot be set through this argument.");
+ }
+
+ [Fact]
+ public void SilverlightDefaults()
+ {
+ string html = Video.Silverlight(GetContext(), _pathUtility, "http://foo.bar.com/foo.xap", "100px", "100px").ToString().Replace("\r\n", "");
+ Assert.True(html.StartsWith(
+ "<object data=\"data:application/x-silverlight-2,\" height=\"100px\" type=\"application/x-silverlight-2\" " +
+ "width=\"100px\" >"
+ ));
+ Assert.True(html.Contains("<param name=\"source\" value=\"http://foo.bar.com/foo.xap\" />"));
+ Assert.True(html.Contains(
+ "<a href=\"http://go.microsoft.com/fwlink/?LinkID=149156\" style=\"text-decoration:none\">" +
+ "<img src=\"http://go.microsoft.com/fwlink?LinkId=108181\" alt=\"Get Microsoft Silverlight\" " +
+ "style=\"border-style:none\"/></a>"));
+ Assert.True(html.EndsWith("</object>"));
+ }
+
+ [Fact]
+ public void SilverlightThrowsWhenPathIsEmpty()
+ {
+ Assert.ThrowsArgumentNullOrEmptyString(() => { Video.Silverlight(GetContext(), _pathUtility, String.Empty, "100px", "100px"); }, "path");
+ }
+
+ [Fact]
+ public void SilverlightThrowsWhenPathIsNull()
+ {
+ Assert.ThrowsArgumentNullOrEmptyString(() => { Video.Silverlight(GetContext(), _pathUtility, null, "100px", "100px"); }, "path");
+ }
+
+ [Fact]
+ public void SilverlightThrowsWhenHeightIsEmpty()
+ {
+ Assert.ThrowsArgumentNullOrEmptyString(() => { Video.Silverlight(GetContext(), _pathUtility, "http://foo.bar.com/foo.xap", "100px", String.Empty); }, "height");
+ }
+
+ [Fact]
+ public void SilverlightThrowsWhenHeightIsNull()
+ {
+ Assert.ThrowsArgumentNullOrEmptyString(() => { Video.Silverlight(GetContext(), _pathUtility, "http://foo.bar.com/foo.xap", "100px", null); }, "height");
+ }
+
+ [Fact]
+ public void SilverlightThrowsWhenWidthIsEmpty()
+ {
+ Assert.ThrowsArgumentNullOrEmptyString(() => { Video.Silverlight(GetContext(), _pathUtility, "http://foo.bar.com/foo.xap", String.Empty, "100px"); }, "width");
+ }
+
+ [Fact]
+ public void SilverlightThrowsWhenWidthIsNull()
+ {
+ Assert.ThrowsArgumentNullOrEmptyString(() => { Video.Silverlight(GetContext(), _pathUtility, "http://foo.bar.com/foo.xap", null, "100px"); }, "width");
+ }
+
+ [Fact]
+ public void SilverlightWithExposedOptions()
+ {
+ string html = Video.Silverlight(GetContext(), _pathUtility, "http://foo.bar.com/foo.xap", width: "85%", height: "85%",
+ backgroundColor: "red", initParameters: "X=Y", minimumVersion: "1.0.0.0", autoUpgrade: false,
+ htmlAttributes: new { id = "sl" }).ToString().Replace("\r\n", "");
+ Assert.True(html.StartsWith(
+ "<object data=\"data:application/x-silverlight-2,\" height=\"85%\" id=\"sl\" " +
+ "type=\"application/x-silverlight-2\" width=\"85%\" >"
+ ));
+ Assert.True(html.Contains("<param name=\"background\" value=\"red\" />"));
+ Assert.True(html.Contains("<param name=\"initparams\" value=\"X=Y\" />"));
+ Assert.True(html.Contains("<param name=\"minruntimeversion\" value=\"1.0.0.0\" />"));
+ Assert.True(html.Contains("<param name=\"autoUpgrade\" value=\"False\" />"));
+
+ var embed = new Regex("<embed.*/>").Match(html);
+ Assert.False(embed.Success);
+ }
+
+ [Fact]
+ public void SilverlightWithUnexposedOptions()
+ {
+ string html = Video.Silverlight(GetContext(), _pathUtility, "http://foo.bar.com/foo.xap", width: "50px", height: "50px",
+ options: new { X = "Y", Z = 123 }).ToString().Replace("\r\n", "");
+ Assert.True(html.Contains("<param name=\"X\" value=\"Y\" />"));
+ Assert.True(html.Contains("<param name=\"Z\" value=\"123\" />"));
+ }
+
+ [Fact]
+ public void ValidatePathResolvesExistingLocalPath()
+ {
+ string path = Assembly.GetExecutingAssembly().Location;
+ Mock<VirtualPathUtilityBase> pathUtility = new Mock<VirtualPathUtilityBase>();
+ pathUtility.Setup(p => p.Combine(It.IsAny<string>(), It.IsAny<string>())).Returns(path);
+ pathUtility.Setup(p => p.ToAbsolute(It.IsAny<string>())).Returns(path);
+
+ Mock<HttpServerUtilityBase> serverMock = new Mock<HttpServerUtilityBase>();
+ serverMock.Setup(s => s.MapPath(It.IsAny<string>())).Returns(path);
+ HttpContextBase context = GetContext(serverMock.Object);
+
+ string html = Video.Flash(context, pathUtility.Object, "foo.bar").ToString();
+ Assert.True(html.StartsWith("<object"));
+ Assert.True(html.Contains(HttpUtility.HtmlAttributeEncode(HttpUtility.UrlPathEncode(path))));
+ }
+
+ [Fact]
+ public void ValidatePathThrowsForNonExistingLocalPath()
+ {
+ string path = "c:\\does\\not\\exist.swf";
+ Mock<VirtualPathUtilityBase> pathUtility = new Mock<VirtualPathUtilityBase>();
+ pathUtility.Setup(p => p.Combine(It.IsAny<string>(), It.IsAny<string>())).Returns(path);
+ pathUtility.Setup(p => p.ToAbsolute(It.IsAny<string>())).Returns(path);
+
+ Mock<HttpServerUtilityBase> serverMock = new Mock<HttpServerUtilityBase>();
+ serverMock.Setup(s => s.MapPath(It.IsAny<string>())).Returns(path);
+ HttpContextBase context = GetContext(serverMock.Object);
+
+ Assert.Throws<InvalidOperationException>(() => { Video.Flash(context, pathUtility.Object, "exist.swf"); }, "The media file \"exist.swf\" does not exist.");
+ }
+
+ private static HttpContextBase GetContext(HttpServerUtilityBase serverUtility = null)
+ {
+ // simple mocked context - won't reference as long as path starts with 'http'
+ Mock<HttpRequestBase> requestMock = new Mock<HttpRequestBase>();
+ Mock<HttpContextBase> contextMock = new Mock<HttpContextBase>();
+ contextMock.Setup(context => context.Request).Returns(requestMock.Object);
+ contextMock.Setup(context => context.Server).Returns(serverUtility);
+ return contextMock.Object;
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Helpers.Test/packages.config b/test/Microsoft.Web.Helpers.Test/packages.config
new file mode 100644
index 00000000..d5aa6401
--- /dev/null
+++ b/test/Microsoft.Web.Helpers.Test/packages.config
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="Moq" version="4.0.10827" />
+ <package id="xunit" version="1.9.0.1566" />
+ <package id="xunit.extensions" version="1.9.0.1566" />
+</packages> \ No newline at end of file
diff --git a/test/Microsoft.Web.Http.Data.Test/ChangeSetTests.cs b/test/Microsoft.Web.Http.Data.Test/ChangeSetTests.cs
new file mode 100644
index 00000000..cf7d6cdd
--- /dev/null
+++ b/test/Microsoft.Web.Http.Data.Test/ChangeSetTests.cs
@@ -0,0 +1,164 @@
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Web.Http.Data;
+using Microsoft.Web.Http.Data.Test.Models;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.ServiceModel.DomainServices.Server.Test
+{
+ public class ChangeSetTests
+ {
+ /// <summary>
+ /// Verify ChangeSet validation when specifying/requesting original for Insert operations.
+ /// </summary>
+ [Fact]
+ public void Changeset_OriginalInvalidForInserts()
+ {
+ // can't specify an original for an insert operation
+ Product curr = new Product { ProductID = 1 };
+ Product orig = new Product { ProductID = 1 };
+ ChangeSetEntry entry = new ChangeSetEntry { Id = 1, Entity = curr, OriginalEntity = orig, Operation = ChangeOperation.Insert };
+ ChangeSet cs = null;
+ Assert.Throws<InvalidOperationException>(delegate
+ {
+ cs = new ChangeSet(new ChangeSetEntry[] { entry });
+ },
+ String.Format(Resource.InvalidChangeSet, Resource.InvalidChangeSet_InsertsCantHaveOriginal));
+
+ // get original should throw for insert operations
+ entry = new ChangeSetEntry { Id = 1, Entity = curr, OriginalEntity = null, Operation = ChangeOperation.Insert };
+ cs = new ChangeSet(new ChangeSetEntry[] { entry });
+ Assert.Throws<InvalidOperationException>(delegate
+ {
+ cs.GetOriginal(curr);
+ },
+ String.Format(Resource.ChangeSet_OriginalNotValidForInsert));
+ }
+
+ [Fact]
+ public void Constructor_HasErrors()
+ {
+ ChangeSet changeSet = this.GenerateChangeSet();
+ Assert.False(changeSet.HasError);
+
+ changeSet = this.GenerateChangeSet();
+ changeSet.ChangeSetEntries.First().ValidationErrors = new List<ValidationResultInfo>() { new ValidationResultInfo("Error", new[] { "Error" }) };
+ Assert.True(changeSet.HasError, "Expected ChangeSet to have errors");
+ }
+
+ [Fact]
+ public void GetOriginal()
+ {
+ ChangeSet changeSet = this.GenerateChangeSet();
+ ChangeSetEntry op = changeSet.ChangeSetEntries.First();
+
+ Product currentEntity = new Product();
+ Product originalEntity = new Product();
+
+ op.Entity = currentEntity;
+ op.OriginalEntity = originalEntity;
+
+ Product changeSetOriginalEntity = changeSet.GetOriginal(currentEntity);
+
+ // Verify we returned the original
+ Assert.Same(originalEntity, changeSetOriginalEntity);
+ }
+
+ [Fact]
+ public void GetOriginal_EntityExistsMoreThanOnce()
+ {
+ ChangeSet changeSet = this.GenerateChangeSet();
+ ChangeSetEntry op1 = changeSet.ChangeSetEntries.Skip(0).First();
+ ChangeSetEntry op2 = changeSet.ChangeSetEntries.Skip(1).First();
+ ChangeSetEntry op3 = changeSet.ChangeSetEntries.Skip(2).First();
+
+ Product currentEntity = new Product(), originalEntity = new Product();
+
+ op1.Entity = currentEntity;
+ op1.OriginalEntity = originalEntity;
+
+ op2.Entity = currentEntity;
+ op2.OriginalEntity = originalEntity;
+
+ op3.Entity = currentEntity;
+ op3.OriginalEntity = null;
+
+ Product changeSetOriginalEntity = changeSet.GetOriginal(currentEntity);
+
+ // Verify we returned the original
+ Assert.Same(originalEntity, changeSetOriginalEntity);
+ }
+
+ [Fact]
+ public void GetOriginal_InvalidArgs()
+ {
+ ChangeSet changeSet = this.GenerateChangeSet();
+
+ Assert.ThrowsArgumentNull(
+ () => changeSet.GetOriginal<Product>(null),
+ "clientEntity");
+ }
+
+ [Fact]
+ public void GetOriginal_EntityOperationNotFound()
+ {
+ ChangeSet changeSet = this.GenerateChangeSet();
+ Assert.Throws<ArgumentException>(
+ () => changeSet.GetOriginal(new Product()),
+ Resource.ChangeSet_ChangeSetEntryNotFound);
+ }
+
+ private ChangeSet GenerateChangeSet()
+ {
+ return new ChangeSet(this.GenerateEntityOperations(false));
+ }
+
+ private IEnumerable<ChangeSetEntry> GenerateEntityOperations(bool alternateTypes)
+ {
+ List<ChangeSetEntry> ops = new List<ChangeSetEntry>(10);
+
+ int id = 1;
+ for (int i = 0; i < ops.Capacity; ++i)
+ {
+ object entity, originalEntity;
+
+ if (!alternateTypes || i % 2 == 0)
+ {
+ entity = new MockEntity1() { FullName = String.Format("FName{0} LName{0}", i) };
+ originalEntity = new MockEntity1() { FullName = String.Format("OriginalFName{0} OriginalLName{0}", i) };
+ }
+ else
+ {
+ entity = new MockEntity2() { FullNameAndID = String.Format("FName{0} LName{0} ID{0}", i) };
+ originalEntity = new MockEntity2() { FullNameAndID = String.Format("OriginalFName{0} OriginalLName{0} OriginalID{0}", i) };
+ }
+
+ ops.Add(new ChangeSetEntry { Id = id++, Entity = entity, OriginalEntity = originalEntity, Operation = ChangeOperation.Update });
+ }
+
+ return ops;
+ }
+
+ public class MockStoreEntity
+ {
+ public int ID { get; set; }
+ public string FirstName { get; set; }
+ public string LastName { get; set; }
+ }
+
+ public class MockEntity1
+ {
+ public string FullName { get; set; }
+ }
+
+ public class MockEntity2
+ {
+ public string FullNameAndID { get; set; }
+ }
+
+ public class MockDerivedEntity : MockEntity1
+ {
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Http.Data.Test/Controllers/CatalogController.cs b/test/Microsoft.Web.Http.Data.Test/Controllers/CatalogController.cs
new file mode 100644
index 00000000..d78fb874
--- /dev/null
+++ b/test/Microsoft.Web.Http.Data.Test/Controllers/CatalogController.cs
@@ -0,0 +1,71 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Security.Principal;
+using System.Web.Http;
+using Microsoft.Web.Http.Data.Test.Models;
+
+namespace Microsoft.Web.Http.Data.Test
+{
+ public class CatalogController : DataController
+ {
+ private Product[] products;
+
+ public CatalogController()
+ {
+ this.products = new Product[] {
+ new Product { ProductID = 1, ProductName = "Frish Gnarbles", UnitPrice = 12.33M, UnitsInStock = 55 },
+ new Product { ProductID = 2, ProductName = "Crispy Snarfs", UnitPrice = 4.22M, UnitsInStock = 11 },
+ new Product { ProductID = 1, ProductName = "Cheezy Snax", UnitPrice = 2.99M, UnitsInStock = 21 },
+ new Product { ProductID = 1, ProductName = "Fruit Yummies", UnitPrice = 5.55M, UnitsInStock = 88 },
+ new Product { ProductID = 1, ProductName = "Choco Wafers", UnitPrice = 1.87M, UnitsInStock = 109 },
+ new Product { ProductID = 1, ProductName = "Fritter Flaps", UnitPrice = 2.45M, UnitsInStock = 444 },
+ new Product { ProductID = 1, ProductName = "Yummy Bears", UnitPrice = 2.00M, UnitsInStock = 27 },
+ new Product { ProductID = 1, ProductName = "Cheddar Gnomes", UnitPrice = 3.99M, UnitsInStock = 975 },
+ new Product { ProductID = 1, ProductName = "Beefcicles", UnitPrice = 0.99M, UnitsInStock = 634 },
+ new Product { ProductID = 1, ProductName = "Butterscotchies", UnitPrice = 1.00M, UnitsInStock = 789 }
+ };
+ }
+
+ [ResultLimit(9)]
+ public IQueryable<Product> GetProducts()
+ {
+ return this.products.AsQueryable();
+ }
+
+ [ResultLimit(5)]
+ public IEnumerable<Product> GetProductsEnumerable()
+ {
+ return this.products.AsQueryable();
+ }
+
+ public IQueryable<Order> GetOrders()
+ {
+ return new Order[] {
+ new Order { OrderID = 1, CustomerID = "ALFKI" },
+ new Order { OrderID = 2, CustomerID = "CHOPS" }
+ }.AsQueryable();
+ }
+
+ public IEnumerable<Order_Detail> GetDetails(int orderId)
+ {
+ return Enumerable.Empty<Order_Detail>();
+ }
+
+ public void InsertOrder(Order order)
+ {
+
+ }
+
+ [Authorize]
+ public void UpdateProduct(Product product)
+ {
+ // demonstrate that the current ActionContext can be accessed by
+ // controller actions
+ IPrincipal user = this.ActionContext.ControllerContext.Request.GetUserPrincipal();
+ }
+
+ public void InsertOrderDetail(Order_Detail detail)
+ {
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Http.Data.Test/Controllers/CitiesController.cs b/test/Microsoft.Web.Http.Data.Test/Controllers/CitiesController.cs
new file mode 100644
index 00000000..547a5eeb
--- /dev/null
+++ b/test/Microsoft.Web.Http.Data.Test/Controllers/CitiesController.cs
@@ -0,0 +1,15 @@
+using System.Linq;
+using Microsoft.Web.Http.Data.Test.Models;
+
+namespace Microsoft.Web.Http.Data.Test
+{
+ public class CitiesController : DataController
+ {
+ private CityData cityData = new CityData();
+
+ public IQueryable<City> GetCities()
+ {
+ return this.cityData.Cities.AsQueryable();
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Http.Data.Test/Controllers/NorthwindEFController.cs b/test/Microsoft.Web.Http.Data.Test/Controllers/NorthwindEFController.cs
new file mode 100644
index 00000000..dfa032f5
--- /dev/null
+++ b/test/Microsoft.Web.Http.Data.Test/Controllers/NorthwindEFController.cs
@@ -0,0 +1,45 @@
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using Microsoft.Web.Http.Data.EntityFramework;
+using Microsoft.Web.Http.Data.Test.Models.EF;
+
+namespace Microsoft.Web.Http.Data.Test
+{
+ public class NorthwindEFTestController : LinqToEntitiesDataController<NorthwindEntities>
+ {
+ public IQueryable<Product> GetProducts()
+ {
+ return this.ObjectContext.Products;
+ }
+
+ public void InsertProduct(Product product)
+ {
+ }
+
+ public void UpdateProduct(Product product)
+ {
+ }
+
+ protected override NorthwindEntities CreateObjectContext()
+ {
+ return new NorthwindEntities(TestHelpers.GetTestEFConnectionString());
+ }
+ }
+}
+
+namespace Microsoft.Web.Http.Data.Test.Models.EF
+{
+ [MetadataType(typeof(ProductMetadata))]
+ public partial class Product
+ {
+ internal sealed class ProductMetadata
+ {
+ [Editable(false, AllowInitialValue = true)]
+ [StringLength(777, MinimumLength = 2)]
+ public string QuantityPerUnit { get; set; }
+
+ [Range(0, 1000000)]
+ public string UnitPrice { get; set; }
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Http.Data.Test/DataControllerDescriptionTest.cs b/test/Microsoft.Web.Http.Data.Test/DataControllerDescriptionTest.cs
new file mode 100644
index 00000000..f2778b87
--- /dev/null
+++ b/test/Microsoft.Web.Http.Data.Test/DataControllerDescriptionTest.cs
@@ -0,0 +1,192 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Web.Http.Controllers;
+using Microsoft.Web.Http.Data.EntityFramework;
+using Microsoft.Web.Http.Data.EntityFramework.Metadata;
+using Microsoft.Web.Http.Data.Test.Models;
+using System.Web.Http;
+using System.Web.Http.Filters;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace Microsoft.Web.Http.Data.Test
+{
+ public class DataControllerDescriptionTest
+ {
+ // verify that the LinqToEntitiesMetadataProvider is registered by default for
+ // LinqToEntitiesDataController<T> derived types
+ [Fact]
+ public void EFMetadataProvider_AttributeInference()
+ {
+ HttpConfiguration configuration = new HttpConfiguration();
+ HttpControllerDescriptor controllerDescriptor = new HttpControllerDescriptor
+ {
+ Configuration = configuration,
+ ControllerType = typeof(NorthwindEFTestController),
+ };
+ DataControllerDescription description = GetDataControllerDescription(typeof(NorthwindEFTestController));
+ PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(typeof(Microsoft.Web.Http.Data.Test.Models.EF.Product));
+
+ // verify key attribute
+ Assert.NotNull(properties["ProductID"].Attributes[typeof(KeyAttribute)]);
+ Assert.Null(properties["ProductName"].Attributes[typeof(KeyAttribute)]);
+
+ // verify StringLengthAttribute
+ StringLengthAttribute sla = (StringLengthAttribute)properties["ProductName"].Attributes[typeof(StringLengthAttribute)];
+ Assert.NotNull(sla);
+ Assert.Equal(40, sla.MaximumLength);
+
+ // verify RequiredAttribute
+ RequiredAttribute ra = (RequiredAttribute)properties["ProductName"].Attributes[typeof(RequiredAttribute)];
+ Assert.NotNull(ra);
+ Assert.False(ra.AllowEmptyStrings);
+
+ // verify association attribute
+ AssociationAttribute aa = (AssociationAttribute)properties["Category"].Attributes[typeof(AssociationAttribute)];
+ Assert.NotNull(aa);
+ Assert.Equal("Category_Product", aa.Name);
+ Assert.True(aa.IsForeignKey);
+ Assert.Equal("CategoryID", aa.ThisKey);
+ Assert.Equal("CategoryID", aa.OtherKey);
+
+ // verify metadata from "buddy class"
+ PropertyDescriptor pd = properties["QuantityPerUnit"];
+ sla = (StringLengthAttribute)pd.Attributes[typeof(StringLengthAttribute)];
+ Assert.NotNull(sla);
+ Assert.Equal(777, sla.MaximumLength);
+ EditableAttribute ea = (EditableAttribute)pd.Attributes[typeof(EditableAttribute)];
+ Assert.False(ea.AllowEdit);
+ Assert.True(ea.AllowInitialValue);
+ }
+
+ [Fact]
+ public void EFTypeDescriptor_ExcludedEntityMembers()
+ {
+ PropertyDescriptor pd = TypeDescriptor.GetProperties(typeof(Microsoft.Web.Http.Data.Test.Models.EF.Product))["EntityState"];
+ Assert.True(LinqToEntitiesTypeDescriptor.ShouldExcludeEntityMember(pd));
+
+ pd = TypeDescriptor.GetProperties(typeof(Microsoft.Web.Http.Data.Test.Models.EF.Product))["EntityState"];
+ Assert.True(LinqToEntitiesTypeDescriptor.ShouldExcludeEntityMember(pd));
+
+ pd = TypeDescriptor.GetProperties(typeof(Microsoft.Web.Http.Data.Test.Models.EF.Product))["SupplierReference"];
+ Assert.True(LinqToEntitiesTypeDescriptor.ShouldExcludeEntityMember(pd));
+ }
+
+ [Fact]
+ public void DescriptionValidation_NonAuthorizationFilter()
+ {
+ Assert.Throws<NotSupportedException>(
+ () => GetDataControllerDescription(typeof(InvalidController_NonAuthMethodFilter)),
+ String.Format(String.Format(Resource.InvalidAction_UnsupportedFilterType, "InvalidController_NonAuthMethodFilter", "UpdateProduct")));
+ }
+
+ /// <summary>
+ /// Verify that associated entities are correctly registered in the description when
+ /// using explicit data contracts
+ /// </summary>
+ [Fact]
+ public void AssociatedEntityTypeDiscovery_ExplicitDataContract()
+ {
+ DataControllerDescription description = GetDataControllerDescription(typeof(IncludedAssociationTestController_ExplicitDataContract));
+ List<Type> entityTypes = description.EntityTypes.ToList();
+ Assert.Equal(8, entityTypes.Count);
+ Assert.True(entityTypes.Contains(typeof(Microsoft.Web.Http.Data.Test.Models.EF.Order)));
+ Assert.True(entityTypes.Contains(typeof(Microsoft.Web.Http.Data.Test.Models.EF.Order_Detail)));
+ Assert.True(entityTypes.Contains(typeof(Microsoft.Web.Http.Data.Test.Models.EF.Customer)));
+ Assert.True(entityTypes.Contains(typeof(Microsoft.Web.Http.Data.Test.Models.EF.Employee)));
+ Assert.True(entityTypes.Contains(typeof(Microsoft.Web.Http.Data.Test.Models.EF.Product)));
+ Assert.True(entityTypes.Contains(typeof(Microsoft.Web.Http.Data.Test.Models.EF.Category)));
+ Assert.True(entityTypes.Contains(typeof(Microsoft.Web.Http.Data.Test.Models.EF.Supplier)));
+ Assert.True(entityTypes.Contains(typeof(Microsoft.Web.Http.Data.Test.Models.EF.Shipper)));
+ }
+
+ /// <summary>
+ /// Verify that associated entities are correctly registered in the description when
+ /// using implicit data contracts
+ /// </summary>
+ [Fact]
+ public void AssociatedEntityTypeDiscovery_ImplicitDataContract()
+ {
+ DataControllerDescription description = GetDataControllerDescription(typeof(IncludedAssociationTestController_ImplicitDataContract));
+ List<Type> entityTypes = description.EntityTypes.ToList();
+ Assert.Equal(3, entityTypes.Count);
+ Assert.True(entityTypes.Contains(typeof(Microsoft.Web.Http.Data.Test.Models.Customer)));
+ Assert.True(entityTypes.Contains(typeof(Microsoft.Web.Http.Data.Test.Models.Order)));
+ Assert.True(entityTypes.Contains(typeof(Microsoft.Web.Http.Data.Test.Models.Order_Detail)));
+ }
+
+ /// <summary>
+ /// Verify that DataControllerDescription correctly handles Task returning actions and discovers
+ /// entity types from those as well (unwrapping the task type).
+ /// </summary>
+ [Fact]
+ public void TaskReturningGetActions()
+ {
+ DataControllerDescription desc = GetDataControllerDescription(typeof(TaskReturningGetActionsController));
+ Assert.Equal(4, desc.EntityTypes.Count());
+ Assert.True(desc.EntityTypes.Contains(typeof(City)));
+ Assert.True(desc.EntityTypes.Contains(typeof(CityWithInfo)));
+ Assert.True(desc.EntityTypes.Contains(typeof(CityWithEditHistory)));
+ Assert.True(desc.EntityTypes.Contains(typeof(State)));
+ }
+
+ internal static DataControllerDescription GetDataControllerDescription(Type controllerType)
+ {
+ HttpConfiguration configuration = new HttpConfiguration();
+ HttpControllerDescriptor controllerDescriptor = new HttpControllerDescriptor
+ {
+ Configuration = configuration,
+ ControllerType = controllerType
+ };
+ return DataControllerDescription.GetDescription(controllerDescriptor);
+ }
+ }
+
+ internal class InvalidController_NonAuthMethodFilter : DataController
+ {
+ // attempt to apply a non-auth filter
+ [TestActionFilter]
+ public void UpdateProduct(Microsoft.Web.Http.Data.Test.Models.EF.Product product)
+ {
+ }
+
+ // the restriction doesn't apply for non CUD actions
+ [TestActionFilter]
+ public IEnumerable<Microsoft.Web.Http.Data.Test.Models.EF.Product> GetProducts()
+ {
+ return null;
+ }
+ }
+
+ internal class TaskReturningGetActionsController : DataController
+ {
+ public Task<IEnumerable<City>> GetCities()
+ {
+ return null;
+ }
+
+ public Task<State> GetState(string name)
+ {
+ return null;
+ }
+ }
+
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
+ public class TestActionFilterAttribute : ActionFilterAttribute
+ {
+ }
+
+ internal class IncludedAssociationTestController_ExplicitDataContract : LinqToEntitiesDataController<Microsoft.Web.Http.Data.Test.Models.EF.NorthwindEntities>
+ {
+ public IQueryable<Microsoft.Web.Http.Data.Test.Models.EF.Order> GetOrders() { return null; }
+ }
+
+ internal class IncludedAssociationTestController_ImplicitDataContract : DataController
+ {
+ public IQueryable<Microsoft.Web.Http.Data.Test.Models.Customer> GetCustomers() { return null; }
+ }
+}
diff --git a/test/Microsoft.Web.Http.Data.Test/DataControllerQueryTests.cs b/test/Microsoft.Web.Http.Data.Test/DataControllerQueryTests.cs
new file mode 100644
index 00000000..806ea680
--- /dev/null
+++ b/test/Microsoft.Web.Http.Data.Test/DataControllerQueryTests.cs
@@ -0,0 +1,231 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Net.Http.Formatting;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http;
+using System.Web.Http.Dispatcher;
+using System.Web.Http.Routing;
+using Microsoft.Web.Http.Data.Test.Models;
+using Xunit;
+
+namespace Microsoft.Web.Http.Data.Test
+{
+ public class DataControllerQueryTests
+ {
+ /// <summary>
+ /// Execute a simple query with limited results
+ /// </summary>
+ [Fact]
+ public void GetProducts()
+ {
+ HttpConfiguration config = GetTestConfiguration();
+ HttpServer server = GetTestCatalogServer(config);
+
+ HttpRequestMessage request = TestHelpers.CreateTestMessage(TestConstants.CatalogUrl + "GetProducts", HttpMethod.Get, config);
+ HttpResponseMessage response = server.SubmitRequestAsync(request, CancellationToken.None).Result;
+
+ Product[] products = response.Content.ReadAsAsync<IQueryable<Product>>().Result.ToArray();
+ Assert.Equal(9, products.Length);
+ }
+
+ /// <summary>
+ /// Execute a query with an OData filter specified
+ /// </summary>
+ [Fact]
+ public void Query_Filter()
+ {
+ HttpConfiguration config = GetTestConfiguration();
+ HttpServer server = GetTestCatalogServer(config);
+
+ string query = "?$filter=UnitPrice lt 5.0";
+ HttpRequestMessage request = TestHelpers.CreateTestMessage(TestConstants.CatalogUrl + "GetProducts" + query, HttpMethod.Get, config);
+ HttpResponseMessage response = server.SubmitRequestAsync(request, CancellationToken.None).Result;
+
+ Product[] products = response.Content.ReadAsAsync<IQueryable<Product>>().Result.ToArray();
+ Assert.Equal(8, products.Length);
+ }
+
+ /// <summary>
+ /// Verify that the json/xml formatter instances are not shared between controllers, since
+ /// their serializers are configured per controller.
+ /// </summary>
+ [Fact(Skip = "Need to verify if this test still makes sense given changed ObjectContent design")]
+ public void Query_VerifyFormatterConfiguration()
+ {
+ HttpConfiguration config = GetTestConfiguration();
+ HttpServer catalogServer = GetTestCatalogServer(config);
+ HttpServer citiesServer = GetTestCitiesServer(config);
+
+ // verify products query
+ HttpRequestMessage request = TestHelpers.CreateTestMessage(TestConstants.CatalogUrl + "GetProducts", HttpMethod.Get, config);
+ HttpResponseMessage response = catalogServer.SubmitRequestAsync(request, CancellationToken.None).Result;
+ Product[] products = response.Content.ReadAsAsync<IQueryable<Product>>().Result.ToArray();
+ Assert.Equal(9, products.Length);
+
+ // verify serialization
+ QueryResult qr = new QueryResult(products, products.Length);
+ ObjectContent oc = (ObjectContent)response.Content;
+ MemoryStream ms = new MemoryStream();
+ Task task = new JsonMediaTypeFormatter().WriteToStreamAsync(typeof(QueryResult), qr, ms, oc.Headers, null);
+ task.Wait();
+ Assert.True(ms.Length > 0);
+
+ // verify cities query
+ request = TestHelpers.CreateTestMessage(TestConstants.CitiesUrl + "GetCities", HttpMethod.Get, config);
+ response = citiesServer.SubmitRequestAsync(request, CancellationToken.None).Result;
+ City[] cities = response.Content.ReadAsAsync<IQueryable<City>>().Result.ToArray();
+ Assert.Equal(11, cities.Length);
+
+ // verify serialization
+ qr = new QueryResult(cities, cities.Length);
+ oc = (ObjectContent)response.Content;
+ ms = new MemoryStream();
+ task = new JsonMediaTypeFormatter().WriteToStreamAsync(typeof(QueryResult), qr, ms, oc.Headers, null);
+ task.Wait();
+ Assert.True(ms.Length > 0);
+ }
+
+ /// <summary>
+ /// Execute a query that requests an inline count with a paging query applied.
+ /// </summary>
+ [Fact]
+ public void Query_InlineCount_SkipTop()
+ {
+ HttpConfiguration config = GetTestConfiguration();
+ HttpServer server = GetTestCatalogServer(config);
+
+ string query = "?$filter=UnitPrice lt 5.0&$skip=2&$top=5&$inlinecount=allpages";
+ HttpRequestMessage request = TestHelpers.CreateTestMessage(TestConstants.CatalogUrl + "GetProducts" + query, HttpMethod.Get, config);
+ HttpResponseMessage response = server.SubmitRequestAsync(request, CancellationToken.None).Result;
+
+ QueryResult queryResult = response.Content.ReadAsAsync<QueryResult>().Result;
+ Assert.Equal(5, queryResult.Results.Cast<object>().Count());
+ Assert.Equal(8, queryResult.TotalCount);
+ }
+
+ /// <summary>
+ /// Execute a query that requests an inline count with only a top operation applied in the query.
+ /// Expect the total count to not inlcude the take operation.
+ /// </summary>
+ [Fact]
+ public void Query_IncludeTotalCount_Top()
+ {
+ HttpConfiguration config = GetTestConfiguration();
+ HttpServer server = GetTestCatalogServer(config);
+
+ string query = "?$filter=UnitPrice lt 5.0&$top=5&$inlinecount=allpages";
+ HttpRequestMessage request = TestHelpers.CreateTestMessage(TestConstants.CatalogUrl + "GetProducts" + query, HttpMethod.Get, config);
+ HttpResponseMessage response = server.SubmitRequestAsync(request, CancellationToken.None).Result;
+
+ QueryResult queryResult = response.Content.ReadAsAsync<QueryResult>().Result;
+ Assert.Equal(5, queryResult.Results.Cast<object>().Count());
+ Assert.Equal(8, queryResult.TotalCount);
+ }
+
+ /// <summary>
+ /// Execute a query that requests an inline count with no paging operations specified in the
+ /// user query. There is however still a server specified limit.
+ /// </summary>
+ [Fact]
+ public void Query_IncludeTotalCount_NoPaging()
+ {
+ HttpConfiguration config = GetTestConfiguration();
+ HttpServer server = GetTestCatalogServer(config);
+
+ string query = "?$filter=UnitPrice lt 5.0&$inlinecount=allpages";
+ HttpRequestMessage request = TestHelpers.CreateTestMessage(TestConstants.CatalogUrl + "GetProducts" + query, HttpMethod.Get, config);
+ HttpResponseMessage response = server.SubmitRequestAsync(request, CancellationToken.None).Result;
+
+ QueryResult queryResult = response.Content.ReadAsAsync<QueryResult>().Result;
+ Assert.Equal(8, queryResult.Results.Cast<object>().Count());
+ Assert.Equal(8, queryResult.TotalCount);
+ }
+
+ /// <summary>
+ /// Execute a query that sets the inlinecount option explicitly to 'none', and verify count is not returned.
+ /// </summary>
+ [Fact]
+ public void Query_IncludeTotalCount_False()
+ {
+ HttpConfiguration config = GetTestConfiguration();
+ HttpServer server = GetTestCatalogServer(config);
+
+ string query = "?$filter=UnitPrice lt 5.0&$inlinecount=none";
+ HttpRequestMessage request = TestHelpers.CreateTestMessage(TestConstants.CatalogUrl + "GetProducts" + query, HttpMethod.Get, config);
+ HttpResponseMessage response = server.SubmitRequestAsync(request, CancellationToken.None).Result;
+
+ Product[] products = response.Content.ReadAsAsync<IQueryable<Product>>().Result.ToArray();
+ Assert.Equal(8, products.Length);
+ }
+
+ /// <summary>
+ /// Verify that result limits are applied to IEnumerable returning actions as well.
+ /// </summary>
+ [Fact]
+ public void Query_ResultLimit_Enumerable()
+ {
+ HttpConfiguration config = GetTestConfiguration();
+ HttpServer server = GetTestCatalogServer(config);
+
+ HttpRequestMessage request = TestHelpers.CreateTestMessage(TestConstants.CatalogUrl + "GetProductsEnumerable", HttpMethod.Get, config);
+ HttpResponseMessage response = server.SubmitRequestAsync(request, CancellationToken.None).Result;
+
+ Product[] products = response.Content.ReadAsAsync<IEnumerable<Product>>().Result.ToArray();
+ Assert.Equal(5, products.Length);
+ }
+
+ /// <summary>
+ /// Verify that when no skip/top query operations are performed (and no result limits are active
+ /// on the action server side), the total count returned is -1, indicating that the total count
+ /// equals the result count. This avoids the fx having to double enumerate the query results to
+ /// set the count server side.
+ /// </summary>
+ [Fact]
+ public void Query_TotalCount_Equals_ResultCount()
+ {
+ HttpConfiguration config = GetTestConfiguration();
+ HttpServer server = GetTestCatalogServer(config);
+
+ string query = "?$inlinecount=allpages";
+ HttpRequestMessage request = TestHelpers.CreateTestMessage(TestConstants.CatalogUrl + "GetOrders" + query, HttpMethod.Get, config);
+ HttpResponseMessage response = server.SubmitRequestAsync(request, CancellationToken.None).Result;
+
+ QueryResult result = response.Content.ReadAsAsync<QueryResult>().Result;
+ Assert.Equal(2, result.Results.Cast<object>().Count());
+ Assert.Equal(-1, result.TotalCount);
+ }
+
+ private HttpConfiguration GetTestConfiguration()
+ {
+ HttpConfiguration config = new HttpConfiguration();
+ return config;
+ }
+
+ private HttpServer GetTestCatalogServer(HttpConfiguration config)
+ {
+ HttpControllerDispatcher dispatcher = new HttpControllerDispatcher(config);
+
+ HttpRoute route = new HttpRoute("{controller}/{action}", new HttpRouteValueDictionary("Catalog"));
+ config.Routes.Add("catalog", route);
+
+ HttpServer server = new HttpServer(config, dispatcher);
+
+ return server;
+ }
+
+ private HttpServer GetTestCitiesServer(HttpConfiguration config)
+ {
+ HttpControllerDispatcher dispatcher = new HttpControllerDispatcher(config);
+
+ HttpRoute route = new HttpRoute("{controller}/{action}", new HttpRouteValueDictionary("Cities"));
+ config.Routes.Add("cities", route);
+
+ HttpServer server = new HttpServer(config, dispatcher);
+
+ return server;
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Http.Data.Test/DataControllerSubmitTests.cs b/test/Microsoft.Web.Http.Data.Test/DataControllerSubmitTests.cs
new file mode 100644
index 00000000..62bc8602
--- /dev/null
+++ b/test/Microsoft.Web.Http.Data.Test/DataControllerSubmitTests.cs
@@ -0,0 +1,372 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Runtime.Serialization;
+using System.Text;
+using System.Threading;
+using System.Web.Http;
+using System.Web.Http.Controllers;
+using System.Web.Http.Dispatcher;
+using System.Web.Http.Filters;
+using System.Web.Http.Routing;
+using Microsoft.Web.Http.Data.Test.Models;
+using Newtonsoft.Json;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace Microsoft.Web.Http.Data.Test
+{
+ public class DataControllerSubmitTests
+ {
+ // Verify that POSTs directly to CUD actions still go through the submit pipeline
+ [Fact]
+ public void Submit_Proxy_Insert()
+ {
+ Order order = new Order { OrderID = 1, OrderDate = DateTime.Now };
+
+ HttpResponseMessage response = this.ExecuteSelfHostRequest(TestConstants.CatalogUrl + "InsertOrder", "Catalog", order);
+ Order resultOrder = response.Content.ReadAsAsync<Order>().Result;
+ Assert.NotNull(resultOrder);
+ }
+
+ // Submit a changeset with multiple entries
+ [Fact]
+ public void Submit_Multiple_Success()
+ {
+ Order order = new Order { OrderID = 1, OrderDate = DateTime.Now };
+ Product product = new Product { ProductID = 1, ProductName = "Choco Wafers" };
+ ChangeSetEntry[] changeSet = new ChangeSetEntry[] {
+ new ChangeSetEntry { Id = 1, Entity = order, Operation = ChangeOperation.Insert },
+ new ChangeSetEntry { Id = 2, Entity = product, Operation = ChangeOperation.Update }
+ };
+
+ ChangeSetEntry[] resultChangeSet = this.ExecuteSubmit(TestConstants.CatalogUrl + "Submit", "Catalog", changeSet);
+ Assert.Equal(2, resultChangeSet.Length);
+ Assert.True(resultChangeSet.All(p => !p.HasError));
+ }
+
+ // Submit a changeset with one parent object and multiple dependent children
+ [Fact]
+ public void Submit_Tree_Success()
+ {
+ Order order = new Order { OrderID = 1, OrderDate = DateTime.Now };
+ Order_Detail d1 = new Order_Detail { ProductID = 1 };
+ Order_Detail d2 = new Order_Detail { ProductID = 2 };
+ Dictionary<string, int[]> detailsAssociation = new Dictionary<string, int[]>();
+ detailsAssociation.Add("Order_Details", new int[] { 2, 3 });
+ ChangeSetEntry[] changeSet = new ChangeSetEntry[] {
+ new ChangeSetEntry { Id = 1, Entity = order, Operation = ChangeOperation.Insert, Associations = detailsAssociation },
+ new ChangeSetEntry { Id = 2, Entity = d1, Operation = ChangeOperation.Insert },
+ new ChangeSetEntry { Id = 3, Entity = d2, Operation = ChangeOperation.Insert }
+ };
+
+ ChangeSetEntry[] resultChangeSet = this.ExecuteSubmit(TestConstants.CatalogUrl + "Submit", "Catalog", changeSet);
+ Assert.Equal(3, resultChangeSet.Length);
+ Assert.True(resultChangeSet.All(p => !p.HasError));
+ }
+
+ /// <summary>
+ /// End to end validation scenario showing changeset validation. DataAnnotations validation attributes are applied to
+ /// the model by DataController metadata providers (metadata coming all the way from the EF model, as well as "buddy
+ /// class" metadata), and these are validated during changeset validation. The validation results per entity/member are
+ /// returned via the changeset and verified.
+ /// </summary>
+ [Fact]
+ public void Submit_Validation_Failure()
+ {
+ Microsoft.Web.Http.Data.Test.Models.EF.Product newProduct = new Microsoft.Web.Http.Data.Test.Models.EF.Product { ProductID = 1, ProductName = String.Empty, UnitPrice = -1 };
+ Microsoft.Web.Http.Data.Test.Models.EF.Product updateProduct = new Microsoft.Web.Http.Data.Test.Models.EF.Product { ProductID = 1, ProductName = new string('x', 50), UnitPrice = 55.77M };
+ ChangeSetEntry[] changeSet = new ChangeSetEntry[] {
+ new ChangeSetEntry { Id = 1, Entity = newProduct, Operation = ChangeOperation.Insert },
+ new ChangeSetEntry { Id = 2, Entity = updateProduct, Operation = ChangeOperation.Update }
+ };
+
+ HttpResponseMessage response = this.ExecuteSelfHostRequest("http://testhost/NorthwindEFTest/Submit", "NorthwindEFTest", changeSet);
+ changeSet = response.Content.ReadAsAsync<ChangeSetEntry[]>().Result;
+
+ // errors for the new product
+ ValidationResultInfo[] errors = changeSet[0].ValidationErrors.ToArray();
+ Assert.Equal(2, errors.Length);
+ Assert.True(changeSet[0].HasError);
+
+ // validation rule inferred from EF model
+ Assert.Equal("ProductName", errors[0].SourceMemberNames.Single());
+ Assert.Equal("The ProductName field is required.", errors[0].Message);
+
+ // validation rule coming from buddy class
+ Assert.Equal("UnitPrice", errors[1].SourceMemberNames.Single());
+ Assert.Equal("The field UnitPrice must be between 0 and 1000000.", errors[1].Message);
+
+ // errors for the updated product
+ errors = changeSet[1].ValidationErrors.ToArray();
+ Assert.Equal(1, errors.Length);
+ Assert.True(changeSet[1].HasError);
+
+ // validation rule inferred from EF model
+ Assert.Equal("ProductName", errors[0].SourceMemberNames.Single());
+ Assert.Equal("The field ProductName must be a string with a maximum length of 40.", errors[0].Message);
+ }
+
+ [Fact]
+ public void Submit_Authorization_Success()
+ {
+ TestAuthAttribute.Reset();
+
+ Product product = new Product { ProductID = 1, ProductName = "Choco Wafers" };
+ ChangeSetEntry[] changeSet = new ChangeSetEntry[] {
+ new ChangeSetEntry { Id = 1, Entity = product, Operation = ChangeOperation.Update }
+ };
+
+ ChangeSetEntry[] resultChangeSet = this.ExecuteSubmit("http://testhost/TestAuth/Submit", "TestAuth", changeSet);
+ Assert.Equal(1, resultChangeSet.Length);
+ Assert.True(TestAuthAttribute.Log.SequenceEqual(new string[] { "Global", "Class", "SubmitMethod", "UserMethod" }));
+ }
+
+ [Fact]
+ public void Submit_Authorization_Fail_UserMethod()
+ {
+ TestAuthAttribute.Reset();
+
+ Product product = new Product { ProductID = 1, ProductName = "Choco Wafers" };
+ ChangeSetEntry[] changeSet = new ChangeSetEntry[] {
+ new ChangeSetEntry { Id = 1, Entity = product, Operation = ChangeOperation.Update }
+ };
+
+ TestAuthAttribute.FailLevel = "UserMethod";
+ HttpResponseMessage response = this.ExecuteSelfHostRequest("http://testhost/TestAuth/Submit", "TestAuth", changeSet);
+
+ Assert.True(TestAuthAttribute.Log.SequenceEqual(new string[] { "Global", "Class", "SubmitMethod", "UserMethod" }));
+ Assert.Equal("Not Authorized", response.ReasonPhrase);
+ Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
+ }
+
+ [Fact]
+ public void Submit_Authorization_Fail_SubmitMethod()
+ {
+ TestAuthAttribute.Reset();
+
+ Product product = new Product { ProductID = 1, ProductName = "Choco Wafers" };
+ ChangeSetEntry[] changeSet = new ChangeSetEntry[] {
+ new ChangeSetEntry { Id = 1, Entity = product, Operation = ChangeOperation.Update }
+ };
+
+ TestAuthAttribute.FailLevel = "SubmitMethod";
+ HttpResponseMessage response = this.ExecuteSelfHostRequest("http://testhost/TestAuth/Submit", "TestAuth", changeSet);
+
+ Assert.True(TestAuthAttribute.Log.SequenceEqual(new string[] { "Global", "Class", "SubmitMethod" }));
+ Assert.Equal("Not Authorized", response.ReasonPhrase);
+ Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
+ }
+
+ [Fact]
+ public void Submit_Authorization_Fail_Class()
+ {
+ TestAuthAttribute.Reset();
+
+ Product product = new Product { ProductID = 1, ProductName = "Choco Wafers" };
+ ChangeSetEntry[] changeSet = new ChangeSetEntry[] {
+ new ChangeSetEntry { Id = 1, Entity = product, Operation = ChangeOperation.Update }
+ };
+
+ TestAuthAttribute.FailLevel = "Class";
+ HttpResponseMessage response = this.ExecuteSelfHostRequest("http://testhost/TestAuth/Submit", "TestAuth", changeSet);
+
+ Assert.True(TestAuthAttribute.Log.SequenceEqual(new string[] { "Global", "Class" }));
+ Assert.Equal("Not Authorized", response.ReasonPhrase);
+ Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
+ }
+
+ [Fact]
+ public void Submit_Authorization_Fail_Global()
+ {
+ TestAuthAttribute.Reset();
+
+ Product product = new Product { ProductID = 1, ProductName = "Choco Wafers" };
+ ChangeSetEntry[] changeSet = new ChangeSetEntry[] {
+ new ChangeSetEntry { Id = 1, Entity = product, Operation = ChangeOperation.Update }
+ };
+
+ TestAuthAttribute.FailLevel = "Global";
+ HttpResponseMessage response = this.ExecuteSelfHostRequest("http://testhost/TestAuth/Submit", "TestAuth", changeSet);
+
+ Assert.True(TestAuthAttribute.Log.SequenceEqual(new string[] { "Global" }));
+ Assert.Equal("Not Authorized", response.ReasonPhrase);
+ Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
+ }
+
+ // Verify that a CUD operation that isn't supported for a given entity type
+ // results in a server error
+ [Fact]
+ public void Submit_ResolveActions_UnsupportedAction()
+ {
+ Product product = new Product { ProductID = 1, ProductName = "Choco Wafers" };
+ ChangeSetEntry[] changeSet = new ChangeSetEntry[] {
+ new ChangeSetEntry { Id = 1, Entity = product, Operation = ChangeOperation.Delete }
+ };
+
+ HttpConfiguration configuration = new HttpConfiguration();
+ HttpControllerDescriptor controllerDescriptor = new HttpControllerDescriptor(configuration, "NorthwindEFTestController", typeof(NorthwindEFTestController));
+ DataControllerDescription description = DataControllerDescription.GetDescription(controllerDescriptor);
+ Assert.Throws<InvalidOperationException>(
+ () => DataController.ResolveActions(description, changeSet),
+ String.Format(Resource.DataController_InvalidAction, "Delete", "Product"));
+ }
+
+ /// <summary>
+ /// Execute a full roundtrip Submit request for the specified changeset, going through
+ /// the full serialization pipeline.
+ /// </summary>
+ private ChangeSetEntry[] ExecuteSubmit(string url, string controllerName, ChangeSetEntry[] changeSet)
+ {
+ HttpResponseMessage response = this.ExecuteSelfHostRequest(url, controllerName, changeSet);
+ ChangeSetEntry[] resultChangeSet = GetChangesetResponse(response);
+ return changeSet;
+ }
+
+ private HttpResponseMessage ExecuteSelfHostRequest(string url, string controller, object data)
+ {
+ return ExecuteSelfHostRequest(url, controller, data, "application/json");
+ }
+
+ private HttpResponseMessage ExecuteSelfHostRequest(string url, string controller, object data, string mediaType)
+ {
+ HttpConfiguration config = new HttpConfiguration();
+ IHttpRoute routeData;
+ if (!config.Routes.TryGetValue(controller, out routeData))
+ {
+ HttpRoute route = new HttpRoute("{controller}/{action}", new HttpRouteValueDictionary(controller));
+ config.Routes.Add(controller, route);
+ }
+
+ HttpControllerDispatcher dispatcher = new HttpControllerDispatcher(config);
+ HttpServer server = new HttpServer(config, dispatcher);
+
+ string serializedChangeSet = String.Empty;
+ if (mediaType == "application/json")
+ {
+ JsonSerializer serializer = new JsonSerializer() { PreserveReferencesHandling = PreserveReferencesHandling.Objects, TypeNameHandling = TypeNameHandling.All };
+ MemoryStream ms = new MemoryStream();
+ JsonWriter writer = new JsonTextWriter(new StreamWriter(ms));
+ serializer.Serialize(writer, data);
+ writer.Flush();
+ ms.Seek(0, 0);
+ serializedChangeSet = Encoding.UTF8.GetString(ms.GetBuffer()).TrimEnd('\0');
+ }
+ else
+ {
+ DataContractSerializer ser = new DataContractSerializer(data.GetType(), GetTestKnownTypes());
+ MemoryStream ms = new MemoryStream();
+ ser.WriteObject(ms, data);
+ ms.Flush();
+ ms.Seek(0, 0);
+ serializedChangeSet = Encoding.UTF8.GetString(ms.GetBuffer()).TrimEnd('\0');
+ }
+
+ HttpRequestMessage request = TestHelpers.CreateTestMessage(url, HttpMethod.Post, config);
+ request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(mediaType));
+ request.Content = new StringContent(serializedChangeSet, Encoding.UTF8, mediaType);
+
+ return server.SubmitRequestAsync(request, CancellationToken.None).Result;
+ }
+
+ /// <summary>
+ /// For the given Submit response, serialize and deserialize the content. This forces the
+ /// formatter pipeline to run so we can verify that registered serializers are being used
+ /// properly.
+ /// </summary>
+ private ChangeSetEntry[] GetChangesetResponse(HttpResponseMessage responseMessage)
+ {
+ // serialize the content to a stream
+ ObjectContent content = (ObjectContent)responseMessage.Content;
+ MemoryStream ms = new MemoryStream();
+ content.CopyToAsync(ms).Wait();
+ ms.Flush();
+ ms.Seek(0, 0);
+
+ // deserialize based on content type
+ ChangeSetEntry[] changeSet = null;
+ string mediaType = responseMessage.RequestMessage.Content.Headers.ContentType.MediaType;
+ if (mediaType == "application/json")
+ {
+ JsonSerializer ser = new JsonSerializer() { PreserveReferencesHandling = PreserveReferencesHandling.Objects, TypeNameHandling = TypeNameHandling.All };
+ changeSet = (ChangeSetEntry[])ser.Deserialize(new JsonTextReader(new StreamReader(ms)), content.ObjectType);
+ }
+ else
+ {
+ DataContractSerializer ser = new DataContractSerializer(content.ObjectType, GetTestKnownTypes());
+ changeSet = (ChangeSetEntry[])ser.ReadObject(ms);
+ }
+
+ return changeSet;
+ }
+
+ private IEnumerable<Type> GetTestKnownTypes()
+ {
+ List<Type> knownTypes = new List<Type>(new Type[] { typeof(Order), typeof(Product), typeof(Order_Detail) });
+ knownTypes.AddRange(new Type[] { typeof(Microsoft.Web.Http.Data.Test.Models.EF.Order), typeof(Microsoft.Web.Http.Data.Test.Models.EF.Product), typeof(Microsoft.Web.Http.Data.Test.Models.EF.Order_Detail) });
+ return knownTypes;
+ }
+ }
+
+ /// <summary>
+ /// Test controller used for multi-level authorization testing
+ /// </summary>
+ [TestAuth(Level = "Class")]
+ public class TestAuthController : DataController
+ {
+ [TestAuth(Level = "UserMethod")]
+ public void UpdateProduct(Product product)
+ {
+ }
+
+ [TestAuth(Level = "SubmitMethod")]
+ public override bool Submit(ChangeSet changeSet)
+ {
+ return base.Submit(changeSet);
+ }
+
+ protected override void Initialize(HttpControllerContext controllerContext)
+ {
+ controllerContext.Configuration.Filters.Add(new TestAuthAttribute() { Level = "Global" });
+
+ base.Initialize(controllerContext);
+ }
+ }
+
+ /// <summary>
+ /// Test authorization attribute used to verify authorization behavior.
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = true)]
+ public class TestAuthAttribute : AuthorizationFilterAttribute
+ {
+ public string Level;
+
+ public static string FailLevel;
+
+ public static List<string> Log = new List<string>();
+
+ public override void OnAuthorization(HttpActionContext context)
+ {
+ TestAuthAttribute.Log.Add(Level);
+
+ if (FailLevel != null && FailLevel == Level)
+ {
+ HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
+ response.ReasonPhrase = "Not Authorized";
+ context.Response = response;
+ }
+
+ base.OnAuthorization(context);
+ }
+
+ public static void Reset()
+ {
+ FailLevel = null;
+ Log.Clear();
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Http.Data.Test/MetadataExtensionsTests.cs b/test/Microsoft.Web.Http.Data.Test/MetadataExtensionsTests.cs
new file mode 100644
index 00000000..651efa40
--- /dev/null
+++ b/test/Microsoft.Web.Http.Data.Test/MetadataExtensionsTests.cs
@@ -0,0 +1,59 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Json;
+using System.Linq;
+using Microsoft.Web.Http.Data.Helpers;
+using Xunit;
+
+namespace Microsoft.Web.Http.Data.Test
+{
+ public class MetadataExtensionsTests
+ {
+ /// <summary>
+ /// Serialize metadata for a test controller exposing types with various
+ /// metadata annotations.
+ /// </summary>
+ [Fact]
+ public void TestMetadataSerialization()
+ {
+ JsonValue metadata = GenerateMetadata(typeof(TestController));
+ string s = metadata.ToString();
+ Assert.True(s.Contains("{\"range\":[-10,20.5]}"));
+ }
+
+ private static JsonValue GenerateMetadata(Type dataControllerType)
+ {
+ DataControllerDescription desc = DataControllerDescriptionTest.GetDataControllerDescription(dataControllerType);
+ var metadata = DataControllerMetadataGenerator.GetMetadata(desc);
+
+ var jsonData = metadata.Select(m => new KeyValuePair<string, JsonValue>(m.EncodedTypeName, m.ToJsonValue()));
+ JsonValue metadataValue = new JsonObject(jsonData);
+
+ return metadataValue;
+ }
+ }
+
+ public class TestClass
+ {
+ [Key]
+ public int ID { get; set; }
+
+ [Required]
+ public string Name { get; set; }
+
+ [Range(-10.0, 20.5)]
+ public double Number { get; set; }
+
+ [StringLength(5)]
+ public string Address { get; set; }
+ }
+
+ public class TestController : DataController
+ {
+ public TestClass GetTestClass(int id)
+ {
+ return null;
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Http.Data.Test/Microsoft.Web.Http.Data.Test.csproj b/test/Microsoft.Web.Http.Data.Test/Microsoft.Web.Http.Data.Test.csproj
new file mode 100644
index 00000000..bc26417b
--- /dev/null
+++ b/test/Microsoft.Web.Http.Data.Test/Microsoft.Web.Http.Data.Test.csproj
@@ -0,0 +1,143 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProductVersion>9.0.30729</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{81876811-6C36-492A-9609-F0E85990FBC9}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>Microsoft.Web.Http.Data.Test</RootNamespace>
+ <AssemblyName>Microsoft.Web.Http.Data.Test</AssemblyName>
+ <FileAlignment>512</FileAlignment>
+ <ProjectTypeGuids>{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Debug\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Release\Test\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'CodeCoverage' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\CodeCoverage\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="EntityFramework">
+ <HintPath>..\..\packages\EntityFramework.4.1.10331.0\lib\EntityFramework.dll</HintPath>
+ </Reference>
+ <Reference Include="Moq">
+ <HintPath>..\..\packages\Moq.4.0.10827\lib\NET40\Moq.dll</HintPath>
+ </Reference>
+ <Reference Include="Newtonsoft.Json, Version=4.0.8.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>..\..\packages\Newtonsoft.Json.4.0.8\lib\net40\Newtonsoft.Json.dll</HintPath>
+ </Reference>
+ <Reference Include="System" />
+ <Reference Include="System.ComponentModel.DataAnnotations" />
+ <Reference Include="System.Core">
+ <RequiredTargetFramework>3.5</RequiredTargetFramework>
+ </Reference>
+ <Reference Include="System.Data" />
+ <Reference Include="System.Data.Entity" />
+ <Reference Include="System.Net.Http">
+ <HintPath>..\..\packages\Microsoft.Net.Http.2.0.20302.1\lib\net40\System.Net.Http.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Net.Http.WebRequest">
+ <HintPath>..\..\packages\Microsoft.Net.Http.2.0.20302.1\lib\net40\System.Net.Http.WebRequest.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Runtime.Serialization" />
+ <Reference Include="System.Web" />
+ <Reference Include="System.Web.Routing" />
+ <Reference Include="System.XML" />
+ <Reference Include="xunit">
+ <HintPath>..\..\packages\xunit.1.9.0.1566\lib\xunit.dll</HintPath>
+ </Reference>
+ <Reference Include="xunit.extensions">
+ <HintPath>..\..\packages\xunit.extensions.1.9.0.1566\lib\xunit.extensions.dll</HintPath>
+ </Reference>
+ </ItemGroup>
+ <ItemGroup>
+ <CodeAnalysisDependentAssemblyPaths Condition=" '$(VS100COMNTOOLS)' != '' " Include="$(VS100COMNTOOLS)..\IDE\PrivateAssemblies">
+ <Visible>False</Visible>
+ </CodeAnalysisDependentAssemblyPaths>
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="ChangeSetTests.cs" />
+ <Compile Include="Controllers\CitiesController.cs" />
+ <Compile Include="Controllers\NorthwindEFController.cs" />
+ <Compile Include="DataControllerDescriptionTest.cs" />
+ <Compile Include="DataControllerQueryTests.cs" />
+ <Compile Include="DataControllerSubmitTests.cs" />
+ <Compile Include="MetadataExtensionsTests.cs" />
+ <Compile Include="Models\CatalogEntities.cs" />
+ <Compile Include="Models\Cities.cs" />
+ <Compile Include="Models\Northwind.Designer.cs">
+ <AutoGen>True</AutoGen>
+ <DesignTime>True</DesignTime>
+ <DependentUpon>Northwind.edmx</DependentUpon>
+ </Compile>
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="Controllers\CatalogController.cs" />
+ <Compile Include="TestHelpers.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\System.Json\System.Json.csproj">
+ <Project>{F0441BE9-BDC0-4629-BE5A-8765FFAA2481}</Project>
+ <Name>System.Json</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\src\System.Net.Http.Formatting\System.Net.Http.Formatting.csproj">
+ <Project>{668E9021-CE84-49D9-98FB-DF125A9FCDB0}</Project>
+ <Name>System.Net.Http.Formatting</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\src\Microsoft.Web.Http.Data.Helpers\Microsoft.Web.Http.Data.Helpers.csproj">
+ <Project>{B6895A1B-382F-4A69-99EC-E965E19B0AB3}</Project>
+ <Name>Microsoft.Web.Http.Data.Helpers</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\src\System.Web.Http\System.Web.Http.csproj">
+ <Project>{DDC1CE0C-486E-4E35-BB3B-EAB61F8F9440}</Project>
+ <Name>System.Web.Http</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\src\Microsoft.Web.Http.Data.EntityFramework\Microsoft.Web.Http.Data.EntityFramework.csproj">
+ <Project>{653F3946-541C-42D3-BBC1-CE89B392BDA9}</Project>
+ <Name>Microsoft.Web.Http.Data.EntityFramework</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\src\Microsoft.Web.Http.Data\Microsoft.Web.Http.Data.csproj">
+ <Project>{ACE91549-D86E-4EB6-8C2A-5FF51386BB68}</Project>
+ <Name>Microsoft.Web.Http.Data</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\Microsoft.TestCommon\Microsoft.TestCommon.csproj">
+ <Project>{FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0}</Project>
+ <Name>Microsoft.TestCommon</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <EntityDeploy Include="Models\Northwind.edmx">
+ <Generator>EntityModelCodeGenerator</Generator>
+ <LastGenOutput>Northwind.Designer.cs</LastGenOutput>
+ <CustomToolNamespace>System.Web.Http.Data.Test.Models.EF</CustomToolNamespace>
+ </EntityDeploy>
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="packages.config" />
+ </ItemGroup>
+ <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+</Project> \ No newline at end of file
diff --git a/test/Microsoft.Web.Http.Data.Test/Models/CatalogEntities.cs b/test/Microsoft.Web.Http.Data.Test/Models/CatalogEntities.cs
new file mode 100644
index 00000000..a2c497e7
--- /dev/null
+++ b/test/Microsoft.Web.Http.Data.Test/Models/CatalogEntities.cs
@@ -0,0 +1,169 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+
+namespace Microsoft.Web.Http.Data.Test.Models
+{
+ public partial class Category
+ {
+ public Category()
+ {
+ this.Products = new HashSet<Product>();
+ }
+
+ [Key]
+ public int CategoryID { get; set; }
+ public string CategoryName { get; set; }
+ public string Description { get; set; }
+ public byte[] Picture { get; set; }
+
+ public ICollection<Product> Products { get; set; }
+ }
+
+ public partial class Customer
+ {
+ public Customer()
+ {
+ this.Orders = new HashSet<Order>();
+ }
+
+ [Key]
+ public string CustomerID { get; set; }
+ public string CompanyName { get; set; }
+ public string ContactName { get; set; }
+ public string ContactTitle { get; set; }
+ public string Address { get; set; }
+ public string City { get; set; }
+ public string Region { get; set; }
+ public string PostalCode { get; set; }
+ public string Country { get; set; }
+ public string Phone { get; set; }
+ public string Fax { get; set; }
+
+ [Association("Customer_Orders", "CustomerID", "CustomerID")]
+ public ICollection<Order> Orders { get; set; }
+ }
+
+ public partial class Order
+ {
+ private List<Order_Detail> _details;
+
+ [Key]
+ public int OrderID { get; set; }
+ public string CustomerID { get; set; }
+ public Nullable<int> EmployeeID { get; set; }
+ public Nullable<System.DateTime> OrderDate { get; set; }
+ public Nullable<System.DateTime> RequiredDate { get; set; }
+ public Nullable<System.DateTime> ShippedDate { get; set; }
+ public Nullable<int> ShipVia { get; set; }
+ public Nullable<decimal> Freight { get; set; }
+ [StringLength(50, MinimumLength = 0)]
+ public string ShipName { get; set; }
+ public string ShipAddress { get; set; }
+ public string ShipCity { get; set; }
+ public string ShipRegion { get; set; }
+ public string ShipPostalCode { get; set; }
+ public string ShipCountry { get; set; }
+
+ [Association("Customer_Orders", "CustomerID", "CustomerID", IsForeignKey = true)]
+ public Customer Customer { get; set; }
+
+ [Association("Order_Details", "OrderID", "OrderID")]
+ public List<Order_Detail> Order_Details
+ {
+ get
+ {
+ if (this._details == null)
+ {
+ this._details = new List<Order_Detail>();
+ }
+ return this._details;
+ }
+ set
+ {
+ this._details = value;
+ }
+ }
+
+ public Shipper Shipper { get; set; }
+ }
+
+ public partial class Order_Detail
+ {
+ [Key]
+ [Column(Order = 1)]
+ public int OrderID { get; set; }
+ [Key]
+ [Column(Order = 2)]
+ public int ProductID { get; set; }
+ public decimal UnitPrice { get; set; }
+ public short Quantity { get; set; }
+ public float Discount { get; set; }
+
+ public Order Order { get; set; }
+ public Product Product { get; set; }
+ }
+
+ public partial class Shipper
+ {
+ public Shipper()
+ {
+ this.Orders = new HashSet<Order>();
+ }
+
+ [Key]
+ public int ShipperID { get; set; }
+ public string CompanyName { get; set; }
+ public string Phone { get; set; }
+
+ public ICollection<Order> Orders { get; set; }
+ }
+
+ public partial class Product
+ {
+ public Product()
+ {
+ this.Order_Details = new HashSet<Order_Detail>();
+ }
+
+ [Key]
+ public int ProductID { get; set; }
+ public string ProductName { get; set; }
+ public Nullable<int> SupplierID { get; set; }
+ public Nullable<int> CategoryID { get; set; }
+ public string QuantityPerUnit { get; set; }
+ public Nullable<decimal> UnitPrice { get; set; }
+ public Nullable<short> UnitsInStock { get; set; }
+ public Nullable<short> UnitsOnOrder { get; set; }
+ public Nullable<short> ReorderLevel { get; set; }
+ public bool Discontinued { get; set; }
+
+ public Category Category { get; set; }
+ public ICollection<Order_Detail> Order_Details { get; set; }
+ public Supplier Supplier { get; set; }
+ }
+
+ public partial class Supplier
+ {
+ public Supplier()
+ {
+ this.Products = new HashSet<Product>();
+ }
+
+ [Key]
+ public int SupplierID { get; set; }
+ public string CompanyName { get; set; }
+ public string ContactName { get; set; }
+ public string ContactTitle { get; set; }
+ public string Address { get; set; }
+ public string City { get; set; }
+ public string Region { get; set; }
+ public string PostalCode { get; set; }
+ public string Country { get; set; }
+ public string Phone { get; set; }
+ public string Fax { get; set; }
+ public string HomePage { get; set; }
+
+ public virtual ICollection<Product> Products { get; set; }
+ }
+} \ No newline at end of file
diff --git a/test/Microsoft.Web.Http.Data.Test/Models/Cities.cs b/test/Microsoft.Web.Http.Data.Test/Models/Cities.cs
new file mode 100644
index 00000000..2b8360a8
--- /dev/null
+++ b/test/Microsoft.Web.Http.Data.Test/Models/Cities.cs
@@ -0,0 +1,302 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Runtime.Serialization;
+
+namespace Microsoft.Web.Http.Data.Test.Models
+{
+ /// <summary>
+ /// Sample data class
+ /// </summary>
+ /// <remarks>
+ /// This class exposes several data types (City, County, State and Zip) and some sample
+ /// data for each.
+ /// </remarks>
+ public partial class CityData
+ {
+ private List<State> _states;
+ private List<County> _counties;
+ private List<City> _cities;
+ private List<Zip> _zips;
+ private List<ZipWithInfo> _zipsWithInfo;
+ private List<CityWithInfo> _citiesWithInfo;
+
+ public CityData()
+ {
+ _states = new List<State>()
+ {
+ new State() { Name="WA", FullName="Washington", TimeZone = TimeZone.Pacific },
+ new State() { Name="OR", FullName="Oregon", TimeZone = TimeZone.Pacific },
+ new State() { Name="CA", FullName="California", TimeZone = TimeZone.Pacific },
+ new State() { Name="OH", FullName="Ohio", TimeZone = TimeZone.Eastern, ShippingZone=ShippingZone.Eastern }
+ };
+
+ _counties = new List<County>()
+ {
+ new County() { Name="King", StateName="WA" },
+ new County() { Name="Pierce", StateName="WA" },
+ new County() { Name="Snohomish", StateName="WA" },
+
+ new County() { Name="Tillamook", StateName="OR" },
+ new County() { Name="Wallowa", StateName="OR" },
+ new County() { Name="Jackson", StateName="OR" },
+
+ new County() { Name="Orange", StateName="CA" },
+ new County() { Name="Santa Barbara",StateName="CA" },
+
+ new County() { Name="Lucas", StateName="OH" }
+ };
+ foreach (State state in _states)
+ {
+ foreach (County county in _counties.Where(p => p.StateName == state.Name))
+ {
+ state.Counties.Add(county);
+ county.State = state;
+ }
+ }
+
+ _cities = new List<City>()
+ {
+ new CityWithInfo() {Name="Redmond", CountyName="King", StateName="WA", Info="Has Microsoft campus", LastUpdated=DateTime.Now},
+ new CityWithInfo() {Name="Bellevue", CountyName="King", StateName="WA", Info="Means beautiful view", LastUpdated=DateTime.Now},
+ new City() {Name="Duvall", CountyName="King", StateName="WA"},
+ new City() {Name="Carnation", CountyName="King", StateName="WA"},
+ new City() {Name="Everett", CountyName="King", StateName="WA"},
+ new City() {Name="Tacoma", CountyName="Pierce", StateName="WA"},
+
+ new City() {Name="Ashland", CountyName="Jackson", StateName="OR"},
+
+ new City() {Name="Santa Barbara", CountyName="Santa Barbara", StateName="CA"},
+ new City() {Name="Orange", CountyName="Orange", StateName="CA"},
+
+ new City() {Name="Oregon", CountyName="Lucas", StateName="OH"},
+ new City() {Name="Toledo", CountyName="Lucas", StateName="OH"}
+ };
+
+ _citiesWithInfo = new List<CityWithInfo>(this._cities.OfType<CityWithInfo>());
+
+ foreach (County county in _counties)
+ {
+ foreach (City city in _cities.Where(p => p.CountyName == county.Name && p.StateName == county.StateName))
+ {
+ county.Cities.Add(city);
+ city.County = county;
+ }
+ }
+
+ _zips = new List<Zip>()
+ {
+ new Zip() { Code=98053, FourDigit=8625, CityName="Redmond", CountyName="King", StateName="WA" },
+ new ZipWithInfo() { Code=98052, FourDigit=8300, CityName="Redmond", CountyName="King", StateName="WA", Info="Microsoft" },
+ new Zip() { Code=98052, FourDigit=6399, CityName="Redmond", CountyName="King", StateName="WA" },
+ };
+
+ _zipsWithInfo = new List<ZipWithInfo>(this._zips.OfType<ZipWithInfo>());
+
+ foreach (City city in _cities)
+ {
+ foreach (Zip zip in _zips.Where(p => p.CityName == city.Name && p.CountyName == city.CountyName && p.StateName == city.StateName))
+ {
+ city.ZipCodes.Add(zip);
+ zip.City = city;
+ }
+ }
+
+ foreach (CityWithInfo city in _citiesWithInfo)
+ {
+ foreach (ZipWithInfo zip in _zipsWithInfo.Where(p => p.CityName == city.Name && p.CountyName == city.CountyName && p.StateName == city.StateName))
+ {
+ city.ZipCodesWithInfo.Add(zip);
+ zip.City = city;
+ }
+ }
+ }
+
+ public List<State> States { get { return this._states; } }
+ public List<County> Counties { get { return this._counties; } }
+ public List<City> Cities { get { return this._cities; } }
+ public List<CityWithInfo> CitiesWithInfo { get { return this._citiesWithInfo; } }
+ public List<Zip> Zips { get { return this._zips; } }
+ public List<ZipWithInfo> ZipsWithInfo { get { return this._zipsWithInfo; } }
+ }
+
+ /// <summary>
+ /// These types are simple data types that can be used to build
+ /// mocks and simple data stores.
+ /// </summary>
+ public partial class State
+ {
+ private readonly List<County> _counties = new List<County>();
+
+ [Key]
+ public string Name { get; set; }
+ [Key]
+ public string FullName { get; set; }
+ public TimeZone TimeZone { get; set; }
+ public ShippingZone ShippingZone { get; set; }
+ public List<County> Counties { get { return this._counties; } }
+ }
+
+ [DataContract(Name = "CityName", Namespace = "CityNamespace")]
+ public enum ShippingZone
+ {
+ [EnumMember(Value = "P")]
+ Pacific = 0, // default
+
+ [EnumMember(Value = "C")]
+ Central,
+
+ [EnumMember(Value = "E")]
+ Eastern
+ }
+
+ public enum TimeZone
+ {
+ Central,
+ Mountain,
+ Eastern,
+ Pacific
+ }
+
+ public partial class County
+ {
+ public County()
+ {
+ Cities = new List<City>();
+ }
+
+ [Key]
+ public string Name { get; set; }
+ [Key]
+ public string StateName { get; set; }
+
+ [IgnoreDataMember]
+ public State State { get; set; }
+
+ public List<City> Cities { get; set; }
+ }
+
+ [KnownType(typeof(CityWithEditHistory))]
+ [KnownType(typeof(CityWithInfo))]
+ public partial class City
+ {
+ public City()
+ {
+ ZipCodes = new List<Zip>();
+ }
+
+ [Key]
+ public string Name { get; set; }
+ [Key]
+ public string CountyName { get; set; }
+ [Key]
+ public string StateName { get; set; }
+
+ [IgnoreDataMember]
+ public County County { get; set; }
+ public string ZoneName { get; set; }
+ public string CalculatedCounty { get { return this.CountyName; } set { } }
+ public int ZoneID { get; set; }
+
+ public List<Zip> ZipCodes { get; set; }
+
+ public override string ToString()
+ {
+ return this.GetType().Name + " Name=" + this.Name + ", State=" + this.StateName + ", County=" + this.CountyName;
+ }
+
+ public int this[int index]
+ {
+ get
+ {
+ return index;
+ }
+ set
+ {
+ }
+ }
+ }
+
+ public abstract partial class CityWithEditHistory : City
+ {
+ private string _editHistory;
+
+ public CityWithEditHistory()
+ {
+ this.EditHistory = "new";
+ }
+
+ // Edit history always appends, never overwrites
+ public string EditHistory
+ {
+ get
+ {
+ return this._editHistory;
+ }
+ set
+ {
+ this._editHistory = this._editHistory == null ? value : (this._editHistory + "," + value);
+ this.LastUpdated = DateTime.Now;
+ }
+ }
+
+ public DateTime LastUpdated
+ {
+ get;
+ set;
+ }
+
+ public override string ToString()
+ {
+ return base.ToString() + ", History=" + this.EditHistory + ", Updated=" + this.LastUpdated;
+ }
+
+ }
+
+ public partial class CityWithInfo : CityWithEditHistory
+ {
+ public CityWithInfo()
+ {
+ ZipCodesWithInfo = new List<ZipWithInfo>();
+ }
+
+ public string Info
+ {
+ get;
+ set;
+ }
+
+ public List<ZipWithInfo> ZipCodesWithInfo { get; set; }
+
+ public override string ToString()
+ {
+ return base.ToString() + ", Info=" + this.Info;
+ }
+
+ }
+
+ [KnownType(typeof(ZipWithInfo))]
+ public partial class Zip
+ {
+ [Key]
+ public int Code { get; set; }
+ [Key]
+ public int FourDigit { get; set; }
+ public string CityName { get; set; }
+ public string CountyName { get; set; }
+ public string StateName { get; set; }
+
+ [IgnoreDataMember]
+ public City City { get; set; }
+ }
+
+ public partial class ZipWithInfo : Zip
+ {
+ public string Info
+ {
+ get;
+ set;
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Http.Data.Test/Models/Northwind.Designer.cs b/test/Microsoft.Web.Http.Data.Test/Models/Northwind.Designer.cs
new file mode 100644
index 00000000..6d749e42
--- /dev/null
+++ b/test/Microsoft.Web.Http.Data.Test/Models/Northwind.Designer.cs
@@ -0,0 +1,3411 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated from a template.
+//
+// Manual changes to this file may cause unexpected behavior in your application.
+// Manual changes to this file will be overwritten if the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+using System;
+using System.Data.Objects;
+using System.Data.Objects.DataClasses;
+using System.Data.EntityClient;
+using System.ComponentModel;
+using System.Xml.Serialization;
+using System.Runtime.Serialization;
+
+[assembly: EdmSchemaAttribute()]
+#region EDM Relationship Metadata
+
+[assembly: EdmRelationshipAttribute("northwindModel", "FK_Products_Categories", "Categories", System.Data.Metadata.Edm.RelationshipMultiplicity.ZeroOrOne, typeof(Microsoft.Web.Http.Data.Test.Models.EF.Category), "Products", System.Data.Metadata.Edm.RelationshipMultiplicity.Many, typeof(Microsoft.Web.Http.Data.Test.Models.EF.Product), true)]
+[assembly: EdmRelationshipAttribute("northwindModel", "FK_Orders_Customers", "Customers", System.Data.Metadata.Edm.RelationshipMultiplicity.ZeroOrOne, typeof(Microsoft.Web.Http.Data.Test.Models.EF.Customer), "Orders", System.Data.Metadata.Edm.RelationshipMultiplicity.Many, typeof(Microsoft.Web.Http.Data.Test.Models.EF.Order), true)]
+[assembly: EdmRelationshipAttribute("northwindModel", "FK_Employees_Employees", "Employees", System.Data.Metadata.Edm.RelationshipMultiplicity.ZeroOrOne, typeof(Microsoft.Web.Http.Data.Test.Models.EF.Employee), "Employees1", System.Data.Metadata.Edm.RelationshipMultiplicity.Many, typeof(Microsoft.Web.Http.Data.Test.Models.EF.Employee), true)]
+[assembly: EdmRelationshipAttribute("northwindModel", "FK_Orders_Employees", "Employees", System.Data.Metadata.Edm.RelationshipMultiplicity.ZeroOrOne, typeof(Microsoft.Web.Http.Data.Test.Models.EF.Employee), "Orders", System.Data.Metadata.Edm.RelationshipMultiplicity.Many, typeof(Microsoft.Web.Http.Data.Test.Models.EF.Order), true)]
+[assembly: EdmRelationshipAttribute("northwindModel", "FK_Order_Details_Orders", "Orders", System.Data.Metadata.Edm.RelationshipMultiplicity.One, typeof(Microsoft.Web.Http.Data.Test.Models.EF.Order), "Order_Details", System.Data.Metadata.Edm.RelationshipMultiplicity.Many, typeof(Microsoft.Web.Http.Data.Test.Models.EF.Order_Detail), true)]
+[assembly: EdmRelationshipAttribute("northwindModel", "FK_Order_Details_Products", "Products", System.Data.Metadata.Edm.RelationshipMultiplicity.One, typeof(Microsoft.Web.Http.Data.Test.Models.EF.Product), "Order_Details", System.Data.Metadata.Edm.RelationshipMultiplicity.Many, typeof(Microsoft.Web.Http.Data.Test.Models.EF.Order_Detail), true)]
+[assembly: EdmRelationshipAttribute("northwindModel", "FK_Orders_Shippers", "Shippers", System.Data.Metadata.Edm.RelationshipMultiplicity.ZeroOrOne, typeof(Microsoft.Web.Http.Data.Test.Models.EF.Shipper), "Orders", System.Data.Metadata.Edm.RelationshipMultiplicity.Many, typeof(Microsoft.Web.Http.Data.Test.Models.EF.Order), true)]
+[assembly: EdmRelationshipAttribute("northwindModel", "FK_Products_Suppliers", "Suppliers", System.Data.Metadata.Edm.RelationshipMultiplicity.ZeroOrOne, typeof(Microsoft.Web.Http.Data.Test.Models.EF.Supplier), "Products", System.Data.Metadata.Edm.RelationshipMultiplicity.Many, typeof(Microsoft.Web.Http.Data.Test.Models.EF.Product), true)]
+[assembly: EdmRelationshipAttribute("northwindModel", "FK_Territories_Region", "Region", System.Data.Metadata.Edm.RelationshipMultiplicity.One, typeof(Microsoft.Web.Http.Data.Test.Models.EF.Region), "Territories", System.Data.Metadata.Edm.RelationshipMultiplicity.Many, typeof(Microsoft.Web.Http.Data.Test.Models.EF.Territory), true)]
+[assembly: EdmRelationshipAttribute("northwindModel", "CustomerCustomerDemo", "CustomerDemographics", System.Data.Metadata.Edm.RelationshipMultiplicity.Many, typeof(Microsoft.Web.Http.Data.Test.Models.EF.CustomerDemographic), "Customers", System.Data.Metadata.Edm.RelationshipMultiplicity.Many, typeof(Microsoft.Web.Http.Data.Test.Models.EF.Customer))]
+[assembly: EdmRelationshipAttribute("northwindModel", "EmployeeTerritories", "Employees", System.Data.Metadata.Edm.RelationshipMultiplicity.Many, typeof(Microsoft.Web.Http.Data.Test.Models.EF.Employee), "Territories", System.Data.Metadata.Edm.RelationshipMultiplicity.Many, typeof(Microsoft.Web.Http.Data.Test.Models.EF.Territory))]
+
+#endregion
+
+namespace Microsoft.Web.Http.Data.Test.Models.EF
+{
+ #region Contexts
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ public partial class NorthwindEntities : ObjectContext
+ {
+ #region Constructors
+
+ /// <summary>
+ /// Initializes a new NorthwindEntities object using the connection string found in the 'NorthwindEntities' section of the application configuration file.
+ /// </summary>
+ public NorthwindEntities() : base("name=NorthwindEntities", "NorthwindEntities")
+ {
+ this.ContextOptions.LazyLoadingEnabled = true;
+ OnContextCreated();
+ }
+
+ /// <summary>
+ /// Initialize a new NorthwindEntities object.
+ /// </summary>
+ public NorthwindEntities(string connectionString) : base(connectionString, "NorthwindEntities")
+ {
+ this.ContextOptions.LazyLoadingEnabled = true;
+ OnContextCreated();
+ }
+
+ /// <summary>
+ /// Initialize a new NorthwindEntities object.
+ /// </summary>
+ public NorthwindEntities(EntityConnection connection) : base(connection, "NorthwindEntities")
+ {
+ this.ContextOptions.LazyLoadingEnabled = true;
+ OnContextCreated();
+ }
+
+ #endregion
+
+ #region Partial Methods
+
+ partial void OnContextCreated();
+
+ #endregion
+
+ #region ObjectSet Properties
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ public ObjectSet<Category> Categories
+ {
+ get
+ {
+ if ((_Categories == null))
+ {
+ _Categories = base.CreateObjectSet<Category>("Categories");
+ }
+ return _Categories;
+ }
+ }
+ private ObjectSet<Category> _Categories;
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ public ObjectSet<CustomerDemographic> CustomerDemographics
+ {
+ get
+ {
+ if ((_CustomerDemographics == null))
+ {
+ _CustomerDemographics = base.CreateObjectSet<CustomerDemographic>("CustomerDemographics");
+ }
+ return _CustomerDemographics;
+ }
+ }
+ private ObjectSet<CustomerDemographic> _CustomerDemographics;
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ public ObjectSet<Customer> Customers
+ {
+ get
+ {
+ if ((_Customers == null))
+ {
+ _Customers = base.CreateObjectSet<Customer>("Customers");
+ }
+ return _Customers;
+ }
+ }
+ private ObjectSet<Customer> _Customers;
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ public ObjectSet<Employee> Employees
+ {
+ get
+ {
+ if ((_Employees == null))
+ {
+ _Employees = base.CreateObjectSet<Employee>("Employees");
+ }
+ return _Employees;
+ }
+ }
+ private ObjectSet<Employee> _Employees;
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ public ObjectSet<Order_Detail> Order_Details
+ {
+ get
+ {
+ if ((_Order_Details == null))
+ {
+ _Order_Details = base.CreateObjectSet<Order_Detail>("Order_Details");
+ }
+ return _Order_Details;
+ }
+ }
+ private ObjectSet<Order_Detail> _Order_Details;
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ public ObjectSet<Order> Orders
+ {
+ get
+ {
+ if ((_Orders == null))
+ {
+ _Orders = base.CreateObjectSet<Order>("Orders");
+ }
+ return _Orders;
+ }
+ }
+ private ObjectSet<Order> _Orders;
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ public ObjectSet<Product> Products
+ {
+ get
+ {
+ if ((_Products == null))
+ {
+ _Products = base.CreateObjectSet<Product>("Products");
+ }
+ return _Products;
+ }
+ }
+ private ObjectSet<Product> _Products;
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ public ObjectSet<Region> Regions
+ {
+ get
+ {
+ if ((_Regions == null))
+ {
+ _Regions = base.CreateObjectSet<Region>("Regions");
+ }
+ return _Regions;
+ }
+ }
+ private ObjectSet<Region> _Regions;
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ public ObjectSet<Shipper> Shippers
+ {
+ get
+ {
+ if ((_Shippers == null))
+ {
+ _Shippers = base.CreateObjectSet<Shipper>("Shippers");
+ }
+ return _Shippers;
+ }
+ }
+ private ObjectSet<Shipper> _Shippers;
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ public ObjectSet<Supplier> Suppliers
+ {
+ get
+ {
+ if ((_Suppliers == null))
+ {
+ _Suppliers = base.CreateObjectSet<Supplier>("Suppliers");
+ }
+ return _Suppliers;
+ }
+ }
+ private ObjectSet<Supplier> _Suppliers;
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ public ObjectSet<Territory> Territories
+ {
+ get
+ {
+ if ((_Territories == null))
+ {
+ _Territories = base.CreateObjectSet<Territory>("Territories");
+ }
+ return _Territories;
+ }
+ }
+ private ObjectSet<Territory> _Territories;
+
+ #endregion
+ #region AddTo Methods
+
+ /// <summary>
+ /// Deprecated Method for adding a new object to the Categories EntitySet. Consider using the .Add method of the associated ObjectSet&lt;T&gt; property instead.
+ /// </summary>
+ public void AddToCategories(Category category)
+ {
+ base.AddObject("Categories", category);
+ }
+
+ /// <summary>
+ /// Deprecated Method for adding a new object to the CustomerDemographics EntitySet. Consider using the .Add method of the associated ObjectSet&lt;T&gt; property instead.
+ /// </summary>
+ public void AddToCustomerDemographics(CustomerDemographic customerDemographic)
+ {
+ base.AddObject("CustomerDemographics", customerDemographic);
+ }
+
+ /// <summary>
+ /// Deprecated Method for adding a new object to the Customers EntitySet. Consider using the .Add method of the associated ObjectSet&lt;T&gt; property instead.
+ /// </summary>
+ public void AddToCustomers(Customer customer)
+ {
+ base.AddObject("Customers", customer);
+ }
+
+ /// <summary>
+ /// Deprecated Method for adding a new object to the Employees EntitySet. Consider using the .Add method of the associated ObjectSet&lt;T&gt; property instead.
+ /// </summary>
+ public void AddToEmployees(Employee employee)
+ {
+ base.AddObject("Employees", employee);
+ }
+
+ /// <summary>
+ /// Deprecated Method for adding a new object to the Order_Details EntitySet. Consider using the .Add method of the associated ObjectSet&lt;T&gt; property instead.
+ /// </summary>
+ public void AddToOrder_Details(Order_Detail order_Detail)
+ {
+ base.AddObject("Order_Details", order_Detail);
+ }
+
+ /// <summary>
+ /// Deprecated Method for adding a new object to the Orders EntitySet. Consider using the .Add method of the associated ObjectSet&lt;T&gt; property instead.
+ /// </summary>
+ public void AddToOrders(Order order)
+ {
+ base.AddObject("Orders", order);
+ }
+
+ /// <summary>
+ /// Deprecated Method for adding a new object to the Products EntitySet. Consider using the .Add method of the associated ObjectSet&lt;T&gt; property instead.
+ /// </summary>
+ public void AddToProducts(Product product)
+ {
+ base.AddObject("Products", product);
+ }
+
+ /// <summary>
+ /// Deprecated Method for adding a new object to the Regions EntitySet. Consider using the .Add method of the associated ObjectSet&lt;T&gt; property instead.
+ /// </summary>
+ public void AddToRegions(Region region)
+ {
+ base.AddObject("Regions", region);
+ }
+
+ /// <summary>
+ /// Deprecated Method for adding a new object to the Shippers EntitySet. Consider using the .Add method of the associated ObjectSet&lt;T&gt; property instead.
+ /// </summary>
+ public void AddToShippers(Shipper shipper)
+ {
+ base.AddObject("Shippers", shipper);
+ }
+
+ /// <summary>
+ /// Deprecated Method for adding a new object to the Suppliers EntitySet. Consider using the .Add method of the associated ObjectSet&lt;T&gt; property instead.
+ /// </summary>
+ public void AddToSuppliers(Supplier supplier)
+ {
+ base.AddObject("Suppliers", supplier);
+ }
+
+ /// <summary>
+ /// Deprecated Method for adding a new object to the Territories EntitySet. Consider using the .Add method of the associated ObjectSet&lt;T&gt; property instead.
+ /// </summary>
+ public void AddToTerritories(Territory territory)
+ {
+ base.AddObject("Territories", territory);
+ }
+
+ #endregion
+ }
+
+
+ #endregion
+
+ #region Entities
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmEntityTypeAttribute(NamespaceName="northwindModel", Name="Category")]
+ [Serializable()]
+ [DataContractAttribute(IsReference=true)]
+ public partial class Category : EntityObject
+ {
+ #region Factory Method
+
+ /// <summary>
+ /// Create a new Category object.
+ /// </summary>
+ /// <param name="categoryID">Initial value of the CategoryID property.</param>
+ /// <param name="categoryName">Initial value of the CategoryName property.</param>
+ public static Category CreateCategory(global::System.Int32 categoryID, global::System.String categoryName)
+ {
+ Category category = new Category();
+ category.CategoryID = categoryID;
+ category.CategoryName = categoryName;
+ return category;
+ }
+
+ #endregion
+ #region Primitive Properties
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=true, IsNullable=false)]
+ [DataMemberAttribute()]
+ public global::System.Int32 CategoryID
+ {
+ get
+ {
+ return _CategoryID;
+ }
+ set
+ {
+ if (_CategoryID != value)
+ {
+ OnCategoryIDChanging(value);
+ ReportPropertyChanging("CategoryID");
+ _CategoryID = StructuralObject.SetValidValue(value);
+ ReportPropertyChanged("CategoryID");
+ OnCategoryIDChanged();
+ }
+ }
+ }
+ private global::System.Int32 _CategoryID;
+ partial void OnCategoryIDChanging(global::System.Int32 value);
+ partial void OnCategoryIDChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=false)]
+ [DataMemberAttribute()]
+ public global::System.String CategoryName
+ {
+ get
+ {
+ return _CategoryName;
+ }
+ set
+ {
+ OnCategoryNameChanging(value);
+ ReportPropertyChanging("CategoryName");
+ _CategoryName = StructuralObject.SetValidValue(value, false);
+ ReportPropertyChanged("CategoryName");
+ OnCategoryNameChanged();
+ }
+ }
+ private global::System.String _CategoryName;
+ partial void OnCategoryNameChanging(global::System.String value);
+ partial void OnCategoryNameChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public global::System.String Description
+ {
+ get
+ {
+ return _Description;
+ }
+ set
+ {
+ OnDescriptionChanging(value);
+ ReportPropertyChanging("Description");
+ _Description = StructuralObject.SetValidValue(value, true);
+ ReportPropertyChanged("Description");
+ OnDescriptionChanged();
+ }
+ }
+ private global::System.String _Description;
+ partial void OnDescriptionChanging(global::System.String value);
+ partial void OnDescriptionChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public global::System.Byte[] Picture
+ {
+ get
+ {
+ return StructuralObject.GetValidValue(_Picture);
+ }
+ set
+ {
+ OnPictureChanging(value);
+ ReportPropertyChanging("Picture");
+ _Picture = StructuralObject.SetValidValue(value, true);
+ ReportPropertyChanged("Picture");
+ OnPictureChanged();
+ }
+ }
+ private global::System.Byte[] _Picture;
+ partial void OnPictureChanging(global::System.Byte[] value);
+ partial void OnPictureChanged();
+
+ #endregion
+
+ #region Navigation Properties
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [XmlIgnoreAttribute()]
+ [SoapIgnoreAttribute()]
+ [DataMemberAttribute()]
+ [EdmRelationshipNavigationPropertyAttribute("northwindModel", "FK_Products_Categories", "Products")]
+ public EntityCollection<Product> Products
+ {
+ get
+ {
+ return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedCollection<Product>("northwindModel.FK_Products_Categories", "Products");
+ }
+ set
+ {
+ if ((value != null))
+ {
+ ((IEntityWithRelationships)this).RelationshipManager.InitializeRelatedCollection<Product>("northwindModel.FK_Products_Categories", "Products", value);
+ }
+ }
+ }
+
+ #endregion
+ }
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmEntityTypeAttribute(NamespaceName="northwindModel", Name="Customer")]
+ [Serializable()]
+ [DataContractAttribute(IsReference=true)]
+ public partial class Customer : EntityObject
+ {
+ #region Factory Method
+
+ /// <summary>
+ /// Create a new Customer object.
+ /// </summary>
+ /// <param name="customerID">Initial value of the CustomerID property.</param>
+ /// <param name="companyName">Initial value of the CompanyName property.</param>
+ public static Customer CreateCustomer(global::System.String customerID, global::System.String companyName)
+ {
+ Customer customer = new Customer();
+ customer.CustomerID = customerID;
+ customer.CompanyName = companyName;
+ return customer;
+ }
+
+ #endregion
+ #region Primitive Properties
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=true, IsNullable=false)]
+ [DataMemberAttribute()]
+ public global::System.String CustomerID
+ {
+ get
+ {
+ return _CustomerID;
+ }
+ set
+ {
+ if (_CustomerID != value)
+ {
+ OnCustomerIDChanging(value);
+ ReportPropertyChanging("CustomerID");
+ _CustomerID = StructuralObject.SetValidValue(value, false);
+ ReportPropertyChanged("CustomerID");
+ OnCustomerIDChanged();
+ }
+ }
+ }
+ private global::System.String _CustomerID;
+ partial void OnCustomerIDChanging(global::System.String value);
+ partial void OnCustomerIDChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=false)]
+ [DataMemberAttribute()]
+ public global::System.String CompanyName
+ {
+ get
+ {
+ return _CompanyName;
+ }
+ set
+ {
+ OnCompanyNameChanging(value);
+ ReportPropertyChanging("CompanyName");
+ _CompanyName = StructuralObject.SetValidValue(value, false);
+ ReportPropertyChanged("CompanyName");
+ OnCompanyNameChanged();
+ }
+ }
+ private global::System.String _CompanyName;
+ partial void OnCompanyNameChanging(global::System.String value);
+ partial void OnCompanyNameChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public global::System.String ContactName
+ {
+ get
+ {
+ return _ContactName;
+ }
+ set
+ {
+ OnContactNameChanging(value);
+ ReportPropertyChanging("ContactName");
+ _ContactName = StructuralObject.SetValidValue(value, true);
+ ReportPropertyChanged("ContactName");
+ OnContactNameChanged();
+ }
+ }
+ private global::System.String _ContactName;
+ partial void OnContactNameChanging(global::System.String value);
+ partial void OnContactNameChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public global::System.String ContactTitle
+ {
+ get
+ {
+ return _ContactTitle;
+ }
+ set
+ {
+ OnContactTitleChanging(value);
+ ReportPropertyChanging("ContactTitle");
+ _ContactTitle = StructuralObject.SetValidValue(value, true);
+ ReportPropertyChanged("ContactTitle");
+ OnContactTitleChanged();
+ }
+ }
+ private global::System.String _ContactTitle;
+ partial void OnContactTitleChanging(global::System.String value);
+ partial void OnContactTitleChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public global::System.String Address
+ {
+ get
+ {
+ return _Address;
+ }
+ set
+ {
+ OnAddressChanging(value);
+ ReportPropertyChanging("Address");
+ _Address = StructuralObject.SetValidValue(value, true);
+ ReportPropertyChanged("Address");
+ OnAddressChanged();
+ }
+ }
+ private global::System.String _Address;
+ partial void OnAddressChanging(global::System.String value);
+ partial void OnAddressChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public global::System.String City
+ {
+ get
+ {
+ return _City;
+ }
+ set
+ {
+ OnCityChanging(value);
+ ReportPropertyChanging("City");
+ _City = StructuralObject.SetValidValue(value, true);
+ ReportPropertyChanged("City");
+ OnCityChanged();
+ }
+ }
+ private global::System.String _City;
+ partial void OnCityChanging(global::System.String value);
+ partial void OnCityChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public global::System.String Region
+ {
+ get
+ {
+ return _Region;
+ }
+ set
+ {
+ OnRegionChanging(value);
+ ReportPropertyChanging("Region");
+ _Region = StructuralObject.SetValidValue(value, true);
+ ReportPropertyChanged("Region");
+ OnRegionChanged();
+ }
+ }
+ private global::System.String _Region;
+ partial void OnRegionChanging(global::System.String value);
+ partial void OnRegionChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public global::System.String PostalCode
+ {
+ get
+ {
+ return _PostalCode;
+ }
+ set
+ {
+ OnPostalCodeChanging(value);
+ ReportPropertyChanging("PostalCode");
+ _PostalCode = StructuralObject.SetValidValue(value, true);
+ ReportPropertyChanged("PostalCode");
+ OnPostalCodeChanged();
+ }
+ }
+ private global::System.String _PostalCode;
+ partial void OnPostalCodeChanging(global::System.String value);
+ partial void OnPostalCodeChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public global::System.String Country
+ {
+ get
+ {
+ return _Country;
+ }
+ set
+ {
+ OnCountryChanging(value);
+ ReportPropertyChanging("Country");
+ _Country = StructuralObject.SetValidValue(value, true);
+ ReportPropertyChanged("Country");
+ OnCountryChanged();
+ }
+ }
+ private global::System.String _Country;
+ partial void OnCountryChanging(global::System.String value);
+ partial void OnCountryChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public global::System.String Phone
+ {
+ get
+ {
+ return _Phone;
+ }
+ set
+ {
+ OnPhoneChanging(value);
+ ReportPropertyChanging("Phone");
+ _Phone = StructuralObject.SetValidValue(value, true);
+ ReportPropertyChanged("Phone");
+ OnPhoneChanged();
+ }
+ }
+ private global::System.String _Phone;
+ partial void OnPhoneChanging(global::System.String value);
+ partial void OnPhoneChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public global::System.String Fax
+ {
+ get
+ {
+ return _Fax;
+ }
+ set
+ {
+ OnFaxChanging(value);
+ ReportPropertyChanging("Fax");
+ _Fax = StructuralObject.SetValidValue(value, true);
+ ReportPropertyChanged("Fax");
+ OnFaxChanged();
+ }
+ }
+ private global::System.String _Fax;
+ partial void OnFaxChanging(global::System.String value);
+ partial void OnFaxChanged();
+
+ #endregion
+
+ #region Navigation Properties
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [XmlIgnoreAttribute()]
+ [SoapIgnoreAttribute()]
+ [DataMemberAttribute()]
+ [EdmRelationshipNavigationPropertyAttribute("northwindModel", "FK_Orders_Customers", "Orders")]
+ public EntityCollection<Order> Orders
+ {
+ get
+ {
+ return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedCollection<Order>("northwindModel.FK_Orders_Customers", "Orders");
+ }
+ set
+ {
+ if ((value != null))
+ {
+ ((IEntityWithRelationships)this).RelationshipManager.InitializeRelatedCollection<Order>("northwindModel.FK_Orders_Customers", "Orders", value);
+ }
+ }
+ }
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [XmlIgnoreAttribute()]
+ [SoapIgnoreAttribute()]
+ [DataMemberAttribute()]
+ [EdmRelationshipNavigationPropertyAttribute("northwindModel", "CustomerCustomerDemo", "CustomerDemographics")]
+ public EntityCollection<CustomerDemographic> CustomerDemographics
+ {
+ get
+ {
+ return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedCollection<CustomerDemographic>("northwindModel.CustomerCustomerDemo", "CustomerDemographics");
+ }
+ set
+ {
+ if ((value != null))
+ {
+ ((IEntityWithRelationships)this).RelationshipManager.InitializeRelatedCollection<CustomerDemographic>("northwindModel.CustomerCustomerDemo", "CustomerDemographics", value);
+ }
+ }
+ }
+
+ #endregion
+ }
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmEntityTypeAttribute(NamespaceName="northwindModel", Name="CustomerDemographic")]
+ [Serializable()]
+ [DataContractAttribute(IsReference=true)]
+ public partial class CustomerDemographic : EntityObject
+ {
+ #region Factory Method
+
+ /// <summary>
+ /// Create a new CustomerDemographic object.
+ /// </summary>
+ /// <param name="customerTypeID">Initial value of the CustomerTypeID property.</param>
+ public static CustomerDemographic CreateCustomerDemographic(global::System.String customerTypeID)
+ {
+ CustomerDemographic customerDemographic = new CustomerDemographic();
+ customerDemographic.CustomerTypeID = customerTypeID;
+ return customerDemographic;
+ }
+
+ #endregion
+ #region Primitive Properties
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=true, IsNullable=false)]
+ [DataMemberAttribute()]
+ public global::System.String CustomerTypeID
+ {
+ get
+ {
+ return _CustomerTypeID;
+ }
+ set
+ {
+ if (_CustomerTypeID != value)
+ {
+ OnCustomerTypeIDChanging(value);
+ ReportPropertyChanging("CustomerTypeID");
+ _CustomerTypeID = StructuralObject.SetValidValue(value, false);
+ ReportPropertyChanged("CustomerTypeID");
+ OnCustomerTypeIDChanged();
+ }
+ }
+ }
+ private global::System.String _CustomerTypeID;
+ partial void OnCustomerTypeIDChanging(global::System.String value);
+ partial void OnCustomerTypeIDChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public global::System.String CustomerDesc
+ {
+ get
+ {
+ return _CustomerDesc;
+ }
+ set
+ {
+ OnCustomerDescChanging(value);
+ ReportPropertyChanging("CustomerDesc");
+ _CustomerDesc = StructuralObject.SetValidValue(value, true);
+ ReportPropertyChanged("CustomerDesc");
+ OnCustomerDescChanged();
+ }
+ }
+ private global::System.String _CustomerDesc;
+ partial void OnCustomerDescChanging(global::System.String value);
+ partial void OnCustomerDescChanged();
+
+ #endregion
+
+ #region Navigation Properties
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [XmlIgnoreAttribute()]
+ [SoapIgnoreAttribute()]
+ [DataMemberAttribute()]
+ [EdmRelationshipNavigationPropertyAttribute("northwindModel", "CustomerCustomerDemo", "Customers")]
+ public EntityCollection<Customer> Customers
+ {
+ get
+ {
+ return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedCollection<Customer>("northwindModel.CustomerCustomerDemo", "Customers");
+ }
+ set
+ {
+ if ((value != null))
+ {
+ ((IEntityWithRelationships)this).RelationshipManager.InitializeRelatedCollection<Customer>("northwindModel.CustomerCustomerDemo", "Customers", value);
+ }
+ }
+ }
+
+ #endregion
+ }
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmEntityTypeAttribute(NamespaceName="northwindModel", Name="Employee")]
+ [Serializable()]
+ [DataContractAttribute(IsReference=true)]
+ public partial class Employee : EntityObject
+ {
+ #region Factory Method
+
+ /// <summary>
+ /// Create a new Employee object.
+ /// </summary>
+ /// <param name="employeeID">Initial value of the EmployeeID property.</param>
+ /// <param name="lastName">Initial value of the LastName property.</param>
+ /// <param name="firstName">Initial value of the FirstName property.</param>
+ public static Employee CreateEmployee(global::System.Int32 employeeID, global::System.String lastName, global::System.String firstName)
+ {
+ Employee employee = new Employee();
+ employee.EmployeeID = employeeID;
+ employee.LastName = lastName;
+ employee.FirstName = firstName;
+ return employee;
+ }
+
+ #endregion
+ #region Primitive Properties
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=true, IsNullable=false)]
+ [DataMemberAttribute()]
+ public global::System.Int32 EmployeeID
+ {
+ get
+ {
+ return _EmployeeID;
+ }
+ set
+ {
+ if (_EmployeeID != value)
+ {
+ OnEmployeeIDChanging(value);
+ ReportPropertyChanging("EmployeeID");
+ _EmployeeID = StructuralObject.SetValidValue(value);
+ ReportPropertyChanged("EmployeeID");
+ OnEmployeeIDChanged();
+ }
+ }
+ }
+ private global::System.Int32 _EmployeeID;
+ partial void OnEmployeeIDChanging(global::System.Int32 value);
+ partial void OnEmployeeIDChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=false)]
+ [DataMemberAttribute()]
+ public global::System.String LastName
+ {
+ get
+ {
+ return _LastName;
+ }
+ set
+ {
+ OnLastNameChanging(value);
+ ReportPropertyChanging("LastName");
+ _LastName = StructuralObject.SetValidValue(value, false);
+ ReportPropertyChanged("LastName");
+ OnLastNameChanged();
+ }
+ }
+ private global::System.String _LastName;
+ partial void OnLastNameChanging(global::System.String value);
+ partial void OnLastNameChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=false)]
+ [DataMemberAttribute()]
+ public global::System.String FirstName
+ {
+ get
+ {
+ return _FirstName;
+ }
+ set
+ {
+ OnFirstNameChanging(value);
+ ReportPropertyChanging("FirstName");
+ _FirstName = StructuralObject.SetValidValue(value, false);
+ ReportPropertyChanged("FirstName");
+ OnFirstNameChanged();
+ }
+ }
+ private global::System.String _FirstName;
+ partial void OnFirstNameChanging(global::System.String value);
+ partial void OnFirstNameChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public global::System.String Title
+ {
+ get
+ {
+ return _Title;
+ }
+ set
+ {
+ OnTitleChanging(value);
+ ReportPropertyChanging("Title");
+ _Title = StructuralObject.SetValidValue(value, true);
+ ReportPropertyChanged("Title");
+ OnTitleChanged();
+ }
+ }
+ private global::System.String _Title;
+ partial void OnTitleChanging(global::System.String value);
+ partial void OnTitleChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public global::System.String TitleOfCourtesy
+ {
+ get
+ {
+ return _TitleOfCourtesy;
+ }
+ set
+ {
+ OnTitleOfCourtesyChanging(value);
+ ReportPropertyChanging("TitleOfCourtesy");
+ _TitleOfCourtesy = StructuralObject.SetValidValue(value, true);
+ ReportPropertyChanged("TitleOfCourtesy");
+ OnTitleOfCourtesyChanged();
+ }
+ }
+ private global::System.String _TitleOfCourtesy;
+ partial void OnTitleOfCourtesyChanging(global::System.String value);
+ partial void OnTitleOfCourtesyChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public Nullable<global::System.DateTime> BirthDate
+ {
+ get
+ {
+ return _BirthDate;
+ }
+ set
+ {
+ OnBirthDateChanging(value);
+ ReportPropertyChanging("BirthDate");
+ _BirthDate = StructuralObject.SetValidValue(value);
+ ReportPropertyChanged("BirthDate");
+ OnBirthDateChanged();
+ }
+ }
+ private Nullable<global::System.DateTime> _BirthDate;
+ partial void OnBirthDateChanging(Nullable<global::System.DateTime> value);
+ partial void OnBirthDateChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public Nullable<global::System.DateTime> HireDate
+ {
+ get
+ {
+ return _HireDate;
+ }
+ set
+ {
+ OnHireDateChanging(value);
+ ReportPropertyChanging("HireDate");
+ _HireDate = StructuralObject.SetValidValue(value);
+ ReportPropertyChanged("HireDate");
+ OnHireDateChanged();
+ }
+ }
+ private Nullable<global::System.DateTime> _HireDate;
+ partial void OnHireDateChanging(Nullable<global::System.DateTime> value);
+ partial void OnHireDateChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public global::System.String Address
+ {
+ get
+ {
+ return _Address;
+ }
+ set
+ {
+ OnAddressChanging(value);
+ ReportPropertyChanging("Address");
+ _Address = StructuralObject.SetValidValue(value, true);
+ ReportPropertyChanged("Address");
+ OnAddressChanged();
+ }
+ }
+ private global::System.String _Address;
+ partial void OnAddressChanging(global::System.String value);
+ partial void OnAddressChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public global::System.String City
+ {
+ get
+ {
+ return _City;
+ }
+ set
+ {
+ OnCityChanging(value);
+ ReportPropertyChanging("City");
+ _City = StructuralObject.SetValidValue(value, true);
+ ReportPropertyChanged("City");
+ OnCityChanged();
+ }
+ }
+ private global::System.String _City;
+ partial void OnCityChanging(global::System.String value);
+ partial void OnCityChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public global::System.String Region
+ {
+ get
+ {
+ return _Region;
+ }
+ set
+ {
+ OnRegionChanging(value);
+ ReportPropertyChanging("Region");
+ _Region = StructuralObject.SetValidValue(value, true);
+ ReportPropertyChanged("Region");
+ OnRegionChanged();
+ }
+ }
+ private global::System.String _Region;
+ partial void OnRegionChanging(global::System.String value);
+ partial void OnRegionChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public global::System.String PostalCode
+ {
+ get
+ {
+ return _PostalCode;
+ }
+ set
+ {
+ OnPostalCodeChanging(value);
+ ReportPropertyChanging("PostalCode");
+ _PostalCode = StructuralObject.SetValidValue(value, true);
+ ReportPropertyChanged("PostalCode");
+ OnPostalCodeChanged();
+ }
+ }
+ private global::System.String _PostalCode;
+ partial void OnPostalCodeChanging(global::System.String value);
+ partial void OnPostalCodeChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public global::System.String Country
+ {
+ get
+ {
+ return _Country;
+ }
+ set
+ {
+ OnCountryChanging(value);
+ ReportPropertyChanging("Country");
+ _Country = StructuralObject.SetValidValue(value, true);
+ ReportPropertyChanged("Country");
+ OnCountryChanged();
+ }
+ }
+ private global::System.String _Country;
+ partial void OnCountryChanging(global::System.String value);
+ partial void OnCountryChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public global::System.String HomePhone
+ {
+ get
+ {
+ return _HomePhone;
+ }
+ set
+ {
+ OnHomePhoneChanging(value);
+ ReportPropertyChanging("HomePhone");
+ _HomePhone = StructuralObject.SetValidValue(value, true);
+ ReportPropertyChanged("HomePhone");
+ OnHomePhoneChanged();
+ }
+ }
+ private global::System.String _HomePhone;
+ partial void OnHomePhoneChanging(global::System.String value);
+ partial void OnHomePhoneChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public global::System.String Extension
+ {
+ get
+ {
+ return _Extension;
+ }
+ set
+ {
+ OnExtensionChanging(value);
+ ReportPropertyChanging("Extension");
+ _Extension = StructuralObject.SetValidValue(value, true);
+ ReportPropertyChanged("Extension");
+ OnExtensionChanged();
+ }
+ }
+ private global::System.String _Extension;
+ partial void OnExtensionChanging(global::System.String value);
+ partial void OnExtensionChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public global::System.Byte[] Photo
+ {
+ get
+ {
+ return StructuralObject.GetValidValue(_Photo);
+ }
+ set
+ {
+ OnPhotoChanging(value);
+ ReportPropertyChanging("Photo");
+ _Photo = StructuralObject.SetValidValue(value, true);
+ ReportPropertyChanged("Photo");
+ OnPhotoChanged();
+ }
+ }
+ private global::System.Byte[] _Photo;
+ partial void OnPhotoChanging(global::System.Byte[] value);
+ partial void OnPhotoChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public global::System.String Notes
+ {
+ get
+ {
+ return _Notes;
+ }
+ set
+ {
+ OnNotesChanging(value);
+ ReportPropertyChanging("Notes");
+ _Notes = StructuralObject.SetValidValue(value, true);
+ ReportPropertyChanged("Notes");
+ OnNotesChanged();
+ }
+ }
+ private global::System.String _Notes;
+ partial void OnNotesChanging(global::System.String value);
+ partial void OnNotesChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public Nullable<global::System.Int32> ReportsTo
+ {
+ get
+ {
+ return _ReportsTo;
+ }
+ set
+ {
+ OnReportsToChanging(value);
+ ReportPropertyChanging("ReportsTo");
+ _ReportsTo = StructuralObject.SetValidValue(value);
+ ReportPropertyChanged("ReportsTo");
+ OnReportsToChanged();
+ }
+ }
+ private Nullable<global::System.Int32> _ReportsTo;
+ partial void OnReportsToChanging(Nullable<global::System.Int32> value);
+ partial void OnReportsToChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public global::System.String PhotoPath
+ {
+ get
+ {
+ return _PhotoPath;
+ }
+ set
+ {
+ OnPhotoPathChanging(value);
+ ReportPropertyChanging("PhotoPath");
+ _PhotoPath = StructuralObject.SetValidValue(value, true);
+ ReportPropertyChanged("PhotoPath");
+ OnPhotoPathChanged();
+ }
+ }
+ private global::System.String _PhotoPath;
+ partial void OnPhotoPathChanging(global::System.String value);
+ partial void OnPhotoPathChanged();
+
+ #endregion
+
+ #region Navigation Properties
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [XmlIgnoreAttribute()]
+ [SoapIgnoreAttribute()]
+ [DataMemberAttribute()]
+ [EdmRelationshipNavigationPropertyAttribute("northwindModel", "FK_Employees_Employees", "Employees1")]
+ public EntityCollection<Employee> Employees1
+ {
+ get
+ {
+ return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedCollection<Employee>("northwindModel.FK_Employees_Employees", "Employees1");
+ }
+ set
+ {
+ if ((value != null))
+ {
+ ((IEntityWithRelationships)this).RelationshipManager.InitializeRelatedCollection<Employee>("northwindModel.FK_Employees_Employees", "Employees1", value);
+ }
+ }
+ }
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [XmlIgnoreAttribute()]
+ [SoapIgnoreAttribute()]
+ [DataMemberAttribute()]
+ [EdmRelationshipNavigationPropertyAttribute("northwindModel", "FK_Employees_Employees", "Employees")]
+ public Employee Employee1
+ {
+ get
+ {
+ return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<Employee>("northwindModel.FK_Employees_Employees", "Employees").Value;
+ }
+ set
+ {
+ ((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<Employee>("northwindModel.FK_Employees_Employees", "Employees").Value = value;
+ }
+ }
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [BrowsableAttribute(false)]
+ [DataMemberAttribute()]
+ public EntityReference<Employee> Employee1Reference
+ {
+ get
+ {
+ return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<Employee>("northwindModel.FK_Employees_Employees", "Employees");
+ }
+ set
+ {
+ if ((value != null))
+ {
+ ((IEntityWithRelationships)this).RelationshipManager.InitializeRelatedReference<Employee>("northwindModel.FK_Employees_Employees", "Employees", value);
+ }
+ }
+ }
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [XmlIgnoreAttribute()]
+ [SoapIgnoreAttribute()]
+ [DataMemberAttribute()]
+ [EdmRelationshipNavigationPropertyAttribute("northwindModel", "FK_Orders_Employees", "Orders")]
+ public EntityCollection<Order> Orders
+ {
+ get
+ {
+ return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedCollection<Order>("northwindModel.FK_Orders_Employees", "Orders");
+ }
+ set
+ {
+ if ((value != null))
+ {
+ ((IEntityWithRelationships)this).RelationshipManager.InitializeRelatedCollection<Order>("northwindModel.FK_Orders_Employees", "Orders", value);
+ }
+ }
+ }
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [XmlIgnoreAttribute()]
+ [SoapIgnoreAttribute()]
+ [DataMemberAttribute()]
+ [EdmRelationshipNavigationPropertyAttribute("northwindModel", "EmployeeTerritories", "Territories")]
+ public EntityCollection<Territory> Territories
+ {
+ get
+ {
+ return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedCollection<Territory>("northwindModel.EmployeeTerritories", "Territories");
+ }
+ set
+ {
+ if ((value != null))
+ {
+ ((IEntityWithRelationships)this).RelationshipManager.InitializeRelatedCollection<Territory>("northwindModel.EmployeeTerritories", "Territories", value);
+ }
+ }
+ }
+
+ #endregion
+ }
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmEntityTypeAttribute(NamespaceName="northwindModel", Name="Order")]
+ [Serializable()]
+ [DataContractAttribute(IsReference=true)]
+ public partial class Order : EntityObject
+ {
+ #region Factory Method
+
+ /// <summary>
+ /// Create a new Order object.
+ /// </summary>
+ /// <param name="orderID">Initial value of the OrderID property.</param>
+ public static Order CreateOrder(global::System.Int32 orderID)
+ {
+ Order order = new Order();
+ order.OrderID = orderID;
+ return order;
+ }
+
+ #endregion
+ #region Primitive Properties
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=true, IsNullable=false)]
+ [DataMemberAttribute()]
+ public global::System.Int32 OrderID
+ {
+ get
+ {
+ return _OrderID;
+ }
+ set
+ {
+ if (_OrderID != value)
+ {
+ OnOrderIDChanging(value);
+ ReportPropertyChanging("OrderID");
+ _OrderID = StructuralObject.SetValidValue(value);
+ ReportPropertyChanged("OrderID");
+ OnOrderIDChanged();
+ }
+ }
+ }
+ private global::System.Int32 _OrderID;
+ partial void OnOrderIDChanging(global::System.Int32 value);
+ partial void OnOrderIDChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public global::System.String CustomerID
+ {
+ get
+ {
+ return _CustomerID;
+ }
+ set
+ {
+ OnCustomerIDChanging(value);
+ ReportPropertyChanging("CustomerID");
+ _CustomerID = StructuralObject.SetValidValue(value, true);
+ ReportPropertyChanged("CustomerID");
+ OnCustomerIDChanged();
+ }
+ }
+ private global::System.String _CustomerID;
+ partial void OnCustomerIDChanging(global::System.String value);
+ partial void OnCustomerIDChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public Nullable<global::System.Int32> EmployeeID
+ {
+ get
+ {
+ return _EmployeeID;
+ }
+ set
+ {
+ OnEmployeeIDChanging(value);
+ ReportPropertyChanging("EmployeeID");
+ _EmployeeID = StructuralObject.SetValidValue(value);
+ ReportPropertyChanged("EmployeeID");
+ OnEmployeeIDChanged();
+ }
+ }
+ private Nullable<global::System.Int32> _EmployeeID;
+ partial void OnEmployeeIDChanging(Nullable<global::System.Int32> value);
+ partial void OnEmployeeIDChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public Nullable<global::System.DateTime> OrderDate
+ {
+ get
+ {
+ return _OrderDate;
+ }
+ set
+ {
+ OnOrderDateChanging(value);
+ ReportPropertyChanging("OrderDate");
+ _OrderDate = StructuralObject.SetValidValue(value);
+ ReportPropertyChanged("OrderDate");
+ OnOrderDateChanged();
+ }
+ }
+ private Nullable<global::System.DateTime> _OrderDate;
+ partial void OnOrderDateChanging(Nullable<global::System.DateTime> value);
+ partial void OnOrderDateChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public Nullable<global::System.DateTime> RequiredDate
+ {
+ get
+ {
+ return _RequiredDate;
+ }
+ set
+ {
+ OnRequiredDateChanging(value);
+ ReportPropertyChanging("RequiredDate");
+ _RequiredDate = StructuralObject.SetValidValue(value);
+ ReportPropertyChanged("RequiredDate");
+ OnRequiredDateChanged();
+ }
+ }
+ private Nullable<global::System.DateTime> _RequiredDate;
+ partial void OnRequiredDateChanging(Nullable<global::System.DateTime> value);
+ partial void OnRequiredDateChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public Nullable<global::System.DateTime> ShippedDate
+ {
+ get
+ {
+ return _ShippedDate;
+ }
+ set
+ {
+ OnShippedDateChanging(value);
+ ReportPropertyChanging("ShippedDate");
+ _ShippedDate = StructuralObject.SetValidValue(value);
+ ReportPropertyChanged("ShippedDate");
+ OnShippedDateChanged();
+ }
+ }
+ private Nullable<global::System.DateTime> _ShippedDate;
+ partial void OnShippedDateChanging(Nullable<global::System.DateTime> value);
+ partial void OnShippedDateChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public Nullable<global::System.Int32> ShipVia
+ {
+ get
+ {
+ return _ShipVia;
+ }
+ set
+ {
+ OnShipViaChanging(value);
+ ReportPropertyChanging("ShipVia");
+ _ShipVia = StructuralObject.SetValidValue(value);
+ ReportPropertyChanged("ShipVia");
+ OnShipViaChanged();
+ }
+ }
+ private Nullable<global::System.Int32> _ShipVia;
+ partial void OnShipViaChanging(Nullable<global::System.Int32> value);
+ partial void OnShipViaChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public Nullable<global::System.Decimal> Freight
+ {
+ get
+ {
+ return _Freight;
+ }
+ set
+ {
+ OnFreightChanging(value);
+ ReportPropertyChanging("Freight");
+ _Freight = StructuralObject.SetValidValue(value);
+ ReportPropertyChanged("Freight");
+ OnFreightChanged();
+ }
+ }
+ private Nullable<global::System.Decimal> _Freight;
+ partial void OnFreightChanging(Nullable<global::System.Decimal> value);
+ partial void OnFreightChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public global::System.String ShipName
+ {
+ get
+ {
+ return _ShipName;
+ }
+ set
+ {
+ OnShipNameChanging(value);
+ ReportPropertyChanging("ShipName");
+ _ShipName = StructuralObject.SetValidValue(value, true);
+ ReportPropertyChanged("ShipName");
+ OnShipNameChanged();
+ }
+ }
+ private global::System.String _ShipName;
+ partial void OnShipNameChanging(global::System.String value);
+ partial void OnShipNameChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public global::System.String ShipAddress
+ {
+ get
+ {
+ return _ShipAddress;
+ }
+ set
+ {
+ OnShipAddressChanging(value);
+ ReportPropertyChanging("ShipAddress");
+ _ShipAddress = StructuralObject.SetValidValue(value, true);
+ ReportPropertyChanged("ShipAddress");
+ OnShipAddressChanged();
+ }
+ }
+ private global::System.String _ShipAddress;
+ partial void OnShipAddressChanging(global::System.String value);
+ partial void OnShipAddressChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public global::System.String ShipCity
+ {
+ get
+ {
+ return _ShipCity;
+ }
+ set
+ {
+ OnShipCityChanging(value);
+ ReportPropertyChanging("ShipCity");
+ _ShipCity = StructuralObject.SetValidValue(value, true);
+ ReportPropertyChanged("ShipCity");
+ OnShipCityChanged();
+ }
+ }
+ private global::System.String _ShipCity;
+ partial void OnShipCityChanging(global::System.String value);
+ partial void OnShipCityChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public global::System.String ShipRegion
+ {
+ get
+ {
+ return _ShipRegion;
+ }
+ set
+ {
+ OnShipRegionChanging(value);
+ ReportPropertyChanging("ShipRegion");
+ _ShipRegion = StructuralObject.SetValidValue(value, true);
+ ReportPropertyChanged("ShipRegion");
+ OnShipRegionChanged();
+ }
+ }
+ private global::System.String _ShipRegion;
+ partial void OnShipRegionChanging(global::System.String value);
+ partial void OnShipRegionChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public global::System.String ShipPostalCode
+ {
+ get
+ {
+ return _ShipPostalCode;
+ }
+ set
+ {
+ OnShipPostalCodeChanging(value);
+ ReportPropertyChanging("ShipPostalCode");
+ _ShipPostalCode = StructuralObject.SetValidValue(value, true);
+ ReportPropertyChanged("ShipPostalCode");
+ OnShipPostalCodeChanged();
+ }
+ }
+ private global::System.String _ShipPostalCode;
+ partial void OnShipPostalCodeChanging(global::System.String value);
+ partial void OnShipPostalCodeChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public global::System.String ShipCountry
+ {
+ get
+ {
+ return _ShipCountry;
+ }
+ set
+ {
+ OnShipCountryChanging(value);
+ ReportPropertyChanging("ShipCountry");
+ _ShipCountry = StructuralObject.SetValidValue(value, true);
+ ReportPropertyChanged("ShipCountry");
+ OnShipCountryChanged();
+ }
+ }
+ private global::System.String _ShipCountry;
+ partial void OnShipCountryChanging(global::System.String value);
+ partial void OnShipCountryChanged();
+
+ #endregion
+
+ #region Navigation Properties
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [XmlIgnoreAttribute()]
+ [SoapIgnoreAttribute()]
+ [DataMemberAttribute()]
+ [EdmRelationshipNavigationPropertyAttribute("northwindModel", "FK_Orders_Customers", "Customers")]
+ public Customer Customer
+ {
+ get
+ {
+ return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<Customer>("northwindModel.FK_Orders_Customers", "Customers").Value;
+ }
+ set
+ {
+ ((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<Customer>("northwindModel.FK_Orders_Customers", "Customers").Value = value;
+ }
+ }
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [BrowsableAttribute(false)]
+ [DataMemberAttribute()]
+ public EntityReference<Customer> CustomerReference
+ {
+ get
+ {
+ return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<Customer>("northwindModel.FK_Orders_Customers", "Customers");
+ }
+ set
+ {
+ if ((value != null))
+ {
+ ((IEntityWithRelationships)this).RelationshipManager.InitializeRelatedReference<Customer>("northwindModel.FK_Orders_Customers", "Customers", value);
+ }
+ }
+ }
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [XmlIgnoreAttribute()]
+ [SoapIgnoreAttribute()]
+ [DataMemberAttribute()]
+ [EdmRelationshipNavigationPropertyAttribute("northwindModel", "FK_Orders_Employees", "Employees")]
+ public Employee Employee
+ {
+ get
+ {
+ return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<Employee>("northwindModel.FK_Orders_Employees", "Employees").Value;
+ }
+ set
+ {
+ ((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<Employee>("northwindModel.FK_Orders_Employees", "Employees").Value = value;
+ }
+ }
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [BrowsableAttribute(false)]
+ [DataMemberAttribute()]
+ public EntityReference<Employee> EmployeeReference
+ {
+ get
+ {
+ return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<Employee>("northwindModel.FK_Orders_Employees", "Employees");
+ }
+ set
+ {
+ if ((value != null))
+ {
+ ((IEntityWithRelationships)this).RelationshipManager.InitializeRelatedReference<Employee>("northwindModel.FK_Orders_Employees", "Employees", value);
+ }
+ }
+ }
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [XmlIgnoreAttribute()]
+ [SoapIgnoreAttribute()]
+ [DataMemberAttribute()]
+ [EdmRelationshipNavigationPropertyAttribute("northwindModel", "FK_Order_Details_Orders", "Order_Details")]
+ public EntityCollection<Order_Detail> Order_Details
+ {
+ get
+ {
+ return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedCollection<Order_Detail>("northwindModel.FK_Order_Details_Orders", "Order_Details");
+ }
+ set
+ {
+ if ((value != null))
+ {
+ ((IEntityWithRelationships)this).RelationshipManager.InitializeRelatedCollection<Order_Detail>("northwindModel.FK_Order_Details_Orders", "Order_Details", value);
+ }
+ }
+ }
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [XmlIgnoreAttribute()]
+ [SoapIgnoreAttribute()]
+ [DataMemberAttribute()]
+ [EdmRelationshipNavigationPropertyAttribute("northwindModel", "FK_Orders_Shippers", "Shippers")]
+ public Shipper Shipper
+ {
+ get
+ {
+ return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<Shipper>("northwindModel.FK_Orders_Shippers", "Shippers").Value;
+ }
+ set
+ {
+ ((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<Shipper>("northwindModel.FK_Orders_Shippers", "Shippers").Value = value;
+ }
+ }
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [BrowsableAttribute(false)]
+ [DataMemberAttribute()]
+ public EntityReference<Shipper> ShipperReference
+ {
+ get
+ {
+ return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<Shipper>("northwindModel.FK_Orders_Shippers", "Shippers");
+ }
+ set
+ {
+ if ((value != null))
+ {
+ ((IEntityWithRelationships)this).RelationshipManager.InitializeRelatedReference<Shipper>("northwindModel.FK_Orders_Shippers", "Shippers", value);
+ }
+ }
+ }
+
+ #endregion
+ }
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmEntityTypeAttribute(NamespaceName="northwindModel", Name="Order_Detail")]
+ [Serializable()]
+ [DataContractAttribute(IsReference=true)]
+ public partial class Order_Detail : EntityObject
+ {
+ #region Factory Method
+
+ /// <summary>
+ /// Create a new Order_Detail object.
+ /// </summary>
+ /// <param name="orderID">Initial value of the OrderID property.</param>
+ /// <param name="productID">Initial value of the ProductID property.</param>
+ /// <param name="unitPrice">Initial value of the UnitPrice property.</param>
+ /// <param name="quantity">Initial value of the Quantity property.</param>
+ /// <param name="discount">Initial value of the Discount property.</param>
+ public static Order_Detail CreateOrder_Detail(global::System.Int32 orderID, global::System.Int32 productID, global::System.Decimal unitPrice, global::System.Int16 quantity, global::System.Single discount)
+ {
+ Order_Detail order_Detail = new Order_Detail();
+ order_Detail.OrderID = orderID;
+ order_Detail.ProductID = productID;
+ order_Detail.UnitPrice = unitPrice;
+ order_Detail.Quantity = quantity;
+ order_Detail.Discount = discount;
+ return order_Detail;
+ }
+
+ #endregion
+ #region Primitive Properties
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=true, IsNullable=false)]
+ [DataMemberAttribute()]
+ public global::System.Int32 OrderID
+ {
+ get
+ {
+ return _OrderID;
+ }
+ set
+ {
+ if (_OrderID != value)
+ {
+ OnOrderIDChanging(value);
+ ReportPropertyChanging("OrderID");
+ _OrderID = StructuralObject.SetValidValue(value);
+ ReportPropertyChanged("OrderID");
+ OnOrderIDChanged();
+ }
+ }
+ }
+ private global::System.Int32 _OrderID;
+ partial void OnOrderIDChanging(global::System.Int32 value);
+ partial void OnOrderIDChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=true, IsNullable=false)]
+ [DataMemberAttribute()]
+ public global::System.Int32 ProductID
+ {
+ get
+ {
+ return _ProductID;
+ }
+ set
+ {
+ if (_ProductID != value)
+ {
+ OnProductIDChanging(value);
+ ReportPropertyChanging("ProductID");
+ _ProductID = StructuralObject.SetValidValue(value);
+ ReportPropertyChanged("ProductID");
+ OnProductIDChanged();
+ }
+ }
+ }
+ private global::System.Int32 _ProductID;
+ partial void OnProductIDChanging(global::System.Int32 value);
+ partial void OnProductIDChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=false)]
+ [DataMemberAttribute()]
+ public global::System.Decimal UnitPrice
+ {
+ get
+ {
+ return _UnitPrice;
+ }
+ set
+ {
+ OnUnitPriceChanging(value);
+ ReportPropertyChanging("UnitPrice");
+ _UnitPrice = StructuralObject.SetValidValue(value);
+ ReportPropertyChanged("UnitPrice");
+ OnUnitPriceChanged();
+ }
+ }
+ private global::System.Decimal _UnitPrice;
+ partial void OnUnitPriceChanging(global::System.Decimal value);
+ partial void OnUnitPriceChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=false)]
+ [DataMemberAttribute()]
+ public global::System.Int16 Quantity
+ {
+ get
+ {
+ return _Quantity;
+ }
+ set
+ {
+ OnQuantityChanging(value);
+ ReportPropertyChanging("Quantity");
+ _Quantity = StructuralObject.SetValidValue(value);
+ ReportPropertyChanged("Quantity");
+ OnQuantityChanged();
+ }
+ }
+ private global::System.Int16 _Quantity;
+ partial void OnQuantityChanging(global::System.Int16 value);
+ partial void OnQuantityChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=false)]
+ [DataMemberAttribute()]
+ public global::System.Single Discount
+ {
+ get
+ {
+ return _Discount;
+ }
+ set
+ {
+ OnDiscountChanging(value);
+ ReportPropertyChanging("Discount");
+ _Discount = StructuralObject.SetValidValue(value);
+ ReportPropertyChanged("Discount");
+ OnDiscountChanged();
+ }
+ }
+ private global::System.Single _Discount;
+ partial void OnDiscountChanging(global::System.Single value);
+ partial void OnDiscountChanged();
+
+ #endregion
+
+ #region Navigation Properties
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [XmlIgnoreAttribute()]
+ [SoapIgnoreAttribute()]
+ [DataMemberAttribute()]
+ [EdmRelationshipNavigationPropertyAttribute("northwindModel", "FK_Order_Details_Orders", "Orders")]
+ public Order Order
+ {
+ get
+ {
+ return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<Order>("northwindModel.FK_Order_Details_Orders", "Orders").Value;
+ }
+ set
+ {
+ ((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<Order>("northwindModel.FK_Order_Details_Orders", "Orders").Value = value;
+ }
+ }
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [BrowsableAttribute(false)]
+ [DataMemberAttribute()]
+ public EntityReference<Order> OrderReference
+ {
+ get
+ {
+ return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<Order>("northwindModel.FK_Order_Details_Orders", "Orders");
+ }
+ set
+ {
+ if ((value != null))
+ {
+ ((IEntityWithRelationships)this).RelationshipManager.InitializeRelatedReference<Order>("northwindModel.FK_Order_Details_Orders", "Orders", value);
+ }
+ }
+ }
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [XmlIgnoreAttribute()]
+ [SoapIgnoreAttribute()]
+ [DataMemberAttribute()]
+ [EdmRelationshipNavigationPropertyAttribute("northwindModel", "FK_Order_Details_Products", "Products")]
+ public Product Product
+ {
+ get
+ {
+ return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<Product>("northwindModel.FK_Order_Details_Products", "Products").Value;
+ }
+ set
+ {
+ ((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<Product>("northwindModel.FK_Order_Details_Products", "Products").Value = value;
+ }
+ }
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [BrowsableAttribute(false)]
+ [DataMemberAttribute()]
+ public EntityReference<Product> ProductReference
+ {
+ get
+ {
+ return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<Product>("northwindModel.FK_Order_Details_Products", "Products");
+ }
+ set
+ {
+ if ((value != null))
+ {
+ ((IEntityWithRelationships)this).RelationshipManager.InitializeRelatedReference<Product>("northwindModel.FK_Order_Details_Products", "Products", value);
+ }
+ }
+ }
+
+ #endregion
+ }
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmEntityTypeAttribute(NamespaceName="northwindModel", Name="Product")]
+ [Serializable()]
+ [DataContractAttribute(IsReference=true)]
+ public partial class Product : EntityObject
+ {
+ #region Factory Method
+
+ /// <summary>
+ /// Create a new Product object.
+ /// </summary>
+ /// <param name="productID">Initial value of the ProductID property.</param>
+ /// <param name="productName">Initial value of the ProductName property.</param>
+ /// <param name="discontinued">Initial value of the Discontinued property.</param>
+ public static Product CreateProduct(global::System.Int32 productID, global::System.String productName, global::System.Boolean discontinued)
+ {
+ Product product = new Product();
+ product.ProductID = productID;
+ product.ProductName = productName;
+ product.Discontinued = discontinued;
+ return product;
+ }
+
+ #endregion
+ #region Primitive Properties
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=true, IsNullable=false)]
+ [DataMemberAttribute()]
+ public global::System.Int32 ProductID
+ {
+ get
+ {
+ return _ProductID;
+ }
+ set
+ {
+ if (_ProductID != value)
+ {
+ OnProductIDChanging(value);
+ ReportPropertyChanging("ProductID");
+ _ProductID = StructuralObject.SetValidValue(value);
+ ReportPropertyChanged("ProductID");
+ OnProductIDChanged();
+ }
+ }
+ }
+ private global::System.Int32 _ProductID;
+ partial void OnProductIDChanging(global::System.Int32 value);
+ partial void OnProductIDChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=false)]
+ [DataMemberAttribute()]
+ public global::System.String ProductName
+ {
+ get
+ {
+ return _ProductName;
+ }
+ set
+ {
+ OnProductNameChanging(value);
+ ReportPropertyChanging("ProductName");
+ _ProductName = StructuralObject.SetValidValue(value, false);
+ ReportPropertyChanged("ProductName");
+ OnProductNameChanged();
+ }
+ }
+ private global::System.String _ProductName;
+ partial void OnProductNameChanging(global::System.String value);
+ partial void OnProductNameChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public Nullable<global::System.Int32> SupplierID
+ {
+ get
+ {
+ return _SupplierID;
+ }
+ set
+ {
+ OnSupplierIDChanging(value);
+ ReportPropertyChanging("SupplierID");
+ _SupplierID = StructuralObject.SetValidValue(value);
+ ReportPropertyChanged("SupplierID");
+ OnSupplierIDChanged();
+ }
+ }
+ private Nullable<global::System.Int32> _SupplierID;
+ partial void OnSupplierIDChanging(Nullable<global::System.Int32> value);
+ partial void OnSupplierIDChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public Nullable<global::System.Int32> CategoryID
+ {
+ get
+ {
+ return _CategoryID;
+ }
+ set
+ {
+ OnCategoryIDChanging(value);
+ ReportPropertyChanging("CategoryID");
+ _CategoryID = StructuralObject.SetValidValue(value);
+ ReportPropertyChanged("CategoryID");
+ OnCategoryIDChanged();
+ }
+ }
+ private Nullable<global::System.Int32> _CategoryID;
+ partial void OnCategoryIDChanging(Nullable<global::System.Int32> value);
+ partial void OnCategoryIDChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public global::System.String QuantityPerUnit
+ {
+ get
+ {
+ return _QuantityPerUnit;
+ }
+ set
+ {
+ OnQuantityPerUnitChanging(value);
+ ReportPropertyChanging("QuantityPerUnit");
+ _QuantityPerUnit = StructuralObject.SetValidValue(value, true);
+ ReportPropertyChanged("QuantityPerUnit");
+ OnQuantityPerUnitChanged();
+ }
+ }
+ private global::System.String _QuantityPerUnit;
+ partial void OnQuantityPerUnitChanging(global::System.String value);
+ partial void OnQuantityPerUnitChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public Nullable<global::System.Decimal> UnitPrice
+ {
+ get
+ {
+ return _UnitPrice;
+ }
+ set
+ {
+ OnUnitPriceChanging(value);
+ ReportPropertyChanging("UnitPrice");
+ _UnitPrice = StructuralObject.SetValidValue(value);
+ ReportPropertyChanged("UnitPrice");
+ OnUnitPriceChanged();
+ }
+ }
+ private Nullable<global::System.Decimal> _UnitPrice;
+ partial void OnUnitPriceChanging(Nullable<global::System.Decimal> value);
+ partial void OnUnitPriceChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public Nullable<global::System.Int16> UnitsInStock
+ {
+ get
+ {
+ return _UnitsInStock;
+ }
+ set
+ {
+ OnUnitsInStockChanging(value);
+ ReportPropertyChanging("UnitsInStock");
+ _UnitsInStock = StructuralObject.SetValidValue(value);
+ ReportPropertyChanged("UnitsInStock");
+ OnUnitsInStockChanged();
+ }
+ }
+ private Nullable<global::System.Int16> _UnitsInStock;
+ partial void OnUnitsInStockChanging(Nullable<global::System.Int16> value);
+ partial void OnUnitsInStockChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public Nullable<global::System.Int16> UnitsOnOrder
+ {
+ get
+ {
+ return _UnitsOnOrder;
+ }
+ set
+ {
+ OnUnitsOnOrderChanging(value);
+ ReportPropertyChanging("UnitsOnOrder");
+ _UnitsOnOrder = StructuralObject.SetValidValue(value);
+ ReportPropertyChanged("UnitsOnOrder");
+ OnUnitsOnOrderChanged();
+ }
+ }
+ private Nullable<global::System.Int16> _UnitsOnOrder;
+ partial void OnUnitsOnOrderChanging(Nullable<global::System.Int16> value);
+ partial void OnUnitsOnOrderChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public Nullable<global::System.Int16> ReorderLevel
+ {
+ get
+ {
+ return _ReorderLevel;
+ }
+ set
+ {
+ OnReorderLevelChanging(value);
+ ReportPropertyChanging("ReorderLevel");
+ _ReorderLevel = StructuralObject.SetValidValue(value);
+ ReportPropertyChanged("ReorderLevel");
+ OnReorderLevelChanged();
+ }
+ }
+ private Nullable<global::System.Int16> _ReorderLevel;
+ partial void OnReorderLevelChanging(Nullable<global::System.Int16> value);
+ partial void OnReorderLevelChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=false)]
+ [DataMemberAttribute()]
+ public global::System.Boolean Discontinued
+ {
+ get
+ {
+ return _Discontinued;
+ }
+ set
+ {
+ OnDiscontinuedChanging(value);
+ ReportPropertyChanging("Discontinued");
+ _Discontinued = StructuralObject.SetValidValue(value);
+ ReportPropertyChanged("Discontinued");
+ OnDiscontinuedChanged();
+ }
+ }
+ private global::System.Boolean _Discontinued;
+ partial void OnDiscontinuedChanging(global::System.Boolean value);
+ partial void OnDiscontinuedChanged();
+
+ #endregion
+
+ #region Navigation Properties
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [XmlIgnoreAttribute()]
+ [SoapIgnoreAttribute()]
+ [DataMemberAttribute()]
+ [EdmRelationshipNavigationPropertyAttribute("northwindModel", "FK_Products_Categories", "Categories")]
+ public Category Category
+ {
+ get
+ {
+ return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<Category>("northwindModel.FK_Products_Categories", "Categories").Value;
+ }
+ set
+ {
+ ((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<Category>("northwindModel.FK_Products_Categories", "Categories").Value = value;
+ }
+ }
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [BrowsableAttribute(false)]
+ [DataMemberAttribute()]
+ public EntityReference<Category> CategoryReference
+ {
+ get
+ {
+ return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<Category>("northwindModel.FK_Products_Categories", "Categories");
+ }
+ set
+ {
+ if ((value != null))
+ {
+ ((IEntityWithRelationships)this).RelationshipManager.InitializeRelatedReference<Category>("northwindModel.FK_Products_Categories", "Categories", value);
+ }
+ }
+ }
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [XmlIgnoreAttribute()]
+ [SoapIgnoreAttribute()]
+ [DataMemberAttribute()]
+ [EdmRelationshipNavigationPropertyAttribute("northwindModel", "FK_Order_Details_Products", "Order_Details")]
+ public EntityCollection<Order_Detail> Order_Details
+ {
+ get
+ {
+ return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedCollection<Order_Detail>("northwindModel.FK_Order_Details_Products", "Order_Details");
+ }
+ set
+ {
+ if ((value != null))
+ {
+ ((IEntityWithRelationships)this).RelationshipManager.InitializeRelatedCollection<Order_Detail>("northwindModel.FK_Order_Details_Products", "Order_Details", value);
+ }
+ }
+ }
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [XmlIgnoreAttribute()]
+ [SoapIgnoreAttribute()]
+ [DataMemberAttribute()]
+ [EdmRelationshipNavigationPropertyAttribute("northwindModel", "FK_Products_Suppliers", "Suppliers")]
+ public Supplier Supplier
+ {
+ get
+ {
+ return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<Supplier>("northwindModel.FK_Products_Suppliers", "Suppliers").Value;
+ }
+ set
+ {
+ ((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<Supplier>("northwindModel.FK_Products_Suppliers", "Suppliers").Value = value;
+ }
+ }
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [BrowsableAttribute(false)]
+ [DataMemberAttribute()]
+ public EntityReference<Supplier> SupplierReference
+ {
+ get
+ {
+ return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<Supplier>("northwindModel.FK_Products_Suppliers", "Suppliers");
+ }
+ set
+ {
+ if ((value != null))
+ {
+ ((IEntityWithRelationships)this).RelationshipManager.InitializeRelatedReference<Supplier>("northwindModel.FK_Products_Suppliers", "Suppliers", value);
+ }
+ }
+ }
+
+ #endregion
+ }
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmEntityTypeAttribute(NamespaceName="northwindModel", Name="Region")]
+ [Serializable()]
+ [DataContractAttribute(IsReference=true)]
+ public partial class Region : EntityObject
+ {
+ #region Factory Method
+
+ /// <summary>
+ /// Create a new Region object.
+ /// </summary>
+ /// <param name="regionID">Initial value of the RegionID property.</param>
+ /// <param name="regionDescription">Initial value of the RegionDescription property.</param>
+ public static Region CreateRegion(global::System.Int32 regionID, global::System.String regionDescription)
+ {
+ Region region = new Region();
+ region.RegionID = regionID;
+ region.RegionDescription = regionDescription;
+ return region;
+ }
+
+ #endregion
+ #region Primitive Properties
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=true, IsNullable=false)]
+ [DataMemberAttribute()]
+ public global::System.Int32 RegionID
+ {
+ get
+ {
+ return _RegionID;
+ }
+ set
+ {
+ if (_RegionID != value)
+ {
+ OnRegionIDChanging(value);
+ ReportPropertyChanging("RegionID");
+ _RegionID = StructuralObject.SetValidValue(value);
+ ReportPropertyChanged("RegionID");
+ OnRegionIDChanged();
+ }
+ }
+ }
+ private global::System.Int32 _RegionID;
+ partial void OnRegionIDChanging(global::System.Int32 value);
+ partial void OnRegionIDChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=false)]
+ [DataMemberAttribute()]
+ public global::System.String RegionDescription
+ {
+ get
+ {
+ return _RegionDescription;
+ }
+ set
+ {
+ OnRegionDescriptionChanging(value);
+ ReportPropertyChanging("RegionDescription");
+ _RegionDescription = StructuralObject.SetValidValue(value, false);
+ ReportPropertyChanged("RegionDescription");
+ OnRegionDescriptionChanged();
+ }
+ }
+ private global::System.String _RegionDescription;
+ partial void OnRegionDescriptionChanging(global::System.String value);
+ partial void OnRegionDescriptionChanged();
+
+ #endregion
+
+ #region Navigation Properties
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [XmlIgnoreAttribute()]
+ [SoapIgnoreAttribute()]
+ [DataMemberAttribute()]
+ [EdmRelationshipNavigationPropertyAttribute("northwindModel", "FK_Territories_Region", "Territories")]
+ public EntityCollection<Territory> Territories
+ {
+ get
+ {
+ return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedCollection<Territory>("northwindModel.FK_Territories_Region", "Territories");
+ }
+ set
+ {
+ if ((value != null))
+ {
+ ((IEntityWithRelationships)this).RelationshipManager.InitializeRelatedCollection<Territory>("northwindModel.FK_Territories_Region", "Territories", value);
+ }
+ }
+ }
+
+ #endregion
+ }
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmEntityTypeAttribute(NamespaceName="northwindModel", Name="Shipper")]
+ [Serializable()]
+ [DataContractAttribute(IsReference=true)]
+ public partial class Shipper : EntityObject
+ {
+ #region Factory Method
+
+ /// <summary>
+ /// Create a new Shipper object.
+ /// </summary>
+ /// <param name="shipperID">Initial value of the ShipperID property.</param>
+ /// <param name="companyName">Initial value of the CompanyName property.</param>
+ public static Shipper CreateShipper(global::System.Int32 shipperID, global::System.String companyName)
+ {
+ Shipper shipper = new Shipper();
+ shipper.ShipperID = shipperID;
+ shipper.CompanyName = companyName;
+ return shipper;
+ }
+
+ #endregion
+ #region Primitive Properties
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=true, IsNullable=false)]
+ [DataMemberAttribute()]
+ public global::System.Int32 ShipperID
+ {
+ get
+ {
+ return _ShipperID;
+ }
+ set
+ {
+ if (_ShipperID != value)
+ {
+ OnShipperIDChanging(value);
+ ReportPropertyChanging("ShipperID");
+ _ShipperID = StructuralObject.SetValidValue(value);
+ ReportPropertyChanged("ShipperID");
+ OnShipperIDChanged();
+ }
+ }
+ }
+ private global::System.Int32 _ShipperID;
+ partial void OnShipperIDChanging(global::System.Int32 value);
+ partial void OnShipperIDChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=false)]
+ [DataMemberAttribute()]
+ public global::System.String CompanyName
+ {
+ get
+ {
+ return _CompanyName;
+ }
+ set
+ {
+ OnCompanyNameChanging(value);
+ ReportPropertyChanging("CompanyName");
+ _CompanyName = StructuralObject.SetValidValue(value, false);
+ ReportPropertyChanged("CompanyName");
+ OnCompanyNameChanged();
+ }
+ }
+ private global::System.String _CompanyName;
+ partial void OnCompanyNameChanging(global::System.String value);
+ partial void OnCompanyNameChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public global::System.String Phone
+ {
+ get
+ {
+ return _Phone;
+ }
+ set
+ {
+ OnPhoneChanging(value);
+ ReportPropertyChanging("Phone");
+ _Phone = StructuralObject.SetValidValue(value, true);
+ ReportPropertyChanged("Phone");
+ OnPhoneChanged();
+ }
+ }
+ private global::System.String _Phone;
+ partial void OnPhoneChanging(global::System.String value);
+ partial void OnPhoneChanged();
+
+ #endregion
+
+ #region Navigation Properties
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [XmlIgnoreAttribute()]
+ [SoapIgnoreAttribute()]
+ [DataMemberAttribute()]
+ [EdmRelationshipNavigationPropertyAttribute("northwindModel", "FK_Orders_Shippers", "Orders")]
+ public EntityCollection<Order> Orders
+ {
+ get
+ {
+ return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedCollection<Order>("northwindModel.FK_Orders_Shippers", "Orders");
+ }
+ set
+ {
+ if ((value != null))
+ {
+ ((IEntityWithRelationships)this).RelationshipManager.InitializeRelatedCollection<Order>("northwindModel.FK_Orders_Shippers", "Orders", value);
+ }
+ }
+ }
+
+ #endregion
+ }
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmEntityTypeAttribute(NamespaceName="northwindModel", Name="Supplier")]
+ [Serializable()]
+ [DataContractAttribute(IsReference=true)]
+ public partial class Supplier : EntityObject
+ {
+ #region Factory Method
+
+ /// <summary>
+ /// Create a new Supplier object.
+ /// </summary>
+ /// <param name="supplierID">Initial value of the SupplierID property.</param>
+ /// <param name="companyName">Initial value of the CompanyName property.</param>
+ public static Supplier CreateSupplier(global::System.Int32 supplierID, global::System.String companyName)
+ {
+ Supplier supplier = new Supplier();
+ supplier.SupplierID = supplierID;
+ supplier.CompanyName = companyName;
+ return supplier;
+ }
+
+ #endregion
+ #region Primitive Properties
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=true, IsNullable=false)]
+ [DataMemberAttribute()]
+ public global::System.Int32 SupplierID
+ {
+ get
+ {
+ return _SupplierID;
+ }
+ set
+ {
+ if (_SupplierID != value)
+ {
+ OnSupplierIDChanging(value);
+ ReportPropertyChanging("SupplierID");
+ _SupplierID = StructuralObject.SetValidValue(value);
+ ReportPropertyChanged("SupplierID");
+ OnSupplierIDChanged();
+ }
+ }
+ }
+ private global::System.Int32 _SupplierID;
+ partial void OnSupplierIDChanging(global::System.Int32 value);
+ partial void OnSupplierIDChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=false)]
+ [DataMemberAttribute()]
+ public global::System.String CompanyName
+ {
+ get
+ {
+ return _CompanyName;
+ }
+ set
+ {
+ OnCompanyNameChanging(value);
+ ReportPropertyChanging("CompanyName");
+ _CompanyName = StructuralObject.SetValidValue(value, false);
+ ReportPropertyChanged("CompanyName");
+ OnCompanyNameChanged();
+ }
+ }
+ private global::System.String _CompanyName;
+ partial void OnCompanyNameChanging(global::System.String value);
+ partial void OnCompanyNameChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public global::System.String ContactName
+ {
+ get
+ {
+ return _ContactName;
+ }
+ set
+ {
+ OnContactNameChanging(value);
+ ReportPropertyChanging("ContactName");
+ _ContactName = StructuralObject.SetValidValue(value, true);
+ ReportPropertyChanged("ContactName");
+ OnContactNameChanged();
+ }
+ }
+ private global::System.String _ContactName;
+ partial void OnContactNameChanging(global::System.String value);
+ partial void OnContactNameChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public global::System.String ContactTitle
+ {
+ get
+ {
+ return _ContactTitle;
+ }
+ set
+ {
+ OnContactTitleChanging(value);
+ ReportPropertyChanging("ContactTitle");
+ _ContactTitle = StructuralObject.SetValidValue(value, true);
+ ReportPropertyChanged("ContactTitle");
+ OnContactTitleChanged();
+ }
+ }
+ private global::System.String _ContactTitle;
+ partial void OnContactTitleChanging(global::System.String value);
+ partial void OnContactTitleChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public global::System.String Address
+ {
+ get
+ {
+ return _Address;
+ }
+ set
+ {
+ OnAddressChanging(value);
+ ReportPropertyChanging("Address");
+ _Address = StructuralObject.SetValidValue(value, true);
+ ReportPropertyChanged("Address");
+ OnAddressChanged();
+ }
+ }
+ private global::System.String _Address;
+ partial void OnAddressChanging(global::System.String value);
+ partial void OnAddressChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public global::System.String City
+ {
+ get
+ {
+ return _City;
+ }
+ set
+ {
+ OnCityChanging(value);
+ ReportPropertyChanging("City");
+ _City = StructuralObject.SetValidValue(value, true);
+ ReportPropertyChanged("City");
+ OnCityChanged();
+ }
+ }
+ private global::System.String _City;
+ partial void OnCityChanging(global::System.String value);
+ partial void OnCityChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public global::System.String Region
+ {
+ get
+ {
+ return _Region;
+ }
+ set
+ {
+ OnRegionChanging(value);
+ ReportPropertyChanging("Region");
+ _Region = StructuralObject.SetValidValue(value, true);
+ ReportPropertyChanged("Region");
+ OnRegionChanged();
+ }
+ }
+ private global::System.String _Region;
+ partial void OnRegionChanging(global::System.String value);
+ partial void OnRegionChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public global::System.String PostalCode
+ {
+ get
+ {
+ return _PostalCode;
+ }
+ set
+ {
+ OnPostalCodeChanging(value);
+ ReportPropertyChanging("PostalCode");
+ _PostalCode = StructuralObject.SetValidValue(value, true);
+ ReportPropertyChanged("PostalCode");
+ OnPostalCodeChanged();
+ }
+ }
+ private global::System.String _PostalCode;
+ partial void OnPostalCodeChanging(global::System.String value);
+ partial void OnPostalCodeChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public global::System.String Country
+ {
+ get
+ {
+ return _Country;
+ }
+ set
+ {
+ OnCountryChanging(value);
+ ReportPropertyChanging("Country");
+ _Country = StructuralObject.SetValidValue(value, true);
+ ReportPropertyChanged("Country");
+ OnCountryChanged();
+ }
+ }
+ private global::System.String _Country;
+ partial void OnCountryChanging(global::System.String value);
+ partial void OnCountryChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public global::System.String Phone
+ {
+ get
+ {
+ return _Phone;
+ }
+ set
+ {
+ OnPhoneChanging(value);
+ ReportPropertyChanging("Phone");
+ _Phone = StructuralObject.SetValidValue(value, true);
+ ReportPropertyChanged("Phone");
+ OnPhoneChanged();
+ }
+ }
+ private global::System.String _Phone;
+ partial void OnPhoneChanging(global::System.String value);
+ partial void OnPhoneChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public global::System.String Fax
+ {
+ get
+ {
+ return _Fax;
+ }
+ set
+ {
+ OnFaxChanging(value);
+ ReportPropertyChanging("Fax");
+ _Fax = StructuralObject.SetValidValue(value, true);
+ ReportPropertyChanged("Fax");
+ OnFaxChanged();
+ }
+ }
+ private global::System.String _Fax;
+ partial void OnFaxChanging(global::System.String value);
+ partial void OnFaxChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
+ [DataMemberAttribute()]
+ public global::System.String HomePage
+ {
+ get
+ {
+ return _HomePage;
+ }
+ set
+ {
+ OnHomePageChanging(value);
+ ReportPropertyChanging("HomePage");
+ _HomePage = StructuralObject.SetValidValue(value, true);
+ ReportPropertyChanged("HomePage");
+ OnHomePageChanged();
+ }
+ }
+ private global::System.String _HomePage;
+ partial void OnHomePageChanging(global::System.String value);
+ partial void OnHomePageChanged();
+
+ #endregion
+
+ #region Navigation Properties
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [XmlIgnoreAttribute()]
+ [SoapIgnoreAttribute()]
+ [DataMemberAttribute()]
+ [EdmRelationshipNavigationPropertyAttribute("northwindModel", "FK_Products_Suppliers", "Products")]
+ public EntityCollection<Product> Products
+ {
+ get
+ {
+ return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedCollection<Product>("northwindModel.FK_Products_Suppliers", "Products");
+ }
+ set
+ {
+ if ((value != null))
+ {
+ ((IEntityWithRelationships)this).RelationshipManager.InitializeRelatedCollection<Product>("northwindModel.FK_Products_Suppliers", "Products", value);
+ }
+ }
+ }
+
+ #endregion
+ }
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmEntityTypeAttribute(NamespaceName="northwindModel", Name="Territory")]
+ [Serializable()]
+ [DataContractAttribute(IsReference=true)]
+ public partial class Territory : EntityObject
+ {
+ #region Factory Method
+
+ /// <summary>
+ /// Create a new Territory object.
+ /// </summary>
+ /// <param name="territoryID">Initial value of the TerritoryID property.</param>
+ /// <param name="territoryDescription">Initial value of the TerritoryDescription property.</param>
+ /// <param name="regionID">Initial value of the RegionID property.</param>
+ public static Territory CreateTerritory(global::System.String territoryID, global::System.String territoryDescription, global::System.Int32 regionID)
+ {
+ Territory territory = new Territory();
+ territory.TerritoryID = territoryID;
+ territory.TerritoryDescription = territoryDescription;
+ territory.RegionID = regionID;
+ return territory;
+ }
+
+ #endregion
+ #region Primitive Properties
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=true, IsNullable=false)]
+ [DataMemberAttribute()]
+ public global::System.String TerritoryID
+ {
+ get
+ {
+ return _TerritoryID;
+ }
+ set
+ {
+ if (_TerritoryID != value)
+ {
+ OnTerritoryIDChanging(value);
+ ReportPropertyChanging("TerritoryID");
+ _TerritoryID = StructuralObject.SetValidValue(value, false);
+ ReportPropertyChanged("TerritoryID");
+ OnTerritoryIDChanged();
+ }
+ }
+ }
+ private global::System.String _TerritoryID;
+ partial void OnTerritoryIDChanging(global::System.String value);
+ partial void OnTerritoryIDChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=false)]
+ [DataMemberAttribute()]
+ public global::System.String TerritoryDescription
+ {
+ get
+ {
+ return _TerritoryDescription;
+ }
+ set
+ {
+ OnTerritoryDescriptionChanging(value);
+ ReportPropertyChanging("TerritoryDescription");
+ _TerritoryDescription = StructuralObject.SetValidValue(value, false);
+ ReportPropertyChanged("TerritoryDescription");
+ OnTerritoryDescriptionChanged();
+ }
+ }
+ private global::System.String _TerritoryDescription;
+ partial void OnTerritoryDescriptionChanging(global::System.String value);
+ partial void OnTerritoryDescriptionChanged();
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=false)]
+ [DataMemberAttribute()]
+ public global::System.Int32 RegionID
+ {
+ get
+ {
+ return _RegionID;
+ }
+ set
+ {
+ OnRegionIDChanging(value);
+ ReportPropertyChanging("RegionID");
+ _RegionID = StructuralObject.SetValidValue(value);
+ ReportPropertyChanged("RegionID");
+ OnRegionIDChanged();
+ }
+ }
+ private global::System.Int32 _RegionID;
+ partial void OnRegionIDChanging(global::System.Int32 value);
+ partial void OnRegionIDChanged();
+
+ #endregion
+
+ #region Navigation Properties
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [XmlIgnoreAttribute()]
+ [SoapIgnoreAttribute()]
+ [DataMemberAttribute()]
+ [EdmRelationshipNavigationPropertyAttribute("northwindModel", "FK_Territories_Region", "Region")]
+ public Region Region
+ {
+ get
+ {
+ return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<Region>("northwindModel.FK_Territories_Region", "Region").Value;
+ }
+ set
+ {
+ ((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<Region>("northwindModel.FK_Territories_Region", "Region").Value = value;
+ }
+ }
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [BrowsableAttribute(false)]
+ [DataMemberAttribute()]
+ public EntityReference<Region> RegionReference
+ {
+ get
+ {
+ return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<Region>("northwindModel.FK_Territories_Region", "Region");
+ }
+ set
+ {
+ if ((value != null))
+ {
+ ((IEntityWithRelationships)this).RelationshipManager.InitializeRelatedReference<Region>("northwindModel.FK_Territories_Region", "Region", value);
+ }
+ }
+ }
+
+ /// <summary>
+ /// No Metadata Documentation available.
+ /// </summary>
+ [XmlIgnoreAttribute()]
+ [SoapIgnoreAttribute()]
+ [DataMemberAttribute()]
+ [EdmRelationshipNavigationPropertyAttribute("northwindModel", "EmployeeTerritories", "Employees")]
+ public EntityCollection<Employee> Employees
+ {
+ get
+ {
+ return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedCollection<Employee>("northwindModel.EmployeeTerritories", "Employees");
+ }
+ set
+ {
+ if ((value != null))
+ {
+ ((IEntityWithRelationships)this).RelationshipManager.InitializeRelatedCollection<Employee>("northwindModel.EmployeeTerritories", "Employees", value);
+ }
+ }
+ }
+
+ #endregion
+ }
+
+ #endregion
+
+}
diff --git a/test/Microsoft.Web.Http.Data.Test/Models/Northwind.edmx b/test/Microsoft.Web.Http.Data.Test/Models/Northwind.edmx
new file mode 100644
index 00000000..f74d16cb
--- /dev/null
+++ b/test/Microsoft.Web.Http.Data.Test/Models/Northwind.edmx
@@ -0,0 +1,933 @@
+<?xml version="1.0" encoding="utf-8"?>
+<edmx:Edmx Version="2.0" xmlns:edmx="http://schemas.microsoft.com/ado/2008/10/edmx">
+ <!-- EF Runtime content -->
+ <edmx:Runtime>
+ <!-- SSDL content -->
+ <edmx:StorageModels>
+ <Schema Namespace="northwindModel.Store" Alias="Self" Provider="System.Data.SqlClient" ProviderManifestToken="2005" xmlns:store="http://schemas.microsoft.com/ado/2007/12/edm/EntityStoreSchemaGenerator" xmlns="http://schemas.microsoft.com/ado/2009/02/edm/ssdl">
+ <EntityContainer Name="northwindModelStoreContainer">
+ <EntitySet Name="Categories" EntityType="northwindModel.Store.Categories" store:Type="Tables" Schema="dbo" />
+ <EntitySet Name="CustomerCustomerDemo" EntityType="northwindModel.Store.CustomerCustomerDemo" store:Type="Tables" Schema="dbo" />
+ <EntitySet Name="CustomerDemographics" EntityType="northwindModel.Store.CustomerDemographics" store:Type="Tables" Schema="dbo" />
+ <EntitySet Name="Customers" EntityType="northwindModel.Store.Customers" store:Type="Tables" Schema="dbo" />
+ <EntitySet Name="Employees" EntityType="northwindModel.Store.Employees" store:Type="Tables" Schema="dbo" />
+ <EntitySet Name="EmployeeTerritories" EntityType="northwindModel.Store.EmployeeTerritories" store:Type="Tables" Schema="dbo" />
+ <EntitySet Name="Order Details" EntityType="northwindModel.Store.Order Details" store:Type="Tables" Schema="dbo" />
+ <EntitySet Name="Orders" EntityType="northwindModel.Store.Orders" store:Type="Tables" Schema="dbo" />
+ <EntitySet Name="Products" EntityType="northwindModel.Store.Products" store:Type="Tables" Schema="dbo" />
+ <EntitySet Name="Region" EntityType="northwindModel.Store.Region" store:Type="Tables" Schema="dbo" />
+ <EntitySet Name="Shippers" EntityType="northwindModel.Store.Shippers" store:Type="Tables" Schema="dbo" />
+ <EntitySet Name="Suppliers" EntityType="northwindModel.Store.Suppliers" store:Type="Tables" Schema="dbo" />
+ <EntitySet Name="Territories" EntityType="northwindModel.Store.Territories" store:Type="Tables" Schema="dbo" />
+ <AssociationSet Name="FK_CustomerCustomerDemo" Association="northwindModel.Store.FK_CustomerCustomerDemo">
+ <End Role="CustomerDemographics" EntitySet="CustomerDemographics" />
+ <End Role="CustomerCustomerDemo" EntitySet="CustomerCustomerDemo" />
+ </AssociationSet>
+ <AssociationSet Name="FK_CustomerCustomerDemo_Customers" Association="northwindModel.Store.FK_CustomerCustomerDemo_Customers">
+ <End Role="Customers" EntitySet="Customers" />
+ <End Role="CustomerCustomerDemo" EntitySet="CustomerCustomerDemo" />
+ </AssociationSet>
+ <AssociationSet Name="FK_Employees_Employees" Association="northwindModel.Store.FK_Employees_Employees">
+ <End Role="Employees" EntitySet="Employees" />
+ <End Role="Employees1" EntitySet="Employees" />
+ </AssociationSet>
+ <AssociationSet Name="FK_EmployeeTerritories_Employees" Association="northwindModel.Store.FK_EmployeeTerritories_Employees">
+ <End Role="Employees" EntitySet="Employees" />
+ <End Role="EmployeeTerritories" EntitySet="EmployeeTerritories" />
+ </AssociationSet>
+ <AssociationSet Name="FK_EmployeeTerritories_Territories" Association="northwindModel.Store.FK_EmployeeTerritories_Territories">
+ <End Role="Territories" EntitySet="Territories" />
+ <End Role="EmployeeTerritories" EntitySet="EmployeeTerritories" />
+ </AssociationSet>
+ <AssociationSet Name="FK_Order_Details_Orders" Association="northwindModel.Store.FK_Order_Details_Orders">
+ <End Role="Orders" EntitySet="Orders" />
+ <End Role="Order Details" EntitySet="Order Details" />
+ </AssociationSet>
+ <AssociationSet Name="FK_Order_Details_Products" Association="northwindModel.Store.FK_Order_Details_Products">
+ <End Role="Products" EntitySet="Products" />
+ <End Role="Order Details" EntitySet="Order Details" />
+ </AssociationSet>
+ <AssociationSet Name="FK_Orders_Customers" Association="northwindModel.Store.FK_Orders_Customers">
+ <End Role="Customers" EntitySet="Customers" />
+ <End Role="Orders" EntitySet="Orders" />
+ </AssociationSet>
+ <AssociationSet Name="FK_Orders_Employees" Association="northwindModel.Store.FK_Orders_Employees">
+ <End Role="Employees" EntitySet="Employees" />
+ <End Role="Orders" EntitySet="Orders" />
+ </AssociationSet>
+ <AssociationSet Name="FK_Orders_Shippers" Association="northwindModel.Store.FK_Orders_Shippers">
+ <End Role="Shippers" EntitySet="Shippers" />
+ <End Role="Orders" EntitySet="Orders" />
+ </AssociationSet>
+ <AssociationSet Name="FK_Products_Categories" Association="northwindModel.Store.FK_Products_Categories">
+ <End Role="Categories" EntitySet="Categories" />
+ <End Role="Products" EntitySet="Products" />
+ </AssociationSet>
+ <AssociationSet Name="FK_Products_Suppliers" Association="northwindModel.Store.FK_Products_Suppliers">
+ <End Role="Suppliers" EntitySet="Suppliers" />
+ <End Role="Products" EntitySet="Products" />
+ </AssociationSet>
+ <AssociationSet Name="FK_Territories_Region" Association="northwindModel.Store.FK_Territories_Region">
+ <End Role="Region" EntitySet="Region" />
+ <End Role="Territories" EntitySet="Territories" />
+ </AssociationSet>
+ </EntityContainer>
+ <EntityType Name="Categories">
+ <Key>
+ <PropertyRef Name="CategoryID" />
+ </Key>
+ <Property Name="CategoryID" Type="int" Nullable="false" StoreGeneratedPattern="Identity" />
+ <Property Name="CategoryName" Type="nvarchar" Nullable="false" MaxLength="15" />
+ <Property Name="Description" Type="ntext" />
+ <Property Name="Picture" Type="image" />
+ </EntityType>
+ <EntityType Name="CustomerCustomerDemo">
+ <Key>
+ <PropertyRef Name="CustomerID" />
+ <PropertyRef Name="CustomerTypeID" />
+ </Key>
+ <Property Name="CustomerID" Type="nchar" Nullable="false" MaxLength="5" />
+ <Property Name="CustomerTypeID" Type="nchar" Nullable="false" MaxLength="10" />
+ </EntityType>
+ <EntityType Name="CustomerDemographics">
+ <Key>
+ <PropertyRef Name="CustomerTypeID" />
+ </Key>
+ <Property Name="CustomerTypeID" Type="nchar" Nullable="false" MaxLength="10" />
+ <Property Name="CustomerDesc" Type="ntext" />
+ </EntityType>
+ <EntityType Name="Customers">
+ <Key>
+ <PropertyRef Name="CustomerID" />
+ </Key>
+ <Property Name="CustomerID" Type="nchar" Nullable="false" MaxLength="5" />
+ <Property Name="CompanyName" Type="nvarchar" Nullable="false" MaxLength="40" />
+ <Property Name="ContactName" Type="nvarchar" MaxLength="30" />
+ <Property Name="ContactTitle" Type="nvarchar" MaxLength="30" />
+ <Property Name="Address" Type="nvarchar" MaxLength="60" />
+ <Property Name="City" Type="nvarchar" MaxLength="15" />
+ <Property Name="Region" Type="nvarchar" MaxLength="15" />
+ <Property Name="PostalCode" Type="nvarchar" MaxLength="10" />
+ <Property Name="Country" Type="nvarchar" MaxLength="15" />
+ <Property Name="Phone" Type="nvarchar" MaxLength="24" />
+ <Property Name="Fax" Type="nvarchar" MaxLength="24" />
+ </EntityType>
+ <EntityType Name="Employees">
+ <Key>
+ <PropertyRef Name="EmployeeID" />
+ </Key>
+ <Property Name="EmployeeID" Type="int" Nullable="false" StoreGeneratedPattern="Identity" />
+ <Property Name="LastName" Type="nvarchar" Nullable="false" MaxLength="20" />
+ <Property Name="FirstName" Type="nvarchar" Nullable="false" MaxLength="10" />
+ <Property Name="Title" Type="nvarchar" MaxLength="30" />
+ <Property Name="TitleOfCourtesy" Type="nvarchar" MaxLength="25" />
+ <Property Name="BirthDate" Type="datetime" />
+ <Property Name="HireDate" Type="datetime" />
+ <Property Name="Address" Type="nvarchar" MaxLength="60" />
+ <Property Name="City" Type="nvarchar" MaxLength="15" />
+ <Property Name="Region" Type="nvarchar" MaxLength="15" />
+ <Property Name="PostalCode" Type="nvarchar" MaxLength="10" />
+ <Property Name="Country" Type="nvarchar" MaxLength="15" />
+ <Property Name="HomePhone" Type="nvarchar" MaxLength="24" />
+ <Property Name="Extension" Type="nvarchar" MaxLength="4" />
+ <Property Name="Photo" Type="image" />
+ <Property Name="Notes" Type="ntext" />
+ <Property Name="ReportsTo" Type="int" />
+ <Property Name="PhotoPath" Type="nvarchar" MaxLength="255" />
+ </EntityType>
+ <EntityType Name="EmployeeTerritories">
+ <Key>
+ <PropertyRef Name="EmployeeID" />
+ <PropertyRef Name="TerritoryID" />
+ </Key>
+ <Property Name="EmployeeID" Type="int" Nullable="false" />
+ <Property Name="TerritoryID" Type="nvarchar" Nullable="false" MaxLength="20" />
+ </EntityType>
+ <EntityType Name="Order Details">
+ <Key>
+ <PropertyRef Name="OrderID" />
+ <PropertyRef Name="ProductID" />
+ </Key>
+ <Property Name="OrderID" Type="int" Nullable="false" />
+ <Property Name="ProductID" Type="int" Nullable="false" />
+ <Property Name="UnitPrice" Type="money" Nullable="false" />
+ <Property Name="Quantity" Type="smallint" Nullable="false" />
+ <Property Name="Discount" Type="real" Nullable="false" />
+ </EntityType>
+ <EntityType Name="Orders">
+ <Key>
+ <PropertyRef Name="OrderID" />
+ </Key>
+ <Property Name="OrderID" Type="int" Nullable="false" StoreGeneratedPattern="Identity" />
+ <Property Name="CustomerID" Type="nchar" MaxLength="5" />
+ <Property Name="EmployeeID" Type="int" />
+ <Property Name="OrderDate" Type="datetime" />
+ <Property Name="RequiredDate" Type="datetime" />
+ <Property Name="ShippedDate" Type="datetime" />
+ <Property Name="ShipVia" Type="int" />
+ <Property Name="Freight" Type="money" />
+ <Property Name="ShipName" Type="nvarchar" MaxLength="40" />
+ <Property Name="ShipAddress" Type="nvarchar" MaxLength="60" />
+ <Property Name="ShipCity" Type="nvarchar" MaxLength="15" />
+ <Property Name="ShipRegion" Type="nvarchar" MaxLength="15" />
+ <Property Name="ShipPostalCode" Type="nvarchar" MaxLength="10" />
+ <Property Name="ShipCountry" Type="nvarchar" MaxLength="15" />
+ </EntityType>
+ <EntityType Name="Products">
+ <Key>
+ <PropertyRef Name="ProductID" />
+ </Key>
+ <Property Name="ProductID" Type="int" Nullable="false" StoreGeneratedPattern="Identity" />
+ <Property Name="ProductName" Type="nvarchar" Nullable="false" MaxLength="40" />
+ <Property Name="SupplierID" Type="int" />
+ <Property Name="CategoryID" Type="int" />
+ <Property Name="QuantityPerUnit" Type="nvarchar" MaxLength="20" />
+ <Property Name="UnitPrice" Type="money" />
+ <Property Name="UnitsInStock" Type="smallint" />
+ <Property Name="UnitsOnOrder" Type="smallint" />
+ <Property Name="ReorderLevel" Type="smallint" />
+ <Property Name="Discontinued" Type="bit" Nullable="false" />
+ </EntityType>
+ <EntityType Name="Region">
+ <Key>
+ <PropertyRef Name="RegionID" />
+ </Key>
+ <Property Name="RegionID" Type="int" Nullable="false" />
+ <Property Name="RegionDescription" Type="nchar" Nullable="false" MaxLength="50" />
+ </EntityType>
+ <EntityType Name="Shippers">
+ <Key>
+ <PropertyRef Name="ShipperID" />
+ </Key>
+ <Property Name="ShipperID" Type="int" Nullable="false" StoreGeneratedPattern="Identity" />
+ <Property Name="CompanyName" Type="nvarchar" Nullable="false" MaxLength="40" />
+ <Property Name="Phone" Type="nvarchar" MaxLength="24" />
+ </EntityType>
+ <EntityType Name="Suppliers">
+ <Key>
+ <PropertyRef Name="SupplierID" />
+ </Key>
+ <Property Name="SupplierID" Type="int" Nullable="false" StoreGeneratedPattern="Identity" />
+ <Property Name="CompanyName" Type="nvarchar" Nullable="false" MaxLength="40" />
+ <Property Name="ContactName" Type="nvarchar" MaxLength="30" />
+ <Property Name="ContactTitle" Type="nvarchar" MaxLength="30" />
+ <Property Name="Address" Type="nvarchar" MaxLength="60" />
+ <Property Name="City" Type="nvarchar" MaxLength="15" />
+ <Property Name="Region" Type="nvarchar" MaxLength="15" />
+ <Property Name="PostalCode" Type="nvarchar" MaxLength="10" />
+ <Property Name="Country" Type="nvarchar" MaxLength="15" />
+ <Property Name="Phone" Type="nvarchar" MaxLength="24" />
+ <Property Name="Fax" Type="nvarchar" MaxLength="24" />
+ <Property Name="HomePage" Type="ntext" />
+ </EntityType>
+ <EntityType Name="Territories">
+ <Key>
+ <PropertyRef Name="TerritoryID" />
+ </Key>
+ <Property Name="TerritoryID" Type="nvarchar" Nullable="false" MaxLength="20" />
+ <Property Name="TerritoryDescription" Type="nchar" Nullable="false" MaxLength="50" />
+ <Property Name="RegionID" Type="int" Nullable="false" />
+ </EntityType>
+ <Association Name="FK_CustomerCustomerDemo">
+ <End Role="CustomerDemographics" Type="northwindModel.Store.CustomerDemographics" Multiplicity="1" />
+ <End Role="CustomerCustomerDemo" Type="northwindModel.Store.CustomerCustomerDemo" Multiplicity="*" />
+ <ReferentialConstraint>
+ <Principal Role="CustomerDemographics">
+ <PropertyRef Name="CustomerTypeID" />
+ </Principal>
+ <Dependent Role="CustomerCustomerDemo">
+ <PropertyRef Name="CustomerTypeID" />
+ </Dependent>
+ </ReferentialConstraint>
+ </Association>
+ <Association Name="FK_CustomerCustomerDemo_Customers">
+ <End Role="Customers" Type="northwindModel.Store.Customers" Multiplicity="1" />
+ <End Role="CustomerCustomerDemo" Type="northwindModel.Store.CustomerCustomerDemo" Multiplicity="*" />
+ <ReferentialConstraint>
+ <Principal Role="Customers">
+ <PropertyRef Name="CustomerID" />
+ </Principal>
+ <Dependent Role="CustomerCustomerDemo">
+ <PropertyRef Name="CustomerID" />
+ </Dependent>
+ </ReferentialConstraint>
+ </Association>
+ <Association Name="FK_Employees_Employees">
+ <End Role="Employees" Type="northwindModel.Store.Employees" Multiplicity="0..1" />
+ <End Role="Employees1" Type="northwindModel.Store.Employees" Multiplicity="*" />
+ <ReferentialConstraint>
+ <Principal Role="Employees">
+ <PropertyRef Name="EmployeeID" />
+ </Principal>
+ <Dependent Role="Employees1">
+ <PropertyRef Name="ReportsTo" />
+ </Dependent>
+ </ReferentialConstraint>
+ </Association>
+ <Association Name="FK_EmployeeTerritories_Employees">
+ <End Role="Employees" Type="northwindModel.Store.Employees" Multiplicity="1" />
+ <End Role="EmployeeTerritories" Type="northwindModel.Store.EmployeeTerritories" Multiplicity="*" />
+ <ReferentialConstraint>
+ <Principal Role="Employees">
+ <PropertyRef Name="EmployeeID" />
+ </Principal>
+ <Dependent Role="EmployeeTerritories">
+ <PropertyRef Name="EmployeeID" />
+ </Dependent>
+ </ReferentialConstraint>
+ </Association>
+ <Association Name="FK_EmployeeTerritories_Territories">
+ <End Role="Territories" Type="northwindModel.Store.Territories" Multiplicity="1" />
+ <End Role="EmployeeTerritories" Type="northwindModel.Store.EmployeeTerritories" Multiplicity="*" />
+ <ReferentialConstraint>
+ <Principal Role="Territories">
+ <PropertyRef Name="TerritoryID" />
+ </Principal>
+ <Dependent Role="EmployeeTerritories">
+ <PropertyRef Name="TerritoryID" />
+ </Dependent>
+ </ReferentialConstraint>
+ </Association>
+ <Association Name="FK_Order_Details_Orders">
+ <End Role="Orders" Type="northwindModel.Store.Orders" Multiplicity="1" />
+ <End Role="Order Details" Type="northwindModel.Store.Order Details" Multiplicity="*" />
+ <ReferentialConstraint>
+ <Principal Role="Orders">
+ <PropertyRef Name="OrderID" />
+ </Principal>
+ <Dependent Role="Order Details">
+ <PropertyRef Name="OrderID" />
+ </Dependent>
+ </ReferentialConstraint>
+ </Association>
+ <Association Name="FK_Order_Details_Products">
+ <End Role="Products" Type="northwindModel.Store.Products" Multiplicity="1" />
+ <End Role="Order Details" Type="northwindModel.Store.Order Details" Multiplicity="*" />
+ <ReferentialConstraint>
+ <Principal Role="Products">
+ <PropertyRef Name="ProductID" />
+ </Principal>
+ <Dependent Role="Order Details">
+ <PropertyRef Name="ProductID" />
+ </Dependent>
+ </ReferentialConstraint>
+ </Association>
+ <Association Name="FK_Orders_Customers">
+ <End Role="Customers" Type="northwindModel.Store.Customers" Multiplicity="0..1" />
+ <End Role="Orders" Type="northwindModel.Store.Orders" Multiplicity="*" />
+ <ReferentialConstraint>
+ <Principal Role="Customers">
+ <PropertyRef Name="CustomerID" />
+ </Principal>
+ <Dependent Role="Orders">
+ <PropertyRef Name="CustomerID" />
+ </Dependent>
+ </ReferentialConstraint>
+ </Association>
+ <Association Name="FK_Orders_Employees">
+ <End Role="Employees" Type="northwindModel.Store.Employees" Multiplicity="0..1" />
+ <End Role="Orders" Type="northwindModel.Store.Orders" Multiplicity="*" />
+ <ReferentialConstraint>
+ <Principal Role="Employees">
+ <PropertyRef Name="EmployeeID" />
+ </Principal>
+ <Dependent Role="Orders">
+ <PropertyRef Name="EmployeeID" />
+ </Dependent>
+ </ReferentialConstraint>
+ </Association>
+ <Association Name="FK_Orders_Shippers">
+ <End Role="Shippers" Type="northwindModel.Store.Shippers" Multiplicity="0..1" />
+ <End Role="Orders" Type="northwindModel.Store.Orders" Multiplicity="*" />
+ <ReferentialConstraint>
+ <Principal Role="Shippers">
+ <PropertyRef Name="ShipperID" />
+ </Principal>
+ <Dependent Role="Orders">
+ <PropertyRef Name="ShipVia" />
+ </Dependent>
+ </ReferentialConstraint>
+ </Association>
+ <Association Name="FK_Products_Categories">
+ <End Role="Categories" Type="northwindModel.Store.Categories" Multiplicity="0..1" />
+ <End Role="Products" Type="northwindModel.Store.Products" Multiplicity="*" />
+ <ReferentialConstraint>
+ <Principal Role="Categories">
+ <PropertyRef Name="CategoryID" />
+ </Principal>
+ <Dependent Role="Products">
+ <PropertyRef Name="CategoryID" />
+ </Dependent>
+ </ReferentialConstraint>
+ </Association>
+ <Association Name="FK_Products_Suppliers">
+ <End Role="Suppliers" Type="northwindModel.Store.Suppliers" Multiplicity="0..1" />
+ <End Role="Products" Type="northwindModel.Store.Products" Multiplicity="*" />
+ <ReferentialConstraint>
+ <Principal Role="Suppliers">
+ <PropertyRef Name="SupplierID" />
+ </Principal>
+ <Dependent Role="Products">
+ <PropertyRef Name="SupplierID" />
+ </Dependent>
+ </ReferentialConstraint>
+ </Association>
+ <Association Name="FK_Territories_Region">
+ <End Role="Region" Type="northwindModel.Store.Region" Multiplicity="1" />
+ <End Role="Territories" Type="northwindModel.Store.Territories" Multiplicity="*" />
+ <ReferentialConstraint>
+ <Principal Role="Region">
+ <PropertyRef Name="RegionID" />
+ </Principal>
+ <Dependent Role="Territories">
+ <PropertyRef Name="RegionID" />
+ </Dependent>
+ </ReferentialConstraint>
+ </Association>
+ </Schema>
+ </edmx:StorageModels>
+ <!-- CSDL content -->
+ <edmx:ConceptualModels>
+ <Schema Namespace="northwindModel" Alias="Self" xmlns:annotation="http://schemas.microsoft.com/ado/2009/02/edm/annotation" xmlns="http://schemas.microsoft.com/ado/2008/09/edm">
+ <EntityContainer Name="NorthwindEntities" annotation:LazyLoadingEnabled="true">
+ <EntitySet Name="Categories" EntityType="northwindModel.Category" />
+ <EntitySet Name="CustomerDemographics" EntityType="northwindModel.CustomerDemographic" />
+ <EntitySet Name="Customers" EntityType="northwindModel.Customer" />
+ <EntitySet Name="Employees" EntityType="northwindModel.Employee" />
+ <EntitySet Name="Order_Details" EntityType="northwindModel.Order_Detail" />
+ <EntitySet Name="Orders" EntityType="northwindModel.Order" />
+ <EntitySet Name="Products" EntityType="northwindModel.Product" />
+ <EntitySet Name="Regions" EntityType="northwindModel.Region" />
+ <EntitySet Name="Shippers" EntityType="northwindModel.Shipper" />
+ <EntitySet Name="Suppliers" EntityType="northwindModel.Supplier" />
+ <EntitySet Name="Territories" EntityType="northwindModel.Territory" />
+ <AssociationSet Name="FK_Products_Categories" Association="northwindModel.FK_Products_Categories">
+ <End Role="Categories" EntitySet="Categories" />
+ <End Role="Products" EntitySet="Products" />
+ </AssociationSet>
+ <AssociationSet Name="FK_Orders_Customers" Association="northwindModel.FK_Orders_Customers">
+ <End Role="Customers" EntitySet="Customers" />
+ <End Role="Orders" EntitySet="Orders" />
+ </AssociationSet>
+ <AssociationSet Name="FK_Employees_Employees" Association="northwindModel.FK_Employees_Employees">
+ <End Role="Employees" EntitySet="Employees" />
+ <End Role="Employees1" EntitySet="Employees" />
+ </AssociationSet>
+ <AssociationSet Name="FK_Orders_Employees" Association="northwindModel.FK_Orders_Employees">
+ <End Role="Employees" EntitySet="Employees" />
+ <End Role="Orders" EntitySet="Orders" />
+ </AssociationSet>
+ <AssociationSet Name="FK_Order_Details_Orders" Association="northwindModel.FK_Order_Details_Orders">
+ <End Role="Orders" EntitySet="Orders" />
+ <End Role="Order_Details" EntitySet="Order_Details" />
+ </AssociationSet>
+ <AssociationSet Name="FK_Order_Details_Products" Association="northwindModel.FK_Order_Details_Products">
+ <End Role="Products" EntitySet="Products" />
+ <End Role="Order_Details" EntitySet="Order_Details" />
+ </AssociationSet>
+ <AssociationSet Name="FK_Orders_Shippers" Association="northwindModel.FK_Orders_Shippers">
+ <End Role="Shippers" EntitySet="Shippers" />
+ <End Role="Orders" EntitySet="Orders" />
+ </AssociationSet>
+ <AssociationSet Name="FK_Products_Suppliers" Association="northwindModel.FK_Products_Suppliers">
+ <End Role="Suppliers" EntitySet="Suppliers" />
+ <End Role="Products" EntitySet="Products" />
+ </AssociationSet>
+ <AssociationSet Name="FK_Territories_Region" Association="northwindModel.FK_Territories_Region">
+ <End Role="Region" EntitySet="Regions" />
+ <End Role="Territories" EntitySet="Territories" />
+ </AssociationSet>
+ <AssociationSet Name="CustomerCustomerDemo" Association="northwindModel.CustomerCustomerDemo">
+ <End Role="CustomerDemographics" EntitySet="CustomerDemographics" />
+ <End Role="Customers" EntitySet="Customers" />
+ </AssociationSet>
+ <AssociationSet Name="EmployeeTerritories" Association="northwindModel.EmployeeTerritories">
+ <End Role="Employees" EntitySet="Employees" />
+ <End Role="Territories" EntitySet="Territories" />
+ </AssociationSet>
+ </EntityContainer>
+ <EntityType Name="Category">
+ <Key>
+ <PropertyRef Name="CategoryID" />
+ </Key>
+ <Property Name="CategoryID" Type="Int32" Nullable="false" annotation:StoreGeneratedPattern="Identity" ConcurrencyMode="Fixed" />
+ <Property Name="CategoryName" Type="String" Nullable="false" MaxLength="15" Unicode="true" FixedLength="false" ConcurrencyMode="None" />
+ <Property Name="Description" Type="String" MaxLength="Max" Unicode="true" FixedLength="false" ConcurrencyMode="None" />
+ <Property Name="Picture" Type="Binary" MaxLength="Max" FixedLength="false" />
+ <NavigationProperty Name="Products" Relationship="northwindModel.FK_Products_Categories" FromRole="Categories" ToRole="Products" />
+ </EntityType>
+ <EntityType Name="CustomerDemographic">
+ <Key>
+ <PropertyRef Name="CustomerTypeID" />
+ </Key>
+ <Property Name="CustomerTypeID" Type="String" Nullable="false" MaxLength="10" Unicode="true" FixedLength="true" />
+ <Property Name="CustomerDesc" Type="String" MaxLength="Max" Unicode="true" FixedLength="false" />
+ <NavigationProperty Name="Customers" Relationship="northwindModel.CustomerCustomerDemo" FromRole="CustomerDemographics" ToRole="Customers" />
+ </EntityType>
+ <EntityType Name="Customer">
+ <Key>
+ <PropertyRef Name="CustomerID" />
+ </Key>
+ <Property Name="CustomerID" Type="String" Nullable="false" MaxLength="5" Unicode="true" FixedLength="true" ConcurrencyMode="Fixed" />
+ <Property Name="CompanyName" Type="String" Nullable="false" MaxLength="40" Unicode="true" FixedLength="false" ConcurrencyMode="Fixed" />
+ <Property Name="ContactName" Type="String" MaxLength="30" Unicode="true" FixedLength="false" ConcurrencyMode="Fixed" />
+ <Property Name="ContactTitle" Type="String" MaxLength="30" Unicode="true" FixedLength="false" ConcurrencyMode="Fixed" />
+ <Property Name="Address" Type="String" MaxLength="60" Unicode="true" FixedLength="false" ConcurrencyMode="Fixed" />
+ <Property Name="City" Type="String" MaxLength="15" Unicode="true" FixedLength="false" ConcurrencyMode="Fixed" />
+ <Property Name="Region" Type="String" MaxLength="15" Unicode="true" FixedLength="false" ConcurrencyMode="Fixed" />
+ <Property Name="PostalCode" Type="String" MaxLength="10" Unicode="true" FixedLength="false" ConcurrencyMode="Fixed" />
+ <Property Name="Country" Type="String" MaxLength="15" Unicode="true" FixedLength="false" ConcurrencyMode="Fixed" />
+ <Property Name="Phone" Type="String" MaxLength="24" Unicode="true" FixedLength="false" ConcurrencyMode="Fixed" />
+ <Property Name="Fax" Type="String" MaxLength="24" Unicode="true" FixedLength="false" ConcurrencyMode="Fixed" />
+ <NavigationProperty Name="Orders" Relationship="northwindModel.FK_Orders_Customers" FromRole="Customers" ToRole="Orders" />
+ <NavigationProperty Name="CustomerDemographics" Relationship="northwindModel.CustomerCustomerDemo" FromRole="Customers" ToRole="CustomerDemographics" />
+ </EntityType>
+ <EntityType Name="Employee">
+ <Key>
+ <PropertyRef Name="EmployeeID" />
+ </Key>
+ <Property Name="EmployeeID" Type="Int32" Nullable="false" annotation:StoreGeneratedPattern="Identity" ConcurrencyMode="Fixed" />
+ <Property Name="LastName" Type="String" Nullable="false" MaxLength="20" Unicode="true" FixedLength="false" ConcurrencyMode="Fixed" />
+ <Property Name="FirstName" Type="String" Nullable="false" MaxLength="10" Unicode="true" FixedLength="false" ConcurrencyMode="Fixed" />
+ <Property Name="Title" Type="String" MaxLength="30" Unicode="true" FixedLength="false" ConcurrencyMode="Fixed" />
+ <Property Name="TitleOfCourtesy" Type="String" MaxLength="25" Unicode="true" FixedLength="false" ConcurrencyMode="Fixed" />
+ <Property Name="BirthDate" Type="DateTime" ConcurrencyMode="Fixed" />
+ <Property Name="HireDate" Type="DateTime" ConcurrencyMode="Fixed" />
+ <Property Name="Address" Type="String" MaxLength="60" Unicode="true" FixedLength="false" ConcurrencyMode="Fixed" />
+ <Property Name="City" Type="String" MaxLength="15" Unicode="true" FixedLength="false" ConcurrencyMode="Fixed" />
+ <Property Name="Region" Type="String" MaxLength="15" Unicode="true" FixedLength="false" ConcurrencyMode="Fixed" />
+ <Property Name="PostalCode" Type="String" MaxLength="10" Unicode="true" FixedLength="false" ConcurrencyMode="Fixed" />
+ <Property Name="Country" Type="String" MaxLength="15" Unicode="true" FixedLength="false" ConcurrencyMode="Fixed" />
+ <Property Name="HomePhone" Type="String" MaxLength="24" Unicode="true" FixedLength="false" ConcurrencyMode="Fixed" />
+ <Property Name="Extension" Type="String" MaxLength="4" Unicode="true" FixedLength="false" ConcurrencyMode="Fixed" />
+ <Property Name="Photo" Type="Binary" MaxLength="Max" FixedLength="false" />
+ <Property Name="Notes" Type="String" MaxLength="Max" Unicode="true" FixedLength="false" />
+ <Property Name="ReportsTo" Type="Int32" ConcurrencyMode="Fixed" />
+ <Property Name="PhotoPath" Type="String" MaxLength="255" Unicode="true" FixedLength="false" />
+ <NavigationProperty Name="Employees1" Relationship="northwindModel.FK_Employees_Employees" FromRole="Employees" ToRole="Employees1" />
+ <NavigationProperty Name="Employee1" Relationship="northwindModel.FK_Employees_Employees" FromRole="Employees1" ToRole="Employees" />
+ <NavigationProperty Name="Orders" Relationship="northwindModel.FK_Orders_Employees" FromRole="Employees" ToRole="Orders" />
+ <NavigationProperty Name="Territories" Relationship="northwindModel.EmployeeTerritories" FromRole="Employees" ToRole="Territories" />
+ </EntityType>
+ <EntityType Name="Order_Detail">
+ <Key>
+ <PropertyRef Name="OrderID" />
+ <PropertyRef Name="ProductID" />
+ </Key>
+ <Property Name="OrderID" Type="Int32" Nullable="false" ConcurrencyMode="Fixed" />
+ <Property Name="ProductID" Type="Int32" Nullable="false" ConcurrencyMode="Fixed" />
+ <Property Name="UnitPrice" Type="Decimal" Nullable="false" Precision="19" Scale="4" ConcurrencyMode="Fixed" />
+ <Property Name="Quantity" Type="Int16" Nullable="false" ConcurrencyMode="Fixed" />
+ <Property Name="Discount" Type="Single" Nullable="false" ConcurrencyMode="Fixed" />
+ <NavigationProperty Name="Order" Relationship="northwindModel.FK_Order_Details_Orders" FromRole="Order_Details" ToRole="Orders" />
+ <NavigationProperty Name="Product" Relationship="northwindModel.FK_Order_Details_Products" FromRole="Order_Details" ToRole="Products" />
+ </EntityType>
+ <EntityType Name="Order">
+ <Key>
+ <PropertyRef Name="OrderID" />
+ </Key>
+ <Property Name="OrderID" Type="Int32" Nullable="false" annotation:StoreGeneratedPattern="Identity" ConcurrencyMode="Fixed" />
+ <Property Name="CustomerID" Type="String" MaxLength="5" Unicode="true" FixedLength="true" ConcurrencyMode="Fixed" />
+ <Property Name="EmployeeID" Type="Int32" ConcurrencyMode="Fixed" />
+ <Property Name="OrderDate" Type="DateTime" ConcurrencyMode="Fixed" />
+ <Property Name="RequiredDate" Type="DateTime" ConcurrencyMode="Fixed" />
+ <Property Name="ShippedDate" Type="DateTime" ConcurrencyMode="Fixed" />
+ <Property Name="ShipVia" Type="Int32" ConcurrencyMode="Fixed" />
+ <Property Name="Freight" Type="Decimal" Precision="19" Scale="4" ConcurrencyMode="Fixed" />
+ <Property Name="ShipName" Type="String" MaxLength="40" Unicode="true" FixedLength="false" ConcurrencyMode="Fixed" />
+ <Property Name="ShipAddress" Type="String" MaxLength="60" Unicode="true" FixedLength="false" ConcurrencyMode="Fixed" />
+ <Property Name="ShipCity" Type="String" MaxLength="15" Unicode="true" FixedLength="false" ConcurrencyMode="Fixed" />
+ <Property Name="ShipRegion" Type="String" MaxLength="15" Unicode="true" FixedLength="false" ConcurrencyMode="Fixed" />
+ <Property Name="ShipPostalCode" Type="String" MaxLength="10" Unicode="true" FixedLength="false" ConcurrencyMode="Fixed" />
+ <Property Name="ShipCountry" Type="String" MaxLength="15" Unicode="true" FixedLength="false" ConcurrencyMode="Fixed" />
+ <NavigationProperty Name="Customer" Relationship="northwindModel.FK_Orders_Customers" FromRole="Orders" ToRole="Customers" />
+ <NavigationProperty Name="Employee" Relationship="northwindModel.FK_Orders_Employees" FromRole="Orders" ToRole="Employees" />
+ <NavigationProperty Name="Order_Details" Relationship="northwindModel.FK_Order_Details_Orders" FromRole="Orders" ToRole="Order_Details" />
+ <NavigationProperty Name="Shipper" Relationship="northwindModel.FK_Orders_Shippers" FromRole="Orders" ToRole="Shippers" />
+ </EntityType>
+ <EntityType Name="Product">
+ <Key>
+ <PropertyRef Name="ProductID" />
+ </Key>
+ <Property Name="ProductID" Type="Int32" Nullable="false" annotation:StoreGeneratedPattern="Identity" ConcurrencyMode="None" />
+ <Property Name="ProductName" Type="String" Nullable="false" MaxLength="40" Unicode="true" FixedLength="false" ConcurrencyMode="Fixed" />
+ <Property Name="SupplierID" Type="Int32" ConcurrencyMode="Fixed" />
+ <Property Name="CategoryID" Type="Int32" ConcurrencyMode="Fixed" />
+ <Property Name="QuantityPerUnit" Type="String" MaxLength="20" Unicode="true" FixedLength="false" ConcurrencyMode="Fixed" />
+ <Property Name="UnitPrice" Type="Decimal" Precision="19" Scale="4" ConcurrencyMode="Fixed" />
+ <Property Name="UnitsInStock" Type="Int16" ConcurrencyMode="Fixed" />
+ <Property Name="UnitsOnOrder" Type="Int16" ConcurrencyMode="Fixed" />
+ <Property Name="ReorderLevel" Type="Int16" ConcurrencyMode="Fixed" />
+ <Property Name="Discontinued" Type="Boolean" Nullable="false" ConcurrencyMode="Fixed" />
+ <NavigationProperty Name="Category" Relationship="northwindModel.FK_Products_Categories" FromRole="Products" ToRole="Categories" />
+ <NavigationProperty Name="Order_Details" Relationship="northwindModel.FK_Order_Details_Products" FromRole="Products" ToRole="Order_Details" />
+ <NavigationProperty Name="Supplier" Relationship="northwindModel.FK_Products_Suppliers" FromRole="Products" ToRole="Suppliers" />
+ </EntityType>
+ <EntityType Name="Region">
+ <Key>
+ <PropertyRef Name="RegionID" />
+ </Key>
+ <Property Name="RegionID" Type="Int32" Nullable="false" />
+ <Property Name="RegionDescription" Type="String" Nullable="false" MaxLength="50" Unicode="true" FixedLength="true" />
+ <NavigationProperty Name="Territories" Relationship="northwindModel.FK_Territories_Region" FromRole="Region" ToRole="Territories" />
+ </EntityType>
+ <EntityType Name="Shipper">
+ <Key>
+ <PropertyRef Name="ShipperID" />
+ </Key>
+ <Property Name="ShipperID" Type="Int32" Nullable="false" annotation:StoreGeneratedPattern="Identity" />
+ <Property Name="CompanyName" Type="String" Nullable="false" MaxLength="40" Unicode="true" FixedLength="false" />
+ <Property Name="Phone" Type="String" MaxLength="24" Unicode="true" FixedLength="false" />
+ <NavigationProperty Name="Orders" Relationship="northwindModel.FK_Orders_Shippers" FromRole="Shippers" ToRole="Orders" />
+ </EntityType>
+ <EntityType Name="Supplier">
+ <Key>
+ <PropertyRef Name="SupplierID" />
+ </Key>
+ <Property Name="SupplierID" Type="Int32" Nullable="false" annotation:StoreGeneratedPattern="Identity" />
+ <Property Name="CompanyName" Type="String" Nullable="false" MaxLength="40" Unicode="true" FixedLength="false" />
+ <Property Name="ContactName" Type="String" MaxLength="30" Unicode="true" FixedLength="false" />
+ <Property Name="ContactTitle" Type="String" MaxLength="30" Unicode="true" FixedLength="false" />
+ <Property Name="Address" Type="String" MaxLength="60" Unicode="true" FixedLength="false" />
+ <Property Name="City" Type="String" MaxLength="15" Unicode="true" FixedLength="false" />
+ <Property Name="Region" Type="String" MaxLength="15" Unicode="true" FixedLength="false" />
+ <Property Name="PostalCode" Type="String" MaxLength="10" Unicode="true" FixedLength="false" />
+ <Property Name="Country" Type="String" MaxLength="15" Unicode="true" FixedLength="false" />
+ <Property Name="Phone" Type="String" MaxLength="24" Unicode="true" FixedLength="false" />
+ <Property Name="Fax" Type="String" MaxLength="24" Unicode="true" FixedLength="false" />
+ <Property Name="HomePage" Type="String" MaxLength="Max" Unicode="true" FixedLength="false" />
+ <NavigationProperty Name="Products" Relationship="northwindModel.FK_Products_Suppliers" FromRole="Suppliers" ToRole="Products" />
+ </EntityType>
+ <EntityType Name="Territory">
+ <Key>
+ <PropertyRef Name="TerritoryID" />
+ </Key>
+ <Property Name="TerritoryID" Type="String" Nullable="false" MaxLength="20" Unicode="true" FixedLength="false" />
+ <Property Name="TerritoryDescription" Type="String" Nullable="false" MaxLength="50" Unicode="true" FixedLength="true" />
+ <Property Name="RegionID" Type="Int32" Nullable="false" />
+ <NavigationProperty Name="Region" Relationship="northwindModel.FK_Territories_Region" FromRole="Territories" ToRole="Region" />
+ <NavigationProperty Name="Employees" Relationship="northwindModel.EmployeeTerritories" FromRole="Territories" ToRole="Employees" />
+ </EntityType>
+ <Association Name="FK_Products_Categories">
+ <End Role="Categories" Type="northwindModel.Category" Multiplicity="0..1" />
+ <End Role="Products" Type="northwindModel.Product" Multiplicity="*" />
+ <ReferentialConstraint>
+ <Principal Role="Categories">
+ <PropertyRef Name="CategoryID" />
+ </Principal>
+ <Dependent Role="Products">
+ <PropertyRef Name="CategoryID" />
+ </Dependent>
+ </ReferentialConstraint>
+ </Association>
+ <Association Name="FK_Orders_Customers">
+ <End Role="Customers" Type="northwindModel.Customer" Multiplicity="0..1" />
+ <End Role="Orders" Type="northwindModel.Order" Multiplicity="*" />
+ <ReferentialConstraint>
+ <Principal Role="Customers">
+ <PropertyRef Name="CustomerID" />
+ </Principal>
+ <Dependent Role="Orders">
+ <PropertyRef Name="CustomerID" />
+ </Dependent>
+ </ReferentialConstraint>
+ </Association>
+ <Association Name="FK_Employees_Employees">
+ <End Role="Employees" Type="northwindModel.Employee" Multiplicity="0..1" />
+ <End Role="Employees1" Type="northwindModel.Employee" Multiplicity="*" />
+ <ReferentialConstraint>
+ <Principal Role="Employees">
+ <PropertyRef Name="EmployeeID" />
+ </Principal>
+ <Dependent Role="Employees1">
+ <PropertyRef Name="ReportsTo" />
+ </Dependent>
+ </ReferentialConstraint>
+ </Association>
+ <Association Name="FK_Orders_Employees">
+ <End Role="Employees" Type="northwindModel.Employee" Multiplicity="0..1" />
+ <End Role="Orders" Type="northwindModel.Order" Multiplicity="*" />
+ <ReferentialConstraint>
+ <Principal Role="Employees">
+ <PropertyRef Name="EmployeeID" />
+ </Principal>
+ <Dependent Role="Orders">
+ <PropertyRef Name="EmployeeID" />
+ </Dependent>
+ </ReferentialConstraint>
+ </Association>
+ <Association Name="FK_Order_Details_Orders">
+ <End Role="Orders" Type="northwindModel.Order" Multiplicity="1" />
+ <End Role="Order_Details" Type="northwindModel.Order_Detail" Multiplicity="*" />
+ <ReferentialConstraint>
+ <Principal Role="Orders">
+ <PropertyRef Name="OrderID" />
+ </Principal>
+ <Dependent Role="Order_Details">
+ <PropertyRef Name="OrderID" />
+ </Dependent>
+ </ReferentialConstraint>
+ </Association>
+ <Association Name="FK_Order_Details_Products">
+ <End Role="Products" Type="northwindModel.Product" Multiplicity="1" />
+ <End Role="Order_Details" Type="northwindModel.Order_Detail" Multiplicity="*" />
+ <ReferentialConstraint>
+ <Principal Role="Products">
+ <PropertyRef Name="ProductID" />
+ </Principal>
+ <Dependent Role="Order_Details">
+ <PropertyRef Name="ProductID" />
+ </Dependent>
+ </ReferentialConstraint>
+ </Association>
+ <Association Name="FK_Orders_Shippers">
+ <End Role="Shippers" Type="northwindModel.Shipper" Multiplicity="0..1" />
+ <End Role="Orders" Type="northwindModel.Order" Multiplicity="*" />
+ <ReferentialConstraint>
+ <Principal Role="Shippers">
+ <PropertyRef Name="ShipperID" />
+ </Principal>
+ <Dependent Role="Orders">
+ <PropertyRef Name="ShipVia" />
+ </Dependent>
+ </ReferentialConstraint>
+ </Association>
+ <Association Name="FK_Products_Suppliers">
+ <End Role="Suppliers" Type="northwindModel.Supplier" Multiplicity="0..1" />
+ <End Role="Products" Type="northwindModel.Product" Multiplicity="*" />
+ <ReferentialConstraint>
+ <Principal Role="Suppliers">
+ <PropertyRef Name="SupplierID" />
+ </Principal>
+ <Dependent Role="Products">
+ <PropertyRef Name="SupplierID" />
+ </Dependent>
+ </ReferentialConstraint>
+ </Association>
+ <Association Name="FK_Territories_Region">
+ <End Role="Region" Type="northwindModel.Region" Multiplicity="1" />
+ <End Role="Territories" Type="northwindModel.Territory" Multiplicity="*" />
+ <ReferentialConstraint>
+ <Principal Role="Region">
+ <PropertyRef Name="RegionID" />
+ </Principal>
+ <Dependent Role="Territories">
+ <PropertyRef Name="RegionID" />
+ </Dependent>
+ </ReferentialConstraint>
+ </Association>
+ <Association Name="CustomerCustomerDemo">
+ <End Role="CustomerDemographics" Type="northwindModel.CustomerDemographic" Multiplicity="*" />
+ <End Role="Customers" Type="northwindModel.Customer" Multiplicity="*" />
+ </Association>
+ <Association Name="EmployeeTerritories">
+ <End Role="Employees" Type="northwindModel.Employee" Multiplicity="*" />
+ <End Role="Territories" Type="northwindModel.Territory" Multiplicity="*" />
+ </Association>
+ </Schema>
+ </edmx:ConceptualModels>
+ <!-- C-S mapping content -->
+ <edmx:Mappings>
+ <Mapping Space="C-S" xmlns="http://schemas.microsoft.com/ado/2008/09/mapping/cs">
+ <EntityContainerMapping StorageEntityContainer="northwindModelStoreContainer" CdmEntityContainer="NorthwindEntities">
+ <EntitySetMapping Name="Categories"><EntityTypeMapping TypeName="northwindModel.Category"><MappingFragment StoreEntitySet="Categories">
+ <ScalarProperty Name="CategoryID" ColumnName="CategoryID" />
+ <ScalarProperty Name="CategoryName" ColumnName="CategoryName" />
+ <ScalarProperty Name="Description" ColumnName="Description" />
+ <ScalarProperty Name="Picture" ColumnName="Picture" />
+ </MappingFragment></EntityTypeMapping></EntitySetMapping>
+ <EntitySetMapping Name="CustomerDemographics"><EntityTypeMapping TypeName="northwindModel.CustomerDemographic"><MappingFragment StoreEntitySet="CustomerDemographics">
+ <ScalarProperty Name="CustomerTypeID" ColumnName="CustomerTypeID" />
+ <ScalarProperty Name="CustomerDesc" ColumnName="CustomerDesc" />
+ </MappingFragment></EntityTypeMapping></EntitySetMapping>
+ <EntitySetMapping Name="Customers"><EntityTypeMapping TypeName="northwindModel.Customer"><MappingFragment StoreEntitySet="Customers">
+ <ScalarProperty Name="CustomerID" ColumnName="CustomerID" />
+ <ScalarProperty Name="CompanyName" ColumnName="CompanyName" />
+ <ScalarProperty Name="ContactName" ColumnName="ContactName" />
+ <ScalarProperty Name="ContactTitle" ColumnName="ContactTitle" />
+ <ScalarProperty Name="Address" ColumnName="Address" />
+ <ScalarProperty Name="City" ColumnName="City" />
+ <ScalarProperty Name="Region" ColumnName="Region" />
+ <ScalarProperty Name="PostalCode" ColumnName="PostalCode" />
+ <ScalarProperty Name="Country" ColumnName="Country" />
+ <ScalarProperty Name="Phone" ColumnName="Phone" />
+ <ScalarProperty Name="Fax" ColumnName="Fax" />
+ </MappingFragment></EntityTypeMapping></EntitySetMapping>
+ <EntitySetMapping Name="Employees"><EntityTypeMapping TypeName="northwindModel.Employee"><MappingFragment StoreEntitySet="Employees">
+ <ScalarProperty Name="EmployeeID" ColumnName="EmployeeID" />
+ <ScalarProperty Name="LastName" ColumnName="LastName" />
+ <ScalarProperty Name="FirstName" ColumnName="FirstName" />
+ <ScalarProperty Name="Title" ColumnName="Title" />
+ <ScalarProperty Name="TitleOfCourtesy" ColumnName="TitleOfCourtesy" />
+ <ScalarProperty Name="BirthDate" ColumnName="BirthDate" />
+ <ScalarProperty Name="HireDate" ColumnName="HireDate" />
+ <ScalarProperty Name="Address" ColumnName="Address" />
+ <ScalarProperty Name="City" ColumnName="City" />
+ <ScalarProperty Name="Region" ColumnName="Region" />
+ <ScalarProperty Name="PostalCode" ColumnName="PostalCode" />
+ <ScalarProperty Name="Country" ColumnName="Country" />
+ <ScalarProperty Name="HomePhone" ColumnName="HomePhone" />
+ <ScalarProperty Name="Extension" ColumnName="Extension" />
+ <ScalarProperty Name="Photo" ColumnName="Photo" />
+ <ScalarProperty Name="Notes" ColumnName="Notes" />
+ <ScalarProperty Name="ReportsTo" ColumnName="ReportsTo" />
+ <ScalarProperty Name="PhotoPath" ColumnName="PhotoPath" />
+ </MappingFragment></EntityTypeMapping></EntitySetMapping>
+ <EntitySetMapping Name="Order_Details"><EntityTypeMapping TypeName="northwindModel.Order_Detail"><MappingFragment StoreEntitySet="Order Details">
+ <ScalarProperty Name="OrderID" ColumnName="OrderID" />
+ <ScalarProperty Name="ProductID" ColumnName="ProductID" />
+ <ScalarProperty Name="UnitPrice" ColumnName="UnitPrice" />
+ <ScalarProperty Name="Quantity" ColumnName="Quantity" />
+ <ScalarProperty Name="Discount" ColumnName="Discount" />
+ </MappingFragment></EntityTypeMapping></EntitySetMapping>
+ <EntitySetMapping Name="Orders"><EntityTypeMapping TypeName="northwindModel.Order"><MappingFragment StoreEntitySet="Orders">
+ <ScalarProperty Name="OrderID" ColumnName="OrderID" />
+ <ScalarProperty Name="CustomerID" ColumnName="CustomerID" />
+ <ScalarProperty Name="EmployeeID" ColumnName="EmployeeID" />
+ <ScalarProperty Name="OrderDate" ColumnName="OrderDate" />
+ <ScalarProperty Name="RequiredDate" ColumnName="RequiredDate" />
+ <ScalarProperty Name="ShippedDate" ColumnName="ShippedDate" />
+ <ScalarProperty Name="ShipVia" ColumnName="ShipVia" />
+ <ScalarProperty Name="Freight" ColumnName="Freight" />
+ <ScalarProperty Name="ShipName" ColumnName="ShipName" />
+ <ScalarProperty Name="ShipAddress" ColumnName="ShipAddress" />
+ <ScalarProperty Name="ShipCity" ColumnName="ShipCity" />
+ <ScalarProperty Name="ShipRegion" ColumnName="ShipRegion" />
+ <ScalarProperty Name="ShipPostalCode" ColumnName="ShipPostalCode" />
+ <ScalarProperty Name="ShipCountry" ColumnName="ShipCountry" />
+ </MappingFragment></EntityTypeMapping></EntitySetMapping>
+ <EntitySetMapping Name="Products"><EntityTypeMapping TypeName="northwindModel.Product"><MappingFragment StoreEntitySet="Products">
+ <ScalarProperty Name="ProductID" ColumnName="ProductID" />
+ <ScalarProperty Name="ProductName" ColumnName="ProductName" />
+ <ScalarProperty Name="SupplierID" ColumnName="SupplierID" />
+ <ScalarProperty Name="CategoryID" ColumnName="CategoryID" />
+ <ScalarProperty Name="QuantityPerUnit" ColumnName="QuantityPerUnit" />
+ <ScalarProperty Name="UnitPrice" ColumnName="UnitPrice" />
+ <ScalarProperty Name="UnitsInStock" ColumnName="UnitsInStock" />
+ <ScalarProperty Name="UnitsOnOrder" ColumnName="UnitsOnOrder" />
+ <ScalarProperty Name="ReorderLevel" ColumnName="ReorderLevel" />
+ <ScalarProperty Name="Discontinued" ColumnName="Discontinued" />
+ </MappingFragment></EntityTypeMapping></EntitySetMapping>
+ <EntitySetMapping Name="Regions"><EntityTypeMapping TypeName="northwindModel.Region"><MappingFragment StoreEntitySet="Region">
+ <ScalarProperty Name="RegionID" ColumnName="RegionID" />
+ <ScalarProperty Name="RegionDescription" ColumnName="RegionDescription" />
+ </MappingFragment></EntityTypeMapping></EntitySetMapping>
+ <EntitySetMapping Name="Shippers"><EntityTypeMapping TypeName="northwindModel.Shipper"><MappingFragment StoreEntitySet="Shippers">
+ <ScalarProperty Name="ShipperID" ColumnName="ShipperID" />
+ <ScalarProperty Name="CompanyName" ColumnName="CompanyName" />
+ <ScalarProperty Name="Phone" ColumnName="Phone" />
+ </MappingFragment></EntityTypeMapping></EntitySetMapping>
+ <EntitySetMapping Name="Suppliers"><EntityTypeMapping TypeName="northwindModel.Supplier"><MappingFragment StoreEntitySet="Suppliers">
+ <ScalarProperty Name="SupplierID" ColumnName="SupplierID" />
+ <ScalarProperty Name="CompanyName" ColumnName="CompanyName" />
+ <ScalarProperty Name="ContactName" ColumnName="ContactName" />
+ <ScalarProperty Name="ContactTitle" ColumnName="ContactTitle" />
+ <ScalarProperty Name="Address" ColumnName="Address" />
+ <ScalarProperty Name="City" ColumnName="City" />
+ <ScalarProperty Name="Region" ColumnName="Region" />
+ <ScalarProperty Name="PostalCode" ColumnName="PostalCode" />
+ <ScalarProperty Name="Country" ColumnName="Country" />
+ <ScalarProperty Name="Phone" ColumnName="Phone" />
+ <ScalarProperty Name="Fax" ColumnName="Fax" />
+ <ScalarProperty Name="HomePage" ColumnName="HomePage" />
+ </MappingFragment></EntityTypeMapping></EntitySetMapping>
+ <EntitySetMapping Name="Territories"><EntityTypeMapping TypeName="northwindModel.Territory"><MappingFragment StoreEntitySet="Territories">
+ <ScalarProperty Name="TerritoryID" ColumnName="TerritoryID" />
+ <ScalarProperty Name="TerritoryDescription" ColumnName="TerritoryDescription" />
+ <ScalarProperty Name="RegionID" ColumnName="RegionID" />
+ </MappingFragment></EntityTypeMapping></EntitySetMapping>
+ <AssociationSetMapping Name="CustomerCustomerDemo" TypeName="northwindModel.CustomerCustomerDemo" StoreEntitySet="CustomerCustomerDemo">
+ <EndProperty Name="CustomerDemographics">
+ <ScalarProperty Name="CustomerTypeID" ColumnName="CustomerTypeID" />
+ </EndProperty>
+ <EndProperty Name="Customers">
+ <ScalarProperty Name="CustomerID" ColumnName="CustomerID" />
+ </EndProperty>
+ </AssociationSetMapping>
+ <AssociationSetMapping Name="EmployeeTerritories" TypeName="northwindModel.EmployeeTerritories" StoreEntitySet="EmployeeTerritories">
+ <EndProperty Name="Employees">
+ <ScalarProperty Name="EmployeeID" ColumnName="EmployeeID" />
+ </EndProperty>
+ <EndProperty Name="Territories">
+ <ScalarProperty Name="TerritoryID" ColumnName="TerritoryID" />
+ </EndProperty>
+ </AssociationSetMapping>
+ </EntityContainerMapping>
+ </Mapping>
+ </edmx:Mappings>
+ </edmx:Runtime>
+ <!-- EF Designer content (DO NOT EDIT MANUALLY BELOW HERE) -->
+ <Designer xmlns="http://schemas.microsoft.com/ado/2008/10/edmx">
+ <Connection>
+ <DesignerInfoPropertySet>
+ <DesignerProperty Name="MetadataArtifactProcessing" Value="EmbedInOutputAssembly" />
+ </DesignerInfoPropertySet>
+ </Connection>
+ <Options>
+ <DesignerInfoPropertySet>
+ <DesignerProperty Name="ValidateOnBuild" Value="true" />
+ <DesignerProperty Name="EnablePluralization" Value="True" />
+ <DesignerProperty Name="IncludeForeignKeysInModel" Value="True" />
+ </DesignerInfoPropertySet>
+ </Options>
+ <!-- Diagram content (shape and connector positions) -->
+ <Diagrams>
+ <Diagram Name="Northwind">
+ <EntityTypeShape EntityType="northwindModel.Category" Width="1.5" PointX="3" PointY="14.5" Height="1.9802864583333317" IsExpanded="true" />
+ <EntityTypeShape EntityType="northwindModel.CustomerDemographic" Width="1.5" PointX="0.75" PointY="2.5" Height="1.5956835937500005" IsExpanded="true" />
+ <EntityTypeShape EntityType="northwindModel.Customer" Width="1.5" PointX="3" PointY="1.5" Height="3.5186979166666661" IsExpanded="true" />
+ <EntityTypeShape EntityType="northwindModel.Employee" Width="1.5" PointX="3" PointY="8.375" Height="5.2494108072916674" IsExpanded="true" />
+ <EntityTypeShape EntityType="northwindModel.Order_Detail" Width="1.5" PointX="7.5" PointY="2.125" Height="2.3648893229166656" IsExpanded="true" />
+ <EntityTypeShape EntityType="northwindModel.Order" Width="1.5" PointX="5.25" PointY="1" Height="4.480205078125" IsExpanded="true" />
+ <EntityTypeShape EntityType="northwindModel.Product" Width="1.5" PointX="5.25" PointY="15.875" Height="3.5186979166666674" IsExpanded="true" />
+ <EntityTypeShape EntityType="northwindModel.Region" Width="1.5" PointX="5.75" PointY="7.125" Height="1.5956835937499996" IsExpanded="true" />
+ <EntityTypeShape EntityType="northwindModel.Shipper" Width="1.5" PointX="3" PointY="5.875" Height="1.7879850260416674" IsExpanded="true" />
+ <EntityTypeShape EntityType="northwindModel.Supplier" Width="1.5" PointX="3" PointY="17.25" Height="3.5186979166666674" IsExpanded="true" />
+ <EntityTypeShape EntityType="northwindModel.Territory" Width="1.5" PointX="8" PointY="9.375" Height="1.9802864583333353" IsExpanded="true" />
+ <AssociationConnector Association="northwindModel.FK_Products_Categories" ManuallyRouted="false">
+ <ConnectorPoint PointX="4.5" PointY="16.177643229166666" />
+ <ConnectorPoint PointX="5.25" PointY="16.177643229166666" /></AssociationConnector>
+ <AssociationConnector Association="northwindModel.FK_Orders_Customers" ManuallyRouted="false">
+ <ConnectorPoint PointX="4.5" PointY="3.2593489583333328" />
+ <ConnectorPoint PointX="5.25" PointY="3.2593489583333328" /></AssociationConnector>
+ <AssociationConnector Association="northwindModel.FK_Employees_Employees" ManuallyRouted="false">
+ <ConnectorPoint PointX="3.5319230769230767" PointY="13.624410807291667" />
+ <ConnectorPoint PointX="3.5319230769230767" PointY="13.874410807291667" />
+ <ConnectorPoint PointX="3.9784615384615383" PointY="13.874410807291667" />
+ <ConnectorPoint PointX="3.9784615384615383" PointY="13.624410807291667" /></AssociationConnector>
+ <AssociationConnector Association="northwindModel.FK_Orders_Employees" ManuallyRouted="false">
+ <ConnectorPoint PointX="4.5" PointY="11.203797200520834" />
+ <ConnectorPoint PointX="5.46875" PointY="11.203797200520834" />
+ <ConnectorPoint PointX="5.46875" PointY="5.480205078125" /></AssociationConnector>
+ <AssociationConnector Association="northwindModel.FK_Order_Details_Orders" ManuallyRouted="false">
+ <ConnectorPoint PointX="6.75" PointY="3.3074446614583328" />
+ <ConnectorPoint PointX="7.5" PointY="3.3074446614583328" /></AssociationConnector>
+ <AssociationConnector Association="northwindModel.FK_Order_Details_Products" ManuallyRouted="false">
+ <ConnectorPoint PointX="6.75" PointY="17.634348958333334" />
+ <ConnectorPoint PointX="7.71875" PointY="17.634348958333334" />
+ <ConnectorPoint PointX="7.71875" PointY="4.4898893229166656" /></AssociationConnector>
+ <AssociationConnector Association="northwindModel.FK_Orders_Shippers" ManuallyRouted="false">
+ <ConnectorPoint PointX="4.5" PointY="6.46875" />
+ <ConnectorPoint PointX="5.3281225" PointY="6.46875" />
+ <ConnectorPoint PointX="5.3281225" PointY="5.480205078125" /></AssociationConnector>
+ <AssociationConnector Association="northwindModel.FK_Products_Suppliers" ManuallyRouted="false">
+ <ConnectorPoint PointX="4.5" PointY="18.321848958333334" />
+ <ConnectorPoint PointX="5.25" PointY="18.321848958333334" /></AssociationConnector>
+ <AssociationConnector Association="northwindModel.FK_Territories_Region" ManuallyRouted="false">
+ <ConnectorPoint PointX="7.25" PointY="7.922841796875" />
+ <ConnectorPoint PointX="7.635416666666667" PointY="7.9228417968749989" />
+ <ConnectorPoint PointX="7.802083333333333" PointY="7.922841796875" />
+ <ConnectorPoint PointX="8.75" PointY="7.922841796875" />
+ <ConnectorPoint PointX="8.75" PointY="9.375" /></AssociationConnector>
+ <AssociationConnector Association="northwindModel.CustomerCustomerDemo" ManuallyRouted="false">
+ <ConnectorPoint PointX="2.25" PointY="3.2978417968750002" />
+ <ConnectorPoint PointX="3" PointY="3.2978417968750002" /></AssociationConnector>
+ <AssociationConnector Association="northwindModel.EmployeeTerritories" ManuallyRouted="false">
+ <ConnectorPoint PointX="4.5" PointY="10.226898600260416" />
+ <ConnectorPoint PointX="5.385416666666667" PointY="10.226898600260416" />
+ <ConnectorPoint PointX="5.552083333333333" PointY="10.226898600260416" />
+ <ConnectorPoint PointX="7.635416666666667" PointY="10.226898600260416" />
+ <ConnectorPoint PointX="7.802083333333333" PointY="10.226898600260416" />
+ <ConnectorPoint PointX="8" PointY="10.226898600260416" /></AssociationConnector></Diagram></Diagrams>
+ </Designer>
+</edmx:Edmx> \ No newline at end of file
diff --git a/test/Microsoft.Web.Http.Data.Test/Properties/AssemblyInfo.cs b/test/Microsoft.Web.Http.Data.Test/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..4cebce01
--- /dev/null
+++ b/test/Microsoft.Web.Http.Data.Test/Properties/AssemblyInfo.cs
@@ -0,0 +1,34 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("Microsoft.Web.Http.Data.Test")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("Microsoft")]
+[assembly: AssemblyProduct("Microsoft.Web.Http.Data.Test")]
+[assembly: AssemblyCopyright("Copyright © Microsoft 2011")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("8e9662e7-dc7b-4ffa-8cb0-1002491aae66")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/test/Microsoft.Web.Http.Data.Test/TestHelpers.cs b/test/Microsoft.Web.Http.Data.Test/TestHelpers.cs
new file mode 100644
index 00000000..8315be78
--- /dev/null
+++ b/test/Microsoft.Web.Http.Data.Test/TestHelpers.cs
@@ -0,0 +1,114 @@
+using System;
+using System.Data.EntityClient;
+using System.Net.Http;
+using System.Security.Principal;
+using System.Web;
+using System.Web.Http;
+using System.Web.Http.Hosting;
+using System.Web.Http.Routing;
+using Moq;
+
+namespace Microsoft.Web.Http.Data.Test
+{
+ internal static class TestHelpers
+ {
+ internal static HttpRequestMessage CreateTestMessage(string url, HttpMethod httpMethod, HttpConfiguration config)
+ {
+ HttpRequestMessage requestMessage = new HttpRequestMessage(httpMethod, url);
+ IHttpRouteData rd = config.Routes[0].GetRouteData("/", requestMessage);
+ requestMessage.Properties.Add(HttpPropertyKeys.HttpRouteDataKey, rd);
+ var principalMock = new Mock<IPrincipal>();
+ principalMock.Setup(p => p.Identity.IsAuthenticated).Returns(true);
+ requestMessage.Properties.Add(HttpPropertyKeys.UserPrincipalKey, principalMock.Object);
+ return requestMessage;
+ }
+
+ // Return a non-functional connection string for an EF context. This will
+ // allow a context to be instantiated, but not used.
+ internal static string GetTestEFConnectionString()
+ {
+ string connectionString = new EntityConnectionStringBuilder
+ {
+ Metadata = "res://*",
+ Provider = "System.Data.SqlClient",
+ ProviderConnectionString = new System.Data.SqlClient.SqlConnectionStringBuilder
+ {
+ InitialCatalog = "Northwind",
+ DataSource = "xyz",
+ IntegratedSecurity = false,
+ UserID = "xyz",
+ Password = "xyz",
+ }.ConnectionString
+ }.ConnectionString;
+
+ return connectionString;
+ }
+ }
+
+ internal static class TestConstants
+ {
+ public static string BaseUrl = "http://testhost/";
+ public static string CatalogUrl = "http://testhost/Catalog/";
+ public static string CitiesUrl = "http://testhost/Cities/";
+ }
+
+ internal class HttpContextStub : HttpContextBase
+ {
+ private HttpRequestStub request;
+
+ public HttpContextStub(Uri baseAddress, HttpRequestMessage request)
+ {
+ this.request = new HttpRequestStub(baseAddress, request);
+ }
+
+ public override HttpRequestBase Request
+ {
+ get
+ {
+ return this.request;
+ }
+ }
+ }
+
+ internal class HttpRequestStub : HttpRequestBase
+ {
+ private const string AppRelativePrefix = "~/";
+ private string appRelativeCurrentExecutionFilePath;
+
+ public HttpRequestStub(Uri baseAddress, HttpRequestMessage request)
+ {
+ this.appRelativeCurrentExecutionFilePath = GetAppRelativeCurrentExecutionFilePath(baseAddress.AbsoluteUri, request.RequestUri.AbsoluteUri);
+ }
+
+ public override string AppRelativeCurrentExecutionFilePath
+ {
+ get
+ {
+ return this.appRelativeCurrentExecutionFilePath;
+ }
+ }
+
+ public override string PathInfo
+ {
+ get
+ {
+ return String.Empty;
+ }
+ }
+
+ private static string GetAppRelativeCurrentExecutionFilePath(string baseAddress, string requestUri)
+ {
+ int queryPos = requestUri.IndexOf('?');
+ string requestUriNoQuery = queryPos < 0 ? requestUri : requestUri.Substring(0, queryPos);
+
+ if (baseAddress.Length >= requestUriNoQuery.Length)
+ {
+ return AppRelativePrefix;
+ }
+ else
+ {
+ return AppRelativePrefix + requestUriNoQuery.Substring(baseAddress.Length);
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Http.Data.Test/packages.config b/test/Microsoft.Web.Http.Data.Test/packages.config
new file mode 100644
index 00000000..881d2097
--- /dev/null
+++ b/test/Microsoft.Web.Http.Data.Test/packages.config
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="EntityFramework" version="4.1.10331.0" />
+ <package id="Microsoft.Net.Http" version="2.0.20302.1" />
+ <package id="Moq" version="4.0.10827" />
+ <package id="Newtonsoft.Json" version="4.0.8" />
+ <package id="xunit" version="1.9.0.1566" />
+ <package id="xunit.extensions" version="1.9.0.1566" />
+</packages> \ No newline at end of file
diff --git a/test/Microsoft.Web.Mvc.Test/Controls/Test/DesignModeSite.cs b/test/Microsoft.Web.Mvc.Test/Controls/Test/DesignModeSite.cs
new file mode 100644
index 00000000..19d6db0f
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Controls/Test/DesignModeSite.cs
@@ -0,0 +1,34 @@
+using System;
+using System.ComponentModel;
+
+namespace Microsoft.Web.Mvc.Controls.Test
+{
+ public class DesignModeSite : ISite
+ {
+ IComponent ISite.Component
+ {
+ get { throw new NotImplementedException(); }
+ }
+
+ IContainer ISite.Container
+ {
+ get { throw new NotImplementedException(); }
+ }
+
+ bool ISite.DesignMode
+ {
+ get { return true; }
+ }
+
+ string ISite.Name
+ {
+ get { throw new NotImplementedException(); }
+ set { throw new NotImplementedException(); }
+ }
+
+ object IServiceProvider.GetService(Type serviceType)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/Controls/Test/DropDownListTest.cs b/test/Microsoft.Web.Mvc.Test/Controls/Test/DropDownListTest.cs
new file mode 100644
index 00000000..26322d09
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Controls/Test/DropDownListTest.cs
@@ -0,0 +1,128 @@
+using System.Web.Mvc;
+using Xunit;
+
+namespace Microsoft.Web.Mvc.Controls.Test
+{
+ public class DropDownListTest
+ {
+ [Fact]
+ public void NameProperty()
+ {
+ // TODO: This
+ }
+
+ [Fact]
+ public void RenderWithNoNameNotInDesignModeThrows()
+ {
+ // TODO: This
+ }
+
+ [Fact]
+ public void RenderWithNoNameInDesignModeRendersWithSampleData()
+ {
+ // Setup
+ DropDownList c = new DropDownList();
+
+ // Execute
+ string html = MvcTestHelper.GetControlRendering(c, true);
+
+ // Verify
+ Assert.Equal(@"<select>
+ <option>
+ Sample Item
+ </option>
+</select>", html);
+ }
+
+ [Fact]
+ public void RenderWithNoAttributes()
+ {
+ // Setup
+ DropDownList c = new DropDownList();
+ c.Name = "nameKey";
+
+ ViewDataContainer vdc = new ViewDataContainer();
+ vdc.Controls.Add(c);
+ vdc.ViewData = new ViewDataDictionary();
+ vdc.ViewData["nameKey"] = new SelectList(new[] { "aaa", "bbb", "ccc" }, "bbb");
+
+ // Execute
+ string html = MvcTestHelper.GetControlRendering(c, false);
+
+ // Verify
+ Assert.Equal(@"<select name=""nameKey"">
+ <option>
+ aaa
+ </option><option selected=""selected"">
+ bbb
+ </option><option>
+ ccc
+ </option>
+</select>", html);
+ }
+
+ [Fact]
+ public void RenderWithTextsAndValues()
+ {
+ // Setup
+ DropDownList c = new DropDownList();
+ c.Name = "nameKey";
+
+ ViewDataContainer vdc = new ViewDataContainer();
+ vdc.Controls.Add(c);
+ vdc.ViewData = new ViewDataDictionary();
+ vdc.ViewData["nameKey"] = new SelectList(
+ new[]
+ {
+ new { Text = "aaa", Value = "111" },
+ new { Text = "bbb", Value = "222" },
+ new { Text = "ccc", Value = "333" }
+ },
+ "Value",
+ "Text",
+ "222");
+
+ // Execute
+ string html = MvcTestHelper.GetControlRendering(c, false);
+
+ // Verify
+ Assert.Equal(@"<select name=""nameKey"">
+ <option value=""111"">
+ aaa
+ </option><option value=""222"" selected=""selected"">
+ bbb
+ </option><option value=""333"">
+ ccc
+ </option>
+</select>", html);
+ }
+
+ [Fact]
+ public void RenderWithNameAndIdRendersNameAndIdAttribute()
+ {
+ // Setup
+ DropDownList c = new DropDownList();
+ c.Name = "nameKey";
+ c.ID = "someID";
+
+ ViewDataContainer vdc = new ViewDataContainer();
+ vdc.Controls.Add(c);
+ vdc.ViewData = new ViewDataDictionary();
+ vdc.ViewData["nameKey"] = new SelectList(new[] { "aaa", "bbb", "ccc" }, "bbb");
+
+ // Execute
+ string html = MvcTestHelper.GetControlRendering(c, false);
+
+ // Verify
+ Assert.Equal(@"<select id=""someID"" name=""nameKey"">
+ <option>
+ aaa
+ </option><option selected=""selected"">
+ bbb
+ </option><option>
+ ccc
+ </option>
+</select>", html);
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/Controls/Test/MvcControlTest.cs b/test/Microsoft.Web.Mvc.Test/Controls/Test/MvcControlTest.cs
new file mode 100644
index 00000000..66275637
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Controls/Test/MvcControlTest.cs
@@ -0,0 +1,80 @@
+using System.Collections.Generic;
+using System.Web.Mvc;
+using System.Web.UI;
+using Xunit;
+
+namespace Microsoft.Web.Mvc.Controls.Test
+{
+ public class MvcControlTest
+ {
+ [Fact]
+ public void AttributesProperty()
+ {
+ // Setup
+ DummyMvcControl c = new DummyMvcControl();
+
+ // Execute
+ IDictionary<string, string> attrs = c.Attributes;
+
+ // Verify
+ Assert.NotNull(attrs);
+ Assert.Empty(attrs);
+ }
+
+ [Fact]
+ public void GetSetAttributes()
+ {
+ // Setup
+ DummyMvcControl c = new DummyMvcControl();
+ IAttributeAccessor attrAccessor = c;
+ IDictionary<string, string> attrs = c.Attributes;
+
+ // Execute and Verify
+ string value;
+ value = attrAccessor.GetAttribute("xyz");
+ Assert.Null(value);
+
+ attrAccessor.SetAttribute("a1", "v1");
+ value = attrAccessor.GetAttribute("a1");
+ Assert.Equal("v1", value);
+ Assert.Single(attrs);
+ value = c.Attributes["a1"];
+ Assert.Equal("v1", value);
+ }
+
+ [Fact]
+ public void EnableViewStateProperty()
+ {
+ DummyMvcControl c = new DummyMvcControl();
+ Assert.True(c.EnableViewState);
+ Assert.True((c).EnableViewState);
+
+ c.EnableViewState = false;
+ Assert.False(c.EnableViewState);
+ Assert.False((c).EnableViewState);
+
+ c.EnableViewState = true;
+ Assert.True(c.EnableViewState);
+ Assert.True((c).EnableViewState);
+ }
+
+ [Fact]
+ public void ViewContextWithNoPageIsNull()
+ {
+ // Setup
+ DummyMvcControl c = new DummyMvcControl();
+ Control c1 = new Control();
+ c1.Controls.Add(c);
+
+ // Execute
+ ViewContext vc = c.ViewContext;
+
+ // Verify
+ Assert.Null(vc);
+ }
+
+ private sealed class DummyMvcControl : MvcControl
+ {
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/Controls/Test/MvcTestHelper.cs b/test/Microsoft.Web.Mvc.Test/Controls/Test/MvcTestHelper.cs
new file mode 100644
index 00000000..fbd4b60a
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Controls/Test/MvcTestHelper.cs
@@ -0,0 +1,19 @@
+using System.IO;
+using System.Web.UI;
+
+namespace Microsoft.Web.Mvc.Controls.Test
+{
+ public static class MvcTestHelper
+ {
+ public static string GetControlRendering(Control c, bool designMode)
+ {
+ if (designMode)
+ {
+ c.Site = new DesignModeSite();
+ }
+ HtmlTextWriter writer = new HtmlTextWriter(new StringWriter());
+ c.RenderControl(writer);
+ return writer.InnerWriter.ToString();
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/Controls/Test/ViewDataContainer.cs b/test/Microsoft.Web.Mvc.Test/Controls/Test/ViewDataContainer.cs
new file mode 100644
index 00000000..a0c1a842
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Controls/Test/ViewDataContainer.cs
@@ -0,0 +1,10 @@
+using System.Web.Mvc;
+using System.Web.UI;
+
+namespace Microsoft.Web.Mvc.Controls.Test
+{
+ public class ViewDataContainer : Control, IViewDataContainer
+ {
+ public ViewDataDictionary ViewData { get; set; }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/Microsoft.Web.Mvc.Test.csproj b/test/Microsoft.Web.Mvc.Test/Microsoft.Web.Mvc.Test.csproj
new file mode 100644
index 00000000..89289b40
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Microsoft.Web.Mvc.Test.csproj
@@ -0,0 +1,167 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProductVersion>9.0.30729</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{6C28DA70-60F1-4442-967F-591BF3962EC5}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>Microsoft.Web</RootNamespace>
+ <AssemblyName>Microsoft.Web.Mvc.Test</AssemblyName>
+ <FileAlignment>512</FileAlignment>
+ <ProjectTypeGuids>{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ <!-- Force signing off -->
+ <SignAssembly>false</SignAssembly>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Debug\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Release\Test\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'CodeCoverage' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\CodeCoverage\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="Microsoft.CSharp" />
+ <Reference Include="Moq, Version=4.0.10827.0, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL">
+ <HintPath>..\..\packages\Moq.4.0.10827\lib\NET40\Moq.dll</HintPath>
+ </Reference>
+ <Reference Include="System" />
+ <Reference Include="System.ComponentModel.DataAnnotations" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.Data.Linq" />
+ <Reference Include="System.Web" />
+ <Reference Include="System.Web.Abstractions" />
+ <Reference Include="System.Web.Routing" />
+ <Reference Include="xunit">
+ <HintPath>..\..\packages\xunit.1.9.0.1566\lib\xunit.dll</HintPath>
+ </Reference>
+ <Reference Include="xunit.extensions">
+ <HintPath>..\..\packages\xunit.extensions.1.9.0.1566\lib\xunit.extensions.dll</HintPath>
+ </Reference>
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="Controls\Test\DesignModeSite.cs" />
+ <Compile Include="Controls\Test\DropDownListTest.cs" />
+ <Compile Include="Controls\Test\MvcControlTest.cs" />
+ <Compile Include="Controls\Test\MvcTestHelper.cs" />
+ <Compile Include="Controls\Test\ViewDataContainer.cs" />
+ <Compile Include="ModelBinding\Test\BindingBehaviorAttributeTest.cs" />
+ <Compile Include="ModelBinding\Test\BinaryDataModelBinderProviderTest.cs" />
+ <Compile Include="ModelBinding\Test\ExtensibleModelBinderAdapterTest.cs" />
+ <Compile Include="ModelBinding\Test\ExtensibleModelBindingContextTest.cs" />
+ <Compile Include="ModelBinding\Test\GenericModelBinderProviderTest.cs" />
+ <Compile Include="Test\AreaHelpersTest.cs" />
+ <Compile Include="ModelBinding\Test\CollectionModelBinderProviderTest.cs" />
+ <Compile Include="ModelBinding\Test\CollectionModelBinderTest.cs" />
+ <Compile Include="ModelBinding\Test\CollectionModelBinderUtilTest.cs" />
+ <Compile Include="ModelBinding\Test\ComplexModelDtoResultTest.cs" />
+ <Compile Include="ModelBinding\Test\ComplexModelDtoTest.cs" />
+ <Compile Include="ModelBinding\Test\ComplexModelDtoModelBinderTest.cs" />
+ <Compile Include="ModelBinding\Test\ComplexModelDtoModelBinderProviderTest.cs" />
+ <Compile Include="ModelBinding\Test\ArrayModelBinderTest.cs" />
+ <Compile Include="ModelBinding\Test\ArrayModelBinderProviderTest.cs" />
+ <Compile Include="ModelBinding\Test\DictionaryModelBinderProviderTest.cs" />
+ <Compile Include="ModelBinding\Test\DictionaryModelBinderTest.cs" />
+ <Compile Include="ModelBinding\Test\KeyValuePairModelBinderUtilTest.cs" />
+ <Compile Include="ModelBinding\Test\KeyValuePairModelBinderTest.cs" />
+ <Compile Include="ModelBinding\Test\KeyValuePairModelBinderProviderTest.cs" />
+ <Compile Include="ModelBinding\Test\MutableObjectModelBinderProviderTest.cs" />
+ <Compile Include="ModelBinding\Test\MutableObjectModelBinderTest.cs" />
+ <Compile Include="ModelBinding\Test\TypeConverterModelBinderTest.cs" />
+ <Compile Include="ModelBinding\Test\TypeConverterModelBinderProviderTest.cs" />
+ <Compile Include="ModelBinding\Test\ModelBinderConfigTest.cs" />
+ <Compile Include="ModelBinding\Test\SimpleModelBinderProviderTest.cs" />
+ <Compile Include="ModelBinding\Test\TypeMatchModelBinderProviderTest.cs" />
+ <Compile Include="ModelBinding\Test\TypeMatchModelBinderTest.cs" />
+ <Compile Include="ModelBinding\Test\ModelBinderProviderCollectionTest.cs" />
+ <Compile Include="ModelBinding\Test\ModelBinderUtilTest.cs" />
+ <Compile Include="ModelBinding\Test\ModelValidationNodeTest.cs" />
+ <Compile Include="ModelBinding\Test\ModelBinderProvidersTest.cs" />
+ <Compile Include="Test\ButtonTest.cs" />
+ <Compile Include="Test\ContentTypeAttributeTest.cs" />
+ <Compile Include="Test\ControllerExtensionsTest.cs" />
+ <Compile Include="Test\CookieTempDataProviderTest.cs" />
+ <Compile Include="Test\AjaxOnlyAttributeTest.cs" />
+ <Compile Include="Test\AsyncManagerExtensionsTest.cs" />
+ <Compile Include="Test\CookieValueProviderFactoryTest.cs" />
+ <Compile Include="Test\CreditCardAttributeTest.cs" />
+ <Compile Include="Test\DynamicReflectionObjectTest.cs" />
+ <Compile Include="Test\DynamicViewDataDictionaryTest.cs" />
+ <Compile Include="Test\DynamicViewPageTest.cs" />
+ <Compile Include="Test\EmailAddressAttribueTest.cs" />
+ <Compile Include="Test\FileExtensionsAttributeTest.cs" />
+ <Compile Include="Test\ModelCopierTest.cs" />
+ <Compile Include="Test\ElementalValueProviderTest.cs" />
+ <Compile Include="Test\UrlAttributeTest.cs" />
+ <Compile Include="Test\ValueProviderUtilTest.cs" />
+ <Compile Include="Test\TempDataValueProviderFactoryTest.cs" />
+ <Compile Include="Test\SessionValueProviderFactoryTest.cs" />
+ <Compile Include="Test\ServerVariablesValueProviderFactoryTest.cs" />
+ <Compile Include="Test\CopyAsyncParametersAttributeTest.cs" />
+ <Compile Include="Test\CssExtensionsTests.cs" />
+ <Compile Include="Test\DeserializeAttributeTest.cs" />
+ <Compile Include="Test\ScriptExtensionsTest.cs" />
+ <Compile Include="Test\SerializationExtensionsTest.cs" />
+ <Compile Include="Test\MvcSerializerTest.cs" />
+ <Compile Include="Test\ExpressionHelperTest.cs" />
+ <Compile Include="Test\MailToExtensionsTest.cs" />
+ <Compile Include="Test\ReaderWriterCacheTest.cs" />
+ <Compile Include="Test\RenderActionTest.cs" />
+ <Compile Include="Test\SkipBindingAttributeTest.cs" />
+ <Compile Include="Test\FormExtensionsTest.cs" />
+ <Compile Include="Test\RadioExtensionsTest.cs" />
+ <Compile Include="Test\SubmitImageExtensionsTest.cs" />
+ <Compile Include="Test\ImageExtensionsTest.cs" />
+ <Compile Include="Test\SubmitButtonExtensionsTest.cs" />
+ <Compile Include="Test\TypeHelpersTest.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.Web.Mvc\Microsoft.Web.Mvc.csproj">
+ <Project>{D3CF7430-6DA4-42B0-BD90-CA39D16687B2}</Project>
+ <Name>Microsoft.Web.Mvc</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\src\System.Web.Mvc\System.Web.Mvc.csproj">
+ <Project>{3D3FFD8A-624D-4E9B-954B-E1C105507975}</Project>
+ <Name>System.Web.Mvc</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\src\System.Web.WebPages\System.Web.WebPages.csproj">
+ <Project>{76EFA9C5-8D7E-4FDF-B710-E20F8B6B00D2}</Project>
+ <Name>System.Web.WebPages</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\Microsoft.TestCommon\Microsoft.TestCommon.csproj">
+ <Project>{FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0}</Project>
+ <Name>Microsoft.TestCommon</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\System.Web.Mvc.Test\System.Web.Mvc.Test.csproj">
+ <Project>{8AC2A2E4-2F11-4D40-A887-62E2583A65E6}</Project>
+ <Name>System.Web.Mvc.Test</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="packages.config" />
+ </ItemGroup>
+ <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+</Project> \ No newline at end of file
diff --git a/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ArrayModelBinderProviderTest.cs b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ArrayModelBinderProviderTest.cs
new file mode 100644
index 00000000..34219007
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ArrayModelBinderProviderTest.cs
@@ -0,0 +1,100 @@
+using System.Collections.Generic;
+using System.Web.Mvc;
+using Microsoft.Web.UnitTestUtil;
+using Xunit;
+
+namespace Microsoft.Web.Mvc.ModelBinding.Test
+{
+ public class ArrayModelBinderProviderTest
+ {
+ [Fact]
+ public void GetBinder_CorrectModelTypeAndValueProviderEntries_ReturnsBinder()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int[])),
+ ModelName = "foo",
+ ValueProvider = new SimpleValueProvider
+ {
+ { "foo[0]", "42" },
+ }
+ };
+
+ ArrayModelBinderProvider binderProvider = new ArrayModelBinderProvider();
+
+ // Act
+ IExtensibleModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.IsType<ArrayModelBinder<int>>(binder);
+ }
+
+ [Fact]
+ public void GetBinder_ModelMetadataReturnsReadOnly_ReturnsNull()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int[])),
+ ModelName = "foo",
+ ValueProvider = new SimpleValueProvider
+ {
+ { "foo[0]", "42" },
+ }
+ };
+ bindingContext.ModelMetadata.IsReadOnly = true;
+
+ ArrayModelBinderProvider binderProvider = new ArrayModelBinderProvider();
+
+ // Act
+ IExtensibleModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(binder);
+ }
+
+ [Fact]
+ public void GetBinder_ModelTypeIsIncorrect_ReturnsNull()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(ICollection<int>)),
+ ModelName = "foo",
+ ValueProvider = new SimpleValueProvider
+ {
+ { "foo[0]", "42" },
+ }
+ };
+
+ ArrayModelBinderProvider binderProvider = new ArrayModelBinderProvider();
+
+ // Act
+ IExtensibleModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(binder);
+ }
+
+ [Fact]
+ public void GetBinder_ValueProviderDoesNotContainPrefix_ReturnsNull()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int[])),
+ ModelName = "foo",
+ ValueProvider = new SimpleValueProvider()
+ };
+
+ ArrayModelBinderProvider binderProvider = new ArrayModelBinderProvider();
+
+ // Act
+ IExtensibleModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(binder);
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ArrayModelBinderTest.cs b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ArrayModelBinderTest.cs
new file mode 100644
index 00000000..0a9229fd
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ArrayModelBinderTest.cs
@@ -0,0 +1,48 @@
+using System.Web.Mvc;
+using Microsoft.Web.UnitTestUtil;
+using Moq;
+using Xunit;
+
+namespace Microsoft.Web.Mvc.ModelBinding.Test
+{
+ public class ArrayModelBinderTest
+ {
+ [Fact]
+ public void BindModel()
+ {
+ // Arrange
+ ControllerContext controllerContext = new ControllerContext();
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int[])),
+ ModelName = "someName",
+ ModelBinderProviders = new ModelBinderProviderCollection(),
+ ValueProvider = new SimpleValueProvider
+ {
+ { "someName[0]", "42" },
+ { "someName[1]", "84" }
+ }
+ };
+
+ Mock<IExtensibleModelBinder> mockIntBinder = new Mock<IExtensibleModelBinder>();
+ mockIntBinder
+ .Setup(o => o.BindModel(controllerContext, It.IsAny<ExtensibleModelBindingContext>()))
+ .Returns(
+ delegate(ControllerContext cc, ExtensibleModelBindingContext mbc)
+ {
+ mbc.Model = mbc.ValueProvider.GetValue(mbc.ModelName).ConvertTo(mbc.ModelType);
+ return true;
+ });
+ bindingContext.ModelBinderProviders.RegisterBinderForType(typeof(int), mockIntBinder.Object, false /* suppressPrefixCheck */);
+
+ // Act
+ bool retVal = new ArrayModelBinder<int>().BindModel(controllerContext, bindingContext);
+
+ // Assert
+ Assert.True(retVal);
+
+ int[] array = bindingContext.Model as int[];
+ Assert.Equal(new[] { 42, 84 }, array);
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/BinaryDataModelBinderProviderTest.cs b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/BinaryDataModelBinderProviderTest.cs
new file mode 100644
index 00000000..8c1563db
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/BinaryDataModelBinderProviderTest.cs
@@ -0,0 +1,159 @@
+using System.Data.Linq;
+using System.Web.Mvc;
+using Microsoft.Web.UnitTestUtil;
+using Xunit;
+
+namespace Microsoft.Web.Mvc.ModelBinding.Test
+{
+ public class BinaryDataModelBinderProviderTest
+ {
+ private static readonly byte[] _base64Bytes = new byte[] { 0x12, 0x20, 0x34, 0x40 };
+ private const string _base64String = "EiA0QA==";
+
+ [Fact]
+ public void BindModel_BadValue_Fails()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(byte[])),
+ ModelName = "foo",
+ ValueProvider = new SimpleValueProvider
+ {
+ { "foo", "not base64 encoded!" }
+ }
+ };
+
+ BinaryDataModelBinderProvider binderProvider = new BinaryDataModelBinderProvider();
+
+ // Act
+ IExtensibleModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+ bool retVal = binder.BindModel(null, bindingContext);
+
+ // Assert
+ Assert.False(retVal);
+ }
+
+ [Fact]
+ public void BindModel_EmptyValue_Fails()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(byte[])),
+ ModelName = "foo",
+ ValueProvider = new SimpleValueProvider
+ {
+ { "foo", "" }
+ }
+ };
+
+ BinaryDataModelBinderProvider binderProvider = new BinaryDataModelBinderProvider();
+
+ // Act
+ IExtensibleModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+ bool retVal = binder.BindModel(null, bindingContext);
+
+ // Assert
+ Assert.False(retVal);
+ }
+
+ [Fact]
+ public void BindModel_GoodValue_ByteArray_Succeeds()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(byte[])),
+ ModelName = "foo",
+ ValueProvider = new SimpleValueProvider
+ {
+ { "foo", _base64String }
+ }
+ };
+
+ BinaryDataModelBinderProvider binderProvider = new BinaryDataModelBinderProvider();
+
+ // Act
+ IExtensibleModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+ bool retVal = binder.BindModel(null, bindingContext);
+
+ // Assert
+ Assert.True(retVal);
+ Assert.Equal(_base64Bytes, (byte[])bindingContext.Model);
+ }
+
+ [Fact]
+ public void BindModel_GoodValue_LinqBinary_Succeeds()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(Binary)),
+ ModelName = "foo",
+ ValueProvider = new SimpleValueProvider
+ {
+ { "foo", _base64String }
+ }
+ };
+
+ BinaryDataModelBinderProvider binderProvider = new BinaryDataModelBinderProvider();
+
+ // Act
+ IExtensibleModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+ bool retVal = binder.BindModel(null, bindingContext);
+
+ // Assert
+ Assert.True(retVal);
+ Binary binaryModel = Assert.IsType<Binary>(bindingContext.Model);
+ Assert.Equal(_base64Bytes, binaryModel.ToArray());
+ }
+
+ [Fact]
+ public void BindModel_NoValue_Fails()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(byte[])),
+ ModelName = "foo",
+ ValueProvider = new SimpleValueProvider
+ {
+ { "foo.bar", _base64String }
+ }
+ };
+
+ BinaryDataModelBinderProvider binderProvider = new BinaryDataModelBinderProvider();
+
+ // Act
+ IExtensibleModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+ bool retVal = binder.BindModel(null, bindingContext);
+
+ // Assert
+ Assert.False(retVal);
+ }
+
+ [Fact]
+ public void GetBinder_WrongModelType_ReturnsNull()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(object)),
+ ModelName = "foo",
+ ValueProvider = new SimpleValueProvider
+ {
+ { "foo", _base64String }
+ }
+ };
+
+ BinaryDataModelBinderProvider binderProvider = new BinaryDataModelBinderProvider();
+
+ // Act
+ IExtensibleModelBinder modelBinder = binderProvider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(modelBinder);
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/BindingBehaviorAttributeTest.cs b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/BindingBehaviorAttributeTest.cs
new file mode 100644
index 00000000..7f7de8cc
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/BindingBehaviorAttributeTest.cs
@@ -0,0 +1,51 @@
+using Xunit;
+
+namespace Microsoft.Web.Mvc.ModelBinding.Test
+{
+ public class BindingBehaviorAttributeTest
+ {
+ [Fact]
+ public void Behavior_Property()
+ {
+ // Arrange
+ BindingBehavior expectedBehavior = (BindingBehavior)(-20);
+
+ // Act
+ BindingBehaviorAttribute attr = new BindingBehaviorAttribute(expectedBehavior);
+
+ // Assert
+ Assert.Equal(expectedBehavior, attr.Behavior);
+ }
+
+ [Fact]
+ public void TypeId_ReturnsSameValue()
+ {
+ // Arrange
+ BindNeverAttribute neverAttr = new BindNeverAttribute();
+ BindRequiredAttribute requiredAttr = new BindRequiredAttribute();
+
+ // Act & assert
+ Assert.Same(neverAttr.TypeId, requiredAttr.TypeId);
+ }
+
+ [Fact]
+ public void BindNever_SetsBehavior()
+ {
+ // Act
+ BindingBehaviorAttribute attr = new BindNeverAttribute();
+
+ // Assert
+ Assert.Equal(BindingBehavior.Never, attr.Behavior);
+ }
+
+ [Fact]
+ public void BindRequired_SetsBehavior()
+ {
+ // Act
+ BindingBehaviorAttribute attr = new BindRequiredAttribute();
+
+ // Assert
+ Assert.Equal(BindingBehavior.Required, attr.Behavior);
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/CollectionModelBinderProviderTest.cs b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/CollectionModelBinderProviderTest.cs
new file mode 100644
index 00000000..db156ac1
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/CollectionModelBinderProviderTest.cs
@@ -0,0 +1,76 @@
+using System.Collections.Generic;
+using System.Web.Mvc;
+using Microsoft.Web.UnitTestUtil;
+using Xunit;
+
+namespace Microsoft.Web.Mvc.ModelBinding.Test
+{
+ public class CollectionModelBinderProviderTest
+ {
+ [Fact]
+ public void GetBinder_CorrectModelTypeAndValueProviderEntries_ReturnsBinder()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(IEnumerable<int>)),
+ ModelName = "foo",
+ ValueProvider = new SimpleValueProvider
+ {
+ { "foo[0]", "42" },
+ }
+ };
+
+ CollectionModelBinderProvider binderProvider = new CollectionModelBinderProvider();
+
+ // Act
+ IExtensibleModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.IsType<CollectionModelBinder<int>>(binder);
+ }
+
+ [Fact]
+ public void GetBinder_ModelTypeIsIncorrect_ReturnsNull()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int)),
+ ModelName = "foo",
+ ValueProvider = new SimpleValueProvider
+ {
+ { "foo[0]", "42" },
+ }
+ };
+
+ CollectionModelBinderProvider binderProvider = new CollectionModelBinderProvider();
+
+ // Act
+ IExtensibleModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(binder);
+ }
+
+ [Fact]
+ public void GetBinder_ValueProviderDoesNotContainPrefix_ReturnsNull()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(IEnumerable<int>)),
+ ModelName = "foo",
+ ValueProvider = new SimpleValueProvider()
+ };
+
+ CollectionModelBinderProvider binderProvider = new CollectionModelBinderProvider();
+
+ // Act
+ IExtensibleModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(binder);
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/CollectionModelBinderTest.cs b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/CollectionModelBinderTest.cs
new file mode 100644
index 00000000..238ac466
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/CollectionModelBinderTest.cs
@@ -0,0 +1,245 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Web.Mvc;
+using Microsoft.Web.UnitTestUtil;
+using Moq;
+using Xunit;
+
+namespace Microsoft.Web.Mvc.ModelBinding.Test
+{
+ public class CollectionModelBinderTest
+ {
+ [Fact]
+ public void BindComplexCollectionFromIndexes_FiniteIndexes()
+ {
+ // Arrange
+ ControllerContext controllerContext = new ControllerContext();
+ CultureInfo culture = CultureInfo.GetCultureInfo("fr-FR");
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int)),
+ ModelName = "someName",
+ ModelBinderProviders = new ModelBinderProviderCollection(),
+ ValueProvider = new SimpleValueProvider
+ {
+ { "someName[foo]", "42" },
+ { "someName[baz]", "200" }
+ }
+ };
+
+ Mock<IExtensibleModelBinder> mockIntBinder = new Mock<IExtensibleModelBinder>();
+ mockIntBinder
+ .Setup(o => o.BindModel(controllerContext, It.IsAny<ExtensibleModelBindingContext>()))
+ .Returns(
+ delegate(ControllerContext cc, ExtensibleModelBindingContext mbc)
+ {
+ mbc.Model = mbc.ValueProvider.GetValue(mbc.ModelName).ConvertTo(mbc.ModelType);
+ return true;
+ });
+ bindingContext.ModelBinderProviders.RegisterBinderForType(typeof(int), mockIntBinder.Object, false /* suppressPrefixCheck */);
+
+ // Act
+ List<int> boundCollection = CollectionModelBinder<int>.BindComplexCollectionFromIndexes(controllerContext, bindingContext, new[] { "foo", "bar", "baz" });
+
+ // Assert
+ Assert.Equal(new[] { 42, 0, 200 }, boundCollection.ToArray());
+ Assert.Equal(new[] { "someName[foo]", "someName[baz]" }, bindingContext.ValidationNode.ChildNodes.Select(o => o.ModelStateKey).ToArray());
+ }
+
+ [Fact]
+ public void BindComplexCollectionFromIndexes_InfiniteIndexes()
+ {
+ // Arrange
+ ControllerContext controllerContext = new ControllerContext();
+ CultureInfo culture = CultureInfo.GetCultureInfo("fr-FR");
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int)),
+ ModelName = "someName",
+ ModelBinderProviders = new ModelBinderProviderCollection(),
+ ValueProvider = new SimpleValueProvider
+ {
+ { "someName[0]", "42" },
+ { "someName[1]", "100" },
+ { "someName[3]", "400" }
+ }
+ };
+
+ Mock<IExtensibleModelBinder> mockIntBinder = new Mock<IExtensibleModelBinder>();
+ mockIntBinder
+ .Setup(o => o.BindModel(controllerContext, It.IsAny<ExtensibleModelBindingContext>()))
+ .Returns(
+ delegate(ControllerContext cc, ExtensibleModelBindingContext mbc)
+ {
+ mbc.Model = mbc.ValueProvider.GetValue(mbc.ModelName).ConvertTo(mbc.ModelType);
+ return true;
+ });
+ bindingContext.ModelBinderProviders.RegisterBinderForType(typeof(int), mockIntBinder.Object, false /* suppressPrefixCheck */);
+
+ // Act
+ List<int> boundCollection = CollectionModelBinder<int>.BindComplexCollectionFromIndexes(controllerContext, bindingContext, null /* indexNames */);
+
+ // Assert
+ Assert.Equal(new[] { 42, 100 }, boundCollection.ToArray());
+ Assert.Equal(new[] { "someName[0]", "someName[1]" }, bindingContext.ValidationNode.ChildNodes.Select(o => o.ModelStateKey).ToArray());
+ }
+
+ [Fact]
+ public void BindModel_ComplexCollection()
+ {
+ // Arrange
+ ControllerContext controllerContext = new ControllerContext();
+ CultureInfo culture = CultureInfo.GetCultureInfo("fr-FR");
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int)),
+ ModelName = "someName",
+ ModelBinderProviders = new ModelBinderProviderCollection(),
+ ValueProvider = new SimpleValueProvider
+ {
+ { "someName.index", new[] { "foo", "bar", "baz" } },
+ { "someName[foo]", "42" },
+ { "someName[bar]", "100" },
+ { "someName[baz]", "200" }
+ }
+ };
+
+ Mock<IExtensibleModelBinder> mockIntBinder = new Mock<IExtensibleModelBinder>();
+ mockIntBinder
+ .Setup(o => o.BindModel(controllerContext, It.IsAny<ExtensibleModelBindingContext>()))
+ .Returns(
+ delegate(ControllerContext cc, ExtensibleModelBindingContext mbc)
+ {
+ mbc.Model = mbc.ValueProvider.GetValue(mbc.ModelName).ConvertTo(mbc.ModelType);
+ return true;
+ });
+ bindingContext.ModelBinderProviders.RegisterBinderForType(typeof(int), mockIntBinder.Object, true /* suppressPrefixCheck */);
+
+ CollectionModelBinder<int> modelBinder = new CollectionModelBinder<int>();
+
+ // Act
+ bool retVal = modelBinder.BindModel(controllerContext, bindingContext);
+
+ // Assert
+ Assert.Equal(new[] { 42, 100, 200 }, ((List<int>)bindingContext.Model).ToArray());
+ }
+
+ [Fact]
+ public void BindModel_SimpleCollection()
+ {
+ // Arrange
+ ControllerContext controllerContext = new ControllerContext();
+ CultureInfo culture = CultureInfo.GetCultureInfo("fr-FR");
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int)),
+ ModelName = "someName",
+ ModelBinderProviders = new ModelBinderProviderCollection(),
+ ValueProvider = new SimpleValueProvider
+ {
+ { "someName", new[] { "42", "100", "200" } }
+ }
+ };
+
+ Mock<IExtensibleModelBinder> mockIntBinder = new Mock<IExtensibleModelBinder>();
+ mockIntBinder
+ .Setup(o => o.BindModel(controllerContext, It.IsAny<ExtensibleModelBindingContext>()))
+ .Returns(
+ delegate(ControllerContext cc, ExtensibleModelBindingContext mbc)
+ {
+ mbc.Model = mbc.ValueProvider.GetValue(mbc.ModelName).ConvertTo(mbc.ModelType);
+ return true;
+ });
+ bindingContext.ModelBinderProviders.RegisterBinderForType(typeof(int), mockIntBinder.Object, true /* suppressPrefixCheck */);
+
+ CollectionModelBinder<int> modelBinder = new CollectionModelBinder<int>();
+
+ // Act
+ bool retVal = modelBinder.BindModel(controllerContext, bindingContext);
+
+ // Assert
+ Assert.True(retVal);
+ Assert.Equal(new[] { 42, 100, 200 }, ((List<int>)bindingContext.Model).ToArray());
+ }
+
+ [Fact]
+ public void BindSimpleCollection_RawValueIsEmptyCollection_ReturnsEmptyList()
+ {
+ // Act
+ List<int> boundCollection = CollectionModelBinder<int>.BindSimpleCollection(null, null, new object[0], null);
+
+ // Assert
+ Assert.NotNull(boundCollection);
+ Assert.Empty(boundCollection);
+ }
+
+ [Fact]
+ public void BindSimpleCollection_RawValueIsNull_ReturnsNull()
+ {
+ // Act
+ List<int> boundCollection = CollectionModelBinder<int>.BindSimpleCollection(null, null, null, null);
+
+ // Assert
+ Assert.Null(boundCollection);
+ }
+
+ [Fact]
+ public void BindSimpleCollection_SubBinderDoesNotExist()
+ {
+ // Arrange
+ ControllerContext controllerContext = new ControllerContext();
+ CultureInfo culture = CultureInfo.GetCultureInfo("fr-FR");
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int)),
+ ModelName = "someName",
+ ModelBinderProviders = new ModelBinderProviderCollection(),
+ ValueProvider = new SimpleValueProvider()
+ };
+
+ // Act
+ List<int> boundCollection = CollectionModelBinder<int>.BindSimpleCollection(controllerContext, bindingContext, new int[1], culture);
+
+ // Assert
+ Assert.Equal(new[] { 0 }, boundCollection.ToArray());
+ Assert.Empty(bindingContext.ValidationNode.ChildNodes);
+ }
+
+ [Fact]
+ public void BindSimpleCollection_SubBindingSucceeds()
+ {
+ // Arrange
+ ControllerContext controllerContext = new ControllerContext();
+ CultureInfo culture = CultureInfo.GetCultureInfo("fr-FR");
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int)),
+ ModelName = "someName",
+ ModelBinderProviders = new ModelBinderProviderCollection(),
+ ValueProvider = new SimpleValueProvider()
+ };
+
+ ModelValidationNode childValidationNode = null;
+ Mock<IExtensibleModelBinder> mockIntBinder = new Mock<IExtensibleModelBinder>();
+ mockIntBinder
+ .Setup(o => o.BindModel(controllerContext, It.IsAny<ExtensibleModelBindingContext>()))
+ .Returns(
+ delegate(ControllerContext cc, ExtensibleModelBindingContext mbc)
+ {
+ Assert.Equal("someName", mbc.ModelName);
+ childValidationNode = mbc.ValidationNode;
+ mbc.Model = 42;
+ return true;
+ });
+ bindingContext.ModelBinderProviders.RegisterBinderForType(typeof(int), mockIntBinder.Object, true /* suppressPrefixCheck */);
+
+ // Act
+ List<int> boundCollection = CollectionModelBinder<int>.BindSimpleCollection(controllerContext, bindingContext, new int[1], culture);
+
+ // Assert
+ Assert.Equal(new[] { 42 }, boundCollection.ToArray());
+ Assert.Equal(new[] { childValidationNode }, bindingContext.ValidationNode.ChildNodes.ToArray());
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/CollectionModelBinderUtilTest.cs b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/CollectionModelBinderUtilTest.cs
new file mode 100644
index 00000000..073508ae
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/CollectionModelBinderUtilTest.cs
@@ -0,0 +1,391 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Web.Mvc;
+using Xunit;
+
+namespace Microsoft.Web.Mvc.ModelBinding.Test
+{
+ public class CollectionModelBinderUtilTest
+ {
+ [Fact]
+ public void CreateOrReplaceCollection_OriginalModelImmutable_CreatesNewInstance()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => new ReadOnlyCollection<int>(new int[0]), typeof(ICollection<int>))
+ };
+
+ // Act
+ CollectionModelBinderUtil.CreateOrReplaceCollection(bindingContext, new[] { 10, 20, 30 }, () => new List<int>());
+
+ // Assert
+ int[] newModel = (bindingContext.Model as ICollection<int>).ToArray();
+ Assert.Equal(new[] { 10, 20, 30 }, newModel);
+ }
+
+ [Fact]
+ public void CreateOrReplaceCollection_OriginalModelMutable_UpdatesOriginalInstance()
+ {
+ // Arrange
+ List<int> originalInstance = new List<int> { 10, 20, 30 };
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => originalInstance, typeof(ICollection<int>))
+ };
+
+ // Act
+ CollectionModelBinderUtil.CreateOrReplaceCollection(bindingContext, new[] { 40, 50, 60 }, () => new List<int>());
+
+ // Assert
+ Assert.Same(originalInstance, bindingContext.Model);
+ Assert.Equal(new[] { 40, 50, 60 }, originalInstance.ToArray());
+ }
+
+ [Fact]
+ public void CreateOrReplaceCollection_OriginalModelNotCollection_CreatesNewInstance()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(ICollection<int>))
+ };
+
+ // Act
+ CollectionModelBinderUtil.CreateOrReplaceCollection(bindingContext, new[] { 10, 20, 30 }, () => new List<int>());
+
+ // Assert
+ int[] newModel = (bindingContext.Model as ICollection<int>).ToArray();
+ Assert.Equal(new[] { 10, 20, 30 }, newModel);
+ }
+
+ [Fact]
+ public void CreateOrReplaceDictionary_DisallowsDuplicateKeys()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(Dictionary<string, int>))
+ };
+
+ // Act
+ CollectionModelBinderUtil.CreateOrReplaceDictionary(
+ bindingContext,
+ new[]
+ {
+ new KeyValuePair<string, int>("forty-two", 40),
+ new KeyValuePair<string, int>("forty-two", 2),
+ new KeyValuePair<string, int>("forty-two", 42)
+ },
+ () => new Dictionary<string, int>());
+
+ // Assert
+ IDictionary<string, int> newModel = bindingContext.Model as IDictionary<string, int>;
+ Assert.Equal(new[] { "forty-two" }, newModel.Keys.ToArray());
+ Assert.Equal(42, newModel["forty-two"]);
+ }
+
+ [Fact]
+ public void CreateOrReplaceDictionary_DisallowsNullKeys()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(Dictionary<string, int>))
+ };
+
+ // Act
+ CollectionModelBinderUtil.CreateOrReplaceDictionary(
+ bindingContext,
+ new[]
+ {
+ new KeyValuePair<string, int>("forty-two", 42),
+ new KeyValuePair<string, int>(null, 84)
+ },
+ () => new Dictionary<string, int>());
+
+ // Assert
+ IDictionary<string, int> newModel = bindingContext.Model as IDictionary<string, int>;
+ Assert.Equal(new[] { "forty-two" }, newModel.Keys.ToArray());
+ Assert.Equal(42, newModel["forty-two"]);
+ }
+
+ [Fact]
+ public void CreateOrReplaceDictionary_OriginalModelImmutable_CreatesNewInstance()
+ {
+ // Arrange
+ ReadOnlyDictionary<string, string> originalModel = new ReadOnlyDictionary<string, string>();
+
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => originalModel, typeof(IDictionary<string, string>))
+ };
+
+ // Act
+ CollectionModelBinderUtil.CreateOrReplaceDictionary(
+ bindingContext,
+ new Dictionary<string, string>
+ {
+ { "Hello", "World" }
+ },
+ () => new Dictionary<string, string>());
+
+ // Assert
+ IDictionary<string, string> newModel = bindingContext.Model as IDictionary<string, string>;
+ Assert.NotSame(originalModel, newModel);
+ Assert.Equal(new[] { "Hello" }, newModel.Keys.ToArray());
+ Assert.Equal("World", newModel["Hello"]);
+ }
+
+ [Fact]
+ public void CreateOrReplaceDictionary_OriginalModelMutable_UpdatesOriginalInstance()
+ {
+ // Arrange
+ Dictionary<string, string> originalInstance = new Dictionary<string, string>
+ {
+ { "dog", "Canidae" },
+ { "cat", "Felidae" }
+ };
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => originalInstance, typeof(IDictionary<string, string>))
+ };
+
+ // Act
+ CollectionModelBinderUtil.CreateOrReplaceDictionary(
+ bindingContext,
+ new Dictionary<string, string>
+ {
+ { "horse", "Equidae" },
+ { "bear", "Ursidae" }
+ },
+ () => new Dictionary<string, string>());
+
+ // Assert
+ Assert.Same(originalInstance, bindingContext.Model);
+ Assert.Equal(new[] { "horse", "bear" }, originalInstance.Keys.ToArray());
+ Assert.Equal("Equidae", originalInstance["horse"]);
+ Assert.Equal("Ursidae", originalInstance["bear"]);
+ }
+
+ [Fact]
+ public void CreateOrReplaceDictionary_OriginalModelNotDictionary_CreatesNewInstance()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(IDictionary<string, string>))
+ };
+
+ // Act
+ CollectionModelBinderUtil.CreateOrReplaceDictionary(
+ bindingContext,
+ new Dictionary<string, string>
+ {
+ { "horse", "Equidae" },
+ { "bear", "Ursidae" }
+ },
+ () => new Dictionary<string, string>());
+
+ // Assert
+ IDictionary<string, string> newModel = bindingContext.Model as IDictionary<string, string>;
+ Assert.Equal(new[] { "horse", "bear" }, newModel.Keys.ToArray());
+ Assert.Equal("Equidae", newModel["horse"]);
+ Assert.Equal("Ursidae", newModel["bear"]);
+ }
+
+ [Fact]
+ public void GetIndexNamesFromValueProviderResult_ValueProviderResultIsNull_ReturnsNull()
+ {
+ // Act
+ IEnumerable<string> indexNames = CollectionModelBinderUtil.GetIndexNamesFromValueProviderResult(null);
+
+ // Assert
+ Assert.Null(indexNames);
+ }
+
+ [Fact]
+ public void GetIndexNamesFromValueProviderResult_ValueProviderResultReturnsEmptyArray_ReturnsNull()
+ {
+ // Arrange
+ ValueProviderResult vpResult = new ValueProviderResult(new string[0], "", null);
+
+ // Act
+ IEnumerable<string> indexNames = CollectionModelBinderUtil.GetIndexNamesFromValueProviderResult(vpResult);
+
+ // Assert
+ Assert.Null(indexNames);
+ }
+
+ [Fact]
+ public void GetIndexNamesFromValueProviderResult_ValueProviderResultReturnsNonEmptyArray_ReturnsArray()
+ {
+ // Arrange
+ ValueProviderResult vpResult = new ValueProviderResult(new[] { "foo", "bar", "baz" }, "foo,bar,baz", null);
+
+ // Act
+ IEnumerable<string> indexNames = CollectionModelBinderUtil.GetIndexNamesFromValueProviderResult(vpResult);
+
+ // Assert
+ Assert.NotNull(indexNames);
+ Assert.Equal(new[] { "foo", "bar", "baz" }, indexNames.ToArray());
+ }
+
+ [Fact]
+ public void GetIndexNamesFromValueProviderResult_ValueProviderResultReturnsNull_ReturnsNull()
+ {
+ // Arrange
+ ValueProviderResult vpResult = new ValueProviderResult(null, null, null);
+
+ // Act
+ IEnumerable<string> indexNames = CollectionModelBinderUtil.GetIndexNamesFromValueProviderResult(vpResult);
+
+ // Assert
+ Assert.Null(indexNames);
+ }
+
+ [Fact]
+ public void GetTypeArgumentsForUpdatableGenericCollection_ModelTypeNotGeneric_Fail()
+ {
+ // Arrange
+ ModelMetadata modelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int));
+
+ // Act
+ Type[] typeArguments = CollectionModelBinderUtil.GetTypeArgumentsForUpdatableGenericCollection(null, null, modelMetadata);
+
+ // Assert
+ Assert.Null(typeArguments);
+ }
+
+ [Fact]
+ public void GetTypeArgumentsForUpdatableGenericCollection_ModelTypeOpenGeneric_Fail()
+ {
+ // Arrange
+ ModelMetadata modelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(IList<>));
+
+ // Act
+ Type[] typeArguments = CollectionModelBinderUtil.GetTypeArgumentsForUpdatableGenericCollection(null, null, modelMetadata);
+
+ // Assert
+ Assert.Null(typeArguments);
+ }
+
+ [Fact]
+ public void GetTypeArgumentsForUpdatableGenericCollection_ModelTypeWrongNumberOfGenericArguments_Fail()
+ {
+ // Arrange
+ ModelMetadata modelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(KeyValuePair<int, string>));
+
+ // Act
+ Type[] typeArguments = CollectionModelBinderUtil.GetTypeArgumentsForUpdatableGenericCollection(typeof(ICollection<>), null, modelMetadata);
+
+ // Assert
+ Assert.Null(typeArguments);
+ }
+
+ [Fact]
+ public void GetTypeArgumentsForUpdatableGenericCollection_ReadOnlyReference_ModelInstanceImmutable_Valid()
+ {
+ // Arrange
+ ModelMetadata modelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => new int[0], typeof(IList<int>));
+ modelMetadata.IsReadOnly = true;
+
+ // Act
+ Type[] typeArguments = CollectionModelBinderUtil.GetTypeArgumentsForUpdatableGenericCollection(typeof(IList<>), typeof(List<>), modelMetadata);
+
+ // Assert
+ Assert.Null(typeArguments);
+ }
+
+ [Fact]
+ public void GetTypeArgumentsForUpdatableGenericCollection_ReadOnlyReference_ModelInstanceMutable_Valid()
+ {
+ // Arrange
+ ModelMetadata modelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => new List<int>(), typeof(IList<int>));
+ modelMetadata.IsReadOnly = true;
+
+ // Act
+ Type[] typeArguments = CollectionModelBinderUtil.GetTypeArgumentsForUpdatableGenericCollection(typeof(IList<>), typeof(List<>), modelMetadata);
+
+ // Assert
+ Assert.Equal(new[] { typeof(int) }, typeArguments);
+ }
+
+ [Fact]
+ public void GetTypeArgumentsForUpdatableGenericCollection_ReadOnlyReference_ModelInstanceOfWrongType_Fail()
+ {
+ // Arrange
+ ModelMetadata modelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => new HashSet<int>(), typeof(ICollection<int>));
+ modelMetadata.IsReadOnly = true;
+
+ // Act
+ Type[] typeArguments = CollectionModelBinderUtil.GetTypeArgumentsForUpdatableGenericCollection(typeof(IList<>), typeof(List<>), modelMetadata);
+
+ // Assert
+ // HashSet<> is not an IList<>, so we can't update
+ Assert.Null(typeArguments);
+ }
+
+ [Fact]
+ public void GetTypeArgumentsForUpdatableGenericCollection_ReadOnlyReference_ModelIsNull_Fail()
+ {
+ // Arrange
+ ModelMetadata modelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(IList<int>));
+ modelMetadata.IsReadOnly = true;
+
+ // Act
+ Type[] typeArguments = CollectionModelBinderUtil.GetTypeArgumentsForUpdatableGenericCollection(typeof(ICollection<>), typeof(List<>), modelMetadata);
+
+ // Assert
+ Assert.Null(typeArguments);
+ }
+
+ [Fact]
+ public void GetTypeArgumentsForUpdatableGenericCollection_ReadWriteReference_NewInstanceAssignableToModelType_Success()
+ {
+ // Arrange
+ ModelMetadata modelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(IList<int>));
+ modelMetadata.IsReadOnly = false;
+
+ // Act
+ Type[] typeArguments = CollectionModelBinderUtil.GetTypeArgumentsForUpdatableGenericCollection(typeof(ICollection<>), typeof(List<>), modelMetadata);
+
+ // Assert
+ Assert.Equal(new[] { typeof(int) }, typeArguments);
+ }
+
+ [Fact]
+ public void GetTypeArgumentsForUpdatableGenericCollection_ReadWriteReference_NewInstanceNotAssignableToModelType_MutableInstance_Success()
+ {
+ // Arrange
+ ModelMetadata modelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => new Collection<int>(), typeof(Collection<int>));
+ modelMetadata.IsReadOnly = false;
+
+ // Act
+ Type[] typeArguments = CollectionModelBinderUtil.GetTypeArgumentsForUpdatableGenericCollection(typeof(ICollection<>), typeof(List<>), modelMetadata);
+
+ // Assert
+ Assert.Equal(new[] { typeof(int) }, typeArguments);
+ }
+
+ [Fact]
+ public void GetZeroBasedIndexes()
+ {
+ // Act
+ string[] indexes = CollectionModelBinderUtil.GetZeroBasedIndexes().Take(5).ToArray();
+
+ // Assert
+ Assert.Equal(new[] { "0", "1", "2", "3", "4" }, indexes);
+ }
+
+ private class ReadOnlyDictionary<TKey, TValue> : Dictionary<TKey, TValue>, ICollection<KeyValuePair<TKey, TValue>>
+ {
+ bool ICollection<KeyValuePair<TKey, TValue>>.IsReadOnly
+ {
+ get { return true; }
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ComplexModelDtoModelBinderProviderTest.cs b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ComplexModelDtoModelBinderProviderTest.cs
new file mode 100644
index 00000000..0b80d699
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ComplexModelDtoModelBinderProviderTest.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Web.Mvc;
+using Xunit;
+
+namespace Microsoft.Web.Mvc.ModelBinding.Test
+{
+ public class ComplexModelDtoModelBinderProviderTest
+ {
+ [Fact]
+ public void GetBinder_TypeDoesNotMatch_ReturnsNull()
+ {
+ // Arrange
+ ComplexModelDtoModelBinderProvider provider = new ComplexModelDtoModelBinderProvider();
+ ExtensibleModelBindingContext bindingContext = GetBindingContext(typeof(object));
+
+ // Act
+ IExtensibleModelBinder binder = provider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(binder);
+ }
+
+ [Fact]
+ public void GetBinder_TypeMatches_ReturnsBinder()
+ {
+ // Arrange
+ ComplexModelDtoModelBinderProvider provider = new ComplexModelDtoModelBinderProvider();
+ ExtensibleModelBindingContext bindingContext = GetBindingContext(typeof(ComplexModelDto));
+
+ // Act
+ IExtensibleModelBinder binder = provider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.IsType<ComplexModelDtoModelBinder>(binder);
+ }
+
+ private static ExtensibleModelBindingContext GetBindingContext(Type modelType)
+ {
+ return new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => null, modelType)
+ };
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ComplexModelDtoModelBinderTest.cs b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ComplexModelDtoModelBinderTest.cs
new file mode 100644
index 00000000..74bb8174
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ComplexModelDtoModelBinderTest.cs
@@ -0,0 +1,106 @@
+using System;
+using System.Linq;
+using System.Web.Mvc;
+using Moq;
+using Xunit;
+
+namespace Microsoft.Web.Mvc.ModelBinding.Test
+{
+ public class ComplexModelDtoModelBinderTest
+ {
+ [Fact]
+ public void BindModel()
+ {
+ // Arrange
+ ControllerContext controllerContext = new ControllerContext();
+ MyModel model = new MyModel();
+ ModelMetadata modelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => model, typeof(MyModel));
+ ComplexModelDto dto = new ComplexModelDto(modelMetadata, modelMetadata.Properties);
+
+ Mock<IExtensibleModelBinder> mockStringBinder = new Mock<IExtensibleModelBinder>();
+ mockStringBinder
+ .Setup(b => b.BindModel(controllerContext, It.IsAny<ExtensibleModelBindingContext>()))
+ .Returns(
+ delegate(ControllerContext cc, ExtensibleModelBindingContext mbc)
+ {
+ Assert.Equal(typeof(string), mbc.ModelType);
+ Assert.Equal("theModel.StringProperty", mbc.ModelName);
+ mbc.ValidationNode = new ModelValidationNode(mbc.ModelMetadata, "theModel.StringProperty");
+ mbc.Model = "someStringValue";
+ return true;
+ });
+
+ Mock<IExtensibleModelBinder> mockIntBinder = new Mock<IExtensibleModelBinder>();
+ mockIntBinder
+ .Setup(b => b.BindModel(controllerContext, It.IsAny<ExtensibleModelBindingContext>()))
+ .Returns(
+ delegate(ControllerContext cc, ExtensibleModelBindingContext mbc)
+ {
+ Assert.Equal(typeof(int), mbc.ModelType);
+ Assert.Equal("theModel.IntProperty", mbc.ModelName);
+ mbc.ValidationNode = new ModelValidationNode(mbc.ModelMetadata, "theModel.IntProperty");
+ mbc.Model = 42;
+ return true;
+ });
+
+ Mock<IExtensibleModelBinder> mockDateTimeBinder = new Mock<IExtensibleModelBinder>();
+ mockDateTimeBinder
+ .Setup(b => b.BindModel(controllerContext, It.IsAny<ExtensibleModelBindingContext>()))
+ .Returns(
+ delegate(ControllerContext cc, ExtensibleModelBindingContext mbc)
+ {
+ Assert.Equal(typeof(DateTime), mbc.ModelType);
+ Assert.Equal("theModel.DateTimeProperty", mbc.ModelName);
+ return false;
+ });
+
+ ModelBinderProviderCollection binders = new ModelBinderProviderCollection();
+ binders.RegisterBinderForType(typeof(string), mockStringBinder.Object, true /* suppressPrefixCheck */);
+ binders.RegisterBinderForType(typeof(int), mockIntBinder.Object, true /* suppressPrefixCheck */);
+ binders.RegisterBinderForType(typeof(DateTime), mockDateTimeBinder.Object, true /* suppressPrefixCheck */);
+
+ ExtensibleModelBindingContext parentBindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => dto, typeof(ComplexModelDto)),
+ ModelName = "theModel",
+ ModelBinderProviders = binders
+ };
+
+ ComplexModelDtoModelBinder binder = new ComplexModelDtoModelBinder();
+
+ // Act
+ bool retVal = binder.BindModel(controllerContext, parentBindingContext);
+
+ // Assert
+ Assert.True(retVal);
+ Assert.Equal(dto, parentBindingContext.Model);
+
+ ComplexModelDtoResult stringDtoResult = dto.Results[dto.PropertyMetadata.Where(m => m.ModelType == typeof(string)).First()];
+ Assert.Equal("someStringValue", stringDtoResult.Model);
+ Assert.Equal("theModel.StringProperty", stringDtoResult.ValidationNode.ModelStateKey);
+
+ ComplexModelDtoResult intDtoResult = dto.Results[dto.PropertyMetadata.Where(m => m.ModelType == typeof(int)).First()];
+ Assert.Equal(42, intDtoResult.Model);
+ Assert.Equal("theModel.IntProperty", intDtoResult.ValidationNode.ModelStateKey);
+
+ ComplexModelDtoResult dateTimeDtoResult = dto.Results[dto.PropertyMetadata.Where(m => m.ModelType == typeof(DateTime)).First()];
+ Assert.Null(dateTimeDtoResult);
+ }
+
+ private static ModelBindingContext GetBindingContext(Type modelType)
+ {
+ return new ModelBindingContext
+ {
+ ModelMetadata = new ModelMetadata(new Mock<ModelMetadataProvider>().Object, null, null, modelType, "SomeProperty")
+ };
+ }
+
+ private sealed class MyModel
+ {
+ public string StringProperty { get; set; }
+ public int IntProperty { get; set; }
+ public object ObjectProperty { get; set; } // no binding should happen since no registered binder
+ public DateTime DateTimeProperty { get; set; } // registered binder returns false
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ComplexModelDtoResultTest.cs b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ComplexModelDtoResultTest.cs
new file mode 100644
index 00000000..ebb45189
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ComplexModelDtoResultTest.cs
@@ -0,0 +1,38 @@
+using System.Web.Mvc;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace Microsoft.Web.Mvc.ModelBinding.Test
+{
+ public class ComplexModelDtoResultTest
+ {
+ [Fact]
+ public void Constructor_ThrowsIfValidationNodeIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new ComplexModelDtoResult("some string", null); }, "validationNode");
+ }
+
+ [Fact]
+ public void Constructor_SetsProperties()
+ {
+ // Arrange
+ ModelValidationNode validationNode = GetValidationNode();
+
+ // Act
+ ComplexModelDtoResult result = new ComplexModelDtoResult("some string", validationNode);
+
+ // Assert
+ Assert.Equal("some string", result.Model);
+ Assert.Equal(validationNode, result.ValidationNode);
+ }
+
+ private static ModelValidationNode GetValidationNode()
+ {
+ EmptyModelMetadataProvider provider = new EmptyModelMetadataProvider();
+ ModelMetadata metadata = provider.GetMetadataForType(null, typeof(object));
+ return new ModelValidationNode(metadata, "someKey");
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ComplexModelDtoTest.cs b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ComplexModelDtoTest.cs
new file mode 100644
index 00000000..a8cfe6b7
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ComplexModelDtoTest.cs
@@ -0,0 +1,50 @@
+using System.Linq;
+using System.Web.Mvc;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace Microsoft.Web.Mvc.ModelBinding.Test
+{
+ public class ComplexModelDtoTest
+ {
+ [Fact]
+ public void ConstructorThrowsIfModelMetadataIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new ComplexModelDto(null, Enumerable.Empty<ModelMetadata>()); }, "modelMetadata");
+ }
+
+ [Fact]
+ public void ConstructorThrowsIfPropertyMetadataIsNull()
+ {
+ // Arrange
+ ModelMetadata modelMetadata = GetModelMetadata();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new ComplexModelDto(modelMetadata, null); }, "propertyMetadata");
+ }
+
+ [Fact]
+ public void ConstructorSetsProperties()
+ {
+ // Arrange
+ ModelMetadata modelMetadata = GetModelMetadata();
+ ModelMetadata[] propertyMetadata = new ModelMetadata[0];
+
+ // Act
+ ComplexModelDto dto = new ComplexModelDto(modelMetadata, propertyMetadata);
+
+ // Assert
+ Assert.Equal(modelMetadata, dto.ModelMetadata);
+ Assert.Equal(propertyMetadata, dto.PropertyMetadata.ToArray());
+ Assert.Empty(dto.Results);
+ }
+
+ private static ModelMetadata GetModelMetadata()
+ {
+ return new ModelMetadata(new EmptyModelMetadataProvider(), typeof(object), null, typeof(object), "PropertyName");
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/DictionaryModelBinderProviderTest.cs b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/DictionaryModelBinderProviderTest.cs
new file mode 100644
index 00000000..d64b2e14
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/DictionaryModelBinderProviderTest.cs
@@ -0,0 +1,76 @@
+using System.Collections.Generic;
+using System.Web.Mvc;
+using Microsoft.Web.UnitTestUtil;
+using Xunit;
+
+namespace Microsoft.Web.Mvc.ModelBinding.Test
+{
+ public class DictionaryModelBinderProviderTest
+ {
+ [Fact]
+ public void GetBinder_CorrectModelTypeAndValueProviderEntries_ReturnsBinder()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(IDictionary<int, string>)),
+ ModelName = "foo",
+ ValueProvider = new SimpleValueProvider
+ {
+ { "foo[0]", "42" },
+ }
+ };
+
+ DictionaryModelBinderProvider binderProvider = new DictionaryModelBinderProvider();
+
+ // Act
+ IExtensibleModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.IsType<DictionaryModelBinder<int, string>>(binder);
+ }
+
+ [Fact]
+ public void GetBinder_ModelTypeIsIncorrect_ReturnsNull()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int)),
+ ModelName = "foo",
+ ValueProvider = new SimpleValueProvider
+ {
+ { "foo[0]", "42" },
+ }
+ };
+
+ DictionaryModelBinderProvider binderProvider = new DictionaryModelBinderProvider();
+
+ // Act
+ IExtensibleModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(binder);
+ }
+
+ [Fact]
+ public void GetBinder_ValueProviderDoesNotContainPrefix_ReturnsNull()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(IDictionary<int, string>)),
+ ModelName = "foo",
+ ValueProvider = new SimpleValueProvider()
+ };
+
+ DictionaryModelBinderProvider binderProvider = new DictionaryModelBinderProvider();
+
+ // Act
+ IExtensibleModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(binder);
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/DictionaryModelBinderTest.cs b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/DictionaryModelBinderTest.cs
new file mode 100644
index 00000000..d9b571ae
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/DictionaryModelBinderTest.cs
@@ -0,0 +1,52 @@
+using System.Collections.Generic;
+using System.Web.Mvc;
+using Microsoft.Web.UnitTestUtil;
+using Moq;
+using Xunit;
+
+namespace Microsoft.Web.Mvc.ModelBinding.Test
+{
+ public class DictionaryModelBinderTest
+ {
+ [Fact]
+ public void BindModel()
+ {
+ // Arrange
+ ControllerContext controllerContext = new ControllerContext();
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(IDictionary<int, string>)),
+ ModelName = "someName",
+ ModelBinderProviders = new ModelBinderProviderCollection(),
+ ValueProvider = new SimpleValueProvider
+ {
+ { "someName[0]", new KeyValuePair<int, string>(42, "forty-two") },
+ { "someName[1]", new KeyValuePair<int, string>(84, "eighty-four") }
+ }
+ };
+
+ Mock<IExtensibleModelBinder> mockKvpBinder = new Mock<IExtensibleModelBinder>();
+ mockKvpBinder
+ .Setup(o => o.BindModel(controllerContext, It.IsAny<ExtensibleModelBindingContext>()))
+ .Returns(
+ delegate(ControllerContext cc, ExtensibleModelBindingContext mbc)
+ {
+ mbc.Model = mbc.ValueProvider.GetValue(mbc.ModelName).ConvertTo(mbc.ModelType);
+ return true;
+ });
+ bindingContext.ModelBinderProviders.RegisterBinderForType(typeof(KeyValuePair<int, string>), mockKvpBinder.Object, false /* suppressPrefixCheck */);
+
+ // Act
+ bool retVal = new DictionaryModelBinder<int, string>().BindModel(controllerContext, bindingContext);
+
+ // Assert
+ Assert.True(retVal);
+
+ var dictionary = Assert.IsAssignableFrom<IDictionary<int, string>>(bindingContext.Model);
+ Assert.NotNull(dictionary);
+ Assert.Equal(2, dictionary.Count);
+ Assert.Equal("forty-two", dictionary[42]);
+ Assert.Equal("eighty-four", dictionary[84]);
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ExtensibleModelBinderAdapterTest.cs b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ExtensibleModelBinderAdapterTest.cs
new file mode 100644
index 00000000..41e0f8c0
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ExtensibleModelBinderAdapterTest.cs
@@ -0,0 +1,210 @@
+using System;
+using System.Collections.Generic;
+using System.Web.Mvc;
+using Microsoft.Web.UnitTestUtil;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace Microsoft.Web.Mvc.ModelBinding.Test
+{
+ public class ExtensibleModelBinderAdapterTest
+ {
+ [Fact]
+ public void BindModel_PropertyFilterIsSet_Throws()
+ {
+ // Arrange
+ ControllerContext controllerContext = GetControllerContext();
+
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ FallbackToEmptyPrefix = true,
+ ModelMetadata = new DataAnnotationsModelMetadataProvider().GetMetadataForType(null, typeof(SimpleModel)),
+ PropertyFilter = (new BindAttribute { Include = "FirstName " }).IsPropertyAllowed
+ };
+
+ ModelBinderProviderCollection binderProviders = new ModelBinderProviderCollection();
+ ExtensibleModelBinderAdapter shimBinder = new ExtensibleModelBinderAdapter(binderProviders);
+
+ // Act & assert
+
+ Assert.Throws<InvalidOperationException>(
+ delegate { shimBinder.BindModel(controllerContext, bindingContext); },
+ @"The new model binding system cannot be used when a property whitelist or blacklist has been specified in [Bind] or via the call to UpdateModel() / TryUpdateModel(). Use the [BindRequired] and [BindNever] attributes on the model type or its properties instead.");
+ }
+
+ [Fact]
+ public void BindModel_SuccessfulBind_RunsValidationAndReturnsModel()
+ {
+ // Arrange
+ ControllerContext controllerContext = GetControllerContext();
+ bool validationCalled = false;
+
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ FallbackToEmptyPrefix = true,
+ ModelMetadata = new DataAnnotationsModelMetadataProvider().GetMetadataForType(null, typeof(int)),
+ ModelName = "someName",
+ ModelState = controllerContext.Controller.ViewData.ModelState,
+ PropertyFilter = _ => true,
+ ValueProvider = new SimpleValueProvider
+ {
+ { "someName", "dummyValue" }
+ }
+ };
+
+ Mock<IExtensibleModelBinder> mockIntBinder = new Mock<IExtensibleModelBinder>();
+ mockIntBinder
+ .Setup(o => o.BindModel(controllerContext, It.IsAny<ExtensibleModelBindingContext>()))
+ .Returns(
+ delegate(ControllerContext cc, ExtensibleModelBindingContext mbc)
+ {
+ Assert.Same(bindingContext.ModelMetadata, mbc.ModelMetadata);
+ Assert.Equal("someName", mbc.ModelName);
+ Assert.Same(bindingContext.ValueProvider, mbc.ValueProvider);
+
+ mbc.Model = 42;
+ mbc.ValidationNode.Validating += delegate { validationCalled = true; };
+ return true;
+ });
+
+ ModelBinderProviderCollection binderProviders = new ModelBinderProviderCollection();
+ binderProviders.RegisterBinderForType(typeof(int), mockIntBinder.Object, false /* suppressPrefixCheck */);
+ ExtensibleModelBinderAdapter shimBinder = new ExtensibleModelBinderAdapter(binderProviders);
+
+ // Act
+ object retVal = shimBinder.BindModel(controllerContext, bindingContext);
+
+ // Assert
+ Assert.Equal(42, retVal);
+ Assert.True(validationCalled);
+ Assert.True(bindingContext.ModelState.IsValid);
+ }
+
+ [Fact]
+ public void BindModel_SuccessfulBind_ComplexTypeFallback_RunsValidationAndReturnsModel()
+ {
+ // Arrange
+ ControllerContext controllerContext = GetControllerContext();
+
+ bool validationCalled = false;
+ List<int> expectedModel = new List<int> { 1, 2, 3, 4, 5 };
+
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ FallbackToEmptyPrefix = true,
+ ModelMetadata = new DataAnnotationsModelMetadataProvider().GetMetadataForType(null, typeof(List<int>)),
+ ModelName = "someName",
+ ModelState = controllerContext.Controller.ViewData.ModelState,
+ PropertyFilter = _ => true,
+ ValueProvider = new SimpleValueProvider
+ {
+ { "someOtherName", "dummyValue" }
+ }
+ };
+
+ Mock<IExtensibleModelBinder> mockIntBinder = new Mock<IExtensibleModelBinder>();
+ mockIntBinder
+ .Setup(o => o.BindModel(controllerContext, It.IsAny<ExtensibleModelBindingContext>()))
+ .Returns(
+ delegate(ControllerContext cc, ExtensibleModelBindingContext mbc)
+ {
+ Assert.Same(bindingContext.ModelMetadata, mbc.ModelMetadata);
+ Assert.Equal("", mbc.ModelName);
+ Assert.Same(bindingContext.ValueProvider, mbc.ValueProvider);
+
+ mbc.Model = expectedModel;
+ mbc.ValidationNode.Validating += delegate { validationCalled = true; };
+ return true;
+ });
+
+ ModelBinderProviderCollection binderProviders = new ModelBinderProviderCollection();
+ binderProviders.RegisterBinderForType(typeof(List<int>), mockIntBinder.Object, false /* suppressPrefixCheck */);
+ ExtensibleModelBinderAdapter shimBinder = new ExtensibleModelBinderAdapter(binderProviders);
+
+ // Act
+ object retVal = shimBinder.BindModel(controllerContext, bindingContext);
+
+ // Assert
+ Assert.Equal(expectedModel, retVal);
+ Assert.True(validationCalled);
+ Assert.True(bindingContext.ModelState.IsValid);
+ }
+
+ [Fact]
+ public void BindModel_UnsuccessfulBind_BinderFails_ReturnsNull()
+ {
+ // Arrange
+ ControllerContext controllerContext = GetControllerContext();
+ Mock<IExtensibleModelBinder> mockListBinder = new Mock<IExtensibleModelBinder>();
+ mockListBinder.Setup(o => o.BindModel(controllerContext, It.IsAny<ExtensibleModelBindingContext>())).Returns(false).Verifiable();
+
+ ModelBinderProviderCollection binderProviders = new ModelBinderProviderCollection();
+ binderProviders.RegisterBinderForType(typeof(List<int>), mockListBinder.Object, true /* suppressPrefixCheck */);
+ ExtensibleModelBinderAdapter shimBinder = new ExtensibleModelBinderAdapter(binderProviders);
+
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ FallbackToEmptyPrefix = false,
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(List<int>)),
+ ModelState = controllerContext.Controller.ViewData.ModelState
+ };
+
+ // Act
+ object retVal = shimBinder.BindModel(controllerContext, bindingContext);
+
+ // Assert
+ Assert.Null(retVal);
+ Assert.True(bindingContext.ModelState.IsValid);
+ mockListBinder.Verify();
+ }
+
+ [Fact]
+ public void BindModel_UnsuccessfulBind_SimpleTypeNoFallback_ReturnsNull()
+ {
+ // Arrange
+ ControllerContext controllerContext = GetControllerContext();
+ Mock<ModelBinderProvider> mockBinderProvider = new Mock<ModelBinderProvider>();
+ mockBinderProvider.Setup(o => o.GetBinder(controllerContext, It.IsAny<ExtensibleModelBindingContext>())).Returns((IExtensibleModelBinder)null).Verifiable();
+ ModelBinderProviderCollection binderProviders = new ModelBinderProviderCollection
+ {
+ mockBinderProvider.Object
+ };
+ ExtensibleModelBinderAdapter shimBinder = new ExtensibleModelBinderAdapter(binderProviders);
+
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ FallbackToEmptyPrefix = true,
+ ModelMetadata = new DataAnnotationsModelMetadataProvider().GetMetadataForType(null, typeof(int)),
+ ModelState = controllerContext.Controller.ViewData.ModelState
+ };
+
+ // Act
+ object retVal = shimBinder.BindModel(controllerContext, bindingContext);
+
+ // Assert
+ Assert.Null(retVal);
+ Assert.True(bindingContext.ModelState.IsValid);
+ mockBinderProvider.Verify();
+ mockBinderProvider.Verify(o => o.GetBinder(controllerContext, It.IsAny<ExtensibleModelBindingContext>()), Times.AtMostOnce());
+ }
+
+ private static ControllerContext GetControllerContext()
+ {
+ return new ControllerContext
+ {
+ Controller = new SimpleController()
+ };
+ }
+
+ private class SimpleController : Controller
+ {
+ }
+
+ private class SimpleModel
+ {
+ public string FirstName { get; set; }
+ public string LastName { get; set; }
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ExtensibleModelBindingContextTest.cs b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ExtensibleModelBindingContextTest.cs
new file mode 100644
index 00000000..e746bcb9
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ExtensibleModelBindingContextTest.cs
@@ -0,0 +1,136 @@
+using System;
+using System.Web.Mvc;
+using System.Web.TestUtil;
+using Microsoft.Web.UnitTestUtil;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace Microsoft.Web.Mvc.ModelBinding.Test
+{
+ public class ExtensibleModelBindingContextTest
+ {
+ [Fact]
+ public void CopyConstructor()
+ {
+ // Arrange
+ ExtensibleModelBindingContext originalBindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(object)),
+ ModelName = "theName",
+ ModelState = new ModelStateDictionary(),
+ ValueProvider = new SimpleValueProvider()
+ };
+
+ // Act
+ ExtensibleModelBindingContext newBindingContext = new ExtensibleModelBindingContext(originalBindingContext);
+
+ // Assert
+ Assert.Null(newBindingContext.ModelMetadata);
+ Assert.Equal("", newBindingContext.ModelName);
+ Assert.Equal(originalBindingContext.ModelState, newBindingContext.ModelState);
+ Assert.Equal(originalBindingContext.ValueProvider, newBindingContext.ValueProvider);
+ }
+
+ [Fact]
+ public void ModelBinderProvidersProperty()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext();
+
+ // Act & assert
+ MemberHelper.TestPropertyWithDefaultInstance(bindingContext, "ModelBinderProviders", new ModelBinderProviderCollection(), ModelBinderProviders.Providers);
+ }
+
+ [Fact]
+ public void ModelProperty()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int))
+ };
+
+ // Act & assert
+ MemberHelper.TestPropertyValue(bindingContext, "Model", 42);
+ }
+
+ [Fact]
+ public void ModelProperty_ThrowsIfModelMetadataDoesNotExist()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext();
+
+ // Act & assert
+ Assert.Throws<InvalidOperationException>(
+ delegate { bindingContext.Model = null; },
+ "The ModelMetadata property must be set before accessing this property.");
+ }
+
+ [Fact]
+ public void ModelNameProperty()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext();
+
+ // Act & assert
+ Assert.Reflection.StringProperty(bindingContext, (context) => context.ModelName, String.Empty);
+ }
+
+ [Fact]
+ public void ModelStateProperty()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext();
+ ModelStateDictionary modelState = new ModelStateDictionary();
+
+ // Act & assert
+ MemberHelper.TestPropertyWithDefaultInstance(bindingContext, "ModelState", modelState);
+ }
+
+ [Fact]
+ public void ModelAndModelTypeAreFedFromModelMetadata()
+ {
+ // Act
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => 42, typeof(int))
+ };
+
+ // Assert
+ Assert.Equal(42, bindingContext.Model);
+ Assert.Equal(typeof(int), bindingContext.ModelType);
+ }
+
+ [Fact]
+ public void ValidationNodeProperty()
+ {
+ // Act
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => 42, typeof(int))
+ };
+
+ // Act & assert
+ MemberHelper.TestPropertyWithDefaultInstance(bindingContext, "ValidationNode", new ModelValidationNode(bindingContext.ModelMetadata, "someName"));
+ }
+
+ [Fact]
+ public void ValidationNodeProperty_DefaultValues()
+ {
+ // Act
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => 42, typeof(int)),
+ ModelName = "theInt"
+ };
+
+ // Act
+ ModelValidationNode validationNode = bindingContext.ValidationNode;
+
+ // Assert
+ Assert.NotNull(validationNode);
+ Assert.Equal(bindingContext.ModelMetadata, validationNode.ModelMetadata);
+ Assert.Equal(bindingContext.ModelName, validationNode.ModelStateKey);
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/GenericModelBinderProviderTest.cs b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/GenericModelBinderProviderTest.cs
new file mode 100644
index 00000000..9a8a60f7
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/GenericModelBinderProviderTest.cs
@@ -0,0 +1,245 @@
+using System;
+using System.Collections.Generic;
+using System.Web.Mvc;
+using Microsoft.Web.UnitTestUtil;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace Microsoft.Web.Mvc.ModelBinding.Test
+{
+ public class GenericModelBinderProviderTest
+ {
+ [Fact]
+ public void Constructor_WithFactory_ThrowsIfModelBinderFactoryIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new GenericModelBinderProvider(typeof(List<>), (Func<Type[], IExtensibleModelBinder>)null); }, "modelBinderFactory");
+ }
+
+ [Fact]
+ public void Constructor_WithFactory_ThrowsIfModelTypeIsNotOpenGeneric()
+ {
+ // Act & assert
+ Assert.Throws<ArgumentException>(
+ delegate { new GenericModelBinderProvider(typeof(List<int>), _ => null); },
+ @"The type 'System.Collections.Generic.List`1[System.Int32]' is not an open generic type.
+Parameter name: modelType");
+ }
+
+ [Fact]
+ public void Constructor_WithFactory_ThrowsIfModelTypeIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new GenericModelBinderProvider(null, _ => null); }, "modelType");
+ }
+
+ [Fact]
+ public void Constructor_WithInstance_ThrowsIfModelBinderIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new GenericModelBinderProvider(typeof(List<>), (IExtensibleModelBinder)null); }, "modelBinder");
+ }
+
+ [Fact]
+ public void Constructor_WithInstance_ThrowsIfModelTypeIsNotOpenGeneric()
+ {
+ // Act & assert
+ Assert.Throws<ArgumentException>(
+ delegate { new GenericModelBinderProvider(typeof(List<int>), new MutableObjectModelBinder()); },
+ @"The type 'System.Collections.Generic.List`1[System.Int32]' is not an open generic type.
+Parameter name: modelType");
+ }
+
+ [Fact]
+ public void Constructor_WithInstance_ThrowsIfModelTypeIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new GenericModelBinderProvider(null, new MutableObjectModelBinder()); }, "modelType");
+ }
+
+ [Fact]
+ public void Constructor_WithType_ThrowsIfModelBinderTypeIsNotModelBinder()
+ {
+ // Act & assert
+ Assert.Throws<ArgumentException>(
+ delegate { new GenericModelBinderProvider(typeof(List<>), typeof(string)); },
+ @"The type 'System.String' does not implement the interface 'Microsoft.Web.Mvc.ModelBinding.IExtensibleModelBinder'.
+Parameter name: modelBinderType");
+ }
+
+ [Fact]
+ public void Constructor_WithType_ThrowsIfModelBinderTypeIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new GenericModelBinderProvider(typeof(List<>), (Type)null); }, "modelBinderType");
+ }
+
+ [Fact]
+ public void Constructor_WithType_ThrowsIfModelBinderTypeTypeArgumentMismatch()
+ {
+ // Act & assert
+ Assert.Throws<ArgumentException>(
+ delegate { new GenericModelBinderProvider(typeof(List<>), typeof(DictionaryModelBinder<,>)); },
+ @"The open model type 'System.Collections.Generic.List`1[T]' has 1 generic type argument(s), but the open binder type 'Microsoft.Web.Mvc.ModelBinding.DictionaryModelBinder`2[TKey,TValue]' has 2 generic type argument(s). The binder type must not be an open generic type or must have the same number of generic arguments as the open model type.
+Parameter name: modelBinderType");
+ }
+
+ [Fact]
+ public void Constructor_WithType_ThrowsIfModelTypeIsNotOpenGeneric()
+ {
+ // Act & assert
+ Assert.Throws<ArgumentException>(
+ delegate { new GenericModelBinderProvider(typeof(List<int>), typeof(MutableObjectModelBinder)); },
+ @"The type 'System.Collections.Generic.List`1[System.Int32]' is not an open generic type.
+Parameter name: modelType");
+ }
+
+ [Fact]
+ public void Constructor_WithType_ThrowsIfModelTypeIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new GenericModelBinderProvider(null, typeof(MutableObjectModelBinder)); }, "modelType");
+ }
+
+ [Fact]
+ public void GetBinder_TypeDoesNotMatch_ModelTypeIsInterface_ReturnsNull()
+ {
+ // Arrange
+ GenericModelBinderProvider provider = new GenericModelBinderProvider(typeof(IEnumerable<>), typeof(CollectionModelBinder<>))
+ {
+ SuppressPrefixCheck = true
+ };
+ ExtensibleModelBindingContext bindingContext = GetBindingContext(typeof(object));
+
+ // Act
+ IExtensibleModelBinder binder = provider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(binder);
+ }
+
+ [Fact]
+ public void GetBinder_TypeDoesNotMatch_ModelTypeIsNotInterface_ReturnsNull()
+ {
+ // Arrange
+ GenericModelBinderProvider provider = new GenericModelBinderProvider(typeof(List<>), typeof(CollectionModelBinder<>))
+ {
+ SuppressPrefixCheck = true
+ };
+ ExtensibleModelBindingContext bindingContext = GetBindingContext(typeof(object));
+
+ // Act
+ IExtensibleModelBinder binder = provider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(binder);
+ }
+
+ [Fact]
+ public void GetBinder_TypeMatches_PrefixNotFound_ReturnsNull()
+ {
+ // Arrange
+ IExtensibleModelBinder binderInstance = new Mock<IExtensibleModelBinder>().Object;
+ GenericModelBinderProvider provider = new GenericModelBinderProvider(typeof(List<>), binderInstance);
+
+ ExtensibleModelBindingContext bindingContext = GetBindingContext(typeof(List<int>));
+ bindingContext.ValueProvider = new SimpleValueProvider();
+
+ // Act
+ IExtensibleModelBinder returnedBinder = provider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(returnedBinder);
+ }
+
+ [Fact]
+ public void GetBinder_TypeMatches_Success_Factory_ReturnsBinder()
+ {
+ // Arrange
+ IExtensibleModelBinder binderInstance = new Mock<IExtensibleModelBinder>().Object;
+
+ Func<Type[], IExtensibleModelBinder> binderFactory = typeArguments =>
+ {
+ Assert.Equal(new[] { typeof(int) }, typeArguments);
+ return binderInstance;
+ };
+
+ GenericModelBinderProvider provider = new GenericModelBinderProvider(typeof(IList<>), binderFactory)
+ {
+ SuppressPrefixCheck = true
+ };
+
+ ExtensibleModelBindingContext bindingContext = GetBindingContext(typeof(List<int>));
+
+ // Act
+ IExtensibleModelBinder returnedBinder = provider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Same(binderInstance, returnedBinder);
+ }
+
+ [Fact]
+ public void GetBinder_TypeMatches_Success_Instance_ReturnsBinder()
+ {
+ // Arrange
+ IExtensibleModelBinder binderInstance = new Mock<IExtensibleModelBinder>().Object;
+
+ GenericModelBinderProvider provider = new GenericModelBinderProvider(typeof(List<>), binderInstance)
+ {
+ SuppressPrefixCheck = true
+ };
+
+ ExtensibleModelBindingContext bindingContext = GetBindingContext(typeof(List<int>));
+
+ // Act
+ IExtensibleModelBinder returnedBinder = provider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Same(binderInstance, returnedBinder);
+ }
+
+ [Fact]
+ public void GetBinder_TypeMatches_Success_TypeActivation_ReturnsBinder()
+ {
+ // Arrange
+ GenericModelBinderProvider provider = new GenericModelBinderProvider(typeof(List<>), typeof(CollectionModelBinder<>))
+ {
+ SuppressPrefixCheck = true
+ };
+
+ ExtensibleModelBindingContext bindingContext = GetBindingContext(typeof(List<int>));
+
+ // Act
+ IExtensibleModelBinder returnedBinder = provider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.IsType<CollectionModelBinder<int>>(returnedBinder);
+ }
+
+ [Fact]
+ public void GetBinderThrowsIfBindingContextIsNull()
+ {
+ // Arrange
+ GenericModelBinderProvider provider = new GenericModelBinderProvider(typeof(IEnumerable<>), typeof(CollectionModelBinder<>));
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { provider.GetBinder(null, null); }, "bindingContext");
+ }
+
+ private static ExtensibleModelBindingContext GetBindingContext(Type modelType)
+ {
+ return new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => null, modelType)
+ };
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/KeyValuePairModelBinderProviderTest.cs b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/KeyValuePairModelBinderProviderTest.cs
new file mode 100644
index 00000000..79db5694
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/KeyValuePairModelBinderProviderTest.cs
@@ -0,0 +1,104 @@
+using System.Collections.Generic;
+using System.Web.Mvc;
+using Microsoft.Web.UnitTestUtil;
+using Xunit;
+
+namespace Microsoft.Web.Mvc.ModelBinding.Test
+{
+ public class KeyValuePairModelBinderProviderTest
+ {
+ [Fact]
+ public void GetBinder_CorrectModelTypeAndValueProviderEntries_ReturnsBinder()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(KeyValuePair<int, string>)),
+ ModelName = "foo",
+ ValueProvider = new SimpleValueProvider
+ {
+ { "foo.key", 42 },
+ { "foo.value", "someValue" }
+ }
+ };
+
+ KeyValuePairModelBinderProvider binderProvider = new KeyValuePairModelBinderProvider();
+
+ // Act
+ IExtensibleModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.IsType<KeyValuePairModelBinder<int, string>>(binder);
+ }
+
+ [Fact]
+ public void GetBinder_ModelTypeIsIncorrect_ReturnsNull()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(List<int>)),
+ ModelName = "foo",
+ ValueProvider = new SimpleValueProvider
+ {
+ { "foo.key", 42 },
+ { "foo.value", "someValue" }
+ }
+ };
+
+ KeyValuePairModelBinderProvider binderProvider = new KeyValuePairModelBinderProvider();
+
+ // Act
+ IExtensibleModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(binder);
+ }
+
+ [Fact]
+ public void GetBinder_ValueProviderDoesNotContainKeyProperty_ReturnsNull()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(KeyValuePair<int, string>)),
+ ModelName = "foo",
+ ValueProvider = new SimpleValueProvider
+ {
+ { "foo.value", "someValue" }
+ }
+ };
+
+ KeyValuePairModelBinderProvider binderProvider = new KeyValuePairModelBinderProvider();
+
+ // Act
+ IExtensibleModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(binder);
+ }
+
+ [Fact]
+ public void GetBinder_ValueProviderDoesNotContainValueProperty_ReturnsNull()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(KeyValuePair<int, string>)),
+ ModelName = "foo",
+ ValueProvider = new SimpleValueProvider
+ {
+ { "foo.key", 42 }
+ }
+ };
+
+ KeyValuePairModelBinderProvider binderProvider = new KeyValuePairModelBinderProvider();
+
+ // Act
+ IExtensibleModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(binder);
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/KeyValuePairModelBinderTest.cs b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/KeyValuePairModelBinderTest.cs
new file mode 100644
index 00000000..35fd4a14
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/KeyValuePairModelBinderTest.cs
@@ -0,0 +1,116 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Web.Mvc;
+using Microsoft.Web.UnitTestUtil;
+using Moq;
+using Xunit;
+
+namespace Microsoft.Web.Mvc.ModelBinding.Test
+{
+ public class KeyValuePairModelBinderTest
+ {
+ [Fact]
+ public void BindModel_MissingKey_ReturnsFalse()
+ {
+ // Arrange
+ ControllerContext controllerContext = new ControllerContext();
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(KeyValuePair<int, string>)),
+ ModelName = "someName",
+ ModelBinderProviders = new ModelBinderProviderCollection(),
+ ValueProvider = new SimpleValueProvider()
+ };
+
+ KeyValuePairModelBinder<int, string> binder = new KeyValuePairModelBinder<int, string>();
+
+ // Act
+ bool retVal = binder.BindModel(controllerContext, bindingContext);
+
+ // Assert
+ Assert.False(retVal);
+ Assert.Null(bindingContext.Model);
+ Assert.Empty(bindingContext.ValidationNode.ChildNodes);
+ }
+
+ [Fact]
+ public void BindModel_MissingValue_ReturnsTrue()
+ {
+ // Arrange
+ ControllerContext controllerContext = new ControllerContext();
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(KeyValuePair<int, string>)),
+ ModelName = "someName",
+ ModelBinderProviders = new ModelBinderProviderCollection(),
+ ValueProvider = new SimpleValueProvider()
+ };
+
+ Mock<IExtensibleModelBinder> mockIntBinder = new Mock<IExtensibleModelBinder>();
+ mockIntBinder
+ .Setup(o => o.BindModel(controllerContext, It.IsAny<ExtensibleModelBindingContext>()))
+ .Returns(
+ delegate(ControllerContext cc, ExtensibleModelBindingContext mbc)
+ {
+ mbc.Model = 42;
+ return true;
+ });
+ bindingContext.ModelBinderProviders.RegisterBinderForType(typeof(int), mockIntBinder.Object, true /* suppressPrefixCheck */);
+
+ KeyValuePairModelBinder<int, string> binder = new KeyValuePairModelBinder<int, string>();
+
+ // Act
+ bool retVal = binder.BindModel(controllerContext, bindingContext);
+
+ // Assert
+ Assert.True(retVal);
+ Assert.Null(bindingContext.Model);
+ Assert.Equal(new[] { "someName.key" }, bindingContext.ValidationNode.ChildNodes.Select(n => n.ModelStateKey).ToArray());
+ }
+
+ [Fact]
+ public void BindModel_SubBindingSucceeds()
+ {
+ // Arrange
+ ControllerContext controllerContext = new ControllerContext();
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(KeyValuePair<int, string>)),
+ ModelName = "someName",
+ ModelBinderProviders = new ModelBinderProviderCollection(),
+ ValueProvider = new SimpleValueProvider()
+ };
+
+ Mock<IExtensibleModelBinder> mockIntBinder = new Mock<IExtensibleModelBinder>();
+ mockIntBinder
+ .Setup(o => o.BindModel(controllerContext, It.IsAny<ExtensibleModelBindingContext>()))
+ .Returns(
+ delegate(ControllerContext cc, ExtensibleModelBindingContext mbc)
+ {
+ mbc.Model = 42;
+ return true;
+ });
+ bindingContext.ModelBinderProviders.RegisterBinderForType(typeof(int), mockIntBinder.Object, true /* suppressPrefixCheck */);
+ Mock<IExtensibleModelBinder> mockStringBinder = new Mock<IExtensibleModelBinder>();
+ mockStringBinder
+ .Setup(o => o.BindModel(controllerContext, It.IsAny<ExtensibleModelBindingContext>()))
+ .Returns(
+ delegate(ControllerContext cc, ExtensibleModelBindingContext mbc)
+ {
+ mbc.Model = "forty-two";
+ return true;
+ });
+ bindingContext.ModelBinderProviders.RegisterBinderForType(typeof(string), mockStringBinder.Object, true /* suppressPrefixCheck */);
+
+ KeyValuePairModelBinder<int, string> binder = new KeyValuePairModelBinder<int, string>();
+
+ // Act
+ bool retVal = binder.BindModel(controllerContext, bindingContext);
+
+ // Assert
+ Assert.True(retVal);
+ Assert.Equal(new KeyValuePair<int, string>(42, "forty-two"), bindingContext.Model);
+ Assert.Equal(new[] { "someName.key", "someName.value" }, bindingContext.ValidationNode.ChildNodes.Select(n => n.ModelStateKey).ToArray());
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/KeyValuePairModelBinderUtilTest.cs b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/KeyValuePairModelBinderUtilTest.cs
new file mode 100644
index 00000000..39a37673
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/KeyValuePairModelBinderUtilTest.cs
@@ -0,0 +1,108 @@
+using System.Web.Mvc;
+using Microsoft.Web.UnitTestUtil;
+using Moq;
+using Xunit;
+
+namespace Microsoft.Web.Mvc.ModelBinding.Test
+{
+ public class KeyValuePairModelBinderUtilTest
+ {
+ [Fact]
+ public void TryBindStrongModel_BinderExists_BinderReturnsCorrectlyTypedObject_ReturnsTrue()
+ {
+ // Arrange
+ ControllerContext controllerContext = new ControllerContext();
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int)),
+ ModelName = "someName",
+ ModelState = new ModelStateDictionary(),
+ ModelBinderProviders = new ModelBinderProviderCollection(),
+ ValueProvider = new SimpleValueProvider()
+ };
+
+ Mock<IExtensibleModelBinder> mockIntBinder = new Mock<IExtensibleModelBinder>();
+ mockIntBinder
+ .Setup(o => o.BindModel(controllerContext, It.IsAny<ExtensibleModelBindingContext>()))
+ .Returns(
+ delegate(ControllerContext cc, ExtensibleModelBindingContext mbc)
+ {
+ Assert.Equal("someName.key", mbc.ModelName);
+ mbc.Model = 42;
+ return true;
+ });
+ bindingContext.ModelBinderProviders.RegisterBinderForType(typeof(int), mockIntBinder.Object, true /* suppressPrefixCheck */);
+
+ // Act
+ int model;
+ bool retVal = KeyValuePairModelBinderUtil.TryBindStrongModel(controllerContext, bindingContext, "key", new EmptyModelMetadataProvider(), out model);
+
+ // Assert
+ Assert.True(retVal);
+ Assert.Equal(42, model);
+ Assert.Single(bindingContext.ValidationNode.ChildNodes);
+ Assert.Empty(bindingContext.ModelState);
+ }
+
+ [Fact]
+ public void TryBindStrongModel_BinderExists_BinderReturnsIncorrectlyTypedObject_ReturnsTrue()
+ {
+ // Arrange
+ ControllerContext controllerContext = new ControllerContext();
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int)),
+ ModelName = "someName",
+ ModelState = new ModelStateDictionary(),
+ ModelBinderProviders = new ModelBinderProviderCollection(),
+ ValueProvider = new SimpleValueProvider()
+ };
+
+ Mock<IExtensibleModelBinder> mockIntBinder = new Mock<IExtensibleModelBinder>();
+ mockIntBinder
+ .Setup(o => o.BindModel(controllerContext, It.IsAny<ExtensibleModelBindingContext>()))
+ .Returns(
+ delegate(ControllerContext cc, ExtensibleModelBindingContext mbc)
+ {
+ Assert.Equal("someName.key", mbc.ModelName);
+ return true;
+ });
+ bindingContext.ModelBinderProviders.RegisterBinderForType(typeof(int), mockIntBinder.Object, true /* suppressPrefixCheck */);
+
+ // Act
+ int model;
+ bool retVal = KeyValuePairModelBinderUtil.TryBindStrongModel(controllerContext, bindingContext, "key", new EmptyModelMetadataProvider(), out model);
+
+ // Assert
+ Assert.True(retVal);
+ Assert.Equal(default(int), model);
+ Assert.Single(bindingContext.ValidationNode.ChildNodes);
+ Assert.Empty(bindingContext.ModelState);
+ }
+
+ [Fact]
+ public void TryBindStrongModel_NoBinder_ReturnsFalse()
+ {
+ // Arrange
+ ControllerContext controllerContext = new ControllerContext();
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int)),
+ ModelName = "someName",
+ ModelState = new ModelStateDictionary(),
+ ModelBinderProviders = new ModelBinderProviderCollection(),
+ ValueProvider = new SimpleValueProvider()
+ };
+
+ // Act
+ int model;
+ bool retVal = KeyValuePairModelBinderUtil.TryBindStrongModel(controllerContext, bindingContext, "key", new EmptyModelMetadataProvider(), out model);
+
+ // Assert
+ Assert.False(retVal);
+ Assert.Equal(default(int), model);
+ Assert.Empty(bindingContext.ValidationNode.ChildNodes);
+ Assert.Empty(bindingContext.ModelState);
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ModelBinderConfigTest.cs b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ModelBinderConfigTest.cs
new file mode 100644
index 00000000..cbeefe1c
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ModelBinderConfigTest.cs
@@ -0,0 +1,166 @@
+using System;
+using System.Globalization;
+using System.Web;
+using System.Web.Mvc;
+using System.Web.TestUtil;
+using Moq;
+using Xunit;
+
+namespace Microsoft.Web.Mvc.ModelBinding.Test
+{
+ public class ModelBinderConfigTest
+ {
+ [Fact]
+ public void GetUserResourceString_NullControllerContext_ReturnsNull()
+ {
+ // Act
+ string customResourceString = ModelBinderConfig.GetUserResourceString(null /* controllerContext */, "someResourceName", "someResourceClassKey");
+
+ // Assert
+ Assert.Null(customResourceString);
+ }
+
+ [Fact]
+ public void GetUserResourceString_NullHttpContext_ReturnsNull()
+ {
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.Setup(o => o.HttpContext).Returns((HttpContextBase)null);
+
+ // Act
+ string customResourceString = ModelBinderConfig.GetUserResourceString(mockControllerContext.Object, "someResourceName", "someResourceClassKey");
+
+ // Assert
+ Assert.Null(customResourceString);
+ }
+
+ [Fact]
+ public void GetUserResourceString_NullResourceKey_ReturnsNull()
+ {
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+
+ // Act
+ string customResourceString = ModelBinderConfig.GetUserResourceString(mockControllerContext.Object, "someResourceName", null /* resourceClassKey */);
+
+ // Assert
+ mockControllerContext.Verify(o => o.HttpContext, Times.Never());
+ Assert.Null(customResourceString);
+ }
+
+ [Fact]
+ public void GetUserResourceString_ValidResourceObject_ReturnsResourceString()
+ {
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.Setup(o => o.HttpContext.GetGlobalResourceObject("someResourceClassKey", "someResourceName", CultureInfo.CurrentUICulture)).Returns("My custom resource string");
+
+ // Act
+ string customResourceString = ModelBinderConfig.GetUserResourceString(mockControllerContext.Object, "someResourceName", "someResourceClassKey");
+
+ // Assert
+ Assert.Equal("My custom resource string", customResourceString);
+ }
+
+ [Fact]
+ public void Initialize_ReplacesOriginalCollection()
+ {
+ // Arrange
+ ModelBinderDictionary oldBinders = new ModelBinderDictionary();
+ oldBinders[typeof(int)] = new Mock<IModelBinder>().Object;
+ ModelBinderProviderCollection newBinderProviders = new ModelBinderProviderCollection();
+
+ // Act
+ ModelBinderConfig.Initialize(oldBinders, newBinderProviders);
+
+ // Assert
+ Assert.Empty(oldBinders);
+
+ var shimBinder = Assert.IsType<ExtensibleModelBinderAdapter>(oldBinders.DefaultBinder);
+ Assert.Same(newBinderProviders, shimBinder.Providers);
+ }
+
+ [Fact]
+ public void TypeConversionErrorMessageProvider_DefaultValue()
+ {
+ // Arrange
+ ModelMetadata metadata = new ModelMetadata(new Mock<ModelMetadataProvider>().Object, null, null, typeof(int), "SomePropertyName");
+
+ // Act
+ string errorString = ModelBinderConfig.TypeConversionErrorMessageProvider(null, metadata, "some incoming value");
+
+ // Assert
+ Assert.Equal("The value 'some incoming value' is not valid for SomePropertyName.", errorString);
+ }
+
+ [Fact]
+ public void TypeConversionErrorMessageProvider_Property()
+ {
+ // Arrange
+ ModelBinderConfigWrapper wrapper = new ModelBinderConfigWrapper();
+
+ // Act & assert
+ try
+ {
+ MemberHelper.TestPropertyWithDefaultInstance(wrapper, "TypeConversionErrorMessageProvider", (ModelBinderErrorMessageProvider)DummyErrorSelector);
+ }
+ finally
+ {
+ wrapper.Reset();
+ }
+ }
+
+ [Fact]
+ public void ValueRequiredErrorMessageProvider_DefaultValue()
+ {
+ // Arrange
+ ModelMetadata metadata = new ModelMetadata(new Mock<ModelMetadataProvider>().Object, null, null, typeof(int), "SomePropertyName");
+
+ // Act
+ string errorString = ModelBinderConfig.ValueRequiredErrorMessageProvider(null, metadata, "some incoming value");
+
+ // Assert
+ Assert.Equal("A value is required.", errorString);
+ }
+
+ [Fact]
+ public void ValueRequiredErrorMessageProvider_Property()
+ {
+ // Arrange
+ ModelBinderConfigWrapper wrapper = new ModelBinderConfigWrapper();
+
+ // Act & assert
+ try
+ {
+ MemberHelper.TestPropertyWithDefaultInstance(wrapper, "ValueRequiredErrorMessageProvider", (ModelBinderErrorMessageProvider)DummyErrorSelector);
+ }
+ finally
+ {
+ wrapper.Reset();
+ }
+ }
+
+ private string DummyErrorSelector(ControllerContext controllerContext, ModelMetadata modelMetadata, object incomingValue)
+ {
+ throw new NotImplementedException();
+ }
+
+ private sealed class ModelBinderConfigWrapper
+ {
+ public ModelBinderErrorMessageProvider TypeConversionErrorMessageProvider
+ {
+ get { return ModelBinderConfig.TypeConversionErrorMessageProvider; }
+ set { ModelBinderConfig.TypeConversionErrorMessageProvider = value; }
+ }
+
+ public ModelBinderErrorMessageProvider ValueRequiredErrorMessageProvider
+ {
+ get { return ModelBinderConfig.ValueRequiredErrorMessageProvider; }
+ set { ModelBinderConfig.ValueRequiredErrorMessageProvider = value; }
+ }
+
+ public void Reset()
+ {
+ ModelBinderConfig.TypeConversionErrorMessageProvider = null;
+ ModelBinderConfig.ValueRequiredErrorMessageProvider = null;
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ModelBinderProviderCollectionTest.cs b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ModelBinderProviderCollectionTest.cs
new file mode 100644
index 00000000..064dfb03
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ModelBinderProviderCollectionTest.cs
@@ -0,0 +1,528 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Web.Mvc;
+using System.Web.TestUtil;
+using Microsoft.Web.UnitTestUtil;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace Microsoft.Web.Mvc.ModelBinding.Test
+{
+ public class ModelBinderProviderCollectionTest
+ {
+ [Fact]
+ public void ListWrappingConstructor()
+ {
+ // Arrange
+ ModelBinderProvider[] providers = new[]
+ {
+ new Mock<ModelBinderProvider>().Object,
+ new Mock<ModelBinderProvider>().Object
+ };
+
+ // Act
+ ModelBinderProviderCollection collection = new ModelBinderProviderCollection(providers);
+
+ // Assert
+ Assert.Equal(providers, collection.ToArray());
+ }
+
+ [Fact]
+ public void DefaultConstructor()
+ {
+ // Act
+ ModelBinderProviderCollection collection = new ModelBinderProviderCollection();
+
+ // Assert
+ Assert.Empty(collection);
+ }
+
+ [Fact]
+ public void AddNullProviderThrows()
+ {
+ // Arrange
+ ModelBinderProviderCollection collection = new ModelBinderProviderCollection();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { collection.Add(null); },
+ "item");
+ }
+
+ [Fact]
+ public void RegisterBinderForGenericType_Factory()
+ {
+ // Arrange
+ ModelBinderProvider mockProvider = new Mock<ModelBinderProvider>().Object;
+ IExtensibleModelBinder mockBinder = new Mock<IExtensibleModelBinder>().Object;
+
+ ModelBinderProviderCollection collection = new ModelBinderProviderCollection
+ {
+ mockProvider
+ };
+
+ // Act
+ collection.RegisterBinderForGenericType(typeof(List<>), _ => mockBinder);
+
+ // Assert
+ var genericProvider = Assert.IsType<GenericModelBinderProvider>(collection[0]);
+ Assert.Equal(typeof(List<>), genericProvider.ModelType);
+ Assert.Equal(mockProvider, collection[1]);
+ }
+
+ [Fact]
+ public void RegisterBinderForGenericType_Instance()
+ {
+ // Arrange
+ ModelBinderProvider mockProvider = new Mock<ModelBinderProvider>().Object;
+ IExtensibleModelBinder mockBinder = new Mock<IExtensibleModelBinder>().Object;
+
+ ModelBinderProviderCollection collection = new ModelBinderProviderCollection
+ {
+ mockProvider
+ };
+
+ // Act
+ collection.RegisterBinderForGenericType(typeof(List<>), mockBinder);
+
+ // Assert
+ var genericProvider = Assert.IsType<GenericModelBinderProvider>(collection[0]);
+ Assert.Equal(typeof(List<>), genericProvider.ModelType);
+ Assert.Equal(mockProvider, collection[1]);
+ }
+
+ [Fact]
+ public void RegisterBinderForGenericType_Type()
+ {
+ // Arrange
+ ModelBinderProvider mockProvider = new Mock<ModelBinderProvider>().Object;
+ IExtensibleModelBinder mockBinder = new Mock<IExtensibleModelBinder>().Object;
+
+ ModelBinderProviderCollection collection = new ModelBinderProviderCollection
+ {
+ mockProvider
+ };
+
+ // Act
+ collection.RegisterBinderForGenericType(typeof(List<>), typeof(CollectionModelBinder<>));
+
+ // Assert
+ var genericProvider = Assert.IsType<GenericModelBinderProvider>(collection[0]);
+ Assert.Equal(typeof(List<>), genericProvider.ModelType);
+ Assert.Equal(mockProvider, collection[1]);
+ }
+
+ [Fact]
+ public void RegisterBinderForType_Factory()
+ {
+ // Arrange
+ ModelBinderProvider mockProvider = new Mock<ModelBinderProvider>().Object;
+ IExtensibleModelBinder mockBinder = new Mock<IExtensibleModelBinder>().Object;
+
+ ModelBinderProviderCollection collection = new ModelBinderProviderCollection
+ {
+ mockProvider
+ };
+
+ // Act
+ collection.RegisterBinderForType(typeof(int), () => mockBinder);
+
+ // Assert
+ var simpleProvider = Assert.IsType<SimpleModelBinderProvider>(collection[0]);
+ Assert.Equal(typeof(int), simpleProvider.ModelType);
+ Assert.Equal(mockProvider, collection[1]);
+ }
+
+ [Fact]
+ public void RegisterBinderForType_Instance()
+ {
+ // Arrange
+ ModelBinderProvider mockProvider = new Mock<ModelBinderProvider>().Object;
+ IExtensibleModelBinder mockBinder = new Mock<IExtensibleModelBinder>().Object;
+
+ ModelBinderProviderCollection collection = new ModelBinderProviderCollection
+ {
+ mockProvider
+ };
+
+ // Act
+ collection.RegisterBinderForType(typeof(int), mockBinder);
+
+ // Assert
+ var simpleProvider = Assert.IsType<SimpleModelBinderProvider>(collection[0]);
+ Assert.Equal(typeof(int), simpleProvider.ModelType);
+ Assert.Equal(mockProvider, collection[1]);
+ }
+
+ [Fact]
+ public void RegisterBinderForType_Instance_InsertsNewProviderBehindFrontOfListProviders()
+ {
+ // Arrange
+ ModelBinderProvider frontOfListProvider = new ProviderAtFront();
+ IExtensibleModelBinder mockBinder = new Mock<IExtensibleModelBinder>().Object;
+
+ ModelBinderProviderCollection collection = new ModelBinderProviderCollection
+ {
+ frontOfListProvider
+ };
+
+ // Act
+ collection.RegisterBinderForType(typeof(int), mockBinder);
+
+ // Assert
+ Assert.Equal(
+ new[] { typeof(ProviderAtFront), typeof(SimpleModelBinderProvider) },
+ collection.Select(o => o.GetType()).ToArray());
+ }
+
+ [Fact]
+ public void SetItem()
+ {
+ // Arrange
+ ModelBinderProvider provider0 = new Mock<ModelBinderProvider>().Object;
+ ModelBinderProvider provider1 = new Mock<ModelBinderProvider>().Object;
+ ModelBinderProvider provider2 = new Mock<ModelBinderProvider>().Object;
+
+ ModelBinderProviderCollection collection = new ModelBinderProviderCollection();
+ collection.Add(provider0);
+ collection.Add(provider1);
+
+ // Act
+ collection[1] = provider2;
+
+ // Assert
+ Assert.Equal(new[] { provider0, provider2 }, collection.ToArray());
+ }
+
+ [Fact]
+ public void SetNullProviderThrows()
+ {
+ // Arrange
+ ModelBinderProviderCollection collection = new ModelBinderProviderCollection();
+ collection.Add(new Mock<ModelBinderProvider>().Object);
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { collection[0] = null; },
+ "item");
+ }
+
+ [Fact]
+ public void GetBinder_FromAttribute_BadAttribute_Throws()
+ {
+ // Arrange
+ ControllerContext controllerContext = new ControllerContext();
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(ModelWithProviderAttribute_BadAttribute))
+ };
+
+ ModelBinderProviderCollection providers = new ModelBinderProviderCollection();
+
+ // Act & assert
+ Assert.Throws<InvalidOperationException>(
+ delegate { providers.GetBinder(controllerContext, bindingContext); },
+ @"The type 'System.Object' does not subclass Microsoft.Web.Mvc.ModelBinding.ModelBinderProvider or implement the interface Microsoft.Web.Mvc.ModelBinding.IExtensibleModelBinder.");
+ }
+
+ [Fact]
+ public void GetBinder_FromAttribute_Binder_Generic_ReturnsBinder()
+ {
+ // Arrange
+ ControllerContext controllerContext = new ControllerContext();
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(ModelWithProviderAttribute_Binder_Generic<int>)),
+ ModelName = "foo",
+ ValueProvider = new SimpleValueProvider
+ {
+ { "foo", "fooValue" }
+ }
+ };
+
+ ModelBinderProviderCollection providers = new ModelBinderProviderCollection();
+ providers.RegisterBinderForType(typeof(ModelWithProviderAttribute_Binder_Generic<int>), new Mock<IExtensibleModelBinder>().Object, true /* suppressPrefix */);
+
+ // Act
+ IExtensibleModelBinder binder = providers.GetBinder(controllerContext, bindingContext);
+
+ // Assert
+ Assert.IsType<CustomGenericBinder<int>>(binder);
+ }
+
+ [Fact]
+ public void GetBinder_FromAttribute_Binder_SuppressPrefixCheck_ReturnsBinder()
+ {
+ // Arrange
+ ControllerContext controllerContext = new ControllerContext();
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(ModelWithProviderAttribute_Binder_SuppressPrefix)),
+ ModelName = "foo",
+ ValueProvider = new SimpleValueProvider
+ {
+ { "bar", "barValue" }
+ }
+ };
+
+ ModelBinderProviderCollection providers = new ModelBinderProviderCollection();
+ providers.RegisterBinderForType(typeof(ModelWithProviderAttribute_Binder_SuppressPrefix), new Mock<IExtensibleModelBinder>().Object, true /* suppressPrefix */);
+
+ // Act
+ IExtensibleModelBinder binder = providers.GetBinder(controllerContext, bindingContext);
+
+ // Assert
+ Assert.IsType<CustomBinder>(binder);
+ }
+
+ [Fact]
+ public void GetBinder_FromAttribute_Binder_ValueNotPresent_ReturnsNull()
+ {
+ // Arrange
+ ControllerContext controllerContext = new ControllerContext();
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(ModelWithProviderAttribute_Binder)),
+ ModelName = "foo",
+ ValueProvider = new SimpleValueProvider
+ {
+ { "bar", "barValue" }
+ }
+ };
+
+ ModelBinderProviderCollection providers = new ModelBinderProviderCollection();
+ providers.RegisterBinderForType(typeof(ModelWithProviderAttribute_Binder), new Mock<IExtensibleModelBinder>().Object, true /* suppressPrefix */);
+
+ // Act
+ IExtensibleModelBinder binder = providers.GetBinder(controllerContext, bindingContext);
+
+ // Assert
+ Assert.Null(binder);
+ }
+
+ [Fact]
+ public void GetBinder_FromAttribute_Binder_ValuePresent_ReturnsBinder()
+ {
+ // Arrange
+ ControllerContext controllerContext = new ControllerContext();
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(ModelWithProviderAttribute_Binder)),
+ ModelName = "foo",
+ ValueProvider = new SimpleValueProvider
+ {
+ { "foo", "fooValue" }
+ }
+ };
+
+ ModelBinderProviderCollection providers = new ModelBinderProviderCollection();
+ providers.RegisterBinderForType(typeof(ModelWithProviderAttribute_Binder), new Mock<IExtensibleModelBinder>().Object, true /* suppressPrefix */);
+
+ // Act
+ IExtensibleModelBinder binder = providers.GetBinder(controllerContext, bindingContext);
+
+ // Assert
+ Assert.IsType<CustomBinder>(binder);
+ }
+
+ [Fact]
+ public void GetBinder_FromAttribute_Provider_ReturnsBinder()
+ {
+ // Arrange
+ ControllerContext controllerContext = new ControllerContext();
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(ModelWithProviderAttribute_Provider))
+ };
+
+ ModelBinderProviderCollection providers = new ModelBinderProviderCollection();
+ providers.RegisterBinderForType(typeof(ModelWithProviderAttribute_Provider), new Mock<IExtensibleModelBinder>().Object, true /* suppressPrefix */);
+
+ // Act
+ IExtensibleModelBinder binder = providers.GetBinder(controllerContext, bindingContext);
+
+ // Assert
+ Assert.IsType<CustomBinder>(binder);
+ }
+
+ [Fact]
+ public void GetBinderReturnsFirstBinderFromProviders()
+ {
+ // Arrange
+ ControllerContext controllerContext = new ControllerContext();
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(object))
+ };
+ IExtensibleModelBinder expectedBinder = new Mock<IExtensibleModelBinder>().Object;
+
+ Mock<ModelBinderProvider> mockProvider = new Mock<ModelBinderProvider>();
+ mockProvider.Setup(p => p.GetBinder(controllerContext, bindingContext)).Returns(expectedBinder);
+
+ ModelBinderProviderCollection collection = new ModelBinderProviderCollection(new[]
+ {
+ new Mock<ModelBinderProvider>().Object,
+ mockProvider.Object,
+ new Mock<ModelBinderProvider>().Object
+ });
+
+ // Act
+ IExtensibleModelBinder returned = collection.GetBinder(controllerContext, bindingContext);
+
+ // Assert
+ Assert.Equal(expectedBinder, returned);
+ }
+
+ [Fact]
+ public void GetBinderReturnsNullIfNoProviderMatches()
+ {
+ // Arrange
+ ControllerContext controllerContext = new ControllerContext();
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(object))
+ };
+
+ ModelBinderProviderCollection collection = new ModelBinderProviderCollection(new[]
+ {
+ new Mock<ModelBinderProvider>().Object,
+ });
+
+ // Act
+ IExtensibleModelBinder returned = collection.GetBinder(controllerContext, bindingContext);
+
+ // Assert
+ Assert.Null(returned);
+ }
+
+ [Fact]
+ public void GetBinderThrowsIfBindingContextIsNull()
+ {
+ // Arrange
+ ModelBinderProviderCollection collection = new ModelBinderProviderCollection();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { collection.GetBinder(new ControllerContext(), null); }, "bindingContext");
+ }
+
+ [Fact]
+ public void GetBinderThrowsIfControllerContextIsNull()
+ {
+ // Arrange
+ ModelBinderProviderCollection collection = new ModelBinderProviderCollection();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { collection.GetBinder(null, new ExtensibleModelBindingContext()); }, "controllerContext");
+ }
+
+ [Fact]
+ public void GetBinderThrowsIfModelTypeHasBindAttribute()
+ {
+ // Arrange
+ ControllerContext controllerContext = new ControllerContext();
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(ModelWithBindAttribute))
+ };
+ ModelBinderProviderCollection collection = new ModelBinderProviderCollection();
+
+ // Act & assert
+ Assert.Throws<InvalidOperationException>(
+ delegate { collection.GetBinder(controllerContext, bindingContext); },
+ @"The model of type 'Microsoft.Web.Mvc.ModelBinding.Test.ModelBinderProviderCollectionTest+ModelWithBindAttribute' has a [Bind] attribute. The new model binding system cannot be used with models that have type-level [Bind] attributes. Use the [BindRequired] and [BindNever] attributes on the model type or its properties instead.");
+ }
+
+ [Fact]
+ public void GetRequiredBinderThrowsIfNoProviderMatches()
+ {
+ // Arrange
+ ControllerContext controllerContext = new ControllerContext();
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int))
+ };
+
+ ModelBinderProviderCollection collection = new ModelBinderProviderCollection(new[]
+ {
+ new Mock<ModelBinderProvider>().Object,
+ });
+
+ // Act & assert
+ Assert.Throws<InvalidOperationException>(
+ delegate { collection.GetRequiredBinder(controllerContext, bindingContext); },
+ @"A binder for type System.Int32 could not be located.");
+ }
+
+ [MetadataType(typeof(ModelWithBindAttribute_Buddy))]
+ private class ModelWithBindAttribute
+ {
+ [Bind]
+ private class ModelWithBindAttribute_Buddy
+ {
+ }
+ }
+
+ [ModelBinderProviderOptions(FrontOfList = true)]
+ private class ProviderAtFront : ModelBinderProvider
+ {
+ public override IExtensibleModelBinder GetBinder(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ [ExtensibleModelBinder(typeof(object))]
+ private class ModelWithProviderAttribute_BadAttribute
+ {
+ }
+
+ [ExtensibleModelBinder(typeof(CustomBinder))]
+ private class ModelWithProviderAttribute_Binder
+ {
+ }
+
+ [ExtensibleModelBinder(typeof(CustomGenericBinder<>))]
+ private class ModelWithProviderAttribute_Binder_Generic<T>
+ {
+ }
+
+ [ExtensibleModelBinder(typeof(CustomBinder), SuppressPrefixCheck = true)]
+ private class ModelWithProviderAttribute_Binder_SuppressPrefix
+ {
+ }
+
+ [ExtensibleModelBinder(typeof(CustomProvider))]
+ private class ModelWithProviderAttribute_Provider
+ {
+ }
+
+ private class CustomProvider : ModelBinderProvider
+ {
+ public override IExtensibleModelBinder GetBinder(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext)
+ {
+ return new CustomBinder();
+ }
+ }
+
+ private class CustomBinder : IExtensibleModelBinder
+ {
+ public bool BindModel(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ private class CustomGenericBinder<T> : IExtensibleModelBinder
+ {
+ public bool BindModel(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext)
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ModelBinderProvidersTest.cs b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ModelBinderProvidersTest.cs
new file mode 100644
index 00000000..15437490
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ModelBinderProvidersTest.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Linq;
+using Xunit;
+
+namespace Microsoft.Web.Mvc.ModelBinding.Test
+{
+ public class ModelBinderProvidersTest
+ {
+ [Fact]
+ public void CollectionDefaults()
+ {
+ // Arrange
+ Type[] expectedTypes = new[]
+ {
+ typeof(TypeMatchModelBinderProvider),
+ typeof(BinaryDataModelBinderProvider),
+ typeof(KeyValuePairModelBinderProvider),
+ typeof(ComplexModelDtoModelBinderProvider),
+ typeof(ArrayModelBinderProvider),
+ typeof(DictionaryModelBinderProvider),
+ typeof(CollectionModelBinderProvider),
+ typeof(TypeConverterModelBinderProvider),
+ typeof(MutableObjectModelBinderProvider)
+ };
+
+ // Act
+ Type[] actualTypes = ModelBinderProviders.Providers.Select(p => p.GetType()).ToArray();
+
+ // Assert
+ Assert.Equal(expectedTypes, actualTypes);
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ModelBinderUtilTest.cs b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ModelBinderUtilTest.cs
new file mode 100644
index 00000000..1f6faf17
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ModelBinderUtilTest.cs
@@ -0,0 +1,324 @@
+using System;
+using System.Collections.Generic;
+using System.Web.Mvc;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace Microsoft.Web.Mvc.ModelBinding.Test
+{
+ public class ModelBinderUtilTest
+ {
+ [Fact]
+ public void CastOrDefault_CorrectType_ReturnsInput()
+ {
+ // Act
+ int retVal = ModelBinderUtil.CastOrDefault<int>(42);
+
+ // Assert
+ Assert.Equal(42, retVal);
+ }
+
+ [Fact]
+ public void CastOrDefault_IncorrectType_ReturnsDefaultTModel()
+ {
+ // Act
+ DateTime retVal = ModelBinderUtil.CastOrDefault<DateTime>(42);
+
+ // Assert
+ Assert.Equal(default(DateTime), retVal);
+ }
+
+ [Fact]
+ public void CreateIndexModelName_EmptyParentName()
+ {
+ // Act
+ string fullChildName = ModelBinderUtil.CreateIndexModelName("", 42);
+
+ // Assert
+ Assert.Equal("[42]", fullChildName);
+ }
+
+ [Fact]
+ public void CreateIndexModelName_IntIndex()
+ {
+ // Act
+ string fullChildName = ModelBinderUtil.CreateIndexModelName("parentName", 42);
+
+ // Assert
+ Assert.Equal("parentName[42]", fullChildName);
+ }
+
+ [Fact]
+ public void CreateIndexModelName_StringIndex()
+ {
+ // Act
+ string fullChildName = ModelBinderUtil.CreateIndexModelName("parentName", "index");
+
+ // Assert
+ Assert.Equal("parentName[index]", fullChildName);
+ }
+
+ [Fact]
+ public void CreatePropertyModelName()
+ {
+ // Act
+ string fullChildName = ModelBinderUtil.CreatePropertyModelName("parentName", "childName");
+
+ // Assert
+ Assert.Equal("parentName.childName", fullChildName);
+ }
+
+ [Fact]
+ public void CreatePropertyModelName_EmptyParentName()
+ {
+ // Act
+ string fullChildName = ModelBinderUtil.CreatePropertyModelName("", "childName");
+
+ // Assert
+ Assert.Equal("childName", fullChildName);
+ }
+
+ [Fact]
+ public void GetPossibleBinderInstance_Match_ReturnsBinder()
+ {
+ // Act
+ IExtensibleModelBinder binder = ModelBinderUtil.GetPossibleBinderInstance(typeof(List<int>), typeof(List<>), typeof(SampleGenericBinder<>));
+
+ // Assert
+ Assert.IsType<SampleGenericBinder<int>>(binder);
+ }
+
+ [Fact]
+ public void GetPossibleBinderInstance_NoMatch_ReturnsNull()
+ {
+ // Act
+ IExtensibleModelBinder binder = ModelBinderUtil.GetPossibleBinderInstance(typeof(ArraySegment<int>), typeof(List<>), typeof(SampleGenericBinder<>));
+
+ // Assert
+ Assert.Null(binder);
+ }
+
+ [Fact]
+ public void RawValueToObjectArray_RawValueIsEnumerable_ReturnsInputAsArray()
+ {
+ // Assert
+ List<int> original = new List<int> { 1, 2, 3, 4 };
+
+ // Act
+ object[] retVal = ModelBinderUtil.RawValueToObjectArray(original);
+
+ // Assert
+ Assert.Equal(new object[] { 1, 2, 3, 4 }, retVal);
+ }
+
+ [Fact]
+ public void RawValueToObjectArray_RawValueIsObject_WrapsObjectInSingleElementArray()
+ {
+ // Act
+ object[] retVal = ModelBinderUtil.RawValueToObjectArray(42);
+
+ // Assert
+ Assert.Equal(new object[] { 42 }, retVal);
+ }
+
+ [Fact]
+ public void RawValueToObjectArray_RawValueIsObjectArray_ReturnsInputInstance()
+ {
+ // Assert
+ object[] original = new object[2];
+
+ // Act
+ object[] retVal = ModelBinderUtil.RawValueToObjectArray(original);
+
+ // Assert
+ Assert.Same(original, retVal);
+ }
+
+ [Fact]
+ public void RawValueToObjectArray_RawValueIsString_WrapsStringInSingleElementArray()
+ {
+ // Act
+ object[] retVal = ModelBinderUtil.RawValueToObjectArray("hello");
+
+ // Assert
+ Assert.Equal(new object[] { "hello" }, retVal);
+ }
+
+ [Fact]
+ public void ReplaceEmptyStringWithNull_ConvertEmptyStringToNullDisabled_ModelIsEmptyString_LeavesModelAlone()
+ {
+ // Arrange
+ ModelMetadata modelMetadata = GetMetadata(typeof(string));
+ modelMetadata.ConvertEmptyStringToNull = false;
+
+ // Act
+ object model = "";
+ ModelBinderUtil.ReplaceEmptyStringWithNull(modelMetadata, ref model);
+
+ // Assert
+ Assert.Equal("", model);
+ }
+
+ [Fact]
+ public void ReplaceEmptyStringWithNull_ConvertEmptyStringToNullEnabled_ModelIsEmptyString_ReplacesModelWithNull()
+ {
+ // Arrange
+ ModelMetadata modelMetadata = GetMetadata(typeof(string));
+ modelMetadata.ConvertEmptyStringToNull = true;
+
+ // Act
+ object model = "";
+ ModelBinderUtil.ReplaceEmptyStringWithNull(modelMetadata, ref model);
+
+ // Assert
+ Assert.Null(model);
+ }
+
+ [Fact]
+ public void ReplaceEmptyStringWithNull_ConvertEmptyStringToNullEnabled_ModelIsWhitespaceString_ReplacesModelWithNull()
+ {
+ // Arrange
+ ModelMetadata modelMetadata = GetMetadata(typeof(string));
+ modelMetadata.ConvertEmptyStringToNull = true;
+
+ // Act
+ object model = " "; // whitespace
+ ModelBinderUtil.ReplaceEmptyStringWithNull(modelMetadata, ref model);
+
+ // Assert
+ Assert.Null(model);
+ }
+
+ [Fact]
+ public void ReplaceEmptyStringWithNull_ConvertEmptyStringToNullDisabled_ModelIsNotEmptyString_LeavesModelAlone()
+ {
+ // Arrange
+ ModelMetadata modelMetadata = GetMetadata(typeof(string));
+ modelMetadata.ConvertEmptyStringToNull = true;
+
+ // Act
+ object model = 42;
+ ModelBinderUtil.ReplaceEmptyStringWithNull(modelMetadata, ref model);
+
+ // Assert
+ Assert.Equal(42, model);
+ }
+
+ [Fact]
+ public void ValidateBindingContext_SuccessWithNonNullModel()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = GetMetadata(typeof(string))
+ };
+ bindingContext.ModelMetadata.Model = "hello!";
+
+ // Act
+ ModelBinderUtil.ValidateBindingContext(bindingContext, typeof(string), false);
+
+ // Assert
+ // Nothing to do - if we got this far without throwing, the test succeeded
+ }
+
+ [Fact]
+ public void ValidateBindingContext_SuccessWithNullModel()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = GetMetadata(typeof(string))
+ };
+
+ // Act
+ ModelBinderUtil.ValidateBindingContext(bindingContext, typeof(string), true);
+
+ // Assert
+ // Nothing to do - if we got this far without throwing, the test succeeded
+ }
+
+ [Fact]
+ public void ValidateBindingContextThrowsIfBindingContextIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { ModelBinderUtil.ValidateBindingContext(null, typeof(string), true); }, "bindingContext");
+ }
+
+ [Fact]
+ public void ValidateBindingContextThrowsIfModelInstanceIsWrongType()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = GetMetadata(typeof(string))
+ };
+ bindingContext.ModelMetadata.Model = 42;
+
+ // Act & assert
+ Assert.Throws<ArgumentException>(
+ delegate { ModelBinderUtil.ValidateBindingContext(bindingContext, typeof(string), true); },
+ @"The binding context has a Model of type 'System.Int32', but this binder can only operate on models of type 'System.String'.
+Parameter name: bindingContext");
+ }
+
+ [Fact]
+ public void ValidateBindingContextThrowsIfModelIsNullButCannotBe()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = GetMetadata(typeof(string))
+ };
+
+ // Act & assert
+ Assert.Throws<ArgumentException>(
+ delegate { ModelBinderUtil.ValidateBindingContext(bindingContext, typeof(string), false); },
+ @"The binding context has a null Model, but this binder requires a non-null model of type 'System.String'.
+Parameter name: bindingContext");
+ }
+
+ [Fact]
+ public void ValidateBindingContextThrowsIfModelMetadataIsNull()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext();
+
+ // Act & assert
+ Assert.Throws<ArgumentException>(
+ delegate { ModelBinderUtil.ValidateBindingContext(bindingContext, typeof(string), true); },
+ @"The binding context cannot have a null ModelMetadata.
+Parameter name: bindingContext");
+ }
+
+ [Fact]
+ public void ValidateBindingContextThrowsIfModelTypeIsWrong()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = GetMetadata(typeof(object))
+ };
+
+ // Act & assert
+ Assert.Throws<ArgumentException>(
+ delegate { ModelBinderUtil.ValidateBindingContext(bindingContext, typeof(string), true); },
+ @"The binding context has a ModelType of 'System.Object', but this binder can only operate on models of type 'System.String'.
+Parameter name: bindingContext");
+ }
+
+ private static ModelMetadata GetMetadata(Type modelType)
+ {
+ EmptyModelMetadataProvider provider = new EmptyModelMetadataProvider();
+ return provider.GetMetadataForType(null, modelType);
+ }
+
+ private class SampleGenericBinder<T> : IExtensibleModelBinder
+ {
+ public bool BindModel(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext)
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ModelValidationNodeTest.cs b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ModelValidationNodeTest.cs
new file mode 100644
index 00000000..893e8e85
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/ModelValidationNodeTest.cs
@@ -0,0 +1,389 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Web.Mvc;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace Microsoft.Web.Mvc.ModelBinding.Test
+{
+ public class ModelValidationNodeTest
+ {
+ [Fact]
+ public void ConstructorSetsCollectionInstance()
+ {
+ // Arrange
+ ModelMetadata metadata = GetModelMetadata();
+ string modelStateKey = "someKey";
+ ModelValidationNode[] childNodes = new[]
+ {
+ new ModelValidationNode(metadata, "someKey0"),
+ new ModelValidationNode(metadata, "someKey1")
+ };
+
+ // Act
+ ModelValidationNode node = new ModelValidationNode(metadata, modelStateKey, childNodes);
+
+ // Assert
+ Assert.Equal(childNodes, node.ChildNodes.ToArray());
+ }
+
+ [Fact]
+ public void ConstructorThrowsIfModelMetadataIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new ModelValidationNode(null, "someKey"); }, "modelMetadata");
+ }
+
+ [Fact]
+ public void ConstructorThrowsIfModelStateKeyIsNull()
+ {
+ // Arrange
+ ModelMetadata metadata = GetModelMetadata();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new ModelValidationNode(metadata, null); }, "modelStateKey");
+ }
+
+ [Fact]
+ public void PropertiesAreSet()
+ {
+ // Arrange
+ ModelMetadata metadata = GetModelMetadata();
+ string modelStateKey = "someKey";
+
+ // Act
+ ModelValidationNode node = new ModelValidationNode(metadata, modelStateKey);
+
+ // Assert
+ Assert.Equal(metadata, node.ModelMetadata);
+ Assert.Equal(modelStateKey, node.ModelStateKey);
+ Assert.NotNull(node.ChildNodes);
+ Assert.Empty(node.ChildNodes);
+ }
+
+ [Fact]
+ public void CombineWith()
+ {
+ // Arrange
+ List<string> log = new List<string>();
+
+ ModelValidationNode[] allChildNodes = new[]
+ {
+ new ModelValidationNode(GetModelMetadata(), "key1"),
+ new ModelValidationNode(GetModelMetadata(), "key2"),
+ new ModelValidationNode(GetModelMetadata(), "key3"),
+ };
+
+ ModelValidationNode parentNode1 = new ModelValidationNode(GetModelMetadata(), "parent1");
+ parentNode1.ChildNodes.Add(allChildNodes[0]);
+ parentNode1.Validating += delegate { log.Add("Validating parent1."); };
+ parentNode1.Validated += delegate { log.Add("Validated parent1."); };
+
+ ModelValidationNode parentNode2 = new ModelValidationNode(GetModelMetadata(), "parent2");
+ parentNode2.ChildNodes.Add(allChildNodes[1]);
+ parentNode2.ChildNodes.Add(allChildNodes[2]);
+ parentNode2.Validating += delegate { log.Add("Validating parent2."); };
+ parentNode2.Validated += delegate { log.Add("Validated parent2."); };
+
+ // Act
+ parentNode1.CombineWith(parentNode2);
+ parentNode1.Validate(new ControllerContext { Controller = new EmptyController() });
+
+ // Assert
+ Assert.Equal(new[] { "Validating parent1.", "Validating parent2.", "Validated parent1.", "Validated parent2." }, log.ToArray());
+ Assert.Equal(allChildNodes, parentNode1.ChildNodes.ToArray());
+ }
+
+ [Fact]
+ public void CombineWith_OtherNodeIsSuppressed_DoesNothing()
+ {
+ // Arrange
+ List<string> log = new List<string>();
+
+ ModelValidationNode[] allChildNodes = new[]
+ {
+ new ModelValidationNode(GetModelMetadata(), "key1"),
+ new ModelValidationNode(GetModelMetadata(), "key2"),
+ new ModelValidationNode(GetModelMetadata(), "key3"),
+ };
+
+ ModelValidationNode[] expectedChildNodes = new[]
+ {
+ allChildNodes[0]
+ };
+
+ ModelValidationNode parentNode1 = new ModelValidationNode(GetModelMetadata(), "parent1");
+ parentNode1.ChildNodes.Add(allChildNodes[0]);
+ parentNode1.Validating += delegate { log.Add("Validating parent1."); };
+ parentNode1.Validated += delegate { log.Add("Validated parent1."); };
+
+ ModelValidationNode parentNode2 = new ModelValidationNode(GetModelMetadata(), "parent2");
+ parentNode2.ChildNodes.Add(allChildNodes[1]);
+ parentNode2.ChildNodes.Add(allChildNodes[2]);
+ parentNode2.Validating += delegate { log.Add("Validating parent2."); };
+ parentNode2.Validated += delegate { log.Add("Validated parent2."); };
+ parentNode2.SuppressValidation = true;
+
+ // Act
+ parentNode1.CombineWith(parentNode2);
+ parentNode1.Validate(new ControllerContext { Controller = new EmptyController() });
+
+ // Assert
+ Assert.Equal(new[] { "Validating parent1.", "Validated parent1." }, log.ToArray());
+ Assert.Equal(expectedChildNodes, parentNode1.ChildNodes.ToArray());
+ }
+
+ [Fact]
+ public void Validate_Ordering()
+ {
+ // Proper order of invocation:
+ // 1. OnValidating()
+ // 2. Child validators
+ // 3. This validator
+ // 4. OnValidated()
+
+ // Arrange
+ List<string> log = new List<string>();
+ LoggingDataErrorInfoModel model = new LoggingDataErrorInfoModel(log);
+ ModelMetadata modelMetadata = GetModelMetadata(model);
+
+ ControllerContext controllerContext = new ControllerContext
+ {
+ Controller = new EmptyController()
+ };
+ ModelValidationNode node = new ModelValidationNode(modelMetadata, "theKey");
+
+ ModelMetadata childMetadata = new EmptyModelMetadataProvider().GetMetadataForProperty(() => model, model.GetType(), "ValidStringProperty");
+ node.ChildNodes.Add(new ModelValidationNode(childMetadata, "theKey.ValidStringProperty"));
+
+ node.Validating += delegate { log.Add("In OnValidating()"); };
+ node.Validated += delegate { log.Add("In OnValidated()"); };
+
+ // Act
+ node.Validate(controllerContext);
+
+ // Assert
+ Assert.Equal(new[] { "In OnValidating()", "In IDataErrorInfo.get_Item('ValidStringProperty')", "In IDataErrorInfo.get_Error()", "In OnValidated()" }, log.ToArray());
+ }
+
+ [Fact]
+ public void Validate_PassesNullContainerInstanceIfCannotBeConvertedToProperType()
+ {
+ // Arrange
+ List<string> log1 = new List<string>();
+ LoggingDataErrorInfoModel model1 = new LoggingDataErrorInfoModel(log1);
+ ModelMetadata modelMetadata1 = GetModelMetadata(model1);
+
+ List<string> log2 = new List<string>();
+ LoggingDataErrorInfoModel model2 = new LoggingDataErrorInfoModel(log2);
+ ModelMetadata modelMetadata2 = GetModelMetadata(model2);
+
+ ControllerContext controllerContext = new ControllerContext
+ {
+ Controller = new EmptyController()
+ };
+ ModelValidationNode node = new ModelValidationNode(modelMetadata1, "theKey");
+ node.ChildNodes.Add(new ModelValidationNode(modelMetadata2, "theKey.SomeProperty"));
+
+ // Act
+ node.Validate(controllerContext);
+
+ // Assert
+ Assert.Equal(new[] { "In IDataErrorInfo.get_Error()" }, log1.ToArray());
+ Assert.Equal(new[] { "In IDataErrorInfo.get_Error()" }, log2.ToArray());
+ }
+
+ [Fact]
+ public void Validate_SkipsRemainingValidationIfModelStateIsInvalid()
+ {
+ // Because a property validator fails, the model validator shouldn't run
+
+ // Arrange
+ List<string> log = new List<string>();
+ LoggingDataErrorInfoModel model = new LoggingDataErrorInfoModel(log);
+ ModelMetadata modelMetadata = GetModelMetadata(model);
+
+ ControllerContext controllerContext = new ControllerContext
+ {
+ Controller = new EmptyController()
+ };
+ ModelValidationNode node = new ModelValidationNode(modelMetadata, "theKey");
+
+ ModelMetadata childMetadata = new EmptyModelMetadataProvider().GetMetadataForProperty(() => model, model.GetType(), "InvalidStringProperty");
+ node.ChildNodes.Add(new ModelValidationNode(childMetadata, "theKey.InvalidStringProperty"));
+
+ node.Validating += delegate { log.Add("In OnValidating()"); };
+ node.Validated += delegate { log.Add("In OnValidated()"); };
+
+ // Act
+ node.Validate(controllerContext);
+
+ // Assert
+ Assert.Equal(new[] { "In OnValidating()", "In IDataErrorInfo.get_Item('InvalidStringProperty')", "In OnValidated()" }, log.ToArray());
+ Assert.Equal("Sample error message", controllerContext.Controller.ViewData.ModelState["theKey.InvalidStringProperty"].Errors[0].ErrorMessage);
+ }
+
+ [Fact]
+ public void Validate_SkipsValidationIfHandlerCancels()
+ {
+ // Arrange
+ List<string> log = new List<string>();
+ LoggingDataErrorInfoModel model = new LoggingDataErrorInfoModel(log);
+ ModelMetadata modelMetadata = GetModelMetadata(model);
+
+ ControllerContext controllerContext = new ControllerContext
+ {
+ Controller = new EmptyController()
+ };
+ ModelValidationNode node = new ModelValidationNode(modelMetadata, "theKey");
+
+ node.Validating += (sender, e) =>
+ {
+ log.Add("In OnValidating()");
+ e.Cancel = true;
+ };
+ node.Validated += delegate { log.Add("In OnValidated()"); };
+
+ // Act
+ node.Validate(controllerContext);
+
+ // Assert
+ Assert.Equal(new[] { "In OnValidating()" }, log.ToArray());
+ }
+
+ [Fact]
+ public void Validate_SkipsValidationIfSuppressed()
+ {
+ // Arrange
+ List<string> log = new List<string>();
+ LoggingDataErrorInfoModel model = new LoggingDataErrorInfoModel(log);
+ ModelMetadata modelMetadata = GetModelMetadata(model);
+
+ ControllerContext controllerContext = new ControllerContext
+ {
+ Controller = new EmptyController()
+ };
+ ModelValidationNode node = new ModelValidationNode(modelMetadata, "theKey")
+ {
+ SuppressValidation = true
+ };
+
+ node.Validating += (sender, e) => { log.Add("In OnValidating()"); };
+ node.Validated += delegate { log.Add("In OnValidated()"); };
+
+ // Act
+ node.Validate(controllerContext);
+
+ // Assert
+ Assert.Empty(log);
+ }
+
+ [Fact]
+ public void Validate_ThrowsIfControllerContextIsNull()
+ {
+ // Arrange
+ ModelValidationNode node = new ModelValidationNode(GetModelMetadata(), "someKey");
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { node.Validate(null); }, "controllerContext");
+ }
+
+ [Fact]
+ public void Validate_ValidateAllProperties_AddsValidationErrors()
+ {
+ // Arrange
+ ValidateAllPropertiesModel model = new ValidateAllPropertiesModel
+ {
+ RequiredString = null /* error */,
+ RangedInt = 0 /* error */,
+ ValidString = "dog"
+ };
+
+ ModelMetadata modelMetadata = GetModelMetadata(model);
+ ControllerContext controllerContext = new ControllerContext
+ {
+ Controller = new EmptyController()
+ };
+ ModelValidationNode node = new ModelValidationNode(modelMetadata, "theKey")
+ {
+ ValidateAllProperties = true
+ };
+
+ controllerContext.Controller.ViewData.ModelState.AddModelError("theKey.RequiredString.Dummy", "existing Error Text");
+
+ // Act
+ node.Validate(controllerContext);
+
+ // Assert
+ Assert.Null(controllerContext.Controller.ViewData.ModelState["theKey.RequiredString"]);
+ Assert.Equal("existing Error Text", controllerContext.Controller.ViewData.ModelState["theKey.RequiredString.Dummy"].Errors[0].ErrorMessage);
+ Assert.Equal("The field RangedInt must be between 10 and 30.", controllerContext.Controller.ViewData.ModelState["theKey.RangedInt"].Errors[0].ErrorMessage);
+ Assert.Null(controllerContext.Controller.ViewData.ModelState["theKey.ValidString"]);
+ Assert.Null(controllerContext.Controller.ViewData.ModelState["theKey"]);
+ }
+
+ private static ModelMetadata GetModelMetadata()
+ {
+ EmptyModelMetadataProvider provider = new EmptyModelMetadataProvider();
+ return provider.GetMetadataForType(null, typeof(object));
+ }
+
+ private static ModelMetadata GetModelMetadata(object o)
+ {
+ DataAnnotationsModelMetadataProvider provider = new DataAnnotationsModelMetadataProvider();
+ return provider.GetMetadataForType(() => o, o.GetType());
+ }
+
+ private sealed class EmptyController : Controller
+ {
+ }
+
+ private sealed class LoggingDataErrorInfoModel : IDataErrorInfo
+ {
+ private readonly IList<string> _log;
+
+ public LoggingDataErrorInfoModel(IList<string> log)
+ {
+ _log = log;
+ }
+
+ string IDataErrorInfo.Error
+ {
+ get
+ {
+ _log.Add("In IDataErrorInfo.get_Error()");
+ return null;
+ }
+ }
+
+ string IDataErrorInfo.this[string columnName]
+ {
+ get
+ {
+ _log.Add("In IDataErrorInfo.get_Item('" + columnName + "')");
+ return (columnName == "ValidStringProperty") ? null : "Sample error message";
+ }
+ }
+
+ public string ValidStringProperty { get; set; }
+ public string InvalidStringProperty { get; set; }
+ }
+
+ private class ValidateAllPropertiesModel
+ {
+ [Required]
+ public string RequiredString { get; set; }
+
+ [Range(10, 30)]
+ public int RangedInt { get; set; }
+
+ [RegularExpression("dog")]
+ public string ValidString { get; set; }
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/MutableObjectModelBinderProviderTest.cs b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/MutableObjectModelBinderProviderTest.cs
new file mode 100644
index 00000000..f4c06ac5
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/MutableObjectModelBinderProviderTest.cs
@@ -0,0 +1,76 @@
+using System.Web.Mvc;
+using Microsoft.Web.UnitTestUtil;
+using Xunit;
+
+namespace Microsoft.Web.Mvc.ModelBinding.Test
+{
+ public class MutableObjectModelBinderProviderTest
+ {
+ [Fact]
+ public void GetBinder_NoPrefixInValueProvider_ReturnsNull()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => 42, typeof(int)),
+ ModelName = "foo",
+ ValueProvider = new SimpleValueProvider()
+ };
+
+ MutableObjectModelBinderProvider binderProvider = new MutableObjectModelBinderProvider();
+
+ // Act
+ IExtensibleModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(binder);
+ }
+
+ [Fact]
+ public void GetBinder_PrefixInValueProvider_ReturnsBinder()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => 42, typeof(int)),
+ ModelName = "foo",
+ ValueProvider = new SimpleValueProvider
+ {
+ { "foo.bar", "someValue" }
+ }
+ };
+
+ MutableObjectModelBinderProvider binderProvider = new MutableObjectModelBinderProvider();
+
+ // Act
+ IExtensibleModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.NotNull(binder);
+ Assert.IsType<MutableObjectModelBinder>(binder);
+ }
+
+ [Fact]
+ public void GetBinder_TypeIsComplexModelDto_ReturnsNull()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(ComplexModelDto)),
+ ModelName = "foo",
+ ValueProvider = new SimpleValueProvider
+ {
+ { "foo.bar", "someValue" }
+ }
+ };
+
+ MutableObjectModelBinderProvider binderProvider = new MutableObjectModelBinderProvider();
+
+ // Act
+ IExtensibleModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(binder);
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/MutableObjectModelBinderTest.cs b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/MutableObjectModelBinderTest.cs
new file mode 100644
index 00000000..ddaafc08
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/MutableObjectModelBinderTest.cs
@@ -0,0 +1,769 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Web.Mvc;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace Microsoft.Web.Mvc.ModelBinding.Test
+{
+ public class MutableObjectModelBinderTest
+ {
+ [Fact]
+ public void BindModel()
+ {
+ // Arrange
+ ControllerContext controllerContext = new ControllerContext();
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelBinderProviders = new ModelBinderProviderCollection(),
+ ModelMetadata = GetMetadataForObject(new Person()),
+ ModelName = "someName"
+ };
+
+ Mock<IExtensibleModelBinder> mockDtoBinder = new Mock<IExtensibleModelBinder>();
+ mockDtoBinder
+ .Setup(o => o.BindModel(controllerContext, It.IsAny<ExtensibleModelBindingContext>()))
+ .Returns(
+ delegate(ControllerContext cc, ExtensibleModelBindingContext mbc2)
+ {
+ return true; // just return the DTO unchanged
+ });
+ bindingContext.ModelBinderProviders.RegisterBinderForType(typeof(ComplexModelDto), mockDtoBinder.Object, true /* suppressPrefixCheck */);
+
+ Mock<TestableMutableObjectModelBinder> mockTestableBinder = new Mock<TestableMutableObjectModelBinder> { CallBase = true };
+ mockTestableBinder.Setup(o => o.EnsureModelPublic(controllerContext, bindingContext)).Verifiable();
+ mockTestableBinder.Setup(o => o.GetMetadataForPropertiesPublic(controllerContext, bindingContext)).Returns(new ModelMetadata[0]).Verifiable();
+ TestableMutableObjectModelBinder testableBinder = mockTestableBinder.Object;
+ testableBinder.MetadataProvider = new DataAnnotationsModelMetadataProvider();
+
+ // Act
+ bool retValue = testableBinder.BindModel(controllerContext, bindingContext);
+
+ // Assert
+ Assert.True(retValue);
+ Assert.IsType<Person>(bindingContext.Model);
+ Assert.True(bindingContext.ValidationNode.ValidateAllProperties);
+ mockTestableBinder.Verify();
+ }
+
+ [Fact]
+ public void CanUpdateProperty_HasPublicSetter_ReturnsTrue()
+ {
+ // Arrange
+ ModelMetadata propertyMetadata = GetMetadataForCanUpdateProperty("ReadWriteString");
+
+ // Act
+ bool canUpdate = MutableObjectModelBinder.CanUpdatePropertyInternal(propertyMetadata);
+
+ // Assert
+ Assert.True(canUpdate);
+ }
+
+ [Fact]
+ public void CanUpdateProperty_ReadOnlyArray_ReturnsFalse()
+ {
+ // Arrange
+ ModelMetadata propertyMetadata = GetMetadataForCanUpdateProperty("ReadOnlyArray");
+
+ // Act
+ bool canUpdate = MutableObjectModelBinder.CanUpdatePropertyInternal(propertyMetadata);
+
+ // Assert
+ Assert.False(canUpdate);
+ }
+
+ [Fact]
+ public void CanUpdateProperty_ReadOnlyReferenceTypeNotBlacklisted_ReturnsTrue()
+ {
+ // Arrange
+ ModelMetadata propertyMetadata = GetMetadataForCanUpdateProperty("ReadOnlyObject");
+
+ // Act
+ bool canUpdate = MutableObjectModelBinder.CanUpdatePropertyInternal(propertyMetadata);
+
+ // Assert
+ Assert.True(canUpdate);
+ }
+
+ [Fact]
+ public void CanUpdateProperty_ReadOnlyString_ReturnsFalse()
+ {
+ // Arrange
+ ModelMetadata propertyMetadata = GetMetadataForCanUpdateProperty("ReadOnlyString");
+
+ // Act
+ bool canUpdate = MutableObjectModelBinder.CanUpdatePropertyInternal(propertyMetadata);
+
+ // Assert
+ Assert.False(canUpdate);
+ }
+
+ [Fact]
+ public void CanUpdateProperty_ReadOnlyValueType_ReturnsFalse()
+ {
+ // Arrange
+ ModelMetadata propertyMetadata = GetMetadataForCanUpdateProperty("ReadOnlyInt");
+
+ // Act
+ bool canUpdate = MutableObjectModelBinder.CanUpdatePropertyInternal(propertyMetadata);
+
+ // Assert
+ Assert.False(canUpdate);
+ }
+
+ [Fact]
+ public void CreateModel()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = GetMetadataForType(typeof(Person))
+ };
+
+ TestableMutableObjectModelBinder testableBinder = new TestableMutableObjectModelBinder();
+
+ // Act
+ object retModel = testableBinder.CreateModelPublic(null, bindingContext);
+
+ // Assert
+ Assert.IsType<Person>(retModel);
+ }
+
+ [Fact]
+ public void EnsureModel_ModelIsNotNull_DoesNothing()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = GetMetadataForObject(new Person())
+ };
+
+ Mock<TestableMutableObjectModelBinder> mockTestableBinder = new Mock<TestableMutableObjectModelBinder> { CallBase = true };
+ TestableMutableObjectModelBinder testableBinder = mockTestableBinder.Object;
+
+ // Act
+ object originalModel = bindingContext.Model;
+ testableBinder.EnsureModelPublic(null, bindingContext);
+ object newModel = bindingContext.Model;
+
+ // Assert
+ Assert.Same(originalModel, newModel);
+ mockTestableBinder.Verify(o => o.CreateModelPublic(null, bindingContext), Times.Never());
+ }
+
+ [Fact]
+ public void EnsureModel_ModelIsNull_CallsCreateModel()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = GetMetadataForType(typeof(Person))
+ };
+
+ Mock<TestableMutableObjectModelBinder> mockTestableBinder = new Mock<TestableMutableObjectModelBinder> { CallBase = true };
+ mockTestableBinder.Setup(o => o.CreateModelPublic(null, bindingContext)).Returns(new Person()).Verifiable();
+ TestableMutableObjectModelBinder testableBinder = mockTestableBinder.Object;
+
+ // Act
+ object originalModel = bindingContext.Model;
+ testableBinder.EnsureModelPublic(null, bindingContext);
+ object newModel = bindingContext.Model;
+
+ // Assert
+ Assert.Null(originalModel);
+ Assert.IsType<Person>(newModel);
+ mockTestableBinder.Verify();
+ }
+
+ [Fact]
+ public void GetMetadataForProperties_WithBindAttribute()
+ {
+ // Arrange
+ string[] expectedPropertyNames = new[] { "FirstName", "LastName" };
+
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = GetMetadataForType(typeof(PersonWithBindExclusion))
+ };
+
+ TestableMutableObjectModelBinder testableBinder = new TestableMutableObjectModelBinder();
+
+ // Act
+ IEnumerable<ModelMetadata> propertyMetadatas = testableBinder.GetMetadataForPropertiesPublic(null, bindingContext);
+ string[] returnedPropertyNames = propertyMetadatas.Select(o => o.PropertyName).ToArray();
+
+ // Assert
+ Assert.Equal(expectedPropertyNames, returnedPropertyNames);
+ }
+
+ [Fact]
+ public void GetMetadataForProperties_WithoutBindAttribute()
+ {
+ // Arrange
+ string[] expectedPropertyNames = new[] { "DateOfBirth", "DateOfDeath", "ValueTypeRequired", "FirstName", "LastName", "PropertyWithDefaultValue" };
+
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = GetMetadataForType(typeof(Person))
+ };
+
+ TestableMutableObjectModelBinder testableBinder = new TestableMutableObjectModelBinder();
+
+ // Act
+ IEnumerable<ModelMetadata> propertyMetadatas = testableBinder.GetMetadataForPropertiesPublic(null, bindingContext);
+ string[] returnedPropertyNames = propertyMetadatas.Select(o => o.PropertyName).ToArray();
+
+ // Assert
+ Assert.Equal(expectedPropertyNames, returnedPropertyNames);
+ }
+
+ [Fact]
+ public void GetRequiredPropertiesCollection_MixedAttributes()
+ {
+ // Arrange
+ Type modelType = typeof(ModelWithMixedBindingBehaviors);
+
+ // Act
+ HashSet<string> requiredProperties;
+ HashSet<string> skipProperties;
+ MutableObjectModelBinder.GetRequiredPropertiesCollection(modelType, out requiredProperties, out skipProperties);
+
+ // Assert
+ Assert.Equal(new[] { "Required" }, requiredProperties.ToArray());
+ Assert.Equal(new[] { "Never" }, skipProperties.ToArray());
+ }
+
+ [Fact]
+ public void NullCheckFailedHandler_ModelStateAlreadyInvalid_DoesNothing()
+ {
+ // Arrange
+ ControllerContext controllerContext = new ControllerContext
+ {
+ Controller = new EmptyController()
+ };
+ controllerContext.Controller.ViewData.ModelState.AddModelError("foo.bar", "Some existing error.");
+
+ ModelMetadata modelMetadata = GetMetadataForType(typeof(Person));
+ ModelValidationNode validationNode = new ModelValidationNode(modelMetadata, "foo");
+ ModelValidatedEventArgs e = new ModelValidatedEventArgs(controllerContext, null /* parentNode */);
+
+ // Act
+ EventHandler<ModelValidatedEventArgs> handler = MutableObjectModelBinder.CreateNullCheckFailedHandler(controllerContext, modelMetadata, null /* incomingValue */);
+ handler(validationNode, e);
+
+ // Assert
+ Assert.False(controllerContext.Controller.ViewData.ModelState.ContainsKey("foo"));
+ }
+
+ [Fact]
+ public void NullCheckFailedHandler_ModelStateValid_AddsErrorString()
+ {
+ // Arrange
+ ControllerContext controllerContext = new ControllerContext
+ {
+ Controller = new EmptyController()
+ };
+
+ ModelMetadata modelMetadata = GetMetadataForType(typeof(Person));
+ ModelValidationNode validationNode = new ModelValidationNode(modelMetadata, "foo");
+ ModelValidatedEventArgs e = new ModelValidatedEventArgs(controllerContext, null /* parentNode */);
+
+ // Act
+ EventHandler<ModelValidatedEventArgs> handler = MutableObjectModelBinder.CreateNullCheckFailedHandler(controllerContext, modelMetadata, null /* incomingValue */);
+ handler(validationNode, e);
+
+ // Assert
+ Assert.True(controllerContext.Controller.ViewData.ModelState.ContainsKey("foo"));
+ Assert.Equal("A value is required.", controllerContext.Controller.ViewData.ModelState["foo"].Errors[0].ErrorMessage);
+ }
+
+ [Fact]
+ public void NullCheckFailedHandler_ModelStateValid_CallbackReturnsNull_DoesNothing()
+ {
+ // Arrange
+ ControllerContext controllerContext = new ControllerContext
+ {
+ Controller = new EmptyController()
+ };
+
+ ModelMetadata modelMetadata = GetMetadataForType(typeof(Person));
+ ModelValidationNode validationNode = new ModelValidationNode(modelMetadata, "foo");
+ ModelValidatedEventArgs e = new ModelValidatedEventArgs(controllerContext, null /* parentNode */);
+
+ // Act
+ ModelBinderErrorMessageProvider originalProvider = ModelBinderConfig.ValueRequiredErrorMessageProvider;
+ try
+ {
+ ModelBinderConfig.ValueRequiredErrorMessageProvider = delegate { return null; };
+ EventHandler<ModelValidatedEventArgs> handler = MutableObjectModelBinder.CreateNullCheckFailedHandler(controllerContext, modelMetadata, null /* incomingValue */);
+ handler(validationNode, e);
+ }
+ finally
+ {
+ ModelBinderConfig.ValueRequiredErrorMessageProvider = originalProvider;
+ }
+
+ // Assert
+ Assert.True(controllerContext.Controller.ViewData.ModelState.IsValid);
+ }
+
+ [Fact]
+ public void ProcessDto_BindRequiredFieldMissing_Throws()
+ {
+ // Arrange
+ ModelWithBindRequired model = new ModelWithBindRequired
+ {
+ Name = "original value",
+ Age = -20
+ };
+
+ ModelMetadata containerMetadata = GetMetadataForObject(model);
+
+ ControllerContext controllerContext = new ControllerContext();
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = containerMetadata,
+ ModelName = "theModel"
+ };
+ ComplexModelDto dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties);
+
+ ModelMetadata nameProperty = dto.PropertyMetadata.Single(o => o.PropertyName == "Name");
+ dto.Results[nameProperty] = new ComplexModelDtoResult("John Doe", new ModelValidationNode(nameProperty, ""));
+
+ TestableMutableObjectModelBinder testableBinder = new TestableMutableObjectModelBinder();
+
+ // Act & assert
+ Assert.Throws<InvalidOperationException>(
+ delegate { testableBinder.ProcessDto(controllerContext, bindingContext, dto); },
+ @"A value for 'theModel.Age' is required but was not present in the request.");
+
+ Assert.Equal("original value", model.Name);
+ Assert.Equal(-20, model.Age);
+ }
+
+ [Fact]
+ public void ProcessDto_Success()
+ {
+ // Arrange
+ DateTime dob = new DateTime(2001, 1, 1);
+ Person model = new Person
+ {
+ DateOfBirth = dob
+ };
+ ModelMetadata containerMetadata = GetMetadataForObject(model);
+
+ ControllerContext controllerContext = new ControllerContext();
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = containerMetadata
+ };
+ ComplexModelDto dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties);
+
+ ModelMetadata firstNameProperty = dto.PropertyMetadata.Single(o => o.PropertyName == "FirstName");
+ dto.Results[firstNameProperty] = new ComplexModelDtoResult("John", new ModelValidationNode(firstNameProperty, ""));
+ ModelMetadata lastNameProperty = dto.PropertyMetadata.Single(o => o.PropertyName == "LastName");
+ dto.Results[lastNameProperty] = new ComplexModelDtoResult("Doe", new ModelValidationNode(lastNameProperty, ""));
+ ModelMetadata dobProperty = dto.PropertyMetadata.Single(o => o.PropertyName == "DateOfBirth");
+ dto.Results[dobProperty] = null;
+
+ TestableMutableObjectModelBinder testableBinder = new TestableMutableObjectModelBinder();
+
+ // Act
+ testableBinder.ProcessDto(controllerContext, bindingContext, dto);
+
+ // Assert
+ Assert.Equal("John", model.FirstName);
+ Assert.Equal("Doe", model.LastName);
+ Assert.Equal(dob, model.DateOfBirth);
+ Assert.True(bindingContext.ModelState.IsValid);
+ }
+
+ [Fact]
+ public void SetProperty_PropertyHasDefaultValue_SetsDefaultValue()
+ {
+ // Arrange
+ ControllerContext controllerContext = new ControllerContext
+ {
+ Controller = new EmptyController()
+ };
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = GetMetadataForObject(new Person())
+ };
+
+ ModelMetadata propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "PropertyWithDefaultValue");
+ ModelValidationNode validationNode = new ModelValidationNode(propertyMetadata, "foo");
+ ComplexModelDtoResult dtoResult = new ComplexModelDtoResult(null /* model */, validationNode);
+
+ TestableMutableObjectModelBinder testableBinder = new TestableMutableObjectModelBinder();
+
+ // Act
+ testableBinder.SetPropertyPublic(controllerContext, bindingContext, propertyMetadata, dtoResult);
+
+ // Assert
+ var person = Assert.IsType<Person>(bindingContext.Model);
+ Assert.Equal(123.456m, person.PropertyWithDefaultValue);
+ Assert.True(controllerContext.Controller.ViewData.ModelState.IsValid);
+ }
+
+ [Fact]
+ public void SetProperty_PropertyIsReadOnly_DoesNothing()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = GetMetadataForType(typeof(Person))
+ };
+
+ ModelMetadata propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "NonUpdateableProperty");
+ ModelValidationNode validationNode = new ModelValidationNode(propertyMetadata, "foo");
+ ComplexModelDtoResult dtoResult = new ComplexModelDtoResult(null /* model */, validationNode);
+
+ TestableMutableObjectModelBinder testableBinder = new TestableMutableObjectModelBinder();
+
+ // Act
+ testableBinder.SetPropertyPublic(null, bindingContext, propertyMetadata, dtoResult);
+
+ // Assert
+ // If didn't throw, success!
+ }
+
+ [Fact]
+ public void SetProperty_PropertyIsSettable_CallsSetter()
+ {
+ // Arrange
+ Person model = new Person();
+ ControllerContext controllerContext = new ControllerContext
+ {
+ Controller = new EmptyController()
+ };
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = GetMetadataForObject(model)
+ };
+
+ ModelMetadata propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "DateOfBirth");
+ ModelValidationNode validationNode = new ModelValidationNode(propertyMetadata, "foo");
+ ComplexModelDtoResult dtoResult = new ComplexModelDtoResult(new DateTime(2001, 1, 1), validationNode);
+
+ TestableMutableObjectModelBinder testableBinder = new TestableMutableObjectModelBinder();
+
+ // Act
+ testableBinder.SetPropertyPublic(controllerContext, bindingContext, propertyMetadata, dtoResult);
+
+ // Assert
+ validationNode.Validate(controllerContext);
+ Assert.True(controllerContext.Controller.ViewData.ModelState.IsValid);
+ Assert.Equal(new DateTime(2001, 1, 1), model.DateOfBirth);
+ }
+
+ [Fact]
+ public void SetProperty_PropertyIsSettable_SetterThrows_RecordsError()
+ {
+ // Arrange
+ Person model = new Person
+ {
+ DateOfBirth = new DateTime(1900, 1, 1)
+ };
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = GetMetadataForObject(model)
+ };
+
+ ModelMetadata propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "DateOfDeath");
+ ModelValidationNode validationNode = new ModelValidationNode(propertyMetadata, "foo");
+ ComplexModelDtoResult dtoResult = new ComplexModelDtoResult(new DateTime(1800, 1, 1), validationNode);
+
+ TestableMutableObjectModelBinder testableBinder = new TestableMutableObjectModelBinder();
+
+ // Act
+ testableBinder.SetPropertyPublic(null, bindingContext, propertyMetadata, dtoResult);
+
+ // Assert
+ Assert.Equal(@"Date of death can't be before date of birth.
+Parameter name: value", bindingContext.ModelState["foo"].Errors[0].Exception.Message);
+ }
+
+ [Fact]
+ public void SetProperty_SettingNonNullableValueTypeToNull_RequiredValidatorNotPresent_RegistersValidationCallback()
+ {
+ // Arrange
+ ControllerContext controllerContext = new ControllerContext
+ {
+ Controller = new EmptyController()
+ };
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = GetMetadataForObject(new Person()),
+ };
+
+ ModelMetadata propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "DateOfBirth");
+ ModelValidationNode validationNode = new ModelValidationNode(propertyMetadata, "foo");
+ ComplexModelDtoResult dtoResult = new ComplexModelDtoResult(null /* model */, validationNode);
+
+ TestableMutableObjectModelBinder testableBinder = new TestableMutableObjectModelBinder();
+
+ // Act
+ testableBinder.SetPropertyPublic(controllerContext, bindingContext, propertyMetadata, dtoResult);
+
+ // Assert
+ Assert.True(controllerContext.Controller.ViewData.ModelState.IsValid);
+ validationNode.Validate(controllerContext, bindingContext.ValidationNode);
+ Assert.False(controllerContext.Controller.ViewData.ModelState.IsValid);
+ }
+
+ [Fact]
+ public void SetProperty_SettingNonNullableValueTypeToNull_RequiredValidatorPresent_AddsModelError()
+ {
+ // Arrange
+ ControllerContext controllerContext = new ControllerContext
+ {
+ Controller = new EmptyController()
+ };
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = GetMetadataForObject(new Person()),
+ ModelName = "foo"
+ };
+
+ ModelMetadata propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "ValueTypeRequired");
+ ModelValidationNode validationNode = new ModelValidationNode(propertyMetadata, "foo.ValueTypeRequired");
+ ComplexModelDtoResult dtoResult = new ComplexModelDtoResult(null /* model */, validationNode);
+
+ TestableMutableObjectModelBinder testableBinder = new TestableMutableObjectModelBinder();
+
+ // Act
+ testableBinder.SetPropertyPublic(controllerContext, bindingContext, propertyMetadata, dtoResult);
+
+ // Assert
+ Assert.False(bindingContext.ModelState.IsValid);
+ Assert.Equal("Sample message", bindingContext.ModelState["foo.ValueTypeRequired"].Errors[0].ErrorMessage);
+ }
+
+ [Fact]
+ public void SetProperty_SettingNullableTypeToNull_RequiredValidatorNotPresent_PropertySetterThrows_AddsRequiredMessageString()
+ {
+ // Arrange
+ ControllerContext controllerContext = new ControllerContext
+ {
+ Controller = new EmptyController()
+ };
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = GetMetadataForObject(new ModelWhosePropertySetterThrows()),
+ ModelName = "foo"
+ };
+
+ ModelMetadata propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "NameNoAttribute");
+ ModelValidationNode validationNode = new ModelValidationNode(propertyMetadata, "foo.NameNoAttribute");
+ ComplexModelDtoResult dtoResult = new ComplexModelDtoResult(null /* model */, validationNode);
+
+ TestableMutableObjectModelBinder testableBinder = new TestableMutableObjectModelBinder();
+
+ // Act
+ testableBinder.SetPropertyPublic(controllerContext, bindingContext, propertyMetadata, dtoResult);
+
+ // Assert
+ Assert.False(bindingContext.ModelState.IsValid);
+ Assert.Equal(1, bindingContext.ModelState["foo.NameNoAttribute"].Errors.Count);
+ Assert.Equal(@"This is a different exception.
+Parameter name: value", bindingContext.ModelState["foo.NameNoAttribute"].Errors[0].Exception.Message);
+ }
+
+ [Fact]
+ public void SetProperty_SettingNullableTypeToNull_RequiredValidatorPresent_PropertySetterThrows_AddsRequiredMessageString()
+ {
+ // Arrange
+ ControllerContext controllerContext = new ControllerContext
+ {
+ Controller = new EmptyController()
+ };
+ ExtensibleModelBindingContext bindingContext = new ExtensibleModelBindingContext
+ {
+ ModelMetadata = GetMetadataForObject(new ModelWhosePropertySetterThrows()),
+ ModelName = "foo"
+ };
+
+ ModelMetadata propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "Name");
+ ModelValidationNode validationNode = new ModelValidationNode(propertyMetadata, "foo.Name");
+ ComplexModelDtoResult dtoResult = new ComplexModelDtoResult(null /* model */, validationNode);
+
+ TestableMutableObjectModelBinder testableBinder = new TestableMutableObjectModelBinder();
+
+ // Act
+ testableBinder.SetPropertyPublic(controllerContext, bindingContext, propertyMetadata, dtoResult);
+
+ // Assert
+ Assert.False(bindingContext.ModelState.IsValid);
+ Assert.Equal(1, bindingContext.ModelState["foo.Name"].Errors.Count);
+ Assert.Equal("This message comes from the [Required] attribute.", bindingContext.ModelState["foo.Name"].Errors[0].ErrorMessage);
+ }
+
+ private static ModelMetadata GetMetadataForCanUpdateProperty(string propertyName)
+ {
+ DataAnnotationsModelMetadataProvider metadataProvider = new DataAnnotationsModelMetadataProvider();
+ return metadataProvider.GetMetadataForProperty(null, typeof(MyModelTestingCanUpdateProperty), propertyName);
+ }
+
+ private static ModelMetadata GetMetadataForObject(object o)
+ {
+ DataAnnotationsModelMetadataProvider metadataProvider = new DataAnnotationsModelMetadataProvider();
+ return metadataProvider.GetMetadataForType(() => o, o.GetType());
+ }
+
+ private static ModelMetadata GetMetadataForType(Type t)
+ {
+ DataAnnotationsModelMetadataProvider metadataProvider = new DataAnnotationsModelMetadataProvider();
+ return metadataProvider.GetMetadataForType(null, t);
+ }
+
+ private class Person
+ {
+ private DateTime? _dateOfDeath;
+
+ public DateTime DateOfBirth { get; set; }
+
+ public DateTime? DateOfDeath
+ {
+ get { return _dateOfDeath; }
+ set
+ {
+ if (value < DateOfBirth)
+ {
+ throw new ArgumentOutOfRangeException("value", "Date of death can't be before date of birth.");
+ }
+ _dateOfDeath = value;
+ }
+ }
+
+ [Required(ErrorMessage = "Sample message")]
+ public int ValueTypeRequired { get; set; }
+
+ public string FirstName { get; set; }
+ public string LastName { get; set; }
+ public string NonUpdateableProperty { get; private set; }
+
+ [DefaultValue(typeof(decimal), "123.456")]
+ public decimal PropertyWithDefaultValue { get; set; }
+ }
+
+ private class PersonWithBindExclusion
+ {
+ [BindNever]
+ public DateTime DateOfBirth { get; set; }
+
+ [BindNever]
+ public DateTime? DateOfDeath { get; set; }
+
+ public string FirstName { get; set; }
+ public string LastName { get; set; }
+ public string NonUpdateableProperty { get; private set; }
+ }
+
+ private class ModelWithBindRequired
+ {
+ public string Name { get; set; }
+
+ [BindRequired]
+ public int Age { get; set; }
+ }
+
+ [BindRequired]
+ private class ModelWithMixedBindingBehaviors
+ {
+ public string Required { get; set; }
+
+ [BindNever]
+ public string Never { get; set; }
+
+ [BindingBehavior(BindingBehavior.Optional)]
+ public string Optional { get; set; }
+ }
+
+ private sealed class MyModelTestingCanUpdateProperty
+ {
+ public int ReadOnlyInt { get; private set; }
+ public string ReadOnlyString { get; private set; }
+ public string[] ReadOnlyArray { get; private set; }
+ public object ReadOnlyObject { get; private set; }
+ public string ReadWriteString { get; set; }
+ }
+
+ private sealed class ModelWhosePropertySetterThrows
+ {
+ [Required(ErrorMessage = "This message comes from the [Required] attribute.")]
+ public string Name
+ {
+ get { return null; }
+ set { throw new ArgumentException("This is an exception.", "value"); }
+ }
+
+ public string NameNoAttribute
+ {
+ get { return null; }
+ set { throw new ArgumentException("This is a different exception.", "value"); }
+ }
+ }
+
+ public class TestableMutableObjectModelBinder : MutableObjectModelBinder
+ {
+ public virtual bool CanUpdatePropertyPublic(ModelMetadata propertyMetadata)
+ {
+ return base.CanUpdateProperty(propertyMetadata);
+ }
+
+ protected override bool CanUpdateProperty(ModelMetadata propertyMetadata)
+ {
+ return CanUpdatePropertyPublic(propertyMetadata);
+ }
+
+ public virtual object CreateModelPublic(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext)
+ {
+ return base.CreateModel(controllerContext, bindingContext);
+ }
+
+ protected override object CreateModel(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext)
+ {
+ return CreateModelPublic(controllerContext, bindingContext);
+ }
+
+ public virtual void EnsureModelPublic(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext)
+ {
+ base.EnsureModel(controllerContext, bindingContext);
+ }
+
+ protected override void EnsureModel(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext)
+ {
+ EnsureModelPublic(controllerContext, bindingContext);
+ }
+
+ public virtual IEnumerable<ModelMetadata> GetMetadataForPropertiesPublic(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext)
+ {
+ return base.GetMetadataForProperties(controllerContext, bindingContext);
+ }
+
+ protected override IEnumerable<ModelMetadata> GetMetadataForProperties(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext)
+ {
+ return GetMetadataForPropertiesPublic(controllerContext, bindingContext);
+ }
+
+ public virtual void SetPropertyPublic(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext, ModelMetadata propertyMetadata, ComplexModelDtoResult dtoResult)
+ {
+ base.SetProperty(controllerContext, bindingContext, propertyMetadata, dtoResult);
+ }
+
+ protected override void SetProperty(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext, ModelMetadata propertyMetadata, ComplexModelDtoResult dtoResult)
+ {
+ SetPropertyPublic(controllerContext, bindingContext, propertyMetadata, dtoResult);
+ }
+ }
+
+ private class EmptyController : Controller
+ {
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/SimpleModelBinderProviderTest.cs b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/SimpleModelBinderProviderTest.cs
new file mode 100644
index 00000000..33ab6f42
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/SimpleModelBinderProviderTest.cs
@@ -0,0 +1,152 @@
+using System;
+using System.Web.Mvc;
+using Microsoft.Web.UnitTestUtil;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace Microsoft.Web.Mvc.ModelBinding.Test
+{
+ public class SimpleModelBinderProviderTest
+ {
+ [Fact]
+ public void ConstructorWithFactoryThrowsIfModelBinderFactoryIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new SimpleModelBinderProvider(typeof(object), (Func<IExtensibleModelBinder>)null); }, "modelBinderFactory");
+ }
+
+ [Fact]
+ public void ConstructorWithFactoryThrowsIfModelTypeIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new SimpleModelBinderProvider(null, () => null); }, "modelType");
+ }
+
+ [Fact]
+ public void ConstructorWithInstanceThrowsIfModelBinderIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new SimpleModelBinderProvider(typeof(object), (IExtensibleModelBinder)null); }, "modelBinder");
+ }
+
+ [Fact]
+ public void ConstructorWithInstanceThrowsIfModelTypeIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new SimpleModelBinderProvider(null, new Mock<IExtensibleModelBinder>().Object); }, "modelType");
+ }
+
+ [Fact]
+ public void GetBinder_TypeDoesNotMatch_ReturnsNull()
+ {
+ // Arrange
+ SimpleModelBinderProvider provider = new SimpleModelBinderProvider(typeof(string), new Mock<IExtensibleModelBinder>().Object)
+ {
+ SuppressPrefixCheck = true
+ };
+ ExtensibleModelBindingContext bindingContext = GetBindingContext(typeof(object));
+
+ // Act
+ IExtensibleModelBinder binder = provider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(binder);
+ }
+
+ [Fact]
+ public void GetBinder_TypeMatches_PrefixNotFound_ReturnsNull()
+ {
+ // Arrange
+ IExtensibleModelBinder binderInstance = new Mock<IExtensibleModelBinder>().Object;
+ SimpleModelBinderProvider provider = new SimpleModelBinderProvider(typeof(string), binderInstance);
+
+ ExtensibleModelBindingContext bindingContext = GetBindingContext(typeof(string));
+ bindingContext.ValueProvider = new SimpleValueProvider();
+
+ // Act
+ IExtensibleModelBinder returnedBinder = provider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(returnedBinder);
+ }
+
+ [Fact]
+ public void GetBinder_TypeMatches_PrefixSuppressed_ReturnsFactoryInstance()
+ {
+ // Arrange
+ int numExecutions = 0;
+ IExtensibleModelBinder theBinderInstance = new Mock<IExtensibleModelBinder>().Object;
+ Func<IExtensibleModelBinder> factory = delegate
+ {
+ numExecutions++;
+ return theBinderInstance;
+ };
+
+ SimpleModelBinderProvider provider = new SimpleModelBinderProvider(typeof(string), factory)
+ {
+ SuppressPrefixCheck = true
+ };
+ ExtensibleModelBindingContext bindingContext = GetBindingContext(typeof(string));
+
+ // Act
+ IExtensibleModelBinder returnedBinder = provider.GetBinder(null, bindingContext);
+ returnedBinder = provider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Equal(2, numExecutions);
+ Assert.Equal(theBinderInstance, returnedBinder);
+ }
+
+ [Fact]
+ public void GetBinder_TypeMatches_PrefixSuppressed_ReturnsInstance()
+ {
+ // Arrange
+ IExtensibleModelBinder theBinderInstance = new Mock<IExtensibleModelBinder>().Object;
+ SimpleModelBinderProvider provider = new SimpleModelBinderProvider(typeof(string), theBinderInstance)
+ {
+ SuppressPrefixCheck = true
+ };
+ ExtensibleModelBindingContext bindingContext = GetBindingContext(typeof(string));
+
+ // Act
+ IExtensibleModelBinder returnedBinder = provider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Equal(theBinderInstance, returnedBinder);
+ }
+
+ [Fact]
+ public void GetBinderThrowsIfBindingContextIsNull()
+ {
+ // Arrange
+ SimpleModelBinderProvider provider = new SimpleModelBinderProvider(typeof(string), new Mock<IExtensibleModelBinder>().Object);
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { provider.GetBinder(null, null); }, "bindingContext");
+ }
+
+ [Fact]
+ public void ModelTypeProperty()
+ {
+ // Arrange
+ SimpleModelBinderProvider provider = new SimpleModelBinderProvider(typeof(string), new Mock<IExtensibleModelBinder>().Object);
+
+ // Act & assert
+ Assert.Equal(typeof(string), provider.ModelType);
+ }
+
+ private static ExtensibleModelBindingContext GetBindingContext(Type modelType)
+ {
+ return new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => null, modelType)
+ };
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/TypeConverterModelBinderProviderTest.cs b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/TypeConverterModelBinderProviderTest.cs
new file mode 100644
index 00000000..6c6b41a3
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/TypeConverterModelBinderProviderTest.cs
@@ -0,0 +1,69 @@
+using System;
+using System.Web.Mvc;
+using Microsoft.Web.UnitTestUtil;
+using Xunit;
+
+namespace Microsoft.Web.Mvc.ModelBinding.Test
+{
+ public class TypeConverterModelBinderProviderTest
+ {
+ [Fact]
+ public void GetBinder_NoTypeConverterExistsFromString_ReturnsNull()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = GetBindingContext(typeof(void)); // no TypeConverter exists Void -> String
+
+ TypeConverterModelBinderProvider provider = new TypeConverterModelBinderProvider();
+
+ // Act
+ IExtensibleModelBinder binder = provider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(binder);
+ }
+
+ [Fact]
+ public void GetBinder_NullValueProviderResult_ReturnsNull()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = GetBindingContext(typeof(int));
+ bindingContext.ValueProvider = new SimpleValueProvider(); // clear the ValueProvider
+
+ TypeConverterModelBinderProvider provider = new TypeConverterModelBinderProvider();
+
+ // Act
+ IExtensibleModelBinder binder = provider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(binder);
+ }
+
+ [Fact]
+ public void GetBinder_TypeConverterExistsFromString_ReturnsNull()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = GetBindingContext(typeof(int)); // TypeConverter exists Int32 -> String
+
+ TypeConverterModelBinderProvider provider = new TypeConverterModelBinderProvider();
+
+ // Act
+ IExtensibleModelBinder binder = provider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.IsType<TypeConverterModelBinder>(binder);
+ }
+
+ private static ExtensibleModelBindingContext GetBindingContext(Type modelType)
+ {
+ return new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, modelType),
+ ModelName = "theModelName",
+ ValueProvider = new SimpleValueProvider
+ {
+ { "theModelName", "someValue" }
+ }
+ };
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/TypeConverterModelBinderTest.cs b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/TypeConverterModelBinderTest.cs
new file mode 100644
index 00000000..7788c92a
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/TypeConverterModelBinderTest.cs
@@ -0,0 +1,171 @@
+using System;
+using System.ComponentModel;
+using System.Globalization;
+using System.Web.Mvc;
+using Microsoft.Web.UnitTestUtil;
+using Xunit;
+
+namespace Microsoft.Web.Mvc.ModelBinding.Test
+{
+ public class TypeConverterModelBinderTest
+ {
+ [Fact]
+ public void BindModel_Error_FormatExceptionsTurnedIntoStringsInModelState()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = GetBindingContext(typeof(int));
+ bindingContext.ValueProvider = new SimpleValueProvider
+ {
+ { "theModelName", "not an integer" }
+ };
+
+ TypeConverterModelBinder binder = new TypeConverterModelBinder();
+
+ // Act
+ bool retVal = binder.BindModel(null, bindingContext);
+
+ // Assert
+ Assert.False(retVal);
+ Assert.Equal("The value 'not an integer' is not valid for Int32.", bindingContext.ModelState["theModelName"].Errors[0].ErrorMessage);
+ }
+
+ [Fact]
+ public void BindModel_Error_FormatExceptionsTurnedIntoStringsInModelState_ErrorNotAddedIfCallbackReturnsNull()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = GetBindingContext(typeof(int));
+ bindingContext.ValueProvider = new SimpleValueProvider
+ {
+ { "theModelName", "not an integer" }
+ };
+
+ TypeConverterModelBinder binder = new TypeConverterModelBinder();
+
+ // Act
+ ModelBinderErrorMessageProvider originalProvider = ModelBinderConfig.TypeConversionErrorMessageProvider;
+ bool retVal;
+ try
+ {
+ ModelBinderConfig.TypeConversionErrorMessageProvider = delegate { return null; };
+ retVal = binder.BindModel(null, bindingContext);
+ }
+ finally
+ {
+ ModelBinderConfig.TypeConversionErrorMessageProvider = originalProvider;
+ }
+
+ // Assert
+ Assert.False(retVal);
+ Assert.Null(bindingContext.Model);
+ Assert.True(bindingContext.ModelState.IsValid);
+ }
+
+ [Fact]
+ public void BindModel_Error_GeneralExceptionsSavedInModelState()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = GetBindingContext(typeof(Dummy));
+ bindingContext.ValueProvider = new SimpleValueProvider
+ {
+ { "theModelName", "foo" }
+ };
+
+ TypeConverterModelBinder binder = new TypeConverterModelBinder();
+
+ // Act
+ bool retVal = binder.BindModel(null, bindingContext);
+
+ // Assert
+ Assert.False(retVal);
+ Assert.Null(bindingContext.Model);
+ Assert.Equal("The parameter conversion from type 'System.String' to type 'Microsoft.Web.Mvc.ModelBinding.Test.TypeConverterModelBinderTest+Dummy' failed. See the inner exception for more information.", bindingContext.ModelState["theModelName"].Errors[0].Exception.Message);
+ Assert.Equal("From DummyTypeConverter: foo", bindingContext.ModelState["theModelName"].Errors[0].Exception.InnerException.Message);
+ }
+
+ [Fact]
+ public void BindModel_NullValueProviderResult_ReturnsFalse()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = GetBindingContext(typeof(int));
+
+ TypeConverterModelBinder binder = new TypeConverterModelBinder();
+
+ // Act
+ bool retVal = binder.BindModel(null, bindingContext);
+
+ // Assert
+ Assert.False(retVal, "BindModel should have returned null.");
+ Assert.Empty(bindingContext.ModelState);
+ }
+
+ [Fact]
+ public void BindModel_ValidValueProviderResult_ConvertEmptyStringsToNull()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = GetBindingContext(typeof(string));
+ bindingContext.ValueProvider = new SimpleValueProvider
+ {
+ { "theModelName", "" }
+ };
+
+ TypeConverterModelBinder binder = new TypeConverterModelBinder();
+
+ // Act
+ bool retVal = binder.BindModel(null, bindingContext);
+
+ // Assert
+ Assert.True(retVal);
+ Assert.Null(bindingContext.Model);
+ Assert.True(bindingContext.ModelState.ContainsKey("theModelName"));
+ }
+
+ [Fact]
+ public void BindModel_ValidValueProviderResult_ReturnsModel()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = GetBindingContext(typeof(int));
+ bindingContext.ValueProvider = new SimpleValueProvider
+ {
+ { "theModelName", "42" }
+ };
+
+ TypeConverterModelBinder binder = new TypeConverterModelBinder();
+
+ // Act
+ bool retVal = binder.BindModel(null, bindingContext);
+
+ // Assert
+ Assert.True(retVal);
+ Assert.Equal(42, bindingContext.Model);
+ Assert.True(bindingContext.ModelState.ContainsKey("theModelName"));
+ }
+
+ private static ExtensibleModelBindingContext GetBindingContext(Type modelType)
+ {
+ return new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, modelType),
+ ModelName = "theModelName",
+ ValueProvider = new SimpleValueProvider() // empty
+ };
+ }
+
+ [TypeConverter(typeof(DummyTypeConverter))]
+ private struct Dummy
+ {
+ }
+
+ private sealed class DummyTypeConverter : TypeConverter
+ {
+ public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
+ {
+ return (sourceType == typeof(string)) || base.CanConvertFrom(context, sourceType);
+ }
+
+ public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
+ {
+ throw new InvalidOperationException(String.Format("From DummyTypeConverter: {0}", value));
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/TypeMatchModelBinderProviderTest.cs b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/TypeMatchModelBinderProviderTest.cs
new file mode 100644
index 00000000..d6df4673
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/TypeMatchModelBinderProviderTest.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Linq;
+using System.Web.Mvc;
+using Microsoft.Web.UnitTestUtil;
+using Xunit;
+
+namespace Microsoft.Web.Mvc.ModelBinding.Test
+{
+ public class TypeMatchModelBinderProviderTest
+ {
+ [Fact]
+ public void ProviderIsMarkedFrontOfList()
+ {
+ // Arrange
+ Type t = typeof(TypeMatchModelBinderProvider);
+
+ // Act & assert
+ Assert.True(t.GetCustomAttributes(typeof(ModelBinderProviderOptionsAttribute), true /* inherit */).Cast<ModelBinderProviderOptionsAttribute>().Single().FrontOfList);
+ }
+
+ [Fact]
+ public void GetBinder_InvalidValueProviderResult_ReturnsNull()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = GetBindingContext();
+ bindingContext.ValueProvider = new SimpleValueProvider
+ {
+ { "theModelName", "not an integer" }
+ };
+
+ TypeMatchModelBinderProvider provider = new TypeMatchModelBinderProvider();
+
+ // Act
+ IExtensibleModelBinder binder = provider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(binder);
+ }
+
+ [Fact]
+ public void BindModel_ValidValueProviderResult_ReturnsBinder()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = GetBindingContext();
+ bindingContext.ValueProvider = new SimpleValueProvider
+ {
+ { "theModelName", 42 }
+ };
+
+ TypeMatchModelBinderProvider provider = new TypeMatchModelBinderProvider();
+
+ // Act
+ IExtensibleModelBinder binder = provider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.IsType<TypeMatchModelBinder>(binder);
+ }
+
+ private static ExtensibleModelBindingContext GetBindingContext()
+ {
+ return GetBindingContext(typeof(int));
+ }
+
+ private static ExtensibleModelBindingContext GetBindingContext(Type modelType)
+ {
+ return new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, modelType),
+ ModelName = "theModelName"
+ };
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/TypeMatchModelBinderTest.cs b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/TypeMatchModelBinderTest.cs
new file mode 100644
index 00000000..8c8298e3
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/ModelBinding/Test/TypeMatchModelBinderTest.cs
@@ -0,0 +1,113 @@
+using System;
+using System.Web.Mvc;
+using Microsoft.Web.UnitTestUtil;
+using Xunit;
+
+namespace Microsoft.Web.Mvc.ModelBinding.Test
+{
+ public class TypeMatchModelBinderTest
+ {
+ [Fact]
+ public void BindModel_InvalidValueProviderResult_ReturnsFalse()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = GetBindingContext();
+ bindingContext.ValueProvider = new SimpleValueProvider
+ {
+ { "theModelName", "not an integer" }
+ };
+
+ TypeMatchModelBinder binder = new TypeMatchModelBinder();
+
+ // Act
+ bool retVal = binder.BindModel(null, bindingContext);
+
+ // Assert
+ Assert.False(retVal);
+ Assert.Empty(bindingContext.ModelState);
+ }
+
+ [Fact]
+ public void BindModel_ValidValueProviderResult_ReturnsTrue()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = GetBindingContext();
+ bindingContext.ValueProvider = new SimpleValueProvider
+ {
+ { "theModelName", 42 }
+ };
+
+ TypeMatchModelBinder binder = new TypeMatchModelBinder();
+
+ // Act
+ bool retVal = binder.BindModel(null, bindingContext);
+
+ // Assert
+ Assert.True(retVal);
+ Assert.Equal(42, bindingContext.Model);
+ Assert.True(bindingContext.ModelState.ContainsKey("theModelName"));
+ }
+
+ [Fact]
+ public void GetCompatibleValueProviderResult_ValueProviderResultRawValueIncorrect_ReturnsNull()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = GetBindingContext();
+ bindingContext.ValueProvider = new SimpleValueProvider
+ {
+ { "theModelName", "not an integer" }
+ };
+
+ // Act
+ ValueProviderResult vpResult = TypeMatchModelBinder.GetCompatibleValueProviderResult(bindingContext);
+
+ // Assert
+ Assert.Null(vpResult); // Raw value is the wrong type
+ }
+
+ [Fact]
+ public void GetCompatibleValueProviderResult_ValueProviderResultValid_ReturnsValueProviderResult()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = GetBindingContext();
+ bindingContext.ValueProvider = new SimpleValueProvider
+ {
+ { "theModelName", 42 }
+ };
+
+ // Act
+ ValueProviderResult vpResult = TypeMatchModelBinder.GetCompatibleValueProviderResult(bindingContext);
+
+ // Assert
+ Assert.NotNull(vpResult);
+ }
+
+ [Fact]
+ public void GetCompatibleValueProviderResult_ValueProviderReturnsNull_ReturnsNull()
+ {
+ // Arrange
+ ExtensibleModelBindingContext bindingContext = GetBindingContext();
+ bindingContext.ValueProvider = new SimpleValueProvider();
+
+ // Act
+ ValueProviderResult vpResult = TypeMatchModelBinder.GetCompatibleValueProviderResult(bindingContext);
+
+ // Assert
+ Assert.Null(vpResult); // No key matched
+ }
+
+ private static ExtensibleModelBindingContext GetBindingContext()
+ {
+ return GetBindingContext(typeof(int));
+ }
+
+ private static ExtensibleModelBindingContext GetBindingContext(Type modelType)
+ {
+ return new ExtensibleModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, modelType),
+ ModelName = "theModelName"
+ };
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/Properties/AssemblyInfo.cs b/test/Microsoft.Web.Mvc.Test/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..76660dea
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Properties/AssemblyInfo.cs
@@ -0,0 +1,38 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+
+[assembly: AssemblyTitle("MvcToolkitTest")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("MvcToolkitTest")]
+[assembly: AssemblyCopyright("Copyright © 2008")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM componenets. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+
+[assembly: Guid("f2404c7a-e41f-4a7f-b952-e38a056df1ff")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Revision and Build Numbers
+// by using the '*' as shown below:
+
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/test/Microsoft.Web.Mvc.Test/Test/AjaxOnlyAttributeTest.cs b/test/Microsoft.Web.Mvc.Test/Test/AjaxOnlyAttributeTest.cs
new file mode 100644
index 00000000..4cf3b17b
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Test/AjaxOnlyAttributeTest.cs
@@ -0,0 +1,66 @@
+using System.Collections.Specialized;
+using System.Web.Mvc;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace Microsoft.Web.Mvc.Test
+{
+ public class AjaxOnlyAttributeTest
+ {
+ [Fact]
+ public void IsValidForRequestReturnsFalseIfHeaderNotPresent()
+ {
+ // Arrange
+ AjaxOnlyAttribute attr = new AjaxOnlyAttribute();
+ ControllerContext controllerContext = GetControllerContext(containsHeader: false);
+
+ // Act
+ bool isValid = attr.IsValidForRequest(controllerContext, null);
+
+ // Assert
+ Assert.False(isValid);
+ }
+
+ [Fact]
+ public void IsValidForRequestReturnsTrueIfHeaderIsPresent()
+ {
+ // Arrange
+ AjaxOnlyAttribute attr = new AjaxOnlyAttribute();
+ ControllerContext controllerContext = GetControllerContext(containsHeader: true);
+
+ // Act
+ bool isValid = attr.IsValidForRequest(controllerContext, null);
+
+ // Assert
+ Assert.True(isValid);
+ }
+
+ [Fact]
+ public void IsValidForRequestThrowsIfControllerContextIsNull()
+ {
+ // Arrange
+ AjaxOnlyAttribute attr = new AjaxOnlyAttribute();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { attr.IsValidForRequest(null, null); }, "controllerContext");
+ }
+
+ private static ControllerContext GetControllerContext(bool containsHeader)
+ {
+ Mock<ControllerContext> mockContext = new Mock<ControllerContext> { DefaultValue = DefaultValue.Mock };
+
+ NameValueCollection nvc = new NameValueCollection();
+ if (containsHeader)
+ {
+ nvc["X-Requested-With"] = "XMLHttpRequest";
+ }
+
+ mockContext.Setup(o => o.HttpContext.Request.Headers).Returns(nvc);
+ mockContext.Setup(o => o.HttpContext.Request["X-Requested-With"]).Returns("XMLHttpRequest"); // always assume the request contains this, e.g. as a form value
+
+ return mockContext.Object;
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/Test/AreaHelpersTest.cs b/test/Microsoft.Web.Mvc.Test/Test/AreaHelpersTest.cs
new file mode 100644
index 00000000..df9aace3
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Test/AreaHelpersTest.cs
@@ -0,0 +1,100 @@
+using System;
+using System.Web;
+using System.Web.Mvc;
+using System.Web.Routing;
+using Xunit;
+
+namespace Microsoft.Web.Mvc.Test
+{
+ public class AreaHelpersTest
+ {
+ [Fact]
+ public void GetAreaNameFromAreaRouteCollectionRoute()
+ {
+ // Arrange
+ RouteCollection routes = new RouteCollection();
+ AreaRegistrationContext context = new AreaRegistrationContext("area_name", routes);
+ Route route = context.MapRoute(null, "the_url");
+
+ // Act
+ string areaName = AreaHelpers.GetAreaName(route);
+
+ // Assert
+ Assert.Equal("area_name", areaName);
+ }
+
+ [Fact]
+ public void GetAreaNameFromIAreaAssociatedItem()
+ {
+ // Arrange
+ CustomRouteWithArea route = new CustomRouteWithArea();
+
+ // Act
+ string areaName = AreaHelpers.GetAreaName(route);
+
+ // Assert
+ Assert.Equal("area_name", areaName);
+ }
+
+ [Fact]
+ public void GetAreaNameFromRouteData()
+ {
+ // Arrange
+ RouteData routeData = new RouteData();
+ routeData.DataTokens["area"] = "area_name";
+
+ // Act
+ string areaName = AreaHelpers.GetAreaName(routeData);
+
+ // Assert
+ Assert.Equal("area_name", areaName);
+ }
+
+ [Fact]
+ public void GetAreaNameFromRouteDataFallsBackToRoute()
+ {
+ // Arrange
+ RouteCollection routes = new RouteCollection();
+ AreaRegistrationContext context = new AreaRegistrationContext("area_name", routes);
+ Route route = context.MapRoute(null, "the_url");
+ RouteData routeData = new RouteData(route, new MvcRouteHandler());
+
+ // Act
+ string areaName = AreaHelpers.GetAreaName(routeData);
+
+ // Assert
+ Assert.Equal("area_name", areaName);
+ }
+
+ [Fact]
+ public void GetAreaNameReturnsNullIfRouteNotAreaAware()
+ {
+ // Arrange
+ Route route = new Route("the_url", new MvcRouteHandler());
+
+ // Act
+ string areaName = AreaHelpers.GetAreaName(route);
+
+ // Assert
+ Assert.Null(areaName);
+ }
+
+ private class CustomRouteWithArea : RouteBase, IRouteWithArea
+ {
+ public string Area
+ {
+ get { return "area_name"; }
+ }
+
+ public override RouteData GetRouteData(HttpContextBase httpContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/Test/AsyncManagerExtensionsTest.cs b/test/Microsoft.Web.Mvc.Test/Test/AsyncManagerExtensionsTest.cs
new file mode 100644
index 00000000..0eab7918
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Test/AsyncManagerExtensionsTest.cs
@@ -0,0 +1,189 @@
+using System;
+using System.Threading;
+using System.Web.Mvc.Async;
+using System.Web.TestUtil;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace Microsoft.Web.Mvc.Test
+{
+ public class AsyncManagerExtensionsTest
+ {
+ [Fact]
+ public void RegisterTask_AsynchronousCompletion()
+ {
+ // Arrange
+ SimpleSynchronizationContext syncContext = new SimpleSynchronizationContext();
+ AsyncManager asyncManager = new AsyncManager(syncContext);
+ bool endDelegateWasCalled = false;
+
+ ManualResetEvent waitHandle = new ManualResetEvent(false /* initialState */);
+
+ Func<AsyncCallback, IAsyncResult> beginDelegate = callback =>
+ {
+ Assert.Equal(1, asyncManager.OutstandingOperations.Count);
+ MockAsyncResult asyncResult = new MockAsyncResult(false /* completedSynchronously */);
+ ThreadPool.QueueUserWorkItem(_ =>
+ {
+ Assert.Equal(1, asyncManager.OutstandingOperations.Count);
+ callback(asyncResult);
+ waitHandle.Set();
+ });
+ return asyncResult;
+ };
+ Action<IAsyncResult> endDelegate = delegate { endDelegateWasCalled = true; };
+
+ // Act
+ asyncManager.RegisterTask(beginDelegate, endDelegate);
+ waitHandle.WaitOne();
+
+ // Assert
+ Assert.True(endDelegateWasCalled);
+ Assert.True(syncContext.SendWasCalled);
+ Assert.Equal(0, asyncManager.OutstandingOperations.Count);
+ }
+
+ [Fact]
+ public void RegisterTask_AsynchronousCompletion_SwallowsExceptionsThrownByEndDelegate()
+ {
+ // Arrange
+ SimpleSynchronizationContext syncContext = new SimpleSynchronizationContext();
+ AsyncManager asyncManager = new AsyncManager(syncContext);
+ bool endDelegateWasCalled = false;
+
+ ManualResetEvent waitHandle = new ManualResetEvent(false /* initialState */);
+
+ Func<AsyncCallback, IAsyncResult> beginDelegate = callback =>
+ {
+ MockAsyncResult asyncResult = new MockAsyncResult(false /* completedSynchronously */);
+ ThreadPool.QueueUserWorkItem(_ =>
+ {
+ callback(asyncResult);
+ waitHandle.Set();
+ });
+ return asyncResult;
+ };
+ Action<IAsyncResult> endDelegate = delegate
+ {
+ endDelegateWasCalled = true;
+ throw new Exception("This is a sample exception.");
+ };
+
+ // Act
+ asyncManager.RegisterTask(beginDelegate, endDelegate);
+ waitHandle.WaitOne();
+
+ // Assert
+ Assert.True(endDelegateWasCalled);
+ Assert.Equal(0, asyncManager.OutstandingOperations.Count);
+ }
+
+ [Fact]
+ public void RegisterTask_ResetsOutstandingOperationCountIfBeginMethodThrows()
+ {
+ // Arrange
+ SimpleSynchronizationContext syncContext = new SimpleSynchronizationContext();
+ AsyncManager asyncManager = new AsyncManager(syncContext);
+
+ Func<AsyncCallback, IAsyncResult> beginDelegate = cb => { throw new InvalidOperationException("BeginDelegate throws."); };
+ Action<IAsyncResult> endDelegate = ar => { Assert.True(false, "This should never be called."); };
+
+ // Act & assert
+ Assert.Throws<InvalidOperationException>(
+ delegate { asyncManager.RegisterTask(beginDelegate, endDelegate); }, "BeginDelegate throws.");
+
+ Assert.Equal(0, asyncManager.OutstandingOperations.Count);
+ }
+
+ [Fact]
+ public void RegisterTask_SynchronousCompletion()
+ {
+ // Arrange
+ SimpleSynchronizationContext syncContext = new SimpleSynchronizationContext();
+ AsyncManager asyncManager = new AsyncManager(syncContext);
+ bool endDelegateWasCalled = false;
+
+ Func<AsyncCallback, IAsyncResult> beginDelegate = callback =>
+ {
+ Assert.Equal(1, asyncManager.OutstandingOperations.Count);
+ MockAsyncResult asyncResult = new MockAsyncResult(true /* completedSynchronously */);
+ callback(asyncResult);
+ return asyncResult;
+ };
+ Action<IAsyncResult> endDelegate = delegate { endDelegateWasCalled = true; };
+
+ // Act
+ asyncManager.RegisterTask(beginDelegate, endDelegate);
+
+ // Assert
+ Assert.True(endDelegateWasCalled);
+ Assert.False(syncContext.SendWasCalled);
+ Assert.Equal(0, asyncManager.OutstandingOperations.Count);
+ }
+
+ [Fact]
+ public void RegisterTask_ThrowsIfAsyncManagerIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { AsyncManagerExtensions.RegisterTask(null, _ => null, _ => { }); }, "asyncManager");
+ }
+
+ [Fact]
+ public void RegisterTask_ThrowsIfBeginDelegateIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new AsyncManager().RegisterTask(null, _ => { }); }, "beginDelegate");
+ }
+
+ [Fact]
+ public void RegisterTask_ThrowsIfEndDelegateIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new AsyncManager().RegisterTask(_ => null, null); }, "endDelegate");
+ }
+
+ private class SimpleSynchronizationContext : SynchronizationContext
+ {
+ public bool SendWasCalled;
+
+ public override void Send(SendOrPostCallback d, object state)
+ {
+ SendWasCalled = true;
+ d(state);
+ }
+ }
+
+ private class MockAsyncResult : IAsyncResult
+ {
+ private readonly bool _completedSynchronously;
+
+ public MockAsyncResult(bool completedSynchronously)
+ {
+ _completedSynchronously = completedSynchronously;
+ }
+
+ public object AsyncState
+ {
+ get { throw new NotImplementedException(); }
+ }
+
+ public WaitHandle AsyncWaitHandle
+ {
+ get { throw new NotImplementedException(); }
+ }
+
+ public bool CompletedSynchronously
+ {
+ get { return _completedSynchronously; }
+ }
+
+ public bool IsCompleted
+ {
+ get { throw new NotImplementedException(); }
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/Test/ButtonTest.cs b/test/Microsoft.Web.Mvc.Test/Test/ButtonTest.cs
new file mode 100644
index 00000000..3f8d4c7d
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Test/ButtonTest.cs
@@ -0,0 +1,66 @@
+using System.Web.Mvc;
+using System.Web.Routing;
+using Microsoft.Web.UnitTestUtil;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace Microsoft.Web.Mvc.Test
+{
+ public class ButtonTest
+ {
+ [Fact]
+ public void ButtonWithNullNameThrowsArgumentNullException()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+ Assert.ThrowsArgumentNull(() => html.Button(null, "text", HtmlButtonType.Button), "name");
+ }
+
+ [Fact]
+ public void ButtonRendersBaseAttributes()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+ MvcHtmlString result = html.Button("nameAttr", "buttonText", HtmlButtonType.Reset, "onclickAttr");
+ Assert.Equal("<button name=\"nameAttr\" onclick=\"onclickAttr\" type=\"reset\">buttonText</button>", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void ButtonWithoutOnClickDoesNotRenderOnclickAttribute()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+ MvcHtmlString result = html.Button("nameAttr", "buttonText", HtmlButtonType.Reset);
+ Assert.Equal("<button name=\"nameAttr\" type=\"reset\">buttonText</button>", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void ButtonAllowsInnerHtml()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+ MvcHtmlString result = html.Button("nameAttr", "<img src=\"puppy.jpg\" />", HtmlButtonType.Submit, "onclickAttr");
+ Assert.Equal("<button name=\"nameAttr\" onclick=\"onclickAttr\" type=\"submit\"><img src=\"puppy.jpg\" /></button>", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void ButtonRendersExplicitAttributes()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+ MvcHtmlString result = html.Button("nameAttr", "buttonText", HtmlButtonType.Reset, "onclickAttr", new { title = "the-title" });
+ Assert.Equal("<button name=\"nameAttr\" onclick=\"onclickAttr\" title=\"the-title\" type=\"reset\">buttonText</button>", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void ButtonRendersExplicitAttributesWithUnderscores()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+ MvcHtmlString result = html.Button("nameAttr", "buttonText", HtmlButtonType.Reset, "onclickAttr", new { foo_bar = "baz" });
+ Assert.Equal("<button foo-bar=\"baz\" name=\"nameAttr\" onclick=\"onclickAttr\" type=\"reset\">buttonText</button>", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void ButtonRendersExplicitDictionaryAttributes()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+ MvcHtmlString result = html.Button("nameAttr", "buttonText", HtmlButtonType.Button, "onclickAttr", new RouteValueDictionary(new { title = "the-title" }));
+ Assert.Equal("<button name=\"nameAttr\" onclick=\"onclickAttr\" title=\"the-title\" type=\"button\">buttonText</button>", result.ToHtmlString());
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/Test/ContentTypeAttributeTest.cs b/test/Microsoft.Web.Mvc.Test/Test/ContentTypeAttributeTest.cs
new file mode 100644
index 00000000..39dbdff1
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Test/ContentTypeAttributeTest.cs
@@ -0,0 +1,43 @@
+using System.Web;
+using System.Web.Mvc;
+using System.Web.Routing;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace Microsoft.Web.Mvc.Test
+{
+ public class ContentTypeAttributeTest
+ {
+ [Fact]
+ public void ContentTypeSetInCtor()
+ {
+ var attr = new ContentTypeAttribute("text/html");
+ Assert.Equal("text/html", attr.ContentType);
+ }
+
+ [Fact]
+ public void ContentTypeCtorThrowsArgumentExceptionWhenContentTypeIsNull()
+ {
+ Assert.ThrowsArgumentNullOrEmpty(() => new ContentTypeAttribute(null), "contentType");
+ }
+
+ [Fact]
+ public void ExecuteResultSetsContentType()
+ {
+ var mockHttpResponse = new Mock<HttpResponseBase>();
+ var mockHttpContext = new Mock<HttpContextBase>();
+ mockHttpContext.Setup(c => c.Response).Returns(mockHttpResponse.Object);
+
+ var mockController = new Mock<Controller>();
+ var controllerContext = new ControllerContext(new RequestContext(mockHttpContext.Object, new RouteData()), mockController.Object);
+ var result = new ContentResult { Content = "blah blah" };
+ var filterContext = new ResultExecutingContext(controllerContext, result);
+
+ var filter = new ContentTypeAttribute("text/xml");
+ filter.OnResultExecuting(filterContext);
+
+ mockHttpResponse.VerifySet(r => r.ContentType = "text/xml");
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/Test/ControllerExtensionsTest.cs b/test/Microsoft.Web.Mvc.Test/Test/ControllerExtensionsTest.cs
new file mode 100644
index 00000000..ae2a2e1c
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Test/ControllerExtensionsTest.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Web.Mvc;
+using Microsoft.Web.UnitTestUtil;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace Microsoft.Web.Mvc.Test
+{
+ public class ControllerExtensionsTest
+ {
+ private const string AppPathModifier = MvcHelper.AppPathModifier;
+
+ [Fact]
+ public void RedirectToAction_DifferentController()
+ {
+ // Act
+ RedirectToRouteResult result = new SampleController().RedirectToAction<DifferentController>(x => x.SomeOtherMethod(84));
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal("", result.RouteName);
+ Assert.Equal(3, result.RouteValues.Count);
+ Assert.Equal("Different", result.RouteValues["controller"]);
+ Assert.Equal("SomeOtherMethod", result.RouteValues["action"]);
+ Assert.Equal(84, result.RouteValues["someOtherParameter"]);
+ }
+
+ [Fact]
+ public void RedirectToAction_SameController()
+ {
+ // Act
+ RedirectToRouteResult result = new SampleController().RedirectToAction(x => x.SomeMethod(42));
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal("", result.RouteName);
+ Assert.Equal(3, result.RouteValues.Count);
+ Assert.Equal("Sample", result.RouteValues["controller"]);
+ Assert.Equal("SomeMethod", result.RouteValues["action"]);
+ Assert.Equal(42, result.RouteValues["someParameter"]);
+ }
+
+ [Fact]
+ public void RedirectToAction_ThrowsIfControllerIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { ((SampleController)null).RedirectToAction(x => x.SomeMethod(42)); }, "controller");
+ }
+
+ private class SampleController : Controller
+ {
+ public ActionResult SomeMethod(int someParameter)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ private class DifferentController : Controller
+ {
+ public ActionResult SomeOtherMethod(int someOtherParameter)
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/Test/CookieTempDataProviderTest.cs b/test/Microsoft.Web.Mvc.Test/Test/CookieTempDataProviderTest.cs
new file mode 100644
index 00000000..799f7564
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Test/CookieTempDataProviderTest.cs
@@ -0,0 +1,171 @@
+using System;
+using System.Collections.Generic;
+using System.Web;
+using System.Web.Mvc;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace Microsoft.Web.Mvc.Test
+{
+ public class CookieTempDataProviderTest
+ {
+ [Fact]
+ public void ConstructProviderThrowsOnNullHttpContext()
+ {
+ Assert.ThrowsArgumentNull(
+ delegate { new CookieTempDataProvider(null); },
+ "httpContext");
+ }
+
+ [Fact]
+ public void CtorSetsHttpContextProperty()
+ {
+ var httpContext = new Mock<HttpContextBase>().Object;
+ var provider = new CookieTempDataProvider(httpContext);
+
+ Assert.Equal(httpContext, provider.HttpContext);
+ }
+
+ [Fact]
+ public void LoadTempDataWithEmptyCookieReturnsEmptyDictionary()
+ {
+ HttpCookie cookie = new HttpCookie("__ControllerTempData");
+ cookie.Value = String.Empty;
+ var cookies = new HttpCookieCollection();
+ cookies.Add(cookie);
+
+ var requestMock = new Mock<HttpRequestBase>();
+ requestMock.Setup(r => r.Cookies).Returns(cookies);
+
+ var httpContextMock = new Mock<HttpContextBase>();
+ httpContextMock.Setup(c => c.Request).Returns(requestMock.Object);
+
+ ITempDataProvider provider = new CookieTempDataProvider(httpContextMock.Object);
+
+ IDictionary<string, object> tempData = provider.LoadTempData(null /* controllerContext */);
+ Assert.NotNull(tempData);
+ Assert.Equal(0, tempData.Count);
+ }
+
+ [Fact]
+ public void LoadTempDataWithNullCookieReturnsEmptyTempDataDictionary()
+ {
+ var cookies = new HttpCookieCollection();
+
+ var requestMock = new Mock<HttpRequestBase>();
+ requestMock.Setup(r => r.Cookies).Returns(cookies);
+
+ var httpContextMock = new Mock<HttpContextBase>();
+ httpContextMock.Setup(c => c.Request).Returns(requestMock.Object);
+
+ ITempDataProvider provider = new CookieTempDataProvider(httpContextMock.Object);
+
+ IDictionary<string, object> tempData = provider.LoadTempData(null /* controllerContext */);
+ Assert.NotNull(tempData);
+ Assert.Equal(0, tempData.Count);
+ }
+
+ [Fact]
+ public void LoadTempDataIgnoresNullResponseCookieDoesNotThrowException()
+ {
+ HttpCookie cookie = new HttpCookie("__ControllerTempData");
+ var initialTempData = new Dictionary<string, object>();
+ initialTempData.Add("WhatIsInHere?", "Stuff");
+ cookie.Value = CookieTempDataProvider.DictionaryToBase64String(initialTempData);
+ var cookies = new HttpCookieCollection();
+ cookies.Add(cookie);
+
+ var requestMock = new Mock<HttpRequestBase>();
+ requestMock.Setup(r => r.Cookies).Returns(cookies);
+
+ var responseMock = new Mock<HttpResponseBase>();
+ responseMock.Setup(r => r.Cookies).Returns((HttpCookieCollection)null);
+
+ var httpContextMock = new Mock<HttpContextBase>();
+ httpContextMock.Setup(c => c.Request).Returns(requestMock.Object);
+ httpContextMock.Setup(c => c.Response).Returns(responseMock.Object);
+
+ ITempDataProvider provider = new CookieTempDataProvider(httpContextMock.Object);
+
+ IDictionary<string, object> tempData = provider.LoadTempData(null /* controllerContext */);
+ Assert.Equal("Stuff", tempData["WhatIsInHere?"]);
+ }
+
+ [Fact]
+ public void LoadTempDataWithNullResponseDoesNotThrowException()
+ {
+ HttpCookie cookie = new HttpCookie("__ControllerTempData");
+ var initialTempData = new Dictionary<string, object>();
+ initialTempData.Add("WhatIsInHere?", "Stuff");
+ cookie.Value = CookieTempDataProvider.DictionaryToBase64String(initialTempData);
+ var cookies = new HttpCookieCollection();
+ cookies.Add(cookie);
+
+ var requestMock = new Mock<HttpRequestBase>();
+ requestMock.Setup(r => r.Cookies).Returns(cookies);
+
+ var httpContextMock = new Mock<HttpContextBase>();
+ httpContextMock.Setup(c => c.Request).Returns(requestMock.Object);
+ httpContextMock.Setup(c => c.Response).Returns((HttpResponseBase)null);
+
+ ITempDataProvider provider = new CookieTempDataProvider(httpContextMock.Object);
+
+ IDictionary<string, object> tempData = provider.LoadTempData(null /* controllerContext */);
+ Assert.Equal("Stuff", tempData["WhatIsInHere?"]);
+ }
+
+ [Fact]
+ public void SaveTempDataStoresSerializedFormInCookie()
+ {
+ var cookies = new HttpCookieCollection();
+ var responseMock = new Mock<HttpResponseBase>();
+ responseMock.Setup(r => r.Cookies).Returns(cookies);
+
+ var httpContextMock = new Mock<HttpContextBase>();
+ httpContextMock.Setup(c => c.Response).Returns(responseMock.Object);
+
+ ITempDataProvider provider = new CookieTempDataProvider(httpContextMock.Object);
+ var tempData = new Dictionary<string, object>();
+ tempData.Add("Testing", "Turn it up to 11");
+ tempData.Add("Testing2", 1.23);
+
+ provider.SaveTempData(null, tempData);
+ HttpCookie cookie = cookies["__ControllerTempData"];
+ string serialized = cookie.Value;
+ IDictionary<string, object> deserializedTempData = CookieTempDataProvider.Base64StringToDictionary(serialized);
+ Assert.Equal("Turn it up to 11", deserializedTempData["Testing"]);
+ Assert.Equal(1.23, deserializedTempData["Testing2"]);
+ }
+
+ [Fact]
+ public void CanLoadTempDataFromCookie()
+ {
+ var tempData = new Dictionary<string, object>();
+ tempData.Add("abc", "easy as 123");
+ tempData.Add("price", 1.234);
+ string serializedTempData = CookieTempDataProvider.DictionaryToBase64String(tempData);
+
+ var cookies = new HttpCookieCollection();
+ var httpCookie = new HttpCookie("__ControllerTempData");
+ httpCookie.Value = serializedTempData;
+ cookies.Add(httpCookie);
+
+ var requestMock = new Mock<HttpRequestBase>();
+ requestMock.Setup(r => r.Cookies).Returns(cookies);
+
+ var responseMock = new Mock<HttpResponseBase>();
+ responseMock.Setup(r => r.Cookies).Returns(cookies);
+
+ var httpContextMock = new Mock<HttpContextBase>();
+ httpContextMock.Setup(c => c.Request).Returns(requestMock.Object);
+ httpContextMock.Setup(c => c.Response).Returns(responseMock.Object);
+
+ ITempDataProvider provider = new CookieTempDataProvider(httpContextMock.Object);
+ IDictionary<string, object> loadedTempData = provider.LoadTempData(null /* controllerContext */);
+ Assert.Equal(2, loadedTempData.Count);
+ Assert.Equal("easy as 123", loadedTempData["abc"]);
+ Assert.Equal(1.234, loadedTempData["price"]);
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/Test/CookieValueProviderFactoryTest.cs b/test/Microsoft.Web.Mvc.Test/Test/CookieValueProviderFactoryTest.cs
new file mode 100644
index 00000000..36cebfe6
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Test/CookieValueProviderFactoryTest.cs
@@ -0,0 +1,38 @@
+using System.Globalization;
+using System.Web;
+using System.Web.Mvc;
+using Moq;
+using Xunit;
+
+namespace Microsoft.Web.Mvc.Test
+{
+ public class CookieValueProviderFactoryTest
+ {
+ [Fact]
+ public void GetValueProvider()
+ {
+ // Arrange
+ HttpCookieCollection cookies = new HttpCookieCollection
+ {
+ new HttpCookie("foo", "fooValue"),
+ new HttpCookie("bar.baz", "barBazValue"),
+ new HttpCookie("", "emptyValue"),
+ new HttpCookie(null, "nullValue")
+ };
+
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.Setup(o => o.HttpContext.Request.Cookies).Returns(cookies);
+
+ CookieValueProviderFactory factory = new CookieValueProviderFactory();
+
+ // Act
+ IValueProvider provider = factory.GetValueProvider(mockControllerContext.Object);
+
+ // Assert
+ Assert.Null(provider.GetValue(""));
+ Assert.True(provider.ContainsPrefix("bar"));
+ Assert.Equal("fooValue", provider.GetValue("foo").AttemptedValue);
+ Assert.Equal(CultureInfo.InvariantCulture, provider.GetValue("foo").Culture);
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/Test/CopyAsyncParametersAttributeTest.cs b/test/Microsoft.Web.Mvc.Test/Test/CopyAsyncParametersAttributeTest.cs
new file mode 100644
index 00000000..87244f62
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Test/CopyAsyncParametersAttributeTest.cs
@@ -0,0 +1,78 @@
+using System;
+using System.Collections.Generic;
+using System.Web.Mvc;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace Microsoft.Web.Mvc.Test
+{
+ public class CopyAsyncParametersAttributeTest
+ {
+ [Fact]
+ public void OnActionExecuting_CopiesParametersIfControllerIsAsync()
+ {
+ // Arrange
+ CopyAsyncParametersAttribute attr = new CopyAsyncParametersAttribute();
+ SampleAsyncController controller = new SampleAsyncController();
+
+ ActionExecutingContext filterContext = new ActionExecutingContext
+ {
+ ActionParameters = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase),
+ Controller = controller
+ };
+ filterContext.ActionParameters["foo"] = "fooAction";
+ filterContext.ActionParameters["bar"] = "barAction";
+ controller.AsyncManager.Parameters["bar"] = "barAsync";
+ controller.AsyncManager.Parameters["baz"] = "bazAsync";
+
+ // Act
+ attr.OnActionExecuting(filterContext);
+
+ // Assert
+ Assert.Equal("fooAction", controller.AsyncManager.Parameters["foo"]);
+ Assert.Equal("barAction", controller.AsyncManager.Parameters["bar"]);
+ Assert.Equal("bazAsync", controller.AsyncManager.Parameters["baz"]);
+ }
+
+ [Fact]
+ public void OnActionExecuting_DoesNothingIfControllerNotAsync()
+ {
+ // Arrange
+ CopyAsyncParametersAttribute attr = new CopyAsyncParametersAttribute();
+ SampleSyncController controller = new SampleSyncController();
+
+ ActionExecutingContext filterContext = new ActionExecutingContext
+ {
+ ActionParameters = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase),
+ Controller = controller
+ };
+ filterContext.ActionParameters["foo"] = "originalFoo";
+ filterContext.ActionParameters["bar"] = "originalBar";
+
+ // Act
+ attr.OnActionExecuting(filterContext);
+
+ // Assert
+ // If we got this far without crashing, life is good :)
+ }
+
+ [Fact]
+ public void OnActionExecuting_ThrowsIfFilterContextIsNull()
+ {
+ // Arrange
+ CopyAsyncParametersAttribute attr = new CopyAsyncParametersAttribute();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { attr.OnActionExecuting(null); }, "filterContext");
+ }
+
+ private class SampleSyncController : Controller
+ {
+ }
+
+ private class SampleAsyncController : AsyncController
+ {
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/Test/CreditCardAttributeTest.cs b/test/Microsoft.Web.Mvc.Test/Test/CreditCardAttributeTest.cs
new file mode 100644
index 00000000..af5bb98b
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Test/CreditCardAttributeTest.cs
@@ -0,0 +1,42 @@
+using System.Linq;
+using System.Web.Mvc;
+using Moq;
+using Xunit;
+
+namespace Microsoft.Web.Mvc.Test
+{
+ public class CreditCardAttributeTest
+ {
+ [Fact]
+ public void ClientRule()
+ {
+ // Arrange
+ var attribute = new CreditCardAttribute();
+ var provider = new Mock<ModelMetadataProvider>();
+ var metadata = new ModelMetadata(provider.Object, null, null, typeof(string), "PropertyName");
+
+ // Act
+ ModelClientValidationRule clientRule = attribute.GetClientValidationRules(metadata, null).Single();
+
+ // Assert
+ Assert.Equal("creditcard", clientRule.ValidationType);
+ Assert.Equal("The PropertyName field is not a valid credit card number.", clientRule.ErrorMessage);
+ Assert.Empty(clientRule.ValidationParameters);
+ }
+
+ [Fact]
+ public void IsValidTests()
+ {
+ // Arrange
+ var attribute = new CreditCardAttribute();
+
+ // Act & Assert
+ Assert.True(attribute.IsValid(null)); // Optional values are always valid
+ Assert.True(attribute.IsValid("0000000000000000")); // Simplest valid value
+ Assert.True(attribute.IsValid("1234567890123452")); // Good checksum
+ Assert.True(attribute.IsValid("1234-5678-9012-3452")); // Good checksum, with dashes
+ Assert.False(attribute.IsValid("0000000000000001")); // Bad checksum
+ Assert.False(attribute.IsValid(0)); // Non-string
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/Test/CssExtensionsTests.cs b/test/Microsoft.Web.Mvc.Test/Test/CssExtensionsTests.cs
new file mode 100644
index 00000000..f15918dc
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Test/CssExtensionsTests.cs
@@ -0,0 +1,138 @@
+using System;
+using System.Web.Mvc;
+using Microsoft.Web.UnitTestUtil;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace Microsoft.Web.Mvc.Test
+{
+ public class CssExtensionsTests
+ {
+ [Fact]
+ public void CssWithoutFileThrowsArgumentNullException()
+ {
+ // Arrange
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+
+ // Assert
+ Assert.ThrowsArgumentNullOrEmpty(() => html.Css(null), "file");
+ }
+
+ [Fact]
+ public void CssWithRootedPathRendersProperElement()
+ {
+ // Arrange
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+
+ // Act
+ MvcHtmlString result = html.Css("~/Correct/Path.css");
+
+ // Assert
+ Assert.Equal("<link href=\"/$(SESSION)/Correct/Path.css\" rel=\"stylesheet\" type=\"text/css\" />", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void CssWithRelativePathRendersProperElement()
+ {
+ // Arrange
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+
+ // Act
+ MvcHtmlString result = html.Css("../../Correct/Path.css");
+
+ // Assert
+ Assert.Equal("<link href=\"../../Correct/Path.css\" rel=\"stylesheet\" type=\"text/css\" />", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void CssWithRelativeCurrentPathRendersProperElement()
+ {
+ // Arrange
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+
+ // Act
+ MvcHtmlString result = html.Css("/Correct/Path.css");
+
+ // Assert
+ Assert.Equal("<link href=\"/Correct/Path.css\" rel=\"stylesheet\" type=\"text/css\" />", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void CssWithContentRelativePathRendersProperElement()
+ {
+ // Arrange
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+
+ // Act
+ MvcHtmlString result = html.Css("Correct/Path.css");
+
+ // Assert
+ Assert.Equal("<link href=\"/$(SESSION)/Content/Correct/Path.css\" rel=\"stylesheet\" type=\"text/css\" />", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void CssWithNullMediaTypeRendersProperElement()
+ {
+ // Arrange
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+
+ // Act
+ MvcHtmlString result = html.Css("Correct/Path.css", null);
+
+ // Assert
+ Assert.Equal("<link href=\"/$(SESSION)/Content/Correct/Path.css\" rel=\"stylesheet\" type=\"text/css\" />", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void CssWithEmptyMediaTypeRendersProperElement()
+ {
+ // Arrange
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+
+ // Act
+ MvcHtmlString result = html.Css("Correct/Path.css", String.Empty);
+
+ // Assert
+ Assert.Equal("<link href=\"/$(SESSION)/Content/Correct/Path.css\" media=\"\" rel=\"stylesheet\" type=\"text/css\" />", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void CssWithMediaTypeRendersProperElement()
+ {
+ // Arrange
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+
+ // Act
+ MvcHtmlString result = html.Css("Correct/Path.css", "Print");
+
+ // Assert
+ Assert.Equal("<link href=\"/$(SESSION)/Content/Correct/Path.css\" media=\"Print\" rel=\"stylesheet\" type=\"text/css\" />", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void CssWithUrlRendersProperElement()
+ {
+ // Arrange
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+
+ // Act
+ MvcHtmlString result = html.Css("http://ajax.Correct.com/Path.js");
+
+ // Assert
+ Assert.Equal("<link href=\"http://ajax.Correct.com/Path.js\" rel=\"stylesheet\" type=\"text/css\" />", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void CssWithSecureUrlRendersProperElement()
+ {
+ // Arrange
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+
+ // Act
+ MvcHtmlString result = html.Css("https://ajax.Correct.com/Path.js");
+
+ // Assert
+ Assert.Equal("<link href=\"https://ajax.Correct.com/Path.js\" rel=\"stylesheet\" type=\"text/css\" />", result.ToHtmlString());
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/Test/DeserializeAttributeTest.cs b/test/Microsoft.Web.Mvc.Test/Test/DeserializeAttributeTest.cs
new file mode 100644
index 00000000..41739173
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Test/DeserializeAttributeTest.cs
@@ -0,0 +1,105 @@
+using System;
+using System.Runtime.Serialization;
+using System.Web.Mvc;
+using Microsoft.Web.UnitTestUtil;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace Microsoft.Web.Mvc.Test
+{
+ public class DeserializeAttributeTest
+ {
+ [Fact]
+ public void BinderReturnsDeserializedValue()
+ {
+ // Arrange
+ Mock<MvcSerializer> mockSerializer = new Mock<MvcSerializer>();
+ mockSerializer.Setup(o => o.Deserialize("some-value", SerializationMode.EncryptedAndSigned)).Returns(42);
+ DeserializeAttribute attr = new DeserializeAttribute(SerializationMode.EncryptedAndSigned) { Serializer = mockSerializer.Object };
+
+ IModelBinder binder = attr.GetBinder();
+ ModelBindingContext mbContext = new ModelBindingContext
+ {
+ ModelName = "someKey",
+ ValueProvider = new SimpleValueProvider
+ {
+ { "someKey", "some-value" }
+ }
+ };
+
+ // Act
+ object retVal = binder.BindModel(null, mbContext);
+
+ // Assert
+ Assert.Equal(42, retVal);
+ }
+
+ [Fact]
+ public void BinderReturnsNullIfValueProviderDoesNotContainKey()
+ {
+ // Arrange
+ DeserializeAttribute attr = new DeserializeAttribute();
+ IModelBinder binder = attr.GetBinder();
+ ModelBindingContext mbContext = new ModelBindingContext
+ {
+ ModelName = "someKey",
+ ValueProvider = new SimpleValueProvider()
+ };
+
+ // Act
+ object retVal = binder.BindModel(null, mbContext);
+
+ // Assert
+ Assert.Null(retVal);
+ }
+
+ [Fact]
+ public void BinderThrowsIfBindingContextIsNull()
+ {
+ // Arrange
+ DeserializeAttribute attr = new DeserializeAttribute();
+ IModelBinder binder = attr.GetBinder();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { binder.BindModel(null, null); }, "bindingContext");
+ }
+
+ [Fact]
+ public void BinderThrowsIfDataCorrupt()
+ {
+ // Arrange
+ Mock<MvcSerializer> mockSerializer = new Mock<MvcSerializer>();
+ mockSerializer.Setup(o => o.Deserialize(It.IsAny<string>(), It.IsAny<SerializationMode>())).Throws(new SerializationException());
+ DeserializeAttribute attr = new DeserializeAttribute { Serializer = mockSerializer.Object };
+
+ IModelBinder binder = attr.GetBinder();
+ ModelBindingContext mbContext = new ModelBindingContext
+ {
+ ModelName = "someKey",
+ ValueProvider = new SimpleValueProvider
+ {
+ { "someKey", "This data is corrupted." }
+ }
+ };
+
+ // Act & assert
+ Exception exception = Assert.Throws<SerializationException>(
+ delegate { binder.BindModel(null, mbContext); });
+ }
+
+ [Fact]
+ public void ModeDefaultsToSigned()
+ {
+ // Arrange
+ DeserializeAttribute attr = new DeserializeAttribute();
+
+ // Act
+ SerializationMode defaultMode = attr.Mode;
+
+ // Assert
+ Assert.Equal(SerializationMode.Signed, defaultMode);
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/Test/DynamicReflectionObjectTest.cs b/test/Microsoft.Web.Mvc.Test/Test/DynamicReflectionObjectTest.cs
new file mode 100644
index 00000000..fd5f8a11
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Test/DynamicReflectionObjectTest.cs
@@ -0,0 +1,54 @@
+using System;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace Microsoft.Web.Mvc.Test
+{
+ public class DynamicReflectionObjectTest
+ {
+ [Fact]
+ public void NoPropertiesThrows()
+ {
+ // Arrange
+ dynamic dro = DynamicReflectionObject.Wrap(new { });
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ () => dro.baz,
+ "The property baz doesn't exist. There are no public properties on this object.");
+ }
+
+ [Fact]
+ public void UnknownPropertyThrows()
+ {
+ // Arrange
+ dynamic dro = DynamicReflectionObject.Wrap(new { foo = 3.4, biff = "Two", bar = 1 });
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ () => dro.baz,
+ "The property baz doesn't exist. Supported properties are: bar, biff, foo.");
+ }
+
+ [Fact]
+ public void CanAccessProperties()
+ {
+ // Arrange
+ dynamic dro = DynamicReflectionObject.Wrap(new { foo = "Hello world!", bar = 42 });
+
+ // Act & Assert
+ Assert.Equal("Hello world!", dro.foo);
+ Assert.Equal(42, dro.bar);
+ }
+
+ [Fact]
+ public void CanAccessNestedAnonymousProperties()
+ {
+ // Arrange
+ dynamic dro = DynamicReflectionObject.Wrap(new { foo = new { bar = "Hello world!" } });
+
+ // Act & Assert
+ Assert.Equal("Hello world!", dro.foo.bar);
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/Test/DynamicViewDataDictionaryTest.cs b/test/Microsoft.Web.Mvc.Test/Test/DynamicViewDataDictionaryTest.cs
new file mode 100644
index 00000000..09666ed9
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Test/DynamicViewDataDictionaryTest.cs
@@ -0,0 +1,115 @@
+using System;
+using System.Web.Mvc;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace Microsoft.Web.Mvc.Test
+{
+ public class DynamicViewDataDictionaryTest
+ {
+ // Property-style accessor
+
+ [Fact]
+ public void Property_UnknownItemReturnsEmptyString()
+ {
+ // Arrange
+ ViewDataDictionary vdd = new ViewDataDictionary();
+ dynamic dvdd = DynamicViewDataDictionary.Wrap(vdd);
+
+ // Act
+ object result = dvdd.Foo;
+
+ // Assert
+ Assert.Equal(String.Empty, result);
+ }
+
+ [Fact]
+ public void Property_CanAccessViewDataValues()
+ {
+ // Arrange
+ ViewDataDictionary vdd = new ViewDataDictionary();
+ vdd["Foo"] = "Value for Foo";
+ dynamic dvdd = DynamicViewDataDictionary.Wrap(vdd);
+
+ // Act
+ object result = dvdd.Foo;
+
+ // Assert
+ Assert.Equal("Value for Foo", result);
+ }
+
+ [Fact]
+ public void Property_CanAccessModelProperties()
+ {
+ ViewDataDictionary vdd = new ViewDataDictionary(new { Foo = "Value for Foo" });
+ dynamic dvdd = DynamicViewDataDictionary.Wrap(vdd);
+
+ // Act
+ object result = dvdd.Foo;
+
+ // Assert
+ Assert.Equal("Value for Foo", result);
+ }
+
+ // Index-style accessor
+
+ [Fact]
+ public void Indexer_GuardClauses()
+ {
+ // Arrange
+ ViewDataDictionary vdd = new ViewDataDictionary();
+ dynamic dvdd = DynamicViewDataDictionary.Wrap(vdd);
+
+ // Act & Assert
+ Assert.Throws<ArgumentException>(
+ () => { var x = dvdd["foo", "bar"]; },
+ "DynamicViewDataDictionary only supports single indexers.");
+
+ Assert.Throws<ArgumentException>(
+ () => { var x = dvdd[42]; },
+ "DynamicViewDataDictionary only supports string-based indexers.");
+ }
+
+ [Fact]
+ public void Indexer_UnknownItemReturnsEmptyString()
+ {
+ // Arrange
+ ViewDataDictionary vdd = new ViewDataDictionary();
+ dynamic dvdd = DynamicViewDataDictionary.Wrap(vdd);
+
+ // Act
+ object result = dvdd["Foo"];
+
+ // Assert
+ Assert.Equal(String.Empty, result);
+ }
+
+ [Fact]
+ public void Indexer_CanAccessViewDataValues()
+ {
+ // Arrange
+ ViewDataDictionary vdd = new ViewDataDictionary();
+ vdd["Foo"] = "Value for Foo";
+ dynamic dvdd = DynamicViewDataDictionary.Wrap(vdd);
+
+ // Act
+ object result = dvdd["Foo"];
+
+ // Assert
+ Assert.Equal("Value for Foo", result);
+ }
+
+ [Fact]
+ public void Indexer_CanAccessModelProperties()
+ {
+ ViewDataDictionary vdd = new ViewDataDictionary(new { Foo = "Value for Foo" });
+ dynamic dvdd = DynamicViewDataDictionary.Wrap(vdd);
+
+ // Act
+ object result = dvdd["Foo"];
+
+ // Assert
+ Assert.Equal("Value for Foo", result);
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/Test/DynamicViewPageTest.cs b/test/Microsoft.Web.Mvc.Test/Test/DynamicViewPageTest.cs
new file mode 100644
index 00000000..d0ca8c65
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Test/DynamicViewPageTest.cs
@@ -0,0 +1,53 @@
+using Xunit;
+
+namespace Microsoft.Web.Mvc.Test
+{
+ public class DynamicViewPageTest
+ {
+ // DynamicViewPage
+
+ [Fact]
+ public void AnonymousObjectsAreWrapped()
+ {
+ // Arrange
+ DynamicViewPage page = new DynamicViewPage();
+ page.ViewData.Model = new { foo = "Hello world!" };
+
+ // Act & Assert
+ Assert.Equal("Microsoft.Web.Mvc.DynamicReflectionObject", page.Model.GetType().FullName);
+ }
+
+ [Fact]
+ public void NonAnonymousObjectsAreNotWrapped()
+ {
+ // Arrange
+ DynamicViewPage page = new DynamicViewPage();
+ page.ViewData.Model = "Hello world!";
+
+ // Act & Assert
+ Assert.Equal(typeof(string), page.Model.GetType());
+ }
+
+ [Fact]
+ public void ViewDataDictionaryIsWrapped()
+ {
+ // Arrange
+ DynamicViewPage page = new DynamicViewPage();
+
+ // Act & Assert
+ Assert.Equal("Microsoft.Web.Mvc.DynamicViewDataDictionary", page.ViewData.GetType().FullName);
+ }
+
+ // DynamicViewPage<T>
+
+ [Fact]
+ public void Generic_ViewDataDictionaryIsWrapped()
+ {
+ // Arrange
+ DynamicViewPage<object> page = new DynamicViewPage<object>();
+
+ // Act & Assert
+ Assert.Equal("Microsoft.Web.Mvc.DynamicViewDataDictionary", page.ViewData.GetType().FullName);
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/Test/ElementalValueProviderTest.cs b/test/Microsoft.Web.Mvc.Test/Test/ElementalValueProviderTest.cs
new file mode 100644
index 00000000..5179f515
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Test/ElementalValueProviderTest.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Globalization;
+using System.Web.Mvc;
+using Xunit;
+
+namespace Microsoft.Web.Mvc.Test
+{
+ public class ElementalValueProviderTest
+ {
+ [Fact]
+ public void ContainsPrefix()
+ {
+ // Arrange
+ ElementalValueProvider valueProvider = new ElementalValueProvider("foo", 42, null);
+
+ // Act & assert
+ Assert.True(valueProvider.ContainsPrefix("foo"));
+ Assert.False(valueProvider.ContainsPrefix("bar"));
+ }
+
+ [Fact]
+ public void GetValue_NameDoesNotMatch_ReturnsNull()
+ {
+ // Arrange
+ CultureInfo culture = CultureInfo.GetCultureInfo("fr-FR");
+ DateTime rawValue = new DateTime(2001, 1, 2);
+ ElementalValueProvider valueProvider = new ElementalValueProvider("foo", rawValue, culture);
+
+ // Act
+ ValueProviderResult vpResult = valueProvider.GetValue("bar");
+
+ // Assert
+ Assert.Null(vpResult);
+ }
+
+ [Fact]
+ public void GetValue_NameMatches_ReturnsValueProviderResult()
+ {
+ // Arrange
+ CultureInfo culture = CultureInfo.GetCultureInfo("fr-FR");
+ DateTime rawValue = new DateTime(2001, 1, 2);
+ ElementalValueProvider valueProvider = new ElementalValueProvider("foo", rawValue, culture);
+
+ // Act
+ ValueProviderResult vpResult = valueProvider.GetValue("FOO");
+
+ // Assert
+ Assert.NotNull(vpResult);
+ Assert.Equal(rawValue, vpResult.RawValue);
+ Assert.Equal("02/01/2001 00:00:00", vpResult.AttemptedValue);
+ Assert.Equal(culture, vpResult.Culture);
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/Test/EmailAddressAttribueTest.cs b/test/Microsoft.Web.Mvc.Test/Test/EmailAddressAttribueTest.cs
new file mode 100644
index 00000000..0fc5ab8c
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Test/EmailAddressAttribueTest.cs
@@ -0,0 +1,42 @@
+using System.Linq;
+using System.Web.Mvc;
+using Moq;
+using Xunit;
+
+namespace Microsoft.Web.Mvc.Test
+{
+ public class EmailAddressAttribueTest
+ {
+ [Fact]
+ public void ClientRule()
+ {
+ // Arrange
+ var attribute = new EmailAddressAttribute();
+ var provider = new Mock<ModelMetadataProvider>();
+ var metadata = new ModelMetadata(provider.Object, null, null, typeof(string), "PropertyName");
+
+ // Act
+ ModelClientValidationRule clientRule = attribute.GetClientValidationRules(metadata, null).Single();
+
+ // Assert
+ Assert.Equal("email", clientRule.ValidationType);
+ Assert.Equal("The PropertyName field is not a valid e-mail address.", clientRule.ErrorMessage);
+ Assert.Empty(clientRule.ValidationParameters);
+ }
+
+ [Fact]
+ public void IsValidTests()
+ {
+ // Arrange
+ var attribute = new EmailAddressAttribute();
+
+ // Act & Assert
+ Assert.True(attribute.IsValid(null)); // Optional values are always valid
+ Assert.True(attribute.IsValid("joe@contoso.com"));
+ Assert.True(attribute.IsValid("joe%fred@contoso.com"));
+ Assert.False(attribute.IsValid("joe"));
+ Assert.False(attribute.IsValid("joe@"));
+ Assert.False(attribute.IsValid("joe@contoso"));
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/Test/ExpressionHelperTest.cs b/test/Microsoft.Web.Mvc.Test/Test/ExpressionHelperTest.cs
new file mode 100644
index 00000000..3f96bf70
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Test/ExpressionHelperTest.cs
@@ -0,0 +1,302 @@
+using System;
+using System.Linq.Expressions;
+using System.Web.Mvc;
+using System.Web.Routing;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+using ExpressionHelper = Microsoft.Web.Mvc.Internal.ExpressionHelper;
+
+namespace Microsoft.Web.Mvc.Test
+{
+ public class ExpressionHelperTest
+ {
+ [Fact]
+ public void BuildRouteValueDictionary_TargetsAsynchronousAsyncMethod_StripsSuffix()
+ {
+ // Arrange
+ Expression<Action<TestAsyncController>> expr = (c => c.AsynchronousAsync());
+
+ // Act
+ RouteValueDictionary rvd = ExpressionHelper.GetRouteValuesFromExpression(expr);
+
+ // Assert
+ Assert.Equal("Asynchronous", rvd["action"]);
+ Assert.Equal("TestAsync", rvd["controller"]);
+ Assert.False(rvd.ContainsKey("area"));
+ }
+
+ [Fact]
+ public void BuildRouteValueDictionary_TargetsAsynchronousCompletedMethod_Throws()
+ {
+ // Arrange
+ Expression<Action<TestAsyncController>> expr = (c => c.AsynchronousCompleted());
+
+ // Act & assert
+ Assert.Throws<InvalidOperationException>(
+ delegate { ExpressionHelper.GetRouteValuesFromExpression(expr); },
+ @"The method 'AsynchronousCompleted' is an asynchronous completion method and cannot be called directly.");
+ }
+
+ [Fact]
+ public void BuildRouteValueDictionary_TargetsControllerWithAreaAttribute_AddsAreaName()
+ {
+ // Arrange
+ Expression<Action<ControllerWithAreaController>> expr = c => c.Index();
+
+ // Act
+ RouteValueDictionary rvd = ExpressionHelper.GetRouteValuesFromExpression(expr);
+
+ // Assert
+ Assert.Equal("Index", rvd["action"]);
+ Assert.Equal("ControllerWithArea", rvd["controller"]);
+ Assert.Equal("the area name", rvd["area"]);
+ }
+
+ [Fact]
+ public void BuildRouteValueDictionary_TargetsNonActionMethod_Throws()
+ {
+ // Arrange
+ Expression<Action<TestController>> expr = (c => c.NotAnAction());
+
+ // Act & assert
+ Assert.Throws<InvalidOperationException>(
+ delegate { ExpressionHelper.GetRouteValuesFromExpression(expr); },
+ @"The method 'NotAnAction' is marked [NonAction] and cannot be called directly.");
+ }
+
+ [Fact]
+ public void BuildRouteValueDictionary_TargetsRenamedMethod_UsesNewName()
+ {
+ // Arrange
+ Expression<Action<TestController>> expr = (c => c.Renamed());
+
+ // Act
+ RouteValueDictionary rvd = ExpressionHelper.GetRouteValuesFromExpression(expr);
+
+ // Assert
+ Assert.Equal("NewName", rvd["action"]);
+ Assert.Equal("Test", rvd["controller"]);
+ Assert.False(rvd.ContainsKey("area"));
+ }
+
+ [Fact]
+ public void BuildRouteValueDictionary_TargetsSynchronousMethodOnAsyncController_ReturnsOriginalName()
+ {
+ // Arrange
+ Expression<Action<TestAsyncController>> expr = (c => c.Synchronous());
+
+ // Act
+ RouteValueDictionary rvd = ExpressionHelper.GetRouteValuesFromExpression(expr);
+
+ // Assert
+ Assert.Equal("Synchronous", rvd["action"]);
+ Assert.Equal("TestAsync", rvd["controller"]);
+ Assert.False(rvd.ContainsKey("area"));
+ }
+
+ [Fact]
+ public void BuildRouteValueDictionaryWithNullExpressionThrowsArgumentNullException()
+ {
+ Assert.ThrowsArgumentNull(
+ () => ExpressionHelper.GetRouteValuesFromExpression<TestController>(null),
+ "action");
+ }
+
+ [Fact]
+ public void BuildRouteValueDictionaryWithNonMethodExpressionThrowsInvalidOperationException()
+ {
+ // Arrange
+ Expression<Action<TestController>> expression = c => new TestController();
+
+ // Act & Assert
+ Assert.Throws<ArgumentException>(
+ () => ExpressionHelper.GetRouteValuesFromExpression(expression),
+ "Expression must be a method call." + Environment.NewLine + "Parameter name: action");
+ }
+
+ [Fact]
+ public void BuildRouteValueDictionaryWithoutControllerSuffixThrowsInvalidOperationException()
+ {
+ // Arrange
+ Expression<Action<TestControllerNot>> index = (c => c.Index(123));
+
+ // Act & Assert
+ Assert.Throws<ArgumentException>(
+ () => ExpressionHelper.GetRouteValuesFromExpression(index),
+ "Controller name must end in 'Controller'." + Environment.NewLine + "Parameter name: action");
+ }
+
+ [Fact]
+ public void BuildRouteValueDictionaryWithControllerBaseClassThrowsInvalidOperationException()
+ {
+ // Arrange
+ Expression<Action<Controller>> index = (c => c.Dispose());
+
+ // Act & Assert
+ Assert.Throws<ArgumentException>(
+ () => ExpressionHelper.GetRouteValuesFromExpression(index),
+ "Cannot route to class named 'Controller'." + Environment.NewLine + "Parameter name: action");
+ }
+
+ [Fact]
+ public void BuildRouteValueDictionaryAddsControllerNameToDictionary()
+ {
+ // Arrange
+ Expression<Action<TestController>> index = (c => c.Index(123));
+
+ // Act
+ RouteValueDictionary rvd = ExpressionHelper.GetRouteValuesFromExpression(index);
+
+ // Assert
+ Assert.Equal("Test", rvd["Controller"]);
+ }
+
+ [Fact]
+ public void BuildRouteValueDictionaryFromExpressionReturnsCorrectDictionary()
+ {
+ // Arrange
+ Expression<Action<TestController>> index = (c => c.Index(123));
+
+ // Act
+ RouteValueDictionary rvd = ExpressionHelper.GetRouteValuesFromExpression(index);
+
+ // Assert
+ Assert.Equal("Test", rvd["Controller"]);
+ Assert.Equal("Index", rvd["Action"]);
+ Assert.Equal(123, rvd["page"]);
+ }
+
+ [Fact]
+ public void BuildRouteValueDictionaryFromNonConstantExpressionReturnsCorrectDictionary()
+ {
+ // Arrange
+ Expression<Action<TestController>> index = (c => c.About(Foo));
+
+ // Act
+ RouteValueDictionary rvd = ExpressionHelper.GetRouteValuesFromExpression(index);
+
+ // Assert
+ Assert.Equal("Test", rvd["Controller"]);
+ Assert.Equal("About", rvd["Action"]);
+ Assert.Equal("FooValue", rvd["s"]);
+ }
+
+ [Fact]
+ public void GetInputNameFromPropertyExpressionReturnsPropertyName()
+ {
+ // Arrange
+ Expression<Func<TestModel, int>> expression = m => m.IntProperty;
+
+ // Act
+ string name = ExpressionHelper.GetInputName(expression);
+
+ // Assert
+ Assert.Equal("IntProperty", name);
+ }
+
+ [Fact]
+ public void GetInputNameFromPropertyWithMethodCallExpressionReturnsPropertyName()
+ {
+ // Arrange
+ Expression<Func<TestModel, string>> expression = m => m.IntProperty.ToString();
+
+ // Act
+ string name = ExpressionHelper.GetInputName(expression);
+
+ // Assert
+ Assert.Equal("IntProperty", name);
+ }
+
+ [Fact]
+ public void GetInputNameFromPropertyWithTwoMethodCallExpressionReturnsPropertyName()
+ {
+ // Arrange
+ Expression<Func<TestModel, string>> expression = m => m.IntProperty.ToString().ToUpper();
+
+ // Act
+ string name = ExpressionHelper.GetInputName(expression);
+
+ // Assert
+ Assert.Equal("IntProperty", name);
+ }
+
+ [Fact]
+ public void GetInputNameFromExpressionWithTwoPropertiesUsesWholeExpression()
+ {
+ // Arrange
+ Expression<Func<TestModel, int>> expression = m => m.StringProperty.Length;
+
+ // Act
+ string name = ExpressionHelper.GetInputName(expression);
+
+ // Assert
+ Assert.Equal("StringProperty.Length", name);
+ }
+
+ public class TestController : Controller
+ {
+ public ActionResult Index(int page)
+ {
+ return null;
+ }
+
+ public string About(string s)
+ {
+ return "The value is " + s;
+ }
+
+ [ActionName("NewName")]
+ public void Renamed()
+ {
+ }
+
+ [NonAction]
+ public void NotAnAction()
+ {
+ }
+ }
+
+ public class TestAsyncController : AsyncController
+ {
+ public void Synchronous()
+ {
+ }
+
+ public void AsynchronousAsync()
+ {
+ }
+
+ public void AsynchronousCompleted()
+ {
+ }
+ }
+
+ public string Foo
+ {
+ get { return "FooValue"; }
+ }
+
+ public class TestControllerNot : Controller
+ {
+ public ActionResult Index(int page)
+ {
+ return null;
+ }
+ }
+
+ [ActionLinkArea("the area name")]
+ public class ControllerWithAreaController : Controller
+ {
+ public ActionResult Index()
+ {
+ return null;
+ }
+ }
+
+ public class TestModel
+ {
+ public int IntProperty { get; set; }
+ public string StringProperty { get; set; }
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/Test/FileExtensionsAttributeTest.cs b/test/Microsoft.Web.Mvc.Test/Test/FileExtensionsAttributeTest.cs
new file mode 100644
index 00000000..35386414
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Test/FileExtensionsAttributeTest.cs
@@ -0,0 +1,53 @@
+using System.Linq;
+using System.Web.Mvc;
+using Moq;
+using Xunit;
+
+namespace Microsoft.Web.Mvc.Test
+{
+ public class FileExtensionsAttributeTest
+ {
+ [Fact]
+ public void DefaultExtensions()
+ {
+ Assert.Equal("png,jpg,jpeg,gif", new FileExtensionsAttribute().Extensions);
+ }
+
+ [Fact]
+ public void ClientRule()
+ {
+ // Arrange
+ var attribute = new FileExtensionsAttribute { Extensions = " FoO, .bar,baz " };
+ var provider = new Mock<ModelMetadataProvider>();
+ var metadata = new ModelMetadata(provider.Object, null, null, typeof(string), "PropertyName");
+
+ // Act
+ ModelClientValidationRule clientRule = attribute.GetClientValidationRules(metadata, null).Single();
+
+ // Assert
+ Assert.Equal("accept", clientRule.ValidationType);
+ Assert.Equal("The PropertyName field only accepts files with the following extensions: .foo, .bar, .baz", clientRule.ErrorMessage);
+ Assert.Single(clientRule.ValidationParameters);
+ Assert.Equal("foo,bar,baz", clientRule.ValidationParameters["exts"]);
+ }
+
+ [Fact]
+ public void IsValidTests()
+ {
+ // Arrange
+ var attribute = new FileExtensionsAttribute();
+
+ // Act & Assert
+ Assert.True(attribute.IsValid(null)); // Optional values are always valid
+ Assert.True(attribute.IsValid("foo.png"));
+ Assert.True(attribute.IsValid("foo.jpeg"));
+ Assert.True(attribute.IsValid("foo.jpg"));
+ Assert.True(attribute.IsValid("foo.gif"));
+ Assert.True(attribute.IsValid(@"C:\Foo\baz.jpg"));
+ Assert.False(attribute.IsValid("foo"));
+ Assert.False(attribute.IsValid("foo.png.pif"));
+ Assert.False(attribute.IsValid(@"C:\foo.png\bar"));
+ Assert.False(attribute.IsValid("\0foo.png")); // Illegal character
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/Test/FormExtensionsTest.cs b/test/Microsoft.Web.Mvc.Test/Test/FormExtensionsTest.cs
new file mode 100644
index 00000000..5b9f8874
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Test/FormExtensionsTest.cs
@@ -0,0 +1,98 @@
+using System;
+using System.IO;
+using System.Web;
+using System.Web.Mvc;
+using System.Web.Routing;
+using Moq;
+using Xunit;
+
+namespace Microsoft.Web.Mvc.Test
+{
+ public class FormExtensionsTest
+ {
+ internal const string AppPathModifier = "/$(SESSION)";
+
+ [Fact]
+ public void FormWithPostAction()
+ {
+ // Arrange
+ StringWriter writer;
+ HtmlHelper htmlHelper = GetFormHelper(out writer);
+
+ // Act
+ IDisposable formDisposable = htmlHelper.BeginForm<FormController>(action => action.About());
+ formDisposable.Dispose();
+
+ // Assert
+ Assert.Equal(@"<form action=""" + AppPathModifier + @"/Form/About"" method=""post""></form>", writer.ToString());
+ }
+
+ [Fact]
+ public void FormWithPostActionAndObjectAttributes()
+ {
+ // Arrange
+ StringWriter writer;
+ HtmlHelper htmlHelper = GetFormHelper(out writer);
+
+ // Act
+ IDisposable formDisposable = htmlHelper.BeginForm<FormController>(action => action.About(), FormMethod.Get, new { baz = "baz" });
+ formDisposable.Dispose();
+
+ // Assert
+ Assert.Equal(@"<form action=""" + AppPathModifier + @"/Form/About"" baz=""baz"" method=""get""></form>", writer.ToString());
+ }
+
+ [Fact]
+ public void FormWithPostActionAndObjectAttributesWithUnderscores()
+ {
+ // Arrange
+ StringWriter writer;
+ HtmlHelper htmlHelper = GetFormHelper(out writer);
+
+ // Act
+ IDisposable formDisposable = htmlHelper.BeginForm<FormController>(action => action.About(), FormMethod.Get, new { foo_baz = "baz" });
+ formDisposable.Dispose();
+
+ // Assert
+ Assert.Equal(@"<form action=""" + AppPathModifier + @"/Form/About"" foo-baz=""baz"" method=""get""></form>", writer.ToString());
+ }
+
+ public class FormController : Controller
+ {
+ public ActionResult About()
+ {
+ return RedirectToAction("foo");
+ }
+ }
+
+ private static HtmlHelper GetFormHelper(out StringWriter writer)
+ {
+ Mock<HttpRequestBase> mockHttpRequest = new Mock<HttpRequestBase>();
+ mockHttpRequest.Setup(r => r.Url).Returns(new Uri("http://www.contoso.com/some/path"));
+ Mock<HttpResponseBase> mockHttpResponse = new Mock<HttpResponseBase>(MockBehavior.Strict);
+
+ mockHttpResponse.Setup(r => r.ApplyAppPathModifier(It.IsAny<string>())).Returns<string>(r => AppPathModifier + r);
+ Mock<HttpContextBase> mockHttpContext = new Mock<HttpContextBase>();
+ mockHttpContext.Setup(c => c.Request).Returns(mockHttpRequest.Object);
+ mockHttpContext.Setup(c => c.Response).Returns(mockHttpResponse.Object);
+ RouteCollection rt = new RouteCollection();
+ rt.Add(new Route("{controller}/{action}/{id}", null) { Defaults = new RouteValueDictionary(new { id = "defaultid" }) });
+ rt.Add("namedroute", new Route("named/{controller}/{action}/{id}", null) { Defaults = new RouteValueDictionary(new { id = "defaultid" }) });
+ RouteData rd = new RouteData();
+ rd.Values.Add("controller", "home");
+ rd.Values.Add("action", "oldaction");
+
+ Mock<ViewContext> mockViewContext = new Mock<ViewContext>();
+ mockViewContext.Setup(c => c.HttpContext).Returns(mockHttpContext.Object);
+ mockViewContext.Setup(c => c.RouteData).Returns(rd);
+ writer = new StringWriter();
+ mockViewContext.Setup(c => c.Writer).Returns(writer);
+
+ HtmlHelper helper = new HtmlHelper(
+ mockViewContext.Object,
+ new Mock<IViewDataContainer>().Object,
+ rt);
+ return helper;
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/Test/ImageExtensionsTest.cs b/test/Microsoft.Web.Mvc.Test/Test/ImageExtensionsTest.cs
new file mode 100644
index 00000000..ab5ddb74
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Test/ImageExtensionsTest.cs
@@ -0,0 +1,130 @@
+using System.Web.Mvc;
+using System.Web.Routing;
+using Microsoft.Web.UnitTestUtil;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace Microsoft.Web.Mvc.Test
+{
+ public class ImageExtensionsTest
+ {
+ [Fact]
+ public void ImageWithEmptyRelativeUrlThrowsArgumentNullException()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+ Assert.ThrowsArgumentNullOrEmpty(() => html.Image(null), "imageRelativeUrl");
+ }
+
+ [Fact]
+ public void ImageStaticWithEmptyRelativeUrlThrowsArgumentNullException()
+ {
+ Assert.ThrowsArgumentNullOrEmpty(() => ImageExtensions.Image((string)null, "alt", null), "imageUrl");
+ }
+
+ [Fact]
+ public void ImageWithRelativeUrlRendersProperImageTag()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+ MvcHtmlString imageResult = html.Image("/system/web/mvc.jpg");
+ // NOTE: Although XHTML requires an alt tag, we don't construct one for you. Specify it yourself.
+ Assert.Equal("<img src=\"/system/web/mvc.jpg\" />", imageResult.ToHtmlString());
+ }
+
+ [Fact]
+ public void ImageWithWithAttributesWithUnderscores()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+ MvcHtmlString imageResult = html.Image("/system/web/mvc.jpg", new { foo_bar = "baz" });
+ Assert.Equal("<img foo-bar=\"baz\" src=\"/system/web/mvc.jpg\" />", imageResult.ToHtmlString());
+ }
+
+ [Fact]
+ public void ImageWithAltValueRendersImageWithAltTag()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+ MvcHtmlString imageResult = html.Image("/system/web/mvc.jpg", "this is an alt value");
+ Assert.Equal("<img alt=\"this is an alt value\" src=\"/system/web/mvc.jpg\" title=\"this is an alt value\" />", imageResult.ToHtmlString());
+ }
+
+ [Fact]
+ public void ImageWithAltValueInObjectDictionaryRendersImageWithAltAndTitleTag()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+ MvcHtmlString imageResult = html.Image("/system/web/mvc.jpg", new { alt = "this is an alt value" });
+ Assert.Equal("<img alt=\"this is an alt value\" src=\"/system/web/mvc.jpg\" title=\"this is an alt value\" />", imageResult.ToHtmlString());
+ }
+
+ [Fact]
+ public void ImageWithAltValueHtmlAttributeEncodesAltTag()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+ MvcHtmlString imageResult = html.Image("/system/web/mvc.jpg", @"<"">");
+ Assert.Equal("<img alt=\"&lt;&quot;>\" src=\"/system/web/mvc.jpg\" title=\"&lt;&quot;>\" />", imageResult.ToHtmlString());
+ }
+
+ [Fact]
+ public void ImageWithAltValueInObjectDictionaryHtmlAttributeEncodesAltTag()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+ MvcHtmlString imageResult = html.Image("/system/web/mvc.jpg", new { alt = "this is an alt value" });
+ Assert.Equal("<img alt=\"this is an alt value\" src=\"/system/web/mvc.jpg\" title=\"this is an alt value\" />", imageResult.ToHtmlString());
+ }
+
+ [Fact]
+ public void ImageWithAltSpecifiedAndInDictionaryRendersExplicit()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+ MvcHtmlString imageResult = html.Image("/system/web/mvc.jpg", "specified-alt", new { alt = "object-dictionary-alt" });
+ Assert.Equal("<img alt=\"object-dictionary-alt\" src=\"/system/web/mvc.jpg\" title=\"object-dictionary-alt\" />", imageResult.ToHtmlString());
+ }
+
+ [Fact]
+ public void ImageWithAltAndAttributesWithUnderscores()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+ MvcHtmlString imageResult = html.Image("/system/web/mvc.jpg", "specified-alt", new { foo_bar = "baz" });
+ Assert.Equal("<img alt=\"specified-alt\" foo-bar=\"baz\" src=\"/system/web/mvc.jpg\" title=\"specified-alt\" />", imageResult.ToHtmlString());
+ }
+
+ [Fact]
+ public void ImageWithSrcSpecifiedAndInDictionaryRendersExplicit()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+ MvcHtmlString imageResult = html.Image("/system/web/mvc.jpg", new { src = "explicit.jpg" });
+ Assert.Equal("<img src=\"explicit.jpg\" />", imageResult.ToHtmlString());
+ }
+
+ [Fact]
+ public void ImageWithOtherAttributesRendersThoseAttributesCaseSensitively()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+ MvcHtmlString imageResult = html.Image("/system/web/mvc.jpg", new { width = 100, Height = 200 });
+ Assert.Equal("<img Height=\"200\" src=\"/system/web/mvc.jpg\" width=\"100\" />", imageResult.ToHtmlString());
+ }
+
+ [Fact]
+ public void ImageWithUrlAndDictionaryRendersAttributes()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+ var attributes = new RouteValueDictionary(new { width = 125 });
+ MvcHtmlString imageResult = html.Image("/system/web/mvc.jpg", attributes);
+ Assert.Equal("<img src=\"/system/web/mvc.jpg\" width=\"125\" />", imageResult.ToHtmlString());
+ }
+
+ [Fact]
+ public void ImageWithTildePathAndAppPathResolvesCorrectly()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary(), "/app");
+ MvcHtmlString imageResult = html.Image("~/system/web/mvc.jpg");
+ Assert.Equal("<img src=\"/$(SESSION)/app/system/web/mvc.jpg\" />", imageResult.ToHtmlString());
+ }
+
+ [Fact]
+ public void ImageWithTildePathWithoutAppPathResolvesCorrectly()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary(), "/");
+ MvcHtmlString imageResult = html.Image("~/system/web/mvc.jpg");
+ Assert.Equal("<img src=\"/$(SESSION)/system/web/mvc.jpg\" />", imageResult.ToHtmlString());
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/Test/MailToExtensionsTest.cs b/test/Microsoft.Web.Mvc.Test/Test/MailToExtensionsTest.cs
new file mode 100644
index 00000000..eb35f84e
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Test/MailToExtensionsTest.cs
@@ -0,0 +1,132 @@
+using System.Web.Mvc;
+using System.Web.Routing;
+using Microsoft.Web.UnitTestUtil;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace Microsoft.Web.Mvc.Test
+{
+ public class MailToExtensionsTest
+ {
+ [Fact]
+ public void MailToWithoutEmailThrowsArgumentNullException()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+ Assert.ThrowsArgumentNull(() => html.Mailto("link text", null), "emailAddress");
+ }
+
+ [Fact]
+ public void MailToWithoutLinkTextThrowsArgumentNullException()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+ Assert.ThrowsArgumentNull(() => html.Mailto(null, "somebody@example.com"), "linkText");
+ }
+
+ [Fact]
+ public void MailToWithLinkTextAndEmailRendersProperElement()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+ MvcHtmlString result = html.Mailto("This is a test", "test@example.com");
+ Assert.Equal("<a href=\"mailto:test@example.com\">This is a test</a>", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void MailToWithLinkTextEmailAndHtmlAttributesRendersAttributes()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+ MvcHtmlString result = html.Mailto("This is a test", "test@example.com", new { title = "this is a test" });
+ Assert.Equal("<a href=\"mailto:test@example.com\" title=\"this is a test\">This is a test</a>", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void MailToWithLinkTextEmailAndHtmlAttributesDictionaryRendersAttributes()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+ MvcHtmlString result = html.Mailto("This is a test", "test@example.com", new RouteValueDictionary(new { title = "this is a test" }));
+ Assert.Equal("<a href=\"mailto:test@example.com\" title=\"this is a test\">This is a test</a>", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void MailToWithSubjectAndHtmlAttributesRendersAttributes()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+ MvcHtmlString result = html.Mailto("This is a test", "test@example.com", "The subject", new { title = "this is a test" });
+ Assert.Equal("<a href=\"mailto:test@example.com?subject=The subject\" title=\"this is a test\">This is a test</a>", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void MailToWithSubjectAndHtmlAttributesDictionaryRendersAttributes()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+ MvcHtmlString result = html.Mailto("This is a test", "test@example.com", "The subject", new RouteValueDictionary(new { title = "this is a test" }));
+ Assert.Equal("<a href=\"mailto:test@example.com?subject=The subject\" title=\"this is a test\">This is a test</a>", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void MailToAttributeEncodesEmail()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+ MvcHtmlString result = html.Mailto("This is a test", "te\">st@example.com");
+ Assert.Equal("<a href=\"mailto:te&quot;>st@example.com\">This is a test</a>", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void MailToWithMultipleRecipientsRendersWithCommas()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+ MvcHtmlString result = html.Mailto("This is a test", "te\">st@example.com,test2@example.com");
+ Assert.Equal("<a href=\"mailto:te&quot;>st@example.com,test2@example.com\">This is a test</a>", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void MailToWithSubjectAppendsSubjectQuery()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+ MvcHtmlString result = html.Mailto("This is a test", "test@example.com", "This is the subject");
+ Assert.Equal("<a href=\"mailto:test@example.com?subject=This is the subject\">This is a test</a>", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void MailToWithCopyOnlyAppendsCopyQuery()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+ MvcHtmlString result = html.Mailto("This is a test", "test@example.com", null, null, "cctest@example.com", null, null);
+ Assert.Equal("<a href=\"mailto:test@example.com?cc=cctest@example.com\">This is a test</a>", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void MailToWithMultipartBodyRendersProperMailtoEncoding()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+ string body = @"Line one
+Line two
+Line three";
+ MvcHtmlString result = html.Mailto("email me", "test@example.com", null, body, null, null, null);
+ Assert.Equal("<a href=\"mailto:test@example.com?body=Line one%0ALine two%0ALine three\">email me</a>", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void MailToWithAllValuesProvidedRendersCorrectTag()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+ string body = @"Line one
+Line two
+Line three";
+ MvcHtmlString result = html.Mailto("email me", "test@example.com", "the subject", body, "cc@example.com", "bcc@example.com", new { title = "email test" });
+ string expected = @"<a href=""mailto:test@example.com?subject=the subject&amp;cc=cc@example.com&amp;bcc=bcc@example.com&amp;body=Line one%0ALine two%0ALine three"" title=""email test"">email me</a>";
+ Assert.Equal(expected, result.ToHtmlString());
+ }
+
+ [Fact]
+ public void MailToWithAttributesWithUnderscores()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+ string body = @"Line one
+Line two
+Line three";
+ MvcHtmlString result = html.Mailto("email me", "test@example.com", "the subject", body, "cc@example.com", "bcc@example.com", new { foo_bar = "baz" });
+ string expected = @"<a foo-bar=""baz"" href=""mailto:test@example.com?subject=the subject&amp;cc=cc@example.com&amp;bcc=bcc@example.com&amp;body=Line one%0ALine two%0ALine three"">email me</a>";
+ Assert.Equal(expected, result.ToHtmlString());
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/Test/ModelCopierTest.cs b/test/Microsoft.Web.Mvc.Test/Test/ModelCopierTest.cs
new file mode 100644
index 00000000..6f250090
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Test/ModelCopierTest.cs
@@ -0,0 +1,223 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using Xunit;
+
+namespace Microsoft.Web.Mvc.Test
+{
+ public class ModelCopierTest
+ {
+ [Fact]
+ public void CopyCollection_FromIsNull_DoesNothing()
+ {
+ // Arrange
+ int[] from = null;
+ List<int> to = new List<int> { 1, 2, 3 };
+
+ // Act
+ ModelCopier.CopyCollection(from, to);
+
+ // Assert
+ Assert.Equal(new[] { 1, 2, 3 }, to.ToArray());
+ }
+
+ [Fact]
+ public void CopyCollection_ToIsImmutable_DoesNothing()
+ {
+ // Arrange
+ List<int> from = new List<int> { 1, 2, 3 };
+ ICollection<int> to = new ReadOnlyCollection<int>(new[] { 4, 5, 6 });
+
+ // Act
+ ModelCopier.CopyCollection(from, to);
+
+ // Assert
+ Assert.Equal(new[] { 1, 2, 3 }, from.ToArray());
+ Assert.Equal(new[] { 4, 5, 6 }, to.ToArray());
+ }
+
+ [Fact]
+ public void CopyCollection_ToIsMmutable_ClearsAndCopies()
+ {
+ // Arrange
+ List<int> from = new List<int> { 1, 2, 3 };
+ ICollection<int> to = new List<int> { 4, 5, 6 };
+
+ // Act
+ ModelCopier.CopyCollection(from, to);
+
+ // Assert
+ Assert.Equal(new[] { 1, 2, 3 }, from.ToArray());
+ Assert.Equal(new[] { 1, 2, 3 }, to.ToArray());
+ }
+
+ [Fact]
+ public void CopyCollection_ToIsNull_DoesNothing()
+ {
+ // Arrange
+ List<int> from = new List<int> { 1, 2, 3 };
+ List<int> to = null;
+
+ // Act
+ ModelCopier.CopyCollection(from, to);
+
+ // Assert
+ Assert.Equal(new[] { 1, 2, 3 }, from.ToArray());
+ }
+
+ [Fact]
+ public void CopyModel_ExactTypeMatch_Copies()
+ {
+ // Arrange
+ GenericModel<int> from = new GenericModel<int> { TheProperty = 21 };
+ GenericModel<int> to = new GenericModel<int> { TheProperty = 42 };
+
+ // Act
+ ModelCopier.CopyModel(from, to);
+
+ // Assert
+ Assert.Equal(21, from.TheProperty);
+ Assert.Equal(21, to.TheProperty);
+ }
+
+ [Fact]
+ public void CopyModel_FromIsNull_DoesNothing()
+ {
+ // Arrange
+ GenericModel<int> from = null;
+ GenericModel<int> to = new GenericModel<int> { TheProperty = 42 };
+
+ // Act
+ ModelCopier.CopyModel(from, to);
+
+ // Assert
+ Assert.Equal(42, to.TheProperty);
+ }
+
+ [Fact]
+ public void CopyModel_LiftedTypeMatch_ActualValueIsNotNull_Copies()
+ {
+ // Arrange
+ GenericModel<int?> from = new GenericModel<int?> { TheProperty = 21 };
+ GenericModel<int> to = new GenericModel<int> { TheProperty = 42 };
+
+ // Act
+ ModelCopier.CopyModel(from, to);
+
+ // Assert
+ Assert.Equal(21, from.TheProperty);
+ Assert.Equal(21, to.TheProperty);
+ }
+
+ [Fact]
+ public void CopyModel_LiftedTypeMatch_ActualValueIsNull_DoesNothing()
+ {
+ // Arrange
+ GenericModel<int?> from = new GenericModel<int?> { TheProperty = null };
+ GenericModel<int> to = new GenericModel<int> { TheProperty = 42 };
+
+ // Act
+ ModelCopier.CopyModel(from, to);
+
+ // Assert
+ Assert.Null(from.TheProperty);
+ Assert.Equal(42, to.TheProperty);
+ }
+
+ [Fact]
+ public void CopyModel_NoTypeMatch_DoesNothing()
+ {
+ // Arrange
+ GenericModel<int> from = new GenericModel<int> { TheProperty = 21 };
+ GenericModel<long> to = new GenericModel<long> { TheProperty = 42 };
+
+ // Act
+ ModelCopier.CopyModel(from, to);
+
+ // Assert
+ Assert.Equal(21, from.TheProperty);
+ Assert.Equal(42, to.TheProperty);
+ }
+
+ [Fact]
+ public void CopyModel_SubclassedTypeMatch_Copies()
+ {
+ // Arrange
+ string originalModel = "Hello, world!";
+
+ GenericModel<string> from = new GenericModel<string> { TheProperty = originalModel };
+ GenericModel<object> to = new GenericModel<object> { TheProperty = 42 };
+
+ // Act
+ ModelCopier.CopyModel(from, to);
+
+ // Assert
+ Assert.Same(originalModel, from.TheProperty);
+ Assert.Same(originalModel, to.TheProperty);
+ }
+
+ [Fact]
+ public void CopyModel_ToDoesNotContainProperty_DoesNothing()
+ {
+ // Arrange
+ GenericModel<int> from = new GenericModel<int> { TheProperty = 21 };
+ OtherGenericModel<int> to = new OtherGenericModel<int> { SomeOtherProperty = 42 };
+
+ // Act
+ ModelCopier.CopyModel(from, to);
+
+ // Assert
+ Assert.Equal(21, from.TheProperty);
+ Assert.Equal(42, to.SomeOtherProperty);
+ }
+
+ [Fact]
+ public void CopyModel_ToIsNull_DoesNothing()
+ {
+ // Arrange
+ GenericModel<int> from = new GenericModel<int> { TheProperty = 21 };
+ GenericModel<int> to = null;
+
+ // Act
+ ModelCopier.CopyModel(from, to);
+
+ // Assert
+ Assert.Equal(21, from.TheProperty);
+ }
+
+ [Fact]
+ public void CopyModel_ToIsReadOnly_DoesNothing()
+ {
+ // Arrange
+ GenericModel<int> from = new GenericModel<int> { TheProperty = 21 };
+ ReadOnlyGenericModel<int> to = new ReadOnlyGenericModel<int>(42);
+
+ // Act
+ ModelCopier.CopyModel(from, to);
+
+ // Assert
+ Assert.Equal(21, from.TheProperty);
+ Assert.Equal(42, to.TheProperty);
+ }
+
+ private class GenericModel<T>
+ {
+ public T TheProperty { get; set; }
+ }
+
+ private class OtherGenericModel<T>
+ {
+ public T SomeOtherProperty { get; set; }
+ }
+
+ private class ReadOnlyGenericModel<T>
+ {
+ public ReadOnlyGenericModel(T propertyValue)
+ {
+ TheProperty = propertyValue;
+ }
+
+ public T TheProperty { get; private set; }
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/Test/MvcSerializerTest.cs b/test/Microsoft.Web.Mvc.Test/Test/MvcSerializerTest.cs
new file mode 100644
index 00000000..b51bb344
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Test/MvcSerializerTest.cs
@@ -0,0 +1,122 @@
+using System;
+using System.Runtime.Serialization;
+using System.Web.Security;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace Microsoft.Web.Mvc.Test
+{
+ public class MvcSerializerTest
+ {
+ [Fact]
+ public void DeserializeThrowsIfModeIsOutOfRange()
+ {
+ // Arrange
+ MvcSerializer serializer = new MvcSerializer();
+
+ // Act & assert
+ Assert.ThrowsArgumentOutOfRange(
+ delegate { serializer.Serialize("blah", (SerializationMode)(-1)); },
+ "mode",
+ @"The provided SerializationMode is invalid.");
+ }
+
+ [Fact]
+ public void DeserializeThrowsIfSerializedValueIsCorrupt()
+ {
+ // Arrange
+ IMachineKey machineKey = new MockMachineKey();
+
+ // Act & assert
+ Exception exception = Assert.Throws<SerializationException>(
+ delegate { MvcSerializer.Deserialize("This is a corrupted value.", SerializationMode.Signed, machineKey); },
+ @"Deserialization failed. Verify that the data is being deserialized using the same SerializationMode with which it was serialized. Otherwise see the inner exception.");
+
+ Assert.NotNull(exception.InnerException);
+ }
+
+ [Fact]
+ public void DeserializeThrowsIfSerializedValueIsEmpty()
+ {
+ // Arrange
+ MvcSerializer serializer = new MvcSerializer();
+
+ // Act & assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { serializer.Deserialize("", SerializationMode.Signed); }, "serializedValue");
+ }
+
+ [Fact]
+ public void DeserializeThrowsIfSerializedValueIsNull()
+ {
+ // Arrange
+ MvcSerializer serializer = new MvcSerializer();
+
+ // Act & assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { serializer.Deserialize(null, SerializationMode.Signed); }, "serializedValue");
+ }
+
+ [Fact]
+ public void SerializeAllowsNullValues()
+ {
+ // Arrange
+ IMachineKey machineKey = new MockMachineKey();
+
+ // Act
+ string serializedValue = MvcSerializer.Serialize(null, SerializationMode.EncryptedAndSigned, machineKey);
+
+ // Assert
+ Assert.Equal(@"All-LPgGI1dzEbp3B2FueVR5cGUuA25pbIYJAXozaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS8yMDAzLzEwL1NlcmlhbGl6YXRpb24vCQFpKWh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlAQ==", serializedValue);
+ }
+
+ [Fact]
+ public void SerializeAndDeserializeRoundTripsValue()
+ {
+ // Arrange
+ IMachineKey machineKey = new MockMachineKey();
+
+ // Act
+ string serializedValue = MvcSerializer.Serialize(42, SerializationMode.EncryptedAndSigned, machineKey);
+ object deserializedValue = MvcSerializer.Deserialize(serializedValue, SerializationMode.EncryptedAndSigned, machineKey);
+
+ // Assert
+ Assert.Equal(42, deserializedValue);
+ }
+
+ [Fact]
+ public void SerializeThrowsIfModeIsOutOfRange()
+ {
+ // Arrange
+ MvcSerializer serializer = new MvcSerializer();
+
+ // Act & assert
+ Assert.ThrowsArgumentOutOfRange(
+ delegate { serializer.Serialize(null, (SerializationMode)(-1)); },
+ "mode",
+ @"The provided SerializationMode is invalid.");
+ }
+
+ private sealed class MockMachineKey : IMachineKey
+ {
+ public byte[] Decode(string encodedData, MachineKeyProtection protectionOption)
+ {
+ string optionString = protectionOption.ToString();
+ if (encodedData.StartsWith(optionString, StringComparison.Ordinal))
+ {
+ encodedData = encodedData.Substring(optionString.Length + 1);
+ }
+ else
+ {
+ throw new Exception("Corrupted data.");
+ }
+ return Convert.FromBase64String(encodedData);
+ }
+
+ public string Encode(byte[] data, MachineKeyProtection protectionOption)
+ {
+ return protectionOption.ToString() + "-" + Convert.ToBase64String(data);
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/Test/RadioExtensionsTest.cs b/test/Microsoft.Web.Mvc.Test/Test/RadioExtensionsTest.cs
new file mode 100644
index 00000000..699e8708
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Test/RadioExtensionsTest.cs
@@ -0,0 +1,199 @@
+using System.Collections.Generic;
+using System.Web.Mvc;
+using System.Web.Routing;
+using Microsoft.Web.UnitTestUtil;
+using Xunit;
+
+namespace Microsoft.Web.Mvc.Test
+{
+ public class RadioExtensionsTest
+ {
+ [Fact]
+ public void RadioButtonListNothingSelected()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+
+ // Act
+ MvcHtmlString[] html = htmlHelper.RadioButtonList("FooList", GetRadioButtonListData(false));
+
+ // Assert
+ Assert.Equal(@"<input id=""FooList"" name=""FooList"" type=""radio"" value=""foo"" />", html[0].ToHtmlString());
+ Assert.Equal(@"<input id=""FooList"" name=""FooList"" type=""radio"" value=""bar"" />", html[1].ToHtmlString());
+ Assert.Equal(@"<input id=""FooList"" name=""FooList"" type=""radio"" value=""baz"" />", html[2].ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonListItemSelected()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+
+ // Act
+ MvcHtmlString[] html = htmlHelper.RadioButtonList("FooList", GetRadioButtonListData(true));
+
+ // Assert
+ Assert.Equal(@"<input id=""FooList"" name=""FooList"" type=""radio"" value=""foo"" />", html[0].ToHtmlString());
+ Assert.Equal(@"<input id=""FooList"" name=""FooList"" type=""radio"" value=""bar"" />", html[1].ToHtmlString());
+ Assert.Equal(@"<input checked=""checked"" id=""FooList"" name=""FooList"" type=""radio"" value=""baz"" />", html[2].ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonListItemSelectedWithValueFromViewData()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(new ViewDataDictionary(new { foolist = "bar" }));
+
+ // Act
+ MvcHtmlString[] html = htmlHelper.RadioButtonList("FooList", GetRadioButtonListData(false));
+
+ // Assert
+ Assert.Equal(@"<input id=""FooList"" name=""FooList"" type=""radio"" value=""foo"" />", html[0].ToHtmlString());
+ Assert.Equal(@"<input checked=""checked"" id=""FooList"" name=""FooList"" type=""radio"" value=""bar"" />", html[1].ToHtmlString());
+ Assert.Equal(@"<input id=""FooList"" name=""FooList"" type=""radio"" value=""baz"" />", html[2].ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonListWithObjectAttributes()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+
+ // Act
+ MvcHtmlString[] html = htmlHelper.RadioButtonList("FooList", GetRadioButtonListData(true), new { attr1 = "value1" });
+
+ // Assert
+ Assert.Equal(@"<input attr1=""value1"" id=""FooList"" name=""FooList"" type=""radio"" value=""foo"" />", html[0].ToHtmlString());
+ Assert.Equal(@"<input attr1=""value1"" id=""FooList"" name=""FooList"" type=""radio"" value=""bar"" />", html[1].ToHtmlString());
+ Assert.Equal(@"<input attr1=""value1"" checked=""checked"" id=""FooList"" name=""FooList"" type=""radio"" value=""baz"" />", html[2].ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonListWithObjectAttributesWithUnderscores()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+
+ // Act
+ MvcHtmlString[] html = htmlHelper.RadioButtonList("FooList", GetRadioButtonListData(true), new { foo_bar = "baz" });
+
+ // Assert
+ Assert.Equal(@"<input foo-bar=""baz"" id=""FooList"" name=""FooList"" type=""radio"" value=""foo"" />", html[0].ToHtmlString());
+ Assert.Equal(@"<input foo-bar=""baz"" id=""FooList"" name=""FooList"" type=""radio"" value=""bar"" />", html[1].ToHtmlString());
+ Assert.Equal(@"<input checked=""checked"" foo-bar=""baz"" id=""FooList"" name=""FooList"" type=""radio"" value=""baz"" />", html[2].ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonListWithDictionaryAttributes()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+
+ // Act
+ MvcHtmlString[] html = htmlHelper.RadioButtonList("FooList", GetRadioButtonListData(true), new RouteValueDictionary(new { attr1 = "value1" }));
+
+ // Assert
+ Assert.Equal(@"<input attr1=""value1"" id=""FooList"" name=""FooList"" type=""radio"" value=""foo"" />", html[0].ToHtmlString());
+ Assert.Equal(@"<input attr1=""value1"" id=""FooList"" name=""FooList"" type=""radio"" value=""bar"" />", html[1].ToHtmlString());
+ Assert.Equal(@"<input attr1=""value1"" checked=""checked"" id=""FooList"" name=""FooList"" type=""radio"" value=""baz"" />", html[2].ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonListNothingSelectedWithSelectListFromViewData()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(GetRadioButtonListViewData(false));
+
+ // Act
+ MvcHtmlString[] html = htmlHelper.RadioButtonList("FooList");
+
+ // Assert
+ Assert.Equal(@"<input id=""FooList"" name=""FooList"" type=""radio"" value=""foo"" />", html[0].ToHtmlString());
+ Assert.Equal(@"<input id=""FooList"" name=""FooList"" type=""radio"" value=""bar"" />", html[1].ToHtmlString());
+ Assert.Equal(@"<input id=""FooList"" name=""FooList"" type=""radio"" value=""baz"" />", html[2].ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonListItemSelectedWithSelectListFromViewData()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(GetRadioButtonListViewData(true));
+
+ // Act
+ MvcHtmlString[] html = htmlHelper.RadioButtonList("FooList");
+
+ // Assert
+ Assert.Equal(@"<input id=""FooList"" name=""FooList"" type=""radio"" value=""foo"" />", html[0].ToHtmlString());
+ Assert.Equal(@"<input id=""FooList"" name=""FooList"" type=""radio"" value=""bar"" />", html[1].ToHtmlString());
+ Assert.Equal(@"<input checked=""checked"" id=""FooList"" name=""FooList"" type=""radio"" value=""baz"" />", html[2].ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonListWithObjectAttributesWithSelectListFromViewData()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(GetRadioButtonListViewData(true));
+
+ // Act
+ MvcHtmlString[] html = htmlHelper.RadioButtonList("FooList", new { attr1 = "value1" });
+
+ // Assert
+ Assert.Equal(@"<input attr1=""value1"" id=""FooList"" name=""FooList"" type=""radio"" value=""foo"" />", html[0].ToHtmlString());
+ Assert.Equal(@"<input attr1=""value1"" id=""FooList"" name=""FooList"" type=""radio"" value=""bar"" />", html[1].ToHtmlString());
+ Assert.Equal(@"<input attr1=""value1"" checked=""checked"" id=""FooList"" name=""FooList"" type=""radio"" value=""baz"" />", html[2].ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonListWithObjectAttributesWithUnderscoresWithSelectListFromViewData()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(GetRadioButtonListViewData(true));
+
+ // Act
+ MvcHtmlString[] html = htmlHelper.RadioButtonList("FooList", new { foo_bar = "baz" });
+
+ // Assert
+ Assert.Equal(@"<input foo-bar=""baz"" id=""FooList"" name=""FooList"" type=""radio"" value=""foo"" />", html[0].ToHtmlString());
+ Assert.Equal(@"<input foo-bar=""baz"" id=""FooList"" name=""FooList"" type=""radio"" value=""bar"" />", html[1].ToHtmlString());
+ Assert.Equal(@"<input checked=""checked"" foo-bar=""baz"" id=""FooList"" name=""FooList"" type=""radio"" value=""baz"" />", html[2].ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonListWithDictionaryAttributesWithSelectListFromViewData()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(GetRadioButtonListViewData(true));
+
+ // Act
+ MvcHtmlString[] html = htmlHelper.RadioButtonList("FooList", new RouteValueDictionary(new { attr1 = "value1" }));
+
+ // Assert
+ Assert.Equal(@"<input attr1=""value1"" id=""FooList"" name=""FooList"" type=""radio"" value=""foo"" />", html[0].ToHtmlString());
+ Assert.Equal(@"<input attr1=""value1"" id=""FooList"" name=""FooList"" type=""radio"" value=""bar"" />", html[1].ToHtmlString());
+ Assert.Equal(@"<input attr1=""value1"" checked=""checked"" id=""FooList"" name=""FooList"" type=""radio"" value=""baz"" />", html[2].ToHtmlString());
+ }
+
+ private static SelectList GetRadioButtonListData(bool selectBaz)
+ {
+ List<RadioItem> list = new List<RadioItem>();
+ list.Add(new RadioItem { Text = "text-foo", Value = "foo" });
+ list.Add(new RadioItem { Text = "text-bar", Value = "bar" });
+ list.Add(new RadioItem { Text = "text-baz", Value = "baz" });
+ return new SelectList(list, "value", "TEXT", selectBaz ? "baz" : "something-else");
+ }
+
+ private static ViewDataDictionary GetRadioButtonListViewData(bool selectBaz)
+ {
+ ViewDataDictionary viewData = new ViewDataDictionary();
+ viewData["FooList"] = GetRadioButtonListData(selectBaz);
+ return viewData;
+ }
+
+ private class RadioItem
+ {
+ public string Text { get; set; }
+
+ public string Value { get; set; }
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/Test/ReaderWriterCacheTest.cs b/test/Microsoft.Web.Mvc.Test/Test/ReaderWriterCacheTest.cs
new file mode 100644
index 00000000..360f4874
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Test/ReaderWriterCacheTest.cs
@@ -0,0 +1,77 @@
+using System;
+using System.Collections.Generic;
+using Xunit;
+
+namespace Microsoft.Web.Mvc.Test
+{
+ public class ReaderWriterCacheTest
+ {
+ [Fact]
+ public void PublicFetchOrCreateItemCreatesItemIfNotAlreadyInCache()
+ {
+ // Arrange
+ ReaderWriterCacheHelper<int, string> helper = new ReaderWriterCacheHelper<int, string>();
+ Dictionary<int, string> cache = helper.PublicCache;
+
+ // Act
+ string item = helper.PublicFetchOrCreateItem(42, () => "new");
+
+ // Assert
+ Assert.Equal("new", cache[42]);
+ Assert.Equal("new", item);
+ }
+
+ [Fact]
+ public void PublicFetchOrCreateItemReturnsExistingItemIfFound()
+ {
+ // Arrange
+ ReaderWriterCacheHelper<int, string> helper = new ReaderWriterCacheHelper<int, string>();
+ Dictionary<int, string> cache = helper.PublicCache;
+ helper.PublicCache[42] = "original";
+
+ // Act
+ string item = helper.PublicFetchOrCreateItem(42, () => "new");
+
+ // Assert
+ Assert.Equal("original", cache[42]);
+ Assert.Equal("original", item);
+ }
+
+ [Fact]
+ public void PublicFetchOrCreateItemReturnsFirstItemIfTwoThreadsUpdateCacheSimultaneously()
+ {
+ // Arrange
+ ReaderWriterCacheHelper<int, string> helper = new ReaderWriterCacheHelper<int, string>();
+ Dictionary<int, string> cache = helper.PublicCache;
+ Func<string> creator = delegate
+ {
+ // fake a second thread coming along when we weren't looking
+ string firstItem = helper.PublicFetchOrCreateItem(42, () => "original");
+
+ Assert.Equal("original", cache[42]);
+ Assert.Equal("original", firstItem);
+ return "new";
+ };
+
+ // Act
+ string secondItem = helper.PublicFetchOrCreateItem(42, creator);
+
+ // Assert
+ Assert.Equal("original", cache[42]);
+ Assert.Equal("original", secondItem);
+ }
+
+ private class ReaderWriterCacheHelper<TKey, TValue> : ReaderWriterCache<TKey, TValue>
+ {
+ public Dictionary<TKey, TValue> PublicCache
+ {
+ get { return Cache; }
+ }
+
+ public TValue PublicFetchOrCreateItem(TKey key, Func<TValue> creator)
+ {
+ return FetchOrCreateItem(key, creator);
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/Test/RenderActionTest.cs b/test/Microsoft.Web.Mvc.Test/Test/RenderActionTest.cs
new file mode 100644
index 00000000..f6fdb85f
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Test/RenderActionTest.cs
@@ -0,0 +1,115 @@
+using System;
+using System.IO;
+using System.Reflection;
+using System.Web;
+using System.Web.Mvc;
+using System.Web.Routing;
+using Microsoft.Web.UnitTestUtil;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace Microsoft.Web.Mvc.Test
+{
+ public class RenderActionTest
+ {
+ [Fact]
+ public void RenderActionUsingExpressionWithParametersInViewContextRendersCorrectly()
+ {
+ // Arrange
+ Func<RequestContext> requestContextAccessor;
+ HtmlHelper html = GetHtmlHelper(out requestContextAccessor);
+ html.ViewContext.RouteData.Values.Add("stuff", "42");
+
+ // Act
+ html.RenderAction<TestController>(c => c.Stuff());
+ RequestContext requestContext = requestContextAccessor();
+
+ // Assert
+ Assert.NotNull(requestContext);
+ Assert.Equal("Test", requestContext.RouteData.Values["controller"]);
+ Assert.Equal("Stuff", requestContext.RouteData.Values["action"]);
+ Assert.Equal("42", requestContext.RouteData.Values["stuff"]);
+ }
+
+ [Fact]
+ public void RenderActionUsingExpressionRendersCorrectly()
+ {
+ // Arrange
+ Func<RequestContext> requestContextAccessor;
+ HtmlHelper html = GetHtmlHelper(out requestContextAccessor);
+
+ // Act
+ html.RenderAction<TestController>(c => c.About(76));
+ RequestContext requestContext = requestContextAccessor();
+
+ // Assert
+ Assert.NotNull(requestContext);
+ Assert.Equal("Test", requestContext.RouteData.Values["controller"]);
+ Assert.Equal("About", requestContext.RouteData.Values["action"]);
+ Assert.Equal(76, requestContext.RouteData.Values["page"]);
+ }
+
+ [Fact]
+ public void RenderRouteWithNullRouteValueDictionaryThrowsException()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary(), "/");
+ Assert.ThrowsArgumentNull(() => html.RenderRoute(null), "routeValues");
+ }
+
+ [Fact]
+ public void RenderRouteWithActionAndControllerSpecifiedRendersCorrectAction()
+ {
+ // Arrange
+ Func<RequestContext> requestContextAccessor;
+ HtmlHelper html = GetHtmlHelper(out requestContextAccessor);
+
+ // Act
+ html.RenderRoute(new RouteValueDictionary(new { action = "Index", controller = "Test" }));
+ RequestContext requestContext = requestContextAccessor();
+
+ // Assert
+ Assert.NotNull(requestContext);
+ Assert.Equal("Test", requestContext.RouteData.Values["controller"]);
+ Assert.Equal("Index", requestContext.RouteData.Values["action"]);
+ }
+
+ private static HtmlHelper GetHtmlHelper(out Func<RequestContext> requestContextAccessor)
+ {
+ RequestContext requestContext = null;
+
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary(), "/");
+
+ html.RouteCollection.MapRoute(null, "{*dummy}");
+ Mock.Get(html.ViewContext.HttpContext)
+ .Setup(o => o.Server.Execute(It.IsAny<IHttpHandler>(), It.IsAny<TextWriter>(), It.IsAny<bool>()))
+ .Callback<IHttpHandler, TextWriter, bool>((_h, _w, _pf) =>
+ {
+ MvcHandler mvcHandler = _h.GetType().GetProperty("InnerHandler", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(_h, null) as MvcHandler;
+ requestContext = mvcHandler.RequestContext;
+ });
+
+ requestContextAccessor = () => requestContext;
+ return html;
+ }
+
+ public class TestController : Controller
+ {
+ public string Index()
+ {
+ return "It Worked!";
+ }
+
+ public string About(int page)
+ {
+ return "This is page #" + page;
+ }
+
+ public string Stuff()
+ {
+ string stuff = ControllerContext.RouteData.Values["stuff"] as string;
+ return "Argument was " + stuff;
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/Test/ScriptExtensionsTest.cs b/test/Microsoft.Web.Mvc.Test/Test/ScriptExtensionsTest.cs
new file mode 100644
index 00000000..8667970a
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Test/ScriptExtensionsTest.cs
@@ -0,0 +1,123 @@
+using System.Web.Mvc;
+using Microsoft.Web.UnitTestUtil;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace Microsoft.Web.Mvc.Test
+{
+ public class ScriptExtensionsTest
+ {
+ [Fact]
+ public void ScriptWithoutReleaseFileThrowsArgumentNullException()
+ {
+ // Arrange
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+
+ // Assert
+ Assert.ThrowsArgumentNullOrEmpty(() => html.Script(null, "file"), "releaseFile");
+ }
+
+ [Fact]
+ public void ScriptWithoutDebugFileThrowsArgumentNullException()
+ {
+ // Arrange
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+
+ // Assert
+ Assert.ThrowsArgumentNullOrEmpty(() => html.Script("File", null), "debugFile");
+ }
+
+ [Fact]
+ public void ScriptWithRootedPathRendersProperElement()
+ {
+ // Arrange
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+
+ // Act
+ MvcHtmlString result = html.Script("~/Correct/Path.js", "~/Correct/Debug/Path.js");
+
+ // Assert
+ Assert.Equal("<script src=\"/$(SESSION)/Correct/Path.js\" type=\"text/javascript\"></script>", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void ScriptWithRelativePathRendersProperElement()
+ {
+ // Arrange
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+
+ // Act
+ MvcHtmlString result = html.Script("../../Correct/Path.js", "../../Correct/Debug/Path.js");
+
+ // Assert
+ Assert.Equal("<script src=\"../../Correct/Path.js\" type=\"text/javascript\"></script>", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void ScriptWithRelativeCurrentPathRendersProperElement()
+ {
+ // Arrange
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+
+ // Act
+ MvcHtmlString result = html.Script("/Correct/Path.js", "/Correct/Debug/Path.js");
+
+ // Assert
+ Assert.Equal("<script src=\"/Correct/Path.js\" type=\"text/javascript\"></script>", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void ScriptWithScriptRelativePathRendersProperElement()
+ {
+ // Arrange
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+
+ // Act
+ MvcHtmlString result = html.Script("Correct/Path.js", "Correct/Debug/Path.js");
+
+ // Assert
+ Assert.Equal("<script src=\"/$(SESSION)/Scripts/Correct/Path.js\" type=\"text/javascript\"></script>", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void ScriptWithUrlRendersProperElement()
+ {
+ // Arrange
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+
+ // Act
+ MvcHtmlString result = html.Script("http://ajax.Correct.com/Path.js", "http://ajax.Debug.com/Path.js");
+
+ // Assert
+ Assert.Equal("<script src=\"http://ajax.Correct.com/Path.js\" type=\"text/javascript\"></script>", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void ScriptWithSecureUrlRendersProperElement()
+ {
+ // Arrange
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+
+ // Act
+ MvcHtmlString result = html.Script("https://ajax.Correct.com/Path.js", "https://ajax.Debug.com/Path.js");
+
+ // Assert
+ Assert.Equal("<script src=\"https://ajax.Correct.com/Path.js\" type=\"text/javascript\"></script>", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void ScriptWithDebuggingOnUsesDebugUrl()
+ {
+ // Arrange
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary());
+ Mock.Get(html.ViewContext.HttpContext).Setup(v => v.IsDebuggingEnabled).Returns(true);
+
+ // Act
+ MvcHtmlString result = html.Script("Correct/Path.js", "Correct/Debug/Path.js");
+
+ // Assert
+ Assert.Equal("<script src=\"/$(SESSION)/Scripts/Correct/Debug/Path.js\" type=\"text/javascript\"></script>", result.ToHtmlString());
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/Test/SerializationExtensionsTest.cs b/test/Microsoft.Web.Mvc.Test/Test/SerializationExtensionsTest.cs
new file mode 100644
index 00000000..aefc6712
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Test/SerializationExtensionsTest.cs
@@ -0,0 +1,78 @@
+using System.Web.Mvc;
+using Microsoft.Web.UnitTestUtil;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace Microsoft.Web.Mvc.Test
+{
+ public class SerializationExtensionsTest
+ {
+ [Fact]
+ public void SerializeFromProvidedValueOverridesViewData()
+ {
+ // Arrange
+ ViewDataDictionary vdd = new ViewDataDictionary
+ {
+ { "someKey", 42 }
+ };
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(vdd);
+
+ Mock<MvcSerializer> mockSerializer = new Mock<MvcSerializer>();
+ mockSerializer.Setup(o => o.Serialize("Hello!", SerializationMode.Signed)).Returns("some-value");
+
+ // Act
+ MvcHtmlString htmlString = helper.Serialize("someKey", "Hello!", SerializationMode.Signed, mockSerializer.Object);
+
+ // Assert
+ Assert.Equal(@"<input name=""someKey"" type=""hidden"" value=""some-value"" />", htmlString.ToHtmlString());
+ }
+
+ [Fact]
+ public void SerializeFromViewData()
+ {
+ // Arrange
+ ViewDataDictionary vdd = new ViewDataDictionary
+ {
+ { "someKey", 42 }
+ };
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(vdd);
+
+ Mock<MvcSerializer> mockSerializer = new Mock<MvcSerializer>();
+ mockSerializer.Setup(o => o.Serialize(42, SerializationMode.EncryptedAndSigned)).Returns("some-other-value");
+
+ // Act
+ MvcHtmlString htmlString = helper.Serialize("someKey", SerializationMode.EncryptedAndSigned, mockSerializer.Object);
+
+ // Assert
+ Assert.Equal(@"<input name=""someKey"" type=""hidden"" value=""some-other-value"" />", htmlString.ToHtmlString());
+ }
+
+ [Fact]
+ public void SerializeThrowsIfHtmlHelperIsNull()
+ {
+ Assert.ThrowsArgumentNull(
+ delegate { SerializationExtensions.Serialize(null, "someName"); }, "htmlHelper");
+ }
+
+ [Fact]
+ public void SerializeThrowsIfNameIsEmpty()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { helper.Serialize(""); }, "name");
+ }
+
+ [Fact]
+ public void SerializeThrowsIfNameIsNull()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { helper.Serialize(null); }, "name");
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/Test/ServerVariablesValueProviderFactoryTest.cs b/test/Microsoft.Web.Mvc.Test/Test/ServerVariablesValueProviderFactoryTest.cs
new file mode 100644
index 00000000..d57a63cc
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Test/ServerVariablesValueProviderFactoryTest.cs
@@ -0,0 +1,35 @@
+using System.Collections.Specialized;
+using System.Globalization;
+using System.Web.Mvc;
+using Moq;
+using Xunit;
+
+namespace Microsoft.Web.Mvc.Test
+{
+ public class ServerVariablesValueProviderFactoryTest
+ {
+ [Fact]
+ public void GetValueProvider()
+ {
+ // Arrange
+ NameValueCollection serverVars = new NameValueCollection
+ {
+ { "foo", "fooValue" },
+ { "bar.baz", "barBazValue" }
+ };
+
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.Setup(o => o.HttpContext.Request.ServerVariables).Returns(serverVars);
+
+ ServerVariablesValueProviderFactory factory = new ServerVariablesValueProviderFactory();
+
+ // Act
+ IValueProvider provider = factory.GetValueProvider(mockControllerContext.Object);
+
+ // Assert
+ Assert.True(provider.ContainsPrefix("bar"));
+ Assert.Equal("fooValue", provider.GetValue("foo").AttemptedValue);
+ Assert.Equal(CultureInfo.InvariantCulture, provider.GetValue("foo").Culture);
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/Test/SessionValueProviderFactoryTest.cs b/test/Microsoft.Web.Mvc.Test/Test/SessionValueProviderFactoryTest.cs
new file mode 100644
index 00000000..e75b577f
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Test/SessionValueProviderFactoryTest.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Web;
+using System.Web.Mvc;
+using Moq;
+using Xunit;
+
+namespace Microsoft.Web.Mvc.Test
+{
+ public class SessionValueProviderFactoryTest
+ {
+ [Fact]
+ public void GetValueProvider()
+ {
+ // Arrange
+ Dictionary<string, object> backingStore = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
+ {
+ { "foo", "fooValue" },
+ { "bar.baz", "barBazValue" }
+ };
+ MockSessionState session = new MockSessionState(backingStore);
+
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.Setup(o => o.HttpContext.Session).Returns(session);
+
+ SessionValueProviderFactory factory = new SessionValueProviderFactory();
+
+ // Act
+ IValueProvider provider = factory.GetValueProvider(mockControllerContext.Object);
+
+ // Assert
+ Assert.True(provider.ContainsPrefix("bar"));
+ Assert.Equal("fooValue", provider.GetValue("foo").AttemptedValue);
+ Assert.Equal(CultureInfo.InvariantCulture, provider.GetValue("foo").Culture);
+ }
+
+ private sealed class MockSessionState : HttpSessionStateBase
+ {
+ private readonly IDictionary<string, object> _backingStore;
+
+ public MockSessionState(IDictionary<string, object> backingStore)
+ {
+ _backingStore = backingStore;
+ }
+
+ public override object this[string name]
+ {
+ get { return _backingStore[name]; }
+ set { _backingStore[name] = value; }
+ }
+
+ public override IEnumerator GetEnumerator()
+ {
+ return _backingStore.Keys.GetEnumerator();
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/Test/SkipBindingAttributeTest.cs b/test/Microsoft.Web.Mvc.Test/Test/SkipBindingAttributeTest.cs
new file mode 100644
index 00000000..4d3836a9
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Test/SkipBindingAttributeTest.cs
@@ -0,0 +1,22 @@
+using System.Web.Mvc;
+using Xunit;
+
+namespace Microsoft.Web.Mvc.Test
+{
+ public class SkipBindingAttributeTest
+ {
+ [Fact]
+ public void GetBinderReturnsModelBinderWhichReturnsNull()
+ {
+ // Arrange
+ CustomModelBinderAttribute attr = new SkipBindingAttribute();
+ IModelBinder binder = attr.GetBinder();
+
+ // Act
+ object result = binder.BindModel(null, null);
+
+ // Assert
+ Assert.Null(result);
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/Test/SubmitButtonExtensionsTest.cs b/test/Microsoft.Web.Mvc.Test/Test/SubmitButtonExtensionsTest.cs
new file mode 100644
index 00000000..20e9e375
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Test/SubmitButtonExtensionsTest.cs
@@ -0,0 +1,82 @@
+using System.Web.Mvc;
+using System.Web.Routing;
+using Microsoft.Web.UnitTestUtil;
+using Xunit;
+
+namespace Microsoft.Web.Mvc.Test
+{
+ public class SubmitButtonExtensionsTest
+ {
+ [Fact]
+ public void SubmitButtonRendersWithJustTypeAttribute()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+ MvcHtmlString button = html.SubmitButton();
+ Assert.Equal("<input type=\"submit\" />", button.ToHtmlString());
+ }
+
+ [Fact]
+ public void SubmitButtonWithAttributesWithUnderscores()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+ MvcHtmlString button = html.SubmitButton(null, "blah", new { foo_bar = "baz" });
+ Assert.Equal("<input foo-bar=\"baz\" type=\"submit\" value=\"blah\" />", button.ToHtmlString());
+ }
+
+ [Fact]
+ public void SubmitButtonWithNameRendersButtonWithNameAttribute()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+ MvcHtmlString button = html.SubmitButton("button-name");
+ Assert.Equal("<input id=\"button-name\" name=\"button-name\" type=\"submit\" />", button.ToHtmlString());
+ }
+
+ [Fact]
+ public void SubmitButtonWithIdDifferentFromNameRendersButtonWithId()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+ MvcHtmlString button = html.SubmitButton("button-name", "blah", new { id = "foo" });
+ Assert.Equal("<input id=\"foo\" name=\"button-name\" type=\"submit\" value=\"blah\" />", button.ToHtmlString());
+ }
+
+ [Fact]
+ public void SubmitButtonWithNameAndTextRendersAttributes()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+ MvcHtmlString button = html.SubmitButton("button-name", "button-text");
+ Assert.Equal("<input id=\"button-name\" name=\"button-name\" type=\"submit\" value=\"button-text\" />", button.ToHtmlString());
+ }
+
+ [Fact]
+ public void SubmitButtonWithNameAndValueRendersBothAttributes()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+ MvcHtmlString button = html.SubmitButton("button-name", "button-value", new { id = "button-id" });
+ Assert.Equal("<input id=\"button-id\" name=\"button-name\" type=\"submit\" value=\"button-value\" />", button.ToHtmlString());
+ }
+
+ [Fact]
+ public void SubmitButtonWithNameAndIdRendersBothAttributesCorrectly()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+ MvcHtmlString button = html.SubmitButton("button-name", "button-value", new { id = "button-id" });
+ Assert.Equal("<input id=\"button-id\" name=\"button-name\" type=\"submit\" value=\"button-value\" />", button.ToHtmlString());
+ }
+
+ [Fact]
+ public void SubmitButtonWithTypeAttributeRendersCorrectType()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+ MvcHtmlString button = html.SubmitButton("specified-name", "button-value", new { type = "not-submit" });
+ Assert.Equal("<input id=\"specified-name\" name=\"specified-name\" type=\"not-submit\" value=\"button-value\" />", button.ToHtmlString());
+ }
+
+ [Fact]
+ public void SubmitButtonWithNameAndValueSpecifiedAndPassedInAsAttributeChoosesSpecified()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+ MvcHtmlString button = html.SubmitButton("specified-name", "button-value", new RouteValueDictionary(new { name = "name-attribute-value", value = "value-attribute" }));
+ Assert.Equal("<input id=\"specified-name\" name=\"name-attribute-value\" type=\"submit\" value=\"value-attribute\" />", button.ToHtmlString());
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/Test/SubmitImageExtensionsTest.cs b/test/Microsoft.Web.Mvc.Test/Test/SubmitImageExtensionsTest.cs
new file mode 100644
index 00000000..489c0a4d
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Test/SubmitImageExtensionsTest.cs
@@ -0,0 +1,66 @@
+using System.Web.Mvc;
+using System.Web.Routing;
+using Microsoft.Web.UnitTestUtil;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace Microsoft.Web.Mvc.Test
+{
+ public class SubmitImageExtensionsTest
+ {
+ [Fact]
+ public void SubmitImageWithEmptyImageSrcThrowsArgumentNullException()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+ Assert.ThrowsArgumentNull(() => html.SubmitImage("name", null), "imageSrc");
+ }
+
+ [Fact]
+ public void SubmitImageWithAttributesWithUnderscores()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+ MvcHtmlString button = html.SubmitImage("specified-name", "/mvc.jpg", new { foo_bar = "baz" });
+ Assert.Equal("<input foo-bar=\"baz\" id=\"specified-name\" name=\"specified-name\" src=\"/mvc.jpg\" type=\"image\" />", button.ToHtmlString());
+ }
+
+ [Fact]
+ public void SubmitImageWithTypeAttributeRendersExplicitTypeAttribute()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+ MvcHtmlString button = html.SubmitImage("specified-name", "/mvc.jpg", new { type = "not-image" });
+ Assert.Equal("<input id=\"specified-name\" name=\"specified-name\" src=\"/mvc.jpg\" type=\"not-image\" />", button.ToHtmlString());
+ }
+
+ [Fact]
+ public void SubmitImageWithNameAndImageUrlRendersNameAndSrcAttributes()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+ MvcHtmlString button = html.SubmitImage("button-name", "/mvc.gif");
+ Assert.Equal("<input id=\"button-name\" name=\"button-name\" src=\"/mvc.gif\" type=\"image\" />", button.ToHtmlString());
+ }
+
+ [Fact]
+ public void SubmitImageWithImageUrlStartingWithTildeRendersAppPath()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelperWithPath(new ViewDataDictionary(), "/app");
+ MvcHtmlString button = html.SubmitImage("button-name", "~/mvc.gif");
+ Assert.Equal("<input id=\"button-name\" name=\"button-name\" src=\"/$(SESSION)/app/mvc.gif\" type=\"image\" />", button.ToHtmlString());
+ }
+
+ [Fact]
+ public void SubmitImageWithNameAndIdRendersBothAttributesCorrectly()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+ MvcHtmlString button = html.SubmitImage("button-name", "/mvc.png", new { id = "button-id" });
+ Assert.Equal("<input id=\"button-id\" name=\"button-name\" src=\"/mvc.png\" type=\"image\" />", button.ToHtmlString());
+ }
+
+ [Fact]
+ public void SubmitButtonWithNameAndValueSpecifiedAndPassedInAsAttributeChoosesExplicitAttributes()
+ {
+ HtmlHelper html = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+ MvcHtmlString button = html.SubmitImage("specified-name", "/specified-src.bmp", new RouteValueDictionary(new { name = "name-attribute", src = "src-attribute" }));
+ Assert.Equal("<input id=\"specified-name\" name=\"name-attribute\" src=\"src-attribute\" type=\"image\" />", button.ToHtmlString());
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/Test/TempDataValueProviderFactoryTest.cs b/test/Microsoft.Web.Mvc.Test/Test/TempDataValueProviderFactoryTest.cs
new file mode 100644
index 00000000..51e99248
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Test/TempDataValueProviderFactoryTest.cs
@@ -0,0 +1,86 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Web.Mvc;
+using Moq;
+using Xunit;
+
+namespace Microsoft.Web.Mvc.Test
+{
+ public class TempDataValueProviderFactoryTest
+ {
+ [Fact]
+ public void GetValueProvider_CorrectlyRetainsOrRemovesKeys()
+ {
+ // Arrange
+ string[] expectedRetainedKeys = new[]
+ {
+ "retainMe"
+ };
+
+ TempDataDictionary tempData = new TempDataDictionary
+ {
+ { "retainMe", "retainMeValue" },
+ { "removeMe", "removeMeValue" },
+ { "previouslyRemoved", "previouslyRemovedValue" }
+ };
+ object dummy = tempData["previouslyRemoved"]; // mark value for removal
+
+ ControllerContext controllerContext = GetControllerContext(tempData);
+
+ TempDataValueProviderFactory factory = new TempDataValueProviderFactory();
+
+ // Act
+ IValueProvider valueProvider = factory.GetValueProvider(controllerContext);
+ ValueProviderResult nonExistentResult = valueProvider.GetValue("nonExistent");
+ ValueProviderResult removeMeResult = valueProvider.GetValue("removeme");
+
+ // Assert
+ Assert.Null(nonExistentResult);
+ Assert.Equal("removeMeValue", removeMeResult.AttemptedValue);
+ Assert.Equal(CultureInfo.InvariantCulture, removeMeResult.Culture);
+
+ // Verify that keys have been removed or retained correctly by the provider
+ Mock<ITempDataProvider> mockTempDataProvider = new Mock<ITempDataProvider>();
+ string[] retainedKeys = null;
+ mockTempDataProvider
+ .Setup(o => o.SaveTempData(controllerContext, It.IsAny<IDictionary<string, object>>()))
+ .Callback(
+ delegate(ControllerContext cc, IDictionary<string, object> d) { retainedKeys = d.Keys.ToArray(); });
+
+ tempData.Save(controllerContext, mockTempDataProvider.Object);
+ Assert.Equal(expectedRetainedKeys, retainedKeys);
+ }
+
+ [Fact]
+ public void GetValueProvider_EmptyTempData_ReturnsNull()
+ {
+ // Arrange
+ TempDataDictionary tempData = new TempDataDictionary();
+ ControllerContext controllerContext = GetControllerContext(tempData);
+
+ TempDataValueProviderFactory factory = new TempDataValueProviderFactory();
+
+ // Act
+ IValueProvider provider = factory.GetValueProvider(controllerContext);
+
+ // Assert
+ Assert.Null(provider);
+ }
+
+ private static ControllerContext GetControllerContext(TempDataDictionary tempData)
+ {
+ return new ControllerContext
+ {
+ Controller = new EmptyController
+ {
+ TempData = tempData
+ }
+ };
+ }
+
+ private sealed class EmptyController : Controller
+ {
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/Test/TypeHelpersTest.cs b/test/Microsoft.Web.Mvc.Test/Test/TypeHelpersTest.cs
new file mode 100644
index 00000000..e934b165
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Test/TypeHelpersTest.cs
@@ -0,0 +1,121 @@
+using System;
+using System.Collections.Generic;
+using Xunit;
+
+namespace Microsoft.Web.Mvc.Test
+{
+ public class TypeHelpersTest
+ {
+ [Fact]
+ public void GetTypeArgumentsIfMatch_ClosedTypeIsGenericAndMatches_ReturnsType()
+ {
+ // Act
+ Type[] typeArguments = TypeHelpers.GetTypeArgumentsIfMatch(typeof(List<int>), typeof(List<>));
+
+ // Assert
+ Assert.Equal(new[] { typeof(int) }, typeArguments);
+ }
+
+ [Fact]
+ public void GetTypeArgumentsIfMatch_ClosedTypeIsGenericButDoesNotMatch_ReturnsNull()
+ {
+ // Act
+ Type[] typeArguments = TypeHelpers.GetTypeArgumentsIfMatch(typeof(int?), typeof(List<>));
+
+ // Assert
+ Assert.Null(typeArguments);
+ }
+
+ [Fact]
+ public void GetTypeArgumentsIfMatch_ClosedTypeIsNotGeneric_ReturnsNull()
+ {
+ // Act
+ Type[] typeArguments = TypeHelpers.GetTypeArgumentsIfMatch(typeof(int), null);
+
+ // Assert
+ Assert.Null(typeArguments);
+ }
+
+ [Fact]
+ public void IsCompatibleObjectReturnsTrueIfTypeIsNotNullableAndValueIsNull()
+ {
+ // Act
+ bool retVal = TypeHelpers.IsCompatibleObject(typeof(int), null);
+
+ // Assert
+ Assert.False(retVal);
+ }
+
+ [Fact]
+ public void IsCompatibleObjectReturnsFalseIfValueIsIncorrectType()
+ {
+ // Arrange
+ object value = new[] { "Hello", "world" };
+
+ // Act
+ bool retVal = TypeHelpers.IsCompatibleObject(typeof(int), value);
+
+ // Assert
+ Assert.False(retVal);
+ }
+
+ [Fact]
+ public void IsCompatibleObjectReturnsTrueIfTypeIsNullableAndValueIsNull()
+ {
+ // Act
+ bool retVal = TypeHelpers.IsCompatibleObject(typeof(int?), null);
+
+ // Assert
+ Assert.True(retVal);
+ }
+
+ [Fact]
+ public void IsCompatibleObjectReturnsTrueIfValueIsOfCorrectType()
+ {
+ // Arrange
+ object value = new[] { "Hello", "world" };
+
+ // Act
+ bool retVal = TypeHelpers.IsCompatibleObject(typeof(IEnumerable<string>), value);
+
+ // Assert
+ Assert.True(retVal);
+ }
+
+ [Fact]
+ public void TypeAllowsNullValueReturnsFalseForNonNullableGenericValueType()
+ {
+ Assert.False(TypeHelpers.TypeAllowsNullValue(typeof(KeyValuePair<int, string>)));
+ }
+
+ [Fact]
+ public void TypeAllowsNullValueReturnsFalseForNonNullableGenericValueTypeDefinition()
+ {
+ Assert.False(TypeHelpers.TypeAllowsNullValue(typeof(KeyValuePair<,>)));
+ }
+
+ [Fact]
+ public void TypeAllowsNullValueReturnsFalseForNonNullableValueType()
+ {
+ Assert.False(TypeHelpers.TypeAllowsNullValue(typeof(int)));
+ }
+
+ [Fact]
+ public void TypeAllowsNullValueReturnsTrueForInterfaceType()
+ {
+ Assert.True(TypeHelpers.TypeAllowsNullValue(typeof(IDisposable)));
+ }
+
+ [Fact]
+ public void TypeAllowsNullValueReturnsTrueForNullableType()
+ {
+ Assert.True(TypeHelpers.TypeAllowsNullValue(typeof(int?)));
+ }
+
+ [Fact]
+ public void TypeAllowsNullValueReturnsTrueForReferenceType()
+ {
+ Assert.True(TypeHelpers.TypeAllowsNullValue(typeof(object)));
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/Test/UrlAttributeTest.cs b/test/Microsoft.Web.Mvc.Test/Test/UrlAttributeTest.cs
new file mode 100644
index 00000000..462f4bb2
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Test/UrlAttributeTest.cs
@@ -0,0 +1,44 @@
+using System.Linq;
+using System.Web.Mvc;
+using Moq;
+using Xunit;
+
+namespace Microsoft.Web.Mvc.Test
+{
+ public class UrlAttributeTest
+ {
+ [Fact]
+ public void ClientRule()
+ {
+ // Arrange
+ var attribute = new UrlAttribute();
+ var provider = new Mock<ModelMetadataProvider>();
+ var metadata = new ModelMetadata(provider.Object, null, null, typeof(string), "PropertyName");
+
+ // Act
+ ModelClientValidationRule clientRule = attribute.GetClientValidationRules(metadata, null).Single();
+
+ // Assert
+ Assert.Equal("url", clientRule.ValidationType);
+ Assert.Equal("The PropertyName field is not a valid fully-qualified http, https, or ftp URL.", clientRule.ErrorMessage);
+ Assert.Empty(clientRule.ValidationParameters);
+ }
+
+ [Fact]
+ public void IsValidTests()
+ {
+ // Arrange
+ var attribute = new UrlAttribute();
+
+ // Act & Assert
+ Assert.True(attribute.IsValid(null)); // Optional values are always valid
+ Assert.True(attribute.IsValid("http://foo.bar"));
+ Assert.True(attribute.IsValid("https://foo.bar"));
+ Assert.True(attribute.IsValid("ftp://foo.bar"));
+ Assert.False(attribute.IsValid("file:///foo.bar"));
+ Assert.False(attribute.IsValid("http://user%password@foo.bar/"));
+ Assert.False(attribute.IsValid("foo.png"));
+ Assert.False(attribute.IsValid("\0foo.png")); // Illegal character
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/Test/ValueProviderUtilTest.cs b/test/Microsoft.Web.Mvc.Test/Test/ValueProviderUtilTest.cs
new file mode 100644
index 00000000..ed920b30
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/Test/ValueProviderUtilTest.cs
@@ -0,0 +1,46 @@
+using Xunit;
+
+namespace Microsoft.Web.Mvc.Test
+{
+ public class ValueProviderUtilTest
+ {
+ [Fact]
+ public void IsPrefixMatch_Misses()
+ {
+ // Arrange
+ var tests = new[]
+ {
+ new { Prefix = "Prefix", TestString = (string)null, Reason = "Null test string shouldn't match anything." },
+ new { Prefix = "Foo", TestString = "NotFoo", Reason = "Prefix 'foo' doesn't match 'notfoo'." },
+ new { Prefix = "Foo", TestString = "FooBar", Reason = "Prefix 'foo' was not followed by a delimiter in the test string." }
+ };
+
+ // Act & assert
+ foreach (var test in tests)
+ {
+ bool retVal = ValueProviderUtil.IsPrefixMatch(test.Prefix, test.TestString);
+ Assert.False(retVal, test.Reason);
+ }
+ }
+
+ [Fact]
+ public void IsPrefixMatch_Hits()
+ {
+ // Arrange
+ var tests = new[]
+ {
+ new { Prefix = "", TestString = "SomeTestString", Reason = "Empty prefix should match any non-null test string." },
+ new { Prefix = "SomeString", TestString = "SomeString", Reason = "This was an exact match." },
+ new { Prefix = "Foo", TestString = "foo.bar", Reason = "Prefix 'foo' matched." },
+ new { Prefix = "Foo", TestString = "foo[bar]", Reason = "Prefix 'foo' matched." },
+ };
+
+ // Act & assert
+ foreach (var test in tests)
+ {
+ bool retVal = ValueProviderUtil.IsPrefixMatch(test.Prefix, test.TestString);
+ Assert.True(retVal, test.Reason);
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.Web.Mvc.Test/packages.config b/test/Microsoft.Web.Mvc.Test/packages.config
new file mode 100644
index 00000000..d5aa6401
--- /dev/null
+++ b/test/Microsoft.Web.Mvc.Test/packages.config
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="Moq" version="4.0.10827" />
+ <package id="xunit" version="1.9.0.1566" />
+ <package id="xunit.extensions" version="1.9.0.1566" />
+</packages> \ No newline at end of file
diff --git a/test/Microsoft.Web.WebPages.OAuth.Test/Microsoft.Web.WebPages.OAuth.Test.csproj b/test/Microsoft.Web.WebPages.OAuth.Test/Microsoft.Web.WebPages.OAuth.Test.csproj
new file mode 100644
index 00000000..74b256dc
--- /dev/null
+++ b/test/Microsoft.Web.WebPages.OAuth.Test/Microsoft.Web.WebPages.OAuth.Test.csproj
@@ -0,0 +1,104 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProductVersion>8.0.30703</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{694C6EDF-EA52-438F-B745-82B025ECC0E7}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>Microsoft.Web.WebPages.OAuth.Test</RootNamespace>
+ <AssemblyName>Microsoft.Web.WebPages.OAuth.Test</AssemblyName>
+ <TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
+ <FileAlignment>512</FileAlignment>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Debug\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Release\Test\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'CodeCoverage' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\CodeCoverage\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="DotNetOpenAuth.AspNet, Version=4.0.0.12065, Culture=neutral, PublicKeyToken=2780ccd10d57b246, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>..\..\packages\DotNetOpenAuth.AspNet.4.0.0-beta2\lib\net40-full\DotNetOpenAuth.AspNet.dll</HintPath>
+ </Reference>
+ <Reference Include="DotNetOpenAuth.Core, Version=4.0.0.12065, Culture=neutral, PublicKeyToken=2780ccd10d57b246, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>..\..\packages\DotNetOpenAuth.Core.4.0.0-beta2\lib\net40-full\DotNetOpenAuth.Core.dll</HintPath>
+ </Reference>
+ <Reference Include="DotNetOpenAuth.OAuth, Version=4.0.0.12065, Culture=neutral, PublicKeyToken=2780ccd10d57b246, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>..\..\packages\DotNetOpenAuth.OAuth.Core.4.0.0-beta2\lib\net40-full\DotNetOpenAuth.OAuth.dll</HintPath>
+ </Reference>
+ <Reference Include="DotNetOpenAuth.OAuth.Consumer, Version=4.0.0.12065, Culture=neutral, PublicKeyToken=2780ccd10d57b246, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>..\..\packages\DotNetOpenAuth.OAuth.Consumer.4.0.0-beta2\lib\net40-full\DotNetOpenAuth.OAuth.Consumer.dll</HintPath>
+ </Reference>
+ <Reference Include="DotNetOpenAuth.OpenId, Version=4.0.0.12065, Culture=neutral, PublicKeyToken=2780ccd10d57b246, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>..\..\packages\DotNetOpenAuth.OpenId.Core.4.0.0-beta2\lib\net40-full\DotNetOpenAuth.OpenId.dll</HintPath>
+ </Reference>
+ <Reference Include="DotNetOpenAuth.OpenId.RelyingParty, Version=4.0.0.12065, Culture=neutral, PublicKeyToken=2780ccd10d57b246, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>..\..\packages\DotNetOpenAuth.OpenId.RelyingParty.4.0.0-beta2\lib\net40-full\DotNetOpenAuth.OpenId.RelyingParty.dll</HintPath>
+ </Reference>
+ <Reference Include="Moq">
+ <HintPath>..\..\packages\Moq.4.0.10827\lib\NET40\Moq.dll</HintPath>
+ </Reference>
+ <Reference Include="System" />
+ <Reference Include="System.Configuration" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.Web" />
+ <Reference Include="xunit">
+ <HintPath>..\..\packages\xunit.1.9.0.1566\lib\xunit.dll</HintPath>
+ </Reference>
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="OAuthWebSecurityTest.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="PreAppStartCodeTest.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="packages.config" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.Web.WebPages.OAuth\Microsoft.Web.WebPages.OAuth.csproj">
+ <Project>{4CBFC7D3-1600-4CE5-BC6B-AC7BC2D6F853}</Project>
+ <Name>Microsoft.Web.WebPages.OAuth</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\Microsoft.TestCommon\Microsoft.TestCommon.csproj">
+ <Project>{FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0}</Project>
+ <Name>Microsoft.TestCommon</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+ <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
+ Other similar extension points exist, see Microsoft.Common.targets.
+ <Target Name="BeforeBuild">
+ </Target>
+ <Target Name="AfterBuild">
+ </Target>
+ -->
+</Project> \ No newline at end of file
diff --git a/test/Microsoft.Web.WebPages.OAuth.Test/OAuthWebSecurityTest.cs b/test/Microsoft.Web.WebPages.OAuth.Test/OAuthWebSecurityTest.cs
new file mode 100644
index 00000000..45129d79
--- /dev/null
+++ b/test/Microsoft.Web.WebPages.OAuth.Test/OAuthWebSecurityTest.cs
@@ -0,0 +1,389 @@
+using System;
+using System.Collections.Specialized;
+using System.Web;
+using System.Web.Security;
+using DotNetOpenAuth.AspNet;
+using Moq;
+using Xunit;
+using Microsoft.TestCommon;
+
+namespace Microsoft.Web.WebPages.OAuth.Test
+{
+ public class OAuthWebSecurityTest : IDisposable
+ {
+ [Fact]
+ public void RegisterClientThrowsOnNullValue()
+ {
+ AssertEx.ThrowsArgumentNull(() => OAuthWebSecurity.RegisterClient(null), "client");
+ }
+
+ [Fact]
+ public void RegisterClientThrowsIfProviderNameIsEmpty()
+ {
+ // Arrange
+ var client = new Mock<IAuthenticationClient>();
+ client.Setup(c => c.ProviderName).Returns((string)null);
+
+ // Act & Assert
+ AssertEx.ThrowsArgument(() => OAuthWebSecurity.RegisterClient(client.Object), "client");
+
+ client.Setup(c => c.ProviderName).Returns("");
+
+ // Act & Assert
+ AssertEx.ThrowsArgument(() => OAuthWebSecurity.RegisterClient(client.Object), "client");
+ }
+
+ [Fact]
+ public void RegisterClientThrowsRegisterMoreThanOneClientWithTheSameName()
+ {
+ // Arrange
+ var client1 = new Mock<IAuthenticationClient>();
+ client1.Setup(c => c.ProviderName).Returns("provider");
+
+ var client2 = new Mock<IAuthenticationClient>();
+ client2.Setup(c => c.ProviderName).Returns("provider");
+
+ OAuthWebSecurity.RegisterClient(client1.Object);
+
+ // Act & Assert
+ AssertEx.ThrowsArgument(() => OAuthWebSecurity.RegisterClient(client2.Object), null);
+ }
+
+ [Fact]
+ public void RegisterOAuthClient()
+ {
+ // Arrange
+ var clients = new BuiltInOAuthClient[]
+ {
+ BuiltInOAuthClient.Facebook,
+ BuiltInOAuthClient.Twitter,
+ BuiltInOAuthClient.LinkedIn,
+ BuiltInOAuthClient.WindowsLive
+ };
+ var clientNames = new string[]
+ {
+ "Facebook",
+ "Twitter",
+ "LinkedIn",
+ "WindowsLive"
+ };
+
+ for (int i = 0; i < clients.Length; i++)
+ {
+ // Act
+ OAuthWebSecurity.RegisterOAuthClient(clients[i], "key", "secret");
+
+ var client = new Mock<IAuthenticationClient>();
+ client.Setup(c => c.ProviderName).Returns(clientNames[i]);
+
+ // Assert
+ Assert.Throws(typeof(ArgumentException), () => OAuthWebSecurity.RegisterClient(client.Object));
+ }
+ }
+
+ [Fact]
+ public void RegisterOpenIDClient()
+ {
+ // Arrange
+ var clients = new BuiltInOpenIDClient[]
+ {
+ BuiltInOpenIDClient.Google,
+ BuiltInOpenIDClient.Yahoo
+ };
+ var clientNames = new string[]
+ {
+ "Google",
+ "Yahoo"
+ };
+
+ for (int i = 0; i < clients.Length; i++)
+ {
+ // Act
+ OAuthWebSecurity.RegisterOpenIDClient(clients[i]);
+
+ var client = new Mock<IAuthenticationClient>();
+ client.Setup(c => c.ProviderName).Returns(clientNames[i]);
+
+ // Assert
+ AssertEx.ThrowsArgument(() => OAuthWebSecurity.RegisterClient(client.Object), null);
+ }
+ }
+
+ [Fact]
+ public void RequestAuthenticationRedirectsToProviderWithNullReturnUrl()
+ {
+ // Arrange
+ var context = new Mock<HttpContextBase>();
+ context.Setup(c => c.Request.ServerVariables).Returns(
+ new NameValueCollection());
+ context.Setup(c => c.Request.Url).Returns(new Uri("http://live.com/login.aspx"));
+ context.Setup(c => c.Request.RawUrl).Returns("/login.aspx");
+
+ var client = new Mock<IAuthenticationClient>();
+ client.Setup(c => c.ProviderName).Returns("windowslive");
+ client.Setup(c => c.RequestAuthentication(
+ context.Object,
+ It.Is<Uri>(u => u.AbsoluteUri.Equals("http://live.com/login.aspx?__provider__=windowslive", StringComparison.OrdinalIgnoreCase))))
+ .Verifiable();
+
+ OAuthWebSecurity.RegisterClient(client.Object);
+
+ // Act
+ OAuthWebSecurity.RequestAuthenticationCore(context.Object, "windowslive", null);
+
+ // Assert
+ client.Verify();
+ }
+
+ [Fact]
+ public void RequestAuthenticationRedirectsToProviderWithReturnUrl()
+ {
+ // Arrange
+ var context = new Mock<HttpContextBase>();
+ context.Setup(c => c.Request.ServerVariables).Returns(
+ new NameValueCollection());
+ context.Setup(c => c.Request.Url).Returns(new Uri("http://live.com/login.aspx"));
+ context.Setup(c => c.Request.RawUrl).Returns("/login.aspx");
+
+ var client = new Mock<IAuthenticationClient>();
+ client.Setup(c => c.ProviderName).Returns("yahoo");
+ client.Setup(c => c.RequestAuthentication(
+ context.Object,
+ It.Is<Uri>(u => u.AbsoluteUri.Equals("http://yahoo.com/?__provider__=yahoo", StringComparison.OrdinalIgnoreCase))))
+ .Verifiable();
+
+ OAuthWebSecurity.RegisterClient(client.Object);
+
+ // Act
+ OAuthWebSecurity.RequestAuthenticationCore(context.Object, "yahoo", "http://yahoo.com");
+
+ // Assert
+ client.Verify();
+ }
+
+ [Fact]
+ public void VerifyAuthenticationSucceed()
+ {
+ // Arrange
+ var queryStrings = new NameValueCollection();
+ queryStrings.Add("__provider__", "facebook");
+
+ var context = new Mock<HttpContextBase>();
+ context.Setup(c => c.Request.QueryString).Returns(queryStrings);
+
+ var client = new Mock<IAuthenticationClient>(MockBehavior.Strict);
+ client.Setup(c => c.ProviderName).Returns("facebook");
+ client.Setup(c => c.VerifyAuthentication(context.Object)).Returns(new AuthenticationResult(true, "facebook", "123",
+ "super", null));
+
+ var anotherClient = new Mock<IAuthenticationClient>(MockBehavior.Strict);
+ anotherClient.Setup(c => c.ProviderName).Returns("twitter");
+ anotherClient.Setup(c => c.VerifyAuthentication(context.Object)).Returns(AuthenticationResult.Failed);
+
+ OAuthWebSecurity.RegisterClient(client.Object);
+ OAuthWebSecurity.RegisterClient(anotherClient.Object);
+
+ // Act
+ AuthenticationResult result = OAuthWebSecurity.VerifyAuthenticationCore(context.Object);
+
+ // Assert
+ Assert.True(result.IsSuccessful);
+ Assert.Equal("facebook", result.Provider);
+ Assert.Equal("123", result.ProviderUserId);
+ Assert.Equal("super", result.UserName);
+ Assert.Null(result.Error);
+ Assert.Null(result.ExtraData);
+ }
+
+ [Fact]
+ public void VerifyAuthenticationFail()
+ {
+ // Arrange
+ var queryStrings = new NameValueCollection();
+ queryStrings.Add("__provider__", "twitter");
+
+ var context = new Mock<HttpContextBase>();
+ context.Setup(c => c.Request.QueryString).Returns(queryStrings);
+
+ var client = new Mock<IAuthenticationClient>(MockBehavior.Strict);
+ client.Setup(c => c.ProviderName).Returns("facebook");
+ client.Setup(c => c.VerifyAuthentication(context.Object)).Returns(new AuthenticationResult(true, "facebook", "123",
+ "super", null));
+
+ var anotherClient = new Mock<IAuthenticationClient>(MockBehavior.Strict);
+ anotherClient.Setup(c => c.ProviderName).Returns("twitter");
+ anotherClient.Setup(c => c.VerifyAuthentication(context.Object)).Returns(AuthenticationResult.Failed);
+
+ OAuthWebSecurity.RegisterClient(client.Object);
+ OAuthWebSecurity.RegisterClient(anotherClient.Object);
+
+ // Act
+ AuthenticationResult result = OAuthWebSecurity.VerifyAuthenticationCore(context.Object);
+
+ // Assert
+ Assert.False(result.IsSuccessful);
+ Assert.Equal("twitter", result.Provider);
+ }
+
+ [Fact]
+ public void VerifyAuthenticationFailIfNoProviderInQueryString()
+ {
+ // Arrange
+ var context = new Mock<HttpContextBase>();
+ context.Setup(c => c.Request.QueryString).Returns(new NameValueCollection());
+
+ var client = new Mock<IAuthenticationClient>(MockBehavior.Strict);
+ client.Setup(c => c.ProviderName).Returns("facebook");
+
+ var anotherClient = new Mock<IAuthenticationClient>(MockBehavior.Strict);
+ anotherClient.Setup(c => c.ProviderName).Returns("twitter");
+
+ OAuthWebSecurity.RegisterClient(client.Object);
+ OAuthWebSecurity.RegisterClient(anotherClient.Object);
+
+ // Act
+ AuthenticationResult result = OAuthWebSecurity.VerifyAuthenticationCore(context.Object);
+
+ // Assert
+ Assert.False(result.IsSuccessful);
+ Assert.Null(result.Provider);
+ }
+
+ [Fact]
+ public void LoginSetAuthenticationTicketIfSuccessful()
+ {
+ // Arrange
+ var cookies = new HttpCookieCollection();
+ var context = new Mock<HttpContextBase>();
+ context.Setup(c => c.Request.IsSecureConnection).Returns(true);
+ context.Setup(c => c.Response.Cookies).Returns(cookies);
+
+ var dataProvider = new Mock<IOpenAuthDataProvider>(MockBehavior.Strict);
+ dataProvider.Setup(p => p.GetUserNameFromOpenAuth("twitter", "12345")).Returns("hola");
+ OAuthWebSecurity.OAuthDataProvider = dataProvider.Object;
+
+ OAuthWebSecurity.RegisterOAuthClient(BuiltInOAuthClient.Twitter, "sdfdsfsd", "dfdsfdsf");
+
+ // Act
+ bool successful = OAuthWebSecurity.LoginCore(context.Object, "twitter", "12345", createPersistentCookie: false);
+
+ // Assert
+ Assert.True(successful);
+
+ Assert.Equal(1, cookies.Count);
+ HttpCookie addedCookie = cookies[0];
+
+ Assert.Equal(FormsAuthentication.FormsCookieName, addedCookie.Name);
+ Assert.True(addedCookie.HttpOnly);
+ Assert.Equal("/", addedCookie.Path);
+ Assert.False(addedCookie.Secure);
+ Assert.False(String.IsNullOrEmpty(addedCookie.Value));
+
+ FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(addedCookie.Value);
+ Assert.NotNull(ticket);
+ Assert.Equal(2, ticket.Version);
+ Assert.Equal("hola", ticket.Name);
+ Assert.Equal("OAuth", ticket.UserData);
+ Assert.False(ticket.IsPersistent);
+ }
+
+ [Fact]
+ public void LoginFailIfUserIsNotFound()
+ {
+ // Arrange
+ var context = new Mock<HttpContextBase>();
+ OAuthWebSecurity.RegisterOAuthClient(BuiltInOAuthClient.Twitter, "consumerKey", "consumerSecrte");
+
+ var dataProvider = new Mock<IOpenAuthDataProvider>();
+ dataProvider.Setup(p => p.GetUserNameFromOpenAuth("twitter", "12345")).Returns((string)null);
+ OAuthWebSecurity.OAuthDataProvider = dataProvider.Object;
+
+ // Act
+ bool successful = OAuthWebSecurity.LoginCore(context.Object, "twitter", "12345", createPersistentCookie: false);
+
+ // Assert
+ Assert.False(successful);
+ }
+
+ [Fact]
+ public void GetOAuthClientReturnsTheCorrectClient()
+ {
+ // Arrange
+ var client = new Mock<IAuthenticationClient>();
+ client.Setup(c => c.ProviderName).Returns("facebook");
+ OAuthWebSecurity.RegisterClient(client.Object);
+
+ var anotherClient = new Mock<IAuthenticationClient>();
+ anotherClient.Setup(c => c.ProviderName).Returns("hulu");
+ OAuthWebSecurity.RegisterClient(anotherClient.Object);
+
+ // Act
+ var expectedClient = OAuthWebSecurity.GetOAuthClient("facebook");
+
+ // Assert
+ Assert.Same(expectedClient, client.Object);
+ }
+
+ [Fact]
+ public void GetOAuthClientThrowsIfClientIsNotFound()
+ {
+ // Arrange
+ var client = new Mock<IAuthenticationClient>();
+ client.Setup(c => c.ProviderName).Returns("facebook");
+ OAuthWebSecurity.RegisterClient(client.Object);
+
+ var anotherClient = new Mock<IAuthenticationClient>();
+ anotherClient.Setup(c => c.ProviderName).Returns("hulu");
+ OAuthWebSecurity.RegisterClient(anotherClient.Object);
+
+ // Act & Assert
+ Assert.Throws<ArgumentException>(() => OAuthWebSecurity.GetOAuthClient("live"));
+ }
+
+ [Fact]
+ public void TryGetOAuthClientSucceeds()
+ {
+ // Arrange
+ var client = new Mock<IAuthenticationClient>();
+ client.Setup(c => c.ProviderName).Returns("facebook");
+ OAuthWebSecurity.RegisterClient(client.Object);
+
+ var anotherClient = new Mock<IAuthenticationClient>();
+ anotherClient.Setup(c => c.ProviderName).Returns("hulu");
+ OAuthWebSecurity.RegisterClient(anotherClient.Object);
+
+ // Act
+ IAuthenticationClient expectedClient;
+ bool result = OAuthWebSecurity.TryGetOAuthClient("facebook", out expectedClient);
+
+ // Assert
+ Assert.Same(expectedClient, client.Object);
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void TryGetOAuthClientFail()
+ {
+ // Arrange
+ var client = new Mock<IAuthenticationClient>();
+ client.Setup(c => c.ProviderName).Returns("facebook");
+ OAuthWebSecurity.RegisterClient(client.Object);
+
+ var anotherClient = new Mock<IAuthenticationClient>();
+ anotherClient.Setup(c => c.ProviderName).Returns("hulu");
+ OAuthWebSecurity.RegisterClient(anotherClient.Object);
+
+ // Act
+ IAuthenticationClient expectedClient;
+ bool result = OAuthWebSecurity.TryGetOAuthClient("live", out expectedClient);
+
+ // Assert
+ Assert.Null(expectedClient);
+ Assert.False(result);
+ }
+
+ public void Dispose() {
+ OAuthWebSecurity.ClearProviders();
+ }
+ }
+}
diff --git a/test/Microsoft.Web.WebPages.OAuth.Test/PreAppStartCodeTest.cs b/test/Microsoft.Web.WebPages.OAuth.Test/PreAppStartCodeTest.cs
new file mode 100644
index 00000000..bd386c39
--- /dev/null
+++ b/test/Microsoft.Web.WebPages.OAuth.Test/PreAppStartCodeTest.cs
@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Microsoft.Web.WebPages.OAuth.Test
+{
+ public class PreAppStartCodeTest
+ {
+
+ }
+}
diff --git a/test/Microsoft.Web.WebPages.OAuth.Test/Properties/AssemblyInfo.cs b/test/Microsoft.Web.WebPages.OAuth.Test/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..6f956b96
--- /dev/null
+++ b/test/Microsoft.Web.WebPages.OAuth.Test/Properties/AssemblyInfo.cs
@@ -0,0 +1,7 @@
+using System.Reflection;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("Microsoft.Web.DotNetOpenAuth.Test")]
+[assembly: AssemblyDescription("")] \ No newline at end of file
diff --git a/test/Microsoft.Web.WebPages.OAuth.Test/packages.config b/test/Microsoft.Web.WebPages.OAuth.Test/packages.config
new file mode 100644
index 00000000..27bf7a5b
--- /dev/null
+++ b/test/Microsoft.Web.WebPages.OAuth.Test/packages.config
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="DotNetOpenAuth.AspNet" version="4.0.0-beta2" />
+ <package id="DotNetOpenAuth.Core" version="4.0.0-beta2" />
+ <package id="DotNetOpenAuth.OAuth.Consumer" version="4.0.0-beta2" />
+ <package id="DotNetOpenAuth.OAuth.Core" version="4.0.0-beta2" />
+ <package id="DotNetOpenAuth.OpenId.Core" version="4.0.0-beta2" />
+ <package id="DotNetOpenAuth.OpenId.RelyingParty" version="4.0.0-beta2" />
+ <package id="Moq" version="4.0.10827" />
+ <package id="xunit" version="1.9.0.1566" />
+</packages> \ No newline at end of file
diff --git a/test/SPA.Test/Index.html b/test/SPA.Test/Index.html
new file mode 100644
index 00000000..996d602a
--- /dev/null
+++ b/test/SPA.Test/Index.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Unit tests for Upshot</title>
+ <meta http-equiv="X-UA-Compatible" content="IE=Edge" />
+
+ <link href="css/qunit.css" rel="stylesheet" type="text/css" />
+ <link href="css/tests.css" rel="stylesheet" type="text/css" />
+
+ <script src="_Scripts/qunit/qunit.js" type="text/javascript"></script>
+
+ <script src="_Scripts/jquery/jquery-1.6.2.js" type="text/javascript"></script>
+ <script src="_Scripts/jquery/jquery.customfunctions.js" type="text/javascript"></script>
+ <!-- jquery.ui requirements for upshot.dataview.js -->
+ <script src="_Scripts/jquery/jquery.ui.widget.js" type="text/javascript"></script>
+ <script src="_Scripts/jquery/jquery.ui.observable.js" type="text/javascript"></script>
+ <script src="_Scripts/jquery/jquery.ui.dataview.js" type="text/javascript"></script>
+
+ <script src="_Scripts/knockout/knockout-2.0.0.js" type="text/javascript"></script>
+
+ <script src="_Scripts/upshot/upshot.js" type="text/javascript"></script>
+ <!-- Knockout compat is loaded before jQueryUI so it's not the default -->
+ <script src="_Scripts/upshot/Upshot.Compat.Knockout.js" type="text/javascript"></script>
+ <script src="_Scripts/upshot/Upshot.Compat.jQueryUI.js" type="text/javascript"></script>
+ <script src="_Scripts/upshot/upshot.dataview.js" type="text/javascript"></script>
+
+ <!-- Prepare test bed -->
+ <script src="Scripts/TestSetup.js" type="text/javascript"></script>
+
+ <script src="upshot/Init.js" type="text/javascript"></script>
+</head>
+<body>
+ <h2 id="qunit-banner">
+ </h2>
+ <h1>Unit Tests</h1>
+ <div class="test-results">
+ <div id="qunit-testrunner-toolbar">
+ </div>
+ <h2 id="qunit-userAgent">
+ </h2>
+ <ol id="qunit-tests">
+ </ol>
+ <div id="qunit-fixture">
+ test markup, will be hidden</div>
+ </div>
+</body>
+</html> \ No newline at end of file
diff --git a/test/SPA.Test/Properties/AssemblyInfo.cs b/test/SPA.Test/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..b8c1d123
--- /dev/null
+++ b/test/SPA.Test/Properties/AssemblyInfo.cs
@@ -0,0 +1,35 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("SPA.Test")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("Microsoft IT")]
+[assembly: AssemblyProduct("SPA.Test")]
+[assembly: AssemblyCopyright("Copyright © Microsoft IT 2012")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("eec1a4da-c756-420c-8d83-c4a82fd490ab")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Revision and Build Numbers
+// by using the '*' as shown below:
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/test/SPA.Test/SPA.Test.csproj b/test/SPA.Test/SPA.Test.csproj
new file mode 100644
index 00000000..3edad23d
--- /dev/null
+++ b/test/SPA.Test/SPA.Test.csproj
@@ -0,0 +1,133 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProductVersion>
+ </ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{7B8601F8-8D1F-4B9C-8C20-772B673A2FA6}</ProjectGuid>
+ <ProjectTypeGuids>{349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc}</ProjectTypeGuids>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>SPA.Test</RootNamespace>
+ <AssemblyName>SPA.Test</AssemblyName>
+ <TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
+ <UseIISExpress>false</UseIISExpress>
+ <SrcOutputPath>$(OutputPath)</SrcOutputPath>
+ <OutputPath>$(OutputPath)\Test\</OutputPath>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <DefineConstants>DEBUG;TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <DefineConstants>TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="Microsoft.CSharp" />
+ <Reference Include="System.Web.DynamicData" />
+ <Reference Include="System.Web.Entity" />
+ <Reference Include="System.Web.ApplicationServices" />
+ <Reference Include="System" />
+ <Reference Include="System.Data" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.Data.DataSetExtensions" />
+ <Reference Include="System.Web.Extensions" />
+ <Reference Include="System.Xml.Linq" />
+ <Reference Include="System.Drawing" />
+ <Reference Include="System.Web" />
+ <Reference Include="System.Xml" />
+ <Reference Include="System.Configuration" />
+ <Reference Include="System.Web.Services" />
+ <Reference Include="System.EnterpriseServices" />
+ </ItemGroup>
+ <ItemGroup>
+ <Content Include="css\qunit.css" />
+ <Content Include="css\tests.css" />
+ <Content Include="Index.html" />
+ <Content Include="Scripts\IntellisenseFix.js" />
+ <Content Include="Scripts\References.js" />
+ <Content Include="Scripts\TestSetup.js" />
+ <Content Include="upshot\ChangeTracking.tests.js" />
+ <Content Include="upshot\Consistency.tests.js" />
+ <Content Include="upshot\Core.tests.js" />
+ <Content Include="upshot\DataContext.tests.js" />
+ <Content Include="upshot\DataProvider.tests.js" />
+ <Content Include="upshot\Datasets.js" />
+ <Content Include="upshot\DataSource.Common.js" />
+ <Content Include="upshot\DataSource.Tests.js" />
+ <Content Include="upshot\Delete.Tests.js" />
+ <Content Include="upshot\EntitySet.tests.js" />
+ <Content Include="upshot\Init.js" />
+ <Content Include="upshot\jQuery.DataView.Tests.js" />
+ <Content Include="upshot\Mapping.tests.js" />
+ <Content Include="upshot\RecordSet.js" />
+ <Content Include="Web.config" />
+ <Content Include="Web.Debug.config">
+ <DependentUpon>Web.config</DependentUpon>
+ </Content>
+ <Content Include="Web.Release.config">
+ <DependentUpon>Web.config</DependentUpon>
+ </Content>
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\SPA\SPA.csproj">
+ <Project>{1ACEF677-B6A0-4680-A076-7893DE176D6B}</Project>
+ <Name>SPA</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+ <Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v10.0\WebApplications\Microsoft.WebApplication.targets" />
+ <ProjectExtensions>
+ <VisualStudio>
+ <FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}">
+ <WebProjectProperties>
+ <UseIIS>False</UseIIS>
+ <AutoAssignPort>True</AutoAssignPort>
+ <DevelopmentServerPort>34597</DevelopmentServerPort>
+ <DevelopmentServerVPath>/</DevelopmentServerVPath>
+ <IISUrl>
+ </IISUrl>
+ <NTLMAuthentication>False</NTLMAuthentication>
+ <UseCustomServer>False</UseCustomServer>
+ <CustomServerUrl>
+ </CustomServerUrl>
+ <SaveServerSettingsInUserFile>False</SaveServerSettingsInUserFile>
+ </WebProjectProperties>
+ </FlavorProperties>
+ </VisualStudio>
+ </ProjectExtensions>
+ <Target Name="AfterBuild">
+ <ItemGroup>
+ <SpaFiles Include="$(SrcOutputPath)\upshot.js" />
+ <SpaFiles Include="$(SrcOutputPath)\Upshot.Compat.Knockout.js" />
+ <SpaFiles Include="$(SrcOutputPath)\Upshot.Compat.jQueryUI.js" />
+ <SpaFiles Include="$(SrcOutputPath)\upshot.dataview.js" />
+ </ItemGroup>
+ <Copy SourceFiles="@(SpaFiles)" DestinationFolder="_Scripts\upshot" />
+ </Target>
+ <Target Name="AfterClean">
+ <ItemGroup>
+ <SpaFiles Include="_Scripts\upshot\*" />
+ </ItemGroup>
+ <Delete Files="@(SpaFiles)" />
+ </Target>
+ <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
+ Other similar extension points exist, see Microsoft.Common.targets.
+ <Target Name="BeforeBuild">
+ </Target>
+ -->
+</Project> \ No newline at end of file
diff --git a/test/SPA.Test/Scripts/IntellisenseFix.js b/test/SPA.Test/Scripts/IntellisenseFix.js
new file mode 100644
index 00000000..8527233e
--- /dev/null
+++ b/test/SPA.Test/Scripts/IntellisenseFix.js
@@ -0,0 +1,32 @@
+// These changes remove Intellisense "Function expected" or "Object expected" messages due to
+// code that relies on running in the browser. Simply include a reference in
+// before QUnit or jQuery like so: /// <reference path="{path}/IntellisenseFix.js" />
+// There is no need to include this in the actual html page
+
+//QUnit fixes
+(function () {
+ var test = document.getElementById("");
+ if (test && !test.style) {
+ var oldGet = document.getElementById;
+ document.getElementById = function (id) {
+ var el = oldGet(id);
+ el.style = el.getAttribute("style");
+ return el;
+ };
+ }
+
+ if (window.location && !window.location.search) {
+ window.location.search = "";
+ }
+})();
+
+//jQuery fixes 1.6.1
+(function () {
+ if (!document.documentElement.childNodes[0]) {
+ document.documentElement.childNodes = [{ nodeType: null}];
+ }
+
+ if (!location.href) {
+ location.href = "";
+ }
+})(); \ No newline at end of file
diff --git a/test/SPA.Test/Scripts/References.js b/test/SPA.Test/Scripts/References.js
new file mode 100644
index 00000000..8aa83134
--- /dev/null
+++ b/test/SPA.Test/Scripts/References.js
@@ -0,0 +1,20 @@
+/// <reference path="IntellisenseFix.js" />
+/// <reference path="qunit/qunit.js" />
+/// <reference path="SharedScripts/jquery-1.6.2.js" />
+/// <reference path="SharedScripts/jquery.tmpl.js" />
+/// <reference path="SharedScripts/jquery.ui.observable.js" />
+/// <reference path="SharedScripts/jquery.datalink.js" />
+/// <reference path="SharedScripts/jquery.customfunctions.js" />
+/// <reference path="UpshotScripts/upshot.js" />
+/// <reference path="UpshotScripts/Upshot.Compat.Knockout.js" />
+/// <reference path="UpshotScripts/Upshot.Compat.jQueryUI.js" />
+/// <reference path="UpshotScripts/Upshot.Compat.JsViews.js" />
+/// <reference path="SharedScripts/jquery-ui.js" />
+/// <reference path="SharedScripts/jquery.render.js" />
+/// <reference path="SharedScripts/jquery.views.js" />
+/// <reference path="SharedScripts/jquery.list.js" />
+/// <reference path="SharedScripts/knockout-2.0.0.debug.js" />
+
+/// <reference path="TestSetup.js" />
+
+// This file enables VS IntelliSense in JavaScript, otherwise it can be removed \ No newline at end of file
diff --git a/test/SPA.Test/Scripts/TestSetup.js b/test/SPA.Test/Scripts/TestSetup.js
new file mode 100644
index 00000000..37799b34
--- /dev/null
+++ b/test/SPA.Test/Scripts/TestSetup.js
@@ -0,0 +1,320 @@
+/// <reference path="../../Scripts/References.js" />
+
+if (window.sessionStorage) {
+ window.sessionStorage.clear();
+}
+module("DataSource Setup");
+
+var defaultTestTimeout = 10000,
+ retryLimit = 10;
+
+QUnit.config.testTimeout = defaultTestTimeout;
+
+var testHelper = {
+ isChrome: /chrome/.test(navigator.userAgent.toLowerCase()),
+ isFirefox: /mozilla/.test(navigator.userAgent.toLowerCase()) && (!/(compatible|webkit)/.test(navigator.userAgent.toLowerCase())),
+ initService: function (url, typeOrOptions) {
+ QUnit.ok(true, "start Init service:" + url + ", time: " + new Date().toLocaleString());
+ QUnit.config.testTimeout = 120 * 1000;
+ stop();
+ var beat = setInterval(function () {
+ if (window.TestSwarm && window.TestSwarm.heartbeat) {
+ window.TestSwarm.heartbeat();
+ }
+ }, 1000);
+ testHelper.setCookie("dbi", null, -1);
+
+ var options;
+ if (typeof(typeOrOptions) === "string") {
+ options = { type: typeOrOptions };
+ } else {
+ options = typeOrOptions;
+ }
+
+ testHelper.serviceCall(url, options, 0);
+ },
+ serviceCall: function (url, options, retried, beat) {
+ jQuery.ajax(url, options).then(function () {
+ QUnit.config.testTimeout = defaultTestTimeout;
+ clearInterval(beat);
+ QUnit.ok(true, "Service request succeeded, tried: " + (retried + 1) + " times, timestamp:" + new Date().toLocaleString());
+ start();
+ }, function () {
+ QUnit.ok(true, "Service request failed, retrying " + (retryLimit - retried - 1) + " more times, timestamp:" + new Date().toLocaleString());
+ setTimeout(function () {
+ retried++;
+ if (retried < retryLimit) {
+ testHelper.serviceCall(url, options, retried);
+ } else {
+ // Fail the rest quickly
+ QUnit.ok(false, "Service call to " + url + " failed");
+ QUnit.config.testTimeout = 100;
+ clearInterval(beat);
+ start();
+ }
+ }, 1000);
+ })
+ },
+
+ setCookie: function (name, value, days) {
+ if (days) { var date = new Date(); date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); var expires = "; expires=" + date.toGMTString(); } else var expires = ""; document.cookie = name + "=" + value + expires + "; path=/";
+ },
+
+ startOnPageSetupComplete: function (url) {
+ stop();
+ jQuery("#testFrame").one("load", function () {
+ // make $ reference the jQuery in the iframe, intentionally global for tests to use
+ var testFrame = window.frames[0];
+ $ = testFrame.jQuery;
+
+ //Wait for the page to trigger complete
+ $(testFrame).one("PageSetupComplete", function () {
+ start()
+ });
+ });
+ jQuery("#testFrame").attr("src", url);
+ },
+ startOnPageLoad: function (url, cond) {
+ stop();
+ jQuery("#testFrame").one("load", function () {
+ // make $ reference the jQuery in the iframe, intentionally global for tests to use
+ var testFrame = window.frames[0];
+ $ = testFrame.jQuery;
+ testFrame.alert = function (error) { ok(false, "'alert(" + error + ")' called, failing test"); };
+ if (cond) {
+ var checkCond = function () {
+ if (cond()) {
+ start();
+ } else {
+ setTimeout(function () { checkCond(); }, 200);
+ }
+ }
+ checkCond();
+ } else {
+ start();
+ }
+ });
+ jQuery("#testFrame").attr("src", url);
+ },
+ wrapFunctions: function (functionNames, triggerName) {
+ // Allow string or array of strings to be passed
+ functionNames = $.isArray(functionNames) ? functionNames : [functionNames];
+
+ $.each(functionNames, function (index, functionName) {
+ // Only allow one wrapping per function
+ if ($.fn[functionName].__oldFunction__) {
+ testHelper.unwrapFunctions([functionName]);
+ }
+ var newFunction = function () {
+ // Create trigger arguments with function's name that was wrapped
+ var returnedArgs = [functionName].concat(Array.prototype.slice.call(arguments, 0)),
+ returnValue = newFunction.__oldFunction__.apply(this, arguments);
+ // trigger after applying to allow state to be checked in callback
+ $(this).trigger(triggerName, returnedArgs);
+ // return the results so wrapped function behaves the same
+ return returnValue;
+ };
+ // Store the new function on the old one
+ newFunction.__oldFunction__ = $.fn[functionName];
+ $.fn[functionName] = newFunction;
+ });
+ return function () {
+ //do cleanup
+ testHelper.unwrapFunctions(functionNames);
+ };
+ },
+ unwrapFunctions: function (functionNames) {
+ $.each(functionNames, function (index, name) {
+ $.fn[name] = $.fn[name].__oldFunction__;
+ });
+ },
+ curryStartOnPageLoad: function (url) {
+ // Return a function with url already set, helpful for passing a callback to a module
+ return function () {
+ stop();
+ jQuery("#testFrame").one("load", function () {
+ // make $ reference the jQuery in the iframe, intentionally global for tests to use
+ window.$ = window.frames[0].jQuery;
+ // TODO: Fix this workaround for page load being too slow to load a new one every test function
+ // especially slow when debugging
+ setTimeout(QUnit.start, 100);
+ });
+ jQuery("#testFrame").attr("src", url);
+ }
+ },
+ curryPost: function (url) {
+ return function () {
+ QUnit.stop();
+ //REVIEW: This should be an implementation detail of DataSource / DomainSerivceProxy
+ jQuery.post(url, function () {
+ QUnit.start();
+ }, "json");
+ }
+ },
+ setLatency: function (url, queryDelay, cudDelay, success) {
+ jQuery.ajax({
+ type: "POST",
+ url: url + "/SetLatency",
+ data: '{"queryDelay":' + queryDelay + ',"cudDelay":' + cudDelay + '}',
+ dataType: "json",
+ contentType: "application/json",
+ success: function () {
+ success();
+ },
+ error: function () {
+ ok(false, "setLatency request failed");
+ start();
+ }
+ });
+ },
+ wrapOrAddFunction: function (baseObject, functionName, options) {
+ // This function either:
+ // 1) replaces the existing function making callbacks before and after the function would have been invoked
+ // 2) creates the function making callbacks before and after the function would have been invoked
+ // If the function existed it will be called as if it were not replaced
+ // After the function is invoked once it will be reverted back to its original state unless
+ // revertToOriginal: false is given via the options parameter
+ // The original function is the return value which allows for the function to be reverted at any time (not after a single callback as usual)
+
+ var before = options.before,
+ after = options.after,
+ revertToOriginal = options.revertToOriginal !== false, // Default to true when unsupplied
+ backupOfOriginalFunction;
+
+ // If function exists back it up
+ if (baseObject[functionName]) {
+ backupOfOriginalFunction = baseObject[functionName];
+ }
+
+ // Overwrite or create function
+ baseObject[functionName] = function () {
+ // Revert to original method, revertToOriginal is true by default
+ if (revertToOriginal) {
+ if (backupOfOriginalFunction) {
+ baseObject[functionName] = backupOfOriginalFunction;
+ } else {
+ // If there was no original function remove the one added by wrapOrAddFunction
+ delete baseObject[functionName];
+ }
+ }
+
+ if (before) {
+ before.apply(this, Array.prototype.slice.call(arguments));
+ }
+
+ if (backupOfOriginalFunction) {
+ backupOfOriginalFunction.apply(this, Array.prototype.slice.call(arguments));
+ }
+
+ if (after) {
+ after.apply(this, Array.prototype.slice.call(arguments));
+ }
+ }
+
+ // Allows one to revert to original state in their own code when revertToOriginal is false
+ return backupOfOriginalFunction;
+ },
+ whenCondition: function (callback, interval, timeout) {
+ var retry,
+ deferred = $.Deferred();
+ interval = interval || 500;
+ timeout = timeout || QUnit.config.testTimeout;
+ retry = timeout / interval;
+
+ if (!callback()) {
+ var pollCallback = function () {
+ if (!callback()) {
+ --retry;
+ if (retry > 0) {
+ setTimeout(pollCallback, interval);
+ } else {
+ deferred.reject();
+ }
+ } else {
+ deferred.resolve();
+ }
+ };
+ setTimeout(pollCallback, interval);
+ } else {
+ deferred.resolve();
+ }
+
+ return deferred.promise();
+ },
+ startOnCondition: function (callback, interval, timeout) {
+ var retry;
+ interval = interval || 500;
+ timeout = timeout || QUnit.config.testTimeout;
+ retry = timeout / interval;
+ stop();
+ testHelper.whenCondition(callback, interval)
+ .then(start);
+ },
+ changeValueAndWait: function (selector, value, delay) {
+ stop();
+ var input = $(selector);
+ if (input.length !== 1) {
+ throw "Only support one input";
+ }
+ var test = function () {
+ input.unbind("change", test);
+ setTimeout(function () { start(); }, delay ? delay : 100);
+ }
+ input.one("change", test).focusin().val(value).focusout().trigger("change");
+ },
+ // this will allow one-time ajax mock.
+ mockAjaxOnce: function (url, result, statusText, error) {
+ if ($.ajax._simulatedurl) {
+ throw "cannot simulate ajax concurrently (prev=" + $.ajax._simulatedurl + ")";
+ }
+ var $ajax = $.ajax;
+ $.ajax = function (settings) {
+ if (settings.url.indexOf(url) == 0) {
+ // revert $.ajax to orginal
+ $.ajax = $ajax;
+ var deferred = $.Deferred();
+ setTimeout(function () {
+ // this follows $.ajax().fail() and $.ajax().done() signature
+ if (statusText) {
+ if (settings.error) {
+ settings.error.apply(null, [{ responseText: statusText, status: 200 }, statusText, error]);
+ }
+ deferred.reject(undefined, statusText, error);
+ } else {
+ if (settings.type === "POST") {
+ var data = JSON.parse(settings.data);
+ var ret = result || {
+ SubmitChangesResult: [{ Entity: data.changeSet[0].Entity}]
+ };
+ if (settings.success) {
+ settings.success.apply(null, [ret, "statusText", { status: 200}]);
+ }
+ deferred.resolve(ret);
+ } else {
+ var ret = result || {
+ EmptyResult: {
+ RootResults: [],
+ Metadata: [{ type: "dummy"}]
+ }
+ };
+ if (settings.success) {
+ settings.success.apply(null, [ret]);
+ }
+ deferred.resolve(ret);
+ }
+ }
+ }, 10);
+ return deferred.promise();
+ } else {
+ return $ajax(settings);
+ }
+ }
+ $.ajax._simulatedurl = url;
+ $.ajax._$ajax = $ajax;
+ },
+ unmockAjax: function () {
+ if ($.ajax._$ajax) {
+ $.ajax = $.ajax._$ajax;
+ }
+ }
+}; \ No newline at end of file
diff --git a/test/SPA.Test/Web.Debug.config b/test/SPA.Test/Web.Debug.config
new file mode 100644
index 00000000..2c6dd51a
--- /dev/null
+++ b/test/SPA.Test/Web.Debug.config
@@ -0,0 +1,30 @@
+<?xml version="1.0"?>
+
+<!-- For more information on using web.config transformation visit http://go.microsoft.com/fwlink/?LinkId=125889 -->
+
+<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
+ <!--
+ In the example below, the "SetAttributes" transform will change the value of
+ "connectionString" to use "ReleaseSQLServer" only when the "Match" locator
+ finds an atrribute "name" that has a value of "MyDB".
+
+ <connectionStrings>
+ <add name="MyDB"
+ connectionString="Data Source=ReleaseSQLServer;Initial Catalog=MyReleaseDB;Integrated Security=True"
+ xdt:Transform="SetAttributes" xdt:Locator="Match(name)"/>
+ </connectionStrings>
+ -->
+ <system.web>
+ <!--
+ In the example below, the "Replace" transform will replace the entire
+ <customErrors> section of your web.config file.
+ Note that because there is only one customErrors section under the
+ <system.web> node, there is no need to use the "xdt:Locator" attribute.
+
+ <customErrors defaultRedirect="GenericError.htm"
+ mode="RemoteOnly" xdt:Transform="Replace">
+ <error statusCode="500" redirect="InternalError.htm"/>
+ </customErrors>
+ -->
+ </system.web>
+</configuration> \ No newline at end of file
diff --git a/test/SPA.Test/Web.Release.config b/test/SPA.Test/Web.Release.config
new file mode 100644
index 00000000..4122d79b
--- /dev/null
+++ b/test/SPA.Test/Web.Release.config
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+
+<!-- For more information on using web.config transformation visit http://go.microsoft.com/fwlink/?LinkId=125889 -->
+
+<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
+ <!--
+ In the example below, the "SetAttributes" transform will change the value of
+ "connectionString" to use "ReleaseSQLServer" only when the "Match" locator
+ finds an atrribute "name" that has a value of "MyDB".
+
+ <connectionStrings>
+ <add name="MyDB"
+ connectionString="Data Source=ReleaseSQLServer;Initial Catalog=MyReleaseDB;Integrated Security=True"
+ xdt:Transform="SetAttributes" xdt:Locator="Match(name)"/>
+ </connectionStrings>
+ -->
+ <system.web>
+ <compilation xdt:Transform="RemoveAttributes(debug)" />
+ <!--
+ In the example below, the "Replace" transform will replace the entire
+ <customErrors> section of your web.config file.
+ Note that because there is only one customErrors section under the
+ <system.web> node, there is no need to use the "xdt:Locator" attribute.
+
+ <customErrors defaultRedirect="GenericError.htm"
+ mode="RemoteOnly" xdt:Transform="Replace">
+ <error statusCode="500" redirect="InternalError.htm"/>
+ </customErrors>
+ -->
+ </system.web>
+</configuration> \ No newline at end of file
diff --git a/test/SPA.Test/Web.config b/test/SPA.Test/Web.config
new file mode 100644
index 00000000..ea5e4d6e
--- /dev/null
+++ b/test/SPA.Test/Web.config
@@ -0,0 +1,13 @@
+<?xml version="1.0"?>
+
+<!--
+ For more information on how to configure your ASP.NET application, please visit
+ http://go.microsoft.com/fwlink/?LinkId=169433
+ -->
+
+<configuration>
+ <system.web>
+ <compilation debug="true" targetFramework="4.0" />
+ </system.web>
+
+</configuration>
diff --git a/test/SPA.Test/css/qunit.css b/test/SPA.Test/css/qunit.css
new file mode 100644
index 00000000..e114ea06
--- /dev/null
+++ b/test/SPA.Test/css/qunit.css
@@ -0,0 +1,226 @@
+/**
+ * QUnit 1.2.0pre - A JavaScript Unit Testing Framework
+ *
+ * http://docs.jquery.com/QUnit
+ *
+ * Copyright (c) 2011 John Resig, Jörn Zaefferer
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * or GPL (GPL-LICENSE.txt) licenses.
+ */
+
+/** Font Family and Sizes */
+
+#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult {
+ font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
+}
+
+#qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; }
+#qunit-tests { font-size: smaller; }
+
+
+/** Resets */
+
+#qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult {
+ margin: 0;
+ padding: 0;
+}
+
+
+/** Header */
+
+#qunit-header {
+ padding: 0.5em 0 0.5em 1em;
+
+ color: #8699a4;
+ background-color: #0d3349;
+
+ font-size: 1.5em;
+ line-height: 1em;
+ font-weight: normal;
+
+ border-radius: 15px 15px 0 0;
+ -moz-border-radius: 15px 15px 0 0;
+ -webkit-border-top-right-radius: 15px;
+ -webkit-border-top-left-radius: 15px;
+}
+
+#qunit-header a {
+ text-decoration: none;
+ color: #c2ccd1;
+}
+
+#qunit-header a:hover,
+#qunit-header a:focus {
+ color: #fff;
+}
+
+#qunit-banner {
+ height: 5px;
+}
+
+#qunit-testrunner-toolbar {
+ padding: 0.5em 0 0.5em 2em;
+ color: #5E740B;
+ background-color: #eee;
+}
+
+#qunit-userAgent {
+ padding: 0.5em 0 0.5em 2.5em;
+ background-color: #2b81af;
+ color: #fff;
+ text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
+}
+
+
+/** Tests: Pass/Fail */
+
+#qunit-tests {
+ list-style-position: inside;
+}
+
+#qunit-tests li {
+ padding: 0.4em 0.5em 0.4em 2.5em;
+ border-bottom: 1px solid #fff;
+ list-style-position: inside;
+}
+
+#qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running {
+ display: none;
+}
+
+#qunit-tests li strong {
+ cursor: pointer;
+}
+
+#qunit-tests li a {
+ padding: 0.5em;
+ color: #c2ccd1;
+ text-decoration: none;
+}
+#qunit-tests li a:hover,
+#qunit-tests li a:focus {
+ color: #000;
+}
+
+#qunit-tests ol {
+ margin-top: 0.5em;
+ padding: 0.5em;
+
+ background-color: #fff;
+
+ border-radius: 15px;
+ -moz-border-radius: 15px;
+ -webkit-border-radius: 15px;
+
+ box-shadow: inset 0px 2px 13px #999;
+ -moz-box-shadow: inset 0px 2px 13px #999;
+ -webkit-box-shadow: inset 0px 2px 13px #999;
+}
+
+#qunit-tests table {
+ border-collapse: collapse;
+ margin-top: .2em;
+}
+
+#qunit-tests th {
+ text-align: right;
+ vertical-align: top;
+ padding: 0 .5em 0 0;
+}
+
+#qunit-tests td {
+ vertical-align: top;
+}
+
+#qunit-tests pre {
+ margin: 0;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+}
+
+#qunit-tests del {
+ background-color: #e0f2be;
+ color: #374e0c;
+ text-decoration: none;
+}
+
+#qunit-tests ins {
+ background-color: #ffcaca;
+ color: #500;
+ text-decoration: none;
+}
+
+/*** Test Counts */
+
+#qunit-tests b.counts { color: black; }
+#qunit-tests b.passed { color: #5E740B; }
+#qunit-tests b.failed { color: #710909; }
+
+#qunit-tests li li {
+ margin: 0.5em;
+ padding: 0.4em 0.5em 0.4em 0.5em;
+ background-color: #fff;
+ border-bottom: none;
+ list-style-position: inside;
+}
+
+/*** Passing Styles */
+
+#qunit-tests li li.pass {
+ color: #5E740B;
+ background-color: #fff;
+ border-left: 26px solid #C6E746;
+}
+
+#qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; }
+#qunit-tests .pass .test-name { color: #366097; }
+
+#qunit-tests .pass .test-actual,
+#qunit-tests .pass .test-expected { color: #999999; }
+
+#qunit-banner.qunit-pass { background-color: #C6E746; }
+
+/*** Failing Styles */
+
+#qunit-tests li li.fail {
+ color: #710909;
+ background-color: #fff;
+ border-left: 26px solid #EE5757;
+ white-space: pre;
+}
+
+#qunit-tests > li:last-child {
+ border-radius: 0 0 15px 15px;
+ -moz-border-radius: 0 0 15px 15px;
+ -webkit-border-bottom-right-radius: 15px;
+ -webkit-border-bottom-left-radius: 15px;
+}
+
+#qunit-tests .fail { color: #000000; background-color: #EE5757; }
+#qunit-tests .fail .test-name,
+#qunit-tests .fail .module-name { color: #000000; }
+
+#qunit-tests .fail .test-actual { color: #EE5757; }
+#qunit-tests .fail .test-expected { color: green; }
+
+#qunit-banner.qunit-fail { background-color: #EE5757; }
+
+
+/** Result */
+
+#qunit-testresult {
+ padding: 0.5em 0.5em 0.5em 2.5em;
+
+ color: #2b81af;
+ background-color: #D2E0E6;
+
+ border-bottom: 1px solid white;
+}
+
+/** Fixture */
+
+#qunit-fixture {
+ position: absolute;
+ top: -10000px;
+ left: -10000px;
+}
diff --git a/test/SPA.Test/css/tests.css b/test/SPA.Test/css/tests.css
new file mode 100644
index 00000000..cfd317c9
--- /dev/null
+++ b/test/SPA.Test/css/tests.css
@@ -0,0 +1,15 @@
+body
+{
+ padding: 0px;
+ margin: 0px;
+}
+h1
+{
+ margin: 5px;
+}
+div.test-results
+{
+ width: 500px;
+ height: 700px;
+ margin:5px;
+}
diff --git a/test/SPA.Test/upshot/ChangeTracking.tests.js b/test/SPA.Test/upshot/ChangeTracking.tests.js
new file mode 100644
index 00000000..aacafce7
--- /dev/null
+++ b/test/SPA.Test/upshot/ChangeTracking.tests.js
@@ -0,0 +1,348 @@
+/// <reference path="../Scripts/References.js" />
+
+(function (upshot, ko, undefined) {
+
+ module("ChangeTracking");
+
+ var observability = upshot.observability;
+
+ test("Explicit commit multiple property edits", 5, function () {
+ var submitCount = 0;
+ var propertyChangedCount = 0;
+
+ var dc = createTestContext(function () {
+ submitCount++;
+ }, true);
+
+ var entitySet = dc.getEntitySet("Product");
+ var prod = entitySet.getEntities()[0];
+ var state = entitySet.getEntityState(prod);
+ equal(state, upshot.EntityState.Unmodified);
+
+ entitySet.bind("propertyChanged", function () {
+ propertyChangedCount++;
+ });
+
+ $.observable(prod).property("Name", "xyz");
+ $.observable(prod).property("Name", "foo");
+ $.observable(prod).property("Price", 9.99);
+
+ equal(propertyChangedCount, 3);
+
+ state = entitySet.getEntityState(prod);
+ equal(state, upshot.EntityState.ClientUpdated);
+
+ equal(submitCount, 0);
+
+ dc.commitChanges();
+
+ equal(submitCount, 1);
+ });
+
+ asyncTest("Implicit commit multiple property edits", 5, function () {
+ var submitCount = 0;
+ var propertyChangedCount = 0;
+
+ var dc = createTestContext(function () {
+ submitCount++;
+ }, false);
+
+ var entitySet = dc.getEntitySet("Product");
+ var prod = entitySet.getEntities()[0];
+ var state = entitySet.getEntityState(prod);
+ equal(state, upshot.EntityState.Unmodified);
+
+ entitySet.bind("propertyChanged", function () {
+ propertyChangedCount++;
+ });
+
+ $.observable(prod).property("Name", "xyz");
+ $.observable(prod).property("Name", "foo");
+ $.observable(prod).property("Price", 9.99);
+
+ // before the timeout has expired we shouldn't have committed anything
+ equal(submitCount, 0);
+
+ equal(propertyChangedCount, 3);
+
+ state = entitySet.getEntityState(prod);
+ equal(state, upshot.EntityState.ClientUpdated);
+
+ // Here we queue the test verification and start so that it runs
+ // AFTER the queued commit
+ setTimeout(function () {
+ equal(submitCount, 1);
+ start();
+ }, 0);
+
+ });
+
+ asyncTest("Implicit commit multiple array operations", 5, function () {
+ var submitCount = 0;
+
+ var dc = createTestContext(function (options, editedEntities) {
+ submitCount++;
+ equal(editedEntities.length, 2);
+ }, false);
+
+ var entitySet = dc.getEntitySet("Product");
+ var prod = entitySet.getEntities()[0];
+
+ // do an insert
+ var newProd = {
+ ID: 2,
+ Name: "Frish Gnarbles",
+ Category: "Snacks",
+ Price: 7.99
+ };
+ $.observable(entitySet.getEntities()).insert(newProd);
+ var state = entitySet.getEntityState(newProd);
+ equal(state, upshot.EntityState.ClientAdded);
+
+ // do a delete
+ entitySet.deleteEntity(prod);
+ state = entitySet.getEntityState(prod);
+ equal(state, upshot.EntityState.ClientDeleted);
+
+ // before the timeout has expired we shouldn't have committed anything
+ equal(submitCount, 0);
+
+ // Here we queue the test verification and start so that it runs
+ // AFTER the queued commit
+ setTimeout(function () {
+ equal(submitCount, 1);
+ start();
+ }, 0);
+ });
+
+/* TODO: We forced managed associations on for Dev11 beta, since unmanaged associations are broken.
+ test("Nested entities can be added to an entity, and navigation properties are untracked", 16, function () {
+ try {
+ upshot.observability.configuration = observability.knockout;
+
+ var manageAssociations = false;
+ var dc = createKoTestContext(function () { }, false, manageAssociations);
+
+ var orders = dc.getEntitySet("Order"),
+ orderDetails = dc.getEntitySet("OrderDetail"),
+ order = orders.getEntities()()[0],
+ order2 = orders.getEntities()()[1],
+ orderDetail = orderDetails.getEntities()()[0];
+
+ order.OrderDetails.remove(orderDetail);
+ equal(1, order.OrderDetails().length, "There should only be a single order detail");
+ equal(upshot.EntityState.Unmodified, orders.getEntityState(order), "The order should not be modified");
+ equal(upshot.EntityState.Unmodified, orderDetails.getEntityState(orderDetail), "The order detail should not be modified");
+
+ order.OrderDetails.push(orderDetail);
+ equal(2, order.OrderDetails().length, "There should be two order details");
+ equal(upshot.EntityState.Unmodified, orders.getEntityState(order), "The order should not be modified");
+ equal(upshot.EntityState.Unmodified, orderDetails.getEntityState(orderDetail), "The order detail should not be modified");
+
+ orderDetail.Order(order2);
+ equal(orderDetail.OrderId(), order2.Id(), "Ids should be equal");
+ equal(upshot.EntityState.Unmodified, orders.getEntityState(order2), "The order should not be modified");
+ equal(upshot.EntityState.ClientUpdated, orderDetails.getEntityState(orderDetail), "The order detail should not be modified");
+ equal(true, orderDetails.isUpdated(orderDetail, "OrderId"), "The OrderId should be modified");
+ equal(false, orderDetails.isUpdated(orderDetail, "Order"), "The Order should not be tracked");
+
+ var properties = [];
+ $.each(observability.knockout.unmap(orderDetail, "OrderDetail"), function (key, value) {
+ properties.push(key);
+ equal(ko.utils.unwrapObservable(orderDetail[key]), value, "Properties should be equal");
+ });
+ equal(properties.length, 4, "The should be 4 serialized properties");
+
+ } finally {
+ upshot.observability.configuration = observability.jquery;
+ }
+ });
+*/
+
+ test("Nested entities can be added to an entity, and navigation properties are computed", 39, function () {
+ try {
+ upshot.observability.configuration = observability.knockout;
+
+ var dc = createKoTestContext(function () { }, false);
+
+ var orders = dc.getEntitySet("Order"),
+ orderDetails = dc.getEntitySet("OrderDetail"),
+ order = orders.getEntities()()[0],
+ order2 = orders.getEntities()()[1],
+ orderDetail = orderDetails.getEntities()()[0];
+
+ ok(order.OrderDetails.indexOf(orderDetail) >= 0 && order2.OrderDetails.indexOf(orderDetail) < 0 && orderDetail.Order() === order, "The order detail is a child of order1");
+ equal(orders.getEntityState(order), upshot.EntityState.Unmodified, "order1 should not be modified");
+ equal(orders.getEntityState(order2), upshot.EntityState.Unmodified, "order2 should not be modified");
+ equal(orderDetails.getEntityState(orderDetail), upshot.EntityState.Unmodified, "The order detail should not be modified");
+ equal(orderDetails.isUpdated(orderDetail, "OrderId"), false, "The OrderId should not be modified");
+ equal(orderDetails.isUpdated(orderDetail, "Order"), false, "The Order should not be tracked");
+
+ orderDetail.OrderId(order2.Id());
+ ok(order2.OrderDetails.indexOf(orderDetail) >= 0 && order.OrderDetails.indexOf(orderDetail) < 0 && orderDetail.Order() === order2, "The order detail is a child of order2");
+ equal(orders.getEntityState(order), upshot.EntityState.Unmodified, "order1 should not be modified");
+ equal(orders.getEntityState(order2), upshot.EntityState.Unmodified, "order2 should not be modified");
+ equal(orderDetails.getEntityState(orderDetail), upshot.EntityState.ClientUpdated, "The order detail should be ClientUpdated");
+ equal(orderDetails.isUpdated(orderDetail, "OrderId"), true, "The OrderId should be modified");
+ equal(orderDetails.isUpdated(orderDetail, "Order"), false, "The Order should not be tracked");
+
+ orderDetails.revertUpdates(orderDetail);
+ ok(order.OrderDetails.indexOf(orderDetail) >= 0 && order2.OrderDetails.indexOf(orderDetail) < 0 && orderDetail.Order() === order, "The order detail is a child of order1");
+ equal(orders.getEntityState(order), upshot.EntityState.Unmodified, "order1 should not be modified");
+ equal(orders.getEntityState(order2), upshot.EntityState.Unmodified, "order2 should not be modified");
+ equal(orderDetails.getEntityState(orderDetail), upshot.EntityState.Unmodified, "The order detail should not be modified");
+ equal(orderDetails.isUpdated(orderDetail, "OrderId"), false, "The OrderId should not be modified");
+ equal(orderDetails.isUpdated(orderDetail, "Order"), false, "The Order should not be tracked");
+
+ orderDetail.Order(order2);
+ ok(orderDetail.OrderId() === order2.Id(), "Ids should be equal");
+ equal(orders.getEntityState(order2), upshot.EntityState.Unmodified, "The order should not be modified");
+ equal(orderDetails.getEntityState(orderDetail), upshot.EntityState.ClientUpdated, "The order detail should be ClientUpdated");
+ equal(orderDetails.isUpdated(orderDetail, "OrderId"), true, "The OrderId should be modified");
+ equal(orderDetails.isUpdated(orderDetail, "Order"), false, "The Order should not be tracked");
+
+ orderDetails.revertUpdates(orderDetail);
+ ok(order.OrderDetails.indexOf(orderDetail) >= 0 && order2.OrderDetails.indexOf(orderDetail) < 0 && orderDetail.Order() === order, "The order detail is a child of order1");
+ equal(orders.getEntityState(order), upshot.EntityState.Unmodified, "order1 should not be modified");
+ equal(orders.getEntityState(order2), upshot.EntityState.Unmodified, "order2 should not be modified");
+ equal(orderDetails.getEntityState(orderDetail), upshot.EntityState.Unmodified, "The order detail should not be modified");
+ equal(orderDetails.isUpdated(orderDetail, "OrderId"), false, "The OrderId should not be modified");
+ equal(orderDetails.isUpdated(orderDetail, "Order"), false, "The Order should not be tracked");
+
+ orderDetail.Order(null);
+ equal(orderDetail.OrderId(), null, "FK should be null");
+ equal(orders.getEntityState(order), upshot.EntityState.Unmodified, "The old order should not be modified");
+ equal(orderDetails.getEntityState(orderDetail), upshot.EntityState.ClientUpdated, "The order detail should be ClientUpdated");
+ equal(orderDetails.isUpdated(orderDetail, "OrderId"), true, "The OrderId should be modified");
+ equal(orderDetails.isUpdated(orderDetail, "Order"), false, "The Order should not be tracked");
+
+ var properties = [];
+ $.each(observability.knockout.unmap(orderDetail, "OrderDetail"), function (key, value) {
+ properties.push(key);
+ equal(ko.utils.unwrapObservable(orderDetail[key]), value, "Properties should be equal");
+ });
+ equal(properties.length, 4, "The should be 4 serialized properties");
+
+ } finally {
+ upshot.observability.configuration = observability.jquery;
+ }
+ });
+
+ test("Nested entities can be added to an entity, and property changes do not bubble to the parent", 2, function () {
+ try {
+ upshot.observability.configuration = observability.knockout;
+
+ var dc = createKoTestContext(function () { }, false);
+
+ var orders = dc.getEntitySet("Order"),
+ orderDetails = dc.getEntitySet("OrderDetail"),
+ order = orders.getEntities()()[0],
+ orderDetail = orderDetails.getEntities()()[0];
+
+ orderDetail.Name("asdf");
+ equal(orders.getEntityState(order), upshot.EntityState.Unmodified, "The order should not be modified");
+ equal(orderDetails.getEntityState(orderDetail), upshot.EntityState.ClientUpdated, "The order detail should not be modified");
+ } finally {
+ upshot.observability.configuration = observability.jquery;
+ }
+ });
+
+ // Create and return a context using the specified submitChanges mock
+ function createTestContext(submitChangesMock, bufferChanges) {
+ var dataProvider = new upshot.DataProvider();
+ var implicitCommitHandler;
+ if (!bufferChanges) {
+ implicitCommitHandler = function () {
+ dc._commitChanges({ providerParameters: {} });
+ }
+ }
+ var dc = new upshot.DataContext(dataProvider, implicitCommitHandler);
+ dc._submitChanges = submitChangesMock;
+
+ // add a single product to the context
+ var type = "Product";
+ var products = [];
+ products.push({
+ ID: 1,
+ Name: "Crispy Snarfs",
+ Category: "Snacks",
+ Price: 12.99
+ });
+ products.push({
+ ID: 2,
+ Name: "Cheezy Snax",
+ Category: "Snacks",
+ Price: 1.99
+ });
+
+ // mock out enough metadata to do the attach
+ upshot.metadata(type, { key: ["ID"] });
+
+ dc.merge(products, type, null);
+
+ return dc;
+ }
+
+ function createKoTestContext(submitChangesMock, bufferChanges) {
+ var dataProvider = new upshot.DataProvider();
+ var implicitCommitHandler;
+ if (!bufferChanges) {
+ implicitCommitHandler = function () {
+ dc._commitChanges({ providerParameters: {} });
+ }
+ }
+ var dc = new upshot.DataContext(dataProvider, implicitCommitHandler);
+ dc._submitChanges = submitChangesMock;
+
+ // add a single product to the context
+ var manageAssociations = true; // TODO: Lift this to a createKoTestContext parameter when unmanaged associations is supported.
+ var OrderDetail = function (data) {
+ this.Id = ko.observable(data.Id);
+ this.Name = ko.observable(data.Name);
+ this.Order = ko.observable();
+ this.OrderId = manageAssociations ? ko.observable(data.OrderId) : ko.computed(function () {
+ return this.Order() ? this.Order().Id() : data.OrderId;
+ }, this);
+ this.Extra = ko.observable("extra");
+ };
+ var Order = function (data) {
+ this.Id = ko.observable(data.Id);
+ this.Name = ko.observable(data.Name);
+ this.OrderDetails = ko.observableArray(ko.utils.arrayMap(data.OrderDetails, function (od) { return new OrderDetail(od); }));
+ this.Extra = ko.observable("extra");
+ };
+
+ var orders = [],
+ order = {
+ Id: 1,
+ Name: "Order 1",
+ OrderDetails: []
+ };
+ orders.push(new Order(order));
+ orders.push(new Order({
+ Id: 2,
+ Name: "Order 2",
+ OrderDetails: []
+ }));
+ var orderDetails = [];
+ orderDetails.push(new OrderDetail({
+ Id: 1,
+ Name: "Order Detail 1",
+ OrderId: order.Id
+ }));
+ orderDetails.push(new OrderDetail({
+ Id: 2,
+ Name: "Order Detail 2",
+ OrderId: order.Id
+ }));
+ orders[0].OrderDetails(orderDetails);
+
+ // mock out enough metadata to do the attach
+ upshot.metadata("Order", { key: ["Id"], fields: { Id: { type: "Int32:#System" }, Name: { type: "String:#System" }, OrderDetails: { type: "OrderDetail", association: { Name: "O_OD", isForeignKey: false, thisKey: ["Id"], otherKey: ["OrderId"] }, array: true } } });
+ upshot.metadata("OrderDetail", { key: ["Id"], fields: { Id: { type: "Int32:#System" }, Name: { type: "String:#System" }, Order: { type: "Order", association: { Name: "O_OD", isForeignKey: true, thisKey: ["OrderId"], otherKey: ["Id"]} }, OrderId: { type: "Int32:#System"}} });
+
+ dc.merge(orders, "Order", null);
+
+ return dc;
+ }
+})(upshot, ko);
diff --git a/test/SPA.Test/upshot/Consistency.tests.js b/test/SPA.Test/upshot/Consistency.tests.js
new file mode 100644
index 00000000..38a2c249
--- /dev/null
+++ b/test/SPA.Test/upshot/Consistency.tests.js
@@ -0,0 +1,158 @@
+/// <reference path="../Scripts/References.js" />
+(function (global, upshot, undefined) {
+
+ module("Consistency.tests.js");
+
+ function getEntitySet() {
+ upshot.metadata("Employee", {
+ key: ["Id"],
+ fields: {
+ Name: {
+ type: "String:#System"
+ },
+ Manager: {
+ type: "Employee",
+ association: {
+ name: "Employee_Employee",
+ thisKey: ["ManagerId"],
+ otherKey: ["Id"],
+ isForeignKey: true
+ }
+ },
+ Reports: {
+ type: "Employee",
+ array: true,
+ association: {
+ name: "Employee_Employee2",
+ thisKey: ["Id"],
+ otherKey: ["ManagerId"],
+ isForeignKey: false
+ }
+ },
+ Id: {
+ type: "Int32:#System"
+ },
+ ManagerId: {
+ type: "Int32:#System"
+ }
+ }
+ });
+
+ var context = new upshot.DataContext();
+ context.merge([
+ { Id:1, Name: "Fred", ManagerId: 2 },
+ { Id:2, Name: "Bob" },
+ { Id:3, Name: "Jane" }
+ ], "Employee");
+
+ return context.getEntitySet("Employee");
+ }
+
+ test("AssociatedEntitiesView wrt FK update and revert", 6, function () {
+ var entitySet = getEntitySet(),
+ entities = entitySet.getEntities();
+
+ ok(entities[1].Reports.length === 1, "Bob should have Fred as a report");
+ ok(entities[2].Reports.length === 0, "Jane has no reports");
+
+ $.observable(entities[0]).property("ManagerId", 3);
+
+ ok(entities[1].Reports.length === 0, "Bob should have no reports");
+ ok(entities[2].Reports.length === 1, "Jane should have Fred as a report");
+
+ entitySet.revertChanges();
+
+ ok(entities[1].Reports.length === 1, "Bob should have Fred as a report");
+ ok(entities[2].Reports.length === 0, "Jane has no reports");
+ });
+
+ test("AssociatedEntitiesView wrt insert and revert", 3, function () {
+ var entitySet = getEntitySet(),
+ entities = entitySet.getEntities();
+
+ ok(entities[1].Reports.length === 1, "Bob should have Fred as a report");
+
+ $.observable(entities[1].Reports).insert(entities[2]);
+
+ ok(entities[1].Reports.length === 2, "Bob should have Fred and Jane as reports");
+
+ entitySet.revertChanges();
+
+ ok(entities[1].Reports.length === 1, "Bob should have Fred as a report");
+ });
+
+ test("LocalDataSource auto-refresh over AssociatedEntitiesView", 9, function () {
+ stop();
+
+ var entitySet = getEntitySet(),
+ entities = entitySet.getEntities();
+
+ ok(entities[1].Reports.length === 1, "Bob should have Fred as a report");
+ ok(entities[2].Reports.length === 0, "Jane has no reports");
+
+ var localDataSource = new upshot.LocalDataSource({
+ source: entities[1].Reports,
+ autoRefresh: true,
+ filter: { property: "Name", operator: "!=", value: "Joan" }
+ });
+ localDataSource.refresh(function () {
+ ok(localDataSource.getEntities().length === 1, "Bob should have Fred as a report");
+
+ $.observable(entities[0]).property("ManagerId", 3);
+
+ ok(entities[1].Reports.length === 0, "Bob should have no reports");
+ ok(entities[2].Reports.length === 1, "Jane should have Fred as a report");
+ ok(localDataSource.getEntities().length === 0, "Bob should have no reports");
+
+ entitySet.revertChanges();
+
+ ok(entities[1].Reports.length === 1, "Bob should have Fred as a report");
+ ok(entities[2].Reports.length === 0, "Jane has no reports");
+ ok(localDataSource.getEntities().length === 1, "Bob should have Fred as a report");
+
+ start();
+ });
+ });
+
+ test("LocalDataSource auto-refresh over EntitySet wrt property updates and revert", 3, function () {
+ stop();
+
+ var entitySet = getEntitySet();
+
+ var localDataSource = new upshot.LocalDataSource({
+ source: entitySet,
+ autoRefresh: true,
+ filter: { property: "Name", value: "Fred" }
+ });
+ localDataSource.refresh(function () {
+ ok(localDataSource.getEntities().length === 1, "We have an employee named Fred");
+
+ $.observable(localDataSource.getEntities()[0]).property("Name", "Fredrick");
+
+ ok(localDataSource.getEntities().length === 0, "We have no employees named Fred");
+
+ entitySet.revertChanges();
+
+ ok(localDataSource.getEntities().length === 1, "We have an employee named Fred");
+
+ start();
+ });
+ });
+
+ test("EntitySet wrt insert and revert", 4, function () {
+ var entitySet = getEntitySet();
+
+ ok(entitySet.getEntities().length === 3, "Have 3 entities");
+
+ var newEmployee = { Name: "Barb" };
+ $.observable(entitySet.getEntities()).insert(newEmployee);
+
+ ok(entitySet.getEntities().length === 4, "Now 4 entities");
+
+ entitySet.revertChanges();
+
+ ok(entitySet.getEntities().length === 3, "Have 3 entities");
+ ok((entitySet.getEntityState(newEmployee) || upshot.EntityState.Deleted) === upshot.EntityState.Deleted);
+ });
+
+})(this, upshot);
diff --git a/test/SPA.Test/upshot/Core.tests.js b/test/SPA.Test/upshot/Core.tests.js
new file mode 100644
index 00000000..b1d7153c
--- /dev/null
+++ b/test/SPA.Test/upshot/Core.tests.js
@@ -0,0 +1,293 @@
+
+(function(upshot) {
+
+ module("Core.js");
+
+ var Custom = upshot.defineClass(null);
+
+ test("classof utility test", 50, function () {
+ var testCases = [
+ [ true, "boolean"],
+ [ null, "null"],
+ [ 1, "number"],
+ [ [], "array"],
+ [ "s", "string"],
+ [ {}, "object"],
+ [ new Custom(), "object" ],
+ [ Boolean(true), "boolean" ],
+ [ new Boolean(true), "boolean" ],
+ [ 1, "number" ],
+ [ 1.0, "number" ],
+ [ Number(1), "number" ],
+ [ new Number(1), "number" ],
+ [ Number.MAX_VALUE, "number" ],
+ [ Number.MIN_VALUE, "number" ],
+ [ Number.NaN, "number" ],
+ [ Number.NEGATIVE_INFINITY, "number" ],
+ [ Number.POSITIVE_INFINITY, "number" ],
+ [ "A", "string" ],
+ [ String("A"), "string" ],
+ [ new String("A"), "string" ],
+ [ new Date(), "date" ],
+ [ undefined, "undefined" ],
+ [ function () {}, "function" ],
+ [ /./, "regexp" ]
+ ];
+
+ for(var i = 0; i < testCases.length; i++) {
+ // verify our classof function
+ var testPair = testCases[i];
+ var result = upshot.classof(testPair[0]);
+ equal(result, testPair[1]);
+
+ // verify that we produce the same results as the jQuery function
+ var jQueryResult = $.type(testPair[0]);
+ equal(result, jQueryResult);
+ }
+ });
+
+ test("isArray utility test", 2, function () {
+ equal(upshot.isArray([]), true);
+ equal(upshot.isArray(5), false);
+ });
+
+ test("HelloWorldNs test", 2, function () {
+ equal(typeof upshot.defineNamespace, "function", "pre test");
+ upshot.defineNamespace("HelloWorldNs");
+ equal(typeof HelloWorldNs, "object", "HelloWorldNs is defined");
+ });
+
+ test("HelloWorldCls simple test", 4, function () {
+ var HelloWorldCls = upshot.defineClass(
+ function(result) {
+ this.result = result;
+ },
+ {
+ add: function(value) {
+ this.result += value;
+ },
+ subtract: function(value) {
+ this.result -= value;
+ }
+ }
+ );
+ equal(typeof HelloWorldCls, "function", "HelloWorldCls is defined");
+ var val = 5;
+ var calc = new HelloWorldCls(val);
+ equal(calc.result, val, "precheck result");
+ val += 3;
+ calc.add(3);
+ equal(calc.result, val, "add check");
+ val -= 4;
+ calc.subtract(4);
+ equal(calc.result, val, "subtract check");
+ });
+
+ test("HelloWorldCls inherit test: prototype inheritance", 2, function () {
+ var result;
+ var HelloWorldBase = upshot.defineClass(
+ function() {
+ this.id = 1;
+ },
+ {
+ foo: function(val) {
+ result += this.id + ".HelloWorldBase.foo(" + val + ")";
+ }
+ }
+ );
+ var HelloWorldCls = upshot.deriveClass(
+ HelloWorldBase.prototype,
+ function() {
+ this.id = 2;
+ },
+ {
+ bar: function(val) {
+ result += this.id + ".HelloWorldCls.bar(" + val + ")";
+ }
+ }
+ );
+
+ var tmp = new HelloWorldCls();
+ result = "";
+ tmp.foo("hi");
+ equal(result, "2.HelloWorldBase.foo(hi)", "test foo");
+ result = "";
+ tmp.bar("hey");
+ equal(result, "2.HelloWorldCls.bar(hey)", "test bar");
+ });
+
+ test("HelloWorldCls inherit test: override ctor", 1, function () {
+ var result;
+ var HelloWorldBase = upshot.defineClass(
+ function(val) {
+ result += "HelloWorldBase.ctor(" + val + ")";
+ }
+ );
+ var base = HelloWorldBase.prototype,
+ HelloWorldCls = upshot.deriveClass(
+ base,
+ function(val) {
+ result += "HelloWorldCls.ctor(" + val + ")";
+ base.constructor.call(this, val);
+ }
+ );
+
+ result = "";
+ var tmp = new HelloWorldCls("hey");
+ equal(result, "HelloWorldCls.ctor(hey)HelloWorldBase.ctor(hey)", "test ctor");
+ });
+
+ test("HelloWorldCls inherit test: override method", 1, function () {
+ var result;
+ var HelloWorldBase = upshot.defineClass(
+ function() {
+ this.id = 1;
+ },
+ {
+ foo: function(val) {
+ result += this.id + ".HelloWorldBase.foo(" + val + ")";
+ }
+ }
+ );
+ var base = HelloWorldBase.prototype,
+ HelloWorldCls = upshot.deriveClass(
+ base,
+ function() {
+ this.id = 2;
+ },
+ {
+ foo: function(val) {
+ result += this.id + ".HelloWorldCls.foo(" + val + ")";
+ base.foo.call(this, val);
+ }
+ }
+ );
+
+ var tmp = new HelloWorldCls();
+ result = "";
+ tmp.foo("hey");
+ equal(result, "2.HelloWorldCls.foo(hey)2.HelloWorldBase.foo(hey)", "test foo");
+ });
+
+ test("HelloWorldCls inherit test: multi prototype inheritance", 3, function () {
+ var result;
+ var HelloWorldBase = upshot.defineClass(
+ function() {
+ this.id = 1;
+ },
+ {
+ foo: function(val) {
+ result += this.id + ".HelloWorldBase.foo(" + val + ")";
+ }
+ }
+ );
+ var HelloWorldInt = upshot.deriveClass(
+ HelloWorldBase.prototype,
+ function() {
+ this.id = 2;
+ },
+ {
+ fred: function(val) {
+ result += this.id + ".HelloWorldInt.fred(" + val + ")";
+ }
+ }
+ );
+ var HelloWorldCls = upshot.deriveClass(
+ HelloWorldInt.prototype,
+ function() {
+ this.id = 3;
+ },
+ {
+ bar: function(val) {
+ result += this.id + ".HelloWorldCls.bar(" + val + ")";
+ }
+ }
+ );
+
+ var tmp = new HelloWorldCls();
+ result = "";
+ tmp.foo("hi");
+ equal(result, "3.HelloWorldBase.foo(hi)", "test foo");
+ result = "";
+ tmp.fred("huh");
+ equal(result, "3.HelloWorldInt.fred(huh)", "test fred");
+ result = "";
+ tmp.bar("hey");
+ equal(result, "3.HelloWorldCls.bar(hey)", "test bar");
+ });
+
+ test("HelloWorldCls inherit test: multi override ctor", 1, function () {
+ var result;
+ var HelloWorldBase = upshot.defineClass(
+ function(val) {
+ result += "HelloWorldBase.ctor(" + val + ")";
+ }
+ );
+ var base = HelloWorldBase.prototype,
+ HelloWorldInt = upshot.deriveClass(
+ base,
+ function(val) {
+ result += "HelloWorldInt.ctor(" + val + ")";
+ base.constructor.call(this, val);
+ }
+ );
+ var baseInt = HelloWorldInt.prototype,
+ HelloWorldCls = upshot.deriveClass(
+ baseInt,
+ function(val) {
+ result += "HelloWorldCls.ctor(" + val + ")";
+ baseInt.constructor.call(this, val);
+ }
+ );
+
+ result = "";
+ var tmp = new HelloWorldCls("hey");
+ equal(result, "HelloWorldCls.ctor(hey)HelloWorldInt.ctor(hey)HelloWorldBase.ctor(hey)", "test ctor");
+ });
+
+ test("HelloWorldCls inherit test: multi override method", 1, function () {
+ var result;
+ var HelloWorldBase = upshot.defineClass(
+ function() {
+ this.id = 1;
+ },
+ {
+ foo: function(val) {
+ result += this.id + ".HelloWorldBase.foo(" + val + ")";
+ }
+ }
+ );
+ var base = HelloWorldBase.prototype,
+ HelloWorldInt = upshot.deriveClass(
+ base,
+ function() {
+ this.id = 2;
+ },
+ {
+ foo: function(val) {
+ result += this.id + ".HelloWorldInt.foo(" + val + ")";
+ base.foo.call(this, val);
+ }
+ }
+ );
+ var baseInt = HelloWorldInt.prototype,
+ HelloWorldCls = upshot.deriveClass(
+ baseInt,
+ function() {
+ this.id = 3;
+ },
+ {
+ foo: function(val) {
+ result += this.id + ".HelloWorldCls.foo(" + val + ")";
+ baseInt.foo.call(this, val);
+ }
+ }
+ );
+
+ var tmp = new HelloWorldCls();
+ result = "";
+ tmp.foo("hey");
+ equal(result, "3.HelloWorldCls.foo(hey)3.HelloWorldInt.foo(hey)3.HelloWorldBase.foo(hey)", "test foo");
+ });
+
+})(upshot); \ No newline at end of file
diff --git a/test/SPA.Test/upshot/DataContext.tests.js b/test/SPA.Test/upshot/DataContext.tests.js
new file mode 100644
index 00000000..e816f8ad
--- /dev/null
+++ b/test/SPA.Test/upshot/DataContext.tests.js
@@ -0,0 +1,7 @@
+/// <reference path="../Scripts/References.js" />
+
+(function (upshot) {
+
+ module("DataContext tests");
+
+})(upshot); \ No newline at end of file
diff --git a/test/SPA.Test/upshot/DataProvider.tests.js b/test/SPA.Test/upshot/DataProvider.tests.js
new file mode 100644
index 00000000..12762f51
--- /dev/null
+++ b/test/SPA.Test/upshot/DataProvider.tests.js
@@ -0,0 +1,156 @@
+/// <reference path="../Scripts/References.js" />
+(function (global, upshot, undefined) {
+
+ module("DataProvider tests");
+
+ function getTestProvider(verifyGet, verifySubmit) {
+ return {
+ get: function (getParameters, queryParameters, success, error) {
+ var queryResult = {
+ entities: [{ ID: 1, Name: "Mathew" }, { ID: 2, Name: "Amy"}],
+ totalCount: 2
+ };
+ if (verifyGet) {
+ verifyGet(getParameters, queryParameters);
+ }
+ success(queryResult);
+ },
+ submit: function (submitParameters, changeSet, success, error) {
+ if (verifySubmit) {
+ verifySubmit(submitParameters, changeSet);
+ }
+ success(changeSet);
+ }
+ };
+ }
+
+ function getTestContext() {
+ var dc = new upshot.DataContext(getTestProvider());
+ upshot.metadata(getTestMetadata());
+ return dc;
+ }
+
+ function getTestMetadata() {
+ return { Contact: { key: ["ID"] } };
+ }
+
+ // Verify an offline type scenario where a DataProvider is used directly and results
+ // are "offlined" and rehydrated back into the context (metadata and entities)
+ test("Offline scenario", 4, function () {
+ // execute a direct query using the dataprovider and simulate
+ // caching of the results
+ var provider = getTestProvider();
+ var cachedEntities;
+ provider.get({ operationName: "contacts" }, null, function (result) {
+ cachedEntities = result.entities;
+ });
+ equal(2, cachedEntities.length);
+
+ // create a new context and load the cached entities into it
+ var dataContext = getTestContext();
+ var mergedEntities = dataContext.merge(cachedEntities, "Contact", null);
+
+ // verify that after merge, entities are annotated with their type
+ var entitySet = dataContext.getEntitySet("Contact");
+ var contact = entitySet.getEntities()[0];
+ equal(contact.__type, "Contact");
+
+ // verify that the "rehydrated" context is fully functional
+ equal(entitySet.getEntityState(contact), upshot.EntityState.Unmodified);
+ $.observable(contact).property("Name", "xyz");
+ equal(entitySet.getEntityState(contact), upshot.EntityState.ClientUpdated);
+ });
+
+ test("Custom data provider", 4, function () {
+ var verifySubmit = function (submitParameters, changeSet) {
+ // verify we got the expected changeset
+ equal(changeSet.length, 1);
+ equal(changeSet[0].Entity.Name, "foo");
+ };
+ var provider = getTestProvider(null, verifySubmit);
+
+ // create a datasource using the provider and verify an E2E query + update cycle
+ var ds = upshot.RemoteDataSource({
+ providerParameters: { operationName: "contacts" },
+ entityType: "Contact",
+ provider: provider,
+ bufferChanges: true,
+ refreshSuccess: function (entities) {
+ equal(entities.length, 2);
+
+ // modify an entity
+ $.observable(entities[0]).property("Name", "foo");
+
+ ds.commitChanges(function () {
+ ok(true);
+ });
+ }
+ });
+ upshot.metadata(getTestMetadata());
+ ds.refresh();
+ });
+
+ test("Verify get/submit parameter handling", 15, function () {
+ var verifySubmit = function (submitParameters, changeSet) {
+ // verify "outer params" are pushed in
+ equal(submitParameters.outerA, "outerA");
+ equal(submitParameters.outerB, "outerB");
+
+ // verify submit only params
+ equal(submitParameters.submitA, "submitA");
+ equal(submitParameters.submitB, "submitB");
+
+ equal(submitParameters.getA, undefined);
+ equal(submitParameters.getB, undefined);
+ };
+ var verifyGet = function (getParameters) {
+ // verify "outer params" are pushed in
+ equal(getParameters.outerA, "outerA");
+ equal(getParameters.outerB, "outerB");
+
+ // verify get only params
+ equal(getParameters.getA, "getA");
+ equal(getParameters.getB, "getB");
+
+ equal(getParameters.submitA, undefined);
+ equal(getParameters.submitB, undefined);
+
+ // verify original provider parameter objects weren't modified
+ equal(providerParameters.get.url, undefined);
+ };
+ var provider = getTestProvider(verifyGet, verifySubmit);
+
+ var providerParameters = {
+ outerA: "outerA",
+ outerB: "outerB",
+ get: {
+ getA: "getA",
+ getB: "getB"
+ },
+ submit: {
+ submitA: "submitA",
+ submitB: "submitB"
+ }
+ };
+
+ var ds = upshot.RemoteDataSource({
+ providerParameters: providerParameters,
+ entityType: "Contact",
+ provider: provider,
+ bufferChanges: true,
+ refreshSuccess: function (entities) {
+ equal(entities.length, 2);
+
+ // modify an entity
+ $.observable(entities[0]).property("Name", "foo");
+
+ ds.commitChanges(function () {
+ ok(true);
+ });
+ }
+ });
+ upshot.metadata(getTestMetadata());
+ ds.refresh();
+ });
+
+})(this, upshot); \ No newline at end of file
diff --git a/test/SPA.Test/upshot/DataSource.Common.js b/test/SPA.Test/upshot/DataSource.Common.js
new file mode 100644
index 00000000..acf04282
--- /dev/null
+++ b/test/SPA.Test/upshot/DataSource.Common.js
@@ -0,0 +1,161 @@
+/// <reference path="../Scripts/References.js" />
+
+
+var dsTestDriver;
+
+(function (global, upshot, undefined) {
+
+ if ($.isPlainObject(dsTestDriver)) {
+ return;
+ }
+
+ function createProductsResult () {
+ return {
+ GetProductsResult: {
+ TotalCount: 3,
+ RootResults: [
+ { ID: 1, Manufacturer: "Canon", Price: 200 },
+ { ID: 2, Manufacturer: "Nikon", Price: 400 },
+ { ID: 3, Manufacturer: "Pentax", Price: 500 }
+ ],
+ Metadata: [
+ {
+ type: "Product:#Sample.Models",
+ key: ["ID"],
+ fields: {
+ ID: { type: "Int32:#System" },
+ Manufacturer: { type: "String:#System" },
+ Price: { type: "Decimal:#System" }
+ },
+ rules: {
+ ID: { required: true },
+ Price: { range: [0, 1000] }
+ }
+ }
+ ]
+ }
+ };
+ }
+
+ dsTestDriver = {
+ ds: null,
+ simulatedSuccess: true,
+ errorStatus: "ErrorStatus",
+ errorValue: "ErrorValue",
+ validationError: {
+ SubmitChangesResult: [{
+ ValidationErrors: [{
+ Message: "The ID field is required!"
+ }]
+ }]
+ },
+
+ simulateSuccessService: function (results) {
+ testHelper.mockAjaxOnce("unused", this.productsResult = results || createProductsResult());
+ this.simulatedSuccess = true;
+ },
+ simulatePostSuccessService: function (results) {
+ testHelper.mockAjaxOnce("unused", results);
+ this.simulatedSuccess = true;
+ },
+ simulateErrorService: function () {
+ testHelper.mockAjaxOnce("unused", null, this.errorStatus, this.errorValue);
+ this.simulatedSuccess = false;
+ },
+ simulateValidationErrorService: function () {
+ testHelper.mockAjaxOnce("unused", this.validationError);
+ this.simulatedSuccess = false;
+ },
+
+ onRefreshStartEvent: function (event) {
+ equal(event.type, "refreshStart", "Event triggered");
+ var args = Array.prototype.slice.call(arguments);
+ args.shift();
+ dsTestDriver.onRefreshStart.apply(this, args);
+ },
+ onRefreshStart: function () {
+ ok(true, "Callback called");
+ equal(arguments.length, 0, "Argument checked");
+ ok(this === dsTestDriver.ds, "Context checked");
+ },
+ onRefreshSuccessEvent: function (event, entities, totalCount) {
+ equal(event.type, "refreshSuccess", "Event triggered");
+ var args = Array.prototype.slice.call(arguments);
+ args.shift();
+ dsTestDriver.onRefreshSuccess.apply(this, args);
+ },
+ onRefreshSuccess: function (entities, totalCount) {
+ ok(true, "Callback called");
+ ok(dsTestDriver.simulatedSuccess, "Simulation checked");
+ equal(arguments.length, 2, "Argument checked");
+ ok(this === dsTestDriver.ds, "Context checked");
+ equal(totalCount, dsTestDriver.productsResult.GetProductsResult.TotalCount, "Count checked");
+ var lastIndex = dsTestDriver.ds._take || (dsTestDriver.ds.dataSource && dsTestDriver.ds.dataSource._take) || totalCount;
+ equal(entities[lastIndex - 1].Price, dsTestDriver.productsResult.GetProductsResult.RootResults[lastIndex - 1].Price, "Price checked");
+ start();
+ },
+ onRefreshErrorEvent: function (event) {
+ equal(event.type, "refreshError", "Event triggered");
+ var args = Array.prototype.slice.call(arguments);
+ args.shift();
+ dsTestDriver.onRefreshError.apply(this, args);
+ },
+ onRefreshError: function (httpStatus, errorText, jqXHR) {
+ ok(true, "Callback called");
+ ok(!dsTestDriver.simulatedSuccess, "Simulation checked");
+ equal(arguments.length, 3, "Argument checked");
+ ok(this === dsTestDriver.ds, "Context checked");
+ equal(errorText, dsTestDriver.errorValue, "error checked");
+ start();
+ },
+ onCommitStartEvent: function (event) {
+ equal(event.type, "commitStart", "Event triggered");
+ var args = Array.prototype.slice.call(arguments);
+ args.shift();
+ dsTestDriver.onCommitStart.apply(this, args);
+ },
+ onCommitStart: function () {
+ ok(true, "Callback called");
+ equal(arguments.length, 0, "Argument checked");
+ ok(this === dsTestDriver.ds, "Context checked");
+ },
+ onCommitSuccessEvent: function (event) {
+ equal(event.type, "commitSuccess", "Event triggered");
+ var args = Array.prototype.slice.call(arguments);
+ args.shift();
+ dsTestDriver.onCommitSuccess.apply(this, args);
+ },
+ onCommitSuccess: function () {
+ ok(true, "Callback called");
+ ok(dsTestDriver.simulatedSuccess, "Simulation checked");
+ equal(arguments.length, 1, "Argument checked");
+ var submitResults = arguments[0];
+ equal(submitResults.length, 1, "submitResults checked");
+ ok(this === dsTestDriver.ds, "Context checked");
+ start();
+ },
+ onCommitErrorEvent: function (event) {
+ equal(event.type, "commitError", "Event triggered");
+ var args = Array.prototype.slice.call(arguments);
+ args.shift();
+ dsTestDriver.onCommitError.apply(this, args);
+ },
+ onCommitError: function (httpStatus, errorText, jqXHR, submitResult) {
+ ok(true, "Callback called");
+ ok(!dsTestDriver.simulatedSuccess, "Simulation checked");
+ equal(arguments.length, 4, "Argument checked");
+ ok(this === dsTestDriver.ds, "Context checked");
+ equal(errorText, dsTestDriver.errorValue, "errorText checked");
+ start();
+ },
+ onCommitValidationError: function (httpStatus, errorText, jqXHR, submitResult) {
+ ok(true, "Callback called");
+ ok(!dsTestDriver.simulatedSuccess, "Simulation checked");
+ equal(arguments.length, 4, "Argument checked");
+ ok(this === dsTestDriver.ds, "Context checked");
+ equal(errorText, dsTestDriver.validationError.SubmitChangesResult[0].ValidationErrors[0].Message, "Validation text checked");
+ start();
+ }
+ };
+
+})(this, upshot);
diff --git a/test/SPA.Test/upshot/DataSource.Tests.js b/test/SPA.Test/upshot/DataSource.Tests.js
new file mode 100644
index 00000000..b0fc0735
--- /dev/null
+++ b/test/SPA.Test/upshot/DataSource.Tests.js
@@ -0,0 +1,349 @@
+/// <reference path="../Scripts/References.js" />
+(function (global, upshot, undefined) {
+
+ module("DataSource.tests.js", {
+ teardown: function () {
+ testHelper.unmockAjax();
+ }
+ });
+
+ function createRemoteDataSource(options) {
+ options = $.extend({ providerParameters: { url: "unused", operationName: "" }, provider: upshot.riaDataProvider }, options || {});
+ return new upshot.RemoteDataSource(options);
+ }
+
+ function createTestDataContext() {
+ return new upshot.DataContext(new upshot.riaDataProvider());
+ }
+
+ // refreshStart
+ test("refreshStart RemoteDataSource", 3, function () {
+ stop();
+ dsTestDriver.simulateSuccessService();
+ dsTestDriver.ds = upshot.RemoteDataSource({ providerParameters: { url: "unused", operationName: "" }, provider: upshot.riaDataProvider });
+ dsTestDriver.ds.bind("refreshStart", dsTestDriver.onRefreshStart);
+ dsTestDriver.ds.refresh(function () { start(); });
+ });
+
+ test("refreshStart observer LocalDataSource over RemoteDataSource", 3, function () {
+ stop();
+ dsTestDriver.simulateSuccessService();
+ dsTestDriver.ds = upshot.LocalDataSource({ source: createRemoteDataSource() });
+ dsTestDriver.ds.bind({
+ refreshStart: dsTestDriver.onRefreshStart,
+ refreshSuccess: function () { start(); }
+ });
+ dsTestDriver.ds.refresh({ all: true });
+ });
+
+ test("refreshStart observer LocalDataSource over EntitySet", 3, function () {
+ stop();
+ dsTestDriver.simulateSuccessService();
+ var dataContext = upshot.DataContext(new upshot.riaDataProvider());
+ dataContext.__load({ providerParameters: { url: "unused", operationName: ""} }, function (entitySet) {
+ dsTestDriver.ds = new upshot.LocalDataSource({ source: entitySet });
+ dsTestDriver.ds.bind("refreshStart", dsTestDriver.onRefreshStart);
+ dsTestDriver.ds.bind("refreshSuccess", function () { start(); });
+ dsTestDriver.ds.refresh();
+ });
+ });
+
+ // refreshSuccess
+ test("refreshSuccess RemoteDataSource", 6, function () {
+ stop();
+ dsTestDriver.simulateSuccessService();
+ dsTestDriver.ds = createRemoteDataSource();
+ dsTestDriver.ds.refresh(dsTestDriver.onRefreshSuccess, dsTestDriver.onRefreshError);
+ });
+
+ test("refreshSuccess observer RemoteDataSource", 6, function () {
+ stop();
+ dsTestDriver.simulateSuccessService();
+ dsTestDriver.ds = createRemoteDataSource();
+ dsTestDriver.ds.bind("refreshSuccess", dsTestDriver.onRefreshSuccess);
+ dsTestDriver.ds.bind("refreshError", dsTestDriver.onRefreshError);
+ dsTestDriver.ds.refresh();
+ });
+
+ test("refreshSuccess LocalDataSource over RemoteDataSource", 6, function () {
+ stop();
+ dsTestDriver.simulateSuccessService();
+ dsTestDriver.ds = new upshot.LocalDataSource({ source: createRemoteDataSource() });
+ dsTestDriver.ds.refresh({ all: true }, dsTestDriver.onRefreshSuccess, dsTestDriver.onRefreshError);
+ });
+
+ test("refreshSuccess observer LocalDataSource over RemoteDataSource", 6, function () {
+ stop();
+ dsTestDriver.simulateSuccessService();
+ dsTestDriver.ds = new upshot.LocalDataSource({ source: createRemoteDataSource() })
+ .bind("refreshSuccess", dsTestDriver.onRefreshSuccess)
+ .bind("refreshError", dsTestDriver.onRefreshError);
+ dsTestDriver.ds.refresh({ all: true });
+ });
+
+ test("refreshSuccess LocalDataSource over EntitySet", 6, function () {
+ stop();
+ dsTestDriver.simulateSuccessService();
+ var dataContext = createTestDataContext();
+ dataContext.__load({ providerParameters: { url: "unused"} }, function (entitySet) {
+ dsTestDriver.ds = new upshot.LocalDataSource({ source: entitySet });
+ dsTestDriver.ds.refresh(dsTestDriver.onRefreshSuccess, dsTestDriver.onRefreshError);
+ });
+ });
+
+ test("refreshSuccess observer LocalDataSource over EntitySet", 6, function () {
+ stop();
+ dsTestDriver.simulateSuccessService();
+ var dataContext = createTestDataContext();
+ dataContext.__load({ providerParameters: { url: "unused"} }, function (entitySet) {
+ dsTestDriver.ds = new upshot.LocalDataSource({ source: entitySet })
+ .bind({
+ refreshSuccess: dsTestDriver.onRefreshSuccess,
+ refreshError: dsTestDriver.onRefreshError
+ });
+ dsTestDriver.ds.refresh();
+ });
+ });
+
+ // refreshError
+ test("refreshError RemoteDataSource", 5, function () {
+ stop();
+ dsTestDriver.simulateErrorService();
+ dsTestDriver.ds = createRemoteDataSource();
+ dsTestDriver.ds.refresh(dsTestDriver.onRefreshSuccess, dsTestDriver.onRefreshError);
+ });
+
+ test("refreshError observer RemoteDataSource", 5, function () {
+ stop();
+ dsTestDriver.simulateErrorService();
+ dsTestDriver.ds = createRemoteDataSource();
+ dsTestDriver.ds.bind("refreshSuccess", dsTestDriver.onRefreshSuccess);
+ dsTestDriver.ds.bind("refreshError", dsTestDriver.onRefreshError);
+ dsTestDriver.ds.refresh();
+ });
+
+ test("refreshError LocalDataSource over RemoteDataSource", 5, function () {
+ stop();
+ dsTestDriver.simulateErrorService();
+ dsTestDriver.ds = new upshot.LocalDataSource({ source: createRemoteDataSource() });
+ dsTestDriver.ds.refresh({ all: true }, dsTestDriver.onRefreshSuccess, dsTestDriver.onRefreshError);
+ });
+
+ test("refreshError observer LocalDataSource over RemoteDataSource", 5, function () {
+ stop();
+ dsTestDriver.simulateErrorService();
+ dsTestDriver.ds = new upshot.LocalDataSource({ source: createRemoteDataSource() });
+ dsTestDriver.ds.bind("refreshSuccess", dsTestDriver.onRefreshSuccess);
+ dsTestDriver.ds.bind("refreshError", dsTestDriver.onRefreshError);
+ dsTestDriver.ds.refresh({ all: true });
+ });
+
+ // entityChanged
+ asyncTest("entityChanged event is raised on a successful update", 12, function () {
+ var listener = {
+ events: [],
+ onEntityUpdated: function (entity, path, eventArgs) {
+ listener.events.push({ entity: entity, path: path, eventArgs: eventArgs });
+ }
+ };
+ dsTestDriver.ds = new upshot.RemoteDataSource({ providerParameters: { url: "unused", operationName: "" }, provider: upshot.riaDataProvider, entityType: "Product:#Sample.Models", bufferChanges: true });
+ dsTestDriver.ds.bind("entityUpdated", listener.onEntityUpdated);
+
+ dsTestDriver.simulateSuccessService();
+ dsTestDriver.ds.refresh(function (entities) {
+ var entity = entities[0];
+
+ equal(dsTestDriver.ds.isUpdated(entity), false, "The entity should not have changes");
+ equal(listener.events.length, 0, "There should have been one event");
+
+ $.observable(entity).property("Price", 700);
+
+ equal(dsTestDriver.ds.isUpdated(entity), true, "The entity should have changes");
+ equal(listener.events.length, 1, "There should have been one event");
+ equal(listener.events[0].entity, entity, "The event should have been for the entity");
+ equal(listener.events[0].path, "", "The event should have been directly on the entity");
+ equal(listener.events[0].eventArgs.newValues.Price, 700, "The event should have been a property change");
+
+ listener.events.length = 0;
+
+ dsTestDriver.simulatePostSuccessService();
+ dsTestDriver.ds.commitChanges(function () {
+ equal(dsTestDriver.ds.isUpdated(entity), false, "The entity should no longer have changes");
+ equal(listener.events.length, 1, "There should have been one event");
+ equal(listener.events[0].entity, entity, "The event should have been for the entity");
+ equal(listener.events[0].path, undefined, "The event should not include a path");
+ equal(listener.events[0].eventArgs, undefined, "The event should not include args");
+ start();
+ });
+ });
+ });
+
+ test("insert without initial refresh over RemoteDataSource", 5, function () {
+ stop();
+
+ dsTestDriver.ds = new upshot.RemoteDataSource({
+ providerParameters: { url: "unused" },
+ provider: upshot.riaDataProvider,
+ entityType: "Foo"
+ });
+ dsTestDriver.ds.bind("commitSuccess", dsTestDriver.onCommitSuccess);
+ upshot.metadata("Foo", { key: ["FooId"] });
+
+ dsTestDriver.simulatePostSuccessService();
+ $.observable(dsTestDriver.ds.getEntities()).insert({ FooId: 1 });
+ });
+
+ test("revertChanges without initial refresh over RemoteDataSource", 2, function () {
+ stop();
+
+ var entities = [],
+ ds = new upshot.RemoteDataSource({
+ entityType: "Foo",
+ result: entities,
+ bufferChanges: true,
+ provider: upshot.riaDataProvider
+ });
+ upshot.metadata("Foo", { key: ["FooId"] });
+
+ $.observable(entities).insert({});
+ equal(entities.length, 1);
+
+ ds.revertChanges();
+ equal(entities.length, 0);
+
+ start();
+ });
+
+ test("DataSource reset", 23, function () {
+ stop();
+ dsTestDriver.simulateSuccessService();
+
+ var rds = dsTestDriver.ds = createRemoteDataSource();
+ rds.refresh(function () {
+ stop();
+ dsTestDriver.onRefreshSuccess.apply(this, arguments);
+
+ var lds = upshot.LocalDataSource({ source: rds });
+ lds.refresh(function () {
+
+ // RDS and LDS should be loaded and in sync.
+ equal(lds.refreshNeeded(), false, "LocalDataSource should not need refresh");
+ ok(upshot.sameArrayContents(rds.getEntities(), lds.getEntities()), "LocalDataSource refreshed properly");
+
+ rds.reset();
+ // RDS should have been reset. LDS should need a refresh.
+ equal(rds.getEntities().length, 0, "RemoteDataSource has no entities");
+ equal(rds.getTotalEntityCount(), undefined, "RemoteDataSource has no total count");
+ equal(lds.refreshNeeded(), true, "LocalDataSource should need a refresh");
+
+ lds.reset();
+ // LDS should have been reset.
+ equal(lds.getEntities().length, 0, "LocalDataSource has no entities");
+ equal(lds.getTotalEntityCount(), undefined, "LocalDataSource has no total count");
+
+ // Put the RDS back in its loaded state.
+ dsTestDriver.simulateSuccessService();
+ rds.refresh(function () {
+ stop();
+ dsTestDriver.onRefreshSuccess.apply(this, arguments);
+
+ // Put the LDS back in sync with the RDS.
+ equal(lds.refreshNeeded(), true, "LocalDataSource should need a refresh");
+ lds.refresh(function () {
+ ok(upshot.sameArrayContents(rds.getEntities(), lds.getEntities()), "LocalDataSource refreshed properly");
+
+ lds.reset();
+ // LDS should have been reset.
+ equal(lds.getEntities().length, 0, "LocalDataSource has no entities");
+ equal(lds.getTotalEntityCount(), undefined, "LocalDataSource has no total count");
+
+ start();
+ });
+ });
+ });
+ });
+ });
+
+ test("refresh with edits", 5, function () {
+ stop();
+
+ var rds = dsTestDriver.ds = createRemoteDataSource();
+
+ dsTestDriver.simulateSuccessService();
+ rds.refresh(function (entities) {
+ ok(entities.length === 3, "Successful initial refresh");
+
+ var entity = entities[0];
+ $.observable(entity).property("Manufacturer", "Foo");
+
+ var exception;
+ try {
+ rds.refresh();
+ } catch (ex) {
+ exception = true;
+ }
+ ok(!!exception, "Can't load with edits to DataSource entities");
+ rds.revertChanges();
+
+ setTimeout(function () {
+ var emptyProductsResult = {
+ GetProductsResult: {
+ TotalCount: 3,
+ RootResults: [],
+ Metadata: [ $.extend({}, { type: "Product:#Sample.Models" }, upshot.metadata()["Product:#Sample.Models"]) ]
+ }
+ };
+ dsTestDriver.simulateSuccessService(emptyProductsResult);
+ rds.refresh(function (entities) {
+ ok(entities.length === 0, "'Canon' isn't in filtered result");
+
+ $.observable(entity).property("Manufacturer", "Foo");
+
+ setTimeout(function () {
+ var exception;
+ try {
+ dsTestDriver.simulateSuccessService(emptyProductsResult);
+ rds.refresh(function(entities) {
+ ok(entities.length === 0, "Can load with edits not included in DataSource entities");
+ start();
+ });
+ } catch (ex) {
+ exception = true;
+ }
+ ok(!exception, "Can load with edits not included in DataSource entities");
+ }, 0);
+ });
+ }, 0);
+ });
+ });
+
+ test("refresh with edits and 'allowRefreshWithEdits' option", 3, function () {
+ stop();
+
+ var rds = dsTestDriver.ds = createRemoteDataSource({ allowRefreshWithEdits: true });
+
+ dsTestDriver.simulateSuccessService();
+ rds.refresh(function (entities) {
+ ok(entities.length === 3, "Successful initial refresh");
+
+ var entity = entities[0];
+ $.observable(entity).property("Manufacturer", "Foo");
+
+ setTimeout(function () {
+ var exception;
+ try {
+ dsTestDriver.simulateSuccessService();
+ rds.refresh(function (entities) {
+ ok(entities.length === 3, "Can load with edits to DataSource entities (using allowRefreshWithEdits)");
+ start();
+ });
+ } catch (ex) {
+ exception = true;
+ }
+
+ ok(!exception, "Can load with edits to DataSource entities (using allowRefreshWithEdits)");
+ }, 0);
+ });
+ });
+
+})(this, upshot);
diff --git a/test/SPA.Test/upshot/Datasets.js b/test/SPA.Test/upshot/Datasets.js
new file mode 100644
index 00000000..1ca63c47
--- /dev/null
+++ b/test/SPA.Test/upshot/Datasets.js
@@ -0,0 +1,257 @@
+/// <reference path="../Scripts/References.js" />
+
+(function (global, upshot, undefined) {
+
+ var datasets = {
+ primitives: {
+ create: function (id) {
+ return {
+ Id: id || 0,
+ B: true,
+ N: 1,
+ S: "A"
+ };
+ },
+ count: 4
+ },
+
+ scalars: {
+ create: function (id) {
+ return {
+ Id: id || 0,
+ B: true,
+ N: 1,
+ S: "A",
+ D: new Date(2011, 0, 1),
+ _: null
+ };
+ },
+ count: 6
+ },
+
+ nested: {
+ create: function (id) {
+ return {
+ Id: id || 0,
+ B: true,
+ N: 1,
+ S: "A",
+ O: {
+ B: true,
+ N: 1,
+ S: "A"
+ }
+ };
+ },
+ count: 8
+ },
+
+ tree: {
+ create: function (id) {
+ return {
+ Id: id || 0,
+ B: true,
+ N: 1,
+ S: "A",
+ O: {
+ B: true,
+ N: 1,
+ S: "A",
+ O1: {
+ B: true,
+ N: 1,
+ S: "A"
+ },
+ O2: {
+ B: true,
+ N: 1,
+ S: "A"
+ }
+ }
+ };
+ },
+ count: 16
+ },
+
+ array: {
+ create: function (id) {
+ return {
+ Id: id || 0,
+ B: true,
+ N: 1,
+ S: "A",
+ A: [{
+ B: true,
+ N: 1,
+ S: "A"
+ },
+ {
+ B: true,
+ N: 1,
+ S: "A"
+ }
+ ]
+ };
+ },
+ count: 13
+ },
+
+ nestedArrays: {
+ create: function (id) {
+ return {
+ Id: id || 0,
+ B: true,
+ N: 1,
+ S: "A",
+ A: [{
+ B: true,
+ N: 1,
+ S: "A",
+ A: [{
+ B: true,
+ N: 1,
+ S: "A"
+ }
+ ]
+ },
+ {
+ B: true,
+ N: 1,
+ S: "A",
+ A: [{
+ B: true,
+ N: 1,
+ S: "A"
+ }
+ ]
+ }
+ ]
+ };
+ },
+ count: 23
+ },
+
+ ko_primitives: function (id, extend) {
+ var obj = {
+ Id: ko.observable(id || 0),
+ B: ko.observable(true),
+ N: ko.observable(1),
+ S: ko.observable("A")
+ };
+ if (extend) {
+ upshot.addEntityProperties(obj);
+ upshot.addUpdatedProperties(obj);
+ }
+ return obj;
+ },
+
+ ko_tree: function (id, extend) {
+ var obj = {
+ Id: ko.observable(id || 0),
+ B: ko.observable(true),
+ N: ko.observable(1),
+ S: ko.observable("A"),
+ O: ko.observable({
+ B: ko.observable(true),
+ N: ko.observable(1),
+ S: ko.observable("A"),
+ O1: ko.observable({
+ B: ko.observable(true),
+ N: ko.observable(1),
+ S: ko.observable("A")
+ }),
+ O2: ko.observable({
+ B: ko.observable(true),
+ N: ko.observable(1),
+ S: ko.observable("A")
+ })
+ })
+ };
+ if (extend) {
+ upshot.addEntityProperties(obj);
+ upshot.addUpdatedProperties(obj);
+ upshot.addUpdatedProperties(obj.O());
+ upshot.addUpdatedProperties(obj.O().O1());
+ upshot.addUpdatedProperties(obj.O().O2());
+ }
+ return obj;
+ },
+
+ ko_array: function (id, extend) {
+ var obj = {
+ Id: ko.observable(id || 0),
+ B: ko.observable(true),
+ N: ko.observable(1),
+ S: ko.observable("A"),
+ A: ko.observableArray([
+ {
+ B: ko.observable(true),
+ N: ko.observable(1),
+ S: ko.observable("A")
+ },
+ {
+ B: ko.observable(true),
+ N: ko.observable(1),
+ S: ko.observable("A")
+ }
+ ])
+ };
+ if (extend) {
+ upshot.addEntityProperties(obj);
+ upshot.addUpdatedProperties(obj);
+ upshot.addUpdatedProperties(obj.A()[0]);
+ upshot.addUpdatedProperties(obj.A()[1]);
+ }
+ return obj;
+ },
+
+ ko_nestedArrays: function (id, extend) {
+ var obj = {
+ Id: ko.observable(id || 0),
+ B: ko.observable(true),
+ N: ko.observable(1),
+ S: ko.observable("A"),
+ A: ko.observableArray([
+ {
+ B: ko.observable(true),
+ N: ko.observable(1),
+ S: ko.observable("A"),
+ A: ko.observableArray([
+ {
+ B: ko.observable(true),
+ N: ko.observable(1),
+ S: ko.observable("A")
+ }
+ ])
+ },
+ {
+ B: ko.observable(true),
+ N: ko.observable(1),
+ S: ko.observable("A"),
+ A: ko.observableArray([
+ {
+ B: ko.observable(true),
+ N: ko.observable(1),
+ S: ko.observable("A")
+ }
+ ])
+ }
+ ])
+ };
+ if (extend) {
+ upshot.addEntityProperties(obj);
+ upshot.addUpdatedProperties(obj);
+ upshot.addUpdatedProperties(obj.A()[0]);
+ upshot.addUpdatedProperties(obj.A()[0].A()[0]);
+ upshot.addUpdatedProperties(obj.A()[1]);
+ upshot.addUpdatedProperties(obj.A()[1].A()[0]);
+ }
+ return obj;
+
+ }
+ };
+
+ upshot.test || (upshot.test = {});
+ upshot.test.datasets = datasets;
+
+})(this, upshot); \ No newline at end of file
diff --git a/test/SPA.Test/upshot/Delete.Tests.js b/test/SPA.Test/upshot/Delete.Tests.js
new file mode 100644
index 00000000..6efb9175
--- /dev/null
+++ b/test/SPA.Test/upshot/Delete.Tests.js
@@ -0,0 +1,753 @@
+/// <reference path="../Scripts/References.js" />
+(function (global, upshot, undefined) {
+
+ module("Delete.tests.js", {
+ teardown: function () {
+ testHelper.unmockAjax();
+ }
+ });
+
+ function createDataSource(useLocal, result, bufferChanges) {
+ if (useLocal) {
+ var lds = new upshot.LocalDataSource({
+ source: createDataSource(false, null, !!bufferChanges),
+ result: result,
+ filter: [{ property: "Price", operator: ">", value: 300 }, { property: "Price", operator: "<", value: 700 }]
+ });
+ return lds;
+ } else {
+ return new upshot.RemoteDataSource({
+ providerParameters: { url: "unused", operationName: "" },
+ provider: upshot.riaDataProvider,
+ result: result,
+ bufferChanges: !!bufferChanges
+ });
+ }
+ }
+
+
+ function createProductsResult() {
+ return {
+ GetProductsResult: {
+ TotalCount: 5,
+ RootResults: [
+ { ID: 1, Manufacturer: "Canon", Price: 200 },
+ { ID: 2, Manufacturer: "Nikon", Price: 400 },
+ { ID: 3, Manufacturer: "Pentax", Price: 500 },
+ { ID: 4, Manufacturer: "Sony", Price: 600 },
+ { ID: 5, Manufacturer: "Olympus", Price: 800 }
+ ],
+ Metadata: [
+ {
+ type: "Product:#Sample.Models",
+ key: ["ID"],
+ fields: {
+ ID: { type: "Int32:#System" },
+ Manufacturer: { type: "String:#System" },
+ Price: { type: "Decimal:#System" }
+ },
+ rules: {
+ ID: { required: true },
+ Price: { range: [0, 1000] }
+ }
+ }
+ ]
+ }
+ };
+ }
+
+ function createProfileResult() {
+ return {
+ GetProfileForProfileUpdateResult: {
+ TotalCount: -1,
+ IncludedResults: [
+ { __type: "Friend:#BigShelf.Models", FriendId: 2, Id: 1, ProfileId: 1 },
+ { __type: "Friend:#BigShelf.Models", FriendId: 3, Id: 2, ProfileId: 1 },
+ { __type: "Friend:#BigShelf.Models", FriendId: 6, Id: 4, ProfileId: 1 },
+ { __type: "Profile:#BigShelf.Models", AspNetUserGuid: "5ad7976c-7a95-47aa-87ba-29c3fc80643e", EmailAddress: "deepm@microsoft.com", Id: 2, Name: "Deepesh Mohnani" },
+ { __type: "Profile:#BigShelf.Models", AspNetUserGuid: "9330619d-9a8c-4269-9bde-ba4cb1b7b354", EmailAddress: "jeffhand@microsoft.com", Id: 3, Name: "Jeff Handley" },
+ { __type: "Profile:#BigShelf.Models", AspNetUserGuid: "990c62c5-30a4-4ca9-92c5-f248241f6d44", EmailAddress: "gblock@microsoft.com", Id: 6, Name: "Glenn Block" }
+ ],
+ RootResults: [{ AspNetUserGuid: "3730F12D-1A2A-4499-859B-9F586B2858A4", EmailAddress: "demo@microsoft.com", Id: 1, Name: "Demo User"}],
+ Metadata: [
+ {
+ type: "Profile:#BigShelf.Models",
+ key: ["Id"],
+ fields: {
+ AspNetUserGuid: { type: "String:#System" },
+ EmailAddress: { type: "String:#System" },
+ Friends: { type: "Friend:#BigShelf.Models", array: true, association: { name: "Profile_Friend", thisKey: ["Id"], otherKey: ["ProfileId"], isForeignKey: false} },
+ Id: { type: "Int32:#System" },
+ Name: { type: "String:#System" }
+ },
+ rules: {
+ EmailAddress: { required: true, email: true },
+ Name: { required: true }
+ }
+ },
+ {
+ type: "Friend:#BigShelf.Models",
+ key: ["Id"],
+ fields: {
+ FriendId: { type: "Int32:#System" },
+ FriendProfile: { type: "Profile:#BigShelf.Models", association: { name: "Profile_Friend1", thisKey: ["FriendId"], otherKey: ["Id"], isForeignKey: true} },
+ Id: { type: "Int32:#System" },
+ Profile: { type: "Profile:#BigShelf.Models", association: { name: "Profile_Friend", thisKey: ["ProfileId"], otherKey: ["Id"], isForeignKey: true} },
+ ProfileId: { type: "Int32:#System" }
+ }
+ }
+ ]
+ }
+ };
+ }
+
+ // "GetProfileForProfileUpdateResult":{"TotalCount":-1,"IncludedResults":[{"__type":"Friend:#BigShelf.Models","FriendId":2,"Id":1,"ProfileId":1},{"__type":"Friend:#BigShelf.Models","FriendId":3,"Id":2,"ProfileId":1},{"__type":"Friend:#BigShelf.Models","FriendId":6,"Id":4,"ProfileId":1},{"__type":"Profile:#BigShelf.Models","AspNetUserGuid":"5ad7976c-7a95-47aa-87ba-29c3fc80643e","EmailAddress":"deepm@microsoft.com","Id":2,"Name":"Deepesh Mohnani"},{"__type":"Profile:#BigShelf.Models","AspNetUserGuid":"9330619d-9a8c-4269-9bde-ba4cb1b7b354","EmailAddress":"jeffhand@microsoft.com","Id":3,"Name":"Jeff Handley"},{"__type":"Profile:#BigShelf.Models","AspNetUserGuid":"990c62c5-30a4-4ca9-92c5-f248241f6d44","EmailAddress":"gblock@microsoft.com","Id":6,"Name":"Glenn Block"}],"RootResults":[{"AspNetUserGuid":"3730F12D-1A2A-4499-859B-9F586B2858A4","EmailAddress":"demo@microsoft.com","Id":1,"Name":"Demo User"}],"Metadata":[{"type":"Profile:#BigShelf.Models","key":["Id"],"fields":{"AspNetUserGuid":{"type":"String:#System"},"Categories":{"type":"Category:#BigShelf.Models","array":true,"association":{"name":"Profile_Category","thisKey":["Id"],"otherKey":["ProfileId"],"isForeignKey":false}},"EmailAddress":{"type":"String:#System"},"FlaggedBooks":{"type":"FlaggedBook:#BigShelf.Models","array":true,"association":{"name":"Profile_FlaggedBook","thisKey":["Id"],"otherKey":["ProfileId"],"isForeignKey":false}},"Friends":{"type":"Friend:#BigShelf.Models","array":true,"association":{"name":"Profile_Friend","thisKey":["Id"],"otherKey":["ProfileId"],"isForeignKey":false}},"Id":{"type":"Int32:#System"},"Name":{"type":"String:#System"}},"rules":{"EmailAddress":{"required":true,"email":true},"Name":{"required":true}}},{"type":"Book:#BigShelf.Models","key":["Id"],"fields":{"AddedDate":{"type":"DateTime:#System"},"ASIN":{"type":"String:#System"},"Author":{"type":"String:#System"},"CategoryId":{"type":"Int32:#System"},"CategoryName":{"type":"CategoryName:#BigShelf.Models","association":{"name":"CategoryName_Book","thisKey":["CategoryId"],"otherKey":["Id"],"isForeignKey":true}},"Description":{"type":"String:#System"},"FlaggedBooks":{"type":"FlaggedBook:#BigShelf.Models","array":true,"association":{"name":"Book_FlaggedBook","thisKey":["Id"],"otherKey":["BookId"],"isForeignKey":false}},"Id":{"type":"Int32:#System"},"PublishDate":{"type":"DateTime:#System"},"Title":{"type":"String:#System"}}},{"type":"FlaggedBook:#BigShelf.Models","key":["Id"],"fields":{"Book":{"type":"Book:#BigShelf.Models","association":{"name":"Book_FlaggedBook","thisKey":["BookId"],"otherKey":["Id"],"isForeignKey":true}},"BookId":{"type":"Int32:#System"},"Id":{"type":"Int32:#System"},"IsFlaggedToRead":{"type":"Int32:#System"},"Profile":{"type":"Profile:#BigShelf.Models","association":{"name":"Profile_FlaggedBook","thisKey":["ProfileId"],"otherKey":["Id"],"isForeignKey":true}},"ProfileId":{"type":"Int32:#System"},"Rating":{"type":"Int32:#System"}}},{"type":"Friend:#BigShelf.Models","key":["Id"],"fields":{"FriendId":{"type":"Int32:#System"},"FriendProfile":{"type":"Profile:#BigShelf.Models","association":{"name":"Profile_Friend1","thisKey":["FriendId"],"otherKey":["Id"],"isForeignKey":true}},"Id":{"type":"Int32:#System"},"Profile":{"type":"Profile:#BigShelf.Models","association":{"name":"Profile_Friend","thisKey":["ProfileId"],"otherKey":["Id"],"isForeignKey":true}},"ProfileId":{"type":"Int32:#System"}}},{"type":"Category:#BigShelf.Models","key":["Id"],"fields":{"CategoryId":{"type":"Int32:#System"},"CategoryName":{"type":"CategoryName:#BigShelf.Models","association":{"name":"CategoryName_Category","thisKey":["CategoryId"],"otherKey":["Id"],"isForeignKey":true}},"Id":{"type":"Int32:#System"},"Profile":{"type":"Profile:#BigShelf.Models","association":{"name":"Profile_Category","thisKey":["ProfileId"],"otherKey":["Id"],"isForeignKey":true}},"ProfileId":{"type":"Int32:#System"}}},{"type":"CategoryName:#BigShelf.Models","key":["Id"],"fields":{"Books":{"type":"Book:#BigShelf.Models","array":true,"association":{"name":"CategoryName_Book","thisKey":["Id"],"otherKey":["CategoryId"],"isForeignKey":false}},"Categories":{"type":"Category:#BigShelf.Models","array":true,"association":{"name":"CategoryName_Category","thisKey":["Id"],"otherKey":["CategoryId"],"isForeignKey":false}},"Id":{"type":"Int32:#System"},"Name":{"type":"String:#System"}}}]}}" String
+
+ function createSubmitResult(results) {
+ var changeResult = [];
+ for (var i = 0; i < results.length; ++i) {
+ var result = {
+ Entity: results[i].entity,
+ EntityActions: null,
+ HasMemberChanges: false,
+ Id: i,
+ Operation: 4
+ };
+ if (results[i].errors) {
+ result.ValidationErrors = results[i].errors;
+ }
+ changeResult.push(result);
+ }
+ return { SubmitChangesResult: changeResult };
+ }
+
+ function deleteEntities(products, results, bufferChanges, destructive, revert) {
+ dsTestDriver.simulatePostSuccessService(createSubmitResult(results));
+ setTimeout(function () {
+ for (var i = 0; i < results.length; ++i) {
+ if (destructive) {
+ $.observable(products).remove(results[i].entity);
+ equal(upshot.EntitySource.as(products).getEntityState(results[i].entity), upshot.EntityState.ClientDeleted, "expect delete state");
+ } else {
+ upshot.EntitySource.as(products).deleteEntity(results[i].entity);
+ equal(upshot.EntitySource.as(products).getEntityState(results[i].entity), upshot.EntityState.ClientDeleted, "expect delete state");
+ }
+ }
+ if (bufferChanges) {
+ if (revert) {
+ upshot.EntitySource.as(products).revertChanges();
+ for (var i = 0; i < results.length; ++i) {
+ equal(upshot.EntitySource.as(products).getEntityState(results[i].entity), upshot.EntityState.Unmodified, "expect revert state");
+ }
+ revert();
+ } else {
+ upshot.EntitySource.as(products).commitChanges();
+ }
+ }
+ }, 10);
+ }
+
+ for (var d = 0; d < 1; ++d) { // TODO: Destructive delete tests disabled until we can redevelop this feature.
+ for (var b = 0; b < 2; ++b) {
+ (function (bufferChanges, destructive) {
+
+ test((!destructive ? "Non-destructive " : "Destructive ") + "LDS over RDS" + (bufferChanges ? " batch-" : " ") + "success", 7, function () {
+ stop();
+ dsTestDriver.simulateSuccessService(createProductsResult());
+ var rproducts = [];
+ var lproducts = [];
+ var refreshNeeded = 0;
+ var rds = createDataSource(false, rproducts, bufferChanges);
+ var lds = new upshot.LocalDataSource({
+ source: rds,
+ result: lproducts,
+ filter: [{ property: "Price", operator: ">", value: 300 }, { property: "Price", operator: "<", value: 700}]
+ });
+
+ $([lproducts]).bind("replaceAll", function () {
+ equal(lproducts.length, 3, "lcount matched");
+ equal(rproducts.length, 5, "rcount matched");
+ deleteEntities(rproducts, [{ entity: rproducts[1] }, { entity: rproducts[3]}], bufferChanges, destructive);
+ });
+
+ lds.bind({
+ refreshNeeded: function () {
+ ++refreshNeeded;
+ }
+ });
+
+ rds.bind({
+ commitSuccess: function () {
+ // the lproducts is purged automatically thru purge sequence
+ equal(lproducts.length, 1, "lcount matched");
+ equal(rproducts.length, 3, "rcount matched");
+ equal(refreshNeeded, destructive ? 1 : 0, "refreshNeeded matched"); // Non-destructive will only trigger refreshNeeded when the LDS has paging parameters.
+ start();
+ }
+ });
+
+ lds.refresh({ all: true });
+ });
+
+ if (bufferChanges) {
+ test((!destructive ? "Non-destructive " : "Destructive ") + "LDS over RDS" + (bufferChanges ? " batch-" : " ") + "revert", 9, function () {
+ stop();
+ dsTestDriver.simulateSuccessService(createProductsResult());
+ var rproducts = [];
+ var lproducts = [];
+ var refreshNeeded = 0;
+ var rds = createDataSource(false, rproducts, bufferChanges);
+ var lds = new upshot.LocalDataSource({
+ source: rds,
+ result: lproducts,
+ filter: [{ property: "Price", operator: ">", value: 300 }, { property: "Price", operator: "<", value: 700 }]
+ });
+
+ $([lproducts]).bind("replaceAll", function () {
+ var revert = function () {
+ equal(lproducts.length, 3, "lcount matched");
+ equal(rproducts.length, destructive ? 3 : 5, "rcount matched");
+ equal(refreshNeeded, destructive ? 1 : 0, "refreshNeeded matched"); // Non-destructive will only see entity states change. No reason to refresh.
+ start();
+ }
+ equal(lproducts.length, 3, "lcount matched");
+ equal(rproducts.length, 5, "rcount matched");
+ deleteEntities(rproducts, [{ entity: rproducts[1] }, { entity: rproducts[3]}], bufferChanges, destructive, revert);
+ });
+
+ lds.bind({
+ refreshNeeded: function () {
+ ++refreshNeeded;
+ }
+ });
+
+ lds.refresh({ all: true });
+ });
+ }
+ })(!!b, !!d);
+ }
+ }
+
+ for (var d = 0; d < 1; ++d) { // TODO: Destructive delete tests disabled until we can redevelop this feature.
+ for (var b = 0; b < 2; ++b) {
+ (function (bufferChanges, destructive) {
+ // AssociatedEntitiesView does not support batch commit
+ if (!bufferChanges) {
+ test((!destructive ? "Non-destructive " : "Destructive ") + "AssociatedEntitiesView" + (bufferChanges ? " batch-" : " ") + "success", 4, function () {
+ stop();
+ dsTestDriver.simulateSuccessService(createProfileResult());
+ var profiles = [];
+ var profile;
+ var rds = createDataSource(false, profiles, bufferChanges);
+
+ $([profiles]).bind("replaceAll", function () {
+ equal(profiles.length, 1, "profiles length");
+ profile = profiles[0];
+ var friends = profile.Friends;
+ equal(friends.length, 3, "friends length");
+ deleteEntities(friends, [{ entity: friends[1]}], bufferChanges, destructive);
+ });
+
+ rds.bind({
+ commitSuccess: function () {
+ // the lproducts is purged automatically thru purge sequence
+ var friends = profile.Friends;
+ equal(friends.length, 2, "friends length");
+ start();
+ }
+ });
+
+ rds.refresh();
+ });
+ }
+
+ if (bufferChanges) {
+ test((!destructive ? "Non-destructive " : "Destructive ") + "AssociatedEntitiesView" + (bufferChanges ? " batch-" : " ") + "revert", 5, function () {
+ stop();
+ dsTestDriver.simulateSuccessService(createProfileResult());
+ var profiles = [];
+ var profile;
+ var rds = createDataSource(false, profiles, bufferChanges);
+
+ $([profiles]).bind("replaceAll", function () {
+ equal(profiles.length, 1, "profiles length");
+ profile = profiles[0];
+ var friends = profile.Friends;
+ equal(friends.length, 3, "friends length");
+
+ var revert = function () {
+ // the lproducts is purged automatically thru purge sequence
+ var friends = profile.Friends;
+ equal(friends.length, destructive ? 2 : 3, "friends length");
+ start();
+ };
+ deleteEntities(friends, [{ entity: friends[1]}], bufferChanges, destructive, revert);
+ });
+
+ rds.refresh();
+ });
+ }
+
+ })(!!b, !!d);
+ }
+ }
+
+ // return; // TODO, suwatch: below tests multiple variations and scenarios.
+ // commented out for now as it might be hard to debug. Will only be run by me.
+
+ // Testing a combination of ..
+ // LDS vs. RDS
+ // Batch vs. Auto commit
+ // Destructive vs. Non-destructive delete
+ // success vs. failure (failure-revert vs. failure-recommit) vs. simply revert
+ for (var d = 0; d < 1; ++d) { // TODO: Destructive delete tests disabled until we can redevelop this feature.
+ for (var l = 0; l < 2; ++l) {
+ for (var b = 0; b < 2; ++b) {
+
+ (function (useLocal, bufferChanges, destructive) {
+
+ test((!destructive ? "Non-destructive " : "Destructive ") + (!!useLocal ? "LDS" : "RDS") + (bufferChanges ? " batch-" : " ") + "success", 13, function () {
+ stop();
+ dsTestDriver.simulateSuccessService(createProductsResult());
+ var products = [];
+ var removes = [];
+ var refreshNeeded = 0;
+
+ $([products]).bind("replaceAll", function () {
+ if (useLocal) {
+ equal(products.length, 3, "count matched");
+ deleteEntities(products, [{ entity: products[0] }, { entity: products[2]}], bufferChanges, destructive);
+ } else {
+ equal(products.length, 5, "count matched");
+ deleteEntities(products, [{ entity: products[1] }, { entity: products[3]}], bufferChanges, destructive);
+ }
+ });
+
+ $([products]).bind("remove insert", function (event, args) {
+ equal(event.type, "remove", "expect event");
+ removes.push(args.items[0]);
+ });
+
+ var ds = createDataSource(useLocal, products, bufferChanges);
+
+ var refreshNeededObserver = {
+ refreshNeeded: function () {
+ ++refreshNeeded;
+ }
+ };
+
+ var commitObserver = {
+ commitSuccess: function () {
+ equal(removes.length, 2, "removes matched");
+ equal(removes[0].ID, 2, "REMOVE0 matched");
+ equal(removes[1].ID, 4, "REMOVE1 matched");
+ removes = [];
+ if (useLocal) {
+ equal(products.length, 1, "count matched");
+ equal(products[0].ID, 3, "ID0 matched");
+ equal(products[1], undefined, "ID1 matched");
+ equal(products[2], undefined, "ID2 matched");
+ } else {
+ equal(products.length, 3, "count matched");
+ equal(products[0].ID, 1, "ID0 matched");
+ equal(products[1].ID, 3, "ID1 matched");
+ equal(products[2].ID, 5, "ID2 matched");
+ }
+ equal(refreshNeeded, 0, "refreshNeeded event"); // Non-destructive will only trigger refreshNeeded when the LDS has paging parameters.
+ start();
+ }
+ };
+
+ if (useLocal) {
+ // LDS does not have Commit api
+ ds.commitChanges = ds.commitChanges || function () { ds._entitySource.commitChanges(); };
+ ds.bind(refreshNeededObserver);
+ ds._entitySource.bind(commitObserver);
+ } else {
+ ds.bind(commitObserver);
+ }
+
+ ds.refresh(useLocal && { all: true });
+ });
+
+ if (bufferChanges) {
+ test((!destructive ? "Non-destructive " : "Destructive ") + (!!useLocal ? "LDS" : "RDS") + " batch-revert", 8, function () {
+ stop();
+ dsTestDriver.simulateSuccessService(createProductsResult());
+ var products = [];
+ var removes = [];
+ var refreshNeeded = 0;
+ $([products]).bind("replaceAll", function () {
+ var revert = function () {
+ equal(removes.length, destructive ? 2 : 0, "removes length matched");
+ removes = [];
+ if (useLocal) {
+ equal(products.length, destructive ? 1 : 3, "count matched");
+ } else {
+ equal(products.length, destructive ? 3 : 5, "count matched");
+ }
+ // revert destructive should raise refreshNeeded event
+ equal(refreshNeeded, (useLocal && destructive) ? 1 : 0, "refreshNeeded event"); // Non-destructive will only see entity states change. No reason to refresh.
+ start();
+ };
+ if (useLocal) {
+ equal(products.length, 3, "count matched");
+ deleteEntities(products, [{ entity: products[0] }, { entity: products[2]}], bufferChanges, destructive, revert);
+ } else {
+ equal(products.length, 5, "count matched");
+ deleteEntities(products, [{ entity: products[1] }, { entity: products[3]}], bufferChanges, destructive, revert);
+ }
+ });
+ $([products]).bind("remove", function (event, args) {
+ //equal(event.type, "remove", "expect event");
+ removes.push(args.items[0]);
+ });
+ var ds = createDataSource(useLocal, products, bufferChanges);
+ if (useLocal) {
+ ds.bind({ refreshNeeded: function () { refreshNeeded++; } });
+ }
+ ds.refresh(useLocal && { all: true });
+ });
+ }
+
+ if (!destructive) {
+ test((!destructive ? "Non-destructive " : "Destructive ") + (!!useLocal ? "LDS" : "RDS") + (bufferChanges ? " batch-" : " ") + "failure-recommit", 21, function () {
+ stop();
+ dsTestDriver.simulateSuccessService(createProductsResult());
+ var products = [];
+ var removes = [];
+ var refreshNeeded = 0;
+ $([products]).bind("replaceAll", function () {
+ if (useLocal) {
+ equal(products.length, 3, "count matched");
+ deleteEntities(products, [{ entity: products[0] }, { entity: products[2], errors: [{ Message: "Simulated failure!"}]}], bufferChanges, destructive);
+ } else {
+ equal(products.length, 5, "count matched");
+ deleteEntities(products, [{ entity: products[1] }, { entity: products[3], errors: [{ Message: "Simulated failure!"}]}], bufferChanges, destructive);
+ }
+ });
+ $([products]).bind("remove", function (event, args) {
+ //equal(event.type, "remove", "expect event");
+ removes.push(args.items[0]);
+ });
+ var ds = createDataSource(useLocal, products, bufferChanges);
+
+ var refreshNeededObserver = {
+ refreshNeeded: function () {
+ ++refreshNeeded;
+ }
+ };
+
+ var commitObserver = {
+ commitSuccess: function () {
+ equal(removes.length, 1, "removes matched");
+ equal(removes[0].ID, 4, "REMOVE0 matched");
+ removes = [];
+ if (useLocal) {
+ equal(products.length, 1, "count matched");
+ equal(products[0].ID, 3, "ID1 matched");
+ equal(products[1], undefined, "ID1 matched");
+ equal(products[2], undefined, "ID2 matched");
+ } else {
+ equal(products.length, 3, "count matched");
+ equal(products[0].ID, 1, "ID0 matched");
+ equal(products[1].ID, 3, "ID1 matched");
+ equal(products[2].ID, 5, "ID2 matched");
+ }
+ equal(refreshNeeded, (useLocal && destructive) ? 1 : 0, "refreshNeeded event"); // Non-destructive will only trigger refreshNeeded when the LDS has paging parameters.
+ start();
+ },
+ commitError: function (httpStatus, errorText) {
+ equal(removes.length, destructive ? 2 : 1, "removes matched");
+ equal(removes[0].ID, 2, "REMOVE0 matched");
+ var removed;
+ if (destructive) {
+ equal(removes[1].ID, 4, "REMOVE1 matched");
+ removed = removes[1];
+ } else {
+ equal(removes[1], undefined, "REMOVE1 matched");
+ }
+ removes = [];
+ if (useLocal) {
+ equal(products.length, destructive ? 1 : 2, "count matched");
+ equal(errorText, "Simulated failure!", "errorText matched");
+ equal(products[0].ID, 3, "ID0 matched");
+ if (destructive) {
+ equal(products[1], undefined, "ID1 matched");
+ equal(this.getEntityState(removed), upshot.EntityState.ClientDeleted, "state checked");
+ } else {
+ equal(products[1].ID, 4, "ID1 matched");
+ equal(this.getEntityState(products[1]), upshot.EntityState.ClientDeleted, "state checked");
+ }
+ equal(products[2], undefined, "ID2 matched");
+ equal(products[3], undefined, "ID3 matched");
+ deleteEntities(products, [{ entity: removed || products[1]}], bufferChanges, destructive);
+ } else {
+ equal(products.length, destructive ? 3 : 4, "count matched");
+ equal(errorText, "Simulated failure!", "errorText matched");
+ equal(products[0].ID, 1, "ID0 matched");
+ equal(products[1].ID, 3, "ID1 matched");
+ if (destructive) {
+ equal(products[2].ID, 5, "ID2 matched");
+ equal(products[3], undefined, "ID3 matched");
+ equal(this.getEntityState(removed), undefined, "state checked");
+ } else {
+ equal(products[2].ID, 4, "ID2 matched");
+ equal(products[3].ID, 5, "ID3 matched");
+ equal(this.getEntityState(products[2]), upshot.EntityState.ClientDeleted, "state checked");
+ }
+ deleteEntities(products, [{ entity: removed || products[2]}], bufferChanges, destructive);
+ }
+ }
+ };
+
+ if (useLocal) {
+ // LDS does not have Commit api
+ ds.commitChanges = ds.commitChanges || function () { ds._entitySource.commitChanges(); };
+ ds.bind(refreshNeededObserver);
+ ds._entitySource.bind(commitObserver);
+ } else {
+ ds.bind(commitObserver);
+ }
+
+ ds.refresh(useLocal && { all: true });
+ });
+ }
+
+ test((!destructive ? "Non-destructive " : "Destructive ") + (!!useLocal ? "LDS" : "RDS") + (bufferChanges ? " batch-" : " ") + "failure-revert", 15, function () {
+ stop();
+ dsTestDriver.simulateSuccessService(createProductsResult());
+ var products = [];
+ var removes = [];
+ var refreshNeeded = 0;
+
+ $([products]).bind("replaceAll", function () {
+ if (useLocal) {
+ equal(products.length, 3, "count matched");
+ deleteEntities(products, [{ entity: products[0] }, { entity: products[2], errors: [{ Message: "Simulated failure!"}]}], bufferChanges, destructive);
+ } else {
+ equal(products.length, 5, "count matched");
+ deleteEntities(products, [{ entity: products[1] }, { entity: products[3], errors: [{ Message: "Simulated failure!"}]}], bufferChanges, destructive);
+ }
+ });
+
+ $([products]).bind("remove", function (event, args) {
+ //equal(event.type, "remove", "expect event");
+ removes.push(args.items[0]);
+ });
+
+ var ds = createDataSource(useLocal, products, bufferChanges);
+
+ var refreshNeededObserver = {
+ refreshNeeded: function () {
+ refreshNeeded++;
+ }
+ };
+
+ var commitObserver = {
+ commitError: function (httpStatus, errorText) {
+ equal(removes.length, destructive ? 2 : 1, "removes matched");
+ equal(removes[0].ID, 2, "REMOVE0 matched");
+ var tmpProducts = [];
+ $.each(products, function (unused, product) {
+ tmpProducts.push(product);
+ });
+
+ if (destructive) {
+ equal(removes[1].ID, 4, "REMOVE1 matched");
+ tmpProducts.splice(useLocal ? 1 : 2, 0, removes[1]);
+ } else {
+ equal(removes[1], undefined, "REMOVE1 matched");
+ }
+ removes = [];
+
+ if (useLocal) {
+ equal(tmpProducts.length, 2, "count matched");
+ equal(errorText, "Simulated failure!", "errorText matched");
+ equal(tmpProducts[0].ID, 3, "ID0 matched");
+ equal(tmpProducts[1].ID, 4, "ID1 matched");
+ equal(tmpProducts[2], undefined, "ID2 matched");
+ equal(tmpProducts[3], undefined, "ID3 matched");
+ equal(this.getEntityState(tmpProducts[1]), upshot.EntityState.ClientDeleted, "state checked");
+ this.revertChanges();
+ equal(this.getEntityState(tmpProducts[1]), upshot.EntityState.Unmodified, "state checked");
+ } else {
+ equal(tmpProducts.length, 4, "count matched");
+ equal(errorText, "Simulated failure!", "errorText matched");
+ equal(tmpProducts[0].ID, 1, "ID0 matched");
+ equal(tmpProducts[1].ID, 3, "ID1 matched");
+ equal(tmpProducts[2].ID, 4, "ID2 matched");
+ equal(tmpProducts[3].ID, 5, "ID3 matched");
+ equal(this.getEntityState(tmpProducts[2]), upshot.EntityState.ClientDeleted, "state checked");
+ this.revertChanges();
+ equal(this.getEntityState(tmpProducts[2]), upshot.EntityState.Unmodified, "state checked");
+ }
+
+ equal(refreshNeeded, (useLocal && destructive) ? 1 : 0, "refreshNeeded event"); // Non-destructive will only see entity states change. No reason to refresh.
+ start();
+ }
+ };
+
+ if (useLocal) {
+ // LDS does not have Commit api
+ ds.commitChanges = ds.commitChanges || function () { ds._entitySource.commitChanges(); };
+ ds.bind(refreshNeededObserver);
+ ds._entitySource.bind(commitObserver);
+ } else {
+ ds.bind(commitObserver);
+ }
+
+ ds.refresh(useLocal && { all: true });
+ });
+
+ })(!!l, !!b, !!d);
+ }
+ }
+ }
+
+ for (var d = 0; d < 1; ++d) { // TODO: Destructive delete tests disabled until we can redevelop this feature.
+ for (var b = 0; b < 2; ++b) {
+ (function (bufferChanges, destructive) {
+
+ test((!destructive ? "Non-destructive " : "Destructive ") + "LDS over RDS" + (bufferChanges ? " batch-" : " ") + "success", 7, function () {
+ stop();
+ dsTestDriver.simulateSuccessService(createProductsResult());
+ var rproducts = [];
+ var lproducts = [];
+ var refreshNeeded = 0;
+ var rds = createDataSource(false, rproducts, bufferChanges);
+ var lds = new upshot.LocalDataSource({
+ source: rds,
+ result: lproducts,
+ filter: [{ property: "Price", operator: ">", value: 300 }, { property: "Price", operator: "<", value: 700 }]
+ });
+
+ $([lproducts]).bind("replaceAll", function () {
+ equal(lproducts.length, 3, "lcount matched");
+ equal(rproducts.length, 5, "rcount matched");
+ deleteEntities(rproducts, [{ entity: rproducts[1] }, { entity: rproducts[3]}], bufferChanges, destructive);
+ });
+
+ lds.bind({
+ refreshNeeded: function () {
+ ++refreshNeeded;
+ }
+ });
+
+ rds.bind({
+ commitSuccess: function () {
+ // the lproducts is purged automatically thru purge sequence
+ equal(lproducts.length, 1, "lcount matched");
+ equal(rproducts.length, 3, "rcount matched");
+ equal(refreshNeeded, destructive ? 1 : 0, "refreshNeeded matched"); // Non-destructive will only trigger refreshNeeded when the LDS has paging parameters.
+ start();
+ }
+ });
+
+ lds.refresh({ all: true });
+ });
+
+ if (bufferChanges) {
+ test((!destructive ? "Non-destructive " : "Destructive ") + "LDS over RDS" + (bufferChanges ? " batch-" : " ") + "revert", 9, function () {
+ stop();
+ dsTestDriver.simulateSuccessService(createProductsResult());
+ var rproducts = [];
+ var lproducts = [];
+ var refreshNeeded = 0;
+ var rds = createDataSource(false, rproducts, bufferChanges);
+ var lds = new upshot.LocalDataSource({
+ source: rds,
+ result: lproducts,
+ filter: [{ property: "Price", operator: ">", value: 300 }, { property: "Price", operator: "<", value: 700 }]
+ });
+
+ $([lproducts]).bind("replaceAll", function () {
+ var revert = function () {
+ equal(lproducts.length, 3, "lcount matched");
+ equal(rproducts.length, destructive ? 3 : 5, "rcount matched");
+ equal(refreshNeeded, destructive ? 1 : 0, "refreshNeeded matched"); // Non-destructive will only see entity states change. No reason to refresh.
+ start();
+ }
+ equal(lproducts.length, 3, "lcount matched");
+ equal(rproducts.length, 5, "rcount matched");
+ deleteEntities(rproducts, [{ entity: rproducts[1] }, { entity: rproducts[3]}], bufferChanges, destructive, revert);
+ });
+
+ lds.bind({
+ refreshNeeded: function () {
+ ++refreshNeeded;
+ }
+ });
+
+ lds.refresh({ all: true });
+ });
+ }
+ })(!!b, !!d);
+ }
+ }
+
+ for (var d = 0; d < 1; ++d) { // TODO: Destructive delete tests disabled until we can redevelop this feature.
+ for (var b = 0; b < 2; ++b) {
+ (function (bufferChanges, destructive) {
+ // AssociatedEntitiesView does not support batch commit
+ if (!bufferChanges) {
+ test((!destructive ? "Non-destructive " : "Destructive ") + "AssociatedEntitiesView" + (bufferChanges ? " batch-" : " ") + "success", 4, function () {
+ stop();
+ dsTestDriver.simulateSuccessService(createProfileResult());
+ var profiles = [];
+ var profile;
+ var rds = createDataSource(false, profiles, bufferChanges);
+
+ $([profiles]).bind("replaceAll", function () {
+ equal(profiles.length, 1, "profiles length");
+ profile = profiles[0];
+ var friends = profile.Friends;
+ equal(friends.length, 3, "friends length");
+ deleteEntities(friends, [{ entity: friends[1]}], bufferChanges, destructive);
+ });
+
+ rds.bind({
+ commitSuccess: function () {
+ // the lproducts is purged automatically thru purge sequence
+ var friends = profile.Friends;
+ equal(friends.length, 2, "friends length");
+ start();
+ }
+ });
+
+ rds.refresh();
+ });
+ }
+
+ if (bufferChanges) {
+ test((!destructive ? "Non-destructive " : "Destructive ") + "AssociatedEntitiesView" + (bufferChanges ? " batch-" : " ") + "revert", 5, function () {
+ stop();
+ dsTestDriver.simulateSuccessService(createProfileResult());
+ var profiles = [];
+ var profile;
+ var rds = createDataSource(false, profiles, bufferChanges);
+
+ $([profiles]).bind("replaceAll", function () {
+ equal(profiles.length, 1, "profiles length");
+ profile = profiles[0];
+ var friends = profile.Friends;
+ equal(friends.length, 3, "friends length");
+
+ var revert = function () {
+ // the lproducts is purged automatically thru purge sequence
+ var friends = profile.Friends;
+ equal(friends.length, destructive ? 2 : 3, "friends length");
+ start();
+ };
+ deleteEntities(friends, [{ entity: friends[1]}], bufferChanges, destructive, revert);
+ });
+
+ rds.refresh();
+ });
+ }
+
+ })(!!b, !!d);
+ }
+ }
+
+})(this, upshot);
diff --git a/test/SPA.Test/upshot/EntitySet.tests.js b/test/SPA.Test/upshot/EntitySet.tests.js
new file mode 100644
index 00000000..d9b60acb
--- /dev/null
+++ b/test/SPA.Test/upshot/EntitySet.tests.js
@@ -0,0 +1,1525 @@
+/// <reference path="../Scripts/References.js" />
+
+(function (global, $, upshot, undefined) {
+
+ module("EntitySet.tests.js");
+
+ var datasets = upshot.test.datasets,
+ observability = upshot.observability;
+
+ // direct tests against the default identity algorithm
+ test("Compute identity tests", 14, function () {
+ var metadata = {
+ A: { key: ["k1"] },
+ B: { key: ["k1", "k2", "k3"] },
+ C: {}, // missing key metadata
+ D: { key: ["a.b.k1", "a.b.k2"] }
+ };
+ var dc = new upshot.DataContext(new upshot.riaDataProvider());
+ upshot.metadata(metadata);
+
+ // valid single part key scenarios
+ equal(upshot.EntitySet.__getIdentity({ k1: "1" }, "A"), "1");
+ equal(upshot.EntitySet.__getIdentity({ k1: 1234 }, "A"), "1234");
+ equal(upshot.EntitySet.__getIdentity({ k1: 1.234 }, "A"), "1.234");
+ equal(upshot.EntitySet.__getIdentity({ k1: "E93CF47D-E7A6-44B2-8E30-0869B187BFB4" }, "A"), "E93CF47D-E7A6-44B2-8E30-0869B187BFB4");
+
+ // valid multipart key scenarios
+ equal(upshot.EntitySet.__getIdentity({ k1: "1", k2: "2", k3: "3" }, "B"), "1,2,3");
+ equal(upshot.EntitySet.__getIdentity({ k1: 1, k2: 2, k3: 3 }, "B"), "1,2,3");
+ equal(upshot.EntitySet.__getIdentity({ k1: true, k2: "E93CF47D-E7A6-44B2-8E30-0869B187BFB4", k3: 5 }, "B"), "true,E93CF47D-E7A6-44B2-8E30-0869B187BFB4,5");
+ equal(upshot.EntitySet.__getIdentity({ k1: 1, k2: null, k3: undefined }, "B"), "1,null,undefined");
+
+ // verify pathing scenarios (e.g. used by the OData provider)
+ equal(upshot.EntitySet.__getIdentity({ a: { b: { k1: "1", k2: "2"}} }, "D"), "1,2");
+
+ // no key metadata specified
+ try {
+ upshot.EntitySet.__getIdentity({ k1: "1", k2: "2", k3: "3" }, "C", "C");
+ }
+ catch (e) {
+ equal(e, "No key metadata specified for entity type 'C'");
+ }
+
+ // invalid key member specification - missing member
+ try {
+ upshot.EntitySet.__getIdentity({ k1: "1", k2: "2" /* missing k3 member */ }, "B");
+ }
+ catch (e) {
+ equal(e, "Key member 'k3' doesn't exist on entity type 'B'");
+ }
+
+ // invalid path specification - missing member
+ try {
+ upshot.EntitySet.__getIdentity({ a: { b: { k1: "1" /* k2 member missing */}} }, "D");
+ }
+ catch (e) {
+ equal(e, "Key member 'a.b.k2' doesn't exist on entity type 'D'");
+ }
+
+ // invalid path specification - null path part
+ try {
+ upshot.EntitySet.__getIdentity({ a: { b: null} }, "D");
+ }
+ catch (e) {
+ equal(e, "Key member 'a.b.k1' doesn't exist on entity type 'D'");
+ }
+
+ // no metadata registered for type
+ try {
+ upshot.EntitySet.__getIdentity({}, "E");
+ }
+ catch (e) {
+ equal(e, "No metadata available for type 'E'. Register metadata using 'upshot.metadata(...)'.");
+ }
+ });
+
+ test("Load entities with multipart keys", 4, function () {
+ var metadata = {
+ A: { key: ["k1"] },
+ B: { key: ["k1", "k2", "k3"] }
+ };
+ var dc = new upshot.DataContext(new upshot.riaDataProvider());
+ upshot.metadata(metadata);
+
+ var entities = [{ k1: "1" }, { k1: 1234 }, { k1: 1.234 }, { k1: "E93CF47D-E7A6-44B2-8E30-0869B187BFB4"}];
+ dc.merge(entities, "A", null);
+
+ entities = [
+ { k1: "1", k2: "2", k3: "3" },
+ { k1: 1, k2: 2, k3: 3 },
+ { k1: true, k2: "E93CF47D-E7A6-44B2-8E30-0869B187BFB4", k3: 5 }
+ ];
+ dc.merge(entities, "B", null);
+
+ // verify the entities are in the cache
+ equal(dc.getEntitySet("A")._entityStates["1"], upshot.EntityState.Unmodified);
+ equal(dc.getEntitySet("A")._entityStates["E93CF47D-E7A6-44B2-8E30-0869B187BFB4"], upshot.EntityState.Unmodified);
+ equal(dc.getEntitySet("B")._entityStates["1,2,3"], upshot.EntityState.Unmodified);
+ equal(dc.getEntitySet("B")._entityStates["true,E93CF47D-E7A6-44B2-8E30-0869B187BFB4,5"], upshot.EntityState.Unmodified);
+ });
+
+ test("Raises events when primitive values change", 10, function () {
+ var es = createEntitySet(),
+ entity = loadEntities(es, datasets.primitives.create(1));
+
+ equal(es.getEntities().length, 1, "There should be a single entity loaded");
+
+ var tracker = createEventTracker(es);
+
+ $.observable(entity).property("B", false);
+
+ equal(tracker.events.length, 2, "There should have been two events");
+ equal(tracker.events[0].type, "propertyChanged", "The event should be 'propertyChanged'");
+ equal(tracker.events[0].entity, entity, "The entity should have been modified");
+ equal(tracker.events[0].path, "B", "Property 'B' should have been set");
+ equal(tracker.events[0].value, entity.B, "The new value should be false");
+ equal(tracker.events[1].type, "entityUpdated", "The event should be 'entityUpdated'");
+ equal(tracker.events[1].entity, entity, "The entity should have been modified");
+ equal(tracker.events[1].path, "", "The event should have occured on the entity");
+ notEqual(tracker.events[1].eventArgs, null, "The event args should not be null");
+ });
+
+ test("Raises events when scalar values change", 10, function () {
+ var es = createEntitySet(),
+ entity = loadEntities(es, datasets.scalars.create(1));
+
+ equal(es.getEntities().length, 1, "There should be a single entity loaded");
+
+ var tracker = createEventTracker(es);
+
+ $.observable(entity).property("D", new Date());
+
+ equal(tracker.events.length, 2, "There should have been two events");
+ equal(tracker.events[0].type, "propertyChanged", "The event should be 'propertyChanged'");
+ equal(tracker.events[0].entity, entity, "The entity should have been modified");
+ equal(tracker.events[0].path, "D", "Property 'D' should have been set");
+ equal(tracker.events[0].value, entity.D, "The new value should be false");
+ equal(tracker.events[1].type, "entityUpdated", "The event should be 'entityUpdated'");
+ equal(tracker.events[1].entity, entity, "The entity should have been modified");
+ equal(tracker.events[1].path, "", "The event should have occured on the entity");
+ notEqual(tracker.events[1].eventArgs, null, "The event args should not be null");
+ });
+
+ test("Raises event when nested values change", 6, function () {
+ var es = createEntitySet(),
+ entity = loadEntities(es, datasets.nested.create(1));
+
+ equal(es.getEntities().length, 1, "There should be a single entity loaded");
+
+ var tracker = createEventTracker(es);
+
+ $.observable(entity.O).property("B", false);
+
+ equal(tracker.events.length, 1, "There should have been one event");
+ equal(tracker.events[0].type, "entityUpdated", "The event should be 'entityUpdated'");
+ equal(tracker.events[0].entity, entity, "The entity should have been modified");
+ equal(tracker.events[0].path, "O", "The event should have occured on 'O'");
+ notEqual(tracker.events[0].eventArgs, null, "The event args should not be null");
+ });
+
+ test("Raises event when tree values change", 6, function () {
+ var es = createEntitySet(),
+ entity = loadEntities(es, datasets.tree.create(1));
+
+ equal(es.getEntities().length, 1, "There should be a single entity loaded");
+
+ var tracker = createEventTracker(es);
+
+ $.observable(entity.O.O1).property("B", false);
+
+ equal(tracker.events.length, 1, "There should have been one event");
+ equal(tracker.events[0].type, "entityUpdated", "The event should be 'entityUpdated'");
+ equal(tracker.events[0].entity, entity, "The entity should have been modified");
+ equal(tracker.events[0].path, "O.O1", "The event should have occured on 'O.O1'");
+ notEqual(tracker.events[0].eventArgs, null, "The event args should not be null");
+ });
+
+ test("Raises event when array values change", 6, function () {
+ var es = createEntitySet(),
+ entity = loadEntities(es, datasets.array.create(1));
+
+ equal(es.getEntities().length, 1, "There should be a single entity loaded");
+
+ var tracker = createEventTracker(es);
+
+ $.observable(entity.A[0]).property("B", false);
+
+ equal(tracker.events.length, 1, "There should have been one event");
+ equal(tracker.events[0].type, "entityUpdated", "The event should be 'entityUpdated'");
+ equal(tracker.events[0].entity, entity, "The entity should have been modified");
+ equal(tracker.events[0].path, "A[0]", "The event should have occured on 'A[0]'");
+ notEqual(tracker.events[0].eventArgs, null, "The event args should not be null");
+ });
+
+ test("Raises event when nested array values change", 6, function () {
+ var es = createEntitySet(),
+ entity = loadEntities(es, datasets.nestedArrays.create(1));
+
+ equal(es.getEntities().length, 1, "There should be a single entity loaded");
+
+ var tracker = createEventTracker(es);
+
+ $.observable(entity.A[0].A[0]).property("B", false);
+
+ equal(tracker.events.length, 1, "There should have been one event");
+ equal(tracker.events[0].type, "entityUpdated", "The event should be 'entityUpdated'");
+ equal(tracker.events[0].entity, entity, "The entity should have been modified");
+ equal(tracker.events[0].path, "A[0].A[0]", "The event should have occured on 'A[0].A[0]'");
+ notEqual(tracker.events[0].eventArgs, null, "The event args should not be null");
+ });
+
+ test("Raises events on revertChanges", 11, function () {
+ var es = createEntitySet(),
+ entity = loadEntities(es, datasets.primitives.create(1));
+
+ equal(es.getEntities().length, 1, "There should be a single entity loaded");
+
+ var tracker = createEventTracker(es);
+
+ $.observable(entity).property("B", false);
+
+ equal(tracker.events.length, 2, "There should have been two events");
+
+ tracker.events.length = 0;
+
+ es.revertChanges();
+
+ equal(tracker.events.length, 2, "There should have been two events");
+ equal(tracker.events[0].type, "propertyChanged", "The event should be 'propertyChanged'");
+ equal(tracker.events[0].entity, entity, "The entity should have been modified");
+ equal(tracker.events[0].path, "B", "Property 'B' should have been set");
+ equal(tracker.events[0].value, entity.B, "The reverted value should be true");
+ equal(tracker.events[1].type, "entityUpdated", "The event should be 'entityUpdated'");
+ equal(tracker.events[1].entity, entity, "The entity should have been modified");
+ equal(tracker.events[1].path, undefined, "The event path should be undefined");
+ equal(tracker.events[1].eventArgs, undefined, "The event args should be undefined");
+ });
+
+ test("Raises events on revertChanges with entity", 11, function () {
+ var es = createEntitySet(),
+ entity = loadEntities(es, datasets.primitives.create(1));
+
+ equal(es.getEntities().length, 1, "There should be a single entity loaded");
+
+ var tracker = createEventTracker(es);
+
+ $.observable(entity).property("B", false);
+
+ equal(tracker.events.length, 2, "There should have been two events");
+
+ tracker.events.length = 0;
+
+ es.revertChanges(entity);
+
+ equal(tracker.events.length, 2, "There should have been two events");
+ equal(tracker.events[0].type, "propertyChanged", "The event should be 'propertyChanged'");
+ equal(tracker.events[0].entity, entity, "The entity should have been modified");
+ equal(tracker.events[0].path, "B", "Property 'B' should have been set");
+ equal(tracker.events[0].value, entity.B, "The reverted value should be true");
+ equal(tracker.events[1].type, "entityUpdated", "The event should be 'entityUpdated'");
+ equal(tracker.events[1].entity, entity, "The entity should have been modified");
+ equal(tracker.events[1].path, undefined, "The event path should be undefined");
+ equal(tracker.events[1].eventArgs, undefined, "The event args should be undefined");
+ });
+
+ test("Raises events on revertUpdates", 11, function () {
+ var es = createEntitySet(),
+ entity = loadEntities(es, datasets.primitives.create(1));
+
+ equal(es.getEntities().length, 1, "There should be a single entity loaded");
+
+ var tracker = createEventTracker(es);
+
+ $.observable(entity).property("B", false);
+
+ equal(tracker.events.length, 2, "There should have been two events");
+
+ tracker.events.length = 0;
+
+ es.revertUpdates(entity);
+
+ equal(tracker.events.length, 2, "There should have been two events");
+ equal(tracker.events[0].type, "propertyChanged", "The event should be 'propertyChanged'");
+ equal(tracker.events[0].entity, entity, "The entity should have been modified");
+ equal(tracker.events[0].path, "B", "Property 'B' should have been set");
+ equal(tracker.events[0].value, entity.B, "The reverted value should be true");
+ equal(tracker.events[1].type, "entityUpdated", "The event should be 'entityUpdated'");
+ equal(tracker.events[1].entity, entity, "The entity should have been modified");
+ equal(tracker.events[1].path, undefined, "The event path should be undefined");
+ equal(tracker.events[1].eventArgs, undefined, "The event args should be undefined");
+ });
+
+ test("Raises events on revertUpdates for property", 11, function () {
+ var es = createEntitySet(),
+ entity = loadEntities(es, datasets.primitives.create(1));
+
+ equal(es.getEntities().length, 1, "There should be a single entity loaded");
+
+ var tracker = createEventTracker(es);
+
+ $.observable(entity).property("B", false);
+
+ equal(tracker.events.length, 2, "There should have been two events");
+
+ tracker.events.length = 0;
+
+ es.revertUpdates(entity, "B");
+
+ equal(tracker.events.length, 2, "There should have been two events");
+ equal(tracker.events[0].type, "propertyChanged", "The event should be 'propertyChanged'");
+ equal(tracker.events[0].entity, entity, "The entity should have been modified");
+ equal(tracker.events[0].path, "B", "Property 'B' should have been set");
+ equal(tracker.events[0].value, entity.B, "The reverted value should be true");
+ equal(tracker.events[1].type, "entityUpdated", "The event should be 'entityUpdated'");
+ equal(tracker.events[1].entity, entity, "The entity should have been modified");
+ equal(tracker.events[1].path, undefined, "The event path should be undefined");
+ equal(tracker.events[1].eventArgs, undefined, "The event args should be undefined");
+ });
+
+ test("Raises event when knockout array values change", 6, function () {
+ try {
+ upshot.observability.configuration = observability.knockout;
+
+ var es = createEntitySet(),
+ entity = loadEntities(es, datasets.ko_array(1));
+
+ equal(es.getEntities()().length, 1, "There should be a single entity loaded");
+
+ var tracker = createEventTracker(es);
+
+ entity.A()[0].B(false);
+
+ equal(tracker.events.length, 1, "There should have been one event");
+ equal(tracker.events[0].type, "entityUpdated", "The event should be 'entityUpdated'");
+ equal(tracker.events[0].entity, entity, "The entity should have been modified");
+ equal(tracker.events[0].path, "A[0]", "The event should have occured on 'A[0]'");
+ notEqual(tracker.events[0].eventArgs, null, "The event args should not be null");
+ } finally {
+ upshot.observability.configuration = observability.jquery;
+ }
+ });
+
+ test("Subscriptions are adjusted when nested values change", 5, function () {
+ var es = createEntitySet(),
+ entity = loadEntities(es, datasets.nested.create(1));
+
+ equal(es.getEntities().length, 1, "There should be a single entity loaded");
+
+ var original = entity.O,
+ _new = { B: false };
+ var tracker = createEventTracker(es);
+
+ $.observable(original).property("B", false);
+
+ equal(tracker.events.length, 1, "There should have been one event");
+
+ tracker.events.length = 0;
+
+ $.observable(entity).property("O", _new);
+
+ equal(tracker.events.length, 2, "There should have been two events");
+
+ tracker.events.length = 0;
+
+ $.observable(original).property("B", true);
+
+ equal(tracker.events.length, 0, "There should not have been any events");
+
+ $.observable(_new).property("B", true);
+
+ equal(tracker.events.length, 1, "There should have been one event");
+ });
+
+ test("Subscriptions are adjusted when tree values change", 5, function () {
+ var es = createEntitySet(),
+ entity = loadEntities(es, datasets.tree.create(1));
+
+ equal(es.getEntities().length, 1, "There should be a single entity loaded");
+
+ var original = entity.O,
+ _new = { O1: { B: false} };
+ var tracker = createEventTracker(es);
+
+ $.observable(original.O1).property("B", false);
+
+ equal(tracker.events.length, 1, "There should have been one event");
+
+ tracker.events.length = 0;
+
+ $.observable(entity).property("O", _new);
+
+ equal(tracker.events.length, 2, "There should have been two events");
+
+ tracker.events.length = 0;
+
+ $.observable(original.O1).property("B", true);
+
+ equal(tracker.events.length, 0, "There should not have been any events");
+
+ tracker.events.length = 0;
+
+ $.observable(_new.O1).property("B", true);
+
+ equal(tracker.events.length, 1, "There should have been one event");
+ });
+
+ test("Subscriptions are added on array insert", 3, function () {
+ var es = createEntitySet(),
+ entity = loadEntities(es, datasets.array.create(1));
+
+ equal(es.getEntities().length, 1, "There should be a single entity loaded");
+
+ var _new = { B: false };
+ var tracker = createEventTracker(es);
+
+ $.observable(entity.A).insert(entity.A.length, _new);
+
+ equal(tracker.events.length, 1, "There should have been one event");
+
+ tracker.events.length = 0;
+
+ $.observable(_new).property("B", true);
+
+ equal(tracker.events.length, 1, "There should have been one event");
+ });
+
+ test("Subscriptions are removed on array remove", 4, function () {
+ var es = createEntitySet(),
+ entity = loadEntities(es, datasets.array.create(1));
+
+ equal(es.getEntities().length, 1, "There should be a single entity loaded");
+
+ var original = entity.A[0];
+ var tracker = createEventTracker(es);
+
+ $.observable(original).property("B", false);
+
+ equal(tracker.events.length, 1, "There should have been one event");
+
+ tracker.events.length = 0;
+
+ $.observable(entity.A).remove(0, 1);
+
+ equal(tracker.events.length, 1, "There should have been one event");
+
+ tracker.events.length = 0;
+
+ $.observable(original).property("B", true);
+
+ equal(tracker.events.length, 0, "There should not have been any events");
+ });
+
+ test("Subscriptions are adjusted on array replaceAll", 5, function () {
+ var es = createEntitySet(),
+ entity = loadEntities(es, datasets.array.create(1));
+
+ equal(es.getEntities().length, 1, "There should be a single entity loaded");
+
+ var original = entity.A[0],
+ _new = { B: false };
+ var tracker = createEventTracker(es);
+
+ $.observable(original).property("B", false);
+
+ equal(tracker.events.length, 1, "There should have been one event");
+
+ tracker.events.length = 0;
+
+ $.observable(entity.A).replaceAll([_new]);
+
+ equal(tracker.events.length, 1, "There should have been one event");
+
+ tracker.events.length = 0;
+
+ $.observable(original).property("B", true);
+
+ equal(tracker.events.length, 0, "There should not have been any events");
+
+ tracker.events.length = 0;
+
+ $.observable(_new).property("B", true);
+
+ equal(tracker.events.length, 1, "There should have been one event");
+ });
+
+ testWithRevertVariations(function (revertFn) {
+ test("isUpdated returns true when primitive values change", 17, function () {
+ var es = createEntitySet(),
+ entity = loadEntities(es, datasets.primitives.create(1));
+
+ equal(es.getEntities().length, 1, "There should be a single entity loaded");
+
+ equal(es.getEntityState(entity), upshot.EntityState.Unmodified, "The entity state should be 'Unmodified'");
+ equal(es.isUpdated(entity), false, "The entity should not have changes");
+ equal(es.isUpdated(entity, "B"), false, "The entity should not have changes for 'B'");
+
+ $.observable(entity).property("B", false);
+
+ equal(es.getEntityState(entity), upshot.EntityState.ClientUpdated, "The entity state should be 'ClientUpdated'");
+ equal(es.isUpdated(entity), true, "The entity should have changes");
+ equal(es.isUpdated(entity, "B"), true, "The entity should have changes for 'B'");
+ equal(es.isUpdated(entity, "N"), false, "The entity should not have changes for 'N'");
+
+ $.observable(entity).property("B", true);
+
+ equal(es.getEntityState(entity), upshot.EntityState.ClientUpdated, "The entity state should still be 'ClientUpdated'");
+ equal(es.isUpdated(entity), true, "The entity should still have changes");
+ equal(es.isUpdated(entity, "B"), true, "The entity should still have changes for 'B'");
+ equal(es.isUpdated(entity, "N"), false, "The entity should still not have changes for 'N'");
+
+ revertFn(es, entity, "B");
+
+ equal(es.isUpdated(entity, "B"), false, "The entity should no longer have changes for 'B'");
+ dataEqual(entity, datasets.primitives.create(1), "The entity should be reverted");
+ });
+ });
+
+ testWithRevertVariations(function (revertFn) {
+ test("isUpdated returns true when scalar values change", 12, function () {
+ var es = createEntitySet(),
+ entity = loadEntities(es, datasets.scalars.create(1));
+
+ equal(es.getEntities().length, 1, "There should be a single entity loaded");
+
+ equal(es.getEntityState(entity), upshot.EntityState.Unmodified, "The entity state should be 'Unmodified'");
+ equal(es.isUpdated(entity), false, "The entity should not have changes");
+ equal(es.isUpdated(entity, "D"), false, "The entity should not have changes for 'D'");
+
+ $.observable(entity).property("D", new Date());
+
+ equal(es.getEntityState(entity), upshot.EntityState.ClientUpdated, "The entity state should be 'ClientUpdated'");
+ equal(es.isUpdated(entity), true, "The entity should have changes");
+ equal(es.isUpdated(entity, "D"), true, "The entity should have changes for 'D'");
+
+ revertFn(es, entity, "D");
+
+ equal(es.isUpdated(entity, "D"), false, "The entity should no longer have changes for 'D'");
+ dataEqual(entity, datasets.scalars.create(1), "The entity should be reverted");
+ });
+ });
+
+ testWithRevertVariations(true, function (revertFn) {
+ test("isUpdated returns true when nested values change", 15, function () {
+ var es = createEntitySet(),
+ entity = loadEntities(es, datasets.nested.create(1));
+
+ equal(es.getEntities().length, 1, "There should be a single entity loaded");
+
+ equal(es.getEntityState(entity), upshot.EntityState.Unmodified, "The entity state should be 'Unmodified'");
+ equal(es.isUpdated(entity), false, "The entity should not have changes");
+ equal(es.isUpdated(entity, "O"), false, "The nested object 'O' should not have changes");
+ equal(es.isUpdated(entity, "O.B"), false, "The nested object 'O' should not have changes for 'B'");
+
+ $.observable(entity.O).property("B", false);
+
+ equal(es.getEntityState(entity), upshot.EntityState.ClientUpdated, "The entity state should be 'ClientUpdated'");
+ equal(es.isUpdated(entity), true, "The entity should have changes");
+ equal(es.isUpdated(entity, "O"), true, "The nested object 'O' should have changes");
+ equal(es.isUpdated(entity, "O.B"), true, "The nested object 'O' should have changes for 'B'");
+ equal(es.isUpdated(entity, "O", true), false, "The entity should not have changes for 'O'");
+
+ revertFn(es, entity, "O");
+
+ equal(es.isUpdated(entity, "O"), false, "The nested object 'O' should no longer have changes");
+ equal(es.isUpdated(entity, "O.B"), false, "The nested object 'O' should no longer have changes for 'B'");
+ dataEqual(entity, datasets.nested.create(1), "The entity should be reverted");
+ });
+ });
+
+ testWithRevertVariations(true, function (revertFn) {
+ test("isUpdated returns true when tree values change", 19, function () {
+ var es = createEntitySet(),
+ entity = loadEntities(es, datasets.tree.create(1));
+
+ equal(es.getEntities().length, 1, "There should be a single entity loaded");
+
+ equal(es.getEntityState(entity), upshot.EntityState.Unmodified, "The entity state should be 'Unmodified'");
+ equal(es.isUpdated(entity), false, "The entity should not have changes");
+ equal(es.isUpdated(entity, "O"), false, "The nested object 'O' should not have changes");
+ equal(es.isUpdated(entity, "O.O1"), false, "The nested object 'O.O1' should not have changes");
+ equal(es.isUpdated(entity, "O.O1.B"), false, "The nested object 'O.O1' should not have changes for 'B'");
+
+ $.observable(entity.O.O1).property("B", false);
+
+ equal(es.getEntityState(entity), upshot.EntityState.ClientUpdated, "The entity state should be 'ClientUpdated'");
+ equal(es.isUpdated(entity), true, "The entity should have changes");
+ equal(es.isUpdated(entity, "O"), true, "The nested object 'O' should have changes");
+ equal(es.isUpdated(entity, "O.O1"), true, "The nested object 'O.O1' should have changes");
+ equal(es.isUpdated(entity, "O.O1.B"), true, "The nested object 'O.O1' should have changes for 'B'");
+ equal(es.isUpdated(entity, "O", true), false, "The entity should not have changes for 'O'");
+ equal(es.isUpdated(entity, "O.O1", true), false, "The nested object 'O' should not have changes for 'O1'");
+
+ revertFn(es, entity, "O");
+
+ equal(es.isUpdated(entity, "O"), false, "The nested object 'O' should no longer have changes");
+ equal(es.isUpdated(entity, "O.O1"), false, "The nested object 'O.O1' should no longer have changes");
+ equal(es.isUpdated(entity, "O.O1.B"), false, "The nested object 'O.O1' should no longer have changes for 'B'");
+ dataEqual(entity, datasets.tree.create(1), "The entity should be reverted");
+ });
+ });
+
+ testWithRevertVariations(true, function (revertFn) {
+ test("isUpdated returns true when array values change", 18, function () {
+ var es = createEntitySet(),
+ entity = loadEntities(es, datasets.array.create(1));
+
+ equal(es.getEntities().length, 1, "There should be a single entity loaded");
+
+ equal(es.getEntityState(entity), upshot.EntityState.Unmodified, "The entity state should be 'Unmodified'");
+ equal(es.isUpdated(entity), false, "The entity should not have changes");
+ equal(es.isUpdated(entity, "A"), false, "The array should not have changes");
+ equal(es.isUpdated(entity, "A[0]"), false, "The object at index 0 should not have changes");
+ equal(es.isUpdated(entity, "A[0].B"), false, "The object at index 0 should not have changes for 'B'");
+
+ $.observable(entity.A[0]).property("B", false);
+
+ equal(es.getEntityState(entity), upshot.EntityState.ClientUpdated, "The entity state should be 'ClientUpdated'");
+ equal(es.isUpdated(entity), true, "The entity should have changes");
+ equal(es.isUpdated(entity, "A"), true, "The array should have changes");
+ equal(es.isUpdated(entity, "A[0]"), true, "The object at index 0 should have changes");
+ equal(es.isUpdated(entity, "A[0].B"), true, "The object at index 0 should have changes for 'B'");
+ equal(es.isUpdated(entity, "A", true), false, "The entity should not have changes for 'A'");
+
+ revertFn(es, entity, "A");
+
+ equal(es.isUpdated(entity, "A"), false, "The array should no longer have changes");
+ equal(es.isUpdated(entity, "A[0]"), false, "The object at index 0 should no longer have changes");
+ equal(es.isUpdated(entity, "A[0].B"), false, "The object at index 0 should no longer have changes for 'B'");
+ dataEqual(entity, datasets.array.create(1), "The entity should be reverted");
+ });
+ });
+
+ testWithRevertVariations(true, function (revertFn) {
+ test("isUpdated returns true when nested array values change", 25, function () {
+ var es = createEntitySet(),
+ entity = loadEntities(es, datasets.nestedArrays.create(1));
+
+ equal(es.getEntities().length, 1, "There should be a single entity loaded");
+
+ equal(es.getEntityState(entity), upshot.EntityState.Unmodified, "The entity state should be 'Unmodified'");
+ equal(es.isUpdated(entity), false, "The entity should not have changes");
+ equal(es.isUpdated(entity, "A"), false, "The array should not have changes");
+ equal(es.isUpdated(entity, "A[0]"), false, "The object at index 0 should not have changes");
+ equal(es.isUpdated(entity, "A[0].A"), false, "The nested array should not have changes");
+ equal(es.isUpdated(entity, "A[0].A[0]"), false, "The nested object at index 0 should not have changes");
+ equal(es.isUpdated(entity, "A[0].A[0].B"), false, "The nested object at index 0 should not have changes for 'B'");
+
+ $.observable(entity.A[0].A[0]).property("B", false);
+
+ equal(es.getEntityState(entity), upshot.EntityState.ClientUpdated, "The entity state should be 'ClientUpdated'");
+ equal(es.isUpdated(entity), true, "The entity should have changes");
+ equal(es.isUpdated(entity, "A"), true, "The array should have changes");
+ equal(es.isUpdated(entity, "A[0]"), true, "The object at index 0 should have changes");
+ equal(es.isUpdated(entity, "A[0].A"), true, "The nested array should have changes");
+ equal(es.isUpdated(entity, "A[0].A[0]"), true, "The nested object at index 0 should have changes");
+ equal(es.isUpdated(entity, "A[0].A[0].B"), true, "The nested object at index 0 should have changes for 'B'");
+ equal(es.isUpdated(entity, "A", true), false, "The entity should not have changes for 'A'");
+ equal(es.isUpdated(entity, "A[0].A", true), false, "The object at index 0 not have changes for 'A'");
+
+ revertFn(es, entity, "A");
+
+ equal(es.isUpdated(entity, "A"), false, "The array should no longer have changes");
+ equal(es.isUpdated(entity, "A[0]"), false, "The object at index 0 should no longer have changes");
+ equal(es.isUpdated(entity, "A[0].A"), false, "The nested array should no longer have changes");
+ equal(es.isUpdated(entity, "A[0].A[0]"), false, "The nested object at index 0 should no longer have changes");
+ equal(es.isUpdated(entity, "A[0].A[0].B"), false, "The nested object at index 0 should no longer have changes for 'B'");
+ dataEqual(entity, datasets.nestedArrays.create(1), "The entity should be reverted");
+ });
+ });
+
+ testWithRevertVariations(function (revertFn) {
+ test("isUpdated returns true when knockout primitive values change", 27, function () {
+ try {
+ upshot.observability.configuration = observability.knockout;
+
+ var es = createEntitySet(),
+ entity = loadEntities(es, datasets.ko_primitives(1, true));
+
+ equal(es.getEntities()().length, 1, "There should be a single entity loaded");
+
+ equal(es.getEntityState(entity), upshot.EntityState.Unmodified, "The entity state should be 'Unmodified'");
+ equal(es.isUpdated(entity), false, "The entity should not have changes");
+ equal(es.isUpdated(entity, "B"), false, "The entity should not have changes for 'B'");
+ equal(entity.IsUpdated(), false, "The entity should not be changed");
+ equal(entity.B.IsUpdated(), false, "The entity should not be changed for 'B'");
+
+ entity.B(false);
+
+ equal(es.getEntityState(entity), upshot.EntityState.ClientUpdated, "The entity state should be 'ClientUpdated'");
+ equal(es.isUpdated(entity), true, "The entity should have changes");
+ equal(es.isUpdated(entity, "B"), true, "The entity should have changes for 'B'");
+ equal(es.isUpdated(entity, "N"), false, "The entity should not have changes for 'N'");
+ equal(entity.IsUpdated(), true, "The entity should be changed");
+ equal(entity.B.IsUpdated(), true, "The entity should be changed for 'B'");
+ equal(entity.N.IsUpdated(), false, "The entity should not be changed for 'N'");
+
+ entity.B(true);
+
+ equal(es.getEntityState(entity), upshot.EntityState.ClientUpdated, "The entity state should still be 'ClientUpdated'");
+ equal(es.isUpdated(entity), true, "The entity should still have changes");
+ equal(es.isUpdated(entity, "B"), true, "The entity should still have changes for 'B'");
+ equal(es.isUpdated(entity, "N"), false, "The entity should still not have changes for 'N'");
+ equal(entity.IsUpdated(), true, "The entity should still be changed");
+ equal(entity.B.IsUpdated(), true, "The entity should still be changed for 'B'");
+ equal(entity.N.IsUpdated(), false, "The entity should still not be changed for 'N'");
+
+ revertFn(es, entity, "B");
+
+ equal(es.isUpdated(entity, "B"), false, "The entity should no longer have changes for 'B'");
+ equal(entity.IsUpdated(), false, "The entity should no longer be changed");
+ equal(entity.B.IsUpdated(), false, "The entity should no longer be changed for 'B'");
+ dataEqual(entity, datasets.ko_primitives(1), "The entity should be reverted");
+ } finally {
+ upshot.observability.configuration = observability.jquery;
+ }
+ });
+ });
+
+ testWithRevertVariations(true, function (revertFn) {
+ test("isUpdated returns true when knockout tree values change", 29, function () {
+ try {
+ upshot.observability.configuration = observability.knockout;
+
+ var es = createEntitySet(),
+ entity = loadEntities(es, datasets.ko_tree(1, true));
+
+ equal(es.getEntities()().length, 1, "There should be a single entity loaded");
+
+ equal(es.getEntityState(entity), upshot.EntityState.Unmodified, "The entity state should be 'Unmodified'");
+ equal(es.isUpdated(entity), false, "The entity should not have changes");
+ equal(es.isUpdated(entity, "O"), false, "The nested object 'O' should not have changes");
+ equal(es.isUpdated(entity, "O.O1"), false, "The nested object 'O.O1' should not have changes");
+ equal(es.isUpdated(entity, "O.O1.B"), false, "The nested object 'O.O1' should not have changes for 'B'");
+ equal(entity.IsUpdated(), false, "The entity should not be changed");
+ equal(entity.O.IsUpdated(), false, "The nested object 'O' should not be changed");
+ equal(entity.O().O1.IsUpdated(), false, "The nested object 'O.O1' should not be changed");
+ equal(entity.O().O1().B.IsUpdated(), false, "The nested object 'O.O1' should not be changed for 'B'");
+
+ entity.O().O1().B(false);
+
+ equal(es.getEntityState(entity), upshot.EntityState.ClientUpdated, "The entity state should be 'ClientUpdated'");
+ equal(es.isUpdated(entity), true, "The entity should have changes");
+ equal(es.isUpdated(entity, "O"), true, "The nested object 'O' should have changes");
+ equal(es.isUpdated(entity, "O.O1"), true, "The nested object 'O.O1' should have changes");
+ equal(es.isUpdated(entity, "O.O1.B"), true, "The nested object 'O.O1' should have changes for 'B'");
+ equal(es.isUpdated(entity, "O", true), false, "The entity should not have changes for 'O'");
+ equal(es.isUpdated(entity, "O.O1", true), false, "The nested object 'O' should not have changes for 'O1'");
+ equal(entity.IsUpdated(), true, "The entity should be changed");
+ equal(entity.O.IsUpdated(), false, "The nested object 'O' should still not be changed");
+ equal(entity.O().O1.IsUpdated(), false, "The nested object 'O.O1' should still not be changed");
+ equal(entity.O().O1().B.IsUpdated(), true, "The nested object 'O.O1' should be changed for 'B'");
+
+ revertFn(es, entity, "O");
+
+ equal(es.isUpdated(entity, "O"), false, "The nested object 'O' should no longer have changes");
+ equal(es.isUpdated(entity, "O.O1"), false, "The nested object 'O.O1' should no longer have changes");
+ equal(es.isUpdated(entity, "O.O1.B"), false, "The nested object 'O.O1' should no longer have changes for 'B'");
+ equal(entity.IsUpdated(), false, "The entity should no longer be changed");
+ equal(entity.O().O1().B.IsUpdated(), false, "The nested object 'O.O1' should no longer be changed for 'B'");
+ dataEqual(entity, datasets.ko_tree(1), "The entity should be reverted");
+ } finally {
+ upshot.observability.configuration = observability.jquery;
+ }
+ });
+ });
+
+ testWithRevertVariations(true, function (revertFn) {
+ test("isUpdated returns true when knockout array values change", 23, function () {
+ try {
+ upshot.observability.configuration = observability.knockout;
+
+ var es = createEntitySet(),
+ entity = loadEntities(es, datasets.ko_array(1, true));
+
+ equal(es.getEntities()().length, 1, "There should be a single entity loaded");
+
+ equal(es.getEntityState(entity), "Unmodified", "The entity state should be 'Unmodified'");
+ equal(es.isUpdated(entity), false, "The entity should not have changes");
+ equal(es.isUpdated(entity, "A[0]"), false, "The object at index 0 should not have changes");
+ equal(es.isUpdated(entity, "A[0].B"), false, "The object at index 0 should not have changes for 'B'");
+ equal(entity.IsUpdated(), false, "The entity should not be changed");
+ equal(entity.A.IsUpdated(), false, "The entity should not be changed for 'A'");
+ equal(entity.A()[0].B.IsUpdated(), false, "The object at index 0 should not be changed for 'B'");
+
+ entity.A()[0].B(false);
+
+ equal(es.getEntityState(entity), "ClientUpdated", "The entity state should be 'ClientUpdated'");
+ equal(es.isUpdated(entity), true, "The entity should have changes");
+ equal(es.isUpdated(entity, "A[0]"), true, "The object at index 0 should have changes");
+ equal(es.isUpdated(entity, "A[0].B"), true, "The object at index 0 should have changes for 'B'");
+ equal(es.isUpdated(entity, "A", true), false, "The entity should not have changes for 'A'");
+ equal(entity.IsUpdated(), true, "The entity should be changed");
+ equal(entity.A.IsUpdated(), false, "The entity should not be changed for 'A'");
+ equal(entity.A()[0].B.IsUpdated(), true, "The object at index 0 should be changed for 'B'");
+
+ revertFn(es, entity, "A");
+
+ equal(es.isUpdated(entity, "A[0]"), false, "The object at index 0 should no longer have changes");
+ equal(es.isUpdated(entity, "A[0].B"), false, "The object at index 0 should no longer have changes for 'B'");
+ equal(entity.IsUpdated(), false, "The entity should no longer be changed");
+ equal(entity.A()[0].B.IsUpdated(), false, "The object at index 0 should no longer be changed for 'B'");
+ dataEqual(entity, datasets.ko_array(1), "The entity should be reverted");
+ } finally {
+ upshot.observability.configuration = observability.jquery;
+ }
+ });
+ });
+
+ testWithRevertVariations(true, function (revertFn) {
+ test("isUpdated returns true when knockout nested array values change", 35, function () {
+ try {
+ upshot.observability.configuration = observability.knockout;
+
+ var es = createEntitySet(),
+ entity = loadEntities(es, datasets.ko_nestedArrays(1, true));
+
+ equal(es.getEntities()().length, 1, "There should be a single entity loaded");
+
+ equal(es.getEntityState(entity), upshot.EntityState.Unmodified, "The entity state should be 'Unmodified'");
+ equal(es.isUpdated(entity), false, "The entity should not have changes");
+ equal(es.isUpdated(entity, "A"), false, "The array should not have changes");
+ equal(es.isUpdated(entity, "A[0]"), false, "The object at index 0 should not have changes");
+ equal(es.isUpdated(entity, "A[0].A"), false, "The nested array should not have changes");
+ equal(es.isUpdated(entity, "A[0].A[0]"), false, "The nested object at index 0 should not have changes");
+ equal(es.isUpdated(entity, "A[0].A[0].B"), false, "The nested object at index 0 should not have changes for 'B'");
+ equal(entity.IsUpdated(), false, "The entity should not be changed");
+ equal(entity.A.IsUpdated(), false, "The entity should not be changed for 'A'");
+ equal(entity.A()[0].A.IsUpdated(), false, "The object at index 0 should not be changed for 'A'");
+ equal(entity.A()[0].A()[0].B.IsUpdated(), false, "The nested object at index 0 should not be changed for 'B'");
+
+ entity.A()[0].A()[0].B(false);
+
+ equal(es.getEntityState(entity), upshot.EntityState.ClientUpdated, "The entity state should be 'ClientUpdated'");
+ equal(es.isUpdated(entity), true, "The entity should have changes");
+ equal(es.isUpdated(entity, "A"), true, "The array should have changes");
+ equal(es.isUpdated(entity, "A[0]"), true, "The object at index 0 should have changes");
+ equal(es.isUpdated(entity, "A[0].A"), true, "The nested array should have changes");
+ equal(es.isUpdated(entity, "A[0].A[0]"), true, "The nested object at index 0 should have changes");
+ equal(es.isUpdated(entity, "A[0].A[0].B"), true, "The nested object at index 0 should have changes for 'B'");
+ equal(es.isUpdated(entity, "A", true), false, "The entity should not have changes for 'A'");
+ equal(es.isUpdated(entity, "A[0].A", true), false, "The object at index 0 not have changes for 'A'");
+ equal(entity.IsUpdated(), true, "The entity should not be changed");
+ equal(entity.A.IsUpdated(), false, "The entity should still not be changed for 'A'");
+ equal(entity.A()[0].A.IsUpdated(), false, "The object at index 0 should still not be changed for 'A'");
+ equal(entity.A()[0].A()[0].B.IsUpdated(), true, "The nested object at index 0 should be changed for 'B'");
+
+ revertFn(es, entity, "A");
+
+ equal(es.isUpdated(entity, "A"), false, "The array should no longer have changes");
+ equal(es.isUpdated(entity, "A[0]"), false, "The object at index 0 should no longer have changes");
+ equal(es.isUpdated(entity, "A[0].A"), false, "The nested array should no longer have changes");
+ equal(es.isUpdated(entity, "A[0].A[0]"), false, "The nested object at index 0 should no longer have changes");
+ equal(es.isUpdated(entity, "A[0].A[0].B"), false, "The nested object at index 0 should no longer have changes for 'B'");
+ equal(entity.IsUpdated(), false, "The entity should no longer be changed");
+ equal(entity.A()[0].A()[0].B.IsUpdated(), false, "The nested object at index 0 should no longer be changed for 'B'");
+ dataEqual(entity, datasets.ko_nestedArrays(1), "The entity should be reverted");
+ } finally {
+ upshot.observability.configuration = observability.jquery;
+ }
+ });
+ });
+
+ testWithRevertVariations(function (revertFn) {
+ test("isUpdated returns true when multiple properties are updated", 16, function () {
+ var es = createEntitySet(),
+ entity = loadEntities(es, datasets.primitives.create(1));
+
+ equal(es.getEntities().length, 1, "There should be a single entity loaded");
+
+ equal(es.isUpdated(entity, "B"), false, "The entity should not have changes for 'B'");
+ equal(es.isUpdated(entity, "N"), false, "The entity should not have changes for 'N'");
+
+ $.observable(entity).property("B", false);
+
+ equal(es.isUpdated(entity, "B"), true, "The entity should have changes for 'B'");
+ equal(es.isUpdated(entity, "N"), false, "The entity should still not have changes for 'N'");
+
+ $.observable(entity).property("N", -1);
+
+ equal(es.isUpdated(entity, "B"), true, "The entity should still have changes for 'B'");
+ equal(es.isUpdated(entity, "N"), true, "The entity should have changes for 'N'");
+
+ revertFn(es, entity, "B", true);
+ revertFn(es, entity, "N");
+
+ equal(es.isUpdated(entity, "B"), false, "The entity should no longer have changes for 'B'");
+ equal(es.isUpdated(entity, "N"), false, "The entity should no longer have changes for 'N'");
+ dataEqual(entity, datasets.primitives.create(1), "The entity should be reverted");
+ });
+ });
+
+ testWithRevertVariations(true, function (revertFn) {
+ test("isUpdated returns true when multiple nested properties are updated", 14, function () {
+ var es = createEntitySet(),
+ entity = loadEntities(es, datasets.nested.create(1));
+
+ equal(es.getEntities().length, 1, "There should be a single entity loaded");
+
+ equal(es.isUpdated(entity, "B"), false, "The entity should not have changes for 'B'");
+ equal(es.isUpdated(entity, "O.B"), false, "The nested object 'O' should not have changes for 'B'");
+
+ $.observable(entity).property("B", false);
+
+ equal(es.isUpdated(entity, "B"), true, "The entity should have changes for 'B'");
+ equal(es.isUpdated(entity, "O.B"), false, "The nested object 'O' should still not have changes for 'B'");
+
+ $.observable(entity.O).property("B", false);
+
+ equal(es.isUpdated(entity, "B"), true, "The entity should still have changes for 'B'");
+ equal(es.isUpdated(entity, "O.B"), true, "The nested object 'O' should have changes for 'B'");
+
+ revertFn(es, entity, "B", true);
+ revertFn(es, entity, "O");
+
+ equal(es.isUpdated(entity, "B"), false, "The entity should no longer have changes for 'B'");
+ equal(es.isUpdated(entity, "O.B"), false, "The nested object 'O' should no longer have changes for 'B'");
+ dataEqual(entity, datasets.nested.create(1), "The entity should be reverted");
+ });
+ });
+
+ test("_clearChanges resets tracking on multiple nested properties updates", 11, function () {
+ var es = createEntitySet(),
+ entity = loadEntities(es, datasets.nested.create(1));
+
+ equal(es.getEntities().length, 1, "There should be a single entity loaded");
+
+ equal(es.isUpdated(entity, "B"), false, "The entity should not have changes for 'B'");
+ equal(es.isUpdated(entity, "O.B"), false, "The nested object 'O' should not have changes for 'B'");
+
+ $.observable(entity).property("B", false);
+
+ equal(es.isUpdated(entity, "B"), true, "The entity should have changes for 'B'");
+ equal(es.isUpdated(entity, "O.B"), false, "The nested object 'O' should still not have changes for 'B'");
+
+ $.observable(entity.O).property("B", false);
+
+ equal(es.isUpdated(entity, "B"), true, "The entity should still have changes for 'B'");
+ equal(es.isUpdated(entity, "O.B"), true, "The nested object 'O' should have changes for 'B'");
+
+ es._clearChanges(entity, false);
+
+ equal(es.isUpdated(entity), false, "The entity should no longer have changes");
+ equal(es.isUpdated(entity, "O"), false, "The nested object should no longer have changes");
+ equal(es.isUpdated(entity, "B"), false, "The entity should no longer have changes for 'B'");
+ equal(es.isUpdated(entity, "O.B"), false, "The nested object 'O' should no longer have changes for 'B'");
+ });
+
+ testWithRevertVariations(true, function (revertFn) {
+ test("isUpdated returns true when multiple knockout nested properties are updated", 23, function () {
+ try {
+ upshot.observability.configuration = observability.knockout;
+
+ var es = createEntitySet(),
+ entity = loadEntities(es, datasets.ko_tree(1, true));
+
+ equal(es.getEntities()().length, 1, "There should be a single entity loaded");
+
+ equal(es.isUpdated(entity, "B"), false, "The entity should not have changes for 'B'");
+ equal(es.isUpdated(entity, "O.B"), false, "The nested object 'O' should not have changes for 'B'");
+ equal(entity.B.IsUpdated(), false, "The entity should not be changed for 'B'");
+ equal(entity.O().B.IsUpdated(), false, "The nested object 'O' should not be changed for 'B'");
+
+ entity.B(false);
+
+ equal(es.isUpdated(entity, "B"), true, "The entity should have changes for 'B'");
+ equal(es.isUpdated(entity, "O.B"), false, "The nested object 'O' should still not have changes for 'B'");
+ equal(entity.B.IsUpdated(), true, "The entity should be changed for 'B'");
+ equal(entity.O().B.IsUpdated(), false, "The nested object 'O' should still not be changed for 'B'");
+
+ entity.O().B(false);
+
+ equal(es.isUpdated(entity, "B"), true, "The entity should still have changes for 'B'");
+ equal(es.isUpdated(entity, "O.B"), true, "The nested object 'O' should have changes for 'B'");
+ equal(entity.B.IsUpdated(), true, "The entity should still be changed for 'B'");
+ equal(entity.O().B.IsUpdated(), true, "The nested object 'O' should be changed for 'B'");
+
+ revertFn(es, entity, "B", true);
+ revertFn(es, entity, "O");
+
+ equal(es.isUpdated(entity, "B"), false, "The entity should no longer have changes for 'B'");
+ equal(es.isUpdated(entity, "O.B"), false, "The nested object 'O' should no longer have changes for 'B'");
+ equal(entity.IsUpdated(), false, "The entity should no longer be changed");
+ equal(entity.B.IsUpdated(), false, "The entity should no longer be changed for 'B'");
+ equal(entity.O().B.IsUpdated(), false, "The nested object 'O' should no longer be changed for 'B'");
+ dataEqual(entity, datasets.ko_tree(1), "The entity should be reverted");
+ } finally {
+ upshot.observability.configuration = observability.jquery;
+ }
+ });
+ });
+
+ test("_clearChanges resets tracking on multiple knockout nested properties updates", 19, function () {
+ try {
+ upshot.observability.configuration = observability.knockout;
+
+ var es = createEntitySet(),
+ entity = loadEntities(es, datasets.ko_tree(1, true));
+
+ equal(es.getEntities()().length, 1, "There should be a single entity loaded");
+
+ equal(es.isUpdated(entity, "B"), false, "The entity should not have changes for 'B'");
+ equal(es.isUpdated(entity, "O.B"), false, "The nested object 'O' should not have changes for 'B'");
+ equal(entity.B.IsUpdated(), false, "The entity should not be changed for 'B'");
+ equal(entity.O().B.IsUpdated(), false, "The nested object 'O' should not be changed for 'B'");
+
+ entity.B(false);
+
+ equal(es.isUpdated(entity, "B"), true, "The entity should have changes for 'B'");
+ equal(es.isUpdated(entity, "O.B"), false, "The nested object 'O' should still not have changes for 'B'");
+ equal(entity.B.IsUpdated(), true, "The entity should be changed for 'B'");
+ equal(entity.O().B.IsUpdated(), false, "The nested object 'O' should still not be changed for 'B'");
+
+ entity.O().B(false);
+
+ equal(es.isUpdated(entity, "B"), true, "The entity should still have changes for 'B'");
+ equal(es.isUpdated(entity, "O.B"), true, "The nested object 'O' should have changes for 'B'");
+ equal(entity.B.IsUpdated(), true, "The entity should still be changed for 'B'");
+ equal(entity.O().B.IsUpdated(), true, "The nested object 'O' should be changed for 'B'");
+
+ es._clearChanges(entity, false);
+
+ equal(es.isUpdated(entity), false, "The entity should no longer have changes");
+ equal(es.isUpdated(entity, "O"), false, "The nested object should no longer have changes");
+ equal(es.isUpdated(entity, "B"), false, "The entity should no longer have changes for 'B'");
+ equal(es.isUpdated(entity, "O.B"), false, "The nested object 'O' should no longer have changes for 'B'");
+ equal(entity.B.IsUpdated(), false, "The entity should no longer be changed for 'B'");
+ equal(entity.O().B.IsUpdated(), false, "The nested object 'O' should no longer be changed for 'B'");
+ } finally {
+ upshot.observability.configuration = observability.jquery;
+ }
+ });
+
+ testWithRevertVariations(true, function (revertFn) {
+ test("isUpdated returns true on array insert", 13, function () {
+ var es = createEntitySet(),
+ entity = loadEntities(es, datasets.array.create(1));
+
+ equal(es.getEntities().length, 1, "There should be a single entity loaded");
+
+ equal(es.getEntityState(entity), upshot.EntityState.Unmodified, "The entity state should be 'Unmodified'");
+ equal(es.isUpdated(entity), false, "The entity should not have changes");
+ equal(es.isUpdated(entity, "A"), false, "The array should not have changes");
+
+ var length = entity.A.length,
+ _new = { B: false };
+
+ $.observable(entity.A).insert(length, _new);
+
+ equal(es.getEntityState(entity), upshot.EntityState.ClientUpdated, "The entity state should be 'ClientUpdated'");
+ equal(es.isUpdated(entity), true, "The entity should have changes");
+ equal(es.isUpdated(entity, "A"), true, "The array should have changes");
+ equal(es.isUpdated(entity, "A[1]"), false, "The last object should not have changes");
+ equal(entity.A.length, length + 1, "The updated array lengths should be equal");
+
+ revertFn(es, entity, "A");
+
+ equal(es.isUpdated(entity, "A"), false, "The array should no longer have changes");
+ dataEqual(entity, datasets.array.create(1), "The entity should be reverted");
+ });
+ });
+
+ testWithRevertVariations(true, function (revertFn) {
+ test("isUpdated returns true on array remove", 12, function () {
+ var es = createEntitySet(),
+ entity = loadEntities(es, datasets.array.create(1));
+
+ equal(es.getEntities().length, 1, "There should be a single entity loaded");
+
+ equal(es.getEntityState(entity), upshot.EntityState.Unmodified, "The entity state should be 'Unmodified'");
+ equal(es.isUpdated(entity), false, "The entity should not have changes");
+ equal(es.isUpdated(entity, "A"), false, "The array should not have changes");
+
+ var length = entity.A.length;
+
+ $.observable(entity.A).remove(0, 1);
+
+ equal(es.getEntityState(entity), upshot.EntityState.ClientUpdated, "The entity state should be 'ClientUpdated'");
+ equal(es.isUpdated(entity), true, "The entity should have changes");
+ equal(es.isUpdated(entity, "A"), true, "The array should have changes");
+ equal(entity.A.length, length - 1, "The updated array lengths should be equal");
+
+ revertFn(es, entity, "A");
+
+ equal(es.isUpdated(entity, "A"), false, "The array should no longer have changes");
+ dataEqual(entity, datasets.array.create(1), "The entity should be reverted");
+ });
+ });
+
+ testWithRevertVariations(true, function (revertFn) {
+ test("isUpdated returns true on array replaceAll", 13, function () {
+ var es = createEntitySet(),
+ entity = loadEntities(es, datasets.array.create(1));
+
+ equal(es.getEntities().length, 1, "There should be a single entity loaded");
+
+ equal(es.getEntityState(entity), upshot.EntityState.Unmodified, "The entity state should be 'Unmodified'");
+ equal(es.isUpdated(entity), false, "The entity should not have changes");
+ equal(es.isUpdated(entity, "A"), false, "The array should not have changes");
+
+ var length = entity.A.length,
+ _new = { B: false };
+
+ $.observable(entity.A).replaceAll([_new]);
+
+ equal(es.getEntityState(entity), upshot.EntityState.ClientUpdated, "The entity state should be 'ClientUpdated'");
+ equal(es.isUpdated(entity), true, "The entity should have changes");
+ equal(es.isUpdated(entity, "A"), true, "The array should have changes");
+ equal(es.isUpdated(entity, "A[0]"), false, "The object at index '0' should not have changes");
+ equal(entity.A.length, 1, "The updated array lengths should be equal");
+
+ revertFn(es, entity, "A");
+
+ equal(es.isUpdated(entity, "A"), false, "The array should no longer have changes");
+ dataEqual(entity, datasets.array.create(1), "The entity should be reverted");
+ });
+ });
+
+ testWithRevertVariations(true, function (revertFn) {
+ test("isUpdated returns true on multiple array updates", 16, function () {
+ var es = createEntitySet(),
+ entity = loadEntities(es, datasets.array.create(1));
+
+ equal(es.getEntities().length, 1, "There should be a single entity loaded");
+
+ equal(es.getEntityState(entity), upshot.EntityState.Unmodified, "The entity state should be 'Unmodified'");
+ equal(es.isUpdated(entity), false, "The entity should not have changes");
+ equal(es.isUpdated(entity, "A"), false, "The array should not have changes");
+
+ var length = entity.A.length,
+ _new = { B: false };
+
+ $.observable(entity.A).insert(length, _new);
+
+ equal(es.getEntityState(entity), upshot.EntityState.ClientUpdated, "The entity state should be 'ClientUpdated'");
+ equal(es.isUpdated(entity), true, "The entity should have changes");
+ equal(es.isUpdated(entity, "A"), true, "The array should have changes");
+ equal(es.isUpdated(entity, "A[2]"), false, "The last object should not have changes");
+ equal(entity.A.length, length + 1, "The updated array lengths should be equal");
+
+ $.observable(entity.A).remove(1, 2);
+
+ equal(es.isUpdated(entity), true, "The entity should still have changes");
+ equal(es.isUpdated(entity, "A"), true, "The array should still have changes");
+ equal(entity.A.length, length - 1, "The updated array lengths should be equal");
+
+ revertFn(es, entity, "A");
+
+ equal(es.isUpdated(entity, "A"), false, "The array should no longer have changes");
+ dataEqual(entity, datasets.array.create(1), "The entity should be reverted");
+ });
+ });
+
+ testWithRevertVariations(true, function (revertFn) {
+ test("isUpdated returns true on knockout array insert", 17, function () {
+ try {
+ upshot.observability.configuration = observability.knockout;
+
+ var es = createEntitySet(),
+ entity = loadEntities(es, datasets.ko_array(1, true));
+
+ equal(es.getEntities()().length, 1, "There should be a single entity loaded");
+
+ equal(es.getEntityState(entity), "Unmodified", "The entity state should be 'Unmodified'");
+ equal(es.isUpdated(entity), false, "The entity should not have changes");
+ equal(es.isUpdated(entity, "A"), false, "The array should not have changes");
+ equal(entity.A.IsUpdated(), false, "The entity should not be changed for 'A'");
+
+ var length = entity.A().length,
+ _new = { B: ko.observable(false) };
+
+ entity.A.push(_new);
+
+ equal(es.getEntityState(entity), "ClientUpdated", "The entity state should be 'ClientUpdated'");
+ equal(es.isUpdated(entity), true, "The entity should have changes");
+ equal(es.isUpdated(entity, "A"), true, "The array should have changes");
+ equal(es.isUpdated(entity, "A[1]"), false, "The last object should not have changes");
+ equal(entity.A().length, length + 1, "The updated array lengths should be equal");
+ equal(entity.A.IsUpdated(), true, "The entity should be changed for 'A'");
+
+ revertFn(es, entity, "A");
+
+ equal(es.isUpdated(entity, "A"), false, "The array should no longer have changes");
+ equal(entity.IsUpdated(), false, "The entity should no longer be changed");
+ equal(entity.A.IsUpdated(), false, "The entity should no longer be changed for 'A'");
+ dataEqual(entity, datasets.ko_array(1), "The entity should be reverted");
+ } finally {
+ upshot.observability.configuration = observability.jquery;
+ }
+ });
+ });
+
+ testWithRevertVariations(true, function (revertFn) {
+ test("isUpdated returns true on knockout array remove", 16, function () {
+ try {
+ upshot.observability.configuration = observability.knockout;
+
+ var es = createEntitySet(),
+ entity = loadEntities(es, datasets.ko_array(1, true));
+
+ equal(es.getEntities()().length, 1, "There should be a single entity loaded");
+
+ equal(es.getEntityState(entity), "Unmodified", "The entity state should be 'Unmodified'");
+ equal(es.isUpdated(entity), false, "The entity should not have changes");
+ equal(es.isUpdated(entity, "A"), false, "The array should not have changes");
+ equal(entity.A.IsUpdated(), false, "The entity should not be changed for 'A'");
+
+ var length = entity.A().length;
+
+ entity.A.shift();
+
+ equal(es.getEntityState(entity), "ClientUpdated", "The entity state should be 'ClientUpdated'");
+ equal(es.isUpdated(entity), true, "The entity should have changes");
+ equal(es.isUpdated(entity, "A"), true, "The array should have changes");
+ equal(entity.A().length, length - 1, "The updated array lengths should be equal");
+ equal(entity.A.IsUpdated(), true, "The entity should be changed for 'A'");
+
+ revertFn(es, entity, "A");
+
+ equal(es.isUpdated(entity, "A"), false, "The array should no longer have changes");
+ equal(entity.IsUpdated(), false, "The entity should no longer be changed");
+ equal(entity.A.IsUpdated(), false, "The entity should no longer be changed for 'A'");
+ dataEqual(entity, datasets.ko_array(1), "The entity should be reverted");
+ } finally {
+ upshot.observability.configuration = observability.jquery;
+ }
+ });
+ });
+
+ testWithRevertVariations(true, function (revertFn) {
+ test("isUpdated returns true on multiple knockout array updates", 21, function () {
+ try {
+ upshot.observability.configuration = observability.knockout;
+
+ var es = createEntitySet(),
+ entity = loadEntities(es, datasets.ko_array(1, true));
+
+ equal(es.getEntities()().length, 1, "There should be a single entity loaded");
+
+ equal(es.getEntityState(entity), upshot.EntityState.Unmodified, "The entity state should be 'Unmodified'");
+ equal(es.isUpdated(entity), false, "The entity should not have changes");
+ equal(es.isUpdated(entity, "A"), false, "The array should not have changes");
+ equal(entity.A.IsUpdated(), false, "The entity should not be changed for 'A'");
+
+ var length = entity.A().length,
+ _new = { B: false };
+
+ entity.A.push(_new);
+
+ equal(es.getEntityState(entity), upshot.EntityState.ClientUpdated, "The entity state should be 'ClientUpdated'");
+ equal(es.isUpdated(entity), true, "The entity should have changes");
+ equal(es.isUpdated(entity, "A"), true, "The array should have changes");
+ equal(es.isUpdated(entity, "A[1]"), false, "The last object should not have changes");
+ equal(entity.A().length, length + 1, "The updated array lengths should be equal");
+ equal(entity.A.IsUpdated(), true, "The entity should be changed for 'A'");
+
+ entity.A.pop();
+ entity.A.pop();
+
+ equal(es.isUpdated(entity), true, "The entity should still have changes");
+ equal(es.isUpdated(entity, "A"), true, "The array should still have changes");
+ equal(entity.A().length, length - 1, "The updated array lengths should be equal");
+ equal(entity.A.IsUpdated(), true, "The entity should still be changed for 'A'");
+
+ revertFn(es, entity, "A");
+
+ equal(es.isUpdated(entity, "A"), false, "The array should no longer have changes");
+ equal(entity.IsUpdated(), false, "The entity should no longer be changed");
+ equal(entity.A.IsUpdated(), false, "The entity should no longer be changed for 'A'");
+ dataEqual(entity, datasets.ko_array(1), "The entity should be reverted");
+ } finally {
+ upshot.observability.configuration = observability.jquery;
+ }
+ });
+ });
+
+ testWithRevertVariations(true, function (revertFn) {
+ test("changes to a nested object are cleared on revert changes", 14, function () {
+ var es = createEntitySet(),
+ entity = loadEntities(es, datasets.nested.create(1));
+
+ equal(es.getEntities().length, 1, "There should be a single entity loaded");
+
+ equal(es.getEntityState(entity), upshot.EntityState.Unmodified, "The entity state should be 'Unmodified'");
+ equal(es.isUpdated(entity), false, "The entity should not have changes");
+ equal(es.isUpdated(entity, "O"), false, "The nested object should not have changes");
+
+ $.observable(entity.O).property("B", false);
+
+ equal(es.getEntityState(entity), upshot.EntityState.ClientUpdated, "The entity state should be 'ClientUpdated'");
+ equal(es.isUpdated(entity), true, "The entity should have changes");
+ equal(es.isUpdated(entity, "O"), true, "The nested object should have changes");
+ equal(es.isUpdated(entity, "O.B"), true, "The nested object should have changes for 'B'");
+
+ var original = entity.O;
+ $.observable(entity).property("O", null);
+
+ equal(es.isUpdated(entity), true, "The entity should have changes");
+ equal(es.isUpdated(entity, "O"), true, "The entity should have changes for 'O'");
+
+ revertFn(es, entity, "O");
+
+ equal(es.isUpdated(entity, "O"), false, "The nested object should no longer have changes");
+ dataEqual(entity, datasets.nested.create(1), "The entity should be reverted");
+ });
+ });
+
+ testWithRevertVariations(true, function (revertFn) {
+ test("changes to an array are cleared on revert changes", 20, function () {
+ var es = createEntitySet(),
+ entity = loadEntities(es, datasets.nestedArrays.create(1));
+
+ equal(es.getEntities().length, 1, "There should be a single entity loaded");
+
+ equal(es.getEntityState(entity), upshot.EntityState.Unmodified, "The entity state should be 'Unmodified'");
+ equal(es.isUpdated(entity), false, "The entity should not have changes");
+ equal(es.isUpdated(entity, "A"), false, "The array should not have changes");
+ equal(es.isUpdated(entity, "A[0]"), false, "The object at index 0 should not have changes");
+ equal(es.isUpdated(entity, "A[0].A"), false, "The nested array should not have changes");
+
+ $.observable(entity.A[0].A).remove(0, 1);
+
+ equal(es.getEntityState(entity), upshot.EntityState.ClientUpdated, "The entity state should be 'ClientUpdated'");
+ equal(es.isUpdated(entity), true, "The entity should have changes");
+ equal(es.isUpdated(entity, "A"), true, "The array should have changes");
+ equal(es.isUpdated(entity, "A[0]"), true, "The object at index 0 should have changes");
+ equal(es.isUpdated(entity, "A[0].A"), true, "The nested array should have changes");
+
+ var original = entity.A[0];
+ $.observable(entity.A).remove(0, 1);
+
+ equal(es.isUpdated(entity), true, "The entity should have changes");
+ equal(es.isUpdated(entity, "A"), true, "The array should have changes");
+ equal(es.isUpdated(entity, "A[0]"), false, "The object at index 0 should not have changes");
+
+ revertFn(es, entity, "A");
+
+ equal(es.isUpdated(entity, "A"), false, "The array should no longer have changes");
+ equal(es.isUpdated(entity, "A[0]"), false, "The object at index 0 should no longer have changes");
+ equal(es.isUpdated(entity, "A[0].A"), false, "The nested array should no longer have changes");
+ dataEqual(entity, datasets.nestedArrays.create(1), "The entity should be reverted");
+ });
+ });
+
+ test("_getOriginalValue returns the original value", 4, function () {
+ var es = createEntitySet(),
+ entity = loadEntities(es, datasets.nestedArrays.create(1));
+
+ equal(es.getEntities().length, 1, "There should be a single entity loaded");
+
+ equal(es.getEntityState(entity), upshot.EntityState.Unmodified, "The entity state should be 'Unmodified'");
+
+ $.observable(entity).property("B", false);
+ $.observable(entity).property("S", "S");
+ $.observable(entity.A).insert(entity.A.length, [{ B: true}]);
+ $.observable(entity.A[1]).property("N", -1);
+ $.observable(entity.A[0].A).replaceAll([{ B: true }, { B: true}]);
+
+ equal(es.isUpdated(entity), true, "The entity should have changes");
+
+ dataEqual(es._getOriginalValue(entity), datasets.nestedArrays.create(1), "Original values should be equal.");
+ });
+
+ test("_merge sets new values into entity", 2, function () {
+ var es = createEntitySet(),
+ entity = loadEntities(es, datasets.nestedArrays.create(1));
+
+ equal(es.getEntities().length, 1, "There should be a single entity loaded");
+
+ var _new = datasets.nestedArrays.create(1);
+
+ _new.B = false;
+ _new.N = 3;
+ _new.A[0].B = false;
+ _new.A[1].A.push({ B: true });
+
+ es._merge(entity, _new);
+
+ dataEqual(entity, _new, "The entity should equal new values");
+ });
+
+ testWithRevertVariations(function (revertFn) {
+ test("_merge does not update a modified entity", 15, function () {
+ var es = createEntitySet(),
+ entity = loadEntities(es, datasets.nestedArrays.create(1));
+
+ equal(es.getEntities().length, 1, "There should be a single entity loaded");
+
+ equal(es.getEntityState(entity), upshot.EntityState.Unmodified, "The entity state should be 'Unmodified'");
+
+ $.observable(entity).property("N", -1);
+
+ equal(es.isUpdated(entity), true, "The entity should have changes");
+ equal(es.isUpdated(entity, "N"), true, "The entity should have changes for 'N'");
+
+ var original = es._getOriginalValue(entity),
+ _new = datasets.nestedArrays.create(1);
+ _new.B = false;
+
+ es._merge(entity, _new);
+
+ equal(es.isUpdated(entity), true, "The entity should have changes");
+ equal(es.isUpdated(entity, "N"), true, "The entity should have changes for 'N'");
+ equal(entity.B, original.B, "The value of 'B' should match the original value");
+ notEqual(entity.B, _new.B, "The value of 'B' should not match the new value");
+
+ revertFn(es, entity, "N");
+
+ equal(es.isUpdated(entity, "N"), false, "The entity should no longer have changes for 'N'");
+ equal(entity.B, original.B, "The value of 'B' should match the original value");
+ notEqual(entity.B, _new.B, "The value of 'B' should not match the new value");
+ dataEqual(entity, original, "The entity should equal the original values");
+ });
+ });
+
+
+ function createEntitySet() {
+ var dataContext = new upshot.DataContext(new upshot.riaDataProvider()),
+ entitySet = new upshot.EntitySet(dataContext, "entityType");
+
+ upshot.metadata("entityType", {
+ key: ["Id"]
+ });
+ return entitySet;
+ }
+
+ function loadEntities(entitySet, entities) {
+ var entities2 = entities;
+ if (!upshot.isArray(entities)) {
+ entities2 = [entities];
+ }
+ entities2 = entitySet.__loadEntities(entities2);
+ if (!upshot.isArray(entities)) {
+ return entities2[0];
+ }
+ return entities2;
+ }
+
+ function createEventTracker(entitySet) {
+ var e = [],
+ tracker = {
+ events: e,
+ propertyChanged: function (entity, path, value) {
+ e.push({ type: "propertyChanged", entity: entity, path: path, value: value });
+ },
+ entityUpdated: function (entity, path, eventArgs) {
+ e.push({ type: "entityUpdated", entity: entity, path: path, eventArgs: eventArgs });
+ }
+ };
+ entitySet.bind("propertyChanged", tracker.propertyChanged);
+ entitySet.bind("entityUpdated", tracker.entityUpdated);
+ return tracker;
+ }
+
+ function dataEqual(actual, expected, message) {
+ var count = 0;
+ function compare(actualValue, expectedValue, property) {
+ count++;
+ if (actualValue !== expectedValue) {
+ return count + ": property '" + property + "' with value '" + actualValue + "' does not equal expected value '" + expectedValue + "'.\n";
+ }
+ return "";
+ }
+
+ equal(dataEqualRecursive(actual, expected, compare), "", message);
+ }
+
+ function dataEqualRecursive(actual, expected, compare, property) {
+ var difference = "";
+ if (upshot.isArray(actual) && upshot.isArray(expected)) {
+ $.each(expected, function (index, value) {
+ difference += dataEqualRecursive(actual[index], value, compare, index);
+ });
+ } else if (upshot.isObject(actual) && upshot.isObject(expected)) {
+ $.each(expected, function (key, value) {
+ difference += dataEqualRecursive(actual[key], value, compare, key);
+ });
+ } else if (ko.isObservable(actual) && ko.isObservable(expected)) {
+ difference = dataEqualRecursive(ko.utils.unwrapObservable(actual), ko.utils.unwrapObservable(expected), compare, property);
+ } else if (upshot.isDate(actual) && upshot.isDate(expected)) {
+ difference = compare(actual.toString(), expected.toString(), property);
+ } else {
+ difference = compare(actual, expected, property);
+ }
+ return difference;
+ }
+
+ function testWithRevertVariations(nested, testFn) {
+ if ($.isFunction(nested)) {
+ testFn = nested;
+ nested = false;
+ }
+ testFn(function (es, entity, propertyName) {
+ var observer;
+ if (!nested) {
+ observer = function () {
+ equal(es.isUpdated(entity, propertyName), false, "The entity should not have property changes during callbacks");
+ };
+ es.bind("propertyChanged", observer);
+ }
+ es.revertChanges();
+ if (!nested) {
+ es.unbind("propertyChanged", observer);
+ }
+ equal(es.getEntityState(entity), upshot.EntityState.Unmodified, "The entity state should be 'Unmodified' again");
+ equal(es.isUpdated(entity), false, "The entity should no longer have changes");
+ });
+ testFn(function (es, entity, propertyName, isUpdated) {
+ var observer;
+ if (!nested) {
+ observer = function () {
+ equal(es.isUpdated(entity, propertyName), false, "The entity should not have property changes during callbacks");
+ };
+ es.bind("propertyChanged", observer);
+ }
+ es.revertUpdates(entity, propertyName);
+ if (!nested) {
+ es.unbind("propertyChanged", observer);
+ }
+ var expectedState = !!isUpdated ? upshot.EntityState.ClientUpdated : upshot.EntityState.Unmodified;
+ equal(es.getEntityState(entity), expectedState, "The entity state should still be '" + expectedState + "'");
+ equal(es.isUpdated(entity), !!isUpdated, "The entity should " + (!!isUpdated ? "" : "no longer") + " have changes");
+ });
+ }
+
+})(this, jQuery, upshot);
diff --git a/test/SPA.Test/upshot/Init.js b/test/SPA.Test/upshot/Init.js
new file mode 100644
index 00000000..7b980e40
--- /dev/null
+++ b/test/SPA.Test/upshot/Init.js
@@ -0,0 +1,41 @@
+/// <reference path="../Scripts/References.js" />
+/// <reference path="TestGen.js" />
+
+// Start the tests after all scripts have been dynamically loaded
+QUnit.config.autostart = false;
+
+// Resets the database and starts test generation
+var TestPri = (function ($) {
+ /// <param name="$" type="jQuery" />
+
+ function loadScripts() {
+ // Scripts are now a variable that can be dynamically modified to switch to vsdoc or minified.
+ var scriptsToLoad = [
+ "upshot/ChangeTracking.tests.js",
+ "upshot/Consistency.tests.js",
+ "upshot/Core.tests.js",
+ "upshot/DataContext.tests.js",
+ "upshot/DataProvider.tests.js",
+ "upshot/DataSource.Common.js",
+ "upshot/DataSource.tests.js",
+ "upshot/Delete.tests.js",
+ "upshot/EntitySet.tests.js",
+ "upshot/jQuery.DataView.tests.js",
+ "upshot/Mapping.tests.js",
+ "upshot/RecordSet.js"
+ ];
+
+ // Avoid browser caching by appending a query string to the url ?13099...
+ $.each(scriptsToLoad, function (i, item) {
+ scriptsToLoad[i] = item + "?" + (new Date()).getTime();
+ });
+
+ return $.getScriptByReference("upshot/Datasets.js").pipe(function () {
+ return $.whenAll($.getScriptsByReference(scriptsToLoad));
+ });
+ }
+
+ $(window).load(function () { loadScripts().then(QUnit.start); }); // QUnit initializes on window load. Don't call QUnit.start until then.
+
+ return 0;
+})(jQuery); \ No newline at end of file
diff --git a/test/SPA.Test/upshot/Mapping.tests.js b/test/SPA.Test/upshot/Mapping.tests.js
new file mode 100644
index 00000000..1c9e6d40
--- /dev/null
+++ b/test/SPA.Test/upshot/Mapping.tests.js
@@ -0,0 +1,441 @@
+/// <reference path="../Scripts/References.js" />
+
+(function (upshot, $, ko, undefined) {
+
+ var obsOld = upshot.observability.configuration;
+ module("mapping.tests.js", {
+ teardown: function () {
+ upshot.observability.configuration = obsOld;
+ testHelper.unmockAjax();
+ }
+ });
+
+ function createRemoteDataSource(options) {
+ options = $.extend({ providerParameters: { url: "unused", operationName: ""} }, options);
+ return new upshot.RemoteDataSource(options);
+ }
+
+ function createCustomersResult() {
+ return [
+ {
+ ID: 1,
+ Name: "Joe",
+ Orders: [
+ { ID: 97, ProductName: "Smarties", CustomerID: 1 }
+ ]
+ },
+ {
+ ID: 2,
+ Name: "Stan",
+ Orders: [
+ { ID: 99, ProductName: "Shreddies", CustomerID: 2 }
+ ]
+ },
+ {
+ ID: 3,
+ Name: "Fred",
+ Orders: [
+ { ID: 98, ProductName: "Wheatabix", CustomerID: 3 },
+ { ID: 96, ProductName: "Red Rose Tea", CustomerID: 3 }
+ ]
+ }
+ ];
+ }
+
+ var customersMetadata = {
+ Customer_Mapping: {
+ key: ["ID"],
+ fields: {
+ ID: { type: "Int32:#System" },
+ Name: { type: "String:#System" },
+ Orders: {
+ type: "Order_Mapping",
+ array: true,
+ association: {
+ thisKey: ["ID"],
+ otherKey: ["CustomerID"]
+ }
+ }
+ }
+ },
+ Order_Mapping: {
+ key: ["ID"],
+ fields: {
+ ID: { type: "Int32:#System" },
+ ProductName: { type: "String:#System" },
+ CustomerID: { type: "Int32:#System" },
+ Customer: {
+ type: "Customer_Mapping",
+ association: {
+ isForeignKey: true,
+ thisKey: ["CustomerID"],
+ otherKey: ["ID"]
+ }
+ }
+ }
+ },
+ Entity_Mapping: {
+ key: ["Id"],
+ fields: {
+ Id: { type: "Int32:#System" },
+ String: { type: "String:#System" },
+ Number: { type: "Int32:#System" }
+ }
+ },
+ CT_Mapping: {
+ fields: {
+ String: { type: "String:#System" },
+ Number: { type: "Int32:#System" },
+ CT: { type: "CT_Mapping" }
+ }
+ }
+ };
+
+ function happyMapper(entityType) {
+ return function (data) {
+ var mapped = upshot.map(data, entityType);
+ mapped.Happy = true;
+ return mapped;
+ }
+ }
+
+ (function () {
+ var mappingOptions = [
+ {
+ entityType: "Customer_Mapping",
+ mapping: happyMapper("Customer_Mapping")
+ },
+ {
+ entityType: "Customer_Mapping",
+ mapping: {
+ map: happyMapper("Customer_Mapping"),
+ unmap: function () { throw "Not reached"; }
+ }
+ },
+ {
+ entityType: "Customer_Mapping",
+ mapping: {
+ Customer_Mapping: happyMapper("Customer_Mapping")
+ }
+ },
+ {
+ entityType: "Customer_Mapping",
+ mapping: {
+ Customer_Mapping: {
+ map: happyMapper("Customer_Mapping"),
+ unmap: function () { throw "Not reached"; }
+ }
+ }
+ }
+ ];
+ for (var i = 0; i < mappingOptions.length; i++) {
+ (function (options) {
+ test("Verify customer parent mapping, default child mapping", 3, function () {
+ stop();
+
+ upshot.observability.configuration = upshot.observability.knockout;
+ upshot.metadata(customersMetadata);
+ dsTestDriver.simulateSuccessService(createCustomersResult());
+
+ var rds = createRemoteDataSource(options);
+ rds.refresh(function (entities) {
+ equal(entities[1].Orders()[0].ProductName(), "Shreddies", "Mapping and associations applied");
+ equal($.grep(entities, function (entity) { return entity.Happy; }).length, 3, "Customers mapped with custom mapping");
+ equal($.grep(rds.getDataContext().getEntitySet("Order_Mapping").getEntities()(), function (entity) { return entity.Happy; }).length, 0, "Orders mapped with default mapping");
+
+ start();
+ });
+ });
+ })(mappingOptions[i]);
+ }
+ })();
+
+ (function () {
+ var mappingOptions = [
+ {
+ entityType: "Customer_Mapping",
+ mapping: {
+ Order_Mapping: happyMapper("Order_Mapping")
+ }
+ },
+ {
+ entityType: "Customer_Mapping",
+ mapping: {
+ Order_Mapping: {
+ map: happyMapper("Order_Mapping"),
+ unmap: function () { throw "Not reached"; }
+ }
+ }
+ }
+ ];
+ for (var i = 0; i < mappingOptions.length; i++) {
+ (function (options) {
+ test("Verify default parent mapping, custom child mapping", 3, function () {
+ stop();
+
+ upshot.observability.configuration = upshot.observability.knockout;
+ upshot.metadata(customersMetadata);
+ dsTestDriver.simulateSuccessService(createCustomersResult());
+
+ var rds = createRemoteDataSource(options);
+ rds.refresh(function (entities) {
+ equal(entities[1].Orders()[0].ProductName(), "Shreddies", "Mapping and associations applied");
+ equal($.grep(entities, function (entity) { return entity.Happy; }).length, 0, "Customers mapped with default mapping");
+ equal($.grep(rds.getDataContext().getEntitySet("Order_Mapping").getEntities()(), function (entity) { return entity.Happy; }).length, 4, "Orders mapped with custom mapping");
+
+ start();
+ });
+ });
+ })(mappingOptions[i]);
+ }
+ })();
+
+ function Customer(data) {
+ this.ID = ko.observable(data.ID);
+ this.Orders = upshot.map(data.Orders, "Order_Mapping");
+
+ this.Happy = true;
+ }
+
+ test("Verify use of ctor as map", 3, function () {
+ stop();
+
+ upshot.observability.configuration = upshot.observability.knockout;
+ upshot.metadata(customersMetadata);
+ dsTestDriver.simulateSuccessService(createCustomersResult());
+
+ var rds = createRemoteDataSource({
+ entityType: "Customer_Mapping",
+ mapping: Customer
+ });
+ rds.refresh(function (entities) {
+ equal(entities[1].Orders()[0].ProductName(), "Shreddies", "Mapping and associations applied");
+ equal($.grep(entities, function (entity) { return entity.Happy; }).length, 3, "Customers mapped with custom mapping");
+ equal($.grep(rds.getDataContext().getEntitySet("Order_Mapping").getEntities()(), function (entity) { return entity.Happy; }).length, 0, "Orders mapped with default mapping");
+
+ start();
+ });
+ });
+
+ // TODO: Factor our KO test setup elsewhere and move this test to a better home.
+ test("LDS over ASEV produces correct filtered result", 2, function () {
+ stop();
+
+ upshot.observability.configuration = upshot.observability.knockout;
+ upshot.metadata(customersMetadata);
+ dsTestDriver.simulateSuccessService(createCustomersResult());
+
+ var rds = createRemoteDataSource({
+ entityType: "Customer_Mapping",
+ mapping: Customer
+ });
+
+ rds.refresh(function (entities) {
+ var lds = new upshot.LocalDataSource({
+ source: entities[2].Orders,
+ filter: { property: "ProductName", value: "Wheatabix" }
+ });
+ lds.refresh(function (entities2) {
+ ok(entities2.length === 1 && entities2[0].ProductName() === "Wheatabix", "Correct LDS refresh result");
+
+ var lds2 = new upshot.LocalDataSource({
+ source: upshot.EntitySource.as(entities[2].Orders),
+ filter: { property: "ProductName", value: "Wheatabix" }
+ });
+ lds2.refresh(function (entities3) {
+ ok(entities3.length === 1 && entities3[0].ProductName() === "Wheatabix", "Correct LDS refresh result");
+
+ start();
+ });
+ });
+ });
+ });
+
+ test("Default knockout mapping adds entity and updated properties", 2, function () {
+ upshot.observability.configuration = upshot.observability.knockout;
+ upshot.metadata(customersMetadata);
+
+ var entity = upshot.map({
+ Id: 1,
+ String: "String",
+ Number: 1
+ }, "Entity_Mapping");
+
+ equal(entity.hasOwnProperty("EntityState"), true, "Entity should have 'EntityState' property");
+ equal(entity.String.hasOwnProperty("IsUpdated"), true, "String should have 'IsUpdated' property");
+ });
+
+ test("Default knockout mapping for a complex type adds updated properties", 4, function () {
+ upshot.observability.configuration = upshot.observability.knockout;
+ upshot.metadata(customersMetadata);
+
+ var ct = upshot.map({
+ String: "String",
+ Number: 1,
+ CT: {
+ String: "String2",
+ Number: 2,
+ CT: null
+ }
+ }, "CT_Mapping");
+
+ equal(ct.hasOwnProperty("EntityState"), false, "CT should not have 'EntityState' property");
+ equal(ct.String.hasOwnProperty("IsUpdated"), true, "String should have 'IsUpdated' property");
+ equal(ct.CT().hasOwnProperty("EntityState"), false, "Nested CT should not have 'EntityState' property");
+ equal(ct.CT().String.hasOwnProperty("IsUpdated"), true, "Nested String should have 'IsUpdated' property");
+ });
+
+ test("upshot.addEntityProperties for knockout adds entity properties", 12, function () {
+ upshot.observability.configuration = upshot.observability.knockout;
+ upshot.metadata(customersMetadata);
+
+ var entity = upshot.map({
+ Id: 1,
+ String: "String",
+ Number: 1
+ }, "Entity_Mapping");
+
+ equal(entity.hasOwnProperty("EntityState"), true, "Entity should have 'EntityState' property");
+ equal(entity.EntityState(), upshot.EntityState.Unmodified, "EntityState should be unmodified");
+ equal(entity.hasOwnProperty("EntityError"), true, "Entity should have 'EntityError' property");
+ equal(entity.EntityError(), null, "EntityError should be null");
+ equal(entity.hasOwnProperty("IsUpdated"), true, "Entity should have 'IsUpdated' property");
+ equal(entity.IsUpdated(), false, "IsUpdated should be false");
+ equal(entity.hasOwnProperty("IsAdded"), true, "Entity should have 'IsAdded' property");
+ equal(entity.IsAdded(), false, "IsAdded should be false");
+ equal(entity.hasOwnProperty("IsDeleted"), true, "Entity should have 'IsDeleted' property");
+ equal(entity.IsDeleted(), false, "IsDeleted should be false");
+ equal(entity.hasOwnProperty("IsChanged"), true, "Entity should have 'IsChanged' property");
+ equal(entity.IsChanged(), false, "IsChanged should be false");
+ });
+
+ test("upshot.addUpdatedProperties for knockout adds updated properties", 6, function () {
+ upshot.observability.configuration = upshot.observability.knockout;
+ upshot.metadata(customersMetadata);
+
+ var entity = upshot.map({
+ Id: 1,
+ String: "String",
+ Number: 1
+ }, "Entity_Mapping");
+
+ equal(entity.Id.hasOwnProperty("IsUpdated"), true, "Id should have 'IsUpdated' property");
+ equal(entity.Id.IsUpdated(), false, "Id.IsUpdated should be false");
+ equal(entity.String.hasOwnProperty("IsUpdated"), true, "String should have 'IsUpdated' property");
+ equal(entity.String.IsUpdated(), false, "String.IsUpdated should be false");
+ equal(entity.Number.hasOwnProperty("IsUpdated"), true, "Number should have 'IsUpdated' property");
+ equal(entity.Number.IsUpdated(), false, "Number.IsUpdated should be false");
+ });
+
+ function getEntityStates() {
+ var states = {};
+ $.each(upshot.EntityState, function (key, value) {
+ if (typeof value === "string") {
+ states[value] = false;
+ }
+ });
+ return states;
+ }
+
+ test("knockout entity.IsUpdated should reflect EntityState", 8, function () {
+ upshot.observability.configuration = upshot.observability.knockout;
+ upshot.metadata(customersMetadata);
+
+ var entity = upshot.map({
+ Id: 1,
+ String: "String",
+ Number: 1
+ }, "Entity_Mapping");
+
+ var states = getEntityStates();
+ states[upshot.EntityState.ClientUpdated] = true;
+ states[upshot.EntityState.ServerUpdating] = true;
+ $.each(states, function (key, value) {
+ entity.EntityState(key);
+ equal(entity.IsUpdated(), value, "IsUpdated should be " + value + " for state " + key);
+ });
+ });
+
+ test("knockout entity.IsAdded should reflect EntityState", 8, function () {
+ upshot.observability.configuration = upshot.observability.knockout;
+ upshot.metadata(customersMetadata);
+
+ var entity = upshot.map({
+ Id: 1,
+ String: "String",
+ Number: 1
+ }, "Entity_Mapping");
+
+ var states = getEntityStates();
+ states[upshot.EntityState.ClientAdded] = true;
+ states[upshot.EntityState.ServerAdding] = true;
+ $.each(states, function (key, value) {
+ entity.EntityState(key);
+ equal(entity.IsAdded(), value, "IsAdded should be " + value + " for state " + key);
+ });
+ });
+
+ test("knockout entity.IsDeleted should reflect EntityState", 8, function () {
+ upshot.observability.configuration = upshot.observability.knockout;
+ upshot.metadata(customersMetadata);
+
+ var entity = upshot.map({
+ Id: 1,
+ String: "String",
+ Number: 1
+ }, "Entity_Mapping");
+
+ var states = getEntityStates();
+ states[upshot.EntityState.ClientDeleted] = true;
+ states[upshot.EntityState.ServerDeleting] = true;
+ states[upshot.EntityState.Deleted] = true;
+ $.each(states, function (key, value) {
+ entity.EntityState(key);
+ equal(entity.IsDeleted(), value, "IsDeleted should be " + value + " for state " + key);
+ });
+ });
+
+ test("knockout entity.IsChanged should reflect EntityState", 8, function () {
+ upshot.observability.configuration = upshot.observability.knockout;
+ upshot.metadata(customersMetadata);
+
+ var entity = upshot.map({
+ Id: 1,
+ String: "String",
+ Number: 1
+ }, "Entity_Mapping");
+
+ var states = getEntityStates();
+ states[upshot.EntityState.Unmodified] = true;
+ states[upshot.EntityState.Deleted] = true;
+ $.each(states, function (key, value) {
+ entity.EntityState(key);
+ equal(entity.IsChanged(), !value, "IsChanged should be " + !value + " for state " + key);
+ });
+ });
+
+ test("upshot.registerType and upshot.type use", 4, function () {
+ upshot.registerType("FooType", function () { return Foo; });
+ function Foo() {};
+ equal(upshot.type(Foo), "FooType", "upshot.registerType works before key declaration");
+
+ function Foo2() {};
+ upshot.registerType("FooType", function () { return Foo2; });
+ equal(upshot.type(Foo2), "FooType", "upshot.registerType works after key declaration");
+
+ upshot.registerType({ Bar1Type: function () { return Bar1; }, Bar2Type: function () { return Bar2; } });
+ function Bar1() {};
+ function Bar2() {};
+ ok(upshot.type(Bar1) === "Bar1Type" && upshot.type(Bar2) === "Bar2Type", "upshot.registerType supports multiple registrations");
+
+ var exception;
+ try {
+ function Zip() {};
+ upshot.type(Zip);
+ } catch (ex) {
+ exception = true;
+ }
+ ok(!!exception, "upshot.type with no preceding upshot.registerType throws exception");
+ });
+
+})(upshot, jQuery, ko);
diff --git a/test/SPA.Test/upshot/RecordSet.js b/test/SPA.Test/upshot/RecordSet.js
new file mode 100644
index 00000000..3018fc7f
--- /dev/null
+++ b/test/SPA.Test/upshot/RecordSet.js
@@ -0,0 +1,67 @@
+module("EntitySet.js");
+
+(function (global, upshot, undefined) {
+
+ function mockDs() {
+ var mockDs = new upshot.DataContext();
+ upshot.metadata("mockType", { key: ["mockId"] });
+ return mockDs;
+ }
+
+ function mockType() {
+ return "mockType";
+ }
+
+ function mockNewEs() {
+ return new upshot.EntitySet(mockDs(), mockType());
+ }
+
+ test("EntitySource Bind event", 2, function () {
+
+ var es = mockNewEs(),
+ event = "dummyevent",
+ eventArg = "dummyarg";
+
+ es.bind(event, function() {
+ equal(this, es, "Context matched");
+ equal(arguments[0], eventArg, "arg matched");
+ })._trigger(event, eventArg);
+ });
+
+ test("EntitySource Unbind event", 0, function () {
+
+ var es = mockNewEs(),
+ event = "dummyevent",
+ eventArg = "dummyarg",
+ eventCallback = function() {
+ ok(false, "should not callback unbind event");
+ };
+
+ es.bind(event, eventCallback).unbind(event, eventCallback)._trigger(event, eventArg);
+ });
+
+ test("DataContext Bind event", 2, function () {
+
+ var es = upshot.DataContext("unused"),
+ event = "dummyevent",
+ eventArg = "dummyarg";
+
+ es.bind(event, function() {
+ equal(this, es, "Context matched");
+ equal(arguments[0], eventArg, "arg matched");
+ })._trigger(event, eventArg);
+ });
+
+ test("DataContext Unbind event", 0, function () {
+
+ var es = upshot.DataContext("unused"),
+ event = "dummyevent",
+ eventArg = "dummyarg",
+ eventCallback = function() {
+ ok(false, "should not callback unbind event");
+ };
+
+ es.bind(event, eventCallback).unbind(event, eventCallback)._trigger(event, eventArg);
+ });
+
+})(this, upshot);
diff --git a/test/SPA.Test/upshot/jQuery.DataView.Tests.js b/test/SPA.Test/upshot/jQuery.DataView.Tests.js
new file mode 100644
index 00000000..550f4c80
--- /dev/null
+++ b/test/SPA.Test/upshot/jQuery.DataView.Tests.js
@@ -0,0 +1,287 @@
+/// <reference path="../Scripts/References.js" />
+(function (global, upshot, undefined) {
+
+ module("jQuery.dataview.Tests.js", {
+ teardown: function () {
+ testHelper.unmockAjax();
+ }
+ });
+
+ // refreshStart
+ test("refreshStart ctor", 3, function () {
+ stop();
+ dsTestDriver.simulateSuccessService();
+ dsTestDriver.ds = $.upshot.remoteDataview({
+ providerParameters: { url: "unused" },
+ provider: upshot.riaDataProvider,
+ refreshStart: dsTestDriver.onRefreshStart
+ });
+ dsTestDriver.ds.refresh(function () { start(); });
+ });
+
+ test("refreshStart bind", 4, function () {
+ stop();
+ dsTestDriver.simulateSuccessService();
+ dsTestDriver.ds = $.upshot.remoteDataview({ providerParameters: { url: "unused"}, provider: upshot.riaDataProvider });
+ $(dsTestDriver.ds).bind("refreshStart", dsTestDriver.onRefreshStartEvent);
+ dsTestDriver.ds.refresh(function () { start(); });
+ });
+
+ // refreshSuccess
+ test("refreshSuccess ctor", 6, function () {
+ stop();
+ dsTestDriver.simulateSuccessService();
+ dsTestDriver.ds = $.upshot.remoteDataview({
+ providerParameters: { url: "unused" },
+ provider: upshot.riaDataProvider,
+ refreshSuccess: dsTestDriver.onRefreshSuccess
+ });
+ dsTestDriver.ds.refresh();
+ });
+
+ test("refreshSuccess refresh simplest option", 6, function () {
+ stop();
+ dsTestDriver.simulateSuccessService();
+ dsTestDriver.ds = $.upshot.remoteDataview({ providerParameters: { url: "unused"}, provider: upshot.riaDataProvider, })
+ .refresh(dsTestDriver.onRefreshSuccess);
+ });
+
+ test("refreshSuccess bind", 7, function () {
+ stop();
+ dsTestDriver.simulateSuccessService();
+ dsTestDriver.ds = $.upshot.remoteDataview({ providerParameters: { url: "unused"}, provider: upshot.riaDataProvider, });
+ $(dsTestDriver.ds).one("refreshSuccess", dsTestDriver.onRefreshSuccessEvent);
+ dsTestDriver.ds.refresh();
+ });
+
+ test("refreshSuccess localDataview ctor", 6, function () {
+ stop();
+ dsTestDriver.simulateSuccessService();
+ dsTestDriver.ds = $.upshot.remoteDataview({
+ providerParameters: { url: "unused" },
+ provider: upshot.riaDataProvider,
+ refreshSuccess: function () {
+ dsTestDriver.ds = $.upshot.localDataview({
+ input: this,
+ paging: { limit: 2 },
+ refreshSuccess: dsTestDriver.onRefreshSuccess
+ });
+ setTimeout(function () { dsTestDriver.ds.refresh(); }, 10);
+ }
+ });
+ dsTestDriver.ds.refresh();
+ });
+
+ test("refreshSuccess localDataview refresh simplest option", 6, function () {
+ stop();
+ dsTestDriver.simulateSuccessService();
+ dsTestDriver.ds = $.upshot.remoteDataview({ providerParameters: { url: "unused"}, provider: upshot.riaDataProvider })
+ .refresh(function () {
+ dsTestDriver.ds = $.upshot.localDataview({
+ input: this,
+ paging: { limit: 2 }
+ });
+ setTimeout(function () { dsTestDriver.ds.refresh(dsTestDriver.onRefreshSuccess); }, 10);
+ });
+ });
+
+ test("refreshSuccess localDataview bind", 7, function () {
+ stop();
+ dsTestDriver.simulateSuccessService();
+ dsTestDriver.ds = $.upshot.remoteDataview({ providerParameters: { url: "unused"}, provider: upshot.riaDataProvider, });
+ $(dsTestDriver.ds).one("refreshSuccess", function () {
+ dsTestDriver.ds = $.upshot.localDataview({
+ input: this,
+ paging: { limit: 1 }
+ });
+ $(dsTestDriver.ds).one("refreshSuccess", dsTestDriver.onRefreshSuccessEvent);
+ setTimeout(function () { dsTestDriver.ds.refresh(); }, 10);
+ });
+ dsTestDriver.ds.refresh();
+ });
+
+ // refreshError
+ test("refreshError ctor", 5, function () {
+ stop();
+ dsTestDriver.simulateErrorService();
+ dsTestDriver.ds = $.upshot.remoteDataview({
+ providerParameters: { url: "unused" },
+ provider: upshot.riaDataProvider,
+ refreshError: dsTestDriver.onRefreshError
+ }).refresh();
+ });
+
+ test("refreshError refresh simplest option", 5, function () {
+ stop();
+ dsTestDriver.simulateErrorService();
+ dsTestDriver.ds = $.upshot.remoteDataview({ providerParameters: { url: "unused"}, provider: upshot.riaDataProvider, });
+ dsTestDriver.ds.refresh(null, dsTestDriver.onRefreshError);
+ });
+
+ test("refreshError bind", 6, function () {
+ stop();
+ dsTestDriver.simulateErrorService();
+ dsTestDriver.ds = $.upshot.remoteDataview({ providerParameters: { url: "unused"}, provider: upshot.riaDataProvider });
+ $(dsTestDriver.ds).one("refreshError", dsTestDriver.onRefreshErrorEvent);
+ dsTestDriver.ds.refresh();
+ });
+
+ // commitStart
+ test("commitStart ctor", 4, function () {
+ stop();
+ dsTestDriver.simulateSuccessService();
+ var products = [];
+ dsTestDriver.ds = $.upshot.remoteDataview({
+ providerParameters: { url: "unused" },
+ provider: upshot.riaDataProvider,
+ commitStart: dsTestDriver.onCommitStart,
+ result: products,
+ commitSuccess: function () { start(); }
+ }).refresh(function () {
+ equal(products.length, dsTestDriver.productsResult.GetProductsResult.TotalCount, "Count checked");
+ dsTestDriver.simulatePostSuccessService();
+ $.observable(products[0]).property("Price", products[0].Price + 100);
+ });
+ });
+
+ test("commitStart bind ctor", 5, function () {
+ stop();
+ dsTestDriver.simulateSuccessService();
+ var products = [];
+ dsTestDriver.ds = $.upshot.remoteDataview({
+ providerParameters: { url: "unused" },
+ provider: upshot.riaDataProvider,
+ result: products,
+ commitSuccess: function () { start(); }
+ });
+ $(dsTestDriver.ds).bind("commitStart", dsTestDriver.onCommitStartEvent);
+ dsTestDriver.ds.refresh(function () {
+ equal(products.length, dsTestDriver.productsResult.GetProductsResult.TotalCount, "Count checked");
+ dsTestDriver.simulatePostSuccessService();
+ $.observable(products[0]).property("Price", products[0].Price + 100);
+ });
+ });
+
+ // commitSuccess
+ test("commitSuccess ctor", 6, function () {
+ stop();
+ dsTestDriver.simulateSuccessService();
+ var products = [];
+ dsTestDriver.ds = $.upshot.remoteDataview({
+ providerParameters: { url: "unused" },
+ provider: upshot.riaDataProvider,
+ result: products,
+ commitSuccess: dsTestDriver.onCommitSuccess
+ }).refresh(function () {
+ equal(products.length, dsTestDriver.productsResult.GetProductsResult.TotalCount, "Count checked");
+ dsTestDriver.simulatePostSuccessService();
+ $.observable(products[0]).property("Price", products[0].Price + 100);
+ });
+ });
+
+ test("commitSuccess commit callback", 6, function () {
+ stop();
+ dsTestDriver.simulateSuccessService();
+ var products = [];
+ dsTestDriver.ds = $.upshot.remoteDataview({
+ providerParameters: { url: "unused" },
+ provider: upshot.riaDataProvider,
+ result: products,
+ bufferChanges: true
+ });
+ dsTestDriver.ds.refresh(function () {
+ equal(products.length, dsTestDriver.productsResult.GetProductsResult.TotalCount, "Count checked");
+ dsTestDriver.simulatePostSuccessService();
+ $.observable(products[0]).property("Price", products[0].Price + 100);
+ this.commitChanges(dsTestDriver.onCommitSuccess, dsTestDriver.onCommitError);
+ });
+ });
+
+ test("commitSuccess bind", 7, function () {
+ stop();
+ dsTestDriver.simulateSuccessService();
+ var products = [];
+ dsTestDriver.ds = $.upshot.remoteDataview({
+ providerParameters: { url: "unused" },
+ provider: upshot.riaDataProvider,
+ result: products
+ });
+ $(dsTestDriver.ds).bind("commitSuccess", dsTestDriver.onCommitSuccessEvent);
+ dsTestDriver.ds.refresh(function () {
+ equal(products.length, dsTestDriver.productsResult.GetProductsResult.TotalCount, "Count checked");
+ dsTestDriver.simulatePostSuccessService();
+ $.observable(products[0]).property("Price", products[0].Price + 100);
+ });
+ });
+
+ // commitError
+ test("commitError ctor", 6, function () {
+ stop();
+ dsTestDriver.simulateSuccessService();
+ var products = [];
+ dsTestDriver.ds = $.upshot.remoteDataview({
+ providerParameters: { url: "unused" },
+ provider: upshot.riaDataProvider,
+ result: products,
+ commitError: dsTestDriver.onCommitError
+ }).refresh(function () {
+ equal(products.length, dsTestDriver.productsResult.GetProductsResult.TotalCount, "Count checked");
+ dsTestDriver.simulateErrorService();
+ $.observable(products[0]).property("Price", products[0].Price + 100);
+ });
+ });
+
+ test("commitError commit callback", 6, function () {
+ stop();
+ dsTestDriver.simulateSuccessService();
+ var products = [];
+ dsTestDriver.ds = $.upshot.remoteDataview({
+ providerParameters: { url: "unused" },
+ provider: upshot.riaDataProvider,
+ result: products,
+ bufferChanges: true
+ });
+ dsTestDriver.ds.refresh(function () {
+ equal(products.length, dsTestDriver.productsResult.GetProductsResult.TotalCount, "Count checked");
+ dsTestDriver.simulateErrorService();
+ $.observable(products[0]).property("Price", products[0].Price + 100);
+ this.commitChanges(dsTestDriver.onCommitSuccess, dsTestDriver.onCommitError);
+ });
+ });
+
+ test("commitError bind", 7, function () {
+ stop();
+ dsTestDriver.simulateSuccessService();
+ var products = [];
+ dsTestDriver.ds = $.upshot.remoteDataview({
+ providerParameters: { url: "unused" },
+ provider: upshot.riaDataProvider,
+ result: products
+ });
+ $(dsTestDriver.ds).bind("commitError", dsTestDriver.onCommitErrorEvent);
+ dsTestDriver.ds.refresh(function () {
+ equal(products.length, dsTestDriver.productsResult.GetProductsResult.TotalCount, "Count checked");
+ dsTestDriver.simulateErrorService();
+ $.observable(products[0]).property("Price", products[0].Price + 100);
+ });
+ });
+
+ test("commitValidationError commit callback", 6, function () {
+ stop();
+ dsTestDriver.simulateSuccessService();
+ var products = [];
+ dsTestDriver.ds = $.upshot.remoteDataview({
+ providerParameters: { url: "unused" },
+ provider: upshot.riaDataProvider,
+ result: products,
+ bufferChanges: true
+ });
+ dsTestDriver.ds.refresh(function () {
+ equal(products.length, dsTestDriver.productsResult.GetProductsResult.TotalCount, "Count checked");
+ dsTestDriver.simulateValidationErrorService();
+ $.observable(products).insert(0, { ID: 4, Manufacturer: "Kodak", Price: 800 });
+ this.commitChanges(dsTestDriver.onCommitSuccess, dsTestDriver.onCommitValidationError);
+ });
+ });
+
+})(this, upshot);
diff --git a/test/Settings.StyleCop b/test/Settings.StyleCop
new file mode 100644
index 00000000..d65b20f9
--- /dev/null
+++ b/test/Settings.StyleCop
@@ -0,0 +1,145 @@
+<StyleCopSettings Version="4.3">
+ <GlobalSettings>
+ <BooleanProperty Name="RulesEnabledByDefault">False</BooleanProperty>
+ <StringProperty Name="MergeSettingsFiles">NoMerge</StringProperty>
+ </GlobalSettings>
+ <Analyzers>
+
+ <Analyzer AnalyzerId="Microsoft.StyleCop.CSharp.NamingRules">
+ <AnalyzerSettings>
+ <CollectionProperty Name="Hungarian">
+ <Value>as</Value>
+ <Value>db</Value>
+ <Value>dc</Value>
+ <Value>do</Value>
+ <Value>ef</Value>
+ <Value>id</Value>
+ <Value>if</Value>
+ <Value>in</Value>
+ <Value>is</Value>
+ <Value>my</Value>
+ <Value>no</Value>
+ <Value>on</Value>
+ <Value>sl</Value>
+ <Value>to</Value>
+ <Value>ui</Value>
+ <Value>vs</Value>
+ </CollectionProperty>
+ </AnalyzerSettings>
+ </Analyzer>
+
+ <Analyzer AnalyzerId="Microsoft.StyleCop.CSharp.DocumentationRules">
+ <AnalyzerSettings>
+ <BooleanProperty Name="IgnorePrivates">True</BooleanProperty>
+ <BooleanProperty Name="IgnoreInternals">True</BooleanProperty>
+ <BooleanProperty Name="IncludeFields">False</BooleanProperty>
+
+ <StringProperty Name="Copyright">Copyright (c) Microsoft Corporation. All rights reserved.</StringProperty>
+ </AnalyzerSettings>
+
+ <Rules>
+ <!-- For non-product code, do not require enum items to be documented. -->
+ <Rule Name="EnumerationItemsMustBeDocumented">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+
+ <!-- For non-product code, do not require specific wording of property and indexer summary. -->
+ <Rule Name="PropertySummaryDocumentationMustMatchAccessors">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="PropertySummaryDocumentationMustOmitSetAccessorWithRestrictedAccess">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+
+ <!-- For non-product code, do not require specific wording of constuctor and destructor summary. -->
+ <Rule Name="ConstructorSummaryDocumentationMustBeginWithStandardText">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="DestructorSummaryDocumentationMustBeginWithStandardText">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+
+ <!-- Documentation headers can contain blank lines, since they are not directly consumed for external documentation. -->
+ <Rule Name="DocumentationHeadersMustNotContainBlankLines">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+
+ <!-- Do not require the file header to contain the name of the file. -->
+ <Rule Name="FileHeaderMustContainFileName">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="FileHeaderFileNameDocumentationMustMatchFileName">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+
+ <!-- Do not require the file header to contain a Company attribute. -->
+ <Rule Name="FileHeaderMustHaveValidCompanyText">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ </Rules>
+ </Analyzer>
+
+ <Analyzer AnalyzerId="Microsoft.StyleCop.CSharp.OrderingRules">
+ <!-- For non-product code, do not enforce specific ordering of elements. -->
+ <Rules>
+ <Rule Name="ElementsMustBeOrderedByAccess">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="StaticElementsMustAppearBeforeInstanceElements">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ </Rules>
+ <AnalyzerSettings />
+ </Analyzer>
+
+ <Analyzer AnalyzerId="Microsoft.StyleCop.CSharp.MaintainabilityRules">
+ <Rules>
+ <!-- For non-product code, allow multiple classes and namespaces within a file. -->
+ <Rule Name="FileMayOnlyContainASingleClass">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ <Rule Name="FileMayOnlyContainASingleNamespace">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ </Rules>
+ </Analyzer>
+
+ <Analyzer AnalyzerId="Microsoft.StyleCop.CSharp.ReadabilityRules">
+ <Rules>
+ <!-- Per ADP guidelines, method parameter are allowed to span across multiple lines (rather than having to be assigned to a temporary variable). -->
+ <Rule Name="ParameterMustNotSpanMultipleLines">
+ <RuleSettings>
+ <BooleanProperty Name="Enabled">False</BooleanProperty>
+ </RuleSettings>
+ </Rule>
+ </Rules>
+ </Analyzer>
+
+ </Analyzers>
+</StyleCopSettings>
diff --git a/test/System.Json.Test.Integration/Common/InstanceCreator.cs b/test/System.Json.Test.Integration/Common/InstanceCreator.cs
new file mode 100644
index 00000000..80d25e7c
--- /dev/null
+++ b/test/System.Json.Test.Integration/Common/InstanceCreator.cs
@@ -0,0 +1,1585 @@
+using System.Collections.Generic;
+using System.Reflection;
+using System.Runtime.Serialization;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace System.Json
+{
+ /// <summary>
+ /// Settings used by the <see cref="InstanceCreator"/> class.
+ /// </summary>
+ public static class CreatorSettings
+ {
+ static CreatorSettings()
+ {
+ MaxArrayLength = 10;
+ MaxListLength = 10;
+ MaxStringLength = 100;
+ CreateOnlyAsciiChars = false;
+ DontCreateSurrogateChars = false;
+ CreateDateTimeWithSubMilliseconds = true;
+ NullValueProbability = 0.01;
+ AvoidStackOverflowDueToTypeCycles = false;
+ CreatorSurrogate = null;
+ }
+
+ /// <summary>
+ /// Gets or sets the maximum length of arrays created by the <see cref="InstanceCreator"/>.
+ /// </summary>
+ public static int MaxArrayLength { get; set; }
+
+ /// <summary>
+ /// Gets or sets the maximum length of lists created by the <see cref="InstanceCreator"/>.
+ /// </summary>
+ public static int MaxListLength { get; set; }
+
+ /// <summary>
+ /// Gets or sets the maximum length of strings created by the <see cref="InstanceCreator"/>.
+ /// </summary>
+ public static int MaxStringLength { get; set; }
+
+ /// <summary>
+ /// Gets or sets a flag indicating whether only ascii chars should be used when creating strings.
+ /// </summary>
+ public static bool CreateOnlyAsciiChars { get; set; }
+
+ /// <summary>
+ /// Gets or sets a flag indicating whether chars in the surrogate range can be returned by the
+ /// <see cref="InstanceCreator"/> when creating char instances.
+ /// </summary>
+ public static bool DontCreateSurrogateChars { get; set; }
+
+ /// <summary>
+ /// Gets or sets a flag indicating whether <see cref="DateTime"/> values created by the
+ /// <see cref="InstanceCreator"/> can have submillisecond precision.
+ /// </summary>
+ public static bool CreateDateTimeWithSubMilliseconds { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value (0-1) indicating the probability of the <see cref="InstanceCreator"/>
+ /// returning a <code>null</code> value when creating instances of class types.
+ /// </summary>
+ public static double NullValueProbability { get; set; }
+
+ /// <summary>
+ /// Gets or sets a flag indicating whether the protection against stack overflow
+ /// for cyclic types is enabled. If this flag is set, whenever a type which has already
+ /// been created up in the stack is created again, the <see cref="InstanceCreator"/>
+ /// will return the default value for that type.
+ /// </summary>
+ public static bool AvoidStackOverflowDueToTypeCycles { get; set; }
+
+ /// <summary>
+ /// Gets or sets the instance of an <see cref="InstanceCreatorSurrogate"/> which can intercept
+ /// requests to create instances on the <see cref="InstanceCreator"/>.
+ /// </summary>
+ public static InstanceCreatorSurrogate CreatorSurrogate { get; set; }
+ }
+
+ /// <summary>
+ /// Utility class used to create test instances of primitive types.
+ /// </summary>
+ public static class PrimitiveCreator
+ {
+ static readonly Regex RelativeIPv6UriRegex = new Regex(@"^\/\/(.+\@)?\[\:\:\d\]");
+ static Dictionary<Type, MethodInfo> creators;
+
+ static PrimitiveCreator()
+ {
+ Type primitiveCreatorType = typeof(PrimitiveCreator);
+ creators = new Dictionary<Type, MethodInfo>();
+ creators.Add(typeof(bool), primitiveCreatorType.GetMethod("CreateInstanceOfBoolean", BindingFlags.Public | BindingFlags.Static));
+ creators.Add(typeof(byte), primitiveCreatorType.GetMethod("CreateInstanceOfByte", BindingFlags.Public | BindingFlags.Static));
+ creators.Add(typeof(char), primitiveCreatorType.GetMethod("CreateInstanceOfChar", BindingFlags.Public | BindingFlags.Static));
+ creators.Add(typeof(DateTime), primitiveCreatorType.GetMethod("CreateInstanceOfDateTime", BindingFlags.Public | BindingFlags.Static));
+ creators.Add(typeof(DateTimeOffset), primitiveCreatorType.GetMethod("CreateInstanceOfDateTimeOffset", BindingFlags.Public | BindingFlags.Static));
+ creators.Add(typeof(decimal), primitiveCreatorType.GetMethod("CreateInstanceOfDecimal", BindingFlags.Public | BindingFlags.Static));
+ creators.Add(typeof(double), primitiveCreatorType.GetMethod("CreateInstanceOfDouble", BindingFlags.Public | BindingFlags.Static));
+ creators.Add(typeof(Guid), primitiveCreatorType.GetMethod("CreateInstanceOfGuid", BindingFlags.Public | BindingFlags.Static));
+ creators.Add(typeof(short), primitiveCreatorType.GetMethod("CreateInstanceOfInt16", BindingFlags.Public | BindingFlags.Static));
+ creators.Add(typeof(int), primitiveCreatorType.GetMethod("CreateInstanceOfInt32", BindingFlags.Public | BindingFlags.Static));
+ creators.Add(typeof(long), primitiveCreatorType.GetMethod("CreateInstanceOfInt64", BindingFlags.Public | BindingFlags.Static));
+ creators.Add(typeof(object), primitiveCreatorType.GetMethod("CreateInstanceOfObject", BindingFlags.Public | BindingFlags.Static));
+ creators.Add(typeof(sbyte), primitiveCreatorType.GetMethod("CreateInstanceOfSByte", BindingFlags.Public | BindingFlags.Static));
+ creators.Add(typeof(float), primitiveCreatorType.GetMethod("CreateInstanceOfSingle", BindingFlags.Public | BindingFlags.Static));
+ creators.Add(typeof(string), primitiveCreatorType.GetMethod("CreateInstanceOfString", BindingFlags.Public | BindingFlags.Static, null, new Type[] { typeof(Random) }, null));
+ creators.Add(typeof(ushort), primitiveCreatorType.GetMethod("CreateInstanceOfUInt16", BindingFlags.Public | BindingFlags.Static));
+ creators.Add(typeof(uint), primitiveCreatorType.GetMethod("CreateInstanceOfUInt32", BindingFlags.Public | BindingFlags.Static));
+ creators.Add(typeof(ulong), primitiveCreatorType.GetMethod("CreateInstanceOfUInt64", BindingFlags.Public | BindingFlags.Static));
+ creators.Add(typeof(Uri), primitiveCreatorType.GetMethod("CreateInstanceOfUri", BindingFlags.Public | BindingFlags.Static));
+ }
+
+ /// <summary>
+ /// Creates an instance of the <see cref="Boolean"/> type.
+ /// </summary>
+ /// <param name="rndGen">A <see cref="Random"/> used to create the instance.</param>
+ /// <returns>An instance of the <see cref="Boolean"/> type.</returns>
+ public static bool CreateInstanceOfBoolean(Random rndGen)
+ {
+ return rndGen.Next(2) == 0;
+ }
+
+ /// <summary>
+ /// Creates an instance of the <see cref="Byte"/> type.
+ /// </summary>
+ /// <param name="rndGen">A <see cref="Random"/> used to create the instance.</param>
+ /// <returns>An instance of the <see cref="Byte"/> type.</returns>
+ public static byte CreateInstanceOfByte(Random rndGen)
+ {
+ byte[] rndValue = new byte[1];
+ rndGen.NextBytes(rndValue);
+ return rndValue[0];
+ }
+
+ /// <summary>
+ /// Creates an instance of the <see cref="Char"/> type.
+ /// </summary>
+ /// <param name="rndGen">A <see cref="Random"/> used to create the instance.</param>
+ /// <returns>An instance of the <see cref="Char"/> type.</returns>
+ public static char CreateInstanceOfChar(Random rndGen)
+ {
+ if (CreatorSettings.CreateOnlyAsciiChars)
+ {
+ return (char)rndGen.Next(0x20, 0x7F);
+ }
+ else if (CreatorSettings.DontCreateSurrogateChars)
+ {
+ char c;
+ do
+ {
+ c = (char)rndGen.Next((int)Char.MinValue, (int)Char.MaxValue);
+ }
+ while (Char.IsSurrogate(c));
+ return c;
+ }
+ else
+ {
+ return (char)rndGen.Next((int)Char.MinValue, (int)Char.MaxValue + 1);
+ }
+ }
+
+ /// <summary>
+ /// Creates an instance of the <see cref="DateTime"/> type.
+ /// </summary>
+ /// <param name="rndGen">A <see cref="Random"/> used to create the instance.</param>
+ /// <returns>An instance of the <see cref="DateTime"/> type.</returns>
+ public static System.DateTime CreateInstanceOfDateTime(Random rndGen)
+ {
+ long temp = CreateInstanceOfInt64(rndGen);
+ temp = Math.Abs(temp);
+ DateTime result;
+ try
+ {
+ result = new DateTime(temp % (DateTime.MaxValue.Ticks + 1));
+ }
+ catch (ArgumentOutOfRangeException)
+ {
+ result = DateTime.Now;
+ }
+
+ int kind = rndGen.Next(3);
+ switch (kind)
+ {
+ case 0:
+ result = DateTime.SpecifyKind(result, DateTimeKind.Local);
+ break;
+ case 1:
+ result = DateTime.SpecifyKind(result, DateTimeKind.Unspecified);
+ break;
+ default:
+ result = DateTime.SpecifyKind(result, DateTimeKind.Utc);
+ break;
+ }
+
+ if (!CreatorSettings.CreateDateTimeWithSubMilliseconds)
+ {
+ result = new DateTime(
+ result.Year,
+ result.Month,
+ result.Day,
+ result.Hour,
+ result.Minute,
+ result.Second,
+ result.Millisecond,
+ result.Kind);
+ }
+
+ return result;
+ }
+
+ /// <summary>
+ /// Creates an instance of the <see cref="DateTimeOffset"/> type.
+ /// </summary>
+ /// <param name="rndGen">A <see cref="Random"/> used to create the instance.</param>
+ /// <returns>An instance of the <see cref="DateTimeOffset"/> type.</returns>
+ public static System.DateTimeOffset CreateInstanceOfDateTimeOffset(Random rndGen)
+ {
+ DateTime temp = CreateInstanceOfDateTime(rndGen);
+ temp = DateTime.SpecifyKind(temp, DateTimeKind.Unspecified);
+ int offsetMinutes = rndGen.Next(-14 * 60, 14 * 60);
+ DateTimeOffset result = new DateTimeOffset(temp, TimeSpan.FromMinutes(offsetMinutes));
+ return result;
+ }
+
+ /// <summary>
+ /// Creates an instance of the <see cref="Decimal"/> type.
+ /// </summary>
+ /// <param name="rndGen">A <see cref="Random"/> used to create the instance.</param>
+ /// <returns>An instance of the <see cref="Decimal"/> type.</returns>
+ public static decimal CreateInstanceOfDecimal(Random rndGen)
+ {
+ int low = CreateInstanceOfInt32(rndGen);
+ int mid = CreateInstanceOfInt32(rndGen);
+ int high = CreateInstanceOfInt32(rndGen);
+ bool isNegative = rndGen.Next(2) == 0;
+ const int MaxDecimalScale = 28;
+ byte scale = (byte)rndGen.Next(0, MaxDecimalScale + 1);
+ return new decimal(low, mid, high, isNegative, scale);
+ }
+
+ /// <summary>
+ /// Creates an instance of the <see cref="Double"/> type.
+ /// </summary>
+ /// <param name="rndGen">A <see cref="Random"/> used to create the instance.</param>
+ /// <returns>An instance of the <see cref="Double"/> type.</returns>
+ public static double CreateInstanceOfDouble(Random rndGen)
+ {
+ bool negative = rndGen.Next(2) == 0;
+ int temp = rndGen.Next(40);
+ double result;
+ switch (temp)
+ {
+ case 0: return Double.NaN;
+ case 1: return Double.PositiveInfinity;
+ case 2: return Double.NegativeInfinity;
+ case 3: return Double.MinValue;
+ case 4: return Double.MaxValue;
+ case 5: return Double.Epsilon;
+ default:
+ result = (double)(rndGen.NextDouble() * 100000);
+ if (negative)
+ {
+ result = -result;
+ }
+
+ return result;
+ }
+ }
+
+ /// <summary>
+ /// Creates an instance of the <see cref="Guid"/> type.
+ /// </summary>
+ /// <param name="rndGen">A <see cref="Random"/> used to create the instance.</param>
+ /// <returns>An instance of the <see cref="Guid"/> type.</returns>
+ public static System.Guid CreateInstanceOfGuid(Random rndGen)
+ {
+ byte[] temp = new byte[16];
+ rndGen.NextBytes(temp);
+ return new Guid(temp);
+ }
+
+ /// <summary>
+ /// Creates an instance of the <see cref="Int16"/> type.
+ /// </summary>
+ /// <param name="rndGen">A <see cref="Random"/> used to create the instance.</param>
+ /// <returns>An instance of the <see cref="Int16"/> type.</returns>
+ public static short CreateInstanceOfInt16(Random rndGen)
+ {
+ byte[] rndValue = new byte[2];
+ rndGen.NextBytes(rndValue);
+ short result = 0;
+ for (int i = 0; i < rndValue.Length; i++)
+ {
+ result = (short)(result << 8);
+ result = (short)(result | (short)rndValue[i]);
+ }
+
+ return result;
+ }
+
+ /// <summary>
+ /// Creates an instance of the <see cref="Int32"/> type.
+ /// </summary>
+ /// <param name="rndGen">A <see cref="Random"/> used to create the instance.</param>
+ /// <returns>An instance of the <see cref="Int32"/> type.</returns>
+ public static int CreateInstanceOfInt32(Random rndGen)
+ {
+ byte[] rndValue = new byte[4];
+ rndGen.NextBytes(rndValue);
+ int result = 0;
+ for (int i = 0; i < rndValue.Length; i++)
+ {
+ result = (int)(result << 8);
+ result = (int)(result | (int)rndValue[i]);
+ }
+
+ return result;
+ }
+
+ /// <summary>
+ /// Creates an instance of the <see cref="Int64"/> type.
+ /// </summary>
+ /// <param name="rndGen">A <see cref="Random"/> used to create the instance.</param>
+ /// <returns>An instance of the <see cref="Int64"/> type.</returns>
+ public static long CreateInstanceOfInt64(Random rndGen)
+ {
+ byte[] rndValue = new byte[8];
+ rndGen.NextBytes(rndValue);
+ long result = 0;
+ for (int i = 0; i < rndValue.Length; i++)
+ {
+ result = (long)(result << 8);
+ result = (long)(result | (long)rndValue[i]);
+ }
+
+ return result;
+ }
+
+ /// <summary>
+ /// Creates an instance of the <see cref="Object"/> type.
+ /// </summary>
+ /// <param name="rndGen">A <see cref="Random"/> used to create the instance.</param>
+ /// <returns>An instance of the <see cref="Object"/> type.</returns>
+ public static object CreateInstanceOfObject(Random rndGen)
+ {
+ return (rndGen.Next(5) == 0) ? null : new object();
+ }
+
+ /// <summary>
+ /// Creates an instance of the <see cref="SByte"/> type.
+ /// </summary>
+ /// <param name="rndGen">A <see cref="Random"/> used to create the instance.</param>
+ /// <returns>An instance of the <see cref="SByte"/> type.</returns>
+ [CLSCompliant(false)]
+ public static sbyte CreateInstanceOfSByte(Random rndGen)
+ {
+ byte[] rndValue = new byte[1];
+ rndGen.NextBytes(rndValue);
+ sbyte result = (sbyte)rndValue[0];
+ return result;
+ }
+
+ /// <summary>
+ /// Creates an instance of the <see cref="Single"/> type.
+ /// </summary>
+ /// <param name="rndGen">A <see cref="Random"/> used to create the instance.</param>
+ /// <returns>An instance of the <see cref="Single"/> type.</returns>
+ public static float CreateInstanceOfSingle(Random rndGen)
+ {
+ bool negative = rndGen.Next(2) == 0;
+ int temp = rndGen.Next(40);
+ float result;
+ switch (temp)
+ {
+ case 0: return Single.NaN;
+ case 1: return Single.PositiveInfinity;
+ case 2: return Single.NegativeInfinity;
+ case 3: return Single.MinValue;
+ case 4: return Single.MaxValue;
+ case 5: return Single.Epsilon;
+ default:
+ result = (float)(rndGen.NextDouble() * 100000);
+ if (negative)
+ {
+ result = -result;
+ }
+
+ return result;
+ }
+ }
+
+ /// <summary>
+ /// Creates an instance of the <see cref="String"/> type.
+ /// </summary>
+ /// <param name="rndGen">A <see cref="Random"/> used to create the instance.</param>
+ /// <param name="size">The size of the string to be creted.</param>
+ /// <param name="charsToUse">The characters to use when creating the string.</param>
+ /// <returns>An instance of the <see cref="String"/> type.</returns>
+ public static string CreateRandomString(Random rndGen, int size, string charsToUse)
+ {
+ int maxSize = CreatorSettings.MaxStringLength;
+
+ // invalid per the XML spec (http://www.w3.org/TR/REC-xml/#charsets), cannot be sent as XML
+ string invalidXmlChars = "\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\u0008\u000B\u000C\u000E\u000F\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001A\u001B\u001C\u001D\u001E\u001F\uFFFE\uFFFF";
+
+ const int LowSurrogateMin = 0xDC00;
+ const int LowSurrogateMax = 0xDFFF;
+ const int HighSurrogateMin = 0xD800;
+ const int HighSurrogateMax = 0xDBFF;
+
+ if (size < 0)
+ {
+ double rndNumber = rndGen.NextDouble();
+ if (rndNumber < CreatorSettings.NullValueProbability)
+ {
+ return null; // 1% chance of null value
+ }
+
+ size = (int)Math.Pow(maxSize, rndNumber); // this will create more small strings than large ones
+ size--;
+ }
+
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < size; i++)
+ {
+ char c;
+ if (charsToUse != null)
+ {
+ c = charsToUse[rndGen.Next(charsToUse.Length)];
+ sb.Append(c);
+ }
+ else
+ {
+ if (CreatorSettings.CreateOnlyAsciiChars || rndGen.Next(2) == 0)
+ {
+ c = (char)rndGen.Next(0x20, 0x7F); // low-ascii chars
+ sb.Append(c);
+ }
+ else
+ {
+ do
+ {
+ c = (char)rndGen.Next((int)Char.MinValue, (int)Char.MaxValue + 1);
+ }
+ while ((LowSurrogateMin <= c && c <= LowSurrogateMax) || (invalidXmlChars.IndexOf(c) >= 0));
+
+ sb.Append(c);
+ if (HighSurrogateMin <= c && c <= HighSurrogateMax)
+ {
+ // need to add a low surrogate
+ c = (char)rndGen.Next(LowSurrogateMin, LowSurrogateMax + 1);
+ sb.Append(c);
+ }
+ }
+ }
+ }
+
+ return sb.ToString();
+ }
+
+ /// <summary>
+ /// Creates an instance of the <see cref="String"/> type.
+ /// </summary>
+ /// <param name="rndGen">A <see cref="Random"/> used to create the instance.</param>
+ /// <returns>An instance of the <see cref="String"/> type.</returns>
+ public static string CreateInstanceOfString(Random rndGen)
+ {
+ return CreateInstanceOfString(rndGen, true);
+ }
+
+ /// <summary>
+ /// Creates an instance of the <see cref="String"/> type.
+ /// </summary>
+ /// <param name="rndGen">A <see cref="Random"/> used to create the instance.</param>
+ /// <param name="allowNull">A flag indicating whether null values can be returned.</param>
+ /// <returns>An instance of the <see cref="String"/> type.</returns>
+ public static string CreateInstanceOfString(Random rndGen, bool allowNull)
+ {
+ string result;
+ do
+ {
+ result = CreateRandomString(rndGen, -1, null);
+ }
+ while (result == null && !allowNull);
+
+ return result;
+ }
+
+ /// <summary>
+ /// Creates an instance of the <see cref="String"/> type.
+ /// </summary>
+ /// <param name="rndGen">A <see cref="Random"/> used to create the instance.</param>
+ /// <param name="size">The size of the string to be creted.</param>
+ /// <param name="charsToUse">The characters to use when creating the string.</param>
+ /// <returns>An instance of the <see cref="String"/> type.</returns>
+ public static string CreateInstanceOfString(Random rndGen, int size, string charsToUse)
+ {
+ return CreateRandomString(rndGen, size, charsToUse);
+ }
+
+ /// <summary>
+ /// Creates an instance of the <see cref="UInt16"/> type.
+ /// </summary>
+ /// <param name="rndGen">A <see cref="Random"/> used to create the instance.</param>
+ /// <returns>An instance of the <see cref="UInt16"/> type.</returns>
+ [CLSCompliant(false)]
+ public static ushort CreateInstanceOfUInt16(Random rndGen)
+ {
+ byte[] rndValue = new byte[2];
+ rndGen.NextBytes(rndValue);
+ ushort result = 0;
+ for (int i = 0; i < rndValue.Length; i++)
+ {
+ result = (ushort)(result << 8);
+ result = (ushort)(result | (ushort)rndValue[i]);
+ }
+
+ return result;
+ }
+
+ /// <summary>
+ /// Creates an instance of the <see cref="UInt32"/> type.
+ /// </summary>
+ /// <param name="rndGen">A <see cref="Random"/> used to create the instance.</param>
+ /// <returns>An instance of the <see cref="UInt32"/> type.</returns>
+ [CLSCompliant(false)]
+ public static uint CreateInstanceOfUInt32(Random rndGen)
+ {
+ byte[] rndValue = new byte[4];
+ rndGen.NextBytes(rndValue);
+ uint result = 0;
+ for (int i = 0; i < rndValue.Length; i++)
+ {
+ result = (uint)(result << 8);
+ result = (uint)(result | (uint)rndValue[i]);
+ }
+
+ return result;
+ }
+
+ /// <summary>
+ /// Creates an instance of the <see cref="UInt64"/> type.
+ /// </summary>
+ /// <param name="rndGen">A <see cref="Random"/> used to create the instance.</param>
+ /// <returns>An instance of the <see cref="UInt64"/> type.</returns>
+ [CLSCompliant(false)]
+ public static ulong CreateInstanceOfUInt64(Random rndGen)
+ {
+ byte[] rndValue = new byte[8];
+ rndGen.NextBytes(rndValue);
+ ulong result = 0;
+ for (int i = 0; i < rndValue.Length; i++)
+ {
+ result = (ulong)(result << 8);
+ result = (ulong)(result | (ulong)rndValue[i]);
+ }
+
+ return result;
+ }
+
+ /// <summary>
+ /// Creates an instance of the <see cref="Uri"/> type.
+ /// </summary>
+ /// <param name="rndGen">A <see cref="Random"/> used to create the instance.</param>
+ /// <returns>An instance of the <see cref="Uri"/> type.</returns>
+ public static System.Uri CreateInstanceOfUri(Random rndGen)
+ {
+ Uri result;
+ UriKind kind;
+ try
+ {
+ string uriString;
+ do
+ {
+ uriString = UriCreator.CreateUri(rndGen, out kind);
+ }
+ while (IsRelativeIPv6Uri(uriString, kind));
+ result = new Uri(uriString, kind);
+ }
+ catch (ArgumentException)
+ {
+ result = new Uri("my.schema://userName:password@my.domain/path1/path2?query1=123&query2=%22hello%22");
+ }
+
+ return result;
+ }
+
+ /// <summary>
+ /// Creates an instance of the a string which represents an <see cref="Uri"/>.
+ /// </summary>
+ /// <param name="rndGen">A <see cref="Random"/> used to create the instance.</param>
+ /// <returns>An instance of the a string which represents an <see cref="Uri"/>.</returns>
+ public static string CreateInstanceOfUriString(Random rndGen)
+ {
+ UriKind kind;
+ return UriCreator.CreateUri(rndGen, out kind);
+ }
+
+ /// <summary>
+ /// Checks whether this creator can create an instance of the given type.
+ /// </summary>
+ /// <param name="type">The type to be created.</param>
+ /// <returns><code>true</code> if this creator can create an instance of the given type; <code>false</code> otherwise.</returns>
+ public static bool CanCreateInstanceOf(Type type)
+ {
+ return creators.ContainsKey(type);
+ }
+
+ /// <summary>
+ /// Creates an instance of the given primitive type.
+ /// </summary>
+ /// <param name="type">The type to create an instance.</param>
+ /// <param name="rndGen">A <see cref="Random"/> used to create the instance.</param>
+ /// <returns>An instance of the given type.</returns>
+ public static object CreatePrimitiveInstance(Type type, Random rndGen)
+ {
+ if (creators.ContainsKey(type))
+ {
+ return creators[type].Invoke(null, new object[] { rndGen });
+ }
+ else
+ {
+ throw new ArgumentException("Type " + type.FullName + " not supported");
+ }
+ }
+
+ private static bool IsRelativeIPv6Uri(string uriString, UriKind kind)
+ {
+ return kind == UriKind.Relative && RelativeIPv6UriRegex.Match(uriString).Success;
+ }
+
+ /// <summary>
+ /// Creates URI instances based on RFC 2396
+ /// </summary>
+ internal static class UriCreator
+ {
+ static readonly string digit;
+ static readonly string upalpha;
+ static readonly string lowalpha;
+ static readonly string alpha;
+ static readonly string alphanum;
+ static readonly string hex;
+ static readonly string mark;
+ static readonly string unreserved;
+ static readonly string reserved;
+
+ static UriCreator()
+ {
+ digit = "0123456789";
+ upalpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+ lowalpha = upalpha.ToLower();
+ alpha = upalpha + lowalpha;
+ alphanum = alpha + digit;
+ hex = digit + "ABCDEFabcdef";
+ mark = "-_.!~*'()";
+ unreserved = alphanum + mark;
+ reserved = ";/?:@&=+$,";
+ }
+
+ internal static string CreateUri(Random rndGen, out UriKind kind)
+ {
+ StringBuilder sb = new StringBuilder();
+ kind = UriKind.Relative;
+ if (rndGen.Next(3) > 0)
+ {
+ // Add URI scheme
+ CreateScheme(sb, rndGen);
+ kind = UriKind.Absolute;
+ }
+
+ if (rndGen.Next(3) > 0)
+ {
+ // Add URI host
+ sb.Append("//");
+ if (rndGen.Next(10) == 0)
+ {
+ CreateUserInfo(sb, rndGen);
+ }
+
+ CreateHost(sb, rndGen);
+ if (rndGen.Next(2) > 0)
+ {
+ sb.Append(':');
+ sb.Append(rndGen.Next(65536));
+ }
+ }
+
+ if (rndGen.Next(4) > 0)
+ {
+ // Add URI path
+ for (int i = 0; i < rndGen.Next(1, 4); i++)
+ {
+ sb.Append('/');
+ AddPathSegment(sb, rndGen);
+ }
+ }
+
+ if (rndGen.Next(3) == 0)
+ {
+ // Add URI query string
+ sb.Append('?');
+ AddUriC(sb, rndGen);
+ }
+
+ return sb.ToString();
+ }
+
+ private static void CreateScheme(StringBuilder sb, Random rndGen)
+ {
+ int size = rndGen.Next(1, 10);
+ AddChars(sb, rndGen, alpha, 1);
+ string schemeChars = alpha + digit + "+-.";
+ AddChars(sb, rndGen, schemeChars, size);
+ sb.Append(':');
+ }
+
+ private static void CreateIPv4Address(StringBuilder sb, Random rndGen)
+ {
+ for (int i = 0; i < 4; i++)
+ {
+ if (i > 0)
+ {
+ sb.Append('.');
+ }
+
+ sb.Append(rndGen.Next(1000));
+ }
+ }
+
+ private static void AddIPv6AddressPart(StringBuilder sb, Random rndGen)
+ {
+ int size = rndGen.Next(1, 10);
+ if (size > 4)
+ {
+ size = 4;
+ }
+
+ AddChars(sb, rndGen, hex, size);
+ }
+
+ private static void CreateIPv6Address(StringBuilder sb, Random rndGen)
+ {
+ sb.Append('[');
+ int temp = rndGen.Next(6);
+ int i;
+ switch (temp)
+ {
+ case 0:
+ sb.Append("::");
+ break;
+ case 1:
+ sb.Append("::1");
+ break;
+ case 2:
+ sb.Append("FF01::101");
+ break;
+ case 3:
+ sb.Append("::1");
+ break;
+ case 4:
+ for (i = 0; i < 3; i++)
+ {
+ AddIPv6AddressPart(sb, rndGen);
+ sb.Append(':');
+ }
+
+ for (i = 0; i < 3; i++)
+ {
+ sb.Append(':');
+ AddIPv6AddressPart(sb, rndGen);
+ }
+
+ break;
+ default:
+ for (i = 0; i < 8; i++)
+ {
+ if (i > 0)
+ {
+ sb.Append(':');
+ }
+
+ AddIPv6AddressPart(sb, rndGen);
+ }
+
+ break;
+ }
+
+ sb.Append(']');
+ }
+
+ private static void AddChars(StringBuilder sb, Random rndGen, string validChars, int size)
+ {
+ for (int i = 0; i < size; i++)
+ {
+ sb.Append(validChars[rndGen.Next(validChars.Length)]);
+ }
+ }
+
+ private static void CreateHostName(StringBuilder sb, Random rndGen)
+ {
+ int domainLabelCount = rndGen.Next(4);
+ int size;
+ for (int i = 0; i < domainLabelCount; i++)
+ {
+ AddChars(sb, rndGen, alphanum, 1);
+ size = rndGen.Next(10) - 1;
+ if (size > 0)
+ {
+ AddChars(sb, rndGen, alphanum + "-", size);
+ AddChars(sb, rndGen, alphanum, 1);
+ }
+
+ sb.Append('.');
+ }
+
+ AddChars(sb, rndGen, alpha, 1);
+ size = rndGen.Next(10) - 1;
+ if (size > 0)
+ {
+ AddChars(sb, rndGen, alphanum + "-", size);
+ AddChars(sb, rndGen, alphanum, 1);
+ }
+ }
+
+ private static void CreateHost(StringBuilder sb, Random rndGen)
+ {
+ int temp = rndGen.Next(3);
+ switch (temp)
+ {
+ case 0:
+ CreateIPv4Address(sb, rndGen);
+ break;
+ case 1:
+ CreateIPv6Address(sb, rndGen);
+ break;
+ case 2:
+ CreateHostName(sb, rndGen);
+ break;
+ }
+ }
+
+ private static void CreateUserInfo(StringBuilder sb, Random rndGen)
+ {
+ AddChars(sb, rndGen, alpha, rndGen.Next(1, 10));
+ if (rndGen.Next(3) > 0)
+ {
+ sb.Append(':');
+ AddChars(sb, rndGen, alpha, rndGen.Next(1, 10));
+ }
+
+ sb.Append('@');
+ }
+
+ private static void AddEscapedChar(StringBuilder sb, Random rndGen)
+ {
+ sb.Append('%');
+ AddChars(sb, rndGen, hex, 2);
+ }
+
+ private static void AddPathSegment(StringBuilder sb, Random rndGen)
+ {
+ string pchar = unreserved + ":@&=+$,";
+ int size = rndGen.Next(1, 10);
+ for (int i = 0; i < size; i++)
+ {
+ if (rndGen.Next(pchar.Length + 1) > 0)
+ {
+ AddChars(sb, rndGen, pchar, 1);
+ }
+ else
+ {
+ AddEscapedChar(sb, rndGen);
+ }
+ }
+ }
+
+ private static void AddUriC(StringBuilder sb, Random rndGen)
+ {
+ int size = rndGen.Next(20);
+ string reservedPlusUnreserved = reserved + unreserved;
+ for (int i = 0; i < size; i++)
+ {
+ if (rndGen.Next(5) > 0)
+ {
+ AddChars(sb, rndGen, reservedPlusUnreserved, 1);
+ }
+ else
+ {
+ AddEscapedChar(sb, rndGen);
+ }
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Utility class used to create test instances of arbitrary types.
+ /// </summary>
+ public static class InstanceCreator
+ {
+ private static Stack<Type> typesInCreationStack = new Stack<Type>();
+
+ /// <summary>
+ /// Creates an instance of an array type.
+ /// </summary>
+ /// <param name="arrayType">The array type.</param>
+ /// <param name="rndGen">A <see cref="Random"/> used to create the instance.</param>
+ /// <returns>An instance of the given array type.</returns>
+ public static object CreateInstanceOfArray(Type arrayType, Random rndGen)
+ {
+ Type type = arrayType.GetElementType();
+ double rndNumber = rndGen.NextDouble();
+ if (rndNumber < CreatorSettings.NullValueProbability)
+ {
+ return null; // 1% chance of null value
+ }
+
+ int size = (int)Math.Pow(CreatorSettings.MaxArrayLength, rndNumber); // this will create more small arrays than large ones
+ size--;
+ Array result = Array.CreateInstance(type, size);
+ for (int i = 0; i < size; i++)
+ {
+ result.SetValue(CreateInstanceOf(type, rndGen), i);
+ }
+
+ return result;
+ }
+
+ /// <summary>
+ /// Creates an instance of a <see cref="List{T}"/>.
+ /// </summary>
+ /// <param name="listType">The List&lt;T&gt; type.</param>
+ /// <param name="rndGen">A <see cref="Random"/> used to create the instance.</param>
+ /// <returns>An instance of the given list type.</returns>
+ public static object CreateInstanceOfListOfT(Type listType, Random rndGen)
+ {
+ Type type = listType.GetGenericArguments()[0];
+ double rndNumber = rndGen.NextDouble();
+ if (rndNumber < CreatorSettings.NullValueProbability)
+ {
+ return null; // 1% chance of null value
+ }
+
+ int size = (int)Math.Pow(CreatorSettings.MaxListLength, rndNumber); // this will create more small lists than large ones
+ size--;
+ object result = Activator.CreateInstance(listType);
+ MethodInfo addMethod = listType.GetMethod("Add");
+ for (int i = 0; i < size; i++)
+ {
+ addMethod.Invoke(result, new object[] { CreateInstanceOf(type, rndGen) });
+ }
+
+ return result;
+ }
+
+ /// <summary>
+ /// Creates an instance of a <see cref="LinkedList{T}"/>.
+ /// </summary>
+ /// <param name="listType">The LinkedList&lt;T&gt; type.</param>
+ /// <param name="rndGen">A <see cref="Random"/> used to create the instance.</param>
+ /// <returns>An instance of the given list type.</returns>
+ public static object CreateInstanceOfLinkedListOfT(Type listType, Random rndGen)
+ {
+ Type type = listType.GetGenericArguments()[0];
+ double rndNumber = rndGen.NextDouble();
+ if (rndNumber < CreatorSettings.NullValueProbability)
+ {
+ return null; // 1% chance of null value
+ }
+
+ int size = (int)Math.Pow(CreatorSettings.MaxListLength, rndNumber); // this will create more small lists than large ones
+ size--;
+ object result = Activator.CreateInstance(listType);
+ MethodInfo addMethod = listType.GetMethod("AddLast", new Type[] { type });
+ for (int i = 0; i < size; i++)
+ {
+ addMethod.Invoke(result, new object[] { CreateInstanceOf(type, rndGen) });
+ }
+
+ return result;
+ }
+
+ /// <summary>
+ /// Creates an instance of a <see cref="IEnumerable{T}"/>.
+ /// </summary>
+ /// <param name="enumerableOfTType">The IEnumerable&lt;T&gt; type.</param>
+ /// <param name="enumeredType">The type to be enumerated.</param>
+ /// <param name="rndGen">A <see cref="Random"/> used to create the instance.</param>
+ /// <returns>An instance of the given enumerable type.</returns>
+ public static object CreateInstanceOfIEnumerableOfT(Type enumerableOfTType, Type enumeredType, Random rndGen)
+ {
+ double rndNumber = rndGen.NextDouble();
+ if (!enumerableOfTType.IsValueType && rndNumber < CreatorSettings.NullValueProbability)
+ {
+ return null; // 1% chance of null value
+ }
+
+ int size = (int)Math.Pow(CreatorSettings.MaxListLength, rndNumber); // this will create more small lists than large ones
+ size--;
+ object result = Activator.CreateInstance(enumerableOfTType);
+ MethodInfo addMethod = enumerableOfTType.GetMethod("Add", new Type[] { enumeredType });
+ if (addMethod == null)
+ {
+ throw new ArgumentException("Cannot create an instance of an IEnumerable<T> type which does not have a public Add method");
+ }
+
+ for (int i = 0; i < size; i++)
+ {
+ addMethod.Invoke(result, new object[] { CreateInstanceOf(enumeredType, rndGen) });
+ }
+
+ return result;
+ }
+
+ /// <summary>
+ /// Creates an instance of a <see cref="Nullable{T}"/>.
+ /// </summary>
+ /// <param name="nullableOfTType">The Nullable&lt;T&gt; type.</param>
+ /// <param name="rndGen">A <see cref="Random"/> used to create the instance.</param>
+ /// <returns>An instance of the given nullable type.</returns>
+ public static object CreateInstanceOfNullableOfT(Type nullableOfTType, Random rndGen)
+ {
+ if (rndGen.Next(5) == 0)
+ {
+ return null;
+ }
+
+ Type type = nullableOfTType.GetGenericArguments()[0];
+ return CreateInstanceOf(type, rndGen);
+ }
+
+ /// <summary>
+ /// Creates an instance of an enum type..
+ /// </summary>
+ /// <param name="enumType">The enum type.</param>
+ /// <param name="rndGen">A <see cref="Random"/> used to create the instance.</param>
+ /// <returns>An instance of the given enum type.</returns>
+ public static object CreateInstanceOfEnum(Type enumType, Random rndGen)
+ {
+ bool hasFlags = enumType.GetCustomAttributes(typeof(FlagsAttribute), true).Length > 0;
+ Array possibleValues = Enum.GetValues(enumType);
+ if (possibleValues.Length == 0)
+ {
+ return 0;
+ }
+
+ if (!hasFlags)
+ {
+ return possibleValues.GetValue(rndGen.Next(possibleValues.Length));
+ }
+ else
+ {
+ Type underlyingType = Enum.GetUnderlyingType(enumType);
+ string strResult;
+ if (underlyingType.FullName == typeof(ulong).FullName)
+ {
+ ulong result = 0;
+ if (rndGen.Next(10) > 0)
+ {
+ // 10% chance of value zero
+ foreach (object value in possibleValues)
+ {
+ if (rndGen.Next(2) == 0)
+ {
+ result |= ((IConvertible)value).ToUInt64(null);
+ }
+ }
+ }
+
+ strResult = result.ToString();
+ }
+ else
+ {
+ long result = 0;
+ if (rndGen.Next(10) > 0)
+ {
+ // 10% chance of value zero
+ foreach (object value in possibleValues)
+ {
+ if (rndGen.Next(2) == 0)
+ {
+ result |= ((IConvertible)value).ToInt64(null);
+ }
+ }
+ }
+
+ strResult = result.ToString();
+ }
+
+ return Enum.Parse(enumType, strResult, true);
+ }
+ }
+
+ /// <summary>
+ /// Creates an instance of a <see cref="Dictionary{K,V}"/>.
+ /// </summary>
+ /// <param name="dictionaryType">The Dictionary&lt;K,V&gt; type.</param>
+ /// <param name="rndGen">A <see cref="Random"/> used to create the instance.</param>
+ /// <returns>An instance of the given dictionary type.</returns>
+ public static object CreateInstanceOfDictionaryOfKAndV(Type dictionaryType, Random rndGen)
+ {
+ Type[] genericArgs = dictionaryType.GetGenericArguments();
+ Type typeK = genericArgs[0];
+ Type typeV = genericArgs[1];
+ double rndNumber = rndGen.NextDouble();
+ if (rndNumber < CreatorSettings.NullValueProbability)
+ {
+ return null; // 1% chance of null value
+ }
+
+ int size = (int)Math.Pow(CreatorSettings.MaxListLength, rndNumber); // this will create more small dictionaries than large ones
+ size--;
+ object result = Activator.CreateInstance(dictionaryType);
+ MethodInfo addMethod = dictionaryType.GetMethod("Add");
+ MethodInfo containsKeyMethod = dictionaryType.GetMethod("ContainsKey");
+ for (int i = 0; i < size; i++)
+ {
+ object newKey;
+ do
+ {
+ newKey = CreateInstanceOf(typeK, rndGen);
+ }
+ while (newKey == null);
+
+ bool containsKey = (bool)containsKeyMethod.Invoke(result, new object[] { newKey });
+ if (!containsKey)
+ {
+ object newValue = CreateInstanceOf(typeV, rndGen);
+ addMethod.Invoke(result, new object[] { newKey, newValue });
+ }
+ }
+
+ return result;
+ }
+
+ /// <summary>
+ /// Creates an instance of the given type.
+ /// </summary>
+ /// <typeparam name="T">The type to create an instance from.</typeparam>
+ /// <param name="rndGen">A random generator used to populate the instance.</param>
+ /// <returns>An instance of the given type.</returns>
+ public static T CreateInstanceOf<T>(Random rndGen)
+ {
+ return (T)InstanceCreator.CreateInstanceOf(typeof(T), rndGen);
+ }
+
+ /// <summary>
+ /// Creates an instance of the given type.
+ /// </summary>
+ /// <param name="type">The type to create an instance from.</param>
+ /// <param name="rndGen">A random generator used to populate the instance.</param>
+ /// <param name="allowNulls">A flag indicating whether null values can be returned by this method.</param>
+ /// <returns>An instance of the given type.</returns>
+ public static object CreateInstanceOf(Type type, Random rndGen, bool allowNulls)
+ {
+ double currentNullProbability = CreatorSettings.NullValueProbability;
+ if (!allowNulls)
+ {
+ CreatorSettings.NullValueProbability = 0;
+ }
+
+ object result = CreateInstanceOf(type, rndGen);
+ if (!allowNulls)
+ {
+ CreatorSettings.NullValueProbability = currentNullProbability;
+ }
+
+ return result;
+ }
+
+ /// <summary>
+ /// Creates an instance of the given type.
+ /// </summary>
+ /// <param name="type">The type to create an instance from.</param>
+ /// <param name="rndGen">A random generator used to populate the instance.</param>
+ /// <returns>An instance of the given type.</returns>
+ public static object CreateInstanceOf(Type type, Random rndGen)
+ {
+ if (CreatorSettings.CreatorSurrogate != null)
+ {
+ if (CreatorSettings.CreatorSurrogate.CanCreateInstanceOf(type))
+ {
+ return CreatorSettings.CreatorSurrogate.CreateInstanceOf(type, rndGen);
+ }
+ }
+
+ ConstructorInfo randomConstructor = type.GetConstructor(new Type[] { typeof(Random) });
+ if (randomConstructor != null)
+ {
+ // it's possible that a class with a Type.GetConstructor will return a constructor
+ // which takes a System.Object; we only want to use if it really takes a Random argument.
+ ParameterInfo[] ctorParameters = randomConstructor.GetParameters();
+ if (ctorParameters.Length == 1 && ctorParameters[0].ParameterType == typeof(Random))
+ {
+ return randomConstructor.Invoke(new object[] { rndGen });
+ }
+ }
+
+ // Allow for a static factory method (called 'CreateInstance' to allow for inheritance scenarios
+ MethodInfo randomFactoryMethod = type.GetMethod("CreateInstance", BindingFlags.Public | BindingFlags.Static, null, new Type[] { typeof(Random) }, null);
+ if (randomFactoryMethod != null)
+ {
+ ParameterInfo[] methodParameters = randomFactoryMethod.GetParameters();
+ if (methodParameters.Length == 1 && methodParameters[0].ParameterType == typeof(Random))
+ {
+ return randomFactoryMethod.Invoke(null, new object[] { rndGen });
+ }
+ }
+
+ object result = null;
+ Type genericElementType = null;
+ if (CreatorSettings.AvoidStackOverflowDueToTypeCycles)
+ {
+ if (typesInCreationStack.Contains(type))
+ {
+ if (type.IsValueType)
+ {
+ return Activator.CreateInstance(type);
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ typesInCreationStack.Push(type);
+ }
+
+ if (PrimitiveCreator.CanCreateInstanceOf(type))
+ {
+ result = PrimitiveCreator.CreatePrimitiveInstance(type, rndGen);
+ }
+ else if (type.IsEnum)
+ {
+ result = CreateInstanceOfEnum(type, rndGen);
+ }
+ else if (type.IsArray)
+ {
+ result = CreateInstanceOfArray(type, rndGen);
+ }
+ else if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
+ {
+ result = CreateInstanceOfNullableOfT(type, rndGen);
+ }
+ else if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>))
+ {
+ result = CreateInstanceOfListOfT(type, rndGen);
+ }
+ else if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(LinkedList<>))
+ {
+ result = CreateInstanceOfLinkedListOfT(type, rndGen);
+ }
+ else if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))
+ {
+ result = CreateInstanceOfDictionaryOfKAndV(type, rndGen);
+ }
+ else if (ContainsAttribute(type, typeof(DataContractAttribute)))
+ {
+ result = ClassInstanceCreator.DataContractCreator.CreateInstanceOf(type, rndGen);
+ }
+ else if (IsIEnumerableOfT(type, out genericElementType))
+ {
+ result = CreateInstanceOfIEnumerableOfT(type, genericElementType, rndGen);
+ }
+ else if (type.IsPublic || type.IsNestedPublic)
+ {
+ result = ClassInstanceCreator.POCOCreator.CreateInstanceOf(type, rndGen);
+ }
+ else
+ {
+ result = Activator.CreateInstance(type);
+ }
+
+ if (CreatorSettings.AvoidStackOverflowDueToTypeCycles)
+ {
+ typesInCreationStack.Pop();
+ }
+
+ return result;
+ }
+
+ internal static bool ContainsAttribute(MemberInfo member, Type attributeType)
+ {
+ object[] attributes = member.GetCustomAttributes(attributeType, false);
+ return attributes != null && attributes.Length > 0;
+ }
+
+ static bool IsIEnumerableOfT(Type type, out Type genericArgumentType)
+ {
+ genericArgumentType = null;
+ foreach (Type interfaceType in type.GetInterfaces())
+ {
+ if (interfaceType.IsGenericType && interfaceType.GetGenericTypeDefinition() == typeof(IEnumerable<>))
+ {
+ genericArgumentType = interfaceType.GetGenericArguments()[0];
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// Helper class used to create test instances.
+ /// </summary>
+ public class ClassInstanceCreator
+ {
+ static ClassInstanceCreator dataContractCreatorWithNonPublicMembers;
+ static ClassInstanceCreator dataContractCreator;
+ static ClassInstanceCreator pocoCreator;
+
+ bool includeNonPublicMembers;
+ GetMemberNameDelegate getMemberNameDelegate;
+ ShouldBeIncludedDelegate shouldBeIncludedDelegate;
+
+ private ClassInstanceCreator(GetMemberNameDelegate getMemberNameDelegate, ShouldBeIncludedDelegate shouldBeIncludedDelegate, bool includeNonPublicMembers)
+ : this(getMemberNameDelegate, shouldBeIncludedDelegate)
+ {
+ this.includeNonPublicMembers = includeNonPublicMembers;
+ }
+
+ private ClassInstanceCreator(GetMemberNameDelegate getMemberNameDelegate, ShouldBeIncludedDelegate shouldBeIncludedDelegate)
+ {
+ this.getMemberNameDelegate = getMemberNameDelegate;
+ this.shouldBeIncludedDelegate = shouldBeIncludedDelegate;
+ }
+
+ delegate string GetMemberNameDelegate(MemberInfo member);
+
+ delegate bool ShouldBeIncludedDelegate(MemberInfo member);
+
+ /// <summary>
+ /// Gets an instance of a creator which knows how to create instance of types
+ /// decorated with the <see cref="DataContractAttribute"/>.
+ /// </summary>
+ public static ClassInstanceCreator DataContractCreator
+ {
+ get
+ {
+ if (dataContractCreator == null)
+ {
+ dataContractCreator = new ClassInstanceCreator(GetDataMemberName, IncludeDataMembersOnly);
+ }
+
+ return dataContractCreator;
+ }
+ }
+
+ /// <summary>
+ /// Gets an instance of a creator which knows how to create instance of types
+ /// which only sets members decorated with the <see cref="DataMemberAttribute"/>.
+ /// </summary>
+ public static ClassInstanceCreator DataContractCreatorWithNonPublicMembers
+ {
+ get
+ {
+ if (dataContractCreatorWithNonPublicMembers == null)
+ {
+ dataContractCreatorWithNonPublicMembers = new ClassInstanceCreator(GetDataMemberName, IncludeDataMembersOnly, true);
+ }
+
+ return dataContractCreatorWithNonPublicMembers;
+ }
+ }
+
+ /// <summary>
+ /// Gets an instance of a creator which knows how to create instances of POCO
+ /// types (public types, not decorated with the <see cref="DataContractAttribute"/> and
+ /// which have a parameterless public constructor).
+ /// </summary>
+ public static ClassInstanceCreator POCOCreator
+ {
+ get
+ {
+ if (pocoCreator == null)
+ {
+ pocoCreator = new ClassInstanceCreator(
+ delegate(MemberInfo member) { return member.Name; },
+ DoNotIncludeExcludedMembers);
+ }
+
+ return pocoCreator;
+ }
+ }
+
+ /// <summary>
+ /// Creates an instance of the given type.
+ /// </summary>
+ /// <param name="type">The type to create an instance from.</param>
+ /// <param name="rndGen">A random generator used to populate the instance.</param>
+ /// <returns>An instance of the given type.</returns>
+ public object CreateInstanceOf(Type type, Random rndGen)
+ {
+ object result = null;
+ if (rndGen.NextDouble() < CreatorSettings.NullValueProbability && !type.IsValueType)
+ {
+ // 1% chance of null object, if it is not a struct
+ return null;
+ }
+
+ ConstructorInfo randomConstructor = type.GetConstructor(new Type[] { typeof(Random) });
+ if (randomConstructor != null && randomConstructor.GetParameters()[0].ParameterType == typeof(Random))
+ {
+ result = randomConstructor.Invoke(new object[] { rndGen });
+ }
+ else
+ {
+ ConstructorInfo defaultConstructor = type.GetConstructor(new Type[0]);
+ if (defaultConstructor != null || type.IsValueType)
+ {
+ result = Activator.CreateInstance(type);
+ this.SetFieldsAndProperties(type, result, rndGen);
+ }
+ else
+ {
+ throw new ArgumentException("Don't know how to create an instance of " + type.FullName);
+ }
+ }
+
+ return result;
+ }
+
+ private static string GetDataMemberName(MemberInfo member)
+ {
+ DataMemberAttribute[] dataMemberAttr = (DataMemberAttribute[])member.GetCustomAttributes(typeof(DataMemberAttribute), false);
+ if (dataMemberAttr == null || dataMemberAttr.Length == 0 || dataMemberAttr[0].Name == null)
+ {
+ return member.Name;
+ }
+ else
+ {
+ return dataMemberAttr[0].Name;
+ }
+ }
+
+ private static bool ContainsAttribute(MemberInfo member, string attributeName)
+ {
+ object[] customAttributes = member.GetCustomAttributes(false);
+ foreach (object attribute in customAttributes)
+ {
+ if (attribute != null && attribute.GetType().Name == attributeName)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static bool IncludeDataMembersOnly(MemberInfo member)
+ {
+ return ContainsAttribute(member, "DataMemberAttribute");
+ }
+
+ private static bool DoNotIncludeExcludedMembers(MemberInfo member)
+ {
+ return !ContainsAttribute(member, "IgnoreDataMemberAttribute");
+ }
+
+ int CompareDataMembers(MemberInfo member1, MemberInfo member2)
+ {
+ return this.getMemberNameDelegate(member1).CompareTo(this.getMemberNameDelegate(member2));
+ }
+
+ private void SetFieldsAndProperties(Type type, object obj, Random rndGen)
+ {
+ List<MemberInfo> members = new List<MemberInfo>();
+ BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public;
+ if (this.includeNonPublicMembers)
+ {
+ bindingFlags |= BindingFlags.NonPublic;
+ }
+
+ FieldInfo[] fields = type.GetFields(bindingFlags);
+ PropertyInfo[] properties = type.GetProperties(bindingFlags);
+
+ foreach (FieldInfo field in fields)
+ {
+ if (this.shouldBeIncludedDelegate(field))
+ {
+ members.Add(field);
+ }
+ }
+
+ foreach (PropertyInfo prop in properties)
+ {
+ if (this.shouldBeIncludedDelegate(prop))
+ {
+ members.Add(prop);
+ }
+ }
+
+ members.Sort(new Comparison<MemberInfo>(this.CompareDataMembers));
+
+ foreach (MemberInfo member in members)
+ {
+ if (member is FieldInfo)
+ {
+ object fieldValue = InstanceCreator.CreateInstanceOf(((FieldInfo)member).FieldType, rndGen);
+ ((FieldInfo)member).SetValue(obj, fieldValue);
+ }
+ else
+ {
+ PropertyInfo propInfo = (PropertyInfo)member;
+ if (propInfo.CanWrite)
+ {
+ object propertyValue = InstanceCreator.CreateInstanceOf(propInfo.PropertyType, rndGen);
+ propInfo.SetValue(obj, propertyValue, null);
+ }
+ else
+ {
+ if (!this.TrySettingMembersOfGetOnlyCollection(rndGen, propInfo, obj))
+ {
+ throw new ArgumentException("Cannot set property " + propInfo.Name + " of type " + type.FullName);
+ }
+ }
+ }
+ }
+ }
+
+ private bool TrySettingMembersOfGetOnlyCollection(Random rndGen, PropertyInfo propInfo, object obj)
+ {
+ Type propType = propInfo.PropertyType;
+ object propValue = propInfo.GetValue(obj, null);
+ if (propValue == null)
+ {
+ // need something to set the value to
+ return false;
+ }
+
+ if (propType.IsArray)
+ {
+ Array propArray = (Array)propValue;
+ Type arrayType = propType.GetElementType();
+ for (int i = 0; i < propArray.Length; i++)
+ {
+ object elementValue = InstanceCreator.CreateInstanceOf(arrayType, rndGen);
+ propArray.SetValue(elementValue, i);
+ }
+
+ return true;
+ }
+ else if (propType.IsGenericType && propType.GetGenericTypeDefinition() == typeof(List<>))
+ {
+ Type listType = propType.GetGenericArguments()[0];
+ MethodInfo addMethod = propType.GetMethod("Add", new Type[] { listType });
+ double rndNumber = rndGen.NextDouble();
+ int size = (int)Math.Pow(CreatorSettings.MaxListLength, rndNumber); // this will create more small lists than large ones
+ size--;
+ for (int i = 0; i < size; i++)
+ {
+ object listValue = InstanceCreator.CreateInstanceOf(listType, rndGen);
+ addMethod.Invoke(propValue, new object[] { listValue });
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// Enables tests to create specific instances of certain types.
+ /// </summary>
+ public abstract class InstanceCreatorSurrogate
+ {
+ /// <summary>
+ /// Checks whether this surrogate can create instances of a given type.
+ /// </summary>
+ /// <param name="type">The type which needs to be created.</param>
+ /// <returns>A true value if this surrogate can create the given type; a
+ /// false value otherwise.</returns>
+ public abstract bool CanCreateInstanceOf(Type type);
+
+ /// <summary>
+ /// Creates an instance of the given type.
+ /// </summary>
+ /// <param name="type">The type to create an instance for.</param>
+ /// <param name="rndGen">A Random generator to assist in creating the instance.</param>
+ /// <returns>An instance of the given type.</returns>
+ public abstract object CreateInstanceOf(Type type, Random rndGen);
+ }
+}
diff --git a/test/System.Json.Test.Integration/Common/JsonValueCreatorSurrogate.cs b/test/System.Json.Test.Integration/Common/JsonValueCreatorSurrogate.cs
new file mode 100644
index 00000000..83373920
--- /dev/null
+++ b/test/System.Json.Test.Integration/Common/JsonValueCreatorSurrogate.cs
@@ -0,0 +1,149 @@
+namespace System.Json
+{
+ public class JsonValueCreatorSurrogate : InstanceCreatorSurrogate
+ {
+ private const int MaxDepth = 4;
+
+ public override bool CanCreateInstanceOf(Type type)
+ {
+ return (type == typeof(JsonValue) || type == typeof(JsonArray) || type == typeof(JsonObject) || type == typeof(JsonPrimitive));
+ }
+
+ public override object CreateInstanceOf(Type type, Random rndGen)
+ {
+ if (!this.CanCreateInstanceOf(type))
+ {
+ return null;
+ }
+
+ if (type == typeof(JsonValue))
+ {
+ return CreateJsonValue(rndGen, 0);
+ }
+ else if (type == typeof(JsonArray))
+ {
+ return CreateJsonArray(rndGen, 0);
+ }
+ else if (type == typeof(JsonObject))
+ {
+ return CreateJsonObject(rndGen, 0);
+ }
+ else
+ {
+ return CreateJsonPrimitive(rndGen);
+ }
+ }
+
+ private static JsonValue CreateJsonValue(Random rndGen, int depth)
+ {
+ if (rndGen.Next() < CreatorSettings.NullValueProbability)
+ {
+ return null;
+ }
+
+ if (depth < MaxDepth)
+ {
+ switch (rndGen.Next(10))
+ {
+ case 0:
+ case 1:
+ case 2:
+ // 30% chance to create an array
+ return CreateJsonArray(rndGen, depth);
+ case 3:
+ case 4:
+ case 5:
+ // 30% chance to create an object
+ return CreateJsonObject(rndGen, depth);
+ default:
+ // 40% chance to create a primitive
+ break;
+ }
+ }
+
+ return CreateJsonPrimitive(rndGen);
+ }
+
+ static JsonValue CreateJsonPrimitive(Random rndGen)
+ {
+ switch (rndGen.Next(17))
+ {
+ case 0:
+ return PrimitiveCreator.CreateInstanceOfChar(rndGen);
+ case 1:
+ return PrimitiveCreator.CreateInstanceOfByte(rndGen);
+ case 2:
+ return PrimitiveCreator.CreateInstanceOfSByte(rndGen);
+ case 3:
+ return PrimitiveCreator.CreateInstanceOfInt16(rndGen);
+ case 4:
+ return PrimitiveCreator.CreateInstanceOfUInt16(rndGen);
+ case 5:
+ return PrimitiveCreator.CreateInstanceOfInt32(rndGen);
+ case 6:
+ return PrimitiveCreator.CreateInstanceOfUInt32(rndGen);
+ case 7:
+ return PrimitiveCreator.CreateInstanceOfInt64(rndGen);
+ case 8:
+ return PrimitiveCreator.CreateInstanceOfUInt64(rndGen);
+ case 9:
+ return PrimitiveCreator.CreateInstanceOfDecimal(rndGen);
+ case 10:
+ return PrimitiveCreator.CreateInstanceOfDouble(rndGen);
+ case 11:
+ return PrimitiveCreator.CreateInstanceOfSingle(rndGen);
+ case 12:
+ return PrimitiveCreator.CreateInstanceOfDateTime(rndGen);
+ case 13:
+ return PrimitiveCreator.CreateInstanceOfDateTimeOffset(rndGen);
+ case 14:
+ case 15:
+ // TODO: 199532 fix uri comparer
+ return PrimitiveCreator.CreateInstanceOfString(rndGen);
+ default:
+ return PrimitiveCreator.CreateInstanceOfBoolean(rndGen);
+ }
+ }
+
+ static JsonArray CreateJsonArray(Random rndGen, int depth)
+ {
+ int size = rndGen.Next(CreatorSettings.MaxArrayLength);
+ if (CreatorSettings.NullValueProbability == 0 && size == 0)
+ {
+ size++;
+ }
+
+ JsonArray result = new JsonArray();
+ for (int i = 0; i < size; i++)
+ {
+ result.Add(CreateJsonValue(rndGen, depth + 1));
+ }
+
+ return result;
+ }
+
+ static JsonObject CreateJsonObject(Random rndGen, int depth)
+ {
+ const string keyChars = "abcdefghijklmnopqrstuvwxyz0123456789";
+ int size = rndGen.Next(CreatorSettings.MaxArrayLength);
+ if (CreatorSettings.NullValueProbability == 0 && size == 0)
+ {
+ size++;
+ }
+
+ JsonObject result = new JsonObject();
+ for (int i = 0; i < size; i++)
+ {
+ string key;
+ do
+ {
+ key = PrimitiveCreator.CreateInstanceOfString(rndGen, 10, keyChars);
+ } while (result.ContainsKey(key));
+
+ result.Add(key, CreateJsonValue(rndGen, depth + 1));
+ }
+
+ return result;
+ }
+ }
+} \ No newline at end of file
diff --git a/test/System.Json.Test.Integration/Common/Log.cs b/test/System.Json.Test.Integration/Common/Log.cs
new file mode 100644
index 00000000..1504101d
--- /dev/null
+++ b/test/System.Json.Test.Integration/Common/Log.cs
@@ -0,0 +1,10 @@
+namespace System.Json
+{
+ internal static class Log
+ {
+ public static void Info(string text, params object[] args)
+ {
+ Console.WriteLine(text, args);
+ }
+ }
+}
diff --git a/test/System.Json.Test.Integration/Common/TypeLibrary.cs b/test/System.Json.Test.Integration/Common/TypeLibrary.cs
new file mode 100644
index 00000000..01c90afa
--- /dev/null
+++ b/test/System.Json.Test.Integration/Common/TypeLibrary.cs
@@ -0,0 +1,1943 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Runtime.Serialization;
+using System.Text;
+
+namespace System.Json
+{
+ /// <summary>
+ /// Test type.
+ /// </summary>
+ [DataContract]
+ public enum EnumType_17
+ {
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [EnumMember]
+ member_0,
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [EnumMember]
+ member_1 = 2,
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [EnumMember]
+ member_2,
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [EnumMember]
+ member_3,
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [EnumMember]
+ member_4,
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [EnumMember]
+ member_5,
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [EnumMember]
+ member_6,
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [EnumMember]
+ member_7,
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [EnumMember]
+ member_8 = 9,
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [EnumMember]
+ member_9,
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [EnumMember]
+ member_10,
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [EnumMember]
+ member_11 = 12,
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [EnumMember]
+ member_12 = 13,
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [EnumMember]
+ member_13,
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [EnumMember]
+ member_14,
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [EnumMember]
+ member_15,
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [EnumMember]
+ member_16,
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [EnumMember]
+ member_17,
+ }
+
+ /// <summary>
+ /// Test type.
+ /// </summary>
+ [DataContract]
+ public enum EnumType_35
+ {
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [EnumMember]
+ member_0,
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [EnumMember]
+ member_1,
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [EnumMember]
+ member_2,
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [EnumMember]
+ member_3,
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [EnumMember]
+ member_4,
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [EnumMember]
+ member_5,
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [EnumMember]
+ member_6,
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [EnumMember]
+ member_7,
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [EnumMember]
+ member_8,
+ }
+
+ /// <summary>
+ /// Test type.
+ /// </summary>
+ public interface IEmptyInterface
+ {
+ }
+
+ /// <summary>
+ /// Test type.
+ /// </summary>
+ [DataContract]
+ public struct StructInt16
+ {
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public short Int16Member;
+ }
+
+ /// <summary>
+ /// Test type.
+ /// </summary>
+ [DataContract]
+ public struct StructGuid
+ {
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public Guid GuidMember;
+ }
+
+ /// <summary>
+ /// Test type.
+ /// </summary>
+ [DataContract]
+ public class DCType_1
+ {
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public byte Member0 { get; set; }
+
+ /// <summary>
+ /// Compares this instance with the given object.
+ /// </summary>
+ /// <param name="obj">The object to compare.</param>
+ /// <returns><code>true</code> if the given instance is equal to this one; <code>false</code> otherwise.</returns>
+ public override bool Equals(object obj)
+ {
+ DCType_1 other = obj as DCType_1;
+ return (other != null) && this.Member0.Equals(other.Member0);
+ }
+
+ /// <summary>
+ /// Returns a hash code for this instance.
+ /// </summary>
+ /// <returns>A hash code for this instance.</returns>
+ public override int GetHashCode()
+ {
+ int result = this.Member0.GetHashCode();
+ return result;
+ }
+
+ /// <summary>
+ /// Returns a debug representation for this instance.
+ /// </summary>
+ /// <returns>A debug representation for this instance.</returns>
+ public override string ToString()
+ {
+ return String.Format(CultureInfo.InvariantCulture, "DCType_1<Member0={0:X2}>", (int)this.Member0);
+ }
+ }
+
+ /// <summary>
+ /// Test type.
+ /// </summary>
+ [CLSCompliant(false)]
+ [DataContract]
+ public class DCType_3
+ {
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public ulong Member2 { get; set; }
+
+ /// <summary>
+ /// Compares this instance with the given object.
+ /// </summary>
+ /// <param name="obj">The object to compare.</param>
+ /// <returns><code>true</code> if the given instance is equal to this one; <code>false</code> otherwise.</returns>
+ public override bool Equals(object obj)
+ {
+ DCType_3 other = obj as DCType_3;
+ return (other != null) && this.Member2.Equals(other.Member2);
+ }
+
+ /// <summary>
+ /// Returns a hash code for this instance.
+ /// </summary>
+ /// <returns>A hash code for this instance.</returns>
+ public override int GetHashCode()
+ {
+ int result = this.Member2.GetHashCode();
+ return result;
+ }
+ }
+
+ /// <summary>
+ /// Test type.
+ /// </summary>
+ [Serializable]
+ public class SerType_4
+ {
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [SuppressMessage("Microsoft.StyleCop.CSharp.MaintainabilityRules", "SA1401:FieldsMustBePrivate",
+ Justification = "Testing serialization of [Serializable] types, which needs public fields.")]
+ public char Member0;
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [SuppressMessage("Microsoft.StyleCop.CSharp.MaintainabilityRules", "SA1401:FieldsMustBePrivate",
+ Justification = "Testing serialization of [Serializable] types, which needs public fields.")]
+ public short? Member1;
+
+ /// <summary>
+ /// Compares this instance with the given object.
+ /// </summary>
+ /// <param name="obj">The object to compare.</param>
+ /// <returns><code>true</code> if the given instance is equal to this one; <code>false</code> otherwise.</returns>
+ public override bool Equals(object obj)
+ {
+ SerType_4 other = obj as SerType_4;
+ return (other != null) && this.Member0.Equals(other.Member0) && Util.CompareNullable<short>(this.Member1, other.Member1);
+ }
+
+ /// <summary>
+ /// Returns a hash code for this instance.
+ /// </summary>
+ /// <returns>A hash code for this instance.</returns>
+ public override int GetHashCode()
+ {
+ int result = 0;
+ result ^= this.Member0.GetHashCode();
+ result ^= (this.Member1 == null) ? 0 : this.Member1.GetHashCode();
+ return result;
+ }
+
+ /// <summary>
+ /// Returns a debug representation for this instance.
+ /// </summary>
+ /// <returns>A debug representation for this instance.</returns>
+ public override string ToString()
+ {
+ return String.Format(CultureInfo.InvariantCulture, "SerType_4<Member0=(char){0},Member1={1}>", (int)this.Member0, Util.EscapeString(this.Member1));
+ }
+ }
+
+ /// <summary>
+ /// Test type.
+ /// </summary>
+ [CLSCompliant(false)]
+ [Serializable]
+ public class SerType_5
+ {
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [SuppressMessage("Microsoft.StyleCop.CSharp.MaintainabilityRules", "SA1401:FieldsMustBePrivate",
+ Justification = "Testing serialization of [Serializable] types, which needs public fields.")]
+ public char Member0;
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [SuppressMessage("Microsoft.StyleCop.CSharp.MaintainabilityRules", "SA1401:FieldsMustBePrivate",
+ Justification = "Testing serialization of [Serializable] types, which needs public fields.")]
+ public byte? Member1;
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [SuppressMessage("Microsoft.StyleCop.CSharp.MaintainabilityRules", "SA1401:FieldsMustBePrivate",
+ Justification = "Testing serialization of [Serializable] types, which needs public fields.")]
+ public char Member2;
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [SuppressMessage("Microsoft.StyleCop.CSharp.MaintainabilityRules", "SA1401:FieldsMustBePrivate",
+ Justification = "Testing serialization of [Serializable] types, which needs public fields.")]
+ public bool Member3;
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [SuppressMessage("Microsoft.StyleCop.CSharp.MaintainabilityRules", "SA1401:FieldsMustBePrivate",
+ Justification = "Testing serialization of [Serializable] types, which needs public fields.")]
+ public sbyte Member4;
+
+ /// <summary>
+ /// Compares this instance with the given object.
+ /// </summary>
+ /// <param name="obj">The object to compare.</param>
+ /// <returns><code>true</code> if the given instance is equal to this one; <code>false</code> otherwise.</returns>
+ public override bool Equals(object obj)
+ {
+ SerType_5 other = obj as SerType_5;
+ return (other != null) &&
+ this.Member0.Equals(other.Member0) &&
+ Util.CompareNullable<byte>(this.Member1, other.Member1) &&
+ this.Member2.Equals(other.Member2) &&
+ this.Member3.Equals(other.Member3) &&
+ this.Member4.Equals(other.Member4);
+ }
+
+ /// <summary>
+ /// Returns a hash code for this instance.
+ /// </summary>
+ /// <returns>A hash code for this instance.</returns>
+ public override int GetHashCode()
+ {
+ int result = 0;
+ result ^= this.Member0.GetHashCode();
+ result ^= (this.Member1 == null) ? 0 : this.Member1.GetHashCode();
+ result ^= this.Member2.GetHashCode();
+ result ^= this.Member3.GetHashCode();
+ result ^= this.Member4.GetHashCode();
+ return result;
+ }
+ }
+
+ /// <summary>
+ /// Test type.
+ /// </summary>
+ [CLSCompliant(false)]
+ [DataContract]
+ public class DCType_7
+ {
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public long? Member1 { get; set; }
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public sbyte Member2 { get; set; }
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public byte[] Member3 { get; set; }
+
+ /// <summary>
+ /// Compares this instance with the given object.
+ /// </summary>
+ /// <param name="obj">The object to compare.</param>
+ /// <returns><code>true</code> if the given instance is equal to this one; <code>false</code> otherwise.</returns>
+ public override bool Equals(object obj)
+ {
+ DCType_7 other = obj as DCType_7;
+ return (other != null) &&
+ Util.CompareNullable<long>(this.Member1, other.Member1) &&
+ this.Member2.Equals(other.Member2) &&
+ Util.CompareArrays(this.Member3, other.Member3);
+ }
+
+ /// <summary>
+ /// Returns a hash code for this instance.
+ /// </summary>
+ /// <returns>A hash code for this instance.</returns>
+ public override int GetHashCode()
+ {
+ int result = 0;
+ result ^= (this.Member1 == null) ? 0 : this.Member1.GetHashCode();
+ result ^= this.Member2.GetHashCode();
+ result ^= Util.ComputeArrayHashCode(this.Member3);
+ return result;
+ }
+ }
+
+ /// <summary>
+ /// Test type.
+ /// </summary>
+ [CLSCompliant(false)]
+ [DataContract]
+ public class DCType_9
+ {
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public sbyte? Member0 { get; set; }
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public Guid Member1 { get; set; }
+
+ /// <summary>
+ /// Compares this instance with the given object.
+ /// </summary>
+ /// <param name="obj">The object to compare.</param>
+ /// <returns><code>true</code> if the given instance is equal to this one; <code>false</code> otherwise.</returns>
+ public override bool Equals(object obj)
+ {
+ DCType_9 other = obj as DCType_9;
+ return (other != null) &&
+ Util.CompareNullable<sbyte>(this.Member0, other.Member0) &&
+ this.Member1.Equals(other.Member1);
+ }
+
+ /// <summary>
+ /// Returns a hash code for this instance.
+ /// </summary>
+ /// <returns>A hash code for this instance.</returns>
+ public override int GetHashCode()
+ {
+ int result = 0;
+ result ^= (this.Member0 == null) ? 0 : this.Member0.GetHashCode();
+ result ^= this.Member1.GetHashCode();
+ return result;
+ }
+ }
+
+ /// <summary>
+ /// Test type.
+ /// </summary>
+ [Serializable]
+ public class SerType_11
+ {
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [SuppressMessage("Microsoft.StyleCop.CSharp.MaintainabilityRules", "SA1401:FieldsMustBePrivate",
+ Justification = "Testing serialization of [Serializable] types, which needs public fields.")]
+ public float Member0;
+
+ /// <summary>
+ /// Compares this instance with the given object.
+ /// </summary>
+ /// <param name="obj">The object to compare.</param>
+ /// <returns><code>true</code> if the given instance is equal to this one; <code>false</code> otherwise.</returns>
+ public override bool Equals(object obj)
+ {
+ SerType_11 other = obj as SerType_11;
+ return (other != null) && this.Member0.Equals(other.Member0);
+ }
+
+ /// <summary>
+ /// Returns a hash code for this instance.
+ /// </summary>
+ /// <returns>A hash code for this instance.</returns>
+ public override int GetHashCode()
+ {
+ int result = this.Member0.GetHashCode();
+ return result;
+ }
+ }
+
+ /// <summary>
+ /// Test type.
+ /// </summary>
+ [CLSCompliant(false)]
+ [DataContract]
+ public class DCType_15
+ {
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ public byte[] Member0 { get; set; }
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ public ushort Member1 { get; set; }
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public Guid Member2 { get; set; }
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ public DCType_1 Member3 { get; set; }
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public DCType_7 Member5 { get; set; }
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public int Member6 { get; set; }
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public DCType_9 Member7 { get; set; }
+
+ /// <summary>
+ /// Compares this instance with the given object.
+ /// </summary>
+ /// <param name="obj">The object to compare.</param>
+ /// <returns><code>true</code> if the given instance is equal to this one; <code>false</code> otherwise.</returns>
+ public override bool Equals(object obj)
+ {
+ DCType_15 other = obj as DCType_15;
+ return (other != null) &&
+ this.Member2.Equals(other.Member2) &&
+ Util.CompareObjects<DCType_7>(this.Member5, other.Member5) &&
+ this.Member6.Equals(other.Member6) &&
+ Util.CompareObjects<DCType_9>(this.Member7, other.Member7);
+ }
+
+ /// <summary>
+ /// Returns a hash code for this instance.
+ /// </summary>
+ /// <returns>A hash code for this instance.</returns>
+ public override int GetHashCode()
+ {
+ int result = 0;
+ result ^= this.Member2.GetHashCode();
+ result ^= (this.Member5 == null) ? 0 : this.Member5.GetHashCode();
+ result ^= this.Member6.GetHashCode();
+ result ^= (this.Member7 == null) ? 0 : this.Member7.GetHashCode();
+ return result;
+ }
+ }
+
+ /// <summary>
+ /// Test type.
+ /// </summary>
+ [DataContract]
+ public class DCType_16
+ {
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public decimal Member0 { get; set; }
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public SerType_11 Member1 { get; set; }
+
+ /// <summary>
+ /// Compares this instance with the given object.
+ /// </summary>
+ /// <param name="obj">The object to compare.</param>
+ /// <returns><code>true</code> if the given instance is equal to this one; <code>false</code> otherwise.</returns>
+ public override bool Equals(object obj)
+ {
+ DCType_16 other = obj as DCType_16;
+ return (other != null) &&
+ this.Member0.Equals(other.Member0) &&
+ Util.CompareObjects<SerType_11>(this.Member1, other.Member1);
+ }
+
+ /// <summary>
+ /// Returns a hash code for this instance.
+ /// </summary>
+ /// <returns>A hash code for this instance.</returns>
+ public override int GetHashCode()
+ {
+ int result = 0;
+ result ^= this.Member0.GetHashCode();
+ result ^= (this.Member1 == null) ? 0 : this.Member1.GetHashCode();
+ return result;
+ }
+ }
+
+ /// <summary>
+ /// Test type.
+ /// </summary>
+ [CLSCompliant(false)]
+ [DataContract]
+ public class DCType_18
+ {
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public SerType_5 Member0 { get; set; }
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public short Member1 { get; set; }
+
+ /// <summary>
+ /// Compares this instance with the given object.
+ /// </summary>
+ /// <param name="obj">The object to compare.</param>
+ /// <returns><code>true</code> if the given instance is equal to this one; <code>false</code> otherwise.</returns>
+ public override bool Equals(object obj)
+ {
+ DCType_18 other = obj as DCType_18;
+ return (other != null) &&
+ Util.CompareObjects<SerType_5>(this.Member0, other.Member0) &&
+ this.Member1.Equals(other.Member1);
+ }
+
+ /// <summary>
+ /// Returns a hash code for this instance.
+ /// </summary>
+ /// <returns>A hash code for this instance.</returns>
+ public override int GetHashCode()
+ {
+ int result = 0;
+ result ^= (this.Member0 == null) ? 0 : this.Member0.GetHashCode();
+ result ^= this.Member1.GetHashCode();
+ return result;
+ }
+ }
+
+ /// <summary>
+ /// Test type.
+ /// </summary>
+ [CLSCompliant(false)]
+ [DataContract]
+ public class DCType_19
+ {
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public uint? Member0 { get; set; }
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public byte? Member1 { get; set; }
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public long? Member2 { get; set; }
+
+ /// <summary>
+ /// Compares this instance with the given object.
+ /// </summary>
+ /// <param name="obj">The object to compare.</param>
+ /// <returns><code>true</code> if the given instance is equal to this one; <code>false</code> otherwise.</returns>
+ public override bool Equals(object obj)
+ {
+ DCType_19 other = obj as DCType_19;
+ return (other != null) &&
+ Util.CompareNullable<uint>(this.Member0, other.Member0) &&
+ Util.CompareNullable<byte>(this.Member1, other.Member1) &&
+ Util.CompareNullable<long>(this.Member2, other.Member2);
+ }
+
+ /// <summary>
+ /// Returns a hash code for this instance.
+ /// </summary>
+ /// <returns>A hash code for this instance.</returns>
+ public override int GetHashCode()
+ {
+ int result = 0;
+ result ^= (this.Member0 == null) ? 0 : this.Member0.GetHashCode();
+ result ^= (this.Member1 == null) ? 0 : this.Member1.GetHashCode();
+ result ^= (this.Member2 == null) ? 0 : this.Member2.GetHashCode();
+ return result;
+ }
+ }
+
+ /// <summary>
+ /// Test type.
+ /// </summary>
+ [CLSCompliant(false)]
+ [DataContract]
+ public class DCType_20
+ {
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public DCType_9 Member0 { get; set; }
+
+ /// <summary>
+ /// Compares this instance with the given object.
+ /// </summary>
+ /// <param name="obj">The object to compare.</param>
+ /// <returns><code>true</code> if the given instance is equal to this one; <code>false</code> otherwise.</returns>
+ public override bool Equals(object obj)
+ {
+ DCType_20 other = obj as DCType_20;
+ return (other != null) &&
+ Util.CompareObjects<DCType_9>(this.Member0, other.Member0);
+ }
+
+ /// <summary>
+ /// Returns a hash code for this instance.
+ /// </summary>
+ /// <returns>A hash code for this instance.</returns>
+ public override int GetHashCode()
+ {
+ int result = (this.Member0 == null) ? 0 : this.Member0.GetHashCode();
+ return result;
+ }
+ }
+
+ /// <summary>
+ /// Test type.
+ /// </summary>
+ [Serializable]
+ public class SerType_22
+ {
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [SuppressMessage("Microsoft.StyleCop.CSharp.MaintainabilityRules", "SA1401:FieldsMustBePrivate",
+ Justification = "Testing serialization of [Serializable] types, which needs public fields.")]
+ public byte Member0;
+
+ /// <summary>
+ /// Compares this instance with the given object.
+ /// </summary>
+ /// <param name="obj">The object to compare.</param>
+ /// <returns><code>true</code> if the given instance is equal to this one; <code>false</code> otherwise.</returns>
+ public override bool Equals(object obj)
+ {
+ SerType_22 other = obj as SerType_22;
+ return (other != null) &&
+ this.Member0.Equals(other.Member0);
+ }
+
+ /// <summary>
+ /// Returns a hash code for this instance.
+ /// </summary>
+ /// <returns>A hash code for this instance.</returns>
+ public override int GetHashCode()
+ {
+ int result = this.Member0.GetHashCode();
+ return result;
+ }
+ }
+
+ /// <summary>
+ /// Test type.
+ /// </summary>
+ [DataContract]
+ public class DCType_25
+ {
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public SerType_22 Member0 { get; set; }
+
+ /// <summary>
+ /// Compares this instance with the given object.
+ /// </summary>
+ /// <param name="obj">The object to compare.</param>
+ /// <returns><code>true</code> if the given instance is equal to this one; <code>false</code> otherwise.</returns>
+ public override bool Equals(object obj)
+ {
+ DCType_25 other = obj as DCType_25;
+ return (other != null) &&
+ Util.CompareObjects<SerType_22>(this.Member0, other.Member0);
+ }
+
+ /// <summary>
+ /// Returns a hash code for this instance.
+ /// </summary>
+ /// <returns>A hash code for this instance.</returns>
+ public override int GetHashCode()
+ {
+ int result = (this.Member0 == null) ? 0 : this.Member0.GetHashCode();
+ return result;
+ }
+ }
+
+ /// <summary>
+ /// Test type.
+ /// </summary>
+ [CLSCompliant(false)]
+ [Serializable]
+ public class SerType_26
+ {
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [SuppressMessage("Microsoft.StyleCop.CSharp.MaintainabilityRules", "SA1401:FieldsMustBePrivate",
+ Justification = "Testing serialization of [Serializable] types, which needs public fields.")]
+ public char Member0;
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [SuppressMessage("Microsoft.StyleCop.CSharp.MaintainabilityRules", "SA1401:FieldsMustBePrivate",
+ Justification = "Testing serialization of [Serializable] types, which needs public fields.")]
+ public short? Member1;
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [SuppressMessage("Microsoft.StyleCop.CSharp.MaintainabilityRules", "SA1401:FieldsMustBePrivate",
+ Justification = "Testing serialization of [Serializable] types, which needs public fields.")]
+ public SerType_4 Member2;
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [SuppressMessage("Microsoft.StyleCop.CSharp.MaintainabilityRules", "SA1401:FieldsMustBePrivate",
+ Justification = "Testing serialization of [Serializable] types, which needs public fields.")]
+ public decimal Member3;
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [SuppressMessage("Microsoft.StyleCop.CSharp.MaintainabilityRules", "SA1401:FieldsMustBePrivate",
+ Justification = "Testing serialization of [Serializable] types, which needs public fields.")]
+ [SuppressMessage("Microsoft.Usage", "CA2235:MarkAllNonSerializableFields",
+ Justification = "The type is serializable (it contains a [DataContract] attribute).")]
+ public DCType_3 Member4;
+
+ /// <summary>
+ /// Compares this instance with the given object.
+ /// </summary>
+ /// <param name="obj">The object to compare.</param>
+ /// <returns><code>true</code> if the given instance is equal to this one; <code>false</code> otherwise.</returns>
+ public override bool Equals(object obj)
+ {
+ SerType_26 other = obj as SerType_26;
+ return (other != null) &&
+ this.Member0.Equals(other.Member0) &&
+ Util.CompareNullable<short>(this.Member1, other.Member1) &&
+ Util.CompareObjects<SerType_4>(this.Member2, other.Member2) &&
+ this.Member3.Equals(other.Member3) &&
+ Util.CompareObjects<DCType_3>(this.Member4, other.Member4);
+ }
+
+ /// <summary>
+ /// Returns a hash code for this instance.
+ /// </summary>
+ /// <returns>A hash code for this instance.</returns>
+ public override int GetHashCode()
+ {
+ int result = 0;
+ result ^= this.Member0.GetHashCode();
+ result ^= (this.Member1 == null) ? 0 : this.Member1.GetHashCode();
+ result ^= (this.Member2 == null) ? 0 : this.Member2.GetHashCode();
+ result ^= this.Member3.GetHashCode();
+ result ^= (this.Member4 == null) ? 0 : this.Member4.GetHashCode();
+ return result;
+ }
+ }
+
+ /// <summary>
+ /// Test type.
+ /// </summary>
+ [DataContract]
+ public class DCType_31
+ {
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public SerType_22 Member0 { get; set; }
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public byte Member1 { get; set; }
+
+ /// <summary>
+ /// Compares this instance with the given object.
+ /// </summary>
+ /// <param name="obj">The object to compare.</param>
+ /// <returns><code>true</code> if the given instance is equal to this one; <code>false</code> otherwise.</returns>
+ public override bool Equals(object obj)
+ {
+ DCType_31 other = obj as DCType_31;
+ return (other != null) &&
+ Util.CompareObjects<SerType_22>(this.Member0, other.Member0) &&
+ this.Member1.Equals(other.Member1);
+ }
+
+ /// <summary>
+ /// Returns a hash code for this instance.
+ /// </summary>
+ /// <returns>A hash code for this instance.</returns>
+ public override int GetHashCode()
+ {
+ int result = 0;
+ result ^= (this.Member0 == null) ? 0 : this.Member0.GetHashCode();
+ result ^= this.Member1.GetHashCode();
+ return result;
+ }
+ }
+
+ /// <summary>
+ /// Test type.
+ /// </summary>
+ [CLSCompliant(false)]
+ [DataContract]
+ public class DCType_32
+ {
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public int Member0 { get; set; }
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public DCType_20 Member1 { get; set; }
+
+ /// <summary>
+ /// Compares this instance with the given object.
+ /// </summary>
+ /// <param name="obj">The object to compare.</param>
+ /// <returns><code>true</code> if the given instance is equal to this one; <code>false</code> otherwise.</returns>
+ public override bool Equals(object obj)
+ {
+ DCType_32 other = obj as DCType_32;
+ return (other != null) &&
+ this.Member0.Equals(other.Member0) &&
+ Util.CompareObjects<DCType_20>(this.Member1, other.Member1);
+ }
+
+ /// <summary>
+ /// Returns a hash code for this instance.
+ /// </summary>
+ /// <returns>A hash code for this instance.</returns>
+ public override int GetHashCode()
+ {
+ int result = 0;
+ result ^= this.Member0.GetHashCode();
+ result ^= (this.Member1 == null) ? 0 : this.Member1.GetHashCode();
+ return result;
+ }
+ }
+
+ /// <summary>
+ /// Test type.
+ /// </summary>
+ [CLSCompliant(false)]
+ [Serializable]
+ public class SerType_33
+ {
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [SuppressMessage("Microsoft.StyleCop.CSharp.MaintainabilityRules", "SA1401:FieldsMustBePrivate",
+ Justification = "Testing serialization of [Serializable] types, which needs public fields.")]
+ public long? Member0;
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [SuppressMessage("Microsoft.StyleCop.CSharp.MaintainabilityRules", "SA1401:FieldsMustBePrivate",
+ Justification = "Testing serialization of [Serializable] types, which needs public fields.")]
+ [SuppressMessage("Microsoft.Usage", "CA2235:MarkAllNonSerializableFields",
+ Justification = "The type is serializable (it contains a [DataContract] attribute).")]
+ public DCType_20 Member1;
+
+ /// <summary>
+ /// Compares this instance with the given object.
+ /// </summary>
+ /// <param name="obj">The object to compare.</param>
+ /// <returns><code>true</code> if the given instance is equal to this one; <code>false</code> otherwise.</returns>
+ public override bool Equals(object obj)
+ {
+ SerType_33 other = obj as SerType_33;
+ return (other != null) &&
+ this.Member0.Equals(other.Member0) &&
+ Util.CompareObjects<DCType_20>(this.Member1, other.Member1);
+ }
+
+ /// <summary>
+ /// Returns a hash code for this instance.
+ /// </summary>
+ /// <returns>A hash code for this instance.</returns>
+ public override int GetHashCode()
+ {
+ int result = 0;
+ result ^= (this.Member0 == null) ? 0 : this.Member0.GetHashCode();
+ result ^= (this.Member1 == null) ? 0 : this.Member1.GetHashCode();
+ return result;
+ }
+ }
+
+ /// <summary>
+ /// Test type.
+ /// </summary>
+ [DataContract]
+ public class DCType_34
+ {
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public short? Member0 { get; set; }
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public float Member1 { get; set; }
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public EnumType_17 Member2 { get; set; }
+
+ /// <summary>
+ /// Compares this instance with the given object.
+ /// </summary>
+ /// <param name="obj">The object to compare.</param>
+ /// <returns><code>true</code> if the given instance is equal to this one; <code>false</code> otherwise.</returns>
+ public override bool Equals(object obj)
+ {
+ DCType_34 other = obj as DCType_34;
+ return (other != null) &&
+ Util.CompareNullable<short>(this.Member0, other.Member0) &&
+ this.Member1.Equals(other.Member1) &&
+ this.Member2.Equals(other.Member2);
+ }
+
+ /// <summary>
+ /// Returns a hash code for this instance.
+ /// </summary>
+ /// <returns>A hash code for this instance.</returns>
+ public override int GetHashCode()
+ {
+ int result = 0;
+ result ^= (this.Member0 == null) ? 0 : this.Member0.GetHashCode();
+ result ^= this.Member1.GetHashCode();
+ result ^= this.Member2.GetHashCode();
+ return result;
+ }
+ }
+
+ /// <summary>
+ /// Test type.
+ /// </summary>
+ [DataContract]
+ public class DCType_36
+ {
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public bool? Member0 { get; set; }
+
+ /// <summary>
+ /// Compares this instance with the given object.
+ /// </summary>
+ /// <param name="obj">The object to compare.</param>
+ /// <returns><code>true</code> if the given instance is equal to this one; <code>false</code> otherwise.</returns>
+ public override bool Equals(object obj)
+ {
+ DCType_36 other = obj as DCType_36;
+ return (other != null) &&
+ Util.CompareNullable<bool>(this.Member0, other.Member0);
+ }
+
+ /// <summary>
+ /// Returns a hash code for this instance.
+ /// </summary>
+ /// <returns>A hash code for this instance.</returns>
+ public override int GetHashCode()
+ {
+ int result = (this.Member0 == null) ? 0 : this.Member0.GetHashCode();
+ return result;
+ }
+ }
+
+ /// <summary>
+ /// Test type.
+ /// </summary>
+ [CLSCompliant(false)]
+ [DataContract]
+ public class DCType_38
+ {
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public ulong Member0 { get; set; }
+
+ /// <summary>
+ /// Compares this instance with the given object.
+ /// </summary>
+ /// <param name="obj">The object to compare.</param>
+ /// <returns><code>true</code> if the given instance is equal to this one; <code>false</code> otherwise.</returns>
+ public override bool Equals(object obj)
+ {
+ DCType_38 other = obj as DCType_38;
+ return (other != null) &&
+ this.Member0.Equals(other.Member0);
+ }
+
+ /// <summary>
+ /// Returns a hash code for this instance.
+ /// </summary>
+ /// <returns>A hash code for this instance.</returns>
+ public override int GetHashCode()
+ {
+ int result = this.Member0.GetHashCode();
+ return result;
+ }
+ }
+
+ /// <summary>
+ /// Test type.
+ /// </summary>
+ [DataContract]
+ public class DCType_40
+ {
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public SerType_22 Member0 { get; set; }
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public short Member1 { get; set; }
+
+ /// <summary>
+ /// Compares this instance with the given object.
+ /// </summary>
+ /// <param name="obj">The object to compare.</param>
+ /// <returns><code>true</code> if the given instance is equal to this one; <code>false</code> otherwise.</returns>
+ public override bool Equals(object obj)
+ {
+ DCType_40 other = obj as DCType_40;
+ return (other != null) &&
+ Util.CompareObjects<SerType_22>(this.Member0, other.Member0) &&
+ this.Member1.Equals(other.Member1);
+ }
+
+ /// <summary>
+ /// Returns a hash code for this instance.
+ /// </summary>
+ /// <returns>A hash code for this instance.</returns>
+ public override int GetHashCode()
+ {
+ int result = 0;
+ result ^= (this.Member0 == null) ? 0 : this.Member0.GetHashCode();
+ result ^= this.Member1.GetHashCode();
+ return result;
+ }
+ }
+
+ /// <summary>
+ /// Test type.
+ /// </summary>
+ [CLSCompliant(false)]
+ [DataContract]
+ public class DCType_42
+ {
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public SerType_11 Member0 { get; set; }
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public EnumType_35 Member1 { get; set; }
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public SerType_5 Member2 { get; set; }
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public DCType_3 Member3 { get; set; }
+
+ /// <summary>
+ /// Compares this instance with the given object.
+ /// </summary>
+ /// <param name="obj">The object to compare.</param>
+ /// <returns><code>true</code> if the given instance is equal to this one; <code>false</code> otherwise.</returns>
+ public override bool Equals(object obj)
+ {
+ DCType_42 other = obj as DCType_42;
+ return (other != null) &&
+ Util.CompareObjects<SerType_11>(this.Member0, other.Member0) &&
+ this.Member1.Equals(other.Member1) &&
+ Util.CompareObjects<SerType_5>(this.Member2, other.Member2) &&
+ Util.CompareObjects<DCType_3>(this.Member3, other.Member3);
+ }
+
+ /// <summary>
+ /// Returns a hash code for this instance.
+ /// </summary>
+ /// <returns>A hash code for this instance.</returns>
+ public override int GetHashCode()
+ {
+ int result = 0;
+ result ^= (this.Member0 == null) ? 0 : this.Member0.GetHashCode();
+ result ^= this.Member1.GetHashCode();
+ result ^= (this.Member2 == null) ? 0 : this.Member2.GetHashCode();
+ result ^= (this.Member3 == null) ? 0 : this.Member3.GetHashCode();
+ return result;
+ }
+ }
+
+ /// <summary>
+ /// Test type.
+ /// </summary>
+ [CLSCompliant(false)]
+ [DataContract]
+ public class DCType_65
+ {
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public ulong? Member0 { get; set; }
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public DCType_7 Member1 { get; set; }
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public DCType_36 Member4 { get; set; }
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public uint Member5 { get; set; }
+
+ /// <summary>
+ /// Compares this instance with the given object.
+ /// </summary>
+ /// <param name="obj">The object to compare.</param>
+ /// <returns><code>true</code> if the given instance is equal to this one; <code>false</code> otherwise.</returns>
+ public override bool Equals(object obj)
+ {
+ DCType_65 other = obj as DCType_65;
+ return (other != null) &&
+ Util.CompareNullable<ulong>(this.Member0, other.Member0) &&
+ Util.CompareObjects<DCType_7>(this.Member1, other.Member1) &&
+ Util.CompareObjects<DCType_36>(this.Member4, other.Member4) &&
+ this.Member5.Equals(other.Member5);
+ }
+
+ /// <summary>
+ /// Returns a hash code for this instance.
+ /// </summary>
+ /// <returns>A hash code for this instance.</returns>
+ public override int GetHashCode()
+ {
+ int result = 0;
+ result ^= (this.Member0 == null) ? 0 : this.Member0.GetHashCode();
+ result ^= (this.Member1 == null) ? 0 : this.Member1.GetHashCode();
+ result ^= (this.Member4 == null) ? 0 : this.Member4.GetHashCode();
+ result ^= this.Member5.GetHashCode();
+ return result;
+ }
+ }
+
+ /// <summary>
+ /// Test type.
+ /// </summary>
+ [CLSCompliant(false)]
+ public class ListType_1
+ {
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ public List<DCType_15> Member0 { get; set; }
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ public List<DCType_34> Member1 { get; set; }
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ public List<SerType_33> Member2 { get; set; }
+
+ /// <summary>
+ /// Compares this instance with the given object.
+ /// </summary>
+ /// <param name="obj">The object to compare.</param>
+ /// <returns><code>true</code> if the given instance is equal to this one; <code>false</code> otherwise.</returns>
+ public override bool Equals(object obj)
+ {
+ ListType_1 other = obj as ListType_1;
+ return (other != null) &&
+ Util.CompareLists<DCType_15>(this.Member0, other.Member0) &&
+ Util.CompareLists<DCType_34>(this.Member1, other.Member1) &&
+ Util.CompareLists<SerType_33>(this.Member2, other.Member2);
+ }
+
+ /// <summary>
+ /// Returns a hash code for this instance.
+ /// </summary>
+ /// <returns>A hash code for this instance.</returns>
+ public override int GetHashCode()
+ {
+ int result = 0;
+ if (this.Member0 != null)
+ {
+ result ^= Util.ComputeArrayHashCode(this.Member0.ToArray());
+ }
+
+ if (this.Member1 != null)
+ {
+ result ^= Util.ComputeArrayHashCode(this.Member0.ToArray());
+ }
+
+ if (this.Member2 != null)
+ {
+ result ^= Util.ComputeArrayHashCode(this.Member0.ToArray());
+ }
+
+ return result;
+ }
+ }
+
+ /// <summary>
+ /// Test type.
+ /// </summary>
+ [CLSCompliant(false)]
+ [DataContract]
+ public class ListType_2
+ {
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public SerType_4[] Member0 { get; set; }
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public DCType_32[] Member1 { get; set; }
+
+ /// <summary>
+ /// Compares this instance with the given object.
+ /// </summary>
+ /// <param name="obj">The object to compare.</param>
+ /// <returns><code>true</code> if the given instance is equal to this one; <code>false</code> otherwise.</returns>
+ public override bool Equals(object obj)
+ {
+ ListType_2 other = obj as ListType_2;
+ return (other != null) &&
+ Util.CompareArrays(this.Member0, other.Member0) &&
+ Util.CompareArrays(this.Member1, other.Member1);
+ }
+
+ /// <summary>
+ /// Returns a hash code for this instance.
+ /// </summary>
+ /// <returns>A hash code for this instance.</returns>
+ public override int GetHashCode()
+ {
+ int result = 0;
+ result ^= Util.ComputeArrayHashCode(this.Member0);
+ result ^= Util.ComputeArrayHashCode(this.Member0);
+ return result;
+ }
+ }
+
+ /// <summary>
+ /// Test type.
+ /// </summary>
+ [DataContract]
+ [KnownType(typeof(DerivedType))]
+ public class BaseType
+ {
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public string Member0 { get; set; }
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public DCType_1 Member1 { get; set; }
+
+ /// <summary>
+ /// Creates an instance of this type.
+ /// </summary>
+ /// <param name="rndGen">The random generator used to populate this type.</param>
+ /// <returns>An instance of the <see cref="DerivedType"/>.</returns>
+ public static BaseType CreateInstance(Random rndGen)
+ {
+ return new DerivedType(rndGen);
+ }
+
+ /// <summary>
+ /// Returns a debug representation for this instance.
+ /// </summary>
+ /// <returns>A debug representation for this instance.</returns>
+ public override string ToString()
+ {
+ return String.Format(CultureInfo.InvariantCulture, "BaseType<Member0={0},Member1={1}>", Util.EscapeString(this.Member0), Util.EscapeString(this.Member1));
+ }
+ }
+
+ /// <summary>
+ /// Test type.
+ /// </summary>
+ [DataContract]
+ public class DerivedType : BaseType, IEmptyInterface
+ {
+ /// <summary>
+ /// Initializes an instance of this type.
+ /// </summary>
+ /// <param name="rndGen">The random generator used to populate this type.</param>
+ public DerivedType(Random rndGen)
+ {
+ this.Member0 = InstanceCreator.CreateInstanceOf<string>(rndGen);
+ this.Member1 = InstanceCreator.CreateInstanceOf<DCType_1>(rndGen);
+ this.Member2 = InstanceCreator.CreateInstanceOf<SerType_4>(rndGen);
+ this.Member3 = InstanceCreator.CreateInstanceOf<decimal>(rndGen);
+ }
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public SerType_4 Member2 { get; set; }
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public decimal Member3 { get; set; }
+
+ /// <summary>
+ /// Compares this instance with the given object.
+ /// </summary>
+ /// <param name="obj">The object to compare.</param>
+ /// <returns><code>true</code> if the given instance is equal to this one; <code>false</code> otherwise.</returns>
+ public override bool Equals(object obj)
+ {
+ DerivedType other = obj as DerivedType;
+ return (other != null) &&
+ Util.CompareObjects<string>(this.Member0, other.Member0) &&
+ Util.CompareObjects<DCType_1>(this.Member1, other.Member1) &&
+ Util.CompareObjects<SerType_4>(this.Member2, other.Member2) &&
+ this.Member3.Equals(other.Member3);
+ }
+
+ /// <summary>
+ /// Returns a hash code for this instance.
+ /// </summary>
+ /// <returns>A hash code for this instance.</returns>
+ public override int GetHashCode()
+ {
+ int result = 0;
+ result ^= this.Member0 == null ? 0 : this.Member0.GetHashCode();
+ result ^= this.Member1 == null ? 0 : this.Member1.GetHashCode();
+ result ^= this.Member2 == null ? 0 : this.Member2.GetHashCode();
+ result ^= this.Member3.GetHashCode();
+ return result;
+ }
+
+ /// <summary>
+ /// Returns a debug representation for this instance.
+ /// </summary>
+ /// <returns>A debug representation for this instance.</returns>
+ public override string ToString()
+ {
+ return String.Format(
+ CultureInfo.InvariantCulture,
+ "DerivedType<Base={0},Member2={1},Member3={2}>",
+ base.ToString(),
+ Util.EscapeString(this.Member2),
+ Util.EscapeString(this.Member3));
+ }
+ }
+
+ /// <summary>
+ /// Test type.
+ /// </summary>
+ [DataContract]
+ public class PolymorphicMember
+ {
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public BaseType Member_0 { get; set; }
+
+ /// <summary>
+ /// Compares this instance with the given object.
+ /// </summary>
+ /// <param name="obj">The object to compare.</param>
+ /// <returns><code>true</code> if the given instance is equal to this one; <code>false</code> otherwise.</returns>
+ public override bool Equals(object obj)
+ {
+ PolymorphicMember other = obj as PolymorphicMember;
+ return (other != null) &&
+ Util.CompareObjects<BaseType>(this.Member_0, other.Member_0);
+ }
+
+ /// <summary>
+ /// Returns a hash code for this instance.
+ /// </summary>
+ /// <returns>A hash code for this instance.</returns>
+ public override int GetHashCode()
+ {
+ return this.Member_0 == null ? 0 : this.Member_0.GetHashCode();
+ }
+ }
+
+ /// <summary>
+ /// Test type.
+ /// </summary>
+ [DataContract, KnownType(typeof(DerivedType))]
+ public class PolymorphicAsInterfaceMember
+ {
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public IEmptyInterface Member_0 { get; set; }
+
+ /// <summary>
+ /// Compares this instance with the given object.
+ /// </summary>
+ /// <param name="obj">The object to compare.</param>
+ /// <returns><code>true</code> if the given instance is equal to this one; <code>false</code> otherwise.</returns>
+ public override bool Equals(object obj)
+ {
+ PolymorphicAsInterfaceMember other = obj as PolymorphicAsInterfaceMember;
+ return (other != null) &&
+ Util.CompareObjects<IEmptyInterface>(this.Member_0, other.Member_0);
+ }
+
+ /// <summary>
+ /// Returns a hash code for this instance.
+ /// </summary>
+ /// <returns>A hash code for this instance.</returns>
+ public override int GetHashCode()
+ {
+ return this.Member_0 == null ? 0 : this.Member_0.GetHashCode();
+ }
+ }
+
+ /// <summary>
+ /// Test type.
+ /// </summary>
+ [DataContract]
+ [KnownType(typeof(DerivedType))]
+ public class CollectionsWithPolymorphicMember
+ {
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public List<BaseType> ListOfBase { get; set; }
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public List<IEmptyInterface> ListOfInterface { get; set; }
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public Dictionary<string, BaseType> DictionaryOfBase { get; set; }
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ [DataMember]
+ public Dictionary<string, IEmptyInterface> DictionaryOfInterface { get; set; }
+
+ /// <summary>
+ /// Compares this instance with the given object.
+ /// </summary>
+ /// <param name="obj">The object to compare.</param>
+ /// <returns><code>true</code> if the given instance is equal to this one; <code>false</code> otherwise.</returns>
+ public override bool Equals(object obj)
+ {
+ CollectionsWithPolymorphicMember other = obj as CollectionsWithPolymorphicMember;
+ return (other != null) &&
+ Util.CompareLists<BaseType>(this.ListOfBase, other.ListOfBase) &&
+ Util.CompareLists<IEmptyInterface>(this.ListOfInterface, other.ListOfInterface) &&
+ Util.CompareDictionaries<string, BaseType>(this.DictionaryOfBase, other.DictionaryOfBase) &&
+ Util.CompareDictionaries<string, IEmptyInterface>(this.DictionaryOfInterface, other.DictionaryOfInterface);
+ }
+
+ /// <summary>
+ /// Returns a hash code for this instance.
+ /// </summary>
+ /// <returns>A hash code for this instance.</returns>
+ public override int GetHashCode()
+ {
+ int result = 0;
+ result ^= this.ListOfBase == null ? 0 : Util.ComputeArrayHashCode(this.ListOfBase.ToArray());
+ result ^= this.ListOfInterface == null ? 0 : Util.ComputeArrayHashCode(this.ListOfInterface.ToArray());
+ result ^= this.DictionaryOfBase == null ? 0 : Util.ComputeArrayHashCode(new List<string>(this.DictionaryOfBase.Keys).ToArray());
+ result ^= this.DictionaryOfBase == null ? 0 : Util.ComputeArrayHashCode(new List<BaseType>(this.DictionaryOfBase.Values).ToArray());
+ result ^= this.DictionaryOfInterface == null ? 0 : Util.ComputeArrayHashCode(new List<string>(this.DictionaryOfInterface.Keys).ToArray());
+ result ^= this.DictionaryOfInterface == null ? 0 : Util.ComputeArrayHashCode(new List<IEmptyInterface>(this.DictionaryOfInterface.Values).ToArray());
+ return result;
+ }
+
+ /// <summary>
+ /// Returns a debug representation for this instance.
+ /// </summary>
+ /// <returns>A debug representation for this instance.</returns>
+ public override string ToString()
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.Append("CollectionsWithPolymorphicMember<");
+ PrintList(sb, "ListOfBase", this.ListOfBase);
+ sb.Append(", ");
+ PrintList(sb, "ListOfInterface", this.ListOfBase);
+ sb.Append(", ");
+ PrintDictionary(sb, "DictionaryOfBase", this.DictionaryOfBase);
+ sb.Append(", ");
+ PrintDictionary(sb, "DictionaryOfInterface", this.DictionaryOfInterface);
+ sb.Append('>');
+ return sb.ToString();
+ }
+
+ private static void PrintList<T>(StringBuilder sb, string name, List<T> list)
+ {
+ sb.Append(name);
+ sb.Append('=');
+ if (list == null)
+ {
+ sb.Append("<<null>>");
+ }
+ else
+ {
+ sb.Append('[');
+ for (int i = 0; i < list.Count; i++)
+ {
+ if (i > 0)
+ {
+ sb.Append(',');
+ }
+
+ sb.Append(Util.EscapeString(list[i]));
+ }
+
+ sb.Append(']');
+ }
+ }
+
+ private static void PrintDictionary<T>(StringBuilder sb, string name, Dictionary<string, T> dict)
+ {
+ sb.Append(name);
+ sb.Append('=');
+ if (dict == null)
+ {
+ sb.Append("<<null>>");
+ }
+ else
+ {
+ sb.Append('{');
+ bool first = true;
+ foreach (string key in dict.Keys)
+ {
+ if (first)
+ {
+ first = false;
+ }
+ else
+ {
+ sb.Append(',');
+ }
+
+ sb.AppendFormat("\"{0}\":", Util.EscapeString(key));
+ sb.Append(Util.EscapeString(dict[key]));
+ }
+
+ sb.Append('}');
+ }
+ }
+ }
+
+ /// <summary>
+ /// Test type.
+ /// </summary>
+ public class Person
+ {
+ internal const string Letters = "abcdefghijklmnopqrstuvwxyz";
+
+ /// <summary>
+ /// Initializes an instance of this class.
+ /// </summary>
+ public Person()
+ {
+ this.Friends = new List<Person>();
+ }
+
+ /// <summary>
+ /// Initializes an instance of this class.
+ /// </summary>
+ /// <param name="rndGen">The random generator used to populate this type.</param>
+ public Person(Random rndGen)
+ {
+ this.Name = PrimitiveCreator.CreateInstanceOfString(rndGen, rndGen.Next(5, 15), Letters);
+ this.Age = PrimitiveCreator.CreateInstanceOfInt32(rndGen);
+ this.Address = new Address(rndGen);
+ this.Friends = new List<Person>();
+ }
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ public int Age { get; set; }
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ public Address Address { get; set; }
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ public List<Person> Friends { get; set; }
+
+ /// <summary>
+ /// Adds new instances of <see cref="Person"/> as <see cref="Person.Friends"/> of this instance.
+ /// </summary>
+ /// <param name="count">The number of instances to add.</param>
+ /// <param name="rndGen">The random generator used to populate the instances.</param>
+ public void AddFriends(int count, Random rndGen)
+ {
+ for (int i = 0; i < count; i++)
+ {
+ this.Friends.Add(new Person(rndGen));
+ }
+ }
+
+ /// <summary>
+ /// Returns a string representation of the "friends" of this instance, for logging purposes.
+ /// </summary>
+ /// <returns>A string representation of the "friends" of this instance.</returns>
+ public string FriendsToString()
+ {
+ string s = "";
+
+ foreach (Person p in this.Friends)
+ {
+ s += p + ",";
+ }
+
+ return s;
+ }
+
+ /// <summary>
+ /// Returns a readable representation of a <see cref="Person"/> instance.
+ /// </summary>
+ /// <returns>A readable representation of this instance.</returns>
+ public override string ToString()
+ {
+ return String.Format("Person{{{0}, {1}, [{2}], Friends=[{3}]}}", this.Name, this.Age, this.Address, this.FriendsToString());
+ }
+ }
+
+ /// <summary>
+ /// Test type.
+ /// </summary>
+ public class Address
+ {
+ /// <summary>
+ /// Initializes an instance of this class.
+ /// </summary>
+ public Address()
+ {
+ }
+
+ /// <summary>
+ /// Initializes an instance of this class.
+ /// </summary>
+ /// <param name="rndGen">The random generator used to populate this type.</param>
+ public Address(Random rndGen)
+ {
+ this.Street = PrimitiveCreator.CreateInstanceOfString(rndGen, rndGen.Next(5, 15), Person.Letters);
+ this.City = PrimitiveCreator.CreateInstanceOfString(rndGen, rndGen.Next(5, 15), Person.Letters);
+ this.State = PrimitiveCreator.CreateInstanceOfString(rndGen, rndGen.Next(5, 15), Person.Letters);
+ }
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ public string Street { get; set; }
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ public string City { get; set; }
+
+ /// <summary>
+ /// Test member.
+ /// </summary>
+ public string State { get; set; }
+
+ /// <summary>
+ /// Returns a readable representation of a <see cref="Address"/> instance.
+ /// </summary>
+ /// <returns>A readable representation of this instance.</returns>
+ public override string ToString()
+ {
+ return String.Format("Address{{{0}, {1}, {2}}}", this.Street, this.City, this.State);
+ }
+ }
+}
+
diff --git a/test/System.Json.Test.Integration/Common/Util.cs b/test/System.Json.Test.Integration/Common/Util.cs
new file mode 100644
index 00000000..26b65231
--- /dev/null
+++ b/test/System.Json.Test.Integration/Common/Util.cs
@@ -0,0 +1,201 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace System.Json
+{
+ internal static class Util
+ {
+ public static bool CompareObjects<T>(T o1, T o2) where T : class
+ {
+ if ((o1 == null) != (o2 == null))
+ {
+ return false;
+ }
+
+ return (o1 == null) || o1.Equals(o2);
+ }
+
+ public static bool CompareNullable<T>(Nullable<T> n1, Nullable<T> n2) where T : struct
+ {
+ if (n1.HasValue != n2.HasValue)
+ {
+ return false;
+ }
+
+ return (!n1.HasValue) || n1.Value.Equals(n2.Value);
+ }
+
+ public static bool CompareLists<T>(List<T> list1, List<T> list2)
+ {
+ if (list1 == null)
+ {
+ return list2 == null;
+ }
+
+ if (list2 == null)
+ {
+ return false;
+ }
+
+ return CompareArrays(list1.ToArray(), list2.ToArray());
+ }
+
+ public static bool CompareDictionaries<K, V>(Dictionary<K, V> dict1, Dictionary<K, V> dict2)
+ where K : IComparable
+ where V : class
+ {
+ if (dict1 == null)
+ {
+ return dict2 == null;
+ }
+
+ if (dict2 == null)
+ {
+ return false;
+ }
+
+ List<K> keys1 = new List<K>(dict1.Keys);
+ List<K> keys2 = new List<K>(dict2.Keys);
+ keys1.Sort();
+ keys2.Sort();
+ if (!CompareLists<K>(keys1, keys2))
+ {
+ return false;
+ }
+
+ foreach (K key in keys1)
+ {
+ V value1 = dict1[key];
+ V value2 = dict2[key];
+ if (!CompareObjects<V>(value1, value2))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public static bool CompareArrays(Array array1, Array array2)
+ {
+ if (array1 == null)
+ {
+ return array2 == null;
+ }
+
+ if (array2 == null || array1.Length != array2.Length)
+ {
+ return false;
+ }
+
+ for (int i = 0; i < array1.Length; i++)
+ {
+ object o1 = array1.GetValue(i);
+ object o2 = array2.GetValue(i);
+ if ((o1 == null) != (o2 == null))
+ {
+ return false;
+ }
+
+ if (o1 != null)
+ {
+ if ((o1 is Array) && (o2 is Array))
+ {
+ if (!CompareArrays((Array)o1, (Array)o2))
+ {
+ return false;
+ }
+ }
+ else if (o1 is IEnumerable && o2 is IEnumerable)
+ {
+ if (!CompareArrays(ToObjectArray((IEnumerable)o1), ToObjectArray((IEnumerable)o2)))
+ {
+ return false;
+ }
+ }
+ else
+ {
+ if (!o1.Equals(o2))
+ {
+ return false;
+ }
+ }
+ }
+ }
+
+ return true;
+ }
+
+ public static int ComputeArrayHashCode(Array array)
+ {
+ if (array == null)
+ {
+ return 0;
+ }
+
+ int result = 0;
+ result += array.Length;
+ for (int i = 0; i < array.Length; i++)
+ {
+ object o = array.GetValue(i);
+ if (o != null)
+ {
+ if (o is Array)
+ {
+ result ^= ComputeArrayHashCode((Array)o);
+ }
+ else if (o is Enumerable)
+ {
+ result ^= ComputeArrayHashCode(ToObjectArray((IEnumerable)o));
+ }
+ else
+ {
+ result ^= o.GetHashCode();
+ }
+ }
+ }
+
+ return result;
+ }
+
+ public static string EscapeString(object obj)
+ {
+ StringBuilder sb = new StringBuilder();
+ if (obj == null)
+ {
+ return "<<null>>";
+ }
+ else
+ {
+ string str = obj.ToString();
+ for (int i = 0; i < str.Length; i++)
+ {
+ char c = str[i];
+ if (c < ' ' || c > '~')
+ {
+ sb.AppendFormat("\\u{0:X4}", (int)c);
+ }
+ else
+ {
+ sb.Append(c);
+ }
+ }
+ }
+
+ return sb.ToString();
+ }
+
+ static object[] ToObjectArray(IEnumerable enumerable)
+ {
+ List<object> result = new List<object>();
+ foreach (var item in enumerable)
+ {
+ result.Add(item);
+ }
+
+ return result.ToArray();
+ }
+ }
+} \ No newline at end of file
diff --git a/test/System.Json.Test.Integration/JObjectFunctionalTest.cs b/test/System.Json.Test.Integration/JObjectFunctionalTest.cs
new file mode 100644
index 00000000..6a1cdd86
--- /dev/null
+++ b/test/System.Json.Test.Integration/JObjectFunctionalTest.cs
@@ -0,0 +1,990 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.Text;
+using System.Threading;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Json
+{
+ /// <summary>
+ /// Functional tests for the JsonObject class.
+ /// </summary>
+ public class JObjectFunctionalTest
+ {
+ static int iterationCount = 500;
+ static int arrayLength = 10;
+
+ /// <summary>
+ /// Validates round-trip of a JsonArray containing both primitives and objects.
+ /// </summary>
+ [Fact]
+ public void MixedJsonTypeFunctionalTest()
+ {
+ bool oldValue = CreatorSettings.CreateDateTimeWithSubMilliseconds;
+ CreatorSettings.CreateDateTimeWithSubMilliseconds = false;
+ try
+ {
+ int seed = 1;
+
+ for (int i = 0; i < iterationCount; i++)
+ {
+ seed++;
+ Log.Info("Seed: {0}", seed);
+ Random rndGen = new Random(seed);
+
+ JsonArray sourceJson = new JsonArray(new List<JsonValue>()
+ {
+ PrimitiveCreator.CreateInstanceOfBoolean(rndGen),
+ PrimitiveCreator.CreateInstanceOfByte(rndGen),
+ PrimitiveCreator.CreateInstanceOfDateTime(rndGen),
+ PrimitiveCreator.CreateInstanceOfDateTimeOffset(rndGen),
+ PrimitiveCreator.CreateInstanceOfDecimal(rndGen),
+ PrimitiveCreator.CreateInstanceOfDouble(rndGen),
+ PrimitiveCreator.CreateInstanceOfInt16(rndGen),
+ PrimitiveCreator.CreateInstanceOfInt32(rndGen),
+ PrimitiveCreator.CreateInstanceOfInt64(rndGen),
+ PrimitiveCreator.CreateInstanceOfSByte(rndGen),
+ PrimitiveCreator.CreateInstanceOfSingle(rndGen),
+ PrimitiveCreator.CreateInstanceOfString(rndGen),
+ PrimitiveCreator.CreateInstanceOfUInt16(rndGen),
+ PrimitiveCreator.CreateInstanceOfUInt32(rndGen),
+ PrimitiveCreator.CreateInstanceOfUInt64(rndGen),
+ new JsonObject(new Dictionary<string, JsonValue>()
+ {
+ { "Boolean", PrimitiveCreator.CreateInstanceOfBoolean(rndGen) },
+ { "Byte", PrimitiveCreator.CreateInstanceOfByte(rndGen) },
+ { "DateTime", PrimitiveCreator.CreateInstanceOfDateTime(rndGen) },
+ { "DateTimeOffset", PrimitiveCreator.CreateInstanceOfDateTimeOffset(rndGen) },
+ { "Decimal", PrimitiveCreator.CreateInstanceOfDecimal(rndGen) },
+ { "Double", PrimitiveCreator.CreateInstanceOfDouble(rndGen) },
+ { "Int16", PrimitiveCreator.CreateInstanceOfInt16(rndGen) },
+ { "Int32", PrimitiveCreator.CreateInstanceOfInt32(rndGen) },
+ { "Int64", PrimitiveCreator.CreateInstanceOfInt64(rndGen) },
+ { "SByte", PrimitiveCreator.CreateInstanceOfSByte(rndGen) },
+ { "Single", PrimitiveCreator.CreateInstanceOfSingle(rndGen) },
+ { "String", PrimitiveCreator.CreateInstanceOfString(rndGen) },
+ { "UInt16", PrimitiveCreator.CreateInstanceOfUInt16(rndGen) },
+ { "UInt32", PrimitiveCreator.CreateInstanceOfUInt32(rndGen) },
+ { "UInt64", PrimitiveCreator.CreateInstanceOfUInt64(rndGen) }
+ })
+ });
+
+ JsonArray newJson = (JsonArray)JsonValue.Parse(sourceJson.ToString());
+ Assert.True(JsonValueVerifier.Compare(sourceJson, newJson));
+ }
+ }
+ finally
+ {
+ CreatorSettings.CreateDateTimeWithSubMilliseconds = oldValue;
+ }
+ }
+
+ /// <summary>
+ /// Tests for the <see cref="System.Json.JsonArray.CopyTo"/> method.
+ /// </summary>
+ [Fact]
+ public void JsonArrayCopytoFunctionalTest()
+ {
+ int seed = 1;
+
+ for (int i = 0; i < iterationCount / 10; i++)
+ {
+ seed++;
+ Log.Info("Seed: {0}", seed);
+ Random rndGen = new Random(seed);
+
+ bool retValue = true;
+
+ JsonArray sourceJson = SpecialJsonValueHelper.CreatePrePopulatedJsonArray(seed, arrayLength);
+ JsonValue[] destJson = new JsonValue[arrayLength];
+ sourceJson.CopyTo(destJson, 0);
+
+ for (int k = 0; k < destJson.Length; k++)
+ {
+ if (destJson[k] != sourceJson[k])
+ {
+ retValue = false;
+ }
+ }
+
+ Assert.True(retValue, "[JsonArrayCopytoFunctionalTest] JsonArray.CopyTo() failed to function properly. destJson.GetLength(0) = " + destJson.GetLength(0));
+ }
+ }
+
+ /// <summary>
+ /// Tests for add and remove methods in the <see cref="System.Json.JsonArray"/> class.
+ /// </summary>
+ [Fact]
+ public void JsonArrayAddRemoveFunctionalTest()
+ {
+ int seed = 1;
+
+ for (int i = 0; i < iterationCount / 10; i++)
+ {
+ seed++;
+ Log.Info("Seed: {0}", seed);
+ Random rndGen = new Random(seed);
+ bool retValue = true;
+
+ JsonArray sourceJson = SpecialJsonValueHelper.CreatePrePopulatedJsonArray(seed, arrayLength);
+ JsonValue[] cloneJson = SpecialJsonValueHelper.CreatePrePopulatedJsonValueArray(seed, 3);
+
+ // JsonArray.AddRange(JsonValue[])
+ sourceJson.AddRange(cloneJson);
+ if (sourceJson.Count != arrayLength + cloneJson.Length)
+ {
+ Log.Info("[JsonArrayAddRemoveFunctionalTest] JsonArray.AddRange(JsonValue[]) failed to function properly.");
+ retValue = false;
+ }
+ else
+ {
+ Log.Info("[JsonArrayAddRemoveFunctionalTest] JsonArray.AddRange(JsonValue[]) passed test.");
+ }
+
+ // JsonArray.RemoveAt(int)
+ int count = sourceJson.Count;
+ for (int j = 0; j < count; j++)
+ {
+ sourceJson.RemoveAt(0);
+ }
+
+ if (sourceJson.Count > 0)
+ {
+ Log.Info("[JsonArrayAddRemoveFunctionalTest] JsonArray.RemoveAt(int) failed to function properly.");
+ retValue = false;
+ }
+ else
+ {
+ Log.Info("[JsonArrayAddRemoveFunctionalTest] JsonArray.RemoveAt(int) passed test.");
+ }
+
+ // JsonArray.JsonType
+ if (sourceJson.JsonType != JsonType.Array)
+ {
+ Log.Info("[JsonArrayAddRemoveFunctionalTest] JsonArray.JsonType failed to function properly.");
+ retValue = false;
+ }
+
+ // JsonArray.Clear()
+ sourceJson = SpecialJsonValueHelper.CreatePrePopulatedJsonArray(seed, arrayLength);
+ sourceJson.Clear();
+ if (sourceJson.Count > 0)
+ {
+ Log.Info("[JsonArrayAddRemoveFunctionalTest] JsonArray.Clear() failed to function properly.");
+ retValue = false;
+ }
+ else
+ {
+ Log.Info("[JsonArrayAddRemoveFunctionalTest] JsonArray.Clear() passed test.");
+ }
+
+ // JsonArray.AddRange(JsonValue)
+ sourceJson = SpecialJsonValueHelper.CreatePrePopulatedJsonArray(seed, arrayLength);
+
+ // adding one additional value to the array
+ sourceJson.AddRange(SpecialJsonValueHelper.GetRandomJsonPrimitives(seed));
+ if (sourceJson.Count != arrayLength + 1)
+ {
+ Log.Info("[JsonArrayAddRemoveFunctionalTest] JsonArray.AddRange(JsonValue) failed to function properly.");
+ retValue = false;
+ }
+ else
+ {
+ Log.Info("[JsonArrayAddRemoveFunctionalTest] JsonArray.AddRange(JsonValue) passed test.");
+ }
+
+ // JsonArray.AddRange(IEnumerable<JsonValue> items)
+ sourceJson = SpecialJsonValueHelper.CreatePrePopulatedJsonArray(seed, arrayLength);
+ MyJsonValueCollection<JsonValue> myCols = new MyJsonValueCollection<JsonValue>();
+ myCols.Add(new JsonPrimitive(PrimitiveCreator.CreateInstanceOfUInt32(rndGen)));
+ string str;
+ do
+ {
+ str = PrimitiveCreator.CreateInstanceOfString(rndGen);
+ } while (str == null);
+
+ myCols.Add(new JsonPrimitive(str));
+ myCols.Add(new JsonPrimitive(PrimitiveCreator.CreateInstanceOfDateTime(rndGen)));
+
+ // adding 3 additional value to the array
+ sourceJson.AddRange(myCols);
+ if (sourceJson.Count != arrayLength + 3)
+ {
+ Log.Info("[JsonArrayAddRemoveFunctionalTest] JsonArray.AddRange(IEnumerable<JsonValue> items) failed to function properly.");
+ retValue = false;
+ }
+ else
+ {
+ Log.Info("[JsonArrayAddRemoveFunctionalTest] JsonArray.AddRange(IEnumerable<JsonValue> items) passed test.");
+ }
+
+ // JsonArray[index].set_Item
+ sourceJson = SpecialJsonValueHelper.CreatePrePopulatedJsonArray(seed, arrayLength);
+ string temp;
+ do
+ {
+ temp = PrimitiveCreator.CreateInstanceOfString(rndGen);
+ } while (temp == null);
+
+ sourceJson[1] = temp;
+ if ((string)sourceJson[1] != temp)
+ {
+ Log.Info("[JsonArrayAddRemoveFunctionalTest] JsonArray[index].set_Item failed to function properly.");
+ retValue = false;
+ }
+ else
+ {
+ Log.Info("[JsonArrayAddRemoveFunctionalTest] JsonArray[index].set_Item passed test.");
+ }
+
+ // JsonArray.Remove(JsonValue)
+ count = sourceJson.Count;
+ for (int j = 0; j < count; j++)
+ {
+ sourceJson.Remove(sourceJson[0]);
+ }
+
+ if (sourceJson.Count > 0)
+ {
+ Log.Info("[JsonArrayAddRemoveFunctionalTest] JsonArray.Remove(JsonValue) failed to function properly.");
+ retValue = false;
+ }
+
+ Assert.True(retValue);
+ }
+ }
+
+ /// <summary>
+ /// Tests for indexers in the <see cref="System.Json.JsonArray"/> class.
+ /// </summary>
+ [Fact]
+ public void JsonArrayItemsFunctionalTest()
+ {
+ int seed = 1;
+
+ for (int i = 0; i < iterationCount / 10; i++)
+ {
+ seed++;
+ Log.Info("Seed: {0}", seed);
+ Random rndGen = new Random(seed);
+ bool retValue = true;
+
+ // JsonArray.Contains(JsonValue)
+ // JsonArray.IndexOf(JsonValue)
+ JsonArray sourceJson = SpecialJsonValueHelper.CreatePrePopulatedJsonArray(seed, arrayLength);
+ for (int j = 0; j < sourceJson.Count; j++)
+ {
+ if (!sourceJson.Contains(sourceJson[j]))
+ {
+ Log.Info("[JsonArrayItemsFunctionalTest] JsonArray.Contains(JsonValue) failed to function properly.");
+ retValue = false;
+ }
+ else
+ {
+ Log.Info("[JsonArrayItemsFunctionalTest] JsonArray.Contains(JsonValue) passed test.");
+ }
+
+ if (sourceJson.IndexOf(sourceJson[j]) != j)
+ {
+ Log.Info("[JsonArrayItemsFunctionalTest] JsonArray.IndexOf(JsonValue) failed to function properly.");
+ retValue = false;
+ }
+ else
+ {
+ Log.Info("[JsonArrayItemsFunctionalTest] JsonArray.IndexOf(JsonValue) passed test.");
+ }
+ }
+
+ // JsonArray.Insert(int, JsonValue)
+ JsonValue newItem = SpecialJsonValueHelper.GetRandomJsonPrimitives(seed);
+ sourceJson.Insert(3, newItem);
+ if (sourceJson[3] != newItem || sourceJson.Count != arrayLength + 1)
+ {
+ Log.Info("[JsonArrayItemsFunctionalTest] JsonArray.Insert(int, JsonValue) failed to function properly.");
+ retValue = false;
+ }
+ else
+ {
+ Log.Info("[JsonArrayItemsFunctionalTest] JsonArray.Insert(int, JsonValue) passed test.");
+ }
+
+ Assert.True(retValue);
+ }
+ }
+
+ /// <summary>
+ /// Tests for the CopyTo methods in the <see cref="System.Json.JsonObject"/> class.
+ /// </summary>
+ [Fact]
+ public void JsonObjectCopytoFunctionalTest()
+ {
+ int seed = 1;
+
+ for (int i = 0; i < iterationCount / 10; i++)
+ {
+ seed++;
+ Log.Info("Seed: {0}", seed);
+ Random rndGen = new Random(seed);
+
+ bool retValue = true;
+
+ JsonObject sourceJson = SpecialJsonValueHelper.CreateIndexPopulatedJsonObject(seed, arrayLength);
+ KeyValuePair<string, JsonValue>[] destJson = new KeyValuePair<string, JsonValue>[arrayLength];
+ if (sourceJson != null && destJson != null)
+ {
+ sourceJson.CopyTo(destJson, 0);
+ }
+ else
+ {
+ Log.Info("[JsonObjectCopytoFunctionalTest] sourceJson.ToString() = " + sourceJson.ToString());
+ Log.Info("[JsonObjectCopytoFunctionalTest] destJson.ToString() = " + destJson.ToString());
+ Assert.False(true, "[JsonObjectCopytoFunctionalTest] failed to create the source JsonObject object.");
+ return;
+ }
+
+ if (destJson.Length == arrayLength)
+ {
+ for (int k = 0; k < destJson.Length; k++)
+ {
+ JsonValue temp;
+ sourceJson.TryGetValue(k.ToString(), out temp);
+ if (!(temp != null && destJson[k].Value == temp))
+ {
+ retValue = false;
+ }
+ }
+ }
+ else
+ {
+ retValue = false;
+ }
+
+ Assert.True(retValue, "[JsonObjectCopytoFunctionalTest] JsonObject.CopyTo() failed to function properly. destJson.GetLength(0) = " + destJson.GetLength(0));
+ }
+ }
+
+ /// <summary>
+ /// Tests for the add and remove methods in the <see cref="System.Json.JsonObject"/> class.
+ /// </summary>
+ [Fact]
+ public void JsonObjectAddRemoveFunctionalTest()
+ {
+ int seed = 1;
+
+ for (int i = 0; i < iterationCount / 10; i++)
+ {
+ seed++;
+ Log.Info("Seed: {0}", seed);
+ Random rndGen = new Random(seed);
+ bool retValue = true;
+
+ JsonObject sourceJson = SpecialJsonValueHelper.CreateIndexPopulatedJsonObject(seed, arrayLength);
+
+ // JsonObject.JsonType
+ if (sourceJson.JsonType != JsonType.Object)
+ {
+ Log.Info("[JsonObjectAddRemoveFunctionalTest] JsonArray.JsonType failed to function properly.");
+ retValue = false;
+ }
+
+ // JsonObject.Add(KeyValuePair<string, JsonValue> item)
+ // JsonObject.Add(string key, JsonValue value)
+ // + various numers below so .AddRange() won't try to add an already existing value
+ sourceJson.Add(SpecialJsonValueHelper.GetUniqueNonNullInstanceOfString(seed + 3, sourceJson), SpecialJsonValueHelper.GetUniqueValue(seed, sourceJson));
+ KeyValuePair<string, JsonValue> kvp;
+ int startingSeed = seed + 1;
+ do
+ {
+ kvp = SpecialJsonValueHelper.CreatePrePopulatedKeyValuePair(startingSeed);
+ startingSeed++;
+ }
+ while (sourceJson.ContainsKey(kvp.Key));
+
+ sourceJson.Add(kvp);
+ do
+ {
+ kvp = SpecialJsonValueHelper.CreatePrePopulatedKeyValuePair(startingSeed);
+ startingSeed++;
+ }
+ while (sourceJson.ContainsKey(kvp.Key));
+
+ sourceJson.Add(kvp);
+ if (sourceJson.Count != arrayLength + 3)
+ {
+ Log.Info("[JsonObjectAddRemoveFunctionalTest] JsonObject.Add() failed to function properly.");
+ retValue = false;
+ }
+ else
+ {
+ Log.Info("[JsonObjectAddRemoveFunctionalTest] JsonObject.Add() passed test.");
+ }
+
+ // JsonObject.Clear()
+ sourceJson.Clear();
+ if (sourceJson.Count > 0)
+ {
+ Log.Info("[JsonObjectAddRemoveFunctionalTest] JsonObject.Clear() failed to function properly.");
+ retValue = false;
+ }
+ else
+ {
+ Log.Info("[JsonObjectAddRemoveFunctionalTest] JsonObject.Clear() passed test.");
+ }
+
+ // JsonObject.AddRange(IEnumerable<KeyValuePair<string, JsonValue>> items)
+ sourceJson = SpecialJsonValueHelper.CreateIndexPopulatedJsonObject(seed, arrayLength);
+
+ // + various numers below so .AddRange() won't try to add an already existing value
+ sourceJson.AddRange(SpecialJsonValueHelper.CreatePrePopulatedListofKeyValuePair(seed + 13 + (arrayLength * 2), 5));
+ if (sourceJson.Count != arrayLength + 5)
+ {
+ Log.Info("[JsonObjectAddRemoveFunctionalTest] JsonObject.AddRange(IEnumerable<KeyValuePair<string, JsonValue>> items) failed to function properly.");
+ retValue = false;
+ }
+ else
+ {
+ Log.Info("[JsonObjectAddRemoveFunctionalTest] JsonObject.AddRange(IEnumerable<KeyValuePair<string, JsonValue>> items) passed test.");
+ }
+
+ // JsonObject.AddRange(params KeyValuePair<string, JsonValue>[] items)
+ sourceJson = SpecialJsonValueHelper.CreateIndexPopulatedJsonObject(seed, arrayLength);
+
+ // + various numers below so .AddRange() won't try to add an already existing value
+ KeyValuePair<string, JsonValue> item1 = SpecialJsonValueHelper.CreatePrePopulatedKeyValuePair(seed + arrayLength + 41);
+ KeyValuePair<string, JsonValue> item2 = SpecialJsonValueHelper.CreatePrePopulatedKeyValuePair(seed + arrayLength + 47);
+ KeyValuePair<string, JsonValue> item3 = SpecialJsonValueHelper.CreatePrePopulatedKeyValuePair(seed + arrayLength + 53);
+ sourceJson.AddRange(new KeyValuePair<string, JsonValue>[] { item1, item2, item3 });
+ if (sourceJson.Count != arrayLength + 3)
+ {
+ Log.Info("[JsonObjectAddRemoveFunctionalTest] JsonObject.AddRange(params KeyValuePair<string, JsonValue>[] items) failed to function properly.");
+ retValue = false;
+ }
+ else
+ {
+ Log.Info("[JsonObjectAddRemoveFunctionalTest] JsonObject.AddRange(params KeyValuePair<string, JsonValue>[] items) passed test.");
+ }
+
+ sourceJson.Clear();
+
+ // JsonObject.Remove(Key)
+ sourceJson = SpecialJsonValueHelper.CreateIndexPopulatedJsonObject(seed, arrayLength);
+ int count = sourceJson.Count;
+ List<string> keys = new List<string>(sourceJson.Keys);
+ foreach (string key in keys)
+ {
+ sourceJson.Remove(key);
+ }
+
+ if (sourceJson.Count > 0)
+ {
+ Log.Info("[JsonObjectAddRemoveFunctionalTest] JsonObject.Remove(Key) failed to function properly.");
+ retValue = false;
+ }
+ else
+ {
+ Log.Info("[JsonObjectAddRemoveFunctionalTest] JsonObject.Remove(Key) passed test.");
+ }
+
+ Assert.True(retValue);
+ }
+ }
+
+ /// <summary>
+ /// Tests for the indexers in the <see cref="System.Json.JsonObject"/> class.
+ /// </summary>
+ [Fact]
+ public void JsonObjectItemsFunctionalTest()
+ {
+ int seed = 1;
+
+ for (int i = 0; i < iterationCount / 10; i++)
+ {
+ seed++;
+ Log.Info("Seed: {0}", seed);
+ Random rndGen = new Random(seed);
+ bool retValue = true;
+
+ JsonObject sourceJson = SpecialJsonValueHelper.CreateIndexPopulatedJsonObject(seed, arrayLength);
+
+ // JsonObject[key].set_Item
+ sourceJson["1"] = new JsonPrimitive(true);
+ if (sourceJson["1"].ToString() != "true")
+ {
+ Log.Info("[JsonObjectItemsFunctionalTest] JsonObject[key].set_Item failed to function properly.");
+ retValue = false;
+ }
+ else
+ {
+ Log.Info("[JsonObjectItemsFunctionalTest] JsonObject[key].set_Item passed test.");
+ }
+
+ // ICollection<KeyValuePair<string, JsonValue>>.Contains(KeyValuePair<string, JsonValue> item)
+ KeyValuePair<string, System.Json.JsonValue> kp = new KeyValuePair<string, JsonValue>("5", sourceJson["5"]);
+ if (!((ICollection<KeyValuePair<string, JsonValue>>)sourceJson).Contains(kp))
+ {
+ Log.Info("[JsonObjectItemsFunctionalTest] ICollection<KeyValuePair<string, JsonValue>>.Contains(KeyValuePair<string, JsonValue> item) failed to function properly.");
+ retValue = false;
+ }
+ else
+ {
+ Log.Info("[JsonObjectItemsFunctionalTest] ICollection<KeyValuePair<string, JsonValue>>.Contains(KeyValuePair<string, JsonValue> item) passed test.");
+ }
+
+ // ICollection<KeyValuePair<string, JsonValue>>.IsReadOnly
+ if (((ICollection<KeyValuePair<string, JsonValue>>)sourceJson).IsReadOnly)
+ {
+ Log.Info("[JsonObjectItemsFunctionalTest] ICollection<KeyValuePair<string, JsonValue>>.IsReadOnly failed to function properly.");
+ retValue = false;
+ }
+ else
+ {
+ Log.Info("[JsonObjectItemsFunctionalTest] ICollection<KeyValuePair<string, JsonValue>>.IsReadOnly passed test.");
+ }
+
+ // ICollection<KeyValuePair<string, JsonValue>>.Add(KeyValuePair<string, JsonValue> item)
+ kp = new KeyValuePair<string, JsonValue>("100", new JsonPrimitive(100));
+ ((ICollection<KeyValuePair<string, JsonValue>>)sourceJson).Add(kp);
+ if (sourceJson.Count != arrayLength + 1)
+ {
+ Log.Info("[JsonObjectItemsFunctionalTest] ICollection<KeyValuePair<string, JsonValue>>.Add(KeyValuePair<string, JsonValue> item) failed to function properly.");
+ retValue = false;
+ }
+ else
+ {
+ Log.Info("[JsonObjectItemsFunctionalTest] ICollection<KeyValuePair<string, JsonValue>>.Add(KeyValuePair<string, JsonValue> item) passed test.");
+ }
+
+ // ICollection<KeyValuePair<string, JsonValue>>.Remove(KeyValuePair<string, JsonValue> item)
+ ((ICollection<KeyValuePair<string, JsonValue>>)sourceJson).Remove(kp);
+ if (sourceJson.Count != arrayLength)
+ {
+ Log.Info("[JsonObjectItemsFunctionalTest] ICollection<KeyValuePair<string, JsonValue>>.Remove(KeyValuePair<string, JsonValue> item) failed to function properly.");
+ retValue = false;
+ }
+ else
+ {
+ Log.Info("[JsonObjectItemsFunctionalTest] ICollection<KeyValuePair<string, JsonValue>>.Remove(KeyValuePair<string, JsonValue> item) passed test.");
+ }
+
+ // ICollection<KeyValuePair<string, JsonValue>>.GetEnumerator()
+ JsonObject jo = new JsonObject { { "member 1", 123 }, { "member 2", new JsonArray { 1, 2, 3 } } };
+ List<string> expected = new List<string> { "member 1 - 123", "member 2 - [1,2,3]" };
+ expected.Sort();
+ IEnumerator<KeyValuePair<string, JsonValue>> ko = ((ICollection<KeyValuePair<string, JsonValue>>)jo).GetEnumerator();
+ List<string> actual = new List<string>();
+ ko.Reset();
+ ko.MoveNext();
+ do
+ {
+ actual.Add(String.Format("{0} - {1}", ko.Current.Key, ko.Current.Value.ToString()));
+ Log.Info("added one item: {0}", String.Format("{0} - {1}", ko.Current.Key, ko.Current.Value));
+ ko.MoveNext();
+ }
+ while (ko.Current.Value != null);
+
+ actual.Sort();
+ if (!JsonValueVerifier.CompareStringLists(expected, actual))
+ {
+ Log.Info("[JsonObjectItemsFunctionalTest] ICollection<KeyValuePair<string, JsonValue>>.GetEnumerator() failed to function properly.");
+ retValue = false;
+ }
+ else
+ {
+ Log.Info("[JsonObjectItemsFunctionalTest] ICollection<KeyValuePair<string, JsonValue>>.GetEnumerator() passed test.");
+ }
+
+ // JsonObject.Values
+ sourceJson = SpecialJsonValueHelper.CreateIndexPopulatedJsonObject(seed, arrayLength);
+ JsonValue[] manyValues = SpecialJsonValueHelper.CreatePrePopulatedJsonValueArray(seed, arrayLength);
+ JsonObject jov = new JsonObject();
+ for (int j = 0; j < manyValues.Length; j++)
+ {
+ jov.Add("member" + j, manyValues[j]);
+ }
+
+ List<string> expectedList = new List<string>();
+ foreach (JsonValue v in manyValues)
+ {
+ expectedList.Add(v.ToString());
+ }
+
+ expectedList.Sort();
+ List<string> actualList = new List<string>();
+ foreach (JsonValue v in jov.Values)
+ {
+ actualList.Add(v.ToString());
+ }
+
+ actualList.Sort();
+ if (!JsonValueVerifier.CompareStringLists(expectedList, actualList))
+ {
+ Log.Info("[JsonObjectItemsFunctionalTest] JsonObject.Values failed to function properly.");
+ retValue = false;
+ }
+ else
+ {
+ Log.Info("[JsonObjectItemsFunctionalTest] JsonObject.Values passed test.");
+ }
+
+ for (int j = 0; j < sourceJson.Count; j++)
+ {
+ // JsonObject.Contains(Key)
+ if (!sourceJson.ContainsKey(j.ToString()))
+ {
+ Log.Info("[JsonObjectItemsFunctionalTest] JsonObject.Contains(Key) failed to function properly.");
+ retValue = false;
+ }
+ else
+ {
+ Log.Info("[JsonObjectItemsFunctionalTest] JsonObject.Contains(Key) passed test.");
+ }
+
+ // JsonObject.TryGetValue(String, out JsonValue)
+ JsonValue retJson;
+ if (!sourceJson.TryGetValue(j.ToString(), out retJson))
+ {
+ Log.Info("[JsonObjectItemsFunctionalTest] JsonObject.TryGetValue(String, out JsonValue) failed to function properly.");
+ retValue = false;
+ }
+ else if (retJson != sourceJson[j.ToString()])
+ {
+ // JsonObjectthis[string key]
+ Log.Info("[JsonObjectItemsFunctionalTest] JsonObject[string key] or JsonObject.TryGetValue(String, out JsonValue) failed to function properly.");
+ retValue = false;
+ }
+ else
+ {
+ Log.Info("[JsonObjectItemsFunctionalTest] JsonObject.TryGetValue(String, out JsonValue) & JsonObject[string key] passed test.");
+ }
+ }
+
+ Assert.True(retValue);
+ }
+ }
+
+ /// <summary>
+ /// Tests for casting to integer values.
+ /// </summary>
+ [Fact]
+ public void GettingIntegerValueTest()
+ {
+ string json = "{\"byte\":160,\"sbyte\":-89,\"short\":12345,\"ushort\":65530," +
+ "\"int\":1234567890,\"uint\":3000000000,\"long\":1234567890123456," +
+ "\"ulong\":10000000000000000000}";
+ Dictionary<string, object> expected = new Dictionary<string, object>();
+ expected.Add("byte", (byte)160);
+ expected.Add("sbyte", (sbyte)-89);
+ expected.Add("short", (short)12345);
+ expected.Add("ushort", (ushort)65530);
+ expected.Add("int", (int)1234567890);
+ expected.Add("uint", (uint)3000000000);
+ expected.Add("long", (long)1234567890123456L);
+ expected.Add("ulong", (((ulong)5000000000000000000L) * 2));
+ JsonObject jo = (JsonObject)JsonValue.Parse(json);
+ bool success = true;
+ foreach (string key in jo.Keys)
+ {
+ object expectedObj = expected[key];
+ Log.Info("Testing for type = {0}", key);
+ try
+ {
+ switch (key)
+ {
+ case "byte":
+ Assert.Equal<byte>((byte)expectedObj, (byte)jo[key]);
+ break;
+ case "sbyte":
+ Assert.Equal<sbyte>((sbyte)expectedObj, (sbyte)jo[key]);
+ break;
+ case "short":
+ Assert.Equal<short>((short)expectedObj, (short)jo[key]);
+ break;
+ case "ushort":
+ Assert.Equal<ushort>((ushort)expectedObj, (ushort)jo[key]);
+ break;
+ case "int":
+ Assert.Equal<int>((int)expectedObj, (int)jo[key]);
+ break;
+ case "uint":
+ Assert.Equal<uint>((uint)expectedObj, (uint)jo[key]);
+ break;
+ case "long":
+ Assert.Equal<long>((long)expectedObj, (long)jo[key]);
+ break;
+ case "ulong":
+ Assert.Equal<ulong>((ulong)expectedObj, (ulong)jo[key]);
+ break;
+ }
+ }
+ catch (InvalidCastException e)
+ {
+ Log.Info("Caught InvalidCastException: {0}", e);
+ success = false;
+ }
+ }
+
+ Assert.True(success);
+ }
+
+ /// <summary>
+ /// Tests for casting to floating point values.
+ /// </summary>
+ [Fact]
+ public void GettingFloatingPointValueTest()
+ {
+ string json = "{\"float\":1.23,\"double\":1.23e+290,\"decimal\":1234567890.123456789}";
+ Dictionary<string, object> expected = new Dictionary<string, object>();
+ expected.Add("float", 1.23f);
+ expected.Add("double", 1.23e+290);
+ expected.Add("decimal", 1234567890.123456789m);
+ JsonObject jo = (JsonObject)JsonValue.Parse(json);
+ bool success = true;
+ foreach (string key in jo.Keys)
+ {
+ object expectedObj = expected[key];
+ Log.Info("Testing for type = {0}", key);
+ try
+ {
+ switch (key)
+ {
+ case "float":
+ Assert.Equal<float>((float)expectedObj, (float)jo[key]);
+ break;
+ case "double":
+ Assert.Equal<double>((double)expectedObj, (double)jo[key]);
+ break;
+ case "decimal":
+ Assert.Equal<decimal>((decimal)expectedObj, (decimal)jo[key]);
+ break;
+ }
+ }
+ catch (InvalidCastException e)
+ {
+ Log.Info("Caught InvalidCastException: {0}", e);
+ success = false;
+ }
+ }
+
+ Assert.True(success);
+ }
+
+ /// <summary>
+ /// Negative tests for invalid operations.
+ /// </summary>
+ [Fact]
+ public void TestInvalidOperations()
+ {
+ JsonArray ja = new JsonArray { 1, null, "hello" };
+ JsonObject jo = new JsonObject
+ {
+ { "first", 1 },
+ { "second", null },
+ { "third", "hello" },
+ };
+ JsonPrimitive jp = new JsonPrimitive("hello");
+
+ Assert.Throws<InvalidOperationException>(() => "jp[\"hello\"] should fail: " + jp["hello"].ToString());
+
+ Assert.Throws<InvalidOperationException>(() => "ja[\"hello\"] should fail: " + ja["hello"].ToString());
+
+
+ Assert.Throws<InvalidOperationException>(() => jp["hello"] = "This shouldn't happen");
+
+
+ Assert.Throws<InvalidOperationException>(() => ja["hello"] = "This shouldn't happen");
+
+ Assert.Throws<InvalidOperationException>(() => ("jp[1] should fail: " + jp[1].ToString()));
+
+ Assert.Throws<InvalidOperationException>(() => "jo[0] should fail: " + jo[1].ToString());
+
+ Assert.Throws<InvalidOperationException>(() => jp[0] = "This shouldn't happen");
+
+ Assert.Throws<InvalidOperationException>(() => jo[0] = "This shouldn't happen");
+
+ Assert.Throws<InvalidCastException>(() => "(DateTimeOffset)jp[\"hello\"] should fail: " + (DateTimeOffset)jp);
+
+ Assert.Throws<InvalidCastException>(() => ("(Char)jp[\"hello\"] should fail: " + (char)jp));
+
+ Assert.Throws<InvalidCastException>(() =>
+ {
+ short jprim = (short)new JsonPrimitive(false);
+ });
+ }
+
+ /// <summary>
+ /// Test for consuming deeply nested object graphs.
+ /// </summary>
+ [Fact]
+ public void TestDeeplyNestedObjectGraph()
+ {
+ JsonObject jo = new JsonObject();
+ JsonObject current = jo;
+ StringBuilder builderExpected = new StringBuilder();
+ builderExpected.Append('{');
+ int depth = 10000;
+ for (int i = 0; i < depth; i++)
+ {
+ JsonObject next = new JsonObject();
+ string key = i.ToString(CultureInfo.InvariantCulture);
+ builderExpected.AppendFormat("\"{0}\":{{", key);
+ current.Add(key, next);
+ current = next;
+ }
+
+ for (int i = 0; i < depth + 1; i++)
+ {
+ builderExpected.Append('}');
+ }
+
+ Assert.Equal(builderExpected.ToString(), jo.ToString());
+ }
+
+ /// <summary>
+ /// Test for consuming deeply nested array graphs.
+ /// </summary>
+ [Fact]
+ public void TestDeeplyNestedArrayGraph()
+ {
+ JsonArray ja = new JsonArray();
+ JsonArray current = ja;
+ StringBuilder builderExpected = new StringBuilder();
+ builderExpected.Append('[');
+ int depth = 10000;
+ for (int i = 0; i < depth; i++)
+ {
+ JsonArray next = new JsonArray();
+ builderExpected.Append('[');
+ current.Add(next);
+ current = next;
+ }
+
+ for (int i = 0; i < depth + 1; i++)
+ {
+ builderExpected.Append(']');
+ }
+
+ Assert.Equal(builderExpected.ToString(), ja.ToString());
+ }
+
+ /// <summary>
+ /// Test for consuming deeply nested object and array graphs.
+ /// </summary>
+ [Fact]
+ public void TestDeeplyNestedObjectAndArrayGraph()
+ {
+ JsonObject jo = new JsonObject();
+ JsonObject current = jo;
+ StringBuilder builderExpected = new StringBuilder();
+ builderExpected.Append('{');
+ int depth = 10000;
+ for (int i = 0; i < depth; i++)
+ {
+ JsonObject next = new JsonObject();
+ string key = i.ToString(CultureInfo.InvariantCulture);
+ builderExpected.AppendFormat("\"{0}\":[{{", key);
+ current.Add(key, new JsonArray(next));
+ current = next;
+ }
+
+ for (int i = 0; i < depth; i++)
+ {
+ builderExpected.Append("}]");
+ }
+
+ builderExpected.Append('}');
+
+ Assert.Equal(builderExpected.ToString(), jo.ToString());
+ }
+
+ /// <summary>
+ /// Test for calling <see cref="JsonValue.ToString()"/> on the same instance in different threads.
+ /// </summary>
+ [Fact]
+ public void TestConcurrentToString()
+ {
+ bool exceptionThrown = false;
+ bool incorrectValue = false;
+ JsonObject jo = new JsonObject();
+ StringBuilder sb = new StringBuilder();
+ sb.Append('{');
+ for (int i = 0; i < 100000; i++)
+ {
+ if (i > 0)
+ {
+ sb.Append(',');
+ }
+
+ string key = i.ToString(CultureInfo.InvariantCulture);
+ jo.Add(key, i);
+ sb.AppendFormat("\"{0}\":{0}", key);
+ }
+
+ sb.Append('}');
+ string expected = sb.ToString();
+
+ int numberOfThreads = 5;
+ Thread[] threads = new Thread[numberOfThreads];
+ for (int i = 0; i < numberOfThreads; i++)
+ {
+ threads[i] = new Thread(new ThreadStart(delegate
+ {
+ for (int j = 0; j < 10; j++)
+ {
+ try
+ {
+ string str = jo.ToString();
+ if (str != expected)
+ {
+ incorrectValue = true;
+ Log.Info("Value is incorrect");
+ }
+ }
+ catch (Exception e)
+ {
+ exceptionThrown = true;
+ Log.Info("Exception thrown: {0}", e);
+ }
+ }
+ }));
+ }
+
+ for (int i = 0; i < numberOfThreads; i++)
+ {
+ threads[i].Start();
+ }
+
+ for (int i = 0; i < numberOfThreads; i++)
+ {
+ threads[i].Join();
+ }
+
+ Assert.False(incorrectValue);
+ Assert.False(exceptionThrown);
+ }
+
+ class MyJsonValueCollection<JsonValue> : System.Collections.Generic.IEnumerable<JsonValue>
+ {
+ List<JsonValue> internalList = new List<JsonValue>();
+
+ public MyJsonValueCollection()
+ {
+ }
+
+ public void Add(JsonValue obj)
+ {
+ this.internalList.Add(obj);
+ }
+
+ public IEnumerator<JsonValue> GetEnumerator()
+ {
+ return this.internalList.GetEnumerator();
+ }
+
+ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
+ {
+ return this.GetEnumerator();
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/test/System.Json.Test.Integration/JsonPrimitiveTests.cs b/test/System.Json.Test.Integration/JsonPrimitiveTests.cs
new file mode 100644
index 00000000..5ffcb89c
--- /dev/null
+++ b/test/System.Json.Test.Integration/JsonPrimitiveTests.cs
@@ -0,0 +1,1068 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Reflection;
+using System.Runtime.Serialization.Json;
+using System.Text;
+using Xunit;
+
+namespace System.Json
+{
+ /// <summary>
+ /// JsonPrimitive unit tests
+ /// </summary>
+ public class JsonPrimitiveTests
+ {
+ const string DateTimeFormat = "yyyy-MM-ddTHH:mm:ss.fffK";
+
+ /// <summary>
+ /// Validates round-trip of <see cref="JsonPrimitive"/> values created from <see cref="Int16"/> values.
+ /// </summary>
+ [Fact]
+ public void JsonPrimitiveFromInt16()
+ {
+ short[] values = new short[] { Int16.MinValue, Int16.MaxValue, 1 };
+ for (int i = 0; i < values.Length; i++)
+ {
+ this.ValidateJson(new JsonPrimitive(values[i]), GetExpectedRepresentation(values[i]), JsonType.Number);
+ this.TestReadAsRoundtrip<short>(new JsonPrimitive(values[i]), values[i]);
+ }
+ }
+
+ /// <summary>
+ /// Validates round-trip of <see cref="JsonPrimitive"/> values created from <see cref="Int32"/> values.
+ /// </summary>
+ [Fact]
+ public void JsonPrimitiveFromInt32()
+ {
+ int[] values = new int[] { Int32.MinValue, Int32.MaxValue, 12345678 };
+ for (int i = 0; i < values.Length; i++)
+ {
+ this.ValidateJson(new JsonPrimitive(values[i]), GetExpectedRepresentation(values[i]), JsonType.Number);
+ this.TestReadAsRoundtrip<int>(new JsonPrimitive(values[i]), values[i]);
+ }
+ }
+
+ /// <summary>
+ /// Validates round-trip of <see cref="JsonPrimitive"/> values created from <see cref="Int64"/> values.
+ /// </summary>
+ [Fact]
+ public void JsonPrimitiveFromInt64()
+ {
+ long[] values = new long[] { Int64.MinValue, Int64.MaxValue, 12345678901232L };
+ for (int i = 0; i < values.Length; i++)
+ {
+ this.ValidateJson(new JsonPrimitive(values[i]), GetExpectedRepresentation(values[i]), JsonType.Number);
+ this.TestReadAsRoundtrip<long>(new JsonPrimitive(values[i]), values[i]);
+ }
+ }
+
+ /// <summary>
+ /// Validates round-trip of <see cref="JsonPrimitive"/> values created from <see cref="UInt64"/> values.
+ /// </summary>
+ [Fact]
+ public void JsonPrimitiveFromUInt64()
+ {
+ ulong[] values = new ulong[] { UInt64.MinValue, UInt64.MaxValue, 12345678901232L };
+ for (int i = 0; i < values.Length; i++)
+ {
+ this.ValidateJson(new JsonPrimitive(values[i]), GetExpectedRepresentation(values[i]), JsonType.Number);
+ this.TestReadAsRoundtrip<ulong>(new JsonPrimitive(values[i]), values[i]);
+ }
+ }
+
+ /// <summary>
+ /// Validates round-trip of <see cref="JsonPrimitive"/> values created from <see cref="UInt32"/> values.
+ /// </summary>
+ [Fact]
+ public void JsonPrimitiveFromUInt32()
+ {
+ uint[] values = new uint[] { UInt32.MinValue, UInt32.MaxValue, 3234567890 };
+ for (int i = 0; i < values.Length; i++)
+ {
+ this.ValidateJson(new JsonPrimitive(values[i]), GetExpectedRepresentation(values[i]), JsonType.Number);
+ this.TestReadAsRoundtrip<uint>(new JsonPrimitive(values[i]), values[i]);
+ }
+ }
+
+ /// <summary>
+ /// Validates round-trip of <see cref="JsonPrimitive"/> values created from <see cref="UInt16"/> values.
+ /// </summary>
+ [Fact]
+ public void JsonPrimitiveFromUInt16()
+ {
+ ushort[] values = new ushort[] { UInt16.MinValue, UInt16.MaxValue, 33333 };
+ for (int i = 0; i < values.Length; i++)
+ {
+ this.ValidateJson(new JsonPrimitive(values[i]), GetExpectedRepresentation(values[i]), JsonType.Number);
+ this.TestReadAsRoundtrip<ushort>(new JsonPrimitive(values[i]), values[i]);
+ }
+ }
+
+ /// <summary>
+ /// Validates round-trip of <see cref="JsonPrimitive"/> values created from <see cref="Byte"/> values.
+ /// </summary>
+ [Fact]
+ public void JsonPrimitiveFromByte()
+ {
+ byte[] values = new byte[] { Byte.MinValue, Byte.MaxValue, 0x83 };
+ for (int i = 0; i < values.Length; i++)
+ {
+ this.ValidateJson(new JsonPrimitive(values[i]), GetExpectedRepresentation(values[i]), JsonType.Number);
+ this.TestReadAsRoundtrip<byte>(new JsonPrimitive(values[i]), values[i]);
+ }
+ }
+
+ /// <summary>
+ /// Validates round-trip of <see cref="JsonPrimitive"/> values created from <see cref="SByte"/> values.
+ /// </summary>
+ [Fact]
+ public void JsonPrimitiveFromSByte()
+ {
+ sbyte[] values = new sbyte[] { SByte.MinValue, SByte.MaxValue, -0x33 };
+ for (int i = 0; i < values.Length; i++)
+ {
+ this.ValidateJson(new JsonPrimitive(values[i]), GetExpectedRepresentation(values[i]), JsonType.Number);
+ this.TestReadAsRoundtrip<sbyte>(new JsonPrimitive(values[i]), values[i]);
+ }
+ }
+
+ /// <summary>
+ /// Validates round-trip of <see cref="JsonPrimitive"/> values created from <see cref="Single"/> values.
+ /// </summary>
+ [Fact]
+ public void JsonPrimitiveFromFloat()
+ {
+ float[] values = new float[] { float.MinValue, float.MaxValue, 1.234f, float.PositiveInfinity, float.NegativeInfinity, float.NaN };
+ for (int i = 0; i < values.Length; i++)
+ {
+ this.ValidateJson(new JsonPrimitive(values[i]), GetExpectedRepresentation(values[i]), JsonType.Number);
+ this.TestReadAsRoundtrip<float>(new JsonPrimitive(values[i]), values[i]);
+ }
+ }
+
+ /// <summary>
+ /// Validates round-trip of <see cref="JsonPrimitive"/> values created from <see cref="Double"/> values.
+ /// </summary>
+ [Fact]
+ public void JsonPrimitiveFromDouble()
+ {
+ double[] values = new double[] { double.MinValue, double.MaxValue, 1.234, double.PositiveInfinity, double.NegativeInfinity, double.NaN };
+ for (int i = 0; i < values.Length; i++)
+ {
+ this.ValidateJson(new JsonPrimitive(values[i]), GetExpectedRepresentation(values[i]), JsonType.Number);
+ this.TestReadAsRoundtrip<double>(new JsonPrimitive(values[i]), values[i]);
+ }
+ }
+
+ /// <summary>
+ /// Validates round-trip of <see cref="JsonPrimitive"/> values created from <see cref="Decimal"/> values.
+ /// </summary>
+ [Fact]
+ public void JsonPrimitiveFromDecimal()
+ {
+ decimal[] values = new decimal[] { decimal.MinValue, decimal.MaxValue, 123456789.123456789m };
+ for (int i = 0; i < values.Length; i++)
+ {
+ this.ValidateJson(new JsonPrimitive(values[i]), GetExpectedRepresentation(values[i]), JsonType.Number);
+ this.TestReadAsRoundtrip<decimal>(new JsonPrimitive(values[i]), values[i]);
+ }
+ }
+
+ /// <summary>
+ /// Validates round-trip of <see cref="JsonPrimitive"/> values created from <see cref="Boolean"/> values.
+ /// </summary>
+ [Fact]
+ public void JsonPrimitiveFromBoolean()
+ {
+ bool[] values = new bool[] { true, false };
+ for (int i = 0; i < values.Length; i++)
+ {
+ this.ValidateJson(new JsonPrimitive(values[i]), GetExpectedRepresentation(values[i]), JsonType.Boolean);
+ this.TestReadAsRoundtrip<bool>(new JsonPrimitive(values[i]), values[i]);
+ }
+ }
+
+ /// <summary>
+ /// Validates round-trip of <see cref="JsonPrimitive"/> values created from <see cref="Char"/> values.
+ /// </summary>
+ [Fact]
+ public void JsonPrimitiveFromChar()
+ {
+ char[] values = new char[] { 'H', '\0', '\uffff' };
+ for (int i = 0; i < values.Length; i++)
+ {
+ this.ValidateJson(new JsonPrimitive(values[i]), GetExpectedRepresentation(values[i]), JsonType.String);
+ this.TestReadAsRoundtrip<char>(new JsonPrimitive(values[i]), values[i]);
+ }
+ }
+
+ /// <summary>
+ /// Validates round-trip of <see cref="JsonPrimitive"/> values created from <see cref="String"/> values.
+ /// </summary>
+ [Fact]
+ public void JsonPrimitiveFromString()
+ {
+ string[] values = new string[] { "Hello", "abcdef", "\r\t123\n32" };
+ for (int i = 0; i < values.Length; i++)
+ {
+ this.ValidateJson(new JsonPrimitive(values[i]), GetExpectedRepresentation(values[i]), JsonType.String);
+ this.TestReadAsRoundtrip<string>(new JsonPrimitive(values[i]), values[i]);
+ }
+ }
+
+ /// <summary>
+ /// Validates round-trip of <see cref="JsonPrimitive"/> values created from <see cref="DateTime"/> values.
+ /// </summary>
+ [Fact]
+ public void JsonPrimitiveFromDateTime()
+ {
+ DateTime[] values = new DateTime[]
+ {
+ new DateTime(2000, 10, 16, 8, 0, 0, DateTimeKind.Utc),
+ new DateTime(2000, 10, 16, 8, 0, 0, DateTimeKind.Local),
+ };
+ for (int i = 0; i < values.Length; i++)
+ {
+ this.ValidateJson(new JsonPrimitive(values[i]), GetExpectedRepresentation(values[i]), JsonType.String);
+ this.TestReadAsRoundtrip<DateTime>(new JsonPrimitive(values[i]), values[i]);
+ }
+ }
+
+ /// <summary>
+ /// Validates round-trip of <see cref="JsonPrimitive"/> values created from <see cref="Uri"/> values.
+ /// </summary>
+ [Fact]
+ public void JsonPrimitiveFromUri()
+ {
+ Uri[] values = new Uri[] { new Uri("http://tempuri.org"), new Uri("foo/bar", UriKind.Relative) };
+ for (int i = 0; i < values.Length; i++)
+ {
+ this.ValidateJson(new JsonPrimitive(values[i]), GetExpectedRepresentation(values[i]), JsonType.String);
+ this.TestReadAsRoundtrip<Uri>(new JsonPrimitive(values[i]), values[i]);
+ }
+ }
+
+ /// <summary>
+ /// Validates round-trip of <see cref="JsonPrimitive"/> values created from <see cref="Guid"/> values.
+ /// </summary>
+ [Fact]
+ public void JsonPrimitiveFromGuid()
+ {
+ Guid[] values = new Guid[] { Guid.NewGuid(), Guid.Empty, Guid.NewGuid() };
+ for (int i = 0; i < values.Length; i++)
+ {
+ this.ValidateJson(new JsonPrimitive(values[i]), GetExpectedRepresentation(values[i]), JsonType.String);
+ this.TestReadAsRoundtrip<Guid>(new JsonPrimitive(values[i]), values[i]);
+ }
+ }
+
+ /// <summary>
+ /// Validates round-trip of <see cref="JsonPrimitive"/> values created from different types of values.
+ /// </summary>
+ [Fact]
+ public void JsonPrimitiveFromObject()
+ {
+ List<KeyValuePair<object, JsonType>> values = new List<KeyValuePair<object, JsonType>>
+ {
+ new KeyValuePair<object, JsonType>(true, JsonType.Boolean),
+ new KeyValuePair<object, JsonType>((short)1, JsonType.Number),
+ new KeyValuePair<object, JsonType>(234, JsonType.Number),
+ new KeyValuePair<object, JsonType>(3435434233443L, JsonType.Number),
+ new KeyValuePair<object, JsonType>(UInt64.MaxValue, JsonType.Number),
+ new KeyValuePair<object, JsonType>(UInt32.MaxValue, JsonType.Number),
+ new KeyValuePair<object, JsonType>(UInt16.MaxValue, JsonType.Number),
+ new KeyValuePair<object, JsonType>(Byte.MaxValue, JsonType.Number),
+ new KeyValuePair<object, JsonType>(SByte.MinValue, JsonType.Number),
+ new KeyValuePair<object, JsonType>(double.MaxValue, JsonType.Number),
+ new KeyValuePair<object, JsonType>(float.Epsilon, JsonType.Number),
+ new KeyValuePair<object, JsonType>(decimal.MinusOne, JsonType.Number),
+ new KeyValuePair<object, JsonType>("hello", JsonType.String),
+ new KeyValuePair<object, JsonType>(Guid.NewGuid(), JsonType.String),
+ new KeyValuePair<object, JsonType>(DateTime.UtcNow, JsonType.String),
+ new KeyValuePair<object, JsonType>(new Uri("http://www.microsoft.com"), JsonType.String),
+ };
+
+ foreach (var value in values)
+ {
+ string json = GetExpectedRepresentation(value.Key);
+ JsonValue jsonValue = JsonValue.Parse(json);
+ Assert.IsType(typeof(JsonPrimitive), jsonValue);
+ this.ValidateJson((JsonPrimitive)jsonValue, json, value.Value);
+ }
+ }
+
+ /// <summary>
+ /// Negative tests for <see cref="JsonPrimitive"/> constructors with null values.
+ /// </summary>
+ [Fact]
+ public void NullChecks()
+ {
+ ExpectException<ArgumentNullException>(() => new JsonPrimitive((string)null));
+ ExpectException<ArgumentNullException>(() => new JsonPrimitive((Uri)null));
+ }
+
+ /// <summary>
+ /// Tests for casting string values into non-string values.
+ /// </summary>
+ [Fact]
+ public void CastingFromStringTests()
+ {
+ int seed = MethodBase.GetCurrentMethod().Name.GetHashCode();
+ Random rndGen = new Random(seed);
+
+ Assert.Equal(false, (bool)(new JsonPrimitive("false")));
+ Assert.Equal(false, (bool)(new JsonPrimitive("False")));
+ Assert.Equal(true, (bool)(new JsonPrimitive("true")));
+ Assert.Equal(true, (bool)(new JsonPrimitive("True")));
+
+ byte b = PrimitiveCreator.CreateInstanceOfByte(rndGen);
+ Assert.Equal(b, (byte)(new JsonPrimitive(b.ToString(CultureInfo.InvariantCulture))));
+
+ decimal dec = PrimitiveCreator.CreateInstanceOfDecimal(rndGen);
+ Assert.Equal(dec, (decimal)(new JsonPrimitive(dec.ToString(CultureInfo.InvariantCulture))));
+
+ double dbl = rndGen.NextDouble() * rndGen.Next();
+ Assert.Equal(dbl, (double)(new JsonPrimitive(dbl.ToString("R", CultureInfo.InvariantCulture))));
+
+ Assert.Equal(Double.PositiveInfinity, (double)(new JsonPrimitive("Infinity")));
+ Assert.Equal(Double.NegativeInfinity, (double)(new JsonPrimitive("-Infinity")));
+ Assert.Equal(Double.NaN, (double)(new JsonPrimitive("NaN")));
+
+ ExpectException<InvalidCastException>(delegate { var d = (double)(new JsonPrimitive("INF")); });
+ ExpectException<InvalidCastException>(delegate { var d = (double)(new JsonPrimitive("-INF")); });
+ ExpectException<InvalidCastException>(delegate { var d = (double)(new JsonPrimitive("infinity")); });
+ ExpectException<InvalidCastException>(delegate { var d = (double)(new JsonPrimitive("INFINITY")); });
+ ExpectException<InvalidCastException>(delegate { var d = (double)(new JsonPrimitive("nan")); });
+ ExpectException<InvalidCastException>(delegate { var d = (double)(new JsonPrimitive("Nan")); });
+
+ float flt = (float)(rndGen.NextDouble() * rndGen.Next());
+ Assert.Equal(flt, (float)(new JsonPrimitive(flt.ToString("R", CultureInfo.InvariantCulture))));
+
+ Assert.Equal(Single.PositiveInfinity, (float)(new JsonPrimitive("Infinity")));
+ Assert.Equal(Single.NegativeInfinity, (float)(new JsonPrimitive("-Infinity")));
+ Assert.Equal(Single.NaN, (float)(new JsonPrimitive("NaN")));
+
+ ExpectException<InvalidCastException>(delegate { var f = (float)(new JsonPrimitive("INF")); });
+ ExpectException<InvalidCastException>(delegate { var f = (float)(new JsonPrimitive("-INF")); });
+ ExpectException<InvalidCastException>(delegate { var f = (float)(new JsonPrimitive("infinity")); });
+ ExpectException<InvalidCastException>(delegate { var f = (float)(new JsonPrimitive("INFINITY")); });
+ ExpectException<InvalidCastException>(delegate { var f = (float)(new JsonPrimitive("nan")); });
+ ExpectException<InvalidCastException>(delegate { var f = (float)(new JsonPrimitive("Nan")); });
+
+ int i = PrimitiveCreator.CreateInstanceOfInt32(rndGen);
+ Assert.Equal(i, (int)(new JsonPrimitive(i.ToString(CultureInfo.InvariantCulture))));
+
+ long l = PrimitiveCreator.CreateInstanceOfInt64(rndGen);
+ Assert.Equal(l, (long)(new JsonPrimitive(l.ToString(CultureInfo.InvariantCulture))));
+
+ sbyte sb = PrimitiveCreator.CreateInstanceOfSByte(rndGen);
+ Assert.Equal(sb, (sbyte)(new JsonPrimitive(sb.ToString(CultureInfo.InvariantCulture))));
+
+ short s = PrimitiveCreator.CreateInstanceOfInt16(rndGen);
+ Assert.Equal(s, (short)(new JsonPrimitive(s.ToString(CultureInfo.InvariantCulture))));
+
+ ushort ui16 = PrimitiveCreator.CreateInstanceOfUInt16(rndGen);
+ Assert.Equal(ui16, (ushort)(new JsonPrimitive(ui16.ToString(CultureInfo.InvariantCulture))));
+
+ uint ui32 = PrimitiveCreator.CreateInstanceOfUInt32(rndGen);
+ Assert.Equal(ui32, (uint)(new JsonPrimitive(ui32.ToString(CultureInfo.InvariantCulture))));
+
+ ulong ui64 = PrimitiveCreator.CreateInstanceOfUInt64(rndGen);
+ Assert.Equal(ui64, (ulong)(new JsonPrimitive(ui64.ToString(CultureInfo.InvariantCulture))));
+ }
+
+ /// <summary>
+ /// Tests for casting <see cref="JsonPrimitive"/> created from special floating point values (infinity, NaN).
+ /// </summary>
+ [Fact]
+ public void CastingNumbersTest()
+ {
+ Assert.Equal(float.PositiveInfinity, (float)(new JsonPrimitive(double.PositiveInfinity)));
+ Assert.Equal(float.NegativeInfinity, (float)(new JsonPrimitive(double.NegativeInfinity)));
+ Assert.Equal(float.NaN, (float)(new JsonPrimitive(double.NaN)));
+
+ Assert.Equal(double.PositiveInfinity, (double)(new JsonPrimitive(float.PositiveInfinity)));
+ Assert.Equal(double.NegativeInfinity, (double)(new JsonPrimitive(float.NegativeInfinity)));
+ Assert.Equal(double.NaN, (double)(new JsonPrimitive(float.NaN)));
+ }
+
+ /// <summary>
+ /// Tests for the many formats which can be cast to a <see cref="DateTime"/>.
+ /// </summary>
+ [Fact]
+ public void CastingDateTimeTest()
+ {
+ int seed = MethodBase.GetCurrentMethod().Name.GetHashCode();
+ Random rndGen = new Random(seed);
+ DateTime dt = new DateTime(
+ rndGen.Next(1000, 3000), // year
+ rndGen.Next(1, 13), // month
+ rndGen.Next(1, 28), // day
+ rndGen.Next(0, 24), // hour
+ rndGen.Next(0, 60), // minute
+ rndGen.Next(0, 60), // second
+ DateTimeKind.Utc);
+ Log.Info("dt = {0}", dt);
+
+ const string JsonDateFormat = "yyyy-MM-ddTHH:mm:ssZ";
+ string dateString = dt.ToString(JsonDateFormat, CultureInfo.InvariantCulture);
+ JsonValue jv = dateString;
+ DateTime dt2 = (DateTime)jv;
+ Assert.Equal(dt.ToUniversalTime(), dt2.ToUniversalTime());
+
+ const string DateTimeLocalFormat = "yyyy-MM-ddTHH:mm:ss";
+ const string DateLocalFormat = "yyyy-MM-dd";
+ const string TimeLocalFormat = "HH:mm:ss";
+
+ for (int i = 0; i < 100; i++)
+ {
+ DateTime dateLocal = PrimitiveCreator.CreateInstanceOfDateTime(rndGen).ToLocalTime();
+ dateLocal = new DateTime(dateLocal.Year, dateLocal.Month, dateLocal.Day, dateLocal.Hour, dateLocal.Minute, dateLocal.Second, DateTimeKind.Local);
+ string localDateTime = dateLocal.ToString(DateTimeLocalFormat, CultureInfo.InvariantCulture);
+ string localDate = dateLocal.ToString(DateLocalFormat, CultureInfo.InvariantCulture);
+ string localTime = dateLocal.ToString(TimeLocalFormat, CultureInfo.InvariantCulture);
+
+ Assert.Equal(dateLocal, new JsonPrimitive(localDateTime).ReadAs<DateTime>());
+ Assert.Equal(dateLocal.Date, new JsonPrimitive(localDate).ReadAs<DateTime>());
+ DateTime timeOnly = new JsonPrimitive(localTime).ReadAs<DateTime>();
+ Assert.Equal(dateLocal.Hour, timeOnly.Hour);
+ Assert.Equal(dateLocal.Minute, timeOnly.Minute);
+ Assert.Equal(dateLocal.Second, timeOnly.Second);
+
+ DataContractJsonSerializer dcjs = new DataContractJsonSerializer(typeof(DateTime));
+ using (MemoryStream ms = new MemoryStream())
+ {
+ dcjs.WriteObject(ms, dateLocal);
+ ms.Position = 0;
+ JsonValue jvFromString = JsonValue.Load(ms);
+ Assert.Equal(dateLocal, jvFromString.ReadAs<DateTime>());
+ }
+
+ using (MemoryStream ms = new MemoryStream())
+ {
+ DateTime dateUtc = dateLocal.ToUniversalTime();
+ dcjs.WriteObject(ms, dateUtc);
+ ms.Position = 0;
+ JsonValue jvFromString = JsonValue.Load(ms);
+ Assert.Equal(dateUtc, jvFromString.ReadAs<DateTime>());
+ }
+ }
+ }
+
+ /// <summary>
+ /// Tests for date parsing form the RFC2822 format.
+ /// </summary>
+ [Fact]
+ public void Rfc2822DateTimeFormatTest()
+ {
+ string[] localFormats = new string[]
+ {
+ "ddd, d MMM yyyy HH:mm:ss zzz",
+ "d MMM yyyy HH:mm:ss zzz",
+ "ddd, dd MMM yyyy HH:mm:ss zzz",
+ "ddd, dd MMM yyyy HH:mm zzz",
+ };
+
+ string[] utcFormats = new string[]
+ {
+ @"ddd, d MMM yyyy HH:mm:ss \U\T\C",
+ "d MMM yyyy HH:mm:ssZ",
+ @"ddd, dd MMM yyyy HH:mm:ss \U\T\C",
+ "ddd, dd MMM yyyy HH:mmZ",
+ };
+
+ DateTime today = DateTime.Today;
+ int seed = today.Year * 10000 + today.Month * 100 + today.Day;
+ Log.Info("Seed: {0}", seed);
+ Random rndGen = new Random(seed);
+ const int DatesToTry = 100;
+ const string DateTraceFormat = "ddd yyyy/MM/dd HH:mm:ss.fffZ";
+
+ for (int i = 0; i < DatesToTry; i++)
+ {
+ DateTime dt = PrimitiveCreator.CreateInstanceOfDateTime(rndGen);
+ dt = new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second, dt.Kind);
+ Log.Info("Test with date: {0} ({1})", dt.ToString(DateTraceFormat, CultureInfo.InvariantCulture), dt.Kind);
+ string[] formatsToTest = dt.Kind == DateTimeKind.Utc ? utcFormats : localFormats;
+ foreach (string format in formatsToTest)
+ {
+ string strDate = dt.ToString(format, CultureInfo.InvariantCulture);
+ Log.Info("As string: {0} (format = {1})", strDate, format);
+ JsonPrimitive jp = new JsonPrimitive(strDate);
+ DateTime parsedDate = jp.ReadAs<DateTime>();
+ Log.Info("Parsed date: {0} ({1})", parsedDate.ToString(DateTraceFormat, CultureInfo.InvariantCulture), parsedDate.Kind);
+
+ DateTime dtExpected = dt;
+ DateTime dtActual = parsedDate;
+
+ if (dt.Kind != parsedDate.Kind)
+ {
+ dtExpected = dtExpected.ToUniversalTime();
+ dtActual = dtActual.ToUniversalTime();
+ }
+
+ Assert.Equal(dtExpected.Year, dtActual.Year);
+ Assert.Equal(dtExpected.Month, dtActual.Month);
+ Assert.Equal(dtExpected.Day, dtActual.Day);
+ Assert.Equal(dtExpected.Hour, dtActual.Hour);
+ Assert.Equal(dtExpected.Minute, dtActual.Minute);
+ if (format.Contains(":ss"))
+ {
+ Assert.Equal(dtExpected.Second, dtActual.Second);
+ }
+ else
+ {
+ Assert.Equal(0, parsedDate.Second);
+ }
+ }
+
+ Log.Info("");
+ }
+ }
+
+ /// <summary>
+ /// Tests for the <see cref="System.Json.JsonValue.ReadAs{T}()"/> function from string values.
+ /// </summary>
+ [Fact]
+ public void ReadAsFromStringTests()
+ {
+ int seed = MethodBase.GetCurrentMethod().Name.GetHashCode();
+ Random rndGen = new Random(seed);
+
+ TestReadAsFromStringRoundtrip<bool>(false, "false");
+ TestReadAsFromStringRoundtrip<bool>(false, "False");
+ TestReadAsFromStringRoundtrip<bool>(true, "true");
+ TestReadAsFromStringRoundtrip<bool>(true, "True");
+ TestReadAsFromStringRoundtrip<byte>(PrimitiveCreator.CreateInstanceOfByte(rndGen));
+ TestReadAsFromStringRoundtrip<char>(PrimitiveCreator.CreateInstanceOfChar(rndGen));
+ TestReadAsFromStringRoundtrip<decimal>(PrimitiveCreator.CreateInstanceOfDecimal(rndGen));
+ TestReadAsFromStringRoundtrip<int>(PrimitiveCreator.CreateInstanceOfInt32(rndGen));
+ TestReadAsFromStringRoundtrip<long>(PrimitiveCreator.CreateInstanceOfInt64(rndGen));
+ TestReadAsFromStringRoundtrip<sbyte>(PrimitiveCreator.CreateInstanceOfSByte(rndGen));
+ TestReadAsFromStringRoundtrip<short>(PrimitiveCreator.CreateInstanceOfInt16(rndGen));
+ TestReadAsFromStringRoundtrip<ushort>(PrimitiveCreator.CreateInstanceOfUInt16(rndGen));
+ TestReadAsFromStringRoundtrip<uint>(PrimitiveCreator.CreateInstanceOfUInt32(rndGen));
+ TestReadAsFromStringRoundtrip<ulong>(PrimitiveCreator.CreateInstanceOfUInt64(rndGen));
+ double dbl = rndGen.NextDouble() * rndGen.Next();
+ TestReadAsFromStringRoundtrip<double>(dbl, dbl.ToString("R", CultureInfo.InvariantCulture));
+ TestReadAsFromStringRoundtrip<double>(double.PositiveInfinity, "Infinity");
+ TestReadAsFromStringRoundtrip<double>(double.NegativeInfinity, "-Infinity");
+ TestReadAsFromStringRoundtrip<double>(double.NaN, "NaN");
+ float flt = (float)(rndGen.NextDouble() * rndGen.Next());
+ TestReadAsFromStringRoundtrip<float>(flt, flt.ToString("R", CultureInfo.InvariantCulture));
+ TestReadAsFromStringRoundtrip<float>(float.PositiveInfinity, "Infinity");
+ TestReadAsFromStringRoundtrip<float>(float.NegativeInfinity, "-Infinity");
+ TestReadAsFromStringRoundtrip<float>(float.NaN, "NaN");
+ Guid guid = PrimitiveCreator.CreateInstanceOfGuid(rndGen);
+ TestReadAsFromStringRoundtrip<Guid>(guid, guid.ToString("N", CultureInfo.InvariantCulture));
+ TestReadAsFromStringRoundtrip<Guid>(guid, guid.ToString("D", CultureInfo.InvariantCulture));
+ TestReadAsFromStringRoundtrip<Guid>(guid, guid.ToString("B", CultureInfo.InvariantCulture));
+ TestReadAsFromStringRoundtrip<Guid>(guid, guid.ToString("P", CultureInfo.InvariantCulture));
+ TestReadAsFromStringRoundtrip<Guid>(guid, guid.ToString("X", CultureInfo.InvariantCulture));
+ TestReadAsFromStringRoundtrip<Guid>(guid, guid.ToString("X", CultureInfo.InvariantCulture).Replace("0x", "0X"));
+ TestReadAsFromStringRoundtrip<Guid>(guid, guid.ToString("X", CultureInfo.InvariantCulture).Replace("{", " { "));
+ TestReadAsFromStringRoundtrip<Guid>(guid, guid.ToString("X", CultureInfo.InvariantCulture).Replace("}", " } "));
+ TestReadAsFromStringRoundtrip<Guid>(guid, guid.ToString("X", CultureInfo.InvariantCulture).Replace(",", " , "));
+ TestReadAsFromStringRoundtrip<Guid>(guid, guid.ToString("X", CultureInfo.InvariantCulture).Replace("0x", "0x0000"));
+ Uri uri = null;
+ do
+ {
+ try
+ {
+ uri = PrimitiveCreator.CreateInstanceOfUri(rndGen);
+ }
+ catch (UriFormatException)
+ {
+ }
+ } while (uri == null);
+
+ TestReadAsFromStringRoundtrip<Uri>(uri);
+ TestReadAsFromStringRoundtrip<string>(PrimitiveCreator.CreateInstanceOfString(rndGen));
+
+ // Roundtrip reference DateTime to remove some of the precision in the ticks. Otherwise, value is too precise.
+ DateTimeOffset dateTimeOffset = PrimitiveCreator.CreateInstanceOfDateTimeOffset(rndGen);
+ const string ISO8601Format = "yyyy-MM-ddTHH:mm:sszzz";
+ dateTimeOffset = DateTimeOffset.ParseExact(dateTimeOffset.ToString(ISO8601Format, CultureInfo.InvariantCulture), ISO8601Format, CultureInfo.InvariantCulture);
+ DateTime dateTime = dateTimeOffset.UtcDateTime;
+ TestReadAsFromStringRoundtrip<DateTime>(dateTime, dateTimeOffset.ToUniversalTime().ToString(@"ddd, d MMM yyyy HH:mm:ss \U\T\C"));
+ TestReadAsFromStringRoundtrip<DateTime>(dateTime, dateTimeOffset.ToUniversalTime().ToString(@"ddd, d MMM yyyy HH:mm:ss \G\M\T"));
+ TestReadAsFromStringRoundtrip<DateTime>(dateTime.ToLocalTime(), dateTimeOffset.ToString(@"ddd, d MMM yyyy HH:mm:ss zzz"));
+ TestReadAsFromStringRoundtrip<DateTime>(dateTime, dateTime.ToString("yyyy-MM-ddTHH:mm:ssK"));
+ TestReadAsFromStringRoundtrip<DateTime>(dateTime.ToLocalTime(), dateTimeOffset.ToString(@"ddd, d MMM yyyy HH:mm:ss zzz"));
+ TestReadAsFromStringRoundtrip<DateTime>(dateTime.ToLocalTime(), dateTimeOffset.ToString("yyyy-MM-ddTHH:mm:sszzz"));
+ TestReadAsFromStringRoundtrip<DateTimeOffset>(dateTimeOffset.UtcDateTime, dateTimeOffset.ToUniversalTime().ToString(@"ddd, d MMM yyyy HH:mm:ss \U\T\C"));
+ TestReadAsFromStringRoundtrip<DateTimeOffset>(dateTimeOffset.UtcDateTime, dateTimeOffset.ToUniversalTime().ToString(@"ddd, d MMM yyyy HH:mm:ss \G\M\T"));
+ TestReadAsFromStringRoundtrip<DateTimeOffset>(dateTimeOffset, dateTimeOffset.ToUniversalTime().ToString(@"ddd, d MMM yyyy HH:mm:ss zzz"));
+ TestReadAsFromStringRoundtrip<DateTimeOffset>(dateTimeOffset, dateTime.ToString("yyyy-MM-ddTHH:mm:ssK"));
+ TestReadAsFromStringRoundtrip<DateTimeOffset>(dateTimeOffset, dateTimeOffset.ToString("yyyy-MM-ddTHH:mm:sszzz"));
+ TestReadAsFromStringRoundtrip<DateTimeOffset>(dateTimeOffset, dateTimeOffset.ToString(@"ddd, d MMM yyyy HH:mm:ss zzz"));
+
+ // Create ASPNetFormat DateTime
+ long unixEpochMilliseconds = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).Ticks / 10000;
+ long millisecondsFromUnixEpoch = dateTime.Ticks / 10000 - unixEpochMilliseconds;
+ string AspNetFormattedDateTime = String.Format("/Date({0})/", millisecondsFromUnixEpoch);
+ string AspNetFormattedDateTimeWithValidTZ = String.Format("/Date({0}+0700)/", millisecondsFromUnixEpoch);
+ string AspNetFormattedDateTimeInvalid1 = String.Format("/Date({0}+99999)/", millisecondsFromUnixEpoch);
+ string AspNetFormattedDateTimeInvalid2 = String.Format("/Date({0}+07z0)/", millisecondsFromUnixEpoch);
+ TestReadAsFromStringRoundtrip<DateTime>(dateTime, AspNetFormattedDateTime);
+ TestReadAsFromStringRoundtrip<DateTime>(dateTime.ToLocalTime(), AspNetFormattedDateTimeWithValidTZ);
+ TestReadAsFromStringRoundtrip<DateTimeOffset>(dateTimeOffset, AspNetFormattedDateTime);
+ TestReadAsFromStringRoundtrip<DateTimeOffset>(dateTimeOffset, AspNetFormattedDateTimeWithValidTZ);
+
+ ExpectException<FormatException>(delegate { new JsonPrimitive(AspNetFormattedDateTimeInvalid1).ReadAs<DateTime>(); });
+ ExpectException<FormatException>(delegate { new JsonPrimitive(AspNetFormattedDateTimeInvalid2).ReadAs<DateTime>(); });
+ ExpectException<FormatException>(delegate { new JsonPrimitive(AspNetFormattedDateTimeInvalid1).ReadAs<DateTimeOffset>(); });
+ ExpectException<FormatException>(delegate { new JsonPrimitive(AspNetFormattedDateTimeInvalid2).ReadAs<DateTimeOffset>(); });
+
+ ExpectException<FormatException>(delegate { new JsonPrimitive("INF").ReadAs<float>(); });
+ ExpectException<FormatException>(delegate { new JsonPrimitive("-INF").ReadAs<float>(); });
+ ExpectException<FormatException>(delegate { new JsonPrimitive("infinity").ReadAs<float>(); });
+ ExpectException<FormatException>(delegate { new JsonPrimitive("INFINITY").ReadAs<float>(); });
+ ExpectException<FormatException>(delegate { new JsonPrimitive("nan").ReadAs<float>(); });
+ ExpectException<FormatException>(delegate { new JsonPrimitive("Nan").ReadAs<float>(); });
+
+ ExpectException<FormatException>(delegate { new JsonPrimitive("INF").ReadAs<double>(); });
+ ExpectException<FormatException>(delegate { new JsonPrimitive("-INF").ReadAs<double>(); });
+ ExpectException<FormatException>(delegate { new JsonPrimitive("infinity").ReadAs<double>(); });
+ ExpectException<FormatException>(delegate { new JsonPrimitive("INFINITY").ReadAs<double>(); });
+ ExpectException<FormatException>(delegate { new JsonPrimitive("nan").ReadAs<double>(); });
+ ExpectException<FormatException>(delegate { new JsonPrimitive("Nan").ReadAs<double>(); });
+ }
+
+ /// <summary>
+ /// Tests for the <see cref="System.Json.JsonValue.ReadAs{T}()">JsonValue.ReadAs&lt;string&gt;</see> method from number values.
+ /// </summary>
+ [Fact]
+ public void TestReadAsStringFromNumbers()
+ {
+ int seed = MethodBase.GetCurrentMethod().Name.GetHashCode();
+ Random rndGen = new Random(seed);
+
+ int intValue = PrimitiveCreator.CreateInstanceOfInt32(rndGen);
+ JsonValue jv = intValue;
+ Assert.Equal(intValue.ToString(CultureInfo.InvariantCulture), jv.ToString());
+ Assert.Equal(intValue.ToString(CultureInfo.InvariantCulture), jv.ReadAs<string>());
+
+ uint uintValue = PrimitiveCreator.CreateInstanceOfUInt32(rndGen);
+ jv = uintValue;
+ Assert.Equal(uintValue.ToString(CultureInfo.InvariantCulture), jv.ToString());
+ Assert.Equal(uintValue.ToString(CultureInfo.InvariantCulture), jv.ReadAs<string>());
+
+ long longValue = PrimitiveCreator.CreateInstanceOfInt64(rndGen);
+ jv = longValue;
+ Assert.Equal(longValue.ToString(CultureInfo.InvariantCulture), jv.ToString());
+ Assert.Equal(longValue.ToString(CultureInfo.InvariantCulture), jv.ReadAs<string>());
+
+ ulong ulongValue = PrimitiveCreator.CreateInstanceOfUInt64(rndGen);
+ jv = ulongValue;
+ Assert.Equal(ulongValue.ToString(CultureInfo.InvariantCulture), jv.ToString());
+ Assert.Equal(ulongValue.ToString(CultureInfo.InvariantCulture), jv.ReadAs<string>());
+
+ short shortValue = PrimitiveCreator.CreateInstanceOfInt16(rndGen);
+ jv = shortValue;
+ Assert.Equal(shortValue.ToString(CultureInfo.InvariantCulture), jv.ToString());
+ Assert.Equal(shortValue.ToString(CultureInfo.InvariantCulture), jv.ReadAs<string>());
+
+ ushort ushortValue = PrimitiveCreator.CreateInstanceOfUInt16(rndGen);
+ jv = ushortValue;
+ Assert.Equal(ushortValue.ToString(CultureInfo.InvariantCulture), jv.ToString());
+ Assert.Equal(ushortValue.ToString(CultureInfo.InvariantCulture), jv.ReadAs<string>());
+
+ byte byteValue = PrimitiveCreator.CreateInstanceOfByte(rndGen);
+ jv = byteValue;
+ Assert.Equal(byteValue.ToString(CultureInfo.InvariantCulture), jv.ToString());
+ Assert.Equal(byteValue.ToString(CultureInfo.InvariantCulture), jv.ReadAs<string>());
+
+ sbyte sbyteValue = PrimitiveCreator.CreateInstanceOfSByte(rndGen);
+ jv = sbyteValue;
+ Assert.Equal(sbyteValue.ToString(CultureInfo.InvariantCulture), jv.ToString());
+ Assert.Equal(sbyteValue.ToString(CultureInfo.InvariantCulture), jv.ReadAs<string>());
+
+ decimal decValue = PrimitiveCreator.CreateInstanceOfDecimal(rndGen);
+ jv = decValue;
+ Assert.Equal(decValue.ToString(CultureInfo.InvariantCulture), jv.ToString());
+ Assert.Equal(decValue.ToString(CultureInfo.InvariantCulture), jv.ReadAs<string>());
+
+ float fltValue = PrimitiveCreator.CreateInstanceOfSingle(rndGen);
+ jv = fltValue;
+ Assert.Equal(fltValue.ToString("R", CultureInfo.InvariantCulture), jv.ToString());
+ Assert.Equal(fltValue.ToString("R", CultureInfo.InvariantCulture), jv.ReadAs<string>());
+
+ double dblValue = PrimitiveCreator.CreateInstanceOfDouble(rndGen);
+ jv = dblValue;
+ Assert.Equal(dblValue.ToString("R", CultureInfo.InvariantCulture), jv.ToString());
+ Assert.Equal(dblValue.ToString("R", CultureInfo.InvariantCulture), jv.ReadAs<string>());
+ }
+
+ /// <summary>
+ /// Tests for the <see cref="System.Json.JsonValue.ReadAs{T}()">JsonValue.ReadAs&lt;string&gt;</see> method from date values.
+ /// </summary>
+ [Fact]
+ public void TestReadAsStringFromDates()
+ {
+ int seed = MethodBase.GetCurrentMethod().Name.GetHashCode();
+ Random rndGen = new Random(seed);
+
+ DateTime dateTimeValue = PrimitiveCreator.CreateInstanceOfDateTime(rndGen);
+ JsonValue jv = dateTimeValue;
+ Assert.Equal("\"" + dateTimeValue.ToString(DateTimeFormat, CultureInfo.InvariantCulture) + "\"", jv.ToString());
+ Assert.Equal(dateTimeValue.ToString(DateTimeFormat, CultureInfo.InvariantCulture), jv.ReadAs<string>());
+ }
+
+ /// <summary>
+ /// Tests for the <see cref="System.Json.JsonValue.ReadAs{T}()">JsonValue.ReadAs&lt;string&gt;</see> method from char values.
+ /// </summary>
+ [Fact]
+ public void TestReadAsStringFromChar()
+ {
+ char[] chars = "abc\u0000\b\f\r\n\t\ufedc".ToCharArray();
+
+ foreach (char c in chars)
+ {
+ string expected = new string(c, 1);
+ JsonValue jv = c;
+ string actual1 = jv.ReadAs<string>();
+ string actual2 = (string)jv;
+
+ Assert.Equal(expected, actual1);
+ Assert.Equal(expected, actual2);
+ }
+ }
+
+ /// <summary>
+ /// Tests for the <see cref="System.Json.JsonValue.ReadAs{T}()"/> method where T is a number type and the value is created from a string.
+ /// </summary>
+ [Fact]
+ public void TestReadAsNumberFromStrings()
+ {
+ Dictionary<object, List<Type>> valuesToNonOverflowingTypesMapping = new Dictionary<object, List<Type>>
+ {
+ { double.NaN.ToString("R", CultureInfo.InvariantCulture), new List<Type> { typeof(float), typeof(double) } },
+ { double.NegativeInfinity.ToString("R", CultureInfo.InvariantCulture), new List<Type> { typeof(float), typeof(double) } },
+ { double.PositiveInfinity.ToString("R", CultureInfo.InvariantCulture), new List<Type> { typeof(float), typeof(double) } },
+ { double.MaxValue.ToString("R", CultureInfo.InvariantCulture), new List<Type> { typeof(double), typeof(float) } },
+ { double.MinValue.ToString("R", CultureInfo.InvariantCulture), new List<Type> { typeof(double), typeof(float) } },
+ { float.MaxValue.ToString("R", CultureInfo.InvariantCulture), new List<Type> { typeof(double), typeof(float) } },
+ { float.MinValue.ToString("R", CultureInfo.InvariantCulture), new List<Type> { typeof(double), typeof(float) } },
+ { Int64.MaxValue.ToString(), new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(ulong) } },
+ { Int64.MinValue.ToString(), new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long) } },
+ { Int32.MaxValue.ToString(), new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int), typeof(ulong), typeof(uint) } },
+ { Int32.MinValue.ToString(), new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int) } },
+ { Int16.MaxValue.ToString(), new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int), typeof(short), typeof(ulong), typeof(uint), typeof(ushort) } },
+ { Int16.MinValue.ToString(), new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int), typeof(short) } },
+ { SByte.MaxValue.ToString(), new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int), typeof(short), typeof(sbyte), typeof(ulong), typeof(uint), typeof(ushort), typeof(byte) } },
+ { SByte.MinValue.ToString(), new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int), typeof(short), typeof(sbyte) } },
+ { UInt64.MaxValue.ToString(), new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(ulong) } },
+ { UInt32.MaxValue.ToString(), new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(ulong), typeof(uint) } },
+ { UInt16.MaxValue.ToString(), new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int), typeof(ulong), typeof(uint), typeof(ushort) } },
+ { Byte.MaxValue.ToString(), new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int), typeof(short), typeof(ulong), typeof(uint), typeof(ushort), typeof(byte) } },
+ { Byte.MinValue.ToString(), new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int), typeof(short), typeof(sbyte), typeof(ulong), typeof(uint), typeof(ushort), typeof(byte) } },
+ { "1", new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int), typeof(short), typeof(sbyte), typeof(ulong), typeof(uint), typeof(ushort), typeof(byte) } },
+ { "+01", new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int), typeof(short), typeof(sbyte), typeof(ulong), typeof(uint), typeof(ushort), typeof(byte) } },
+ { "01.1e+01", new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int), typeof(short), typeof(sbyte), typeof(ulong), typeof(uint), typeof(ushort), typeof(byte) } },
+ { "1e1", new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int), typeof(short), typeof(sbyte), typeof(ulong), typeof(uint), typeof(ushort), typeof(byte) } },
+ { "1.0", new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int), typeof(short), typeof(sbyte), typeof(ulong), typeof(uint), typeof(ushort), typeof(byte) } },
+ { "01.0", new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int), typeof(short), typeof(sbyte), typeof(ulong), typeof(uint), typeof(ushort), typeof(byte) } },
+ { "-1", new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int), typeof(short), typeof(sbyte) } },
+ { "-1.0", new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int), typeof(short), typeof(sbyte) } },
+ { "-01.0", new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int), typeof(short), typeof(sbyte) } },
+ { "-01.0e+01", new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int), typeof(short), typeof(sbyte) } },
+ { "-01.0e-01", new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int), typeof(short), typeof(sbyte), typeof(ulong), typeof(uint), typeof(ushort), typeof(byte) } },
+ { "-.1", new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int), typeof(short), typeof(sbyte), typeof(ulong), typeof(uint), typeof(ushort), typeof(byte) } },
+ { "-0100.0e-1", new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int), typeof(short), typeof(sbyte) } },
+ };
+
+ foreach (KeyValuePair<object, List<Type>> mapping in valuesToNonOverflowingTypesMapping)
+ {
+ ConvertValueToNumber<double>(mapping);
+ ConvertValueToNumber<float>(mapping);
+ ConvertValueToNumber<decimal>(mapping);
+ ConvertValueToNumber<long>(mapping);
+ ConvertValueToNumber<int>(mapping);
+ ConvertValueToNumber<short>(mapping);
+ ConvertValueToNumber<sbyte>(mapping);
+ ConvertValueToNumber<ulong>(mapping);
+ ConvertValueToNumber<uint>(mapping);
+ ConvertValueToNumber<ushort>(mapping);
+ ConvertValueToNumber<byte>(mapping);
+ }
+
+ Dictionary<object, List<Type>> valuesThatAreInvalidNumber = new Dictionary<object, List<Type>>
+ {
+ { "1L", new List<Type> { } },
+ { "0x1", new List<Type> { } },
+ { "1e309", new List<Type> { } },
+ { "", new List<Type> { } },
+ { "-", new List<Type> { } },
+ { "e10", new List<Type> { } },
+ };
+
+ foreach (KeyValuePair<object, List<Type>> mapping in valuesThatAreInvalidNumber)
+ {
+ ConvertValueToNumber<double, FormatException>(mapping);
+ ConvertValueToNumber<float, FormatException>(mapping);
+ ConvertValueToNumber<decimal, FormatException>(mapping);
+ ConvertValueToNumber<long, FormatException>(mapping);
+ ConvertValueToNumber<int, FormatException>(mapping);
+ ConvertValueToNumber<short, FormatException>(mapping);
+ ConvertValueToNumber<sbyte, FormatException>(mapping);
+ ConvertValueToNumber<ulong, FormatException>(mapping);
+ ConvertValueToNumber<uint, FormatException>(mapping);
+ ConvertValueToNumber<ushort, FormatException>(mapping);
+ ConvertValueToNumber<byte, FormatException>(mapping);
+ }
+ }
+
+ /// <summary>
+ /// Tests for the <see cref="System.Json.JsonValue.ReadAs{T}()"/> method where T is a number type and the value is created from a number.
+ /// This is essentially a number conversion test.
+ /// </summary>
+ [Fact]
+ public void TestReadAsNumberFromNumber()
+ {
+ Dictionary<object, List<Type>> valuesToNonOverflowingTypesMapping = new Dictionary<object, List<Type>>
+ {
+ { double.NaN, new List<Type> { typeof(float), typeof(double) } },
+ { double.NegativeInfinity, new List<Type> { typeof(float), typeof(double) } },
+ { double.PositiveInfinity, new List<Type> { typeof(float), typeof(double) } },
+ { float.NaN, new List<Type> { typeof(float), typeof(double) } },
+ { float.NegativeInfinity, new List<Type> { typeof(float), typeof(double) } },
+ { float.PositiveInfinity, new List<Type> { typeof(float), typeof(double) } },
+ { double.MaxValue, new List<Type> { typeof(double), typeof(float) } },
+ { double.MinValue, new List<Type> { typeof(double), typeof(float) } },
+ { float.MaxValue, new List<Type> { typeof(double), typeof(float) } },
+ { float.MinValue, new List<Type> { typeof(double), typeof(float) } },
+ { Int64.MaxValue, new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(ulong) } },
+ { Int64.MinValue, new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long) } },
+ { Int32.MaxValue, new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int), typeof(ulong), typeof(uint) } },
+ { Int32.MinValue, new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int) } },
+ { Int16.MaxValue, new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int), typeof(short), typeof(ulong), typeof(uint), typeof(ushort) } },
+ { Int16.MinValue, new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int), typeof(short) } },
+ { SByte.MaxValue, new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int), typeof(short), typeof(sbyte), typeof(ulong), typeof(uint), typeof(ushort), typeof(byte) } },
+ { SByte.MinValue, new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int), typeof(short), typeof(sbyte) } },
+ { UInt64.MaxValue, new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(ulong) } },
+ { UInt64.MinValue, new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int), typeof(short), typeof(sbyte), typeof(ulong), typeof(uint), typeof(ushort), typeof(byte) } },
+ { UInt32.MaxValue, new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(ulong), typeof(uint) } },
+ { UInt32.MinValue, new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int), typeof(short), typeof(sbyte), typeof(ulong), typeof(uint), typeof(ushort), typeof(byte) } },
+ { UInt16.MaxValue, new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int), typeof(ulong), typeof(uint), typeof(ushort) } },
+ { UInt16.MinValue, new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int), typeof(short), typeof(sbyte), typeof(ulong), typeof(uint), typeof(ushort), typeof(byte) } },
+ { Byte.MaxValue, new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int), typeof(short), typeof(ulong), typeof(uint), typeof(ushort), typeof(byte) } },
+ { Byte.MinValue, new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int), typeof(short), typeof(sbyte), typeof(ulong), typeof(uint), typeof(ushort), typeof(byte) } },
+ { (double)1, new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int), typeof(short), typeof(sbyte), typeof(ulong), typeof(uint), typeof(ushort), typeof(byte) } },
+ { (float)1, new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int), typeof(short), typeof(sbyte), typeof(ulong), typeof(uint), typeof(ushort), typeof(byte) } },
+ { (decimal)1, new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int), typeof(short), typeof(sbyte), typeof(ulong), typeof(uint), typeof(ushort), typeof(byte) } },
+ { (long)1, new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int), typeof(short), typeof(sbyte), typeof(ulong), typeof(uint), typeof(ushort), typeof(byte) } },
+ { (int)1, new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int), typeof(short), typeof(sbyte), typeof(ulong), typeof(uint), typeof(ushort), typeof(byte) } },
+ { (short)1, new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int), typeof(short), typeof(sbyte), typeof(ulong), typeof(uint), typeof(ushort), typeof(byte) } },
+ { (sbyte)1, new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int), typeof(short), typeof(sbyte), typeof(ulong), typeof(uint), typeof(ushort), typeof(byte) } },
+ { (ulong)1, new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int), typeof(short), typeof(sbyte), typeof(ulong), typeof(uint), typeof(ushort), typeof(byte) } },
+ { (uint)1, new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int), typeof(short), typeof(sbyte), typeof(ulong), typeof(uint), typeof(ushort), typeof(byte) } },
+ { (ushort)1, new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int), typeof(short), typeof(sbyte), typeof(ulong), typeof(uint), typeof(ushort), typeof(byte) } },
+ { (byte)1, new List<Type> { typeof(double), typeof(float), typeof(decimal), typeof(long), typeof(int), typeof(short), typeof(sbyte), typeof(ulong), typeof(uint), typeof(ushort), typeof(byte) } },
+ };
+
+ foreach (KeyValuePair<object, List<Type>> mapping in valuesToNonOverflowingTypesMapping)
+ {
+ ConvertValueToNumber<double>(mapping);
+ ConvertValueToNumber<float>(mapping);
+ ConvertValueToNumber<decimal>(mapping);
+ ConvertValueToNumber<long>(mapping);
+ ConvertValueToNumber<int>(mapping);
+ ConvertValueToNumber<short>(mapping);
+ ConvertValueToNumber<sbyte>(mapping);
+ ConvertValueToNumber<ulong>(mapping);
+ ConvertValueToNumber<uint>(mapping);
+ ConvertValueToNumber<ushort>(mapping);
+ ConvertValueToNumber<byte>(mapping);
+ }
+ }
+
+ static void ConvertValueToNumber<T>(KeyValuePair<object, List<Type>> mapping)
+ {
+ ConvertValueToNumber<T, OverflowException>(mapping);
+ }
+
+ static void ConvertValueToNumber<T, TException>(KeyValuePair<object, List<Type>> mapping)
+ where TException : Exception
+ {
+ JsonValue jsonValue = CastToJsonValue(mapping.Key);
+
+ Log.Info("Converting value {0} of type {1} to type {2}.", mapping.Key, mapping.Key.GetType().Name, typeof(T).Name);
+
+ if (mapping.Value.Contains(typeof(T)))
+ {
+ Console.Write("Conversion should work... ");
+ T valueOfT;
+ Assert.True(jsonValue.TryReadAs<T>(out valueOfT));
+ if (mapping.Key.GetType() != typeof(string))
+ {
+ Console.Write("and original value casted to {0} should be the same as the retrieved value... ", typeof(T).Name);
+ T castValue = (T)Convert.ChangeType(mapping.Key, typeof(T), CultureInfo.InvariantCulture);
+ Assert.Equal<T>(castValue, valueOfT);
+ }
+ }
+ else
+ {
+ Console.Write("Conversion should fail... ");
+ T valueOfT;
+ Assert.False(jsonValue.TryReadAs<T>(out valueOfT), String.Format("It was possible to read the value as {0}", valueOfT));
+ ExpectException<TException>(delegate
+ {
+ jsonValue.ReadAs<T>();
+ });
+ }
+
+ Log.Info("Success!");
+ }
+
+ static JsonValue CastToJsonValue(object o)
+ {
+ switch (Type.GetTypeCode(o.GetType()))
+ {
+ case TypeCode.Boolean:
+ return (JsonValue)(bool)o;
+ case TypeCode.Byte:
+ return (JsonValue)(byte)o;
+ case TypeCode.Char:
+ return (JsonValue)(char)o;
+ case TypeCode.DateTime:
+ return (JsonValue)(DateTime)o;
+ case TypeCode.Decimal:
+ return (JsonValue)(decimal)o;
+ case TypeCode.Double:
+ return (JsonValue)(double)o;
+ case TypeCode.Int16:
+ return (JsonValue)(short)o;
+ case TypeCode.Int32:
+ return (JsonValue)(int)o;
+ case TypeCode.Int64:
+ return (JsonValue)(long)o;
+ case TypeCode.SByte:
+ return (JsonValue)(sbyte)o;
+ case TypeCode.Single:
+ return (JsonValue)(float)o;
+ case TypeCode.String:
+ return (JsonValue)(string)o;
+ case TypeCode.UInt16:
+ return (JsonValue)(ushort)o;
+ case TypeCode.UInt32:
+ return (JsonValue)(uint)o;
+ case TypeCode.UInt64:
+ return (JsonValue)(ulong)o;
+ default:
+ if (o.GetType() == typeof(DateTimeOffset))
+ {
+ return (JsonValue)(DateTimeOffset)o;
+ }
+
+ if (o.GetType() == typeof(Guid))
+ {
+ return (JsonValue)(Guid)o;
+ }
+
+ if (o.GetType() == typeof(Uri))
+ {
+ return (JsonValue)(Uri)o;
+ }
+
+ break;
+ }
+
+ return (JsonObject)o;
+ }
+
+ static void ExpectException<T>(Action action) where T : Exception
+ {
+ JsonValueTests.ExpectException<T>(action);
+ }
+
+ static string GetExpectedRepresentation(object obj)
+ {
+ if (obj is double)
+ {
+ double dbl = (double)obj;
+ if (Double.IsPositiveInfinity(dbl))
+ {
+ return "Infinity";
+ }
+ else if (Double.IsNegativeInfinity(dbl))
+ {
+ return "-Infinity";
+ }
+ }
+ else if (obj is float)
+ {
+ float flt = (float)obj;
+ if (Single.IsPositiveInfinity(flt))
+ {
+ return "Infinity";
+ }
+ else if (Single.IsNegativeInfinity(flt))
+ {
+ return "-Infinity";
+ }
+ }
+ else if (obj is DateTime)
+ {
+ DateTime dt = (DateTime)obj;
+ return "\"" + dt.ToString(DateTimeFormat, CultureInfo.InvariantCulture) + "\"";
+ }
+
+ using (MemoryStream ms = new MemoryStream())
+ {
+ DataContractJsonSerializer dcjs = new DataContractJsonSerializer(obj.GetType());
+ dcjs.WriteObject(ms, obj);
+ return Encoding.UTF8.GetString(ms.GetBuffer(), 0, (int)ms.Position);
+ }
+ }
+
+ void ValidateJson(JsonPrimitive jsonPrim, string expectedJson, JsonType expectedJsonType)
+ {
+ Assert.Equal(expectedJson, jsonPrim.ToString());
+ Assert.Equal(expectedJsonType, jsonPrim.JsonType);
+ }
+
+ void TestReadAsRoundtrip<T>(JsonPrimitive jsonPrim, T myOriginalObjectOfT)
+ {
+ T myReadObjectOfT = jsonPrim.ReadAs<T>();
+ T myTryReadObjectOfT;
+ Assert.True(jsonPrim.TryReadAs<T>(out myTryReadObjectOfT));
+ Assert.Equal(myOriginalObjectOfT, myReadObjectOfT);
+ Assert.Equal(myOriginalObjectOfT, myTryReadObjectOfT);
+
+ string stringValue;
+ Assert.True(jsonPrim.TryReadAs<string>(out stringValue));
+ if (typeof(T) == typeof(bool))
+ {
+ // bool returns a lowercase version. make sure we get something usable by doing another roundtrip of the value in .NET
+ Assert.Equal(String.Format(CultureInfo.InvariantCulture, "{0}", myOriginalObjectOfT), bool.Parse(stringValue).ToString(CultureInfo.InvariantCulture));
+ }
+ else if (typeof(T) == typeof(float) || typeof(T) == typeof(double))
+ {
+ Assert.Equal(String.Format(CultureInfo.InvariantCulture, "{0:R}", myOriginalObjectOfT), stringValue);
+ }
+ else if (typeof(T) == typeof(DateTime))
+ {
+ Assert.Equal(String.Format(CultureInfo.InvariantCulture, "{0:" + DateTimeFormat + "}", myOriginalObjectOfT), stringValue);
+ }
+ else
+ {
+ Assert.Equal(String.Format(CultureInfo.InvariantCulture, "{0}", myOriginalObjectOfT), stringValue);
+ }
+ }
+
+ void TestReadAsFromStringRoundtrip<T>(T value)
+ {
+ TestReadAsFromStringRoundtrip<T>(value, String.Format(CultureInfo.InvariantCulture, "{0}", value));
+ }
+
+ void TestReadAsFromStringRoundtrip<T>(T value, string valueString)
+ {
+ T tempOfT;
+ JsonPrimitive jsonPrim = new JsonPrimitive(valueString);
+ Assert.True(jsonPrim.TryReadAs<T>(out tempOfT));
+ Assert.Equal<T>(value, tempOfT);
+ }
+ }
+}
diff --git a/test/System.Json.Test.Integration/JsonStringRoundTripTests.cs b/test/System.Json.Test.Integration/JsonStringRoundTripTests.cs
new file mode 100644
index 00000000..6ef61aa3
--- /dev/null
+++ b/test/System.Json.Test.Integration/JsonStringRoundTripTests.cs
@@ -0,0 +1,583 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using Xunit;
+
+namespace System.Json
+{
+ /// <summary>
+ /// Tests for round-tripping <see cref="JsonValue"/> instances via JSON strings.
+ /// </summary>
+ public class JsonStringRoundTripTests
+ {
+ /// <summary>
+ /// Tests for <see cref="JsonObject"/> round-trip.
+ /// </summary>
+ [Fact]
+ public void ValidJsonObjectRoundTrip()
+ {
+ bool oldValue = CreatorSettings.CreateDateTimeWithSubMilliseconds;
+ CreatorSettings.CreateDateTimeWithSubMilliseconds = false;
+ try
+ {
+ int seed = 1;
+ Log.Info("Seed: {0}", seed);
+ Random rndGen = new Random(seed);
+
+ JsonObject sourceJson = new JsonObject(new Dictionary<string, JsonValue>()
+ {
+ { "Name", PrimitiveCreator.CreateInstanceOfString(rndGen) },
+ { "Age", PrimitiveCreator.CreateInstanceOfInt32(rndGen) },
+ { "DateTimeOffset", PrimitiveCreator.CreateInstanceOfDateTimeOffset(rndGen) },
+ { "Birthday", PrimitiveCreator.CreateInstanceOfDateTime(rndGen) }
+ });
+ sourceJson.Add("NewItem1", PrimitiveCreator.CreateInstanceOfString(rndGen));
+ sourceJson.Add(new KeyValuePair<string, JsonValue>("NewItem2", PrimitiveCreator.CreateInstanceOfString(rndGen)));
+
+ JsonObject newJson = (JsonObject)JsonValue.Parse(sourceJson.ToString());
+
+ newJson.Remove("NewItem1");
+ sourceJson.Remove("NewItem1");
+
+ Assert.False(newJson.ContainsKey("NewItem1"));
+
+ Assert.False(!JsonValueVerifier.Compare(sourceJson, newJson));
+ }
+ finally
+ {
+ CreatorSettings.CreateDateTimeWithSubMilliseconds = oldValue;
+ }
+ }
+
+ /// <summary>
+ /// Test for <see cref="JsonPrimitive"/> round-trip created via <see cref="DateTime"/>.
+ /// </summary>
+ [Fact]
+ public void SimpleDateTimeTest()
+ {
+ JsonValue jv = DateTime.Now;
+ JsonValue jv2 = JsonValue.Parse(jv.ToString());
+ Assert.Equal(jv.ToString(), jv2.ToString());
+ }
+
+ /// <summary>
+ /// Test for <see cref="JsonPrimitive"/> round-trip created via <see cref="DateTimeOffset"/>.
+ /// </summary>
+ [Fact]
+ public void ValidJsonObjectDateTimeOffsetRoundTrip()
+ {
+ int seed = 1;
+ Log.Info("Seed: {0}", seed);
+ Random rndGen = new Random(seed);
+
+ JsonPrimitive sourceJson = new JsonPrimitive(PrimitiveCreator.CreateInstanceOfDateTimeOffset(rndGen));
+ JsonPrimitive newJson = (JsonPrimitive)JsonValue.Parse(sourceJson.ToString());
+
+ Assert.True(JsonValueVerifier.Compare(sourceJson, newJson));
+ }
+
+ /// <summary>
+ /// Tests for <see cref="JsonArray"/> round-trip.
+ /// </summary>
+ [Fact]
+ public void ValidJsonArrayRoundTrip()
+ {
+ bool oldValue = CreatorSettings.CreateDateTimeWithSubMilliseconds;
+ CreatorSettings.CreateDateTimeWithSubMilliseconds = false;
+ try
+ {
+ int seed = 1;
+ Log.Info("Seed: {0}", seed);
+ Random rndGen = new Random(seed);
+
+ JsonArray sourceJson = new JsonArray(new JsonValue[]
+ {
+ PrimitiveCreator.CreateInstanceOfBoolean(rndGen),
+ PrimitiveCreator.CreateInstanceOfByte(rndGen),
+ PrimitiveCreator.CreateInstanceOfDateTime(rndGen),
+ PrimitiveCreator.CreateInstanceOfDateTimeOffset(rndGen),
+ PrimitiveCreator.CreateInstanceOfDecimal(rndGen),
+ PrimitiveCreator.CreateInstanceOfDouble(rndGen),
+ PrimitiveCreator.CreateInstanceOfInt16(rndGen),
+ PrimitiveCreator.CreateInstanceOfInt32(rndGen),
+ PrimitiveCreator.CreateInstanceOfInt64(rndGen),
+ PrimitiveCreator.CreateInstanceOfSByte(rndGen),
+ PrimitiveCreator.CreateInstanceOfSingle(rndGen),
+ PrimitiveCreator.CreateInstanceOfString(rndGen),
+ PrimitiveCreator.CreateInstanceOfUInt16(rndGen),
+ PrimitiveCreator.CreateInstanceOfUInt32(rndGen),
+ PrimitiveCreator.CreateInstanceOfUInt64(rndGen)
+ });
+
+ JsonArray newJson = (JsonArray)JsonValue.Parse(sourceJson.ToString());
+
+ Log.Info("Original JsonArray object is: {0}", sourceJson);
+ Log.Info("Round-tripped JsonArray object is: {0}", newJson);
+
+ Assert.True(JsonValueVerifier.Compare(sourceJson, newJson));
+ }
+ finally
+ {
+ CreatorSettings.CreateDateTimeWithSubMilliseconds = oldValue;
+ }
+ }
+
+ /// <summary>
+ /// Test for <see cref="JsonPrimitive"/> round-trip created via <see cref="String"/>.
+ /// </summary>
+ [Fact]
+ public void ValidPrimitiveStringRoundTrip()
+ {
+ Assert.True(this.TestPrimitiveType("String"));
+ }
+
+ /// <summary>
+ /// Test for <see cref="JsonPrimitive"/> round-trip created via <see cref="DateTime"/>.
+ /// </summary>
+ [Fact]
+ public void ValidPrimitiveDateTimeRoundTrip()
+ {
+ bool oldValue = CreatorSettings.CreateDateTimeWithSubMilliseconds;
+ CreatorSettings.CreateDateTimeWithSubMilliseconds = false;
+ try
+ {
+ Assert.True(this.TestPrimitiveType("DateTime"));
+ }
+ finally
+ {
+ CreatorSettings.CreateDateTimeWithSubMilliseconds = oldValue;
+ }
+ }
+
+ /// <summary>
+ /// Test for <see cref="JsonPrimitive"/> round-trip created via <see cref="Boolean"/>.
+ /// </summary>
+ [Fact]
+ public void ValidPrimitiveBooleanRoundTrip()
+ {
+ Assert.True(this.TestPrimitiveType("Boolean"));
+ }
+
+ /// <summary>
+ /// Test for <see cref="JsonPrimitive"/> round-trip created via <see cref="Byte"/>.
+ /// </summary>
+ [Fact]
+ public void ValidPrimitiveByteRoundTrip()
+ {
+ Assert.True(this.TestPrimitiveType("Byte"));
+ }
+
+ /// <summary>
+ /// Test for <see cref="JsonPrimitive"/> round-trip created via <see cref="Decimal"/>.
+ /// </summary>
+ [Fact]
+ public void ValidPrimitiveDecimalRoundTrip()
+ {
+ Assert.True(this.TestPrimitiveType("Decimal"));
+ }
+
+ /// <summary>
+ /// Test for <see cref="JsonPrimitive"/> round-trip created via <see cref="Double"/>.
+ /// </summary>
+ [Fact]
+ public void ValidPrimitiveDoubleRoundTrip()
+ {
+ Assert.True(this.TestPrimitiveType("Double"));
+ }
+
+ /// <summary>
+ /// Test for <see cref="JsonPrimitive"/> round-trip created via <see cref="Int16"/>.
+ /// </summary>
+ [Fact]
+ public void ValidPrimitiveInt16RoundTrip()
+ {
+ Assert.True(this.TestPrimitiveType("Int16"));
+ }
+
+ /// <summary>
+ /// Test for <see cref="JsonPrimitive"/> round-trip created via <see cref="Int32"/>.
+ /// </summary>
+ [Fact]
+ public void ValidPrimitiveInt32RoundTrip()
+ {
+ Assert.True(this.TestPrimitiveType("Int32"));
+ }
+
+ /// <summary>
+ /// Test for <see cref="JsonPrimitive"/> round-trip created via <see cref="Int64"/>.
+ /// </summary>
+ [Fact]
+ public void ValidPrimitiveInt64RoundTrip()
+ {
+ Assert.True(this.TestPrimitiveType("Int64"));
+ }
+
+ /// <summary>
+ /// Test for <see cref="JsonPrimitive"/> round-trip created via <see cref="SByte"/>.
+ /// </summary>
+ [Fact]
+ public void ValidPrimitiveSByteRoundTrip()
+ {
+ Assert.True(this.TestPrimitiveType("SByte"));
+ }
+
+ /// <summary>
+ /// Test for <see cref="JsonPrimitive"/> round-trip created via <see cref="UInt16"/>.
+ /// </summary>
+ [Fact]
+ public void ValidPrimitiveUInt16RoundTrip()
+ {
+ Assert.True(this.TestPrimitiveType("Uint16"));
+ }
+
+ /// <summary>
+ /// Test for <see cref="JsonPrimitive"/> round-trip created via <see cref="UInt32"/>.
+ /// </summary>
+ [Fact]
+ public void ValidPrimitiveUInt32RoundTrip()
+ {
+ Assert.True(this.TestPrimitiveType("UInt32"));
+ }
+
+ /// <summary>
+ /// Test for <see cref="JsonPrimitive"/> round-trip created via <see cref="UInt64"/>.
+ /// </summary>
+ [Fact]
+ public void ValidPrimitiveUInt64RoundTrip()
+ {
+ Assert.True(this.TestPrimitiveType("UInt64"));
+ }
+
+ /// <summary>
+ /// Test for <see cref="JsonPrimitive"/> round-trip created via <see cref="Char"/>.
+ /// </summary>
+ [Fact]
+ public void ValidPrimitiveCharRoundTrip()
+ {
+ Assert.True(this.TestPrimitiveType("Char"));
+ }
+
+ /// <summary>
+ /// Test for <see cref="JsonPrimitive"/> round-trip created via <see cref="Guid"/>.
+ /// </summary>
+ [Fact]
+ public void ValidPrimitiveGuidRoundTrip()
+ {
+ Assert.True(this.TestPrimitiveType("Guid"));
+ }
+
+ /// <summary>
+ /// Test for <see cref="JsonPrimitive"/> round-trip created via <see cref="Uri"/>.
+ /// </summary>
+ [Fact]
+ public void ValidPrimitiveUriRoundTrip()
+ {
+ Assert.True(this.TestPrimitiveType("Uri"));
+ }
+
+ /// <summary>
+ /// Tests for <see cref="JsonValue"/> round-trip created via <code>null</code> values.
+ /// </summary>
+ [Fact]
+ public void ValidPrimitiveNullRoundTrip()
+ {
+ Assert.True(this.TestPrimitiveType("Null"));
+ }
+
+ /// <summary>
+ /// Tests for round-tripping <see cref="JsonPrimitive"/> objects via casting to CLR instances.
+ /// </summary>
+ [Fact]
+ public void JsonValueRoundTripCastTests()
+ {
+ int seed = 1;
+ Log.Info("Seed: {0}", seed);
+ Random rndGen = new Random(seed);
+
+ this.DoRoundTripCasting(String.Empty, typeof(string));
+ this.DoRoundTripCasting("null", typeof(string));
+ string str;
+ do
+ {
+ str = PrimitiveCreator.CreateInstanceOfString(rndGen);
+ } while (str == null);
+
+ this.DoRoundTripCasting(str, typeof(string));
+ this.DoRoundTripCasting(PrimitiveCreator.CreateInstanceOfInt16(rndGen), typeof(int));
+ this.DoRoundTripCasting(PrimitiveCreator.CreateInstanceOfInt32(rndGen), typeof(int));
+ this.DoRoundTripCasting(PrimitiveCreator.CreateInstanceOfInt64(rndGen), typeof(int));
+ this.DoRoundTripCasting(PrimitiveCreator.CreateInstanceOfUInt16(rndGen), typeof(int));
+ this.DoRoundTripCasting(PrimitiveCreator.CreateInstanceOfUInt32(rndGen), typeof(int));
+ this.DoRoundTripCasting(PrimitiveCreator.CreateInstanceOfUInt64(rndGen), typeof(int));
+ this.DoRoundTripCasting(PrimitiveCreator.CreateInstanceOfGuid(rndGen), typeof(Guid));
+ this.DoRoundTripCasting(new Uri("http://bug/test?param=hello%0a"), typeof(Uri));
+ this.DoRoundTripCasting(PrimitiveCreator.CreateInstanceOfChar(rndGen), typeof(char));
+ this.DoRoundTripCasting(PrimitiveCreator.CreateInstanceOfBoolean(rndGen), typeof(bool));
+ this.DoRoundTripCasting(PrimitiveCreator.CreateInstanceOfDateTime(rndGen), typeof(DateTime));
+ this.DoRoundTripCasting(PrimitiveCreator.CreateInstanceOfDateTimeOffset(rndGen), typeof(DateTimeOffset));
+ this.DoRoundTripCasting(PrimitiveCreator.CreateInstanceOfDouble(rndGen), typeof(double));
+ this.DoRoundTripCasting(PrimitiveCreator.CreateInstanceOfDouble(rndGen), typeof(float));
+ this.DoRoundTripCasting(0.12345f, typeof(double));
+ this.DoRoundTripCasting(0.12345f, typeof(float));
+ }
+
+ private bool TestPrimitiveType(string typeName)
+ {
+ bool retValue = true;
+ bool specialCase = false;
+
+ int seed = 1;
+ Log.Info("Seed: {0}", seed);
+ Random rndGen = new Random(seed);
+
+ JsonPrimitive sourceJson = null;
+ JsonPrimitive sourceJson2;
+ object tempValue = null;
+ switch (typeName.ToLower())
+ {
+ case "boolean":
+ tempValue = PrimitiveCreator.CreateInstanceOfBoolean(rndGen);
+ sourceJson = (JsonPrimitive)JsonValue.Parse(tempValue.ToString().ToLower());
+ sourceJson2 = new JsonPrimitive((bool)tempValue);
+ break;
+ case "byte":
+ tempValue = PrimitiveCreator.CreateInstanceOfByte(rndGen);
+ sourceJson = (JsonPrimitive)JsonValue.Parse(tempValue.ToString());
+ sourceJson2 = new JsonPrimitive((byte)tempValue);
+ break;
+ case "char":
+ sourceJson2 = new JsonPrimitive((char)PrimitiveCreator.CreateInstanceOfChar(rndGen));
+ specialCase = true;
+ break;
+ case "datetime":
+ tempValue = PrimitiveCreator.CreateInstanceOfDateTime(rndGen);
+ sourceJson2 = new JsonPrimitive((DateTime)tempValue);
+ sourceJson = (JsonPrimitive)JsonValue.Parse(sourceJson2.ToString());
+ break;
+ case "decimal":
+ tempValue = PrimitiveCreator.CreateInstanceOfDecimal(rndGen);
+ sourceJson = (JsonPrimitive)JsonValue.Parse(((decimal)tempValue).ToString(NumberFormatInfo.InvariantInfo));
+ sourceJson2 = new JsonPrimitive((decimal)tempValue);
+ break;
+ case "double":
+ double tempDouble = PrimitiveCreator.CreateInstanceOfDouble(rndGen);
+ sourceJson = (JsonPrimitive)JsonValue.Parse(tempDouble.ToString("R", NumberFormatInfo.InvariantInfo));
+ sourceJson2 = new JsonPrimitive(tempDouble);
+ break;
+ case "guid":
+ sourceJson2 = new JsonPrimitive(PrimitiveCreator.CreateInstanceOfGuid(rndGen));
+ specialCase = true;
+ break;
+ case "int16":
+ tempValue = PrimitiveCreator.CreateInstanceOfInt16(rndGen);
+ sourceJson = (JsonPrimitive)JsonValue.Parse(tempValue.ToString());
+ sourceJson2 = new JsonPrimitive((short)tempValue);
+ break;
+ case "int32":
+ tempValue = PrimitiveCreator.CreateInstanceOfInt32(rndGen);
+ sourceJson = (JsonPrimitive)JsonValue.Parse(tempValue.ToString());
+ sourceJson2 = new JsonPrimitive((int)tempValue);
+ break;
+ case "int64":
+ tempValue = PrimitiveCreator.CreateInstanceOfInt64(rndGen);
+ sourceJson = (JsonPrimitive)JsonValue.Parse(tempValue.ToString());
+ sourceJson2 = new JsonPrimitive((long)tempValue);
+ break;
+ case "sbyte":
+ tempValue = PrimitiveCreator.CreateInstanceOfSByte(rndGen);
+ sourceJson = (JsonPrimitive)JsonValue.Parse(tempValue.ToString());
+ sourceJson2 = new JsonPrimitive((sbyte)tempValue);
+ break;
+ case "single":
+ float fltValue = PrimitiveCreator.CreateInstanceOfSingle(rndGen);
+ sourceJson = (JsonPrimitive)JsonValue.Parse(fltValue.ToString("R", NumberFormatInfo.InvariantInfo));
+ sourceJson2 = new JsonPrimitive(fltValue);
+ break;
+ case "string":
+ do
+ {
+ tempValue = PrimitiveCreator.CreateInstanceOfString(rndGen);
+ } while (tempValue == null);
+
+ sourceJson2 = new JsonPrimitive((string)tempValue);
+ sourceJson = (JsonPrimitive)JsonValue.Parse(sourceJson2.ToString());
+ break;
+ case "uint16":
+ tempValue = PrimitiveCreator.CreateInstanceOfUInt16(rndGen);
+ sourceJson = (JsonPrimitive)JsonValue.Parse(tempValue.ToString());
+ sourceJson2 = new JsonPrimitive((ushort)tempValue);
+ break;
+ case "uint32":
+ tempValue = PrimitiveCreator.CreateInstanceOfUInt32(rndGen);
+ sourceJson = (JsonPrimitive)JsonValue.Parse(tempValue.ToString());
+ sourceJson2 = new JsonPrimitive((uint)tempValue);
+ break;
+ case "uint64":
+ tempValue = PrimitiveCreator.CreateInstanceOfUInt64(rndGen);
+ sourceJson = (JsonPrimitive)JsonValue.Parse(tempValue.ToString());
+ sourceJson2 = new JsonPrimitive((ulong)tempValue);
+ break;
+ case "uri":
+ Uri uri = null;
+ do
+ {
+ try
+ {
+ uri = PrimitiveCreator.CreateInstanceOfUri(rndGen);
+ }
+ catch (UriFormatException)
+ {
+ }
+ } while (uri == null);
+
+ sourceJson2 = new JsonPrimitive(uri);
+ specialCase = true;
+ break;
+ case "null":
+ sourceJson = (JsonPrimitive)JsonValue.Parse("null");
+ sourceJson2 = null;
+ break;
+ default:
+ sourceJson = null;
+ sourceJson2 = null;
+ break;
+ }
+
+ if (!specialCase)
+ {
+ // comparison between two constructors
+ if (!JsonValueVerifier.Compare(sourceJson, sourceJson2))
+ {
+ Log.Info("(JsonPrimitive)JsonValue.Parse(string) failed to match the results from default JsonPrimitive(obj)constructor for type {0}", typeName);
+ retValue = false;
+ }
+
+ if (sourceJson != null)
+ {
+ // test JsonValue.Load(TextReader)
+ JsonPrimitive newJson = null;
+ using (StringReader sr = new StringReader(sourceJson.ToString()))
+ {
+ newJson = (JsonPrimitive)JsonValue.Load(sr);
+ }
+
+ if (!JsonValueVerifier.Compare(sourceJson, newJson))
+ {
+ Log.Info("JsonValue.Load(TextReader) failed to function properly for type {0}", typeName);
+ retValue = false;
+ }
+
+ // test JsonValue.Load(Stream) is located in the JObjectFromGenoTypeLib test case
+
+ // test JsonValue.Parse(string)
+ newJson = null;
+ newJson = (JsonPrimitive)JsonValue.Parse(sourceJson.ToString());
+ if (!JsonValueVerifier.Compare(sourceJson, newJson))
+ {
+ Log.Info("JsonValue.Parse(string) failed to function properly for type {0}", typeName);
+ retValue = false;
+ }
+ }
+ }
+ else
+ {
+ // test JsonValue.Load(TextReader)
+ JsonPrimitive newJson2 = null;
+ using (StringReader sr = new StringReader(sourceJson2.ToString()))
+ {
+ newJson2 = (JsonPrimitive)JsonValue.Load(sr);
+ }
+
+ if (!JsonValueVerifier.Compare(sourceJson2, newJson2))
+ {
+ Log.Info("JsonValue.Load(TextReader) failed to function properly for type {0}", typeName);
+ retValue = false;
+ }
+
+ // test JsonValue.Load(Stream) is located in the JObjectFromGenoTypeLib test case
+
+ // test JsonValue.Parse(string)
+ newJson2 = null;
+ newJson2 = (JsonPrimitive)JsonValue.Parse(sourceJson2.ToString());
+ if (!JsonValueVerifier.Compare(sourceJson2, newJson2))
+ {
+ Log.Info("JsonValue.Parse(string) failed to function properly for type {0}", typeName);
+ retValue = false;
+ }
+ }
+
+ return retValue;
+ }
+
+ private void DoRoundTripCasting(JsonValue jo, Type type)
+ {
+ bool result = false;
+
+ // Casting
+ if (jo.JsonType == JsonType.String)
+ {
+ JsonValue jstr = (string)jo;
+ if (type == typeof(DateTime))
+ {
+ Log.Info("{0} Value:{1}", type.Name, ((DateTime)jstr).ToString(DateTimeFormatInfo.InvariantInfo));
+ }
+ else if (type == typeof(DateTimeOffset))
+ {
+ Log.Info("{0} Value:{1}", type.Name, ((DateTimeOffset)jstr).ToString(DateTimeFormatInfo.InvariantInfo));
+ }
+ else if (type == typeof(Guid))
+ {
+ Log.Info("{0} Value:{1}", type.Name, (Guid)jstr);
+ }
+ else if (type == typeof(char))
+ {
+ Log.Info("{0} Value:{1}", type.Name, (char)jstr);
+ }
+ else if (type == typeof(Uri))
+ {
+ Log.Info("{0} Value:{1}", type.Name, ((Uri)jstr).AbsoluteUri);
+ }
+ else
+ {
+ Log.Info("{0} Value:{1}", type.Name, (string)jstr);
+ }
+
+ if (jo.ToString() == jstr.ToString())
+ {
+ result = true;
+ }
+ }
+ else if (jo.JsonType == JsonType.Object)
+ {
+ JsonObject jobj = new JsonObject((JsonObject)jo);
+
+ if (jo.ToString() == jobj.ToString())
+ {
+ result = true;
+ }
+ }
+ else if (jo.JsonType == JsonType.Number)
+ {
+ JsonPrimitive jprim = (JsonPrimitive)jo;
+ Log.Info("{0} Value:{1}", type.Name, jprim);
+
+ if (jo.ToString() == jprim.ToString())
+ {
+ result = true;
+ }
+ }
+ else if (jo.JsonType == JsonType.Boolean)
+ {
+ JsonPrimitive jprim = (JsonPrimitive)jo;
+ Log.Info("{0} Value:{1}", type.Name, (bool)jprim);
+
+ if (jo.ToString() == jprim.ToString())
+ {
+ result = true;
+ }
+ }
+
+ Assert.True(result);
+ }
+ }
+}
diff --git a/test/System.Json.Test.Integration/JsonValueAndComplexTypesTests.cs b/test/System.Json.Test.Integration/JsonValueAndComplexTypesTests.cs
new file mode 100644
index 00000000..3b878d04
--- /dev/null
+++ b/test/System.Json.Test.Integration/JsonValueAndComplexTypesTests.cs
@@ -0,0 +1,329 @@
+using System.Collections.Generic;
+using System.Dynamic;
+using System.IO;
+using System.Runtime.Serialization.Json;
+using System.Text;
+using Xunit;
+
+namespace System.Json
+{
+ /// <summary>
+ /// Tests for the methods to convert between <see cref="JsonValue"/> instances and complex types.
+ /// </summary>
+ public class JsonValueAndComplexTypesTests
+ {
+ static readonly Type[] testTypes = new Type[]
+ {
+ typeof(DCType_1),
+ typeof(StructGuid),
+ typeof(StructInt16),
+ typeof(DCType_3),
+ typeof(SerType_4),
+ typeof(SerType_5),
+ typeof(DCType_7),
+ typeof(DCType_9),
+ typeof(SerType_11),
+ typeof(DCType_15),
+ typeof(DCType_16),
+ typeof(DCType_18),
+ typeof(DCType_19),
+ typeof(DCType_20),
+ typeof(SerType_22),
+ typeof(DCType_25),
+ typeof(SerType_26),
+ typeof(DCType_31),
+ typeof(DCType_32),
+ typeof(SerType_33),
+ typeof(DCType_34),
+ typeof(DCType_36),
+ typeof(DCType_38),
+ typeof(DCType_40),
+ typeof(DCType_42),
+ typeof(DCType_65),
+ typeof(ListType_1),
+ typeof(ListType_2),
+ typeof(BaseType),
+ typeof(PolymorphicMember),
+ typeof(PolymorphicAsInterfaceMember),
+ typeof(CollectionsWithPolymorphicMember),
+ };
+
+ /// <summary>
+ /// Tests for the <see cref="JsonValueExtensions.CreateFrom"/> method.
+ /// </summary>
+ [Fact]
+ public void CreateFromTests()
+ {
+ InstanceCreatorSurrogate oldSurrogate = CreatorSettings.CreatorSurrogate;
+ try
+ {
+ CreatorSettings.CreatorSurrogate = new NoInfinityFloatSurrogate();
+ DateTime now = DateTime.Now;
+ int seed = (10000 * now.Year) + (100 * now.Month) + now.Day;
+ Log.Info("Seed: {0}", seed);
+ Random rndGen = new Random(seed);
+ foreach (Type testType in testTypes)
+ {
+ object instance = InstanceCreator.CreateInstanceOf(testType, rndGen);
+ JsonValue jv = JsonValueExtensions.CreateFrom(instance);
+
+ if (instance == null)
+ {
+ Assert.Null(jv);
+ }
+ else
+ {
+ DataContractJsonSerializer dcjs = new DataContractJsonSerializer(instance == null ? testType : instance.GetType());
+ string fromDCJS;
+ using (MemoryStream ms = new MemoryStream())
+ {
+ dcjs.WriteObject(ms, instance);
+ fromDCJS = Encoding.UTF8.GetString(ms.ToArray());
+ }
+
+ Log.Info("{0}: {1}", testType.Name, fromDCJS);
+
+ if (instance == null)
+ {
+ Assert.Null(jv);
+ }
+ else
+ {
+ string fromJsonValue = jv.ToString();
+ Assert.Equal(fromDCJS, fromJsonValue);
+ }
+ }
+ }
+ }
+ finally
+ {
+ CreatorSettings.CreatorSurrogate = oldSurrogate;
+ }
+ }
+
+ /// <summary>
+ /// Tests for the <see cref="JsonValueExtensions.ReadAsType{T}(JsonValue)"/> method.
+ /// </summary>
+ [Fact]
+ public void ReadAsTests()
+ {
+ InstanceCreatorSurrogate oldSurrogate = CreatorSettings.CreatorSurrogate;
+ try
+ {
+ CreatorSettings.CreatorSurrogate = new NoInfinityFloatSurrogate();
+ DateTime now = DateTime.Now;
+ int seed = (10000 * now.Year) + (100 * now.Month) + now.Day;
+ Log.Info("Seed: {0}", seed);
+ Random rndGen = new Random(seed);
+
+ this.ReadAsTest<DCType_1>(rndGen);
+ this.ReadAsTest<StructGuid>(rndGen);
+ this.ReadAsTest<StructInt16>(rndGen);
+ this.ReadAsTest<DCType_3>(rndGen);
+ this.ReadAsTest<SerType_4>(rndGen);
+ this.ReadAsTest<SerType_5>(rndGen);
+ this.ReadAsTest<DCType_7>(rndGen);
+ this.ReadAsTest<DCType_9>(rndGen);
+ this.ReadAsTest<SerType_11>(rndGen);
+ this.ReadAsTest<DCType_15>(rndGen);
+ this.ReadAsTest<DCType_16>(rndGen);
+ this.ReadAsTest<DCType_18>(rndGen);
+ this.ReadAsTest<DCType_19>(rndGen);
+ this.ReadAsTest<DCType_20>(rndGen);
+ this.ReadAsTest<SerType_22>(rndGen);
+ this.ReadAsTest<DCType_25>(rndGen);
+ this.ReadAsTest<SerType_26>(rndGen);
+ this.ReadAsTest<DCType_31>(rndGen);
+ this.ReadAsTest<DCType_32>(rndGen);
+ this.ReadAsTest<SerType_33>(rndGen);
+ this.ReadAsTest<DCType_34>(rndGen);
+ this.ReadAsTest<DCType_36>(rndGen);
+ this.ReadAsTest<DCType_38>(rndGen);
+ this.ReadAsTest<DCType_40>(rndGen);
+ this.ReadAsTest<DCType_42>(rndGen);
+ this.ReadAsTest<DCType_65>(rndGen);
+ this.ReadAsTest<ListType_1>(rndGen);
+ this.ReadAsTest<ListType_2>(rndGen);
+ this.ReadAsTest<BaseType>(rndGen);
+ this.ReadAsTest<PolymorphicMember>(rndGen);
+ this.ReadAsTest<PolymorphicAsInterfaceMember>(rndGen);
+ this.ReadAsTest<CollectionsWithPolymorphicMember>(rndGen);
+ }
+ finally
+ {
+ CreatorSettings.CreatorSurrogate = oldSurrogate;
+ }
+ }
+
+ /// <summary>
+ /// Tests for the <see cref="JsonValueExtensions.CreateFrom"/> for <see cref="DateTime"/>
+ /// and <see cref="DateTimeOffset"/> values.
+ /// </summary>
+ [Fact]
+ public void CreateFromDateTimeTest()
+ {
+ DateTime dt = DateTime.Now;
+ DateTimeOffset dto = DateTimeOffset.Now;
+
+ JsonValue jvDt1 = (JsonValue)dt;
+ JsonValue jvDt2 = JsonValueExtensions.CreateFrom(dt);
+
+ JsonValue jvDto1 = (JsonValue)dto;
+ JsonValue jvDto2 = JsonValueExtensions.CreateFrom(dto);
+
+ Assert.Equal(dt, (DateTime)jvDt1);
+ Assert.Equal(dt, (DateTime)jvDt2);
+
+ Assert.Equal(dto, (DateTimeOffset)jvDto1);
+ Assert.Equal(dto, (DateTimeOffset)jvDto2);
+
+ Assert.Equal(dt, jvDt1.ReadAs<DateTime>());
+ Assert.Equal(dt, jvDt2.ReadAs<DateTime>());
+
+ Assert.Equal(dto, jvDto1.ReadAs<DateTimeOffset>());
+ Assert.Equal(dto, jvDto2.ReadAs<DateTimeOffset>());
+
+ Assert.Equal(jvDt1.ToString(), jvDt2.ToString());
+ Assert.Equal(jvDto1.ToString(), jvDto2.ToString());
+ }
+
+ /// <summary>
+ /// Tests for creating <see cref="JsonValue"/> instances from dynamic objects.
+ /// </summary>
+ [Fact]
+ public void CreateFromDynamic()
+ {
+ string expectedJson = "{\"int\":12,\"str\":\"hello\",\"jv\":[1,{\"a\":true}],\"dyn\":{\"char\":\"c\",\"null\":null}}";
+ MyDynamicObject obj = new MyDynamicObject();
+ obj.fields.Add("int", 12);
+ obj.fields.Add("str", "hello");
+ obj.fields.Add("jv", new JsonArray(1, new JsonObject { { "a", true } }));
+ MyDynamicObject dyn = new MyDynamicObject();
+ obj.fields.Add("dyn", dyn);
+ dyn.fields.Add("char", 'c');
+ dyn.fields.Add("null", null);
+
+ JsonValue jv = JsonValueExtensions.CreateFrom(obj);
+ Assert.Equal(expectedJson, jv.ToString());
+ }
+
+ void ReadAsTest<T>(Random rndGen)
+ {
+ T instance = InstanceCreator.CreateInstanceOf<T>(rndGen);
+ Log.Info("ReadAsTest<{0}>, instance = {1}", typeof(T).Name, instance);
+ DataContractJsonSerializer dcjs = new DataContractJsonSerializer(typeof(T));
+ JsonValue jv;
+ using (MemoryStream ms = new MemoryStream())
+ {
+ dcjs.WriteObject(ms, instance);
+ Log.Info("{0}: {1}", typeof(T).Name, Encoding.UTF8.GetString(ms.ToArray()));
+ ms.Position = 0;
+ jv = JsonValue.Load(ms);
+ }
+
+ if (instance == null)
+ {
+ Assert.Null(jv);
+ }
+ else
+ {
+ T newInstance = jv.ReadAsType<T>();
+ Assert.Equal(instance, newInstance);
+ }
+ }
+
+ /// <summary>
+ /// Test class.
+ /// </summary>
+ public class MyDynamicObject : DynamicObject
+ {
+ /// <summary>
+ /// Test member
+ /// </summary>
+ public Dictionary<string, object> fields = new Dictionary<string, object>();
+
+ /// <summary>
+ /// Returnes the member names in this dynamic object.
+ /// </summary>
+ /// <returns>The member names in this dynamic object.</returns>
+ public override IEnumerable<string> GetDynamicMemberNames()
+ {
+ return fields.Keys;
+ }
+
+ /// <summary>
+ /// Attempts to get a named member from this dynamic object.
+ /// </summary>
+ /// <param name="binder">The dynamic binder which contains the member name.</param>
+ /// <param name="result">The value of the member, if it exists in this dynamic object.</param>
+ /// <returns><code>true</code> if the member can be returned; <code>false</code> otherwise.</returns>
+ public override bool TryGetMember(GetMemberBinder binder, out object result)
+ {
+ if (binder != null && binder.Name != null && this.fields.ContainsKey(binder.Name))
+ {
+ result = this.fields[binder.Name];
+ return true;
+ }
+
+ return base.TryGetMember(binder, out result);
+ }
+
+ /// <summary>
+ /// Attempts to set a named member from this dynamic object.
+ /// </summary>
+ /// <param name="binder">The dynamic binder which contains the member name.</param>
+ /// <param name="value">The value of the member to be set.</param>
+ /// <returns><code>true</code> if the member can be set; <code>false</code> otherwise.</returns>
+ public override bool TrySetMember(SetMemberBinder binder, object value)
+ {
+ if (binder != null && binder.Name != null)
+ {
+ this.fields[binder.Name] = value;
+ return true;
+ }
+
+ return base.TrySetMember(binder, value);
+ }
+ }
+
+ // Currently there are some differences in treatment of infinity between
+ // JsonValue (which writes them as Infinity/-Infinity) and DataContractJsonSerializer
+ // (which writes them as INF/-INF). This prevents those values from being used in the test.
+ // This also allows the creation of an instance of an IEmptyInterface type, used in the test.
+ class NoInfinityFloatSurrogate : InstanceCreatorSurrogate
+ {
+ public override bool CanCreateInstanceOf(Type type)
+ {
+ return type == typeof(float) || type == typeof(double) || type == typeof(IEmptyInterface) || type == typeof(BaseType);
+ }
+
+ public override object CreateInstanceOf(Type type, Random rndGen)
+ {
+ if (type == typeof(float))
+ {
+ float result;
+ do
+ {
+ result = PrimitiveCreator.CreateInstanceOfSingle(rndGen);
+ }
+ while (float.IsInfinity(result));
+ return result;
+ }
+ else if (type == typeof(double))
+ {
+ double result;
+ do
+ {
+ result = PrimitiveCreator.CreateInstanceOfDouble(rndGen);
+ }
+ while (double.IsInfinity(result));
+ return result;
+ }
+ else
+ {
+ return new DerivedType(rndGen);
+ }
+ }
+ }
+ }
+}
diff --git a/test/System.Json.Test.Integration/JsonValueDynamicTests.cs b/test/System.Json.Test.Integration/JsonValueDynamicTests.cs
new file mode 100644
index 00000000..7a21757a
--- /dev/null
+++ b/test/System.Json.Test.Integration/JsonValueDynamicTests.cs
@@ -0,0 +1,1108 @@
+using System.Collections.Generic;
+using System.Dynamic;
+using System.Globalization;
+using System.Reflection;
+using System.Runtime.Serialization.Json;
+using Xunit;
+
+namespace System.Json
+{
+ /// <summary>
+ /// Tests for the dynamic support for <see cref="JsonValue"/>.
+ /// </summary>
+ public class JsonValueDynamicTests
+ {
+ string teamNameValue = "WCF RIA Base";
+ string[] teamMembersValues = { "Carlos", "Chris", "Joe", "Miguel", "Yavor" };
+
+ /// <summary>
+ /// Tests for the dynamic getters in <see cref="JsonObject"/> instances.
+ /// </summary>
+ [Fact]
+ public void JsonObjectDynamicGetters()
+ {
+ dynamic team = new JsonObject();
+ team["TeamSize"] = this.teamMembersValues.Length;
+ team["TeamName"] = this.teamNameValue;
+ team["TeamMascots"] = null;
+ team["TeamMembers"] = new JsonArray
+ {
+ this.teamMembersValues[0], this.teamMembersValues[1], this.teamMembersValues[2],
+ this.teamMembersValues[3], this.teamMembersValues[4]
+ };
+
+ Assert.Equal(this.teamMembersValues.Length, (int)team.TeamSize);
+ Assert.Equal(this.teamNameValue, (string)team.TeamName);
+ Assert.NotNull(team.TeamMascots);
+ Assert.True(team.TeamMascots is JsonValue); // default
+
+ for (int i = 0; i < this.teamMembersValues.Length; i++)
+ {
+ Assert.Equal(this.teamMembersValues[i], (string)team.TeamMembers[i]);
+ }
+
+ for (int i = 0; i < this.teamMembersValues.Length; i++)
+ {
+ Assert.Equal(this.teamMembersValues[i], (string)team.TeamMembers[i]);
+ }
+
+ // Negative tests for getters
+ JsonValueTests.ExpectException<InvalidCastException>(delegate { int fail = (int)team.NonExistentProp; });
+ }
+
+ /// <summary>
+ /// Tests for the dynamic setters in <see cref="JsonObject"/> instances.
+ /// </summary>
+ [Fact]
+ public void JsonObjectDynamicSetters()
+ {
+ dynamic team = new JsonObject();
+ team.TeamSize = this.teamMembersValues.Length;
+ team.TeamName = this.teamNameValue;
+ team.TeamMascots = null;
+ team.TeamMembers = new JsonArray
+ {
+ this.teamMembersValues[0], this.teamMembersValues[1], this.teamMembersValues[2],
+ this.teamMembersValues[3], this.teamMembersValues[4]
+ };
+
+ Assert.Equal(this.teamMembersValues.Length, (int)team["TeamSize"]);
+ Assert.Equal(this.teamNameValue, (string)team["TeamName"]);
+ Assert.NotNull(team["TeamMascots"]);
+ Assert.True(team["TeamMascots"] is JsonValue);
+
+ for (int i = 0; i < this.teamMembersValues.Length; i++)
+ {
+ Assert.Equal(this.teamMembersValues[i], (string)team["TeamMembers"][i]);
+ }
+
+ // Could not come up with negative setter
+ }
+
+ /// <summary>
+ /// Tests for the dynamic indexers in <see cref="JsonArray"/> instances.
+ /// </summary>
+ [Fact]
+ public void JsonArrayDynamicSanity()
+ {
+ // Sanity test for JsonArray to ensure [] still works even if dynamic
+ dynamic people = new JsonArray();
+ foreach (string member in this.teamMembersValues)
+ {
+ people.Add(member);
+ }
+
+ Assert.Equal(this.teamMembersValues[0], (string)people[0]);
+ Assert.Equal(this.teamMembersValues[1], (string)people[1]);
+ Assert.Equal(this.teamMembersValues[2], (string)people[2]);
+ Assert.Equal(this.teamMembersValues[3], (string)people[3]);
+ Assert.Equal(this.teamMembersValues[4], (string)people[4]);
+
+ // Note: this test and the above execute the dynamic binder differently.
+ for (int i = 0; i < people.Count; i++)
+ {
+ Assert.Equal(this.teamMembersValues[i], (string)people[i]);
+ }
+
+ people.Add(this.teamMembersValues.Length);
+ people.Add(this.teamNameValue);
+
+ Assert.Equal(this.teamMembersValues.Length, (int)people[5]);
+ Assert.Equal(this.teamNameValue, (string)people[6]);
+ }
+
+ /// <summary>
+ /// Tests for calling methods in dynamic references to <see cref="JsonValue"/> instances.
+ /// </summary>
+ [Fact]
+ public void DynamicMethodCalling()
+ {
+ JsonObject jo = new JsonObject();
+ dynamic dyn = jo;
+ dyn.Foo = "bar";
+ Assert.Equal(1, jo.Count);
+ Assert.Equal(1, dyn.Count);
+ dyn.Remove("Foo");
+ Assert.Equal(0, jo.Count);
+ }
+
+ /// <summary>
+ /// Tests for using boolean operators in dynamic references to <see cref="JsonValue"/> instances.
+ /// </summary>
+ [Fact(Skip = "Ignore")]
+ public void DynamicBooleanOperators()
+ {
+ JsonValue jv;
+ dynamic dyn;
+ foreach (bool value in new bool[] { true, false })
+ {
+ jv = value;
+ dyn = jv;
+ Log.Info("IsTrue, {0}", jv);
+ if (dyn)
+ {
+ Assert.True(value, "Boolean evaluation should not enter 'if' clause.");
+ }
+ else
+ {
+ Assert.False(value, "Boolean evaluation should not enter 'else' clause.");
+ }
+ }
+
+ foreach (string value in new string[] { "true", "false", "True", "False" })
+ {
+ bool isTrueValue = value.Equals("true", StringComparison.InvariantCultureIgnoreCase);
+ jv = new JsonPrimitive(value);
+ dyn = jv;
+ Log.Info("IsTrue, {0}", jv);
+ if (dyn)
+ {
+ Assert.True(isTrueValue, "Boolean evaluation should not enter 'if' clause.");
+ }
+ else
+ {
+ Assert.False(isTrueValue, "Boolean evaluation should not enter 'else' clause.");
+ }
+ }
+
+ foreach (bool first in new bool[] { false, true })
+ {
+ dynamic dyn1 = new JsonPrimitive(first);
+ Log.Info("Negation, {0}", first);
+ Assert.Equal(!first, !dyn1);
+ foreach (bool second in new bool[] { false, true })
+ {
+ dynamic dyn2 = new JsonPrimitive(second);
+ Log.Info("Boolean AND, {0} && {1}", first, second);
+ Assert.Equal(first && second, (bool)(dyn1 && dyn2));
+ Log.Info("Boolean OR, {0} && {1}", first, second);
+ Assert.Equal(first || second, (bool)(dyn1 || dyn2));
+ }
+ }
+
+ Log.Info("Invalid boolean operator usage");
+ dynamic boolDyn = new JsonPrimitive(true);
+ dynamic intDyn = new JsonPrimitive(1);
+ dynamic strDyn = new JsonPrimitive("hello");
+
+ JsonValueTests.ExpectException<InvalidOperationException>(() => { Log.Info("{0}", !intDyn); });
+
+ JsonValueTests.ExpectException<InvalidOperationException>(() => { Log.Info("{0}", !strDyn); });
+ JsonValueTests.ExpectException<InvalidCastException>(() => { Log.Info("{0}", intDyn && intDyn); });
+ JsonValueTests.ExpectException<InvalidCastException>(() => { Log.Info("{0}", intDyn || true); });
+ JsonValueTests.ExpectException<InvalidOperationException>(() => { Log.Info("{0}", boolDyn && 1); });
+ JsonValueTests.ExpectException<InvalidOperationException>(() => { Log.Info("{0}", boolDyn && intDyn); });
+ JsonValueTests.ExpectException<InvalidOperationException>(() => { Log.Info("{0}", boolDyn && "hello"); });
+ JsonValueTests.ExpectException<InvalidOperationException>(() => { Log.Info("{0}", boolDyn && strDyn); });
+ JsonValueTests.ExpectException<FormatException>(() => { Log.Info("{0}", strDyn && boolDyn); });
+ JsonValueTests.ExpectException<FormatException>(() => { Log.Info("{0}", strDyn || true); });
+
+ JsonValueTests.ExpectException<InvalidOperationException>(() => { Log.Info("{0}", !intDyn.NotHere); });
+ JsonValueTests.ExpectException<InvalidOperationException>(() => { Log.Info("{0}", !intDyn.NotHere && true); });
+ JsonValueTests.ExpectException<InvalidOperationException>(() => { Log.Info("{0}", !intDyn.NotHere || false); });
+ }
+
+ /// <summary>
+ /// Tests for using relational operators in dynamic references to <see cref="JsonValue"/> instances.
+ /// </summary>
+ [Fact(Skip = "Ignore")]
+ public void DynamicRelationalOperators()
+ {
+ JsonValue jv = new JsonObject { { "one", 1 }, { "one_point_two", 1.2 }, { "decimal_one_point_one", 1.1m }, { "trueValue", true }, { "str", "hello" } };
+ dynamic dyn = jv;
+ JsonValue defaultJsonValue = jv.ValueOrDefault(-1);
+
+ Log.Info("Equality");
+ Assert.True(dyn.one == 1);
+ Assert.True(dyn.one_point_two == 1.2);
+ Assert.False(dyn.one == 1.2);
+ Assert.False(dyn.one_point_two == 1);
+ Assert.False(dyn.one == 2);
+ Assert.False(dyn.one_point_two == 1.3);
+ Assert.True(dyn.one == 1m);
+ Assert.False(dyn.one == 2m);
+ Assert.True(dyn.decimal_one_point_one == 1.1m);
+
+ Assert.True(dyn.NotHere == null);
+ Assert.True(dyn.NotHere == dyn.NotHere);
+ Assert.True(dyn.NotHere == defaultJsonValue);
+ // DISABLED, 197375, Assert.False(dyn.NotHere == 1);
+ Assert.False(dyn.NotHere == jv);
+
+ Log.Info("Inequality");
+ Assert.False(dyn.one != 1);
+ Assert.False(dyn.one_point_two != 1.2);
+ Assert.True(dyn.one != 1.2);
+ Assert.True(dyn.one_point_two != 1);
+ Assert.True(dyn.one != 2);
+ Assert.True(dyn.one_point_two != 1.3);
+ Assert.False(dyn.one != 1m);
+ Assert.True(dyn.one != 2m);
+
+ Assert.False(dyn.NotHere != null);
+ Assert.False(dyn.NotHere != dyn.NotHere);
+ Assert.False(dyn.NotHere != defaultJsonValue);
+ // DISABLED, 197375, Assert.True(dyn.NotHere != 1);
+ Assert.True(dyn.NotHere != jv);
+
+ Log.Info("Less than");
+ Assert.True(dyn.one < 2);
+ Assert.False(dyn.one < 1);
+ Assert.False(dyn.one < 0);
+ Assert.True(dyn.one_point_two < 1.3);
+ Assert.False(dyn.one_point_two < 1.2);
+ Assert.False(dyn.one_point_two < 1.1);
+
+ Assert.True(dyn.one < 1.1);
+ Assert.Equal(1 < 1.0, dyn.one < 1.0);
+ Assert.False(dyn.one < 0.9);
+ Assert.True(dyn.one_point_two < 2);
+ Assert.False(dyn.one_point_two < 1);
+ Assert.Equal(1.2 < 1.2f, dyn.one_point_two < 1.2f);
+
+ Log.Info("Greater than");
+ Assert.False(dyn.one > 2);
+ Assert.False(dyn.one > 1);
+ Assert.True(dyn.one > 0);
+ Assert.False(dyn.one_point_two > 1.3);
+ Assert.False(dyn.one_point_two > 1.2);
+ Assert.True(dyn.one_point_two > 1.1);
+
+ Assert.False(dyn.one > 1.1);
+ Assert.Equal(1 > 1.0, dyn.one > 1.0);
+ Assert.True(dyn.one > 0.9);
+ Assert.False(dyn.one_point_two > 2);
+ Assert.True(dyn.one_point_two > 1);
+ Assert.Equal(1.2 > 1.2f, dyn.one_point_two > 1.2f);
+
+ Log.Info("Less than or equals");
+ Assert.True(dyn.one <= 2);
+ Assert.True(dyn.one <= 1);
+ Assert.False(dyn.one <= 0);
+ Assert.True(dyn.one_point_two <= 1.3);
+ Assert.True(dyn.one_point_two <= 1.2);
+ Assert.False(dyn.one_point_two <= 1.1);
+
+ Assert.True(dyn.one <= 1.1);
+ Assert.Equal(1 <= 1.0, dyn.one <= 1.0);
+ Assert.False(dyn.one <= 0.9);
+ Assert.True(dyn.one_point_two <= 2);
+ Assert.False(dyn.one_point_two <= 1);
+ Assert.Equal(1.2 <= 1.2f, dyn.one_point_two <= 1.2f);
+
+ Log.Info("Greater than or equals");
+ Assert.False(dyn.one >= 2);
+ Assert.True(dyn.one >= 1);
+ Assert.True(dyn.one >= 0);
+ Assert.False(dyn.one_point_two >= 1.3);
+ Assert.True(dyn.one_point_two >= 1.2);
+ Assert.True(dyn.one_point_two >= 1.1);
+
+ Assert.False(dyn.one >= 1.1);
+ Assert.Equal(1 >= 1.0, dyn.one >= 1.0);
+ Assert.True(dyn.one >= 0.9);
+ Assert.False(dyn.one_point_two >= 2);
+ Assert.True(dyn.one_point_two >= 1);
+ Assert.Equal(1.2 >= 1.2f, dyn.one_point_two >= 1.2f);
+
+ Log.Info("Invalid number conversions");
+ JsonValueTests.ExpectException<InvalidOperationException>(() => { Log.Info(dyn.decimal_one_point_one == 1.1); });
+ JsonValueTests.ExpectException<InvalidOperationException>(() => { Log.Info(dyn.one != (uint)2); });
+
+ Log.Info("Invalid data types for relational operators");
+ JsonValueTests.ExpectException<InvalidOperationException>(() => { Log.Info(dyn.trueValue >= dyn.trueValue); });
+ JsonValueTests.ExpectException<InvalidOperationException>(() => { Log.Info(dyn.NotHere < dyn.NotHere); });
+ JsonValueTests.ExpectException<InvalidOperationException>(() => { Log.Info(dyn.str < "Jello"); });
+
+ // DISABLED, 197315
+ Log.Info("Conversions from string");
+ jv = new JsonObject { { "one", "1" }, { "twelve_point_two", "1.22e1" } };
+ dyn = jv;
+ Assert.True(dyn.one == 1);
+ Assert.True(dyn.twelve_point_two == 1.22e1);
+ Assert.True(dyn.one >= 0.5f);
+ Assert.True(dyn.twelve_point_two <= 13);
+ Assert.True(dyn.one < 2);
+ Assert.Equal(dyn.twelve_point_two.ReadAs<int>() > 12, dyn.twelve_point_two > 12);
+ }
+
+ /// <summary>
+ /// Tests for using arithmetic operators in dynamic references to <see cref="JsonValue"/> instances.
+ /// </summary>
+ [Fact(Skip = "Ignore")]
+ public void ArithmeticOperators()
+ {
+ int seed = MethodBase.GetCurrentMethod().Name.GetHashCode();
+ Log.Info("Seed: {0}", seed);
+ Random rndGen = new Random(seed);
+ int i1 = rndGen.Next(-10000, 10000);
+ int i2 = rndGen.Next(-10000, 10000);
+ JsonValue jv1 = i1;
+ JsonValue jv2 = i2;
+ Log.Info("jv1 = {0}, jv2 = {1}", jv1, jv2);
+ dynamic dyn1 = jv1;
+ dynamic dyn2 = jv2;
+
+ string str1 = i1.ToString(CultureInfo.InvariantCulture);
+ string str2 = i2.ToString(CultureInfo.InvariantCulture);
+ JsonValue jvstr1 = str1;
+ JsonValue jvstr2 = str2;
+
+ Log.Info("Unary +");
+ Assert.Equal<int>(+i1, +dyn1);
+ Assert.Equal<int>(+i2, +dyn2);
+
+ Log.Info("Unary -");
+ Assert.Equal<int>(-i1, -dyn1);
+ Assert.Equal<int>(-i2, -dyn2);
+
+ Log.Info("Unary ~ (bitwise NOT)");
+ Assert.Equal<int>(~i1, ~dyn1);
+ Assert.Equal<int>(~i2, ~dyn2);
+
+ Log.Info("Binary +: {0}", i1 + i2);
+ Assert.Equal<int>(i1 + i2, dyn1 + dyn2);
+ Assert.Equal<int>(i1 + i2, dyn2 + dyn1);
+ Assert.Equal<int>(i1 + i2, dyn1 + i2);
+ Assert.Equal<int>(i1 + i2, dyn2 + i1);
+
+ // DISABLED, 197394
+ // Assert.Equal<int>(i1 + i2, dyn1 + str2);
+ // Assert.Equal<int>(i1 + i2, dyn1 + jvstr2);
+
+ Log.Info("Binary -: {0}, {1}", i1 - i2, i2 - i1);
+ Assert.Equal<int>(i1 - i2, dyn1 - dyn2);
+ Assert.Equal<int>(i2 - i1, dyn2 - dyn1);
+ Assert.Equal<int>(i1 - i2, dyn1 - i2);
+ Assert.Equal<int>(i2 - i1, dyn2 - i1);
+
+ Log.Info("Binary *: {0}", i1 * i2);
+ Assert.Equal<int>(i1 * i2, dyn1 * dyn2);
+ Assert.Equal<int>(i1 * i2, dyn2 * dyn1);
+ Assert.Equal<int>(i1 * i2, dyn1 * i2);
+ Assert.Equal<int>(i1 * i2, dyn2 * i1);
+
+ while (i1 == 0)
+ {
+ i1 = rndGen.Next(-10000, 10000);
+ jv1 = i1;
+ dyn1 = jv1;
+ Log.Info("Using new (non-zero) i1 value: {0}", i1);
+ }
+
+ while (i2 == 0)
+ {
+ i2 = rndGen.Next(-10000, 10000);
+ jv2 = i2;
+ dyn2 = jv2;
+ Log.Info("Using new (non-zero) i2 value: {0}", i2);
+ }
+
+ Log.Info("Binary / (integer division): {0}, {1}", i1 / i2, i2 / i1);
+ Assert.Equal<int>(i1 / i2, dyn1 / dyn2);
+ Assert.Equal<int>(i2 / i1, dyn2 / dyn1);
+ Assert.Equal<int>(i1 / i2, dyn1 / i2);
+ Assert.Equal<int>(i2 / i1, dyn2 / i1);
+
+ Log.Info("Binary % (modulo): {0}, {1}", i1 % i2, i2 % i1);
+ Assert.Equal<int>(i1 % i2, dyn1 % dyn2);
+ Assert.Equal<int>(i2 % i1, dyn2 % dyn1);
+ Assert.Equal<int>(i1 % i2, dyn1 % i2);
+ Assert.Equal<int>(i2 % i1, dyn2 % i1);
+
+ Log.Info("Binary & (bitwise AND): {0}", i1 & i2);
+ Assert.Equal<int>(i1 & i2, dyn1 & dyn2);
+ Assert.Equal<int>(i1 & i2, dyn2 & dyn1);
+ Assert.Equal<int>(i1 & i2, dyn1 & i2);
+ Assert.Equal<int>(i1 & i2, dyn2 & i1);
+
+ Log.Info("Binary | (bitwise OR): {0}", i1 | i2);
+ Assert.Equal<int>(i1 | i2, dyn1 | dyn2);
+ Assert.Equal<int>(i1 | i2, dyn2 | dyn1);
+ Assert.Equal<int>(i1 | i2, dyn1 | i2);
+ Assert.Equal<int>(i1 | i2, dyn2 | i1);
+
+ Log.Info("Binary ^ (bitwise XOR): {0}", i1 ^ i2);
+ Assert.Equal<int>(i1 ^ i2, dyn1 ^ dyn2);
+ Assert.Equal<int>(i1 ^ i2, dyn2 ^ dyn1);
+ Assert.Equal<int>(i1 ^ i2, dyn1 ^ i2);
+ Assert.Equal<int>(i1 ^ i2, dyn2 ^ i1);
+
+ i1 = rndGen.Next(1, 10);
+ i2 = rndGen.Next(1, 10);
+ jv1 = i1;
+ jv2 = i2;
+ dyn1 = jv1;
+ dyn2 = jv2;
+ Log.Info("New i1, i2: {0}, {1}", i1, i2);
+
+ Log.Info("Left shift: {0}", i1 << i2);
+ Assert.Equal<int>(i1 << i2, dyn1 << dyn2);
+ Assert.Equal<int>(i1 << i2, dyn1 << i2);
+
+ i1 = i1 << i2;
+ jv1 = i1;
+ dyn1 = jv1;
+ Log.Info("New i1: {0}", i1);
+ Log.Info("Right shift: {0}", i1 >> i2);
+ Assert.Equal<int>(i1 >> i2, dyn1 >> dyn2);
+ Assert.Equal<int>(i1 >> i2, dyn1 >> i2);
+
+ i2 += 4;
+ jv2 = i2;
+ dyn2 = jv2;
+ Log.Info("New i2: {0}", i2);
+ Log.Info("Right shift: {0}", i1 >> i2);
+ Assert.Equal<int>(i1 >> i2, dyn1 >> dyn2);
+ Assert.Equal<int>(i1 >> i2, dyn1 >> i2);
+ }
+
+ /// <summary>
+ /// Tests for conversions between data types in arithmetic operations.
+ /// </summary>
+ [Fact(Skip = "Ignore")]
+ public void ArithmeticConversion()
+ {
+ JsonObject jo = new JsonObject
+ {
+ { "byteVal", (byte)10 },
+ { "sbyteVal", (sbyte)10 },
+ { "shortVal", (short)10 },
+ { "ushortVal", (ushort)10 },
+ { "intVal", 10 },
+ { "uintVal", (uint)10 },
+ { "longVal", 10L },
+ { "ulongVal", (ulong)10 },
+ { "charVal", (char)10 },
+ { "decimalVal", 10m },
+ { "doubleVal", 10.0 },
+ { "floatVal", 10f },
+ };
+ dynamic dyn = jo;
+
+ Log.Info("Conversion from byte");
+ // DISABLED, 197387, ValidateResult<int>(dyn.byteVal + (byte)10, 20);
+ JsonValueTests.ExpectException<InvalidOperationException>(() => Log.Info("{0}", dyn.byteVal + (sbyte)10));
+ ValidateResult<short>(dyn.byteVal + (short)10, 20);
+ ValidateResult<ushort>(dyn.byteVal + (ushort)10, 20);
+ ValidateResult<int>(dyn.byteVal + (int)10, 20);
+ ValidateResult<uint>(dyn.byteVal + (uint)10, 20);
+ ValidateResult<long>(dyn.byteVal + 10L, 20);
+ ValidateResult<ulong>(dyn.byteVal + (ulong)10, 20);
+ ValidateResult<decimal>(dyn.byteVal + 10m, 20);
+ ValidateResult<float>(dyn.byteVal + 10f, 20);
+ ValidateResult<double>(dyn.byteVal + 10.0, 20);
+
+ Log.Info("Conversion from sbyte");
+ JsonValueTests.ExpectException<InvalidOperationException>(() => Log.Info("{0}", dyn.sbyteVal + (byte)10));
+ // DISABLED, 197387, ValidateResult<int>(dyn.sbyteVal + (sbyte)10, 20);
+ ValidateResult<short>(dyn.sbyteVal + (short)10, 20);
+ JsonValueTests.ExpectException<InvalidOperationException>(() => Log.Info("{0}", dyn.sbyteVal + (ushort)10));
+ ValidateResult<int>(dyn.sbyteVal + (int)10, 20);
+ JsonValueTests.ExpectException<InvalidOperationException>(() => Log.Info("{0}", dyn.sbyteVal + (uint)10));
+ ValidateResult<long>(dyn.sbyteVal + 10L, 20);
+ JsonValueTests.ExpectException<InvalidOperationException>(() => Log.Info("{0}", dyn.sbyteVal + (ulong)10));
+ ValidateResult<decimal>(dyn.sbyteVal + 10m, 20);
+ ValidateResult<float>(dyn.sbyteVal + 10f, 20);
+ ValidateResult<double>(dyn.sbyteVal + 10.0, 20);
+
+ Log.Info("Conversion from short");
+ ValidateResult<short>(dyn.shortVal + (byte)10, 20);
+ ValidateResult<short>(dyn.shortVal + (sbyte)10, 20);
+ ValidateResult<short>(dyn.shortVal + (short)10, 20);
+ JsonValueTests.ExpectException<InvalidOperationException>(() => Log.Info("{0}", dyn.shortVal + (ushort)10));
+ ValidateResult<int>(dyn.shortVal + (int)10, 20);
+ JsonValueTests.ExpectException<InvalidOperationException>(() => Log.Info("{0}", dyn.shortVal + (uint)10));
+ ValidateResult<long>(dyn.shortVal + 10L, 20);
+ JsonValueTests.ExpectException<InvalidOperationException>(() => Log.Info("{0}", dyn.shortVal + (ulong)10));
+ ValidateResult<decimal>(dyn.shortVal + 10m, 20);
+ ValidateResult<float>(dyn.shortVal + 10f, 20);
+ ValidateResult<double>(dyn.shortVal + 10.0, 20);
+
+ Log.Info("Conversion from ushort");
+ ValidateResult<ushort>(dyn.ushortVal + (byte)10, 20);
+ JsonValueTests.ExpectException<InvalidOperationException>(() => Log.Info("{0}", dyn.ushortVal + (sbyte)10));
+ JsonValueTests.ExpectException<InvalidOperationException>(() => Log.Info("{0}", dyn.ushortVal + (short)10));
+ ValidateResult<ushort>(dyn.ushortVal + (ushort)10, 20);
+ ValidateResult<int>(dyn.ushortVal + (int)10, 20);
+ ValidateResult<uint>(dyn.ushortVal + (uint)10, 20);
+ ValidateResult<long>(dyn.ushortVal + 10L, 20);
+ ValidateResult<ulong>(dyn.ushortVal + (ulong)10, 20);
+ ValidateResult<decimal>(dyn.ushortVal + 10m, 20);
+ ValidateResult<float>(dyn.ushortVal + 10f, 20);
+ ValidateResult<double>(dyn.ushortVal + 10.0, 20);
+
+ Log.Info("Conversion from int");
+ ValidateResult<int>(dyn.intVal + (byte)10, 20);
+ ValidateResult<int>(dyn.intVal + (sbyte)10, 20);
+ ValidateResult<int>(dyn.intVal + (short)10, 20);
+ ValidateResult<int>(dyn.intVal + (ushort)10, 20);
+ ValidateResult<int>(dyn.intVal + (int)10, 20);
+ JsonValueTests.ExpectException<InvalidOperationException>(() => Log.Info("{0}", dyn.intVal + (uint)10));
+ ValidateResult<long>(dyn.intVal + 10L, 20);
+ JsonValueTests.ExpectException<InvalidOperationException>(() => Log.Info("{0}", dyn.intVal + (ulong)10));
+ ValidateResult<decimal>(dyn.intVal + 10m, 20);
+ ValidateResult<float>(dyn.intVal + 10f, 20);
+ ValidateResult<double>(dyn.intVal + 10.0, 20);
+
+ Log.Info("Conversion from uint");
+ ValidateResult<uint>(dyn.uintVal + (byte)10, 20);
+ JsonValueTests.ExpectException<InvalidOperationException>(() => Log.Info("{0}", dyn.uintVal + (sbyte)10));
+ JsonValueTests.ExpectException<InvalidOperationException>(() => Log.Info("{0}", dyn.uintVal + (short)10));
+ ValidateResult<uint>(dyn.uintVal + (ushort)10, 20);
+ JsonValueTests.ExpectException<InvalidOperationException>(() => Log.Info("{0}", dyn.uintVal + (int)10));
+ ValidateResult<uint>(dyn.uintVal + (uint)10, 20);
+ ValidateResult<long>(dyn.uintVal + 10L, 20);
+ ValidateResult<ulong>(dyn.uintVal + (ulong)10, 20);
+ ValidateResult<decimal>(dyn.uintVal + 10m, 20);
+ ValidateResult<float>(dyn.uintVal + 10f, 20);
+ ValidateResult<double>(dyn.uintVal + 10.0, 20);
+
+ Log.Info("Conversion from long");
+ ValidateResult<long>(dyn.longVal + (byte)10, 20);
+ ValidateResult<long>(dyn.longVal + (sbyte)10, 20);
+ ValidateResult<long>(dyn.longVal + (short)10, 20);
+ ValidateResult<long>(dyn.longVal + (ushort)10, 20);
+ ValidateResult<long>(dyn.longVal + (int)10, 20);
+ ValidateResult<long>(dyn.longVal + (uint)10, 20);
+ ValidateResult<long>(dyn.longVal + 10L, 20);
+ JsonValueTests.ExpectException<InvalidOperationException>(() => Log.Info("{0}", dyn.longVal + (ulong)10));
+ ValidateResult<decimal>(dyn.longVal + 10m, 20);
+ ValidateResult<float>(dyn.longVal + 10f, 20);
+ ValidateResult<double>(dyn.longVal + 10.0, 20);
+
+ Log.Info("Conversion from ulong");
+ ValidateResult<ulong>(dyn.ulongVal + (byte)10, 20);
+ JsonValueTests.ExpectException<InvalidOperationException>(() => Log.Info("{0}", dyn.ulongVal + (sbyte)10));
+ JsonValueTests.ExpectException<InvalidOperationException>(() => Log.Info("{0}", dyn.ulongVal + (short)10));
+ ValidateResult<ulong>(dyn.ulongVal + (ushort)10, 20);
+ JsonValueTests.ExpectException<InvalidOperationException>(() => Log.Info("{0}", dyn.ulongVal + (int)10));
+ ValidateResult<ulong>(dyn.ulongVal + (uint)10, 20);
+ JsonValueTests.ExpectException<InvalidOperationException>(() => Log.Info("{0}", dyn.ulongVal + (long)10));
+ ValidateResult<ulong>(dyn.ulongVal + (ulong)10, 20);
+ ValidateResult<decimal>(dyn.ulongVal + 10m, 20);
+ ValidateResult<float>(dyn.ulongVal + 10f, 20);
+ ValidateResult<double>(dyn.ulongVal + 10.0, 20);
+
+ Log.Info("Conversion from float");
+ ValidateResult<float>(dyn.floatVal + (byte)10, 20);
+ ValidateResult<float>(dyn.floatVal + (sbyte)10, 20);
+ ValidateResult<float>(dyn.floatVal + (short)10, 20);
+ ValidateResult<float>(dyn.floatVal + (ushort)10, 20);
+ ValidateResult<float>(dyn.floatVal + (int)10, 20);
+ ValidateResult<float>(dyn.floatVal + (uint)10, 20);
+ ValidateResult<float>(dyn.floatVal + 10L, 20);
+ ValidateResult<float>(dyn.floatVal + (ulong)10, 20);
+ JsonValueTests.ExpectException<InvalidOperationException>(() => Log.Info("{0}", dyn.floatVal + 10m));
+ ValidateResult<float>(dyn.floatVal + 10f, 20);
+ ValidateResult<double>(dyn.floatVal + 10.0, 20);
+
+ Log.Info("Conversion from double");
+ ValidateResult<double>(dyn.doubleVal + (byte)10, 20);
+ ValidateResult<double>(dyn.doubleVal + (sbyte)10, 20);
+ ValidateResult<double>(dyn.doubleVal + (short)10, 20);
+ ValidateResult<double>(dyn.doubleVal + (ushort)10, 20);
+ ValidateResult<double>(dyn.doubleVal + (int)10, 20);
+ ValidateResult<double>(dyn.doubleVal + (uint)10, 20);
+ ValidateResult<double>(dyn.doubleVal + 10L, 20);
+ ValidateResult<double>(dyn.doubleVal + (ulong)10, 20);
+ JsonValueTests.ExpectException<InvalidOperationException>(() => Log.Info("{0}", dyn.doubleVal + 10m));
+ ValidateResult<double>(dyn.doubleVal + 10f, 20);
+ ValidateResult<double>(dyn.doubleVal + 10.0, 20);
+
+ Log.Info("Conversion from decimal");
+ ValidateResult<decimal>(dyn.decimalVal + (byte)10, 20);
+ ValidateResult<decimal>(dyn.decimalVal + (sbyte)10, 20);
+ ValidateResult<decimal>(dyn.decimalVal + (short)10, 20);
+ ValidateResult<decimal>(dyn.decimalVal + (ushort)10, 20);
+ ValidateResult<decimal>(dyn.decimalVal + (int)10, 20);
+ ValidateResult<decimal>(dyn.decimalVal + (uint)10, 20);
+ ValidateResult<decimal>(dyn.decimalVal + 10L, 20);
+ ValidateResult<decimal>(dyn.decimalVal + (ulong)10, 20);
+ ValidateResult<decimal>(dyn.decimalVal + 10m, 20);
+ JsonValueTests.ExpectException<InvalidOperationException>(() => Log.Info("{0}", dyn.decimalVal + 10f));
+ JsonValueTests.ExpectException<InvalidOperationException>(() => Log.Info("{0}", dyn.decimalVal + 10.0));
+ }
+
+ /// <summary>
+ /// Tests for implicit casts between dynamic references to <see cref="JsonPrimitive"/> instances
+ /// and the supported CLR types.
+ /// </summary>
+ [Fact]
+ public void ImplicitPrimitiveCastTests()
+ {
+ DateTime now = DateTime.Now;
+ int seed = now.Year * 10000 + now.Month * 100 + now.Day;
+ Log.Info("Seed: {0}", seed);
+ Random rndGen = new Random(seed);
+ int intValue = rndGen.Next(1, 127);
+ Log.Info("Value: {0}", intValue);
+
+ uint uintValue = (uint)intValue;
+ short shortValue = (short)intValue;
+ ushort ushortValue = (ushort)intValue;
+ long longValue = (long)intValue;
+ ulong ulongValue = (ulong)intValue;
+ byte byteValue = (byte)intValue;
+ sbyte sbyteValue = (sbyte)intValue;
+ float floatValue = (float)intValue;
+ double doubleValue = (double)intValue;
+ decimal decimalValue = (decimal)intValue;
+ string stringValue = intValue.ToString(CultureInfo.InvariantCulture);
+
+ dynamic dyn = new JsonObject
+ {
+ { "Byte", byteValue },
+ { "SByte", sbyteValue },
+ { "Int16", shortValue },
+ { "UInt16", ushortValue },
+ { "Int32", intValue },
+ { "UInt32", uintValue },
+ { "Int64", longValue },
+ { "UInt64", ulongValue },
+ { "Double", doubleValue },
+ { "Single", floatValue },
+ { "Decimal", decimalValue },
+ { "String", stringValue },
+ { "True", "true" },
+ { "False", "false" },
+ };
+
+ Log.Info("dyn: {0}", dyn);
+
+ Log.Info("Casts to Byte");
+
+ byte byteFromByte = dyn.Byte;
+ byte byteFromSByte = dyn.SByte;
+ byte byteFromShort = dyn.Int16;
+ byte byteFromUShort = dyn.UInt16;
+ byte byteFromInt = dyn.Int32;
+ byte byteFromUInt = dyn.UInt32;
+ byte byteFromLong = dyn.Int64;
+ byte byteFromULong = dyn.UInt64;
+ byte byteFromDouble = dyn.Double;
+ byte byteFromFloat = dyn.Single;
+ byte byteFromDecimal = dyn.Decimal;
+ byte byteFromString = dyn.String;
+
+ Assert.Equal<byte>(byteValue, byteFromByte);
+ Assert.Equal<byte>(byteValue, byteFromSByte);
+ Assert.Equal<byte>(byteValue, byteFromShort);
+ Assert.Equal<byte>(byteValue, byteFromUShort);
+ Assert.Equal<byte>(byteValue, byteFromInt);
+ Assert.Equal<byte>(byteValue, byteFromUInt);
+ Assert.Equal<byte>(byteValue, byteFromLong);
+ Assert.Equal<byte>(byteValue, byteFromULong);
+ Assert.Equal<byte>(byteValue, byteFromDouble);
+ Assert.Equal<byte>(byteValue, byteFromFloat);
+ Assert.Equal<byte>(byteValue, byteFromDecimal);
+ Assert.Equal<byte>(byteValue, byteFromString);
+
+ Log.Info("Casts to SByte");
+
+ sbyte sbyteFromByte = dyn.Byte;
+ sbyte sbyteFromSByte = dyn.SByte;
+ sbyte sbyteFromShort = dyn.Int16;
+ sbyte sbyteFromUShort = dyn.UInt16;
+ sbyte sbyteFromInt = dyn.Int32;
+ sbyte sbyteFromUInt = dyn.UInt32;
+ sbyte sbyteFromLong = dyn.Int64;
+ sbyte sbyteFromULong = dyn.UInt64;
+ sbyte sbyteFromDouble = dyn.Double;
+ sbyte sbyteFromFloat = dyn.Single;
+ sbyte sbyteFromDecimal = dyn.Decimal;
+ sbyte sbyteFromString = dyn.String;
+
+ Assert.Equal<sbyte>(sbyteValue, sbyteFromByte);
+ Assert.Equal<sbyte>(sbyteValue, sbyteFromSByte);
+ Assert.Equal<sbyte>(sbyteValue, sbyteFromShort);
+ Assert.Equal<sbyte>(sbyteValue, sbyteFromUShort);
+ Assert.Equal<sbyte>(sbyteValue, sbyteFromInt);
+ Assert.Equal<sbyte>(sbyteValue, sbyteFromUInt);
+ Assert.Equal<sbyte>(sbyteValue, sbyteFromLong);
+ Assert.Equal<sbyte>(sbyteValue, sbyteFromULong);
+ Assert.Equal<sbyte>(sbyteValue, sbyteFromDouble);
+ Assert.Equal<sbyte>(sbyteValue, sbyteFromFloat);
+ Assert.Equal<sbyte>(sbyteValue, sbyteFromDecimal);
+ Assert.Equal<sbyte>(sbyteValue, sbyteFromString);
+
+ Log.Info("Casts to Short");
+
+ short shortFromByte = dyn.Byte;
+ short shortFromSByte = dyn.SByte;
+ short shortFromShort = dyn.Int16;
+ short shortFromUShort = dyn.UInt16;
+ short shortFromInt = dyn.Int32;
+ short shortFromUInt = dyn.UInt32;
+ short shortFromLong = dyn.Int64;
+ short shortFromULong = dyn.UInt64;
+ short shortFromDouble = dyn.Double;
+ short shortFromFloat = dyn.Single;
+ short shortFromDecimal = dyn.Decimal;
+ short shortFromString = dyn.String;
+
+ Assert.Equal<short>(shortValue, shortFromByte);
+ Assert.Equal<short>(shortValue, shortFromSByte);
+ Assert.Equal<short>(shortValue, shortFromShort);
+ Assert.Equal<short>(shortValue, shortFromUShort);
+ Assert.Equal<short>(shortValue, shortFromInt);
+ Assert.Equal<short>(shortValue, shortFromUInt);
+ Assert.Equal<short>(shortValue, shortFromLong);
+ Assert.Equal<short>(shortValue, shortFromULong);
+ Assert.Equal<short>(shortValue, shortFromDouble);
+ Assert.Equal<short>(shortValue, shortFromFloat);
+ Assert.Equal<short>(shortValue, shortFromDecimal);
+ Assert.Equal<short>(shortValue, shortFromString);
+
+ Log.Info("Casts to UShort");
+
+ ushort ushortFromByte = dyn.Byte;
+ ushort ushortFromSByte = dyn.SByte;
+ ushort ushortFromShort = dyn.Int16;
+ ushort ushortFromUShort = dyn.UInt16;
+ ushort ushortFromInt = dyn.Int32;
+ ushort ushortFromUInt = dyn.UInt32;
+ ushort ushortFromLong = dyn.Int64;
+ ushort ushortFromULong = dyn.UInt64;
+ ushort ushortFromDouble = dyn.Double;
+ ushort ushortFromFloat = dyn.Single;
+ ushort ushortFromDecimal = dyn.Decimal;
+ ushort ushortFromString = dyn.String;
+
+ Assert.Equal<ushort>(ushortValue, ushortFromByte);
+ Assert.Equal<ushort>(ushortValue, ushortFromSByte);
+ Assert.Equal<ushort>(ushortValue, ushortFromShort);
+ Assert.Equal<ushort>(ushortValue, ushortFromUShort);
+ Assert.Equal<ushort>(ushortValue, ushortFromInt);
+ Assert.Equal<ushort>(ushortValue, ushortFromUInt);
+ Assert.Equal<ushort>(ushortValue, ushortFromLong);
+ Assert.Equal<ushort>(ushortValue, ushortFromULong);
+ Assert.Equal<ushort>(ushortValue, ushortFromDouble);
+ Assert.Equal<ushort>(ushortValue, ushortFromFloat);
+ Assert.Equal<ushort>(ushortValue, ushortFromDecimal);
+ Assert.Equal<ushort>(ushortValue, ushortFromString);
+
+ Log.Info("Casts to Int");
+
+ int intFromByte = dyn.Byte;
+ int intFromSByte = dyn.SByte;
+ int intFromShort = dyn.Int16;
+ int intFromUShort = dyn.UInt16;
+ int intFromInt = dyn.Int32;
+ int intFromUInt = dyn.UInt32;
+ int intFromLong = dyn.Int64;
+ int intFromULong = dyn.UInt64;
+ int intFromDouble = dyn.Double;
+ int intFromFloat = dyn.Single;
+ int intFromDecimal = dyn.Decimal;
+ int intFromString = dyn.String;
+
+ Assert.Equal<int>(intValue, intFromByte);
+ Assert.Equal<int>(intValue, intFromSByte);
+ Assert.Equal<int>(intValue, intFromShort);
+ Assert.Equal<int>(intValue, intFromUShort);
+ Assert.Equal<int>(intValue, intFromInt);
+ Assert.Equal<int>(intValue, intFromUInt);
+ Assert.Equal<int>(intValue, intFromLong);
+ Assert.Equal<int>(intValue, intFromULong);
+ Assert.Equal<int>(intValue, intFromDouble);
+ Assert.Equal<int>(intValue, intFromFloat);
+ Assert.Equal<int>(intValue, intFromDecimal);
+ Assert.Equal<int>(intValue, intFromString);
+
+ Log.Info("Casts to UInt");
+
+ uint uintFromByte = dyn.Byte;
+ uint uintFromSByte = dyn.SByte;
+ uint uintFromShort = dyn.Int16;
+ uint uintFromUShort = dyn.UInt16;
+ uint uintFromInt = dyn.Int32;
+ uint uintFromUInt = dyn.UInt32;
+ uint uintFromLong = dyn.Int64;
+ uint uintFromULong = dyn.UInt64;
+ uint uintFromDouble = dyn.Double;
+ uint uintFromFloat = dyn.Single;
+ uint uintFromDecimal = dyn.Decimal;
+ uint uintFromString = dyn.String;
+
+ Assert.Equal<uint>(uintValue, uintFromByte);
+ Assert.Equal<uint>(uintValue, uintFromSByte);
+ Assert.Equal<uint>(uintValue, uintFromShort);
+ Assert.Equal<uint>(uintValue, uintFromUShort);
+ Assert.Equal<uint>(uintValue, uintFromInt);
+ Assert.Equal<uint>(uintValue, uintFromUInt);
+ Assert.Equal<uint>(uintValue, uintFromLong);
+ Assert.Equal<uint>(uintValue, uintFromULong);
+ Assert.Equal<uint>(uintValue, uintFromDouble);
+ Assert.Equal<uint>(uintValue, uintFromFloat);
+ Assert.Equal<uint>(uintValue, uintFromDecimal);
+ Assert.Equal<uint>(uintValue, uintFromString);
+
+ Log.Info("Casts to Long");
+
+ long longFromByte = dyn.Byte;
+ long longFromSByte = dyn.SByte;
+ long longFromShort = dyn.Int16;
+ long longFromUShort = dyn.UInt16;
+ long longFromInt = dyn.Int32;
+ long longFromUInt = dyn.UInt32;
+ long longFromLong = dyn.Int64;
+ long longFromULong = dyn.UInt64;
+ long longFromDouble = dyn.Double;
+ long longFromFloat = dyn.Single;
+ long longFromDecimal = dyn.Decimal;
+ long longFromString = dyn.String;
+
+ Assert.Equal<long>(longValue, longFromByte);
+ Assert.Equal<long>(longValue, longFromSByte);
+ Assert.Equal<long>(longValue, longFromShort);
+ Assert.Equal<long>(longValue, longFromUShort);
+ Assert.Equal<long>(longValue, longFromInt);
+ Assert.Equal<long>(longValue, longFromUInt);
+ Assert.Equal<long>(longValue, longFromLong);
+ Assert.Equal<long>(longValue, longFromULong);
+ Assert.Equal<long>(longValue, longFromDouble);
+ Assert.Equal<long>(longValue, longFromFloat);
+ Assert.Equal<long>(longValue, longFromDecimal);
+ Assert.Equal<long>(longValue, longFromString);
+
+ Log.Info("Casts to ULong");
+
+ ulong ulongFromByte = dyn.Byte;
+ ulong ulongFromSByte = dyn.SByte;
+ ulong ulongFromShort = dyn.Int16;
+ ulong ulongFromUShort = dyn.UInt16;
+ ulong ulongFromInt = dyn.Int32;
+ ulong ulongFromUInt = dyn.UInt32;
+ ulong ulongFromLong = dyn.Int64;
+ ulong ulongFromULong = dyn.UInt64;
+ ulong ulongFromDouble = dyn.Double;
+ ulong ulongFromFloat = dyn.Single;
+ ulong ulongFromDecimal = dyn.Decimal;
+ ulong ulongFromString = dyn.String;
+
+ Assert.Equal<ulong>(ulongValue, ulongFromByte);
+ Assert.Equal<ulong>(ulongValue, ulongFromSByte);
+ Assert.Equal<ulong>(ulongValue, ulongFromShort);
+ Assert.Equal<ulong>(ulongValue, ulongFromUShort);
+ Assert.Equal<ulong>(ulongValue, ulongFromInt);
+ Assert.Equal<ulong>(ulongValue, ulongFromUInt);
+ Assert.Equal<ulong>(ulongValue, ulongFromLong);
+ Assert.Equal<ulong>(ulongValue, ulongFromULong);
+ Assert.Equal<ulong>(ulongValue, ulongFromDouble);
+ Assert.Equal<ulong>(ulongValue, ulongFromFloat);
+ Assert.Equal<ulong>(ulongValue, ulongFromDecimal);
+ Assert.Equal<ulong>(ulongValue, ulongFromString);
+
+ Log.Info("Casts to Float");
+
+ float floatFromByte = dyn.Byte;
+ float floatFromSByte = dyn.SByte;
+ float floatFromShort = dyn.Int16;
+ float floatFromUShort = dyn.UInt16;
+ float floatFromInt = dyn.Int32;
+ float floatFromUInt = dyn.UInt32;
+ float floatFromLong = dyn.Int64;
+ float floatFromULong = dyn.UInt64;
+ float floatFromDouble = dyn.Double;
+ float floatFromFloat = dyn.Single;
+ float floatFromDecimal = dyn.Decimal;
+ float floatFromString = dyn.String;
+
+ Assert.Equal<float>(floatValue, floatFromByte);
+ Assert.Equal<float>(floatValue, floatFromSByte);
+ Assert.Equal<float>(floatValue, floatFromShort);
+ Assert.Equal<float>(floatValue, floatFromUShort);
+ Assert.Equal<float>(floatValue, floatFromInt);
+ Assert.Equal<float>(floatValue, floatFromUInt);
+ Assert.Equal<float>(floatValue, floatFromLong);
+ Assert.Equal<float>(floatValue, floatFromULong);
+ Assert.Equal<float>(floatValue, floatFromDouble);
+ Assert.Equal<float>(floatValue, floatFromFloat);
+ Assert.Equal<float>(floatValue, floatFromDecimal);
+ Assert.Equal<float>(floatValue, floatFromString);
+
+ Log.Info("Casts to Double");
+
+ double doubleFromByte = dyn.Byte;
+ double doubleFromSByte = dyn.SByte;
+ double doubleFromShort = dyn.Int16;
+ double doubleFromUShort = dyn.UInt16;
+ double doubleFromInt = dyn.Int32;
+ double doubleFromUInt = dyn.UInt32;
+ double doubleFromLong = dyn.Int64;
+ double doubleFromULong = dyn.UInt64;
+ double doubleFromDouble = dyn.Double;
+ double doubleFromFloat = dyn.Single;
+ double doubleFromDecimal = dyn.Decimal;
+ double doubleFromString = dyn.String;
+
+ Assert.Equal<double>(doubleValue, doubleFromByte);
+ Assert.Equal<double>(doubleValue, doubleFromSByte);
+ Assert.Equal<double>(doubleValue, doubleFromShort);
+ Assert.Equal<double>(doubleValue, doubleFromUShort);
+ Assert.Equal<double>(doubleValue, doubleFromInt);
+ Assert.Equal<double>(doubleValue, doubleFromUInt);
+ Assert.Equal<double>(doubleValue, doubleFromLong);
+ Assert.Equal<double>(doubleValue, doubleFromULong);
+ Assert.Equal<double>(doubleValue, doubleFromDouble);
+ Assert.Equal<double>(doubleValue, doubleFromFloat);
+ Assert.Equal<double>(doubleValue, doubleFromDecimal);
+ Assert.Equal<double>(doubleValue, doubleFromString);
+
+ Log.Info("Casts to Decimal");
+
+ decimal decimalFromByte = dyn.Byte;
+ decimal decimalFromSByte = dyn.SByte;
+ decimal decimalFromShort = dyn.Int16;
+ decimal decimalFromUShort = dyn.UInt16;
+ decimal decimalFromInt = dyn.Int32;
+ decimal decimalFromUInt = dyn.UInt32;
+ decimal decimalFromLong = dyn.Int64;
+ decimal decimalFromULong = dyn.UInt64;
+ decimal decimalFromDouble = dyn.Double;
+ decimal decimalFromFloat = dyn.Single;
+ decimal decimalFromDecimal = dyn.Decimal;
+ decimal decimalFromString = dyn.String;
+
+ Assert.Equal<decimal>(decimalValue, decimalFromByte);
+ Assert.Equal<decimal>(decimalValue, decimalFromSByte);
+ Assert.Equal<decimal>(decimalValue, decimalFromShort);
+ Assert.Equal<decimal>(decimalValue, decimalFromUShort);
+ Assert.Equal<decimal>(decimalValue, decimalFromInt);
+ Assert.Equal<decimal>(decimalValue, decimalFromUInt);
+ Assert.Equal<decimal>(decimalValue, decimalFromLong);
+ Assert.Equal<decimal>(decimalValue, decimalFromULong);
+ Assert.Equal<decimal>(decimalValue, decimalFromDouble);
+ Assert.Equal<decimal>(decimalValue, decimalFromFloat);
+ Assert.Equal<decimal>(decimalValue, decimalFromDecimal);
+ Assert.Equal<decimal>(decimalValue, decimalFromString);
+
+ Log.Info("Casts to String");
+
+ string stringFromByte = dyn.Byte;
+ string stringFromSByte = dyn.SByte;
+ string stringFromShort = dyn.Int16;
+ string stringFromUShort = dyn.UInt16;
+ string stringFromInt = dyn.Int32;
+ string stringFromUInt = dyn.UInt32;
+ string stringFromLong = dyn.Int64;
+ string stringFromULong = dyn.UInt64;
+ string stringFromDouble = dyn.Double;
+ string stringFromFloat = dyn.Single;
+ string stringFromDecimal = dyn.Decimal;
+ string stringFromString = dyn.String;
+
+ Assert.Equal(stringValue, stringFromByte);
+ Assert.Equal(stringValue, stringFromSByte);
+ Assert.Equal(stringValue, stringFromShort);
+ Assert.Equal(stringValue, stringFromUShort);
+ Assert.Equal(stringValue, stringFromInt);
+ Assert.Equal(stringValue, stringFromUInt);
+ Assert.Equal(stringValue, stringFromLong);
+ Assert.Equal(stringValue, stringFromULong);
+ Assert.Equal(stringValue, stringFromDouble);
+ Assert.Equal(stringValue, stringFromFloat);
+ Assert.Equal(stringValue, stringFromDecimal);
+ Assert.Equal(stringValue, stringFromString);
+
+ Log.Info("Casts to Boolean");
+
+ bool bTrue = dyn.True;
+ bool bFalse = dyn.False;
+ Assert.True(bTrue);
+ Assert.False(bFalse);
+ }
+
+ /// <summary>
+ /// Test for creating a JsonValue from a deep-nested dynamic object.
+ /// </summary>
+ [Fact]
+ public void CreateFromDeepNestedDynamic()
+ {
+ int count = 5000;
+ string expected = "";
+
+ dynamic dyn = new TestDynamicObject();
+ dynamic cur = dyn;
+
+ for (int i = 0; i < count; i++)
+ {
+ expected += "{\"" + i + "\":";
+ cur[i.ToString()] = new TestDynamicObject();
+ cur = cur[i.ToString()];
+ }
+
+ expected += "{}";
+
+ for (int i = 0; i < count; i++)
+ {
+ expected += "}";
+ }
+
+ JsonValue jv = JsonValueExtensions.CreateFrom(dyn);
+ Assert.Equal(expected, jv.ToString());
+ }
+
+ private void ValidateResult<ResultType>(dynamic value, ResultType expectedResult)
+ {
+ Assert.IsAssignableFrom(typeof(ResultType), value);
+ Assert.Equal<ResultType>(expectedResult, (ResultType)value);
+ }
+
+ /// <summary>
+ /// Concrete DynamicObject class for testing purposes.
+ /// </summary>
+ internal class TestDynamicObject : DynamicObject
+ {
+ private IDictionary<string, object> _values = new Dictionary<string, object>();
+
+ public override IEnumerable<string> GetDynamicMemberNames()
+ {
+ return _values.Keys;
+ }
+
+ public override bool TrySetMember(SetMemberBinder binder, object value)
+ {
+ _values[binder.Name] = value;
+ return true;
+ }
+
+ public override bool TryGetMember(GetMemberBinder binder, out object result)
+ {
+ return _values.TryGetValue(binder.Name, out result);
+ }
+
+ public override bool TrySetIndex(SetIndexBinder binder, object[] indexes, object value)
+ {
+ string key = indexes[0].ToString();
+
+ if (_values.ContainsKey(key))
+ {
+ _values[key] = value;
+ }
+ else
+ {
+ _values.Add(key, value);
+ }
+ return true;
+ }
+
+ public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result)
+ {
+ string key = indexes[0].ToString();
+
+ if (_values.ContainsKey(key))
+ {
+ result = _values[key];
+ return true;
+ }
+ else
+ {
+ result = null;
+ return false;
+ }
+ }
+ }
+ }
+}
diff --git a/test/System.Json.Test.Integration/JsonValueEventsTests.cs b/test/System.Json.Test.Integration/JsonValueEventsTests.cs
new file mode 100644
index 00000000..9b64b46a
--- /dev/null
+++ b/test/System.Json.Test.Integration/JsonValueEventsTests.cs
@@ -0,0 +1,518 @@
+using System.Collections.Generic;
+using System.Linq;
+using Xunit;
+
+namespace System.Json
+{
+ /// <summary>
+ /// Tests for events on <see cref="JsonValue"/> instances.
+ /// </summary>
+ public class JsonValueEventsTests
+ {
+ /// <summary>
+ /// Events tests for JsonArray, test all method the causes change and all change type and validate changing/changed child and sub/unsub
+ /// </summary>
+ [Fact]
+ public void JsonArrayEventsTest()
+ {
+ int seed = 1;
+ const int maxArrayLength = 1024;
+ Random rand = new Random(seed);
+ JsonArray ja = SpecialJsonValueHelper.CreatePrePopulatedJsonArray(seed, rand.Next(maxArrayLength));
+ int addPosition = ja.Count;
+ JsonValue insertValue = SpecialJsonValueHelper.GetRandomJsonPrimitives(seed);
+
+ TestEvents(
+ ja,
+ arr => arr.Add(insertValue),
+ new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>
+ {
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, ja, new JsonValueChangeEventArgs(insertValue, JsonValueChange.Add, addPosition)),
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, ja, new JsonValueChangeEventArgs(insertValue, JsonValueChange.Add, addPosition)),
+ });
+
+ addPosition = ja.Count;
+ JsonValue jv1 = SpecialJsonValueHelper.GetRandomJsonPrimitives(seed);
+ JsonValue jv2 = SpecialJsonValueHelper.GetRandomJsonPrimitives(seed);
+ TestEvents(
+ ja,
+ arr => arr.AddRange(jv1, jv2),
+ new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>
+ {
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, ja,
+ new JsonValueChangeEventArgs(
+ jv1,
+ JsonValueChange.Add, addPosition)),
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, ja,
+ new JsonValueChangeEventArgs(
+ jv2,
+ JsonValueChange.Add, addPosition + 1)),
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, ja,
+ new JsonValueChangeEventArgs(
+ jv1,
+ JsonValueChange.Add, addPosition)),
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, ja,
+ new JsonValueChangeEventArgs(
+ jv2,
+ JsonValueChange.Add, addPosition + 1)),
+ });
+
+ int replacePosition = rand.Next(ja.Count - 1);
+ JsonValue oldValue = ja[replacePosition];
+ JsonValue newValue = SpecialJsonValueHelper.GetRandomJsonPrimitives(seed);
+ TestEvents(
+ ja,
+ arr => arr[replacePosition] = newValue,
+ new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>
+ {
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, ja, new JsonValueChangeEventArgs(newValue, JsonValueChange.Replace, replacePosition)),
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, ja, new JsonValueChangeEventArgs(oldValue, JsonValueChange.Replace, replacePosition)),
+ });
+
+ int insertPosition = rand.Next(ja.Count - 1);
+ insertValue = SpecialJsonValueHelper.GetRandomJsonPrimitives(seed);
+
+ TestEvents(
+ ja,
+ arr => arr.Insert(insertPosition, insertValue),
+ new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>
+ {
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, ja, new JsonValueChangeEventArgs(insertValue, JsonValueChange.Add, insertPosition)),
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, ja, new JsonValueChangeEventArgs(insertValue, JsonValueChange.Add, insertPosition)),
+ });
+
+ TestEvents(
+ ja,
+ arr => arr.RemoveAt(insertPosition),
+ new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>
+ {
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, ja, new JsonValueChangeEventArgs(insertValue, JsonValueChange.Remove, insertPosition)),
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, ja, new JsonValueChangeEventArgs(insertValue, JsonValueChange.Remove, insertPosition)),
+ });
+
+ ja.Insert(0, insertValue);
+ TestEvents(
+ ja,
+ arr => arr.Remove(insertValue),
+ new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>
+ {
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, ja, new JsonValueChangeEventArgs(insertValue, JsonValueChange.Remove, 0)),
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, ja, new JsonValueChangeEventArgs(insertValue, JsonValueChange.Remove, 0)),
+ });
+
+ TestEvents(
+ ja,
+ arr => arr.Clear(),
+ new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>
+ {
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, ja, new JsonValueChangeEventArgs(null, JsonValueChange.Clear, 0)),
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, ja, new JsonValueChangeEventArgs(null, JsonValueChange.Clear, 0)),
+ });
+
+ ja = new JsonArray(1, 2, 3);
+ TestEvents(
+ ja,
+ arr => arr.Remove(new JsonPrimitive("Not there")),
+ new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>());
+
+ JsonValue elementInArray = ja[1];
+ TestEvents(
+ ja,
+ arr => arr.Remove(elementInArray),
+ new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>
+ {
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, ja, new JsonValueChangeEventArgs(elementInArray, JsonValueChange.Remove, 1)),
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, ja, new JsonValueChangeEventArgs(elementInArray, JsonValueChange.Remove, 1)),
+ });
+ }
+
+ /// <summary>
+ /// Tests for events for <see cref="JsonValue"/> instances when using the dynamic programming.
+ /// </summary>
+ [Fact]
+ public void DynamicEventsTest()
+ {
+ int seed = 1;
+ int maxObj = 10;
+ JsonArray ja = new JsonArray();
+ dynamic d = ja.AsDynamic();
+ TestEventsDynamic(
+ d,
+ (Action<dynamic>)(arr => arr.Add(1)),
+ new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>
+ {
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, ja, new JsonValueChangeEventArgs(1, JsonValueChange.Add, 0)),
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, ja, new JsonValueChangeEventArgs(1, JsonValueChange.Add, 0)),
+ });
+
+ const string key1 = "first";
+ const string key2 = "second";
+ JsonObject jo = new JsonObject
+ {
+ { key1, SpecialJsonValueHelper.GetRandomJsonPrimitives(seed) },
+ };
+
+ JsonObject objectToAdd = SpecialJsonValueHelper.CreateRandomPopulatedJsonObject(seed, maxObj);
+ dynamic d2 = jo.AsDynamic();
+ TestEventsDynamic(
+ d2,
+ (Action<dynamic>)(obj => obj[key2] = objectToAdd),
+ new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>
+ {
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, jo, new JsonValueChangeEventArgs(objectToAdd, JsonValueChange.Add, key2)),
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, jo, new JsonValueChangeEventArgs(objectToAdd, JsonValueChange.Add, key2)),
+ });
+
+ TestEventsDynamic(
+ d2,
+ (Action<dynamic>)(obj => obj[key2] = objectToAdd),
+ new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>
+ {
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, jo, new JsonValueChangeEventArgs(objectToAdd, JsonValueChange.Replace, key2)),
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, jo, new JsonValueChangeEventArgs(objectToAdd, JsonValueChange.Replace, key2)),
+ });
+ }
+
+ /// <summary>
+ /// Tests for events in <see cref="JsonObject"/> instances.
+ /// </summary>
+ [Fact]
+ public void JsonObjectEventsTest()
+ {
+ int seed = 1;
+ const int maxObj = 10;
+
+ const string key1 = "first";
+ const string key2 = "second";
+ const string key3 = "third";
+ const string key4 = "fourth";
+ const string key5 = "fifth";
+ JsonObject jo = new JsonObject
+ {
+ { key1, SpecialJsonValueHelper.GetRandomJsonPrimitives(seed) },
+ { key2, SpecialJsonValueHelper.GetRandomJsonPrimitives(seed) },
+ { key3, null },
+ };
+
+ JsonObject objToAdd = SpecialJsonValueHelper.CreateRandomPopulatedJsonObject(seed, maxObj);
+ TestEvents(
+ jo,
+ obj => obj.Add(key4, objToAdd),
+ new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>
+ {
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, jo, new JsonValueChangeEventArgs(objToAdd, JsonValueChange.Add, key4)),
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, jo, new JsonValueChangeEventArgs(objToAdd, JsonValueChange.Add, key4)),
+ },
+ obj => obj.Add("key44", objToAdd));
+
+ JsonArray jaToAdd = SpecialJsonValueHelper.CreatePrePopulatedJsonArray(seed, maxObj);
+ JsonValue replaced = jo[key2];
+ TestEvents(
+ jo,
+ obj => obj[key2] = jaToAdd,
+ new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>
+ {
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, jo, new JsonValueChangeEventArgs(jaToAdd, JsonValueChange.Replace, key2)),
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, jo, new JsonValueChangeEventArgs(replaced, JsonValueChange.Replace, key2)),
+ });
+
+ JsonValue jpToAdd = SpecialJsonValueHelper.GetRandomJsonPrimitives(seed);
+ TestEvents(
+ jo,
+ obj => obj[key5] = jpToAdd,
+ new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>
+ {
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, jo, new JsonValueChangeEventArgs(jpToAdd, JsonValueChange.Add, key5)),
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, jo, new JsonValueChangeEventArgs(jpToAdd, JsonValueChange.Add, key5)),
+ });
+
+ jo.Remove(key4);
+ jo.Remove(key5);
+
+ JsonValue jp1 = SpecialJsonValueHelper.GetRandomJsonPrimitives(seed);
+ JsonValue jp2 = SpecialJsonValueHelper.GetRandomJsonPrimitives(seed);
+ TestEvents(
+ jo,
+ obj => obj.AddRange(new JsonObject { { key4, jp1 }, { key5, jp1 } }),
+ new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>
+ {
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, jo, new JsonValueChangeEventArgs(jp1, JsonValueChange.Add, key4)),
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, jo, new JsonValueChangeEventArgs(jp2, JsonValueChange.Add, key5)),
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, jo, new JsonValueChangeEventArgs(jp1, JsonValueChange.Add, key4)),
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, jo, new JsonValueChangeEventArgs(jp2, JsonValueChange.Add, key5)),
+ },
+ obj => obj.AddRange(new JsonObject { { "new key", jp1 }, { "newnewKey", jp2 } }));
+
+ TestEvents(
+ jo,
+ obj => obj.Remove(key5),
+ new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>
+ {
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, jo, new JsonValueChangeEventArgs(jp2, JsonValueChange.Remove, key5)),
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, jo, new JsonValueChangeEventArgs(jp2, JsonValueChange.Remove, key5)),
+ });
+
+ TestEvents(
+ jo,
+ obj => obj.Remove("not there"),
+ new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>());
+
+ jo = new JsonObject { { key1, 1 }, { key2, 2 }, { key3, 3 } };
+
+ TestEvents(
+ jo,
+ obj => obj.Clear(),
+ new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>
+ {
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, jo, new JsonValueChangeEventArgs(null, JsonValueChange.Clear, null)),
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, jo, new JsonValueChangeEventArgs(null, JsonValueChange.Clear, null)),
+ });
+
+ jo = new JsonObject { { key1, 1 }, { key2, 2 }, { key3, 3 } };
+ TestEvents(
+ jo,
+ obj => ((IDictionary<string, JsonValue>)obj).Remove(new KeyValuePair<string, JsonValue>(key2, jo[key2])),
+ new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>
+ {
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, jo, new JsonValueChangeEventArgs(2, JsonValueChange.Remove, key2)),
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, jo, new JsonValueChangeEventArgs(2, JsonValueChange.Remove, key2)),
+ },
+ obj => ((IDictionary<string, JsonValue>)obj).Remove(new KeyValuePair<string, JsonValue>(key1, jo[key1])));
+ }
+
+ /// <summary>
+ /// Tests for events in <see cref="JsonValue"/> instances when multiple listeners are registered.
+ /// </summary>
+ [Fact]
+ public void MultipleListenersTest()
+ {
+ const string key1 = "first";
+ const string key2 = "second";
+ const string key3 = "third";
+
+ for (int changingListeners = 0; changingListeners <= 3; changingListeners++)
+ {
+ for (int changedListeners = 0; changedListeners <= 3; changedListeners++)
+ {
+ MultipleListenersTestInternal<JsonObject>(
+ () => new JsonObject { { key1, 1 }, { key2, 2 } },
+ delegate(JsonObject obj)
+ {
+ obj[key2] = "hello";
+ obj.Remove(key1);
+ obj.Add(key3, "world");
+ obj.Clear();
+ },
+ new List<JsonValueChangeEventArgs>
+ {
+ new JsonValueChangeEventArgs("hello", JsonValueChange.Replace, key2),
+ new JsonValueChangeEventArgs(1, JsonValueChange.Remove, key1),
+ new JsonValueChangeEventArgs("world", JsonValueChange.Add, key3),
+ new JsonValueChangeEventArgs(null, JsonValueChange.Clear, null),
+ },
+ new List<JsonValueChangeEventArgs>
+ {
+ new JsonValueChangeEventArgs(2, JsonValueChange.Replace, key2),
+ new JsonValueChangeEventArgs(1, JsonValueChange.Remove, key1),
+ new JsonValueChangeEventArgs("world", JsonValueChange.Add, key3),
+ new JsonValueChangeEventArgs(null, JsonValueChange.Clear, null),
+ },
+ changingListeners,
+ changedListeners);
+
+ MultipleListenersTestInternal<JsonArray>(
+ () => new JsonArray(1, 2),
+ delegate(JsonArray arr)
+ {
+ arr[1] = "hello";
+ arr.RemoveAt(0);
+ arr.Add("world");
+ arr.Clear();
+ },
+ new List<JsonValueChangeEventArgs>
+ {
+ new JsonValueChangeEventArgs("hello", JsonValueChange.Replace, 1),
+ new JsonValueChangeEventArgs(1, JsonValueChange.Remove, 0),
+ new JsonValueChangeEventArgs("world", JsonValueChange.Add, 1),
+ new JsonValueChangeEventArgs(null, JsonValueChange.Clear, 0),
+ },
+ new List<JsonValueChangeEventArgs>
+ {
+ new JsonValueChangeEventArgs(2, JsonValueChange.Replace, 1),
+ new JsonValueChangeEventArgs(1, JsonValueChange.Remove, 0),
+ new JsonValueChangeEventArgs("world", JsonValueChange.Add, 1),
+ new JsonValueChangeEventArgs(null, JsonValueChange.Clear, 0),
+ },
+ changingListeners,
+ changedListeners);
+ }
+ }
+ }
+
+ internal static void TestEvents<JsonValueType>(JsonValueType target, Action<JsonValueType> actionToTriggerEvent, List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>> expectedEvents, Action<JsonValueType> actionToTriggerEvent2 = null) where JsonValueType : JsonValue
+ {
+ var actualEvents = new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>();
+ EventHandler<JsonValueChangeEventArgs> changingHandler = (sender, e) => actualEvents.Add(new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, sender as JsonValue, e));
+ EventHandler<JsonValueChangeEventArgs> changedHandler = (sender, e) => actualEvents.Add(new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, sender as JsonValue, e));
+
+ target.Changing += changingHandler;
+ target.Changed += changedHandler;
+ actionToTriggerEvent(target);
+
+ target.Changing -= changingHandler;
+ target.Changed -= changedHandler;
+ ValidateExpectedEvents(expectedEvents, actualEvents);
+ if (actionToTriggerEvent2 == null)
+ {
+ actionToTriggerEvent(target);
+ }
+ else
+ {
+ actionToTriggerEvent2(target);
+ }
+
+ ValidateExpectedEvents(expectedEvents, actualEvents);
+ }
+
+ internal static void TestEventsDynamic(dynamic target, Action<dynamic> actionToTriggerEvent, List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>> expectedEvents)
+ {
+ var actualEvents = new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>();
+ EventHandler<JsonValueChangeEventArgs> changingHandler = (sender, e) => actualEvents.Add(new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, sender as JsonValue, e));
+ EventHandler<JsonValueChangeEventArgs> changedHandler = (sender, e) => actualEvents.Add(new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, sender as JsonValue, e));
+
+ target.Changing += changingHandler;
+ target.Changed += changedHandler;
+ actionToTriggerEvent(target);
+
+ target.Changing -= changingHandler;
+ target.Changed -= changedHandler;
+ ValidateExpectedEvents(expectedEvents, actualEvents);
+
+ actionToTriggerEvent(target);
+ ValidateExpectedEvents(expectedEvents, actualEvents);
+ }
+
+ private static void ValidateExpectedEvents(List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>> expectedEvents, List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>> actualEvents)
+ {
+ Assert.Equal(expectedEvents.Count, actualEvents.Count);
+ for (int i = 0; i < expectedEvents.Count; i++)
+ {
+ bool expectedIsChanging = expectedEvents[i].Item1;
+ bool actualIsChanging = expectedEvents[i].Item1;
+ Assert.Equal(expectedIsChanging, actualIsChanging);
+
+ JsonValue expectedSender = expectedEvents[i].Item2;
+ JsonValue actualSender = actualEvents[i].Item2;
+ Assert.Equal(expectedSender, actualSender);
+
+ JsonValueChangeEventArgs expectedEventArgs = expectedEvents[i].Item3;
+ JsonValueChangeEventArgs actualEventArgs = actualEvents[i].Item3;
+ Assert.Equal(expectedEventArgs.Change, actualEventArgs.Change);
+ Assert.Equal(expectedEventArgs.Index, actualEventArgs.Index);
+ Assert.Equal(expectedEventArgs.Key, actualEventArgs.Key);
+
+ string expectedChild = expectedEventArgs.Child == null ? "null" : expectedEventArgs.Child.ToString();
+ string actualChild = actualEventArgs.Child == null ? "null" : actualEventArgs.Child.ToString();
+ Assert.Equal(expectedChild, actualChild);
+ }
+ }
+
+ internal static void MultipleListenersTestInternal<JsonValueType>(
+ Func<JsonValueType> createTarget,
+ Action<JsonValueType> actionToTriggerEvents,
+ List<JsonValueChangeEventArgs> expectedChangingEventArgs,
+ List<JsonValueChangeEventArgs> expectedChangedEventArgs,
+ int changingListeners,
+ int changedListeners) where JsonValueType : JsonValue
+ {
+ Log.Info("Testing events on a {0} for {1} changING listeners and {2} changED listeners", typeof(JsonValueType).Name, changingListeners, changedListeners);
+ JsonValueType target = createTarget();
+ List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>[] actualChangingEvents = new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>[changingListeners];
+ List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>[] actualChangedEvents = new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>[changedListeners];
+ List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>> expectedChangingEvents = new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>(
+ expectedChangingEventArgs.Select((args) => new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, target, args)));
+ List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>> expectedChangedEvents = new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>(
+ expectedChangedEventArgs.Select((args) => new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, target, args)));
+
+ for (int i = 0; i < changingListeners; i++)
+ {
+ actualChangingEvents[i] = new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>();
+ var index = i;
+ target.Changing += delegate(object sender, JsonValueChangeEventArgs e)
+ {
+ actualChangingEvents[index].Add(new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, sender as JsonValue, e));
+ };
+ }
+
+ for (int i = 0; i < changedListeners; i++)
+ {
+ actualChangedEvents[i] = new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>();
+ var index = i;
+ target.Changed += delegate(object sender, JsonValueChangeEventArgs e)
+ {
+ actualChangedEvents[index].Add(new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, sender as JsonValue, e));
+ };
+ }
+
+ actionToTriggerEvents(target);
+ for (int i = 0; i < changingListeners; i++)
+ {
+ Log.Info("Validating Changing events for listener {0}", i);
+ ValidateExpectedEvents(expectedChangingEvents, actualChangingEvents[i]);
+ }
+
+ for (int i = 0; i < changedListeners; i++)
+ {
+ Log.Info("Validating Changed events for listener {0}", i);
+ ValidateExpectedEvents(expectedChangedEvents, actualChangedEvents[i]);
+ }
+
+ for (int i = 0; i < changingListeners; i++)
+ {
+ actualChangingEvents[i] = new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>();
+ var index = i;
+ target.Changing -= delegate(object sender, JsonValueChangeEventArgs e)
+ {
+ actualChangingEvents[i].Add(new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, sender as JsonValue, e));
+ };
+ }
+
+ for (int i = 0; i < changedListeners; i++)
+ {
+ actualChangedEvents[i] = new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>();
+ var index = i;
+ target.Changed -= delegate(object sender, JsonValueChangeEventArgs e)
+ {
+ actualChangedEvents[i].Add(new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, sender as JsonValue, e));
+ };
+ }
+
+ target = createTarget();
+ expectedChangingEvents.Clear();
+ expectedChangedEvents.Clear();
+ actionToTriggerEvents(target);
+
+ for (int i = 0; i < changingListeners; i++)
+ {
+ Log.Info("Validating Changing events for listener {0}", i);
+ ValidateExpectedEvents(expectedChangingEvents, actualChangingEvents[i]);
+ }
+
+ for (int i = 0; i < changedListeners; i++)
+ {
+ Log.Info("Validating Changed events for listener {0}", i);
+ ValidateExpectedEvents(expectedChangedEvents, actualChangedEvents[i]);
+ }
+ }
+
+ private static void ValidateJsonArrayItems(JsonArray jsonArray, IEnumerable<JsonValue> expectedItems)
+ {
+ List<JsonValue> expected = new List<JsonValue>(expectedItems);
+ Assert.Equal(expected.Count, jsonArray.Count);
+ for (int i = 0; i < expected.Count; i++)
+ {
+ Assert.Equal(expected[i], jsonArray[i]);
+ }
+ }
+ }
+}
diff --git a/test/System.Json.Test.Integration/JsonValueLinqExtensionsIntegrationTest.cs b/test/System.Json.Test.Integration/JsonValueLinqExtensionsIntegrationTest.cs
new file mode 100644
index 00000000..1e556f4a
--- /dev/null
+++ b/test/System.Json.Test.Integration/JsonValueLinqExtensionsIntegrationTest.cs
@@ -0,0 +1,68 @@
+using System.Linq;
+using Xunit;
+
+namespace System.Json
+{
+ /// <summary>
+ /// Tests for the Linq extensions to the <see cref="JsonValue"/> types.
+ /// </summary>
+ public class JsonValueLinqExtensionsIntegrationTest
+ {
+ /// <summary>
+ /// Test for the <see cref="JsonValueLinqExtensions.ToJsonArray"/> method.
+ /// </summary>
+ [Fact]
+ public void ToJsonArrayTest()
+ {
+ string json = "{\"SearchResponse\":{\"Phonebook\":{\"Results\":[{\"name\":1,\"rating\":1}, {\"name\":2,\"rating\":2}, {\"name\":3,\"rating\":3}]}}}";
+ string expected = "[{\"name\":2,\"rating\":2},{\"name\":3,\"rating\":3}]";
+
+ JsonValue jv = JsonValue.Parse(json);
+ double rating = 1;
+ var jsonResult = from n in jv.ValueOrDefault("SearchResponse", "Phonebook", "Results")
+ where n.Value.ValueOrDefault("rating").ReadAs<double>(0.0) > rating
+ select n.Value;
+ var ja = jsonResult.ToJsonArray();
+
+ Assert.Equal(expected, ja.ToString());
+ }
+
+ /// <summary>
+ /// Test for the <see cref="JsonValueLinqExtensions.ToJsonObject"/> method.
+ /// </summary>
+ [Fact]
+ public void ToJsonObjectTest()
+ {
+ string json = "{\"Name\":\"Bill Gates\",\"Age\":23,\"AnnualIncome\":45340.45,\"MaritalStatus\":\"Single\",\"EducationLevel\":\"MiddleSchool\",\"SSN\":432332453,\"CellNumber\":2340393420}";
+ string expected = "{\"AnnualIncome\":45340.45,\"SSN\":432332453,\"CellNumber\":2340393420}";
+
+ JsonValue jv = JsonValue.Parse(json);
+ decimal decVal;
+ var jsonResult = from n in jv
+ where n.Value.TryReadAs<decimal>(out decVal) && decVal > 100
+ select n;
+ var jo = jsonResult.ToJsonObject();
+
+ Assert.Equal(expected, jo.ToString());
+ }
+
+ /// <summary>
+ /// Test for the <see cref="JsonValueLinqExtensions.ToJsonObject"/> method where the origin is a <see cref="JsonArray"/>.
+ /// </summary>
+ [Fact]
+ public void ToJsonObjectFromArrayTest()
+ {
+ string json = "{\"SearchResponse\":{\"Phonebook\":{\"Results\":[{\"name\":1,\"rating\":1}, {\"name\":2,\"rating\":2}, {\"name\":3,\"rating\":3}]}}}";
+ string expected = "{\"1\":{\"name\":2,\"rating\":2},\"2\":{\"name\":3,\"rating\":3}}";
+
+ JsonValue jv = JsonValue.Parse(json);
+ double rating = 1;
+ var jsonResult = from n in jv.ValueOrDefault("SearchResponse", "Phonebook", "Results")
+ where n.Value.ValueOrDefault("rating").ReadAs<double>(0.0) > rating
+ select n;
+ var jo = jsonResult.ToJsonObject();
+
+ Assert.Equal(expected, jo.ToString());
+ }
+ }
+}
diff --git a/test/System.Json.Test.Integration/JsonValuePartialTrustTests.cs b/test/System.Json.Test.Integration/JsonValuePartialTrustTests.cs
new file mode 100644
index 00000000..69ce8531
--- /dev/null
+++ b/test/System.Json.Test.Integration/JsonValuePartialTrustTests.cs
@@ -0,0 +1,186 @@
+using System.IO;
+using System.Reflection;
+using System.Runtime.Serialization.Json;
+using System.Security;
+using System.Security.Policy;
+using Xunit;
+
+namespace System.Json
+{
+ /// <summary>
+ /// Tests for using the <see cref="JsonValue"/> types in partial trust.
+ /// </summary>
+ [Serializable]
+ public class JsonValuePartialTrustTests
+ {
+ /// <summary>
+ /// Validates the condition, throwing an exception if it is false.
+ /// </summary>
+ /// <param name="condition">The condition to be evaluated.</param>
+ /// <param name="msg">The exception message to be thrown, in case the condition is false.</param>
+ public static void AssertIsTrue(bool condition, string msg)
+ {
+ if (!condition)
+ {
+ throw new InvalidOperationException(msg);
+ }
+ }
+
+ /// <summary>
+ /// Validates that the two objects are equal, throwing an exception if it is false.
+ /// </summary>
+ /// <param name="obj1">The first object to be compared.</param>
+ /// <param name="obj2">The second object to be compared.</param>
+ /// <param name="msg">The exception message to be thrown, in case the condition is false.</param>
+ public static void AssertAreEqual(object obj1, object obj2, string msg)
+ {
+ if (obj1 == obj2)
+ {
+ return;
+ }
+
+ if (obj1 == null || obj2 == null || !obj1.Equals(obj2))
+ {
+ throw new InvalidOperationException(String.Format("[{0}, {2}] and [{1}, {3}] expected to be equal. {4}", obj1, obj2, obj1.GetType().Name, obj2.GetType().Name, msg));
+ }
+ }
+
+ /// <summary>
+ /// Partial trust tests for <see cref="JsonValue"/> instances where no dynamic references are used.
+ /// </summary>
+ [Fact(Skip = "Re-enable when CSDMain 216528: 'Partial trust support for Web API' has been fixed")]
+ public void RunNonDynamicTest()
+ {
+ RunInPartialTrust(this.NonDynamicTest);
+ }
+
+ /// <summary>
+ /// Partial trust tests for <see cref="JsonValue"/> with dynamic references.
+ /// </summary>
+ [Fact(Skip = "Re-enable when CSDMain 216528: 'Partial trust support for Web API' has been fixed")]
+ public void RunDynamicTest()
+ {
+ RunInPartialTrust(this.DynamicTest);
+ }
+
+ /// <summary>
+ /// Tests for <see cref="JsonValue"/> instances without dynamic references.
+ /// </summary>
+ public void NonDynamicTest()
+ {
+ int seed = GetRandomSeed();
+ Log.Info("Seed: {0}", seed);
+ Random rndGen = new Random(seed);
+
+ AssertIsTrue(Assembly.GetExecutingAssembly().IsFullyTrusted == false, "Executing assembly not expected to be fully trusted!");
+
+ Person person = new Person(rndGen);
+ Person person2 = new Person(rndGen);
+
+ person.AddFriends(3, rndGen);
+ person2.AddFriends(3, rndGen);
+
+ JsonValue jo = JsonValueExtensions.CreateFrom(person);
+ JsonValue jo2 = JsonValueExtensions.CreateFrom(person2);
+
+ AssertAreEqual(person.Address.City, jo["Address"]["City"].ReadAs<string>(), "Address.City");
+ AssertAreEqual(person.Friends[1].Age, jo["Friends"][1]["Age"].ReadAs<int>(), "Friends[1].Age");
+
+ string newCityName = "Bellevue";
+
+ jo["Address"]["City"] = newCityName;
+ AssertAreEqual(newCityName, (string)jo["Address"]["City"], "Address.City2");
+
+ jo["Friends"][1] = jo2;
+ AssertAreEqual(person2.Age, (int)jo["Friends"][1]["Age"], "Friends[1].Age2");
+
+ AssertAreEqual(person2.Address.City, jo.ValueOrDefault("Friends").ValueOrDefault(1).ValueOrDefault("Address").ValueOrDefault("City").ReadAs<string>(), "Address.City3");
+ AssertAreEqual(person2.Age, (int)jo.ValueOrDefault("Friends").ValueOrDefault(1).ValueOrDefault("Age"), "Friends[1].Age3");
+
+ AssertAreEqual(person2.Address.City, jo.ValueOrDefault("Friends", 1, "Address", "City").ReadAs<string>(), "Address.City3");
+ AssertAreEqual(person2.Age, (int)jo.ValueOrDefault("Friends", 1, "Age"), "Friends[1].Age3");
+
+ int newAge = 42;
+ JsonValue ageValue = jo["Friends"][1]["Age"] = newAge;
+ AssertAreEqual(newAge, (int)ageValue, "Friends[1].Age4");
+ }
+
+ /// <summary>
+ /// Tests for <see cref="JsonValue"/> instances with dynamic references.
+ /// </summary>
+ public void DynamicTest()
+ {
+ int seed = GetRandomSeed();
+ Log.Info("Seed: {0}", seed);
+ Random rndGen = new Random(seed);
+
+ AssertIsTrue(Assembly.GetExecutingAssembly().IsFullyTrusted == false, "Executing assembly not expected to be fully trusted!");
+
+ Person person = new Person(rndGen);
+ person.AddFriends(1, rndGen);
+
+ dynamic jo = JsonValueExtensions.CreateFrom(person);
+
+ AssertAreEqual(person.Friends[0].Name, jo.Friends[0].Name.ReadAs<string>(), "Friends[0].Name");
+ AssertAreEqual(person.Address.City, jo.Address.City.ReadAs<string>(), "Address.City");
+ AssertAreEqual(person.Friends[0].Age, (int)jo.Friends[0].Age, "Friends[0].Age");
+
+ string newCityName = "Bellevue";
+
+ jo.Address.City = newCityName;
+ AssertAreEqual(newCityName, (string)jo.Address.City, "Address.City2");
+
+ AssertAreEqual(person.Friends[0].Address.City, jo.ValueOrDefault("Friends").ValueOrDefault(0).ValueOrDefault("Address").ValueOrDefault("City").ReadAs<string>(), "Friends[0].Address.City");
+ AssertAreEqual(person.Friends[0].Age, (int)jo.ValueOrDefault("Friends").ValueOrDefault(0).ValueOrDefault("Age"), "Friends[0].Age2");
+
+ AssertAreEqual(person.Friends[0].Address.City, jo.ValueOrDefault("Friends", 0, "Address", "City").ReadAs<string>(), "Friends[0].Address.City");
+ AssertAreEqual(person.Friends[0].Age, (int)jo.ValueOrDefault("Friends", 0, "Age"), "Friends[0].Age2");
+
+ int newAge = 42;
+ JsonValue ageValue = jo.Friends[0].Age = newAge;
+ AssertAreEqual(newAge, (int)ageValue, "Friends[0].Age3");
+
+ AssertIsTrue(jo.NonExistentProperty is JsonValue, "Expected a JsonValue");
+ AssertIsTrue(jo.NonExistentProperty.JsonType == JsonType.Default, "Expected default JsonValue");
+ }
+
+ private static void RunInPartialTrust(CrossAppDomainDelegate testMethod)
+ {
+ Assert.True(Assembly.GetExecutingAssembly().IsFullyTrusted);
+
+ AppDomainSetup setup = new AppDomainSetup();
+ setup.ApplicationBase = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
+ PermissionSet perms = PermissionsHelper.InternetZone;
+ AppDomain domain = AppDomain.CreateDomain("PartialTrustSandBox", null, setup, perms);
+
+ domain.DoCallBack(testMethod);
+ }
+
+ private static int GetRandomSeed()
+ {
+ DateTime now = DateTime.Now;
+ return (now.Year * 10000) + (now.Month * 100) + now.Day;
+ }
+
+ internal static class PermissionsHelper
+ {
+ private static PermissionSet internetZone;
+
+ public static PermissionSet InternetZone
+ {
+ get
+ {
+ if (internetZone == null)
+ {
+ Evidence evidence = new Evidence();
+ evidence.AddHostEvidence(new Zone(SecurityZone.Internet));
+
+ internetZone = SecurityManager.GetStandardSandbox(evidence);
+ }
+
+ return internetZone;
+ }
+ }
+ }
+ }
+}
diff --git a/test/System.Json.Test.Integration/JsonValueTestHelper.cs b/test/System.Json.Test.Integration/JsonValueTestHelper.cs
new file mode 100644
index 00000000..8896fe64
--- /dev/null
+++ b/test/System.Json.Test.Integration/JsonValueTestHelper.cs
@@ -0,0 +1,609 @@
+using System.Collections.Generic;
+using System.Globalization;
+
+namespace System.Json
+{
+ internal static class JsonValueVerifier
+ {
+ public static bool Compare(JsonValue objA, JsonValue objB)
+ {
+ if (objA == null && objB == null)
+ {
+ return true;
+ }
+
+ if ((objA == null && objB != null) || (objA != null && objB == null))
+ {
+ Log.Info("JsonValueVerifier Error: At least one of the JsonValue compared is null");
+ return false;
+ }
+
+ if (objA.JsonType != objB.JsonType)
+ {
+ Log.Info("JsonValueVerifier Error: These two JsonValues are not of the same Type!");
+ Log.Info("objA is of type {0} while objB is of type {1}", objA.JsonType.ToString(), objB.JsonType.ToString());
+ return false;
+ }
+
+ return CompareJsonValues(objA, objB);
+ }
+
+ public static bool CompareStringLists(List<string> strListA, List<string> strListB)
+ {
+ bool retValue = true;
+ if (strListA.Count != strListB.Count)
+ {
+ retValue = false;
+ }
+ else
+ {
+ for (int i = 0; i < strListA.Count; i++)
+ {
+ if (strListA[i] != strListB[i])
+ {
+ retValue = false;
+ break;
+ }
+ }
+ }
+
+ return retValue;
+ }
+
+ // Because we are currently taking a "flat design" model on JsonValues, the intellense doesn't work, and we have to be smart about what to verify
+ // and what not to so to avoid any potentially invalid access
+ private static bool CompareJsonValues(JsonValue objA, JsonValue objB)
+ {
+ bool retValue = false;
+ switch (objA.JsonType)
+ {
+ case JsonType.Array:
+ retValue = CompareJsonArrayTypes((JsonArray)objA, (JsonArray)objB);
+ break;
+ case JsonType.Object:
+ retValue = CompareJsonObjectTypes((JsonObject)objA, (JsonObject)objB);
+ break;
+ case JsonType.Boolean:
+ case JsonType.Number:
+ case JsonType.String:
+ retValue = CompareJsonPrimitiveTypes((JsonPrimitive)objA, (JsonPrimitive)objB);
+ break;
+ default:
+ Log.Info("JsonValueVerifier Error: the JsonValue isn’t an array, a complex type or a primitive type!");
+ break;
+ }
+
+ return retValue;
+ }
+
+ private static bool CompareJsonArrayTypes(JsonArray objA, JsonArray objB)
+ {
+ bool retValue = true;
+
+ if (objA == null || objB == null || objA.Count != objB.Count || objA.IsReadOnly != objB.IsReadOnly)
+ {
+ return false;
+ }
+
+ try
+ {
+ for (int i = 0; i < objA.Count; i++)
+ {
+ if (!Compare(objA[i], objB[i]))
+ {
+ Log.Info("JsonValueVerifier (JsonArrayType) Error: objA[{0}] = {1}", i, objA[i].ToString());
+ Log.Info("JsonValueVerifier (JsonArrayType) Error: objB[{0}] = {1}", i, objB[i].ToString());
+ return false;
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ Log.Info("JsonValueVerifier (JsonArrayType) Error: An Exception was thrown: " + e);
+ return false;
+ }
+
+ return retValue;
+ }
+
+ private static bool CompareJsonObjectTypes(JsonObject objA, JsonObject objB)
+ {
+ bool retValue = true;
+
+ try
+ {
+ if (objA.Keys.Count != objB.Keys.Count)
+ {
+ Log.Info("JsonValueVerifier (JsonObjectTypes) Error: objA.Keys.Count does not match objB.Keys.Count!");
+ Log.Info("JsonValueVerifier (JsonObjectTypes) Error: objA.Keys.Count = {0}, objB.Keys.Count = {1}", objA.Keys.Count, objB.Keys.Count);
+ return false;
+ }
+
+ if (objA.Keys.IsReadOnly != objB.Keys.IsReadOnly)
+ {
+ Log.Info("JsonValueVerifier (JsonObjectTypes) Error: objA.Keys.IsReadOnly does not match objB.Keys.IsReadOnly!");
+ Log.Info("JsonValueVerifier (JsonObjectTypes) Error: objA.Keys.IsReadOnly = {0}, objB.Keys.IsReadOnly = {1}", objA.Keys.IsReadOnly, objB.Keys.IsReadOnly);
+ return false;
+ }
+ else
+ {
+ foreach (string keyA in objA.Keys)
+ {
+ if (!objB.ContainsKey(keyA))
+ {
+ Log.Info("JsonValueVerifier (JsonObjectTypes) Error: objB does not contain Key " + keyA + "!");
+ return false;
+ }
+
+ if (!Compare(objA[keyA], objB[keyA]))
+ {
+ Log.Info("JsonValueVerifier (JsonObjectTypes) Error: objA[" + keyA + "] = " + objA[keyA]);
+ Log.Info("JsonValueVerifier (JsonObjectTypes) Error: objB[" + keyA + "] = " + objB[keyA]);
+ return false;
+ }
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ Log.Info("JsonValueVerifier (JsonObjectTypes) Error: An Exception was thrown: " + e);
+ return false;
+ }
+
+ return retValue;
+ }
+
+ private static bool CompareJsonPrimitiveTypes(JsonPrimitive objA, JsonPrimitive objB)
+ {
+ try
+ {
+ if (objA.ToString() != objB.ToString())
+ {
+ // Special case due to daylight saving hours change: every March on the morning of the third Sunday, we adjust the time
+ // from 2am to 3am straight, so for that one hour 2:13am = 3:15am. We must result to the UTC ticks to verify the actual
+ // time is always the same, regardless of the loc/glob setup on the machine
+ if (objA.ToString().StartsWith("\"\\/Date(") && objA.ToString().EndsWith(")\\/\""))
+ {
+ return GetUTCTicks(objA) == GetUTCTicks(objB);
+ }
+ else
+ {
+ Log.Info("JsonValueVerifier (JsonPrimitiveTypes) Error: objA = " + objA.ToString());
+ Log.Info("JsonValueVerifier (JsonPrimitiveTypes) Error: objB = " + objB.ToString());
+ return false;
+ }
+ }
+ else
+ {
+ return true;
+ }
+ }
+ catch (Exception e)
+ {
+ Log.Info("JsonValueVerifier (JsonPrimitiveTypes) Error: An Exception was thrown: " + e);
+ return false;
+ }
+ }
+
+ // the input JsonPrimitive DateTime format is "\/Date(24735422733034-0700)\/" or "\/Date(24735422733034)\/"
+ // the only thing useful for us is the UTC ticks "24735422733034"
+ // everything after - if present - is just an optional offset between the local time and UTC
+ private static string GetUTCTicks(JsonPrimitive jprim)
+ {
+ string retValue = String.Empty;
+
+ string origStr = jprim.ToString();
+ int startIndex = origStr.IndexOf("Date(") + 5;
+ int endIndex = origStr.IndexOf('-', startIndex + 1); // the UTC ticks can start with a '-' sign (dates prior to 1970/01/01)
+
+ // if the optional offset is present in the data format, we want to take only the UTC ticks
+ if (startIndex < endIndex)
+ {
+ retValue = origStr.Substring(startIndex, endIndex - startIndex);
+ }
+ else
+ {
+ // otherwise we assume the time format is without the oiptional offset, or unexpected, and use the whole string for comparison.
+ retValue = origStr;
+ }
+
+ return retValue;
+ }
+ }
+
+ internal static class SpecialJsonValueHelper
+ {
+ public static JsonArray CreateDeepLevelJsonValuePair(int seed, out JsonArray newOrderJson)
+ {
+ Log.Info("Seed: {0}", seed);
+ Random rndGen = new Random(seed);
+
+ bool myBool = PrimitiveCreator.CreateInstanceOfBoolean(rndGen);
+ byte myByte = PrimitiveCreator.CreateInstanceOfByte(rndGen);
+ DateTime myDatetime = PrimitiveCreator.CreateInstanceOfDateTime(rndGen);
+ DateTimeOffset myDateTimeOffset = PrimitiveCreator.CreateInstanceOfDateTimeOffset(rndGen);
+ decimal myDecimal = PrimitiveCreator.CreateInstanceOfDecimal(rndGen);
+ double myDouble = PrimitiveCreator.CreateInstanceOfDouble(rndGen);
+ short myInt16 = PrimitiveCreator.CreateInstanceOfInt16(rndGen);
+ int myInt32 = PrimitiveCreator.CreateInstanceOfInt32(rndGen);
+ long myInt64 = PrimitiveCreator.CreateInstanceOfInt64(rndGen);
+ sbyte mySByte = PrimitiveCreator.CreateInstanceOfSByte(rndGen);
+ float mySingle = PrimitiveCreator.CreateInstanceOfSingle(rndGen);
+ string myString = PrimitiveCreator.CreateInstanceOfString(rndGen, 20, null);
+ ushort myUInt16 = PrimitiveCreator.CreateInstanceOfUInt16(rndGen);
+ uint myUInt32 = PrimitiveCreator.CreateInstanceOfUInt32(rndGen);
+ ulong myUInt64 = PrimitiveCreator.CreateInstanceOfUInt64(rndGen);
+ JsonArray myArray = new JsonArray { myBool, myByte, myDatetime, myDateTimeOffset, myDecimal, myDouble, myInt16, myInt32, myInt64, mySByte, mySingle, myString, myUInt16, myUInt32, myUInt64 };
+ JsonArray myArrayLevel2 = new JsonArray { myArray, myArray, myArray };
+ JsonArray myArrayLevel3 = new JsonArray { myArrayLevel2, myArrayLevel2, myArrayLevel2 };
+ JsonArray myArrayLevel4 = new JsonArray { myArrayLevel3, myArrayLevel3, myArrayLevel3 };
+ JsonArray myArrayLevel5 = new JsonArray { myArrayLevel4, myArrayLevel4, myArrayLevel4 };
+ JsonArray myArrayLevel6 = new JsonArray { myArrayLevel5, myArrayLevel5, myArrayLevel5 };
+ JsonArray myArrayLevel7 = new JsonArray { myArrayLevel6, myArrayLevel6, myArrayLevel6 };
+
+ JsonArray sourceJson = BuildJsonArrayinSequence1(myBool, myByte, myDatetime, myDateTimeOffset, myDecimal, myDouble, myInt16, myInt32, myInt64, mySByte, mySingle, myString, myUInt16, myUInt32, myUInt64, myArray, myArrayLevel2, myArrayLevel3, myArrayLevel4, myArrayLevel5, myArrayLevel6, myArrayLevel7);
+
+ newOrderJson = BuildJsonArrayinSequence2(myBool, myByte, myDatetime, myDateTimeOffset, myDecimal, myDouble, myInt16, myInt32, myInt64, mySByte, mySingle, myString, myUInt16, myUInt32, myUInt64, myArray, myArrayLevel2, myArrayLevel3, myArrayLevel4, myArrayLevel5, myArrayLevel6, myArrayLevel7);
+
+ return sourceJson;
+ }
+
+ public static JsonValue CreateDeepLevelJsonValue()
+ {
+ int seed = Environment.TickCount;
+ Log.Info("Seed: {0}", seed);
+ Random rndGen = new Random(seed);
+
+ bool myBool = PrimitiveCreator.CreateInstanceOfBoolean(rndGen);
+ byte myByte = PrimitiveCreator.CreateInstanceOfByte(rndGen);
+ DateTime myDatetime = PrimitiveCreator.CreateInstanceOfDateTime(rndGen);
+ DateTimeOffset myDateTimeOffset = PrimitiveCreator.CreateInstanceOfDateTimeOffset(rndGen);
+ decimal myDecimal = PrimitiveCreator.CreateInstanceOfDecimal(rndGen);
+ double myDouble = PrimitiveCreator.CreateInstanceOfDouble(rndGen);
+ short myInt16 = PrimitiveCreator.CreateInstanceOfInt16(rndGen);
+ int myInt32 = PrimitiveCreator.CreateInstanceOfInt32(rndGen);
+ long myInt64 = PrimitiveCreator.CreateInstanceOfInt64(rndGen);
+ sbyte mySByte = PrimitiveCreator.CreateInstanceOfSByte(rndGen);
+ float mySingle = PrimitiveCreator.CreateInstanceOfSingle(rndGen);
+ string myString = PrimitiveCreator.CreateInstanceOfString(rndGen, 20, null);
+ ushort myUInt16 = PrimitiveCreator.CreateInstanceOfUInt16(rndGen);
+ uint myUInt32 = PrimitiveCreator.CreateInstanceOfUInt32(rndGen);
+ ulong myUInt64 = PrimitiveCreator.CreateInstanceOfUInt64(rndGen);
+ JsonArray myArray = new JsonArray { myBool, myByte, myDatetime, myDateTimeOffset, myDecimal, myDouble, myInt16, myInt32, myInt64, mySByte, mySingle, myString, myUInt16, myUInt32, myUInt64 };
+ JsonArray myArrayLevel2 = new JsonArray { myArray, myArray, myArray };
+ JsonArray myArrayLevel3 = new JsonArray { myArrayLevel2, myArrayLevel2, myArrayLevel2 };
+ JsonArray myArrayLevel4 = new JsonArray { myArrayLevel3, myArrayLevel3, myArrayLevel3 };
+ JsonArray myArrayLevel5 = new JsonArray { myArrayLevel4, myArrayLevel4, myArrayLevel4 };
+ JsonArray myArrayLevel6 = new JsonArray { myArrayLevel5, myArrayLevel5, myArrayLevel5 };
+ JsonArray myArrayLevel7 = new JsonArray { myArrayLevel6, myArrayLevel6, myArrayLevel6 };
+
+ JsonArray sourceJson = BuildJsonArrayinSequence1(myBool, myByte, myDatetime, myDateTimeOffset, myDecimal, myDouble, myInt16, myInt32, myInt64, mySByte, mySingle, myString, myUInt16, myUInt32, myUInt64, myArray, myArrayLevel2, myArrayLevel3, myArrayLevel4, myArrayLevel5, myArrayLevel6, myArrayLevel7);
+
+ return sourceJson;
+ }
+
+ public static JsonObject CreateRandomPopulatedJsonObject(int seed, int length)
+ {
+ JsonObject myObject;
+
+ myObject = new JsonObject(
+ new Dictionary<string, JsonValue>()
+ {
+ { "Name", "myArray" },
+ { "Index", 1 }
+ });
+
+ for (int i = myObject.Count; i < length / 2; i++)
+ {
+ myObject.Add(PrimitiveCreator.CreateInstanceOfString(new Random(seed + i)), GetRandomJsonPrimitives(seed + (i * 2)));
+ }
+
+ for (int i = myObject.Count; i < length; i++)
+ {
+ myObject.Add(new KeyValuePair<string, JsonValue>(PrimitiveCreator.CreateInstanceOfString(new Random(seed + (i * 10))), GetRandomJsonPrimitives(seed + (i * 20))));
+ }
+
+ return myObject;
+ }
+
+ public static JsonArray CreatePrePopulatedJsonArray(int seed, int length)
+ {
+ JsonArray myObject;
+
+ myObject = new JsonArray(new List<JsonValue>());
+
+ for (int i = myObject.Count; i < length; i++)
+ {
+ myObject.Add(GetRandomJsonPrimitives(seed + i));
+ }
+
+ return myObject;
+ }
+
+ public static JsonObject CreateIndexPopulatedJsonObject(int seed, int length)
+ {
+ JsonObject myObject;
+ myObject = new JsonObject(new Dictionary<string, JsonValue>() { });
+
+ for (int i = myObject.Count; i < length; i++)
+ {
+ myObject.Add(i.ToString(CultureInfo.InvariantCulture), GetRandomJsonPrimitives(seed + i));
+ }
+
+ return myObject;
+ }
+
+ public static JsonValue[] CreatePrePopulatedJsonValueArray(int seed, int length)
+ {
+ JsonValue[] myObject = new JsonValue[length];
+
+ for (int i = 0; i < length; i++)
+ {
+ myObject[i] = GetRandomJsonPrimitives(seed + i);
+ }
+
+ return myObject;
+ }
+
+ public static KeyValuePair<string, JsonValue> CreatePrePopulatedKeyValuePair(int seed)
+ {
+ KeyValuePair<string, JsonValue> myObject = new KeyValuePair<string, JsonValue>(seed.ToString(), GetRandomJsonPrimitives(seed));
+ return myObject;
+ }
+
+ public static List<KeyValuePair<string, JsonValue>> CreatePrePopulatedListofKeyValuePair(int seed, int length)
+ {
+ List<KeyValuePair<string, JsonValue>> myObject = new List<KeyValuePair<string, JsonValue>>();
+
+ for (int i = 0; i < length; i++)
+ {
+ myObject.Add(CreatePrePopulatedKeyValuePair(seed + i));
+ }
+
+ return myObject;
+ }
+
+ public static JsonPrimitive GetRandomJsonPrimitives(int seed)
+ {
+ JsonPrimitive myObject;
+ Random rndGen = new Random(seed);
+
+ int mod = seed % 13;
+ switch (mod)
+ {
+ case 1:
+ myObject = new JsonPrimitive(PrimitiveCreator.CreateInstanceOfBoolean(rndGen));
+ break;
+ case 2:
+ myObject = new JsonPrimitive(PrimitiveCreator.CreateInstanceOfByte(rndGen));
+ break;
+ case 3:
+ myObject = new JsonPrimitive(PrimitiveCreator.CreateInstanceOfDateTime(rndGen));
+ break;
+ case 4:
+ myObject = new JsonPrimitive(PrimitiveCreator.CreateInstanceOfDecimal(rndGen));
+ break;
+ case 5:
+ myObject = new JsonPrimitive(PrimitiveCreator.CreateInstanceOfInt16(rndGen));
+ break;
+ case 6:
+ myObject = new JsonPrimitive(PrimitiveCreator.CreateInstanceOfInt32(rndGen));
+ break;
+ case 7:
+ myObject = new JsonPrimitive(PrimitiveCreator.CreateInstanceOfInt64(rndGen));
+ break;
+ case 8:
+ myObject = new JsonPrimitive(PrimitiveCreator.CreateInstanceOfSByte(rndGen));
+ break;
+ case 9:
+ myObject = new JsonPrimitive(PrimitiveCreator.CreateInstanceOfSingle(rndGen));
+ break;
+ case 10:
+ string temp;
+ do
+ {
+ temp = PrimitiveCreator.CreateInstanceOfString(rndGen);
+ }
+ while (temp == null);
+
+ myObject = new JsonPrimitive(temp);
+ break;
+ case 11:
+ myObject = new JsonPrimitive(PrimitiveCreator.CreateInstanceOfUInt16(rndGen));
+ break;
+ case 12:
+ myObject = new JsonPrimitive(PrimitiveCreator.CreateInstanceOfUInt32(rndGen));
+ break;
+ default:
+ myObject = new JsonPrimitive(PrimitiveCreator.CreateInstanceOfUInt64(rndGen));
+ break;
+ }
+
+ return myObject;
+ }
+
+ public static string GetUniqueNonNullInstanceOfString(int seed, JsonObject sourceJson)
+ {
+ string retValue = String.Empty;
+ Random rndGen = new Random(seed);
+ do
+ {
+ retValue = PrimitiveCreator.CreateInstanceOfString(rndGen);
+ }
+ while (retValue == null || sourceJson.Keys.Contains(retValue));
+
+ return retValue;
+ }
+
+ public static JsonPrimitive GetUniqueValue(int seed, JsonObject sourceJson)
+ {
+ JsonPrimitive newValue;
+ int i = 0;
+ do
+ {
+ newValue = SpecialJsonValueHelper.GetRandomJsonPrimitives(seed + i);
+ i++;
+ }
+ while (sourceJson.ToString().IndexOf(newValue.ToString()) > 0);
+
+ return newValue;
+ }
+
+ private static JsonArray BuildJsonArrayinSequence2(bool myBool, byte myByte, DateTime myDatetime, DateTimeOffset myDateTimeOffset, decimal myDecimal, double myDouble, short myInt16, int myInt32, long myInt64, sbyte mySByte, float mySingle, string myString, ushort myUInt16, uint myUInt32, ulong myUInt64, JsonArray myArray, JsonArray myArrayLevel2, JsonArray myArrayLevel3, JsonArray myArrayLevel4, JsonArray myArrayLevel5, JsonArray myArrayLevel6, JsonArray myArrayLevel7)
+ {
+ JsonArray newOrderJson;
+ newOrderJson = new JsonArray
+ {
+ new JsonObject { { "Name", "myArray" }, { "Index", 1 }, { "Obj", myArray } },
+ new JsonObject { { "Name", "myArrayLevel2" }, { "Index", 2 }, { "Obj", myArrayLevel2 } },
+ new JsonObject { { "Name", "myArrayLevel2" }, { "Index", 2 }, { "Obj", myArrayLevel2 } },
+ new JsonObject { { "Name", "myUInt32" }, { "Index", 21 }, { "Obj", myUInt32 } },
+ new JsonObject { { "Name", "myUInt32" }, { "Index", 21 }, { "Obj", myUInt32 } },
+ new JsonObject { { "Name", "myArrayLevel3" }, { "Index", 3 }, { "Obj", myArrayLevel3 } },
+ new JsonObject { { "Name", "myArrayLevel4" }, { "Index", 4 }, { "Obj", myArrayLevel4 } },
+ new JsonObject { { "Name", "myArrayLevel4" }, { "Index", 4 }, { "Obj", myArrayLevel4 } },
+ new JsonObject { { "Name", "myArrayLevel5" }, { "Index", 5 }, { "Obj", myArrayLevel5 } },
+ new JsonObject { { "Name", "myArrayLevel3" }, { "Index", 3 }, { "Obj", myArrayLevel3 } },
+ new JsonObject { { "Name", "myInt64" }, { "Index", 16 }, { "Obj", myInt64 } },
+ new JsonObject { { "Name", "myArrayLevel5" }, { "Index", 5 }, { "Obj", myArrayLevel5 } },
+ new JsonObject { { "Name", "myArrayLevel5" }, { "Index", 5 }, { "Obj", myArrayLevel5 } },
+ new JsonObject { { "Name", "myArrayLevel4" }, { "Index", 4 }, { "Obj", myArrayLevel4 } },
+ new JsonObject { { "Name", "myArrayLevel6" }, { "Index", 6 }, { "Obj", myArrayLevel6 } },
+ new JsonObject { { "Name", "myInt64" }, { "Index", 16 }, { "Obj", myInt64 } },
+ new JsonObject { { "Name", "myArrayLevel5" }, { "Index", 5 }, { "Obj", myArrayLevel5 } },
+ new JsonObject { { "Name", "myArrayLevel5" }, { "Index", 5 }, { "Obj", myArrayLevel5 } },
+ new JsonObject { { "Name", "myArrayLevel7" }, { "Index", 7 }, { "Obj", myArrayLevel7 } },
+ new JsonObject { { "Name", "myDecimal" }, { "Index", 12 }, { "Obj", myDecimal } },
+ new JsonObject { { "Name", "myArrayLevel7" }, { "Index", 7 }, { "Obj", myArrayLevel7 } },
+ new JsonObject { { "Name", "myArrayLevel6" }, { "Index", 6 }, { "Obj", myArrayLevel6 } },
+ new JsonObject { { "Name", "myArrayLevel4" }, { "Index", 4 }, { "Obj", myArrayLevel4 } },
+ new JsonObject { { "Name", "myArrayLevel6" }, { "Index", 6 }, { "Obj", myArrayLevel6 } },
+ new JsonObject { { "Name", "myArrayLevel7" }, { "Index", 7 }, { "Obj", myArrayLevel7 } },
+ new JsonObject { { "Name", "myBool" }, { "Index", 8 }, { "Obj", myBool } },
+ new JsonObject { { "Name", "myByte" }, { "Index", 9 }, { "Obj", myByte } },
+ null,
+ new JsonObject { { "Name", "myDecimal" }, { "Index", 12 }, { "Obj", myDecimal } },
+ new JsonObject { { "Name", "myArrayLevel7" }, { "Index", 7 }, { "Obj", myArrayLevel7 } },
+ new JsonObject { { "Name", "myByte" }, { "Index", 9 }, { "Obj", myByte } },
+ new JsonObject { { "Name", "myArrayLevel7" }, { "Index", 7 }, { "Obj", myArrayLevel7 } },
+ new JsonObject { { "Name", "myArrayLevel6" }, { "Index", 6 }, { "Obj", myArrayLevel6 } },
+ new JsonObject { { "Name", "myDatetime" }, { "Index", 10 }, { "Obj", myDatetime } },
+ new JsonObject { { "Name", "myArrayLevel6" }, { "Index", 6 }, { "Obj", myArrayLevel6 } },
+ new JsonObject { { "Name", "myDatetime" }, { "Index", 10 }, { "Obj", myDatetime } },
+ new JsonObject { { "Name", "myDateTimeOffset" }, { "Index", 11 }, { "Obj", myDateTimeOffset } },
+ new JsonObject { { "Name", "myArrayLevel7" }, { "Index", 7 }, { "Obj", myArrayLevel7 } },
+ new JsonObject { { "Name", "myUInt16" }, { "Index", 20 }, { "Obj", myUInt16 } },
+ new JsonObject { { "Name", "myDateTimeOffset" }, { "Index", 11 }, { "Obj", myDateTimeOffset } },
+ new JsonObject { { "Name", "myDecimal" }, { "Index", 12 }, { "Obj", myDecimal } },
+ new JsonObject { { "Name", "myInt32" }, { "Index", 15 }, { "Obj", myInt32 } },
+ new JsonObject { { "Name", "mySByte" }, { "Index", 17 }, { "Obj", mySByte } },
+ new JsonObject { { "Name", "myDecimal" }, { "Index", 12 }, { "Obj", myDecimal } },
+ new JsonObject { { "Name", "myDouble" }, { "Index", 13 }, { "Obj", myDouble } },
+ new JsonObject { { "Name", "myInt16" }, { "Index", 14 }, { "Obj", myInt16 } },
+ new JsonObject { { "Name", "myString" }, { "Index", 19 }, { "Obj", myString } },
+ new JsonObject { { "Name", "myUInt16" }, { "Index", 20 }, { "Obj", myUInt16 } },
+ new JsonObject { { "Name", "myUInt16" }, { "Index", 20 }, { "Obj", myUInt16 } },
+ new JsonObject { { "Name", "myArrayLevel7" }, { "Index", 7 }, { "Obj", myArrayLevel7 } },
+ new JsonObject { { "Name", "myUInt32" }, { "Index", 21 }, { "Obj", myUInt32 } },
+ new JsonObject { { "Name", "myInt16" }, { "Index", 14 }, { "Obj", myInt16 } },
+ new JsonObject { { "Name", "myInt32" }, { "Index", 15 }, { "Obj", myInt32 } },
+ new JsonObject { { "Name", "mySByte" }, { "Index", 17 }, { "Obj", mySByte } },
+ new JsonObject { { "Name", "myDateTimeOffset" }, { "Index", 11 }, { "Obj", myDateTimeOffset } },
+ new JsonObject { { "Name", "myArrayLevel6" }, { "Index", 6 }, { "Obj", myArrayLevel6 } },
+ new JsonObject { { "Name", "myDatetime" }, { "Index", 10 }, { "Obj", myDatetime } },
+ new JsonObject { { "Name", "mySByte" }, { "Index", 17 }, { "Obj", mySByte } },
+ new JsonObject { { "Name", "myInt32" }, { "Index", 15 }, { "Obj", myInt32 } },
+ new JsonObject { { "Name", "myInt64" }, { "Index", 16 }, { "Obj", myInt64 } },
+ new JsonObject { { "Name", "myUInt32" }, { "Index", 21 }, { "Obj", myUInt32 } },
+ new JsonObject { { "Name", "myArrayLevel3" }, { "Index", 3 }, { "Obj", myArrayLevel3 } },
+ new JsonObject { { "Name", "myInt64" }, { "Index", 16 }, { "Obj", myInt64 } },
+ new JsonObject { { "Name", "mySByte" }, { "Index", 17 }, { "Obj", mySByte } },
+ new JsonObject { { "Name", "mySByte" }, { "Index", 17 }, { "Obj", mySByte } },
+ new JsonObject { { "Name", "mySingle" }, { "Index", 18 }, { "Obj", mySingle } },
+ new JsonObject { { "Name", "myDateTimeOffset" }, { "Index", 11 }, { "Obj", myDateTimeOffset } },
+ new JsonObject { { "Name", "myDecimal" }, { "Index", 12 }, { "Obj", myDecimal } },
+ new JsonObject { { "Name", "myString" }, { "Index", 19 }, { "Obj", myString } },
+ new JsonObject { { "Name", "myUInt64" }, { "Index", 22 }, { "Obj", myUInt64 } }
+ };
+ return newOrderJson;
+ }
+
+ private static JsonArray BuildJsonArrayinSequence1(bool myBool, byte myByte, DateTime myDatetime, DateTimeOffset myDateTimeOffset, decimal myDecimal, double myDouble, short myInt16, int myInt32, long myInt64, sbyte mySByte, float mySingle, string myString, ushort myUInt16, uint myUInt32, ulong myUInt64, JsonArray myArray, JsonArray myArrayLevel2, JsonArray myArrayLevel3, JsonArray myArrayLevel4, JsonArray myArrayLevel5, JsonArray myArrayLevel6, JsonArray myArrayLevel7)
+ {
+ JsonArray sourceJson = new JsonArray
+ {
+ new JsonObject { { "Name", "myArray" }, { "Index", 1 }, { "Obj", myArray } },
+ new JsonObject { { "Name", "myArrayLevel2" }, { "Index", 2 }, { "Obj", myArrayLevel2 } },
+ new JsonObject { { "Name", "myArrayLevel2" }, { "Index", 2 }, { "Obj", myArrayLevel2 } },
+ new JsonObject { { "Name", "myArrayLevel3" }, { "Index", 3 }, { "Obj", myArrayLevel3 } },
+ new JsonObject { { "Name", "myArrayLevel3" }, { "Index", 3 }, { "Obj", myArrayLevel3 } },
+ new JsonObject { { "Name", "myArrayLevel3" }, { "Index", 3 }, { "Obj", myArrayLevel3 } },
+ new JsonObject { { "Name", "myArrayLevel4" }, { "Index", 4 }, { "Obj", myArrayLevel4 } },
+ new JsonObject { { "Name", "myArrayLevel4" }, { "Index", 4 }, { "Obj", myArrayLevel4 } },
+ new JsonObject { { "Name", "myArrayLevel4" }, { "Index", 4 }, { "Obj", myArrayLevel4 } },
+ new JsonObject { { "Name", "myArrayLevel4" }, { "Index", 4 }, { "Obj", myArrayLevel4 } },
+ new JsonObject { { "Name", "myArrayLevel5" }, { "Index", 5 }, { "Obj", myArrayLevel5 } },
+ new JsonObject { { "Name", "myArrayLevel5" }, { "Index", 5 }, { "Obj", myArrayLevel5 } },
+ new JsonObject { { "Name", "myArrayLevel5" }, { "Index", 5 }, { "Obj", myArrayLevel5 } },
+ new JsonObject { { "Name", "myArrayLevel5" }, { "Index", 5 }, { "Obj", myArrayLevel5 } },
+ new JsonObject { { "Name", "myArrayLevel5" }, { "Index", 5 }, { "Obj", myArrayLevel5 } },
+ new JsonObject { { "Name", "myArrayLevel6" }, { "Index", 6 }, { "Obj", myArrayLevel6 } },
+ new JsonObject { { "Name", "myArrayLevel6" }, { "Index", 6 }, { "Obj", myArrayLevel6 } },
+ new JsonObject { { "Name", "myArrayLevel6" }, { "Index", 6 }, { "Obj", myArrayLevel6 } },
+ new JsonObject { { "Name", "myArrayLevel6" }, { "Index", 6 }, { "Obj", myArrayLevel6 } },
+ new JsonObject { { "Name", "myArrayLevel6" }, { "Index", 6 }, { "Obj", myArrayLevel6 } },
+ new JsonObject { { "Name", "myArrayLevel6" }, { "Index", 6 }, { "Obj", myArrayLevel6 } },
+ new JsonObject { { "Name", "myArrayLevel7" }, { "Index", 7 }, { "Obj", myArrayLevel7 } },
+ new JsonObject { { "Name", "myArrayLevel7" }, { "Index", 7 }, { "Obj", myArrayLevel7 } },
+ new JsonObject { { "Name", "myArrayLevel7" }, { "Index", 7 }, { "Obj", myArrayLevel7 } },
+ new JsonObject { { "Name", "myArrayLevel7" }, { "Index", 7 }, { "Obj", myArrayLevel7 } },
+ new JsonObject { { "Name", "myArrayLevel7" }, { "Index", 7 }, { "Obj", myArrayLevel7 } },
+ new JsonObject { { "Name", "myArrayLevel7" }, { "Index", 7 }, { "Obj", myArrayLevel7 } },
+ new JsonObject { { "Name", "myArrayLevel7" }, { "Index", 7 }, { "Obj", myArrayLevel7 } },
+ new JsonObject { { "Name", "myBool" }, { "Index", 8 }, { "Obj", myBool } },
+ new JsonObject { { "Name", "myByte" }, { "Index", 9 }, { "Obj", myByte } },
+ null,
+ new JsonObject { { "Name", "myByte" }, { "Index", 9 }, { "Obj", myByte } },
+ new JsonObject { { "Name", "myDatetime" }, { "Index", 10 }, { "Obj", myDatetime } },
+ new JsonObject { { "Name", "myDatetime" }, { "Index", 10 }, { "Obj", myDatetime } },
+ new JsonObject { { "Name", "myDatetime" }, { "Index", 10 }, { "Obj", myDatetime } },
+ new JsonObject { { "Name", "myDateTimeOffset" }, { "Index", 11 }, { "Obj", myDateTimeOffset } },
+ new JsonObject { { "Name", "myDateTimeOffset" }, { "Index", 11 }, { "Obj", myDateTimeOffset } },
+ new JsonObject { { "Name", "myDateTimeOffset" }, { "Index", 11 }, { "Obj", myDateTimeOffset } },
+ new JsonObject { { "Name", "myDateTimeOffset" }, { "Index", 11 }, { "Obj", myDateTimeOffset } },
+ new JsonObject { { "Name", "myDecimal" }, { "Index", 12 }, { "Obj", myDecimal } },
+ new JsonObject { { "Name", "myDecimal" }, { "Index", 12 }, { "Obj", myDecimal } },
+ new JsonObject { { "Name", "myDecimal" }, { "Index", 12 }, { "Obj", myDecimal } },
+ new JsonObject { { "Name", "myDecimal" }, { "Index", 12 }, { "Obj", myDecimal } },
+ new JsonObject { { "Name", "myDecimal" }, { "Index", 12 }, { "Obj", myDecimal } },
+ new JsonObject { { "Name", "myDouble" }, { "Index", 13 }, { "Obj", myDouble } },
+ new JsonObject { { "Name", "myInt16" }, { "Index", 14 }, { "Obj", myInt16 } },
+ new JsonObject { { "Name", "myInt16" }, { "Index", 14 }, { "Obj", myInt16 } },
+ new JsonObject { { "Name", "myInt32" }, { "Index", 15 }, { "Obj", myInt32 } },
+ new JsonObject { { "Name", "myInt32" }, { "Index", 15 }, { "Obj", myInt32 } },
+ new JsonObject { { "Name", "myInt32" }, { "Index", 15 }, { "Obj", myInt32 } },
+ new JsonObject { { "Name", "myInt64" }, { "Index", 16 }, { "Obj", myInt64 } },
+ new JsonObject { { "Name", "myInt64" }, { "Index", 16 }, { "Obj", myInt64 } },
+ new JsonObject { { "Name", "myInt64" }, { "Index", 16 }, { "Obj", myInt64 } },
+ new JsonObject { { "Name", "myInt64" }, { "Index", 16 }, { "Obj", myInt64 } },
+ new JsonObject { { "Name", "mySByte" }, { "Index", 17 }, { "Obj", mySByte } },
+ new JsonObject { { "Name", "mySByte" }, { "Index", 17 }, { "Obj", mySByte } },
+ new JsonObject { { "Name", "mySByte" }, { "Index", 17 }, { "Obj", mySByte } },
+ new JsonObject { { "Name", "mySByte" }, { "Index", 17 }, { "Obj", mySByte } },
+ new JsonObject { { "Name", "mySByte" }, { "Index", 17 }, { "Obj", mySByte } },
+ new JsonObject { { "Name", "mySingle" }, { "Index", 18 }, { "Obj", mySingle } },
+ new JsonObject { { "Name", "myString" }, { "Index", 19 }, { "Obj", myString } },
+ new JsonObject { { "Name", "myString" }, { "Index", 19 }, { "Obj", myString } },
+ new JsonObject { { "Name", "myUInt16" }, { "Index", 20 }, { "Obj", myUInt16 } },
+ new JsonObject { { "Name", "myUInt16" }, { "Index", 20 }, { "Obj", myUInt16 } },
+ new JsonObject { { "Name", "myUInt16" }, { "Index", 20 }, { "Obj", myUInt16 } },
+ new JsonObject { { "Name", "myUInt32" }, { "Index", 21 }, { "Obj", myUInt32 } },
+ new JsonObject { { "Name", "myUInt32" }, { "Index", 21 }, { "Obj", myUInt32 } },
+ new JsonObject { { "Name", "myUInt32" }, { "Index", 21 }, { "Obj", myUInt32 } },
+ new JsonObject { { "Name", "myUInt32" }, { "Index", 21 }, { "Obj", myUInt32 } },
+ new JsonObject { { "Name", "myUInt64" }, { "Index", 22 }, { "Obj", myUInt64 } }
+ };
+ return sourceJson;
+ }
+ }
+}
diff --git a/test/System.Json.Test.Integration/JsonValueTests.cs b/test/System.Json.Test.Integration/JsonValueTests.cs
new file mode 100644
index 00000000..89cc169c
--- /dev/null
+++ b/test/System.Json.Test.Integration/JsonValueTests.cs
@@ -0,0 +1,380 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Text;
+using Xunit;
+using Xunit.Extensions;
+
+namespace System.Json
+{
+ /// <summary>
+ /// JsonValue unit tests
+ /// </summary>
+ public class JsonValueTests
+ {
+
+ public static IEnumerable<object[]> StreamLoadingTestData
+ {
+ get
+ {
+ bool[] useSeekableStreams = new bool[] { true, false };
+ Dictionary<string, Encoding> allEncodings = new Dictionary<string, Encoding>
+ {
+ { "UTF8, no BOM", new UTF8Encoding(false) },
+ { "Unicode, no BOM", new UnicodeEncoding(false, false) },
+ { "BigEndianUnicode, no BOM", new UnicodeEncoding(true, false) },
+ };
+
+ string[] jsonStrings = { "[1, 2, null, false, {\"foo\": 1, \"bar\":true, \"baz\":null}, 1.23e+56]", "4" };
+
+ foreach (string jsonString in jsonStrings)
+ {
+ foreach (bool useSeekableStream in useSeekableStreams)
+ {
+ foreach (var kvp in allEncodings)
+ {
+ yield return new object[] { jsonString, useSeekableStream, kvp.Key, kvp.Value };
+ }
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Tests for <see cref="JsonValue.Load(Stream)"/>.
+ /// </summary>
+ [Theory]
+ [PropertyData("StreamLoadingTestData")]
+ public void StreamLoading(string jsonString, bool useSeekableStream, string encodingName, Encoding encoding)
+ {
+ using (MemoryStream ms = new MemoryStream())
+ {
+ StreamWriter sw = new StreamWriter(ms, encoding);
+ sw.Write(jsonString);
+ sw.Flush();
+ Log.Info("[{0}] {1}: size of the json stream: {2}", useSeekableStream ? "seekable" : "non-seekable", encodingName, ms.Position);
+ ms.Position = 0;
+ JsonValue parsed = JsonValue.Parse(jsonString);
+ JsonValue loaded = useSeekableStream ? JsonValue.Load(ms) : JsonValue.Load(new NonSeekableStream(ms));
+ using (StringReader sr = new StringReader(jsonString))
+ {
+ JsonValue loadedFromTextReader = JsonValue.Load(sr);
+ Assert.Equal(parsed.ToString(), loaded.ToString());
+ Assert.Equal(parsed.ToString(), loadedFromTextReader.ToString());
+ }
+ }
+ }
+
+ [Fact]
+ public void ZeroedStreamLoadingThrowsFormatException()
+ {
+ ExpectException<FormatException>(delegate
+ {
+ using (MemoryStream ms = new MemoryStream(new byte[10]))
+ {
+ JsonValue.Load(ms);
+ }
+ });
+ }
+
+ /// <summary>
+ /// Tests for handling with escaped characters.
+ /// </summary>
+ [Fact]
+ public void EscapedCharacters()
+ {
+ string str = null;
+ JsonValue value = null;
+ str = (string)value;
+ Assert.Null(str);
+ value = "abc\b\t\r\u1234\uDC80\uDB11def\\\0ghi";
+ str = (string)value;
+ Assert.Equal("\"abc\\u0008\\u0009\\u000d\u1234\\udc80\\udb11def\\\\\\u0000ghi\"", value.ToString());
+ value = '\u0000';
+ str = (string)value;
+ Assert.Equal("\u0000", str);
+ }
+
+ /// <summary>
+ /// Tests for JSON objects with the special '__type' object member.
+ /// </summary>
+ [Fact]
+ public void TypeHintAttributeTests()
+ {
+ string json = "{\"__type\":\"TypeHint\",\"a\":123}";
+ JsonValue jv = JsonValue.Parse(json);
+ string newJson = jv.ToString();
+ Assert.Equal(json, newJson);
+
+ json = "{\"b\":567,\"__type\":\"TypeHint\",\"a\":123}";
+ jv = JsonValue.Parse(json);
+ newJson = jv.ToString();
+ Assert.Equal(json, newJson);
+
+ json = "[12,{\"__type\":\"TypeHint\",\"a\":123,\"obj\":{\"__type\":\"hint2\",\"b\":333}},null]";
+ jv = JsonValue.Parse(json);
+ newJson = jv.ToString();
+ Assert.Equal(json, newJson);
+ }
+
+ /// <summary>
+ /// Tests for reading JSON with different member names.
+ /// </summary>
+ [Fact]
+ public void ObjectNameTests()
+ {
+ string[] objectNames = new string[]
+ {
+ "simple",
+ "with spaces",
+ "with<>brackets",
+ "",
+ };
+
+ foreach (string objectName in objectNames)
+ {
+ string json = String.Format(CultureInfo.InvariantCulture, "{{\"{0}\":123}}", objectName);
+ JsonValue jv = JsonValue.Parse(json);
+ Assert.Equal(123, jv[objectName].ReadAs<int>());
+ string newJson = jv.ToString();
+ Assert.Equal(json, newJson);
+
+ JsonObject jo = new JsonObject { { objectName, 123 } };
+ Assert.Equal(123, jo[objectName].ReadAs<int>());
+ newJson = jo.ToString();
+ Assert.Equal(json, newJson);
+ }
+
+ ExpectException<FormatException>(() => JsonValue.Parse("{\"nonXmlChar\u0000\":123}"));
+ }
+
+ /// <summary>
+ /// Miscellaneous tests for parsing JSON.
+ /// </summary>
+ [Fact]
+ public void ParseMiscellaneousTest()
+ {
+ string[] jsonValues =
+ {
+ "[]",
+ "[1]",
+ "[1,2,3,[4.1,4.2],5]",
+ "{}",
+ "{\"a\":1}",
+ "{\"a\":1,\"b\":2,\"c\":3,\"d\":4}",
+ "{\"a\":1,\"b\":[2,3],\"c\":3}",
+ "{\"a\":1,\"b\":2,\"c\":[1,2,3,[4.1,4.2],5],\"d\":4}",
+ "{\"a\":1,\"b\":[2.1,2.2],\"c\":3,\"d\":4,\"e\":[4.1,4.2,4.3,[4.41,4.42],4.4],\"f\":5}",
+ "{\"a\":1,\"b\":[2.1,2.2,[[[{\"b1\":2.21}]]],2.3],\"c\":{\"d\":4,\"e\":[4.1,4.2,4.3,[4.41,4.42],4.4],\"f\":5}}"
+ };
+
+ foreach (string json in jsonValues)
+ {
+ JsonValue jv = JsonValue.Parse(json);
+ Log.Info("{0}", jv.ToString());
+
+ string jvstr = jv.ToString();
+ Assert.Equal(json, jvstr);
+ }
+ }
+
+ /// <summary>
+ /// Negative tests for parsing "unbalanced" JSON (i.e., JSON documents which aren't properly closed).
+ /// </summary>
+ [Fact]
+ public void ParseUnbalancedJsonTest()
+ {
+ string[] jsonValues =
+ {
+ "[",
+ "[1,{]",
+ "[1,2,3,{{}}",
+ "}",
+ "{\"a\":}",
+ "{\"a\":1,\"b\":[,\"c\":3,\"d\":4}",
+ "{\"a\":1,\"b\":[2,\"c\":3}",
+ "{\"a\":1,\"b\":[2.1,2.2,\"c\":3,\"d\":4,\"e\":[4.1,4.2,4.3,[4.41,4.42],4.4],\"f\":5}",
+ "{\"a\":1,\"b\":[2.1,2.2,[[[[{\"b1\":2.21}]]],\"c\":{\"d\":4,\"e\":[4.1,4.2,4.3,[4.41,4.42],4.4],\"f\":5}}"
+ };
+
+ foreach (string json in jsonValues)
+ {
+ Log.Info("Testing unbalanced JSON: {0}", json);
+ ExpectException<FormatException>(() => JsonValue.Parse(json));
+ }
+ }
+
+ /// <summary>
+ /// Test for parsing a deeply nested JSON object.
+ /// </summary>
+ [Fact]
+ public void ParseDeeplyNestedJsonObjectString()
+ {
+ StringBuilder builderExpected = new StringBuilder();
+ builderExpected.Append('{');
+ int depth = 10000;
+ for (int i = 0; i < depth; i++)
+ {
+ string key = i.ToString(CultureInfo.InvariantCulture);
+ builderExpected.AppendFormat("\"{0}\":{{", key);
+ }
+
+ for (int i = 0; i < depth + 1; i++)
+ {
+ builderExpected.Append('}');
+ }
+
+ string json = builderExpected.ToString();
+ JsonValue jsonValue = JsonValue.Parse(json);
+ string jvstr = jsonValue.ToString();
+
+ Assert.Equal(json, jvstr);
+ }
+
+ /// <summary>
+ /// Test for parsing a deeply nested JSON array.
+ /// </summary>
+ [Fact]
+ public void ParseDeeplyNestedJsonArrayString()
+ {
+ StringBuilder builderExpected = new StringBuilder();
+ builderExpected.Append('[');
+ int depth = 10000;
+ for (int i = 0; i < depth; i++)
+ {
+ builderExpected.Append('[');
+ }
+
+ for (int i = 0; i < depth + 1; i++)
+ {
+ builderExpected.Append(']');
+ }
+
+ string json = builderExpected.ToString();
+ JsonValue jsonValue = JsonValue.Parse(json);
+ string jvstr = jsonValue.ToString();
+
+ Assert.Equal(json, jvstr);
+ }
+
+ /// <summary>
+ /// Test for parsing a deeply nested JSON graph, containing both objects and arrays.
+ /// </summary>
+ [Fact]
+ public void ParseDeeplyNestedJsonString()
+ {
+ StringBuilder builderExpected = new StringBuilder();
+ builderExpected.Append('{');
+ int depth = 10000;
+ for (int i = 0; i < depth; i++)
+ {
+ string key = i.ToString(CultureInfo.InvariantCulture);
+ builderExpected.AppendFormat("\"{0}\":[{{", key);
+ }
+
+ for (int i = 0; i < depth; i++)
+ {
+ builderExpected.Append("}]");
+ }
+
+ builderExpected.Append('}');
+
+ string json = builderExpected.ToString();
+ JsonValue jsonValue = JsonValue.Parse(json);
+ string jvstr = jsonValue.ToString();
+
+ Assert.Equal(json, jvstr);
+ }
+
+ internal static void ExpectException<T>(Action action) where T : Exception
+ {
+ ExpectException<T>(action, null);
+ }
+
+ internal static void ExpectException<T>(Action action, string partOfExceptionString) where T : Exception
+ {
+ try
+ {
+ action();
+ Assert.False(true, "This should have thrown");
+ }
+ catch (T e)
+ {
+ if (partOfExceptionString != null)
+ {
+ Assert.True(e.Message.Contains(partOfExceptionString));
+ }
+ }
+ }
+
+ internal class NonSeekableStream : Stream
+ {
+ Stream innerStream;
+
+ public NonSeekableStream(Stream innerStream)
+ {
+ this.innerStream = innerStream;
+ }
+
+ public override bool CanRead
+ {
+ get { return true; }
+ }
+
+ public override bool CanSeek
+ {
+ get { return false; }
+ }
+
+ public override bool CanWrite
+ {
+ get { return false; }
+ }
+
+ public override long Position
+ {
+ get
+ {
+ throw new NotSupportedException();
+ }
+
+ set
+ {
+ throw new NotSupportedException();
+ }
+ }
+
+ public override long Length
+ {
+ get
+ {
+ throw new NotSupportedException();
+ }
+ }
+
+ public override void Flush()
+ {
+ }
+
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ return this.innerStream.Read(buffer, offset, count);
+ }
+
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ throw new NotSupportedException();
+ }
+
+ public override void SetLength(long value)
+ {
+ throw new NotSupportedException();
+ }
+
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ throw new NotSupportedException();
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/test/System.Json.Test.Integration/JsonValueUsageTest.cs b/test/System.Json.Test.Integration/JsonValueUsageTest.cs
new file mode 100644
index 00000000..3f2a5dd2
--- /dev/null
+++ b/test/System.Json.Test.Integration/JsonValueUsageTest.cs
@@ -0,0 +1,433 @@
+using System.Collections.Generic;
+using System.Linq;
+using Xunit;
+
+namespace System.Json
+{
+ /// <summary>
+ /// Test class for some scenario usages for <see cref="JsonValue"/> types.
+ /// </summary>
+ public class JsonValueUsageTest
+ {
+ /// <summary>
+ /// Test for consuming <see cref="JsonValue"/> objects in a Linq query.
+ /// </summary>
+ [Fact]
+ public void JLinqSimpleCreationQueryTest()
+ {
+ int seed = 1;
+ Random rndGen = new Random(seed);
+
+ JsonArray sourceJson = new JsonArray
+ {
+ new JsonObject { { "Name", "Alex" }, { "Age", 18 }, { "Birthday", PrimitiveCreator.CreateInstanceOfDateTime(rndGen) } },
+ new JsonObject { { "Name", "Joe" }, { "Age", 19 }, { "Birthday", DateTime.MinValue } },
+ new JsonObject { { "Name", "Chris" }, { "Age", 20 }, { "Birthday", DateTime.Now } },
+ new JsonObject { { "Name", "Jeff" }, { "Age", 21 }, { "Birthday", DateTime.MaxValue } },
+ new JsonObject { { "Name", "Carlos" }, { "Age", 22 }, { "Birthday", PrimitiveCreator.CreateInstanceOfDateTime(rndGen) } },
+ new JsonObject { { "Name", "Mohammad" }, { "Age", 23 }, { "Birthday", PrimitiveCreator.CreateInstanceOfDateTime(rndGen) } },
+ new JsonObject { { "Name", "Sara" }, { "Age", 24 }, { "Birthday", new DateTime(1998, 3, 20) } },
+ new JsonObject { { "Name", "Tomasz" }, { "Age", 25 }, { "Birthday", PrimitiveCreator.CreateInstanceOfDateTime(rndGen) } },
+ new JsonObject { { "Name", "Suwat" }, { "Age", 26 }, { "Birthday", new DateTime(1500, 12, 20) } },
+ new JsonObject { { "Name", "Eugene" }, { "Age", 27 }, { "Birthday", PrimitiveCreator.CreateInstanceOfDateTime(rndGen) } }
+ };
+
+ var adults = from JsonValue adult in sourceJson
+ where (int)adult["Age"] > 21
+ select adult;
+ Log.Info("Team contains: ");
+ int count = 0;
+ foreach (JsonValue adult in adults)
+ {
+ count++;
+ Log.Info((string)adult["Name"]);
+ }
+
+ Assert.Equal(count, 6);
+ }
+
+ /// <summary>
+ /// Test for consuming <see cref="JsonValue"/> arrays in a Linq query.
+ /// </summary>
+ [Fact]
+ public void JLinqSimpleQueryTest()
+ {
+ JsonArray sourceJson = this.CreateArrayOfPeople();
+
+ var adults = from JsonValue adult in sourceJson
+ where (int)adult["Age"] > 21
+ select adult;
+ Log.Info("Team contains: ");
+ int count = 0;
+ foreach (JsonValue adult in adults)
+ {
+ count++;
+ Log.Info((string)adult["Name"]);
+ }
+
+ Assert.Equal(count, 6);
+ }
+
+ /// <summary>
+ /// Test for consuming deep <see cref="JsonValue"/> objects in a Linq query.
+ /// </summary>
+ [Fact]
+ public void JLinqDeepQueryTest()
+ {
+ int seed = 1;
+
+ JsonArray mixedOrderJsonObj;
+ JsonArray myJsonObj = SpecialJsonValueHelper.CreateDeepLevelJsonValuePair(seed, out mixedOrderJsonObj);
+
+ if (myJsonObj != null && mixedOrderJsonObj != null)
+ {
+ bool retValue = true;
+
+ var dict = new Dictionary<string, int>
+ {
+ { "myArray", 1 },
+ { "myArrayLevel2", 2 },
+ { "myArrayLevel3", 3 },
+ { "myArrayLevel4", 4 },
+ { "myArrayLevel5", 5 },
+ { "myArrayLevel6", 6 },
+ { "myArrayLevel7", 7 },
+ { "myBool", 8 },
+ { "myByte", 9 },
+ { "myDatetime", 10 },
+ { "myDateTimeOffset", 11 },
+ { "myDecimal", 12 },
+ { "myDouble", 13 },
+ { "myInt16", 14 },
+ { "myInt32", 15 },
+ { "myInt64", 16 },
+ { "mySByte", 17 },
+ { "mySingle", 18 },
+ { "myString", 19 },
+ { "myUInt16", 20 },
+ { "myUInt32", 21 },
+ { "myUInt64", 22 }
+ };
+
+ foreach (string name in dict.Keys)
+ {
+ if (!this.InternalVerificationViaLinqQuery(myJsonObj, name, dict[name]))
+ {
+ retValue = false;
+ }
+
+ if (!this.InternalVerificationViaLinqQuery(mixedOrderJsonObj, name, dict[name]))
+ {
+ retValue = false;
+ }
+
+ if (!this.CrossJsonValueVerificationOnNameViaLinqQuery(myJsonObj, mixedOrderJsonObj, name))
+ {
+ retValue = false;
+ }
+
+ if (!this.CrossJsonValueVerificationOnIndexViaLinqQuery(myJsonObj, mixedOrderJsonObj, dict[name]))
+ {
+ retValue = false;
+ }
+ }
+
+ Assert.True(retValue, "The JsonValue did not verify as expected!");
+ }
+ else
+ {
+ Assert.True(false, "Failed to create the pair of JsonValues!");
+ }
+ }
+
+ /// <summary>
+ /// Test for consuming <see cref="JsonValue"/> objects in a Linq query using the dynamic notation.
+ /// </summary>
+ [Fact]
+ public void LinqToDynamicJsonArrayTest()
+ {
+ JsonValue people = this.CreateArrayOfPeople();
+
+ var match = from person in people select person;
+ Assert.True(match.Count() == people.Count, "IEnumerable returned different number of elements that JsonArray contains");
+
+ int sum = 0;
+ foreach (KeyValuePair<string, JsonValue> kv in match)
+ {
+ sum += Int32.Parse(kv.Key);
+ }
+
+ Assert.True(sum == (people.Count * (people.Count - 1) / 2), "Not all elements of the array were enumerated exactly once");
+
+ match = from person in people
+ where person.Value.AsDynamic().Name.ReadAs<string>().StartsWith("S")
+ && person.Value.AsDynamic().Age.ReadAs<int>() > 20
+ select person;
+ Assert.True(match.Count() == 2, "Number of matches was expected to be 2 but was " + match.Count());
+ }
+
+ /// <summary>
+ /// Test for consuming <see cref="JsonValue"/> objects in a Linq query.
+ /// </summary>
+ [Fact]
+ public void LinqToJsonObjectTest()
+ {
+ JsonValue person = this.CreateArrayOfPeople()[0];
+ var match = from nameValue in person select nameValue;
+ Assert.True(match.Count() == 3, "IEnumerable of JsonObject returned a different number of elements than there are name value pairs in the JsonObject" + match.Count());
+
+ List<string> missingNames = new List<string>(new string[] { "Name", "Age", "Birthday" });
+ foreach (KeyValuePair<string, JsonValue> kv in match)
+ {
+ Assert.Equal(person[kv.Key], kv.Value);
+ missingNames.Remove(kv.Key);
+ }
+
+ Assert.True(missingNames.Count == 0, "Not all JsonObject properties were present in the enumeration");
+ }
+
+ /// <summary>
+ /// Test for consuming <see cref="JsonValue"/> objects in a Linq query.
+ /// </summary>
+ [Fact]
+ public void LinqToJsonObjectAsAssociativeArrayTest()
+ {
+ JsonValue gameScores = new JsonObject(new Dictionary<string, JsonValue>()
+ {
+ { "tomek", 12 },
+ { "suwat", 27 },
+ { "carlos", 127 },
+ { "miguel", 57 },
+ { "henrik", 2 },
+ { "joe", 15 }
+ });
+
+ var match = from score in gameScores
+ where score.Key.Contains("o") && score.Value.ReadAs<int>() > 100
+ select score;
+ Assert.True(match.Count() == 1, "Incorrect number of matching game scores");
+ }
+
+ /// <summary>
+ /// Test for consuming <see cref="JsonPrimitive"/> objects in a Linq query.
+ /// </summary>
+ [Fact]
+ public void LinqToJsonPrimitiveTest()
+ {
+ JsonValue primitive = 12;
+
+ var match = from m in primitive select m;
+ KeyValuePair<string, JsonValue>[] kv = match.ToArray();
+ Assert.True(kv.Length == 0);
+ }
+
+ /// <summary>
+ /// Test for consuming <see cref="JsonValue"/> objects with <see cref="JsonType">JsonType.Default</see> in a Linq query.
+ /// </summary>
+ [Fact]
+ public void LinqToJsonUndefinedTest()
+ {
+ JsonValue primitive = 12;
+
+ var match = from m in primitive.ValueOrDefault("idontexist")
+ select m;
+ Assert.True(match.Count() == 0);
+ }
+
+ /// <summary>
+ /// Test for consuming calling <see cref="JsonValue.ReadAs{T}(T)"/> in a Linq query.
+ /// </summary>
+ [Fact]
+ public void LinqToDynamicJsonUndefinedWithFallbackTest()
+ {
+ JsonValue people = this.CreateArrayOfPeople();
+
+ var match = from person in people
+ where person.Value.AsDynamic().IDontExist.IAlsoDontExist.ReadAs<int>(5) > 2
+ select person;
+ Assert.True(match.Count() == people.Count, "Number of matches was expected to be " + people.Count + " but was " + match.Count());
+
+ match = from person in people
+ where person.Value.AsDynamic().Age.ReadAs<int>(1) < 21
+ select person;
+ Assert.True(match.Count() == 3);
+ }
+
+ private JsonArray CreateArrayOfPeople()
+ {
+ int seed = 1;
+ Random rndGen = new Random(seed);
+ return new JsonArray(new List<JsonValue>()
+ {
+ new JsonObject(new Dictionary<string, JsonValue>()
+ {
+ { "Name", "Alex" },
+ { "Age", 18 },
+ { "Birthday", PrimitiveCreator.CreateInstanceOfDateTime(rndGen) }
+ }),
+ new JsonObject(new Dictionary<string, JsonValue>()
+ {
+ { "Name", "Joe" },
+ { "Age", 19 },
+ { "Birthday", DateTime.MinValue }
+ }),
+ new JsonObject(new Dictionary<string, JsonValue>()
+ {
+ { "Name", "Chris" },
+ { "Age", 20 },
+ { "Birthday", DateTime.Now }
+ }),
+ new JsonObject(new Dictionary<string, JsonValue>()
+ {
+ { "Name", "Jeff" },
+ { "Age", 21 },
+ { "Birthday", DateTime.MaxValue }
+ }),
+ new JsonObject(new Dictionary<string, JsonValue>()
+ {
+ { "Name", "Carlos" },
+ { "Age", 22 },
+ { "Birthday", PrimitiveCreator.CreateInstanceOfDateTime(rndGen) }
+ }),
+ new JsonObject(new Dictionary<string, JsonValue>()
+ {
+ { "Name", "Mohammad" },
+ { "Age", 23 },
+ { "Birthday", PrimitiveCreator.CreateInstanceOfDateTime(rndGen) }
+ }),
+ new JsonObject(new Dictionary<string, JsonValue>()
+ {
+ { "Name", "Sara" },
+ { "Age", 24 },
+ { "Birthday", new DateTime(1998, 3, 20) }
+ }),
+ new JsonObject(new Dictionary<string, JsonValue>()
+ {
+ { "Name", "Tomasz" },
+ { "Age", 25 },
+ { "Birthday", PrimitiveCreator.CreateInstanceOfDateTime(rndGen) }
+ }),
+ new JsonObject(new Dictionary<string, JsonValue>()
+ {
+ { "Name", "Suwat" },
+ { "Age", 26 },
+ { "Birthday", new DateTime(1500, 12, 20) }
+ }),
+ new JsonObject(new Dictionary<string, JsonValue>()
+ {
+ { "Name", "Eugene" },
+ { "Age", 27 },
+ { "Birthday", PrimitiveCreator.CreateInstanceOfDateTime(rndGen) }
+ })
+ });
+ }
+
+ private bool InternalVerificationViaLinqQuery(JsonArray sourceJson, string name, int index)
+ {
+ var itemsByName = from JsonValue itemByName in sourceJson
+ where (itemByName != null && (string)itemByName["Name"] == name)
+ select itemByName;
+ int countByName = 0;
+ foreach (JsonValue a in itemsByName)
+ {
+ countByName++;
+ }
+
+ Log.Info("Collection contains: " + countByName + " item By Name " + name);
+
+ var itemsByIndex = from JsonValue itemByIndex in sourceJson
+ where (itemByIndex != null && (int)itemByIndex["Index"] == index)
+ select itemByIndex;
+ int countByIndex = 0;
+ foreach (JsonValue a in itemsByIndex)
+ {
+ countByIndex++;
+ }
+
+ Log.Info("Collection contains: " + countByIndex + " item By Index " + index);
+
+ if (countByIndex != countByName)
+ {
+ Log.Info("Count by Name = " + countByName + "; Count by Index = " + countByIndex);
+ Log.Info("The number of items matching the provided Name does NOT equal to that matching the provided Index, The two JsonValues are not equal!");
+ return false;
+ }
+ else
+ {
+ return true;
+ }
+ }
+
+ private bool CrossJsonValueVerificationOnNameViaLinqQuery(JsonArray sourceJson, JsonArray newJson, string name)
+ {
+ var itemsByName = from JsonValue itemByName in sourceJson
+ where (itemByName != null && (string)itemByName["Name"] == name)
+ select itemByName;
+ int countByName = 0;
+ foreach (JsonValue a in itemsByName)
+ {
+ countByName++;
+ }
+
+ Log.Info("Original Collection contains: " + countByName + " item By Name " + name);
+
+ var newItemsByName = from JsonValue newItemByName in newJson
+ where (newItemByName != null && (string)newItemByName["Name"] == name)
+ select newItemByName;
+ int newcountByName = 0;
+ foreach (JsonValue a in newItemsByName)
+ {
+ newcountByName++;
+ }
+
+ Log.Info("New Collection contains: " + newcountByName + " item By Name " + name);
+
+ if (countByName != newcountByName)
+ {
+ Log.Info("Count by Original JsonValue = " + countByName + "; Count by New JsonValue = " + newcountByName);
+ Log.Info("The number of items matching the provided Name does NOT equal between these two JsonValues!");
+ return false;
+ }
+ else
+ {
+ return true;
+ }
+ }
+
+ private bool CrossJsonValueVerificationOnIndexViaLinqQuery(JsonArray sourceJson, JsonArray newJson, int index)
+ {
+ var itemsByIndex = from JsonValue itemByIndex in sourceJson
+ where (itemByIndex != null && (int)itemByIndex["Index"] == index)
+ select itemByIndex;
+ int countByIndex = 0;
+ foreach (JsonValue a in itemsByIndex)
+ {
+ countByIndex++;
+ }
+
+ Log.Info("Original Collection contains: " + countByIndex + " item By Index " + index);
+
+ var newItemsByIndex = from JsonValue newItemByIndex in newJson
+ where (newItemByIndex != null && (int)newItemByIndex["Index"] == index)
+ select newItemByIndex;
+ int newcountByIndex = 0;
+ foreach (JsonValue a in newItemsByIndex)
+ {
+ newcountByIndex++;
+ }
+
+ Log.Info("New Collection contains: " + newcountByIndex + " item By Index " + index);
+
+ if (countByIndex != newcountByIndex)
+ {
+ Log.Info("Count by Original JsonValue = " + countByIndex + "; Count by New JsonValue = " + newcountByIndex);
+ Log.Info("The number of items matching the provided Index does NOT equal between these two JsonValues!");
+ return false;
+ }
+ else
+ {
+ return true;
+ }
+ }
+ }
+}
diff --git a/test/System.Json.Test.Integration/Properties/AssemblyInfo.cs b/test/System.Json.Test.Integration/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..5a9c850b
--- /dev/null
+++ b/test/System.Json.Test.Integration/Properties/AssemblyInfo.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("System.Json.Test.Integration")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("Microsoft")]
+[assembly: AssemblyProduct("System.Json.Test.Integration")]
+[assembly: AssemblyCopyright("Copyright © Microsoft 2011")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+[assembly: CLSCompliant(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("750a87c6-d9c9-476b-89ab-b3b3bce96bec")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/test/System.Json.Test.Integration/System.Json.Test.Integration.csproj b/test/System.Json.Test.Integration/System.Json.Test.Integration.csproj
new file mode 100644
index 00000000..88d12bd5
--- /dev/null
+++ b/test/System.Json.Test.Integration/System.Json.Test.Integration.csproj
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProductVersion>9.0.30729</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{A7B1264E-BCE5-42A8-8B5E-001A5360B128}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>System.Json.Test.Integration</RootNamespace>
+ <AssemblyName>System.Json.Test.Integration</AssemblyName>
+ <FileAlignment>512</FileAlignment>
+ <ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Debug\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Release\Test\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'CodeCoverage' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\CodeCoverage\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="Microsoft.CSharp" />
+ <Reference Include="System" />
+ <Reference Include="System.Runtime.Serialization" />
+ <Reference Include="System.XML" />
+ <Reference Include="xunit, Version=1.9.0.1566, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL">
+ <HintPath>..\..\packages\xunit.1.9.0.1566\lib\xunit.dll</HintPath>
+ </Reference>
+ <Reference Include="xunit.extensions, Version=1.9.0.1566, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL">
+ <HintPath>..\..\packages\xunit.extensions.1.9.0.1566\lib\xunit.extensions.dll</HintPath>
+ </Reference>
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="Common\InstanceCreator.cs" />
+ <Compile Include="Common\JsonValueCreatorSurrogate.cs" />
+ <Compile Include="Common\Log.cs" />
+ <Compile Include="Common\TypeLibrary.cs" />
+ <Compile Include="Common\Util.cs" />
+ <Compile Include="JObjectFunctionalTest.cs" />
+ <Compile Include="JsonPrimitiveTests.cs" />
+ <Compile Include="JsonStringRoundTripTests.cs" />
+ <Compile Include="JsonValueAndComplexTypesTests.cs" />
+ <Compile Include="JsonValueDynamicTests.cs" />
+ <Compile Include="JsonValueEventsTests.cs" />
+ <Compile Include="JsonValueLinqExtensionsIntegrationTest.cs" />
+ <Compile Include="JsonValuePartialTrustTests.cs" />
+ <Compile Include="JsonValueTestHelper.cs" />
+ <Compile Include="JsonValueTests.cs" />
+ <Compile Include="JsonValueUsageTest.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\System.Json\System.Json.csproj">
+ <Project>{F0441BE9-BDC0-4629-BE5A-8765FFAA2481}</Project>
+ <Name>System.Json</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\Microsoft.TestCommon\Microsoft.TestCommon.csproj">
+ <Project>{FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0}</Project>
+ <Name>Microsoft.TestCommon</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="packages.config" />
+ </ItemGroup>
+ <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+</Project> \ No newline at end of file
diff --git a/test/System.Json.Test.Integration/packages.config b/test/System.Json.Test.Integration/packages.config
new file mode 100644
index 00000000..d82739c0
--- /dev/null
+++ b/test/System.Json.Test.Integration/packages.config
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="xunit" version="1.9.0.1566" />
+ <package id="xunit.extensions" version="1.9.0.1566" />
+</packages> \ No newline at end of file
diff --git a/test/System.Json.Test.Unit/Common/AnyInstance.cs b/test/System.Json.Test.Unit/Common/AnyInstance.cs
new file mode 100644
index 00000000..899614fb
--- /dev/null
+++ b/test/System.Json.Test.Unit/Common/AnyInstance.cs
@@ -0,0 +1,220 @@
+using System.Collections.Generic;
+using System.Dynamic;
+using System.Linq.Expressions;
+using System.Reflection;
+
+namespace System.Json
+{
+ public static class AnyInstance
+ {
+ public const bool AnyBool = true;
+ public const string AnyString = "hello";
+ public const string AnyString2 = "world";
+ public const char AnyChar = 'c';
+ public const int AnyInt = 123456789;
+ [CLSCompliant(false)]
+ public const uint AnyUInt = 3123456789;
+ public const long AnyLong = 123456789012345L;
+ [CLSCompliant(false)]
+ public const ulong AnyULong = UInt64.MaxValue;
+ public const short AnyShort = -12345;
+ [CLSCompliant(false)]
+ public const ushort AnyUShort = 40000;
+ public const byte AnyByte = 0xDC;
+ [CLSCompliant(false)]
+ public const sbyte AnySByte = -34;
+ public const double AnyDouble = 123.45;
+ public const float AnyFloat = 23.4f;
+ public const decimal AnyDecimal = 1234.5678m;
+ public static readonly Guid AnyGuid = new Guid(0x11223344, 0x5566, 0x7788, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00);
+ public static readonly DateTime AnyDateTime = new DateTime(2010, 02, 15, 22, 45, 20, DateTimeKind.Utc);
+ public static readonly DateTimeOffset AnyDateTimeOffset = new DateTimeOffset(2010, 2, 5, 15, 45, 20, TimeSpan.FromHours(-3));
+ public static readonly Uri AnyUri = new Uri("http://tempuri.org/");
+
+ public static readonly JsonArray AnyJsonArray;
+ public static readonly JsonObject AnyJsonObject;
+
+ public static readonly JsonPrimitive AnyJsonPrimitive = new JsonPrimitive("hello");
+
+ public static readonly JsonValue AnyJsonValue1 = AnyJsonPrimitive;
+ public static readonly JsonValue AnyJsonValue2;
+ public static readonly JsonValue AnyJsonValue3 = null;
+
+ public static readonly JsonValue DefaultJsonValue = GetDefaultJsonValue();
+
+ public static readonly Person AnyPerson = Person.CreateSample();
+ public static readonly Address AnyAddress = Address.CreateSample();
+
+ public static readonly dynamic AnyDynamic = TestDynamicObject.CreatePersonAsDynamic(AnyPerson);
+
+ public static JsonValue[] AnyJsonValueArray
+ {
+ get
+ {
+ return new JsonValue[]
+ {
+ AnyInstance.AnyJsonArray,
+ AnyInstance.AnyJsonObject,
+ AnyInstance.AnyJsonPrimitive,
+ AnyInstance.DefaultJsonValue
+ };
+ }
+ }
+
+ static AnyInstance()
+ {
+ AnyJsonArray = new JsonArray { 1, 2, 3 };
+ AnyJsonObject = new JsonObject { { "one", 1 }, { "two", 2 } };
+ AnyJsonArray.Changing += new EventHandler<JsonValueChangeEventArgs>(PreventChanging);
+ AnyJsonObject.Changing += new EventHandler<JsonValueChangeEventArgs>(PreventChanging);
+ AnyJsonValue2 = AnyJsonArray;
+ }
+
+ private static void PreventChanging(object sender, JsonValueChangeEventArgs e)
+ {
+ throw new InvalidOperationException("AnyInstance.AnyJsonArray or AnyJsonObject cannot be modified; please clone the instance if the test needs to change it.");
+ }
+
+ private static JsonValue GetDefaultJsonValue()
+ {
+ PropertyInfo propInfo = typeof(JsonValue).GetProperty("DefaultInstance", BindingFlags.Static | BindingFlags.NonPublic);
+ return propInfo.GetValue(null, null) as JsonValue;
+ }
+ }
+
+ public class Person
+ {
+ public string Name { get; set; }
+
+ public int Age { get; set; }
+
+ public Address Address { get; set; }
+
+ public List<Person> Friends { get; set; }
+
+ public static Person CreateSample()
+ {
+ Person anyObject = new Person
+ {
+ Name = AnyInstance.AnyString,
+ Age = AnyInstance.AnyInt,
+ Address = Address.CreateSample(),
+ Friends = new List<Person> { new Person { Name = "Bill Gates", Age = 23, Address = Address.CreateSample() }, new Person { Name = "Steve Ballmer", Age = 19, Address = Address.CreateSample() } }
+ };
+
+ return anyObject;
+ }
+
+ public string FriendsToString()
+ {
+ string s = "";
+
+ if (this.Friends != null)
+ {
+ foreach (Person p in this.Friends)
+ {
+ s += p + ",";
+ }
+ }
+
+ return s;
+ }
+
+ public override string ToString()
+ {
+ return String.Format("{0}, {1}, [{2}], Friends=[{3}]", this.Name, this.Age, this.Address, this.FriendsToString());
+ }
+ }
+
+ public class Address
+ {
+ public const string AnyStreet = "123 1st Ave";
+
+ public const string AnyCity = "Springfield";
+
+ public const string AnyState = "ZZ";
+
+ public string Street { get; set; }
+
+ public string City { get; set; }
+
+ public string State { get; set; }
+
+ public static Address CreateSample()
+ {
+ Address address = new Address
+ {
+ Street = AnyStreet,
+ City = AnyCity,
+ State = AnyState,
+ };
+
+ return address;
+ }
+
+ public override string ToString()
+ {
+ return String.Format("{0}, {1}, {2}", this.Street, this.City, this.State);
+ }
+ }
+
+ public class TestDynamicObject : DynamicObject
+ {
+ private IDictionary<string, object> _values = new Dictionary<string, object>();
+
+ public bool UseFallbackMethod { get; set; }
+ public bool UseErrorSuggestion { get; set; }
+
+ public string TestProperty { get; set; }
+
+ public override IEnumerable<string> GetDynamicMemberNames()
+ {
+ return _values.Keys;
+ }
+
+ public override bool TrySetMember(SetMemberBinder binder, object value)
+ {
+ _values[binder.Name] = value;
+ return true;
+ }
+
+ public override bool TryGetMember(GetMemberBinder binder, out object result)
+ {
+ if (this.UseFallbackMethod)
+ {
+ DynamicMetaObject target = new DynamicMetaObject(Expression.Parameter(this.GetType()), BindingRestrictions.Empty);
+ DynamicMetaObject errorSuggestion = null;
+
+ if (this.UseErrorSuggestion)
+ {
+ errorSuggestion = new DynamicMetaObject(Expression.Throw(Expression.Constant(new TestDynamicObjectException())), BindingRestrictions.Empty);
+ }
+
+ DynamicMetaObject metaObj = binder.FallbackGetMember(target, errorSuggestion);
+ Expression<Action> lambda = Expression.Lambda<Action>(metaObj.Expression, new ParameterExpression[] { });
+ lambda.Compile().Invoke();
+ }
+
+ return _values.TryGetValue(binder.Name, out result);
+ }
+
+ public static dynamic CreatePersonAsDynamic(Person person)
+ {
+ dynamic dynObj = new TestDynamicObject();
+
+ dynObj.Name = person.Name;
+ dynObj.Age = person.Age;
+ dynObj.Address = new Address();
+ dynObj.Address.City = person.Address.City;
+ dynObj.Address.Street = person.Address.Street;
+ dynObj.Address.State = person.Address.State;
+ dynObj.Friends = person.Friends;
+
+ return dynObj;
+ }
+
+ public class TestDynamicObjectException : Exception
+ {
+ }
+ }
+}
diff --git a/test/System.Json.Test.Unit/Common/ExceptionTestHelper.cs b/test/System.Json.Test.Unit/Common/ExceptionTestHelper.cs
new file mode 100644
index 00000000..254afcda
--- /dev/null
+++ b/test/System.Json.Test.Unit/Common/ExceptionTestHelper.cs
@@ -0,0 +1,27 @@
+using Xunit;
+
+namespace System.Json
+{
+ public static class ExceptionHelper
+ {
+ public static void Throws<TException>(Assert.ThrowsDelegate act, Action<TException> exceptionAssert) where TException : Exception
+ {
+ Exception ex = Record.Exception(act);
+ Assert.NotNull(ex);
+ TException tex = Assert.IsAssignableFrom<TException>(ex);
+ exceptionAssert(tex);
+ }
+
+ public static void Throws<TException>(Assert.ThrowsDelegate act, string message) where TException : Exception
+ {
+ Throws<TException>(act, ex => Assert.Equal(message, ex.Message));
+ }
+
+ public static void Throws<TException>(Assert.ThrowsDelegate act) where TException : Exception
+ {
+ Throws<TException>(act, _ => { });
+ }
+
+
+ }
+}
diff --git a/test/System.Json.Test.Unit/Extensions/JsonValueExtensionsTest.cs b/test/System.Json.Test.Unit/Extensions/JsonValueExtensionsTest.cs
new file mode 100644
index 00000000..ac7a7f68
--- /dev/null
+++ b/test/System.Json.Test.Unit/Extensions/JsonValueExtensionsTest.cs
@@ -0,0 +1,476 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Runtime.Serialization;
+using System.Runtime.Serialization.Json;
+using Xunit;
+using Xunit.Extensions;
+
+namespace System.Json
+{
+ public class JsonValueExtensionsTest
+ {
+ const string DynamicPropertyNotDefined = "'{0}' does not contain a definition for property '{1}'.";
+ const string OperationNotSupportedOnJsonTypeMsgFormat = "Operation not supported on JsonValue instance of 'JsonType.{0}' type.";
+
+ [Fact]
+ public void CreateFromTypeTest()
+ {
+ JsonValue[] values =
+ {
+ AnyInstance.AnyJsonObject,
+ AnyInstance.AnyJsonArray,
+ AnyInstance.AnyJsonPrimitive,
+ AnyInstance.DefaultJsonValue
+ };
+
+ foreach (JsonValue value in values)
+ {
+ Assert.Same(value, JsonValueExtensions.CreateFrom(value));
+ }
+ }
+
+ public static IEnumerable<object[]> PrimitiveTestData
+ {
+ get
+ {
+ yield return new object[] { AnyInstance.AnyBool };
+ yield return new object[] { AnyInstance.AnyByte };
+ yield return new object[] { AnyInstance.AnyChar };
+ yield return new object[] { AnyInstance.AnyDateTime };
+ yield return new object[] { AnyInstance.AnyDateTimeOffset };
+ yield return new object[] { AnyInstance.AnyDecimal };
+ yield return new object[] { AnyInstance.AnyDouble };
+ yield return new object[] { AnyInstance.AnyFloat };
+ yield return new object[] { AnyInstance.AnyGuid };
+ yield return new object[] { AnyInstance.AnyLong };
+ yield return new object[] { AnyInstance.AnySByte };
+ yield return new object[] { AnyInstance.AnyShort };
+ yield return new object[] { AnyInstance.AnyUInt };
+ yield return new object[] { AnyInstance.AnyULong };
+ yield return new object[] { AnyInstance.AnyUri };
+ yield return new object[] { AnyInstance.AnyUShort };
+ yield return new object[] { AnyInstance.AnyInt };
+ yield return new object[] { AnyInstance.AnyString };
+ }
+ }
+
+ [Theory]
+ [PropertyData("PrimitiveTestData")]
+ public void CreateFromPrimitiveTest(object value)
+ {
+ Type valueType = value.GetType();
+ Assert.Equal(value, JsonValueExtensions.CreateFrom(value).ReadAs(valueType));
+ }
+
+ [Fact]
+ public void CreateFromComplexTest()
+ {
+ JsonValue target = JsonValueExtensions.CreateFrom(AnyInstance.AnyPerson);
+
+ Assert.Equal(AnyInstance.AnyPerson.Name, (string)target["Name"]);
+ Assert.Equal(AnyInstance.AnyPerson.Age, (int)target["Age"]);
+ Assert.Equal(AnyInstance.AnyPerson.Address.City, (string)target.ValueOrDefault("Address", "City"));
+ }
+
+ [Fact]
+ public void CreateFromDynamicSimpleTest()
+ {
+ JsonValue target;
+
+ target = JsonValueExtensions.CreateFrom(AnyInstance.AnyDynamic);
+ Assert.NotNull(target);
+
+ string expected = "{\"Name\":\"Bill Gates\",\"Age\":21,\"Grades\":[\"F\",\"B-\",\"C\"]}";
+ dynamic obj = new TestDynamicObject();
+ obj.Name = "Bill Gates";
+ obj.Age = 21;
+ obj.Grades = new[] { "F", "B-", "C" };
+
+ target = JsonValueExtensions.CreateFrom(obj);
+ Assert.Equal<string>(expected, target.ToString());
+
+ target = JsonValueExtensions.CreateFrom(new TestDynamicObject());
+ Assert.Equal<string>("{}", target.ToString());
+ }
+
+ [Fact]
+ public void CreateFromDynamicComplextTest()
+ {
+ JsonValue target;
+ Person person = AnyInstance.AnyPerson;
+ dynamic dyn = TestDynamicObject.CreatePersonAsDynamic(person);
+
+ dyn.TestProperty = AnyInstance.AnyString;
+
+ target = JsonValueExtensions.CreateFrom(dyn);
+ Assert.NotNull(target);
+ Assert.Equal<string>(AnyInstance.AnyString, dyn.TestProperty);
+ Person jvPerson = target.ReadAsType<Person>();
+ Assert.Equal(person.ToString(), jvPerson.ToString());
+
+ Person p1 = Person.CreateSample();
+ Person p2 = Person.CreateSample();
+
+ p2.Name += "__2";
+ p2.Age += 10;
+ p2.Address.City += "__2";
+
+ Person[] friends = new Person[] { p1, p2 };
+ target = JsonValueExtensions.CreateFrom(friends);
+ Person[] personArr = target.ReadAsType<Person[]>();
+ Assert.Equal<int>(friends.Length, personArr.Length);
+ Assert.Equal<string>(friends[0].ToString(), personArr[0].ToString());
+ Assert.Equal<string>(friends[1].ToString(), personArr[1].ToString());
+ }
+
+ [Fact]
+ public void CreateFromDynamicBinderFallbackTest()
+ {
+ JsonValue target;
+ Person person = AnyInstance.AnyPerson;
+ dynamic dyn = new TestDynamicObject();
+ dyn.Name = AnyInstance.AnyString;
+
+ dyn.UseFallbackMethod = true;
+ string expectedMessage = String.Format(DynamicPropertyNotDefined, dyn.GetType().FullName, "Name");
+ ExceptionHelper.Throws<InvalidOperationException>(() => target = JsonValueExtensions.CreateFrom(dyn), expectedMessage);
+
+ dyn.UseErrorSuggestion = true;
+ ExceptionHelper.Throws<TestDynamicObject.TestDynamicObjectException>(() => target = JsonValueExtensions.CreateFrom(dyn));
+ }
+
+ [Fact]
+ public void CreateFromNestedDynamicTest()
+ {
+ JsonValue target;
+ string expected = "{\"Name\":\"Root\",\"Level1\":{\"Name\":\"Level1\",\"Level2\":{\"Name\":\"Level2\"}}}";
+ dynamic dyn = new TestDynamicObject();
+ dyn.Name = "Root";
+ dyn.Level1 = new TestDynamicObject();
+ dyn.Level1.Name = "Level1";
+ dyn.Level1.Level2 = new TestDynamicObject();
+ dyn.Level1.Level2.Name = "Level2";
+
+ target = JsonValueExtensions.CreateFrom(dyn);
+ Assert.NotNull(target);
+ Assert.Equal<string>(expected, target.ToString());
+ }
+
+ [Fact]
+ public void CreateFromDynamicWithJsonValueChildrenTest()
+ {
+ JsonValue target;
+ string level3 = "{\"Name\":\"Level3\",\"Null\":null}";
+ string level2 = "{\"Name\":\"Level2\",\"JsonObject\":" + AnyInstance.AnyJsonObject.ToString() + ",\"JsonArray\":" + AnyInstance.AnyJsonArray.ToString() + ",\"Level3\":" + level3 + "}";
+ string level1 = "{\"Name\":\"Level1\",\"JsonPrimitive\":" + AnyInstance.AnyJsonPrimitive.ToString() + ",\"Level2\":" + level2 + "}";
+ string expected = "{\"Name\":\"Root\",\"Level1\":" + level1 + "}";
+
+ dynamic dyn = new TestDynamicObject();
+ dyn.Name = "Root";
+ dyn.Level1 = new TestDynamicObject();
+ dyn.Level1.Name = "Level1";
+ dyn.Level1.JsonPrimitive = AnyInstance.AnyJsonPrimitive;
+ dyn.Level1.Level2 = new TestDynamicObject();
+ dyn.Level1.Level2.Name = "Level2";
+ dyn.Level1.Level2.JsonObject = AnyInstance.AnyJsonObject;
+ dyn.Level1.Level2.JsonArray = AnyInstance.AnyJsonArray;
+ dyn.Level1.Level2.Level3 = new TestDynamicObject();
+ dyn.Level1.Level2.Level3.Name = "Level3";
+ dyn.Level1.Level2.Level3.Null = null;
+
+ target = JsonValueExtensions.CreateFrom(dyn);
+ Assert.Equal<string>(expected, target.ToString());
+ }
+
+ [Fact]
+ public void CreateFromDynamicJVTest()
+ {
+ JsonValue target;
+
+ dynamic[] values = new dynamic[]
+ {
+ AnyInstance.AnyJsonArray,
+ AnyInstance.AnyJsonObject,
+ AnyInstance.AnyJsonPrimitive,
+ AnyInstance.DefaultJsonValue
+ };
+
+ foreach (dynamic dyn in values)
+ {
+ target = JsonValueExtensions.CreateFrom(dyn);
+ Assert.Same(dyn, target);
+ }
+ }
+
+ [Fact]
+ public void ReadAsTypeFallbackTest()
+ {
+ JsonValue jv = AnyInstance.AnyInt;
+ Person personFallback = Person.CreateSample();
+
+ Person personResult = jv.ReadAsType<Person>(personFallback);
+ Assert.Same(personFallback, personResult);
+
+ int intFallback = 45;
+ int intValue = jv.ReadAsType<int>(intFallback);
+ Assert.Equal<int>(AnyInstance.AnyInt, intValue);
+ }
+
+ [Fact(Skip = "See bug #228569 in CSDMain")]
+ public void ReadAsTypeCollectionTest()
+ {
+ JsonValue jsonValue;
+ jsonValue = JsonValue.Parse("[1,2,3]");
+
+ List<object> list = jsonValue.ReadAsType<List<object>>();
+ Array array = jsonValue.ReadAsType<Array>();
+ object[] objArr = jsonValue.ReadAsType<object[]>();
+
+ IList[] collections =
+ {
+ list, array, objArr
+ };
+
+ foreach (IList collection in collections)
+ {
+ Assert.Equal<int>(jsonValue.Count, collection.Count);
+
+ for (int i = 0; i < jsonValue.Count; i++)
+ {
+ Assert.Equal<int>((int)jsonValue[i], (int)collection[i]);
+ }
+ }
+
+ jsonValue = JsonValue.Parse("{\"A\":1,\"B\":2,\"C\":3}");
+ Dictionary<string, object> dictionary = jsonValue.ReadAsType<Dictionary<string, object>>();
+
+ Assert.Equal<int>(jsonValue.Count, dictionary.Count);
+ foreach (KeyValuePair<string, JsonValue> pair in jsonValue)
+ {
+ Assert.Equal((int)jsonValue[pair.Key], (int)dictionary[pair.Key]);
+ }
+ }
+
+ [Fact]
+ public void TryReadAsInvalidCollectionTest()
+ {
+ JsonValue jo = AnyInstance.AnyJsonObject;
+ JsonValue ja = AnyInstance.AnyJsonArray;
+ JsonValue jp = AnyInstance.AnyJsonPrimitive;
+ JsonValue jd = AnyInstance.DefaultJsonValue;
+
+ JsonValue[] invalidArrays =
+ {
+ jo, jp, jd
+ };
+
+ JsonValue[] invalidDictionaries =
+ {
+ ja, jp, jd
+ };
+
+ bool success;
+ object[] array;
+ Dictionary<string, object> dictionary;
+
+ foreach (JsonValue value in invalidArrays)
+ {
+ success = value.TryReadAsType<object[]>(out array);
+ Console.WriteLine("Try reading {0} as object[]; success = {1}", value.ToString(), success);
+ Assert.False(success);
+ Assert.Null(array);
+ }
+
+ foreach (JsonValue value in invalidDictionaries)
+ {
+ success = value.TryReadAsType<Dictionary<string, object>>(out dictionary);
+ Console.WriteLine("Try reading {0} as Dictionary<string, object>; success = {1}", value.ToString(), success);
+ Assert.False(success);
+ Assert.Null(dictionary);
+ }
+ }
+
+ [Fact]
+ public void ReadAsExtensionsOnDynamicTest()
+ {
+ dynamic jv = JsonValueExtensions.CreateFrom(AnyInstance.AnyPerson);
+ bool success;
+ object obj;
+
+ success = jv.TryReadAsType(typeof(Person), out obj);
+ Assert.True(success);
+ Assert.NotNull(obj);
+ Assert.Equal<string>(AnyInstance.AnyPerson.ToString(), obj.ToString());
+
+ obj = jv.ReadAsType(typeof(Person));
+ Assert.NotNull(obj);
+ Assert.Equal<string>(AnyInstance.AnyPerson.ToString(), obj.ToString());
+ }
+
+#if CODEPLEX
+ [Fact]
+ public void ToCollectionTest()
+ {
+ JsonValue target;
+ object[] array;
+
+ target = AnyInstance.AnyJsonArray;
+ array = target.ToObjectArray();
+
+ Assert.Equal(target.Count, array.Length);
+
+ for (int i = 0; i < target.Count; i++)
+ {
+ Assert.Equal(array[i], target[i].ReadAs(array[i].GetType()));
+ }
+
+ target = AnyInstance.AnyJsonObject;
+ IDictionary<string, object> dictionary = target.ToDictionary();
+
+ Assert.Equal(target.Count, dictionary.Count);
+
+ foreach (KeyValuePair<string, JsonValue> pair in target)
+ {
+ Assert.True(dictionary.ContainsKey(pair.Key));
+ Assert.Equal<string>(target[pair.Key].ToString(), dictionary[pair.Key].ToString());
+ }
+ }
+
+ [Fact]
+ public void ToCollectionsNestedTest()
+ {
+ JsonArray ja = JsonValue.Parse("[1, {\"A\":[1,2,3]}, 5]") as JsonArray;
+ JsonObject jo = JsonValue.Parse("{\"A\":1,\"B\":[1,2,3]}") as JsonObject;
+
+ object[] objArray = ja.ToObjectArray();
+ Assert.NotNull(objArray);
+ Assert.Equal<int>(ja.Count, objArray.Length);
+ Assert.Equal((int)ja[0], (int)objArray[0]);
+ Assert.Equal((int)ja[2], (int)objArray[2]);
+
+ IDictionary<string, object> dict = objArray[1] as IDictionary<string, object>;
+ Assert.NotNull(dict);
+
+ objArray = dict["A"] as object[];
+ Assert.NotNull(objArray);
+ for (int i = 0; i < 3; i++)
+ {
+ Assert.Equal(i + 1, (int)objArray[i]);
+ }
+
+ dict = jo.ToDictionary();
+ Assert.NotNull(dict);
+ Assert.Equal<int>(jo.Count, dict.Count);
+ Assert.Equal<int>(1, (int)dict["A"]);
+
+ objArray = dict["B"] as object[];
+ Assert.NotNull(objArray);
+ for (int i = 1; i < 3; i++)
+ {
+ Assert.Equal(i + 1, (int)objArray[i]);
+ }
+ }
+
+ [Fact]
+ public void ToCollectionsInvalidTest()
+ {
+ JsonValue jo = AnyInstance.AnyJsonObject;
+ JsonValue ja = AnyInstance.AnyJsonArray;
+ JsonValue jp = AnyInstance.AnyJsonPrimitive;
+ JsonValue jd = AnyInstance.DefaultJsonValue;
+
+ ExceptionTestHelper.ExpectException<NotSupportedException>(delegate { var ret = jd.ToObjectArray(); }, String.Format(OperationNotSupportedOnJsonTypeMsgFormat, jd.JsonType));
+ ExceptionTestHelper.ExpectException<NotSupportedException>(delegate { var ret = jd.ToDictionary(); }, String.Format(OperationNotSupportedOnJsonTypeMsgFormat, jd.JsonType));
+
+ ExceptionTestHelper.ExpectException<NotSupportedException>(delegate { var ret = jp.ToObjectArray(); }, String.Format(OperationNotSupportedOnJsonTypeMsgFormat, jp.JsonType));
+ ExceptionTestHelper.ExpectException<NotSupportedException>(delegate { var ret = jp.ToDictionary(); }, String.Format(OperationNotSupportedOnJsonTypeMsgFormat, jp.JsonType));
+
+ ExceptionTestHelper.ExpectException<NotSupportedException>(delegate { var ret = jo.ToObjectArray(); }, String.Format(OperationNotSupportedOnJsonTypeMsgFormat, jo.JsonType));
+ ExceptionTestHelper.ExpectException<NotSupportedException>(delegate { var ret = ja.ToDictionary(); }, String.Format(OperationNotSupportedOnJsonTypeMsgFormat, ja.JsonType));
+ }
+
+ // 195843 JsonValue to support generic extension methods defined in JsonValueExtensions.
+ // 195867 Consider creating extension point for allowing new extension methods to be callable via dynamic interface
+ //[Fact] This requires knowledge of the C# binder to be able to get the generic call parameters.
+ public void ReadAsGenericExtensionsOnDynamicTest()
+ {
+ dynamic jv = JsonValueExtensions.CreateFrom(AnyInstance.AnyPerson);
+ Person person;
+ bool success;
+
+ person = jv.ReadAsType<Person>();
+ Assert.NotNull(person);
+ Assert.Equal<string>(AnyInstance.AnyPerson.ToString(), person.ToString());
+
+ success = jv.TryReadAsType<Person>(out person);
+ Assert.True(success);
+ Assert.NotNull(person);
+ Assert.Equal<string>(AnyInstance.AnyPerson.ToString(), person.ToString());
+ }
+#else
+ [Fact(Skip = "See bug #228569 in CSDMain")]
+ public void TestDataContractJsonSerializerSettings()
+ {
+ TestTypeForSerializerSettings instance = new TestTypeForSerializerSettings
+ {
+ BaseRef = new DerivedType(),
+ Date = AnyInstance.AnyDateTime,
+ Dict = new Dictionary<string, object>
+ {
+ { "one", 1 },
+ { "two", 2 },
+ { "two point five", 2.5 },
+ }
+ };
+
+ JsonObject dict = new JsonObject
+ {
+ { "one", 1 },
+ { "two", 2 },
+ { "two point five", 2.5 },
+ };
+
+ JsonObject equivalentJsonObject = new JsonObject
+ {
+ { "BaseRef", new JsonObject { { "__type", "DerivedType:NS" } } },
+ { "Date", AnyInstance.AnyDateTime },
+ { "Dict", dict },
+ };
+
+ JsonObject createdFromType = JsonValueExtensions.CreateFrom(instance) as JsonObject;
+ Assert.Equal(equivalentJsonObject.ToString(), createdFromType.ToString());
+
+ TestTypeForSerializerSettings newInstance = equivalentJsonObject.ReadAsType<TestTypeForSerializerSettings>();
+ // DISABLED, 198487 - Assert.Equal(instance.Date, newInstance.Date);
+ Assert.Equal(instance.BaseRef.GetType().FullName, newInstance.BaseRef.GetType().FullName);
+ Assert.Equal(3, newInstance.Dict.Count);
+ Assert.Equal(1, newInstance.Dict["one"]);
+ Assert.Equal(2, newInstance.Dict["two"]);
+ Assert.Equal(2.5, Convert.ToDouble(newInstance.Dict["two point five"], CultureInfo.InvariantCulture));
+ }
+
+ [DataContract]
+ public class TestTypeForSerializerSettings
+ {
+ [DataMember]
+ public BaseType BaseRef { get; set; }
+ [DataMember]
+ public DateTime Date { get; set; }
+ [DataMember]
+ public Dictionary<string, object> Dict { get; set; }
+ }
+
+ [DataContract(Name = "BaseType", Namespace = "NS")]
+ [KnownType(typeof(DerivedType))]
+ public class BaseType
+ {
+ }
+
+ [DataContract(Name = "DerivedType", Namespace = "NS")]
+ public class DerivedType : BaseType
+ {
+ }
+#endif
+ }
+}
diff --git a/test/System.Json.Test.Unit/FormUrlEncodedJsonTests.cs b/test/System.Json.Test.Unit/FormUrlEncodedJsonTests.cs
new file mode 100644
index 00000000..a114abba
--- /dev/null
+++ b/test/System.Json.Test.Unit/FormUrlEncodedJsonTests.cs
@@ -0,0 +1,74 @@
+using System.Collections.Generic;
+using Microsoft.TestCommon;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Json
+{
+ public class FormUrlEncodedJsonTests
+ {
+ [Fact]
+ public void TypeIsCorrect()
+ {
+ Assert.Type.HasProperties(typeof(FormUrlEncodedJson), TypeAssert.TypeProperties.IsPublicVisibleClass | TypeAssert.TypeProperties.IsStatic);
+ }
+
+ [Fact]
+ public void ParseThrowsOnNull()
+ {
+ Assert.ThrowsArgumentNull(() => FormUrlEncodedJson.Parse(null), null);
+ }
+
+ [Fact]
+ public void ParseThrowsInvalidMaxDepth()
+ {
+ Assert.ThrowsArgument(() => FormUrlEncodedJson.Parse(CreateQuery(), -1), "maxDepth");
+ Assert.ThrowsArgument(() => FormUrlEncodedJson.Parse(CreateQuery(), 0), "maxDepth");
+ }
+
+ [Fact]
+ public void ParseThrowsMaxDepthExceeded()
+ {
+ // Depth of 'a[b]=1' is 3
+ IEnumerable<KeyValuePair<string, string>> query = CreateQuery(new KeyValuePair<string, string>("a[b]", "1"));
+ Assert.ThrowsArgument(() => { FormUrlEncodedJson.Parse(query, 2); }, null);
+
+ // This should succeed
+ Assert.NotNull(FormUrlEncodedJson.Parse(query, 3));
+ }
+
+ [Fact]
+ public void TryParseThrowsOnNull()
+ {
+ JsonObject value;
+ Assert.ThrowsArgumentNull(() => FormUrlEncodedJson.TryParse(null, out value), null);
+ }
+
+ [Fact]
+ public void TryParseThrowsInvalidMaxDepth()
+ {
+ JsonObject value;
+ Assert.ThrowsArgument(() => FormUrlEncodedJson.TryParse(CreateQuery(), -1, out value), "maxDepth");
+ Assert.ThrowsArgument(() => FormUrlEncodedJson.TryParse(CreateQuery(), 0, out value), "maxDepth");
+ }
+
+ [Fact]
+ public void TryParseReturnsFalseMaxDepthExceeded()
+ {
+ JsonObject value;
+
+ // Depth of 'a[b]=1' is 3
+ IEnumerable<KeyValuePair<string, string>> query = CreateQuery(new KeyValuePair<string, string>("a[b]", "1"));
+ Assert.False(FormUrlEncodedJson.TryParse(query, 2, out value), "Parse should have failed due to too high depth.");
+
+ // This should succeed
+ Assert.True(FormUrlEncodedJson.TryParse(query, 3, out value), "Expected non-null JsonObject instance");
+ Assert.NotNull(value);
+ }
+
+ private static IEnumerable<KeyValuePair<string, string>> CreateQuery(params KeyValuePair<string, string>[] namevaluepairs)
+ {
+ return new List<KeyValuePair<string, string>>(namevaluepairs);
+ }
+ }
+} \ No newline at end of file
diff --git a/test/System.Json.Test.Unit/JsonArrayTest.cs b/test/System.Json.Test.Unit/JsonArrayTest.cs
new file mode 100644
index 00000000..1d351d05
--- /dev/null
+++ b/test/System.Json.Test.Unit/JsonArrayTest.cs
@@ -0,0 +1,604 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Runtime.Serialization.Json;
+using Xunit;
+
+namespace System.Json
+{
+ public class JsonArrayTest
+ {
+ [Fact]
+ public void JsonArrayConstructorParamsTest()
+ {
+ JsonArray target;
+
+ target = new JsonArray();
+ Assert.Equal(0, target.Count);
+
+ target = new JsonArray(null);
+ Assert.Equal(0, target.Count);
+
+ List<JsonValue> items = new List<JsonValue> { AnyInstance.AnyJsonValue1, AnyInstance.AnyJsonValue2 };
+ target = new JsonArray(items.ToArray());
+ ValidateJsonArrayItems(target, items);
+
+ target = new JsonArray(items[0], items[1]);
+ ValidateJsonArrayItems(target, items);
+
+ // Invalide tests
+ items.Add(AnyInstance.DefaultJsonValue);
+ ExceptionHelper.Throws<ArgumentException>(() => new JsonArray(items.ToArray()));
+ ExceptionHelper.Throws<ArgumentException>(() => new JsonArray(items[0], items[1], items[2]));
+ }
+
+ [Fact]
+ public void JsonArrayConstructorEnumTest()
+ {
+ List<JsonValue> items = new List<JsonValue> { AnyInstance.AnyJsonValue1, AnyInstance.AnyJsonValue2, AnyInstance.AnyJsonValue3 };
+ JsonArray target;
+
+ target = new JsonArray(items);
+ ValidateJsonArrayItems(target, items);
+
+ ExceptionHelper.Throws<ArgumentNullException>(() => new JsonArray((IEnumerable<JsonValue>)null));
+
+ items.Add(AnyInstance.DefaultJsonValue);
+ ExceptionHelper.Throws<ArgumentException>(() => new JsonArray(items));
+ }
+
+ [Fact]
+ public void AddTest()
+ {
+ JsonArray target = new JsonArray();
+ JsonValue item = AnyInstance.AnyJsonValue1;
+ Assert.False(target.Contains(item));
+ target.Add(item);
+ Assert.Equal(1, target.Count);
+ Assert.Equal(item, target[0]);
+ Assert.True(target.Contains(item));
+
+ ExceptionHelper.Throws<ArgumentException>(() => target.Add(AnyInstance.DefaultJsonValue));
+ }
+
+ [Fact]
+ public void AddRangeEnumTest()
+ {
+ List<JsonValue> items = new List<JsonValue> { AnyInstance.AnyJsonValue1, AnyInstance.AnyJsonValue2 };
+
+ JsonArray target = new JsonArray();
+ target.AddRange(items);
+ ValidateJsonArrayItems(target, items);
+
+ ExceptionHelper.Throws<ArgumentNullException>(() => new JsonArray().AddRange((IEnumerable<JsonValue>)null));
+
+ items.Add(AnyInstance.DefaultJsonValue);
+ ExceptionHelper.Throws<ArgumentException>(() => new JsonArray().AddRange(items));
+ }
+
+ [Fact]
+ public void AddRangeParamsTest()
+ {
+ List<JsonValue> items = new List<JsonValue> { AnyInstance.AnyJsonValue1, AnyInstance.AnyJsonValue2, AnyInstance.AnyJsonValue3 };
+ JsonArray target;
+
+ target = new JsonArray();
+ target.AddRange(items[0], items[1], items[2]);
+ ValidateJsonArrayItems(target, items);
+
+ target = new JsonArray();
+ target.AddRange(items.ToArray());
+ ValidateJsonArrayItems(target, items);
+
+ target.AddRange();
+ ValidateJsonArrayItems(target, items);
+
+ items.Add(AnyInstance.DefaultJsonValue);
+ ExceptionHelper.Throws<ArgumentException>(() => new JsonArray().AddRange(items[items.Count - 1]));
+ ExceptionHelper.Throws<ArgumentException>(() => new JsonArray().AddRange(items));
+ }
+
+ [Fact]
+ public void ClearTest()
+ {
+ JsonArray target = new JsonArray(AnyInstance.AnyJsonValue1, AnyInstance.AnyJsonValue2);
+ Assert.Equal(2, target.Count);
+ target.Clear();
+ Assert.Equal(0, target.Count);
+ }
+
+ [Fact]
+ public void ContainsTest()
+ {
+ JsonValue item1 = AnyInstance.AnyJsonValue1;
+ JsonValue item2 = AnyInstance.AnyJsonValue2;
+ JsonArray target = new JsonArray(item1);
+ Assert.True(target.Contains(item1));
+ Assert.False(target.Contains(item2));
+
+ target.Add(item2);
+ Assert.True(target.Contains(item1));
+ Assert.True(target.Contains(item2));
+
+ target.Remove(item1);
+ Assert.False(target.Contains(item1));
+ Assert.True(target.Contains(item2));
+ }
+
+ [Fact]
+ public void ReadAsComplexTypeTest()
+ {
+ JsonArray target = new JsonArray(AnyInstance.AnyInt, AnyInstance.AnyInt + 1, AnyInstance.AnyInt + 2);
+ int[] intArray1 = (int[])target.ReadAsType(typeof(int[]));
+ int[] intArray2 = target.ReadAsType<int[]>();
+
+ Assert.Equal(((JsonArray)target).Count, intArray1.Length);
+ Assert.Equal(((JsonArray)target).Count, intArray2.Length);
+
+ for (int i = 0; i < intArray1.Length; i++)
+ {
+ Assert.Equal(AnyInstance.AnyInt + i, intArray1[i]);
+ Assert.Equal(AnyInstance.AnyInt + i, intArray2[i]);
+ }
+ }
+
+ [Fact]
+ public void CopyToTest()
+ {
+ JsonValue item1 = AnyInstance.AnyJsonValue1;
+ JsonValue item2 = AnyInstance.AnyJsonValue2;
+ JsonArray target = new JsonArray(item1, item2);
+ JsonValue[] array = new JsonValue[target.Count + 1];
+
+ target.CopyTo(array, 0);
+ Assert.Equal(item1, array[0]);
+ Assert.Equal(item2, array[1]);
+
+ target.CopyTo(array, 1);
+ Assert.Equal(item1, array[1]);
+ Assert.Equal(item2, array[2]);
+
+ ExceptionHelper.Throws<ArgumentNullException>(() => target.CopyTo(null, 0));
+ ExceptionHelper.Throws<ArgumentOutOfRangeException>(() => target.CopyTo(array, -1));
+ ExceptionHelper.Throws<ArgumentException>(() => target.CopyTo(array, array.Length - target.Count + 1));
+ }
+
+ [Fact]
+ public void IndexOfTest()
+ {
+ JsonValue item1 = AnyInstance.AnyJsonValue1;
+ JsonValue item2 = AnyInstance.AnyJsonValue2;
+ JsonValue item3 = AnyInstance.AnyJsonValue3;
+ JsonArray target = new JsonArray(item1, item2);
+
+ Assert.Equal(0, target.IndexOf(item1));
+ Assert.Equal(1, target.IndexOf(item2));
+ Assert.Equal(-1, target.IndexOf(item3));
+ }
+
+ [Fact]
+ public void InsertTest()
+ {
+ JsonValue item1 = AnyInstance.AnyJsonValue1;
+ JsonValue item2 = AnyInstance.AnyJsonValue2;
+ JsonValue item3 = AnyInstance.AnyJsonValue3;
+ JsonArray target = new JsonArray(item1);
+
+ Assert.Equal(1, target.Count);
+ target.Insert(0, item2);
+ Assert.Equal(2, target.Count);
+ Assert.Equal(item2, target[0]);
+ Assert.Equal(item1, target[1]);
+
+ target.Insert(1, item3);
+ Assert.Equal(3, target.Count);
+ Assert.Equal(item2, target[0]);
+ Assert.Equal(item3, target[1]);
+ Assert.Equal(item1, target[2]);
+
+ target.Insert(target.Count, item2);
+ Assert.Equal(4, target.Count);
+ Assert.Equal(item2, target[0]);
+ Assert.Equal(item3, target[1]);
+ Assert.Equal(item1, target[2]);
+ Assert.Equal(item2, target[3]);
+
+ ExceptionHelper.Throws<ArgumentOutOfRangeException>(() => target.Insert(-1, item3));
+ ExceptionHelper.Throws<ArgumentOutOfRangeException>(() => target.Insert(target.Count + 1, item1));
+ ExceptionHelper.Throws<ArgumentException>(() => target.Insert(0, AnyInstance.DefaultJsonValue));
+ }
+
+ [Fact]
+ public void RemoveTest()
+ {
+ JsonValue item1 = AnyInstance.AnyJsonValue1;
+ JsonValue item2 = AnyInstance.AnyJsonValue2;
+ JsonValue item3 = AnyInstance.AnyJsonValue3;
+ JsonArray target = new JsonArray(item1, item2, item3);
+
+ Assert.True(target.Remove(item2));
+ Assert.Equal(2, target.Count);
+ Assert.Equal(item1, target[0]);
+ Assert.Equal(item3, target[1]);
+
+ Assert.False(target.Remove(item2));
+ Assert.Equal(2, target.Count);
+ }
+
+ [Fact]
+ public void RemoveAtTest()
+ {
+ JsonValue item1 = AnyInstance.AnyJsonValue1;
+ JsonValue item2 = AnyInstance.AnyJsonValue2;
+ JsonValue item3 = AnyInstance.AnyJsonValue3;
+ JsonArray target = new JsonArray(item1, item2, item3);
+
+ target.RemoveAt(1);
+ Assert.Equal(2, target.Count);
+ Assert.Equal(item1, target[0]);
+ Assert.Equal(item3, target[1]);
+
+ ExceptionHelper.Throws<ArgumentOutOfRangeException>(() => target.RemoveAt(-1));
+ ExceptionHelper.Throws<ArgumentOutOfRangeException>(() => target.RemoveAt(target.Count));
+ }
+
+ [Fact]
+ public void ToStringTest()
+ {
+ JsonArray target;
+ JsonValue item1 = AnyInstance.AnyJsonValue1;
+ JsonValue item2 = null;
+ JsonValue item3 = AnyInstance.AnyJsonValue2;
+
+ target = new JsonArray(item1, item2, item3);
+
+ string expected = String.Format(CultureInfo.InvariantCulture, "[{0},null,{1}]", item1.ToString(), item3.ToString());
+ Assert.Equal(expected, target.ToString());
+
+ string json = "[\r\n \"hello\",\r\n null,\r\n [\r\n 1,\r\n 2,\r\n 3\r\n ]\r\n]";
+ target = JsonValue.Parse(json) as JsonArray;
+
+ Assert.Equal<string>(json.Replace("\r\n", "").Replace(" ", ""), target.ToString());
+ }
+
+ [Fact]
+ public void GetEnumeratorTest()
+ {
+ JsonValue item1 = AnyInstance.AnyJsonValue1;
+ JsonValue item2 = AnyInstance.AnyJsonValue2;
+
+ IEnumerable<JsonValue> target = new JsonArray(item1, item2);
+ IEnumerator<JsonValue> enumerator = target.GetEnumerator();
+ Assert.True(enumerator.MoveNext());
+ Assert.Equal(item1, enumerator.Current);
+ Assert.True(enumerator.MoveNext());
+ Assert.Equal(item2, enumerator.Current);
+ Assert.False(enumerator.MoveNext());
+ }
+
+ [Fact]
+ public void GetEnumeratorTest1()
+ {
+ JsonValue item1 = AnyInstance.AnyJsonValue1;
+ JsonValue item2 = AnyInstance.AnyJsonValue2;
+
+ IEnumerable target = new JsonArray(item1, item2);
+ IEnumerator enumerator = target.GetEnumerator();
+ Assert.True(enumerator.MoveNext());
+ Assert.Equal(item1, enumerator.Current);
+ Assert.True(enumerator.MoveNext());
+ Assert.Equal(item2, enumerator.Current);
+ Assert.False(enumerator.MoveNext());
+ }
+
+ [Fact]
+ public void CountTest()
+ {
+ JsonValue item1 = AnyInstance.AnyJsonValue1;
+ JsonValue item2 = AnyInstance.AnyJsonValue2;
+
+ JsonArray target = new JsonArray();
+ Assert.Equal(0, target.Count);
+ target.Add(item1);
+ Assert.Equal(1, target.Count);
+ target.Add(item2);
+ Assert.Equal(2, target.Count);
+ target.Remove(item1);
+ Assert.Equal(1, target.Count);
+ }
+
+ [Fact]
+ public void IsReadOnlyTest()
+ {
+ JsonArray target = AnyInstance.AnyJsonArray;
+ Assert.False(target.IsReadOnly);
+ }
+
+ [Fact]
+ public void ItemTest()
+ {
+ JsonValue item1 = AnyInstance.AnyJsonValue1;
+ JsonValue item2 = AnyInstance.AnyJsonValue2;
+
+ JsonArray target = new JsonArray(item1);
+ Assert.Equal(item1, target[0]);
+ target[0] = item2;
+ Assert.Equal(item2, target[0]);
+ Assert.Equal(item2, target[(short)0]);
+ Assert.Equal(item2, target[(ushort)0]);
+ Assert.Equal(item2, target[(byte)0]);
+ Assert.Equal(item2, target[(sbyte)0]);
+ Assert.Equal(item2, target[(char)0]);
+
+ ExceptionHelper.Throws<ArgumentOutOfRangeException>(delegate { var i = target[-1]; });
+ ExceptionHelper.Throws<ArgumentOutOfRangeException>(delegate { var i = target[target.Count]; });
+ ExceptionHelper.Throws<ArgumentOutOfRangeException>(delegate { target[-1] = AnyInstance.AnyJsonValue1; });
+ ExceptionHelper.Throws<ArgumentOutOfRangeException>(delegate { target[target.Count] = AnyInstance.AnyJsonValue2; });
+ ExceptionHelper.Throws<ArgumentException>(delegate { target[0] = AnyInstance.DefaultJsonValue; });
+ }
+
+ [Fact]
+ public void ChangingEventsTest()
+ {
+ JsonArray ja = new JsonArray(AnyInstance.AnyInt, AnyInstance.AnyBool, null);
+ TestEvents(
+ ja,
+ arr => arr.Add(1),
+ new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>
+ {
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, ja, new JsonValueChangeEventArgs(1, JsonValueChange.Add, 3)),
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, ja, new JsonValueChangeEventArgs(1, JsonValueChange.Add, 3)),
+ });
+
+ TestEvents(
+ ja,
+ arr => arr.AddRange(AnyInstance.AnyString, AnyInstance.AnyDouble),
+ new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>
+ {
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, ja, new JsonValueChangeEventArgs(AnyInstance.AnyString, JsonValueChange.Add, 4)),
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, ja, new JsonValueChangeEventArgs(AnyInstance.AnyDouble, JsonValueChange.Add, 5)),
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, ja, new JsonValueChangeEventArgs(AnyInstance.AnyString, JsonValueChange.Add, 4)),
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, ja, new JsonValueChangeEventArgs(AnyInstance.AnyDouble, JsonValueChange.Add, 5)),
+ });
+
+ TestEvents(
+ ja,
+ arr => arr[1] = 2,
+ new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>
+ {
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, ja, new JsonValueChangeEventArgs(2, JsonValueChange.Replace, 1)),
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, ja, new JsonValueChangeEventArgs(AnyInstance.AnyBool, JsonValueChange.Replace, 1)),
+ });
+
+ ja = new JsonArray { 1, 2, 3 };
+ TestEvents(
+ ja,
+ arr => arr.Insert(1, "new value"),
+ new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>
+ {
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, ja, new JsonValueChangeEventArgs("new value", JsonValueChange.Add, 1)),
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, ja, new JsonValueChangeEventArgs("new value", JsonValueChange.Add, 1)),
+ });
+
+ TestEvents(
+ ja,
+ arr => arr.RemoveAt(1),
+ new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>
+ {
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, ja, new JsonValueChangeEventArgs("new value", JsonValueChange.Remove, 1)),
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, ja, new JsonValueChangeEventArgs("new value", JsonValueChange.Remove, 1)),
+ });
+
+ TestEvents(
+ ja,
+ arr => arr.Clear(),
+ new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>
+ {
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, ja, new JsonValueChangeEventArgs(null, JsonValueChange.Clear, 0)),
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, ja, new JsonValueChangeEventArgs(null, JsonValueChange.Clear, 0)),
+ });
+
+ ja = new JsonArray(1, 2, 3);
+ TestEvents(
+ ja,
+ arr => arr.Remove(new JsonPrimitive("Not there")),
+ new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>());
+
+ JsonValue elementInArray = ja[1];
+ TestEvents(
+ ja,
+ arr => arr.Remove(elementInArray),
+ new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>
+ {
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, ja, new JsonValueChangeEventArgs(elementInArray, JsonValueChange.Remove, 1)),
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, ja, new JsonValueChangeEventArgs(elementInArray, JsonValueChange.Remove, 1)),
+ });
+ }
+
+ [Fact]
+ public void NestedChangingEventTest()
+ {
+ JsonArray target = new JsonArray { new JsonArray { 1, 2 }, new JsonArray { 3, 4 } };
+ JsonArray child = target[1] as JsonArray;
+ TestEvents(
+ target,
+ arr => ((JsonArray)arr[1]).Add(5),
+ new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>());
+
+ target = new JsonArray();
+ child = new JsonArray(1, 2);
+ TestEvents(
+ target,
+ arr =>
+ {
+ arr.Add(child);
+ ((JsonArray)arr[0]).Add(5);
+ },
+ new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>
+ {
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, target, new JsonValueChangeEventArgs(child, JsonValueChange.Add, 0)),
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, target, new JsonValueChangeEventArgs(child, JsonValueChange.Add, 0)),
+ });
+ }
+
+ [Fact]
+ public void MultipleListenersTest()
+ {
+ for (int changingListeners = 0; changingListeners <= 2; changingListeners++)
+ {
+ for (int changedListeners = 0; changedListeners <= 2; changedListeners++)
+ {
+ MultipleListenersTestHelper<JsonArray>(
+ () => new JsonArray(1, 2),
+ delegate(JsonArray arr)
+ {
+ arr[1] = "hello";
+ arr.RemoveAt(0);
+ arr.Add("world");
+ arr.Clear();
+ },
+ new List<JsonValueChangeEventArgs>
+ {
+ new JsonValueChangeEventArgs("hello", JsonValueChange.Replace, 1),
+ new JsonValueChangeEventArgs(1, JsonValueChange.Remove, 0),
+ new JsonValueChangeEventArgs("world", JsonValueChange.Add, 1),
+ new JsonValueChangeEventArgs(null, JsonValueChange.Clear, 0),
+ },
+ new List<JsonValueChangeEventArgs>
+ {
+ new JsonValueChangeEventArgs(2, JsonValueChange.Replace, 1),
+ new JsonValueChangeEventArgs(1, JsonValueChange.Remove, 0),
+ new JsonValueChangeEventArgs("world", JsonValueChange.Add, 1),
+ new JsonValueChangeEventArgs(null, JsonValueChange.Clear, 0),
+ },
+ changingListeners,
+ changedListeners);
+ }
+ }
+ }
+
+ [Fact]
+ public void JsonTypeTest()
+ {
+ JsonArray target = AnyInstance.AnyJsonArray;
+ Assert.Equal(JsonType.Array, target.JsonType);
+ }
+
+ internal static void TestEvents<JsonValueType>(JsonValueType target, Action<JsonValueType> actionToTriggerEvent, List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>> expectedEvents) where JsonValueType : JsonValue
+ {
+ List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>> actualEvents = new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>();
+ EventHandler<JsonValueChangeEventArgs> changingHandler = delegate(object sender, JsonValueChangeEventArgs e)
+ {
+ actualEvents.Add(new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, sender as JsonValue, e));
+ };
+
+ EventHandler<JsonValueChangeEventArgs> changedHandler = delegate(object sender, JsonValueChangeEventArgs e)
+ {
+ actualEvents.Add(new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, sender as JsonValue, e));
+ };
+
+ target.Changing += new EventHandler<JsonValueChangeEventArgs>(changingHandler);
+ target.Changed += new EventHandler<JsonValueChangeEventArgs>(changedHandler);
+
+ actionToTriggerEvent(target);
+
+ target.Changing -= new EventHandler<JsonValueChangeEventArgs>(changingHandler);
+ target.Changed -= new EventHandler<JsonValueChangeEventArgs>(changedHandler);
+
+ ValidateExpectedEvents(expectedEvents, actualEvents);
+ }
+
+ private static void TestEvents(JsonArray array, Action<JsonArray> actionToTriggerEvent, List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>> expectedEvents)
+ {
+ TestEvents<JsonArray>(array, actionToTriggerEvent, expectedEvents);
+ }
+
+ private static void ValidateExpectedEvents(List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>> expectedEvents, List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>> actualEvents)
+ {
+ Assert.Equal(expectedEvents.Count, actualEvents.Count);
+ for (int i = 0; i < expectedEvents.Count; i++)
+ {
+ bool expectedIsChanging = expectedEvents[i].Item1;
+ bool actualIsChanging = expectedEvents[i].Item1;
+ Assert.Equal(expectedIsChanging, actualIsChanging);
+
+ JsonValue expectedSender = expectedEvents[i].Item2;
+ JsonValue actualSender = actualEvents[i].Item2;
+ Assert.Same(expectedSender, actualSender);
+
+ JsonValueChangeEventArgs expectedEventArgs = expectedEvents[i].Item3;
+ JsonValueChangeEventArgs actualEventArgs = actualEvents[i].Item3;
+ Assert.Equal(expectedEventArgs.Change, actualEventArgs.Change);
+ Assert.Equal(expectedEventArgs.Index, actualEventArgs.Index);
+ Assert.Equal(expectedEventArgs.Key, actualEventArgs.Key);
+
+ string expectedChild = expectedEventArgs.Child == null ? "null" : expectedEventArgs.Child.ToString();
+ string actualChild = actualEventArgs.Child == null ? "null" : actualEventArgs.Child.ToString();
+ Assert.Equal(expectedChild, actualChild);
+ }
+ }
+
+ internal static void MultipleListenersTestHelper<JsonValueType>(
+ Func<JsonValueType> createTarget,
+ Action<JsonValueType> actionToTriggerEvents,
+ List<JsonValueChangeEventArgs> expectedChangingEventArgs,
+ List<JsonValueChangeEventArgs> expectedChangedEventArgs,
+ int changingListeners,
+ int changedListeners) where JsonValueType : JsonValue
+ {
+ Console.WriteLine("Testing events on a {0} for {1} changING listeners and {2} changED listeners", typeof(JsonValueType).Name, changingListeners, changedListeners);
+ JsonValueType target = createTarget();
+ List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>[] actualChangingEvents = new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>[changingListeners];
+ List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>[] actualChangedEvents = new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>[changedListeners];
+ List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>> expectedChangingEvents = new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>(
+ expectedChangingEventArgs.Select((args) => new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, target, args)));
+ List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>> expectedChangedEvents = new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>(
+ expectedChangedEventArgs.Select((args) => new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, target, args)));
+
+ for (int i = 0; i < changingListeners; i++)
+ {
+ actualChangingEvents[i] = new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>();
+ int index = i;
+ target.Changing += delegate(object sender, JsonValueChangeEventArgs e)
+ {
+ actualChangingEvents[index].Add(new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, sender as JsonValue, e));
+ };
+ }
+
+ for (int i = 0; i < changedListeners; i++)
+ {
+ actualChangedEvents[i] = new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>();
+ int index = i;
+ target.Changed += delegate(object sender, JsonValueChangeEventArgs e)
+ {
+ actualChangedEvents[index].Add(new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, sender as JsonValue, e));
+ };
+ }
+
+ actionToTriggerEvents(target);
+
+ for (int i = 0; i < changingListeners; i++)
+ {
+ Console.WriteLine("Validating Changing events for listener {0}", i);
+ ValidateExpectedEvents(expectedChangingEvents, actualChangingEvents[i]);
+ }
+
+ for (int i = 0; i < changedListeners; i++)
+ {
+ Console.WriteLine("Validating Changed events for listener {0}", i);
+ ValidateExpectedEvents(expectedChangedEvents, actualChangedEvents[i]);
+ }
+ }
+
+ private static void ValidateJsonArrayItems(JsonArray jsonArray, IEnumerable<JsonValue> expectedItems)
+ {
+ List<JsonValue> expected = new List<JsonValue>(expectedItems);
+ Assert.Equal(expected.Count, jsonArray.Count);
+ for (int i = 0; i < expected.Count; i++)
+ {
+ Assert.Equal(expected[i], jsonArray[i]);
+ }
+ }
+ }
+}
diff --git a/test/System.Json.Test.Unit/JsonDefaultTest.cs b/test/System.Json.Test.Unit/JsonDefaultTest.cs
new file mode 100644
index 00000000..15549d66
--- /dev/null
+++ b/test/System.Json.Test.Unit/JsonDefaultTest.cs
@@ -0,0 +1,153 @@
+using System.IO;
+using System.Runtime.Serialization.Json;
+using Xunit;
+
+namespace System.Json
+{
+ public class JsonDefaultTest
+ {
+ const string IndexerNotSupportedMsgFormat = "'{0}' type indexer is not supported on JsonValue of 'JsonType.Default' type.";
+ const string OperationNotAllowedOnDefaultMsgFormat = "Operation not supported on JsonValue instances of 'JsonType.Default' type.";
+
+ [Fact]
+ public void PropertiesTest()
+ {
+ JsonValue target = AnyInstance.DefaultJsonValue;
+
+ Assert.Equal(JsonType.Default, target.JsonType);
+ Assert.Equal(0, target.Count);
+ Assert.Equal(false, target.ContainsKey("hello"));
+ Assert.Equal(false, target.ContainsKey(String.Empty));
+ }
+
+ [Fact]
+ public void SaveTest()
+ {
+ JsonValue target = AnyInstance.DefaultJsonValue;
+ using (MemoryStream ms = new MemoryStream())
+ {
+ ExceptionHelper.Throws<InvalidOperationException>(() => target.Save(ms), "Operation not supported on JsonValue instances of 'JsonType.Default' type.");
+ }
+ }
+
+ [Fact]
+ public void ToStringTest()
+ {
+ JsonValue target;
+
+ target = AnyInstance.DefaultJsonValue;
+ Assert.Equal(target.ToString(), "Default");
+ }
+
+ [Fact(Skip = "See bug #228569 in CSDMain")]
+ public void ReadAsTests()
+ {
+ JsonValue target = AnyInstance.DefaultJsonValue;
+ string typeName = target.GetType().FullName;
+
+ string errorMsgFormat = "Cannot read '{0}' as '{1}' type.";
+
+ ExceptionHelper.Throws<NotSupportedException>(delegate { target.ReadAs(typeof(bool)); }, String.Format(errorMsgFormat, typeName, typeof(bool)));
+ ExceptionHelper.Throws<NotSupportedException>(delegate { target.ReadAs(typeof(string)); }, String.Format(errorMsgFormat, typeName, typeof(string)));
+ ExceptionHelper.Throws<NotSupportedException>(delegate { target.ReadAs(typeof(JsonObject)); }, String.Format(errorMsgFormat, typeName, typeof(JsonObject)));
+
+ ExceptionHelper.Throws<NotSupportedException>(delegate { target.ReadAs<bool>(); }, String.Format(errorMsgFormat, typeName, typeof(bool)));
+ ExceptionHelper.Throws<NotSupportedException>(delegate { target.ReadAs<string>(); }, String.Format(errorMsgFormat, typeName, typeof(string)));
+ ExceptionHelper.Throws<NotSupportedException>(delegate { target.ReadAs<JsonObject>(); }, String.Format(errorMsgFormat, typeName, typeof(JsonObject)));
+
+ bool boolValue;
+ string stringValue;
+ JsonObject objValue;
+
+ object value;
+
+ Assert.False(target.TryReadAs(typeof(bool), out value), "TryReadAs expected to return false");
+ Assert.Null(value);
+
+ Assert.False(target.TryReadAs(typeof(string), out value), "TryReadAs expected to return false");
+ Assert.Null(value);
+
+ Assert.False(target.TryReadAs(typeof(JsonObject), out value), "TryReadAs expected to return false");
+ Assert.Null(value);
+
+ Assert.False(target.TryReadAs<bool>(out boolValue), "TryReadAs expected to return false");
+ Assert.False(boolValue);
+
+ Assert.False(target.TryReadAs<string>(out stringValue), "TryReadAs expected to return false");
+ Assert.Null(stringValue);
+
+ Assert.False(target.TryReadAs<JsonObject>(out objValue), "TryReadAs expected to return false");
+ Assert.Null(objValue);
+ }
+
+ [Fact]
+ public void ItemTests()
+ {
+ JsonValue target = AnyInstance.DefaultJsonValue;
+
+ ExceptionHelper.Throws<InvalidOperationException>(delegate { var v = target["MissingProperty"]; }, String.Format(IndexerNotSupportedMsgFormat, typeof(string).FullName));
+ ExceptionHelper.Throws<InvalidOperationException>(delegate { target["NewProperty"] = AnyInstance.AnyJsonValue1; }, String.Format(IndexerNotSupportedMsgFormat, typeof(string).FullName));
+ }
+
+ [Fact]
+ public void DynamicItemTests()
+ {
+ dynamic target = AnyInstance.DefaultJsonValue;
+
+ var getByKey = target["SomeKey"];
+ Assert.Same(getByKey, AnyInstance.DefaultJsonValue);
+
+ var getByIndex = target[10];
+ Assert.Same(getByIndex, AnyInstance.DefaultJsonValue);
+
+ ExceptionHelper.Throws<InvalidOperationException>(delegate { target["SomeKey"] = AnyInstance.AnyJsonObject; }, String.Format(IndexerNotSupportedMsgFormat, typeof(string).FullName));
+ ExceptionHelper.Throws<InvalidOperationException>(delegate { target[10] = AnyInstance.AnyJsonObject; }, String.Format(IndexerNotSupportedMsgFormat, typeof(int).FullName));
+ }
+
+ [Fact(Skip = "See bug #228569 in CSDMain")]
+ public void InvalidAssignmentValueTest()
+ {
+ JsonValue target;
+ JsonValue value = AnyInstance.DefaultJsonValue;
+
+ target = AnyInstance.AnyJsonArray;
+ ExceptionHelper.Throws<ArgumentException>(delegate { target[0] = value; }, OperationNotAllowedOnDefaultMsgFormat);
+
+ target = AnyInstance.AnyJsonObject;
+ ExceptionHelper.Throws<ArgumentException>(delegate { target["key"] = value; }, OperationNotAllowedOnDefaultMsgFormat);
+ }
+
+ [Fact]
+ public void DefaultConcatTest()
+ {
+ JsonValue jv = JsonValueExtensions.CreateFrom(AnyInstance.AnyPerson);
+ dynamic target = JsonValueExtensions.CreateFrom(AnyInstance.AnyPerson);
+ Person person = AnyInstance.AnyPerson;
+
+ Assert.Equal(JsonType.Default, target.Friends[100000].Name.JsonType);
+ Assert.Equal(JsonType.Default, target.Friends[0].Age.Minutes.JsonType);
+
+ JsonValue jv1 = target.MissingProperty as JsonValue;
+ Assert.NotNull(jv1);
+
+ JsonValue jv2 = target.MissingProperty1.MissingProperty2 as JsonValue;
+ Assert.NotNull(jv2);
+
+ Assert.Same(jv1, jv2);
+ Assert.Same(target.Person.Name.MissingProperty, AnyInstance.DefaultJsonValue);
+ }
+
+ [Fact]
+ public void CastingDefaultValueTest()
+ {
+ JsonValue jv = AnyInstance.DefaultJsonValue;
+ dynamic d = jv;
+
+ ExceptionHelper.Throws<InvalidCastException>(delegate { float p = (float)d; });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { byte p = (byte)d; });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { int p = (int)d; });
+
+ Assert.Null((string)d);
+ }
+ }
+}
diff --git a/test/System.Json.Test.Unit/JsonObjectTest.cs b/test/System.Json.Test.Unit/JsonObjectTest.cs
new file mode 100644
index 00000000..7f8bb4a4
--- /dev/null
+++ b/test/System.Json.Test.Unit/JsonObjectTest.cs
@@ -0,0 +1,787 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Runtime.Serialization.Json;
+using Xunit;
+
+namespace System.Json
+{
+ public class JsonObjectTest
+ {
+ [Fact]
+ public void JsonObjectConstructorEnumTest()
+ {
+ string key1 = AnyInstance.AnyString;
+ string key2 = AnyInstance.AnyString2;
+ JsonValue value1 = AnyInstance.AnyJsonValue1;
+ JsonValue value2 = AnyInstance.AnyJsonValue2;
+
+ List<KeyValuePair<string, JsonValue>> items = new List<KeyValuePair<string, JsonValue>>()
+ {
+ new KeyValuePair<string, JsonValue>(key1, value1),
+ new KeyValuePair<string, JsonValue>(key2, value2),
+ };
+
+ JsonObject target = new JsonObject(null);
+ Assert.Equal(0, target.Count);
+
+ target = new JsonObject(items);
+ Assert.Equal(2, target.Count);
+ ValidateJsonObjectItems(target, key1, value1, key2, value2);
+
+ // Invalid tests
+ items.Add(new KeyValuePair<string, JsonValue>(key1, AnyInstance.DefaultJsonValue));
+ ExceptionHelper.Throws<ArgumentException>(delegate { new JsonObject(items); });
+ }
+
+ [Fact]
+ public void JsonObjectConstructorParmsTest()
+ {
+ JsonObject target = new JsonObject();
+ Assert.Equal(0, target.Count);
+
+ string key1 = AnyInstance.AnyString;
+ string key2 = AnyInstance.AnyString2;
+ JsonValue value1 = AnyInstance.AnyJsonValue1;
+ JsonValue value2 = AnyInstance.AnyJsonValue2;
+
+ List<KeyValuePair<string, JsonValue>> items = new List<KeyValuePair<string, JsonValue>>()
+ {
+ new KeyValuePair<string, JsonValue>(key1, value1),
+ new KeyValuePair<string, JsonValue>(key2, value2),
+ };
+
+ target = new JsonObject(items[0], items[1]);
+ Assert.Equal(2, target.Count);
+ ValidateJsonObjectItems(target, key1, value1, key2, value2);
+
+ target = new JsonObject(items.ToArray());
+ Assert.Equal(2, target.Count);
+ ValidateJsonObjectItems(target, key1, value1, key2, value2);
+
+ // Invalid tests
+ items.Add(new KeyValuePair<string, JsonValue>(key1, AnyInstance.DefaultJsonValue));
+ ExceptionHelper.Throws<ArgumentException>(delegate { new JsonObject(items[0], items[1], items[2]); });
+ ExceptionHelper.Throws<ArgumentException>(delegate { new JsonObject(items.ToArray()); });
+ }
+
+ [Fact]
+ public void AddTest()
+ {
+ string key1 = AnyInstance.AnyString;
+ string key2 = AnyInstance.AnyString2;
+ JsonValue value1 = AnyInstance.AnyJsonValue1;
+ JsonValue value2 = AnyInstance.AnyJsonValue2;
+
+ JsonObject target;
+
+ target = new JsonObject();
+ target.Add(new KeyValuePair<string, JsonValue>(key1, value1));
+ Assert.Equal(1, target.Count);
+ Assert.True(target.ContainsKey(key1));
+ Assert.Equal(value1, target[key1]);
+
+ target.Add(key2, value2);
+ Assert.Equal(2, target.Count);
+ Assert.True(target.ContainsKey(key2));
+ Assert.Equal(value2, target[key2]);
+
+ ExceptionHelper.Throws<ArgumentNullException>(delegate { new JsonObject().Add(null, value1); });
+ ExceptionHelper.Throws<ArgumentNullException>(delegate { new JsonObject().Add(new KeyValuePair<string, JsonValue>(null, value1)); });
+
+ ExceptionHelper.Throws<ArgumentException>(delegate { new JsonObject().Add(key1, AnyInstance.DefaultJsonValue); });
+ ExceptionHelper.Throws<ArgumentException>(delegate { new JsonArray().Add(AnyInstance.DefaultJsonValue); });
+ }
+
+ [Fact]
+ public void AddRangeParamsTest()
+ {
+ string key1 = AnyInstance.AnyString;
+ string key2 = AnyInstance.AnyString2;
+ JsonValue value1 = AnyInstance.AnyJsonValue1;
+ JsonValue value2 = AnyInstance.AnyJsonValue2;
+
+ List<KeyValuePair<string, JsonValue>> items = new List<KeyValuePair<string, JsonValue>>()
+ {
+ new KeyValuePair<string, JsonValue>(key1, value1),
+ new KeyValuePair<string, JsonValue>(key2, value2),
+ };
+
+ JsonObject target;
+
+ target = new JsonObject();
+ target.AddRange(items[0], items[1]);
+ Assert.Equal(2, target.Count);
+ ValidateJsonObjectItems(target, key1, value1, key2, value2);
+
+ target = new JsonObject();
+ target.AddRange(items.ToArray());
+ Assert.Equal(2, target.Count);
+ ValidateJsonObjectItems(target, key1, value1, key2, value2);
+
+ ExceptionHelper.Throws<ArgumentNullException>(delegate { new JsonObject().AddRange((KeyValuePair<string, JsonValue>[])null); });
+ ExceptionHelper.Throws<ArgumentNullException>(delegate { new JsonObject().AddRange((IEnumerable<KeyValuePair<string, JsonValue>>)null); });
+
+ items[1] = new KeyValuePair<string, JsonValue>(key2, AnyInstance.DefaultJsonValue);
+ ExceptionHelper.Throws<ArgumentException>(delegate { new JsonObject().AddRange(items.ToArray()); });
+ ExceptionHelper.Throws<ArgumentException>(delegate { new JsonObject().AddRange(items[0], items[1]); });
+ }
+
+ [Fact]
+ public void AddRangeEnumTest()
+ {
+ string key1 = AnyInstance.AnyString;
+ string key2 = AnyInstance.AnyString2;
+ JsonValue value1 = AnyInstance.AnyJsonValue1;
+ JsonValue value2 = AnyInstance.AnyJsonValue2;
+
+ List<KeyValuePair<string, JsonValue>> items = new List<KeyValuePair<string, JsonValue>>()
+ {
+ new KeyValuePair<string, JsonValue>(key1, value1),
+ new KeyValuePair<string, JsonValue>(key2, value2),
+ };
+
+ JsonObject target;
+
+ target = new JsonObject();
+ target.AddRange(items);
+ Assert.Equal(2, target.Count);
+ ValidateJsonObjectItems(target, key1, value1, key2, value2);
+
+ ExceptionHelper.Throws<ArgumentNullException>(delegate { new JsonObject().AddRange(null); });
+
+ items[1] = new KeyValuePair<string, JsonValue>(key2, AnyInstance.DefaultJsonValue);
+ ExceptionHelper.Throws<ArgumentException>(delegate { new JsonObject().AddRange(items); });
+ }
+
+ [Fact]
+ public void ClearTest()
+ {
+ string key1 = AnyInstance.AnyString;
+ string key2 = AnyInstance.AnyString2;
+ JsonValue value1 = AnyInstance.AnyJsonValue1;
+ JsonValue value2 = AnyInstance.AnyJsonValue2;
+
+ JsonObject target = new JsonObject();
+ target.Add(key1, value1);
+ target.Clear();
+ Assert.Equal(0, target.Count);
+ Assert.False(target.ContainsKey(key1));
+
+ target.Add(key2, value2);
+ Assert.Equal(1, target.Count);
+ Assert.False(target.ContainsKey(key1));
+ Assert.True(target.ContainsKey(key2));
+ }
+
+ [Fact]
+ public void ContainsKeyTest()
+ {
+ string key1 = AnyInstance.AnyString;
+ JsonValue value1 = AnyInstance.AnyJsonValue1;
+
+ JsonObject target = new JsonObject();
+ Assert.False(target.ContainsKey(key1));
+ target.Add(key1, value1);
+ Assert.True(target.ContainsKey(key1));
+ target.Clear();
+ Assert.False(target.ContainsKey(key1));
+
+ ExceptionHelper.Throws<ArgumentNullException>(delegate { new JsonObject().ContainsKey(null); });
+ }
+
+ [Fact]
+ public void CopyToTest()
+ {
+ string key1 = AnyInstance.AnyString;
+ string key2 = AnyInstance.AnyString2;
+ JsonValue value1 = AnyInstance.AnyJsonValue1;
+ JsonValue value2 = AnyInstance.AnyJsonValue2;
+
+ JsonObject target = new JsonObject { { key1, value1 }, { key2, value2 } };
+
+ KeyValuePair<string, JsonValue>[] array = new KeyValuePair<string, JsonValue>[target.Count + 1];
+
+ target.CopyTo(array, 1);
+ int index1 = key1 == array[1].Key ? 1 : 2;
+ int index2 = index1 == 1 ? 2 : 1;
+
+ Assert.Equal(key1, array[index1].Key);
+ Assert.Equal(value1, array[index1].Value);
+ Assert.Equal(key2, array[index2].Key);
+ Assert.Equal(value2, array[index2].Value);
+
+ ExceptionHelper.Throws<ArgumentNullException>(() => target.CopyTo(null, 0));
+ ExceptionHelper.Throws<ArgumentOutOfRangeException>(() => target.CopyTo(array, -1));
+ ExceptionHelper.Throws<ArgumentException>(() => target.CopyTo(array, array.Length - target.Count + 1));
+ }
+
+ [Fact]
+ public void CreateFromComplexTypeTest()
+ {
+ Assert.Null(JsonValueExtensions.CreateFrom(null));
+
+ Person anyObject = AnyInstance.AnyPerson;
+
+ JsonObject jv = JsonValueExtensions.CreateFrom(anyObject) as JsonObject;
+ Assert.NotNull(jv);
+ Assert.Equal(4, jv.Count);
+ foreach (string key in "Name Age Address".Split())
+ {
+ Assert.True(jv.ContainsKey(key));
+ }
+
+ Assert.Equal(AnyInstance.AnyString, (string)jv["Name"]);
+ Assert.Equal(AnyInstance.AnyInt, (int)jv["Age"]);
+
+ JsonObject nestedObject = jv["Address"] as JsonObject;
+ Assert.NotNull(nestedObject);
+ Assert.Equal(3, nestedObject.Count);
+ foreach (string key in "Street City State".Split())
+ {
+ Assert.True(nestedObject.ContainsKey(key));
+ }
+
+ Assert.Equal(Address.AnyStreet, (string)nestedObject["Street"]);
+ Assert.Equal(Address.AnyCity, (string)nestedObject["City"]);
+ Assert.Equal(Address.AnyState, (string)nestedObject["State"]);
+ }
+
+ [Fact]
+ public void ReadAsComplexTypeTest()
+ {
+ JsonObject target = new JsonObject
+ {
+ { "Name", AnyInstance.AnyString },
+ { "Age", AnyInstance.AnyInt },
+ { "Address", new JsonObject { { "Street", Address.AnyStreet }, { "City", Address.AnyCity }, { "State", Address.AnyState } } },
+ };
+
+ Person person = target.ReadAsType<Person>();
+ Assert.Equal(AnyInstance.AnyString, person.Name);
+ Assert.Equal(AnyInstance.AnyInt, person.Age);
+ Assert.NotNull(person.Address);
+ Assert.Equal(Address.AnyStreet, person.Address.Street);
+ Assert.Equal(Address.AnyCity, person.Address.City);
+ Assert.Equal(Address.AnyState, person.Address.State);
+ }
+
+ [Fact]
+ public void GetEnumeratorTest()
+ {
+ string key1 = AnyInstance.AnyString;
+ string key2 = AnyInstance.AnyString2;
+ JsonValue value1 = AnyInstance.AnyJsonValue1;
+ JsonValue value2 = AnyInstance.AnyJsonValue2;
+
+ JsonObject target = new JsonObject { { key1, value1 }, { key2, value2 } };
+
+ IEnumerator<KeyValuePair<string, JsonValue>> enumerator = target.GetEnumerator();
+ Assert.True(enumerator.MoveNext());
+ bool key1IsFirst = key1 == enumerator.Current.Key;
+ if (key1IsFirst)
+ {
+ Assert.Equal(key1, enumerator.Current.Key);
+ Assert.Equal(value1, enumerator.Current.Value);
+ }
+ else
+ {
+ Assert.Equal(key2, enumerator.Current.Key);
+ Assert.Equal(value2, enumerator.Current.Value);
+ }
+
+ Assert.True(enumerator.MoveNext());
+ if (key1IsFirst)
+ {
+ Assert.Equal(key2, enumerator.Current.Key);
+ Assert.Equal(value2, enumerator.Current.Value);
+ }
+ else
+ {
+ Assert.Equal(key1, enumerator.Current.Key);
+ Assert.Equal(value1, enumerator.Current.Value);
+ }
+
+ Assert.False(enumerator.MoveNext());
+ }
+
+ [Fact]
+ public void RemoveTest()
+ {
+ string key1 = AnyInstance.AnyString;
+ string key2 = AnyInstance.AnyString2;
+ JsonValue value1 = AnyInstance.AnyJsonValue1;
+ JsonValue value2 = AnyInstance.AnyJsonValue2;
+
+ JsonObject target = new JsonObject { { key1, value1 }, { key2, value2 } };
+
+ Assert.True(target.ContainsKey(key1));
+ Assert.True(target.ContainsKey(key2));
+ Assert.Equal(2, target.Count);
+
+ Assert.True(target.Remove(key2));
+ Assert.True(target.ContainsKey(key1));
+ Assert.False(target.ContainsKey(key2));
+ Assert.Equal(1, target.Count);
+
+ Assert.False(target.Remove(key2));
+ Assert.True(target.ContainsKey(key1));
+ Assert.False(target.ContainsKey(key2));
+ Assert.Equal(1, target.Count);
+ }
+
+ [Fact]
+ public void ToStringTest()
+ {
+ JsonObject target = new JsonObject();
+
+ JsonValue item1 = AnyInstance.AnyJsonValue1 ?? "not null";
+ JsonValue item2 = null;
+ JsonValue item3 = AnyInstance.AnyJsonValue2 ?? "not null";
+ JsonValue item4 = AnyInstance.AnyJsonValue3 ?? "not null";
+ target.Add("item1", item1);
+ target.Add("item2", item2);
+ target.Add("item3", item3);
+ target.Add("", item4);
+
+ string expected = String.Format(CultureInfo.InvariantCulture, "{{\"item1\":{0},\"item2\":null,\"item3\":{1},\"\":{2}}}", item1.ToString(), item3.ToString(), item4.ToString());
+ Assert.Equal<string>(expected, target.ToString());
+
+ string json = "{\r\n \"item1\": \"hello\",\r\n \"item2\": null,\r\n \"item3\": [\r\n 1,\r\n 2,\r\n 3\r\n ],\r\n \"\": \"notnull\"\r\n}";
+ target = JsonValue.Parse(json) as JsonObject;
+
+ Assert.Equal<string>(json.Replace("\r\n", "").Replace(" ", ""), target.ToString());
+ }
+
+ [Fact]
+ public void ContainsKVPTest()
+ {
+ JsonObject target = new JsonObject();
+ KeyValuePair<string, JsonValue> item = new KeyValuePair<string, JsonValue>(AnyInstance.AnyString, AnyInstance.AnyJsonValue1);
+ KeyValuePair<string, JsonValue> item2 = new KeyValuePair<string, JsonValue>(AnyInstance.AnyString2, AnyInstance.AnyJsonValue2);
+ target.Add(item);
+ Assert.True(((ICollection<KeyValuePair<string, JsonValue>>)target).Contains(item));
+ Assert.False(((ICollection<KeyValuePair<string, JsonValue>>)target).Contains(item2));
+ }
+
+ [Fact]
+ public void RemoveKVPTest()
+ {
+ JsonObject target = new JsonObject();
+ KeyValuePair<string, JsonValue> item1 = new KeyValuePair<string, JsonValue>(AnyInstance.AnyString, AnyInstance.AnyJsonValue1);
+ KeyValuePair<string, JsonValue> item2 = new KeyValuePair<string, JsonValue>(AnyInstance.AnyString2, AnyInstance.AnyJsonValue2);
+ target.AddRange(item1, item2);
+
+ Assert.Equal(2, target.Count);
+ Assert.True(((ICollection<KeyValuePair<string, JsonValue>>)target).Contains(item1));
+ Assert.True(((ICollection<KeyValuePair<string, JsonValue>>)target).Contains(item2));
+
+ Assert.True(((ICollection<KeyValuePair<string, JsonValue>>)target).Remove(item1));
+ Assert.Equal(1, target.Count);
+ Assert.False(((ICollection<KeyValuePair<string, JsonValue>>)target).Contains(item1));
+ Assert.True(((ICollection<KeyValuePair<string, JsonValue>>)target).Contains(item2));
+
+ Assert.False(((ICollection<KeyValuePair<string, JsonValue>>)target).Remove(item1));
+ Assert.Equal(1, target.Count);
+ Assert.False(((ICollection<KeyValuePair<string, JsonValue>>)target).Contains(item1));
+ Assert.True(((ICollection<KeyValuePair<string, JsonValue>>)target).Contains(item2));
+ }
+
+ [Fact]
+ public void GetEnumeratorTest1()
+ {
+ string key1 = AnyInstance.AnyString;
+ string key2 = AnyInstance.AnyString2;
+ JsonValue value1 = AnyInstance.AnyJsonValue1;
+ JsonValue value2 = AnyInstance.AnyJsonValue2;
+
+ JsonObject target = new JsonObject { { key1, value1 }, { key2, value2 } };
+
+ IEnumerator enumerator = ((IEnumerable)target).GetEnumerator();
+ Assert.True(enumerator.MoveNext());
+ Assert.IsType<KeyValuePair<string, JsonValue>>(enumerator.Current);
+ KeyValuePair<string, JsonValue> current = (KeyValuePair<string, JsonValue>)enumerator.Current;
+
+ bool key1IsFirst = key1 == current.Key;
+ if (key1IsFirst)
+ {
+ Assert.Equal(key1, current.Key);
+ Assert.Equal(value1, current.Value);
+ }
+ else
+ {
+ Assert.Equal(key2, current.Key);
+ Assert.Equal(value2, current.Value);
+ }
+
+ Assert.True(enumerator.MoveNext());
+ Assert.IsType<KeyValuePair<string, JsonValue>>(enumerator.Current);
+ current = (KeyValuePair<string, JsonValue>)enumerator.Current;
+ if (key1IsFirst)
+ {
+ Assert.Equal(key2, current.Key);
+ Assert.Equal(value2, current.Value);
+ }
+ else
+ {
+ Assert.Equal(key1, current.Key);
+ Assert.Equal(value1, current.Value);
+ }
+
+ Assert.False(enumerator.MoveNext());
+ }
+
+ [Fact]
+ public void TryGetValueTest()
+ {
+ string key1 = AnyInstance.AnyString;
+ string key2 = AnyInstance.AnyString2;
+ JsonValue value1 = AnyInstance.AnyJsonValue1;
+ JsonValue value2 = AnyInstance.AnyJsonValue2;
+
+ JsonObject target = new JsonObject { { key1, value1 }, { key2, value2 } };
+
+ JsonValue value;
+ Assert.True(target.TryGetValue(key2, out value));
+ Assert.Equal(value2, value);
+
+ Assert.False(target.TryGetValue("not a key", out value));
+ Assert.Null(value);
+ }
+
+ [Fact]
+ public void GetValueOrDefaultTest()
+ {
+ bool boolValue;
+ JsonValue target;
+ JsonValue jsonValue;
+
+ Person person = AnyInstance.AnyPerson;
+ JsonObject jo = JsonValueExtensions.CreateFrom(person) as JsonObject;
+ Assert.Equal<int>(person.Age, jo.ValueOrDefault("Age").ReadAs<int>()); // JsonPrimitive
+
+ Assert.Equal<string>(person.Address.ToString(), jo.ValueOrDefault("Address").ReadAsType<Address>().ToString()); // JsonObject
+ Assert.Equal<int>(person.Friends.Count, jo.ValueOrDefault("Friends").Count); // JsonArray
+
+ target = jo.ValueOrDefault("Address").ValueOrDefault("City"); // JsonPrimitive
+ Assert.NotNull(target);
+ Assert.Equal<string>(person.Address.City, target.ReadAs<string>());
+
+ target = jo.ValueOrDefault("Address", "City"); // JsonPrimitive
+ Assert.NotNull(target);
+ Assert.Equal<string>(person.Address.City, target.ReadAs<string>());
+
+ target = jo.ValueOrDefault("Address").ValueOrDefault("NonExistentProp").ValueOrDefault("NonExistentProp2"); // JsonObject
+ Assert.Equal(JsonType.Default, target.JsonType);
+ Assert.NotNull(target);
+ Assert.False(target.TryReadAs<bool>(out boolValue));
+ Assert.True(target.TryReadAs<JsonValue>(out jsonValue));
+
+ target = jo.ValueOrDefault("Address", "NonExistentProp", "NonExistentProp2"); // JsonObject
+ Assert.Equal(JsonType.Default, target.JsonType);
+ Assert.NotNull(target);
+ Assert.False(target.TryReadAs<bool>(out boolValue));
+ Assert.True(target.TryReadAs<JsonValue>(out jsonValue));
+ Assert.Same(target, jsonValue);
+ }
+
+ [Fact]
+ public void CountTest()
+ {
+ string key1 = AnyInstance.AnyString;
+ string key2 = AnyInstance.AnyString2;
+ JsonValue value1 = AnyInstance.AnyJsonValue1;
+ JsonValue value2 = AnyInstance.AnyJsonValue2;
+
+ JsonObject target = new JsonObject();
+ Assert.Equal(0, target.Count);
+ target.Add(key1, value1);
+ Assert.Equal(1, target.Count);
+ target.Add(key2, value2);
+ Assert.Equal(2, target.Count);
+ target.Remove(key2);
+ Assert.Equal(1, target.Count);
+ }
+
+ [Fact]
+ public void ItemTest()
+ {
+ string key1 = AnyInstance.AnyString;
+ string key2 = AnyInstance.AnyString2;
+ JsonValue value1 = AnyInstance.AnyJsonValue1;
+ JsonValue value2 = AnyInstance.AnyJsonValue2;
+ JsonValue value3 = AnyInstance.AnyJsonValue3;
+
+ JsonObject target;
+
+ target = new JsonObject { { key1, value1 }, { key2, value2 } };
+ Assert.Equal(value1, target[key1]);
+ Assert.Equal(value2, target[key2]);
+ target[key1] = value3;
+ Assert.Equal(value3, target[key1]);
+ Assert.Equal(value2, target[key2]);
+
+ ExceptionHelper.Throws<KeyNotFoundException>(delegate { var o = target["not a key"]; });
+ ExceptionHelper.Throws<ArgumentNullException>(delegate { var o = target[null]; });
+ ExceptionHelper.Throws<ArgumentNullException>(delegate { target[null] = 123; });
+ ExceptionHelper.Throws<ArgumentException>(delegate { target[key1] = AnyInstance.DefaultJsonValue; });
+ }
+
+ [Fact]
+ public void ChangingEventsTest()
+ {
+ const string key1 = "first";
+ const string key2 = "second";
+ const string key3 = "third";
+ const string key4 = "fourth";
+ const string key5 = "fifth";
+ JsonObject jo = new JsonObject
+ {
+ { key1, AnyInstance.AnyString },
+ { key2, AnyInstance.AnyBool },
+ { key3, null },
+ };
+
+ TestEvents(
+ jo,
+ obj => obj.Add(key4, 1),
+ new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>
+ {
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, jo, new JsonValueChangeEventArgs(1, JsonValueChange.Add, key4)),
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, jo, new JsonValueChangeEventArgs(1, JsonValueChange.Add, key4)),
+ });
+
+ TestEvents(
+ jo,
+ obj => obj[key2] = 2,
+ new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>
+ {
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, jo, new JsonValueChangeEventArgs(2, JsonValueChange.Replace, key2)),
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, jo, new JsonValueChangeEventArgs(AnyInstance.AnyBool, JsonValueChange.Replace, key2)),
+ });
+
+ TestEvents(
+ jo,
+ obj => obj[key5] = 3,
+ new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>
+ {
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, jo, new JsonValueChangeEventArgs(3, JsonValueChange.Add, key5)),
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, jo, new JsonValueChangeEventArgs(3, JsonValueChange.Add, key5)),
+ });
+
+ jo.Remove(key4);
+ jo.Remove(key5);
+
+ TestEvents(
+ jo,
+ obj => obj.AddRange(new JsonObject { { key4, AnyInstance.AnyString }, { key5, AnyInstance.AnyDouble } }),
+ new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>
+ {
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, jo, new JsonValueChangeEventArgs(AnyInstance.AnyString, JsonValueChange.Add, key4)),
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, jo, new JsonValueChangeEventArgs(AnyInstance.AnyDouble, JsonValueChange.Add, key5)),
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, jo, new JsonValueChangeEventArgs(AnyInstance.AnyString, JsonValueChange.Add, key4)),
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, jo, new JsonValueChangeEventArgs(AnyInstance.AnyDouble, JsonValueChange.Add, key5)),
+ });
+
+ TestEvents(
+ jo,
+ obj => obj.Remove(key5),
+ new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>
+ {
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, jo, new JsonValueChangeEventArgs(AnyInstance.AnyDouble, JsonValueChange.Remove, key5)),
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, jo, new JsonValueChangeEventArgs(AnyInstance.AnyDouble, JsonValueChange.Remove, key5)),
+ });
+
+ TestEvents(
+ jo,
+ obj => obj.Remove("not there"),
+ new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>());
+
+ jo = new JsonObject { { key1, 1 }, { key2, 2 }, { key3, 3 } };
+
+ TestEvents(
+ jo,
+ obj => obj.Clear(),
+ new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>
+ {
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, jo, new JsonValueChangeEventArgs(null, JsonValueChange.Clear, null)),
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, jo, new JsonValueChangeEventArgs(null, JsonValueChange.Clear, null)),
+ });
+
+ jo = new JsonObject { { key1, 1 }, { key2, 2 }, { key3, 3 } };
+ TestEvents(
+ jo,
+ obj => ((IDictionary<string, JsonValue>)obj).Remove(new KeyValuePair<string, JsonValue>(key2, jo[key2])),
+ new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>
+ {
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, jo, new JsonValueChangeEventArgs(2, JsonValueChange.Remove, key2)),
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, jo, new JsonValueChangeEventArgs(2, JsonValueChange.Remove, key2)),
+ });
+
+ TestEvents(
+ jo,
+ obj => ((IDictionary<string, JsonValue>)obj).Remove(new KeyValuePair<string, JsonValue>("key not in object", jo[key1])),
+ new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>
+ {
+ });
+
+ TestEvents(
+ jo,
+ obj => ((IDictionary<string, JsonValue>)obj).Remove(new KeyValuePair<string, JsonValue>(key1, "different object")),
+ new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>
+ {
+ });
+
+ ExceptionHelper.Throws<ArgumentNullException>(() => new JsonValueChangeEventArgs(1, JsonValueChange.Add, null));
+ }
+
+ [Fact]
+ public void NestedChangingEventTest()
+ {
+ const string key1 = "first";
+
+ JsonObject target = new JsonObject { { key1, new JsonArray { 1, 2 } } };
+ JsonArray child = target[key1] as JsonArray;
+ TestEvents(
+ target,
+ obj => ((JsonArray)obj[key1]).Add(5),
+ new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>());
+
+ target = new JsonObject();
+ child = new JsonArray(1, 2);
+ TestEvents(
+ target,
+ obj =>
+ {
+ obj.Add(key1, child);
+ ((JsonArray)obj[key1]).Add(5);
+ },
+ new List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>>
+ {
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(true, target, new JsonValueChangeEventArgs(child, JsonValueChange.Add, key1)),
+ new Tuple<bool, JsonValue, JsonValueChangeEventArgs>(false, target, new JsonValueChangeEventArgs(child, JsonValueChange.Add, key1)),
+ });
+ }
+
+ [Fact]
+ public void MultipleListenersTest()
+ {
+ const string key1 = "first";
+ const string key2 = "second";
+ const string key3 = "third";
+
+ for (int changingListeners = 0; changingListeners <= 2; changingListeners++)
+ {
+ for (int changedListeners = 0; changedListeners <= 2; changedListeners++)
+ {
+ JsonArrayTest.MultipleListenersTestHelper<JsonObject>(
+ () => new JsonObject { { key1, 1 }, { key2, 2 } },
+ delegate(JsonObject obj)
+ {
+ obj[key2] = "hello";
+ obj.Remove(key1);
+ obj.Add(key3, "world");
+ obj.Clear();
+ },
+ new List<JsonValueChangeEventArgs>
+ {
+ new JsonValueChangeEventArgs("hello", JsonValueChange.Replace, key2),
+ new JsonValueChangeEventArgs(1, JsonValueChange.Remove, key1),
+ new JsonValueChangeEventArgs("world", JsonValueChange.Add, key3),
+ new JsonValueChangeEventArgs(null, JsonValueChange.Clear, null),
+ },
+ new List<JsonValueChangeEventArgs>
+ {
+ new JsonValueChangeEventArgs(2, JsonValueChange.Replace, key2),
+ new JsonValueChangeEventArgs(1, JsonValueChange.Remove, key1),
+ new JsonValueChangeEventArgs("world", JsonValueChange.Add, key3),
+ new JsonValueChangeEventArgs(null, JsonValueChange.Clear, null),
+ },
+ changingListeners,
+ changedListeners);
+ }
+ }
+ }
+
+ [Fact]
+ public void JsonTypeTest()
+ {
+ JsonObject target = AnyInstance.AnyJsonObject;
+ Assert.Equal(JsonType.Object, target.JsonType);
+ }
+
+ [Fact]
+ public void KeysTest()
+ {
+ string key1 = AnyInstance.AnyString;
+ string key2 = AnyInstance.AnyString2;
+ JsonValue value1 = AnyInstance.AnyJsonValue1;
+ JsonValue value2 = AnyInstance.AnyJsonValue2;
+
+ JsonObject target = new JsonObject { { key1, value1 }, { key2, value2 } };
+
+ List<string> expected = new List<string> { key1, key2 };
+ List<string> actual = new List<string>(target.Keys);
+
+ Assert.Equal(expected.Count, actual.Count);
+
+ expected.Sort();
+ actual.Sort();
+ for (int i = 0; i < expected.Count; i++)
+ {
+ Assert.Equal(expected[i], actual[i]);
+ }
+ }
+
+ [Fact]
+ public void IsReadOnlyTest()
+ {
+ JsonObject target = AnyInstance.AnyJsonObject;
+ Assert.False(((ICollection<KeyValuePair<string, JsonValue>>)target).IsReadOnly);
+ }
+
+ [Fact]
+ public void ValuesTest()
+ {
+ string key1 = AnyInstance.AnyString;
+ string key2 = AnyInstance.AnyString2;
+ JsonValue value1 = AnyInstance.AnyJsonValue1;
+ JsonValue value2 = AnyInstance.AnyJsonValue2;
+
+ JsonObject target = new JsonObject { { key1, value1 }, { key2, value2 } };
+
+ List<JsonValue> values = new List<JsonValue>(target.Values);
+ Assert.Equal(2, values.Count);
+ bool value1IsFirst = value1 == values[0];
+ Assert.True(value1IsFirst || value1 == values[1]);
+ Assert.Equal(value2, values[value1IsFirst ? 1 : 0]);
+ }
+
+ private static void ValidateJsonObjectItems(JsonObject jsonObject, params object[] keyValuePairs)
+ {
+ Dictionary<string, JsonValue> expected = new Dictionary<string, JsonValue>();
+ Assert.True((keyValuePairs.Length % 2) == 0, "Test error");
+ for (int i = 0; i < keyValuePairs.Length; i += 2)
+ {
+ Assert.IsType<String>(keyValuePairs[i]);
+ Assert.IsAssignableFrom<JsonValue>(keyValuePairs[i + 1]);
+ expected.Add((string)keyValuePairs[i], (JsonValue)keyValuePairs[i + 1]);
+ }
+ }
+
+ private static void ValidateJsonObjectItems(JsonObject jsonObject, Dictionary<string, JsonValue> expected)
+ {
+ Assert.Equal(expected.Count, jsonObject.Count);
+ foreach (string key in expected.Keys)
+ {
+ Assert.True(jsonObject.ContainsKey(key));
+ Assert.Equal(expected[key], jsonObject[key]);
+ }
+ }
+
+ private static void TestEvents(JsonObject obj, Action<JsonObject> actionToTriggerEvent, List<Tuple<bool, JsonValue, JsonValueChangeEventArgs>> expectedEvents)
+ {
+ JsonArrayTest.TestEvents<JsonObject>(obj, actionToTriggerEvent, expectedEvents);
+ }
+ }
+}
diff --git a/test/System.Json.Test.Unit/JsonPrimitiveTest.cs b/test/System.Json.Test.Unit/JsonPrimitiveTest.cs
new file mode 100644
index 00000000..225d17cf
--- /dev/null
+++ b/test/System.Json.Test.Unit/JsonPrimitiveTest.cs
@@ -0,0 +1,410 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Runtime.Serialization.Json;
+using System.Text;
+using Xunit;
+
+namespace System.Json
+{
+ public class JsonPrimitiveTest
+ {
+ const string DateTimeFormat = "yyyy-MM-ddTHH:mm:ss.fffK";
+
+ [Fact]
+ public void JsonPrimitiveConstructorTest()
+ {
+ Assert.Equal(AnyInstance.AnyString, (string)(new JsonPrimitive(AnyInstance.AnyString)));
+ Assert.Equal(AnyInstance.AnyChar, (char)(new JsonPrimitive(AnyInstance.AnyChar)));
+ Assert.Equal(AnyInstance.AnyUri, (Uri)(new JsonPrimitive(AnyInstance.AnyUri)));
+ Assert.Equal(AnyInstance.AnyGuid, (Guid)(new JsonPrimitive(AnyInstance.AnyGuid)));
+ Assert.Equal(AnyInstance.AnyDateTime, (DateTime)(new JsonPrimitive(AnyInstance.AnyDateTime)));
+ Assert.Equal(AnyInstance.AnyDateTimeOffset, (DateTimeOffset)(new JsonPrimitive(AnyInstance.AnyDateTimeOffset)));
+ Assert.Equal(AnyInstance.AnyBool, (bool)(new JsonPrimitive(AnyInstance.AnyBool)));
+ Assert.Equal(AnyInstance.AnyByte, (byte)(new JsonPrimitive(AnyInstance.AnyByte)));
+ Assert.Equal(AnyInstance.AnyShort, (short)(new JsonPrimitive(AnyInstance.AnyShort)));
+ Assert.Equal(AnyInstance.AnyInt, (int)(new JsonPrimitive(AnyInstance.AnyInt)));
+ Assert.Equal(AnyInstance.AnyLong, (long)(new JsonPrimitive(AnyInstance.AnyLong)));
+ Assert.Equal(AnyInstance.AnySByte, (sbyte)(new JsonPrimitive(AnyInstance.AnySByte)));
+ Assert.Equal(AnyInstance.AnyUShort, (ushort)(new JsonPrimitive(AnyInstance.AnyUShort)));
+ Assert.Equal(AnyInstance.AnyUInt, (uint)(new JsonPrimitive(AnyInstance.AnyUInt)));
+ Assert.Equal(AnyInstance.AnyULong, (ulong)(new JsonPrimitive(AnyInstance.AnyULong)));
+ Assert.Equal(AnyInstance.AnyDecimal, (decimal)(new JsonPrimitive(AnyInstance.AnyDecimal)));
+ Assert.Equal(AnyInstance.AnyFloat, (float)(new JsonPrimitive(AnyInstance.AnyFloat)));
+ Assert.Equal(AnyInstance.AnyDouble, (double)(new JsonPrimitive(AnyInstance.AnyDouble)));
+ }
+
+ [Fact]
+ public void ValueTest()
+ {
+ object[] values =
+ {
+ AnyInstance.AnyInt, AnyInstance.AnyString, AnyInstance.AnyGuid, AnyInstance.AnyDecimal, AnyInstance.AnyBool, AnyInstance.AnyDateTime
+ };
+
+ foreach (object value in values)
+ {
+ JsonPrimitive jp;
+ bool success = JsonPrimitive.TryCreate(value, out jp);
+ Assert.True(success);
+ Assert.Equal(value, jp.Value);
+ }
+ }
+
+ [Fact]
+ public void TryCreateTest()
+ {
+ object[] numberValues =
+ {
+ AnyInstance.AnyByte, AnyInstance.AnySByte, AnyInstance.AnyShort, AnyInstance.AnyDecimal,
+ AnyInstance.AnyDouble, AnyInstance.AnyShort, AnyInstance.AnyInt, AnyInstance.AnyLong,
+ AnyInstance.AnyUShort, AnyInstance.AnyUInt, AnyInstance.AnyULong, AnyInstance.AnyFloat
+ };
+
+ object[] booleanValues =
+ {
+ true, false
+ };
+
+
+ object[] stringValues =
+ {
+ AnyInstance.AnyString, AnyInstance.AnyChar,
+ AnyInstance.AnyDateTime, AnyInstance.AnyDateTimeOffset,
+ AnyInstance.AnyGuid, AnyInstance.AnyUri
+ };
+
+ CheckValues(numberValues, JsonType.Number);
+ CheckValues(booleanValues, JsonType.Boolean);
+ CheckValues(stringValues, JsonType.String);
+ }
+
+ [Fact]
+ public void TryCreateInvalidTest()
+ {
+ bool success;
+ JsonPrimitive target;
+
+ object[] values =
+ {
+ AnyInstance.AnyJsonArray, AnyInstance.AnyJsonObject, AnyInstance.AnyJsonPrimitive,
+ null, AnyInstance.DefaultJsonValue, AnyInstance.AnyDynamic, AnyInstance.AnyAddress,
+ AnyInstance.AnyPerson
+ };
+
+ foreach (object value in values)
+ {
+ success = JsonPrimitive.TryCreate(value, out target);
+ Assert.False(success);
+ Assert.Null(target);
+ }
+ }
+
+ [Fact]
+ public void NumberToNumberConversionTest()
+ {
+ long longValue;
+ Assert.Equal((long)AnyInstance.AnyInt, (long)(new JsonPrimitive(AnyInstance.AnyInt)));
+ Assert.Equal((long)AnyInstance.AnyUInt, (long)(new JsonPrimitive(AnyInstance.AnyUInt)));
+ Assert.True(new JsonPrimitive(AnyInstance.AnyInt).TryReadAs<long>(out longValue));
+ Assert.Equal((long)AnyInstance.AnyInt, longValue);
+
+ int intValue;
+ Assert.Equal((int)AnyInstance.AnyShort, (int)(new JsonPrimitive(AnyInstance.AnyShort)));
+ Assert.Equal((int)AnyInstance.AnyUShort, (int)(new JsonPrimitive(AnyInstance.AnyUShort)));
+ Assert.True(new JsonPrimitive(AnyInstance.AnyUShort).TryReadAs<int>(out intValue));
+ Assert.Equal((int)AnyInstance.AnyUShort, intValue);
+
+ short shortValue;
+ Assert.Equal((short)AnyInstance.AnyByte, (short)(new JsonPrimitive(AnyInstance.AnyByte)));
+ Assert.Equal((short)AnyInstance.AnySByte, (short)(new JsonPrimitive(AnyInstance.AnySByte)));
+ Assert.True(new JsonPrimitive(AnyInstance.AnyByte).TryReadAs<short>(out shortValue));
+ Assert.Equal((short)AnyInstance.AnyByte, shortValue);
+
+ double dblValue;
+ Assert.Equal((double)AnyInstance.AnyFloat, (double)(new JsonPrimitive(AnyInstance.AnyFloat)));
+ Assert.Equal((double)AnyInstance.AnyDecimal, (double)(new JsonPrimitive(AnyInstance.AnyDecimal)));
+ Assert.True(new JsonPrimitive(AnyInstance.AnyFloat).TryReadAs<double>(out dblValue));
+ Assert.Equal((double)AnyInstance.AnyFloat, dblValue);
+ ExceptionHelper.Throws<OverflowException>(delegate { int i = (int)(new JsonPrimitive(1L << 32)); });
+ Assert.False(new JsonPrimitive(1L << 32).TryReadAs<int>(out intValue));
+ Assert.Equal(default(int), intValue);
+
+ byte byteValue;
+ ExceptionHelper.Throws<OverflowException>(delegate { byte b = (byte)(new JsonPrimitive(1L << 32)); });
+ ExceptionHelper.Throws<OverflowException>(delegate { byte b = (byte)(new JsonPrimitive(SByte.MinValue)); });
+ Assert.False(new JsonPrimitive(SByte.MinValue).TryReadAs<byte>(out byteValue));
+ Assert.Equal(default(byte), byteValue);
+ }
+
+ [Fact]
+ public void NumberToStringConverstionTest()
+ {
+ Dictionary<string, JsonPrimitive> allNumbers = new Dictionary<string, JsonPrimitive>
+ {
+ { AnyInstance.AnyByte.ToString(CultureInfo.InvariantCulture), new JsonPrimitive(AnyInstance.AnyByte) },
+ { AnyInstance.AnySByte.ToString(CultureInfo.InvariantCulture), new JsonPrimitive(AnyInstance.AnySByte) },
+ { AnyInstance.AnyShort.ToString(CultureInfo.InvariantCulture), new JsonPrimitive(AnyInstance.AnyShort) },
+ { AnyInstance.AnyUShort.ToString(CultureInfo.InvariantCulture), new JsonPrimitive(AnyInstance.AnyUShort) },
+ { AnyInstance.AnyInt.ToString(CultureInfo.InvariantCulture), new JsonPrimitive(AnyInstance.AnyInt) },
+ { AnyInstance.AnyUInt.ToString(CultureInfo.InvariantCulture), new JsonPrimitive(AnyInstance.AnyUInt) },
+ { AnyInstance.AnyLong.ToString(CultureInfo.InvariantCulture), new JsonPrimitive(AnyInstance.AnyLong) },
+ { AnyInstance.AnyULong.ToString(CultureInfo.InvariantCulture), new JsonPrimitive(AnyInstance.AnyULong) },
+ { AnyInstance.AnyDecimal.ToString(CultureInfo.InvariantCulture), new JsonPrimitive(AnyInstance.AnyDecimal) },
+ { AnyInstance.AnyDouble.ToString("R", CultureInfo.InvariantCulture), new JsonPrimitive(AnyInstance.AnyDouble) },
+ { AnyInstance.AnyFloat.ToString("R", CultureInfo.InvariantCulture), new JsonPrimitive(AnyInstance.AnyFloat) },
+ };
+
+ foreach (string stringRepresentation in allNumbers.Keys)
+ {
+ JsonPrimitive jp = allNumbers[stringRepresentation];
+ Assert.Equal(stringRepresentation, (string)jp);
+ Assert.Equal(stringRepresentation, jp.ReadAs<string>());
+ }
+ }
+
+ [Fact]
+ public void NonNumberToStringConversionTest()
+ {
+ Dictionary<string, JsonPrimitive> allValues = new Dictionary<string, JsonPrimitive>
+ {
+ { new string(AnyInstance.AnyChar, 1), new JsonPrimitive(AnyInstance.AnyChar) },
+ { AnyInstance.AnyBool.ToString(CultureInfo.InvariantCulture).ToLowerInvariant(), new JsonPrimitive(AnyInstance.AnyBool) },
+ { AnyInstance.AnyGuid.ToString("D", CultureInfo.InvariantCulture), new JsonPrimitive(AnyInstance.AnyGuid) },
+ { AnyInstance.AnyDateTime.ToString(DateTimeFormat, CultureInfo.InvariantCulture), new JsonPrimitive(AnyInstance.AnyDateTime) },
+ { AnyInstance.AnyDateTimeOffset.ToString(DateTimeFormat, CultureInfo.InvariantCulture), new JsonPrimitive(AnyInstance.AnyDateTimeOffset) },
+ };
+
+ foreach (char escapedChar in "\r\n\t\u0000\uffff\u001f\\\"")
+ {
+ allValues.Add(new string(escapedChar, 1), new JsonPrimitive(escapedChar));
+ }
+
+ foreach (string stringRepresentation in allValues.Keys)
+ {
+ JsonPrimitive jp = allValues[stringRepresentation];
+ Assert.Equal(stringRepresentation, (string)jp);
+ Assert.Equal(stringRepresentation, jp.ReadAs<string>());
+ }
+ }
+
+ [Fact]
+ public void NonNumberToNumberConversionTest()
+ {
+ Assert.Equal(1, new JsonPrimitive('1').ReadAs<int>());
+ Assert.Equal<byte>(AnyInstance.AnyByte, new JsonPrimitive(AnyInstance.AnyByte.ToString(CultureInfo.InvariantCulture)).ReadAs<byte>());
+ Assert.Equal<sbyte>(AnyInstance.AnySByte, (sbyte)(new JsonPrimitive(AnyInstance.AnySByte.ToString(CultureInfo.InvariantCulture))));
+ Assert.Equal<short>(AnyInstance.AnyShort, (short)(new JsonPrimitive(AnyInstance.AnyShort.ToString(CultureInfo.InvariantCulture))));
+ Assert.Equal<ushort>(AnyInstance.AnyUShort, new JsonPrimitive(AnyInstance.AnyUShort.ToString(CultureInfo.InvariantCulture)).ReadAs<ushort>());
+ Assert.Equal<int>(AnyInstance.AnyInt, new JsonPrimitive(AnyInstance.AnyInt.ToString(CultureInfo.InvariantCulture)).ReadAs<int>());
+ Assert.Equal<uint>(AnyInstance.AnyUInt, (uint)(new JsonPrimitive(AnyInstance.AnyUInt.ToString(CultureInfo.InvariantCulture))));
+ Assert.Equal<long>(AnyInstance.AnyLong, (long)(new JsonPrimitive(AnyInstance.AnyLong.ToString(CultureInfo.InvariantCulture))));
+ Assert.Equal<ulong>(AnyInstance.AnyULong, new JsonPrimitive(AnyInstance.AnyULong.ToString(CultureInfo.InvariantCulture)).ReadAs<ulong>());
+
+ Assert.Equal<decimal>(AnyInstance.AnyDecimal, (decimal)(new JsonPrimitive(AnyInstance.AnyDecimal.ToString(CultureInfo.InvariantCulture))));
+ Assert.Equal<float>(AnyInstance.AnyFloat, new JsonPrimitive(AnyInstance.AnyFloat.ToString(CultureInfo.InvariantCulture)).ReadAs<float>());
+ Assert.Equal<double>(AnyInstance.AnyDouble, (double)(new JsonPrimitive(AnyInstance.AnyDouble.ToString(CultureInfo.InvariantCulture))));
+
+ Assert.Equal<byte>(Convert.ToByte(1.23, CultureInfo.InvariantCulture), new JsonPrimitive("1.23").ReadAs<byte>());
+ Assert.Equal<int>(Convert.ToInt32(12345.6789, CultureInfo.InvariantCulture), new JsonPrimitive("12345.6789").ReadAs<int>());
+ Assert.Equal<short>(Convert.ToInt16(1.23e2), (short)new JsonPrimitive("1.23e2"));
+ Assert.Equal<float>(Convert.ToSingle(1.23e40), (float)new JsonPrimitive("1.23e40"));
+ Assert.Equal<float>(Convert.ToSingle(1.23e-38), (float)new JsonPrimitive("1.23e-38"));
+
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var n = new JsonPrimitive(AnyInstance.AnyBool).ReadAs<sbyte>(); });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var n = new JsonPrimitive(AnyInstance.AnyBool).ReadAs<short>(); });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var n = new JsonPrimitive(AnyInstance.AnyBool).ReadAs<uint>(); });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var n = new JsonPrimitive(AnyInstance.AnyBool).ReadAs<long>(); });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var n = new JsonPrimitive(AnyInstance.AnyBool).ReadAs<double>(); });
+
+ ExceptionHelper.Throws<FormatException>(delegate { var n = new JsonPrimitive(AnyInstance.AnyUri).ReadAs<int>(); });
+ ExceptionHelper.Throws<FormatException>(delegate { var n = new JsonPrimitive(AnyInstance.AnyDateTime).ReadAs<float>(); });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var n = (decimal)(new JsonPrimitive('c')); });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var n = (byte)(new JsonPrimitive("0xFF")); });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var n = (sbyte)(new JsonPrimitive(AnyInstance.AnyDateTimeOffset)); });
+ ExceptionHelper.Throws<FormatException>(delegate { var n = new JsonPrimitive(AnyInstance.AnyUri).ReadAs<uint>(); });
+ ExceptionHelper.Throws<FormatException>(delegate { var n = new JsonPrimitive(AnyInstance.AnyDateTime).ReadAs<double>(); });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var n = (long)(new JsonPrimitive('c')); });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var n = (ulong)(new JsonPrimitive("0xFF")); });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var n = (short)(new JsonPrimitive(AnyInstance.AnyDateTimeOffset)); });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var n = (ushort)(new JsonPrimitive('c')); });
+
+ ExceptionHelper.Throws<OverflowException>(delegate { int i = (int)new JsonPrimitive((1L << 32).ToString(CultureInfo.InvariantCulture)); });
+ ExceptionHelper.Throws<OverflowException>(delegate { byte b = (byte)new JsonPrimitive("-1"); });
+ }
+
+ [Fact]
+ public void StringToNonNumberConversionTest()
+ {
+ const string DateTimeWithOffsetFormat = "yyyy-MM-ddTHH:mm:sszzz";
+ const string DateTimeWithOffsetFormat2 = "yyy-MM-ddTHH:mm:ss.fffK";
+ const string DateTimeWithoutOffsetWithoutTimeFormat = "yyy-MM-dd";
+ const string DateTimeWithoutOffsetFormat = "yyy-MM-ddTHH:mm:ss";
+ const string DateTimeWithoutOffsetFormat2 = "yyy-MM-ddTHH:mm:ss.fff";
+ const string TimeWithoutOffsetFormat = "HH:mm:ss";
+ const string TimeWithoutOffsetFormat2 = "HH:mm";
+
+ Assert.Equal(false, new JsonPrimitive("false").ReadAs<bool>());
+ Assert.Equal(false, (bool)(new JsonPrimitive("False")));
+ Assert.Equal(true, (bool)(new JsonPrimitive("true")));
+ Assert.Equal(true, new JsonPrimitive("True").ReadAs<bool>());
+
+ Assert.Equal<Uri>(AnyInstance.AnyUri, new JsonPrimitive(AnyInstance.AnyUri.ToString()).ReadAs<Uri>());
+ Assert.Equal<char>(AnyInstance.AnyChar, (char)(new JsonPrimitive(new string(AnyInstance.AnyChar, 1))));
+ Assert.Equal<Guid>(AnyInstance.AnyGuid, (Guid)(new JsonPrimitive(AnyInstance.AnyGuid.ToString("D", CultureInfo.InvariantCulture))));
+
+ DateTime anyLocalDateTime = AnyInstance.AnyDateTime.ToLocalTime();
+ DateTime anyUtcDateTime = AnyInstance.AnyDateTime.ToUniversalTime();
+
+ Assert.Equal<DateTime>(anyUtcDateTime, (DateTime)(new JsonPrimitive(anyUtcDateTime.ToString(DateTimeFormat, CultureInfo.InvariantCulture))));
+ Assert.Equal<DateTime>(anyLocalDateTime, new JsonPrimitive(anyLocalDateTime.ToString(DateTimeWithOffsetFormat2, CultureInfo.InvariantCulture)).ReadAs<DateTime>());
+ Assert.Equal<DateTime>(anyUtcDateTime, new JsonPrimitive(anyUtcDateTime.ToString(DateTimeWithOffsetFormat2, CultureInfo.InvariantCulture)).ReadAs<DateTime>());
+ Assert.Equal<DateTime>(anyLocalDateTime.Date, (DateTime)(new JsonPrimitive(anyLocalDateTime.ToString(DateTimeWithoutOffsetWithoutTimeFormat, CultureInfo.InvariantCulture))));
+ Assert.Equal<DateTime>(anyLocalDateTime, new JsonPrimitive(anyLocalDateTime.ToString(DateTimeWithoutOffsetFormat, CultureInfo.InvariantCulture)).ReadAs<DateTime>());
+ Assert.Equal<DateTime>(anyLocalDateTime, new JsonPrimitive(anyLocalDateTime.ToString(DateTimeWithoutOffsetFormat2, CultureInfo.InvariantCulture)).ReadAs<DateTime>());
+
+ DateTime dt = new JsonPrimitive(anyLocalDateTime.ToString(TimeWithoutOffsetFormat, CultureInfo.InvariantCulture)).ReadAs<DateTime>();
+ Assert.Equal(anyLocalDateTime.Hour, dt.Hour);
+ Assert.Equal(anyLocalDateTime.Minute, dt.Minute);
+ Assert.Equal(anyLocalDateTime.Second, dt.Second);
+
+ dt = new JsonPrimitive(anyLocalDateTime.ToString(TimeWithoutOffsetFormat2, CultureInfo.InvariantCulture)).ReadAs<DateTime>();
+ Assert.Equal(anyLocalDateTime.Hour, dt.Hour);
+ Assert.Equal(anyLocalDateTime.Minute, dt.Minute);
+ Assert.Equal(0, dt.Second);
+
+ Assert.Equal<DateTimeOffset>(AnyInstance.AnyDateTimeOffset, new JsonPrimitive(AnyInstance.AnyDateTimeOffset.ToString(DateTimeFormat, CultureInfo.InvariantCulture)).ReadAs<DateTimeOffset>());
+ Assert.Equal<DateTimeOffset>(AnyInstance.AnyDateTimeOffset, new JsonPrimitive(AnyInstance.AnyDateTimeOffset.ToString(DateTimeWithOffsetFormat, CultureInfo.InvariantCulture)).ReadAs<DateTimeOffset>());
+ Assert.Equal<DateTimeOffset>(AnyInstance.AnyDateTimeOffset, new JsonPrimitive(AnyInstance.AnyDateTimeOffset.ToString(DateTimeWithOffsetFormat2, CultureInfo.InvariantCulture)).ReadAs<DateTimeOffset>());
+ Assert.Equal<DateTimeOffset>(AnyInstance.AnyDateTimeOffset.ToLocalTime(), (DateTimeOffset)(new JsonPrimitive(AnyInstance.AnyDateTimeOffset.ToLocalTime().ToString(DateTimeWithoutOffsetFormat, CultureInfo.InvariantCulture))));
+ Assert.Equal<DateTimeOffset>(AnyInstance.AnyDateTimeOffset.ToLocalTime(), (DateTimeOffset)(new JsonPrimitive(AnyInstance.AnyDateTimeOffset.ToLocalTime().ToString(DateTimeWithoutOffsetFormat2, CultureInfo.InvariantCulture))));
+
+ DataContractJsonSerializer dcjs = new DataContractJsonSerializer(typeof(DateTime));
+ MemoryStream ms = new MemoryStream();
+ dcjs.WriteObject(ms, AnyInstance.AnyDateTime);
+ string dcjsSerializedDateTime = Encoding.UTF8.GetString(ms.ToArray());
+ Assert.Equal(AnyInstance.AnyDateTime, JsonValue.Parse(dcjsSerializedDateTime).ReadAs<DateTime>());
+
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var b = (bool)(new JsonPrimitive("notBool")); });
+ ExceptionHelper.Throws<UriFormatException>(delegate { var u = new JsonPrimitive("not an uri - " + new string('r', 100000)).ReadAs<Uri>(); });
+ ExceptionHelper.Throws<FormatException>(delegate { var date = new JsonPrimitive("not a date time").ReadAs<DateTime>(); });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var dto = (DateTimeOffset)(new JsonPrimitive("not a date time offset")); });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var c = (char)new JsonPrimitive(""); });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var c = (char)new JsonPrimitive("cc"); });
+ ExceptionHelper.Throws<FormatException>(delegate { var g = new JsonPrimitive("not a guid").ReadAs<Guid>(); });
+ }
+
+ [Fact]
+ public void AspNetDateTimeFormatConversionTest()
+ {
+ DateTime unixEpochUtc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
+ DateTime unixEpochLocal = unixEpochUtc.ToLocalTime();
+ Assert.Equal(unixEpochUtc, new JsonPrimitive("/Date(0)/").ReadAs<DateTime>());
+ Assert.Equal(unixEpochLocal, new JsonPrimitive("/Date(0-0900)/").ReadAs<DateTime>());
+ Assert.Equal(unixEpochLocal, new JsonPrimitive("/Date(0+1000)/").ReadAs<DateTime>());
+ }
+
+ [Fact]
+ public void ToStringTest()
+ {
+ char anyUnescapedChar = 'c';
+ string anyUnescapedString = "hello";
+
+ Dictionary<string, JsonPrimitive> toStringResults = new Dictionary<string, JsonPrimitive>
+ {
+ // Boolean types
+ { AnyInstance.AnyBool.ToString(CultureInfo.InvariantCulture).ToLowerInvariant(), new JsonPrimitive(AnyInstance.AnyBool) },
+
+ // Numeric types
+ { AnyInstance.AnyByte.ToString(CultureInfo.InvariantCulture), new JsonPrimitive(AnyInstance.AnyByte) },
+ { AnyInstance.AnySByte.ToString(CultureInfo.InvariantCulture), new JsonPrimitive(AnyInstance.AnySByte) },
+ { AnyInstance.AnyShort.ToString(CultureInfo.InvariantCulture), new JsonPrimitive(AnyInstance.AnyShort) },
+ { AnyInstance.AnyUShort.ToString(CultureInfo.InvariantCulture), new JsonPrimitive(AnyInstance.AnyUShort) },
+ { AnyInstance.AnyInt.ToString(CultureInfo.InvariantCulture), new JsonPrimitive(AnyInstance.AnyInt) },
+ { AnyInstance.AnyUInt.ToString(CultureInfo.InvariantCulture), new JsonPrimitive(AnyInstance.AnyUInt) },
+ { AnyInstance.AnyLong.ToString(CultureInfo.InvariantCulture), new JsonPrimitive(AnyInstance.AnyLong) },
+ { AnyInstance.AnyULong.ToString(CultureInfo.InvariantCulture), new JsonPrimitive(AnyInstance.AnyULong) },
+ { AnyInstance.AnyFloat.ToString("R", CultureInfo.InvariantCulture), new JsonPrimitive(AnyInstance.AnyFloat) },
+ { AnyInstance.AnyDouble.ToString("R", CultureInfo.InvariantCulture), new JsonPrimitive(AnyInstance.AnyDouble) },
+ { AnyInstance.AnyDecimal.ToString(CultureInfo.InvariantCulture), new JsonPrimitive(AnyInstance.AnyDecimal) },
+
+ // String types
+ { "\"" + new string(anyUnescapedChar, 1) + "\"", new JsonPrimitive(anyUnescapedChar) },
+ { "\"" + anyUnescapedString + "\"", new JsonPrimitive(anyUnescapedString) },
+ { "\"" + AnyInstance.AnyDateTime.ToString(DateTimeFormat, CultureInfo.InvariantCulture) + "\"", new JsonPrimitive(AnyInstance.AnyDateTime) },
+ { "\"" + AnyInstance.AnyDateTimeOffset.ToString(DateTimeFormat, CultureInfo.InvariantCulture) + "\"", new JsonPrimitive(AnyInstance.AnyDateTimeOffset) },
+ { "\"" + AnyInstance.AnyUri.GetComponents(UriComponents.SerializationInfoString, UriFormat.UriEscaped).Replace("/", "\\/") + "\"", new JsonPrimitive(AnyInstance.AnyUri) },
+ { "\"" + AnyInstance.AnyGuid.ToString("D", CultureInfo.InvariantCulture) + "\"", new JsonPrimitive(AnyInstance.AnyGuid) },
+ };
+
+ foreach (string stringRepresentation in toStringResults.Keys)
+ {
+ string actualResult = toStringResults[stringRepresentation].ToString();
+ Assert.Equal(stringRepresentation, actualResult);
+ }
+
+ Dictionary<string, JsonPrimitive> escapedValues = new Dictionary<string, JsonPrimitive>
+ {
+ { "\"\\u000d\"", new JsonPrimitive('\r') },
+ { "\"\\u000a\"", new JsonPrimitive('\n') },
+ { "\"\\\\\"", new JsonPrimitive('\\') },
+ { "\"\\/\"", new JsonPrimitive('/') },
+ { "\"\\u000b\"", new JsonPrimitive('\u000b') },
+ { "\"\\\"\"", new JsonPrimitive('\"') },
+ { "\"slash-r-\\u000d-fffe-\\ufffe-ffff-\\uffff-tab-\\u0009\"", new JsonPrimitive("slash-r-\r-fffe-\ufffe-ffff-\uffff-tab-\t") },
+ };
+
+ foreach (string stringRepresentation in escapedValues.Keys)
+ {
+ string actualResult = escapedValues[stringRepresentation].ToString();
+ Assert.Equal(stringRepresentation, actualResult);
+ }
+ }
+
+ [Fact]
+ public void JsonTypeTest()
+ {
+ Assert.Equal(JsonType.Boolean, new JsonPrimitive(AnyInstance.AnyBool).JsonType);
+ Assert.Equal(JsonType.Number, new JsonPrimitive(AnyInstance.AnyByte).JsonType);
+ Assert.Equal(JsonType.Number, new JsonPrimitive(AnyInstance.AnySByte).JsonType);
+ Assert.Equal(JsonType.Number, new JsonPrimitive(AnyInstance.AnyShort).JsonType);
+ Assert.Equal(JsonType.Number, new JsonPrimitive(AnyInstance.AnyUShort).JsonType);
+ Assert.Equal(JsonType.Number, new JsonPrimitive(AnyInstance.AnyInt).JsonType);
+ Assert.Equal(JsonType.Number, new JsonPrimitive(AnyInstance.AnyUInt).JsonType);
+ Assert.Equal(JsonType.Number, new JsonPrimitive(AnyInstance.AnyLong).JsonType);
+ Assert.Equal(JsonType.Number, new JsonPrimitive(AnyInstance.AnyULong).JsonType);
+ Assert.Equal(JsonType.Number, new JsonPrimitive(AnyInstance.AnyDecimal).JsonType);
+ Assert.Equal(JsonType.Number, new JsonPrimitive(AnyInstance.AnyDouble).JsonType);
+ Assert.Equal(JsonType.Number, new JsonPrimitive(AnyInstance.AnyFloat).JsonType);
+ Assert.Equal(JsonType.String, new JsonPrimitive(AnyInstance.AnyChar).JsonType);
+ Assert.Equal(JsonType.String, new JsonPrimitive(AnyInstance.AnyString).JsonType);
+ Assert.Equal(JsonType.String, new JsonPrimitive(AnyInstance.AnyUri).JsonType);
+ Assert.Equal(JsonType.String, new JsonPrimitive(AnyInstance.AnyGuid).JsonType);
+ Assert.Equal(JsonType.String, new JsonPrimitive(AnyInstance.AnyDateTime).JsonType);
+ Assert.Equal(JsonType.String, new JsonPrimitive(AnyInstance.AnyDateTimeOffset).JsonType);
+ }
+
+ [Fact]
+ public void InvalidPropertyTest()
+ {
+ JsonValue target = AnyInstance.AnyJsonPrimitive;
+ Assert.True(target.Count == 0);
+ Assert.False(target.ContainsKey(String.Empty));
+ Assert.False(target.ContainsKey(AnyInstance.AnyString));
+ }
+
+ private void CheckValues(object[] values, JsonType expectedType)
+ {
+ JsonPrimitive target;
+ bool success;
+
+ foreach (object value in values)
+ {
+ success = JsonPrimitive.TryCreate(value, out target);
+ Assert.True(success);
+ Assert.NotNull(target);
+ Assert.Equal(expectedType, target.JsonType);
+ }
+ }
+ }
+}
diff --git a/test/System.Json.Test.Unit/JsonTypeTest.cs b/test/System.Json.Test.Unit/JsonTypeTest.cs
new file mode 100644
index 00000000..aa02df7d
--- /dev/null
+++ b/test/System.Json.Test.Unit/JsonTypeTest.cs
@@ -0,0 +1,26 @@
+using System.Collections.Generic;
+using System.Linq;
+using Xunit;
+
+namespace System.Json
+{
+ public class JsonTypeTest
+ {
+ [Fact]
+ public void JsonTypeValues()
+ {
+ string[] allJsonTypeExpectedValues = new string[] { "Array", "Boolean", "Default", "Number", "Object", "String" };
+ JsonType[] allJsonTypeActualValues = (JsonType[])Enum.GetValues(typeof(JsonType));
+
+ Assert.Equal(allJsonTypeExpectedValues.Length, allJsonTypeActualValues.Length);
+
+ List<string> allJsonTypeActualStringValues = new List<string>(allJsonTypeActualValues.Select((x) => x.ToString()));
+ allJsonTypeActualStringValues.Sort(StringComparer.Ordinal);
+
+ for (int i = 0; i < allJsonTypeExpectedValues.Length; i++)
+ {
+ Assert.Equal(allJsonTypeExpectedValues[i], allJsonTypeActualStringValues[i]);
+ }
+ }
+ }
+}
diff --git a/test/System.Json.Test.Unit/JsonValueDynamicMetaObjectTest.cs b/test/System.Json.Test.Unit/JsonValueDynamicMetaObjectTest.cs
new file mode 100644
index 00000000..e6a2621e
--- /dev/null
+++ b/test/System.Json.Test.Unit/JsonValueDynamicMetaObjectTest.cs
@@ -0,0 +1,534 @@
+using System.Collections.Generic;
+using System.Dynamic;
+using System.Linq.Expressions;
+using Xunit;
+
+namespace System.Json
+{
+ /// <summary>
+ ///This is a test class for JsonValueDynamicMetaObjectTest and is intended to perform sanity tests on this class.
+ ///Extended tests are performed by the JsonValue dynamic feature tests.
+ ///</summary>
+ public class JsonValueDynamicMetaObjectTest
+ {
+ const string NonSingleNonNullIndexNotSupported = "Null index or multidimensional indexing is not supported by this indexer; use 'System.Int32' or 'System.String' for array and object indexing respectively.";
+
+ /// <summary>
+ /// A test for GetMetaObject
+ ///</summary>
+ [Fact]
+ public void GetMetaObjectTest()
+ {
+ ExceptionHelper.Throws<ArgumentNullException>(() => { var target = GetJsonValueDynamicMetaObject(AnyInstance.AnyJsonObject, null); });
+ }
+
+ /// <summary>
+ /// A test for BindInvokeMember
+ ///</summary>
+ [Fact]
+ public void BindInvokeMemberTest()
+ {
+ JsonValue value = AnyInstance.AnyJsonValue1;
+ DynamicMetaObject target = GetJsonValueDynamicMetaObject(value);
+
+ TestInvokeMemberBinder.TestBindParams(target);
+
+ string methodName;
+ object[] arguments;
+ object result = null;
+
+ methodName = "ToString";
+ arguments = new object[] { };
+ TestInvokeMemberBinder.TestMetaObject(target, methodName, arguments);
+
+ methodName = "TryReadAs";
+ arguments = new object[] { typeof(int), result };
+ TestInvokeMemberBinder.TestMetaObject(target, methodName, arguments);
+
+ methodName = "TryReadAsType";
+ arguments = new object[] { typeof(Person), result };
+ TestInvokeMemberBinder.TestMetaObject(target, methodName, arguments, true);
+ }
+
+ /// <summary>
+ /// A test for BindConvert
+ ///</summary>
+ [Fact]
+ public void BindConvertTest()
+ {
+ JsonValue value;
+ DynamicMetaObject target;
+
+ value = (JsonValue)AnyInstance.AnyInt;
+ target = GetJsonValueDynamicMetaObject(value);
+ TestConvertBinder.TestBindParams(target);
+
+ Type[] intTypes = { typeof(int), typeof(uint), typeof(long), };
+
+ foreach (Type type in intTypes)
+ {
+ TestConvertBinder.TestMetaObject(target, type);
+ }
+
+ value = (JsonValue)AnyInstance.AnyString;
+ target = GetJsonValueDynamicMetaObject(value);
+ TestConvertBinder.TestMetaObject(target, typeof(string));
+
+ value = (JsonValue)AnyInstance.AnyJsonValue1;
+ target = GetJsonValueDynamicMetaObject(value);
+ TestConvertBinder.TestMetaObject(target, typeof(JsonValue));
+ TestConvertBinder.TestMetaObject(target, typeof(IEnumerable<KeyValuePair<string, JsonValue>>));
+ TestConvertBinder.TestMetaObject(target, typeof(IDynamicMetaObjectProvider));
+ TestConvertBinder.TestMetaObject(target, typeof(object));
+
+ TestConvertBinder.TestMetaObject(target, typeof(Person), false);
+ }
+
+ /// <summary>
+ /// A test for BindGetIndex
+ ///</summary>
+ [Fact]
+ public void BindGetIndexTest()
+ {
+ JsonValue value = AnyInstance.AnyJsonArray;
+
+ DynamicMetaObject target = GetJsonValueDynamicMetaObject(value);
+
+ TestGetIndexBinder.TestBindParams(target);
+
+ foreach (KeyValuePair<string, JsonValue> pair in value)
+ {
+ TestGetIndexBinder.TestMetaObject(target, Int32.Parse(pair.Key));
+ }
+ }
+
+ /// <summary>
+ /// A test for BindSetIndex
+ ///</summary>
+ [Fact]
+ public void BindSetIndexTest()
+ {
+ JsonValue jsonValue = AnyInstance.AnyJsonArray;
+
+ DynamicMetaObject target = GetJsonValueDynamicMetaObject(jsonValue);
+
+ TestSetIndexBinder.TestBindParams(target);
+
+ int value = 0;
+
+ foreach (KeyValuePair<string, JsonValue> pair in jsonValue)
+ {
+ TestSetIndexBinder.TestMetaObject(target, Int32.Parse(pair.Key), value++);
+ }
+ }
+
+ /// <summary>
+ /// A test for BindGetMember.
+ ///</summary>
+ [Fact]
+ public void BindGetMemberTest()
+ {
+ JsonValue value = AnyInstance.AnyJsonObject;
+
+ DynamicMetaObject target = GetJsonValueDynamicMetaObject(value);
+
+ TestGetMemberBinder.TestBindParams(target);
+
+ foreach (KeyValuePair<string, JsonValue> pair in value)
+ {
+ TestGetMemberBinder.TestMetaObject(target, pair.Key);
+ }
+ }
+
+ /// <summary>
+ /// A test for BindSetMember.
+ ///</summary>
+ [Fact]
+ public void BindSetMemberTest()
+ {
+ JsonValue value = AnyInstance.AnyJsonObject;
+
+ string expectedMethodSignature = "System.Json.JsonValue SetValue(System.String, System.Object)";
+
+ DynamicMetaObject target = GetJsonValueDynamicMetaObject(value);
+ DynamicMetaObject arg = new DynamicMetaObject(Expression.Parameter(typeof(int)), BindingRestrictions.Empty, AnyInstance.AnyInt);
+
+ TestSetMemberBinder.TestBindParams(target, arg);
+
+ foreach (KeyValuePair<string, JsonValue> pair in value)
+ {
+ TestSetMemberBinder.TestMetaObject(target, pair.Key, arg, expectedMethodSignature);
+ }
+ }
+
+ /// <summary>
+ /// A test for GetDynamicMemberNames
+ ///</summary>
+ [Fact]
+ public void GetDynamicMemberNamesTest()
+ {
+ JsonValue[] values = AnyInstance.AnyJsonValueArray;
+
+ foreach (JsonValue value in values)
+ {
+ DynamicMetaObject target = GetJsonValueDynamicMetaObject(value);
+
+ List<string> expected = new List<string>();
+ foreach (KeyValuePair<string, JsonValue> pair in value)
+ {
+ expected.Add(pair.Key);
+ }
+
+ IEnumerable<string> retEnumerable = target.GetDynamicMemberNames();
+ Assert.NotNull(retEnumerable);
+
+ List<string> actual = new List<string>(retEnumerable);
+ Assert.Equal(expected.Count, actual.Count);
+
+ for (int i = 0; i < expected.Count; i++)
+ {
+ Assert.Equal<string>(expected[i], actual[i]);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Helper method for getting a <see cref="JsonValueDynamicMetaObject"/>.
+ /// </summary>
+ /// <param name="jsonValue">The <see cref="JsonValue"/> instance to get the dynamic meta-object from.</param>
+ /// <returns></returns>
+ private static DynamicMetaObject GetJsonValueDynamicMetaObject(JsonValue jsonValue)
+ {
+ return GetJsonValueDynamicMetaObject(jsonValue, Expression.Parameter(typeof(object)));
+ }
+
+ private static DynamicMetaObject GetJsonValueDynamicMetaObject(JsonValue jsonValue, Expression expression)
+ {
+ return ((IDynamicMetaObjectProvider)jsonValue).GetMetaObject(expression);
+ }
+
+ /// <summary>
+ /// Test binder for method call operation.
+ /// </summary>
+ private class TestInvokeMemberBinder : InvokeMemberBinder
+ {
+ public TestInvokeMemberBinder(string name, int argCount)
+ : base(name, false, new CallInfo(argCount, new string[] { }))
+ {
+ }
+
+ public static void TestBindParams(DynamicMetaObject target)
+ {
+ string methodName = "ToString";
+ object[] arguments = new object[] { };
+
+ InvokeMemberBinder binder = new TestInvokeMemberBinder(methodName, arguments.Length);
+ DynamicMetaObject[] args = new DynamicMetaObject[arguments.Length];
+
+ ExceptionHelper.Throws<ArgumentNullException>(() => { var result = target.BindInvokeMember(null, args); });
+ ExceptionHelper.Throws<ArgumentNullException>(() => { var result = target.BindInvokeMember(binder, null); });
+ }
+
+ public static void TestMetaObject(DynamicMetaObject target, string methodName, object[] arguments, bool isExtension = false)
+ {
+ InvokeMemberBinder binder = new TestInvokeMemberBinder(methodName, arguments.Length);
+ DynamicMetaObject[] args = new DynamicMetaObject[arguments.Length];
+
+ for (int idx = 0; idx < args.Length; idx++)
+ {
+ object value = arguments[idx];
+ Type valueType = value != null ? value.GetType() : typeof(object);
+ args[idx] = new DynamicMetaObject(Expression.Parameter(valueType), BindingRestrictions.Empty, value);
+ }
+
+ DynamicMetaObject result = target.BindInvokeMember(binder, args);
+ Assert.NotNull(result);
+
+ if (isExtension)
+ {
+ UnaryExpression expression = result.Expression as UnaryExpression;
+ Assert.NotNull(expression);
+
+ MethodCallExpression callExpression = expression.Operand as MethodCallExpression;
+ Assert.NotNull(callExpression);
+
+ Assert.True(callExpression.Method.ToString().Contains(methodName));
+ }
+ else
+ {
+ Assert.Same(target, result.Value);
+ }
+ }
+
+ public override DynamicMetaObject FallbackInvoke(DynamicMetaObject target, DynamicMetaObject[] args, DynamicMetaObject errorSuggestion)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override DynamicMetaObject FallbackInvokeMember(DynamicMetaObject target, DynamicMetaObject[] args, DynamicMetaObject errorSuggestion)
+ {
+ // This is where the C# binder does the actual binding.
+ return new DynamicMetaObject(Expression.Constant("FallbackInvokeMember called"), BindingRestrictions.Empty, target);
+ }
+ }
+
+ /// <summary>
+ /// The binder for the cast operation.
+ /// </summary>
+ private class TestConvertBinder : ConvertBinder
+ {
+ public TestConvertBinder(Type type)
+ : base(type, false)
+ {
+ }
+
+ public static void TestBindParams(DynamicMetaObject target)
+ {
+ ConvertBinder binder = new TestConvertBinder(typeof(int));
+ ExceptionHelper.Throws<ArgumentNullException>(() => { var result = target.BindConvert(null); });
+ }
+
+ public static void TestMetaObject(DynamicMetaObject target, Type type, bool isValid = true)
+ {
+ ConvertBinder binder = new TestConvertBinder(type);
+ DynamicMetaObject result = target.BindConvert(binder);
+ Assert.NotNull(result);
+
+ // Convert expression
+ UnaryExpression expression = result.Expression as UnaryExpression;
+ Assert.NotNull(expression);
+ Assert.Equal<Type>(binder.Type, expression.Type);
+
+ if (isValid)
+ {
+ MethodCallExpression methodCallExp = expression.Operand as MethodCallExpression;
+
+ if (methodCallExp != null)
+ {
+ Assert.True(methodCallExp.Method.ToString().Contains("CastValue"));
+ }
+ else
+ {
+ ParameterExpression paramExpression = expression.Operand as ParameterExpression;
+ Assert.NotNull(paramExpression);
+ }
+ }
+ else
+ {
+ Expression<Action> throwExp = Expression.Lambda<Action>(Expression.Block(expression), new ParameterExpression[] { });
+ ExceptionHelper.Throws<InvalidCastException>(() => throwExp.Compile().Invoke());
+ }
+ }
+
+ public override DynamicMetaObject FallbackConvert(DynamicMetaObject target, DynamicMetaObject errorSuggestion)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ /// <summary>
+ /// Test binder for int indexer getter operation.
+ /// </summary>
+ private class TestGetIndexBinder : GetIndexBinder
+ {
+ public TestGetIndexBinder()
+ : base(new CallInfo(0, new string[] { }))
+ {
+ }
+
+ public static void TestBindParams(DynamicMetaObject target)
+ {
+ GetIndexBinder binder = new TestGetIndexBinder();
+ Expression typeExpression = Expression.Parameter(typeof(int));
+
+ DynamicMetaObject[] indexes =
+ {
+ new DynamicMetaObject(typeExpression, BindingRestrictions.Empty, 0),
+ new DynamicMetaObject(typeExpression, BindingRestrictions.Empty, 1),
+ new DynamicMetaObject(typeExpression, BindingRestrictions.Empty, 2)
+ };
+
+ ExceptionHelper.Throws<ArgumentNullException>(() => { var result = target.BindGetIndex(null, indexes); });
+ ExceptionHelper.Throws<ArgumentNullException>(() => { var result = target.BindGetIndex(binder, null); });
+
+ DynamicMetaObject[][] invalidIndexesParam =
+ {
+ indexes,
+ new DynamicMetaObject[] { new DynamicMetaObject(typeExpression, BindingRestrictions.Empty, null) },
+ new DynamicMetaObject[] { null },
+ new DynamicMetaObject[] { }
+ };
+
+ foreach (DynamicMetaObject[] indexesParam in invalidIndexesParam)
+ {
+ DynamicMetaObject metaObj = target.BindGetIndex(binder, indexesParam);
+
+ Expression<Action> expression = Expression.Lambda<Action>(Expression.Block(metaObj.Expression), new ParameterExpression[] { });
+ ExceptionHelper.Throws<ArgumentException>(() => { expression.Compile().Invoke(); }, NonSingleNonNullIndexNotSupported);
+ }
+ }
+
+ public static void TestMetaObject(DynamicMetaObject target, int index, bool isValid = true)
+ {
+ string expectedMethodSignature = "System.Json.JsonValue GetValue(Int32)";
+
+ GetIndexBinder binder = new TestGetIndexBinder();
+ DynamicMetaObject[] indexes = { new DynamicMetaObject(Expression.Parameter(typeof(int)), BindingRestrictions.Empty, index) };
+
+ DynamicMetaObject result = target.BindGetIndex(binder, indexes);
+ Assert.NotNull(result);
+
+ MethodCallExpression expression = result.Expression as MethodCallExpression;
+ Assert.NotNull(expression);
+ Assert.Equal<string>(expectedMethodSignature, expression.Method.ToString());
+ }
+
+ public override DynamicMetaObject FallbackGetIndex(DynamicMetaObject target, DynamicMetaObject[] indexes, DynamicMetaObject errorSuggestion)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ /// <summary>
+ /// Test binder for int indexer setter operation.
+ /// </summary>
+ private class TestSetIndexBinder : SetIndexBinder
+ {
+ public TestSetIndexBinder()
+ : base(new CallInfo(0, new string[] { }))
+ {
+ }
+
+ public static void TestBindParams(DynamicMetaObject target)
+ {
+ SetIndexBinder binder = new TestSetIndexBinder();
+ Expression typeExpression = Expression.Parameter(typeof(int));
+ DynamicMetaObject[] indexes = new DynamicMetaObject[] { new DynamicMetaObject(typeExpression, BindingRestrictions.Empty, 0) };
+ DynamicMetaObject value = new DynamicMetaObject(typeExpression, BindingRestrictions.Empty, (JsonValue)10);
+
+ ExceptionHelper.Throws<ArgumentNullException>(() => { var result = target.BindSetIndex(null, indexes, value); });
+ ExceptionHelper.Throws<ArgumentNullException>(() => { var result = target.BindSetIndex(binder, null, value); });
+ ExceptionHelper.Throws<ArgumentNullException>(() => { var result = target.BindSetIndex(binder, indexes, null); });
+
+ DynamicMetaObject[][] invalidIndexesParam =
+ {
+ new DynamicMetaObject[]
+ {
+ new DynamicMetaObject(typeExpression, BindingRestrictions.Empty, 0),
+ new DynamicMetaObject(typeExpression, BindingRestrictions.Empty, 1),
+ new DynamicMetaObject(typeExpression, BindingRestrictions.Empty, 2)
+ },
+
+ new DynamicMetaObject[]
+ {
+ new DynamicMetaObject(typeExpression, BindingRestrictions.Empty, null)
+ },
+
+ new DynamicMetaObject[]
+ {
+ }
+ };
+
+ foreach (DynamicMetaObject[] indexesParam in invalidIndexesParam)
+ {
+ DynamicMetaObject metaObj = target.BindSetIndex(binder, indexesParam, value);
+
+ Expression<Action> expression = Expression.Lambda<Action>(Expression.Block(metaObj.Expression), new ParameterExpression[] { });
+ ExceptionHelper.Throws<ArgumentException>(() => { expression.Compile().Invoke(); }, NonSingleNonNullIndexNotSupported);
+ }
+ }
+
+ public static void TestMetaObject(DynamicMetaObject target, int index, JsonValue jsonValue, bool isValid = true)
+ {
+ string expectedMethodSignature = "System.Json.JsonValue SetValue(Int32, System.Object)";
+
+ SetIndexBinder binder = new TestSetIndexBinder();
+ DynamicMetaObject[] indexes = { new DynamicMetaObject(Expression.Parameter(typeof(int)), BindingRestrictions.Empty, index) };
+ DynamicMetaObject value = new DynamicMetaObject(Expression.Parameter(jsonValue.GetType()), BindingRestrictions.Empty, jsonValue);
+ DynamicMetaObject result = target.BindSetIndex(binder, indexes, value);
+ Assert.NotNull(result);
+
+ MethodCallExpression expression = result.Expression as MethodCallExpression;
+ Assert.NotNull(expression);
+ Assert.Equal<string>(expectedMethodSignature, expression.Method.ToString());
+ }
+
+ public override DynamicMetaObject FallbackSetIndex(DynamicMetaObject target, DynamicMetaObject[] indexes, DynamicMetaObject value, DynamicMetaObject errorSuggestion)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ /// <summary>
+ /// Test binder for key indexer getter.
+ /// </summary>
+ private class TestGetMemberBinder : GetMemberBinder
+ {
+ public TestGetMemberBinder(string name)
+ : base(name, false)
+ {
+ }
+
+ public static void TestBindParams(DynamicMetaObject target)
+ {
+ GetMemberBinder binder = new TestGetMemberBinder("AnyProperty");
+ ExceptionHelper.Throws<ArgumentNullException>(() => { var result = target.BindGetMember(null); });
+ }
+
+ public static void TestMetaObject(DynamicMetaObject target, string name, bool isValid = true)
+ {
+ string expectedMethodSignature = "System.Json.JsonValue GetValue(System.String)";
+
+ GetMemberBinder binder = new TestGetMemberBinder(name);
+
+ DynamicMetaObject result = target.BindGetMember(binder);
+ Assert.NotNull(result);
+
+ MethodCallExpression expression = result.Expression as MethodCallExpression;
+ Assert.NotNull(expression);
+ Assert.Equal<string>(expectedMethodSignature, expression.Method.ToString());
+ }
+
+ public override DynamicMetaObject FallbackGetMember(DynamicMetaObject target, DynamicMetaObject errorSuggestion)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ /// <summary>
+ /// Test binder for key indexer setter.
+ /// </summary>
+ private class TestSetMemberBinder : SetMemberBinder
+ {
+ public TestSetMemberBinder(string name)
+ : base(name, false)
+ {
+ }
+
+ public static void TestBindParams(DynamicMetaObject target, DynamicMetaObject value)
+ {
+ SetMemberBinder binder = new TestSetMemberBinder("AnyProperty");
+
+ ExceptionHelper.Throws<ArgumentNullException>(() => { var result = target.BindSetMember(null, value); });
+ ExceptionHelper.Throws<ArgumentNullException>(() => { var result = target.BindSetMember(binder, null); });
+ }
+
+ public static void TestMetaObject(DynamicMetaObject target, string name, DynamicMetaObject value, string expectedMethodSignature, bool isValid = true)
+ {
+ SetMemberBinder binder = new TestSetMemberBinder(name);
+
+ DynamicMetaObject result = target.BindSetMember(binder, value);
+ Assert.NotNull(result);
+
+ MethodCallExpression expression = result.Expression as MethodCallExpression;
+ Assert.NotNull(expression);
+ Assert.Equal<string>(expectedMethodSignature, expression.Method.ToString());
+ }
+
+ public override DynamicMetaObject FallbackSetMember(DynamicMetaObject target, DynamicMetaObject value, DynamicMetaObject errorSuggestion)
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
diff --git a/test/System.Json.Test.Unit/JsonValueDynamicTest.cs b/test/System.Json.Test.Unit/JsonValueDynamicTest.cs
new file mode 100644
index 00000000..cd0055c0
--- /dev/null
+++ b/test/System.Json.Test.Unit/JsonValueDynamicTest.cs
@@ -0,0 +1,468 @@
+using System.Collections.Generic;
+using System.Dynamic;
+using System.Runtime.Serialization.Json;
+using Xunit;
+
+namespace System.Json
+{
+ public class JsonValueDynamicTest
+ {
+ const string InvalidIndexType = "Invalid '{0}' index type; only 'System.String' and non-negative 'System.Int32' types are supported.";
+ const string NonSingleNonNullIndexNotSupported = "Null index or multidimensional indexing is not supported by this indexer; use 'System.Int32' or 'System.String' for array and object indexing respectively.";
+
+ [Fact]
+ public void SettingDifferentValueTypes()
+ {
+ dynamic dyn = new JsonObject();
+ dyn.boolean = AnyInstance.AnyBool;
+ dyn.int16 = AnyInstance.AnyShort;
+ dyn.int32 = AnyInstance.AnyInt;
+ dyn.int64 = AnyInstance.AnyLong;
+ dyn.uint16 = AnyInstance.AnyUShort;
+ dyn.uint32 = AnyInstance.AnyUInt;
+ dyn.uint64 = AnyInstance.AnyULong;
+ dyn.@char = AnyInstance.AnyChar;
+ dyn.dbl = AnyInstance.AnyDouble;
+ dyn.flt = AnyInstance.AnyFloat;
+ dyn.dec = AnyInstance.AnyDecimal;
+ dyn.str = AnyInstance.AnyString;
+ dyn.uri = AnyInstance.AnyUri;
+ dyn.@byte = AnyInstance.AnyByte;
+ dyn.@sbyte = AnyInstance.AnySByte;
+ dyn.guid = AnyInstance.AnyGuid;
+ dyn.dateTime = AnyInstance.AnyDateTime;
+ dyn.dateTimeOffset = AnyInstance.AnyDateTimeOffset;
+ dyn.JsonArray = AnyInstance.AnyJsonArray;
+ dyn.JsonPrimitive = AnyInstance.AnyJsonPrimitive;
+ dyn.JsonObject = AnyInstance.AnyJsonObject;
+
+ JsonObject jo = (JsonObject)dyn;
+ Assert.Equal(AnyInstance.AnyBool, (bool)jo["boolean"]);
+ Assert.Equal(AnyInstance.AnyShort, (short)jo["int16"]);
+ Assert.Equal(AnyInstance.AnyUShort, (ushort)jo["uint16"]);
+ Assert.Equal(AnyInstance.AnyInt, (int)jo["int32"]);
+ Assert.Equal(AnyInstance.AnyUInt, (uint)jo["uint32"]);
+ Assert.Equal(AnyInstance.AnyLong, (long)jo["int64"]);
+ Assert.Equal(AnyInstance.AnyULong, (ulong)jo["uint64"]);
+ Assert.Equal(AnyInstance.AnySByte, (sbyte)jo["sbyte"]);
+ Assert.Equal(AnyInstance.AnyByte, (byte)jo["byte"]);
+ Assert.Equal(AnyInstance.AnyChar, (char)jo["char"]);
+ Assert.Equal(AnyInstance.AnyDouble, (double)jo["dbl"]);
+ Assert.Equal(AnyInstance.AnyFloat, (float)jo["flt"]);
+ Assert.Equal(AnyInstance.AnyDecimal, (decimal)jo["dec"]);
+ Assert.Equal(AnyInstance.AnyString, (string)jo["str"]);
+ Assert.Equal(AnyInstance.AnyUri, (Uri)jo["uri"]);
+ Assert.Equal(AnyInstance.AnyGuid, (Guid)jo["guid"]);
+ Assert.Equal(AnyInstance.AnyDateTime, (DateTime)jo["dateTime"]);
+ Assert.Equal(AnyInstance.AnyDateTimeOffset, (DateTimeOffset)jo["dateTimeOffset"]);
+ Assert.Same(AnyInstance.AnyJsonArray, jo["JsonArray"]);
+ Assert.Equal(AnyInstance.AnyJsonPrimitive, jo["JsonPrimitive"]);
+ Assert.Same(AnyInstance.AnyJsonObject, jo["JsonObject"]);
+
+ Assert.Equal(AnyInstance.AnyBool, (bool)dyn.boolean);
+ Assert.Equal(AnyInstance.AnyShort, (short)dyn.int16);
+ Assert.Equal(AnyInstance.AnyUShort, (ushort)dyn.uint16);
+ Assert.Equal(AnyInstance.AnyInt, (int)dyn.int32);
+ Assert.Equal(AnyInstance.AnyUInt, (uint)dyn.uint32);
+ Assert.Equal(AnyInstance.AnyLong, (long)dyn.int64);
+ Assert.Equal(AnyInstance.AnyULong, (ulong)dyn.uint64);
+ Assert.Equal(AnyInstance.AnySByte, (sbyte)dyn.@sbyte);
+ Assert.Equal(AnyInstance.AnyByte, (byte)dyn.@byte);
+ Assert.Equal(AnyInstance.AnyChar, (char)dyn.@char);
+ Assert.Equal(AnyInstance.AnyDouble, (double)dyn.dbl);
+ Assert.Equal(AnyInstance.AnyFloat, (float)dyn.flt);
+ Assert.Equal(AnyInstance.AnyDecimal, (decimal)dyn.dec);
+ Assert.Equal(AnyInstance.AnyString, (string)dyn.str);
+ Assert.Equal(AnyInstance.AnyUri, (Uri)dyn.uri);
+ Assert.Equal(AnyInstance.AnyGuid, (Guid)dyn.guid);
+ Assert.Equal(AnyInstance.AnyDateTime, (DateTime)dyn.dateTime);
+ Assert.Equal(AnyInstance.AnyDateTimeOffset, (DateTimeOffset)dyn.dateTimeOffset);
+ Assert.Same(AnyInstance.AnyJsonArray, dyn.JsonArray);
+ Assert.Equal(AnyInstance.AnyJsonPrimitive, dyn.JsonPrimitive);
+ Assert.Same(AnyInstance.AnyJsonObject, dyn.JsonObject);
+
+ ExceptionHelper.Throws<ArgumentException>(delegate { dyn.other = Console.Out; });
+ ExceptionHelper.Throws<ArgumentException>(delegate { dyn.other = dyn.NonExistentProp; });
+ }
+
+ [Fact]
+ public void NullTests()
+ {
+ dynamic dyn = new JsonObject();
+ JsonObject jo = (JsonObject)dyn;
+
+ dyn.@null = null;
+ Assert.Same(dyn.@null, AnyInstance.DefaultJsonValue);
+
+ jo["@null"] = null;
+ Assert.Null(jo["@null"]);
+ }
+
+ [Fact]
+ public void DynamicNotationTest()
+ {
+ bool boolValue;
+ JsonValue jsonValue;
+
+ Person person = Person.CreateSample();
+ dynamic jo = JsonValueExtensions.CreateFrom(person);
+
+ dynamic target = jo;
+ Assert.Equal<int>(person.Age, target.Age.ReadAs<int>()); // JsonPrimitive
+ Assert.Equal<string>(person.Address.ToString(), ((JsonObject)target.Address).ReadAsType<Address>().ToString()); // JsonObject
+
+ target = jo.Address.City; // JsonPrimitive
+ Assert.NotNull(target);
+ Assert.Equal<string>(target.ReadAs<string>(), person.Address.City);
+
+ target = jo.Friends; // JsonArray
+ Assert.NotNull(target);
+ jsonValue = target as JsonValue;
+ Assert.Equal<int>(person.Friends.Count, jsonValue.ReadAsType<List<Person>>().Count);
+
+ target = jo.Friends[1].Address.City;
+ Assert.NotNull(target);
+ Assert.Equal<string>(target.ReadAs<string>(), person.Address.City);
+
+ target = jo.Address.NonExistentProp.NonExistentProp2; // JsonObject (default)
+ Assert.NotNull(target);
+ Assert.True(jo is JsonObject);
+ Assert.False(target.TryReadAs<bool>(out boolValue));
+ Assert.True(target.TryReadAs<JsonValue>(out jsonValue));
+ Assert.Same(target, jsonValue);
+
+ Assert.Same(jo.Address.NonExistent, AnyInstance.DefaultJsonValue);
+ Assert.Same(jo.Friends[1000], AnyInstance.DefaultJsonValue);
+ Assert.Same(jo.Age.NonExistentProp, AnyInstance.DefaultJsonValue);
+ Assert.Same(jo.Friends.NonExistentProp, AnyInstance.DefaultJsonValue);
+ }
+
+ [Fact]
+ public void PropertyAccessTest()
+ {
+ Person p = AnyInstance.AnyPerson;
+ JsonObject jo = JsonValueExtensions.CreateFrom(p) as JsonObject;
+ JsonArray ja = JsonValueExtensions.CreateFrom(p.Friends) as JsonArray;
+ JsonPrimitive jp = AnyInstance.AnyJsonPrimitive;
+ JsonValue jv = AnyInstance.DefaultJsonValue;
+
+ dynamic jod = jo;
+ dynamic jad = ja;
+ dynamic jpd = jp;
+ dynamic jvd = jv;
+
+ Assert.Equal(jo.Count, jod.Count);
+ Assert.Equal(jo.JsonType, jod.JsonType);
+ Assert.Equal(jo.Keys.Count, jod.Keys.Count);
+ Assert.Equal(jo.Values.Count, jod.Values.Count);
+ Assert.Equal(p.Age, (int)jod.Age);
+ Assert.Equal(p.Age, (int)jod["Age"]);
+ Assert.Equal(p.Age, (int)jo["Age"]);
+ Assert.Equal(p.Address.City, (string)jo["Address"]["City"]);
+ Assert.Equal(p.Address.City, (string)jod["Address"]["City"]);
+ Assert.Equal(p.Address.City, (string)jod.Address.City);
+
+ Assert.Equal(p.Friends.Count, ja.Count);
+ Assert.Equal(ja.Count, jad.Count);
+ Assert.Equal(ja.IsReadOnly, jad.IsReadOnly);
+ Assert.Equal(ja.JsonType, jad.JsonType);
+ Assert.Equal(p.Friends[0].Age, (int)ja[0]["Age"]);
+ Assert.Equal(p.Friends[0].Age, (int)jad[0].Age);
+
+ Assert.Equal(jp.JsonType, jpd.JsonType);
+ }
+
+ [Fact]
+ public void ConcatDynamicAssignmentTest()
+ {
+ string value = "MyValue";
+ dynamic dynArray = JsonValue.Parse(AnyInstance.AnyJsonArray.ToString());
+ dynamic dynObj = JsonValue.Parse(AnyInstance.AnyJsonObject.ToString());
+
+ JsonValue target;
+
+ target = dynArray[0] = dynArray[1] = dynArray[2] = value;
+ Assert.Equal((string)target, value);
+ Assert.Equal((string)dynArray[0], value);
+ Assert.Equal((string)dynArray[1], value);
+ Assert.Equal((string)dynArray[2], value);
+
+ target = dynObj["key0"] = dynObj["key1"] = dynObj["key2"] = value;
+ Assert.Equal((string)target, value);
+ Assert.Equal((string)dynObj["key0"], value);
+ Assert.Equal((string)dynObj["key1"], value);
+ Assert.Equal((string)dynObj["key2"], value);
+ foreach (KeyValuePair<string, JsonValue> pair in AnyInstance.AnyJsonObject)
+ {
+ Assert.Equal<string>(AnyInstance.AnyJsonObject[pair.Key].ToString(), dynObj[pair.Key].ToString());
+ }
+ }
+
+ [Fact]
+ public void IndexConversionTest()
+ {
+ dynamic target = AnyInstance.AnyJsonArray;
+ dynamic expected = AnyInstance.AnyJsonArray[0];
+ dynamic result;
+
+ dynamic[] zero_indexes =
+ {
+ (short)0,
+ (ushort)0,
+ (byte)0,
+ (sbyte)0,
+ (char)0,
+ (int)0
+ };
+
+
+ result = target[(short)0];
+ Assert.Same(expected, result);
+ result = target[(ushort)0];
+ Assert.Same(expected, result);
+ result = target[(byte)0];
+ Assert.Same(expected, result);
+ result = target[(sbyte)0];
+ Assert.Same(expected, result);
+ result = target[(char)0];
+ Assert.Same(expected, result);
+
+ foreach (dynamic zero_index in zero_indexes)
+ {
+ result = target[zero_index];
+ Assert.Same(expected, result);
+ }
+ }
+
+ [Fact]
+ public void InvalidIndexTest()
+ {
+ object index1 = new object();
+ bool index2 = true;
+ Person index3 = AnyInstance.AnyPerson;
+ JsonObject jo = AnyInstance.AnyJsonObject;
+
+ dynamic target;
+ object ret;
+
+ JsonValue[] values = { AnyInstance.AnyJsonObject, AnyInstance.AnyJsonArray };
+
+ foreach (JsonValue value in values)
+ {
+ target = value;
+
+ ExceptionHelper.Throws<ArgumentException>(delegate { ret = target[index1]; }, String.Format(InvalidIndexType, index1.GetType().FullName));
+ ExceptionHelper.Throws<ArgumentException>(delegate { ret = target[index2]; }, String.Format(InvalidIndexType, index2.GetType().FullName));
+ ExceptionHelper.Throws<ArgumentException>(delegate { ret = target[index3]; }, String.Format(InvalidIndexType, index3.GetType().FullName));
+ ExceptionHelper.Throws<ArgumentException>(delegate { ret = target[null]; }, NonSingleNonNullIndexNotSupported);
+
+ ExceptionHelper.Throws<ArgumentException>(delegate { ret = target[0, 1]; }, NonSingleNonNullIndexNotSupported);
+ ExceptionHelper.Throws<ArgumentException>(delegate { ret = target["key1", "key2"]; }, NonSingleNonNullIndexNotSupported);
+
+ ExceptionHelper.Throws<ArgumentException>(delegate { ret = target[true]; }, String.Format(InvalidIndexType, true.GetType().FullName));
+
+ ExceptionHelper.Throws<ArgumentException>(delegate { target[index1] = jo; }, String.Format(InvalidIndexType, index1.GetType().FullName));
+ ExceptionHelper.Throws<ArgumentException>(delegate { target[index2] = jo; }, String.Format(InvalidIndexType, index2.GetType().FullName));
+ ExceptionHelper.Throws<ArgumentException>(delegate { target[index3] = jo; }, String.Format(InvalidIndexType, index3.GetType().FullName));
+ ExceptionHelper.Throws<ArgumentException>(delegate { target[null] = jo; }, NonSingleNonNullIndexNotSupported);
+
+ ExceptionHelper.Throws<ArgumentException>(delegate { target[0, 1] = jo; }, NonSingleNonNullIndexNotSupported);
+ ExceptionHelper.Throws<ArgumentException>(delegate { target["key1", "key2"] = jo; }, NonSingleNonNullIndexNotSupported);
+
+ ExceptionHelper.Throws<ArgumentException>(delegate { target[true] = jo; }, String.Format(InvalidIndexType, true.GetType().FullName));
+ }
+ }
+
+ [Fact]
+ public void InvalidCastingTests()
+ {
+ dynamic dyn;
+ string value = "NameValue";
+
+ dyn = AnyInstance.AnyJsonPrimitive;
+ ExceptionHelper.Throws<InvalidOperationException>(delegate { dyn.name = value; });
+
+ dyn = AnyInstance.AnyJsonArray;
+ ExceptionHelper.Throws<InvalidOperationException>(delegate { dyn.name = value; });
+
+ dyn = new JsonObject(AnyInstance.AnyJsonObject);
+ dyn.name = value;
+ Assert.Equal((string)dyn.name, value);
+
+ dyn = AnyInstance.DefaultJsonValue;
+ ExceptionHelper.Throws<InvalidOperationException>(delegate { dyn.name = value; });
+ }
+
+ [Fact]
+ public void CastTests()
+ {
+ dynamic dyn = JsonValueExtensions.CreateFrom(AnyInstance.AnyPerson) as JsonObject;
+ string city = dyn.Address.City;
+
+ Assert.Equal<string>(AnyInstance.AnyPerson.Address.City, dyn.Address.City.ReadAs<string>());
+ Assert.Equal<string>(AnyInstance.AnyPerson.Address.City, city);
+
+ JsonValue[] values =
+ {
+ AnyInstance.AnyInt,
+ AnyInstance.AnyString,
+ AnyInstance.AnyDateTime,
+ AnyInstance.AnyJsonObject,
+ AnyInstance.AnyJsonArray,
+ AnyInstance.DefaultJsonValue
+ };
+
+ int loopCount = 2;
+ bool explicitCast = true;
+
+ while (loopCount > 0)
+ {
+ loopCount--;
+
+ foreach (JsonValue jv in values)
+ {
+ EvaluateNoExceptions<JsonValue>(null, explicitCast);
+ EvaluateNoExceptions<JsonValue>(jv, explicitCast);
+ EvaluateNoExceptions<object>(jv, explicitCast);
+ EvaluateNoExceptions<IDynamicMetaObjectProvider>(jv, explicitCast);
+ EvaluateNoExceptions<IEnumerable<KeyValuePair<string, JsonValue>>>(jv, explicitCast);
+ EvaluateNoExceptions<string>(null, explicitCast);
+
+ EvaluateExpectExceptions<int>(null, explicitCast);
+ EvaluateExpectExceptions<Person>(jv, explicitCast);
+ EvaluateExpectExceptions<Exception>(jv, explicitCast);
+
+ EvaluateIgnoreExceptions<JsonObject>(jv, explicitCast);
+ EvaluateIgnoreExceptions<int>(jv, explicitCast);
+ EvaluateIgnoreExceptions<string>(jv, explicitCast);
+ EvaluateIgnoreExceptions<DateTime>(jv, explicitCast);
+ EvaluateIgnoreExceptions<JsonArray>(jv, explicitCast);
+ EvaluateIgnoreExceptions<JsonPrimitive>(jv, explicitCast);
+ }
+
+ explicitCast = false;
+ }
+
+ EvaluateNoExceptions<IDictionary<string, JsonValue>>(AnyInstance.AnyJsonObject, false);
+ EvaluateNoExceptions<IList<JsonValue>>(AnyInstance.AnyJsonArray, false);
+ }
+
+ static void EvaluateNoExceptions<T>(JsonValue value, bool cast)
+ {
+ Evaluate<T>(value, cast, false, true);
+ }
+
+ static void EvaluateExpectExceptions<T>(JsonValue value, bool cast)
+ {
+ Evaluate<T>(value, cast, true, true);
+ }
+
+ static void EvaluateIgnoreExceptions<T>(JsonValue value, bool cast)
+ {
+ Evaluate<T>(value, cast, true, false);
+ }
+
+ static void Evaluate<T>(JsonValue value, bool cast, bool throwExpected, bool assertExceptions)
+ {
+ T ret2;
+ object obj = null;
+ bool exceptionThrown = false;
+ string retstr2, retstr1;
+
+ Console.WriteLine("Test info: expected:[{0}], explicitCast type:[{1}]", value, typeof(T));
+
+ try
+ {
+ if (typeof(int) == typeof(T))
+ {
+ obj = ((int)value);
+ }
+ else if (typeof(string) == typeof(T))
+ {
+ obj = ((string)value);
+ }
+ else if (typeof(DateTime) == typeof(T))
+ {
+ obj = ((DateTime)value);
+ }
+ else if (typeof(IList<JsonValue>) == typeof(T))
+ {
+ obj = (IList<JsonValue>)value;
+ }
+ else if (typeof(IDictionary<string, JsonValue>) == typeof(T))
+ {
+ obj = (IDictionary<string, JsonValue>)value;
+ }
+ else if (typeof(JsonValue) == typeof(T))
+ {
+ obj = (JsonValue)value;
+ }
+ else if (typeof(JsonObject) == typeof(T))
+ {
+ obj = (JsonObject)value;
+ }
+ else if (typeof(JsonArray) == typeof(T))
+ {
+ obj = (JsonArray)value;
+ }
+ else if (typeof(JsonPrimitive) == typeof(T))
+ {
+ obj = (JsonPrimitive)value;
+ }
+ else
+ {
+ obj = (T)(object)value;
+ }
+
+ retstr1 = obj == null ? "null" : obj.ToString();
+ }
+ catch (Exception ex)
+ {
+ exceptionThrown = true;
+ retstr1 = ex.Message;
+ }
+
+ if (assertExceptions)
+ {
+ Assert.Equal<bool>(throwExpected, exceptionThrown);
+ }
+
+ exceptionThrown = false;
+
+ try
+ {
+ dynamic dyn = value as dynamic;
+ if (cast)
+ {
+ ret2 = (T)dyn;
+ }
+ else
+ {
+ ret2 = dyn;
+ }
+ retstr2 = ret2 != null ? ret2.ToString() : "null";
+ }
+ catch (Exception ex)
+ {
+ exceptionThrown = true;
+ retstr2 = ex.Message;
+ }
+
+ if (assertExceptions)
+ {
+ Assert.Equal<bool>(throwExpected, exceptionThrown);
+ }
+
+ // fixup string
+ retstr1 = retstr1.Replace("\'Person\'", String.Format("\'{0}\'", typeof(Person).FullName));
+ if (retstr1.EndsWith(".")) retstr1 = retstr1.Substring(0, retstr1.Length - 1);
+
+ // fixup string
+ retstr2 = retstr2.Replace("\'string\'", String.Format("\'{0}\'", typeof(string).FullName));
+ retstr2 = retstr2.Replace("\'int\'", String.Format("\'{0}\'", typeof(int).FullName));
+ if (retstr2.EndsWith(".")) retstr2 = retstr2.Substring(0, retstr2.Length - 1);
+
+ Assert.Equal<string>(retstr1, retstr2);
+ }
+ }
+}
diff --git a/test/System.Json.Test.Unit/JsonValueLinqExtensionsTest.cs b/test/System.Json.Test.Unit/JsonValueLinqExtensionsTest.cs
new file mode 100644
index 00000000..7729e128
--- /dev/null
+++ b/test/System.Json.Test.Unit/JsonValueLinqExtensionsTest.cs
@@ -0,0 +1,35 @@
+using System.Collections.Generic;
+using System.Linq;
+using Xunit;
+
+namespace System.Json
+{
+ public class JsonValueLinqExtensionsTest
+ {
+ [Fact]
+ public void ToJsonArrayTest()
+ {
+ var target = (new List<int>(new[] { 1, 2, 3 }).Select(i => (JsonValue)i).ToJsonArray());
+ Assert.Equal("[1,2,3]", target.ToString());
+ }
+
+ [Fact]
+ public void ToJsonObjectTest()
+ {
+ JsonValue jv = new JsonObject { { "one", 1 }, { "two", 2 }, { "three", 3 } };
+
+ var result = from n in jv
+ where n.Value.ReadAs<int>() > 1
+ select n;
+ Assert.Equal("{\"two\":2,\"three\":3}", result.ToJsonObject().ToString());
+ }
+
+ [Fact]
+ public void ToJsonObjectFromArray()
+ {
+ JsonArray ja = new JsonArray("first", "second");
+ JsonObject jo = ja.ToJsonObject();
+ Assert.Equal("{\"0\":\"first\",\"1\":\"second\"}", jo.ToString());
+ }
+ }
+}
diff --git a/test/System.Json.Test.Unit/JsonValueTest.cs b/test/System.Json.Test.Unit/JsonValueTest.cs
new file mode 100644
index 00000000..52f10080
--- /dev/null
+++ b/test/System.Json.Test.Unit/JsonValueTest.cs
@@ -0,0 +1,565 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Runtime.Serialization;
+using System.Runtime.Serialization.Json;
+using System.Text;
+using Xunit;
+
+namespace System.Json
+{
+ public class JsonValueTest
+ {
+ const string IndexerNotSupportedOnJsonType = "'{0}' type indexer is not supported on JsonValue of 'JsonType.{1}' type.";
+ const string InvalidIndexType = "Invalid '{0}' index type; only 'System.String' and non-negative 'System.Int32' types are supported.\r\nParameter name: indexes";
+
+ [Fact]
+ public void ContainsKeyTest()
+ {
+ JsonObject target = new JsonObject { { AnyInstance.AnyString, AnyInstance.AnyString } };
+ Assert.True(target.ContainsKey(AnyInstance.AnyString));
+ }
+
+ [Fact]
+ public void LoadTest()
+ {
+ string json = "{\"a\":123,\"b\":[false,null,12.34]}";
+ foreach (bool useLoadTextReader in new bool[] { false, true })
+ {
+ JsonValue jv;
+ if (useLoadTextReader)
+ {
+ using (StringReader sr = new StringReader(json))
+ {
+ jv = JsonValue.Load(sr);
+ }
+ }
+ else
+ {
+ using (MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(json)))
+ {
+ jv = JsonValue.Load(ms);
+ }
+ }
+
+ Assert.Equal(json, jv.ToString());
+ }
+
+ ExceptionHelper.Throws<ArgumentNullException>(() => JsonValue.Load((Stream)null));
+ ExceptionHelper.Throws<ArgumentNullException>(() => JsonValue.Load((TextReader)null));
+ }
+
+ [Fact]
+ public void ParseTest()
+ {
+ JsonValue target;
+ string indentedJson = "{\r\n \"a\": 123,\r\n \"b\": [\r\n false,\r\n null,\r\n 12.34\r\n ],\r\n \"with space\": \"hello\",\r\n \"\": \"empty key\",\r\n \"withTypeHint\": {\r\n \"__type\": \"typeHint\"\r\n }\r\n}";
+ string plainJson = indentedJson.Replace("\r\n", "").Replace(" ", "").Replace("emptykey", "empty key").Replace("withspace", "with space");
+
+ target = JsonValue.Parse(indentedJson);
+ Assert.Equal(plainJson, target.ToString());
+
+ target = JsonValue.Parse(plainJson);
+ Assert.Equal(plainJson, target.ToString());
+
+ ExceptionHelper.Throws<ArgumentNullException>(() => JsonValue.Parse(null));
+ ExceptionHelper.Throws<ArgumentException>(() => JsonValue.Parse(""));
+ }
+
+ [Fact]
+ public void ParseNumbersTest()
+ {
+ string json = "{\"long\":12345678901234,\"zero\":0.0,\"double\":1.23e+200}";
+ string expectedJson = "{\"long\":12345678901234,\"zero\":0,\"double\":1.23E+200}";
+ JsonValue jv = JsonValue.Parse(json);
+
+ Assert.Equal(expectedJson, jv.ToString());
+ Assert.Equal(12345678901234L, (long)jv["long"]);
+ Assert.Equal<double>(0, jv["zero"].ReadAs<double>());
+ Assert.Equal<double>(1.23e200, jv["double"].ReadAs<double>());
+
+ ExceptionHelper.Throws<ArgumentException>(() => JsonValue.Parse("[1.2e+400]"));
+ }
+
+ [Fact]
+ public void ReadAsTest()
+ {
+ JsonValue target = new JsonPrimitive(AnyInstance.AnyInt);
+ Assert.Equal(AnyInstance.AnyInt.ToString(CultureInfo.InvariantCulture), target.ReadAs(typeof(string)));
+ Assert.Equal(AnyInstance.AnyInt.ToString(CultureInfo.InvariantCulture), target.ReadAs<string>());
+ object value;
+ double dblValue;
+ Assert.True(target.TryReadAs(typeof(double), out value));
+ Assert.True(target.TryReadAs<double>(out dblValue));
+ Assert.Equal(Convert.ToDouble(AnyInstance.AnyInt, CultureInfo.InvariantCulture), (double)value);
+ Assert.Equal(Convert.ToDouble(AnyInstance.AnyInt, CultureInfo.InvariantCulture), dblValue);
+ Assert.False(target.TryReadAs(typeof(Guid), out value), "TryReadAs should have failed to read a double as a Guid");
+ Assert.Null(value);
+ }
+
+ [Fact(Skip = "See bug #228569 in CSDMain")]
+ public void SaveTest()
+ {
+ JsonObject jo = new JsonObject
+ {
+ { "first", 1 },
+ { "second", 2 },
+ };
+ JsonValue jv = new JsonArray(123, null, jo);
+ string indentedJson = "[\r\n 123,\r\n null,\r\n {\r\n \"first\": 1,\r\n \"second\": 2\r\n }\r\n]";
+ string plainJson = indentedJson.Replace("\r\n", "").Replace(" ", "");
+
+ SaveJsonValue(jv, plainJson, false);
+ SaveJsonValue(jv, plainJson, true);
+
+ JsonValue target = AnyInstance.DefaultJsonValue;
+ using (MemoryStream ms = new MemoryStream())
+ {
+ ExceptionHelper.Throws<InvalidOperationException>(() => target.Save(ms));
+ }
+ }
+
+ private static void SaveJsonValue(JsonValue jv, string expectedJson, bool useStream)
+ {
+ string json;
+ if (useStream)
+ {
+ using (MemoryStream ms = new MemoryStream())
+ {
+ jv.Save(ms);
+ json = Encoding.UTF8.GetString(ms.ToArray());
+ }
+ }
+ else
+ {
+ StringBuilder sb = new StringBuilder();
+ using (TextWriter writer = new StringWriter(sb))
+ {
+ jv.Save(writer);
+ json = sb.ToString();
+ }
+ }
+
+ Assert.Equal(expectedJson, json);
+ }
+
+ [Fact]
+ public void GetEnumeratorTest()
+ {
+ IEnumerable target = new JsonArray(AnyInstance.AnyGuid);
+ IEnumerator enumerator = target.GetEnumerator();
+ Assert.True(enumerator.MoveNext());
+ Assert.Equal(AnyInstance.AnyGuid, (Guid)(JsonValue)enumerator.Current);
+ Assert.False(enumerator.MoveNext());
+
+ target = new JsonObject();
+ enumerator = target.GetEnumerator();
+ Assert.False(enumerator.MoveNext());
+ }
+
+ [Fact]
+ public void IEnumerableTest()
+ {
+ JsonValue target = AnyInstance.AnyJsonArray;
+
+ // Test IEnumerable<JsonValue> on JsonArray
+ int count = 0;
+
+ foreach (JsonValue value in ((JsonArray)target))
+ {
+ Assert.Same(target[count], value);
+ count++;
+ }
+
+ Assert.Equal<int>(target.Count, count);
+
+ // Test IEnumerable<KeyValuePair<string, JsonValue>> on JsonValue
+ count = 0;
+ foreach (KeyValuePair<string, JsonValue> pair in target)
+ {
+ int index = Int32.Parse(pair.Key);
+ Assert.Equal(count, index);
+ Assert.Same(target[index], pair.Value);
+ count++;
+ }
+ Assert.Equal<int>(target.Count, count);
+
+ target = AnyInstance.AnyJsonObject;
+ count = 0;
+ foreach (KeyValuePair<string, JsonValue> pair in target)
+ {
+ count++;
+ Assert.Same(AnyInstance.AnyJsonObject[pair.Key], pair.Value);
+ }
+ Assert.Equal<int>(AnyInstance.AnyJsonObject.Count, count);
+ }
+
+ [Fact]
+ public void GetJsonPrimitiveEnumeratorTest()
+ {
+ JsonValue target = AnyInstance.AnyJsonPrimitive;
+ IEnumerator<KeyValuePair<string, JsonValue>> enumerator = target.GetEnumerator();
+ Assert.False(enumerator.MoveNext());
+ }
+
+ [Fact]
+ public void GetJsonUndefinedEnumeratorTest()
+ {
+ JsonValue target = AnyInstance.AnyJsonPrimitive.AsDynamic().IDontExist;
+ IEnumerator<KeyValuePair<string, JsonValue>> enumerator = target.GetEnumerator();
+ Assert.False(enumerator.MoveNext());
+ }
+
+ [Fact]
+ public void ToStringTest()
+ {
+ JsonObject jo = new JsonObject
+ {
+ { "first", 1 },
+ { "second", 2 },
+ { "third", new JsonObject { { "inner_one", 4 }, { "", null }, { "inner_3", "" } } },
+ { "fourth", new JsonArray { "Item1", 2, false } },
+ { "fifth", null }
+ };
+ JsonValue jv = new JsonArray(123, null, jo);
+ string expectedJson = "[\r\n 123,\r\n null,\r\n {\r\n \"first\": 1,\r\n \"second\": 2,\r\n \"third\": {\r\n \"inner_one\": 4,\r\n \"\": null,\r\n \"inner_3\": \"\"\r\n },\r\n \"fourth\": [\r\n \"Item1\",\r\n 2,\r\n false\r\n ],\r\n \"fifth\": null\r\n }\r\n]";
+ Assert.Equal<string>(expectedJson.Replace("\r\n", "").Replace(" ", ""), jv.ToString());
+ }
+
+ [Fact]
+ public void CastTests()
+ {
+ int value = 10;
+ JsonValue target = new JsonPrimitive(value);
+
+ int v1 = JsonValue.CastValue<int>(target);
+ Assert.Equal<int>(value, v1);
+ v1 = (int)target;
+ Assert.Equal<int>(value, v1);
+
+ long v2 = JsonValue.CastValue<long>(target);
+ Assert.Equal<long>(value, v2);
+ v2 = (long)target;
+ Assert.Equal<long>(value, v2);
+
+ string s = JsonValue.CastValue<string>(target);
+ Assert.Equal<string>(value.ToString(), s);
+ s = (string)target;
+ Assert.Equal<string>(value.ToString(), s);
+
+ object obj = JsonValue.CastValue<object>(target);
+ Assert.Equal(target, obj);
+ obj = (object)target;
+ Assert.Equal(target, obj);
+
+ object nill = JsonValue.CastValue<object>(null);
+ Assert.Null(nill);
+
+ dynamic dyn = target;
+ JsonValue defaultJv = dyn.IamDefault;
+ nill = JsonValue.CastValue<string>(defaultJv);
+ Assert.Null(nill);
+ nill = (string)defaultJv;
+ Assert.Null(nill);
+
+ obj = JsonValue.CastValue<object>(defaultJv);
+ Assert.Same(defaultJv, obj);
+ obj = (object)defaultJv;
+ Assert.Same(defaultJv, obj);
+
+ JsonValue jv = JsonValue.CastValue<JsonValue>(target);
+ Assert.Equal<JsonValue>(target, jv);
+
+ jv = JsonValue.CastValue<JsonValue>(defaultJv);
+ Assert.Equal<JsonValue>(defaultJv, jv);
+
+ jv = JsonValue.CastValue<JsonPrimitive>(target);
+ Assert.Equal<JsonValue>(target, jv);
+
+ ExceptionHelper.Throws<InvalidCastException>(delegate { int i = JsonValue.CastValue<int>(null); });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { int i = JsonValue.CastValue<int>(defaultJv); });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { int i = JsonValue.CastValue<char>(target); });
+ }
+
+ [Fact]
+ public void CastingTests()
+ {
+ JsonValue target = new JsonPrimitive(AnyInstance.AnyInt);
+
+ Assert.Equal(AnyInstance.AnyInt.ToString(CultureInfo.InvariantCulture), (string)target);
+ Assert.Equal(Convert.ToDouble(AnyInstance.AnyInt, CultureInfo.InvariantCulture), (double)target);
+
+ Assert.Equal(AnyInstance.AnyString, (string)(JsonValue)AnyInstance.AnyString);
+ Assert.Equal(AnyInstance.AnyChar, (char)(JsonValue)AnyInstance.AnyChar);
+ Assert.Equal(AnyInstance.AnyUri, (Uri)(JsonValue)AnyInstance.AnyUri);
+ Assert.Equal(AnyInstance.AnyGuid, (Guid)(JsonValue)AnyInstance.AnyGuid);
+ Assert.Equal(AnyInstance.AnyDateTime, (DateTime)(JsonValue)AnyInstance.AnyDateTime);
+ Assert.Equal(AnyInstance.AnyDateTimeOffset, (DateTimeOffset)(JsonValue)AnyInstance.AnyDateTimeOffset);
+ Assert.Equal(AnyInstance.AnyBool, (bool)(JsonValue)AnyInstance.AnyBool);
+ Assert.Equal(AnyInstance.AnyByte, (byte)(JsonValue)AnyInstance.AnyByte);
+ Assert.Equal(AnyInstance.AnyShort, (short)(JsonValue)AnyInstance.AnyShort);
+ Assert.Equal(AnyInstance.AnyInt, (int)(JsonValue)AnyInstance.AnyInt);
+ Assert.Equal(AnyInstance.AnyLong, (long)(JsonValue)AnyInstance.AnyLong);
+ Assert.Equal(AnyInstance.AnySByte, (sbyte)(JsonValue)AnyInstance.AnySByte);
+ Assert.Equal(AnyInstance.AnyUShort, (ushort)(JsonValue)AnyInstance.AnyUShort);
+ Assert.Equal(AnyInstance.AnyUInt, (uint)(JsonValue)AnyInstance.AnyUInt);
+ Assert.Equal(AnyInstance.AnyULong, (ulong)(JsonValue)AnyInstance.AnyULong);
+ Assert.Equal(AnyInstance.AnyDecimal, (decimal)(JsonValue)AnyInstance.AnyDecimal);
+ Assert.Equal(AnyInstance.AnyFloat, (float)(JsonValue)AnyInstance.AnyFloat);
+ Assert.Equal(AnyInstance.AnyDouble, (double)(JsonValue)AnyInstance.AnyDouble);
+
+ Uri uri = null;
+ string str = null;
+
+ JsonValue jv = uri;
+ Assert.Null(jv);
+ uri = (Uri)jv;
+ Assert.Null(uri);
+
+ jv = str;
+ Assert.Null(jv);
+ str = (string)jv;
+ Assert.Null(str);
+
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var s = (string)AnyInstance.AnyJsonArray; });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var s = (string)AnyInstance.AnyJsonObject; });
+ }
+
+ [Fact]
+ public void InvalidCastTest()
+ {
+ JsonValue nullValue = (JsonValue)null;
+ JsonValue strValue = new JsonPrimitive(AnyInstance.AnyString);
+ JsonValue boolValue = new JsonPrimitive(AnyInstance.AnyBool);
+ JsonValue intValue = new JsonPrimitive(AnyInstance.AnyInt);
+
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (double)nullValue; });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (double)strValue; });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (double)boolValue; });
+ Assert.Equal<double>(AnyInstance.AnyInt, (double)intValue);
+
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (float)nullValue; });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (float)strValue; });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (float)boolValue; });
+ Assert.Equal<float>(AnyInstance.AnyInt, (float)intValue);
+
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (decimal)nullValue; });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (decimal)strValue; });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (decimal)boolValue; });
+ Assert.Equal<decimal>(AnyInstance.AnyInt, (decimal)intValue);
+
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (long)nullValue; });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (long)strValue; });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (long)boolValue; });
+ Assert.Equal<long>(AnyInstance.AnyInt, (long)intValue);
+
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (ulong)nullValue; });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (ulong)strValue; });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (ulong)boolValue; });
+ Assert.Equal<ulong>(AnyInstance.AnyInt, (ulong)intValue);
+
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (int)nullValue; });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (int)strValue; });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (int)boolValue; });
+ Assert.Equal<int>(AnyInstance.AnyInt, (int)intValue);
+
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (uint)nullValue; });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (uint)strValue; });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (uint)boolValue; });
+ Assert.Equal<uint>(AnyInstance.AnyInt, (uint)intValue);
+
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (short)nullValue; });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (short)strValue; });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (short)boolValue; });
+
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (ushort)nullValue; });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (ushort)strValue; });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (ushort)boolValue; });
+
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (sbyte)nullValue; });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (sbyte)strValue; });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (sbyte)boolValue; });
+
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (byte)nullValue; });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (byte)strValue; });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (byte)boolValue; });
+
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (Guid)nullValue; });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (Guid)strValue; });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (Guid)boolValue; });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (Guid)intValue; });
+
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (DateTime)nullValue; });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (DateTime)strValue; });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (DateTime)boolValue; });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (DateTime)intValue; });
+
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (char)nullValue; });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (char)strValue; });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (char)boolValue; });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (char)intValue; });
+
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (DateTimeOffset)nullValue; });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (DateTimeOffset)strValue; });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (DateTimeOffset)boolValue; });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (DateTimeOffset)intValue; });
+
+ Assert.Null((Uri)nullValue);
+ Assert.Equal(((Uri)strValue).ToString(), (string)strValue);
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (Uri)boolValue; });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (Uri)intValue; });
+
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (bool)nullValue; });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (bool)strValue; });
+ Assert.Equal(AnyInstance.AnyBool, (bool)boolValue);
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (bool)intValue; });
+
+ Assert.Equal(null, (string)nullValue);
+ Assert.Equal(AnyInstance.AnyString, (string)strValue);
+ Assert.Equal(AnyInstance.AnyBool.ToString().ToLowerInvariant(), ((string)boolValue).ToLowerInvariant());
+ Assert.Equal(AnyInstance.AnyInt.ToString(CultureInfo.InvariantCulture), (string)intValue);
+
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (int)nullValue; });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (int)strValue; });
+ ExceptionHelper.Throws<InvalidCastException>(delegate { var v = (int)boolValue; });
+ Assert.Equal(AnyInstance.AnyInt, (int)intValue);
+ }
+
+ [Fact]
+ public void CountTest()
+ {
+ JsonArray ja = new JsonArray(1, 2);
+ Assert.Equal(2, ja.Count);
+
+ JsonObject jo = new JsonObject
+ {
+ { "key1", 123 },
+ { "key2", null },
+ { "key3", "hello" },
+ };
+ Assert.Equal(3, jo.Count);
+ }
+
+ [Fact]
+ public void ItemTest()
+ {
+ //// Positive tests for Item on JsonArray and JsonObject are on JsonArrayTest and JsonObjectTest, respectively.
+
+ JsonValue target;
+ target = AnyInstance.AnyJsonPrimitive;
+ ExceptionHelper.Throws<InvalidOperationException>(delegate { var c = target[1]; }, String.Format(IndexerNotSupportedOnJsonType, typeof(int), target.JsonType));
+ ExceptionHelper.Throws<InvalidOperationException>(delegate { target[0] = 123; }, String.Format(IndexerNotSupportedOnJsonType, typeof(int), target.JsonType));
+ ExceptionHelper.Throws<InvalidOperationException>(delegate { var c = target["key"]; }, String.Format(IndexerNotSupportedOnJsonType, typeof(string), target.JsonType));
+ ExceptionHelper.Throws<InvalidOperationException>(delegate { target["here"] = 123; }, String.Format(IndexerNotSupportedOnJsonType, typeof(string), target.JsonType));
+
+ target = AnyInstance.AnyJsonObject;
+ ExceptionHelper.Throws<InvalidOperationException>(delegate { var c = target[0]; }, String.Format(IndexerNotSupportedOnJsonType, typeof(int), target.JsonType));
+ ExceptionHelper.Throws<InvalidOperationException>(delegate { target[0] = 123; }, String.Format(IndexerNotSupportedOnJsonType, typeof(int), target.JsonType));
+
+ target = AnyInstance.AnyJsonArray;
+ ExceptionHelper.Throws<InvalidOperationException>(delegate { var c = target["key"]; }, String.Format(IndexerNotSupportedOnJsonType, typeof(string), target.JsonType));
+ ExceptionHelper.Throws<InvalidOperationException>(delegate { target["here"] = 123; }, String.Format(IndexerNotSupportedOnJsonType, typeof(string), target.JsonType));
+ }
+
+ [Fact(Skip = "Re-enable when DCS have been removed -- see CSDMain 234538")]
+ public void NonSerializableTest()
+ {
+ DataContractJsonSerializer dcjs = new DataContractJsonSerializer(typeof(JsonValue));
+ ExceptionHelper.Throws<NotSupportedException>(() => dcjs.WriteObject(Stream.Null, AnyInstance.DefaultJsonValue));
+ }
+
+ [Fact]
+ public void DefaultConcatTest()
+ {
+ JsonValue jv = JsonValueExtensions.CreateFrom(AnyInstance.AnyPerson);
+ dynamic target = JsonValueExtensions.CreateFrom(AnyInstance.AnyPerson);
+ Person person = AnyInstance.AnyPerson;
+
+ Assert.Equal(person.Address.City, target.Address.City.ReadAs<string>());
+ Assert.Equal(person.Friends[0].Age, target.Friends[0].Age.ReadAs<int>());
+
+ Assert.Equal(target.ValueOrDefault("Address").ValueOrDefault("City"), target.Address.City);
+ Assert.Equal(target.ValueOrDefault("Address", "City"), target.Address.City);
+
+ Assert.Equal(target.ValueOrDefault("Friends").ValueOrDefault(0).ValueOrDefault("Age"), target.Friends[0].Age);
+ Assert.Equal(target.ValueOrDefault("Friends", 0, "Age"), target.Friends[0].Age);
+
+ Assert.Equal(JsonType.Default, AnyInstance.AnyJsonValue1.ValueOrDefault((object[])null).JsonType);
+ Assert.Equal(JsonType.Default, jv.ValueOrDefault("Friends", null).JsonType);
+ Assert.Equal(JsonType.Default, AnyInstance.AnyJsonValue1.ValueOrDefault((string)null).JsonType);
+ Assert.Equal(JsonType.Default, AnyInstance.AnyJsonPrimitive.ValueOrDefault(AnyInstance.AnyString, AnyInstance.AnyShort).JsonType);
+ Assert.Equal(JsonType.Default, AnyInstance.AnyJsonArray.ValueOrDefault((string)null).JsonType);
+ Assert.Equal(JsonType.Default, AnyInstance.AnyJsonObject.ValueOrDefault(AnyInstance.AnyString, null).JsonType);
+ Assert.Equal(JsonType.Default, AnyInstance.AnyJsonArray.ValueOrDefault(-1).JsonType);
+
+ Assert.Same(AnyInstance.AnyJsonValue1, AnyInstance.AnyJsonValue1.ValueOrDefault());
+
+ Assert.Same(AnyInstance.AnyJsonArray.ValueOrDefault(0), AnyInstance.AnyJsonArray.ValueOrDefault((short)0));
+ Assert.Same(AnyInstance.AnyJsonArray.ValueOrDefault(0), AnyInstance.AnyJsonArray.ValueOrDefault((ushort)0));
+ Assert.Same(AnyInstance.AnyJsonArray.ValueOrDefault(0), AnyInstance.AnyJsonArray.ValueOrDefault((byte)0));
+ Assert.Same(AnyInstance.AnyJsonArray.ValueOrDefault(0), AnyInstance.AnyJsonArray.ValueOrDefault((sbyte)0));
+ Assert.Same(AnyInstance.AnyJsonArray.ValueOrDefault(0), AnyInstance.AnyJsonArray.ValueOrDefault((char)0));
+
+ jv = new JsonObject();
+ jv[AnyInstance.AnyString] = AnyInstance.AnyJsonArray;
+
+ Assert.Same(jv.ValueOrDefault(AnyInstance.AnyString, 0), jv.ValueOrDefault(AnyInstance.AnyString, (short)0));
+ Assert.Same(jv.ValueOrDefault(AnyInstance.AnyString, 0), jv.ValueOrDefault(AnyInstance.AnyString, (ushort)0));
+ Assert.Same(jv.ValueOrDefault(AnyInstance.AnyString, 0), jv.ValueOrDefault(AnyInstance.AnyString, (byte)0));
+ Assert.Same(jv.ValueOrDefault(AnyInstance.AnyString, 0), jv.ValueOrDefault(AnyInstance.AnyString, (sbyte)0));
+ Assert.Same(jv.ValueOrDefault(AnyInstance.AnyString, 0), jv.ValueOrDefault(AnyInstance.AnyString, (char)0));
+
+ jv = AnyInstance.AnyJsonObject;
+
+ ExceptionHelper.Throws<ArgumentException>(delegate { var c = jv.ValueOrDefault(AnyInstance.AnyString, AnyInstance.AnyLong); }, String.Format(InvalidIndexType, typeof(long)));
+ ExceptionHelper.Throws<ArgumentException>(delegate { var c = jv.ValueOrDefault(AnyInstance.AnyString, AnyInstance.AnyUInt); }, String.Format(InvalidIndexType, typeof(uint)));
+ ExceptionHelper.Throws<ArgumentException>(delegate { var c = jv.ValueOrDefault(AnyInstance.AnyString, AnyInstance.AnyBool); }, String.Format(InvalidIndexType, typeof(bool)));
+ }
+
+
+ [Fact]
+ public void DataContractSerializerTest()
+ {
+ ValidateSerialization(new JsonPrimitive(DateTime.Now));
+ ValidateSerialization(new JsonObject { { "a", 1 }, { "b", 2 }, { "c", 3 } });
+ ValidateSerialization(new JsonArray { "a", "b", "c", 1, 2, 3 });
+
+ JsonObject beforeObject = new JsonObject { { "a", 1 }, { "b", 2 }, { "c", 3 } };
+ JsonObject afterObject1 = (JsonObject)ValidateSerialization(beforeObject);
+ beforeObject.Add("d", 4);
+ afterObject1.Add("d", 4);
+ Assert.Equal(beforeObject.ToString(), afterObject1.ToString());
+
+ JsonObject afterObject2 = (JsonObject)ValidateSerialization(beforeObject);
+ beforeObject.Add("e", 5);
+ afterObject2.Add("e", 5);
+ Assert.Equal(beforeObject.ToString(), afterObject2.ToString());
+
+ JsonArray beforeArray = new JsonArray { "a", "b", "c" };
+ JsonArray afterArray1 = (JsonArray)ValidateSerialization(beforeArray);
+ beforeArray.Add("d");
+ afterArray1.Add("d");
+ Assert.Equal(beforeArray.ToString(), afterArray1.ToString());
+
+ JsonArray afterArray2 = (JsonArray)ValidateSerialization(beforeArray);
+ beforeArray.Add("e");
+ afterArray2.Add("e");
+ Assert.Equal(beforeArray.ToString(), afterArray2.ToString());
+ }
+
+ private static JsonValue ValidateSerialization(JsonValue beforeSerialization)
+ {
+ Assert.NotNull(beforeSerialization);
+ NetDataContractSerializer serializer = new NetDataContractSerializer();
+ using (MemoryStream memStream = new MemoryStream())
+ {
+ serializer.Serialize(memStream, beforeSerialization);
+ memStream.Position = 0;
+ JsonValue afterDeserialization = (JsonValue)serializer.Deserialize(memStream);
+ Assert.Equal(beforeSerialization.ToString(), afterDeserialization.ToString());
+ return afterDeserialization;
+ }
+ }
+ }
+}
diff --git a/test/System.Json.Test.Unit/Properties/AssemblyInfo.cs b/test/System.Json.Test.Unit/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..fe40f430
--- /dev/null
+++ b/test/System.Json.Test.Unit/Properties/AssemblyInfo.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("System.Json.Test.Unit")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("Microsoft")]
+[assembly: AssemblyProduct("System.Json.Test.Unit")]
+[assembly: AssemblyCopyright("Copyright © Microsoft 2011")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+[assembly: CLSCompliant(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("2c4d325c-ef22-46b8-92af-79bea6364683")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/test/System.Json.Test.Unit/System.Json.Test.Unit.csproj b/test/System.Json.Test.Unit/System.Json.Test.Unit.csproj
new file mode 100644
index 00000000..c589897f
--- /dev/null
+++ b/test/System.Json.Test.Unit/System.Json.Test.Unit.csproj
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProductVersion>9.0.30729</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{EB09CD33-992B-4A31-AB95-8673BA90F1CD}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>System.Json.Test</RootNamespace>
+ <AssemblyName>System.Json.Test</AssemblyName>
+ <FileAlignment>512</FileAlignment>
+ <ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Debug\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Release\Test\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'CodeCoverage' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\CodeCoverage\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="Microsoft.CSharp" />
+ <Reference Include="System" />
+ <Reference Include="System.Runtime.Serialization" />
+ <Reference Include="System.XML" />
+ <Reference Include="xunit">
+ <HintPath>..\..\packages\xunit.1.9.0.1566\lib\xunit.dll</HintPath>
+ </Reference>
+ <Reference Include="xunit.extensions">
+ <HintPath>..\..\packages\xunit.extensions.1.9.0.1566\lib\xunit.extensions.dll</HintPath>
+ </Reference>
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="Common\AnyInstance.cs" />
+ <Compile Include="Common\ExceptionTestHelper.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="FormUrlEncodedJsonTests.cs" />
+ <Compile Include="JsonArrayTest.cs" />
+ <Compile Include="JsonDefaultTest.cs" />
+ <Compile Include="JsonObjectTest.cs" />
+ <Compile Include="JsonPrimitiveTest.cs" />
+ <Compile Include="JsonTypeTest.cs" />
+ <Compile Include="JsonValueDynamicMetaObjectTest.cs" />
+ <Compile Include="JsonValueDynamicTest.cs" />
+ <Compile Include="JsonValueLinqExtensionsTest.cs" />
+ <Compile Include="JsonValueTest.cs" />
+ <Compile Include="Extensions\JsonValueExtensionsTest.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\Microsoft.TestCommon\Microsoft.TestCommon.csproj">
+ <Project>{FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0}</Project>
+ <Name>Microsoft.TestCommon</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\src\System.Json\System.Json.csproj">
+ <Project>{F0441BE9-BDC0-4629-BE5A-8765FFAA2481}</Project>
+ <Name>System.Json</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="packages.config" />
+ </ItemGroup>
+ <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+</Project> \ No newline at end of file
diff --git a/test/System.Json.Test.Unit/packages.config b/test/System.Json.Test.Unit/packages.config
new file mode 100644
index 00000000..d82739c0
--- /dev/null
+++ b/test/System.Json.Test.Unit/packages.config
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="xunit" version="1.9.0.1566" />
+ <package id="xunit.extensions" version="1.9.0.1566" />
+</packages> \ No newline at end of file
diff --git a/test/System.Net.Http.Formatting.Test.Integration/FormUrlEncodedFromContentTests.cs b/test/System.Net.Http.Formatting.Test.Integration/FormUrlEncodedFromContentTests.cs
new file mode 100644
index 00000000..4a374c69
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Integration/FormUrlEncodedFromContentTests.cs
@@ -0,0 +1,527 @@
+using System.Collections.Generic;
+using System.Json;
+using System.Net.Http.Formatting.Parsers;
+using System.Net.Http.Internal;
+using System.Text;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Net.Http.Formatting
+{
+ public class FormUrlEncodedJsonFromContentTests
+ {
+ #region Tests
+
+ [Theory,
+ InlineData("abc", "{\"abc\":null}"),
+ InlineData("%2eabc%2e", "{\".abc.\":null}"),
+ InlineData("", "{}"),
+ InlineData("a=1", "{\"a\":\"1\"}")]
+ public void SimpleStringsTest(string encoded, string expectedResult)
+ {
+ ValidateFormUrlEncoded(encoded, expectedResult);
+
+ }
+
+ [Theory,
+ InlineData("a=2", "{\"a\":\"2\"}"),
+ InlineData("b=true", "{\"b\":\"true\"}"),
+ InlineData("c=hello", "{\"c\":\"hello\"}"),
+ InlineData("d=", "{\"d\":\"\"}"),
+ InlineData("e=null", "{\"e\":null}")]
+ public void SimpleObjectsTest(string encoded, string expectedResult)
+ {
+ ValidateFormUrlEncoded(encoded, expectedResult);
+
+ }
+
+ [Fact]
+ public void LegacyArraysTest()
+ {
+ ValidateFormUrlEncoded("a=1&a=hello&a=333", "{\"a\":[\"1\",\"hello\",\"333\"]}");
+
+ // Only valid in shallow serialization
+ ParseInvalidFormUrlEncoded("a[z]=2&a[z]=3");
+ }
+
+ [Theory,
+ InlineData("a[]=1&a[]=hello&a[]=333", "{\"a\":[\"1\",\"hello\",\"333\"]}"),
+ InlineData("a[b][]=1&a[b][]=hello&a[b][]=333", "{\"a\":{\"b\":[\"1\",\"hello\",\"333\"]}}"),
+ InlineData("a[]=", "{\"a\":[\"\"]}"),
+ InlineData("a%5B%5D=2", @"{""a"":[""2""]}"),
+ InlineData("a[x][0]=1&a[x][]=2", @"{""a"":{""x"":[""1"",""2""]}}")]
+ public void ArraysTest(string encoded, string expectedResult)
+ {
+ ValidateFormUrlEncoded(encoded, expectedResult);
+ }
+
+ [Theory,
+ InlineData("a[0][]=1&a[0][]=hello&a[1][]=333", "{\"a\":[[\"1\",\"hello\"],[\"333\"]]}"),
+ InlineData("a[b][0][]=1&a[b][1][]=hello&a[b][1][]=333", "{\"a\":{\"b\":[[\"1\"],[\"hello\",\"333\"]]}}"),
+ InlineData("a[0][0][0][]=1", "{\"a\":[[[[\"1\"]]]]}")]
+ public void MultidimensionalArraysTest(string encoded, string expectedResult)
+ {
+ ValidateFormUrlEncoded(encoded, expectedResult);
+ }
+
+ [Theory,
+ InlineData("a[0][]=hello&a[2][]=333", "{\"a\":{\"0\":[\"hello\"],\"2\":[\"333\"]}}"),
+ InlineData("a[0]=hello", "{\"a\":[\"hello\"]}"),
+ InlineData("a[1][]=hello", "{\"a\":{\"1\":[\"hello\"]}}"),
+ InlineData("a[1][0]=hello", "{\"a\":{\"1\":[\"hello\"]}}")]
+ public void SparseArraysTest(string encoded, string expectedResult)
+ {
+ ValidateFormUrlEncoded(encoded, expectedResult);
+ }
+
+ [Theory,
+ InlineData("b[]=2&b[1][c]=d", "{\"b\":[\"2\",{\"c\":\"d\"}]}")]
+ public void ArraysWithMixedMembers(string encoded, string expectedResult)
+ {
+ ValidateFormUrlEncoded(encoded, expectedResult);
+ }
+
+ [Theory,
+ InlineData("=3", "{\"\":\"3\"}"),
+ InlineData("a=1&=3", "{\"a\":\"1\",\"\":\"3\"}"),
+ InlineData("=3&b=2", "{\"\":\"3\",\"b\":\"2\"}")]
+ public void EmptyKeyTest(string encoded, string expectedResult)
+ {
+ ValidateFormUrlEncoded(encoded, expectedResult);
+ }
+
+ [Theory,
+ InlineData("a[b]=1&a=2"),
+ InlineData("a[b]=1&a[b][]=2"),
+ InlineData("a[x][]=1&a[x][0]=2"),
+ InlineData("a=2&a[b]=1"),
+ InlineData("[]=1"),
+ InlineData("a[][]=0"),
+ InlineData("a[][x]=0"),
+ InlineData("a&a[b]=1"),
+ InlineData("a&a=1")]
+ public void InvalidObjectGraphsTest(string encoded)
+ {
+ ParseInvalidFormUrlEncoded(encoded);
+ }
+
+ [Theory,
+ InlineData("a[b=2"),
+ InlineData("a[[b]=2"),
+ InlineData("a[b]]=2")]
+ public void InvalidFormUrlEncodingTest(string encoded)
+ {
+ ParseInvalidFormUrlEncoded(encoded);
+ }
+
+ /// <summary>
+ /// Tests for parsing form-urlencoded data originated from JS primitives.
+ /// </summary>
+ [Theory,
+ InlineData("abc", @"{""abc"":null}"),
+ InlineData("123", @"{""123"":null}"),
+ InlineData("true", @"{""true"":null}"),
+ InlineData("", "{}"),
+ InlineData("%2fabc%2f", @"{""\/abc\/"":null}")]
+ public void TestJsonPrimitive(string encoded, string expectedResult)
+ {
+ ValidateFormUrlEncoded(encoded, expectedResult);
+ }
+
+ /// <summary>
+ /// Negative tests for parsing form-urlencoded data originated from JS primitives.
+ /// </summary>
+ [Theory,
+ InlineData("a[b]=1&a=2"),
+ InlineData("a=2&a[b]=1"),
+ InlineData("[]=1")]
+ public void TestJsonPrimitiveNegative(string encoded)
+ {
+ ParseInvalidFormUrlEncoded(encoded);
+ }
+
+ /// <summary>
+ /// Tests for parsing form-urlencoded data originated from JS objects.
+ /// </summary>
+ [Theory,
+ InlineData("a=NaN", @"{""a"":""NaN""}"),
+ InlineData("a=false", @"{""a"":""false""}"),
+ InlineData("a=foo", @"{""a"":""foo""}"),
+ InlineData("1=1", "{\"1\":\"1\"}")]
+ public void TestObjects(string encoded, string expectedResult)
+ {
+ ValidateFormUrlEncoded(encoded, expectedResult);
+ }
+
+ /// <summary>
+ /// Tests for parsing form-urlencoded data originated from JS arrays.
+ /// </summary>
+ [Theory,
+ InlineData("a[]=2", @"{""a"":[""2""]}"),
+ InlineData("a[]=", @"{""a"":[""""]}"),
+ InlineData("a[0][0][]=1", @"{""a"":[[[""1""]]]}"),
+ InlineData("z[]=9&z[]=true&z[]=undefined&z[]=", @"{""z"":[""9"",""true"",""undefined"",""""]}"),
+ InlineData("z[]=9&z[]=true&z[]=undefined&z[]=null", @"{""z"":[""9"",""true"",""undefined"",null]}"),
+ InlineData("z[0][]=9&z[0][]=true&z[1][]=undefined&z[1][]=null", @"{""z"":[[""9"",""true""],[""undefined"",null]]}"),
+ InlineData("a[0][x]=2", @"{""a"":[{""x"":""2""}]}"),
+ InlineData("a%5B%5D=2", @"{""a"":[""2""]}"),
+ InlineData("a%5B%5D=", @"{""a"":[""""]}"),
+ InlineData("z%5B%5D=9&z%5B%5D=true&z%5B%5D=undefined&z%5B%5D=", @"{""z"":[""9"",""true"",""undefined"",""""]}"),
+ InlineData("z%5B%5D=9&z%5B%5D=true&z%5B%5D=undefined&z%5B%5D=null", @"{""z"":[""9"",""true"",""undefined"",null]}"),
+ InlineData("z%5B0%5D%5B%5D=9&z%5B0%5D%5B%5D=true&z%5B1%5D%5B%5D=undefined&z%5B1%5D%5B%5D=null", @"{""z"":[[""9"",""true""],[""undefined"",null]]}")]
+ public void TestArray(string encoded, string expectedResult)
+ {
+ ValidateFormUrlEncoded(encoded, expectedResult);
+ }
+
+ /// <summary>
+ /// Tests for parsing form-urlencoded data originated from JS arrays, using the jQuery 1.3 format (no []'s).
+ /// </summary>
+ [Theory,
+ InlineData("z=9&z=true&z=undefined&z=", @"{""z"":[""9"",""true"",""undefined"",""""]}"),
+ InlineData("z=9&z=true&z=undefined&z=null", @"{""z"":[""9"",""true"",""undefined"",null]}"),
+ InlineData("z=9&z=true&z=undefined&z=null&a=hello", @"{""z"":[""9"",""true"",""undefined"",null],""a"":""hello""}")]
+ public void TestArrayCompat(string encoded, string expectedResult)
+ {
+ ValidateFormUrlEncoded(encoded, expectedResult);
+ }
+
+ /// <summary>
+ /// Negative tests for parsing form-urlencoded data originated from JS arrays.
+ /// </summary>
+ [Theory,
+ InlineData("a[z]=2&a[z]=3")]
+ public void TestArrayCompatNegative(string encoded)
+ {
+ ParseInvalidFormUrlEncoded(encoded);
+ }
+
+ /// <summary>
+ /// Tests for form-urlencoded data originated from sparse JS arrays.
+ /// </summary>
+ [Theory,
+ InlineData("a[2]=hello", @"{""a"":{""2"":""hello""}}"),
+ InlineData("a[x][0]=2", @"{""a"":{""x"":[""2""]}}"),
+ InlineData("a[x][1]=2", @"{""a"":{""x"":{""1"":""2""}}}"),
+ InlineData("a[x][0]=0&a[x][1]=1", @"{""a"":{""x"":[""0"",""1""]}}"),
+ InlineData("a[0][0][0]=hello&a[1][0][0][0][]=hello", @"{""a"":[[[""hello""]],[[[[""hello""]]]]]}"),
+ InlineData("a[0][0][0]=hello&a[1][0][0][0]=hello", @"{""a"":[[[""hello""]],[[[""hello""]]]]}"),
+ InlineData("a[1][0][]=1", @"{""a"":{""1"":[[""1""]]}}"),
+ InlineData("a[1][1][]=1", @"{""a"":{""1"":{""1"":[""1""]}}}"),
+ InlineData("a[1][1][0]=1", @"{""a"":{""1"":{""1"":[""1""]}}}"),
+ InlineData("a[0][]=2&a[0][]=3&a[2][]=1", "{\"a\":{\"0\":[\"2\",\"3\"],\"2\":[\"1\"]}}"),
+ InlineData("a[x][]=1&a[x][1]=2", @"{""a"":{""x"":[""1"",""2""]}}"),
+ InlineData("a[x][0]=1&a[x][]=2", @"{""a"":{""x"":[""1"",""2""]}}")]
+ public void TestArraySparse(string encoded, string expectedResult)
+ {
+ ValidateFormUrlEncoded(encoded, expectedResult);
+ }
+
+ /// <summary>
+ /// Negative tests for parsing form-urlencoded arrays.
+ /// </summary>
+ [Theory,
+ InlineData("a[x]=2&a[x][]=3"),
+ InlineData("a[]=1&a[0][]=2"),
+ InlineData("a[]=1&a[0][0][]=2"),
+ InlineData("a[x][]=1&a[x][0]=2"),
+ InlineData("a[][]=0"),
+ InlineData("a[][x]=0")]
+ public void TestArrayIndexNegative(string encoded)
+ {
+ ParseInvalidFormUrlEncoded(encoded);
+ }
+
+ public static IEnumerable<object[]> TestObjectTestData
+ {
+ get
+ {
+ string encoded = "a[]=4&a[]=5&b[x][]=7&b[y]=8&b[z][]=9&b[z][]=true&b[z][]=undefined&b[z][]=&c=1&f=";
+ string resultStr = @"{""a"":[""4"",""5""],""b"":{""x"":[""7""],""y"":""8"",""z"":[""9"",""true"",""undefined"",""""]},""c"":""1"",""f"":""""}";
+ yield return new[] { encoded, resultStr };
+
+ encoded = "customer[Name]=Pete&customer[Address]=Redmond&customer[Age][0][]=23&customer[Age][0][]=24&customer[Age][1][]=25&" +
+ "customer[Age][1][]=26&customer[Phones][]=425+888+1111&customer[Phones][]=425+345+7777&customer[Phones][]=425+888+4564&" +
+ "customer[EnrolmentDate]=%22%5C%2FDate(1276562539537)%5C%2F%22&role=NewRole&changeDate=3&count=15";
+ resultStr = @"{""customer"":{""Name"":""Pete"",""Address"":""Redmond"",""Age"":[[""23"",""24""],[""25"",""26""]]," +
+ @"""Phones"":[""425 888 1111"",""425 345 7777"",""425 888 4564""],""EnrolmentDate"":""\""\\\/Date(1276562539537)\\\/\""""},""role"":""NewRole"",""changeDate"":""3"",""count"":""15""}";
+ yield return new[] { encoded, resultStr };
+
+ encoded = "customers[0][Name]=Pete2&customers[0][Address]=Redmond2&customers[0][Age][0][]=23&customers[0][Age][0][]=24&" +
+ "customers[0][Age][1][]=25&customers[0][Age][1][]=26&customers[0][Phones][]=425+888+1111&customers[0][Phones][]=425+345+7777&" +
+ "customers[0][Phones][]=425+888+4564&customers[0][EnrolmentDate]=%22%5C%2FDate(1276634840700)%5C%2F%22&customers[1][Name]=Pete3&" +
+ "customers[1][Address]=Redmond3&customers[1][Age][0][]=23&customers[1][Age][0][]=24&customers[1][Age][1][]=25&customers[1][Age][1][]=26&" +
+ "customers[1][Phones][]=425+888+1111&customers[1][Phones][]=425+345+7777&customers[1][Phones][]=425+888+4564&customers[1][EnrolmentDate]=%22%5C%2FDate(1276634840700)%5C%2F%22";
+ resultStr = @"{""customers"":[{""Name"":""Pete2"",""Address"":""Redmond2"",""Age"":[[""23"",""24""],[""25"",""26""]]," +
+ @"""Phones"":[""425 888 1111"",""425 345 7777"",""425 888 4564""],""EnrolmentDate"":""\""\\\/Date(1276634840700)\\\/\""""}," +
+ @"{""Name"":""Pete3"",""Address"":""Redmond3"",""Age"":[[""23"",""24""],[""25"",""26""]],""Phones"":[""425 888 1111"",""425 345 7777"",""425 888 4564""],""EnrolmentDate"":""\""\\\/Date(1276634840700)\\\/\""""}]}";
+ yield return new[] { encoded, resultStr };
+
+ encoded = "ab%5B%5D=hello";
+ resultStr = @"{""ab"":[""hello""]}";
+ yield return new[] { encoded, resultStr };
+
+ encoded = "123=hello";
+ resultStr = @"{""123"":""hello""}";
+ yield return new[] { encoded, resultStr };
+
+ encoded = "a%5B%5D=1&a";
+ resultStr = @"{""a"":[""1"",null]}";
+ yield return new[] { encoded, resultStr };
+
+ encoded = "a=1&a";
+ resultStr = @"{""a"":[""1"",null]}";
+ yield return new[] { encoded, resultStr };
+ }
+ }
+
+ /// <summary>
+ /// Tests for parsing complex object graphs form-urlencoded.
+ /// </summary>
+ [Theory]
+ [PropertyData("TestObjectTestData")]
+ public void TestObject(string encoded, string expectedResult)
+ {
+ ValidateFormUrlEncoded(encoded, expectedResult);
+ }
+
+
+ public static IEnumerable<object[]> TestEncodedNameTestData
+ {
+ get
+ {
+ string encoded = "some+thing=10";
+ string resultStr = @"{""some thing"":""10""}";
+ yield return new[] { encoded, resultStr };
+
+ encoded = "%E5%B8%A6%E4%B8%89%E4%B8%AA%E8%A1%A8=bar";
+ resultStr = @"{""带三个表"":""bar""}";
+ yield return new[] { encoded, resultStr };
+
+ encoded = "some+thing=10&%E5%B8%A6%E4%B8%89%E4%B8%AA%E8%A1%A8=bar";
+ resultStr = @"{""some thing"":""10"",""带三个表"":""bar""}";
+ yield return new[] { encoded, resultStr };
+
+ encoded = "a[0\r\n][b]=1";
+ resultStr = "{\"a\":{\"0\\u000d\\u000a\":{\"b\":\"1\"}}}";
+ yield return new[] { encoded, resultStr };
+ yield return new[] { encoded.Replace("\r", "%0D").Replace("\n", "%0A"), resultStr };
+
+ yield return new[] { "a[0\0]=1", "{\"a\":{\"0\\u0000\":\"1\"}}" };
+ yield return new[] { "a[0%00]=1", "{\"a\":{\"0\\u0000\":\"1\"}}" };
+ yield return new[] { "a[\00]=1", "{\"a\":{\"\\u00000\":\"1\"}}" };
+ yield return new[] { "a[%000]=1", "{\"a\":{\"\\u00000\":\"1\"}}" };
+ }
+ }
+ /// <summary>
+ /// Tests for parsing form-urlencoded data with encoded names.
+ /// </summary>
+ [Theory]
+ [PropertyData("TestEncodedNameTestData")]
+ public void TestEncodedName(string encoded, string expectedResult)
+ {
+ ValidateFormUrlEncoded(encoded, expectedResult);
+ }
+
+ /// <summary>
+ /// Tests for malformed form-urlencoded data.
+ /// </summary>
+ [Theory,
+ InlineData("a[b=2"),
+ InlineData("a[[b]=2"),
+ InlineData("a[b]]=2")]
+ public void TestNegative(string encoded)
+ {
+ ParseInvalidFormUrlEncoded(encoded);
+ }
+
+ /// <summary>
+ /// Tests for parsing generated form-urlencoded data.
+ /// </summary>
+ [Fact]
+ public void GeneratedJsonValueTest()
+ {
+ Random rndGen = new Random(1);
+ int oldMaxArray = CreatorSettings.MaxArrayLength;
+ int oldMaxList = CreatorSettings.MaxListLength;
+ int oldMaxStr = CreatorSettings.MaxStringLength;
+ double oldNullProbability = CreatorSettings.NullValueProbability;
+ bool oldCreateAscii = CreatorSettings.CreateOnlyAsciiChars;
+ CreatorSettings.MaxArrayLength = 5;
+ CreatorSettings.MaxListLength = 3;
+ CreatorSettings.MaxStringLength = 3;
+ CreatorSettings.NullValueProbability = 0;
+ CreatorSettings.CreateOnlyAsciiChars = true;
+ JsonValueCreatorSurrogate jsonValueCreator = new JsonValueCreatorSurrogate();
+ try
+ {
+ for (int i = 0; i < 1000; i++)
+ {
+ JsonValue jv = (JsonValue)jsonValueCreator.CreateInstanceOf(typeof(JsonValue), rndGen);
+ if (jv.JsonType == JsonType.Array || jv.JsonType == JsonType.Object)
+ {
+ string jaStr = FormUrlEncoding(jv);
+ byte[] data = Encoding.UTF8.GetBytes(jaStr);
+ for (var cnt = 1; cnt <= data.Length; cnt += 4)
+ {
+ ICollection<KeyValuePair<string, string>> collection;
+ FormUrlEncodedParser parser = FormUrlEncodedParserTests.CreateParser(data.Length + 1, out collection);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed;
+ ParserState state = FormUrlEncodedParserTests.ParseBufferInSteps(parser, data, cnt, out totalBytesConsumed);
+ Assert.Equal(ParserState.Done, state);
+ Assert.Equal(data.Length, totalBytesConsumed);
+
+ JsonValue deserJv = FormUrlEncodedJson.Parse(collection);
+ Assert.NotNull(deserJv);
+ bool compare = true;
+ if (deserJv is JsonObject && ((JsonObject)deserJv).ContainsKey("JV"))
+ {
+ compare = JsonValueRoundTripComparer.Compare(jv, deserJv["JV"]);
+ }
+ else
+ {
+ compare = JsonValueRoundTripComparer.Compare(jv, deserJv);
+ }
+
+ Assert.True(compare, "Comparison failed for test instance " + i);
+ }
+ }
+ }
+ }
+ finally
+ {
+ CreatorSettings.MaxArrayLength = oldMaxArray;
+ CreatorSettings.MaxListLength = oldMaxList;
+ CreatorSettings.MaxStringLength = oldMaxStr;
+ CreatorSettings.NullValueProbability = oldNullProbability;
+ CreatorSettings.CreateOnlyAsciiChars = oldCreateAscii;
+ }
+ }
+
+ #endregion
+
+ #region Helpers
+
+ private static string FormUrlEncoding(JsonValue jsonValue)
+ {
+ List<string> results = new List<string>();
+ if (jsonValue is JsonPrimitive)
+ {
+ return UriQueryUtility.UrlEncode(((JsonPrimitive)jsonValue).Value.ToString());
+ }
+
+ BuildParams("JV", jsonValue, results);
+ StringBuilder strResult = new StringBuilder();
+ foreach (var result in results)
+ {
+ strResult.Append("&" + result);
+ }
+
+ if (strResult.Length > 0)
+ {
+ return strResult.Remove(0, 1).ToString();
+ }
+
+ return strResult.ToString();
+ }
+
+ private static void BuildParams(string prefix, JsonValue jsonValue, List<string> results)
+ {
+ if (jsonValue is JsonPrimitive)
+ {
+ JsonPrimitive jsonPrimitive = jsonValue as JsonPrimitive;
+ if (jsonPrimitive != null)
+ {
+ if (jsonPrimitive.JsonType == JsonType.String && String.IsNullOrEmpty(jsonPrimitive.Value.ToString()))
+ {
+ results.Add(prefix + "=" + String.Empty);
+ }
+ else
+ {
+ if (jsonPrimitive.Value is DateTime || jsonPrimitive.Value is DateTimeOffset)
+ {
+ string dateStr = jsonPrimitive.ToString();
+ if (!String.IsNullOrEmpty(dateStr) && dateStr.StartsWith("\""))
+ {
+ dateStr = dateStr.Substring(1, dateStr.Length - 2);
+ }
+ results.Add(prefix + "=" + UriQueryUtility.UrlEncode(dateStr));
+ }
+ else
+ {
+ results.Add(prefix + "=" + UriQueryUtility.UrlEncode(jsonPrimitive.Value.ToString()));
+ }
+ }
+ }
+ else
+ {
+ results.Add(prefix + "=" + String.Empty);
+ }
+ }
+ else if (jsonValue is JsonArray)
+ {
+ for (int i = 0; i < jsonValue.Count; i++)
+ {
+ if (jsonValue[i] is JsonArray || jsonValue[i] is JsonObject)
+ {
+ BuildParams(prefix + "[" + i + "]", jsonValue[i], results);
+ }
+ else
+ {
+ BuildParams(prefix + "[]", jsonValue[i], results);
+ }
+ }
+ }
+ else //jsonValue is JsonObject
+ {
+ foreach (KeyValuePair<string, JsonValue> item in jsonValue)
+ {
+ BuildParams(prefix + "[" + item.Key + "]", item.Value, results);
+ }
+ }
+ }
+
+ private static void ParseInvalidFormUrlEncoded(string encoded)
+ {
+ byte[] data = Encoding.UTF8.GetBytes(encoded);
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ ICollection<KeyValuePair<string, string>> collection;
+ FormUrlEncodedParser parser = FormUrlEncodedParserTests.CreateParser(data.Length + 1, out collection);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed;
+ ParserState state = FormUrlEncodedParserTests.ParseBufferInSteps(parser, data, cnt, out totalBytesConsumed);
+ Assert.Equal(ParserState.Done, state);
+ Assert.Equal(data.Length, totalBytesConsumed);
+
+ Assert.ThrowsArgument(() => { FormUrlEncodedJson.Parse(collection); }, null);
+ }
+ }
+
+ private static void ValidateFormUrlEncoded(string encoded, string expectedResult)
+ {
+ byte[] data = Encoding.UTF8.GetBytes(encoded);
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ ICollection<KeyValuePair<string, string>> collection;
+ FormUrlEncodedParser parser = FormUrlEncodedParserTests.CreateParser(data.Length + 1, out collection);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed;
+ ParserState state = FormUrlEncodedParserTests.ParseBufferInSteps(parser, data, cnt, out totalBytesConsumed);
+ Assert.Equal(ParserState.Done, state);
+ Assert.Equal(data.Length, totalBytesConsumed);
+
+ JsonObject result = FormUrlEncodedJson.Parse(collection);
+ Assert.NotNull(result);
+ Assert.Equal(expectedResult, result.ToString());
+ }
+ }
+
+ #endregion
+ }
+} \ No newline at end of file
diff --git a/test/System.Net.Http.Formatting.Test.Integration/FormUrlEncodedFromUriQueryTests.cs b/test/System.Net.Http.Formatting.Test.Integration/FormUrlEncodedFromUriQueryTests.cs
new file mode 100644
index 00000000..66c19964
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Integration/FormUrlEncodedFromUriQueryTests.cs
@@ -0,0 +1,503 @@
+using System.Collections.Generic;
+using System.Json;
+using System.Net.Http.Internal;
+using System.Text;
+using Xunit;
+using Xunit.Extensions;
+
+namespace System.Net.Http.Formatting
+{
+ public class FormUrlEncodedJsonFromUriQueryTests
+ {
+ #region Tests
+
+ [Theory,
+ InlineData("abc", "{\"abc\":null}"),
+ InlineData("%2eabc%2e", "{\".abc.\":null}"),
+ InlineData("", "{}"),
+ InlineData("a=1", "{\"a\":\"1\"}")]
+ public void SimpleStringsTest(string encoded, string expectedResult)
+ {
+ ValidateFormUrlEncoded(encoded, expectedResult);
+ }
+
+ [Theory,
+ InlineData("a=2", "{\"a\":\"2\"}"),
+ InlineData("b=true", "{\"b\":\"true\"}"),
+ InlineData("c=hello", "{\"c\":\"hello\"}"),
+ InlineData("d=", "{\"d\":\"\"}"),
+ InlineData("e=null", "{\"e\":null}")]
+ public void SimpleObjectsTest(string encoded, string expectedResult)
+ {
+ ValidateFormUrlEncoded(encoded, expectedResult);
+ }
+
+ [Fact]
+ public void LegacyArraysTest()
+ {
+ ValidateFormUrlEncoded("a=1&a=hello&a=333", "{\"a\":[\"1\",\"hello\",\"333\"]}");
+
+ // Only valid in shallow serialization
+ ParseInvalidFormUrlEncoded("a[z]=2&a[z]=3");
+ }
+
+ [Theory,
+ InlineData("a[]=1&a[]=hello&a[]=333", "{\"a\":[\"1\",\"hello\",\"333\"]}"),
+ InlineData("a[b][]=1&a[b][]=hello&a[b][]=333", "{\"a\":{\"b\":[\"1\",\"hello\",\"333\"]}}"),
+ InlineData("a[]=", "{\"a\":[\"\"]}"),
+ InlineData("a%5B%5D=2", @"{""a"":[""2""]}"),
+ InlineData("a[x][0]=1&a[x][]=2", @"{""a"":{""x"":[""1"",""2""]}}")]
+ public void ArraysTest(string encoded, string expectedResult)
+ {
+ ValidateFormUrlEncoded(encoded, expectedResult);
+ }
+
+ [Theory,
+ InlineData("a[0][]=1&a[0][]=hello&a[1][]=333", "{\"a\":[[\"1\",\"hello\"],[\"333\"]]}"),
+ InlineData("a[b][0][]=1&a[b][1][]=hello&a[b][1][]=333", "{\"a\":{\"b\":[[\"1\"],[\"hello\",\"333\"]]}}"),
+ InlineData("a[0][0][0][]=1", "{\"a\":[[[[\"1\"]]]]}")]
+ public void MultidimensionalArraysTest(string encoded, string expectedResult)
+ {
+ ValidateFormUrlEncoded(encoded, expectedResult);
+ }
+
+ [Theory,
+ InlineData("a[0][]=hello&a[2][]=333", "{\"a\":{\"0\":[\"hello\"],\"2\":[\"333\"]}}"),
+ InlineData("a[0]=hello", "{\"a\":[\"hello\"]}"),
+ InlineData("a[1][]=hello", "{\"a\":{\"1\":[\"hello\"]}}"),
+ InlineData("a[1][0]=hello", "{\"a\":{\"1\":[\"hello\"]}}")]
+ public void SparseArraysTest(string encoded, string expectedResult)
+ {
+ ValidateFormUrlEncoded(encoded, expectedResult);
+ }
+
+ [Theory,
+ InlineData("b[]=2&b[1][c]=d", "{\"b\":[\"2\",{\"c\":\"d\"}]}")]
+ public void ArraysWithMixedMembers(string encoded, string expectedResult)
+ {
+ ValidateFormUrlEncoded(encoded, expectedResult);
+ }
+
+ [Theory,
+ InlineData("=3", "{\"\":\"3\"}"),
+ InlineData("a=1&=3", "{\"a\":\"1\",\"\":\"3\"}"),
+ InlineData("=3&b=2", "{\"\":\"3\",\"b\":\"2\"}")]
+ public void EmptyKeyTest(string encoded, string expectedResult)
+ {
+ ValidateFormUrlEncoded(encoded, expectedResult);
+ }
+
+ [Theory,
+ InlineData("a[b]=1&a=2"),
+ InlineData("a[b]=1&a[b][]=2"),
+ InlineData("a[x][]=1&a[x][0]=2"),
+ InlineData("a=2&a[b]=1"),
+ InlineData("[]=1"),
+ InlineData("a[][]=0"),
+ InlineData("a[][x]=0"),
+ InlineData("a&a[b]=1"),
+ InlineData("a&a=1")]
+ public void InvalidObjectGraphsTest(string encoded)
+ {
+ ParseInvalidFormUrlEncoded(encoded);
+ }
+
+ [Theory,
+ InlineData("a[b=2"),
+ InlineData("a[[b]=2"),
+ InlineData("a[b]]=2")]
+ public void InvalidFormUrlEncodingTest(string encoded)
+ {
+ ParseInvalidFormUrlEncoded(encoded);
+ }
+
+ /// <summary>
+ /// Tests for parsing form-urlencoded data originated from JS primitives.
+ /// </summary>
+ [Theory,
+ InlineData("abc", @"{""abc"":null}"),
+ InlineData("123", @"{""123"":null}"),
+ InlineData("true", @"{""true"":null}"),
+ InlineData("", "{}"),
+ InlineData("%2fabc%2f", @"{""\/abc\/"":null}")]
+ public void TestJsonPrimitive(string encoded, string expectedResult)
+ {
+ ValidateFormUrlEncoded(encoded, expectedResult);
+ }
+
+ /// <summary>
+ /// Negative tests for parsing form-urlencoded data originated from JS primitives.
+ /// </summary>
+ [Theory,
+ InlineData("a[b]=1&a=2"),
+ InlineData("a=2&a[b]=1"),
+ InlineData("[]=1")]
+ public void TestJsonPrimitiveNegative(string encoded)
+ {
+ ParseInvalidFormUrlEncoded(encoded);
+ }
+
+ /// <summary>
+ /// Tests for parsing form-urlencoded data originated from JS objects.
+ /// </summary>
+ [Theory,
+ InlineData("a=NaN", @"{""a"":""NaN""}"),
+ InlineData("a=false", @"{""a"":""false""}"),
+ InlineData("a=foo", @"{""a"":""foo""}"),
+ InlineData("1=1", "{\"1\":\"1\"}")]
+ public void TestObjects(string encoded, string expectedResult)
+ {
+ ValidateFormUrlEncoded(encoded, expectedResult);
+ }
+
+ /// <summary>
+ /// Tests for parsing form-urlencoded data originated from JS arrays.
+ /// </summary>
+ [Theory,
+ InlineData("a[]=2", @"{""a"":[""2""]}"),
+ InlineData("a[]=", @"{""a"":[""""]}"),
+ InlineData("a[0][0][]=1", @"{""a"":[[[""1""]]]}"),
+ InlineData("z[]=9&z[]=true&z[]=undefined&z[]=", @"{""z"":[""9"",""true"",""undefined"",""""]}"),
+ InlineData("z[]=9&z[]=true&z[]=undefined&z[]=null", @"{""z"":[""9"",""true"",""undefined"",null]}"),
+ InlineData("z[0][]=9&z[0][]=true&z[1][]=undefined&z[1][]=null", @"{""z"":[[""9"",""true""],[""undefined"",null]]}"),
+ InlineData("a[0][x]=2", @"{""a"":[{""x"":""2""}]}"),
+ InlineData("a%5B%5D=2", @"{""a"":[""2""]}"),
+ InlineData("a%5B%5D=", @"{""a"":[""""]}"),
+ InlineData("z%5B%5D=9&z%5B%5D=true&z%5B%5D=undefined&z%5B%5D=", @"{""z"":[""9"",""true"",""undefined"",""""]}"),
+ InlineData("z%5B%5D=9&z%5B%5D=true&z%5B%5D=undefined&z%5B%5D=null", @"{""z"":[""9"",""true"",""undefined"",null]}"),
+ InlineData("z%5B0%5D%5B%5D=9&z%5B0%5D%5B%5D=true&z%5B1%5D%5B%5D=undefined&z%5B1%5D%5B%5D=null", @"{""z"":[[""9"",""true""],[""undefined"",null]]}")]
+ public void TestArray(string encoded, string expectedResult)
+ {
+ ValidateFormUrlEncoded(encoded, expectedResult);
+ }
+
+ /// <summary>
+ /// Tests for parsing form-urlencoded data originated from JS arrays, using the jQuery 1.3 format (no []'s).
+ /// </summary>
+ [Theory,
+ InlineData("z=9&z=true&z=undefined&z=", @"{""z"":[""9"",""true"",""undefined"",""""]}"),
+ InlineData("z=9&z=true&z=undefined&z=null", @"{""z"":[""9"",""true"",""undefined"",null]}"),
+ InlineData("z=9&z=true&z=undefined&z=null&a=hello", @"{""z"":[""9"",""true"",""undefined"",null],""a"":""hello""}")]
+ public void TestArrayCompat(string encoded, string expectedResult)
+ {
+ ValidateFormUrlEncoded(encoded, expectedResult);
+ }
+
+ /// <summary>
+ /// Negative tests for parsing form-urlencoded data originated from JS arrays.
+ /// </summary>
+ [Theory,
+ InlineData("a[z]=2&a[z]=3")]
+ public void TestArrayCompatNegative(string encoded)
+ {
+ ParseInvalidFormUrlEncoded(encoded);
+ }
+
+ /// <summary>
+ /// Tests for form-urlencoded data originated from sparse JS arrays.
+ /// </summary>
+ [Theory,
+ InlineData("a[2]=hello", @"{""a"":{""2"":""hello""}}"),
+ InlineData("a[x][0]=2", @"{""a"":{""x"":[""2""]}}"),
+ InlineData("a[x][1]=2", @"{""a"":{""x"":{""1"":""2""}}}"),
+ InlineData("a[x][0]=0&a[x][1]=1", @"{""a"":{""x"":[""0"",""1""]}}"),
+ InlineData("a[0][0][0]=hello&a[1][0][0][0][]=hello", @"{""a"":[[[""hello""]],[[[[""hello""]]]]]}"),
+ InlineData("a[0][0][0]=hello&a[1][0][0][0]=hello", @"{""a"":[[[""hello""]],[[[""hello""]]]]}"),
+ InlineData("a[1][0][]=1", @"{""a"":{""1"":[[""1""]]}}"),
+ InlineData("a[1][1][]=1", @"{""a"":{""1"":{""1"":[""1""]}}}"),
+ InlineData("a[1][1][0]=1", @"{""a"":{""1"":{""1"":[""1""]}}}"),
+ InlineData("a[0][]=2&a[0][]=3&a[2][]=1", "{\"a\":{\"0\":[\"2\",\"3\"],\"2\":[\"1\"]}}"),
+ InlineData("a[x][]=1&a[x][1]=2", @"{""a"":{""x"":[""1"",""2""]}}"),
+ InlineData("a[x][0]=1&a[x][]=2", @"{""a"":{""x"":[""1"",""2""]}}")]
+ public void TestArraySparse(string encoded, string expectedResult)
+ {
+ ValidateFormUrlEncoded(encoded, expectedResult);
+ }
+
+ /// <summary>
+ /// Negative tests for parsing form-urlencoded arrays.
+ /// </summary>
+ [Theory,
+ InlineData("a[x]=2&a[x][]=3"),
+ InlineData("a[]=1&a[0][]=2"),
+ InlineData("a[]=1&a[0][0][]=2"),
+ InlineData("a[x][]=1&a[x][0]=2"),
+ InlineData("a[][]=0"),
+ InlineData("a[][x]=0")]
+ public void TestArrayIndexNegative(string encoded)
+ {
+ ParseInvalidFormUrlEncoded(encoded);
+ }
+
+
+ public static IEnumerable<object[]> TestObjectPropertyData
+ {
+ get
+ {
+ string encoded = "a[]=4&a[]=5&b[x][]=7&b[y]=8&b[z][]=9&b[z][]=true&b[z][]=undefined&b[z][]=&c=1&f=";
+ string resultStr = @"{""a"":[""4"",""5""],""b"":{""x"":[""7""],""y"":""8"",""z"":[""9"",""true"",""undefined"",""""]},""c"":""1"",""f"":""""}";
+ yield return new[] { encoded, resultStr };
+
+ encoded = "customer[Name]=Pete&customer[Address]=Redmond&customer[Age][0][]=23&customer[Age][0][]=24&customer[Age][1][]=25&" +
+ "customer[Age][1][]=26&customer[Phones][]=425+888+1111&customer[Phones][]=425+345+7777&customer[Phones][]=425+888+4564&" +
+ "customer[EnrolmentDate]=%22%5C%2FDate(1276562539537)%5C%2F%22&role=NewRole&changeDate=3&count=15";
+ resultStr = @"{""customer"":{""Name"":""Pete"",""Address"":""Redmond"",""Age"":[[""23"",""24""],[""25"",""26""]]," +
+ @"""Phones"":[""425 888 1111"",""425 345 7777"",""425 888 4564""],""EnrolmentDate"":""\""\\\/Date(1276562539537)\\\/\""""},""role"":""NewRole"",""changeDate"":""3"",""count"":""15""}";
+ yield return new[] { encoded, resultStr };
+
+ encoded = "customers[0][Name]=Pete2&customers[0][Address]=Redmond2&customers[0][Age][0][]=23&customers[0][Age][0][]=24&" +
+ "customers[0][Age][1][]=25&customers[0][Age][1][]=26&customers[0][Phones][]=425+888+1111&customers[0][Phones][]=425+345+7777&" +
+ "customers[0][Phones][]=425+888+4564&customers[0][EnrolmentDate]=%22%5C%2FDate(1276634840700)%5C%2F%22&customers[1][Name]=Pete3&" +
+ "customers[1][Address]=Redmond3&customers[1][Age][0][]=23&customers[1][Age][0][]=24&customers[1][Age][1][]=25&customers[1][Age][1][]=26&" +
+ "customers[1][Phones][]=425+888+1111&customers[1][Phones][]=425+345+7777&customers[1][Phones][]=425+888+4564&customers[1][EnrolmentDate]=%22%5C%2FDate(1276634840700)%5C%2F%22";
+ resultStr = @"{""customers"":[{""Name"":""Pete2"",""Address"":""Redmond2"",""Age"":[[""23"",""24""],[""25"",""26""]]," +
+ @"""Phones"":[""425 888 1111"",""425 345 7777"",""425 888 4564""],""EnrolmentDate"":""\""\\\/Date(1276634840700)\\\/\""""}," +
+ @"{""Name"":""Pete3"",""Address"":""Redmond3"",""Age"":[[""23"",""24""],[""25"",""26""]],""Phones"":[""425 888 1111"",""425 345 7777"",""425 888 4564""],""EnrolmentDate"":""\""\\\/Date(1276634840700)\\\/\""""}]}";
+ yield return new[] { encoded, resultStr };
+
+ encoded = "ab%5B%5D=hello";
+ resultStr = @"{""ab"":[""hello""]}";
+ yield return new[] { encoded, resultStr };
+
+ encoded = "123=hello";
+ resultStr = @"{""123"":""hello""}";
+ yield return new[] { encoded, resultStr };
+
+ encoded = "a%5B%5D=1&a";
+ resultStr = @"{""a"":[""1"",null]}";
+ yield return new[] { encoded, resultStr };
+
+ encoded = "a=1&a";
+ resultStr = @"{""a"":[""1"",null]}";
+ yield return new[] { encoded, resultStr };
+ }
+ }
+
+ /// <summary>
+ /// Tests for parsing complex object graphs form-urlencoded.
+ /// </summary>
+ [Theory]
+ [PropertyData("TestObjectPropertyData")]
+ public void TestObject(string encoded, string expectedResult)
+ {
+ ValidateFormUrlEncoded(encoded, expectedResult);
+ }
+
+ public static IEnumerable<object[]> TestEncodedNameTestData
+ {
+ get
+ {
+ string encoded = "some+thing=10";
+ string resultStr = @"{""some thing"":""10""}";
+ yield return new[] { encoded, resultStr };
+
+ encoded = "%E5%B8%A6%E4%B8%89%E4%B8%AA%E8%A1%A8=bar";
+ resultStr = @"{""带三个表"":""bar""}";
+ yield return new[] { encoded, resultStr };
+
+ encoded = "some+thing=10&%E5%B8%A6%E4%B8%89%E4%B8%AA%E8%A1%A8=bar";
+ resultStr = @"{""some thing"":""10"",""带三个表"":""bar""}";
+ yield return new[] { encoded, resultStr };
+
+ encoded = "a[0\r\n][b]=1";
+ resultStr = "{\"a\":{\"0\\u000d\\u000a\":{\"b\":\"1\"}}}";
+ yield return new[] { encoded, resultStr };
+ yield return new[] { encoded.Replace("\r", "%0D").Replace("\n", "%0A"), resultStr };
+
+ yield return new[] { "a[0\0]=1", "{\"a\":{\"0\\u0000\":\"1\"}}" };
+ yield return new[] { "a[0%00]=1", "{\"a\":{\"0\\u0000\":\"1\"}}" };
+ yield return new[] { "a[\00]=1", "{\"a\":{\"\\u00000\":\"1\"}}" };
+ yield return new[] { "a[%000]=1", "{\"a\":{\"\\u00000\":\"1\"}}" };
+ }
+ }
+
+ /// <summary>
+ /// Tests for parsing form-urlencoded data with encoded names.
+ /// </summary>
+ [Theory]
+ [PropertyData("TestEncodedNameTestData")]
+ public void TestEncodedName(string encoded, string expectedResult)
+ {
+ ValidateFormUrlEncoded(encoded, expectedResult);
+ }
+
+ /// <summary>
+ /// Tests for malformed form-urlencoded data.
+ /// </summary>
+ [Theory,
+ InlineData("a[b=2"),
+ InlineData("a[[b]=2"),
+ InlineData("a[b]]=2")]
+ public void TestNegative(string encoded)
+ {
+ ParseInvalidFormUrlEncoded(encoded);
+ }
+
+ /// <summary>
+ /// Tests for parsing generated form-urlencoded data.
+ /// </summary>
+ [Fact]
+ public void GeneratedJsonValueTest()
+ {
+ Random rndGen = new Random(1);
+ int oldMaxArray = CreatorSettings.MaxArrayLength;
+ int oldMaxList = CreatorSettings.MaxListLength;
+ int oldMaxStr = CreatorSettings.MaxStringLength;
+ double oldNullProbability = CreatorSettings.NullValueProbability;
+ bool oldCreateAscii = CreatorSettings.CreateOnlyAsciiChars;
+ CreatorSettings.MaxArrayLength = 5;
+ CreatorSettings.MaxListLength = 3;
+ CreatorSettings.MaxStringLength = 3;
+ CreatorSettings.NullValueProbability = 0;
+ CreatorSettings.CreateOnlyAsciiChars = true;
+ JsonValueCreatorSurrogate jsonValueCreator = new JsonValueCreatorSurrogate();
+ try
+ {
+ for (int i = 0; i < 1000; i++)
+ {
+ JsonValue jv = (JsonValue)jsonValueCreator.CreateInstanceOf(typeof(JsonValue), rndGen);
+ if (jv.JsonType == JsonType.Array || jv.JsonType == JsonType.Object)
+ {
+ string jaStr = FormUrlEncoding(jv);
+ JsonValue deserJv = ParseFormUrlEncoded(jaStr);
+ bool compare = true;
+ if (deserJv is JsonObject && ((JsonObject)deserJv).ContainsKey("JV"))
+ {
+ compare = JsonValueRoundTripComparer.Compare(jv, deserJv["JV"]);
+ }
+ else
+ {
+ compare = JsonValueRoundTripComparer.Compare(jv, deserJv);
+ }
+
+ Assert.True(compare, "Comparison failed for test instance " + i);
+ }
+ }
+ }
+ finally
+ {
+ CreatorSettings.MaxArrayLength = oldMaxArray;
+ CreatorSettings.MaxListLength = oldMaxList;
+ CreatorSettings.MaxStringLength = oldMaxStr;
+ CreatorSettings.NullValueProbability = oldNullProbability;
+ CreatorSettings.CreateOnlyAsciiChars = oldCreateAscii;
+ }
+ }
+
+ #endregion
+
+ #region Helpers
+ private static string FormUrlEncoding(JsonValue jsonValue)
+ {
+ List<string> results = new List<string>();
+ if (jsonValue is JsonPrimitive)
+ {
+ return UriQueryUtility.UrlEncode(((JsonPrimitive)jsonValue).Value.ToString());
+ }
+
+ BuildParams("JV", jsonValue, results);
+ StringBuilder strResult = new StringBuilder();
+ foreach (var result in results)
+ {
+ strResult.Append("&" + result);
+ }
+
+ if (strResult.Length > 0)
+ {
+ return strResult.Remove(0, 1).ToString();
+ }
+
+ return strResult.ToString();
+ }
+
+ private static void BuildParams(string prefix, JsonValue jsonValue, List<string> results)
+ {
+ if (jsonValue is JsonPrimitive)
+ {
+ JsonPrimitive jsonPrimitive = jsonValue as JsonPrimitive;
+ if (jsonPrimitive != null)
+ {
+ if (jsonPrimitive.JsonType == JsonType.String && String.IsNullOrEmpty(jsonPrimitive.Value.ToString()))
+ {
+ results.Add(prefix + "=" + String.Empty);
+ }
+ else
+ {
+ if (jsonPrimitive.Value is DateTime || jsonPrimitive.Value is DateTimeOffset)
+ {
+ string dateStr = jsonPrimitive.ToString();
+ if (!String.IsNullOrEmpty(dateStr) && dateStr.StartsWith("\""))
+ {
+ dateStr = dateStr.Substring(1, dateStr.Length - 2);
+ }
+ results.Add(prefix + "=" + UriQueryUtility.UrlEncode(dateStr));
+ }
+ else
+ {
+ results.Add(prefix + "=" + UriQueryUtility.UrlEncode(jsonPrimitive.Value.ToString()));
+ }
+ }
+ }
+ else
+ {
+ results.Add(prefix + "=" + String.Empty);
+ }
+ }
+ else if (jsonValue is JsonArray)
+ {
+ for (int i = 0; i < jsonValue.Count; i++)
+ {
+ if (jsonValue[i] is JsonArray || jsonValue[i] is JsonObject)
+ {
+ BuildParams(prefix + "[" + i + "]", jsonValue[i], results);
+ }
+ else
+ {
+ BuildParams(prefix + "[]", jsonValue[i], results);
+ }
+ }
+ }
+ else //jsonValue is JsonObject
+ {
+ foreach (KeyValuePair<string, JsonValue> item in jsonValue)
+ {
+ BuildParams(prefix + "[" + item.Key + "]", item.Value, results);
+ }
+ }
+ }
+
+ private static Uri GetQueryUri(string query)
+ {
+ UriBuilder uriBuilder = new UriBuilder("http://some.host");
+ uriBuilder.Query = query;
+ return uriBuilder.Uri;
+ }
+
+ private static JsonValue ParseFormUrlEncoded(string encoded)
+ {
+ Uri address = GetQueryUri(encoded);
+ JsonObject result;
+ Assert.True(address.TryReadQueryAsJson(out result), "Expected parsing to return true");
+ return result;
+ }
+
+ private static void ParseInvalidFormUrlEncoded(string encoded)
+ {
+ Uri address = GetQueryUri(encoded);
+ JsonObject result;
+ Assert.False(address.TryReadQueryAsJson(out result), "Expected parsing to return false");
+ Assert.Null(result);
+ }
+
+ private static void ValidateFormUrlEncoded(string encoded, string expectedResult)
+ {
+ Uri address = GetQueryUri(encoded);
+ JsonObject result;
+ Assert.True(address.TryReadQueryAsJson(out result), "Expected parsing to return true");
+ Assert.NotNull(result);
+ Assert.Equal(expectedResult, result.ToString());
+ }
+
+ #endregion
+ }
+} \ No newline at end of file
diff --git a/test/System.Net.Http.Formatting.Test.Integration/JsonNetSerializationTest.cs b/test/System.Net.Http.Formatting.Test.Integration/JsonNetSerializationTest.cs
new file mode 100644
index 00000000..6348e893
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Integration/JsonNetSerializationTest.cs
@@ -0,0 +1,536 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Json;
+using System.Runtime.Serialization;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.TestCommon;
+using Newtonsoft.Json;
+using Xunit;
+using Xunit.Extensions;
+
+namespace System.Net.Http.Formatting
+{
+ public class JsonNetSerializationTest
+ {
+ public static IEnumerable<object[]> SerializedJson
+ {
+ get
+ {
+ return new TheoryDataSet<object, string>()
+ {
+ // Primitives
+ { 'f', "\"f\"" },
+ { "abc", "\"abc\"" },
+ { "\"\\", @"""\""\\""" },
+ { 256, "256" },
+ { (ulong)long.MaxValue, long.MaxValue.ToString() },
+ { 45.78m, "45.78" },
+ { .00000457823432, "4.57823432E-06" },
+ { (byte)24, "24" },
+ { false, "false" },
+ { AttributeTargets.Assembly | AttributeTargets.Constructor, "33" },
+ { ConsoleColor.DarkCyan, "3" },
+ { new DateTimeOffset(1999, 5, 27, 4, 34, 45, TimeSpan.Zero), "\"\\/Date(927779685000+0000)\\/\"" },
+ { new TimeSpan(5, 30, 0), "\"05:30:00\"" },
+ { new Uri("http://www.bing.com"), @"""http://www.bing.com/""" },
+ { new Guid("4ed1cd44-11d7-4b27-b623-0b8b553c8906"), "\"4ed1cd44-11d7-4b27-b623-0b8b553c8906\"" },
+
+ // Structs
+ { new Point() { x = 45, Y = -5}, "{\"x\":45,\"Y\":-5}" },
+
+ // Arrays
+ { new object[] {}, "[]" },
+ { new int[] { 1, 2, 3}, "[1,2,3]" },
+ { new string[] { "a", "b"}, "[\"a\",\"b\"]" },
+ { new Point[] { new Point() { x = 10, Y = 10}, new Point() { x = 20, Y = 20}}, "[{\"x\":10,\"Y\":10},{\"x\":20,\"Y\":20}]" },
+
+ // Collections
+ { new List<int> { 1, 2, 3}, "[1,2,3]" },
+ { new List<string> { "a", "b"}, "[\"a\",\"b\"]" },
+ { new List<Point> { new Point() { x = 10, Y = 10}, new Point() { x = 20, Y = 20}}, "[{\"x\":10,\"Y\":10},{\"x\":20,\"Y\":20}]" },
+ { new MyList<int> { 1, 2, 3}, "[1,2,3]" },
+ { new MyList<string> { "a", "b"}, "[\"a\",\"b\"]" },
+ { new MyList<Point> { new Point() { x = 10, Y = 10}, new Point() { x = 20, Y = 20}}, "[{\"x\":10,\"Y\":10},{\"x\":20,\"Y\":20}]" },
+
+ // Dictionaries
+
+ { new Dictionary<string, string> { { "k1", "v1" }, { "k2", "v2" } }, "{\"k1\":\"v1\",\"k2\":\"v2\"}" },
+ { new Dictionary<int, string> { { 1, "v1" }, { 2, "v2" } }, "{\"1\":\"v1\",\"2\":\"v2\"}" },
+
+ // Anonymous types
+ { new { Anon1 = 56, Anon2 = "foo"}, "{\"Anon1\":56,\"Anon2\":\"foo\"}" },
+
+ // Classes
+ { new DataContractType() { s = "foo", i = 49, NotAMember = "Error" }, "{\"s\":\"foo\",\"i\":49}" },
+ { new POCOType() { s = "foo", t = "Error"}, "{\"s\":\"foo\"}" },
+ { new SerializableType("protected") { publicField = "public", protectedInternalField = "protected internal", internalField = "internal", PublicProperty = "private", nonSerializedField = "Error" }, "{\"publicField\":\"public\",\"internalField\":\"internal\",\"protectedInternalField\":\"protected internal\",\"protectedField\":\"protected\",\"privateField\":\"private\"}" },
+
+ // Generics
+ { new KeyValuePair<string, bool>("foo", false), "{\"Key\":\"foo\",\"Value\":false}" },
+
+ // ISerializable types
+ { new ArgumentNullException("param"), "{\"ClassName\":\"System.ArgumentNullException\",\"Message\":\"Value cannot be null.\",\"Data\":null,\"InnerException\":null,\"HelpURL\":null,\"StackTraceString\":null,\"RemoteStackTraceString\":null,\"RemoteStackIndex\":0,\"ExceptionMethod\":null,\"HResult\":-2147467261,\"Source\":null,\"WatsonBuckets\":null,\"ParamName\":\"param\"}" },
+
+ // JSON Values
+ { new JsonPrimitive(false), "false" },
+ { new JsonPrimitive(54), "54" },
+ { new JsonPrimitive("s"), "\"s\"" },
+ { new JsonArray(new JsonPrimitive(1), new JsonPrimitive(2)), "[1,2]" },
+ { new JsonObject(new KeyValuePair<string, JsonValue>("k1", new JsonPrimitive("v1")), new KeyValuePair<string, JsonValue>("k2", new JsonPrimitive("v2"))), "{\"k1\":\"v1\",\"k2\":\"v2\"}" },
+ { new KeyValuePair<JsonValue, JsonValue>(new JsonPrimitive("k"), new JsonArray(new JsonPrimitive("v1"), new JsonPrimitive("v2"))), "{\"Key\":\"k\",\"Value\":[\"v1\",\"v2\"]}" },
+ };
+ }
+ }
+
+ public static IEnumerable<object[]> TypedSerializedJson
+ {
+ get
+ {
+ return new TheoryDataSet<object, string, Type>()
+ {
+ // Null
+ { null, "null", typeof(POCOType) },
+ { null, "null", typeof(JsonValue) },
+
+ // Nullables
+ { new int?(), "null", typeof(int?) },
+ { new Point?(), "null", typeof(Point?) },
+ { new ConsoleColor?(), "null", typeof(ConsoleColor?) },
+ { new int?(45), "45", typeof(int?) },
+ { new Point?(new Point() { x = 45, Y = -5 }), "{\"x\":45,\"Y\":-5}", typeof(Point?) },
+ { new ConsoleColor?(ConsoleColor.DarkMagenta), "5", typeof(ConsoleColor?)},
+ };
+ }
+ }
+
+ [Theory]
+ [PropertyData("SerializedJson")]
+ public void ObjectsSerializeToExpectedJson(object o, string expectedJson)
+ {
+ ObjectsSerializeToExpectedJsonWithProvidedType(o, expectedJson, o.GetType());
+ }
+
+ [Theory]
+ [PropertyData("SerializedJson")]
+ public void JsonDeserializesToExpectedObject(object expectedObject, string json)
+ {
+ JsonDeserializesToExpectedObjectWithProvidedType(expectedObject, json, expectedObject.GetType());
+ }
+
+ [Theory]
+ [PropertyData("TypedSerializedJson")]
+ public void ObjectsSerializeToExpectedJsonWithProvidedType(object o, string expectedJson, Type type)
+ {
+ Assert.Equal(expectedJson, Serialize(o, type));
+ }
+
+ [Theory]
+ [PropertyData("TypedSerializedJson")]
+ public void JsonDeserializesToExpectedObjectWithProvidedType(object expectedObject, string json, Type type)
+ {
+ if (expectedObject == null)
+ {
+ Assert.Null(Deserialize(json, type));
+ }
+ else
+ {
+ Assert.Equal(expectedObject, Deserialize(json, type), new ObjectComparer());
+ }
+ }
+
+ [Fact]
+ public void CallbacksGetCalled()
+ {
+ TypeWithCallbacks o = new TypeWithCallbacks();
+
+ string json = Serialize(o, typeof(TypeWithCallbacks));
+ Assert.Equal("12", o.callbackOrder);
+
+ TypeWithCallbacks deserializedObject = Deserialize(json, typeof(TypeWithCallbacks)) as TypeWithCallbacks;
+ Assert.Equal("34", deserializedObject.callbackOrder);
+ }
+
+ [Fact]
+ public void DerivedTypesArePreserved()
+ {
+ JsonMediaTypeFormatter formatter = new JsonMediaTypeFormatter();
+ formatter.Serializer.TypeNameHandling = TypeNameHandling.Objects;
+ string json = Serialize(new Derived(), typeof(Base), formatter);
+ object deserializedObject = Deserialize(json, typeof(Base), formatter);
+ Assert.IsType(typeof(Derived), deserializedObject);
+ }
+
+ [Fact]
+ public void ArbitraryTypesArentDeserializedByDefault()
+ {
+ string json = "{\"$type\":\"" + typeof(DangerousType).AssemblyQualifiedName + "\"}";
+ object deserializedObject = Deserialize(json, typeof(object));
+ Assert.IsNotType(typeof(DangerousType), deserializedObject);
+ }
+
+ [Fact]
+ public void ReferencesArePreserved()
+ {
+ Ref ref1 = new Ref();
+ Ref ref2 = new Ref();
+ ref1.Reference = ref2;
+ ref2.Reference = ref1;
+
+ string json = Serialize(ref1, typeof(Ref));
+ Ref deserializedObject = Deserialize(json, typeof(Ref)) as Ref;
+
+ Assert.ReferenceEquals(deserializedObject, deserializedObject.Reference.Reference);
+ }
+
+ [Fact]
+ public void DeserializingDeepObjectsThrows()
+ {
+ StringBuilder sb = new StringBuilder();
+ int depth = 1500;
+ for (int i = 0; i < depth; i++)
+ {
+ sb.Append("{\"a\":");
+ }
+ sb.Append("null");
+ for (int i = 0; i < depth; i++)
+ {
+ sb.Append("}");
+ }
+ string json = sb.ToString();
+
+ MediaTypeFormatter.SkipStreamLimitChecks = true;
+ Assert.Throws(typeof(JsonSerializationException), () => Deserialize(json, typeof(object)));
+ }
+
+ [Fact]
+ public void DeserializingDeepArraysThrows()
+ {
+ StringBuilder sb = new StringBuilder();
+ int depth = 1500;
+ for (int i = 0; i < depth; i++)
+ {
+ sb.Append("[");
+ }
+ sb.Append("null");
+ for (int i = 0; i < depth; i++)
+ {
+ sb.Append("]");
+ }
+ string json = sb.ToString();
+
+ Assert.Throws(typeof(JsonSerializationException), () => Deserialize(json, typeof(object)));
+ }
+
+ [Theory]
+ // existing good surrogate pair
+ [InlineData("ABC \\ud800\\udc00 DEF", "ABC \ud800\udc00 DEF")]
+ // invalid surrogates (two high back-to-back)
+ [InlineData("ABC \\ud800\\ud800 DEF", "ABC \ufffd\ufffd DEF")]
+ // invalid high surrogate at end of string
+ [InlineData("ABC \\ud800", "ABC \ufffd")]
+ // high surrogate not followed by low surrogate
+ [InlineData("ABC \\ud800 DEF", "ABC \ufffd DEF")]
+ // low surrogate not preceded by high surrogate
+ [InlineData("ABC \\udc00\\ud800 DEF", "ABC \ufffd\ufffd DEF")]
+ // make sure unencoded invalid surrogate characters don't make it through
+ [InlineData("\udc00\ud800\ud800", "??????")]
+ public void InvalidUnicodeStringsAreFixedUp(string input, string expectedString)
+ {
+ string json = "\"" + input + "\"";
+ string deserializedString = Deserialize(json, typeof(string)) as string;
+
+ Assert.Equal(expectedString, deserializedString);
+
+ }
+
+ string Serialize(object o, Type type, MediaTypeFormatter formatter = null)
+ {
+ formatter = formatter ?? new JsonMediaTypeFormatter();
+ MemoryStream ms = new MemoryStream();
+ formatter.WriteToStreamAsync(type, o, ms, null, null).Wait();
+ ms.Flush();
+ ms.Position = 0;
+ return new StreamReader(ms).ReadToEnd();
+ }
+
+ object Deserialize(string json, Type type, MediaTypeFormatter formatter = null)
+ {
+ formatter = formatter ?? new JsonMediaTypeFormatter();
+ MemoryStream ms = new MemoryStream();
+ byte[] bytes = Encoding.Default.GetBytes(json);
+ ms.Write(bytes, 0, bytes.Length);
+ ms.Flush();
+ ms.Position = 0;
+ Task<object> readTask = formatter.ReadFromStreamAsync(type, ms, contentHeaders: null, formatterLogger: null);
+ readTask.WaitUntilCompleted();
+ if (readTask.IsFaulted)
+ {
+ throw readTask.Exception.GetBaseException();
+ }
+ return readTask.Result;
+ }
+ }
+
+ public class ObjectComparer : IEqualityComparer<object>
+ {
+ bool IEqualityComparer<object>.Equals(object x, object y)
+ {
+ Type xType = x.GetType();
+
+ if (xType == y.GetType())
+ {
+ if (typeof(JsonValue).IsAssignableFrom(xType) || xType == typeof(ArgumentNullException) || xType == typeof(KeyValuePair<JsonValue, JsonValue>))
+ {
+ return x.ToString() == y.ToString();
+ }
+ if (xType == typeof(DataContractType))
+ {
+ return Equals<DataContractType>(x, y);
+ }
+ if (xType == typeof(POCOType))
+ {
+ return Equals<POCOType>(x, y);
+ }
+
+ if (xType == typeof(SerializableType))
+ {
+ return Equals<SerializableType>(x, y);
+ }
+
+ if (xType == typeof(Point))
+ {
+ return Equals<Point>(x, y);
+ }
+
+ if (typeof(IEnumerable).IsAssignableFrom(xType))
+ {
+ IEnumerator xEnumerator = ((IEnumerable)x).GetEnumerator();
+ IEnumerator yEnumerator = ((IEnumerable)y).GetEnumerator();
+ while (xEnumerator.MoveNext())
+ {
+ // if x is longer than y
+ if (!yEnumerator.MoveNext())
+ {
+ return false;
+ }
+ else
+ {
+ if (!xEnumerator.Current.Equals(yEnumerator.Current))
+ {
+ return false;
+ }
+ }
+ }
+ // if y is longer than x
+ if (yEnumerator.MoveNext())
+ {
+ return false;
+ }
+ return true;
+ }
+ }
+
+ return x.Equals(y);
+ }
+
+ int IEqualityComparer<object>.GetHashCode(object obj)
+ {
+ throw new NotImplementedException();
+ }
+
+ bool Equals<T>(object x, object y) where T : IEquatable<T>
+ {
+ IEquatable<T> yEquatable = (IEquatable<T>)y;
+ return yEquatable.Equals((T)x);
+ }
+ }
+
+ // Marked as [Serializable] to check that [DataContract] takes precedence over [Serializable]
+ [DataContract]
+ [Serializable]
+ public class DataContractType : IEquatable<DataContractType>
+ {
+ [DataMember]
+ public string s;
+
+ [DataMember]
+ internal int i;
+
+ public string NotAMember;
+
+ public bool Equals(DataContractType other)
+ {
+ return this.s == other.s && this.i == other.i;
+ }
+ }
+
+ public class POCOType : IEquatable<POCOType>
+ {
+ public string s;
+ internal string t;
+
+ public bool Equals(POCOType other)
+ {
+ return this.s == other.s;
+ }
+ }
+
+ public class MyList<T> : ICollection<T>
+ {
+ List<T> innerList = new List<T>();
+
+ public IEnumerator<T> GetEnumerator()
+ {
+ return innerList.GetEnumerator();
+ }
+
+ public void Add(T item)
+ {
+ innerList.Add(item);
+ }
+
+ public void Clear()
+ {
+ innerList.Clear();
+ }
+
+ public bool Contains(T item)
+ {
+ return innerList.Contains(item);
+ }
+
+ public void CopyTo(T[] array, int arrayIndex)
+ {
+ innerList.CopyTo(array, arrayIndex);
+ }
+
+ public int Count
+ {
+ get { return innerList.Count; }
+ }
+
+ public bool IsReadOnly
+ {
+ get { return ((ICollection<T>)innerList).IsReadOnly; }
+ }
+
+ public bool Remove(T item)
+ {
+ return innerList.Remove(item);
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return innerList.GetEnumerator();
+ }
+
+ IEnumerator<T> IEnumerable<T>.GetEnumerator()
+ {
+ return innerList.GetEnumerator();
+ }
+ }
+
+ [Serializable]
+ class SerializableType : IEquatable<SerializableType>
+ {
+ public SerializableType(string protectedFieldValue)
+ {
+ this.protectedField = protectedFieldValue;
+ }
+
+ public string publicField;
+ internal string internalField;
+ protected internal string protectedInternalField;
+ protected string protectedField;
+ private string privateField;
+
+ public string PublicProperty
+ {
+ get
+ {
+ return privateField;
+ }
+ set
+ {
+ this.privateField = value;
+ }
+ }
+
+ [NonSerialized]
+ public string nonSerializedField;
+
+ public bool Equals(SerializableType other)
+ {
+ return this.publicField == other.publicField &&
+ this.internalField == other.internalField &&
+ this.protectedInternalField == other.protectedInternalField &&
+ this.protectedField == other.protectedField &&
+ this.privateField == other.privateField;
+ }
+ }
+
+ public struct Point : IEquatable<Point>
+ {
+ public int x;
+ public int Y { get; set; }
+
+ public bool Equals(Point other)
+ {
+ return this.x == other.x && this.Y == other.Y;
+ }
+ }
+
+ [DataContract(IsReference = true)]
+ public class Ref
+ {
+ [DataMember]
+ public Ref Reference;
+ }
+
+ public class Base
+ {
+
+ }
+
+ public class Derived : Base
+ {
+
+ }
+
+ [DataContract]
+ public class TypeWithCallbacks
+ {
+ public string callbackOrder = "";
+
+ [OnSerializing]
+ public void OnSerializing(StreamingContext c)
+ {
+ callbackOrder += "1";
+ }
+
+ [OnSerialized]
+ public void OnSerialized(StreamingContext c)
+ {
+ callbackOrder += "2";
+ }
+
+ [OnDeserializing]
+ public void OnDeserializing(StreamingContext c)
+ {
+ callbackOrder += "3";
+ }
+
+ [OnDeserialized]
+ public void OnDeserialized(StreamingContext c)
+ {
+ callbackOrder += "4";
+ }
+ }
+
+ public class DangerousType
+ {
+
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Integration/JsonValueRoundTripComparer.cs b/test/System.Net.Http.Formatting.Test.Integration/JsonValueRoundTripComparer.cs
new file mode 100644
index 00000000..8a0fa2e2
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Integration/JsonValueRoundTripComparer.cs
@@ -0,0 +1,83 @@
+using System.Collections.Generic;
+using System.Json;
+using System.Net.Http.Internal;
+
+namespace System.Net.Http.Formatting
+{
+ internal class JsonValueRoundTripComparer
+ {
+ public static bool Compare(JsonValue initValue, JsonValue newValue)
+ {
+ if (initValue == null && newValue == null)
+ {
+ return true;
+ }
+
+ if (initValue == null || newValue == null)
+ {
+ return false;
+ }
+
+ if (initValue is JsonPrimitive)
+ {
+ string initStr;
+ if (initValue.JsonType == JsonType.String)
+ {
+ initStr = initValue.ToString();
+ }
+ else
+ {
+ initStr = String.Format("\"{0}\"", ((JsonPrimitive)initValue).Value.ToString());
+ }
+
+ string newStr;
+ if (newValue is JsonPrimitive)
+ {
+ newStr = newValue.ToString();
+ initStr = UriQueryUtility.UrlDecode(UriQueryUtility.UrlEncode(initStr));
+ return initStr.Equals(newStr);
+ }
+ else if (newValue is JsonObject && newValue.Count == 1)
+ {
+ initStr = String.Format("{0}", initValue.ToString());
+ return ((JsonObject)newValue).Keys.Contains(initStr);
+ }
+
+ return false;
+ }
+
+ if (initValue.Count != newValue.Count)
+ {
+ return false;
+ }
+
+ if (initValue is JsonObject && newValue is JsonObject)
+ {
+ foreach (KeyValuePair<string, JsonValue> item in initValue)
+ {
+ if (!Compare(item.Value, newValue[item.Key]))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ if (initValue is JsonArray && newValue is JsonArray)
+ {
+ for (int i = 0; i < initValue.Count; i++)
+ {
+ if (!Compare(initValue[i], newValue[i]))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Integration/Properties/AssemblyInfo.cs b/test/System.Net.Http.Formatting.Test.Integration/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..34e306e0
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Integration/Properties/AssemblyInfo.cs
@@ -0,0 +1,34 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("System.Net.Http.Formatting.Test.Integration")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("Microsoft")]
+[assembly: AssemblyProduct("System.Net.Http.Formatting.Test.Integration")]
+[assembly: AssemblyCopyright("Copyright © Microsoft 2011")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("c858cf22-d435-4996-ba01-fa63ebe12a47")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/test/System.Net.Http.Formatting.Test.Integration/System.Net.Http.Formatting.Test.Integration.csproj b/test/System.Net.Http.Formatting.Test.Integration/System.Net.Http.Formatting.Test.Integration.csproj
new file mode 100644
index 00000000..d4c75d3d
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Integration/System.Net.Http.Formatting.Test.Integration.csproj
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProductVersion>9.0.30729</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{6C18CC83-1E4C-42D2-B93E-55D6C363850C}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>System.Net.Http.Formatting</RootNamespace>
+ <AssemblyName>System.Net.Http.Formatting.Test.Integration</AssemblyName>
+ <FileAlignment>512</FileAlignment>
+ <ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Debug\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Release\Test\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'CodeCoverage' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\CodeCoverage\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="Newtonsoft.Json, Version=4.0.8.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>..\..\packages\Newtonsoft.Json.4.0.8\lib\net40\Newtonsoft.Json.dll</HintPath>
+ </Reference>
+ <Reference Include="System" />
+ <Reference Include="System.Net.Http">
+ <HintPath>..\..\packages\Microsoft.Net.Http.2.0.20302.1\lib\net40\System.Net.Http.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Net.Http.WebRequest">
+ <HintPath>..\..\packages\Microsoft.Net.Http.2.0.20302.1\lib\net40\System.Net.Http.WebRequest.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Runtime.Serialization" />
+ <Reference Include="xunit, Version=1.9.0.1566, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL">
+ <HintPath>..\..\packages\xunit.1.9.0.1566\lib\xunit.dll</HintPath>
+ </Reference>
+ <Reference Include="xunit.extensions">
+ <HintPath>..\..\packages\xunit.extensions.1.9.0.1566\lib\xunit.extensions.dll</HintPath>
+ </Reference>
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="JsonNetSerializationTest.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="FormUrlEncodedFromContentTests.cs" />
+ <Compile Include="FormUrlEncodedFromUriQueryTests.cs" />
+ <Compile Include="JsonValueRoundTripComparer.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\System.Json\System.Json.csproj">
+ <Project>{F0441BE9-BDC0-4629-BE5A-8765FFAA2481}</Project>
+ <Name>System.Json</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\src\System.Net.Http.Formatting\System.Net.Http.Formatting.csproj">
+ <Project>{668E9021-CE84-49D9-98FB-DF125A9FCDB0}</Project>
+ <Name>System.Net.Http.Formatting</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\Microsoft.TestCommon\Microsoft.TestCommon.csproj">
+ <Project>{FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0}</Project>
+ <Name>Microsoft.TestCommon</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\System.Net.Http.Formatting.Test.Unit\System.Net.Http.Formatting.Test.Unit.csproj">
+ <Project>{7AF77741-9158-4D5F-8782-8F21FADF025F}</Project>
+ <Name>System.Net.Http.Formatting.Test.Unit</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\System.Json.Test.Integration\System.Json.Test.Integration.csproj">
+ <Project>{A7B1264E-BCE5-42A8-8B5E-001A5360B128}</Project>
+ <Name>System.Json.Test.Integration</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="packages.config" />
+ </ItemGroup>
+ <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+</Project> \ No newline at end of file
diff --git a/test/System.Net.Http.Formatting.Test.Integration/packages.config b/test/System.Net.Http.Formatting.Test.Integration/packages.config
new file mode 100644
index 00000000..2ed4df49
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Integration/packages.config
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="Microsoft.Net.Http" version="2.0.20302.1" />
+ <package id="Newtonsoft.Json" version="4.0.8" />
+ <package id="xunit" version="1.9.0.1566" />
+ <package id="xunit.extensions" version="1.9.0.1566" />
+</packages> \ No newline at end of file
diff --git a/test/System.Net.Http.Formatting.Test.Unit/ContentDispositionHeaderValueExtensionsTests.cs b/test/System.Net.Http.Formatting.Test.Unit/ContentDispositionHeaderValueExtensionsTests.cs
new file mode 100644
index 00000000..c714bf93
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/ContentDispositionHeaderValueExtensionsTests.cs
@@ -0,0 +1,48 @@
+using System.Net.Http.Headers;
+using Microsoft.TestCommon;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Net.Http
+{
+ public class ContentDispositionHeaderValueExtensionsTests
+ {
+ [Fact]
+ public void TypeIsCorrect()
+ {
+ Assert.Type.HasProperties(typeof(ContentDispositionHeaderValueExtensions), TypeAssert.TypeProperties.IsClass | TypeAssert.TypeProperties.IsStatic);
+ }
+
+ [Fact]
+ public void ExtractLocalFileNameThrowsOnNull()
+ {
+ ContentDispositionHeaderValue test = null;
+ Assert.ThrowsArgumentNull(() => ContentDispositionHeaderValueExtensions.ExtractLocalFileName(test), "contentDisposition");
+ }
+
+ [Theory]
+ [TestDataSet(typeof(CommonUnitTestDataSets), "NonNullEmptyStrings")]
+ public void ExtractLocalFileNameThrowsOnQuotedEmpty(string empty)
+ {
+ Assert.ThrowsArgument(
+ () =>
+ {
+ ContentDispositionHeaderValue contentDisposition = null;
+ ContentDispositionHeaderValue.TryParse(String.Format("formdata; filename=\"{0}\"", empty), out contentDisposition);
+ Assert.NotNull(contentDisposition.FileName);
+ ContentDispositionHeaderValueExtensions.ExtractLocalFileName(contentDisposition);
+ }, "contentDisposition");
+ }
+
+ [Fact]
+ public void ExtractLocalFileNamePicksFileNameStarOverFilename()
+ {
+ // ExtractLocalFileName picks filename* over filename.
+ ContentDispositionHeaderValue contentDisposition = null;
+ ContentDispositionHeaderValue.TryParse("formdata; filename=\"aaa\"; filename*=utf-8''%e2BBB", out contentDisposition);
+ string localFilename = ContentDispositionHeaderValueExtensions.ExtractLocalFileName(contentDisposition);
+ Assert.Equal("�BBB", localFilename);
+ }
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/DataSets/HttpUnitTestDataSets.cs b/test/System.Net.Http.Formatting.Test.Unit/DataSets/HttpUnitTestDataSets.cs
new file mode 100644
index 00000000..ed97defa
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/DataSets/HttpUnitTestDataSets.cs
@@ -0,0 +1,103 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Net.Http.Formatting.DataSets.Types;
+using System.Net.Http.Headers;
+using Microsoft.TestCommon;
+
+namespace System.Net.Http.Formatting.DataSets
+{
+ public class HttpUnitTestDataSets
+ {
+ public static TestData<HttpMethod> AllHttpMethods { get { return HttpTestData.AllHttpMethods; } }
+
+ public static TestData<HttpMethod> StandardHttpMethods { get { return HttpTestData.StandardHttpMethods; } }
+
+ public static TestData<HttpMethod> CustomHttpMethods { get { return HttpTestData.CustomHttpMethods; } }
+
+ public static TestData<HttpStatusCode> AllHttpStatusCodes { get { return HttpTestData.AllHttpStatusCodes; } }
+
+ public static TestData<HttpStatusCode> CustomHttpStatusCodes { get { return HttpTestData.CustomHttpStatusCodes; } }
+
+ public static ReadOnlyCollection<TestData> ConvertablePrimitiveValueTypes { get { return HttpTestData.ConvertablePrimitiveValueTypes; } }
+
+ public static ReadOnlyCollection<TestData> ConvertableEnumTypes { get { return HttpTestData.ConvertableEnumTypes; } }
+
+ public static ReadOnlyCollection<TestData> ConvertableValueTypes { get { return HttpTestData.ConvertableValueTypes; } }
+
+ public static TestData<MediaTypeHeaderValue> StandardJsonMediaTypes { get { return HttpTestData.StandardJsonMediaTypes; } }
+
+ public static TestData<MediaTypeHeaderValue> StandardXmlMediaTypes { get { return HttpTestData.StandardXmlMediaTypes; } }
+
+ public static TestData<MediaTypeHeaderValue> StandardODataMediaTypes { get { return HttpTestData.StandardODataMediaTypes; } }
+
+ public static TestData<MediaTypeHeaderValue> StandardFormUrlEncodedMediaTypes { get { return HttpTestData.StandardFormUrlEncodedMediaTypes; } }
+
+ public static TestData<MediaTypeWithQualityHeaderValue> StandardMediaTypesWithQuality { get { return HttpTestData.StandardMediaTypesWithQuality; } }
+
+ public static TestData<string> StandardJsonMediaTypeStrings { get { return HttpTestData.StandardXmlMediaTypeStrings; } }
+
+ public static TestData<string> StandardXmlMediaTypeStrings { get { return HttpTestData.StandardXmlMediaTypeStrings; } }
+
+ public static TestData<string> LegalMediaTypeStrings { get { return HttpTestData.LegalMediaTypeStrings; } }
+
+ public static TestData<string> IllegalMediaTypeStrings { get { return HttpTestData.IllegalMediaTypeStrings; } }
+
+ public static TestData<MediaTypeHeaderValue> LegalMediaTypeHeaderValues { get { return HttpTestData.LegalMediaTypeHeaderValues; } }
+
+ public static TestData<HttpContent> StandardHttpContents { get { return HttpTestData.StandardHttpContents; } }
+
+ public static TestData<MediaTypeMapping> StandardMediaTypeMappings { get { return HttpTestData.StandardMediaTypeMappings; } }
+
+ public static TestData<QueryStringMapping> QueryStringMappings { get { return HttpTestData.QueryStringMappings; } }
+
+ public static TestData<UriPathExtensionMapping> UriPathExtensionMappings { get { return HttpTestData.UriPathExtensionMappings; } }
+
+ public static TestData<MediaRangeMapping> MediaRangeMappings { get { return HttpTestData.MediaRangeMappings; } }
+
+ public static TestData<string> LegalUriPathExtensions { get { return HttpTestData.LegalUriPathExtensions; } }
+
+ public static TestData<string> LegalQueryStringParameterNames { get { return HttpTestData.LegalQueryStringParameterNames; } }
+
+ public static TestData<string> LegalQueryStringParameterValues { get { return HttpTestData.LegalQueryStringParameterValues; } }
+
+ public static TestData<string> LegalHttpHeaderNames { get { return HttpTestData.LegalHttpHeaderNames; } }
+
+ public static TestData<string> LegalHttpHeaderValues { get { return HttpTestData.LegalHttpHeaderValues; } }
+
+ public static TestData<string> LegalMediaRangeStrings { get { return HttpTestData.LegalMediaRangeStrings; } }
+
+ public static TestData<MediaTypeHeaderValue> LegalMediaRangeValues { get { return HttpTestData.LegalMediaRangeValues; } }
+
+ public static TestData<MediaTypeWithQualityHeaderValue> MediaRangeValuesWithQuality { get { return HttpTestData.MediaRangeValuesWithQuality; } }
+
+ public static TestData<string> IllegalMediaRangeStrings { get { return HttpTestData.IllegalMediaRangeStrings; } }
+
+ public static TestData<MediaTypeHeaderValue> IllegalMediaRangeValues { get { return HttpTestData.IllegalMediaRangeValues; } }
+
+ public static TestData<MediaTypeFormatter> StandardFormatters { get { return HttpTestData.StandardFormatters; } }
+
+ public static TestData<Type> StandardFormatterTypes { get { return HttpTestData.StandardFormatterTypes; } }
+
+ public static TestData<MediaTypeFormatter> DerivedFormatters { get { return HttpTestData.DerivedFormatters; } }
+
+ public static TestData<IEnumerable<MediaTypeFormatter>> AllFormatterCollections { get { return HttpTestData.AllFormatterCollections; } }
+
+ public static TestData<string> LegalHttpAddresses { get { return HttpTestData.LegalHttpAddresses; } }
+
+ public static TestData<string> AddressesWithIllegalSchemes { get { return HttpTestData.AddressesWithIllegalSchemes; } }
+
+ public static TestData<HttpRequestMessage> NullContentHttpRequestMessages { get { return HttpTestData.NullContentHttpRequestMessages; } }
+
+ public static ReadOnlyCollection<TestData> RepresentativeValueAndRefTypeTestDataCollection { get { return HttpTestData.RepresentativeValueAndRefTypeTestDataCollection; } }
+
+ public static TestData<string> LegalHttpParameterNames { get { return HttpTestData.LegalHttpParameterNames; } }
+
+ public static TestData<Type> LegalHttpParameterTypes { get { return HttpTestData.LegalHttpParameterTypes; } }
+
+ public static RefTypeTestData<Uri> Uris { get { return HttpTestData.UriTestData; } }
+
+ public static RefTypeTestData<string> UriStrings { get { return HttpTestData.UriTestDataStrings; } }
+
+ public static RefTypeTestData<WcfPocoType> PocoTypesWithNull { get { return HttpTestData.WcfPocoTypeTestDataWithNull; } }
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/DataContractEnum.cs b/test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/DataContractEnum.cs
new file mode 100644
index 00000000..853544d5
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/DataContractEnum.cs
@@ -0,0 +1,16 @@
+using System.Runtime.Serialization;
+
+namespace System.Net.Http.Formatting.DataSets.Types
+{
+ [DataContract]
+ public enum DataContractEnum
+ {
+ [EnumMember]
+ First,
+
+ [EnumMember]
+ Second,
+
+ Third
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/DataContractType.cs b/test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/DataContractType.cs
new file mode 100644
index 00000000..39dbdd7c
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/DataContractType.cs
@@ -0,0 +1,75 @@
+using System.Collections.Generic;
+using System.Runtime.Serialization;
+using System.Xml.Serialization;
+using Microsoft.TestCommon.Types;
+
+namespace System.Net.Http.Formatting.DataSets.Types
+{
+ [DataContract]
+ [KnownType(typeof(DerivedDataContractType))]
+ [XmlInclude(typeof(DerivedDataContractType))]
+ public class DataContractType : INameAndIdContainer
+ {
+ private int id;
+ private string name;
+
+ public DataContractType()
+ {
+ }
+
+ public DataContractType(int id, string name)
+ {
+ this.id = id;
+ this.name = name;
+ }
+
+ [DataMember]
+ public int Id
+ {
+ get
+ {
+ return this.id;
+ }
+
+ set
+ {
+ this.IdSet = true;
+ this.id = value;
+ }
+ }
+
+ [DataMember]
+ public string Name
+ {
+ get
+ {
+ return this.name;
+ }
+
+ set
+ {
+ this.NameSet = true;
+ this.name = value;
+ }
+ }
+
+ [XmlIgnore]
+ public bool IdSet { get; private set; }
+
+ [XmlIgnore]
+ public bool NameSet { get; private set; }
+
+ public static IEnumerable<DataContractType> GetTestData()
+ {
+ return new DataContractType[] { new DataContractType(), new DataContractType(1, "SomeName") };
+ }
+
+ public static IEnumerable<DerivedDataContractType> GetDerivedTypeTestData()
+ {
+ return new DerivedDataContractType[] {
+ new DerivedDataContractType(),
+ new DerivedDataContractType(1, "SomeName", null),
+ new DerivedDataContractType(1, "SomeName", new WcfPocoType(2, "SomeOtherName"))};
+ }
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/DerivedDataContractType.cs b/test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/DerivedDataContractType.cs
new file mode 100644
index 00000000..0ca4857a
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/DerivedDataContractType.cs
@@ -0,0 +1,59 @@
+using System.Collections.Generic;
+using System.Runtime.Serialization;
+using System.Xml.Serialization;
+
+namespace System.Net.Http.Formatting.DataSets.Types
+{
+ [DataContract]
+ [KnownType(typeof(DerivedWcfPocoType))]
+ [KnownType(typeof(DerivedDataContractType))]
+ [XmlInclude(typeof(DerivedWcfPocoType))]
+ [XmlInclude(typeof(DerivedDataContractType))]
+ public class DerivedDataContractType : DataContractType
+ {
+ private WcfPocoType reference;
+
+ public DerivedDataContractType()
+ {
+ }
+
+ public DerivedDataContractType(int id, string name, WcfPocoType reference)
+ : base(id, name)
+ {
+ this.reference = reference;
+ }
+
+ [DataMember]
+ public WcfPocoType Reference
+ {
+ get
+ {
+ return this.reference;
+ }
+
+ set
+ {
+ this.ReferenceSet = true;
+ this.reference = value;
+ }
+ }
+
+ [XmlIgnore]
+ public bool ReferenceSet { get; private set; }
+
+ public static new IEnumerable<DerivedDataContractType> GetTestData()
+ {
+ return new DerivedDataContractType[] {
+ new DerivedDataContractType(),
+ new DerivedDataContractType(1, "SomeName", new WcfPocoType(2, "SomeOtherName")) };
+ }
+
+ public static IEnumerable<DerivedDataContractType> GetKnownTypeTestData()
+ {
+ return new DerivedDataContractType[] {
+ new DerivedDataContractType(),
+ new DerivedDataContractType(1, "SomeName", null),
+ new DerivedDataContractType(1, "SomeName", new DerivedWcfPocoType(2, "SomeOtherName", null))};
+ }
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/DerivedFormUrlEncodedMediaTypeFormatter.cs b/test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/DerivedFormUrlEncodedMediaTypeFormatter.cs
new file mode 100644
index 00000000..2b7f80b8
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/DerivedFormUrlEncodedMediaTypeFormatter.cs
@@ -0,0 +1,7 @@
+
+namespace System.Net.Http.Formatting.DataSets.Types
+{
+ public class DerivedFormUrlEncodedMediaTypeFormatter : FormUrlEncodedMediaTypeFormatter
+ {
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/DerivedJsonMediaTypeFormatter.cs b/test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/DerivedJsonMediaTypeFormatter.cs
new file mode 100644
index 00000000..846635a9
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/DerivedJsonMediaTypeFormatter.cs
@@ -0,0 +1,7 @@
+
+namespace System.Net.Http.Formatting.DataSets.Types
+{
+ public class DerivedJsonMediaTypeFormatter : JsonMediaTypeFormatter
+ {
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/DerivedWcfPocoType.cs b/test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/DerivedWcfPocoType.cs
new file mode 100644
index 00000000..602d2cf2
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/DerivedWcfPocoType.cs
@@ -0,0 +1,38 @@
+using System.Runtime.Serialization;
+using System.Xml.Serialization;
+
+namespace System.Net.Http.Formatting.DataSets.Types
+{
+ public class DerivedWcfPocoType : WcfPocoType
+ {
+ private WcfPocoType reference;
+
+ public DerivedWcfPocoType()
+ {
+ }
+
+ public DerivedWcfPocoType(int id, string name, WcfPocoType reference)
+ : base(id, name)
+ {
+ this.reference = reference;
+ }
+
+ public WcfPocoType Reference
+ {
+ get
+ {
+ return this.reference;
+ }
+
+ set
+ {
+ this.ReferenceSet = true;
+ this.reference = value;
+ }
+ }
+
+ [IgnoreDataMember]
+ [XmlIgnore]
+ public bool ReferenceSet { get; private set; }
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/DerivedXmlMediaTypeFormatter.cs b/test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/DerivedXmlMediaTypeFormatter.cs
new file mode 100644
index 00000000..acbb4b7f
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/DerivedXmlMediaTypeFormatter.cs
@@ -0,0 +1,7 @@
+
+namespace System.Net.Http.Formatting.DataSets.Types
+{
+ public class DerivedXmlMediaTypeFormatter : XmlMediaTypeFormatter
+ {
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/DerivedXmlSerializableType.cs b/test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/DerivedXmlSerializableType.cs
new file mode 100644
index 00000000..a8847770
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/DerivedXmlSerializableType.cs
@@ -0,0 +1,56 @@
+using System.Collections.Generic;
+using System.Runtime.Serialization;
+using System.Xml.Serialization;
+
+namespace System.Net.Http.Formatting.DataSets.Types
+{
+ [KnownType(typeof(DerivedWcfPocoType))]
+ [XmlInclude(typeof(DerivedWcfPocoType))]
+ public class DerivedXmlSerializableType : XmlSerializableType, INotJsonSerializable
+ {
+ private WcfPocoType reference;
+
+ public DerivedXmlSerializableType()
+ {
+ }
+
+ public DerivedXmlSerializableType(int id, string name, WcfPocoType reference)
+ : base(id, name)
+ {
+ this.reference = reference;
+ }
+
+ [XmlElement]
+ public WcfPocoType Reference
+ {
+ get
+ {
+ return this.reference;
+ }
+
+ set
+ {
+ this.ReferenceSet = true;
+ this.reference = value;
+ }
+ }
+
+ [XmlIgnore]
+ public bool ReferenceSet { get; private set; }
+
+ public static new IEnumerable<DerivedXmlSerializableType> GetTestData()
+ {
+ return new DerivedXmlSerializableType[] {
+ new DerivedXmlSerializableType(),
+ new DerivedXmlSerializableType(1, "SomeName", new WcfPocoType(2, "SomeOtherName")) };
+ }
+
+ public static IEnumerable<DerivedXmlSerializableType> GetKnownTypeTestData()
+ {
+ return new DerivedXmlSerializableType[] {
+ new DerivedXmlSerializableType(),
+ new DerivedXmlSerializableType(1, "SomeName", null),
+ new DerivedXmlSerializableType(1, "SomeName", new DerivedWcfPocoType(2, "SomeOtherName", null))};
+ }
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/HttpTestData.cs b/test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/HttpTestData.cs
new file mode 100644
index 00000000..9a7c4776
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/HttpTestData.cs
@@ -0,0 +1,427 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.IO;
+using System.Linq;
+using System.Net.Http.Headers;
+using Microsoft.TestCommon;
+
+namespace System.Net.Http.Formatting.DataSets.Types
+{
+ public static class HttpTestData
+ {
+ public static readonly TestData<HttpMethod> AllHttpMethods = new RefTypeTestData<HttpMethod>(() =>
+ StandardHttpMethods.Concat(CustomHttpMethods).ToList());
+
+ public static readonly TestData<HttpMethod> StandardHttpMethods = new RefTypeTestData<HttpMethod>(() => new List<HttpMethod>()
+ {
+ HttpMethod.Head,
+ HttpMethod.Get,
+ HttpMethod.Post,
+ HttpMethod.Put,
+ HttpMethod.Delete,
+ HttpMethod.Options,
+ HttpMethod.Trace,
+ });
+
+ public static readonly TestData<HttpMethod> CustomHttpMethods = new RefTypeTestData<HttpMethod>(() => new List<HttpMethod>()
+ {
+ new HttpMethod("Custom")
+ });
+
+ public static readonly TestData<HttpStatusCode> AllHttpStatusCodes = new ValueTypeTestData<HttpStatusCode>(new HttpStatusCode[]
+ {
+ HttpStatusCode.Accepted,
+ HttpStatusCode.Ambiguous,
+ HttpStatusCode.BadGateway,
+ HttpStatusCode.BadRequest,
+ HttpStatusCode.Conflict,
+ HttpStatusCode.Continue,
+ HttpStatusCode.Created,
+ HttpStatusCode.ExpectationFailed,
+ HttpStatusCode.Forbidden,
+ HttpStatusCode.Found,
+ HttpStatusCode.GatewayTimeout,
+ HttpStatusCode.Gone,
+ HttpStatusCode.HttpVersionNotSupported,
+ HttpStatusCode.InternalServerError,
+ HttpStatusCode.LengthRequired,
+ HttpStatusCode.MethodNotAllowed,
+ HttpStatusCode.Moved,
+ HttpStatusCode.MovedPermanently,
+ HttpStatusCode.MultipleChoices,
+ HttpStatusCode.NoContent,
+ HttpStatusCode.NonAuthoritativeInformation,
+ HttpStatusCode.NotAcceptable,
+ HttpStatusCode.NotFound,
+ HttpStatusCode.NotImplemented,
+ HttpStatusCode.NotModified,
+ HttpStatusCode.OK,
+ HttpStatusCode.PartialContent,
+ HttpStatusCode.PaymentRequired,
+ HttpStatusCode.PreconditionFailed,
+ HttpStatusCode.ProxyAuthenticationRequired,
+ HttpStatusCode.Redirect,
+ HttpStatusCode.RedirectKeepVerb,
+ HttpStatusCode.RedirectMethod,
+ HttpStatusCode.RequestedRangeNotSatisfiable,
+ HttpStatusCode.RequestEntityTooLarge,
+ HttpStatusCode.RequestTimeout,
+ HttpStatusCode.RequestUriTooLong,
+ HttpStatusCode.ResetContent,
+ HttpStatusCode.SeeOther,
+ HttpStatusCode.ServiceUnavailable,
+ HttpStatusCode.SwitchingProtocols,
+ HttpStatusCode.TemporaryRedirect,
+ HttpStatusCode.Unauthorized,
+ HttpStatusCode.UnsupportedMediaType,
+ HttpStatusCode.Unused,
+ HttpStatusCode.UseProxy
+ });
+
+ public static readonly TestData<HttpStatusCode> CustomHttpStatusCodes = new ValueTypeTestData<HttpStatusCode>(new HttpStatusCode[]
+ {
+ (HttpStatusCode)199,
+ (HttpStatusCode)299,
+ (HttpStatusCode)399,
+ (HttpStatusCode)499,
+ (HttpStatusCode)599,
+ (HttpStatusCode)699,
+ (HttpStatusCode)799,
+ (HttpStatusCode)899,
+ (HttpStatusCode)999,
+ });
+
+ public static readonly ReadOnlyCollection<TestData> ConvertablePrimitiveValueTypes = new ReadOnlyCollection<TestData>(new TestData[] {
+ TestData.CharTestData,
+ TestData.IntTestData,
+ TestData.UintTestData,
+ TestData.ShortTestData,
+ TestData.UshortTestData,
+ TestData.LongTestData,
+ TestData.UlongTestData,
+ TestData.ByteTestData,
+ TestData.SByteTestData,
+ TestData.BoolTestData,
+ TestData.DoubleTestData,
+ TestData.FloatTestData,
+ TestData.DecimalTestData,
+ TestData.TimeSpanTestData,
+ TestData.GuidTestData,
+ TestData.DateTimeTestData,
+ TestData.DateTimeOffsetTestData});
+
+ public static readonly ReadOnlyCollection<TestData> ConvertableEnumTypes = new ReadOnlyCollection<TestData>(new TestData[] {
+ TestData.SimpleEnumTestData,
+ TestData.LongEnumTestData,
+ TestData.FlagsEnumTestData,
+ DataContractEnumTestData});
+
+ public static readonly ReadOnlyCollection<TestData> ConvertableValueTypes = new ReadOnlyCollection<TestData>(
+ ConvertablePrimitiveValueTypes.Concat(ConvertableEnumTypes).ToList());
+
+ public static readonly TestData<MediaTypeHeaderValue> StandardJsonMediaTypes = new RefTypeTestData<MediaTypeHeaderValue>(() => new List<MediaTypeHeaderValue>()
+ {
+ new MediaTypeHeaderValue("application/json"),
+ new MediaTypeHeaderValue("text/json")
+ });
+
+ public static readonly TestData<MediaTypeHeaderValue> StandardXmlMediaTypes = new RefTypeTestData<MediaTypeHeaderValue>(() => new List<MediaTypeHeaderValue>()
+ {
+ new MediaTypeHeaderValue("application/xml"),
+ new MediaTypeHeaderValue("text/xml")
+ });
+
+ public static readonly TestData<MediaTypeHeaderValue> StandardODataMediaTypes = new RefTypeTestData<MediaTypeHeaderValue>(() => new List<MediaTypeHeaderValue>()
+ {
+ new MediaTypeHeaderValue("application/atom+xml"),
+ new MediaTypeHeaderValue("application/json"),
+ });
+
+ public static readonly TestData<MediaTypeHeaderValue> StandardFormUrlEncodedMediaTypes = new RefTypeTestData<MediaTypeHeaderValue>(() => new List<MediaTypeHeaderValue>()
+ {
+ new MediaTypeHeaderValue("application/x-www-form-urlencoded")
+ });
+
+ public static readonly TestData<string> StandardJsonMediaTypeStrings = new RefTypeTestData<string>(() => new List<string>()
+ {
+ "application/json",
+ "text/json"
+ });
+
+ public static readonly TestData<string> StandardXmlMediaTypeStrings = new RefTypeTestData<string>(() => new List<string>()
+ {
+ "application/xml",
+ "text/xml"
+ });
+
+ public static readonly TestData<string> LegalMediaTypeStrings = new RefTypeTestData<string>(() =>
+ StandardXmlMediaTypeStrings.Concat(StandardJsonMediaTypeStrings).ToList());
+
+
+ // Illegal media type strings. These will cause the MediaTypeHeaderValue ctor to throw FormatException
+ public static readonly TestData<string> IllegalMediaTypeStrings = new RefTypeTestData<string>(() => new List<string>()
+ {
+ "\0",
+ "9\r\n"
+ });
+
+ //// TODO: complete this list
+ // Legal MediaTypeHeaderValues
+ public static readonly TestData<MediaTypeHeaderValue> LegalMediaTypeHeaderValues = new RefTypeTestData<MediaTypeHeaderValue>(
+ () => LegalMediaTypeStrings.Select<string, MediaTypeHeaderValue>((mediaType) => new MediaTypeHeaderValue(mediaType)).ToList());
+
+ public static readonly TestData<MediaTypeWithQualityHeaderValue> StandardMediaTypesWithQuality = new RefTypeTestData<MediaTypeWithQualityHeaderValue>(() => new List<MediaTypeWithQualityHeaderValue>()
+ {
+ new MediaTypeWithQualityHeaderValue("application/json", .1) { CharSet="utf-8"},
+ new MediaTypeWithQualityHeaderValue("text/json", .2) { CharSet="utf-8"},
+ new MediaTypeWithQualityHeaderValue("application/xml", .3) { CharSet="utf-8"},
+ new MediaTypeWithQualityHeaderValue("text/xml", .4) { CharSet="utf-8"},
+ new MediaTypeWithQualityHeaderValue("application/atom+xml", .5) { CharSet="utf-8"},
+ });
+
+ public static readonly TestData<HttpContent> StandardHttpContents = new RefTypeTestData<HttpContent>(() => new List<HttpContent>()
+ {
+ new ByteArrayContent(new byte[0]),
+ new FormUrlEncodedContent(new KeyValuePair<string, string>[0]),
+ new MultipartContent(),
+ new StringContent(""),
+ new StreamContent(new MemoryStream())
+ });
+
+ //// TODO: make this list compose from other data?
+ // Collection of legal instances of all standard MediaTypeMapping types
+ public static readonly TestData<MediaTypeMapping> StandardMediaTypeMappings = new RefTypeTestData<MediaTypeMapping>(() =>
+ QueryStringMappings.Cast<MediaTypeMapping>().Concat(
+ UriPathExtensionMappings.Cast<MediaTypeMapping>().Concat(
+ MediaRangeMappings.Cast<MediaTypeMapping>())).ToList()
+ );
+
+ public static readonly TestData<QueryStringMapping> QueryStringMappings = new RefTypeTestData<QueryStringMapping>(() => new List<QueryStringMapping>()
+ {
+ new QueryStringMapping("format", "json", new MediaTypeHeaderValue("application/json"))
+ });
+
+ public static readonly TestData<UriPathExtensionMapping> UriPathExtensionMappings = new RefTypeTestData<UriPathExtensionMapping>(() => new List<UriPathExtensionMapping>()
+ {
+ new UriPathExtensionMapping("xml", new MediaTypeHeaderValue("application/xml")),
+ new UriPathExtensionMapping("json", new MediaTypeHeaderValue("application/json")),
+ });
+
+ public static readonly TestData<MediaRangeMapping> MediaRangeMappings = new RefTypeTestData<MediaRangeMapping>(() => new List<MediaRangeMapping>()
+ {
+ new MediaRangeMapping(new MediaTypeHeaderValue("application/*"), new MediaTypeHeaderValue("application/xml"))
+ });
+
+ public static readonly TestData<string> LegalUriPathExtensions = new RefTypeTestData<string>(() => new List<string>()
+ {
+ "xml",
+ "json"
+ });
+
+ public static readonly TestData<string> LegalQueryStringParameterNames = new RefTypeTestData<string>(() => new List<string>()
+ {
+ "format",
+ "fmt"
+ });
+
+ public static readonly TestData<string> LegalHttpHeaderNames = new RefTypeTestData<string>(() => new List<string>()
+ {
+ "x-requested-with",
+ "some-random-name"
+ });
+
+ public static readonly TestData<string> LegalHttpHeaderValues = new RefTypeTestData<string>(() => new List<string>()
+ {
+ "1",
+ "XMLHttpRequest",
+ "\"quoted-string\""
+ });
+
+ public static readonly TestData<string> LegalQueryStringParameterValues = new RefTypeTestData<string>(() => new List<string>()
+ {
+ "xml",
+ "json"
+ });
+
+ public static readonly TestData<string> LegalMediaRangeStrings = new RefTypeTestData<string>(() => new List<string>()
+ {
+ "application/*",
+ "text/*"
+ });
+
+ public static readonly TestData<MediaTypeHeaderValue> LegalMediaRangeValues = new RefTypeTestData<MediaTypeHeaderValue>(() =>
+ LegalMediaRangeStrings.Select<string, MediaTypeHeaderValue>((s) => new MediaTypeHeaderValue(s)).ToList()
+ );
+
+ public static readonly TestData<MediaTypeWithQualityHeaderValue> MediaRangeValuesWithQuality = new RefTypeTestData<MediaTypeWithQualityHeaderValue>(() => new List<MediaTypeWithQualityHeaderValue>()
+ {
+ new MediaTypeWithQualityHeaderValue("application/*", .1),
+ new MediaTypeWithQualityHeaderValue("text/*", .2),
+ });
+
+ public static readonly TestData<string> IllegalMediaRangeStrings = new RefTypeTestData<string>(() => new List<string>()
+ {
+ "application/xml",
+ "text/xml"
+ });
+
+ public static readonly TestData<MediaTypeHeaderValue> IllegalMediaRangeValues = new RefTypeTestData<MediaTypeHeaderValue>(() =>
+ IllegalMediaRangeStrings.Select<string, MediaTypeHeaderValue>((s) => new MediaTypeHeaderValue(s)).ToList()
+ );
+
+ public static readonly TestData<MediaTypeFormatter> StandardFormatters = new RefTypeTestData<MediaTypeFormatter>(() => new List<MediaTypeFormatter>()
+ {
+ new XmlMediaTypeFormatter(),
+ new JsonMediaTypeFormatter(),
+ new FormUrlEncodedMediaTypeFormatter()
+ });
+
+ public static readonly TestData<Type> StandardFormatterTypes = new RefTypeTestData<Type>(() =>
+ StandardFormatters.Select<MediaTypeFormatter, Type>((m) => m.GetType()));
+
+ public static readonly TestData<MediaTypeFormatter> DerivedFormatters = new RefTypeTestData<MediaTypeFormatter>(() => new List<MediaTypeFormatter>()
+ {
+ new DerivedXmlMediaTypeFormatter(),
+ new DerivedJsonMediaTypeFormatter(),
+ new DerivedFormUrlEncodedMediaTypeFormatter(),
+ });
+
+ public static readonly TestData<IEnumerable<MediaTypeFormatter>> AllFormatterCollections =
+ new RefTypeTestData<IEnumerable<MediaTypeFormatter>>(() => new List<IEnumerable<MediaTypeFormatter>>()
+ {
+ new MediaTypeFormatter[0],
+ StandardFormatters,
+ DerivedFormatters,
+ });
+
+ public static readonly TestData<string> LegalHttpAddresses = new RefTypeTestData<string>(() => new List<string>()
+ {
+ "http://somehost",
+ "https://somehost",
+ });
+
+ public static readonly TestData<string> AddressesWithIllegalSchemes = new RefTypeTestData<string>(() => new List<string>()
+ {
+ "net.tcp://somehost",
+ "file://somehost",
+ "net.pipe://somehost",
+ "mailto:somehost",
+ "ftp://somehost",
+ "news://somehost",
+ "ws://somehost",
+ "abc://somehost"
+ });
+
+ /// <summary>
+ /// A read-only collection of representative values and reference type test data.
+ /// Uses where exhaustive coverage is not required. It includes null values.
+ /// </summary>
+ public static readonly ReadOnlyCollection<TestData> RepresentativeValueAndRefTypeTestDataCollection = new ReadOnlyCollection<TestData>(new TestData[] {
+ TestData.ByteTestData,
+ TestData.IntTestData,
+ TestData.BoolTestData,
+ TestData.SimpleEnumTestData,
+ TestData.StringTestData,
+ TestData.DateTimeTestData,
+ TestData.DateTimeOffsetTestData,
+ TestData.TimeSpanTestData,
+ WcfPocoTypeTestDataWithNull
+ });
+
+ public static readonly TestData<HttpRequestMessage> NullContentHttpRequestMessages = new RefTypeTestData<HttpRequestMessage>(() => new List<HttpRequestMessage>()
+ {
+ new HttpRequestMessage() { Content = null },
+ });
+
+ public static readonly TestData<string> LegalHttpParameterNames = new RefTypeTestData<string>(() => new List<string>()
+ {
+ "文",
+ "A",
+ "a",
+ "b",
+ " a",
+ "arg1",
+ "arg2",
+ "1",
+ "@",
+ "!"
+
+ });
+
+ public static readonly TestData<Type> LegalHttpParameterTypes = new RefTypeTestData<Type>(() => new List<Type>()
+ {
+ typeof(string),
+ typeof(byte[]),
+ typeof(byte[][]),
+ typeof(byte[][]),
+ typeof(char),
+ typeof(DateTime),
+ typeof(decimal),
+ typeof(double),
+ typeof(Guid),
+ typeof(Int16),
+ typeof(Int32),
+ typeof(object),
+ typeof(sbyte),
+ typeof(Single),
+ typeof(TimeSpan),
+ typeof(UInt16),
+ typeof(UInt32),
+ typeof(UInt64),
+ typeof(Uri),
+ typeof(Enum),
+ typeof(Collection<object>),
+ typeof(IList<object>),
+ typeof(System.Runtime.Serialization.ISerializable),
+ typeof(System.Data.DataSet),
+ typeof(System.Xml.Serialization.IXmlSerializable),
+ typeof(Nullable),
+ typeof(Nullable<DateTime>),
+ typeof(Stream),
+ typeof(HttpRequestMessage),
+ typeof(HttpResponseMessage),
+ typeof(ObjectContent),
+ typeof(ObjectContent<object>),
+ typeof(HttpContent),
+ typeof(Delegate),
+ typeof(Action),
+ typeof(System.Threading.Tasks.Task<object>),
+ typeof(System.Threading.Tasks.Task),
+ typeof(List<dynamic>)
+ });
+
+ /// <summary>
+ /// Common <see cref="TestData"/> for an <c>enum</c> decorated with a <see cref="DataContractAttribute"/>.
+ /// </summary>
+ public static readonly ValueTypeTestData<DataContractEnum> DataContractEnumTestData = new ValueTypeTestData<DataContractEnum>(
+ DataContractEnum.First,
+ DataContractEnum.Second);
+
+ /// <summary>
+ /// Common <see cref="TestData"/> for the string form of a <see cref="Uri"/>.
+ /// </summary>
+ public static readonly RefTypeTestData<string> UriTestDataStrings = new RefTypeTestData<string>(() => new List<string>(){
+ "http://somehost",
+ "http://somehost:8080",
+ "http://somehost/",
+ "http://somehost:8080/",
+ "http://somehost/somepath",
+ "http://somehost/somepath/",
+ "http://somehost/somepath?somequery=somevalue"});
+
+ /// <summary>
+ /// Common <see cref="TestData"/> for a <see cref="Uri"/>.
+ /// </summary>
+ public static readonly RefTypeTestData<Uri> UriTestData = new RefTypeTestData<Uri>(() =>
+ UriTestDataStrings.Select<string, Uri>((s) => new Uri(s)).ToList());
+
+ /// <summary>
+ /// Common <see cref="TestData"/> for a POCO class type that includes null values
+ /// for both the base class and derived classes.
+ /// </summary>
+ public static readonly RefTypeTestData<WcfPocoType> WcfPocoTypeTestDataWithNull = new RefTypeTestData<WcfPocoType>(
+ WcfPocoType.GetTestDataWithNull,
+ WcfPocoType.GetDerivedTypeTestDataWithNull,
+ null);
+ }
+} \ No newline at end of file
diff --git a/test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/INotJsonSerializable.cs b/test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/INotJsonSerializable.cs
new file mode 100644
index 00000000..61d9e64f
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/INotJsonSerializable.cs
@@ -0,0 +1,9 @@
+namespace System.Net.Http.Formatting.DataSets.Types
+{
+ /// <summary>
+ /// Tagging interface to indicate types which we know Json cannot serialize.
+ /// </summary>
+ public interface INotJsonSerializable
+ {
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/WcfPocoType.cs b/test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/WcfPocoType.cs
new file mode 100644
index 00000000..2ced7009
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/WcfPocoType.cs
@@ -0,0 +1,86 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.Serialization;
+using System.Xml.Serialization;
+using Microsoft.TestCommon.Types;
+
+namespace System.Net.Http.Formatting.DataSets.Types
+{
+ [KnownType(typeof(DerivedWcfPocoType))]
+ [XmlInclude(typeof(DerivedWcfPocoType))]
+ public class WcfPocoType : INameAndIdContainer
+ {
+ private int id;
+ private string name;
+
+ public WcfPocoType()
+ {
+ }
+
+ public WcfPocoType(int id, string name)
+ {
+ this.id = id;
+ this.name = name;
+ }
+
+ public int Id
+ {
+ get
+ {
+ return this.id;
+ }
+
+ set
+ {
+ this.IdSet = true;
+ this.id = value;
+ }
+ }
+
+ public string Name
+ {
+ get
+ {
+ return this.name;
+ }
+
+ set
+ {
+ this.NameSet = true;
+ this.name = value;
+ }
+
+ }
+
+ [IgnoreDataMember]
+ [XmlIgnore]
+ public bool IdSet { get; private set; }
+
+ [IgnoreDataMember]
+ [XmlIgnore]
+ public bool NameSet { get; private set; }
+
+ public static IEnumerable<WcfPocoType> GetTestData()
+ {
+ return new WcfPocoType[] { new WcfPocoType(), new WcfPocoType(1, "SomeName") };
+ }
+
+ public static IEnumerable<WcfPocoType> GetTestDataWithNull()
+ {
+ return GetTestData().Concat(new WcfPocoType[] { null });
+ }
+
+ public static IEnumerable<DerivedWcfPocoType> GetDerivedTypeTestData()
+ {
+ return new DerivedWcfPocoType[] {
+ new DerivedWcfPocoType(),
+ new DerivedWcfPocoType(1, "SomeName", null),
+ new DerivedWcfPocoType(1, "SomeName", new WcfPocoType(2, "SomeOtherName"))};
+ }
+
+ public static IEnumerable<DerivedWcfPocoType> GetDerivedTypeTestDataWithNull()
+ {
+ return GetDerivedTypeTestData().Concat(new DerivedWcfPocoType[] { null });
+ }
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/XmlSerializableType.cs b/test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/XmlSerializableType.cs
new file mode 100644
index 00000000..bd4218e4
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/DataSets/Types/XmlSerializableType.cs
@@ -0,0 +1,96 @@
+using System.Collections.Generic;
+using System.Runtime.Serialization;
+using System.Xml.Serialization;
+using Microsoft.TestCommon.Types;
+
+namespace System.Net.Http.Formatting.DataSets.Types
+{
+ [KnownType(typeof(DerivedXmlSerializableType))]
+ [XmlInclude(typeof(DerivedXmlSerializableType))]
+ public class XmlSerializableType : INameAndIdContainer
+ {
+ private int id;
+ private string name;
+
+ public XmlSerializableType()
+ {
+ }
+
+ public XmlSerializableType(int id, string name)
+ {
+ this.id = id;
+ this.name = name;
+ }
+
+ [XmlAttribute]
+ public int Id
+ {
+ get
+ {
+ return this.id;
+ }
+
+ set
+ {
+ this.IdSet = true;
+ this.id = value;
+ }
+ }
+
+ [XmlElement]
+ public string Name
+ {
+ get
+ {
+ return this.name;
+ }
+
+ set
+ {
+ this.NameSet = true;
+ this.name = value;
+ }
+
+ }
+
+ [XmlIgnore]
+ public bool IdSet { get; private set; }
+
+ [XmlIgnore]
+ public bool NameSet { get; private set; }
+
+ public override bool Equals(object obj)
+ {
+ if (Object.ReferenceEquals(this, obj))
+ {
+ return true;
+ }
+
+ XmlSerializableType other = obj as XmlSerializableType;
+ if (Object.ReferenceEquals(other, null))
+ {
+ return false;
+ }
+
+ return String.Equals(this.Name, other.Name, StringComparison.Ordinal) && this.Id == other.Id;
+ }
+
+ public override int GetHashCode()
+ {
+ return base.GetHashCode();
+ }
+
+ public static IEnumerable<XmlSerializableType> GetTestData()
+ {
+ return new XmlSerializableType[] { new XmlSerializableType(), new XmlSerializableType(1, "SomeName") };
+ }
+
+ public static IEnumerable<DerivedXmlSerializableType> GetDerivedTypeTestData()
+ {
+ return new DerivedXmlSerializableType[] {
+ new DerivedXmlSerializableType(),
+ new DerivedXmlSerializableType(1, "SomeName", null),
+ new DerivedXmlSerializableType(1, "SomeName", new WcfPocoType(2, "SomeOtherName"))};
+ }
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/Formatting/BufferedMediaTypeFormatterTests.cs b/test/System.Net.Http.Formatting.Test.Unit/Formatting/BufferedMediaTypeFormatterTests.cs
new file mode 100644
index 00000000..3c17839d
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/Formatting/BufferedMediaTypeFormatterTests.cs
@@ -0,0 +1,95 @@
+using System.IO;
+using System.Net.Http.Headers;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Net.Http.Formatting
+{
+ public class BufferedMediaTypeFormatterTests
+ {
+ private BufferedMediaTypeFormatter _formatter = new TestableBufferedMediaTypeFormatter();
+
+ [Fact]
+ public void WriteToStreamAsync_WhenTypeParameterIsNull_ThrowsException()
+ {
+ Assert.ThrowsArgumentNull(
+ () => _formatter.WriteToStreamAsync(null, new object(), new MemoryStream(), null, null), "type");
+ }
+
+ [Fact]
+ public void WriteToStreamAsync_WhenStreamParameterIsNull_ThrowsException()
+ {
+ Assert.ThrowsArgumentNull(
+ () => _formatter.WriteToStreamAsync(typeof(object), new object(), null, null, null), "stream");
+ }
+
+ [Fact]
+ public void ReadFromStreamAsync_WhenTypeParamterIsNull_ThrowsException()
+ {
+ Assert.ThrowsArgumentNull(() => _formatter.ReadFromStreamAsync(null, new MemoryStream(), null, null), "type");
+ }
+
+ [Fact]
+ public void ReadFromStreamAsync_WhenStreamParamterIsNull_ThrowsException()
+ {
+ Assert.ThrowsArgumentNull(() => _formatter.ReadFromStreamAsync(typeof(object), null, null, null), "stream");
+ }
+
+ [Fact]
+ public void BufferedWrite()
+ {
+ // Arrange. Specifically use the base class with async signatures.
+ MediaTypeFormatter formatter = new TestableBufferedMediaTypeFormatter();
+ MemoryStream stream = new MemoryStream();
+
+ // Act. Call the async signature.
+ string dummyData = "ignored";
+ formatter.WriteToStreamAsync(dummyData.GetType(), dummyData, stream, null, null).Wait();
+
+ // Assert
+ byte[] buffer = stream.GetBuffer();
+ Assert.Equal(123, buffer[0]);
+ }
+
+ [Fact]
+ public void BufferedRead()
+ {
+ // Arrange. Specifically use the base class with async signatures.
+ MediaTypeFormatter formatter = new TestableBufferedMediaTypeFormatter();
+ MemoryStream stream = new MemoryStream();
+ byte data = 45;
+ stream.WriteByte(data);
+ stream.Position = 0;
+
+ // Act. Call the async signature.
+ string dummyData = "ignored";
+ object result = formatter.ReadFromStreamAsync(dummyData.GetType(), stream, null, null).Result;
+
+ // Assert
+ Assert.Equal(data, result);
+ }
+
+ class TestableBufferedMediaTypeFormatter : BufferedMediaTypeFormatter
+ {
+ public override bool CanReadType(Type type)
+ {
+ return true;
+ }
+
+ public override bool CanWriteType(Type type)
+ {
+ return true;
+ }
+ public override object OnReadFromStream(Type type, Stream stream, HttpContentHeaders contentHeaders, IFormatterLogger formatterLogger)
+ {
+ byte data = (byte)stream.ReadByte();
+ return data;
+ }
+
+ public override void OnWriteToStream(Type type, object value, Stream stream, HttpContentHeaders contentHeaders, TransportContext transportContext)
+ {
+ stream.WriteByte(123);
+ }
+ }
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/Formatting/DefaultContentNegotiatorTests.cs b/test/System.Net.Http.Formatting.Test.Unit/Formatting/DefaultContentNegotiatorTests.cs
new file mode 100644
index 00000000..e60e3ea9
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/Formatting/DefaultContentNegotiatorTests.cs
@@ -0,0 +1,220 @@
+using System.Json;
+using System.Linq;
+using System.Net.Http.Formatting.Mocks;
+using System.Net.Http.Headers;
+using Microsoft.TestCommon;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Net.Http.Formatting
+{
+ public class DefaultContentNegotiatorTests
+ {
+ [Fact]
+ public void TypeIsCorrect()
+ {
+ Assert.Type.HasProperties(typeof(DefaultContentNegotiator), TypeAssert.TypeProperties.IsPublicVisibleClass);
+ }
+
+ [Fact]
+ public void Negotiate_WhenTypeParameterIsNull_ThrowsException()
+ {
+ DefaultContentNegotiator selector = new DefaultContentNegotiator();
+ HttpRequestMessage request = new HttpRequestMessage();
+ MediaTypeHeaderValue mediaType;
+
+ Assert.ThrowsArgumentNull(() => selector.Negotiate(null, request, Enumerable.Empty<MediaTypeFormatter>(), out mediaType), "type");
+ }
+
+ [Fact]
+ public void Negotiate_WhenRequestParameterIsNull_ThrowsException()
+ {
+ DefaultContentNegotiator selector = new DefaultContentNegotiator();
+ MediaTypeHeaderValue mediaType;
+
+ Assert.ThrowsArgumentNull(() => selector.Negotiate(typeof(string), null, Enumerable.Empty<MediaTypeFormatter>(), out mediaType), "request");
+ }
+
+ [Fact]
+ public void Negotiate_WhenFormattersParameterIsNull_ThrowsException()
+ {
+ DefaultContentNegotiator selector = new DefaultContentNegotiator();
+ HttpRequestMessage request = new HttpRequestMessage();
+ MediaTypeHeaderValue mediaType;
+
+ Assert.ThrowsArgumentNull(() => selector.Negotiate(typeof(string), request, null, out mediaType), "formatters");
+ }
+
+ [Fact]
+ public void Negotiate_ForEmptyFormatterCollection_ReturnsNull()
+ {
+ DefaultContentNegotiator selector = new DefaultContentNegotiator();
+ HttpRequestMessage request = new HttpRequestMessage();
+ MediaTypeHeaderValue mediaType;
+
+ MediaTypeFormatter formatter = selector.Negotiate(typeof(string), request, Enumerable.Empty<MediaTypeFormatter>(), out mediaType);
+
+ Assert.Null(formatter);
+ Assert.Null(mediaType);
+ }
+
+ [Fact]
+ public void Negotiate_ForRequestReturnsFirstMatchingFormatter()
+ {
+ MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue("application/myMediaType");
+
+ MediaTypeFormatter formatter1 = new MockMediaTypeFormatter()
+ {
+ CanWriteTypeCallback = (Type t) => false
+ };
+
+ MediaTypeFormatter formatter2 = new MockMediaTypeFormatter()
+ {
+ CanWriteTypeCallback = (Type t) => true
+ };
+
+ formatter2.SupportedMediaTypes.Add(mediaType);
+
+ MediaTypeFormatterCollection collection = new MediaTypeFormatterCollection(
+ new MediaTypeFormatter[]
+ {
+ formatter1,
+ formatter2
+ });
+
+ HttpContent content = new StringContent("test");
+ content.Headers.ContentType = mediaType;
+ HttpRequestMessage request = new HttpRequestMessage()
+ {
+ Content = content
+ };
+
+ DefaultContentNegotiator selector = new DefaultContentNegotiator();
+ MediaTypeHeaderValue mediaTypeReturned = null;
+ MediaTypeFormatter formatter = selector.Negotiate(typeof(string), request, collection, out mediaTypeReturned);
+ Assert.Same(formatter2, formatter);
+ Assert.MediaType.AreEqual(mediaType, mediaTypeReturned, "Expected the formatter's media type to be returned.");
+ }
+
+ [Fact]
+ public void Negotiate_SelectsJsonAsDefaultFormatter()
+ {
+ // Arrange
+ HttpRequestMessage request = new HttpRequestMessage()
+ {
+ Content = new StringContent("test")
+ };
+ DefaultContentNegotiator selector = new DefaultContentNegotiator();
+ MediaTypeHeaderValue mediaTypeReturned = null;
+
+ // Act
+ MediaTypeFormatter formatter = selector.Negotiate(typeof(string), request, new MediaTypeFormatterCollection(), out mediaTypeReturned);
+
+ // Assert
+ Assert.IsType<JsonMediaTypeFormatter>(formatter);
+ Assert.Equal(mediaTypeReturned.MediaType, MediaTypeConstants.ApplicationJsonMediaType.MediaType);
+ }
+
+ [Fact]
+ public void Negotiate_SelectsXmlFormatter_ForXhrRequestThatAcceptsXml()
+ {
+ // Arrange
+ HttpRequestMessage request = new HttpRequestMessage()
+ {
+ Content = new StringContent("test")
+ };
+ request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml"));
+ request.Headers.Add("x-requested-with", "XMLHttpRequest");
+ DefaultContentNegotiator selector = new DefaultContentNegotiator();
+ MediaTypeHeaderValue mediaTypeReturned = null;
+
+ // Act
+ MediaTypeFormatter formatter = selector.Negotiate(typeof(string), request, new MediaTypeFormatterCollection(), out mediaTypeReturned);
+
+ // Assert
+ Assert.Equal("application/xml", mediaTypeReturned.MediaType);
+ Assert.IsType<XmlMediaTypeFormatter>(formatter);
+ }
+
+ [Fact]
+ public void Negotiate_SelectsJsonFormatter_ForXhrRequestThatDoesNotSpecifyAcceptHeaders()
+ {
+ // Arrange
+ HttpRequestMessage request = new HttpRequestMessage()
+ {
+ Content = new StringContent("test")
+ };
+ request.Headers.Add("x-requested-with", "XMLHttpRequest");
+ DefaultContentNegotiator selector = new DefaultContentNegotiator();
+ MediaTypeHeaderValue mediaTypeReturned = null;
+
+ // Act
+ MediaTypeFormatter formatter = selector.Negotiate(typeof(string), request, new MediaTypeFormatterCollection(), out mediaTypeReturned);
+
+ // Assert
+ Assert.Equal("application/json", mediaTypeReturned.MediaType);
+ Assert.IsType<JsonMediaTypeFormatter>(formatter);
+ }
+
+ [Fact]
+ public void Negotiate_SelectsJsonFormatter_ForXHRAndJsonValueResponse()
+ {
+ // Arrange
+ HttpRequestMessage request = new HttpRequestMessage()
+ {
+ Content = new StringContent("test")
+ };
+ request.Headers.Add("x-requested-with", "XMLHttpRequest");
+ DefaultContentNegotiator selector = new DefaultContentNegotiator();
+ MediaTypeHeaderValue mediaTypeReturned = null;
+
+ // Act
+ MediaTypeFormatter formatter = selector.Negotiate(typeof(JsonValue), request, new MediaTypeFormatterCollection(), out mediaTypeReturned);
+
+ Assert.Equal("application/json", mediaTypeReturned.MediaType);
+ Assert.IsType<JsonMediaTypeFormatter>(formatter);
+ }
+
+ [Fact]
+ public void Negotiate_SelectsJsonFormatter_ForXHRAndMatchAllAcceptHeader()
+ {
+ // Accept
+ HttpRequestMessage request = new HttpRequestMessage()
+ {
+ Content = new StringContent("test")
+ };
+ request.Headers.Add("x-requested-with", "XMLHttpRequest");
+ request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("*/*"));
+ DefaultContentNegotiator selector = new DefaultContentNegotiator();
+ MediaTypeHeaderValue mediaTypeReturned = null;
+
+ // Act
+ MediaTypeFormatter formatter = selector.Negotiate(typeof(string), request, new MediaTypeFormatterCollection(), out mediaTypeReturned);
+
+ // Assert
+ Assert.Equal("application/json", mediaTypeReturned.MediaType);
+ Assert.IsType<JsonMediaTypeFormatter>(formatter);
+ }
+
+ [Fact]
+ public void Negotiate_UsesRequestedFormatterForXHRAndMatchAllPlusOtherAcceptHeader()
+ {
+ // Arrange
+ HttpRequestMessage request = new HttpRequestMessage()
+ {
+ Content = new StringContent("test")
+ };
+ request.Headers.Add("x-requested-with", "XMLHttpRequest");
+ request.Headers.Accept.ParseAdd("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); // XHR header sent by Firefox 3b5
+ DefaultContentNegotiator selector = new DefaultContentNegotiator();
+ MediaTypeHeaderValue mediaTypeReturned = null;
+
+ // Act
+ MediaTypeFormatter formatter = selector.Negotiate(typeof(string), request, new MediaTypeFormatterCollection(), out mediaTypeReturned);
+
+ // Assert
+ Assert.Equal("application/xml", mediaTypeReturned.MediaType);
+ Assert.IsType<XmlMediaTypeFormatter>(formatter);
+ }
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/Formatting/FormDataCollectionTests.cs b/test/System.Net.Http.Formatting.Test.Unit/Formatting/FormDataCollectionTests.cs
new file mode 100644
index 00000000..1e250a04
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/Formatting/FormDataCollectionTests.cs
@@ -0,0 +1,107 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Json;
+using System.Linq;
+using System.Net.Http.Formatting.DataSets;
+using System.Net.Http.Formatting.DataSets.Types;
+using System.Net.Http.Headers;
+using System.Runtime.Serialization.Json;
+using System.Text;
+using Microsoft.TestCommon;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+using System.Collections.Specialized;
+
+namespace System.Net.Http.Formatting
+{
+ public class FormDataCollectionTests
+ {
+ [Fact]
+ public void CreateFromUri()
+ {
+ FormDataCollection form = new FormDataCollection(new Uri("http://foo.com/?x=1&y=2"));
+
+ Assert.Equal("1", form.Get("x"));
+ Assert.Equal("2", form.Get("y"));
+ }
+
+ [Fact]
+ public void CreateFromEmptyUri()
+ {
+ FormDataCollection form = new FormDataCollection(new Uri("http://foo.com"));
+
+ Assert.Equal(0, form.Count());
+ }
+
+ [Fact]
+ public void UriConstructorThrowsNull()
+ {
+ Assert.Throws<ArgumentNullException>(() => new FormDataCollection((Uri)null));
+ }
+
+ [Fact]
+ public void PairConstructorThrowsNull()
+ {
+ var arg = (IEnumerable<KeyValuePair<string, string>>)null;
+ Assert.Throws<ArgumentNullException>(() => new FormDataCollection(arg));
+ }
+
+ [Fact]
+ public void CreateFromPairs()
+ {
+ Dictionary<string, string> pairs = new Dictionary<string,string>
+ {
+ { "x", "1"},
+ { "y" , "2"}
+ };
+
+ var form = new FormDataCollection(pairs);
+
+ Assert.Equal("1", form.Get("x"));
+ Assert.Equal("2", form.Get("y"));
+ }
+
+ [Fact]
+ public void Enumeration()
+ {
+ FormDataCollection form = new FormDataCollection(new Uri("http://foo.com/?x=1&y=2"));
+
+ // Enumeration should be ordered
+ String s = "";
+ foreach (KeyValuePair<string, string> kv in form)
+ {
+ s += string.Format("{0}={1};", kv.Key, kv.Value);
+ }
+
+ Assert.Equal("x=1;y=2;", s);
+ }
+
+ [Fact]
+ public void GetValues()
+ {
+ FormDataCollection form = new FormDataCollection(new Uri("http://foo.com/?x=1&x=2&x=3"));
+
+ Assert.Equal(new string [] { "1", "2", "3"}, form.GetValues("x"));
+ }
+
+ [Fact]
+ public void ToNameValueCollection()
+ {
+ FormDataCollection form = new FormDataCollection(new Uri("http://foo.com/?x=1a&y=2&x=1b&=ValueOnly&KeyOnly"));
+
+ NameValueCollection nvc = form.ReadAsNameValueCollection();
+
+ // y=2
+ // x=1a;x=1b
+ // =ValueOnly
+ // KeyOnly
+ Assert.Equal(4, nvc.Count);
+ Assert.Equal(new string[] { "1a", "1b"}, nvc.GetValues("x"));
+ Assert.Equal("1a,1b", nvc.Get("x"));
+ Assert.Equal("2", nvc.Get("y"));
+ Assert.Equal(null, nvc.Get("KeyOnly"));
+ Assert.Equal("ValueOnly", nvc.Get(""));
+ }
+ }
+} \ No newline at end of file
diff --git a/test/System.Net.Http.Formatting.Test.Unit/Formatting/FormUrlEncodedMediaTypeFormatterTests.cs b/test/System.Net.Http.Formatting.Test.Unit/Formatting/FormUrlEncodedMediaTypeFormatterTests.cs
new file mode 100644
index 00000000..8727fc8b
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/Formatting/FormUrlEncodedMediaTypeFormatterTests.cs
@@ -0,0 +1,148 @@
+using System.IO;
+using System.Net.Http.Formatting.DataSets;
+using System.Net.Http.Headers;
+using Microsoft.TestCommon;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Net.Http.Formatting
+{
+ public class FormUrlEncodedMediaTypeFormatterTests
+ {
+ private const int MinBufferSize = 256;
+ private const int DefaultBufferSize = 32 * 1024;
+
+ [Fact]
+ [Trait("Description", "FormUrlEncodedMediaTypeFormatter is public, concrete, and unsealed.")]
+ public void TypeIsCorrect()
+ {
+ Assert.Type.HasProperties(typeof(FormUrlEncodedMediaTypeFormatter), TypeAssert.TypeProperties.IsPublicVisibleClass);
+ }
+
+ [Theory]
+ [TestDataSet(typeof(HttpUnitTestDataSets), "StandardFormUrlEncodedMediaTypes")]
+ [Trait("Description", "FormUrlEncodedMediaTypeFormatter() constructor sets standard form URL encoded media types in SupportedMediaTypes.")]
+ public void Constructor(MediaTypeHeaderValue mediaType)
+ {
+ FormUrlEncodedMediaTypeFormatter formatter = new FormUrlEncodedMediaTypeFormatter();
+ Assert.True(formatter.SupportedMediaTypes.Contains(mediaType), String.Format("SupportedMediaTypes should have included {0}.", mediaType.ToString()));
+ }
+
+ [Fact]
+ [Trait("Description", "DefaultMediaType property returns application/x-www-form-urlencoded.")]
+ public void DefaultMediaTypeReturnsApplicationJson()
+ {
+ MediaTypeHeaderValue mediaType = FormUrlEncodedMediaTypeFormatter.DefaultMediaType;
+ Assert.NotNull(mediaType);
+ Assert.Equal("application/x-www-form-urlencoded", mediaType.MediaType);
+ }
+
+ [Fact]
+ [Trait("Description", "ReadBufferSize return correct value.")]
+ public void ReadBufferSizeReturnsCorrectValue()
+ {
+ FormUrlEncodedMediaTypeFormatter formatter = new FormUrlEncodedMediaTypeFormatter();
+ Assert.Equal(DefaultBufferSize, formatter.ReadBufferSize);
+ formatter.ReadBufferSize = MinBufferSize;
+ Assert.Equal(MinBufferSize, formatter.ReadBufferSize);
+ }
+
+ [Fact]
+ [Trait("Description", "ReadBufferSize throws on invalid value.")]
+ public void ReadBufferSizeThrowsOnInvalidValue()
+ {
+ FormUrlEncodedMediaTypeFormatter formatter = new FormUrlEncodedMediaTypeFormatter();
+ Assert.ThrowsArgument(() => { formatter.ReadBufferSize = -1; }, "value");
+ Assert.ThrowsArgument(() => { formatter.ReadBufferSize = 0; }, "value");
+ Assert.ThrowsArgument(() => { formatter.ReadBufferSize = MinBufferSize - 1; }, "value");
+ }
+
+ [Fact]
+ [Trait("Description", "CanReadType() throws on null.")]
+ public void CanReadTypeThrowsOnNull()
+ {
+ TestFormUrlEncodedMediaTypeFormatter formatter = new TestFormUrlEncodedMediaTypeFormatter();
+ Assert.ThrowsArgumentNull(() => { formatter.CanReadType(null); }, "type");
+ }
+
+ [Theory]
+ [InlineData(typeof(FormDataCollection))]
+ [InlineData(typeof(System.Json.JsonValue))]
+ [InlineData(typeof(IKeyValueModel))]
+ public void CanReadTypeTrue(Type type)
+ {
+ TestFormUrlEncodedMediaTypeFormatter formatter = new TestFormUrlEncodedMediaTypeFormatter();
+
+ Assert.True(formatter.CanReadType(type));
+ }
+
+
+ [Theory]
+ [TestDataSet(typeof(CommonUnitTestDataSets), "RepresentativeValueAndRefTypeTestDataCollection")]
+ [Trait("Description", "CanReadType(Type) returns false.")]
+ public void CanReadTypeReturnsFalse(Type variationType, object testData)
+ {
+ TestFormUrlEncodedMediaTypeFormatter formatter = new TestFormUrlEncodedMediaTypeFormatter();
+
+ Assert.False(formatter.CanReadType(variationType));
+
+ // Ask a 2nd time to probe whether the cached result is treated the same
+ Assert.False(formatter.CanReadType(variationType));
+ }
+
+
+ [Fact]
+ [Trait("Description", "CanWriteType(Type) throws on null.")]
+ public void CanWriteTypeThrowsOnNull()
+ {
+ TestFormUrlEncodedMediaTypeFormatter formatter = new TestFormUrlEncodedMediaTypeFormatter();
+ Assert.ThrowsArgumentNull(() => { formatter.CanWriteType(null); }, "type");
+ }
+
+ [Theory]
+ [TestDataSet(typeof(CommonUnitTestDataSets), "RepresentativeValueAndRefTypeTestDataCollection")]
+ [Trait("Description", "CanWriteType() returns false.")]
+ public void CanWriteTypeReturnsFalse(Type variationType, object testData)
+ {
+ TestFormUrlEncodedMediaTypeFormatter formatter = new TestFormUrlEncodedMediaTypeFormatter();
+
+ Assert.False(formatter.CanWriteType(variationType), "formatter should have returned false.");
+
+ // Ask a 2nd time to probe whether the cached result is treated the same
+ Assert.False(formatter.CanWriteType(variationType), "formatter should have returned false on 2nd try as well.");
+ }
+
+ [Fact]
+ [Trait("Description", "ReadFromStreamAsync() throws on null.")]
+ public void ReadFromStreamThrowsOnNull()
+ {
+ TestFormUrlEncodedMediaTypeFormatter formatter = new TestFormUrlEncodedMediaTypeFormatter();
+ Assert.ThrowsArgumentNull(() => { formatter.ReadFromStreamAsync(null, Stream.Null, null, null); }, "type");
+ Assert.ThrowsArgumentNull(() => { formatter.ReadFromStreamAsync(typeof(object), null, null, null); }, "stream");
+ }
+
+ [Fact]
+ [Trait("Description", "WriteToStreamAsync() throws not implemented.")]
+ public void WriteToStreamAsyncThrowsNotImplemented()
+ {
+ FormUrlEncodedMediaTypeFormatter formatter = new FormUrlEncodedMediaTypeFormatter();
+ Assert.Throws<NotSupportedException>(
+ () => formatter.WriteToStreamAsync(typeof(object), new object(), Stream.Null, null, null),
+ "The media type formatter of type 'System.Net.Http.Formatting.FormUrlEncodedMediaTypeFormatter' does not support writing since it does not implement the WriteToStreamAsync method.");
+ }
+
+ public class TestFormUrlEncodedMediaTypeFormatter : FormUrlEncodedMediaTypeFormatter
+ {
+ public new bool CanReadType(Type type)
+ {
+ return base.CanReadType(type);
+ }
+
+ public new bool CanWriteType(Type type)
+ {
+ return base.CanWriteType(type);
+ }
+ }
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/Formatting/JsonKeyValueModelTest.cs b/test/System.Net.Http.Formatting.Test.Unit/Formatting/JsonKeyValueModelTest.cs
new file mode 100644
index 00000000..36a5a19e
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/Formatting/JsonKeyValueModelTest.cs
@@ -0,0 +1,39 @@
+using System.Json;
+using Microsoft.TestCommon;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Net.Http.Formatting
+{
+ public class JsonKeyValueModelTest
+ {
+ [Theory]
+ [PropertyData("JsonKeyValueModelBuildsCorrectlyData")]
+ public void JsonKeyValueModelBuildsCorrectly(string json, string expectedKey, object expectedValue)
+ {
+ JsonKeyValueModel keyValueModel = new JsonKeyValueModel(JsonValue.Parse(json));
+
+ object value;
+ Assert.Contains(expectedKey, keyValueModel.Keys);
+ Assert.True(keyValueModel.TryGetValue(expectedKey, out value));
+ Assert.Equal(expectedValue, value);
+ }
+
+ public static TheoryDataSet<string, string, object> JsonKeyValueModelBuildsCorrectlyData
+ {
+ get
+ {
+ return new TheoryDataSet<string, string, object>
+ {
+ { "{ \"input\" : [1, 2, 3] }", "input[0]", "1" }, // array inside dict
+ { "{ \"input\" : [1, 2, 3] }", "input[1]", "2" }, // array inside dict
+ { "{ \"input\" : [1, 2, 3] }", "input[2]", "3" }, // array inside dict
+ { "[1, 2, 3]" , "[0]", "1" }, // just a json array
+ { "{ \"foo\" : [ { \"bar\" : 1 }, { \"bar\" : 1 } ] }", "foo[1].bar", "1" }, // dict inside array inside dict
+ { "[ [1, 2, 3], [ 2, 3, 4] ]", "[1][2]", "4" }, // array of array
+ { "[ { \"foo\" : \"bar\" }, { \"foo1\" : \"bar1\" }]", "[1].foo1" , "bar1" } // array of dicts
+ };
+ }
+ }
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/Formatting/JsonMediaTypeFormatterTests.cs b/test/System.Net.Http.Formatting.Test.Unit/Formatting/JsonMediaTypeFormatterTests.cs
new file mode 100644
index 00000000..f8daf91c
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/Formatting/JsonMediaTypeFormatterTests.cs
@@ -0,0 +1,244 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Json;
+using System.Linq;
+using System.Net.Http.Formatting.DataSets;
+using System.Net.Http.Formatting.DataSets.Types;
+using System.Net.Http.Headers;
+using System.Runtime.Serialization.Json;
+using System.Text;
+using Microsoft.TestCommon;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Net.Http.Formatting
+{
+ public class JsonMediaTypeFormatterTests
+ {
+ public static List<Type> JsonValueTypes
+ {
+ get
+ {
+ return new List<Type>
+ {
+ typeof(JsonValue),
+ typeof(JsonPrimitive),
+ typeof(JsonArray),
+ typeof(JsonObject)
+ };
+ }
+ }
+
+ public static IEnumerable<TestData> ValueAndRefTypeTestDataCollectionExceptULong
+ {
+ get
+ {
+ return CommonUnitTestDataSets.ValueAndRefTypeTestDataCollection.Except(new[] { CommonUnitTestDataSets.Ulongs });
+ }
+ }
+
+ [Fact]
+ public void TypeIsCorrect()
+ {
+ Assert.Type.HasProperties<JsonMediaTypeFormatter, MediaTypeFormatter>(TypeAssert.TypeProperties.IsPublicVisibleClass);
+ }
+
+ [Fact]
+ [Trait("Description", "JsonMediaTypeFormatter() constructor sets standard Json media types in SupportedMediaTypes.")]
+ public void Constructor()
+ {
+ JsonMediaTypeFormatter formatter = new JsonMediaTypeFormatter();
+ foreach (MediaTypeHeaderValue mediaType in HttpUnitTestDataSets.StandardJsonMediaTypes)
+ {
+ Assert.True(formatter.SupportedMediaTypes.Contains(mediaType), String.Format("SupportedMediaTypes should have included {0}.", mediaType.ToString()));
+ }
+ }
+
+ [Fact]
+ [Trait("Description", "DefaultMediaType property returns application/json.")]
+ public void DefaultMediaTypeReturnsApplicationJson()
+ {
+ MediaTypeHeaderValue mediaType = JsonMediaTypeFormatter.DefaultMediaType;
+ Assert.NotNull(mediaType);
+ Assert.Equal("application/json", mediaType.MediaType);
+ }
+
+
+ [Fact]
+ [Trait("Description", "CharacterEncoding property handles Get/Set correctly.")]
+ public void CharacterEncodingGetSet()
+ {
+ JsonMediaTypeFormatter jsonFormatter = new JsonMediaTypeFormatter();
+ Assert.IsType<UTF8Encoding>(jsonFormatter.CharacterEncoding);
+ jsonFormatter.CharacterEncoding = Encoding.Unicode;
+ Assert.Same(Encoding.Unicode, jsonFormatter.CharacterEncoding);
+ jsonFormatter.CharacterEncoding = Encoding.UTF8;
+ Assert.Same(Encoding.UTF8, jsonFormatter.CharacterEncoding);
+ }
+
+ [Fact]
+ [Trait("Description", "CharacterEncoding property throws on invalid arguments")]
+ public void CharacterEncodingSetThrows()
+ {
+ JsonMediaTypeFormatter jsonFormatter = new JsonMediaTypeFormatter();
+ Assert.ThrowsArgumentNull(() => { jsonFormatter.CharacterEncoding = null; }, "value");
+ Assert.ThrowsArgument(() => { jsonFormatter.CharacterEncoding = Encoding.UTF32; }, "value");
+ }
+
+ [Theory]
+ [TestDataSet(typeof(CommonUnitTestDataSets), "RepresentativeValueAndRefTypeTestDataCollection")]
+ [Trait("Description", "CanReadType() returns the expected results for all known value and reference types.")]
+ public void CanReadTypeReturnsExpectedValues(Type variationType, object testData)
+ {
+ TestJsonMediaTypeFormatter formatter = new TestJsonMediaTypeFormatter();
+
+ bool isSerializable = IsTypeSerializableWithJsonSerializer(variationType, testData);
+ bool canSupport = formatter.CanReadTypeProxy(variationType);
+
+ // If we don't agree, we assert only if the DCJ serializer says it cannot support something we think it should
+ Assert.False(isSerializable != canSupport && isSerializable, String.Format("CanReadType returned wrong value for '{0}'.", variationType));
+
+ // Ask a 2nd time to probe whether the cached result is treated the same
+ canSupport = formatter.CanReadTypeProxy(variationType);
+ Assert.False(isSerializable != canSupport && isSerializable, String.Format("2nd CanReadType returned wrong value for '{0}'.", variationType));
+ }
+
+ [Fact]
+ [Trait("Description", "CanReadType() returns true on JsonValue.")]
+ public void CanReadTypeReturnsTrueOnJsonValue()
+ {
+ TestJsonMediaTypeFormatter formatter = new TestJsonMediaTypeFormatter();
+ foreach (Type type in JsonValueTypes)
+ {
+ Assert.True(formatter.CanReadTypeProxy(type), "formatter should have returned true.");
+ }
+ }
+
+ [Fact]
+ [Trait("Description", "CanWriteType() returns true on JsonValue.")]
+ public void CanWriteTypeReturnsTrueOnJsonValue()
+ {
+ TestJsonMediaTypeFormatter formatter = new TestJsonMediaTypeFormatter();
+ foreach (Type type in JsonValueTypes)
+ {
+ Assert.True(formatter.CanWriteTypeProxy(type), "formatter should have returned false.");
+ }
+ }
+
+ [Theory]
+ [TestDataSet(typeof(JsonMediaTypeFormatterTests), "ValueAndRefTypeTestDataCollectionExceptULong")]
+ [Trait("Description", "ReadFromStream() returns all value and reference types serialized via WriteToStream.")]
+ public void ReadFromAsyncStreamRoundTripsWriteToStreamAsync(Type variationType, object testData)
+ {
+ TestJsonMediaTypeFormatter formatter = new TestJsonMediaTypeFormatter();
+ HttpContentHeaders contentHeaders = new StringContent(String.Empty).Headers;
+
+
+ bool canSerialize = IsTypeSerializableWithJsonSerializer(variationType, testData) && Assert.Http.CanRoundTrip(variationType);
+ if (canSerialize)
+ {
+ object readObj = null;
+ Assert.Stream.WriteAndRead(
+ stream => Assert.Task.Succeeds(formatter.WriteToStreamAsync(variationType, testData, stream, contentHeaders, transportContext: null)),
+ stream => readObj = Assert.Task.SucceedsWithResult<object>(formatter.ReadFromStreamAsync(variationType, stream, contentHeaders, null)));
+ Assert.Equal(testData, readObj);
+ }
+ }
+
+ [Fact]
+ [Trait("Description", "ReadFromStreamAsync() roundtrips JsonValue.")]
+ public void ReadFromStreamAsyncRoundTripsJsonValue()
+ {
+ string beforeMessage = "Hello World";
+ TestJsonMediaTypeFormatter formatter = new TestJsonMediaTypeFormatter();
+ JsonValue before = beforeMessage;
+ MemoryStream memStream = new MemoryStream();
+ before.Save(memStream);
+ memStream.Position = 0;
+
+ JsonValue after = Assert.Task.SucceedsWithResult<object>(formatter.ReadFromStreamAsync(typeof(JsonValue), memStream, null, null)) as JsonValue;
+ Assert.NotNull(after);
+ string afterMessage = after.ReadAs<string>();
+
+ Assert.Equal(beforeMessage, afterMessage);
+ }
+
+ [Theory]
+ [TestDataSet(typeof(CommonUnitTestDataSets), "RepresentativeValueAndRefTypeTestDataCollection")]
+ [Trait("Description", "ReadFromStreamAsync() returns all value and reference types serialized via WriteToStreamAsync.")]
+ public void ReadFromStreamAsyncRoundTripsWriteToStreamAsync(Type variationType, object testData)
+ {
+ TestJsonMediaTypeFormatter formatter = new TestJsonMediaTypeFormatter();
+ HttpContentHeaders contentHeaders = new StringContent(String.Empty).Headers;
+
+ bool canSerialize = IsTypeSerializableWithJsonSerializer(variationType, testData) && Assert.Http.CanRoundTrip(variationType);
+ if (canSerialize)
+ {
+ object readObj = null;
+ Assert.Stream.WriteAndRead(
+ stream => Assert.Task.Succeeds(formatter.WriteToStreamAsync(variationType, testData, stream, contentHeaders, transportContext: null)),
+ stream => readObj = Assert.Task.SucceedsWithResult(formatter.ReadFromStreamAsync(variationType, stream, contentHeaders, null)));
+ Assert.Equal(testData, readObj);
+ }
+
+ }
+
+ [Fact]
+ [Trait("Description", "OnWriteToStreamAsync() throws on null.")]
+ public void WriteToStreamAsyncThrowsOnNull()
+ {
+ TestJsonMediaTypeFormatter formatter = new TestJsonMediaTypeFormatter();
+ Assert.ThrowsArgumentNull(() => { formatter.WriteToStreamAsync(null, new object(), Stream.Null, null, null); }, "type");
+ Assert.ThrowsArgumentNull(() => { formatter.WriteToStreamAsync(typeof(object), new object(), null, null, null); }, "stream");
+ }
+
+ [Fact]
+ [Trait("Description", "OnWriteToStreamAsync() roundtrips JsonValue.")]
+ public void WriteToStreamAsyncRoundTripsJsonValue()
+ {
+ string beforeMessage = "Hello World";
+ TestJsonMediaTypeFormatter formatter = new TestJsonMediaTypeFormatter();
+ JsonValue before = new JsonPrimitive(beforeMessage);
+ MemoryStream memStream = new MemoryStream();
+
+ Assert.Task.Succeeds(formatter.WriteToStreamAsync(typeof(JsonValue), before, memStream, null, null));
+ memStream.Position = 0;
+ JsonValue after = JsonValue.Load(memStream);
+ string afterMessage = after.ReadAs<string>();
+
+ Assert.Equal(beforeMessage, afterMessage);
+ }
+
+ public class TestJsonMediaTypeFormatter : JsonMediaTypeFormatter
+ {
+ public bool CanReadTypeProxy(Type type)
+ {
+ return CanReadType(type);
+ }
+
+ public bool CanWriteTypeProxy(Type type)
+ {
+ return CanWriteType(type);
+ }
+ }
+
+ private bool IsTypeSerializableWithJsonSerializer(Type type, object obj)
+ {
+ try
+ {
+ new DataContractJsonSerializer(type);
+ if (obj != null && obj.GetType() != type)
+ {
+ new DataContractJsonSerializer(obj.GetType());
+ }
+ }
+ catch
+ {
+ return false;
+ }
+
+ return !Assert.Http.IsKnownUnserializable(type, obj, (t) => typeof(INotJsonSerializable).IsAssignableFrom(t));
+ }
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/Formatting/MediaRangeMappingTests.cs b/test/System.Net.Http.Formatting.Test.Unit/Formatting/MediaRangeMappingTests.cs
new file mode 100644
index 00000000..51d4e981
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/Formatting/MediaRangeMappingTests.cs
@@ -0,0 +1,151 @@
+using System.Net.Http.Formatting.DataSets;
+using System.Net.Http.Headers;
+using Microsoft.TestCommon;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Net.Http.Formatting
+{
+ public class MediaRangeMappingTests
+ {
+ [Fact]
+ public void TypeIsCorrect()
+ {
+ Assert.Type.HasProperties<MediaRangeMapping, MediaTypeMapping>(TypeAssert.TypeProperties.IsPublicVisibleClass | TypeAssert.TypeProperties.IsSealed);
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalMediaRangeValues",
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeHeaderValues")]
+ [Trait("Description", "MediaRangeMapping(MediaTypeHeaderValue, MediaTypeHeaderValue) sets public properties.")]
+ public void Constructor(MediaTypeHeaderValue mediaRange, MediaTypeHeaderValue mediaType)
+ {
+ MediaRangeMapping mapping = new MediaRangeMapping(mediaRange, mediaType);
+ Assert.MediaType.AreEqual(mediaRange, mapping.MediaRange, "MediaRange failed to set.");
+ Assert.MediaType.AreEqual(mediaType, mapping.MediaType, "MediaType failed to set.");
+ }
+
+ [Theory]
+ [TestDataSet(typeof(HttpUnitTestDataSets), "LegalMediaTypeHeaderValues")]
+ [Trait("Description", "MediaRangeMapping(MediaTypeHeaderValue, MediaTypeHeaderValue) throws if the MediaRange parameter is null.")]
+ public void ConstructorThrowsWithNullMediaRange(MediaTypeHeaderValue mediaType)
+ {
+ Assert.ThrowsArgumentNull(() => new MediaRangeMapping((MediaTypeHeaderValue)null, mediaType), "mediaRange");
+ }
+
+ [Theory]
+ [TestDataSet(typeof(HttpUnitTestDataSets), "LegalMediaRangeValues")]
+ [Trait("Description", "MediaRangeMapping(MediaTypeHeaderValue, MediaTypeHeaderValue) throws if the MediaType parameter is null.")]
+ public void ConstructorThrowsWithNullMediaType(MediaTypeHeaderValue mediaRange)
+ {
+ Assert.ThrowsArgumentNull(() => new MediaRangeMapping(mediaRange, (MediaTypeHeaderValue)null), "mediaType");
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "IllegalMediaRangeValues",
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeHeaderValues")]
+ [Trait("Description", "MediaRangeMapping(MediaTypeHeaderValue, MediaTypeHeaderValue) throws if the MediaRange parameter is not really a media range.")]
+ public void ConstructorThrowsWithIllegalMediaRange(MediaTypeHeaderValue mediaRange, MediaTypeHeaderValue mediaType)
+ {
+ string errorMessage = RS.Format(Properties.Resources.InvalidMediaRange, mediaRange.MediaType);
+ Assert.Throws<InvalidOperationException>(() => new MediaRangeMapping(mediaRange, mediaType), errorMessage);
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalMediaRangeStrings",
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeStrings")]
+ [Trait("Description", "MediaRangeMapping(string, string) sets public properties.")]
+ public void Constructor1(string mediaRange, string mediaType)
+ {
+ MediaRangeMapping mapping = new MediaRangeMapping(mediaRange, mediaType);
+ Assert.MediaType.AreEqual(mediaRange, mapping.MediaRange, "MediaRange failed to set.");
+ Assert.MediaType.AreEqual(mediaType, mapping.MediaType, "MediaType failed to set.");
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(CommonUnitTestDataSets), "EmptyStrings",
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeStrings")]
+ [Trait("Description", "MediaRangeMapping(string, string) throws if the MediaRange parameter is empty.")]
+ public void Constructor1ThrowsWithEmptyMediaRange(string mediaRange, string mediaType)
+ {
+ Assert.ThrowsArgumentNull(() => new MediaRangeMapping(mediaRange, mediaType), "mediaRange");
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalMediaRangeStrings",
+ typeof(CommonUnitTestDataSets), "EmptyStrings")]
+ [Trait("Description", "MediaRangeMapping(string, string) throws if the MediaType parameter is empty.")]
+ public void Constructor1ThrowsWithEmptyMediaType(string mediaRange, string mediaType)
+ {
+ Assert.ThrowsArgumentNull(() => new MediaRangeMapping(mediaRange, mediaType), "mediaType");
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "IllegalMediaRangeStrings",
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeStrings")]
+ [Trait("Description", "MediaRangeMapping(string, string) throws if the MediaRange parameter is not really a media range.")]
+ public void Constructor1ThrowsWithIllegalMediaRange(string mediaRange, string mediaType)
+ {
+ string errorMessage = RS.Format(Properties.Resources.InvalidMediaRange, mediaRange);
+ Assert.Throws<InvalidOperationException>(() => new MediaRangeMapping(mediaRange, mediaType), errorMessage);
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalMediaRangeStrings",
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeStrings")]
+ [Trait("Description", "TryMatchMediaType(HttpRequestMessage) throws with null HttpRequestMessage.")]
+ public void TryMatchMediaTypeThrowsWithNullHttpRequestMessage(string mediaRange, string mediaType)
+ {
+ MediaRangeMapping mapping = new MediaRangeMapping(mediaRange, mediaType);
+ Assert.ThrowsArgumentNull(() => mapping.TryMatchMediaType(request: null), "request");
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalMediaRangeStrings",
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeStrings")]
+ [Trait("Description", "TryMatchMediaType(HttpRequestMessage) returns 1.0 when the MediaRange is in the accept headers.")]
+ public void TryMatchMediaTypeReturnsOneWithMediaRangeInAcceptHeader(string mediaRange, string mediaType)
+ {
+ MediaRangeMapping mapping = new MediaRangeMapping(mediaRange, mediaType);
+ HttpRequestMessage request = new HttpRequestMessage();
+ request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(mediaRange));
+ Assert.Equal(1.0, mapping.TryMatchMediaType(request));
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "MediaRangeValuesWithQuality",
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeHeaderValues")]
+ [Trait("Description", "TryMatchMediaType(HttpRequestMessage) returns quality factor when a MediaRange with quality is in the accept headers.")]
+ public void TryMatchMediaTypeReturnsQualityWithMediaRangeWithQualityInAcceptHeader(MediaTypeWithQualityHeaderValue mediaRangeWithQuality, MediaTypeHeaderValue mediaType)
+ {
+ MediaTypeWithQualityHeaderValue mediaRangeWithNoQuality = new MediaTypeWithQualityHeaderValue(mediaRangeWithQuality.MediaType);
+ MediaRangeMapping mapping = new MediaRangeMapping(mediaRangeWithNoQuality, mediaType);
+ HttpRequestMessage request = new HttpRequestMessage();
+ request.Headers.Accept.Add(mediaRangeWithQuality);
+ double quality = mediaRangeWithQuality.Quality.Value;
+ Assert.Equal(quality, mapping.TryMatchMediaType(request));
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalMediaRangeStrings",
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeStrings")]
+ [Trait("Description", "TryMatchMediaType(HttpRequestMessage) returns 0.0 when the MediaRange is not in the accept headers.")]
+ public void TryMatchMediaTypeReturnsFalseWithMediaRangeNotInAcceptHeader(string mediaRange, string mediaType)
+ {
+ MediaRangeMapping mapping = new MediaRangeMapping(mediaRange, mediaType);
+ HttpRequestMessage request = new HttpRequestMessage();
+ Assert.Equal(0.0, mapping.TryMatchMediaType(request));
+ }
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/Formatting/MediaTypeConstantsTests.cs b/test/System.Net.Http.Formatting.Test.Unit/Formatting/MediaTypeConstantsTests.cs
new file mode 100644
index 00000000..1df9553f
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/Formatting/MediaTypeConstantsTests.cs
@@ -0,0 +1,74 @@
+using System.Net.Http.Formatting;
+using System.Net.Http.Headers;
+using System.Text;
+using Microsoft.TestCommon;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Net.Http
+{
+ public class MediaTypeConstantsTests
+ {
+ [Fact]
+ [Trait("Description", "Class is internal static type.")]
+ public void TypeIsCorrect()
+ {
+ Assert.Type.HasProperties(typeof(MediaTypeConstants), TypeAssert.TypeProperties.IsClass | TypeAssert.TypeProperties.IsStatic);
+ }
+
+
+ private static void ValidateClones(MediaTypeHeaderValue clone1, MediaTypeHeaderValue clone2, string charset)
+ {
+ Assert.NotNull(clone1);
+ Assert.NotNull(clone2);
+ Assert.NotSame(clone1, clone2);
+ Assert.Equal(clone1.MediaType, clone2.MediaType);
+ Assert.Equal(charset, clone1.CharSet);
+ Assert.Equal(charset, clone2.CharSet);
+ }
+
+ [Fact]
+ [Trait("Description", "HtmlMediaType returns clone")]
+ public void HtmlMediaTypeReturnsClone()
+ {
+ ValidateClones(MediaTypeConstants.HtmlMediaType, MediaTypeConstants.HtmlMediaType, Encoding.UTF8.WebName);
+ }
+
+ [Fact]
+ [Trait("Description", "ApplicationXmlMediaType returns clone")]
+ public void ApplicationXmlMediaTypeReturnsClone()
+ {
+ ValidateClones(MediaTypeConstants.ApplicationXmlMediaType, MediaTypeConstants.ApplicationXmlMediaType, null);
+ }
+
+ [Fact]
+ [Trait("Description", "ApplicationJsonMediaType returns clone")]
+ public void ApplicationJsonMediaTypeReturnsClone()
+ {
+ ValidateClones(MediaTypeConstants.ApplicationJsonMediaType, MediaTypeConstants.ApplicationJsonMediaType, null);
+ }
+
+ [Fact]
+ [Trait("Description", "TextXmlMediaType returns clone")]
+ public void TextXmlMediaTypeReturnsClone()
+ {
+ ValidateClones(MediaTypeConstants.TextXmlMediaType, MediaTypeConstants.TextXmlMediaType, null);
+ }
+
+ [Fact]
+ [Trait("Description", "TextJsonMediaType returns clone")]
+ public void TextJsonMediaTypeReturnsClone()
+ {
+ ValidateClones(MediaTypeConstants.TextJsonMediaType, MediaTypeConstants.TextJsonMediaType, null);
+ }
+
+ [Fact]
+ [Trait("Description", "ApplicationFormUrlEncodedMediaType returns clone")]
+ public void ApplicationFormUrlEncodedMediaTypeReturnsClone()
+ {
+ ValidateClones(MediaTypeConstants.ApplicationFormUrlEncodedMediaType, MediaTypeConstants.ApplicationFormUrlEncodedMediaType, null);
+ }
+
+
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/Formatting/MediaTypeFormatterCollectionTests.cs b/test/System.Net.Http.Formatting.Test.Unit/Formatting/MediaTypeFormatterCollectionTests.cs
new file mode 100644
index 00000000..c713e2d4
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/Formatting/MediaTypeFormatterCollectionTests.cs
@@ -0,0 +1,222 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Net.Http.Formatting.DataSets;
+using Microsoft.TestCommon;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Net.Http.Formatting
+{
+ public class MediaTypeFormatterCollectionTests
+ {
+
+ [Fact]
+ [Trait("Description", "MediaTypeFormatterCollection is public, concrete, and unsealed.")]
+ public void TypeIsCorrect()
+ {
+ Assert.Type.HasProperties(typeof(MediaTypeFormatterCollection), TypeAssert.TypeProperties.IsPublicVisibleClass, typeof(Collection<MediaTypeFormatter>));
+ }
+
+ [Fact]
+ [Trait("Description", "MediaTypeFormatterCollection() initializes default formatters.")]
+ public void Constructor()
+ {
+ MediaTypeFormatterCollection collection = new MediaTypeFormatterCollection();
+ Assert.Equal(3, collection.Count);
+ Assert.NotNull(collection.XmlFormatter);
+ Assert.NotNull(collection.JsonFormatter);
+ Assert.NotNull(collection.FormUrlEncodedFormatter);
+ }
+
+ [Fact]
+ [Trait("Description", "MediaTypeFormatterCollection(IEnumerable<MediaTypeFormatter>) accepts empty collection and does not add to it.")]
+ public void Constructor1AcceptsEmptyList()
+ {
+ MediaTypeFormatterCollection collection = new MediaTypeFormatterCollection(new MediaTypeFormatter[0]);
+ Assert.Equal(0, collection.Count);
+ }
+
+ [Fact]
+ [Trait("Description", "MediaTypeFormatterCollection(IEnumerable<MediaTypeFormatter>) sets XmlFormatter and JsonFormatter for all known collections of formatters that contain them.")]
+ public void Constructor1SetsProperties()
+ {
+ // All combination of formatters presented to ctor should still set XmlFormatter
+ foreach (IEnumerable<MediaTypeFormatter> formatterCollection in HttpUnitTestDataSets.AllFormatterCollections)
+ {
+ MediaTypeFormatterCollection collection = new MediaTypeFormatterCollection(formatterCollection);
+ if (collection.OfType<XmlMediaTypeFormatter>().Any())
+ {
+ Assert.NotNull(collection.XmlFormatter);
+ }
+ else
+ {
+ Assert.Null(collection.XmlFormatter);
+ }
+
+ if (collection.OfType<JsonMediaTypeFormatter>().Any())
+ {
+ Assert.NotNull(collection.JsonFormatter);
+ }
+ else
+ {
+ Assert.Null(collection.JsonFormatter);
+ }
+ }
+ }
+
+ [Fact]
+ [Trait("Description", "MediaTypeFormatterCollection(IEnumerable<MediaTypeFormatter>) sets derived classes of Xml and Json formatters.")]
+ public void Constructor1SetsDerivedFormatters()
+ {
+ // force to array to get stable instances
+ MediaTypeFormatter[] derivedFormatters = HttpUnitTestDataSets.DerivedFormatters.ToArray();
+ MediaTypeFormatterCollection collection = new MediaTypeFormatterCollection(derivedFormatters);
+ Assert.True(derivedFormatters.SequenceEqual(collection));
+ }
+
+ [Fact]
+ [Trait("Description", "MediaTypeFormatterCollection(IEnumerable<MediaTypeFormatter>) throws with null formatters collection.")]
+ public void Constructor1ThrowsWithNullFormatters()
+ {
+ Assert.ThrowsArgumentNull(() => new MediaTypeFormatterCollection(null), "formatters");
+ }
+
+ [Fact]
+ [Trait("Description", "MediaTypeFormatterCollection(IEnumerable<MediaTypeFormatter>) throws with null formatter in formatters collection.")]
+ public void Constructor1ThrowsWithNullFormatterInCollection()
+ {
+ Assert.ThrowsArgument(
+ () => new MediaTypeFormatterCollection(new MediaTypeFormatter[] { null }), "formatters",
+ RS.Format(Properties.Resources.CannotHaveNullInList,
+ typeof(MediaTypeFormatter).Name));
+ }
+
+ [Fact]
+ [Trait("Description", "MediaTypeFormatterCollection(IEnumerable<MediaTypeFormatter>) accepts multiple instances of same formatter type.")]
+ public void Constructor1AcceptsDuplicateFormatterTypes()
+ {
+ MediaTypeFormatter[] formatters = new MediaTypeFormatter[]
+ {
+ new XmlMediaTypeFormatter(),
+ new JsonMediaTypeFormatter(),
+ new FormUrlEncodedMediaTypeFormatter(),
+ new XmlMediaTypeFormatter(),
+ new JsonMediaTypeFormatter(),
+ new FormUrlEncodedMediaTypeFormatter(),
+ };
+
+ MediaTypeFormatterCollection collection = new MediaTypeFormatterCollection(formatters);
+ Assert.True(formatters.SequenceEqual(collection));
+ }
+
+ [Fact]
+ [Trait("Description", "XmlFormatter is set by ctor.")]
+ public void XmlFormatterSetByCtor()
+ {
+ XmlMediaTypeFormatter formatter = new XmlMediaTypeFormatter();
+ MediaTypeFormatterCollection collection = new MediaTypeFormatterCollection(new MediaTypeFormatter[] { formatter });
+ Assert.Same(formatter, collection.XmlFormatter);
+ }
+
+ [Fact]
+ [Trait("Description", "XmlFormatter is cleared by ctor with empty collection.")]
+ public void XmlFormatterClearedByCtor()
+ {
+ MediaTypeFormatterCollection collection = new MediaTypeFormatterCollection(new MediaTypeFormatter[0]);
+ Assert.Null(collection.XmlFormatter);
+ }
+
+
+
+ [Fact]
+ [Trait("Description", "JsonFormatter is set by ctor.")]
+ public void JsonFormatterSetByCtor()
+ {
+ JsonMediaTypeFormatter formatter = new JsonMediaTypeFormatter();
+ MediaTypeFormatterCollection collection = new MediaTypeFormatterCollection(new MediaTypeFormatter[] { formatter });
+ Assert.Same(formatter, collection.JsonFormatter);
+ }
+
+ [Fact]
+ [Trait("Description", "JsonFormatter is cleared by ctor with empty collection.")]
+ public void JsonFormatterClearedByCtor()
+ {
+ MediaTypeFormatterCollection collection = new MediaTypeFormatterCollection(new MediaTypeFormatter[0]);
+ Assert.Null(collection.JsonFormatter);
+ }
+
+
+
+
+ [Fact]
+ [Trait("Description", "FormUrlEncodedFormatter is set by ctor.")]
+ public void FormUrlEncodedFormatterSetByCtor()
+ {
+ FormUrlEncodedMediaTypeFormatter formatter = new FormUrlEncodedMediaTypeFormatter();
+ MediaTypeFormatterCollection collection = new MediaTypeFormatterCollection(new MediaTypeFormatter[] { formatter });
+ Assert.Same(formatter, collection.FormUrlEncodedFormatter);
+ }
+
+ [Fact]
+ [Trait("Description", "FormUrlEncodedFormatter is cleared by ctor with empty collection.")]
+ public void FormUrlEncodedFormatterClearedByCtor()
+ {
+ MediaTypeFormatterCollection collection = new MediaTypeFormatterCollection(new MediaTypeFormatter[0]);
+ Assert.Null(collection.FormUrlEncodedFormatter);
+ }
+
+
+
+
+
+
+ [Fact]
+ [Trait("Description", "Remove(MediaTypeFormatter) sets XmlFormatter to null.")]
+ public void RemoveSetsXmlFormatter()
+ {
+ MediaTypeFormatterCollection collection = new MediaTypeFormatterCollection();
+ int count = collection.Count;
+ collection.Remove(collection.XmlFormatter);
+ Assert.Null(collection.XmlFormatter);
+ Assert.Equal(count - 1, collection.Count);
+ }
+
+ [Fact]
+ [Trait("Description", "Remove(MediaTypeFormatter) sets JsonFormatter to null.")]
+ public void RemoveSetsJsonFormatter()
+ {
+ MediaTypeFormatterCollection collection = new MediaTypeFormatterCollection();
+ int count = collection.Count;
+ collection.Remove(collection.JsonFormatter);
+ Assert.Null(collection.JsonFormatter);
+ Assert.Equal(count - 1, collection.Count);
+ }
+
+ [Fact]
+ [Trait("Description", "Insert(int, MediaTypeFormatter) sets XmlFormatter.")]
+ public void InsertSetsXmlFormatter()
+ {
+ MediaTypeFormatterCollection collection = new MediaTypeFormatterCollection();
+ int count = collection.Count;
+ XmlMediaTypeFormatter formatter = new XmlMediaTypeFormatter();
+ collection.Insert(0, formatter);
+ Assert.Same(formatter, collection.XmlFormatter);
+ Assert.Equal(count + 1, collection.Count);
+ }
+
+ [Fact]
+ [Trait("Description", "Insert(int, MediaTypeFormatter) sets JsonFormatter.")]
+
+ public void InsertSetsJsonFormatter()
+ {
+ MediaTypeFormatterCollection collection = new MediaTypeFormatterCollection();
+ int count = collection.Count;
+ JsonMediaTypeFormatter formatter = new JsonMediaTypeFormatter();
+ collection.Insert(0, formatter);
+ Assert.Same(formatter, collection.JsonFormatter);
+ Assert.Equal(count + 1, collection.Count);
+ }
+
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/Formatting/MediaTypeFormatterExtensionsTests.cs b/test/System.Net.Http.Formatting.Test.Unit/Formatting/MediaTypeFormatterExtensionsTests.cs
new file mode 100644
index 00000000..8a285c25
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/Formatting/MediaTypeFormatterExtensionsTests.cs
@@ -0,0 +1,120 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http.Formatting.Mocks;
+using System.Net.Http.Headers;
+using Microsoft.TestCommon;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Net.Http.Formatting
+{
+ public class MediaTypeFormatterExtensionsTests
+ {
+ [Fact]
+ [Trait("Description", "MediaTypeFormatterExtensionMethods is public and static.")]
+ public void TypeIsCorrect()
+ {
+ Assert.Type.HasProperties(typeof(MediaTypeFormatterExtensions), TypeAssert.TypeProperties.IsPublicVisibleClass | TypeAssert.TypeProperties.IsStatic);
+ }
+
+ [Fact]
+ [Trait("Description", "AddQueryStringMapping(MediaTypeFormatter, string, string, MediaTypeHeaderValue) throws for null 'this'.")]
+ public void AddQueryStringMappingThrowsWithNullThis()
+ {
+ MediaTypeFormatter formatter = null;
+ Assert.ThrowsArgumentNull(() => formatter.AddQueryStringMapping("name", "value", new MediaTypeHeaderValue("application/xml")), "formatter");
+ }
+
+ [Fact]
+ [Trait("Description", "AddQueryStringMapping(MediaTypeFormatter, string, string, string) throws for null 'this'.")]
+ public void AddQueryStringMapping1ThrowsWithNullThis()
+ {
+ MediaTypeFormatter formatter = null;
+ Assert.ThrowsArgumentNull(() => formatter.AddQueryStringMapping("name", "value", "application/xml"), "formatter");
+ }
+
+ [Fact]
+ [Trait("Description", "AddUriPathExtensionMapping(MediaTypeFormatter, string, MediaTypeHeaderValue) throws for null 'this'.")]
+ public void AddUriPathExtensionMappingThrowsWithNullThis()
+ {
+ MediaTypeFormatter formatter = null;
+ Assert.ThrowsArgumentNull(() => formatter.AddUriPathExtensionMapping("xml", new MediaTypeHeaderValue("application/xml")), "formatter");
+ }
+
+ [Fact]
+ [Trait("Description", "AddUriPathExtensionMapping(MediaTypeFormatter, string, string) throws for null 'this'.")]
+ public void AddUriPathExtensionMapping1ThrowsWithNullThis()
+ {
+ MediaTypeFormatter formatter = null;
+ Assert.ThrowsArgumentNull(() => formatter.AddUriPathExtensionMapping("xml", "application/xml"), "formatter");
+ }
+
+ [Fact]
+ [Trait("Description", "AddMediaRangeMapping(MediaTypeFormatter, MediaTypeHeaderValue, MediaTypeHeaderValue) throws for null 'this'.")]
+ public void AddMediaRangeMappingThrowsWithNullThis()
+ {
+ MediaTypeFormatter formatter = null;
+ Assert.ThrowsArgumentNull(() => formatter.AddMediaRangeMapping(new MediaTypeHeaderValue("application/*"), new MediaTypeHeaderValue("application/xml")), "formatter");
+ }
+
+ [Fact]
+ [Trait("Description", "AddMediaRangeMapping(MediaTypeFormatter, string, string) throws for null 'this'.")]
+ public void AddMediaRangeMapping1ThrowsWithNullThis()
+ {
+ MediaTypeFormatter formatter = null;
+ Assert.ThrowsArgumentNull(() => formatter.AddMediaRangeMapping("application/*", "application/xml"), "formatter");
+ }
+
+
+
+ [Fact]
+ [Trait("Description", "AddRequestHeaderMapping(MediaTypeFormatter, string, string, StringComparison, bool, MediaTypeHeaderValue) throws for null 'this'.")]
+ public void AddRequestHeaderMappingThrowsWithNullThis()
+ {
+ MediaTypeFormatter formatter = null;
+ Assert.ThrowsArgumentNull(() => formatter.AddRequestHeaderMapping("name", "value", StringComparison.CurrentCulture, true, new MediaTypeHeaderValue("application/xml")), "formatter");
+ }
+
+ [Fact]
+ [Trait("Description", "AddRequestHeaderMapping(MediaTypeFormatter, string, string, StringComparison, bool, MediaTypeHeaderValue) adds formatter on 'this'.")]
+ public void AddRequestHeaderMappingAddsSuccessfully()
+ {
+ MediaTypeFormatter formatter = new MockMediaTypeFormatter();
+ Assert.Equal(0, formatter.MediaTypeMappings.Count);
+ formatter.AddRequestHeaderMapping("name", "value", StringComparison.CurrentCulture, true, new MediaTypeHeaderValue("application/xml"));
+ IEnumerable<RequestHeaderMapping> mappings = formatter.MediaTypeMappings.OfType<RequestHeaderMapping>();
+ Assert.Equal(1, mappings.Count());
+ RequestHeaderMapping mapping = mappings.ElementAt(0);
+ Assert.Equal("name", mapping.HeaderName);
+ Assert.Equal("value", mapping.HeaderValue);
+ Assert.Equal(StringComparison.CurrentCulture, mapping.HeaderValueComparison);
+ Assert.Equal(true, mapping.IsValueSubstring);
+ Assert.Equal(new MediaTypeHeaderValue("application/xml"), mapping.MediaType);
+ }
+
+ [Fact]
+ [Trait("Description", "AddRequestHeaderMapping(MediaTypeFormatter, string, string, StringComparison, bool, string) throws for null 'this'.")]
+ public void AddRequestHeaderMapping1ThrowsWithNullThis()
+ {
+ MediaTypeFormatter formatter = null;
+ Assert.ThrowsArgumentNull(() => formatter.AddRequestHeaderMapping("name", "value", StringComparison.CurrentCulture, true, "application/xml"), "formatter");
+ }
+
+ [Fact]
+ [Trait("Description", "AddRequestHeaderMapping(MediaTypeFormatter, string, string, StringComparison, bool, string) adds formatter on 'this'.")]
+ public void AddRequestHeaderMapping1AddsSuccessfully()
+ {
+ MediaTypeFormatter formatter = new MockMediaTypeFormatter();
+ Assert.Equal(0, formatter.MediaTypeMappings.Count);
+ formatter.AddRequestHeaderMapping("name", "value", StringComparison.CurrentCulture, true, "application/xml");
+ IEnumerable<RequestHeaderMapping> mappings = formatter.MediaTypeMappings.OfType<RequestHeaderMapping>();
+ Assert.Equal(1, mappings.Count());
+ RequestHeaderMapping mapping = mappings.ElementAt(0);
+ Assert.Equal("name", mapping.HeaderName);
+ Assert.Equal("value", mapping.HeaderValue);
+ Assert.Equal(StringComparison.CurrentCulture, mapping.HeaderValueComparison);
+ Assert.Equal(true, mapping.IsValueSubstring);
+ Assert.Equal(new MediaTypeHeaderValue("application/xml"), mapping.MediaType);
+ }
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/Formatting/MediaTypeFormatterTests.cs b/test/System.Net.Http.Formatting.Test.Unit/Formatting/MediaTypeFormatterTests.cs
new file mode 100644
index 00000000..f8476d5b
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/Formatting/MediaTypeFormatterTests.cs
@@ -0,0 +1,401 @@
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Net.Http.Formatting.DataSets;
+using System.Net.Http.Formatting.Mocks;
+using System.Net.Http.Headers;
+using Microsoft.TestCommon;
+using Moq;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Net.Http.Formatting
+{
+ public class MediaTypeFormatterTests
+ {
+ [Fact]
+ [Trait("Description", "MediaTypeFormatter is public, abstract, and unsealed.")]
+ public void TypeIsCorrect()
+ {
+ Assert.Type.HasProperties(typeof(MediaTypeFormatter), TypeAssert.TypeProperties.IsPublicVisibleClass | TypeAssert.TypeProperties.IsAbstract);
+ }
+
+ [Fact]
+ [Trait("Description", "MediaTypeFormatter() constructor (via derived class) sets SupportedMediaTypes and MediaTypeMappings.")]
+ public void Constructor()
+ {
+ MockMediaTypeFormatter formatter = new MockMediaTypeFormatter();
+ Collection<MediaTypeHeaderValue> supportedMediaTypes = formatter.SupportedMediaTypes;
+ Assert.NotNull(supportedMediaTypes);
+ Assert.Equal(0, supportedMediaTypes.Count);
+
+ Collection<MediaTypeMapping> mappings = formatter.MediaTypeMappings;
+ Assert.NotNull(mappings);
+ Assert.Equal(0, mappings.Count);
+ }
+
+ [Fact]
+ [Trait("Description", "SupportedMediaTypes is a mutable collection.")]
+ public void SupportedMediaTypesIsMutable()
+ {
+ MockMediaTypeFormatter formatter = new MockMediaTypeFormatter();
+ Collection<MediaTypeHeaderValue> supportedMediaTypes = formatter.SupportedMediaTypes;
+ MediaTypeHeaderValue[] mediaTypes = HttpUnitTestDataSets.LegalMediaTypeHeaderValues.ToArray();
+ foreach (MediaTypeHeaderValue mediaType in mediaTypes)
+ {
+ supportedMediaTypes.Add(mediaType);
+ }
+
+ Assert.True(mediaTypes.SequenceEqual(formatter.SupportedMediaTypes));
+ }
+
+ [Fact]
+ [Trait("Description", "SupportedMediaTypes Add throws with a null media type.")]
+ public void SupportedMediaTypesAddThrowsWithNullMediaType()
+ {
+ MockMediaTypeFormatter formatter = new MockMediaTypeFormatter();
+ Collection<MediaTypeHeaderValue> supportedMediaTypes = formatter.SupportedMediaTypes;
+
+ Assert.ThrowsArgumentNull(() => supportedMediaTypes.Add(null), "item");
+ }
+
+ [Theory]
+ [TestDataSet(typeof(HttpUnitTestDataSets), "LegalMediaRangeValues")]
+ [Trait("Description", "SupportedMediaTypes Add throws with a media range.")]
+ public void SupportedMediaTypesAddThrowsWithMediaRange(MediaTypeHeaderValue mediaType)
+ {
+ MockMediaTypeFormatter formatter = new MockMediaTypeFormatter();
+ Collection<MediaTypeHeaderValue> supportedMediaTypes = formatter.SupportedMediaTypes;
+ Assert.ThrowsArgument(() => supportedMediaTypes.Add(mediaType), "item", RS.Format(Properties.Resources.CannotUseMediaRangeForSupportedMediaType, typeof(MediaTypeHeaderValue).Name, mediaType.MediaType));
+ }
+
+ [Fact]
+ [Trait("Description", "SupportedMediaTypes Insert throws with a null media type.")]
+ public void SupportedMediaTypesInsertThrowsWithNullMediaType()
+ {
+ MockMediaTypeFormatter formatter = new MockMediaTypeFormatter();
+ Collection<MediaTypeHeaderValue> supportedMediaTypes = formatter.SupportedMediaTypes;
+
+ Assert.ThrowsArgumentNull(() => supportedMediaTypes.Insert(0, null), "item");
+ }
+
+ [Theory]
+ [TestDataSet(typeof(HttpUnitTestDataSets), "LegalMediaRangeValues")]
+ [Trait("Description", "SupportedMediaTypes Insert throws with a media range.")]
+ public void SupportedMediaTypesInsertThrowsWithMediaRange(MediaTypeHeaderValue mediaType)
+ {
+ MockMediaTypeFormatter formatter = new MockMediaTypeFormatter();
+ Collection<MediaTypeHeaderValue> supportedMediaTypes = formatter.SupportedMediaTypes;
+
+ Assert.ThrowsArgument(() => supportedMediaTypes.Insert(0, mediaType), "item", RS.Format(Properties.Resources.CannotUseMediaRangeForSupportedMediaType, typeof(MediaTypeHeaderValue).Name, mediaType.MediaType));
+ }
+
+ [Fact]
+ [Trait("Description", "MediaTypeMappings is a mutable collection.")]
+ public void MediaTypeMappingsIsMutable()
+ {
+ MockMediaTypeFormatter formatter = new MockMediaTypeFormatter();
+ Collection<MediaTypeMapping> mappings = formatter.MediaTypeMappings;
+ MediaTypeMapping[] standardMappings = HttpUnitTestDataSets.StandardMediaTypeMappings.ToArray();
+ foreach (MediaTypeMapping mapping in standardMappings)
+ {
+ mappings.Add(mapping);
+ }
+
+ Assert.True(standardMappings.SequenceEqual(formatter.MediaTypeMappings));
+ }
+
+ [Theory]
+ [TestDataSet(typeof(HttpUnitTestDataSets), "StandardMediaTypesWithQuality")]
+ [Trait("Description", "TryMatchSupportedMediaType(MediaTypeHeaderValue, out MediaTypeMatch) returns media type and quality.")]
+ public void TryMatchSupportedMediaTypeWithQuality(MediaTypeWithQualityHeaderValue mediaTypeWithQuality)
+ {
+ MockMediaTypeFormatter formatter = new MockMediaTypeFormatter();
+ MediaTypeHeaderValue mediaTypeWithoutQuality = new MediaTypeHeaderValue(mediaTypeWithQuality.MediaType);
+ formatter.SupportedMediaTypes.Add(mediaTypeWithoutQuality);
+ MediaTypeMatch match;
+ bool result = formatter.TryMatchSupportedMediaType(mediaTypeWithQuality, out match);
+ Assert.True(result, String.Format("TryMatchSupportedMediaType should have succeeded for '{0}'.", mediaTypeWithQuality));
+ Assert.NotNull(match);
+ double quality = mediaTypeWithQuality.Quality.Value;
+ Assert.Equal(quality, match.Quality);
+ Assert.NotNull(match.MediaType);
+ Assert.Equal(mediaTypeWithoutQuality.MediaType, match.MediaType.MediaType);
+ }
+
+ [Theory]
+ [TestDataSet(typeof(HttpUnitTestDataSets), "StandardMediaTypesWithQuality")]
+ [Trait("Description", "TryMatchSupportedMediaType(MediaTypeHeaderValue, out MediaTypeMatch) returns cloned media type, not original.")]
+ public void TryMatchSupportedMediaTypeReturnsClone(MediaTypeWithQualityHeaderValue mediaTypeWithQuality)
+ {
+ MockMediaTypeFormatter formatter = new MockMediaTypeFormatter();
+ MediaTypeHeaderValue mediaTypeWithoutQuality = new MediaTypeHeaderValue(mediaTypeWithQuality.MediaType);
+ formatter.SupportedMediaTypes.Add(mediaTypeWithoutQuality);
+ MediaTypeMatch match;
+ bool result = formatter.TryMatchSupportedMediaType(mediaTypeWithQuality, out match);
+
+ Assert.True(result);
+ Assert.NotNull(match);
+ Assert.NotNull(match.MediaType);
+ Assert.NotSame(mediaTypeWithoutQuality, match.MediaType);
+ }
+
+ [Theory]
+ [TestDataSet(typeof(HttpUnitTestDataSets), "MediaRangeValuesWithQuality")]
+ [Trait("Description", "TryMatchMediaTypeMapping(HttpRequestMessage, out MediaTypeMatch) returns media type and quality from media range with quality.")]
+ public void TryMatchMediaTypeMappingWithQuality(MediaTypeWithQualityHeaderValue mediaRangeWithQuality)
+ {
+ MockMediaTypeFormatter formatter = new MockMediaTypeFormatter();
+ MediaTypeHeaderValue mediaRangeWithoutQuality = new MediaTypeHeaderValue(mediaRangeWithQuality.MediaType);
+ MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue("application/xml");
+ MediaRangeMapping mapping = new MediaRangeMapping(mediaRangeWithoutQuality, mediaType);
+ formatter.MediaTypeMappings.Add(mapping);
+
+ HttpRequestMessage request = new HttpRequestMessage();
+ request.Headers.Accept.Add(mediaRangeWithQuality);
+ MediaTypeMatch match;
+ bool result = formatter.TryMatchMediaTypeMapping(request, out match);
+ Assert.True(result, String.Format("TryMatchMediaTypeMapping should have succeeded for '{0}'.", mediaRangeWithQuality));
+ Assert.NotNull(match);
+ double quality = mediaRangeWithQuality.Quality.Value;
+ Assert.Equal(quality, match.Quality);
+ Assert.NotNull(match.MediaType);
+ Assert.Equal(mediaType.MediaType, match.MediaType.MediaType);
+ }
+
+ [Theory]
+ [TestDataSet(typeof(HttpUnitTestDataSets), "MediaRangeValuesWithQuality")]
+ [Trait("Description", "TryMatchMediaTypeMapping(HttpRequestMessage, out MediaTypeMatch) returns a clone of the original media type.")]
+ public void TryMatchMediaTypeMappingClonesMediaType(MediaTypeWithQualityHeaderValue mediaRangeWithQuality)
+ {
+ MockMediaTypeFormatter formatter = new MockMediaTypeFormatter();
+ MediaTypeHeaderValue mediaRangeWithoutQuality = new MediaTypeHeaderValue(mediaRangeWithQuality.MediaType);
+ MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue("application/xml");
+ MediaRangeMapping mapping = new MediaRangeMapping(mediaRangeWithoutQuality, mediaType);
+ formatter.MediaTypeMappings.Add(mapping);
+
+ HttpRequestMessage request = new HttpRequestMessage();
+ request.Headers.Accept.Add(mediaRangeWithQuality);
+ MediaTypeMatch match;
+ formatter.TryMatchMediaTypeMapping(request, out match);
+ Assert.NotNull(match);
+ Assert.NotNull(match.MediaType);
+ Assert.NotSame(mediaType, match.MediaType);
+ }
+
+ [Fact]
+ [Trait("Description", "SelectResponseMediaType(Type, HttpRequestMessage) matches based only on type.")]
+ public void SelectResponseMediaTypeMatchesType()
+ {
+ MockMediaTypeFormatter formatter = new MockMediaTypeFormatter() { CallBase = true };
+ HttpRequestMessage request = new HttpRequestMessage();
+ ResponseMediaTypeMatch match = formatter.SelectResponseMediaType(typeof(string), request);
+
+ Assert.NotNull(match);
+ Assert.Equal(ResponseFormatterSelectionResult.MatchOnCanWriteType, match.ResponseFormatterSelectionResult);
+ Assert.Null(match.MediaTypeMatch.MediaType);
+ }
+
+ [Theory]
+ [TestDataSet(typeof(HttpUnitTestDataSets), "LegalMediaTypeHeaderValues")]
+ [Trait("Description", "SelectResponseMediaType(Type, HttpRequestMessage) matches media type from request content type.")]
+ public void SelectResponseMediaTypeMatchesRequestContentType(MediaTypeHeaderValue mediaType)
+ {
+ MockMediaTypeFormatter formatter = new MockMediaTypeFormatter() { CallBase = true };
+ formatter.SupportedMediaTypes.Add(mediaType);
+ HttpRequestMessage request = new HttpRequestMessage() { Content = new StringContent("fred") };
+ request.Content.Headers.ContentType = mediaType;
+ HttpResponseMessage response = new HttpResponseMessage() { RequestMessage = request };
+ ResponseMediaTypeMatch match = formatter.SelectResponseMediaType(typeof(string), request);
+
+ Assert.NotNull(match);
+ Assert.Equal(ResponseFormatterSelectionResult.MatchOnRequestContentType, match.ResponseFormatterSelectionResult);
+ Assert.NotNull(match.MediaTypeMatch.MediaType);
+ Assert.Equal(mediaType.MediaType, match.MediaTypeMatch.MediaType.MediaType);
+ }
+
+ [TestDataSet(typeof(HttpUnitTestDataSets), "LegalMediaTypeHeaderValues")]
+ [Trait("Description", "SelectResponseMediaType(Type, HttpRequestMessage) matches media type from response content type.")]
+ public void SelectResponseMediaTypeMatchesResponseContentType(MediaTypeHeaderValue mediaType)
+ {
+ MockMediaTypeFormatter formatter = new MockMediaTypeFormatter() { CallBase = true };
+ formatter.SupportedMediaTypes.Add(mediaType);
+ HttpRequestMessage request = new HttpRequestMessage();
+ HttpResponseMessage response = new HttpResponseMessage() { RequestMessage = request, Content = new StringContent("fred") };
+ response.Content.Headers.ContentType = mediaType;
+ ResponseMediaTypeMatch match = formatter.SelectResponseMediaType(typeof(string), request);
+
+ Assert.NotNull(match);
+ Assert.Equal(ResponseFormatterSelectionResult.MatchOnResponseContentType, match.ResponseFormatterSelectionResult);
+ Assert.NotNull(match.MediaTypeMatch.MediaType);
+ Assert.Equal(mediaType.MediaType, match.MediaTypeMatch.MediaType.MediaType);
+ }
+
+ [Theory]
+ [TestDataSet(typeof(HttpUnitTestDataSets), "StandardMediaTypesWithQuality")]
+ [Trait("Description", "SelectResponseMediaType(Type, HttpRequestMessage) matches supported media type from accept headers.")]
+ public void SelectResponseMediaTypeMatchesAcceptHeaderToSupportedMediaTypes(MediaTypeWithQualityHeaderValue mediaTypeWithQuality)
+ {
+ MockMediaTypeFormatter formatter = new MockMediaTypeFormatter() { CallBase = true };
+ MediaTypeHeaderValue mediaTypeWithoutQuality = new MediaTypeHeaderValue(mediaTypeWithQuality.MediaType);
+ formatter.SupportedMediaTypes.Add(mediaTypeWithoutQuality);
+
+ HttpRequestMessage request = new HttpRequestMessage();
+ request.Headers.Accept.Add(mediaTypeWithQuality);
+ ResponseMediaTypeMatch match = formatter.SelectResponseMediaType(typeof(string), request);
+
+ Assert.NotNull(match);
+ Assert.Equal(ResponseFormatterSelectionResult.MatchOnRequestAcceptHeader, match.ResponseFormatterSelectionResult);
+ double quality = mediaTypeWithQuality.Quality.Value;
+ Assert.Equal(quality, match.MediaTypeMatch.Quality);
+ Assert.NotNull(match.MediaTypeMatch.MediaType);
+ Assert.Equal(mediaTypeWithoutQuality.MediaType, match.MediaTypeMatch.MediaType.MediaType);
+ }
+
+ [TestDataSet(typeof(HttpUnitTestDataSets), "MediaRangeValuesWithQuality")]
+ [Trait("Description", "SelectResponseMediaType(Type, HttpRequestMessage) matches media type with quality from media type mapping.")]
+ public void SelectResponseMediaTypeMatchesAcceptHeaderWithMediaTypeMapping(MediaTypeWithQualityHeaderValue mediaRangeWithQuality)
+ {
+ MockMediaTypeFormatter formatter = new MockMediaTypeFormatter() { CallBase = true };
+ MediaTypeHeaderValue mediaRangeWithoutQuality = new MediaTypeHeaderValue(mediaRangeWithQuality.MediaType);
+ MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue("application/xml");
+ MediaRangeMapping mapping = new MediaRangeMapping(mediaRangeWithoutQuality, mediaType);
+ formatter.MediaTypeMappings.Add(mapping);
+
+ HttpRequestMessage request = new HttpRequestMessage();
+ request.Headers.Accept.Add(mediaRangeWithQuality);
+ ResponseMediaTypeMatch match = formatter.SelectResponseMediaType(typeof(string), request);
+
+ Assert.NotNull(match);
+ Assert.Equal(ResponseFormatterSelectionResult.MatchOnRequestAcceptHeaderWithMediaTypeMapping, match.ResponseFormatterSelectionResult);
+ double quality = mediaRangeWithQuality.Quality.Value;
+ Assert.Equal(quality, match.MediaTypeMatch.Quality);
+ Assert.NotNull(match.MediaTypeMatch.MediaType);
+ Assert.Equal(mediaType.MediaType, match.MediaTypeMatch.MediaType.MediaType);
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(CommonUnitTestDataSets), "RepresentativeValueAndRefTypeTestDataCollection",
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeStrings")]
+ [Trait("Description", "CanReadAs(Type, MediaTypeHeaderValue) returns true for all standard media types.")]
+ public void CanReadAsReturnsTrue(Type variationType, object testData, string mediaType)
+ {
+ MockMediaTypeFormatter formatter = new MockMediaTypeFormatter() { CallBase = true };
+ string[] legalMediaTypeStrings = HttpUnitTestDataSets.LegalMediaTypeStrings.ToArray();
+ foreach (string legalMediaType in legalMediaTypeStrings)
+ {
+ formatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue(legalMediaType));
+ }
+
+ MediaTypeHeaderValue contentType = new MediaTypeHeaderValue(mediaType);
+ Assert.True(formatter.CanReadAs(variationType, contentType));
+ }
+
+ [Fact]
+ [Trait("Description", "CanReadAs(Type, MediaTypeHeaderValue) throws with null type.")]
+ public void CanReadAsThrowsWithNullType()
+ {
+ MockMediaTypeFormatter formatter = new MockMediaTypeFormatter();
+ Assert.ThrowsArgumentNull(() => formatter.CanReadAs(type: null, mediaType: null), "type");
+ }
+
+ [Fact]
+ [Trait("Description", "CanReadAs(Type, MediaTypeHeaderValue) throws with null formatter context.")]
+ public void CanReadAsThrowsWithNullMediaType()
+ {
+ MockMediaTypeFormatter formatter = new MockMediaTypeFormatter();
+ Assert.ThrowsArgumentNull(() => formatter.CanReadAs(typeof(int), mediaType: null), "mediaType");
+ }
+
+ [Theory]
+ [TestDataSet(typeof(CommonUnitTestDataSets), "RepresentativeValueAndRefTypeTestDataCollection")]
+ [Trait("Description", "CanWriteAs(Type, MediaTypeHeaderValue, out MediaTypeHeaderValue) returns true always for supported media types.")]
+ public void CanWriteAsReturnsTrue(Type variationType, object testData)
+ {
+ MockMediaTypeFormatter formatter = new MockMediaTypeFormatter() { CallBase = true };
+ foreach (string mediaType in HttpUnitTestDataSets.LegalMediaTypeStrings)
+ {
+ formatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue(mediaType));
+ }
+
+ MediaTypeHeaderValue matchedMediaType = null;
+ Assert.True(formatter.CanWriteAs(variationType, formatter.SupportedMediaTypes[0], out matchedMediaType));
+ }
+
+ [Fact]
+ [Trait("Description", "CanWriteAs(Type, MediaTypeHeaderValue, out MediaTypeHeaderValue) throws with null content.")]
+ public void CanWriteAsThrowsWithNullContent()
+ {
+ MockMediaTypeFormatter formatter = new MockMediaTypeFormatter();
+ MediaTypeHeaderValue mediaType = null;
+ Assert.ThrowsArgumentNull(() => formatter.CanWriteAs(typeof(int), null, out mediaType), "mediaType");
+ }
+
+ [Theory]
+ [TestDataSet(typeof(CommonUnitTestDataSets), "RepresentativeValueAndRefTypeTestDataCollection")]
+ [Trait("Description", "CanWriteAs(Type, MediaTypeHeaderValue, out MediaTypeHeaderValue) returns true always for supported media types.")]
+ public void CanWriteAsUsingRequestReturnsTrue(Type variationType, object testData)
+ {
+ MockMediaTypeFormatter formatter = new MockMediaTypeFormatter() { CallBase = true };
+ foreach (string mediaType in HttpUnitTestDataSets.LegalMediaTypeStrings)
+ {
+ formatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue(mediaType));
+ }
+
+ MediaTypeHeaderValue matchedMediaType = null;
+ Assert.True(formatter.CanWriteAs(variationType, formatter.SupportedMediaTypes[0], out matchedMediaType));
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(CommonUnitTestDataSets), "RepresentativeValueAndRefTypeTestDataCollection",
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeStrings")]
+ [Trait("Description", "CanReadType(Type) base implementation returns true for all types.")]
+ public void CanReadTypeReturnsTrue(Type variationType, object testData, string mediaType)
+ {
+ MockMediaTypeFormatter formatter = new MockMediaTypeFormatter() { CallBase = true };
+ string[] legalMediaTypeStrings = HttpUnitTestDataSets.LegalMediaTypeStrings.ToArray();
+ foreach (string mediaTypeTmp in legalMediaTypeStrings)
+ {
+ formatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue(mediaTypeTmp));
+ }
+
+ // Invoke CanReadAs because it invokes CanReadType
+ Assert.True(formatter.CanReadAs(variationType, new MediaTypeHeaderValue(mediaType)));
+ }
+
+ [Theory]
+ [TestDataSet(typeof(CommonUnitTestDataSets), "RepresentativeValueAndRefTypeTestDataCollection")]
+ [Trait("Description", "CanWriteType() base implementation returns true always.")]
+ public void CanWriteTypeReturnsTrue(Type variationType, object testData)
+ {
+ MockMediaTypeFormatter formatter = new MockMediaTypeFormatter() { CallBase = true };
+ foreach (string mediaType in HttpUnitTestDataSets.LegalMediaTypeStrings)
+ {
+ formatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue(mediaType));
+ }
+
+ MediaTypeHeaderValue matchedMediaType = null;
+ Assert.True(formatter.CanWriteAs(variationType, formatter.SupportedMediaTypes[0], out matchedMediaType));
+ }
+
+ [Fact]
+ public void ReadFromStreamAsync_ThrowsNotSupportedException()
+ {
+ var formatter = new Mock<MediaTypeFormatter> { CallBase = true }.Object;
+
+ Assert.Throws<NotSupportedException>(() => formatter.ReadFromStreamAsync(null, null, null, null),
+ "The media type formatter of type 'Castle.Proxies.MediaTypeFormatterProxy' does not support reading since it does not implement the ReadFromStreamAsync method.");
+ }
+
+ [Fact]
+ public void WriteToStreamAsync_ThrowsNotSupportedException()
+ {
+ var formatter = new Mock<MediaTypeFormatter> { CallBase = true }.Object;
+
+ Assert.Throws<NotSupportedException>(() => formatter.WriteToStreamAsync(null, null, null, null, null),
+ "The media type formatter of type 'Castle.Proxies.MediaTypeFormatterProxy' does not support writing since it does not implement the WriteToStreamAsync method.");
+ }
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/Formatting/MediaTypeHeadeValueComparerTests.cs b/test/System.Net.Http.Formatting.Test.Unit/Formatting/MediaTypeHeadeValueComparerTests.cs
new file mode 100644
index 00000000..0638d462
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/Formatting/MediaTypeHeadeValueComparerTests.cs
@@ -0,0 +1,209 @@
+using System.Net.Http.Headers;
+using Xunit;
+
+namespace System.Net.Http.Formatting
+{
+ public class MediaTypeHeadeValueComparerTests
+ {
+
+ [Fact]
+ [Trait("Description", "MediaTypeHeadeValueComparer.Comparer returns same MediaTypeHeadeValueComparer instance each time.")]
+ public void Comparer_Returns_MediaTypeHeadeValueComparer()
+ {
+ MediaTypeHeaderValueComparer comparer1 = MediaTypeHeaderValueComparer.Comparer;
+ MediaTypeHeaderValueComparer comparer2 = MediaTypeHeaderValueComparer.Comparer;
+
+ Assert.NotNull(comparer1);
+ Assert.Same(comparer1, comparer2);
+ }
+
+ [Fact]
+ [Trait("Description", "MediaTypeHeadeValueComparer.Compare returns 0 for same MediaTypeHeaderValue instance.")]
+ public void Compare_Returns_0_For_Same_MediaTypeHeaderValue()
+ {
+ MediaTypeHeaderValueComparer comparer = MediaTypeHeaderValueComparer.Comparer;
+ MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue("text/xml");
+
+ Assert.Equal(0, comparer.Compare(mediaType, mediaType));
+ }
+
+ [Fact]
+ [Trait("Description", "MediaTypeHeadeValueComparer.Compare returns 0 for MediaTypeHeaderValue instances that differ only by case.")]
+ public void Compare_Returns_0_For_MediaTypeHeaderValues_Differing_Only_By_Case()
+ {
+ MediaTypeHeaderValueComparer comparer = MediaTypeHeaderValueComparer.Comparer;
+
+ MediaTypeHeaderValue mediaType1 = new MediaTypeHeaderValue("text/Xml");
+ MediaTypeHeaderValue mediaType2 = new MediaTypeHeaderValue("texT/xml");
+ Assert.Equal(0, comparer.Compare(mediaType1, mediaType2));
+
+ mediaType1 = new MediaTypeHeaderValue("application/*");
+ mediaType2 = new MediaTypeHeaderValue("APPLICATION/*");
+ Assert.Equal(0, comparer.Compare(mediaType1, mediaType2));
+
+ mediaType1 = new MediaTypeHeaderValue("*/*");
+ mediaType2 = new MediaTypeHeaderValue("*/*");
+ Assert.Equal(0, comparer.Compare(mediaType1, mediaType2));
+ }
+
+ [Fact]
+ [Trait("Description", "MediaTypeHeadeValueComparer.Compare returns 0 for MediaTypeHeaderValue instances that differ by non-q-value parameters.")]
+ public void Compare_Returns_0_For_MediaTypeHeaderValues_Differ_By_Non_Q_Parameters()
+ {
+ MediaTypeHeaderValueComparer comparer = MediaTypeHeaderValueComparer.Comparer;
+
+ MediaTypeHeaderValue mediaType1 = new MediaTypeHeaderValue("*/*");
+ mediaType1.CharSet = "someCharset";
+ mediaType1.Parameters.Add(new NameValueHeaderValue("someName", "someValue"));
+ MediaTypeHeaderValue mediaType2 = new MediaTypeHeaderValue("*/*");
+ mediaType2.CharSet = "someOtherCharset";
+ mediaType2.Parameters.Add(new NameValueHeaderValue("someName", "someOtherValue"));
+ Assert.Equal(0, comparer.Compare(mediaType1, mediaType2));
+ }
+
+ [Fact]
+ [Trait("Description", "MediaTypeHeadeValueComparer.Compare returns 0 for MediaTypeHeaderValue with the same Q when the Media types are not media ranges or are the same media ranges.")]
+ public void Compare_Returns_0_For_MediaTypeHeaderValues_With_Same_Q_Value()
+ {
+ MediaTypeHeaderValueComparer comparer = MediaTypeHeaderValueComparer.Comparer;
+
+ MediaTypeWithQualityHeaderValue mediaType1 = new MediaTypeWithQualityHeaderValue("text/xml", 0.5);
+ MediaTypeWithQualityHeaderValue mediaType2 = new MediaTypeWithQualityHeaderValue("text/xml", 0.50);
+ Assert.Equal(0, comparer.Compare(mediaType1, mediaType2));
+
+ mediaType1 = new MediaTypeWithQualityHeaderValue("text/xml", .7);
+ mediaType2 = new MediaTypeWithQualityHeaderValue("application/xml", .7);
+ Assert.Equal(0, comparer.Compare(mediaType1, mediaType2));
+
+ mediaType1 = new MediaTypeWithQualityHeaderValue("text/xml");
+ mediaType2 = new MediaTypeWithQualityHeaderValue("text/xml");
+ Assert.Equal(0, comparer.Compare(mediaType1, mediaType2));
+
+ mediaType1 = new MediaTypeWithQualityHeaderValue("text/xml");
+ mediaType2 = new MediaTypeWithQualityHeaderValue("text/plain");
+ Assert.Equal(0, comparer.Compare(mediaType1, mediaType2));
+
+ mediaType1 = new MediaTypeWithQualityHeaderValue("text/*", 0.3);
+ mediaType2 = new MediaTypeWithQualityHeaderValue("text/*", .3);
+ Assert.Equal(0, comparer.Compare(mediaType1, mediaType2));
+
+ mediaType1 = new MediaTypeWithQualityHeaderValue("*/*");
+ mediaType2 = new MediaTypeWithQualityHeaderValue("*/*");
+ Assert.Equal(0, comparer.Compare(mediaType1, mediaType2));
+
+ mediaType1 = new MediaTypeWithQualityHeaderValue("text/*", .1);
+ mediaType2 = new MediaTypeWithQualityHeaderValue("application/xml", .1);
+ Assert.Equal(0, comparer.Compare(mediaType1, mediaType2));
+ }
+
+ [Fact]
+ [Trait("Description", "MediaTypeHeadeValueComparer.Compare returns 1 if the first parameter has a smaller Q value.")]
+ public void Compare_Returns_1_If_MediaType1_Has_Smaller_Q_Value()
+ {
+ MediaTypeHeaderValueComparer comparer = MediaTypeHeaderValueComparer.Comparer;
+
+ MediaTypeWithQualityHeaderValue mediaType1 = new MediaTypeWithQualityHeaderValue("text/xml", 0.49);
+ MediaTypeWithQualityHeaderValue mediaType2 = new MediaTypeWithQualityHeaderValue("text/xml", 0.50);
+ Assert.Equal(1, comparer.Compare(mediaType1, mediaType2));
+
+ mediaType1 = new MediaTypeWithQualityHeaderValue("text/xml", .0);
+ mediaType2 = new MediaTypeWithQualityHeaderValue("application/xml", .7);
+ Assert.Equal(1, comparer.Compare(mediaType1, mediaType2));
+
+ mediaType1 = new MediaTypeWithQualityHeaderValue("text/xml", 0.9);
+ mediaType2 = new MediaTypeWithQualityHeaderValue("text/xml");
+ Assert.Equal(1, comparer.Compare(mediaType1, mediaType2));
+
+ mediaType1 = new MediaTypeWithQualityHeaderValue("text/xml", 0);
+ mediaType2 = new MediaTypeWithQualityHeaderValue("text/plain", 0.1);
+ Assert.Equal(1, comparer.Compare(mediaType1, mediaType2));
+
+ mediaType1 = new MediaTypeWithQualityHeaderValue("*/*", 0.3);
+ mediaType2 = new MediaTypeWithQualityHeaderValue("text/*", .31);
+ Assert.Equal(1, comparer.Compare(mediaType1, mediaType2));
+
+ mediaType1 = new MediaTypeWithQualityHeaderValue("text/*", 0.5);
+ mediaType2 = new MediaTypeWithQualityHeaderValue("*/*", 0.6);
+ Assert.Equal(1, comparer.Compare(mediaType1, mediaType2));
+ }
+
+ [Fact]
+ [Trait("Description", "MediaTypeHeadeValueComparer.Compare returns 1 if the Q values are the same but the first parameter is a less specific media range.")]
+ public void Compare_Returns_1_If_Q_Value_Is_Same_But_MediaType1_Is_Media_Range()
+ {
+ MediaTypeHeaderValueComparer comparer = MediaTypeHeaderValueComparer.Comparer;
+
+ MediaTypeWithQualityHeaderValue mediaType1 = new MediaTypeWithQualityHeaderValue("text/*", 0.50);
+ MediaTypeWithQualityHeaderValue mediaType2 = new MediaTypeWithQualityHeaderValue("text/xml", 0.50);
+ Assert.Equal(1, comparer.Compare(mediaType1, mediaType2));
+
+ mediaType1 = new MediaTypeWithQualityHeaderValue("*/*");
+ mediaType2 = new MediaTypeWithQualityHeaderValue("text/xml");
+ Assert.Equal(1, comparer.Compare(mediaType1, mediaType2));
+
+ mediaType1 = new MediaTypeWithQualityHeaderValue("*/*", 0.2);
+ mediaType2 = new MediaTypeWithQualityHeaderValue("text/*", 0.2);
+ Assert.Equal(1, comparer.Compare(mediaType1, mediaType2));
+
+ mediaType1 = new MediaTypeWithQualityHeaderValue("application/json", 0.2);
+ mediaType2 = new MediaTypeWithQualityHeaderValue("application/json", 0.2);
+ mediaType2.CharSet = "someCharSet";
+ Assert.Equal(1, comparer.Compare(mediaType1, mediaType2));
+ }
+
+ [Fact]
+ [Trait("Description", "MediaTypeHeadeValueComparer.Compare returns -1 if the first parameter has a larger Q value.")]
+ public void Compare_Returns_Negative_1_If_MediaType1_Has_Larger_Q_Value()
+ {
+ MediaTypeHeaderValueComparer comparer = MediaTypeHeaderValueComparer.Comparer;
+
+ MediaTypeWithQualityHeaderValue mediaType1 = new MediaTypeWithQualityHeaderValue("text/xml", 0.51);
+ MediaTypeWithQualityHeaderValue mediaType2 = new MediaTypeWithQualityHeaderValue("text/xml", 0.50);
+ Assert.Equal(-1, comparer.Compare(mediaType1, mediaType2));
+
+ mediaType1 = new MediaTypeWithQualityHeaderValue("text/xml", .7);
+ mediaType2 = new MediaTypeWithQualityHeaderValue("application/xml", .0);
+ Assert.Equal(-1, comparer.Compare(mediaType1, mediaType2));
+
+ mediaType1 = new MediaTypeWithQualityHeaderValue("text/xml");
+ mediaType2 = new MediaTypeWithQualityHeaderValue("text/xml", 0.9);
+ Assert.Equal(-1, comparer.Compare(mediaType1, mediaType2));
+
+ mediaType1 = new MediaTypeWithQualityHeaderValue("text/xml", 0.1);
+ mediaType2 = new MediaTypeWithQualityHeaderValue("text/plain", 0);
+ Assert.Equal(-1, comparer.Compare(mediaType1, mediaType2));
+
+ mediaType1 = new MediaTypeWithQualityHeaderValue("*/*", 0.31);
+ mediaType2 = new MediaTypeWithQualityHeaderValue("text/*", .30);
+ Assert.Equal(-1, comparer.Compare(mediaType1, mediaType2));
+
+ mediaType1 = new MediaTypeWithQualityHeaderValue("text/*", 0.6);
+ mediaType2 = new MediaTypeWithQualityHeaderValue("*/*", 0.5);
+ Assert.Equal(-1, comparer.Compare(mediaType1, mediaType2));
+ }
+
+ [Fact]
+ [Trait("Description", "MediaTypeHeadeValueComparer.Compare returns negative 1 if the Q values are the same but the second parameter is a less specific media range.")]
+ public void Compare_Returns_Negative_1_If_Q_Value_Is_Same_But_MediaType2_Is_Media_Range()
+ {
+ MediaTypeHeaderValueComparer comparer = MediaTypeHeaderValueComparer.Comparer;
+
+ MediaTypeWithQualityHeaderValue mediaType1 = new MediaTypeWithQualityHeaderValue("text/xml", 0.50);
+ MediaTypeWithQualityHeaderValue mediaType2 = new MediaTypeWithQualityHeaderValue("text/*", 0.50);
+ Assert.Equal(-1, comparer.Compare(mediaType1, mediaType2));
+
+ mediaType1 = new MediaTypeWithQualityHeaderValue("x/y");
+ mediaType2 = new MediaTypeWithQualityHeaderValue("*/*");
+ Assert.Equal(-1, comparer.Compare(mediaType1, mediaType2));
+
+ mediaType1 = new MediaTypeWithQualityHeaderValue("application/*", 0.2);
+ mediaType2 = new MediaTypeWithQualityHeaderValue("*/*", 0.2);
+ Assert.Equal(-1, comparer.Compare(mediaType1, mediaType2));
+
+ mediaType1 = new MediaTypeWithQualityHeaderValue("application/json", 0.2);
+ mediaType1.CharSet = "someCharSet";
+ mediaType2 = new MediaTypeWithQualityHeaderValue("application/json", 0.2);
+ Assert.Equal(-1, comparer.Compare(mediaType1, mediaType2));
+ }
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/Formatting/MediaTypeHeadeValueExtensionsTests.cs b/test/System.Net.Http.Formatting.Test.Unit/Formatting/MediaTypeHeadeValueExtensionsTests.cs
new file mode 100644
index 00000000..86b53724
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/Formatting/MediaTypeHeadeValueExtensionsTests.cs
@@ -0,0 +1,176 @@
+using System.Net.Http.Headers;
+using Xunit;
+
+namespace System.Net.Http.Formatting
+{
+ public class MediaTypeHeadeValueExtensionsTests
+ {
+
+ [Fact]
+
+
+ [Trait("Description", "MediaTypeHeadeValueExtensionMethods.IsMediaRange returns true for media ranges.")]
+ public void IsMediaRange_Returns_True_For_Media_Ranges()
+ {
+ MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue("text/*");
+ Assert.True(mediaType.IsMediaRange(), "MediaTypeHeadeValueExtensionMethods.IsMediaRange should have returned true for 'text/*'.");
+
+ mediaType = new MediaTypeHeaderValue("application/*");
+ mediaType.CharSet = "ISO-8859-1";
+ Assert.True(mediaType.IsMediaRange(), "MediaTypeHeadeValueExtensionMethods.IsMediaRange should have returned true for 'application/*'.");
+
+ mediaType = new MediaTypeHeaderValue("someType/*");
+ Assert.True(mediaType.IsMediaRange(), "MediaTypeHeadeValueExtensionMethods.IsMediaRange should have returned true for 'someType/*'.");
+
+ mediaType = new MediaTypeHeaderValue("*/*");
+ Assert.True(mediaType.IsMediaRange(), "MediaTypeHeadeValueExtensionMethods.IsMediaRange should have returned true for '*/*'.");
+ }
+
+ [Fact]
+
+
+ [Trait("Description", "MediaTypeHeadeValueExtensionMethods.IsMediaRange returns false for non-media ranges.")]
+ public void IsMediaRange_Returns_False_For_Non_Media_Ranges()
+ {
+ MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue("text/xml");
+ Assert.False(mediaType.IsMediaRange(), "MediaTypeHeadeValueExtensionMethods.IsMediaRange should have returned false for 'text/xml'.");
+
+ mediaType = new MediaTypeHeaderValue("*/someSubType");
+ Assert.False(mediaType.IsMediaRange(), "MediaTypeHeadeValueExtensionMethods.IsMediaRange should have returned true for '*/someSubType'.");
+ }
+
+
+
+ [Fact]
+
+
+ [Trait("Description", "MediaTypeHeadeValueExtensionMethods.IsWithinMediaRange returns true for media ranges.")]
+ public void IsWithinMediaRange_Returns_True_For_Media_Ranges()
+ {
+ MediaTypeHeaderValue mediaRange = new MediaTypeHeaderValue("text/*");
+ MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue("text/xml");
+ Assert.True(mediaType.IsWithinMediaRange(mediaRange), "MediaTypeHeadeValueExtensionMethods.IsWithinMediaRange should have returned true for 'text/*'.");
+
+ mediaRange = new MediaTypeHeaderValue("*/*");
+ Assert.True(mediaType.IsWithinMediaRange(mediaRange), "MediaTypeHeadeValueExtensionMethods.IsWithinMediaRange should have returned true for '*/*'.");
+ }
+
+ [Fact]
+
+
+ [Trait("Description", "MediaTypeHeadeValueExtensionMethods.IsWithinMediaRange returns true for media ranges regardless of case.")]
+ public void IsWithinMediaRange_Returns_True_For_Media_Ranges_Regardless_Of_Case()
+ {
+ MediaTypeHeaderValue mediaRange = new MediaTypeHeaderValue("Text/*");
+ MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue("texT/xml");
+ Assert.True(mediaType.IsWithinMediaRange(mediaRange), "MediaTypeHeadeValueExtensionMethods.IsWithinMediaRange should have returned true for 'text/*'.");
+ }
+
+ [Fact]
+
+
+ [Trait("Description", "MediaTypeHeadeValueExtensionMethods.IsWithinMediaRange returns true when the media type is equaivalent to the media range.")]
+ public void IsWithinMediaRange_Returns_True_For_Media_Types_Equaivalent_To_The_Media_Range()
+ {
+ MediaTypeHeaderValue mediaRange = new MediaTypeHeaderValue("application/xml");
+ MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue("application/xml");
+ Assert.True(mediaType.IsWithinMediaRange(mediaRange), "MediaTypeHeadeValueExtensionMethods.IsWithinMediaRange should have returned true for 'application/xml'.");
+ }
+
+ [Fact]
+
+
+ [Trait("Description", "MediaTypeHeadeValueExtensionMethods.IsWithinMediaRange returns true when the media type is a media range equaivalent to the given media range.")]
+ public void IsWithinMediaRange_Returns_True_For_Media_Types_That_Are_Equaivalent_Media_Ranges()
+ {
+ MediaTypeHeaderValue mediaRange = new MediaTypeHeaderValue("text/*");
+ MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue("text/*");
+ Assert.True(mediaType.IsWithinMediaRange(mediaRange), "MediaTypeHeadeValueExtensionMethods.IsWithinMediaRange should have returned true for 'text/*'.");
+ }
+
+ [Fact]
+
+
+ [Trait("Description", "MediaTypeHeadeValueExtensionMethods.IsWithinMediaRange returns true when the media type is a media range more specific than the given media range.")]
+ public void IsWithinMediaRange_Returns_True_For_Media_Types_More_Specific_Than_The_Media_Range()
+ {
+ MediaTypeHeaderValue mediaRange = new MediaTypeHeaderValue("*/*");
+ MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue("text/*");
+ Assert.True(mediaType.IsWithinMediaRange(mediaRange), "MediaTypeHeadeValueExtensionMethods.IsWithinMediaRange should have returned true for '*/*'.");
+ }
+
+ [Fact]
+
+
+ [Trait("Description", "MediaTypeHeadeValueExtensionMethods.IsWithinMediaRange returns true when a charset is given.")]
+ public void IsWithinMediaRange_Returns_True_For_Media_Types_With_Charset()
+ {
+ MediaTypeHeaderValue mediaRange = new MediaTypeHeaderValue("text/*");
+ MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue("text/xml");
+ mediaType.CharSet = "US-ASCII";
+ Assert.True(mediaType.IsWithinMediaRange(mediaRange), "MediaTypeHeadeValueExtensionMethods.IsWithinMediaRange should have returned true for 'text/*'.");
+ }
+
+ [Fact]
+
+
+ [Trait("Description", "MediaTypeHeadeValueExtensionMethods.IsWithinMediaRange returns true when the same charset is given for both the media type and the media range.")]
+ public void IsWithinMediaRange_Returns_True_For_Media_Types_With_Charset_And_Media_Ranges_With_Same_Charset()
+ {
+ MediaTypeHeaderValue mediaRange = new MediaTypeHeaderValue("text/*");
+ MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue("text/xml");
+ mediaType.CharSet = "US-ASCII";
+ mediaRange.CharSet = "US-ASCII";
+ Assert.True(mediaType.IsWithinMediaRange(mediaRange), "MediaTypeHeadeValueExtensionMethods.IsWithinMediaRange should have returned true for 'text/*'.");
+ }
+
+ [Fact]
+
+
+ [Trait("Description", "MediaTypeHeadeValueExtensionMethods.IsWithinMediaRange returns true regardless if the media range has a charset.")]
+ public void IsWithinMediaRange_Returns_True_Regardless_Of_Media_Ranges_With_Charset()
+ {
+ MediaTypeHeaderValue mediaRange = new MediaTypeHeaderValue("text/*");
+ MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue("text/xml");
+ mediaRange.CharSet = "US-ASCII";
+ Assert.True(mediaType.IsWithinMediaRange(mediaRange), "MediaTypeHeadeValueExtensionMethods.IsWithinMediaRange should have returned true for 'text/*' even if the media range has a charset.");
+ }
+
+ [Fact]
+
+
+ [Trait("Description", "MediaTypeHeadeValueExtensionMethods.IsMediaRange returns false when the media type and media range have different types.")]
+ public void IsWithinMediaRange_Returns_False_When_Type_Is_Different()
+ {
+ MediaTypeHeaderValue mediaRange = new MediaTypeHeaderValue("text/*");
+ MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue("application/xml");
+ Assert.False(mediaType.IsWithinMediaRange(mediaRange), "MediaTypeHeadeValueExtensionMethods.IsWithinMediaRange should have returned false for 'text/*' because the media type is 'application/xml'.");
+ }
+
+ [Fact]
+
+
+ [Trait("Description", "MediaTypeHeadeValueExtensionMethods.IsMediaRange returns false when the media type and media range have different sub types.")]
+ public void IsWithinMediaRange_Returns_False_When_SubType_Is_Different()
+ {
+ MediaTypeHeaderValue mediaRange = new MediaTypeHeaderValue("application/json");
+ MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue("application/xml");
+ Assert.False(mediaType.IsWithinMediaRange(mediaRange), "MediaTypeHeadeValueExtensionMethods.IsWithinMediaRange should have returned false because of the different sub types.");
+ }
+
+ [Fact]
+
+
+ [Trait("Description", "MediaTypeHeadeValueExtensionMethods.IsMediaRange returns false when the media type and media range have different charsets.")]
+ public void IsWithinMediaRange_Returns_False_When_Charset_Is_Different()
+ {
+ MediaTypeHeaderValue mediaRange = new MediaTypeHeaderValue("application/xml");
+ MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue("application/xml");
+ mediaType.CharSet = "US-ASCII";
+ mediaRange.CharSet = "OtherCharSet";
+ Assert.False(mediaType.IsWithinMediaRange(mediaRange), "MediaTypeHeadeValueExtensionMethods.IsWithinMediaRange should have returned false because of the different charsets.");
+ }
+
+
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/Formatting/MediaTypeHeaderValueEqualityComparerTests.cs b/test/System.Net.Http.Formatting.Test.Unit/Formatting/MediaTypeHeaderValueEqualityComparerTests.cs
new file mode 100644
index 00000000..cba890d3
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/Formatting/MediaTypeHeaderValueEqualityComparerTests.cs
@@ -0,0 +1,199 @@
+using System.Net.Http.Formatting.DataSets;
+using System.Net.Http.Headers;
+using Microsoft.TestCommon;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Net.Http.Formatting
+{
+ public class MediaTypeHeaderValueEqualityComparerTests
+ {
+
+ [Fact]
+ [Trait("Description", "MediaTypeHeaderValueEqualityComparer is internal, concrete, and not sealed.")]
+ public void TypeIsCorrect()
+ {
+ Assert.Type.HasProperties(typeof(MediaTypeHeaderValueEqualityComparer), TypeAssert.TypeProperties.IsClass);
+ }
+
+ [Fact]
+ [Trait("Description", "EqualityComparer returns same MediaTypeHeadeValueEqualityComparer instance each time.")]
+ public void EqualityComparerReturnsMediaTypeHeadeValueEqualityComparer()
+ {
+ MediaTypeHeaderValueEqualityComparer comparer1 = MediaTypeHeaderValueEqualityComparer.EqualityComparer;
+ MediaTypeHeaderValueEqualityComparer comparer2 = MediaTypeHeaderValueEqualityComparer.EqualityComparer;
+
+ Assert.NotNull(comparer1);
+ Assert.Same(comparer1, comparer2);
+ }
+
+ [Fact]
+ [Trait("Description", "GetHashCode(MediaTypeHeaderValue) returns the same hash code for media types that differe only be case.")]
+ public void GetHashCodeReturnsSameHashCodeRegardlessOfCase()
+ {
+ MediaTypeHeaderValueEqualityComparer comparer = MediaTypeHeaderValueEqualityComparer.EqualityComparer;
+
+ MediaTypeHeaderValue mediaType1 = new MediaTypeHeaderValue("text/xml");
+ MediaTypeHeaderValue mediaType2 = new MediaTypeHeaderValue("TEXT/xml");
+ Assert.Equal(comparer.GetHashCode(mediaType1), comparer.GetHashCode(mediaType2));
+
+ mediaType1 = new MediaTypeHeaderValue("text/*");
+ mediaType2 = new MediaTypeHeaderValue("TEXT/*");
+ Assert.Equal(comparer.GetHashCode(mediaType1), comparer.GetHashCode(mediaType2));
+
+ mediaType1 = new MediaTypeHeaderValue("*/*");
+ mediaType2 = new MediaTypeHeaderValue("*/*");
+ Assert.Equal(comparer.GetHashCode(mediaType1), comparer.GetHashCode(mediaType2));
+ }
+
+
+ [Fact]
+ [Trait("Description", "GetHashCode(MediaTypeHeaderValue) returns different hash codes if the media types are different.")]
+ public void GetHashCodeReturnsDifferentHashCodeForDifferentMediaType()
+ {
+ MediaTypeHeaderValueEqualityComparer comparer = MediaTypeHeaderValueEqualityComparer.EqualityComparer;
+
+ MediaTypeHeaderValue mediaType1 = new MediaTypeHeaderValue("text/*");
+ MediaTypeHeaderValue mediaType2 = new MediaTypeHeaderValue("TEXT/xml");
+ Assert.NotEqual(comparer.GetHashCode(mediaType1), comparer.GetHashCode(mediaType2));
+
+ mediaType1 = new MediaTypeHeaderValue("application/*");
+ mediaType2 = new MediaTypeHeaderValue("TEXT/*");
+ Assert.NotEqual(comparer.GetHashCode(mediaType1), comparer.GetHashCode(mediaType2));
+
+ mediaType1 = new MediaTypeHeaderValue("application/*");
+ mediaType2 = new MediaTypeHeaderValue("*/*");
+ Assert.NotEqual(comparer.GetHashCode(mediaType1), comparer.GetHashCode(mediaType2));
+ }
+
+
+ [Fact]
+ [Trait("Description", "Equals(MediaTypeHeaderValue, MediaTypeHeaderValue) returns true if media type 1 is a subset of 2.")]
+ public void EqualsReturnsTrueIfMediaType1IsSubset()
+ {
+ string[] parameters = new string[]
+ {
+ ";name=value",
+ ";q=1.0",
+ ";version=1",
+ };
+
+ MediaTypeHeaderValueEqualityComparer comparer = MediaTypeHeaderValueEqualityComparer.EqualityComparer;
+
+ MediaTypeHeaderValue mediaType1 = new MediaTypeHeaderValue("text/*");
+ mediaType1.CharSet = "someCharset";
+ MediaTypeHeaderValue mediaType2 = new MediaTypeHeaderValue("TEXT/*");
+ mediaType2.CharSet = "SOMECHARSET";
+ Assert.Equal(mediaType1, mediaType2, comparer);
+
+ mediaType1 = new MediaTypeHeaderValue("application/*");
+ mediaType1.CharSet = "";
+ mediaType2 = new MediaTypeHeaderValue("application/*");
+ mediaType2.CharSet = null;
+ Assert.Equal(mediaType1, mediaType2, comparer);
+
+ foreach (string parameter in parameters)
+ {
+ mediaType1 = new MediaTypeHeaderValue("text/xml");
+ mediaType2 = MediaTypeHeaderValue.Parse("TEXT/xml" + parameter);
+ Assert.Equal(mediaType1, mediaType2, comparer);
+
+ mediaType1 = new MediaTypeHeaderValue("text/*");
+ mediaType2 = MediaTypeHeaderValue.Parse("TEXT/*" + parameter);
+ Assert.Equal(mediaType1, mediaType2, comparer);
+
+ mediaType1 = new MediaTypeHeaderValue("*/*");
+ mediaType2 = MediaTypeHeaderValue.Parse("*/*" + parameter);
+ Assert.Equal(mediaType1, mediaType2, comparer);
+ }
+ }
+
+ [Fact]
+ [Trait("Description", "Equals(MediaTypeHeaderValue, MediaTypeHeaderValue) returns false if media type 1 is a superset of 2.")]
+ public void EqualsReturnsFalseIfMediaType1IsSuperset()
+ {
+ string[] parameters = new string[]
+ {
+ ";name=value",
+ ";q=1.0",
+ ";version=1",
+ };
+
+ MediaTypeHeaderValueEqualityComparer comparer = MediaTypeHeaderValueEqualityComparer.EqualityComparer;
+
+ foreach (string parameter in parameters)
+ {
+ MediaTypeHeaderValue mediaType1 = MediaTypeHeaderValue.Parse("text/xml" + parameter);
+ MediaTypeHeaderValue mediaType2 = new MediaTypeHeaderValue("TEXT/xml");
+ Assert.NotEqual(mediaType1, mediaType2, comparer);
+
+ mediaType1 = MediaTypeHeaderValue.Parse("text/*" + parameter);
+ mediaType2 = new MediaTypeHeaderValue("TEXT/*");
+ Assert.NotEqual(mediaType1, mediaType2, comparer);
+
+ mediaType1 = MediaTypeHeaderValue.Parse("*/*" + parameter);
+ mediaType2 = new MediaTypeHeaderValue("*/*");
+ Assert.NotEqual(mediaType1, mediaType2, comparer);
+ }
+ }
+
+ [Fact]
+ [Trait("Description", "Equals(MediaTypeHeaderValue, MediaTypeHeaderValue) returns true if media types and charsets differ only by case.")]
+ public void Equals1ReturnsTrueIfMediaTypesDifferOnlyByCase()
+ {
+ MediaTypeHeaderValueEqualityComparer comparer = MediaTypeHeaderValueEqualityComparer.EqualityComparer;
+
+ MediaTypeHeaderValue mediaType1 = new MediaTypeHeaderValue("text/xml");
+ MediaTypeHeaderValue mediaType2 = new MediaTypeHeaderValue("TEXT/xml");
+ Assert.Equal(mediaType1, mediaType2, comparer);
+
+ mediaType1 = new MediaTypeHeaderValue("text/*");
+ mediaType2 = new MediaTypeHeaderValue("TEXT/*");
+ Assert.Equal(mediaType1, mediaType2, comparer);
+
+ mediaType1 = new MediaTypeHeaderValue("*/*");
+ mediaType2 = new MediaTypeHeaderValue("*/*");
+ Assert.Equal(mediaType1, mediaType2, comparer);
+
+ mediaType1 = new MediaTypeHeaderValue("text/*");
+ mediaType1.CharSet = "someCharset";
+ mediaType2 = new MediaTypeHeaderValue("TEXT/*");
+ mediaType2.CharSet = "SOMECHARSET";
+ Assert.Equal(mediaType1, mediaType2, comparer);
+
+ mediaType1 = new MediaTypeHeaderValue("application/*");
+ mediaType1.CharSet = "";
+ mediaType2 = new MediaTypeHeaderValue("application/*");
+ mediaType2.CharSet = null;
+ Assert.Equal(mediaType1, mediaType2, comparer);
+ }
+
+ [Fact]
+ [Trait("Description", "Equals(MediaTypeHeaderValue, MediaTypeHeaderValue) returns false if media types and charsets differ by more than case.")]
+ public void EqualsReturnsFalseIfMediaTypesDifferByMoreThanCase()
+ {
+ MediaTypeHeaderValueEqualityComparer comparer = MediaTypeHeaderValueEqualityComparer.EqualityComparer;
+
+ MediaTypeHeaderValue mediaType1 = new MediaTypeHeaderValue("text/xml");
+ MediaTypeHeaderValue mediaType2 = new MediaTypeHeaderValue("TEST/xml");
+ Assert.NotEqual(mediaType1, mediaType2, comparer);
+
+ mediaType1 = new MediaTypeHeaderValue("text/*");
+ mediaType1.CharSet = "someCharset";
+ mediaType2 = new MediaTypeHeaderValue("TEXT/*");
+ mediaType2.CharSet = "SOMEOTHERCHARSET";
+ Assert.NotEqual(mediaType1, mediaType2, comparer);
+ }
+
+ [Theory]
+ [TestDataSet(typeof(HttpUnitTestDataSets), "StandardMediaTypesWithQuality")]
+ [Trait("Description", "Equals(MediaTypeHeaderValue, MediaTypeHeaderValue) returns true if media types differ only in quality.")]
+ public void EqualsReturnsTrueIfMediaTypesDifferOnlyByQuality(MediaTypeWithQualityHeaderValue mediaType1)
+ {
+ MediaTypeHeaderValueEqualityComparer comparer = MediaTypeHeaderValueEqualityComparer.EqualityComparer;
+ MediaTypeHeaderValue mediaType2 = new MediaTypeHeaderValue(mediaType1.MediaType);
+ Assert.Equal(mediaType2, mediaType1, comparer);
+ }
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/Formatting/ParsedMediaTypeHeaderValueTests.cs b/test/System.Net.Http.Formatting.Test.Unit/Formatting/ParsedMediaTypeHeaderValueTests.cs
new file mode 100644
index 00000000..64450302
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/Formatting/ParsedMediaTypeHeaderValueTests.cs
@@ -0,0 +1,168 @@
+using System.Net.Http.Headers;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Net.Http.Formatting
+{
+ public class ParsedMediaTypeHeadeValueTests
+ {
+ [Fact]
+ [Trait("Description", "MediaTypeHeaderValue ensures only valid media types are constructed.")]
+ public void MediaTypeHeaderValue_Ensures_Valid_MediaType()
+ {
+ string[] invalidMediaTypes = new string[] { "", " ", "\n", "\t", "text", "text/", "text\\", "\\", "//", "text/[", "text/ ", " text/", " text/ ", "text\\ ", " text\\", " text\\ ", "text\\xml", "text//xml" };
+
+ foreach (string invalidMediaType in invalidMediaTypes)
+ {
+ Assert.Throws<Exception>(() => new MediaTypeHeaderValue(invalidMediaType), exceptionMessage: null, allowDerivedExceptions: true);
+ }
+ }
+
+ [Fact]
+ [Trait("Description", "ParsedMediaTypeHeadeValue.Type returns the media type.")]
+ public void Type_Returns_Just_The_Type()
+ {
+ MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue("text/xml");
+ ParsedMediaTypeHeaderValue parsedMediaType = new ParsedMediaTypeHeaderValue(mediaType);
+ Assert.Equal("text", parsedMediaType.Type);
+
+ mediaType = new MediaTypeHeaderValue("text/*");
+ parsedMediaType = new ParsedMediaTypeHeaderValue(mediaType);
+ Assert.Equal("text", parsedMediaType.Type);
+
+ mediaType = new MediaTypeHeaderValue("*/*");
+ parsedMediaType = new ParsedMediaTypeHeaderValue(mediaType);
+ Assert.Equal("*", parsedMediaType.Type);
+ }
+
+ [Fact]
+ [Trait("Description", "ParsedMediaTypeHeadeValue.SubType returns the media sub-type.")]
+ public void SubType_Returns_Just_The_Sub_Type()
+ {
+ MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue("text/xml");
+ ParsedMediaTypeHeaderValue parsedMediaType = new ParsedMediaTypeHeaderValue(mediaType);
+ Assert.Equal("xml", parsedMediaType.SubType);
+
+ mediaType = new MediaTypeHeaderValue("text/*");
+ parsedMediaType = new ParsedMediaTypeHeaderValue(mediaType);
+ Assert.Equal("*", parsedMediaType.SubType);
+
+ mediaType = new MediaTypeHeaderValue("*/*");
+ parsedMediaType = new ParsedMediaTypeHeaderValue(mediaType);
+ Assert.Equal("*", parsedMediaType.SubType);
+ }
+
+ [Fact]
+ [Trait("Description", "ParsedMediaTypeHeadeValue.IsSubTypeMediaRange returns true for media ranges.")]
+ public void IsSubTypeMediaRange_Returns_True_For_Media_Ranges()
+ {
+ MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue("text/*");
+ ParsedMediaTypeHeaderValue parsedMediaType = new ParsedMediaTypeHeaderValue(mediaType);
+ Assert.True(parsedMediaType.IsSubTypeMediaRange, "ParsedMediaTypeHeadeValue.IsSubTypeMediaRange should have returned true.");
+
+ mediaType = new MediaTypeHeaderValue("*/*");
+ parsedMediaType = new ParsedMediaTypeHeaderValue(mediaType);
+ Assert.True(parsedMediaType.IsSubTypeMediaRange, "ParsedMediaTypeHeadeValue.IsSubTypeMediaRange should have returned true.");
+ }
+
+ [Fact]
+ [Trait("Description", "ParsedMediaTypeHeadeValue.IsAllMediaRange returns true only when both the type and subtype are wildcard characters.")]
+ public void IsAllMediaRange_Returns_True_Only_When_Type_And_SubType_Are_Wildcards()
+ {
+ MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue("text/*");
+ ParsedMediaTypeHeaderValue parsedMediaType = new ParsedMediaTypeHeaderValue(mediaType);
+ Assert.False(parsedMediaType.IsAllMediaRange, "ParsedMediaTypeHeadeValue.IsAllMediaRange should have returned false.");
+
+ mediaType = new MediaTypeHeaderValue("*/*");
+ parsedMediaType = new ParsedMediaTypeHeaderValue(mediaType);
+ Assert.True(parsedMediaType.IsAllMediaRange, "ParsedMediaTypeHeadeValue.IsAllMediaRange should have returned true.");
+
+ mediaType = new MediaTypeHeaderValue("*/xml");
+ parsedMediaType = new ParsedMediaTypeHeaderValue(mediaType);
+ Assert.False(parsedMediaType.IsAllMediaRange, "ParsedMediaTypeHeadeValue.IsAllMediaRange should have returned false.");
+ }
+
+ [Fact]
+
+
+ [Trait("Description", "ParsedMediaTypeHeadeValue.QualityFactor always returns 1.0 for MediaTypeHeaderValue.")]
+ public void QualityFactor_Returns_1_For_MediaTypeHeaderValue()
+ {
+ MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue("text/*");
+ ParsedMediaTypeHeaderValue parsedMediaType = new ParsedMediaTypeHeaderValue(mediaType);
+ Assert.Equal(1.0, parsedMediaType.QualityFactor);
+
+ mediaType = new MediaTypeHeaderValue("*/*");
+ parsedMediaType = new ParsedMediaTypeHeaderValue(mediaType);
+ Assert.Equal(1.0, parsedMediaType.QualityFactor);
+
+ mediaType = new MediaTypeHeaderValue("application/xml");
+ parsedMediaType = new ParsedMediaTypeHeaderValue(mediaType);
+ Assert.Equal(1.0, parsedMediaType.QualityFactor);
+
+ mediaType = new MediaTypeHeaderValue("application/xml");
+ mediaType.Parameters.Add(new NameValueHeaderValue("q", "0.5"));
+ parsedMediaType = new ParsedMediaTypeHeaderValue(mediaType);
+ Assert.Equal(1.0, parsedMediaType.QualityFactor);
+ }
+
+ [Fact]
+
+
+ [Trait("Description", "ParsedMediaTypeHeadeValue.QualityFactor returns q value given by MediaTypeWithQualityHeaderValue.")]
+ public void QualityFactor_Returns_Q_Value_For_MediaTypeWithQualityHeaderValue()
+ {
+ MediaTypeHeaderValue mediaType = new MediaTypeWithQualityHeaderValue("text/*", 0.5);
+ ParsedMediaTypeHeaderValue parsedMediaType = new ParsedMediaTypeHeaderValue(mediaType);
+ Assert.Equal(0.5, parsedMediaType.QualityFactor);
+
+ mediaType = new MediaTypeWithQualityHeaderValue("*/*", 0.0);
+ parsedMediaType = new ParsedMediaTypeHeaderValue(mediaType);
+ Assert.Equal(0.0, parsedMediaType.QualityFactor);
+
+ mediaType = new MediaTypeWithQualityHeaderValue("application/xml", 1.0);
+ parsedMediaType = new ParsedMediaTypeHeaderValue(mediaType);
+ Assert.Equal(1.0, parsedMediaType.QualityFactor);
+
+ mediaType = new MediaTypeWithQualityHeaderValue("application/xml");
+ parsedMediaType = new ParsedMediaTypeHeaderValue(mediaType);
+ Assert.Equal(1.0, parsedMediaType.QualityFactor);
+
+ mediaType = new MediaTypeWithQualityHeaderValue("application/xml");
+ mediaType.Parameters.Add(new NameValueHeaderValue("q", "0.5"));
+ parsedMediaType = new ParsedMediaTypeHeaderValue(mediaType);
+ Assert.Equal(0.5, parsedMediaType.QualityFactor);
+
+ MediaTypeWithQualityHeaderValue mediaTypeWithQuality = new MediaTypeWithQualityHeaderValue("application/xml");
+ mediaTypeWithQuality.Quality = 0.2;
+ parsedMediaType = new ParsedMediaTypeHeaderValue(mediaTypeWithQuality);
+ Assert.Equal(0.2, parsedMediaType.QualityFactor);
+ }
+
+ [Fact]
+
+
+ [Trait("Description", "ParsedMediaTypeHeadeValue.CharSet is just the value of the CharSet from the MediaTypeHeaderValue.")]
+ public void CharSet_Is_CharSet_Of_MediaTypeHeaderValue()
+ {
+ MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue("application/*");
+ ParsedMediaTypeHeaderValue parsedMediaType = new ParsedMediaTypeHeaderValue(mediaType);
+ Assert.Null(parsedMediaType.CharSet);
+
+ mediaType = new MediaTypeHeaderValue("application/*");
+ mediaType.CharSet = "";
+ parsedMediaType = new ParsedMediaTypeHeaderValue(mediaType);
+ Assert.Null(parsedMediaType.CharSet);
+
+ mediaType = new MediaTypeHeaderValue("application/xml");
+ mediaType.CharSet = "someCharSet";
+ parsedMediaType = new ParsedMediaTypeHeaderValue(mediaType);
+ Assert.Equal("someCharSet", parsedMediaType.CharSet);
+
+ mediaType = new MediaTypeHeaderValue("text/xml");
+ mediaType.CharSet = "someCharSet";
+ parsedMediaType = new ParsedMediaTypeHeaderValue(mediaType);
+ Assert.Equal("someCharSet", parsedMediaType.CharSet);
+ }
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/Formatting/Parsers/FormUrlEncodedParserTests.cs b/test/System.Net.Http.Formatting.Test.Unit/Formatting/Parsers/FormUrlEncodedParserTests.cs
new file mode 100644
index 00000000..de09caed
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/Formatting/Parsers/FormUrlEncodedParserTests.cs
@@ -0,0 +1,176 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Text;
+using Microsoft.TestCommon;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Net.Http.Formatting.Parsers
+{
+ public class FormUrlEncodedParserTests
+ {
+ private const int MinMessageSize = 1;
+ private const int Iterations = 16;
+
+ internal static Collection<KeyValuePair<string, string>> CreateCollection()
+ {
+ return new Collection<KeyValuePair<string, string>>();
+ }
+
+ internal static FormUrlEncodedParser CreateParser(int maxMessageSize, out ICollection<KeyValuePair<string, string>> nameValuePairs)
+ {
+ nameValuePairs = CreateCollection();
+ return new FormUrlEncodedParser(nameValuePairs, maxMessageSize);
+ }
+
+ internal static byte[] CreateBuffer(params string[] nameValuePairs)
+ {
+ StringBuilder buffer = new StringBuilder();
+ bool first = true;
+ foreach (var h in nameValuePairs)
+ {
+ if (first)
+ {
+ first = false;
+ }
+ else
+ {
+ buffer.Append('&');
+ }
+
+ buffer.Append(h);
+ }
+
+ return Encoding.UTF8.GetBytes(buffer.ToString());
+ }
+
+ internal static ParserState ParseBufferInSteps(FormUrlEncodedParser parser, byte[] buffer, int readsize, out int totalBytesConsumed)
+ {
+ ParserState state = ParserState.Invalid;
+ totalBytesConsumed = 0;
+ while (totalBytesConsumed <= buffer.Length)
+ {
+ int size = Math.Min(buffer.Length - totalBytesConsumed, readsize);
+ byte[] parseBuffer = new byte[size];
+ Buffer.BlockCopy(buffer, totalBytesConsumed, parseBuffer, 0, size);
+
+ int bytesConsumed = 0;
+ state = parser.ParseBuffer(parseBuffer, parseBuffer.Length, ref bytesConsumed, totalBytesConsumed == buffer.Length - size);
+ totalBytesConsumed += bytesConsumed;
+
+ if (state != ParserState.NeedMoreData)
+ {
+ return state;
+ }
+ }
+
+ return state;
+ }
+
+ [Fact]
+ public void TypeIsCorrect()
+ {
+ Assert.Type.HasProperties<FormUrlEncodedParser>(TypeAssert.TypeProperties.IsClass);
+ }
+
+ [Fact]
+ public void FormUrlEncodedParserThrowsOnNull()
+ {
+ Assert.ThrowsArgumentNull(() => { new FormUrlEncodedParser(null, ParserData.MinHeaderSize); }, "nameValuePairs");
+ }
+
+ [Fact]
+ public void FormUrlEncodedParserThrowsOnInvalidSize()
+ {
+ Assert.ThrowsArgument(() => { new FormUrlEncodedParser(CreateCollection(), MinMessageSize - 1); }, "maxMessageSize");
+
+ FormUrlEncodedParser parser = new FormUrlEncodedParser(CreateCollection(), MinMessageSize);
+ Assert.NotNull(parser);
+
+ parser = new FormUrlEncodedParser(CreateCollection(), MinMessageSize + 1);
+ Assert.NotNull(parser);
+ }
+
+ [Fact]
+ public void ParseBufferThrowsOnNullBuffer()
+ {
+ ICollection<KeyValuePair<string, string>> collection;
+ FormUrlEncodedParser parser = CreateParser(128, out collection);
+ int bytesConsumed = 0;
+ Assert.ThrowsArgumentNull(() => { parser.ParseBuffer(null, 0, ref bytesConsumed, false); }, "buffer");
+ }
+
+ [Fact]
+ public void ParseBufferHandlesEmptyBuffer()
+ {
+ byte[] data = CreateBuffer();
+ ICollection<KeyValuePair<string, string>> collection;
+ FormUrlEncodedParser parser = CreateParser(MinMessageSize, out collection);
+
+ int bytesConsumed = 0;
+ ParserState state = parser.ParseBuffer(data, data.Length, ref bytesConsumed, true);
+ Assert.Equal(ParserState.Done, state);
+ Assert.Equal(data.Length, bytesConsumed);
+ Assert.Equal(0, collection.Count());
+ }
+
+ public static TheoryDataSet<string, string, string> UriQueryData
+ {
+ get
+ {
+ return UriQueryTestData.UriQueryData;
+ }
+ }
+
+ [Theory]
+ [InlineData("N", null, "N")]
+ [InlineData("%26", null, "&")]
+ [PropertyData("UriQueryData")]
+ public void ParseBufferCorrectly(string segment, string name, string value)
+ {
+ for (int index = 1; index < Iterations; index++)
+ {
+ List<string> segments = new List<string>();
+ for (int cnt = 0; cnt < index; cnt++)
+ {
+ segments.Add(segment);
+ }
+
+ byte[] data = CreateBuffer(segments.ToArray());
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ ICollection<KeyValuePair<string, string>> collection;
+ FormUrlEncodedParser parser = CreateParser(data.Length + 1, out collection);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed;
+ ParserState state = ParseBufferInSteps(parser, data, cnt, out totalBytesConsumed);
+ Assert.Equal(ParserState.Done, state);
+ Assert.Equal(data.Length, totalBytesConsumed);
+
+ Assert.Equal(index, collection.Count());
+ foreach (KeyValuePair<string, string> element in collection)
+ {
+ Assert.Equal(name, element.Key);
+ Assert.Equal(value, element.Value);
+ }
+ }
+ }
+ }
+
+ [Fact]
+ public void HeaderParserDataTooBig()
+ {
+ byte[] data = CreateBuffer("N=V");
+ ICollection<KeyValuePair<string, string>> collection;
+ FormUrlEncodedParser parser = CreateParser(MinMessageSize, out collection);
+
+ int bytesConsumed = 0;
+ ParserState state = parser.ParseBuffer(data, data.Length, ref bytesConsumed, true);
+ Assert.Equal(ParserState.DataTooBig, state);
+ Assert.Equal(MinMessageSize, bytesConsumed);
+ }
+ }
+} \ No newline at end of file
diff --git a/test/System.Net.Http.Formatting.Test.Unit/Formatting/Parsers/HttpRequestHeaderParserTests.cs b/test/System.Net.Http.Formatting.Test.Unit/Formatting/Parsers/HttpRequestHeaderParserTests.cs
new file mode 100644
index 00000000..1f8188c4
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/Formatting/Parsers/HttpRequestHeaderParserTests.cs
@@ -0,0 +1,273 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http.Formatting.DataSets;
+using System.Text;
+using Microsoft.TestCommon;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Net.Http.Formatting.Parsers
+{
+ public class HttpRequestHeaderParserTests
+ {
+ [Fact]
+ [Trait("Description", "HttpRequestHeaderParser is internal class")]
+ public void TypeIsCorrect()
+ {
+ Assert.Type.HasProperties<HttpRequestHeaderParser>(TypeAssert.TypeProperties.IsClass);
+ }
+
+
+ private static byte[] CreateBuffer(string method, string address, string version, Dictionary<string, string> headers)
+ {
+ const string SP = " ";
+ const string CRLF = "\r\n";
+ string lws = SP;
+
+ StringBuilder request = new StringBuilder();
+ request.AppendFormat("{0}{1}{2}{3}{4}{5}", method, lws, address, lws, version, CRLF);
+ if (headers != null)
+ {
+ foreach (var h in headers)
+ {
+ request.AppendFormat("{0}: {1}{2}", h.Key, h.Value, CRLF);
+ }
+ }
+
+ request.Append(CRLF);
+ return Encoding.UTF8.GetBytes(request.ToString());
+ }
+
+ private static ParserState ParseBufferInSteps(HttpRequestHeaderParser parser, byte[] buffer, int readsize, out int totalBytesConsumed)
+ {
+ ParserState state = ParserState.Invalid;
+ totalBytesConsumed = 0;
+ while (totalBytesConsumed <= buffer.Length)
+ {
+ int size = Math.Min(buffer.Length - totalBytesConsumed, readsize);
+ byte[] parseBuffer = new byte[size];
+ Buffer.BlockCopy(buffer, totalBytesConsumed, parseBuffer, 0, size);
+
+ int bytesConsumed = 0;
+ state = parser.ParseBuffer(parseBuffer, parseBuffer.Length, ref bytesConsumed);
+ totalBytesConsumed += bytesConsumed;
+
+ if (state != ParserState.NeedMoreData)
+ {
+ return state;
+ }
+ }
+
+ return state;
+ }
+
+ private static void ValidateResult(
+ HttpUnsortedRequest requestLine,
+ string method,
+ string requestUri,
+ Version version,
+ Dictionary<string, string> headers)
+ {
+ Assert.Equal(new HttpMethod(method), requestLine.Method);
+ Assert.Equal(requestUri, requestLine.RequestUri);
+ Assert.Equal(version, requestLine.Version);
+
+ if (headers != null)
+ {
+ Assert.Equal(headers.Count, requestLine.HttpHeaders.Count());
+ foreach (var header in headers)
+ {
+ Assert.True(requestLine.HttpHeaders.Contains(header.Key), "Parsed header did not contain expected key " + header.Key);
+ Assert.Equal(header.Value, requestLine.HttpHeaders.GetValues(header.Key).ElementAt(0));
+ }
+ }
+ }
+
+ [Fact]
+ [Trait("Description", "HttpRequestHeaderParser constructor throws on invalid arguments")]
+ public void HttpRequestHeaderParserConstructorTest()
+ {
+ HttpUnsortedRequest result = new HttpUnsortedRequest();
+ Assert.NotNull(result);
+
+ Assert.ThrowsArgument(() => { new HttpRequestHeaderParser(result, ParserData.MinRequestLineSize - 1, ParserData.MinHeaderSize); }, "maxRequestLineSize");
+
+ Assert.ThrowsArgument(() => { new HttpRequestHeaderParser(result, ParserData.MinRequestLineSize, ParserData.MinHeaderSize - 1); }, "maxHeaderSize");
+
+ HttpRequestHeaderParser parser = new HttpRequestHeaderParser(result, ParserData.MinRequestLineSize, ParserData.MinHeaderSize);
+ Assert.NotNull(parser);
+
+ Assert.ThrowsArgumentNull(() => { new HttpRequestHeaderParser(null); }, "httpRequest");
+ }
+
+
+ [Fact]
+ [Trait("Description", "HttpRequestHeaderParser.ParseBuffer throws on null buffer.")]
+ public void RequestHeaderParserNullBuffer()
+ {
+ HttpUnsortedRequest result = new HttpUnsortedRequest();
+ HttpRequestHeaderParser parser = new HttpRequestHeaderParser(result, ParserData.MinRequestLineSize, ParserData.MinHeaderSize);
+ Assert.NotNull(parser);
+ int bytesConsumed = 0;
+ Assert.ThrowsArgumentNull(() => { parser.ParseBuffer(null, 0, ref bytesConsumed); }, "buffer");
+ }
+
+ [Fact]
+ [Trait("Description", "HttpRequestHeaderParser.ParseBuffer parses minimum requestline.")]
+ public void RequestHeaderParserMinimumBuffer()
+ {
+ byte[] data = CreateBuffer("G", "/", "HTTP/1.1", null);
+ HttpUnsortedRequest result = new HttpUnsortedRequest();
+ HttpRequestHeaderParser parser = new HttpRequestHeaderParser(result, ParserData.MinRequestLineSize, ParserData.MinHeaderSize);
+ Assert.NotNull(parser);
+
+ int bytesConsumed = 0;
+ ParserState state = parser.ParseBuffer(data, data.Length, ref bytesConsumed);
+ Assert.Equal(ParserState.Done, state);
+ Assert.Equal(data.Length, bytesConsumed);
+
+ ValidateResult(result, "G", "/", new Version("1.1"), null);
+ }
+
+ [Fact]
+ [Trait("Description", "HttpRequestHeaderParser.ParseBuffer parses standard methods.")]
+ public void RequestHeaderParserAcceptsStandardMethods()
+ {
+ foreach (HttpMethod method in HttpUnitTestDataSets.AllHttpMethods)
+ {
+ byte[] data = CreateBuffer(method.ToString(), "/", "HTTP/1.1", ParserData.ValidHeaders);
+
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ HttpUnsortedRequest result = new HttpUnsortedRequest();
+ HttpRequestHeaderParser parser = new HttpRequestHeaderParser(result);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed = 0;
+ ParserState state = ParseBufferInSteps(parser, data, cnt, out totalBytesConsumed);
+ Assert.Equal(ParserState.Done, state);
+ Assert.Equal(data.Length, totalBytesConsumed);
+
+ ValidateResult(result, method.ToString(), "/", new Version("1.1"), ParserData.ValidHeaders);
+ }
+ }
+ }
+
+ [Fact]
+ [Trait("Description", "HttpRequestHeaderParser.ParseBuffer parses custom methods.")]
+ public void RequestHeaderParserAcceptsCustomMethods()
+ {
+ foreach (HttpMethod method in HttpUnitTestDataSets.CustomHttpMethods)
+ {
+ byte[] data = CreateBuffer(method.ToString(), "/", "HTTP/1.1", ParserData.ValidHeaders);
+
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ HttpUnsortedRequest result = new HttpUnsortedRequest();
+ HttpRequestHeaderParser parser = new HttpRequestHeaderParser(result);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed = 0;
+ ParserState state = ParseBufferInSteps(parser, data, cnt, out totalBytesConsumed);
+ Assert.Equal(ParserState.Done, state);
+ Assert.Equal(data.Length, totalBytesConsumed);
+
+ ValidateResult(result, method.ToString(), "/", new Version("1.1"), ParserData.ValidHeaders);
+ }
+ }
+ }
+
+ [Fact]
+ [Trait("Description", "HttpRequestHeaderParser.ParseBuffer rejects invalid method")]
+ public void RequestHeaderParserRejectsInvalidMethod()
+ {
+ foreach (string invalidMethod in ParserData.InvalidMethods)
+ {
+ byte[] data = CreateBuffer(invalidMethod, "/", "HTTP/1.1", ParserData.ValidHeaders);
+
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ HttpUnsortedRequest result = new HttpUnsortedRequest();
+ HttpRequestHeaderParser parser = new HttpRequestHeaderParser(result);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed = 0;
+ ParserState state = ParseBufferInSteps(parser, data, cnt, out totalBytesConsumed);
+ Assert.Equal(ParserState.Invalid, state);
+ }
+ }
+ }
+
+ [Fact]
+ [Trait("Description", "HttpRequestHeaderParser.ParseBuffer rejects invalid URI.")]
+ public void RequestHeaderParserRejectsInvalidUri()
+ {
+ foreach (string invalidRequestUri in ParserData.InvalidRequestUris)
+ {
+ byte[] data = CreateBuffer("GET", invalidRequestUri, "HTTP/1.1", ParserData.ValidHeaders);
+
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ HttpUnsortedRequest result = new HttpUnsortedRequest();
+ HttpRequestHeaderParser parser = new HttpRequestHeaderParser(result);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed = 0;
+ ParserState state = ParseBufferInSteps(parser, data, cnt, out totalBytesConsumed);
+ Assert.Equal(ParserState.Invalid, state);
+ }
+ }
+ }
+
+ public static IEnumerable<object[]> Versions
+ {
+ get { return ParserData.Versions; }
+ }
+
+ [Theory]
+ [PropertyData("Versions")]
+ [Trait("Description", "HttpRequestHeaderParser.ParseBuffer accepts valid versions.")]
+ public void RequestHeaderParserAcceptsValidVersion(Version version)
+ {
+ byte[] data = CreateBuffer("GET", "/", String.Format("HTTP/{0}", version.ToString(2)), ParserData.ValidHeaders);
+
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ HttpUnsortedRequest result = new HttpUnsortedRequest();
+ HttpRequestHeaderParser parser = new HttpRequestHeaderParser(result);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed = 0;
+ ParserState state = ParseBufferInSteps(parser, data, cnt, out totalBytesConsumed);
+ Assert.Equal(data.Length, totalBytesConsumed);
+
+ ValidateResult(result, "GET", "/", version, ParserData.ValidHeaders);
+ }
+ }
+
+ public static IEnumerable<object[]> InvalidVersions
+ {
+ get { return ParserData.InvalidVersions; }
+ }
+
+ [Theory]
+ [PropertyData("InvalidVersions")]
+ [Trait("Description", "HttpRequestHeaderParser.ParseBuffer rejects lower case protocol version.")]
+ public void RequestHeaderParserRejectsInvalidVersion(string invalidVersion)
+ {
+ byte[] data = CreateBuffer("GET", "/", invalidVersion, ParserData.ValidHeaders);
+
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ HttpUnsortedRequest result = new HttpUnsortedRequest();
+ HttpRequestHeaderParser parser = new HttpRequestHeaderParser(result);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed = 0;
+ ParserState state = ParseBufferInSteps(parser, data, cnt, out totalBytesConsumed);
+ Assert.Equal(ParserState.Invalid, state);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/test/System.Net.Http.Formatting.Test.Unit/Formatting/Parsers/HttpRequestLineParserTests.cs b/test/System.Net.Http.Formatting.Test.Unit/Formatting/Parsers/HttpRequestLineParserTests.cs
new file mode 100644
index 00000000..dccf41cc
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/Formatting/Parsers/HttpRequestLineParserTests.cs
@@ -0,0 +1,273 @@
+using System.Collections.Generic;
+using System.Net.Http.Formatting.DataSets;
+using System.Text;
+using Microsoft.TestCommon;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Net.Http.Formatting.Parsers
+{
+ public class HttpRequestLineParserTests
+ {
+ [Fact]
+ [Trait("Description", "HttpRequestLineParser is internal class")]
+ public void TypeIsCorrect()
+ {
+ Assert.Type.HasProperties<HttpRequestLineParser>(TypeAssert.TypeProperties.IsClass);
+ }
+
+ internal static byte[] CreateBuffer(string method, string address, string version)
+ {
+ return CreateBuffer(method, address, version, false);
+ }
+
+ private static byte[] CreateBuffer(string method, string address, string version, bool withLws)
+ {
+ const string SP = " ";
+ const string HTAB = "\t";
+ const string CRLF = "\r\n";
+
+ string lws = SP;
+ if (withLws)
+ {
+ lws = SP + SP + HTAB + SP;
+ }
+
+ string requestLine = String.Format("{0}{1}{2}{3}{4}{5}", method, lws, address, lws, version, CRLF);
+ return Encoding.UTF8.GetBytes(requestLine);
+ }
+
+ private static ParserState ParseBufferInSteps(HttpRequestLineParser parser, byte[] buffer, int readsize, out int totalBytesConsumed)
+ {
+ ParserState state = ParserState.Invalid;
+ totalBytesConsumed = 0;
+ while (totalBytesConsumed <= buffer.Length)
+ {
+ int size = Math.Min(buffer.Length - totalBytesConsumed, readsize);
+ byte[] parseBuffer = new byte[size];
+ Buffer.BlockCopy(buffer, totalBytesConsumed, parseBuffer, 0, size);
+
+ int bytesConsumed = 0;
+ state = parser.ParseBuffer(parseBuffer, parseBuffer.Length, ref bytesConsumed);
+ totalBytesConsumed += bytesConsumed;
+
+ if (state != ParserState.NeedMoreData)
+ {
+ return state;
+ }
+ }
+
+ return state;
+ }
+
+ private static void ValidateResult(HttpUnsortedRequest requestLine, string method, string requestUri, Version version)
+ {
+ Assert.Equal(new HttpMethod(method), requestLine.Method);
+ Assert.Equal(requestUri, requestLine.RequestUri);
+ Assert.Equal(version, requestLine.Version);
+ }
+
+ [Fact]
+ [Trait("Description", "HttpRequestLineParser constructor throws on invalid arguments")]
+ public void HttpRequestLineParserConstructorTest()
+ {
+ HttpUnsortedRequest requestLine = new HttpUnsortedRequest();
+ Assert.NotNull(requestLine);
+
+ Assert.ThrowsArgument(() => { new HttpRequestLineParser(requestLine, ParserData.MinRequestLineSize - 1); }, "maxRequestLineSize");
+
+ HttpRequestLineParser parser = new HttpRequestLineParser(requestLine, ParserData.MinRequestLineSize);
+ Assert.NotNull(parser);
+
+ Assert.ThrowsArgumentNull(() => { new HttpRequestLineParser(null, ParserData.MinRequestLineSize); }, "httpRequest");
+ }
+
+
+ [Fact]
+ [Trait("Description", "HttpRequestLineParser.ParseBuffer throws on null buffer.")]
+ public void RequestLineParserNullBuffer()
+ {
+ HttpUnsortedRequest requestLine = new HttpUnsortedRequest();
+ HttpRequestLineParser parser = new HttpRequestLineParser(requestLine, ParserData.MinRequestLineSize);
+ Assert.NotNull(parser);
+ int bytesConsumed = 0;
+ Assert.ThrowsArgumentNull(() => { parser.ParseBuffer(null, 0, ref bytesConsumed); }, "buffer");
+ }
+
+ [Fact]
+ [Trait("Description", "HttpRequestLineParser.ParseBuffer parses minimum requestline.")]
+ public void RequestLineParserMinimumBuffer()
+ {
+ byte[] data = CreateBuffer("G", "/", "HTTP/1.1");
+ HttpUnsortedRequest requestLine = new HttpUnsortedRequest();
+ HttpRequestLineParser parser = new HttpRequestLineParser(requestLine, ParserData.MinRequestLineSize);
+ Assert.NotNull(parser);
+
+ int bytesConsumed = 0;
+ ParserState state = parser.ParseBuffer(data, data.Length, ref bytesConsumed);
+ Assert.Equal(ParserState.Done, state);
+ Assert.Equal(data.Length, bytesConsumed);
+
+ ValidateResult(requestLine, "G", "/", new Version("1.1"));
+ }
+
+ [Fact]
+ [Trait("Description", "HttpRequestLineParser.ParseBuffer rejects LWS requestline.")]
+ public void RequestLineParserRejectsLws()
+ {
+ byte[] data = CreateBuffer("GET", "/", "HTTP/1.1", true);
+
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ HttpUnsortedRequest requestLine = new HttpUnsortedRequest();
+ HttpRequestLineParser parser = new HttpRequestLineParser(requestLine, data.Length);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed = 0;
+ ParserState state = ParseBufferInSteps(parser, data, cnt, out totalBytesConsumed);
+ Assert.Equal(ParserState.Invalid, state);
+ }
+ }
+
+ [Fact]
+ [Trait("Description", "HttpRequestLineParser.ParseBuffer parses standard methods.")]
+ public void RequestLineParserAcceptsStandardMethods()
+ {
+ foreach (HttpMethod method in HttpUnitTestDataSets.AllHttpMethods)
+ {
+ byte[] data = CreateBuffer(method.ToString(), "/", "HTTP/1.1");
+
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ HttpUnsortedRequest requestLine = new HttpUnsortedRequest();
+ HttpRequestLineParser parser = new HttpRequestLineParser(requestLine, data.Length);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed = 0;
+ ParserState state = ParseBufferInSteps(parser, data, cnt, out totalBytesConsumed);
+ Assert.Equal(ParserState.Done, state);
+ Assert.Equal(data.Length, totalBytesConsumed);
+
+ ValidateResult(requestLine, method.ToString(), "/", new Version("1.1"));
+ }
+ }
+ }
+
+ [Fact]
+ [Trait("Description", "HttpRequestLineParser.ParseBuffer parses custom methods.")]
+ public void RequestLineParserAcceptsCustomMethods()
+ {
+ foreach (HttpMethod method in HttpUnitTestDataSets.CustomHttpMethods)
+ {
+ byte[] data = CreateBuffer(method.ToString(), "/", "HTTP/1.1");
+
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ HttpUnsortedRequest requestLine = new HttpUnsortedRequest();
+ HttpRequestLineParser parser = new HttpRequestLineParser(requestLine, data.Length);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed = 0;
+ ParserState state = ParseBufferInSteps(parser, data, cnt, out totalBytesConsumed);
+ Assert.Equal(ParserState.Done, state);
+ Assert.Equal(data.Length, totalBytesConsumed);
+
+ ValidateResult(requestLine, method.ToString(), "/", new Version("1.1"));
+ }
+ }
+ }
+
+ [Fact]
+ [Trait("Description", "HttpRequestLineParser.ParseBuffer rejects invalid method")]
+ public void RequestLineParserRejectsInvalidMethod()
+ {
+ foreach (string invalidMethod in ParserData.InvalidMethods)
+ {
+ byte[] data = CreateBuffer(invalidMethod, "/", "HTTP/1.1");
+
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ HttpUnsortedRequest requestLine = new HttpUnsortedRequest();
+ HttpRequestLineParser parser = new HttpRequestLineParser(requestLine, 256);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed = 0;
+ ParserState state = ParseBufferInSteps(parser, data, cnt, out totalBytesConsumed);
+ Assert.Equal(ParserState.Invalid, state);
+ }
+ }
+ }
+
+ [Fact]
+ [Trait("Description", "HttpRequestLineParser.ParseBuffer rejects invalid URI.")]
+ public void RequestLineParserRejectsInvalidUri()
+ {
+ foreach (string invalidRequestUri in ParserData.InvalidRequestUris)
+ {
+ byte[] data = CreateBuffer("GET", invalidRequestUri, "HTTP/1.1");
+
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ HttpUnsortedRequest requestLine = new HttpUnsortedRequest();
+ HttpRequestLineParser parser = new HttpRequestLineParser(requestLine, 256);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed = 0;
+ ParserState state = ParseBufferInSteps(parser, data, cnt, out totalBytesConsumed);
+ Assert.Equal(ParserState.Invalid, state);
+ }
+ }
+ }
+
+ public static IEnumerable<object[]> Versions
+ {
+ get { return ParserData.Versions; }
+ }
+
+ [Theory]
+ [PropertyData("Versions")]
+ [Trait("Description", "HttpRequestLineParser.ParseBuffer accepts valid versions.")]
+ public void RequestLineParserAcceptsValidVersion(Version version)
+ {
+ byte[] data = CreateBuffer("GET", "/", String.Format("HTTP/{0}", version.ToString(2)));
+
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ HttpUnsortedRequest requestLine = new HttpUnsortedRequest();
+ HttpRequestLineParser parser = new HttpRequestLineParser(requestLine, 256);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed = 0;
+ ParserState state = ParseBufferInSteps(parser, data, cnt, out totalBytesConsumed);
+ Assert.Equal(data.Length, totalBytesConsumed);
+
+ ValidateResult(requestLine, "GET", "/", version);
+ }
+ }
+
+ public static IEnumerable<object[]> InvalidVersions
+ {
+ get { return ParserData.InvalidVersions; }
+ }
+
+ [Theory]
+ [PropertyData("InvalidVersions")]
+ [Trait("Description", "HttpRequestLineParser.ParseBuffer rejects invalid protocol version.")]
+ public void RequestLineParserRejectsInvalidVersion(string invalidVersion)
+ {
+ byte[] data = CreateBuffer("GET", "/", invalidVersion);
+
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ HttpUnsortedRequest requestLine = new HttpUnsortedRequest();
+ HttpRequestLineParser parser = new HttpRequestLineParser(requestLine, 256);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed = 0;
+ ParserState state = ParseBufferInSteps(parser, data, cnt, out totalBytesConsumed);
+ Assert.Equal(ParserState.Invalid, state);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/test/System.Net.Http.Formatting.Test.Unit/Formatting/Parsers/HttpResponseHeaderParserTests.cs b/test/System.Net.Http.Formatting.Test.Unit/Formatting/Parsers/HttpResponseHeaderParserTests.cs
new file mode 100644
index 00000000..a82cd032
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/Formatting/Parsers/HttpResponseHeaderParserTests.cs
@@ -0,0 +1,272 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http.Formatting.DataSets;
+using System.Text;
+using Microsoft.TestCommon;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Net.Http.Formatting.Parsers
+{
+ public class HttpResponseHeaderParserTests
+ {
+ [Fact]
+ [Trait("Description", "HttpResponseHeaderParser is internal class")]
+ public void TypeIsCorrect()
+ {
+ Assert.Type.HasProperties<HttpResponseHeaderParser>(TypeAssert.TypeProperties.IsClass);
+ }
+
+ private static byte[] CreateBuffer(string version, string statusCode, string reasonPhrase, Dictionary<string, string> headers)
+ {
+ const string SP = " ";
+ const string CRLF = "\r\n";
+ string lws = SP;
+
+ StringBuilder response = new StringBuilder();
+ response.AppendFormat("{0}{1}{2}{3}{4}{5}", version, lws, statusCode, lws, reasonPhrase, CRLF);
+ if (headers != null)
+ {
+ foreach (var h in headers)
+ {
+ response.AppendFormat("{0}: {1}{2}", h.Key, h.Value, CRLF);
+ }
+ }
+
+ response.Append(CRLF);
+ return Encoding.UTF8.GetBytes(response.ToString());
+ }
+
+ private static ParserState ParseBufferInSteps(HttpResponseHeaderParser parser, byte[] buffer, int readsize, out int totalBytesConsumed)
+ {
+ ParserState state = ParserState.Invalid;
+ totalBytesConsumed = 0;
+ while (totalBytesConsumed <= buffer.Length)
+ {
+ int size = Math.Min(buffer.Length - totalBytesConsumed, readsize);
+ byte[] parseBuffer = new byte[size];
+ Buffer.BlockCopy(buffer, totalBytesConsumed, parseBuffer, 0, size);
+
+ int bytesConsumed = 0;
+ state = parser.ParseBuffer(parseBuffer, parseBuffer.Length, ref bytesConsumed);
+ totalBytesConsumed += bytesConsumed;
+
+ if (state != ParserState.NeedMoreData)
+ {
+ return state;
+ }
+ }
+
+ return state;
+ }
+
+ private static void ValidateResult(
+ HttpUnsortedResponse statusLine,
+ Version version,
+ HttpStatusCode statusCode,
+ string reasonPhrase,
+ Dictionary<string, string> headers)
+ {
+ Assert.Equal(version, statusLine.Version);
+ Assert.Equal(statusCode, statusLine.StatusCode);
+ Assert.Equal(reasonPhrase, statusLine.ReasonPhrase);
+
+ if (headers != null)
+ {
+ Assert.Equal(headers.Count, statusLine.HttpHeaders.Count());
+ foreach (var header in headers)
+ {
+ Assert.True(statusLine.HttpHeaders.Contains(header.Key), "Parsed header did not contain expected key " + header.Key);
+ Assert.Equal(header.Value, statusLine.HttpHeaders.GetValues(header.Key).ElementAt(0));
+ }
+ }
+ }
+
+ [Fact]
+ [Trait("Description", "HttpResponseHeaderParser constructor throws on invalid arguments")]
+ public void HttpResponseHeaderParserConstructorTest()
+ {
+ HttpUnsortedResponse result = new HttpUnsortedResponse();
+ Assert.NotNull(result);
+
+ Assert.ThrowsArgument(() => { new HttpResponseHeaderParser(result, ParserData.MinStatusLineSize - 1, ParserData.MinHeaderSize); }, "maxStatusLineSize");
+
+ Assert.ThrowsArgument(() => { new HttpResponseHeaderParser(result, ParserData.MinStatusLineSize, ParserData.MinHeaderSize - 1); }, "maxHeaderSize");
+
+ HttpResponseHeaderParser parser = new HttpResponseHeaderParser(result, ParserData.MinStatusLineSize, ParserData.MinHeaderSize);
+ Assert.NotNull(parser);
+
+ Assert.ThrowsArgumentNull(() => { new HttpResponseHeaderParser(null); }, "httpResponse");
+ }
+
+
+ [Fact]
+ [Trait("Description", "HttpResponseHeaderParser.ParseBuffer throws on null buffer.")]
+ public void ResponseHeaderParserNullBuffer()
+ {
+ HttpUnsortedResponse result = new HttpUnsortedResponse();
+ HttpResponseHeaderParser parser = new HttpResponseHeaderParser(result, ParserData.MinStatusLineSize, ParserData.MinHeaderSize);
+ Assert.NotNull(parser);
+ int bytesConsumed = 0;
+ Assert.ThrowsArgumentNull(() => { parser.ParseBuffer(null, 0, ref bytesConsumed); }, "buffer");
+ }
+
+ [Fact]
+ [Trait("Description", "HttpResponseHeaderParser.ParseBuffer parses minimum statusLine.")]
+ public void ResponseHeaderParserMinimumBuffer()
+ {
+ byte[] data = CreateBuffer("HTTP/1.1", "200", "", null);
+ HttpUnsortedResponse result = new HttpUnsortedResponse();
+ HttpResponseHeaderParser parser = new HttpResponseHeaderParser(result, ParserData.MinStatusLineSize, ParserData.MinHeaderSize);
+ Assert.NotNull(parser);
+
+ int bytesConsumed = 0;
+ ParserState state = parser.ParseBuffer(data, data.Length, ref bytesConsumed);
+ Assert.Equal(ParserState.Done, state);
+ Assert.Equal(data.Length, bytesConsumed);
+
+ ValidateResult(result, new Version("1.1"), HttpStatusCode.OK, "", null);
+ }
+
+ [Fact]
+ [Trait("Description", "HttpResponseHeaderParser.ParseBuffer parses standard status codes.")]
+ public void ResponseHeaderParserAcceptsStandardStatusCodes()
+ {
+ foreach (HttpStatusCode status in HttpUnitTestDataSets.AllHttpStatusCodes)
+ {
+ byte[] data = CreateBuffer("HTTP/1.1", ((int)status).ToString(), "Reason", ParserData.ValidHeaders);
+
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ HttpUnsortedResponse result = new HttpUnsortedResponse();
+ HttpResponseHeaderParser parser = new HttpResponseHeaderParser(result);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed = 0;
+ ParserState state = ParseBufferInSteps(parser, data, cnt, out totalBytesConsumed);
+ Assert.Equal(ParserState.Done, state);
+ Assert.Equal(data.Length, totalBytesConsumed);
+
+ ValidateResult(result, new Version("1.1"), status, "Reason", ParserData.ValidHeaders);
+ }
+ }
+ }
+
+ [Fact]
+ [Trait("Description", "HttpResponseHeaderParser.ParseBuffer parses custom status codes.")]
+ public void ResponseHeaderParserAcceptsCustomStatusCodes()
+ {
+ foreach (HttpStatusCode status in HttpUnitTestDataSets.CustomHttpStatusCodes)
+ {
+ byte[] data = CreateBuffer("HTTP/1.1", ((int)status).ToString(), "Reason", ParserData.ValidHeaders);
+
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ HttpUnsortedResponse result = new HttpUnsortedResponse();
+ HttpResponseHeaderParser parser = new HttpResponseHeaderParser(result);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed = 0;
+ ParserState state = ParseBufferInSteps(parser, data, cnt, out totalBytesConsumed);
+ Assert.Equal(ParserState.Done, state);
+ Assert.Equal(data.Length, totalBytesConsumed);
+
+ ValidateResult(result, new Version("1.1"), status, "Reason", ParserData.ValidHeaders);
+ }
+ }
+ }
+
+ [Fact]
+ [Trait("Description", "HttpResponseHeaderParser.ParseBuffer rejects invalid status codes")]
+ public void ResponseHeaderParserRejectsInvalidStatusCodes()
+ {
+ foreach (string invalidStatus in ParserData.InvalidStatusCodes)
+ {
+ byte[] data = CreateBuffer("HTTP/1.1", invalidStatus, "Reason", ParserData.ValidHeaders);
+
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ HttpUnsortedResponse result = new HttpUnsortedResponse();
+ HttpResponseHeaderParser parser = new HttpResponseHeaderParser(result);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed = 0;
+ ParserState state = ParseBufferInSteps(parser, data, cnt, out totalBytesConsumed);
+ Assert.Equal(ParserState.Invalid, state);
+ }
+ }
+ }
+
+ [Fact]
+ [Trait("Description", "HttpResponseHeaderParser.ParseBuffer rejects invalid reason phrase.")]
+ public void ResponseHeaderParserRejectsInvalidReasonPhrase()
+ {
+ foreach (string invalidReason in ParserData.InvalidReasonPhrases)
+ {
+ byte[] data = CreateBuffer("HTTP/1.1", "200", invalidReason, ParserData.ValidHeaders);
+
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ HttpUnsortedResponse result = new HttpUnsortedResponse();
+ HttpResponseHeaderParser parser = new HttpResponseHeaderParser(result);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed = 0;
+ ParserState state = ParseBufferInSteps(parser, data, cnt, out totalBytesConsumed);
+ Assert.Equal(ParserState.Invalid, state);
+ }
+ }
+ }
+
+ public static IEnumerable<object[]> Versions
+ {
+ get { return ParserData.Versions; }
+ }
+
+ [Theory]
+ [PropertyData("Versions")]
+ [Trait("Description", "HttpResponseHeaderParser.ParseBuffer accepts valid versions.")]
+ public void ResponseHeaderParserAcceptsValidVersion(Version version)
+ {
+ byte[] data = CreateBuffer(String.Format("HTTP/{0}", version.ToString(2)), "200", "Reason", ParserData.ValidHeaders);
+
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ HttpUnsortedResponse result = new HttpUnsortedResponse();
+ HttpResponseHeaderParser parser = new HttpResponseHeaderParser(result);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed = 0;
+ ParserState state = ParseBufferInSteps(parser, data, cnt, out totalBytesConsumed);
+ Assert.Equal(data.Length, totalBytesConsumed);
+
+ ValidateResult(result, version, HttpStatusCode.OK, "Reason", ParserData.ValidHeaders);
+ }
+ }
+
+ public static IEnumerable<object[]> InvalidVersions
+ {
+ get { return ParserData.InvalidVersions; }
+ }
+
+ [Theory]
+ [PropertyData("InvalidVersions")]
+ [Trait("Description", "HttpResponseHeaderParser.ParseBuffer rejects invalid protocol version.")]
+ public void ResponseHeaderParserRejectsInvalidVersion(string invalid)
+ {
+ byte[] data = CreateBuffer(invalid, "200", "Reason", ParserData.ValidHeaders);
+
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ HttpUnsortedResponse result = new HttpUnsortedResponse();
+ HttpResponseHeaderParser parser = new HttpResponseHeaderParser(result);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed = 0;
+ ParserState state = ParseBufferInSteps(parser, data, cnt, out totalBytesConsumed);
+ Assert.Equal(ParserState.Invalid, state);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/test/System.Net.Http.Formatting.Test.Unit/Formatting/Parsers/HttpStatusLineParserTests.cs b/test/System.Net.Http.Formatting.Test.Unit/Formatting/Parsers/HttpStatusLineParserTests.cs
new file mode 100644
index 00000000..c7057b12
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/Formatting/Parsers/HttpStatusLineParserTests.cs
@@ -0,0 +1,284 @@
+using System.Collections.Generic;
+using System.Net.Http.Formatting.DataSets;
+using System.Text;
+using Microsoft.TestCommon;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Net.Http.Formatting.Parsers
+{
+ public class HttpStatusLineParserTests
+ {
+ [Fact]
+ [Trait("Description", "HttpStatusLineParser is internal class")]
+ public void TypeIsCorrect()
+ {
+ Assert.Type.HasProperties<HttpStatusLineParser>(TypeAssert.TypeProperties.IsClass);
+ }
+
+ internal static byte[] CreateBuffer(string version, string statusCode, string reasonPhrase)
+ {
+ return CreateBuffer(version, statusCode, reasonPhrase, false);
+ }
+
+ private static byte[] CreateBuffer(string version, string statusCode, string reasonPhrase, bool withLws)
+ {
+ const string SP = " ";
+ const string HTAB = "\t";
+ const string CRLF = "\r\n";
+
+ string lws = SP;
+ if (withLws)
+ {
+ lws = SP + SP + HTAB + SP;
+ }
+
+ string statusLine = String.Format("{0}{1}{2}{3}{4}{5}", version, lws, statusCode, lws, reasonPhrase, CRLF);
+ return Encoding.UTF8.GetBytes(statusLine);
+ }
+
+ private static ParserState ParseBufferInSteps(HttpStatusLineParser parser, byte[] buffer, int readsize, out int totalBytesConsumed)
+ {
+ ParserState state = ParserState.Invalid;
+ totalBytesConsumed = 0;
+ while (totalBytesConsumed <= buffer.Length)
+ {
+ int size = Math.Min(buffer.Length - totalBytesConsumed, readsize);
+ byte[] parseBuffer = new byte[size];
+ Buffer.BlockCopy(buffer, totalBytesConsumed, parseBuffer, 0, size);
+
+ int bytesConsumed = 0;
+ state = parser.ParseBuffer(parseBuffer, parseBuffer.Length, ref bytesConsumed);
+ totalBytesConsumed += bytesConsumed;
+
+ if (state != ParserState.NeedMoreData)
+ {
+ return state;
+ }
+ }
+
+ return state;
+ }
+
+ private static void ValidateResult(HttpUnsortedResponse statusLine, Version version, HttpStatusCode statusCode, string reasonPhrase)
+ {
+ Assert.Equal(version, statusLine.Version);
+ Assert.Equal(statusCode, statusLine.StatusCode);
+ Assert.Equal(reasonPhrase, statusLine.ReasonPhrase);
+ }
+
+ [Fact]
+ [Trait("Description", "HttpStatusLineParser constructor throws on invalid arguments")]
+ public void HttpStatusLineParserConstructorTest()
+ {
+ HttpUnsortedResponse statusLine = new HttpUnsortedResponse();
+ Assert.NotNull(statusLine);
+
+ Assert.ThrowsArgument(() => { new HttpStatusLineParser(statusLine, ParserData.MinStatusLineSize - 1); }, "maxStatusLineSize");
+
+ HttpStatusLineParser parser = new HttpStatusLineParser(statusLine, ParserData.MinStatusLineSize);
+ Assert.NotNull(parser);
+
+ Assert.ThrowsArgumentNull(() => { new HttpStatusLineParser(null, ParserData.MinStatusLineSize); }, "httpResponse");
+ }
+
+
+ [Fact]
+ [Trait("Description", "HttpStatusLineParser.ParseBuffer throws on null buffer.")]
+ public void StatusLineParserNullBuffer()
+ {
+ HttpUnsortedResponse statusLine = new HttpUnsortedResponse();
+ HttpStatusLineParser parser = new HttpStatusLineParser(statusLine, ParserData.MinStatusLineSize);
+ Assert.NotNull(parser);
+ int bytesConsumed = 0;
+ Assert.ThrowsArgumentNull(() => { parser.ParseBuffer(null, 0, ref bytesConsumed); }, "buffer");
+ }
+
+ [Fact]
+ [Trait("Description", "HttpStatusLineParser.ParseBuffer parses minimum requestline.")]
+ public void StatusLineParserMinimumBuffer()
+ {
+ byte[] data = CreateBuffer("HTTP/1.1", "200", "");
+ HttpUnsortedResponse statusLine = new HttpUnsortedResponse();
+ HttpStatusLineParser parser = new HttpStatusLineParser(statusLine, ParserData.MinStatusLineSize);
+ Assert.NotNull(parser);
+
+ int bytesConsumed = 0;
+ ParserState state = parser.ParseBuffer(data, data.Length, ref bytesConsumed);
+ Assert.Equal(ParserState.Done, state);
+ Assert.Equal(data.Length, bytesConsumed);
+
+ ValidateResult(statusLine, new Version("1.1"), HttpStatusCode.OK, "");
+ }
+
+ [Fact]
+ [Trait("Description", "HttpStatusLineParser.ParseBuffer rejects LWS requestline.")]
+ public void StatusLineParserRejectsLws()
+ {
+ byte[] data = CreateBuffer("HTTP/1.1", "200", "Reason", true);
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ HttpUnsortedResponse statusLine = new HttpUnsortedResponse();
+ HttpStatusLineParser parser = new HttpStatusLineParser(statusLine, data.Length);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed = 0;
+ ParserState state = ParseBufferInSteps(parser, data, cnt, out totalBytesConsumed);
+ Assert.Equal(ParserState.Invalid, state);
+ }
+ }
+
+ [Fact]
+ [Trait("Description", "HttpStatusLineParser.ParseBuffer parses standard status codes.")]
+ public void StatusLineParserAcceptsStandardStatusCodes()
+ {
+ foreach (HttpStatusCode status in HttpUnitTestDataSets.AllHttpStatusCodes)
+ {
+ byte[] data = CreateBuffer("HTTP/1.1", ((int)status).ToString(), "Reason");
+
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ HttpUnsortedResponse statusLine = new HttpUnsortedResponse();
+ HttpStatusLineParser parser = new HttpStatusLineParser(statusLine, data.Length);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed = 0;
+ ParserState state = ParseBufferInSteps(parser, data, cnt, out totalBytesConsumed);
+ Assert.Equal(ParserState.Done, state);
+ Assert.Equal(data.Length, totalBytesConsumed);
+
+ ValidateResult(statusLine, new Version("1.1"), status, "Reason");
+ }
+ }
+ }
+
+ [Fact]
+ [Trait("Description", "HttpStatusLineParser.ParseBuffer parses custom status codes.")]
+ public void StatusLineParserAcceptsCustomStatusCodes()
+ {
+ foreach (HttpStatusCode status in HttpUnitTestDataSets.CustomHttpStatusCodes)
+ {
+ byte[] data = CreateBuffer("HTTP/1.1", ((int)status).ToString(), "Reason");
+
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ HttpUnsortedResponse statusLine = new HttpUnsortedResponse();
+ HttpStatusLineParser parser = new HttpStatusLineParser(statusLine, data.Length);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed = 0;
+ ParserState state = ParseBufferInSteps(parser, data, cnt, out totalBytesConsumed);
+ Assert.Equal(ParserState.Done, state);
+ Assert.Equal(data.Length, totalBytesConsumed);
+
+ ValidateResult(statusLine, new Version("1.1"), status, "Reason");
+ }
+ }
+ }
+
+ [Fact]
+ [Trait("Description", "HttpStatusLineParser.ParseBuffer rejects invalid status codes")]
+ public void StatusLineParserRejectsInvalidStatusCodes()
+ {
+ foreach (string invalidStatus in ParserData.InvalidStatusCodes)
+ {
+ byte[] data = CreateBuffer("HTTP/1.1", invalidStatus, "Reason");
+
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ HttpUnsortedResponse statusLine = new HttpUnsortedResponse();
+ HttpStatusLineParser parser = new HttpStatusLineParser(statusLine, 256);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed = 0;
+ ParserState state = ParseBufferInSteps(parser, data, cnt, out totalBytesConsumed);
+ Assert.Equal(ParserState.Invalid, state);
+ }
+ }
+ }
+
+ public static IEnumerable<object[]> ValidReasonPhrases
+ {
+ get
+ {
+ yield return new object[] { "" };
+ yield return new object[] { "Ok" };
+ yield return new object[] { "public Server Error" };
+ yield return new object[] { "r e a s o n" };
+ yield return new object[] { "reason " };
+ yield return new object[] { " reason " };
+ }
+ }
+
+ [Theory]
+ [PropertyData("ValidReasonPhrases")]
+ [Trait("Description", "HttpStatusLineParser.ParseBuffer accepts valid reason phrase.")]
+ public void StatusLineParserAcceptsValidReasonPhrase(string validReasonPhrase)
+ {
+ byte[] data = CreateBuffer("HTTP/1.1", "200", validReasonPhrase);
+
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ HttpUnsortedResponse statusLine = new HttpUnsortedResponse();
+ HttpStatusLineParser parser = new HttpStatusLineParser(statusLine, 256);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed = 0;
+ ParserState state = ParseBufferInSteps(parser, data, cnt, out totalBytesConsumed);
+
+ ValidateResult(statusLine, new Version("1.1"), HttpStatusCode.OK, validReasonPhrase);
+ }
+ }
+
+ public static IEnumerable<object[]> Versions
+ {
+ get { return ParserData.Versions; }
+ }
+
+ [Theory]
+ [PropertyData("Versions")]
+ [Trait("Description", "HttpStatusLineParser.ParseBuffer accepts valid versions.")]
+ public void StatusLineParserAcceptsValidVersion(Version version)
+ {
+ byte[] data = CreateBuffer(String.Format("HTTP/{0}", version.ToString(2)), "200", "Reason");
+
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ HttpUnsortedResponse statusLine = new HttpUnsortedResponse();
+ HttpStatusLineParser parser = new HttpStatusLineParser(statusLine, 256);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed = 0;
+ ParserState state = ParseBufferInSteps(parser, data, cnt, out totalBytesConsumed);
+ Assert.Equal(data.Length, totalBytesConsumed);
+
+ ValidateResult(statusLine, version, HttpStatusCode.OK, "Reason");
+ }
+ }
+
+ public static IEnumerable<object[]> InvalidVersions
+ {
+ get { return ParserData.InvalidVersions; }
+ }
+
+ [Theory]
+ [PropertyData("InvalidVersions")]
+ [Trait("Description", "HttpStatusLineParser.ParseBuffer rejects invalid protocol version.")]
+ public void StatusLineParserRejectsInvalidVersion(string invalidVersion)
+ {
+ byte[] data = CreateBuffer(invalidVersion, "200", "Reason");
+
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ HttpUnsortedResponse statusLine = new HttpUnsortedResponse();
+ HttpStatusLineParser parser = new HttpStatusLineParser(statusLine, 256);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed = 0;
+ ParserState state = ParseBufferInSteps(parser, data, cnt, out totalBytesConsumed);
+ Assert.Equal(ParserState.Invalid, state);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/test/System.Net.Http.Formatting.Test.Unit/Formatting/Parsers/InternetMessageFormatHeaderParserTests.cs b/test/System.Net.Http.Formatting.Test.Unit/Formatting/Parsers/InternetMessageFormatHeaderParserTests.cs
new file mode 100644
index 00000000..786d0b7e
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/Formatting/Parsers/InternetMessageFormatHeaderParserTests.cs
@@ -0,0 +1,664 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http.Headers;
+using System.Text;
+using Microsoft.TestCommon;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Net.Http.Formatting.Parsers
+{
+ public class InternetMessageFormatHeaderParserTests
+ {
+ [Fact]
+ [Trait("Description", "HeaderParser is internal class")]
+ public void TypeIsCorrect()
+ {
+ Assert.Type.HasProperties<InternetMessageFormatHeaderParser>(TypeAssert.TypeProperties.IsClass);
+ }
+
+ private static IEnumerable<HttpHeaders> CreateHttpHeaders()
+ {
+ return new HttpHeaders[]
+ {
+ new HttpRequestMessage().Headers,
+ new HttpResponseMessage().Headers,
+ new StringContent(String.Empty).Headers,
+ };
+ }
+
+ private static InternetMessageFormatHeaderParser CreateHeaderParser(int maximumHeaderLength, out HttpHeaders headers)
+ {
+ headers = new HttpRequestMessage().Headers;
+ return new InternetMessageFormatHeaderParser(headers, maximumHeaderLength);
+ }
+
+ internal static byte[] CreateBuffer(params string[] headers)
+ {
+ const string CRLF = "\r\n";
+ StringBuilder header = new StringBuilder();
+ foreach (var h in headers)
+ {
+ header.Append(h + CRLF);
+ }
+
+ header.Append(CRLF);
+ return Encoding.UTF8.GetBytes(header.ToString());
+ }
+
+ private static void RunRfc5322SampleTest(string[] testHeaders, Action<HttpHeaders> validation)
+ {
+ byte[] data = InternetMessageFormatHeaderParserTests.CreateBuffer(testHeaders);
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ HttpHeaders headers;
+ InternetMessageFormatHeaderParser parser = InternetMessageFormatHeaderParserTests.CreateHeaderParser(data.Length, out headers);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed = 0;
+ ParserState state = InternetMessageFormatHeaderParserTests.ParseBufferInSteps(parser, data, cnt, out totalBytesConsumed);
+ Assert.Equal(ParserState.Done, state);
+ Assert.Equal(data.Length, totalBytesConsumed);
+
+ validation(headers);
+ }
+ }
+
+ private static ParserState ParseBufferInSteps(InternetMessageFormatHeaderParser parser, byte[] buffer, int readsize, out int totalBytesConsumed)
+ {
+ ParserState state = ParserState.Invalid;
+ totalBytesConsumed = 0;
+ while (totalBytesConsumed <= buffer.Length)
+ {
+ int size = Math.Min(buffer.Length - totalBytesConsumed, readsize);
+ byte[] parseBuffer = new byte[size];
+ Buffer.BlockCopy(buffer, totalBytesConsumed, parseBuffer, 0, size);
+
+ int bytesConsumed = 0;
+ state = parser.ParseBuffer(parseBuffer, parseBuffer.Length, ref bytesConsumed);
+ totalBytesConsumed += bytesConsumed;
+
+ if (state != ParserState.NeedMoreData)
+ {
+ return state;
+ }
+ }
+
+ return state;
+ }
+
+
+ [Fact]
+ [Trait("Description", "HeaderParser constructor throws on invalid arguments")]
+ public void HeaderParserConstructorTest()
+ {
+ IEnumerable<HttpHeaders> headers = InternetMessageFormatHeaderParserTests.CreateHttpHeaders();
+ foreach (var header in headers)
+ {
+ InternetMessageFormatHeaderParser parser = new InternetMessageFormatHeaderParser(header, ParserData.MinHeaderSize);
+ Assert.NotNull(parser);
+ }
+
+ Assert.ThrowsArgument(() => { new InternetMessageFormatHeaderParser(headers.ElementAt(0), -1); }, "maxHeaderSize");
+ Assert.ThrowsArgument(() => { new InternetMessageFormatHeaderParser(headers.ElementAt(0), 0); }, "maxHeaderSize");
+ Assert.ThrowsArgument(() => { new InternetMessageFormatHeaderParser(headers.ElementAt(0), 1); }, "maxHeaderSize");
+
+ Assert.ThrowsArgumentNull(() => { new InternetMessageFormatHeaderParser(null, ParserData.MinHeaderSize); }, "headers");
+ }
+
+
+ [Fact]
+ [Trait("Description", "HeaderParser.ParseBuffer throws on null buffer.")]
+ public void HeaderParserNullBuffer()
+ {
+ HttpHeaders headers;
+ InternetMessageFormatHeaderParser parser = InternetMessageFormatHeaderParserTests.CreateHeaderParser(128, out headers);
+ Assert.NotNull(parser);
+ int bytesConsumed = 0;
+ Assert.ThrowsArgumentNull(() => { parser.ParseBuffer(null, 0, ref bytesConsumed); }, "buffer");
+ }
+
+ [Fact]
+ [Trait("Description", "HeaderParser.ParseBuffer parses empty header.")]
+ public void HeaderParserEmptyBuffer()
+ {
+ byte[] data = InternetMessageFormatHeaderParserTests.CreateBuffer();
+ HttpHeaders headers;
+ InternetMessageFormatHeaderParser parser = InternetMessageFormatHeaderParserTests.CreateHeaderParser(data.Length, out headers);
+ Assert.NotNull(parser);
+
+ int bytesConsumed = 0;
+ ParserState state = parser.ParseBuffer(data, data.Length, ref bytesConsumed);
+ Assert.Equal(ParserState.Done, state);
+ Assert.Equal(data.Length, bytesConsumed);
+
+ Assert.Equal(0, headers.Count());
+ }
+
+ [Fact]
+ [Trait("Description", "HeaderParser.ParseBuffer parses single header field.")]
+ public void HeaderParserSingleNameValueHeader()
+ {
+ byte[] data = InternetMessageFormatHeaderParserTests.CreateBuffer("N:V");
+
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ HttpHeaders headers;
+ InternetMessageFormatHeaderParser parser = InternetMessageFormatHeaderParserTests.CreateHeaderParser(data.Length, out headers);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed;
+ ParserState state = InternetMessageFormatHeaderParserTests.ParseBufferInSteps(parser, data, cnt, out totalBytesConsumed);
+ Assert.Equal(ParserState.Done, state);
+ Assert.Equal(data.Length, totalBytesConsumed);
+
+ Assert.Equal(1, headers.Count());
+ IEnumerable<string> parsedValues = headers.GetValues("N");
+ Assert.Equal(1, parsedValues.Count());
+ Assert.Equal(parsedValues.ElementAt(0), "V");
+ }
+ }
+
+ [Fact]
+ [Trait("Description", "HeaderParser.ParseBuffer parses single header field with name only.")]
+ public void HeaderParserSingleNameHeader()
+ {
+ byte[] data = InternetMessageFormatHeaderParserTests.CreateBuffer("N:");
+
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ HttpHeaders headers;
+ InternetMessageFormatHeaderParser parser = InternetMessageFormatHeaderParserTests.CreateHeaderParser(data.Length, out headers);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed;
+ ParserState state = InternetMessageFormatHeaderParserTests.ParseBufferInSteps(parser, data, cnt, out totalBytesConsumed);
+ Assert.Equal(ParserState.Done, state);
+ Assert.Equal(data.Length, totalBytesConsumed);
+
+ Assert.Equal(1, headers.Count());
+ IEnumerable<string> parsedValues = headers.GetValues("N");
+ Assert.Equal(1, parsedValues.Count());
+ Assert.Equal("", parsedValues.ElementAt(0));
+ }
+ }
+
+ [Fact]
+ [Trait("Description", "HeaderParser.ParseBuffer parses multiple header fields.")]
+ public void HeaderParserMultipleNameHeader()
+ {
+ byte[] data = InternetMessageFormatHeaderParserTests.CreateBuffer("N:V1", "N:V2");
+
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ HttpHeaders headers;
+ InternetMessageFormatHeaderParser parser = InternetMessageFormatHeaderParserTests.CreateHeaderParser(data.Length, out headers);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed;
+ ParserState state = InternetMessageFormatHeaderParserTests.ParseBufferInSteps(parser, data, cnt, out totalBytesConsumed);
+ Assert.Equal(ParserState.Done, state);
+ Assert.Equal(data.Length, totalBytesConsumed);
+
+ Assert.Equal(1, headers.Count());
+ IEnumerable<string> parsedValues = headers.GetValues("N");
+ Assert.Equal(2, parsedValues.Count());
+ Assert.Equal("V1", parsedValues.ElementAt(0));
+ Assert.Equal("V2", parsedValues.ElementAt(1));
+ }
+ }
+
+ [Fact]
+ [Trait("Description", "HeaderParser.ParseBuffer parses multiple header fields with linear white space.")]
+ public void HeaderParserLwsHeader()
+ {
+ byte[] data = InternetMessageFormatHeaderParserTests.CreateBuffer("N1:V1", "N2: V2", "N3:\tV3");
+
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ HttpHeaders headers;
+ InternetMessageFormatHeaderParser parser = InternetMessageFormatHeaderParserTests.CreateHeaderParser(data.Length, out headers);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed = 0;
+ ParserState state = InternetMessageFormatHeaderParserTests.ParseBufferInSteps(parser, data, cnt, out totalBytesConsumed);
+ Assert.Equal(ParserState.Done, state);
+ Assert.Equal(data.Length, totalBytesConsumed);
+
+ Assert.Equal(3, headers.Count());
+
+ IEnumerable<string> parsedValues = headers.GetValues("N1");
+ Assert.Equal(1, parsedValues.Count());
+ Assert.Equal("V1", parsedValues.ElementAt(0));
+
+ parsedValues = headers.GetValues("N2");
+ Assert.Equal(1, parsedValues.Count());
+ Assert.Equal("V2", parsedValues.ElementAt(0));
+
+ parsedValues = headers.GetValues("N3");
+ Assert.Equal(1, parsedValues.Count());
+ Assert.Equal("V3", parsedValues.ElementAt(0));
+ }
+ }
+
+ [Fact]
+ [Trait("Description", "HeaderParser.ParseBuffer parses invalid header field.")]
+ public void HeaderParserInvalidHeader()
+ {
+ byte[] data = InternetMessageFormatHeaderParserTests.CreateBuffer("N1 :V1");
+
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ HttpHeaders headers;
+ InternetMessageFormatHeaderParser parser = InternetMessageFormatHeaderParserTests.CreateHeaderParser(data.Length, out headers);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed = 0;
+ ParserState state = InternetMessageFormatHeaderParserTests.ParseBufferInSteps(parser, data, cnt, out totalBytesConsumed);
+ Assert.Equal(ParserState.Invalid, state);
+ Assert.Equal(data.Length - 2, totalBytesConsumed);
+ }
+ }
+
+ [Fact]
+ [Trait("Description", "HeaderParser.ParseBuffer parses various specialized header fields including JSON, P3P, etc.")]
+ public void HeaderParserSpecializedHeaders()
+ {
+ Dictionary<string, string> headerData = new Dictionary<string, string>
+ {
+ { @"JsonProperties0", @"{ ""SessionId"": ""{27729E1-B37B-4D29-AA0A-E367906C206E}"", ""MessageId"": ""{701332E1-B37B-4D29-AA0A-E367906C206E}"", ""TimeToLive"" : 90, ""CorrelationId"": ""{701332F3-B37B-4D29-AA0A-E367906C206E}"", ""SequenceNumber"" : 12345, ""DeliveryCount"" : 2, ""To"" : ""http://contoso.com/path1"", ""ReplyTo"" : ""http://fabrikam.com/path1"", ""SentTimeUtc"" : ""Sun, 06 Nov 1994 08:49:37 GMT"", ""ScheduledEnqueueTimeUtc"" : ""Sun, 06 Nov 1994 08:49:37 GMT""}" },
+ { @"JsonProperties1", @"{ ""SessionId"": ""{2813D4D2-46A9-4F4D-8904-E9BDE3712B70}"", ""MessageId"": ""{24AE31D6-63B8-46F3-9975-A3DAF1B6D3F4}"", ""TimeToLive"" : 80, ""CorrelationId"": ""{896DD5BD-1645-44D7-9E7C-D7F70958ECD6}"", ""SequenceNumber"" : 54321, ""DeliveryCount"" : 4, ""To"" : ""http://contoso.com/path2"", ""ReplyTo"" : ""http://fabrikam.com/path2"", ""SentTimeUtc"" : ""Sun, 06 Nov 1994 10:49:37 GMT"", ""ScheduledEnqueueTimeUtc"" : ""Sun, 06 Nov 1994 10:49:37 GMT""}" },
+ { @"P3P", @"CP=""ALL IND DSP COR ADM CONo CUR CUSo IVAo IVDo PSA PSD TAI TELo OUR SAMo CNT COM INT NAV ONL PHY PRE PUR UNI""" },
+ { @"Cookie", @"omniID=1297715979621_9f45_1519_3f8a_f22f85346ac6; WT_FPC=id=65.55.227.138-2323234032.30136233:lv=1309374389020:ss=1309374389020; A=I&I=AxUFAAAAAACNCAAADYEZ7CFPss7Swnujy4PXZA!!&M=1&CS=126mAa0002ZB51a02gZB51a; MC1=GUID=568428660ad44d4ab8f46133f4b03738&HASH=6628&LV=20113&V=3; WT_NVR_RU=0=msdn:1=:2=; MUID=A44DE185EA1B4E8088CCF7B348C5D65F; MSID=Microsoft.CreationDate=03/04/2011 23:38:15&Microsoft.LastVisitDate=06/20/2011 04:15:08&Microsoft.VisitStartDate=06/20/2011 04:15:08&Microsoft.CookieId=f658f3f2-e6d6-42ab-b86b-96791b942b6f&Microsoft.TokenId=ffffffff-ffff-ffff-ffff-ffffffffffff&Microsoft.NumberOfVisits=106&Microsoft.CookieFirstVisit=1&Microsoft.IdentityToken=AA==&Microsoft.MicrosoftId=0441-6141-1523-9969; msresearch=%7B%22version%22%3A%224.6%22%2C%22state%22%3A%7B%22name%22%3A%22IDLE%22%2C%22url%22%3Aundefined%2C%22timestamp%22%3A1299281911415%7D%2C%22lastinvited%22%3A1299281911415%2C%22userid%22%3A%2212992819114151265672533023080%22%2C%22vendorid%22%3A1%2C%22surveys%22%3A%5Bundefined%5D%7D; CodeSnippetContainerLang=C#; msdn=L=1033; ADS=SN=175A21EF; s_cc=true; s_sq=%5B%5BB%5D%5D; TocHashCookie=ms310241(n)/aa187916(n)/aa187917(n)/dd273952(n)/dd295083(n)/ff472634(n)/ee667046(n)/ee667070(n)/gg259047(n)/gg618436(n)/; WT_NVR=0=/:1=query|library|en-us:2=en-us/vcsharp|en-us/library" },
+ { @"Set-Cookie", @"A=I&I=AxUFAAAAAADsBgAA1sWZz4FGun/kOeyV4LGZVg!!&M=1; domain=.microsoft.com; expires=Sun, 30-Jun-2041 00:14:40 GMT; path=/" },
+ };
+
+ byte[] data = InternetMessageFormatHeaderParserTests.CreateBuffer(headerData.Select((kv) => { return String.Format("{0}: {1}", kv.Key, kv.Value); }).ToArray());
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ HttpHeaders headers;
+ InternetMessageFormatHeaderParser parser = InternetMessageFormatHeaderParserTests.CreateHeaderParser(data.Length, out headers);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed = 0;
+ ParserState state = InternetMessageFormatHeaderParserTests.ParseBufferInSteps(parser, data, cnt, out totalBytesConsumed);
+ Assert.Equal(ParserState.Done, state);
+ Assert.Equal(data.Length, totalBytesConsumed);
+
+ Assert.Equal(headerData.Count, headers.Count());
+ for (int hCnt = 0; hCnt < headerData.Count; hCnt++)
+ {
+ Assert.Equal(headerData.Keys.ElementAt(hCnt), headers.ElementAt(hCnt).Key);
+ Assert.Equal(headerData.Values.ElementAt(hCnt), headers.ElementAt(hCnt).Value.ElementAt(0));
+ }
+ }
+ }
+
+ [Fact]
+ [Trait("Description", "HeaderParser.ParseBuffer parses multi-line header field.")]
+ public void HeaderParserSplitHeader()
+ {
+ byte[] data = InternetMessageFormatHeaderParserTests.CreateBuffer("N:V1,", " V2,", "\tV3,", " V4,", " \tV5");
+
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ HttpHeaders headers;
+ InternetMessageFormatHeaderParser parser = InternetMessageFormatHeaderParserTests.CreateHeaderParser(data.Length, out headers);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed = 0;
+ ParserState state = InternetMessageFormatHeaderParserTests.ParseBufferInSteps(parser, data, cnt, out totalBytesConsumed);
+ Assert.Equal(ParserState.Done, state);
+ Assert.Equal(data.Length, totalBytesConsumed);
+
+ Assert.Equal(1, headers.Count());
+ IEnumerable<string> parsedValues = headers.GetValues("N");
+ Assert.Equal(1, parsedValues.Count());
+ Assert.Equal("V1, V2, V3, V4, \tV5", parsedValues.ElementAt(0));
+ }
+ }
+
+ [Fact]
+ [Trait("Description", "HeaderParser.ParseBuffer parses too big header with single header field.")]
+ public void HeaderParserDataTooBigSingle()
+ {
+ byte[] data = InternetMessageFormatHeaderParserTests.CreateBuffer("N:V");
+
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ HttpHeaders headers;
+ InternetMessageFormatHeaderParser parser = InternetMessageFormatHeaderParserTests.CreateHeaderParser(ParserData.MinHeaderSize, out headers);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed = 0;
+ ParserState state = InternetMessageFormatHeaderParserTests.ParseBufferInSteps(parser, data, cnt, out totalBytesConsumed);
+ Assert.Equal(ParserState.DataTooBig, state);
+ Assert.Equal(ParserData.MinHeaderSize, totalBytesConsumed);
+ }
+ }
+
+ [Fact]
+ [Trait("Description", "HeaderParser.ParseBuffer parses too big header with multiple header field.")]
+ public void HeaderParserTestDataTooBigMulti()
+ {
+ byte[] data = InternetMessageFormatHeaderParserTests.CreateBuffer("N1:V1", "N2:V2", "N3:V3");
+
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ HttpHeaders headers;
+ InternetMessageFormatHeaderParser parser = InternetMessageFormatHeaderParserTests.CreateHeaderParser(10, out headers);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed = 0;
+ ParserState state = InternetMessageFormatHeaderParserTests.ParseBufferInSteps(parser, data, cnt, out totalBytesConsumed);
+ Assert.Equal(ParserState.DataTooBig, state);
+ Assert.Equal(10, totalBytesConsumed);
+ }
+ }
+
+
+ // Set of samples from RFC 5322 with times adjusted to GMT following HTTP style for date time format.
+
+ static readonly string[] Rfc5322Sample1 = new string[] {
+ @"From: John Doe <jdoe@machine.example>",
+ @"To: Mary Smith <mary@example.net>",
+ @"Subject: Saying Hello",
+ @"Date: Fri, 21 Nov 1997 09:55:06 GMT",
+ @"Message-ID: <1234@local.machine.example>",
+ };
+
+ static readonly string[] Rfc5322Sample2 = new string[] {
+ @"From: John Doe <jdoe@machine.example>",
+ @"Sender: Michael Jones <mjones@machine.example>",
+ @"To: Mary Smith <mary@example.net>",
+ @"Subject: Saying Hello",
+ @"Date: Fri, 21 Nov 1997 09:55:06 GMT",
+ @"Message-ID: <1234@local.machine.example>",
+ };
+
+ static readonly string[] Rfc5322Sample3 = new string[] {
+ @"From: ""Joe Q. Public"" <john.q.public@example.com>",
+ @"To: Mary Smith <mary@x.test>, jdoe@example.org, Who? <one@y.test>",
+ @"Cc: <boss@nil.test>, ""Giant; \""Big\"" Box"" <sysservices@example.net>",
+ @"Date: Tue, 01 Jul 2003 10:52:37 GMT",
+ @"Message-ID: <5678.21-Nov-1997@example.com>",
+ };
+
+ static readonly string[] Rfc5322Sample4 = new string[] {
+ @"From: Pete <pete@silly.example>",
+ @"To: A Group:Ed Jones <c@a.test>,joe@where.test,John <jdoe@one.test>;",
+ @"Cc: Undisclosed recipients:;",
+ @"Date: Thu, 13 Feb 1969 23:32:54 GMT",
+ @"Message-ID: <testabcd.1234@silly.example>",
+ };
+
+ static readonly string[] Rfc5322Sample5 = new string[] {
+ @"From: John Doe <jdoe@machine.example>",
+ @"To: Mary Smith <mary@example.net>",
+ @"Subject: Saying Hello",
+ @"Date: Fri, 21 Nov 1997 09:55:06 GMT",
+ @"Message-ID: <1234@local.machine.example>",
+ };
+
+ static readonly string[] Rfc5322Sample6 = new string[] {
+ @"From: Mary Smith <mary@example.net>",
+ @"To: John Doe <jdoe@machine.example>",
+ @"Reply-To: ""Mary Smith: Personal Account"" <smith@home.example>",
+ @"Subject: Re: Saying Hello",
+ @"Date: Fri, 21 Nov 1997 10:01:10 GMT",
+ @"Message-ID: <3456@example.net>",
+ @"In-Reply-To: <1234@local.machine.example>",
+ @"References: <1234@local.machine.example>",
+ };
+
+ static readonly string[] Rfc5322Sample7 = new string[] {
+ @"To: ""Mary Smith: Personal Account"" <smith@home.example>",
+ @"From: John Doe <jdoe@machine.example>",
+ @"Subject: Re: Saying Hello",
+ @"Date: Fri, 21 Nov 1997 11:00:00 GMT",
+ @"Message-ID: <abcd.1234@local.machine.test>",
+ @"In-Reply-To: <3456@example.net>",
+ @"References: <1234@local.machine.example> <3456@example.net>",
+ };
+
+ static readonly string[] Rfc5322Sample8 = new string[] {
+ @"From: John Doe <jdoe@machine.example>",
+ @"To: Mary Smith <mary@example.net>",
+ @"Subject: Saying Hello",
+ @"Date: Fri, 21 Nov 1997 09:55:06 GMT",
+ @"Message-ID: <1234@local.machine.example>",
+ };
+
+ static readonly string[] Rfc5322Sample9 = new string[] {
+ @"Resent-From: Mary Smith <mary@example.net>",
+ @"Resent-To: Jane Brown <j-brown@other.example>",
+ @"Resent-Date: Mon, 24 Nov 1997 14:22:01 GMT",
+ @"Resent-Message-ID: <78910@example.net>",
+ @"From: John Doe <jdoe@machine.example>",
+ @"To: Mary Smith <mary@example.net>",
+ @"Subject: Saying Hello",
+ @"Date: Fri, 21 Nov 1997 09:55:06 GMT",
+ @"Message-ID: <1234@local.machine.example>",
+ };
+
+ static readonly string[] Rfc5322Sample10 = new string[] {
+ @"Received: from x.y.test",
+ @" by example.net",
+ @" via TCP",
+ @" with ESMTP",
+ @" id ABC12345",
+ @" for <mary@example.net>; 21 Nov 1997 10:05:43 GMT",
+ @"Received: from node.example by x.y.test; 21 Nov 1997 10:01:22 GMT",
+ @"From: John Doe <jdoe@node.example>",
+ @"To: Mary Smith <mary@example.net>",
+ @"Subject: Saying Hello",
+ @"Date: Fri, 21 Nov 1997 09:55:06 GMT",
+ @"Message-ID: <1234@local.node.example>",
+ };
+
+ static readonly string[] Rfc5322Sample11 = new string[] {
+ @"From: Pete(A nice \) chap) <pete(his account)@silly.test(his host)>",
+ @"To:A Group(Some people)",
+ @" :Chris Jones <c@(Chris's host.)public.example>,",
+ @" joe@example.org,",
+ @" John <jdoe@one.test> (my dear friend); (the end of the group)",
+ @"Cc:(Empty list)(start)Hidden recipients :(nobody(that I know)) ;",
+ @"Date: Thu,",
+ @" 13",
+ @" Feb",
+ @" 1969",
+ @" 23:32:00",
+ @" GMT",
+ @"Message-ID: <testabcd.1234@silly.test>",
+ };
+
+ [Fact]
+ [Trait("Description", "HeaderParser.ParseBuffer Rfc5322Sample1 header.")]
+ public void Rfc5322Sample1Test()
+ {
+ RunRfc5322SampleTest(Rfc5322Sample1,
+ (headers) =>
+ {
+ Assert.NotNull(headers);
+ Assert.True(headers.Contains("from"));
+ Assert.True(headers.Contains("to"));
+ Assert.True(headers.Contains("subject"));
+ Assert.True(headers.Contains("date"));
+ Assert.True(headers.Contains("message-id"));
+ });
+ }
+
+ [Fact]
+ [Trait("Description", "HeaderParser.ParseBuffer Rfc5322Sample2 header.")]
+ public void Rfc5322Sample2Test()
+ {
+ RunRfc5322SampleTest(Rfc5322Sample2,
+ (headers) =>
+ {
+ Assert.NotNull(headers);
+ Assert.True(headers.Contains("from"));
+ Assert.True(headers.Contains("sender"));
+ Assert.True(headers.Contains("to"));
+ Assert.True(headers.Contains("subject"));
+ Assert.True(headers.Contains("date"));
+ Assert.True(headers.Contains("message-id"));
+ });
+ }
+
+ [Fact]
+ [Trait("Description", "HeaderParser.ParseBuffer Rfc5322Sample3 header.")]
+ public void Rfc5322Sample3Test()
+ {
+ RunRfc5322SampleTest(Rfc5322Sample3,
+ (headers) =>
+ {
+ Assert.NotNull(headers);
+ Assert.True(headers.Contains("from"));
+ Assert.True(headers.Contains("to"));
+ Assert.True(headers.Contains("cc"));
+ Assert.True(headers.Contains("date"));
+ Assert.True(headers.Contains("message-id"));
+ });
+ }
+
+ [Fact]
+ [Trait("Description", "HeaderParser.ParseBuffer Rfc5322Sample4 header.")]
+ public void Rfc5322Sample4Test()
+ {
+ RunRfc5322SampleTest(Rfc5322Sample4,
+ (headers) =>
+ {
+ Assert.NotNull(headers);
+ Assert.True(headers.Contains("from"));
+ Assert.True(headers.Contains("to"));
+ Assert.True(headers.Contains("cc"));
+ Assert.True(headers.Contains("date"));
+ Assert.True(headers.Contains("message-id"));
+ });
+ }
+
+ [Fact]
+ [Trait("Description", "HeaderParser.ParseBuffer Rfc5322Sample5 header.")]
+ public void Rfc5322Sample5Test()
+ {
+ RunRfc5322SampleTest(Rfc5322Sample5,
+ (headers) =>
+ {
+ Assert.NotNull(headers);
+ Assert.True(headers.Contains("from"));
+ Assert.True(headers.Contains("to"));
+ Assert.True(headers.Contains("subject"));
+ Assert.True(headers.Contains("date"));
+ Assert.True(headers.Contains("message-id"));
+ });
+ }
+
+ [Fact]
+ [Trait("Description", "HeaderParser.ParseBuffer Rfc5322Sample6 header.")]
+ public void Rfc5322Sample6Test()
+ {
+ RunRfc5322SampleTest(Rfc5322Sample6,
+ (headers) =>
+ {
+ Assert.NotNull(headers);
+ Assert.True(headers.Contains("from"));
+ Assert.True(headers.Contains("to"));
+ Assert.True(headers.Contains("reply-to"));
+ Assert.True(headers.Contains("subject"));
+ Assert.True(headers.Contains("date"));
+ Assert.True(headers.Contains("message-id"));
+ Assert.True(headers.Contains("in-reply-to"));
+ Assert.True(headers.Contains("references"));
+ });
+ }
+
+ [Fact]
+ [Trait("Description", "HeaderParser.ParseBuffer Rfc5322Sample7 header.")]
+ public void Rfc5322Sample7Test()
+ {
+ RunRfc5322SampleTest(Rfc5322Sample7,
+ (headers) =>
+ {
+ Assert.NotNull(headers);
+ Assert.True(headers.Contains("to"));
+ Assert.True(headers.Contains("from"));
+ Assert.True(headers.Contains("subject"));
+ Assert.True(headers.Contains("date"));
+ Assert.True(headers.Contains("message-id"));
+ Assert.True(headers.Contains("in-reply-to"));
+ Assert.True(headers.Contains("references"));
+ });
+ }
+
+ [Fact]
+ [Trait("Description", "HeaderParser.ParseBuffer Rfc5322Sample8 header.")]
+ public void Rfc5322Sample8Test()
+ {
+ RunRfc5322SampleTest(Rfc5322Sample8,
+ (headers) =>
+ {
+ Assert.NotNull(headers);
+ Assert.True(headers.Contains("from"));
+ Assert.True(headers.Contains("to"));
+ Assert.True(headers.Contains("subject"));
+ Assert.True(headers.Contains("date"));
+ Assert.True(headers.Contains("message-id"));
+ });
+ }
+
+ [Fact]
+ [Trait("Description", "HeaderParser.ParseBuffer Rfc5322Sample9 header.")]
+ public void Rfc5322Sample9Test()
+ {
+ RunRfc5322SampleTest(Rfc5322Sample9,
+ (headers) =>
+ {
+ Assert.NotNull(headers);
+ Assert.True(headers.Contains("resent-from"));
+ Assert.True(headers.Contains("resent-to"));
+ Assert.True(headers.Contains("resent-date"));
+ Assert.True(headers.Contains("resent-message-id"));
+ Assert.True(headers.Contains("from"));
+ Assert.True(headers.Contains("to"));
+ Assert.True(headers.Contains("subject"));
+ Assert.True(headers.Contains("date"));
+ Assert.True(headers.Contains("message-id"));
+ });
+ }
+
+ [Fact]
+ [Trait("Description", "HeaderParser.ParseBuffer Rfc5322Sample10 header.")]
+ public void Rfc5322Sample10Test()
+ {
+ RunRfc5322SampleTest(Rfc5322Sample10,
+ (headers) =>
+ {
+ Assert.NotNull(headers);
+ Assert.True(headers.Contains("received"));
+ Assert.Equal(2, headers.GetValues("received").Count());
+ Assert.True(headers.Contains("from"));
+ Assert.True(headers.Contains("to"));
+ Assert.True(headers.Contains("subject"));
+ Assert.True(headers.Contains("date"));
+ Assert.True(headers.Contains("message-id"));
+ });
+ }
+
+ [Fact]
+ [Trait("Description", "HeaderParser.ParseBuffer Rfc5322Sample11 header.")]
+ public void Rfc5322Sample11Test()
+ {
+ RunRfc5322SampleTest(Rfc5322Sample11,
+ (headers) =>
+ {
+ Assert.NotNull(headers);
+ Assert.True(headers.Contains("from"));
+ Assert.True(headers.Contains("to"));
+ Assert.True(headers.Contains("cc"));
+ Assert.True(headers.Contains("date"));
+ Assert.True(headers.Contains("message-id"));
+ });
+ }
+
+ }
+} \ No newline at end of file
diff --git a/test/System.Net.Http.Formatting.Test.Unit/Formatting/Parsers/MimeMultipartParserTests.cs b/test/System.Net.Http.Formatting.Test.Unit/Formatting/Parsers/MimeMultipartParserTests.cs
new file mode 100644
index 00000000..ff7b1509
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/Formatting/Parsers/MimeMultipartParserTests.cs
@@ -0,0 +1,482 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using Microsoft.TestCommon;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Net.Http.Formatting.Parsers
+{
+ public class MimeMultipartParserTests
+ {
+ [Fact]
+ public void MimeMultipartParserTypeIsCorrect()
+ {
+ Assert.Type.HasProperties<InternetMessageFormatHeaderParser>(TypeAssert.TypeProperties.IsClass);
+ }
+
+ private static MimeMultipartParser CreateMimeMultipartParser(int maximumHeaderLength, string boundary)
+ {
+ return new MimeMultipartParser(boundary, maximumHeaderLength);
+ }
+
+ internal static byte[] CreateBuffer(string boundary, params string[] bodyparts)
+ {
+ return CreateBuffer(boundary, false, bodyparts);
+ }
+
+ internal static string CreateNestedBuffer(int count)
+ {
+ StringBuilder buffer = new StringBuilder("content");
+
+ for (var cnt = 0; cnt < count; cnt++)
+ {
+ byte[] nested = CreateBuffer("N" + cnt.ToString(), buffer.ToString());
+ var message = Encoding.UTF8.GetString(nested);
+ buffer.Length = 0;
+ buffer.AppendLine(message);
+ }
+
+ return buffer.ToString();
+ }
+
+ private static byte[] CreateBuffer(string boundary, bool withLws, params string[] bodyparts)
+ {
+ const string SP = " ";
+ const string HTAB = "\t";
+ const string CRLF = "\r\n";
+ const string DashDash = "--";
+
+ string lws = String.Empty;
+ if (withLws)
+ {
+ lws = SP + SP + HTAB + SP;
+ }
+
+ StringBuilder message = new StringBuilder();
+ message.Append(DashDash + boundary + lws + CRLF);
+ for (var cnt = 0; cnt < bodyparts.Length; cnt++)
+ {
+ message.Append(bodyparts[cnt]);
+ if (cnt < bodyparts.Length - 1)
+ {
+ message.Append(CRLF + DashDash + boundary + lws + CRLF);
+ }
+ }
+
+ // Note: We rely on a final CRLF even though it is not required by the BNF existing application do send it
+ message.Append(CRLF + DashDash + boundary + DashDash + lws + CRLF);
+ return Encoding.UTF8.GetBytes(message.ToString());
+ }
+
+ private static MimeMultipartParser.State ParseBufferInSteps(MimeMultipartParser parser, byte[] buffer, int readsize, out List<string> bodyParts, out int totalBytesConsumed)
+ {
+ MimeMultipartParser.State state = MimeMultipartParser.State.Invalid;
+ totalBytesConsumed = 0;
+ bodyParts = new List<string>();
+ bool isFinal = false;
+ byte[] currentBodyPart = new byte[32 * 1024];
+ int currentBodyLength = 0;
+
+ while (totalBytesConsumed <= buffer.Length)
+ {
+ int size = Math.Min(buffer.Length - totalBytesConsumed, readsize);
+ byte[] parseBuffer = new byte[size];
+ Buffer.BlockCopy(buffer, totalBytesConsumed, parseBuffer, 0, size);
+
+ int bytesConsumed = 0;
+ ArraySegment<byte> out1;
+ ArraySegment<byte> out2;
+ state = parser.ParseBuffer(parseBuffer, parseBuffer.Length, ref bytesConsumed, out out1, out out2, out isFinal);
+ totalBytesConsumed += bytesConsumed;
+
+ Buffer.BlockCopy(out1.Array, out1.Offset, currentBodyPart, currentBodyLength, out1.Count);
+ currentBodyLength += out1.Count;
+
+ Buffer.BlockCopy(out2.Array, out2.Offset, currentBodyPart, currentBodyLength, out2.Count);
+ currentBodyLength += out2.Count;
+
+ if (state == MimeMultipartParser.State.BodyPartCompleted)
+ {
+ var bPart = new byte[currentBodyLength];
+ Buffer.BlockCopy(currentBodyPart, 0, bPart, 0, currentBodyLength);
+ bodyParts.Add(Encoding.UTF8.GetString(bPart));
+ currentBodyLength = 0;
+ if (isFinal)
+ {
+ break;
+ }
+ }
+ else if (state != MimeMultipartParser.State.NeedMoreData)
+ {
+ return state;
+ }
+ }
+
+ Assert.True(isFinal);
+ return state;
+ }
+
+ public static IEnumerable<object[]> Boundaries
+ {
+ get { return ParserData.Boundaries; }
+ }
+
+ [Theory]
+ [PropertyData("Boundaries")]
+ public void MimeMultipartParserConstructorTest(string boundary)
+ {
+ MimeMultipartParser parser = new MimeMultipartParser(boundary, ParserData.MinMessageSize);
+ Assert.NotNull(parser);
+
+ Assert.ThrowsArgument(() => { new MimeMultipartParser("-", ParserData.MinMessageSize - 1); }, "maxMessageSize");
+
+ foreach (string empty in TestData.EmptyStrings)
+ {
+ Assert.ThrowsArgument(() => { new MimeMultipartParser(empty, ParserData.MinMessageSize); }, "boundary", allowDerivedExceptions: true);
+ }
+
+ Assert.ThrowsArgument(() => { new MimeMultipartParser("trailingspace ", ParserData.MinMessageSize); }, "boundary");
+
+ Assert.ThrowsArgumentNull(() => { new MimeMultipartParser(null, ParserData.MinMessageSize); }, "boundary");
+ }
+
+
+ [Fact]
+ public void MultipartParserNullBuffer()
+ {
+ MimeMultipartParser parser = CreateMimeMultipartParser(128, "-");
+ Assert.NotNull(parser);
+
+ int bytesConsumed = 0;
+ ArraySegment<byte> out1;
+ ArraySegment<byte> out2;
+ bool isFinal;
+ Assert.ThrowsArgumentNull(() => { parser.ParseBuffer(null, 0, ref bytesConsumed, out out1, out out2, out isFinal); }, "buffer");
+ }
+
+ [Theory]
+ [PropertyData("Boundaries")]
+ public void MultipartParserEmptyBuffer(string boundary)
+ {
+ byte[] data = CreateBuffer(boundary);
+
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ MimeMultipartParser parser = CreateMimeMultipartParser(data.Length, boundary);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed;
+ List<string> bodyParts;
+ MimeMultipartParser.State state = ParseBufferInSteps(parser, data, cnt, out bodyParts, out totalBytesConsumed);
+ Assert.Equal(MimeMultipartParser.State.BodyPartCompleted, state);
+ Assert.Equal(data.Length, totalBytesConsumed);
+
+ Assert.Equal(2, bodyParts.Count);
+ Assert.Equal(0, bodyParts[0].Length);
+ Assert.Equal(0, bodyParts[1].Length);
+ }
+ }
+
+ [Theory]
+ [PropertyData("Boundaries")]
+ public void MultipartParserSingleShortBodyPart(string boundary)
+ {
+
+ byte[] data = CreateBuffer(boundary, "A");
+
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ MimeMultipartParser parser = CreateMimeMultipartParser(data.Length, boundary);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed;
+ List<string> bodyParts;
+ MimeMultipartParser.State state = ParseBufferInSteps(parser, data, cnt, out bodyParts, out totalBytesConsumed);
+ Assert.Equal(MimeMultipartParser.State.BodyPartCompleted, state);
+ Assert.Equal(data.Length, totalBytesConsumed);
+
+ Assert.Equal(2, bodyParts.Count);
+ Assert.Equal(0, bodyParts[0].Length);
+ Assert.Equal(1, bodyParts[1].Length);
+ Assert.Equal("A", bodyParts[1]);
+ }
+ }
+
+ [Theory]
+ [PropertyData("Boundaries")]
+ public void MultipartParserMultipleShortBodyParts(string boundary)
+ {
+ string[] text = new string[] { "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" };
+
+ byte[] data = CreateBuffer(boundary, text);
+
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ MimeMultipartParser parser = CreateMimeMultipartParser(data.Length, boundary);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed;
+ List<string> bodyParts;
+ MimeMultipartParser.State state = ParseBufferInSteps(parser, data, cnt, out bodyParts, out totalBytesConsumed);
+ Assert.Equal(MimeMultipartParser.State.BodyPartCompleted, state);
+ Assert.Equal(data.Length, totalBytesConsumed);
+
+ Assert.Equal(text.Length + 1, bodyParts.Count);
+ Assert.Equal(0, bodyParts[0].Length);
+
+ for (var check = 0; check < text.Length; check++)
+ {
+ Assert.Equal(1, bodyParts[check + 1].Length);
+ Assert.Equal(text[check], bodyParts[check + 1]);
+ }
+ }
+ }
+
+ [Theory]
+ [PropertyData("Boundaries")]
+ public void MultipartParserMultipleShortBodyPartsWithLws(string boundary)
+ {
+ string[] text = new string[] { "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" };
+
+ byte[] data = CreateBuffer(boundary, true, text);
+
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ MimeMultipartParser parser = CreateMimeMultipartParser(data.Length, boundary);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed;
+ List<string> bodyParts;
+ MimeMultipartParser.State state = ParseBufferInSteps(parser, data, cnt, out bodyParts, out totalBytesConsumed);
+ Assert.Equal(MimeMultipartParser.State.BodyPartCompleted, state);
+ Assert.Equal(data.Length, totalBytesConsumed);
+
+ Assert.Equal(text.Length + 1, bodyParts.Count);
+ Assert.Equal(0, bodyParts[0].Length);
+
+ for (var check = 0; check < text.Length; check++)
+ {
+ Assert.Equal(1, bodyParts[check + 1].Length);
+ Assert.Equal(text[check], bodyParts[check + 1]);
+ }
+ }
+ }
+
+ [Theory]
+ [PropertyData("Boundaries")]
+ public void MultipartParserSingleLongBodyPart(string boundary)
+ {
+ const string text = "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789";
+
+ byte[] data = CreateBuffer(boundary, text);
+
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ MimeMultipartParser parser = CreateMimeMultipartParser(data.Length, boundary);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed;
+ List<string> bodyParts;
+ MimeMultipartParser.State state = ParseBufferInSteps(parser, data, cnt, out bodyParts, out totalBytesConsumed);
+ Assert.Equal(MimeMultipartParser.State.BodyPartCompleted, state);
+ Assert.Equal(data.Length, totalBytesConsumed);
+
+ Assert.Equal(2, bodyParts.Count);
+ Assert.Equal(0, bodyParts[0].Length);
+
+ Assert.Equal(text.Length, bodyParts[1].Length);
+ Assert.Equal(text, bodyParts[1]);
+ }
+ }
+
+ [Theory]
+ [PropertyData("Boundaries")]
+ public void MultipartParserMultipleLongBodyParts(string boundary)
+ {
+ const string middleText = "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789";
+ string[] text = new string[] {
+ "A" + middleText + "A",
+ "B" + middleText + "B",
+ "C" + middleText + "C",
+ "D" + middleText + "D",
+ "E" + middleText + "E",
+ "F" + middleText + "F",
+ "G" + middleText + "G",
+ "H" + middleText + "H",
+ "I" + middleText + "I",
+ "J" + middleText + "J",
+ "K" + middleText + "K",
+ "L" + middleText + "L",
+ "M" + middleText + "M",
+ "N" + middleText + "N",
+ "O" + middleText + "O",
+ "P" + middleText + "P",
+ "Q" + middleText + "Q",
+ "R" + middleText + "R",
+ "S" + middleText + "S",
+ "T" + middleText + "T",
+ "U" + middleText + "U",
+ "V" + middleText + "V",
+ "W" + middleText + "W",
+ "X" + middleText + "X",
+ "Y" + middleText + "Y",
+ "Z" + middleText + "Z"};
+
+ byte[] data = CreateBuffer(boundary, text);
+
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ MimeMultipartParser parser = CreateMimeMultipartParser(data.Length, boundary);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed;
+ List<string> bodyParts;
+ MimeMultipartParser.State state = ParseBufferInSteps(parser, data, cnt, out bodyParts, out totalBytesConsumed);
+ Assert.Equal(MimeMultipartParser.State.BodyPartCompleted, state);
+ Assert.Equal(data.Length, totalBytesConsumed);
+
+ Assert.Equal(text.Length + 1, bodyParts.Count);
+ Assert.Equal(0, bodyParts[0].Length);
+
+ for (var check = 0; check < text.Length; check++)
+ {
+ Assert.Equal(text[check].Length, bodyParts[check + 1].Length);
+ Assert.Equal(text[check], bodyParts[check + 1]);
+ }
+ }
+ }
+
+ [Theory]
+ [PropertyData("Boundaries")]
+ public void MultipartParserNearMatches(string boundary)
+ {
+ const string CR = "\r";
+ const string CRLF = "\r\n";
+ const string Dash = "-";
+ const string DashDash = "--";
+
+ string[] text = new string[] {
+ CR + Dash + "AAA",
+ CRLF + Dash + "AAA",
+ CRLF + DashDash + "AAA" + CR + "AAA",
+ CRLF,
+ "AAA",
+ "AAA" + CRLF,
+ CRLF + CRLF,
+ CRLF + CRLF + CRLF,
+ "AAA" + DashDash + "AAA",
+ CRLF + "AAA" + DashDash + "AAA" + DashDash,
+ CRLF + DashDash + "AAA" + CRLF,
+ CRLF + DashDash + "AAA" + CRLF + CRLF,
+ CRLF + DashDash + "AAA" + DashDash + CRLF,
+ CRLF + DashDash + "AAA" + DashDash + CRLF + CRLF
+ };
+
+ byte[] data = CreateBuffer(boundary, text);
+
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ MimeMultipartParser parser = CreateMimeMultipartParser(data.Length, boundary);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed;
+ List<string> bodyParts;
+ MimeMultipartParser.State state = ParseBufferInSteps(parser, data, cnt, out bodyParts, out totalBytesConsumed);
+ Assert.Equal(MimeMultipartParser.State.BodyPartCompleted, state);
+ Assert.Equal(data.Length, totalBytesConsumed);
+
+ Assert.Equal(text.Length + 1, bodyParts.Count);
+ Assert.Equal(0, bodyParts[0].Length);
+
+ for (var check = 0; check < text.Length; check++)
+ {
+ Assert.Equal(text[check].Length, bodyParts[check + 1].Length);
+ Assert.Equal(text[check], bodyParts[check + 1]);
+ }
+ }
+ }
+
+ [Theory]
+ [PropertyData("Boundaries")]
+ public void MultipartParserNesting(string boundary)
+ {
+ for (var nesting = 0; nesting < 16; nesting++)
+ {
+ string nested = CreateNestedBuffer(nesting);
+
+ byte[] data = CreateBuffer(boundary, nested);
+
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ MimeMultipartParser parser = CreateMimeMultipartParser(data.Length, boundary);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed;
+ List<string> bodyParts;
+ MimeMultipartParser.State state = ParseBufferInSteps(parser, data, cnt, out bodyParts, out totalBytesConsumed);
+ Assert.Equal(MimeMultipartParser.State.BodyPartCompleted, state);
+ Assert.Equal(data.Length, totalBytesConsumed);
+
+ Assert.Equal(2, bodyParts.Count);
+ Assert.Equal(0, bodyParts[0].Length);
+ Assert.Equal(nested.Length, bodyParts[1].Length);
+ }
+ }
+ }
+
+ [Theory]
+ [PropertyData("Boundaries")]
+ public void MimeMultipartParserTestDataTooBig(string boundary)
+ {
+ byte[] data = CreateBuffer(boundary);
+
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ MimeMultipartParser parser = CreateMimeMultipartParser(ParserData.MinMessageSize, boundary);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed;
+ List<string> bodyParts;
+ MimeMultipartParser.State state = ParseBufferInSteps(parser, data, cnt, out bodyParts, out totalBytesConsumed);
+ Assert.Equal(MimeMultipartParser.State.DataTooBig, state);
+ Assert.Equal(ParserData.MinMessageSize, totalBytesConsumed);
+ }
+ }
+
+ [Theory]
+ [PropertyData("Boundaries")]
+ public void MimeMultipartParserTestMultipartContent(string boundary)
+ {
+ MultipartContent content = new MultipartContent("mixed", boundary);
+ content.Add(new StringContent("A"));
+ content.Add(new StringContent("B"));
+ content.Add(new StringContent("C"));
+
+ MemoryStream memStream = new MemoryStream();
+ content.CopyToAsync(memStream).Wait();
+ memStream.Position = 0;
+ byte[] data = memStream.ToArray();
+
+ for (var cnt = 1; cnt <= data.Length; cnt++)
+ {
+ MimeMultipartParser parser = CreateMimeMultipartParser(data.Length, boundary);
+ Assert.NotNull(parser);
+
+ int totalBytesConsumed;
+ List<string> bodyParts;
+ MimeMultipartParser.State state = ParseBufferInSteps(parser, data, cnt, out bodyParts, out totalBytesConsumed);
+ Assert.Equal(MimeMultipartParser.State.BodyPartCompleted, state);
+ Assert.Equal(data.Length, totalBytesConsumed);
+
+ Assert.Equal(4, bodyParts.Count);
+ Assert.Empty(bodyParts[0]);
+
+ Assert.True(bodyParts[1].EndsWith("A"));
+ Assert.True(bodyParts[2].EndsWith("B"));
+ Assert.True(bodyParts[3].EndsWith("C"));
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/test/System.Net.Http.Formatting.Test.Unit/Formatting/QueryStringMappingTests.cs b/test/System.Net.Http.Formatting.Test.Unit/Formatting/QueryStringMappingTests.cs
new file mode 100644
index 00000000..5385bc03
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/Formatting/QueryStringMappingTests.cs
@@ -0,0 +1,202 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http.Formatting.DataSets;
+using System.Net.Http.Headers;
+using Microsoft.TestCommon;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Net.Http.Formatting
+{
+ public class QueryStringMappingTests
+ {
+ public static IEnumerable<string> UriStringsWithoutQuery
+ {
+ get
+ {
+ return HttpUnitTestDataSets.UriStrings.Where((s) => !s.Contains('?'));
+ }
+ }
+
+ [Fact]
+ [Trait("Description", "QueryStringMapping is public, concrete, and sealed.")]
+ public void TypeIsCorrect()
+ {
+ Assert.Type.HasProperties(
+ typeof(QueryStringMapping),
+ TypeAssert.TypeProperties.IsPublicVisibleClass | TypeAssert.TypeProperties.IsSealed,
+ typeof(MediaTypeMapping));
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalQueryStringParameterNames",
+ typeof(HttpUnitTestDataSets), "LegalQueryStringParameterValues",
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeHeaderValues")]
+ [Trait("Description", "QueryStringMapping(string, string, MediaTypeHeaderValue) sets properties.")]
+ public void Constructor(string queryStringParameterName, string queryStringParameterValue, MediaTypeHeaderValue mediaType)
+ {
+ QueryStringMapping mapping = new QueryStringMapping(queryStringParameterName, queryStringParameterValue, mediaType);
+ Assert.Equal(queryStringParameterName, mapping.QueryStringParameterName);
+ Assert.Equal(queryStringParameterValue, mapping.QueryStringParameterValue);
+ Assert.MediaType.AreEqual(mediaType, mapping.MediaType, "MediaType failed to set.");
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeHeaderValues",
+ typeof(CommonUnitTestDataSets), "EmptyStrings")]
+ [Trait("Description", "QueryStringMapping(string, string, MediaTypeHeaderValue) throws with empty QueryStringParameterName.")]
+ public void ConstructorThrowsWithEmptyQueryParameterName(MediaTypeHeaderValue mediaType, string queryStringParameterName)
+ {
+ Assert.ThrowsArgumentNull(() => new QueryStringMapping(queryStringParameterName, "json", mediaType), "queryStringParameterName");
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeHeaderValues",
+ typeof(CommonUnitTestDataSets), "EmptyStrings")]
+ [Trait("Description", "QueryStringMapping(string, string, MediaTypeHeaderValue) throws with empty QueryStringParameterValue.")]
+ public void ConstructorThrowsWithEmptyQueryParameterValue(MediaTypeHeaderValue mediaType, string queryStringParameterValue)
+ {
+ Assert.ThrowsArgumentNull(() => new QueryStringMapping("query", queryStringParameterValue, mediaType), "queryStringParameterValue");
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalQueryStringParameterNames",
+ typeof(HttpUnitTestDataSets), "LegalQueryStringParameterValues")]
+ [Trait("Description", "QueryStringMapping(string, string, MediaTypeHeaderValue) throws with null MediaTypeHeaderValue.")]
+ public void ConstructorThrowsWithNullMediaTypeHeaderValue(string queryStringParameterName, string queryStringParameterValue)
+ {
+ Assert.ThrowsArgumentNull(() => new QueryStringMapping(queryStringParameterName, queryStringParameterValue, (MediaTypeHeaderValue)null), "mediaType");
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalQueryStringParameterNames",
+ typeof(HttpUnitTestDataSets), "LegalQueryStringParameterValues",
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeStrings")]
+ [Trait("Description", "QueryStringMapping(string, string, string) sets properties.")]
+ public void Constructor1(string queryStringParameterName, string queryStringParameterValue, string mediaType)
+ {
+ QueryStringMapping mapping = new QueryStringMapping(queryStringParameterName, queryStringParameterValue, mediaType);
+ Assert.Equal(queryStringParameterName, mapping.QueryStringParameterName);
+ Assert.Equal(queryStringParameterValue, mapping.QueryStringParameterValue);
+ Assert.MediaType.AreEqual(mediaType, mapping.MediaType, "MediaType failed to set.");
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeStrings",
+ typeof(CommonUnitTestDataSets), "EmptyStrings")]
+ [Trait("Description", "QueryStringMapping(string, string, string) throws with empty QueryStringParameterName.")]
+ public void Constructor1ThrowsWithEmptyQueryParameterName(string mediaType, string queryStringParameterName)
+ {
+ Assert.ThrowsArgumentNull(() => new QueryStringMapping(queryStringParameterName, "json", mediaType), "queryStringParameterName");
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeStrings",
+ typeof(CommonUnitTestDataSets), "EmptyStrings")]
+ [Trait("Description", "QueryStringMapping(string, string, string) throws with empty QueryStringParameterValue.")]
+ public void Constructor1ThrowsWithEmptyQueryParameterValue(string mediaType, string queryStringParameterValue)
+ {
+ Assert.ThrowsArgumentNull(() => new QueryStringMapping("query", queryStringParameterValue, mediaType), "queryStringParameterValue");
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalQueryStringParameterNames",
+ typeof(HttpUnitTestDataSets), "LegalQueryStringParameterValues",
+ typeof(CommonUnitTestDataSets), "EmptyStrings")]
+ [Trait("Description", "QueryStringMapping(string, string, string) throws with empty MediaType.")]
+ public void Constructor1ThrowsWithEmptyMediaType(string queryStringParameterName, string queryStringParameterValue, string mediaType)
+ {
+ Assert.ThrowsArgumentNull(() => new QueryStringMapping(queryStringParameterName, queryStringParameterValue, (MediaTypeHeaderValue)null), "mediaType");
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalQueryStringParameterNames",
+ typeof(HttpUnitTestDataSets), "LegalQueryStringParameterValues",
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeStrings",
+ typeof(QueryStringMappingTests), "UriStringsWithoutQuery")]
+ [Trait("Description", "TryMatchMediaType(HttpRequestMessage) returns match when the QueryStringParameterName and QueryStringParameterValue are in the Uri.")]
+ public void TryMatchMediaTypeReturnsMatchWithQueryStringParameterNameAndValueInUri(string queryStringParameterName, string queryStringParameterValue, string mediaType, string uriBase)
+ {
+
+ QueryStringMapping mapping = new QueryStringMapping(queryStringParameterName, queryStringParameterValue, mediaType);
+ string uri = uriBase + "?" + queryStringParameterName + "=" + queryStringParameterValue;
+ HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, uri);
+ Assert.Equal(1.0, mapping.TryMatchMediaType(request));
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalQueryStringParameterNames",
+ typeof(HttpUnitTestDataSets), "LegalQueryStringParameterValues",
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeStrings",
+ typeof(QueryStringMappingTests), "UriStringsWithoutQuery")]
+ [Trait("Description", "TryMatchMediaType(HttpRequestMessage) returns 0.0 when the QueryStringParameterName is not in the Uri.")]
+ public void TryMatchMediaTypeReturnsZeroWithQueryStringParameterNameNotInUri(string queryStringParameterName, string queryStringParameterValue, string mediaType, string uriBase)
+ {
+ QueryStringMapping mapping = new QueryStringMapping(queryStringParameterName, queryStringParameterValue, mediaType);
+ string uri = uriBase + "?" + "not" + queryStringParameterName + "=" + queryStringParameterValue;
+ HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, uri);
+ Assert.Equal(0.0, mapping.TryMatchMediaType(request));
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalQueryStringParameterNames",
+ typeof(HttpUnitTestDataSets), "LegalQueryStringParameterValues",
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeStrings",
+ typeof(QueryStringMappingTests), "UriStringsWithoutQuery")]
+ [Trait("Description", "TryMatchMediaType(HttpRequestMessage) returns 0.0 when the QueryStringParameterValue is not in the Uri.")]
+ public void TryMatchMediaTypeReturnsZeroWithQueryStringParameterValueNotInUri(string queryStringParameterName, string queryStringParameterValue, string mediaType, string uriBase)
+ {
+ QueryStringMapping mapping = new QueryStringMapping(queryStringParameterName, queryStringParameterValue, mediaType);
+ string uri = uriBase + "?" + queryStringParameterName + "=" + "not" + queryStringParameterValue;
+ HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, uri);
+ Assert.Equal(0.0, mapping.TryMatchMediaType(request));
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalQueryStringParameterNames",
+ typeof(HttpUnitTestDataSets), "LegalQueryStringParameterValues",
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeStrings")]
+ [Trait("Description", "TryMatchMediaType(HttpRequestMessage) throws with a null HttpRequestMessage.")]
+ public void TryMatchMediaTypeThrowsWithNullHttpRequestMessage(string queryStringParameterName, string queryStringParameterValue, string mediaType)
+ {
+ QueryStringMapping mapping = new QueryStringMapping(queryStringParameterName, queryStringParameterValue, mediaType);
+ Assert.ThrowsArgumentNull(() => mapping.TryMatchMediaType(request: null), "request");
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalQueryStringParameterNames",
+ typeof(HttpUnitTestDataSets), "LegalQueryStringParameterValues",
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeStrings")]
+ [Trait("Description", "TryMatchMediaType(HttpRequestMessage) throws with a null Uri in HttpRequestMessage.")]
+ public void TryMatchMediaTypeThrowsWithNullUriInHttpRequestMessage(string queryStringParameterName, string queryStringParameterValue, string mediaType)
+ {
+ QueryStringMapping mapping = new QueryStringMapping(queryStringParameterName, queryStringParameterValue, mediaType);
+ string errorMessage = RS.Format(Properties.Resources.NonNullUriRequiredForMediaTypeMapping, typeof(QueryStringMapping).Name);
+ Assert.Throws<InvalidOperationException>(() => mapping.TryMatchMediaType(new HttpRequestMessage()), errorMessage);
+ }
+
+ [Theory]
+ [InlineData("nAmE", "VaLuE", "name=value")]
+ [InlineData("Format", "Xml", "format=xml")]
+ public void TryMatchMediaTypeIsCaseInsensitive(string name, string value, string query)
+ {
+ QueryStringMapping mapping = new QueryStringMapping(name, value, "application/json");
+ HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/?" + query);
+ Assert.Equal(1.0, mapping.TryMatchMediaType(request));
+ }
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/Formatting/RequestHeaderMappingTests.cs b/test/System.Net.Http.Formatting.Test.Unit/Formatting/RequestHeaderMappingTests.cs
new file mode 100644
index 00000000..03caeacd
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/Formatting/RequestHeaderMappingTests.cs
@@ -0,0 +1,236 @@
+using System.Net.Http.Formatting.DataSets;
+using System.Net.Http.Headers;
+using Microsoft.TestCommon;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Net.Http.Formatting
+{
+ public class RequestHeaderMappingTests
+ {
+
+ [Fact]
+ [Trait("Description", "RequestHeaderMapping is public, and concrete.")]
+ public void TypeIsCorrect()
+ {
+ Assert.Type.HasProperties(
+ typeof(RequestHeaderMapping),
+ TypeAssert.TypeProperties.IsPublicVisibleClass,
+ typeof(MediaTypeMapping));
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalHttpHeaderNames",
+ typeof(HttpUnitTestDataSets), "LegalHttpHeaderValues",
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeHeaderValues")]
+ [Trait("Description", "RequestHeaderMapping(string, string, StringComparison, bool, MediaTypeHeaderValue) sets properties.")]
+ public void Constructor(string headerName, string headerValue, MediaTypeHeaderValue mediaType)
+ {
+ RequestHeaderMapping mapping = new RequestHeaderMapping(headerName, headerValue, StringComparison.CurrentCulture, true, mediaType);
+ Assert.Equal(headerName, mapping.HeaderName);
+ Assert.Equal(headerValue, mapping.HeaderValue);
+ Assert.Equal(StringComparison.CurrentCulture, mapping.HeaderValueComparison);
+ Assert.Equal(true, mapping.IsValueSubstring);
+ Assert.MediaType.AreEqual(mediaType, mapping.MediaType, "MediaType failed to set.");
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeHeaderValues",
+ typeof(CommonUnitTestDataSets), "EmptyStrings")]
+ [Trait("Description", "RequestHeaderMapping(string, string, StringComparison, bool, MediaTypeHeaderValue) throws with empty headerName.")]
+ public void ConstructorThrowsWithEmptyHeaderName(MediaTypeHeaderValue mediaType, string headerName)
+ {
+ Assert.ThrowsArgumentNull(() => new RequestHeaderMapping(headerName, "value", StringComparison.CurrentCulture, false, mediaType), "headerName");
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeHeaderValues",
+ typeof(CommonUnitTestDataSets), "EmptyStrings")]
+ [Trait("Description", "RequestHeaderMapping(string, string, StringComparison, bool, MediaTypeHeaderValue) throws with empty headerValue.")]
+ public void ConstructorThrowsWithEmptyHeaderValue(MediaTypeHeaderValue mediaType, string headerValue)
+ {
+ Assert.ThrowsArgumentNull(() => new RequestHeaderMapping("name", headerValue, StringComparison.CurrentCulture, false, mediaType), "headerValue");
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalHttpHeaderNames",
+ typeof(HttpUnitTestDataSets), "LegalHttpHeaderValues")]
+ [Trait("Description", "RequestHeaderMapping(string, string, StringComparison, bool, MediaTypeHeaderValue) throws with null MediaTypeHeaderValue.")]
+ public void ConstructorThrowsWithNullMediaTypeHeaderValue(string headerName, string headerValue)
+ {
+ Assert.ThrowsArgumentNull(() => new RequestHeaderMapping(headerName, headerValue, StringComparison.CurrentCulture, false, (MediaTypeHeaderValue)null), "mediaType");
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalHttpHeaderNames",
+ typeof(HttpUnitTestDataSets), "LegalHttpHeaderValues",
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeHeaderValues")]
+ [Trait("Description", "RequestHeaderMapping(string, string, StringComparison, bool, MediaTypeHeaderValue) throws with invalid StringComparison.")]
+ public void ConstructorThrowsWithInvalidStringComparison(string headerName, string headerValue, MediaTypeHeaderValue mediaType)
+ {
+ int invalidValue = 999;
+ Assert.ThrowsInvalidEnumArgument(() => new RequestHeaderMapping(headerName, headerValue, (StringComparison)invalidValue, false, mediaType),
+ "valueComparison", invalidValue, typeof(StringComparison));
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalHttpHeaderNames",
+ typeof(HttpUnitTestDataSets), "LegalHttpHeaderValues",
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeStrings")]
+ [Trait("Description", "RequestHeaderMapping(string, string, StringComparison, bool, string) sets properties.")]
+ public void Constructor1(string headerName, string headerValue, string mediaType)
+ {
+ RequestHeaderMapping mapping = new RequestHeaderMapping(headerName, headerValue, StringComparison.CurrentCulture, true, mediaType);
+ Assert.Equal(headerName, mapping.HeaderName);
+ Assert.Equal(headerValue, mapping.HeaderValue);
+ Assert.Equal(StringComparison.CurrentCulture, mapping.HeaderValueComparison);
+ Assert.Equal(true, mapping.IsValueSubstring);
+ Assert.MediaType.AreEqual(mediaType, mapping.MediaType, "MediaType failed to set.");
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeStrings",
+ typeof(CommonUnitTestDataSets), "EmptyStrings")]
+ [Trait("Description", "RequestHeaderMapping(string, string, StringComparison, bool, string) throws with empty headerName.")]
+ public void Constructor1ThrowsWithEmptyHeaderName(string mediaType, string headerName)
+ {
+ Assert.ThrowsArgumentNull(() => new RequestHeaderMapping(headerName, "value", StringComparison.CurrentCulture, false, mediaType), "headerName");
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeStrings",
+ typeof(CommonUnitTestDataSets), "EmptyStrings")]
+ [Trait("Description", "RequestHeaderMapping(string, string, StringComparison, bool, string) throws with empty headerValue.")]
+ public void Constructor1ThrowsWithEmptyHeaderValue(string mediaType, string headerValue)
+ {
+ Assert.ThrowsArgumentNull(() => new RequestHeaderMapping("name", headerValue, StringComparison.CurrentCulture, false, mediaType), "headerValue");
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalHttpHeaderNames",
+ typeof(HttpUnitTestDataSets), "LegalHttpHeaderValues",
+ typeof(CommonUnitTestDataSets), "EmptyStrings")]
+ [Trait("Description", "RequestHeaderMapping(string, string, StringComparison, bool, string) throws with empty MediaTypeHeaderValue.")]
+ public void Constructor1ThrowsWithEmptyMediaType(string headerName, string headerValue, string mediaType)
+ {
+ Assert.ThrowsArgumentNull(() => new RequestHeaderMapping(headerName, headerValue, StringComparison.CurrentCulture, false, mediaType), "mediaType");
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalHttpHeaderNames",
+ typeof(HttpUnitTestDataSets), "LegalHttpHeaderValues",
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeStrings")]
+ [Trait("Description", "RequestHeaderMapping(string, string, StringComparison, bool, string) throws with invalid StringComparison.")]
+ public void Constructor1ThrowsWithInvalidStringComparison(string headerName, string headerValue, string mediaType)
+ {
+ int invalidValue = 999;
+ Assert.ThrowsInvalidEnumArgument(
+ () => new RequestHeaderMapping(headerName, headerValue, (StringComparison)invalidValue, false, mediaType),
+ "valueComparison", invalidValue, typeof(StringComparison));
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalHttpHeaderNames",
+ typeof(HttpUnitTestDataSets), "LegalHttpHeaderValues",
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeStrings",
+ typeof(CommonUnitTestDataSets), "Bools")]
+ [Trait("Description", "TryMatchMediaType(HttpRequestMessage) returns true when the HeaderName and HeaderValue are in the request.")]
+ public void TryMatchMediaTypeReturnsTrueWithNameAndValueInRequest(string headerName, string headerValue, string mediaType, bool subset)
+ {
+ RequestHeaderMapping mapping = new RequestHeaderMapping(headerName, headerValue, StringComparison.Ordinal, subset, mediaType);
+ HttpRequestMessage request = new HttpRequestMessage();
+ request.Headers.Add(headerName, headerValue);
+ Assert.Equal(1.0, mapping.TryMatchMediaType(request));
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalHttpHeaderNames",
+ typeof(HttpUnitTestDataSets), "LegalHttpHeaderValues",
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeStrings")]
+ [Trait("Description", "TryMatchMediaType(HttpRequestMessage) returns true when the HeaderName and a HeaderValue subset are in the request.")]
+ public void TryMatchMediaTypeReturnsTrueWithNameAndValueSubsetInRequest(string headerName, string headerValue, string mediaType)
+ {
+ RequestHeaderMapping mapping = new RequestHeaderMapping(headerName, headerValue, StringComparison.Ordinal, true, mediaType);
+ HttpRequestMessage request = new HttpRequestMessage();
+ request.Headers.Add(headerName, "prefix" + headerValue);
+ Assert.Equal(1.0, mapping.TryMatchMediaType(request));
+
+ request = new HttpRequestMessage();
+ request.Headers.Add(headerName, headerValue + "postfix");
+ Assert.Equal(1.0, mapping.TryMatchMediaType(request));
+
+ request = new HttpRequestMessage();
+ request.Headers.Add(headerName, "prefix" + headerValue + "postfix");
+ Assert.Equal(1.0, mapping.TryMatchMediaType(request));
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalHttpHeaderNames",
+ typeof(HttpUnitTestDataSets), "LegalHttpHeaderValues",
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeStrings")]
+ [Trait("Description", "TryMatchMediaType(HttpRequestMessage) returns false when HeaderName is not in the request.")]
+ public void TryMatchMediaTypeReturnsFalseWithNameNotInRequest(string headerName, string headerValue, string mediaType)
+ {
+ RequestHeaderMapping mapping = new RequestHeaderMapping(headerName, headerValue, StringComparison.Ordinal, false, mediaType);
+ HttpRequestMessage request = new HttpRequestMessage();
+ request.Headers.Add("prefix" + headerName, headerValue);
+ Assert.Equal(0.0, mapping.TryMatchMediaType(request));
+
+ request = new HttpRequestMessage();
+ request.Headers.Add(headerName + "postfix", headerValue);
+ Assert.Equal(0.0, mapping.TryMatchMediaType(request));
+
+ request = new HttpRequestMessage();
+ request.Headers.Add("prefix" + headerName + "postfix", headerValue);
+ Assert.Equal(0.0, mapping.TryMatchMediaType(request));
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalHttpHeaderNames",
+ typeof(HttpUnitTestDataSets), "LegalHttpHeaderValues",
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeStrings")]
+ [Trait("Description", "TryMatchMediaType(HttpRequestMessage) returns false when HeaderValue is not in the request.")]
+ public void TryMatchMediaTypeReturnsFalseWithValueNotInRequest(string headerName, string headerValue, string mediaType)
+ {
+ RequestHeaderMapping mapping = new RequestHeaderMapping(headerName, headerValue, StringComparison.Ordinal, false, mediaType);
+ HttpRequestMessage request = new HttpRequestMessage();
+ request.Headers.Add(headerName, "prefix" + headerValue);
+ Assert.Equal(0.0, mapping.TryMatchMediaType(request));
+
+ request = new HttpRequestMessage();
+ request.Headers.Add(headerName, headerValue + "postfix");
+ Assert.Equal(0.0, mapping.TryMatchMediaType(request));
+
+ request = new HttpRequestMessage();
+ request.Headers.Add(headerName, "prefix" + headerValue + "postfix");
+ Assert.Equal(0.0, mapping.TryMatchMediaType(request));
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalHttpHeaderNames",
+ typeof(HttpUnitTestDataSets), "LegalHttpHeaderValues",
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeStrings")]
+ [Trait("Description", "TryMatchMediaType(HttpResponseMessage) throws with a null HttpRequestMessage.")]
+ public void TryMatchMediaTypeThrowsWithNullHttpRequestMessage(string headerName, string headerValue, string mediaType)
+ {
+ RequestHeaderMapping mapping = new RequestHeaderMapping(headerName, headerValue, StringComparison.CurrentCulture, true, mediaType);
+ Assert.ThrowsArgumentNull(() => mapping.TryMatchMediaType(request: null), "request");
+ }
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/Formatting/ThresholdStreamTest.cs b/test/System.Net.Http.Formatting.Test.Unit/Formatting/ThresholdStreamTest.cs
new file mode 100644
index 00000000..cdec7c9d
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/Formatting/ThresholdStreamTest.cs
@@ -0,0 +1,114 @@
+using System.IO;
+using System.Net.Http.Internal;
+using System.Text;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Net.Http.Formatting
+{
+ public class ThresholdStreamTests
+ {
+ const string StringData = "abc,,,,,def";
+ const int PassThreshold = 40;
+ const int FailThreshold = 4;
+
+ [Fact]
+ public void HitLimit()
+ {
+ Stream inner = new MemoryStream(Encoding.UTF8.GetBytes(StringData));
+
+ ThresholdStream s = new ThresholdStream(inner, (byte)',', FailThreshold);
+
+ Assert.Throws<InvalidOperationException>(
+ () =>
+ {
+ // Attempt to read the stream. This should cross the limit set above and throw.
+ string content = new StreamReader(s).ReadToEnd();
+ });
+ }
+
+ [Fact]
+ public void NoHitLimit()
+ {
+ Stream inner = new MemoryStream(Encoding.UTF8.GetBytes(StringData));
+ ThresholdStream s = new ThresholdStream(inner, (byte)',', PassThreshold);
+
+ string actual = new StreamReader(s).ReadToEnd();
+
+ Assert.Equal(StringData, actual);
+ }
+
+ [Fact]
+ public void HitLimitAsync()
+ {
+ // Arrange
+ byte[] dataBytes = Encoding.UTF8.GetBytes(StringData);
+ Stream inner = new AsyncTestStream(new MemoryStream(dataBytes));
+ ThresholdStream s = new ThresholdStream(inner, (byte)',', FailThreshold);
+ byte[] buffer = new byte[StringData.Length];
+
+ // Act
+ IAsyncResult r = s.BeginRead(buffer, 0, buffer.Length, null, null);
+
+ Assert.Throws<InvalidOperationException>(
+ () =>
+ {
+ s.EndRead(r);
+ });
+ }
+
+ [Fact]
+ public void NoHitLimitAsync()
+ {
+ // Arrange
+ byte[] dataBytes = Encoding.UTF8.GetBytes(StringData);
+ Stream inner = new AsyncTestStream(new MemoryStream(dataBytes));
+ ThresholdStream s = new ThresholdStream(inner, (byte)',', PassThreshold);
+ byte[] buffer = new byte[StringData.Length];
+
+ // Act
+ IAsyncResult r = s.BeginRead(buffer, 0, buffer.Length, null, null);
+ s.EndRead(r);
+
+
+ // Assert
+ string actual = new StreamReader(new MemoryStream(buffer)).ReadToEnd();
+ Assert.Equal(actual, StringData);
+ }
+
+ // test-only Stream for verifying sync Read operations aren't called.
+ class AsyncTestStream : DelegatingStream
+ {
+ delegate int ReadDelegate(byte[] bytes, int index, int offset);
+ ReadDelegate _read;
+
+ public AsyncTestStream(Stream inner)
+ : base(inner)
+ {
+ }
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ throw new InvalidOperationException("don't use sync apis");
+ }
+
+ public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
+ {
+ Assert.Null(_read);
+ _read = _innerStream.Read;
+ return _read.BeginInvoke(buffer, offset, count, callback, state);
+ }
+ public override int EndRead(IAsyncResult asyncResult)
+ {
+ Assert.NotNull(_read);
+ try
+ {
+ return _read.EndInvoke(asyncResult);
+ }
+ finally
+ {
+ _read = null;
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/test/System.Net.Http.Formatting.Test.Unit/Formatting/UriPathExtensionMappingTests.cs b/test/System.Net.Http.Formatting.Test.Unit/Formatting/UriPathExtensionMappingTests.cs
new file mode 100644
index 00000000..f116cafd
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/Formatting/UriPathExtensionMappingTests.cs
@@ -0,0 +1,168 @@
+using System.Net.Http.Formatting.DataSets;
+using System.Net.Http.Headers;
+using Microsoft.TestCommon;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Net.Http.Formatting
+{
+ public class UriPathExtensionMappingTests
+ {
+ [Fact]
+ [Trait("Description", "UriPathExtensionMapping is public, concrete, and sealed.")]
+ public void TypeIsCorrect()
+ {
+ Assert.Type.HasProperties(
+ typeof(UriPathExtensionMapping),
+ TypeAssert.TypeProperties.IsPublicVisibleClass | TypeAssert.TypeProperties.IsSealed,
+ typeof(MediaTypeMapping));
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalUriPathExtensions",
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeStrings")]
+ [Trait("Description", "UriPathExtensionMapping(string, string) sets UriPathExtension and MediaType.")]
+ public void Constructor(string uriPathExtension, string mediaType)
+ {
+ UriPathExtensionMapping mapping = new UriPathExtensionMapping(uriPathExtension, mediaType);
+ Assert.Equal(uriPathExtension, mapping.UriPathExtension);
+ Assert.MediaType.AreEqual(mediaType, mapping.MediaType, "Failed to set MediaType.");
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(CommonUnitTestDataSets), "EmptyStrings",
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeStrings")]
+ [Trait("Description", "UriPathExtensionMapping(string, string) throws if the UriPathExtensions parameter is null.")]
+ public void ConstructorThrowsWithEmptyUriPathExtension(string uriPathExtension, string mediaType)
+ {
+ Assert.ThrowsArgumentNull(() => new UriPathExtensionMapping(uriPathExtension, mediaType), "uriPathExtension");
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalUriPathExtensions",
+ typeof(CommonUnitTestDataSets), "EmptyStrings")]
+ [Trait("Description", "UriPathExtensionMapping(string, string) throws if the MediaType (string) parameter is empty.")]
+ public void ConstructorThrowsWithEmptyMediaType(string uriPathExtension, string mediaType)
+ {
+ Assert.ThrowsArgumentNull(() => new UriPathExtensionMapping(uriPathExtension, mediaType), "mediaType");
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalUriPathExtensions",
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeHeaderValues")]
+ [Trait("Description", "UriPathExtensionMapping(string, MediaTypeHeaderValue) sets UriPathExtension and MediaType.")]
+ public void Constructor1(string uriPathExtension, MediaTypeHeaderValue mediaType)
+ {
+ UriPathExtensionMapping mapping = new UriPathExtensionMapping(uriPathExtension, mediaType);
+ Assert.Equal(uriPathExtension, mapping.UriPathExtension);
+ Assert.MediaType.AreEqual(mediaType, mapping.MediaType, "Failed to set MediaType.");
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(CommonUnitTestDataSets), "EmptyStrings",
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeHeaderValues")]
+ [Trait("Description", "UriPathExtensionMapping(string, MediaTypeHeaderValue) throws if the UriPathExtensions parameter is null.")]
+ public void Constructor1ThrowsWithEmptyUriPathExtension(string uriPathExtension, MediaTypeHeaderValue mediaType)
+ {
+ Assert.ThrowsArgumentNull(() => new UriPathExtensionMapping(uriPathExtension, mediaType), "uriPathExtension");
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalUriPathExtensions")]
+ [Trait("Description", "UriPathExtensionMapping(string, MediaTypeHeaderValue) constructor throws if the mediaType parameter is null.")]
+ public void Constructor1ThrowsWithNullMediaType(string uriPathExtension)
+ {
+ Assert.ThrowsArgumentNull(() => new UriPathExtensionMapping(uriPathExtension, (MediaTypeHeaderValue)null), "mediaType");
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalUriPathExtensions",
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeStrings",
+ typeof(HttpUnitTestDataSets), "UriStrings")]
+ [Trait("Description", "TryMatchMediaType(HttpRequestMessage) returns 1.0 when the extension is in the Uri.")]
+ public void TryMatchMediaTypeReturnsMatchWithExtensionInUri(string uriPathExtension, string mediaType, string baseUriString)
+ {
+ UriPathExtensionMapping mapping = new UriPathExtensionMapping(uriPathExtension, mediaType);
+ Uri baseUri = new Uri(baseUriString);
+ Uri uri = new Uri(baseUri, "x." + uriPathExtension);
+ HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, uri);
+ Assert.Equal(1.0, mapping.TryMatchMediaType(request));
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalUriPathExtensions",
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeStrings",
+ typeof(HttpUnitTestDataSets), "UriStrings")]
+ [Trait("Description", "TryMatchMediaType(HttpRequestMessage) returns 1.0 when the extension is in the Uri but differs in case")]
+ public void TryMatchMediaTypeReturnsMatchWithExtensionInUriDifferCase(string uriPathExtension, string mediaType, string baseUriString)
+ {
+ UriPathExtensionMapping mapping = new UriPathExtensionMapping(uriPathExtension.ToUpper(), mediaType);
+ Uri baseUri = new Uri(baseUriString);
+ Uri uri = new Uri(baseUri, "x." + uriPathExtension.ToLower());
+ HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, uri);
+ Assert.Equal(1.0, mapping.TryMatchMediaType(request));
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalUriPathExtensions",
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeStrings",
+ typeof(HttpUnitTestDataSets), "UriStrings")]
+ [Trait("Description", "TryMatchMediaType(HttpRequestMessage) returns 0.0 when the extension is not in the Uri.")]
+ public void TryMatchMediaTypeReturnsZeroWithExtensionNotInUri(string uriPathExtension, string mediaType, string baseUriString)
+ {
+ UriPathExtensionMapping mapping = new UriPathExtensionMapping(uriPathExtension, mediaType);
+ Uri baseUri = new Uri(baseUriString);
+ Uri uri = new Uri(baseUri, "x.");
+ HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, uri);
+ Assert.Equal(0.0, mapping.TryMatchMediaType(request));
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalUriPathExtensions",
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeStrings",
+ typeof(HttpUnitTestDataSets), "UriStrings")]
+ [Trait("Description", "TryMatchMediaType(HttpRequestMessage) returns 0.0 when the uri contains the extension but does not end with it.")]
+ public void TryMatchMediaTypeReturnsZeroWithExtensionNotLastInUri(string uriPathExtension, string mediaType, string baseUriString)
+ {
+ UriPathExtensionMapping mapping = new UriPathExtensionMapping(uriPathExtension, mediaType);
+ Uri baseUri = new Uri(baseUriString);
+ Uri uri = new Uri(baseUri, "x." + uriPathExtension + "z");
+ HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, uri);
+ Assert.Equal(0.0, mapping.TryMatchMediaType(request));
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalUriPathExtensions",
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeStrings")]
+ [Trait("Description", "TryMatchMediaType(HttpRequestMessage) throws if the request is null.")]
+ public void TryMatchMediaTypeThrowsWithNullHttpRequestMessage(string uriPathExtension, string mediaType)
+ {
+ UriPathExtensionMapping mapping = new UriPathExtensionMapping(uriPathExtension, mediaType);
+ Assert.ThrowsArgumentNull(() => mapping.TryMatchMediaType(request: null), "request");
+ }
+
+ [Theory]
+ [TestDataSet(
+ typeof(HttpUnitTestDataSets), "LegalUriPathExtensions",
+ typeof(HttpUnitTestDataSets), "LegalMediaTypeStrings")]
+ [Trait("Description", "TryMatchMediaType(HttpRequestMessage) throws if the Uri in the request is null.")]
+ public void TryMatchMediaTypeThrowsWithNullUriInHttpRequestMessage(string uriPathExtension, string mediaType)
+ {
+ UriPathExtensionMapping mapping = new UriPathExtensionMapping(uriPathExtension, mediaType);
+ string errorMessage = RS.Format(Properties.Resources.NonNullUriRequiredForMediaTypeMapping, typeof(UriPathExtensionMapping).Name);
+ Assert.Throws<InvalidOperationException>(() => mapping.TryMatchMediaType(new HttpRequestMessage()), errorMessage);
+ }
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/Formatting/XmlMediaTypeFormatterTests.cs b/test/System.Net.Http.Formatting.Test.Unit/Formatting/XmlMediaTypeFormatterTests.cs
new file mode 100644
index 00000000..0d48a97e
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/Formatting/XmlMediaTypeFormatterTests.cs
@@ -0,0 +1,304 @@
+using System.IO;
+using System.Net.Http.Formatting.DataSets;
+using System.Net.Http.Headers;
+using System.Runtime.Serialization;
+using System.Text;
+using System.Xml.Serialization;
+using Microsoft.TestCommon;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Net.Http.Formatting
+{
+ public class XmlMediaTypeFormatterTests
+ {
+ [Fact]
+ [Trait("Description", "XmlMediaTypeFormatter is public, concrete, and unsealed.")]
+ public void TypeIsCorrect()
+ {
+ Assert.Type.HasProperties(typeof(XmlMediaTypeFormatter), TypeAssert.TypeProperties.IsPublicVisibleClass);
+ }
+
+ [Theory]
+ [TestDataSet(typeof(HttpUnitTestDataSets), "StandardXmlMediaTypes")]
+ [Trait("Description", "XmlMediaTypeFormatter() constructor sets standard Xml media types in SupportedMediaTypes.")]
+ public void Constructor(MediaTypeHeaderValue mediaType)
+ {
+ XmlMediaTypeFormatter formatter = new XmlMediaTypeFormatter();
+ Assert.True(formatter.SupportedMediaTypes.Contains(mediaType), String.Format("SupportedMediaTypes should have included {0}.", mediaType.ToString()));
+ }
+
+ [Fact]
+ [Trait("Description", "DefaultMediaType property returns application/xml.")]
+ public void DefaultMediaTypeReturnsApplicationXml()
+ {
+ MediaTypeHeaderValue mediaType = XmlMediaTypeFormatter.DefaultMediaType;
+ Assert.NotNull(mediaType);
+ Assert.Equal("application/xml", mediaType.MediaType);
+ }
+
+ [Fact]
+ [Trait("Description", "CharacterEncoding property handles Get/Set correctly.")]
+ public void CharacterEncodingGetSet()
+ {
+ XmlMediaTypeFormatter xmlFormatter = new XmlMediaTypeFormatter();
+ Assert.IsType<UTF8Encoding>(xmlFormatter.CharacterEncoding);
+ xmlFormatter.CharacterEncoding = Encoding.Unicode;
+ Assert.Same(Encoding.Unicode, xmlFormatter.CharacterEncoding);
+ xmlFormatter.CharacterEncoding = Encoding.UTF8;
+ Assert.Same(Encoding.UTF8, xmlFormatter.CharacterEncoding);
+ }
+
+ [Fact]
+ [Trait("Description", "CharacterEncoding property throws on invalid arguments")]
+ public void CharacterEncodingSetThrows()
+ {
+ XmlMediaTypeFormatter xmlFormatter = new XmlMediaTypeFormatter();
+ Assert.ThrowsArgumentNull(() => { xmlFormatter.CharacterEncoding = null; }, "value");
+ Assert.ThrowsArgument(() => { xmlFormatter.CharacterEncoding = Encoding.UTF32; }, "value");
+ }
+
+ [Fact]
+ [Trait("Description", "UseDataContractSerializer property should be false by default.")]
+ public void UseDataContractSerializer_Default()
+ {
+ XmlMediaTypeFormatter xmlFormatter = new XmlMediaTypeFormatter();
+ Assert.False(xmlFormatter.UseDataContractSerializer, "The UseDataContractSerializer property should be false by default.");
+ }
+
+ [Fact]
+ [Trait("Description", "UseDataContractSerializer property works when set to true.")]
+ public void UseDataContractSerializer_True()
+ {
+ XmlMediaTypeFormatter xmlFormatter = new XmlMediaTypeFormatter { UseDataContractSerializer = true };
+ MemoryStream memoryStream = new MemoryStream();
+ HttpContentHeaders contentHeaders = new StringContent(String.Empty).Headers;
+ Assert.Task.Succeeds(xmlFormatter.WriteToStreamAsync(typeof(SampleType), new SampleType(), memoryStream, contentHeaders, transportContext: null));
+ memoryStream.Position = 0;
+ string serializedString = new StreamReader(memoryStream).ReadToEnd();
+ Assert.True(serializedString.Contains("DataContractSampleType"),
+ "SampleType should be serialized with data contract name DataContractSampleType because UseDataContractSerializer is set to true.");
+ }
+
+ [Fact]
+ [Trait("Description", "UseDataContractSerializer property works when set to false.")]
+ public void UseDataContractSerializer_False()
+ {
+ XmlMediaTypeFormatter xmlFormatter = new XmlMediaTypeFormatter { UseDataContractSerializer = false };
+ MemoryStream memoryStream = new MemoryStream();
+ HttpContentHeaders contentHeaders = new StringContent(String.Empty).Headers;
+ Assert.Task.Succeeds(xmlFormatter.WriteToStreamAsync(typeof(SampleType), new SampleType(), memoryStream, contentHeaders, transportContext: null));
+ memoryStream.Position = 0;
+ string serializedString = new StreamReader(memoryStream).ReadToEnd();
+ Assert.False(serializedString.Contains("DataContractSampleType"),
+ "SampleType should not be serialized with data contract name DataContractSampleType because UseDataContractSerializer is set to false.");
+ }
+
+ [Theory]
+ [TestDataSet(typeof(CommonUnitTestDataSets), "RepresentativeValueAndRefTypeTestDataCollection")]
+ [Trait("Description", "CanReadType() returns the same result as the XmlSerializer constructor.")]
+ public void CanReadTypeReturnsSameResultAsXmlSerializerConstructor(Type variationType, object testData)
+ {
+ TestXmlMediaTypeFormatter formatter = new TestXmlMediaTypeFormatter();
+
+ bool isSerializable = IsSerializableWithXmlSerializer(variationType, testData);
+ bool canSupport = formatter.CanReadTypeCaller(variationType);
+ if (isSerializable != canSupport)
+ {
+ Assert.Equal(isSerializable, canSupport);
+ }
+
+ // Ask a 2nd time to probe whether the cached result is treated the same
+ canSupport = formatter.CanReadTypeCaller(variationType);
+ Assert.Equal(isSerializable, canSupport);
+
+ }
+
+ [Theory]
+ [TestDataSet(typeof(JsonMediaTypeFormatterTests), "JsonValueTypes")]
+ [Trait("Description", "CanReadType() returns false on JsonValue.")]
+ public void CanReadTypeReturnsFalseOnJsonValue(Type type)
+ {
+ TestXmlMediaTypeFormatter formatter = new TestXmlMediaTypeFormatter();
+ Assert.False(formatter.CanReadTypeCaller(type), "formatter should have returned false.");
+ }
+
+ [Theory]
+ [TestDataSet(typeof(JsonMediaTypeFormatterTests), "JsonValueTypes")]
+ [Trait("Description", "CanWriteType() returns false on JsonValue.")]
+ public void CanWriteTypeReturnsFalseOnJsonValue(Type type)
+ {
+ TestXmlMediaTypeFormatter formatter = new TestXmlMediaTypeFormatter();
+ Assert.False(formatter.CanWriteTypeCaller(type), "formatter should have returned false.");
+ }
+
+ [Fact]
+ [Trait("Description", "SetSerializer(Type, XmlSerializer) throws with null type.")]
+ public void SetSerializerThrowsWithNullType()
+ {
+ XmlMediaTypeFormatter formatter = new XmlMediaTypeFormatter();
+ XmlSerializer xmlSerializer = new XmlSerializer(typeof(string));
+ Assert.ThrowsArgumentNull(() => { formatter.SetSerializer(null, xmlSerializer); }, "type");
+ }
+
+ [Fact]
+ [Trait("Description", "SetSerializer(Type, XmlSerializer) throws with null serializer.")]
+ public void SetSerializerThrowsWithNullSerializer()
+ {
+ XmlMediaTypeFormatter formatter = new XmlMediaTypeFormatter();
+ Assert.ThrowsArgumentNull(() => { formatter.SetSerializer(typeof(string), (XmlSerializer)null); }, "serializer");
+ }
+
+ [Fact]
+ [Trait("Description", "SetSerializer<T>(XmlSerializer) throws with null serializer.")]
+ public void SetSerializer1ThrowsWithNullSerializer()
+ {
+ XmlMediaTypeFormatter formatter = new XmlMediaTypeFormatter();
+ Assert.ThrowsArgumentNull(() => { formatter.SetSerializer<string>((XmlSerializer)null); }, "serializer");
+ }
+
+ [Fact]
+ [Trait("Description", "SetSerializer(Type, XmlObjectSerializer) throws with null type.")]
+ public void SetSerializer2ThrowsWithNullType()
+ {
+ XmlMediaTypeFormatter formatter = new XmlMediaTypeFormatter();
+ XmlObjectSerializer xmlObjectSerializer = new DataContractSerializer(typeof(string));
+ Assert.ThrowsArgumentNull(() => { formatter.SetSerializer(null, xmlObjectSerializer); }, "type");
+ }
+
+ [Fact]
+ [Trait("Description", "SetSerializer(Type, XmlObjectSerializer) throws with null serializer.")]
+ public void SetSerializer2ThrowsWithNullSerializer()
+ {
+ XmlMediaTypeFormatter formatter = new XmlMediaTypeFormatter();
+ Assert.ThrowsArgumentNull(() => { formatter.SetSerializer(typeof(string), (XmlObjectSerializer)null); }, "serializer");
+ }
+
+ [Fact]
+ [Trait("Description", "SetSerializer<T>(XmlObjectSerializer) throws with null serializer.")]
+ public void SetSerializer3ThrowsWithNullSerializer()
+ {
+ XmlMediaTypeFormatter formatter = new XmlMediaTypeFormatter();
+ Assert.ThrowsArgumentNull(() => { formatter.SetSerializer<string>((XmlSerializer)null); }, "serializer");
+ }
+
+ [Fact]
+ [Trait("Description", "RemoveSerializer throws with null type.")]
+ public void RemoveSerializerThrowsWithNullType()
+ {
+ XmlMediaTypeFormatter formatter = new XmlMediaTypeFormatter();
+ Assert.ThrowsArgumentNull(() => { formatter.RemoveSerializer(null); }, "type");
+ }
+
+ [Theory]
+ [TestDataSet(typeof(CommonUnitTestDataSets), "RepresentativeValueAndRefTypeTestDataCollection")]
+ [Trait("Description", "ReadFromStreamAsync() returns all value and reference types serialized via WriteToStreamAsync using XmlSerializer.")]
+ public void ReadFromStreamAsyncRoundTripsWriteToStreamAsyncUsingXmlSerializer(Type variationType, object testData)
+ {
+ TestXmlMediaTypeFormatter formatter = new TestXmlMediaTypeFormatter();
+ HttpContentHeaders contentHeaders = new StringContent(String.Empty).Headers;
+
+ bool canSerialize = IsSerializableWithXmlSerializer(variationType, testData) && Assert.Http.CanRoundTrip(variationType);
+ if (canSerialize)
+ {
+ formatter.SetSerializer(variationType, new XmlSerializer(variationType));
+
+ object readObj = null;
+ Assert.Stream.WriteAndRead(
+ stream => Assert.Task.Succeeds(formatter.WriteToStreamAsync(variationType, testData, stream, contentHeaders, transportContext: null)),
+ stream => readObj = Assert.Task.SucceedsWithResult(formatter.ReadFromStreamAsync(variationType, stream, contentHeaders, null))
+ );
+ Assert.Equal(testData, readObj);
+ }
+ }
+
+ [Theory]
+ [TestDataSet(typeof(CommonUnitTestDataSets), "RepresentativeValueAndRefTypeTestDataCollection")]
+ [Trait("Description", "ReadFromStream() returns all value and reference types serialized via WriteToStream using DataContractSerializer.")]
+ public void ReadFromStreamAsyncRoundTripsWriteToStreamUsingDataContractSerializer(Type variationType, object testData)
+ {
+ TestXmlMediaTypeFormatter formatter = new TestXmlMediaTypeFormatter();
+ HttpContentHeaders contentHeaders = new StringContent(String.Empty).Headers;
+
+ bool canSerialize = IsSerializableWithDataContractSerializer(variationType, testData) && Assert.Http.CanRoundTrip(variationType);
+ if (canSerialize)
+ {
+ formatter.SetSerializer(variationType, new DataContractSerializer(variationType));
+
+ object readObj = null;
+ Assert.Stream.WriteAndRead(
+ stream => Assert.Task.Succeeds(formatter.WriteToStreamAsync(variationType, testData, stream, contentHeaders, transportContext: null)),
+ stream => readObj = Assert.Task.SucceedsWithResult(formatter.ReadFromStreamAsync(variationType, stream, contentHeaders, null))
+ );
+ Assert.Equal(testData, readObj);
+ }
+ }
+
+ public class TestXmlMediaTypeFormatter : XmlMediaTypeFormatter
+ {
+ public bool CanReadTypeCaller(Type type)
+ {
+ return CanReadType(type);
+ }
+
+ public bool CanWriteTypeCaller(Type type)
+ {
+ return CanWriteType(type);
+ }
+ }
+
+ [DataContract(Name = "DataContractSampleType")]
+ public class SampleType
+ {
+ [DataMember]
+ public int Number { get; set; }
+ }
+
+ private bool IsSerializableWithXmlSerializer(Type type, object obj)
+ {
+ if (Assert.Http.IsKnownUnserializable(type, obj))
+ {
+ return false;
+ }
+
+ try
+ {
+ new XmlSerializer(type);
+ if (obj != null && obj.GetType() != type)
+ {
+ new XmlSerializer(obj.GetType());
+ }
+ }
+ catch
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ private bool IsSerializableWithDataContractSerializer(Type type, object obj)
+ {
+ if (Assert.Http.IsKnownUnserializable(type, obj))
+ {
+ return false;
+ }
+
+ try
+ {
+ new DataContractSerializer(type);
+ if (obj != null && obj.GetType() != type)
+ {
+ new DataContractSerializer(obj.GetType());
+ }
+ }
+ catch
+ {
+ return false;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/FormattingUtilitiesTests.cs b/test/System.Net.Http.Formatting.Test.Unit/FormattingUtilitiesTests.cs
new file mode 100644
index 00000000..3664c75b
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/FormattingUtilitiesTests.cs
@@ -0,0 +1,95 @@
+using System.Json;
+using System.Linq;
+using System.Net.Http.Headers;
+using Microsoft.TestCommon;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Net.Http
+{
+ public class FormattingUtilitiesTests
+ {
+ public static TheoryDataSet<string, string> QuotedStrings
+ {
+ get
+ {
+ return new TheoryDataSet<string, string>()
+ {
+ { @"""""", @"" },
+ { @"""string""", @"string" },
+ { @"string", @"string" },
+ { @"""str""ing""", @"str""ing" },
+ };
+ }
+ }
+
+ public static TheoryDataSet<string> NotQuotedStrings
+ {
+ get
+ {
+ return new TheoryDataSet<string>
+ {
+ @" """,
+ @" """"",
+ @"string",
+ @"str""ing",
+ @"s""trin""g",
+ };
+ }
+ }
+
+ [Fact]
+ [Trait("Description", "Utilities is internal static type.")]
+ public void TypeIsCorrect()
+ {
+ Assert.Type.HasProperties(typeof(FormattingUtilities), TypeAssert.TypeProperties.IsClass | TypeAssert.TypeProperties.IsStatic);
+ }
+
+ [Fact]
+ [Trait("Description", "IsJsonValueType returns true")]
+ public void IsJsonValueTypeReturnsTrue()
+ {
+ Assert.True(FormattingUtilities.IsJsonValueType(typeof(JsonValue)), "Should return true");
+ Assert.True(FormattingUtilities.IsJsonValueType(typeof(JsonPrimitive)), "Should return true");
+ Assert.True(FormattingUtilities.IsJsonValueType(typeof(JsonObject)), "Should return true");
+ Assert.True(FormattingUtilities.IsJsonValueType(typeof(JsonArray)), "Should return true");
+ }
+
+ [Fact]
+ [Trait("Description", "CreateEmptyContentHeaders returns empty headers")]
+ public void CreateEmptyContentHeadersReturnsEmptyHeaders()
+ {
+ HttpContentHeaders headers = FormattingUtilities.CreateEmptyContentHeaders();
+ Assert.NotNull(headers);
+ Assert.Equal(0, headers.Count());
+ }
+
+ [Theory]
+ [TestDataSet(typeof(CommonUnitTestDataSets), "EmptyStrings")]
+ [Trait("Description", "UnquoteToken returns same string on null, empty strings")]
+ public void UnquoteTokenReturnsSameRefOnEmpty(string empty)
+ {
+ string result = FormattingUtilities.UnquoteToken(empty);
+ Assert.Same(empty, result);
+ }
+
+ [Theory]
+ [PropertyData("NotQuotedStrings")]
+ [Trait("Description", "UnquoteToken returns unquoted strings")]
+ public void UnquoteTokenReturnsSameRefNonQuotedStrings(string test)
+ {
+ string result = FormattingUtilities.UnquoteToken(test);
+ Assert.Equal(test, result);
+ }
+
+ [Theory]
+ [PropertyData("QuotedStrings")]
+ [Trait("Description", "UnquoteToken returns unquoted strings")]
+ public void UnquoteTokenReturnsUnquotedStrings(string token, string expectedResult)
+ {
+ string result = FormattingUtilities.UnquoteToken(token);
+ Assert.Equal(expectedResult, result);
+ }
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/HttpClientExtensionsTest.cs b/test/System.Net.Http.Formatting.Test.Unit/HttpClientExtensionsTest.cs
new file mode 100644
index 00000000..acbb6463
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/HttpClientExtensionsTest.cs
@@ -0,0 +1,256 @@
+using System.Net.Http.Formatting;
+using System.Net.Http.Formatting.Mocks;
+using System.Net.Http.Internal;
+using System.Net.Http.Mocks;
+using System.Threading;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Net.Http
+{
+ public class HttpClientExtensionsTest
+ {
+ private readonly HttpClient _client;
+
+ public HttpClientExtensionsTest()
+ {
+ Mock<TestableHttpMessageHandler> handlerMock = new Mock<TestableHttpMessageHandler> { CallBase = true };
+ handlerMock
+ .Setup(h => h.SendAsyncPublic(It.IsAny<HttpRequestMessage>(), It.IsAny<CancellationToken>()))
+ .Returns((HttpRequestMessage request, CancellationToken _) => TaskHelpers.FromResult(new HttpResponseMessage() { RequestMessage = request }));
+
+ _client = new HttpClient(handlerMock.Object);
+ }
+
+ [Fact]
+ public void PostAsJsonAsync_WhenClientIsNull_ThrowsException()
+ {
+ HttpClient client = null;
+
+ Assert.ThrowsArgumentNull(() => client.PostAsJsonAsync("http://www.example.com", new object()), "client");
+ }
+
+ [Fact]
+ public void PostAsJsonAsync_WhenUriIsNull_ThrowsException()
+ {
+ Assert.Throws<InvalidOperationException>(() => _client.PostAsJsonAsync(null, new object()),
+ "An invalid request URI was provided. The request URI must either be an absolute URI or BaseAddress must be set.");
+ }
+
+ [Fact]
+ public void PostAsJsonAsync_UsesJsonMediaTypeFormatter()
+ {
+ var result = _client.PostAsJsonAsync("http://example.com", new object());
+
+ var response = result.Result;
+ var content = Assert.IsType<ObjectContent<object>>(response.RequestMessage.Content);
+ Assert.IsType<JsonMediaTypeFormatter>(content.Formatter);
+ }
+
+ [Fact]
+ public void PostAsXmlAsync_WhenClientIsNull_ThrowsException()
+ {
+ HttpClient client = null;
+
+ Assert.ThrowsArgumentNull(() => client.PostAsXmlAsync("http://www.example.com", new object()), "client");
+ }
+
+ [Fact]
+ public void PostAsXmlAsync_WhenUriIsNull_ThrowsException()
+ {
+ Assert.Throws<InvalidOperationException>(() => _client.PostAsXmlAsync(null, new object()),
+ "An invalid request URI was provided. The request URI must either be an absolute URI or BaseAddress must be set.");
+ }
+
+ [Fact]
+ public void PostAsXmlAsync_UsesXmlMediaTypeFormatter()
+ {
+ var result = _client.PostAsXmlAsync("http://example.com", new object());
+
+ var response = result.Result;
+ var content = Assert.IsType<ObjectContent<object>>(response.RequestMessage.Content);
+ Assert.IsType<XmlMediaTypeFormatter>(content.Formatter);
+ }
+
+ [Fact]
+ public void PostAsync_WhenClientIsNull_ThrowsException()
+ {
+ HttpClient client = null;
+
+ Assert.ThrowsArgumentNull(() => client.PostAsync("http://www.example.com", new object(), new JsonMediaTypeFormatter(), "text/json"), "client");
+ }
+
+ [Fact]
+ public void PostAsync_WhenRequestUriIsNull_ThrowsException()
+ {
+ Assert.Throws<InvalidOperationException>(() => _client.PostAsync(null, new object(), new JsonMediaTypeFormatter(), "text/json"),
+ "An invalid request URI was provided. The request URI must either be an absolute URI or BaseAddress must be set.");
+ }
+
+ [Fact]
+ public void PostAsync_WhenRequestUriIsSet_CreatesRequestWithAppropriateUri()
+ {
+ _client.BaseAddress = new Uri("http://example.com/");
+
+ var result = _client.PostAsync("myapi/", new object(), new MockMediaTypeFormatter());
+
+ var request = result.Result.RequestMessage;
+ Assert.Equal("http://example.com/myapi/", request.RequestUri.ToString());
+ }
+
+ [Fact]
+ public void PostAsync_WhenAuthoritativeMediaTypeIsSet_CreatesRequestWithAppropriateContentType()
+ {
+ var result = _client.PostAsync("http://example.com/myapi/", new object(), new MockMediaTypeFormatter(), "foo/bar; charset=utf-16");
+
+ var request = result.Result.RequestMessage;
+ Assert.Equal("foo/bar", request.Content.Headers.ContentType.MediaType);
+ Assert.Equal("utf-16", request.Content.Headers.ContentType.CharSet);
+ }
+
+ [Fact]
+ public void PostAsync_WhenFormatterIsSet_CreatesRequestWithObjectContentAndCorrectFormatter()
+ {
+ var formatter = new MockMediaTypeFormatter();
+
+ var result = _client.PostAsync("http://example.com/myapi/", new object(), formatter, "foo/bar; charset=utf-16");
+
+ var request = result.Result.RequestMessage;
+ var content = Assert.IsType<ObjectContent<object>>(request.Content);
+ Assert.Same(formatter, content.Formatter);
+ }
+
+ [Fact]
+ public void PostAsync_IssuesPostRequest()
+ {
+ var formatter = new MockMediaTypeFormatter();
+
+ var result = _client.PostAsync("http://example.com/myapi/", new object(), formatter);
+
+ var request = result.Result.RequestMessage;
+ Assert.Same(HttpMethod.Post, request.Method);
+ }
+
+ [Fact]
+ public void PostAsync_WhenMediaTypeFormatterIsNull_ThrowsException()
+ {
+ Assert.ThrowsArgumentNull(() => _client.PostAsync("http;//example.com", new object(), formatter: null), "formatter");
+ }
+
+ [Fact]
+ public void PutAsJsonAsync_WhenClientIsNull_ThrowsException()
+ {
+ HttpClient client = null;
+
+ Assert.ThrowsArgumentNull(() => client.PutAsJsonAsync("http://www.example.com", new object()), "client");
+ }
+
+ [Fact]
+ public void PutAsJsonAsync_WhenUriIsNull_ThrowsException()
+ {
+ Assert.Throws<InvalidOperationException>(() => _client.PutAsJsonAsync(null, new object()),
+ "An invalid request URI was provided. The request URI must either be an absolute URI or BaseAddress must be set.");
+ }
+
+ [Fact]
+ public void PutAsJsonAsync_UsesJsonMediaTypeFormatter()
+ {
+ var result = _client.PutAsJsonAsync("http://example.com", new object());
+
+ var response = result.Result;
+ var content = Assert.IsType<ObjectContent<object>>(response.RequestMessage.Content);
+ Assert.IsType<JsonMediaTypeFormatter>(content.Formatter);
+ }
+
+ [Fact]
+ public void PutAsXmlAsync_WhenClientIsNull_ThrowsException()
+ {
+ HttpClient client = null;
+
+ Assert.ThrowsArgumentNull(() => client.PutAsXmlAsync("http://www.example.com", new object()), "client");
+ }
+
+ [Fact]
+ public void PutAsXmlAsync_WhenUriIsNull_ThrowsException()
+ {
+ Assert.Throws<InvalidOperationException>(() => _client.PutAsXmlAsync(null, new object()),
+ "An invalid request URI was provided. The request URI must either be an absolute URI or BaseAddress must be set.");
+ }
+
+ [Fact]
+ public void PutAsXmlAsync_UsesXmlMediaTypeFormatter()
+ {
+ var result = _client.PutAsXmlAsync("http://example.com", new object());
+
+ var response = result.Result;
+ var content = Assert.IsType<ObjectContent<object>>(response.RequestMessage.Content);
+ Assert.IsType<XmlMediaTypeFormatter>(content.Formatter);
+ }
+
+ [Fact]
+ public void PutAsync_WhenClientIsNull_ThrowsException()
+ {
+ HttpClient client = null;
+
+ Assert.ThrowsArgumentNull(() => client.PutAsync("http://www.example.com", new object(), new JsonMediaTypeFormatter(), "text/json"), "client");
+ }
+
+ [Fact]
+ public void PutAsync_WhenRequestUriIsNull_ThrowsException()
+ {
+ Assert.Throws<InvalidOperationException>(() => _client.PutAsync(null, new object(), new JsonMediaTypeFormatter(), "text/json"),
+ "An invalid request URI was provided. The request URI must either be an absolute URI or BaseAddress must be set.");
+ }
+
+ [Fact]
+ public void PutAsync_WhenRequestUriIsSet_CreatesRequestWithAppropriateUri()
+ {
+ _client.BaseAddress = new Uri("http://example.com/");
+
+ var result = _client.PutAsync("myapi/", new object(), new MockMediaTypeFormatter());
+
+ var request = result.Result.RequestMessage;
+ Assert.Equal("http://example.com/myapi/", request.RequestUri.ToString());
+ }
+
+ [Fact]
+ public void PutAsync_WhenAuthoritativeMediaTypeIsSet_CreatesRequestWithAppropriateContentType()
+ {
+ var result = _client.PutAsync("http://example.com/myapi/", new object(), new MockMediaTypeFormatter(), "foo/bar; charset=utf-16");
+
+ var request = result.Result.RequestMessage;
+ Assert.Equal("foo/bar", request.Content.Headers.ContentType.MediaType);
+ Assert.Equal("utf-16", request.Content.Headers.ContentType.CharSet);
+ }
+
+ [Fact]
+ public void PutAsync_WhenFormatterIsSet_CreatesRequestWithObjectContentAndCorrectFormatter()
+ {
+ var formatter = new MockMediaTypeFormatter();
+
+ var result = _client.PutAsync("http://example.com/myapi/", new object(), formatter, "foo/bar; charset=utf-16");
+
+ var request = result.Result.RequestMessage;
+ var content = Assert.IsType<ObjectContent<object>>(request.Content);
+ Assert.Same(formatter, content.Formatter);
+ }
+
+ [Fact]
+ public void PutAsync_IssuesPutRequest()
+ {
+ var formatter = new MockMediaTypeFormatter();
+
+ var result = _client.PutAsync("http://example.com/myapi/", new object(), formatter);
+
+ var request = result.Result.RequestMessage;
+ Assert.Same(HttpMethod.Put, request.Method);
+ }
+
+ [Fact]
+ public void PutAsync_WhenMediaTypeFormatterIsNull_ThrowsException()
+ {
+ Assert.ThrowsArgumentNull(() => _client.PutAsync("http;//example.com", new object(), formatter: null), "formatter");
+ }
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/HttpContentCollectionExtensionsTests.cs b/test/System.Net.Http.Formatting.Test.Unit/HttpContentCollectionExtensionsTests.cs
new file mode 100644
index 00000000..11aec040
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/HttpContentCollectionExtensionsTests.cs
@@ -0,0 +1,336 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http.Headers;
+using System.Text;
+using Microsoft.TestCommon;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Net.Http
+{
+ public class HttpContentCollectionExtensionsTests
+ {
+ private const string contentID = "content-id";
+ private const string matchContentID = "matchID";
+ private const string matchContentType = "text/plain";
+ private const string matchDispositionName = "N1";
+ private const string quotedMatchDispositionName = "\"" + matchDispositionName + "\"";
+ private const string matchDispositionType = "form-data";
+ private const string quotedMatchDispositionType = "\"" + matchDispositionType + "\"";
+
+ private const string noMatchContentID = "nomatchID";
+ private const string noMatchContentType = "text/nomatch";
+ private const string noMatchDispositionName = "nomatchName";
+ private const string noMatchDispositionType = "nomatchType";
+
+ [Fact]
+ [Trait("Description", "IEnumerableHttpContentExtensionMethods is a public static class.")]
+ public void TypeIsCorrect()
+ {
+ Assert.Type.HasProperties(
+ typeof(HttpContentCollectionExtensions),
+ TypeAssert.TypeProperties.IsPublicVisibleClass |
+ TypeAssert.TypeProperties.IsStatic);
+ }
+
+
+ private static IEnumerable<HttpContent> CreateContent()
+ {
+ MultipartFormDataContent multipart = new MultipartFormDataContent();
+
+ multipart.Add(new StringContent("A", UTF8Encoding.UTF8, matchContentType), matchDispositionName);
+ multipart.Add(new StringContent("B", UTF8Encoding.UTF8, matchContentType), "N2");
+ multipart.Add(new StringContent("C", UTF8Encoding.UTF8, matchContentType), "N3");
+
+ multipart.Add(new ByteArrayContent(new byte[] { 0x65 }), "N4");
+ multipart.Add(new ByteArrayContent(new byte[] { 0x65 }), "N5");
+ multipart.Add(new ByteArrayContent(new byte[] { 0x65 }), "N6");
+
+ HttpContent cidContent = new StringContent("<html>A</html>", UTF8Encoding.UTF8, "text/html");
+ cidContent.Headers.Add(contentID, matchContentID);
+ multipart.Add(cidContent);
+
+ return multipart;
+ }
+
+ private static void ClearHeaders(IEnumerable<HttpContent> contents)
+ {
+ foreach (var c in contents)
+ {
+ c.Headers.Clear();
+ }
+ }
+
+
+
+
+ [Fact]
+ [Trait("Description", "FindAllContentType(IEnumerable<HttpContent>, string) throws on null.")]
+ public void FindAllContentTypeString()
+ {
+ Assert.ThrowsArgumentNull(() => { HttpContentCollectionExtensions.FindAllContentType(null, "text/plain"); }, "contents");
+
+ IEnumerable<HttpContent> content = HttpContentCollectionExtensionsTests.CreateContent();
+ Assert.ThrowsArgumentNull(() => { HttpContentCollectionExtensions.FindAllContentType(content, (string)null); }, "contentType");
+ foreach (string empty in TestData.EmptyStrings)
+ {
+ Assert.ThrowsArgumentNull(() => { HttpContentCollectionExtensions.FindAllContentType(content, empty); }, "contentType");
+ }
+ }
+
+ [Fact]
+ [Trait("Description", "FindAllContentType(IEnumerable<HttpContent>, string) no match.")]
+ public void FindAllContentTypeStringNoMatch()
+ {
+ IEnumerable<HttpContent> content = HttpContentCollectionExtensionsTests.CreateContent();
+ IEnumerable<HttpContent> result = null;
+ result = content.FindAllContentType(noMatchContentType);
+ Assert.Equal(0, result.Count());
+
+ ClearHeaders(content);
+ result = content.FindAllContentType(noMatchContentType);
+ Assert.Equal(0, result.Count());
+ }
+
+ [Fact]
+ [Trait("Description", "FindAllContentType(IEnumerable<HttpContent>, string) match.")]
+ public void FindAllContentTypeStringMatch()
+ {
+ IEnumerable<HttpContent> content = HttpContentCollectionExtensionsTests.CreateContent();
+ IEnumerable<HttpContent> result = content.FindAllContentType(matchContentType);
+ Assert.Equal(3, result.Count());
+ }
+
+ [Fact]
+ [Trait("Description", "FindAllContentType(IEnumerable<HttpContent>, MediaTypeHeaderValue) throws on null.")]
+ public void FindAllContentTypeMediaTypeThrows()
+ {
+ Assert.ThrowsArgumentNull(() => { HttpContentCollectionExtensions.FindAllContentType(null, new MediaTypeHeaderValue("text/plain")); }, "contents");
+
+ IEnumerable<HttpContent> content = HttpContentCollectionExtensionsTests.CreateContent();
+ Assert.ThrowsArgumentNull(() => { HttpContentCollectionExtensions.FindAllContentType(content, (MediaTypeHeaderValue)null); }, "contentType");
+ }
+
+ [Fact]
+ [Trait("Description", "FindAllContentType(IEnumerable<HttpContent>, MediaTypeHeaderValue) no match.")]
+ public void FindAllContentTypeMediaTypeNoMatch()
+ {
+ IEnumerable<HttpContent> content = HttpContentCollectionExtensionsTests.CreateContent();
+ IEnumerable<HttpContent> result = null;
+
+ result = content.FindAllContentType(new MediaTypeHeaderValue(noMatchContentType));
+ Assert.Equal(0, result.Count());
+
+ ClearHeaders(content);
+ result = content.FindAllContentType(new MediaTypeHeaderValue(noMatchContentType));
+ Assert.Equal(0, result.Count());
+ }
+
+ [Fact]
+ [Trait("Description", "FindAllContentType(IEnumerable<HttpContent>, string) match.")]
+ public void FindAllContentTypeMediaTypeMatch()
+ {
+ IEnumerable<HttpContent> content = HttpContentCollectionExtensionsTests.CreateContent();
+ IEnumerable<HttpContent> result = content.FindAllContentType(new MediaTypeHeaderValue(matchContentType));
+ Assert.Equal(3, result.Count());
+ }
+
+ [Fact]
+ [Trait("Description", "FirstDispositionName(IEnumerable<HttpContent>, string) throws on null.")]
+ public void FirstDispositionNameThrows()
+ {
+ Assert.ThrowsArgumentNull(() => { HttpContentCollectionExtensions.FirstDispositionName(null, "A"); }, "contents");
+
+ IEnumerable<HttpContent> content = HttpContentCollectionExtensionsTests.CreateContent();
+ Assert.ThrowsArgumentNull(() => { HttpContentCollectionExtensions.FirstDispositionName(content, null); }, "dispositionName");
+ foreach (string empty in TestData.EmptyStrings)
+ {
+ Assert.ThrowsArgumentNull(() => { HttpContentCollectionExtensions.FirstDispositionName(content, empty); }, "dispositionName");
+ }
+ }
+
+ [Fact]
+ [Trait("Description", "FirstDispositionName(IEnumerable<HttpContent>, string) no match.")]
+ public void FirstDispositionNameNoMatch()
+ {
+ IEnumerable<HttpContent> content = HttpContentCollectionExtensionsTests.CreateContent();
+ Assert.Null(content.FirstDispositionNameOrDefault(noMatchDispositionName));
+
+ ClearHeaders(content);
+ Assert.Throws<InvalidOperationException>(() => content.FirstDispositionName(noMatchDispositionName));
+ }
+
+ [Fact]
+ [Trait("Description", "FirstDispositionName(IEnumerable<HttpContent>, string) match.")]
+ public void FirstDispositionNameMatch()
+ {
+ IEnumerable<HttpContent> content = HttpContentCollectionExtensionsTests.CreateContent();
+ Assert.NotNull(content.FirstDispositionName(matchDispositionName));
+ Assert.NotNull(content.FirstDispositionName(quotedMatchDispositionName));
+ }
+
+ [Fact]
+ [Trait("Description", "FirstDispositionNameOrDefault(IEnumerable<HttpContent>, string) throws on null.")]
+ public void FirstDispositionNameOrDefaultThrows()
+ {
+ Assert.ThrowsArgumentNull(() => { HttpContentCollectionExtensions.FirstDispositionNameOrDefault(null, "A"); }, "contents");
+
+ IEnumerable<HttpContent> content = HttpContentCollectionExtensionsTests.CreateContent();
+ Assert.ThrowsArgumentNull(() => { HttpContentCollectionExtensions.FirstDispositionNameOrDefault(content, null); }, "dispositionName");
+ foreach (string empty in TestData.EmptyStrings)
+ {
+ Assert.ThrowsArgumentNull(() => { HttpContentCollectionExtensions.FirstDispositionNameOrDefault(content, empty); }, "dispositionName");
+ }
+ }
+
+ [Fact]
+ [Trait("Description", "FirstDispositionName(IEnumerable<HttpContent>, string) no match.")]
+ public void FirstDispositionNameOrDefaultNoMatch()
+ {
+ IEnumerable<HttpContent> content = HttpContentCollectionExtensionsTests.CreateContent();
+ Assert.Null(content.FirstDispositionNameOrDefault(noMatchDispositionName));
+
+ ClearHeaders(content);
+ Assert.Null(content.FirstDispositionNameOrDefault(noMatchDispositionName));
+ }
+
+ [Fact]
+ [Trait("Description", "FirstDispositionName(IEnumerable<HttpContent>, string) match.")]
+ public void FirstDispositionNameOrDefaultMatch()
+ {
+ IEnumerable<HttpContent> content = HttpContentCollectionExtensionsTests.CreateContent();
+ Assert.NotNull(content.FirstDispositionNameOrDefault(matchDispositionName));
+ Assert.NotNull(content.FirstDispositionNameOrDefault(quotedMatchDispositionName));
+ }
+
+ [Fact]
+ [Trait("Description", "FirstDispositionType(IEnumerable<HttpContent>, string) throws on null.")]
+ public void FirstDispositionTypeThrows()
+ {
+ Assert.ThrowsArgumentNull(() => { HttpContentCollectionExtensions.FirstDispositionType(null, "A"); }, "contents");
+
+ IEnumerable<HttpContent> content = HttpContentCollectionExtensionsTests.CreateContent();
+ Assert.ThrowsArgumentNull(() => { HttpContentCollectionExtensions.FirstDispositionType(content, null); }, "dispositionType");
+ foreach (string empty in TestData.EmptyStrings)
+ {
+ Assert.ThrowsArgumentNull(() => { HttpContentCollectionExtensions.FirstDispositionType(content, empty); }, "dispositionType");
+ }
+ }
+
+ [Fact]
+ [Trait("Description", "FirstDispositionType(IEnumerable<HttpContent>, string) no match.")]
+ public void FirstDispositionTypeNoMatch()
+ {
+ IEnumerable<HttpContent> content = HttpContentCollectionExtensionsTests.CreateContent();
+ Assert.Throws<InvalidOperationException>(() => content.FirstDispositionType(noMatchDispositionType));
+
+ Assert.Null(content.FirstDispositionTypeOrDefault(noMatchDispositionType));
+
+ ClearHeaders(content);
+ Assert.Throws<InvalidOperationException>(() => content.FirstDispositionType(noMatchDispositionType));
+ }
+
+ [Fact]
+ [Trait("Description", "FirstDispositionType(IEnumerable<HttpContent>, string) match.")]
+ public void FirstDispositionTypeMatch()
+ {
+ IEnumerable<HttpContent> content = HttpContentCollectionExtensionsTests.CreateContent();
+ Assert.NotNull(content.FirstDispositionType(matchDispositionType));
+ Assert.NotNull(content.FirstDispositionType(quotedMatchDispositionType));
+ }
+
+ [Fact]
+ [Trait("Description", "FirstDispositionTypeOrDefault(IEnumerable<HttpContent>, string) throws on null.")]
+ public void FirstDispositionTypeOrDefaultThrows()
+ {
+ Assert.ThrowsArgumentNull(() => { HttpContentCollectionExtensions.FirstDispositionTypeOrDefault(null, "A"); }, "contents");
+
+ IEnumerable<HttpContent> content = HttpContentCollectionExtensionsTests.CreateContent();
+ Assert.ThrowsArgumentNull(() => { HttpContentCollectionExtensions.FirstDispositionTypeOrDefault(content, null); }, "dispositionType");
+ foreach (string empty in TestData.EmptyStrings)
+ {
+ Assert.ThrowsArgumentNull(() => { HttpContentCollectionExtensions.FirstDispositionTypeOrDefault(content, empty); }, "dispositionType");
+ }
+ }
+
+ [Fact]
+ [Trait("Description", "FirstDispositionTypeOrDefault(IEnumerable<HttpContent>, string) no match.")]
+ public void FirstDispositionTypeOrDefaultNoMatch()
+ {
+ IEnumerable<HttpContent> content = HttpContentCollectionExtensionsTests.CreateContent();
+ Assert.Null(content.FirstDispositionTypeOrDefault(noMatchDispositionType));
+
+ ClearHeaders(content);
+ Assert.Null(content.FirstDispositionTypeOrDefault(noMatchDispositionType));
+ }
+
+ [Fact]
+ [Trait("Description", "FirstDispositionTypeOrDefault(IEnumerable<HttpContent>, string) match.")]
+ public void FirstDispositionTypeOrDefaultMatch()
+ {
+ IEnumerable<HttpContent> content = HttpContentCollectionExtensionsTests.CreateContent();
+ Assert.NotNull(content.FirstDispositionTypeOrDefault(matchDispositionType));
+ Assert.NotNull(content.FirstDispositionTypeOrDefault(quotedMatchDispositionType));
+ }
+
+ [Fact]
+ [Trait("Description", "FirstStart(IEnumerable<HttpContent>, string) throws on null.")]
+ public void FirstStartThrows()
+ {
+ Assert.ThrowsArgumentNull(() => { HttpContentCollectionExtensions.FirstStart(null, "A"); }, "contents");
+
+ IEnumerable<HttpContent> content = HttpContentCollectionExtensionsTests.CreateContent();
+ Assert.ThrowsArgumentNull(() => { HttpContentCollectionExtensions.FirstStart(content, null); }, "start");
+ foreach (string empty in TestData.EmptyStrings)
+ {
+ Assert.ThrowsArgumentNull(() => { HttpContentCollectionExtensions.FirstStart(content, empty); }, "start");
+ }
+ }
+
+ [Fact]
+ [Trait("Description", "FirstStart(IEnumerable<HttpContent>, string) no match.")]
+ public void FirstStartNoMatch()
+ {
+ IEnumerable<HttpContent> content = HttpContentCollectionExtensionsTests.CreateContent();
+ Assert.Throws<InvalidOperationException>(() => content.FirstStart(noMatchContentID));
+ }
+
+ [Fact]
+ [Trait("Description", "FirstStart(IEnumerable<HttpContent>, string) match.")]
+ public void FirstStartMatch()
+ {
+ IEnumerable<HttpContent> content = HttpContentCollectionExtensionsTests.CreateContent();
+ Assert.NotNull(content.FirstStart(matchContentID));
+ }
+
+ [Fact]
+ [Trait("Description", "FirstStartOrDefault(IEnumerable<HttpContent>, string) throws on null.")]
+ public void FirstStartOrDefaultThrows()
+ {
+ Assert.ThrowsArgumentNull(() => { HttpContentCollectionExtensions.FirstStartOrDefault(null, "A"); }, "contents");
+
+ IEnumerable<HttpContent> content = HttpContentCollectionExtensionsTests.CreateContent();
+ Assert.ThrowsArgumentNull(() => { HttpContentCollectionExtensions.FirstStartOrDefault(content, null); }, "start");
+ foreach (string empty in TestData.EmptyStrings)
+ {
+ Assert.ThrowsArgumentNull(() => { HttpContentCollectionExtensions.FirstStartOrDefault(content, empty); }, "start");
+ }
+ }
+
+ [Fact]
+ [Trait("Description", "FirstStartOrDefault(IEnumerable<HttpContent>, string) no match.")]
+ public void FirstStartOrDefaultNoMatch()
+ {
+ IEnumerable<HttpContent> content = HttpContentCollectionExtensionsTests.CreateContent();
+ Assert.Null(content.FirstStartOrDefault(noMatchContentID));
+ }
+
+ [Fact]
+ [Trait("Description", "FirstStartOrDefault(IEnumerable<HttpContent>, string) match.")]
+ public void FirstStartOrDefaultMatch()
+ {
+ IEnumerable<HttpContent> content = HttpContentCollectionExtensionsTests.CreateContent();
+ Assert.NotNull(content.FirstStartOrDefault(matchContentID));
+ }
+ }
+} \ No newline at end of file
diff --git a/test/System.Net.Http.Formatting.Test.Unit/HttpContentExtensionsTest.cs b/test/System.Net.Http.Formatting.Test.Unit/HttpContentExtensionsTest.cs
new file mode 100644
index 00000000..da4b4cc8
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/HttpContentExtensionsTest.cs
@@ -0,0 +1,127 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net.Http.Formatting;
+using System.Net.Http.Headers;
+using System.Net.Http.Internal;
+using System.Threading.Tasks;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Net.Http
+{
+ public class HttpContentExtensionsTest
+ {
+ private static readonly IEnumerable<MediaTypeFormatter> _emptyFormatterList = Enumerable.Empty<MediaTypeFormatter>();
+ private readonly Mock<MediaTypeFormatter> _formatterMock = new Mock<MediaTypeFormatter>();
+ private readonly MediaTypeHeaderValue _mediaType = new MediaTypeHeaderValue("foo/bar");
+
+ [Fact]
+ public void ReadAsAsync_WhenContentParameterIsNull_Throws()
+ {
+ Assert.ThrowsArgumentNull(() => HttpContentExtensions.ReadAsAsync(null, typeof(string), _emptyFormatterList), "content");
+ }
+
+ [Fact]
+ public void ReadAsAsync_WhenTypeParameterIsNull_Throws()
+ {
+ Assert.ThrowsArgumentNull(() => HttpContentExtensions.ReadAsAsync(new StringContent(""), null, _emptyFormatterList), "type");
+ }
+
+ [Fact]
+ public void ReadAsAsync_WhenFormattersParameterIsNull_Throws()
+ {
+ Assert.ThrowsArgumentNull(() => HttpContentExtensions.ReadAsAsync(new StringContent(""), typeof(string), null), "formatters");
+ }
+
+ [Fact]
+ public void ReadAsAsyncOfT_WhenContentParameterIsNull_Throws()
+ {
+ Assert.ThrowsArgumentNull(() => HttpContentExtensions.ReadAsAsync<string>(null, _emptyFormatterList), "content");
+ }
+
+ [Fact]
+ public void ReadAsAsyncOfT_WhenFormattersParameterIsNull_Throws()
+ {
+ Assert.ThrowsArgumentNull(() => HttpContentExtensions.ReadAsAsync<string>(new StringContent(""), null), "formatters");
+ }
+
+ [Fact]
+ public void ReadAsAsyncOfT_WhenNoMatchingFormatterFound_Throws()
+ {
+ var content = new StringContent("{}");
+ content.Headers.ContentType = _mediaType;
+ content.Headers.ContentType.CharSet = "utf-16";
+ var formatters = new MediaTypeFormatter[] { new JsonMediaTypeFormatter() };
+
+ Assert.Throws<InvalidOperationException>(() => content.ReadAsAsync<List<string>>(formatters),
+ "No MediaTypeFormatter is available to read an object of type 'List`1' from content with media type 'foo/bar'.");
+ }
+
+ [Fact]
+ public void ReadAsAsyncOfT_WhenNoMatchingFormatterFoundForContentWithNoMediaType_Throws()
+ {
+ var content = new StringContent("{}");
+ content.Headers.ContentType = null;
+ var formatters = new MediaTypeFormatter[] { new JsonMediaTypeFormatter() };
+
+ Assert.Throws<InvalidOperationException>(() => content.ReadAsAsync<List<string>>(formatters),
+ "No MediaTypeFormatter is available to read an object of type 'List`1' from content with media type ''undefined''.");
+ }
+
+ [Fact]
+ public void ReadAsAsyncOfT_ReadsFromContent_ThenInvokesFormattersReadFromStreamMethod()
+ {
+ Stream contentStream = null;
+ string value = "42";
+ var contentMock = new Mock<TestableHttpContent> { CallBase = true };
+ contentMock.Setup(c => c.SerializeToStreamAsyncPublic(It.IsAny<Stream>(), It.IsAny<TransportContext>()))
+ .Returns(TaskHelpers.Completed)
+ .Callback((Stream s, TransportContext _) => contentStream = s)
+ .Verifiable();
+ HttpContent content = contentMock.Object;
+ content.Headers.ContentType = _mediaType;
+ _formatterMock
+ .Setup(f => f.ReadFromStreamAsync(typeof(string), It.IsAny<Stream>(), It.IsAny<HttpContentHeaders>(), It.IsAny<IFormatterLogger>()))
+ .Returns(TaskHelpers.FromResult<object>(value));
+ _formatterMock.Setup(f => f.CanReadType(typeof(string))).Returns(true);
+ _formatterMock.Object.SupportedMediaTypes.Add(_mediaType);
+ var formatters = new[] { _formatterMock.Object };
+
+ var result = content.ReadAsAsync<string>(formatters);
+
+ var resultValue = result.Result;
+ Assert.Same(value, resultValue);
+ contentMock.Verify();
+ _formatterMock.Verify(f => f.ReadFromStreamAsync(typeof(string), contentStream, content.Headers, null), Times.Once());
+ }
+
+ public abstract class TestableHttpContent : HttpContent
+ {
+ protected override Task<Stream> CreateContentReadStreamAsync()
+ {
+ return CreateContentReadStreamAsyncPublic();
+ }
+
+ public virtual Task<Stream> CreateContentReadStreamAsyncPublic()
+ {
+ return base.CreateContentReadStreamAsync();
+ }
+
+ protected override Task SerializeToStreamAsync(Stream stream, TransportContext context)
+ {
+ return SerializeToStreamAsyncPublic(stream, context);
+ }
+
+ public abstract Task SerializeToStreamAsyncPublic(Stream stream, TransportContext context);
+
+ protected override bool TryComputeLength(out long length)
+ {
+ return TryComputeLengthPublic(out length);
+ }
+
+ public abstract bool TryComputeLengthPublic(out long length);
+ }
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/HttpContentMessageExtensionsTests.cs b/test/System.Net.Http.Formatting.Test.Unit/HttpContentMessageExtensionsTests.cs
new file mode 100644
index 00000000..ad0f568d
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/HttpContentMessageExtensionsTests.cs
@@ -0,0 +1,491 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using Microsoft.TestCommon;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Net.Http
+{
+ public class HttpContentMessageExtensionsTests
+ {
+ [Fact]
+ public void TypeIsCorrect()
+ {
+ Assert.Type.HasProperties(
+ typeof(HttpContentMessageExtensions),
+ TypeAssert.TypeProperties.IsPublicVisibleClass |
+ TypeAssert.TypeProperties.IsStatic);
+ }
+
+ private static HttpContent CreateContent(bool isRequest, bool hasEntity)
+ {
+ string message;
+ if (isRequest)
+ {
+ message = hasEntity ? ParserData.HttpRequestWithEntity : ParserData.HttpRequest;
+ }
+ else
+ {
+ message = hasEntity ? ParserData.HttpResponseWithEntity : ParserData.HttpResponse;
+ }
+
+ StringContent content = new StringContent(message);
+ content.Headers.ContentType = isRequest ? ParserData.HttpRequestMediaType : ParserData.HttpResponseMediaType;
+ return content;
+ }
+
+ private static HttpContent CreateContent(bool isRequest, IEnumerable<string> header, string body)
+ {
+ StringBuilder message = new StringBuilder();
+ foreach (string h in header)
+ {
+ message.Append(h);
+ message.Append("\r\n");
+ }
+
+ message.Append("\r\n");
+ if (body != null)
+ {
+ message.Append(body);
+ }
+
+ StringContent content = new StringContent(message.ToString());
+ content.Headers.ContentType = isRequest ? ParserData.HttpRequestMediaType : ParserData.HttpResponseMediaType;
+ return content;
+ }
+
+ private static void ValidateEntity(HttpContent content)
+ {
+ Assert.NotNull(content);
+ Assert.Equal(ParserData.TextContentType, content.Headers.ContentType.ToString());
+ string entity = content.ReadAsStringAsync().Result;
+ Assert.Equal(ParserData.HttpMessageEntity, entity);
+ }
+
+ private static void ValidateRequestMessage(HttpRequestMessage request, bool hasEntity)
+ {
+ Assert.NotNull(request);
+ Assert.Equal(Version.Parse("1.2"), request.Version);
+ Assert.Equal(ParserData.HttpMethod, request.Method.ToString());
+ Assert.Equal(ParserData.HttpRequestUri, request.RequestUri);
+ Assert.Equal(ParserData.HttpHostName, request.Headers.Host);
+ Assert.True(request.Headers.Contains("N1"), "request did not contain expected N1 header.");
+ Assert.True(request.Headers.Contains("N2"), "request did not contain expected N2 header.");
+
+ if (hasEntity)
+ {
+ ValidateEntity(request.Content);
+ }
+ }
+
+ private static void ValidateResponseMessage(HttpResponseMessage response, bool hasEntity)
+ {
+ Assert.NotNull(response);
+ Assert.Equal(new Version("1.2"), response.Version);
+ Assert.Equal(ParserData.HttpStatus, response.StatusCode);
+ Assert.Equal(ParserData.HttpReasonPhrase, response.ReasonPhrase);
+ Assert.True(response.Headers.Contains("N1"), "Response did not contain expected N1 header.");
+ Assert.True(response.Headers.Contains("N2"), "Response did not contain expected N2 header.");
+
+ if (hasEntity)
+ {
+ ValidateEntity(response.Content);
+ }
+ }
+
+ [Fact]
+ public void ReadAsHttpRequestMessageVerifyArguments()
+ {
+ Assert.ThrowsArgumentNull(() => HttpContentMessageExtensions.ReadAsHttpRequestMessageAsync(null), "content");
+ Assert.ThrowsArgument(() => new ByteArrayContent(new byte[] { }).ReadAsHttpRequestMessageAsync(), "content");
+ Assert.ThrowsArgument(() => new StringContent(String.Empty).ReadAsHttpRequestMessageAsync(), "content");
+ Assert.ThrowsArgument(() => new StringContent(String.Empty, Encoding.UTF8, "application/http").ReadAsHttpRequestMessageAsync(), "content");
+
+ Assert.ThrowsArgument(() =>
+ {
+ HttpContent content = new StringContent(String.Empty);
+ content.Headers.ContentType = ParserData.HttpResponseMediaType;
+ content.ReadAsHttpRequestMessageAsync();
+ }, "content");
+
+ Assert.ThrowsArgumentNull(() =>
+ {
+ HttpContent content = new StringContent(String.Empty);
+ content.Headers.ContentType = ParserData.HttpRequestMediaType;
+ content.ReadAsHttpRequestMessageAsync(null);
+ }, "uriScheme");
+
+ Assert.ThrowsArgument(() =>
+ {
+ HttpContent content = new StringContent(String.Empty);
+ content.Headers.ContentType = ParserData.HttpRequestMediaType;
+ content.ReadAsHttpRequestMessageAsync("i n v a l i d");
+ }, "uriScheme");
+
+ Assert.ThrowsArgument(() =>
+ {
+ HttpContent content = new StringContent(String.Empty);
+ content.Headers.ContentType = ParserData.HttpRequestMediaType;
+ content.ReadAsHttpRequestMessageAsync(Uri.UriSchemeHttp, ParserData.MinHeaderSize - 1);
+ }, "bufferSize");
+ }
+
+ [Fact]
+ public void ReadAsHttpResponseMessageVerifyArguments()
+ {
+ Assert.ThrowsArgumentNull(() => HttpContentMessageExtensions.ReadAsHttpResponseMessageAsync(null), "content");
+ Assert.ThrowsArgument(() => new ByteArrayContent(new byte[] { }).ReadAsHttpResponseMessageAsync(), "content");
+ Assert.ThrowsArgument(() => new StringContent(String.Empty).ReadAsHttpResponseMessageAsync(), "content");
+ Assert.ThrowsArgument(() => new StringContent(String.Empty, Encoding.UTF8, "application/http").ReadAsHttpResponseMessageAsync(), "content");
+
+ Assert.ThrowsArgument(() =>
+ {
+ HttpContent content = new StringContent(String.Empty);
+ content.Headers.ContentType = ParserData.HttpRequestMediaType;
+ content.ReadAsHttpResponseMessageAsync();
+ }, "content");
+
+ Assert.ThrowsArgument(() =>
+ {
+ HttpContent content = new StringContent(String.Empty);
+ content.Headers.ContentType = ParserData.HttpResponseMediaType;
+ content.ReadAsHttpResponseMessageAsync(ParserData.MinHeaderSize - 1);
+ }, "bufferSize");
+ }
+
+ [Fact]
+ public void IsHttpRequestMessageContentVerifyArguments()
+ {
+ Assert.ThrowsArgumentNull(() => HttpContentMessageExtensions.IsHttpRequestMessageContent(null), "content");
+ }
+
+ [Fact]
+ public void IsHttpResponseMessageContentVerifyArguments()
+ {
+ Assert.ThrowsArgumentNull(() =>
+ {
+ HttpContent content = null;
+ HttpContentMessageExtensions.IsHttpResponseMessageContent(content);
+ }, "content");
+ }
+
+ public static TheoryDataSet<HttpContent> NotHttpMessageContent
+ {
+ get
+ {
+ return new TheoryDataSet<HttpContent>
+ {
+ new ByteArrayContent(new byte[] { }),
+ new StringContent(String.Empty),
+ new StringContent(String.Empty, Encoding.UTF8, "application/http"),
+ };
+ }
+ }
+
+ [Theory]
+ [PropertyData("NotHttpMessageContent")]
+ public void IsHttpRequestMessageContentRespondsFalse(HttpContent notHttpMessageContent)
+ {
+ Assert.False(notHttpMessageContent.IsHttpRequestMessageContent());
+ }
+
+ [Fact]
+ public void IsHttpRequestMessageContentRespondsTrue()
+ {
+ HttpContent content = new StringContent(String.Empty);
+ content.Headers.ContentType = ParserData.HttpRequestMediaType;
+ Assert.True(content.IsHttpRequestMessageContent(), "Content should be HTTP request.");
+ }
+
+ [Theory]
+ [PropertyData("NotHttpMessageContent")]
+ public void IsHttpResponseMessageContent(HttpContent notHttpMessageContent)
+ {
+ Assert.False(notHttpMessageContent.IsHttpResponseMessageContent());
+
+ }
+
+ [Fact]
+ public void IsHttpResponseMessageContentRespondsTrue()
+ {
+ HttpContent content = new StringContent(String.Empty);
+ content.Headers.ContentType = ParserData.HttpResponseMediaType;
+ Assert.True(content.IsHttpResponseMessageContent(), "Content should be HTTP response.");
+ }
+
+ [Fact]
+ public void ReadAsHttpRequestMessageAsync_RequestWithoutEntity_ShouldReturnHttpRequestMessage()
+ {
+ HttpContent content = CreateContent(isRequest: true, hasEntity: false);
+ HttpRequestMessage httpRequest = content.ReadAsHttpRequestMessageAsync().Result;
+ ValidateRequestMessage(httpRequest, hasEntity: false);
+ }
+
+ [Fact]
+ public void ReadAsHttpRequestMessageAsync_RequestWithEntity_ShouldReturnHttpRequestMessage()
+ {
+ HttpContent content = CreateContent(isRequest: true, hasEntity: true);
+ HttpRequestMessage httpRequest = content.ReadAsHttpRequestMessageAsync().Result;
+ ValidateRequestMessage(httpRequest, hasEntity: true);
+ }
+
+ [Fact]
+ public void ReadAsHttpRequestMessageAsync_WithHttpsUriScheme_ReturnsUriWithHttps()
+ {
+ HttpContent content = CreateContent(isRequest: true, hasEntity: true);
+ HttpRequestMessage httpRequest = content.ReadAsHttpRequestMessageAsync(Uri.UriSchemeHttps).Result;
+ Assert.Equal(ParserData.HttpsRequestUri, httpRequest.RequestUri);
+ }
+
+ [Fact]
+ public void ReadAsHttpResponseMessageAsync_ResponseWithoutEntity_ShouldReturnHttpResponseMessage()
+ {
+ HttpContent content = CreateContent(isRequest: false, hasEntity: false);
+ HttpResponseMessage httpResponse = content.ReadAsHttpResponseMessageAsync().Result;
+ ValidateResponseMessage(httpResponse, hasEntity: false);
+ }
+
+ [Fact]
+ public void ReadAsHttpResponseMessageAsync_ResponseWithEntity_ShouldReturnHttpResponseMessage()
+ {
+ HttpContent content = CreateContent(isRequest: false, hasEntity: true);
+ HttpResponseMessage httpResponse = content.ReadAsHttpResponseMessageAsync().Result;
+ ValidateResponseMessage(httpResponse, hasEntity: true);
+ }
+
+ [Fact]
+ public void ReadAsHttpRequestMessageAsync_NoHostHeader_ThrowsIOException()
+ {
+ string[] request = new[] {
+ @"GET / HTTP/1.1",
+ };
+
+ HttpContent content = CreateContent(true, request, null);
+ Assert.Throws<IOException>(() => content.ReadAsHttpRequestMessageAsync().Result);
+ }
+
+ [Fact]
+ [Trait("Description", "ReadAsHttpRequestMessage should return HttpRequestMessage.")]
+ public void ReadAsHttpRequestMessageAsync_TwoHostHeaders_ThrowsIOException()
+ {
+ string[] request = new[] {
+ @"GET / HTTP/1.1",
+ @"Host: somehost.com",
+ @"Host: otherhost.com",
+ };
+
+ HttpContent content = CreateContent(true, request, null);
+ Assert.Throws<IOException>(() => content.ReadAsHttpRequestMessageAsync().Result);
+ }
+
+ [Fact]
+ public void ReadAsHttpRequestMessageAsync_IE_ShouldBeDeserializedCorrectly()
+ {
+ string[] request = new[] {
+ @"GET / HTTP/1.1",
+ @"Accept: text/html, application/xhtml+xml, */*",
+ @"Accept-Language: en-US",
+ @"User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)",
+ @"Accept-Encoding: gzip, deflate",
+ @"Proxy-Connection: Keep-Alive",
+ @"Host: msdn.microsoft.com",
+ @"Cookie: omniID=1297715979621_9f45_1519_3f8a_f22f85346ac6; WT_FPC=id=65.55.227.138-2323234032.30136233:lv=1309374389020:ss=1309374389020; A=I&I=AxUFAAAAAACNCAAADYEZ7CFPss7Swnujy4PXZA!!&M=1&CS=126mAa0002ZB51a02gZB51a; MC1=GUID=568428660ad44d4ab8f46133f4b03738&HASH=6628&LV=20113&V=3; WT_NVR_RU=0=msdn:1=:2=; MUID=A44DE185EA1B4E8088CCF7B348C5D65F; MSID=Microsoft.CreationDate=03/04/2011 23:38:15&Microsoft.LastVisitDate=06/20/2011 04:15:08&Microsoft.VisitStartDate=06/20/2011 04:15:08&Microsoft.CookieId=f658f3f2-e6d6-42ab-b86b-96791b942b6f&Microsoft.TokenId=ffffffff-ffff-ffff-ffff-ffffffffffff&Microsoft.NumberOfVisits=106&Microsoft.CookieFirstVisit=1&Microsoft.IdentityToken=AA==&Microsoft.MicrosoftId=0441-6141-1523-9969; msresearch=%7B%22version%22%3A%224.6%22%2C%22state%22%3A%7B%22name%22%3A%22IDLE%22%2C%22url%22%3Aundefined%2C%22timestamp%22%3A1299281911415%7D%2C%22lastinvited%22%3A1299281911415%2C%22userid%22%3A%2212992819114151265672533023080%22%2C%22vendorid%22%3A1%2C%22surveys%22%3A%5Bundefined%5D%7D; CodeSnippetContainerLang=C#; msdn=L=1033; ADS=SN=175A21EF; s_cc=true; s_sq=%5B%5BB%5D%5D; TocHashCookie=ms310241(n)/aa187916(n)/aa187917(n)/dd273952(n)/dd295083(n)/ff472634(n)/ee667046(n)/ee667070(n)/gg259047(n)/gg618436(n)/; WT_NVR=0=/:1=query|library|en-us:2=en-us/vcsharp|en-us/library",
+ };
+
+ HttpContent content = CreateContent(true, request, null);
+ HttpRequestMessage httpRequest = content.ReadAsHttpRequestMessageAsync().Result;
+ Assert.True(httpRequest.Headers.Contains("cookie"));
+ }
+
+ [Fact]
+ public void ReadAsHttpRequestMessageAsync_Firefox_ShouldBeDeserializedCorrectly()
+ {
+ string[] request = new[] {
+ @"GET / HTTP/1.1",
+ @"Host: msdn.microsoft.com",
+ @"User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:5.0) Gecko/20100101 Firefox/5.0",
+ @"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
+ @"Accept-Language: en-us,en;q=0.5",
+ @"Accept-Encoding: gzip, deflate",
+ @"Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7",
+ @"Proxy-Connection: keep-alive",
+ };
+
+ HttpContent content = CreateContent(true, request, null);
+ HttpRequestMessage httpRequest = content.ReadAsHttpRequestMessageAsync().Result;
+ Assert.True(httpRequest.Headers.Contains("proxy-connection"));
+ }
+
+ [Fact]
+ public void ReadAsHttpRequestMessageAsync_Chrome_ShouldBeDeserializedCorrectly()
+ {
+ string[] request = new string[] {
+ @"GET / HTTP/1.1",
+ @"Host: msdn.microsoft.com",
+ @"Proxy-Connection: keep-alive",
+ @"User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.100 Safari/534.30",
+ @"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
+ @"Accept-Encoding: gzip,deflate,sdch",
+ @"Accept-Language: en-US,en;q=0.8",
+ @"Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3",
+ };
+
+ HttpContent content = CreateContent(true, request, null);
+ HttpRequestMessage httpRequest = content.ReadAsHttpRequestMessageAsync().Result;
+ Assert.True(httpRequest.Headers.Contains("accept-charset"));
+ }
+
+ [Fact]
+ public void ReadAsHttpRequestMessageAsync_Safari_ShouldBeDeserializedCorrectly()
+ {
+ string[] request = new string[] {
+ @"GET / HTTP/1.1",
+ @"Host: msdn.microsoft.com",
+ @"User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/5.0.5 Safari/533.21.1",
+ @"Accept: application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5",
+ @"Accept-Language: en-US",
+ @"Accept-Encoding: gzip, deflate",
+ @"Connection: keep-alive",
+ @"Proxy-Connection: keep-alive",
+ };
+
+ HttpContent content = CreateContent(true, request, null);
+ HttpRequestMessage httpRequest = content.ReadAsHttpRequestMessageAsync().Result;
+ Assert.True(httpRequest.Headers.Contains("proxy-connection"));
+ }
+
+ [Fact]
+ public void ReadAsHttpRequestMessageAsync_Opera_ShouldBeDeserializedCorrectly()
+ {
+ string[] request = new string[] {
+ @"GET / HTTP/1.0",
+ @"User-Agent: Opera/9.80 (Windows NT 6.1; U; en) Presto/2.8.131 Version/11.11",
+ @"Host: msdn.microsoft.com",
+ @"Accept: text/html, application/xml;q=0.9, application/xhtml+xml, image/png, image/webp, image/jpeg, image/gif, image/x-xbitmap, */*;q=0.1",
+ @"Accept-Language: en-US,en;q=0.9",
+ @"Accept-Encoding: gzip, deflate",
+ @"Proxy-Connection: Keep-Alive",
+ };
+
+ HttpContent content = CreateContent(true, request, null);
+ HttpRequestMessage httpRequest = content.ReadAsHttpRequestMessageAsync().Result;
+ Assert.True(httpRequest.Headers.Contains("proxy-connection"));
+ }
+
+ [Fact]
+ public void ReadAsHttpResponseMessageAsync_Asp_ShouldBeDeserializedCorrectly()
+ {
+ string[] response = new string[] {
+ @"HTTP/1.1 302 Found",
+ @"Proxy-Connection: Keep-Alive",
+ @"Connection: Keep-Alive",
+ @"Content-Length: 124",
+ @"Via: 1.1 RED-PRXY-23",
+ @"Date: Thu, 30 Jun 2011 00:16:35 GMT",
+ @"Location: /en-us/",
+ @"Content-Type: text/html; charset=utf-8",
+ @"Server: Microsoft-IIS/7.5",
+ @"Cache-Control: private",
+ @"P3P: CP=""ALL IND DSP COR ADM CONo CUR CUSo IVAo IVDo PSA PSD TAI TELo OUR SAMo CNT COM INT NAV ONL PHY PRE PUR UNI""",
+ @"Set-Cookie: A=I&I=AxUFAAAAAAD7BwAA8Jx0njhGoW3MGASDmzeaGw!!&M=1; domain=.microsoft.com; expires=Sun, 30-Jun-2041 00:16:35 GMT; path=/",
+ @"Set-Cookie: ADS=SN=175A21EF; domain=.microsoft.com; path=/",
+ @"Set-Cookie: Sto.UserLocale=en-us; path=/",
+ @"X-AspNetMvc-Version: 3.0",
+ @"X-AspNet-Version: 4.0.30319",
+ @"X-Powered-By: ASP.NET",
+ @"Set-Cookie: A=I&I=AxUFAAAAAAD7BwAA8Jx0njhGoW3MGASDmzeaGw!!&M=1; domain=.microsoft.com; expires=Sun, 30-Jun-2041 00:16:35 GMT; path=/; path=/",
+ @"Set-Cookie: ADS=SN=175A21EF; domain=.microsoft.com; path=/; path=/",
+ @"P3P: CP=""ALL IND DSP COR ADM CONo CUR CUSo IVAo IVDo PSA PSD TAI TELo OUR SAMo CNT COM INT NAV ONL PHY PRE PUR UNI""",
+ @"X-Powered-By: ASP.NET",
+ };
+ string expectedEntity = @"<html><head><title>Object moved</title></head><body><h2>Object moved to <a href=""/en-us/"">here</a>.</h2></body></html>";
+
+ HttpContent content = CreateContent(false, response, expectedEntity);
+ HttpResponseMessage httpResponse = content.ReadAsHttpResponseMessageAsync().Result;
+ Assert.True(httpResponse.Headers.Contains("x-powered-by"));
+ string actualEntity = httpResponse.Content.ReadAsStringAsync().Result;
+ Assert.Equal(expectedEntity, actualEntity);
+ }
+
+ public static TheoryDataSet<IEnumerable<string>> ServerRoundTripData
+ {
+ get
+ {
+ return new TheoryDataSet<IEnumerable<string>>
+ {
+ new string[]
+ {
+ @"HTTP/1.1 200 OK",
+ @"Server: nginx",
+ @"Date: Mon, 26 Dec 2011 16:33:07 GMT",
+ @"Connection: keep-alive",
+ @"Set-Cookie: CG=US:WA:Bellevue; path=/",
+ @"Vary: Accept-Encoding, User-Agent",
+ @"Cache-Control: max-age=60, private",
+ @"Content-Length: 124",
+ @"Content-Type: text/html; charset=UTF-8",
+ },
+ new string[]
+ {
+ @"HTTP/1.1 302 Found",
+ @"Proxy-Connection: Keep-Alive",
+ @"Connection: Keep-Alive",
+ @"Via: 1.1 RED-PRXY-23",
+ @"Date: Thu, 30 Jun 2011 00:16:35 GMT",
+ @"Location: /en-us/",
+ @"Server: Microsoft-IIS/7.5",
+ @"Cache-Control: private",
+ @"P3P: CP=""ALL IND DSP COR ADM CONo CUR CUSo IVAo IVDo PSA PSD TAI TELo OUR SAMo CNT COM INT NAV ONL PHY PRE PUR UNI"", CP=""ALL IND DSP COR ADM CONo CUR CUSo IVAo IVDo PSA PSD TAI TELo OUR SAMo CNT COM INT NAV ONL PHY PRE PUR UNI""",
+ @"Set-Cookie: A=I&I=AxUFAAAAAAD7BwAA8Jx0njhGoW3MGASDmzeaGw!!&M=1; domain=.microsoft.com; expires=Sun, 30-Jun-2041 00:16:35 GMT; path=/",
+ @"Set-Cookie: ADS=SN=175A21EF; domain=.microsoft.com; path=/",
+ @"Set-Cookie: Sto.UserLocale=en-us; path=/",
+ @"Set-Cookie: A=I&I=AxUFAAAAAAD7BwAA8Jx0njhGoW3MGASDmzeaGw!!&M=1; domain=.microsoft.com; expires=Sun, 30-Jun-2041 00:16:35 GMT; path=/; path=/",
+ @"Set-Cookie: ADS=SN=175A21EF; domain=.microsoft.com; path=/; path=/",
+ @"X-AspNetMvc-Version: 3.0",
+ @"X-AspNet-Version: 4.0.30319",
+ @"X-Powered-By: ASP.NET",
+ @"X-Powered-By: ASP.NET",
+ @"Content-Length: 124",
+ @"Content-Type: text/html; charset=utf-8",
+ },
+ new string[]
+ {
+ @"HTTP/1.1 200 OK",
+ @"Proxy-Connection: Keep-Alive",
+ @"Connection: Keep-Alive",
+ @"Transfer-Encoding: chunked",
+ @"Via: 1.1 RED-PRXY-07",
+ @"Date: Mon, 26 Dec 2011 19:11:47 GMT",
+ @"Server: gws",
+ @"Cache-Control: max-age=0, private",
+ @"Set-Cookie: PREF=ID=e91cfd77b562e989:FF=0:TM=1324926707:LM=1324926707:S=4w8_eSySJPXCCjhT; expires=Wed, 25-Dec-2013 19:11:47 GMT; path=/; domain=.google.com",
+ @"Set-Cookie: NID=54=bSMpxl0q0MVlvG-eZYSBtQuYTF1clqrA-TSIZT8wZcbhrrsdkP9G5zPiXGSBmiNu656QR3xfTXKUPkP-HqY_nSnsjj1fb-ipoZ3DUcyXb9oS9_8tjz3NZ3A44GPCmRPx; expires=Tue, 26-Jun-2012 19:11:47 GMT; path=/; domain=.google.com; HttpOnly",
+ @"P3P: CP=""This is not a P3P policy! See http://www.google.com/support/accounts/bin/answer.py?hl=en&answer=151657 for more info.""",
+ @"X-XSS-Protection: 1; mode=block",
+ @"X-Frame-Options: SAMEORIGIN",
+ @"Expires: -1",
+ @"Content-Type: text/html; charset=ISO-8859-1",
+ },
+ };
+ }
+ }
+
+ [Theory]
+ [PropertyData("ServerRoundTripData")]
+ public void RoundtripServerResponse(IEnumerable<string> message)
+ {
+ HttpContent content = CreateContent(false, message, @"<html><head><title>Object moved</title></head><body><h2>Object moved to <a href=""/en-us/"">here</a>.</h2></body></html>");
+ HttpResponseMessage httpResponse = content.ReadAsHttpResponseMessageAsync().Result;
+ HttpMessageContent httpMessageContent = new HttpMessageContent(httpResponse);
+
+ MemoryStream destination = new MemoryStream();
+ httpMessageContent.CopyToAsync(destination).Wait();
+ destination.Seek(0, SeekOrigin.Begin);
+ string destinationMessage = new StreamReader(destination).ReadToEnd();
+ string sourceMessage = content.ReadAsStringAsync().Result;
+ Assert.Equal(sourceMessage, destinationMessage);
+ }
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/HttpContentMultipartExtensionsTests.cs b/test/System.Net.Http.Formatting.Test.Unit/HttpContentMultipartExtensionsTests.cs
new file mode 100644
index 00000000..a266c634
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/HttpContentMultipartExtensionsTests.cs
@@ -0,0 +1,512 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net.Http.Formatting.Parsers;
+using System.Net.Http.Headers;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.TestCommon;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+using FactAttribute = Microsoft.TestCommon.DefaultTimeoutFactAttribute;
+using TheoryAttribute = Microsoft.TestCommon.DefaultTimeoutTheoryAttribute;
+
+namespace System.Net.Http
+{
+ public class HttpContentMultipartExtensionsTests
+ {
+ private const string DefaultContentType = "text/plain";
+ private const string DefaultContentDisposition = "form-data";
+ private const string ExceptionStreamProviderMessage = "Bad Stream Provider!";
+ private const string ExceptionSyncStreamMessage = "Bad Sync Stream!";
+ private const string ExceptionAsyncStreamMessage = "Bad Async Stream!";
+ private const string LongText = "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789";
+
+ [Fact]
+ [Trait("Description", "HttpContentMultipartExtensionMethods is a public static class")]
+ public void TypeIsCorrect()
+ {
+ Assert.Type.HasProperties(
+ typeof(HttpContentMultipartExtensions),
+ TypeAssert.TypeProperties.IsPublicVisibleClass |
+ TypeAssert.TypeProperties.IsStatic);
+ }
+
+ private static HttpContent CreateContent(string boundary, params string[] bodyEntity)
+ {
+ List<string> entities = new List<string>();
+ int cnt = 0;
+ foreach (var body in bodyEntity)
+ {
+ byte[] header = InternetMessageFormatHeaderParserTests.CreateBuffer(
+ String.Format("N{0}: V{0}", cnt),
+ String.Format("Content-Type: {0}", DefaultContentType),
+ String.Format("Content-Disposition: {0}; FileName=\"N{1}\"", DefaultContentDisposition, cnt));
+ entities.Add(Encoding.UTF8.GetString(header) + body);
+ cnt++;
+ }
+
+ byte[] message = MimeMultipartParserTests.CreateBuffer(boundary, entities.ToArray());
+ HttpContent result = new ByteArrayContent(message);
+ var contentType = new MediaTypeHeaderValue("multipart/form-data");
+ contentType.Parameters.Add(new NameValueHeaderValue("boundary", String.Format("\"{0}\"", boundary)));
+ result.Headers.ContentType = contentType;
+ return result;
+ }
+
+ private static void ValidateContents(IEnumerable<HttpContent> contents)
+ {
+ int cnt = 0;
+ foreach (var content in contents)
+ {
+ Assert.NotNull(content);
+ Assert.NotNull(content.Headers);
+ Assert.Equal(4, content.Headers.Count());
+
+ IEnumerable<string> parsedValues = content.Headers.GetValues(String.Format("N{0}", cnt));
+ Assert.Equal(1, parsedValues.Count());
+ Assert.Equal(String.Format("V{0}", cnt), parsedValues.ElementAt(0));
+
+ Assert.Equal(DefaultContentType, content.Headers.ContentType.MediaType);
+
+ Assert.Equal(DefaultContentDisposition, content.Headers.ContentDisposition.DispositionType);
+ Assert.Equal(String.Format("\"N{0}\"", cnt), content.Headers.ContentDisposition.FileName);
+
+ cnt++;
+ }
+ }
+
+ [Fact]
+ public void ReadAsMultipartAsync_DetectsNonMultipartContent()
+ {
+ Assert.ThrowsArgumentNull(() => HttpContentMultipartExtensions.IsMimeMultipartContent(null), "content");
+ Assert.ThrowsArgument(() => new ByteArrayContent(new byte[0]).ReadAsMultipartAsync().Result, "content");
+ Assert.ThrowsArgument(() => new StringContent(String.Empty).ReadAsMultipartAsync().Result, "content");
+ Assert.ThrowsArgument(() => new StringContent(String.Empty, Encoding.UTF8, "multipart/form-data").ReadAsMultipartAsync().Result, "content");
+ }
+
+ public static IEnumerable<object[]> Boundaries
+ {
+ get { return ParserData.Boundaries; }
+ }
+
+ [Fact]
+ public void ReadAsMultipartAsync_NullStreamProviderThrows()
+ {
+ HttpContent content = CreateContent("---");
+
+ Assert.ThrowsArgumentNull(() =>
+ {
+ content.ReadAsMultipartAsync(null);
+ }, "streamProvider");
+ }
+
+ [Theory]
+ [PropertyData("Boundaries")]
+ [Trait("Description", "ReadAsMultipartAsync(HttpContent, IMultipartStreamProvider, int) throws on buffersize.")]
+ public void ReadAsMultipartAsyncStreamProviderThrowsOnBufferSize(string boundary)
+ {
+ HttpContent content = CreateContent(boundary);
+ Assert.NotNull(content);
+
+ Assert.ThrowsArgument(() =>
+ {
+ content.ReadAsMultipartAsync(new MemoryStreamProvider(), ParserData.MinBufferSize - 1);
+ }, "bufferSize");
+ }
+
+ [Fact]
+ [Trait("Description", "IsMimeMultipartContent(HttpContent) checks extension method arguments.")]
+ public void IsMimeMultipartContentVerifyArguments()
+ {
+ Assert.ThrowsArgumentNull(() =>
+ {
+ HttpContent content = null;
+ HttpContentMultipartExtensions.IsMimeMultipartContent(content);
+ }, "content");
+ }
+
+ [Fact]
+ public void IsMumeMultipartContentReturnsFalseForEmptyValues()
+ {
+ Assert.False(new ByteArrayContent(new byte[] { }).IsMimeMultipartContent(), "HttpContent should not be valid MIME multipart content");
+
+ Assert.False(new StringContent(String.Empty).IsMimeMultipartContent(), "HttpContent should not be valid MIME multipart content");
+
+ Assert.False(new StringContent(String.Empty, Encoding.UTF8, "multipart/form-data").IsMimeMultipartContent(), "HttpContent should not be valid MIME multipart content");
+ }
+
+ [Theory]
+ [PropertyData("Boundaries")]
+ [Trait("Description", "IsMimeMultipartContent(HttpContent) responds correctly to MIME multipart and other content")]
+ public void IsMimeMultipartContent(string boundary)
+ {
+ HttpContent content = CreateContent(boundary);
+ Assert.NotNull(content);
+ Assert.True(content.IsMimeMultipartContent());
+ }
+
+ [Theory]
+ [PropertyData("Boundaries")]
+ [Trait("Description", "IsMimeMultipartContent(HttpContent, string) throws on null string.")]
+ public void IsMimeMultipartContentThrowsOnNullString(string boundary)
+ {
+ HttpContent content = CreateContent(boundary);
+ Assert.NotNull(content);
+ foreach (var subtype in CommonUnitTestDataSets.EmptyStrings)
+ {
+ Assert.ThrowsArgumentNull(() =>
+ {
+ content.IsMimeMultipartContent(subtype);
+ }, "subtype");
+ }
+ }
+
+ [Fact]
+ public void ReadAsMultipartAsync_SuccessfullyParsesContent()
+ {
+ HttpContent successContent;
+ Task<IEnumerable<HttpContent>> task;
+ IEnumerable<HttpContent> result;
+
+ successContent = CreateContent("boundary", "A", "B", "C");
+ task = successContent.ReadAsMultipartAsync();
+ task.Wait(TimeoutConstant.DefaultTimeout);
+ result = task.Result;
+ Assert.Equal(3, result.Count());
+
+ successContent = CreateContent("boundary", "A", "B", "C");
+ task = successContent.ReadAsMultipartAsync(new MemoryStreamProvider());
+ task.Wait(TimeoutConstant.DefaultTimeout);
+ result = task.Result;
+ Assert.Equal(3, result.Count());
+
+ successContent = CreateContent("boundary", "A", "B", "C");
+ task = successContent.ReadAsMultipartAsync(new MemoryStreamProvider(), 1024);
+ task.Wait(TimeoutConstant.DefaultTimeout);
+ result = task.Result;
+ Assert.Equal(3, result.Count());
+ }
+
+ [Theory]
+ [PropertyData("Boundaries")]
+ public void ReadAsMultipartAsync_ParsesEmptyContentSuccessfully(string boundary)
+ {
+ HttpContent content = CreateContent(boundary);
+ Task<IEnumerable<HttpContent>> task = content.ReadAsMultipartAsync();
+ task.Wait(TimeoutConstant.DefaultTimeout);
+ IEnumerable<HttpContent> result = task.Result;
+ Assert.Equal(0, result.Count());
+ }
+
+ [Fact]
+ public void ReadAsMultipartAsync_WithBadStreamProvider_Throws()
+ {
+ HttpContent content = CreateContent("--", "A", "B", "C");
+
+ var invalidOperationException = Assert.Throws<InvalidOperationException>(
+ () => content.ReadAsMultipartAsync(new BadStreamProvider()).Result,
+ "The stream provider of type 'BadStreamProvider' threw an exception."
+ );
+ Assert.NotNull(invalidOperationException.InnerException);
+ Assert.Equal(ExceptionStreamProviderMessage, invalidOperationException.InnerException.Message);
+ }
+
+ [Fact]
+ public void ReadAsMultipartAsync_NullStreamProvider_Throws()
+ {
+ HttpContent content = CreateContent("--", "A", "B", "C");
+
+ Assert.Throws<InvalidOperationException>(
+ () => content.ReadAsMultipartAsync(new NullStreamProvider()).Result,
+ "The stream provider of type 'NullStreamProvider' returned null. It must return a writable 'Stream' instance."
+ );
+ }
+
+ [Fact]
+ public void ReadAsMultipartAsync_ReadOnlyStream_Throws()
+ {
+ HttpContent content = CreateContent("--", "A", "B", "C");
+
+ Assert.Throws<InvalidOperationException>(
+ () => content.ReadAsMultipartAsync(new ReadOnlyStreamProvider()).Result,
+ "The stream provider of type 'ReadOnlyStreamProvider' returned a read-only stream. It must return a writable 'Stream' instance."
+ );
+ }
+
+ [Fact]
+ public void ReadAsMultipartAsync_PrematureEndOfStream_Throws()
+ {
+ HttpContent content = new StreamContent(Stream.Null);
+ var contentType = new MediaTypeHeaderValue("multipart/form-data");
+ contentType.Parameters.Add(new NameValueHeaderValue("boundary", "\"{--\""));
+ content.Headers.ContentType = contentType;
+
+ Assert.Throws<IOException>(
+ () => content.ReadAsMultipartAsync().Result,
+ "Unexpected end of MIME multipart stream. MIME multipart message is not complete."
+ );
+ }
+
+ [Fact]
+ public void ReadAsMultipartAsync_ReadErrorOnStream_Throws()
+ {
+ HttpContent content = new StreamContent(new ReadErrorStream());
+ var contentType = new MediaTypeHeaderValue("multipart/form-data");
+ contentType.Parameters.Add(new NameValueHeaderValue("boundary", "\"--\""));
+ content.Headers.ContentType = contentType;
+
+ var ioException = Assert.Throws<IOException>(
+ () => content.ReadAsMultipartAsync().Result,
+ "Error reading MIME multipart body part."
+ );
+ Assert.NotNull(ioException.InnerException);
+ Assert.Equal(ExceptionAsyncStreamMessage, ioException.InnerException.Message);
+ }
+
+ [Fact]
+ public void ReadAsMultipartAsync_WriteErrorOnStream_Throws()
+ {
+ HttpContent content = CreateContent("--", "A", "B", "C");
+
+ var ioException = Assert.Throws<IOException>(
+ () => content.ReadAsMultipartAsync(new WriteErrorStreamProvider()).Result,
+ "Error writing MIME multipart body part to output stream."
+ );
+ Assert.NotNull(ioException.InnerException);
+ Assert.Equal(ExceptionAsyncStreamMessage, ioException.InnerException.Message);
+ }
+
+ [Theory]
+ [PropertyData("Boundaries")]
+ public void ReadAsMultipartAsync_SingleShortBodyPart_ParsesSuccessfully(string boundary)
+ {
+ HttpContent content = CreateContent(boundary, "A");
+ IEnumerable<HttpContent> result = content.ReadAsMultipartAsync().Result;
+ Assert.Equal(1, result.Count());
+ Assert.Equal("A", result.ElementAt(0).ReadAsStringAsync().Result);
+ ValidateContents(result);
+ }
+
+ [Theory]
+ [PropertyData("Boundaries")]
+ public void ReadAsMultipartAsync_MultipleShortBodyParts_ParsesSuccessfully(string boundary)
+ {
+ string[] text = new string[] { "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" };
+
+ HttpContent content = CreateContent(boundary, text);
+ IEnumerable<HttpContent> result = content.ReadAsMultipartAsync().Result;
+ Assert.Equal(text.Length, result.Count());
+ for (var check = 0; check < text.Length; check++)
+ {
+ Assert.Equal(text[check], result.ElementAt(check).ReadAsStringAsync().Result);
+ }
+
+ ValidateContents(result);
+ }
+
+ [Theory]
+ [PropertyData("Boundaries")]
+ [Trait("Description", "ReadAsMultipartAsync(HttpContent) parses with single long body asynchronously.")]
+ public void ReadAsMultipartAsyncSingleLongBodyPartAsync(string boundary)
+ {
+ HttpContent content = CreateContent(boundary, LongText);
+ Task<IEnumerable<HttpContent>> task = content.ReadAsMultipartAsync();
+ task.Wait(TimeoutConstant.DefaultTimeout);
+ IEnumerable<HttpContent> result = task.Result;
+ Assert.Equal(1, result.Count());
+ Assert.Equal(LongText, result.ElementAt(0).ReadAsStringAsync().Result);
+
+ ValidateContents(result);
+ }
+
+ [Theory]
+ [PropertyData("Boundaries")]
+ [Trait("Description", "ReadAsMultipartAsync(HttpContent) parses with multiple long bodies asynchronously.")]
+ public void ReadAsMultipartAsyncMultipleLongBodyPartsAsync(string boundary)
+ {
+ string[] text = new string[] {
+ "A" + LongText + "A",
+ "B" + LongText + "B",
+ "C" + LongText + "C",
+ "D" + LongText + "D",
+ "E" + LongText + "E",
+ "F" + LongText + "F",
+ "G" + LongText + "G",
+ "H" + LongText + "H",
+ "I" + LongText + "I",
+ "J" + LongText + "J",
+ "K" + LongText + "K",
+ "L" + LongText + "L",
+ "M" + LongText + "M",
+ "N" + LongText + "N",
+ "O" + LongText + "O",
+ "P" + LongText + "P",
+ "Q" + LongText + "Q",
+ "R" + LongText + "R",
+ "S" + LongText + "S",
+ "T" + LongText + "T",
+ "U" + LongText + "U",
+ "V" + LongText + "V",
+ "W" + LongText + "W",
+ "X" + LongText + "X",
+ "Y" + LongText + "Y",
+ "Z" + LongText + "Z"};
+
+ HttpContent content = CreateContent(boundary, text);
+ Task<IEnumerable<HttpContent>> task = content.ReadAsMultipartAsync(new MemoryStreamProvider(), ParserData.MinBufferSize);
+ task.Wait(TimeoutConstant.DefaultTimeout);
+ IEnumerable<HttpContent> result = task.Result;
+ Assert.Equal(text.Length, result.Count());
+ for (var check = 0; check < text.Length; check++)
+ {
+ Assert.Equal(text[check], result.ElementAt(check).ReadAsStringAsync().Result);
+ }
+
+ ValidateContents(result);
+ }
+
+ [Theory]
+ [PropertyData("Boundaries")]
+ [Trait("Description", "ReadAsMultipartAsync(HttpContent) parses content generated by MultipartContent asynchronously.")]
+ public void ReadAsMultipartAsyncUsingMultipartContentAsync(string boundary)
+ {
+ MultipartContent content = new MultipartContent("mixed", boundary);
+ content.Add(new StringContent("A"));
+ content.Add(new StringContent("B"));
+ content.Add(new StringContent("C"));
+
+ MemoryStream memStream = new MemoryStream();
+ content.CopyToAsync(memStream).Wait();
+ memStream.Position = 0;
+ byte[] data = memStream.ToArray();
+ var byteContent = new ByteArrayContent(data);
+ byteContent.Headers.ContentType = content.Headers.ContentType;
+
+ Task<IEnumerable<HttpContent>> task = byteContent.ReadAsMultipartAsync();
+ task.Wait(TimeoutConstant.DefaultTimeout);
+ IEnumerable<HttpContent> result = task.Result;
+ Assert.Equal(3, result.Count());
+ Assert.Equal("A", result.ElementAt(0).ReadAsStringAsync().Result);
+ Assert.Equal("B", result.ElementAt(1).ReadAsStringAsync().Result);
+ Assert.Equal("C", result.ElementAt(2).ReadAsStringAsync().Result);
+ }
+
+ [Theory]
+ [PropertyData("Boundaries")]
+ [Trait("Description", "ReadAsMultipartAsync(HttpContent) parses nested content generated by MultipartContent asynchronously.")]
+ public void ReadAsMultipartAsyncNestedMultipartContentAsync(string boundary)
+ {
+ const int nesting = 10;
+ const string innerText = "Content";
+
+ MultipartContent innerContent = new MultipartContent("mixed", boundary);
+ innerContent.Add(new StringContent(innerText));
+ for (var cnt = 0; cnt < nesting; cnt++)
+ {
+ string outerBoundary = String.Format("{0}_{1}", boundary, cnt);
+ MultipartContent outerContent = new MultipartContent("mixed", outerBoundary);
+ outerContent.Add(innerContent);
+ innerContent = outerContent;
+ }
+
+ MemoryStream memStream = new MemoryStream();
+ innerContent.CopyToAsync(memStream).Wait();
+ memStream.Position = 0;
+ byte[] data = memStream.ToArray();
+ HttpContent content = new ByteArrayContent(data);
+ content.Headers.ContentType = innerContent.Headers.ContentType;
+
+ for (var cnt = 0; cnt < nesting + 1; cnt++)
+ {
+ Task<IEnumerable<HttpContent>> task = content.ReadAsMultipartAsync();
+ task.Wait(TimeoutConstant.DefaultTimeout);
+ IEnumerable<HttpContent> result = task.Result;
+ Assert.Equal(1, result.Count());
+ content = result.ElementAt(0);
+ Assert.NotNull(content);
+ }
+
+ string text = content.ReadAsStringAsync().Result;
+ Assert.Equal(innerText, text);
+ }
+
+ public class ReadOnlyStream : MemoryStream
+ {
+ public override bool CanWrite
+ {
+ get
+ {
+ return false;
+ }
+ }
+ }
+
+ public class ReadErrorStream : MemoryStream
+ {
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ throw new Exception(ExceptionSyncStreamMessage);
+ }
+
+ public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
+ {
+ throw new Exception(ExceptionAsyncStreamMessage);
+ }
+ }
+
+ public class WriteErrorStream : MemoryStream
+ {
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ throw new Exception(ExceptionSyncStreamMessage);
+ }
+
+ public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
+ {
+ throw new Exception(ExceptionAsyncStreamMessage);
+ }
+ }
+
+ public class MemoryStreamProvider : IMultipartStreamProvider
+ {
+ public Stream GetStream(HttpContentHeaders headers)
+ {
+ return new MemoryStream();
+ }
+ }
+
+ public class BadStreamProvider : IMultipartStreamProvider
+ {
+ public Stream GetStream(HttpContentHeaders headers)
+ {
+ throw new Exception(ExceptionStreamProviderMessage);
+ }
+ }
+
+ public class NullStreamProvider : IMultipartStreamProvider
+ {
+ public Stream GetStream(HttpContentHeaders headers)
+ {
+ return null;
+ }
+ }
+
+ public class ReadOnlyStreamProvider : IMultipartStreamProvider
+ {
+ public Stream GetStream(HttpContentHeaders headers)
+ {
+ return new ReadOnlyStream();
+ }
+ }
+
+ public class WriteErrorStreamProvider : IMultipartStreamProvider
+ {
+ public Stream GetStream(HttpContentHeaders headers)
+ {
+ return new WriteErrorStream();
+ }
+ }
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/HttpMessageContentTests.cs b/test/System.Net.Http.Formatting.Test.Unit/HttpMessageContentTests.cs
new file mode 100644
index 00000000..92838147
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/HttpMessageContentTests.cs
@@ -0,0 +1,309 @@
+using System.Net.Http.Headers;
+using System.Threading.Tasks;
+using Microsoft.TestCommon;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Net.Http
+{
+ public class HttpMessageContentTests
+ {
+ private static readonly int iterations = 5;
+
+ private static void AddMessageHeaders(HttpHeaders headers)
+ {
+ headers.Add("N1", new string[] { "V1a", "V1b", "V1c", "V1d", "V1e" });
+ headers.Add("N2", "V2");
+ }
+
+ private static HttpRequestMessage CreateRequest(Uri requestUri, bool containsEntity)
+ {
+ HttpRequestMessage httpRequest = new HttpRequestMessage();
+ httpRequest.Method = new HttpMethod(ParserData.HttpMethod);
+ httpRequest.RequestUri = requestUri;
+ httpRequest.Version = new Version("1.2");
+ AddMessageHeaders(httpRequest.Headers);
+ if (containsEntity)
+ {
+ httpRequest.Content = new StringContent(ParserData.HttpMessageEntity);
+ }
+
+ return httpRequest;
+ }
+
+ private static HttpResponseMessage CreateResponse(bool containsEntity)
+ {
+ HttpResponseMessage httpResponse = new HttpResponseMessage();
+ httpResponse.StatusCode = ParserData.HttpStatus;
+ httpResponse.ReasonPhrase = ParserData.HttpReasonPhrase;
+ httpResponse.Version = new Version("1.2");
+ AddMessageHeaders(httpResponse.Headers);
+ if (containsEntity)
+ {
+ httpResponse.Content = new StringContent(ParserData.HttpMessageEntity);
+ }
+
+ return httpResponse;
+ }
+
+ private static string ReadContentAsync(HttpContent content)
+ {
+ Task task = content.LoadIntoBufferAsync();
+ task.Wait(TimeoutConstant.DefaultTimeout);
+ Assert.Equal(TaskStatus.RanToCompletion, task.Status);
+ return content.ReadAsStringAsync().Result;
+ }
+
+ private static void ValidateRequest(HttpContent content, bool containsEntity)
+ {
+ Assert.Equal(ParserData.HttpRequestMediaType, content.Headers.ContentType);
+ long? length = content.Headers.ContentLength;
+ Assert.NotNull(length);
+
+ string message = ReadContentAsync(content);
+
+ if (containsEntity)
+ {
+ Assert.Equal(ParserData.HttpRequestWithEntity.Length, length);
+ Assert.Equal(ParserData.HttpRequestWithEntity, message);
+ }
+ else
+ {
+ Assert.Equal(ParserData.HttpRequest.Length, length);
+ Assert.Equal(ParserData.HttpRequest, message);
+ }
+ }
+
+ private static void ValidateResponse(HttpContent content, bool containsEntity)
+ {
+ Assert.Equal(ParserData.HttpResponseMediaType, content.Headers.ContentType);
+ long? length = content.Headers.ContentLength;
+ Assert.NotNull(length);
+
+ string message = ReadContentAsync(content);
+
+ if (containsEntity)
+ {
+ Assert.Equal(ParserData.HttpResponseWithEntity.Length, length);
+ Assert.Equal(ParserData.HttpResponseWithEntity, message);
+ }
+ else
+ {
+ Assert.Equal(ParserData.HttpResponse.Length, length);
+ Assert.Equal(ParserData.HttpResponse, message);
+ }
+ }
+
+ [Fact]
+ public void TypeIsCorrect()
+ {
+ Assert.Type.HasProperties<HttpMessageContent, HttpContent>(TypeAssert.TypeProperties.IsPublicVisibleClass | TypeAssert.TypeProperties.IsDisposable);
+ }
+
+ [Fact]
+ public void RequestConstructor()
+ {
+ HttpRequestMessage request = new HttpRequestMessage();
+ HttpMessageContent instance = new HttpMessageContent(request);
+ Assert.NotNull(instance);
+ Assert.Same(request, instance.HttpRequestMessage);
+ Assert.Null(instance.HttpResponseMessage);
+ }
+
+ [Fact]
+ public void RequestConstructorThrowsOnNull()
+ {
+ Assert.ThrowsArgumentNull(() => { new HttpMessageContent((HttpRequestMessage)null); }, "httpRequest");
+ }
+
+ [Fact]
+ public void ResponseConstructor()
+ {
+ HttpResponseMessage response = new HttpResponseMessage();
+ HttpMessageContent instance = new HttpMessageContent(response);
+ Assert.NotNull(instance);
+ Assert.Same(response, instance.HttpResponseMessage);
+ Assert.Null(instance.HttpRequestMessage);
+ }
+
+ [Fact]
+ public void ResponseConstructorThrowsOnNull()
+ {
+ Assert.ThrowsArgumentNull(() => { new HttpMessageContent((HttpResponseMessage)null); }, "httpResponse");
+ }
+
+
+ [Fact]
+ public void SerializeRequest()
+ {
+ for (int cnt = 0; cnt < iterations; cnt++)
+ {
+ HttpRequestMessage request = CreateRequest(ParserData.HttpRequestUri, false);
+ HttpMessageContent instance = new HttpMessageContent(request);
+ ValidateRequest(instance, false);
+ }
+ }
+
+ [Fact]
+ public void SerializeRequestWithExistingHostHeader()
+ {
+ HttpRequestMessage request = CreateRequest(ParserData.HttpRequestUri, false);
+ string host = ParserData.HttpHostName;
+ request.Headers.Host = host;
+ HttpMessageContent instance = new HttpMessageContent(request);
+ string message = ReadContentAsync(instance);
+ Assert.Equal(ParserData.HttpRequestWithHost, message);
+ }
+
+ [Fact]
+ public void SerializeRequestMultipleTimes()
+ {
+ HttpRequestMessage request = CreateRequest(ParserData.HttpRequestUri, false);
+ HttpMessageContent instance = new HttpMessageContent(request);
+ for (int cnt = 0; cnt < iterations; cnt++)
+ {
+ ValidateRequest(instance, false);
+ }
+ }
+
+ [Fact]
+ public void SerializeResponse()
+ {
+ for (int cnt = 0; cnt < iterations; cnt++)
+ {
+ HttpResponseMessage response = CreateResponse(false);
+ HttpMessageContent instance = new HttpMessageContent(response);
+ ValidateResponse(instance, false);
+ }
+ }
+
+ [Fact]
+ public void SerializeResponseMultipleTimes()
+ {
+ HttpResponseMessage response = CreateResponse(false);
+ HttpMessageContent instance = new HttpMessageContent(response);
+ for (int cnt = 0; cnt < iterations; cnt++)
+ {
+ ValidateResponse(instance, false);
+ }
+ }
+
+ [Fact]
+ public void SerializeRequestWithEntity()
+ {
+ for (int cnt = 0; cnt < iterations; cnt++)
+ {
+ HttpRequestMessage request = CreateRequest(ParserData.HttpRequestUri, true);
+ HttpMessageContent instance = new HttpMessageContent(request);
+ ValidateRequest(instance, true);
+ }
+ }
+
+ [Fact]
+ public void SerializeRequestWithEntityMultipleTimes()
+ {
+ HttpRequestMessage request = CreateRequest(ParserData.HttpRequestUri, true);
+ HttpMessageContent instance = new HttpMessageContent(request);
+ for (int cnt = 0; cnt < iterations; cnt++)
+ {
+ ValidateRequest(instance, true);
+ }
+ }
+
+ [Fact]
+ public void SerializeResponseWithEntity()
+ {
+ for (int cnt = 0; cnt < iterations; cnt++)
+ {
+ HttpResponseMessage response = CreateResponse(true);
+ HttpMessageContent instance = new HttpMessageContent(response);
+ ValidateResponse(instance, true);
+ }
+ }
+
+ [Fact]
+ public void SerializeResponseWithEntityMultipleTimes()
+ {
+ HttpResponseMessage response = CreateResponse(true);
+ HttpMessageContent instance = new HttpMessageContent(response);
+ for (int cnt = 0; cnt < iterations; cnt++)
+ {
+ ValidateResponse(instance, true);
+ }
+ }
+
+ [Fact]
+ public void SerializeRequestAsync()
+ {
+ for (int cnt = 0; cnt < iterations; cnt++)
+ {
+ HttpRequestMessage request = CreateRequest(ParserData.HttpRequestUri, false);
+ HttpMessageContent instance = new HttpMessageContent(request);
+ ValidateRequest(instance, false);
+ }
+ }
+
+ [Fact]
+ public void SerializeResponseAsync()
+ {
+ for (int cnt = 0; cnt < iterations; cnt++)
+ {
+ HttpResponseMessage response = CreateResponse(false);
+ HttpMessageContent instance = new HttpMessageContent(response);
+ ValidateResponse(instance, false);
+ }
+ }
+
+ [Fact]
+ public void SerializeRequestWithPortAndQueryAsync()
+ {
+ for (int cnt = 0; cnt < iterations; cnt++)
+ {
+ HttpRequestMessage request = CreateRequest(ParserData.HttpRequestUriWithPortAndQuery, false);
+ HttpMessageContent instance = new HttpMessageContent(request);
+ string message = ReadContentAsync(instance);
+ Assert.Equal(ParserData.HttpRequestWithPortAndQuery, message);
+ }
+ }
+
+ [Fact]
+ public void SerializeRequestWithEntityAsync()
+ {
+ for (int cnt = 0; cnt < iterations; cnt++)
+ {
+ HttpRequestMessage request = CreateRequest(ParserData.HttpRequestUri, true);
+ HttpMessageContent instance = new HttpMessageContent(request);
+ ValidateRequest(instance, true);
+ }
+ }
+
+ [Fact]
+ public void SerializeResponseWithEntityAsync()
+ {
+ for (int cnt = 0; cnt < iterations; cnt++)
+ {
+ HttpResponseMessage response = CreateResponse(true);
+ HttpMessageContent instance = new HttpMessageContent(response);
+ ValidateResponse(instance, true);
+ }
+ }
+
+ [Fact]
+ public void DisposeInnerHttpRequestMessage()
+ {
+ HttpRequestMessage request = CreateRequest(ParserData.HttpRequestUri, false);
+ HttpMessageContent instance = new HttpMessageContent(request);
+ instance.Dispose();
+ Assert.ThrowsObjectDisposed(() => { request.Method = HttpMethod.Get; }, typeof(HttpRequestMessage).FullName);
+ }
+
+ [Fact]
+ public void DisposeInnerHttpResponseMessage()
+ {
+ HttpResponseMessage response = CreateResponse(false);
+ HttpMessageContent instance = new HttpMessageContent(response);
+ instance.Dispose();
+ Assert.ThrowsObjectDisposed(() => { response.StatusCode = HttpStatusCode.OK; }, typeof(HttpResponseMessage).FullName);
+ }
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/Mocks/MockHttpContent.cs b/test/System.Net.Http.Formatting.Test.Unit/Mocks/MockHttpContent.cs
new file mode 100644
index 00000000..58e6665a
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/Mocks/MockHttpContent.cs
@@ -0,0 +1,90 @@
+using System.IO;
+using System.Net.Http.Headers;
+using System.Threading.Tasks;
+
+namespace System.Net.Http.Formatting.Mocks
+{
+ public delegate bool TryComputeLengthDelegate(out long length);
+
+ public class MockHttpContent : HttpContent
+ {
+ public MockHttpContent()
+ {
+ }
+
+ public MockHttpContent(HttpContent innerContent)
+ {
+ InnerContent = innerContent;
+ Headers.ContentType = innerContent.Headers.ContentType;
+ }
+
+ public MockHttpContent(MediaTypeHeaderValue contentType)
+ {
+ if (contentType == null)
+ {
+ throw new ArgumentNullException("contentType");
+ }
+ Headers.ContentType = contentType;
+ }
+
+ public MockHttpContent(string contentType)
+ {
+ if (String.IsNullOrWhiteSpace(contentType))
+ {
+ throw new ArgumentNullException("contentType");
+ }
+ Headers.ContentType = new MediaTypeHeaderValue(contentType);
+ }
+
+ public HttpContent InnerContent { get; set; }
+
+ public Action<bool> DisposeCallback { get; set; }
+ public TryComputeLengthDelegate TryComputeLengthCallback { get; set; }
+ public Action<Stream, TransportContext> SerializeToStreamCallback { get; set; }
+ public Func<Stream, TransportContext, Task> SerializeToStreamAsyncCallback { get; set; }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (DisposeCallback != null)
+ {
+ DisposeCallback(disposing);
+ }
+
+ base.Dispose(disposing);
+ }
+
+ protected override Task SerializeToStreamAsync(Stream stream, TransportContext context)
+ {
+ if (SerializeToStreamAsyncCallback != null)
+ {
+ return SerializeToStreamAsyncCallback(stream, context);
+ }
+ else if (InnerContent != null)
+ {
+ return InnerContent.CopyToAsync(stream, context);
+ }
+ else
+ {
+ throw new InvalidOperationException("Construct with inner HttpContent or set SerializeToStreamCallback first.");
+ }
+ }
+
+ protected override bool TryComputeLength(out long length)
+ {
+ if (TryComputeLengthCallback != null)
+ {
+ return TryComputeLengthCallback(out length);
+ }
+
+ if (InnerContent != null)
+ {
+ long? len = InnerContent.Headers.ContentLength;
+ length = len.HasValue ? len.Value : 0L;
+ return len.HasValue;
+ }
+
+ length = 0L;
+ return false;
+ }
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/Mocks/MockMediaTypeFormatter.cs b/test/System.Net.Http.Formatting.Test.Unit/Mocks/MockMediaTypeFormatter.cs
new file mode 100644
index 00000000..b2381ce8
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/Mocks/MockMediaTypeFormatter.cs
@@ -0,0 +1,30 @@
+
+namespace System.Net.Http.Formatting.Mocks
+{
+ public class MockMediaTypeFormatter : MediaTypeFormatter
+ {
+ public bool CallBase { get; set; }
+ public Func<Type, bool> CanReadTypeCallback { get; set; }
+ public Func<Type, bool> CanWriteTypeCallback { get; set; }
+
+ public override bool CanReadType(Type type)
+ {
+ if (!CallBase && CanReadTypeCallback == null)
+ {
+ throw new InvalidOperationException("CallBase or CanReadTypeCallback must be set first.");
+ }
+
+ return CanReadTypeCallback != null ? CanReadTypeCallback(type) : true;
+ }
+
+ public override bool CanWriteType(Type type)
+ {
+ if (!CallBase && CanWriteTypeCallback == null)
+ {
+ throw new InvalidOperationException("CallBase or CanWriteTypeCallback must be set first.");
+ }
+
+ return CanWriteTypeCallback != null ? CanWriteTypeCallback(type) : true;
+ }
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/Mocks/TestableHttpMessageHandler.cs b/test/System.Net.Http.Formatting.Test.Unit/Mocks/TestableHttpMessageHandler.cs
new file mode 100644
index 00000000..e4c53bdc
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/Mocks/TestableHttpMessageHandler.cs
@@ -0,0 +1,18 @@
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace System.Net.Http.Mocks
+{
+ public class TestableHttpMessageHandler : HttpMessageHandler
+ {
+ public virtual Task<HttpResponseMessage> SendAsyncPublic(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ throw new NotImplementedException();
+ }
+
+ protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ return SendAsyncPublic(request, cancellationToken);
+ }
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/MultipartFileStreamProviderTests.cs b/test/System.Net.Http.Formatting.Test.Unit/MultipartFileStreamProviderTests.cs
new file mode 100644
index 00000000..c8bb0fdb
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/MultipartFileStreamProviderTests.cs
@@ -0,0 +1,117 @@
+using System.IO;
+using System.Linq;
+using Microsoft.TestCommon;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Net.Http
+{
+ public class MultipartFileStreamProviderTests
+ {
+ const int defaultBufferSize = 0x1000;
+ const string validPath = @"c:\some\path";
+
+ [Fact]
+ [Trait("Description", "MultipartFileStreamProvider is public, visible type.")]
+ public void TypeIsCorrect()
+ {
+ Assert.Type.HasProperties(
+ typeof(MultipartFileStreamProvider),
+ TypeAssert.TypeProperties.IsPublicVisibleClass,
+ typeof(IMultipartStreamProvider));
+ }
+
+
+ [Fact]
+ [Trait("Description", "MultipartFileStreamProvider default ctor.")]
+ public void DefaultConstructor()
+ {
+ MultipartFileStreamProvider instance = new MultipartFileStreamProvider();
+ Assert.NotNull(instance);
+ }
+
+ [Fact]
+ [Trait("Description", "MultipartFileStreamProvider ctor with invalid root paths.")]
+ public void ConstructorInvalidRootPath()
+ {
+ Assert.ThrowsArgumentNull(() => { new MultipartFileStreamProvider(null); }, "rootPath");
+
+ foreach (string path in TestData.NotSupportedFilePaths)
+ {
+ Assert.Throws<NotSupportedException>(() => new MultipartFileStreamProvider(path, defaultBufferSize));
+ }
+
+ foreach (string path in TestData.InvalidNonNullFilePaths)
+ {
+ // Note: Path.GetFileName doesn't set the argument name when throwing.
+ Assert.ThrowsArgument(() => { new MultipartFileStreamProvider(path, defaultBufferSize); }, null, allowDerivedExceptions: true);
+ }
+ }
+
+ [Fact]
+ [Trait("Description", "MultipartFileStreamProvider ctor with null path.")]
+ public void ConstructorInvalidBufferSize()
+ {
+ Assert.ThrowsArgumentOutOfRange(() => { new MultipartFileStreamProvider(validPath, -1); }, "bufferSize", exceptionMessage: null);
+ Assert.ThrowsArgumentOutOfRange(() => { new MultipartFileStreamProvider(validPath, 0); }, "bufferSize", exceptionMessage: null);
+ }
+
+
+
+ [Fact]
+ [Trait("Description", "BodyPartFileNames empty.")]
+ public void EmptyBodyPartFileNames()
+ {
+ MultipartFileStreamProvider instance = new MultipartFileStreamProvider();
+ Assert.NotNull(instance.BodyPartFileNames);
+ Assert.Equal(0, instance.BodyPartFileNames.Count());
+ }
+
+ [Fact]
+ [Trait("Description", "GetStream(HttpContentHeaders) throws on null.")]
+ public void GetStreamThrowsOnNull()
+ {
+ MultipartFileStreamProvider instance = new MultipartFileStreamProvider();
+ Assert.ThrowsArgumentNull(() => { instance.GetStream(null); }, "headers");
+ }
+
+ [Fact]
+ [Trait("Description", "GetStream(HttpContentHeaders) validation.")]
+ public void GetStreamValidation()
+ {
+ Stream stream0 = null;
+ Stream stream1 = null;
+
+ try
+ {
+ MultipartFormDataContent content = new MultipartFormDataContent();
+ content.Add(new StringContent("Not a file"), "notafile");
+ content.Add(new StringContent("This is a file"), "file", "filename");
+
+ MultipartFileStreamProvider instance = new MultipartFileStreamProvider();
+ stream0 = instance.GetStream(content.ElementAt(0).Headers);
+ Assert.IsType<FileStream>(stream0);
+ stream1 = instance.GetStream(content.ElementAt(1).Headers);
+ Assert.IsType<FileStream>(stream1);
+
+ Assert.Equal(2, instance.BodyPartFileNames.Count());
+ Assert.Contains("BodyPart", instance.BodyPartFileNames.ElementAt(0));
+ Assert.True(instance.BodyPartFileNames.ElementAt(1).EndsWith("filename"));
+ }
+ finally
+ {
+ if (stream0 != null)
+ {
+ stream0.Close();
+ }
+
+ if (stream1 != null)
+ {
+ stream1.Close();
+ }
+ }
+ }
+
+
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/MultipartFormDataStreamProviderTests.cs b/test/System.Net.Http.Formatting.Test.Unit/MultipartFormDataStreamProviderTests.cs
new file mode 100644
index 00000000..2df66233
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/MultipartFormDataStreamProviderTests.cs
@@ -0,0 +1,120 @@
+using System.IO;
+using System.Linq;
+using Microsoft.TestCommon;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Net.Http
+{
+ public class MultipartFormDataStreamProviderTests
+ {
+ const int defaultBufferSize = 0x1000;
+ const string validPath = @"c:\some\path";
+
+ [Fact]
+ [Trait("Description", "MultipartFormDataStreamProvider is public, visible type.")]
+ public void TypeIsCorrect()
+ {
+ Assert.Type.HasProperties(
+ typeof(MultipartFormDataStreamProvider),
+ TypeAssert.TypeProperties.IsPublicVisibleClass,
+ typeof(IMultipartStreamProvider));
+ }
+
+ [Fact]
+ [Trait("Description", "MultipartFormDataStreamProvider default ctor.")]
+ public void DefaultConstructor()
+ {
+ MultipartFormDataStreamProvider instance = new MultipartFormDataStreamProvider();
+ Assert.NotNull(instance);
+ }
+
+ [Fact]
+ [Trait("Description", "MultipartFormDataStreamProvider ctor with invalid root paths.")]
+ public void ConstructorInvalidRootPath()
+ {
+ Assert.ThrowsArgumentNull(() => { new MultipartFormDataStreamProvider(null); }, "rootPath");
+
+ foreach (string path in TestData.NotSupportedFilePaths)
+ {
+ Assert.Throws<NotSupportedException>(() => new MultipartFormDataStreamProvider(path, defaultBufferSize));
+ }
+
+ foreach (string path in TestData.InvalidNonNullFilePaths)
+ {
+ // Note: Path.GetFileName doesn't set the argument name when throwing.
+ Assert.ThrowsArgument(() => { new MultipartFormDataStreamProvider(path, defaultBufferSize); }, null, allowDerivedExceptions: true);
+ }
+ }
+
+ [Fact]
+ [Trait("Description", "MultipartFormDataStreamProvider ctor with null path.")]
+ public void ConstructorInvalidBufferSize()
+ {
+ Assert.ThrowsArgumentOutOfRange(() => { new MultipartFormDataStreamProvider(validPath, -1); }, "bufferSize", exceptionMessage: null);
+ Assert.ThrowsArgumentOutOfRange(() => { new MultipartFormDataStreamProvider(validPath, 0); }, "bufferSize", exceptionMessage: null);
+ }
+
+ [Fact]
+ [Trait("Description", "BodyPartFileNames empty.")]
+ public void EmptyBodyPartFileNames()
+ {
+ MultipartFormDataStreamProvider instance = new MultipartFormDataStreamProvider();
+ Assert.NotNull(instance.BodyPartFileNames);
+ Assert.Equal(0, instance.BodyPartFileNames.Count);
+ }
+
+ [Fact]
+ [Trait("Description", "GetStream(HttpContentHeaders) throws on null.")]
+ public void GetStreamThrowsOnNull()
+ {
+ MultipartFormDataStreamProvider instance = new MultipartFormDataStreamProvider();
+ Assert.ThrowsArgumentNull(() => { instance.GetStream(null); }, "headers");
+ }
+
+ [Fact]
+ [Trait("Description", "GetStream(HttpContentHeaders) throws on no Content-Disposition header.")]
+ public void GetStreamThrowsOnNoContentDisposition()
+ {
+ MultipartFormDataStreamProvider instance = new MultipartFormDataStreamProvider();
+ HttpContent content = new StringContent("text");
+ Assert.Throws<IOException>(() => { instance.GetStream(content.Headers); }, RS.Format(Properties.Resources.MultipartFormDataStreamProviderNoContentDisposition, "Content-Disposition"));
+ }
+
+ [Fact]
+ [Trait("Description", "GetStream(HttpContentHeaders) validation.")]
+ public void GetStreamValidation()
+ {
+ Stream stream0 = null;
+ Stream stream1 = null;
+
+ try
+ {
+ MultipartFormDataContent content = new MultipartFormDataContent();
+ content.Add(new StringContent("Not a file"), "notafile");
+ content.Add(new StringContent("This is a file"), "file", "filename");
+
+ MultipartFormDataStreamProvider instance = new MultipartFormDataStreamProvider();
+ stream0 = instance.GetStream(content.ElementAt(0).Headers);
+ Assert.IsType<MemoryStream>(stream0);
+ stream1 = instance.GetStream(content.ElementAt(1).Headers);
+ Assert.IsType<FileStream>(stream1);
+
+ Assert.Equal(1, instance.BodyPartFileNames.Count);
+ Assert.Equal(content.ElementAt(1).Headers.ContentDisposition.FileName, instance.BodyPartFileNames.Keys.ElementAt(0));
+ }
+ finally
+ {
+ if (stream0 != null)
+ {
+ stream0.Close();
+ }
+
+ if (stream1 != null)
+ {
+ stream1.Close();
+ }
+ }
+ }
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/MultipartMemoryStreamProviderTests.cs b/test/System.Net.Http.Formatting.Test.Unit/MultipartMemoryStreamProviderTests.cs
new file mode 100644
index 00000000..afb0adb3
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/MultipartMemoryStreamProviderTests.cs
@@ -0,0 +1,55 @@
+using System.IO;
+using Microsoft.TestCommon;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Net.Http
+{
+ public class MultipartMemoryStreamProviderTests
+ {
+ [Fact]
+ [Trait("Description", "MultipartMemoryStreamProvider is internal type.")]
+ public void TypeIsCorrect()
+ {
+ Assert.Type.HasProperties(
+ typeof(MultipartMemoryStreamProvider),
+ TypeAssert.TypeProperties.IsClass,
+ typeof(IMultipartStreamProvider));
+ }
+
+ [Fact]
+ [Trait("Description", "MultipartMemoryStreamProvider default ctor.")]
+ public void DefaultConstructor()
+ {
+ MultipartMemoryStreamProvider instance = MultipartMemoryStreamProvider.Instance;
+ Assert.NotNull(instance);
+ }
+
+ [Fact]
+ [Trait("Description", "GetStream(HttpContentHeaders) throws on null.")]
+ public void GetStreamThrowsOnNull()
+ {
+ MultipartMemoryStreamProvider instance = MultipartMemoryStreamProvider.Instance;
+ Assert.ThrowsArgumentNull(() => { instance.GetStream(null); }, "headers");
+ }
+
+ [Fact]
+ [Trait("Description", "GetStream(HttpContentHeaders) throws on no Content-Disposition header.")]
+ public void GetStreamReturnsMemoryStream()
+ {
+ MultipartMemoryStreamProvider instance = MultipartMemoryStreamProvider.Instance;
+ HttpContent content = new StringContent("text");
+
+ Stream stream = instance.GetStream(content.Headers);
+ Assert.NotNull(stream);
+
+ MemoryStream memStream = stream as MemoryStream;
+ Assert.NotNull(stream);
+
+ Assert.Equal(0, stream.Length);
+ Assert.Equal(0, stream.Position);
+
+ Assert.NotSame(memStream, instance.GetStream(content.Headers));
+ }
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/ObjectContentOfTTests.cs b/test/System.Net.Http.Formatting.Test.Unit/ObjectContentOfTTests.cs
new file mode 100644
index 00000000..9503cb57
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/ObjectContentOfTTests.cs
@@ -0,0 +1,38 @@
+using System.Net.Http.Formatting;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Net.Http
+{
+ public class ObjectContentOfTTests
+ {
+ [Fact]
+ public void Constructor_WhenFormatterParameterIsNull_Throws()
+ {
+ Assert.ThrowsArgumentNull(() => new ObjectContent<string>("", formatter: null), "formatter");
+ Assert.ThrowsArgumentNull(() => new ObjectContent<string>("", formatter: null, mediaType: "foo/bar"), "formatter");
+ }
+
+ [Fact]
+ public void Constructor_SetsFormatterProperty()
+ {
+ var formatter = new Mock<MediaTypeFormatter>().Object;
+
+ var content = new ObjectContent<string>(null, formatter, mediaType: null);
+
+ Assert.Same(formatter, content.Formatter);
+ }
+
+ [Fact]
+ public void Constructor_CallsFormattersGetDefaultContentHeadersMethod()
+ {
+ var formatterMock = new Mock<MediaTypeFormatter>();
+
+ var content = new ObjectContent(typeof(string), "", formatterMock.Object, "foo/bar");
+
+ formatterMock.Verify(f => f.SetDefaultContentHeaders(typeof(string), content.Headers, "foo/bar"),
+ Times.Once());
+ }
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/ObjectContentTests.cs b/test/System.Net.Http.Formatting.Test.Unit/ObjectContentTests.cs
new file mode 100644
index 00000000..e54549b7
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/ObjectContentTests.cs
@@ -0,0 +1,168 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Net.Http.Formatting;
+using System.Net.Http.Headers;
+using System.Threading.Tasks;
+using Microsoft.TestCommon;
+using Moq;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Net.Http
+{
+ public class ObjectContentTests
+ {
+ private readonly object _value = new object();
+ private readonly MediaTypeFormatter _formatter = new TestableMediaTypeFormatter();
+
+ [Fact]
+ public void Constructor_WhenTypeArgumentIsNull_ThrowsEsxception()
+ {
+ Assert.ThrowsArgumentNull(() => new ObjectContent(null, _value, _formatter), "type");
+ Assert.ThrowsArgumentNull(() => new ObjectContent(null, _value, _formatter, mediaType: "foo/bar"), "type");
+ }
+
+ [Fact]
+ public void Constructor_WhenFormatterArgumentIsNull_ThrowsEsxception()
+ {
+ Assert.ThrowsArgumentNull(() => new ObjectContent(typeof(Object), _value, formatter: null), "formatter");
+ Assert.ThrowsArgumentNull(() => new ObjectContent(typeof(Object), _value, formatter: null, mediaType: "foo/bar"), "formatter");
+ }
+
+ [Fact]
+ public void Constructor_WhenValueIsNullAndTypeIsNotCompatible_ThrowsException()
+ {
+ Assert.Throws<InvalidOperationException>(() =>
+ {
+ new ObjectContent(typeof(int), null, new JsonMediaTypeFormatter());
+ }, "The 'ObjectContent' type cannot accept a null value for the value type 'Int32'.");
+ }
+
+ [Fact]
+ public void Constructor_WhenValueIsNotNullButTypeDoesNotMatch_ThrowsException()
+ {
+ Assert.Throws<ArgumentException>(() =>
+ {
+ new ObjectContent(typeof(IList<string>), new Dictionary<string, string>(), new JsonMediaTypeFormatter());
+ }, "An object of type 'Dictionary`2' cannot be used with a type parameter of 'IList`1'.\r\nParameter name: value");
+ }
+
+ [Fact]
+ public void Constructor_SetsFormatterProperty()
+ {
+ var content = new ObjectContent(typeof(object), _value, _formatter, mediaType: null);
+
+ Assert.Same(_formatter, content.Formatter);
+ }
+
+ [Fact]
+ public void Constructor_CallsFormattersGetDefaultContentHeadersMethod()
+ {
+ var formatterMock = new Mock<MediaTypeFormatter>();
+
+ var content = new ObjectContent(typeof(string), "", formatterMock.Object, "foo/bar");
+
+ formatterMock.Verify(f => f.SetDefaultContentHeaders(typeof(string), content.Headers, "foo/bar"),
+ Times.Once());
+ }
+
+ [Theory]
+ [PropertyData("ValidValueTypePairs")]
+ public void Constructor_WhenValueAndTypeAreCompatible_SetsValue(Type type, object value)
+ {
+ var oc = new ObjectContent(type, value, new JsonMediaTypeFormatter());
+
+ Assert.Same(value, oc.Value);
+ Assert.Equal(type, oc.ObjectType);
+ }
+
+ public static TheoryDataSet<Type, object> ValidValueTypePairs
+ {
+ get
+ {
+ return new TheoryDataSet<Type, object>
+ {
+ { typeof(Nullable<int>), null },
+ { typeof(void), null },
+ { typeof(string), null },
+ { typeof(int), 42 },
+ //{ typeof(int), (short)42 }, TODO should this work?
+ { typeof(object), "abc" },
+ { typeof(string), "abc" },
+ { typeof(IList<string>), new List<string>() },
+ };
+ }
+ }
+
+ [Fact]
+ public void SerializeToStreamAsync_CallsUnderlyingFormatter()
+ {
+ var stream = Stream.Null;
+ var context = new Mock<TransportContext>().Object;
+ var formatterMock = new Mock<TestableMediaTypeFormatter> { CallBase = true };
+ var oc = new TestableObjectContent(typeof(string), "abc", formatterMock.Object);
+ var task = new Task(() => { });
+ formatterMock.Setup(f => f.WriteToStreamAsync(typeof(string), "abc", stream, oc.Headers, context))
+ .Returns(task).Verifiable();
+
+ var result = oc.CallSerializeToStreamAsync(stream, context);
+
+ Assert.Same(task, result);
+ formatterMock.VerifyAll();
+ }
+
+ [Fact]
+ public void TryComputeLength_ReturnsFalseAnd0()
+ {
+ var oc = new TestableObjectContent(typeof(string), null, _formatter);
+ long length;
+
+ var result = oc.CallTryComputeLength(out length);
+
+ Assert.False(result);
+ Assert.Equal(-1, length);
+ }
+
+ public class TestableObjectContent : ObjectContent
+ {
+ public TestableObjectContent(Type type, object value, MediaTypeFormatter formatter)
+ : base(type, value, formatter)
+ {
+ }
+
+ public bool CallTryComputeLength(out long length)
+ {
+ return TryComputeLength(out length);
+ }
+
+ public Task CallSerializeToStreamAsync(Stream stream, TransportContext context)
+ {
+ return SerializeToStreamAsync(stream, context);
+ }
+ }
+
+ public class TestableMediaTypeFormatter : MediaTypeFormatter
+ {
+ public TestableMediaTypeFormatter()
+ {
+ SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json"));
+ }
+
+ public override bool CanReadType(Type type)
+ {
+ return true;
+ }
+
+ public override bool CanWriteType(Type type)
+ {
+ return true;
+ }
+
+ public override Task WriteToStreamAsync(Type type, object value, Stream stream, HttpContentHeaders contentHeaders, TransportContext transportContext)
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/ParserData.cs b/test/System.Net.Http.Formatting.Test.Unit/ParserData.cs
new file mode 100644
index 00000000..00ce31d5
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/ParserData.cs
@@ -0,0 +1,192 @@
+using System.Collections.Generic;
+using System.Net.Http.Headers;
+
+namespace System.Net.Http
+{
+ internal static class ParserData
+ {
+ public const int MinHeaderSize = 2;
+
+ public const int MinMessageSize = 10;
+
+ public const int MinRequestLineSize = 14;
+
+ public const int MinStatusLineSize = 15;
+
+ public const int MinBufferSize = 256;
+
+ public static IEnumerable<object[]> Boundaries
+ {
+ get
+ {
+ yield return new object[] { "1" };
+ yield return new object[] { "a" };
+ yield return new object[] { "'" };
+ yield return new object[] { "(" };
+ yield return new object[] { ")" };
+ yield return new object[] { "+" };
+ yield return new object[] { "_" };
+ yield return new object[] { "-" };
+ yield return new object[] { "." };
+ yield return new object[] { "/" };
+ yield return new object[] { ":" };
+ yield return new object[] { "=" };
+ yield return new object[] { "?" };
+ yield return new object[] { "--" };
+ yield return new object[] { "--------------------01234567890123456789" };
+ yield return new object[] { "--------------------01234567890123456789--------------------" };
+ yield return new object[] { "--A--B--C--D--E--F--" };
+ }
+ }
+
+ public static IEnumerable<object[]> Versions
+ {
+ get
+ {
+ yield return new object[] { Version.Parse("1.0") };
+ yield return new object[] { Version.Parse("1.1") };
+ yield return new object[] { Version.Parse("1.2") };
+ yield return new object[] { Version.Parse("2.0") };
+ yield return new object[] { Version.Parse("10.0") };
+ yield return new object[] { Version.Parse("1.15") };
+ }
+ }
+
+ public static IEnumerable<object[]> InvalidVersions
+ {
+ get
+ {
+ yield return new object[] { "" };
+ yield return new object[] { "http/1.1" };
+ yield return new object[] { "HTTP/a.1" };
+ yield return new object[] { "HTTP/1.a" };
+ yield return new object[] { "HTTP 1.1" };
+ yield return new object[] { "HTTP\t1.1" };
+ yield return new object[] { "HTTP 1 1" };
+ yield return new object[] { "\0" };
+ yield return new object[] { "HTTP\01.1" };
+ yield return new object[] { "HTTP/4294967295.4294967295" };
+ }
+ }
+
+ public static readonly string[] InvalidMethods = new string[]
+ {
+ "",
+ "G\tT",
+ "G E T",
+ "\0",
+ "G\0T",
+ "GET\n",
+ };
+
+ public static readonly string[] InvalidReasonPhrases = new string[]
+ {
+ "\0",
+ "\t",
+ "reason\n",
+ };
+
+ // This deliberately only checks for syntac boundaries of the URI, not its content
+ public static readonly string[] InvalidRequestUris = new string[]
+ {
+ "",
+ "p a t h",
+ "path ",
+ " path ",
+ };
+
+ public static readonly string[] InvalidStatusCodes = new string[]
+ {
+ "0",
+ "99",
+ "1a1",
+ "abc",
+ "1001",
+ "2000",
+ Int32.MinValue.ToString(),
+ Int32.MaxValue.ToString(),
+ };
+
+ public static readonly Dictionary<string, string> ValidHeaders = new Dictionary<string, string>
+ {
+ { "N0", "V0"},
+ { "N1", "V1"},
+ { "N2", "V2"},
+ { "N3", "V3"},
+ { "N4", "V4"},
+ { "N5", "V5"},
+ { "N6", "V6"},
+ { "N7", "V7"},
+ { "N8", "V8"},
+ { "N9", "V9"},
+ };
+
+ public static readonly string HttpMethod = "TEG";
+ public static readonly HttpStatusCode HttpStatus = HttpStatusCode.Created;
+ public static readonly string HttpReasonPhrase = "ReasonPhrase";
+ public static readonly string HttpHostName = "example.com";
+ public static readonly int HttpHostPort = 1234;
+ public static readonly string HttpMessageEntity = "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890";
+
+ public static readonly Uri HttpRequestUri = new Uri("http://" + HttpHostName + "/some/path");
+ public static readonly Uri HttpRequestUriWithPortAndQuery = new Uri("http://" + HttpHostName + ":" + HttpHostPort + "/some/path?%C3%A6%C3%B8%C3%A5");
+ public static readonly Uri HttpsRequestUri = new Uri("https://" + HttpHostName + "/some/path");
+
+ public static readonly string HttpRequest =
+ HttpMethod +
+ " /some/path HTTP/1.2\r\nHost: " +
+ HttpHostName +
+ "\r\nN1: V1a, V1b, V1c, V1d, V1e\r\nN2: V2\r\n\r\n";
+
+ public static readonly string HttpRequestWithHost =
+ HttpMethod +
+ " /some/path HTTP/1.2\r\n" +
+ "N1: V1a, V1b, V1c, V1d, V1e\r\nN2: V2\r\nHost: " +
+ HttpHostName + "\r\n\r\n";
+
+ public static readonly string HttpRequestWithPortAndQuery =
+ HttpMethod +
+ " /some/path?%C3%A6%C3%B8%C3%A5 HTTP/1.2\r\nHost: " +
+ HttpHostName + ":" + HttpHostPort.ToString() +
+ "\r\nN1: V1a, V1b, V1c, V1d, V1e\r\nN2: V2\r\n\r\n";
+
+ public static readonly string HttpResponse =
+ "HTTP/1.2 " +
+ ((int)HttpStatus).ToString() +
+ " " +
+ HttpReasonPhrase +
+ "\r\nN1: V1a, V1b, V1c, V1d, V1e\r\nN2: V2\r\n\r\n";
+
+ public static readonly string TextContentType = "text/plain; charset=utf-8";
+
+ public static readonly string HttpRequestWithEntity =
+ HttpMethod +
+ " /some/path HTTP/1.2\r\nHost: " +
+ HttpHostName +
+ "\r\nN1: V1a, V1b, V1c, V1d, V1e\r\nN2: V2\r\nContent-Type: " +
+ TextContentType +
+ "\r\n\r\n" +
+ HttpMessageEntity;
+
+ public static readonly string HttpResponseWithEntity =
+ "HTTP/1.2 " +
+ ((int)HttpStatus).ToString() +
+ " " +
+ HttpReasonPhrase +
+ "\r\nN1: V1a, V1b, V1c, V1d, V1e\r\nN2: V2\r\nContent-Type: " +
+ TextContentType +
+ "\r\n\r\n" +
+ HttpMessageEntity;
+
+ public static readonly MediaTypeHeaderValue HttpRequestMediaType;
+
+ public static readonly MediaTypeHeaderValue HttpResponseMediaType;
+
+ static ParserData()
+ {
+ MediaTypeHeaderValue.TryParse("application/http; msgtype=request", out HttpRequestMediaType);
+ MediaTypeHeaderValue.TryParse("application/http; msgtype=response", out HttpResponseMediaType);
+ }
+
+ }
+} \ No newline at end of file
diff --git a/test/System.Net.Http.Formatting.Test.Unit/Properties/AssemblyInfo.cs b/test/System.Net.Http.Formatting.Test.Unit/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..eba7e052
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/Properties/AssemblyInfo.cs
@@ -0,0 +1,21 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+using System.Resources;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+[assembly: AssemblyTitle("System.Net.Http.Formatting.Test.Unit")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("Microsoft")]
+[assembly: AssemblyProduct("System.Net.Http.Formatting.Test.Unit")]
+[assembly: AssemblyCopyright("Copyright © Microsoft 2011")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+[assembly: ComVisible(false)]
+[assembly: Guid("14258965-9ef5-4504-8655-51f948c96ffb")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
+[assembly: NeutralResourcesLanguage("en-US")]
+[assembly: InternalsVisibleTo("System.Net.Http.Formatting.Test.Integration, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
+[assembly: SuppressMessage("Microsoft.Design", "CA2210:AssembliesShouldHaveValidStrongNames", Justification = "These assemblies are delay-signed.")]
diff --git a/test/System.Net.Http.Formatting.Test.Unit/System.Net.Http.Formatting.Test.Unit.csproj b/test/System.Net.Http.Formatting.Test.Unit/System.Net.Http.Formatting.Test.Unit.csproj
new file mode 100644
index 00000000..ac6f0ca5
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/System.Net.Http.Formatting.Test.Unit.csproj
@@ -0,0 +1,156 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProductVersion>9.0.30729</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{7AF77741-9158-4D5F-8782-8F21FADF025F}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>System.Net.Http</RootNamespace>
+ <AssemblyName>System.Net.Http.Formatting.Test</AssemblyName>
+ <FileAlignment>512</FileAlignment>
+ <ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Debug\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Release\Test\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'CodeCoverage' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\CodeCoverage\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="Moq, Version=4.0.10827.0, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL">
+ <HintPath>..\..\packages\Moq.4.0.10827\lib\NET40\Moq.dll</HintPath>
+ </Reference>
+ <Reference Include="Newtonsoft.Json, Version=4.0.8.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
+ <HintPath>..\..\packages\Newtonsoft.Json.4.0.8\lib\net40\Newtonsoft.Json.dll</HintPath>
+ </Reference>
+ <Reference Include="System" />
+ <Reference Include="System.Data" />
+ <Reference Include="System.Net.Http">
+ <HintPath>..\..\packages\Microsoft.Net.Http.2.0.20302.1\lib\net40\System.Net.Http.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Net.Http.WebRequest">
+ <HintPath>..\..\packages\Microsoft.Net.Http.2.0.20302.1\lib\net40\System.Net.Http.WebRequest.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Runtime.Serialization" />
+ <Reference Include="System.XML" />
+ <Reference Include="xunit">
+ <HintPath>..\..\packages\xunit.1.9.0.1566\lib\xunit.dll</HintPath>
+ </Reference>
+ <Reference Include="xunit.extensions">
+ <HintPath>..\..\packages\xunit.extensions.1.9.0.1566\lib\xunit.extensions.dll</HintPath>
+ </Reference>
+ </ItemGroup>
+ <ItemGroup>
+ <CodeAnalysisDependentAssemblyPaths Condition=" '$(VS100COMNTOOLS)' != '' " Include="$(VS100COMNTOOLS)..\IDE\PrivateAssemblies">
+ <Visible>False</Visible>
+ </CodeAnalysisDependentAssemblyPaths>
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="DataSets\Types\DataContractEnum.cs" />
+ <Compile Include="DataSets\Types\DataContractType.cs" />
+ <Compile Include="DataSets\Types\DerivedDataContractType.cs" />
+ <Compile Include="DataSets\Types\DerivedFormUrlEncodedMediaTypeFormatter.cs" />
+ <Compile Include="DataSets\Types\DerivedJsonMediaTypeFormatter.cs" />
+ <Compile Include="DataSets\Types\DerivedWcfPocoType.cs" />
+ <Compile Include="DataSets\Types\DerivedXmlMediaTypeFormatter.cs" />
+ <Compile Include="DataSets\Types\DerivedXmlSerializableType.cs" />
+ <Compile Include="DataSets\Types\HttpTestData.cs" />
+ <Compile Include="DataSets\Types\INotJsonSerializable.cs" />
+ <Compile Include="DataSets\Types\WcfPocoType.cs" />
+ <Compile Include="DataSets\HttpUnitTestDataSets.cs" />
+ <Compile Include="DataSets\Types\XmlSerializableType.cs" />
+ <Compile Include="Formatting\FormDataCollectionTests.cs" />
+ <Compile Include="HttpClientExtensionsTest.cs" />
+ <Compile Include="HttpContentExtensionsTest.cs" />
+ <Compile Include="Mocks\TestableHttpMessageHandler.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="ContentDispositionHeaderValueExtensionsTests.cs" />
+ <Compile Include="Formatting\ThresholdStreamTest.cs" />
+ <Compile Include="UriQueryDataSet.cs" />
+ <Compile Include="UriQueryUtilityTests.cs" />
+ <Compile Include="FormattingUtilitiesTests.cs" />
+ <Compile Include="Formatting\BufferedMediaTypeFormatterTests.cs" />
+ <Compile Include="Formatting\DefaultContentNegotiatorTests.cs" />
+ <Compile Include="Formatting\FormUrlEncodedMediaTypeFormatterTests.cs" />
+ <Compile Include="Formatting\JsonKeyValueModelTest.cs" />
+ <Compile Include="Formatting\JsonMediaTypeFormatterTests.cs" />
+ <Compile Include="Formatting\MediaRangeMappingTests.cs" />
+ <Compile Include="Formatting\MediaTypeConstantsTests.cs" />
+ <Compile Include="Formatting\MediaTypeFormatterCollectionTests.cs" />
+ <Compile Include="Formatting\MediaTypeFormatterExtensionsTests.cs" />
+ <Compile Include="Formatting\MediaTypeFormatterTests.cs" />
+ <Compile Include="Formatting\MediaTypeHeaderValueEqualityComparerTests.cs" />
+ <Compile Include="Formatting\MediaTypeHeadeValueComparerTests.cs" />
+ <Compile Include="Formatting\MediaTypeHeadeValueExtensionsTests.cs" />
+ <Compile Include="Formatting\ParsedMediaTypeHeaderValueTests.cs" />
+ <Compile Include="Formatting\QueryStringMappingTests.cs" />
+ <Compile Include="Formatting\RequestHeaderMappingTests.cs" />
+ <Compile Include="Formatting\UriPathExtensionMappingTests.cs" />
+ <Compile Include="Formatting\XmlMediaTypeFormatterTests.cs" />
+ <Compile Include="Formatting\Parsers\FormUrlEncodedParserTests.cs" />
+ <Compile Include="HttpContentCollectionExtensionsTests.cs" />
+ <Compile Include="HttpContentMessageExtensionsTests.cs" />
+ <Compile Include="HttpContentMultipartExtensionsTests.cs" />
+ <Compile Include="HttpMessageContentTests.cs" />
+ <Compile Include="Formatting\Parsers\HttpRequestHeaderParserTests.cs" />
+ <Compile Include="Formatting\Parsers\HttpRequestLineParserTests.cs" />
+ <Compile Include="Formatting\Parsers\HttpResponseHeaderParserTests.cs" />
+ <Compile Include="Formatting\Parsers\HttpStatusLineParserTests.cs" />
+ <Compile Include="Formatting\Parsers\InternetMessageFormatHeaderParserTests.cs" />
+ <Compile Include="Formatting\Parsers\MimeMultipartParserTests.cs" />
+ <Compile Include="MultipartFileStreamProviderTests.cs" />
+ <Compile Include="MultipartFormDataStreamProviderTests.cs" />
+ <Compile Include="MultipartMemoryStreamProviderTests.cs" />
+ <Compile Include="ObjectContentOfTTests.cs" />
+ <Compile Include="ObjectContentTests.cs" />
+ <Compile Include="ParserData.cs" />
+ <Compile Include="UriExtensionsTests.cs" />
+ <Compile Include="Mocks\MockHttpContent.cs" />
+ <Compile Include="Mocks\MockMediaTypeFormatter.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\System.Json\System.Json.csproj">
+ <Project>{F0441BE9-BDC0-4629-BE5A-8765FFAA2481}</Project>
+ <Name>System.Json</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\src\System.Net.Http.Formatting\System.Net.Http.Formatting.csproj">
+ <Project>{668E9021-CE84-49D9-98FB-DF125A9FCDB0}</Project>
+ <Name>System.Net.Http.Formatting</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\Microsoft.TestCommon\Microsoft.TestCommon.csproj">
+ <Project>{FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0}</Project>
+ <Name>Microsoft.TestCommon</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="packages.config">
+ <SubType>Designer</SubType>
+ </None>
+ </ItemGroup>
+ <ItemGroup />
+ <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+</Project> \ No newline at end of file
diff --git a/test/System.Net.Http.Formatting.Test.Unit/UriExtensionsTests.cs b/test/System.Net.Http.Formatting.Test.Unit/UriExtensionsTests.cs
new file mode 100644
index 00000000..1d96873f
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/UriExtensionsTests.cs
@@ -0,0 +1,173 @@
+using System.Collections.Specialized;
+using System.Json;
+using System.Net.Http.Formatting.DataSets;
+using Microsoft.TestCommon;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Net.Http
+{
+ public class UriExtensionsTests
+ {
+ private static readonly Uri TestAddress = new Uri("http://www.example.com");
+ private static readonly Type TestType = typeof(string);
+
+ [Fact]
+ [Trait("Description", "UriExtensionMethods is public and static.")]
+ public void TypeIsCorrect()
+ {
+ Assert.Type.HasProperties(typeof(UriExtensions), TypeAssert.TypeProperties.IsPublicVisibleClass | TypeAssert.TypeProperties.IsStatic);
+ }
+
+ [Fact]
+ [Trait("Description", "ParseQueryString(Uri) throws with null 'this'.")]
+ public void ParseQueryStringThrowsWithNull()
+ {
+ Assert.ThrowsArgumentNull(() => ((Uri)null).ParseQueryString(), "address");
+ }
+
+ [Theory]
+ [TestDataSet(typeof(HttpUnitTestDataSets), "Uris")]
+ [Trait("Description", "ParseQueryString(Uri) succeeds with valid URIs.")]
+ public void ParseQueryStringSucceeds(Uri address)
+ {
+ NameValueCollection result = address.ParseQueryString();
+ Assert.NotNull(result);
+
+ bool addressContainsQuery = address.Query.Contains("?");
+ if (!addressContainsQuery)
+ {
+ Assert.Empty(result);
+ }
+ else
+ {
+ Assert.True(result.Count > 0, "Uri with query string should return non-empty set.");
+ }
+ }
+
+ [Fact]
+ [Trait("Description", "TryReadQueryAsJson(Uri, out JsonObject) throws with null 'this'.")]
+ public void TryReadQueryAsJsonThrowsWithNull()
+ {
+ JsonObject value;
+ Assert.ThrowsArgumentNull(() => ((Uri)null).TryReadQueryAsJson(out value), "address");
+ }
+
+ [Theory]
+ [TestDataSet(typeof(HttpUnitTestDataSets), "Uris")]
+ [Trait("Description", "TryReadQueryAsJson(Uri, out JsonObject) succeeds with valid URIs.")]
+ public void TryReadQueryAsJsonSucceeds(Uri address)
+ {
+ JsonObject value;
+ Assert.True(address.TryReadQueryAsJson(out value), "Expected 'true' as result");
+ Assert.NotNull(value);
+ Assert.IsType<JsonObject>(value);
+ }
+
+ [Fact]
+ [Trait("Description", "TryReadQueryAs(Uri, Type, out object) throws with null 'this'.")]
+ public void TryReadQueryAsThrowsWithNull()
+ {
+ object value;
+ Assert.ThrowsArgumentNull(() => ((Uri)null).TryReadQueryAs(TestType, out value), "address");
+ Assert.ThrowsArgumentNull(() => TestAddress.TryReadQueryAs(null, out value), "type");
+ }
+
+ [Fact]
+ [Trait("Description", "TryReadQueryAs(Uri, Type, out object) succeeds with valid URIs.")]
+ public void TryReadQueryAsSucceeds()
+ {
+ object value;
+ UriBuilder address = new UriBuilder("http://some.host");
+
+ address.Query = "a=2";
+ Assert.True(address.Uri.TryReadQueryAs(typeof(SimpleObject1), out value), "Expected 'true' reading valid data");
+ SimpleObject1 so1 = (SimpleObject1)value;
+ Assert.NotNull(so1);
+ Assert.Equal(2, so1.a);
+
+ address.Query = "b=true";
+ Assert.True(address.Uri.TryReadQueryAs(typeof(SimpleObject2), out value), "Expected 'true' reading valid data");
+ SimpleObject2 so2 = (SimpleObject2)value;
+ Assert.NotNull(so2);
+ Assert.True(so2.b, "Value should have been true");
+
+ address.Query = "c=hello";
+ Assert.True(address.Uri.TryReadQueryAs(typeof(SimpleObject3), out value), "Expected 'true' reading valid data");
+ SimpleObject3 so3 = (SimpleObject3)value;
+ Assert.NotNull(so3);
+ Assert.Equal("hello", so3.c);
+
+ address.Query = "c=";
+ Assert.True(address.Uri.TryReadQueryAs(typeof(SimpleObject3), out value), "Expected 'true' reading valid data");
+ so3 = (SimpleObject3)value;
+ Assert.NotNull(so3);
+ Assert.Equal("", so3.c);
+
+ address.Query = "c=null";
+ Assert.True(address.Uri.TryReadQueryAs(typeof(SimpleObject3), out value), "Expected 'true' reading valid data");
+ so3 = (SimpleObject3)value;
+ Assert.NotNull(so3);
+ Assert.Null(so3.c);
+ }
+
+ [Fact]
+ [Trait("Description", "TryReadQueryAs<T>(Uri, out T) throws with null 'this'.")]
+ public void TryReadQueryAsTThrowsWithNull()
+ {
+ object value;
+ Assert.ThrowsArgumentNull(() => ((Uri)null).TryReadQueryAs<object>(out value), "address");
+ }
+
+ [Fact]
+ [Trait("Description", "TryReadQueryAs<T>(Uri, out T) succeeds with valid URIs.")]
+ public void TryReadQueryAsTSucceeds()
+ {
+ UriBuilder address = new UriBuilder("http://some.host");
+ address.Query = "a=2";
+ SimpleObject1 so1;
+ Assert.True(address.Uri.TryReadQueryAs<SimpleObject1>(out so1), "Expected 'true' reading valid data");
+ Assert.NotNull(so1);
+ Assert.Equal(2, so1.a);
+
+ address.Query = "b=true";
+ SimpleObject2 so2;
+ Assert.True(address.Uri.TryReadQueryAs<SimpleObject2>(out so2), "Expected 'true' reading valid data");
+ Assert.NotNull(so2);
+ Assert.True(so2.b, "Value should have been true");
+
+ address.Query = "c=hello";
+ SimpleObject3 so3;
+ Assert.True(address.Uri.TryReadQueryAs<SimpleObject3>(out so3), "Expected 'true' reading valid data");
+ Assert.NotNull(so3);
+ Assert.Equal("hello", so3.c);
+
+ address.Query = "c=";
+ Assert.True(address.Uri.TryReadQueryAs<SimpleObject3>(out so3), "Expected 'true' reading valid data");
+ Assert.NotNull(so3);
+ Assert.Equal("", so3.c);
+
+ address.Query = "c=null";
+ Assert.True(address.Uri.TryReadQueryAs<SimpleObject3>(out so3), "Expected 'true' reading valid data");
+ Assert.NotNull(so3);
+ Assert.Null(so3.c);
+ }
+
+
+ public class SimpleObject1
+ {
+ public int a { get; set; }
+ }
+
+ public class SimpleObject2
+ {
+ public bool b { get; set; }
+ }
+
+ public class SimpleObject3
+ {
+ public string c { get; set; }
+ }
+ }
+}
diff --git a/test/System.Net.Http.Formatting.Test.Unit/UriQueryDataSet.cs b/test/System.Net.Http.Formatting.Test.Unit/UriQueryDataSet.cs
new file mode 100644
index 00000000..dc2b5519
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/UriQueryDataSet.cs
@@ -0,0 +1,31 @@
+using Microsoft.TestCommon;
+
+namespace System.Net.Http
+{
+ public class UriQueryTestData
+ {
+ public static TheoryDataSet<string, string, string> UriQueryData
+ {
+ get
+ {
+ return new TheoryDataSet<string, string, string>
+ {
+ { "=", "", "" },
+ { "N=", "N", "" },
+ { "=N", "", "N" },
+ { "N=V", "N", "V" },
+ { "%26=%26", "&", "&" },
+ { "%3D=%3D", "=", "=" },
+ { "N=A%2BC", "N", "A+C" },
+ { "N=100%25AA%21", "N", "100%AA!"},
+ { "N=%7E%21%40%23%24%25%5E%26%2A%28%29_%2B","N","~!@#$%^&*()_+"},
+ { "N=%601234567890-%3D", "N", "`1234567890-="},
+ { "N=%60%31%32%33%34%35%36%37%38%39%30%2D%3D","N", "`1234567890-="},
+ { "N=%E6%BF%80%E5%85%89%E9%80%99%E5%85%A9%E5%80%8B%E5%AD%97%E6%98%AF%E7%94%9A%E9%BA%BC%E6%84%8F%E6%80%9D", "N", "激光這兩個字是甚麼意思" },
+ { "N=%C3%A6%C3%B8%C3%A5","N","æøå"},
+ { "N=%C3%A6+%C3%B8+%C3%A5","N","æ ø å"},
+ };
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/test/System.Net.Http.Formatting.Test.Unit/UriQueryUtilityTests.cs b/test/System.Net.Http.Formatting.Test.Unit/UriQueryUtilityTests.cs
new file mode 100644
index 00000000..263839e3
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/UriQueryUtilityTests.cs
@@ -0,0 +1,160 @@
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Net.Http.Internal;
+using System.Text;
+using Microsoft.TestCommon;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Net.Http
+{
+ public class UriQueryUtilityTests
+ {
+ [Fact]
+ public void TypeIsCorrect()
+ {
+ Assert.Type.HasProperties(typeof(UriQueryUtility), TypeAssert.TypeProperties.IsClass | TypeAssert.TypeProperties.IsStatic);
+ }
+
+ #region ParseQueryString
+ [Fact]
+ public void ParseQueryStringThrowsOnNullQuery()
+ {
+ Assert.ThrowsArgumentNull(() => UriQueryUtility.ParseQueryString(null), "query");
+ }
+
+ #endregion
+
+ #region UrlEncode
+
+ [Fact]
+ public void UrlEncodeReturnsNull()
+ {
+ Assert.Null(UriQueryUtility.UrlEncode(null));
+ }
+
+ public void UrlEncodeToBytesThrowsOnInvalidArgs()
+ {
+ Assert.Null(UriQueryUtility.UrlEncodeToBytes(null, 0, 0));
+ Assert.ThrowsArgumentNull(() => UriQueryUtility.UrlEncodeToBytes(null, 0, 2), "bytes");
+
+ Assert.ThrowsArgumentOutOfRange(() => UriQueryUtility.UrlEncodeToBytes(new byte[0], -1, 0), "offset", null);
+ Assert.ThrowsArgumentOutOfRange(() => UriQueryUtility.UrlEncodeToBytes(new byte[0], 2, 0), "offset", null);
+
+ Assert.ThrowsArgumentOutOfRange(() => UriQueryUtility.UrlEncodeToBytes(new byte[0], 0, -1), "count", null);
+ Assert.ThrowsArgumentOutOfRange(() => UriQueryUtility.UrlEncodeToBytes(new byte[0], 0, 2), "count", null);
+ }
+
+ #endregion
+
+ #region UrlDecode
+
+ [Fact]
+ public void UrlDecodeReturnsNull()
+ {
+ Assert.Null(UriQueryUtility.UrlDecode(null));
+ }
+
+ [Fact]
+ public void UrlDecodeParsesEmptySegmentsCorrectly()
+ {
+ int iterations = 16;
+ List<string> segments = new List<string>();
+
+ for (int index = 1; index < iterations; index++)
+ {
+ segments.Add("&");
+ string query = string.Join("", segments);
+ NameValueCollection result = UriQueryUtility.ParseQueryString(query);
+ Assert.NotNull(result);
+
+ // Because this is a NameValueCollection, the same name appears only once
+ Assert.Equal(1, result.Count);
+
+ // Values should be a comma separated list of empty strings
+ string[] values = result[""].Split(new char[] { ',' });
+
+ // We expect length+1 segment as the final '&' counts as a segment
+ Assert.Equal(index + 1, values.Length);
+ foreach (var value in values)
+ {
+ Assert.Equal("", value);
+ }
+ }
+ }
+
+ public static TheoryDataSet<string, string, string> UriQueryData
+ {
+ get
+ {
+ return UriQueryTestData.UriQueryData;
+ }
+ }
+
+ private static string CreateQuery(params string[] segments)
+ {
+ StringBuilder buffer = new StringBuilder();
+ bool first = true;
+ foreach (string segment in segments)
+ {
+ if (first)
+ {
+ first = false;
+ }
+ else
+ {
+ buffer.Append('&');
+ }
+
+ buffer.Append(segment);
+ }
+
+ return buffer.ToString();
+ }
+
+ [Theory]
+ [InlineData("N", "", "N")]
+ [InlineData("%26", "", "&")]
+ [PropertyData("UriQueryData")]
+ public void UrlDecodeParsesCorrectly(string segment, string resultName, string resultValue)
+ {
+ int iterations = 16;
+ List<string> segments = new List<string>();
+
+ for (int index = 1; index < iterations; index++)
+ {
+ segments.Add(segment);
+ string query = CreateQuery(segments.ToArray());
+ NameValueCollection result = UriQueryUtility.ParseQueryString(query);
+ Assert.NotNull(result);
+
+ // Because this is a NameValueCollection, the same name appears only once
+ Assert.Equal(1, result.Count);
+
+ // Values should be a comma separated list of resultValue
+ string[] values = result[resultName].Split(new char[] { ',' });
+ Assert.Equal(index, values.Length);
+ foreach (var value in values)
+ {
+ Assert.Equal(resultValue, value);
+ }
+ }
+ }
+
+ public void UrlDecodeToBytesThrowsOnInvalidArgs()
+ {
+ Assert.Null(UriQueryUtility.UrlDecodeToBytes(null, 0, 0));
+ Assert.ThrowsArgumentNull(() => UriQueryUtility.UrlDecodeToBytes(null, 0, 2), "bytes");
+
+ Assert.ThrowsArgumentOutOfRange(() => UriQueryUtility.UrlDecodeToBytes(new byte[0], -1, 0), "offset", null);
+ Assert.ThrowsArgumentOutOfRange(() => UriQueryUtility.UrlDecodeToBytes(new byte[0], 2, 0), "offset", null);
+
+ Assert.ThrowsArgumentOutOfRange(() => UriQueryUtility.UrlDecodeToBytes(new byte[0], 0, -1), "count", null);
+ Assert.ThrowsArgumentOutOfRange(() => UriQueryUtility.UrlDecodeToBytes(new byte[0], 0, 2), "count", null);
+ }
+
+ #endregion
+
+ }
+} \ No newline at end of file
diff --git a/test/System.Net.Http.Formatting.Test.Unit/packages.config b/test/System.Net.Http.Formatting.Test.Unit/packages.config
new file mode 100644
index 00000000..01bb32d0
--- /dev/null
+++ b/test/System.Net.Http.Formatting.Test.Unit/packages.config
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="Microsoft.Net.Http" version="2.0.20302.1" />
+ <package id="Moq" version="4.0.10827" />
+ <package id="Newtonsoft.Json" version="4.0.8" />
+ <package id="xunit" version="1.9.0.1566" />
+ <package id="xunit.extensions" version="1.9.0.1566" />
+</packages> \ No newline at end of file
diff --git a/test/System.Web.Helpers.Test/ChartTest.cs b/test/System.Web.Helpers.Test/ChartTest.cs
new file mode 100644
index 00000000..7d782fed
--- /dev/null
+++ b/test/System.Web.Helpers.Test/ChartTest.cs
@@ -0,0 +1,653 @@
+using System.Collections;
+using System.Drawing;
+using System.IO;
+using System.Web.Hosting;
+using System.Web.UI.DataVisualization.Charting;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Helpers.Test
+{
+ public class ChartTest
+ {
+ private byte[] _writeData;
+
+ public ChartTest()
+ {
+ _writeData = null;
+ }
+
+ [Fact]
+ public void BuildChartAddsDefaultArea()
+ {
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 100, 100);
+ AssertBuiltChartAction(chart, c =>
+ {
+ Assert.Equal(1, c.ChartAreas.Count);
+ Assert.Equal("Default", c.ChartAreas[0].Name);
+ });
+ }
+
+ [Fact]
+ public void XAxisOverrides()
+ {
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 100, 100)
+ .SetXAxis("AxisX", 1, 100);
+ AssertBuiltChartAction(chart, c =>
+ {
+ Assert.Equal(1, c.ChartAreas.Count);
+ Assert.Equal("AxisX", c.ChartAreas[0].AxisX.Title);
+ Assert.Equal(1, c.ChartAreas[0].AxisX.Minimum);
+ Assert.Equal(100, c.ChartAreas[0].AxisX.Maximum);
+ });
+ }
+
+ [Fact]
+ public void YAxisOverrides()
+ {
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 100, 100)
+ .SetYAxis("AxisY", 1, 100);
+ AssertBuiltChartAction(chart, c =>
+ {
+ Assert.Equal(1, c.ChartAreas.Count);
+ Assert.Equal("AxisY", c.ChartAreas[0].AxisY.Title);
+ Assert.Equal(1, c.ChartAreas[0].AxisY.Minimum);
+ Assert.Equal(100, c.ChartAreas[0].AxisY.Maximum);
+ });
+ }
+
+ [Fact]
+ public void ConstructorLoadsTemplate()
+ {
+ var template = WriteTemplate(@"<Chart BorderWidth=""2""></Chart>");
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 100, 100, themePath: template);
+ AssertBuiltChartAction(chart, c => { Assert.Equal(2, c.BorderWidth); });
+ }
+
+ [Fact]
+ public void ConstructorLoadsTheme()
+ {
+ //Vanilla theme
+ /*
+ * <Chart Palette="SemiTransparent" BorderColor="#000" BorderWidth="2" BorderlineDashStyle="Solid">
+ <ChartAreas>
+ <ChartArea _Template_="All" Name="Default">
+ <AxisX>
+ <MinorGrid Enabled="False" />
+ <MajorGrid Enabled="False" />
+ </AxisX>
+ <AxisY>
+ <MajorGrid Enabled="False" />
+ <MinorGrid Enabled="False" />
+ </AxisY>
+ </ChartArea>
+ </ChartAreas>
+ </Chart>
+ */
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 100, 100, theme: ChartTheme.Vanilla);
+ AssertBuiltChartAction(chart, c =>
+ {
+ Assert.Equal(c.Palette, ChartColorPalette.SemiTransparent);
+ Assert.Equal(c.BorderColor, Color.FromArgb(0, Color.Black));
+ Assert.Equal(1, c.ChartAreas.Count);
+ Assert.False(c.ChartAreas[0].AxisX.MajorGrid.Enabled);
+ Assert.False(c.ChartAreas[0].AxisY.MinorGrid.Enabled);
+ });
+ }
+
+ [Fact]
+ public void ConstructorLoadsThemeAndTemplate()
+ {
+ //Vanilla theme
+ /*
+ * <Chart Palette="SemiTransparent" BorderColor="#000" BorderWidth="2" BorderlineDashStyle="Solid">
+ <ChartAreas>
+ <ChartArea _Template_="All" Name="Default">
+ <AxisX>
+ <MinorGrid Enabled="False" />
+ <MajorGrid Enabled="False" />
+ </AxisX>
+ <AxisY>
+ <MajorGrid Enabled="False" />
+ <MinorGrid Enabled="False" />
+ </AxisY>
+ </ChartArea>
+ </ChartAreas>
+ </Chart>
+ */
+ var template = WriteTemplate(@"<Chart BorderlineDashStyle=""DashDot""><Legends><Legend BackColor=""Red"" /></Legends></Chart>");
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 100, 100, theme: ChartTheme.Vanilla, themePath: template);
+ AssertBuiltChartAction(chart, c =>
+ {
+ Assert.Equal(c.Palette, ChartColorPalette.SemiTransparent);
+ Assert.Equal(c.BorderColor, Color.FromArgb(0, Color.Black));
+ Assert.Equal(c.BorderlineDashStyle, ChartDashStyle.DashDot);
+ Assert.Equal(1, c.ChartAreas.Count);
+ Assert.Equal(c.Legends.Count, 1);
+ Assert.Equal(c.Legends[0].BackColor, Color.Red);
+ Assert.False(c.ChartAreas[0].AxisX.MajorGrid.Enabled);
+ Assert.False(c.ChartAreas[0].AxisY.MinorGrid.Enabled);
+ });
+ }
+
+ [Fact]
+ public void ConstructorSetsWidthAndHeight()
+ {
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 101, 102);
+ Assert.Equal(101, chart.Width);
+ Assert.Equal(102, chart.Height);
+ AssertBuiltChartAction(chart, c =>
+ {
+ Assert.Equal(101, c.Width);
+ Assert.Equal(102, c.Height);
+ });
+ }
+
+ [Fact]
+ public void ConstructorThrowsWhenHeightIsLessThanZero()
+ {
+ Assert.ThrowsArgumentOutOfRange(() => { new Chart(GetContext(), GetVirtualPathProvider(), 100, -1); }, "height", "Value must be greater than or equal to 0.");
+ }
+
+ [Fact]
+ public void ConstructorThrowsWhenTemplateNotFound()
+ {
+ var templateFile = @"FileNotFound.xml";
+ Assert.ThrowsArgument(() => { new Chart(GetContext(), GetVirtualPathProvider(), 100, 100, themePath: templateFile); },
+ "themePath",
+ String.Format("The theme file \"{0}\" could not be found.", VirtualPathUtility.Combine(GetContext().Request.AppRelativeCurrentExecutionFilePath, templateFile)));
+ }
+
+ [Fact]
+ public void ConstructorThrowsWhenWidthIsLessThanZero()
+ {
+ Assert.ThrowsArgumentOutOfRange(() => { new Chart(GetContext(), GetVirtualPathProvider(), -1, 100); }, "width", "Value must be greater than or equal to 0.");
+ }
+
+ [Fact]
+ public void DataBindCrossTable()
+ {
+ var data = new[]
+ {
+ new { GroupBy = "1", YValue = 1 },
+ new { GroupBy = "1", YValue = 2 },
+ new { GroupBy = "2", YValue = 1 }
+ };
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 100, 100)
+ .DataBindCrossTable(data, "GroupBy", xField: null, yFields: "YValue");
+ // todo - anything else to verify here?
+ AssertBuiltChartAction(chart, c =>
+ {
+ Assert.Equal(2, c.Series.Count);
+ Assert.Equal(2, c.Series[0].Points.Count);
+ Assert.Equal(1, c.Series[1].Points.Count);
+ });
+ }
+
+ [Fact]
+ public void DataBindCrossTableThrowsWhenDataSourceIsNull()
+ {
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 100, 100);
+ Assert.ThrowsArgumentNull(() => { chart.DataBindCrossTable(null, "GroupBy", xField: null, yFields: "yFields"); }, "dataSource");
+ }
+
+ [Fact]
+ public void DataBindCrossTableThrowsWhenDataSourceIsString()
+ {
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 100, 100);
+ Assert.ThrowsArgument(() => { chart.DataBindCrossTable("DataSource", "GroupBy", xField: null, yFields: "yFields"); }, "dataSource", "A series cannot be data-bound to a string object.");
+ }
+
+ [Fact]
+ public void DataBindCrossTableThrowsWhenGroupByIsNull()
+ {
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 100, 100);
+ Assert.ThrowsArgumentNullOrEmptyString(() => { chart.DataBindCrossTable(new object[0], null, xField: null, yFields: "yFields"); }, "groupByField");
+ }
+
+ [Fact]
+ public void DataBindCrossTableThrowsWhenGroupByIsEmpty()
+ {
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 100, 100);
+ Assert.ThrowsArgumentNullOrEmptyString(() => { chart.DataBindCrossTable(new object[0], "", xField: null, yFields: "yFields"); }, "groupByField");
+ }
+
+ [Fact]
+ public void DataBindCrossTableThrowsWhenYFieldsIsNull()
+ {
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 100, 100);
+ Assert.ThrowsArgumentNullOrEmptyString(() => { chart.DataBindCrossTable(new object[0], "GroupBy", xField: null, yFields: null); }, "yFields");
+ }
+
+ [Fact]
+ public void DataBindCrossTableThrowsWhenYFieldsIsEmpty()
+ {
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 100, 100);
+ Assert.ThrowsArgumentNullOrEmptyString(() => { chart.DataBindCrossTable(new object[0], "GroupBy", xField: null, yFields: ""); }, "yFields");
+ }
+
+ [Fact]
+ public void DataBindTable()
+ {
+ var data = new[]
+ {
+ new { XValue = "1", YValue = 1 },
+ new { XValue = "2", YValue = 2 },
+ new { XValue = "3", YValue = 3 }
+ };
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 100, 100)
+ .DataBindTable(data, xField: "XValue");
+ // todo - anything else to verify here?
+ AssertBuiltChartAction(chart, c =>
+ {
+ Assert.Equal(1, c.Series.Count);
+ Assert.Equal(3, c.Series[0].Points.Count);
+ });
+ }
+
+ [Fact]
+ public void DataBindTableWhenXFieldIsNull()
+ {
+ var data = new[]
+ {
+ new { YValue = 1 },
+ new { YValue = 2 },
+ new { YValue = 3 }
+ };
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 100, 100)
+ .DataBindTable(data, xField: null);
+ // todo - anything else to verify here?
+ AssertBuiltChartAction(chart, c =>
+ {
+ Assert.Equal(1, c.Series.Count);
+ Assert.Equal(3, c.Series[0].Points.Count);
+ });
+ }
+
+ [Fact]
+ public void DataBindTableThrowsWhenDataSourceIsNull()
+ {
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 100, 100);
+ Assert.ThrowsArgumentNull(() => { chart.DataBindTable(null); }, "dataSource");
+ }
+
+ [Fact]
+ public void DataBindTableThrowsWhenDataSourceIsString()
+ {
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 100, 100);
+ Assert.ThrowsArgument(() => { chart.DataBindTable(""); }, "dataSource", "A series cannot be data-bound to a string object.");
+ }
+
+ [Fact]
+ public void GetBytesReturnsNonEmptyArray()
+ {
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 100, 100);
+ Assert.True(chart.GetBytes().Length > 0);
+ }
+
+ [Fact]
+ public void GetBytesThrowsWhenFormatIsEmpty()
+ {
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 100, 100);
+ Assert.ThrowsArgumentNullOrEmptyString(() => { chart.GetBytes(format: String.Empty); }, "format");
+ }
+
+ [Fact]
+ public void GetBytesThrowsWhenFormatIsInvalid()
+ {
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 100, 100);
+ Assert.ThrowsArgument(() => { chart.GetBytes(format: "foo"); }, "format", "\"foo\" is invalid image format. Valid values are image format names like: \"JPEG\", \"BMP\", \"GIF\", \"PNG\", etc.");
+ }
+
+ [Fact]
+ public void GetBytesThrowsWhenFormatIsNull()
+ {
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 100, 100);
+ Assert.ThrowsArgumentNullOrEmptyString(() => { chart.GetBytes(format: null); }, "format");
+ }
+
+ [Fact]
+ public void LegendDefaults()
+ {
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 100, 100).AddLegend();
+ AssertBuiltChartAction(chart, c =>
+ {
+ Assert.Equal(1, c.Legends.Count);
+ // NOTE: Chart.Legends.Add will create default name
+ Assert.Equal("Legend1", c.Legends[0].Name);
+ Assert.Equal(1, c.Legends[0].BorderWidth);
+ });
+ }
+
+ [Fact]
+ public void LegendOverrides()
+ {
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 100, 100).AddLegend("Legend1")
+ .AddLegend("Legend2", "Legend2Name");
+ AssertBuiltChartAction(chart, c =>
+ {
+ Assert.Equal(2, c.Legends.Count);
+ Assert.Equal("Legend1", c.Legends[0].Name);
+ Assert.Equal("Legend2", c.Legends[1].Title);
+ Assert.Equal("Legend2Name", c.Legends[1].Name);
+ });
+ }
+
+ [Fact]
+ public void SaveAndWriteFromCache()
+ {
+ var context1 = GetContext();
+ var chart = new Chart(context1, GetVirtualPathProvider(), 100, 100);
+
+ string key = chart.SaveToCache();
+ Assert.Equal(chart, WebCache.Get(key));
+
+ var context2 = GetContext();
+ Assert.Equal(chart, Chart.GetFromCache(context2, key));
+
+ Chart.WriteFromCache(context2, key);
+
+ Assert.Null(context1.Response.ContentType);
+ Assert.Equal("image/jpeg", context2.Response.ContentType);
+ }
+
+ [Fact]
+ public void SaveThrowsWhenFormatIsEmpty()
+ {
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 100, 100);
+ Assert.ThrowsArgumentNullOrEmptyString(() => { chart.Save(GetContext(), "chartPath", format: String.Empty); }, "format");
+ }
+
+ [Fact]
+ public void SaveWorksWhenFormatIsJPG()
+ {
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 100, 100);
+
+ string fileName = "chartPath";
+
+ chart.Save(GetContext(), "chartPath", format: "jpg");
+ byte[] a = File.ReadAllBytes(fileName);
+
+ chart.Save(GetContext(), "chartPath", format: "jpeg");
+ byte[] b = File.ReadAllBytes(fileName);
+
+ Assert.Equal(a, b);
+ }
+
+ [Fact]
+ public void SaveThrowsWhenFormatIsInvalid()
+ {
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 100, 100);
+ Assert.ThrowsArgument(() => { chart.Save(GetContext(), "chartPath", format: "foo"); }, "format", "\"foo\" is invalid image format. Valid values are image format names like: \"JPEG\", \"BMP\", \"GIF\", \"PNG\", etc.");
+ }
+
+ [Fact]
+ public void SaveThrowsWhenFormatIsNull()
+ {
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 100, 100);
+ Assert.ThrowsArgumentNullOrEmptyString(() => { chart.Save(GetContext(), "chartPath", format: null); }, "format");
+ }
+
+ [Fact]
+ public void SaveThrowsWhenPathIsEmpty()
+ {
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 100, 100);
+ Assert.ThrowsArgumentNullOrEmptyString(() => { chart.Save(GetContext(), path: String.Empty, format: "jpeg"); }, "path");
+ }
+
+ [Fact]
+ public void SaveThrowsWhenPathIsNull()
+ {
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 100, 100);
+ Assert.ThrowsArgumentNullOrEmptyString(() => { chart.Save(GetContext(), path: null, format: "jpeg"); }, "path");
+ }
+
+ [Fact]
+ public void SaveWritesToFile()
+ {
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 100, 100);
+ chart.Save(GetContext(), "SaveWritesToFile.jpg", format: "image/jpeg");
+ Assert.Equal("SaveWritesToFile.jpg", Path.GetFileName(chart.FileName));
+ Assert.True(File.Exists(chart.FileName));
+ }
+
+ [Fact]
+ public void SaveXmlThrowsWhenPathIsEmpty()
+ {
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 100, 100);
+ Assert.ThrowsArgumentNullOrEmptyString(() => { chart.SaveXml(GetContext(), String.Empty); }, "path");
+ }
+
+ [Fact]
+ public void SaveXmlThrowsWhenPathIsNull()
+ {
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 100, 100);
+ Assert.ThrowsArgumentNullOrEmptyString(() => { chart.SaveXml(GetContext(), null); }, "path");
+ }
+
+ [Fact]
+ public void SaveXmlWritesToFile()
+ {
+ var template = WriteTemplate(@"<Chart BorderWidth=""2""></Chart>");
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 100, 100, themePath: template);
+ chart.SaveXml(GetContext(), "SaveXmlWritesToFile.xml");
+ Assert.True(File.Exists("SaveXmlWritesToFile.xml"));
+ string result = File.ReadAllText("SaveXmlWritesToFile.xml");
+ Assert.True(result.Contains("BorderWidth=\"2\""));
+ }
+
+ [Fact]
+ public void TemplateWithCommentsDoesNotThrow()
+ {
+ var template = WriteTemplate(@"<Chart BorderWidth=""2""><!-- This is a XML comment. --> </Chart>");
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 100, 100, themePath: template);
+ Assert.NotNull(chart.ToWebImage());
+ }
+
+ [Fact]
+ public void TemplateWithIncorrectPropertiesThrows()
+ {
+ var template = WriteTemplate(@"<Chart borderWidth=""2""><fjkjkgjklfg /></Chart>");
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 100, 100, themePath: template);
+ Assert.Throws<InvalidOperationException>(() => chart.ToWebImage(),
+ "Cannot deserialize property. Unknown property name 'borderWidth' in object \" System.Web.UI.DataVisualization.Charting.Chart");
+ }
+
+ [Fact]
+ public void WriteWorksWithJPGFormat()
+ {
+ var response = new Mock<HttpResponseBase>();
+ var stream = new MemoryStream();
+ response.Setup(c => c.Output).Returns(new StreamWriter(stream));
+
+ var context = new Mock<HttpContextBase>();
+ context.Setup(c => c.Response).Returns(response.Object);
+
+ var chart = new Chart(context.Object, GetVirtualPathProvider(), 100, 100);
+ chart.Write("jpeg");
+
+ byte[] a = stream.GetBuffer();
+
+ stream.SetLength(0);
+ chart.Write("jpg");
+ byte[] b = stream.GetBuffer();
+
+ Assert.Equal(a, b);
+ }
+
+ [Fact]
+ public void WriteThrowsWithInvalidFormat()
+ {
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 100, 100);
+ Assert.ThrowsArgument(() => chart.Write("foo"),
+ "format", "\"foo\" is invalid image format. Valid values are image format names like: \"JPEG\", \"BMP\", \"GIF\", \"PNG\", etc.");
+ }
+
+ [Fact]
+ public void SeriesOverrides()
+ {
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 100, 100)
+ .AddSeries(chartType: "Bar");
+ AssertBuiltChartAction(chart, c =>
+ {
+ Assert.Equal(1, c.Series.Count);
+ Assert.Equal(SeriesChartType.Bar, c.Series[0].ChartType);
+ });
+ }
+
+ [Fact]
+ public void SeriesThrowsWhenChartTypeIsEmpty()
+ {
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 100, 100);
+ Assert.ThrowsArgumentNullOrEmptyString(() => { chart.AddSeries(chartType: ""); }, "chartType");
+ }
+
+ [Fact]
+ public void SeriesThrowsWhenChartTypeIsNull()
+ {
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 100, 100);
+ Assert.ThrowsArgumentNullOrEmptyString(() => { chart.AddSeries(chartType: null); }, "chartType");
+ }
+
+ [Fact]
+ public void TitleDefaults()
+ {
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 100, 100).AddTitle();
+ AssertBuiltChartAction(chart, c =>
+ {
+ Assert.Equal(1, c.Titles.Count);
+ // NOTE: Chart.Titles.Add will create default name
+ Assert.Equal("Title1", c.Titles[0].Name);
+ Assert.Equal(String.Empty, c.Titles[0].Text);
+ Assert.Equal(1, c.Titles[0].BorderWidth);
+ });
+ }
+
+ [Fact]
+ public void TitleOverrides()
+ {
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 100, 100).AddTitle(name: "Title1")
+ .AddTitle("Title2Text", name: "Title2");
+ AssertBuiltChartAction(chart, c =>
+ {
+ Assert.Equal(2, c.Titles.Count);
+ Assert.Equal("Title1", c.Titles[0].Name);
+ Assert.Equal("Title2", c.Titles[1].Name);
+ Assert.Equal("Title2Text", c.Titles[1].Text);
+ });
+ }
+
+ [Fact]
+ public void ToWebImage()
+ {
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 100, 100);
+ var image = chart.ToWebImage();
+ Assert.NotNull(image);
+ Assert.Equal("jpeg", image.ImageFormat);
+ }
+
+ [Fact]
+ public void ToWebImageUsesFormat()
+ {
+ var chart = new Chart(GetContext(), GetVirtualPathProvider(), 100, 100);
+ var image = chart.ToWebImage(format: "png");
+ Assert.NotNull(image);
+ Assert.Equal("png", image.ImageFormat);
+ }
+
+ [Fact]
+ public void WriteFromCacheIsNoOpIfNotSavedInCache()
+ {
+ var context = GetContext();
+ Assert.Null(Chart.WriteFromCache(context, Guid.NewGuid().ToString()));
+ Assert.Null(context.Response.ContentType);
+ }
+
+ [Fact]
+ public void WriteUpdatesResponse()
+ {
+ var context = GetContext();
+ var chart = new Chart(context, GetVirtualPathProvider(), 100, 100);
+ chart.Write();
+ Assert.Equal("", context.Response.Charset);
+ Assert.Equal("image/jpeg", context.Response.ContentType);
+ Assert.True((_writeData != null) && (_writeData.Length > 0));
+ }
+
+ private void AssertBuiltChartAction(Chart chart, Action<UI.DataVisualization.Charting.Chart> action)
+ {
+ bool actionCalled = false;
+ chart.ExecuteChartAction(c =>
+ {
+ action(c);
+ actionCalled = true;
+ });
+ Assert.True(actionCalled);
+ }
+
+ private HttpContextBase GetContext()
+ {
+ // Strip drive letter for VirtualPathUtility.Combine
+ var testPath = Directory.GetCurrentDirectory().Substring(2) + "/Out";
+ Mock<HttpRequestBase> request = new Mock<HttpRequestBase>();
+ request.Setup(r => r.AppRelativeCurrentExecutionFilePath).Returns(testPath);
+ request.Setup(r => r.MapPath(It.IsAny<string>())).Returns((string path) => path);
+
+ Mock<HttpResponseBase> response = new Mock<HttpResponseBase>();
+ response.SetupProperty(r => r.ContentType);
+ response.SetupProperty(r => r.Charset);
+ response.Setup(r => r.BinaryWrite(It.IsAny<byte[]>())).Callback((byte[] data) => _writeData = data);
+
+ Mock<HttpServerUtilityBase> server = new Mock<HttpServerUtilityBase>();
+ server.Setup(s => s.MapPath(It.IsAny<string>())).Returns((string s) => s);
+
+ var items = new Hashtable();
+
+ Mock<HttpContextBase> context = new Mock<HttpContextBase>();
+ context.Setup(c => c.Request).Returns(request.Object);
+ context.Setup(c => c.Response).Returns(response.Object);
+ context.Setup(c => c.Server).Returns(server.Object);
+ context.Setup(c => c.Items).Returns(items);
+ return context.Object;
+ }
+
+ private string WriteTemplate(string xml)
+ {
+ var path = Guid.NewGuid() + ".xml";
+ File.WriteAllText(path, xml);
+ return path;
+ }
+
+ private MockVirtualPathProvider GetVirtualPathProvider()
+ {
+ return new MockVirtualPathProvider();
+ }
+
+ class MockVirtualPathProvider : VirtualPathProvider
+ {
+ class MockVirtualFile : VirtualFile
+ {
+ public MockVirtualFile(string virtualPath)
+ : base(virtualPath)
+ {
+ }
+
+ public override Stream Open()
+ {
+ return File.Open(this.VirtualPath, FileMode.Open);
+ }
+ }
+
+ public override bool FileExists(string virtualPath)
+ {
+ return File.Exists(virtualPath);
+ }
+
+ public override VirtualFile GetFile(string virtualPath)
+ {
+ return new MockVirtualFile(virtualPath);
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Helpers.Test/ConversionUtilTest.cs b/test/System.Web.Helpers.Test/ConversionUtilTest.cs
new file mode 100644
index 00000000..e856261f
--- /dev/null
+++ b/test/System.Web.Helpers.Test/ConversionUtilTest.cs
@@ -0,0 +1,76 @@
+using System.Drawing;
+using System.Globalization;
+using Xunit;
+
+namespace System.Web.Helpers.Test
+{
+ public class ConversionUtilTest
+ {
+ [Fact]
+ public void ConversionUtilReturnsStringTypes()
+ {
+ // Arrange
+ string original = "Foo";
+
+ // Act
+ object result;
+ bool success = ConversionUtil.TryFromString(typeof(String), original, out result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal(original, result);
+ }
+
+ [Fact]
+ public void ConversionUtilConvertsStringsToColor()
+ {
+ // Arrange
+ string original = "Blue";
+
+ // Act
+ object result;
+ bool success = ConversionUtil.TryFromString(typeof(Color), original, out result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal(Color.Blue, result);
+ }
+
+ [Fact]
+ public void ConversionUtilConvertsEnumValues()
+ {
+ // Arrange
+ string original = "Weekday";
+
+ // Act
+ object result;
+ bool success = ConversionUtil.TryFromString(typeof(TestEnum), original, out result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal(TestEnum.Weekday, result);
+ }
+
+ [Fact]
+ public void ConversionUtilUsesTypeConverterToConvertArbitraryTypes()
+ {
+ // Arrange
+ var date = new DateTime(2010, 01, 01);
+ string original = date.ToString(CultureInfo.InvariantCulture);
+
+ // Act
+ object result;
+ bool success = ConversionUtil.TryFromString(typeof(DateTime), original, out result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal(date, result);
+ }
+
+ private enum TestEnum
+ {
+ Weekend,
+ Weekday
+ }
+ }
+}
diff --git a/test/System.Web.Helpers.Test/CryptoTest.cs b/test/System.Web.Helpers.Test/CryptoTest.cs
new file mode 100644
index 00000000..89b469fe
--- /dev/null
+++ b/test/System.Web.Helpers.Test/CryptoTest.cs
@@ -0,0 +1,170 @@
+using System.IO;
+using System.Security.Cryptography;
+using System.Text;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Helpers.Test
+{
+ /// <summary>
+ ///This is a test class for CryptoTest and is intended
+ ///to contain all CryptoTest Unit Tests
+ ///</summary>
+ public class CryptoTest
+ {
+ [Fact]
+ public void SHA256HashTest_ReturnsValidData()
+ {
+ string data = "foo bar";
+ string expected = "FBC1A9F858EA9E177916964BD88C3D37B91A1E84412765E29950777F265C4B75";
+ string actual;
+ actual = Crypto.SHA256(data);
+ Assert.Equal(expected, actual);
+
+ actual = Crypto.Hash(Encoding.UTF8.GetBytes(data));
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void GenerateSaltTest()
+ {
+ string salt = Crypto.GenerateSalt();
+ salt = Crypto.GenerateSalt(64);
+ Assert.Equal(24, Crypto.GenerateSalt().Length);
+ Assert.Equal(12, Crypto.GenerateSalt(8).Length);
+ Assert.Equal(88, Crypto.GenerateSalt(64).Length);
+ Assert.Equal(44, Crypto.GenerateSalt(32).Length);
+ }
+
+ [Fact]
+ public void HashPassword_PasswordGeneration()
+ {
+ // Act - call helper directly
+ string generatedHash = Crypto.HashPassword("my-password");
+ byte[] salt = new byte[16];
+ Buffer.BlockCopy(Convert.FromBase64String(generatedHash), 1, salt, 0, 16); // extract salt from generated hash
+
+ // Act - perform PBKDF2 directly
+ string generatedHash2;
+ using (var ms = new MemoryStream())
+ {
+ using (var bw = new BinaryWriter(ms))
+ {
+ using (var deriveBytes = new Rfc2898DeriveBytes("my-password", salt, iterations: 1000))
+ {
+ bw.Write((byte)0x00); // version identifier
+ bw.Write(salt); // salt
+ bw.Write(deriveBytes.GetBytes(32)); // subkey
+ }
+
+ generatedHash2 = Convert.ToBase64String(ms.ToArray());
+ }
+ }
+
+ // Assert
+ Assert.Equal(generatedHash2, generatedHash);
+ }
+
+ [Fact]
+ public void HashPassword_RoundTripping()
+ {
+ // Act & assert
+ string password = "ImPepper";
+ Assert.True(Crypto.VerifyHashedPassword(Crypto.HashPassword(password), password));
+ Assert.False(Crypto.VerifyHashedPassword(Crypto.HashPassword(password), "ImSalt"));
+ Assert.False(Crypto.VerifyHashedPassword(Crypto.HashPassword("Impepper"), password));
+ }
+
+ [Fact]
+ public void VerifyHashedPassword_CorrectPassword_ReturnsTrue()
+ {
+ // Arrange
+ string hashedPassword = "ALyuoraY/cIWD1hjo+K81/pf83qo6Q6T+UBYcXN9P3A9WHLvEY10f+lwW5qPG6h9xw=="; // this is for 'my-password'
+
+ // Act
+ bool retVal = Crypto.VerifyHashedPassword(hashedPassword, "my-password");
+
+ // Assert
+ Assert.True(retVal);
+ }
+
+ [Fact]
+ public void VerifyHashedPassword_IncorrectPassword_ReturnsFalse()
+ {
+ // Arrange
+ string hashedPassword = "ALyuoraY/cIWD1hjo+K81/pf83qo6Q6T+UBYcXN9P3A9WHLvEY10f+lwW5qPG6h9xw=="; // this is for 'my-password'
+
+ // Act
+ bool retVal = Crypto.VerifyHashedPassword(hashedPassword, "some-other-password");
+
+ // Assert
+ Assert.False(retVal);
+ }
+
+ [Fact]
+ public void VerifyHashedPassword_InvalidPasswordHash_ReturnsFalse()
+ {
+ // Arrange
+ string hashedPassword = "AAECAw=="; // this is an invalid password hash
+
+ // Act
+ bool retVal = Crypto.VerifyHashedPassword(hashedPassword, "hello-world");
+
+ // Assert
+ Assert.False(retVal);
+ }
+
+ [Fact]
+ public void MD5HashTest_ReturnsValidData()
+ {
+ string data = "foo bar";
+ string expected = "327B6F07435811239BC47E1544353273";
+ string actual;
+ actual = Crypto.Hash(data, algorithm: "md5");
+ Assert.Equal(expected, actual);
+
+ actual = Crypto.Hash(Encoding.UTF8.GetBytes(data), algorithm: "MD5");
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void SHA1HashTest_ReturnsValidData()
+ {
+ string data = "foo bar";
+ string expected = "3773DEA65156909838FA6C22825CAFE090FF8030";
+ string actual;
+ actual = Crypto.SHA1(data);
+ Assert.Equal(expected, actual);
+
+ actual = Crypto.Hash(Encoding.UTF8.GetBytes(data), algorithm: "sha1");
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void SHA1HashTest_WithNull_ThrowsException()
+ {
+ Assert.Throws<ArgumentNullException>(() => Crypto.SHA1((string)null));
+ Assert.Throws<ArgumentNullException>(() => Crypto.Hash((byte[])null, algorithm: "SHa1"));
+ }
+
+ [Fact]
+ public void SHA256HashTest_WithNull_ThrowsException()
+ {
+ Assert.Throws<ArgumentNullException>(() => Crypto.SHA256((string)null));
+ Assert.Throws<ArgumentNullException>(() => Crypto.Hash((byte[])null, algorithm: "sHa256"));
+ }
+
+ [Fact]
+ public void MD5HashTest_WithNull_ThrowsException()
+ {
+ Assert.Throws<ArgumentNullException>(() => Crypto.Hash((string)null, algorithm: "mD5"));
+ Assert.Throws<ArgumentNullException>(() => Crypto.Hash((byte[])null, algorithm: "mD5"));
+ }
+
+ [Fact]
+ public void HashWithUnknownAlg_ThrowsException()
+ {
+ Assert.Throws<InvalidOperationException>(() => Crypto.Hash("sdflksd", algorithm: "hao"), "The hash algorithm 'hao' is not supported, valid values are: sha256, sha1, md5");
+ }
+ }
+}
diff --git a/test/System.Web.Helpers.Test/DynamicDictionary.cs b/test/System.Web.Helpers.Test/DynamicDictionary.cs
new file mode 100644
index 00000000..cdbf598b
--- /dev/null
+++ b/test/System.Web.Helpers.Test/DynamicDictionary.cs
@@ -0,0 +1,103 @@
+using System.Collections.Generic;
+using System.Dynamic;
+using System.Linq.Expressions;
+using System.Reflection;
+
+namespace System.Web.Helpers.Test
+{
+ /// <summary>
+ /// Dynamic object implementation over a dictionary that doesn't implement anything but the interface.
+ /// Used for testing our types that consume dynamic objects to make sure they don't make any assumptions on the implementation.
+ /// </summary>
+ public class DynamicDictionary : IDynamicMetaObjectProvider
+ {
+ private readonly Dictionary<string, object> _values = new Dictionary<string, object>();
+
+ public object this[string name]
+ {
+ get
+ {
+ object result;
+ _values.TryGetValue(name, out result);
+ return result;
+ }
+ set { _values[name] = value; }
+ }
+
+ public DynamicMetaObject GetMetaObject(Expression parameter)
+ {
+ return new DynamicDictionaryMetaObject(parameter, this);
+ }
+
+ private class DynamicDictionaryMetaObject : DynamicMetaObject
+ {
+ private static readonly PropertyInfo ItemPropery = typeof(DynamicDictionary).GetProperty("Item");
+
+ public DynamicDictionaryMetaObject(Expression expression, object value)
+ : base(expression, BindingRestrictions.Empty, value)
+ {
+ }
+
+ private IDictionary<string, object> WrappedDictionary
+ {
+ get { return ((DynamicDictionary)Value)._values; }
+ }
+
+ private Expression GetDynamicExpression()
+ {
+ return Expression.Convert(Expression, typeof(DynamicDictionary));
+ }
+
+ private Expression GetIndexExpression(string key)
+ {
+ return Expression.MakeIndex(
+ GetDynamicExpression(),
+ ItemPropery,
+ new[]
+ {
+ Expression.Constant(key)
+ }
+ );
+ }
+
+ private Expression GetSetValueExpression(string key, object value)
+ {
+ return Expression.Assign(
+ GetIndexExpression(key),
+ Expression.Convert(Expression.Constant(value),
+ typeof(object))
+ );
+ }
+
+ public override DynamicMetaObject BindGetMember(GetMemberBinder binder)
+ {
+ var binderDefault = binder.FallbackGetMember(this);
+
+ var expression = Expression.Convert(GetIndexExpression(binder.Name),
+ typeof(object));
+
+ var dynamicSuggestion = new DynamicMetaObject(expression, BindingRestrictions.GetTypeRestriction(Expression, LimitType)
+ .Merge(binderDefault.Restrictions));
+
+ return binder.FallbackGetMember(this, dynamicSuggestion);
+ }
+
+ public override DynamicMetaObject BindSetMember(SetMemberBinder binder, DynamicMetaObject value)
+ {
+ var binderDefault = binder.FallbackSetMember(this, value);
+
+ Expression expression = GetSetValueExpression(binder.Name, value.Value);
+
+ var dynamicSuggestion = new DynamicMetaObject(expression, BindingRestrictions.GetTypeRestriction(Expression, LimitType)
+ .Merge(binderDefault.Restrictions));
+
+ return binder.FallbackSetMember(this, value, dynamicSuggestion);
+ }
+
+ public override IEnumerable<string> GetDynamicMemberNames()
+ {
+ return WrappedDictionary.Keys;
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Helpers.Test/DynamicHelperTest.cs b/test/System.Web.Helpers.Test/DynamicHelperTest.cs
new file mode 100644
index 00000000..c1851502
--- /dev/null
+++ b/test/System.Web.Helpers.Test/DynamicHelperTest.cs
@@ -0,0 +1,38 @@
+using System.Dynamic;
+using System.Web.WebPages.TestUtils;
+using Microsoft.Internal.Web.Utils;
+using Xunit;
+
+namespace System.Web.Helpers.Test
+{
+ public class DynamicHelperTest
+ {
+ [Fact]
+ public void TryGetMemberValueReturnsValueIfBinderIsNotCSharp()
+ {
+ // Arrange
+ var mockMemberBinder = new MockMemberBinder("Foo");
+ var dynamic = new DynamicWrapper(new { Foo = "Bar" });
+
+ // Act
+ object value;
+ bool result = DynamicHelper.TryGetMemberValue(dynamic, mockMemberBinder, out value);
+
+ // Assert
+ Assert.Equal(value, "Bar");
+ }
+
+ private class MockMemberBinder : GetMemberBinder
+ {
+ public MockMemberBinder(string name)
+ : base(name, false)
+ {
+ }
+
+ public override DynamicMetaObject FallbackGetMember(DynamicMetaObject target, DynamicMetaObject errorSuggestion)
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Helpers.Test/DynamicWrapper.cs b/test/System.Web.Helpers.Test/DynamicWrapper.cs
new file mode 100644
index 00000000..9aec18fb
--- /dev/null
+++ b/test/System.Web.Helpers.Test/DynamicWrapper.cs
@@ -0,0 +1,80 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Dynamic;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+
+namespace System.Web.Helpers.Test
+{
+ /// <summary>
+ /// Dynamic object implementation over a regualar CLR object. Getmember accesses members through reflection.
+ /// </summary>
+ public class DynamicWrapper : IDynamicMetaObjectProvider
+ {
+ private object _object;
+
+ public DynamicWrapper(object obj)
+ {
+ _object = obj;
+ }
+
+ public DynamicMetaObject GetMetaObject(Expression parameter)
+ {
+ return new DynamicWrapperMetaObject(parameter, this);
+ }
+
+ private class DynamicWrapperMetaObject : DynamicMetaObject
+ {
+ public DynamicWrapperMetaObject(Expression expression, object value)
+ : base(expression, BindingRestrictions.Empty, value)
+ {
+ }
+
+ private object WrappedObject
+ {
+ get { return ((DynamicWrapper)Value)._object; }
+ }
+
+ private Expression GetDynamicExpression()
+ {
+ return Expression.Convert(Expression, typeof(DynamicWrapper));
+ }
+
+ private Expression GetWrappedObjectExpression()
+ {
+ FieldInfo fieldInfo = typeof(DynamicWrapper).GetField("_object", BindingFlags.NonPublic | BindingFlags.Instance);
+ Debug.Assert(fieldInfo != null);
+ return Expression.Convert(
+ Expression.Field(GetDynamicExpression(), fieldInfo),
+ WrappedObject.GetType());
+ }
+
+ private Expression GetMemberAccessExpression(string memberName)
+ {
+ return Expression.Property(
+ GetWrappedObjectExpression(),
+ memberName);
+ }
+
+ public override DynamicMetaObject BindGetMember(GetMemberBinder binder)
+ {
+ var binderDefault = binder.FallbackGetMember(this);
+
+ var expression = Expression.Convert(GetMemberAccessExpression(binder.Name), typeof(object));
+
+ var dynamicSuggestion = new DynamicMetaObject(expression, BindingRestrictions.GetTypeRestriction(Expression, LimitType)
+ .Merge(binderDefault.Restrictions));
+
+ return binder.FallbackGetMember(this, dynamicSuggestion);
+ }
+
+ public override IEnumerable<string> GetDynamicMemberNames()
+ {
+ return (from p in WrappedObject.GetType().GetProperties()
+ orderby p.Name
+ select p.Name).ToArray();
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Helpers.Test/HelperResultTest.cs b/test/System.Web.Helpers.Test/HelperResultTest.cs
new file mode 100644
index 00000000..a38cb7bd
--- /dev/null
+++ b/test/System.Web.Helpers.Test/HelperResultTest.cs
@@ -0,0 +1,72 @@
+using System.IO;
+using System.Web.WebPages;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Helpers.Test
+{
+ /// <summary>
+ ///This is a test class for Util is intended
+ ///to contain all HelperResult Unit Tests
+ ///</summary>
+ public class HelperResultTest
+ {
+ [Fact]
+ public void HelperResultConstructorNullTest()
+ {
+ Assert.ThrowsArgumentNull(() => { var helper = new HelperResult(null); }, "action");
+ }
+
+ [Fact]
+ public void ToStringTest()
+ {
+ var text = "Hello";
+ Action<TextWriter> action = tw => tw.Write(text);
+ var helper = new HelperResult(action);
+ Assert.Equal(text, helper.ToString());
+ }
+
+ [Fact]
+ public void WriteToTest()
+ {
+ var text = "Hello";
+ Action<TextWriter> action = tw => tw.Write(text);
+ var helper = new HelperResult(action);
+ var writer = new StringWriter();
+ helper.WriteTo(writer);
+ Assert.Equal(text, writer.ToString());
+ }
+
+ [Fact]
+ public void ToHtmlStringDoesNotEncode()
+ {
+ // Arrange
+ string text = "<strong>This is a test & it uses html.</strong>";
+ Action<TextWriter> action = writer => writer.Write(text);
+ HelperResult helperResult = new HelperResult(action);
+
+ // Act
+ string result = helperResult.ToHtmlString();
+
+ // Assert
+ Assert.Equal(result, text);
+ }
+
+ [Fact]
+ public void ToHtmlStringReturnsSameResultAsWriteTo()
+ {
+ // Arrange
+ string text = "<strong>This is a test & it uses html.</strong>";
+ Action<TextWriter> action = writer => writer.Write(text);
+ HelperResult helperResult = new HelperResult(action);
+ StringWriter stringWriter = new StringWriter();
+
+ // Act
+ string htmlString = helperResult.ToHtmlString();
+ helperResult.WriteTo(stringWriter);
+
+ // Assert
+ Assert.Equal(htmlString, stringWriter.ToString());
+ }
+ }
+}
diff --git a/test/System.Web.Helpers.Test/JsonTest.cs b/test/System.Web.Helpers.Test/JsonTest.cs
new file mode 100644
index 00000000..c4af02b1
--- /dev/null
+++ b/test/System.Web.Helpers.Test/JsonTest.cs
@@ -0,0 +1,369 @@
+using System.Collections.Generic;
+using System.Dynamic;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Helpers.Test
+{
+ public class JsonTest
+ {
+ [Fact]
+ public void EncodeWithDynamicObject()
+ {
+ // Arrange
+ dynamic obj = new DummyDynamicObject();
+ obj.Name = "Hello";
+ obj.Age = 1;
+ obj.Grades = new[] { "A", "B", "C" };
+
+ // Act
+ string json = Json.Encode(obj);
+
+ // Assert
+ Assert.Equal("{\"Name\":\"Hello\",\"Age\":1,\"Grades\":[\"A\",\"B\",\"C\"]}", json);
+ }
+
+ [Fact]
+ public void EncodeArray()
+ {
+ // Arrange
+ object input = new string[] { "one", "2", "three", "4" };
+
+ // Act
+ string json = Json.Encode(input);
+
+ // Assert
+ Assert.Equal("[\"one\",\"2\",\"three\",\"4\"]", json);
+ }
+
+ [Fact]
+ public void EncodeDynamicJsonArrayEncodesAsArray()
+ {
+ // Arrange
+ dynamic array = Json.Decode("[1,2,3]");
+
+ // Act
+ string json = Json.Encode(array);
+
+ // Assert
+ Assert.Equal("[1,2,3]", json);
+ }
+
+ [Fact]
+ public void DecodeDynamicObject()
+ {
+ // Act
+ var obj = Json.Decode("{\"Name\":\"Hello\",\"Age\":1,\"Grades\":[\"A\",\"B\",\"C\"]}");
+
+ // Assert
+ Assert.Equal("Hello", obj.Name);
+ Assert.Equal(1, obj.Age);
+ Assert.Equal(3, obj.Grades.Length);
+ Assert.Equal("A", obj.Grades[0]);
+ Assert.Equal("B", obj.Grades[1]);
+ Assert.Equal("C", obj.Grades[2]);
+ }
+
+ [Fact]
+ public void DecodeDynamicObjectImplicitConversionToDictionary()
+ {
+ // Act
+ IDictionary<string, object> values = Json.Decode("{\"Name\":\"Hello\",\"Age\":1}");
+
+ // Assert
+ Assert.Equal("Hello", values["Name"]);
+ Assert.Equal(1, values["Age"]);
+ }
+
+ [Fact]
+ public void DecodeArrayImplicitConversionToArrayAndObjectArray()
+ {
+ // Act
+ Array array = Json.Decode("[1,2,3]");
+ object[] objArray = Json.Decode("[1,2,3]");
+ IEnumerable<dynamic> dynamicEnumerable = Json.Decode("[{a:1}]");
+
+ // Assert
+ Assert.NotNull(array);
+ Assert.NotNull(objArray);
+ Assert.NotNull(dynamicEnumerable);
+ }
+
+ [Fact]
+ public void DecodeArrayImplicitConversionToArrayArrayValuesAreDynamic()
+ {
+ // Act
+ dynamic[] objArray = Json.Decode("[{\"A\":1}]");
+
+ // Assert
+ Assert.NotNull(objArray);
+ Assert.Equal(1, objArray[0].A);
+ }
+
+ [Fact]
+ public void DecodeDynamicObjectAccessPropertiesByIndexer()
+ {
+ // Arrange
+ var obj = Json.Decode("{\"Name\":\"Hello\",\"Age\":1,\"Grades\":[\"A\",\"B\",\"C\"]}");
+
+ // Assert
+ Assert.Equal("Hello", obj["Name"]);
+ Assert.Equal(1, obj["Age"]);
+ Assert.Equal(3, obj["Grades"].Length);
+ Assert.Equal("A", obj["Grades"][0]);
+ Assert.Equal("B", obj["Grades"][1]);
+ Assert.Equal("C", obj["Grades"][2]);
+ }
+
+ [Fact]
+ public void DecodeDynamicObjectAccessPropertiesByNullIndexerReturnsNull()
+ {
+ // Arrange
+ var obj = Json.Decode("{\"Name\":\"Hello\",\"Age\":1,\"Grades\":[\"A\",\"B\",\"C\"]}");
+
+ // Assert
+ Assert.Null(obj[null]);
+ }
+
+ [Fact]
+ public void DecodeDateTime()
+ {
+ // Act
+ DateTime dateTime = Json.Decode("\"\\/Date(940402800000)\\/\"");
+
+ // Assert
+ Assert.Equal(1999, dateTime.Year);
+ Assert.Equal(10, dateTime.Month);
+ Assert.Equal(20, dateTime.Day);
+ }
+
+ [Fact]
+ public void DecodeNumber()
+ {
+ // Act
+ int number = Json.Decode("1");
+
+ // Assert
+ Assert.Equal(1, number);
+ }
+
+ [Fact]
+ public void DecodeString()
+ {
+ // Act
+ string @string = Json.Decode("\"1\"");
+
+ // Assert
+ Assert.Equal("1", @string);
+ }
+
+ [Fact]
+ public void DecodeArray()
+ {
+ // Act
+ var values = Json.Decode("[11,12,13,14,15]");
+
+ // Assert
+ Assert.Equal(5, values.Length);
+ Assert.Equal(11, values[0]);
+ Assert.Equal(12, values[1]);
+ Assert.Equal(13, values[2]);
+ Assert.Equal(14, values[3]);
+ Assert.Equal(15, values[4]);
+ }
+
+ [Fact]
+ public void DecodeObjectWithArrayProperty()
+ {
+ // Act
+ var obj = Json.Decode("{\"A\":1,\"B\":[1,3,4]}");
+ object[] bValues = obj.B;
+
+ // Assert
+ Assert.Equal(1, obj.A);
+ Assert.Equal(1, bValues[0]);
+ Assert.Equal(3, bValues[1]);
+ Assert.Equal(4, bValues[2]);
+ }
+
+ [Fact]
+ public void DecodeArrayWithObjectValues()
+ {
+ // Act
+ var obj = Json.Decode("[{\"A\":1},{\"B\":3, \"C\": \"hello\"}]");
+
+ // Assert
+ Assert.Equal(2, obj.Length);
+ Assert.Equal(1, obj[0].A);
+ Assert.Equal(3, obj[1].B);
+ Assert.Equal("hello", obj[1].C);
+ }
+
+ [Fact]
+ public void DecodeArraySetValues()
+ {
+ // Arrange
+ var values = Json.Decode("[1,2,3,4,5]");
+ for (int i = 0; i < values.Length; i++)
+ {
+ values[i]++;
+ }
+
+ // Assert
+ Assert.Equal(5, values.Length);
+ Assert.Equal(2, values[0]);
+ Assert.Equal(3, values[1]);
+ Assert.Equal(4, values[2]);
+ Assert.Equal(5, values[3]);
+ Assert.Equal(6, values[4]);
+ }
+
+ [Fact]
+ public void DecodeArrayPassToMethodThatTakesArray()
+ {
+ // Arrange
+ var values = Json.Decode("[3,2,1]");
+
+ // Act
+ int index = Array.IndexOf(values, 2);
+
+ // Assert
+ Assert.Equal(1, index);
+ }
+
+ [Fact]
+ public void DecodeArrayGetEnumerator()
+ {
+ // Arrange
+ var values = Json.Decode("[1,2,3]");
+
+ // Assert
+ int val = 1;
+ foreach (var value in values)
+ {
+ Assert.Equal(val, val);
+ val++;
+ }
+ }
+
+ [Fact]
+ public void DecodeObjectPropertyAccessIsSameObjectInstance()
+ {
+ // Arrange
+ var obj = Json.Decode("{\"Name\":{\"Version:\":4.0, \"Key\":\"Key\"}}");
+
+ // Assert
+ Assert.Same(obj.Name, obj.Name);
+ }
+
+ [Fact]
+ public void DecodeArrayAccessingMembersThatDontExistReturnsNull()
+ {
+ // Act
+ var obj = Json.Decode("[\"a\", \"b\"]");
+
+ // Assert
+ Assert.Null(obj.PropertyThatDoesNotExist);
+ }
+
+ [Fact]
+ public void DecodeObjectSetProperties()
+ {
+ // Act
+ var obj = Json.Decode("{\"A\":{\"B\":100}}");
+ obj.A.B = 20;
+
+ // Assert
+ Assert.Equal(20, obj.A.B);
+ }
+
+ [Fact]
+ public void DecodeObjectSettingObjectProperties()
+ {
+ // Act
+ var obj = Json.Decode("{\"A\":1}");
+ obj.A = new { B = 1, D = 2 };
+
+ // Assert
+ Assert.Equal(1, obj.A.B);
+ Assert.Equal(2, obj.A.D);
+ }
+
+ [Fact]
+ public void DecodeObjectWithArrayPropertyPassPropertyToMethodThatTakesArray()
+ {
+ // Arrange
+ var obj = Json.Decode("{\"A\":[3,2,1]}");
+
+ // Act
+ Array.Sort(obj.A);
+
+ // Assert
+ Assert.Equal(1, obj.A[0]);
+ Assert.Equal(2, obj.A[1]);
+ Assert.Equal(3, obj.A[2]);
+ }
+
+ [Fact]
+ public void DecodeObjectAccessingMembersThatDontExistReturnsNull()
+ {
+ // Act
+ var obj = Json.Decode("{\"A\":1}");
+
+ // Assert
+ Assert.Null(obj.PropertyThatDoesntExist);
+ }
+
+ [Fact]
+ public void DecodeObjectWithSpecificType()
+ {
+ // Act
+ var person = Json.Decode<Person>("{\"Name\":\"David\", \"Age\":2}");
+
+ // Assert
+ Assert.Equal("David", person.Name);
+ Assert.Equal(2, person.Age);
+ }
+
+ [Fact]
+ public void DecodeObjectWithImplicitConversionToNonDynamicTypeThrows()
+ {
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(() => { Person person = Json.Decode("{\"Name\":\"David\", \"Age\":2, \"Address\":{\"Street\":\"Bellevue\"}}"); }, "Unable to convert to \"System.Web.Helpers.Test.JsonTest+Person\". Use Json.Decode<T> instead.");
+ }
+
+ private class DummyDynamicObject : DynamicObject
+ {
+ private IDictionary<string, object> _values = new Dictionary<string, object>();
+
+ public override IEnumerable<string> GetDynamicMemberNames()
+ {
+ return _values.Keys;
+ }
+
+ public override bool TrySetMember(SetMemberBinder binder, object value)
+ {
+ _values[binder.Name] = value;
+ return true;
+ }
+
+ public override bool TryGetMember(GetMemberBinder binder, out object result)
+ {
+ return _values.TryGetValue(binder.Name, out result);
+ }
+ }
+
+ private class Person
+ {
+ public string Name { get; set; }
+ public int Age { get; set; }
+ public int GPA { get; set; }
+ public Address Address { get; set; }
+ }
+
+ private class Address
+ {
+ public string Street { get; set; }
+ }
+ }
+}
diff --git a/test/System.Web.Helpers.Test/ObjectInfoTest.cs b/test/System.Web.Helpers.Test/ObjectInfoTest.cs
new file mode 100644
index 00000000..f93d471a
--- /dev/null
+++ b/test/System.Web.Helpers.Test/ObjectInfoTest.cs
@@ -0,0 +1,728 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Dynamic;
+using System.Linq;
+using System.Web.TestUtil;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Helpers.Test
+{
+ public class ObjectInfoTest
+ {
+ [Fact]
+ public void PrintWithNegativeDepthThrows()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentGreaterThanOrEqualTo(() => ObjectInfo.Print(null, depth: -1), "depth", "0");
+ }
+
+ [Fact]
+ public void PrintWithInvalidEnumerationLength()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentGreaterThan(() => ObjectInfo.Print(null, enumerationLength: -1), "enumerationLength", "0");
+ }
+
+ [Fact]
+ public void PrintWithNull()
+ {
+ // Arrange
+ MockObjectVisitor visitor = CreateObjectVisitor();
+
+ // Act
+ visitor.Print(null);
+
+ // Assert
+ Assert.Equal(1, visitor.Values.Count);
+ Assert.Equal("null", visitor.Values[0]);
+ }
+
+ [Fact]
+ public void PrintWithEmptyString()
+ {
+ // Arrange
+ MockObjectVisitor visitor = CreateObjectVisitor();
+
+ // Act
+ visitor.Print(String.Empty);
+
+ // Assert
+ Assert.Equal(1, visitor.Values.Count);
+ Assert.Equal(String.Empty, visitor.Values[0]);
+ }
+
+ [Fact]
+ public void PrintWithInt()
+ {
+ // Arrange
+ MockObjectVisitor visitor = CreateObjectVisitor();
+
+ // Act
+ visitor.Print(404);
+
+ // Assert
+ Assert.Equal(1, visitor.Values.Count);
+ Assert.Equal("404", visitor.Values[0]);
+ }
+
+ [Fact]
+ public void PrintWithIDictionary()
+ {
+ // Arrange
+ MockObjectVisitor visitor = CreateObjectVisitor();
+ IDictionary dict = new OrderedDictionary();
+ dict.Add("foo", "bar");
+ dict.Add("abc", 500);
+
+ // Act
+ visitor.Print(dict);
+
+ // Assert
+ Assert.Equal("foo = bar", visitor.KeyValuePairs[0]);
+ Assert.Equal("abc = 500", visitor.KeyValuePairs[1]);
+ }
+
+ [Fact]
+ public void PrintWithIEnumerable()
+ {
+ // Arrange
+ MockObjectVisitor visitor = CreateObjectVisitor();
+ var values = Enumerable.Range(0, 10);
+
+ // Act
+ visitor.Print(values);
+
+ // Assert
+ foreach (var num in values)
+ {
+ Assert.True(visitor.Values.Contains(num.ToString()));
+ }
+ }
+
+ [Fact]
+ public void PrintWithGenericIListPrintsIndex()
+ {
+ // Arrange
+ MockObjectVisitor visitor = CreateObjectVisitor();
+ var values = Enumerable.Range(0, 10).ToList();
+
+ // Act
+ visitor.Print(values);
+
+ // Assert
+ for (int i = 0; i < values.Count; i++)
+ {
+ Assert.True(visitor.Values.Contains(values[i].ToString()));
+ Assert.True(visitor.Indexes.Contains(i));
+ }
+ }
+
+ [Fact]
+ public void PrintWithArrayPrintsIndex()
+ {
+ // Arrange
+ MockObjectVisitor visitor = CreateObjectVisitor();
+ var values = Enumerable.Range(0, 10).ToArray();
+
+ // Act
+ visitor.Print(values);
+
+ // Assert
+ for (int i = 0; i < values.Length; i++)
+ {
+ Assert.True(visitor.Values.Contains(values[i].ToString()));
+ Assert.True(visitor.Indexes.Contains(i));
+ }
+ }
+
+ [Fact]
+ public void PrintNameValueCollectionPrintsKeysAndValues()
+ {
+ // Arrange
+ MockObjectVisitor visitor = CreateObjectVisitor();
+ var values = new NameValueCollection();
+ values["a"] = "1";
+ values["b"] = null;
+
+ // Act
+ visitor.Print(values);
+
+ // Assert
+ Assert.Equal("a = 1", visitor.KeyValuePairs[0]);
+ Assert.Equal("b = null", visitor.KeyValuePairs[1]);
+ }
+
+ [Fact]
+ public void PrintDateTime()
+ {
+ using (new CultureReplacer())
+ {
+ // Arrange
+ MockObjectVisitor visitor = CreateObjectVisitor();
+ var dt = new DateTime(2001, 11, 20, 10, 30, 1);
+
+ // Act
+ visitor.Print(dt);
+
+ // Assert
+ Assert.Equal("11/20/2001 10:30:01 AM", visitor.Values[0]);
+ }
+ }
+
+ [Fact]
+ public void PrintCustomObjectPrintsMembers()
+ {
+ // Arrange
+ MockObjectVisitor visitor = CreateObjectVisitor();
+ var person = new Person
+ {
+ Name = "David",
+ Age = 23.3,
+ Dob = new DateTime(1986, 11, 19),
+ LongType = 1000000000,
+ Type = 1
+ };
+
+ using (new CultureReplacer())
+ {
+ // Act
+ visitor.Print(person);
+
+ // Assert
+ Assert.Equal(9, visitor.Members.Count);
+ Assert.True(visitor.Members.Contains("double Age = 23.3"));
+ Assert.True(visitor.Members.Contains("string Name = David"));
+ Assert.True(visitor.Members.Contains("DateTime Dob = 11/19/1986 12:00:00 AM"));
+ Assert.True(visitor.Members.Contains("short Type = 1"));
+ Assert.True(visitor.Members.Contains("float Float = 0"));
+ Assert.True(visitor.Members.Contains("byte Byte = 0"));
+ Assert.True(visitor.Members.Contains("decimal Decimal = 0"));
+ Assert.True(visitor.Members.Contains("bool Bool = False"));
+ }
+ }
+
+ [Fact]
+ public void PrintShowsVisitedWhenCircularReferenceInObjectGraph()
+ {
+ // Arrange
+ MockObjectVisitor visitor = CreateObjectVisitor();
+ PersonNode node = new PersonNode
+ {
+ Person = new Person
+ {
+ Name = "David",
+ Age = 23.3
+ }
+ };
+ node.Next = node;
+
+ // Act
+ visitor.Print(node);
+
+ // Assert
+ Assert.True(visitor.Members.Contains("string Name = David"));
+ Assert.True(visitor.Members.Contains("double Age = 23.3"));
+ Assert.True(visitor.Members.Contains("PersonNode Next = Visited"));
+ }
+
+ [Fact]
+ public void PrintShowsVisitedWhenCircularReferenceIsIEnumerable()
+ {
+ // Arrange
+ MockObjectVisitor visitor = CreateObjectVisitor();
+ List<object> values = new List<object>();
+ values.Add(values);
+
+ // Act
+ visitor.Print(values);
+
+ // Assert
+ Assert.Equal("Visited", visitor.Values[0]);
+ Assert.Equal("Visited " + values.GetHashCode(), visitor.Visited[0]);
+ }
+
+ [Fact]
+ public void PrintShowsVisitedWhenCircularReferenceIsIDictionary()
+ {
+ // Arrange
+ MockObjectVisitor visitor = CreateObjectVisitor();
+ OrderedDictionary values = new OrderedDictionary();
+ values[values] = values;
+
+ // Act
+ visitor.Print(values);
+
+ // Assert
+ Assert.Equal("Visited", visitor.Values[0]);
+ Assert.Equal("Visited " + values.GetHashCode(), visitor.Visited[0]);
+ }
+
+ [Fact]
+ public void PrintShowsVisitedWhenCircularReferenceIsNameValueCollection()
+ {
+ // Arrange
+ MockObjectVisitor visitor = CreateObjectVisitor();
+ NameValueCollection nameValues = new NameValueCollection();
+ nameValues["id"] = "1";
+ List<NameValueCollection> values = new List<NameValueCollection>();
+ values.Add(nameValues);
+ values.Add(nameValues);
+
+ // Act
+ visitor.Print(values);
+
+ // Assert
+ Assert.True(visitor.Values.Contains("Visited"));
+ Assert.True(visitor.Visited.Contains("Visited " + nameValues.GetHashCode()));
+ }
+
+ [Fact]
+ public void PrintExcludesWriteOnlyProperties()
+ {
+ // Arrange
+ MockObjectVisitor visitor = CreateObjectVisitor();
+ ClassWithWriteOnlyProperty cls = new ClassWithWriteOnlyProperty();
+
+ // Act
+ visitor.Print(cls);
+
+ // Assert
+ Assert.Equal(0, visitor.Members.Count);
+ }
+
+ [Fact]
+ public void PrintWritesEnumeratedElementsUntilLimitIsHit()
+ {
+ // Arrange
+ MockObjectVisitor visitor = CreateObjectVisitor();
+ var enumeration = Enumerable.Range(0, 2000);
+
+ // Act
+ visitor.Print(enumeration);
+
+ // Assert
+ for (int i = 0; i <= 2000; i++)
+ {
+ if (i < 1000)
+ {
+ Assert.True(visitor.Values.Contains(i.ToString()));
+ }
+ else
+ {
+ Assert.False(visitor.Values.Contains(i.ToString()));
+ }
+ }
+ Assert.True(visitor.Values.Contains("Limit Exceeded"));
+ }
+
+ [Fact]
+ public void PrintWithAnonymousType()
+ {
+ // Arrange
+ MockObjectVisitor visitor = CreateObjectVisitor();
+ var value = new { Name = "John", X = 1 };
+
+ // Act
+ visitor.Print(value);
+
+ // Assert
+ Assert.True(visitor.Members.Contains("string Name = John"));
+ Assert.True(visitor.Members.Contains("int X = 1"));
+ }
+
+ [Fact]
+ public void PrintClassWithPublicFields()
+ {
+ // Arrange
+ MockObjectVisitor visitor = CreateObjectVisitor();
+ ClassWithFields value = new ClassWithFields();
+ value.Foo = "John";
+ value.Bar = 1;
+
+ // Actt
+ visitor.Print(value);
+
+ // Assert
+ Assert.True(visitor.Members.Contains("string Foo = John"));
+ Assert.True(visitor.Members.Contains("int Bar = 1"));
+ }
+
+ [Fact]
+ public void PrintClassWithDynamicMembersPrintsMembersIfGetDynamicMemberNamesIsImplemented()
+ {
+ // Arrange
+ MockObjectVisitor visitor = CreateObjectVisitor();
+ dynamic d = new DynamicDictionary();
+ d.Cycle = d;
+ d.Name = "Foo";
+ d.Value = null;
+
+ // Act
+ visitor.Print(d);
+
+ // Assert
+ Assert.True(visitor.Members.Contains("DynamicDictionary Cycle = Visited"));
+ Assert.True(visitor.Members.Contains("string Name = Foo"));
+ Assert.True(visitor.Members.Contains("Value = null"));
+ }
+
+ [Fact]
+ public void PrintClassWithDynamicMembersReturningNullPrintsNoMembers()
+ {
+ // Arrange
+ MockObjectVisitor visitor = CreateObjectVisitor();
+ dynamic d = new ClassWithDynamicAnNullMemberNames();
+ d.Cycle = d;
+ d.Name = "Foo";
+ d.Value = null;
+
+ // Act
+ visitor.Print(d);
+
+ // Assert
+ Assert.False(visitor.Members.Any());
+ }
+
+ [Fact]
+ public void PrintUsesToStringOfIConvertibleObjects()
+ {
+ // Arrange
+ MockObjectVisitor visitor = CreateObjectVisitor();
+ ConvertibleClass cls = new ConvertibleClass();
+
+ // Act
+ visitor.Print(cls);
+
+ // Assert
+ Assert.Equal("Test", visitor.Values[0]);
+ }
+
+ [Fact]
+ public void PrintConvertsTypeToString()
+ {
+ // Arrange
+ MockObjectVisitor visitor = CreateObjectVisitor();
+
+ // Act
+ visitor.Print(typeof(string));
+
+ // Assert
+ Assert.Equal("typeof(string)", visitor.Values[0]);
+ }
+
+ [Fact]
+ public void PrintClassWithPropertyThatThrowsExceptionPrintsException()
+ {
+ // Arrange
+ MockObjectVisitor visitor = CreateObjectVisitor();
+ ClassWithPropertyThatThrowsException value = new ClassWithPropertyThatThrowsException();
+
+ // Act
+ visitor.Print(value);
+
+ // Assert
+ Assert.Equal("int MyProperty = Property accessor 'MyProperty' on object 'System.Web.Helpers.Test.ObjectInfoTest+ClassWithPropertyThatThrowsException' threw the following exception:'Property that shows an exception'", visitor.Members[0]);
+ }
+
+ [Fact]
+ public void ConvertEscapeSequencesPrintsStringEscapeSequencesAsLiterals()
+ {
+ // Act
+ string value = HtmlObjectPrinter.ConvertEscapseSequences("\\\'\"\0\a\b\f\n\r\t\v");
+
+ // Assert
+ Assert.Equal("\\\\'\\\"\\0\\a\\b\\f\\n\\r\\t\\v", value);
+ }
+
+ [Fact]
+ public void ConvertEscapeSequencesDoesNotEscapeUnicodeSequences()
+ {
+ // Act
+ string value = HtmlObjectPrinter.ConvertEscapseSequences("\u1023\x2045");
+
+ // Assert
+ Assert.Equal("\u1023\x2045", value);
+ }
+
+ [Fact]
+ public void PrintCharPrintsQuotedString()
+ {
+ // Arrange
+ HtmlObjectPrinter printer = new HtmlObjectPrinter(100, 100);
+ HtmlElement element = new HtmlElement("span");
+ printer.PushElement(element);
+
+ // Act
+ printer.VisitConvertedValue('x', "x");
+
+ // Assert
+ Assert.Equal(1, element.Children.Count);
+ HtmlElement child = element.Children[0];
+ Assert.Equal("'x'", child.InnerText);
+ Assert.Equal("quote", child["class"]);
+ }
+
+ [Fact]
+ public void PrintEscapeCharPrintsEscapedCharAsLiteral()
+ {
+ // Arrange
+ HtmlObjectPrinter printer = new HtmlObjectPrinter(100, 100);
+ HtmlElement element = new HtmlElement("span");
+ printer.PushElement(element);
+
+ // Act
+ printer.VisitConvertedValue('\t', "\t");
+
+ // Assert
+ Assert.Equal(1, element.Children.Count);
+ HtmlElement child = element.Children[0];
+ Assert.Equal("'\\t'", child.InnerText);
+ Assert.Equal("quote", child["class"]);
+ }
+
+ [Fact]
+ public void GetTypeNameConvertsGenericTypesToCsharpSyntax()
+ {
+ // Act
+ string value = ObjectVisitor.GetTypeName(typeof(Func<Func<Func<int, int, object>, Action<int>>>));
+
+ // Assert
+ Assert.Equal("Func<Func<Func<int, int, object>, Action<int>>>", value);
+ }
+
+ private class ConvertibleClass : IConvertible
+ {
+ public TypeCode GetTypeCode()
+ {
+ throw new NotImplementedException();
+ }
+
+ public bool ToBoolean(IFormatProvider provider)
+ {
+ throw new NotImplementedException();
+ }
+
+ public byte ToByte(IFormatProvider provider)
+ {
+ throw new NotImplementedException();
+ }
+
+ public char ToChar(IFormatProvider provider)
+ {
+ throw new NotImplementedException();
+ }
+
+ public DateTime ToDateTime(IFormatProvider provider)
+ {
+ throw new NotImplementedException();
+ }
+
+ public decimal ToDecimal(IFormatProvider provider)
+ {
+ throw new NotImplementedException();
+ }
+
+ public double ToDouble(IFormatProvider provider)
+ {
+ throw new NotImplementedException();
+ }
+
+ public short ToInt16(IFormatProvider provider)
+ {
+ throw new NotImplementedException();
+ }
+
+ public int ToInt32(IFormatProvider provider)
+ {
+ throw new NotImplementedException();
+ }
+
+ public long ToInt64(IFormatProvider provider)
+ {
+ throw new NotImplementedException();
+ }
+
+ public sbyte ToSByte(IFormatProvider provider)
+ {
+ throw new NotImplementedException();
+ }
+
+ public float ToSingle(IFormatProvider provider)
+ {
+ throw new NotImplementedException();
+ }
+
+ public string ToString(IFormatProvider provider)
+ {
+ return "Test";
+ }
+
+ public object ToType(Type conversionType, IFormatProvider provider)
+ {
+ throw new NotImplementedException();
+ }
+
+ public ushort ToUInt16(IFormatProvider provider)
+ {
+ throw new NotImplementedException();
+ }
+
+ public uint ToUInt32(IFormatProvider provider)
+ {
+ throw new NotImplementedException();
+ }
+
+ public ulong ToUInt64(IFormatProvider provider)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ private class ClassWithPropertyThatThrowsException
+ {
+ public int MyProperty
+ {
+ get { throw new InvalidOperationException("Property that shows an exception"); }
+ }
+ }
+
+ private class ClassWithDynamicAnNullMemberNames : DynamicObject
+ {
+ public override IEnumerable<string> GetDynamicMemberNames()
+ {
+ return null;
+ }
+
+ public override bool TryGetMember(GetMemberBinder binder, out object result)
+ {
+ result = null;
+ return true;
+ }
+
+ public override bool TrySetMember(SetMemberBinder binder, object value)
+ {
+ return true;
+ }
+ }
+
+ private class Person
+ {
+ public string Name { get; set; }
+ public double Age { get; set; }
+ public DateTime Dob { get; set; }
+ public short Type { get; set; }
+ public long LongType { get; set; }
+ public float Float { get; set; }
+ public byte Byte { get; set; }
+ public decimal Decimal { get; set; }
+ public bool Bool { get; set; }
+ }
+
+ private class ClassWithFields
+ {
+ public string Foo;
+ public int Bar = 13;
+ }
+
+ private class ClassWithWriteOnlyProperty
+ {
+ public int Value
+ {
+ set { }
+ }
+ }
+
+ private class PersonNode
+ {
+ public Person Person { get; set; }
+ public PersonNode Next { get; set; }
+ }
+
+ private MockObjectVisitor CreateObjectVisitor(int recursionLimit = 10, int enumerationLimit = 1000)
+ {
+ return new MockObjectVisitor(recursionLimit, enumerationLimit);
+ }
+
+ private class MockObjectVisitor : ObjectVisitor
+ {
+ public MockObjectVisitor(int recursionLimit, int enumerationLimit)
+ : base(recursionLimit, enumerationLimit)
+ {
+ Values = new List<string>();
+ KeyValuePairs = new List<string>();
+ Members = new List<string>();
+ Indexes = new List<int>();
+ Visited = new List<string>();
+ }
+
+ public List<string> Values { get; set; }
+ public List<string> KeyValuePairs { get; set; }
+ public List<string> Members { get; set; }
+ public List<int> Indexes { get; set; }
+ public List<string> Visited { get; set; }
+
+ public void Print(object value)
+ {
+ Visit(value, 0);
+ }
+
+ public override void VisitObjectVisitorException(ObjectVisitorException exception)
+ {
+ Values.Add(exception.InnerException.Message);
+ }
+
+ public override void VisitStringValue(string stringValue)
+ {
+ Values.Add(stringValue);
+ base.VisitStringValue(stringValue);
+ }
+
+ public override void VisitVisitedObject(string id, object value)
+ {
+ Visited.Add(String.Format("Visited {0}", id));
+ Values.Add("Visited");
+ base.VisitVisitedObject(id, value);
+ }
+
+ public override void VisitIndexedEnumeratedValue(int index, object item, int depth)
+ {
+ Indexes.Add(index);
+ base.VisitIndexedEnumeratedValue(index, item, depth);
+ }
+
+ public override void VisitEnumeratonLimitExceeded()
+ {
+ Values.Add("Limit Exceeded");
+ base.VisitEnumeratonLimitExceeded();
+ }
+
+ public override void VisitMember(string name, Type type, object value, int depth)
+ {
+ base.VisitMember(name, type, value, depth);
+ type = type ?? (value != null ? value.GetType() : null);
+ if (type == null)
+ {
+ Members.Add(String.Format("{0} = null", name));
+ }
+ else
+ {
+ Members.Add(String.Format("{0} {1} = {2}", GetTypeName(type), name, Values.Last()));
+ }
+ }
+
+ public override void VisitNull()
+ {
+ Values.Add("null");
+ base.VisitNull();
+ }
+
+ public override void VisitKeyValue(object key, object value, int depth)
+ {
+ base.VisitKeyValue(key, value, depth);
+ KeyValuePairs.Add(String.Format("{0} = {1}", Values[Values.Count - 2], Values[Values.Count - 1]));
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Helpers.Test/PreComputedGridDataSourceTest.cs b/test/System.Web.Helpers.Test/PreComputedGridDataSourceTest.cs
new file mode 100644
index 00000000..bf33b2c2
--- /dev/null
+++ b/test/System.Web.Helpers.Test/PreComputedGridDataSourceTest.cs
@@ -0,0 +1,41 @@
+using System.Linq;
+using Moq;
+using Xunit;
+
+namespace System.Web.Helpers.Test
+{
+ public class PreComputedGridDataSourceTest
+ {
+ [Fact]
+ public void PreSortedDataSourceReturnsRowCountItWasSpecified()
+ {
+ // Arrange
+ int rows = 20;
+ var dataSource = new PreComputedGridDataSource(new WebGrid(GetContext()), values: Enumerable.Range(0, 10).Cast<dynamic>(), totalRows: rows);
+
+ // Act and Assert
+ Assert.Equal(rows, dataSource.TotalRowCount);
+ }
+
+ [Fact]
+ public void PreSortedDataSourceReturnsAllRows()
+ {
+ // Arrange
+ var grid = new WebGrid(GetContext());
+ var dataSource = new PreComputedGridDataSource(grid: grid, values: Enumerable.Range(0, 10).Cast<dynamic>(), totalRows: 10);
+
+ // Act
+ var rows = dataSource.GetRows(new SortInfo { SortColumn = String.Empty }, 0);
+
+ // Assert
+ Assert.Equal(rows.Count, 10);
+ Assert.Equal(rows.First().Value, 0);
+ Assert.Equal(rows.Last().Value, 9);
+ }
+
+ private HttpContextBase GetContext()
+ {
+ return new Mock<HttpContextBase>().Object;
+ }
+ }
+}
diff --git a/test/System.Web.Helpers.Test/Properties/AssemblyInfo.cs b/test/System.Web.Helpers.Test/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..6fdbb42e
--- /dev/null
+++ b/test/System.Web.Helpers.Test/Properties/AssemblyInfo.cs
@@ -0,0 +1,34 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+
+[assembly: AssemblyTitle("System.Web.Helpers.Test")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("MSIT")]
+[assembly: AssemblyProduct("System.Web.Helpers.Test")]
+[assembly: AssemblyCopyright("Copyright © MSIT 2010")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+
+[assembly: ComVisible(false)]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/test/System.Web.Helpers.Test/ServerInfoTest.cs b/test/System.Web.Helpers.Test/ServerInfoTest.cs
new file mode 100644
index 00000000..b783ac17
--- /dev/null
+++ b/test/System.Web.Helpers.Test/ServerInfoTest.cs
@@ -0,0 +1,162 @@
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Web.WebPages.TestUtils;
+using Moq;
+using Xunit;
+
+namespace System.Web.Helpers.Test
+{
+ public class InfoTest
+ {
+ [Fact]
+ public void ConfigurationReturnsExpectedInfo()
+ {
+ var configInfo = ServerInfo.Configuration();
+
+ // verification
+ // checks only subset of values
+ Assert.NotNull(configInfo);
+ VerifyKey(configInfo, "Machine Name");
+ VerifyKey(configInfo, "OS Version");
+ VerifyKey(configInfo, "ASP.NET Version");
+ }
+
+ [Fact]
+ public void EnvironmentVariablesReturnsExpectedInfo()
+ {
+ var envVariables = ServerInfo.EnvironmentVariables();
+
+ // verification
+ // checks only subset of values
+ Assert.NotNull(envVariables);
+ VerifyKey(envVariables, "Path");
+ VerifyKey(envVariables, "SystemDrive");
+ }
+
+ [Fact]
+ public void ServerVariablesReturnsExpectedInfoWithNoContext()
+ {
+ var serverVariables = ServerInfo.ServerVariables();
+
+ // verification
+ // since there is no HttpContext this will be empty
+ Assert.NotNull(serverVariables);
+ }
+
+ [Fact]
+ public void ServerVariablesReturnsExpectedInfoWthContext()
+ {
+ var serverVariables = new NameValueCollection();
+ serverVariables.Add("foo", "bar");
+
+ var request = new Mock<HttpRequestBase>();
+ request.Setup(c => c.ServerVariables).Returns(serverVariables);
+
+ var context = new Mock<HttpContextBase>();
+ context.Setup(c => c.Request).Returns(request.Object);
+
+ // verification
+ Assert.NotNull(serverVariables);
+
+ IDictionary<string, string> returnedValues = ServerInfo.ServerVariables(context.Object);
+ Assert.Equal(serverVariables.Count, returnedValues.Count);
+ foreach (var item in returnedValues)
+ {
+ Assert.Equal(serverVariables[item.Key], item.Value);
+ }
+ }
+
+ [Fact]
+ public void HttpRuntimeInfoReturnsExpectedInfo()
+ {
+ var httpRuntimeInfo = ServerInfo.HttpRuntimeInfo();
+
+ // verification
+ // checks only subset of values
+ Assert.NotNull(httpRuntimeInfo);
+ VerifyKey(httpRuntimeInfo, "CLR Install Directory");
+ VerifyKey(httpRuntimeInfo, "Asp Install Directory");
+ VerifyKey(httpRuntimeInfo, "On UNC Share");
+ }
+
+ [Fact]
+ public void ServerInfoDoesNotProduceLegacyCasForHomogenousAppDomain()
+ {
+ // Act and Assert
+ Action action = () =>
+ {
+ IDictionary<string, string> configValue = ServerInfo.LegacyCAS(AppDomain.CurrentDomain);
+
+ Assert.NotNull(configValue);
+ Assert.Equal(0, configValue.Count);
+ };
+
+ AppDomainUtils.RunInSeparateAppDomain(GetAppDomainSetup(legacyCasEnabled: false), action);
+ }
+
+ [Fact]
+ public void ServerInfoProducesLegacyCasForNonHomogenousAppDomain()
+ {
+ // Arrange
+ Action action = () =>
+ {
+ // Act and Assert
+ IDictionary<string, string> configValue = ServerInfo.LegacyCAS(AppDomain.CurrentDomain);
+
+ // Assert
+ Assert.True(configValue.ContainsKey("Legacy Code Access Security"));
+ Assert.Equal(configValue["Legacy Code Access Security"], "Legacy Code Access Security has been detected on your system. Microsoft WebPage features require the ASP.NET 4 Code Access Security model. For information about how to resolve this, contact your server administrator.");
+ };
+
+ AppDomainUtils.RunInSeparateAppDomain(GetAppDomainSetup(legacyCasEnabled: true), action);
+ }
+
+ //[Fact]
+ //public void SqlServerInfoReturnsExpectedInfo() {
+ // var sqlInfo = ServerInfo.SqlServerInfo();
+
+ // // verification
+ // // just verifies that we don't get any unexpected exceptions
+ // Assert.NotNull(sqlInfo);
+ //}
+
+ [Fact]
+ public void RenderResultContainsExpectedTags()
+ {
+ var htmlString = ServerInfo.GetHtml().ToString();
+
+ // just verify that the final HTML produced contains some expected info
+ Assert.True(htmlString.Contains("<table class=\"server-info\" dir=\"ltr\">"));
+ Assert.True(htmlString.Contains("</style>"));
+ Assert.True(htmlString.Contains("Server Configuration"));
+ }
+
+ [Fact]
+ public void RenderGeneratesValidXhtml()
+ {
+ // Result does not validate against XHTML 1.1 and HTML5 because ServerInfo generates
+ // <style> inside <body>. This is by design however since we only use ServerInfo
+ // as debugging aid, not something to be permanently added to a web page.
+ XhtmlAssert.Validate1_0(
+ ServerInfo.GetHtml(),
+ addRoot: true
+ );
+ }
+
+ private void VerifyKey(IDictionary<string, string> info, string key)
+ {
+ Assert.True(info.ContainsKey(key));
+ Assert.False(String.IsNullOrEmpty(info[key]));
+ }
+
+ private AppDomainSetup GetAppDomainSetup(bool legacyCasEnabled)
+ {
+ var setup = new AppDomainSetup();
+ if (legacyCasEnabled)
+ {
+ setup.SetCompatibilitySwitches(new[] { "NetFx40_LegacySecurityPolicy" });
+ }
+ return setup;
+ }
+ }
+}
diff --git a/test/System.Web.Helpers.Test/System.Web.Helpers.Test.csproj b/test/System.Web.Helpers.Test/System.Web.Helpers.Test.csproj
new file mode 100644
index 00000000..40487ed7
--- /dev/null
+++ b/test/System.Web.Helpers.Test/System.Web.Helpers.Test.csproj
@@ -0,0 +1,106 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProductVersion>9.0.30729</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{D3313BDF-8071-4AC8-9D98-ABF7F9E88A57}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>System.Web.Helpers.Test</RootNamespace>
+ <AssemblyName>System.Web.Helpers.Test</AssemblyName>
+ <FileAlignment>512</FileAlignment>
+ <ProjectTypeGuids>{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Debug\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Release\Test\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'CodeCoverage' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\CodeCoverage\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="Moq, Version=4.0.10827.0, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL">
+ <HintPath>..\..\packages\Moq.4.0.10827\lib\NET40\Moq.dll</HintPath>
+ </Reference>
+ <Reference Include="System" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.Drawing" />
+ <Reference Include="System.Web" />
+ <Reference Include="System.Web.DataVisualization" />
+ <Reference Include="Microsoft.CSharp" />
+ <Reference Include="System.XML" />
+ <Reference Include="xunit">
+ <HintPath>..\..\packages\xunit.1.9.0.1566\lib\xunit.dll</HintPath>
+ </Reference>
+ <Reference Include="xunit.extensions">
+ <HintPath>..\..\packages\xunit.extensions.1.9.0.1566\lib\xunit.extensions.dll</HintPath>
+ </Reference>
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="ChartTest.cs" />
+ <Compile Include="ConversionUtilTest.cs" />
+ <Compile Include="CryptoTest.cs" />
+ <Compile Include="DynamicDictionary.cs" />
+ <Compile Include="DynamicHelperTest.cs" />
+ <Compile Include="DynamicWrapper.cs" />
+ <Compile Include="JsonTest.cs" />
+ <Compile Include="ObjectInfoTest.cs" />
+ <Compile Include="PreComputedGridDataSourceTest.cs" />
+ <Compile Include="WebCacheTest.cs" />
+ <Compile Include="HelperResultTest.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="ServerInfoTest.cs" />
+ <Compile Include="WebGridDataSourceTest.cs" />
+ <Compile Include="WebGridTest.cs" />
+ <Compile Include="WebImageTest.cs" />
+ <Compile Include="WebMailTest.cs" />
+ <Compile Include="XhtmlAssert.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\System.Web.WebPages\System.Web.WebPages.csproj">
+ <Project>{76EFA9C5-8D7E-4FDF-B710-E20F8B6B00D2}</Project>
+ <Name>System.Web.WebPages</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\src\System.Web.Helpers\System.Web.Helpers.csproj">
+ <Project>{9B7E3740-6161-4548-833C-4BBCA43B970E}</Project>
+ <Name>System.Web.Helpers</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\Microsoft.TestCommon\Microsoft.TestCommon.csproj">
+ <Project>{FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0}</Project>
+ <Name>Microsoft.TestCommon</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\HiRes.jpg" />
+ <EmbeddedResource Include="TestFiles\LambdaFinal.jpg" />
+ <EmbeddedResource Include="TestFiles\logo.bmp" />
+ <EmbeddedResource Include="TestFiles\NETLogo.png" />
+ <EmbeddedResource Include="TestFiles\xhtml11-flat.dtd" />
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="packages.config" />
+ </ItemGroup>
+ <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+</Project> \ No newline at end of file
diff --git a/test/System.Web.Helpers.Test/TestFiles/HiRes.jpg b/test/System.Web.Helpers.Test/TestFiles/HiRes.jpg
new file mode 100644
index 00000000..d3d8e5b1
--- /dev/null
+++ b/test/System.Web.Helpers.Test/TestFiles/HiRes.jpg
Binary files differ
diff --git a/test/System.Web.Helpers.Test/TestFiles/LambdaFinal.jpg b/test/System.Web.Helpers.Test/TestFiles/LambdaFinal.jpg
new file mode 100644
index 00000000..9f32884f
--- /dev/null
+++ b/test/System.Web.Helpers.Test/TestFiles/LambdaFinal.jpg
Binary files differ
diff --git a/test/System.Web.Helpers.Test/TestFiles/NETLogo.png b/test/System.Web.Helpers.Test/TestFiles/NETLogo.png
new file mode 100644
index 00000000..9749ab86
--- /dev/null
+++ b/test/System.Web.Helpers.Test/TestFiles/NETLogo.png
Binary files differ
diff --git a/test/System.Web.Helpers.Test/TestFiles/logo.bmp b/test/System.Web.Helpers.Test/TestFiles/logo.bmp
new file mode 100644
index 00000000..6c70a296
--- /dev/null
+++ b/test/System.Web.Helpers.Test/TestFiles/logo.bmp
Binary files differ
diff --git a/test/System.Web.Helpers.Test/TestFiles/xhtml11-flat.dtd b/test/System.Web.Helpers.Test/TestFiles/xhtml11-flat.dtd
new file mode 100644
index 00000000..b9f5881e
--- /dev/null
+++ b/test/System.Web.Helpers.Test/TestFiles/xhtml11-flat.dtd
@@ -0,0 +1,4513 @@
+<!-- ....................................................................... -->
+<!-- XHTML 1.1 DTD ........................................................ -->
+<!-- file: xhtml11.dtd
+-->
+
+<!-- XHTML 1.1 DTD
+
+ This is XHTML, a reformulation of HTML as a modular XML application.
+
+ The Extensible HyperText Markup Language (XHTML)
+ Copyright 1998-2000 World Wide Web Consortium
+ (Massachusetts Institute of Technology, Institut National de
+ Recherche en Informatique et en Automatique, Keio University).
+ All Rights Reserved.
+
+ Permission to use, copy, modify and distribute the XHTML DTD and its
+ accompanying documentation for any purpose and without fee is hereby
+ granted in perpetuity, provided that the above copyright notice and
+ this paragraph appear in all copies. The copyright holders make no
+ representation about the suitability of the DTD for any purpose.
+
+ It is provided "as is" without expressed or implied warranty.
+
+ Author: Murray M. Altheim <altheim@eng.sun.com>
+ Revision: $Id: xhtml11.dtd,v 1.20 2001/04/05 14:20:51 ahby Exp $
+
+-->
+<!-- This is the driver file for version 1.1 of the XHTML DTD.
+
+ Please use this formal public identifier to identify it:
+
+ "-//W3C//DTD XHTML 1.1//EN"
+-->
+<!ENTITY % XHTML.version "-//W3C//DTD XHTML 1.1//EN" >
+
+<!-- Use this URI to identify the default namespace:
+
+ "http://www.w3.org/1999/xhtml"
+
+ See the Qualified Names module for information
+ on the use of namespace prefixes in the DTD.
+-->
+<!ENTITY % NS.prefixed "IGNORE" >
+<!ENTITY % XHTML.prefix "" >
+
+<!-- Reserved for use with the XLink namespace:
+-->
+<!ENTITY % XLINK.xmlns "" >
+<!ENTITY % XLINK.xmlns.attrib "" >
+
+<!-- For example, if you are using XHTML 1.1 directly, use the FPI
+ in the DOCTYPE declaration, with the xmlns attribute on the
+ document element to identify the default namespace:
+
+ <?xml version="1.0"?>
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "xhtml11.dtd">
+ <html xmlns="http://www.w3.org/1999/xhtml"
+ xml:lang="en">
+ ...
+ </html>
+
+ Revisions:
+ (none)
+-->
+
+<!-- reserved for future use with document profiles -->
+<!ENTITY % XHTML.profile "" >
+
+<!-- Bidirectional Text features
+ This feature-test entity is used to declare elements
+ and attributes used for bidirectional text support.
+-->
+<!ENTITY % XHTML.bidi "INCLUDE" >
+
+<?doc type="doctype" role="title" { XHTML 1.1 } ?>
+
+<!-- ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: -->
+
+<!-- Pre-Framework Redeclaration placeholder .................... -->
+<!-- this serves as a location to insert markup declarations
+ into the DTD prior to the framework declarations.
+-->
+<!ENTITY % xhtml-prefw-redecl.module "IGNORE" >
+<![%xhtml-prefw-redecl.module;[
+%xhtml-prefw-redecl.mod;
+<!-- end of xhtml-prefw-redecl.module -->]]>
+
+<!ENTITY % xhtml-events.module "INCLUDE" >
+
+<!-- Inline Style Module ........................................ -->
+<!ENTITY % xhtml-inlstyle.module "INCLUDE" >
+<![%xhtml-inlstyle.module;[
+<!ENTITY % xhtml-inlstyle.mod
+ PUBLIC "-//W3C//ELEMENTS XHTML Inline Style 1.0//EN"
+ "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-inlstyle-1.mod" >
+<!-- ...................................................................... -->
+<!-- XHTML Inline Style Module ........................................... -->
+<!-- file: xhtml-inlstyle-1.mod
+
+ This is XHTML, a reformulation of HTML as a modular XML application.
+ Copyright 1998-2001 W3C (MIT, INRIA, Keio), All Rights Reserved.
+ Revision: $Id: xhtml-inlstyle-1.mod,v 4.0 2001/04/02 22:42:49 altheim Exp $
+
+ This DTD module is identified by the PUBLIC and SYSTEM identifiers:
+
+ PUBLIC "-//W3C//ENTITIES XHTML Inline Style 1.0//EN"
+ SYSTEM "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-inlstyle-1.mod"
+
+ Revisions:
+ (none)
+ ....................................................................... -->
+
+<!-- Inline Style
+
+ This module declares the 'style' attribute, used to support inline
+ style markup. This module must be instantiated prior to the XHTML
+ Common Attributes module in order to be included in %Core.attrib;.
+-->
+
+<!ENTITY % style.attrib
+ "style CDATA #IMPLIED"
+>
+
+
+<!ENTITY % Core.extra.attrib
+ "%style.attrib;"
+>
+
+<!-- end of xhtml-inlstyle-1.mod -->
+]]>
+
+<!-- declare Document Model module instantiated in framework
+-->
+<!ENTITY % xhtml-model.mod
+ PUBLIC "-//W3C//ENTITIES XHTML 1.1 Document Model 1.0//EN"
+ "xhtml11-model-1.mod" >
+
+<!-- Modular Framework Module (required) ......................... -->
+<!ENTITY % xhtml-framework.module "INCLUDE" >
+<![%xhtml-framework.module;[
+<!ENTITY % xhtml-framework.mod
+ PUBLIC "-//W3C//ENTITIES XHTML Modular Framework 1.0//EN"
+ "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-framework-1.mod" >
+<!-- ...................................................................... -->
+<!-- XHTML Modular Framework Module ...................................... -->
+<!-- file: xhtml-framework-1.mod
+
+ This is XHTML, a reformulation of HTML as a modular XML application.
+ Copyright 1998-2001 W3C (MIT, INRIA, Keio), All Rights Reserved.
+ Revision: $Id: xhtml-framework-1.mod,v 4.0 2001/04/02 22:42:49 altheim Exp $ SMI
+
+ This DTD module is identified by the PUBLIC and SYSTEM identifiers:
+
+ PUBLIC "-//W3C//ENTITIES XHTML Modular Framework 1.0//EN"
+ SYSTEM "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-framework-1.mod"
+
+ Revisions:
+ (none)
+ ....................................................................... -->
+
+<!-- Modular Framework
+
+ This required module instantiates the modules needed
+ to support the XHTML modularization model, including:
+
+ + notations
+ + datatypes
+ + namespace-qualified names
+ + common attributes
+ + document model
+ + character entities
+
+ The Intrinsic Events module is ignored by default but
+ occurs in this module because it must be instantiated
+ prior to Attributes but after Datatypes.
+-->
+
+<!ENTITY % xhtml-arch.module "IGNORE" >
+<![%xhtml-arch.module;[
+<!ENTITY % xhtml-arch.mod
+ PUBLIC "-//W3C//ELEMENTS XHTML Base Architecture 1.0//EN"
+ "xhtml-arch-1.mod" >
+%xhtml-arch.mod;]]>
+
+<!ENTITY % xhtml-notations.module "INCLUDE" >
+<![%xhtml-notations.module;[
+<!ENTITY % xhtml-notations.mod
+ PUBLIC "-//W3C//NOTATIONS XHTML Notations 1.0//EN"
+ "xhtml-notations-1.mod" >
+<!-- ...................................................................... -->
+<!-- XHTML Notations Module .............................................. -->
+<!-- file: xhtml-notations-1.mod
+
+ This is XHTML, a reformulation of HTML as a modular XML application.
+ Copyright 1998-2001 W3C (MIT, INRIA, Keio), All Rights Reserved.
+ Revision: $Id: xhtml-notations-1.mod,v 4.0 2001/04/02 22:42:49 altheim Exp $
+
+ This DTD module is identified by the PUBLIC and SYSTEM identifiers:
+
+ PUBLIC "-//W3C//NOTATIONS XHTML Notations 1.0//EN"
+ SYSTEM "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-notations-1.mod"
+
+ Revisions:
+ (none)
+ ....................................................................... -->
+
+<!-- Notations
+
+ defines the following notations, many of these imported from
+ other specifications and standards. When an existing FPI is
+ known, it is incorporated here.
+-->
+
+<!-- XML Notations ..................................... -->
+<!-- SGML and XML Notations ............................ -->
+
+<!-- W3C XML 1.0 Recommendation -->
+<!NOTATION w3c-xml
+ PUBLIC "ISO 8879//NOTATION Extensible Markup Language (XML) 1.0//EN" >
+
+<!-- XML 1.0 CDATA -->
+<!NOTATION cdata
+ PUBLIC "-//W3C//NOTATION XML 1.0: CDATA//EN" >
+
+<!-- SGML Formal Public Identifiers -->
+<!NOTATION fpi
+ PUBLIC "ISO 8879:1986//NOTATION Formal Public Identifier//EN" >
+
+<!-- XHTML Notations ................................... -->
+
+<!-- Length defined for cellpadding/cellspacing -->
+
+<!-- nn for pixels or nn% for percentage length -->
+<!NOTATION length
+ PUBLIC "-//W3C//NOTATION XHTML Datatype: Length//EN" >
+
+<!-- space-separated list of link types -->
+<!NOTATION linkTypes
+ PUBLIC "-//W3C//NOTATION XHTML Datatype: LinkTypes//EN" >
+
+<!-- single or comma-separated list of media descriptors -->
+<!NOTATION mediaDesc
+ PUBLIC "-//W3C//NOTATION XHTML Datatype: MediaDesc//EN" >
+
+<!-- pixel, percentage, or relative -->
+<!NOTATION multiLength
+ PUBLIC "-//W3C//NOTATION XHTML Datatype: MultiLength//EN" >
+
+<!-- one or more digits (NUMBER) -->
+<!NOTATION number
+ PUBLIC "-//W3C//NOTATION XHTML Datatype: Number//EN" >
+
+<!-- integer representing length in pixels -->
+<!NOTATION pixels
+ PUBLIC "-//W3C//NOTATION XHTML Datatype: Pixels//EN" >
+
+<!-- script expression -->
+<!NOTATION script
+ PUBLIC "-//W3C//NOTATION XHTML Datatype: Script//EN" >
+
+<!-- textual content -->
+<!NOTATION text
+ PUBLIC "-//W3C//NOTATION XHTML Datatype: Text//EN" >
+
+<!-- Imported Notations ................................ -->
+
+<!-- a single character from [ISO10646] -->
+<!NOTATION character
+ PUBLIC "-//W3C//NOTATION XHTML Datatype: Character//EN" >
+
+<!-- a character encoding, as per [RFC2045] -->
+<!NOTATION charset
+ PUBLIC "-//W3C//NOTATION XHTML Datatype: Charset//EN" >
+
+<!-- a space separated list of character encodings, as per [RFC2045] -->
+<!NOTATION charsets
+ PUBLIC "-//W3C//NOTATION XHTML Datatype: Charsets//EN" >
+
+<!-- media type, as per [RFC2045] -->
+<!NOTATION contentType
+ PUBLIC "-//W3C//NOTATION XHTML Datatype: ContentType//EN" >
+
+<!-- comma-separated list of media types, as per [RFC2045] -->
+<!NOTATION contentTypes
+ PUBLIC "-//W3C//NOTATION XHTML Datatype: ContentTypes//EN" >
+
+<!-- date and time information. ISO date format -->
+<!NOTATION datetime
+ PUBLIC "-//W3C//NOTATION XHTML Datatype: Datetime//EN" >
+
+<!-- a language code, as per [RFC3066] -->
+<!NOTATION languageCode
+ PUBLIC "-//W3C//NOTATION XHTML Datatype: LanguageCode//EN" >
+
+<!-- a Uniform Resource Identifier, see [URI] -->
+<!NOTATION uri
+ PUBLIC "-//W3C//NOTATION XHTML Datatype: URI//EN" >
+
+<!-- a space-separated list of Uniform Resource Identifiers, see [URI] -->
+<!NOTATION uris
+ PUBLIC "-//W3C//NOTATION XHTML Datatype: URIs//EN" >
+
+<!-- end of xhtml-notations-1.mod -->
+]]>
+
+<!ENTITY % xhtml-datatypes.module "INCLUDE" >
+<![%xhtml-datatypes.module;[
+<!ENTITY % xhtml-datatypes.mod
+ PUBLIC "-//W3C//ENTITIES XHTML Datatypes 1.0//EN"
+ "xhtml-datatypes-1.mod" >
+<!-- ...................................................................... -->
+<!-- XHTML Datatypes Module .............................................. -->
+<!-- file: xhtml-datatypes-1.mod
+
+ This is XHTML, a reformulation of HTML as a modular XML application.
+ Copyright 1998-2001 W3C (MIT, INRIA, Keio), All Rights Reserved.
+ Revision: $Id: xhtml-datatypes-1.mod,v 4.0 2001/04/02 22:42:49 altheim Exp $ SMI
+
+ This DTD module is identified by the PUBLIC and SYSTEM identifiers:
+
+ PUBLIC "-//W3C//ENTITIES XHTML Datatypes 1.0//EN"
+ SYSTEM "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-datatypes-1.mod"
+
+ Revisions:
+ (none)
+ ....................................................................... -->
+
+<!-- Datatypes
+
+ defines containers for the following datatypes, many of
+ these imported from other specifications and standards.
+-->
+
+<!-- Length defined for cellpadding/cellspacing -->
+
+<!-- nn for pixels or nn% for percentage length -->
+<!ENTITY % Length.datatype "CDATA" >
+
+<!-- space-separated list of link types -->
+<!ENTITY % LinkTypes.datatype "NMTOKENS" >
+
+<!-- single or comma-separated list of media descriptors -->
+<!ENTITY % MediaDesc.datatype "CDATA" >
+
+<!-- pixel, percentage, or relative -->
+<!ENTITY % MultiLength.datatype "CDATA" >
+
+<!-- one or more digits (NUMBER) -->
+<!ENTITY % Number.datatype "CDATA" >
+
+<!-- integer representing length in pixels -->
+<!ENTITY % Pixels.datatype "CDATA" >
+
+<!-- script expression -->
+<!ENTITY % Script.datatype "CDATA" >
+
+<!-- textual content -->
+<!ENTITY % Text.datatype "CDATA" >
+
+<!-- Imported Datatypes ................................ -->
+
+<!-- a single character from [ISO10646] -->
+<!ENTITY % Character.datatype "CDATA" >
+
+<!-- a character encoding, as per [RFC2045] -->
+<!ENTITY % Charset.datatype "CDATA" >
+
+<!-- a space separated list of character encodings, as per [RFC2045] -->
+<!ENTITY % Charsets.datatype "CDATA" >
+
+<!-- media type, as per [RFC2045] -->
+<!ENTITY % ContentType.datatype "CDATA" >
+
+<!-- comma-separated list of media types, as per [RFC2045] -->
+<!ENTITY % ContentTypes.datatype "CDATA" >
+
+<!-- date and time information. ISO date format -->
+<!ENTITY % Datetime.datatype "CDATA" >
+
+<!-- formal public identifier, as per [ISO8879] -->
+<!ENTITY % FPI.datatype "CDATA" >
+
+<!-- a language code, as per [RFC3066] -->
+<!ENTITY % LanguageCode.datatype "NMTOKEN" >
+
+<!-- a Uniform Resource Identifier, see [URI] -->
+<!ENTITY % URI.datatype "CDATA" >
+
+<!-- a space-separated list of Uniform Resource Identifiers, see [URI] -->
+<!ENTITY % URIs.datatype "CDATA" >
+
+<!-- end of xhtml-datatypes-1.mod -->
+]]>
+
+<!-- placeholder for XLink support module -->
+<!ENTITY % xhtml-xlink.mod "" >
+
+
+<!ENTITY % xhtml-qname.module "INCLUDE" >
+<![%xhtml-qname.module;[
+<!ENTITY % xhtml-qname.mod
+ PUBLIC "-//W3C//ENTITIES XHTML Qualified Names 1.0//EN"
+ "xhtml-qname-1.mod" >
+<!-- ....................................................................... -->
+<!-- XHTML Qname Module ................................................... -->
+<!-- file: xhtml-qname-1.mod
+
+ This is XHTML, a reformulation of HTML as a modular XML application.
+ Copyright 1998-2001 W3C (MIT, INRIA, Keio), All Rights Reserved.
+ Revision: $Id: xhtml-qname-1.mod,v 4.0 2001/04/02 22:42:49 altheim Exp $ SMI
+
+ This DTD module is identified by the PUBLIC and SYSTEM identifiers:
+
+ PUBLIC "-//W3C//ENTITIES XHTML Qualified Names 1.0//EN"
+ SYSTEM "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-qname-1.mod"
+
+ Revisions:
+#2000-10-22: added qname declarations for ruby elements
+ ....................................................................... -->
+
+<!-- XHTML Qname (Qualified Name) Module
+
+ This module is contained in two parts, labeled Section 'A' and 'B':
+
+ Section A declares parameter entities to support namespace-
+ qualified names, namespace declarations, and name prefixing
+ for XHTML and extensions.
+
+ Section B declares parameter entities used to provide
+ namespace-qualified names for all XHTML element types:
+
+ %applet.qname; the xmlns-qualified name for <applet>
+ %base.qname; the xmlns-qualified name for <base>
+ ...
+
+ XHTML extensions would create a module similar to this one.
+ Included in the XHTML distribution is a template module
+ ('template-qname-1.mod') suitable for this purpose.
+-->
+
+<!-- Section A: XHTML XML Namespace Framework :::::::::::::::::::: -->
+
+<!-- 1. Declare a %XHTML.prefixed; conditional section keyword, used
+ to activate namespace prefixing. The default value should
+ inherit '%NS.prefixed;' from the DTD driver, so that unless
+ overridden, the default behaviour follows the overall DTD
+ prefixing scheme.
+-->
+<!ENTITY % NS.prefixed "IGNORE" >
+<!ENTITY % XHTML.prefixed "%NS.prefixed;" >
+
+<!-- 2. Declare a parameter entity (eg., %XHTML.xmlns;) containing
+ the URI reference used to identify the XHTML namespace:
+-->
+<!ENTITY % XHTML.xmlns "http://www.w3.org/1999/xhtml" >
+
+<!-- 3. Declare parameter entities (eg., %XHTML.prefix;) containing
+ the default namespace prefix string(s) to use when prefixing
+ is enabled. This may be overridden in the DTD driver or the
+ internal subset of an document instance. If no default prefix
+ is desired, this may be declared as an empty string.
+
+ NOTE: As specified in [XMLNAMES], the namespace prefix serves
+ as a proxy for the URI reference, and is not in itself significant.
+-->
+<!ENTITY % XHTML.prefix "" >
+
+<!-- 4. Declare parameter entities (eg., %XHTML.pfx;) containing the
+ colonized prefix(es) (eg., '%XHTML.prefix;:') used when
+ prefixing is active, an empty string when it is not.
+-->
+<![%XHTML.prefixed;[
+<!ENTITY % XHTML.pfx "%XHTML.prefix;:" >
+]]>
+<!ENTITY % XHTML.pfx "" >
+
+<!-- declare qualified name extensions here ............ -->
+<!ENTITY % xhtml-qname-extra.mod "" >
+
+
+<!-- 5. The parameter entity %XHTML.xmlns.extra.attrib; may be
+ redeclared to contain any non-XHTML namespace declaration
+ attributes for namespaces embedded in XHTML. The default
+ is an empty string. XLink should be included here if used
+ in the DTD.
+-->
+<!ENTITY % XHTML.xmlns.extra.attrib "" >
+
+<!-- The remainder of Section A is only followed in XHTML, not extensions. -->
+
+<!-- Declare a parameter entity %NS.decl.attrib; containing
+ all XML Namespace declarations used in the DTD, plus the
+ xmlns declaration for XHTML, its form dependent on whether
+ prefixing is active.
+-->
+<![%XHTML.prefixed;[
+<!ENTITY % NS.decl.attrib
+ "xmlns:%XHTML.prefix; %URI.datatype; #FIXED '%XHTML.xmlns;'
+ %XHTML.xmlns.extra.attrib;"
+>
+]]>
+<!ENTITY % NS.decl.attrib
+ "%XHTML.xmlns.extra.attrib;"
+>
+
+<!-- This is a placeholder for future XLink support.
+-->
+<!ENTITY % XLINK.xmlns.attrib "" >
+
+<!-- Declare a parameter entity %NS.decl.attrib; containing all
+ XML namespace declaration attributes used by XHTML, including
+ a default xmlns attribute when prefixing is inactive.
+-->
+<![%XHTML.prefixed;[
+<!ENTITY % XHTML.xmlns.attrib
+ "%NS.decl.attrib;
+ %XLINK.xmlns.attrib;"
+>
+]]>
+<!ENTITY % XHTML.xmlns.attrib
+ "xmlns %URI.datatype; #FIXED '%XHTML.xmlns;'
+ %XLINK.xmlns.attrib;"
+>
+
+<!-- placeholder for qualified name redeclarations -->
+<!ENTITY % xhtml-qname.redecl "" >
+
+
+<!-- Section B: XHTML Qualified Names ::::::::::::::::::::::::::::: -->
+
+<!-- 6. This section declares parameter entities used to provide
+ namespace-qualified names for all XHTML element types.
+-->
+
+<!-- module: xhtml-applet-1.mod -->
+<!ENTITY % applet.qname "%XHTML.pfx;applet" >
+
+<!-- module: xhtml-base-1.mod -->
+<!ENTITY % base.qname "%XHTML.pfx;base" >
+
+<!-- module: xhtml-bdo-1.mod -->
+<!ENTITY % bdo.qname "%XHTML.pfx;bdo" >
+
+<!-- module: xhtml-blkphras-1.mod -->
+<!ENTITY % address.qname "%XHTML.pfx;address" >
+<!ENTITY % blockquote.qname "%XHTML.pfx;blockquote" >
+<!ENTITY % pre.qname "%XHTML.pfx;pre" >
+<!ENTITY % h1.qname "%XHTML.pfx;h1" >
+<!ENTITY % h2.qname "%XHTML.pfx;h2" >
+<!ENTITY % h3.qname "%XHTML.pfx;h3" >
+<!ENTITY % h4.qname "%XHTML.pfx;h4" >
+<!ENTITY % h5.qname "%XHTML.pfx;h5" >
+<!ENTITY % h6.qname "%XHTML.pfx;h6" >
+
+<!-- module: xhtml-blkpres-1.mod -->
+<!ENTITY % hr.qname "%XHTML.pfx;hr" >
+
+<!-- module: xhtml-blkstruct-1.mod -->
+<!ENTITY % div.qname "%XHTML.pfx;div" >
+<!ENTITY % p.qname "%XHTML.pfx;p" >
+
+<!-- module: xhtml-edit-1.mod -->
+<!ENTITY % ins.qname "%XHTML.pfx;ins" >
+<!ENTITY % del.qname "%XHTML.pfx;del" >
+
+<!-- module: xhtml-form-1.mod -->
+<!ENTITY % form.qname "%XHTML.pfx;form" >
+<!ENTITY % label.qname "%XHTML.pfx;label" >
+<!ENTITY % input.qname "%XHTML.pfx;input" >
+<!ENTITY % select.qname "%XHTML.pfx;select" >
+<!ENTITY % optgroup.qname "%XHTML.pfx;optgroup" >
+<!ENTITY % option.qname "%XHTML.pfx;option" >
+<!ENTITY % textarea.qname "%XHTML.pfx;textarea" >
+<!ENTITY % fieldset.qname "%XHTML.pfx;fieldset" >
+<!ENTITY % legend.qname "%XHTML.pfx;legend" >
+<!ENTITY % button.qname "%XHTML.pfx;button" >
+
+<!-- module: xhtml-hypertext-1.mod -->
+<!ENTITY % a.qname "%XHTML.pfx;a" >
+
+<!-- module: xhtml-image-1.mod -->
+<!ENTITY % img.qname "%XHTML.pfx;img" >
+
+<!-- module: xhtml-inlphras-1.mod -->
+<!ENTITY % abbr.qname "%XHTML.pfx;abbr" >
+<!ENTITY % acronym.qname "%XHTML.pfx;acronym" >
+<!ENTITY % cite.qname "%XHTML.pfx;cite" >
+<!ENTITY % code.qname "%XHTML.pfx;code" >
+<!ENTITY % dfn.qname "%XHTML.pfx;dfn" >
+<!ENTITY % em.qname "%XHTML.pfx;em" >
+<!ENTITY % kbd.qname "%XHTML.pfx;kbd" >
+<!ENTITY % q.qname "%XHTML.pfx;q" >
+<!ENTITY % samp.qname "%XHTML.pfx;samp" >
+<!ENTITY % strong.qname "%XHTML.pfx;strong" >
+<!ENTITY % var.qname "%XHTML.pfx;var" >
+
+<!-- module: xhtml-inlpres-1.mod -->
+<!ENTITY % b.qname "%XHTML.pfx;b" >
+<!ENTITY % big.qname "%XHTML.pfx;big" >
+<!ENTITY % i.qname "%XHTML.pfx;i" >
+<!ENTITY % small.qname "%XHTML.pfx;small" >
+<!ENTITY % sub.qname "%XHTML.pfx;sub" >
+<!ENTITY % sup.qname "%XHTML.pfx;sup" >
+<!ENTITY % tt.qname "%XHTML.pfx;tt" >
+
+<!-- module: xhtml-inlstruct-1.mod -->
+<!ENTITY % br.qname "%XHTML.pfx;br" >
+<!ENTITY % span.qname "%XHTML.pfx;span" >
+
+<!-- module: xhtml-ismap-1.mod (also csismap, ssismap) -->
+<!ENTITY % map.qname "%XHTML.pfx;map" >
+<!ENTITY % area.qname "%XHTML.pfx;area" >
+
+<!-- module: xhtml-link-1.mod -->
+<!ENTITY % link.qname "%XHTML.pfx;link" >
+
+<!-- module: xhtml-list-1.mod -->
+<!ENTITY % dl.qname "%XHTML.pfx;dl" >
+<!ENTITY % dt.qname "%XHTML.pfx;dt" >
+<!ENTITY % dd.qname "%XHTML.pfx;dd" >
+<!ENTITY % ol.qname "%XHTML.pfx;ol" >
+<!ENTITY % ul.qname "%XHTML.pfx;ul" >
+<!ENTITY % li.qname "%XHTML.pfx;li" >
+
+<!-- module: xhtml-meta-1.mod -->
+<!ENTITY % meta.qname "%XHTML.pfx;meta" >
+
+<!-- module: xhtml-param-1.mod -->
+<!ENTITY % param.qname "%XHTML.pfx;param" >
+
+<!-- module: xhtml-object-1.mod -->
+<!ENTITY % object.qname "%XHTML.pfx;object" >
+
+<!-- module: xhtml-script-1.mod -->
+<!ENTITY % script.qname "%XHTML.pfx;script" >
+<!ENTITY % noscript.qname "%XHTML.pfx;noscript" >
+
+<!-- module: xhtml-struct-1.mod -->
+<!ENTITY % html.qname "%XHTML.pfx;html" >
+<!ENTITY % head.qname "%XHTML.pfx;head" >
+<!ENTITY % title.qname "%XHTML.pfx;title" >
+<!ENTITY % body.qname "%XHTML.pfx;body" >
+
+<!-- module: xhtml-style-1.mod -->
+<!ENTITY % style.qname "%XHTML.pfx;style" >
+
+<!-- module: xhtml-table-1.mod -->
+<!ENTITY % table.qname "%XHTML.pfx;table" >
+<!ENTITY % caption.qname "%XHTML.pfx;caption" >
+<!ENTITY % thead.qname "%XHTML.pfx;thead" >
+<!ENTITY % tfoot.qname "%XHTML.pfx;tfoot" >
+<!ENTITY % tbody.qname "%XHTML.pfx;tbody" >
+<!ENTITY % colgroup.qname "%XHTML.pfx;colgroup" >
+<!ENTITY % col.qname "%XHTML.pfx;col" >
+<!ENTITY % tr.qname "%XHTML.pfx;tr" >
+<!ENTITY % th.qname "%XHTML.pfx;th" >
+<!ENTITY % td.qname "%XHTML.pfx;td" >
+
+<!-- module: xhtml-ruby-1.mod -->
+
+<!ENTITY % ruby.qname "%XHTML.pfx;ruby" >
+<!ENTITY % rbc.qname "%XHTML.pfx;rbc" >
+<!ENTITY % rtc.qname "%XHTML.pfx;rtc" >
+<!ENTITY % rb.qname "%XHTML.pfx;rb" >
+<!ENTITY % rt.qname "%XHTML.pfx;rt" >
+<!ENTITY % rp.qname "%XHTML.pfx;rp" >
+
+<!-- Provisional XHTML 2.0 Qualified Names ...................... -->
+
+<!-- module: xhtml-image-2.mod -->
+<!ENTITY % alt.qname "%XHTML.pfx;alt" >
+
+<!-- end of xhtml-qname-1.mod -->
+]]>
+
+<!ENTITY % xhtml-events.module "IGNORE" >
+<![%xhtml-events.module;[
+<!ENTITY % xhtml-events.mod
+ PUBLIC "-//W3C//ENTITIES XHTML Intrinsic Events 1.0//EN"
+ "xhtml-events-1.mod" >
+<!-- ...................................................................... -->
+<!-- XHTML Intrinsic Events Module ....................................... -->
+<!-- file: xhtml-events-1.mod
+
+ This is XHTML, a reformulation of HTML as a modular XML application.
+ Copyright 1998-2001 W3C (MIT, INRIA, Keio), All Rights Reserved.
+ Revision: $Id: xhtml-events-1.mod,v 4.0 2001/04/02 22:42:49 altheim Exp $ SMI
+
+ This DTD module is identified by the PUBLIC and SYSTEM identifiers:
+
+ PUBLIC "-//W3C//ENTITIES XHTML Intrinsic Events 1.0//EN"
+ SYSTEM "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-events-1.mod"
+
+ Revisions:
+ (none)
+ ....................................................................... -->
+
+<!-- Intrinsic Event Attributes
+
+ These are the event attributes defined in HTML 4.0,
+ Section 18.2.3 "Intrinsic Events". This module must be
+ instantiated prior to the Attributes Module but after
+ the Datatype Module in the Modular Framework module.
+
+ "Note: Authors of HTML documents are advised that changes
+ are likely to occur in the realm of intrinsic events
+ (e.g., how scripts are bound to events). Research in
+ this realm is carried on by members of the W3C Document
+ Object Model Working Group (see the W3C Web site at
+ http://www.w3.org/ for more information)."
+-->
+<!-- NOTE: Because the ATTLIST declarations in this module occur
+ before their respective ELEMENT declarations in other
+ modules, there may be a dependency on this module that
+ should be considered if any of the parameter entities used
+ for element type names (eg., %a.qname;) are redeclared.
+-->
+
+<!ENTITY % Events.attrib
+ "onclick %Script.datatype; #IMPLIED
+ ondblclick %Script.datatype; #IMPLIED
+ onmousedown %Script.datatype; #IMPLIED
+ onmouseup %Script.datatype; #IMPLIED
+ onmouseover %Script.datatype; #IMPLIED
+ onmousemove %Script.datatype; #IMPLIED
+ onmouseout %Script.datatype; #IMPLIED
+ onkeypress %Script.datatype; #IMPLIED
+ onkeydown %Script.datatype; #IMPLIED
+ onkeyup %Script.datatype; #IMPLIED"
+>
+
+<!-- additional attributes on anchor element
+-->
+<!ATTLIST %a.qname;
+ onfocus %Script.datatype; #IMPLIED
+ onblur %Script.datatype; #IMPLIED
+>
+
+<!-- additional attributes on form element
+-->
+<!ATTLIST %form.qname;
+ onsubmit %Script.datatype; #IMPLIED
+ onreset %Script.datatype; #IMPLIED
+>
+
+<!-- additional attributes on label element
+-->
+<!ATTLIST %label.qname;
+ onfocus %Script.datatype; #IMPLIED
+ onblur %Script.datatype; #IMPLIED
+>
+
+<!-- additional attributes on input element
+-->
+<!ATTLIST %input.qname;
+ onfocus %Script.datatype; #IMPLIED
+ onblur %Script.datatype; #IMPLIED
+ onselect %Script.datatype; #IMPLIED
+ onchange %Script.datatype; #IMPLIED
+>
+
+<!-- additional attributes on select element
+-->
+<!ATTLIST %select.qname;
+ onfocus %Script.datatype; #IMPLIED
+ onblur %Script.datatype; #IMPLIED
+ onchange %Script.datatype; #IMPLIED
+>
+
+<!-- additional attributes on textarea element
+-->
+<!ATTLIST %textarea.qname;
+ onfocus %Script.datatype; #IMPLIED
+ onblur %Script.datatype; #IMPLIED
+ onselect %Script.datatype; #IMPLIED
+ onchange %Script.datatype; #IMPLIED
+>
+
+<!-- additional attributes on button element
+-->
+<!ATTLIST %button.qname;
+ onfocus %Script.datatype; #IMPLIED
+ onblur %Script.datatype; #IMPLIED
+>
+
+<!-- additional attributes on body element
+-->
+<!ATTLIST %body.qname;
+ onload %Script.datatype; #IMPLIED
+ onunload %Script.datatype; #IMPLIED
+>
+
+<!-- additional attributes on area element
+-->
+<!ATTLIST %area.qname;
+ onfocus %Script.datatype; #IMPLIED
+ onblur %Script.datatype; #IMPLIED
+>
+
+<!-- end of xhtml-events-1.mod -->
+]]>
+
+<!ENTITY % xhtml-attribs.module "INCLUDE" >
+<![%xhtml-attribs.module;[
+<!ENTITY % xhtml-attribs.mod
+ PUBLIC "-//W3C//ENTITIES XHTML Common Attributes 1.0//EN"
+ "xhtml-attribs-1.mod" >
+<!-- ...................................................................... -->
+<!-- XHTML Common Attributes Module ...................................... -->
+<!-- file: xhtml-attribs-1.mod
+
+ This is XHTML, a reformulation of HTML as a modular XML application.
+ Copyright 1998-2001 W3C (MIT, INRIA, Keio), All Rights Reserved.
+ Revision: $Id: xhtml-attribs-1.mod,v 4.0 2001/04/02 22:42:49 altheim Exp $ SMI
+
+ This DTD module is identified by the PUBLIC and SYSTEM identifiers:
+
+ PUBLIC "-//W3C//ENTITIES XHTML Common Attributes 1.0//EN"
+ SYSTEM "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-attribs-1.mod"
+
+ Revisions:
+ (none)
+ ....................................................................... -->
+
+<!-- Common Attributes
+
+ This module declares many of the common attributes for the XHTML DTD.
+ %NS.decl.attrib; is declared in the XHTML Qname module.
+-->
+
+<!ENTITY % id.attrib
+ "id ID #IMPLIED"
+>
+
+<!ENTITY % class.attrib
+ "class NMTOKENS #IMPLIED"
+>
+
+<!ENTITY % title.attrib
+ "title %Text.datatype; #IMPLIED"
+>
+
+<!ENTITY % Core.extra.attrib "" >
+
+<!ENTITY % Core.attrib
+ "%XHTML.xmlns.attrib;
+ %id.attrib;
+ %class.attrib;
+ %title.attrib;
+ %Core.extra.attrib;"
+>
+
+<!ENTITY % lang.attrib
+ "xml:lang %LanguageCode.datatype; #IMPLIED"
+>
+
+<![%XHTML.bidi;[
+<!ENTITY % dir.attrib
+ "dir ( ltr | rtl ) #IMPLIED"
+>
+
+<!ENTITY % I18n.attrib
+ "%dir.attrib;
+ %lang.attrib;"
+>
+
+]]>
+<!ENTITY % I18n.attrib
+ "%lang.attrib;"
+>
+
+<!ENTITY % Common.extra.attrib "" >
+
+<!-- intrinsic event attributes declared previously
+-->
+<!ENTITY % Events.attrib "" >
+
+<!ENTITY % Common.attrib
+ "%Core.attrib;
+ %I18n.attrib;
+ %Events.attrib;
+ %Common.extra.attrib;"
+>
+
+<!-- end of xhtml-attribs-1.mod -->
+]]>
+
+<!-- placeholder for content model redeclarations -->
+<!ENTITY % xhtml-model.redecl "" >
+
+
+<!ENTITY % xhtml-model.module "INCLUDE" >
+<![%xhtml-model.module;[
+<!-- instantiate the Document Model module declared in the DTD driver
+-->
+<!-- ....................................................................... -->
+<!-- XHTML 1.1 Document Model Module ...................................... -->
+<!-- file: xhtml11-model-1.mod
+
+ This is XHTML 1.1, a reformulation of HTML as a modular XML application.
+ Copyright 1998-2000 W3C (MIT, INRIA, Keio), All Rights Reserved.
+ Revision: $Id: xhtml11-model-1.mod,v 1.12 2000/11/18 18:20:25 ahby Exp $ SMI
+
+ This DTD module is identified by the PUBLIC and SYSTEM identifiers:
+
+ PUBLIC "-//W3C//ENTITIES XHTML 1.1 Document Model 1.0//EN"
+ SYSTEM "http://www.w3.org/TR/xhtml11/DTD/xhtml11-model-1.mod"
+
+ Revisions:
+ (none)
+ ....................................................................... -->
+
+<!-- XHTML 1.1 Document Model
+
+ This module describes the groupings of elements that make up
+ common content models for XHTML elements.
+
+ XHTML has three basic content models:
+
+ %Inline.mix; character-level elements
+ %Block.mix; block-like elements, eg., paragraphs and lists
+ %Flow.mix; any block or inline elements
+
+ Any parameter entities declared in this module may be used
+ to create element content models, but the above three are
+ considered 'global' (insofar as that term applies here).
+
+ The reserved word '#PCDATA' (indicating a text string) is now
+ included explicitly with each element declaration that is
+ declared as mixed content, as XML requires that this token
+ occur first in a content model specification.
+-->
+<!-- Extending the Model
+
+ While in some cases this module may need to be rewritten to
+ accommodate changes to the document model, minor extensions
+ may be accomplished by redeclaring any of the three *.extra;
+ parameter entities to contain extension element types as follows:
+
+ %Misc.extra; whose parent may be any block or
+ inline element.
+
+ %Inline.extra; whose parent may be any inline element.
+
+ %Block.extra; whose parent may be any block element.
+
+ If used, these parameter entities must be an OR-separated
+ list beginning with an OR separator ("|"), eg., "| a | b | c"
+
+ All block and inline *.class parameter entities not part
+ of the *struct.class classes begin with "| " to allow for
+ exclusion from mixes.
+-->
+
+<!-- .............. Optional Elements in head .................. -->
+
+<!ENTITY % HeadOpts.mix
+ "( %script.qname; | %style.qname; | %meta.qname;
+ | %link.qname; | %object.qname; )*"
+>
+
+<!-- ................. Miscellaneous Elements .................. -->
+
+<!-- ins and del are used to denote editing changes
+-->
+<!ENTITY % Edit.class "| %ins.qname; | %del.qname;" >
+
+<!-- script and noscript are used to contain scripts
+ and alternative content
+-->
+<!ENTITY % Script.class "| %script.qname; | %noscript.qname;" >
+
+<!ENTITY % Misc.extra "" >
+
+<!-- These elements are neither block nor inline, and can
+ essentially be used anywhere in the document body.
+-->
+<!ENTITY % Misc.class
+ "%Edit.class;
+ %Script.class;
+ %Misc.extra;"
+>
+
+<!-- .................... Inline Elements ...................... -->
+
+<!ENTITY % InlStruct.class "%br.qname; | %span.qname;" >
+
+<!ENTITY % InlPhras.class
+ "| %em.qname; | %strong.qname; | %dfn.qname; | %code.qname;
+ | %samp.qname; | %kbd.qname; | %var.qname; | %cite.qname;
+ | %abbr.qname; | %acronym.qname; | %q.qname;" >
+
+<!ENTITY % InlPres.class
+ "| %tt.qname; | %i.qname; | %b.qname; | %big.qname;
+ | %small.qname; | %sub.qname; | %sup.qname;" >
+
+<!ENTITY % I18n.class "| %bdo.qname;" >
+
+<!ENTITY % Anchor.class "| %a.qname;" >
+
+<!ENTITY % InlSpecial.class
+ "| %img.qname; | %map.qname;
+ | %object.qname;" >
+
+<!ENTITY % InlForm.class
+ "| %input.qname; | %select.qname; | %textarea.qname;
+ | %label.qname; | %button.qname;" >
+
+<!ENTITY % Inline.extra "" >
+
+<!ENTITY % Ruby.class "| %ruby.qname;" >
+
+<!-- %Inline.class; includes all inline elements,
+ used as a component in mixes
+-->
+<!ENTITY % Inline.class
+ "%InlStruct.class;
+ %InlPhras.class;
+ %InlPres.class;
+ %I18n.class;
+ %Anchor.class;
+ %InlSpecial.class;
+ %InlForm.class;
+ %Ruby.class;
+ %Inline.extra;"
+>
+
+<!-- %InlNoRuby.class; includes all inline elements
+ except ruby, used as a component in mixes
+-->
+<!ENTITY % InlNoRuby.class
+ "%InlStruct.class;
+ %InlPhras.class;
+ %InlPres.class;
+ %I18n.class;
+ %Anchor.class;
+ %InlSpecial.class;
+ %InlForm.class;
+ %Inline.extra;"
+>
+
+<!-- %NoRuby.content; includes all inlines except ruby
+-->
+<!ENTITY % NoRuby.content
+ "( #PCDATA
+ | %InlNoRuby.class;
+ %Misc.class; )*"
+>
+
+<!-- %InlNoAnchor.class; includes all non-anchor inlines,
+ used as a component in mixes
+-->
+<!ENTITY % InlNoAnchor.class
+ "%InlStruct.class;
+ %InlPhras.class;
+ %InlPres.class;
+ %I18n.class;
+ %InlSpecial.class;
+ %InlForm.class;
+ %Ruby.class;
+ %Inline.extra;"
+>
+
+<!-- %InlNoAnchor.mix; includes all non-anchor inlines
+-->
+<!ENTITY % InlNoAnchor.mix
+ "%InlNoAnchor.class;
+ %Misc.class;"
+>
+
+<!-- %Inline.mix; includes all inline elements, including %Misc.class;
+-->
+<!ENTITY % Inline.mix
+ "%Inline.class;
+ %Misc.class;"
+>
+
+<!-- ..................... Block Elements ...................... -->
+
+<!-- In the HTML 4.0 DTD, heading and list elements were included
+ in the %block; parameter entity. The %Heading.class; and
+ %List.class; parameter entities must now be included explicitly
+ on element declarations where desired.
+-->
+
+<!ENTITY % Heading.class
+ "%h1.qname; | %h2.qname; | %h3.qname;
+ | %h4.qname; | %h5.qname; | %h6.qname;" >
+
+<!ENTITY % List.class "%ul.qname; | %ol.qname; | %dl.qname;" >
+
+<!ENTITY % Table.class "| %table.qname;" >
+
+<!ENTITY % Form.class "| %form.qname;" >
+
+<!ENTITY % Fieldset.class "| %fieldset.qname;" >
+
+<!ENTITY % BlkStruct.class "%p.qname; | %div.qname;" >
+
+<!ENTITY % BlkPhras.class
+ "| %pre.qname; | %blockquote.qname; | %address.qname;" >
+
+<!ENTITY % BlkPres.class "| %hr.qname;" >
+
+<!ENTITY % BlkSpecial.class
+ "%Table.class;
+ %Form.class;
+ %Fieldset.class;"
+>
+
+<!ENTITY % Block.extra "" >
+
+<!-- %Block.class; includes all block elements,
+ used as an component in mixes
+-->
+<!ENTITY % Block.class
+ "%BlkStruct.class;
+ %BlkPhras.class;
+ %BlkPres.class;
+ %BlkSpecial.class;
+ %Block.extra;"
+>
+
+<!-- %Block.mix; includes all block elements plus %Misc.class;
+-->
+<!ENTITY % Block.mix
+ "%Heading.class;
+ | %List.class;
+ | %Block.class;
+ %Misc.class;"
+>
+
+<!-- ................ All Content Elements .................. -->
+
+<!-- %Flow.mix; includes all text content, block and inline
+-->
+<!ENTITY % Flow.mix
+ "%Heading.class;
+ | %List.class;
+ | %Block.class;
+ | %Inline.class;
+ %Misc.class;"
+>
+
+<!-- end of xhtml11-model-1.mod -->
+]]>
+
+<!ENTITY % xhtml-charent.module "INCLUDE" >
+<![%xhtml-charent.module;[
+<!ENTITY % xhtml-charent.mod
+ PUBLIC "-//W3C//ENTITIES XHTML Character Entities 1.0//EN"
+ "xhtml-charent-1.mod" >
+<!-- ...................................................................... -->
+<!-- XHTML Character Entities Module ......................................... -->
+<!-- file: xhtml-charent-1.mod
+
+ This is XHTML, a reformulation of HTML as a modular XML application.
+ Copyright 1998-2001 W3C (MIT, INRIA, Keio), All Rights Reserved.
+ Revision: $Id: xhtml-charent-1.mod,v 4.0 2001/04/02 22:42:49 altheim Exp $ SMI
+
+ This DTD module is identified by the PUBLIC and SYSTEM identifiers:
+
+ PUBLIC "-//W3C//ENTITIES XHTML Character Entities 1.0//EN"
+ SYSTEM "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-charent-1.mod"
+
+ Revisions:
+ (none)
+ ....................................................................... -->
+
+<!-- Character Entities for XHTML
+
+ This module declares the set of character entities for XHTML,
+ including the Latin 1, Symbol and Special character collections.
+-->
+
+<!ENTITY % xhtml-lat1
+ PUBLIC "-//W3C//ENTITIES Latin 1 for XHTML//EN"
+ "xhtml-lat1.ent" >
+<!-- Portions (C) International Organization for Standardization 1986
+ Permission to copy in any form is granted for use with
+ conforming SGML systems and applications as defined in
+ ISO 8879, provided this notice is included in all copies.
+-->
+<!-- Character entity set. Typical invocation:
+ <!ENTITY % HTMLlat1 PUBLIC
+ "-//W3C//ENTITIES Latin 1 for XHTML//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml-lat1.ent">
+ %HTMLlat1;
+-->
+
+<!ENTITY nbsp "&#160;"> <!-- no-break space = non-breaking space,
+ U+00A0 ISOnum -->
+<!ENTITY iexcl "&#161;"> <!-- inverted exclamation mark, U+00A1 ISOnum -->
+<!ENTITY cent "&#162;"> <!-- cent sign, U+00A2 ISOnum -->
+<!ENTITY pound "&#163;"> <!-- pound sign, U+00A3 ISOnum -->
+<!ENTITY curren "&#164;"> <!-- currency sign, U+00A4 ISOnum -->
+<!ENTITY yen "&#165;"> <!-- yen sign = yuan sign, U+00A5 ISOnum -->
+<!ENTITY brvbar "&#166;"> <!-- broken bar = broken vertical bar,
+ U+00A6 ISOnum -->
+<!ENTITY sect "&#167;"> <!-- section sign, U+00A7 ISOnum -->
+<!ENTITY uml "&#168;"> <!-- diaeresis = spacing diaeresis,
+ U+00A8 ISOdia -->
+<!ENTITY copy "&#169;"> <!-- copyright sign, U+00A9 ISOnum -->
+<!ENTITY ordf "&#170;"> <!-- feminine ordinal indicator, U+00AA ISOnum -->
+<!ENTITY laquo "&#171;"> <!-- left-pointing double angle quotation mark
+ = left pointing guillemet, U+00AB ISOnum -->
+<!ENTITY not "&#172;"> <!-- not sign = discretionary hyphen,
+ U+00AC ISOnum -->
+<!ENTITY shy "&#173;"> <!-- soft hyphen = discretionary hyphen,
+ U+00AD ISOnum -->
+<!ENTITY reg "&#174;"> <!-- registered sign = registered trade mark sign,
+ U+00AE ISOnum -->
+<!ENTITY macr "&#175;"> <!-- macron = spacing macron = overline
+ = APL overbar, U+00AF ISOdia -->
+<!ENTITY deg "&#176;"> <!-- degree sign, U+00B0 ISOnum -->
+<!ENTITY plusmn "&#177;"> <!-- plus-minus sign = plus-or-minus sign,
+ U+00B1 ISOnum -->
+<!ENTITY sup2 "&#178;"> <!-- superscript two = superscript digit two
+ = squared, U+00B2 ISOnum -->
+<!ENTITY sup3 "&#179;"> <!-- superscript three = superscript digit three
+ = cubed, U+00B3 ISOnum -->
+<!ENTITY acute "&#180;"> <!-- acute accent = spacing acute,
+ U+00B4 ISOdia -->
+<!ENTITY micro "&#181;"> <!-- micro sign, U+00B5 ISOnum -->
+<!ENTITY para "&#182;"> <!-- pilcrow sign = paragraph sign,
+ U+00B6 ISOnum -->
+<!ENTITY middot "&#183;"> <!-- middle dot = Georgian comma
+ = Greek middle dot, U+00B7 ISOnum -->
+<!ENTITY cedil "&#184;"> <!-- cedilla = spacing cedilla, U+00B8 ISOdia -->
+<!ENTITY sup1 "&#185;"> <!-- superscript one = superscript digit one,
+ U+00B9 ISOnum -->
+<!ENTITY ordm "&#186;"> <!-- masculine ordinal indicator,
+ U+00BA ISOnum -->
+<!ENTITY raquo "&#187;"> <!-- right-pointing double angle quotation mark
+ = right pointing guillemet, U+00BB ISOnum -->
+<!ENTITY frac14 "&#188;"> <!-- vulgar fraction one quarter
+ = fraction one quarter, U+00BC ISOnum -->
+<!ENTITY frac12 "&#189;"> <!-- vulgar fraction one half
+ = fraction one half, U+00BD ISOnum -->
+<!ENTITY frac34 "&#190;"> <!-- vulgar fraction three quarters
+ = fraction three quarters, U+00BE ISOnum -->
+<!ENTITY iquest "&#191;"> <!-- inverted question mark
+ = turned question mark, U+00BF ISOnum -->
+<!ENTITY Agrave "&#192;"> <!-- latin capital letter A with grave
+ = latin capital letter A grave,
+ U+00C0 ISOlat1 -->
+<!ENTITY Aacute "&#193;"> <!-- latin capital letter A with acute,
+ U+00C1 ISOlat1 -->
+<!ENTITY Acirc "&#194;"> <!-- latin capital letter A with circumflex,
+ U+00C2 ISOlat1 -->
+<!ENTITY Atilde "&#195;"> <!-- latin capital letter A with tilde,
+ U+00C3 ISOlat1 -->
+<!ENTITY Auml "&#196;"> <!-- latin capital letter A with diaeresis,
+ U+00C4 ISOlat1 -->
+<!ENTITY Aring "&#197;"> <!-- latin capital letter A with ring above
+ = latin capital letter A ring,
+ U+00C5 ISOlat1 -->
+<!ENTITY AElig "&#198;"> <!-- latin capital letter AE
+ = latin capital ligature AE,
+ U+00C6 ISOlat1 -->
+<!ENTITY Ccedil "&#199;"> <!-- latin capital letter C with cedilla,
+ U+00C7 ISOlat1 -->
+<!ENTITY Egrave "&#200;"> <!-- latin capital letter E with grave,
+ U+00C8 ISOlat1 -->
+<!ENTITY Eacute "&#201;"> <!-- latin capital letter E with acute,
+ U+00C9 ISOlat1 -->
+<!ENTITY Ecirc "&#202;"> <!-- latin capital letter E with circumflex,
+ U+00CA ISOlat1 -->
+<!ENTITY Euml "&#203;"> <!-- latin capital letter E with diaeresis,
+ U+00CB ISOlat1 -->
+<!ENTITY Igrave "&#204;"> <!-- latin capital letter I with grave,
+ U+00CC ISOlat1 -->
+<!ENTITY Iacute "&#205;"> <!-- latin capital letter I with acute,
+ U+00CD ISOlat1 -->
+<!ENTITY Icirc "&#206;"> <!-- latin capital letter I with circumflex,
+ U+00CE ISOlat1 -->
+<!ENTITY Iuml "&#207;"> <!-- latin capital letter I with diaeresis,
+ U+00CF ISOlat1 -->
+<!ENTITY ETH "&#208;"> <!-- latin capital letter ETH, U+00D0 ISOlat1 -->
+<!ENTITY Ntilde "&#209;"> <!-- latin capital letter N with tilde,
+ U+00D1 ISOlat1 -->
+<!ENTITY Ograve "&#210;"> <!-- latin capital letter O with grave,
+ U+00D2 ISOlat1 -->
+<!ENTITY Oacute "&#211;"> <!-- latin capital letter O with acute,
+ U+00D3 ISOlat1 -->
+<!ENTITY Ocirc "&#212;"> <!-- latin capital letter O with circumflex,
+ U+00D4 ISOlat1 -->
+<!ENTITY Otilde "&#213;"> <!-- latin capital letter O with tilde,
+ U+00D5 ISOlat1 -->
+<!ENTITY Ouml "&#214;"> <!-- latin capital letter O with diaeresis,
+ U+00D6 ISOlat1 -->
+<!ENTITY times "&#215;"> <!-- multiplication sign, U+00D7 ISOnum -->
+<!ENTITY Oslash "&#216;"> <!-- latin capital letter O with stroke
+ = latin capital letter O slash,
+ U+00D8 ISOlat1 -->
+<!ENTITY Ugrave "&#217;"> <!-- latin capital letter U with grave,
+ U+00D9 ISOlat1 -->
+<!ENTITY Uacute "&#218;"> <!-- latin capital letter U with acute,
+ U+00DA ISOlat1 -->
+<!ENTITY Ucirc "&#219;"> <!-- latin capital letter U with circumflex,
+ U+00DB ISOlat1 -->
+<!ENTITY Uuml "&#220;"> <!-- latin capital letter U with diaeresis,
+ U+00DC ISOlat1 -->
+<!ENTITY Yacute "&#221;"> <!-- latin capital letter Y with acute,
+ U+00DD ISOlat1 -->
+<!ENTITY THORN "&#222;"> <!-- latin capital letter THORN,
+ U+00DE ISOlat1 -->
+<!ENTITY szlig "&#223;"> <!-- latin small letter sharp s = ess-zed,
+ U+00DF ISOlat1 -->
+<!ENTITY agrave "&#224;"> <!-- latin small letter a with grave
+ = latin small letter a grave,
+ U+00E0 ISOlat1 -->
+<!ENTITY aacute "&#225;"> <!-- latin small letter a with acute,
+ U+00E1 ISOlat1 -->
+<!ENTITY acirc "&#226;"> <!-- latin small letter a with circumflex,
+ U+00E2 ISOlat1 -->
+<!ENTITY atilde "&#227;"> <!-- latin small letter a with tilde,
+ U+00E3 ISOlat1 -->
+<!ENTITY auml "&#228;"> <!-- latin small letter a with diaeresis,
+ U+00E4 ISOlat1 -->
+<!ENTITY aring "&#229;"> <!-- latin small letter a with ring above
+ = latin small letter a ring,
+ U+00E5 ISOlat1 -->
+<!ENTITY aelig "&#230;"> <!-- latin small letter ae
+ = latin small ligature ae, U+00E6 ISOlat1 -->
+<!ENTITY ccedil "&#231;"> <!-- latin small letter c with cedilla,
+ U+00E7 ISOlat1 -->
+<!ENTITY egrave "&#232;"> <!-- latin small letter e with grave,
+ U+00E8 ISOlat1 -->
+<!ENTITY eacute "&#233;"> <!-- latin small letter e with acute,
+ U+00E9 ISOlat1 -->
+<!ENTITY ecirc "&#234;"> <!-- latin small letter e with circumflex,
+ U+00EA ISOlat1 -->
+<!ENTITY euml "&#235;"> <!-- latin small letter e with diaeresis,
+ U+00EB ISOlat1 -->
+<!ENTITY igrave "&#236;"> <!-- latin small letter i with grave,
+ U+00EC ISOlat1 -->
+<!ENTITY iacute "&#237;"> <!-- latin small letter i with acute,
+ U+00ED ISOlat1 -->
+<!ENTITY icirc "&#238;"> <!-- latin small letter i with circumflex,
+ U+00EE ISOlat1 -->
+<!ENTITY iuml "&#239;"> <!-- latin small letter i with diaeresis,
+ U+00EF ISOlat1 -->
+<!ENTITY eth "&#240;"> <!-- latin small letter eth, U+00F0 ISOlat1 -->
+<!ENTITY ntilde "&#241;"> <!-- latin small letter n with tilde,
+ U+00F1 ISOlat1 -->
+<!ENTITY ograve "&#242;"> <!-- latin small letter o with grave,
+ U+00F2 ISOlat1 -->
+<!ENTITY oacute "&#243;"> <!-- latin small letter o with acute,
+ U+00F3 ISOlat1 -->
+<!ENTITY ocirc "&#244;"> <!-- latin small letter o with circumflex,
+ U+00F4 ISOlat1 -->
+<!ENTITY otilde "&#245;"> <!-- latin small letter o with tilde,
+ U+00F5 ISOlat1 -->
+<!ENTITY ouml "&#246;"> <!-- latin small letter o with diaeresis,
+ U+00F6 ISOlat1 -->
+<!ENTITY divide "&#247;"> <!-- division sign, U+00F7 ISOnum -->
+<!ENTITY oslash "&#248;"> <!-- latin small letter o with stroke,
+ = latin small letter o slash,
+ U+00F8 ISOlat1 -->
+<!ENTITY ugrave "&#249;"> <!-- latin small letter u with grave,
+ U+00F9 ISOlat1 -->
+<!ENTITY uacute "&#250;"> <!-- latin small letter u with acute,
+ U+00FA ISOlat1 -->
+<!ENTITY ucirc "&#251;"> <!-- latin small letter u with circumflex,
+ U+00FB ISOlat1 -->
+<!ENTITY uuml "&#252;"> <!-- latin small letter u with diaeresis,
+ U+00FC ISOlat1 -->
+<!ENTITY yacute "&#253;"> <!-- latin small letter y with acute,
+ U+00FD ISOlat1 -->
+<!ENTITY thorn "&#254;"> <!-- latin small letter thorn with,
+ U+00FE ISOlat1 -->
+<!ENTITY yuml "&#255;"> <!-- latin small letter y with diaeresis,
+ U+00FF ISOlat1 -->
+
+
+<!ENTITY % xhtml-symbol
+ PUBLIC "-//W3C//ENTITIES Symbols for XHTML//EN"
+ "xhtml-symbol.ent" >
+<!-- Mathematical, Greek and Symbolic characters for HTML -->
+
+<!-- Character entity set. Typical invocation:
+ <!ENTITY % HTMLsymbol PUBLIC
+ "-//W3C//ENTITIES Symbols for XHTML//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml-symbol.ent">
+ %HTMLsymbol;
+-->
+
+<!-- Portions (C) International Organization for Standardization 1986:
+ Permission to copy in any form is granted for use with
+ conforming SGML systems and applications as defined in
+ ISO 8879, provided this notice is included in all copies.
+-->
+
+<!-- Relevant ISO entity set is given unless names are newly introduced.
+ New names (i.e., not in ISO 8879 list) do not clash with any
+ existing ISO 8879 entity names. ISO 10646 character numbers
+ are given for each character, in hex. values are decimal
+ conversions of the ISO 10646 values and refer to the document
+ character set. Names are Unicode names.
+-->
+
+<!-- Latin Extended-B -->
+<!ENTITY fnof "&#402;"> <!-- latin small f with hook = function
+ = florin, U+0192 ISOtech -->
+
+<!-- Greek -->
+<!ENTITY Alpha "&#913;"> <!-- greek capital letter alpha, U+0391 -->
+<!ENTITY Beta "&#914;"> <!-- greek capital letter beta, U+0392 -->
+<!ENTITY Gamma "&#915;"> <!-- greek capital letter gamma,
+ U+0393 ISOgrk3 -->
+<!ENTITY Delta "&#916;"> <!-- greek capital letter delta,
+ U+0394 ISOgrk3 -->
+<!ENTITY Epsilon "&#917;"> <!-- greek capital letter epsilon, U+0395 -->
+<!ENTITY Zeta "&#918;"> <!-- greek capital letter zeta, U+0396 -->
+<!ENTITY Eta "&#919;"> <!-- greek capital letter eta, U+0397 -->
+<!ENTITY Theta "&#920;"> <!-- greek capital letter theta,
+ U+0398 ISOgrk3 -->
+<!ENTITY Iota "&#921;"> <!-- greek capital letter iota, U+0399 -->
+<!ENTITY Kappa "&#922;"> <!-- greek capital letter kappa, U+039A -->
+<!ENTITY Lambda "&#923;"> <!-- greek capital letter lambda,
+ U+039B ISOgrk3 -->
+<!ENTITY Mu "&#924;"> <!-- greek capital letter mu, U+039C -->
+<!ENTITY Nu "&#925;"> <!-- greek capital letter nu, U+039D -->
+<!ENTITY Xi "&#926;"> <!-- greek capital letter xi, U+039E ISOgrk3 -->
+<!ENTITY Omicron "&#927;"> <!-- greek capital letter omicron, U+039F -->
+<!ENTITY Pi "&#928;"> <!-- greek capital letter pi, U+03A0 ISOgrk3 -->
+<!ENTITY Rho "&#929;"> <!-- greek capital letter rho, U+03A1 -->
+<!-- there is no Sigmaf, and no U+03A2 character either -->
+<!ENTITY Sigma "&#931;"> <!-- greek capital letter sigma,
+ U+03A3 ISOgrk3 -->
+<!ENTITY Tau "&#932;"> <!-- greek capital letter tau, U+03A4 -->
+<!ENTITY Upsilon "&#933;"> <!-- greek capital letter upsilon,
+ U+03A5 ISOgrk3 -->
+<!ENTITY Phi "&#934;"> <!-- greek capital letter phi,
+ U+03A6 ISOgrk3 -->
+<!ENTITY Chi "&#935;"> <!-- greek capital letter chi, U+03A7 -->
+<!ENTITY Psi "&#936;"> <!-- greek capital letter psi,
+ U+03A8 ISOgrk3 -->
+<!ENTITY Omega "&#937;"> <!-- greek capital letter omega,
+ U+03A9 ISOgrk3 -->
+
+<!ENTITY alpha "&#945;"> <!-- greek small letter alpha,
+ U+03B1 ISOgrk3 -->
+<!ENTITY beta "&#946;"> <!-- greek small letter beta, U+03B2 ISOgrk3 -->
+<!ENTITY gamma "&#947;"> <!-- greek small letter gamma,
+ U+03B3 ISOgrk3 -->
+<!ENTITY delta "&#948;"> <!-- greek small letter delta,
+ U+03B4 ISOgrk3 -->
+<!ENTITY epsilon "&#949;"> <!-- greek small letter epsilon,
+ U+03B5 ISOgrk3 -->
+<!ENTITY zeta "&#950;"> <!-- greek small letter zeta, U+03B6 ISOgrk3 -->
+<!ENTITY eta "&#951;"> <!-- greek small letter eta, U+03B7 ISOgrk3 -->
+<!ENTITY theta "&#952;"> <!-- greek small letter theta,
+ U+03B8 ISOgrk3 -->
+<!ENTITY iota "&#953;"> <!-- greek small letter iota, U+03B9 ISOgrk3 -->
+<!ENTITY kappa "&#954;"> <!-- greek small letter kappa,
+ U+03BA ISOgrk3 -->
+<!ENTITY lambda "&#955;"> <!-- greek small letter lambda,
+ U+03BB ISOgrk3 -->
+<!ENTITY mu "&#956;"> <!-- greek small letter mu, U+03BC ISOgrk3 -->
+<!ENTITY nu "&#957;"> <!-- greek small letter nu, U+03BD ISOgrk3 -->
+<!ENTITY xi "&#958;"> <!-- greek small letter xi, U+03BE ISOgrk3 -->
+<!ENTITY omicron "&#959;"> <!-- greek small letter omicron, U+03BF NEW -->
+<!ENTITY pi "&#960;"> <!-- greek small letter pi, U+03C0 ISOgrk3 -->
+<!ENTITY rho "&#961;"> <!-- greek small letter rho, U+03C1 ISOgrk3 -->
+<!ENTITY sigmaf "&#962;"> <!-- greek small letter final sigma,
+ U+03C2 ISOgrk3 -->
+<!ENTITY sigma "&#963;"> <!-- greek small letter sigma,
+ U+03C3 ISOgrk3 -->
+<!ENTITY tau "&#964;"> <!-- greek small letter tau, U+03C4 ISOgrk3 -->
+<!ENTITY upsilon "&#965;"> <!-- greek small letter upsilon,
+ U+03C5 ISOgrk3 -->
+<!ENTITY phi "&#966;"> <!-- greek small letter phi, U+03C6 ISOgrk3 -->
+<!ENTITY chi "&#967;"> <!-- greek small letter chi, U+03C7 ISOgrk3 -->
+<!ENTITY psi "&#968;"> <!-- greek small letter psi, U+03C8 ISOgrk3 -->
+<!ENTITY omega "&#969;"> <!-- greek small letter omega,
+ U+03C9 ISOgrk3 -->
+<!ENTITY thetasym "&#977;"> <!-- greek small letter theta symbol,
+ U+03D1 NEW -->
+<!ENTITY upsih "&#978;"> <!-- greek upsilon with hook symbol,
+ U+03D2 NEW -->
+<!ENTITY piv "&#982;"> <!-- greek pi symbol, U+03D6 ISOgrk3 -->
+
+<!-- General Punctuation -->
+<!ENTITY bull "&#8226;"> <!-- bullet = black small circle,
+ U+2022 ISOpub -->
+<!-- bullet is NOT the same as bullet operator, U+2219 -->
+<!ENTITY hellip "&#8230;"> <!-- horizontal ellipsis = three dot leader,
+ U+2026 ISOpub -->
+<!ENTITY prime "&#8242;"> <!-- prime = minutes = feet, U+2032 ISOtech -->
+<!ENTITY Prime "&#8243;"> <!-- double prime = seconds = inches,
+ U+2033 ISOtech -->
+<!ENTITY oline "&#8254;"> <!-- overline = spacing overscore,
+ U+203E NEW -->
+<!ENTITY frasl "&#8260;"> <!-- fraction slash, U+2044 NEW -->
+
+<!-- Letterlike Symbols -->
+<!ENTITY weierp "&#8472;"> <!-- script capital P = power set
+ = Weierstrass p, U+2118 ISOamso -->
+<!ENTITY image "&#8465;"> <!-- blackletter capital I = imaginary part,
+ U+2111 ISOamso -->
+<!ENTITY real "&#8476;"> <!-- blackletter capital R = real part symbol,
+ U+211C ISOamso -->
+<!ENTITY trade "&#8482;"> <!-- trade mark sign, U+2122 ISOnum -->
+<!ENTITY alefsym "&#8501;"> <!-- alef symbol = first transfinite cardinal,
+ U+2135 NEW -->
+<!-- alef symbol is NOT the same as hebrew letter alef,
+ U+05D0 although the same glyph could be used to depict both characters -->
+
+<!-- Arrows -->
+<!ENTITY larr "&#8592;"> <!-- leftwards arrow, U+2190 ISOnum -->
+<!ENTITY uarr "&#8593;"> <!-- upwards arrow, U+2191 ISOnum-->
+<!ENTITY rarr "&#8594;"> <!-- rightwards arrow, U+2192 ISOnum -->
+<!ENTITY darr "&#8595;"> <!-- downwards arrow, U+2193 ISOnum -->
+<!ENTITY harr "&#8596;"> <!-- left right arrow, U+2194 ISOamsa -->
+<!ENTITY crarr "&#8629;"> <!-- downwards arrow with corner leftwards
+ = carriage return, U+21B5 NEW -->
+<!ENTITY lArr "&#8656;"> <!-- leftwards double arrow, U+21D0 ISOtech -->
+<!-- Unicode does not say that lArr is the same as the 'is implied by' arrow
+ but also does not have any other character for that function. So ? lArr can
+ be used for 'is implied by' as ISOtech suggests -->
+<!ENTITY uArr "&#8657;"> <!-- upwards double arrow, U+21D1 ISOamsa -->
+<!ENTITY rArr "&#8658;"> <!-- rightwards double arrow,
+ U+21D2 ISOtech -->
+<!-- Unicode does not say this is the 'implies' character but does not have
+ another character with this function so ?
+ rArr can be used for 'implies' as ISOtech suggests -->
+<!ENTITY dArr "&#8659;"> <!-- downwards double arrow, U+21D3 ISOamsa -->
+<!ENTITY hArr "&#8660;"> <!-- left right double arrow,
+ U+21D4 ISOamsa -->
+
+<!-- Mathematical Operators -->
+<!ENTITY forall "&#8704;"> <!-- for all, U+2200 ISOtech -->
+<!ENTITY part "&#8706;"> <!-- partial differential, U+2202 ISOtech -->
+<!ENTITY exist "&#8707;"> <!-- there exists, U+2203 ISOtech -->
+<!ENTITY empty "&#8709;"> <!-- empty set = null set = diameter,
+ U+2205 ISOamso -->
+<!ENTITY nabla "&#8711;"> <!-- nabla = backward difference,
+ U+2207 ISOtech -->
+<!ENTITY isin "&#8712;"> <!-- element of, U+2208 ISOtech -->
+<!ENTITY notin "&#8713;"> <!-- not an element of, U+2209 ISOtech -->
+<!ENTITY ni "&#8715;"> <!-- contains as member, U+220B ISOtech -->
+<!-- should there be a more memorable name than 'ni'? -->
+<!ENTITY prod "&#8719;"> <!-- n-ary product = product sign,
+ U+220F ISOamsb -->
+<!-- prod is NOT the same character as U+03A0 'greek capital letter pi' though
+ the same glyph might be used for both -->
+<!ENTITY sum "&#8721;"> <!-- n-ary sumation, U+2211 ISOamsb -->
+<!-- sum is NOT the same character as U+03A3 'greek capital letter sigma'
+ though the same glyph might be used for both -->
+<!ENTITY minus "&#8722;"> <!-- minus sign, U+2212 ISOtech -->
+<!ENTITY lowast "&#8727;"> <!-- asterisk operator, U+2217 ISOtech -->
+<!ENTITY radic "&#8730;"> <!-- square root = radical sign,
+ U+221A ISOtech -->
+<!ENTITY prop "&#8733;"> <!-- proportional to, U+221D ISOtech -->
+<!ENTITY infin "&#8734;"> <!-- infinity, U+221E ISOtech -->
+<!ENTITY ang "&#8736;"> <!-- angle, U+2220 ISOamso -->
+<!ENTITY and "&#8743;"> <!-- logical and = wedge, U+2227 ISOtech -->
+<!ENTITY or "&#8744;"> <!-- logical or = vee, U+2228 ISOtech -->
+<!ENTITY cap "&#8745;"> <!-- intersection = cap, U+2229 ISOtech -->
+<!ENTITY cup "&#8746;"> <!-- union = cup, U+222A ISOtech -->
+<!ENTITY int "&#8747;"> <!-- integral, U+222B ISOtech -->
+<!ENTITY there4 "&#8756;"> <!-- therefore, U+2234 ISOtech -->
+<!ENTITY sim "&#8764;"> <!-- tilde operator = varies with = similar to,
+ U+223C ISOtech -->
+<!-- tilde operator is NOT the same character as the tilde, U+007E,
+ although the same glyph might be used to represent both -->
+<!ENTITY cong "&#8773;"> <!-- approximately equal to, U+2245 ISOtech -->
+<!ENTITY asymp "&#8776;"> <!-- almost equal to = asymptotic to,
+ U+2248 ISOamsr -->
+<!ENTITY ne "&#8800;"> <!-- not equal to, U+2260 ISOtech -->
+<!ENTITY equiv "&#8801;"> <!-- identical to, U+2261 ISOtech -->
+<!ENTITY le "&#8804;"> <!-- less-than or equal to, U+2264 ISOtech -->
+<!ENTITY ge "&#8805;"> <!-- greater-than or equal to,
+ U+2265 ISOtech -->
+<!ENTITY sub "&#8834;"> <!-- subset of, U+2282 ISOtech -->
+<!ENTITY sup "&#8835;"> <!-- superset of, U+2283 ISOtech -->
+<!-- note that nsup, 'not a superset of, U+2283' is not covered by the Symbol
+ font encoding and is not included. Should it be, for symmetry?
+ It is in ISOamsn -->
+<!ENTITY nsub "&#8836;"> <!-- not a subset of, U+2284 ISOamsn -->
+<!ENTITY sube "&#8838;"> <!-- subset of or equal to, U+2286 ISOtech -->
+<!ENTITY supe "&#8839;"> <!-- superset of or equal to,
+ U+2287 ISOtech -->
+<!ENTITY oplus "&#8853;"> <!-- circled plus = direct sum,
+ U+2295 ISOamsb -->
+<!ENTITY otimes "&#8855;"> <!-- circled times = vector product,
+ U+2297 ISOamsb -->
+<!ENTITY perp "&#8869;"> <!-- up tack = orthogonal to = perpendicular,
+ U+22A5 ISOtech -->
+<!ENTITY sdot "&#8901;"> <!-- dot operator, U+22C5 ISOamsb -->
+<!-- dot operator is NOT the same character as U+00B7 middle dot -->
+
+<!-- Miscellaneous Technical -->
+<!ENTITY lceil "&#8968;"> <!-- left ceiling = apl upstile,
+ U+2308 ISOamsc -->
+<!ENTITY rceil "&#8969;"> <!-- right ceiling, U+2309 ISOamsc -->
+<!ENTITY lfloor "&#8970;"> <!-- left floor = apl downstile,
+ U+230A ISOamsc -->
+<!ENTITY rfloor "&#8971;"> <!-- right floor, U+230B ISOamsc -->
+<!ENTITY lang "&#9001;"> <!-- left-pointing angle bracket = bra,
+ U+2329 ISOtech -->
+<!-- lang is NOT the same character as U+003C 'less than'
+ or U+2039 'single left-pointing angle quotation mark' -->
+<!ENTITY rang "&#9002;"> <!-- right-pointing angle bracket = ket,
+ U+232A ISOtech -->
+<!-- rang is NOT the same character as U+003E 'greater than'
+ or U+203A 'single right-pointing angle quotation mark' -->
+
+<!-- Geometric Shapes -->
+<!ENTITY loz "&#9674;"> <!-- lozenge, U+25CA ISOpub -->
+
+<!-- Miscellaneous Symbols -->
+<!ENTITY spades "&#9824;"> <!-- black spade suit, U+2660 ISOpub -->
+<!-- black here seems to mean filled as opposed to hollow -->
+<!ENTITY clubs "&#9827;"> <!-- black club suit = shamrock,
+ U+2663 ISOpub -->
+<!ENTITY hearts "&#9829;"> <!-- black heart suit = valentine,
+ U+2665 ISOpub -->
+<!ENTITY diams "&#9830;"> <!-- black diamond suit, U+2666 ISOpub -->
+
+
+<!ENTITY % xhtml-special
+ PUBLIC "-//W3C//ENTITIES Special for XHTML//EN"
+ "xhtml-special.ent" >
+<!-- Special characters for HTML -->
+
+<!-- Character entity set. Typical invocation:
+ <!ENTITY % HTMLspecial PUBLIC
+ "-//W3C//ENTITIES Special for XHTML//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml-special.ent">
+ %HTMLspecial;
+-->
+
+<!-- Portions (C) International Organization for Standardization 1986:
+ Permission to copy in any form is granted for use with
+ conforming SGML systems and applications as defined in
+ ISO 8879, provided this notice is included in all copies.
+-->
+
+<!-- Relevant ISO entity set is given unless names are newly introduced.
+ New names (i.e., not in ISO 8879 list) do not clash with any
+ existing ISO 8879 entity names. ISO 10646 character numbers
+ are given for each character, in hex. values are decimal
+ conversions of the ISO 10646 values and refer to the document
+ character set. Names are Unicode names.
+-->
+
+<!-- C0 Controls and Basic Latin -->
+<!ENTITY quot "&#34;"> <!-- quotation mark = APL quote,
+ U+0022 ISOnum -->
+<!ENTITY amp "&#38;#38;"> <!-- ampersand, U+0026 ISOnum -->
+<!ENTITY lt "&#38;#60;"> <!-- less-than sign, U+003C ISOnum -->
+<!ENTITY gt "&#62;"> <!-- greater-than sign, U+003E ISOnum -->
+<!ENTITY apos "&#39;"> <!-- apostrophe mark, U+0027 ISOnum -->
+
+<!-- Latin Extended-A -->
+<!ENTITY OElig "&#338;"> <!-- latin capital ligature OE,
+ U+0152 ISOlat2 -->
+<!ENTITY oelig "&#339;"> <!-- latin small ligature oe, U+0153 ISOlat2 -->
+<!-- ligature is a misnomer, this is a separate character in some languages -->
+<!ENTITY Scaron "&#352;"> <!-- latin capital letter S with caron,
+ U+0160 ISOlat2 -->
+<!ENTITY scaron "&#353;"> <!-- latin small letter s with caron,
+ U+0161 ISOlat2 -->
+<!ENTITY Yuml "&#376;"> <!-- latin capital letter Y with diaeresis,
+ U+0178 ISOlat2 -->
+
+<!-- Spacing Modifier Letters -->
+<!ENTITY circ "&#710;"> <!-- modifier letter circumflex accent,
+ U+02C6 ISOpub -->
+<!ENTITY tilde "&#732;"> <!-- small tilde, U+02DC ISOdia -->
+
+<!-- General Punctuation -->
+<!ENTITY ensp "&#8194;"> <!-- en space, U+2002 ISOpub -->
+<!ENTITY emsp "&#8195;"> <!-- em space, U+2003 ISOpub -->
+<!ENTITY thinsp "&#8201;"> <!-- thin space, U+2009 ISOpub -->
+<!ENTITY zwnj "&#8204;"> <!-- zero width non-joiner,
+ U+200C NEW RFC 2070 -->
+<!ENTITY zwj "&#8205;"> <!-- zero width joiner, U+200D NEW RFC 2070 -->
+<!ENTITY lrm "&#8206;"> <!-- left-to-right mark, U+200E NEW RFC 2070 -->
+<!ENTITY rlm "&#8207;"> <!-- right-to-left mark, U+200F NEW RFC 2070 -->
+<!ENTITY ndash "&#8211;"> <!-- en dash, U+2013 ISOpub -->
+<!ENTITY mdash "&#8212;"> <!-- em dash, U+2014 ISOpub -->
+<!ENTITY lsquo "&#8216;"> <!-- left single quotation mark,
+ U+2018 ISOnum -->
+<!ENTITY rsquo "&#8217;"> <!-- right single quotation mark,
+ U+2019 ISOnum -->
+<!ENTITY sbquo "&#8218;"> <!-- single low-9 quotation mark, U+201A NEW -->
+<!ENTITY ldquo "&#8220;"> <!-- left double quotation mark,
+ U+201C ISOnum -->
+<!ENTITY rdquo "&#8221;"> <!-- right double quotation mark,
+ U+201D ISOnum -->
+<!ENTITY bdquo "&#8222;"> <!-- double low-9 quotation mark, U+201E NEW -->
+<!ENTITY dagger "&#8224;"> <!-- dagger, U+2020 ISOpub -->
+<!ENTITY Dagger "&#8225;"> <!-- double dagger, U+2021 ISOpub -->
+<!ENTITY permil "&#8240;"> <!-- per mille sign, U+2030 ISOtech -->
+<!ENTITY lsaquo "&#8249;"> <!-- single left-pointing angle quotation mark,
+ U+2039 ISO proposed -->
+<!-- lsaquo is proposed but not yet ISO standardized -->
+<!ENTITY rsaquo "&#8250;"> <!-- single right-pointing angle quotation mark,
+ U+203A ISO proposed -->
+<!-- rsaquo is proposed but not yet ISO standardized -->
+<!ENTITY euro "&#8364;"> <!-- euro sign, U+20AC NEW -->
+
+
+<!-- end of xhtml-charent-1.mod -->
+]]>
+
+<!-- end of xhtml-framework-1.mod -->
+]]>
+
+<!-- Post-Framework Redeclaration placeholder ................... -->
+<!-- this serves as a location to insert markup declarations
+ into the DTD following the framework declarations.
+-->
+<!ENTITY % xhtml-postfw-redecl.module "IGNORE" >
+<![%xhtml-postfw-redecl.module;[
+%xhtml-postfw-redecl.mod;
+<!-- end of xhtml-postfw-redecl.module -->]]>
+
+<!-- Text Module (Required) ..................................... -->
+<!ENTITY % xhtml-text.module "INCLUDE" >
+<![%xhtml-text.module;[
+<!ENTITY % xhtml-text.mod
+ PUBLIC "-//W3C//ELEMENTS XHTML Text 1.0//EN"
+ "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-text-1.mod" >
+<!-- ...................................................................... -->
+<!-- XHTML Text Module ................................................... -->
+<!-- file: xhtml-text-1.mod
+
+ This is XHTML, a reformulation of HTML as a modular XML application.
+ Copyright 1998-2001 W3C (MIT, INRIA, Keio), All Rights Reserved.
+ Revision: $Id: xhtml-text-1.mod,v 4.0 2001/04/02 22:42:49 altheim Exp $ SMI
+
+ This DTD module is identified by the PUBLIC and SYSTEM identifiers:
+
+ PUBLIC "-//W3C//ELEMENTS XHTML Text 1.0//EN"
+ SYSTEM "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-text-1.mod"
+
+ Revisions:
+ (none)
+ ....................................................................... -->
+
+<!-- Textual Content
+
+ The Text module includes declarations for all core
+ text container elements and their attributes.
+-->
+
+<!ENTITY % xhtml-inlstruct.module "INCLUDE" >
+<![%xhtml-inlstruct.module;[
+<!ENTITY % xhtml-inlstruct.mod
+ PUBLIC "-//W3C//ELEMENTS XHTML Inline Structural 1.0//EN"
+ "xhtml-inlstruct-1.mod" >
+<!-- ...................................................................... -->
+<!-- XHTML Inline Structural Module ...................................... -->
+<!-- file: xhtml-inlstruct-1.mod
+
+ This is XHTML, a reformulation of HTML as a modular XML application.
+ Copyright 1998-2001 W3C (MIT, INRIA, Keio), All Rights Reserved.
+ Revision: $Id: xhtml-inlstruct-1.mod,v 4.0 2001/04/02 22:42:49 altheim Exp $ SMI
+
+ This DTD module is identified by the PUBLIC and SYSTEM identifiers:
+
+ PUBLIC "-//W3C//ELEMENTS XHTML Inline Structural 1.0//EN"
+ SYSTEM "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-inlstruct-1.mod"
+
+ Revisions:
+ (none)
+ ....................................................................... -->
+
+<!-- Inline Structural
+
+ br, span
+
+ This module declares the elements and their attributes
+ used to support inline-level structural markup.
+-->
+
+<!-- br: forced line break ............................. -->
+
+<!ENTITY % br.element "INCLUDE" >
+<![%br.element;[
+
+<!ENTITY % br.content "EMPTY" >
+<!ENTITY % br.qname "br" >
+<!ELEMENT %br.qname; %br.content; >
+
+<!-- end of br.element -->]]>
+
+<!ENTITY % br.attlist "INCLUDE" >
+<![%br.attlist;[
+<!ATTLIST %br.qname;
+ %Core.attrib;
+>
+<!-- end of br.attlist -->]]>
+
+<!-- span: generic inline container .................... -->
+
+<!ENTITY % span.element "INCLUDE" >
+<![%span.element;[
+<!ENTITY % span.content
+ "( #PCDATA | %Inline.mix; )*"
+>
+<!ENTITY % span.qname "span" >
+<!ELEMENT %span.qname; %span.content; >
+<!-- end of span.element -->]]>
+
+<!ENTITY % span.attlist "INCLUDE" >
+<![%span.attlist;[
+<!ATTLIST %span.qname;
+ %Common.attrib;
+>
+<!-- end of span.attlist -->]]>
+
+<!-- end of xhtml-inlstruct-1.mod -->
+]]>
+
+<!ENTITY % xhtml-inlphras.module "INCLUDE" >
+<![%xhtml-inlphras.module;[
+<!ENTITY % xhtml-inlphras.mod
+ PUBLIC "-//W3C//ELEMENTS XHTML Inline Phrasal 1.0//EN"
+ "xhtml-inlphras-1.mod" >
+<!-- ...................................................................... -->
+<!-- XHTML Inline Phrasal Module ......................................... -->
+<!-- file: xhtml-inlphras-1.mod
+
+ This is XHTML, a reformulation of HTML as a modular XML application.
+ Copyright 1998-2001 W3C (MIT, INRIA, Keio), All Rights Reserved.
+ Revision: $Id: xhtml-inlphras-1.mod,v 4.0 2001/04/02 22:42:49 altheim Exp $ SMI
+
+ This DTD module is identified by the PUBLIC and SYSTEM identifiers:
+
+ PUBLIC "-//W3C//ELEMENTS XHTML Inline Phrasal 1.0//EN"
+ SYSTEM "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-inlphras-1.mod"
+
+ Revisions:
+ (none)
+ ....................................................................... -->
+
+<!-- Inline Phrasal
+
+ abbr, acronym, cite, code, dfn, em, kbd, q, samp, strong, var
+
+ This module declares the elements and their attributes used to
+ support inline-level phrasal markup.
+-->
+
+<!ENTITY % abbr.element "INCLUDE" >
+<![%abbr.element;[
+<!ENTITY % abbr.content
+ "( #PCDATA | %Inline.mix; )*"
+>
+<!ENTITY % abbr.qname "abbr" >
+<!ELEMENT %abbr.qname; %abbr.content; >
+<!-- end of abbr.element -->]]>
+
+<!ENTITY % abbr.attlist "INCLUDE" >
+<![%abbr.attlist;[
+<!ATTLIST %abbr.qname;
+ %Common.attrib;
+>
+<!-- end of abbr.attlist -->]]>
+
+<!ENTITY % acronym.element "INCLUDE" >
+<![%acronym.element;[
+<!ENTITY % acronym.content
+ "( #PCDATA | %Inline.mix; )*"
+>
+<!ENTITY % acronym.qname "acronym" >
+<!ELEMENT %acronym.qname; %acronym.content; >
+<!-- end of acronym.element -->]]>
+
+<!ENTITY % acronym.attlist "INCLUDE" >
+<![%acronym.attlist;[
+<!ATTLIST %acronym.qname;
+ %Common.attrib;
+>
+<!-- end of acronym.attlist -->]]>
+
+<!ENTITY % cite.element "INCLUDE" >
+<![%cite.element;[
+<!ENTITY % cite.content
+ "( #PCDATA | %Inline.mix; )*"
+>
+<!ENTITY % cite.qname "cite" >
+<!ELEMENT %cite.qname; %cite.content; >
+<!-- end of cite.element -->]]>
+
+<!ENTITY % cite.attlist "INCLUDE" >
+<![%cite.attlist;[
+<!ATTLIST %cite.qname;
+ %Common.attrib;
+>
+<!-- end of cite.attlist -->]]>
+
+<!ENTITY % code.element "INCLUDE" >
+<![%code.element;[
+<!ENTITY % code.content
+ "( #PCDATA | %Inline.mix; )*"
+>
+<!ENTITY % code.qname "code" >
+<!ELEMENT %code.qname; %code.content; >
+<!-- end of code.element -->]]>
+
+<!ENTITY % code.attlist "INCLUDE" >
+<![%code.attlist;[
+<!ATTLIST %code.qname;
+ %Common.attrib;
+>
+<!-- end of code.attlist -->]]>
+
+<!ENTITY % dfn.element "INCLUDE" >
+<![%dfn.element;[
+<!ENTITY % dfn.content
+ "( #PCDATA | %Inline.mix; )*"
+>
+<!ENTITY % dfn.qname "dfn" >
+<!ELEMENT %dfn.qname; %dfn.content; >
+<!-- end of dfn.element -->]]>
+
+<!ENTITY % dfn.attlist "INCLUDE" >
+<![%dfn.attlist;[
+<!ATTLIST %dfn.qname;
+ %Common.attrib;
+>
+<!-- end of dfn.attlist -->]]>
+
+<!ENTITY % em.element "INCLUDE" >
+<![%em.element;[
+<!ENTITY % em.content
+ "( #PCDATA | %Inline.mix; )*"
+>
+<!ENTITY % em.qname "em" >
+<!ELEMENT %em.qname; %em.content; >
+<!-- end of em.element -->]]>
+
+<!ENTITY % em.attlist "INCLUDE" >
+<![%em.attlist;[
+<!ATTLIST %em.qname;
+ %Common.attrib;
+>
+<!-- end of em.attlist -->]]>
+
+<!ENTITY % kbd.element "INCLUDE" >
+<![%kbd.element;[
+<!ENTITY % kbd.content
+ "( #PCDATA | %Inline.mix; )*"
+>
+<!ENTITY % kbd.qname "kbd" >
+<!ELEMENT %kbd.qname; %kbd.content; >
+<!-- end of kbd.element -->]]>
+
+<!ENTITY % kbd.attlist "INCLUDE" >
+<![%kbd.attlist;[
+<!ATTLIST %kbd.qname;
+ %Common.attrib;
+>
+<!-- end of kbd.attlist -->]]>
+
+<!ENTITY % q.element "INCLUDE" >
+<![%q.element;[
+<!ENTITY % q.content
+ "( #PCDATA | %Inline.mix; )*"
+>
+<!ENTITY % q.qname "q" >
+<!ELEMENT %q.qname; %q.content; >
+<!-- end of q.element -->]]>
+
+<!ENTITY % q.attlist "INCLUDE" >
+<![%q.attlist;[
+<!ATTLIST %q.qname;
+ %Common.attrib;
+ cite %URI.datatype; #IMPLIED
+>
+<!-- end of q.attlist -->]]>
+
+<!ENTITY % samp.element "INCLUDE" >
+<![%samp.element;[
+<!ENTITY % samp.content
+ "( #PCDATA | %Inline.mix; )*"
+>
+<!ENTITY % samp.qname "samp" >
+<!ELEMENT %samp.qname; %samp.content; >
+<!-- end of samp.element -->]]>
+
+<!ENTITY % samp.attlist "INCLUDE" >
+<![%samp.attlist;[
+<!ATTLIST %samp.qname;
+ %Common.attrib;
+>
+<!-- end of samp.attlist -->]]>
+
+<!ENTITY % strong.element "INCLUDE" >
+<![%strong.element;[
+<!ENTITY % strong.content
+ "( #PCDATA | %Inline.mix; )*"
+>
+<!ENTITY % strong.qname "strong" >
+<!ELEMENT %strong.qname; %strong.content; >
+<!-- end of strong.element -->]]>
+
+<!ENTITY % strong.attlist "INCLUDE" >
+<![%strong.attlist;[
+<!ATTLIST %strong.qname;
+ %Common.attrib;
+>
+<!-- end of strong.attlist -->]]>
+
+<!ENTITY % var.element "INCLUDE" >
+<![%var.element;[
+<!ENTITY % var.content
+ "( #PCDATA | %Inline.mix; )*"
+>
+<!ENTITY % var.qname "var" >
+<!ELEMENT %var.qname; %var.content; >
+<!-- end of var.element -->]]>
+
+<!ENTITY % var.attlist "INCLUDE" >
+<![%var.attlist;[
+<!ATTLIST %var.qname;
+ %Common.attrib;
+>
+<!-- end of var.attlist -->]]>
+
+<!-- end of xhtml-inlphras-1.mod -->
+]]>
+
+<!ENTITY % xhtml-blkstruct.module "INCLUDE" >
+<![%xhtml-blkstruct.module;[
+<!ENTITY % xhtml-blkstruct.mod
+ PUBLIC "-//W3C//ELEMENTS XHTML Block Structural 1.0//EN"
+ "xhtml-blkstruct-1.mod" >
+<!-- ...................................................................... -->
+<!-- XHTML Block Structural Module ....................................... -->
+<!-- file: xhtml-blkstruct-1.mod
+
+ This is XHTML, a reformulation of HTML as a modular XML application.
+ Copyright 1998-2001 W3C (MIT, INRIA, Keio), All Rights Reserved.
+ Revision: $Id: xhtml-blkstruct-1.mod,v 4.0 2001/04/02 22:42:49 altheim Exp $ SMI
+
+ This DTD module is identified by the PUBLIC and SYSTEM identifiers:
+
+ PUBLIC "-//W3C//ELEMENTS XHTML Block Structural 1.0//EN"
+ SYSTEM "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-blkstruct-1.mod"
+
+ Revisions:
+ (none)
+ ....................................................................... -->
+
+<!-- Block Structural
+
+ div, p
+
+ This module declares the elements and their attributes used to
+ support block-level structural markup.
+-->
+
+<!ENTITY % div.element "INCLUDE" >
+<![%div.element;[
+<!ENTITY % div.content
+ "( #PCDATA | %Flow.mix; )*"
+>
+<!ENTITY % div.qname "div" >
+<!ELEMENT %div.qname; %div.content; >
+<!-- end of div.element -->]]>
+
+<!ENTITY % div.attlist "INCLUDE" >
+<![%div.attlist;[
+<!ATTLIST %div.qname;
+ %Common.attrib;
+>
+<!-- end of div.attlist -->]]>
+
+<!ENTITY % p.element "INCLUDE" >
+<![%p.element;[
+<!ENTITY % p.content
+ "( #PCDATA | %Inline.mix; )*" >
+<!ENTITY % p.qname "p" >
+<!ELEMENT %p.qname; %p.content; >
+<!-- end of p.element -->]]>
+
+<!ENTITY % p.attlist "INCLUDE" >
+<![%p.attlist;[
+<!ATTLIST %p.qname;
+ %Common.attrib;
+>
+<!-- end of p.attlist -->]]>
+
+<!-- end of xhtml-blkstruct-1.mod -->
+]]>
+
+<!ENTITY % xhtml-blkphras.module "INCLUDE" >
+<![%xhtml-blkphras.module;[
+<!ENTITY % xhtml-blkphras.mod
+ PUBLIC "-//W3C//ELEMENTS XHTML Block Phrasal 1.0//EN"
+ "xhtml-blkphras-1.mod" >
+<!-- ...................................................................... -->
+<!-- XHTML Block Phrasal Module .......................................... -->
+<!-- file: xhtml-blkphras-1.mod
+
+ This is XHTML, a reformulation of HTML as a modular XML application.
+ Copyright 1998-2001 W3C (MIT, INRIA, Keio), All Rights Reserved.
+ Revision: $Id: xhtml-blkphras-1.mod,v 4.0 2001/04/02 22:42:49 altheim Exp $ SMI
+
+ This DTD module is identified by the PUBLIC and SYSTEM identifiers:
+
+ PUBLIC "-//W3C//ELEMENTS XHTML Block Phrasal 1.0//EN"
+ SYSTEM "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-blkphras-1.mod"
+
+ Revisions:
+ (none)
+ ....................................................................... -->
+
+<!-- Block Phrasal
+
+ address, blockquote, pre, h1, h2, h3, h4, h5, h6
+
+ This module declares the elements and their attributes used to
+ support block-level phrasal markup.
+-->
+
+<!ENTITY % address.element "INCLUDE" >
+<![%address.element;[
+<!ENTITY % address.content
+ "( #PCDATA | %Inline.mix; )*" >
+<!ENTITY % address.qname "address" >
+<!ELEMENT %address.qname; %address.content; >
+<!-- end of address.element -->]]>
+
+<!ENTITY % address.attlist "INCLUDE" >
+<![%address.attlist;[
+<!ATTLIST %address.qname;
+ %Common.attrib;
+>
+<!-- end of address.attlist -->]]>
+
+<!ENTITY % blockquote.element "INCLUDE" >
+<![%blockquote.element;[
+<!ENTITY % blockquote.content
+ "( %Block.mix; )+"
+>
+<!ENTITY % blockquote.qname "blockquote" >
+<!ELEMENT %blockquote.qname; %blockquote.content; >
+<!-- end of blockquote.element -->]]>
+
+<!ENTITY % blockquote.attlist "INCLUDE" >
+<![%blockquote.attlist;[
+<!ATTLIST %blockquote.qname;
+ %Common.attrib;
+ cite %URI.datatype; #IMPLIED
+>
+<!-- end of blockquote.attlist -->]]>
+
+<!ENTITY % pre.element "INCLUDE" >
+<![%pre.element;[
+<!ENTITY % pre.content
+ "( #PCDATA
+ | %InlStruct.class;
+ %InlPhras.class;
+ | %tt.qname; | %i.qname; | %b.qname;
+ %I18n.class;
+ %Anchor.class;
+ | %script.qname; | %map.qname;
+ %Inline.extra; )*"
+>
+<!ENTITY % pre.qname "pre" >
+<!ELEMENT %pre.qname; %pre.content; >
+<!-- end of pre.element -->]]>
+
+<!ENTITY % pre.attlist "INCLUDE" >
+<![%pre.attlist;[
+<!ATTLIST %pre.qname;
+ %Common.attrib;
+ xml:space ( preserve ) #FIXED 'preserve'
+>
+<!-- end of pre.attlist -->]]>
+
+<!-- ................... Heading Elements ................... -->
+
+<!ENTITY % Heading.content "( #PCDATA | %Inline.mix; )*" >
+
+<!ENTITY % h1.element "INCLUDE" >
+<![%h1.element;[
+<!ENTITY % h1.qname "h1" >
+<!ELEMENT %h1.qname; %Heading.content; >
+<!-- end of h1.element -->]]>
+
+<!ENTITY % h1.attlist "INCLUDE" >
+<![%h1.attlist;[
+<!ATTLIST %h1.qname;
+ %Common.attrib;
+>
+<!-- end of h1.attlist -->]]>
+
+<!ENTITY % h2.element "INCLUDE" >
+<![%h2.element;[
+<!ENTITY % h2.qname "h2" >
+<!ELEMENT %h2.qname; %Heading.content; >
+<!-- end of h2.element -->]]>
+
+<!ENTITY % h2.attlist "INCLUDE" >
+<![%h2.attlist;[
+<!ATTLIST %h2.qname;
+ %Common.attrib;
+>
+<!-- end of h2.attlist -->]]>
+
+<!ENTITY % h3.element "INCLUDE" >
+<![%h3.element;[
+<!ENTITY % h3.qname "h3" >
+<!ELEMENT %h3.qname; %Heading.content; >
+<!-- end of h3.element -->]]>
+
+<!ENTITY % h3.attlist "INCLUDE" >
+<![%h3.attlist;[
+<!ATTLIST %h3.qname;
+ %Common.attrib;
+>
+<!-- end of h3.attlist -->]]>
+
+<!ENTITY % h4.element "INCLUDE" >
+<![%h4.element;[
+<!ENTITY % h4.qname "h4" >
+<!ELEMENT %h4.qname; %Heading.content; >
+<!-- end of h4.element -->]]>
+
+<!ENTITY % h4.attlist "INCLUDE" >
+<![%h4.attlist;[
+<!ATTLIST %h4.qname;
+ %Common.attrib;
+>
+<!-- end of h4.attlist -->]]>
+
+<!ENTITY % h5.element "INCLUDE" >
+<![%h5.element;[
+<!ENTITY % h5.qname "h5" >
+<!ELEMENT %h5.qname; %Heading.content; >
+<!-- end of h5.element -->]]>
+
+<!ENTITY % h5.attlist "INCLUDE" >
+<![%h5.attlist;[
+<!ATTLIST %h5.qname;
+ %Common.attrib;
+>
+<!-- end of h5.attlist -->]]>
+
+<!ENTITY % h6.element "INCLUDE" >
+<![%h6.element;[
+<!ENTITY % h6.qname "h6" >
+<!ELEMENT %h6.qname; %Heading.content; >
+<!-- end of h6.element -->]]>
+
+<!ENTITY % h6.attlist "INCLUDE" >
+<![%h6.attlist;[
+<!ATTLIST %h6.qname;
+ %Common.attrib;
+>
+<!-- end of h6.attlist -->]]>
+
+<!-- end of xhtml-blkphras-1.mod -->
+]]>
+
+<!-- end of xhtml-text-1.mod -->
+]]>
+
+<!-- Hypertext Module (required) ................................. -->
+<!ENTITY % xhtml-hypertext.module "INCLUDE" >
+<![%xhtml-hypertext.module;[
+<!ENTITY % xhtml-hypertext.mod
+ PUBLIC "-//W3C//ELEMENTS XHTML Hypertext 1.0//EN"
+ "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-hypertext-1.mod" >
+<!-- ...................................................................... -->
+<!-- XHTML Hypertext Module .............................................. -->
+<!-- file: xhtml-hypertext-1.mod
+
+ This is XHTML, a reformulation of HTML as a modular XML application.
+ Copyright 1998-2001 W3C (MIT, INRIA, Keio), All Rights Reserved.
+ Revision: $Id: xhtml-hypertext-1.mod,v 4.0 2001/04/02 22:42:49 altheim Exp $ SMI
+
+ This DTD module is identified by the PUBLIC and SYSTEM identifiers:
+
+ PUBLIC "-//W3C//ELEMENTS XHTML Hypertext 1.0//EN"
+ SYSTEM "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-hypertext-1.mod"
+
+ Revisions:
+ (none)
+ ....................................................................... -->
+
+<!-- Hypertext
+
+ a
+
+ This module declares the anchor ('a') element type, which
+ defines the source of a hypertext link. The destination
+ (or link 'target') is identified via its 'id' attribute
+ rather than the 'name' attribute as was used in HTML.
+-->
+
+<!-- ............ Anchor Element ............ -->
+
+<!ENTITY % a.element "INCLUDE" >
+<![%a.element;[
+<!ENTITY % a.content
+ "( #PCDATA | %InlNoAnchor.mix; )*"
+>
+<!ENTITY % a.qname "a" >
+<!ELEMENT %a.qname; %a.content; >
+<!-- end of a.element -->]]>
+
+<!ENTITY % a.attlist "INCLUDE" >
+<![%a.attlist;[
+<!ATTLIST %a.qname;
+ %Common.attrib;
+ href %URI.datatype; #IMPLIED
+ charset %Charset.datatype; #IMPLIED
+ type %ContentType.datatype; #IMPLIED
+ hreflang %LanguageCode.datatype; #IMPLIED
+ rel %LinkTypes.datatype; #IMPLIED
+ rev %LinkTypes.datatype; #IMPLIED
+ accesskey %Character.datatype; #IMPLIED
+ tabindex %Number.datatype; #IMPLIED
+>
+<!-- end of a.attlist -->]]>
+
+<!-- end of xhtml-hypertext-1.mod -->
+]]>
+
+<!-- Lists Module (required) .................................... -->
+<!ENTITY % xhtml-list.module "INCLUDE" >
+<![%xhtml-list.module;[
+<!ENTITY % xhtml-list.mod
+ PUBLIC "-//W3C//ELEMENTS XHTML Lists 1.0//EN"
+ "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-list-1.mod" >
+<!-- ...................................................................... -->
+<!-- XHTML Lists Module .................................................. -->
+<!-- file: xhtml-list-1.mod
+
+ This is XHTML, a reformulation of HTML as a modular XML application.
+ Copyright 1998-2001 W3C (MIT, INRIA, Keio), All Rights Reserved.
+ Revision: $Id: xhtml-list-1.mod,v 4.0 2001/04/02 22:42:49 altheim Exp $ SMI
+
+ This DTD module is identified by the PUBLIC and SYSTEM identifiers:
+
+ PUBLIC "-//W3C//ELEMENTS XHTML Lists 1.0//EN"
+ SYSTEM "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-list-1.mod"
+
+ Revisions:
+ (none)
+ ....................................................................... -->
+
+<!-- Lists
+
+ dl, dt, dd, ol, ul, li
+
+ This module declares the list-oriented element types
+ and their attributes.
+-->
+
+<!ENTITY % dl.qname "dl" >
+<!ENTITY % dt.qname "dt" >
+<!ENTITY % dd.qname "dd" >
+<!ENTITY % ol.qname "ol" >
+<!ENTITY % ul.qname "ul" >
+<!ENTITY % li.qname "li" >
+
+<!-- dl: Definition List ............................... -->
+
+<!ENTITY % dl.element "INCLUDE" >
+<![%dl.element;[
+<!ENTITY % dl.content "( %dt.qname; | %dd.qname; )+" >
+<!ELEMENT %dl.qname; %dl.content; >
+<!-- end of dl.element -->]]>
+
+<!ENTITY % dl.attlist "INCLUDE" >
+<![%dl.attlist;[
+<!ATTLIST %dl.qname;
+ %Common.attrib;
+>
+<!-- end of dl.attlist -->]]>
+
+<!-- dt: Definition Term ............................... -->
+
+<!ENTITY % dt.element "INCLUDE" >
+<![%dt.element;[
+<!ENTITY % dt.content
+ "( #PCDATA | %Inline.mix; )*"
+>
+<!ELEMENT %dt.qname; %dt.content; >
+<!-- end of dt.element -->]]>
+
+<!ENTITY % dt.attlist "INCLUDE" >
+<![%dt.attlist;[
+<!ATTLIST %dt.qname;
+ %Common.attrib;
+>
+<!-- end of dt.attlist -->]]>
+
+<!-- dd: Definition Description ........................ -->
+
+<!ENTITY % dd.element "INCLUDE" >
+<![%dd.element;[
+<!ENTITY % dd.content
+ "( #PCDATA | %Flow.mix; )*"
+>
+<!ELEMENT %dd.qname; %dd.content; >
+<!-- end of dd.element -->]]>
+
+<!ENTITY % dd.attlist "INCLUDE" >
+<![%dd.attlist;[
+<!ATTLIST %dd.qname;
+ %Common.attrib;
+>
+<!-- end of dd.attlist -->]]>
+
+<!-- ol: Ordered List (numbered styles) ................ -->
+
+<!ENTITY % ol.element "INCLUDE" >
+<![%ol.element;[
+<!ENTITY % ol.content "( %li.qname; )+" >
+<!ELEMENT %ol.qname; %ol.content; >
+<!-- end of ol.element -->]]>
+
+<!ENTITY % ol.attlist "INCLUDE" >
+<![%ol.attlist;[
+<!ATTLIST %ol.qname;
+ %Common.attrib;
+>
+<!-- end of ol.attlist -->]]>
+
+<!-- ul: Unordered List (bullet styles) ................ -->
+
+<!ENTITY % ul.element "INCLUDE" >
+<![%ul.element;[
+<!ENTITY % ul.content "( %li.qname; )+" >
+<!ELEMENT %ul.qname; %ul.content; >
+<!-- end of ul.element -->]]>
+
+<!ENTITY % ul.attlist "INCLUDE" >
+<![%ul.attlist;[
+<!ATTLIST %ul.qname;
+ %Common.attrib;
+>
+<!-- end of ul.attlist -->]]>
+
+<!-- li: List Item ..................................... -->
+
+<!ENTITY % li.element "INCLUDE" >
+<![%li.element;[
+<!ENTITY % li.content
+ "( #PCDATA | %Flow.mix; )*"
+>
+<!ELEMENT %li.qname; %li.content; >
+<!-- end of li.element -->]]>
+
+<!ENTITY % li.attlist "INCLUDE" >
+<![%li.attlist;[
+<!ATTLIST %li.qname;
+ %Common.attrib;
+>
+<!-- end of li.attlist -->]]>
+
+<!-- end of xhtml-list-1.mod -->
+]]>
+
+<!-- ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: -->
+
+<!-- Edit Module ................................................ -->
+<!ENTITY % xhtml-edit.module "INCLUDE" >
+<![%xhtml-edit.module;[
+<!ENTITY % xhtml-edit.mod
+ PUBLIC "-//W3C//ELEMENTS XHTML Editing Elements 1.0//EN"
+ "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-edit-1.mod" >
+<!-- ...................................................................... -->
+<!-- XHTML Editing Elements Module ....................................... -->
+<!-- file: xhtml-edit-1.mod
+
+ This is XHTML, a reformulation of HTML as a modular XML application.
+ Copyright 1998-2001 W3C (MIT, INRIA, Keio), All Rights Reserved.
+ Revision: $Id: xhtml-edit-1.mod,v 4.0 2001/04/02 22:42:49 altheim Exp $ SMI
+
+ This DTD module is identified by the PUBLIC and SYSTEM identifiers:
+
+ PUBLIC "-//W3C//ELEMENTS XHTML Editing Markup 1.0//EN"
+ SYSTEM "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-edit-1.mod"
+
+ Revisions:
+ (none)
+ ....................................................................... -->
+
+<!-- Editing Elements
+
+ ins, del
+
+ This module declares element types and attributes used to indicate
+ inserted and deleted content while editing a document.
+-->
+
+<!-- ins: Inserted Text ............................... -->
+
+<!ENTITY % ins.element "INCLUDE" >
+<![%ins.element;[
+<!ENTITY % ins.content
+ "( #PCDATA | %Flow.mix; )*"
+>
+<!ENTITY % ins.qname "ins" >
+<!ELEMENT %ins.qname; %ins.content; >
+<!-- end of ins.element -->]]>
+
+<!ENTITY % ins.attlist "INCLUDE" >
+<![%ins.attlist;[
+<!ATTLIST %ins.qname;
+ %Common.attrib;
+ cite %URI.datatype; #IMPLIED
+ datetime %Datetime.datatype; #IMPLIED
+>
+<!-- end of ins.attlist -->]]>
+
+<!-- del: Deleted Text ................................ -->
+
+<!ENTITY % del.element "INCLUDE" >
+<![%del.element;[
+<!ENTITY % del.content
+ "( #PCDATA | %Flow.mix; )*"
+>
+<!ENTITY % del.qname "del" >
+<!ELEMENT %del.qname; %del.content; >
+<!-- end of del.element -->]]>
+
+<!ENTITY % del.attlist "INCLUDE" >
+<![%del.attlist;[
+<!ATTLIST %del.qname;
+ %Common.attrib;
+ cite %URI.datatype; #IMPLIED
+ datetime %Datetime.datatype; #IMPLIED
+>
+<!-- end of del.attlist -->]]>
+
+<!-- end of xhtml-edit-1.mod -->
+]]>
+
+<!-- BIDI Override Module ....................................... -->
+<!ENTITY % xhtml-bdo.module "%XHTML.bidi;" >
+<![%xhtml-bdo.module;[
+<!ENTITY % xhtml-bdo.mod
+ PUBLIC "-//W3C//ELEMENTS XHTML BIDI Override Element 1.0//EN"
+ "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-bdo-1.mod" >
+<!-- ...................................................................... -->
+<!-- XHTML BDO Element Module ............................................. -->
+<!-- file: xhtml-bdo-1.mod
+
+ This is XHTML, a reformulation of HTML as a modular XML application.
+ Copyright 1998-2001 W3C (MIT, INRIA, Keio), All Rights Reserved.
+ Revision: $Id: xhtml-bdo-1.mod,v 4.0 2001/04/02 22:42:49 altheim Exp $ SMI
+
+ This DTD module is identified by the PUBLIC and SYSTEM identifiers:
+
+ PUBLIC "-//W3C//ELEMENTS XHTML BDO Element 1.0//EN"
+ SYSTEM "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-bdo-1.mod"
+
+ Revisions:
+ (none)
+ ....................................................................... -->
+
+<!-- Bidirectional Override (bdo) Element
+
+ This modules declares the element 'bdo', used to override the
+ Unicode bidirectional algorithm for selected fragments of text.
+
+ DEPENDENCIES:
+ Relies on the conditional section keyword %XHTML.bidi; declared
+ as "INCLUDE". Bidirectional text support includes both the bdo
+ element and the 'dir' attribute.
+-->
+
+<!ENTITY % bdo.element "INCLUDE" >
+<![%bdo.element;[
+<!ENTITY % bdo.content
+ "( #PCDATA | %Inline.mix; )*"
+>
+<!ENTITY % bdo.qname "bdo" >
+<!ELEMENT %bdo.qname; %bdo.content; >
+<!-- end of bdo.element -->]]>
+
+<!ENTITY % bdo.attlist "INCLUDE" >
+<![%bdo.attlist;[
+<!ATTLIST %bdo.qname;
+ %Core.attrib;
+ xml:lang %LanguageCode.datatype; #IMPLIED
+ dir ( ltr | rtl ) #REQUIRED
+>
+]]>
+
+<!-- end of xhtml-bdo-1.mod -->
+]]>
+
+<!-- Ruby Module ................................................ -->
+<!ENTITY % Ruby.common.attlists "INCLUDE" >
+<!ENTITY % Ruby.common.attrib "%Common.attrib;" >
+<!ENTITY % xhtml-ruby.module "INCLUDE" >
+<![%xhtml-ruby.module;[
+<!ENTITY % xhtml-ruby.mod
+ PUBLIC "-//W3C//ELEMENTS XHTML Ruby 1.0//EN"
+ "http://www.w3.org/TR/ruby/xhtml-ruby-1.mod" >
+<!-- ...................................................................... -->
+<!-- XHTML Ruby Module .................................................... -->
+<!-- file: xhtml-ruby-1.mod
+
+ This is XHTML, a reformulation of HTML as a modular XML application.
+ Copyright 1999-2001 W3C (MIT, INRIA, Keio), All Rights Reserved.
+ Revision: $Id: xhtml-ruby-1.mod,v 4.0 2001/04/03 23:14:33 altheim Exp $
+
+ This module is based on the W3C Ruby Annotation Specification:
+
+ http://www.w3.org/TR/ruby
+
+ This DTD module is identified by the PUBLIC and SYSTEM identifiers:
+
+ PUBLIC "-//W3C//ELEMENTS XHTML Ruby 1.0//EN"
+ SYSTEM "http://www.w3.org/TR/ruby/xhtml-ruby-1.mod"
+
+ ...................................................................... -->
+
+<!-- Ruby Elements
+
+ ruby, rbc, rtc, rb, rt, rp
+
+ This module declares the elements and their attributes used to
+ support ruby annotation markup.
+-->
+
+<!-- declare qualified element type names:
+-->
+<!ENTITY % ruby.qname "ruby" >
+<!ENTITY % rbc.qname "rbc" >
+<!ENTITY % rtc.qname "rtc" >
+<!ENTITY % rb.qname "rb" >
+<!ENTITY % rt.qname "rt" >
+<!ENTITY % rp.qname "rp" >
+
+<!-- rp fallback is included by default.
+-->
+<!ENTITY % Ruby.fallback "INCLUDE" >
+<!ENTITY % Ruby.fallback.mandatory "IGNORE" >
+
+<!-- Complex ruby is included by default; it may be
+ overridden by other modules to ignore it.
+-->
+<!ENTITY % Ruby.complex "INCLUDE" >
+
+<!-- Fragments for the content model of the ruby element -->
+<![%Ruby.fallback;[
+<![%Ruby.fallback.mandatory;[
+<!ENTITY % Ruby.content.simple
+ "( %rb.qname;, %rp.qname;, %rt.qname;, %rp.qname; )"
+>
+]]>
+<!ENTITY % Ruby.content.simple
+ "( %rb.qname;, ( %rt.qname; | ( %rp.qname;, %rt.qname;, %rp.qname; ) ) )"
+>
+]]>
+<!ENTITY % Ruby.content.simple "( %rb.qname;, %rt.qname; )" >
+
+<![%Ruby.complex;[
+<!ENTITY % Ruby.content.complex
+ "| ( %rbc.qname;, %rtc.qname;, %rtc.qname;? )"
+>
+]]>
+<!ENTITY % Ruby.content.complex "" >
+
+<!-- Content models of the rb and the rt elements are intended to
+ allow other inline-level elements of its parent markup language,
+ but it should not include ruby descendent elements. The following
+ parameter entity %NoRuby.content; can be used to redefine
+ those content models with minimum effort. It's defined as
+ '( #PCDATA )' by default.
+-->
+<!ENTITY % NoRuby.content "( #PCDATA )" >
+
+<!-- one or more digits (NUMBER) -->
+<!ENTITY % Number.datatype "CDATA" >
+
+<!-- ruby element ...................................... -->
+
+<!ENTITY % ruby.element "INCLUDE" >
+<![%ruby.element;[
+<!ENTITY % ruby.content
+ "( %Ruby.content.simple; %Ruby.content.complex; )"
+>
+<!ELEMENT %ruby.qname; %ruby.content; >
+<!-- end of ruby.element -->]]>
+
+<![%Ruby.complex;[
+<!-- rbc (ruby base component) element ................. -->
+
+<!ENTITY % rbc.element "INCLUDE" >
+<![%rbc.element;[
+<!ENTITY % rbc.content
+ "(%rb.qname;)+"
+>
+<!ELEMENT %rbc.qname; %rbc.content; >
+<!-- end of rbc.element -->]]>
+
+<!-- rtc (ruby text component) element ................. -->
+
+<!ENTITY % rtc.element "INCLUDE" >
+<![%rtc.element;[
+<!ENTITY % rtc.content
+ "(%rt.qname;)+"
+>
+<!ELEMENT %rtc.qname; %rtc.content; >
+<!-- end of rtc.element -->]]>
+]]>
+
+<!-- rb (ruby base) element ............................ -->
+
+<!ENTITY % rb.element "INCLUDE" >
+<![%rb.element;[
+<!-- %rb.content; uses %NoRuby.content; as its content model,
+ which is '( #PCDATA )' by default. It may be overridden
+ by other modules to allow other inline-level elements
+ of its parent markup language, but it should not include
+ ruby descendent elements.
+-->
+<!ENTITY % rb.content "%NoRuby.content;" >
+<!ELEMENT %rb.qname; %rb.content; >
+<!-- end of rb.element -->]]>
+
+<!-- rt (ruby text) element ............................ -->
+
+<!ENTITY % rt.element "INCLUDE" >
+<![%rt.element;[
+<!-- %rt.content; uses %NoRuby.content; as its content model,
+ which is '( #PCDATA )' by default. It may be overridden
+ by other modules to allow other inline-level elements
+ of its parent markup language, but it should not include
+ ruby descendent elements.
+-->
+<!ENTITY % rt.content "%NoRuby.content;" >
+
+<!ELEMENT %rt.qname; %rt.content; >
+<!-- end of rt.element -->]]>
+
+<!-- rbspan attribute is used for complex ruby only ...... -->
+<![%Ruby.complex;[
+<!ENTITY % rt.attlist "INCLUDE" >
+<![%rt.attlist;[
+<!ATTLIST %rt.qname;
+ rbspan %Number.datatype; "1"
+>
+<!-- end of rt.attlist -->]]>
+]]>
+
+<!-- rp (ruby parenthesis) element ..................... -->
+
+<![%Ruby.fallback;[
+<!ENTITY % rp.element "INCLUDE" >
+<![%rp.element;[
+<!ENTITY % rp.content
+ "( #PCDATA )"
+>
+<!ELEMENT %rp.qname; %rp.content; >
+<!-- end of rp.element -->]]>
+]]>
+
+<!-- Ruby Common Attributes
+
+ The following optional ATTLIST declarations provide an easy way
+ to define common attributes for ruby elements. These declarations
+ are ignored by default.
+
+ Ruby elements are intended to have common attributes of its
+ parent markup language. For example, if a markup language defines
+ common attributes as a parameter entity %attrs;, you may add
+ those attributes by just declaring the following parameter entities
+
+ <!ENTITY % Ruby.common.attlists "INCLUDE" >
+ <!ENTITY % Ruby.common.attrib "%attrs;" >
+
+ before including the Ruby module.
+-->
+
+<!ENTITY % Ruby.common.attlists "IGNORE" >
+<![%Ruby.common.attlists;[
+<!ENTITY % Ruby.common.attrib "" >
+
+<!-- common attributes for ruby ........................ -->
+
+<!ENTITY % Ruby.common.attlist "INCLUDE" >
+<![%Ruby.common.attlist;[
+<!ATTLIST %ruby.qname;
+ %Ruby.common.attrib;
+>
+<!-- end of Ruby.common.attlist -->]]>
+
+<![%Ruby.complex;[
+<!-- common attributes for rbc ......................... -->
+
+<!ENTITY % Rbc.common.attlist "INCLUDE" >
+<![%Rbc.common.attlist;[
+<!ATTLIST %rbc.qname;
+ %Ruby.common.attrib;
+>
+<!-- end of Rbc.common.attlist -->]]>
+
+<!-- common attributes for rtc ......................... -->
+
+<!ENTITY % Rtc.common.attlist "INCLUDE" >
+<![%Rtc.common.attlist;[
+<!ATTLIST %rtc.qname;
+ %Ruby.common.attrib;
+>
+<!-- end of Rtc.common.attlist -->]]>
+]]>
+
+<!-- common attributes for rb .......................... -->
+
+<!ENTITY % Rb.common.attlist "INCLUDE" >
+<![%Rb.common.attlist;[
+<!ATTLIST %rb.qname;
+ %Ruby.common.attrib;
+>
+<!-- end of Rb.common.attlist -->]]>
+
+<!-- common attributes for rt .......................... -->
+
+<!ENTITY % Rt.common.attlist "INCLUDE" >
+<![%Rt.common.attlist;[
+<!ATTLIST %rt.qname;
+ %Ruby.common.attrib;
+>
+<!-- end of Rt.common.attlist -->]]>
+
+<![%Ruby.fallback;[
+<!-- common attributes for rp .......................... -->
+
+<!ENTITY % Rp.common.attlist "INCLUDE" >
+<![%Rp.common.attlist;[
+<!ATTLIST %rp.qname;
+ %Ruby.common.attrib;
+>
+<!-- end of Rp.common.attlist -->]]>
+]]>
+]]>
+
+<!-- end of xhtml-ruby-1.mod -->
+]]>
+
+<!-- Presentation Module ........................................ -->
+<!ENTITY % xhtml-pres.module "INCLUDE" >
+<![%xhtml-pres.module;[
+<!ENTITY % xhtml-pres.mod
+ PUBLIC "-//W3C//ELEMENTS XHTML Presentation 1.0//EN"
+ "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-pres-1.mod" >
+<!-- ...................................................................... -->
+<!-- XHTML Presentation Module ............................................ -->
+<!-- file: xhtml-pres-1.mod
+
+ This is XHTML, a reformulation of HTML as a modular XML application.
+ Copyright 1998-2001 W3C (MIT, INRIA, Keio), All Rights Reserved.
+ Revision: $Id: xhtml-pres-1.mod,v 4.0 2001/04/02 22:42:49 altheim Exp $ SMI
+
+ This DTD module is identified by the PUBLIC and SYSTEM identifiers:
+
+ PUBLIC "-//W3C//ELEMENTS XHTML Presentation 1.0//EN"
+ SYSTEM "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-pres-1.mod"
+
+ Revisions:
+ (none)
+ ....................................................................... -->
+
+<!-- Presentational Elements
+
+ This module defines elements and their attributes for
+ simple presentation-related markup.
+-->
+
+<!ENTITY % xhtml-inlpres.module "INCLUDE" >
+<![%xhtml-inlpres.module;[
+<!ENTITY % xhtml-inlpres.mod
+ PUBLIC "-//W3C//ELEMENTS XHTML Inline Presentation 1.0//EN"
+ "xhtml-inlpres-1.mod" >
+<!-- ...................................................................... -->
+<!-- XHTML Inline Presentation Module .................................... -->
+<!-- file: xhtml-inlpres-1.mod
+
+ This is XHTML, a reformulation of HTML as a modular XML application.
+ Copyright 1998-2001 W3C (MIT, INRIA, Keio), All Rights Reserved.
+ Revision: $Id: xhtml-inlpres-1.mod,v 4.0 2001/04/02 22:42:49 altheim Exp $ SMI
+
+ This DTD module is identified by the PUBLIC and SYSTEM identifiers:
+
+ PUBLIC "-//W3C//ELEMENTS XHTML Inline Presentation 1.0//EN"
+ SYSTEM "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-inlpres-1.mod"
+
+ Revisions:
+ (none)
+ ....................................................................... -->
+
+<!-- Inline Presentational Elements
+
+ b, big, i, small, sub, sup, tt
+
+ This module declares the elements and their attributes used to
+ support inline-level presentational markup.
+-->
+
+<!ENTITY % b.element "INCLUDE" >
+<![%b.element;[
+<!ENTITY % b.content
+ "( #PCDATA | %Inline.mix; )*"
+>
+<!ENTITY % b.qname "b" >
+<!ELEMENT %b.qname; %b.content; >
+<!-- end of b.element -->]]>
+
+<!ENTITY % b.attlist "INCLUDE" >
+<![%b.attlist;[
+<!ATTLIST %b.qname;
+ %Common.attrib;
+>
+<!-- end of b.attlist -->]]>
+
+<!ENTITY % big.element "INCLUDE" >
+<![%big.element;[
+<!ENTITY % big.content
+ "( #PCDATA | %Inline.mix; )*"
+>
+<!ENTITY % big.qname "big" >
+<!ELEMENT %big.qname; %big.content; >
+<!-- end of big.element -->]]>
+
+<!ENTITY % big.attlist "INCLUDE" >
+<![%big.attlist;[
+<!ATTLIST %big.qname;
+ %Common.attrib;
+>
+<!-- end of big.attlist -->]]>
+
+<!ENTITY % i.element "INCLUDE" >
+<![%i.element;[
+<!ENTITY % i.content
+ "( #PCDATA | %Inline.mix; )*"
+>
+<!ENTITY % i.qname "i" >
+<!ELEMENT %i.qname; %i.content; >
+<!-- end of i.element -->]]>
+
+<!ENTITY % i.attlist "INCLUDE" >
+<![%i.attlist;[
+<!ATTLIST %i.qname;
+ %Common.attrib;
+>
+<!-- end of i.attlist -->]]>
+
+<!ENTITY % small.element "INCLUDE" >
+<![%small.element;[
+<!ENTITY % small.content
+ "( #PCDATA | %Inline.mix; )*"
+>
+<!ENTITY % small.qname "small" >
+<!ELEMENT %small.qname; %small.content; >
+<!-- end of small.element -->]]>
+
+<!ENTITY % small.attlist "INCLUDE" >
+<![%small.attlist;[
+<!ATTLIST %small.qname;
+ %Common.attrib;
+>
+<!-- end of small.attlist -->]]>
+
+<!ENTITY % sub.element "INCLUDE" >
+<![%sub.element;[
+<!ENTITY % sub.content
+ "( #PCDATA | %Inline.mix; )*"
+>
+<!ENTITY % sub.qname "sub" >
+<!ELEMENT %sub.qname; %sub.content; >
+<!-- end of sub.element -->]]>
+
+<!ENTITY % sub.attlist "INCLUDE" >
+<![%sub.attlist;[
+<!ATTLIST %sub.qname;
+ %Common.attrib;
+>
+<!-- end of sub.attlist -->]]>
+
+<!ENTITY % sup.element "INCLUDE" >
+<![%sup.element;[
+<!ENTITY % sup.content
+ "( #PCDATA | %Inline.mix; )*"
+>
+<!ENTITY % sup.qname "sup" >
+<!ELEMENT %sup.qname; %sup.content; >
+<!-- end of sup.element -->]]>
+
+<!ENTITY % sup.attlist "INCLUDE" >
+<![%sup.attlist;[
+<!ATTLIST %sup.qname;
+ %Common.attrib;
+>
+<!-- end of sup.attlist -->]]>
+
+<!ENTITY % tt.element "INCLUDE" >
+<![%tt.element;[
+<!ENTITY % tt.content
+ "( #PCDATA | %Inline.mix; )*"
+>
+<!ENTITY % tt.qname "tt" >
+<!ELEMENT %tt.qname; %tt.content; >
+<!-- end of tt.element -->]]>
+
+<!ENTITY % tt.attlist "INCLUDE" >
+<![%tt.attlist;[
+<!ATTLIST %tt.qname;
+ %Common.attrib;
+>
+<!-- end of tt.attlist -->]]>
+
+<!-- end of xhtml-inlpres-1.mod -->
+]]>
+
+<!ENTITY % xhtml-blkpres.module "INCLUDE" >
+<![%xhtml-blkpres.module;[
+<!ENTITY % xhtml-blkpres.mod
+ PUBLIC "-//W3C//ELEMENTS XHTML Block Presentation 1.0//EN"
+ "xhtml-blkpres-1.mod" >
+<!-- ...................................................................... -->
+<!-- XHTML Block Presentation Module ..................................... -->
+<!-- file: xhtml-blkpres-1.mod
+
+ This is XHTML, a reformulation of HTML as a modular XML application.
+ Copyright 1998-2001 W3C (MIT, INRIA, Keio), All Rights Reserved.
+ Revision: $Id: xhtml-blkpres-1.mod,v 4.0 2001/04/02 22:42:49 altheim Exp $ SMI
+
+ This DTD module is identified by the PUBLIC and SYSTEM identifiers:
+
+ PUBLIC "-//W3C//ELEMENTS XHTML Block Presentation 1.0//EN"
+ SYSTEM "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-blkpres-1.mod"
+
+ Revisions:
+ (none)
+ ....................................................................... -->
+
+<!-- Block Presentational Elements
+
+ hr
+
+ This module declares the elements and their attributes used to
+ support block-level presentational markup.
+-->
+
+<!ENTITY % hr.element "INCLUDE" >
+<![%hr.element;[
+<!ENTITY % hr.content "EMPTY" >
+<!ENTITY % hr.qname "hr" >
+<!ELEMENT %hr.qname; %hr.content; >
+<!-- end of hr.element -->]]>
+
+<!ENTITY % hr.attlist "INCLUDE" >
+<![%hr.attlist;[
+<!ATTLIST %hr.qname;
+ %Common.attrib;
+>
+<!-- end of hr.attlist -->]]>
+
+<!-- end of xhtml-blkpres-1.mod -->
+]]>
+
+<!-- end of xhtml-pres-1.mod -->
+]]>
+
+<!-- Link Element Module ........................................ -->
+<!ENTITY % xhtml-link.module "INCLUDE" >
+<![%xhtml-link.module;[
+<!ENTITY % xhtml-link.mod
+ PUBLIC "-//W3C//ELEMENTS XHTML Link Element 1.0//EN"
+ "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-link-1.mod" >
+<!-- ...................................................................... -->
+<!-- XHTML Link Element Module ........................................... -->
+<!-- file: xhtml-link-1.mod
+
+ This is XHTML, a reformulation of HTML as a modular XML application.
+ Copyright 1998-2001 W3C (MIT, INRIA, Keio), All Rights Reserved.
+ Revision: $Id: xhtml-link-1.mod,v 4.0 2001/04/02 22:42:49 altheim Exp $ SMI
+
+ This DTD module is identified by the PUBLIC and SYSTEM identifiers:
+
+ PUBLIC "-//W3C//ELEMENTS XHTML Link Element 1.0//EN"
+ SYSTEM "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-link-1.mod"
+
+ Revisions:
+ (none)
+ ....................................................................... -->
+
+<!-- Link element
+
+ link
+
+ This module declares the link element type and its attributes,
+ which could (in principle) be used to define document-level links
+ to external resources such as:
+
+ a) for document specific toolbars/menus, e.g. start, contents,
+ previous, next, index, end, help
+ b) to link to a separate style sheet (rel="stylesheet")
+ c) to make a link to a script (rel="script")
+ d) by stylesheets to control how collections of html nodes are
+ rendered into printed documents
+ e) to make a link to a printable version of this document
+ e.g. a postscript or pdf version (rel="alternate" media="print")
+-->
+
+<!-- link: Media-Independent Link ...................... -->
+
+<!ENTITY % link.element "INCLUDE" >
+<![%link.element;[
+<!ENTITY % link.content "EMPTY" >
+<!ENTITY % link.qname "link" >
+<!ELEMENT %link.qname; %link.content; >
+<!-- end of link.element -->]]>
+
+<!ENTITY % link.attlist "INCLUDE" >
+<![%link.attlist;[
+<!ATTLIST %link.qname;
+ %Common.attrib;
+ charset %Charset.datatype; #IMPLIED
+ href %URI.datatype; #IMPLIED
+ hreflang %LanguageCode.datatype; #IMPLIED
+ type %ContentType.datatype; #IMPLIED
+ rel %LinkTypes.datatype; #IMPLIED
+ rev %LinkTypes.datatype; #IMPLIED
+ media %MediaDesc.datatype; #IMPLIED
+>
+<!-- end of link.attlist -->]]>
+
+<!-- end of xhtml-link-1.mod -->
+]]>
+
+<!-- Document Metainformation Module ............................ -->
+<!ENTITY % xhtml-meta.module "INCLUDE" >
+<![%xhtml-meta.module;[
+<!ENTITY % xhtml-meta.mod
+ PUBLIC "-//W3C//ELEMENTS XHTML Metainformation 1.0//EN"
+ "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-meta-1.mod" >
+<!-- ...................................................................... -->
+<!-- XHTML Document Metainformation Module ............................... -->
+<!-- file: xhtml-meta-1.mod
+
+ This is XHTML, a reformulation of HTML as a modular XML application.
+ Copyright 1998-2001 W3C (MIT, INRIA, Keio), All Rights Reserved.
+ Revision: $Id: xhtml-meta-1.mod,v 4.0 2001/04/02 22:42:49 altheim Exp $ SMI
+
+ This DTD module is identified by the PUBLIC and SYSTEM identifiers:
+
+ PUBLIC "-//W3C//ELEMENTS XHTML Metainformation 1.0//EN"
+ SYSTEM "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-meta-1.mod"
+
+ Revisions:
+ (none)
+ ....................................................................... -->
+
+<!-- Meta Information
+
+ meta
+
+ This module declares the meta element type and its attributes,
+ used to provide declarative document metainformation.
+-->
+
+<!-- meta: Generic Metainformation ..................... -->
+
+<!ENTITY % meta.element "INCLUDE" >
+<![%meta.element;[
+<!ENTITY % meta.content "EMPTY" >
+<!ENTITY % meta.qname "meta" >
+<!ELEMENT %meta.qname; %meta.content; >
+<!-- end of meta.element -->]]>
+
+<!ENTITY % meta.attlist "INCLUDE" >
+<![%meta.attlist;[
+<!ATTLIST %meta.qname;
+ %XHTML.xmlns.attrib;
+ %I18n.attrib;
+ http-equiv NMTOKEN #IMPLIED
+ name NMTOKEN #IMPLIED
+ content CDATA #REQUIRED
+ scheme CDATA #IMPLIED
+>
+<!-- end of meta.attlist -->]]>
+
+<!-- end of xhtml-meta-1.mod -->
+]]>
+
+<!-- Base Element Module ........................................ -->
+<!ENTITY % xhtml-base.module "INCLUDE" >
+<![%xhtml-base.module;[
+<!ENTITY % xhtml-base.mod
+ PUBLIC "-//W3C//ELEMENTS XHTML Base Element 1.0//EN"
+ "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-base-1.mod" >
+<!-- ...................................................................... -->
+<!-- XHTML Base Element Module ........................................... -->
+<!-- file: xhtml-base-1.mod
+
+ This is XHTML, a reformulation of HTML as a modular XML application.
+ Copyright 1998-2001 W3C (MIT, INRIA, Keio), All Rights Reserved.
+ Revision: $Id: xhtml-base-1.mod,v 4.0 2001/04/02 22:42:49 altheim Exp $ SMI
+
+ This DTD module is identified by the PUBLIC and SYSTEM identifiers:
+
+ PUBLIC "-//W3C//ELEMENTS XHTML Base Element 1.0//EN"
+ SYSTEM "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-base-1.mod"
+
+ Revisions:
+ (none)
+ ....................................................................... -->
+
+<!-- Base element
+
+ base
+
+ This module declares the base element type and its attributes,
+ used to define a base URI against which relative URIs in the
+ document will be resolved.
+
+ Note that this module also redeclares the content model for
+ the head element to include the base element.
+-->
+
+<!-- base: Document Base URI ........................... -->
+
+<!ENTITY % base.element "INCLUDE" >
+<![%base.element;[
+<!ENTITY % base.content "EMPTY" >
+<!ENTITY % base.qname "base" >
+<!ELEMENT %base.qname; %base.content; >
+<!-- end of base.element -->]]>
+
+<!ENTITY % base.attlist "INCLUDE" >
+<![%base.attlist;[
+<!ATTLIST %base.qname;
+ %XHTML.xmlns.attrib;
+ href %URI.datatype; #REQUIRED
+>
+<!-- end of base.attlist -->]]>
+
+<!ENTITY % head.content
+ "( %HeadOpts.mix;,
+ ( ( %title.qname;, %HeadOpts.mix;, ( %base.qname;, %HeadOpts.mix; )? )
+ | ( %base.qname;, %HeadOpts.mix;, ( %title.qname;, %HeadOpts.mix; ))))"
+>
+
+<!-- end of xhtml-base-1.mod -->
+]]>
+
+<!-- Scripting Module ........................................... -->
+<!ENTITY % xhtml-script.module "INCLUDE" >
+<![%xhtml-script.module;[
+<!ENTITY % xhtml-script.mod
+ PUBLIC "-//W3C//ELEMENTS XHTML Scripting 1.0//EN"
+ "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-script-1.mod" >
+<!-- ...................................................................... -->
+<!-- XHTML Document Scripting Module ..................................... -->
+<!-- file: xhtml-script-1.mod
+
+ This is XHTML, a reformulation of HTML as a modular XML application.
+ Copyright 1998-2001 W3C (MIT, INRIA, Keio), All Rights Reserved.
+ Revision: $Id: xhtml-script-1.mod,v 4.0 2001/04/02 22:42:49 altheim Exp $ SMI
+
+ This DTD module is identified by the PUBLIC and SYSTEM identifiers:
+
+ PUBLIC "-//W3C//ELEMENTS XHTML Scripting 1.0//EN"
+ SYSTEM "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-script-1.mod"
+
+ Revisions:
+ (none)
+ ....................................................................... -->
+
+<!-- Scripting
+
+ script, noscript
+
+ This module declares element types and attributes used to provide
+ support for executable scripts as well as an alternate content
+ container where scripts are not supported.
+-->
+
+<!-- script: Scripting Statement ....................... -->
+
+<!ENTITY % script.element "INCLUDE" >
+<![%script.element;[
+<!ENTITY % script.content "( #PCDATA )" >
+<!ENTITY % script.qname "script" >
+<!ELEMENT %script.qname; %script.content; >
+<!-- end of script.element -->]]>
+
+<!ENTITY % script.attlist "INCLUDE" >
+<![%script.attlist;[
+<!ATTLIST %script.qname;
+ %XHTML.xmlns.attrib;
+ charset %Charset.datatype; #IMPLIED
+ type %ContentType.datatype; #REQUIRED
+ src %URI.datatype; #IMPLIED
+ defer ( defer ) #IMPLIED
+ xml:space ( preserve ) #FIXED 'preserve'
+>
+<!-- end of script.attlist -->]]>
+
+<!-- noscript: No-Script Alternate Content ............. -->
+
+<!ENTITY % noscript.element "INCLUDE" >
+<![%noscript.element;[
+<!ENTITY % noscript.content
+ "( %Block.mix; )+"
+>
+<!ENTITY % noscript.qname "noscript" >
+<!ELEMENT %noscript.qname; %noscript.content; >
+<!-- end of noscript.element -->]]>
+
+<!ENTITY % noscript.attlist "INCLUDE" >
+<![%noscript.attlist;[
+<!ATTLIST %noscript.qname;
+ %Common.attrib;
+>
+<!-- end of noscript.attlist -->]]>
+
+<!-- end of xhtml-script-1.mod -->
+]]>
+
+<!-- Style Sheets Module ......................................... -->
+<!ENTITY % xhtml-style.module "INCLUDE" >
+<![%xhtml-style.module;[
+<!ENTITY % xhtml-style.mod
+ PUBLIC "-//W3C//ELEMENTS XHTML Style Sheets 1.0//EN"
+ "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-style-1.mod" >
+<!-- ...................................................................... -->
+<!-- XHTML Document Style Sheet Module .................................... -->
+<!-- file: xhtml-style-1.mod
+
+ This is XHTML, a reformulation of HTML as a modular XML application.
+ Copyright 1998-2001 W3C (MIT, INRIA, Keio), All Rights Reserved.
+ Revision: $Id: xhtml-style-1.mod,v 4.0 2001/04/02 22:42:49 altheim Exp $ SMI
+
+ This DTD module is identified by the PUBLIC and SYSTEM identifiers:
+
+ PUBLIC "-//W3C//DTD XHTML Style Sheets 1.0//EN"
+ SYSTEM "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-style-1.mod"
+
+ Revisions:
+ (none)
+ ....................................................................... -->
+
+<!-- Style Sheets
+
+ style
+
+ This module declares the style element type and its attributes,
+ used to embed stylesheet information in the document head element.
+-->
+
+<!-- style: Style Sheet Information ..................... -->
+
+<!ENTITY % style.element "INCLUDE" >
+<![%style.element;[
+<!ENTITY % style.content "( #PCDATA )" >
+<!ENTITY % style.qname "style" >
+<!ELEMENT %style.qname; %style.content; >
+<!-- end of style.element -->]]>
+
+<!ENTITY % style.attlist "INCLUDE" >
+<![%style.attlist;[
+<!ATTLIST %style.qname;
+ %XHTML.xmlns.attrib;
+ %title.attrib;
+ %I18n.attrib;
+ type %ContentType.datatype; #REQUIRED
+ media %MediaDesc.datatype; #IMPLIED
+ xml:space ( preserve ) #FIXED 'preserve'
+>
+<!-- end of style.attlist -->]]>
+
+<!-- end of xhtml-style-1.mod -->
+]]>
+
+<!-- Image Module ............................................... -->
+<!ENTITY % xhtml-image.module "INCLUDE" >
+<![%xhtml-image.module;[
+<!ENTITY % xhtml-image.mod
+ PUBLIC "-//W3C//ELEMENTS XHTML Images 1.0//EN"
+ "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-image-1.mod" >
+<!-- ...................................................................... -->
+<!-- XHTML Images Module ................................................. -->
+<!-- file: xhtml-image-1.mod
+
+ This is XHTML, a reformulation of HTML as a modular XML application.
+ Copyright 1998-2001 W3C (MIT, INRIA, Keio), All Rights Reserved.
+ Rovision: $Id: xhtml-image-1.mod,v 4.0 2001/04/02 22:42:49 altheim Exp $ SMI
+
+ This DTD module is identified by the PUBLIC and SYSTEM identifiers:
+
+ PUBLIC "-//W3C//ELEMENTS XHTML Images 1.0//EN"
+ SYSTEM "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-image-1.mod"
+
+ Revisions:
+ (none)
+ ....................................................................... -->
+
+<!-- Images
+
+ img
+
+ This module provides markup to support basic image embedding.
+-->
+
+<!-- To avoid problems with text-only UAs as well as to make
+ image content understandable and navigable to users of
+ non-visual UAs, you need to provide a description with
+ the 'alt' attribute, and avoid server-side image maps.
+-->
+
+<!ENTITY % img.element "INCLUDE" >
+<![%img.element;[
+<!ENTITY % img.content "EMPTY" >
+<!ENTITY % img.qname "img" >
+<!ELEMENT %img.qname; %img.content; >
+<!-- end of img.element -->]]>
+
+<!ENTITY % img.attlist "INCLUDE" >
+<![%img.attlist;[
+<!ATTLIST %img.qname;
+ %Common.attrib;
+ src %URI.datatype; #REQUIRED
+ alt %Text.datatype; #REQUIRED
+ longdesc %URI.datatype; #IMPLIED
+ height %Length.datatype; #IMPLIED
+ width %Length.datatype; #IMPLIED
+>
+<!-- end of img.attlist -->]]>
+
+<!-- end of xhtml-image-1.mod -->
+]]>
+
+<!-- Client-side Image Map Module ............................... -->
+<!ENTITY % xhtml-csismap.module "INCLUDE" >
+<![%xhtml-csismap.module;[
+<!ENTITY % xhtml-csismap.mod
+ PUBLIC "-//W3C//ELEMENTS XHTML Client-side Image Maps 1.0//EN"
+ "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-csismap-1.mod" >
+<!-- ...................................................................... -->
+<!-- XHTML Client-side Image Map Module .................................. -->
+<!-- file: xhtml-csismap-1.mod
+
+ This is XHTML, a reformulation of HTML as a modular XML application.
+ Copyright 1998-2001 W3C (MIT, INRIA, Keio), All Rights Reserved.
+ Revision: $Id: xhtml-csismap-1.mod,v 4.0 2001/04/02 22:42:49 altheim Exp $ SMI
+
+ This DTD module is identified by the PUBLIC and SYSTEM identifiers:
+
+ PUBLIC "-//W3C//ELEMENTS XHTML Client-side Image Maps 1.0//EN"
+ SYSTEM "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-csismap-1.mod"
+
+ Revisions:
+ (none)
+ ....................................................................... -->
+
+<!-- Client-side Image Maps
+
+ area, map
+
+ This module declares elements and attributes to support client-side
+ image maps. This requires that the Image Module (or a module
+ declaring the img element type) be included in the DTD.
+
+ These can be placed in the same document or grouped in a
+ separate document, although the latter isn't widely supported
+-->
+
+<!ENTITY % area.element "INCLUDE" >
+<![%area.element;[
+<!ENTITY % area.content "EMPTY" >
+<!ENTITY % area.qname "area" >
+<!ELEMENT %area.qname; %area.content; >
+<!-- end of area.element -->]]>
+
+<!ENTITY % Shape.datatype "( rect | circle | poly | default )">
+<!ENTITY % Coords.datatype "CDATA" >
+
+<!ENTITY % area.attlist "INCLUDE" >
+<![%area.attlist;[
+<!ATTLIST %area.qname;
+ %Common.attrib;
+ href %URI.datatype; #IMPLIED
+ shape %Shape.datatype; 'rect'
+ coords %Coords.datatype; #IMPLIED
+ nohref ( nohref ) #IMPLIED
+ alt %Text.datatype; #REQUIRED
+ tabindex %Number.datatype; #IMPLIED
+ accesskey %Character.datatype; #IMPLIED
+>
+<!-- end of area.attlist -->]]>
+
+<!-- modify anchor attribute definition list
+ to allow for client-side image maps
+-->
+<!ATTLIST %a.qname;
+ shape %Shape.datatype; 'rect'
+ coords %Coords.datatype; #IMPLIED
+>
+
+<!-- modify img attribute definition list
+ to allow for client-side image maps
+-->
+<!ATTLIST %img.qname;
+ usemap IDREF #IMPLIED
+>
+
+<!-- modify form input attribute definition list
+ to allow for client-side image maps
+-->
+<!ATTLIST %input.qname;
+ usemap IDREF #IMPLIED
+>
+
+<!-- modify object attribute definition list
+ to allow for client-side image maps
+-->
+<!ATTLIST %object.qname;
+ usemap IDREF #IMPLIED
+>
+
+<!-- 'usemap' points to the 'id' attribute of a <map> element,
+ which must be in the same document; support for external
+ document maps was not widely supported in HTML and is
+ eliminated in XHTML.
+
+ It is considered an error for the element pointed to by
+ a usemap IDREF to occur in anything but a <map> element.
+-->
+
+<!ENTITY % map.element "INCLUDE" >
+<![%map.element;[
+<!ENTITY % map.content
+ "(( %Block.mix; ) | %area.qname; )+"
+>
+<!ENTITY % map.qname "map" >
+<!ELEMENT %map.qname; %map.content; >
+<!-- end of map.element -->]]>
+
+<!ENTITY % map.attlist "INCLUDE" >
+<![%map.attlist;[
+<!ATTLIST %map.qname;
+ %XHTML.xmlns.attrib;
+ id ID #REQUIRED
+ %class.attrib;
+ %title.attrib;
+ %Core.extra.attrib;
+ %I18n.attrib;
+ %Events.attrib;
+>
+<!-- end of map.attlist -->]]>
+
+<!-- end of xhtml-csismap-1.mod -->
+]]>
+
+<!-- Server-side Image Map Module ............................... -->
+<!ENTITY % xhtml-ssismap.module "INCLUDE" >
+<![%xhtml-ssismap.module;[
+<!ENTITY % xhtml-ssismap.mod
+ PUBLIC "-//W3C//ELEMENTS XHTML Server-side Image Maps 1.0//EN"
+ "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-ssismap-1.mod" >
+<!-- ...................................................................... -->
+<!-- XHTML Server-side Image Map Module .................................. -->
+<!-- file: xhtml-ssismap-1.mod
+
+ This is XHTML, a reformulation of HTML as a modular XML application.
+ Copyright 1998-2001 W3C (MIT, INRIA, Keio), All Rights Reserved.
+ Revision: $Id: xhtml-ssismap-1.mod,v 4.0 2001/04/02 22:42:49 altheim Exp $ SMI
+
+ This DTD module is identified by the PUBLIC and SYSTEM identifiers:
+
+ PUBLIC "-//W3C//ELEMENTS XHTML Server-side Image Maps 1.0//EN"
+ SYSTEM "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-ssismap-1.mod"
+
+ Revisions:
+#2000-10-22: added declaration for 'ismap' on <input>
+ ....................................................................... -->
+
+<!-- Server-side Image Maps
+
+ This adds the 'ismap' attribute to the img and input elements
+ to support server-side processing of a user selection.
+-->
+
+<!ATTLIST %img.qname;
+ ismap ( ismap ) #IMPLIED
+>
+
+<!ATTLIST %input.qname;
+ ismap ( ismap ) #IMPLIED
+>
+
+<!-- end of xhtml-ssismap-1.mod -->
+]]>
+
+<!-- Param Element Module ....................................... -->
+<!ENTITY % xhtml-param.module "INCLUDE" >
+<![%xhtml-param.module;[
+<!ENTITY % xhtml-param.mod
+ PUBLIC "-//W3C//ELEMENTS XHTML Param Element 1.0//EN"
+ "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-param-1.mod" >
+<!-- ...................................................................... -->
+<!-- XHTML Param Element Module ..................................... -->
+<!-- file: xhtml-param-1.mod
+
+ This is XHTML, a reformulation of HTML as a modular XML application.
+ Copyright 1998-2001 W3C (MIT, INRIA, Keio), All Rights Reserved.
+ Revision: $Id: xhtml-param-1.mod,v 4.0 2001/04/02 22:42:49 altheim Exp $ SMI
+
+ This DTD module is identified by the PUBLIC and SYSTEM identifiers:
+
+ PUBLIC "-//W3C//ELEMENTS XHTML Param Element 1.0//EN"
+ SYSTEM "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-param-1.mod"
+
+ Revisions:
+ (none)
+ ....................................................................... -->
+
+<!-- Parameters for Java Applets and Embedded Objects
+
+ param
+
+ This module provides declarations for the param element,
+ used to provide named property values for the applet
+ and object elements.
+-->
+
+<!-- param: Named Property Value ....................... -->
+
+<!ENTITY % param.element "INCLUDE" >
+<![%param.element;[
+<!ENTITY % param.content "EMPTY" >
+<!ENTITY % param.qname "param" >
+<!ELEMENT %param.qname; %param.content; >
+<!-- end of param.element -->]]>
+
+<!ENTITY % param.attlist "INCLUDE" >
+<![%param.attlist;[
+<!ATTLIST %param.qname;
+ %XHTML.xmlns.attrib;
+ %id.attrib;
+ name CDATA #REQUIRED
+ value CDATA #IMPLIED
+ valuetype ( data | ref | object ) 'data'
+ type %ContentType.datatype; #IMPLIED
+>
+<!-- end of param.attlist -->]]>
+
+<!-- end of xhtml-param-1.mod -->
+]]>
+
+<!-- Embedded Object Module ..................................... -->
+<!ENTITY % xhtml-object.module "INCLUDE" >
+<![%xhtml-object.module;[
+<!ENTITY % xhtml-object.mod
+ PUBLIC "-//W3C//ELEMENTS XHTML Embedded Object 1.0//EN"
+ "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-object-1.mod" >
+<!-- ...................................................................... -->
+<!-- XHTML Embedded Object Module ........................................ -->
+<!-- file: xhtml-object-1.mod
+
+ This is XHTML, a reformulation of HTML as a modular XML application.
+ Copyright 1998-2001 W3C (MIT, INRIA, Keio), All Rights Reserved.
+ Revision: $Id: xhtml-object-1.mod,v 4.0 2001/04/02 22:42:49 altheim Exp $ SMI
+
+ This DTD module is identified by the PUBLIC and SYSTEM identifiers:
+
+ PUBLIC "-//W3C//ELEMENTS XHTML Embedded Object 1.0//EN"
+ SYSTEM "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-object-1.mod"
+
+ Revisions:
+ (none)
+ ....................................................................... -->
+
+<!-- Embedded Objects
+
+ object
+
+ This module declares the object element type and its attributes, used
+ to embed external objects as part of XHTML pages. In the document,
+ place param elements prior to other content within the object element.
+
+ Note that use of this module requires instantiation of the Param
+ Element Module.
+-->
+
+<!-- object: Generic Embedded Object ................... -->
+
+<!ENTITY % object.element "INCLUDE" >
+<![%object.element;[
+<!ENTITY % object.content
+ "( #PCDATA | %Flow.mix; | %param.qname; )*"
+>
+<!ENTITY % object.qname "object" >
+<!ELEMENT %object.qname; %object.content; >
+<!-- end of object.element -->]]>
+
+<!ENTITY % object.attlist "INCLUDE" >
+<![%object.attlist;[
+<!ATTLIST %object.qname;
+ %Common.attrib;
+ declare ( declare ) #IMPLIED
+ classid %URI.datatype; #IMPLIED
+ codebase %URI.datatype; #IMPLIED
+ data %URI.datatype; #IMPLIED
+ type %ContentType.datatype; #IMPLIED
+ codetype %ContentType.datatype; #IMPLIED
+ archive %URIs.datatype; #IMPLIED
+ standby %Text.datatype; #IMPLIED
+ height %Length.datatype; #IMPLIED
+ width %Length.datatype; #IMPLIED
+ name CDATA #IMPLIED
+ tabindex %Number.datatype; #IMPLIED
+>
+<!-- end of object.attlist -->]]>
+
+<!-- end of xhtml-object-1.mod -->
+]]>
+
+<!-- Tables Module ............................................... -->
+<!ENTITY % xhtml-table.module "INCLUDE" >
+<![%xhtml-table.module;[
+<!ENTITY % xhtml-table.mod
+ PUBLIC "-//W3C//ELEMENTS XHTML Tables 1.0//EN"
+ "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-table-1.mod" >
+<!-- ...................................................................... -->
+<!-- XHTML Table Module .................................................. -->
+<!-- file: xhtml-table-1.mod
+
+ This is XHTML, a reformulation of HTML as a modular XML application.
+ Copyright 1998-2001 W3C (MIT, INRIA, Keio), All Rights Reserved.
+ Revision: $Id: xhtml-table-1.mod,v 4.0 2001/04/02 22:42:49 altheim Exp $ SMI
+
+ This DTD module is identified by the PUBLIC and SYSTEM identifiers:
+
+ PUBLIC "-//W3C//ELEMENTS XHTML Tables 1.0//EN"
+ SYSTEM "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-table-1.mod"
+
+ Revisions:
+ (none)
+ ....................................................................... -->
+
+<!-- Tables
+
+ table, caption, thead, tfoot, tbody, colgroup, col, tr, th, td
+
+ This module declares element types and attributes used to provide
+ table markup similar to HTML 4.0, including features that enable
+ better accessibility for non-visual user agents.
+-->
+
+<!-- declare qualified element type names:
+-->
+<!ENTITY % table.qname "table" >
+<!ENTITY % caption.qname "caption" >
+<!ENTITY % thead.qname "thead" >
+<!ENTITY % tfoot.qname "tfoot" >
+<!ENTITY % tbody.qname "tbody" >
+<!ENTITY % colgroup.qname "colgroup" >
+<!ENTITY % col.qname "col" >
+<!ENTITY % tr.qname "tr" >
+<!ENTITY % th.qname "th" >
+<!ENTITY % td.qname "td" >
+
+<!-- The frame attribute specifies which parts of the frame around
+ the table should be rendered. The values are not the same as
+ CALS to avoid a name clash with the valign attribute.
+-->
+<!ENTITY % frame.attrib
+ "frame ( void
+ | above
+ | below
+ | hsides
+ | lhs
+ | rhs
+ | vsides
+ | box
+ | border ) #IMPLIED"
+>
+
+<!-- The rules attribute defines which rules to draw between cells:
+
+ If rules is absent then assume:
+
+ "none" if border is absent or border="0" otherwise "all"
+-->
+<!ENTITY % rules.attrib
+ "rules ( none
+ | groups
+ | rows
+ | cols
+ | all ) #IMPLIED"
+>
+
+<!-- horizontal alignment attributes for cell contents
+-->
+<!ENTITY % CellHAlign.attrib
+ "align ( left
+ | center
+ | right
+ | justify
+ | char ) #IMPLIED
+ char %Character.datatype; #IMPLIED
+ charoff %Length.datatype; #IMPLIED"
+>
+
+<!-- vertical alignment attribute for cell contents
+-->
+<!ENTITY % CellVAlign.attrib
+ "valign ( top
+ | middle
+ | bottom
+ | baseline ) #IMPLIED"
+>
+
+<!-- scope is simpler than axes attribute for common tables
+-->
+<!ENTITY % scope.attrib
+ "scope ( row
+ | col
+ | rowgroup
+ | colgroup ) #IMPLIED"
+>
+
+<!-- table: Table Element .............................. -->
+
+<!ENTITY % table.element "INCLUDE" >
+<![%table.element;[
+<!ENTITY % table.content
+ "( %caption.qname;?, ( %col.qname;* | %colgroup.qname;* ),
+ (( %thead.qname;?, %tfoot.qname;?, %tbody.qname;+ ) | ( %tr.qname;+ )))"
+>
+<!ELEMENT %table.qname; %table.content; >
+<!-- end of table.element -->]]>
+
+<!ENTITY % table.attlist "INCLUDE" >
+<![%table.attlist;[
+<!ATTLIST %table.qname;
+ %Common.attrib;
+ summary %Text.datatype; #IMPLIED
+ width %Length.datatype; #IMPLIED
+ border %Pixels.datatype; #IMPLIED
+ %frame.attrib;
+ %rules.attrib;
+ cellspacing %Length.datatype; #IMPLIED
+ cellpadding %Length.datatype; #IMPLIED
+>
+<!-- end of table.attlist -->]]>
+
+<!-- caption: Table Caption ............................ -->
+
+<!ENTITY % caption.element "INCLUDE" >
+<![%caption.element;[
+<!ENTITY % caption.content
+ "( #PCDATA | %Inline.mix; )*"
+>
+<!ELEMENT %caption.qname; %caption.content; >
+<!-- end of caption.element -->]]>
+
+<!ENTITY % caption.attlist "INCLUDE" >
+<![%caption.attlist;[
+<!ATTLIST %caption.qname;
+ %Common.attrib;
+>
+<!-- end of caption.attlist -->]]>
+
+<!-- thead: Table Header ............................... -->
+
+<!-- Use thead to duplicate headers when breaking table
+ across page boundaries, or for static headers when
+ tbody sections are rendered in scrolling panel.
+-->
+
+<!ENTITY % thead.element "INCLUDE" >
+<![%thead.element;[
+<!ENTITY % thead.content "( %tr.qname; )+" >
+<!ELEMENT %thead.qname; %thead.content; >
+<!-- end of thead.element -->]]>
+
+<!ENTITY % thead.attlist "INCLUDE" >
+<![%thead.attlist;[
+<!ATTLIST %thead.qname;
+ %Common.attrib;
+ %CellHAlign.attrib;
+ %CellVAlign.attrib;
+>
+<!-- end of thead.attlist -->]]>
+
+<!-- tfoot: Table Footer ............................... -->
+
+<!-- Use tfoot to duplicate footers when breaking table
+ across page boundaries, or for static footers when
+ tbody sections are rendered in scrolling panel.
+-->
+
+<!ENTITY % tfoot.element "INCLUDE" >
+<![%tfoot.element;[
+<!ENTITY % tfoot.content "( %tr.qname; )+" >
+<!ELEMENT %tfoot.qname; %tfoot.content; >
+<!-- end of tfoot.element -->]]>
+
+<!ENTITY % tfoot.attlist "INCLUDE" >
+<![%tfoot.attlist;[
+<!ATTLIST %tfoot.qname;
+ %Common.attrib;
+ %CellHAlign.attrib;
+ %CellVAlign.attrib;
+>
+<!-- end of tfoot.attlist -->]]>
+
+<!-- tbody: Table Body ................................. -->
+
+<!-- Use multiple tbody sections when rules are needed
+ between groups of table rows.
+-->
+
+<!ENTITY % tbody.element "INCLUDE" >
+<![%tbody.element;[
+<!ENTITY % tbody.content "( %tr.qname; )+" >
+<!ELEMENT %tbody.qname; %tbody.content; >
+<!-- end of tbody.element -->]]>
+
+<!ENTITY % tbody.attlist "INCLUDE" >
+<![%tbody.attlist;[
+<!ATTLIST %tbody.qname;
+ %Common.attrib;
+ %CellHAlign.attrib;
+ %CellVAlign.attrib;
+>
+<!-- end of tbody.attlist -->]]>
+
+<!-- colgroup: Table Column Group ...................... -->
+
+<!-- colgroup groups a set of col elements. It allows you
+ to group several semantically-related columns together.
+-->
+
+<!ENTITY % colgroup.element "INCLUDE" >
+<![%colgroup.element;[
+<!ENTITY % colgroup.content "( %col.qname; )*" >
+<!ELEMENT %colgroup.qname; %colgroup.content; >
+<!-- end of colgroup.element -->]]>
+
+<!ENTITY % colgroup.attlist "INCLUDE" >
+<![%colgroup.attlist;[
+<!ATTLIST %colgroup.qname;
+ %Common.attrib;
+ span %Number.datatype; '1'
+ width %MultiLength.datatype; #IMPLIED
+ %CellHAlign.attrib;
+ %CellVAlign.attrib;
+>
+<!-- end of colgroup.attlist -->]]>
+
+<!-- col: Table Column ................................. -->
+
+<!-- col elements define the alignment properties for
+ cells in one or more columns.
+
+ The width attribute specifies the width of the
+ columns, e.g.
+
+ width="64" width in screen pixels
+ width="0.5*" relative width of 0.5
+
+ The span attribute causes the attributes of one
+ col element to apply to more than one column.
+-->
+
+<!ENTITY % col.element "INCLUDE" >
+<![%col.element;[
+<!ENTITY % col.content "EMPTY" >
+<!ELEMENT %col.qname; %col.content; >
+<!-- end of col.element -->]]>
+
+<!ENTITY % col.attlist "INCLUDE" >
+<![%col.attlist;[
+<!ATTLIST %col.qname;
+ %Common.attrib;
+ span %Number.datatype; '1'
+ width %MultiLength.datatype; #IMPLIED
+ %CellHAlign.attrib;
+ %CellVAlign.attrib;
+>
+<!-- end of col.attlist -->]]>
+
+<!-- tr: Table Row ..................................... -->
+
+<!ENTITY % tr.element "INCLUDE" >
+<![%tr.element;[
+<!ENTITY % tr.content "( %th.qname; | %td.qname; )+" >
+<!ELEMENT %tr.qname; %tr.content; >
+<!-- end of tr.element -->]]>
+
+<!ENTITY % tr.attlist "INCLUDE" >
+<![%tr.attlist;[
+<!ATTLIST %tr.qname;
+ %Common.attrib;
+ %CellHAlign.attrib;
+ %CellVAlign.attrib;
+>
+<!-- end of tr.attlist -->]]>
+
+<!-- th: Table Header Cell ............................. -->
+
+<!-- th is for header cells, td for data,
+ but for cells acting as both use td
+-->
+
+<!ENTITY % th.element "INCLUDE" >
+<![%th.element;[
+<!ENTITY % th.content
+ "( #PCDATA | %Flow.mix; )*"
+>
+<!ELEMENT %th.qname; %th.content; >
+<!-- end of th.element -->]]>
+
+<!ENTITY % th.attlist "INCLUDE" >
+<![%th.attlist;[
+<!ATTLIST %th.qname;
+ %Common.attrib;
+ abbr %Text.datatype; #IMPLIED
+ axis CDATA #IMPLIED
+ headers IDREFS #IMPLIED
+ %scope.attrib;
+ rowspan %Number.datatype; '1'
+ colspan %Number.datatype; '1'
+ %CellHAlign.attrib;
+ %CellVAlign.attrib;
+>
+<!-- end of th.attlist -->]]>
+
+<!-- td: Table Data Cell ............................... -->
+
+<!ENTITY % td.element "INCLUDE" >
+<![%td.element;[
+<!ENTITY % td.content
+ "( #PCDATA | %Flow.mix; )*"
+>
+<!ELEMENT %td.qname; %td.content; >
+<!-- end of td.element -->]]>
+
+<!ENTITY % td.attlist "INCLUDE" >
+<![%td.attlist;[
+<!ATTLIST %td.qname;
+ %Common.attrib;
+ abbr %Text.datatype; #IMPLIED
+ axis CDATA #IMPLIED
+ headers IDREFS #IMPLIED
+ %scope.attrib;
+ rowspan %Number.datatype; '1'
+ colspan %Number.datatype; '1'
+ %CellHAlign.attrib;
+ %CellVAlign.attrib;
+>
+<!-- end of td.attlist -->]]>
+
+<!-- end of xhtml-table-1.mod -->
+]]>
+
+<!-- Forms Module ............................................... -->
+<!ENTITY % xhtml-form.module "INCLUDE" >
+<![%xhtml-form.module;[
+<!ENTITY % xhtml-form.mod
+ PUBLIC "-//W3C//ELEMENTS XHTML Forms 1.0//EN"
+ "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-form-1.mod" >
+<!-- ...................................................................... -->
+<!-- XHTML Forms Module .................................................. -->
+<!-- file: xhtml-form-1.mod
+
+ This is XHTML, a reformulation of HTML as a modular XML application.
+ Copyright 1998-2001 W3C (MIT, INRIA, Keio), All Rights Reserved.
+ Revision: $Id: xhtml-form-1.mod,v 4.0 2001/04/02 22:42:49 altheim Exp $ SMI
+
+ This DTD module is identified by the PUBLIC and SYSTEM identifiers:
+
+ PUBLIC "-//W3C//ELEMENTS XHTML Forms 1.0//EN"
+ SYSTEM "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-form-1.mod"
+
+ Revisions:
+ (none)
+ ....................................................................... -->
+
+<!-- Forms
+
+ form, label, input, select, optgroup, option,
+ textarea, fieldset, legend, button
+
+ This module declares markup to provide support for online
+ forms, based on the features found in HTML 4.0 forms.
+-->
+
+<!-- declare qualified element type names:
+-->
+<!ENTITY % form.qname "form" >
+<!ENTITY % label.qname "label" >
+<!ENTITY % input.qname "input" >
+<!ENTITY % select.qname "select" >
+<!ENTITY % optgroup.qname "optgroup" >
+<!ENTITY % option.qname "option" >
+<!ENTITY % textarea.qname "textarea" >
+<!ENTITY % fieldset.qname "fieldset" >
+<!ENTITY % legend.qname "legend" >
+<!ENTITY % button.qname "button" >
+
+<!-- %BlkNoForm.mix; includes all non-form block elements,
+ plus %Misc.class;
+-->
+<!ENTITY % BlkNoForm.mix
+ "%Heading.class;
+ | %List.class;
+ | %BlkStruct.class;
+ %BlkPhras.class;
+ %BlkPres.class;
+ %Table.class;
+ %Block.extra;
+ %Misc.class;"
+>
+
+<!-- form: Form Element ................................ -->
+
+<!ENTITY % form.element "INCLUDE" >
+<![%form.element;[
+<!ENTITY % form.content
+ "( %BlkNoForm.mix;
+ | %fieldset.qname; )+"
+>
+<!ELEMENT %form.qname; %form.content; >
+<!-- end of form.element -->]]>
+
+<!ENTITY % form.attlist "INCLUDE" >
+<![%form.attlist;[
+<!ATTLIST %form.qname;
+ %Common.attrib;
+ action %URI.datatype; #REQUIRED
+ method ( get | post ) 'get'
+ enctype %ContentType.datatype; 'application/x-www-form-urlencoded'
+ accept-charset %Charsets.datatype; #IMPLIED
+ accept %ContentTypes.datatype; #IMPLIED
+>
+<!-- end of form.attlist -->]]>
+
+<!-- label: Form Field Label Text ...................... -->
+
+<!-- Each label must not contain more than ONE field
+-->
+
+<!ENTITY % label.element "INCLUDE" >
+<![%label.element;[
+<!ENTITY % label.content
+ "( #PCDATA
+ | %input.qname; | %select.qname; | %textarea.qname; | %button.qname;
+ | %InlStruct.class;
+ %InlPhras.class;
+ %I18n.class;
+ %InlPres.class;
+ %Anchor.class;
+ %InlSpecial.class;
+ %Inline.extra;
+ %Misc.class; )*"
+>
+<!ELEMENT %label.qname; %label.content; >
+<!-- end of label.element -->]]>
+
+<!ENTITY % label.attlist "INCLUDE" >
+<![%label.attlist;[
+<!ATTLIST %label.qname;
+ %Common.attrib;
+ for IDREF #IMPLIED
+ accesskey %Character.datatype; #IMPLIED
+>
+<!-- end of label.attlist -->]]>
+
+<!-- input: Form Control ............................... -->
+
+<!ENTITY % input.element "INCLUDE" >
+<![%input.element;[
+<!ENTITY % input.content "EMPTY" >
+<!ELEMENT %input.qname; %input.content; >
+<!-- end of input.element -->]]>
+
+<!ENTITY % input.attlist "INCLUDE" >
+<![%input.attlist;[
+<!ENTITY % InputType.class
+ "( text | password | checkbox | radio | submit
+ | reset | file | hidden | image | button )"
+>
+<!-- attribute 'name' required for all but submit & reset
+-->
+<!ATTLIST %input.qname;
+ %Common.attrib;
+ type %InputType.class; 'text'
+ name CDATA #IMPLIED
+ value CDATA #IMPLIED
+ checked ( checked ) #IMPLIED
+ disabled ( disabled ) #IMPLIED
+ readonly ( readonly ) #IMPLIED
+ size %Number.datatype; #IMPLIED
+ maxlength %Number.datatype; #IMPLIED
+ src %URI.datatype; #IMPLIED
+ alt %Text.datatype; #IMPLIED
+ tabindex %Number.datatype; #IMPLIED
+ accesskey %Character.datatype; #IMPLIED
+ accept %ContentTypes.datatype; #IMPLIED
+>
+<!-- end of input.attlist -->]]>
+
+<!-- select: Option Selector ........................... -->
+
+<!ENTITY % select.element "INCLUDE" >
+<![%select.element;[
+<!ENTITY % select.content
+ "( %optgroup.qname; | %option.qname; )+"
+>
+<!ELEMENT %select.qname; %select.content; >
+<!-- end of select.element -->]]>
+
+<!ENTITY % select.attlist "INCLUDE" >
+<![%select.attlist;[
+<!ATTLIST %select.qname;
+ %Common.attrib;
+ name CDATA #IMPLIED
+ size %Number.datatype; #IMPLIED
+ multiple ( multiple ) #IMPLIED
+ disabled ( disabled ) #IMPLIED
+ tabindex %Number.datatype; #IMPLIED
+>
+<!-- end of select.attlist -->]]>
+
+<!-- optgroup: Option Group ............................ -->
+
+<!ENTITY % optgroup.element "INCLUDE" >
+<![%optgroup.element;[
+<!ENTITY % optgroup.content "( %option.qname; )+" >
+<!ELEMENT %optgroup.qname; %optgroup.content; >
+<!-- end of optgroup.element -->]]>
+
+<!ENTITY % optgroup.attlist "INCLUDE" >
+<![%optgroup.attlist;[
+<!ATTLIST %optgroup.qname;
+ %Common.attrib;
+ disabled ( disabled ) #IMPLIED
+ label %Text.datatype; #REQUIRED
+>
+<!-- end of optgroup.attlist -->]]>
+
+<!-- option: Selectable Choice ......................... -->
+
+<!ENTITY % option.element "INCLUDE" >
+<![%option.element;[
+<!ENTITY % option.content "( #PCDATA )" >
+<!ELEMENT %option.qname; %option.content; >
+<!-- end of option.element -->]]>
+
+<!ENTITY % option.attlist "INCLUDE" >
+<![%option.attlist;[
+<!ATTLIST %option.qname;
+ %Common.attrib;
+ selected ( selected ) #IMPLIED
+ disabled ( disabled ) #IMPLIED
+ label %Text.datatype; #IMPLIED
+ value CDATA #IMPLIED
+>
+<!-- end of option.attlist -->]]>
+
+<!-- textarea: Multi-Line Text Field ................... -->
+
+<!ENTITY % textarea.element "INCLUDE" >
+<![%textarea.element;[
+<!ENTITY % textarea.content "( #PCDATA )" >
+<!ELEMENT %textarea.qname; %textarea.content; >
+<!-- end of textarea.element -->]]>
+
+<!ENTITY % textarea.attlist "INCLUDE" >
+<![%textarea.attlist;[
+<!ATTLIST %textarea.qname;
+ %Common.attrib;
+ name CDATA #IMPLIED
+ rows %Number.datatype; #REQUIRED
+ cols %Number.datatype; #REQUIRED
+ disabled ( disabled ) #IMPLIED
+ readonly ( readonly ) #IMPLIED
+ tabindex %Number.datatype; #IMPLIED
+ accesskey %Character.datatype; #IMPLIED
+>
+<!-- end of textarea.attlist -->]]>
+
+<!-- fieldset: Form Control Group ...................... -->
+
+<!-- #PCDATA is to solve the mixed content problem,
+ per specification only whitespace is allowed
+-->
+
+<!ENTITY % fieldset.element "INCLUDE" >
+<![%fieldset.element;[
+<!ENTITY % fieldset.content
+ "( #PCDATA | %legend.qname; | %Flow.mix; )*"
+>
+<!ELEMENT %fieldset.qname; %fieldset.content; >
+<!-- end of fieldset.element -->]]>
+
+<!ENTITY % fieldset.attlist "INCLUDE" >
+<![%fieldset.attlist;[
+<!ATTLIST %fieldset.qname;
+ %Common.attrib;
+>
+<!-- end of fieldset.attlist -->]]>
+
+<!-- legend: Fieldset Legend ........................... -->
+
+<!ENTITY % legend.element "INCLUDE" >
+<![%legend.element;[
+<!ENTITY % legend.content
+ "( #PCDATA | %Inline.mix; )*"
+>
+<!ELEMENT %legend.qname; %legend.content; >
+<!-- end of legend.element -->]]>
+
+<!ENTITY % legend.attlist "INCLUDE" >
+<![%legend.attlist;[
+<!ATTLIST %legend.qname;
+ %Common.attrib;
+ accesskey %Character.datatype; #IMPLIED
+>
+<!-- end of legend.attlist -->]]>
+
+<!-- button: Push Button ............................... -->
+
+<!ENTITY % button.element "INCLUDE" >
+<![%button.element;[
+<!ENTITY % button.content
+ "( #PCDATA
+ | %BlkNoForm.mix;
+ | %InlStruct.class;
+ %InlPhras.class;
+ %InlPres.class;
+ %I18n.class;
+ %InlSpecial.class;
+ %Inline.extra; )*"
+>
+<!ELEMENT %button.qname; %button.content; >
+<!-- end of button.element -->]]>
+
+<!ENTITY % button.attlist "INCLUDE" >
+<![%button.attlist;[
+<!ATTLIST %button.qname;
+ %Common.attrib;
+ name CDATA #IMPLIED
+ value CDATA #IMPLIED
+ type ( button | submit | reset ) 'submit'
+ disabled ( disabled ) #IMPLIED
+ tabindex %Number.datatype; #IMPLIED
+ accesskey %Character.datatype; #IMPLIED
+>
+<!-- end of button.attlist -->]]>
+
+<!-- end of xhtml-form-1.mod -->
+]]>
+
+<!-- Legacy Markup ............................................... -->
+<!ENTITY % xhtml-legacy.module "IGNORE" >
+<![%xhtml-legacy.module;[
+<!ENTITY % xhtml-legacy.mod
+ PUBLIC "-//W3C//ELEMENTS XHTML Legacy Markup 1.0//EN"
+ "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-legacy-1.mod" >
+%xhtml-legacy.mod;]]>
+
+<!-- Document Structure Module (required) ....................... -->
+<!ENTITY % xhtml-struct.module "INCLUDE" >
+<![%xhtml-struct.module;[
+<!ENTITY % xhtml-struct.mod
+ PUBLIC "-//W3C//ELEMENTS XHTML Document Structure 1.0//EN"
+ "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-struct-1.mod" >
+<!-- ...................................................................... -->
+<!-- XHTML Structure Module .............................................. -->
+<!-- file: xhtml-struct-1.mod
+
+ This is XHTML, a reformulation of HTML as a modular XML application.
+ Copyright 1998-2001 W3C (MIT, INRIA, Keio), All Rights Reserved.
+ Revision: $Id: xhtml-struct-1.mod,v 4.0 2001/04/02 22:42:49 altheim Exp $ SMI
+
+ This DTD module is identified by the PUBLIC and SYSTEM identifiers:
+
+ PUBLIC "-//W3C//ELEMENTS XHTML Document Structure 1.0//EN"
+ SYSTEM "http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-struct-1.mod"
+
+ Revisions:
+ (none)
+ ....................................................................... -->
+
+<!-- Document Structure
+
+ title, head, body, html
+
+ The Structure Module defines the major structural elements and
+ their attributes.
+
+ Note that the content model of the head element type is redeclared
+ when the Base Module is included in the DTD.
+
+ The parameter entity containing the XML namespace URI value used
+ for XHTML is '%XHTML.xmlns;', defined in the Qualified Names module.
+-->
+
+<!-- title: Document Title ............................. -->
+
+<!-- The title element is not considered part of the flow of text.
+ It should be displayed, for example as the page header or
+ window title. Exactly one title is required per document.
+-->
+
+<!ENTITY % title.element "INCLUDE" >
+<![%title.element;[
+<!ENTITY % title.content "( #PCDATA )" >
+<!ENTITY % title.qname "title" >
+<!ELEMENT %title.qname; %title.content; >
+<!-- end of title.element -->]]>
+
+<!ENTITY % title.attlist "INCLUDE" >
+<![%title.attlist;[
+<!ATTLIST %title.qname;
+ %XHTML.xmlns.attrib;
+ %I18n.attrib;
+>
+<!-- end of title.attlist -->]]>
+
+<!-- head: Document Head ............................... -->
+
+<!ENTITY % head.element "INCLUDE" >
+<![%head.element;[
+<!ENTITY % head.content
+ "( %HeadOpts.mix;, %title.qname;, %HeadOpts.mix; )"
+>
+<!ENTITY % head.qname "head" >
+<!ELEMENT %head.qname; %head.content; >
+<!-- end of head.element -->]]>
+
+<!ENTITY % head.attlist "INCLUDE" >
+<![%head.attlist;[
+<!-- reserved for future use with document profiles
+-->
+<!ENTITY % profile.attrib
+ "profile %URI.datatype; '%XHTML.profile;'"
+>
+
+<!ATTLIST %head.qname;
+ %XHTML.xmlns.attrib;
+ %I18n.attrib;
+ %profile.attrib;
+>
+<!-- end of head.attlist -->]]>
+
+<!-- body: Document Body ............................... -->
+
+<!ENTITY % body.element "INCLUDE" >
+<![%body.element;[
+<!ENTITY % body.content
+ "( %Block.mix; )+"
+>
+<!ENTITY % body.qname "body" >
+<!ELEMENT %body.qname; %body.content; >
+<!-- end of body.element -->]]>
+
+<!ENTITY % body.attlist "INCLUDE" >
+<![%body.attlist;[
+<!ATTLIST %body.qname;
+ %Common.attrib;
+>
+<!-- end of body.attlist -->]]>
+
+<!-- html: XHTML Document Element ...................... -->
+
+<!ENTITY % html.element "INCLUDE" >
+<![%html.element;[
+<!ENTITY % html.content "( %head.qname;, %body.qname; )" >
+<!ENTITY % html.qname "html" >
+<!ELEMENT %html.qname; %html.content; >
+<!-- end of html.element -->]]>
+
+<!ENTITY % html.attlist "INCLUDE" >
+<![%html.attlist;[
+<!-- version attribute value defined in driver
+-->
+<!ENTITY % XHTML.version.attrib
+ "version %FPI.datatype; #FIXED '%XHTML.version;'"
+>
+
+<!-- see the Qualified Names module for information
+ on how to extend XHTML using XML namespaces
+-->
+<!ATTLIST %html.qname;
+ %XHTML.xmlns.attrib;
+ %XHTML.version.attrib;
+ %I18n.attrib;
+>
+<!-- end of html.attlist -->]]>
+
+<!-- end of xhtml-struct-1.mod -->
+]]>
+
+<!-- end of XHTML 1.1 DTD ................................................. -->
+<!-- ....................................................................... -->
diff --git a/test/System.Web.Helpers.Test/WebCacheTest.cs b/test/System.Web.Helpers.Test/WebCacheTest.cs
new file mode 100644
index 00000000..9cd551dc
--- /dev/null
+++ b/test/System.Web.Helpers.Test/WebCacheTest.cs
@@ -0,0 +1,117 @@
+using System.Collections.Generic;
+using Microsoft.TestCommon;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Helpers.Test
+{
+ public class WebCacheTest
+ {
+ [Fact]
+ public void GetReturnsExpectedValueTest()
+ {
+ string key = DateTime.UtcNow.Ticks.ToString() + "_GetTest";
+ List<string> expected = new List<string>();
+ WebCache.Set(key, expected);
+
+ var actual = WebCache.Get(key);
+
+ Assert.Equal(expected, actual);
+ Assert.Equal(0, actual.Count);
+ }
+
+ [Fact]
+ public void RemoveRemovesRightValueTest()
+ {
+ string key = DateTime.UtcNow.Ticks.ToString() + "_RemoveTest";
+ List<string> expected = new List<string>();
+ WebCache.Set(key, expected);
+
+ var actual = WebCache.Remove(key);
+
+ Assert.Equal(expected, actual);
+ Assert.Equal(0, actual.Count);
+ }
+
+ [Fact]
+ public void RemoveRemovesValueFromCacheTest()
+ {
+ string key = DateTime.UtcNow.Ticks.ToString() + "_RemoveTest2";
+ List<string> expected = new List<string>();
+ WebCache.Set(key, expected);
+
+ var removed = WebCache.Remove(key);
+
+ Assert.Null(WebCache.Get(key));
+ }
+
+ [Fact]
+ public void SetWithAbsoluteExpirationDoesNotThrow()
+ {
+ string key = DateTime.UtcNow.Ticks.ToString() + "SetWithAbsoluteExpirationDoesNotThrow_SetTest";
+ object expected = new object();
+ int minutesToCache = 10;
+ bool slidingExpiration = false;
+ WebCache.Set(key, expected, minutesToCache, slidingExpiration);
+ object actual = WebCache.Get(key);
+ Assert.True(expected == actual);
+ }
+
+ [Fact]
+ public void CanSetWithSlidingExpiration()
+ {
+ string key = DateTime.UtcNow.Ticks.ToString() + "_CanSetWithSlidingExpiration_SetTest";
+ object expected = new object();
+
+ WebCache.Set(key, expected, slidingExpiration: true);
+ object actual = WebCache.Get(key);
+ Assert.True(expected == actual);
+ }
+
+ [Fact]
+ public void SetWithSlidingExpirationForNegativeTime()
+ {
+ string key = DateTime.UtcNow.Ticks.ToString() + "_SetWithSlidingExpirationForNegativeTime_SetTest";
+ object expected = new object();
+ Assert.ThrowsArgumentGreaterThan(() => WebCache.Set(key, expected, -1), "minutesToCache", "0");
+ }
+
+ [Fact]
+ public void SetWithSlidingExpirationForZeroTime()
+ {
+ string key = DateTime.UtcNow.Ticks.ToString() + "_SetWithSlidingExpirationForZeroTime_SetTest";
+ object expected = new object();
+ Assert.ThrowsArgumentGreaterThan(() => WebCache.Set(key, expected, 0), "minutesToCache", "0");
+ }
+
+ [Fact]
+ public void SetWithSlidingExpirationForYear()
+ {
+ string key = DateTime.UtcNow.Ticks.ToString() + "_SetWithSlidingExpirationForYear_SetTest";
+ object expected = new object();
+
+ WebCache.Set(key, expected, 365 * 24 * 60, true);
+ object actual = WebCache.Get(key);
+ Assert.True(expected == actual);
+ }
+
+ [Fact]
+ public void SetWithSlidingExpirationForMoreThanYear()
+ {
+ string key = DateTime.UtcNow.Ticks.ToString() + "_SetWithSlidingExpirationForMoreThanYear_SetTest";
+ object expected = new object();
+ Assert.ThrowsArgumentLessThanOrEqualTo(() => WebCache.Set(key, expected, 365 * 24 * 60 + 1, true), "minutesToCache", (365 * 24 * 60).ToString());
+ }
+
+ [Fact]
+ public void SetWithAbsoluteExpirationForMoreThanYear()
+ {
+ string key = DateTime.UtcNow.Ticks.ToString() + "_SetWithAbsoluteExpirationForMoreThanYear_SetTest";
+ object expected = new object();
+
+ WebCache.Set(key, expected, 365 * 24 * 60, true);
+ object actual = WebCache.Get(key);
+ Assert.True(expected == actual);
+ }
+ }
+}
diff --git a/test/System.Web.Helpers.Test/WebGridDataSourceTest.cs b/test/System.Web.Helpers.Test/WebGridDataSourceTest.cs
new file mode 100644
index 00000000..dec4a09c
--- /dev/null
+++ b/test/System.Web.Helpers.Test/WebGridDataSourceTest.cs
@@ -0,0 +1,305 @@
+using System.Collections.Generic;
+using System.Dynamic;
+using System.Linq;
+using System.Web.WebPages.TestUtils;
+using Moq;
+using Xunit;
+
+namespace System.Web.Helpers.Test
+{
+ public class WebGridDataSourceTest
+ {
+ [Fact]
+ public void WebGridDataSourceReturnsNumberOfItemsAsTotalRowCount()
+ {
+ // Arrange
+ var rows = GetValues();
+ var dataSource = new WebGridDataSource(new WebGrid(GetContext()), values: GetValues(), elementType: typeof(Person), canPage: false, canSort: false);
+
+ // Act and Assert
+ Assert.Equal(rows.Count(), dataSource.TotalRowCount);
+ }
+
+ [Fact]
+ public void WebGridDataSourceReturnsUnsortedListIfSortColumnIsNull()
+ {
+ // Arrange
+ var values = GetValues();
+ var dataSource = new WebGridDataSource(new WebGrid(GetContext()), values: GetValues(), elementType: typeof(Person), canPage: false, canSort: true);
+
+ // Act
+ var rows = dataSource.GetRows(new SortInfo { SortColumn = null }, 0);
+
+ // Assert
+ Assert.True(Enumerable.SequenceEqual<object>(values.ToList(), rows.Select(r => r.Value).ToList(), new PersonComparer()));
+ }
+
+ [Fact]
+ public void WebGridDataSourceReturnsUnsortedListIfSortColumnIsEmpty()
+ {
+ // Arrange
+ var values = GetValues();
+ var dataSource = new WebGridDataSource(new WebGrid(GetContext()), values: GetValues(), elementType: typeof(Person), canPage: false, canSort: true);
+
+ // Act
+ var rows = dataSource.GetRows(new SortInfo { SortColumn = String.Empty }, 0);
+
+ // Assert
+ Assert.True(Enumerable.SequenceEqual<object>(values.ToList(), rows.Select(r => r.Value).ToList(), new PersonComparer()));
+ }
+
+ [Fact]
+ public void WebGridDataSourceReturnsUnsortedListIfSortCannotBeInferred()
+ {
+ // Arrange
+ var values = GetValues();
+ var dataSource = new WebGridDataSource(new WebGrid(GetContext()), values: GetValues(), elementType: typeof(Person), canPage: false, canSort: true);
+
+ // Act
+ var rows = dataSource.GetRows(new SortInfo { SortColumn = "Does-not-exist" }, 0);
+
+ // Assert
+ Assert.True(Enumerable.SequenceEqual<object>(values.ToList(), rows.Select(r => r.Value).ToList(), new PersonComparer()));
+ }
+
+ [Fact]
+ public void WebGridDataSourceReturnsUnsortedListIfDefaultSortCannotBeInferred()
+ {
+ // Arrange
+ var values = GetValues();
+ var defaultSort = new SortInfo { SortColumn = "cannot-be-inferred" };
+ var dataSource = new WebGridDataSource(new WebGrid(GetContext()), values: GetValues(), elementType: typeof(Person), canSort: true, canPage: false) { DefaultSort = defaultSort };
+
+ // Act
+ var rows = dataSource.GetRows(new SortInfo { SortColumn = "Does-not-exist" }, 0);
+
+ // Assert
+ Assert.True(Enumerable.SequenceEqual<object>(values.ToList(), rows.Select(r => r.Value).ToList(), new PersonComparer()));
+ }
+
+ [Fact]
+ public void WebGridDataSourceUsesDefaultSortWhenCurrentSortCannotBeInferred()
+ {
+ // Arrange
+ var values = GetValues();
+ var defaultSort = new SortInfo { SortColumn = "FirstName" };
+ var dataSource = new WebGridDataSource(new WebGrid(GetContext()), values: GetValues(), elementType: typeof(Person), canSort: true, canPage: false) { DefaultSort = defaultSort };
+
+ // Act
+ var rows = dataSource.GetRows(new SortInfo { SortColumn = "Does-not-exist" }, 0);
+
+ // Assert
+ Assert.True(Enumerable.SequenceEqual<object>(values.OrderBy(p => p.FirstName).ToList(), rows.Select(r => r.Value).ToList(), new PersonComparer()));
+ }
+
+ [Fact]
+ public void WebGridDataSourceSortsUsingSpecifiedSort()
+ {
+ // Arrange
+ var defaultSort = new SortInfo { SortColumn = "FirstName", SortDirection = SortDirection.Ascending };
+ IEnumerable<dynamic> values = new[] { new Person { LastName = "Z" }, new Person { LastName = "X" }, new Person { LastName = "Y" } };
+ var dataSource = new WebGridDataSource(new WebGrid(GetContext()), values: values, elementType: typeof(Person), canSort: true, canPage: false) { DefaultSort = defaultSort };
+
+ // Act
+ var rows = dataSource.GetRows(new SortInfo { SortColumn = "LastName" }, 0);
+
+ // Assert
+ Assert.Equal(rows.ElementAt(0).Value.LastName, "X");
+ Assert.Equal(rows.ElementAt(1).Value.LastName, "Y");
+ Assert.Equal(rows.ElementAt(2).Value.LastName, "Z");
+ }
+
+ [Fact]
+ public void WebGridDataSourceSortsDynamicType()
+ {
+ // Arrange
+ IEnumerable<dynamic> values = new[] { new TestDynamicType("col", "val1"), new TestDynamicType("col", "val2"), new TestDynamicType("col", "val3") };
+ var dataSource = new WebGridDataSource(new WebGrid(GetContext()), values: values, elementType: typeof(TestDynamicType), canSort: true, canPage: false);
+
+ // Act
+ var rows = dataSource.GetRows(new SortInfo { SortColumn = "col", SortDirection = SortDirection.Descending }, 0);
+
+ // Assert
+ Assert.Equal(rows.ElementAt(0).Value.col, "val3");
+ Assert.Equal(rows.ElementAt(1).Value.col, "val2");
+ Assert.Equal(rows.ElementAt(2).Value.col, "val1");
+ }
+
+ [Fact]
+ public void WebGridDataSourceWithNestedPropertySortsCorrectly()
+ {
+ // Arrange
+ var element1 = new { Foo = new { Bar = "val2" } };
+ var element2 = new { Foo = new { Bar = "val1" } };
+ var element3 = new { Foo = new { Bar = "val3" } };
+ IEnumerable<dynamic> values = new[] { element1, element2, element3 };
+
+ var dataSource = new WebGridDataSource(new WebGrid(GetContext()), values: values, elementType: element1.GetType(), canSort: true, canPage: false);
+
+ // Act
+ var rows = dataSource.GetRows(new SortInfo { SortColumn = "Foo.Bar", SortDirection = SortDirection.Descending }, 0);
+
+ // Assert
+ Assert.Equal(rows.ElementAt(0).Value.Foo.Bar, "val3");
+ Assert.Equal(rows.ElementAt(1).Value.Foo.Bar, "val2");
+ Assert.Equal(rows.ElementAt(2).Value.Foo.Bar, "val1");
+ }
+
+ [Fact]
+ public void WebGridDataSourceSortsDictionaryBasedDynamicType()
+ {
+ // Arrange
+ var value1 = new DynamicDictionary();
+ value1["col"] = "val1";
+ var value2 = new DynamicDictionary();
+ value2["col"] = "val2";
+ var value3 = new DynamicDictionary();
+ value3["col"] = "val3";
+ IEnumerable<dynamic> values = new[] { value1, value2, value3 };
+ var dataSource = new WebGridDataSource(new WebGrid(GetContext()), values: values, elementType: typeof(TestDynamicType), canSort: true, canPage: false);
+
+ // Act
+ var rows = dataSource.GetRows(new SortInfo { SortColumn = "col", SortDirection = SortDirection.Descending }, 0);
+
+ // Assert
+ Assert.Equal(rows.ElementAt(0).Value.col, "val3");
+ Assert.Equal(rows.ElementAt(1).Value.col, "val2");
+ Assert.Equal(rows.ElementAt(2).Value.col, "val1");
+ }
+
+ [Fact]
+ public void WebGridDataSourceReturnsOriginalDataSourceIfValuesCannotBeSorted()
+ {
+ // Arrange
+ IEnumerable<dynamic> values = new object[] { new TestDynamicType("col", "val1"), new TestDynamicType("col", "val2"), new TestDynamicType("col", DBNull.Value) };
+ var dataSource = new WebGridDataSource(new WebGrid(GetContext()), values: values, elementType: typeof(TestDynamicType), canSort: true, canPage: false);
+
+ // Act
+ var rows = dataSource.GetRows(new SortInfo { SortColumn = "col", SortDirection = SortDirection.Descending }, 0);
+
+ // Assert
+ Assert.Equal(rows.ElementAt(0).Value.col, "val1");
+ Assert.Equal(rows.ElementAt(1).Value.col, "val2");
+ Assert.Equal(rows.ElementAt(2).Value.col, DBNull.Value);
+ }
+
+ [Fact]
+ public void WebGridDataSourceReturnsPagedResultsIfRowsPerPageIsSpecified()
+ {
+ // Arrange
+ IEnumerable<dynamic> values = GetValues();
+ var dataSource = new WebGridDataSource(new WebGrid(GetContext()), values: values, elementType: typeof(Person), canSort: false, canPage: true) { RowsPerPage = 2 };
+
+ // Act
+ var rows = dataSource.GetRows(new SortInfo(), 0);
+
+ // Assert
+ Assert.Equal(rows.Count, 2);
+ Assert.Equal(rows.ElementAt(0).Value.LastName, "B2");
+ Assert.Equal(rows.ElementAt(1).Value.LastName, "A2");
+ }
+
+ [Fact]
+ public void WebGridDataSourceReturnsPagedSortedResultsIfRowsPerPageAndSortAreSpecified()
+ {
+ // Arrange
+ IEnumerable<dynamic> values = GetValues();
+ var dataSource = new WebGridDataSource(new WebGrid(GetContext()), values: values, elementType: typeof(Person), canSort: true, canPage: true) { RowsPerPage = 2 };
+
+ // Act
+ var rows = dataSource.GetRows(new SortInfo { SortColumn = "LastName", SortDirection = SortDirection.Descending }, 0);
+
+ // Assert
+ Assert.Equal(rows.Count, 2);
+ Assert.Equal(rows.ElementAt(0).Value.LastName, "E2");
+ Assert.Equal(rows.ElementAt(1).Value.LastName, "D2");
+ }
+
+ [Fact]
+ public void WebGridDataSourceReturnsFewerThanRowsPerPageIfNumberOfItemsIsInsufficient()
+ {
+ // Arrange
+ IEnumerable<dynamic> values = GetValues();
+ var dataSource = new WebGridDataSource(new WebGrid(GetContext()), values: values, elementType: typeof(Person), canSort: true, canPage: true) { RowsPerPage = 3 };
+
+ // Act
+ var rows = dataSource.GetRows(new SortInfo(), 1);
+
+ // Assert
+ Assert.Equal(rows.Count, 2);
+ Assert.Equal(rows.ElementAt(0).Value.LastName, "C2");
+ Assert.Equal(rows.ElementAt(1).Value.LastName, "E2");
+ }
+
+ [Fact]
+ public void WebGridDataSourceDoesNotThrowIfValuesAreNull()
+ {
+ // Arrange
+ IEnumerable<dynamic> values = new object[] { String.Empty, null, DBNull.Value, null };
+ var dataSource = new WebGridDataSource(new WebGrid(GetContext()), values: values, elementType: typeof(object), canSort: true, canPage: true) { RowsPerPage = 2 };
+
+ // Act
+ var rows = dataSource.GetRows(new SortInfo(), 0);
+
+ // Assert
+ Assert.Equal(rows.Count, 2);
+ Assert.Equal(rows.ElementAt(0).Value, String.Empty);
+ Assert.Null(rows.ElementAt(1).Value);
+ }
+
+ private IEnumerable<Person> GetValues()
+ {
+ return new[]
+ {
+ new Person { FirstName = "B1", LastName = "B2" },
+ new Person { FirstName = "A1", LastName = "A2" },
+ new Person { FirstName = "D1", LastName = "D2" },
+ new Person { FirstName = "C1", LastName = "C2" },
+ new Person { FirstName = "E1", LastName = "E2" },
+ };
+ }
+
+ private class PersonComparer : IEqualityComparer<object>
+ {
+ public new bool Equals(object x, object y)
+ {
+ dynamic xDynamic = x;
+ dynamic yDynamic = y;
+ return (String.Equals(xDynamic.FirstName, yDynamic.FirstName, StringComparison.OrdinalIgnoreCase)
+ && String.Equals(xDynamic.LastName, yDynamic.LastName, StringComparison.OrdinalIgnoreCase));
+ }
+
+ public int GetHashCode(dynamic obj)
+ {
+ return 4; // Random dice roll
+ }
+ }
+
+ private class TestDynamicType : DynamicObject
+ {
+ public Dictionary<string, object> _values = new Dictionary<string, object>();
+
+ public TestDynamicType(string a, object b)
+ {
+ _values[a] = b;
+ }
+
+ public override bool TryGetMember(GetMemberBinder binder, out object result)
+ {
+ return _values.TryGetValue(binder.Name, out result);
+ }
+ }
+
+ private class Person
+ {
+ public string FirstName { get; set; }
+
+ public string LastName { get; set; }
+ }
+
+ private HttpContextBase GetContext()
+ {
+ return new Mock<HttpContextBase>().Object;
+ }
+ }
+}
diff --git a/test/System.Web.Helpers.Test/WebGridTest.cs b/test/System.Web.Helpers.Test/WebGridTest.cs
new file mode 100644
index 00000000..17000236
--- /dev/null
+++ b/test/System.Web.Helpers.Test/WebGridTest.cs
@@ -0,0 +1,2321 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Dynamic;
+using System.Linq;
+using System.Text;
+using System.Web.TestUtil;
+using System.Web.WebPages;
+using Microsoft.TestCommon;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Helpers.Test
+{
+ public class WebGridTest
+ {
+ [Fact]
+ public void AjaxCheckedOnlyOnce()
+ {
+ var grid = new WebGrid(GetContext(), ajaxUpdateContainerId: "grid")
+ .Bind(new[] { new { First = "First", Second = "Second" } });
+ string html = grid.Table().ToString();
+ Assert.True(html.Contains("<script"));
+ html = grid.Table().ToString();
+ Assert.False(html.Contains("<script"));
+ html = grid.Pager().ToString();
+ Assert.False(html.Contains("<script"));
+ }
+
+ [Fact]
+ public void AjaxCallbackIgnoredIfAjaxUpdateContainerIdIsNotSet()
+ {
+ var grid = new WebGrid(GetContext(), ajaxUpdateCallback: "myCallback")
+ .Bind(new[] { new { First = "First", Second = "Second" } });
+ string html = grid.Table().ToString();
+ Assert.False(html.Contains("<script"));
+ Assert.False(html.Contains("myCallback"));
+ }
+
+ [Fact]
+ public void ColumnNameDefaultsExcludesIndexedProperties()
+ {
+ var grid = new WebGrid(GetContext()).Bind(new[] { "First", "Second" });
+ Assert.Equal(1, grid.ColumnNames.Count());
+ Assert.True(grid.ColumnNames.Contains("Length"));
+ }
+
+ [Fact]
+ public void ColumnNameDefaultsForDynamics()
+ {
+ var grid = new WebGrid(GetContext()).Bind(Dynamics(new { First = "First", Second = "Second" }));
+ Assert.Equal(2, grid.ColumnNames.Count());
+ Assert.True(grid.ColumnNames.Contains("First"));
+ Assert.True(grid.ColumnNames.Contains("Second"));
+ }
+
+ [Fact]
+ public void ColumnNameDefaultsForNonDynamics()
+ {
+ var grid = new WebGrid(GetContext()).Bind(new[] { new { First = "First", Second = "Second" } });
+ Assert.Equal(2, grid.ColumnNames.Count());
+ Assert.True(grid.ColumnNames.Contains("First"));
+ Assert.True(grid.ColumnNames.Contains("Second"));
+ }
+
+ [Fact]
+ public void ColumnNameDefaultsSupportsBindableTypes()
+ {
+ var grid = new WebGrid(GetContext()).Bind(new[]
+ {
+ new
+ {
+ DateTime = DateTime.MinValue,
+ DateTimeOffset = DateTimeOffset.MinValue,
+ Decimal = Decimal.MinValue,
+ Guid = Guid.Empty,
+ Int32 = 1,
+ NullableInt32 = (int?)1,
+ Object = new object(),
+ Projection = new { Foo = "Bar" },
+ TimeSpan = TimeSpan.MinValue
+ }
+ });
+ Assert.Equal(7, grid.ColumnNames.Count());
+ Assert.True(grid.ColumnNames.Contains("DateTime"));
+ Assert.True(grid.ColumnNames.Contains("DateTimeOffset"));
+ Assert.True(grid.ColumnNames.Contains("Decimal"));
+ Assert.True(grid.ColumnNames.Contains("Guid"));
+ Assert.True(grid.ColumnNames.Contains("Int32"));
+ Assert.True(grid.ColumnNames.Contains("NullableInt32"));
+ Assert.True(grid.ColumnNames.Contains("TimeSpan"));
+ Assert.False(grid.ColumnNames.Contains("Object"));
+ Assert.False(grid.ColumnNames.Contains("Projection"));
+ }
+
+ [Fact]
+ public void ColumnsIsNoOp()
+ {
+ var grid = new WebGrid(GetContext()).Bind(new[]
+ {
+ new { First = "First", Second = "Second" }
+ });
+ var columns = new[]
+ {
+ grid.Column("First"), grid.Column("Second")
+ };
+ Assert.Equal(columns, grid.Columns(columns));
+ }
+
+ [Fact]
+ public void ColumnThrowsIfColumnNameIsEmptyAndNoFormat()
+ {
+ var grid = new WebGrid(GetContext()).Bind(new object[0]);
+ Assert.ThrowsArgument(() => { grid.Column(columnName: String.Empty, format: null); }, "columnName", "The column name cannot be null or an empty string unless a custom format is specified.");
+ }
+
+ [Fact]
+ public void ColumnThrowsIfColumnNameIsNullAndNoFormat()
+ {
+ var grid = new WebGrid(GetContext()).Bind(new object[0]);
+ Assert.ThrowsArgument(() => { grid.Column(columnName: null, format: null); }, "columnName", "The column name cannot be null or an empty string unless a custom format is specified.");
+ }
+
+ [Fact]
+ public void BindThrowsIfSourceIsNull()
+ {
+ Assert.ThrowsArgumentNull(() => { new WebGrid(GetContext()).Bind(null); }, "source");
+ }
+
+ [Fact]
+ public void ConstructorThrowsIfRowsPerPageIsLessThanOne()
+ {
+ Assert.ThrowsArgumentOutOfRange(() => { new WebGrid(GetContext(), rowsPerPage: 0); }, "rowsPerPage", "Value must be greater than or equal to 1.", allowDerivedExceptions: true);
+ }
+
+ [Fact]
+ public void GetHtmlDefaults()
+ {
+ var grid = new WebGrid(GetContext(), rowsPerPage: 1)
+ .Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" },
+ new { P1 = 4, P2 = '5', P3 = "6" }
+ });
+ var html = grid.GetHtml();
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(
+ "<table><thead><tr>" +
+ "<th scope=\"col\"><a href=\"?sort=P1&amp;sortdir=ASC\">P1</a></th>" +
+ "<th scope=\"col\"><a href=\"?sort=P2&amp;sortdir=ASC\">P2</a></th>" +
+ "<th scope=\"col\"><a href=\"?sort=P3&amp;sortdir=ASC\">P3</a></th>" +
+ "</tr></thead>" +
+ "<tfoot><tr>" +
+ "<td colspan=\"3\">1 <a href=\"?page=2\">2</a> <a href=\"?page=2\">&gt;</a> </td>" +
+ "</tr></tfoot>" +
+ "<tbody><tr><td>1</td><td>2</td><td>3</td></tr></tbody>" +
+ "</table>", html.ToString());
+ XhtmlAssert.Validate1_1(html);
+ }
+
+ [Fact]
+ public void WebGridProducesValidHtmlWhenSummaryIsSpecified()
+ {
+ var grid = new WebGrid(GetContext(), rowsPerPage: 1)
+ .Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" },
+ new { P1 = 4, P2 = '5', P3 = "6" }
+ });
+ var caption = "WebGrid With Caption";
+ var html = grid.GetHtml(caption: caption);
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(
+ "<table>" +
+ "<caption>" + caption + "</caption>" +
+ "<thead><tr>" +
+ "<th scope=\"col\"><a href=\"?sort=P1&amp;sortdir=ASC\">P1</a></th>" +
+ "<th scope=\"col\"><a href=\"?sort=P2&amp;sortdir=ASC\">P2</a></th>" +
+ "<th scope=\"col\"><a href=\"?sort=P3&amp;sortdir=ASC\">P3</a></th>" +
+ "</tr></thead>" +
+ "<tfoot><tr>" +
+ "<td colspan=\"3\">1 <a href=\"?page=2\">2</a> <a href=\"?page=2\">&gt;</a> </td>" +
+ "</tr></tfoot>" +
+ "<tbody><tr><td>1</td><td>2</td><td>3</td></tr></tbody>" +
+ "</table>", html.ToString());
+ XhtmlAssert.Validate1_1(html);
+ }
+
+ [Fact]
+ public void WebGridEncodesCaptionText()
+ {
+ var grid = new WebGrid(GetContext(), rowsPerPage: 1)
+ .Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" },
+ new { P1 = 4, P2 = '5', P3 = "6" }
+ });
+ var caption = "WebGrid <> With Caption";
+ var html = grid.GetHtml(caption: caption);
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(
+ "<table>" +
+ "<caption>WebGrid &lt;&gt; With Caption</caption>" +
+ "<thead><tr>" +
+ "<th scope=\"col\"><a href=\"?sort=P1&amp;sortdir=ASC\">P1</a></th>" +
+ "<th scope=\"col\"><a href=\"?sort=P2&amp;sortdir=ASC\">P2</a></th>" +
+ "<th scope=\"col\"><a href=\"?sort=P3&amp;sortdir=ASC\">P3</a></th>" +
+ "</tr></thead>" +
+ "<tfoot><tr>" +
+ "<td colspan=\"3\">1 <a href=\"?page=2\">2</a> <a href=\"?page=2\">&gt;</a> </td>" +
+ "</tr></tfoot>" +
+ "<tbody><tr><td>1</td><td>2</td><td>3</td></tr></tbody>" +
+ "</table>", html.ToString());
+ XhtmlAssert.Validate1_1(html);
+ }
+
+ [Fact]
+ public void GetHtmlWhenPageCountIsOne()
+ {
+ var grid = new WebGrid(GetContext())
+ .Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" }
+ });
+ var html = grid.GetHtml();
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(
+ "<table><thead><tr>" +
+ "<th scope=\"col\"><a href=\"?sort=P1&amp;sortdir=ASC\">P1</a></th>" +
+ "<th scope=\"col\"><a href=\"?sort=P2&amp;sortdir=ASC\">P2</a></th>" +
+ "<th scope=\"col\"><a href=\"?sort=P3&amp;sortdir=ASC\">P3</a></th>" +
+ "</tr></thead>" +
+ "<tbody><tr><td>1</td><td>2</td><td>3</td></tr></tbody>" +
+ "</table>", html.ToString());
+ XhtmlAssert.Validate1_1(html);
+ }
+
+ [Fact]
+ public void GetHtmlWhenPagingAndSortingAreDisabled()
+ {
+ var grid = new WebGrid(GetContext(), rowsPerPage: 1, canPage: false, canSort: false)
+ .Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" },
+ new { P1 = 4, P2 = '5', P3 = "6" }
+ });
+ var html = grid.GetHtml();
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(
+ "<table><thead><tr>" +
+ "<th scope=\"col\">P1</th>" +
+ "<th scope=\"col\">P2</th>" +
+ "<th scope=\"col\">P3</th>" +
+ "</tr></thead>" +
+ "<tbody>" +
+ "<tr><td>1</td><td>2</td><td>3</td></tr>" +
+ "<tr><td>4</td><td>5</td><td>6</td></tr>" +
+ "</tbody>" +
+ "</table>", html.ToString());
+ XhtmlAssert.Validate1_1(html);
+ }
+
+ [Fact]
+ public void PageIndexCanBeReset()
+ {
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["page"] = "2";
+ var grid = new WebGrid(GetContext(queryString), rowsPerPage: 1)
+ .Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" },
+ new { P1 = 4, P2 = '5', P3 = "6" }
+ });
+ Assert.Equal(1, grid.PageIndex);
+ grid.PageIndex = 0;
+ Assert.Equal(0, grid.PageIndex);
+ // verify that selection link has updated page
+ Assert.Equal("?page=1&row=1", grid.Rows.FirstOrDefault().GetSelectUrl());
+ }
+
+ [Fact]
+ public void PageIndexCanBeResetToSameValue()
+ {
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["page"] = "2";
+ var grid = new WebGrid(GetContext(queryString), rowsPerPage: 1)
+ .Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" },
+ new { P1 = 4, P2 = '5', P3 = "6" }
+ });
+ grid.PageIndex = 0;
+ Assert.Equal(0, grid.PageIndex);
+ }
+
+ [Fact]
+ public void PageIndexDefaultsToZero()
+ {
+ var grid = new WebGrid(GetContext(), rowsPerPage: 1)
+ .Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" },
+ new { P1 = 4, P2 = '5', P3 = "6" }
+ });
+ Assert.Equal(0, grid.PageIndex);
+ Assert.Equal(1, grid.Rows.Count);
+ Assert.Equal(1, grid.Rows.First()["P1"]);
+ }
+
+ [Fact]
+ public void SetPageIndexThrowsExceptionWhenValueIsNegative()
+ {
+ var grid = new WebGrid(GetContext(), rowsPerPage: 1)
+ .Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" },
+ new { P1 = 4, P2 = '5', P3 = "6" }
+ });
+ Assert.ThrowsArgumentOutOfRange(() => { grid.PageIndex = -1; }, "value", "Value must be between 0 and 1.");
+ }
+
+ [Fact]
+ public void SetPageIndexThrowsExceptionWhenValueIsEqualToPageCount()
+ {
+ var grid = new WebGrid(GetContext(), rowsPerPage: 1)
+ .Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" },
+ new { P1 = 4, P2 = '5', P3 = "6" }
+ });
+ Assert.ThrowsArgumentOutOfRange(() => { grid.PageIndex = grid.PageCount; }, "value", "Value must be between 0 and 1.");
+ }
+
+ [Fact]
+ public void SetPageIndexThrowsExceptionWhenValueIsGreaterToPageCount()
+ {
+ var grid = new WebGrid(GetContext(), rowsPerPage: 1)
+ .Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" },
+ new { P1 = 4, P2 = '5', P3 = "6" }
+ });
+ Assert.ThrowsArgumentOutOfRange(() => { grid.PageIndex = grid.PageCount + 1; }, "value", "Value must be between 0 and 1.");
+ }
+
+ [Fact]
+ public void SetPageIndexThrowsExceptionWhenPagingIsDisabled()
+ {
+ var grid = new WebGrid(GetContext(), canPage: false)
+ .Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" },
+ new { P1 = 4, P2 = '5', P3 = "6" }
+ });
+ Assert.Throws<NotSupportedException>(() => { grid.PageIndex = 1; }, "This operation is not supported when paging is disabled for the \"WebGrid\" object.");
+ }
+
+ [Fact]
+ public void PageIndexResetsToLastPageWhenQueryStringValueGreaterThanPageCount()
+ {
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["page"] = "3";
+ var grid = new WebGrid(GetContext(queryString), rowsPerPage: 1)
+ .Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" },
+ new { P1 = 4, P2 = '5', P3 = "6" }
+ });
+ Assert.Equal(1, grid.PageIndex);
+ Assert.Equal(1, grid.Rows.Count);
+ Assert.Equal(4, grid.Rows.First()["P1"]);
+ }
+
+ [Fact]
+ public void PageIndexResetWhenQueryStringValueIsInvalid()
+ {
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["page"] = "NotAnInt";
+ var grid = new WebGrid(GetContext(queryString), rowsPerPage: 1)
+ .Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" },
+ new { P1 = 4, P2 = '5', P3 = "6" }
+ });
+ Assert.Equal(0, grid.PageIndex);
+ Assert.Equal(1, grid.Rows.Count);
+ Assert.Equal(1, grid.Rows.First()["P1"]);
+ }
+
+ [Fact]
+ public void PageIndexResetWhenQueryStringValueLessThanOne()
+ {
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["page"] = "0";
+ var grid = new WebGrid(GetContext(queryString), rowsPerPage: 1)
+ .Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" },
+ new { P1 = 4, P2 = '5', P3 = "6" }
+ });
+ Assert.Equal(0, grid.PageIndex);
+ Assert.Equal(1, grid.Rows.Count);
+ Assert.Equal(1, grid.Rows.First()["P1"]);
+ }
+
+ [Fact]
+ public void PageIndexUsesCustomQueryString()
+ {
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["g_pg"] = "2";
+ var grid = new WebGrid(GetContext(queryString), rowsPerPage: 1, fieldNamePrefix: "g_", pageFieldName: "pg")
+ .Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" },
+ new { P1 = 4, P2 = '5', P3 = "6" }
+ });
+ Assert.Equal(1, grid.PageIndex);
+ Assert.Equal(1, grid.Rows.Count);
+ Assert.Equal(4, grid.Rows.First()["P1"]);
+ }
+
+ [Fact]
+ public void PageIndexUsesQueryString()
+ {
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["page"] = "2";
+ var grid = new WebGrid(GetContext(queryString), rowsPerPage: 1)
+ .Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" },
+ new { P1 = 4, P2 = '5', P3 = "6" }
+ });
+ Assert.Equal(1, grid.PageIndex);
+ Assert.Equal(1, grid.Rows.Count);
+ Assert.Equal(4, grid.Rows.First()["P1"]);
+ }
+
+ [Fact]
+ public void GetPageCountWhenPagingIsTurnedOn()
+ {
+ var grid = new WebGrid(GetContext(), canPage: true, rowsPerPage: 1)
+ .Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" },
+ new { P1 = 4, P2 = '5', P3 = "6" }
+ });
+ Assert.Equal(2, grid.PageCount);
+ }
+
+ [Fact]
+ public void GetPageIndexWhenPagingIsTurnedOn()
+ {
+ var grid = new WebGrid(GetContext(), canPage: true, rowsPerPage: 1)
+ .Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" },
+ new { P1 = 4, P2 = '5', P3 = "6" },
+ new { P1 = 4, P2 = '5', P3 = "6" },
+ });
+ grid.PageIndex = 1;
+ Assert.Equal(1, grid.PageIndex);
+ Assert.Equal(3, grid.PageCount);
+ grid.PageIndex = 2;
+ Assert.Equal(2, grid.PageIndex);
+ }
+
+ [Fact]
+ public void GetPageCountWhenPagingIsTurnedOff()
+ {
+ var grid = new WebGrid(GetContext(), canPage: false, rowsPerPage: 1)
+ .Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" },
+ new { P1 = 4, P2 = '5', P3 = "6" }
+ });
+ Assert.Equal(1, grid.PageCount);
+ }
+
+ [Fact]
+ public void GetPageIndexWhenPagingIsTurnedOff()
+ {
+ var grid = new WebGrid(GetContext(), canPage: false, rowsPerPage: 1)
+ .Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" },
+ new { P1 = 4, P2 = '5', P3 = "6" },
+ new { P1 = 4, P2 = '5', P3 = "6" },
+ });
+ Assert.Equal(0, grid.PageIndex);
+ Assert.Equal(1, grid.PageCount);
+ }
+
+ [Fact]
+ public void PageUrlResetsSelection()
+ {
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["page"] = "0";
+ queryString["row"] = "0";
+ queryString["sort"] = "P1";
+ queryString["sortdir"] = "DESC";
+ var grid = new WebGrid(GetContext(queryString), rowsPerPage: 1)
+ .Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" },
+ new { P1 = 4, P2 = '5', P3 = "6" }
+ });
+ string url = grid.GetPageUrl(1);
+ Assert.Equal("?page=2&sort=P1&sortdir=DESC", url);
+ }
+
+ [Fact]
+ public void PageUrlResetsSelectionForAjax()
+ {
+ string expected40 = "$(&quot;#grid-container&quot;).swhgLoad(&quot;?page=2&amp;sort=P1&amp;sortdir=DESC&quot;,&quot;#grid-container&quot;);";
+ string expected45 = "$(&quot;#grid-container&quot;).swhgLoad(&quot;?page=2\\u0026sort=P1\\u0026sortdir=DESC&quot;,&quot;#grid-container&quot;);";
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["page"] = "0";
+ queryString["row"] = "0";
+ queryString["sort"] = "P1";
+ queryString["sortdir"] = "DESC";
+ var grid = new WebGrid(GetContext(queryString), rowsPerPage: 1, ajaxUpdateContainerId: "grid-container")
+ .Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" },
+ new { P1 = 4, P2 = '5', P3 = "6" }
+ });
+ string html = grid.GetContainerUpdateScript(grid.GetPageUrl(1)).ToString();
+
+ // Assert
+ Assert.Equal(RuntimeEnvironment.IsVersion45Installed ? expected45 : expected40, html);
+ }
+
+ [Fact]
+ public void PageUrlResetsSelectionForAjaxWithCallback()
+ {
+ string expected40 = "$(&quot;#grid&quot;).swhgLoad(&quot;?page=2&amp;sort=P1&amp;sortdir=DESC&quot;,&quot;#grid&quot;,myCallback);";
+ string expected45 = "$(&quot;#grid&quot;).swhgLoad(&quot;?page=2\\u0026sort=P1\\u0026sortdir=DESC&quot;,&quot;#grid&quot;,myCallback);";
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["page"] = "0";
+ queryString["row"] = "0";
+ queryString["sort"] = "P1";
+ queryString["sortdir"] = "DESC";
+ var grid = new WebGrid(GetContext(queryString), rowsPerPage: 1, ajaxUpdateContainerId: "grid", ajaxUpdateCallback: "myCallback")
+ .Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" },
+ new { P1 = 4, P2 = '5', P3 = "6" }
+ });
+ string html = grid.GetContainerUpdateScript(grid.GetPageUrl(1)).ToString();
+
+ // Assert
+ Assert.Equal(RuntimeEnvironment.IsVersion45Installed ? expected45 : expected40, html);
+ }
+
+ [Fact]
+ public void PageUrlThrowsIfIndexGreaterThanOrEqualToPageCount()
+ {
+ var grid = new WebGrid(GetContext(), rowsPerPage: 1).Bind(new[] { new { }, new { } });
+ Assert.ThrowsArgumentOutOfRange(() => { grid.GetPageUrl(2); }, "pageIndex", "Value must be between 0 and 1.");
+ }
+
+ [Fact]
+ public void PageUrlThrowsIfIndexLessThanZero()
+ {
+ var grid = new WebGrid(GetContext(), rowsPerPage: 1).Bind(new[] { new { }, new { } });
+ Assert.ThrowsArgumentOutOfRange(() => { grid.GetPageUrl(-1); }, "pageIndex", "Value must be between 0 and 1.");
+ }
+
+ [Fact]
+ public void PageUrlThrowsIfPagingIsDisabled()
+ {
+ var grid = new WebGrid(GetContext(), rowsPerPage: 1, canPage: false).Bind(new[] { new { }, new { } });
+ Assert.Throws<NotSupportedException>(() => { grid.GetPageUrl(2); }, "This operation is not supported when paging is disabled for the \"WebGrid\" object.");
+ }
+
+ [Fact]
+ public void PagerRenderingDefaults()
+ {
+ var grid = new WebGrid(GetContext(), rowsPerPage: 1).Bind(new[] { new { }, new { }, new { }, new { } });
+ var html = grid.Pager();
+ Assert.Equal(
+ "1 " +
+ "<a href=\"?page=2\">2</a> " +
+ "<a href=\"?page=3\">3</a> " +
+ "<a href=\"?page=4\">4</a> " +
+ "<a href=\"?page=2\">&gt;</a> ",
+ html.ToString());
+ XhtmlAssert.Validate1_1(html, wrapper: "div");
+ }
+
+ [Fact]
+ public void PagerRenderingOnFirstShowingAll()
+ {
+ var grid = new WebGrid(GetContext(), rowsPerPage: 1).Bind(new[] { new { }, new { }, new { }, new { } });
+ var html = grid.Pager(WebGridPagerModes.All, numericLinksCount: 5);
+ Assert.Equal(
+ "1 " +
+ "<a href=\"?page=2\">2</a> " +
+ "<a href=\"?page=3\">3</a> " +
+ "<a href=\"?page=4\">4</a> " +
+ "<a href=\"?page=2\">&gt;</a> " +
+ "<a href=\"?page=4\">&gt;&gt;</a>",
+ html.ToString());
+ XhtmlAssert.Validate1_1(html, wrapper: "div");
+ }
+
+ [Fact]
+ public void PagerRenderingOnNextToLastShowingAll()
+ {
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["page"] = "3";
+ var grid = new WebGrid(GetContext(queryString), rowsPerPage: 1).Bind(new[]
+ {
+ new { }, new { }, new { }, new { }
+ });
+ var html = grid.Pager(WebGridPagerModes.All, numericLinksCount: 4);
+ Assert.Equal(
+ "<a href=\"?page=1\">&lt;&lt;</a> " +
+ "<a href=\"?page=2\">&lt;</a> " +
+ "<a href=\"?page=1\">1</a> " +
+ "<a href=\"?page=2\">2</a> " +
+ "3 " +
+ "<a href=\"?page=4\">4</a> " +
+ "<a href=\"?page=4\">&gt;</a> ",
+ html.ToString());
+ XhtmlAssert.Validate1_1(html, wrapper: "div");
+ }
+
+ [Fact]
+ public void PagerRenderingOnMiddleShowingAll()
+ {
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["page"] = "3";
+ var grid = new WebGrid(GetContext(queryString), rowsPerPage: 1).Bind(new[]
+ {
+ new { }, new { }, new { }, new { }
+ });
+ var html = grid.Pager(WebGridPagerModes.All, numericLinksCount: 3);
+ Assert.Equal(
+ "<a href=\"?page=1\">&lt;&lt;</a> " +
+ "<a href=\"?page=2\">&lt;</a> " +
+ "<a href=\"?page=2\">2</a> " +
+ "3 " +
+ "<a href=\"?page=4\">4</a> " +
+ "<a href=\"?page=4\">&gt;</a> ",
+ html.ToString());
+ XhtmlAssert.Validate1_1(html, wrapper: "div");
+ }
+
+ [Fact]
+ public void PagerRenderingOnSecondHidingFirstLast()
+ {
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["page"] = "2";
+ var grid = new WebGrid(GetContext(queryString), rowsPerPage: 1).Bind(new[]
+ {
+ new { }, new { }, new { }, new { }
+ });
+ var html = grid.Pager(WebGridPagerModes.NextPrevious | WebGridPagerModes.Numeric, numericLinksCount: 2);
+ Assert.Equal(
+ "<a href=\"?page=1\">&lt;</a> " +
+ "2 " +
+ "<a href=\"?page=3\">3</a> " +
+ "<a href=\"?page=3\">&gt;</a> ",
+ html.ToString());
+ XhtmlAssert.Validate1_1(html, wrapper: "div");
+ }
+
+ [Fact]
+ public void PagerRenderingOnLastHidingFirstLast()
+ {
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["page"] = "4";
+ var grid = new WebGrid(GetContext(queryString), rowsPerPage: 1).Bind(new[]
+ {
+ new { }, new { }, new { }, new { }
+ });
+ var html = grid.Pager(WebGridPagerModes.NextPrevious | WebGridPagerModes.Numeric, numericLinksCount: 1);
+ Assert.Equal(
+ "<a href=\"?page=3\">&lt;</a> " +
+ "4 ",
+ html.ToString());
+ XhtmlAssert.Validate1_1(html, wrapper: "div");
+ }
+
+ [Fact]
+ public void PagerRenderingOnMiddleHidingNextPrevious()
+ {
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["page"] = "3";
+ var grid = new WebGrid(GetContext(queryString), rowsPerPage: 1).Bind(new[]
+ {
+ new { }, new { }, new { }, new { }
+ });
+ var html = grid.Pager(WebGridPagerModes.FirstLast | WebGridPagerModes.Numeric, numericLinksCount: 0);
+ Assert.Equal(
+ "<a href=\"?page=1\">&lt;&lt;</a> ",
+ html.ToString());
+ XhtmlAssert.Validate1_1(html, wrapper: "div");
+ }
+
+ [Fact]
+ public void PagerRenderingOnMiddleWithLinksCountGreaterThanPageCount()
+ {
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["page"] = "3";
+ var grid = new WebGrid(GetContext(queryString), rowsPerPage: 1).Bind(new[]
+ {
+ new { }, new { }, new { }, new { }
+ });
+ var html = grid.Pager(WebGridPagerModes.Numeric, numericLinksCount: 6);
+ Assert.Equal(
+ "<a href=\"?page=1\">1</a> " +
+ "<a href=\"?page=2\">2</a> " +
+ "3 " +
+ "<a href=\"?page=4\">4</a> ",
+ html.ToString());
+ XhtmlAssert.Validate1_1(html, wrapper: "div");
+ }
+
+ [Fact]
+ public void PagerRenderingHidingAll()
+ {
+ var grid = new WebGrid(GetContext(), rowsPerPage: 2).Bind(new[]
+ {
+ new { }, new { }, new { }, new { }
+ });
+ var html = grid.Pager(WebGridPagerModes.Numeric, numericLinksCount: 0);
+ Assert.Equal("", html.ToString());
+ }
+
+ [Fact]
+ public void PagerRenderingTextOverrides()
+ {
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["page"] = "3";
+ var grid = new WebGrid(GetContext(queryString), rowsPerPage: 1).Bind(new[]
+ {
+ new { }, new { }, new { }, new { }, new { }
+ });
+ var html = grid.Pager(WebGridPagerModes.FirstLast | WebGridPagerModes.NextPrevious,
+ firstText: "first", previousText: "previous", nextText: "next", lastText: "last");
+ Assert.Equal(
+ "<a href=\"?page=1\">first</a> " +
+ "<a href=\"?page=2\">previous</a> " +
+ "<a href=\"?page=4\">next</a> " +
+ "<a href=\"?page=5\">last</a>",
+ html.ToString());
+ XhtmlAssert.Validate1_1(html, wrapper: "div");
+ }
+
+ [Fact]
+ public void PagerThrowsIfTextSetAndModeNotEnabled()
+ {
+ var grid = new WebGrid(GetContext(), rowsPerPage: 1).Bind(new[] { new { }, new { } });
+ Assert.ThrowsArgument(() => { grid.Pager(firstText: "first"); }, "firstText", "To use this argument, pager mode \"FirstLast\" must be enabled.");
+ Assert.ThrowsArgument(() => { grid.Pager(mode: WebGridPagerModes.Numeric, previousText: "previous"); }, "previousText", "To use this argument, pager mode \"NextPrevious\" must be enabled.");
+ Assert.ThrowsArgument(() => { grid.Pager(mode: WebGridPagerModes.Numeric, nextText: "next"); }, "nextText", "To use this argument, pager mode \"NextPrevious\" must be enabled.");
+ Assert.ThrowsArgument(() => { grid.Pager(lastText: "last"); }, "lastText", "To use this argument, pager mode \"FirstLast\" must be enabled.");
+ }
+
+ [Fact]
+ public void PagerThrowsIfNumericLinkCountIsLessThanZero()
+ {
+ var grid = new WebGrid(GetContext(), rowsPerPage: 1).Bind(new[] { new { }, new { } });
+ Assert.ThrowsArgumentOutOfRange(() => { grid.Pager(numericLinksCount: -1); }, "numericLinksCount", "Value must be greater than or equal to 0.");
+ }
+
+ [Fact]
+ public void PagerThrowsIfPagingIsDisabled()
+ {
+ var grid = new WebGrid(GetContext(), rowsPerPage: 1, canPage: false).Bind(new[] { new { }, new { } });
+ Assert.Throws<NotSupportedException>(() => { grid.Pager(); }, "This operation is not supported when paging is disabled for the \"WebGrid\" object.");
+ }
+
+ [Fact]
+ public void PagerWithAjax()
+ {
+ var grid = new WebGrid(GetContext(), rowsPerPage: 1, ajaxUpdateContainerId: "grid")
+ .Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" },
+ new { P1 = 4, P2 = '5', P3 = "6" }
+ });
+ string html = grid.Pager().ToString();
+ Assert.True(html.Contains("<script"));
+ }
+
+ [Fact]
+ public void PagerWithAjaxAndCallback()
+ {
+ var grid = new WebGrid(GetContext(), rowsPerPage: 1, ajaxUpdateContainerId: "grid", ajaxUpdateCallback: "myCallback")
+ .Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" },
+ new { P1 = 4, P2 = '5', P3 = "6" }
+ });
+ string html = grid.Pager().ToString();
+ Assert.True(html.Contains("<script"));
+ Assert.True(html.Contains("data-swhgcallback=\"myCallback\""));
+ }
+
+ [Fact]
+ public void PropertySettersDoNotThrowBeforePagingAndSorting()
+ {
+ // test with selection because SelectedIndex getter used to do range checking that caused paging and sorting
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["row"] = "1";
+ var grid = new WebGrid(GetContext(queryString), rowsPerPage: 2).Bind(new[]
+ {
+ new { P1 = 1 }, new { P1 = 2 }, new { P1 = 3 }
+ });
+
+ // invoke other WebGrid properties to ensure they don't cause sorting and paging
+ foreach (var prop in typeof(WebGrid).GetProperties())
+ {
+ // exceptions: these do cause sorting and paging
+ if (prop.Name.Equals("Rows") || prop.Name.Equals("SelectedRow") || prop.Name.Equals("ElementType"))
+ {
+ continue;
+ }
+ prop.GetValue(grid, null);
+ }
+
+ grid.PageIndex = 1;
+ grid.SelectedIndex = 0;
+ grid.SortColumn = "P1";
+ grid.SortDirection = SortDirection.Descending;
+ }
+
+ [Fact]
+ public void PropertySettersDoNotThrowAfterPagingAndSortingIfValuesHaveNotChanged()
+ {
+ var grid = new WebGrid(GetContext(), rowsPerPage: 2).Bind(new[]
+ {
+ new { P1 = 1 }, new { P1 = 2 }, new { P1 = 3 }
+ });
+ // calling Rows will sort and page the data
+ Assert.Equal(2, grid.Rows.Count());
+
+ grid.PageIndex = 0;
+ grid.SelectedIndex = -1;
+ grid.SortColumn = String.Empty;
+ grid.SortDirection = SortDirection.Ascending;
+ }
+
+ [Fact]
+ public void PropertySettersThrowAfterPagingAndSorting()
+ {
+ var grid = new WebGrid(GetContext(), rowsPerPage: 2).Bind(new[]
+ {
+ new { P1 = 1 }, new { P1 = 2 }, new { P1 = 3 }
+ });
+ // calling Rows will sort and page the data
+ Assert.Equal(2, grid.Rows.Count());
+
+ var message = "This property cannot be set after the \"WebGrid\" object has been sorted or paged. Make sure that this property is set prior to invoking the \"Rows\" property directly or indirectly through other methods such as \"GetHtml\", \"Pager\", \"Table\", etc.";
+ Assert.Throws<InvalidOperationException>(() => { grid.PageIndex = 1; }, message);
+ Assert.Throws<InvalidOperationException>(() => { grid.SelectedIndex = 0; }, message);
+ Assert.Throws<InvalidOperationException>(() => { grid.SortColumn = "P1"; }, message);
+ Assert.Throws<InvalidOperationException>(() => { grid.SortDirection = SortDirection.Descending; }, message);
+ }
+
+ [Fact]
+ public void RowColumnsAreDynamicMembersForDynamics()
+ {
+ var grid = new WebGrid(GetContext()).Bind(Dynamics(
+ new { P1 = 1, P2 = '2', P3 = "3" }
+ ));
+ dynamic row = grid.Rows.First();
+ Assert.Equal(1, row.P1);
+ Assert.Equal('2', row.P2);
+ Assert.Equal("3", row.P3);
+ }
+
+ [Fact]
+ public void RowColumnsAreDynamicMembersForNonDynamics()
+ {
+ var grid = new WebGrid(GetContext()).Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" }
+ });
+ dynamic row = grid.Rows.First();
+ Assert.Equal(1, row.P1);
+ Assert.Equal('2', row.P2);
+ Assert.Equal("3", row.P3);
+ }
+
+ [Fact]
+ public void RowExposesRowIndex()
+ {
+ var grid = new WebGrid(GetContext()).Bind(new[]
+ {
+ new { }, new { }, new { }
+ });
+ dynamic row = grid.Rows.First();
+ Assert.Equal(0, row["ROW"]);
+ row = grid.Rows.Skip(1).First();
+ Assert.Equal(1, row.ROW);
+ row = grid.Rows.Skip(2).First();
+ Assert.Equal(2, row.ROW);
+ }
+
+ [Fact]
+ public void RowExposesUnderlyingValue()
+ {
+ var sb = new StringBuilder("Foo");
+ sb.Append("Bar");
+ var grid = new WebGrid(GetContext()).Bind(new[] { sb });
+ var row = grid.Rows.First();
+ Assert.Equal(sb, row.Value);
+ Assert.Equal("FooBar", row.ToString());
+ Assert.Equal(grid, row.WebGrid);
+ }
+
+ [Fact]
+ public void RowIndexerThrowsWhenColumnNameIsEmpty()
+ {
+ var grid = new WebGrid(GetContext()).Bind(new[] { new { } });
+ var row = grid.Rows.First();
+ Assert.ThrowsArgumentNullOrEmptyString(() => { var value = row[String.Empty]; }, "name");
+ }
+
+ [Fact]
+ public void RowIndexerThrowsWhenColumnNameIsNull()
+ {
+ var grid = new WebGrid(GetContext()).Bind(new[] { new { } });
+ var row = grid.Rows.First();
+ Assert.ThrowsArgumentNullOrEmptyString(() => { var value = row[null]; }, "name");
+ }
+
+ [Fact] // todo - should throw ArgumentException?
+ public void RowIndexerThrowsWhenColumnNotFound()
+ {
+ var grid = new WebGrid(GetContext()).Bind(new[] { new { } });
+ var row = grid.Rows.First();
+ Assert.Throws<InvalidOperationException>(() => { var value = row["NotAColumn"]; });
+ }
+
+ [Fact]
+ public void RowIndexerThrowsWhenGreaterThanColumnCount()
+ {
+ var grid = new WebGrid(GetContext()).Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" }
+ });
+ var row = grid.Rows.First();
+ Assert.Throws<ArgumentOutOfRangeException>(() => { var value = row[4]; });
+ }
+
+ [Fact]
+ public void RowIndexerThrowsWhenLessThanZero()
+ {
+ var grid = new WebGrid(GetContext()).Bind(new[] { new { } });
+ var row = grid.Rows.First();
+ Assert.Throws<ArgumentOutOfRangeException>(() => { var value = row[-1]; });
+ }
+
+ [Fact]
+ public void RowIsEnumerableForDynamics()
+ {
+ var grid = new WebGrid(GetContext()).Bind(Dynamics(
+ new { P1 = 1, P2 = '2', P3 = "3" }
+ ));
+ int i = 0;
+ foreach (var col in (IEnumerable)grid.Rows.First())
+ {
+ i++;
+ }
+ Assert.Equal(3, i);
+ }
+
+ [Fact]
+ public void RowIsEnumerableForNonDynamics()
+ {
+ var grid = new WebGrid(GetContext()).Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" }
+ });
+ int i = 0;
+ foreach (var col in grid.Rows.First())
+ {
+ i++;
+ }
+ Assert.Equal(3, i);
+ }
+
+ [Fact]
+ public void RowIsIndexableByColumnForDynamics()
+ {
+ var grid = new WebGrid(GetContext()).Bind(Dynamics(
+ new { P1 = 1, P2 = '2', P3 = "3" }
+ ));
+ var row = grid.Rows.First();
+ Assert.Equal(1, row["P1"]);
+ Assert.Equal('2', row["P2"]);
+ Assert.Equal("3", row["P3"]);
+ }
+
+ [Fact]
+ public void RowIsIndexableByColumnForNonDynamics()
+ {
+ var grid = new WebGrid(GetContext()).Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" }
+ });
+ var row = grid.Rows.First();
+ Assert.Equal(1, row["P1"]);
+ Assert.Equal('2', row["P2"]);
+ Assert.Equal("3", row["P3"]);
+ }
+
+ [Fact]
+ public void RowIsIndexableByIndexForDynamics()
+ {
+ var grid = new WebGrid(GetContext()).Bind(Dynamics(
+ new { P1 = 1, P2 = '2', P3 = "3" }
+ ));
+ var row = grid.Rows.First();
+ Assert.Equal(1, row[0]);
+ Assert.Equal('2', row[1]);
+ Assert.Equal("3", row[2]);
+ }
+
+ [Fact]
+ public void RowIsIndexableByIndexForNonDynamics()
+ {
+ var grid = new WebGrid(GetContext()).Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" }
+ });
+ var row = grid.Rows.First();
+ Assert.Equal(1, row[0]);
+ Assert.Equal('2', row[1]);
+ Assert.Equal("3", row[2]);
+ }
+
+ [Fact]
+ public void RowsNotPagedWhenPagingIsDisabled()
+ {
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["page"] = "2";
+ var grid = new WebGrid(GetContext(queryString), rowsPerPage: 1, canPage: false)
+ .Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" },
+ new { P1 = 4, P2 = '5', P3 = "6" }
+ });
+ // review: should we reset PageIndex or Sort when operation disabled?
+ Assert.Equal(0, grid.PageIndex);
+ Assert.Equal(2, grid.Rows.Count);
+ Assert.Equal(1, grid.Rows.First()["P1"]);
+ Assert.Equal(4, grid.Rows.Skip(1).First()["P1"]);
+ }
+
+ [Fact] // todo - should throw ArgumentException?
+ public void RowTryGetMemberReturnsFalseWhenColumnNotFound()
+ {
+ var grid = new WebGrid(GetContext()).Bind(new[] { new { } });
+ var row = grid.Rows.First();
+ object value = null;
+ Assert.False(row.TryGetMember("NotAColumn", out value));
+ }
+
+ [Fact]
+ public void SelectedIndexCanBeReset()
+ {
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["row"] = "2";
+ var grid = new WebGrid(GetContext(queryString)).Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" },
+ new { P1 = 4, P2 = '5', P3 = "6" }
+ });
+ Assert.Equal(1, grid.SelectedIndex);
+ grid.SelectedIndex = 0;
+ Assert.Equal(0, grid.SelectedIndex);
+ }
+
+ [Fact]
+ public void SelectedIndexCanBeResetToSameValue()
+ {
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["row"] = "2";
+ var grid = new WebGrid(GetContext(queryString)).Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" },
+ new { P1 = 4, P2 = '5', P3 = "6" }
+ });
+ grid.SelectedIndex = -1;
+ Assert.Equal(-1, grid.SelectedIndex);
+ }
+
+ [Fact]
+ public void SelectedIndexDefaultsToNegative()
+ {
+ var grid = new WebGrid(GetContext()).Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" },
+ new { P1 = 4, P2 = '5', P3 = "6" }
+ });
+ Assert.False(grid.HasSelection);
+ Assert.Equal(-1, grid.SelectedIndex);
+ Assert.Equal(null, grid.SelectedRow);
+ }
+
+ [Fact]
+ public void SelectedIndexResetWhenQueryStringValueGreaterThanRowsPerPage()
+ {
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["row"] = "3";
+ var grid = new WebGrid(GetContext(queryString), rowsPerPage: 2).Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" },
+ new { P1 = 4, P2 = '5', P3 = "6" }
+ });
+ Assert.False(grid.HasSelection);
+ Assert.Equal(-1, grid.SelectedIndex);
+ Assert.Equal(null, grid.SelectedRow);
+ }
+
+ [Fact]
+ public void SelectedIndexPersistsWhenPagingTurnedOff()
+ {
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["row"] = "3";
+ var grid = new WebGrid(GetContext(queryString), rowsPerPage: 2, canPage: false).Bind(new[]
+ {
+ new { }, new { }, new { }, new { }
+ });
+ grid.SelectedIndex = 3;
+ Assert.Equal(3, grid.SelectedIndex);
+ }
+
+ [Fact]
+ public void SelectedIndexResetWhenQueryStringValueIsInvalid()
+ {
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["row"] = "NotAnInt";
+ var grid = new WebGrid(GetContext(queryString)).Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" },
+ new { P1 = 4, P2 = '5', P3 = "6" }
+ });
+ Assert.False(grid.HasSelection);
+ Assert.Equal(-1, grid.SelectedIndex);
+ Assert.Equal(null, grid.SelectedRow);
+ }
+
+ [Fact]
+ public void SelectedIndexResetWhenQueryStringValueLessThanOne()
+ {
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["row"] = "0";
+ var grid = new WebGrid(GetContext(queryString)).Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" },
+ new { P1 = 4, P2 = '5', P3 = "6" }
+ });
+ Assert.False(grid.HasSelection);
+ Assert.Equal(-1, grid.SelectedIndex);
+ Assert.Equal(null, grid.SelectedRow);
+ }
+
+ [Fact]
+ public void SelectedIndexUsesCustomQueryString()
+ {
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["g_sel"] = "2";
+ var grid = new WebGrid(GetContext(queryString), fieldNamePrefix: "g_", selectionFieldName: "sel").Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" },
+ new { P1 = 4, P2 = '5', P3 = "6" }
+ });
+ Assert.True(grid.HasSelection);
+ Assert.Equal(1, grid.SelectedIndex);
+ Assert.NotNull(grid.SelectedRow);
+ Assert.Equal(4, grid.SelectedRow["P1"]);
+ }
+
+ [Fact]
+ public void SelectedIndexUsesQueryString()
+ {
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["row"] = "2";
+ var grid = new WebGrid(GetContext(queryString)).Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" },
+ new { P1 = 4, P2 = '5', P3 = "6" }
+ });
+ Assert.True(grid.HasSelection);
+ Assert.Equal(1, grid.SelectedIndex);
+ Assert.NotNull(grid.SelectedRow);
+ Assert.Equal(4, grid.SelectedRow["P1"]);
+ }
+
+ [Fact]
+ public void SelectLink()
+ {
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["page"] = "1";
+ queryString["row"] = "1";
+ queryString["sort"] = "P1";
+ queryString["sortdir"] = "DESC";
+ var grid = new WebGrid(GetContext(queryString)).Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" },
+ new { P1 = 4, P2 = '5', P3 = "6" }
+ });
+ string html = grid.Rows[1].GetSelectLink().ToString();
+ Assert.Equal("<a href=\"?page=1&amp;row=2&amp;sort=P1&amp;sortdir=DESC\">Select</a>", html.ToString());
+ }
+
+ [Fact]
+ public void SortCanBeReset()
+ {
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["sort"] = "P1";
+ var grid = new WebGrid(GetContext(queryString)).Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" },
+ new { P1 = 4, P2 = '5', P3 = "6" }
+ });
+ Assert.Equal("P1", grid.SortColumn);
+ grid.SortColumn = "P2";
+ Assert.Equal("P2", grid.SortColumn);
+ // verify that selection and page links have updated sort
+ Assert.Equal("?sort=P2&row=1", grid.Rows.FirstOrDefault().GetSelectUrl());
+ Assert.Equal("?sort=P2&page=1", grid.GetPageUrl(0));
+ }
+
+ [Fact]
+ public void SortCanBeResetToNull()
+ {
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["sort"] = "P1";
+ var grid = new WebGrid(GetContext(queryString)).Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" },
+ new { P1 = 4, P2 = '5', P3 = "6" }
+ });
+ Assert.Equal("P1", grid.SortColumn);
+ grid.SortColumn = null;
+ Assert.Equal(String.Empty, grid.SortColumn);
+ // verify that selection and page links have updated sort
+ Assert.Equal("?row=1", grid.Rows.FirstOrDefault().GetSelectUrl());
+ Assert.Equal("?page=1", grid.GetPageUrl(0));
+ }
+
+ [Fact]
+ public void SortCanBeResetToSameValue()
+ {
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["sort"] = "P1";
+ var grid = new WebGrid(GetContext(queryString)).Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" },
+ new { P1 = 4, P2 = '5', P3 = "6" }
+ });
+ grid.SortColumn = String.Empty;
+ Assert.Equal(String.Empty, grid.SortColumn);
+ }
+
+ [Fact]
+ public void SortColumnDefaultsToEmpty()
+ {
+ var grid = new WebGrid(GetContext()).Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" }
+ });
+ Assert.Equal(String.Empty, grid.SortColumn);
+ }
+
+ [Fact]
+ public void SortColumnResetWhenQueryStringValueIsInvalid()
+ {
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["sort"] = "P4";
+ var grid = new WebGrid(GetContext(queryString)).Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" }
+ });
+ Assert.Equal("", grid.SortColumn);
+ }
+
+ [Fact]
+ public void SortColumnUsesCustomQueryString()
+ {
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["g_st"] = "P2";
+ var grid = new WebGrid(GetContext(queryString), fieldNamePrefix: "g_", sortFieldName: "st").Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" }
+ });
+ Assert.Equal("P2", grid.SortColumn);
+ }
+
+ [Fact]
+ public void SortColumnUsesQueryString()
+ {
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["sort"] = "P2";
+ var grid = new WebGrid(GetContext(queryString)).Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" }
+ });
+ Assert.Equal("P2", grid.SortColumn);
+ }
+
+ [Fact]
+ public void SortDirectionCanBeReset()
+ {
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["sortdir"] = "DESC";
+ var grid = new WebGrid(GetContext(queryString)).Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" },
+ new { P1 = 4, P2 = '5', P3 = "6" }
+ });
+ Assert.Equal(SortDirection.Descending, grid.SortDirection);
+ grid.SortDirection = SortDirection.Ascending;
+ Assert.Equal(SortDirection.Ascending, grid.SortDirection);
+ // verify that selection and page links have updated sort
+ Assert.Equal("?sortdir=ASC&row=1", grid.Rows.FirstOrDefault().GetSelectUrl());
+ Assert.Equal("?sortdir=ASC&page=1", grid.GetPageUrl(0));
+ }
+
+ [Fact]
+ public void SortDirectionDefaultsToAscending()
+ {
+ var grid = new WebGrid(GetContext()).Bind(new object[0]);
+ Assert.Equal(SortDirection.Ascending, grid.SortDirection);
+ }
+
+ [Fact]
+ public void SortDirectionResetWhenQueryStringValueIsInvalid()
+ {
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["sortdir"] = "NotASortDir";
+ var grid = new WebGrid(GetContext(queryString)).Bind(new object[0]);
+ Assert.Equal(SortDirection.Ascending, grid.SortDirection);
+ }
+
+ [Fact]
+ public void SortDirectionUsesQueryStringOfAsc()
+ {
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["sortdir"] = "aSc";
+ var grid = new WebGrid(GetContext(queryString)).Bind(new object[0]);
+ Assert.Equal(SortDirection.Ascending, grid.SortDirection);
+ }
+
+ [Fact]
+ public void SortDirectionUsesQueryStringOfAscending()
+ {
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["sortdir"] = "AScendING";
+ var grid = new WebGrid(GetContext(queryString)).Bind(new object[0]);
+ Assert.Equal(SortDirection.Ascending, grid.SortDirection);
+ }
+
+ [Fact]
+ public void SortDirectionUsesQueryStringOfDesc()
+ {
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["sortdir"] = "DeSc";
+ var grid = new WebGrid(GetContext(queryString)).Bind(new object[0]);
+ Assert.Equal(SortDirection.Descending, grid.SortDirection);
+ }
+
+ [Fact]
+ public void SortDirectionUsesQueryStringOfDescending()
+ {
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["g_sd"] = "DeScendING";
+ var grid = new WebGrid(GetContext(queryString), fieldNamePrefix: "g_", sortDirectionFieldName: "sd").Bind(new object[0]);
+ Assert.Equal(SortDirection.Descending, grid.SortDirection);
+ }
+
+ [Fact]
+ public void SortDisabledIfSortIsEmpty()
+ {
+ var grid = new WebGrid(GetContext(), defaultSort: String.Empty).Bind(Dynamics(
+ new { FirstName = "Joe", LastName = "Smith" },
+ new { FirstName = "Bob", LastName = "Johnson" },
+ new { FirstName = "Sam", LastName = "Jones" },
+ new { FirstName = "Tom", LastName = "Anderson" }
+ ));
+ Assert.Equal("Joe", grid.Rows[0]["FirstName"]);
+ Assert.Equal("Bob", grid.Rows[1]["FirstName"]);
+ Assert.Equal("Sam", grid.Rows[2]["FirstName"]);
+ Assert.Equal("Tom", grid.Rows[3]["FirstName"]);
+ }
+
+ [Fact]
+ public void SortDisabledIfSortIsNull()
+ {
+ var grid = new WebGrid(GetContext(), defaultSort: null).Bind(Dynamics(
+ new { FirstName = "Joe", LastName = "Smith" },
+ new { FirstName = "Bob", LastName = "Johnson" },
+ new { FirstName = "Sam", LastName = "Jones" },
+ new { FirstName = "Tom", LastName = "Anderson" }
+ ));
+ Assert.Equal("Joe", grid.Rows[0]["FirstName"]);
+ Assert.Equal("Bob", grid.Rows[1]["FirstName"]);
+ Assert.Equal("Sam", grid.Rows[2]["FirstName"]);
+ Assert.Equal("Tom", grid.Rows[3]["FirstName"]);
+ }
+
+ [Fact]
+ public void SortForDynamics()
+ {
+ var grid = new WebGrid(GetContext(), defaultSort: "FirstName").Bind(Dynamics(
+ new { FirstName = "Joe", LastName = "Smith" },
+ new { FirstName = "Bob", LastName = "Johnson" },
+ new { FirstName = "Sam", LastName = "Jones" },
+ new { FirstName = "Tom", LastName = "Anderson" }
+ ));
+ Assert.Equal("Bob", grid.Rows[0]["FirstName"]);
+ Assert.Equal("Joe", grid.Rows[1]["FirstName"]);
+ Assert.Equal("Sam", grid.Rows[2]["FirstName"]);
+ Assert.Equal("Tom", grid.Rows[3]["FirstName"]);
+ }
+
+ [Fact]
+ public void SortForDynamicsDescending()
+ {
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["sort"] = "LastName";
+ queryString["sortdir"] = "DESCENDING";
+ var grid = new WebGrid(GetContext(queryString), defaultSort: "FirstName").Bind(Dynamics(
+ new { FirstName = "Joe", LastName = "Smith" },
+ new { FirstName = "Bob", LastName = "Johnson" },
+ new { FirstName = "Sam", LastName = "Jones" },
+ new { FirstName = "Tom", LastName = "Anderson" }
+ ));
+ Assert.Equal("Smith", grid.Rows[0]["LastName"]);
+ Assert.Equal("Jones", grid.Rows[1]["LastName"]);
+ Assert.Equal("Johnson", grid.Rows[2]["LastName"]);
+ Assert.Equal("Anderson", grid.Rows[3]["LastName"]);
+ }
+
+ [Fact]
+ public void SortForNonDynamicNavigationColumn()
+ {
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["sort"] = "Not.A.Column";
+ var grid = new WebGrid(GetContext(queryString), defaultSort: "Person.FirstName").Bind(new[]
+ {
+ new { Person = new { FirstName = "Joe", LastName = "Smith" } },
+ new { Person = new { FirstName = "Bob", LastName = "Johnson" } },
+ new { Person = new { FirstName = "Sam", LastName = "Jones" } },
+ new { Person = new { FirstName = "Tom", LastName = "Anderson" } }
+ });
+ Assert.Equal("Not.A.Column", grid.SortColumn); // navigation columns are validated during sort
+ Assert.Equal("Bob", grid.Rows[0]["Person.FirstName"]);
+ Assert.Equal("Joe", grid.Rows[1]["Person.FirstName"]);
+ Assert.Equal("Sam", grid.Rows[2]["Person.FirstName"]);
+ Assert.Equal("Tom", grid.Rows[3]["Person.FirstName"]);
+ }
+
+ [Fact]
+ public void SortForNonDynamics()
+ {
+ var grid = new WebGrid(GetContext(), defaultSort: "FirstName").Bind(new[]
+ {
+ new { FirstName = "Joe", LastName = "Smith" },
+ new { FirstName = "Bob", LastName = "Johnson" },
+ new { FirstName = "Sam", LastName = "Jones" },
+ new { FirstName = "Tom", LastName = "Anderson" }
+ });
+ Assert.Equal("Bob", grid.Rows[0]["FirstName"]);
+ Assert.Equal("Joe", grid.Rows[1]["FirstName"]);
+ Assert.Equal("Sam", grid.Rows[2]["FirstName"]);
+ Assert.Equal("Tom", grid.Rows[3]["FirstName"]);
+ }
+
+ [Fact]
+ public void SortForNonDynamicsDescending()
+ {
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["sort"] = "LastName";
+ queryString["sortdir"] = "DESCENDING";
+ var grid = new WebGrid(GetContext(queryString), defaultSort: "FirstName").Bind(new[]
+ {
+ new { FirstName = "Joe", LastName = "Smith" },
+ new { FirstName = "Bob", LastName = "Johnson" },
+ new { FirstName = "Sam", LastName = "Jones" },
+ new { FirstName = "Tom", LastName = "Anderson" }
+ });
+ Assert.Equal("Smith", grid.Rows[0]["LastName"]);
+ Assert.Equal("Jones", grid.Rows[1]["LastName"]);
+ Assert.Equal("Johnson", grid.Rows[2]["LastName"]);
+ Assert.Equal("Anderson", grid.Rows[3]["LastName"]);
+ }
+
+ [Fact]
+ public void SortForNonDynamicsEnumerable()
+ {
+ var grid = new WebGrid(GetContext(), defaultSort: "FirstName").Bind(new[]
+ {
+ new { FirstName = "Joe", LastName = "Smith" },
+ new { FirstName = "Bob", LastName = "Johnson" },
+ new { FirstName = "Sam", LastName = "Jones" },
+ new { FirstName = "Tom", LastName = "Anderson" }
+ }.ToList());
+ Assert.Equal("Bob", grid.Rows[0]["FirstName"]);
+ Assert.Equal("Joe", grid.Rows[1]["FirstName"]);
+ Assert.Equal("Sam", grid.Rows[2]["FirstName"]);
+ Assert.Equal("Tom", grid.Rows[3]["FirstName"]);
+ }
+
+ [Fact]
+ public void SortForNonDynamicsEnumerableDescending()
+ {
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["sort"] = "LastName";
+ queryString["sortdir"] = "DESCENDING";
+ var grid = new WebGrid(GetContext(queryString), defaultSort: "FirstName").Bind(new[]
+ {
+ new { FirstName = "Joe", LastName = "Smith" },
+ new { FirstName = "Bob", LastName = "Johnson" },
+ new { FirstName = "Sam", LastName = "Jones" },
+ new { FirstName = "Tom", LastName = "Anderson" }
+ }.ToList());
+ Assert.Equal("Smith", grid.Rows[0]["LastName"]);
+ Assert.Equal("Jones", grid.Rows[1]["LastName"]);
+ Assert.Equal("Johnson", grid.Rows[2]["LastName"]);
+ Assert.Equal("Anderson", grid.Rows[3]["LastName"]);
+ }
+
+ [Fact]
+ public void SortForNonGenericEnumerable()
+ {
+ var grid = new WebGrid(GetContext(), defaultSort: "FirstName").Bind(new NonGenericEnumerable(new[]
+ {
+ new Person { FirstName = "Joe", LastName = "Smith" },
+ new Person { FirstName = "Bob", LastName = "Johnson" },
+ new Person { FirstName = "Sam", LastName = "Jones" },
+ new Person { FirstName = "Tom", LastName = "Anderson" }
+ }));
+ Assert.Equal("Bob", grid.Rows[0]["FirstName"]);
+ Assert.Equal("Joe", grid.Rows[1]["FirstName"]);
+ Assert.Equal("Sam", grid.Rows[2]["FirstName"]);
+ Assert.Equal("Tom", grid.Rows[3]["FirstName"]);
+ }
+
+ [Fact]
+ public void SortForNonGenericEnumerableDescending()
+ {
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["sort"] = "LastName";
+ queryString["sortdir"] = "DESCENDING";
+ var grid = new WebGrid(GetContext(queryString), defaultSort: "FirstName").Bind(new NonGenericEnumerable(new[]
+ {
+ new Person { FirstName = "Joe", LastName = "Smith" },
+ new Person { FirstName = "Bob", LastName = "Johnson" },
+ new Person { FirstName = "Sam", LastName = "Jones" },
+ new Person { FirstName = "Tom", LastName = "Anderson" }
+ }));
+ Assert.Equal("Smith", grid.Rows[0]["LastName"]);
+ Assert.Equal("Jones", grid.Rows[1]["LastName"]);
+ Assert.Equal("Johnson", grid.Rows[2]["LastName"]);
+ Assert.Equal("Anderson", grid.Rows[3]["LastName"]);
+ }
+
+ [Fact]
+ public void SortUrlDefaults()
+ {
+ var grid = new WebGrid(GetContext()).Bind(new[]
+ {
+ new { FirstName = "Bob" }
+ });
+ string html = grid.GetSortUrl("FirstName");
+ Assert.Equal("?sort=FirstName&sortdir=ASC", html.ToString());
+ }
+
+ [Fact]
+ public void SortUrlThrowsIfColumnNameIsEmpty()
+ {
+ var grid = new WebGrid(GetContext()).Bind(new[]
+ {
+ new { }, new { }
+ });
+ Assert.ThrowsArgumentNullOrEmptyString(() => { grid.GetSortUrl(String.Empty); }, "column");
+ }
+
+ [Fact]
+ public void SortUrlThrowsIfColumnNameIsNull()
+ {
+ var grid = new WebGrid(GetContext()).Bind(new[]
+ {
+ new { }, new { }
+ });
+ Assert.ThrowsArgumentNullOrEmptyString(() => { grid.GetSortUrl(null); }, "column");
+ }
+
+ [Fact]
+ public void SortUrlThrowsIfSortingIsDisabled()
+ {
+ var grid = new WebGrid(GetContext(), canSort: false).Bind(new[]
+ {
+ new { P1 = 1 }, new { P1 = 2 }
+ });
+ Assert.Throws<NotSupportedException>(() => { grid.GetSortUrl("P1"); }, "This operation is not supported when sorting is disabled for the \"WebGrid\" object.");
+ }
+
+ [Fact]
+ public void SortWhenSortIsDisabled()
+ {
+ var grid = new WebGrid(GetContext(), defaultSort: "FirstName", canSort: false).Bind(new[]
+ {
+ new { FirstName = "Joe", LastName = "Smith" },
+ new { FirstName = "Bob", LastName = "Johnson" },
+ new { FirstName = "Sam", LastName = "Jones" },
+ new { FirstName = "Tom", LastName = "Anderson" }
+ });
+ Assert.Equal("Joe", grid.Rows[0]["FirstName"]);
+ Assert.Equal("Bob", grid.Rows[1]["FirstName"]);
+ Assert.Equal("Sam", grid.Rows[2]["FirstName"]);
+ Assert.Equal("Tom", grid.Rows[3]["FirstName"]);
+ }
+
+ [Fact]
+ public void SortWithNullValues()
+ {
+ var data = new[]
+ {
+ new { FirstName = (object)"Joe", LastName = "Smith" },
+ new { FirstName = (object)"Bob", LastName = "Johnson" },
+ new { FirstName = (object)null, LastName = "Jones" }
+ };
+ var grid = new WebGrid(GetContext(), defaultSort: "FirstName").Bind(data);
+
+ Assert.Equal("Jones", grid.Rows[0]["LastName"]);
+ Assert.Equal("Bob", grid.Rows[1]["FirstName"]);
+ Assert.Equal("Joe", grid.Rows[2]["FirstName"]);
+
+ grid = new WebGrid(GetContext(), defaultSort: "FirstName desc").Bind(data);
+
+ Assert.Equal("Joe", grid.Rows[0]["FirstName"]);
+ Assert.Equal("Bob", grid.Rows[1]["FirstName"]);
+ Assert.Equal("Jones", grid.Rows[2]["LastName"]);
+ }
+
+ [Fact]
+ public void SortWithMultipleNullValues()
+ {
+ var data = new[]
+ {
+ new { FirstName = (object)"Joe", LastName = "Smith" },
+ new { FirstName = (object)"Bob", LastName = "Johnson" },
+ new { FirstName = (object)null, LastName = "Hughes" },
+ new { FirstName = (object)null, LastName = "Jones" }
+ };
+ var grid = new WebGrid(GetContext(), defaultSort: "FirstName").Bind(data);
+
+ Assert.Equal("Hughes", grid.Rows[0]["LastName"]);
+ Assert.Equal("Jones", grid.Rows[1]["LastName"]);
+ Assert.Equal("Bob", grid.Rows[2]["FirstName"]);
+ Assert.Equal("Joe", grid.Rows[3]["FirstName"]);
+
+ grid = new WebGrid(GetContext(), defaultSort: "FirstName desc").Bind(data);
+
+ Assert.Equal("Joe", grid.Rows[0]["FirstName"]);
+ Assert.Equal("Bob", grid.Rows[1]["FirstName"]);
+ Assert.Equal("Hughes", grid.Rows[2]["LastName"]);
+ Assert.Equal("Jones", grid.Rows[3]["LastName"]);
+ }
+
+ [Fact]
+ public void SortWithMixedValuesDoesNotThrow()
+ {
+ var data = new[]
+ {
+ new { FirstName = (object)1, LastName = "Smith" },
+ new { FirstName = (object)"Bob", LastName = "Johnson" },
+ new { FirstName = (object)DBNull.Value, LastName = "Jones" }
+ };
+ var grid = new WebGrid(GetContext(), defaultSort: "FirstName").Bind(data);
+
+ Assert.NotNull(grid.Rows);
+
+ Assert.Equal("Smith", grid.Rows[0]["LastName"]);
+ Assert.Equal("Johnson", grid.Rows[1]["LastName"]);
+ Assert.Equal("Jones", grid.Rows[2]["LastName"]);
+ }
+
+ [Fact]
+ public void SortWithUnsortableDoesNotThrow()
+ {
+ var object1 = new object();
+ var object2 = new object();
+ var data = new[]
+ {
+ new { Value = object1 },
+ new { Value = object2 }
+ };
+ var grid = new WebGrid(GetContext(), defaultSort: "Value").Bind(data);
+
+ Assert.NotNull(grid.Rows);
+
+ Assert.Equal(object1, grid.Rows[0]["Value"]);
+ Assert.Equal(object2, grid.Rows[1]["Value"]);
+ }
+
+ [Fact]
+ public void TableRenderingWithColumnTemplates()
+ {
+ var grid = new WebGrid(GetContext(), rowsPerPage: 3).Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" }
+ });
+ var html = grid.Table(displayHeader: false,
+ columns: new[]
+ {
+ grid.Column("P1", format: item => { return "<span>P1: " + item.P1 + "</span>"; }),
+ grid.Column("P2", format: item => { return new HtmlString("<span>P2: " + item.P2 + "</span>"); }),
+ grid.Column("P3", format: item => { return new HelperResult(tw => { tw.Write("<span>P3: " + item.P3 + "</span>"); }); })
+ });
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(
+ "<table><tbody><tr>" +
+ "<td>&lt;span&gt;P1: 1&lt;/span&gt;</td>" +
+ "<td><span>P2: 2</span></td>" +
+ "<td><span>P3: 3</span></td>" +
+ "</tr></tbody></table>", html.ToString());
+ XhtmlAssert.Validate1_1(html);
+ }
+
+ [Fact]
+ public void TableRenderingWithDefaultCellValueOfCustom()
+ {
+ var grid = new WebGrid(GetContext(), rowsPerPage: 3).Bind(new[]
+ {
+ new { P1 = String.Empty, P2 = (string)null },
+ });
+ var html = grid.Table(fillEmptyRows: true, emptyRowCellValue: "N/A", displayHeader: false);
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(
+ "<table><tbody>" +
+ "<tr><td></td><td></td></tr>" +
+ "<tr><td>N/A</td><td>N/A</td></tr>" +
+ "<tr><td>N/A</td><td>N/A</td></tr>" +
+ "</tbody></table>", html.ToString());
+ XhtmlAssert.Validate1_1(html);
+ }
+
+ [Fact]
+ public void TableRenderingWithDefaultCellValueOfEmpty()
+ {
+ var grid = new WebGrid(GetContext(), rowsPerPage: 3).Bind(new[]
+ {
+ new { P1 = String.Empty, P2 = (string)null }
+ });
+ var html = grid.Table(fillEmptyRows: true, emptyRowCellValue: "", displayHeader: false);
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(
+ "<table><tbody>" +
+ "<tr><td></td><td></td></tr>" +
+ "<tr><td></td><td></td></tr>" +
+ "<tr><td></td><td></td></tr>" +
+ "</tbody></table>", html.ToString());
+ XhtmlAssert.Validate1_1(html);
+ }
+
+ [Fact]
+ public void TableRenderingWithDefaultCellValueOfNbsp()
+ {
+ var grid = new WebGrid(GetContext(), rowsPerPage: 3).Bind(new[]
+ {
+ new { P1 = String.Empty, P2 = (string)null }
+ });
+ var html = grid.Table(fillEmptyRows: true, displayHeader: false);
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(
+ "<table><tbody>" +
+ "<tr><td></td><td></td></tr>" +
+ "<tr><td>&nbsp;</td><td>&nbsp;</td></tr>" +
+ "<tr><td>&nbsp;</td><td>&nbsp;</td></tr>" +
+ "</tbody></table>", html.ToString());
+ XhtmlAssert.Validate1_1(html);
+ }
+
+ [Fact]
+ public void TableRenderingWithExclusions()
+ {
+ var grid = new WebGrid(GetContext()).Bind(new[]
+ {
+ new { P1 = 1, P2 = '2', P3 = "3" }
+ });
+ var html = grid.Table(exclusions: new string[] { "P2" });
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(
+ "<table><thead><tr>" +
+ "<th scope=\"col\"><a href=\"?sort=P1&amp;sortdir=ASC\">P1</a></th>" +
+ "<th scope=\"col\"><a href=\"?sort=P3&amp;sortdir=ASC\">P3</a></th>" +
+ "</tr></thead>" +
+ "<tbody>" +
+ "<tr><td>1</td><td>3</td></tr>" +
+ "</tbody></table>", html.ToString());
+ XhtmlAssert.Validate1_1(html);
+ }
+
+ [Fact]
+ public void TableRenderingWithNoStylesAndFillEmptyRows()
+ {
+ var grid = new WebGrid(GetContext(), rowsPerPage: 3).Bind(new[]
+ {
+ new { FirstName = "Joe", LastName = "Smith" }
+ });
+ var html = grid.Table(fillEmptyRows: true);
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(
+ "<table><thead><tr>" +
+ "<th scope=\"col\"><a href=\"?sort=FirstName&amp;sortdir=ASC\">FirstName</a></th>" +
+ "<th scope=\"col\"><a href=\"?sort=LastName&amp;sortdir=ASC\">LastName</a></th>" +
+ "</tr></thead>" +
+ "<tbody>" +
+ "<tr><td>Joe</td><td>Smith</td></tr>" +
+ "<tr><td>&nbsp;</td><td>&nbsp;</td></tr>" +
+ "<tr><td>&nbsp;</td><td>&nbsp;</td></tr>" +
+ "</tbody></table>", html.ToString());
+ XhtmlAssert.Validate1_1(html);
+ }
+
+ [Fact]
+ public void TableRenderingWithSortingDisabled()
+ {
+ var grid = new WebGrid(GetContext(), canSort: false).Bind(new[]
+ {
+ new { FirstName = "Joe", LastName = "Smith" }
+ });
+ var html = grid.Table();
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(
+ "<table><thead><tr>" +
+ "<th scope=\"col\">FirstName</th>" +
+ "<th scope=\"col\">LastName</th>" +
+ "</tr></thead>" +
+ "<tbody>" +
+ "<tr><td>Joe</td><td>Smith</td></tr>" +
+ "</tbody></table>", html.ToString());
+ XhtmlAssert.Validate1_1(html);
+ }
+
+ [Fact]
+ public void TableRenderingWithAttributes()
+ {
+ var grid = new WebGrid(GetContext()).Bind(new[]
+ {
+ new { FirstName = "Joe", LastName = "Smith" }
+ });
+ var html = grid.Table(htmlAttributes: new { id = "my-table-id", summary = "Table summary" });
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(
+ "<table id=\"my-table-id\" summary=\"Table summary\"><thead><tr>" +
+ "<th scope=\"col\"><a href=\"?sort=FirstName&amp;sortdir=ASC\">FirstName</a></th>" +
+ "<th scope=\"col\"><a href=\"?sort=LastName&amp;sortdir=ASC\">LastName</a></th>" +
+ "</tr></thead>" +
+ "<tbody>" +
+ "<tr><td>Joe</td><td>Smith</td></tr>" +
+ "</tbody></table>", html.ToString());
+ XhtmlAssert.Validate1_1(html);
+ }
+
+ [Fact]
+ public void TableRenderingEncodesAttributes()
+ {
+ var grid = new WebGrid(GetContext()).Bind(new[]
+ {
+ new { FirstName = "Joe", LastName = "Smith" }
+ });
+ var html = grid.Table(htmlAttributes: new { summary = "\"<Table summary" });
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(
+ "<table summary=\"&quot;&lt;Table summary\"><thead><tr>" +
+ "<th scope=\"col\"><a href=\"?sort=FirstName&amp;sortdir=ASC\">FirstName</a></th>" +
+ "<th scope=\"col\"><a href=\"?sort=LastName&amp;sortdir=ASC\">LastName</a></th>" +
+ "</tr></thead>" +
+ "<tbody>" +
+ "<tr><td>Joe</td><td>Smith</td></tr>" +
+ "</tbody></table>", html.ToString());
+ XhtmlAssert.Validate1_1(html);
+ }
+
+ [Fact]
+ public void TableRenderingIsNotAffectedWhenAttributesIsNull()
+ {
+ var grid = new WebGrid(GetContext()).Bind(new[]
+ {
+ new { FirstName = "Joe", LastName = "Smith" }
+ });
+ var html = grid.Table(htmlAttributes: null);
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(
+ "<table><thead><tr>" +
+ "<th scope=\"col\"><a href=\"?sort=FirstName&amp;sortdir=ASC\">FirstName</a></th>" +
+ "<th scope=\"col\"><a href=\"?sort=LastName&amp;sortdir=ASC\">LastName</a></th>" +
+ "</tr></thead>" +
+ "<tbody>" +
+ "<tr><td>Joe</td><td>Smith</td></tr>" +
+ "</tbody></table>", html.ToString());
+ XhtmlAssert.Validate1_1(html);
+ }
+
+ [Fact]
+ public void TableRenderingIsNotAffectedWhenAttributesIsEmpty()
+ {
+ var grid = new WebGrid(GetContext()).Bind(new[]
+ {
+ new { FirstName = "Joe", LastName = "Smith" }
+ });
+ var html = grid.Table(htmlAttributes: new { });
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(
+ "<table><thead><tr>" +
+ "<th scope=\"col\"><a href=\"?sort=FirstName&amp;sortdir=ASC\">FirstName</a></th>" +
+ "<th scope=\"col\"><a href=\"?sort=LastName&amp;sortdir=ASC\">LastName</a></th>" +
+ "</tr></thead>" +
+ "<tbody>" +
+ "<tr><td>Joe</td><td>Smith</td></tr>" +
+ "</tbody></table>", html.ToString());
+ XhtmlAssert.Validate1_1(html);
+ }
+
+ [Fact]
+ public void TableRenderingWithStyles()
+ {
+ NameValueCollection queryString = new NameValueCollection();
+ queryString["row"] = "1";
+ var grid = new WebGrid(GetContext(queryString), rowsPerPage: 4).Bind(new[]
+ {
+ new { FirstName = "Joe", LastName = "Smith" },
+ new { FirstName = "Bob", LastName = "Johnson" }
+ });
+ var html = grid.Table(tableStyle: "tbl", headerStyle: "hdr", footerStyle: "ftr",
+ rowStyle: "row", alternatingRowStyle: "arow", selectedRowStyle: "sel", fillEmptyRows: true,
+ footer: item => "footer text",
+ columns: new[]
+ {
+ grid.Column("firstName", style: "c1", canSort: false),
+ grid.Column("lastName", style: "c2", canSort: false)
+ });
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(
+ "<table class=\"tbl\"><thead><tr class=\"hdr\">" +
+ "<th scope=\"col\">firstName</th><th scope=\"col\">lastName</th>" +
+ "</tr></thead>" +
+ "<tfoot>" +
+ "<tr class=\"ftr\"><td colspan=\"2\">footer text</td></tr>" +
+ "</tfoot>" +
+ "<tbody>" +
+ "<tr class=\"row sel\"><td class=\"c1\">Joe</td><td class=\"c2\">Smith</td></tr>" +
+ "<tr class=\"arow\"><td class=\"c1\">Bob</td><td class=\"c2\">Johnson</td></tr>" +
+ "<tr class=\"row\"><td class=\"c1\">&nbsp;</td><td class=\"c2\">&nbsp;</td></tr>" +
+ "<tr class=\"arow\"><td class=\"c1\">&nbsp;</td><td class=\"c2\">&nbsp;</td></tr>" +
+ "</tbody></table>", html.ToString());
+ XhtmlAssert.Validate1_1(html);
+ }
+
+ [Fact]
+ public void TableWithAjax()
+ {
+ var grid = new WebGrid(GetContext(), ajaxUpdateContainerId: "grid").Bind(new[]
+ {
+ new { First = "First", Second = "Second" }
+ });
+ string html = grid.Table().ToString();
+ Assert.True(html.Contains("<script"));
+ Assert.True(html.Contains("swhgajax=\"true\""));
+ }
+
+ [Fact]
+ public void TableWithAjaxAndCallback()
+ {
+ var grid = new WebGrid(GetContext(), ajaxUpdateContainerId: "grid", ajaxUpdateCallback: "myCallback").Bind(new[]
+ {
+ new { First = "First", Second = "Second" }
+ });
+ string html = grid.Table().ToString();
+ Assert.True(html.Contains("<script"));
+ Assert.True(html.Contains("myCallback"));
+ }
+
+ [Fact]
+ public void WebGridEncodesAjaxDataStrings()
+ {
+ var grid = new WebGrid(GetContext(), ajaxUpdateContainerId: "'grid'", ajaxUpdateCallback: "'myCallback'").Bind(new[]
+ {
+ new { First = "First", Second = "Second" }
+ });
+ string html = grid.Table().ToString();
+ Assert.True(html.Contains(@"&#39;grid&#39;"));
+ Assert.True(html.Contains(@"&#39;myCallback&#39;"));
+ }
+
+ [Fact]
+ public void WebGridThrowsIfOperationsArePerformedBeforeBinding()
+ {
+ // Arrange
+ string errorMessage = "A data source must be bound before this operation can be performed.";
+ var grid = new WebGrid(GetContext());
+
+ // Act and Assert
+ Assert.Throws<InvalidOperationException>(() => { var rows = grid.Rows; }, errorMessage);
+ Assert.Throws<InvalidOperationException>(() => { int count = grid.TotalRowCount; }, errorMessage);
+ Assert.Throws<InvalidOperationException>(() => grid.GetHtml().ToString(), errorMessage);
+ Assert.Throws<InvalidOperationException>(() => grid.Pager().ToString(), errorMessage);
+ Assert.Throws<InvalidOperationException>(() => grid.Table().ToString(), errorMessage);
+ Assert.Throws<InvalidOperationException>(() =>
+ {
+ grid.SelectedIndex = 1;
+ var row = grid.SelectedRow;
+ }, errorMessage);
+ }
+
+ [Fact]
+ public void WebGridThrowsIfBindingIsPerformedWhenAlreadyBound()
+ {
+ // Arrange
+ var grid = new WebGrid(GetContext());
+ var values = Enumerable.Range(0, 10).Cast<dynamic>();
+
+ // Act
+ grid.Bind(values);
+
+ // Assert
+ Assert.Throws<InvalidOperationException>(() => grid.Bind(values), "The WebGrid instance is already bound to a data source.");
+ }
+
+ [Fact]
+ public void GetElementTypeReturnsDynamicTypeIfElementIsDynamic()
+ {
+ // Arrange
+ IEnumerable<dynamic> elements = Dynamics(new[] { new Person { FirstName = "Foo", LastName = "Bar" } });
+
+ // Act
+ Type type = WebGrid.GetElementType(elements);
+
+ // Assert
+ Assert.Equal(typeof(IDynamicMetaObjectProvider), type);
+ }
+
+ [Fact]
+ public void GetElementTypeReturnsEnumerableTypeIfFirstInstanceIsNotDynamic()
+ {
+ // Arrange
+ IEnumerable<dynamic> elements = Iterator();
+
+ // Act
+ Type type = WebGrid.GetElementType(elements);
+
+ // Assert
+ Assert.Equal(typeof(Person), type);
+ }
+
+ [Fact]
+ public void TableThrowsIfQueryStringDerivedSortColumnIsExcluded()
+ {
+ // Arrange
+ NameValueCollection collection = new NameValueCollection();
+ collection["sort"] = "Salary";
+ var context = GetContext(collection);
+ IList<Employee> employees = new List<Employee>();
+ employees.Add(new Employee { Name = "A", Salary = 5, Manager = new Employee { Name = "-" } });
+ employees.Add(new Employee { Name = "B", Salary = 20, Manager = employees[0] });
+ employees.Add(new Employee { Name = "C", Salary = 15, Manager = employees[0] });
+ employees.Add(new Employee { Name = "D", Salary = 5, Manager = employees[1] });
+
+ var grid = new WebGrid(context, defaultSort: "Name").Bind(employees);
+
+ // Act and Assert
+ Assert.Throws<InvalidOperationException>(() => grid.GetHtml(exclusions: new[] { "Salary" }), "Column \"Salary\" does not exist.");
+ }
+
+ [Fact]
+ public void TableThrowsIfQueryStringDerivedSortColumnDoesNotExistInColumnsArgument()
+ {
+ // Arrange
+ NameValueCollection collection = new NameValueCollection();
+ collection["sort"] = "Salary";
+ var context = GetContext(collection);
+ IList<Employee> employees = new List<Employee>();
+ employees.Add(new Employee { Name = "A", Salary = 5, Manager = new Employee { Name = "-" } });
+ employees.Add(new Employee { Name = "B", Salary = 20, Manager = employees[0] });
+ employees.Add(new Employee { Name = "C", Salary = 15, Manager = employees[0] });
+ employees.Add(new Employee { Name = "D", Salary = 5, Manager = employees[1] });
+
+ var grid = new WebGrid(context, canSort: true, defaultSort: "Name").Bind(employees);
+
+ // Act and Assert
+ Assert.Throws<InvalidOperationException>(
+ () => grid.Table(columns: new[] { new WebGridColumn { ColumnName = "Name" }, new WebGridColumn { ColumnName = "Manager.Name" } }),
+ "Column \"Salary\" does not exist.");
+ }
+
+ [Fact]
+ public void TableDoesNotThrowIfQueryStringDerivedSortColumnIsVisibleButNotSortable()
+ {
+ // Arrange
+ NameValueCollection collection = new NameValueCollection();
+ collection["sort"] = "Salary";
+ collection["sortDir"] = "Desc";
+ var context = GetContext(collection);
+ IList<Employee> employees = new List<Employee>();
+ employees.Add(new Employee { Name = "A", Salary = 5, Manager = new Employee { Name = "-" } });
+ employees.Add(new Employee { Name = "B", Salary = 20, Manager = employees[0] });
+ employees.Add(new Employee { Name = "C", Salary = 15, Manager = employees[0] });
+ employees.Add(new Employee { Name = "D", Salary = 10, Manager = employees[1] });
+
+ var grid = new WebGrid(context, canSort: true).Bind(employees);
+
+ // Act
+ var html = grid.Table(columns: new[] { new WebGridColumn { ColumnName = "Salary", CanSort = false } });
+
+ // Assert
+ Assert.NotNull(html);
+ Assert.Equal(grid.Rows[0]["Salary"], 20);
+ Assert.Equal(grid.Rows[1]["Salary"], 15);
+ Assert.Equal(grid.Rows[2]["Salary"], 10);
+ Assert.Equal(grid.Rows[3]["Salary"], 5);
+ }
+
+ [Fact]
+ public void TableThrowsIfComplexPropertyIsUnsortable()
+ {
+ // Arrange
+ NameValueCollection collection = new NameValueCollection();
+ collection["sort"] = "Manager.Salary";
+ var context = GetContext(collection);
+ IList<Employee> employees = new List<Employee>();
+ employees.Add(new Employee { Name = "A", Salary = 5, Manager = new Employee { Name = "-" } });
+ employees.Add(new Employee { Name = "B", Salary = 20, Manager = employees[0] });
+ employees.Add(new Employee { Name = "C", Salary = 15, Manager = employees[0] });
+ employees.Add(new Employee { Name = "D", Salary = 5, Manager = employees[1] });
+ var grid = new WebGrid(context).Bind(employees, columnNames: new[] { "Name", "Manager.Name" });
+
+ // Act and Assert
+ Assert.Throws<InvalidOperationException>(() => grid.GetHtml(),
+ "Column \"Manager.Salary\" does not exist.");
+ }
+
+ [Fact]
+ public void TableDoesNotThrowIfUnsortableColumnIsExplicitlySpecifiedByUser()
+ {
+ // Arrange
+ var context = GetContext();
+ IList<Employee> employees = new List<Employee>();
+ employees.Add(new Employee { Name = "A", Salary = 5, Manager = new Employee { Name = "-" } });
+ employees.Add(new Employee { Name = "C", Salary = 15, Manager = employees[0] });
+ employees.Add(new Employee { Name = "D", Salary = 10, Manager = employees[1] });
+
+ // Act
+ var grid = new WebGrid(context).Bind(employees, columnNames: new[] { "Name", "Manager.Name" });
+ grid.SortColumn = "Salary";
+ var html = grid.Table();
+
+ // Assert
+ Assert.Equal(grid.Rows[0]["Salary"], 5);
+ Assert.Equal(grid.Rows[1]["Salary"], 10);
+ Assert.Equal(grid.Rows[2]["Salary"], 15);
+
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(
+ "<table><thead><tr>"
+ + "<th scope=\"col\"><a href=\"?sort=Name&amp;sortdir=ASC\">Name</a></th>"
+ + "<th scope=\"col\"><a href=\"?sort=Manager.Name&amp;sortdir=ASC\">Manager.Name</a></th>"
+ + "</tr></thead><tbody>"
+ + "<tr><td>A</td><td>-</td></tr>"
+ + "<tr><td>D</td><td>C</td></tr>"
+ + "<tr><td>C</td><td>A</td></tr>"
+ + "</tbody></table>", html.ToString());
+ }
+
+ [Fact]
+ public void TableDoesNotThrowIfUnsortableColumnIsDefaultSortColumn()
+ {
+ // Arrange
+ var context = GetContext();
+ IList<Employee> employees = new List<Employee>();
+ employees.Add(new Employee { Name = "A", Salary = 5, Manager = new Employee { Name = "-" } });
+ employees.Add(new Employee { Name = "C", Salary = 15, Manager = employees[0] });
+ employees.Add(new Employee { Name = "D", Salary = 10, Manager = employees[1] });
+
+ // Act
+ var grid = new WebGrid(context, defaultSort: "Salary").Bind(employees, columnNames: new[] { "Name", "Manager.Name" });
+ var html = grid.Table();
+
+ // Assert
+ Assert.Equal(grid.Rows[0]["Salary"], 5);
+ Assert.Equal(grid.Rows[1]["Salary"], 10);
+ Assert.Equal(grid.Rows[2]["Salary"], 15);
+
+ UnitTestHelper.AssertEqualsIgnoreWhitespace(
+ "<table><thead><tr>"
+ + "<th scope=\"col\"><a href=\"?sort=Name&amp;sortdir=ASC\">Name</a></th>"
+ + "<th scope=\"col\"><a href=\"?sort=Manager.Name&amp;sortdir=ASC\">Manager.Name</a></th>"
+ + "</tr></thead><tbody>"
+ + "<tr><td>A</td><td>-</td></tr>"
+ + "<tr><td>D</td><td>C</td></tr>"
+ + "<tr><td>C</td><td>A</td></tr>"
+ + "</tbody></table>", html.ToString());
+ }
+
+ private static IEnumerable<Person> Iterator()
+ {
+ yield return new Person { FirstName = "Foo", LastName = "Bar" };
+ }
+
+ [Fact]
+ public void GetElementTypeReturnsEnumerableTypeIfCollectionPassedImplementsEnumerable()
+ {
+ // Arrange
+ IList<Person> listElements = new List<Person> { new Person { FirstName = "Foo", LastName = "Bar" } };
+ HashSet<dynamic> setElements = new HashSet<dynamic> { new DynamicWrapper(new Person { FirstName = "Foo", LastName = "Bar" }) };
+
+ // Act
+ Type listType = WebGrid.GetElementType(listElements);
+ Type setType = WebGrid.GetElementType(setElements);
+
+ // Assert
+ Assert.Equal(typeof(Person), listType);
+ Assert.Equal(typeof(IDynamicMetaObjectProvider), setType);
+ }
+
+ [Fact]
+ public void GetElementTypeReturnsEnumerableTypeIfCollectionImplementsEnumerable()
+ {
+ // Arrange
+ IEnumerable<Person> elements = new NonGenericEnumerable(new[] { new Person { FirstName = "Foo", LastName = "Bar" } });
+ ;
+
+ // Act
+ Type type = WebGrid.GetElementType(elements);
+
+ // Assert
+ Assert.Equal(typeof(Person), type);
+ }
+
+ [Fact]
+ public void GetElementTypeReturnsEnumerableTypeIfCollectionIsIEnumerable()
+ {
+ // Arrange
+ IEnumerable<Person> elements = new GenericEnumerable<Person>(new[] { new Person { FirstName = "Foo", LastName = "Bar" } });
+ ;
+
+ // Act
+ Type type = WebGrid.GetElementType(elements);
+
+ // Assert
+ Assert.Equal(typeof(Person), type);
+ }
+
+ [Fact]
+ public void GetElementTypeDoesNotThrowIfTypeIsNotGeneric()
+ {
+ // Arrange
+ IEnumerable<dynamic> elements = new[] { new Person { FirstName = "Foo", LastName = "Bar" } };
+
+ // Act
+ Type type = WebGrid.GetElementType(elements);
+
+ // Assert
+ Assert.Equal(typeof(Person), type);
+ }
+
+ private static IEnumerable<dynamic> Dynamics(params object[] objects)
+ {
+ return (from o in objects
+ select new DynamicWrapper(o)).ToArray();
+ }
+
+ private static HttpContextBase GetContext(NameValueCollection queryString = null)
+ {
+ Mock<HttpRequestBase> requestMock = new Mock<HttpRequestBase>();
+ requestMock.Setup(request => request.QueryString).Returns(queryString ?? new NameValueCollection());
+
+ Mock<HttpContextBase> contextMock = new Mock<HttpContextBase>();
+ contextMock.Setup(context => context.Request).Returns(requestMock.Object);
+ contextMock.Setup(context => context.Items).Returns(new Hashtable());
+ return contextMock.Object;
+ }
+
+ class Person
+ {
+ public string FirstName { get; set; }
+ public string LastName { get; set; }
+ }
+
+ private class Employee
+ {
+ public string Name { get; set; }
+ public int Salary { get; set; }
+ public Employee Manager { get; set; }
+ }
+
+ class NonGenericEnumerable : IEnumerable<Person>
+ {
+ private IEnumerable<Person> _source;
+
+ public NonGenericEnumerable(IEnumerable<Person> source)
+ {
+ _source = source;
+ }
+
+ public IEnumerator<Person> GetEnumerator()
+ {
+ return _source.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+ }
+
+ class GenericEnumerable<T> : IEnumerable<T>
+ {
+ private IEnumerable<T> _source;
+
+ public GenericEnumerable(IEnumerable<T> source)
+ {
+ _source = source;
+ }
+
+ public IEnumerator<T> GetEnumerator()
+ {
+ return _source.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Helpers.Test/WebImageTest.cs b/test/System.Web.Helpers.Test/WebImageTest.cs
new file mode 100644
index 00000000..375be0dd
--- /dev/null
+++ b/test/System.Web.Helpers.Test/WebImageTest.cs
@@ -0,0 +1,1162 @@
+using System.Drawing;
+using System.Drawing.Imaging;
+using System.IO;
+using System.Web.WebPages.TestUtils;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Helpers.Test
+{
+ public class WebImageTest
+ {
+ private static readonly byte[] _JpgImageBytes = TestFile.Create("LambdaFinal.jpg").ReadAllBytes();
+ private static readonly byte[] _BmpImageBytes = TestFile.Create("logo.bmp").ReadAllBytes();
+ private static readonly byte[] _PngImageBytes = TestFile.Create("NETLogo.png").ReadAllBytes();
+ private static readonly byte[] _HiResImageBytes = TestFile.Create("HiRes.jpg").ReadAllBytes();
+
+ [Fact]
+ public void ConstructorThrowsWhenFilePathIsNull()
+ {
+ Assert.ThrowsArgument(() =>
+ new WebImage(GetContext(), s => new byte[] { }, filePath: null), "filePath", "Value cannot be null or an empty string.");
+ }
+
+ [Fact]
+ public void ConstructorThrowsWhenFilePathIsEmpty()
+ {
+ Assert.ThrowsArgument(() =>
+ new WebImage(GetContext(), s => new byte[] { }, filePath: String.Empty), "filePath", "Value cannot be null or an empty string.");
+ }
+
+ [Fact]
+ public void ConstructorThrowsWhenFilePathIsInvalid()
+ {
+ Assert.Throws<DirectoryNotFoundException>(() =>
+ new WebImage(GetContext(), s => { throw new DirectoryNotFoundException(); }, @"x:\this\does\not\exist.jpg"));
+ }
+
+ [Fact]
+ public void ConstructorThrowsWhenFileContentIsInvalid()
+ {
+ byte[] imageContent = new byte[] { 32, 111, 209, 138, 76, 32 };
+ Assert.ThrowsArgument(() => new WebImage(imageContent), "content",
+ "An image could not be constructed from the content provided.");
+ }
+
+ [Fact]
+ public void FilePathReturnsCorrectPath()
+ {
+ // Arrange
+ string imageName = @"x:\My-test-image.png";
+
+ // Act
+ WebImage image = new WebImage(GetContext(), s => _PngImageBytes, imageName);
+
+ // Assert
+ Assert.Equal(imageName, image.FileName);
+ }
+
+ [Fact]
+ public void FilePathCanBeSet()
+ {
+ // Arrange
+ string originalPath = @"x:\somePath.png";
+ string newPath = @"x:\someOtherPath.jpg";
+
+ // Act
+ WebImage image = new WebImage(GetContext(), s => _PngImageBytes, originalPath);
+ image.FileName = newPath;
+
+ // Assert
+ Assert.Equal(newPath, image.FileName);
+ }
+
+ [Fact]
+ public void SimpleGetBytesClonesArray()
+ {
+ WebImage image = new WebImage(_PngImageBytes);
+
+ byte[] returnedContent = image.GetBytes();
+
+ Assert.False(ReferenceEquals(_PngImageBytes, returnedContent), "GetBytes should clone array.");
+ Assert.Equal(_PngImageBytes, returnedContent);
+ }
+
+ [Fact]
+ public void WebImagePreservesOriginalFormatFromFile()
+ {
+ WebImage image = new WebImage(_PngImageBytes);
+
+ byte[] returnedContent = image.GetBytes();
+
+ // If format was changed; content would be different
+ Assert.Equal(_PngImageBytes, returnedContent);
+ }
+
+ [Fact]
+ public void WebImagePreservesOriginalFormatFromStream()
+ {
+ WebImage image = null;
+ byte[] originalContent = _PngImageBytes;
+ using (MemoryStream stream = new MemoryStream(originalContent))
+ {
+ image = new WebImage(stream);
+ } // dispose stream; WebImage should have no dependency on it
+
+ byte[] returnedContent = image.GetBytes();
+
+ // If format was changed; content would be different
+ Assert.Equal(originalContent, returnedContent);
+ }
+
+ [Fact]
+ public void WebImageCorrectlyReadsFromNoSeekStream()
+ {
+ WebImage image = null;
+
+ byte[] originalContent = _PngImageBytes;
+ using (MemoryStream stream = new MemoryStream(originalContent))
+ {
+ TestStream ts = new TestStream(stream);
+ image = new WebImage(ts);
+ } // dispose stream; WebImage should have no dependency on it
+
+ byte[] returnedContent = image.GetBytes();
+
+ // If chunks are not assembled correctly; content would be different and image would be corrupted.
+ Assert.Equal(originalContent, returnedContent);
+ Assert.Equal("png", image.ImageFormat);
+ }
+
+ [Fact]
+ public void GetBytesWithNullReturnsClonesArray()
+ {
+ byte[] originalContent = _BmpImageBytes;
+ WebImage image = new WebImage(originalContent);
+
+ byte[] returnedContent = image.GetBytes();
+
+ Assert.False(ReferenceEquals(originalContent, returnedContent), "GetBytes with string null should clone array.");
+ Assert.Equal(originalContent, returnedContent);
+ }
+
+ [Fact]
+ public void GetBytesWithSameFormatReturnsSameFormat()
+ {
+ byte[] originalContent = _JpgImageBytes;
+ WebImage image = new WebImage(originalContent);
+
+ byte[] returnedContent = image.GetBytes("jpeg");
+
+ Assert.False(ReferenceEquals(originalContent, returnedContent), "GetBytes with string null should clone array.");
+ Assert.Equal(originalContent, returnedContent);
+ }
+
+ [Fact]
+ public void GetBytesWithDifferentFormatReturnsExpectedFormat()
+ {
+ byte[] originalContent = _BmpImageBytes;
+ WebImage image = new WebImage(originalContent);
+
+ // Request different format
+ byte[] returnedContent = image.GetBytes("jpg");
+
+ Assert.False(ReferenceEquals(originalContent, returnedContent), "GetBytes with string format should clone array.");
+ using (MemoryStream stream = new MemoryStream(returnedContent))
+ {
+ using (Image tempImage = Image.FromStream(stream))
+ {
+ Assert.Equal(ImageFormat.Jpeg, tempImage.RawFormat);
+ }
+ }
+ }
+
+ [Fact]
+ public void GetBytesWithSameFormatReturnsSameFormatWhenCreatedFromFile()
+ {
+ byte[] originalContent = _BmpImageBytes;
+ // Format is not set during construction.
+ WebImage image = new WebImage(_BmpImageBytes);
+
+ byte[] returnedContent = image.GetBytes("bmp");
+
+ Assert.False(ReferenceEquals(originalContent, returnedContent), "GetBytes with string format should clone array.");
+ Assert.Equal(originalContent, returnedContent);
+ }
+
+ [Fact]
+ public void GetBytesWithNoFormatReturnsInitialFormatEvenAfterTransformations()
+ {
+ byte[] originalContent = _BmpImageBytes;
+ // Format is not set during construction.
+ WebImage image = new WebImage(_BmpImageBytes);
+ image.Crop(top: 10, bottom: 10);
+
+ byte[] returnedContent = image.GetBytes();
+
+ Assert.NotEqual(originalContent, returnedContent);
+ using (MemoryStream stream = new MemoryStream(returnedContent))
+ {
+ using (Image tempImage = Image.FromStream(stream))
+ {
+ Assert.Equal(ImageFormat.Bmp, tempImage.RawFormat);
+ }
+ }
+ }
+
+ [Fact]
+ public void GetBytesThrowsOnIncorrectFormat()
+ {
+ WebImage image = new WebImage(_JpgImageBytes);
+ Assert.ThrowsArgument(
+ () => image.GetBytes("bmpx"),
+ "format",
+ "\"bmpx\" is invalid image format. Valid values are image format names like: \"JPEG\", \"BMP\", \"GIF\", \"PNG\", etc.");
+ }
+
+ [Fact]
+ public void GetBytesWithDifferentFormatReturnsExpectedFormatWhenCreatedFromFile()
+ {
+ // Format is not set during construction.
+ WebImage image = new WebImage(_PngImageBytes);
+
+ // Request different format
+ byte[] returnedContent = image.GetBytes("jpg");
+
+ WebImage newImage = new WebImage(returnedContent);
+
+ Assert.Equal("jpeg", newImage.ImageFormat);
+ }
+
+ [Fact]
+ public void GetImageFromRequestReturnsNullForIncorrectMimeType()
+ {
+ // Arrange
+ Mock<HttpPostedFileBase> postedFile = new Mock<HttpPostedFileBase>();
+ postedFile.Setup(c => c.FileName).Returns("index.cshtml");
+ postedFile.Setup(c => c.ContentType).Returns("image/jpg");
+
+ Mock<HttpFileCollectionBase> files = new Mock<HttpFileCollectionBase>();
+ files.Setup(c => c[0]).Returns(postedFile.Object);
+ Mock<HttpRequestBase> request = new Mock<HttpRequestBase>();
+ request.Setup(r => r.Files).Returns(files.Object);
+
+ // Act and Assert
+ Assert.Null(WebImage.GetImageFromRequest(request.Object));
+ }
+
+ [Fact]
+ public void GetImageFromRequestDeterminesMimeTypeFromExtension()
+ {
+ // Arrange
+ Mock<HttpPostedFileBase> postedFile = new Mock<HttpPostedFileBase>();
+ postedFile.Setup(c => c.FileName).Returns("index.jpeg");
+ postedFile.Setup(c => c.ContentType).Returns("application/octet-stream");
+ postedFile.Setup(c => c.ContentLength).Returns(1);
+ postedFile.Setup(c => c.InputStream).Returns(new MemoryStream(_JpgImageBytes));
+
+ Mock<HttpFileCollectionBase> files = new Mock<HttpFileCollectionBase>();
+ files.Setup(c => c.Count).Returns(1);
+ files.Setup(c => c[0]).Returns(postedFile.Object);
+ Mock<HttpRequestBase> request = new Mock<HttpRequestBase>();
+ request.Setup(r => r.Files).Returns(files.Object);
+
+ // Act
+ WebImage image = WebImage.GetImageFromRequest(request.Object);
+
+ // Assert
+ Assert.NotNull(image);
+ Assert.Equal("jpeg", image.ImageFormat);
+ }
+
+ [Fact]
+ public void GetImageFromRequestIsCaseInsensitive()
+ {
+ // Arrange
+ Mock<HttpPostedFileBase> postedFile = new Mock<HttpPostedFileBase>();
+ postedFile.SetupGet(c => c.FileName).Returns("index.JPg");
+ postedFile.SetupGet(c => c.ContentType).Returns("application/octet-stream");
+ postedFile.SetupGet(c => c.ContentLength).Returns(1);
+ postedFile.SetupGet(c => c.InputStream).Returns(new MemoryStream(_JpgImageBytes));
+
+ Mock<HttpFileCollectionBase> files = new Mock<HttpFileCollectionBase>();
+ files.Setup(c => c.Count).Returns(1);
+ files.Setup(c => c[0]).Returns(postedFile.Object);
+ Mock<HttpRequestBase> request = new Mock<HttpRequestBase>();
+ request.Setup(r => r.Files).Returns(files.Object);
+
+ // Act
+ WebImage image = WebImage.GetImageFromRequest(request.Object);
+
+ // Assert
+ Assert.NotNull(image);
+ Assert.Equal("jpeg", image.ImageFormat);
+ }
+
+ [Fact]
+ public void ImagePropertiesAreCorrectForBmpImage()
+ {
+ WebImage image = new WebImage(_BmpImageBytes);
+
+ Assert.Equal("bmp", image.ImageFormat);
+ Assert.Equal(108, image.Width);
+ Assert.Equal(44, image.Height);
+ }
+
+ [Fact]
+ public void ImagePropertiesAreCorrectForPngImage()
+ {
+ WebImage image = new WebImage(_PngImageBytes);
+
+ Assert.Equal("png", image.ImageFormat);
+ Assert.Equal(160, image.Width);
+ Assert.Equal(152, image.Height);
+ }
+
+ [Fact]
+ public void ImagePropertiesAreCorrectForJpgImage()
+ {
+ WebImage image = new WebImage(_JpgImageBytes);
+
+ Assert.Equal("jpeg", image.ImageFormat);
+ Assert.Equal(634, image.Width);
+ Assert.Equal(489, image.Height);
+ }
+
+ [Fact]
+ public void ResizePreservesRatio()
+ {
+ WebImage image = new WebImage(_JpgImageBytes);
+
+ image.Resize(200, 100, preserveAspectRatio: true, preventEnlarge: true);
+
+ Assert.Equal(130, image.Width);
+ Assert.Equal(100, image.Height);
+ }
+
+ [Fact]
+ public void ResizePreservesResolution()
+ {
+ MemoryStream output = null;
+ Action<string, byte[]> saveAction = (_, content) => { output = new MemoryStream(content); };
+
+ WebImage image = new WebImage(_HiResImageBytes);
+
+ image.Resize(200, 100, preserveAspectRatio: true, preventEnlarge: true);
+
+ image.Save(GetContext(), saveAction, @"x:\ResizePreservesResolution.jpg", "jpeg", forceWellKnownExtension: true);
+ using (Image original = Image.FromStream(new MemoryStream(_HiResImageBytes)))
+ {
+ using (Image modified = Image.FromStream(output))
+ {
+ Assert.Equal(original.HorizontalResolution, modified.HorizontalResolution);
+ Assert.Equal(original.VerticalResolution, modified.VerticalResolution);
+ }
+ }
+ }
+
+ [Fact]
+ public void ResizePreservesFormat()
+ {
+ // Arrange
+ WebImage image = new WebImage(_PngImageBytes);
+ MemoryStream output = null;
+ Action<string, byte[]> saveAction = (_, content) => { output = new MemoryStream(content); };
+
+ // Act
+ image.Resize(200, 100, preserveAspectRatio: true, preventEnlarge: true);
+
+ // Assert
+ Assert.Equal(image.ImageFormat, "png");
+ image.Save(GetContext(), saveAction, @"x:\1.png", null, false);
+
+ using (Image modified = Image.FromStream(output))
+ {
+ Assert.Equal(ImageFormat.Png, modified.RawFormat);
+ }
+ }
+
+ [Fact]
+ public void SaveUpdatesFileNameOfWebImageWhenForcingWellKnownExtension()
+ {
+ // Arrange
+ var context = GetContext();
+
+ // Act
+ WebImage image = new WebImage(context, _ => _JpgImageBytes, @"c:\images\foo.jpg");
+
+ image.Save(context, (_, __) => { }, @"x:\1.exe", "jpg", forceWellKnownExtension: true);
+
+ // Assert
+ Assert.Equal(@"x:\1.exe.jpeg", image.FileName);
+ }
+
+ [Fact]
+ public void SaveUpdatesFileNameOfWebImageWhenFormatChanges()
+ {
+ // Arrange
+ string imagePath = @"x:\images\foo.jpg";
+ var context = GetContext();
+
+ // Act
+ WebImage image = new WebImage(context, _ => _JpgImageBytes, imagePath);
+
+ image.Save(context, (_, __) => { }, imagePath, "png", forceWellKnownExtension: true);
+
+ // Assert
+ Assert.Equal(@"x:\images\foo.jpg.png", image.FileName);
+ }
+
+ [Fact]
+ public void SaveKeepsNameIfFormatIsUnchanged()
+ {
+ // Arrange
+ string imagePath = @"x:\images\foo.jpg";
+ var context = GetContext();
+
+ // Act
+ WebImage image = new WebImage(context, _ => _JpgImageBytes, imagePath);
+
+ image.Save(context, (_, __) => { }, imagePath, "jpg", forceWellKnownExtension: true);
+
+ // Assert
+ Assert.Equal(@"x:\images\foo.jpg", image.FileName);
+ }
+
+ [Fact]
+ public void ResizeThrowsOnIncorrectWidthOrHeight()
+ {
+ WebImage image = new WebImage(_JpgImageBytes);
+
+ Assert.ThrowsArgumentGreaterThan(
+ () => image.Resize(-1, 100, preserveAspectRatio: true, preventEnlarge: true),
+ "width",
+ "0");
+
+ Assert.ThrowsArgumentGreaterThan(
+ () => image.Resize(100, -1, preserveAspectRatio: true, preventEnlarge: true),
+ "height",
+ "0");
+ }
+
+ [Fact]
+ public void ResizeAndRotateDoesOperationsInRightOrder()
+ {
+ WebImage image = new WebImage(_JpgImageBytes);
+ image.Resize(200, 100, preserveAspectRatio: true, preventEnlarge: true).RotateLeft();
+
+ Assert.Equal(100, image.Width);
+ Assert.Equal(130, image.Height);
+ }
+
+ [Fact]
+ public void ClonePreservesAllInformation()
+ {
+ WebImage image = new WebImage(_JpgImageBytes);
+ image.Resize(200, 100, preserveAspectRatio: true, preventEnlarge: true).RotateLeft();
+
+ // this should preserve list of transformations
+ WebImage cloned = image.Clone();
+
+ Assert.Equal(100, cloned.Width);
+ Assert.Equal(130, cloned.Height);
+ }
+
+ [Fact]
+ public void ResizePreventsEnlarge()
+ {
+ WebImage image = new WebImage(_JpgImageBytes);
+
+ int height = image.Height;
+ int width = image.Width;
+
+ image.Resize(width * 2, height, preserveAspectRatio: true, preventEnlarge: true);
+ Assert.Equal(width, image.Width);
+ Assert.Equal(height, image.Height);
+ }
+
+ [Fact]
+ public void CropCreatesCroppedImage()
+ {
+ WebImage image = new WebImage(_JpgImageBytes);
+ image.Crop(20, 20, 20, 20);
+
+ Assert.Equal(594, image.Width);
+ Assert.Equal(449, image.Height);
+ }
+
+ [Fact]
+ public void CropThrowsOnIncorrectArguments()
+ {
+ WebImage image = new WebImage(_JpgImageBytes);
+
+ Assert.ThrowsArgumentGreaterThanOrEqualTo(
+ () => image.Crop(top: -1),
+ "top",
+ "0");
+
+ Assert.ThrowsArgumentGreaterThanOrEqualTo(
+ () => image.Crop(left: -1),
+ "left",
+ "0");
+
+ Assert.ThrowsArgumentGreaterThanOrEqualTo(
+ () => image.Crop(bottom: -1),
+ "bottom",
+ "0");
+
+ Assert.ThrowsArgumentGreaterThanOrEqualTo(
+ () => image.Crop(right: -1),
+ "right",
+ "0");
+ }
+
+ [Fact]
+ public void RotateLeftReturnsRotatedImage()
+ {
+ WebImage image = new WebImage(_PngImageBytes);
+ image.RotateLeft();
+
+ Assert.Equal(152, image.Width);
+ Assert.Equal(160, image.Height);
+ }
+
+ [Fact]
+ public void RotateRightReturnsRotatedImage()
+ {
+ WebImage image = new WebImage(_PngImageBytes);
+ image.RotateRight();
+
+ Assert.Equal(152, image.Width);
+ Assert.Equal(160, image.Height);
+ }
+
+ [Fact]
+ public void FlipVerticalReturnsFlippedImage()
+ {
+ WebImage image = new WebImage(_PngImageBytes);
+ image.FlipVertical();
+
+ Assert.Equal(160, image.Width);
+ Assert.Equal(152, image.Height);
+ }
+
+ [Fact]
+ public void FlipHorizontalReturnsFlippedImage()
+ {
+ WebImage image = new WebImage(_PngImageBytes);
+ image.FlipHorizontal();
+
+ Assert.Equal(160, image.Width);
+ Assert.Equal(152, image.Height);
+ }
+
+ [Fact]
+ public void MultipleCombinedOperationsExecuteInRightOrder()
+ {
+ WebImage image = new WebImage(_JpgImageBytes);
+ image.Resize(200, 100, preserveAspectRatio: true, preventEnlarge: true).RotateLeft();
+ image.Crop(top: 10, right: 10).AddTextWatermark("plan9");
+
+ Assert.Equal(90, image.Width);
+ Assert.Equal(120, image.Height);
+ }
+
+ [Fact]
+ public void AddTextWatermarkPreservesImageDimension()
+ {
+ WebImage image = new WebImage(_JpgImageBytes);
+ image.AddTextWatermark("Plan9", fontSize: 16, horizontalAlign: "Left", verticalAlign: "Bottom", opacity: 50);
+
+ Assert.Equal(634, image.Width);
+ Assert.Equal(489, image.Height);
+ }
+
+ [Fact]
+ public void AddTextWatermarkParsesHexColorCorrectly()
+ {
+ WebImage image = new WebImage(_JpgImageBytes);
+ image.AddTextWatermark("Plan9", fontSize: 16, fontColor: "#FF0000", horizontalAlign: "Center", verticalAlign: "Middle");
+
+ Assert.Equal(634, image.Width);
+ Assert.Equal(489, image.Height);
+ }
+
+ [Fact]
+ public void AddTextWatermarkParsesShortHexColorCorrectly()
+ {
+ WebImage image = new WebImage(_JpgImageBytes);
+ image.AddTextWatermark("Plan9", fontSize: 16, fontColor: "#F00", horizontalAlign: "Center", verticalAlign: "Middle");
+
+ Assert.Equal(634, image.Width);
+ Assert.Equal(489, image.Height);
+ }
+
+ [Fact]
+ public void AddTextWatermarkDoesNotChangeImageIfPaddingIsTooBig()
+ {
+ WebImage image = new WebImage(_JpgImageBytes);
+ image.AddTextWatermark("Plan9", padding: 1000);
+
+ Assert.Equal(634, image.Width);
+ Assert.Equal(489, image.Height);
+ }
+
+ [Fact]
+ public void AddTextWatermarkThrowsOnNegativeOpacity()
+ {
+ WebImage image = new WebImage(_JpgImageBytes);
+
+ Assert.ThrowsArgumentOutOfRange(() => image.AddTextWatermark("Plan9", opacity: -1), "opacity", "Value must be between 0 and 100.");
+ }
+
+ [Fact]
+ public void AddTextWatermarkThrowsOnTooBigOpacity()
+ {
+ WebImage image = new WebImage(_JpgImageBytes);
+
+ Assert.ThrowsArgumentOutOfRange(() => image.AddTextWatermark("Plan9", opacity: 155), "opacity", "Value must be between 0 and 100.");
+ }
+
+ [Fact]
+ public void AddTextWatermarkThrowsOnEmptyText()
+ {
+ WebImage image = new WebImage(_JpgImageBytes);
+ Assert.ThrowsArgumentNullOrEmptyString(
+ () => image.AddTextWatermark(""),
+ "text");
+ }
+
+ [Fact]
+ public void AddTextWatermarkThrowsOnIncorrectColorName()
+ {
+ WebImage image = new WebImage(_JpgImageBytes);
+ Assert.Throws<ArgumentException>(
+ () => image.AddTextWatermark("p9", fontColor: "super"),
+ "The \"fontColor\" value is invalid. Valid values are names like \"White\", \"Black\", or \"DarkBlue\", or hexadecimal values in the form \"#RRGGBB\" or \"#RGB\".");
+ }
+
+ [Fact]
+ public void AddTextWatermarkThrowsOnIncorrectHexColorValue()
+ {
+ WebImage image = new WebImage(_JpgImageBytes);
+ Assert.Throws<ArgumentException>(
+ () => image.AddTextWatermark("p9", fontColor: "#XXX"),
+ "The \"fontColor\" value is invalid. Valid values are names like \"White\", \"Black\", or \"DarkBlue\", or hexadecimal values in the form \"#RRGGBB\" or \"#RGB\".");
+ }
+
+ [Fact]
+ public void AddTextWatermarkThrowsOnIncorrectHexColorLength()
+ {
+ WebImage image = new WebImage(_JpgImageBytes);
+ Assert.Throws<ArgumentException>(
+ () => image.AddTextWatermark("p9", fontColor: "#F000"),
+ "The \"fontColor\" value is invalid. Valid values are names like \"White\", \"Black\", or \"DarkBlue\", or hexadecimal values in the form \"#RRGGBB\" or \"#RGB\".");
+ }
+
+ [Fact]
+ public void AddTextWatermarkThrowsOnIncorrectHorizontalAlignment()
+ {
+ WebImage image = new WebImage(_JpgImageBytes);
+ Assert.Throws<ArgumentException>(
+ () => image.AddTextWatermark("p9", horizontalAlign: "Justify"),
+ "The \"horizontalAlign\" value is invalid. Valid values are: \"Right\", \"Left\", and \"Center\".");
+ }
+
+ [Fact]
+ public void AddTextWatermarkThrowsOnIncorrectVerticalAlignment()
+ {
+ WebImage image = new WebImage(_JpgImageBytes);
+ Assert.Throws<ArgumentException>(
+ () => image.AddTextWatermark("p9", verticalAlign: "NotSet"),
+ "The \"verticalAlign\" value is invalid. Valid values are: \"Top\", \"Bottom\", and \"Middle\".");
+ }
+
+ [Fact]
+ public void AddTextWatermarkThrowsOnNegativePadding()
+ {
+ WebImage image = new WebImage(_JpgImageBytes);
+ Assert.ThrowsArgumentGreaterThanOrEqualTo(
+ () => image.AddTextWatermark("p9", padding: -10),
+ "padding",
+ "0");
+ }
+
+ [Fact]
+ public void AddTextWatermarkThrowsOnIncorrectFontSize()
+ {
+ WebImage image = new WebImage(_JpgImageBytes);
+ Assert.ThrowsArgumentGreaterThan(
+ () => image.AddTextWatermark("p9", fontSize: -10),
+ "fontSize",
+ "0");
+
+ Assert.ThrowsArgumentGreaterThan(
+ () => image.AddTextWatermark("p9", fontSize: 0),
+ "fontSize",
+ "0");
+ }
+
+ [Fact]
+ public void AddTextWatermarkThrowsOnIncorrectFontStyle()
+ {
+ WebImage image = new WebImage(_JpgImageBytes);
+
+ Assert.Throws<ArgumentException>(
+ () => image.AddTextWatermark("p9", fontStyle: "something"),
+ "The \"fontStyle\" value is invalid. Valid values are: \"Regular\", \"Bold\", \"Italic\", \"Underline\", and \"Strikeout\".");
+ }
+
+ [Fact]
+ public void AddTextWatermarkThrowsOnIncorrectFontFamily()
+ {
+ WebImage image = new WebImage(_JpgImageBytes);
+
+ Assert.Throws<ArgumentException>(
+ () => image.AddTextWatermark("p9", fontFamily: "something"),
+ "The \"fontFamily\" value is invalid. Valid values are font family names like: \"Arial\", \"Times New Roman\", etc. Make sure that the font family you are trying to use is installed on the server.");
+ }
+
+ [Fact]
+ public void AddImageWatermarkPreservesImageDimension()
+ {
+ WebImage watermark = new WebImage(_BmpImageBytes);
+ WebImage image = new WebImage(_JpgImageBytes);
+ image.AddImageWatermark(watermark, horizontalAlign: "LEFT", verticalAlign: "top", opacity: 50, padding: 10);
+
+ Assert.Equal(634, image.Width);
+ Assert.Equal(489, image.Height);
+ }
+
+ [Fact]
+ public void CanAddTextAndImageWatermarks()
+ {
+ WebImage watermark = new WebImage(_BmpImageBytes);
+ WebImage image = new WebImage(_JpgImageBytes);
+ image.AddImageWatermark(watermark, horizontalAlign: "LEFT", verticalAlign: "top", opacity: 30, padding: 10);
+ image.AddTextWatermark("plan9");
+
+ Assert.Equal(634, image.Width);
+ Assert.Equal(489, image.Height);
+ }
+
+ [Fact]
+ public void AddImageWatermarkDoesNotChangeWatermarkImage()
+ {
+ WebImage watermark = new WebImage(_BmpImageBytes);
+ WebImage image = new WebImage(_JpgImageBytes);
+ image.AddImageWatermark(watermark, width: 54, height: 22, horizontalAlign: "LEFT", verticalAlign: "top", opacity: 50, padding: 10);
+
+ Assert.Equal(108, watermark.Width);
+ Assert.Equal(44, watermark.Height);
+ }
+
+ [Fact]
+ public void AddImageWatermarkThrowsOnNullImage()
+ {
+ WebImage image = new WebImage(_JpgImageBytes);
+
+ Assert.ThrowsArgumentNull(
+ () => image.AddImageWatermark(watermarkImage: null),
+ "watermarkImage");
+ }
+
+ [Fact]
+ public void AddImageWatermarkThrowsWhenJustOneDimensionIsZero()
+ {
+ WebImage watermark = new WebImage(_BmpImageBytes);
+ WebImage image = new WebImage(_JpgImageBytes);
+
+ string message = "Watermark width and height must both be positive or both be zero.";
+ Assert.Throws<ArgumentException>(
+ () => image.AddImageWatermark(watermark, width: 0, height: 22), message);
+
+ Assert.Throws<ArgumentException>(
+ () => image.AddImageWatermark(watermark, width: 100, height: 0), message);
+ }
+
+ [Fact]
+ public void AddImageWatermarkThrowsWhenOpacityIsIncorrect()
+ {
+ WebImage watermark = new WebImage(_BmpImageBytes);
+ WebImage image = new WebImage(_JpgImageBytes);
+
+ Assert.ThrowsArgumentOutOfRange(() => image.AddImageWatermark(watermark, opacity: -1), "opacity", "Value must be between 0 and 100.");
+
+ Assert.ThrowsArgumentOutOfRange(() => image.AddImageWatermark(watermark, opacity: 120), "opacity", "Value must be between 0 and 100.");
+ }
+
+ [Fact]
+ public void AddImageWatermarkThrowsOnNegativeDimensions()
+ {
+ WebImage watermark = new WebImage(_BmpImageBytes);
+ WebImage image = new WebImage(_JpgImageBytes);
+
+ Assert.ThrowsArgumentGreaterThanOrEqualTo(
+ () => image.AddImageWatermark(watermark, width: -1),
+ "width",
+ "0");
+
+ Assert.ThrowsArgumentGreaterThanOrEqualTo(
+ () => image.AddImageWatermark(watermark, height: -1),
+ "height",
+ "0");
+ }
+
+ [Fact]
+ public void AddImageWatermarkThrowsOnIncorrectHorizontalAlignment()
+ {
+ WebImage watermark = new WebImage(_BmpImageBytes);
+ WebImage image = new WebImage(_JpgImageBytes);
+
+ Assert.Throws<ArgumentException>(
+ () => image.AddImageWatermark(watermark, horizontalAlign: "horizontal"),
+ "The \"horizontalAlign\" value is invalid. Valid values are: \"Right\", \"Left\", and \"Center\".");
+ }
+
+ [Fact]
+ public void AddImageWatermarkThrowsOnIncorrectVerticalAlignment()
+ {
+ WebImage watermark = new WebImage(_BmpImageBytes);
+ WebImage image = new WebImage(_JpgImageBytes);
+
+ Assert.Throws<ArgumentException>(
+ () => image.AddImageWatermark(watermark, verticalAlign: "vertical"),
+ "The \"verticalAlign\" value is invalid. Valid values are: \"Top\", \"Bottom\", and \"Middle\".");
+ }
+
+ [Fact]
+ public void AddImageWatermarkThrowsOnNegativePadding()
+ {
+ WebImage watermark = new WebImage(_BmpImageBytes);
+ WebImage image = new WebImage(_JpgImageBytes);
+
+ Assert.ThrowsArgumentGreaterThanOrEqualTo(
+ () => image.AddImageWatermark(watermark, padding: -10),
+ "padding",
+ "0");
+ }
+
+ [Fact]
+ public void AddImageWatermarkDoesNotChangeImageIfWatermarkIsTooBig()
+ {
+ WebImage watermark = new WebImage(_JpgImageBytes);
+ WebImage image = new WebImage(_BmpImageBytes);
+ byte[] originalBytes = image.GetBytes("jpg");
+
+ // This will use original watermark image dimensions which is bigger than the target image.
+ image.AddImageWatermark(watermark);
+ byte[] watermarkedBytes = image.GetBytes("jpg");
+
+ Assert.Equal(originalBytes, watermarkedBytes);
+ }
+
+ [Fact]
+ public void AddImageWatermarkWithFileNameThrowsExceptionWhenWatermarkDirectoryDoesNotExist()
+ {
+ var context = GetContext();
+ WebImage image = new WebImage(_BmpImageBytes);
+
+ Assert.Throws<DirectoryNotFoundException>(
+ () => image.AddImageWatermark(context, s => { throw new DirectoryNotFoundException(); }, @"x:\path\does\not\exist", width: 0, height: 0, horizontalAlign: "Right", verticalAlign: "Bottom", opacity: 100, padding: 5));
+ }
+
+ [Fact]
+ public void AddImageWatermarkWithFileNameThrowsExceptionWhenWatermarkFileDoesNotExist()
+ {
+ var context = GetContext();
+ WebImage image = new WebImage(_BmpImageBytes);
+ Assert.Throws<FileNotFoundException>(
+ () => image.AddImageWatermark(context, s => { throw new FileNotFoundException(); }, @"x:\there-is-no-file.jpg", width: 0, height: 0, horizontalAlign: "Right", verticalAlign: "Bottom", opacity: 100, padding: 5));
+ }
+
+ [Fact]
+ public void AddImageWatermarkWithFileNameThrowsExceptionWhenWatermarkFilePathIsNull()
+ {
+ var context = GetContext();
+
+ WebImage image = new WebImage(_BmpImageBytes);
+ Assert.ThrowsArgument(
+ () => image.AddImageWatermark(context, s => _JpgImageBytes, watermarkImageFilePath: null, width: 0, height: 0, horizontalAlign: "Right", verticalAlign: "Bottom", opacity: 100, padding: 5),
+ "filePath",
+ "Value cannot be null or an empty string.");
+ }
+
+ [Fact]
+ public void AddImageWatermarkWithFileNameThrowsExceptionWhenWatermarkFilePathIsEmpty()
+ {
+ var context = GetContext();
+ WebImage image = new WebImage(_BmpImageBytes);
+ Assert.ThrowsArgument(
+ () => image.AddImageWatermark(context, s => _JpgImageBytes, watermarkImageFilePath: null, width: 0, height: 0, horizontalAlign: "Right", verticalAlign: "Bottom", opacity: 100, padding: 5),
+ "filePath",
+ "Value cannot be null or an empty string.");
+ }
+
+ [Fact]
+ public void CanAddImageWatermarkWithFileName()
+ {
+ // Arrange
+ var context = GetContext();
+ WebImage image = new WebImage(_BmpImageBytes);
+ WebImage watermark = new WebImage(_JpgImageBytes);
+
+ // Act
+ var watermarkedWithImageArgument = image.AddImageWatermark(watermark).GetBytes();
+ var watermarkedWithFilePathArgument = image.AddImageWatermark(context, (name) => _JpgImageBytes, @"x:\jpegimage.jpg", width: 0, height: 0, horizontalAlign: "Right", verticalAlign: "Bottom", opacity: 100, padding: 5).GetBytes();
+
+ Assert.Equal(watermarkedWithImageArgument, watermarkedWithFilePathArgument);
+ }
+
+ [Fact]
+ public void SaveOverwritesExistingFile()
+ {
+ Action<string, byte[]> saveAction = (path, content) => { };
+
+ WebImage image = new WebImage(_BmpImageBytes);
+ string newFileName = @"x:\newImage.bmp";
+
+ image.Save(GetContext(), saveAction, newFileName, imageFormat: null, forceWellKnownExtension: true);
+
+ image.RotateLeft();
+ // just verify this does not throw
+ image.Save(GetContext(), saveAction, newFileName, imageFormat: null, forceWellKnownExtension: true);
+ }
+
+ [Fact]
+ public void SaveThrowsWhenPathIsNull()
+ {
+ Action<string, byte[]> saveAction = (path, content) => { };
+
+ // this constructor will not set path
+ byte[] originalContent = _BmpImageBytes;
+ WebImage image = new WebImage(originalContent);
+
+ Assert.ThrowsArgumentNullOrEmptyString(
+ () => image.Save(GetContext(), saveAction, filePath: null, imageFormat: null, forceWellKnownExtension: true),
+ "filePath");
+ }
+
+ [Fact]
+ public void SaveThrowsWhenPathIsEmpty()
+ {
+ Action<string, byte[]> saveAction = (path, content) => { };
+ WebImage image = new WebImage(_BmpImageBytes);
+
+ Assert.ThrowsArgumentNullOrEmptyString(
+ () => image.Save(GetContext(), saveAction, filePath: String.Empty, imageFormat: null, forceWellKnownExtension: true),
+ "filePath");
+ }
+
+ [Fact]
+ public void SaveUsesOriginalFormatWhenNoFormatIsSpecified()
+ {
+ // Arrange
+ // Use rooted path so we by pass using HttpContext
+ var specifiedOutputFile = @"C:\some-dir\foo.jpg";
+ string actualOutputFile = null;
+ Action<string, byte[]> saveAction = (fileName, content) => { actualOutputFile = fileName; };
+
+ // Act
+ WebImage image = new WebImage(_PngImageBytes);
+ image.Save(GetContext(), saveAction, filePath: specifiedOutputFile, imageFormat: null, forceWellKnownExtension: true);
+
+ // Assert
+ Assert.Equal(Path.GetExtension(actualOutputFile), ".png");
+ }
+
+ [Fact]
+ public void SaveUsesOriginalFormatForStreamsWhenNoFormatIsSpecified()
+ {
+ // Arrange
+ // Use rooted path so we by pass using HttpContext
+ var specifiedOutputFile = @"x:\some-dir\foo.jpg";
+ string actualOutputFile = null;
+ Action<string, byte[]> saveAction = (fileName, content) => { actualOutputFile = fileName; };
+
+ // Act
+ WebImage image = new WebImage(_PngImageBytes);
+ image.Save(GetContext(), saveAction, filePath: specifiedOutputFile, imageFormat: null, forceWellKnownExtension: true);
+
+ // Assert
+ Assert.Equal(Path.GetExtension(actualOutputFile), ".png");
+ }
+
+ [Fact]
+ public void SaveSetsExtensionBasedOnFormatWhenForceExtensionIsSet()
+ {
+ // Arrange
+ // Use rooted path so we by pass using HttpContext
+ var specifiedOutputFile = @"x:\some-dir\foo.exe";
+ string actualOutputFile = null;
+ Action<string, byte[]> saveAction = (fileName, content) => { actualOutputFile = fileName; };
+
+ // Act
+ WebImage image = new WebImage(_BmpImageBytes);
+ image.Save(GetContext(), saveAction, filePath: specifiedOutputFile, imageFormat: "jpg", forceWellKnownExtension: true);
+
+ // Assert
+ Assert.Equal(".jpeg", Path.GetExtension(actualOutputFile));
+ Assert.Equal(specifiedOutputFile + ".jpeg", actualOutputFile);
+ }
+
+ [Fact]
+ public void SaveAppendsExtensionBasedOnFormatWhenForceExtensionIsSet()
+ {
+ // Arrange
+ // Use rooted path so we by pass using HttpContext
+ var specifiedOutputFile = @"x:\some-dir\foo";
+ string actualOutputFile = null;
+ Action<string, byte[]> saveAction = (fileName, content) => { actualOutputFile = fileName; };
+
+ // Act
+ WebImage image = new WebImage(_BmpImageBytes);
+ image.Save(GetContext(), saveAction, filePath: specifiedOutputFile, imageFormat: "jpg", forceWellKnownExtension: true);
+
+ // Assert
+ Assert.Equal(".jpeg", Path.GetExtension(actualOutputFile));
+ }
+
+ [Fact]
+ public void SaveDoesNotModifyExtensionWhenExtensionIsCorrect()
+ {
+ // Arrange
+ // Use rooted path so we by pass using HttpContext
+ var specifiedOutputFile = @"x:\some-dir\foo.jpg";
+ string actualOutputFile = null;
+ Action<string, byte[]> saveAction = (fileName, content) => { actualOutputFile = fileName; };
+
+ // Act
+ WebImage image = new WebImage(_BmpImageBytes);
+ image.Save(GetContext(), saveAction, filePath: specifiedOutputFile, imageFormat: "jpg", forceWellKnownExtension: true);
+
+ // Assert
+ Assert.Equal(specifiedOutputFile, actualOutputFile);
+ }
+
+ [Fact]
+ public void SaveDoesNotModifyExtensionWhenForceCorrectExtensionRenameIsCleared()
+ {
+ // Arrange
+ // Use rooted path so we by pass using HttpContext
+ var specifiedOutputFile = @"x:\some-dir\foo.exe";
+ string actualOutputFile = null;
+ Action<string, byte[]> saveAction = (fileName, content) => { actualOutputFile = fileName; };
+
+ // Act
+ WebImage image = new WebImage(_BmpImageBytes);
+ image.Save(GetContext(), saveAction, filePath: specifiedOutputFile, imageFormat: "jpg", forceWellKnownExtension: false);
+
+ // Assert
+ Assert.Equal(specifiedOutputFile, actualOutputFile);
+ }
+
+ [Fact]
+ public void ImageFormatIsSavedCorrectly()
+ {
+ WebImage image = new WebImage(_BmpImageBytes);
+ Assert.Equal("bmp", image.ImageFormat);
+ }
+
+ [Fact]
+ public void SaveUsesInitialFormatWhenNoFormatIsSpecified()
+ {
+ // Arrange
+ string savePath = @"x:\some-dir\image.png";
+ MemoryStream stream = null;
+ Action<string, byte[]> saveAction = (path, content) => { stream = new MemoryStream(content); };
+ var image = new WebImage(_PngImageBytes);
+
+ // Act
+ image.FlipVertical().FlipHorizontal();
+
+ // Assert
+ image.Save(GetContext(), saveAction, savePath, imageFormat: null, forceWellKnownExtension: true);
+
+ using (Image savedImage = Image.FromStream(stream))
+ {
+ Assert.Equal(savedImage.RawFormat, ImageFormat.Png);
+ }
+ }
+
+ [Fact]
+ public void ImageFormatIsParsedCorrectly()
+ {
+ WebImage image = new WebImage(_BmpImageBytes);
+ Assert.Equal("bmp", image.ImageFormat);
+ }
+
+ private static HttpContextBase GetContext()
+ {
+ var httpContext = new Mock<HttpContextBase>();
+ var httpRequest = new Mock<HttpRequestBase>();
+ httpRequest.Setup(c => c.MapPath(It.IsAny<string>())).Returns((string path) => path);
+ httpContext.Setup(c => c.Request).Returns(httpRequest.Object);
+
+ return httpContext.Object;
+ }
+
+ // Test stream that pretends it can't seek.
+ private class TestStream : Stream
+ {
+ private MemoryStream _memoryStream;
+
+ public TestStream(MemoryStream memoryStream)
+ {
+ _memoryStream = memoryStream;
+ }
+
+ public override bool CanRead
+ {
+ get { return _memoryStream.CanRead; }
+ }
+
+ public override bool CanSeek
+ {
+ get { return false; }
+ }
+
+ public override bool CanWrite
+ {
+ get { return _memoryStream.CanWrite; }
+ }
+
+ public override void Flush()
+ {
+ _memoryStream.Flush();
+ }
+
+ public override long Length
+ {
+ get { throw new NotSupportedException(); }
+ }
+
+ public override long Position
+ {
+ get { return _memoryStream.Position; }
+ set { _memoryStream.Position = value; }
+ }
+
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ return _memoryStream.Read(buffer, offset, count);
+ }
+
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ throw new NotSupportedException();
+ }
+
+ public override void SetLength(long value)
+ {
+ _memoryStream.SetLength(value);
+ }
+
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ _memoryStream.Write(buffer, offset, count);
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Helpers.Test/WebMailTest.cs b/test/System.Web.Helpers.Test/WebMailTest.cs
new file mode 100644
index 00000000..fa3eb5b4
--- /dev/null
+++ b/test/System.Web.Helpers.Test/WebMailTest.cs
@@ -0,0 +1,378 @@
+using System.IO;
+using System.Linq;
+using System.Net.Mail;
+using System.Text;
+using System.Web.WebPages.Scope;
+using System.Web.WebPages.TestUtils;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Helpers.Test
+{
+ public class WebMailTest
+ {
+ const string FromAddress = "abc@123.com";
+ const string Server = "myserver.com";
+ const int Port = 100;
+ const string UserName = "My UserName";
+ const string Password = "My Password";
+
+ [Fact]
+ public void WebMailSmtpServerTests()
+ {
+ // All tests prior to setting smtp server go here
+ // Verify Send throws if no SmtpServer is set
+ Assert.Throws<InvalidOperationException>(
+ () => WebMail.Send(to: "test@test.com", subject: "test", body: "test body"),
+ "\"SmtpServer\" was not specified."
+ );
+
+ // Verify SmtpServer uses scope storage.
+ // Arrange
+ var value = "value";
+
+ // Act
+ WebMail.SmtpServer = value;
+
+ // Assert
+ Assert.Equal(WebMail.SmtpServer, value);
+ Assert.Equal(ScopeStorage.CurrentScope[WebMail.SmtpServerKey], value);
+ }
+
+ [Fact]
+ public void WebMailSendThrowsIfPriorityIsInvalid()
+ {
+ Assert.ThrowsArgument(
+ () => WebMail.Send(to: "test@test.com", subject: "test", body: "test body", priority: "foo"),
+ "priority",
+ "The \"priority\" value is invalid. Valid values are \"Low\", \"Normal\" and \"High\"."
+ );
+ }
+
+ [Fact]
+ public void WebMailUsesScopeStorageForSmtpPort()
+ {
+ // Arrange
+ var value = 4;
+
+ // Act
+ WebMail.SmtpPort = value;
+
+ // Assert
+ Assert.Equal(WebMail.SmtpPort, value);
+ Assert.Equal(ScopeStorage.CurrentScope[WebMail.SmtpPortKey], value);
+ }
+
+ [Fact]
+ public void WebMailUsesScopeStorageForEnableSsl()
+ {
+ // Arrange
+ var value = true;
+
+ // Act
+ WebMail.EnableSsl = value;
+
+ // Assert
+ Assert.Equal(WebMail.EnableSsl, value);
+ Assert.Equal(ScopeStorage.CurrentScope[WebMail.EnableSslKey], value);
+ }
+
+ [Fact]
+ public void WebMailUsesScopeStorageForDefaultCredentials()
+ {
+ // Arrange
+ var value = true;
+
+ // Act
+ WebMail.SmtpUseDefaultCredentials = value;
+
+ // Assert
+ Assert.Equal(WebMail.SmtpUseDefaultCredentials, value);
+ Assert.Equal(ScopeStorage.CurrentScope[WebMail.SmtpUseDefaultCredentialsKey], value);
+ }
+
+ [Fact]
+ public void WebMailUsesScopeStorageForUserName()
+ {
+ // Arrange
+ var value = "value";
+
+ // Act
+ WebMail.UserName = value;
+
+ // Assert
+ Assert.Equal(WebMail.UserName, value);
+ Assert.Equal(ScopeStorage.CurrentScope[WebMail.UserNameKey], value);
+ }
+
+ [Fact]
+ public void WebMailUsesScopeStorageForPassword()
+ {
+ // Arrange
+ var value = "value";
+
+ // Act
+ WebMail.Password = value;
+
+ // Assert
+ Assert.Equal(WebMail.Password, value);
+ Assert.Equal(ScopeStorage.CurrentScope[WebMail.PasswordKey], value);
+ }
+
+ [Fact]
+ public void WebMailUsesScopeStorageForFrom()
+ {
+ // Arrange
+ var value = "value";
+
+ // Act
+ WebMail.From = value;
+
+ // Assert
+ Assert.Equal(WebMail.From, value);
+ Assert.Equal(ScopeStorage.CurrentScope[WebMail.FromKey], value);
+ }
+
+ [Fact]
+ public void WebMailThrowsWhenSmtpServerValueIsNullOrEmpty()
+ {
+ // Act and Assert
+ Assert.ThrowsArgumentNullOrEmptyString(() => WebMail.SmtpServer = null, "SmtpServer");
+ Assert.ThrowsArgumentNullOrEmptyString(() => WebMail.SmtpServer = String.Empty, "SmtpServer");
+ }
+
+ [Fact]
+ public void ParseHeaderParsesStringInKeyValueFormat()
+ {
+ // Arrange
+ string header = "foo: bar";
+
+ // Act
+ string key, value;
+
+ // Assert
+ Assert.True(WebMail.TryParseHeader(header, out key, out value));
+ Assert.Equal("foo", key);
+ Assert.Equal("bar", value);
+ }
+
+ [Fact]
+ public void ParseHeaderReturnsFalseIfHeaderIsNotInCorrectFormat()
+ {
+ // Arrange
+ string header = "foo bar";
+
+ // Act
+ string key, value;
+
+ // Assert
+ Assert.False(WebMail.TryParseHeader(header, out key, out value));
+ Assert.Null(key);
+ Assert.Null(value);
+ }
+
+ [Fact]
+ public void SetPropertiesOnMessageTest_SetsAllInfoCorrectlyOnMailMessageTest()
+ {
+ // Arrange
+ MailMessage message = new MailMessage();
+ string to = "abc123@xyz.com";
+ string subject = "subject1";
+ string body = "body1";
+ string from = FromAddress;
+ string cc = "cc@xyz.com";
+ string attachmentName = "HiRes.jpg";
+ string bcc = "foo@bar.com";
+ string replyTo = "x@y.com,z@pqr.com";
+ string contentEncoding = "utf-8";
+ string headerEncoding = "utf-16";
+ var priority = MailPriority.Low;
+
+ // Act
+ string fileToAttach = Path.GetTempFileName();
+
+ try
+ {
+ TestFile.Create(attachmentName).Save(fileToAttach);
+ bool isBodyHtml = true;
+ var additionalHeaders = new[] { "header1:value1" };
+ WebMail.SetPropertiesOnMessage(message, to, subject, body, from, cc, bcc, replyTo, contentEncoding, headerEncoding, priority, new[] { fileToAttach }, isBodyHtml, additionalHeaders);
+
+ // Assert
+ Assert.Equal(body, message.Body);
+ Assert.Equal(subject, message.Subject);
+ Assert.Equal(to, message.To[0].Address);
+ Assert.Equal(cc, message.CC[0].Address);
+ Assert.Equal(from, message.From.Address);
+ Assert.Equal(bcc, message.Bcc[0].Address);
+ Assert.Equal("x@y.com", message.ReplyToList[0].Address);
+ Assert.Equal("z@pqr.com", message.ReplyToList[1].Address);
+ Assert.Equal(MailPriority.Low, message.Priority);
+ Assert.Equal(Encoding.UTF8, message.BodyEncoding);
+ Assert.Equal(Encoding.Unicode, message.HeadersEncoding);
+
+ Assert.True(message.Headers.AllKeys.Contains("header1"));
+ Assert.True(message.Attachments.Count == 1);
+ }
+ finally
+ {
+ try
+ {
+ File.Delete(fileToAttach);
+ }
+ catch (IOException)
+ {
+ } // Try our best to clean up after ourselves
+ }
+ }
+
+ [Fact]
+ public void MailSendWithNullInCollection_ThrowsArgumentException()
+ {
+ Assert.Throws<ArgumentException>(
+ () => WebMail.Send("foo@bar.com", "sub", "body", filesToAttach: new string[] { "c:\\foo.txt", null }),
+ "A string in the collection is null or empty.\r\nParameter name: filesToAttach"
+ );
+
+ Assert.Throws<ArgumentException>(
+ () => WebMail.Send("foo@bar.com", "sub", "body", additionalHeaders: new string[] { "foo:bar", null }),
+ "A string in the collection is null or empty.\r\nParameter name: additionalHeaders"
+ );
+ }
+
+ [Fact]
+ public void AssignHeaderValuesIgnoresMalformedHeaders()
+ {
+ // Arrange
+ var message = new MailMessage();
+ var headers = new[] { "foo1:bar1", "foo2", "foo3|bar3", "foo4 bar4" };
+
+ // Act
+ WebMail.AssignHeaderValues(message, headers);
+
+ // Assert
+ Assert.Equal(1, message.Headers.Count);
+ Assert.Equal("foo1", message.Headers.AllKeys[0]);
+ Assert.Equal("bar1", message.Headers[0]);
+ }
+
+ [Fact]
+ public void PropertiesDuplicatedAcrossHeaderAndArgumentDoesNotThrow()
+ {
+ // Arrange
+ var message = new MailMessage();
+ var headers = new[] { "to:to@test.com" };
+
+ // Act
+ WebMail.SetPropertiesOnMessage(message, "to@test.com", null, null, "from@test.com", null, null, null, null, null, MailPriority.Normal, null, false, headers);
+
+ // Assert
+ Assert.Equal(2, message.To.Count);
+ Assert.Equal("to@test.com", message.To.First().Address);
+ Assert.Equal("to@test.com", message.To.Last().Address);
+ }
+
+ [Fact]
+ public void AssignHeaderValuesSetsPropertiesForKnownHeaderValues()
+ {
+ // Arrange
+ var message = new MailMessage();
+ var headers = new[]
+ {
+ "cc:cc@test.com", "bcc:bcc@test.com,bcc2@test.com", "from:from@test.com", "priority:high", "reply-to:replyto1@test.com,replyto2@test.com",
+ "sender: sender@test.com", "to:to@test.com"
+ };
+
+ // Act
+ WebMail.AssignHeaderValues(message, headers);
+
+ // Assert
+ Assert.Equal("cc@test.com", message.CC.Single().Address);
+ Assert.Equal("bcc@test.com", message.Bcc.First().Address);
+ Assert.Equal("bcc2@test.com", message.Bcc.Last().Address);
+ Assert.Equal("from@test.com", message.From.Address);
+ Assert.Equal(MailPriority.High, message.Priority);
+ Assert.Equal("replyto1@test.com", message.ReplyToList.First().Address);
+ Assert.Equal("replyto2@test.com", message.ReplyToList.Last().Address);
+ Assert.Equal("sender@test.com", message.Sender.Address);
+ Assert.Equal("to@test.com", message.To.Single().Address);
+
+ // Assert we transparently set header values
+ Assert.Equal(headers.Count(), message.Headers.Count);
+ }
+
+ [Fact]
+ public void AssignHeaderDoesNotThrowIfPriorityValueIsInvalid()
+ {
+ // Arrange
+ var message = new MailMessage();
+ var headers = new[] { "priority:invalid-value" };
+
+ // Act
+ WebMail.AssignHeaderValues(message, headers);
+
+ // Assert
+ Assert.Equal(MailPriority.Normal, message.Priority);
+
+ // Assert we transparently set header values
+ Assert.Equal(1, message.Headers.Count);
+ Assert.Equal("Priority", message.Headers.Keys[0]);
+ Assert.Equal("invalid-value", message.Headers["Priority"]);
+ }
+
+ [Fact]
+ public void AssignHeaderDoesNotThrowIfMailAddressIsInvalid()
+ {
+ // Arrange
+ var message = new MailMessage();
+ var headers = new[] { "to:not-#-email@@" };
+
+ // Act
+ WebMail.AssignHeaderValues(message, headers);
+
+ // Assert
+ Assert.Equal(0, message.To.Count);
+
+ // Assert we transparently set header values
+ Assert.Equal(1, message.Headers.Count);
+ Assert.Equal("To", message.Headers.Keys[0]);
+ Assert.Equal("not-#-email@@", message.Headers["To"]);
+ }
+
+ [Fact]
+ public void AssignHeaderDoesNotThrowIfKnownHeaderValuesAreEmptyOrMalformed()
+ {
+ // Arrange
+ var message = new MailMessage();
+ var headers = new[] { "to:", ":reply-to", "priority:false" };
+
+ // Act
+ WebMail.AssignHeaderValues(message, headers);
+
+ // Assert
+ Assert.Equal(0, message.To.Count);
+
+ // Assert we transparently set header values
+ Assert.Equal(1, message.Headers.Count);
+ Assert.Equal("Priority", message.Headers.Keys[0]);
+ Assert.Equal("false", message.Headers["Priority"]);
+ }
+
+ [Fact]
+ public void ArgumentsToSendTakePriorityOverHeader()
+ {
+ // Arrange
+ var message = new MailMessage();
+ var headers = new[] { "from:header-from@test.com", "cc:header-cc@test.com", "priority:low" };
+
+ // Act
+ WebMail.SetPropertiesOnMessage(message, null, null, null, "direct-from@test.com", "direct-cc@test.com", null, null, null, null, MailPriority.High, null, false, headers);
+
+ // Assert
+ Assert.Equal("direct-from@test.com", message.From.Address);
+ Assert.Equal("header-cc@test.com", message.CC.First().Address);
+ Assert.Equal("direct-cc@test.com", message.CC.Last().Address);
+ Assert.Equal(MailPriority.High, message.Priority);
+ }
+ }
+}
diff --git a/test/System.Web.Helpers.Test/XhtmlAssert.cs b/test/System.Web.Helpers.Test/XhtmlAssert.cs
new file mode 100644
index 00000000..d7a2879e
--- /dev/null
+++ b/test/System.Web.Helpers.Test/XhtmlAssert.cs
@@ -0,0 +1,128 @@
+using System.IO;
+using System.Net;
+using System.Reflection;
+using System.Text.RegularExpressions;
+using System.Web.WebPages;
+using System.Xml;
+using System.Xml.Resolvers;
+using Xunit;
+
+namespace System.Web.Helpers.Test
+{
+ // see: http://msdn.microsoft.com/en-us/library/hdf992b8(v=VS.100).aspx
+ // see: http://blogs.msdn.com/xmlteam/archive/2008/08/14/introducing-the-xmlpreloadedresolver.aspx
+ public class XhtmlAssert
+ {
+ const string Xhtml10Wrapper = "<html xmlns=\"http://www.w3.org/1999/xhtml\"><head></head><body>{0}</body></html>";
+ const string DOCTYPE_XHTML1_1 = "<!DOCTYPE {0} PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" \"xhtml11-flat.dtd\">\r\n";
+
+ public static void Validate1_0(object result, bool addRoot = false)
+ {
+ string html = null;
+ if (addRoot)
+ {
+ html = String.Format(Xhtml10Wrapper, GetHtml(result));
+ }
+ else
+ {
+ html = GetHtml(result);
+ }
+
+ Validate1_0(html);
+ }
+
+ public static void Validate1_1(object result, string wrapper = null)
+ {
+ string root;
+ string html = GetHtml(result);
+ if (String.IsNullOrEmpty(wrapper))
+ {
+ root = GetRoot(html);
+ }
+ else
+ {
+ root = wrapper;
+ html = String.Format("<{0}>{1}</{0}>", wrapper, html);
+ }
+ Validate1_1(root, html);
+ }
+
+ private static string GetHtml(object result)
+ {
+ Assert.True((result is IHtmlString) || (result is HelperResult), "Helpers should return IHTMLString or HelperResult");
+ return result.ToString();
+ }
+
+ private static string GetRoot(string html)
+ {
+ Regex regex = new Regex(@"<(\w+)[\s>]");
+ Match match = regex.Match(html);
+ Assert.True(match.Success, "Could not determine root element");
+ Assert.True(match.Groups.Count > 1, "Could not determine root element");
+ return match.Groups[1].Value;
+ }
+
+ private static void Validate1_0(string html)
+ {
+ XmlReaderSettings settings = new XmlReaderSettings();
+ settings.DtdProcessing = DtdProcessing.Parse;
+ settings.XmlResolver = new XmlPreloadedResolver(XmlKnownDtds.Xhtml10);
+
+ Validate(settings, html);
+ }
+
+ private static void Validate1_1(string root, string html)
+ {
+ var settings = new XmlReaderSettings { DtdProcessing = DtdProcessing.Parse, ValidationType = ValidationType.DTD, XmlResolver = new AssemblyResourceXmlResolver() };
+
+ string docType = String.Format(DOCTYPE_XHTML1_1, root);
+ Validate(settings, docType + html);
+ }
+
+ private static void Validate(XmlReaderSettings settings, string html)
+ {
+ using (StringReader sr = new StringReader(html))
+ {
+ using (XmlReader reader = XmlReader.Create(sr, settings))
+ {
+ while (reader.Read())
+ {
+ // XHTML element and attribute names must be lowercase, since XML is case sensitive.
+ // The W3C validator detects this, but we must manually check since the XmlReader does not.
+ // See: http://www.w3.org/TR/xhtml1/#h-4.2
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ string element = reader.Name;
+ Assert.True(element == element.ToLowerInvariant());
+ if (reader.HasAttributes)
+ {
+ for (int i = 0; i < reader.AttributeCount; i++)
+ {
+ reader.MoveToAttribute(i);
+ string attribute = reader.Name;
+ Assert.True(attribute == attribute.ToLowerInvariant());
+ }
+ // move back to element node
+ reader.MoveToElement();
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private class AssemblyResourceXmlResolver : XmlResolver
+ {
+ public override ICredentials Credentials
+ {
+ set { throw new NotSupportedException(); }
+ }
+
+ public override object GetEntity(Uri absoluteUri, string role, Type ofObjectToReturn)
+ {
+ Assembly assembly = typeof(XhtmlAssert).Assembly;
+ return assembly.GetManifestResourceStream("System.Web.Helpers.Test.TestFiles.xhtml11-flat.dtd");
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Helpers.Test/packages.config b/test/System.Web.Helpers.Test/packages.config
new file mode 100644
index 00000000..d5aa6401
--- /dev/null
+++ b/test/System.Web.Helpers.Test/packages.config
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="Moq" version="4.0.10827" />
+ <package id="xunit" version="1.9.0.1566" />
+ <package id="xunit.extensions" version="1.9.0.1566" />
+</packages> \ No newline at end of file
diff --git a/test/System.Web.Http.Common.Test/ErrorTests.cs b/test/System.Web.Http.Common.Test/ErrorTests.cs
new file mode 100644
index 00000000..09c187fb
--- /dev/null
+++ b/test/System.Web.Http.Common.Test/ErrorTests.cs
@@ -0,0 +1,21 @@
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Common
+{
+ public class ErrorTests
+ {
+ [Fact]
+ public void Format()
+ {
+ // Arrange
+ string expected = "The formatted message";
+
+ // Act
+ string actual = Error.Format("The {0} message", "formatted");
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Common.Test/HttpRequestMessageCommonExtensionsTest.cs b/test/System.Web.Http.Common.Test/HttpRequestMessageCommonExtensionsTest.cs
new file mode 100644
index 00000000..63c36412
--- /dev/null
+++ b/test/System.Web.Http.Common.Test/HttpRequestMessageCommonExtensionsTest.cs
@@ -0,0 +1,57 @@
+using System.Net;
+using System.Net.Http;
+using Microsoft.TestCommon;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http
+{
+ public class HttpRequestMessageCommonExtensionsTest
+ {
+ [Fact]
+ public void IsCorrectType()
+ {
+ Assert.Type.HasProperties(typeof(HttpRequestMessageCommonExtensions), TypeAssert.TypeProperties.IsStatic | TypeAssert.TypeProperties.IsPublicVisibleClass);
+ }
+
+ [Fact]
+ public void CreateResponseThrowsOnNull()
+ {
+ Assert.ThrowsArgumentNull(() => HttpRequestMessageCommonExtensions.CreateResponse(null), "request");
+ }
+
+ [Fact]
+ public void CreateResponseWithStatusThrowsOnNull()
+ {
+ Assert.ThrowsArgumentNull(() => HttpRequestMessageCommonExtensions.CreateResponse(null, HttpStatusCode.OK), "request");
+ }
+
+ [Fact]
+ public void CreateResponse()
+ {
+ // Arrange
+ HttpRequestMessage request = new HttpRequestMessage();
+
+ // Act
+ HttpResponseMessage response = request.CreateResponse();
+
+ // Assert
+ Assert.Same(request, response.RequestMessage);
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ }
+
+ [Fact]
+ public void CreateResponseWithStatus()
+ {
+ // Arrange
+ HttpRequestMessage request = new HttpRequestMessage();
+
+ // Act
+ HttpResponseMessage response = request.CreateResponse(HttpStatusCode.NotImplemented);
+
+ // Assert
+ Assert.Same(request, response.RequestMessage);
+ Assert.Equal(HttpStatusCode.NotImplemented, response.StatusCode);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Common.Test/System.Web.Http.Common.Test.csproj b/test/System.Web.Http.Common.Test/System.Web.Http.Common.Test.csproj
new file mode 100644
index 00000000..54cf1980
--- /dev/null
+++ b/test/System.Web.Http.Common.Test/System.Web.Http.Common.Test.csproj
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProductVersion>9.0.30729</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{7FB5C0C0-5223-4C79-A8DA-D2A0F264A478}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>System.Web.Http.Common</RootNamespace>
+ <AssemblyName>System.Web.Http.Common.Test</AssemblyName>
+ <FileAlignment>512</FileAlignment>
+ <ProjectTypeGuids>{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Debug\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Release\Test\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'CodeCoverage' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\CodeCoverage\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="Moq, Version=4.0.10827.0, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL">
+ <HintPath>..\..\packages\Moq.4.0.10827\lib\NET40\Moq.dll</HintPath>
+ </Reference>
+ <Reference Include="System" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.Net.Http">
+ <HintPath>..\..\packages\Microsoft.Net.Http.2.0.20302.1\lib\net40\System.Net.Http.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Net.Http.WebRequest">
+ <HintPath>..\..\packages\Microsoft.Net.Http.2.0.20302.1\lib\net40\System.Net.Http.WebRequest.dll</HintPath>
+ </Reference>
+ <Reference Include="xunit">
+ <HintPath>..\..\packages\xunit.1.9.0.1566\lib\xunit.dll</HintPath>
+ </Reference>
+ <Reference Include="xunit.extensions">
+ <HintPath>..\..\packages\xunit.extensions.1.9.0.1566\lib\xunit.extensions.dll</HintPath>
+ </Reference>
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="ErrorTests.cs" />
+ <Compile Include="HttpRequestMessageCommonExtensionsTest.cs" />
+ <Compile Include="TaskHelpersExtensionsTest.cs" />
+ <Compile Include="TaskHelpersTest.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\System.Web.Http.Common\System.Web.Http.Common.csproj">
+ <Project>{03A5E5F2-2E23-48F2-ABCC-6C41BAC9AC02}</Project>
+ <Name>System.Web.Http.Common</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\Microsoft.TestCommon\Microsoft.TestCommon.csproj">
+ <Project>{FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0}</Project>
+ <Name>Microsoft.TestCommon</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="packages.config" />
+ </ItemGroup>
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+</Project> \ No newline at end of file
diff --git a/test/System.Web.Http.Common.Test/TaskHelpersExtensionsTest.cs b/test/System.Web.Http.Common.Test/TaskHelpersExtensionsTest.cs
new file mode 100644
index 00000000..d788a1cd
--- /dev/null
+++ b/test/System.Web.Http.Common.Test/TaskHelpersExtensionsTest.cs
@@ -0,0 +1,2169 @@
+using Microsoft.TestCommon;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+// There are several tests which need unreachable code (return after throw) to guarantee the correct lambda signature
+#pragma warning disable 0162
+
+namespace System.Threading.Tasks
+{
+ public class TaskHelpersExtensionsTest
+ {
+ // -----------------------------------------------------------------
+ // Task.Catch(Func<Exception, Task>)
+
+ [Fact]
+ public Task Catch_NoInputValue_CatchesException_Handled()
+ {
+ // Arrange
+ return TaskHelpers.FromError(new InvalidOperationException())
+
+ // Act
+ .Catch(ex =>
+ {
+ Assert.NotNull(ex);
+ Assert.IsType<InvalidOperationException>(ex);
+ return TaskHelpers.Completed();
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(TaskStatus.RanToCompletion, task.Status);
+ });
+ }
+
+ [Fact]
+ public Task Catch_NoInputValue_CatchesException_Rethrow()
+ {
+ // Arrange
+ return TaskHelpers.FromError(new InvalidOperationException())
+
+ // Act
+ .Catch(ex =>
+ {
+ return TaskHelpers.FromError(ex);
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(TaskStatus.Faulted, task.Status);
+ Assert.IsType<InvalidOperationException>(task.Exception.GetBaseException());
+ });
+ }
+
+ [Fact]
+ public Task Catch_NoInputValue_ReturningNullFromCatchIsProhibited()
+ {
+ // Arrange
+ return TaskHelpers.FromError(new Exception())
+
+ // Act
+ .Catch(ex =>
+ {
+ return null;
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(TaskStatus.Faulted, task.Status);
+ Assert.IsException<InvalidOperationException>(task.Exception, "You cannot return null from the TaskHelpersExtensions.Catch continuation. You must return a valid task or throw an exception.");
+ });
+ }
+
+ [Fact, PreserveSyncContext]
+ public Task Catch_NoInputValue_CompletedTaskOfSuccess_DoesNotRunContinuationAndDoesNotSwitchContexts()
+ {
+ // Arrange
+ bool ranContinuation = false;
+ var syncContext = new Mock<SynchronizationContext> { CallBase = true };
+ SynchronizationContext.SetSynchronizationContext(syncContext.Object);
+
+ return TaskHelpers.Completed()
+
+ // Act
+ .Catch(ex =>
+ {
+ ranContinuation = true;
+ return TaskHelpers.Completed();
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.False(ranContinuation);
+ syncContext.Verify(sc => sc.Post(It.IsAny<SendOrPostCallback>(), null), Times.Never());
+ });
+ }
+
+ [Fact, PreserveSyncContext]
+ public Task Catch_NoInputValue_CompletedTaskOfCancellation_DoesNotRunContinuationAndDoesNotSwitchContexts()
+ {
+ // Arrange
+ bool ranContinuation = false;
+ var syncContext = new Mock<SynchronizationContext> { CallBase = true };
+ SynchronizationContext.SetSynchronizationContext(syncContext.Object);
+
+ return TaskHelpers.Canceled()
+
+ // Act
+ .Catch(ex =>
+ {
+ ranContinuation = true;
+ return TaskHelpers.Completed();
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.False(ranContinuation);
+ syncContext.Verify(sc => sc.Post(It.IsAny<SendOrPostCallback>(), null), Times.Never());
+ });
+ }
+
+ [Fact, PreserveSyncContext]
+ public Task Catch_NoInputValue_CompletedTaskOfFault_RunsOnSameThreadAndDoesNotPostToSynchronizationContext()
+ {
+ // Arrange
+ int outerThreadId = Thread.CurrentThread.ManagedThreadId;
+ int innerThreadId = Int32.MinValue;
+ Exception thrownException = new Exception();
+ Exception caughtException = null;
+ var syncContext = new Mock<SynchronizationContext> { CallBase = true };
+ SynchronizationContext.SetSynchronizationContext(syncContext.Object);
+
+ return TaskHelpers.FromError(thrownException)
+
+ // Act
+ .Catch(ex =>
+ {
+ caughtException = ex;
+ innerThreadId = Thread.CurrentThread.ManagedThreadId;
+ return TaskHelpers.Completed();
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Same(thrownException, caughtException);
+ Assert.Equal(innerThreadId, outerThreadId);
+ syncContext.Verify(sc => sc.Post(It.IsAny<SendOrPostCallback>(), null), Times.Never());
+ });
+ }
+
+ [Fact, PreserveSyncContext]
+ public Task Catch_NoInputValue_IncompleteTaskOfSuccess_DoesNotRunContinuationAndDoesNotSwitchContexts()
+ {
+ // Arrange
+ bool ranContinuation = false;
+ var syncContext = new Mock<SynchronizationContext> { CallBase = true };
+ SynchronizationContext.SetSynchronizationContext(syncContext.Object);
+
+ Task incompleteTask = new Task(() => { });
+
+ // Act
+ Task resultTask = incompleteTask.Catch(ex =>
+ {
+ ranContinuation = true;
+ return TaskHelpers.Completed();
+ });
+
+ // Assert
+ incompleteTask.Start();
+
+ return resultTask.ContinueWith(task =>
+ {
+ Assert.False(ranContinuation);
+ syncContext.Verify(sc => sc.Post(It.IsAny<SendOrPostCallback>(), null), Times.Never());
+ });
+ }
+
+ [Fact, PreserveSyncContext]
+ public Task Catch_NoInputValue_IncompleteTaskOfCancellation_DoesNotRunContinuationAndDoesNotSwitchContexts()
+ {
+ // Arrange
+ bool ranContinuation = false;
+ var syncContext = new Mock<SynchronizationContext> { CallBase = true };
+ SynchronizationContext.SetSynchronizationContext(syncContext.Object);
+
+ Task incompleteTask = new Task(() => { });
+ Task resultTask = incompleteTask.ContinueWith(task => TaskHelpers.Canceled()).Unwrap();
+
+ // Act
+ resultTask = resultTask.Catch(ex =>
+ {
+ ranContinuation = true;
+ return TaskHelpers.Completed();
+ });
+
+ // Assert
+ incompleteTask.Start();
+
+ return resultTask.ContinueWith(task =>
+ {
+ Assert.False(ranContinuation);
+ syncContext.Verify(sc => sc.Post(It.IsAny<SendOrPostCallback>(), null), Times.Never());
+ });
+ }
+
+ [Fact, PreserveSyncContext]
+ public Task Catch_NoInputValue_IncompleteTaskOfFault_RunsOnNewThreadAndPostsToSynchronizationContext()
+ {
+ // Arrange
+ int outerThreadId = Thread.CurrentThread.ManagedThreadId;
+ int innerThreadId = Int32.MinValue;
+ Exception thrownException = new Exception();
+ Exception caughtException = null;
+ var syncContext = new Mock<SynchronizationContext> { CallBase = true };
+ SynchronizationContext.SetSynchronizationContext(syncContext.Object);
+
+ Task incompleteTask = new Task(() => { throw thrownException; });
+
+ // Act
+ Task resultTask = incompleteTask.Catch(ex =>
+ {
+ caughtException = ex;
+ innerThreadId = Thread.CurrentThread.ManagedThreadId;
+ return TaskHelpers.Completed();
+ });
+
+ // Assert
+ incompleteTask.Start();
+
+ return resultTask.ContinueWith(task =>
+ {
+ Assert.Same(thrownException, caughtException);
+ Assert.NotEqual(innerThreadId, outerThreadId);
+ syncContext.Verify(sc => sc.Post(It.IsAny<SendOrPostCallback>(), null), Times.Once());
+ });
+ }
+
+ // -----------------------------------------------------------------
+ // Task<T>.Catch(Func<Exception, Task<T>>)
+
+ [Fact]
+ public Task Catch_WithInputValue_CatchesException_Handled()
+ {
+ // Arrange
+ return TaskHelpers.FromError<int>(new InvalidOperationException())
+
+ // Act
+ .Catch(ex =>
+ {
+ Assert.NotNull(ex);
+ Assert.IsType<InvalidOperationException>(ex);
+ return TaskHelpers.FromResult(42);
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(TaskStatus.RanToCompletion, task.Status);
+ });
+ }
+
+ [Fact]
+ public Task Catch_WithInputValue_CatchesException_Rethrow()
+ {
+ // Arrange
+ return TaskHelpers.FromError<int>(new InvalidOperationException())
+
+ // Act
+ .Catch(ex =>
+ {
+ return TaskHelpers.FromError<int>(ex);
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(TaskStatus.Faulted, task.Status);
+ Assert.IsType<InvalidOperationException>(task.Exception.GetBaseException());
+ });
+ }
+
+ [Fact]
+ public Task Catch_WithInputValue_ReturningNullFromCatchIsProhibited()
+ {
+ // Arrange
+ return TaskHelpers.FromError<int>(new Exception())
+
+ // Act
+ .Catch(ex =>
+ {
+ return null;
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(TaskStatus.Faulted, task.Status);
+ Assert.IsException<InvalidOperationException>(task.Exception, "You cannot return null from the TaskHelpersExtensions.Catch continuation. You must return a valid task or throw an exception.");
+ });
+ }
+
+ [Fact, PreserveSyncContext]
+ public Task Catch_WithInputValue_CompletedTaskOfSuccess_DoesNotRunContinuationAndDoesNotSwitchContexts()
+ {
+ // Arrange
+ bool ranContinuation = false;
+ var syncContext = new Mock<SynchronizationContext> { CallBase = true };
+ SynchronizationContext.SetSynchronizationContext(syncContext.Object);
+
+ return TaskHelpers.FromResult(21)
+
+ // Act
+ .Catch(ex =>
+ {
+ ranContinuation = true;
+ return TaskHelpers.FromResult(42);
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.False(ranContinuation);
+ syncContext.Verify(sc => sc.Post(It.IsAny<SendOrPostCallback>(), null), Times.Never());
+ });
+ }
+
+ [Fact, PreserveSyncContext]
+ public Task Catch_WithInputValue_CompletedTaskOfCancellation_DoesNotRunContinuationAndDoesNotSwitchContexts()
+ {
+ // Arrange
+ bool ranContinuation = false;
+ var syncContext = new Mock<SynchronizationContext> { CallBase = true };
+ SynchronizationContext.SetSynchronizationContext(syncContext.Object);
+
+ return TaskHelpers.Canceled<int>()
+
+ // Act
+ .Catch(ex =>
+ {
+ ranContinuation = true;
+ return TaskHelpers.FromResult(42);
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.False(ranContinuation);
+ syncContext.Verify(sc => sc.Post(It.IsAny<SendOrPostCallback>(), null), Times.Never());
+ });
+ }
+
+ [Fact, PreserveSyncContext]
+ public Task Catch_WithInputValue_CompletedTaskOfFault_RunsOnSameThreadAndDoesNotPostToSynchronizationContext()
+ {
+ // Arrange
+ int outerThreadId = Thread.CurrentThread.ManagedThreadId;
+ int innerThreadId = Int32.MinValue;
+ Exception thrownException = new Exception();
+ Exception caughtException = null;
+ var syncContext = new Mock<SynchronizationContext> { CallBase = true };
+ SynchronizationContext.SetSynchronizationContext(syncContext.Object);
+
+ return TaskHelpers.FromError<int>(thrownException)
+
+ // Act
+ .Catch(ex =>
+ {
+ caughtException = ex;
+ innerThreadId = Thread.CurrentThread.ManagedThreadId;
+ return TaskHelpers.FromResult(42);
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Same(thrownException, caughtException);
+ Assert.Equal(innerThreadId, outerThreadId);
+ syncContext.Verify(sc => sc.Post(It.IsAny<SendOrPostCallback>(), null), Times.Never());
+ });
+ }
+
+ [Fact, PreserveSyncContext]
+ public Task Catch_WithInputValue_IncompleteTaskOfSuccess_DoesNotRunContinuationAndDoesNotSwitchContexts()
+ {
+ // Arrange
+ bool ranContinuation = false;
+ var syncContext = new Mock<SynchronizationContext> { CallBase = true };
+ SynchronizationContext.SetSynchronizationContext(syncContext.Object);
+
+ Task<int> incompleteTask = new Task<int>(() => 42);
+
+ // Act
+ Task resultTask = incompleteTask.Catch(ex =>
+ {
+ ranContinuation = true;
+ return TaskHelpers.Completed();
+ });
+
+ // Assert
+ incompleteTask.Start();
+
+ return resultTask.ContinueWith(task =>
+ {
+ Assert.False(ranContinuation);
+ syncContext.Verify(sc => sc.Post(It.IsAny<SendOrPostCallback>(), null), Times.Never());
+ });
+ }
+
+ [Fact, PreserveSyncContext]
+ public Task Catch_WithInputValue_IncompleteTaskOfCancellation_DoesNotRunContinuationAndDoesNotSwitchContexts()
+ {
+ // Arrange
+ bool ranContinuation = false;
+ var syncContext = new Mock<SynchronizationContext> { CallBase = true };
+ SynchronizationContext.SetSynchronizationContext(syncContext.Object);
+
+ Task<int> incompleteTask = new Task<int>(() => 42);
+ Task resultTask = incompleteTask.ContinueWith(task => TaskHelpers.Canceled<int>()).Unwrap();
+
+ // Act
+ resultTask = resultTask.Catch(ex =>
+ {
+ ranContinuation = true;
+ return TaskHelpers.Completed();
+ });
+
+ // Assert
+ incompleteTask.Start();
+
+ return resultTask.ContinueWith(task =>
+ {
+ Assert.False(ranContinuation);
+ syncContext.Verify(sc => sc.Post(It.IsAny<SendOrPostCallback>(), null), Times.Never());
+ });
+ }
+
+ [Fact, PreserveSyncContext]
+ public Task Catch_WithInputValue_IncompleteTaskOfFault_RunsOnNewThreadAndPostsToSynchronizationContext()
+ {
+ // Arrange
+ int outerThreadId = Thread.CurrentThread.ManagedThreadId;
+ int innerThreadId = Int32.MinValue;
+ Exception thrownException = new Exception();
+ Exception caughtException = null;
+ var syncContext = new Mock<SynchronizationContext> { CallBase = true };
+ SynchronizationContext.SetSynchronizationContext(syncContext.Object);
+
+ Task<int> incompleteTask = new Task<int>(() => { throw thrownException; });
+
+ // Act
+ Task resultTask = incompleteTask.Catch(ex =>
+ {
+ caughtException = ex;
+ innerThreadId = Thread.CurrentThread.ManagedThreadId;
+ return TaskHelpers.FromResult(42);
+ });
+
+ // Assert
+ incompleteTask.Start();
+
+ return resultTask.ContinueWith(task =>
+ {
+ Assert.Same(thrownException, caughtException);
+ Assert.NotEqual(innerThreadId, outerThreadId);
+ syncContext.Verify(sc => sc.Post(It.IsAny<SendOrPostCallback>(), null), Times.Once());
+ });
+ }
+
+ // -----------------------------------------------------------------
+ // Task.CopyResultToCompletionSource(Task)
+
+ [Fact]
+ public Task CopyResultToCompletionSource_NoInputValue_SuccessfulTask()
+ {
+ // Arrange
+ var tcs = new TaskCompletionSource<object>();
+ var expectedResult = new object();
+
+ return TaskHelpers.Completed()
+
+ // Act
+ .CopyResultToCompletionSource(tcs, expectedResult)
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(TaskStatus.RanToCompletion, task.Status); // Outer task always runs to completion
+ Assert.Equal(TaskStatus.RanToCompletion, tcs.Task.Status);
+ Assert.Same(expectedResult, tcs.Task.Result);
+ });
+ }
+
+ [Fact]
+ public Task CopyResultToCompletionSource_NoInputValue_FaultedTask()
+ {
+ // Arrange
+ var tcs = new TaskCompletionSource<object>();
+ var expectedException = new NotImplementedException();
+
+ return TaskHelpers.FromError(expectedException)
+
+ // Act
+ .CopyResultToCompletionSource(tcs, completionResult: null)
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(TaskStatus.RanToCompletion, task.Status); // Outer task always runs to completion
+ Assert.Equal(TaskStatus.Faulted, tcs.Task.Status);
+ Assert.Same(expectedException, tcs.Task.Exception.GetBaseException());
+ });
+ }
+
+ [Fact]
+ public Task CopyResultToCompletionSource_NoInputValue_Canceled()
+ {
+ // Arrange
+ var tcs = new TaskCompletionSource<object>();
+
+ return TaskHelpers.Canceled()
+
+ // Act
+ .CopyResultToCompletionSource(tcs, completionResult: null)
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(TaskStatus.RanToCompletion, task.Status); // Outer task always runs to completion
+ Assert.Equal(TaskStatus.Canceled, tcs.Task.Status);
+ });
+ }
+
+ // -----------------------------------------------------------------
+ // Task.CopyResultToCompletionSource(Task<T>)
+
+ [Fact]
+ public Task CopyResultToCompletionSource_WithInputValue_SuccessfulTask()
+ {
+ // Arrange
+ var tcs = new TaskCompletionSource<int>();
+
+ return TaskHelpers.FromResult(42)
+
+ // Act
+ .CopyResultToCompletionSource(tcs)
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(TaskStatus.RanToCompletion, task.Status); // Outer task always runs to completion
+ Assert.Equal(TaskStatus.RanToCompletion, tcs.Task.Status);
+ Assert.Equal(42, tcs.Task.Result);
+ });
+ }
+
+ [Fact]
+ public Task CopyResultToCompletionSource_WithInputValue_FaultedTask()
+ {
+ // Arrange
+ var tcs = new TaskCompletionSource<int>();
+ var expectedException = new NotImplementedException();
+
+ return TaskHelpers.FromError<int>(expectedException)
+
+ // Act
+ .CopyResultToCompletionSource(tcs)
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(TaskStatus.RanToCompletion, task.Status); // Outer task always runs to completion
+ Assert.Equal(TaskStatus.Faulted, tcs.Task.Status);
+ Assert.Same(expectedException, tcs.Task.Exception.GetBaseException());
+ });
+ }
+
+ [Fact]
+ public Task CopyResultToCompletionSource_WithInputValue_Canceled()
+ {
+ // Arrange
+ var tcs = new TaskCompletionSource<int>();
+
+ return TaskHelpers.Canceled<int>()
+
+ // Act
+ .CopyResultToCompletionSource(tcs)
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(TaskStatus.RanToCompletion, task.Status); // Outer task always runs to completion
+ Assert.Equal(TaskStatus.Canceled, tcs.Task.Status);
+ });
+ }
+
+ // -----------------------------------------------------------------
+ // Task.Finally(Action)
+
+ [Fact, PreserveSyncContext]
+ public Task Finally_NoInputValue_CompletedTaskOfSuccess_RunsOnSameThreadAndDoesNotPostToSynchronizationContext()
+ {
+ // Arrange
+ int originalThreadId = Thread.CurrentThread.ManagedThreadId;
+ int callbackThreadId = Int32.MinValue;
+ var syncContext = new Mock<SynchronizationContext> { CallBase = true };
+ SynchronizationContext.SetSynchronizationContext(syncContext.Object);
+
+ return TaskHelpers.Completed()
+
+ // Act
+ .Finally(() =>
+ {
+ callbackThreadId = Thread.CurrentThread.ManagedThreadId;
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(originalThreadId, callbackThreadId);
+ syncContext.Verify(sc => sc.Post(It.IsAny<SendOrPostCallback>(), null), Times.Never());
+ });
+ }
+
+ [Fact, PreserveSyncContext]
+ public Task Finally_NoInputValue_CompletedTaskOfCancellation_RunsOnSameThreadAndDoesNotPostToSynchronizationContext()
+ {
+ // Arrange
+ int originalThreadId = Thread.CurrentThread.ManagedThreadId;
+ int callbackThreadId = Int32.MinValue;
+ var syncContext = new Mock<SynchronizationContext> { CallBase = true };
+ SynchronizationContext.SetSynchronizationContext(syncContext.Object);
+
+ return TaskHelpers.Canceled()
+
+ // Act
+ .Finally(() =>
+ {
+ callbackThreadId = Thread.CurrentThread.ManagedThreadId;
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(originalThreadId, callbackThreadId);
+ syncContext.Verify(sc => sc.Post(It.IsAny<SendOrPostCallback>(), null), Times.Never());
+ });
+ }
+
+ [Fact, PreserveSyncContext]
+ public Task Finally_NoInputValue_CompletedTaskOfFault_RunsOnSameThreadAndDoesNotPostToSynchronizationContext()
+ {
+ // Arrange
+ int originalThreadId = Thread.CurrentThread.ManagedThreadId;
+ int callbackThreadId = Int32.MinValue;
+ var syncContext = new Mock<SynchronizationContext> { CallBase = true };
+ SynchronizationContext.SetSynchronizationContext(syncContext.Object);
+
+ return TaskHelpers.FromError(new InvalidOperationException())
+
+ // Act
+ .Finally(() =>
+ {
+ callbackThreadId = Thread.CurrentThread.ManagedThreadId;
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ var ex = task.Exception; // Observe the exception
+ Assert.Equal(originalThreadId, callbackThreadId);
+ syncContext.Verify(sc => sc.Post(It.IsAny<SendOrPostCallback>(), null), Times.Never());
+ });
+ }
+
+ [Fact, PreserveSyncContext]
+ public Task Finally_NoInputValue_IncompleteTaskOfSuccess_RunsOnNewThreadAndPostsContinuationToSynchronizationContext()
+ {
+ // Arrange
+ int originalThreadId = Thread.CurrentThread.ManagedThreadId;
+ int callbackThreadId = Int32.MinValue;
+ var syncContext = new Mock<SynchronizationContext> { CallBase = true };
+ SynchronizationContext.SetSynchronizationContext(syncContext.Object);
+
+ Task incompleteTask = new Task(() => { });
+
+ // Act
+ Task resultTask = incompleteTask.Finally(() =>
+ {
+ callbackThreadId = Thread.CurrentThread.ManagedThreadId;
+ });
+
+ // Assert
+ incompleteTask.Start();
+
+ return resultTask.ContinueWith(task =>
+ {
+ Assert.NotEqual(originalThreadId, callbackThreadId);
+ syncContext.Verify(sc => sc.Post(It.IsAny<SendOrPostCallback>(), null), Times.Once());
+ });
+ }
+
+ [Fact, PreserveSyncContext]
+ public Task Finally_NoInputValue_IncompleteTaskOfCancellation_RunsOnNewThreadAndPostsContinuationToSynchronizationContext()
+ {
+ // Arrange
+ int originalThreadId = Thread.CurrentThread.ManagedThreadId;
+ int callbackThreadId = Int32.MinValue;
+ var syncContext = new Mock<SynchronizationContext> { CallBase = true };
+ SynchronizationContext.SetSynchronizationContext(syncContext.Object);
+
+ Task incompleteTask = new Task(() => { });
+ Task resultTask = incompleteTask.ContinueWith(task => TaskHelpers.Canceled()).Unwrap();
+
+ // Act
+ resultTask = resultTask.Finally(() =>
+ {
+ callbackThreadId = Thread.CurrentThread.ManagedThreadId;
+ });
+
+ // Assert
+ incompleteTask.Start();
+
+ return resultTask.ContinueWith(task =>
+ {
+ Assert.NotEqual(originalThreadId, callbackThreadId);
+ syncContext.Verify(sc => sc.Post(It.IsAny<SendOrPostCallback>(), null), Times.Once());
+ });
+ }
+
+ [Fact, PreserveSyncContext]
+ public Task Finally_NoInputValue_IncompleteTaskOfFault_RunsOnNewThreadAndPostsContinuationToSynchronizationContext()
+ {
+ // Arrange
+ int originalThreadId = Thread.CurrentThread.ManagedThreadId;
+ int callbackThreadId = Int32.MinValue;
+ var syncContext = new Mock<SynchronizationContext> { CallBase = true };
+ SynchronizationContext.SetSynchronizationContext(syncContext.Object);
+
+ Task incompleteTask = new Task(() => { throw new InvalidOperationException(); });
+
+ // Act
+ Task resultTask = incompleteTask.Finally(() =>
+ {
+ callbackThreadId = Thread.CurrentThread.ManagedThreadId;
+ });
+
+ // Assert
+ incompleteTask.Start();
+
+ return resultTask.ContinueWith(task =>
+ {
+ var ex = task.Exception; // Observe the exception
+ Assert.NotEqual(originalThreadId, callbackThreadId);
+ syncContext.Verify(sc => sc.Post(It.IsAny<SendOrPostCallback>(), null), Times.Once());
+ });
+ }
+
+ // -----------------------------------------------------------------
+ // Task<T>.Finally(Action)
+
+ [Fact, PreserveSyncContext]
+ public Task Finally_WithInputValue_CompletedTaskOfSuccess_RunsOnSameThreadAndDoesNotPostToSynchronizationContext()
+ {
+ // Arrange
+ int originalThreadId = Thread.CurrentThread.ManagedThreadId;
+ int callbackThreadId = Int32.MinValue;
+ var syncContext = new Mock<SynchronizationContext> { CallBase = true };
+ SynchronizationContext.SetSynchronizationContext(syncContext.Object);
+
+ return TaskHelpers.FromResult(21)
+
+ // Act
+ .Finally(() =>
+ {
+ callbackThreadId = Thread.CurrentThread.ManagedThreadId;
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(21, task.Result);
+ Assert.Equal(originalThreadId, callbackThreadId);
+ syncContext.Verify(sc => sc.Post(It.IsAny<SendOrPostCallback>(), null), Times.Never());
+ });
+ }
+
+ [Fact, PreserveSyncContext]
+ public Task Finally_WithInputValue_CompletedTaskOfCancellation_RunsOnSameThreadAndDoesNotPostToSynchronizationContext()
+ {
+ // Arrange
+ int originalThreadId = Thread.CurrentThread.ManagedThreadId;
+ int callbackThreadId = Int32.MinValue;
+ var syncContext = new Mock<SynchronizationContext> { CallBase = true };
+ SynchronizationContext.SetSynchronizationContext(syncContext.Object);
+
+ return TaskHelpers.Canceled<int>()
+
+ // Act
+ .Finally(() =>
+ {
+ callbackThreadId = Thread.CurrentThread.ManagedThreadId;
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(originalThreadId, callbackThreadId);
+ syncContext.Verify(sc => sc.Post(It.IsAny<SendOrPostCallback>(), null), Times.Never());
+ });
+ }
+
+ [Fact, PreserveSyncContext]
+ public Task Finally_WithInputValue_CompletedTaskOfFault_RunsOnSameThreadAndDoesNotPostToSynchronizationContext()
+ {
+ // Arrange
+ int originalThreadId = Thread.CurrentThread.ManagedThreadId;
+ int callbackThreadId = Int32.MinValue;
+ var syncContext = new Mock<SynchronizationContext> { CallBase = true };
+ SynchronizationContext.SetSynchronizationContext(syncContext.Object);
+
+ return TaskHelpers.FromError<int>(new InvalidOperationException())
+
+ // Act
+ .Finally(() =>
+ {
+ callbackThreadId = Thread.CurrentThread.ManagedThreadId;
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ var ex = task.Exception; // Observe the exception
+ Assert.Equal(originalThreadId, callbackThreadId);
+ syncContext.Verify(sc => sc.Post(It.IsAny<SendOrPostCallback>(), null), Times.Never());
+ });
+ }
+
+ [Fact, PreserveSyncContext]
+ public Task Finally_WithInputValue_IncompleteTaskOfSuccess_RunsOnNewThreadAndPostsContinuationToSynchronizationContext()
+ {
+ // Arrange
+ int originalThreadId = Thread.CurrentThread.ManagedThreadId;
+ int callbackThreadId = Int32.MinValue;
+ var syncContext = new Mock<SynchronizationContext> { CallBase = true };
+ SynchronizationContext.SetSynchronizationContext(syncContext.Object);
+
+ Task incompleteTask = new Task<int>(() => 21);
+
+ // Act
+ Task resultTask = incompleteTask.Finally(() =>
+ {
+ callbackThreadId = Thread.CurrentThread.ManagedThreadId;
+ });
+
+ // Assert
+ incompleteTask.Start();
+
+ return resultTask.ContinueWith(task =>
+ {
+ Assert.NotEqual(originalThreadId, callbackThreadId);
+ syncContext.Verify(sc => sc.Post(It.IsAny<SendOrPostCallback>(), null), Times.Once());
+ });
+ }
+
+ [Fact, PreserveSyncContext]
+ public Task Finally_WithInputValue_IncompleteTaskOfCancellation_RunsOnNewThreadAndPostsContinuationToSynchronizationContext()
+ {
+ // Arrange
+ int originalThreadId = Thread.CurrentThread.ManagedThreadId;
+ int callbackThreadId = Int32.MinValue;
+ var syncContext = new Mock<SynchronizationContext> { CallBase = true };
+ SynchronizationContext.SetSynchronizationContext(syncContext.Object);
+
+ Task<int> incompleteTask = new Task<int>(() => 42);
+ Task resultTask = incompleteTask.ContinueWith(task => TaskHelpers.Canceled<int>()).Unwrap();
+
+ // Act
+ resultTask = resultTask.Finally(() =>
+ {
+ callbackThreadId = Thread.CurrentThread.ManagedThreadId;
+ });
+
+ // Assert
+ incompleteTask.Start();
+
+ return resultTask.ContinueWith(task =>
+ {
+ Assert.NotEqual(originalThreadId, callbackThreadId);
+ syncContext.Verify(sc => sc.Post(It.IsAny<SendOrPostCallback>(), null), Times.Once());
+ });
+ }
+
+ [Fact, PreserveSyncContext]
+ public Task Finally_WithInputValue_IncompleteTaskOfFault_RunsOnNewThreadAndPostsContinuationToSynchronizationContext()
+ {
+ // Arrange
+ int originalThreadId = Thread.CurrentThread.ManagedThreadId;
+ int callbackThreadId = Int32.MinValue;
+ var syncContext = new Mock<SynchronizationContext> { CallBase = true };
+ SynchronizationContext.SetSynchronizationContext(syncContext.Object);
+
+ Task<int> incompleteTask = new Task<int>(() => { throw new InvalidOperationException(); });
+
+ // Act
+ Task resultTask = incompleteTask.Finally(() =>
+ {
+ callbackThreadId = Thread.CurrentThread.ManagedThreadId;
+ });
+
+ // Assert
+ incompleteTask.Start();
+
+ return resultTask.ContinueWith(task =>
+ {
+ var ex = task.Exception; // Observe the exception
+ Assert.NotEqual(originalThreadId, callbackThreadId);
+ syncContext.Verify(sc => sc.Post(It.IsAny<SendOrPostCallback>(), null), Times.Once());
+ });
+ }
+
+ // -----------------------------------------------------------------
+ // Task Task.Then(Action)
+
+ [Fact]
+ public Task Then_NoInputValue_NoReturnValue_CallsContinuation()
+ {
+ // Arrange
+ bool ranContinuation = false;
+
+ return TaskHelpers.Completed()
+
+ // Act
+ .Then(() =>
+ {
+ ranContinuation = true;
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(TaskStatus.RanToCompletion, task.Status);
+ Assert.True(ranContinuation);
+ });
+ }
+
+ [Fact]
+ public Task Then_NoInputValue_NoReturnValue_ThrownExceptionIsPropagated()
+ {
+ // Arrange
+ return TaskHelpers.Completed()
+
+ // Act
+ .Then(() =>
+ {
+ throw new NotImplementedException();
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(TaskStatus.Faulted, task.Status);
+ var ex = Assert.Single(task.Exception.InnerExceptions);
+ Assert.IsType<NotImplementedException>(ex);
+ });
+ }
+
+ [Fact]
+ public Task Then_NoInputValue_NoReturnValue_FaultPreventsFurtherThenStatementsFromExecuting()
+ {
+ // Arrange
+ bool ranContinuation = false;
+
+ return TaskHelpers.FromError(new NotImplementedException())
+
+ // Act
+ .Then(() =>
+ {
+ ranContinuation = true;
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ var ex = task.Exception; // Observe the exception
+ Assert.False(ranContinuation);
+ });
+ }
+
+ [Fact]
+ public Task Then_NoInputValue_NoReturnValue_ManualCancellationPreventsFurtherThenStatementsFromExecuting()
+ {
+ // Arrange
+ bool ranContinuation = false;
+
+ return TaskHelpers.Canceled()
+
+ // Act
+ .Then(() =>
+ {
+ ranContinuation = true;
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(TaskStatus.Canceled, task.Status);
+ Assert.False(ranContinuation);
+ });
+ }
+
+ [Fact]
+ public Task Then_NoInputValue_NoReturnValue_TokenCancellationPreventsFurtherThenStatementsFromExecuting()
+ {
+ // Arrange
+ bool ranContinuation = false;
+ CancellationToken cancellationToken = new CancellationToken(canceled: true);
+
+ return TaskHelpers.Completed()
+
+ // Act
+ .Then(() =>
+ {
+ ranContinuation = true;
+ }, cancellationToken)
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(TaskStatus.Canceled, task.Status);
+ Assert.False(ranContinuation);
+ });
+ }
+
+ [Fact, PreserveSyncContext]
+ public Task Then_NoInputValue_NoReturnValue_IncompleteTask_RunsOnNewThreadAndPostsContinuationToSynchronizationContext()
+ {
+ // Arrange
+ int originalThreadId = Thread.CurrentThread.ManagedThreadId;
+ int callbackThreadId = Int32.MinValue;
+ var syncContext = new Mock<SynchronizationContext> { CallBase = true };
+ SynchronizationContext.SetSynchronizationContext(syncContext.Object);
+
+ Task incompleteTask = new Task(() => { });
+
+ // Act
+ Task resultTask = incompleteTask.Then(() =>
+ {
+ callbackThreadId = Thread.CurrentThread.ManagedThreadId;
+ });
+
+ // Assert
+ incompleteTask.Start();
+
+ return resultTask.ContinueWith(task =>
+ {
+ Assert.NotEqual(originalThreadId, callbackThreadId);
+ syncContext.Verify(sc => sc.Post(It.IsAny<SendOrPostCallback>(), null), Times.Once());
+ });
+ }
+
+ [Fact, PreserveSyncContext]
+ public Task Then_NoInputValue_NoReturnValue_CompleteTask_RunsOnSameThreadAndDoesNotPostToSynchronizationContext()
+ {
+ // Arrange
+ int originalThreadId = Thread.CurrentThread.ManagedThreadId;
+ int callbackThreadId = Int32.MinValue;
+ var syncContext = new Mock<SynchronizationContext> { CallBase = true };
+ SynchronizationContext.SetSynchronizationContext(syncContext.Object);
+
+ return TaskHelpers.Completed()
+
+ // Act
+ .Then(() =>
+ {
+ callbackThreadId = Thread.CurrentThread.ManagedThreadId;
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(originalThreadId, callbackThreadId);
+ syncContext.Verify(sc => sc.Post(It.IsAny<SendOrPostCallback>(), null), Times.Never());
+ });
+ }
+
+ // -----------------------------------------------------------------
+ // Task Task.Then(Func<Task>)
+
+ [Fact]
+ public Task Then_NoInputValue_ReturnsTask_CallsContinuation()
+ {
+ // Arrange
+ bool ranContinuation = false;
+
+ return TaskHelpers.Completed()
+
+ // Act
+ .Then(() =>
+ {
+ ranContinuation = true;
+ return TaskHelpers.Completed();
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(TaskStatus.RanToCompletion, task.Status);
+ Assert.True(ranContinuation);
+ });
+ }
+
+ [Fact]
+ public Task Then_NoInputValue_ReturnsTask_ThrownExceptionIsPropagated()
+ {
+ // Arrange
+ return TaskHelpers.Completed()
+
+ // Act
+ .Then(() =>
+ {
+ throw new NotImplementedException();
+ return TaskHelpers.Completed(); // Return-after-throw to guarantee correct lambda signature
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(TaskStatus.Faulted, task.Status);
+ var ex = Assert.Single(task.Exception.InnerExceptions);
+ Assert.IsType<NotImplementedException>(ex);
+ });
+ }
+
+ [Fact]
+ public Task Then_NoInputValue_ReturnsTask_FaultPreventsFurtherThenStatementsFromExecuting()
+ {
+ // Arrange
+ bool ranContinuation = false;
+
+ return TaskHelpers.FromError(new NotImplementedException())
+
+ // Act
+ .Then(() =>
+ {
+ ranContinuation = true;
+ return TaskHelpers.Completed();
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ var ex = task.Exception; // Observe the exception
+ Assert.False(ranContinuation);
+ });
+ }
+
+ [Fact]
+ public Task Then_NoInputValue_ReturnsTask_ManualCancellationPreventsFurtherThenStatementsFromExecuting()
+ {
+ // Arrange
+ bool ranContinuation = false;
+
+ return TaskHelpers.Canceled()
+
+ // Act
+ .Then(() =>
+ {
+ ranContinuation = true;
+ return TaskHelpers.Completed();
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(TaskStatus.Canceled, task.Status);
+ Assert.False(ranContinuation);
+ });
+ }
+
+ [Fact]
+ public Task Then_NoInputValue_ReturnsTask_TokenCancellationPreventsFurtherThenStatementsFromExecuting()
+ {
+ // Arrange
+ bool ranContinuation = false;
+ CancellationToken cancellationToken = new CancellationToken(canceled: true);
+
+ return TaskHelpers.Completed()
+
+ // Act
+ .Then(() =>
+ {
+ ranContinuation = true;
+ return TaskHelpers.Completed();
+ }, cancellationToken)
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(TaskStatus.Canceled, task.Status);
+ Assert.False(ranContinuation);
+ });
+ }
+
+ [Fact, PreserveSyncContext]
+ public Task Then_NoInputValue_ReturnsTask_IncompleteTask_RunsOnNewThreadAndPostsContinuationToSynchronizationContext()
+ {
+ // Arrange
+ int originalThreadId = Thread.CurrentThread.ManagedThreadId;
+ int callbackThreadId = Int32.MinValue;
+ var syncContext = new Mock<SynchronizationContext> { CallBase = true };
+ SynchronizationContext.SetSynchronizationContext(syncContext.Object);
+
+ Task incompleteTask = new Task(() => { });
+
+ // Act
+ Task resultTask = incompleteTask.Then(() =>
+ {
+ callbackThreadId = Thread.CurrentThread.ManagedThreadId;
+ return TaskHelpers.Completed();
+ });
+
+ // Assert
+ incompleteTask.Start();
+
+ return resultTask.ContinueWith(task =>
+ {
+ Assert.NotEqual(originalThreadId, callbackThreadId);
+ syncContext.Verify(sc => sc.Post(It.IsAny<SendOrPostCallback>(), null), Times.Once());
+ });
+ }
+
+ [Fact, PreserveSyncContext]
+ public Task Then_NoInputValue_ReturnsTask_CompleteTask_RunsOnSameThreadAndDoesNotPostToSynchronizationContext()
+ {
+ // Arrange
+ int originalThreadId = Thread.CurrentThread.ManagedThreadId;
+ int callbackThreadId = Int32.MinValue;
+ var syncContext = new Mock<SynchronizationContext> { CallBase = true };
+ SynchronizationContext.SetSynchronizationContext(syncContext.Object);
+
+ return TaskHelpers.Completed()
+
+ // Act
+ .Then(() =>
+ {
+ callbackThreadId = Thread.CurrentThread.ManagedThreadId;
+ return TaskHelpers.Completed();
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(originalThreadId, callbackThreadId);
+ syncContext.Verify(sc => sc.Post(It.IsAny<SendOrPostCallback>(), null), Times.Never());
+ });
+ }
+
+ // -----------------------------------------------------------------
+ // Task<T> Task.Then(Func<T>)
+
+ [Fact]
+ public Task Then_NoInputValue_WithReturnValue_CallsContinuation()
+ {
+ // Arrange
+ return TaskHelpers.Completed()
+
+ // Act
+ .Then(() =>
+ {
+ return 42;
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(TaskStatus.RanToCompletion, task.Status);
+ Assert.Equal(42, task.Result);
+ });
+ }
+
+ [Fact]
+ public Task Then_NoInputValue_WithReturnValue_ThrownExceptionIsPropagated()
+ {
+ // Arrange
+ return TaskHelpers.Completed()
+
+ // Act
+ .Then(() =>
+ {
+ throw new NotImplementedException();
+ return 0; // Return-after-throw to guarantee correct lambda signature
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(TaskStatus.Faulted, task.Status);
+ var ex = Assert.Single(task.Exception.InnerExceptions);
+ Assert.IsType<NotImplementedException>(ex);
+ });
+ }
+
+ [Fact]
+ public Task Then_NoInputValue_WithReturnValue_FaultPreventsFurtherThenStatementsFromExecuting()
+ {
+ // Arrange
+ bool ranContinuation = false;
+
+ return TaskHelpers.FromError(new NotImplementedException())
+
+ // Act
+ .Then(() =>
+ {
+ ranContinuation = true;
+ return 42;
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ var ex = task.Exception; // Observe the exception
+ Assert.False(ranContinuation);
+ });
+ }
+
+ [Fact]
+ public Task Then_NoInputValue_WithReturnValue_ManualCancellationPreventsFurtherThenStatementsFromExecuting()
+ {
+ // Arrange
+ bool ranContinuation = false;
+
+ return TaskHelpers.Canceled()
+
+ // Act
+ .Then(() =>
+ {
+ ranContinuation = true;
+ return 42;
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(TaskStatus.Canceled, task.Status);
+ Assert.False(ranContinuation);
+ });
+ }
+
+ [Fact]
+ public Task Then_NoInputValue_WithReturnValue_TokenCancellationPreventsFurtherThenStatementsFromExecuting()
+ {
+ // Arrange
+ bool ranContinuation = false;
+ CancellationToken cancellationToken = new CancellationToken(canceled: true);
+
+ return TaskHelpers.Completed()
+
+ // Act
+ .Then(() =>
+ {
+ ranContinuation = true;
+ return 42;
+ }, cancellationToken)
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(TaskStatus.Canceled, task.Status);
+ Assert.False(ranContinuation);
+ });
+ }
+
+ [Fact, PreserveSyncContext]
+ public Task Then_NoInputValue_WithReturnValue_IncompleteTask_RunsOnNewThreadAndPostsContinuationToSynchronizationContext()
+ {
+ // Arrange
+ int originalThreadId = Thread.CurrentThread.ManagedThreadId;
+ int callbackThreadId = Int32.MinValue;
+ var syncContext = new Mock<SynchronizationContext> { CallBase = true };
+ SynchronizationContext.SetSynchronizationContext(syncContext.Object);
+
+ Task incompleteTask = new Task(() => { });
+
+ // Act
+ Task resultTask = incompleteTask.Then(() =>
+ {
+ callbackThreadId = Thread.CurrentThread.ManagedThreadId;
+ return 42;
+ });
+
+ // Assert
+ incompleteTask.Start();
+
+ return resultTask.ContinueWith(task =>
+ {
+ Assert.NotEqual(originalThreadId, callbackThreadId);
+ syncContext.Verify(sc => sc.Post(It.IsAny<SendOrPostCallback>(), null), Times.Once());
+ });
+ }
+
+ [Fact, PreserveSyncContext]
+ public Task Then_NoInputValue_WithReturnValue_CompleteTask_RunsOnSameThreadAndDoesNotPostToSynchronizationContext()
+ {
+ // Arrange
+ int originalThreadId = Thread.CurrentThread.ManagedThreadId;
+ int callbackThreadId = Int32.MinValue;
+ var syncContext = new Mock<SynchronizationContext> { CallBase = true };
+ SynchronizationContext.SetSynchronizationContext(syncContext.Object);
+
+ return TaskHelpers.Completed()
+
+ // Act
+ .Then(() =>
+ {
+ callbackThreadId = Thread.CurrentThread.ManagedThreadId;
+ return 42;
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(originalThreadId, callbackThreadId);
+ syncContext.Verify(sc => sc.Post(It.IsAny<SendOrPostCallback>(), null), Times.Never());
+ });
+ }
+
+ // -----------------------------------------------------------------
+ // Task<T> Task.Then(Func<Task<T>>)
+
+ [Fact]
+ public Task Then_NoInputValue_WithTaskReturnValue_CallsContinuation()
+ {
+ // Arrange
+ return TaskHelpers.Completed()
+
+ // Act
+ .Then(() =>
+ {
+ return TaskHelpers.FromResult(42);
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(TaskStatus.RanToCompletion, task.Status);
+ Assert.Equal(42, task.Result);
+ });
+ }
+
+ [Fact]
+ public Task Then_NoInputValue_WithTaskReturnValue_ThrownExceptionIsPropagated()
+ {
+ // Arrange
+ return TaskHelpers.Completed()
+
+ // Act
+ .Then(() =>
+ {
+ throw new NotImplementedException();
+ return TaskHelpers.FromResult(0); // Return-after-throw to guarantee correct lambda signature
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(TaskStatus.Faulted, task.Status);
+ var ex = Assert.Single(task.Exception.InnerExceptions);
+ Assert.IsType<NotImplementedException>(ex);
+ });
+ }
+
+ [Fact]
+ public Task Then_NoInputValue_WithTaskReturnValue_FaultPreventsFurtherThenStatementsFromExecuting()
+ {
+ // Arrange
+ bool ranContinuation = false;
+
+ return TaskHelpers.FromError(new NotImplementedException())
+
+ // Act
+ .Then(() =>
+ {
+ ranContinuation = true;
+ return TaskHelpers.FromResult(42);
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ var ex = task.Exception; // Observe the exception
+ Assert.False(ranContinuation);
+ });
+ }
+
+ [Fact]
+ public Task Then_NoInputValue_WithTaskReturnValue_ManualCancellationPreventsFurtherThenStatementsFromExecuting()
+ {
+ // Arrange
+ bool ranContinuation = false;
+
+ return TaskHelpers.Canceled()
+
+ // Act
+ .Then(() =>
+ {
+ ranContinuation = true;
+ return TaskHelpers.FromResult(42);
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(TaskStatus.Canceled, task.Status);
+ Assert.False(ranContinuation);
+ });
+ }
+
+ [Fact]
+ public Task Then_NoInputValue_WithTaskReturnValue_TokenCancellationPreventsFurtherThenStatementsFromExecuting()
+ {
+ // Arrange
+ bool ranContinuation = false;
+ CancellationToken cancellationToken = new CancellationToken(canceled: true);
+
+ return TaskHelpers.Completed()
+
+ // Act
+ .Then(() =>
+ {
+ ranContinuation = true;
+ return TaskHelpers.FromResult(42);
+ }, cancellationToken)
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(TaskStatus.Canceled, task.Status);
+ Assert.False(ranContinuation);
+ });
+ }
+
+ [Fact, PreserveSyncContext]
+ public Task Then_NoInputValue_WithTaskReturnValue_IncompleteTask_RunsOnNewThreadAndPostsContinuationToSynchronizationContext()
+ {
+ // Arrange
+ int originalThreadId = Thread.CurrentThread.ManagedThreadId;
+ int callbackThreadId = Int32.MinValue;
+ var syncContext = new Mock<SynchronizationContext> { CallBase = true };
+ SynchronizationContext.SetSynchronizationContext(syncContext.Object);
+
+ Task incompleteTask = new Task(() => { });
+
+ // Act
+ Task resultTask = incompleteTask.Then(() =>
+ {
+ callbackThreadId = Thread.CurrentThread.ManagedThreadId;
+ return TaskHelpers.FromResult(42);
+ });
+
+ // Assert
+ incompleteTask.Start();
+
+ return resultTask.ContinueWith(task =>
+ {
+ Assert.NotEqual(originalThreadId, callbackThreadId);
+ syncContext.Verify(sc => sc.Post(It.IsAny<SendOrPostCallback>(), null), Times.Once());
+ });
+ }
+
+ [Fact, PreserveSyncContext]
+ public Task Then_NoInputValue_WithTaskReturnValue_CompleteTask_RunsOnSameThreadAndDoesNotPostToSynchronizationContext()
+ {
+ // Arrange
+ int originalThreadId = Thread.CurrentThread.ManagedThreadId;
+ int callbackThreadId = Int32.MinValue;
+ var syncContext = new Mock<SynchronizationContext> { CallBase = true };
+ SynchronizationContext.SetSynchronizationContext(syncContext.Object);
+
+ return TaskHelpers.Completed()
+
+ // Act
+ .Then(() =>
+ {
+ callbackThreadId = Thread.CurrentThread.ManagedThreadId;
+ return TaskHelpers.FromResult(42);
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(originalThreadId, callbackThreadId);
+ syncContext.Verify(sc => sc.Post(It.IsAny<SendOrPostCallback>(), null), Times.Never());
+ });
+ }
+
+ // -----------------------------------------------------------------
+ // Task Task<T>.Then(Action)
+
+ [Fact]
+ public Task Then_WithInputValue_NoReturnValue_CallsContinuationWithPriorTaskResult()
+ {
+ // Arrange
+ int passedResult = 0;
+
+ return TaskHelpers.FromResult(21)
+
+ // Act
+ .Then(result =>
+ {
+ passedResult = result;
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(TaskStatus.RanToCompletion, task.Status);
+ Assert.Equal(21, passedResult);
+ });
+ }
+
+ [Fact]
+ public Task Then_WithInputValue_NoReturnValue_ThrownExceptionIsPropagated()
+ {
+ // Arrange
+ return TaskHelpers.FromResult(21)
+
+ // Act
+ .Then(result =>
+ {
+ throw new NotImplementedException();
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(TaskStatus.Faulted, task.Status);
+ var ex = Assert.Single(task.Exception.InnerExceptions);
+ Assert.IsType<NotImplementedException>(ex);
+ });
+ }
+
+ [Fact]
+ public Task Then_WithInputValue_NoReturnValue_FaultPreventsFurtherThenStatementsFromExecuting()
+ {
+ // Arrange
+ bool ranContinuation = false;
+
+ return TaskHelpers.FromError<int>(new NotImplementedException())
+
+ // Act
+ .Then(result =>
+ {
+ ranContinuation = true;
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ var ex = task.Exception; // Observe the exception
+ Assert.False(ranContinuation);
+ });
+ }
+
+ [Fact]
+ public Task Then_WithInputValue_NoReturnValue_ManualCancellationPreventsFurtherThenStatementsFromExecuting()
+ {
+ // Arrange
+ bool ranContinuation = false;
+
+ return TaskHelpers.Canceled<int>()
+
+ // Act
+ .Then(result =>
+ {
+ ranContinuation = true;
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(TaskStatus.Canceled, task.Status);
+ Assert.False(ranContinuation);
+ });
+ }
+
+ [Fact]
+ public Task Then_WithInputValue_NoReturnValue_TokenCancellationPreventsFurtherThenStatementsFromExecuting()
+ {
+ // Arrange
+ bool ranContinuation = false;
+ CancellationToken cancellationToken = new CancellationToken(canceled: true);
+
+ return TaskHelpers.FromResult(21)
+
+ // Act
+ .Then(result =>
+ {
+ ranContinuation = true;
+ }, cancellationToken)
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(TaskStatus.Canceled, task.Status);
+ Assert.False(ranContinuation);
+ });
+ }
+
+ [Fact, PreserveSyncContext]
+ public Task Then_WithInputValue_NoReturnValue_IncompleteTask_RunsOnNewThreadAndPostsContinuationToSynchronizationContext()
+ {
+ // Arrange
+ int originalThreadId = Thread.CurrentThread.ManagedThreadId;
+ int callbackThreadId = Int32.MinValue;
+ var syncContext = new Mock<SynchronizationContext> { CallBase = true };
+ SynchronizationContext.SetSynchronizationContext(syncContext.Object);
+
+ Task<int> incompleteTask = new Task<int>(() => 21);
+
+ // Act
+ Task resultTask = incompleteTask.Then(result =>
+ {
+ callbackThreadId = Thread.CurrentThread.ManagedThreadId;
+ });
+
+ // Assert
+ incompleteTask.Start();
+
+ return resultTask.ContinueWith(task =>
+ {
+ Assert.NotEqual(originalThreadId, callbackThreadId);
+ syncContext.Verify(sc => sc.Post(It.IsAny<SendOrPostCallback>(), null), Times.Once());
+ });
+ }
+
+ [Fact, PreserveSyncContext]
+ public Task Then_WithInputValue_NoReturnValue_CompleteTask_RunsOnSameThreadAndDoesNotPostToSynchronizationContext()
+ {
+ // Arrange
+ int originalThreadId = Thread.CurrentThread.ManagedThreadId;
+ int callbackThreadId = Int32.MinValue;
+ var syncContext = new Mock<SynchronizationContext> { CallBase = true };
+ SynchronizationContext.SetSynchronizationContext(syncContext.Object);
+
+ return TaskHelpers.FromResult(21)
+
+ // Act
+ .Then(result =>
+ {
+ callbackThreadId = Thread.CurrentThread.ManagedThreadId;
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(originalThreadId, callbackThreadId);
+ syncContext.Verify(sc => sc.Post(It.IsAny<SendOrPostCallback>(), null), Times.Never());
+ });
+ }
+
+ // -----------------------------------------------------------------
+ // Task<T> Task.Then(Func<T>)
+
+ [Fact]
+ public Task Then_WithInputValue_WithReturnValue_CallsContinuation()
+ {
+ // Arrange
+ return TaskHelpers.FromResult(21)
+
+ // Act
+ .Then(result =>
+ {
+ return 42;
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(TaskStatus.RanToCompletion, task.Status);
+ Assert.Equal(42, task.Result);
+ });
+ }
+
+ [Fact]
+ public Task Then_WithInputValue_WithReturnValue_ThrownExceptionIsPropagated()
+ {
+ // Arrange
+ return TaskHelpers.FromResult(21)
+
+ // Act
+ .Then(result =>
+ {
+ throw new NotImplementedException();
+ return 0; // Return-after-throw to guarantee correct lambda signature
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(TaskStatus.Faulted, task.Status);
+ var ex = Assert.Single(task.Exception.InnerExceptions);
+ Assert.IsType<NotImplementedException>(ex);
+ });
+ }
+
+ [Fact]
+ public Task Then_WithInputValue_WithReturnValue_FaultPreventsFurtherThenStatementsFromExecuting()
+ {
+ // Arrange
+ bool ranContinuation = false;
+
+ return TaskHelpers.FromError<int>(new NotImplementedException())
+
+ // Act
+ .Then(result =>
+ {
+ ranContinuation = true;
+ return 42;
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ var ex = task.Exception; // Observe the exception
+ Assert.False(ranContinuation);
+ });
+ }
+
+ [Fact]
+ public Task Then_WithInputValue_WithReturnValue_ManualCancellationPreventsFurtherThenStatementsFromExecuting()
+ {
+ // Arrange
+ bool ranContinuation = false;
+
+ return TaskHelpers.Canceled<int>()
+
+ // Act
+ .Then(result =>
+ {
+ ranContinuation = true;
+ return 42;
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(TaskStatus.Canceled, task.Status);
+ Assert.False(ranContinuation);
+ });
+ }
+
+ [Fact]
+ public Task Then_WithInputValue_WithReturnValue_TokenCancellationPreventsFurtherThenStatementsFromExecuting()
+ {
+ // Arrange
+ bool ranContinuation = false;
+ CancellationToken cancellationToken = new CancellationToken(canceled: true);
+
+ return TaskHelpers.FromResult(21)
+
+ // Act
+ .Then(result =>
+ {
+ ranContinuation = true;
+ return 42;
+ }, cancellationToken)
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(TaskStatus.Canceled, task.Status);
+ Assert.False(ranContinuation);
+ });
+ }
+
+ [Fact, PreserveSyncContext]
+ public Task Then_WithInputValue_WithReturnValue_IncompleteTask_RunsOnNewThreadAndPostsContinuationToSynchronizationContext()
+ {
+ // Arrange
+ int originalThreadId = Thread.CurrentThread.ManagedThreadId;
+ int callbackThreadId = Int32.MinValue;
+ var syncContext = new Mock<SynchronizationContext> { CallBase = true };
+ SynchronizationContext.SetSynchronizationContext(syncContext.Object);
+
+ Task<int> incompleteTask = new Task<int>(() => 21);
+
+ // Act
+ Task resultTask = incompleteTask.Then(result =>
+ {
+ callbackThreadId = Thread.CurrentThread.ManagedThreadId;
+ return 42;
+ });
+
+ // Assert
+ incompleteTask.Start();
+
+ return resultTask.ContinueWith(task =>
+ {
+ Assert.NotEqual(originalThreadId, callbackThreadId);
+ syncContext.Verify(sc => sc.Post(It.IsAny<SendOrPostCallback>(), null), Times.Once());
+ });
+ }
+
+ [Fact, PreserveSyncContext]
+ public Task Then_WithInputValue_WithReturnValue_CompleteTask_RunsOnSameThreadAndDoesNotPostToSynchronizationContext()
+ {
+ // Arrange
+ int originalThreadId = Thread.CurrentThread.ManagedThreadId;
+ int callbackThreadId = Int32.MinValue;
+ var syncContext = new Mock<SynchronizationContext> { CallBase = true };
+ SynchronizationContext.SetSynchronizationContext(syncContext.Object);
+
+ return TaskHelpers.FromResult(21)
+
+ // Act
+ .Then(result =>
+ {
+ callbackThreadId = Thread.CurrentThread.ManagedThreadId;
+ return 42;
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(originalThreadId, callbackThreadId);
+ syncContext.Verify(sc => sc.Post(It.IsAny<SendOrPostCallback>(), null), Times.Never());
+ });
+ }
+
+ // -----------------------------------------------------------------
+ // Task<T> Task.Then(Func<Task<T>>)
+
+ [Fact]
+ public Task Then_WithInputValue_WithTaskReturnValue_CallsContinuation()
+ {
+ // Arrange
+ return TaskHelpers.FromResult(21)
+
+ // Act
+ .Then(result =>
+ {
+ return TaskHelpers.FromResult(42);
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(TaskStatus.RanToCompletion, task.Status);
+ Assert.Equal(42, task.Result);
+ });
+ }
+
+ [Fact]
+ public Task Then_WithInputValue_WithTaskReturnValue_ThrownExceptionIsPropagated()
+ {
+ // Arrange
+ return TaskHelpers.FromResult(21)
+
+ // Act
+ .Then(result =>
+ {
+ throw new NotImplementedException();
+ return TaskHelpers.FromResult(0); // Return-after-throw to guarantee correct lambda signature
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(TaskStatus.Faulted, task.Status);
+ var ex = Assert.Single(task.Exception.InnerExceptions);
+ Assert.IsType<NotImplementedException>(ex);
+ });
+ }
+
+ [Fact]
+ public Task Then_WithInputValue_WithTaskReturnValue_FaultPreventsFurtherThenStatementsFromExecuting()
+ {
+ // Arrange
+ bool ranContinuation = false;
+
+ return TaskHelpers.FromError<int>(new NotImplementedException())
+
+ // Act
+ .Then(result =>
+ {
+ ranContinuation = true;
+ return TaskHelpers.FromResult(42);
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ var ex = task.Exception; // Observe the exception
+ Assert.False(ranContinuation);
+ });
+ }
+
+ [Fact]
+ public Task Then_WithInputValue_WithTaskReturnValue_ManualCancellationPreventsFurtherThenStatementsFromExecuting()
+ {
+ // Arrange
+ bool ranContinuation = false;
+
+ return TaskHelpers.Canceled<int>()
+
+ // Act
+ .Then(result =>
+ {
+ ranContinuation = true;
+ return TaskHelpers.FromResult(42);
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(TaskStatus.Canceled, task.Status);
+ Assert.False(ranContinuation);
+ });
+ }
+
+ [Fact]
+ public Task Then_WithInputValue_WithTaskReturnValue_TokenCancellationPreventsFurtherThenStatementsFromExecuting()
+ {
+ // Arrange
+ bool ranContinuation = false;
+ CancellationToken cancellationToken = new CancellationToken(canceled: true);
+
+ return TaskHelpers.FromResult(21)
+
+ // Act
+ .Then(result =>
+ {
+ ranContinuation = true;
+ return TaskHelpers.FromResult(42);
+ }, cancellationToken)
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(TaskStatus.Canceled, task.Status);
+ Assert.False(ranContinuation);
+ });
+ }
+
+ [Fact, PreserveSyncContext]
+ public Task Then_WithInputValue_WithTaskReturnValue_IncompleteTask_RunsOnNewThreadAndPostsContinuationToSynchronizationContext()
+ {
+ // Arrange
+ int originalThreadId = Thread.CurrentThread.ManagedThreadId;
+ int callbackThreadId = Int32.MinValue;
+ var syncContext = new Mock<SynchronizationContext> { CallBase = true };
+ SynchronizationContext.SetSynchronizationContext(syncContext.Object);
+
+ Task<int> incompleteTask = new Task<int>(() => 21);
+
+ // Act
+ Task resultTask = incompleteTask.Then(result =>
+ {
+ callbackThreadId = Thread.CurrentThread.ManagedThreadId;
+ return TaskHelpers.FromResult(42);
+ });
+
+ // Assert
+ incompleteTask.Start();
+
+ return resultTask.ContinueWith(task =>
+ {
+ Assert.NotEqual(originalThreadId, callbackThreadId);
+ syncContext.Verify(sc => sc.Post(It.IsAny<SendOrPostCallback>(), null), Times.Once());
+ });
+ }
+
+ [Fact, PreserveSyncContext]
+ public Task Then_WithInputValue_WithTaskReturnValue_CompleteTask_RunsOnSameThreadAndDoesNotPostToSynchronizationContext()
+ {
+ // Arrange
+ int originalThreadId = Thread.CurrentThread.ManagedThreadId;
+ int callbackThreadId = Int32.MinValue;
+ var syncContext = new Mock<SynchronizationContext> { CallBase = true };
+ SynchronizationContext.SetSynchronizationContext(syncContext.Object);
+
+ return TaskHelpers.FromResult(21)
+
+ // Act
+ .Then(result =>
+ {
+ callbackThreadId = Thread.CurrentThread.ManagedThreadId;
+ return TaskHelpers.FromResult(42);
+ })
+
+ // Assert
+ .ContinueWith(task =>
+ {
+ Assert.Equal(originalThreadId, callbackThreadId);
+ syncContext.Verify(sc => sc.Post(It.IsAny<SendOrPostCallback>(), null), Times.Never());
+ });
+ }
+
+ // -----------------------------------------------------------------
+ // bool Task.TryGetResult(Task<TResult>, out TResult)
+
+ [Fact]
+ public void TryGetResult_CompleteTask_ReturnsTrueAndGivesResult()
+ {
+ // Arrange
+ var task = TaskHelpers.FromResult(42);
+
+ // Act
+ int value;
+ bool result = task.TryGetResult(out value);
+
+ // Assert
+ Assert.True(result);
+ Assert.Equal(42, value);
+ }
+
+ [Fact]
+ public void TryGetResult_FaultedTask_ReturnsFalse()
+ {
+ // Arrange
+ var task = TaskHelpers.FromError<int>(new Exception());
+
+ // Act
+ int value;
+ bool result = task.TryGetResult(out value);
+
+ // Assert
+ Assert.False(result);
+ var ex = task.Exception; // Observe the task exception
+ }
+
+ [Fact]
+ public void TryGetResult_CanceledTask_ReturnsFalse()
+ {
+ // Arrange
+ var task = TaskHelpers.Canceled<int>();
+
+ // Act
+ int value;
+ bool result = task.TryGetResult(out value);
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Fact]
+ public Task TryGetResult_IncompleteTask_ReturnsFalse()
+ {
+ // Arrange
+ var incompleteTask = new Task<int>(() => 42);
+
+ // Act
+ int value;
+ bool result = incompleteTask.TryGetResult(out value);
+
+ // Assert
+ Assert.False(result);
+
+ incompleteTask.Start();
+ return incompleteTask; // Make sure the task gets observed
+ }
+ }
+}
diff --git a/test/System.Web.Http.Common.Test/TaskHelpersTest.cs b/test/System.Web.Http.Common.Test/TaskHelpersTest.cs
new file mode 100644
index 00000000..fe6d1d6a
--- /dev/null
+++ b/test/System.Web.Http.Common.Test/TaskHelpersTest.cs
@@ -0,0 +1,574 @@
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.TestCommon;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Threading.Tasks
+{
+ public class TaskHelpersTest
+ {
+ // -----------------------------------------------------------------
+ // TaskHelpers.Canceled
+
+ [Fact]
+ public void Canceled_ReturnsCanceledTask()
+ {
+ Task result = TaskHelpers.Canceled();
+
+ Assert.NotNull(result);
+ Assert.True(result.IsCanceled);
+ }
+
+ // -----------------------------------------------------------------
+ // TaskHelpers.Canceled<T>
+
+ [Fact]
+ public void Canceled_Generic_ReturnsCanceledTask()
+ {
+ Task<string> result = TaskHelpers.Canceled<string>();
+
+ Assert.NotNull(result);
+ Assert.True(result.IsCanceled);
+ }
+
+ // -----------------------------------------------------------------
+ // TaskHelpers.Completed
+
+ [Fact]
+ public void Completed_ReturnsCompletedTask()
+ {
+ Task result = TaskHelpers.Completed();
+
+ Assert.NotNull(result);
+ Assert.Equal(TaskStatus.RanToCompletion, result.Status);
+ }
+
+ // -----------------------------------------------------------------
+ // TaskHelpers.FromError
+
+ [Fact]
+ public void FromError_ReturnsFaultedTaskWithGivenException()
+ {
+ var exception = new Exception();
+
+ Task result = TaskHelpers.FromError(exception);
+
+ Assert.NotNull(result);
+ Assert.True(result.IsFaulted);
+ Assert.Same(exception, result.Exception.InnerException);
+ }
+
+ // -----------------------------------------------------------------
+ // TaskHelpers.FromError<T>
+
+ [Fact]
+ public void FromError_Generic_ReturnsFaultedTaskWithGivenException()
+ {
+ var exception = new Exception();
+
+ Task<string> result = TaskHelpers.FromError<string>(exception);
+
+ Assert.NotNull(result);
+ Assert.True(result.IsFaulted);
+ Assert.Same(exception, result.Exception.InnerException);
+ }
+
+ // -----------------------------------------------------------------
+ // TaskHelpers.FromErrors
+
+ [Fact]
+ public void FromErrors_ReturnsFaultedTaskWithGivenExceptions()
+ {
+ var exceptions = new[] { new Exception(), new InvalidOperationException() };
+
+ Task result = TaskHelpers.FromErrors(exceptions);
+
+ Assert.NotNull(result);
+ Assert.True(result.IsFaulted);
+ Assert.Equal(exceptions, result.Exception.InnerExceptions.ToArray());
+ }
+
+ // -----------------------------------------------------------------
+ // TaskHelpers.FromErrors<T>
+
+ [Fact]
+ public void FromErrors_Generic_ReturnsFaultedTaskWithGivenExceptions()
+ {
+ var exceptions = new[] { new Exception(), new InvalidOperationException() };
+
+ Task<string> result = TaskHelpers.FromErrors<string>(exceptions);
+
+ Assert.NotNull(result);
+ Assert.True(result.IsFaulted);
+ Assert.Equal(exceptions, result.Exception.InnerExceptions.ToArray());
+ }
+
+ // -----------------------------------------------------------------
+ // TaskHelpers.FromResult<T>
+
+ [Fact]
+ public void FromResult_ReturnsCompletedTaskWithGivenResult()
+ {
+ string s = "ABC";
+
+ Task<string> result = TaskHelpers.FromResult(s);
+
+ Assert.NotNull(result);
+ Assert.True(result.Status == TaskStatus.RanToCompletion);
+ Assert.Same(s, result.Result);
+ }
+
+ // -----------------------------------------------------------------
+ // Task TaskHelpers.Iterate(IEnumerable<Task>)
+
+ [Fact]
+ public void Iterate_NonGeneric_IfProvidedEnumerationContainsNullValue_ReturnsFaultedTaskWithNullReferenceException()
+ {
+ List<string> log = new List<string>();
+
+ var result = TaskHelpers.Iterate(NullTaskEnumerable(log));
+
+ Assert.NotNull(result);
+ result.WaitUntilCompleted();
+ Assert.Equal(TaskStatus.Faulted, result.Status);
+ Assert.IsType<NullReferenceException>(result.Exception.GetBaseException());
+ }
+
+ private static IEnumerable<Task> NullTaskEnumerable(List<string> log)
+ {
+ log.Add("first");
+ yield return null;
+ log.Add("second");
+ }
+
+ [Fact]
+ public void Iterate_NonGeneric_IfProvidedEnumerationThrowsException_ReturnsFaultedTask()
+ {
+ List<string> log = new List<string>();
+ Exception exception = new Exception();
+
+ var result = TaskHelpers.Iterate(ThrowingTaskEnumerable(exception, log));
+
+ Assert.NotNull(result);
+ result.WaitUntilCompleted();
+ Assert.Equal(TaskStatus.Faulted, result.Status);
+ Assert.Same(exception, result.Exception.InnerException);
+ Assert.Equal(new[] { "first" }, log.ToArray());
+ }
+
+ private static IEnumerable<Task> ThrowingTaskEnumerable(Exception e, List<string> log)
+ {
+ log.Add("first");
+ bool a = true; // work around unreachable code warning
+ if (a) throw e;
+ log.Add("second");
+ yield return null;
+ }
+
+ [Fact]
+ public void Iterate_NonGeneric_IfProvidedEnumerableExecutesCancellingTask_ReturnsCanceledTaskAndHaltsEnumeration()
+ {
+ List<string> log = new List<string>();
+
+ var result = TaskHelpers.Iterate(CanceledTaskEnumerable(log));
+
+ Assert.NotNull(result);
+ result.WaitUntilCompleted();
+ Assert.Equal(TaskStatus.Canceled, result.Status);
+ Assert.Equal(new[] { "first" }, log.ToArray());
+ }
+
+ private static IEnumerable<Task> CanceledTaskEnumerable(List<string> log)
+ {
+ log.Add("first");
+ yield return TaskHelpers.Canceled();
+ log.Add("second");
+ }
+
+ [Fact]
+ public void Iterate_NonGeneric_IfProvidedEnumerableExecutesFaultingTask_ReturnsCanceledTaskAndHaltsEnumeration()
+ {
+ List<string> log = new List<string>();
+ Exception exception = new Exception();
+
+ var result = TaskHelpers.Iterate(FaultedTaskEnumerable(exception, log));
+
+ Assert.NotNull(result);
+ result.WaitUntilCompleted();
+ Assert.Equal(TaskStatus.Faulted, result.Status);
+ Assert.Same(exception, result.Exception.InnerException);
+ Assert.Equal(new[] { "first" }, log.ToArray());
+ }
+
+ private static IEnumerable<Task> FaultedTaskEnumerable(Exception e, List<string> log)
+ {
+ log.Add("first");
+ yield return TaskHelpers.FromError(e);
+ log.Add("second");
+ }
+
+ [Fact]
+ public void Iterate_NonGeneric_ExecutesNextTaskOnlyAfterPreviousTaskSucceeded()
+ {
+ List<string> log = new List<string>();
+
+ var result = TaskHelpers.Iterate(SuccessTaskEnumerable(log));
+
+ Assert.NotNull(result);
+ result.WaitUntilCompleted();
+ Assert.Equal(TaskStatus.RanToCompletion, result.Status);
+ Assert.Equal(
+ new[] { "first", "Executing first task. Log size: 1", "second", "Executing second task. Log size: 3" },
+ log.ToArray());
+ }
+
+ private static IEnumerable<Task> SuccessTaskEnumerable(List<string> log)
+ {
+ log.Add("first");
+ yield return Task.Factory.StartNew(() => log.Add("Executing first task. Log size: " + log.Count));
+ log.Add("second");
+ yield return Task.Factory.StartNew(() => log.Add("Executing second task. Log size: " + log.Count));
+ }
+
+ [Fact]
+ public void Iterate_NonGeneric_TasksRunSequentiallyRegardlessOfExecutionTime()
+ {
+ List<string> log = new List<string>();
+
+ Task task = TaskHelpers.Iterate(TasksWithVaryingDelays(log, 100, 1, 50, 2));
+
+ task.WaitUntilCompleted();
+ Assert.Equal(TaskStatus.RanToCompletion, task.Status);
+ Assert.Equal(new[] { "ENTER: 100", "EXIT: 100", "ENTER: 1", "EXIT: 1", "ENTER: 50", "EXIT: 50", "ENTER: 2", "EXIT: 2" }, log);
+ }
+
+ private static IEnumerable<Task> TasksWithVaryingDelays(List<string> log, params int[] delays)
+ {
+ foreach (int delay in delays)
+ yield return Task.Factory.StartNew(timeToSleep =>
+ {
+ log.Add("ENTER: " + timeToSleep);
+ Thread.Sleep((int)timeToSleep);
+ log.Add("EXIT: " + timeToSleep);
+ }, delay);
+ }
+
+ [Fact]
+ public void Iterate_NonGeneric_StopsTaskIterationIfCancellationWasRequested()
+ {
+ List<string> log = new List<string>();
+ CancellationTokenSource cts = new CancellationTokenSource();
+
+ var result = TaskHelpers.Iterate(CancelingTaskEnumerable(log, cts), cts.Token);
+
+ Assert.NotNull(result);
+ result.WaitUntilCompleted();
+ Assert.Equal(TaskStatus.Canceled, result.Status);
+ Assert.Equal(
+ new[] { "first", "Executing first task. Log size: 1" },
+ log.ToArray());
+ }
+
+ private static IEnumerable<Task> CancelingTaskEnumerable(List<string> log, CancellationTokenSource cts)
+ {
+ log.Add("first");
+ yield return Task.Factory.StartNew(() =>
+ {
+ log.Add("Executing first task. Log size: " + log.Count);
+ cts.Cancel();
+ });
+ log.Add("second");
+ yield return Task.Factory.StartNew(() =>
+ {
+ log.Add("Executing second task. Log size: " + log.Count);
+ });
+ }
+
+ [Fact, PreserveSyncContext]
+ public Task Iterate_NonGeneric_IteratorRunsInSynchronizationContext()
+ {
+ ThreadPoolSyncContext sc = new ThreadPoolSyncContext();
+ SynchronizationContext.SetSynchronizationContext(sc);
+
+ return TaskHelpers.Iterate(SyncContextVerifyingEnumerable(sc)).Then(() =>
+ {
+ Assert.Same(sc, SynchronizationContext.Current);
+ });
+ }
+
+ private static IEnumerable<Task> SyncContextVerifyingEnumerable(SynchronizationContext sc)
+ {
+ for (int i = 0; i < 10; i++)
+ {
+ Assert.Same(sc, SynchronizationContext.Current);
+ yield return TaskHelpers.Completed();
+ }
+ }
+
+ // -----------------------------------------------------------------
+ // Task<IEnumerable<T>> TaskHelpers.Iterate(IEnumerable<Task<T>>)
+
+ [Fact]
+ public void Iterate_Generic_IfProvidedEnumerationContainsNullValue_ReturnsFaultedTaskWithNullReferenceException()
+ {
+ List<string> log = new List<string>();
+
+ Task<IEnumerable<object>> result = TaskHelpers.Iterate(NullTaskEnumerable_Generic(log));
+
+ Assert.NotNull(result);
+ result.WaitUntilCompleted();
+ Assert.Equal(TaskStatus.Faulted, result.Status);
+ Assert.IsType<NullReferenceException>(result.Exception.GetBaseException());
+ }
+
+ private static IEnumerable<Task<object>> NullTaskEnumerable_Generic(List<string> log)
+ {
+ log.Add("first");
+ yield return null;
+ log.Add("second");
+ }
+
+ [Fact]
+ public void Iterate_Generic_IfProvidedEnumerationThrowsException_ReturnsFaultedTask()
+ {
+ List<string> log = new List<string>();
+ Exception exception = new Exception();
+
+ Task<IEnumerable<object>> result = TaskHelpers.Iterate(ThrowingTaskEnumerable_Generic(exception, log));
+
+ Assert.NotNull(result);
+ result.WaitUntilCompleted();
+ Assert.Equal(TaskStatus.Faulted, result.Status);
+ Assert.Same(exception, result.Exception.InnerException);
+ Assert.Equal(new[] { "first" }, log.ToArray());
+ }
+
+ private static IEnumerable<Task<object>> ThrowingTaskEnumerable_Generic(Exception e, List<string> log)
+ {
+ log.Add("first");
+ bool a = true; // work around unreachable code warning
+ if (a) throw e;
+ log.Add("second");
+ yield return null;
+ }
+
+ [Fact]
+ public void Iterate_Generic_IfProvidedEnumerableExecutesCancellingTask_ReturnsCanceledTaskAndHaltsEnumeration()
+ {
+ List<string> log = new List<string>();
+
+ Task<IEnumerable<object>> result = TaskHelpers.Iterate(CanceledTaskEnumerable_Generic(log));
+
+ Assert.NotNull(result);
+ result.WaitUntilCompleted();
+ Assert.Equal(TaskStatus.Canceled, result.Status);
+ Assert.Equal(new[] { "first" }, log.ToArray());
+ }
+
+ private static IEnumerable<Task<object>> CanceledTaskEnumerable_Generic(List<string> log)
+ {
+ log.Add("first");
+ yield return TaskHelpers.Canceled<object>();
+ log.Add("second");
+ }
+
+ [Fact]
+ public void Iterate_Generic_IfProvidedEnumerableExecutesFaultingTask_ReturnsCanceledTaskAndHaltsEnumeration()
+ {
+ List<string> log = new List<string>();
+ Exception exception = new Exception();
+
+ Task<IEnumerable<object>> result = TaskHelpers.Iterate(FaultedTaskEnumerable_Generic(exception, log));
+
+ Assert.NotNull(result);
+ result.WaitUntilCompleted();
+ Assert.Equal(TaskStatus.Faulted, result.Status);
+ Assert.Same(exception, result.Exception.InnerException);
+ Assert.Equal(new[] { "first" }, log.ToArray());
+ }
+
+ private static IEnumerable<Task<object>> FaultedTaskEnumerable_Generic(Exception e, List<string> log)
+ {
+ log.Add("first");
+ yield return TaskHelpers.FromError<object>(e);
+ log.Add("second");
+ }
+
+ [Fact]
+ public void Iterate_Generic_ExecutesNextTaskOnlyAfterPreviousTaskSucceeded()
+ {
+ Task<IEnumerable<int>> result = TaskHelpers.Iterate(SuccessTaskEnumerable_Generic());
+
+ Assert.NotNull(result);
+ result.WaitUntilCompleted();
+ Assert.Equal(TaskStatus.RanToCompletion, result.Status);
+ Assert.Equal(new[] { 42, 2112 }, result.Result);
+ }
+
+ private static IEnumerable<Task<int>> SuccessTaskEnumerable_Generic()
+ {
+ yield return Task.Factory.StartNew(() => 42);
+ yield return Task.Factory.StartNew(() => 2112);
+ }
+
+ [Fact]
+ public void Iterate_Generic_TasksRunSequentiallyRegardlessOfExecutionTime()
+ {
+ List<string> log = new List<string>();
+
+ Task<IEnumerable<object>> task = TaskHelpers.Iterate(TasksWithVaryingDelays_Generic(log, 100, 1, 50, 2));
+
+ task.WaitUntilCompleted();
+ Assert.Equal(TaskStatus.RanToCompletion, task.Status);
+ Assert.Equal(new[] { "ENTER: 100", "EXIT: 100", "ENTER: 1", "EXIT: 1", "ENTER: 50", "EXIT: 50", "ENTER: 2", "EXIT: 2" }, log);
+ }
+
+ private static IEnumerable<Task<object>> TasksWithVaryingDelays_Generic(List<string> log, params int[] delays)
+ {
+ foreach (int delay in delays)
+ yield return Task.Factory.StartNew(timeToSleep =>
+ {
+ log.Add("ENTER: " + timeToSleep);
+ Thread.Sleep((int)timeToSleep);
+ log.Add("EXIT: " + timeToSleep);
+ return (object)null;
+ }, delay);
+ }
+
+ [Fact]
+ public void Iterate_Generic_StopsTaskIterationIfCancellationWasRequested()
+ {
+ List<string> log = new List<string>();
+ CancellationTokenSource cts = new CancellationTokenSource();
+
+ var result = TaskHelpers.Iterate(CancelingTaskEnumerable_Generic(log, cts), cts.Token);
+
+ Assert.NotNull(result);
+ result.WaitUntilCompleted();
+ Assert.Equal(TaskStatus.Canceled, result.Status);
+ Assert.Equal(
+ new[] { "first", "Executing first task. Log size: 1" },
+ log.ToArray());
+ }
+
+ private static IEnumerable<Task<object>> CancelingTaskEnumerable_Generic(List<string> log, CancellationTokenSource cts)
+ {
+ log.Add("first");
+ yield return Task.Factory.StartNew(() =>
+ {
+ log.Add("Executing first task. Log size: " + log.Count);
+ cts.Cancel();
+ return (object)null;
+ });
+ log.Add("second");
+ yield return Task.Factory.StartNew(() =>
+ {
+ log.Add("Executing second task. Log size: " + log.Count);
+ return (object)null;
+ });
+ }
+
+ [Fact, PreserveSyncContext]
+ public Task Iterate_Generic_IteratorRunsInSynchronizationContext()
+ {
+ ThreadPoolSyncContext sc = new ThreadPoolSyncContext();
+ SynchronizationContext.SetSynchronizationContext(sc);
+
+ return TaskHelpers.Iterate(SyncContextVerifyingEnumerable_Generic(sc)).Then(result =>
+ {
+ Assert.Same(sc, SynchronizationContext.Current);
+ Assert.Equal(new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }, result);
+ });
+ }
+
+ private static IEnumerable<Task<int>> SyncContextVerifyingEnumerable_Generic(SynchronizationContext sc)
+ {
+ for (int i = 0; i < 10; i++)
+ {
+ Assert.Same(sc, SynchronizationContext.Current);
+ yield return TaskHelpers.FromResult(i);
+ }
+ }
+
+ // -----------------------------------------------------------------
+ // TaskHelpers.TrySetFromTask<T>
+
+ [Fact]
+ public void TrySetFromTask_IfSourceTaskIsCanceled_CancelsTaskCompletionSource()
+ {
+ TaskCompletionSource<string> tcs = new TaskCompletionSource<string>();
+ Task canceledTask = TaskHelpers.Canceled<object>();
+
+ tcs.TrySetFromTask(canceledTask);
+
+ Assert.Equal(TaskStatus.Canceled, tcs.Task.Status);
+ }
+
+ [Fact]
+ public void TrySetFromTask_IfSourceTaskIsFaulted_FaultsTaskCompletionSource()
+ {
+ TaskCompletionSource<string> tcs = new TaskCompletionSource<string>();
+ Exception exception = new Exception();
+ Task faultedTask = TaskHelpers.FromError<object>(exception);
+
+ tcs.TrySetFromTask(faultedTask);
+
+ Assert.Equal(TaskStatus.Faulted, tcs.Task.Status);
+ Assert.Same(exception, tcs.Task.Exception.InnerException);
+ }
+
+ [Fact]
+ public void TrySetFromTask_IfSourceTaskIsSuccessfulAndOfSameResultType_SucceedsTaskCompletionSourceAndSetsResult()
+ {
+ TaskCompletionSource<string> tcs = new TaskCompletionSource<string>();
+ Task<string> successfulTask = TaskHelpers.FromResult("abc");
+
+ tcs.TrySetFromTask(successfulTask);
+
+ Assert.Equal(TaskStatus.RanToCompletion, tcs.Task.Status);
+ Assert.Equal("abc", tcs.Task.Result);
+ }
+
+ [Fact]
+ public void TrySetFromTask_IfSourceTaskIsSuccessfulAndOfDifferentResultType_SucceedsTaskCompletionSourceAndSetsDefaultValueAsResult()
+ {
+ TaskCompletionSource<string> tcs = new TaskCompletionSource<string>();
+ Task<object> successfulTask = TaskHelpers.FromResult(new object());
+
+ tcs.TrySetFromTask(successfulTask);
+
+ Assert.Equal(TaskStatus.RanToCompletion, tcs.Task.Status);
+ Assert.Equal(null, tcs.Task.Result);
+ }
+
+ // -----------------------------------------------------------------
+ // TaskHelpers.RunSynchronously
+
+ [Fact]
+ public void RunSynchronously_Executes_Action()
+ {
+ bool wasRun = false;
+ Task t = TaskHelpers.RunSynchronously(() => { wasRun = true; });
+ t.WaitUntilCompleted();
+ Assert.True(wasRun);
+ }
+
+ [Fact]
+ public void RunSynchronously_Captures_Exception_In_AggregateException()
+ {
+ Task t = TaskHelpers.RunSynchronously(() => { throw new InvalidOperationException(); });
+ Assert.Throws<InvalidOperationException>(() => t.Wait());
+ }
+
+ [Fact]
+ public void RunSynchronously_Cancels()
+ {
+ CancellationTokenSource cts = new CancellationTokenSource();
+ cts.Cancel();
+
+ Task t = TaskHelpers.RunSynchronously(() => { throw new InvalidOperationException(); }, cts.Token);
+ Assert.Throws<TaskCanceledException>(() => t.Wait());
+ }
+ }
+}
diff --git a/test/System.Web.Http.Common.Test/packages.config b/test/System.Web.Http.Common.Test/packages.config
new file mode 100644
index 00000000..b9070f9e
--- /dev/null
+++ b/test/System.Web.Http.Common.Test/packages.config
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="Microsoft.Net.Http" version="2.0.20302.1" />
+ <package id="Moq" version="4.0.10827" />
+ <package id="xunit" version="1.9.0.1566" />
+ <package id="xunit.extensions" version="1.9.0.1566" />
+</packages> \ No newline at end of file
diff --git a/test/System.Web.Http.Integration.Test/ApiExplorer/ApiExplorerSettingsTest.cs b/test/System.Web.Http.Integration.Test/ApiExplorer/ApiExplorerSettingsTest.cs
new file mode 100644
index 00000000..65f33d28
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/ApiExplorer/ApiExplorerSettingsTest.cs
@@ -0,0 +1,62 @@
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Web.Http.Description;
+using System.Web.Http.Dispatcher;
+using Xunit.Extensions;
+
+namespace System.Web.Http.ApiExplorer
+{
+ public class ApiExplorerSettingsTest
+ {
+ public static IEnumerable<object[]> HiddenController_DoesNotShowUpOnDescription_PropertyData
+ {
+ get
+ {
+ object controllerType = typeof(HiddenController);
+ object expectedApiDescriptions = new List<object>();
+ yield return new[] { controllerType, expectedApiDescriptions };
+ }
+ }
+
+ [Theory]
+ [PropertyData("HiddenController_DoesNotShowUpOnDescription_PropertyData")]
+ public void HiddenController_DoesNotShowUpOnDescription(Type controllerType, List<object> expectedResults)
+ {
+ HttpConfiguration config = new HttpConfiguration();
+ config.Routes.MapHttpRoute("Default", "{controller}/{id}", new { id = RouteParameter.Optional });
+
+ DefaultHttpControllerFactory controllerFactory = ApiExplorerHelper.GetStrictControllerFactory(config, controllerType);
+ config.ServiceResolver.SetService(typeof(IHttpControllerFactory), controllerFactory);
+
+ IApiExplorer explorer = config.ServiceResolver.GetApiExplorer();
+ ApiExplorerHelper.VerifyApiDescriptions(explorer.ApiDescriptions, expectedResults);
+ }
+
+ public static IEnumerable<object[]> HiddenAction_DoesNotShowUpOnDescription_PropertyData
+ {
+ get
+ {
+ object controllerType = typeof(HiddenActionController);
+ object expectedApiDescriptions = new List<object>
+ {
+ new { HttpMethod = HttpMethod.Get, RelativePath = "HiddenAction/{id}", HasRequestFormatters = false, HasResponseFormatters = true, NumberOfParameters = 1}
+ };
+ yield return new[] { controllerType, expectedApiDescriptions };
+ }
+ }
+
+ [Theory]
+ [PropertyData("HiddenAction_DoesNotShowUpOnDescription_PropertyData")]
+ public void HiddenAction_DoesNotShowUpOnDescription(Type controllerType, List<object> expectedResults)
+ {
+ HttpConfiguration config = new HttpConfiguration();
+ config.Routes.MapHttpRoute("Default", "{controller}/{id}", new { id = RouteParameter.Optional });
+
+ DefaultHttpControllerFactory controllerFactory = ApiExplorerHelper.GetStrictControllerFactory(config, controllerType);
+ config.ServiceResolver.SetService(typeof(IHttpControllerFactory), controllerFactory);
+
+ IApiExplorer explorer = config.ServiceResolver.GetApiExplorer();
+ ApiExplorerHelper.VerifyApiDescriptions(explorer.ApiDescriptions, expectedResults);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Integration.Test/ApiExplorer/Controllers/DocumentationController.cs b/test/System.Web.Http.Integration.Test/ApiExplorer/Controllers/DocumentationController.cs
new file mode 100644
index 00000000..f0d52c40
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/ApiExplorer/Controllers/DocumentationController.cs
@@ -0,0 +1,31 @@
+
+namespace System.Web.Http.ApiExplorer
+{
+ public class DocumentationController : ApiController
+ {
+ [ApiDocumentation("Get action")]
+ public string Get()
+ {
+ return string.Empty;
+ }
+
+ [ApiDocumentation("Post action")]
+ [ApiParameterDocumentation("value", "value parameter")]
+ public void Post(string value)
+ {
+ }
+
+ [ApiDocumentation("Put action")]
+ [ApiParameterDocumentation("id", "id parameter")]
+ [ApiParameterDocumentation("value", "value parameter")]
+ public void Put(int id, string value)
+ {
+ }
+
+ [ApiDocumentation("Delete action")]
+ [ApiParameterDocumentation("id", "id parameter")]
+ public void Delete(int id)
+ {
+ }
+ }
+}
diff --git a/test/System.Web.Http.Integration.Test/ApiExplorer/Controllers/HiddenActionController.cs b/test/System.Web.Http.Integration.Test/ApiExplorer/Controllers/HiddenActionController.cs
new file mode 100644
index 00000000..ab6cb702
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/ApiExplorer/Controllers/HiddenActionController.cs
@@ -0,0 +1,29 @@
+using System.Web.Http.Description;
+namespace System.Web.Http.ApiExplorer
+{
+ public class HiddenActionController : ApiController
+ {
+ public string GetVisibleAction(int id)
+ {
+ return "visible action";
+ }
+
+ [HttpPost]
+ [ApiExplorerSettings(IgnoreApi = true)]
+ public void AddData()
+ {
+ }
+
+ [ApiExplorerSettings(IgnoreApi = true)]
+ public int Get()
+ {
+ return 0;
+ }
+
+ [NonAction]
+ public string GetHiddenAction()
+ {
+ return "Hidden action";
+ }
+ }
+}
diff --git a/test/System.Web.Http.Integration.Test/ApiExplorer/Controllers/HiddenController.cs b/test/System.Web.Http.Integration.Test/ApiExplorer/Controllers/HiddenController.cs
new file mode 100644
index 00000000..990da738
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/ApiExplorer/Controllers/HiddenController.cs
@@ -0,0 +1,28 @@
+using System.Web.Http.Description;
+namespace System.Web.Http.ApiExplorer
+{
+ [ApiExplorerSettings(IgnoreApi = true)]
+ public class HiddenController : ApiController
+ {
+ public string Get(int id)
+ {
+ return "visible action";
+ }
+
+ [HttpPost]
+ public void AddData()
+ {
+ }
+
+ public int Get()
+ {
+ return 0;
+ }
+
+ [NonAction]
+ public string GetHiddenAction()
+ {
+ return "Hidden action";
+ }
+ }
+}
diff --git a/test/System.Web.Http.Integration.Test/ApiExplorer/Controllers/ItemController.cs b/test/System.Web.Http.Integration.Test/ApiExplorer/Controllers/ItemController.cs
new file mode 100644
index 00000000..03818774
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/ApiExplorer/Controllers/ItemController.cs
@@ -0,0 +1,32 @@
+namespace System.Web.Http.ApiExplorer
+{
+ public class ItemController : ApiController
+ {
+ public Item GetItem(string name, int series)
+ {
+ return new Item()
+ {
+ Name = name,
+ Series = series
+ };
+ }
+
+ [HttpPost]
+ [HttpPut]
+ public Item PostItem(Item item)
+ {
+ return item;
+ }
+
+ [HttpDelete]
+ public void RemoveItem(int id)
+ {
+ }
+
+ public class Item
+ {
+ public int Series { get; set; }
+ public string Name { get; set; }
+ }
+ }
+}
diff --git a/test/System.Web.Http.Integration.Test/ApiExplorer/Controllers/OverloadsController.cs b/test/System.Web.Http.Integration.Test/ApiExplorer/Controllers/OverloadsController.cs
new file mode 100644
index 00000000..87161e7c
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/ApiExplorer/Controllers/OverloadsController.cs
@@ -0,0 +1,28 @@
+using System.Collections.Generic;
+
+namespace System.Web.Http.ApiExplorer
+{
+ public class OverloadsController : ApiController
+ {
+ public Person Get(int id) { return null; }
+ public List<Person> Get() { return null; }
+ public List<Person> Get(string name) { return null; }
+ public Person GetPersonByNameAndId(string name, int id) { return null; }
+ public Person GetPersonByNameAndAge(string name, int age) { return null; }
+ public Person GetPersonByNameAgeAndSsn(string name, int age, int ssn) { return null; }
+ public Person GetPersonByNameIdAndSsn(string name, int id, int ssn) { return null; }
+ public Person GetPersonByNameAndSsn(string name, int ssn) { return null; }
+ public Person Post(Person Person) { return null; }
+ public Person Post(string name, int age) { return null; }
+ public void Delete(int id, string name = "Default Name") { }
+ public void Delete(int id, string name, int age) { }
+
+ public class Person
+ {
+ public string Name { get; set; }
+ public int ID { get; set; }
+ public int SSN { get; set; }
+ public int Age { get; set; }
+ }
+ }
+}
diff --git a/test/System.Web.Http.Integration.Test/ApiExplorer/Controllers/ParameterSourceController.cs b/test/System.Web.Http.Integration.Test/ApiExplorer/Controllers/ParameterSourceController.cs
new file mode 100644
index 00000000..dd39a62c
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/ApiExplorer/Controllers/ParameterSourceController.cs
@@ -0,0 +1,58 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Web.Http.ModelBinding;
+using System.Web.Http.ValueProviders;
+
+namespace System.Web.Http.ApiExplorer
+{
+ public class ParameterSourceController : ApiController
+ {
+ public void GetCompleTypeFromUri([FromUri]ComplexType value, string name)
+ {
+ }
+
+ public void PostSimpleTypeFromBody([FromBody] string name)
+ {
+ }
+
+ public void GetCustomFromUriAttribute([MyFromUriAttribute] ComplexType value, ComplexType bodyValue)
+ {
+ }
+
+ public void GetFromHeaderAttribute([FromHeaderAttribute] string value)
+ {
+ }
+
+ public class ComplexType
+ {
+ public int Id { get; set; }
+ public string Name { get; set; }
+ }
+
+ private class MyFromUriAttribute : ModelBinderAttribute, IUriValueProviderFactory
+ {
+ public override IEnumerable<ValueProviderFactory> GetValueProviderFactories(HttpConfiguration configuration)
+ {
+ var factories = from f in base.GetValueProviderFactories(configuration) where f is IUriValueProviderFactory select f;
+ return factories;
+ }
+ }
+
+ private class FromHeaderAttribute : ModelBinderAttribute
+ {
+ public override IEnumerable<ValueProviderFactory> GetValueProviderFactories(HttpConfiguration configuration)
+ {
+ var factories = new ValueProviderFactory[] { new HeaderValueProvider() };
+ return factories;
+ }
+ }
+
+ private class HeaderValueProvider : ValueProviderFactory
+ {
+ public override IValueProvider GetValueProvider(Controllers.HttpActionContext actionContext)
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Http.Integration.Test/ApiExplorer/DocumentationProviders/AttributeDocumentationProvider.cs b/test/System.Web.Http.Integration.Test/ApiExplorer/DocumentationProviders/AttributeDocumentationProvider.cs
new file mode 100644
index 00000000..02960b62
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/ApiExplorer/DocumentationProviders/AttributeDocumentationProvider.cs
@@ -0,0 +1,56 @@
+using System.Linq;
+using System.Web.Http.Controllers;
+using System.Web.Http.Description;
+
+namespace System.Web.Http.ApiExplorer
+{
+ public class AttributeDocumentationProvider : IDocumentationProvider
+ {
+ public string GetDocumentation(HttpActionDescriptor actionDescriptor)
+ {
+ var apiDocumentation = actionDescriptor.GetCustomAttributes<ApiDocumentationAttribute>().FirstOrDefault();
+ if (apiDocumentation != null)
+ {
+ return apiDocumentation.Description;
+ }
+
+ return string.Empty;
+ }
+
+ public string GetDocumentation(HttpParameterDescriptor parameterDescriptor)
+ {
+ var parameterDocumentation = parameterDescriptor.ActionDescriptor.GetCustomAttributes<ApiParameterDocumentationAttribute>().FirstOrDefault(param => param.ParameterName == parameterDescriptor.ParameterName);
+ if (parameterDocumentation != null)
+ {
+ return parameterDocumentation.Description;
+ }
+
+ return string.Empty;
+ }
+ }
+
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
+ public sealed class ApiDocumentationAttribute : Attribute
+ {
+ public ApiDocumentationAttribute(string description)
+ {
+ Description = description;
+ }
+
+ public string Description { get; private set; }
+ }
+
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
+ public sealed class ApiParameterDocumentationAttribute : Attribute
+ {
+ public ApiParameterDocumentationAttribute(string parameterName, string description)
+ {
+ ParameterName = parameterName;
+ Description = description;
+ }
+
+ public string ParameterName { get; private set; }
+
+ public string Description { get; private set; }
+ }
+}
diff --git a/test/System.Web.Http.Integration.Test/ApiExplorer/DocumentationTest.cs b/test/System.Web.Http.Integration.Test/ApiExplorer/DocumentationTest.cs
new file mode 100644
index 00000000..33f8f538
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/ApiExplorer/DocumentationTest.cs
@@ -0,0 +1,65 @@
+using System.Web.Http.Description;
+using System.Web.Http.Dispatcher;
+using System.Web.Http.Properties;
+using Xunit;
+
+namespace System.Web.Http.ApiExplorer
+{
+ public class DocumentationTest
+ {
+ [Fact]
+ public void VerifyDefaultDocumentationMessage()
+ {
+ HttpConfiguration config = new HttpConfiguration();
+ config.Routes.MapHttpRoute("Default", "{controller}/{id}", new { id = RouteParameter.Optional });
+ ItemFormatter customFormatter = new ItemFormatter();
+ config.Formatters.Add(customFormatter);
+
+ DefaultHttpControllerFactory controllerFactory = ApiExplorerHelper.GetStrictControllerFactory(config, typeof(ItemController));
+ config.ServiceResolver.SetService(typeof(IHttpControllerFactory), controllerFactory);
+
+ IApiExplorer explorer = config.ServiceResolver.GetApiExplorer();
+ foreach (ApiDescription description in explorer.ApiDescriptions)
+ {
+ Assert.Equal(
+ String.Format(SRResources.ApiExplorer_DefaultDocumentation, description.ActionDescriptor.ActionName),
+ description.Documentation);
+ foreach (ApiParameterDescription param in description.ParameterDescriptions)
+ {
+ Assert.Equal(
+ String.Format(SRResources.ApiExplorer_DefaultDocumentation, param.Name),
+ param.Documentation);
+ }
+ }
+ }
+
+ [Fact]
+ public void VerifyCustomDocumentationProviderMessage()
+ {
+ HttpConfiguration config = new HttpConfiguration();
+ config.Routes.MapHttpRoute("Default", "{controller}/{id}", new { id = RouteParameter.Optional });
+ ItemFormatter customFormatter = new ItemFormatter();
+ config.Formatters.Add(customFormatter);
+
+ DefaultHttpControllerFactory controllerFactory = ApiExplorerHelper.GetStrictControllerFactory(config, typeof(DocumentationController));
+ config.ServiceResolver.SetService(typeof(IHttpControllerFactory), controllerFactory);
+
+ AttributeDocumentationProvider documentationProvider = new AttributeDocumentationProvider();
+ config.ServiceResolver.SetService(typeof(IDocumentationProvider), documentationProvider);
+
+ IApiExplorer explorer = config.ServiceResolver.GetApiExplorer();
+ foreach (ApiDescription description in explorer.ApiDescriptions)
+ {
+ Assert.Equal(
+ String.Format("{0} action", description.ActionDescriptor.ActionName),
+ description.Documentation);
+ foreach (ApiParameterDescription param in description.ParameterDescriptions)
+ {
+ Assert.Equal(
+ String.Format("{0} parameter", param.Name),
+ param.Documentation);
+ }
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Http.Integration.Test/ApiExplorer/Formatters/ItemFormatter.cs b/test/System.Web.Http.Integration.Test/ApiExplorer/Formatters/ItemFormatter.cs
new file mode 100644
index 00000000..8c89f59f
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/ApiExplorer/Formatters/ItemFormatter.cs
@@ -0,0 +1,27 @@
+using System.Net.Http.Formatting;
+
+namespace System.Web.Http.ApiExplorer
+{
+ public class ItemFormatter : BufferedMediaTypeFormatter
+ {
+ public override bool CanReadType(Type type)
+ {
+ return typeof(System.Web.Http.ApiExplorer.ItemController.Item).IsAssignableFrom(type);
+ }
+
+ public override bool CanWriteType(Type type)
+ {
+ return typeof(System.Web.Http.ApiExplorer.ItemController.Item).IsAssignableFrom(type);
+ }
+
+ public override object OnReadFromStream(Type type, IO.Stream stream, Net.Http.Headers.HttpContentHeaders contentHeaders, IFormatterLogger formatterLogger)
+ {
+ return base.OnReadFromStream(type, stream, contentHeaders, formatterLogger);
+ }
+
+ public override void OnWriteToStream(Type type, object value, IO.Stream stream, Net.Http.Headers.HttpContentHeaders contentHeaders, Net.TransportContext transportContext)
+ {
+ base.OnWriteToStream(type, value, stream, contentHeaders, transportContext);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Integration.Test/ApiExplorer/FormattersTest.cs b/test/System.Web.Http.Integration.Test/ApiExplorer/FormattersTest.cs
new file mode 100644
index 00000000..5a370177
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/ApiExplorer/FormattersTest.cs
@@ -0,0 +1,42 @@
+using System.Linq;
+using System.Web.Http.Description;
+using System.Web.Http.Dispatcher;
+using Xunit;
+
+namespace System.Web.Http.ApiExplorer
+{
+ public class FormattersTest
+ {
+ [Fact]
+ public void CustomRequestBodyFormatters_ShowUpOnDescription()
+ {
+ HttpConfiguration config = new HttpConfiguration();
+ config.Routes.MapHttpRoute("Default", "{controller}/{id}", new { id = RouteParameter.Optional });
+ ItemFormatter customFormatter = new ItemFormatter();
+ config.Formatters.Add(customFormatter);
+
+ DefaultHttpControllerFactory controllerFactory = ApiExplorerHelper.GetStrictControllerFactory(config, typeof(ItemController));
+ config.ServiceResolver.SetService(typeof(IHttpControllerFactory), controllerFactory);
+
+ IApiExplorer explorer = config.ServiceResolver.GetApiExplorer();
+ ApiDescription description = explorer.ApiDescriptions.FirstOrDefault(desc => desc.ActionDescriptor.ActionName == "PostItem");
+ Assert.True(description.SupportedRequestBodyFormatters.Any(formatter => formatter == customFormatter), "Did not find the custom formatter on the SupportedRequestBodyFormatters.");
+ }
+
+ [Fact]
+ public void CustomResponseFormatters_ShowUpOnDescription()
+ {
+ HttpConfiguration config = new HttpConfiguration();
+ config.Routes.MapHttpRoute("Default", "{controller}/{id}", new { id = RouteParameter.Optional });
+ ItemFormatter customFormatter = new ItemFormatter();
+ config.Formatters.Add(customFormatter);
+
+ DefaultHttpControllerFactory controllerFactory = ApiExplorerHelper.GetStrictControllerFactory(config, typeof(ItemController));
+ config.ServiceResolver.SetService(typeof(IHttpControllerFactory), controllerFactory);
+
+ IApiExplorer explorer = config.ServiceResolver.GetApiExplorer();
+ ApiDescription description = explorer.ApiDescriptions.FirstOrDefault(desc => desc.ActionDescriptor.ActionName == "PostItem");
+ Assert.True(description.SupportedResponseFormatters.Any(formatter => formatter == customFormatter), "Did not find the custom formatter on the SupportedResponseFormatters.");
+ }
+ }
+}
diff --git a/test/System.Web.Http.Integration.Test/ApiExplorer/ParameterSourceTest.cs b/test/System.Web.Http.Integration.Test/ApiExplorer/ParameterSourceTest.cs
new file mode 100644
index 00000000..00783ce6
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/ApiExplorer/ParameterSourceTest.cs
@@ -0,0 +1,57 @@
+using System.Linq;
+using System.Web.Http.Description;
+using System.Web.Http.Dispatcher;
+using Xunit;
+
+namespace System.Web.Http.ApiExplorer
+{
+ public class ParameterSourceTest
+ {
+ [Fact]
+ public void FromUriParameterSource_ShowUpCorrectlyOnDescription()
+ {
+ HttpConfiguration config = new HttpConfiguration();
+ config.Routes.MapHttpRoute("Default", "{controller}/{action}/{id}", new { id = RouteParameter.Optional });
+ DefaultHttpControllerFactory controllerFactory = ApiExplorerHelper.GetStrictControllerFactory(config, typeof(ParameterSourceController));
+ config.ServiceResolver.SetService(typeof(IHttpControllerFactory), controllerFactory);
+ IApiExplorer explorer = config.ServiceResolver.GetApiExplorer();
+
+ ApiDescription description = explorer.ApiDescriptions.FirstOrDefault(desc => desc.ActionDescriptor.ActionName == "GetCompleTypeFromUri");
+ Assert.NotNull(description);
+ Assert.True(description.ParameterDescriptions.All(param => param.Source == ApiParameterSource.FromUri), "All parameters should come from URI.");
+
+ description = explorer.ApiDescriptions.FirstOrDefault(desc => desc.ActionDescriptor.ActionName == "GetCustomFromUriAttribute");
+ Assert.NotNull(description);
+ Assert.True(description.ParameterDescriptions.Any(param => param.Source == ApiParameterSource.FromUri && param.Name == "value"), "The 'value' parameter should come from URI.");
+ Assert.True(description.ParameterDescriptions.Any(param => param.Source == ApiParameterSource.FromBody && param.Name == "bodyValue"), "The 'bodyValue' parameter should come from body.");
+ }
+
+ [Fact]
+ public void FromBodyParameterSource_ShowUpCorrectlyOnDescription()
+ {
+ HttpConfiguration config = new HttpConfiguration();
+ config.Routes.MapHttpRoute("Default", "{controller}/{action}/{id}", new { id = RouteParameter.Optional });
+ DefaultHttpControllerFactory controllerFactory = ApiExplorerHelper.GetStrictControllerFactory(config, typeof(ParameterSourceController));
+ config.ServiceResolver.SetService(typeof(IHttpControllerFactory), controllerFactory);
+ IApiExplorer explorer = config.ServiceResolver.GetApiExplorer();
+
+ ApiDescription description = explorer.ApiDescriptions.FirstOrDefault(desc => desc.ActionDescriptor.ActionName == "PostSimpleTypeFromBody");
+ Assert.NotNull(description);
+ Assert.True(description.ParameterDescriptions.All(param => param.Source == ApiParameterSource.FromBody), "The parameter should come from Body.");
+ }
+
+ [Fact]
+ public void UnknownParameterSource_ShowUpCorrectlyOnDescription()
+ {
+ HttpConfiguration config = new HttpConfiguration();
+ config.Routes.MapHttpRoute("Default", "{controller}/{action}/{id}", new { id = RouteParameter.Optional });
+ DefaultHttpControllerFactory controllerFactory = ApiExplorerHelper.GetStrictControllerFactory(config, typeof(ParameterSourceController));
+ config.ServiceResolver.SetService(typeof(IHttpControllerFactory), controllerFactory);
+ IApiExplorer explorer = config.ServiceResolver.GetApiExplorer();
+
+ ApiDescription description = explorer.ApiDescriptions.FirstOrDefault(desc => desc.ActionDescriptor.ActionName == "GetFromHeaderAttribute");
+ Assert.NotNull(description);
+ Assert.True(description.ParameterDescriptions.All(param => param.Source == ApiParameterSource.Unknown), "The parameter source should be Unknown.");
+ }
+ }
+}
diff --git a/test/System.Web.Http.Integration.Test/ApiExplorer/RouteConstraintsTest.cs b/test/System.Web.Http.Integration.Test/ApiExplorer/RouteConstraintsTest.cs
new file mode 100644
index 00000000..48039aa2
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/ApiExplorer/RouteConstraintsTest.cs
@@ -0,0 +1,116 @@
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Web.Http.Description;
+using System.Web.Http.Dispatcher;
+using System.Web.Http.Routing;
+using Xunit.Extensions;
+
+namespace System.Web.Http.ApiExplorer
+{
+ public class RouteConstraintsTest
+ {
+ public static IEnumerable<object[]> HttpMethodConstraints_LimitsTheDescriptions_PropertyData
+ {
+ get
+ {
+ object controllerType = typeof(ItemController);
+ object expectedApiDescriptions = new List<object>
+ {
+ new { HttpMethod = HttpMethod.Get, RelativePath = "Item?name={name}&series={series}", HasRequestFormatters = false, HasResponseFormatters = true, NumberOfParameters = 2},
+ new { HttpMethod = HttpMethod.Put, RelativePath = "Item", HasRequestFormatters = true, HasResponseFormatters = true, NumberOfParameters = 1}
+ };
+ yield return new[] { controllerType, expectedApiDescriptions };
+
+ controllerType = typeof(OverloadsController);
+ expectedApiDescriptions = new List<object>
+ {
+ new { HttpMethod = HttpMethod.Get, RelativePath = "Overloads/{id}", HasRequestFormatters = false, HasResponseFormatters = true, NumberOfParameters = 1},
+ new { HttpMethod = HttpMethod.Get, RelativePath = "Overloads", HasRequestFormatters = false, HasResponseFormatters = true, NumberOfParameters = 0},
+ new { HttpMethod = HttpMethod.Get, RelativePath = "Overloads?name={name}", HasRequestFormatters = false, HasResponseFormatters = true, NumberOfParameters = 1},
+ new { HttpMethod = HttpMethod.Get, RelativePath = "Overloads/{id}?name={name}", HasRequestFormatters = false, HasResponseFormatters = true, NumberOfParameters = 2},
+ new { HttpMethod = HttpMethod.Get, RelativePath = "Overloads?name={name}&age={age}", HasRequestFormatters = false, HasResponseFormatters = true, NumberOfParameters = 2},
+ new { HttpMethod = HttpMethod.Get, RelativePath = "Overloads?name={name}&age={age}&ssn={ssn}", HasRequestFormatters = false, HasResponseFormatters = true, NumberOfParameters = 3},
+ new { HttpMethod = HttpMethod.Get, RelativePath = "Overloads/{id}?name={name}&ssn={ssn}", HasRequestFormatters = false, HasResponseFormatters = true, NumberOfParameters = 3},
+ new { HttpMethod = HttpMethod.Get, RelativePath = "Overloads?name={name}&ssn={ssn}", HasRequestFormatters = false, HasResponseFormatters = true, NumberOfParameters = 2}
+ };
+ yield return new[] { controllerType, expectedApiDescriptions };
+ }
+ }
+
+ [Theory]
+ [PropertyData("HttpMethodConstraints_LimitsTheDescriptions_PropertyData")]
+ public void HttpMethodConstraints_LimitsTheDescriptions(Type controllerType, List<object> expectedResults)
+ {
+ HttpConfiguration config = new HttpConfiguration();
+ config.Routes.MapHttpRoute("Default", "{controller}/{id}", new { id = RouteParameter.Optional }, new { routeConstraint = new HttpMethodConstraint(HttpMethod.Get, HttpMethod.Put) });
+
+ DefaultHttpControllerFactory controllerFactory = ApiExplorerHelper.GetStrictControllerFactory(config, controllerType);
+ config.ServiceResolver.SetService(typeof(IHttpControllerFactory), controllerFactory);
+
+ IApiExplorer explorer = config.ServiceResolver.GetApiExplorer();
+ ApiExplorerHelper.VerifyApiDescriptions(explorer.ApiDescriptions, expectedResults);
+ }
+
+ public static IEnumerable<object[]> RegexConstraint_LimitsTheController_PropertyData
+ {
+ get
+ {
+ object[] controllerTypes = new Type[] { typeof(OverloadsController), typeof(ItemController) };
+ object expectedApiDescriptions = new List<object>
+ {
+ new { HttpMethod = HttpMethod.Get, RelativePath = "Item?name={name}&series={series}", HasRequestFormatters = false, HasResponseFormatters = true, NumberOfParameters = 2},
+ new { HttpMethod = HttpMethod.Post, RelativePath = "Item", HasRequestFormatters = true, HasResponseFormatters = true, NumberOfParameters = 1},
+ new { HttpMethod = HttpMethod.Put, RelativePath = "Item", HasRequestFormatters = true, HasResponseFormatters = true, NumberOfParameters = 1},
+ new { HttpMethod = HttpMethod.Delete, RelativePath = "Item/{id}", HasRequestFormatters = false, HasResponseFormatters = false, NumberOfParameters = 1}
+ };
+ yield return new[] { controllerTypes, expectedApiDescriptions };
+ }
+ }
+
+ [Theory]
+ [PropertyData("RegexConstraint_LimitsTheController_PropertyData")]
+ public void RegexConstraint_LimitsTheController(Type[] controllerTypes, List<object> expectedResults)
+ {
+ HttpConfiguration config = new HttpConfiguration();
+ config.Routes.MapHttpRoute("Default", "{controller}/{id}", new { id = RouteParameter.Optional }, new { controller = "It.*" }); // controllers that start with "It"
+
+ DefaultHttpControllerFactory controllerFactory = ApiExplorerHelper.GetStrictControllerFactory(config, controllerTypes);
+ config.ServiceResolver.SetService(typeof(IHttpControllerFactory), controllerFactory);
+
+ IApiExplorer explorer = config.ServiceResolver.GetApiExplorer();
+ ApiExplorerHelper.VerifyApiDescriptions(explorer.ApiDescriptions, expectedResults);
+ }
+
+ public static IEnumerable<object[]> RegexConstraint_LimitsTheAction_PropertyData
+ {
+ get
+ {
+ object[] controllerTypes = new Type[] { typeof(OverloadsController), typeof(ItemController) };
+ object expectedApiDescriptions = new List<object>
+ {
+ new { HttpMethod = HttpMethod.Get, RelativePath = "Item/GetItem?name={name}&series={series}", HasRequestFormatters = false, HasResponseFormatters = true, NumberOfParameters = 2},
+ new { HttpMethod = HttpMethod.Get, RelativePath = "Overloads/GetPersonByNameAndId/{id}?name={name}", HasRequestFormatters = false, HasResponseFormatters = true, NumberOfParameters = 2},
+ new { HttpMethod = HttpMethod.Get, RelativePath = "Overloads/GetPersonByNameAndAge?name={name}&age={age}", HasRequestFormatters = false, HasResponseFormatters = true, NumberOfParameters = 2},
+ new { HttpMethod = HttpMethod.Get, RelativePath = "Overloads/GetPersonByNameAgeAndSsn?name={name}&age={age}&ssn={ssn}", HasRequestFormatters = false, HasResponseFormatters = true, NumberOfParameters = 3},
+ new { HttpMethod = HttpMethod.Get, RelativePath = "Overloads/GetPersonByNameIdAndSsn/{id}?name={name}&ssn={ssn}", HasRequestFormatters = false, HasResponseFormatters = true, NumberOfParameters = 3},
+ new { HttpMethod = HttpMethod.Get, RelativePath = "Overloads/GetPersonByNameAndSsn?name={name}&ssn={ssn}", HasRequestFormatters = false, HasResponseFormatters = true, NumberOfParameters = 2}
+ };
+ yield return new[] { controllerTypes, expectedApiDescriptions };
+ }
+ }
+
+ [Theory]
+ [PropertyData("RegexConstraint_LimitsTheAction_PropertyData")]
+ public void RegexConstraint_LimitsTheAction(Type[] controllerTypes, List<object> expectedResults)
+ {
+ HttpConfiguration config = new HttpConfiguration();
+ config.Routes.MapHttpRoute("Default", "{controller}/{action}/{id}", new { id = RouteParameter.Optional }, new { action = "Get.+" }); // actions that start with "Get" and at least one extra character
+
+ DefaultHttpControllerFactory controllerFactory = ApiExplorerHelper.GetStrictControllerFactory(config, controllerTypes);
+ config.ServiceResolver.SetService(typeof(IHttpControllerFactory), controllerFactory);
+
+ IApiExplorer explorer = config.ServiceResolver.GetApiExplorer();
+ ApiExplorerHelper.VerifyApiDescriptions(explorer.ApiDescriptions, expectedResults);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Integration.Test/ApiExplorer/RoutesTest.cs b/test/System.Web.Http.Integration.Test/ApiExplorer/RoutesTest.cs
new file mode 100644
index 00000000..daf8ed84
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/ApiExplorer/RoutesTest.cs
@@ -0,0 +1,203 @@
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Web.Http.Description;
+using System.Web.Http.Dispatcher;
+using Xunit;
+using Xunit.Extensions;
+
+namespace System.Web.Http.ApiExplorer
+{
+ public class RoutesTest
+ {
+ [Fact]
+ public void VerifyDescription_OnEmptyRoute()
+ {
+ HttpConfiguration config = new HttpConfiguration();
+ IApiExplorer explorer = config.ServiceResolver.GetApiExplorer();
+
+ Assert.NotNull(explorer);
+ Assert.Equal(0, explorer.ApiDescriptions.Count);
+ }
+
+ public static IEnumerable<object[]> VerifyDescription_OnDefaultRoute_PropertyData
+ {
+ get
+ {
+ object controllerType = typeof(ItemController);
+ object expectedApiDescriptions = new List<object>
+ {
+ new { HttpMethod = HttpMethod.Get, RelativePath = "Item?name={name}&series={series}", HasRequestFormatters = false, HasResponseFormatters = true, NumberOfParameters = 2},
+ new { HttpMethod = HttpMethod.Post, RelativePath = "Item", HasRequestFormatters = true, HasResponseFormatters = true, NumberOfParameters = 1},
+ new { HttpMethod = HttpMethod.Put, RelativePath = "Item", HasRequestFormatters = true, HasResponseFormatters = true, NumberOfParameters = 1},
+ new { HttpMethod = HttpMethod.Delete, RelativePath = "Item/{id}", HasRequestFormatters = false, HasResponseFormatters = false, NumberOfParameters = 1}
+ };
+ yield return new[] { controllerType, expectedApiDescriptions };
+
+ controllerType = typeof(OverloadsController);
+ expectedApiDescriptions = new List<object>
+ {
+ new { HttpMethod = HttpMethod.Get, RelativePath = "Overloads/{id}", HasRequestFormatters = false, HasResponseFormatters = true, NumberOfParameters = 1},
+ new { HttpMethod = HttpMethod.Get, RelativePath = "Overloads", HasRequestFormatters = false, HasResponseFormatters = true, NumberOfParameters = 0},
+ new { HttpMethod = HttpMethod.Get, RelativePath = "Overloads?name={name}", HasRequestFormatters = false, HasResponseFormatters = true, NumberOfParameters = 1},
+ new { HttpMethod = HttpMethod.Get, RelativePath = "Overloads/{id}?name={name}", HasRequestFormatters = false, HasResponseFormatters = true, NumberOfParameters = 2},
+ new { HttpMethod = HttpMethod.Get, RelativePath = "Overloads?name={name}&age={age}", HasRequestFormatters = false, HasResponseFormatters = true, NumberOfParameters = 2},
+ new { HttpMethod = HttpMethod.Get, RelativePath = "Overloads?name={name}&age={age}&ssn={ssn}", HasRequestFormatters = false, HasResponseFormatters = true, NumberOfParameters = 3},
+ new { HttpMethod = HttpMethod.Get, RelativePath = "Overloads/{id}?name={name}&ssn={ssn}", HasRequestFormatters = false, HasResponseFormatters = true, NumberOfParameters = 3},
+ new { HttpMethod = HttpMethod.Get, RelativePath = "Overloads?name={name}&ssn={ssn}", HasRequestFormatters = false, HasResponseFormatters = true, NumberOfParameters = 2},
+ new { HttpMethod = HttpMethod.Post, RelativePath = "Overloads", HasRequestFormatters = true, HasResponseFormatters = true, NumberOfParameters = 1},
+ new { HttpMethod = HttpMethod.Post, RelativePath = "Overloads?name={name}&age={age}", HasRequestFormatters = false, HasResponseFormatters = true, NumberOfParameters = 2},
+ new { HttpMethod = HttpMethod.Delete, RelativePath = "Overloads/{id}?name={name}", HasRequestFormatters = false, HasResponseFormatters = false, NumberOfParameters = 2},
+ new { HttpMethod = HttpMethod.Delete, RelativePath = "Overloads/{id}?name={name}&age={age}", HasRequestFormatters = false, HasResponseFormatters = false, NumberOfParameters = 3}
+ };
+ yield return new[] { controllerType, expectedApiDescriptions };
+ }
+ }
+
+ [Theory]
+ [PropertyData("VerifyDescription_OnDefaultRoute_PropertyData")]
+ public void VerifyDescription_OnDefaultRoute(Type controllerType, List<object> expectedResults)
+ {
+ HttpConfiguration config = new HttpConfiguration();
+ config.Routes.MapHttpRoute("Default", "{controller}/{id}", new { id = RouteParameter.Optional });
+
+ DefaultHttpControllerFactory controllerFactory = ApiExplorerHelper.GetStrictControllerFactory(config, controllerType);
+ config.ServiceResolver.SetService(typeof(IHttpControllerFactory), controllerFactory);
+
+ IApiExplorer explorer = config.ServiceResolver.GetApiExplorer();
+ ApiExplorerHelper.VerifyApiDescriptions(explorer.ApiDescriptions, expectedResults);
+ }
+
+ public static IEnumerable<object[]> VerifyDescription_OnRouteWithControllerOnDefaults_PropertyData
+ {
+ get
+ {
+ object controllerType = typeof(ItemController);
+ object expectedApiDescriptions = new List<object>
+ {
+ new { HttpMethod = HttpMethod.Get, RelativePath = "myitem?name={name}&series={series}", HasRequestFormatters = false, HasResponseFormatters = true, NumberOfParameters = 2},
+ new { HttpMethod = HttpMethod.Post, RelativePath = "myitem", HasRequestFormatters = true, HasResponseFormatters = true, NumberOfParameters = 1},
+ new { HttpMethod = HttpMethod.Put, RelativePath = "myitem", HasRequestFormatters = true, HasResponseFormatters = true, NumberOfParameters = 1},
+ new { HttpMethod = HttpMethod.Delete, RelativePath = "myitem/{id}", HasRequestFormatters = false, HasResponseFormatters = false, NumberOfParameters = 1}
+ };
+ yield return new[] { controllerType, expectedApiDescriptions };
+ }
+ }
+
+ [Theory]
+ [PropertyData("VerifyDescription_OnRouteWithControllerOnDefaults_PropertyData")]
+ public void VerifyDescription_OnRouteWithControllerOnDefaults(Type controllerType, List<object> expectedResults)
+ {
+ HttpConfiguration config = new HttpConfiguration();
+ config.Routes.MapHttpRoute("Default", "myitem/{id}", new { controller = "Item", id = RouteParameter.Optional });
+
+ DefaultHttpControllerFactory controllerFactory = ApiExplorerHelper.GetStrictControllerFactory(config, controllerType);
+ config.ServiceResolver.SetService(typeof(IHttpControllerFactory), controllerFactory);
+
+ IApiExplorer explorer = config.ServiceResolver.GetApiExplorer();
+ ApiExplorerHelper.VerifyApiDescriptions(explorer.ApiDescriptions, expectedResults);
+ }
+
+ public static IEnumerable<object[]> VerifyDescription_OnRouteWithActionVariable_PropertyData
+ {
+ get
+ {
+ object controllerType = typeof(ItemController);
+ object expectedApiDescriptions = new List<object>
+ {
+ new { HttpMethod = HttpMethod.Get, RelativePath = "Item/GetItem?name={name}&series={series}", HasRequestFormatters = false, HasResponseFormatters = true, NumberOfParameters = 2},
+ new { HttpMethod = HttpMethod.Post, RelativePath = "Item/PostItem", HasRequestFormatters = true, HasResponseFormatters = true, NumberOfParameters = 1},
+ new { HttpMethod = HttpMethod.Put, RelativePath = "Item/PostItem", HasRequestFormatters = true, HasResponseFormatters = true, NumberOfParameters = 1},
+ new { HttpMethod = HttpMethod.Delete, RelativePath = "Item/RemoveItem/{id}", HasRequestFormatters = false, HasResponseFormatters = false, NumberOfParameters = 1}
+ };
+ yield return new[] { controllerType, expectedApiDescriptions };
+
+ controllerType = typeof(OverloadsController);
+ expectedApiDescriptions = new List<object>
+ {
+ new { HttpMethod = HttpMethod.Get, RelativePath = "Overloads/Get/{id}", HasRequestFormatters = false, HasResponseFormatters = true, NumberOfParameters = 1},
+ new { HttpMethod = HttpMethod.Get, RelativePath = "Overloads/Get", HasRequestFormatters = false, HasResponseFormatters = true, NumberOfParameters = 0},
+ new { HttpMethod = HttpMethod.Get, RelativePath = "Overloads/Get?name={name}", HasRequestFormatters = false, HasResponseFormatters = true, NumberOfParameters = 1},
+ new { HttpMethod = HttpMethod.Get, RelativePath = "Overloads/GetPersonByNameAndId/{id}?name={name}", HasRequestFormatters = false, HasResponseFormatters = true, NumberOfParameters = 2},
+ new { HttpMethod = HttpMethod.Get, RelativePath = "Overloads/GetPersonByNameAndAge?name={name}&age={age}", HasRequestFormatters = false, HasResponseFormatters = true, NumberOfParameters = 2},
+ new { HttpMethod = HttpMethod.Get, RelativePath = "Overloads/GetPersonByNameAgeAndSsn?name={name}&age={age}&ssn={ssn}", HasRequestFormatters = false, HasResponseFormatters = true, NumberOfParameters = 3},
+ new { HttpMethod = HttpMethod.Get, RelativePath = "Overloads/GetPersonByNameIdAndSsn/{id}?name={name}&ssn={ssn}", HasRequestFormatters = false, HasResponseFormatters = true, NumberOfParameters = 3},
+ new { HttpMethod = HttpMethod.Get, RelativePath = "Overloads/GetPersonByNameAndSsn?name={name}&ssn={ssn}", HasRequestFormatters = false, HasResponseFormatters = true, NumberOfParameters = 2},
+ new { HttpMethod = HttpMethod.Post, RelativePath = "Overloads/Post", HasRequestFormatters = true, HasResponseFormatters = true, NumberOfParameters = 1},
+ new { HttpMethod = HttpMethod.Post, RelativePath = "Overloads/Post?name={name}&age={age}", HasRequestFormatters = false, HasResponseFormatters = true, NumberOfParameters = 2},
+ new { HttpMethod = HttpMethod.Delete, RelativePath = "Overloads/Delete/{id}?name={name}", HasRequestFormatters = false, HasResponseFormatters = false, NumberOfParameters = 2},
+ new { HttpMethod = HttpMethod.Delete, RelativePath = "Overloads/Delete/{id}?name={name}&age={age}", HasRequestFormatters = false, HasResponseFormatters = false, NumberOfParameters = 3}
+ };
+ yield return new[] { controllerType, expectedApiDescriptions };
+ }
+ }
+
+ [Theory]
+ [PropertyData("VerifyDescription_OnRouteWithActionVariable_PropertyData")]
+ public void VerifyDescription_OnRouteWithActionVariable(Type controllerType, List<object> expectedResults)
+ {
+ HttpConfiguration config = new HttpConfiguration();
+ config.Routes.MapHttpRoute("Default", "{controller}/{action}/{id}", new { id = RouteParameter.Optional });
+
+ DefaultHttpControllerFactory controllerFactory = ApiExplorerHelper.GetStrictControllerFactory(config, controllerType);
+ config.ServiceResolver.SetService(typeof(IHttpControllerFactory), controllerFactory);
+
+ IApiExplorer explorer = config.ServiceResolver.GetApiExplorer();
+ ApiExplorerHelper.VerifyApiDescriptions(explorer.ApiDescriptions, expectedResults);
+ }
+
+ public static IEnumerable<object[]> VerifyDescription_On_RouteWithActionOnDefaults_PropertyData
+ {
+ get
+ {
+ object controllerType = typeof(ItemController);
+ object expectedApiDescriptions = new List<object>
+ {
+ new { HttpMethod = HttpMethod.Delete, RelativePath = "Item/{id}", HasRequestFormatters = false, HasResponseFormatters = false, NumberOfParameters = 1}
+ };
+ yield return new[] { controllerType, expectedApiDescriptions };
+ }
+ }
+
+ [Theory]
+ [PropertyData("VerifyDescription_On_RouteWithActionOnDefaults_PropertyData")]
+ public void VerifyDescription_On_RouteWithActionOnDefaults(Type controllerType, List<object> expectedResults)
+ {
+ HttpConfiguration config = new HttpConfiguration();
+ config.Routes.MapHttpRoute("Default", "{controller}/{id}", new { action = "RemoveItem", id = RouteParameter.Optional });
+
+ DefaultHttpControllerFactory controllerFactory = ApiExplorerHelper.GetStrictControllerFactory(config, controllerType);
+ config.ServiceResolver.SetService(typeof(IHttpControllerFactory), controllerFactory);
+
+ IApiExplorer explorer = config.ServiceResolver.GetApiExplorer();
+ ApiExplorerHelper.VerifyApiDescriptions(explorer.ApiDescriptions, expectedResults);
+ }
+
+ [Fact]
+ public void InvalidActionNameOnRoute_DoesNotThrow()
+ {
+ Type controllerType = typeof(OverloadsController);
+ HttpConfiguration config = new HttpConfiguration();
+ config.Routes.MapHttpRoute("Default", "{controller}/{id}", new { action = "ActionThatDoesNotExist", id = RouteParameter.Optional });
+
+ DefaultHttpControllerFactory controllerFactory = ApiExplorerHelper.GetStrictControllerFactory(config, controllerType);
+ config.ServiceResolver.SetService(typeof(IHttpControllerFactory), controllerFactory);
+
+ IApiExplorer explorer = config.ServiceResolver.GetApiExplorer();
+ Assert.Empty(explorer.ApiDescriptions);
+ }
+
+ [Fact]
+ public void InvalidControllerNameOnRoute_DoesNotThrow()
+ {
+ Type controllerType = typeof(OverloadsController);
+ HttpConfiguration config = new HttpConfiguration();
+ config.Routes.MapHttpRoute("Default", "mycontroller/{id}", new { controller = "ControllerThatDoesNotExist", id = RouteParameter.Optional });
+
+ DefaultHttpControllerFactory controllerFactory = ApiExplorerHelper.GetStrictControllerFactory(config, controllerType);
+ config.ServiceResolver.SetService(typeof(IHttpControllerFactory), controllerFactory);
+
+ IApiExplorer explorer = config.ServiceResolver.GetApiExplorer();
+ Assert.Empty(explorer.ApiDescriptions);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Integration.Test/Authentication/BasicOverHttpTest.cs b/test/System.Web.Http.Integration.Test/Authentication/BasicOverHttpTest.cs
new file mode 100644
index 00000000..990cba36
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/Authentication/BasicOverHttpTest.cs
@@ -0,0 +1,74 @@
+using System.Net;
+using System.Net.Http;
+using System.Web.Http.SelfHost;
+using Xunit;
+
+namespace System.Web.Http
+{
+ public class BasicOverHttpTest
+ {
+ private static readonly string BaseAddress = "http://localhost:8080";
+
+ [Fact]
+ public void AuthenticateWithUsernameTokenSucceed()
+ {
+ RunBasicAuthTest("Sample", "", new NetworkCredential("username", "password"),
+ (response) => Assert.Equal(HttpStatusCode.OK, response.StatusCode)
+ );
+ }
+
+ [Fact]
+ public void AuthenticateWithWrongPasswordFail()
+ {
+ RunBasicAuthTest("Sample", "", new NetworkCredential("username", "wrong password"),
+ (response) => Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode)
+ );
+ }
+
+ [Fact]
+ public void AuthenticateWithNoCredentialFail()
+ {
+ RunBasicAuthTest("Sample", "", null,
+ (response) => Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode)
+ );
+ }
+
+ private static void RunBasicAuthTest(string controllerName, string routeSuffix, NetworkCredential credential, Action<HttpResponseMessage> assert)
+ {
+ // Arrange
+ HttpSelfHostConfiguration config = new HttpSelfHostConfiguration(BaseAddress);
+ config.Routes.MapHttpRoute("Default", "{controller}" + routeSuffix, new { controller = controllerName });
+ config.UserNamePasswordValidator = new CustomUsernamePasswordValidator();
+ config.MessageHandlers.Add(new CustomMessageHandler());
+ HttpSelfHostServer server = new HttpSelfHostServer(config);
+
+ server.OpenAsync().Wait();
+
+ // Create a GET request with correct username and password
+ HttpClientHandler handler = new HttpClientHandler();
+ handler.Credentials = credential;
+ HttpClient client = new HttpClient(handler);
+
+ HttpResponseMessage response = null;
+ try
+ {
+ // Act
+ response = client.GetAsync(BaseAddress).Result;
+
+ // Assert
+ assert(response);
+ }
+ finally
+ {
+ if (response != null)
+ {
+ response.Dispose();
+ }
+ client.Dispose();
+ }
+
+ server.CloseAsync().Wait();
+ }
+
+ }
+}
diff --git a/test/System.Web.Http.Integration.Test/Authentication/CustomMessageHandler.cs b/test/System.Web.Http.Integration.Test/Authentication/CustomMessageHandler.cs
new file mode 100644
index 00000000..ebcb56a9
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/Authentication/CustomMessageHandler.cs
@@ -0,0 +1,33 @@
+using System.Net.Http;
+using System.Security.Principal;
+using System.ServiceModel;
+using System.ServiceModel.Security;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Hosting;
+using System.Web.Http.SelfHost;
+
+namespace System.Web.Http
+{
+ public class CustomMessageHandler : DelegatingHandler
+ {
+ protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ // here you can see the requestor's identity via the request message
+ // convert the Generic Identity to some IPrincipal object, and set it in the request's property
+ // later the authorization filter will use the role information to authorize request.
+ SecurityMessageProperty property;
+ if (request.Properties.TryGetValue<SecurityMessageProperty>(HttpSelfHostServer.SecurityKey, out property))
+ {
+ ServiceSecurityContext context = property.ServiceSecurityContext;
+
+ if (context.PrimaryIdentity.Name == "username")
+ {
+ request.Properties.Add(HttpPropertyKeys.UserPrincipalKey, new GenericPrincipal(context.PrimaryIdentity, new string[] { "Administrators" }));
+ }
+ }
+
+ return base.SendAsync(request, cancellationToken);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Integration.Test/Authentication/CustomUsernamePasswordValidator.cs b/test/System.Web.Http.Integration.Test/Authentication/CustomUsernamePasswordValidator.cs
new file mode 100644
index 00000000..f4bb589d
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/Authentication/CustomUsernamePasswordValidator.cs
@@ -0,0 +1,19 @@
+using System.IdentityModel.Selectors;
+
+namespace System.Web.Http
+{
+ public class CustomUsernamePasswordValidator : UserNamePasswordValidator
+ {
+ public override void Validate(string userName, string password)
+ {
+ if (userName == "username" && password == "password")
+ {
+ return;
+ }
+ else
+ {
+ throw new InvalidOperationException();
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Http.Integration.Test/Authentication/RequireAdminAttribute.cs b/test/System.Web.Http.Integration.Test/Authentication/RequireAdminAttribute.cs
new file mode 100644
index 00000000..9ba6d270
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/Authentication/RequireAdminAttribute.cs
@@ -0,0 +1,21 @@
+using System.Net;
+using System.Net.Http;
+using System.Security.Principal;
+using System.Web.Http.Controllers;
+using System.Web.Http.Filters;
+
+namespace System.Web.Http
+{
+ public class RequireAdminAttribute : AuthorizationFilterAttribute
+ {
+ public override void OnAuthorization(HttpActionContext context)
+ {
+ // do authorization based on the principle.
+ IPrincipal principal = context.ControllerContext.Request.GetUserPrincipal() as IPrincipal;
+ if (principal == null || !principal.IsInRole("Administrators"))
+ {
+ context.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/test/System.Web.Http.Integration.Test/Authentication/SampleController.cs b/test/System.Web.Http.Integration.Test/Authentication/SampleController.cs
new file mode 100644
index 00000000..8ee582ac
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/Authentication/SampleController.cs
@@ -0,0 +1,14 @@
+namespace System.Web.Http
+{
+ /// <summary>
+ /// Sample ApiControler
+ /// </summary>
+ public class SampleController : ApiController
+ {
+ [RequireAdmin]
+ public string Get()
+ {
+ return "hello";
+ }
+ }
+} \ No newline at end of file
diff --git a/test/System.Web.Http.Integration.Test/ContentNegotiation/AcceptHeaderTests.cs b/test/System.Web.Http.Integration.Test/ContentNegotiation/AcceptHeaderTests.cs
new file mode 100644
index 00000000..102b56d2
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/ContentNegotiation/AcceptHeaderTests.cs
@@ -0,0 +1,30 @@
+using System.Net.Http;
+using System.Net.Http.Headers;
+using Xunit;
+using Xunit.Extensions;
+
+namespace System.Web.Http.ContentNegotiation
+{
+ public class AcceptHeaderTests : ContentNegotiationTestBase
+ {
+ [Theory]
+ [InlineData("application/json")]
+ [InlineData("application/xml")]
+ public void Response_Contains_ContentType(string contentType)
+ {
+ // Arrange
+ MediaTypeWithQualityHeaderValue requestContentType = new MediaTypeWithQualityHeaderValue(contentType);
+ MediaTypeHeaderValue responseContentType = null;
+
+ // Act
+ HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, baseUri);
+ request.Headers.Accept.Add(requestContentType);
+ HttpResponseMessage response = httpClient.SendAsync(request).Result;
+ response.EnsureSuccessStatusCode();
+ responseContentType = response.Content.Headers.ContentType;
+
+ // Assert
+ Assert.Equal(requestContentType.MediaType, responseContentType.MediaType);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Integration.Test/ContentNegotiation/ConnegController.cs b/test/System.Web.Http.Integration.Test/ContentNegotiation/ConnegController.cs
new file mode 100644
index 00000000..a5ebbbee
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/ContentNegotiation/ConnegController.cs
@@ -0,0 +1,19 @@
+namespace System.Web.Http.ContentNegotiation
+{
+ public class ConnegController : ApiController
+ {
+ public ConnegItem GetItem(string name = "Fido", int age = 3)
+ {
+ return new ConnegItem()
+ {
+ Name = name,
+ Age = age
+ };
+ }
+
+ public ConnegItem PostItem(ConnegItem item)
+ {
+ return item;
+ }
+ }
+}
diff --git a/test/System.Web.Http.Integration.Test/ContentNegotiation/ConnegItem.cs b/test/System.Web.Http.Integration.Test/ContentNegotiation/ConnegItem.cs
new file mode 100644
index 00000000..c6bd0862
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/ContentNegotiation/ConnegItem.cs
@@ -0,0 +1,8 @@
+namespace System.Web.Http.ContentNegotiation
+{
+ public class ConnegItem
+ {
+ public string Name { get; set; }
+ public int Age { get; set; }
+ }
+}
diff --git a/test/System.Web.Http.Integration.Test/ContentNegotiation/ContentNegotiationTestBase.cs b/test/System.Web.Http.Integration.Test/ContentNegotiation/ContentNegotiationTestBase.cs
new file mode 100644
index 00000000..5772a2af
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/ContentNegotiation/ContentNegotiationTestBase.cs
@@ -0,0 +1,46 @@
+using System.Net.Http;
+using System.Web.Http.SelfHost;
+
+namespace System.Web.Http.ContentNegotiation
+{
+ public class ContentNegotiationTestBase : IDisposable
+ {
+ protected readonly string baseUri = "http://localhost:8080/Conneg";
+ protected HttpSelfHostServer server = null;
+ protected HttpSelfHostConfiguration configuration = null;
+ protected HttpClient httpClient = null;
+
+ public ContentNegotiationTestBase()
+ {
+ this.SetupHost();
+ }
+
+ public void Dispose()
+ {
+ this.CleanupHost();
+ }
+
+ public void SetupHost()
+ {
+ configuration = new HttpSelfHostConfiguration(baseUri);
+ configuration.Routes.MapHttpRoute("Default", "{controller}", new { controller = "Conneg" });
+ server = new HttpSelfHostServer(configuration);
+ server.OpenAsync().Wait();
+
+ httpClient = new HttpClient();
+ }
+
+ public void CleanupHost()
+ {
+ if (server != null)
+ {
+ server.CloseAsync().Wait();
+ }
+
+ if (httpClient != null)
+ {
+ httpClient.Dispose();
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Http.Integration.Test/ContentNegotiation/CustomFormatterTests.cs b/test/System.Web.Http.Integration.Test/ContentNegotiation/CustomFormatterTests.cs
new file mode 100644
index 00000000..266559e0
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/ContentNegotiation/CustomFormatterTests.cs
@@ -0,0 +1,299 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Formatting;
+using System.Net.Http.Headers;
+using System.Threading.Tasks;
+using System.Web.Http.SelfHost;
+using Xunit;
+
+namespace System.Web.Http.ContentNegotiation
+{
+ public class CustomFormatterTests : IDisposable
+ {
+ private HttpSelfHostServer server = null;
+ private string baseAddress = null;
+ private HttpClient httpClient = null;
+ private HttpSelfHostConfiguration config = null;
+
+ public CustomFormatterTests()
+ {
+ SetupHost();
+ }
+
+ public void Dispose()
+ {
+ this.CleanupHost();
+ }
+
+ [Fact]
+ public void CustomFormatter_Overrides_SetResponseHeaders_During_Conneg()
+ {
+ Order reqOrdr = new Order() { OrderId = "100", OrderValue = 100.00 };
+ HttpRequestMessage request = new HttpRequestMessage
+ {
+ Content = new ObjectContent<Order>(reqOrdr, new XmlMediaTypeFormatter())
+ };
+ request.RequestUri = new Uri(baseAddress + "/CustomFormatterTests/EchoOrder");
+ request.Method = HttpMethod.Post;
+ request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/plainwithversioninfo"));
+
+ HttpResponseMessage response = httpClient.SendAsync(request).Result;
+
+ response.EnsureSuccessStatusCode();
+
+ IEnumerable<string> versionHdr = null;
+ Assert.True(response.Headers.TryGetValues("Version", out versionHdr));
+ Assert.Equal<string>("1.3.5.0", versionHdr.First());
+ Assert.NotNull(response.Content);
+ Assert.NotNull(response.Content.Headers.ContentType);
+ Assert.Equal<string>("text/plainwithversioninfo", response.Content.Headers.ContentType.MediaType);
+ }
+
+ [Fact]
+ public void CustomFormatter_Post_Returns_Request_String_Content()
+ {
+ HttpRequestMessage request = new HttpRequestMessage
+ {
+ Content = new ObjectContent<string>("Hello World!", new PlainTextFormatter())
+ };
+ request.RequestUri = new Uri(baseAddress + "/CustomFormatterTests/EchoString");
+ request.Method = HttpMethod.Post;
+
+ HttpResponseMessage response = httpClient.SendAsync(request).Result;
+
+ response.EnsureSuccessStatusCode();
+ Assert.NotNull(response.Content);
+ Assert.NotNull(response.Content.Headers.ContentType);
+ Assert.Equal<string>("text/plain", response.Content.Headers.ContentType.MediaType);
+ Assert.Equal<string>("Hello World!", response.Content.ReadAsStringAsync().Result);
+ }
+
+ [Fact(Skip = "Test update needed, uses IKeyValueModel")]
+ public void CustomFormatter_Post_Returns_Request_Integer_Content()
+ {
+ HttpRequestMessage request = new HttpRequestMessage
+ {
+ Content = new ObjectContent<int>(100, new PlainTextFormatter())
+ };
+
+ request.RequestUri = new Uri(baseAddress + "/CustomFormatterTests/EchoInt");
+ request.Method = HttpMethod.Post;
+
+ HttpResponseMessage response = httpClient.SendAsync(request).Result;
+
+ response.EnsureSuccessStatusCode();
+ Assert.NotNull(response.Content);
+ Assert.NotNull(response.Content.Headers.ContentType);
+ Assert.Equal<string>("text/plain", response.Content.Headers.ContentType.MediaType);
+ Assert.Equal<int>(100, Convert.ToInt32(response.Content.ReadAsStringAsync().Result));
+ }
+
+
+ [Fact(Skip = "Test update needed, uses IKeyValueModel")]
+ public void CustomFormatter_Post_Returns_Request_ComplexType_Content()
+ {
+ Order reqOrdr = new Order() { OrderId = "100", OrderValue = 100.00 };
+ HttpRequestMessage request = new HttpRequestMessage
+ {
+ Content = new ObjectContent<Order>(reqOrdr, new PlainTextFormatter())
+ };
+ request.RequestUri = new Uri(baseAddress + "/CustomFormatterTests/EchoOrder");
+ request.Method = HttpMethod.Post;
+
+ HttpResponseMessage response = httpClient.SendAsync(request).Result;
+
+ response.EnsureSuccessStatusCode();
+ Assert.NotNull(response.Content);
+ Assert.NotNull(response.Content.Headers.ContentType);
+ Assert.Equal<string>("text/plain", response.Content.Headers.ContentType.MediaType);
+ }
+
+ private void SetupHost()
+ {
+ httpClient = new HttpClient();
+ baseAddress = String.Format("http://{0}", Environment.MachineName);
+ config = new HttpSelfHostConfiguration(baseAddress);
+ config.Routes.MapHttpRoute("Default", "{controller}/{action}", new { controller = "CustomFormatterTests", action = "EchoOrder" });
+ config.Formatters.Add(new PlainTextFormatterWithVersionInfo());
+ config.Formatters.Add(new PlainTextFormatter());
+
+ server = new HttpSelfHostServer(config);
+ server.OpenAsync().Wait();
+ }
+
+ private void CleanupHost()
+ {
+ if (server != null)
+ {
+ server.CloseAsync().Wait();
+ }
+ }
+ }
+
+ public class PlainTextFormatterWithVersionInfo : MediaTypeFormatter
+ {
+ public PlainTextFormatterWithVersionInfo()
+ {
+ this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/plainwithversioninfo"));
+ }
+
+ public override bool CanReadType(Type type)
+ {
+ return true;
+ }
+
+ public override bool CanWriteType(Type type)
+ {
+ return true;
+ }
+
+ public override void SetDefaultContentHeaders(Type objectType, HttpContentHeaders contentHeaders, string mediaType)
+ {
+ base.SetDefaultContentHeaders(objectType, contentHeaders, mediaType);
+ contentHeaders.TryAddWithoutValidation("Version", "1.3.5.0");
+ }
+
+ public override Task<object> ReadFromStreamAsync(Type type, Stream stream, HttpContentHeaders contentHeaders, IFormatterLogger formatterLogger)
+ {
+ string content = null;
+
+ using (var reader = new StreamReader(stream))
+ {
+ content = reader.ReadToEnd();
+ }
+
+ TaskCompletionSource<object> tcs = new TaskCompletionSource<object>();
+ tcs.SetResult(type == typeof(IKeyValueModel) ? (object)new StringKeyValueModel(content) : content);
+
+ return tcs.Task;
+ }
+
+ public override Task WriteToStreamAsync(Type type, object value, Stream stream, HttpContentHeaders contentHeaders, TransportContext transportContext)
+ {
+ var output = value.ToString();
+ var writer = new StreamWriter(stream);
+ writer.Write(output);
+ writer.Flush();
+
+ TaskCompletionSource<object> tcs = new TaskCompletionSource<object>();
+ tcs.SetResult(null);
+
+ return tcs.Task;
+ }
+ }
+
+ public class PlainTextFormatter : MediaTypeFormatter
+ {
+ public PlainTextFormatter()
+ {
+ this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/plain"));
+ }
+
+ public override bool CanReadType(Type type)
+ {
+ return true;
+ }
+
+ public override bool CanWriteType(Type type)
+ {
+ return true;
+ }
+
+ public override Task<object> ReadFromStreamAsync(Type type, Stream stream, HttpContentHeaders contentHeaders, IFormatterLogger formatterLogger)
+ {
+ string content = null;
+
+ using (var reader = new StreamReader(stream))
+ {
+ content = reader.ReadToEnd();
+ }
+
+ TaskCompletionSource<object> tcs = new TaskCompletionSource<object>();
+ tcs.SetResult(type == typeof(IKeyValueModel) ? (object)new StringKeyValueModel(content) : content);
+ return tcs.Task;
+ }
+
+ public override Task WriteToStreamAsync(Type type, object value, Stream stream, HttpContentHeaders contentHeaders, TransportContext transportContext)
+ {
+ var output = value == null ? String.Empty : value.ToString();
+ var writer = new StreamWriter(stream);
+ writer.Write(output);
+ writer.Flush();
+
+ TaskCompletionSource<object> tcs = new TaskCompletionSource<object>();
+ tcs.SetResult(null);
+ return tcs.Task;
+ }
+ }
+
+ public class CustomFormatterTestsController : ApiController
+ {
+ [HttpPost]
+ public string EchoString([FromBody] string input)
+ {
+ return input;
+ }
+
+ [HttpPost]
+ public int EchoInt([FromBody] int input)
+ {
+ return input;
+ }
+
+ [HttpPost]
+ public Order EchoOrder(Order order)
+ {
+ return order;
+ }
+ }
+
+ public class StringKeyValueModel : IKeyValueModel
+ {
+ private string _value;
+ public StringKeyValueModel(string value)
+ {
+ _value = value;
+ }
+
+ public bool TryGetValue(string key, out object value)
+ {
+ if (key.Length == 0)
+ {
+ value = _value;
+ return true;
+ }
+
+ value = null;
+ return false;
+ }
+
+ public IEnumerable<string> Keys
+ {
+ get { return new string[0]; }
+ }
+ }
+
+ public class Order : IEquatable<Order>, ICloneable
+ {
+ public string OrderId { get; set; }
+ public double OrderValue { get; set; }
+
+ public bool Equals(Order other)
+ {
+ return (this.OrderId.Equals(other.OrderId) && this.OrderValue.Equals(other.OrderValue));
+ }
+
+ public object Clone()
+ {
+ return this.MemberwiseClone();
+ }
+
+ public override string ToString()
+ {
+ return String.Format("OrderId:{0}, OrderValue:{1}", OrderId, OrderValue);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Integration.Test/ContentNegotiation/DefaultContentNegotiatorTests.cs b/test/System.Web.Http.Integration.Test/ContentNegotiation/DefaultContentNegotiatorTests.cs
new file mode 100644
index 00000000..70d6dae7
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/ContentNegotiation/DefaultContentNegotiatorTests.cs
@@ -0,0 +1,37 @@
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Net.Http.Formatting;
+using System.Net.Http.Headers;
+using Moq;
+using Xunit;
+
+namespace System.Web.Http.ContentNegotiation
+{
+ public class DefaultContentNegotiatorTests : ContentNegotiationTestBase
+ {
+ [Fact]
+ public void Custom_ContentNegotiator_Used_In_Response()
+ {
+ // Arrange
+ configuration.Formatters.Clear();
+ MediaTypeWithQualityHeaderValue requestContentType = new MediaTypeWithQualityHeaderValue("application/xml");
+ MediaTypeHeaderValue responseContentType = null;
+
+ Mock<IContentNegotiator> selector = new Mock<IContentNegotiator>();
+ MediaTypeHeaderValue mediaType;
+ selector.Setup(s => s.Negotiate(It.IsAny<Type>(), It.IsAny<HttpRequestMessage>(), It.IsAny<IEnumerable<MediaTypeFormatter>>(), out mediaType)).Returns(new XmlMediaTypeFormatter());
+
+ configuration.ServiceResolver.SetService(typeof(IContentNegotiator), selector.Object);
+
+ // Act
+ HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, baseUri);
+ request.Headers.Accept.Add(requestContentType);
+ HttpResponseMessage response = httpClient.SendAsync(request).Result;
+ response.EnsureSuccessStatusCode();
+ responseContentType = response.Content.Headers.ContentType;
+
+ // Assert
+ selector.Verify(s => s.Negotiate(It.IsAny<Type>(), It.IsAny<HttpRequestMessage>(), It.IsAny<IEnumerable<MediaTypeFormatter>>(), out mediaType), Times.AtLeastOnce());
+ }
+ }
+}
diff --git a/test/System.Web.Http.Integration.Test/ContentNegotiation/HttpResponseReturnTests.cs b/test/System.Web.Http.Integration.Test/ContentNegotiation/HttpResponseReturnTests.cs
new file mode 100644
index 00000000..a63d0c18
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/ContentNegotiation/HttpResponseReturnTests.cs
@@ -0,0 +1,132 @@
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Formatting;
+using System.Net.Http.Headers;
+using System.Web.Http.SelfHost;
+using Xunit;
+using Xunit.Extensions;
+
+namespace System.Web.Http.ContentNegotiation
+{
+ public class HttpResponseReturnTests : IDisposable
+ {
+ private HttpSelfHostServer server = null;
+ private string baseAddress = null;
+ private HttpClient httpClient = null;
+
+ public HttpResponseReturnTests()
+ {
+ this.SetupHost();
+ }
+
+ public void Dispose()
+ {
+ this.CleanupHost();
+ }
+
+ [Theory]
+ [InlineData("ReturnHttpResponseMessage")]
+ [InlineData("ReturnHttpResponseMessageAsObject")]
+ [InlineData("ReturnObjectContentOfT")]
+ [InlineData("ReturnObjectContent")]
+ [InlineData("ReturnString")]
+ public void ActionReturnsHttpResponseMessage(string action)
+ {
+ string expectedResponseValue = "<?xml version='1.0' encoding='utf-8'?><string>Hello</string>".Replace('\'', '"');
+
+ HttpRequestMessage request = new HttpRequestMessage();
+ request.RequestUri = new Uri(baseAddress + String.Format("HttpResponseReturn/{0}", action));
+ request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml"));
+ request.Method = HttpMethod.Get;
+
+ HttpResponseMessage response = httpClient.SendAsync(request).Result;
+
+ Assert.NotNull(response.Content);
+ Assert.NotNull(response.Content.Headers.ContentType);
+ Assert.Equal<string>("application/xml", response.Content.Headers.ContentType.MediaType);
+ Assert.Equal<string>(expectedResponseValue, response.Content.ReadAsStringAsync().Result);
+ }
+
+ [Theory]
+ [InlineData("ReturnHttpResponseMessageAsXml")]
+ public void ActionReturnsHttpResponseMessageWithExplicitMediaType(string action)
+ {
+ string expectedResponseValue = "<?xml version='1.0' encoding='utf-8'?><string>Hello</string>".Replace('\'', '"');
+
+ HttpRequestMessage request = new HttpRequestMessage();
+ request.RequestUri = new Uri(baseAddress + String.Format("HttpResponseReturn/{0}", action));
+ request.Method = HttpMethod.Get;
+
+ HttpResponseMessage response = httpClient.SendAsync(request).Result;
+
+ Assert.NotNull(response.Content);
+ Assert.NotNull(response.Content.Headers.ContentType);
+ Assert.Equal<string>("application/xml", response.Content.Headers.ContentType.MediaType);
+ Assert.Equal<string>(expectedResponseValue, response.Content.ReadAsStringAsync().Result);
+ }
+
+ public void SetupHost()
+ {
+ httpClient = new HttpClient();
+
+ baseAddress = String.Format("http://{0}/", Environment.MachineName);
+
+ HttpSelfHostConfiguration config = new HttpSelfHostConfiguration(baseAddress);
+ config.Routes.MapHttpRoute("Default", "{controller}/{action}", new { controller = "HttpResponseReturn" });
+
+ server = new HttpSelfHostServer(config);
+ server.OpenAsync().Wait();
+ }
+
+ public void CleanupHost()
+ {
+ if (server != null)
+ {
+ server.CloseAsync().Wait();
+ }
+ }
+ }
+
+ public class HttpResponseReturnController : ApiController
+ {
+ [HttpGet]
+ public HttpResponseMessage ReturnHttpResponseMessage()
+ {
+ return Request.CreateResponse(HttpStatusCode.OK, "Hello");
+ }
+
+ [HttpGet]
+ public object ReturnHttpResponseMessageAsObject()
+ {
+ return ReturnHttpResponseMessage();
+ }
+
+ [HttpGet]
+ public HttpResponseMessage ReturnHttpResponseMessageAsXml()
+ {
+ HttpResponseMessage response = new HttpResponseMessage()
+ {
+ Content = new ObjectContent<string>("Hello", new XmlMediaTypeFormatter())
+ };
+ return response;
+ }
+
+ [HttpGet]
+ public ObjectContent<string> ReturnObjectContentOfT()
+ {
+ return new ObjectContent<string>("Hello", new XmlMediaTypeFormatter());
+ }
+
+ [HttpGet]
+ public ObjectContent ReturnObjectContent()
+ {
+ return new ObjectContent(typeof(string), "Hello", new XmlMediaTypeFormatter());
+ }
+
+ [HttpGet]
+ public string ReturnString()
+ {
+ return "Hello";
+ }
+ }
+} \ No newline at end of file
diff --git a/test/System.Web.Http.Integration.Test/Controllers/ActionAttributesTest.cs b/test/System.Web.Http.Integration.Test/Controllers/ActionAttributesTest.cs
new file mode 100644
index 00000000..9c061ab3
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/Controllers/ActionAttributesTest.cs
@@ -0,0 +1,138 @@
+using System.Web.Http.Controllers;
+using System.Web.Http.Properties;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http
+{
+ [CLSCompliant(false)]
+ public class ActionAttributesTest
+ {
+ [Theory]
+ [InlineData("GET", "ActionAttributeTest/RetriveUsers", "RetriveUsers")]
+ [InlineData("POST", "ActionAttributeTest/AddUsers", "AddUsers")]
+ [InlineData("PUT", "ActionAttributeTest/UpdateUsers", "UpdateUsers")]
+ [InlineData("DELETE", "ActionAttributeTest/RemoveUsers", "RemoveUsers")]
+ [InlineData("PATCH", "ActionAttributeTest/Users", "Users")]
+ [InlineData("HEAD", "ActionAttributeTest/Users", "Users")]
+ [InlineData("GET", "ActionAttributeTest/Deny", "Deny")]
+ [InlineData("POST", "ActionAttributeTest/Deny", "Deny")]
+ [InlineData("PUT", "ActionAttributeTest/Deny", "Deny")]
+ [InlineData("DELETE", "ActionAttributeTest/Deny", "Deny")]
+ [InlineData("PATCH", "ActionAttributeTest/Deny", "Deny")]
+ [InlineData("WHATEVER", "ActionAttributeTest/Deny", "Deny")]
+ [InlineData("GET", "ActionAttributeTest/Approve", "Approve")]
+ [InlineData("POST", "ActionAttributeTest/Approve", "Approve")]
+ [InlineData("PUT", "ActionAttributeTest/Approve", "Approve")]
+ [InlineData("DELETE", "ActionAttributeTest/Approve", "Approve")]
+ [InlineData("PATCH", "ActionAttributeTest/Approve", "Approve")]
+ [InlineData("WHATEVER", "ActionAttributeTest/Approve", "Approve")]
+ public void SelectAction_OnRouteWithActionParameter(string httpMethod, string requestUrl, string expectedActionName)
+ {
+ string routeUrl = "{controller}/{action}/{id}";
+ object routeDefault = new { id = RouteParameter.Optional };
+
+ HttpControllerContext controllerContext = ApiControllerHelper.CreateControllerContext(httpMethod, requestUrl, routeUrl, routeDefault);
+ controllerContext.ControllerDescriptor = new HttpControllerDescriptor(controllerContext.Configuration, "test", typeof(ActionAttributeTestController));
+ HttpActionDescriptor descriptor = ApiControllerHelper.SelectAction(controllerContext);
+
+ Assert.Equal<string>(expectedActionName, descriptor.ActionName);
+ }
+
+ [Theory]
+ [InlineData("POST", "ActionAttributeTest/RetriveUsers")]
+ [InlineData("DELETE", "ActionAttributeTest/RetriveUsers")]
+ [InlineData("WHATEVER", "ActionAttributeTest/RetriveUsers")]
+ [InlineData("GET", "ActionAttributeTest/AddUsers")]
+ [InlineData("PUT", "ActionAttributeTest/AddUsers")]
+ [InlineData("WHATEVER", "ActionAttributeTest/AddUsers")]
+ [InlineData("GET", "ActionAttributeTest/UpdateUsers")]
+ [InlineData("WHATEVER", "ActionAttributeTest/UpdateUsers")]
+ [InlineData("POST", "ActionAttributeTest/RemoveUsers")]
+ [InlineData("DELETEME", "ActionAttributeTest/RemoveUsers")]
+ [InlineData("GET", "ActionAttributeTest/Users")]
+ [InlineData("POST", "ActionAttributeTest/Users")]
+ [InlineData("PATCHING", "ActionAttributeTest/Users")]
+ public void SelectAction_ThrowsMethodNotSupported_OnRouteWithActionParameter(string httpMethod, string requestUrl)
+ {
+ string routeUrl = "{controller}/{action}/{id}";
+ object routeDefault = new { id = RouteParameter.Optional };
+ HttpControllerContext controllerContext = ApiControllerHelper.CreateControllerContext(httpMethod, requestUrl, routeUrl, routeDefault);
+ Type controllerType = typeof(ActionAttributeTestController);
+ controllerContext.ControllerDescriptor = new HttpControllerDescriptor(controllerContext.Configuration, controllerType.Name, controllerType);
+
+ Assert.Throws<HttpResponseException>(() =>
+ {
+ HttpActionDescriptor descriptor = ApiControllerHelper.SelectAction(controllerContext);
+ },
+ String.Format(SRResources.ApiControllerActionSelector_HttpMethodNotSupported, httpMethod));
+ }
+
+ [Theory]
+ [InlineData("GET", "ActionAttributeTest/NonAction")]
+ [InlineData("POST", "ActionAttributeTest/NonAction")]
+ [InlineData("NonAction", "ActionAttributeTest/NonAction")]
+ public void SelectAction_ThrowsNotFound_OnRouteWithActionParameter(string httpMethod, string requestUrl)
+ {
+ string routeUrl = "{controller}/{action}/{id}";
+ object routeDefault = new { id = RouteParameter.Optional };
+ HttpControllerContext controllerContext = ApiControllerHelper.CreateControllerContext(httpMethod, requestUrl, routeUrl, routeDefault);
+ Type controllerType = typeof(ActionAttributeTestController);
+ controllerContext.ControllerDescriptor = new HttpControllerDescriptor(controllerContext.Configuration, controllerType.Name, controllerType);
+
+
+ Assert.Throws<HttpResponseException>(() =>
+ {
+ HttpActionDescriptor descriptor = ApiControllerHelper.SelectAction(controllerContext);
+ },
+ String.Format(SRResources.ApiControllerActionSelector_ActionNotFound, controllerType.Name));
+ }
+
+ [Theory]
+ [InlineData("GET", "ActionAttributeTest/", "RetriveUsers")]
+ [InlineData("GET", "ActionAttributeTest/3", "RetriveUsers")]
+ [InlineData("GET", "ActionAttributeTest/4?ssn=12345", "RetriveUsers")]
+ [InlineData("POST", "ActionAttributeTest/", "AddUsers")]
+ [InlineData("POST", "ActionAttributeTest/1", "AddUsers")]
+ [InlineData("PUT", "ActionAttributeTest", "UpdateUsers")]
+ [InlineData("PUT", "ActionAttributeTest/4", "UpdateUsers")]
+ [InlineData("PUT", "ActionAttributeTest/4?extra=thing", "UpdateUsers")]
+ [InlineData("DELETE", "ActionAttributeTest", "RemoveUsers")]
+ [InlineData("PATCH", "ActionAttributeTest", "Users")]
+ [InlineData("HEAD", "ActionAttributeTest/", "Users")]
+ public void SelectAction_OnDefaultRoute(string httpMethod, string requestUrl, string expectedActionName)
+ {
+ string routeUrl = "{controller}/{id}";
+ object routeDefault = new { id = RouteParameter.Optional };
+
+ HttpControllerContext controllerContext = ApiControllerHelper.CreateControllerContext(httpMethod, requestUrl, routeUrl, routeDefault);
+ controllerContext.ControllerDescriptor = new HttpControllerDescriptor(controllerContext.Configuration, "test", typeof(ActionAttributeTestController));
+ HttpActionDescriptor descriptor = ApiControllerHelper.SelectAction(controllerContext);
+
+ Assert.Equal<string>(expectedActionName, descriptor.ActionName);
+ }
+
+ [Theory]
+ [InlineData("OPTIONS", "ActionAttributeTest")]
+ [InlineData("TRACE", "ActionAttributeTest")]
+ [InlineData("NonAction", "ActionAttributeTest/")]
+ [InlineData("DENY", "ActionAttributeTest")]
+ [InlineData("APP", "ActionAttributeTest")]
+ public void SelectAction_ThrowsMethodNotSupported_OnDefaultRoute(string httpMethod, string requestUrl)
+ {
+ string routeUrl = "{controller}/{id}";
+ object routeDefault = new { id = RouteParameter.Optional };
+ HttpControllerContext controllerContext = ApiControllerHelper.CreateControllerContext(httpMethod, requestUrl, routeUrl, routeDefault);
+ Type controllerType = typeof(ActionAttributeTestController);
+ controllerContext.ControllerDescriptor = new HttpControllerDescriptor(controllerContext.Configuration, controllerType.Name, controllerType);
+
+
+ Assert.Throws<HttpResponseException>(() =>
+ {
+ HttpActionDescriptor descriptor = ApiControllerHelper.SelectAction(controllerContext);
+ },
+ String.Format(SRResources.ApiControllerActionSelector_HttpMethodNotSupported, httpMethod));
+
+ }
+ }
+}
diff --git a/test/System.Web.Http.Integration.Test/Controllers/ApiControllerActionSelectorTest.cs b/test/System.Web.Http.Integration.Test/Controllers/ApiControllerActionSelectorTest.cs
new file mode 100644
index 00000000..d5099a62
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/Controllers/ApiControllerActionSelectorTest.cs
@@ -0,0 +1,191 @@
+using System.Web.Http.Controllers;
+using System.Web.Http.Properties;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http
+{
+ [CLSCompliant(false)]
+ public class ApiControllerActionSelectorTest
+ {
+ [Theory]
+ [InlineData("GET", "Test", "GetUsers")]
+ [InlineData("GET", "Test/2", "GetUser")]
+ [InlineData("GET", "Test/3?name=mario", "GetUserByNameAndId")]
+ [InlineData("GET", "Test/3?name=mario&ssn=123456", "GetUserByNameIdAndSsn")]
+ [InlineData("GET", "Test?name=mario&ssn=123456", "GetUserByNameAndSsn")]
+ [InlineData("GET", "Test?name=mario&ssn=123456&age=3", "GetUserByNameAgeAndSsn")]
+ [InlineData("GET", "Test/4?name=mario&age=20", "GetUserByNameAndId")]
+ [InlineData("GET", "Test/5?random=9", "GetUser")]
+ [InlineData("Post", "Test", "PostUser")]
+ [InlineData("Post", "Test?name=mario&age=10", "PostUserByNameAndAge")]
+ /// Note: Normally the following would not match DeleteUserByIdAndOptName because it has 'id' and 'age' as parameters while the DeleteUserByIdAndOptName action has 'id' and 'name'.
+ /// However, because the default value is provided on action parameter 'name', having the 'id' in the request was enough to match the action.
+ [InlineData("Delete", "Test/6?age=10", "DeleteUserByIdAndOptName")]
+ public void Route_Parameters_Default(string httpMethod, string requestUrl, string expectedActionName)
+ {
+ string routeUrl = "{controller}/{id}";
+ object routeDefault = new { id = RouteParameter.Optional };
+
+ HttpControllerContext context = ApiControllerHelper.CreateControllerContext(httpMethod, requestUrl, routeUrl, routeDefault);
+ context.ControllerDescriptor = new HttpControllerDescriptor(context.Configuration, "test", typeof(TestController));
+ HttpActionDescriptor descriptor = ApiControllerHelper.SelectAction(context);
+
+ Assert.Equal<string>(expectedActionName, descriptor.ActionName);
+ }
+
+ [Theory]
+ [InlineData("GET", "Test", "GetUsers")]
+ [InlineData("GET", "Test/2", "GetUsersByName")]
+ [InlineData("GET", "Test/luigi?ssn=123456", "GetUserByNameAndSsn")]
+ [InlineData("GET", "Test/luigi?ssn=123456&id=2&ssn=12345", "GetUserByNameIdAndSsn")]
+ [InlineData("GET", "Test?age=10&ssn=123456", "GetUsers")]
+ [InlineData("GET", "Test?id=3&ssn=123456&name=luigi", "GetUserByNameIdAndSsn")]
+ [InlineData("POST", "Test/luigi?age=20", "PostUserByNameAndAge")]
+ public void Route_Parameters_Non_Id(string httpMethod, string requestUrl, string expectedActionName)
+ {
+ string routeUrl = "{controller}/{name}";
+ object routeDefault = new { name = RouteParameter.Optional };
+
+ HttpControllerContext context = ApiControllerHelper.CreateControllerContext(httpMethod, requestUrl, routeUrl, routeDefault);
+ context.ControllerDescriptor = new HttpControllerDescriptor(context.Configuration, "test", typeof(TestController));
+ HttpActionDescriptor descriptor = ApiControllerHelper.SelectAction(context);
+
+ Assert.Equal<string>(expectedActionName, descriptor.ActionName);
+ }
+
+ [Theory]
+ [InlineData("GET", "Test/3?NAME=mario", "GetUserByNameAndId")]
+ [InlineData("GET", "Test/3?name=mario&SSN=123456", "GetUserByNameIdAndSsn")]
+ [InlineData("GET", "Test?nAmE=mario&ssn=123456&AgE=3", "GetUserByNameAgeAndSsn")]
+ [InlineData("Delete", "Test/6?AGe=10", "DeleteUserByIdAndOptName")]
+ public void Route_Parameters_Casing(string httpMethod, string requestUrl, string expectedActionName)
+ {
+ string routeUrl = "{controller}/{ID}";
+ object routeDefault = new { id = RouteParameter.Optional };
+
+ HttpControllerContext context = ApiControllerHelper.CreateControllerContext(httpMethod, requestUrl, routeUrl, routeDefault);
+ context.ControllerDescriptor = new HttpControllerDescriptor(context.Configuration, "test", typeof(TestController));
+ HttpActionDescriptor descriptor = ApiControllerHelper.SelectAction(context);
+
+ Assert.Equal<string>(expectedActionName, descriptor.ActionName);
+ }
+
+ [Theory]
+ [InlineData("GET", "Test/GetUsers", "GetUsers")]
+ [InlineData("GET", "Test/GetUser", "GetUser")]
+ [InlineData("GET", "Test/GetUser?id=3", "GetUser")]
+ [InlineData("GET", "Test/GetUser/4?id=3", "GetUser")]
+ [InlineData("GET", "Test/GetUserByNameAgeAndSsn", "GetUserByNameAgeAndSsn")]
+ [InlineData("GET", "Test/GetUserByNameAndSsn", "GetUserByNameAndSsn")]
+ [InlineData("POST", "Test/PostUserByNameAndAddress", "PostUserByNameAndAddress")]
+ public void Route_Action(string httpMethod, string requestUrl, string expectedActionName)
+ {
+ string routeUrl = "{controller}/{action}/{id}";
+ object routeDefault = new { id = RouteParameter.Optional };
+
+ HttpControllerContext context = ApiControllerHelper.CreateControllerContext(httpMethod, requestUrl, routeUrl, routeDefault);
+ context.ControllerDescriptor = new HttpControllerDescriptor(context.Configuration, "test", typeof(TestController));
+ HttpActionDescriptor descriptor = ApiControllerHelper.SelectAction(context);
+
+ Assert.Equal<string>(expectedActionName, descriptor.ActionName);
+ }
+
+ [Theory]
+ [InlineData("GET", "Test/getusers", "GetUsers")]
+ [InlineData("GET", "Test/getuseR", "GetUser")]
+ [InlineData("GET", "Test/Getuser?iD=3", "GetUser")]
+ [InlineData("GET", "Test/GetUser/4?Id=3", "GetUser")]
+ [InlineData("GET", "Test/GetUserByNameAgeandSsn", "GetUserByNameAgeAndSsn")]
+ [InlineData("GET", "Test/getUserByNameAndSsn", "GetUserByNameAndSsn")]
+ [InlineData("POST", "Test/PostUserByNameAndAddress", "PostUserByNameAndAddress")]
+ public void Route_Action_Name_Casing(string httpMethod, string requestUrl, string expectedActionName)
+ {
+ string routeUrl = "{controller}/{action}/{id}";
+ object routeDefault = new { id = RouteParameter.Optional };
+
+ HttpControllerContext context = ApiControllerHelper.CreateControllerContext(httpMethod, requestUrl, routeUrl, routeDefault);
+ context.ControllerDescriptor = new HttpControllerDescriptor(context.Configuration, "test", typeof(TestController));
+ HttpActionDescriptor descriptor = ApiControllerHelper.SelectAction(context);
+
+ Assert.Equal<string>(expectedActionName, descriptor.ActionName);
+ }
+
+ [Theory]
+ [InlineData("GET", "Test", "GetUsers")]
+ [InlineData("GET", "Test/?name=peach", "GetUsersByName")]
+ [InlineData("GET", "Test?name=peach", "GetUsersByName")]
+ [InlineData("GET", "Test?name=peach&ssn=123456", "GetUserByNameAndSsn")]
+ [InlineData("GET", "Test?name=peach&ssn=123456&age=3", "GetUserByNameAgeAndSsn")]
+ public void Route_No_Action(string httpMethod, string requestUrl, string expectedActionName)
+ {
+ string routeUrl = "{controller}";
+
+ HttpControllerContext context = ApiControllerHelper.CreateControllerContext(httpMethod, requestUrl, routeUrl);
+ context.ControllerDescriptor = new HttpControllerDescriptor(context.Configuration, "test", typeof(TestController));
+ HttpActionDescriptor descriptor = ApiControllerHelper.SelectAction(context);
+
+ Assert.Equal<string>(expectedActionName, descriptor.ActionName);
+ }
+
+ [Fact]
+ public void RequestToAmbiguousAction_OnDefaultRoute()
+ {
+ string routeUrl = "{controller}/{id}";
+ object routeDefault = new { id = RouteParameter.Optional };
+ string httpMethod = "Post";
+ string requestUrl = "Test?name=mario";
+
+ // This would result in ambiguous match because complex parameter is not considered for matching.
+ // Therefore, PostUserByNameAndAddress(string name, Address address) would conflicts with PostUserByName(string name)
+ Assert.Throws<HttpResponseException>(() =>
+ {
+ HttpControllerContext context = ApiControllerHelper.CreateControllerContext(httpMethod, requestUrl, routeUrl, routeDefault);
+ context.ControllerDescriptor = new HttpControllerDescriptor(context.Configuration, "test", typeof(TestController));
+ HttpActionDescriptor descriptor = ApiControllerHelper.SelectAction(context);
+ });
+ }
+
+ [Fact]
+ public void RequestToActionWithNotSupportedHttpMethod_OnRouteWithAction()
+ {
+ string routeUrl = "{controller}/{action}/{id}";
+ object routeDefault = new { id = RouteParameter.Optional };
+ string requestUrl = "Test/GetUsers";
+ string httpMethod = "POST";
+ Assert.Throws<HttpResponseException>(() =>
+ {
+ HttpControllerContext context = ApiControllerHelper.CreateControllerContext(httpMethod, requestUrl, routeUrl, routeDefault);
+ context.ControllerDescriptor = new HttpControllerDescriptor(context.Configuration, "test", typeof(TestController));
+ HttpActionDescriptor descriptor = ApiControllerHelper.SelectAction(context);
+ },
+ string.Format(SRResources.ApiControllerActionSelector_HttpMethodNotSupported, httpMethod));
+ }
+
+ [Fact]
+ public void RequestToActionWith_HttpMethodDefinedByAttributeAndActionName()
+ {
+ string routeUrl = "{controller}/{id}";
+ object routeDefault = new { id = RouteParameter.Optional };
+ string requestUrl = "Test";
+ string httpMethod = "PATCH";
+
+ HttpControllerContext context = ApiControllerHelper.CreateControllerContext(httpMethod, requestUrl, routeUrl, routeDefault);
+ context.ControllerDescriptor = new HttpControllerDescriptor(context.Configuration, "test", typeof(TestController));
+ HttpActionDescriptor descriptor = ApiControllerHelper.SelectAction(context);
+
+ Assert.Equal<string>("PutUser", descriptor.ActionName);
+
+ // When you have the HttpMethod attribute, the convention should not be applied.
+ httpMethod = "PUT";
+ Assert.Throws<HttpResponseException>(() =>
+ {
+ context = ApiControllerHelper.CreateControllerContext(httpMethod, requestUrl, routeUrl, routeDefault);
+ context.ControllerDescriptor = new HttpControllerDescriptor(context.Configuration, "test", typeof(TestController));
+ ApiControllerHelper.SelectAction(context);
+ },
+ string.Format(SRResources.ApiControllerActionSelector_HttpMethodNotSupported, httpMethod));
+ }
+ }
+}
diff --git a/test/System.Web.Http.Integration.Test/Controllers/Apis/ActionAttributeTestController.cs b/test/System.Web.Http.Integration.Test/Controllers/Apis/ActionAttributeTestController.cs
new file mode 100644
index 00000000..5e13779e
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/Controllers/Apis/ActionAttributeTestController.cs
@@ -0,0 +1,29 @@
+namespace System.Web.Http
+{
+ public class ActionAttributeTestController : ApiController
+ {
+ [HttpGet]
+ public void RetriveUsers() { }
+
+ [HttpPost]
+ public void AddUsers(int id) { }
+
+ [HttpPut]
+ public void UpdateUsers(User user) { }
+
+ [HttpDelete]
+ public void RemoveUsers(string name) { }
+
+ [AcceptVerbs("PATCH", "HEAD")]
+ [CLSCompliant(false)]
+ public void Users(double key) { }
+
+ [ActionName("Deny")]
+ public void Reject(int id) { }
+
+ public void Approve(int id) { }
+
+ [NonAction]
+ public void NonAction() { }
+ }
+}
diff --git a/test/System.Web.Http.Integration.Test/Controllers/Apis/TestController.cs b/test/System.Web.Http.Integration.Test/Controllers/Apis/TestController.cs
new file mode 100644
index 00000000..048e2766
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/Controllers/Apis/TestController.cs
@@ -0,0 +1,28 @@
+using System.Collections.Generic;
+
+namespace System.Web.Http
+{
+ public class TestController : ApiController
+ {
+ public User GetUser(int id) { return null; }
+ public List<User> GetUsers() { return null; }
+
+ [ActionName("GetUsersByName")]
+ public List<User> RetrieveUsersByName(string name) { return null; }
+
+ [AcceptVerbs("PATCH")]
+ public void PutUser(User user) { }
+
+ public User GetUserByNameAndId(string name, int id) { return null; }
+ public User GetUserByNameAndAge(string name, int age) { return null; }
+ public User GetUserByNameAgeAndSsn(string name, int age, int ssn) { return null; }
+ public User GetUserByNameIdAndSsn(string name, int id, int ssn) { return null; }
+ public User GetUserByNameAndSsn(string name, int ssn) { return null; }
+ public User PostUser(User user) { return null; }
+ public User PostUserByNameAndAge(string name, int age) { return null; }
+ public User PostUserByName(string name) { return null; }
+ public User PostUserByNameAndAddress(string name, UserAddress address) { return null; }
+ public User DeleteUserByIdAndOptName(int id, string name = "DefaultName") { return null; }
+ public User DeleteUserByIdNameAndAge(int id, string name, int age) { return null; }
+ }
+}
diff --git a/test/System.Web.Http.Integration.Test/Controllers/Apis/User.cs b/test/System.Web.Http.Integration.Test/Controllers/Apis/User.cs
new file mode 100644
index 00000000..09fe81ff
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/Controllers/Apis/User.cs
@@ -0,0 +1,9 @@
+
+namespace System.Web.Http
+{
+ public class User
+ {
+ public string FirstName { get; set; }
+ public string LastName { get; set; }
+ }
+}
diff --git a/test/System.Web.Http.Integration.Test/Controllers/Apis/UserAddress.cs b/test/System.Web.Http.Integration.Test/Controllers/Apis/UserAddress.cs
new file mode 100644
index 00000000..f8c78afa
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/Controllers/Apis/UserAddress.cs
@@ -0,0 +1,9 @@
+
+namespace System.Web.Http
+{
+ public class UserAddress
+ {
+ public string Street;
+ public int ZipCode;
+ }
+}
diff --git a/test/System.Web.Http.Integration.Test/Controllers/CustomControllerFactoryTest.cs b/test/System.Web.Http.Integration.Test/Controllers/CustomControllerFactoryTest.cs
new file mode 100644
index 00000000..1cc1f55b
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/Controllers/CustomControllerFactoryTest.cs
@@ -0,0 +1,70 @@
+using System.Collections.Generic;
+using System.Net;
+using System.Net.Http;
+using System.Web.Http.Common;
+using System.Web.Http.Dispatcher;
+using System.Web.Http.ModelBinding;
+using System.Web.Http.SelfHost;
+using Xunit;
+
+namespace System.Web.Http.Controllers
+{
+ public class CustomControllerFactoryTest
+ {
+ [Fact]
+ public void Body_WithSingletonControllerInstance_Fails()
+ {
+ // Arrange
+ HttpClient httpClient = new HttpClient();
+ string baseAddress = "http://localhost";
+ string requestUri = baseAddress + "/Test";
+ HttpSelfHostConfiguration configuration = new HttpSelfHostConfiguration(baseAddress);
+ configuration.Routes.MapHttpRoute("Default", "{controller}", new { controller = "Test" });
+ configuration.ServiceResolver.SetService(typeof(IHttpControllerFactory), new MySingletonControllerFactory());
+ HttpSelfHostServer host = new HttpSelfHostServer(configuration);
+ host.OpenAsync().Wait();
+ HttpResponseMessage response = null;
+
+ try
+ {
+ // Act
+ response = httpClient.GetAsync(requestUri).Result;
+ response = httpClient.GetAsync(requestUri).Result;
+ response = httpClient.GetAsync(requestUri).Result;
+
+ // Assert
+ Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
+ }
+ finally
+ {
+ if (response != null)
+ {
+ response.Dispose();
+ }
+ }
+
+ host.CloseAsync().Wait();
+ }
+
+ private class MySingletonControllerFactory : IHttpControllerFactory
+ {
+ private static TestController singleton = new TestController();
+
+ public IHttpController CreateController(HttpControllerContext controllerContext, string controllerName)
+ {
+ return singleton;
+ }
+
+ public void ReleaseController(HttpControllerContext controllerContext, IHttpController controller)
+ {
+ throw new NotImplementedException();
+ }
+
+ public IDictionary<string, HttpControllerDescriptor> GetControllerMapping()
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ }
+}
diff --git a/test/System.Web.Http.Integration.Test/Controllers/Helpers/ApiControllerHelper.cs b/test/System.Web.Http.Integration.Test/Controllers/Helpers/ApiControllerHelper.cs
new file mode 100644
index 00000000..4389f0c9
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/Controllers/Helpers/ApiControllerHelper.cs
@@ -0,0 +1,54 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Routing;
+
+namespace System.Web.Http
+{
+ public class ApiControllerHelper
+ {
+ public static HttpActionDescriptor SelectAction(HttpControllerContext controllerContext)
+ {
+ ApiControllerActionSelector selector = new ApiControllerActionSelector();
+ HttpActionDescriptor descriptor = selector.SelectAction(controllerContext);
+ return descriptor;
+ }
+
+ public static HttpControllerContext CreateControllerContext(string httpMethod, string requestUrl, string routeUrl, object routeDefault = null)
+ {
+ string baseAddress = "http://localhost/";
+ HttpConfiguration config = new HttpConfiguration();
+ HttpRoute route = routeDefault != null ? new HttpRoute(routeUrl, new HttpRouteValueDictionary(routeDefault)) : new HttpRoute(routeUrl);
+ config.Routes.Add("test", route);
+
+ HttpRequestMessage request = new HttpRequestMessage(HttpMethodHelper.GetHttpMethod(httpMethod), baseAddress + requestUrl);
+
+ IHttpRouteData routeData = config.Routes.GetRouteData(request);
+ if (routeData == null)
+ {
+ throw new InvalidOperationException("Could not dispatch to controller based on the route.");
+ }
+
+ RemoveOptionalRoutingParameters(routeData.Values);
+
+ HttpControllerContext controllerContext = ContextUtil.CreateControllerContext(config, routeData, request);
+ return controllerContext;
+ }
+
+ private static void RemoveOptionalRoutingParameters(IDictionary<string, object> routeValueDictionary)
+ {
+ // Get all keys for which the corresponding value is 'Optional'.
+ // ToArray() necessary so that we don't manipulate the dictionary while enumerating.
+ string[] matchingKeys = (from entry in routeValueDictionary
+ where entry.Value == RouteParameter.Optional
+ select entry.Key).ToArray();
+
+ foreach (string key in matchingKeys)
+ {
+ routeValueDictionary.Remove(key);
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Http.Integration.Test/ExceptionHandling/DuplicateControllers.cs b/test/System.Web.Http.Integration.Test/ExceptionHandling/DuplicateControllers.cs
new file mode 100644
index 00000000..52952ac0
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/ExceptionHandling/DuplicateControllers.cs
@@ -0,0 +1,23 @@
+using System.Web.Http;
+
+namespace System.Web.Http
+{
+ public class DuplicateController : ApiController
+ {
+ public string GetAction()
+ {
+ return "dup";
+ }
+ }
+}
+
+namespace System.Web.Http2
+{
+ public class DuplicateController : ApiController
+ {
+ public string GetAction()
+ {
+ return "dup2";
+ }
+ }
+}
diff --git a/test/System.Web.Http.Integration.Test/ExceptionHandling/ExceptionController.cs b/test/System.Web.Http.Integration.Test/ExceptionHandling/ExceptionController.cs
new file mode 100644
index 00000000..b0958a91
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/ExceptionHandling/ExceptionController.cs
@@ -0,0 +1,102 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
+using System.Web.Http.Controllers;
+using System.Web.Http.Filters;
+
+namespace System.Web.Http
+{
+ public class ExceptionController : ApiController
+ {
+ public static string ResponseExceptionHeaderKey = "responseExceptionStatusCode";
+
+ public HttpResponseMessage Unavailable()
+ {
+ throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.ServiceUnavailable));
+ }
+
+ public Task<HttpResponseMessage> AsyncUnavailable()
+ {
+ throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.ServiceUnavailable));
+ }
+
+ public Task<HttpResponseMessage> AsyncUnavailableDelegate()
+ {
+ return Task.Factory.StartNew<HttpResponseMessage>(() => { throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.ServiceUnavailable)); });
+ }
+
+ public HttpResponseMessage ArgumentNull()
+ {
+ throw new ArgumentNullException("foo");
+ }
+
+ public Task<HttpResponseMessage> AsyncArgumentNull()
+ {
+ return Task.Factory.StartNew<HttpResponseMessage>(() => { throw new ArgumentNullException("foo"); });
+ }
+
+ [HttpGet]
+ public string GetException()
+ {
+ return "foo";
+ }
+
+ [HttpGet]
+ public string GetString()
+ {
+ return "bar";
+ }
+
+ [AuthorizationFilterThrows]
+ public void AuthorizationFilter() { }
+
+ [ActionFilterThrows]
+ public void ActionFilter() { }
+
+ [ExceptionFilterThrows]
+ public void ExceptionFilter() { throw new ArgumentException("exception"); }
+
+ private class AuthorizationFilterThrows : AuthorizeAttribute
+ {
+ public override void OnAuthorization(HttpActionContext actionContext)
+ {
+ TryThrowHttpResponseException(actionContext);
+ throw new ArgumentException("authorization");
+ }
+ }
+
+ private class ActionFilterThrows : ActionFilterAttribute
+ {
+ public override void OnActionExecuting(HttpActionContext actionContext)
+ {
+ TryThrowHttpResponseException(actionContext);
+ throw new ArgumentException("action");
+ }
+ }
+
+ private class ExceptionFilterThrows : ExceptionFilterAttribute
+ {
+ public override void OnException(HttpActionExecutedContext actionExecutedContext)
+ {
+ TryThrowHttpResponseException(actionExecutedContext.ActionContext);
+ throw actionExecutedContext.Exception;
+ }
+ }
+
+ private static void TryThrowHttpResponseException(HttpActionContext actionContext)
+ {
+ IEnumerable<string> values;
+ if (actionContext.ControllerContext.Request.Headers.TryGetValues(ResponseExceptionHeaderKey, out values))
+ {
+ string statusString = values.First() as string;
+ if (!String.IsNullOrEmpty(statusString))
+ {
+ HttpStatusCode status = (HttpStatusCode)Enum.Parse(typeof(HttpStatusCode), statusString);
+ throw new HttpResponseException("HttpResponseExceptionMessage", status);
+ }
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Http.Integration.Test/ExceptionHandling/ExceptionHandlingTest.cs b/test/System.Web.Http.Integration.Test/ExceptionHandling/ExceptionHandlingTest.cs
new file mode 100644
index 00000000..b4d7f683
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/ExceptionHandling/ExceptionHandlingTest.cs
@@ -0,0 +1,225 @@
+using System.Json;
+using System.Net;
+using System.Net.Http;
+using System.Web.Http.Dispatcher;
+using System.Web.Http.Properties;
+using Xunit;
+using Xunit.Extensions;
+
+namespace System.Web.Http
+{
+ public class ExceptionHandlingTest
+ {
+ [Theory]
+ [InlineData("Unavailable")]
+ [InlineData("AsyncUnavailable")]
+ [InlineData("AsyncUnavailableDelegate")]
+ public void ThrowingHttpResponseException_FromAction_GetsReturnedToClient(string actionName)
+ {
+ string controllerName = "Exception";
+ string requestUrl = String.Format("{0}/{1}/{2}", ScenarioHelper.BaseAddress, controllerName, actionName);
+
+ ScenarioHelper.RunTest(
+ controllerName,
+ "/{action}",
+ new HttpRequestMessage(HttpMethod.Get, requestUrl),
+ (response) =>
+ {
+ Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode);
+ }
+ );
+ }
+
+ [Theory]
+ [InlineData("ArgumentNull")]
+ [InlineData("AsyncArgumentNull")]
+ public void ThrowingArgumentNullException_FromAction_GetsReturnedToClient(string actionName)
+ {
+ string controllerName = "Exception";
+ string requestUrl = String.Format("{0}/{1}/{2}", ScenarioHelper.BaseAddress, controllerName, actionName);
+
+ ScenarioHelper.RunTest(
+ controllerName,
+ "/{action}",
+ new HttpRequestMessage(HttpMethod.Get, requestUrl),
+ (response) =>
+ {
+ Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
+ ExceptionSurrogate exception = response.Content.ReadAsAsync<ExceptionSurrogate>().Result;
+ Assert.Equal(typeof(ArgumentNullException).FullName, exception.ExceptionType.ToString());
+ }
+ );
+ }
+
+ [Theory]
+ [InlineData("ArgumentNull")]
+ [InlineData("AsyncArgumentNull")]
+ public void ThrowingArgumentNullException_FromAction_GetsReturnedToClientParsedAsJson(string actionName)
+ {
+ string controllerName = "Exception";
+ string requestUrl = String.Format("{0}/{1}/{2}", ScenarioHelper.BaseAddress, controllerName, actionName);
+
+ ScenarioHelper.RunTest(
+ controllerName,
+ "/{action}",
+ new HttpRequestMessage(HttpMethod.Get, requestUrl),
+ (response) =>
+ {
+ Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
+ dynamic json = JsonValue.Parse(response.Content.ReadAsStringAsync().Result);
+ string result = json.ExceptionType;
+ Assert.Equal(typeof(ArgumentNullException).FullName, result);
+ }
+ );
+ }
+
+ [Theory]
+ [InlineData("AuthorizationFilter")]
+ [InlineData("ActionFilter")]
+ [InlineData("ExceptionFilter")]
+ public void ThrowingArgumentException_FromFilter_GetsReturnedToClient(string actionName)
+ {
+ string controllerName = "Exception";
+ string requestUrl = String.Format("{0}/{1}/{2}", ScenarioHelper.BaseAddress, controllerName, actionName);
+
+ ScenarioHelper.RunTest(
+ controllerName,
+ "/{action}",
+ new HttpRequestMessage(HttpMethod.Get, requestUrl),
+ (response) =>
+ {
+ Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
+ ExceptionSurrogate exception = response.Content.ReadAsAsync<ExceptionSurrogate>().Result;
+ Assert.Equal(typeof(ArgumentException).FullName, exception.ExceptionType.ToString());
+ }
+ );
+ }
+
+ [Theory]
+ [InlineData("AuthorizationFilter", HttpStatusCode.Forbidden)]
+ [InlineData("ActionFilter", HttpStatusCode.NotAcceptable)]
+ [InlineData("ExceptionFilter", HttpStatusCode.NotImplemented)]
+ public void ThrowingHttpResponseException_FromFilter_GetsReturnedToClient(string actionName, HttpStatusCode responseExceptionStatusCode)
+ {
+ string controllerName = "Exception";
+ string requestUrl = String.Format("{0}/{1}/{2}", ScenarioHelper.BaseAddress, controllerName, actionName);
+ HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUrl);
+ request.Headers.Add(ExceptionController.ResponseExceptionHeaderKey, responseExceptionStatusCode.ToString());
+
+ ScenarioHelper.RunTest(
+ controllerName,
+ "/{action}",
+ request,
+ (response) =>
+ {
+ Assert.Equal(responseExceptionStatusCode, response.StatusCode);
+ Assert.Equal("HttpResponseExceptionMessage", response.Content.ReadAsAsync<string>().Result);
+ }
+ );
+ }
+
+ // TODO: add tests that throws from custom model binders
+
+ [Fact]
+ public void Service_ReturnsNotFound_WhenControllerNameDoesNotExist()
+ {
+ string controllerName = "randomControllerThatCannotBeFound";
+ string requestUrl = String.Format("{0}/{1}", ScenarioHelper.BaseAddress, controllerName);
+
+ ScenarioHelper.RunTest(
+ controllerName,
+ "",
+ new HttpRequestMessage(HttpMethod.Get, requestUrl),
+ (response) =>
+ {
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ Assert.Equal(
+ String.Format(SRResources.DefaultControllerFactory_ControllerNameNotFound, controllerName),
+ response.Content.ReadAsAsync<string>().Result);
+ }
+ );
+ }
+
+ [Fact]
+ public void Service_ReturnsNotFound_WhenActionNameDoesNotExist()
+ {
+ string controllerName = "Exception";
+ string actionName = "actionNotFound";
+ string requestUrl = String.Format("{0}/{1}/{2}", ScenarioHelper.BaseAddress, controllerName, actionName);
+
+ ScenarioHelper.RunTest(
+ controllerName,
+ "/{action}",
+ new HttpRequestMessage(HttpMethod.Get, requestUrl),
+ (response) =>
+ {
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ Assert.Equal(
+ String.Format(SRResources.ApiControllerActionSelector_ActionNameNotFound, controllerName, actionName),
+ response.Content.ReadAsAsync<string>().Result);
+ }
+ );
+ }
+
+ [Fact]
+ public void Service_ReturnsMethodNotAllowed_WhenActionsDoesNotSupportTheRequestHttpMethod()
+ {
+ string controllerName = "Exception";
+ string actionName = "GetString";
+ HttpMethod requestMethod = HttpMethod.Post;
+ string requestUrl = String.Format("{0}/{1}/{2}", ScenarioHelper.BaseAddress, controllerName, actionName);
+ ScenarioHelper.RunTest(
+ controllerName,
+ "/{action}",
+ new HttpRequestMessage(requestMethod, requestUrl),
+ (response) =>
+ {
+ Assert.Equal(HttpStatusCode.MethodNotAllowed, response.StatusCode);
+ Assert.Equal(
+ String.Format(SRResources.ApiControllerActionSelector_HttpMethodNotSupported, requestMethod.Method),
+ response.Content.ReadAsAsync<string>().Result);
+ }
+ );
+ }
+
+ [Fact]
+ public void Service_ReturnsInternalServerError_WhenMultipleActionsAreFound()
+ {
+ string controllerName = "Exception";
+ string requestUrl = String.Format("{0}/{1}", ScenarioHelper.BaseAddress, controllerName);
+
+ ScenarioHelper.RunTest(
+ controllerName,
+ "",
+ new HttpRequestMessage(HttpMethod.Get, requestUrl),
+ (response) =>
+ {
+ Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
+ Assert.Contains(
+ String.Format(SRResources.ApiControllerActionSelector_AmbiguousMatch, String.Empty),
+ response.Content.ReadAsAsync<string>().Result);
+ }
+ );
+ }
+
+ [Fact]
+ public void Service_ReturnsInternalServerError_WhenMultipleControllersAreFound()
+ {
+ string controllerName = "Duplicate";
+ string requestUrl = String.Format("{0}/{1}", ScenarioHelper.BaseAddress, controllerName);
+
+ ScenarioHelper.RunTest(
+ controllerName,
+ "",
+ new HttpRequestMessage(HttpMethod.Get, requestUrl),
+ (response) =>
+ {
+ Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
+ Assert.Contains(
+ String.Format(SRResources.DefaultControllerFactory_ControllerNameAmbiguous_WithRouteTemplate, controllerName, "{controller}", String.Empty),
+ response.Content.ReadAsAsync<string>().Result);
+ }
+ );
+ }
+ }
+}
diff --git a/test/System.Web.Http.Integration.Test/ExceptionHandling/HttpResponseExceptionTest.cs b/test/System.Web.Http.Integration.Test/ExceptionHandling/HttpResponseExceptionTest.cs
new file mode 100644
index 00000000..0992808c
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/ExceptionHandling/HttpResponseExceptionTest.cs
@@ -0,0 +1,224 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Formatting;
+using System.Net.Http.Headers;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Controllers;
+using System.Web.Http.Filters;
+using Xunit;
+using Xunit.Extensions;
+
+namespace System.Web.Http.ExceptionHandling
+{
+ public class HttpResponseExceptionTest
+ {
+ [Theory]
+ [InlineData("DoNotThrow")]
+ [InlineData("ActionMethod")]
+ // TODO : 332683 - HttpResponseExceptions in message handlers
+ //[InlineData("RequestMessageHandler")]
+ //[InlineData("ResponseMessageHandler")]
+ [InlineData("RequestAuthorization")]
+ [InlineData("BeforeActionExecuted")]
+ [InlineData("AfterActionExecuted")]
+ [InlineData("ContentNegotiatorNegotiate")]
+ [InlineData("ActionMethodAndExceptionFilter")]
+ [InlineData("MediaTypeFormatterReadFromStreamAsync")]
+ public void HttpResponseExceptionWithExplicitStatusCode(string throwAt)
+ {
+ HttpRequestMessage request = new HttpRequestMessage();
+ request.RequestUri = new Uri(ScenarioHelper.BaseAddress + "/ExceptionTests/ReturnString");
+ request.Method = HttpMethod.Post;
+ request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml"));
+ request.Content = new StringContent("<string>" + throwAt + "</string>", Encoding.UTF8, "application/xml");
+
+ ScenarioHelper.RunTest(
+ "ExceptionTests",
+ "/{action}",
+ request,
+ response =>
+ {
+ Assert.NotNull(response.Content);
+ Assert.NotNull(response.Content.Headers.ContentType);
+ Assert.Equal(response.Content.Headers.ContentType.MediaType, "application/xml");
+
+ if (throwAt == "DoNotThrow")
+ {
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal("Hello World!", response.Content.ReadAsAsync<string>(new List<MediaTypeFormatter>() { new XmlMediaTypeFormatter() }).Result);
+ }
+ else
+ {
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ Assert.Equal(String.Format("Error at {0}", throwAt),
+ response.Content.ReadAsAsync<string>(new List<MediaTypeFormatter>() { new XmlMediaTypeFormatter() }).Result);
+ }
+ },
+ config =>
+ {
+ config.ServiceResolver.SetService(typeof(IContentNegotiator), new CustomContentNegotiator(throwAt));
+
+ config.MessageHandlers.Add(new CustomMessageHandler(throwAt));
+ config.Filters.Add(new CustomActionFilterAttribute(throwAt));
+ config.Filters.Add(new CustomAuthorizationFilterAttribute(throwAt));
+ config.Filters.Add(new CustomExceptionFilterAttribute(throwAt));
+ config.Formatters.Clear();
+ config.Formatters.Add(new CustomXmlMediaTypeFormatter(throwAt));
+ }
+ );
+ }
+ }
+
+ public class CustomMessageHandler : DelegatingHandler
+ {
+ private string _throwAt;
+
+ public CustomMessageHandler(string throwAt)
+ {
+ _throwAt = throwAt;
+ }
+
+ protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ ExceptionTestsUtility.CheckForThrow(_throwAt, "RequestMessageHandler");
+
+ return base.SendAsync(request, cancellationToken).ContinueWith<HttpResponseMessage>((tsk) =>
+ {
+ ExceptionTestsUtility.CheckForThrow(_throwAt, "ResponseMessageHandler");
+
+ HttpResponseMessage response = tsk.Result;
+
+ return response;
+ });
+ }
+ }
+
+ public class ExceptionTestsController : ApiController
+ {
+ [HttpPost]
+ public string ReturnString([FromBody] string throwAt)
+ {
+ string message = "Hello World!";
+
+ // check if the test wants to throw from here
+ ExceptionTestsUtility.CheckForThrow(throwAt, "ActionMethod");
+
+ // NOTE: this indicates that we want to throw from here & after this gets intercepted
+ // by the ExceptionFilter, we want to throw from there too
+ ExceptionTestsUtility.CheckForThrow(throwAt, "ActionMethodAndExceptionFilter");
+
+ return message;
+ }
+ }
+
+ public class CustomAuthorizationFilterAttribute : AuthorizationFilterAttribute
+ {
+ private string _throwAt;
+
+ public CustomAuthorizationFilterAttribute(string throwAt)
+ {
+ _throwAt = throwAt;
+ }
+
+ public override void OnAuthorization(HttpActionContext context)
+ {
+ ExceptionTestsUtility.CheckForThrow(_throwAt, "RequestAuthorization");
+ }
+ }
+
+ public class CustomActionFilterAttribute : ActionFilterAttribute
+ {
+ private string _throwAt;
+
+ public CustomActionFilterAttribute(string throwAt)
+ {
+ _throwAt = throwAt;
+ }
+
+ public override void OnActionExecuting(HttpActionContext context)
+ {
+ ExceptionTestsUtility.CheckForThrow(_throwAt, "BeforeActionExecuted");
+ }
+
+ public override void OnActionExecuted(HttpActionExecutedContext context)
+ {
+ ExceptionTestsUtility.CheckForThrow(_throwAt, "AfterActionExecuted");
+ }
+ }
+
+ public class CustomExceptionFilterAttribute : ExceptionFilterAttribute
+ {
+ private string _throwAt;
+
+ public CustomExceptionFilterAttribute(string throwAt)
+ {
+ _throwAt = throwAt;
+ }
+
+ public override void OnException(HttpActionExecutedContext context)
+ {
+ ExceptionTestsUtility.CheckForThrow(_throwAt, "ActionMethodAndExceptionFilter");
+ }
+ }
+
+ public class CustomContentNegotiator : System.Net.Http.Formatting.DefaultContentNegotiator
+ {
+ private string _throwAt;
+
+ public CustomContentNegotiator(string throwAt)
+ {
+ _throwAt = throwAt;
+ }
+
+ public override MediaTypeFormatter Negotiate(Type type, HttpRequestMessage request, IEnumerable<MediaTypeFormatter> formatters, out System.Net.Http.Headers.MediaTypeHeaderValue mediaType)
+ {
+ ExceptionTestsUtility.CheckForThrow(_throwAt, "ContentNegotiatorNegotiate");
+
+ return base.Negotiate(type, request, formatters, out mediaType);
+ }
+ }
+
+ public class CustomXmlMediaTypeFormatter : XmlMediaTypeFormatter
+ {
+ private string _throwAt;
+
+ public CustomXmlMediaTypeFormatter(string throwAt)
+ {
+ _throwAt = throwAt;
+ }
+
+ public override Task<object> ReadFromStreamAsync(Type type, Stream stream, HttpContentHeaders contentHeaders, IFormatterLogger formatterLogger)
+ {
+ ExceptionTestsUtility.CheckForThrow(_throwAt, "MediaTypeFormatterReadFromStreamAsync");
+
+ return base.ReadFromStreamAsync(type, stream, contentHeaders, formatterLogger);
+ }
+
+ public override Task WriteToStreamAsync(Type type, object value, Stream stream, HttpContentHeaders contentHeaders, TransportContext transportContext)
+ {
+ ExceptionTestsUtility.CheckForThrow(_throwAt, "MediaTypeFormatterWriteToStreamAsync");
+
+ return base.WriteToStreamAsync(type, value, stream, contentHeaders, transportContext);
+ }
+ }
+
+ public static class ExceptionTestsUtility
+ {
+ public static void CheckForThrow(string throwAt, string stage)
+ {
+ if (throwAt == stage)
+ {
+ HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.NotFound)
+ {
+ Content = new ObjectContent<string>(String.Format("Error at {0}", stage), new XmlMediaTypeFormatter())
+ };
+
+ throw new HttpResponseException(response);
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Http.Integration.Test/ExceptionHandling/IncludeErrorDetailTest.cs b/test/System.Web.Http.Integration.Test/ExceptionHandling/IncludeErrorDetailTest.cs
new file mode 100644
index 00000000..53362f4c
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/ExceptionHandling/IncludeErrorDetailTest.cs
@@ -0,0 +1,76 @@
+using System.Collections.Generic;
+using System.Json;
+using System.Net;
+using System.Net.Http;
+using Xunit;
+using Xunit.Extensions;
+
+namespace System.Web.Http
+{
+ public class IncludeErrorDetailTest
+ {
+ public static IEnumerable<object[]> Data
+ {
+ get
+ {
+ return new object[][]
+ {
+ new object[] { "localhost", null, true },
+ new object[] { "127.0.0.1", null, true },
+ new object[] { "www.foo.com", null, false },
+ new object[] { "localhost", IncludeErrorDetailPolicy.LocalOnly, true },
+ new object[] { "www.foo.com", IncludeErrorDetailPolicy.LocalOnly, false },
+ new object[] { "localhost", IncludeErrorDetailPolicy.Always, true },
+ new object[] { "www.foo.com", IncludeErrorDetailPolicy.Always, true },
+ new object[] { "localhost", IncludeErrorDetailPolicy.Never, false },
+ new object[] { "www.foo.com", IncludeErrorDetailPolicy.Never, false }
+ };
+ }
+ }
+
+ [Theory]
+ [PropertyData("Data")]
+ public void ThrowingOnActionIncludesErrorDetail(string hostName, IncludeErrorDetailPolicy? includeErrorDetail, bool shouldIncludeErrorDetail)
+ {
+ string controllerName = "Exception";
+ string requestUrl = String.Format("{0}/{1}/{2}", "http://" + hostName, controllerName, "ArgumentNull");
+ ScenarioHelper.RunTest(
+ controllerName,
+ "/{action}",
+ new HttpRequestMessage(HttpMethod.Get, requestUrl),
+ (response) =>
+ {
+ if (shouldIncludeErrorDetail)
+ {
+ AssertResponseIncludesErrorDetail(response);
+ }
+ else
+ {
+ AssertResponseDoesNotIncludeErrorDetail(response);
+ }
+ },
+ (config) =>
+ {
+ if (includeErrorDetail.HasValue)
+ {
+ config.IncludeErrorDetailPolicy = includeErrorDetail.Value;
+ }
+ }
+ );
+ }
+
+ private void AssertResponseIncludesErrorDetail(HttpResponseMessage response)
+ {
+ Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
+ dynamic json = JsonValue.Parse(response.Content.ReadAsStringAsync().Result);
+ string result = json.ExceptionType;
+ Assert.Equal(typeof(ArgumentNullException).FullName, result);
+ }
+
+ private void AssertResponseDoesNotIncludeErrorDetail(HttpResponseMessage response)
+ {
+ Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
+ Assert.Null(response.Content);
+ }
+ }
+} \ No newline at end of file
diff --git a/test/System.Web.Http.Integration.Test/Filters/IQueryableFilterPipelineTest.cs b/test/System.Web.Http.Integration.Test/Filters/IQueryableFilterPipelineTest.cs
new file mode 100644
index 00000000..0f98bd0a
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/Filters/IQueryableFilterPipelineTest.cs
@@ -0,0 +1,99 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Net.Http;
+using System.Net.Http.Formatting;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Controllers;
+using System.Web.Http.ModelBinding;
+using Moq;
+using Xunit;
+
+namespace System.Web.Http.Filters
+{
+ public class IQueryableFilterPipelineTest
+ {
+ [Fact]
+ public void IQueryableRelatedFiltersAreOrderedCorrectly()
+ {
+ // Arrange
+ Mock<HttpControllerDescriptor> controllerDescriptorMock = new Mock<HttpControllerDescriptor>() { CallBase = true };
+ controllerDescriptorMock.Object.Configuration = new HttpConfiguration();
+ controllerDescriptorMock.Object.ControllerType = typeof(IQueryableFilterPipelineTest);
+ Mock<HttpActionDescriptor> actionDescriptorMock = new Mock<HttpActionDescriptor>(controllerDescriptorMock.Object) { CallBase = true };
+ actionDescriptorMock.Setup(ad => ad.GetFilters()).Returns(new IFilter[] { new ResultLimitAttribute(42) });
+ actionDescriptorMock.Setup(ad => ad.ReturnType).Returns(typeof(IQueryable<string>));
+ HttpActionDescriptor actionDescriptor = actionDescriptorMock.Object;
+
+ // Act
+ var filters = actionDescriptor.GetFilterPipeline();
+
+ // Assert
+ Assert.Equal(3, filters.Count);
+ Assert.IsType<EnumerableEvaluatorFilter>(filters[0].Instance);
+ Assert.IsType<ResultLimitAttribute>(filters[1].Instance);
+ Assert.IsType<QueryCompositionFilterAttribute>(filters[2].Instance);
+ }
+
+ [Fact]
+ public void EnumerableEvaluatorFilterExecutesQuerySoThatExceptionsCanBeCaughtByExceptionFilters()
+ {
+ // Arrange
+ Mock<ApiController> controllerMock = new Mock<ApiController> { CallBase = true };
+ Mock<HttpActionDescriptor> actionDescriptorMock = new Mock<HttpActionDescriptor>();
+ actionDescriptorMock.Setup(ad => ad.ReturnType).Returns(typeof(IEnumerable<string>));
+ Mock<IExceptionFilter> exceptionFilterMock = new Mock<IExceptionFilter>();
+ actionDescriptorMock.Setup(ad => ad.GetFilterPipeline())
+ .Returns(new Collection<FilterInfo>(new IFilter[] { EnumerableEvaluatorFilter.Instance, exceptionFilterMock.As<IFilter>().Object }.Select(f => new FilterInfo(f, FilterScope.Action)).ToList()));
+ Mock<IHttpActionSelector> actionSelectorMock = new Mock<IHttpActionSelector>();
+ actionSelectorMock.Setup(actionSelector => actionSelector.SelectAction(It.IsAny<HttpControllerContext>())).Returns(actionDescriptorMock.Object);
+ Mock<IHttpActionInvoker> actionInvokerMock = new Mock<IHttpActionInvoker>();
+ InvalidOperationException exception = new InvalidOperationException("Bad enumeration");
+ IEnumerable<string> actionResult = Enumerable.Range(0, 1).Select<int, string>(i =>
+ {
+ throw exception;
+ });
+ var invocationTask = Task.Factory.StartNew<HttpResponseMessage>(() => new HttpResponseMessage
+ {
+ Content = new ObjectContent<IEnumerable<string>>(actionResult, new JsonMediaTypeFormatter())
+ });
+ actionInvokerMock.Setup(ai => ai.InvokeActionAsync(It.IsAny<HttpActionContext>(), It.IsAny<CancellationToken>()))
+ .Returns(invocationTask);
+ Mock<HttpControllerDescriptor> controllerDescriptorMock = new Mock<HttpControllerDescriptor>();
+ controllerDescriptorMock.Object.HttpActionSelector = actionSelectorMock.Object;
+ controllerDescriptorMock.Object.HttpActionInvoker = actionInvokerMock.Object;
+
+ Mock<IActionValueBinder> binderMock = new Mock<IActionValueBinder>();
+ Mock<HttpActionBinding> actionBindingMock = new Mock<HttpActionBinding>();
+ actionBindingMock.Setup(b => b.ExecuteBindingAsync(It.IsAny<HttpActionContext>(), It.IsAny<CancellationToken>())).Returns(Task.Factory.StartNew(() => { }));
+ binderMock.Setup(b => b.GetBinding(It.IsAny<HttpActionDescriptor>())).Returns(actionBindingMock.Object);
+ controllerDescriptorMock.Object.ActionValueBinder = binderMock.Object;
+
+ HttpConfiguration config = new HttpConfiguration();
+
+ var controllerContext = new HttpControllerContext { ControllerDescriptor = controllerDescriptorMock.Object, Configuration = config };
+
+ // Act
+ var responseTask = controllerMock.Object.ExecuteAsync(controllerContext, CancellationToken.None);
+
+ // Assert
+ responseTask.WaitUntilCompleted();
+ exceptionFilterMock.Verify(ef => ef.ExecuteExceptionFilterAsync(It.Is<HttpActionExecutedContext>(aec => aec.Exception == exception), It.IsAny<CancellationToken>()));
+ }
+
+ public class TestController : ApiController
+ {
+ private Exception _ex;
+ public TestController(Exception ex)
+ {
+ _ex = ex;
+ }
+ public IEnumerable<string> Get()
+ {
+ yield return "cat";
+ throw _ex;
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Http.Integration.Test/ModelBinding/BodyBindingTests.cs b/test/System.Web.Http.Integration.Test/ModelBinding/BodyBindingTests.cs
new file mode 100644
index 00000000..6d0f15c4
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/ModelBinding/BodyBindingTests.cs
@@ -0,0 +1,120 @@
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Formatting;
+using System.Net.Http.Headers;
+using System.Text;
+using Xunit;
+using Xunit.Extensions;
+
+namespace System.Web.Http.ModelBinding
+{
+ /// <summary>
+ /// End to end functional tests for model binding via request body
+ /// </summary>
+ public class BodyBindingTests : ModelBindingTests
+ {
+ [Fact]
+ public void Body_Bad_Input_Receives_Validation_Error()
+ {
+ // Arrange
+ string formUrlEncodedString = "Id=101&Name=testFirstNameTooLong";
+ StringContent stringContent = new StringContent(formUrlEncodedString, Encoding.UTF8, "application/x-www-form-urlencoded");
+
+ HttpRequestMessage request = new HttpRequestMessage()
+ {
+ RequestUri = new Uri(baseAddress + "ModelBinding/PostComplexWithValidation"),
+ Method = HttpMethod.Post,
+ Content = stringContent,
+ };
+
+ // Act
+ HttpResponseMessage response = httpClient.SendAsync(request).Result;
+
+ // Assert
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ Assert.Equal("Failed to bind customer.Name. The errors are:\nErrorMessage: The field Name must be a string with a maximum length of 6.", response.Content.ReadAsStringAsync().Result);
+ }
+
+ [Fact]
+ public void Body_Good_Input_Succeed()
+ {
+ // Arrange
+ string formUrlEncodedString = "Id=111&Name=John";
+ StringContent stringContent = new StringContent(formUrlEncodedString, Encoding.UTF8, "application/x-www-form-urlencoded");
+
+ HttpRequestMessage request = new HttpRequestMessage()
+ {
+ RequestUri = new Uri(baseAddress + "ModelBinding/PostComplexWithValidation"),
+ Method = HttpMethod.Post,
+ Content = stringContent,
+ };
+
+ // Act
+ HttpResponseMessage response = httpClient.SendAsync(request).Result;
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal("111", response.Content.ReadAsStringAsync().Result);
+ }
+
+ [Theory]
+ [InlineData("PostComplexType", "application/json")]
+ [InlineData("PostComplexType", "application/xml")]
+ [InlineData("PostComplexTypeFromBody", "application/json")]
+ [InlineData("PostComplexTypeFromBody", "application/xml")]
+ public void Body_Binds_ComplexType_Type_Key_Value_Read(string action, string mediaType)
+ {
+ // Arrange
+ ModelBindOrder expectedItem = new ModelBindOrder()
+ {
+ ItemName = "Bike",
+ Quantity = 1,
+ Customer = new ModelBindCustomer { Name = "Fred" }
+ };
+ var formatter = new MediaTypeFormatterCollection().Find(mediaType);
+ HttpRequestMessage request = new HttpRequestMessage
+ {
+ Content = new ObjectContent<ModelBindOrder>(expectedItem, formatter),
+ RequestUri = new Uri(baseAddress + String.Format("ModelBinding/{0}", action)),
+ Method = HttpMethod.Post,
+ };
+
+ // Act
+ HttpResponseMessage response = httpClient.SendAsync(request).Result;
+
+ // Assert
+ ModelBindOrder actualItem = response.Content.ReadAsAsync<ModelBindOrder>().Result;
+ Assert.Equal<ModelBindOrder>(expectedItem, actualItem, new ModelBindOrderEqualityComparer());
+ }
+
+ [Theory]
+ [InlineData("PostComplexType", "application/json")]
+ [InlineData("PostComplexType", "application/xml")]
+ [InlineData("PostComplexTypeFromBody", "application/json")]
+ [InlineData("PostComplexTypeFromBody", "application/xml")]
+ public void Body_Binds_ComplexType_Type_Whole_Body_Read(string action, string mediaType)
+ {
+ // Arrange
+ ModelBindOrder expectedItem = new ModelBindOrder()
+ {
+ ItemName = "Bike",
+ Quantity = 1,
+ Customer = new ModelBindCustomer { Name = "Fred" }
+ };
+ var formatter = new MediaTypeFormatterCollection().Find(mediaType);
+ HttpRequestMessage request = new HttpRequestMessage
+ {
+ Content = new ObjectContent<ModelBindOrder>(expectedItem, formatter),
+ RequestUri = new Uri(baseAddress + String.Format("ModelBinding/{0}", action)),
+ Method = HttpMethod.Post,
+ };
+
+ // Act
+ HttpResponseMessage response = httpClient.SendAsync(request).Result;
+
+ // Assert
+ ModelBindOrder actualItem = response.Content.ReadAsAsync<ModelBindOrder>().Result;
+ Assert.Equal<ModelBindOrder>(expectedItem, actualItem, new ModelBindOrderEqualityComparer());
+ }
+ }
+}
diff --git a/test/System.Web.Http.Integration.Test/ModelBinding/CustomBindingTests.cs b/test/System.Web.Http.Integration.Test/ModelBinding/CustomBindingTests.cs
new file mode 100644
index 00000000..d4a4233a
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/ModelBinding/CustomBindingTests.cs
@@ -0,0 +1,32 @@
+using System.Net.Http;
+using Xunit;
+
+namespace System.Web.Http.ModelBinding
+{
+ /// <summary>
+ /// End to end functional tests for model binding via custom providers
+ /// </summary>
+ public class CustomBindingTests : ModelBindingTests
+ {
+ [Fact]
+ public void Custom_ValueProvider_Binds_Simple_Types_Get()
+ {
+ // Arrange
+ HttpRequestMessage request = new HttpRequestMessage()
+ {
+ RequestUri = new Uri(baseAddress + String.Format("ModelBinding/{0}", "GetIntCustom")),
+ Method = HttpMethod.Get
+ };
+
+ request.Headers.Add("value", "5");
+
+ // Act
+ HttpResponseMessage response = httpClient.SendAsync(request).Result;
+
+ // Assert
+ string responseString = response.Content.ReadAsStringAsync().Result;
+ Assert.Equal<string>("5", responseString);
+ }
+
+ }
+} \ No newline at end of file
diff --git a/test/System.Web.Http.Integration.Test/ModelBinding/DefaultActionValueBinderTest.cs b/test/System.Web.Http.Integration.Test/ModelBinding/DefaultActionValueBinderTest.cs
new file mode 100644
index 00000000..4b9b1b6a
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/ModelBinding/DefaultActionValueBinderTest.cs
@@ -0,0 +1,990 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Globalization;
+using System.Json;
+using System.Linq;
+using System.Net.Http;
+using System.Net.Http.Formatting;
+using System.Net.Http.Headers;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Controllers;
+using System.Web.Http.Routing;
+using System.Web.Http.ValueProviders;
+using Microsoft.TestCommon;
+using Xunit;
+
+namespace System.Web.Http.ModelBinding
+{
+ public class DefaultActionValueBinderTest
+ {
+ [Fact]
+ public void BindValuesAsync_Uses_DefaultValues()
+ {
+ // Arrange
+ HttpActionContext context = ContextUtil.CreateActionContext(
+ ContextUtil.CreateControllerContext(),
+ new ReflectedHttpActionDescriptor() { MethodInfo = typeof(ActionValueController).GetMethod("Get") });
+ CancellationToken cancellationToken = new CancellationToken();
+ DefaultActionValueBinder provider = new DefaultActionValueBinder();
+
+ // Act
+ provider.BindValuesAsync(context, cancellationToken).Wait();
+
+ // Assert
+ Dictionary<string, object> expectedResult = new Dictionary<string, object>();
+ expectedResult["id"] = 0;
+ expectedResult["firstName"] = "DefaultFirstName";
+ expectedResult["lastName"] = "DefaultLastName";
+ Assert.Equal(expectedResult, context.ActionArguments, new DictionaryEqualityComparer());
+ }
+
+ [Fact]
+ public void BindValuesAsync_WithObjectContentInRequest_Works()
+ {
+ // Arrange
+ ActionValueItem cust = new ActionValueItem() { FirstName = "FirstName", LastName = "LastName", Id = 1 };
+ HttpActionContext context = ContextUtil.CreateActionContext(
+ ContextUtil.CreateControllerContext(),
+ new ReflectedHttpActionDescriptor() { MethodInfo = typeof(ActionValueController).GetMethod("PostComplexType") });
+ context.ControllerContext.Request = new HttpRequestMessage
+ {
+ Content = new ObjectContent<ActionValueItem>(cust, new JsonMediaTypeFormatter())
+ };
+ CancellationToken cancellationToken = new CancellationToken();
+ DefaultActionValueBinder provider = new DefaultActionValueBinder();
+
+ // Act
+ provider.BindValuesAsync(context, cancellationToken).Wait();
+
+ // Assert
+ Dictionary<string, object> expectedResult = new Dictionary<string, object>();
+ expectedResult["item"] = cust;
+ Assert.Equal(expectedResult, context.ActionArguments, new DictionaryEqualityComparer());
+ }
+
+ #region Query Strings
+
+ [Fact]
+ public void BindValuesAsync_Query_String_Values_To_Simple_Types()
+ {
+ // Arrange
+ CancellationToken cancellationToken = new CancellationToken();
+ HttpActionContext actionContext = ContextUtil.CreateActionContext(
+ ContextUtil.CreateControllerContext(new HttpRequestMessage()
+ {
+ Method = HttpMethod.Get,
+ RequestUri = new Uri("http://localhost?id=5&firstName=queryFirstName&lastName=queryLastName")
+ }),
+ new ReflectedHttpActionDescriptor() { MethodInfo = typeof(ActionValueController).GetMethod("Get") });
+
+ DefaultActionValueBinder provider = new DefaultActionValueBinder();
+
+ // Act
+ provider.BindValuesAsync(actionContext, cancellationToken).Wait();
+
+ // Assert
+ Dictionary<string, object> expectedResult = new Dictionary<string, object>();
+ expectedResult["id"] = 5;
+ expectedResult["firstName"] = "queryFirstName";
+ expectedResult["lastName"] = "queryLastName";
+ Assert.Equal(expectedResult, actionContext.ActionArguments, new DictionaryEqualityComparer());
+ }
+
+ [Fact]
+ public void BindValuesAsync_Query_String_Values_To_Simple_Types_With_FromUriAttribute()
+ {
+ // Arrange
+ CancellationToken cancellationToken = new CancellationToken();
+ HttpActionContext actionContext = ContextUtil.CreateActionContext(
+ ContextUtil.CreateControllerContext(new HttpRequestMessage()
+ {
+ Method = HttpMethod.Get,
+ RequestUri = new Uri("http://localhost?id=5&firstName=queryFirstName&lastName=queryLastName")
+ }),
+ new ReflectedHttpActionDescriptor() { MethodInfo = typeof(ActionValueController).GetMethod("GetFromUri") });
+
+ DefaultActionValueBinder provider = new DefaultActionValueBinder();
+
+ // Act
+ provider.BindValuesAsync(actionContext, cancellationToken).Wait();
+
+ // Assert
+ Dictionary<string, object> expectedResult = new Dictionary<string, object>();
+ expectedResult["id"] = 5;
+ expectedResult["firstName"] = "queryFirstName";
+ expectedResult["lastName"] = "queryLastName";
+ Assert.Equal(expectedResult, actionContext.ActionArguments, new DictionaryEqualityComparer());
+ }
+
+ [Fact]
+ public void BindValuesAsync_Query_String_Values_To_Complex_Types()
+ {
+ // Arrange
+ CancellationToken cancellationToken = new CancellationToken();
+ HttpActionContext actionContext = ContextUtil.CreateActionContext(
+ ContextUtil.CreateControllerContext(new HttpRequestMessage()
+ {
+ Method = HttpMethod.Get,
+ RequestUri = new Uri("http://localhost?id=5&firstName=queryFirstName&lastName=queryLastName")
+ }),
+ new ReflectedHttpActionDescriptor() { MethodInfo = typeof(ActionValueController).GetMethod("GetItem") });
+
+ DefaultActionValueBinder provider = new DefaultActionValueBinder();
+
+ // Act
+ provider.BindValuesAsync(actionContext, cancellationToken).Wait();
+
+ // Assert
+ Assert.True(actionContext.ModelState.IsValid);
+ Assert.Equal(1, actionContext.ActionArguments.Count);
+ ActionValueItem deserializedActionValueItem = Assert.IsType<ActionValueItem>(actionContext.ActionArguments.First().Value);
+ Assert.Equal(5, deserializedActionValueItem.Id);
+ Assert.Equal("queryFirstName", deserializedActionValueItem.FirstName);
+ Assert.Equal("queryLastName", deserializedActionValueItem.LastName);
+ }
+
+ [Fact]
+ public void BindValuesAsync_Query_String_Values_To_Post_Complex_Types()
+ {
+ // Arrange
+ CancellationToken cancellationToken = new CancellationToken();
+ HttpActionContext actionContext = ContextUtil.CreateActionContext(
+ ContextUtil.CreateControllerContext(new HttpRequestMessage()
+ {
+ Method = HttpMethod.Get,
+ RequestUri = new Uri("http://localhost?id=5&firstName=queryFirstName&lastName=queryLastName")
+ }),
+ new ReflectedHttpActionDescriptor() { MethodInfo = typeof(ActionValueController).GetMethod("PostComplexTypeUri") });
+
+ DefaultActionValueBinder provider = new DefaultActionValueBinder();
+
+ // Act
+ provider.BindValuesAsync(actionContext, cancellationToken).Wait();
+
+ // Assert
+ Assert.True(actionContext.ModelState.IsValid);
+ Assert.Equal(1, actionContext.ActionArguments.Count);
+ ActionValueItem deserializedActionValueItem = Assert.IsType<ActionValueItem>(actionContext.ActionArguments.First().Value);
+ Assert.Equal(5, deserializedActionValueItem.Id);
+ Assert.Equal("queryFirstName", deserializedActionValueItem.FirstName);
+ Assert.Equal("queryLastName", deserializedActionValueItem.LastName);
+ }
+
+ [Fact]
+ public void BindValuesAsync_Query_String_Values_To_Post_Enumerable_Complex_Types()
+ {
+ // Arrange
+ CancellationToken cancellationToken = new CancellationToken();
+ HttpActionContext actionContext = ContextUtil.CreateActionContext(
+ ContextUtil.CreateControllerContext(new HttpRequestMessage()
+ {
+ Method = HttpMethod.Get,
+ RequestUri = new Uri("http://localhost?items[0].id=5&items[0].firstName=queryFirstName&items[0].lastName=queryLastName")
+ }),
+ new ReflectedHttpActionDescriptor() { MethodInfo = typeof(ActionValueController).GetMethod("PostEnumerableUri") });
+
+ DefaultActionValueBinder provider = new DefaultActionValueBinder();
+
+ // Act
+ provider.BindValuesAsync(actionContext, cancellationToken).Wait();
+
+ // Assert
+ Assert.True(actionContext.ModelState.IsValid);
+ Assert.Equal(1, actionContext.ActionArguments.Count);
+ IEnumerable<ActionValueItem> items = Assert.IsAssignableFrom<IEnumerable<ActionValueItem>>(actionContext.ActionArguments.First().Value);
+ ActionValueItem deserializedActionValueItem = items.First();
+ Assert.Equal(5, deserializedActionValueItem.Id);
+ Assert.Equal("queryFirstName", deserializedActionValueItem.FirstName);
+ Assert.Equal("queryLastName", deserializedActionValueItem.LastName);
+ }
+
+ [Fact]
+ public void BindValuesAsync_Query_String_Values_To_Post_Enumerable_Complex_Types_No_Index()
+ {
+ // Arrange
+ CancellationToken cancellationToken = new CancellationToken();
+ HttpActionContext actionContext = ContextUtil.CreateActionContext(
+ ContextUtil.CreateControllerContext(new HttpRequestMessage()
+ {
+ Method = HttpMethod.Get,
+ RequestUri = new Uri("http://localhost?id=5&firstName=queryFirstName&items.lastName=queryLastName")
+ }),
+ new ReflectedHttpActionDescriptor() { MethodInfo = typeof(ActionValueController).GetMethod("PostEnumerableUri") });
+
+ DefaultActionValueBinder provider = new DefaultActionValueBinder();
+
+ // Act
+ provider.BindValuesAsync(actionContext, cancellationToken).Wait();
+
+ // Assert
+ Assert.True(actionContext.ModelState.IsValid);
+ Assert.Equal(1, actionContext.ActionArguments.Count);
+ IEnumerable<ActionValueItem> items = Assert.IsAssignableFrom<IEnumerable<ActionValueItem>>(actionContext.ActionArguments.First().Value);
+ Assert.Equal(0, items.Count()); // expect unsuccessful bind but proves we don't loop infinitely
+ }
+
+ [Fact]
+ public void BindValuesAsync_Query_String_Values_To_ComplexType_Using_Prefixes()
+ {
+ // Arrange
+ CancellationToken cancellationToken = new CancellationToken();
+ HttpActionContext actionContext = ContextUtil.CreateActionContext(
+ ContextUtil.CreateControllerContext(new HttpRequestMessage()
+ {
+ Method = HttpMethod.Get,
+ RequestUri = new Uri("http://localhost?item.id=5&item.firstName=queryFirstName&item.lastName=queryLastName")
+ }),
+ new ReflectedHttpActionDescriptor() { MethodInfo = typeof(ActionValueController).GetMethod("GetItem") });
+
+ DefaultActionValueBinder provider = new DefaultActionValueBinder();
+
+ // Act
+ provider.BindValuesAsync(actionContext, cancellationToken).Wait();
+
+ // Assert
+ Assert.Equal(1, actionContext.ActionArguments.Count);
+ ActionValueItem deserializedActionValueItem = Assert.IsType<ActionValueItem>(actionContext.ActionArguments.First().Value);
+ Assert.Equal(5, deserializedActionValueItem.Id);
+ Assert.Equal("queryFirstName", deserializedActionValueItem.FirstName);
+ Assert.Equal("queryLastName", deserializedActionValueItem.LastName);
+ }
+
+ [Fact]
+ public void BindValuesAsync_Query_String_Values_To_ComplexType_Using_FromUriAttribute()
+ {
+ // Arrange
+ CancellationToken cancellationToken = new CancellationToken();
+ HttpActionContext actionContext = ContextUtil.CreateActionContext(
+ ContextUtil.CreateControllerContext(new HttpRequestMessage()
+ {
+ Method = HttpMethod.Get,
+ RequestUri = new Uri("http://localhost?item.id=5&item.firstName=queryFirstName&item.lastName=queryLastName")
+ }),
+ new ReflectedHttpActionDescriptor() { MethodInfo = typeof(ActionValueController).GetMethod("GetItemFromUri") });
+
+ DefaultActionValueBinder provider = new DefaultActionValueBinder();
+
+ // Act
+ provider.BindValuesAsync(actionContext, cancellationToken).Wait();
+
+ // Assert
+ Assert.Equal(1, actionContext.ActionArguments.Count);
+ ActionValueItem deserializedActionValueItem = Assert.IsType<ActionValueItem>(actionContext.ActionArguments.First().Value);
+ Assert.Equal(5, deserializedActionValueItem.Id);
+ Assert.Equal("queryFirstName", deserializedActionValueItem.FirstName);
+ Assert.Equal("queryLastName", deserializedActionValueItem.LastName);
+ }
+
+ [Fact]
+ public void BindValuesAsync_Query_String_Values_Using_Custom_ValueProviderAttribute()
+ {
+ // Arrange
+ CancellationToken cancellationToken = new CancellationToken();
+ HttpActionContext actionContext = ContextUtil.CreateActionContext(
+ ContextUtil.CreateControllerContext(new HttpRequestMessage()
+ {
+ Method = HttpMethod.Get
+ }),
+ new ReflectedHttpActionDescriptor() { MethodInfo = typeof(ActionValueController).GetMethod("GetFromCustom") });
+
+ DefaultActionValueBinder provider = new DefaultActionValueBinder();
+
+ // Act
+ provider.BindValuesAsync(actionContext, cancellationToken).Wait();
+
+ // Assert
+ Dictionary<string, object> expectedResult = new Dictionary<string, object>();
+ expectedResult["id"] = 99;
+ expectedResult["firstName"] = "99";
+ expectedResult["lastName"] = "99";
+ Assert.Equal(expectedResult, actionContext.ActionArguments, new DictionaryEqualityComparer());
+ }
+
+ [Fact]
+ public void BindValuesAsync_Query_String_Values_Using_Prefix_To_Rename()
+ {
+ // Arrange
+ CancellationToken cancellationToken = new CancellationToken();
+ HttpActionContext actionContext = ContextUtil.CreateActionContext(
+ ContextUtil.CreateControllerContext(new HttpRequestMessage()
+ {
+ Method = HttpMethod.Get,
+ RequestUri = new Uri("http://localhost?custid=5&first=renamedFirstName&last=renamedLastName")
+ // notice the query string names match the prefixes in GetFromNamed() and not the actual parameter names
+ }),
+ new ReflectedHttpActionDescriptor() { MethodInfo = typeof(ActionValueController).GetMethod("GetFromNamed") });
+
+ DefaultActionValueBinder provider = new DefaultActionValueBinder();
+
+ // Act
+ provider.BindValuesAsync(actionContext, cancellationToken).Wait();
+
+ // Assert
+ Dictionary<string, object> expectedResult = new Dictionary<string, object>();
+ expectedResult["id"] = 5;
+ expectedResult["firstName"] = "renamedFirstName";
+ expectedResult["lastName"] = "renamedLastName";
+ Assert.Equal(expectedResult, actionContext.ActionArguments, new DictionaryEqualityComparer());
+ }
+
+ [Fact]
+ public void BindValuesAsync_Query_String_Values_To_Complex_Types_With_Validation_Error()
+ {
+ // Arrange
+ CancellationToken cancellationToken = new CancellationToken();
+ HttpActionContext actionContext = ContextUtil.CreateActionContext(
+ ContextUtil.CreateControllerContext(new HttpRequestMessage()
+ {
+ Method = HttpMethod.Get,
+ RequestUri = new Uri("http://localhost?id=100&firstName=queryFirstName&lastName=queryLastName")
+ }),
+ new ReflectedHttpActionDescriptor() { MethodInfo = typeof(ActionValueController).GetMethod("GetItem") });
+
+ DefaultActionValueBinder provider = new DefaultActionValueBinder();
+
+ // Act
+ provider.BindValuesAsync(actionContext, cancellationToken).Wait();
+
+ // Assert
+ Assert.False(actionContext.ModelState.IsValid);
+ }
+
+ #endregion Query Strings
+
+ #region RouteData
+
+ [Fact]
+ public void BindValuesAsync_RouteData_Values_To_Simple_Types()
+ {
+ // Arrange
+ CancellationToken cancellationToken = new CancellationToken();
+ HttpRouteData route = new HttpRouteData(new HttpRoute());
+ route.Values.Add("id", 6);
+ route.Values.Add("firstName", "routeFirstName");
+ route.Values.Add("lastName", "routeLastName");
+
+ HttpActionContext controllerContext = ContextUtil.CreateActionContext(
+ ContextUtil.CreateControllerContext(route, new HttpRequestMessage()
+ {
+ Method = HttpMethod.Get,
+ RequestUri = new Uri("http://localhost")
+ }),
+ new ReflectedHttpActionDescriptor() { MethodInfo = typeof(ActionValueController).GetMethod("Get") });
+
+ DefaultActionValueBinder provider = new DefaultActionValueBinder();
+
+ // Act
+ provider.BindValuesAsync(controllerContext, cancellationToken).Wait();
+
+ // Assert
+ Dictionary<string, object> expectedResult = new Dictionary<string, object>();
+ expectedResult["id"] = 6;
+ expectedResult["firstName"] = "routeFirstName";
+ expectedResult["lastName"] = "routeLastName";
+ Assert.Equal(expectedResult, controllerContext.ActionArguments, new DictionaryEqualityComparer());
+ }
+
+ [Fact]
+ public void BindValuesAsync_RouteData_Values_To_Simple_Types_Using_FromUriAttribute()
+ {
+ // Arrange
+ CancellationToken cancellationToken = new CancellationToken();
+ HttpRouteData route = new HttpRouteData(new HttpRoute());
+ route.Values.Add("id", 6);
+ route.Values.Add("firstName", "routeFirstName");
+ route.Values.Add("lastName", "routeLastName");
+
+ HttpActionContext controllerContext = ContextUtil.CreateActionContext(
+ ContextUtil.CreateControllerContext(route, new HttpRequestMessage()
+ {
+ Method = HttpMethod.Get,
+ RequestUri = new Uri("http://localhost")
+ }),
+ new ReflectedHttpActionDescriptor() { MethodInfo = typeof(ActionValueController).GetMethod("Get") });
+
+ DefaultActionValueBinder provider = new DefaultActionValueBinder();
+
+ // Act
+ provider.BindValuesAsync(controllerContext, cancellationToken).Wait();
+
+ // Assert
+ Dictionary<string, object> expectedResult = new Dictionary<string, object>();
+ expectedResult["id"] = 6;
+ expectedResult["firstName"] = "routeFirstName";
+ expectedResult["lastName"] = "routeLastName";
+ Assert.Equal(expectedResult, controllerContext.ActionArguments, new DictionaryEqualityComparer());
+ }
+
+ [Fact]
+ public void BindValuesAsync_RouteData_Values_To_Complex_Types()
+ {
+ // Arrange
+ CancellationToken cancellationToken = new CancellationToken();
+ HttpRouteData route = new HttpRouteData(new HttpRoute());
+ route.Values.Add("id", 6);
+ route.Values.Add("firstName", "routeFirstName");
+ route.Values.Add("lastName", "routeLastName");
+
+ HttpActionContext controllerContext = ContextUtil.CreateActionContext(
+ ContextUtil.CreateControllerContext(route, new HttpRequestMessage()
+ {
+ Method = HttpMethod.Get,
+ RequestUri = new Uri("http://localhost")
+ }),
+ new ReflectedHttpActionDescriptor() { MethodInfo = typeof(ActionValueController).GetMethod("GetItem") });
+
+ DefaultActionValueBinder provider = new DefaultActionValueBinder();
+
+ // Act
+ provider.BindValuesAsync(controllerContext, cancellationToken).Wait();
+
+ // Assert
+ Assert.Equal(1, controllerContext.ActionArguments.Count);
+ ActionValueItem deserializedActionValueItem = Assert.IsType<ActionValueItem>(controllerContext.ActionArguments.First().Value);
+ Assert.Equal(6, deserializedActionValueItem.Id);
+ Assert.Equal("routeFirstName", deserializedActionValueItem.FirstName);
+ Assert.Equal("routeLastName", deserializedActionValueItem.LastName);
+ }
+
+ [Fact]
+ public void BindValuesAsync_RouteData_Values_To_Complex_Types_Using_FromUriAttribute()
+ {
+ // Arrange
+ CancellationToken cancellationToken = new CancellationToken();
+ HttpRouteData route = new HttpRouteData(new HttpRoute());
+ route.Values.Add("id", 6);
+ route.Values.Add("firstName", "routeFirstName");
+ route.Values.Add("lastName", "routeLastName");
+
+ HttpActionContext controllerContext = ContextUtil.CreateActionContext(
+ ContextUtil.CreateControllerContext(route, new HttpRequestMessage()
+ {
+ Method = HttpMethod.Get,
+ RequestUri = new Uri("http://localhost")
+ }),
+ new ReflectedHttpActionDescriptor() { MethodInfo = typeof(ActionValueController).GetMethod("GetItemFromUri") });
+
+ DefaultActionValueBinder provider = new DefaultActionValueBinder();
+
+ // Act
+ provider.BindValuesAsync(controllerContext, cancellationToken).Wait();
+
+ // Assert
+ Assert.Equal(1, controllerContext.ActionArguments.Count);
+ ActionValueItem deserializedActionValueItem = Assert.IsType<ActionValueItem>(controllerContext.ActionArguments.First().Value);
+ Assert.Equal(6, deserializedActionValueItem.Id);
+ Assert.Equal("routeFirstName", deserializedActionValueItem.FirstName);
+ Assert.Equal("routeLastName", deserializedActionValueItem.LastName);
+ }
+
+ #endregion RouteData
+
+ #region ControllerContext
+ [Fact]
+ public void BindValuesAsync_ControllerContext_CancellationToken()
+ {
+ // Arrange
+ CancellationToken cancellationToken = new CancellationToken();
+ HttpActionContext actionContext = ContextUtil.CreateActionContext(
+ ContextUtil.CreateControllerContext(new HttpRequestMessage()
+ {
+ Method = HttpMethod.Get
+ }),
+ new ReflectedHttpActionDescriptor() { MethodInfo = typeof(ActionValueController).GetMethod("GetFromCancellationToken") });
+
+ DefaultActionValueBinder provider = new DefaultActionValueBinder();
+
+ // Act
+ provider.BindValuesAsync(actionContext, cancellationToken).Wait();
+
+ // Assert
+ Assert.Equal(1, actionContext.ActionArguments.Count);
+ Assert.Equal(cancellationToken, actionContext.ActionArguments.First().Value);
+ }
+ #endregion ControllerContext
+
+ #region Body
+
+ [Fact]
+ public void BindValuesAsync_Body_To_Complex_Type_Json()
+ {
+ // Arrange
+ CancellationToken cancellationToken = new CancellationToken();
+ string jsonString = "{\"Id\":\"7\",\"FirstName\":\"testFirstName\",\"LastName\":\"testLastName\"}";
+ StringContent stringContent = new StringContent(jsonString, Encoding.UTF8, "application/json");
+
+ HttpRequestMessage request = new HttpRequestMessage() { Content = stringContent };
+ HttpActionContext context = ContextUtil.CreateActionContext(
+ ContextUtil.CreateControllerContext(request),
+ new ReflectedHttpActionDescriptor() { MethodInfo = typeof(ActionValueController).GetMethod("PostComplexType") });
+
+ DefaultActionValueBinder provider = new DefaultActionValueBinder();
+
+ // Act
+ provider.BindValuesAsync(context, cancellationToken).Wait();
+
+ // Assert
+ Assert.Equal(1, context.ActionArguments.Count);
+ ActionValueItem deserializedActionValueItem = Assert.IsAssignableFrom<ActionValueItem>(context.ActionArguments.First().Value);
+ Assert.Equal(7, deserializedActionValueItem.Id);
+ Assert.Equal("testFirstName", deserializedActionValueItem.FirstName);
+ Assert.Equal("testLastName", deserializedActionValueItem.LastName);
+ }
+
+ [Fact]
+ public void BindValuesAsync_Body_To_Complex_Type_Json_With_Validation_Error()
+ {
+ // Arrange
+ CancellationToken cancellationToken = new CancellationToken();
+ string jsonString = "{\"Id\":\"100\",\"FirstName\":\"testFirstName\",\"LastName\":\"testLastName\"}";
+ StringContent stringContent = new StringContent(jsonString, Encoding.UTF8, "application/json");
+
+ HttpRequestMessage request = new HttpRequestMessage() { Content = stringContent };
+ HttpActionContext context = ContextUtil.CreateActionContext(
+ ContextUtil.CreateControllerContext(request),
+ new ReflectedHttpActionDescriptor() { MethodInfo = typeof(ActionValueController).GetMethod("PostComplexType") });
+
+ DefaultActionValueBinder provider = new DefaultActionValueBinder();
+
+ // Act
+ provider.BindValuesAsync(context, cancellationToken).Wait();
+
+ // Assert
+ Assert.False(context.ModelState.IsValid);
+ }
+
+ [Fact]
+ public void BindValuesAsync_Body_To_Complex_Type_FormUrlEncoded()
+ {
+ // Arrange
+ CancellationToken cancellationToken = new CancellationToken();
+ string formUrlEncodedString = "Id=7&FirstName=testFirstName&LastName=testLastName";
+ StringContent stringContent = new StringContent(formUrlEncodedString, Encoding.UTF8, "application/x-www-form-urlencoded");
+
+ HttpRequestMessage request = new HttpRequestMessage() { Content = stringContent };
+ HttpActionContext context = ContextUtil.CreateActionContext(
+ ContextUtil.CreateControllerContext(request),
+ new ReflectedHttpActionDescriptor() { MethodInfo = typeof(ActionValueController).GetMethod("PostComplexType") });
+
+ DefaultActionValueBinder provider = new DefaultActionValueBinder();
+
+ // Act
+ provider.BindValuesAsync(context, cancellationToken).Wait();
+
+ // Assert
+ Assert.Equal(1, context.ActionArguments.Count);
+ ActionValueItem deserializedActionValueItem = Assert.IsAssignableFrom<ActionValueItem>(context.ActionArguments.First().Value);
+ Assert.Equal(7, deserializedActionValueItem.Id);
+ Assert.Equal("testFirstName", deserializedActionValueItem.FirstName);
+ Assert.Equal("testLastName", deserializedActionValueItem.LastName);
+ }
+
+ [Fact]
+ public void BindValuesAsync_Body_To_Complex_Type_FormUrlEncoded_With_Validation_Error()
+ {
+ // Arrange
+ CancellationToken cancellationToken = new CancellationToken();
+ string formUrlEncodedString = "Id=101&FirstName=testFirstName&LastName=testLastName";
+ StringContent stringContent = new StringContent(formUrlEncodedString, Encoding.UTF8, "application/x-www-form-urlencoded");
+
+ HttpRequestMessage request = new HttpRequestMessage() { Content = stringContent };
+ HttpActionContext context = ContextUtil.CreateActionContext(
+ ContextUtil.CreateControllerContext(request),
+ new ReflectedHttpActionDescriptor() { MethodInfo = typeof(ActionValueController).GetMethod("PostComplexType") });
+
+ DefaultActionValueBinder provider = new DefaultActionValueBinder();
+
+ // Act
+ provider.BindValuesAsync(context, cancellationToken).Wait();
+
+ // Assert
+ Assert.False(context.ModelState.IsValid);
+ }
+
+ [Fact]
+ public void BindValuesAsync_Body_To_Complex_Type_Xml()
+ {
+ // Arrange
+ CancellationToken cancellationToken = new CancellationToken();
+ MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue("application/xml");
+ ActionValueItem item = new ActionValueItem() { Id = 7, FirstName = "testFirstName", LastName = "testLastName" };
+ ObjectContent<ActionValueItem> tempContent = new ObjectContent<ActionValueItem>(item, new XmlMediaTypeFormatter());
+ StringContent stringContent = new StringContent(tempContent.ReadAsStringAsync().Result);
+ stringContent.Headers.ContentType = mediaType;
+ HttpRequestMessage request = new HttpRequestMessage() { Content = stringContent };
+ HttpActionContext context = ContextUtil.CreateActionContext(
+ ContextUtil.CreateControllerContext(request),
+ new ReflectedHttpActionDescriptor() { MethodInfo = typeof(ActionValueController).GetMethod("PostComplexType") });
+
+ DefaultActionValueBinder provider = new DefaultActionValueBinder();
+
+ // Act
+ provider.BindValuesAsync(context, cancellationToken).Wait();
+
+ // Assert
+ Assert.Equal(1, context.ActionArguments.Count);
+ ActionValueItem deserializedActionValueItem = Assert.IsAssignableFrom<ActionValueItem>(context.ActionArguments.First().Value);
+ Assert.Equal(item.Id, deserializedActionValueItem.Id);
+ Assert.Equal(item.FirstName, deserializedActionValueItem.FirstName);
+ Assert.Equal(item.LastName, deserializedActionValueItem.LastName);
+ }
+
+ [Fact]
+ public void BindValuesAsync_Body_To_Complex_Type_Xml_Structural()
+ {
+ // Arrange
+ CancellationToken cancellationToken = new CancellationToken();
+ MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue("application/xml");
+
+ // Test sending from a non .NET type (raw xml).
+ // The default XML serializer requires that the xml root name matches the C# class name.
+ string xmlSource =
+ @"<?xml version='1.0' encoding='utf-8'?>
+ <ActionValueItem xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:xsd='http://www.w3.org/2001/XMLSchema'>
+ <Id>7</Id>
+ <FirstName>testFirstName</FirstName>
+ <LastName>testLastName</LastName>
+ </ActionValueItem>".Replace('\'', '"');
+
+ StringContent stringContent = new StringContent(xmlSource);
+ stringContent.Headers.ContentType = mediaType;
+ HttpRequestMessage request = new HttpRequestMessage() { Content = stringContent };
+ HttpActionContext context = ContextUtil.CreateActionContext(
+ ContextUtil.CreateControllerContext(request),
+ new ReflectedHttpActionDescriptor() { MethodInfo = typeof(ActionValueController).GetMethod("PostComplexType") });
+
+ DefaultActionValueBinder provider = new DefaultActionValueBinder();
+
+ // Act
+ provider.BindValuesAsync(context, cancellationToken).Wait();
+
+ // Assert
+ Assert.Equal(1, context.ActionArguments.Count);
+ ActionValueItem deserializedActionValueItem = Assert.IsAssignableFrom<ActionValueItem>(context.ActionArguments.First().Value);
+ Assert.Equal(7, deserializedActionValueItem.Id);
+ Assert.Equal("testFirstName", deserializedActionValueItem.FirstName);
+ Assert.Equal("testLastName", deserializedActionValueItem.LastName);
+ }
+
+ [Fact]
+ public void BindValuesAsync_Body_To_Complex_Type_Xml_With_Validation_Error()
+ {
+ // Arrange
+ CancellationToken cancellationToken = new CancellationToken();
+ MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue("application/xml");
+ ActionValueItem item = new ActionValueItem() { Id = 101, FirstName = "testFirstName", LastName = "testLastName" };
+ var tempContent = new ObjectContent<ActionValueItem>(item, new XmlMediaTypeFormatter());
+ StringContent stringContent = new StringContent(tempContent.ReadAsStringAsync().Result);
+ stringContent.Headers.ContentType = mediaType;
+ HttpRequestMessage request = new HttpRequestMessage() { Content = stringContent };
+ HttpActionContext context = ContextUtil.CreateActionContext(
+ ContextUtil.CreateControllerContext(request),
+ new ReflectedHttpActionDescriptor() { MethodInfo = typeof(ActionValueController).GetMethod("PostComplexType") });
+
+ DefaultActionValueBinder provider = new DefaultActionValueBinder();
+
+ // Act
+ provider.BindValuesAsync(context, cancellationToken).Wait();
+
+ // Assert
+ Assert.False(context.ModelState.IsValid);
+ }
+
+ [Fact]
+ public void BindValuesAsync_Body_To_Complex_And_Uri_To_Simple()
+ {
+ // Arrange
+ string jsonString = "{\"Id\":\"7\",\"FirstName\":\"testFirstName\",\"LastName\":\"testLastName\"}";
+ StringContent stringContent = new StringContent(jsonString, Encoding.UTF8, "application/json");
+
+ HttpRequestMessage request = new HttpRequestMessage()
+ {
+ RequestUri = new Uri("http://localhost/ActionValueController/PostFromBody?id=123"),
+ Content = stringContent
+ };
+
+ HttpActionContext context = ContextUtil.CreateActionContext(
+ ContextUtil.CreateControllerContext(request),
+ new ReflectedHttpActionDescriptor() { MethodInfo = typeof(ActionValueController).GetMethod("PostFromBodyAndUri") });
+
+ DefaultActionValueBinder provider = new DefaultActionValueBinder();
+
+ // Act
+ provider.BindValuesAsync(context, CancellationToken.None).Wait();
+
+ // Assert
+ Assert.Equal(2, context.ActionArguments.Count);
+ Assert.Equal(123, context.ActionArguments["id"]);
+
+ ActionValueItem deserializedActionValueItem = Assert.IsAssignableFrom<ActionValueItem>(context.ActionArguments["item"]);
+ Assert.Equal(7, deserializedActionValueItem.Id);
+ Assert.Equal("testFirstName", deserializedActionValueItem.FirstName);
+ Assert.Equal("testLastName", deserializedActionValueItem.LastName);
+ }
+
+ [Fact]
+ public void BindValuesAsync_Body_To_Complex_Type_Using_FromBodyAttribute()
+ {
+ // Arrange
+ CancellationToken cancellationToken = new CancellationToken();
+ string jsonString = "{\"Id\":\"7\",\"FirstName\":\"testFirstName\",\"LastName\":\"testLastName\"}";
+ StringContent stringContent = new StringContent(jsonString, Encoding.UTF8, "application/json");
+
+ HttpRequestMessage request = new HttpRequestMessage() { Content = stringContent };
+
+ HttpActionContext context = ContextUtil.CreateActionContext(
+ ContextUtil.CreateControllerContext(request),
+ new ReflectedHttpActionDescriptor() { MethodInfo = typeof(ActionValueController).GetMethod("PostFromBody") });
+
+ DefaultActionValueBinder provider = new DefaultActionValueBinder();
+
+ // Act
+ provider.BindValuesAsync(context, cancellationToken).Wait();
+
+ // Assert
+ Assert.Equal(1, context.ActionArguments.Count);
+ ActionValueItem deserializedActionValueItem = Assert.IsAssignableFrom<ActionValueItem>(context.ActionArguments.First().Value);
+ Assert.Equal(7, deserializedActionValueItem.Id);
+ Assert.Equal("testFirstName", deserializedActionValueItem.FirstName);
+ Assert.Equal("testLastName", deserializedActionValueItem.LastName);
+ }
+
+ [Fact]
+ public void BindValuesAsync_Body_To_Complex_Type_Using_Formatter_To_Deserialize()
+ {
+ // Arrange
+ CancellationToken cancellationToken = new CancellationToken();
+ string jsonString = "{\"Id\":\"7\",\"FirstName\":\"testFirstName\",\"LastName\":\"testLastName\"}";
+ StringContent stringContent = new StringContent(jsonString, Encoding.UTF8, "application/json");
+
+ HttpRequestMessage request = new HttpRequestMessage() { Content = stringContent };
+ HttpActionContext context = ContextUtil.CreateActionContext(
+ ContextUtil.CreateControllerContext(request),
+ new ReflectedHttpActionDescriptor() { MethodInfo = typeof(ActionValueController).GetMethod("PostComplexType") });
+ DefaultActionValueBinder provider = new DefaultActionValueBinder();
+
+ // Act
+ provider.BindValuesAsync(context, cancellationToken).Wait();
+
+ // Assert
+ Assert.Equal(1, context.ActionArguments.Count);
+ ActionValueItem deserializedActionValueItem = Assert.IsAssignableFrom<ActionValueItem>(context.ActionArguments.First().Value);
+ Assert.Equal(7, deserializedActionValueItem.Id);
+ Assert.Equal("testFirstName", deserializedActionValueItem.FirstName);
+ Assert.Equal("testLastName", deserializedActionValueItem.LastName);
+ }
+
+
+ [Fact]
+ public void BindValuesAsync_Body_To_IEnumerable_Complex_Type_Json()
+ {
+ // ModelBinding will bind T to IEnumerable<T>, but JSON.Net won't. So enclose JSON in [].
+ // Arrange
+ CancellationToken cancellationToken = new CancellationToken();
+ string jsonString = "[{\"Id\":\"7\",\"FirstName\":\"testFirstName\",\"LastName\":\"testLastName\"}]";
+ StringContent stringContent = new StringContent(jsonString, Encoding.UTF8, "application/json");
+
+ HttpRequestMessage request = new HttpRequestMessage() { Content = stringContent };
+ HttpActionContext context = ContextUtil.CreateActionContext(
+ ContextUtil.CreateControllerContext(request),
+ new ReflectedHttpActionDescriptor() { MethodInfo = typeof(ActionValueController).GetMethod("PostEnumerable") });
+
+ DefaultActionValueBinder provider = new DefaultActionValueBinder();
+
+ // Act
+ provider.BindValuesAsync(context, cancellationToken).Wait();
+
+ // Assert
+ Assert.Equal(1, context.ActionArguments.Count);
+ IEnumerable<ActionValueItem> items = Assert.IsAssignableFrom<IEnumerable<ActionValueItem>>(context.ActionArguments.First().Value);
+ ActionValueItem deserializedActionValueItem = items.First();
+ Assert.Equal(7, deserializedActionValueItem.Id);
+ Assert.Equal("testFirstName", deserializedActionValueItem.FirstName);
+ Assert.Equal("testLastName", deserializedActionValueItem.LastName);
+ }
+
+ [Fact]
+ public void BindValuesAsync_Body_To_JsonValue()
+ {
+ // Arrange
+ CancellationToken cancellationToken = new CancellationToken();
+ MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue("application/json");
+ ActionValueItem item = new ActionValueItem() { Id = 7, FirstName = "testFirstName", LastName = "testLastName" };
+ string json = "{\"a\":123,\"b\":[false,null,12.34]}";
+ JsonValue jv = JsonValue.Parse(json);
+ var tempContent = new ObjectContent<JsonValue>(jv, new JsonMediaTypeFormatter());
+ StringContent stringContent = new StringContent(tempContent.ReadAsStringAsync().Result);
+ stringContent.Headers.ContentType = mediaType;
+ HttpRequestMessage request = new HttpRequestMessage() { Content = stringContent };
+ HttpActionContext context = ContextUtil.CreateActionContext(
+ ContextUtil.CreateControllerContext(request),
+ new ReflectedHttpActionDescriptor() { MethodInfo = typeof(ActionValueController).GetMethod("PostJsonValue") });
+
+ DefaultActionValueBinder provider = new DefaultActionValueBinder();
+
+ // Act
+ provider.BindValuesAsync(context, cancellationToken).Wait();
+
+ // Assert
+ Assert.Equal(1, context.ActionArguments.Count);
+ JsonValue deserializedJsonValue = Assert.IsAssignableFrom<JsonValue>(context.ActionArguments.First().Value);
+ string deserializedJsonAsString = deserializedJsonValue.ToString();
+ Assert.Equal(json, deserializedJsonAsString);
+ }
+
+ #endregion Body
+ }
+
+ public class ActionValueController : ApiController
+ {
+ // Demonstrates parameter that can come from route, query string, or defaults
+ public ActionValueItem Get(int id = 0, string firstName = "DefaultFirstName", string lastName = "DefaultLastName")
+ {
+ return new ActionValueItem() { Id = id, FirstName = firstName, LastName = lastName };
+ }
+
+ // Demonstrates an explicit override to obtain parameters from URL
+ public ActionValueItem GetFromUri([FromUri] int id = 0,
+ [FromUri] string firstName = "DefaultFirstName",
+ [FromUri] string lastName = "DefaultLastName")
+ {
+ return new ActionValueItem() { Id = id, FirstName = firstName, LastName = lastName };
+ }
+
+
+ // Complex objects default to body. But we can bind from URI with an attribute.
+ public ActionValueItem GetItem([FromUri] ActionValueItem item)
+ {
+ return item;
+ }
+
+ // Demonstrates ModelBinding a Item object explicitly from Uri
+ public ActionValueItem GetItemFromUri([FromUri] ActionValueItem item)
+ {
+ return item;
+ }
+
+ // Demonstrates use of renaming parameters via prefix
+ public ActionValueItem GetFromNamed([FromUri(Prefix = "custID")] int id,
+ [FromUri(Prefix = "first")] string firstName,
+ [FromUri(Prefix = "last")] string lastName)
+ {
+ return new ActionValueItem() { Id = id, FirstName = firstName, LastName = lastName };
+ }
+
+ // Demonstrates use of custom ValueProvider via attribute
+ public ActionValueItem GetFromCustom([ValueProvider(typeof(ActionValueControllerValueProviderFactory), Prefix = "id")] int id,
+ [ValueProvider(typeof(ActionValueControllerValueProviderFactory), Prefix = "customFirstName")] string firstName,
+ [ValueProvider(typeof(ActionValueControllerValueProviderFactory), Prefix = "customLastName")] string lastName)
+ {
+ return new ActionValueItem() { Id = id, FirstName = firstName, LastName = lastName };
+ }
+
+ // Demonstrates ModelBinding to the CancellationToken of the current request
+ public string GetFromCancellationToken(CancellationToken cancellationToken)
+ {
+ return cancellationToken.ToString();
+ }
+
+ // Demonstrates ModelBinding to the ModelState of the current request
+ public string GetFromModelState(ModelState modelState)
+ {
+ return modelState.ToString();
+ }
+
+ // Demonstrates binding to complex type from body
+ public ActionValueItem PostComplexType(ActionValueItem item)
+ {
+ return item;
+ }
+
+ // Demonstrates binding to complex type from uri
+ public ActionValueItem PostComplexTypeUri([FromUri] ActionValueItem item)
+ {
+ return item;
+ }
+
+ // Demonstrates binding to IEnumerable of complex type from body or Uri
+ public ActionValueItem PostEnumerable(IEnumerable<ActionValueItem> items)
+ {
+ return items.FirstOrDefault();
+ }
+
+ // Demonstrates binding to IEnumerable of complex type from body or Uri
+ public ActionValueItem PostEnumerableUri([FromUri] IEnumerable<ActionValueItem> items)
+ {
+ return items.FirstOrDefault();
+ }
+
+ // Demonstrates binding to JsonValue from body
+ public JsonValue PostJsonValue(JsonValue jsonValue)
+ {
+ return jsonValue;
+ }
+
+ // Demonstrate what we expect to be the common default scenario. No attributes are required.
+ // A complex object comes from the body, and simple objects come from the URI.
+ public ActionValueItem PostFromBodyAndUri(int id, ActionValueItem item)
+ {
+ return item;
+ }
+
+ // Demonstrates binding to complex type explicitly marked as coming from body
+ public ActionValueItem PostFromBody([FromBody] ActionValueItem item)
+ {
+ return item;
+ }
+
+ // Demonstrates how body can be shredded to name/value pairs to bind to simple types
+ public ActionValueItem PostToSimpleTypes(int id, string firstName, string lastName)
+ {
+ return new ActionValueItem() { Id = id, FirstName = firstName, LastName = lastName };
+ }
+
+ // Demonstrates binding to ObjectContent<T> from request body
+ public ActionValueItem PostObjectContentOfItem(ObjectContent<ActionValueItem> item)
+ {
+ return item.ReadAsAsync<ActionValueItem>().Result;
+ }
+
+ public class ActionValueControllerValueProviderFactory : ValueProviderFactory
+ {
+ public override IValueProvider GetValueProvider(HttpActionContext actionContext)
+ {
+ return new ActionValueControllerValueProvider();
+ }
+ }
+
+ public class ActionValueControllerValueProvider : IValueProvider
+ {
+ public bool ContainsPrefix(string prefix)
+ {
+ return true;
+ }
+
+ public ValueProviderResult GetValue(string key)
+ {
+ return new ValueProviderResult("99", "99", CultureInfo.CurrentCulture);
+ }
+ }
+ }
+
+ static class DefaultActionValueBinderExtensions
+ {
+ public static Task BindValuesAsync(this DefaultActionValueBinder binder, HttpActionContext actionContext, CancellationToken cancellationToken)
+ {
+ HttpActionBinding binding = binder.GetBinding(actionContext.ActionDescriptor);
+ return binding.ExecuteBindingAsync(actionContext, cancellationToken);
+ }
+ }
+
+ public class ActionValueItem
+ {
+ [Range(0, 99)]
+ public int Id { get; set; }
+ public string FirstName { get; set; }
+ public string LastName { get; set; }
+ }
+}
diff --git a/test/System.Web.Http.Integration.Test/ModelBinding/HttpContentBindingTests.cs b/test/System.Web.Http.Integration.Test/ModelBinding/HttpContentBindingTests.cs
new file mode 100644
index 00000000..527a2ea1
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/ModelBinding/HttpContentBindingTests.cs
@@ -0,0 +1,92 @@
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Formatting;
+using System.Web.Http.SelfHost;
+using Xunit;
+using Xunit.Extensions;
+
+namespace System.Web.Http.ModelBinding
+{
+ /// <summary>
+ /// Tests actions that directly use HttpRequestMessage parameters
+ /// </summary>
+ public class HttpContentBindingTests : IDisposable
+ {
+ public HttpContentBindingTests()
+ {
+ this.SetupHost();
+ }
+
+ public void Dispose()
+ {
+ this.CleanupHost();
+ }
+
+ [Theory]
+ [InlineData("application/xml")]
+ [InlineData("text/xml")]
+ [InlineData("application/json")]
+ [InlineData("text/json")]
+ public void Action_Directly_Reads_HttpRequestMessage(string mediaType)
+ {
+ Order order = new Order() { OrderId = "99", OrderValue = 100.0 };
+ var formatter = new MediaTypeFormatterCollection().Find(mediaType);
+ HttpRequestMessage request = new HttpRequestMessage()
+ {
+ Content = new ObjectContent<Order>(order, formatter, mediaType),
+ RequestUri = new Uri(baseAddress + "/HttpContentBinding/HandleMessage"),
+ Method = HttpMethod.Post
+ };
+
+ HttpResponseMessage response = httpClient.SendAsync(request).Result;
+
+ Order receivedOrder = response.Content.ReadAsAsync<Order>().Result;
+ Assert.Equal(order.OrderId, receivedOrder.OrderId);
+ Assert.Equal(order.OrderValue, receivedOrder.OrderValue);
+ }
+
+ private HttpSelfHostServer server = null;
+ private string baseAddress = null;
+ private HttpClient httpClient = null;
+
+ private void SetupHost()
+ {
+ httpClient = new HttpClient();
+
+ baseAddress = String.Format("http://{0}", Environment.MachineName);
+
+ HttpSelfHostConfiguration config = new HttpSelfHostConfiguration(baseAddress);
+ config.Routes.MapHttpRoute("Default", "{controller}/{action}", new { controller = "HttpContentBinding", action = "HandleMessage" });
+
+ server = new HttpSelfHostServer(config);
+ server.OpenAsync().Wait();
+ }
+
+ private void CleanupHost()
+ {
+ if (server != null)
+ {
+ server.CloseAsync().Wait();
+ }
+ }
+ }
+
+ public class Order
+ {
+ public string OrderId { get; set; }
+ public double OrderValue { get; set; }
+ }
+
+ public class HttpContentBindingController : ApiController
+ {
+ [HttpPost]
+ public HttpResponseMessage HandleMessage()
+ {
+ Order order = Request.Content.ReadAsAsync<Order>().Result;
+ return new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new ObjectContent<Order>(order, new JsonMediaTypeFormatter())
+ };
+ }
+ }
+} \ No newline at end of file
diff --git a/test/System.Web.Http.Integration.Test/ModelBinding/ModelBindingController.cs b/test/System.Web.Http.Integration.Test/ModelBinding/ModelBindingController.cs
new file mode 100644
index 00000000..09af7b52
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/ModelBinding/ModelBindingController.cs
@@ -0,0 +1,272 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Globalization;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Controllers;
+using System.Web.Http.ValueProviders;
+using Xunit;
+
+namespace System.Web.Http.ModelBinding
+{
+ public class ModelBindingController : ApiController
+ {
+ public string GetString(string value)
+ {
+ return value;
+ }
+
+ public string GetStringFromRoute(string controller, string action)
+ {
+ return controller + ":" + action;
+ }
+
+ public int GetInt(int value)
+ {
+ return value;
+ }
+
+ public int GetIntWithDefault(int value = -1)
+ {
+ return value;
+ }
+
+ public int GetIntFromUri([FromUri] int value)
+ {
+ return value;
+ }
+
+ public int GetIntPrefixed([FromUri(Prefix = "somePrefix")] int value)
+ {
+ return value;
+ }
+
+ public int GetIntCustom([ValueProvider(typeof(RequestHeadersValueProviderFactory))] int value)
+ {
+ return value;
+ }
+
+ public Task<int> GetIntAsync(int value, CancellationToken token)
+ {
+ Assert.NotNull(token);
+ TaskCompletionSource<int> tcs = new TaskCompletionSource<int>();
+ tcs.TrySetResult(value);
+ return tcs.Task;
+ }
+
+ public bool GetBool(bool value)
+ {
+ return value;
+ }
+
+ public ModelBindOrder GetComplexType(ModelBindOrder item)
+ {
+ return item;
+ }
+
+ public ModelBindOrder GetComplexTypeFromUri([FromUri] ModelBindOrder item)
+ {
+ return item;
+ }
+
+ public string PostString(string value)
+ {
+ return value;
+ }
+
+ public int PostInt(int value)
+ {
+ return value;
+ }
+
+ public HttpResponseMessage PostComplexWithValidation(CustomerNameMax6 customer)
+ {
+ // Request should not be null
+ if (this.Request == null)
+ {
+ throw new HttpResponseException("ApiController.Request should not be null.");
+ }
+
+ // Configuration should not be null
+ if (this.Configuration == null)
+ {
+ throw new HttpResponseException("ApiController.Configuration should not be null.");
+ }
+
+ // ModelState information
+ if (this.ModelState == null)
+ {
+ throw new HttpResponseException("ApiController.ModelState should not be null.");
+ }
+ else
+ {
+ string errors = String.Empty;
+ foreach (var kv in this.ModelState)
+ {
+ int errorCount = kv.Value.Errors.Count;
+
+ if (errorCount > 0)
+ {
+ errors += String.Format("Failed to bind {0}. The errors are:", kv.Key);
+ for (int i = 0; i < errorCount; i++)
+ {
+ ModelError error = kv.Value.Errors[i];
+ errors += "\nErrorMessage: " + error.ErrorMessage;
+
+ if (error.Exception != null)
+ {
+ errors += "\nException" + error.Exception;
+ }
+ }
+ }
+ }
+
+ if (errors != String.Empty)
+ {
+ // Has validation failure
+ // TODO, 334736, support HttpResponseException which takes ModelState
+ // throw new HttpResponseException(this.ModelState);
+ HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.BadRequest);
+ response.Content = new StringContent(errors);
+ throw new HttpResponseException(response);
+ }
+ else
+ {
+ // happy path
+ return Request.CreateResponse<int>(HttpStatusCode.OK, customer.Id);
+ }
+ }
+ }
+
+ public int PostIntFromUri([FromUri] int value)
+ {
+ return value;
+ }
+
+ public int PostIntFromBody([FromBody] int value)
+ {
+ return value;
+ }
+
+ public int PostIntUriPrefixed([FromUri(Prefix = "somePrefix")] int value)
+ {
+ return value;
+ }
+
+ public bool PostBool(bool value)
+ {
+ return value;
+ }
+
+ public int PostIntArray([FromUri] int[] value)
+ {
+ return value.Sum();
+ }
+
+ public ModelBindOrder PostComplexType(ModelBindOrder item)
+ {
+ return item;
+ }
+
+ public ModelBindOrder PostComplexTypeFromUri([FromUri] ModelBindOrder item)
+ {
+ return item;
+ }
+
+ public ModelBindOrder PostComplexTypeFromBody([FromBody] ModelBindOrder item)
+ {
+ return item;
+ }
+
+ // check if HttpRequestMessage prevents binding other parameters
+ public int PostComplexTypeHttpRequestMessage(HttpRequestMessage request, ModelBindOrder order)
+ {
+ return Int32.Parse(order.ItemName) + order.Quantity;
+ }
+ }
+
+ public class CustomerNameMax6
+ {
+ [Required]
+ [StringLength(6)]
+ public string Name { get; set; }
+
+ public int Id { get; set; }
+ }
+
+ public class ModelBindCustomer
+ {
+ public string Name { get; set; }
+ }
+
+ public class ModelBindOrder
+ {
+ public string ItemName { get; set; }
+ public int Quantity { get; set; }
+ public ModelBindCustomer Customer { get; set; }
+ }
+
+ public class ModelBindOrderEqualityComparer : IEqualityComparer<ModelBindOrder>
+ {
+ public bool Equals(ModelBindOrder x, ModelBindOrder y)
+ {
+ Assert.True(x != null, "Expected ModelBindOrder cannot be null.");
+ Assert.True(y != null, "Actual ModelBindOrder was null.");
+ Assert.Equal<string>(x.ItemName, y.ItemName);
+ Assert.Equal<int>(x.Quantity, y.Quantity);
+
+ if (x.Customer != null)
+ {
+ Assert.True(y.Customer != null, "Actual Customer was null but expected was " + x.Customer.Name);
+ }
+ else if (x.Customer == null)
+ {
+ Assert.True(y.Customer == null, "Actual Customer was not null but should have been.");
+ }
+ else
+ {
+ Assert.True(String.Equals(x.Customer.Name, y.Customer.Name, StringComparison.Ordinal), String.Format("Expected Customer.Name '{0}' but actual was '{1}'", x.Customer.Name, y.Customer.Name));
+ }
+
+ return true;
+ }
+
+ public int GetHashCode(ModelBindOrder obj)
+ {
+ return obj.GetHashCode();
+ }
+ }
+
+ public class RequestHeadersValueProviderFactory : ValueProviderFactory
+ {
+ public override IValueProvider GetValueProvider(HttpActionContext actionContext)
+ {
+ return new RequestHeaderValueProvider(actionContext);
+ }
+ }
+
+ public class RequestHeaderValueProvider : IValueProvider
+ {
+ HttpActionContext _actionContext;
+ public RequestHeaderValueProvider(HttpActionContext actionContext)
+ {
+ _actionContext = actionContext;
+ }
+
+ public bool ContainsPrefix(string prefix)
+ {
+ return _actionContext.ControllerContext.Request.Headers.Contains(prefix);
+ }
+
+ public ValueProviderResult GetValue(string key)
+ {
+ string result = _actionContext.ControllerContext.Request.Headers.GetValues(key).FirstOrDefault();
+ return result == null
+ ? null
+ : new ValueProviderResult(result, result, CultureInfo.CurrentCulture);
+ }
+ }
+} \ No newline at end of file
diff --git a/test/System.Web.Http.Integration.Test/ModelBinding/ModelBindingTests.cs b/test/System.Web.Http.Integration.Test/ModelBinding/ModelBindingTests.cs
new file mode 100644
index 00000000..86e26892
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/ModelBinding/ModelBindingTests.cs
@@ -0,0 +1,48 @@
+using System.Net.Http;
+using System.Web.Http.Common;
+using System.Web.Http.SelfHost;
+
+namespace System.Web.Http.ModelBinding
+{
+ /// <summary>
+ /// End to end functional tests for model binding
+ /// </summary>
+ public abstract class ModelBindingTests : IDisposable
+ {
+ protected HttpSelfHostServer server = null;
+ protected HttpSelfHostConfiguration configuration = null;
+ protected string baseAddress = null;
+ protected HttpClient httpClient = null;
+
+ protected ModelBindingTests()
+ {
+ this.SetupHost();
+ }
+
+ public void Dispose()
+ {
+ this.CleanupHost();
+ }
+
+ public void SetupHost()
+ {
+ httpClient = new HttpClient();
+
+ baseAddress = String.Format("http://{0}/", Environment.MachineName);
+
+ configuration = new HttpSelfHostConfiguration(baseAddress);
+ configuration.Routes.MapHttpRoute("Default", "{controller}/{action}", new { controller = "ModelBinding" });
+
+ server = new HttpSelfHostServer(configuration);
+ server.OpenAsync().Wait();
+ }
+
+ public void CleanupHost()
+ {
+ if (server != null)
+ {
+ server.CloseAsync().Wait();
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/test/System.Web.Http.Integration.Test/ModelBinding/QueryStringBindingTests.cs b/test/System.Web.Http.Integration.Test/ModelBinding/QueryStringBindingTests.cs
new file mode 100644
index 00000000..4b774c73
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/ModelBinding/QueryStringBindingTests.cs
@@ -0,0 +1,115 @@
+using System.Net.Http;
+using Xunit;
+using Xunit.Extensions;
+
+namespace System.Web.Http.ModelBinding
+{
+ /// <summary>
+ /// End to end functional tests for model binding via query strings
+ /// </summary>
+ public class QueryStringBindingTests : ModelBindingTests
+ {
+ [Theory]
+ [InlineData("GetString", "?value=test", "\"test\"")]
+ [InlineData("GetInt", "?value=99", "99")]
+ [InlineData("GetBool", "?value=false", "false")]
+ [InlineData("GetBool", "?value=true", "true")]
+ [InlineData("GetIntWithDefault", "?value=99", "99")] // action has default, but we provide value
+ [InlineData("GetIntWithDefault", "", "-1")] // action has default, we provide no value
+ [InlineData("GetIntFromUri", "?value=99", "99")] // [FromUri]
+ [InlineData("GetIntPrefixed", "?somePrefix=99", "99")] // [FromUri(Prefix=somePrefix)]
+ [InlineData("GetIntAsync", "?value=5", "5")]
+ public void Query_String_Binds_Simple_Types_Get(string action, string queryString, string expectedResponse)
+ {
+ // Arrange
+ HttpRequestMessage request = new HttpRequestMessage()
+ {
+ RequestUri = new Uri(baseAddress + String.Format("ModelBinding/{0}{1}", action, queryString)),
+ Method = HttpMethod.Get
+ };
+
+ // Act
+ HttpResponseMessage response = httpClient.SendAsync(request).Result;
+
+ // Assert
+ string responseString = response.Content.ReadAsStringAsync().Result;
+ Assert.Equal<string>(expectedResponse, responseString);
+ }
+
+ [Theory]
+ [InlineData("PostString", "?value=test", "\"test\"")]
+ [InlineData("PostInt", "?value=99", "99")]
+ [InlineData("PostBool", "?value=false", "false")]
+ [InlineData("PostBool", "?value=true", "true")]
+ [InlineData("PostIntFromUri", "?value=99", "99")] // [FromUri]
+ [InlineData("PostIntUriPrefixed", "?somePrefix=99", "99")] // [FromUri(Prefix=somePrefix)]
+ [InlineData("PostIntArray", "?value={[1,2,3]}", "0")] // TODO: DevDiv2 333257 -- make this array real when fix JsonValue array model binding
+ public void Query_String_Binds_Simple_Types_Post(string action, string queryString, string expectedResponse)
+ {
+ // Arrange
+ HttpRequestMessage request = new HttpRequestMessage()
+ {
+ RequestUri = new Uri(baseAddress + String.Format("ModelBinding/{0}{1}", action, queryString)),
+ Method = HttpMethod.Post
+ };
+
+ // Act
+ HttpResponseMessage response = httpClient.SendAsync(request).Result;
+
+ // Assert
+ string responseString = response.Content.ReadAsStringAsync().Result;
+ Assert.Equal<string>(expectedResponse, responseString);
+ }
+
+ [Theory]
+ [InlineData("GetComplexTypeFromUri", "itemName=Tires&quantity=2&customer.Name=Sue", "Tires", 2, "Sue")]
+ public void Query_String_ComplexType_Type_Get(string action, string queryString, string itemName, int quantity, string customerName)
+ {
+ // Arrange
+ HttpRequestMessage request = new HttpRequestMessage()
+ {
+ RequestUri = new Uri(baseAddress + String.Format("ModelBinding/{0}?{1}", action, queryString)),
+ Method = HttpMethod.Get
+ };
+
+ ModelBindOrder expectedItem = new ModelBindOrder()
+ {
+ ItemName = itemName,
+ Quantity = quantity,
+ Customer = new ModelBindCustomer { Name = customerName }
+ };
+
+ // Act
+ HttpResponseMessage response = httpClient.SendAsync(request).Result;
+
+ // Assert
+ ModelBindOrder actualItem = response.Content.ReadAsAsync<ModelBindOrder>().Result;
+ Assert.Equal<ModelBindOrder>(expectedItem, actualItem, new ModelBindOrderEqualityComparer());
+ }
+
+ [Theory]
+ [InlineData("PostComplexTypeFromUri", "itemName=Tires&quantity=2&customer.Name=Bob", "Tires", 2, "Bob")]
+ public void Query_String_ComplexType_Type_Post(string action, string queryString, string itemName, int quantity, string customerName)
+ {
+ // Arrange
+ HttpRequestMessage request = new HttpRequestMessage()
+ {
+ RequestUri = new Uri(baseAddress + String.Format("ModelBinding/{0}?{1}", action, queryString)),
+ Method = HttpMethod.Post
+ };
+ ModelBindOrder expectedItem = new ModelBindOrder()
+ {
+ ItemName = itemName,
+ Quantity = quantity,
+ Customer = new ModelBindCustomer { Name = customerName }
+ };
+
+ // Act
+ HttpResponseMessage response = httpClient.SendAsync(request).Result;
+
+ // Assert
+ ModelBindOrder actualItem = response.Content.ReadAsAsync<ModelBindOrder>().Result;
+ Assert.Equal<ModelBindOrder>(expectedItem, actualItem, new ModelBindOrderEqualityComparer());
+ }
+ }
+} \ No newline at end of file
diff --git a/test/System.Web.Http.Integration.Test/ModelBinding/RouteBindingTests.cs b/test/System.Web.Http.Integration.Test/ModelBinding/RouteBindingTests.cs
new file mode 100644
index 00000000..0cd7ea50
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/ModelBinding/RouteBindingTests.cs
@@ -0,0 +1,29 @@
+using System.Net.Http;
+using Xunit;
+
+namespace System.Web.Http.ModelBinding
+{
+ /// <summary>
+ /// End to end functional tests for model binding via routes
+ /// </summary>
+ public class RouteBindingTests : ModelBindingTests
+ {
+ [Fact]
+ public void Route_Binds_Simple_Types_Get()
+ {
+ // Arrange
+ HttpRequestMessage request = new HttpRequestMessage()
+ {
+ RequestUri = new Uri(baseAddress + "ModelBinding/GetStringFromRoute"),
+ Method = HttpMethod.Get
+ };
+
+ // Act
+ HttpResponseMessage response = httpClient.SendAsync(request).Result;
+
+ // Assert
+ string responseString = response.Content.ReadAsStringAsync().Result;
+ Assert.Equal<string>("\"ModelBinding:GetStringFromRoute\"", responseString);
+ }
+ }
+} \ No newline at end of file
diff --git a/test/System.Web.Http.Integration.Test/PartialTrust/BasicScenarioTest.cs b/test/System.Web.Http.Integration.Test/PartialTrust/BasicScenarioTest.cs
new file mode 100644
index 00000000..99d6d1e0
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/PartialTrust/BasicScenarioTest.cs
@@ -0,0 +1,62 @@
+using System.Net.Http;
+using System.Net.Http.Headers;
+using Xunit;
+using Xunit.Extensions;
+
+namespace System.Web.Http.PartialTrust
+{
+ public class BasicScenarioTest : MarshalByRefObject
+ {
+ [Fact]
+ public void BasicSelfHostedEchoControllerWorks()
+ {
+ ScenarioHelper.RunTest(
+ "Echo",
+ "/{s}",
+ new HttpRequestMessage(HttpMethod.Get, "http://localhost/Echo/foo"),
+ (response) =>
+ {
+ Assert.DoesNotThrow(() => response.EnsureSuccessStatusCode());
+ Assert.Equal("foo", response.Content.ReadAsStringAsync().Result);
+ }
+ );
+ }
+
+ [Theory]
+ [InlineData("application/json")]
+ [InlineData("text/xml")]
+ public void SimpleConNegWorks(string mediaType)
+ {
+ HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Echo/ContentNegotiatedEcho/foo");
+ request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(mediaType));
+
+ ScenarioHelper.RunTest(
+ "Echo",
+ "/{action}/{s}",
+ request,
+ (response) =>
+ {
+ Assert.DoesNotThrow(() => response.EnsureSuccessStatusCode());
+ Assert.Equal(mediaType, response.Content.Headers.ContentType.MediaType);
+ }
+ );
+ }
+ }
+
+ [RunWith(typeof(PartialTrustRunner))]
+ public class PartialTrustBasicScenarioTest : BasicScenarioTest { }
+
+ public class EchoController : ApiController
+ {
+ public HttpResponseMessage Get(string s)
+ {
+ return new HttpResponseMessage() { Content = new StringContent(s) };
+ }
+
+ public string ContentNegotiatedEcho(string s)
+ {
+ return s;
+ }
+ }
+
+}
diff --git a/test/System.Web.Http.Integration.Test/PartialTrust/PartialTrustRunner.cs b/test/System.Web.Http.Integration.Test/PartialTrust/PartialTrustRunner.cs
new file mode 100644
index 00000000..dc2b8cec
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/PartialTrust/PartialTrustRunner.cs
@@ -0,0 +1,132 @@
+using System.Collections.Generic;
+using System.Data.SqlClient;
+using System.Net;
+using System.Net.Mail;
+using System.Reflection;
+using System.Security;
+using System.Security.Permissions;
+using Xunit;
+using Xunit.Sdk;
+
+namespace System.Web.Http.PartialTrust
+{
+ public class PartialTrustRunner : ITestClassCommand
+ {
+ private AppDomain sandbox;
+
+ // Delegate most of the work to the existing TestClassCommand class so that we
+ // can preserve any existing behavior (like supporting IUseFixture<T>).
+ private readonly TestClassCommand originalTestClassCommand = new TestClassCommand();
+
+ public int ChooseNextTest(ICollection<IMethodInfo> testsLeftToRun)
+ {
+ return this.originalTestClassCommand.ChooseNextTest(testsLeftToRun);
+ }
+
+ public Exception ClassFinish()
+ {
+ Exception result = this.originalTestClassCommand.ClassFinish();
+ if (this.sandbox != null)
+ {
+ AppDomain.Unload(this.sandbox);
+ this.sandbox = null;
+ }
+
+ return result;
+ }
+
+ public Exception ClassStart()
+ {
+ this.GuardTypeUnderTest();
+ Assembly xunitAssembly = typeof(FactAttribute).Assembly;
+ this.sandbox = CreatePartialTrustAppDomain();
+
+ return this.originalTestClassCommand.ClassStart();
+ }
+
+ private static AppDomain CreatePartialTrustAppDomain()
+ {
+ PermissionSet permissions = new PermissionSet(PermissionState.None);
+ permissions.AddPermission(new AspNetHostingPermission(AspNetHostingPermissionLevel.Medium));
+ permissions.AddPermission(new DnsPermission(PermissionState.Unrestricted));
+ permissions.AddPermission(new EnvironmentPermission(EnvironmentPermissionAccess.Read, "TEMP;TMP;USERNAME;OS;COMPUTERNAME"));
+ permissions.AddPermission(new FileIOPermission(FileIOPermissionAccess.AllAccess, AppDomain.CurrentDomain.BaseDirectory));
+ permissions.AddPermission(new IsolatedStorageFilePermission(PermissionState.None) { UsageAllowed = IsolatedStorageContainment.AssemblyIsolationByUser, UserQuota = Int64.MaxValue });
+ permissions.AddPermission(new SecurityPermission(SecurityPermissionFlag.Execution));
+ permissions.AddPermission(new SecurityPermission(SecurityPermissionFlag.ControlThread));
+ permissions.AddPermission(new SecurityPermission(SecurityPermissionFlag.ControlPrincipal));
+ permissions.AddPermission(new SecurityPermission(SecurityPermissionFlag.RemotingConfiguration));
+ permissions.AddPermission(new SmtpPermission(SmtpAccess.Connect));
+ permissions.AddPermission(new SqlClientPermission(PermissionState.Unrestricted));
+ permissions.AddPermission(new TypeDescriptorPermission(PermissionState.Unrestricted));
+ permissions.AddPermission(new WebPermission(PermissionState.Unrestricted));
+ permissions.AddPermission(new ReflectionPermission(ReflectionPermissionFlag.RestrictedMemberAccess));
+
+ AppDomainSetup setup = new AppDomainSetup() { ApplicationBase = AppDomain.CurrentDomain.BaseDirectory };
+
+ setup.PartialTrustVisibleAssemblies = new string[]
+ {
+ "System.Web, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293",
+ "System.Web.Extensions, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9",
+ "System.Web.Abstractions, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9",
+ "System.Web.Routing, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9",
+ "System.ComponentModel.DataAnnotations, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9",
+ "System.Web.DynamicData, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9",
+ "System.Web.DataVisualization, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9",
+ "System.Web.ApplicationServices, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9"
+ };
+
+
+ return AppDomain.CreateDomain("Partial Trust Sandbox", null, setup, permissions);
+ }
+
+ public IEnumerable<ITestCommand> EnumerateTestCommands(IMethodInfo testMethod)
+ {
+ return this.originalTestClassCommand.EnumerateTestCommands(testMethod);
+ }
+
+ public IEnumerable<IMethodInfo> EnumerateTestMethods()
+ {
+ return this.originalTestClassCommand.EnumerateTestMethods();
+ }
+
+ public bool IsTestMethod(IMethodInfo testMethod)
+ {
+ return this.originalTestClassCommand.IsTestMethod(testMethod);
+ }
+
+ public object ObjectUnderTest
+ {
+ get
+ {
+ return sandbox.CreateInstanceAndUnwrap(this.TypeUnderTest.Type.Assembly.FullName, this.TypeUnderTest.Type.FullName);
+ }
+ }
+
+ public ITypeInfo TypeUnderTest
+ {
+ get
+ {
+ return this.originalTestClassCommand.TypeUnderTest;
+ }
+ set
+ {
+ if (!typeof(MarshalByRefObject).IsAssignableFrom(value.Type))
+ {
+ throw new InvalidOperationException("Test types to be run in PT must derive from MarshalByRefObject");
+ }
+
+ this.originalTestClassCommand.TypeUnderTest = value;
+ }
+ }
+
+ private void GuardTypeUnderTest()
+ {
+ if (TypeUnderTest == null)
+ {
+ throw new InvalidOperationException("Forgot to set TypeUnderTest before calling ObjectUnderTest");
+ }
+ }
+ }
+
+} \ No newline at end of file
diff --git a/test/System.Web.Http.Integration.Test/Properties/AssemblyInfo.cs b/test/System.Web.Http.Integration.Test/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..83cc064e
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/Properties/AssemblyInfo.cs
@@ -0,0 +1,3 @@
+using System;
+
+[assembly: CLSCompliant(false)] \ No newline at end of file
diff --git a/test/System.Web.Http.Integration.Test/System.Web.Http.Integration.Test.csproj b/test/System.Web.Http.Integration.Test/System.Web.Http.Integration.Test.csproj
new file mode 100644
index 00000000..352b208a
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/System.Web.Http.Integration.Test.csproj
@@ -0,0 +1,155 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProductVersion>9.0.30729</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{3267DFC6-B34D-4011-BC0F-D3B56AF6F608}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>System.Web.Http</RootNamespace>
+ <AssemblyName>System.Web.Http.Integration.Test</AssemblyName>
+ <FileAlignment>512</FileAlignment>
+ <ProjectTypeGuids>{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Debug\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Release\Test\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'CodeCoverage' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\CodeCoverage\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="Moq">
+ <HintPath>..\..\packages\Moq.4.0.10827\lib\NET40\Moq.dll</HintPath>
+ </Reference>
+ <Reference Include="System" />
+ <Reference Include="System.ComponentModel.DataAnnotations" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.IdentityModel" />
+ <Reference Include="System.Net.Http">
+ <HintPath>..\..\packages\Microsoft.Net.Http.2.0.20302.1\lib\net40\System.Net.Http.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Net.Http.WebRequest">
+ <HintPath>..\..\packages\Microsoft.Net.Http.2.0.20302.1\lib\net40\System.Net.Http.WebRequest.dll</HintPath>
+ </Reference>
+ <Reference Include="System.ServiceModel" />
+ <Reference Include="Microsoft.CSharp" />
+ <Reference Include="System.Data" />
+ <Reference Include="xunit">
+ <HintPath>..\..\packages\xunit.1.9.0.1566\lib\xunit.dll</HintPath>
+ </Reference>
+ <Reference Include="xunit.extensions">
+ <HintPath>..\..\packages\xunit.extensions.1.9.0.1566\lib\xunit.extensions.dll</HintPath>
+ </Reference>
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="ApiExplorer\Controllers\DocumentationController.cs" />
+ <Compile Include="ApiExplorer\Controllers\ParameterSourceController.cs" />
+ <Compile Include="ApiExplorer\DocumentationProviders\AttributeDocumentationProvider.cs" />
+ <Compile Include="ApiExplorer\FormattersTest.cs" />
+ <Compile Include="ApiExplorer\Formatters\ItemFormatter.cs" />
+ <Compile Include="ApiExplorer\ParameterSourceTest.cs" />
+ <Compile Include="ApiExplorer\DocumentationTest.cs" />
+ <Compile Include="ApiExplorer\RouteConstraintsTest.cs" />
+ <Compile Include="ApiExplorer\RoutesTest.cs" />
+ <Compile Include="Util\ApiExplorerHelper.cs" />
+ <Compile Include="ApiExplorer\ApiExplorerSettingsTest.cs" />
+ <Compile Include="ApiExplorer\Controllers\HiddenController.cs" />
+ <Compile Include="ApiExplorer\Controllers\ItemController.cs" />
+ <Compile Include="ApiExplorer\Controllers\HiddenActionController.cs" />
+ <Compile Include="ApiExplorer\Controllers\OverloadsController.cs" />
+ <Compile Include="Authentication\BasicOverHttpTest.cs" />
+ <Compile Include="Authentication\CustomMessageHandler.cs" />
+ <Compile Include="Authentication\CustomUsernamePasswordValidator.cs" />
+ <Compile Include="Authentication\RequireAdminAttribute.cs" />
+ <Compile Include="Authentication\SampleController.cs" />
+ <Compile Include="ContentNegotiation\ContentNegotiationTestBase.cs" />
+ <Compile Include="ContentNegotiation\CustomFormatterTests.cs" />
+ <Compile Include="ContentNegotiation\DefaultContentNegotiatorTests.cs" />
+ <Compile Include="ContentNegotiation\HttpResponseReturnTests.cs" />
+ <Compile Include="ContentNegotiation\ConnegController.cs" />
+ <Compile Include="ContentNegotiation\ConnegItem.cs" />
+ <Compile Include="ContentNegotiation\AcceptHeaderTests.cs" />
+ <Compile Include="Controllers\ActionAttributesTest.cs" />
+ <Compile Include="Controllers\ApiControllerActionSelectorTest.cs" />
+ <Compile Include="Controllers\Apis\ActionAttributeTestController.cs" />
+ <Compile Include="Controllers\Apis\TestController.cs" />
+ <Compile Include="Controllers\Apis\User.cs" />
+ <Compile Include="Controllers\Apis\UserAddress.cs" />
+ <Compile Include="Controllers\CustomControllerFactoryTest.cs" />
+ <Compile Include="Controllers\Helpers\ApiControllerHelper.cs" />
+ <Compile Include="ExceptionHandling\DuplicateControllers.cs" />
+ <Compile Include="ExceptionHandling\ExceptionController.cs" />
+ <Compile Include="ExceptionHandling\ExceptionHandlingTest.cs" />
+ <Compile Include="ExceptionHandling\HttpResponseExceptionTest.cs" />
+ <Compile Include="ExceptionHandling\IncludeErrorDetailTest.cs" />
+ <Compile Include="Filters\IQueryableFilterPipelineTest.cs" />
+ <Compile Include="ModelBinding\BodyBindingTests.cs" />
+ <Compile Include="ModelBinding\CustomBindingTests.cs" />
+ <Compile Include="ModelBinding\DefaultActionValueBinderTest.cs" />
+ <Compile Include="ModelBinding\ModelBindingController.cs" />
+ <Compile Include="ModelBinding\ModelBindingTests.cs" />
+ <Compile Include="ModelBinding\HttpContentBindingTests.cs" />
+ <Compile Include="ModelBinding\QueryStringBindingTests.cs" />
+ <Compile Include="ModelBinding\RouteBindingTests.cs" />
+ <Compile Include="PartialTrust\BasicScenarioTest.cs" />
+ <Compile Include="PartialTrust\PartialTrustRunner.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="Util\ContextUtil.cs" />
+ <Compile Include="Util\ScenarioHelper.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\System.Json\System.Json.csproj">
+ <Project>{F0441BE9-BDC0-4629-BE5A-8765FFAA2481}</Project>
+ <Name>System.Json</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\src\System.Net.Http.Formatting\System.Net.Http.Formatting.csproj">
+ <Project>{668E9021-CE84-49D9-98FB-DF125A9FCDB0}</Project>
+ <Name>System.Net.Http.Formatting</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\src\System.Web.Http.Common\System.Web.Http.Common.csproj">
+ <Project>{03A5E5F2-2E23-48F2-ABCC-6C41BAC9AC02}</Project>
+ <Name>System.Web.Http.Common</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\src\System.Web.Http.SelfHost\System.Web.Http.SelfHost.csproj">
+ <Project>{66492E69-CE4C-4FB1-9B1F-88DEE09D06F1}</Project>
+ <Name>System.Web.Http.SelfHost</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\src\System.Web.Http\System.Web.Http.csproj">
+ <Project>{DDC1CE0C-486E-4E35-BB3B-EAB61F8F9440}</Project>
+ <Name>System.Web.Http</Name>
+ <Private>True</Private>
+ </ProjectReference>
+ <ProjectReference Include="..\Microsoft.TestCommon\Microsoft.TestCommon.csproj">
+ <Project>{FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0}</Project>
+ <Name>Microsoft.TestCommon</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="packages.config" />
+ </ItemGroup>
+ <ItemGroup />
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+</Project> \ No newline at end of file
diff --git a/test/System.Web.Http.Integration.Test/Util/ApiExplorerHelper.cs b/test/System.Web.Http.Integration.Test/Util/ApiExplorerHelper.cs
new file mode 100644
index 00000000..0a8c9152
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/Util/ApiExplorerHelper.cs
@@ -0,0 +1,54 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Net.Http;
+using System.Web.Http.Controllers;
+using System.Web.Http.Description;
+using System.Web.Http.Dispatcher;
+using Moq;
+using Xunit;
+
+namespace System.Web.Http.ApiExplorer
+{
+ public static class ApiExplorerHelper
+ {
+ public static void VerifyApiDescriptions(Collection<ApiDescription> apiDescriptions, List<object> expectedResults)
+ {
+ Assert.Equal(expectedResults.Count, apiDescriptions.Count);
+ ApiDescription[] sortedDescriptions = apiDescriptions.OrderBy(description => description.ID).ToArray();
+ object[] sortedExpectedResults = expectedResults.OrderBy(r =>
+ {
+ dynamic expectedResult = r;
+ HttpMethod expectedHttpMethod = expectedResult.HttpMethod;
+ string expectedRelativePath = expectedResult.RelativePath;
+ return expectedHttpMethod + expectedRelativePath;
+ }).ToArray();
+
+ for (int i = 0; i < sortedDescriptions.Length; i++)
+ {
+ dynamic expectedResult = sortedExpectedResults[i];
+ ApiDescription matchingDescription = sortedDescriptions[i];
+ Assert.Equal(expectedResult.HttpMethod, matchingDescription.HttpMethod);
+ Assert.Equal(expectedResult.RelativePath, matchingDescription.RelativePath);
+ Assert.Equal(expectedResult.HasRequestFormatters, matchingDescription.SupportedRequestBodyFormatters.Count > 0);
+ Assert.Equal(expectedResult.HasResponseFormatters, matchingDescription.SupportedResponseFormatters.Count > 0);
+ Assert.Equal(expectedResult.NumberOfParameters, matchingDescription.ParameterDescriptions.Count);
+ }
+ }
+
+ public static DefaultHttpControllerFactory GetStrictControllerFactory(HttpConfiguration config, params Type[] controllerTypes)
+ {
+ Dictionary<string, HttpControllerDescriptor> controllerMapping = new Dictionary<string, HttpControllerDescriptor>();
+ foreach (Type controllerType in controllerTypes)
+ {
+ string controllerName = controllerType.Name.Substring(0, controllerType.Name.Length - DefaultHttpControllerFactory.ControllerSuffix.Length);
+ var controllerDescriptor = new HttpControllerDescriptor(config, controllerName, controllerType);
+ controllerMapping.Add(controllerDescriptor.ControllerName, controllerDescriptor);
+ }
+
+ Mock<DefaultHttpControllerFactory> factory = new Mock<DefaultHttpControllerFactory>(config);
+ factory.Setup(f => f.GetControllerMapping()).Returns(controllerMapping);
+ return factory.Object;
+ }
+ }
+}
diff --git a/test/System.Web.Http.Integration.Test/Util/ContextUtil.cs b/test/System.Web.Http.Integration.Test/Util/ContextUtil.cs
new file mode 100644
index 00000000..67fcddb3
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/Util/ContextUtil.cs
@@ -0,0 +1,62 @@
+using System.Net.Http;
+using System.Web.Http.Controllers;
+using System.Web.Http.Routing;
+using Moq;
+
+// TODO: move this class to TestCommon after it has been refactored
+namespace System.Web.Http
+{
+ public static class ContextUtil
+ {
+ public static HttpControllerContext CreateControllerContext()
+ {
+ return CreateControllerContext(null, null, null);
+ }
+
+ public static HttpControllerContext CreateControllerContext(HttpConfiguration configuration)
+ {
+ return CreateControllerContext(configuration, null, null);
+ }
+
+ public static HttpControllerContext CreateControllerContext(HttpRequestMessage request)
+ {
+ return CreateControllerContext(null, null, request);
+ }
+
+ public static HttpControllerContext CreateControllerContext(HttpConfiguration configuration, IHttpRouteData routeData)
+ {
+ return CreateControllerContext(configuration, routeData, null);
+ }
+
+ public static HttpControllerContext CreateControllerContext(IHttpRouteData routeData, HttpRequestMessage request)
+ {
+ return CreateControllerContext(null, routeData, request);
+ }
+
+ public static HttpControllerContext CreateControllerContext(HttpConfiguration configuration, HttpRequestMessage request)
+ {
+ return CreateControllerContext(configuration, null, request);
+ }
+
+ public static HttpControllerContext CreateControllerContext(HttpConfiguration configuration, IHttpRouteData routeData, HttpRequestMessage request)
+ {
+ HttpConfiguration config = configuration ?? new HttpConfiguration();
+ IHttpRouteData route = routeData ?? new HttpRouteData(new HttpRoute());
+ HttpRequestMessage req = request ?? new HttpRequestMessage();
+ return new HttpControllerContext(config, route, req);
+ }
+
+ public static HttpActionContext CreateActionContext(HttpControllerContext controllerContext = null, HttpActionDescriptor actionDescriptor = null)
+ {
+ HttpControllerContext context = controllerContext ?? ContextUtil.CreateControllerContext();
+ HttpActionDescriptor descriptor = actionDescriptor ?? new Mock<HttpActionDescriptor>() { CallBase = true }.Object;
+
+ if (descriptor.Configuration == null)
+ {
+ descriptor.Configuration = controllerContext.Configuration;
+ }
+
+ return new HttpActionContext(context, descriptor);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Integration.Test/Util/ScenarioHelper.cs b/test/System.Web.Http.Integration.Test/Util/ScenarioHelper.cs
new file mode 100644
index 00000000..908e5d2c
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/Util/ScenarioHelper.cs
@@ -0,0 +1,41 @@
+using System.Net.Http;
+using System.Threading;
+using System.Web.Http.Common;
+
+namespace System.Web.Http
+{
+ public static class ScenarioHelper
+ {
+ public static string BaseAddress = "http://localhost";
+ public static void RunTest(string controllerName, string routeSuffix, HttpRequestMessage request,
+ Action<HttpResponseMessage> assert, Action<HttpConfiguration> configurer = null)
+ {
+ // Arrange
+ HttpConfiguration config = new HttpConfiguration();
+
+ config.Routes.MapHttpRoute("Default", "{controller}" + routeSuffix, new { controller = controllerName });
+ if (configurer != null)
+ {
+ configurer(config);
+ }
+ HttpServer server = new HttpServer(config);
+ HttpResponseMessage response = null;
+ try
+ {
+ // Act
+ response = server.SubmitRequestAsync(request, CancellationToken.None).Result;
+
+ // Assert
+ assert(response);
+ }
+ finally
+ {
+ request.Dispose();
+ if (response != null)
+ {
+ response.Dispose();
+ }
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Http.Integration.Test/packages.config b/test/System.Web.Http.Integration.Test/packages.config
new file mode 100644
index 00000000..b9070f9e
--- /dev/null
+++ b/test/System.Web.Http.Integration.Test/packages.config
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="Microsoft.Net.Http" version="2.0.20302.1" />
+ <package id="Moq" version="4.0.10827" />
+ <package id="xunit" version="1.9.0.1566" />
+ <package id="xunit.extensions" version="1.9.0.1566" />
+</packages> \ No newline at end of file
diff --git a/test/System.Web.Http.Test/AuthorizeAttributeTest.cs b/test/System.Web.Http.Test/AuthorizeAttributeTest.cs
new file mode 100644
index 00000000..bb1b860f
--- /dev/null
+++ b/test/System.Web.Http.Test/AuthorizeAttributeTest.cs
@@ -0,0 +1,280 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Security.Principal;
+using System.Web.Http.Controllers;
+using System.Web.Http.Hosting;
+using Microsoft.TestCommon;
+using Moq;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http
+{
+ public class AuthorizeAttributeTest
+ {
+ private readonly Mock<HttpActionDescriptor> _actionDescriptorMock = new Mock<HttpActionDescriptor>() { CallBase = true };
+ private readonly IEnumerable<AllowAnonymousAttribute> _allowAnonymousAttributeCollection = new[] { new AllowAnonymousAttribute() };
+ private readonly MockableAuthorizeAttribute _attribute;
+ private readonly Mock<MockableAuthorizeAttribute> _attributeMock = new Mock<MockableAuthorizeAttribute>() { CallBase = true };
+ private readonly Mock<HttpControllerDescriptor> _controllerDescriptorMock = new Mock<HttpControllerDescriptor>() { CallBase = true };
+ private readonly HttpControllerContext _controllerContext;
+ private readonly HttpActionContext _actionContext;
+ private readonly Mock<IPrincipal> _principalMock = new Mock<IPrincipal>();
+ private readonly HttpRequestMessage _request = new HttpRequestMessage();
+
+ public AuthorizeAttributeTest()
+ {
+ _attribute = _attributeMock.Object;
+ _controllerContext = new Mock<HttpControllerContext>() { CallBase = true }.Object;
+ _controllerDescriptorMock.Setup(cd => cd.GetCustomAttributes<AllowAnonymousAttribute>()).Returns(Enumerable.Empty<AllowAnonymousAttribute>().ToList().AsReadOnly());
+ _actionDescriptorMock.Setup(ad => ad.GetCustomAttributes<AllowAnonymousAttribute>()).Returns(Enumerable.Empty<AllowAnonymousAttribute>().ToList().AsReadOnly());
+ _controllerContext.ControllerDescriptor = _controllerDescriptorMock.Object;
+ _controllerContext.Request = _request;
+ _actionContext = ContextUtil.CreateActionContext(_controllerContext, _actionDescriptorMock.Object);
+ _request.Properties[HttpPropertyKeys.UserPrincipalKey] = _principalMock.Object;
+ }
+
+ [Fact]
+ public void Roles_Property()
+ {
+ AuthorizeAttribute attribute = new AuthorizeAttribute();
+
+ Assert.Reflection.StringProperty(attribute, a => a.Roles, expectedDefaultValue: String.Empty);
+ }
+
+ [Fact]
+ public void Users_Property()
+ {
+ AuthorizeAttribute attribute = new AuthorizeAttribute();
+
+ Assert.Reflection.StringProperty(attribute, a => a.Users, expectedDefaultValue: String.Empty);
+ }
+
+ [Fact]
+ public void AllowMultiple_ReturnsTrue()
+ {
+ Assert.True(_attribute.AllowMultiple);
+ }
+
+ [Fact]
+ public void TypeId_ReturnsUniqueInstances()
+ {
+ var attribute1 = new AuthorizeAttribute();
+ var attribute2 = new AuthorizeAttribute();
+
+ Assert.NotSame(attribute1.TypeId, attribute2.TypeId);
+ }
+
+ [Fact]
+ public void OnAuthorization_IfContextParameterIsNull_ThrowsException()
+ {
+ Assert.ThrowsArgumentNull(() =>
+ {
+ _attribute.OnAuthorization(actionContext: null);
+ }, "actionContext");
+ }
+
+ [Fact]
+ public void OnAuthorization_IfUserIsAuthenticated_DoesNotShortCircuitRequest()
+ {
+ _principalMock.Setup(p => p.Identity.IsAuthenticated).Returns(true);
+
+ _attribute.OnAuthorization(_actionContext);
+
+ Assert.Null(_actionContext.Response);
+ }
+
+ [Fact]
+ public void OnAuthorization_IfContextDoesNotContainPrincipal_DoesShortCircuitRequest()
+ {
+ _request.Properties.Remove(HttpPropertyKeys.UserPrincipalKey);
+
+ _attribute.OnAuthorization(_actionContext);
+
+ AssertUnauthorizedRequestSet(_actionContext);
+ }
+
+ [Fact]
+ public void OnAuthorization_IfUserIsNotAuthenticated_DoesShortCircuitRequest()
+ {
+ _principalMock.Setup(p => p.Identity.IsAuthenticated).Returns(false).Verifiable();
+
+ _attribute.OnAuthorization(_actionContext);
+
+ AssertUnauthorizedRequestSet(_actionContext);
+ _principalMock.Verify();
+ }
+
+ [Fact]
+ public void OnAuthorization_IfUserIsNotInUsersCollection_DoesShortCircuitRequest()
+ {
+ _attribute.Users = "John";
+ _principalMock.Setup(p => p.Identity.IsAuthenticated).Returns(true).Verifiable();
+ _principalMock.Setup(p => p.Identity.Name).Returns("Mary").Verifiable();
+
+ _attribute.OnAuthorization(_actionContext);
+
+ AssertUnauthorizedRequestSet(_actionContext);
+ _principalMock.Verify();
+ }
+
+ [Fact]
+ public void OnAuthorization_IfUserIsInUsersCollection_DoesNotShortCircuitRequest()
+ {
+ _attribute.Users = " John , Mary ";
+ _principalMock.Setup(p => p.Identity.IsAuthenticated).Returns(true).Verifiable();
+ _principalMock.Setup(p => p.Identity.Name).Returns("Mary").Verifiable();
+
+ _attribute.OnAuthorization(_actionContext);
+
+ Assert.Null(_actionContext.Response);
+ _principalMock.Verify();
+ }
+
+ [Fact]
+ public void OnAuthorization_IfUserIsNotInRolesCollection_DoesShortCircuitRequest()
+ {
+ _attribute.Users = " John , Mary ";
+ _attribute.Roles = "Administrators,PowerUsers";
+ _principalMock.Setup(p => p.Identity.IsAuthenticated).Returns(true).Verifiable();
+ _principalMock.Setup(p => p.Identity.Name).Returns("Mary").Verifiable();
+ _principalMock.Setup(p => p.IsInRole("Administrators")).Returns(false).Verifiable();
+ _principalMock.Setup(p => p.IsInRole("PowerUsers")).Returns(false).Verifiable();
+
+ _attribute.OnAuthorization(_actionContext);
+
+ AssertUnauthorizedRequestSet(_actionContext);
+ _principalMock.Verify();
+ }
+
+ [Fact]
+ public void OnAuthorization_IfUserIsInRolesCollection_DoesNotShortCircuitRequest()
+ {
+ _attribute.Users = " John , Mary ";
+ _attribute.Roles = "Administrators,PowerUsers";
+ _principalMock.Setup(p => p.Identity.IsAuthenticated).Returns(true).Verifiable();
+ _principalMock.Setup(p => p.Identity.Name).Returns("Mary").Verifiable();
+ _principalMock.Setup(p => p.IsInRole("Administrators")).Returns(false).Verifiable();
+ _principalMock.Setup(p => p.IsInRole("PowerUsers")).Returns(true).Verifiable();
+
+ _attribute.OnAuthorization(_actionContext);
+
+ Assert.Null(_actionContext.Response);
+ _principalMock.Verify();
+ }
+
+ [Fact]
+ public void OnAuthorization_IfActionDescriptorIsMarkedWithAllowAnonymousAttribute_DoesNotShortCircuitResponse()
+ {
+ _actionDescriptorMock.Setup(ad => ad.GetCustomAttributes<AllowAnonymousAttribute>()).Returns(_allowAnonymousAttributeCollection);
+ Mock<MockableAuthorizeAttribute> authorizeAttributeMock = new Mock<MockableAuthorizeAttribute>() { CallBase = true };
+ AuthorizeAttribute attribute = authorizeAttributeMock.Object;
+
+ attribute.OnAuthorization(_actionContext);
+
+ Assert.Null(_actionContext.Response);
+ }
+
+ [Fact]
+ public void OnAuthorization_IfControllerDescriptorIsMarkedWithAllowAnonymousAttribute_DoesNotShortCircuitResponse()
+ {
+ _controllerDescriptorMock.Setup(ad => ad.GetCustomAttributes<AllowAnonymousAttribute>()).Returns(_allowAnonymousAttributeCollection);
+ Mock<MockableAuthorizeAttribute> authorizeAttributeMock = new Mock<MockableAuthorizeAttribute>() { CallBase = true };
+ AuthorizeAttribute attribute = authorizeAttributeMock.Object;
+
+ attribute.OnAuthorization(_actionContext);
+
+ Assert.Null(_actionContext.Response);
+ }
+
+ [Fact]
+ public void OnAuthorization_IfRequestNotAuthorized_CallsHandleUnauthorizedRequest()
+ {
+ Mock<MockableAuthorizeAttribute> authorizeAttributeMock = new Mock<MockableAuthorizeAttribute>() { CallBase = true };
+ _principalMock.Setup(p => p.Identity.IsAuthenticated).Returns(false);
+ authorizeAttributeMock.Setup(a => a.HandleUnauthorizedRequestPublic(_actionContext)).Verifiable();
+ AuthorizeAttribute attribute = authorizeAttributeMock.Object;
+
+ attribute.OnAuthorization(_actionContext);
+
+ authorizeAttributeMock.Verify();
+ }
+
+ [Fact]
+ public void HandleUnauthorizedRequest_IfContextParameterIsNull_ThrowsArgumentNullException()
+ {
+ Assert.ThrowsArgumentNull(() =>
+ {
+ _attribute.HandleUnauthorizedRequestPublic(context: null);
+ }, "actionContext");
+ }
+
+ [Fact]
+ public void HandleUnauthorizedRequest_SetsResponseWithUnauthorizedStatusCode()
+ {
+ _attribute.HandleUnauthorizedRequestPublic(_actionContext);
+
+ Assert.NotNull(_actionContext.Response);
+ Assert.Equal(HttpStatusCode.Unauthorized, _actionContext.Response.StatusCode);
+ Assert.Same(_request, _actionContext.Response.RequestMessage);
+ }
+
+ [Theory]
+ [PropertyData("SplitStringTestData")]
+ public void SplitString_SplitsOnCommaAndTrimsWhitespaceAndIgnoresEmptyStrings(string input, params string[] expectedResult)
+ {
+ string[] result = AuthorizeAttribute.SplitString(input);
+
+ Assert.Equal(expectedResult, result);
+ }
+
+ public static IEnumerable<object[]> SplitStringTestData
+ {
+ get
+ {
+ return new ParamsTheoryDataSet<string, string>() {
+ { null },
+ { String.Empty },
+ { " " },
+ { " A ", "A" },
+ { " A, B ", "A", "B" },
+ { " , A, ,B, ", "A", "B" },
+ { " A B ", "A B" },
+ };
+ }
+ }
+
+ [CLSCompliant(false)]
+ public class ParamsTheoryDataSet<TParam1, TParam2> : TheoryDataSet
+ {
+ public void Add(TParam1 p1, params TParam2[] p2)
+ {
+ AddItem(p1, p2);
+ }
+ }
+
+ private static void AssertUnauthorizedRequestSet(HttpActionContext actionContext)
+ {
+ Assert.NotNull(actionContext.Response);
+ Assert.Equal(HttpStatusCode.Unauthorized, actionContext.Response.StatusCode);
+ Assert.Same(actionContext.ControllerContext.Request, actionContext.Response.RequestMessage);
+ }
+
+ public class MockableAuthorizeAttribute : AuthorizeAttribute
+ {
+ protected override void HandleUnauthorizedRequest(HttpActionContext context)
+ {
+ HandleUnauthorizedRequestPublic(context);
+ }
+
+ public virtual void HandleUnauthorizedRequestPublic(HttpActionContext context)
+ {
+ base.HandleUnauthorizedRequest(context);
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Controllers/ApiControllerActionInvokerTest.cs b/test/System.Web.Http.Test/Controllers/ApiControllerActionInvokerTest.cs
new file mode 100644
index 00000000..b1492bd6
--- /dev/null
+++ b/test/System.Web.Http.Test/Controllers/ApiControllerActionInvokerTest.cs
@@ -0,0 +1,49 @@
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Controllers;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http
+{
+ public class ApiControllerActionInvokerTest
+ {
+ [Fact]
+ public void InvokeActionAsync_Calls_ActionMethod()
+ {
+ ApiControllerActionInvoker actionInvoker = new ApiControllerActionInvoker();
+ UsersController controller = new UsersController();
+ Func<HttpResponseMessage> actionMethod = controller.Get;
+ HttpActionContext context = ContextUtil.CreateActionContext(
+ ContextUtil.CreateControllerContext(instance: controller),
+ new ReflectedHttpActionDescriptor { MethodInfo = actionMethod.Method });
+
+ HttpResponseMessage response = actionInvoker.InvokeActionAsync(context, CancellationToken.None).Result;
+
+ Assert.Equal("Default User", response.Content.ReadAsStringAsync().Result);
+ }
+
+ [Fact]
+ public void InvokeActionAsync_Cancels_IfCancellationTokenRequested()
+ {
+ ApiControllerActionInvoker actionInvoker = new ApiControllerActionInvoker();
+ CancellationTokenSource cancellationSource = new CancellationTokenSource();
+ cancellationSource.Cancel();
+
+ var response = actionInvoker.InvokeActionAsync(ContextUtil.CreateActionContext(), cancellationSource.Token);
+
+ Assert.Equal<TaskStatus>(TaskStatus.Canceled, response.Status);
+ }
+
+ [Fact]
+ public void InvokeActionAsync_Throws_IfContextIsNull()
+ {
+ ApiControllerActionInvoker actionInvoker = new ApiControllerActionInvoker();
+
+ Assert.ThrowsArgumentNull(
+ () => actionInvoker.InvokeActionAsync(null, CancellationToken.None),
+ "actionContext");
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Controllers/ApiControllerActionSelectorTest.cs b/test/System.Web.Http.Test/Controllers/ApiControllerActionSelectorTest.cs
new file mode 100644
index 00000000..ab239132
--- /dev/null
+++ b/test/System.Web.Http.Test/Controllers/ApiControllerActionSelectorTest.cs
@@ -0,0 +1,47 @@
+using System.Net.Http;
+using System.Web.Http.Controllers;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http
+{
+ public class ApiControllerActionSelectorTest
+ {
+ [Fact]
+ public void SelectAction_With_DifferentExecutionContexts()
+ {
+ ApiControllerActionSelector actionSelector = new ApiControllerActionSelector();
+ HttpControllerContext GetContext = ContextUtil.CreateControllerContext();
+ HttpControllerDescriptor usersControllerDescriptor = new HttpControllerDescriptor(GetContext.Configuration, "Users", typeof(UsersController));
+ usersControllerDescriptor.HttpActionSelector = actionSelector;
+ GetContext.ControllerDescriptor = usersControllerDescriptor;
+ GetContext.Request = new HttpRequestMessage
+ {
+ Method = HttpMethod.Get
+ };
+ HttpControllerContext PostContext = ContextUtil.CreateControllerContext();
+ usersControllerDescriptor.HttpActionSelector = actionSelector;
+ PostContext.ControllerDescriptor = usersControllerDescriptor;
+ PostContext.Request = new HttpRequestMessage
+ {
+ Method = HttpMethod.Post
+ };
+
+ HttpActionDescriptor getActionDescriptor = actionSelector.SelectAction(GetContext);
+ HttpActionDescriptor postActionDescriptor = actionSelector.SelectAction(PostContext);
+
+ Assert.Equal("Get", getActionDescriptor.ActionName);
+ Assert.Equal("Post", postActionDescriptor.ActionName);
+ }
+
+ [Fact]
+ public void SelectAction_Throws_IfContextIsNull()
+ {
+ ApiControllerActionSelector actionSelector = new ApiControllerActionSelector();
+
+ Assert.ThrowsArgumentNull(
+ () => actionSelector.SelectAction(null),
+ "controllerContext");
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Controllers/ApiControllerTest.cs b/test/System.Web.Http.Test/Controllers/ApiControllerTest.cs
new file mode 100644
index 00000000..e995c4c7
--- /dev/null
+++ b/test/System.Web.Http.Test/Controllers/ApiControllerTest.cs
@@ -0,0 +1,702 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Globalization;
+using System.Linq;
+using System.Net.Http;
+using System.Net.Http.Formatting;
+using System.Net.Http.Headers;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Controllers;
+using System.Web.Http.Filters;
+using System.Web.Http.ModelBinding;
+using System.Web.Http.Properties;
+using System.Web.Http.Routing;
+using System.Web.Http.Services;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http
+{
+ public class ApiControllerTest
+ {
+ private readonly HttpActionContext _actionContextInstance = ContextUtil.CreateActionContext();
+ private readonly HttpConfiguration _configurationInstance = new HttpConfiguration();
+ private readonly HttpActionDescriptor _actionDescriptorInstance = new Mock<HttpActionDescriptor>() { CallBase = true }.Object;
+
+ [Fact]
+ public void Setting_CustomActionInvoker()
+ {
+ // Arrange
+ ApiController api = new UsersController();
+ string responseText = "Hello World";
+ HttpControllerContext controllerContext = ContextUtil.CreateControllerContext();
+
+ HttpControllerDescriptor controllerDescriptor = new HttpControllerDescriptor(controllerContext.Configuration, "test", typeof(UsersController));
+ controllerContext.ControllerDescriptor = controllerDescriptor;
+
+ Mock<IHttpActionInvoker> mockInvoker = new Mock<IHttpActionInvoker>();
+ mockInvoker
+ .Setup(invoker => invoker.InvokeActionAsync(It.IsAny<HttpActionContext>(), It.IsAny<CancellationToken>()))
+ .Returns(() =>
+ {
+ TaskCompletionSource<HttpResponseMessage> tcs = new TaskCompletionSource<HttpResponseMessage>();
+ tcs.TrySetResult(new HttpResponseMessage() { Content = new StringContent(responseText) });
+ return tcs.Task;
+ });
+ controllerDescriptor.HttpActionInvoker = mockInvoker.Object;
+
+ // Act
+ HttpResponseMessage message = api.ExecuteAsync(
+ controllerContext,
+ CancellationToken.None).Result;
+
+ // Assert
+ Assert.Equal(responseText, message.Content.ReadAsStringAsync().Result);
+ }
+
+ [Fact]
+ public void Setting_CustomActionSelector()
+ {
+ // Arrange
+ ApiController api = new UsersController();
+ HttpControllerContext controllerContext = ContextUtil.CreateControllerContext();
+
+ HttpControllerDescriptor controllerDescriptor = new HttpControllerDescriptor(controllerContext.Configuration, "test", typeof(UsersController));
+ controllerContext.ControllerDescriptor = controllerDescriptor;
+
+ Mock<IHttpActionSelector> mockSelector = new Mock<IHttpActionSelector>();
+ mockSelector
+ .Setup(invoker => invoker.SelectAction(It.IsAny<HttpControllerContext>()))
+ .Returns(() =>
+ {
+ Func<HttpResponseMessage> testDelegate =
+ () => new HttpResponseMessage { Content = new StringContent("This is a test") };
+ return new ReflectedHttpActionDescriptor
+ {
+ Configuration = controllerContext.Configuration,
+ ControllerDescriptor = controllerDescriptor,
+ MethodInfo = testDelegate.Method
+ };
+ });
+ controllerDescriptor.HttpActionSelector = mockSelector.Object;
+
+ // Act
+ HttpResponseMessage message = api.ExecuteAsync(
+ controllerContext,
+ CancellationToken.None).Result;
+
+ // Assert
+ Assert.Equal("This is a test", message.Content.ReadAsStringAsync().Result);
+ }
+
+ [Fact]
+ public void Default_Get()
+ {
+ // Arrange
+ ApiController api = new UsersController();
+ HttpControllerContext controllerContext = ContextUtil.CreateControllerContext(instance: api, request: new HttpRequestMessage() { Method = HttpMethod.Get });
+ controllerContext.ControllerDescriptor = new HttpControllerDescriptor(controllerContext.Configuration, "test", typeof(UsersController));
+
+ // Act
+ HttpResponseMessage message = api.ExecuteAsync(
+ controllerContext,
+ CancellationToken.None).Result;
+
+ // Assert
+ Assert.Equal("Default User", message.Content.ReadAsStringAsync().Result);
+ }
+
+ [Fact]
+ public void Default_Post()
+ {
+ // Arrange
+ ApiController api = new UsersController();
+ HttpControllerContext controllerContext = ContextUtil.CreateControllerContext(instance: api, request: new HttpRequestMessage() { Method = HttpMethod.Post });
+ controllerContext.ControllerDescriptor = new HttpControllerDescriptor(controllerContext.Configuration, "test", typeof(UsersController));
+
+ // Act
+ HttpResponseMessage message = api.ExecuteAsync(
+ controllerContext,
+ CancellationToken.None).Result;
+
+ // Assert
+ Assert.Equal("User Posted", message.Content.ReadAsStringAsync().Result);
+ }
+
+ [Fact]
+ public void Default_Put()
+ {
+ // Arrange
+ ApiController api = new UsersController();
+ HttpControllerContext controllerContext = ContextUtil.CreateControllerContext(instance: api, request: new HttpRequestMessage() { Method = HttpMethod.Put });
+ controllerContext.ControllerDescriptor = new HttpControllerDescriptor(controllerContext.Configuration, "test", typeof(UsersController));
+
+ // Act
+ HttpResponseMessage message = api.ExecuteAsync(
+ controllerContext,
+ CancellationToken.None).Result;
+
+ // Assert
+ Assert.Equal("User Updated", message.Content.ReadAsStringAsync().Result);
+ }
+
+ [Fact]
+ public void Default_Delete()
+ {
+ // Arrange
+ ApiController api = new UsersController();
+ HttpControllerContext controllerContext = ContextUtil.CreateControllerContext(instance: api, request: new HttpRequestMessage() { Method = HttpMethod.Delete });
+ controllerContext.ControllerDescriptor = new HttpControllerDescriptor(controllerContext.Configuration, "test", typeof(UsersController));
+
+ // Act
+ HttpResponseMessage message = api.ExecuteAsync(
+ controllerContext,
+ CancellationToken.None).Result;
+
+ // Assert
+ Assert.Equal("User Deleted", message.Content.ReadAsStringAsync().Result);
+ }
+
+ [Fact]
+ public void Route_ActionName()
+ {
+ // Arrange
+ ApiController api = new UsersRpcController();
+ HttpRouteData route = new HttpRouteData(new HttpRoute());
+ route.Values.Add("action", "Admin");
+ HttpControllerContext controllerContext = ContextUtil.CreateControllerContext(instance: api, routeData: route, request: new HttpRequestMessage() { Method = HttpMethod.Get });
+ controllerContext.ControllerDescriptor = new HttpControllerDescriptor(controllerContext.Configuration, "test", typeof(UsersRpcController));
+
+ // Act
+ HttpResponseMessage message = api.ExecuteAsync(controllerContext, CancellationToken.None).Result;
+ User user = message.Content.ReadAsAsync<User>().Result;
+
+ // Assert
+ Assert.Equal("Yao", user.FirstName);
+ Assert.Equal("Huang", user.LastName);
+ }
+
+ [Fact]
+ public void Route_Get_Action_With_Route_Parameters()
+ {
+ // Arrange
+ ApiController api = new UsersRpcController();
+ HttpRouteData route = new HttpRouteData(new HttpRoute());
+ route.Values.Add("action", "EchoUser");
+ route.Values.Add("firstName", "RouteFirstName");
+ route.Values.Add("lastName", "RouteLastName");
+ HttpControllerContext controllerContext = ContextUtil.CreateControllerContext(instance: api, routeData: route, request: new HttpRequestMessage() { Method = HttpMethod.Get });
+ controllerContext.ControllerDescriptor = new HttpControllerDescriptor(controllerContext.Configuration, "test", typeof(UsersRpcController));
+
+ // Act
+ HttpResponseMessage message = api.ExecuteAsync(controllerContext, CancellationToken.None).Result;
+ User user = message.Content.ReadAsAsync<User>().Result;
+
+ // Assert
+ Assert.Equal("RouteFirstName", user.FirstName);
+ Assert.Equal("RouteLastName", user.LastName);
+ }
+
+ [Fact]
+ public void Route_Get_Action_With_Query_Parameters()
+ {
+ // Arrange
+ ApiController api = new UsersRpcController();
+ HttpRouteData route = new HttpRouteData(new HttpRoute());
+ route.Values.Add("action", "EchoUser");
+
+ Uri requestUri = new Uri("http://localhost/?firstName=QueryFirstName&lastName=QueryLastName");
+ HttpControllerContext controllerContext = ContextUtil.CreateControllerContext(instance: api, routeData: route, request: new HttpRequestMessage()
+ {
+ Method = HttpMethod.Get,
+ RequestUri = requestUri
+ });
+ controllerContext.ControllerDescriptor = new HttpControllerDescriptor(controllerContext.Configuration, "test", typeof(UsersRpcController));
+
+ // Act
+ HttpResponseMessage message = api.ExecuteAsync(controllerContext, CancellationToken.None).Result;
+ User user = message.Content.ReadAsAsync<User>().Result;
+
+ // Assert
+ Assert.Equal("QueryFirstName", user.FirstName);
+ Assert.Equal("QueryLastName", user.LastName);
+ }
+
+ [Fact]
+ public void Route_Post_Action_With_Content_Parameter()
+ {
+ // Arrange
+ ApiController api = new UsersRpcController();
+ HttpRouteData route = new HttpRouteData(new HttpRoute());
+ route.Values.Add("action", "EchoUserObject");
+ User postedUser = new User()
+ {
+ FirstName = "SampleFirstName",
+ LastName = "SampleLastName"
+ };
+
+ HttpRequestMessage request = new HttpRequestMessage() { Method = HttpMethod.Post };
+
+ // Create a serialized request because this test directly calls the controller
+ // which would have normally been working with a serialized request content.
+ string serializedUserAsString = null;
+ using (HttpRequestMessage tempRequest = new HttpRequestMessage() { Content = new ObjectContent<User>(postedUser, new XmlMediaTypeFormatter()) })
+ {
+ serializedUserAsString = tempRequest.Content.ReadAsStringAsync().Result;
+ }
+
+ StringContent stringContent = new StringContent(serializedUserAsString);
+ stringContent.Headers.ContentType = new MediaTypeHeaderValue("application/xml");
+ request.Content = stringContent;
+
+ HttpControllerContext controllerContext = ContextUtil.CreateControllerContext(instance: api, routeData: route, request: request);
+ controllerContext.ControllerDescriptor = new HttpControllerDescriptor(controllerContext.Configuration, "test", typeof(UsersRpcController));
+
+ // Act
+ HttpResponseMessage message = api.ExecuteAsync(
+ controllerContext,
+ CancellationToken.None).Result;
+ User user = message.Content.ReadAsAsync<User>().Result;
+
+ // Assert
+ Assert.Equal(postedUser.FirstName, user.FirstName);
+ Assert.Equal(postedUser.LastName, user.LastName);
+ }
+
+ [Fact]
+ public void Invalid_Action_In_Route()
+ {
+ // Arrange
+ ApiController api = new UsersController();
+ HttpRouteData route = new HttpRouteData(new HttpRoute());
+ string actionName = "invalidOp";
+ route.Values.Add("action", actionName);
+ HttpControllerContext controllerContext = ContextUtil.CreateControllerContext(instance: api, routeData: route, request: new HttpRequestMessage() { Method = HttpMethod.Get });
+ Type controllerType = typeof(UsersController);
+ controllerContext.ControllerDescriptor = new HttpControllerDescriptor(controllerContext.Configuration, controllerType.Name, controllerType);
+
+ // Act & Assert
+ Assert.Throws<HttpResponseException>(() =>
+ {
+ HttpResponseMessage message = api.ExecuteAsync(controllerContext, CancellationToken.None).Result;
+ },
+ String.Format(SRResources.ApiControllerActionSelector_ActionNameNotFound, controllerType.Name, actionName));
+ }
+
+ [Fact]
+ public void ExecuteAsync_InvokesAuthorizationFilters_ThenInvokesModelBinding_ThenInvokesActionFilters_ThenInvokesAction()
+ {
+ List<string> log = new List<string>();
+ Mock<ApiController> controllerMock = new Mock<ApiController>() { CallBase = true };
+ var controllerContextMock = new Mock<HttpControllerContext>();
+
+ Mock<IActionValueBinder> binderMock = new Mock<IActionValueBinder>();
+ Mock<HttpActionBinding> actionBindingMock = new Mock<HttpActionBinding>();
+ actionBindingMock.Setup(b => b.ExecuteBindingAsync(It.IsAny<HttpActionContext>(), It.IsAny<CancellationToken>())).Returns(() => Task.Factory.StartNew(() => { log.Add("model binding"); }));
+ binderMock.Setup(b => b.GetBinding(It.IsAny<HttpActionDescriptor>())).Returns(actionBindingMock.Object);
+ HttpConfiguration configuration = new HttpConfiguration();
+
+ HttpControllerContext controllerContext = controllerContextMock.Object;
+ controllerContext.Configuration = configuration;
+ controllerContext.ControllerDescriptor = new HttpControllerDescriptor(configuration, "test", typeof(object));
+ var actionFilterMock = CreateActionFilterMock((ac, ct, cont) =>
+ {
+ log.Add("action filters");
+ return cont();
+ });
+ var authFilterMock = CreateAuthorizationFilterMock((ac, ct, cont) =>
+ {
+ log.Add("auth filters");
+ return cont();
+ });
+ var selectorMock = new Mock<IHttpActionSelector>();
+ selectorMock.Setup(s => s.SelectAction(controllerContext).GetFilterPipeline())
+ .Returns(new Collection<FilterInfo>(new List<FilterInfo>() { new FilterInfo(actionFilterMock.Object, FilterScope.Action), new FilterInfo(authFilterMock.Object, FilterScope.Action) }));
+ ApiController controller = controllerMock.Object;
+ var invokerMock = new Mock<IHttpActionInvoker>();
+ invokerMock.Setup(i => i.InvokeActionAsync(It.IsAny<HttpActionContext>(), It.IsAny<CancellationToken>()))
+ .Returns(() => Task.Factory.StartNew(() =>
+ {
+ log.Add("action");
+ return new HttpResponseMessage();
+ }));
+ controllerContext.ControllerDescriptor.HttpActionInvoker = invokerMock.Object;
+ controllerContext.ControllerDescriptor.HttpActionSelector = selectorMock.Object;
+ controllerContext.ControllerDescriptor.ActionValueBinder = binderMock.Object;
+
+ var task = controller.ExecuteAsync(controllerContext, CancellationToken.None);
+
+ Assert.NotNull(task);
+ task.WaitUntilCompleted();
+ Assert.Equal(new string[] { "auth filters", "model binding", "action filters", "action" }, log.ToArray());
+ }
+
+ [Fact]
+ public void GetFilters_QueriesFilterProvidersFromServiceResolver()
+ {
+ // Arrange
+ Mock<IDependencyResolver> resolverMock = new Mock<IDependencyResolver>();
+ Mock<IFilterProvider> filterProviderMock = new Mock<IFilterProvider>();
+ resolverMock.Setup(r => r.GetServices(typeof(IFilterProvider))).Returns(new object[] { filterProviderMock.Object }).Verifiable();
+ _configurationInstance.ServiceResolver.SetResolver(resolverMock.Object);
+
+ HttpActionDescriptor actionDescriptorMock = new Mock<HttpActionDescriptor>() { CallBase = true }.Object;
+ actionDescriptorMock.Configuration = _configurationInstance;
+
+ // Act
+ actionDescriptorMock.GetFilterPipeline();
+
+ // Assert
+ resolverMock.Verify();
+ }
+
+ [Fact]
+ public void GetFilters_UsesFilterProvidersToGetFilters()
+ {
+ // Arrange
+ Mock<IDependencyResolver> resolverMock = new Mock<IDependencyResolver>();
+ Mock<IFilterProvider> filterProviderMock = new Mock<IFilterProvider>();
+ resolverMock.Setup(r => r.GetServices(typeof(IFilterProvider))).Returns(new[] { filterProviderMock.Object });
+ _configurationInstance.ServiceResolver.SetResolver(resolverMock.Object);
+
+ HttpActionDescriptor actionDescriptorMock = new Mock<HttpActionDescriptor>() { CallBase = true }.Object;
+ actionDescriptorMock.Configuration = _configurationInstance;
+
+ // Act
+ actionDescriptorMock.GetFilterPipeline().ToList();
+
+ // Assert
+ filterProviderMock.Verify(fp => fp.GetFilters(_configurationInstance, actionDescriptorMock));
+ }
+
+ [Fact]
+ public void RequestPropertyGetterSetterWorks()
+ {
+ Assert.Reflection.Property(new Mock<ApiController>().Object,
+ c => c.Request, expectedDefaultValue: null, allowNull: false,
+ roundTripTestValue: new HttpRequestMessage());
+ }
+
+ [Fact]
+ public void ConfigurationPropertyGetterSetterWorks()
+ {
+ Assert.Reflection.Property(new Mock<ApiController>().Object,
+ c => c.Configuration, expectedDefaultValue: null, allowNull: false,
+ roundTripTestValue: new HttpConfiguration());
+ }
+
+ [Fact]
+ public void ModelStatePropertyGetterWorks()
+ {
+ // Arrange
+ ApiController controller = new Mock<ApiController>().Object;
+
+ // Act
+ ModelStateDictionary expected = new ModelStateDictionary();
+ expected.Add("a", new ModelState() { Value = new ValueProviders.ValueProviderResult("result", "attempted", CultureInfo.InvariantCulture) });
+
+ controller.ModelState.Add("a", new ModelState() { Value = new ValueProviders.ValueProviderResult("result", "attempted", CultureInfo.InvariantCulture) });
+
+ // Assert
+ Assert.Equal(expected.Count, controller.ModelState.Count);
+ }
+
+ // TODO: Move these tests to ActionDescriptorTest
+ [Fact]
+ public void GetFilters_OrdersFilters()
+ {
+ // Arrange
+ HttpActionDescriptor actionDescriptorMock = new Mock<HttpActionDescriptor>() { CallBase = true }.Object;
+ actionDescriptorMock.Configuration = _configurationInstance;
+
+ var globalFilter = new FilterInfo(new TestMultiFilter(), FilterScope.Global);
+ var actionFilter = new FilterInfo(new TestMultiFilter(), FilterScope.Action);
+ var controllerFilter = new FilterInfo(new TestMultiFilter(), FilterScope.Controller);
+ Mock<IDependencyResolver> resolverMock = BuildFilterProvidingDependencyResolver(_configurationInstance, actionDescriptorMock, globalFilter, actionFilter, controllerFilter);
+ _configurationInstance.ServiceResolver.SetResolver(resolverMock.Object);
+
+ // Act
+ var result = actionDescriptorMock.GetFilterPipeline().ToArray();
+
+ // Assert
+ Assert.Equal(new[] { globalFilter, controllerFilter, actionFilter }, result);
+ }
+
+ [Fact]
+ public void GetFilters_RemovesDuplicateUniqueFiltersKeepingMostSpecificScope()
+ {
+ // Arrange
+ HttpActionDescriptor actionDescriptorMock = new Mock<HttpActionDescriptor>() { CallBase = true }.Object;
+ actionDescriptorMock.Configuration = _configurationInstance;
+
+ var multiActionFilter = new FilterInfo(new TestMultiFilter(), FilterScope.Action);
+ var multiGlobalFilter = new FilterInfo(new TestMultiFilter(), FilterScope.Global);
+ var uniqueControllerFilter = new FilterInfo(new TestUniqueFilter(), FilterScope.Controller);
+ var uniqueActionFilter = new FilterInfo(new TestUniqueFilter(), FilterScope.Action);
+ Mock<IDependencyResolver> resolverMock = BuildFilterProvidingDependencyResolver(
+ _configurationInstance, actionDescriptorMock,
+ multiActionFilter, multiGlobalFilter, uniqueControllerFilter, uniqueActionFilter);
+ _configurationInstance.ServiceResolver.SetResolver(resolverMock.Object);
+
+ // Act
+ var result = actionDescriptorMock.GetFilterPipeline().ToArray();
+
+ // Assert
+ Assert.Equal(new[] { multiGlobalFilter, multiActionFilter, uniqueActionFilter }, result);
+ }
+
+ [Fact]
+ public void InvokeActionWithActionFilters_ChainsFiltersInOrderFollowedByInnerActionContinuation()
+ {
+ // Arrange
+ List<string> log = new List<string>();
+ Mock<IActionFilter> globalFilterMock = CreateActionFilterMock((ctx, ct, continuation) =>
+ {
+ log.Add("globalFilter");
+ return continuation();
+ });
+ Mock<IActionFilter> actionFilterMock = CreateActionFilterMock((ctx, ct, continuation) =>
+ {
+ log.Add("actionFilter");
+ return continuation();
+ });
+ Func<Task<HttpResponseMessage>> innerAction = () => Task<HttpResponseMessage>.Factory.StartNew(() =>
+ {
+ log.Add("innerAction");
+ return null;
+ });
+ List<IActionFilter> filters = new List<IActionFilter>() {
+ globalFilterMock.Object,
+ actionFilterMock.Object,
+ };
+
+ // Act
+ var result = ApiController.InvokeActionWithActionFilters(_actionContextInstance, CancellationToken.None, filters, innerAction);
+
+ // Assert
+ Assert.NotNull(result);
+ var resultTask = result();
+ Assert.NotNull(resultTask);
+ resultTask.WaitUntilCompleted();
+ Assert.Equal(new[] { "globalFilter", "actionFilter", "innerAction" }, log.ToArray());
+ globalFilterMock.Verify();
+ actionFilterMock.Verify();
+ }
+
+ [Fact]
+ public void InvokeActionWithAuthorizationFilters_ChainsFiltersInOrderFollowedByInnerActionContinuation()
+ {
+ // Arrange
+ List<string> log = new List<string>();
+ Mock<IAuthorizationFilter> globalFilterMock = CreateAuthorizationFilterMock((ctx, ct, continuation) =>
+ {
+ log.Add("globalFilter");
+ return continuation();
+ });
+ Mock<IAuthorizationFilter> actionFilterMock = CreateAuthorizationFilterMock((ctx, ct, continuation) =>
+ {
+ log.Add("actionFilter");
+ return continuation();
+ });
+ Func<Task<HttpResponseMessage>> innerAction = () => Task<HttpResponseMessage>.Factory.StartNew(() =>
+ {
+ log.Add("innerAction");
+ return null;
+ });
+ List<IAuthorizationFilter> filters = new List<IAuthorizationFilter>() {
+ globalFilterMock.Object,
+ actionFilterMock.Object,
+ };
+
+ // Act
+ var result = ApiController.InvokeActionWithAuthorizationFilters(_actionContextInstance, CancellationToken.None, filters, innerAction);
+
+ // Assert
+ Assert.NotNull(result);
+ var resultTask = result();
+ Assert.NotNull(resultTask);
+ resultTask.WaitUntilCompleted();
+ Assert.Equal(new[] { "globalFilter", "actionFilter", "innerAction" }, log.ToArray());
+ globalFilterMock.Verify();
+ actionFilterMock.Verify();
+ }
+
+ [Fact]
+ public void InvokeActionWithExceptionFilters_IfActionTaskIsSuccessful_ReturnsSuccessTask()
+ {
+ // Arrange
+ List<string> log = new List<string>();
+ var response = new HttpResponseMessage();
+ var actionTask = TaskHelpers.FromResult(response);
+ var exceptionFilterMock = CreateExceptionFilterMock((ec, ct) =>
+ {
+ log.Add("exceptionFilter");
+ return Task.Factory.StartNew(() => { });
+ });
+ var filters = new[] { exceptionFilterMock.Object };
+
+ // Act
+ var result = ApiController.InvokeActionWithExceptionFilters(actionTask, _actionContextInstance, CancellationToken.None, filters);
+
+ // Assert
+ Assert.NotNull(result);
+ result.WaitUntilCompleted();
+ Assert.Equal(TaskStatus.RanToCompletion, result.Status);
+ Assert.Same(response, result.Result);
+ Assert.Equal(new string[] { }, log.ToArray());
+ }
+
+ [Fact]
+ public void InvokeActionWithExceptionFilters_IfActionTaskIsCanceled_ReturnsCanceledTask()
+ {
+ // Arrange
+ List<string> log = new List<string>();
+ var actionTask = TaskHelpers.Canceled<HttpResponseMessage>();
+ var exceptionFilterMock = CreateExceptionFilterMock((ec, ct) =>
+ {
+ log.Add("exceptionFilter");
+ return Task.Factory.StartNew(() => { });
+ });
+ var filters = new[] { exceptionFilterMock.Object };
+
+ // Act
+ var result = ApiController.InvokeActionWithExceptionFilters(actionTask, _actionContextInstance, CancellationToken.None, filters);
+
+ // Assert
+ Assert.NotNull(result);
+ result.WaitUntilCompleted();
+ Assert.Equal(TaskStatus.Canceled, result.Status);
+ Assert.Equal(new string[] { }, log.ToArray());
+ }
+
+ [Fact]
+ public void InvokeActionWithExceptionFilters_IfActionTaskIsFaulted_ExecutesFiltersAndReturnsFaultedTaskIfNotHandled()
+ {
+ // Arrange
+ List<string> log = new List<string>();
+ var exception = new Exception();
+ var actionTask = TaskHelpers.FromError<HttpResponseMessage>(exception);
+ Exception exceptionSeenByFilter = null;
+ var exceptionFilterMock = CreateExceptionFilterMock((ec, ct) =>
+ {
+ exceptionSeenByFilter = ec.Exception;
+ log.Add("exceptionFilter");
+ return Task.Factory.StartNew(() => { });
+ });
+ var filters = new[] { exceptionFilterMock.Object };
+
+ // Act
+ var result = ApiController.InvokeActionWithExceptionFilters(actionTask, _actionContextInstance, CancellationToken.None, filters);
+
+ // Assert
+ Assert.NotNull(result);
+ result.WaitUntilCompleted();
+ Assert.Equal(TaskStatus.Faulted, result.Status);
+ Assert.Same(exception, result.Exception.InnerException);
+ Assert.Same(exception, exceptionSeenByFilter);
+ Assert.Equal(new string[] { "exceptionFilter" }, log.ToArray());
+ }
+
+ [Fact]
+ public void InvokeActionWithExceptionFilters_IfActionTaskIsFaulted_ExecutesFiltersAndReturnsResultIfHandled()
+ {
+ // Arrange
+ List<string> log = new List<string>();
+ var exception = new Exception();
+ var actionTask = TaskHelpers.FromError<HttpResponseMessage>(exception);
+ HttpResponseMessage globalFilterResponse = new HttpResponseMessage();
+ HttpResponseMessage actionFilterResponse = new HttpResponseMessage();
+ HttpResponseMessage resultSeenByGlobalFilter = null;
+ var globalFilterMock = CreateExceptionFilterMock((ec, ct) =>
+ {
+ log.Add("globalFilter");
+ resultSeenByGlobalFilter = ec.Result;
+ ec.Result = globalFilterResponse;
+ return Task.Factory.StartNew(() => { });
+ });
+ var actionFilterMock = CreateExceptionFilterMock((ec, ct) =>
+ {
+ log.Add("actionFilter");
+ ec.Result = actionFilterResponse;
+ return Task.Factory.StartNew(() => { });
+ });
+ var filters = new[] { globalFilterMock.Object, actionFilterMock.Object };
+
+ // Act
+ var result = ApiController.InvokeActionWithExceptionFilters(actionTask, _actionContextInstance, CancellationToken.None, filters);
+
+ // Assert
+ Assert.NotNull(result);
+ result.WaitUntilCompleted();
+ Assert.Equal(TaskStatus.RanToCompletion, result.Status);
+ Assert.Same(globalFilterResponse, result.Result);
+ Assert.Same(actionFilterResponse, resultSeenByGlobalFilter);
+ Assert.Equal(new string[] { "actionFilter", "globalFilter" }, log.ToArray());
+ }
+
+ private Mock<IAuthorizationFilter> CreateAuthorizationFilterMock(Func<HttpActionContext, CancellationToken, Func<Task<HttpResponseMessage>>, Task<HttpResponseMessage>> implementation)
+ {
+ Mock<IAuthorizationFilter> filterMock = new Mock<IAuthorizationFilter>();
+ filterMock.Setup(f => f.ExecuteAuthorizationFilterAsync(It.IsAny<HttpActionContext>(),
+ CancellationToken.None,
+ It.IsAny<Func<Task<HttpResponseMessage>>>()))
+ .Returns(implementation)
+ .Verifiable();
+ return filterMock;
+ }
+
+ private Mock<IActionFilter> CreateActionFilterMock(Func<HttpActionContext, CancellationToken, Func<Task<HttpResponseMessage>>, Task<HttpResponseMessage>> implementation)
+ {
+ Mock<IActionFilter> filterMock = new Mock<IActionFilter>();
+ filterMock.Setup(f => f.ExecuteActionFilterAsync(It.IsAny<HttpActionContext>(),
+ CancellationToken.None,
+ It.IsAny<Func<Task<HttpResponseMessage>>>()))
+ .Returns(implementation)
+ .Verifiable();
+ return filterMock;
+ }
+
+ private Mock<IExceptionFilter> CreateExceptionFilterMock(Func<HttpActionExecutedContext, CancellationToken, Task> implementation)
+ {
+ Mock<IExceptionFilter> filterMock = new Mock<IExceptionFilter>();
+ filterMock.Setup(f => f.ExecuteExceptionFilterAsync(It.IsAny<HttpActionExecutedContext>(),
+ CancellationToken.None))
+ .Returns(implementation)
+ .Verifiable();
+ return filterMock;
+ }
+
+ private static Mock<IDependencyResolver> BuildFilterProvidingDependencyResolver(HttpConfiguration configuration, HttpActionDescriptor action, params FilterInfo[] filters)
+ {
+ Mock<IDependencyResolver> resolverMock = new Mock<IDependencyResolver>();
+ Mock<IFilterProvider> filterProviderMock = new Mock<IFilterProvider>();
+ resolverMock.Setup(r => r.GetServices(typeof(IFilterProvider))).Returns(new[] { filterProviderMock.Object });
+ filterProviderMock.Setup(fp => fp.GetFilters(configuration, action)).Returns(filters);
+ return resolverMock;
+ }
+
+ /// <summary>
+ /// Simple IFilter implementation with AllowMultiple = true
+ /// </summary>
+ public class TestMultiFilter : IFilter
+ {
+ public bool AllowMultiple
+ {
+ get { return true; }
+ }
+ }
+
+ /// <summary>
+ /// Simple IFilter implementation with AllowMultiple = false
+ /// </summary>
+ public class TestUniqueFilter : IFilter
+ {
+ public bool AllowMultiple
+ {
+ get { return false; }
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Controllers/Apis/User.cs b/test/System.Web.Http.Test/Controllers/Apis/User.cs
new file mode 100644
index 00000000..09fe81ff
--- /dev/null
+++ b/test/System.Web.Http.Test/Controllers/Apis/User.cs
@@ -0,0 +1,9 @@
+
+namespace System.Web.Http
+{
+ public class User
+ {
+ public string FirstName { get; set; }
+ public string LastName { get; set; }
+ }
+}
diff --git a/test/System.Web.Http.Test/Controllers/Apis/UsersController.cs b/test/System.Web.Http.Test/Controllers/Apis/UsersController.cs
new file mode 100644
index 00000000..b56ba60e
--- /dev/null
+++ b/test/System.Web.Http.Test/Controllers/Apis/UsersController.cs
@@ -0,0 +1,40 @@
+using System.Net;
+using System.Net.Http;
+
+namespace System.Web.Http
+{
+ public class UsersController : ApiController
+ {
+ public HttpResponseMessage Get()
+ {
+ return new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent("Default User")
+ };
+ }
+
+ public HttpResponseMessage Post()
+ {
+ return new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent("User Posted")
+ };
+ }
+
+ public HttpResponseMessage Put()
+ {
+ return new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent("User Updated")
+ };
+ }
+
+ public HttpResponseMessage Delete()
+ {
+ return new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent("User Deleted")
+ };
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Controllers/Apis/UsersRpcController.cs b/test/System.Web.Http.Test/Controllers/Apis/UsersRpcController.cs
new file mode 100644
index 00000000..d5024aaf
--- /dev/null
+++ b/test/System.Web.Http.Test/Controllers/Apis/UsersRpcController.cs
@@ -0,0 +1,57 @@
+
+namespace System.Web.Http
+{
+ public class UsersRpcController : ApiController
+ {
+ public User EchoUser(string firstName, string lastName)
+ {
+ return new User()
+ {
+ FirstName = firstName,
+ LastName = lastName,
+ };
+ }
+
+ [Authorize]
+ [HttpGet]
+ public User AddAdmin(string firstName, string lastName)
+ {
+ return new User()
+ {
+ FirstName = firstName,
+ LastName = lastName,
+ };
+ }
+
+ public User RetriveUser(int id)
+ {
+ return new User()
+ {
+ LastName = "UserLN" + id,
+ FirstName = "UserFN" + id
+ };
+ }
+
+ public User EchoUserObject(User user)
+ {
+ return user;
+ }
+
+ public User Admin()
+ {
+ return new User
+ {
+ FirstName = "Yao",
+ LastName = "Huang"
+ };
+ }
+
+ public void DeleteAllUsers()
+ {
+ }
+
+ public void AddUser([FromBody] User user)
+ {
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Controllers/HttpActionContextTest.cs b/test/System.Web.Http.Test/Controllers/HttpActionContextTest.cs
new file mode 100644
index 00000000..6e878329
--- /dev/null
+++ b/test/System.Web.Http.Test/Controllers/HttpActionContextTest.cs
@@ -0,0 +1,76 @@
+using System.Web.Http.Controllers;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http
+{
+ public class HttpActionContextTest
+ {
+ [Fact]
+ public void Default_Constructor()
+ {
+ HttpActionContext actionContext = new HttpActionContext();
+
+ Assert.Null(actionContext.ControllerContext);
+ Assert.Null(actionContext.ActionDescriptor);
+ Assert.Null(actionContext.Response);
+ Assert.Null(actionContext.Request);
+ Assert.NotNull(actionContext.ActionArguments);
+ Assert.NotNull(actionContext.ModelState);
+ }
+
+ [Fact]
+ public void Parameter_Constructor()
+ {
+ HttpControllerContext controllerContext = ContextUtil.CreateControllerContext();
+ HttpActionDescriptor actionDescriptor = new Mock<HttpActionDescriptor>().Object;
+ HttpActionContext actionContext = new HttpActionContext(controllerContext, actionDescriptor);
+
+ Assert.Same(controllerContext, actionContext.ControllerContext);
+ Assert.Same(actionDescriptor, actionContext.ActionDescriptor);
+ Assert.Same(controllerContext.Request, actionContext.Request);
+ Assert.Null(actionContext.Response);
+ Assert.NotNull(actionContext.ActionArguments);
+ Assert.NotNull(actionContext.ModelState);
+ }
+
+ [Fact]
+ public void Constructor_Throws_IfControllerContextIsNull()
+ {
+ Assert.ThrowsArgumentNull(
+ () => new HttpActionContext(null, new Mock<HttpActionDescriptor>().Object),
+ "controllerContext");
+ }
+
+ [Fact]
+ public void Constructor_Throws_IfActionDescriptorIsNull()
+ {
+ Assert.ThrowsArgumentNull(
+ () => new HttpActionContext(ContextUtil.CreateControllerContext(), null),
+ "actionDescriptor");
+ }
+
+ [Fact]
+ public void ControllerContext_Property()
+ {
+ Assert.Reflection.Property<HttpActionContext, HttpControllerContext>(
+ instance: new HttpActionContext(),
+ propertyGetter: ac => ac.ControllerContext,
+ expectedDefaultValue: null,
+ allowNull: false,
+ roundTripTestValue: ContextUtil.CreateControllerContext());
+ }
+
+ [Fact]
+ public void ActionDescriptor_Property()
+ {
+ Assert.Reflection.Property<HttpActionContext, HttpActionDescriptor>(
+ instance: new HttpActionContext(),
+ propertyGetter: ac => ac.ActionDescriptor,
+ expectedDefaultValue: null,
+ allowNull: false,
+ roundTripTestValue: new Mock<HttpActionDescriptor>().Object);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Controllers/HttpConfigurationTest.cs b/test/System.Web.Http.Test/Controllers/HttpConfigurationTest.cs
new file mode 100644
index 00000000..da0e9718
--- /dev/null
+++ b/test/System.Web.Http.Test/Controllers/HttpConfigurationTest.cs
@@ -0,0 +1,48 @@
+using Microsoft.TestCommon;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http
+{
+ public class HttpConfigurationTest
+ {
+ [Fact]
+ public void TypeIsCorrect()
+ {
+ Assert.Type.HasProperties<HttpConfiguration>(TypeAssert.TypeProperties.IsPublicVisibleClass | TypeAssert.TypeProperties.IsDisposable);
+ }
+
+ [Fact]
+ public void Default_Constructor()
+ {
+ HttpConfiguration configuration = new HttpConfiguration();
+
+ Assert.Empty(configuration.Filters);
+ Assert.NotEmpty(configuration.Formatters);
+ Assert.Empty(configuration.MessageHandlers);
+ Assert.Empty(configuration.Properties);
+ Assert.Empty(configuration.Routes);
+ Assert.NotNull(configuration.ServiceResolver);
+ Assert.Equal("/", configuration.VirtualPathRoot);
+ }
+
+ [Fact]
+ public void Parameter_Constructor()
+ {
+ string path = "/some/path";
+ HttpRouteCollection routes = new HttpRouteCollection(path);
+ HttpConfiguration configuration = new HttpConfiguration(routes);
+
+ Assert.Same(routes, configuration.Routes);
+ Assert.Equal(path, configuration.VirtualPathRoot);
+ }
+
+ [Fact]
+ public void Dispose_Idempotent()
+ {
+ HttpConfiguration configuration = new HttpConfiguration();
+ configuration.Dispose();
+ configuration.Dispose();
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Controllers/HttpControllerContextTest.cs b/test/System.Web.Http.Test/Controllers/HttpControllerContextTest.cs
new file mode 100644
index 00000000..c745fd59
--- /dev/null
+++ b/test/System.Web.Http.Test/Controllers/HttpControllerContextTest.cs
@@ -0,0 +1,108 @@
+using System.Net.Http;
+using System.Web.Http.Controllers;
+using System.Web.Http.Routing;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http
+{
+ public class HttpControllerContextTest
+ {
+ [Fact]
+ public void Default_Constructor()
+ {
+ HttpControllerContext controllerContext = new HttpControllerContext();
+
+ Assert.Null(controllerContext.Configuration);
+ Assert.Null(controllerContext.Controller);
+ Assert.Null(controllerContext.ControllerDescriptor);
+ Assert.Null(controllerContext.Request);
+ Assert.Null(controllerContext.RouteData);
+ }
+
+ [Fact]
+ public void Parameter_Constructor()
+ {
+ HttpConfiguration config = new HttpConfiguration();
+ IHttpRouteData routeData = new Mock<IHttpRouteData>().Object;
+ HttpRequestMessage request = new HttpRequestMessage();
+ HttpControllerContext controllerContext = new HttpControllerContext(config, routeData, request);
+
+ Assert.Same(config, controllerContext.Configuration);
+ Assert.Same(request, controllerContext.Request);
+ Assert.Same(routeData, controllerContext.RouteData);
+ Assert.Null(controllerContext.Controller);
+ Assert.Null(controllerContext.ControllerDescriptor);
+ }
+
+ [Fact]
+ public void Constructor_Throws_IfConfigurationIsNull()
+ {
+ Assert.ThrowsArgumentNull(
+ () => new HttpControllerContext(null, new Mock<IHttpRouteData>().Object, new HttpRequestMessage()),
+ "configuration");
+ }
+
+ [Fact]
+ public void Constructor_Throws_IfRouteDataIsNull()
+ {
+ Assert.ThrowsArgumentNull(
+ () => new HttpControllerContext(new HttpConfiguration(), null, new HttpRequestMessage()),
+ "routeData");
+ }
+
+ [Fact]
+ public void Constructor_Throws_IfRequestIsNull()
+ {
+ Assert.ThrowsArgumentNull(
+ () => new HttpControllerContext(new HttpConfiguration(), new Mock<IHttpRouteData>().Object, null),
+ "request");
+ }
+
+ [Fact]
+ public void Configuration_Property()
+ {
+ Assert.Reflection.Property<HttpControllerContext, HttpConfiguration>(
+ instance: new HttpControllerContext(),
+ propertyGetter: cc => cc.Configuration,
+ expectedDefaultValue: null,
+ allowNull: false,
+ roundTripTestValue: new HttpConfiguration());
+ }
+
+ [Fact]
+ public void Controller_Property()
+ {
+ Assert.Reflection.Property<HttpControllerContext, IHttpController>(
+ instance: new HttpControllerContext(),
+ propertyGetter: cc => cc.Controller,
+ expectedDefaultValue: null,
+ allowNull: false,
+ roundTripTestValue: new Mock<IHttpController>().Object);
+ }
+
+ [Fact]
+ public void ControllerDescriptor_Property()
+ {
+ Assert.Reflection.Property<HttpControllerContext, HttpControllerDescriptor>(
+ instance: new HttpControllerContext(),
+ propertyGetter: cc => cc.ControllerDescriptor,
+ expectedDefaultValue: null,
+ allowNull: false,
+ roundTripTestValue: new HttpControllerDescriptor());
+ }
+
+ [Fact]
+ public void RouteData_Property()
+ {
+ Assert.Reflection.Property<HttpControllerContext, IHttpRouteData>(
+ instance: new HttpControllerContext(),
+ propertyGetter: cc => cc.RouteData,
+ expectedDefaultValue: null,
+ allowNull: false,
+ roundTripTestValue: new Mock<IHttpRouteData>().Object);
+ }
+
+ }
+}
diff --git a/test/System.Web.Http.Test/Controllers/HttpControllerDescriptorTest.cs b/test/System.Web.Http.Test/Controllers/HttpControllerDescriptorTest.cs
new file mode 100644
index 00000000..62c90f80
--- /dev/null
+++ b/test/System.Web.Http.Test/Controllers/HttpControllerDescriptorTest.cs
@@ -0,0 +1,183 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Web.Http.Controllers;
+using System.Web.Http.Dispatcher;
+using System.Web.Http.Filters;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http
+{
+ public class HttpControllerDescriptorTest
+ {
+ [Fact]
+ public void Default_Constructor()
+ {
+ HttpControllerDescriptor controllerDescriptor = new HttpControllerDescriptor();
+
+ Assert.Null(controllerDescriptor.ControllerName);
+ Assert.Null(controllerDescriptor.Configuration);
+ Assert.Null(controllerDescriptor.ControllerType);
+ Assert.Null(controllerDescriptor.HttpActionInvoker);
+ Assert.Null(controllerDescriptor.HttpActionSelector);
+ Assert.Null(controllerDescriptor.HttpControllerActivator);
+ Assert.NotNull(controllerDescriptor.Properties);
+ }
+
+ [Fact]
+ public void Parameter_Constructor()
+ {
+ HttpConfiguration config = new HttpConfiguration();
+ string controllerName = "UsersController";
+ Type controllerType = typeof(UsersController);
+
+ HttpControllerDescriptor controllerDescriptor = new HttpControllerDescriptor(config, controllerName, controllerType);
+ Assert.NotNull(controllerDescriptor.ControllerName);
+ Assert.NotNull(controllerDescriptor.Configuration);
+ Assert.NotNull(controllerDescriptor.ControllerType);
+ Assert.NotNull(controllerDescriptor.HttpActionInvoker);
+ Assert.NotNull(controllerDescriptor.HttpActionSelector);
+ Assert.NotNull(controllerDescriptor.HttpControllerActivator);
+ Assert.NotNull(controllerDescriptor.Properties);
+ Assert.Equal(config, controllerDescriptor.Configuration);
+ Assert.Equal(controllerName, controllerDescriptor.ControllerName);
+ Assert.Equal(controllerType, controllerDescriptor.ControllerType);
+ }
+
+ [Fact]
+ public void Constructor_Throws_IfConfigurationIsNull()
+ {
+ Assert.ThrowsArgumentNull(
+ () => new HttpControllerDescriptor(null, "UsersController", typeof(UsersController)),
+ "configuration");
+ }
+
+ [Fact]
+ public void Constructor_Throws_IfControllerNameIsNull()
+ {
+ Assert.ThrowsArgumentNull(
+ () => new HttpControllerDescriptor(new HttpConfiguration(), null, typeof(UsersController)),
+ "controllerName");
+ }
+
+ [Fact]
+ public void Constructor_Throws_IfControllerTypeIsNull()
+ {
+ Assert.ThrowsArgumentNull(
+ () => new HttpControllerDescriptor(new HttpConfiguration(), "UsersController", null),
+ "controllerType");
+ }
+
+ [Fact]
+ public void Configuration_Property()
+ {
+ HttpConfiguration config = new HttpConfiguration();
+ HttpControllerDescriptor controllerDescriptor = new HttpControllerDescriptor();
+
+ Assert.Reflection.Property<HttpControllerDescriptor, HttpConfiguration>(
+ instance: controllerDescriptor,
+ propertyGetter: cd => cd.Configuration,
+ expectedDefaultValue: null,
+ allowNull: false,
+ roundTripTestValue: config);
+ }
+
+ [Fact]
+ public void ControllerName_Property()
+ {
+ string controllerName = "UsersController";
+ HttpControllerDescriptor controllerDescriptor = new HttpControllerDescriptor();
+
+ Assert.Reflection.Property<HttpControllerDescriptor, string>(
+ instance: controllerDescriptor,
+ propertyGetter: cd => cd.ControllerName,
+ expectedDefaultValue: null,
+ allowNull: false,
+ roundTripTestValue: controllerName);
+ }
+
+ [Fact]
+ public void ControllerType_Property()
+ {
+ Type controllerType = typeof(UsersController);
+ HttpControllerDescriptor controllerDescriptor = new HttpControllerDescriptor();
+
+ Assert.Reflection.Property<HttpControllerDescriptor, Type>(
+ instance: controllerDescriptor,
+ propertyGetter: cd => cd.ControllerType,
+ expectedDefaultValue: null,
+ allowNull: false,
+ roundTripTestValue: controllerType);
+ }
+
+ [Fact]
+ public void HttpActionInvoker_Property()
+ {
+ IHttpActionInvoker invoker = new ApiControllerActionInvoker();
+ HttpControllerDescriptor controllerDescriptor = new HttpControllerDescriptor();
+
+ Assert.Reflection.Property<HttpControllerDescriptor, IHttpActionInvoker>(
+ instance: controllerDescriptor,
+ propertyGetter: cd => cd.HttpActionInvoker,
+ expectedDefaultValue: null,
+ allowNull: false,
+ roundTripTestValue: invoker);
+ }
+
+ [Fact]
+ public void HttpActionSelector_Property()
+ {
+ IHttpActionSelector selector = new ApiControllerActionSelector();
+ HttpControllerDescriptor controllerDescriptor = new HttpControllerDescriptor();
+
+ Assert.Reflection.Property<HttpControllerDescriptor, IHttpActionSelector>(
+ instance: controllerDescriptor,
+ propertyGetter: cd => cd.HttpActionSelector,
+ expectedDefaultValue: null,
+ allowNull: false,
+ roundTripTestValue: selector);
+ }
+
+ [Fact]
+ public void HttpControllerActivator_Property()
+ {
+ IHttpControllerActivator activator = new Mock<IHttpControllerActivator>().Object;
+ HttpControllerDescriptor controllerDescriptor = new HttpControllerDescriptor();
+
+ Assert.Reflection.Property<HttpControllerDescriptor, IHttpControllerActivator>(
+ instance: controllerDescriptor,
+ propertyGetter: cd => cd.HttpControllerActivator,
+ expectedDefaultValue: null,
+ allowNull: false,
+ roundTripTestValue: activator);
+ }
+
+ [Fact]
+ public void ActionValueBinder_Property()
+ {
+ IActionValueBinder activator = new Mock<IActionValueBinder>().Object;
+ HttpControllerDescriptor controllerDescriptor = new HttpControllerDescriptor();
+
+ Assert.Reflection.Property<HttpControllerDescriptor, IActionValueBinder>(
+ instance: controllerDescriptor,
+ propertyGetter: cd => cd.ActionValueBinder,
+ expectedDefaultValue: null,
+ allowNull: false,
+ roundTripTestValue: activator);
+ }
+
+ [Fact]
+ public void GetFilters_InvokesGetCustomAttributesMethod()
+ {
+ var descriptorMock = new Mock<HttpControllerDescriptor> { CallBase = true };
+ var filters = new ReadOnlyCollection<IFilter>(new List<IFilter>());
+ descriptorMock.Setup(d => d.GetCustomAttributes<IFilter>()).Returns(filters).Verifiable();
+
+ var result = descriptorMock.Object.GetFilters();
+
+ Assert.Same(filters, result);
+ descriptorMock.Verify();
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Controllers/HttpParameterDescriptorTest.cs b/test/System.Web.Http.Test/Controllers/HttpParameterDescriptorTest.cs
new file mode 100644
index 00000000..44ae4113
--- /dev/null
+++ b/test/System.Web.Http.Test/Controllers/HttpParameterDescriptorTest.cs
@@ -0,0 +1,73 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Web.Http.Controllers;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http
+{
+ public class HttpParameterDescriptorTest
+ {
+ [Fact]
+ public void Default_Constructor()
+ {
+ HttpParameterDescriptor parameterDescriptor = new Mock<HttpParameterDescriptor>().Object;
+
+ Assert.Null(parameterDescriptor.ParameterName);
+ Assert.Null(parameterDescriptor.ParameterType);
+ Assert.Null(parameterDescriptor.Configuration);
+ Assert.Null(parameterDescriptor.Prefix);
+ Assert.Null(parameterDescriptor.ModelBinderAttribute);
+ Assert.Null(parameterDescriptor.ActionDescriptor);
+ Assert.Null(parameterDescriptor.DefaultValue);
+ Assert.NotNull(parameterDescriptor.Properties);
+ }
+
+ [Fact]
+ public void Configuration_Property()
+ {
+ HttpConfiguration config = new HttpConfiguration();
+ HttpParameterDescriptor parameterDescriptor = new Mock<HttpParameterDescriptor> { CallBase = true }.Object;
+
+ Assert.Reflection.Property<HttpParameterDescriptor, HttpConfiguration>(
+ instance: parameterDescriptor,
+ propertyGetter: pd => pd.Configuration,
+ expectedDefaultValue: null,
+ allowNull: false,
+ roundTripTestValue: config);
+ }
+
+ [Fact]
+ public void ActionDescriptor_Property()
+ {
+ HttpParameterDescriptor parameterDescriptor = new Mock<HttpParameterDescriptor> { CallBase = true }.Object;
+ HttpActionDescriptor actionDescriptor = new Mock<HttpActionDescriptor>().Object;
+
+ Assert.Reflection.Property<HttpParameterDescriptor, HttpActionDescriptor>(
+ instance: parameterDescriptor,
+ propertyGetter: pd => pd.ActionDescriptor,
+ expectedDefaultValue: null,
+ allowNull: false,
+ roundTripTestValue: actionDescriptor);
+ }
+
+ [Fact]
+ public void GetCustomAttributes_Returns_EmptyAttributes()
+ {
+ HttpParameterDescriptor parameterDescriptor = new Mock<HttpParameterDescriptor> { CallBase = true }.Object;
+ IEnumerable<object> attributes = parameterDescriptor.GetCustomAttributes<object>();
+
+ Assert.Equal(0, attributes.Count());
+ }
+
+ [Fact]
+ public void GetCustomAttributes_AttributeType_Returns_EmptyAttributes()
+ {
+ HttpParameterDescriptor parameterDescriptor = new Mock<HttpParameterDescriptor> { CallBase = true }.Object;
+ IEnumerable<FromBodyAttribute> attributes = parameterDescriptor.GetCustomAttributes<FromBodyAttribute>();
+
+ Assert.Equal(0, attributes.Count());
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Controllers/ReflectedHttpActionDescriptorTest.cs b/test/System.Web.Http.Test/Controllers/ReflectedHttpActionDescriptorTest.cs
new file mode 100644
index 00000000..253fdada
--- /dev/null
+++ b/test/System.Web.Http.Test/Controllers/ReflectedHttpActionDescriptorTest.cs
@@ -0,0 +1,299 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Reflection;
+using System.Web.Http.Common;
+using System.Web.Http.Controllers;
+using System.Web.Http.Filters;
+using System.Web.Http.Properties;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http
+{
+ public class ReflectedHttpActionDescriptorTest
+ {
+ [Fact]
+ public void Default_Constructor()
+ {
+ ReflectedHttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor();
+
+ Assert.Null(actionDescriptor.ActionName);
+ Assert.Null(actionDescriptor.Configuration);
+ Assert.Null(actionDescriptor.ControllerDescriptor);
+ Assert.Null(actionDescriptor.MethodInfo);
+ Assert.Null(actionDescriptor.ReturnType);
+ Assert.NotNull(actionDescriptor.Properties);
+ }
+
+ [Fact]
+ public void Parameter_Constructor()
+ {
+ UsersRpcController controller = new UsersRpcController();
+ Func<string, string, User> echoUserMethod = controller.EchoUser;
+ HttpConfiguration config = new HttpConfiguration();
+ HttpControllerDescriptor controllerDescriptor = new HttpControllerDescriptor(config, "", typeof(UsersRpcController));
+ ReflectedHttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor(controllerDescriptor, echoUserMethod.Method);
+
+ Assert.Equal("EchoUser", actionDescriptor.ActionName);
+ Assert.Equal(config, actionDescriptor.Configuration);
+ Assert.Equal(typeof(UsersRpcController), actionDescriptor.ControllerDescriptor.ControllerType);
+ Assert.Equal(echoUserMethod.Method, actionDescriptor.MethodInfo);
+ Assert.Equal(typeof(User), actionDescriptor.ReturnType);
+ Assert.NotNull(actionDescriptor.Properties);
+ }
+
+ [Fact]
+ public void Constructor_Throws_IfMethodInfoIsNull()
+ {
+ Assert.ThrowsArgumentNull(
+ () => new ReflectedHttpActionDescriptor(new HttpControllerDescriptor(), null),
+ "methodInfo");
+ }
+
+ [Fact]
+ public void MethodInfo_Property()
+ {
+ ReflectedHttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor();
+ Action action = new Action(() => { });
+
+ Assert.Reflection.Property<ReflectedHttpActionDescriptor, MethodInfo>(
+ instance: actionDescriptor,
+ propertyGetter: ad => ad.MethodInfo,
+ expectedDefaultValue: null,
+ allowNull: false,
+ roundTripTestValue: action.Method);
+ }
+
+ [Fact]
+ public void ControllerDescriptor_Property()
+ {
+ ReflectedHttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor();
+ HttpControllerDescriptor controllerDescriptor = new HttpControllerDescriptor();
+
+ Assert.Reflection.Property<ReflectedHttpActionDescriptor, HttpControllerDescriptor>(
+ instance: actionDescriptor,
+ propertyGetter: ad => ad.ControllerDescriptor,
+ expectedDefaultValue: null,
+ allowNull: false,
+ roundTripTestValue: controllerDescriptor);
+ }
+
+ [Fact]
+ public void Configuration_Property()
+ {
+ ReflectedHttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor();
+ HttpConfiguration config = new HttpConfiguration();
+
+ Assert.Reflection.Property<ReflectedHttpActionDescriptor, HttpConfiguration>(
+ instance: actionDescriptor,
+ propertyGetter: ad => ad.Configuration,
+ expectedDefaultValue: null,
+ allowNull: false,
+ roundTripTestValue: config);
+ }
+
+ [Fact]
+ public void GetFilter_Returns_AttributedFilter()
+ {
+ UsersRpcController controller = new UsersRpcController();
+ Func<string, string, User> echoUserMethod = controller.AddAdmin;
+ ReflectedHttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor { MethodInfo = echoUserMethod.Method };
+ HttpControllerContext context = ContextUtil.CreateControllerContext(instance: controller);
+ Dictionary<string, object> arguments = new Dictionary<string, object>
+ {
+ {"firstName", "test"},
+ {"lastName", "unit"}
+ };
+
+ IEnumerable<IFilter> filters = actionDescriptor.GetFilters();
+
+ Assert.NotNull(filters);
+ Assert.Equal(1, filters.Count());
+ Assert.Equal(typeof(AuthorizeAttribute), filters.First().GetType());
+ }
+
+ [Fact]
+ public void GetFilterPipeline_Returns_ConfigurationFilters()
+ {
+ IActionFilter actionFilter = new Mock<IActionFilter>().Object;
+ IExceptionFilter exceptionFilter = new Mock<IExceptionFilter>().Object;
+ IAuthorizationFilter authorizationFilter = new AuthorizeAttribute();
+ UsersRpcController controller = new UsersRpcController();
+ Action deleteAllUsersMethod = controller.DeleteAllUsers;
+
+ HttpControllerDescriptor controllerDescriptor = new HttpControllerDescriptor(new HttpConfiguration(), "UsersRpcController", typeof(UsersRpcController));
+ controllerDescriptor.Configuration.Filters.Add(actionFilter);
+ controllerDescriptor.Configuration.Filters.Add(exceptionFilter);
+ controllerDescriptor.Configuration.Filters.Add(authorizationFilter);
+ ReflectedHttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor(controllerDescriptor, deleteAllUsersMethod.Method);
+
+ Collection<FilterInfo> filters = actionDescriptor.GetFilterPipeline();
+
+ Assert.Same(actionFilter, filters[0].Instance);
+ Assert.Same(exceptionFilter, filters[1].Instance);
+ Assert.Same(authorizationFilter, filters[2].Instance);
+ }
+
+ [Fact]
+ public void GetCustomAttributes_Returns_ActionAttributes()
+ {
+ UsersRpcController controller = new UsersRpcController();
+ Func<string, string, User> echoUserMethod = controller.AddAdmin;
+ ReflectedHttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor { MethodInfo = echoUserMethod.Method };
+ HttpControllerContext context = ContextUtil.CreateControllerContext(instance: controller);
+
+ IEnumerable<IFilter> filters = actionDescriptor.GetCustomAttributes<IFilter>();
+ IEnumerable<HttpGetAttribute> httpGet = actionDescriptor.GetCustomAttributes<HttpGetAttribute>();
+
+ Assert.NotNull(filters);
+ Assert.Equal(1, filters.Count());
+ Assert.Equal(typeof(AuthorizeAttribute), filters.First().GetType());
+ Assert.NotNull(httpGet);
+ Assert.Equal(1, httpGet.Count());
+ }
+
+ [Fact]
+ public void GetParameters_Returns_ActionParameters()
+ {
+ UsersRpcController controller = new UsersRpcController();
+ Func<string, string, User> echoUserMethod = controller.EchoUser;
+ ReflectedHttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor { MethodInfo = echoUserMethod.Method };
+ HttpControllerContext context = ContextUtil.CreateControllerContext(instance: controller);
+
+ Collection<HttpParameterDescriptor> parameterDescriptors = actionDescriptor.GetParameters();
+
+ Assert.Equal(2, parameterDescriptors.Count);
+ Assert.NotNull(parameterDescriptors.Where(p => p.ParameterName == "firstName").FirstOrDefault());
+ Assert.NotNull(parameterDescriptors.Where(p => p.ParameterName == "lastName").FirstOrDefault());
+ }
+
+ [Fact]
+ public void Execute_Returns_Null_ForVoidAction()
+ {
+ UsersRpcController controller = new UsersRpcController();
+ Action deleteAllUsersMethod = controller.DeleteAllUsers;
+ ReflectedHttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor { MethodInfo = deleteAllUsersMethod.Method };
+ HttpControllerContext context = ContextUtil.CreateControllerContext(instance: controller);
+ Dictionary<string, object> arguments = new Dictionary<string, object>();
+
+ object returnValue = actionDescriptor.Execute(context, arguments);
+
+ Assert.Null(returnValue);
+ }
+
+ [Fact]
+ public void Execute_Returns_Results_ForNonVoidAction()
+ {
+ UsersRpcController controller = new UsersRpcController();
+ Func<string, string, User> echoUserMethod = controller.EchoUser;
+ ReflectedHttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor { MethodInfo = echoUserMethod.Method };
+ HttpControllerContext context = ContextUtil.CreateControllerContext(instance: controller);
+ Dictionary<string, object> arguments = new Dictionary<string, object>
+ {
+ {"firstName", "test"},
+ {"lastName", "unit"}
+ };
+
+ User returnValue = actionDescriptor.Execute(context, arguments) as User;
+
+ Assert.NotNull(returnValue);
+ Assert.Equal("test", returnValue.FirstName);
+ Assert.Equal("unit", returnValue.LastName);
+ }
+
+ [Fact]
+ public void Execute_Throws_IfContextIsNull()
+ {
+ UsersRpcController controller = new UsersRpcController();
+ Func<string, string, User> echoUserMethod = controller.EchoUser;
+ ReflectedHttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor { MethodInfo = echoUserMethod.Method };
+ Dictionary<string, object> arguments = new Dictionary<string, object>();
+
+ Assert.ThrowsArgumentNull(
+ () => actionDescriptor.Execute(null, arguments),
+ "controllerContext");
+ }
+
+ [Fact]
+ public void Execute_Throws_IfArgumentsIsNull()
+ {
+ UsersRpcController controller = new UsersRpcController();
+ Func<string, string, User> echoUserMethod = controller.EchoUser;
+ ReflectedHttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor { MethodInfo = echoUserMethod.Method };
+ HttpControllerContext context = ContextUtil.CreateControllerContext();
+
+ Assert.ThrowsArgumentNull(
+ () => actionDescriptor.Execute(context, null),
+ "arguments");
+ }
+
+ [Fact]
+ public void Execute_Throws_IfValueTypeArgumentsIsNull()
+ {
+ UsersRpcController controller = new UsersRpcController();
+ Func<int, User> retrieveUserMethod = controller.RetriveUser;
+ ReflectedHttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor { MethodInfo = retrieveUserMethod.Method };
+ HttpControllerContext context = ContextUtil.CreateControllerContext(instance: controller);
+ Dictionary<string, object> arguments = new Dictionary<string, object>
+ {
+ {"id", null}
+ };
+
+ Assert.Throws<HttpResponseException>(
+ () => actionDescriptor.Execute(context, arguments),
+ Error.Format(
+ SRResources.ReflectedActionDescriptor_ParameterCannotBeNull,
+ "id",
+ typeof(int),
+ actionDescriptor.MethodInfo,
+ controller.GetType()));
+ }
+
+ [Fact]
+ public void Execute_Throws_IfArgumentNameIsWrong()
+ {
+ UsersRpcController controller = new UsersRpcController();
+ Func<int, User> retrieveUserMethod = controller.RetriveUser;
+ ReflectedHttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor { MethodInfo = retrieveUserMethod.Method };
+ HttpControllerContext context = ContextUtil.CreateControllerContext(instance: controller);
+ Dictionary<string, object> arguments = new Dictionary<string, object>
+ {
+ {"otherId", 6}
+ };
+
+ Assert.Throws<HttpResponseException>(
+ () => actionDescriptor.Execute(context, arguments),
+ Error.Format(
+ SRResources.ReflectedActionDescriptor_ParameterNotInDictionary,
+ "id",
+ typeof(int),
+ actionDescriptor.MethodInfo,
+ controller.GetType()));
+ }
+
+ [Fact]
+ public void Execute_Throws_IfArgumentTypeIsWrong()
+ {
+ UsersRpcController controller = new UsersRpcController();
+ Func<int, User> retrieveUserMethod = controller.RetriveUser;
+ ReflectedHttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor { MethodInfo = retrieveUserMethod.Method };
+ HttpControllerContext context = ContextUtil.CreateControllerContext(instance: controller);
+ Dictionary<string, object> arguments = new Dictionary<string, object>
+ {
+ {"id", new DateTime()}
+ };
+
+ Assert.Throws<HttpResponseException>(
+ () => actionDescriptor.Execute(context, arguments),
+ Error.Format(
+ SRResources.ReflectedActionDescriptor_ParameterValueHasWrongType,
+ "id",
+ actionDescriptor.MethodInfo,
+ controller.GetType(),
+ typeof(DateTime),
+ typeof(int)));
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Controllers/ReflectedHttpParameterDescriptorTest.cs b/test/System.Web.Http.Test/Controllers/ReflectedHttpParameterDescriptorTest.cs
new file mode 100644
index 00000000..69fd841b
--- /dev/null
+++ b/test/System.Web.Http.Test/Controllers/ReflectedHttpParameterDescriptorTest.cs
@@ -0,0 +1,92 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Web.Http.Controllers;
+using System.Web.Http.ValueProviders;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http
+{
+ public class ReflectedHttpParameterDescriptorTest
+ {
+ [Fact]
+ public void Parameter_Constructor()
+ {
+ UsersRpcController controller = new UsersRpcController();
+ Func<string, string, User> echoUserMethod = controller.EchoUser;
+ ReflectedHttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor { MethodInfo = echoUserMethod.Method };
+ ParameterInfo parameterInfo = echoUserMethod.Method.GetParameters()[0];
+ ReflectedHttpParameterDescriptor parameterDescriptor = new ReflectedHttpParameterDescriptor(actionDescriptor, parameterInfo);
+
+ Assert.Equal(actionDescriptor, parameterDescriptor.ActionDescriptor);
+ Assert.Null(parameterDescriptor.DefaultValue);
+ Assert.Equal(parameterInfo, parameterDescriptor.ParameterInfo);
+ Assert.Equal(parameterInfo.Name, parameterDescriptor.ParameterName);
+ Assert.Equal(typeof(string), parameterDescriptor.ParameterType);
+ Assert.Null(parameterDescriptor.Prefix);
+ Assert.Null(parameterDescriptor.ModelBinderAttribute);
+ }
+
+ [Fact]
+ public void Constructor_Throws_IfParameterInfoIsNull()
+ {
+ Assert.ThrowsArgumentNull(
+ () => new ReflectedHttpParameterDescriptor(new Mock<HttpActionDescriptor>().Object, null),
+ "parameterInfo");
+ }
+
+ [Fact]
+ public void Constructor_Throws_IfActionDescriptorIsNull()
+ {
+ Assert.ThrowsArgumentNull(
+ () => new ReflectedHttpParameterDescriptor(null, new Mock<ParameterInfo>().Object),
+ "actionDescriptor");
+ }
+
+ [Fact]
+ public void ParameterInfo_Property()
+ {
+ ParameterInfo referenceParameter = new Mock<ParameterInfo>().Object;
+ Assert.Reflection.Property(new ReflectedHttpParameterDescriptor(), d => d.ParameterInfo, expectedDefaultValue: null, allowNull: false, roundTripTestValue: referenceParameter);
+ }
+
+ [Fact]
+ public void IsDefined_Retruns_True_WhenParameterAttributeIsFound()
+ {
+ UsersRpcController controller = new UsersRpcController();
+ Action<User> addUserMethod = controller.AddUser;
+ ReflectedHttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor { MethodInfo = addUserMethod.Method };
+ ParameterInfo parameterInfo = addUserMethod.Method.GetParameters()[0];
+ ReflectedHttpParameterDescriptor parameterDescriptor = new ReflectedHttpParameterDescriptor(actionDescriptor, parameterInfo);
+ }
+
+ [Fact]
+ public void GetCustomAttributes_Returns_ParameterAttributes()
+ {
+ UsersRpcController controller = new UsersRpcController();
+ Action<User> addUserMethod = controller.AddUser;
+ ReflectedHttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor { MethodInfo = addUserMethod.Method };
+ ParameterInfo parameterInfo = addUserMethod.Method.GetParameters()[0];
+ ReflectedHttpParameterDescriptor parameterDescriptor = new ReflectedHttpParameterDescriptor(actionDescriptor, parameterInfo);
+ object[] attributes = parameterDescriptor.GetCustomAttributes<object>().ToArray();
+
+ Assert.Equal(1, attributes.Length);
+ Assert.Equal(typeof(FromBodyAttribute), attributes[0].GetType());
+ }
+
+ [Fact]
+ public void GetCustomAttributes_AttributeType_Returns_ParameterAttributes()
+ {
+ UsersRpcController controller = new UsersRpcController();
+ Action<User> addUserMethod = controller.AddUser;
+ ReflectedHttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor { MethodInfo = addUserMethod.Method };
+ ParameterInfo parameterInfo = addUserMethod.Method.GetParameters()[0];
+ ReflectedHttpParameterDescriptor parameterDescriptor = new ReflectedHttpParameterDescriptor(actionDescriptor, parameterInfo);
+ IEnumerable<FromBodyAttribute> attributes = parameterDescriptor.GetCustomAttributes<FromBodyAttribute>();
+
+ Assert.Equal(1, attributes.Count());
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/DictionaryExtensionsTest.cs b/test/System.Web.Http.Test/DictionaryExtensionsTest.cs
new file mode 100644
index 00000000..6362f7f9
--- /dev/null
+++ b/test/System.Web.Http.Test/DictionaryExtensionsTest.cs
@@ -0,0 +1,127 @@
+using System.Collections.Generic;
+using System.Net;
+using Microsoft.TestCommon;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http
+{
+ public class DictionaryExtensionsTest
+ {
+ [Fact]
+ public void IsCorrectType()
+ {
+ Assert.Type.HasProperties(typeof(DictionaryExtensions), TypeAssert.TypeProperties.IsStatic | TypeAssert.TypeProperties.IsPublicVisibleClass);
+ }
+
+ [Fact]
+ public void TryGetValueThrowsOnNullCollection()
+ {
+ string value;
+ Assert.ThrowsArgumentNull(() => DictionaryExtensions.TryGetValue<string>(null, String.Empty, out value), "collection");
+ }
+
+ [Fact]
+ public void TryGetValueThrowsOnNullKey()
+ {
+ IDictionary<string, object> dict = new Dictionary<string, object>();
+ string value;
+ Assert.ThrowsArgumentNull(() => dict.TryGetValue<string>(null, out value), "key");
+ }
+
+ public static TheoryDataSet<object> DictionaryValues
+ {
+ get
+ {
+ return new TheoryDataSet<object>
+ {
+ "test",
+ new string[] { "A", "B", "C" },
+ 8,
+ new List<int> {1, 2, 3},
+ 1D,
+ (IEnumerable<double>)new List<double> { 1D, 2D, 3D },
+ new Uri("http://some.host"),
+ Guid.NewGuid(),
+ HttpStatusCode.NotImplemented,
+ new HttpStatusCode[] { HttpStatusCode.Accepted, HttpStatusCode.Ambiguous, HttpStatusCode.BadGateway }
+ };
+ }
+ }
+
+ [Fact]
+ public void TryGetValueReturnsFalse()
+ {
+ // Arrange
+ IDictionary<string, object> dict = new Dictionary<string, object>();
+
+ // Act
+ string resultValue = null;
+ bool result = dict.TryGetValue("notfound", out resultValue);
+
+ // Assert
+ Assert.False(result);
+ Assert.Null(resultValue);
+ }
+
+ [Theory]
+ [PropertyData("DictionaryValues")]
+ public void TryGetValueReturnsTrue<T>(T value)
+ {
+ // Arrange
+ IDictionary<string, object> dict = new Dictionary<string, object>()
+ {
+ { "key", value }
+ };
+
+
+ // Act
+ T resultValue;
+ bool result = DictionaryExtensions.TryGetValue(dict, "key", out resultValue);
+
+ // Assert
+ Assert.True(result);
+ Assert.Equal(typeof(T), resultValue.GetType());
+ Assert.Equal(value, resultValue);
+ }
+
+ [Fact]
+ public void GetValueThrowsOnNullCollection()
+ {
+ Assert.ThrowsArgumentNull(() => DictionaryExtensions.GetValue<string>(null, String.Empty), "collection");
+ }
+
+ [Fact]
+ public void GetValueThrowsOnNullKey()
+ {
+ IDictionary<string, object> dict = new Dictionary<string, object>();
+ Assert.ThrowsArgumentNull(() => dict.GetValue<string>(null), "key");
+ }
+
+ [Fact]
+ public void GetValueThrowsOnNotFound()
+ {
+ IDictionary<string, object> dict = new Dictionary<string, object>();
+ Assert.Throws<InvalidOperationException>(() => dict.GetValue<string>("notfound"));
+ }
+
+ [Theory]
+ [PropertyData("DictionaryValues")]
+ public void GetValueReturnsValue<T>(T value)
+ {
+ // Arrange
+ IDictionary<string, object> dict = new Dictionary<string, object>()
+ {
+ { "key", value }
+ };
+
+ // Act
+ T resultValue = DictionaryExtensions.GetValue<T>(dict, "key");
+
+ // Assert
+ Assert.Equal(typeof(T), resultValue.GetType());
+ Assert.Equal(value, resultValue);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Dispatcher/DefaultBuildManagerTest.cs b/test/System.Web.Http.Test/Dispatcher/DefaultBuildManagerTest.cs
new file mode 100644
index 00000000..36ed2c7c
--- /dev/null
+++ b/test/System.Web.Http.Test/Dispatcher/DefaultBuildManagerTest.cs
@@ -0,0 +1,73 @@
+using System.Collections;
+using System.IO;
+using System.Web.Http.Dispatcher;
+using Microsoft.TestCommon;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Filters
+{
+ public class DefaultBuildManagerTest
+ {
+ [Fact]
+ public void TypeIsCorrect()
+ {
+ Assert.Type.HasProperties<DefaultBuildManager, IBuildManager>(TypeAssert.TypeProperties.IsClass);
+ }
+
+ [Fact]
+ public void Constructor()
+ {
+ Assert.NotNull(new DefaultBuildManager());
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData("path")]
+ public void FileExists(string path)
+ {
+ IBuildManager buildManager = new DefaultBuildManager();
+ Assert.False(buildManager.FileExists(path));
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData("path")]
+ public void GetCompiledType(string path)
+ {
+ IBuildManager buildManager = new DefaultBuildManager();
+ Assert.Null(buildManager.GetCompiledType(path));
+ }
+
+ [Fact]
+ public void GetReferencedAssemblies()
+ {
+ IBuildManager buildManager = new DefaultBuildManager();
+ ICollection assemblies = buildManager.GetReferencedAssemblies();
+ Assert.NotEmpty(assemblies);
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData("path")]
+ public void ReadCachedFile(string path)
+ {
+ IBuildManager buildManager = new DefaultBuildManager();
+ Assert.Null(buildManager.ReadCachedFile(path));
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData("path")]
+ public void CreateCachedFile(string path)
+ {
+ IBuildManager buildManager = new DefaultBuildManager();
+ Assert.Same(Stream.Null, buildManager.CreateCachedFile(path));
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Filters/ActionDescriptorFilterProviderTest.cs b/test/System.Web.Http.Test/Filters/ActionDescriptorFilterProviderTest.cs
new file mode 100644
index 00000000..709e6674
--- /dev/null
+++ b/test/System.Web.Http.Test/Filters/ActionDescriptorFilterProviderTest.cs
@@ -0,0 +1,75 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Web.Http.Controllers;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Filters
+{
+ public class ActionDescriptorFilterProviderTest
+ {
+ private readonly ActionDescriptorFilterProvider _provider = new ActionDescriptorFilterProvider();
+ private static readonly HttpConfiguration _configuration = new HttpConfiguration();
+
+ [Fact]
+ public void GetFilters_IfConfigurationParameterIsNull_ThrowsException()
+ {
+ Assert.ThrowsArgumentNull(() =>
+ {
+ _provider.GetFilters(configuration: null, actionDescriptor: new Mock<HttpActionDescriptor>().Object);
+ }, "configuration");
+ }
+
+ [Fact]
+ public void GetFilters_IfActionDescriptorParameterIsNull_ThrowsException()
+ {
+ Assert.ThrowsArgumentNull(() =>
+ {
+ _provider.GetFilters(_configuration, actionDescriptor: null);
+ }, "actionDescriptor");
+ }
+
+ [Fact]
+ public void GetFilters_GetsFilterObjectsFromActionDescriptorAndItsControllerDescriptor()
+ {
+ // Arrange
+ Mock<HttpActionDescriptor> adMock = new Mock<HttpActionDescriptor>();
+ IFilter filter1 = new Mock<IFilter>().Object;
+ IFilter filter2 = new Mock<IFilter>().Object;
+ IFilter filter3 = new Mock<IFilter>().Object;
+ adMock.Setup(ad => ad.GetFilters()).Returns(new[] { filter1, filter2 }).Verifiable();
+
+ Mock<HttpControllerDescriptor> cdMock = new Mock<HttpControllerDescriptor>();
+ cdMock.Setup(cd => cd.GetFilters()).Returns(new[] { filter3 }).Verifiable();
+
+ HttpActionDescriptor actionDescriptor = adMock.Object;
+ actionDescriptor.ControllerDescriptor = cdMock.Object;
+
+ // Act
+ var result = _provider.GetFilters(_configuration, actionDescriptor).ToList();
+
+ // Assert
+ adMock.Verify();
+ cdMock.Verify();
+ Assert.Equal(3, result.Count);
+ Assert.Equal(new FilterInfo(filter3, FilterScope.Controller), result[0], new TestFilterInfoComparer());
+ Assert.Equal(new FilterInfo(filter1, FilterScope.Action), result[1], new TestFilterInfoComparer());
+ Assert.Equal(new FilterInfo(filter2, FilterScope.Action), result[2], new TestFilterInfoComparer());
+ }
+
+ public class TestFilterInfoComparer : IEqualityComparer<FilterInfo>
+ {
+ public bool Equals(FilterInfo x, FilterInfo y)
+ {
+ return (x == null && y == null) || (Object.ReferenceEquals(x.Instance, y.Instance) && x.Scope == y.Scope);
+ }
+
+ public int GetHashCode(FilterInfo obj)
+ {
+ return obj.GetHashCode();
+ }
+ }
+
+ }
+}
diff --git a/test/System.Web.Http.Test/Filters/ActionFilterAttributeTest.cs b/test/System.Web.Http.Test/Filters/ActionFilterAttributeTest.cs
new file mode 100644
index 00000000..5acbe7ca
--- /dev/null
+++ b/test/System.Web.Http.Test/Filters/ActionFilterAttributeTest.cs
@@ -0,0 +1,334 @@
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Controllers;
+using System.Web.TestUtil;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Filters
+{
+ public class ActionFilterAttributeTest
+ {
+ [Fact]
+ public void AllowsMultiple_DefaultReturnsTrue()
+ {
+ ActionFilterAttribute actionFilter = new TestableActionFilter();
+
+ Assert.True(actionFilter.AllowMultiple);
+ }
+
+ [Fact]
+ public void ExecuteActionFilterAsync_IfContextParameterIsNull_ThrowsException()
+ {
+ var filter = new TestableActionFilter() as IActionFilter;
+ Assert.ThrowsArgumentNull(() =>
+ {
+ filter.ExecuteActionFilterAsync(actionContext: null, cancellationToken: CancellationToken.None, continuation: () => null);
+ }, "actionContext");
+ }
+
+ [Fact]
+ public void ExecuteActionFilterAsync_IfContinuationParameterIsNull_ThrowsException()
+ {
+ var filter = new TestableActionFilter() as IActionFilter;
+ Assert.ThrowsArgumentNull(() =>
+ {
+ filter.ExecuteActionFilterAsync(actionContext: ContextUtil.CreateActionContext(), cancellationToken: CancellationToken.None, continuation: null);
+ }, "continuation");
+ }
+
+ [Fact]
+ public void ExecuteActionFilterAsync_InvokesOnActionExecutingBeforeContinuation()
+ {
+ // Arrange
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ Mock<ActionFilterAttribute> filterMock = new Mock<ActionFilterAttribute>() { CallBase = true };
+ bool onActionExecutingInvoked = false;
+ filterMock.Setup(f => f.OnActionExecuting(It.IsAny<HttpActionContext>())).Callback(() =>
+ {
+ onActionExecutingInvoked = true;
+ });
+ bool? flagWhenContinuationInvoked = null;
+ Func<Task<HttpResponseMessage>> continuation = () =>
+ {
+ flagWhenContinuationInvoked = onActionExecutingInvoked;
+ return TaskHelpers.FromResult(new HttpResponseMessage());
+ };
+ var filter = (IActionFilter)filterMock.Object;
+
+ // Act
+ filter.ExecuteActionFilterAsync(context, CancellationToken.None, continuation).Wait();
+ // Assert
+ Assert.True(flagWhenContinuationInvoked.Value);
+ }
+
+ [Fact]
+ public void ExecuteActionFilterAsync_OnActionExecutingMethodGetsPassedControllerContext()
+ {
+ // Arrange
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ Mock<ActionFilterAttribute> filterMock = new Mock<ActionFilterAttribute>() { CallBase = false };
+ var filter = (IActionFilter)filterMock.Object;
+
+ // Act
+ filter.ExecuteActionFilterAsync(context, CancellationToken.None, () =>
+ {
+ return TaskHelpers.FromResult(new HttpResponseMessage());
+ }).Wait();
+
+ // Assert
+ filterMock.Verify(f => f.OnActionExecuting(context));
+ }
+
+ [Fact]
+ public void ExecuteActionFilterAsync_IfOnActionExecutingThrowsException_ReturnsFaultedTask()
+ {
+ // Arrange
+ Exception e = new Exception("{51C81EE9-F8D2-4F63-A1F8-B56052E0F2A4}");
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ Mock<ActionFilterAttribute> filterMock = new Mock<ActionFilterAttribute>();
+ filterMock.Setup(f => f.OnActionExecuting(It.IsAny<HttpActionContext>())).Throws(e);
+ var filter = (IActionFilter)filterMock.Object;
+ bool continuationCalled = false;
+
+ // Act
+ var result = filter.ExecuteActionFilterAsync(context, CancellationToken.None, () =>
+ {
+ continuationCalled = true;
+ return null;
+ });
+
+ // Assert
+ result.WaitUntilCompleted();
+ Assert.True(result.IsFaulted);
+ Assert.Same(e, result.Exception.InnerException);
+ Assert.False(continuationCalled);
+ }
+
+ [Fact]
+ public void ExecuteActionFilterAsync_IfOnActionExecutingSetsResult_ShortCircuits()
+ {
+ // Arrange
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ Mock<ActionFilterAttribute> filterMock = new Mock<ActionFilterAttribute>();
+ HttpResponseMessage response = new HttpResponseMessage();
+ filterMock.Setup(f => f.OnActionExecuting(It.IsAny<HttpActionContext>())).Callback<HttpActionContext>(c =>
+ {
+ c.Response = response;
+ });
+ bool continuationCalled = false;
+ var filter = (IActionFilter)filterMock.Object;
+
+ // Act
+ var result = filter.ExecuteActionFilterAsync(context, CancellationToken.None, () =>
+ {
+ continuationCalled = true;
+ return null;
+ }).Result;
+
+ // Assert
+ Assert.False(continuationCalled);
+ Assert.Same(response, result);
+ }
+
+ [Fact]
+ public void ExecuteActionFilterAsync_IfContinuationTaskWasCanceled_ReturnsCanceledTask()
+ {
+ // Arrange
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ Mock<ActionFilterAttribute> filterMock = new Mock<ActionFilterAttribute>();
+ var filter = (IActionFilter)filterMock.Object;
+
+ // Act
+ var result = filter.ExecuteActionFilterAsync(context, CancellationToken.None, () => TaskHelpers.Canceled<HttpResponseMessage>());
+
+ // Assert
+ result.WaitUntilCompleted();
+ Assert.True(result.IsCanceled);
+ }
+
+ [Fact]
+ public void ExecuteActionFilterAsync_IfContinuationSucceeded_InvokesOnActionExecutedAsSuccess()
+ {
+ // Arrange
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ Mock<ActionFilterAttribute> filterMock = new Mock<ActionFilterAttribute>();
+ var filter = (IActionFilter)filterMock.Object;
+ HttpResponseMessage response = new HttpResponseMessage();
+
+ // Act
+ var result = filter.ExecuteActionFilterAsync(context, CancellationToken.None, () => TaskHelpers.FromResult(response));
+
+ // Assert
+ result.WaitUntilCompleted();
+ filterMock.Verify(f => f.OnActionExecuted(It.Is<HttpActionExecutedContext>(ec =>
+ Object.ReferenceEquals(ec.Result, response)
+ && ec.Exception == null
+ && Object.ReferenceEquals(ec.ActionContext, context)
+ )));
+ }
+
+ [Fact]
+ public void ExecuteActionFilterAsync_IfContinuationFaulted_InvokesOnActionExecutedAsError()
+ {
+ // Arrange
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ Mock<ActionFilterAttribute> filterMock = new Mock<ActionFilterAttribute>();
+ var filter = (IActionFilter)filterMock.Object;
+ Exception exception = new Exception("{ABCC912C-B6D1-4C27-9059-732ABC644A0C}");
+ Func<Task<HttpResponseMessage>> continuation = () => TaskHelpers.FromError<HttpResponseMessage>(new AggregateException(exception));
+
+ // Act
+ var result = filter.ExecuteActionFilterAsync(context, CancellationToken.None, continuation);
+
+ // Assert
+ result.WaitUntilCompleted();
+ filterMock.Verify(f => f.OnActionExecuted(It.Is<HttpActionExecutedContext>(ec =>
+ Object.ReferenceEquals(ec.Exception, exception)
+ && ec.Result == null
+ && Object.ReferenceEquals(ec.ActionContext, context)
+ )));
+ }
+
+ [Fact]
+ public void ExecuteActionFilterAsync_IfOnActionExecutedDoesNotHandleExceptionFromContinuation_ReturnsFaultedTask()
+ {
+ // Arrange
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ Mock<ActionFilterAttribute> filterMock = new Mock<ActionFilterAttribute>();
+ var filter = (IActionFilter)filterMock.Object;
+ Exception exception = new Exception("{1EC330A2-33D0-4892-9335-2D833849D54E}");
+ filterMock.Setup(f => f.OnActionExecuted(It.IsAny<HttpActionExecutedContext>())).Callback<HttpActionExecutedContext>(ec =>
+ {
+ ec.Result = null;
+ });
+
+ // Act
+ var result = filter.ExecuteActionFilterAsync(context, CancellationToken.None, () => TaskHelpers.FromError<HttpResponseMessage>(exception));
+
+ // Assert
+ result.WaitUntilCompleted();
+ Assert.True(result.IsFaulted);
+ Assert.Same(exception, result.Exception.InnerException);
+ }
+
+ [Fact]
+ public void ExecuteActionFilterAsync_IfOnActionExecutedDoesHandleExceptionFromContinuation_ReturnsSuccessfulTask()
+ {
+ // Arrange
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ Mock<ActionFilterAttribute> filterMock = new Mock<ActionFilterAttribute>();
+ var filter = (IActionFilter)filterMock.Object;
+ HttpResponseMessage newResponse = new HttpResponseMessage();
+ filterMock.Setup(f => f.OnActionExecuted(It.IsAny<HttpActionExecutedContext>())).Callback<HttpActionExecutedContext>(ec =>
+ {
+ ec.Result = newResponse;
+ });
+
+ // Act
+ var result = filter.ExecuteActionFilterAsync(context, CancellationToken.None, () => TaskHelpers.FromError<HttpResponseMessage>(new Exception("{ED525C8E-7165-4207-B3F6-4AB095739017}")));
+
+ // Assert
+ result.WaitUntilCompleted();
+ Assert.True(result.IsCompleted);
+ Assert.Same(newResponse, result.Result);
+ }
+
+ [Fact]
+ public void ExecuteActionFilterAsync_IfOnActionExecutedThrowsException_ReturnsFaultedTask()
+ {
+ // Arrange
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ Mock<ActionFilterAttribute> filterMock = new Mock<ActionFilterAttribute>();
+ var filter = (IActionFilter)filterMock.Object;
+ Exception exception = new Exception("{AC32AD02-36A7-45E5-8955-76A4E3B461C6}");
+ filterMock.Setup(f => f.OnActionExecuted(It.IsAny<HttpActionExecutedContext>())).Callback<HttpActionExecutedContext>(ec =>
+ {
+ throw exception;
+ });
+
+ // Act
+ var result = filter.ExecuteActionFilterAsync(context, CancellationToken.None, () => TaskHelpers.FromResult(new HttpResponseMessage()));
+
+ // Assert
+ result.WaitUntilCompleted();
+ Assert.True(result.IsFaulted);
+ Assert.Same(exception, result.Exception.InnerException);
+ }
+
+ [Fact]
+ public void ExecuteActionFilterAsync_IfOnActionExecutedSetsResult_ReturnsNewResult()
+ {
+ // Arrange
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ Mock<ActionFilterAttribute> filterMock = new Mock<ActionFilterAttribute>();
+ var filter = (IActionFilter)filterMock.Object;
+ HttpResponseMessage newResponse = new HttpResponseMessage();
+ filterMock.Setup(f => f.OnActionExecuted(It.IsAny<HttpActionExecutedContext>())).Callback<HttpActionExecutedContext>(ec =>
+ {
+ ec.Result = newResponse;
+ });
+
+ // Act
+ var result = filter.ExecuteActionFilterAsync(context, CancellationToken.None, () => TaskHelpers.FromResult(new HttpResponseMessage()));
+
+ // Assert
+ result.WaitUntilCompleted();
+ Assert.True(result.IsCompleted);
+ Assert.Same(newResponse, result.Result);
+ }
+
+ [Fact]
+ public void ExecuteActionFilterAsync_IfOnActionExecutedDoesNotChangeResult_ReturnsSameResult()
+ {
+ // Arrange
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ Mock<ActionFilterAttribute> filterMock = new Mock<ActionFilterAttribute>();
+ var filter = (IActionFilter)filterMock.Object;
+ HttpResponseMessage response = new HttpResponseMessage();
+ filterMock.Setup(f => f.OnActionExecuted(It.IsAny<HttpActionExecutedContext>())).Callback<HttpActionExecutedContext>(ec =>
+ {
+ ec.Result = ec.Result;
+ });
+
+ // Act
+ var result = filter.ExecuteActionFilterAsync(context, CancellationToken.None, () => TaskHelpers.FromResult(response));
+
+ // Assert
+ result.WaitUntilCompleted();
+ Assert.True(result.IsCompleted);
+ Assert.Same(response, result.Result);
+ }
+
+ [Fact]
+ public void ExecuteActionFilterAsync_IfOnActionExecutedRemovesSuccessfulResult_ReturnsFaultedTask()
+ {
+ // Arrange
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ Mock<ActionFilterAttribute> filterMock = new Mock<ActionFilterAttribute>();
+ var filter = (IActionFilter)filterMock.Object;
+ HttpResponseMessage response = new HttpResponseMessage();
+ filterMock.Setup(f => f.OnActionExecuted(It.IsAny<HttpActionExecutedContext>())).Callback<HttpActionExecutedContext>(ec =>
+ {
+ ec.Result = null;
+ });
+
+ // Act
+ var result = filter.ExecuteActionFilterAsync(context, CancellationToken.None, () => TaskHelpers.FromResult(response));
+
+ // Assert
+ result.WaitUntilCompleted();
+ Assert.Equal(TaskStatus.Faulted, result.Status);
+ Assert.IsException<InvalidOperationException>(
+ exception: result.Exception,
+ expectedMessage: "After calling ActionFilterAttributeProxy.OnActionExecuted, the HttpActionExecutedContext properties Result and Exception were both null. At least one of these values must be non-null. To provide a new response, please set the Result object; to indicate an error, please throw an exception."
+ );
+ }
+
+ public class TestableActionFilter : ActionFilterAttribute
+ {
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Filters/AuthorizationFilterAttributeTest.cs b/test/System.Web.Http.Test/Filters/AuthorizationFilterAttributeTest.cs
new file mode 100644
index 00000000..d49c4e74
--- /dev/null
+++ b/test/System.Web.Http.Test/Filters/AuthorizationFilterAttributeTest.cs
@@ -0,0 +1,189 @@
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Controllers;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Filters
+{
+ public class AuthorizationFilterAttributeTest
+ {
+ [Fact]
+ public void AllowsMultiple_DefaultReturnsTrue()
+ {
+ AuthorizationFilterAttribute actionFilter = new TestableAuthorizationFilter();
+
+ Assert.True(actionFilter.AllowMultiple);
+ }
+
+ [Fact]
+ public void ExecuteAuthorizationFilterAsync_IfContextParameterIsNull_ThrowsException()
+ {
+ var filter = new TestableAuthorizationFilter() as IAuthorizationFilter;
+ Assert.ThrowsArgumentNull(() =>
+ {
+ filter.ExecuteAuthorizationFilterAsync(actionContext: null, cancellationToken: CancellationToken.None, continuation: () => null);
+ }, "actionContext");
+ }
+
+ [Fact]
+ public void ExecuteAuthorizationFilterAsync_IfContinuationParameterIsNull_ThrowsException()
+ {
+ var filter = new TestableAuthorizationFilter() as IAuthorizationFilter;
+ Assert.ThrowsArgumentNull(() =>
+ {
+ filter.ExecuteAuthorizationFilterAsync(actionContext: ContextUtil.CreateActionContext(), cancellationToken: CancellationToken.None, continuation: null);
+ }, "continuation");
+ }
+
+ [Fact]
+ public void ExecuteAuthorizationFilterAsync_InvokesOnActionExecutingBeforeContinuation()
+ {
+ // Arrange
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ Mock<AuthorizationFilterAttribute> filterMock = new Mock<AuthorizationFilterAttribute>() { CallBase = true };
+ bool onActionExecutingInvoked = false;
+ filterMock.Setup(f => f.OnAuthorization(It.IsAny<HttpActionContext>())).Callback(() =>
+ {
+ onActionExecutingInvoked = true;
+ });
+ bool? flagWhenContinuationInvoked = null;
+ Func<Task<HttpResponseMessage>> continuation = () =>
+ {
+ flagWhenContinuationInvoked = onActionExecutingInvoked;
+ return TaskHelpers.FromResult(new HttpResponseMessage());
+ };
+ var filter = (IAuthorizationFilter)filterMock.Object;
+
+ // Act
+ filter.ExecuteAuthorizationFilterAsync(context, CancellationToken.None, continuation).Wait();
+
+ // Assert
+ Assert.True(flagWhenContinuationInvoked.Value);
+ }
+
+ public void ExecuteAuthorizationFilterAsync_IfOnActionExecutingSetsResult_ShortCircuits()
+ {
+ // Arrange
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ Mock<AuthorizationFilterAttribute> filterMock = new Mock<AuthorizationFilterAttribute>();
+ HttpResponseMessage response = new HttpResponseMessage();
+ filterMock.Setup(f => f.OnAuthorization(It.IsAny<HttpActionContext>())).Callback<HttpActionContext>(c =>
+ {
+ c.Response = response;
+ });
+ bool continuationCalled = false;
+ var filter = (IAuthorizationFilter)filterMock.Object;
+
+ // Act
+ var result = filter.ExecuteAuthorizationFilterAsync(context, CancellationToken.None, () =>
+ {
+ continuationCalled = true;
+ return null;
+ }).Result;
+
+ // Assert
+ Assert.False(continuationCalled);
+ Assert.Same(response, result);
+ }
+
+ [Fact]
+ public void ExecuteAuthorizationFilterAsync_IfOnActionExecutingThrowsException_ReturnsFaultedTask()
+ {
+ // Arrange
+ Exception e = new Exception();
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ Mock<AuthorizationFilterAttribute> filterMock = new Mock<AuthorizationFilterAttribute>();
+ filterMock.Setup(f => f.OnAuthorization(It.IsAny<HttpActionContext>())).Throws(e);
+ var filter = (IAuthorizationFilter)filterMock.Object;
+ bool continuationCalled = false;
+
+ // Act
+ var result = filter.ExecuteAuthorizationFilterAsync(context, CancellationToken.None, () =>
+ {
+ continuationCalled = true;
+ return null;
+ });
+
+ // Assert
+ result.WaitUntilCompleted();
+ Assert.True(result.IsFaulted);
+ Assert.Same(e, result.Exception.InnerException);
+ Assert.False(continuationCalled);
+ }
+
+ [Fact]
+ public void ExecuteAuthorizationFilterAsync_OnActionExecutingMethodGetsPassedControllerContext()
+ {
+ // Arrange
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ Mock<AuthorizationFilterAttribute> filterMock = new Mock<AuthorizationFilterAttribute>() { CallBase = false };
+ var filter = (IAuthorizationFilter)filterMock.Object;
+
+ // Act
+ filter.ExecuteAuthorizationFilterAsync(context, CancellationToken.None, () =>
+ {
+ return TaskHelpers.FromResult(new HttpResponseMessage());
+ }).Wait();
+
+ // Assert
+ filterMock.Verify(f => f.OnAuthorization(context));
+ }
+
+ [Fact]
+ public void ExecuteAuthorizationFilterAsync_IfContinuationTaskWasCanceled_ReturnsCanceledTask()
+ {
+ // Arrange
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ Mock<AuthorizationFilterAttribute> filterMock = new Mock<AuthorizationFilterAttribute>();
+ var filter = (IAuthorizationFilter)filterMock.Object;
+
+ // Act
+ var result = filter.ExecuteAuthorizationFilterAsync(context, CancellationToken.None, () => TaskHelpers.Canceled<HttpResponseMessage>());
+
+ // Assert
+ result.WaitUntilCompleted();
+ Assert.True(result.IsCanceled);
+ }
+
+ [Fact]
+ public void ExecuteAuthorizationFilterAsync_IfContinuationSucceeded_ReturnsSuccessTask()
+ {
+ // Arrange
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ Mock<AuthorizationFilterAttribute> filterMock = new Mock<AuthorizationFilterAttribute>();
+ var filter = (IAuthorizationFilter)filterMock.Object;
+ HttpResponseMessage response = new HttpResponseMessage();
+
+ // Act
+ var result = filter.ExecuteAuthorizationFilterAsync(context, CancellationToken.None, () => TaskHelpers.FromResult(response));
+
+ // Assert
+ result.WaitUntilCompleted();
+ Assert.Same(response, result.Result);
+ }
+
+ [Fact]
+ public void ExecuteAuthorizationFilterAsync_IfContinuationFaulted_ReturnsFaultedTask()
+ {
+ // Arrange
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ Mock<AuthorizationFilterAttribute> filterMock = new Mock<AuthorizationFilterAttribute>();
+ var filter = (IAuthorizationFilter)filterMock.Object;
+ Exception exception = new Exception();
+
+ // Act
+ var result = filter.ExecuteAuthorizationFilterAsync(context, CancellationToken.None, () => TaskHelpers.FromError<HttpResponseMessage>(exception));
+
+ // Assert
+ result.WaitUntilCompleted();
+ Assert.Same(exception, result.Exception.InnerException);
+ }
+ }
+
+ public class TestableAuthorizationFilter : AuthorizationFilterAttribute
+ {
+ }
+}
diff --git a/test/System.Web.Http.Test/Filters/ConfigurationFilterProviderTest.cs b/test/System.Web.Http.Test/Filters/ConfigurationFilterProviderTest.cs
new file mode 100644
index 00000000..3f40af29
--- /dev/null
+++ b/test/System.Web.Http.Test/Filters/ConfigurationFilterProviderTest.cs
@@ -0,0 +1,34 @@
+using System.Linq;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Filters
+{
+ public class ConfigurationFilterProviderTest
+ {
+ private readonly ConfigurationFilterProvider provider = new ConfigurationFilterProvider();
+
+ [Fact]
+ public void GetFilters_IfContextParameterIsNull_ThrowsException()
+ {
+ Assert.ThrowsArgumentNull(() =>
+ {
+ provider.GetFilters(configuration: null, actionDescriptor: null);
+ }, "configuration");
+ }
+
+ [Fact]
+ public void GetFilters_ReturnsFiltersFromConfiguration()
+ {
+ var config = new HttpConfiguration();
+ IFilter filter1 = new Mock<IFilter>().Object;
+ config.Filters.Add(filter1);
+
+ var result = provider.GetFilters(config, actionDescriptor: null);
+
+ Assert.True(result.All(f => f.Scope == FilterScope.Global));
+ Assert.Same(filter1, result.ToArray()[0].Instance);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Filters/EnumerableEvaluatorFilterProviderTest.cs b/test/System.Web.Http.Test/Filters/EnumerableEvaluatorFilterProviderTest.cs
new file mode 100644
index 00000000..e0997ba5
--- /dev/null
+++ b/test/System.Web.Http.Test/Filters/EnumerableEvaluatorFilterProviderTest.cs
@@ -0,0 +1,82 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Threading.Tasks;
+using System.Web.Http.Controllers;
+using Moq;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Filters
+{
+ public class EnumerableEvaluatorFilterProviderTest
+ {
+ private HttpConfiguration _configuration = new HttpConfiguration();
+ private Mock<HttpActionDescriptor> _actionDescriptorMock = new Mock<HttpActionDescriptor>();
+ private EnumerableEvaluatorFilterProvider _filterProvider = new EnumerableEvaluatorFilterProvider();
+
+ [Fact]
+ public void GetFilters_WhenConfigurationParameterIsNull_ThrowsException()
+ {
+ Assert.ThrowsArgumentNull(() =>
+ {
+ _filterProvider.GetFilters(configuration: null, actionDescriptor: _actionDescriptorMock.Object);
+ }, "configuration");
+ }
+
+ [Fact]
+ public void GetFilters_WhenActionDescriptorParameterIsNull_ThrowsException()
+ {
+ Assert.ThrowsArgumentNull(() =>
+ {
+ _filterProvider.GetFilters(_configuration, actionDescriptor: null);
+ }, "actionDescriptor");
+ }
+
+ [Theory]
+ [InlineData(typeof(IEnumerable<string>))]
+ [InlineData(typeof(IEnumerable<object>))]
+ [InlineData(typeof(IQueryable<string>))]
+ //[InlineData(typeof(HttpResponseMessage))] // static signature problems
+ //[InlineData(typeof(Task<HttpResponseMessage>))] // static signature problems
+ [InlineData(typeof(ObjectContent<IEnumerable<string>>))]
+ [InlineData(typeof(Task<ObjectContent<IEnumerable<string>>>))]
+ public void GetFilters_IfActionResultTypeIsSupported_ReturnsFilterInstance(Type actionReturnType)
+ {
+ // Arrange
+ _actionDescriptorMock.Setup(ad => ad.ReturnType).Returns(actionReturnType);
+
+ // Act
+ IEnumerable<FilterInfo> result = _filterProvider.GetFilters(_configuration, _actionDescriptorMock.Object);
+
+ // Assert
+ FilterInfo filter = result.Single();
+ Assert.NotNull(filter);
+ Assert.IsType<EnumerableEvaluatorFilter>(filter.Instance);
+ Assert.Equal(FilterScope.First, filter.Scope);
+ }
+
+ [Theory]
+ [InlineData(typeof(Object))]
+ [InlineData(typeof(String))]
+ [InlineData(typeof(Int32))]
+ [InlineData(typeof(object[]))]
+ [InlineData(typeof(List<string>))]
+ [InlineData(typeof(IList<string>))]
+ [InlineData(typeof(IEnumerable))]
+ [InlineData(typeof(IQueryable))]
+ public void GetFilters_IfActionResultTypeIsNotSupported_ReturnsEmptyResult(Type actionReturnType)
+ {
+ // Arrange
+ _actionDescriptorMock.Setup(ad => ad.ReturnType).Returns(actionReturnType);
+
+ // Act
+ IEnumerable<FilterInfo> result = _filterProvider.GetFilters(_configuration, _actionDescriptorMock.Object);
+
+ // Assert
+ Assert.Empty(result);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Filters/EnumerableEvaluatorFilterTest.cs b/test/System.Web.Http.Test/Filters/EnumerableEvaluatorFilterTest.cs
new file mode 100644
index 00000000..8f056ceb
--- /dev/null
+++ b/test/System.Web.Http.Test/Filters/EnumerableEvaluatorFilterTest.cs
@@ -0,0 +1,191 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Threading.Tasks;
+using System.Web.Http.Controllers;
+using Microsoft.TestCommon;
+using Moq;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+using System.Net.Http.Formatting;
+
+namespace System.Web.Http.Filters
+{
+ public class EnumerableEvaluatorFilterTest
+ {
+ private EnumerableEvaluatorFilter _filter = new EnumerableEvaluatorFilter();
+ private HttpActionExecutedContext _actionExecutedContext;
+ private Mock<HttpActionDescriptor> _actionDescriptorMock;
+
+ public EnumerableEvaluatorFilterTest()
+ {
+ _actionDescriptorMock = new Mock<HttpActionDescriptor>();
+ var actionContext = new HttpActionContext { ActionDescriptor = _actionDescriptorMock.Object };
+ _actionExecutedContext = new HttpActionExecutedContext(actionContext, exception: null);
+ }
+
+ [Fact]
+ public void AllowMultiple_ReturnsFalse()
+ {
+ Assert.False(_filter.AllowMultiple);
+ }
+
+ [Fact]
+ public void Instance_IsSingletonProperty()
+ {
+ var first = EnumerableEvaluatorFilter.Instance;
+ Assert.NotNull(first);
+ var second = EnumerableEvaluatorFilter.Instance;
+ Assert.Same(first, second);
+ }
+
+ [Fact]
+ public void OnActionExecuted_IfContextParameterIsNull_ThrowsException()
+ {
+ Assert.ThrowsArgumentNull(() =>
+ {
+ _filter.OnActionExecuted(actionExecutedContext: null);
+ }, "actionExecutedContext");
+ }
+
+ [Fact]
+ public void OnActionExecuted_IfActionContentTypeIsNotIEnumerable_ButResponseContentIsAnIEnumerable_DoesNothing()
+ {
+ // Arrange
+ _actionDescriptorMock.Setup(ad => ad.ReturnType).Returns(typeof(object));
+ _actionExecutedContext.Result = new HttpResponseMessage() { Content = new ObjectContent<IEnumerable<string>>(new List<string>(), new JsonMediaTypeFormatter()) };
+ var content = _actionExecutedContext.Result.Content;
+
+ // Act
+ _filter.OnActionExecuted(_actionExecutedContext);
+
+ // Assert
+ Assert.Same(content, _actionExecutedContext.Result.Content);
+ }
+
+ [Fact]
+ public void OnActionExecuted_IfActionContentTypeIsIEnumerable_ButResponseContentIsNull_DoesNothing()
+ {
+ // Arrange
+ _actionDescriptorMock.Setup(ad => ad.ReturnType).Returns(typeof(IEnumerable<string>));
+ _actionExecutedContext.Result = new HttpResponseMessage() { Content = new ObjectContent<IEnumerable<string>>(null, new JsonMediaTypeFormatter()) };
+ var content = _actionExecutedContext.Result.Content;
+
+ // Act
+ _filter.OnActionExecuted(_actionExecutedContext);
+
+ // Assert
+ Assert.Same(content, _actionExecutedContext.Result.Content);
+ }
+
+ [Theory]
+ [PropertyData("NotSupportedTypesTestData")]
+ public void OnActionExecuted_IfActionContentTypeIsNotIEnumerable_DoesNothing(Type actionReturnType, object input)
+ {
+ // Arrange
+ _actionDescriptorMock.Setup(ad => ad.ReturnType).Returns(actionReturnType);
+ _actionExecutedContext.Result = new HttpResponseMessage() { Content = new ObjectContent<object>(input, new JsonMediaTypeFormatter()) };
+ var content = _actionExecutedContext.Result.Content;
+
+ // Act
+ _filter.OnActionExecuted(_actionExecutedContext);
+
+ // Assert
+ object output;
+ Assert.True(_actionExecutedContext.Result.TryGetObjectValue(out output));
+ Assert.Same(input, output);
+ Assert.Same(content, _actionExecutedContext.Result.Content);
+ }
+
+ public static TheoryDataSet<Type, object> NotSupportedTypesTestData
+ {
+ get
+ {
+ return new TheoryDataSet<Type, object>
+ {
+ {typeof(int), 42},
+ {typeof(object), new object()},
+ {typeof(IEnumerable), new ArrayList()},
+ {typeof(IQueryable), new List<string>().AsQueryable()},
+ {typeof(string), "some value"},
+ {typeof(byte[]), new byte[3]},
+ {typeof(string[]), new string[3]},
+ {typeof(List<string>), new List<string>()},
+ };
+ }
+ }
+
+ [Theory]
+ [InlineData(typeof(IEnumerable<string>))]
+ //[InlineData(typeof(HttpResponseMessage))] // static signature problems
+ // [InlineData(typeof(Task<HttpResponseMessage>))] // static signature problems
+ [InlineData(typeof(ObjectContent<IEnumerable<string>>))]
+ [InlineData(typeof(Task<ObjectContent<IEnumerable<string>>>))]
+ public void OnActionExecuted_IfActionContentTypeIsIEnumerable_AndResponseContentTypeMatches_CopiesContentToNewList(Type actionReturnType)
+ {
+ // Arrange
+ _actionDescriptorMock.Setup(ad => ad.ReturnType).Returns(actionReturnType);
+ List<string> input = Enumerable.Range(0, 3).Select(i => "Item " + i).ToList();
+ _actionExecutedContext.Result = new HttpResponseMessage() { Content = new ObjectContent<List<string>>(input, new JsonMediaTypeFormatter()) };
+
+ // Act
+ _filter.OnActionExecuted(_actionExecutedContext);
+
+ // Assert
+ object output;
+ Assert.True(_actionExecutedContext.Result.TryGetObjectValue(out output));
+ Assert.NotSame(input, output);
+ Assert.IsType<List<string>>(output);
+ }
+
+ [Fact]
+ public void OnActionExecuted_IfActionContentTypeIsIEnumerable_ButResponseContentTypeIsDifferentIEnumerable_DoesNothing()
+ {
+ // Arrange
+ _actionDescriptorMock.Setup(ad => ad.ReturnType).Returns(typeof(IEnumerable<string>));
+ List<int> input = Enumerable.Range(0, 3).ToList();
+ _actionExecutedContext.Result = new HttpResponseMessage() { Content = new ObjectContent<List<int>>(input, new JsonMediaTypeFormatter()) };
+
+ // Act
+ _filter.OnActionExecuted(_actionExecutedContext);
+
+ // Assert
+ List<int> output;
+ Assert.True(_actionExecutedContext.Result.TryGetObjectValue(out output));
+ Assert.Same(input, output);
+ }
+
+ [Fact]
+ public void OnActionExecuted_IfActionContentTypeIsIQueryable_CopiesContentToNewResult()
+ {
+ // Arrange
+ _actionDescriptorMock.Setup(ad => ad.ReturnType).Returns(typeof(IQueryable<string>));
+ IQueryable<string> input = Enumerable.Range(0, 3).Select(i => "Item " + i).AsQueryable();
+ _actionExecutedContext.Result = new HttpResponseMessage() { Content = new ObjectContent<IQueryable<string>>(input, new JsonMediaTypeFormatter()) };
+
+ // Act
+ _filter.OnActionExecuted(_actionExecutedContext);
+
+ // Assert
+ object output;
+ Assert.True(_actionExecutedContext.Result.TryGetObjectValue(out output));
+ Assert.NotSame(input, output);
+ }
+
+ [Fact]
+ public void OnActionExecuted_IfResponseIsNull_DoesNothing()
+ {
+ // Arrange
+ _actionDescriptorMock.Setup(ad => ad.ReturnType).Returns(typeof(IEnumerable<string>));
+ _actionExecutedContext.Result = null;
+
+ // Act
+ _filter.OnActionExecuted(_actionExecutedContext);
+
+ // Assert
+ Assert.Null(_actionExecutedContext.Result);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Filters/ExceptionFilterAttributeTest.cs b/test/System.Web.Http.Test/Filters/ExceptionFilterAttributeTest.cs
new file mode 100644
index 00000000..62371c13
--- /dev/null
+++ b/test/System.Web.Http.Test/Filters/ExceptionFilterAttributeTest.cs
@@ -0,0 +1,82 @@
+using System.Threading;
+using System.Threading.Tasks;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Filters
+{
+ public class ExceptionFilterAttributeTest
+ {
+ private readonly HttpActionExecutedContext _context = new HttpActionExecutedContext(ContextUtil.CreateActionContext(), new Exception());
+
+ [Fact]
+ public void AllowsMultiple_DefaultReturnsTrue()
+ {
+ ExceptionFilterAttribute actionFilter = new TestableExceptionFilter();
+
+ Assert.True(actionFilter.AllowMultiple);
+ }
+
+ [Fact]
+ public void ExecuteExceptionFilterAsync_IfContextParameterIsNull_ThrowsException()
+ {
+ IExceptionFilter filter = new Mock<ExceptionFilterAttribute>().Object;
+
+ Assert.ThrowsArgumentNull(() =>
+ {
+ filter.ExecuteExceptionFilterAsync(actionExecutedContext: null, cancellationToken: CancellationToken.None);
+ }, "actionExecutedContext");
+ }
+
+ [Fact]
+ public void ExecuteExceptionFilterAsync_IfOnExceptionThrowsException_RethrowsTheSameException()
+ {
+ // Arrange
+ var mockFilter = new Mock<ExceptionFilterAttribute>();
+ Exception exception = new Exception();
+ mockFilter.Setup(f => f.OnException(_context)).Throws(exception);
+ IExceptionFilter filter = mockFilter.Object;
+
+ // Act & Assert
+ var thrownException = Assert.Throws<Exception>(() =>
+ {
+ filter.ExecuteExceptionFilterAsync(_context, CancellationToken.None);
+ });
+ Assert.Same(exception, thrownException);
+ }
+
+ [Fact]
+ public void ExecuteExceptionFilterAsync_InvokesOnExceptionMethod()
+ {
+ // Arrange
+ var mockFilter = new Mock<ExceptionFilterAttribute>();
+ IExceptionFilter filter = mockFilter.Object;
+
+ // Act
+ filter.ExecuteExceptionFilterAsync(_context, CancellationToken.None);
+
+ // Assert
+ mockFilter.Verify(f => f.OnException(_context));
+ }
+
+ [Fact]
+ public void ExecuteExceptionFilterAsync_ReturnsCompletedTask()
+ {
+ // Arrange
+ var mockFilter = new Mock<ExceptionFilterAttribute>();
+ IExceptionFilter filter = mockFilter.Object;
+
+ // Act
+ var result = filter.ExecuteExceptionFilterAsync(_context, CancellationToken.None);
+
+ // Assert
+ Assert.True(result.Status == TaskStatus.RanToCompletion);
+ }
+
+ public sealed class TestableExceptionFilter : ExceptionFilterAttribute
+ {
+
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Filters/FilterAttributeTest.cs b/test/System.Web.Http.Test/Filters/FilterAttributeTest.cs
new file mode 100644
index 00000000..909b3478
--- /dev/null
+++ b/test/System.Web.Http.Test/Filters/FilterAttributeTest.cs
@@ -0,0 +1,34 @@
+using Xunit;
+using Xunit.Extensions;
+
+namespace System.Web.Http.Filters
+{
+ [CLSCompliant(false)]
+ public class FilterAttributeTest
+ {
+ [Theory]
+ [InlineData(typeof(UniqueFilterAttribute), false)]
+ [InlineData(typeof(MultiFilterAttribute), true)]
+ [InlineData(typeof(DefaultFilterAttribute), true)]
+ public void AllowMultiple(Type filterType, bool expectedAllowsMultiple)
+ {
+ var attribute = (FilterAttribute)Activator.CreateInstance(filterType);
+
+ Assert.Equal(expectedAllowsMultiple, attribute.AllowMultiple);
+ }
+
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
+ public sealed class UniqueFilterAttribute : FilterAttribute
+ {
+ }
+
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
+ public sealed class MultiFilterAttribute : FilterAttribute
+ {
+ }
+
+ public sealed class DefaultFilterAttribute : FilterAttribute
+ {
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Filters/FilterInfoComparerTest.cs b/test/System.Web.Http.Test/Filters/FilterInfoComparerTest.cs
new file mode 100644
index 00000000..53828ef3
--- /dev/null
+++ b/test/System.Web.Http.Test/Filters/FilterInfoComparerTest.cs
@@ -0,0 +1,37 @@
+using System.Collections.Generic;
+using Microsoft.TestCommon;
+using Moq;
+using Xunit;
+using Xunit.Extensions;
+
+namespace System.Web.Http.Filters
+{
+ public class FilterInfoComparerTest
+ {
+ [Theory]
+ [PropertyData("CompareTestData")]
+ public void Compare(FilterInfo x, FilterInfo y, int expectedSign)
+ {
+ int result = FilterInfoComparer.Instance.Compare(x, y);
+
+ Assert.Equal(expectedSign, Math.Sign(result));
+ }
+
+ public static IEnumerable<object[]> CompareTestData
+ {
+ get
+ {
+ IFilter f = new Mock<IFilter>().Object;
+ return new TheoryDataSet<FilterInfo, FilterInfo, int>()
+ {
+ { null, null, 0 },
+ { new FilterInfo(f, FilterScope.Action), null, 1 },
+ { null, new FilterInfo(f, FilterScope.Action), -1 },
+ { new FilterInfo(f, FilterScope.Action), new FilterInfo(f, FilterScope.Action), 0 },
+ { new FilterInfo(f, FilterScope.First), new FilterInfo(f, FilterScope.Action), -1 },
+ { new FilterInfo(f, FilterScope.Action), new FilterInfo(f, FilterScope.First), 1 },
+ };
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Filters/FilterInfoTest.cs b/test/System.Web.Http.Test/Filters/FilterInfoTest.cs
new file mode 100644
index 00000000..d364aa5c
--- /dev/null
+++ b/test/System.Web.Http.Test/Filters/FilterInfoTest.cs
@@ -0,0 +1,29 @@
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Filters
+{
+ public class FilterInfoTest
+ {
+ [Fact]
+ public void Constructor()
+ {
+ var filterInstance = new Mock<IFilter>().Object;
+
+ FilterInfo filter = new FilterInfo(filterInstance, FilterScope.Controller);
+
+ Assert.Equal(FilterScope.Controller, filter.Scope);
+ Assert.Same(filterInstance, filter.Instance);
+ }
+
+ [Fact]
+ public void Constructor_IfInstanceParameterIsNull_ThrowsException()
+ {
+ Assert.ThrowsArgumentNull(() =>
+ {
+ new FilterInfo(instance: null, scope: FilterScope.Controller);
+ }, "instance");
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Filters/HttpActionExecutedContextTest.cs b/test/System.Web.Http.Test/Filters/HttpActionExecutedContextTest.cs
new file mode 100644
index 00000000..a59a9440
--- /dev/null
+++ b/test/System.Web.Http.Test/Filters/HttpActionExecutedContextTest.cs
@@ -0,0 +1,88 @@
+using System.Net.Http;
+using System.Web.Http.Controllers;
+using System.Web.TestUtil;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Filters
+{
+ public class HttpActionExecutedContextTest
+ {
+ [Fact]
+ public void Default_Constructor()
+ {
+ HttpActionExecutedContext actionExecutedContext = new HttpActionExecutedContext();
+
+ Assert.Null(actionExecutedContext.ActionContext);
+ Assert.Null(actionExecutedContext.Exception);
+ Assert.Null(actionExecutedContext.Request);
+ Assert.Null(actionExecutedContext.Result);
+ }
+
+ [Fact]
+ public void Parameter_Constructor()
+ {
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ Exception exception = new Exception();
+
+ var actionContext = new HttpActionExecutedContext(context, exception);
+
+ Assert.Same(context, actionContext.ActionContext);
+ Assert.Same(exception, actionContext.Exception);
+ Assert.Same(context.ControllerContext.Request, actionContext.Request);
+ Assert.Null(actionContext.Result);
+ }
+
+ [Fact]
+ public void Constructor_AllowsNullExceptionParameter()
+ {
+ HttpActionContext context = ContextUtil.CreateActionContext();
+
+ var actionContext = new HttpActionExecutedContext(context, exception: null);
+
+ Assert.Null(actionContext.Exception);
+ }
+
+ [Fact]
+ public void Constructor_IfContextParameterIsNull_ThrowsException()
+ {
+ Assert.ThrowsArgumentNull(() =>
+ {
+ new HttpActionExecutedContext(actionContext: null, exception: null);
+ }, "actionContext");
+ }
+
+ [Fact]
+ public void ActionContext_Property()
+ {
+ Assert.Reflection.Property<HttpActionExecutedContext, HttpActionContext>(
+ instance: new HttpActionExecutedContext(),
+ propertyGetter: aec => aec.ActionContext,
+ expectedDefaultValue: null,
+ allowNull: false,
+ roundTripTestValue: ContextUtil.CreateActionContext());
+ }
+
+ [Fact]
+ public void Exception_Property()
+ {
+ Assert.Reflection.Property<HttpActionExecutedContext, Exception>(
+ instance: new HttpActionExecutedContext(),
+ propertyGetter: aec => aec.Exception,
+ expectedDefaultValue: null,
+ allowNull: true,
+ roundTripTestValue: new ArgumentException());
+ }
+
+ [Fact]
+ public void Result_Property()
+ {
+ Assert.Reflection.Property<HttpActionExecutedContext, HttpResponseMessage>(
+ instance: new HttpActionExecutedContext(),
+ propertyGetter: aec => aec.Result,
+ expectedDefaultValue: null,
+ allowNull: true,
+ roundTripTestValue: new HttpResponseMessage());
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Filters/HttpFilterCollectionTest.cs b/test/System.Web.Http.Test/Filters/HttpFilterCollectionTest.cs
new file mode 100644
index 00000000..1fa89d78
--- /dev/null
+++ b/test/System.Web.Http.Test/Filters/HttpFilterCollectionTest.cs
@@ -0,0 +1,94 @@
+using System.Linq;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Filters
+{
+ public class HttpFilterCollectionTest
+ {
+ private readonly IFilter _filter = new Mock<IFilter>().Object;
+ private readonly HttpFilterCollection _collection = new HttpFilterCollection();
+
+ [Fact]
+ public void Add_WhenFilterParameterIsNull_Throws()
+ {
+ Assert.ThrowsArgumentNull(() => _collection.Add(filter: null), "filter");
+ }
+
+ [Fact]
+ public void Add_AddsFilterWithGlobalScope()
+ {
+ _collection.Add(_filter);
+
+ Assert.Same(_filter, _collection.First().Instance);
+ Assert.Equal(FilterScope.Global, _collection.First().Scope);
+ }
+
+ [Fact]
+ public void Add_AllowsAddingSameInstanceMultipleTimes()
+ {
+ _collection.Add(_filter);
+ _collection.Add(_filter);
+
+ Assert.Equal(2, _collection.Count);
+ }
+
+ [Fact]
+ public void Clear_EmptiesCollection()
+ {
+ _collection.Add(_filter);
+
+ _collection.Clear();
+
+ Assert.Equal(0, _collection.Count);
+ }
+
+ [Fact]
+ public void Contains_WhenFilterNotInCollection_ReturnsFalse()
+ {
+ Assert.False(_collection.Contains(_filter));
+ }
+
+ [Fact]
+ public void Contains_WhenFilterInCollection_ReturnsTrue()
+ {
+ _collection.Add(_filter);
+
+ Assert.True(_collection.Contains(_filter));
+ }
+
+ [Fact]
+ public void Count_WhenCollectionIsEmpty_ReturnsZero()
+ {
+ Assert.Equal(0, _collection.Count);
+ }
+
+ [Fact]
+ public void Count_WhenItemAddedToCollection_ReturnsOne()
+ {
+ _collection.Add(_filter);
+
+ Assert.Equal(1, _collection.Count);
+ }
+
+ [Fact]
+ public void Remove_WhenCollectionDoesNotHaveFilter_DoesNothing()
+ {
+ _collection.Remove(_filter);
+
+ Assert.Equal(0, _collection.Count);
+ }
+
+ [Fact]
+ public void Remove_WhenCollectionHasFilter_RemovesIt()
+ {
+ _collection.Add(_filter);
+ _collection.Add(_filter);
+
+ _collection.Remove(_filter);
+
+ Assert.Equal(0, _collection.Count);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Filters/QueryCompositionFilterAttributeTest.cs b/test/System.Web.Http.Test/Filters/QueryCompositionFilterAttributeTest.cs
new file mode 100644
index 00000000..d79d93e4
--- /dev/null
+++ b/test/System.Web.Http.Test/Filters/QueryCompositionFilterAttributeTest.cs
@@ -0,0 +1,88 @@
+using System.Linq;
+using System.Net.Http;
+using System.Net.Http.Formatting;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Filters
+{
+ public class QueryCompositionFilterAttributeTest
+ {
+ private const string QueryKey = "MS_QueryKey";
+
+ QueryCompositionFilterAttribute _filter = new QueryCompositionFilterAttribute(typeof(int), queryValidator: null);
+
+ [Fact]
+ public void ConstructorThrowsOnNullInput()
+ {
+ Assert.ThrowsArgumentNull(() => new QueryCompositionFilterAttribute(null, queryValidator: null), "queryElementType");
+ }
+
+ [Fact]
+ public void OnActionExecutingSetsQueryPropertyOnRequestMessage()
+ {
+ // Arrange
+ var actionContext = ContextUtil.GetHttpActionContext(new HttpRequestMessage(HttpMethod.Get, "http://localhost/?$top=100"));
+
+ // Act
+ _filter.OnActionExecuting(actionContext);
+ var requestProperties = actionContext.ControllerContext.Request.Properties;
+
+ // Assert
+ Assert.True(requestProperties.ContainsKey(QueryKey));
+ Assert.IsAssignableFrom<IQueryable<int>>(requestProperties[QueryKey]);
+ }
+
+ [Fact]
+ public void OnActionExecutedAppendsQueryToResponse()
+ {
+ // Arrange
+ HttpRequestMessage request = new HttpRequestMessage();
+ request.Properties[QueryKey] = (new int[0]).AsQueryable().Take(100);
+ HttpResponseMessage response = new HttpResponseMessage() { Content = new ObjectContent<IQueryable<int>>(Enumerable.Range(1, 1000).AsQueryable(), new JsonMediaTypeFormatter()) };
+
+ var actionExecutedContext = ContextUtil.GetActionExecutedContext(request, response);
+
+ // Act
+ _filter.OnActionExecuted(actionExecutedContext);
+ HttpResponseMessage result = actionExecutedContext.Result;
+
+ // Assert
+ // TODO: we are depending on the correctness of QueryComposer here to test the filter which
+ // is sub-optimal. Reason being QueryComposer is a static class. cleanup with bug#325697
+ Assert.NotNull(result);
+ Assert.Equal(100, result.Content.ReadAsAsync<IQueryable<int>>().Result.Count());
+ }
+
+ [Theory]
+ [InlineData("$top=error")]
+ [InlineData("$filter=error")]
+ [InlineData("$skip=-100")]
+ public void OnActionExecutingThrowsForIncorrectTopQuery(string query)
+ {
+ // Arrange
+ const string baseAddress = "http://localhost/?{0}";
+ var request = new HttpRequestMessage(HttpMethod.Get, String.Format(baseAddress, query));
+ var actionContext = ContextUtil.GetHttpActionContext(request);
+
+ // Act & Assert
+ Assert.Throws<HttpRequestException>(
+ () => _filter.OnActionExecuting(actionContext),
+ "The query specified in the URI is not valid.");
+ }
+
+ [Fact]
+ public void OnActionExecutedOnNullResponse()
+ {
+ // Arrange
+ HttpRequestMessage request = new HttpRequestMessage();
+ request.Properties[QueryKey] = (new int[0]).AsQueryable().Take(100);
+ var actionContext = ContextUtil.GetActionExecutedContext(request, response: null);
+
+ // Act & Assert
+ Assert.DoesNotThrow(() => _filter.OnActionExecuted(actionContext));
+ Assert.Null(actionContext.Result);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Filters/QueryCompositionFilterProviderTest.cs b/test/System.Web.Http.Test/Filters/QueryCompositionFilterProviderTest.cs
new file mode 100644
index 00000000..98ec3b5d
--- /dev/null
+++ b/test/System.Web.Http.Test/Filters/QueryCompositionFilterProviderTest.cs
@@ -0,0 +1,72 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Threading.Tasks;
+using System.Web.Http.Controllers;
+using Microsoft.TestCommon;
+using Moq;
+using Xunit;
+using Xunit.Extensions;
+
+namespace System.Web.Http.Filters
+{
+ public class QueryCompositionFilterProviderTest
+ {
+ private QueryCompositionFilterProvider filterProvider = new QueryCompositionFilterProvider();
+
+ public static TheoryDataSet<Type> GetFiltersReturnsEmptySetForNonQueryableReturnTypesData
+ {
+ get
+ {
+ return new TheoryDataSet<Type>
+ {
+ { typeof(int) },
+ { typeof(string) },
+ { typeof(void) },
+ { typeof(IEnumerable<int>) },
+ { typeof(List<int>) }
+ };
+ }
+ }
+
+ public static TheoryDataSet<Type, Type> GetFiltersReturnsSingleFilterForQueryableReturnTypesData
+ {
+ get
+ {
+ return new TheoryDataSet<Type, Type>
+ {
+ { typeof(IQueryable<int>), typeof(int) },
+ { typeof(IQueryable<string>), typeof(string)},
+ { typeof(IQueryable<IQueryable<int>>), typeof(IQueryable<int>) },
+ { typeof(Task<IQueryable<int>>), typeof(int) }
+ // { typeof(HttpResponseMessage), typeof(int) } // static signature problems
+ };
+ }
+ }
+
+ [Theory]
+ [PropertyData("GetFiltersReturnsEmptySetForNonQueryableReturnTypesData")]
+ public void GetFiltersReturnsEmptySetForNonQueryableReturnTypes(Type returnType)
+ {
+ Mock<HttpActionDescriptor> mockActionDescriptor = new Mock<HttpActionDescriptor>();
+ mockActionDescriptor.Setup((actionDescriptor) => actionDescriptor.ReturnType).Returns(returnType);
+
+ var filters = filterProvider.GetFilters(configuration: null, actionDescriptor: mockActionDescriptor.Object);
+
+ Assert.Empty(filters);
+ }
+
+ [Theory]
+ [PropertyData("GetFiltersReturnsSingleFilterForQueryableReturnTypesData")]
+ public void GetFiltersReturnsSingleFilterForQueryableReturnTypes(Type returnType, Type queryType)
+ {
+ Mock<HttpActionDescriptor> mockActionDescriptor = new Mock<HttpActionDescriptor>();
+ mockActionDescriptor.Setup((actionDescriptor) => actionDescriptor.ReturnType).Returns(returnType);
+
+ var filters = filterProvider.GetFilters(configuration: null, actionDescriptor: mockActionDescriptor.Object);
+
+ Assert.True(filters.Count() == 1);
+ Assert.Equal(Assert.IsType<QueryCompositionFilterAttribute>(filters.First().Instance).QueryElementType, queryType);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Hosting/HttpRouteTest.cs b/test/System.Web.Http.Test/Hosting/HttpRouteTest.cs
new file mode 100644
index 00000000..6dc0de2e
--- /dev/null
+++ b/test/System.Web.Http.Test/Hosting/HttpRouteTest.cs
@@ -0,0 +1,38 @@
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Web.Http.Routing;
+using Microsoft.TestCommon;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Hosting
+{
+ public class HttpRouteTest
+ {
+ [Theory]
+ [InlineData("{controller}/{id}", "/SelfHostServer", "http://localhost/SelfHostServer/Customer/999")]
+ [InlineData("{controller}/{id}", "", "http://localhost/Customer/999")]
+ [InlineData("{controller}", "", "http://localhost/")]
+ [InlineData("{controller}", "/SelfHostServer", "http://localhost/SelfHostServer")]
+ [InlineData("{controller}", "", "http://localhost")]
+ [InlineData("{controller}/{id}", "", "http://localhost/")]
+ [InlineData("{controller}/{id}", "/SelfHostServer", "http://localhost/SelfHostServer")]
+ [InlineData("{controller}/{id}", "", "http://localhost")]
+ public void GetRouteDataShouldMatch(string uriTemplate, string virtualPathRoot, string requestUri)
+ {
+ HttpRoute route = new HttpRoute(uriTemplate);
+ route.Defaults.Add("controller", "Customer");
+ route.Defaults.Add("id", "999");
+ HttpRequestMessage request = new HttpRequestMessage();
+ request.RequestUri = new Uri(requestUri);
+ IHttpRouteData data = route.GetRouteData(virtualPathRoot, request);
+
+ // Assert
+ Assert.NotNull(data);
+ IDictionary<string, object> expectedResult = new Dictionary<string, object>();
+ expectedResult["controller"] = "Customer";
+ expectedResult["id"] = "999";
+ Assert.Equal(expectedResult, data.Values, new DictionaryEqualityComparer());
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/HttpBindingBehaviorAttributeTest.cs b/test/System.Web.Http.Test/HttpBindingBehaviorAttributeTest.cs
new file mode 100644
index 00000000..6e3bb6b3
--- /dev/null
+++ b/test/System.Web.Http.Test/HttpBindingBehaviorAttributeTest.cs
@@ -0,0 +1,51 @@
+using Xunit;
+
+namespace System.Web.Http
+{
+ public class HttpHttpBindingBehaviorAttributeTest
+ {
+ [Fact]
+ public void Behavior_Property()
+ {
+ // Arrange
+ HttpBindingBehavior expectedBehavior = (HttpBindingBehavior)(-20);
+
+ // Act
+ HttpBindingBehaviorAttribute attr = new HttpBindingBehaviorAttribute(expectedBehavior);
+
+ // Assert
+ Assert.Equal(expectedBehavior, attr.Behavior);
+ }
+
+ [Fact]
+ public void TypeId_ReturnsSameValue()
+ {
+ // Arrange
+ HttpBindNeverAttribute neverAttr = new HttpBindNeverAttribute();
+ HttpBindRequiredAttribute requiredAttr = new HttpBindRequiredAttribute();
+
+ // Act & assert
+ Assert.Same(neverAttr.TypeId, requiredAttr.TypeId);
+ }
+
+ [Fact]
+ public void BindNever_SetsBehavior()
+ {
+ // Act
+ HttpBindingBehaviorAttribute attr = new HttpBindNeverAttribute();
+
+ // Assert
+ Assert.Equal(HttpBindingBehavior.Never, attr.Behavior);
+ }
+
+ [Fact]
+ public void BindRequired_SetsBehavior()
+ {
+ // Act
+ HttpBindingBehaviorAttribute attr = new HttpBindRequiredAttribute();
+
+ // Assert
+ Assert.Equal(HttpBindingBehavior.Required, attr.Behavior);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/HttpRequestMessageExtensionsTest.cs b/test/System.Web.Http.Test/HttpRequestMessageExtensionsTest.cs
new file mode 100644
index 00000000..a27db862
--- /dev/null
+++ b/test/System.Web.Http.Test/HttpRequestMessageExtensionsTest.cs
@@ -0,0 +1,256 @@
+using System.Collections.Generic;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Formatting;
+using System.Net.Http.Headers;
+using System.Security.Principal;
+using System.Threading;
+using System.Web.Http.Hosting;
+using System.Web.Http.Routing;
+using System.Web.Http.Services;
+using Microsoft.TestCommon;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http
+{
+ public class HttpRequestMessageExtensionsTest
+ {
+ private readonly HttpRequestMessage _request = new HttpRequestMessage();
+ private readonly HttpConfiguration _config = new HttpConfiguration();
+ private readonly object _value = new object();
+ private readonly Mock<IDisposable> _disposableMock = new Mock<IDisposable>();
+ private readonly IDisposable _disposable;
+
+ public HttpRequestMessageExtensionsTest()
+ {
+ _disposable = _disposableMock.Object;
+ }
+
+ [Fact]
+ public void IsCorrectType()
+ {
+ Assert.Type.HasProperties(typeof(HttpRequestMessageExtensions), TypeAssert.TypeProperties.IsStatic | TypeAssert.TypeProperties.IsPublicVisibleClass);
+ }
+
+ [Fact]
+ public void GetConfigurationThrowsOnNull()
+ {
+ Assert.ThrowsArgumentNull(() => HttpRequestMessageExtensions.GetConfiguration(null), "request");
+ }
+
+ [Fact]
+ public void GetConfiguration()
+ {
+ // Arrange
+ _request.Properties.Add(HttpPropertyKeys.HttpConfigurationKey, _config);
+
+ // Act
+ HttpConfiguration afterConfig = _request.GetConfiguration();
+
+ // Assert
+ Assert.Same(_config, afterConfig);
+ }
+
+ [Fact]
+ public void GetUserPrincipalThrowsOnNull()
+ {
+ Assert.ThrowsArgumentNull(() => HttpRequestMessageExtensions.GetUserPrincipal(null), "request");
+ }
+
+ [Fact]
+ public void GetUserPrincipal()
+ {
+ // Arrange
+ Mock<IPrincipal> principalMock = new Mock<IPrincipal>();
+ IPrincipal beforePrincipal = principalMock.Object;
+ _request.Properties.Add(HttpPropertyKeys.UserPrincipalKey, beforePrincipal);
+
+ // Act
+ IPrincipal afterPrincipal = _request.GetUserPrincipal();
+
+ // Assert
+ Assert.Same(beforePrincipal, afterPrincipal);
+ }
+
+ [Fact]
+ public void GetSynchronizationContextThrowsOnNull()
+ {
+ Assert.ThrowsArgumentNull(() => HttpRequestMessageExtensions.GetSynchronizationContext(null), "request");
+ }
+
+ [Fact]
+ public void GetSynchronizationContext()
+ {
+ // Arrange
+ Mock<SynchronizationContext> syncContextMock = new Mock<SynchronizationContext>();
+ SynchronizationContext beforeSyncContext = syncContextMock.Object;
+ _request.Properties.Add(HttpPropertyKeys.SynchronizationContextKey, beforeSyncContext);
+
+ // Act
+ SynchronizationContext afterSyncContext = _request.GetSynchronizationContext();
+
+ // Assert
+ Assert.Same(beforeSyncContext, afterSyncContext);
+ }
+
+ [Fact]
+ public void GetRouteData()
+ {
+ // Arrange
+ IHttpRouteData routeData = new Mock<IHttpRouteData>().Object;
+ _request.Properties.Add(HttpPropertyKeys.HttpRouteDataKey, routeData);
+
+ // Act
+ var httpRouteData = _request.GetRouteData();
+
+ // Assert
+ Assert.Same(routeData, httpRouteData);
+ }
+
+ [Fact]
+ public void CreateResponse_OnNullRequest_ThrowsException()
+ {
+ Assert.ThrowsArgumentNull(() =>
+ {
+ HttpRequestMessageExtensions.CreateResponse(null, HttpStatusCode.OK, _value);
+ }, "request");
+
+ Assert.ThrowsArgumentNull(() =>
+ {
+ HttpRequestMessageExtensions.CreateResponse(null, HttpStatusCode.OK, _value, configuration: null);
+ }, "request");
+ }
+
+ [Fact]
+ public void CreateResponse_OnNullConfiguration_ThrowsException()
+ {
+ Assert.Throws<InvalidOperationException>(() =>
+ {
+ HttpRequestMessageExtensions.CreateResponse(_request, HttpStatusCode.OK, _value, configuration: null);
+ }, "The request does not have an associated configuration object or the provided configuration was null.");
+ }
+
+ [Fact]
+ public void CreateResponse_RetrievesContentNegotiatorFromServiceResolver()
+ {
+ // Arrange
+ Mock<IDependencyResolver> resolverMock = new Mock<IDependencyResolver>();
+ _config.ServiceResolver.SetResolver(resolverMock.Object);
+
+ // Act
+ HttpRequestMessageExtensions.CreateResponse(_request, HttpStatusCode.OK, _value, _config);
+
+ // Assert
+ resolverMock.Verify(r => r.GetService(typeof(IContentNegotiator)), Times.Once());
+ }
+
+ [Fact]
+ public void CreateResponse_PerformsContentNegotiationAndCreatesContentUsingResults()
+ {
+ // Arrange
+ Mock<IContentNegotiator> resolverMock = new Mock<IContentNegotiator>();
+ MediaTypeHeaderValue mediaType;
+ XmlMediaTypeFormatter formatter = new XmlMediaTypeFormatter();
+ resolverMock.Setup(r => r.Negotiate(typeof(object), _request, _config.Formatters, out mediaType))
+ .Returns(formatter);
+ _config.ServiceResolver.SetService(typeof(IContentNegotiator), resolverMock.Object);
+
+ // Act
+ var response = HttpRequestMessageExtensions.CreateResponse<object>(_request, HttpStatusCode.NoContent, _value, _config);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
+ Assert.Same(_request, response.RequestMessage);
+ Assert.IsType<ObjectContent<object>>(response.Content);
+ object contentValue;
+ Assert.True(response.TryGetObjectValue<object>(out contentValue));
+ Assert.Same(_value, contentValue);
+ }
+
+ [Fact]
+ public void RegisterForDispose_WhenRequestParameterIsNull_Throws()
+ {
+ Assert.ThrowsArgumentNull(
+ () => HttpRequestMessageExtensions.RegisterForDispose(request: null, resource: null), "request");
+ }
+
+ [Fact]
+ public void RegisterForDispose_WhenResourceParamterIsNull_DoesNothing()
+ {
+ _request.RegisterForDispose(resource: null);
+
+ Assert.False(_request.Properties.ContainsKey(HttpPropertyKeys.DisposableRequestResourcesKey));
+ }
+
+ [Fact]
+ public void RegisterForDispose_WhenResourceListDoesNotExist_CreatesListAndAddsResource()
+ {
+ _request.Properties.Remove(HttpPropertyKeys.DisposableRequestResourcesKey);
+
+ _request.RegisterForDispose(_disposable);
+
+ var list = Assert.IsType<List<IDisposable>>(_request.Properties[HttpPropertyKeys.DisposableRequestResourcesKey]);
+ Assert.Equal(1, list.Count);
+ Assert.Same(_disposable, list[0]);
+ }
+
+ [Fact]
+ public void RegisterForDispose_WhenResourceListExists_AddsResource()
+ {
+ var list = new List<IDisposable>();
+ _request.Properties[HttpPropertyKeys.DisposableRequestResourcesKey] = list;
+
+ _request.RegisterForDispose(_disposable);
+
+ Assert.Same(list, _request.Properties[HttpPropertyKeys.DisposableRequestResourcesKey]);
+ Assert.Equal(1, list.Count);
+ Assert.Same(_disposable, list[0]);
+ }
+
+ [Fact]
+ public void DisposeRequestResources_WhenRequestParameterIsNull_Throws()
+ {
+ Assert.ThrowsArgumentNull(
+ () => HttpRequestMessageExtensions.DisposeRequestResources(request: null), "request");
+ }
+
+ [Fact]
+ public void DisposeRequestResources_WhenResourceListDoesNotExists_DoesNothing()
+ {
+ _request.Properties.Remove(HttpPropertyKeys.DisposableRequestResourcesKey);
+
+ _request.DisposeRequestResources();
+
+ Assert.False(_request.Properties.ContainsKey(HttpPropertyKeys.DisposableRequestResourcesKey));
+ }
+
+ [Fact]
+ public void DisposeRequestResources_WhenResourceListExists_DisposesResourceAndClearsReferences()
+ {
+ var list = new List<IDisposable> { _disposable };
+ _request.Properties[HttpPropertyKeys.DisposableRequestResourcesKey] = list;
+
+ _request.DisposeRequestResources();
+
+ _disposableMock.Verify(d => d.Dispose());
+ Assert.Equal(0, list.Count);
+ }
+
+ [Fact]
+ public void DisposeRequestResources_WhenResourcesDisposeMethodThrowsException_IgnoresExceptionsAndContinuesDisposingOtherResources()
+ {
+ Mock<IDisposable> throwingDisposableMock = new Mock<IDisposable>();
+ throwingDisposableMock.Setup(d => d.Dispose()).Throws(new Exception());
+ var list = new List<IDisposable> { throwingDisposableMock.Object, _disposable };
+ _request.Properties[HttpPropertyKeys.DisposableRequestResourcesKey] = list;
+
+ _request.DisposeRequestResources();
+
+ throwingDisposableMock.Verify(d => d.Dispose());
+ _disposableMock.Verify(d => d.Dispose());
+ Assert.Equal(0, list.Count);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/HttpRouteCollectionExtensionsTest.cs b/test/System.Web.Http.Test/HttpRouteCollectionExtensionsTest.cs
new file mode 100644
index 00000000..ace893f3
--- /dev/null
+++ b/test/System.Web.Http.Test/HttpRouteCollectionExtensionsTest.cs
@@ -0,0 +1,67 @@
+using System.Web.Http.Routing;
+using Microsoft.TestCommon;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http
+{
+ public class HttpRouteCollectionExtensionsTest
+ {
+ [Fact]
+ public void IsCorrectType()
+ {
+ Assert.Type.HasProperties(typeof(HttpRouteCollectionExtensions), TypeAssert.TypeProperties.IsStatic | TypeAssert.TypeProperties.IsPublicVisibleClass);
+ }
+
+ [Fact]
+ public void MapHttpRoute1ThrowsOnNullRouteCollection()
+ {
+ Assert.ThrowsArgumentNull(() => HttpRouteCollectionExtensions.MapHttpRoute(null, "", "", null), "routes");
+ }
+
+ [Fact]
+ public void MapHttpRoute1CreatesRoute()
+ {
+ // Arrange
+ HttpRouteCollection routes = new HttpRouteCollection();
+ object defaults = new { d1 = "D1" };
+
+ // Act
+ IHttpRoute route = routes.MapHttpRoute("name", "template", defaults);
+
+ // Assert
+ Assert.NotNull(route);
+ Assert.Equal("template", route.RouteTemplate);
+ Assert.Equal(1, route.Defaults.Count);
+ Assert.Equal("D1", route.Defaults["d1"]);
+ Assert.Same(route, routes["name"]);
+ }
+
+ [Fact]
+ public void MapHttpRoute2ThrowsOnNullRouteCollection()
+ {
+ Assert.ThrowsArgumentNull(() => HttpRouteCollectionExtensions.MapHttpRoute(null, "", "", null, null), "routes");
+ }
+
+ [Fact]
+ public void MapHttpRoute2CreatesRoute()
+ {
+ // Arrange
+ HttpRouteCollection routes = new HttpRouteCollection();
+ object defaults = new { d1 = "D1" };
+ object constraints = new { c1 = "C1" };
+
+ // Act
+ IHttpRoute route = routes.MapHttpRoute("name", "template", defaults, constraints);
+
+ // Assert
+ Assert.NotNull(route);
+ Assert.Equal("template", route.RouteTemplate);
+ Assert.Equal(1, route.Defaults.Count);
+ Assert.Equal("D1", route.Defaults["d1"]);
+ Assert.Equal(1, route.Defaults.Count);
+ Assert.Equal("C1", route.Constraints["c1"]);
+ Assert.Same(route, routes["name"]);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/HttpServerTest.cs b/test/System.Web.Http.Test/HttpServerTest.cs
new file mode 100644
index 00000000..973b9327
--- /dev/null
+++ b/test/System.Web.Http.Test/HttpServerTest.cs
@@ -0,0 +1,163 @@
+using System.Net;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Dispatcher;
+using Microsoft.TestCommon;
+using Moq;
+using Moq.Protected;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http
+{
+ public class HttpServerTest
+ {
+ [Fact]
+ public void IsCorrectType()
+ {
+ Assert.Type.HasProperties<HttpServer, DelegatingHandler>(TypeAssert.TypeProperties.IsPublicVisibleClass | TypeAssert.TypeProperties.IsDisposable);
+ }
+
+ [Fact]
+ public void DefaultConstructor()
+ {
+ Assert.NotNull(new HttpServer());
+ }
+
+ [Fact]
+ public void ConstructorConfigThrowsOnNull()
+ {
+ Assert.ThrowsArgumentNull(() => new HttpServer((HttpConfiguration)null), "configuration");
+ }
+
+ [Fact]
+ public void ConstructorConfigSetsUpProperties()
+ {
+ // Arrange
+ HttpConfiguration config = new HttpConfiguration();
+
+ // Act
+ HttpServer server = new HttpServer(config);
+
+ // Assert
+ Assert.Same(config, server.Configuration);
+ }
+
+ [Fact]
+ public void ConstructorDispatcherThrowsOnNull()
+ {
+ Assert.ThrowsArgumentNull(() => new HttpServer((HttpControllerDispatcher)null), "dispatcher");
+ }
+
+ [Fact]
+ public void ConstructorDispatcherSetsUpProperties()
+ {
+ // Arrange
+ Mock<HttpControllerDispatcher> controllerDispatcherMock = new Mock<HttpControllerDispatcher>();
+
+ // Act
+ HttpServer server = new HttpServer(controllerDispatcherMock.Object);
+
+ // Assert
+ Assert.Same(controllerDispatcherMock.Object, server.Dispatcher);
+ }
+
+ [Fact]
+ public void ConstructorThrowsOnNull()
+ {
+ Mock<HttpControllerDispatcher> controllerDispatcherMock = new Mock<HttpControllerDispatcher>();
+ Assert.ThrowsArgumentNull(() => new HttpServer((HttpConfiguration)null, controllerDispatcherMock.Object), "configuration");
+ Assert.ThrowsArgumentNull(() => new HttpServer(new HttpConfiguration(), null), "dispatcher");
+ }
+
+ [Fact]
+ public void ConstructorSetsUpProperties()
+ {
+ // Arrange
+ HttpConfiguration config = new HttpConfiguration();
+ Mock<HttpControllerDispatcher> controllerDispatcherMock = new Mock<HttpControllerDispatcher>();
+
+ // Act
+ HttpServer server = new HttpServer(config, controllerDispatcherMock.Object);
+
+ // Assert
+ Assert.Same(config, server.Configuration);
+ Assert.Same(controllerDispatcherMock.Object, server.Dispatcher);
+ }
+
+ [Fact]
+ public Task<HttpResponseMessage> DisposedReturnsServiceUnavailable()
+ {
+ // Arrange
+ Mock<HttpControllerDispatcher> dispatcherMock = new Mock<HttpControllerDispatcher>();
+ HttpServer server = new HttpServer(dispatcherMock.Object);
+ server.Dispose();
+ HttpRequestMessage request = new HttpRequestMessage();
+
+ // Act
+ return server.SubmitRequestAsync(request, CancellationToken.None).ContinueWith(
+ (reqTask) =>
+ {
+ // Assert
+ dispatcherMock.Protected().Verify<Task<HttpResponseMessage>>("SendAsync", Times.Never(), request, CancellationToken.None);
+ Assert.Equal(HttpStatusCode.ServiceUnavailable, reqTask.Result.StatusCode);
+ return reqTask.Result;
+ }
+ );
+ }
+
+ [Fact]
+ public Task<HttpResponseMessage> RequestGetsConfigurationAsParameter()
+ {
+ // Arrange
+ HttpRequestMessage request = new HttpRequestMessage();
+
+ Mock<HttpControllerDispatcher> dispatcherMock = new Mock<HttpControllerDispatcher>();
+ dispatcherMock.Protected().Setup<Task<HttpResponseMessage>>("SendAsync", request, CancellationToken.None).
+ Returns(TaskHelpers.FromResult<HttpResponseMessage>(request.CreateResponse()));
+
+ HttpConfiguration config = new HttpConfiguration();
+ HttpServer server = new HttpServer(config, dispatcherMock.Object);
+
+ // Act
+ return server.SubmitRequestAsync(request, CancellationToken.None).ContinueWith(
+ (reqTask) =>
+ {
+ // Assert
+ dispatcherMock.Protected().Verify<Task<HttpResponseMessage>>("SendAsync", Times.Once(), request, CancellationToken.None);
+ Assert.Same(config, request.GetConfiguration());
+ return reqTask.Result;
+ }
+ );
+ }
+
+ [Fact]
+ public Task<HttpResponseMessage> RequestGetsSyncContextAsParameter()
+ {
+ // Arrange
+ HttpRequestMessage request = new HttpRequestMessage();
+
+ Mock<HttpControllerDispatcher> dispatcherMock = new Mock<HttpControllerDispatcher>();
+ dispatcherMock.Protected().Setup<Task<HttpResponseMessage>>("SendAsync", request, CancellationToken.None).
+ Returns(TaskHelpers.FromResult<HttpResponseMessage>(request.CreateResponse()));
+
+ HttpConfiguration config = new HttpConfiguration();
+ HttpServer server = new HttpServer(config, dispatcherMock.Object);
+
+ SynchronizationContext syncContext = new SynchronizationContext();
+ SynchronizationContext.SetSynchronizationContext(syncContext);
+
+ // Act
+ return server.SubmitRequestAsync(request, CancellationToken.None).ContinueWith(
+ (reqTask) =>
+ {
+ // Assert
+ dispatcherMock.Protected().Verify<Task<HttpResponseMessage>>("SendAsync", Times.Once(), request, CancellationToken.None);
+ Assert.Same(syncContext, request.GetSynchronizationContext());
+ return reqTask.Result;
+ }
+ );
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Internal/CollectionModelBinderUtilTest.cs b/test/System.Web.Http.Test/Internal/CollectionModelBinderUtilTest.cs
new file mode 100644
index 00000000..a8806203
--- /dev/null
+++ b/test/System.Web.Http.Test/Internal/CollectionModelBinderUtilTest.cs
@@ -0,0 +1,394 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Web.Http.Metadata;
+using System.Web.Http.Metadata.Providers;
+using System.Web.Http.ModelBinding;
+using System.Web.Http.ValueProviders;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Internal
+{
+ public class CollectionModelBinderUtilTest
+ {
+ [Fact]
+ public void CreateOrReplaceCollection_OriginalModelImmutable_CreatesNewInstance()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => new ReadOnlyCollection<int>(new int[0]), typeof(ICollection<int>))
+ };
+
+ // Act
+ CollectionModelBinderUtil.CreateOrReplaceCollection(bindingContext, new[] { 10, 20, 30 }, () => new List<int>());
+
+ // Assert
+ int[] newModel = (bindingContext.Model as ICollection<int>).ToArray();
+ Assert.Equal(new[] { 10, 20, 30 }, newModel);
+ }
+
+ [Fact]
+ public void CreateOrReplaceCollection_OriginalModelMutable_UpdatesOriginalInstance()
+ {
+ // Arrange
+ List<int> originalInstance = new List<int> { 10, 20, 30 };
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => originalInstance, typeof(ICollection<int>))
+ };
+
+ // Act
+ CollectionModelBinderUtil.CreateOrReplaceCollection(bindingContext, new[] { 40, 50, 60 }, () => new List<int>());
+
+ // Assert
+ Assert.Same(originalInstance, bindingContext.Model);
+ Assert.Equal(new[] { 40, 50, 60 }, originalInstance.ToArray());
+ }
+
+ [Fact]
+ public void CreateOrReplaceCollection_OriginalModelNotCollection_CreatesNewInstance()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(ICollection<int>))
+ };
+
+ // Act
+ CollectionModelBinderUtil.CreateOrReplaceCollection(bindingContext, new[] { 10, 20, 30 }, () => new List<int>());
+
+ // Assert
+ int[] newModel = (bindingContext.Model as ICollection<int>).ToArray();
+ Assert.Equal(new[] { 10, 20, 30 }, newModel);
+ }
+
+ [Fact]
+ public void CreateOrReplaceDictionary_DisallowsDuplicateKeys()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(Dictionary<string, int>))
+ };
+
+ // Act
+ CollectionModelBinderUtil.CreateOrReplaceDictionary(
+ bindingContext,
+ new[]
+ {
+ new KeyValuePair<string, int>("forty-two", 40),
+ new KeyValuePair<string, int>("forty-two", 2),
+ new KeyValuePair<string, int>("forty-two", 42)
+ },
+ () => new Dictionary<string, int>());
+
+ // Assert
+ IDictionary<string, int> newModel = bindingContext.Model as IDictionary<string, int>;
+ Assert.Equal(new[] { "forty-two" }, newModel.Keys.ToArray());
+ Assert.Equal(42, newModel["forty-two"]);
+ }
+
+ [Fact]
+ public void CreateOrReplaceDictionary_DisallowsNullKeys()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(Dictionary<string, int>))
+ };
+
+ // Act
+ CollectionModelBinderUtil.CreateOrReplaceDictionary(
+ bindingContext,
+ new[]
+ {
+ new KeyValuePair<string, int>("forty-two", 42),
+ new KeyValuePair<string, int>(null, 84)
+ },
+ () => new Dictionary<string, int>());
+
+ // Assert
+ IDictionary<string, int> newModel = bindingContext.Model as IDictionary<string, int>;
+ Assert.Equal(new[] { "forty-two" }, newModel.Keys.ToArray());
+ Assert.Equal(42, newModel["forty-two"]);
+ }
+
+ [Fact]
+ public void CreateOrReplaceDictionary_OriginalModelImmutable_CreatesNewInstance()
+ {
+ // Arrange
+ ReadOnlyDictionary<string, string> originalModel = new ReadOnlyDictionary<string, string>();
+
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => originalModel, typeof(IDictionary<string, string>))
+ };
+
+ // Act
+ CollectionModelBinderUtil.CreateOrReplaceDictionary(
+ bindingContext,
+ new Dictionary<string, string>
+ {
+ { "Hello", "World" }
+ },
+ () => new Dictionary<string, string>());
+
+ // Assert
+ IDictionary<string, string> newModel = bindingContext.Model as IDictionary<string, string>;
+ Assert.NotSame(originalModel, newModel);
+ Assert.Equal(new[] { "Hello" }, newModel.Keys.ToArray());
+ Assert.Equal("World", newModel["Hello"]);
+ }
+
+ [Fact]
+ public void CreateOrReplaceDictionary_OriginalModelMutable_UpdatesOriginalInstance()
+ {
+ // Arrange
+ Dictionary<string, string> originalInstance = new Dictionary<string, string>
+ {
+ { "dog", "Canidae" },
+ { "cat", "Felidae" }
+ };
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => originalInstance, typeof(IDictionary<string, string>))
+ };
+
+ // Act
+ CollectionModelBinderUtil.CreateOrReplaceDictionary(
+ bindingContext,
+ new Dictionary<string, string>
+ {
+ { "horse", "Equidae" },
+ { "bear", "Ursidae" }
+ },
+ () => new Dictionary<string, string>());
+
+ // Assert
+ Assert.Same(originalInstance, bindingContext.Model);
+ Assert.Equal(new[] { "horse", "bear" }, originalInstance.Keys.ToArray());
+ Assert.Equal("Equidae", originalInstance["horse"]);
+ Assert.Equal("Ursidae", originalInstance["bear"]);
+ }
+
+ [Fact]
+ public void CreateOrReplaceDictionary_OriginalModelNotDictionary_CreatesNewInstance()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(IDictionary<string, string>))
+ };
+
+ // Act
+ CollectionModelBinderUtil.CreateOrReplaceDictionary(
+ bindingContext,
+ new Dictionary<string, string>
+ {
+ { "horse", "Equidae" },
+ { "bear", "Ursidae" }
+ },
+ () => new Dictionary<string, string>());
+
+ // Assert
+ IDictionary<string, string> newModel = bindingContext.Model as IDictionary<string, string>;
+ Assert.Equal(new[] { "horse", "bear" }, newModel.Keys.ToArray());
+ Assert.Equal("Equidae", newModel["horse"]);
+ Assert.Equal("Ursidae", newModel["bear"]);
+ }
+
+ [Fact]
+ public void GetIndexNamesFromValueProviderResult_ValueProviderResultIsNull_ReturnsNull()
+ {
+ // Act
+ IEnumerable<string> indexNames = CollectionModelBinderUtil.GetIndexNamesFromValueProviderResult(null);
+
+ // Assert
+ Assert.Null(indexNames);
+ }
+
+ [Fact]
+ public void GetIndexNamesFromValueProviderResult_ValueProviderResultReturnsEmptyArray_ReturnsNull()
+ {
+ // Arrange
+ ValueProviderResult vpResult = new ValueProviderResult(new string[0], "", null);
+
+ // Act
+ IEnumerable<string> indexNames = CollectionModelBinderUtil.GetIndexNamesFromValueProviderResult(vpResult);
+
+ // Assert
+ Assert.Null(indexNames);
+ }
+
+ [Fact]
+ public void GetIndexNamesFromValueProviderResult_ValueProviderResultReturnsNonEmptyArray_ReturnsArray()
+ {
+ // Arrange
+ ValueProviderResult vpResult = new ValueProviderResult(new[] { "foo", "bar", "baz" }, "foo,bar,baz", null);
+
+ // Act
+ IEnumerable<string> indexNames = CollectionModelBinderUtil.GetIndexNamesFromValueProviderResult(vpResult);
+
+ // Assert
+ Assert.NotNull(indexNames);
+ Assert.Equal(new[] { "foo", "bar", "baz" }, indexNames.ToArray());
+ }
+
+ [Fact]
+ public void GetIndexNamesFromValueProviderResult_ValueProviderResultReturnsNull_ReturnsNull()
+ {
+ // Arrange
+ ValueProviderResult vpResult = new ValueProviderResult(null, null, null);
+
+ // Act
+ IEnumerable<string> indexNames = CollectionModelBinderUtil.GetIndexNamesFromValueProviderResult(vpResult);
+
+ // Assert
+ Assert.Null(indexNames);
+ }
+
+ [Fact]
+ public void GetTypeArgumentsForUpdatableGenericCollection_ModelTypeNotGeneric_Fail()
+ {
+ // Arrange
+ ModelMetadata modelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int));
+
+ // Act
+ Type[] typeArguments = CollectionModelBinderUtil.GetTypeArgumentsForUpdatableGenericCollection(null, null, modelMetadata);
+
+ // Assert
+ Assert.Null(typeArguments);
+ }
+
+ [Fact]
+ public void GetTypeArgumentsForUpdatableGenericCollection_ModelTypeOpenGeneric_Fail()
+ {
+ // Arrange
+ ModelMetadata modelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(IList<>));
+
+ // Act
+ Type[] typeArguments = CollectionModelBinderUtil.GetTypeArgumentsForUpdatableGenericCollection(null, null, modelMetadata);
+
+ // Assert
+ Assert.Null(typeArguments);
+ }
+
+ [Fact]
+ public void GetTypeArgumentsForUpdatableGenericCollection_ModelTypeWrongNumberOfGenericArguments_Fail()
+ {
+ // Arrange
+ ModelMetadata modelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(KeyValuePair<int, string>));
+
+ // Act
+ Type[] typeArguments = CollectionModelBinderUtil.GetTypeArgumentsForUpdatableGenericCollection(typeof(ICollection<>), null, modelMetadata);
+
+ // Assert
+ Assert.Null(typeArguments);
+ }
+
+ [Fact]
+ public void GetTypeArgumentsForUpdatableGenericCollection_ReadOnlyReference_ModelInstanceImmutable_Valid()
+ {
+ // Arrange
+ ModelMetadata modelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => new int[0], typeof(IList<int>));
+ modelMetadata.IsReadOnly = true;
+
+ // Act
+ Type[] typeArguments = CollectionModelBinderUtil.GetTypeArgumentsForUpdatableGenericCollection(typeof(IList<>), typeof(List<>), modelMetadata);
+
+ // Assert
+ Assert.Null(typeArguments);
+ }
+
+ [Fact]
+ public void GetTypeArgumentsForUpdatableGenericCollection_ReadOnlyReference_ModelInstanceMutable_Valid()
+ {
+ // Arrange
+ ModelMetadata modelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => new List<int>(), typeof(IList<int>));
+ modelMetadata.IsReadOnly = true;
+
+ // Act
+ Type[] typeArguments = CollectionModelBinderUtil.GetTypeArgumentsForUpdatableGenericCollection(typeof(IList<>), typeof(List<>), modelMetadata);
+
+ // Assert
+ Assert.Equal(new[] { typeof(int) }, typeArguments);
+ }
+
+ [Fact]
+ public void GetTypeArgumentsForUpdatableGenericCollection_ReadOnlyReference_ModelInstanceOfWrongType_Fail()
+ {
+ // Arrange
+ ModelMetadata modelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => new HashSet<int>(), typeof(ICollection<int>));
+ modelMetadata.IsReadOnly = true;
+
+ // Act
+ Type[] typeArguments = CollectionModelBinderUtil.GetTypeArgumentsForUpdatableGenericCollection(typeof(IList<>), typeof(List<>), modelMetadata);
+
+ // Assert
+ // HashSet<> is not an IList<>, so we can't update
+ Assert.Null(typeArguments);
+ }
+
+ [Fact]
+ public void GetTypeArgumentsForUpdatableGenericCollection_ReadOnlyReference_ModelIsNull_Fail()
+ {
+ // Arrange
+ ModelMetadata modelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(IList<int>));
+ modelMetadata.IsReadOnly = true;
+
+ // Act
+ Type[] typeArguments = CollectionModelBinderUtil.GetTypeArgumentsForUpdatableGenericCollection(typeof(ICollection<>), typeof(List<>), modelMetadata);
+
+ // Assert
+ Assert.Null(typeArguments);
+ }
+
+ [Fact]
+ public void GetTypeArgumentsForUpdatableGenericCollection_ReadWriteReference_NewInstanceAssignableToModelType_Success()
+ {
+ // Arrange
+ ModelMetadata modelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(IList<int>));
+ modelMetadata.IsReadOnly = false;
+
+ // Act
+ Type[] typeArguments = CollectionModelBinderUtil.GetTypeArgumentsForUpdatableGenericCollection(typeof(ICollection<>), typeof(List<>), modelMetadata);
+
+ // Assert
+ Assert.Equal(new[] { typeof(int) }, typeArguments);
+ }
+
+ [Fact]
+ public void GetTypeArgumentsForUpdatableGenericCollection_ReadWriteReference_NewInstanceNotAssignableToModelType_MutableInstance_Success()
+ {
+ // Arrange
+ ModelMetadata modelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => new Collection<int>(), typeof(Collection<int>));
+ modelMetadata.IsReadOnly = false;
+
+ // Act
+ Type[] typeArguments = CollectionModelBinderUtil.GetTypeArgumentsForUpdatableGenericCollection(typeof(ICollection<>), typeof(List<>), modelMetadata);
+
+ // Assert
+ Assert.Equal(new[] { typeof(int) }, typeArguments);
+ }
+
+ [Fact]
+ public void GetZeroBasedIndexes()
+ {
+ // Act
+ string[] indexes = CollectionModelBinderUtil.GetZeroBasedIndexes().Take(5).ToArray();
+
+ // Assert
+ Assert.Equal(new[] { "0", "1", "2", "3", "4" }, indexes);
+ }
+
+ private class ReadOnlyDictionary<TKey, TValue> : Dictionary<TKey, TValue>, ICollection<KeyValuePair<TKey, TValue>>
+ {
+ bool ICollection<KeyValuePair<TKey, TValue>>.IsReadOnly
+ {
+ get { return true; }
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Internal/TypeActivatorTest.cs b/test/System.Web.Http.Test/Internal/TypeActivatorTest.cs
new file mode 100644
index 00000000..1ef026d4
--- /dev/null
+++ b/test/System.Web.Http.Test/Internal/TypeActivatorTest.cs
@@ -0,0 +1,158 @@
+using System.Collections.Generic;
+using System.Net;
+using System.Net.Http;
+using System.Reflection;
+using System.Web.Http.Controllers;
+using Microsoft.TestCommon;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Internal
+{
+ public class TypeActivatorTest
+ {
+ [Fact]
+ public void TypeIsCorrect()
+ {
+ Assert.Type.HasProperties(typeof(TypeActivator), TypeAssert.TypeProperties.IsClass | TypeAssert.TypeProperties.IsStatic);
+ }
+
+ public static TheoryDataSet<Type, Type> ValidTypeParameters
+ {
+ get
+ {
+ return new TheoryDataSet<Type, Type>
+ {
+ { typeof(List<int>), typeof(IList<int>)},
+ { typeof(Dictionary<int, int>), typeof(IDictionary<int, int>)},
+ { typeof(HttpRequestMessage), typeof(HttpRequestMessage)},
+ { typeof(HttpConfiguration), typeof(HttpConfiguration)},
+ { typeof(ReflectedHttpActionDescriptor), typeof(HttpActionDescriptor) },
+ { typeof(ApiControllerActionSelector), typeof(IHttpActionSelector)},
+ { typeof(ApiControllerActionInvoker), typeof(IHttpActionInvoker)},
+ { typeof(List<HttpStatusCode>), typeof(IEnumerable<HttpStatusCode>)},
+ };
+ }
+ }
+
+ [Theory]
+ [PropertyData("ValidTypeParameters")]
+ public void CreateType(Type instanceType, Type baseType)
+ {
+ // Arrange
+ Func<object> instanceDelegate = TypeActivator.Create(instanceType);
+
+ // Act
+ object instance = instanceDelegate();
+
+ // Assert
+ Assert.IsType(instanceType, instance);
+ }
+
+ [Theory]
+ [InlineData(typeof(int))]
+ [InlineData(typeof(Guid))]
+ [InlineData(typeof(HttpStatusCode))]
+ [InlineData(typeof(string))]
+ [InlineData(typeof(Uri))]
+ [InlineData(typeof(IDictionary<object, object>))]
+ [InlineData(typeof(List<>))]
+ public void CreateTypeInvalidThrowsInvalidArgument(Type type)
+ {
+ // Value types, interfaces, and open generics cause ArgumentException
+ Assert.Throws<ArgumentException>(() => TypeActivator.Create(type));
+ }
+
+ [Theory]
+ [InlineData(typeof(HttpContent))]
+ [InlineData(typeof(HttpActionDescriptor))]
+ public void CreateTypeInvalidThrowsInvalidOperation(Type type)
+ {
+ // Abstract types cause InvalidOperationException
+ Assert.Throws<InvalidOperationException>(() => TypeActivator.Create(type));
+ }
+
+ [Theory]
+ [PropertyData("ValidTypeParameters")]
+ public void CreateOfT(Type instanceType, Type baseType)
+ {
+ // Arrange
+ Type activatorType = typeof(TypeActivator);
+ MethodInfo createMethodInfo = activatorType.GetMethod("Create", Type.EmptyTypes);
+ MethodInfo genericCreateMethodInfo = createMethodInfo.MakeGenericMethod(instanceType);
+ Func<object> instanceDelegate = (Func<object>)genericCreateMethodInfo.Invoke(null, null);
+
+ // Act
+ object instance = instanceDelegate();
+
+ // Assert
+ Assert.IsType(instanceType, instance);
+ }
+
+ [Fact]
+ public void CreateOfTInvalid()
+ {
+ // string doesn't have a default ctor
+ Assert.Throws<ArgumentException>(() => TypeActivator.Create<string>());
+
+ // Uri doesn't have a default ctor
+ Assert.Throws<ArgumentException>(() => TypeActivator.Create<Uri>());
+
+ // HttpContent is abstract
+ Assert.Throws<InvalidOperationException>(() => TypeActivator.Create<HttpContent>());
+
+ // HttpActionDescriptor is abstract
+ Assert.Throws<InvalidOperationException>(() => TypeActivator.Create<HttpActionDescriptor>());
+ }
+
+ [Theory]
+ [PropertyData("ValidTypeParameters")]
+ public void CreateOfTBase(Type instanceType, Type baseType)
+ {
+ // Arrange
+ Type activatorType = typeof(TypeActivator);
+ MethodInfo createMethodInfo = null;
+ foreach (MethodInfo methodInfo in activatorType.GetMethods())
+ {
+ ParameterInfo[] parameterInfo = methodInfo.GetParameters();
+ if (methodInfo.Name == "Create" && methodInfo.ContainsGenericParameters && parameterInfo.Length == 1 && parameterInfo[0].ParameterType == typeof(Type))
+ {
+ createMethodInfo = methodInfo;
+ break;
+ }
+ }
+
+ MethodInfo genericCreateMethodInfo = createMethodInfo.MakeGenericMethod(baseType);
+ Func<object> instanceDelegate = (Func<object>)genericCreateMethodInfo.Invoke(null, new object[] { instanceType });
+
+ // Act
+ object instance = instanceDelegate();
+
+ // Assert
+ Assert.IsType(instanceType, instance);
+ }
+
+ [Fact]
+ public void CreateOfTBaseInvalid()
+ {
+ // int not being a ref type
+ Assert.Throws<ArgumentException>(() => TypeActivator.Create<object>(typeof(int)));
+
+ // GUID is not a ref type
+ Assert.Throws<ArgumentException>(() => TypeActivator.Create<object>(typeof(Guid)));
+
+ // HttpStatusCode is not a ref type
+ Assert.Throws<ArgumentException>(() => TypeActivator.Create<object>(typeof(HttpStatusCode)));
+
+ // string does not have a default ctor
+ Assert.Throws<ArgumentException>(() => TypeActivator.Create<string>(typeof(string)));
+
+ // ObjectContent does not have a default ctor
+ Assert.Throws<ArgumentException>(() => TypeActivator.Create<HttpContent>(typeof(ObjectContent)));
+
+ // Base type and instance type flipped
+ Assert.Throws<ArgumentException>(() => TypeActivator.Create<ReflectedHttpActionDescriptor>(typeof(HttpActionDescriptor)));
+ }
+ }
+} \ No newline at end of file
diff --git a/test/System.Web.Http.Test/ModelBinding/Binders/ArrayModelBinderProviderTest.cs b/test/System.Web.Http.Test/ModelBinding/Binders/ArrayModelBinderProviderTest.cs
new file mode 100644
index 00000000..edd08d82
--- /dev/null
+++ b/test/System.Web.Http.Test/ModelBinding/Binders/ArrayModelBinderProviderTest.cs
@@ -0,0 +1,100 @@
+using System.Collections.Generic;
+using System.Web.Http.Metadata.Providers;
+using System.Web.Http.Util;
+using Xunit;
+
+namespace System.Web.Http.ModelBinding.Binders
+{
+ public class ArrayModelBinderProviderTest
+ {
+ [Fact]
+ public void GetBinder_CorrectModelTypeAndValueProviderEntries_ReturnsBinder()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int[])),
+ ModelName = "foo",
+ ValueProvider = new SimpleHttpValueProvider
+ {
+ { "foo[0]", "42" },
+ }
+ };
+
+ ArrayModelBinderProvider binderProvider = new ArrayModelBinderProvider();
+
+ // Act
+ IModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.IsType<ArrayModelBinder<int>>(binder);
+ }
+
+ [Fact]
+ public void GetBinder_ModelMetadataReturnsReadOnly_ReturnsNull()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int[])),
+ ModelName = "foo",
+ ValueProvider = new SimpleHttpValueProvider
+ {
+ { "foo[0]", "42" },
+ }
+ };
+ bindingContext.ModelMetadata.IsReadOnly = true;
+
+ ArrayModelBinderProvider binderProvider = new ArrayModelBinderProvider();
+
+ // Act
+ IModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(binder);
+ }
+
+ [Fact]
+ public void GetBinder_ModelTypeIsIncorrect_ReturnsNull()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(ICollection<int>)),
+ ModelName = "foo",
+ ValueProvider = new SimpleHttpValueProvider
+ {
+ { "foo[0]", "42" },
+ }
+ };
+
+ ArrayModelBinderProvider binderProvider = new ArrayModelBinderProvider();
+
+ // Act
+ IModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(binder);
+ }
+
+ [Fact]
+ public void GetBinder_ValueProviderDoesNotContainPrefix_ReturnsNull()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int[])),
+ ModelName = "foo",
+ ValueProvider = new SimpleHttpValueProvider()
+ };
+
+ ArrayModelBinderProvider binderProvider = new ArrayModelBinderProvider();
+
+ // Act
+ IModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(binder);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/ModelBinding/Binders/ArrayModelBinderTest.cs b/test/System.Web.Http.Test/ModelBinding/Binders/ArrayModelBinderTest.cs
new file mode 100644
index 00000000..a8ed1136
--- /dev/null
+++ b/test/System.Web.Http.Test/ModelBinding/Binders/ArrayModelBinderTest.cs
@@ -0,0 +1,47 @@
+using System.Web.Http.Controllers;
+using System.Web.Http.Metadata.Providers;
+using System.Web.Http.Util;
+using Moq;
+using Xunit;
+
+namespace System.Web.Http.ModelBinding.Binders
+{
+ public class ArrayModelBinderTest
+ {
+ [Fact]
+ public void BindModel()
+ {
+ // Arrange
+ Mock<IModelBinder> mockIntBinder = new Mock<IModelBinder>();
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ context.ControllerContext.Configuration.ServiceResolver.SetService(typeof(ModelBinderProvider), new SimpleModelBinderProvider(typeof(int), mockIntBinder.Object));
+
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int[])),
+ ModelName = "someName",
+ ValueProvider = new SimpleHttpValueProvider
+ {
+ { "someName[0]", "42" },
+ { "someName[1]", "84" }
+ }
+ };
+ mockIntBinder
+ .Setup(o => o.BindModel(context, It.IsAny<ModelBindingContext>()))
+ .Returns((HttpActionContext ec, ModelBindingContext mbc) =>
+ {
+ mbc.Model = mbc.ValueProvider.GetValue(mbc.ModelName).ConvertTo(mbc.ModelType);
+ return true;
+ });
+
+ // Act
+ bool retVal = new ArrayModelBinder<int>().BindModel(context, bindingContext);
+
+ // Assert
+ Assert.True(retVal);
+
+ int[] array = bindingContext.Model as int[];
+ Assert.Equal(new[] { 42, 84 }, array);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/ModelBinding/Binders/BinaryDataModelBinderProviderTest.cs b/test/System.Web.Http.Test/ModelBinding/Binders/BinaryDataModelBinderProviderTest.cs
new file mode 100644
index 00000000..82ec5e63
--- /dev/null
+++ b/test/System.Web.Http.Test/ModelBinding/Binders/BinaryDataModelBinderProviderTest.cs
@@ -0,0 +1,159 @@
+using System.Data.Linq;
+using System.Web.Http.Metadata.Providers;
+using System.Web.Http.Util;
+using Xunit;
+
+namespace System.Web.Http.ModelBinding.Binders
+{
+ public class BinaryDataModelBinderProviderTest
+ {
+ private static readonly byte[] _base64Bytes = new byte[] { 0x12, 0x20, 0x34, 0x40 };
+ private const string _base64String = "EiA0QA==";
+
+ [Fact]
+ public void BindModel_BadValue_Fails()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(byte[])),
+ ModelName = "foo",
+ ValueProvider = new SimpleHttpValueProvider
+ {
+ { "foo", "not base64 encoded!" }
+ }
+ };
+
+ BinaryDataModelBinderProvider binderProvider = new BinaryDataModelBinderProvider();
+
+ // Act
+ IModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+ bool retVal = binder.BindModel(null, bindingContext);
+
+ // Assert
+ Assert.False(retVal);
+ }
+
+ [Fact]
+ public void BindModel_EmptyValue_Fails()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(byte[])),
+ ModelName = "foo",
+ ValueProvider = new SimpleHttpValueProvider
+ {
+ { "foo", "" }
+ }
+ };
+
+ BinaryDataModelBinderProvider binderProvider = new BinaryDataModelBinderProvider();
+
+ // Act
+ IModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+ bool retVal = binder.BindModel(null, bindingContext);
+
+ // Assert
+ Assert.False(retVal);
+ }
+
+ [Fact]
+ public void BindModel_GoodValue_ByteArray_Succeeds()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(byte[])),
+ ModelName = "foo",
+ ValueProvider = new SimpleHttpValueProvider
+ {
+ { "foo", _base64String }
+ }
+ };
+
+ BinaryDataModelBinderProvider binderProvider = new BinaryDataModelBinderProvider();
+
+ // Act
+ IModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+ bool retVal = binder.BindModel(null, bindingContext);
+
+ // Assert
+ Assert.True(retVal);
+ Assert.Equal(_base64Bytes, (byte[])bindingContext.Model);
+ }
+
+ [Fact]
+ public void BindModel_GoodValue_LinqBinary_Succeeds()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(Binary)),
+ ModelName = "foo",
+ ValueProvider = new SimpleHttpValueProvider
+ {
+ { "foo", _base64String }
+ }
+ };
+
+ BinaryDataModelBinderProvider binderProvider = new BinaryDataModelBinderProvider();
+
+ // Act
+ IModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+ bool retVal = binder.BindModel(null, bindingContext);
+
+ // Assert
+ Assert.True(retVal);
+ Binary binaryModel = Assert.IsType<Binary>(bindingContext.Model);
+ Assert.Equal(_base64Bytes, binaryModel.ToArray());
+ }
+
+ [Fact]
+ public void BindModel_NoValue_Fails()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(byte[])),
+ ModelName = "foo",
+ ValueProvider = new SimpleHttpValueProvider
+ {
+ { "foo.bar", _base64String }
+ }
+ };
+
+ BinaryDataModelBinderProvider binderProvider = new BinaryDataModelBinderProvider();
+
+ // Act
+ IModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+ bool retVal = binder.BindModel(null, bindingContext);
+
+ // Assert
+ Assert.False(retVal);
+ }
+
+ [Fact]
+ public void GetBinder_WrongModelType_ReturnsNull()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(object)),
+ ModelName = "foo",
+ ValueProvider = new SimpleHttpValueProvider
+ {
+ { "foo", _base64String }
+ }
+ };
+
+ BinaryDataModelBinderProvider binderProvider = new BinaryDataModelBinderProvider();
+
+ // Act
+ IModelBinder modelBinder = binderProvider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(modelBinder);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/ModelBinding/Binders/CollectionModelBinderProviderTest.cs b/test/System.Web.Http.Test/ModelBinding/Binders/CollectionModelBinderProviderTest.cs
new file mode 100644
index 00000000..be2f438e
--- /dev/null
+++ b/test/System.Web.Http.Test/ModelBinding/Binders/CollectionModelBinderProviderTest.cs
@@ -0,0 +1,77 @@
+using System.Collections.Generic;
+using System.Web.Http.Metadata.Providers;
+using System.Web.Http.ModelBinding.Binders;
+using System.Web.Http.Util;
+using Xunit;
+
+namespace System.Web.Http.ModelBinding
+{
+ public class CollectionModelBinderProviderTest
+ {
+ [Fact]
+ public void GetBinder_CorrectModelTypeAndValueProviderEntries_ReturnsBinder()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(IEnumerable<int>)),
+ ModelName = "foo",
+ ValueProvider = new SimpleHttpValueProvider
+ {
+ { "foo[0]", "42" },
+ }
+ };
+
+ CollectionModelBinderProvider binderProvider = new CollectionModelBinderProvider();
+
+ // Act
+ IModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.IsType<CollectionModelBinder<int>>(binder);
+ }
+
+ [Fact]
+ public void GetBinder_ModelTypeIsIncorrect_ReturnsNull()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int)),
+ ModelName = "foo",
+ ValueProvider = new SimpleHttpValueProvider
+ {
+ { "foo[0]", "42" },
+ }
+ };
+
+ CollectionModelBinderProvider binderProvider = new CollectionModelBinderProvider();
+
+ // Act
+ IModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(binder);
+ }
+
+ [Fact]
+ public void GetBinder_ValueProviderDoesNotContainPrefix_ReturnsNull()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(IEnumerable<int>)),
+ ModelName = "foo",
+ ValueProvider = new SimpleHttpValueProvider()
+ };
+
+ CollectionModelBinderProvider binderProvider = new CollectionModelBinderProvider();
+
+ // Act
+ IModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(binder);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/ModelBinding/Binders/CollectionModelBinderTest.cs b/test/System.Web.Http.Test/ModelBinding/Binders/CollectionModelBinderTest.cs
new file mode 100644
index 00000000..65cfa2ec
--- /dev/null
+++ b/test/System.Web.Http.Test/ModelBinding/Binders/CollectionModelBinderTest.cs
@@ -0,0 +1,240 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Web.Http.Controllers;
+using System.Web.Http.Metadata.Providers;
+using System.Web.Http.ModelBinding.Binders;
+using System.Web.Http.Util;
+using System.Web.Http.Validation;
+using Moq;
+using Xunit;
+
+namespace System.Web.Http.ModelBinding
+{
+ public class CollectionModelBinderTest
+ {
+ [Fact]
+ public void BindComplexCollectionFromIndexes_FiniteIndexes()
+ {
+ // Arrange
+ CultureInfo culture = CultureInfo.GetCultureInfo("fr-FR");
+ Mock<IModelBinder> mockIntBinder = new Mock<IModelBinder>();
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int)),
+ ModelName = "someName",
+ ValueProvider = new SimpleHttpValueProvider
+ {
+ { "someName[foo]", "42" },
+ { "someName[baz]", "200" }
+ }
+ };
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ context.ControllerContext.Configuration.ServiceResolver.SetService(typeof(ModelBinderProvider), new SimpleModelBinderProvider(typeof(int), mockIntBinder.Object));
+
+ mockIntBinder
+ .Setup(o => o.BindModel(context, It.IsAny<ModelBindingContext>()))
+ .Returns((HttpActionContext ec, ModelBindingContext mbc) =>
+ {
+ mbc.Model = mbc.ValueProvider.GetValue(mbc.ModelName).ConvertTo(mbc.ModelType);
+ return true;
+ });
+
+ // Act
+ List<int> boundCollection = CollectionModelBinder<int>.BindComplexCollectionFromIndexes(context, bindingContext, new[] { "foo", "bar", "baz" });
+
+ // Assert
+ Assert.Equal(new[] { 42, 0, 200 }, boundCollection.ToArray());
+ Assert.Equal(new[] { "someName[foo]", "someName[baz]" }, bindingContext.ValidationNode.ChildNodes.Select(o => o.ModelStateKey).ToArray());
+ }
+
+ [Fact]
+ public void BindComplexCollectionFromIndexes_InfiniteIndexes()
+ {
+ // Arrange
+ CultureInfo culture = CultureInfo.GetCultureInfo("fr-FR");
+ Mock<IModelBinder> mockIntBinder = new Mock<IModelBinder>();
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int)),
+ ModelName = "someName",
+ ValueProvider = new SimpleHttpValueProvider
+ {
+ { "someName[0]", "42" },
+ { "someName[1]", "100" },
+ { "someName[3]", "400" }
+ }
+ };
+
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ context.ControllerContext.Configuration.ServiceResolver.SetService(typeof(ModelBinderProvider), new SimpleModelBinderProvider(typeof(int), mockIntBinder.Object));
+
+ mockIntBinder
+ .Setup(o => o.BindModel(context, It.IsAny<ModelBindingContext>()))
+ .Returns((HttpActionContext ec, ModelBindingContext mbc) =>
+ {
+ mbc.Model = mbc.ValueProvider.GetValue(mbc.ModelName).ConvertTo(mbc.ModelType);
+ return true;
+ });
+
+ // Act
+ List<int> boundCollection = CollectionModelBinder<int>.BindComplexCollectionFromIndexes(context, bindingContext, null /* indexNames */);
+
+ // Assert
+ Assert.Equal(new[] { 42, 100 }, boundCollection.ToArray());
+ Assert.Equal(new[] { "someName[0]", "someName[1]" }, bindingContext.ValidationNode.ChildNodes.Select(o => o.ModelStateKey).ToArray());
+ }
+
+ [Fact]
+ public void BindModel_ComplexCollection()
+ {
+ // Arrange
+ CultureInfo culture = CultureInfo.GetCultureInfo("fr-FR");
+ Mock<IModelBinder> mockIntBinder = new Mock<IModelBinder>();
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int)),
+ ModelName = "someName",
+ ValueProvider = new SimpleHttpValueProvider
+ {
+ { "someName.index", new[] { "foo", "bar", "baz" } },
+ { "someName[foo]", "42" },
+ { "someName[bar]", "100" },
+ { "someName[baz]", "200" }
+ }
+ };
+
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ context.ControllerContext.Configuration.ServiceResolver.SetService(typeof(ModelBinderProvider), new SimpleModelBinderProvider(typeof(int), mockIntBinder.Object));
+
+ mockIntBinder
+ .Setup(o => o.BindModel(context, It.IsAny<ModelBindingContext>()))
+ .Returns((HttpActionContext ec, ModelBindingContext mbc) =>
+ {
+ mbc.Model = mbc.ValueProvider.GetValue(mbc.ModelName).ConvertTo(mbc.ModelType);
+ return true;
+ });
+
+ CollectionModelBinder<int> modelBinder = new CollectionModelBinder<int>();
+
+ // Act
+ bool retVal = modelBinder.BindModel(context, bindingContext);
+
+ // Assert
+ Assert.Equal(new[] { 42, 100, 200 }, ((List<int>)bindingContext.Model).ToArray());
+ }
+
+ [Fact]
+ public void BindModel_SimpleCollection()
+ {
+ // Arrange
+ CultureInfo culture = CultureInfo.GetCultureInfo("fr-FR");
+ Mock<IModelBinder> mockIntBinder = new Mock<IModelBinder>();
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int)),
+ ModelName = "someName",
+ ValueProvider = new SimpleHttpValueProvider
+ {
+ { "someName", new[] { "42", "100", "200" } }
+ }
+ };
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ context.ControllerContext.Configuration.ServiceResolver.SetService(typeof(ModelBinderProvider), new SimpleModelBinderProvider(typeof(int), mockIntBinder.Object));
+
+ mockIntBinder
+ .Setup(o => o.BindModel(context, It.IsAny<ModelBindingContext>()))
+ .Returns((HttpActionContext ec, ModelBindingContext mbc) =>
+ {
+ mbc.Model = mbc.ValueProvider.GetValue(mbc.ModelName).ConvertTo(mbc.ModelType);
+ return true;
+ });
+
+ CollectionModelBinder<int> modelBinder = new CollectionModelBinder<int>();
+
+ // Act
+ bool retVal = modelBinder.BindModel(context, bindingContext);
+
+ // Assert
+ Assert.True(retVal);
+ Assert.Equal(new[] { 42, 100, 200 }, ((List<int>)bindingContext.Model).ToArray());
+ }
+
+ [Fact]
+ public void BindSimpleCollection_RawValueIsEmptyCollection_ReturnsEmptyList()
+ {
+ // Act
+ List<int> boundCollection = CollectionModelBinder<int>.BindSimpleCollection(null, null, new object[0], null);
+
+ // Assert
+ Assert.NotNull(boundCollection);
+ Assert.Empty(boundCollection);
+ }
+
+ [Fact]
+ public void BindSimpleCollection_RawValueIsNull_ReturnsNull()
+ {
+ // Act
+ List<int> boundCollection = CollectionModelBinder<int>.BindSimpleCollection(null, null, null, null);
+
+ // Assert
+ Assert.Null(boundCollection);
+ }
+
+ [Fact(Skip = "is this test checking a valid invariant?")]
+ public void BindSimpleCollection_SubBinderDoesNotExist()
+ {
+ // Arrange
+ CultureInfo culture = CultureInfo.GetCultureInfo("fr-FR");
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int)),
+ ModelName = "someName",
+ ValueProvider = new SimpleHttpValueProvider()
+ };
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ context.ControllerContext.Configuration.ServiceResolver.SetService(typeof(ModelBinderProvider), null); // completely remove from resolution?
+
+ // Act
+ List<int> boundCollection = CollectionModelBinder<int>.BindSimpleCollection(context, bindingContext, new int[1], culture);
+
+ // Assert
+ Assert.Equal(new[] { 0 }, boundCollection.ToArray());
+ Assert.Empty(bindingContext.ValidationNode.ChildNodes);
+ }
+
+ [Fact]
+ public void BindSimpleCollection_SubBindingSucceeds()
+ {
+ // Arrange
+ CultureInfo culture = CultureInfo.GetCultureInfo("fr-FR");
+ Mock<IModelBinder> mockIntBinder = new Mock<IModelBinder>();
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int)),
+ ModelName = "someName",
+ ValueProvider = new SimpleHttpValueProvider()
+ };
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ context.ControllerContext.Configuration.ServiceResolver.SetService(typeof(ModelBinderProvider), new SimpleModelBinderProvider(typeof(int), mockIntBinder.Object));
+
+ ModelValidationNode childValidationNode = null;
+ mockIntBinder
+ .Setup(o => o.BindModel(context, It.IsAny<ModelBindingContext>()))
+ .Returns((HttpActionContext ec, ModelBindingContext mbc) =>
+ {
+ Assert.Equal("someName", mbc.ModelName);
+ childValidationNode = mbc.ValidationNode;
+ mbc.Model = 42;
+ return true;
+ });
+
+ // Act
+ List<int> boundCollection = CollectionModelBinder<int>.BindSimpleCollection(context, bindingContext, new int[1], culture);
+
+ // Assert
+ Assert.Equal(new[] { 42 }, boundCollection.ToArray());
+ Assert.Equal(new[] { childValidationNode }, bindingContext.ValidationNode.ChildNodes.ToArray());
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/ModelBinding/Binders/ComplexModelDtoModelBinderProviderTest.cs b/test/System.Web.Http.Test/ModelBinding/Binders/ComplexModelDtoModelBinderProviderTest.cs
new file mode 100644
index 00000000..51e4e69c
--- /dev/null
+++ b/test/System.Web.Http.Test/ModelBinding/Binders/ComplexModelDtoModelBinderProviderTest.cs
@@ -0,0 +1,44 @@
+using System.Web.Http.Metadata.Providers;
+using Xunit;
+
+namespace System.Web.Http.ModelBinding.Binders
+{
+ public class ComplexModelDtoModelBinderProviderTest
+ {
+ [Fact]
+ public void GetBinder_TypeDoesNotMatch_ReturnsNull()
+ {
+ // Arrange
+ ComplexModelDtoModelBinderProvider provider = new ComplexModelDtoModelBinderProvider();
+ ModelBindingContext bindingContext = GetBindingContext(typeof(object));
+
+ // Act
+ IModelBinder binder = provider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(binder);
+ }
+
+ [Fact]
+ public void GetBinder_TypeMatches_ReturnsBinder()
+ {
+ // Arrange
+ ComplexModelDtoModelBinderProvider provider = new ComplexModelDtoModelBinderProvider();
+ ModelBindingContext bindingContext = GetBindingContext(typeof(ComplexModelDto));
+
+ // Act
+ IModelBinder binder = provider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.IsType<ComplexModelDtoModelBinder>(binder);
+ }
+
+ private static ModelBindingContext GetBindingContext(Type modelType)
+ {
+ return new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => null, modelType)
+ };
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/ModelBinding/Binders/ComplexModelDtoModelBinderTest.cs b/test/System.Web.Http.Test/ModelBinding/Binders/ComplexModelDtoModelBinderTest.cs
new file mode 100644
index 00000000..533f5209
--- /dev/null
+++ b/test/System.Web.Http.Test/ModelBinding/Binders/ComplexModelDtoModelBinderTest.cs
@@ -0,0 +1,91 @@
+using System.Linq;
+using System.Web.Http.Controllers;
+using System.Web.Http.Metadata;
+using System.Web.Http.Metadata.Providers;
+using System.Web.Http.Validation;
+using Moq;
+using Xunit;
+
+namespace System.Web.Http.ModelBinding.Binders
+{
+ public class ComplexModelDtoModelBinderTest
+ {
+ [Fact]
+ public void BindModel()
+ {
+ // Arrange
+ MyModel model = new MyModel();
+ ModelMetadata modelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => model, typeof(MyModel));
+ ComplexModelDto dto = new ComplexModelDto(modelMetadata, modelMetadata.Properties);
+ Mock<IModelBinder> mockStringBinder = new Mock<IModelBinder>();
+ Mock<IModelBinder> mockIntBinder = new Mock<IModelBinder>();
+ Mock<IModelBinder> mockDateTimeBinder = new Mock<IModelBinder>();
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ context.ControllerContext.Configuration.ServiceResolver.SetServices(typeof(ModelBinderProvider),
+ new SimpleModelBinderProvider(typeof(string), mockStringBinder.Object) { SuppressPrefixCheck = true },
+ new SimpleModelBinderProvider(typeof(int), mockIntBinder.Object) { SuppressPrefixCheck = true },
+ new SimpleModelBinderProvider(typeof(DateTime), mockDateTimeBinder.Object) { SuppressPrefixCheck = true });
+
+ mockStringBinder
+ .Setup(b => b.BindModel(context, It.IsAny<ModelBindingContext>()))
+ .Returns((HttpActionContext ec, ModelBindingContext mbc) =>
+ {
+ Assert.Equal(typeof(string), mbc.ModelType);
+ Assert.Equal("theModel.StringProperty", mbc.ModelName);
+ mbc.ValidationNode = new ModelValidationNode(mbc.ModelMetadata, "theModel.StringProperty");
+ mbc.Model = "someStringValue";
+ return true;
+ });
+ mockIntBinder
+ .Setup(b => b.BindModel(context, It.IsAny<ModelBindingContext>()))
+ .Returns((HttpActionContext ec, ModelBindingContext mbc) =>
+ {
+ Assert.Equal(typeof(int), mbc.ModelType);
+ Assert.Equal("theModel.IntProperty", mbc.ModelName);
+ mbc.ValidationNode = new ModelValidationNode(mbc.ModelMetadata, "theModel.IntProperty");
+ mbc.Model = 42;
+ return true;
+ });
+ mockDateTimeBinder
+ .Setup(b => b.BindModel(context, It.IsAny<ModelBindingContext>()))
+ .Returns((HttpActionContext ec, ModelBindingContext mbc) =>
+ {
+ Assert.Equal(typeof(DateTime), mbc.ModelType);
+ Assert.Equal("theModel.DateTimeProperty", mbc.ModelName);
+ return false;
+ });
+ ModelBindingContext parentBindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => dto, typeof(ComplexModelDto)),
+ ModelName = "theModel",
+ };
+ ComplexModelDtoModelBinder binder = new ComplexModelDtoModelBinder();
+
+ // Act
+ bool retVal = binder.BindModel(context, parentBindingContext);
+
+ // Assert
+ Assert.True(retVal);
+ Assert.Equal(dto, parentBindingContext.Model);
+
+ ComplexModelDtoResult stringDtoResult = dto.Results[dto.PropertyMetadata.Where(m => m.ModelType == typeof(string)).First()];
+ Assert.Equal("someStringValue", stringDtoResult.Model);
+ Assert.Equal("theModel.StringProperty", stringDtoResult.ValidationNode.ModelStateKey);
+
+ ComplexModelDtoResult intDtoResult = dto.Results[dto.PropertyMetadata.Where(m => m.ModelType == typeof(int)).First()];
+ Assert.Equal(42, intDtoResult.Model);
+ Assert.Equal("theModel.IntProperty", intDtoResult.ValidationNode.ModelStateKey);
+
+ ComplexModelDtoResult dateTimeDtoResult = dto.Results[dto.PropertyMetadata.Where(m => m.ModelType == typeof(DateTime)).First()];
+ Assert.Null(dateTimeDtoResult);
+ }
+
+ private sealed class MyModel
+ {
+ public string StringProperty { get; set; }
+ public int IntProperty { get; set; }
+ public object ObjectProperty { get; set; } // no binding should happen since no registered binder
+ public DateTime DateTimeProperty { get; set; } // registered binder returns false
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/ModelBinding/Binders/ComplexModelDtoResultTest.cs b/test/System.Web.Http.Test/ModelBinding/Binders/ComplexModelDtoResultTest.cs
new file mode 100644
index 00000000..f9cb4ea5
--- /dev/null
+++ b/test/System.Web.Http.Test/ModelBinding/Binders/ComplexModelDtoResultTest.cs
@@ -0,0 +1,41 @@
+using System.Web.Http.Metadata;
+using System.Web.Http.Metadata.Providers;
+using System.Web.Http.Validation;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.ModelBinding.Binders
+{
+ public class ComplexModelDtoResultTest
+ {
+ [Fact]
+ public void Constructor_ThrowsIfValidationNodeIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ () => new ComplexModelDtoResult("some string", null),
+ "validationNode");
+ }
+
+ [Fact]
+ public void Constructor_SetsProperties()
+ {
+ // Arrange
+ ModelValidationNode validationNode = GetValidationNode();
+
+ // Act
+ ComplexModelDtoResult result = new ComplexModelDtoResult("some string", validationNode);
+
+ // Assert
+ Assert.Equal("some string", result.Model);
+ Assert.Equal(validationNode, result.ValidationNode);
+ }
+
+ private static ModelValidationNode GetValidationNode()
+ {
+ EmptyModelMetadataProvider provider = new EmptyModelMetadataProvider();
+ ModelMetadata metadata = provider.GetMetadataForType(null, typeof(object));
+ return new ModelValidationNode(metadata, "someKey");
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/ModelBinding/Binders/ComplexModelDtoTest.cs b/test/System.Web.Http.Test/ModelBinding/Binders/ComplexModelDtoTest.cs
new file mode 100644
index 00000000..4cce9a1f
--- /dev/null
+++ b/test/System.Web.Http.Test/ModelBinding/Binders/ComplexModelDtoTest.cs
@@ -0,0 +1,53 @@
+using System.Linq;
+using System.Web.Http.Metadata;
+using System.Web.Http.Metadata.Providers;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.ModelBinding.Binders
+{
+ public class ComplexModelDtoTest
+ {
+ [Fact]
+ public void ConstructorThrowsIfModelMetadataIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ () => new ComplexModelDto(null, Enumerable.Empty<ModelMetadata>()),
+ "modelMetadata");
+ }
+
+ [Fact]
+ public void ConstructorThrowsIfPropertyMetadataIsNull()
+ {
+ // Arrange
+ ModelMetadata modelMetadata = GetModelMetadata();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ () => new ComplexModelDto(modelMetadata, null),
+ "propertyMetadata");
+ }
+
+ [Fact]
+ public void ConstructorSetsProperties()
+ {
+ // Arrange
+ ModelMetadata modelMetadata = GetModelMetadata();
+ ModelMetadata[] propertyMetadata = new ModelMetadata[0];
+
+ // Act
+ ComplexModelDto dto = new ComplexModelDto(modelMetadata, propertyMetadata);
+
+ // Assert
+ Assert.Equal(modelMetadata, dto.ModelMetadata);
+ Assert.Equal(propertyMetadata, dto.PropertyMetadata.ToArray());
+ Assert.Empty(dto.Results);
+ }
+
+ private static ModelMetadata GetModelMetadata()
+ {
+ return new ModelMetadata(new EmptyModelMetadataProvider(), typeof(object), null, typeof(object), "PropertyName");
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/ModelBinding/Binders/DictionaryModelBinderProviderTest.cs b/test/System.Web.Http.Test/ModelBinding/Binders/DictionaryModelBinderProviderTest.cs
new file mode 100644
index 00000000..d9c73bc0
--- /dev/null
+++ b/test/System.Web.Http.Test/ModelBinding/Binders/DictionaryModelBinderProviderTest.cs
@@ -0,0 +1,76 @@
+using System.Collections.Generic;
+using System.Web.Http.Metadata.Providers;
+using System.Web.Http.Util;
+using Xunit;
+
+namespace System.Web.Http.ModelBinding.Binders
+{
+ public class DictionaryModelBinderProviderTest
+ {
+ [Fact]
+ public void GetBinder_CorrectModelTypeAndValueProviderEntries_ReturnsBinder()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(IDictionary<int, string>)),
+ ModelName = "foo",
+ ValueProvider = new SimpleHttpValueProvider
+ {
+ { "foo[0]", "42" },
+ }
+ };
+
+ DictionaryModelBinderProvider binderProvider = new DictionaryModelBinderProvider();
+
+ // Act
+ IModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.IsType<DictionaryModelBinder<int, string>>(binder);
+ }
+
+ [Fact]
+ public void GetBinder_ModelTypeIsIncorrect_ReturnsNull()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int)),
+ ModelName = "foo",
+ ValueProvider = new SimpleHttpValueProvider
+ {
+ { "foo[0]", "42" },
+ }
+ };
+
+ DictionaryModelBinderProvider binderProvider = new DictionaryModelBinderProvider();
+
+ // Act
+ IModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(binder);
+ }
+
+ [Fact]
+ public void GetBinder_ValueProviderDoesNotContainPrefix_ReturnsNull()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(IDictionary<int, string>)),
+ ModelName = "foo",
+ ValueProvider = new SimpleHttpValueProvider()
+ };
+
+ DictionaryModelBinderProvider binderProvider = new DictionaryModelBinderProvider();
+
+ // Act
+ IModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(binder);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/ModelBinding/Binders/DictionaryModelBinderTest.cs b/test/System.Web.Http.Test/ModelBinding/Binders/DictionaryModelBinderTest.cs
new file mode 100644
index 00000000..38efd146
--- /dev/null
+++ b/test/System.Web.Http.Test/ModelBinding/Binders/DictionaryModelBinderTest.cs
@@ -0,0 +1,51 @@
+using System.Collections.Generic;
+using System.Web.Http.Controllers;
+using System.Web.Http.Metadata.Providers;
+using System.Web.Http.Util;
+using Moq;
+using Xunit;
+
+namespace System.Web.Http.ModelBinding.Binders
+{
+ public class DictionaryModelBinderTest
+ {
+ [Fact]
+ public void BindModel()
+ {
+ // Arrange
+ Mock<IModelBinder> mockKvpBinder = new Mock<IModelBinder>();
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(IDictionary<int, string>)),
+ ModelName = "someName",
+ ValueProvider = new SimpleHttpValueProvider
+ {
+ { "someName[0]", new KeyValuePair<int, string>(42, "forty-two") },
+ { "someName[1]", new KeyValuePair<int, string>(84, "eighty-four") }
+ }
+ };
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ context.ControllerContext.Configuration.ServiceResolver.SetService(typeof(ModelBinderProvider), (new SimpleModelBinderProvider(typeof(KeyValuePair<int, string>), mockKvpBinder.Object)));
+
+ mockKvpBinder
+ .Setup(o => o.BindModel(context, It.IsAny<ModelBindingContext>()))
+ .Returns((HttpActionContext cc, ModelBindingContext mbc) =>
+ {
+ mbc.Model = mbc.ValueProvider.GetValue(mbc.ModelName).ConvertTo(mbc.ModelType);
+ return true;
+ });
+
+ // Act
+ bool retVal = new DictionaryModelBinder<int, string>().BindModel(context, bindingContext);
+
+ // Assert
+ Assert.True(retVal);
+
+ var dictionary = Assert.IsAssignableFrom<IDictionary<int, string>>(bindingContext.Model);
+ Assert.NotNull(dictionary);
+ Assert.Equal(2, dictionary.Count);
+ Assert.Equal("forty-two", dictionary[42]);
+ Assert.Equal("eighty-four", dictionary[84]);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/ModelBinding/Binders/GenericModelBinderProviderTest.cs b/test/System.Web.Http.Test/ModelBinding/Binders/GenericModelBinderProviderTest.cs
new file mode 100644
index 00000000..6d83cda8
--- /dev/null
+++ b/test/System.Web.Http.Test/ModelBinding/Binders/GenericModelBinderProviderTest.cs
@@ -0,0 +1,251 @@
+using System.Collections.Generic;
+using System.Web.Http.Metadata.Providers;
+using System.Web.Http.Util;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.ModelBinding.Binders
+{
+ public class GenericModelBinderProviderTest
+ {
+ [Fact]
+ public void Constructor_WithFactory_ThrowsIfModelBinderFactoryIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ () => new GenericModelBinderProvider(typeof(List<>), (Func<Type[], IModelBinder>)null),
+ "modelBinderFactory");
+ }
+
+ [Fact]
+ public void Constructor_WithFactory_ThrowsIfModelTypeIsNotOpenGeneric()
+ {
+ // Act & assert
+ Assert.Throws<ArgumentException>(
+ () => new GenericModelBinderProvider(typeof(List<int>), _ => null),
+ @"The type 'System.Collections.Generic.List`1[System.Int32]' is not an open generic type.
+Parameter name: modelType");
+ }
+
+ [Fact]
+ public void Constructor_WithFactory_ThrowsIfModelTypeIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ () => new GenericModelBinderProvider(null, _ => null),
+ "modelType");
+ }
+
+ [Fact]
+ public void Constructor_WithInstance_ThrowsIfModelBinderIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ () => new GenericModelBinderProvider(typeof(List<>), (IModelBinder)null),
+ "modelBinder");
+ }
+
+ [Fact]
+ public void Constructor_WithInstance_ThrowsIfModelTypeIsNotOpenGeneric()
+ {
+ // Act & assert
+ Assert.Throws<ArgumentException>(
+ () => new GenericModelBinderProvider(typeof(List<int>), new MutableObjectModelBinder()),
+ @"The type 'System.Collections.Generic.List`1[System.Int32]' is not an open generic type.
+Parameter name: modelType");
+ }
+
+ [Fact]
+ public void Constructor_WithInstance_ThrowsIfModelTypeIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ () => new GenericModelBinderProvider(null, new MutableObjectModelBinder()),
+ "modelType");
+ }
+
+ [Fact]
+ public void Constructor_WithType_ThrowsIfModelBinderTypeIsNotModelBinder()
+ {
+ // Act & assert
+ Assert.Throws<ArgumentException>(
+ () => new GenericModelBinderProvider(typeof(List<>), typeof(string)),
+ @"The type 'System.String' does not implement the interface 'System.Web.Http.ModelBinding.IModelBinder'.
+Parameter name: modelBinderType");
+ }
+
+ [Fact]
+ public void Constructor_WithType_ThrowsIfModelBinderTypeIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ () => new GenericModelBinderProvider(typeof(List<>), (Type)null),
+ "modelBinderType");
+ }
+
+ [Fact]
+ public void Constructor_WithType_ThrowsIfModelBinderTypeTypeArgumentMismatch()
+ {
+ // Act & assert
+ Assert.Throws<ArgumentException>(
+ () => new GenericModelBinderProvider(typeof(List<>), typeof(DictionaryModelBinder<,>)),
+ @"The open model type 'System.Collections.Generic.List`1[T]' has 1 generic type argument(s), but the open binder type 'System.Web.Http.ModelBinding.Binders.DictionaryModelBinder`2[TKey,TValue]' has 2 generic type argument(s). The binder type must not be an open generic type or must have the same number of generic arguments as the open model type.
+Parameter name: modelBinderType");
+ }
+
+ [Fact]
+ public void Constructor_WithType_ThrowsIfModelTypeIsNotOpenGeneric()
+ {
+ // Act & assert
+ Assert.Throws<ArgumentException>(
+ () => new GenericModelBinderProvider(typeof(List<int>), typeof(MutableObjectModelBinder)),
+ @"The type 'System.Collections.Generic.List`1[System.Int32]' is not an open generic type.
+Parameter name: modelType");
+ }
+
+ [Fact]
+ public void Constructor_WithType_ThrowsIfModelTypeIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ () => new GenericModelBinderProvider(null, typeof(MutableObjectModelBinder)),
+ "modelType");
+ }
+
+ [Fact]
+ public void GetBinder_TypeDoesNotMatch_ModelTypeIsInterface_ReturnsNull()
+ {
+ // Arrange
+ GenericModelBinderProvider provider = new GenericModelBinderProvider(typeof(IEnumerable<>), typeof(CollectionModelBinder<>))
+ {
+ SuppressPrefixCheck = true
+ };
+ ModelBindingContext bindingContext = GetBindingContext(typeof(object));
+
+ // Act
+ IModelBinder binder = provider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(binder);
+ }
+
+ [Fact]
+ public void GetBinder_TypeDoesNotMatch_ModelTypeIsNotInterface_ReturnsNull()
+ {
+ // Arrange
+ GenericModelBinderProvider provider = new GenericModelBinderProvider(typeof(List<>), typeof(CollectionModelBinder<>))
+ {
+ SuppressPrefixCheck = true
+ };
+ ModelBindingContext bindingContext = GetBindingContext(typeof(object));
+
+ // Act
+ IModelBinder binder = provider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(binder);
+ }
+
+ [Fact]
+ public void GetBinder_TypeMatches_PrefixNotFound_ReturnsNull()
+ {
+ // Arrange
+ IModelBinder binderInstance = new Mock<IModelBinder>().Object;
+ GenericModelBinderProvider provider = new GenericModelBinderProvider(typeof(List<>), binderInstance);
+
+ ModelBindingContext bindingContext = GetBindingContext(typeof(List<int>));
+ bindingContext.ValueProvider = new SimpleHttpValueProvider();
+
+ // Act
+ IModelBinder returnedBinder = provider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(returnedBinder);
+ }
+
+ [Fact]
+ public void GetBinder_TypeMatches_Success_Factory_ReturnsBinder()
+ {
+ // Arrange
+ IModelBinder binderInstance = new Mock<IModelBinder>().Object;
+
+ Func<Type[], IModelBinder> binderFactory = typeArguments =>
+ {
+ Assert.Equal(new[] { typeof(int) }, typeArguments);
+ return binderInstance;
+ };
+
+ GenericModelBinderProvider provider = new GenericModelBinderProvider(typeof(IList<>), binderFactory)
+ {
+ SuppressPrefixCheck = true
+ };
+
+ ModelBindingContext bindingContext = GetBindingContext(typeof(List<int>));
+
+ // Act
+ IModelBinder returnedBinder = provider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Same(binderInstance, returnedBinder);
+ }
+
+ [Fact]
+ public void GetBinder_TypeMatches_Success_Instance_ReturnsBinder()
+ {
+ // Arrange
+ IModelBinder binderInstance = new Mock<IModelBinder>().Object;
+
+ GenericModelBinderProvider provider = new GenericModelBinderProvider(typeof(List<>), binderInstance)
+ {
+ SuppressPrefixCheck = true
+ };
+
+ ModelBindingContext bindingContext = GetBindingContext(typeof(List<int>));
+
+ // Act
+ IModelBinder returnedBinder = provider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Same(binderInstance, returnedBinder);
+ }
+
+ [Fact]
+ public void GetBinder_TypeMatches_Success_TypeActivation_ReturnsBinder()
+ {
+ // Arrange
+ GenericModelBinderProvider provider = new GenericModelBinderProvider(typeof(List<>), typeof(CollectionModelBinder<>))
+ {
+ SuppressPrefixCheck = true
+ };
+
+ ModelBindingContext bindingContext = GetBindingContext(typeof(List<int>));
+
+ // Act
+ IModelBinder returnedBinder = provider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.IsType<CollectionModelBinder<int>>(returnedBinder);
+ }
+
+ [Fact]
+ public void GetBinderThrowsIfBindingContextIsNull()
+ {
+ // Arrange
+ GenericModelBinderProvider provider = new GenericModelBinderProvider(typeof(IEnumerable<>), typeof(CollectionModelBinder<>));
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ () => provider.GetBinder(null, null),
+ "bindingContext");
+ }
+
+ private static ModelBindingContext GetBindingContext(Type modelType)
+ {
+ return new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => null, modelType)
+ };
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/ModelBinding/Binders/KeyValuePairModelBinderProviderTest.cs b/test/System.Web.Http.Test/ModelBinding/Binders/KeyValuePairModelBinderProviderTest.cs
new file mode 100644
index 00000000..dea8b70e
--- /dev/null
+++ b/test/System.Web.Http.Test/ModelBinding/Binders/KeyValuePairModelBinderProviderTest.cs
@@ -0,0 +1,104 @@
+using System.Collections.Generic;
+using System.Web.Http.Metadata.Providers;
+using System.Web.Http.Util;
+using Xunit;
+
+namespace System.Web.Http.ModelBinding.Binders
+{
+ public class KeyValuePairModelBinderProviderTest
+ {
+ [Fact]
+ public void GetBinder_CorrectModelTypeAndValueProviderEntries_ReturnsBinder()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(KeyValuePair<int, string>)),
+ ModelName = "foo",
+ ValueProvider = new SimpleHttpValueProvider
+ {
+ { "foo.key", 42 },
+ { "foo.value", "someValue" }
+ }
+ };
+
+ KeyValuePairModelBinderProvider binderProvider = new KeyValuePairModelBinderProvider();
+
+ // Act
+ IModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.IsType<KeyValuePairModelBinder<int, string>>(binder);
+ }
+
+ [Fact]
+ public void GetBinder_ModelTypeIsIncorrect_ReturnsNull()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(List<int>)),
+ ModelName = "foo",
+ ValueProvider = new SimpleHttpValueProvider
+ {
+ { "foo.key", 42 },
+ { "foo.value", "someValue" }
+ }
+ };
+
+ KeyValuePairModelBinderProvider binderProvider = new KeyValuePairModelBinderProvider();
+
+ // Act
+ IModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(binder);
+ }
+
+ [Fact]
+ public void GetBinder_ValueProviderDoesNotContainKeyProperty_ReturnsNull()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(KeyValuePair<int, string>)),
+ ModelName = "foo",
+ ValueProvider = new SimpleHttpValueProvider
+ {
+ { "foo.value", "someValue" }
+ }
+ };
+
+ KeyValuePairModelBinderProvider binderProvider = new KeyValuePairModelBinderProvider();
+
+ // Act
+ IModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(binder);
+ }
+
+ [Fact]
+ public void GetBinder_ValueProviderDoesNotContainValueProperty_ReturnsNull()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(KeyValuePair<int, string>)),
+ ModelName = "foo",
+ ValueProvider = new SimpleHttpValueProvider
+ {
+ { "foo.key", 42 }
+ }
+ };
+
+ KeyValuePairModelBinderProvider binderProvider = new KeyValuePairModelBinderProvider();
+
+ // Act
+ IModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(binder);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/ModelBinding/Binders/KeyValuePairModelBinderTest.cs b/test/System.Web.Http.Test/ModelBinding/Binders/KeyValuePairModelBinderTest.cs
new file mode 100644
index 00000000..4b5fba6c
--- /dev/null
+++ b/test/System.Web.Http.Test/ModelBinding/Binders/KeyValuePairModelBinderTest.cs
@@ -0,0 +1,110 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Web.Http.Controllers;
+using System.Web.Http.Metadata.Providers;
+using System.Web.Http.Util;
+using Moq;
+using Xunit;
+
+namespace System.Web.Http.ModelBinding.Binders
+{
+ public class KeyValuePairModelBinderTest
+ {
+ [Fact]
+ public void BindModel_MissingKey_ReturnsFalse()
+ {
+ // Arrange
+ KeyValuePairModelBinder<int, string> binder = new KeyValuePairModelBinder<int, string>();
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(KeyValuePair<int, string>)),
+ ModelName = "someName",
+ ValueProvider = new SimpleHttpValueProvider()
+ };
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ context.ControllerContext.Configuration.ServiceResolver.SetService(typeof(ModelBinderProvider), new SimpleModelBinderProvider(typeof(KeyValuePair<int, string>), binder));
+
+ // Act
+ bool retVal = binder.BindModel(context, bindingContext);
+
+ // Assert
+ Assert.False(retVal);
+ Assert.Null(bindingContext.Model);
+ Assert.Empty(bindingContext.ValidationNode.ChildNodes);
+ }
+
+ [Fact]
+ public void BindModel_MissingValue_ReturnsTrue()
+ {
+ // Arrange
+ Mock<IModelBinder> mockIntBinder = new Mock<IModelBinder>();
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(KeyValuePair<int, string>)),
+ ModelName = "someName",
+ ValueProvider = new SimpleHttpValueProvider()
+ };
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ context.ControllerContext.Configuration.ServiceResolver.SetService(typeof(ModelBinderProvider), new SimpleModelBinderProvider(typeof(int), mockIntBinder.Object) { SuppressPrefixCheck = true });
+
+ mockIntBinder
+ .Setup(o => o.BindModel(context, It.IsAny<ModelBindingContext>()))
+ .Returns((HttpActionContext cc, ModelBindingContext mbc) =>
+ {
+ mbc.Model = 42;
+ return true;
+ });
+ KeyValuePairModelBinder<int, string> binder = new KeyValuePairModelBinder<int, string>();
+
+ // Act
+ bool retVal = binder.BindModel(context, bindingContext);
+
+ // Assert
+ Assert.True(retVal);
+ Assert.Null(bindingContext.Model);
+ Assert.Equal(new[] { "someName.key" }, bindingContext.ValidationNode.ChildNodes.Select(n => n.ModelStateKey).ToArray());
+ }
+
+ [Fact]
+ public void BindModel_SubBindingSucceeds()
+ {
+ // Arrange
+ Mock<IModelBinder> mockIntBinder = new Mock<IModelBinder>();
+ Mock<IModelBinder> mockStringBinder = new Mock<IModelBinder>();
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(KeyValuePair<int, string>)),
+ ModelName = "someName",
+ ValueProvider = new SimpleHttpValueProvider()
+ };
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ context.ControllerContext.Configuration.ServiceResolver.SetServices(typeof(ModelBinderProvider),
+ new SimpleModelBinderProvider(typeof(int), mockIntBinder.Object) { SuppressPrefixCheck = true },
+ new SimpleModelBinderProvider(typeof(string), mockStringBinder.Object) { SuppressPrefixCheck = true });
+
+ mockIntBinder
+ .Setup(o => o.BindModel(context, It.IsAny<ModelBindingContext>()))
+ .Returns((HttpActionContext cc, ModelBindingContext mbc) =>
+ {
+ mbc.Model = 42;
+ return true;
+ });
+ mockStringBinder
+ .Setup(o => o.BindModel(context, It.IsAny<ModelBindingContext>()))
+ .Returns((HttpActionContext cc, ModelBindingContext mbc) =>
+ {
+ mbc.Model = "forty-two";
+ return true;
+ });
+ KeyValuePairModelBinder<int, string> binder = new KeyValuePairModelBinder<int, string>();
+
+ // Act
+ bool retVal = binder.BindModel(context, bindingContext);
+
+ // Assert
+ Assert.True(retVal);
+ Assert.Equal(new KeyValuePair<int, string>(42, "forty-two"), bindingContext.Model);
+ Assert.Equal(new[] { "someName.key", "someName.value" }, bindingContext.ValidationNode.ChildNodes.Select(n => n.ModelStateKey).ToArray());
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/ModelBinding/Binders/KeyValuePairModelBinderUtilTest.cs b/test/System.Web.Http.Test/ModelBinding/Binders/KeyValuePairModelBinderUtilTest.cs
new file mode 100644
index 00000000..a4adce06
--- /dev/null
+++ b/test/System.Web.Http.Test/ModelBinding/Binders/KeyValuePairModelBinderUtilTest.cs
@@ -0,0 +1,105 @@
+using System.Web.Http.Controllers;
+using System.Web.Http.Internal;
+using System.Web.Http.Metadata.Providers;
+using System.Web.Http.Util;
+using Moq;
+using Xunit;
+
+namespace System.Web.Http.ModelBinding.Binders
+{
+ public class KeyValuePairModelBinderUtilTest
+ {
+ [Fact]
+ public void TryBindStrongModel_BinderExists_BinderReturnsCorrectlyTypedObject_ReturnsTrue()
+ {
+ // Arrange
+ Mock<IModelBinder> mockIntBinder = new Mock<IModelBinder>();
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int)),
+ ModelName = "someName",
+ ModelState = new ModelStateDictionary(),
+ ValueProvider = new SimpleHttpValueProvider()
+ };
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ context.ControllerContext.Configuration.ServiceResolver.SetService(typeof(ModelBinderProvider), new SimpleModelBinderProvider(typeof(int), mockIntBinder.Object) { SuppressPrefixCheck = true });
+
+ mockIntBinder
+ .Setup(o => o.BindModel(context, It.IsAny<ModelBindingContext>()))
+ .Returns((HttpActionContext cc, ModelBindingContext mbc) =>
+ {
+ Assert.Equal("someName.key", mbc.ModelName);
+ mbc.Model = 42;
+ return true;
+ });
+
+ // Act
+ int model;
+ bool retVal = context.TryBindStrongModel(bindingContext, "key", new EmptyModelMetadataProvider(), out model);
+
+ // Assert
+ Assert.True(retVal);
+ Assert.Equal(42, model);
+ Assert.Single(bindingContext.ValidationNode.ChildNodes);
+ Assert.Empty(bindingContext.ModelState);
+ }
+
+ [Fact]
+ public void TryBindStrongModel_BinderExists_BinderReturnsIncorrectlyTypedObject_ReturnsTrue()
+ {
+ // Arrange
+ Mock<IModelBinder> mockIntBinder = new Mock<IModelBinder>();
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int)),
+ ModelName = "someName",
+ ModelState = new ModelStateDictionary(),
+ ValueProvider = new SimpleHttpValueProvider()
+ };
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ context.ControllerContext.Configuration.ServiceResolver.SetService(typeof(ModelBinderProvider), new SimpleModelBinderProvider(typeof(int), mockIntBinder.Object) { SuppressPrefixCheck = true });
+
+ mockIntBinder
+ .Setup(o => o.BindModel(context, It.IsAny<ModelBindingContext>()))
+ .Returns((HttpActionContext cc, ModelBindingContext mbc) =>
+ {
+ Assert.Equal("someName.key", mbc.ModelName);
+ return true;
+ });
+
+ // Act
+ int model;
+ bool retVal = context.TryBindStrongModel(bindingContext, "key", new EmptyModelMetadataProvider(), out model);
+
+ // Assert
+ Assert.True(retVal);
+ Assert.Equal(default(int), model);
+ Assert.Single(bindingContext.ValidationNode.ChildNodes);
+ Assert.Empty(bindingContext.ModelState);
+ }
+
+ [Fact]
+ public void TryBindStrongModel_NoBinder_ReturnsFalse()
+ {
+ // Arrange
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int)),
+ ModelName = "someName",
+ ModelState = new ModelStateDictionary(),
+ ValueProvider = new SimpleHttpValueProvider()
+ };
+
+ // Act
+ int model;
+ bool retVal = context.TryBindStrongModel(bindingContext, "key", new EmptyModelMetadataProvider(), out model);
+
+ // Assert
+ Assert.False(retVal);
+ Assert.Equal(default(int), model);
+ Assert.Empty(bindingContext.ValidationNode.ChildNodes);
+ Assert.Empty(bindingContext.ModelState);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/ModelBinding/Binders/MutableObjectModelBinderProviderTest.cs b/test/System.Web.Http.Test/ModelBinding/Binders/MutableObjectModelBinderProviderTest.cs
new file mode 100644
index 00000000..60863755
--- /dev/null
+++ b/test/System.Web.Http.Test/ModelBinding/Binders/MutableObjectModelBinderProviderTest.cs
@@ -0,0 +1,103 @@
+using System.Web.Http.Metadata.Providers;
+using System.Web.Http.Util;
+using Xunit;
+
+namespace System.Web.Http.ModelBinding.Binders
+{
+ public class MutableObjectModelBinderProviderTest
+ {
+ [Fact]
+ public void GetBinder_NoPrefixInValueProvider_ReturnsNull()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => 42, typeof(int)),
+ ModelName = "foo",
+ ValueProvider = new SimpleHttpValueProvider()
+ };
+
+ MutableObjectModelBinderProvider binderProvider = new MutableObjectModelBinderProvider();
+
+ // Act
+ IModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(binder);
+ }
+
+ [Fact]
+ public void GetBinder_PrefixInValueProvider_ComplexType_ReturnsBinder()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => new MutableTestType(), typeof(MutableTestType)),
+ ModelName = "foo",
+ ValueProvider = new SimpleHttpValueProvider
+ {
+ { "foo.bar", "someValue" }
+ }
+ };
+
+ MutableObjectModelBinderProvider binderProvider = new MutableObjectModelBinderProvider();
+
+ // Act
+ IModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.NotNull(binder);
+ Assert.IsType<MutableObjectModelBinder>(binder);
+ }
+
+ [Fact]
+ public void GetBinder_PrefixInValueProvider_SimpleType_ReturnsNull()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => 42, typeof(int)),
+ ModelName = "foo",
+ ValueProvider = new SimpleHttpValueProvider
+ {
+ { "foo.bar", "someValue" }
+ }
+ };
+
+ MutableObjectModelBinderProvider binderProvider = new MutableObjectModelBinderProvider();
+
+ // Act
+ IModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(binder);
+ }
+
+ [Fact]
+ public void GetBinder_TypeIsComplexModelDto_ReturnsNull()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(ComplexModelDto)),
+ ModelName = "foo",
+ ValueProvider = new SimpleHttpValueProvider
+ {
+ { "foo.bar", "someValue" }
+ }
+ };
+
+ MutableObjectModelBinderProvider binderProvider = new MutableObjectModelBinderProvider();
+
+ // Act
+ IModelBinder binder = binderProvider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(binder);
+ }
+
+ class MutableTestType
+ {
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/ModelBinding/Binders/MutableObjectModelBinderTest.cs b/test/System.Web.Http.Test/ModelBinding/Binders/MutableObjectModelBinderTest.cs
new file mode 100644
index 00000000..081eda7a
--- /dev/null
+++ b/test/System.Web.Http.Test/ModelBinding/Binders/MutableObjectModelBinderTest.cs
@@ -0,0 +1,737 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Web.Http.Controllers;
+using System.Web.Http.Metadata;
+using System.Web.Http.Metadata.Providers;
+using System.Web.Http.Validation;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.ModelBinding.Binders
+{
+ public class MutableObjectModelBinderTest
+ {
+ [Fact]
+ public void BindModel()
+ {
+ // Arrange
+ Mock<IModelBinder> mockDtoBinder = new Mock<IModelBinder>();
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = GetMetadataForObject(new Person()),
+ ModelName = "someName"
+ };
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ context.ControllerContext.Configuration.ServiceResolver.SetService(typeof(ModelBinderProvider), new SimpleModelBinderProvider(typeof(ComplexModelDto), mockDtoBinder.Object) { SuppressPrefixCheck = true });
+
+ mockDtoBinder
+ .Setup(o => o.BindModel(context, It.IsAny<ModelBindingContext>()))
+ .Returns((HttpActionContext cc, ModelBindingContext mbc2) =>
+ {
+ return true; // just return the DTO unchanged
+ });
+
+ Mock<TestableMutableObjectModelBinder> mockTestableBinder = new Mock<TestableMutableObjectModelBinder> { CallBase = true };
+ mockTestableBinder.Setup(o => o.EnsureModelPublic(context, bindingContext)).Verifiable();
+ mockTestableBinder.Setup(o => o.GetMetadataForPropertiesPublic(context, bindingContext)).Returns(new ModelMetadata[0]).Verifiable();
+ TestableMutableObjectModelBinder testableBinder = mockTestableBinder.Object;
+ testableBinder.MetadataProvider = new CachedDataAnnotationsModelMetadataProvider();
+
+ // Act
+ bool retValue = testableBinder.BindModel(context, bindingContext);
+
+ // Assert
+ Assert.True(retValue);
+ Assert.IsType<Person>(bindingContext.Model);
+ Assert.True(bindingContext.ValidationNode.ValidateAllProperties);
+ mockTestableBinder.Verify();
+ }
+
+ [Fact]
+ public void CanUpdateProperty_HasPublicSetter_ReturnsTrue()
+ {
+ // Arrange
+ ModelMetadata propertyMetadata = GetMetadataForCanUpdateProperty("ReadWriteString");
+
+ // Act
+ bool canUpdate = MutableObjectModelBinder.CanUpdatePropertyInternal(propertyMetadata);
+
+ // Assert
+ Assert.True(canUpdate);
+ }
+
+ [Fact]
+ public void CanUpdateProperty_ReadOnlyArray_ReturnsFalse()
+ {
+ // Arrange
+ ModelMetadata propertyMetadata = GetMetadataForCanUpdateProperty("ReadOnlyArray");
+
+ // Act
+ bool canUpdate = MutableObjectModelBinder.CanUpdatePropertyInternal(propertyMetadata);
+
+ // Assert
+ Assert.False(canUpdate);
+ }
+
+ [Fact]
+ public void CanUpdateProperty_ReadOnlyReferenceTypeNotBlacklisted_ReturnsTrue()
+ {
+ // Arrange
+ ModelMetadata propertyMetadata = GetMetadataForCanUpdateProperty("ReadOnlyObject");
+
+ // Act
+ bool canUpdate = MutableObjectModelBinder.CanUpdatePropertyInternal(propertyMetadata);
+
+ // Assert
+ Assert.True(canUpdate);
+ }
+
+ [Fact]
+ public void CanUpdateProperty_ReadOnlyString_ReturnsFalse()
+ {
+ // Arrange
+ ModelMetadata propertyMetadata = GetMetadataForCanUpdateProperty("ReadOnlyString");
+
+ // Act
+ bool canUpdate = MutableObjectModelBinder.CanUpdatePropertyInternal(propertyMetadata);
+
+ // Assert
+ Assert.False(canUpdate);
+ }
+
+ [Fact]
+ public void CanUpdateProperty_ReadOnlyValueType_ReturnsFalse()
+ {
+ // Arrange
+ ModelMetadata propertyMetadata = GetMetadataForCanUpdateProperty("ReadOnlyInt");
+
+ // Act
+ bool canUpdate = MutableObjectModelBinder.CanUpdatePropertyInternal(propertyMetadata);
+
+ // Assert
+ Assert.False(canUpdate);
+ }
+
+ [Fact]
+ public void CreateModel()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = GetMetadataForType(typeof(Person))
+ };
+
+ TestableMutableObjectModelBinder testableBinder = new TestableMutableObjectModelBinder();
+
+ // Act
+ object retModel = testableBinder.CreateModelPublic(null, bindingContext);
+
+ // Assert
+ Assert.IsType<Person>(retModel);
+ }
+
+ [Fact]
+ public void EnsureModel_ModelIsNotNull_DoesNothing()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = GetMetadataForObject(new Person())
+ };
+
+ Mock<TestableMutableObjectModelBinder> mockTestableBinder = new Mock<TestableMutableObjectModelBinder> { CallBase = true };
+ TestableMutableObjectModelBinder testableBinder = mockTestableBinder.Object;
+
+ // Act
+ object originalModel = bindingContext.Model;
+ testableBinder.EnsureModelPublic(null, bindingContext);
+ object newModel = bindingContext.Model;
+
+ // Assert
+ Assert.Same(originalModel, newModel);
+ mockTestableBinder.Verify(o => o.CreateModelPublic(null, bindingContext), Times.Never());
+ }
+
+ [Fact]
+ public void EnsureModel_ModelIsNull_CallsCreateModel()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = GetMetadataForType(typeof(Person))
+ };
+
+ Mock<TestableMutableObjectModelBinder> mockTestableBinder = new Mock<TestableMutableObjectModelBinder> { CallBase = true };
+ mockTestableBinder.Setup(o => o.CreateModelPublic(null, bindingContext)).Returns(new Person()).Verifiable();
+ TestableMutableObjectModelBinder testableBinder = mockTestableBinder.Object;
+
+ // Act
+ object originalModel = bindingContext.Model;
+ testableBinder.EnsureModelPublic(null, bindingContext);
+ object newModel = bindingContext.Model;
+
+ // Assert
+ Assert.Null(originalModel);
+ Assert.IsType<Person>(newModel);
+ mockTestableBinder.Verify();
+ }
+
+ [Fact]
+ public void GetMetadataForProperties_WithBindAttribute()
+ {
+ // Arrange
+ string[] expectedPropertyNames = new[] { "FirstName", "LastName" };
+
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = GetMetadataForType(typeof(PersonWithBindExclusion))
+ };
+
+ TestableMutableObjectModelBinder testableBinder = new TestableMutableObjectModelBinder();
+
+ // Act
+ IEnumerable<ModelMetadata> propertyMetadatas = testableBinder.GetMetadataForPropertiesPublic(null, bindingContext);
+ string[] returnedPropertyNames = propertyMetadatas.Select(o => o.PropertyName).ToArray();
+
+ // Assert
+ Assert.Equal(expectedPropertyNames, returnedPropertyNames);
+ }
+
+ [Fact]
+ public void GetMetadataForProperties_WithoutBindAttribute()
+ {
+ // Arrange
+ string[] expectedPropertyNames = new[] { "DateOfBirth", "DateOfDeath", "ValueTypeRequired", "FirstName", "LastName", "PropertyWithDefaultValue" };
+
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = GetMetadataForType(typeof(Person))
+ };
+
+ TestableMutableObjectModelBinder testableBinder = new TestableMutableObjectModelBinder();
+
+ // Act
+ IEnumerable<ModelMetadata> propertyMetadatas = testableBinder.GetMetadataForPropertiesPublic(null, bindingContext);
+ string[] returnedPropertyNames = propertyMetadatas.Select(o => o.PropertyName).ToArray();
+
+ // Assert
+ Assert.Equal(expectedPropertyNames, returnedPropertyNames);
+ }
+
+ [Fact]
+ public void GetRequiredPropertiesCollection_MixedAttributes()
+ {
+ // Arrange
+ Type modelType = typeof(ModelWithMixedBindingBehaviors);
+
+ // Act
+ HashSet<string> requiredProperties;
+ HashSet<string> skipProperties;
+ MutableObjectModelBinder.GetRequiredPropertiesCollection(modelType, out requiredProperties, out skipProperties);
+
+ // Assert
+ Assert.Equal(new[] { "Required" }, requiredProperties.ToArray());
+ Assert.Equal(new[] { "Never" }, skipProperties.ToArray());
+ }
+
+ [Fact]
+ public void NullCheckFailedHandler_ModelStateAlreadyInvalid_DoesNothing()
+ {
+ // Arrange
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ context.ModelState.AddModelError("foo.bar", "Some existing error.");
+
+ ModelMetadata modelMetadata = GetMetadataForType(typeof(Person));
+ ModelValidationNode validationNode = new ModelValidationNode(modelMetadata, "foo");
+ ModelValidatedEventArgs e = new ModelValidatedEventArgs(context, null /* parentNode */);
+
+ // Act
+ EventHandler<ModelValidatedEventArgs> handler = MutableObjectModelBinder.CreateNullCheckFailedHandler(modelMetadata, null /* incomingValue */);
+ handler(validationNode, e);
+
+ // Assert
+ Assert.False(context.ModelState.ContainsKey("foo"));
+ }
+
+ [Fact]
+ public void NullCheckFailedHandler_ModelStateValid_AddsErrorString()
+ {
+ // Arrange
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ ModelMetadata modelMetadata = GetMetadataForType(typeof(Person));
+ ModelValidationNode validationNode = new ModelValidationNode(modelMetadata, "foo");
+ ModelValidatedEventArgs e = new ModelValidatedEventArgs(context, null /* parentNode */);
+
+ // Act
+ EventHandler<ModelValidatedEventArgs> handler = MutableObjectModelBinder.CreateNullCheckFailedHandler(modelMetadata, null /* incomingValue */);
+ handler(validationNode, e);
+
+ // Assert
+ Assert.True(context.ModelState.ContainsKey("foo"));
+ Assert.Equal("A value is required.", context.ModelState["foo"].Errors[0].ErrorMessage);
+ }
+
+ [Fact]
+ public void NullCheckFailedHandler_ModelStateValid_CallbackReturnsNull_DoesNothing()
+ {
+ // Arrange
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ ModelMetadata modelMetadata = GetMetadataForType(typeof(Person));
+ ModelValidationNode validationNode = new ModelValidationNode(modelMetadata, "foo");
+ ModelValidatedEventArgs e = new ModelValidatedEventArgs(context, null /* parentNode */);
+
+ // Act
+ ModelBinderErrorMessageProvider originalProvider = ModelBinderConfig.ValueRequiredErrorMessageProvider;
+ try
+ {
+ ModelBinderConfig.ValueRequiredErrorMessageProvider = (ec, mm, value) => null;
+ EventHandler<ModelValidatedEventArgs> handler = MutableObjectModelBinder.CreateNullCheckFailedHandler(modelMetadata, null /* incomingValue */);
+ handler(validationNode, e);
+ }
+ finally
+ {
+ ModelBinderConfig.ValueRequiredErrorMessageProvider = originalProvider;
+ }
+
+ // Assert
+ Assert.True(context.ModelState.IsValid);
+ }
+
+ [Fact]
+ public void ProcessDto_BindRequiredFieldMissing_Throws()
+ {
+ // Arrange
+ ModelWithBindRequired model = new ModelWithBindRequired
+ {
+ Name = "original value",
+ Age = -20
+ };
+
+ ModelMetadata containerMetadata = GetMetadataForObject(model);
+
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = containerMetadata,
+ ModelName = "theModel"
+ };
+ ComplexModelDto dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties);
+
+ ModelMetadata nameProperty = dto.PropertyMetadata.Single(o => o.PropertyName == "Name");
+ dto.Results[nameProperty] = new ComplexModelDtoResult("John Doe", new ModelValidationNode(nameProperty, ""));
+
+ TestableMutableObjectModelBinder testableBinder = new TestableMutableObjectModelBinder();
+
+ // Act & assert
+ Assert.Throws<InvalidOperationException>(
+ () => testableBinder.ProcessDto(context, bindingContext, dto),
+ @"A value for 'theModel.Age' is required but was not present in the request.");
+
+ Assert.Equal("original value", model.Name);
+ Assert.Equal(-20, model.Age);
+ }
+
+ [Fact]
+ public void ProcessDto_Success()
+ {
+ // Arrange
+ DateTime dob = new DateTime(2001, 1, 1);
+ Person model = new Person
+ {
+ DateOfBirth = dob
+ };
+ ModelMetadata containerMetadata = GetMetadataForObject(model);
+
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = containerMetadata
+ };
+ ComplexModelDto dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties);
+
+ ModelMetadata firstNameProperty = dto.PropertyMetadata.Single(o => o.PropertyName == "FirstName");
+ dto.Results[firstNameProperty] = new ComplexModelDtoResult("John", new ModelValidationNode(firstNameProperty, ""));
+ ModelMetadata lastNameProperty = dto.PropertyMetadata.Single(o => o.PropertyName == "LastName");
+ dto.Results[lastNameProperty] = new ComplexModelDtoResult("Doe", new ModelValidationNode(lastNameProperty, ""));
+ ModelMetadata dobProperty = dto.PropertyMetadata.Single(o => o.PropertyName == "DateOfBirth");
+ dto.Results[dobProperty] = null;
+
+ TestableMutableObjectModelBinder testableBinder = new TestableMutableObjectModelBinder();
+
+ // Act
+ testableBinder.ProcessDto(context, bindingContext, dto);
+
+ // Assert
+ Assert.Equal("John", model.FirstName);
+ Assert.Equal("Doe", model.LastName);
+ Assert.Equal(dob, model.DateOfBirth);
+ Assert.True(bindingContext.ModelState.IsValid);
+ }
+
+ [Fact]
+ public void SetProperty_PropertyHasDefaultValue_SetsDefaultValue()
+ {
+ // Arrange
+ HttpActionContext context = ContextUtil.CreateActionContext();
+
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = GetMetadataForObject(new Person())
+ };
+
+ ModelMetadata propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "PropertyWithDefaultValue");
+ ModelValidationNode validationNode = new ModelValidationNode(propertyMetadata, "foo");
+ ComplexModelDtoResult dtoResult = new ComplexModelDtoResult(null /* model */, validationNode);
+
+ TestableMutableObjectModelBinder testableBinder = new TestableMutableObjectModelBinder();
+
+ // Act
+ testableBinder.SetPropertyPublic(context, bindingContext, propertyMetadata, dtoResult);
+
+ // Assert
+ var person = Assert.IsType<Person>(bindingContext.Model);
+ Assert.Equal(123.456m, person.PropertyWithDefaultValue);
+ Assert.True(context.ModelState.IsValid);
+ }
+
+ [Fact]
+ public void SetProperty_PropertyIsReadOnly_DoesNothing()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = GetMetadataForType(typeof(Person))
+ };
+
+ ModelMetadata propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "NonUpdateableProperty");
+ ModelValidationNode validationNode = new ModelValidationNode(propertyMetadata, "foo");
+ ComplexModelDtoResult dtoResult = new ComplexModelDtoResult(null /* model */, validationNode);
+
+ TestableMutableObjectModelBinder testableBinder = new TestableMutableObjectModelBinder();
+
+ // Act
+ testableBinder.SetPropertyPublic(null, bindingContext, propertyMetadata, dtoResult);
+
+ // Assert
+ // If didn't throw, success!
+ }
+
+ [Fact]
+ public void SetProperty_PropertyIsSettable_CallsSetter()
+ {
+ // Arrange
+ Person model = new Person();
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = GetMetadataForObject(model)
+ };
+
+ ModelMetadata propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "DateOfBirth");
+ ModelValidationNode validationNode = new ModelValidationNode(propertyMetadata, "foo");
+ ComplexModelDtoResult dtoResult = new ComplexModelDtoResult(new DateTime(2001, 1, 1), validationNode);
+
+ TestableMutableObjectModelBinder testableBinder = new TestableMutableObjectModelBinder();
+
+ // Act
+ testableBinder.SetPropertyPublic(context, bindingContext, propertyMetadata, dtoResult);
+
+ // Assert
+ validationNode.Validate(context);
+ Assert.True(context.ModelState.IsValid);
+ Assert.Equal(new DateTime(2001, 1, 1), model.DateOfBirth);
+ }
+
+ [Fact]
+ public void SetProperty_PropertyIsSettable_SetterThrows_RecordsError()
+ {
+ // Arrange
+ Person model = new Person
+ {
+ DateOfBirth = new DateTime(1900, 1, 1)
+ };
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = GetMetadataForObject(model)
+ };
+
+ ModelMetadata propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "DateOfDeath");
+ ModelValidationNode validationNode = new ModelValidationNode(propertyMetadata, "foo");
+ ComplexModelDtoResult dtoResult = new ComplexModelDtoResult(new DateTime(1800, 1, 1), validationNode);
+
+ TestableMutableObjectModelBinder testableBinder = new TestableMutableObjectModelBinder();
+
+ // Act
+ testableBinder.SetPropertyPublic(null, bindingContext, propertyMetadata, dtoResult);
+
+ // Assert
+ Assert.Equal(@"Date of death can't be before date of birth.
+Parameter name: value", bindingContext.ModelState["foo"].Errors[0].Exception.Message);
+ }
+
+ [Fact]
+ public void SetProperty_SettingNonNullableValueTypeToNull_RequiredValidatorNotPresent_RegistersValidationCallback()
+ {
+ // Arrange
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = GetMetadataForObject(new Person()),
+ };
+
+ ModelMetadata propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "DateOfBirth");
+ ModelValidationNode validationNode = new ModelValidationNode(propertyMetadata, "foo");
+ ComplexModelDtoResult dtoResult = new ComplexModelDtoResult(null /* model */, validationNode);
+
+ TestableMutableObjectModelBinder testableBinder = new TestableMutableObjectModelBinder();
+
+ // Act
+ testableBinder.SetPropertyPublic(context, bindingContext, propertyMetadata, dtoResult);
+
+ // Assert
+ Assert.True(context.ModelState.IsValid);
+ validationNode.Validate(context, bindingContext.ValidationNode);
+ Assert.False(context.ModelState.IsValid);
+ }
+
+ [Fact]
+ public void SetProperty_SettingNonNullableValueTypeToNull_RequiredValidatorPresent_AddsModelError()
+ {
+ // Arrange
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = GetMetadataForObject(new Person()),
+ ModelName = "foo"
+ };
+
+ ModelMetadata propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "ValueTypeRequired");
+ ModelValidationNode validationNode = new ModelValidationNode(propertyMetadata, "foo.ValueTypeRequired");
+ ComplexModelDtoResult dtoResult = new ComplexModelDtoResult(null /* model */, validationNode);
+
+ TestableMutableObjectModelBinder testableBinder = new TestableMutableObjectModelBinder();
+
+ // Act
+ testableBinder.SetPropertyPublic(context, bindingContext, propertyMetadata, dtoResult);
+
+ // Assert
+ Assert.False(bindingContext.ModelState.IsValid);
+ Assert.Equal("Sample message", bindingContext.ModelState["foo.ValueTypeRequired"].Errors[0].ErrorMessage);
+ }
+
+ [Fact]
+ public void SetProperty_SettingNullableTypeToNull_RequiredValidatorNotPresent_PropertySetterThrows_AddsRequiredMessageString()
+ {
+ // Arrange
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = GetMetadataForObject(new ModelWhosePropertySetterThrows()),
+ ModelName = "foo"
+ };
+
+ ModelMetadata propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "NameNoAttribute");
+ ModelValidationNode validationNode = new ModelValidationNode(propertyMetadata, "foo.NameNoAttribute");
+ ComplexModelDtoResult dtoResult = new ComplexModelDtoResult(null /* model */, validationNode);
+
+ TestableMutableObjectModelBinder testableBinder = new TestableMutableObjectModelBinder();
+
+ // Act
+ testableBinder.SetPropertyPublic(context, bindingContext, propertyMetadata, dtoResult);
+
+ // Assert
+ Assert.False(bindingContext.ModelState.IsValid);
+ Assert.Equal(1, bindingContext.ModelState["foo.NameNoAttribute"].Errors.Count);
+ Assert.Equal(@"This is a different exception.
+Parameter name: value", bindingContext.ModelState["foo.NameNoAttribute"].Errors[0].Exception.Message);
+ }
+
+ [Fact]
+ public void SetProperty_SettingNullableTypeToNull_RequiredValidatorPresent_PropertySetterThrows_AddsRequiredMessageString()
+ {
+ // Arrange
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = GetMetadataForObject(new ModelWhosePropertySetterThrows()),
+ ModelName = "foo"
+ };
+
+ ModelMetadata propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "Name");
+ ModelValidationNode validationNode = new ModelValidationNode(propertyMetadata, "foo.Name");
+ ComplexModelDtoResult dtoResult = new ComplexModelDtoResult(null /* model */, validationNode);
+
+ TestableMutableObjectModelBinder testableBinder = new TestableMutableObjectModelBinder();
+
+ // Act
+ testableBinder.SetPropertyPublic(context, bindingContext, propertyMetadata, dtoResult);
+
+ // Assert
+ Assert.False(bindingContext.ModelState.IsValid);
+ Assert.Equal(1, bindingContext.ModelState["foo.Name"].Errors.Count);
+ Assert.Equal("This message comes from the [Required] attribute.", bindingContext.ModelState["foo.Name"].Errors[0].ErrorMessage);
+ }
+
+ private static ModelMetadata GetMetadataForCanUpdateProperty(string propertyName)
+ {
+ CachedDataAnnotationsModelMetadataProvider metadataProvider = new CachedDataAnnotationsModelMetadataProvider();
+ return metadataProvider.GetMetadataForProperty(null, typeof(MyModelTestingCanUpdateProperty), propertyName);
+ }
+
+ private static ModelMetadata GetMetadataForObject(object o)
+ {
+ CachedDataAnnotationsModelMetadataProvider metadataProvider = new CachedDataAnnotationsModelMetadataProvider();
+ return metadataProvider.GetMetadataForType(() => o, o.GetType());
+ }
+
+ private static ModelMetadata GetMetadataForType(Type t)
+ {
+ CachedDataAnnotationsModelMetadataProvider metadataProvider = new CachedDataAnnotationsModelMetadataProvider();
+ return metadataProvider.GetMetadataForType(null, t);
+ }
+
+ private class Person
+ {
+ private DateTime? _dateOfDeath;
+
+ public DateTime DateOfBirth { get; set; }
+
+ public DateTime? DateOfDeath
+ {
+ get { return _dateOfDeath; }
+ set
+ {
+ if (value < DateOfBirth)
+ {
+ throw new ArgumentOutOfRangeException("value", "Date of death can't be before date of birth.");
+ }
+ _dateOfDeath = value;
+ }
+ }
+
+ [Required(ErrorMessage = "Sample message")]
+ public int ValueTypeRequired { get; set; }
+
+ public string FirstName { get; set; }
+ public string LastName { get; set; }
+ public string NonUpdateableProperty { get; private set; }
+
+ [DefaultValue(typeof(decimal), "123.456")]
+ public decimal PropertyWithDefaultValue { get; set; }
+ }
+
+ private class PersonWithBindExclusion
+ {
+ [HttpBindNever]
+ public DateTime DateOfBirth { get; set; }
+
+ [HttpBindNever]
+ public DateTime? DateOfDeath { get; set; }
+
+ public string FirstName { get; set; }
+ public string LastName { get; set; }
+ public string NonUpdateableProperty { get; private set; }
+ }
+
+ private class ModelWithBindRequired
+ {
+ public string Name { get; set; }
+
+ [HttpBindRequired]
+ public int Age { get; set; }
+ }
+
+ [HttpBindRequired]
+ private class ModelWithMixedBindingBehaviors
+ {
+ public string Required { get; set; }
+
+ [HttpBindNever]
+ public string Never { get; set; }
+
+ [HttpBindingBehavior(HttpBindingBehavior.Optional)]
+ public string Optional { get; set; }
+ }
+
+ private sealed class MyModelTestingCanUpdateProperty
+ {
+ public int ReadOnlyInt { get; private set; }
+ public string ReadOnlyString { get; private set; }
+ public string[] ReadOnlyArray { get; private set; }
+ public object ReadOnlyObject { get; private set; }
+ public string ReadWriteString { get; set; }
+ }
+
+ private sealed class ModelWhosePropertySetterThrows
+ {
+ [Required(ErrorMessage = "This message comes from the [Required] attribute.")]
+ public string Name
+ {
+ get { return null; }
+ set { throw new ArgumentException("This is an exception.", "value"); }
+ }
+
+ public string NameNoAttribute
+ {
+ get { return null; }
+ set { throw new ArgumentException("This is a different exception.", "value"); }
+ }
+ }
+
+ public class TestableMutableObjectModelBinder : MutableObjectModelBinder
+ {
+ public virtual bool CanUpdatePropertyPublic(ModelMetadata propertyMetadata)
+ {
+ return base.CanUpdateProperty(propertyMetadata);
+ }
+
+ protected override bool CanUpdateProperty(ModelMetadata propertyMetadata)
+ {
+ return CanUpdatePropertyPublic(propertyMetadata);
+ }
+
+ public virtual object CreateModelPublic(HttpActionContext context, ModelBindingContext bindingContext)
+ {
+ return base.CreateModel(context, bindingContext);
+ }
+
+ protected override object CreateModel(HttpActionContext context, ModelBindingContext bindingContext)
+ {
+ return CreateModelPublic(context, bindingContext);
+ }
+
+ public virtual void EnsureModelPublic(HttpActionContext context, ModelBindingContext bindingContext)
+ {
+ base.EnsureModel(context, bindingContext);
+ }
+
+ protected override void EnsureModel(HttpActionContext context, ModelBindingContext bindingContext)
+ {
+ EnsureModelPublic(context, bindingContext);
+ }
+
+ public virtual IEnumerable<ModelMetadata> GetMetadataForPropertiesPublic(HttpActionContext context, ModelBindingContext bindingContext)
+ {
+ return base.GetMetadataForProperties(context, bindingContext);
+ }
+
+ protected override IEnumerable<ModelMetadata> GetMetadataForProperties(HttpActionContext context, ModelBindingContext bindingContext)
+ {
+ return GetMetadataForPropertiesPublic(context, bindingContext);
+ }
+
+ public virtual void SetPropertyPublic(HttpActionContext context, ModelBindingContext bindingContext, ModelMetadata propertyMetadata, ComplexModelDtoResult dtoResult)
+ {
+ base.SetProperty(context, bindingContext, propertyMetadata, dtoResult);
+ }
+
+ protected override void SetProperty(HttpActionContext context, ModelBindingContext bindingContext, ModelMetadata propertyMetadata, ComplexModelDtoResult dtoResult)
+ {
+ SetPropertyPublic(context, bindingContext, propertyMetadata, dtoResult);
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/ModelBinding/Binders/SimpleModelBinderProviderTest.cs b/test/System.Web.Http.Test/ModelBinding/Binders/SimpleModelBinderProviderTest.cs
new file mode 100644
index 00000000..b9565985
--- /dev/null
+++ b/test/System.Web.Http.Test/ModelBinding/Binders/SimpleModelBinderProviderTest.cs
@@ -0,0 +1,155 @@
+using System.Web.Http.Metadata.Providers;
+using System.Web.Http.Util;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.ModelBinding.Binders
+{
+ public class SimpleModelBinderProviderTest
+ {
+ [Fact]
+ public void ConstructorWithFactoryThrowsIfModelBinderFactoryIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ () => new SimpleModelBinderProvider(typeof(object), (Func<IModelBinder>)null),
+ "modelBinderFactory");
+ }
+
+ [Fact]
+ public void ConstructorWithFactoryThrowsIfModelTypeIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ () => new SimpleModelBinderProvider(null, () => null),
+ "modelType");
+ }
+
+ [Fact]
+ public void ConstructorWithInstanceThrowsIfModelBinderIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ () => new SimpleModelBinderProvider(typeof(object), (IModelBinder)null),
+ "modelBinder");
+ }
+
+ [Fact]
+ public void ConstructorWithInstanceThrowsIfModelTypeIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ () => new SimpleModelBinderProvider(null, new Mock<IModelBinder>().Object),
+ "modelType");
+ }
+
+ [Fact]
+ public void GetBinder_TypeDoesNotMatch_ReturnsNull()
+ {
+ // Arrange
+ SimpleModelBinderProvider provider = new SimpleModelBinderProvider(typeof(string), new Mock<IModelBinder>().Object)
+ {
+ SuppressPrefixCheck = true
+ };
+ ModelBindingContext bindingContext = GetBindingContext(typeof(object));
+
+ // Act
+ IModelBinder binder = provider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(binder);
+ }
+
+ [Fact]
+ public void GetBinder_TypeMatches_PrefixNotFound_ReturnsNull()
+ {
+ // Arrange
+ IModelBinder binderInstance = new Mock<IModelBinder>().Object;
+ SimpleModelBinderProvider provider = new SimpleModelBinderProvider(typeof(string), binderInstance);
+
+ ModelBindingContext bindingContext = GetBindingContext(typeof(string));
+ bindingContext.ValueProvider = new SimpleHttpValueProvider();
+
+ // Act
+ IModelBinder returnedBinder = provider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(returnedBinder);
+ }
+
+ [Fact]
+ public void GetBinder_TypeMatches_PrefixSuppressed_ReturnsFactoryInstance()
+ {
+ // Arrange
+ int numExecutions = 0;
+ IModelBinder theBinderInstance = new Mock<IModelBinder>().Object;
+ Func<IModelBinder> factory = delegate
+ {
+ numExecutions++;
+ return theBinderInstance;
+ };
+
+ SimpleModelBinderProvider provider = new SimpleModelBinderProvider(typeof(string), factory)
+ {
+ SuppressPrefixCheck = true
+ };
+ ModelBindingContext bindingContext = GetBindingContext(typeof(string));
+
+ // Act
+ IModelBinder returnedBinder = provider.GetBinder(null, bindingContext);
+ returnedBinder = provider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Equal(2, numExecutions);
+ Assert.Equal(theBinderInstance, returnedBinder);
+ }
+
+ [Fact]
+ public void GetBinder_TypeMatches_PrefixSuppressed_ReturnsInstance()
+ {
+ // Arrange
+ IModelBinder theBinderInstance = new Mock<IModelBinder>().Object;
+ SimpleModelBinderProvider provider = new SimpleModelBinderProvider(typeof(string), theBinderInstance)
+ {
+ SuppressPrefixCheck = true
+ };
+ ModelBindingContext bindingContext = GetBindingContext(typeof(string));
+
+ // Act
+ IModelBinder returnedBinder = provider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Equal(theBinderInstance, returnedBinder);
+ }
+
+ [Fact]
+ public void GetBinderThrowsIfBindingContextIsNull()
+ {
+ // Arrange
+ SimpleModelBinderProvider provider = new SimpleModelBinderProvider(typeof(string), new Mock<IModelBinder>().Object);
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { provider.GetBinder(null, null); }, "bindingContext");
+ }
+
+ [Fact]
+ public void ModelTypeProperty()
+ {
+ // Arrange
+ SimpleModelBinderProvider provider = new SimpleModelBinderProvider(typeof(string), new Mock<IModelBinder>().Object);
+
+ // Act & assert
+ Assert.Equal(typeof(string), provider.ModelType);
+ }
+
+ private static ModelBindingContext GetBindingContext(Type modelType)
+ {
+ return new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => null, modelType)
+ };
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/ModelBinding/Binders/TypeConverterModelBinderProviderTest.cs b/test/System.Web.Http.Test/ModelBinding/Binders/TypeConverterModelBinderProviderTest.cs
new file mode 100644
index 00000000..cdab0348
--- /dev/null
+++ b/test/System.Web.Http.Test/ModelBinding/Binders/TypeConverterModelBinderProviderTest.cs
@@ -0,0 +1,68 @@
+using System.Web.Http.Metadata.Providers;
+using System.Web.Http.Util;
+using Xunit;
+
+namespace System.Web.Http.ModelBinding.Binders
+{
+ public class TypeConverterModelBinderProviderTest
+ {
+ [Fact]
+ public void GetBinder_NoTypeConverterExistsFromString_ReturnsNull()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = GetBindingContext(typeof(void)); // no TypeConverter exists Void -> String
+
+ TypeConverterModelBinderProvider provider = new TypeConverterModelBinderProvider();
+
+ // Act
+ IModelBinder binder = provider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(binder);
+ }
+
+ [Fact]
+ public void GetBinder_NullValueProviderResult_ReturnsNull()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = GetBindingContext(typeof(int));
+ bindingContext.ValueProvider = new SimpleHttpValueProvider(); // clear the ValueProvider
+
+ TypeConverterModelBinderProvider provider = new TypeConverterModelBinderProvider();
+
+ // Act
+ IModelBinder binder = provider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(binder);
+ }
+
+ [Fact]
+ public void GetBinder_TypeConverterExistsFromString_ReturnsNull()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = GetBindingContext(typeof(int)); // TypeConverter exists Int32 -> String
+
+ TypeConverterModelBinderProvider provider = new TypeConverterModelBinderProvider();
+
+ // Act
+ IModelBinder binder = provider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.IsType<TypeConverterModelBinder>(binder);
+ }
+
+ private static ModelBindingContext GetBindingContext(Type modelType)
+ {
+ return new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, modelType),
+ ModelName = "theModelName",
+ ValueProvider = new SimpleHttpValueProvider
+ {
+ { "theModelName", "someValue" }
+ }
+ };
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/ModelBinding/Binders/TypeConverterModelBinderTest.cs b/test/System.Web.Http.Test/ModelBinding/Binders/TypeConverterModelBinderTest.cs
new file mode 100644
index 00000000..6b55e620
--- /dev/null
+++ b/test/System.Web.Http.Test/ModelBinding/Binders/TypeConverterModelBinderTest.cs
@@ -0,0 +1,170 @@
+using System.ComponentModel;
+using System.Globalization;
+using System.Web.Http.Metadata.Providers;
+using System.Web.Http.Util;
+using Xunit;
+
+namespace System.Web.Http.ModelBinding.Binders
+{
+ public class TypeConverterModelBinderTest
+ {
+ [Fact]
+ public void BindModel_Error_FormatExceptionsTurnedIntoStringsInModelState()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = GetBindingContext(typeof(int));
+ bindingContext.ValueProvider = new SimpleHttpValueProvider
+ {
+ { "theModelName", "not an integer" }
+ };
+
+ TypeConverterModelBinder binder = new TypeConverterModelBinder();
+
+ // Act
+ bool retVal = binder.BindModel(null, bindingContext);
+
+ // Assert
+ Assert.False(retVal);
+ Assert.Equal("The value 'not an integer' is not valid for Int32.", bindingContext.ModelState["theModelName"].Errors[0].ErrorMessage);
+ }
+
+ [Fact]
+ public void BindModel_Error_FormatExceptionsTurnedIntoStringsInModelState_ErrorNotAddedIfCallbackReturnsNull()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = GetBindingContext(typeof(int));
+ bindingContext.ValueProvider = new SimpleHttpValueProvider
+ {
+ { "theModelName", "not an integer" }
+ };
+
+ TypeConverterModelBinder binder = new TypeConverterModelBinder();
+
+ // Act
+ ModelBinderErrorMessageProvider originalProvider = ModelBinderConfig.TypeConversionErrorMessageProvider;
+ bool retVal;
+ try
+ {
+ ModelBinderConfig.TypeConversionErrorMessageProvider = delegate { return null; };
+ retVal = binder.BindModel(null, bindingContext);
+ }
+ finally
+ {
+ ModelBinderConfig.TypeConversionErrorMessageProvider = originalProvider;
+ }
+
+ // Assert
+ Assert.False(retVal);
+ Assert.Null(bindingContext.Model);
+ Assert.True(bindingContext.ModelState.IsValid);
+ }
+
+ [Fact]
+ public void BindModel_Error_GeneralExceptionsSavedInModelState()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = GetBindingContext(typeof(Dummy));
+ bindingContext.ValueProvider = new SimpleHttpValueProvider
+ {
+ { "theModelName", "foo" }
+ };
+
+ TypeConverterModelBinder binder = new TypeConverterModelBinder();
+
+ // Act
+ bool retVal = binder.BindModel(null, bindingContext);
+
+ // Assert
+ Assert.False(retVal);
+ Assert.Null(bindingContext.Model);
+ Assert.Equal("The parameter conversion from type 'System.String' to type 'System.Web.Http.ModelBinding.Binders.TypeConverterModelBinderTest+Dummy' failed. See the inner exception for more information.", bindingContext.ModelState["theModelName"].Errors[0].Exception.Message);
+ Assert.Equal("From DummyTypeConverter: foo", bindingContext.ModelState["theModelName"].Errors[0].Exception.InnerException.Message);
+ }
+
+ [Fact]
+ public void BindModel_NullValueProviderResult_ReturnsFalse()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = GetBindingContext(typeof(int));
+
+ TypeConverterModelBinder binder = new TypeConverterModelBinder();
+
+ // Act
+ bool retVal = binder.BindModel(null, bindingContext);
+
+ // Assert
+ Assert.False(retVal, "BindModel should have returned null.");
+ Assert.Empty(bindingContext.ModelState);
+ }
+
+ [Fact]
+ public void BindModel_ValidValueProviderResult_ConvertEmptyStringsToNull()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = GetBindingContext(typeof(string));
+ bindingContext.ValueProvider = new SimpleHttpValueProvider
+ {
+ { "theModelName", "" }
+ };
+
+ TypeConverterModelBinder binder = new TypeConverterModelBinder();
+
+ // Act
+ bool retVal = binder.BindModel(null, bindingContext);
+
+ // Assert
+ Assert.True(retVal);
+ Assert.Null(bindingContext.Model);
+ Assert.True(bindingContext.ModelState.ContainsKey("theModelName"));
+ }
+
+ [Fact]
+ public void BindModel_ValidValueProviderResult_ReturnsModel()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = GetBindingContext(typeof(int));
+ bindingContext.ValueProvider = new SimpleHttpValueProvider
+ {
+ { "theModelName", "42" }
+ };
+
+ TypeConverterModelBinder binder = new TypeConverterModelBinder();
+
+ // Act
+ bool retVal = binder.BindModel(null, bindingContext);
+
+ // Assert
+ Assert.True(retVal);
+ Assert.Equal(42, bindingContext.Model);
+ Assert.True(bindingContext.ModelState.ContainsKey("theModelName"));
+ }
+
+ private static ModelBindingContext GetBindingContext(Type modelType)
+ {
+ return new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, modelType),
+ ModelName = "theModelName",
+ ValueProvider = new SimpleHttpValueProvider() // empty
+ };
+ }
+
+ [TypeConverter(typeof(DummyTypeConverter))]
+ private struct Dummy
+ {
+ }
+
+ private sealed class DummyTypeConverter : TypeConverter
+ {
+ public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
+ {
+ return (sourceType == typeof(string)) || base.CanConvertFrom(context, sourceType);
+ }
+
+ public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
+ {
+ throw new InvalidOperationException(String.Format("From DummyTypeConverter: {0}", value));
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/ModelBinding/Binders/TypeMatchModelBinderProviderTest.cs b/test/System.Web.Http.Test/ModelBinding/Binders/TypeMatchModelBinderProviderTest.cs
new file mode 100644
index 00000000..7143a483
--- /dev/null
+++ b/test/System.Web.Http.Test/ModelBinding/Binders/TypeMatchModelBinderProviderTest.cs
@@ -0,0 +1,61 @@
+using System.Web.Http.Metadata.Providers;
+using System.Web.Http.Util;
+using Xunit;
+
+namespace System.Web.Http.ModelBinding.Binders
+{
+ public class TypeMatchModelBinderProviderTest
+ {
+ [Fact]
+ public void GetBinder_InvalidValueProviderResult_ReturnsNull()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = GetBindingContext();
+ bindingContext.ValueProvider = new SimpleHttpValueProvider
+ {
+ { "theModelName", "not an integer" }
+ };
+
+ TypeMatchModelBinderProvider provider = new TypeMatchModelBinderProvider();
+
+ // Act
+ IModelBinder binder = provider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.Null(binder);
+ }
+
+ [Fact]
+ public void BindModel_ValidValueProviderResult_ReturnsBinder()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = GetBindingContext();
+ bindingContext.ValueProvider = new SimpleHttpValueProvider
+ {
+ { "theModelName", 42 }
+ };
+
+ TypeMatchModelBinderProvider provider = new TypeMatchModelBinderProvider();
+
+ // Act
+ IModelBinder binder = provider.GetBinder(null, bindingContext);
+
+ // Assert
+ Assert.IsType<TypeMatchModelBinder>(binder);
+ }
+
+ private static ModelBindingContext GetBindingContext()
+ {
+ return GetBindingContext(typeof(int));
+ }
+
+ private static ModelBindingContext GetBindingContext(Type modelType)
+ {
+ return new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, modelType),
+ ModelName = "theModelName"
+ };
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/ModelBinding/Binders/TypeMatchModelBinderTest.cs b/test/System.Web.Http.Test/ModelBinding/Binders/TypeMatchModelBinderTest.cs
new file mode 100644
index 00000000..14f4828f
--- /dev/null
+++ b/test/System.Web.Http.Test/ModelBinding/Binders/TypeMatchModelBinderTest.cs
@@ -0,0 +1,113 @@
+using System.Web.Http.Metadata.Providers;
+using System.Web.Http.Util;
+using System.Web.Http.ValueProviders;
+using Xunit;
+
+namespace System.Web.Http.ModelBinding.Binders
+{
+ public class TypeMatchModelBinderTest
+ {
+ [Fact]
+ public void BindModel_InvalidValueProviderResult_ReturnsFalse()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = GetBindingContext();
+ bindingContext.ValueProvider = new SimpleHttpValueProvider
+ {
+ { "theModelName", "not an integer" }
+ };
+
+ TypeMatchModelBinder binder = new TypeMatchModelBinder();
+
+ // Act
+ bool retVal = binder.BindModel(null, bindingContext);
+
+ // Assert
+ Assert.False(retVal);
+ Assert.Empty(bindingContext.ModelState);
+ }
+
+ [Fact]
+ public void BindModel_ValidValueProviderResult_ReturnsTrue()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = GetBindingContext();
+ bindingContext.ValueProvider = new SimpleHttpValueProvider
+ {
+ { "theModelName", 42 }
+ };
+
+ TypeMatchModelBinder binder = new TypeMatchModelBinder();
+
+ // Act
+ bool retVal = binder.BindModel(null, bindingContext);
+
+ // Assert
+ Assert.True(retVal);
+ Assert.Equal(42, bindingContext.Model);
+ Assert.True(bindingContext.ModelState.ContainsKey("theModelName"));
+ }
+
+ [Fact]
+ public void GetCompatibleValueProviderResult_ValueProviderResultRawValueIncorrect_ReturnsNull()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = GetBindingContext();
+ bindingContext.ValueProvider = new SimpleHttpValueProvider
+ {
+ { "theModelName", "not an integer" }
+ };
+
+ // Act
+ ValueProviderResult vpResult = TypeMatchModelBinder.GetCompatibleValueProviderResult(bindingContext);
+
+ // Assert
+ Assert.Null(vpResult); // Raw value is the wrong type
+ }
+
+ [Fact]
+ public void GetCompatibleValueProviderResult_ValueProviderResultValid_ReturnsValueProviderResult()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = GetBindingContext();
+ bindingContext.ValueProvider = new SimpleHttpValueProvider
+ {
+ { "theModelName", 42 }
+ };
+
+ // Act
+ ValueProviderResult vpResult = TypeMatchModelBinder.GetCompatibleValueProviderResult(bindingContext);
+
+ // Assert
+ Assert.NotNull(vpResult);
+ }
+
+ [Fact]
+ public void GetCompatibleValueProviderResult_ValueProviderReturnsNull_ReturnsNull()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = GetBindingContext();
+ bindingContext.ValueProvider = new SimpleHttpValueProvider();
+
+ // Act
+ ValueProviderResult vpResult = TypeMatchModelBinder.GetCompatibleValueProviderResult(bindingContext);
+
+ // Assert
+ Assert.Null(vpResult); // No key matched
+ }
+
+ private static ModelBindingContext GetBindingContext()
+ {
+ return GetBindingContext(typeof(int));
+ }
+
+ private static ModelBindingContext GetBindingContext(Type modelType)
+ {
+ return new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, modelType),
+ ModelName = "theModelName"
+ };
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/ModelBinding/CompositeModelBinderTest.cs b/test/System.Web.Http.Test/ModelBinding/CompositeModelBinderTest.cs
new file mode 100644
index 00000000..cce66b0b
--- /dev/null
+++ b/test/System.Web.Http.Test/ModelBinding/CompositeModelBinderTest.cs
@@ -0,0 +1,312 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.Web.Http.Controllers;
+using System.Web.Http.Metadata.Providers;
+using System.Web.Http.ModelBinding.Binders;
+using System.Web.Http.ValueProviders;
+using Moq;
+using Xunit;
+
+namespace System.Web.Http.ModelBinding
+{
+ public class CompositeModelBinderTest
+ {
+ //// REVIEW: remove or activate when PropertyFilter is activated
+ ////[Fact]
+ ////public void BindModel_PropertyFilterIsSet_Throws()
+ ////{
+ //// // Arrange
+ //// HttpExecutionContext executionContext = GetHttpExecutionContext();
+
+ //// ModelBindingContext bindingContext = new ModelBindingContext
+ //// {
+ //// FallbackToEmptyPrefix = true,
+ //// ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(SimpleModel)),
+ //// //PropertyFilter = (new BindAttribute { Include = "FirstName " }).IsPropertyAllowed
+ //// };
+
+ //// List<ModelBinderProvider> binderProviders = new List<ModelBinderProvider>();
+ //// CompositeModelBinder shimBinder = new CompositeModelBinder(binderProviders);
+
+ //// // Act & assert
+ //// Assert.Throws<InvalidOperationException>(
+ //// delegate { shimBinder.BindModel(executionContext, bindingContext); },
+ //// @"The new model binding system cannot be used when a property allow list or disallow list has been specified in [Bind] or via the call to UpdateModel() / TryUpdateModel(). Use the [BindRequired] and [BindNever] attributes on the model type or its properties instead.");
+ ////}
+
+ [Fact]
+ public void BindModel_SuccessfulBind_RunsValidationAndReturnsModel()
+ {
+ // Arrange
+ HttpActionContext actionContext = ContextUtil.CreateActionContext(GetHttpControllerContext());
+ bool validationCalled = false;
+
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ FallbackToEmptyPrefix = true,
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int)),
+ ModelName = "someName",
+ //ModelState = executionContext.Controller.ViewData.ModelState,
+ //PropertyFilter = _ => true,
+ ValueProvider = new SimpleValueProvider
+ {
+ { "someName", "dummyValue" }
+ }
+ };
+
+ Mock<IModelBinder> mockIntBinder = new Mock<IModelBinder>();
+ mockIntBinder
+ .Setup(o => o.BindModel(actionContext, It.IsAny<ModelBindingContext>()))
+ .Returns(
+ delegate(HttpActionContext cc, ModelBindingContext mbc)
+ {
+ Assert.Same(bindingContext.ModelMetadata, mbc.ModelMetadata);
+ Assert.Equal("someName", mbc.ModelName);
+ Assert.Same(bindingContext.ValueProvider, mbc.ValueProvider);
+
+ mbc.Model = 42;
+ mbc.ValidationNode.Validating += delegate { validationCalled = true; };
+ return true;
+ });
+
+ Mock<ModelBinderProvider> mockBinderProvider = new Mock<ModelBinderProvider>();
+ mockBinderProvider.Setup(o => o.GetBinder(actionContext, It.IsAny<ModelBindingContext>())).Returns((IModelBinder)mockIntBinder.Object).Verifiable();
+ List<ModelBinderProvider> binderProviders = new List<ModelBinderProvider>()
+ {
+ mockBinderProvider.Object
+ };
+
+ //binderProviders.RegisterBinderForType(typeof(int), mockIntBinder.Object, false /* suppressPrefixCheck */);
+ CompositeModelBinder shimBinder = new CompositeModelBinder(binderProviders);
+
+ // Act
+ bool isBound = shimBinder.BindModel(actionContext, bindingContext);
+
+ // Assert
+ Assert.True(isBound);
+ Assert.Equal(42, bindingContext.Model);
+ Assert.True(validationCalled);
+ Assert.True(bindingContext.ModelState.IsValid);
+ }
+
+ [Fact]
+ public void BindModel_SuccessfulBind_ComplexTypeFallback_RunsValidationAndReturnsModel()
+ {
+ // Arrange
+ HttpActionContext actionContext = ContextUtil.CreateActionContext(GetHttpControllerContext());
+
+ bool validationCalled = false;
+ List<int> expectedModel = new List<int> { 1, 2, 3, 4, 5 };
+
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ FallbackToEmptyPrefix = true,
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(List<int>)),
+ ModelName = "someName",
+ //ModelState = executionContext.Controller.ViewData.ModelState,
+ //PropertyFilter = _ => true,
+ ValueProvider = new SimpleValueProvider
+ {
+ { "someOtherName", "dummyValue" }
+ }
+ };
+
+ Mock<IModelBinder> mockIntBinder = new Mock<IModelBinder>();
+ mockIntBinder
+ .Setup(o => o.BindModel(actionContext, It.IsAny<ModelBindingContext>()))
+ .Returns(
+ delegate(HttpActionContext cc, ModelBindingContext mbc)
+ {
+ Assert.Same(bindingContext.ModelMetadata, mbc.ModelMetadata);
+ Assert.Equal("", mbc.ModelName);
+ Assert.Same(bindingContext.ValueProvider, mbc.ValueProvider);
+
+ mbc.Model = expectedModel;
+ mbc.ValidationNode.Validating += delegate { validationCalled = true; };
+ return true;
+ });
+
+ List<ModelBinderProvider> binderProviders = new List<ModelBinderProvider>()
+ {
+ new SimpleModelBinderProvider()
+ {
+ Binder = mockIntBinder.Object,
+ OnlyWithEmptyModelName = true
+ }
+ };
+
+ //binderProviders.RegisterBinderForType(typeof(List<int>), mockIntBinder.Object, false /* suppressPrefixCheck */);
+ CompositeModelBinder shimBinder = new CompositeModelBinder(binderProviders);
+
+ // Act
+ bool isBound = shimBinder.BindModel(actionContext, bindingContext);
+
+ // Assert
+ Assert.True(isBound);
+ Assert.Equal(expectedModel, bindingContext.Model);
+ Assert.True(validationCalled);
+ Assert.True(bindingContext.ModelState.IsValid);
+ }
+
+ [Fact]
+ public void BindModel_UnsuccessfulBind_BinderFails_ReturnsNull()
+ {
+ // Arrange
+ HttpActionContext actionContext = ContextUtil.CreateActionContext(GetHttpControllerContext());
+ Mock<IModelBinder> mockListBinder = new Mock<IModelBinder>();
+ mockListBinder.Setup(o => o.BindModel(actionContext, It.IsAny<ModelBindingContext>())).Returns(false).Verifiable();
+
+ Mock<ModelBinderProvider> mockBinderProvider = new Mock<ModelBinderProvider>();
+ mockBinderProvider.Setup(o => o.GetBinder(actionContext, It.IsAny<ModelBindingContext>())).Returns((IModelBinder)mockListBinder.Object).Verifiable();
+ List<ModelBinderProvider> binderProviders = new List<ModelBinderProvider>()
+ {
+ mockBinderProvider.Object
+ };
+
+ CompositeModelBinder shimBinder = new CompositeModelBinder(binderProviders);
+
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ FallbackToEmptyPrefix = false,
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(List<int>)),
+ //ModelState = executionContext.Controller.ViewData.ModelState
+ };
+
+ // Act
+ bool isBound = shimBinder.BindModel(actionContext, bindingContext);
+
+ // Assert
+ Assert.False(isBound);
+ Assert.Null(bindingContext.Model);
+ Assert.True(bindingContext.ModelState.IsValid);
+ mockListBinder.Verify();
+ }
+
+ [Fact]
+ public void BindModel_UnsuccessfulBind_SimpleTypeNoFallback_ReturnsNull()
+ {
+ // Arrange
+ HttpActionContext actionContext = ContextUtil.CreateActionContext(GetHttpControllerContext());
+ Mock<ModelBinderProvider> mockBinderProvider = new Mock<ModelBinderProvider>();
+ mockBinderProvider.Setup(o => o.GetBinder(actionContext, It.IsAny<ModelBindingContext>())).Returns((IModelBinder)null).Verifiable();
+ List<ModelBinderProvider> binderProviders = new List<ModelBinderProvider>()
+ {
+ mockBinderProvider.Object
+ };
+ CompositeModelBinder shimBinder = new CompositeModelBinder(binderProviders);
+
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ FallbackToEmptyPrefix = true,
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int)),
+ //ModelState = executionContext.Controller.ViewData.ModelState
+ };
+
+ // Act
+ bool isBound = shimBinder.BindModel(actionContext, bindingContext);
+
+ // Assert
+ Assert.False(isBound);
+ Assert.Null(bindingContext.Model);
+ Assert.True(bindingContext.ModelState.IsValid);
+ mockBinderProvider.Verify();
+ mockBinderProvider.Verify(o => o.GetBinder(actionContext, It.IsAny<ModelBindingContext>()), Times.AtMostOnce());
+ }
+
+ private static HttpControllerContext GetHttpControllerContext()
+ {
+ return ContextUtil.CreateControllerContext();
+ }
+
+ private class SimpleController : ApiController
+ {
+ }
+
+ private class SimpleModel
+ {
+ public string FirstName { get; set; }
+ public string LastName { get; set; }
+ }
+
+ private class SimpleModelBinderProvider : ModelBinderProvider
+ {
+ public IModelBinder Binder { get; set; }
+
+ public bool OnlyWithEmptyModelName { get; set; }
+
+ public override IModelBinder GetBinder(HttpActionContext actionContext, ModelBindingContext bindingContext)
+ {
+ if (OnlyWithEmptyModelName && !String.IsNullOrEmpty(bindingContext.ModelName))
+ {
+ return null;
+ }
+
+ return Binder;
+ }
+ }
+
+ private class SimpleValueProvider : Dictionary<string, object>, IValueProvider
+ {
+ private readonly CultureInfo _culture;
+
+ public SimpleValueProvider()
+ : this(null)
+ {
+ }
+
+ public SimpleValueProvider(CultureInfo culture)
+ : base(StringComparer.OrdinalIgnoreCase)
+ {
+ _culture = culture ?? CultureInfo.InvariantCulture;
+ }
+
+ // copied from ValueProviderUtil
+ public bool ContainsPrefix(string prefix)
+ {
+ foreach (string key in Keys)
+ {
+ if (key != null)
+ {
+ if (prefix.Length == 0)
+ {
+ return true; // shortcut - non-null key matches empty prefix
+ }
+
+ if (key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+ {
+ if (key.Length == prefix.Length)
+ {
+ return true; // exact match
+ }
+ else
+ {
+ switch (key[prefix.Length])
+ {
+ case '.': // known separator characters
+ case '[':
+ return true;
+ }
+ }
+ }
+ }
+ }
+
+ return false; // nothing found
+ }
+
+ public ValueProviderResult GetValue(string key)
+ {
+ object rawValue;
+ if (TryGetValue(key, out rawValue))
+ {
+ return new ValueProviderResult(rawValue, Convert.ToString(rawValue, _culture), _culture);
+ }
+ else
+ {
+ // value not found
+ return null;
+ }
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/ModelBinding/DefaultActionValueBinderTest.cs b/test/System.Web.Http.Test/ModelBinding/DefaultActionValueBinderTest.cs
new file mode 100644
index 00000000..72dd3ae8
--- /dev/null
+++ b/test/System.Web.Http.Test/ModelBinding/DefaultActionValueBinderTest.cs
@@ -0,0 +1,490 @@
+using System.ComponentModel;
+using System.Linq;
+using System.Net.Http;
+using System.Reflection;
+using System.Threading;
+using System.Web.Http.Controllers;
+using System.Web.Http.ValueProviders;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.ModelBinding
+{
+ // These tests primarily focus on getting the right binding contract. They don't actually execute the contract.
+ public class DefaultActionValueBinderTest
+ {
+ [Fact]
+ public void BindValuesAsync_Throws_Null_ActionDescriptor()
+ {
+ // Arrange
+ HttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor { MethodInfo = (MethodInfo)MethodInfo.GetCurrentMethod() };
+
+ // Act and Assert
+ Assert.ThrowsArgumentNull(
+ () => new DefaultActionValueBinder().GetBinding(null),
+ "actionDescriptor");
+ }
+
+ private void Action_Int(int id) { }
+
+ [Fact]
+ public void Check_Int_Is_ModelBound()
+ {
+ DefaultActionValueBinder binder = new DefaultActionValueBinder();
+
+ var binding = binder.GetBinding(GetAction("Action_Int"));
+
+ Assert.Equal(1, binding.ParameterBindings.Length);
+ AssertIsModelBound(binding, 0);
+ }
+
+ private void Action_Int_FromUri([FromUri] int id) { }
+
+ [Fact]
+ public void Check_Explicit_Int_Is_ModelBound()
+ {
+ // Even though int is implicitly model bound, still ok to specify it explicitly
+ DefaultActionValueBinder binder = new DefaultActionValueBinder();
+
+ var binding = binder.GetBinding(GetAction("Action_Int_FromUri"));
+
+ Assert.Equal(1, binding.ParameterBindings.Length);
+ AssertIsModelBound(binding, 0);
+ }
+
+ // All types in this signature are model bound
+ private void Action_SimpleTypes(char ch, Byte b, Int16 i16, UInt16 u16, Int32 i32, UInt32 u32, Int64 i64, UInt64 u64, string s, DateTime d, Decimal dec, Guid g, DateTimeOffset dateTimeOffset, TimeSpan timespan) { }
+
+ [Fact]
+ public void Check_SimpleTypes_Are_ModelBound()
+ {
+ DefaultActionValueBinder binder = new DefaultActionValueBinder();
+
+ var binding = binder.GetBinding(GetAction("Action_SimpleTypes"));
+
+ for(int i = 0; i < binding.ParameterBindings.Length; i++)
+ {
+ AssertIsModelBound(binding, 0);
+ }
+ }
+
+ private void Action_ComplexTypeWithStringConverter(ComplexTypeWithStringConverter x) { }
+
+ [Fact]
+ public void Check_String_TypeConverter_Is_ModelBound()
+ {
+ DefaultActionValueBinder binder = new DefaultActionValueBinder();
+
+ var binding = binder.GetBinding(GetAction("Action_ComplexTypeWithStringConverter"));
+
+ Assert.Equal(1, binding.ParameterBindings.Length);
+ AssertIsModelBound(binding, 0);
+ }
+
+ private void Action_ComplexTypeWithStringConverter_Body_Override([FromBody] ComplexTypeWithStringConverter x) { }
+
+ [Fact]
+ public void Check_String_TypeConverter_With_Body_Override()
+ {
+ DefaultActionValueBinder binder = new DefaultActionValueBinder();
+
+ var binding = binder.GetBinding(GetAction("Action_ComplexTypeWithStringConverter_Body_Override"));
+
+ Assert.Equal(1, binding.ParameterBindings.Length);
+ AssertIsBody(binding, 0);
+ }
+
+ private void Action_NullableInt(Nullable<int> id) { }
+
+ [Fact]
+ public void Check_NullableInt_Is_ModelBound()
+ {
+ DefaultActionValueBinder binder = new DefaultActionValueBinder();
+
+ var binding = binder.GetBinding(GetAction("Action_NullableInt"));
+
+ Assert.Equal(1, binding.ParameterBindings.Length);
+ AssertIsModelBound(binding, 0);
+ }
+
+ private void Action_Nullable_ValueType(Nullable<ComplexValueType> id) { }
+
+ [Fact]
+ public void Check_Nullable_ValueType_Is_FromBody()
+ {
+ DefaultActionValueBinder binder = new DefaultActionValueBinder();
+
+ var binding = binder.GetBinding(GetAction("Action_Nullable_ValueType"));
+
+ Assert.Equal(1, binding.ParameterBindings.Length);
+ AssertIsBody(binding, 0);
+ }
+
+
+ private void Action_IntArray(int[] arrayFrombody) { }
+
+ [Fact]
+ public void Check_IntArray_Is_FromBody()
+ {
+ DefaultActionValueBinder binder = new DefaultActionValueBinder();
+
+ var binding = binder.GetBinding(GetAction("Action_IntArray"));
+
+ Assert.Equal(1, binding.ParameterBindings.Length);
+ AssertIsBody(binding, 0);
+ }
+
+
+ private void Action_SimpleType_Body([FromBody] int i) { }
+
+ [Fact]
+ public void Check_SimpleType_Body()
+ {
+ DefaultActionValueBinder binder = new DefaultActionValueBinder();
+
+ var binding = binder.GetBinding(GetAction("Action_SimpleType_Body"));
+
+ Assert.Equal(1, binding.ParameterBindings.Length);
+ AssertIsBody(binding, 0);
+ }
+
+ private void Action_Empty() { }
+
+ [Fact]
+ public void Check_Empty_Action()
+ {
+ DefaultActionValueBinder binder = new DefaultActionValueBinder();
+
+ var binding = binder.GetBinding(GetAction("Action_Empty"));
+
+ Assert.NotNull(binding.ParameterBindings);
+ Assert.Equal(0, binding.ParameterBindings.Length);
+ }
+
+ private void Action_String_String(string s1, string s2) { }
+
+ [Fact]
+ public void Check_String_String_IsModelBound()
+ {
+ DefaultActionValueBinder binder = new DefaultActionValueBinder();
+
+ var binding = binder.GetBinding(GetAction("Action_String_String"));
+
+ Assert.Equal(2, binding.ParameterBindings.Length);
+ AssertIsModelBound(binding, 0);
+ AssertIsModelBound(binding, 1);
+ }
+
+ private void Action_Complex_Type(ComplexType complex) { }
+
+ [Fact]
+ public void Check_Complex_Type_FromBody()
+ {
+ DefaultActionValueBinder binder = new DefaultActionValueBinder();
+
+ var binding = binder.GetBinding(GetAction("Action_Complex_Type"));
+
+ Assert.Equal(1, binding.ParameterBindings.Length);
+ AssertIsBody(binding, 0);
+ }
+
+ private void Action_Complex_ValueType(ComplexValueType complex) { }
+
+ [Fact]
+ public void Check_Complex_ValueType_FromBody()
+ {
+ DefaultActionValueBinder binder = new DefaultActionValueBinder();
+
+ var binding = binder.GetBinding(GetAction("Action_Complex_ValueType"));
+
+ Assert.Equal(1, binding.ParameterBindings.Length);
+ AssertIsBody(binding, 0);
+ }
+
+ private void Action_Default_Custom_Model_Binder([ModelBinder] ComplexType complex) { }
+
+ [Fact]
+ public void Check_Customer_Binder()
+ {
+ // Mere presence of a ModelBinder attribute means the type is model bound.
+
+ DefaultActionValueBinder binder = new DefaultActionValueBinder();
+
+ var binding = binder.GetBinding(GetAction("Action_Default_Custom_Model_Binder"));
+
+ Assert.Equal(1, binding.ParameterBindings.Length);
+ AssertIsModelBound(binding, 0);
+ }
+
+ private void Action_Complex_Type_Uri([FromUri] ComplexType complex) { }
+
+ [Fact]
+ public void Check_Complex_Type_FromUri()
+ {
+ // [FromUri] is just a specific instance of ModelBinder attribute
+ DefaultActionValueBinder binder = new DefaultActionValueBinder();
+
+ var binding = binder.GetBinding(GetAction("Action_Complex_Type_Uri"));
+
+ Assert.Equal(1, binding.ParameterBindings.Length);
+ AssertIsModelBound(binding, 0);
+ }
+
+ private void Action_Two_Complex_Types(ComplexType complexBody1, ComplexType complexBody2) { }
+
+ [Fact]
+ public void Check_Two_Complex_Types_FromBody()
+ {
+ DefaultActionValueBinder binder = new DefaultActionValueBinder();
+
+ // It's illegal to have multiple parameters from the body.
+ // But we should still be able to get a binding for it. We just can't execute it.
+ var binding = binder.GetBinding(GetAction("Action_Two_Complex_Types"));
+
+ Assert.Equal(2, binding.ParameterBindings.Length);
+ AssertIsError(binding, 0);
+ AssertIsError(binding, 1);
+ }
+
+ private void Action_Complex_Type_UriAndBody([FromUri] ComplexType complexUri, ComplexType complexBody) { }
+
+ [Fact]
+ public void Check_Complex_Type_FromBody_And_FromUri()
+ {
+ DefaultActionValueBinder binder = new DefaultActionValueBinder();
+
+ var binding = binder.GetBinding(GetAction("Action_Complex_Type_UriAndBody"));
+
+ Assert.Equal(2, binding.ParameterBindings.Length);
+ AssertIsModelBound(binding, 0);
+ AssertIsBody(binding, 1);
+ }
+
+ private void Action_CancellationToken(CancellationToken ct) { }
+
+ [Fact]
+ public void Check_Cancellation_Token()
+ {
+ DefaultActionValueBinder binder = new DefaultActionValueBinder();
+
+ var binding = binder.GetBinding(GetAction("Action_CancellationToken"));
+
+ Assert.Equal(1, binding.ParameterBindings.Length);
+ AssertIsCancellationToken(binding, 0);
+ }
+
+ private void Action_CustomModelBinder_On_Parameter_WithProvider([ModelBinder(typeof(CustomModelBinderProvider))] ComplexType complex) { }
+
+ [Fact]
+ public void Check_CustomModelBinder_On_Parameter()
+ {
+ HttpConfiguration config = new HttpConfiguration();
+ config.ServiceResolver.SetServices(typeof(ValueProviderFactory), new ValueProviderFactory[] {
+ new CustomValueProviderFactory(),
+ });
+
+ DefaultActionValueBinder binder = new DefaultActionValueBinder();
+
+ var binding = binder.GetBinding(GetAction("Action_CustomModelBinder_On_Parameter_WithProvider", config));
+
+ Assert.Equal(1, binding.ParameterBindings.Length);
+ AssertIsModelBound(binding, 0);
+
+ ModelBinderParameterBinding p = (ModelBinderParameterBinding) binding.ParameterBindings[0];
+ Assert.IsType<CustomModelBinderProvider>(p.ModelBinderProvider);
+
+ // Since the ModelBinderAttribute didn't specify the valueproviders, we should pull those from config.
+ Assert.Equal(1, p.ValueProviderFactories.Count());
+ Assert.IsType<CustomValueProviderFactory>(p.ValueProviderFactories.First());
+ }
+
+ // Model binder attribute is on the type's declaration.
+ private void Action_ComplexParameter_With_ModelBinder(ComplexTypeWithModelBinder complex) { }
+
+ [Fact]
+ public void Check_Parameter_With_ModelBinder_Attribute_On_Type()
+ {
+ DefaultActionValueBinder binder = new DefaultActionValueBinder();
+
+ var binding = binder.GetBinding(GetAction("Action_ComplexParameter_With_ModelBinder"));
+
+ Assert.Equal(1, binding.ParameterBindings.Length);
+ AssertIsModelBound(binding, 0);
+ }
+
+ private void Action_Conflicting_Attributes([FromBody][FromUri] int i) { }
+
+ [Fact]
+ public void Error_Conflicting_Attributes()
+ {
+ DefaultActionValueBinder binder = new DefaultActionValueBinder();
+
+ var binding = binder.GetBinding(GetAction("Action_Conflicting_Attributes"));
+
+ // Have 2 attributes that conflict with each other. Still get the contract, but it has an error in it.
+ Assert.Equal(1, binding.ParameterBindings.Length);
+ AssertIsError(binding, 0);
+ }
+
+ private void Action_HttpContent_Parameter(HttpContent c) { }
+
+ [Fact]
+ public void Check_HttpContent()
+ {
+ DefaultActionValueBinder binder = new DefaultActionValueBinder();
+
+ var binding = binder.GetBinding(GetAction("Action_HttpContent_Parameter"));
+
+ Assert.Equal(1, binding.ParameterBindings.Length);
+ AssertIsError(binding, 0);
+ }
+
+ private void Action_Derived_HttpContent_Parameter(StreamContent c) { }
+
+ [Fact]
+ public void Check_Derived_HttpContent()
+ {
+ DefaultActionValueBinder binder = new DefaultActionValueBinder();
+
+ var binding = binder.GetBinding(GetAction("Action_Derived_HttpContent_Parameter"));
+
+ Assert.Equal(1, binding.ParameterBindings.Length);
+ AssertIsError(binding, 0);
+ }
+
+ private void Action_Request_Parameter(HttpRequestMessage request) { }
+
+ [Fact]
+ public void Check_Request_Parameter()
+ {
+ DefaultActionValueBinder binder = new DefaultActionValueBinder();
+
+ var binding = binder.GetBinding(GetAction("Action_Request_Parameter"));
+
+ Assert.Equal(1, binding.ParameterBindings.Length);
+ AssertIsCustomBinder<HttpRequestParameterBinding>(binding, 0);
+ }
+
+
+
+ // Assert that the binding contract says the given parameter comes from the body
+ private void AssertIsBody(HttpActionBinding binding, int paramIdx)
+ {
+ HttpParameterBinding p = binding.ParameterBindings[paramIdx];
+ Assert.NotNull(p);
+ Assert.True(p.IsValid);
+ Assert.True(p.WillReadBody);
+ }
+
+ // Assert that the binding contract says the given parameter is not from the body (will be handled by model binding)
+ private void AssertIsModelBound(HttpActionBinding binding, int paramIdx)
+ {
+ HttpParameterBinding p = binding.ParameterBindings[paramIdx];
+ Assert.NotNull(p);
+ Assert.IsType<ModelBinderParameterBinding>(p);
+ Assert.True(p.IsValid);
+ Assert.False(p.WillReadBody);
+ }
+
+ // Assert that the binding contract says the given parameter will be bound to the cancellation token.
+ private void AssertIsCancellationToken(HttpActionBinding binding, int paramIdx)
+ {
+ AssertIsCustomBinder<CancellationTokenParameterBinding>(binding, paramIdx);
+ }
+
+ private void AssertIsError(HttpActionBinding binding, int paramIdx)
+ {
+ HttpParameterBinding p = binding.ParameterBindings[paramIdx];
+ Assert.NotNull(p);
+ Assert.False(p.IsValid);
+ Assert.False(p.WillReadBody);
+ }
+
+ private void AssertIsCustomBinder<T>(HttpActionBinding binding, int paramIdx)
+ {
+ HttpParameterBinding p = binding.ParameterBindings[paramIdx];
+ Assert.NotNull(p);
+ Assert.IsType<T>(p);
+ Assert.True(p.IsValid);
+ Assert.False(p.WillReadBody);
+ }
+
+
+ // Helper to get an ActionDescriptor for a method name.
+ private HttpActionDescriptor GetAction(string name)
+ {
+ return GetAction(name, new HttpConfiguration());
+ }
+
+ private HttpActionDescriptor GetAction(string name, HttpConfiguration config)
+ {
+ MethodInfo method = this.GetType().GetMethod(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
+ Assert.NotNull(method);
+ return new ReflectedHttpActionDescriptor { MethodInfo = method, Configuration = config };
+ }
+
+ // Complex type to use with tests
+ class ComplexType
+ {
+ }
+
+ struct ComplexValueType
+ {
+ }
+
+ // Complex type to use with tests
+ [ModelBinder]
+ class ComplexTypeWithModelBinder
+ {
+ }
+
+ // Add Type converter for string, which causes the type to be viewed as a Simple type.
+ [TypeConverter(typeof(MyTypeConverter))]
+ public class ComplexTypeWithStringConverter
+ {
+ public string Data { get; set; }
+ public ComplexTypeWithStringConverter(string data)
+ {
+ Data = data;
+ }
+ }
+
+ // A string type converter
+ public class MyTypeConverter : TypeConverter
+ {
+ public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
+ {
+ if (sourceType == typeof(string))
+ {
+ return true;
+ }
+ return base.CanConvertFrom(context, sourceType);
+ }
+
+ public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
+ {
+ if (value is string)
+ {
+ return new ComplexTypeWithStringConverter((string)value);
+ }
+
+ return base.ConvertFrom(context, culture, value);
+ }
+ }
+
+ class CustomModelBinderProvider : ModelBinderProvider
+ {
+ public override IModelBinder GetBinder(HttpActionContext actionContext, ModelBindingContext bindingContext)
+ {
+ throw new NotImplementedException();
+ }
+ }
+ class CustomValueProviderFactory : ValueProviderFactory
+ {
+ public override IValueProvider GetValueProvider(HttpActionContext actionContext)
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/ModelBinding/FormDataCollectionExtensionsTest.cs b/test/System.Web.Http.Test/ModelBinding/FormDataCollectionExtensionsTest.cs
new file mode 100644
index 00000000..533b73ce
--- /dev/null
+++ b/test/System.Web.Http.Test/ModelBinding/FormDataCollectionExtensionsTest.cs
@@ -0,0 +1,229 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Net.Http;
+using System.Net.Http.Formatting;
+using System.Net.Http.Headers;
+using System.Reflection;
+using System.Threading;
+using System.Web.Http.Controllers;
+using System.Web.Http.ValueProviders;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.ModelBinding
+{
+ public class FormDataCollectionExtensionsTest
+ {
+ [Theory]
+ [InlineData("", null)]
+ [InlineData("", "")] // empty
+ [InlineData("x", "x")] // normal key
+ [InlineData("", "[]")] // trim []
+ [InlineData("x", "x[]")] // trim []
+ [InlineData("x[234]", "x[234]")] // array index
+ [InlineData("x.y", "x[y]")] // field lookup
+ [InlineData("x.y.z", "x[y][z]")] // nested field lookup
+ [InlineData("x.y[234].x", "x[y][234][x]")] // compound
+ public void TestNormalize(string expectedMvc, string jqueryString)
+ {
+ Assert.Equal(expectedMvc, FormDataCollectionExtensions.NormalizeJQueryToMvc(jqueryString));
+ }
+
+ private static HttpContent FormContent(string s)
+ {
+ HttpContent content = new StringContent(s);
+ content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
+
+ return content;
+ }
+
+ private T ParseJQuery<T>(string jquery)
+ {
+ HttpContent content = FormContent(jquery);
+ FormDataCollection fd = content.ReadAsAsync<FormDataCollection>().Result;
+ T result = fd.ReadAs<T>();
+ return result;
+ }
+
+ [Fact]
+ public void ReadIntArray()
+ {
+ // No key name means the top level object is an array
+ int[] result = ParseJQuery<int[]>("=30&=40&=50");
+
+ Assert.Equal(new int[] { 30,40,50 } , result);
+ }
+
+ [Fact]
+ public void ReadIntArrayWithBrackets()
+ {
+ // brackets for explicit array
+ int[] result = ParseJQuery<int[]>("[]=30&[]=40&[]=50");
+
+ Assert.Equal(new int[] { 30, 40, 50 }, result);
+ }
+
+ [Fact]
+ public void ReadIntArrayFromSingleElement()
+ {
+ // No key name means the top level object is an array
+ int[] result = ParseJQuery<int[]>("=30");
+
+ Assert.Equal(new int[] { 30 }, result);
+ }
+
+ public class ClassWithArrayField
+ {
+ public int[] x { get; set; }
+ }
+
+ [Fact]
+ public void ReadClassWithIntArray()
+ {
+ // specifying key name 'x=30' means that we have a field named x.
+ // multiple x keys mean that field is an array.
+ var result = ParseJQuery<ClassWithArrayField>("x=30&x=40&x=50");
+
+ Assert.Equal(new int[] { 30, 40, 50 }, result.x);
+ }
+
+ public class ComplexType
+ {
+ public string Str { get; set; }
+ public int I { get; set; }
+ public Point P { get; set; }
+ }
+ public class Point
+ {
+ public int X { get; set; }
+ public int Y { get; set; }
+ }
+
+ [Fact]
+ public void ReadClassWithFields()
+ {
+ // Basic container class with multiple fields
+ var result = ParseJQuery<Point>("X=3&Y=4");
+ Assert.Equal(3, result.X);
+ Assert.Equal(4, result.Y);
+ }
+
+ [Fact]
+ public void ReadClassWithFieldsFromUri()
+ {
+ var uri = new Uri("http://foo.com/?X=3&Y=4&Z=5");
+ FormDataCollection fd = new FormDataCollection(uri);
+ var result = fd.ReadAs<Point>();
+
+ Assert.Equal(3, result.X);
+ Assert.Equal(4, result.Y);
+ }
+
+ [Fact]
+ public void ReadClassWithFieldsAndPartialBind()
+ {
+ // Basic container class with multiple fields
+ // Extra Z=5 field, ignored since we're reading point.
+ var result = ParseJQuery<Point>("X=3&Y=4&Z=5");
+ Assert.Equal(3, result.X);
+ Assert.Equal(4, result.Y);
+ }
+
+ public class ClassWithPointArray
+ {
+ public Point[] Data { get; set; }
+ }
+
+ [Fact]
+ public void ReadArrayOfClasses()
+ {
+ // Array of classes.
+ string s = "Data[0][X]=10&Data[0][Y]=20&Data[1][X]=30&Data[1][Y]=40";
+ var result = ParseJQuery<ClassWithPointArray>(s);
+
+ Assert.NotNull(result.Data);
+ Assert.Equal(2, result.Data.Length);
+ Assert.Equal(10, result.Data[0].X);
+ Assert.Equal(20, result.Data[0].Y);
+ Assert.Equal(30, result.Data[1].X);
+ Assert.Equal(40, result.Data[1].Y);
+ }
+
+
+ [Fact]
+ public void ReadComplexNestedType()
+ {
+ var result = ParseJQuery<ComplexType>("Str=Hello+world&I=123&P[X]=3&P[Y]=4");
+ Assert.Equal("Hello world", result.Str);
+ Assert.Equal(123, result.I);
+ Assert.NotNull(result.P); // failed to find P
+ Assert.Equal(3, result.P.X);
+ Assert.Equal(4, result.P.Y);
+ }
+
+
+
+ class ComplexType2
+ {
+ public class Epsilon
+ {
+ public int[] f { get; set; }
+ }
+
+ public class Beta
+ {
+ public int c { get; set; }
+ public int d { get; set; }
+ }
+
+ public int[] a { get; set; }
+ public Beta[] b { get; set; }
+ public Epsilon e { get; set; }
+ }
+
+ [Fact]
+ public void ReadComplexNestedType2()
+ {
+ // Jquery encoding from this JSON: "{a:[1,2],b:[{c:3,d:4},{c:5,d:6}],e:{f:[7,8,9]}}";
+ string s = "a[]=1&a[]=2&b[0][c]=3&b[0][d]=4&b[1][c]=5&b[1][d]=6&e[f][]=7&e[f][]=8&e[f][]=9";
+ var result = ParseJQuery<ComplexType2>(s);
+
+ Assert.NotNull(result);
+ Assert.Equal(new int[] { 1, 2 }, result.a);
+ Assert.Equal(2, result.b.Length);
+ Assert.Equal(3, result.b[0].c);
+ Assert.Equal(4, result.b[0].d);
+ Assert.Equal(5, result.b[1].c);
+ Assert.Equal(6, result.b[1].d);
+ Assert.Equal(new int[] { 7, 8, 9 }, result.e.f);
+ }
+
+ [Fact]
+ public void ReadJaggedArray()
+ {
+ string s = "[0][]=9&[0][]=10&[1][]=11&[1][]=12&[2][]=13&[2][]=14";
+ var result = ParseJQuery<int[][]>(s);
+
+ Assert.Equal(9, result[0][0]);
+ Assert.Equal(10, result[0][1]);
+ Assert.Equal(11, result[1][0]);
+ Assert.Equal(12, result[1][1]);
+ Assert.Equal(13, result[2][0]);
+ Assert.Equal(14, result[2][1]);
+ }
+
+ [Fact]
+ public void ReadMultipleParameters()
+ {
+ // Basic container class with multiple fields
+ HttpContent content = FormContent("X=3&Y=4");
+ FormDataCollection fd = content.ReadAsAsync<FormDataCollection>().Result;
+
+ Assert.Equal(3, fd.ReadAs<int>("X"));
+ Assert.Equal("3", fd.ReadAs<string>("X"));
+ Assert.Equal(4, fd.ReadAs<int>("Y"));
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/ModelBinding/ModelBinderAttributeTest.cs b/test/System.Web.Http.Test/ModelBinding/ModelBinderAttributeTest.cs
new file mode 100644
index 00000000..ab42d323
--- /dev/null
+++ b/test/System.Web.Http.Test/ModelBinding/ModelBinderAttributeTest.cs
@@ -0,0 +1,101 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Net.Http;
+using System.Reflection;
+using System.Threading;
+using System.Web.Http.Controllers;
+using System.Web.Http.ValueProviders;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.ModelBinding
+{
+ public class ModelBinderAttributeTest
+ {
+ [Fact]
+ public void Empty_BinderType()
+ {
+ HttpConfiguration config = new HttpConfiguration();
+ config.ServiceResolver.SetServices(typeof(ModelBinderProvider), new CustomModelBinderProvider());
+
+ ModelBinderAttribute attr = new ModelBinderAttribute();
+
+ ModelBinderProvider provider = attr.GetModelBinderProvider(config);
+ Assert.IsType<CustomModelBinderProvider>(provider);
+ }
+
+ [Fact]
+ public void Illegal_BinderType()
+ {
+ // Given an illegal type.
+ // Constructor shouldn't throw. But trying to instantiate the model binder provider will throw.
+ ModelBinderAttribute attr = new ModelBinderAttribute(typeof(object));
+
+ Assert.Equal(typeof(object), attr.BinderType); // can still lookup illegal type
+ Assert.Throws<InvalidOperationException>(
+ () => attr.GetModelBinderProvider(new HttpConfiguration())
+ );
+ }
+
+ [Fact]
+ public void BinderType_Provided()
+ {
+ HttpConfiguration config = new HttpConfiguration();
+ ModelBinderAttribute attr = new ModelBinderAttribute(typeof(CustomModelBinderProvider));
+
+ ModelBinderProvider provider = attr.GetModelBinderProvider(config);
+ Assert.IsType<CustomModelBinderProvider>(provider);
+ }
+
+ [Fact]
+ public void BinderType_From_ServiceResolver()
+ {
+ // To test ServiceResolver, the registered type and actual type should be different.
+ HttpConfiguration config = new HttpConfiguration();
+ config.ServiceResolver.SetService(typeof(CustomModelBinderProvider), new SecondCustomModelBinderProvider());
+
+ ModelBinderAttribute attr = new ModelBinderAttribute(typeof(CustomModelBinderProvider));
+
+ ModelBinderProvider provider = attr.GetModelBinderProvider(config);
+ Assert.IsType<SecondCustomModelBinderProvider>(provider);
+ }
+
+ [Fact]
+ public void Set_ModelBinder_And_ValueProviders()
+ {
+ HttpConfiguration config = new HttpConfiguration();
+ ModelBinderAttribute attr = new ValueProviderAttribute(typeof(CustomValueProviderFactory)) { BinderType = typeof(CustomModelBinderProvider) };
+ IEnumerable<ValueProviderFactory> vpfs = attr.GetValueProviderFactories(config);
+
+ Assert.IsType<CustomModelBinderProvider>(attr.GetModelBinderProvider(config));
+ Assert.Equal(1, vpfs.Count());
+ Assert.IsType<CustomValueProviderFactory>(vpfs.First());
+ }
+
+ private class CustomModelBinderProvider : ModelBinderProvider
+ {
+ public override IModelBinder GetBinder(HttpActionContext actionContext, ModelBindingContext bindingContext)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ private class SecondCustomModelBinderProvider : ModelBinderProvider
+ {
+ public override IModelBinder GetBinder(HttpActionContext actionContext, ModelBindingContext bindingContext)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ private class CustomValueProviderFactory : ValueProviderFactory
+ {
+ public override IValueProvider GetValueProvider(HttpActionContext actionContext)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ }
+}
diff --git a/test/System.Web.Http.Test/ModelBinding/ModelBinderConfigTest.cs b/test/System.Web.Http.Test/ModelBinding/ModelBinderConfigTest.cs
new file mode 100644
index 00000000..21adea4c
--- /dev/null
+++ b/test/System.Web.Http.Test/ModelBinding/ModelBinderConfigTest.cs
@@ -0,0 +1,147 @@
+using System.Web.Http.Controllers;
+using System.Web.Http.Metadata;
+using System.Web.TestUtil;
+using Moq;
+using Xunit;
+
+namespace System.Web.Http.ModelBinding
+{
+ public class ModelBinderConfigTest
+ {
+ [Fact]
+ public void GetUserResourceString_NullControllerContext_ReturnsNull()
+ {
+ // Act
+ string customResourceString = ModelBinderConfig.GetUserResourceString(null /* controllerContext */, "someResourceName", "someResourceClassKey");
+
+ // Assert
+ Assert.Null(customResourceString);
+ }
+
+ [Fact(Skip = "This functionality isn't enabled yet")]
+ public void GetUserResourceString_NullHttpContext_ReturnsNull()
+ {
+ Mock<HttpActionContext> context = new Mock<HttpActionContext>();
+ //context.Setup(o => o.HttpContext).Returns((HttpContextBase)null);
+
+ // Act
+ string customResourceString = ModelBinderConfig.GetUserResourceString(context.Object, "someResourceName", "someResourceClassKey");
+
+ // Assert
+ Assert.Null(customResourceString);
+ }
+
+ [Fact(Skip = "This functionality isn't enabled yet")]
+ public void GetUserResourceString_NullResourceKey_ReturnsNull()
+ {
+ Mock<HttpActionContext> context = new Mock<HttpActionContext>();
+
+ // Act
+ string customResourceString = ModelBinderConfig.GetUserResourceString(context.Object, "someResourceName", null /* resourceClassKey */);
+
+ // Assert
+ //context.Verify(o => o.HttpContext, Times.Never());
+ Assert.Null(customResourceString);
+ }
+
+ [Fact(Skip = "This functionality isn't enabled yet")]
+ public void GetUserResourceString_ValidResourceObject_ReturnsResourceString()
+ {
+ Mock<HttpActionContext> context = new Mock<HttpActionContext>();
+ //context.Setup(o => o.HttpContext.GetGlobalResourceObject("someResourceClassKey", "someResourceName", CultureInfo.CurrentUICulture))
+ // .Returns("My custom resource string");
+
+ // Act
+ string customResourceString = ModelBinderConfig.GetUserResourceString(context.Object, "someResourceName", "someResourceClassKey");
+
+ // Assert
+ Assert.Equal("My custom resource string", customResourceString);
+ }
+
+ [Fact]
+ public void TypeConversionErrorMessageProvider_DefaultValue()
+ {
+ // Arrange
+ ModelMetadata metadata = new ModelMetadata(new Mock<ModelMetadataProvider>().Object, null, null, typeof(int), "SomePropertyName");
+
+ // Act
+ string errorString = ModelBinderConfig.TypeConversionErrorMessageProvider(null, metadata, "some incoming value");
+
+ // Assert
+ Assert.Equal("The value 'some incoming value' is not valid for SomePropertyName.", errorString);
+ }
+
+ [Fact]
+ public void TypeConversionErrorMessageProvider_Property()
+ {
+ // Arrange
+ ModelBinderConfigWrapper wrapper = new ModelBinderConfigWrapper();
+
+ // Act & assert
+ try
+ {
+ MemberHelper.TestPropertyWithDefaultInstance(wrapper, "TypeConversionErrorMessageProvider", (ModelBinderErrorMessageProvider)DummyErrorSelector);
+ }
+ finally
+ {
+ wrapper.Reset();
+ }
+ }
+
+ [Fact]
+ public void ValueRequiredErrorMessageProvider_DefaultValue()
+ {
+ // Arrange
+ ModelMetadata metadata = new ModelMetadata(new Mock<ModelMetadataProvider>().Object, null, null, typeof(int), "SomePropertyName");
+
+ // Act
+ string errorString = ModelBinderConfig.ValueRequiredErrorMessageProvider(null, metadata, "some incoming value");
+
+ // Assert
+ Assert.Equal("A value is required.", errorString);
+ }
+
+ [Fact]
+ public void ValueRequiredErrorMessageProvider_Property()
+ {
+ // Arrange
+ ModelBinderConfigWrapper wrapper = new ModelBinderConfigWrapper();
+
+ // Act & assert
+ try
+ {
+ MemberHelper.TestPropertyWithDefaultInstance(wrapper, "ValueRequiredErrorMessageProvider", (ModelBinderErrorMessageProvider)DummyErrorSelector);
+ }
+ finally
+ {
+ wrapper.Reset();
+ }
+ }
+
+ private string DummyErrorSelector(HttpActionContext actionContext, ModelMetadata modelMetadata, object incomingValue)
+ {
+ throw new NotImplementedException();
+ }
+
+ private sealed class ModelBinderConfigWrapper
+ {
+ public ModelBinderErrorMessageProvider TypeConversionErrorMessageProvider
+ {
+ get { return ModelBinderConfig.TypeConversionErrorMessageProvider; }
+ set { ModelBinderConfig.TypeConversionErrorMessageProvider = value; }
+ }
+
+ public ModelBinderErrorMessageProvider ValueRequiredErrorMessageProvider
+ {
+ get { return ModelBinderConfig.ValueRequiredErrorMessageProvider; }
+ set { ModelBinderConfig.ValueRequiredErrorMessageProvider = value; }
+ }
+
+ public void Reset()
+ {
+ ModelBinderConfig.TypeConversionErrorMessageProvider = null;
+ ModelBinderConfig.ValueRequiredErrorMessageProvider = null;
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/ModelBinding/ModelBindingContextTest.cs b/test/System.Web.Http.Test/ModelBinding/ModelBindingContextTest.cs
new file mode 100644
index 00000000..f433fb67
--- /dev/null
+++ b/test/System.Web.Http.Test/ModelBinding/ModelBindingContextTest.cs
@@ -0,0 +1,126 @@
+using System.Web.Http.Metadata.Providers;
+using System.Web.Http.Util;
+using System.Web.Http.Validation;
+using System.Web.TestUtil;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.ModelBinding
+{
+ public class ModelBindingContextTest
+ {
+ [Fact]
+ public void CopyConstructor()
+ {
+ // Arrange
+ ModelBindingContext originalBindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(object)),
+ ModelName = "theName",
+ ModelState = new ModelStateDictionary(),
+ ValueProvider = new SimpleHttpValueProvider()
+ };
+
+ // Act
+ ModelBindingContext newBindingContext = new ModelBindingContext(originalBindingContext);
+
+ // Assert
+ Assert.Null(newBindingContext.ModelMetadata);
+ Assert.Equal("", newBindingContext.ModelName);
+ Assert.Equal(originalBindingContext.ModelState, newBindingContext.ModelState);
+ Assert.Equal(originalBindingContext.ValueProvider, newBindingContext.ValueProvider);
+ }
+
+ [Fact]
+ public void ModelProperty()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int))
+ };
+
+ // Act & assert
+ MemberHelper.TestPropertyValue(bindingContext, "Model", 42);
+ }
+
+ [Fact]
+ public void ModelProperty_ThrowsIfModelMetadataDoesNotExist()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext();
+
+ // Act & assert
+ Assert.Throws<InvalidOperationException>(
+ () => bindingContext.Model = null,
+ "The ModelMetadata property must be set before accessing this property.");
+ }
+
+ [Fact]
+ public void ModelNameProperty()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext();
+
+ // Act & assert
+ Assert.Reflection.StringProperty(bindingContext, bc => bc.ModelName, String.Empty);
+ }
+
+ [Fact]
+ public void ModelStateProperty()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext();
+ ModelStateDictionary modelState = new ModelStateDictionary();
+
+ // Act & assert
+ MemberHelper.TestPropertyWithDefaultInstance(bindingContext, "ModelState", modelState);
+ }
+
+ [Fact]
+ public void ModelAndModelTypeAreFedFromModelMetadata()
+ {
+ // Act
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => 42, typeof(int))
+ };
+
+ // Assert
+ Assert.Equal(42, bindingContext.Model);
+ Assert.Equal(typeof(int), bindingContext.ModelType);
+ }
+
+ [Fact]
+ public void ValidationNodeProperty()
+ {
+ // Act
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => 42, typeof(int))
+ };
+
+ // Act & assert
+ MemberHelper.TestPropertyWithDefaultInstance(bindingContext, "ValidationNode", new ModelValidationNode(bindingContext.ModelMetadata, "someName"));
+ }
+
+ [Fact]
+ public void ValidationNodeProperty_DefaultValues()
+ {
+ // Act
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => 42, typeof(int)),
+ ModelName = "theInt"
+ };
+
+ // Act
+ ModelValidationNode validationNode = bindingContext.ValidationNode;
+
+ // Assert
+ Assert.NotNull(validationNode);
+ Assert.Equal(bindingContext.ModelMetadata, validationNode.ModelMetadata);
+ Assert.Equal(bindingContext.ModelName, validationNode.ModelStateKey);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/ModelBinding/ModelBindingUtilTest.cs b/test/System.Web.Http.Test/ModelBinding/ModelBindingUtilTest.cs
new file mode 100644
index 00000000..db2f76b4
--- /dev/null
+++ b/test/System.Web.Http.Test/ModelBinding/ModelBindingUtilTest.cs
@@ -0,0 +1,388 @@
+using System.Collections.Generic;
+using System.Web.Http.Controllers;
+using System.Web.Http.Metadata;
+using System.Web.Http.Metadata.Providers;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.ModelBinding
+{
+ public class ModelBindingUtilTest
+ {
+ [Fact]
+ public void CastOrDefault_CorrectType_ReturnsInput()
+ {
+ // Act
+ int retVal = ModelBindingHelper.CastOrDefault<int>(42);
+
+ // Assert
+ Assert.Equal(42, retVal);
+ }
+
+ [Fact]
+ public void CastOrDefault_IncorrectType_ReturnsDefaultTModel()
+ {
+ // Act
+ DateTime retVal = ModelBindingHelper.CastOrDefault<DateTime>(42);
+
+ // Assert
+ Assert.Equal(default(DateTime), retVal);
+ }
+
+ [Fact]
+ public void CreateIndexModelName_EmptyParentName()
+ {
+ // Act
+ string fullChildName = ModelBindingHelper.CreateIndexModelName("", 42);
+
+ // Assert
+ Assert.Equal("[42]", fullChildName);
+ }
+
+ [Fact]
+ public void CreateIndexModelName_IntIndex()
+ {
+ // Act
+ string fullChildName = ModelBindingHelper.CreateIndexModelName("parentName", 42);
+
+ // Assert
+ Assert.Equal("parentName[42]", fullChildName);
+ }
+
+ [Fact]
+ public void CreateIndexModelName_StringIndex()
+ {
+ // Act
+ string fullChildName = ModelBindingHelper.CreateIndexModelName("parentName", "index");
+
+ // Assert
+ Assert.Equal("parentName[index]", fullChildName);
+ }
+
+ [Fact]
+ public void CreatePropertyModelName()
+ {
+ // Act
+ string fullChildName = ModelBindingHelper.CreatePropertyModelName("parentName", "childName");
+
+ // Assert
+ Assert.Equal("parentName.childName", fullChildName);
+ }
+
+ [Fact]
+ public void CreatePropertyModelName_EmptyParentName()
+ {
+ // Act
+ string fullChildName = ModelBindingHelper.CreatePropertyModelName("", "childName");
+
+ // Assert
+ Assert.Equal("childName", fullChildName);
+ }
+
+ [Fact]
+ public void GetPossibleBinderInstance_Match_ReturnsBinder()
+ {
+ // Act
+ IModelBinder binder = ModelBindingHelper.GetPossibleBinderInstance(typeof(List<int>), typeof(List<>), typeof(SampleGenericBinder<>));
+
+ // Assert
+ Assert.IsType<SampleGenericBinder<int>>(binder);
+ }
+
+ [Fact]
+ public void GetPossibleBinderInstance_NoMatch_ReturnsNull()
+ {
+ // Act
+ IModelBinder binder = ModelBindingHelper.GetPossibleBinderInstance(typeof(ArraySegment<int>), typeof(List<>), typeof(SampleGenericBinder<>));
+
+ // Assert
+ Assert.Null(binder);
+ }
+
+ [Fact]
+ public void RawValueToObjectArray_RawValueIsEnumerable_ReturnsInputAsArray()
+ {
+ // Assert
+ List<int> original = new List<int> { 1, 2, 3, 4 };
+
+ // Act
+ object[] retVal = ModelBindingHelper.RawValueToObjectArray(original);
+
+ // Assert
+ Assert.Equal(new object[] { 1, 2, 3, 4 }, retVal);
+ }
+
+ [Fact]
+ public void RawValueToObjectArray_RawValueIsObject_WrapsObjectInSingleElementArray()
+ {
+ // Act
+ object[] retVal = ModelBindingHelper.RawValueToObjectArray(42);
+
+ // Assert
+ Assert.Equal(new object[] { 42 }, retVal);
+ }
+
+ [Fact]
+ public void RawValueToObjectArray_RawValueIsObjectArray_ReturnsInputInstance()
+ {
+ // Assert
+ object[] original = new object[2];
+
+ // Act
+ object[] retVal = ModelBindingHelper.RawValueToObjectArray(original);
+
+ // Assert
+ Assert.Same(original, retVal);
+ }
+
+ [Fact]
+ public void RawValueToObjectArray_RawValueIsString_WrapsStringInSingleElementArray()
+ {
+ // Act
+ object[] retVal = ModelBindingHelper.RawValueToObjectArray("hello");
+
+ // Assert
+ Assert.Equal(new object[] { "hello" }, retVal);
+ }
+
+ [Fact]
+ public void ReplaceEmptyStringWithNull_ConvertEmptyStringToNullDisabled_ModelIsEmptyString_LeavesModelAlone()
+ {
+ // Arrange
+ ModelMetadata modelMetadata = GetMetadata(typeof(string));
+ modelMetadata.ConvertEmptyStringToNull = false;
+
+ // Act
+ object model = "";
+ ModelBindingHelper.ReplaceEmptyStringWithNull(modelMetadata, ref model);
+
+ // Assert
+ Assert.Equal("", model);
+ }
+
+ [Fact]
+ public void ReplaceEmptyStringWithNull_ConvertEmptyStringToNullEnabled_ModelIsEmptyString_ReplacesModelWithNull()
+ {
+ // Arrange
+ ModelMetadata modelMetadata = GetMetadata(typeof(string));
+ modelMetadata.ConvertEmptyStringToNull = true;
+
+ // Act
+ object model = "";
+ ModelBindingHelper.ReplaceEmptyStringWithNull(modelMetadata, ref model);
+
+ // Assert
+ Assert.Null(model);
+ }
+
+ [Fact]
+ public void ReplaceEmptyStringWithNull_ConvertEmptyStringToNullEnabled_ModelIsWhitespaceString_ReplacesModelWithNull()
+ {
+ // Arrange
+ ModelMetadata modelMetadata = GetMetadata(typeof(string));
+ modelMetadata.ConvertEmptyStringToNull = true;
+
+ // Act
+ object model = " "; // whitespace
+ ModelBindingHelper.ReplaceEmptyStringWithNull(modelMetadata, ref model);
+
+ // Assert
+ Assert.Null(model);
+ }
+
+ [Fact]
+ public void ReplaceEmptyStringWithNull_ConvertEmptyStringToNullDisabled_ModelIsNotEmptyString_LeavesModelAlone()
+ {
+ // Arrange
+ ModelMetadata modelMetadata = GetMetadata(typeof(string));
+ modelMetadata.ConvertEmptyStringToNull = true;
+
+ // Act
+ object model = 42;
+ ModelBindingHelper.ReplaceEmptyStringWithNull(modelMetadata, ref model);
+
+ // Assert
+ Assert.Equal(42, model);
+ }
+
+ [Fact]
+ public void ValidateBindingContext_SuccessWithNonNullModel()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = GetMetadata(typeof(string))
+ };
+ bindingContext.ModelMetadata.Model = "hello!";
+
+ // Act
+ ModelBindingHelper.ValidateBindingContext(bindingContext, typeof(string), false);
+
+ // Assert
+ // Nothing to do - if we got this far without throwing, the test succeeded
+ }
+
+ [Fact]
+ public void ValidateBindingContext_SuccessWithNullModel()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = GetMetadata(typeof(string))
+ };
+
+ // Act
+ ModelBindingHelper.ValidateBindingContext(bindingContext, typeof(string), true);
+
+ // Assert
+ // Nothing to do - if we got this far without throwing, the test succeeded
+ }
+
+ [Fact]
+ public void ValidateBindingContextThrowsIfBindingContextIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ () => ModelBindingHelper.ValidateBindingContext(null, typeof(string), true),
+ "bindingContext");
+ }
+
+ [Fact]
+ public void ValidateBindingContextThrowsIfModelInstanceIsWrongType()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = GetMetadata(typeof(string))
+ };
+ bindingContext.ModelMetadata.Model = 42;
+
+ // Act & assert
+ Assert.Throws<ArgumentException>(
+ () => ModelBindingHelper.ValidateBindingContext(bindingContext, typeof(string), true),
+ @"The binding context has a Model of type 'System.Int32', but this binder can only operate on models of type 'System.String'.
+Parameter name: bindingContext");
+ }
+
+ [Fact]
+ public void ValidateBindingContextThrowsIfModelIsNullButCannotBe()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = GetMetadata(typeof(string))
+ };
+
+ // Act & assert
+ Assert.Throws<ArgumentException>(
+ () => ModelBindingHelper.ValidateBindingContext(bindingContext, typeof(string), false),
+ @"The binding context has a null Model, but this binder requires a non-null model of type 'System.String'.
+Parameter name: bindingContext");
+ }
+
+ [Fact]
+ public void ValidateBindingContextThrowsIfModelMetadataIsNull()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext();
+
+ // Act & assert
+ Assert.Throws<ArgumentException>(
+ () => ModelBindingHelper.ValidateBindingContext(bindingContext, typeof(string), true),
+ @"The binding context cannot have a null ModelMetadata.
+Parameter name: bindingContext");
+ }
+
+ [Fact]
+ public void ValidateBindingContextThrowsIfModelTypeIsWrong()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = GetMetadata(typeof(object))
+ };
+
+ // Act & assert
+ Assert.Throws<ArgumentException>(
+ () => ModelBindingHelper.ValidateBindingContext(bindingContext, typeof(string), true),
+ @"The binding context has a ModelType of 'System.Object', but this binder can only operate on models of type 'System.String'.
+Parameter name: bindingContext");
+ }
+
+ //[MetadataType(typeof(ModelWithBindAttribute_Buddy))]
+ //private class ModelWithBindAttribute
+ //{
+ // [Bind]
+ // private class ModelWithBindAttribute_Buddy
+ // {
+ // }
+ //}
+
+ //[ModelBinderProviderOptions(FrontOfList = true)]
+ //private class ProviderAtFront : ModelBinderProvider
+ //{
+ // public override IModelBinder GetBinder(HttpExecutionContext context, ModelBindingContext bindingContext)
+ // {
+ // throw new NotImplementedException();
+ // }
+ //}
+
+ //[ModelBinder(typeof(CustomBinder))]
+ //private class ModelWithProviderAttribute_Binder
+ //{
+ //}
+
+ //[ModelBinder(typeof(CustomGenericBinder<>))]
+ //private class ModelWithProviderAttribute_Binder_Generic<T>
+ //{
+ //}
+
+ //[ModelBinder(typeof(CustomBinder), SuppressPrefixCheck = true)]
+ //private class ModelWithProviderAttribute_Binder_SuppressPrefix
+ //{
+ //}
+
+ //[ModelBinder(typeof(CustomProvider))]
+ //private class ModelWithProviderAttribute_Provider
+ //{
+ //}
+
+ //private class CustomProvider : ModelBinderProvider
+ //{
+ // public override IModelBinder GetBinder(HttpExecutionContext context, ModelBindingContext bindingContext)
+ // {
+ // return new CustomBinder();
+ // }
+ //}
+
+ //private class CustomBinder : IModelBinder
+ //{
+ // public bool BindModel(HttpExecutionContext context, ModelBindingContext bindingContext)
+ // {
+ // throw new NotImplementedException();
+ // }
+ //}
+
+ //private class CustomGenericBinder<T> : IModelBinder
+ //{
+ // public bool BindModel(HttpExecutionContext context, ModelBindingContext bindingContext)
+ // {
+ // throw new NotImplementedException();
+ // }
+ //}
+
+ private static ModelMetadata GetMetadata(Type modelType)
+ {
+ EmptyModelMetadataProvider provider = new EmptyModelMetadataProvider();
+ return provider.GetMetadataForType(null, modelType);
+ }
+
+ private class SampleGenericBinder<T> : IModelBinder
+ {
+ public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Properties/AssemblyInfo.cs b/test/System.Web.Http.Test/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..9155b469
--- /dev/null
+++ b/test/System.Web.Http.Test/Properties/AssemblyInfo.cs
@@ -0,0 +1,3 @@
+using System;
+
+[assembly: CLSCompliant(false)]
diff --git a/test/System.Web.Http.Test/Query/DataModel.cs b/test/System.Web.Http.Test/Query/DataModel.cs
new file mode 100644
index 00000000..3dcd0894
--- /dev/null
+++ b/test/System.Web.Http.Test/Query/DataModel.cs
@@ -0,0 +1,43 @@
+namespace System.Web.Http.Query
+{
+ public class Product
+ {
+ public int ProductID { get; set; }
+
+ public string ProductName { get; set; }
+ public int SupplierID { get; set; }
+ public int CategoryID { get; set; }
+ public string QuantityPerUnit { get; set; }
+ public decimal UnitPrice { get; set; }
+ public short UnitsInStock { get; set; }
+ public short UnitsOnOrder { get; set; }
+
+ public short ReorderLevel { get; set; }
+ public bool Discontinued { get; set; }
+ public DateTime DiscontinuedDate { get; set; }
+
+ public Category Category { get; set; }
+ }
+
+ public class Category
+ {
+ public int CategoryID { get; set; }
+ public string CategoryName { get; set; }
+ }
+
+ public class DataTypes
+ {
+ public Guid GuidProp { get; set; }
+ public DateTime DateTimeProp { get; set; }
+ public DateTimeOffset DateTimeOffsetProp { get; set; }
+ public byte[] ByteArrayProp { get; set; }
+ public TimeSpan TimeSpanProp { get; set; }
+ public decimal DecimalProp { get; set; }
+ public double DoubleProp { get; set; }
+ public float FloatProp { get; set; }
+ public long LongProp { get; set; }
+ public int IntProp { get; set; }
+
+ public string Inaccessable() { return String.Empty; }
+ }
+}
diff --git a/test/System.Web.Http.Test/Query/ODataQueryDeserializerTests.cs b/test/System.Web.Http.Test/Query/ODataQueryDeserializerTests.cs
new file mode 100644
index 00000000..aa6e6fe1
--- /dev/null
+++ b/test/System.Web.Http.Test/Query/ODataQueryDeserializerTests.cs
@@ -0,0 +1,623 @@
+using System.Linq;
+using System.Linq.Expressions;
+using Microsoft.TestCommon;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Query
+{
+ public class ODataQueryDeserializerTests
+ {
+ [Fact]
+ public void SimpleMultipartQuery()
+ {
+ VerifyQueryDeserialization(
+ "$filter=ProductName eq 'Doritos'&$orderby=UnitPrice&$top=100",
+ "Where(Param_0 => (Param_0.ProductName == \"Doritos\")).OrderBy(Param_1 => Param_1.UnitPrice).Take(100)");
+ }
+
+ #region Ordering
+ [Fact]
+ public void OrderBy()
+ {
+ VerifyQueryDeserialization(
+ "$orderby=UnitPrice",
+ "OrderBy(Param_0 => Param_0.UnitPrice)");
+ }
+
+ [Fact]
+ public void OrderByAscending()
+ {
+ VerifyQueryDeserialization(
+ "$orderby=UnitPrice asc",
+ "OrderBy(Param_0 => Param_0.UnitPrice)");
+ }
+
+ [Fact]
+ public void OrderByDescending()
+ {
+ VerifyQueryDeserialization(
+ "$orderby=UnitPrice desc",
+ "OrderByDescending(Param_0 => Param_0.UnitPrice)");
+ }
+ #endregion
+
+ #region Inequalities
+ [Fact]
+ public void EqualityOperator()
+ {
+ VerifyQueryDeserialization(
+ "$filter=ProductName eq 'Doritos'",
+ "Where(Param_0 => (Param_0.ProductName == \"Doritos\"))");
+ }
+
+ [Fact]
+ public void NotEqualOperator()
+ {
+ VerifyQueryDeserialization(
+ "$filter=ProductName ne 'Doritos'",
+ "Where(Param_0 => (Param_0.ProductName != \"Doritos\"))");
+ }
+
+ [Fact]
+ public void GreaterThanOperator()
+ {
+ VerifyQueryDeserialization(
+ "$filter=UnitPrice gt 5.00",
+ "Where(Param_0 => (Param_0.UnitPrice > 5.00))");
+ }
+
+ [Fact]
+ public void GreaterThanEqualOperator()
+ {
+ VerifyQueryDeserialization(
+ "$filter=UnitPrice ge 5.00",
+ "Where(Param_0 => (Param_0.UnitPrice >= 5.00))");
+ }
+
+ [Fact]
+ public void LessThanOperator()
+ {
+ VerifyQueryDeserialization(
+ "$filter=UnitPrice lt 5.00",
+ "Where(Param_0 => (Param_0.UnitPrice < 5.00))");
+ }
+
+ [Fact]
+ public void LessThanOrEqualOperator()
+ {
+ VerifyQueryDeserialization(
+ "$filter=UnitPrice le 5.00",
+ "Where(Param_0 => (Param_0.UnitPrice <= 5.00))");
+ }
+
+ [Fact]
+ public void NegativeNumbers()
+ {
+ VerifyQueryDeserialization(
+ "$filter=UnitPrice le -5.00",
+ "Where(Param_0 => (Param_0.UnitPrice <= -5.00))");
+ }
+ #endregion
+
+ #region Logical Operators
+ [Fact]
+ public void OrOperator()
+ {
+ VerifyQueryDeserialization(
+ "$filter=UnitPrice eq 5.00 or UnitPrice eq 10.00",
+ "Where(Param_0 => ((Param_0.UnitPrice == 5.00) OrElse (Param_0.UnitPrice == 10.00)))");
+ }
+
+ [Fact]
+ public void AndOperator()
+ {
+ VerifyQueryDeserialization(
+ "$filter=UnitPrice eq 5.00 and UnitPrice eq 10.00",
+ "Where(Param_0 => ((Param_0.UnitPrice == 5.00) AndAlso (Param_0.UnitPrice == 10.00)))");
+ }
+
+ [Fact]
+ public void Negation()
+ {
+ VerifyQueryDeserialization(
+ "$filter=not (UnitPrice eq 5.00)",
+ "Where(Param_0 => Not((Param_0.UnitPrice == 5.00)))");
+ }
+ #endregion
+
+ #region Arithmetic Operators
+ [Fact]
+ public void Subtraction()
+ {
+ VerifyQueryDeserialization(
+ "$filter=UnitPrice sub 1.00 lt 5.00",
+ "Where(Param_0 => ((Param_0.UnitPrice - 1.00) < 5.00))");
+ }
+
+ [Fact]
+ public void Addition()
+ {
+ VerifyQueryDeserialization(
+ "$filter=UnitPrice add 1.00 lt 5.00",
+ "Where(Param_0 => ((Param_0.UnitPrice + 1.00) < 5.00))");
+ }
+
+ [Fact]
+ public void Multiplication()
+ {
+ VerifyQueryDeserialization(
+ "$filter=UnitPrice mul 1.00 lt 5.00",
+ "Where(Param_0 => ((Param_0.UnitPrice * 1.00) < 5.00))");
+ }
+
+ [Fact]
+ public void Division()
+ {
+ VerifyQueryDeserialization(
+ "$filter=UnitPrice div 1.00 lt 5.00",
+ "Where(Param_0 => ((Param_0.UnitPrice / 1.00) < 5.00))");
+ }
+
+ [Fact]
+ public void Modulo()
+ {
+ VerifyQueryDeserialization(
+ "$filter=UnitPrice mod 1.00 lt 5.00",
+ "Where(Param_0 => ((Param_0.UnitPrice % 1.00) < 5.00))");
+ }
+ #endregion
+
+ [Fact]
+ public void Grouping()
+ {
+ VerifyQueryDeserialization(
+ "$filter=((ProductName ne 'Doritos') or (UnitPrice lt 5.00))",
+ "Where(Param_0 => ((Param_0.ProductName != \"Doritos\") OrElse (Param_0.UnitPrice < 5.00)))");
+ }
+
+ [Fact]
+ public void MemberExpressions()
+ {
+ VerifyQueryDeserialization(
+ "$filter=Category/CategoryName eq 'Snacks'",
+ "Where(Param_0 => (Param_0.Category.CategoryName == \"Snacks\"))");
+ }
+
+ #region String Functions
+ [Fact]
+ public void StringSubstringOf()
+ {
+ // In OData, the order of parameters is actually reversed in the resulting
+ // string.Contains expression
+
+ VerifyQueryDeserialization(
+ "$filter=substringof('Abc', ProductName) eq true",
+ "Where(Param_0 => (Param_0.ProductName.Contains(\"Abc\") == True))");
+
+ VerifyQueryDeserialization(
+ "$filter=substringof(ProductName, 'Abc') eq true",
+ "Where(Param_0 => (\"Abc\".Contains(Param_0.ProductName) == True))");
+ }
+
+ [Fact]
+ public void StringStartsWith()
+ {
+ VerifyQueryDeserialization(
+ "$filter=startswith(ProductName, 'Abc') eq true",
+ "Where(Param_0 => (Param_0.ProductName.StartsWith(\"Abc\") == True))");
+ }
+
+ [Fact]
+ public void StringEndsWith()
+ {
+ VerifyQueryDeserialization(
+ "$filter=endswith(ProductName, 'Abc') eq true",
+ "Where(Param_0 => (Param_0.ProductName.EndsWith(\"Abc\") == True))");
+ }
+
+ [Fact]
+ public void StringLength()
+ {
+ VerifyQueryDeserialization(
+ "$filter=length(ProductName) gt 0",
+ "Where(Param_0 => (Param_0.ProductName.Length > 0))");
+ }
+
+ [Fact]
+ public void StringIndexOf()
+ {
+ VerifyQueryDeserialization(
+ "$filter=indexof(ProductName, 'Abc') eq 5",
+ "Where(Param_0 => (Param_0.ProductName.IndexOf(\"Abc\") == 5))");
+ }
+
+ [Fact]
+ public void StringReplace()
+ {
+ VerifyQueryDeserialization(
+ "$filter=replace(ProductName, 'Abc', 'Def') eq \"FooDef\"",
+ "Where(Param_0 => (Param_0.ProductName.Replace(\"Abc\", \"Def\") == \"FooDef\"))");
+ }
+
+ [Fact]
+ public void StringSubstring()
+ {
+ VerifyQueryDeserialization(
+ "$filter=substring(ProductName, 3) eq \"uctName\"",
+ "Where(Param_0 => (Param_0.ProductName.Substring(3) == \"uctName\"))");
+
+ VerifyQueryDeserialization(
+ "$filter=substring(ProductName, 3, 4) eq \"uctN\"",
+ "Where(Param_0 => (Param_0.ProductName.Substring(3, 4) == \"uctN\"))");
+ }
+
+ [Fact]
+ public void StringToLower()
+ {
+ VerifyQueryDeserialization(
+ "$filter=tolower(ProductName) eq 'tasty treats'",
+ "Where(Param_0 => (Param_0.ProductName.ToLower() == \"tasty treats\"))");
+ }
+
+ [Fact]
+ public void StringToUpper()
+ {
+ VerifyQueryDeserialization(
+ "$filter=toupper(ProductName) eq 'TASTY TREATS'",
+ "Where(Param_0 => (Param_0.ProductName.ToUpper() == \"TASTY TREATS\"))");
+ }
+
+ [Fact]
+ public void StringTrim()
+ {
+ VerifyQueryDeserialization(
+ "$filter=trim(ProductName) eq 'Tasty Treats'",
+ "Where(Param_0 => (Param_0.ProductName.Trim() == \"Tasty Treats\"))");
+ }
+
+ [Fact]
+ public void StringConcat()
+ {
+ VerifyQueryDeserialization(
+ "$filter=concat('Foo', 'Bar') eq 'FooBar'",
+ "Where(Param_0 => (Concat(\"Foo\", \"Bar\") == \"FooBar\"))");
+ }
+ #endregion
+
+ #region Date Functions
+ [Fact]
+ public void DateDay()
+ {
+ VerifyQueryDeserialization(
+ "$filter=day(DiscontinuedDate) eq 8",
+ "Where(Param_0 => (Param_0.DiscontinuedDate.Day == 8))");
+ }
+
+ [Fact]
+ public void DateMonth()
+ {
+ VerifyQueryDeserialization(
+ "$filter=month(DiscontinuedDate) eq 8",
+ "Where(Param_0 => (Param_0.DiscontinuedDate.Month == 8))");
+ }
+
+ [Fact]
+ public void DateYear()
+ {
+ VerifyQueryDeserialization(
+ "$filter=year(DiscontinuedDate) eq 1974",
+ "Where(Param_0 => (Param_0.DiscontinuedDate.Year == 1974))");
+ }
+
+ [Fact]
+ public void DateHour()
+ {
+ VerifyQueryDeserialization("$filter=hour(DiscontinuedDate) eq 8",
+ "Where(Param_0 => (Param_0.DiscontinuedDate.Hour == 8))");
+ }
+
+ [Fact]
+ public void DateMinute()
+ {
+ VerifyQueryDeserialization(
+ "$filter=minute(DiscontinuedDate) eq 12",
+ "Where(Param_0 => (Param_0.DiscontinuedDate.Minute == 12))");
+ }
+
+ [Fact]
+ public void DateSecond()
+ {
+ VerifyQueryDeserialization(
+ "$filter=second(DiscontinuedDate) eq 33",
+ "Where(Param_0 => (Param_0.DiscontinuedDate.Second == 33))");
+ }
+ #endregion
+
+ #region Math Functions
+ [Fact]
+ public void MathRound()
+ {
+ VerifyQueryDeserialization(
+ "$filter=round(UnitPrice) gt 5.00",
+ "Where(Param_0 => (Round(Param_0.UnitPrice) > 5.00))");
+ }
+
+ [Fact]
+ public void MathFloor()
+ {
+ VerifyQueryDeserialization(
+ "$filter=floor(UnitPrice) eq 5",
+ "Where(Param_0 => (Floor(Param_0.UnitPrice) == 5))");
+ }
+
+ [Fact]
+ public void MathCeiling()
+ {
+ VerifyQueryDeserialization(
+ "$filter=ceiling(UnitPrice) eq 5",
+ "Where(Param_0 => (Ceiling(Param_0.UnitPrice) == 5))");
+ }
+ #endregion
+
+ #region Data Types
+ [Fact]
+ public void GuidExpression()
+ {
+ VerifyQueryDeserialization<DataTypes>(
+ "$filter=GuidProp eq guid'0EFDAECF-A9F0-42F3-A384-1295917AF95E'",
+ "Where(Param_0 => (Param_0.GuidProp == 0efdaecf-a9f0-42f3-a384-1295917af95e))");
+
+ // verify case insensitivity
+ VerifyQueryDeserialization<DataTypes>(
+ "$filter=GuidProp eq GuiD'0EFDAECF-A9F0-42F3-A384-1295917AF95E'",
+ "Where(Param_0 => (Param_0.GuidProp == 0efdaecf-a9f0-42f3-a384-1295917af95e))");
+ }
+
+ [Fact]
+ public void DateTimeExpression()
+ {
+ VerifyQueryDeserialization<DataTypes>(
+ "$filter=DateTimeProp lt datetime'2000-12-12T12:00'",
+ "Where(Param_0 => (Param_0.DateTimeProp < 12/12/2000 12:00:00 PM))");
+ }
+
+ [Fact]
+ public void DateTimeOffsetExpression()
+ {
+ VerifyQueryDeserialization<DataTypes>(
+ "$filter=DateTimeOffsetProp ge datetimeoffset'2002-10-10T17:00:00Z'",
+ "Where(Param_0 => (Param_0.DateTimeOffsetProp >= 10/10/2002 5:00:00 PM +00:00))");
+ }
+
+ [Fact]
+ public void TimeExpression()
+ {
+ VerifyQueryDeserialization<DataTypes>(
+ "$filter=TimeSpanProp ge time'13:20:00'",
+ "Where(Param_0 => (Param_0.TimeSpanProp >= 13:20:00))");
+
+ // verify parse error for invalid literal format
+ Assert.Throws<ParseException>(delegate
+ {
+ VerifyQueryDeserialization<DataTypes>("$filter=TimeSpanProp ge time'invalid'", String.Empty);
+ },
+ "String was not recognized as a valid TimeSpan. (at index 20)");
+ }
+
+ [Fact]
+ public void BinaryExpression()
+ {
+ string binary = "23ABFF";
+ byte[] bytes = new byte[] {
+ byte.Parse("23", Globalization.NumberStyles.HexNumber),
+ byte.Parse("AB", Globalization.NumberStyles.HexNumber),
+ byte.Parse("FF", Globalization.NumberStyles.HexNumber)
+ };
+
+ VerifyQueryDeserialization<DataTypes>(
+ String.Format("$filter=ByteArrayProp eq binary'{0}'", binary),
+ "Where(Param_0 => (Param_0.ByteArrayProp == value(System.Byte[])))",
+ q =>
+ {
+ // verify that the binary data was deserialized into a constant expression of type byte[]
+ LambdaExpression lex = (LambdaExpression)((UnaryExpression)((MethodCallExpression)q.Expression).Arguments[1]).Operand;
+ BinaryExpression bex = (BinaryExpression)lex.Body;
+ byte[] actualBytes = (byte[])((ConstantExpression)bex.Right).Value;
+ Assert.True(actualBytes.SequenceEqual(bytes));
+ });
+
+ // test alternate 'X' syntax
+ VerifyQueryDeserialization<DataTypes>(
+ String.Format("$filter=ByteArrayProp eq X'{0}'", binary),
+ "Where(Param_0 => (Param_0.ByteArrayProp == value(System.Byte[])))",
+ q =>
+ {
+ // verify that the binary data was deserialized into a constant expression of type byte[]
+ LambdaExpression lex = (LambdaExpression)((UnaryExpression)((MethodCallExpression)q.Expression).Arguments[1]).Operand;
+ BinaryExpression bex = (BinaryExpression)lex.Body;
+ byte[] actualBytes = (byte[])((ConstantExpression)bex.Right).Value;
+ Assert.True(actualBytes.SequenceEqual(bytes));
+ });
+
+ // verify parse error for invalid literal format
+ Assert.Throws<ParseException>(delegate
+ {
+ VerifyQueryDeserialization<DataTypes>(
+ String.Format("$filter=ByteArrayProp eq binary'{0}'", "WXYZ"), String.Empty);
+ },
+ "Input string was not in a correct format. (at index 23)");
+
+ // verify parse error for invalid hex literal (odd hex strings are not supported)
+ Assert.Throws<ParseException>(delegate
+ {
+ VerifyQueryDeserialization<DataTypes>(
+ String.Format("$filter=ByteArrayProp eq binary'23A'", "XYZ"), String.Empty);
+ },
+ "Invalid hexadecimal literal. (at index 23)");
+ }
+
+ [Fact]
+ public void IntegerLiteralSuffix()
+ {
+ // long L
+ VerifyQueryDeserialization<DataTypes>(
+ "$filter=LongProp lt 987654321L and LongProp gt 123456789l",
+ "Where(Param_0 => ((Param_0.LongProp < 987654321) AndAlso (Param_0.LongProp > 123456789)))");
+
+ VerifyQueryDeserialization<DataTypes>(
+ "$filter=LongProp lt -987654321L and LongProp gt -123456789l",
+ "Where(Param_0 => ((Param_0.LongProp < -987654321) AndAlso (Param_0.LongProp > -123456789)))");
+ }
+
+ [Fact]
+ public void RealLiteralSuffixes()
+ {
+ // Float F
+ VerifyQueryDeserialization<DataTypes>(
+ "$filter=FloatProp lt 4321.56F and FloatProp gt 1234.56f",
+ "Where(Param_0 => ((Param_0.FloatProp < 4321.56) AndAlso (Param_0.FloatProp > 1234.56)))");
+
+ // Decimal M
+ VerifyQueryDeserialization<DataTypes>(
+ "$filter=DecimalProp lt 4321.56M and DecimalProp gt 1234.56m",
+ "Where(Param_0 => ((Param_0.DecimalProp < 4321.56) AndAlso (Param_0.DecimalProp > 1234.56)))");
+ }
+ #endregion
+
+ #region Negative tests
+ [Fact]
+ public void InvalidTypeCreationExpression()
+ {
+ // underminated string literal
+ Assert.Throws<ParseException>(delegate
+ {
+ VerifyQueryDeserialization<DataTypes>("$filter=TimeSpanProp ge time'13:20:00", String.Empty);
+ },
+ "Unterminated string literal (at index 29)");
+
+ // use of parens rather than quotes
+ Assert.Throws<ParseException>(delegate
+ {
+ VerifyQueryDeserialization<DataTypes>("$filter=TimeSpanProp ge time(13:20:00)", String.Empty);
+ },
+ "Invalid 'time' type creation expression. (at index 16)");
+
+ // verify the exception returned when type expression that isn't
+ // one of the supported keyword types is used. In this case it falls
+ // through as a member expression
+ Assert.Throws<ParseException>(delegate
+ {
+ VerifyQueryDeserialization("$filter=math'123' eq true", String.Empty);
+ },
+ "No property or field 'math' exists in type 'Product' (at index 0)");
+ }
+
+ [Fact]
+ public void InvalidMethodCall()
+ {
+ // incorrect casing of supported method
+ Assert.Throws<ParseException>(delegate
+ {
+ VerifyQueryDeserialization("$filter=Startswith(ProductName, 'Abc') eq true", String.Empty);
+ },
+ "Unknown identifier 'Startswith' (at index 0)");
+
+ // attempt to access a method defined on the entity type
+ Assert.Throws<ParseException>(delegate
+ {
+ VerifyQueryDeserialization<DataTypes>("$filter=Inaccessable() eq \"Bar\"", String.Empty);
+ },
+ "Unknown identifier 'Inaccessable' (at index 0)");
+
+ // verify that Type methods like string.PadLeft, etc. are not supported.
+ Assert.Throws<ParseException>(delegate
+ {
+ VerifyQueryDeserialization("$filter=ProductName/PadLeft(100000000000000000000) eq \"Foo\"", String.Empty);
+ },
+ "Unknown identifier 'PadLeft' (at index 12)");
+ }
+
+ [Fact]
+ public void InvalidQueryParameterToTop()
+ {
+ Assert.Throws<InvalidOperationException>(
+ () => VerifyQueryDeserialization("$top=-42", String.Empty),
+ "The OData query parameter '$top' has an invalid value. The value should be a positive integer. The provided value was '-42'");
+ }
+
+ [Fact]
+ public void InvalidQueryParameterToSkip()
+ {
+ Assert.Throws<InvalidOperationException>(
+ () => VerifyQueryDeserialization("$skip=-42", String.Empty),
+ "The OData query parameter '$skip' has an invalid value. The value should be a positive integer. The provided value was '-42'");
+ }
+
+ [Fact]
+ public void InvalidFunctionCall_DoesntStartWithOpenParen()
+ {
+ Assert.Throws<ParseException>(
+ () => VerifyQueryDeserialization("$filter=length%n(ProductName) eq 12", String.Empty),
+ "'(' expected (at index 6)");
+ }
+
+ [Fact]
+ public void InvalidFunctionCall_EmptyArguments()
+ {
+ Assert.Throws<ParseException>(
+ () => VerifyQueryDeserialization("$filter=length() eq 12", String.Empty),
+ "No applicable method 'Length' exists in type 'System.String' (at index 0)");
+ }
+ #endregion
+
+ [Fact(DisplayName = "ODataQueryDeserializer is internal.")]
+ public void TypeIsCorrect()
+ {
+ Assert.Type.HasProperties(typeof(ODataQueryDeserializer), TypeAssert.TypeProperties.IsStatic | TypeAssert.TypeProperties.IsClass);
+ }
+
+ /// <summary>
+ /// Call the query deserializer and verify the results
+ /// </summary>
+ /// <param name="queryString">The URL query string to deserialize (e.g. $filter=ProductName eq 'Doritos')</param>
+ /// <param name="expectedResult">The Expression.ToString() representation of the expected result (e.g. Where(Param_0 => (Param_0.ProductName == \"Doritos\"))</param>
+ private void VerifyQueryDeserialization(string queryString, string expectedResult)
+ {
+ VerifyQueryDeserialization<Product>(queryString, expectedResult, null);
+ }
+
+ private void VerifyQueryDeserialization<T>(string queryString, string expectedResult)
+ {
+ VerifyQueryDeserialization<T>(queryString, expectedResult, null);
+ }
+
+ private void VerifyQueryDeserialization<T>(string queryString, string expectedResult, Action<IQueryable<T>> verify)
+ {
+ string uri = "http://myhost/odata.svc/Get?" + queryString;
+
+ IQueryable<T> baseQuery = new T[0].AsQueryable();
+ IQueryable<T> resultQuery = (IQueryable<T>)ODataQueryDeserializer.Deserialize(baseQuery, new Uri(uri));
+ VerifyExpression(resultQuery, expectedResult);
+
+ if (verify != null)
+ {
+ verify(resultQuery);
+ }
+
+ QueryValidator.Instance.Validate(resultQuery);
+ }
+
+ private void VerifyExpression(IQueryable query, string expectedExpression)
+ {
+ // strip off the beginning part of the expression to get to the first
+ // actual query operator
+ string resultExpression = query.Expression.ToString();
+ int startIdx = (query.ElementType.FullName + "[]").Length + 1;
+ resultExpression = resultExpression.Substring(startIdx);
+
+ Assert.True(resultExpression == expectedExpression,
+ String.Format("Expected expression '{0}' but the deserializer produced '{1}'", expectedExpression, resultExpression));
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Query/QueryValidatorTest.cs b/test/System.Web.Http.Test/Query/QueryValidatorTest.cs
new file mode 100644
index 00000000..ff5875b4
--- /dev/null
+++ b/test/System.Web.Http.Test/Query/QueryValidatorTest.cs
@@ -0,0 +1,93 @@
+using System.Linq;
+using System.Runtime.Serialization;
+using System.Web.TestUtil;
+using System.Xml.Serialization;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Query
+{
+ public class QueryValidatorTest
+ {
+ QueryValidator _queryValidator = QueryValidator.Instance;
+
+ [Fact]
+ public void XmlIgnoreAttributeCausesInvalidOperation()
+ {
+ IQueryable<QueryValidatorSampleClass> query = new QueryValidatorSampleClass[0].AsQueryable();
+
+ Assert.Throws<InvalidOperationException>(
+ () => _queryValidator.Validate(query.Where((sample) => sample.XmlIgnoreProperty == 0)),
+ "No property or field 'XmlIgnoreProperty' exists in type 'QueryValidatorSampleClass'");
+ }
+
+ [Fact]
+ public void IgnoreDataMemberAttributeCausesInvalidOperation()
+ {
+ IQueryable<QueryValidatorSampleClass> query = new QueryValidatorSampleClass[0].AsQueryable();
+
+ Assert.Throws<InvalidOperationException>(
+ () => _queryValidator.Validate(query.Where((sample) => sample.IgnoreDataMemberProperty == 0)),
+ "No property or field 'IgnoreDataMemberProperty' exists in type 'QueryValidatorSampleClass'");
+ }
+
+ [Fact]
+ public void NonSerializedAttributeCausesInvalidOperation()
+ {
+ IQueryable<QueryValidatorSampleClass> query = new QueryValidatorSampleClass[0].AsQueryable();
+
+ Assert.Throws<InvalidOperationException>(
+ () => _queryValidator.Validate(query.Where((sample) => sample.NonSerializedAttributeField == 0)),
+ "No property or field 'NonSerializedAttributeField' exists in type 'QueryValidatorSampleClass'");
+ }
+
+ [Fact]
+ public void NormalPropertyAccessDoesnotThrow()
+ {
+ IQueryable<QueryValidatorSampleClass> query = new QueryValidatorSampleClass[0].AsQueryable();
+
+ _queryValidator.Validate(query.Where((sample) => sample.NormalProperty == 0));
+ }
+
+ [Fact]
+ public void DataContractDataMemberPropertyAccessDoesnotThrow()
+ {
+ IQueryable<QueryValidatorSampleDataContractClass> query = new QueryValidatorSampleDataContractClass[0].AsQueryable();
+
+ _queryValidator.Validate(query.Where((sample) => sample.DataMemberProperty == 0));
+ }
+
+ [Fact]
+ public void DataContractNonDataMemberPropertyAccessCausesInvalidOperation()
+ {
+ IQueryable<QueryValidatorSampleDataContractClass> query = new QueryValidatorSampleDataContractClass[0].AsQueryable();
+
+ Assert.Throws<InvalidOperationException>(
+ () => _queryValidator.Validate(query.Where((sample) => sample.NonDataMemberProperty == 0)),
+ "No property or field 'NonDataMemberProperty' exists in type 'QueryValidatorSampleDataContractClass'");
+ }
+
+ public class QueryValidatorSampleClass
+ {
+ [XmlIgnore]
+ public int XmlIgnoreProperty { get; set; }
+
+ [IgnoreDataMember]
+ public int IgnoreDataMemberProperty { get; set; }
+
+ [NonSerialized]
+ public int NonSerializedAttributeField = 0;
+
+ public int NormalProperty { get; set; }
+ }
+
+ [DataContract]
+ public class QueryValidatorSampleDataContractClass
+ {
+ [DataMember]
+ public int DataMemberProperty { get; set; }
+
+ public int NonDataMemberProperty { get; set; }
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Routing/HttpRouteTest.cs b/test/System.Web.Http.Test/Routing/HttpRouteTest.cs
new file mode 100644
index 00000000..689030ca
--- /dev/null
+++ b/test/System.Web.Http.Test/Routing/HttpRouteTest.cs
@@ -0,0 +1,27 @@
+using System.Net.Http;
+using Xunit;
+using Xunit.Extensions;
+
+namespace System.Web.Http.Routing
+{
+ public class HttpRouteTest
+ {
+ [Theory]
+ [InlineData("123 456")]
+ [InlineData("123 {456")]
+ [InlineData("123 [45]6")]
+ [InlineData("123 (4)56")]
+ [InlineData("abc+56")]
+ [InlineData("abc.56")]
+ [InlineData("abc*56")]
+ [InlineData(@"hello12.1[)]*^$=!@23}")]
+ public void GetRouteData_HandlesUrlEncoding(string id)
+ {
+ HttpRoute route = new HttpRoute("{controller}/{id}");
+ Uri uri = new Uri("http://localhost/test/" + Uri.EscapeDataString(id) + "/");
+ IHttpRouteData routeData = route.GetRouteData("", new HttpRequestMessage(HttpMethod.Get, uri));
+ Assert.Equal("test", routeData.Values["controller"]);
+ Assert.Equal(id, routeData.Values["id"]);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Routing/UrlHelperTest.cs b/test/System.Web.Http.Test/Routing/UrlHelperTest.cs
new file mode 100644
index 00000000..2ba3e4f8
--- /dev/null
+++ b/test/System.Web.Http.Test/Routing/UrlHelperTest.cs
@@ -0,0 +1,149 @@
+using System.Collections.Generic;
+using System.Web.Http.Controllers;
+using Microsoft.TestCommon;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Routing
+{
+ public class UrlHelperTest
+ {
+ [Fact]
+ public void UrlHelper_CtorThrows_WithNullContext()
+ {
+ Assert.ThrowsArgumentNull(
+ () => new UrlHelper(null),
+ "controllerContext");
+ }
+
+ [Fact]
+ public void ControllerContext_HasUrlHelperWithValidContext()
+ {
+ HttpControllerContext cc = new HttpControllerContext();
+
+ Assert.NotNull(cc.Url);
+ Assert.IsType<UrlHelper>(cc.Url);
+ Assert.Same(cc, cc.Url.ControllerContext);
+ }
+
+ [Theory]
+ [PropertyData("UrlGeneratorTestData")]
+ public void UrlHelper_UsesCurrentRouteDataToPopulateValues_WithObjectValues(string controller, int? id, string expectedUrl)
+ {
+ var url = GetUrlHelperForApi();
+
+ object routeValues = null;
+ if (controller == null)
+ {
+ if (id == null)
+ {
+ routeValues = null;
+ }
+ else
+ {
+ routeValues = new { id };
+ }
+ }
+ else
+ {
+ if (id == null)
+ {
+ routeValues = new { controller };
+ }
+ else
+ {
+ routeValues = new { controller, id };
+ }
+ }
+ string generatedUrl = url.Route("route1", routeValues);
+
+ Assert.Equal(expectedUrl, generatedUrl);
+ }
+
+ [Theory]
+ [PropertyData("UrlGeneratorTestData")]
+ public void UrlHelper_UsesCurrentRouteDataToPopulateValues_WithDictionaryValues(string controller, int? id, string expectedUrl)
+ {
+ var url = GetUrlHelperForApi();
+
+ Dictionary<string, object> routeValues = new Dictionary<string, object>();
+ if (controller == null)
+ {
+ if (id == null)
+ {
+ routeValues = null;
+ }
+ else
+ {
+ routeValues.Add("id", id);
+ }
+ }
+ else
+ {
+ if (id == null)
+ {
+ routeValues.Add("controller", controller);
+ }
+ else
+ {
+ routeValues.Add("controller", controller);
+ routeValues.Add("id", id);
+ }
+ }
+ string generatedUrl = url.Route("route1", routeValues);
+
+ Assert.Equal(expectedUrl, generatedUrl);
+ }
+
+ [Fact]
+ public void UrlHelper_Throws_WhenWrongNameUsed_WithObjectValues()
+ {
+ var url = GetUrlHelperForApi();
+ Assert.ThrowsArgument(
+ () => url.Route("route-doesn't-exist", null),
+ "name",
+ "A route named 'route-doesn't-exist' could not be found in the route collection.");
+ }
+
+ [Fact]
+ public void UrlHelper_Throws_WhenWrongNameUsed_WithDictionaryValues()
+ {
+ var url = GetUrlHelperForApi();
+ Assert.ThrowsArgument(
+ () => url.Route("route-doesn't-exist", (IDictionary<string, object>)null),
+ "name",
+ "A route named 'route-doesn't-exist' could not be found in the route collection.");
+ }
+
+ private static UrlHelper GetUrlHelperForApi()
+ {
+ HttpControllerContext cc = new HttpControllerContext();
+
+ // Set up routes
+ var routes = new HttpRouteCollection("/somerootpath");
+ IHttpRoute route = routes.MapHttpRoute("route1", "{controller}/{id}");
+ cc.Configuration = new HttpConfiguration(routes);
+
+ cc.RouteData = new HttpRouteData(route, new HttpRouteValueDictionary(new { controller = "people", id = "123" }));
+
+ return cc.Url;
+ }
+
+ public static IEnumerable<object[]> UrlGeneratorTestData
+ {
+ get
+ {
+ return new TheoryDataSet<string, int?, string>()
+ {
+ { null, 456, "/somerootpath/people/456"}, // Just override ID, so ID is replaced
+ { "people", 456, "/somerootpath/people/456"}, // Just override ID, so ID is replaced
+ { null, null, "/somerootpath/people/123"}, // Override nothing, so everything the same
+ { "people", null, "/somerootpath/people/123"}, // Override nothing, so everything the same
+ { "customers", 456, "/somerootpath/customers/456"}, // Override everything, so everything changed
+ { "customers", null, null}, // Override controller, which clears out the ID, so it doesn't match (i.e. null)
+ };
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Services/DependencyResolverTests.cs b/test/System.Web.Http.Test/Services/DependencyResolverTests.cs
new file mode 100644
index 00000000..0b121c4c
--- /dev/null
+++ b/test/System.Web.Http.Test/Services/DependencyResolverTests.cs
@@ -0,0 +1,219 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Web.Http.Controllers;
+using Microsoft.TestCommon;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Services
+{
+ public class DependencyResolverTests
+ {
+ // TODO: Add tests for SetService and GetCachedService
+
+ [Fact]
+ public void TypeIsCorrect()
+ {
+ Assert.Type.HasProperties<DependencyResolver>(TypeAssert.TypeProperties.IsPublicVisibleClass);
+ }
+
+ [Fact]
+ public void ConstructorThrowsOnNullConfig()
+ {
+ Assert.ThrowsArgumentNull(() => new DependencyResolver(null), "configuration");
+ Assert.ThrowsArgumentNull(() => new DependencyResolver(null, null), "configuration");
+ }
+
+ [Fact]
+ public void ConstructorWithUserNullDependencyResolver()
+ {
+ // Arrange
+ HttpConfiguration config = new HttpConfiguration();
+ DependencyResolver resolver = new DependencyResolver(config, null);
+
+ // Act
+ object service = resolver.GetService(typeof(IHttpActionSelector));
+
+ // Assert
+ Assert.Null(service);
+ }
+
+ [Fact]
+ public void ConstructorWithUserDependencyResolver()
+ {
+ // Arrange
+ HttpConfiguration config = new HttpConfiguration();
+ Mock<IDependencyResolver> userResolverMock = new Mock<IDependencyResolver>();
+ IHttpActionSelector actionSelector = new Mock<IHttpActionSelector>().Object;
+ userResolverMock.Setup(ur => ur.GetService(typeof(IHttpActionSelector))).Returns(actionSelector).Verifiable();
+ DependencyResolver resolver = new DependencyResolver(config, userResolverMock.Object);
+
+ // Act
+ object service = resolver.GetService(typeof(IHttpActionSelector));
+
+ // Assert
+ userResolverMock.Verify();
+ Assert.Same(actionSelector, service);
+ }
+
+ [Fact]
+ public void GetServiceThrowsOnNull()
+ {
+ // Arrange
+ HttpConfiguration config = new HttpConfiguration();
+ DependencyResolver resolver = new DependencyResolver(config, null);
+
+ // Act
+ Assert.ThrowsArgumentNull(() => resolver.GetService(null), "serviceType");
+ }
+
+ [Fact]
+ public void GetServiceDoesntEagerlyCreate()
+ {
+ // Arrange
+ HttpConfiguration config = new HttpConfiguration();
+ DependencyResolver resolver = new DependencyResolver(config);
+
+ // Act
+ object result = resolver.GetService(typeof(SomeClass));
+
+ // Assert
+ // Service resolver should not have created an instance or arbitrary class.
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void GetServicesThrowsOnNull()
+ {
+ // Arrange
+ HttpConfiguration config = new HttpConfiguration();
+ DependencyResolver resolver = new DependencyResolver(config, null);
+
+ // Act
+ Assert.ThrowsArgumentNull(() => resolver.GetServices(null), "serviceType");
+ }
+
+ [Fact]
+ public void GetServicesDoesntEagerlyCreate()
+ {
+ // Arrange
+ HttpConfiguration config = new HttpConfiguration();
+ DependencyResolver resolver = new DependencyResolver(config);
+
+ // Act
+ IEnumerable<object> result = resolver.GetServices(typeof(SomeClass));
+
+ // Assert
+ // Service resolver should not have created an instance or arbitrary class.
+ Assert.Empty(result);
+ }
+
+ // Arbitrary test class that we can use with the service resolver
+ internal class SomeClass
+ {
+ }
+
+ [Fact]
+ public void SetResolverIDependencyResolverThrowsOnNull()
+ {
+ HttpConfiguration config = new HttpConfiguration();
+ DependencyResolver resolver = new DependencyResolver(config);
+ Assert.ThrowsArgumentNull(() => resolver.SetResolver((IDependencyResolver)null), "resolver");
+ }
+
+ [Fact]
+ public void SetResolverIDependencyResolver()
+ {
+ // Arrange
+ HttpConfiguration config = new HttpConfiguration();
+ DependencyResolver resolver = new DependencyResolver(config);
+
+ Mock<IDependencyResolver> userResolverMock = new Mock<IDependencyResolver>();
+ IHttpActionSelector actionSelector = new Mock<IHttpActionSelector>().Object;
+ userResolverMock.Setup(ur => ur.GetService(typeof(IHttpActionSelector))).Returns(actionSelector).Verifiable();
+ userResolverMock.Setup(ur => ur.GetServices(typeof(IHttpActionSelector))).Returns(new List<object> { actionSelector }).Verifiable();
+
+ resolver.SetResolver(userResolverMock.Object);
+
+ // Act
+ object service = resolver.GetService(typeof(IHttpActionSelector));
+ IEnumerable<object> services = resolver.GetServices(typeof(IHttpActionSelector));
+
+ // Assert
+ userResolverMock.Verify();
+ Assert.Same(actionSelector, service);
+ Assert.Same(actionSelector, services.ElementAt(0));
+ }
+
+ [Fact]
+ public void SetResolverCommonServiceLocatorThrowsOnNull()
+ {
+ HttpConfiguration config = new HttpConfiguration();
+ DependencyResolver resolver = new DependencyResolver(config);
+ Assert.ThrowsArgumentNull(() => resolver.SetResolver((object)null), "commonServiceLocator");
+ }
+
+ [Fact]
+ public void SetResolverCommonServiceLocator()
+ {
+ // Arrange
+ HttpConfiguration config = new HttpConfiguration();
+ DependencyResolver resolver = new DependencyResolver(config);
+
+ Mock<CommonServiceLocatorSlim> userResolverMock = new Mock<CommonServiceLocatorSlim>();
+ IHttpActionSelector actionSelector = new Mock<IHttpActionSelector>().Object;
+ userResolverMock.Setup(ur => ur.GetInstance(typeof(IHttpActionSelector))).Returns(actionSelector).Verifiable();
+ userResolverMock.Setup(ur => ur.GetAllInstances(typeof(IHttpActionSelector))).Returns(new List<object> { actionSelector }).Verifiable();
+
+ resolver.SetResolver(userResolverMock.Object);
+
+ // Act
+ object service = resolver.GetService(typeof(IHttpActionSelector));
+ IEnumerable<object> services = resolver.GetServices(typeof(IHttpActionSelector));
+
+ // Assert
+ userResolverMock.Verify();
+ Assert.Same(actionSelector, service);
+ Assert.Same(actionSelector, services.ElementAt(0));
+ }
+
+ public interface CommonServiceLocatorSlim
+ {
+ object GetInstance(Type serviceType);
+
+ IEnumerable<object> GetAllInstances(Type serviceType);
+ }
+
+ [Fact]
+ public void SetResolverFuncThrowsOnNull()
+ {
+ HttpConfiguration config = new HttpConfiguration();
+ DependencyResolver resolver = new DependencyResolver(config);
+ Assert.ThrowsArgumentNull(() => resolver.SetResolver(null, _ => null), "getService");
+ Assert.ThrowsArgumentNull(() => resolver.SetResolver(_ => null, null), "getServices");
+ }
+
+ [Fact]
+ public void SetResolverFunc()
+ {
+ // Arrange
+ HttpConfiguration config = new HttpConfiguration();
+ DependencyResolver resolver = new DependencyResolver(config);
+
+ Mock<CommonServiceLocatorSlim> userResolverMock = new Mock<CommonServiceLocatorSlim>();
+ IHttpActionSelector actionSelector = new Mock<IHttpActionSelector>().Object;
+
+ resolver.SetResolver(_ => actionSelector, _ => new List<object> { actionSelector });
+
+ // Act
+ object service = resolver.GetService(typeof(IHttpActionSelector));
+ IEnumerable<object> services = resolver.GetServices(typeof(IHttpActionSelector));
+
+ // Assert
+ userResolverMock.Verify();
+ Assert.Same(actionSelector, service);
+ Assert.Same(actionSelector, services.ElementAt(0));
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/System.Web.Http.Test.csproj b/test/System.Web.Http.Test/System.Web.Http.Test.csproj
new file mode 100644
index 00000000..d6e7815d
--- /dev/null
+++ b/test/System.Web.Http.Test/System.Web.Http.Test.csproj
@@ -0,0 +1,247 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProductVersion>9.0.30729</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{7F2C796F-43B2-4F8F-ABFF-A154EC8AAFA1}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>System.Web.Http</RootNamespace>
+ <AssemblyName>System.Web.Http.Test</AssemblyName>
+ <FileAlignment>512</FileAlignment>
+ <ProjectTypeGuids>{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Debug\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Release\Test\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'CodeCoverage' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\CodeCoverage\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="Moq, Version=4.0.10827.0, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL">
+ <HintPath>..\..\packages\Moq.4.0.10827\lib\NET40\Moq.dll</HintPath>
+ </Reference>
+ <Reference Include="System" />
+ <Reference Include="System.ComponentModel.DataAnnotations" />
+ <Reference Include="System.Configuration" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.Data.Linq" />
+ <Reference Include="System.Net.Http">
+ <HintPath>..\..\packages\Microsoft.Net.Http.2.0.20302.1\lib\net40\System.Net.Http.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Net.Http.WebRequest">
+ <HintPath>..\..\packages\Microsoft.Net.Http.2.0.20302.1\lib\net40\System.Net.Http.WebRequest.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Runtime.Serialization" />
+ <Reference Include="System.Web" />
+ <Reference Include="System.XML" />
+ <Reference Include="xunit">
+ <HintPath>..\..\packages\xunit.1.9.0.1566\lib\xunit.dll</HintPath>
+ </Reference>
+ <Reference Include="xunit.extensions">
+ <HintPath>..\..\packages\xunit.extensions.1.9.0.1566\lib\xunit.extensions.dll</HintPath>
+ </Reference>
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="AuthorizeAttributeTest.cs" />
+ <Compile Include="Controllers\Apis\User.cs" />
+ <Compile Include="Controllers\Apis\UsersRpcController.cs" />
+ <Compile Include="Controllers\HttpControllerContextTest.cs" />
+ <Compile Include="Controllers\HttpConfigurationTest.cs" />
+ <Compile Include="DictionaryExtensionsTest.cs" />
+ <Compile Include="Filters\HttpFilterCollectionTest.cs" />
+ <Compile Include="HttpServerTest.cs" />
+ <Compile Include="HttpRequestMessageExtensionsTest.cs" />
+ <Compile Include="HttpRouteCollectionExtensionsTest.cs" />
+ <Compile Include="Dispatcher\DefaultBuildManagerTest.cs" />
+ <Compile Include="Filters\QueryCompositionFilterAttributeTest.cs" />
+ <Compile Include="Filters\EnumerableEvaluatorFilterProviderTest.cs" />
+ <Compile Include="Controllers\ApiControllerActionInvokerTest.cs" />
+ <Compile Include="Controllers\ApiControllerActionSelectorTest.cs" />
+ <Compile Include="Controllers\HttpParameterDescriptorTest.cs" />
+ <Compile Include="Controllers\ReflectedHttpParameterDescriptorTest.cs" />
+ <Compile Include="Controllers\HttpControllerDescriptorTest.cs" />
+ <Compile Include="Controllers\ReflectedHttpActionDescriptorTest.cs" />
+ <Compile Include="Controllers\HttpActionContextTest.cs" />
+ <Compile Include="Filters\QueryCompositionFilterProviderTest.cs" />
+ <Compile Include="Filters\EnumerableEvaluatorFilterTest.cs" />
+ <Compile Include="Hosting\HttpRouteTest.cs" />
+ <Compile Include="Internal\TypeActivatorTest.cs" />
+ <Compile Include="ModelBinding\FormDataCollectionExtensionsTest.cs" />
+ <Compile Include="ModelBinding\ModelBinderAttributeTest.cs" />
+ <Compile Include="Query\DataModel.cs" />
+ <Compile Include="Query\ODataQueryDeserializerTests.cs" />
+ <Compile Include="Routing\HttpRouteTest.cs" />
+ <Compile Include="Routing\UrlHelperTest.cs" />
+ <Compile Include="Services\DependencyResolverTests.cs" />
+ <Compile Include="Tracing\FormattingUtilitiesTest.cs" />
+ <Compile Include="Tracing\HttpRequestMessageExtensionsTest.cs" />
+ <Compile Include="Tracing\ITraceWriterExtensionsTest.cs" />
+ <Compile Include="Tracing\TestTraceWriter.cs" />
+ <Compile Include="Tracing\TraceManagerTest.cs" />
+ <Compile Include="Tracing\TraceRecordComparer.cs" />
+ <Compile Include="Tracing\Tracers\HttpActionBindingTracerTest.cs" />
+ <Compile Include="Tracing\Tracers\HttpActionDescriptorTracerTest.cs" />
+ <Compile Include="Tracing\Tracers\ActionFilterAttributeTracerTest.cs" />
+ <Compile Include="Tracing\Tracers\ActionFilterTracerTest.cs" />
+ <Compile Include="Tracing\Tracers\HttpActionInvokerTracerTest.cs" />
+ <Compile Include="Tracing\Tracers\HttpActionSelectorTracerTest.cs" />
+ <Compile Include="Tracing\Tracers\ActionValueBinderTracerTest.cs" />
+ <Compile Include="Tracing\Tracers\HttpControllerTracerTest.cs" />
+ <Compile Include="Tracing\Tracers\AuthorizationFilterAttributeTracerTest.cs" />
+ <Compile Include="Tracing\Tracers\AuthorizationFilterTracerTest.cs" />
+ <Compile Include="Tracing\Tracers\ContentNegotiatorTracerTest.cs" />
+ <Compile Include="Tracing\Tracers\HttpControllerActivatorTracerTest.cs" />
+ <Compile Include="Tracing\Tracers\HttpControllerFactoryTracerTest.cs" />
+ <Compile Include="Tracing\Tracers\ExceptionFilterAttributeTracerTest.cs" />
+ <Compile Include="Tracing\Tracers\ExceptionFilterTracerTest.cs" />
+ <Compile Include="Tracing\Tracers\FilterTracerTest.cs" />
+ <Compile Include="Tracing\Tracers\FormatterParameterBindingTracerTest.cs" />
+ <Compile Include="Tracing\Tracers\HttpParameterBindingTracerTest.cs" />
+ <Compile Include="Tracing\Tracers\MediaTypeFormatterTracerTest.cs" />
+ <Compile Include="Tracing\Tracers\MessageHandlerTracerTest.cs" />
+ <Compile Include="Tracing\Tracers\RequestMessageHandlerTracerTest.cs" />
+ <Compile Include="Util\ContextUtil.cs" />
+ <Compile Include="Query\QueryValidatorTest.cs" />
+ <Compile Include="Filters\HttpActionExecutedContextTest.cs" />
+ <Compile Include="Filters\ActionFilterAttributeTest.cs" />
+ <Compile Include="Filters\ActionDescriptorFilterProviderTest.cs" />
+ <Compile Include="Filters\AuthorizationFilterAttributeTest.cs" />
+ <Compile Include="Filters\ExceptionFilterAttributeTest.cs" />
+ <Compile Include="Filters\FilterAttributeTest.cs" />
+ <Compile Include="Filters\FilterInfoComparerTest.cs" />
+ <Compile Include="Filters\FilterInfoTest.cs" />
+ <Compile Include="Filters\ConfigurationFilterProviderTest.cs" />
+ <Compile Include="ModelBinding\CompositeModelBinderTest.cs" />
+ <Compile Include="ModelBinding\DefaultActionValueBinderTest.cs" />
+ <Compile Include="Controllers\ApiControllerTest.cs" />
+ <Compile Include="Controllers\Apis\UsersController.cs" />
+ <Compile Include="HttpBindingBehaviorAttributeTest.cs" />
+ <Compile Include="Internal\CollectionModelBinderUtilTest.cs" />
+ <Compile Include="ModelBinding\Binders\ArrayModelBinderProviderTest.cs" />
+ <Compile Include="ModelBinding\Binders\ArrayModelBinderTest.cs" />
+ <Compile Include="ModelBinding\Binders\BinaryDataModelBinderProviderTest.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="ModelBinding\Binders\CollectionModelBinderProviderTest.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="ModelBinding\Binders\CollectionModelBinderTest.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="ModelBinding\Binders\ComplexModelDtoModelBinderProviderTest.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="ModelBinding\Binders\ComplexModelDtoModelBinderTest.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="ModelBinding\Binders\ComplexModelDtoResultTest.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="ModelBinding\Binders\ComplexModelDtoTest.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="ModelBinding\Binders\DictionaryModelBinderProviderTest.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="ModelBinding\Binders\DictionaryModelBinderTest.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="ModelBinding\Binders\GenericModelBinderProviderTest.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="ModelBinding\Binders\KeyValuePairModelBinderProviderTest.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="ModelBinding\Binders\KeyValuePairModelBinderTest.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="ModelBinding\Binders\KeyValuePairModelBinderUtilTest.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="ModelBinding\Binders\MutableObjectModelBinderProviderTest.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="ModelBinding\Binders\MutableObjectModelBinderTest.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="ModelBinding\Binders\SimpleModelBinderProviderTest.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="ModelBinding\Binders\TypeConverterModelBinderProviderTest.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="ModelBinding\Binders\TypeConverterModelBinderTest.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="ModelBinding\Binders\TypeMatchModelBinderProviderTest.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="ModelBinding\Binders\TypeMatchModelBinderTest.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="ModelBinding\ModelBinderConfigTest.cs" />
+ <Compile Include="ModelBinding\ModelBindingUtilTest.cs" />
+ <Compile Include="ModelBinding\ModelBindingContextTest.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="Util\SimpleHttpValueProvider.cs" />
+ <Compile Include="Validation\DefaultBodyModelValidatorTest.cs" />
+ <Compile Include="Validation\ModelValidationNodeTest.cs" />
+ <Compile Include="Validation\ModelValidationRequiredMemberSelectorTest.cs" />
+ <Compile Include="ValueProviders\Providers\KeyValueModelValueProviderTest.cs" />
+ <Compile Include="ValueProviders\Providers\NameValueCollectionValueProviderTest.cs" />
+ <Compile Include="ValueProviders\Providers\QueryStringValueProviderTest.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="packages.config" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\System.Json\System.Json.csproj">
+ <Project>{F0441BE9-BDC0-4629-BE5A-8765FFAA2481}</Project>
+ <Name>System.Json</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\src\System.Net.Http.Formatting\System.Net.Http.Formatting.csproj">
+ <Project>{668E9021-CE84-49D9-98FB-DF125A9FCDB0}</Project>
+ <Name>System.Net.Http.Formatting</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\src\System.Web.Http.Common\System.Web.Http.Common.csproj">
+ <Project>{03A5E5F2-2E23-48F2-ABCC-6C41BAC9AC02}</Project>
+ <Name>System.Web.Http.Common</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\src\System.Web.Http\System.Web.Http.csproj">
+ <Project>{DDC1CE0C-486E-4E35-BB3B-EAB61F8F9440}</Project>
+ <Name>System.Web.Http</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\Microsoft.TestCommon\Microsoft.TestCommon.csproj">
+ <Project>{FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0}</Project>
+ <Name>Microsoft.TestCommon</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <Folder Include="Controllers\Helpers\" />
+ </ItemGroup>
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+</Project> \ No newline at end of file
diff --git a/test/System.Web.Http.Test/Tracing/FormattingUtilitiesTest.cs b/test/System.Web.Http.Test/Tracing/FormattingUtilitiesTest.cs
new file mode 100644
index 00000000..7617959f
--- /dev/null
+++ b/test/System.Web.Http.Test/Tracing/FormattingUtilitiesTest.cs
@@ -0,0 +1,283 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Globalization;
+using System.Linq;
+using System.Net.Http.Formatting;
+using System.Web.Http.Controllers;
+using System.Web.Http.ModelBinding;
+using System.Web.Http.ModelBinding.Binders;
+using System.Web.Http.Routing;
+using System.Web.Http.ValueProviders;
+using System.Web.Http.ValueProviders.Providers;
+using Microsoft.TestCommon;
+using Moq;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Tracing
+{
+ public class FormattingUtilitiesTest
+ {
+ [Theory]
+ [TestDataSet(typeof(CommonUnitTestDataSets), "RefTypeTestDataCollection")]
+ public void ValueToString_Formats(Type variationType, object testData)
+ {
+ // Arrange
+ string expected = Convert.ToString(testData, CultureInfo.CurrentCulture);
+
+ // Act
+ string actual = FormattingUtilities.ValueToString(testData, CultureInfo.CurrentCulture);
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void ValueToString_Formats_Null_Value()
+ {
+ // Arrange & Act
+ string actual = FormattingUtilities.ValueToString(null, CultureInfo.CurrentCulture);
+
+ // Assert
+ Assert.Equal("null", actual);
+ }
+
+ [Fact]
+ public void ActionArgumentsToString_Formats()
+ {
+ // Arrange
+ Dictionary<string, object> arguments = new Dictionary<string, object>()
+ {
+ {"p1", 1},
+ {"p2", true}
+ };
+
+ string expected = String.Format("p1={0}, p2={1}",
+ FormattingUtilities.ValueToString(arguments["p1"], CultureInfo.CurrentCulture),
+ FormattingUtilities.ValueToString(arguments["p2"], CultureInfo.CurrentCulture));
+
+ // Act
+ string actual = FormattingUtilities.ActionArgumentsToString(arguments);
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void ActionDescriptorToString_Formats()
+ {
+ // Arrange
+ Mock<HttpParameterDescriptor> paramDescriptor1 = new Mock<HttpParameterDescriptor>() { CallBase = true };
+ paramDescriptor1.Setup(p => p.ParameterName).Returns("p1");
+ paramDescriptor1.Setup(p => p.ParameterType).Returns(typeof(int));
+ Mock<HttpParameterDescriptor> paramDescriptor2 = new Mock<HttpParameterDescriptor>() { CallBase = true };
+ paramDescriptor2.Setup(p => p.ParameterName).Returns("p2");
+ paramDescriptor2.Setup(p => p.ParameterType).Returns(typeof(bool));
+
+ Collection<HttpParameterDescriptor> parameterCollection = new Collection<HttpParameterDescriptor>(
+ new HttpParameterDescriptor[] { paramDescriptor1.Object, paramDescriptor2.Object });
+ Mock<HttpActionDescriptor> mockActionDescriptor = new Mock<HttpActionDescriptor>() { CallBase = true };
+ mockActionDescriptor.Setup(a => a.GetParameters()).Returns(parameterCollection);
+ mockActionDescriptor.Setup(a => a.ActionName).Returns("SampleAction");
+
+ string expected = String.Format("SampleAction({0} p1, {1} p2)", typeof(int).Name, typeof(bool).Name);
+
+ // Act
+ string actual = FormattingUtilities.ActionDescriptorToString(mockActionDescriptor.Object);
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void ActionInvokeToString_Formats()
+ {
+ // Arrange
+ Dictionary<string, object> arguments = new Dictionary<string, object>()
+ {
+ {"p1", 1},
+ {"p2", true}
+ };
+
+ string expected = String.Format("SampleAction(p1={0}, p2={1})",
+ FormattingUtilities.ValueToString(arguments["p1"], CultureInfo.CurrentCulture),
+ FormattingUtilities.ValueToString(arguments["p2"], CultureInfo.CurrentCulture));
+
+ // Act
+ string actual = FormattingUtilities.ActionInvokeToString("SampleAction", arguments);
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void ActionInvokeToString_With_ActionContext_Formats()
+ {
+ // Arrange
+ Mock<HttpParameterDescriptor> paramDescriptor1 = new Mock<HttpParameterDescriptor>() { CallBase = true };
+ paramDescriptor1.Setup(p => p.ParameterName).Returns("p1");
+ paramDescriptor1.Setup(p => p.ParameterType).Returns(typeof(int));
+ Mock<HttpParameterDescriptor> paramDescriptor2 = new Mock<HttpParameterDescriptor>() { CallBase = true };
+ paramDescriptor2.Setup(p => p.ParameterName).Returns("p2");
+ paramDescriptor2.Setup(p => p.ParameterType).Returns(typeof(bool));
+
+ Collection<HttpParameterDescriptor> parameterCollection = new Collection<HttpParameterDescriptor>(
+ new HttpParameterDescriptor[] { paramDescriptor1.Object, paramDescriptor2.Object });
+ Mock<HttpActionDescriptor> mockActionDescriptor = new Mock<HttpActionDescriptor>() { CallBase = true };
+ mockActionDescriptor.Setup(a => a.GetParameters()).Returns(parameterCollection);
+ mockActionDescriptor.Setup(a => a.ActionName).Returns("SampleAction");
+
+ HttpActionContext actionContext =
+ ContextUtil.CreateActionContext(actionDescriptor: mockActionDescriptor.Object);
+ actionContext.ActionArguments["p1"] = 1;
+ actionContext.ActionArguments["p2"] = true;
+
+ string expected = String.Format("SampleAction(p1={0}, p2={1})",
+ FormattingUtilities.ValueToString(actionContext.ActionArguments["p1"], CultureInfo.CurrentCulture),
+ FormattingUtilities.ValueToString(actionContext.ActionArguments["p2"], CultureInfo.CurrentCulture));
+
+ // Act
+ string actual = FormattingUtilities.ActionInvokeToString(actionContext);
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void FormattersToString_Formats()
+ {
+ // Arrange
+ MediaTypeFormatterCollection formatters = new MediaTypeFormatterCollection();
+ string expected = String.Join(", ", formatters.Select<MediaTypeFormatter, string>((f) => f.GetType().Name));
+
+ // Act
+ string actual = FormattingUtilities.FormattersToString(formatters);
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void ModelBinderToString_Formats()
+ {
+ // Arrange
+ ModelBinderProvider provider = new SimpleModelBinderProvider(typeof (int), () => null);
+ string expected = typeof (SimpleModelBinderProvider).Name;
+
+ // Act
+ string actual = FormattingUtilities.ModelBinderToString(provider);
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void ModelBinderToString_With_CompositeModelBinder_Formats()
+ {
+ // Arrange
+ ModelBinderProvider innerProvider1 = new SimpleModelBinderProvider(typeof(int), () => null);
+ ModelBinderProvider innerProvider2 = new ArrayModelBinderProvider();
+ CompositeModelBinderProvider compositeProvider = new CompositeModelBinderProvider(new ModelBinderProvider[] { innerProvider1, innerProvider2 });
+ string expected = String.Format(
+ "{0}({1}, {2})",
+ typeof(CompositeModelBinderProvider).Name,
+ typeof(SimpleModelBinderProvider).Name,
+ typeof(ArrayModelBinderProvider).Name);
+
+ // Act
+ string actual = FormattingUtilities.ModelBinderToString(compositeProvider);
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void ValueProviderToString_Formats()
+ {
+ // Arrange
+ IValueProvider provider = new ElementalValueProvider("unused", 1, CultureInfo.CurrentCulture);
+ string expected = typeof(ElementalValueProvider).Name;
+
+ // Act
+ string actual = FormattingUtilities.ValueProviderToString(provider);
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void ValueProviderToString_With_CompositeProvider_Formats()
+ {
+ // Arrange
+ List<IValueProvider> providers = new List<IValueProvider>()
+ {
+ new ElementalValueProvider("unused", 1, CultureInfo.CurrentCulture),
+ new NameValueCollectionValueProvider(() => null, CultureInfo.CurrentCulture)
+ };
+
+ CompositeValueProvider compositeProvider = new CompositeValueProvider(providers);
+ string expected = String.Format(
+ "{0}({1}, {2})",
+ typeof(CompositeValueProvider).Name,
+ typeof(ElementalValueProvider).Name,
+ typeof(NameValueCollectionValueProvider).Name);
+
+ // Act
+ string actual = FormattingUtilities.ValueProviderToString(compositeProvider);
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void RouteToString_Formats()
+ {
+ // Arrange
+ Dictionary<string, object> routeDictionary = new Dictionary<string, object>()
+ {
+ {"r1", "c1"},
+ {"r2", "c2"}
+ };
+ Mock<IHttpRouteData> mockRouteData = new Mock<IHttpRouteData>() { CallBase = true};
+ mockRouteData.Setup(r => r.Values).Returns(routeDictionary);
+ string expected = "r1:c1,r2:c2";
+
+ // Act
+ string actual = FormattingUtilities.RouteToString(mockRouteData.Object);
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void ModelStateToString_Formats_With_Valid_ModelState()
+ {
+ // Arrange
+ ModelStateDictionary modelState = new ModelStateDictionary();
+ string expected = String.Empty;
+
+ // Act
+ string actual = FormattingUtilities.ModelStateToString(modelState);
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void ModelStateToString_Formats_With_InValid_ModelState()
+ {
+ // Arrange
+ ModelStateDictionary modelState = new ModelStateDictionary();
+ modelState.AddModelError("p1", "is bad");
+
+ string expected = "p1: is bad";
+
+ // Act
+ string actual = FormattingUtilities.ModelStateToString(modelState);
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Tracing/HttpRequestMessageExtensionsTest.cs b/test/System.Web.Http.Test/Tracing/HttpRequestMessageExtensionsTest.cs
new file mode 100644
index 00000000..8da38a7f
--- /dev/null
+++ b/test/System.Web.Http.Test/Tracing/HttpRequestMessageExtensionsTest.cs
@@ -0,0 +1,24 @@
+using System.Net.Http;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Tracing
+{
+ public class HttpRequestMessageExtensionsTest
+ {
+ [Fact]
+ public void GetCorrelationId_Returns_Valid_Guid()
+ {
+ // Arrange
+ HttpRequestMessage request = new HttpRequestMessage();
+
+ // Act
+ Guid guid1 = request.GetCorrelationId();
+ Guid guid2 = request.GetCorrelationId();
+
+ // Assert
+ Assert.Equal(guid1, guid2);
+ Assert.NotEqual(guid1, Guid.Empty);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Tracing/ITraceWriterExtensionsTest.cs b/test/System.Web.Http.Test/Tracing/ITraceWriterExtensionsTest.cs
new file mode 100644
index 00000000..facca2cd
--- /dev/null
+++ b/test/System.Web.Http.Test/Tracing/ITraceWriterExtensionsTest.cs
@@ -0,0 +1,1004 @@
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Tracing
+{
+ public class ITraceWriterExtensionsTest
+ {
+ [Fact]
+ public void Debug_With_Message_Traces()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(request, "testCategory", TraceLevel.Debug) { Kind = TraceKind.Trace, Message = "The formatted message" },
+ };
+
+ // Act
+ traceWriter.Debug(request, "testCategory", "The {0} message", "formatted");
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ }
+
+ [Fact]
+ public void Debug_With_Exception_Traces()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ InvalidOperationException exception = new InvalidOperationException();
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(request, "testCategory", TraceLevel.Debug) { Kind = TraceKind.Trace, Exception = exception },
+ };
+
+ // Act
+ traceWriter.Debug(request, "testCategory", exception);
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ }
+
+ [Fact]
+ public void Debug_With_Message_And_Exception_Traces()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ InvalidOperationException exception = new InvalidOperationException();
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(request, "testCategory", TraceLevel.Debug) { Kind = TraceKind.Trace, Message = "The formatted message", Exception = exception },
+ };
+
+ // Act
+ traceWriter.Debug(request, "testCategory", exception, "The {0} message", "formatted");
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ }
+
+ [Fact]
+ public void Info_With_Message_Traces()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(request, "testCategory", TraceLevel.Info) { Kind = TraceKind.Trace, Message = "The formatted message" },
+ };
+
+ // Act
+ traceWriter.Info(request, "testCategory", "The {0} message", "formatted");
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ }
+
+ [Fact]
+ public void Info_With_Exception_Traces()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ InvalidOperationException exception = new InvalidOperationException();
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(request, "testCategory", TraceLevel.Info) { Kind = TraceKind.Trace, Exception = exception },
+ };
+
+ // Act
+ traceWriter.Info(request, "testCategory", exception);
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ }
+
+ [Fact]
+ public void Info_With_Message_And_Exception_Traces()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ InvalidOperationException exception = new InvalidOperationException();
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(request, "testCategory", TraceLevel.Info) { Kind = TraceKind.Trace, Message = "The formatted message", Exception = exception },
+ };
+
+ // Act
+ traceWriter.Info(request, "testCategory", exception, "The {0} message", "formatted");
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ }
+
+ [Fact]
+ public void Warn_With_Message_Traces()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(request, "testCategory", TraceLevel.Warn) { Kind = TraceKind.Trace, Message = "The formatted message" },
+ };
+
+ // Act
+ traceWriter.Warn(request, "testCategory", "The {0} message", "formatted");
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ }
+
+ [Fact]
+ public void Warn_With_Exception_Traces()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ InvalidOperationException exception = new InvalidOperationException();
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(request, "testCategory", TraceLevel.Warn) { Kind = TraceKind.Trace, Exception = exception },
+ };
+
+ // Act
+ traceWriter.Warn(request, "testCategory", exception);
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ }
+
+ [Fact]
+ public void Warn_With_Message_And_Exception_Traces()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ InvalidOperationException exception = new InvalidOperationException();
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(request, "testCategory", TraceLevel.Warn) { Kind = TraceKind.Trace, Message = "The formatted message", Exception = exception },
+ };
+
+ // Act
+ traceWriter.Warn(request, "testCategory", exception, "The {0} message", "formatted");
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ }
+
+ [Fact]
+ public void Error_With_Message_Traces()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(request, "testCategory", TraceLevel.Error) { Kind = TraceKind.Trace, Message = "The formatted message" },
+ };
+
+ // Act
+ traceWriter.Error(request, "testCategory", "The {0} message", "formatted");
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ }
+
+ [Fact]
+ public void Error_With_Exception_Traces()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ InvalidOperationException exception = new InvalidOperationException();
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(request, "testCategory", TraceLevel.Error) { Kind = TraceKind.Trace, Exception = exception },
+ };
+
+ // Act
+ traceWriter.Error(request, "testCategory", exception);
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ }
+
+ [Fact]
+ public void Error_With_Message_And_Exception_Traces()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ InvalidOperationException exception = new InvalidOperationException();
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(request, "testCategory", TraceLevel.Error) { Kind = TraceKind.Trace, Message = "The formatted message", Exception = exception },
+ };
+
+ // Act
+ traceWriter.Error(request, "testCategory", exception, "The {0} message", "formatted");
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ }
+
+ [Fact]
+ public void Fatal_With_Message_Traces()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(request, "testCategory", TraceLevel.Fatal) { Kind = TraceKind.Trace, Message = "The formatted message" },
+ };
+
+ // Act
+ traceWriter.Fatal(request, "testCategory", "The {0} message", "formatted");
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ }
+
+ [Fact]
+ public void Fatal_With_Exception_Traces()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ InvalidOperationException exception = new InvalidOperationException();
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(request, "testCategory", TraceLevel.Fatal) { Kind = TraceKind.Trace, Exception = exception },
+ };
+
+ // Act
+ traceWriter.Fatal(request, "testCategory", exception);
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ }
+
+ [Fact]
+ public void Fatal_With_Message_And_Exception_Traces()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ InvalidOperationException exception = new InvalidOperationException();
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(request, "testCategory", TraceLevel.Fatal) { Kind = TraceKind.Trace, Message = "The formatted message", Exception = exception },
+ };
+
+ // Act
+ traceWriter.Fatal(request, "testCategory", exception, "The {0} message", "formatted");
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ }
+
+ [Fact]
+ public void TraceBeginEnd_Throws_With_Null_This()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = null;
+ HttpRequestMessage request = new HttpRequestMessage();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(() => traceWriter.TraceBeginEnd(request,
+ "",
+ TraceLevel.Off,
+ "",
+ "",
+ beginTrace: null,
+ execute: () => { },
+ endTrace: null,
+ errorTrace: null),
+ "traceWriter");
+ }
+
+ [Fact]
+ public void TraceBeginEnd_Throws_With_Null_Execute_Action()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(() => traceWriter.TraceBeginEnd(request,
+ "",
+ TraceLevel.Off,
+ "",
+ "",
+ beginTrace: null,
+ execute: null,
+ endTrace: null,
+ errorTrace: null),
+ "execute");
+ }
+
+ [Fact]
+ public void TraceBeginEnd_Accepts_Null_Trace_Actions()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+
+ // Act & Assert
+ traceWriter.TraceBeginEnd(request,
+ "",
+ TraceLevel.Off,
+ "",
+ "",
+ beginTrace: null,
+ execute: () => { },
+ endTrace: null,
+ errorTrace: null);
+ }
+
+ [Fact]
+ public void TraceBeginEnd_Invokes_BeginTrace()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ bool invoked = false;
+
+ // Act
+ traceWriter.TraceBeginEnd(request,
+ "",
+ TraceLevel.Fatal,
+ "",
+ "",
+ beginTrace: (tr) => { invoked = true; },
+ execute: () => { },
+ endTrace: (tr) => { },
+ errorTrace: (tr) => { });
+
+ // Assert
+ Assert.True(invoked);
+ }
+
+ [Fact]
+ public void TraceBeginEnd_Invokes_Execute()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ bool invoked = false;
+
+ // Act
+ traceWriter.TraceBeginEnd(request,
+ "",
+ TraceLevel.Fatal,
+ "",
+ "",
+ beginTrace: (tr) => { },
+ execute: () => { invoked = true; },
+ endTrace: (tr) => { },
+ errorTrace: (tr) => { });
+
+ // Assert
+ Assert.True(invoked);
+ }
+
+
+ [Fact]
+ public void TraceBeginEnd_Invokes_EndTrace()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ bool invoked = false;
+
+ // Act
+ traceWriter.TraceBeginEnd(request,
+ "",
+ TraceLevel.Off,
+ "",
+ "",
+ beginTrace: (tr) => { },
+ execute: () => { },
+ endTrace: (tr) => { invoked = true; },
+ errorTrace: (tr) => { });
+
+ // Assert
+ Assert.True(invoked);
+ }
+
+ [Fact]
+ public void TraceBeginEnd_Invokes_ErrorTrace()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ Exception exception = new InvalidOperationException();
+ bool invoked = false;
+
+ // Act
+ Exception thrown = Assert.Throws<InvalidOperationException>(
+ () => traceWriter.TraceBeginEnd(request,
+ "",
+ TraceLevel.Off,
+ "",
+ "",
+ beginTrace: (tr) => { },
+ execute: () => { throw exception; },
+ endTrace: (tr) => { },
+ errorTrace: (tr) => { invoked = true; }));
+
+ // Assert
+ Assert.True(invoked);
+ Assert.Same(exception, thrown);
+ }
+
+ [Fact]
+ public void TraceBeginEnd_Traces()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(request, "testCategory", TraceLevel.Info) { Kind = TraceKind.Begin, Operator = "tester", Operation = "testOp", Message = "beginMessage" },
+ new TraceRecord(request, "testCategory", TraceLevel.Info) { Kind = TraceKind.End, Operator = "tester", Operation = "testOp", Message = "endMessage" },
+ };
+
+ // Act
+ traceWriter.TraceBeginEnd(request,
+ "testCategory",
+ TraceLevel.Info,
+ "tester",
+ "testOp",
+ beginTrace: (tr) => { tr.Message = "beginMessage"; },
+ execute: () => { },
+ endTrace: (tr) => { tr.Message = "endMessage"; },
+ errorTrace: (tr) => { tr.Message = "won't happen"; });
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ }
+
+ [Fact]
+ public void TraceBeginEnd_Traces_And_Throws_When_Execute_Throws()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ InvalidOperationException exception = new InvalidOperationException("test exception");
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(request, "testCategory", TraceLevel.Info) { Kind = TraceKind.Begin, Operator = "tester", Operation = "testOp", Message = "beginMessage" },
+ new TraceRecord(request, "testCategory", TraceLevel.Error) { Kind = TraceKind.End, Operator = "tester", Operation = "testOp", Exception = exception, Message = "errorMessage" },
+ };
+
+ // Act
+ Exception thrown = Assert.Throws<InvalidOperationException>(
+ () => traceWriter.TraceBeginEnd(request,
+ "testCategory",
+ TraceLevel.Info,
+ "tester",
+ "testOp",
+ beginTrace: (tr) => { tr.Message = "beginMessage"; },
+ execute: () => { throw exception; },
+ endTrace: (tr) => { tr.Message = "won't happen"; },
+ errorTrace: (tr) => { tr.Message = "errorMessage"; }));
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ Assert.Same(exception, thrown);
+ }
+
+ [Fact]
+ public void TraceBeginEndAsync_Throws_With_Null_This()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = null;
+ HttpRequestMessage request = new HttpRequestMessage();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(() => traceWriter.TraceBeginEndAsync(request,
+ "",
+ TraceLevel.Off,
+ "",
+ "",
+ beginTrace: null,
+ execute: () => TaskHelpers.Completed(),
+ endTrace: null,
+ errorTrace: null),
+ "traceWriter");
+ }
+
+ [Fact]
+ public void TraceBeginEndAsync_Throws_With_Null_Execute_Action()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(() => traceWriter.TraceBeginEndAsync(request,
+ "",
+ TraceLevel.Off,
+ "",
+ "",
+ beginTrace: null,
+ execute: null,
+ endTrace: null,
+ errorTrace: null),
+ "execute");
+ }
+
+ [Fact]
+ public void TraceBeginEndAsync_Accepts_Null_Trace_Actions()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+
+ // Act & Assert
+ Task t = traceWriter.TraceBeginEndAsync(request,
+ "",
+ TraceLevel.Off,
+ "",
+ "",
+ beginTrace: null,
+ execute: () => TaskHelpers.Completed(),
+ endTrace: null,
+ errorTrace: null);
+ t.Wait();
+ }
+
+ [Fact]
+ public void TraceBeginEndAsync_Invokes_BeginTrace()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ bool invoked = false;
+
+ // Act
+ traceWriter.TraceBeginEndAsync(request,
+ "",
+ TraceLevel.Fatal,
+ "",
+ "",
+ beginTrace: (tr) => { invoked = true; },
+ execute: () => TaskHelpers.Completed(),
+ endTrace: (tr) => { },
+ errorTrace: (tr) => { }).Wait();
+
+ // Assert
+ Assert.True(invoked);
+ }
+
+ [Fact]
+ public void TraceBeginEndAsync_Invokes_Execute()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ bool invoked = false;
+
+ // Act
+ traceWriter.TraceBeginEndAsync(request,
+ "",
+ TraceLevel.Fatal,
+ "",
+ "",
+ beginTrace: (tr) => { },
+ execute: () =>
+ {
+ invoked = true;
+ return TaskHelpers.Completed();
+ },
+ endTrace: (tr) => { },
+ errorTrace: (tr) => { }).Wait();
+
+ // Assert
+ Assert.True(invoked);
+ }
+
+ [Fact]
+ public void TraceBeginEndAsync_Invokes_EndTrace()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ bool invoked = false;
+
+ // Act
+ traceWriter.TraceBeginEndAsync(request,
+ "",
+ TraceLevel.Off,
+ "",
+ "",
+ beginTrace: (tr) => { },
+ execute: () => TaskHelpers.Completed(),
+ endTrace: (tr) => { invoked = true; },
+ errorTrace: (tr) => { }).Wait();
+
+ // Assert
+ Assert.True(invoked);
+ }
+
+ [Fact]
+ public void TraceBeginEndAsync_Invokes_ErrorTrace()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ bool invoked = false;
+ Exception exception = new InvalidOperationException();
+ TaskCompletionSource<object> tcs = new TaskCompletionSource<object>(null);
+ tcs.TrySetException(exception);
+
+ // Act
+ Exception thrown = Assert.Throws<InvalidOperationException>(
+ () => traceWriter.TraceBeginEndAsync(request,
+ "",
+ TraceLevel.Off,
+ "",
+ "",
+ beginTrace: (tr) => { },
+ execute: () => tcs.Task,
+ endTrace: (tr) => { },
+ errorTrace: (tr) => { invoked = true; }).Wait());
+
+ // Assert
+ Assert.True(invoked);
+ Assert.Same(exception, thrown);
+ }
+
+
+ [Fact]
+ public void TraceBeginEndAsync_Traces()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(request, "testCategory", TraceLevel.Info) { Kind = TraceKind.Begin, Operator = "tester", Operation = "testOp", Message = "beginMessage" },
+ new TraceRecord(request, "testCategory", TraceLevel.Info) { Kind = TraceKind.End, Operator = "tester", Operation = "testOp", Message = "endMessage" },
+ };
+
+ // Act
+ traceWriter.TraceBeginEndAsync(request,
+ "testCategory",
+ TraceLevel.Info,
+ "tester",
+ "testOp",
+ beginTrace: (tr) => { tr.Message = "beginMessage"; },
+ execute: () => TaskHelpers.Completed(),
+ endTrace: (tr) => { tr.Message = "endMessage"; },
+ errorTrace: (tr) => { tr.Message = "won't happen"; }).Wait();
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ }
+
+ [Fact]
+ public void TraceBeginEndAsync_Traces_When_Inner_Cancels()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(request, "testCategory", TraceLevel.Info) { Kind = TraceKind.Begin, Operator = "tester", Operation = "testOp", Message = "beginMessage" },
+ new TraceRecord(request, "testCategory", TraceLevel.Warn) { Kind = TraceKind.End, Operator = "tester", Operation = "testOp", Message = "errorMessage" },
+ };
+
+ // Act & Assert
+ Assert.Throws<TaskCanceledException>(
+ () => traceWriter.TraceBeginEndAsync(request,
+ "testCategory",
+ TraceLevel.Info,
+ "tester",
+ "testOp",
+ beginTrace: (tr) => { tr.Message = "beginMessage"; },
+ execute: () => TaskHelpers.Canceled(),
+ endTrace: (tr) => { tr.Message = "won't happen"; },
+ errorTrace: (tr) => { tr.Message = "errorMessage"; }).Wait());
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ }
+
+ [Fact]
+ public void TraceBeginAsync_Traces_And_Faults_When_Inner_Faults()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ InvalidOperationException exception = new InvalidOperationException();
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(request, "testCategory", TraceLevel.Info) { Kind = TraceKind.Begin, Operator = "tester", Operation = "testOp", Message = "beginMessage" },
+ new TraceRecord(request, "testCategory", TraceLevel.Error) { Kind = TraceKind.End, Operator = "tester", Operation = "testOp", Message = "errorMessage", Exception = exception },
+ };
+
+ TaskCompletionSource<object> tcs = new TaskCompletionSource<object>(null);
+ tcs.TrySetException(exception);
+
+ // Act & Assert
+ InvalidOperationException thrown = Assert.Throws<InvalidOperationException>(
+ () => traceWriter.TraceBeginEndAsync(request,
+ "testCategory",
+ TraceLevel.Info,
+ "tester",
+ "testOp",
+ beginTrace: (tr) => { tr.Message = "beginMessage"; },
+ execute: () => tcs.Task,
+ endTrace: (tr) => { tr.Message = "won't happen"; },
+ errorTrace: (tr) => { tr.Message = "errorMessage"; }).Wait());
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ Assert.Same(exception, thrown);
+ }
+
+ [Fact]
+ public void TraceBeginEndAsyncGeneric_Throws_With_Null_This()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = null;
+ HttpRequestMessage request = new HttpRequestMessage();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(() => traceWriter.TraceBeginEndAsync<int>(request,
+ "",
+ TraceLevel.Off,
+ "",
+ "",
+ beginTrace: null,
+ execute: () => TaskHelpers.FromResult<int>(1),
+ endTrace: null,
+ errorTrace: null),
+ "traceWriter");
+ }
+
+ [Fact]
+ public void TraceBeginEndAsyncGeneric_Throws_With_Null_Execute_Action()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(() => traceWriter.TraceBeginEndAsync<int>(request,
+ "",
+ TraceLevel.Off,
+ "",
+ "",
+ beginTrace: null,
+ execute: null,
+ endTrace: null,
+ errorTrace: null),
+ "execute");
+ }
+
+ [Fact]
+ public void TraceBeginEndAsyncGeneric_Accepts_Null_Trace_Actions()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+
+ // Act & Assert
+ Task t = traceWriter.TraceBeginEndAsync<int>(request,
+ "",
+ TraceLevel.Off,
+ "",
+ "",
+ beginTrace: null,
+ execute: () => TaskHelpers.FromResult<int>(1),
+ endTrace: null,
+ errorTrace: null);
+ t.Wait();
+ }
+
+ [Fact]
+ public void TraceBeginEndAsyncGeneric_Invokes_BeginTrace()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ bool invoked = false;
+
+ // Act
+ traceWriter.TraceBeginEndAsync<int>(request,
+ "",
+ TraceLevel.Fatal,
+ "",
+ "",
+ beginTrace: (tr) => { invoked = true; },
+ execute: () => TaskHelpers.FromResult<int>(1),
+ endTrace: (tr, value) => { },
+ errorTrace: (tr) => { }).Wait();
+
+ // Assert
+ Assert.True(invoked);
+ }
+
+ [Fact]
+ public void TraceBeginEndAsyncGeneric_Invokes_Execute()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ bool invoked = false;
+
+ // Act
+ traceWriter.TraceBeginEndAsync<int>(request,
+ "",
+ TraceLevel.Fatal,
+ "",
+ "",
+ beginTrace: (tr) => { },
+ execute: () =>
+ {
+ invoked = true;
+ return TaskHelpers.FromResult<int>(1);
+ },
+ endTrace: (tr, value) => { },
+ errorTrace: (tr) => { }).Wait();
+
+ // Assert
+ Assert.True(invoked);
+ }
+
+ [Fact]
+ public void TraceBeginEndAsyncGeneric_Invokes_EndTrace()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ bool invoked = false;
+ int invokedValue = 0;
+
+ // Act
+ traceWriter.TraceBeginEndAsync<int>(request,
+ "",
+ TraceLevel.Off,
+ "",
+ "",
+ beginTrace: (tr) => { },
+ execute: () => TaskHelpers.FromResult<int>(1),
+ endTrace: (tr, value) => { invoked = true; invokedValue = value; },
+ errorTrace: (tr) => { }).Wait();
+
+ // Assert
+ Assert.True(invoked);
+ Assert.Equal(1, invokedValue);
+ }
+
+ [Fact]
+ public void TraceBeginEndAsyncGeneric_Invokes_ErrorTrace()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ bool invoked = false;
+ Exception exception = new InvalidOperationException();
+ TaskCompletionSource<int> tcs = new TaskCompletionSource<int>(0);
+ tcs.TrySetException(exception);
+
+ // Act
+ Exception thrown = Assert.Throws<InvalidOperationException>(
+ () => traceWriter.TraceBeginEndAsync<int>(request,
+ "",
+ TraceLevel.Off,
+ "",
+ "",
+ beginTrace: (tr) => { },
+ execute: () => tcs.Task,
+ endTrace: (tr, value) => { },
+ errorTrace: (tr) => { invoked = true; }).Wait());
+
+ // Assert
+ Assert.True(invoked);
+ Assert.Same(exception, thrown);
+ }
+
+ [Fact]
+ public void TraceBeginEndAsyncGeneric_Traces()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(request, "testCategory", TraceLevel.Info) { Kind = TraceKind.Begin, Operator = "tester", Operation = "testOp", Message = "beginMessage" },
+ new TraceRecord(request, "testCategory", TraceLevel.Info) { Kind = TraceKind.End, Operator = "tester", Operation = "testOp", Message = "endMessage1" },
+ };
+
+ // Act
+ traceWriter.TraceBeginEndAsync<int>(request,
+ "testCategory",
+ TraceLevel.Info,
+ "tester",
+ "testOp",
+ beginTrace: (tr) => { tr.Message = "beginMessage"; },
+ execute: () => TaskHelpers.FromResult<int>(1),
+ endTrace: (tr, value) => { tr.Message = "endMessage" + value; },
+ errorTrace: (tr) => { tr.Message = "won't happen"; }).Wait();
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ }
+
+ [Fact]
+ public void TraceBeginEndAsyncGeneric_Traces_When_Inner_Cancels()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(request, "testCategory", TraceLevel.Info) { Kind = TraceKind.Begin, Operator = "tester", Operation = "testOp", Message = "beginMessage" },
+ new TraceRecord(request, "testCategory", TraceLevel.Warn) { Kind = TraceKind.End, Operator = "tester", Operation = "testOp", Message = "errorMessage" },
+ };
+
+ // Act & Assert
+ Assert.Throws<TaskCanceledException>(
+ () => traceWriter.TraceBeginEndAsync<int>(request,
+ "testCategory",
+ TraceLevel.Info,
+ "tester",
+ "testOp",
+ beginTrace: (tr) => { tr.Message = "beginMessage"; },
+ execute: () => TaskHelpers.Canceled<int>(),
+ endTrace: (tr, value) => { tr.Message = "won't happen"; },
+ errorTrace: (tr) => { tr.Message = "errorMessage"; }).Wait());
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ }
+
+ [Fact]
+ public void TraceBeginAsyncGeneric_Traces_And_Faults_When_Inner_Faults()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ InvalidOperationException exception = new InvalidOperationException();
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(request, "testCategory", TraceLevel.Info) { Kind = TraceKind.Begin, Operator = "tester", Operation = "testOp", Message = "beginMessage" },
+ new TraceRecord(request, "testCategory", TraceLevel.Error) { Kind = TraceKind.End, Operator = "tester", Operation = "testOp", Message = "errorMessage", Exception = exception },
+ };
+
+ TaskCompletionSource<int> tcs = new TaskCompletionSource<int>(1);
+ tcs.TrySetException(exception);
+
+ // Act & Assert
+ InvalidOperationException thrown = Assert.Throws<InvalidOperationException>(
+ () => traceWriter.TraceBeginEndAsync<int>(request,
+ "testCategory",
+ TraceLevel.Info,
+ "tester",
+ "testOp",
+ beginTrace: (tr) => { tr.Message = "beginMessage"; },
+ execute: () => tcs.Task,
+ endTrace: (tr, value) => { tr.Message = "won't happen"; },
+ errorTrace: (tr) => { tr.Message = "errorMessage"; }).Wait());
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ Assert.Same(exception, thrown);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Tracing/TestTraceWriter.cs b/test/System.Web.Http.Test/Tracing/TestTraceWriter.cs
new file mode 100644
index 00000000..34090935
--- /dev/null
+++ b/test/System.Web.Http.Test/Tracing/TestTraceWriter.cs
@@ -0,0 +1,30 @@
+using System.Collections.Generic;
+using System.Net.Http;
+
+namespace System.Web.Http.Tracing
+{
+ /// <summary>
+ /// Test spy used internally to capture <see cref="TraceRecord"/>s.
+ /// </summary>
+ internal class TestTraceWriter : ITraceWriter
+ {
+ private List<TraceRecord> _traceRecords = new List<TraceRecord>();
+
+ public IList<TraceRecord> Traces { get { return _traceRecords; } }
+
+ public bool IsEnabled(string category, TraceLevel level)
+ {
+ return true;
+ }
+
+ public void Trace(HttpRequestMessage request, string category, TraceLevel level, Action<TraceRecord> traceAction)
+ {
+ TraceRecord traceRecord = new TraceRecord(request, category, level);
+ traceAction(traceRecord);
+ lock (_traceRecords)
+ {
+ _traceRecords.Add(traceRecord);
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Tracing/TraceManagerTest.cs b/test/System.Web.Http.Test/Tracing/TraceManagerTest.cs
new file mode 100644
index 00000000..e1ecdfe8
--- /dev/null
+++ b/test/System.Web.Http.Test/Tracing/TraceManagerTest.cs
@@ -0,0 +1,137 @@
+using System.Net.Http;
+using System.Net.Http.Formatting;
+using System.Web.Http.Controllers;
+using System.Web.Http.Dispatcher;
+using System.Web.Http.Tracing.Tracers;
+using Moq;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Tracing
+{
+ public class TraceManagerTest
+ {
+ [Fact]
+ public void TraceManager_Is_In_Default_ServiceResolver()
+ {
+ // Arrange
+ HttpConfiguration config = new HttpConfiguration();
+
+ // Act
+ ITraceManager traceManager = config.ServiceResolver.GetService(typeof (ITraceManager)) as ITraceManager;
+
+ // Assert
+ Assert.IsType<TraceManager>(traceManager);
+ }
+
+ [Theory]
+ [InlineData(typeof(IHttpControllerFactory))]
+ [InlineData(typeof(IHttpControllerActivator))]
+ [InlineData(typeof(IHttpActionSelector))]
+ [InlineData(typeof(IHttpActionInvoker))]
+ [InlineData(typeof(IActionValueBinder))]
+ [InlineData(typeof(IContentNegotiator))]
+ public void Initialize_Does_Not_Alter_Configuration_When_No_TraceWriter_Present(Type serviceType)
+ {
+ // Arrange
+ HttpConfiguration config = new HttpConfiguration();
+ object defaultService = config.ServiceResolver.GetService(serviceType);
+
+ // Act
+ new TraceManager().Initialize(config);
+
+ // Assert
+ Assert.Same(defaultService.GetType(), config.ServiceResolver.GetService(serviceType).GetType());
+ }
+
+ [Theory]
+ [InlineData(typeof(IHttpControllerFactory))]
+ [InlineData(typeof(IHttpControllerActivator))]
+ [InlineData(typeof(IHttpActionSelector))]
+ [InlineData(typeof(IHttpActionInvoker))]
+ [InlineData(typeof(IActionValueBinder))]
+ [InlineData(typeof(IContentNegotiator))]
+ public void Initialize_Alters_Configuration_When_TraceWriter_Present(Type serviceType)
+ {
+ // Arrange
+ HttpConfiguration config = new HttpConfiguration();
+ Mock<ITraceWriter> traceWriter = new Mock<ITraceWriter>() { CallBase = true };
+ config.ServiceResolver.SetService(typeof(ITraceWriter), traceWriter.Object);
+ object defaultService = config.ServiceResolver.GetService(serviceType);
+
+ // Act
+ new TraceManager().Initialize(config);
+
+ // Assert
+ Assert.NotSame(defaultService.GetType(), config.ServiceResolver.GetService(serviceType).GetType());
+ }
+
+ [Fact]
+ public void Initialize_Does_Not_Alter_MessageHandlers_When_No_TraceWriter_Present()
+ {
+ // Arrange
+ HttpConfiguration config = new HttpConfiguration();
+ Mock<DelegatingHandler> mockHandler = new Mock<DelegatingHandler>() { CallBase = true };
+ config.MessageHandlers.Add(mockHandler.Object);
+
+ // Act
+ new TraceManager().Initialize(config);
+
+ // Assert
+ Assert.Equal(config.MessageHandlers[config.MessageHandlers.Count - 1].GetType(), mockHandler.Object.GetType());
+ }
+
+ [Fact]
+ public void Initialize_Alters_MessageHandlers_WhenTraceWriter_Present()
+ {
+ // Arrange
+ HttpConfiguration config = new HttpConfiguration();
+ Mock<ITraceWriter> traceWriter = new Mock<ITraceWriter>() { CallBase = true };
+ config.ServiceResolver.SetService(typeof(ITraceWriter), traceWriter.Object);
+ Mock<DelegatingHandler> mockHandler = new Mock<DelegatingHandler>() { CallBase = true };
+ config.MessageHandlers.Add(mockHandler.Object);
+
+ // Act
+ new TraceManager().Initialize(config);
+
+ // Assert
+ Assert.IsAssignableFrom<RequestMessageHandlerTracer>(config.MessageHandlers[config.MessageHandlers.Count - 1]);
+ Assert.IsAssignableFrom<MessageHandlerTracer>(config.MessageHandlers[config.MessageHandlers.Count - 2]);
+ }
+
+ [Fact]
+ public void Initialize_Does_Not_Alter_MediaTypeFormatters_When_No_TraceWriter_Present()
+ {
+ // Arrange
+ HttpConfiguration config = new HttpConfiguration();
+
+ // Act
+ new TraceManager().Initialize(config);
+
+ // Assert
+ foreach (var formatter in config.Formatters)
+ {
+ Assert.False(typeof(IFormatterTracer).IsAssignableFrom(formatter.GetType()));
+ }
+ }
+
+ [Fact]
+ public void Initialize_Alters_MediaTypeFormatters_WhenTraceWriter_Present()
+ {
+ // Arrange
+ HttpConfiguration config = new HttpConfiguration();
+ Mock<ITraceWriter> traceWriter = new Mock<ITraceWriter>() { CallBase = true };
+ config.ServiceResolver.SetService(typeof(ITraceWriter), traceWriter.Object);
+
+ // Act
+ new TraceManager().Initialize(config);
+
+ // Assert
+ foreach (var formatter in config.Formatters)
+ {
+ Assert.IsAssignableFrom<IFormatterTracer>(formatter);
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Tracing/TraceRecordComparer.cs b/test/System.Web.Http.Test/Tracing/TraceRecordComparer.cs
new file mode 100644
index 00000000..71953b35
--- /dev/null
+++ b/test/System.Web.Http.Test/Tracing/TraceRecordComparer.cs
@@ -0,0 +1,39 @@
+using System.Collections.Generic;
+
+namespace System.Web.Http.Tracing
+{
+ /// <summary>
+ /// Comparer class to allow xUnit asserts for <see cref="TraceRecord"/>.
+ /// </summary>
+ class TraceRecordComparer : IEqualityComparer<TraceRecord>
+ {
+ public bool Equals(TraceRecord x, TraceRecord y)
+ {
+ if (!String.Equals(x.Category, y.Category) ||
+ x.Level != y.Level ||
+ x.Kind != y.Kind ||
+ !Object.ReferenceEquals(x.Request, y.Request))
+ return false;
+
+ // The following must match only if they are present on 'x' -- the expected value
+ if (x.Exception != null && !Object.ReferenceEquals(x.Exception, y.Exception))
+ return false;
+
+ if (!String.IsNullOrEmpty(x.Message) && !String.Equals(x.Message, y.Message))
+ return false;
+
+ if (!String.IsNullOrEmpty(x.Operation) && !String.Equals(x.Operation, y.Operation))
+ return false;
+
+ if (!String.IsNullOrEmpty(x.Operator) && !String.Equals(x.Operator, y.Operator))
+ return false;
+
+ return true;
+ }
+
+ public int GetHashCode(TraceRecord obj)
+ {
+ return obj.GetHashCode();
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Tracing/Tracers/ActionFilterAttributeTracerTest.cs b/test/System.Web.Http.Test/Tracing/Tracers/ActionFilterAttributeTracerTest.cs
new file mode 100644
index 00000000..351fd604
--- /dev/null
+++ b/test/System.Web.Http.Test/Tracing/Tracers/ActionFilterAttributeTracerTest.cs
@@ -0,0 +1,108 @@
+using System.Collections.ObjectModel;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Controllers;
+using System.Web.Http.Filters;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Tracing.Tracers
+{
+ public class ActionFilterAttributeTracerTest
+ {
+ [Fact]
+ public void ExecuteActionFilterAsync_Traces_Executing_And_Executed()
+ {
+ // Arrange
+ Mock<ActionFilterAttribute> mockAttr = new Mock<ActionFilterAttribute>() { CallBase = true };
+ Mock<HttpActionDescriptor> mockActionDescriptor = new Mock<HttpActionDescriptor>() {CallBase = true};
+ mockActionDescriptor.Setup(a => a.ActionName).Returns("test");
+ mockActionDescriptor.Setup(a => a.GetParameters()).Returns(new Collection<HttpParameterDescriptor>(new HttpParameterDescriptor[0]));
+ HttpActionContext actionContext = ContextUtil.CreateActionContext(actionDescriptor: mockActionDescriptor.Object);
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ ActionFilterAttributeTracer tracer = new ActionFilterAttributeTracer(mockAttr.Object, traceWriter);
+ Func<Task<HttpResponseMessage>> continuation =
+ () => TaskHelpers.FromResult<HttpResponseMessage>(new HttpResponseMessage());
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(actionContext.Request, TraceCategories.FiltersCategory, TraceLevel.Info) { Kind = TraceKind.Begin, Operation = "ActionExecuting" },
+ new TraceRecord(actionContext.Request, TraceCategories.FiltersCategory, TraceLevel.Info) { Kind = TraceKind.End, Operation = "ActionExecuting" },
+ new TraceRecord(actionContext.Request, TraceCategories.FiltersCategory, TraceLevel.Info) { Kind = TraceKind.Begin, Operation = "ActionExecuted" },
+ new TraceRecord(actionContext.Request, TraceCategories.FiltersCategory, TraceLevel.Info) { Kind = TraceKind.End, Operation = "ActionExecuted" }
+ };
+
+ // Act
+ Task<HttpResponseMessage> task = ((IActionFilter) tracer).ExecuteActionFilterAsync(actionContext, CancellationToken.None, continuation);
+ task.Wait();
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ }
+
+ [Fact]
+ public void ExecuteActionFilterAsync_Faults_And_Traces_When_OnExecuting_Faults()
+ {
+ // Arrange
+ Mock<ActionFilterAttribute> mockAttr = new Mock<ActionFilterAttribute>() { CallBase = true };
+ InvalidOperationException exception = new InvalidOperationException("test");
+ mockAttr.Setup(a => a.OnActionExecuting(It.IsAny<HttpActionContext>())).Throws(exception);
+ Mock<HttpActionDescriptor> mockActionDescriptor = new Mock<HttpActionDescriptor>() { CallBase = true };
+ mockActionDescriptor.Setup(a => a.ActionName).Returns("test");
+ mockActionDescriptor.Setup(a => a.GetParameters()).Returns(new Collection<HttpParameterDescriptor>(new HttpParameterDescriptor[0]));
+ HttpActionContext actionContext = ContextUtil.CreateActionContext(actionDescriptor: mockActionDescriptor.Object);
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ ActionFilterAttributeTracer tracer = new ActionFilterAttributeTracer(mockAttr.Object, traceWriter);
+ Func<Task<HttpResponseMessage>> continuation =
+ () => TaskHelpers.FromResult<HttpResponseMessage>(new HttpResponseMessage());
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(actionContext.Request, TraceCategories.FiltersCategory, TraceLevel.Info) { Kind = TraceKind.Begin, Operation = "ActionExecuting" },
+ new TraceRecord(actionContext.Request, TraceCategories.FiltersCategory, TraceLevel.Error) { Kind = TraceKind.End, Operation = "ActionExecuting" }
+ };
+
+ // Act
+ Task<HttpResponseMessage> task = ((IActionFilter)tracer).ExecuteActionFilterAsync(actionContext, CancellationToken.None, continuation);
+
+ // Assert
+ Exception thrown = Assert.Throws<InvalidOperationException>(() => task.Wait());
+ Assert.Same(exception, thrown);
+ Assert.Same(exception, traceWriter.Traces[1].Exception);
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ }
+
+ [Fact]
+ public void ExecuteActionFilterAsync_Faults_And_Traces_When_OnExecuted_Faults()
+ {
+ // Arrange
+ Mock<ActionFilterAttribute> mockAttr = new Mock<ActionFilterAttribute>() { CallBase = true };
+ InvalidOperationException exception = new InvalidOperationException("test");
+ mockAttr.Setup(a => a.OnActionExecuted(It.IsAny<HttpActionExecutedContext>())).Throws(exception);
+ Mock<HttpActionDescriptor> mockActionDescriptor = new Mock<HttpActionDescriptor>() { CallBase = true };
+ mockActionDescriptor.Setup(a => a.ActionName).Returns("test");
+ mockActionDescriptor.Setup(a => a.GetParameters()).Returns(new Collection<HttpParameterDescriptor>(new HttpParameterDescriptor[0]));
+ HttpActionContext actionContext = ContextUtil.CreateActionContext(actionDescriptor: mockActionDescriptor.Object);
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ ActionFilterAttributeTracer tracer = new ActionFilterAttributeTracer(mockAttr.Object, traceWriter);
+ Func<Task<HttpResponseMessage>> continuation =
+ () => TaskHelpers.FromResult<HttpResponseMessage>(new HttpResponseMessage());
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(actionContext.Request, TraceCategories.FiltersCategory, TraceLevel.Info) { Kind = TraceKind.Begin, Operation = "ActionExecuting" },
+ new TraceRecord(actionContext.Request, TraceCategories.FiltersCategory, TraceLevel.Info) { Kind = TraceKind.End, Operation = "ActionExecuting" },
+ new TraceRecord(actionContext.Request, TraceCategories.FiltersCategory, TraceLevel.Info) { Kind = TraceKind.Begin, Operation = "ActionExecuted" },
+ new TraceRecord(actionContext.Request, TraceCategories.FiltersCategory, TraceLevel.Error) { Kind = TraceKind.End, Operation = "ActionExecuted" }
+ };
+
+ // Act
+ Task<HttpResponseMessage> task = ((IActionFilter)tracer).ExecuteActionFilterAsync(actionContext, CancellationToken.None, continuation);
+
+ // Assert
+ Exception thrown = Assert.Throws<InvalidOperationException>(() => task.Wait());
+ Assert.Same(exception, thrown);
+ Assert.Same(exception, traceWriter.Traces[3].Exception);
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Tracing/Tracers/ActionFilterTracerTest.cs b/test/System.Web.Http.Test/Tracing/Tracers/ActionFilterTracerTest.cs
new file mode 100644
index 00000000..4325ee3a
--- /dev/null
+++ b/test/System.Web.Http.Test/Tracing/Tracers/ActionFilterTracerTest.cs
@@ -0,0 +1,82 @@
+using System.Collections.ObjectModel;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Controllers;
+using System.Web.Http.Filters;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Tracing.Tracers
+{
+ public class ActionFilterTracerTest
+ {
+ [Fact]
+ public void ExecuteActionAsync_Traces_ExecuteActionFilterAsync()
+ {
+ // Arrange
+ HttpResponseMessage response = new HttpResponseMessage();
+ Mock<IActionFilter> mockFilter = new Mock<IActionFilter>() { CallBase = true };
+ mockFilter.Setup(
+ f =>
+ f.ExecuteActionFilterAsync(It.IsAny<HttpActionContext>(), It.IsAny<CancellationToken>(),
+ It.IsAny<Func<Task<HttpResponseMessage>>>())).Returns(
+ TaskHelpers.FromResult<HttpResponseMessage>(response));
+ Mock<HttpActionDescriptor> mockActionDescriptor = new Mock<HttpActionDescriptor>() { CallBase = true };
+ mockActionDescriptor.Setup(a => a.ActionName).Returns("test");
+ mockActionDescriptor.Setup(a => a.GetParameters()).Returns(new Collection<HttpParameterDescriptor>(new HttpParameterDescriptor[0]));
+ HttpActionContext actionContext = ContextUtil.CreateActionContext(actionDescriptor: mockActionDescriptor.Object);
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ ActionFilterTracer tracer = new ActionFilterTracer(mockFilter.Object, traceWriter);
+ Func<Task<HttpResponseMessage>> continuation =
+ () => TaskHelpers.FromResult<HttpResponseMessage>(new HttpResponseMessage());
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(actionContext.Request, TraceCategories.FiltersCategory, TraceLevel.Info) { Kind = TraceKind.Begin, Operation = "ExecuteActionFilterAsync" },
+ new TraceRecord(actionContext.Request, TraceCategories.FiltersCategory, TraceLevel.Info) { Kind = TraceKind.End, Operation = "ExecuteActionFilterAsync" },
+ };
+
+ // Act
+ Task<HttpResponseMessage> task = ((IActionFilter)tracer).ExecuteActionFilterAsync(actionContext, CancellationToken.None, continuation);
+ task.Wait();
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ }
+
+ [Fact]
+ public void ExecuteActionAsync_Faults_And_Traces_When_Inner_Faults()
+ {
+ // Arrange
+ InvalidOperationException exception = new InvalidOperationException("test");
+ Mock<IActionFilter> mockFilter = new Mock<IActionFilter>() { CallBase = true };
+ TaskCompletionSource<HttpResponseMessage> tcs = new TaskCompletionSource<HttpResponseMessage>(null);
+ tcs.TrySetException(exception);
+ mockFilter.Setup(f => f.ExecuteActionFilterAsync(It.IsAny<HttpActionContext>(), It.IsAny<CancellationToken>(),
+ It.IsAny<Func<Task<HttpResponseMessage>>>())).Returns(tcs.Task);
+ Mock<HttpActionDescriptor> mockActionDescriptor = new Mock<HttpActionDescriptor>() { CallBase = true };
+ mockActionDescriptor.Setup(a => a.ActionName).Returns("test");
+ mockActionDescriptor.Setup(a => a.GetParameters()).Returns(new Collection<HttpParameterDescriptor>(new HttpParameterDescriptor[0]));
+ HttpActionContext actionContext = ContextUtil.CreateActionContext(actionDescriptor: mockActionDescriptor.Object);
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ ActionFilterTracer tracer = new ActionFilterTracer(mockFilter.Object, traceWriter);
+ Func<Task<HttpResponseMessage>> continuation =
+ () => TaskHelpers.FromResult<HttpResponseMessage>(new HttpResponseMessage());
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(actionContext.Request, TraceCategories.FiltersCategory, TraceLevel.Info) { Kind = TraceKind.Begin, Operation = "ExecuteActionFilterAsync" },
+ new TraceRecord(actionContext.Request, TraceCategories.FiltersCategory, TraceLevel.Error) { Kind = TraceKind.End, Operation = "ExecuteActionFilterAsync" },
+ };
+
+ // Act
+ Task<HttpResponseMessage> task = ((IActionFilter)tracer).ExecuteActionFilterAsync(actionContext, CancellationToken.None, continuation);
+
+
+ // Assert
+ Exception thrown = Assert.Throws<InvalidOperationException>(() => task.Wait());
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ Assert.Same(exception, traceWriter.Traces[1].Exception);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Tracing/Tracers/ActionValueBinderTracerTest.cs b/test/System.Web.Http.Test/Tracing/Tracers/ActionValueBinderTracerTest.cs
new file mode 100644
index 00000000..459281bd
--- /dev/null
+++ b/test/System.Web.Http.Test/Tracing/Tracers/ActionValueBinderTracerTest.cs
@@ -0,0 +1,71 @@
+using System.Collections.ObjectModel;
+using System.Net.Http;
+using System.Net.Http.Formatting;
+using System.Web.Http.Controllers;
+using System.Web.Http.ModelBinding;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Tracing.Tracers
+{
+ public class ActionValueBinderTracerTest
+ {
+ [Fact]
+ public void GetBinding_Invokes_Inner_And_Returns_ActionBinder_With_Tracing_HttpParameterBinding()
+ {
+ // Arrange
+ Mock<HttpActionDescriptor> mockActionDescriptor = new Mock<HttpActionDescriptor>() { CallBase = true };
+ mockActionDescriptor.Setup(a => a.ActionName).Returns("test");
+ mockActionDescriptor.Setup(a => a.GetParameters()).Returns(new Collection<HttpParameterDescriptor>(new HttpParameterDescriptor[0]));
+
+ Mock<HttpParameterDescriptor> mockParameterDescriptor = new Mock<HttpParameterDescriptor>() { CallBase = true };
+ Mock<HttpParameterBinding> mockParameterBinding = new Mock<HttpParameterBinding>(mockParameterDescriptor.Object) { CallBase = true };
+ HttpActionBinding actionBinding = new HttpActionBinding(mockActionDescriptor.Object, new HttpParameterBinding[] { mockParameterBinding.Object });
+
+ HttpControllerDescriptor controllerDescriptor = new HttpControllerDescriptor(new HttpConfiguration(), "controller", typeof(ApiController));
+
+ HttpControllerContext controllerContext = ContextUtil.CreateControllerContext(request: new HttpRequestMessage());
+ controllerContext.ControllerDescriptor = controllerDescriptor;
+
+ Mock<IActionValueBinder> mockBinder = new Mock<IActionValueBinder>() {CallBase = true};
+ mockBinder.Setup(b => b.GetBinding(It.IsAny<HttpActionDescriptor>())).Returns(actionBinding);
+ ActionValueBinderTracer tracer = new ActionValueBinderTracer(mockBinder.Object, new TestTraceWriter());
+
+ // Act
+ HttpActionBinding actualBinding = ((IActionValueBinder) tracer).GetBinding(mockActionDescriptor.Object);
+
+ // Assert
+ Assert.IsAssignableFrom<HttpParameterBindingTracer>(actualBinding.ParameterBindings[0]);
+ }
+
+ [Fact]
+ public void GetBinding_Invokes_Inner_And_Returns_ActionBinder_With_Tracing_FormatterParameterBinding()
+ {
+ // Arrange
+ Mock<HttpActionDescriptor> mockActionDescriptor = new Mock<HttpActionDescriptor>() { CallBase = true };
+ mockActionDescriptor.Setup(a => a.ActionName).Returns("test");
+ mockActionDescriptor.Setup(a => a.GetParameters()).Returns(new Collection<HttpParameterDescriptor>(new HttpParameterDescriptor[0]));
+
+ Mock<HttpParameterDescriptor> mockParameterDescriptor = new Mock<HttpParameterDescriptor>() { CallBase = true };
+ Mock<FormatterParameterBinding> mockParameterBinding = new Mock<FormatterParameterBinding>(mockParameterDescriptor.Object, new MediaTypeFormatterCollection(), null) { CallBase = true };
+ HttpActionBinding actionBinding = new HttpActionBinding(mockActionDescriptor.Object, new HttpParameterBinding[] { mockParameterBinding.Object });
+
+ HttpControllerDescriptor controllerDescriptor = new HttpControllerDescriptor(new HttpConfiguration(), "controller", typeof(ApiController));
+
+ HttpControllerContext controllerContext = ContextUtil.CreateControllerContext(request: new HttpRequestMessage());
+ controllerContext.ControllerDescriptor = controllerDescriptor;
+
+ Mock<IActionValueBinder> mockBinder = new Mock<IActionValueBinder>() { CallBase = true };
+ mockBinder.Setup(b => b.GetBinding(It.IsAny<HttpActionDescriptor>())).Returns(actionBinding);
+ ActionValueBinderTracer tracer = new ActionValueBinderTracer(mockBinder.Object, new TestTraceWriter());
+
+ // Act
+ HttpActionBinding actualBinding = ((IActionValueBinder)tracer).GetBinding(mockActionDescriptor.Object);
+
+ // Assert
+ Assert.IsAssignableFrom<FormatterParameterBindingTracer>(actualBinding.ParameterBindings[0]);
+ }
+
+ }
+}
diff --git a/test/System.Web.Http.Test/Tracing/Tracers/AuthorizationFilterAttributeTracerTest.cs b/test/System.Web.Http.Test/Tracing/Tracers/AuthorizationFilterAttributeTracerTest.cs
new file mode 100644
index 00000000..c3c1b60a
--- /dev/null
+++ b/test/System.Web.Http.Test/Tracing/Tracers/AuthorizationFilterAttributeTracerTest.cs
@@ -0,0 +1,72 @@
+using System.Collections.ObjectModel;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Controllers;
+using System.Web.Http.Filters;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Tracing.Tracers
+{
+ public class AuthorizationFilterAttributeTracerTest
+ {
+ [Fact]
+ public void ExecuteAuthorizationFilterAsync_Traces()
+ {
+ // Arrange
+ Mock<AuthorizationFilterAttribute> mockAttr = new Mock<AuthorizationFilterAttribute>() { CallBase = true };
+ Mock<HttpActionDescriptor> mockActionDescriptor = new Mock<HttpActionDescriptor>() { CallBase = true };
+ mockActionDescriptor.Setup(a => a.ActionName).Returns("test");
+ mockActionDescriptor.Setup(a => a.GetParameters()).Returns(new Collection<HttpParameterDescriptor>(new HttpParameterDescriptor[0]));
+ HttpActionContext actionContext = ContextUtil.CreateActionContext(actionDescriptor: mockActionDescriptor.Object);
+ Func<Task<HttpResponseMessage>> continuation = () => TaskHelpers.FromResult<HttpResponseMessage>(new HttpResponseMessage());
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ AuthorizationFilterAttributeTracer tracer = new AuthorizationFilterAttributeTracer(mockAttr.Object, traceWriter);
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(actionContext.Request, TraceCategories.FiltersCategory, TraceLevel.Info) { Kind = TraceKind.Begin, Operation = "OnAuthorization" },
+ new TraceRecord(actionContext.Request, TraceCategories.FiltersCategory, TraceLevel.Info) { Kind = TraceKind.End, Operation = "OnAuthorization" },
+ };
+
+ // Act
+ Task task = ((IAuthorizationFilter)tracer).ExecuteAuthorizationFilterAsync(actionContext, CancellationToken.None, continuation);
+ task.Wait();
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ }
+
+ [Fact]
+ public void ExecuteAuthorizationFilterAsync_Throws_And_Traces_When_Inner_OnException_Throws()
+ {
+ // Arrange
+ Mock<AuthorizationFilterAttribute> mockAttr = new Mock<AuthorizationFilterAttribute>() { CallBase = true };
+ InvalidOperationException exception = new InvalidOperationException("test");
+ mockAttr.Setup(a => a.OnAuthorization(It.IsAny<HttpActionContext>())).Throws(exception);
+ Mock<HttpActionDescriptor> mockActionDescriptor = new Mock<HttpActionDescriptor>() { CallBase = true };
+ mockActionDescriptor.Setup(a => a.ActionName).Returns("test");
+ mockActionDescriptor.Setup(a => a.GetParameters()).Returns(new Collection<HttpParameterDescriptor>(new HttpParameterDescriptor[0]));
+ HttpActionContext actionContext = ContextUtil.CreateActionContext(actionDescriptor: mockActionDescriptor.Object);
+ Func<Task<HttpResponseMessage>> continuation = () => TaskHelpers.FromResult<HttpResponseMessage>(new HttpResponseMessage());
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ AuthorizationFilterAttributeTracer tracer = new AuthorizationFilterAttributeTracer(mockAttr.Object, traceWriter);
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(actionContext.Request, TraceCategories.FiltersCategory, TraceLevel.Info) { Kind = TraceKind.Begin, Operation = "OnAuthorization" },
+ new TraceRecord(actionContext.Request, TraceCategories.FiltersCategory, TraceLevel.Error) { Kind = TraceKind.End, Operation = "OnAuthorization" }
+ };
+
+ // Act
+ Exception thrown =
+ Assert.Throws<InvalidOperationException>(
+ () => ((IAuthorizationFilter)tracer).ExecuteAuthorizationFilterAsync(actionContext, CancellationToken.None, continuation).Wait());
+
+ // Assert
+ Assert.Same(exception, thrown);
+ Assert.Same(exception, traceWriter.Traces[1].Exception);
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Tracing/Tracers/AuthorizationFilterTracerTest.cs b/test/System.Web.Http.Test/Tracing/Tracers/AuthorizationFilterTracerTest.cs
new file mode 100644
index 00000000..7164b6d1
--- /dev/null
+++ b/test/System.Web.Http.Test/Tracing/Tracers/AuthorizationFilterTracerTest.cs
@@ -0,0 +1,76 @@
+using System.Collections.ObjectModel;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Controllers;
+using System.Web.Http.Filters;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Tracing.Tracers
+{
+ public class AuthorizationFilterTracerTest
+ {
+ [Fact]
+ public void ExecuteAuthorizationFilterAsync_Traces()
+ {
+ // Arrange
+ HttpResponseMessage response = new HttpResponseMessage();
+ Mock<IAuthorizationFilter> mockFilter = new Mock<IAuthorizationFilter>() { CallBase = true };
+ mockFilter.Setup(f => f.ExecuteAuthorizationFilterAsync(It.IsAny<HttpActionContext>(), It.IsAny<CancellationToken>(), It.IsAny<Func<Task<HttpResponseMessage>>>())).Returns(TaskHelpers.FromResult(response));
+ Mock<HttpActionDescriptor> mockActionDescriptor = new Mock<HttpActionDescriptor>() { CallBase = true };
+ mockActionDescriptor.Setup(a => a.ActionName).Returns("test");
+ mockActionDescriptor.Setup(a => a.GetParameters()).Returns(new Collection<HttpParameterDescriptor>(new HttpParameterDescriptor[0]));
+ HttpActionContext actionContext = ContextUtil.CreateActionContext(actionDescriptor: mockActionDescriptor.Object);
+ Func<Task<HttpResponseMessage>> continuation = () => TaskHelpers.FromResult<HttpResponseMessage>(new HttpResponseMessage());
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ AuthorizationFilterTracer tracer = new AuthorizationFilterTracer(mockFilter.Object, traceWriter);
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(actionContext.Request, TraceCategories.FiltersCategory, TraceLevel.Info) { Kind = TraceKind.Begin, Operation = "ExecuteAuthorizationFilterAsync" },
+ new TraceRecord(actionContext.Request, TraceCategories.FiltersCategory, TraceLevel.Info) { Kind = TraceKind.End, Operation = "ExecuteAuthorizationFilterAsync" },
+ };
+
+ // Act
+ Task task = ((IAuthorizationFilter)tracer).ExecuteAuthorizationFilterAsync(actionContext, CancellationToken.None, continuation);
+ task.Wait();
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ }
+
+ [Fact]
+ public void ExecuteAuthorizationFilterAsync_Faults_And_Traces_When_Inner_Faults()
+ {
+ // Arrange
+ Mock<IAuthorizationFilter> mockAttr = new Mock<IAuthorizationFilter>() { CallBase = true };
+ HttpResponseMessage response = new HttpResponseMessage();
+ InvalidOperationException exception = new InvalidOperationException("test");
+ TaskCompletionSource<HttpResponseMessage> tcs = new TaskCompletionSource<HttpResponseMessage>(response);
+ tcs.TrySetException(exception);
+ mockAttr.Setup(a => a.ExecuteAuthorizationFilterAsync(It.IsAny<HttpActionContext>(), It.IsAny<CancellationToken>(), It.IsAny<Func<Task<HttpResponseMessage>>>())).Returns(tcs.Task);
+ Mock<HttpActionDescriptor> mockActionDescriptor = new Mock<HttpActionDescriptor>() { CallBase = true };
+ mockActionDescriptor.Setup(a => a.ActionName).Returns("test");
+ mockActionDescriptor.Setup(a => a.GetParameters()).Returns(new Collection<HttpParameterDescriptor>(new HttpParameterDescriptor[0]));
+ HttpActionContext actionContext = ContextUtil.CreateActionContext(actionDescriptor: mockActionDescriptor.Object);
+ Func<Task<HttpResponseMessage>> continuation = () => TaskHelpers.FromResult<HttpResponseMessage>(response);
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ AuthorizationFilterTracer tracer = new AuthorizationFilterTracer(mockAttr.Object, traceWriter);
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(actionContext.Request, TraceCategories.FiltersCategory, TraceLevel.Info) { Kind = TraceKind.Begin, Operation = "ExecuteAuthorizationFilterAsync" },
+ new TraceRecord(actionContext.Request, TraceCategories.FiltersCategory, TraceLevel.Error) { Kind = TraceKind.End, Operation = "ExecuteAuthorizationFilterAsync" }
+ };
+
+ // Act & Assert
+ Task task = ((IAuthorizationFilter)tracer).ExecuteAuthorizationFilterAsync(actionContext, CancellationToken.None, continuation);
+ Exception thrown = Assert.Throws<InvalidOperationException>(() => task.Wait());
+
+ // Assert
+ Assert.Same(exception, thrown);
+ Assert.Same(exception, traceWriter.Traces[1].Exception);
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Tracing/Tracers/ContentNegotiatorTracerTest.cs b/test/System.Web.Http.Test/Tracing/Tracers/ContentNegotiatorTracerTest.cs
new file mode 100644
index 00000000..0d5f84d1
--- /dev/null
+++ b/test/System.Web.Http.Test/Tracing/Tracers/ContentNegotiatorTracerTest.cs
@@ -0,0 +1,227 @@
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Net.Http.Formatting;
+using System.Net.Http.Headers;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Tracing.Tracers
+{
+ public class ContentNegotiatorTracerTest
+ {
+ [Fact]
+ public void Negotiate_Calls_Inner_Negotiate()
+ {
+ // Arrange
+ MediaTypeHeaderValue innerSelectedMediaType = null;
+ MediaTypeHeaderValue outerSelectedMediaType = null;
+ bool negotiateCalled = false;
+ HttpRequestMessage request = new HttpRequestMessage();
+ Mock<IContentNegotiator> mockNegotiator = new Mock<IContentNegotiator>();
+ mockNegotiator.Setup(
+ n =>
+ n.Negotiate(It.IsAny<Type>(), It.IsAny<HttpRequestMessage>(),
+ It.IsAny<IEnumerable<MediaTypeFormatter>>(), out innerSelectedMediaType)).Callback(
+ () => {negotiateCalled = true;});
+
+ ContentNegotiatorTracer tracer = new ContentNegotiatorTracer(mockNegotiator.Object, new TestTraceWriter());
+
+ // Act
+ ((IContentNegotiator) tracer).Negotiate(typeof (int), request, new MediaTypeFormatter[0], out outerSelectedMediaType);
+
+ // Assert
+ Assert.True(negotiateCalled);
+ }
+
+ [Fact]
+ public void Negotiate_Returns_Inner_MediaType()
+ {
+ // Arrange
+ MediaTypeHeaderValue expectedMediaType = new MediaTypeHeaderValue("application/xml");
+ MediaTypeHeaderValue innerSelectedMediaType = expectedMediaType;
+ MediaTypeHeaderValue outerSelectedMediaType = null;
+ HttpRequestMessage request = new HttpRequestMessage();
+ Mock<IContentNegotiator> mockNegotiator = new Mock<IContentNegotiator>();
+ mockNegotiator.Setup(
+ n =>
+ n.Negotiate(It.IsAny<Type>(), It.IsAny<HttpRequestMessage>(),
+ It.IsAny<IEnumerable<MediaTypeFormatter>>(), out innerSelectedMediaType));
+
+ ContentNegotiatorTracer tracer = new ContentNegotiatorTracer(mockNegotiator.Object, new TestTraceWriter());
+
+ // Act
+ ((IContentNegotiator)tracer).Negotiate(typeof(int), request, new MediaTypeFormatter[0], out outerSelectedMediaType);
+
+ // Assert
+ Assert.Same(expectedMediaType, outerSelectedMediaType);
+ }
+
+ [Fact]
+ public void Negotiate_Returns_Wrapped_Inner_XmlFormatter()
+ {
+ // Arrange
+ MediaTypeHeaderValue mediaType = null;
+ MediaTypeFormatter expectedFormatter = new XmlMediaTypeFormatter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ Mock<IContentNegotiator> mockNegotiator = new Mock<IContentNegotiator>();
+ mockNegotiator.Setup(
+ n =>
+ n.Negotiate(It.IsAny<Type>(), It.IsAny<HttpRequestMessage>(),
+ It.IsAny<IEnumerable<MediaTypeFormatter>>(), out mediaType)).Returns(
+ expectedFormatter);
+ ContentNegotiatorTracer tracer = new ContentNegotiatorTracer(mockNegotiator.Object, new TestTraceWriter());
+
+ // Act
+ var actualFormatter = ((IContentNegotiator)tracer).Negotiate(typeof(int), request, new MediaTypeFormatter[0], out mediaType);
+
+ // Assert
+ Assert.IsType<XmlMediaTypeFormatterTracer>(actualFormatter);
+ }
+
+ [Fact]
+ public void Negotiate_Returns_Wrapped_Inner_JsonFormatter()
+ {
+ // Arrange
+ MediaTypeHeaderValue mediaType = null;
+ MediaTypeFormatter expectedFormatter = new JsonMediaTypeFormatter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ Mock<IContentNegotiator> mockNegotiator = new Mock<IContentNegotiator>();
+ mockNegotiator.Setup(
+ n =>
+ n.Negotiate(It.IsAny<Type>(), It.IsAny<HttpRequestMessage>(),
+ It.IsAny<IEnumerable<MediaTypeFormatter>>(), out mediaType)).Returns(
+ expectedFormatter);
+ ContentNegotiatorTracer tracer = new ContentNegotiatorTracer(mockNegotiator.Object, new TestTraceWriter());
+
+ // Act
+ var actualFormatter = ((IContentNegotiator)tracer).Negotiate(typeof(int), request, new MediaTypeFormatter[0], out mediaType);
+
+ // Assert
+ Assert.IsType<JsonMediaTypeFormatterTracer>(actualFormatter);
+ }
+
+ [Fact]
+ public void Negotiate_Returns_Wrapped_Inner_FormUrlEncodedFormatter()
+ {
+ // Arrange
+ MediaTypeHeaderValue mediaType = null;
+ MediaTypeFormatter expectedFormatter = new FormUrlEncodedMediaTypeFormatter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ Mock<IContentNegotiator> mockNegotiator = new Mock<IContentNegotiator>();
+ mockNegotiator.Setup(
+ n =>
+ n.Negotiate(It.IsAny<Type>(), It.IsAny<HttpRequestMessage>(),
+ It.IsAny<IEnumerable<MediaTypeFormatter>>(), out mediaType)).Returns(
+ expectedFormatter);
+ ContentNegotiatorTracer tracer = new ContentNegotiatorTracer(mockNegotiator.Object, new TestTraceWriter());
+
+ // Act
+ var actualFormatter = ((IContentNegotiator)tracer).Negotiate(typeof(int), request, new MediaTypeFormatter[0], out mediaType);
+
+ // Assert
+ Assert.IsType<FormUrlEncodedMediaTypeFormatterTracer>(actualFormatter);
+ }
+
+ [Fact]
+ public void Negotiate_Returns_Null_Inner_Formatter()
+ {
+ // Arrange
+ MediaTypeHeaderValue mediaType = null;
+ HttpRequestMessage request = new HttpRequestMessage();
+ Mock<IContentNegotiator> mockNegotiator = new Mock<IContentNegotiator>();
+ mockNegotiator.Setup(
+ n =>
+ n.Negotiate(It.IsAny<Type>(), It.IsAny<HttpRequestMessage>(),
+ It.IsAny<IEnumerable<MediaTypeFormatter>>(), out mediaType)).Returns(
+ (MediaTypeFormatter) null);
+ ContentNegotiatorTracer tracer = new ContentNegotiatorTracer(mockNegotiator.Object, new TestTraceWriter());
+
+ // Act
+ var actualFormatter = ((IContentNegotiator)tracer).Negotiate(typeof(int), request, new MediaTypeFormatter[0], out mediaType);
+
+ // Assert
+ Assert.Null(actualFormatter);
+ }
+
+ [Fact]
+ public void Negotiate_Traces_BeginEnd()
+ {
+ // Arrange
+ MediaTypeHeaderValue mediaType = null;
+ MediaTypeFormatter expectedFormatter = new XmlMediaTypeFormatter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ Mock<IContentNegotiator> mockNegotiator = new Mock<IContentNegotiator>();
+ mockNegotiator.Setup(
+ n =>
+ n.Negotiate(It.IsAny<Type>(), It.IsAny<HttpRequestMessage>(),
+ It.IsAny<IEnumerable<MediaTypeFormatter>>(), out mediaType)).Returns(
+ expectedFormatter);
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ ContentNegotiatorTracer tracer = new ContentNegotiatorTracer(mockNegotiator.Object, traceWriter);
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(request, TraceCategories.FormattingCategory, TraceLevel.Info) { Kind = TraceKind.Begin },
+ new TraceRecord(request, TraceCategories.FormattingCategory, TraceLevel.Info) { Kind = TraceKind.End }
+ };
+
+ // Act
+ ((IContentNegotiator)tracer).Negotiate(typeof(int), request, new MediaTypeFormatter[0], out mediaType);
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ }
+
+ [Fact]
+ public void Negotiate_Throws_When_Inner_Throws()
+ {
+ // Arrange
+ MediaTypeHeaderValue mediaType = null;
+ MediaTypeFormatter expectedFormatter = new XmlMediaTypeFormatter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ InvalidOperationException expectedException = new InvalidOperationException("test");
+ Mock<IContentNegotiator> mockNegotiator = new Mock<IContentNegotiator>();
+ mockNegotiator.Setup(
+ n =>
+ n.Negotiate(It.IsAny<Type>(), It.IsAny<HttpRequestMessage>(),
+ It.IsAny<IEnumerable<MediaTypeFormatter>>(), out mediaType)).Throws(expectedException);
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ ContentNegotiatorTracer tracer = new ContentNegotiatorTracer(mockNegotiator.Object, traceWriter);
+
+ // Act & Assert
+ InvalidOperationException actualException = Assert.Throws<InvalidOperationException>(() => ((IContentNegotiator)tracer).Negotiate(typeof(int), request, new MediaTypeFormatter[0], out mediaType));
+
+ // Assert
+ Assert.Same(expectedException, actualException);
+ }
+
+ [Fact]
+ public void Negotiate_Traces_BeginEnd_When_Inner_Throws()
+ {
+ // Arrange
+ MediaTypeHeaderValue mediaType = null;
+ MediaTypeFormatter expectedFormatter = new XmlMediaTypeFormatter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ InvalidOperationException expectedException = new InvalidOperationException("test");
+ Mock<IContentNegotiator> mockNegotiator = new Mock<IContentNegotiator>();
+ mockNegotiator.Setup(
+ n =>
+ n.Negotiate(It.IsAny<Type>(), It.IsAny<HttpRequestMessage>(),
+ It.IsAny<IEnumerable<MediaTypeFormatter>>(), out mediaType)).Throws(expectedException);
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ ContentNegotiatorTracer tracer = new ContentNegotiatorTracer(mockNegotiator.Object, traceWriter);
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(request, TraceCategories.FormattingCategory, TraceLevel.Info) { Kind = TraceKind.Begin },
+ new TraceRecord(request, TraceCategories.FormattingCategory, TraceLevel.Error) { Kind = TraceKind.End }
+ };
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(() => ((IContentNegotiator)tracer).Negotiate(typeof(int), request, new MediaTypeFormatter[0], out mediaType));
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ Assert.Same(expectedException, traceWriter.Traces[1].Exception);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Tracing/Tracers/ExceptionFilterAttributeTracerTest.cs b/test/System.Web.Http.Test/Tracing/Tracers/ExceptionFilterAttributeTracerTest.cs
new file mode 100644
index 00000000..bed138c1
--- /dev/null
+++ b/test/System.Web.Http.Test/Tracing/Tracers/ExceptionFilterAttributeTracerTest.cs
@@ -0,0 +1,74 @@
+using System.Collections.ObjectModel;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Controllers;
+using System.Web.Http.Filters;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Tracing.Tracers
+{
+ public class ExceptionFilterAttributeTracerTest
+ {
+ [Fact]
+ public void ExecuteExceptionFilterAsync_Traces()
+ {
+ // Arrange
+ HttpRequestMessage request = new HttpRequestMessage();
+ HttpResponseMessage response = new HttpResponseMessage();
+ Mock<ExceptionFilterAttribute> mockAttr = new Mock<ExceptionFilterAttribute>() { CallBase = true };
+ Mock<HttpActionDescriptor> mockActionDescriptor = new Mock<HttpActionDescriptor>() { CallBase = true };
+ mockActionDescriptor.Setup(a => a.ActionName).Returns("test");
+ mockActionDescriptor.Setup(a => a.GetParameters()).Returns(new Collection<HttpParameterDescriptor>(new HttpParameterDescriptor[0]));
+ HttpActionExecutedContext actionExecutedContext = ContextUtil.GetActionExecutedContext(request, response);
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ ExceptionFilterAttributeTracer tracer = new ExceptionFilterAttributeTracer(mockAttr.Object, traceWriter);
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(request, TraceCategories.FiltersCategory, TraceLevel.Info) { Kind = TraceKind.Begin, Operation = "OnException" },
+ new TraceRecord(request, TraceCategories.FiltersCategory, TraceLevel.Info) { Kind = TraceKind.End, Operation = "OnException" },
+ };
+
+ // Act
+ Task task = ((IExceptionFilter)tracer).ExecuteExceptionFilterAsync(actionExecutedContext, CancellationToken.None);
+ task.Wait();
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ }
+
+ [Fact]
+ public void ExecuteExceptionFilterAsync_Throws_And_Traces_When_Inner_OnException_Throws()
+ {
+ // Arrange
+ HttpRequestMessage request = new HttpRequestMessage();
+ HttpResponseMessage response = new HttpResponseMessage();
+ Mock<ExceptionFilterAttribute> mockAttr = new Mock<ExceptionFilterAttribute>() { CallBase = true };
+ InvalidOperationException exception = new InvalidOperationException("test");
+ mockAttr.Setup(a => a.OnException(It.IsAny<HttpActionExecutedContext>())).Throws(exception);
+ Mock<HttpActionDescriptor> mockActionDescriptor = new Mock<HttpActionDescriptor>() { CallBase = true };
+ mockActionDescriptor.Setup(a => a.ActionName).Returns("test");
+ mockActionDescriptor.Setup(a => a.GetParameters()).Returns(new Collection<HttpParameterDescriptor>(new HttpParameterDescriptor[0]));
+ HttpActionExecutedContext actionExecutedContext = ContextUtil.GetActionExecutedContext(request, response);
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ ExceptionFilterAttributeTracer tracer = new ExceptionFilterAttributeTracer(mockAttr.Object, traceWriter);
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(request, TraceCategories.FiltersCategory, TraceLevel.Info) { Kind = TraceKind.Begin, Operation = "OnException" },
+ new TraceRecord(request, TraceCategories.FiltersCategory, TraceLevel.Error) { Kind = TraceKind.End, Operation = "OnException" }
+ };
+
+ // Act
+ Exception thrown =
+ Assert.Throws<InvalidOperationException>(
+ () => ((IExceptionFilter) tracer).ExecuteExceptionFilterAsync(actionExecutedContext, CancellationToken.None));
+
+ // Assert
+ Assert.Same(exception, thrown);
+ Assert.Same(exception, traceWriter.Traces[1].Exception);
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Tracing/Tracers/ExceptionFilterTracerTest.cs b/test/System.Web.Http.Test/Tracing/Tracers/ExceptionFilterTracerTest.cs
new file mode 100644
index 00000000..aedd4e78
--- /dev/null
+++ b/test/System.Web.Http.Test/Tracing/Tracers/ExceptionFilterTracerTest.cs
@@ -0,0 +1,71 @@
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Filters;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Tracing.Tracers
+{
+ public class ExceptionFilterTracerTest
+ {
+ [Fact]
+ public void ExecuteExceptionFilterAsync_Traces()
+ {
+ // Arrange
+ HttpRequestMessage request = new HttpRequestMessage();
+ HttpResponseMessage response = new HttpResponseMessage();
+ Mock<IExceptionFilter> mockFilter = new Mock<IExceptionFilter>() { CallBase = true };
+ mockFilter.Setup(
+ f => f.ExecuteExceptionFilterAsync(It.IsAny<HttpActionExecutedContext>(), It.IsAny<CancellationToken>())).
+ Returns(TaskHelpers.Completed());
+ HttpActionExecutedContext actionExecutedContext = ContextUtil.GetActionExecutedContext(request, response);
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ ExceptionFilterTracer tracer = new ExceptionFilterTracer(mockFilter.Object, traceWriter);
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(request, TraceCategories.FiltersCategory, TraceLevel.Info) { Kind = TraceKind.Begin, Operation = "ExecuteExceptionFilterAsync" },
+ new TraceRecord(request, TraceCategories.FiltersCategory, TraceLevel.Info) { Kind = TraceKind.End, Operation = "ExecuteExceptionFilterAsync" },
+ };
+
+ // Act
+ Task task = ((IExceptionFilter)tracer).ExecuteExceptionFilterAsync(actionExecutedContext, CancellationToken.None);
+ task.Wait();
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ }
+
+ [Fact]
+ public void ExecuteExceptionFilterAsync_Faults_And_Traces_When_Inner_Faults()
+ {
+ // Arrange
+ HttpRequestMessage request = new HttpRequestMessage();
+ HttpResponseMessage response = new HttpResponseMessage();
+ Mock<IExceptionFilter> mockFilter = new Mock<IExceptionFilter>() { CallBase = true };
+ InvalidOperationException exception = new InvalidOperationException("test");
+ TaskCompletionSource<object> tcs = new TaskCompletionSource<object>(null);
+ tcs.TrySetException(exception);
+ mockFilter.Setup(a => a.ExecuteExceptionFilterAsync(It.IsAny<HttpActionExecutedContext>(), It.IsAny<CancellationToken>())).Returns(tcs.Task);
+ HttpActionExecutedContext actionExecutedContext = ContextUtil.GetActionExecutedContext(request, response);
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ ExceptionFilterTracer tracer = new ExceptionFilterTracer(mockFilter.Object, traceWriter);
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(request, TraceCategories.FiltersCategory, TraceLevel.Info) { Kind = TraceKind.Begin, Operation = "ExecuteExceptionFilterAsync" },
+ new TraceRecord(request, TraceCategories.FiltersCategory, TraceLevel.Error) { Kind = TraceKind.End, Operation = "ExecuteExceptionFilterAsync" }
+ };
+
+ // Act
+ Task task = ((IExceptionFilter)tracer).ExecuteExceptionFilterAsync(actionExecutedContext, CancellationToken.None);
+
+
+ // Assert
+ Exception thrown = Assert.Throws<InvalidOperationException>(() => task.Wait());
+ Assert.Same(exception, thrown);
+ Assert.Same(exception, traceWriter.Traces[1].Exception);
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Tracing/Tracers/FilterTracerTest.cs b/test/System.Web.Http.Test/Tracing/Tracers/FilterTracerTest.cs
new file mode 100644
index 00000000..76fe50cf
--- /dev/null
+++ b/test/System.Web.Http.Test/Tracing/Tracers/FilterTracerTest.cs
@@ -0,0 +1,274 @@
+using System.Linq;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Filters;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+using System.Web.Http.Controllers;
+
+namespace System.Web.Http.Tracing.Tracers
+{
+ public class FilterTracerTest
+ {
+ [Fact]
+ public void CreateFilterTracers_IFilter_With_IFilter_Returns_Single_Wrapped_IFilter()
+ {
+ // Arrange
+ Mock<IFilter> mockFilter = new Mock<IFilter>();
+
+ // Act
+ IFilter[] wrappedFilters = FilterTracer.CreateFilterTracers(mockFilter.Object, new TestTraceWriter()).ToArray();
+
+ // Assert
+ Assert.Equal(1, wrappedFilters.Length);
+ Assert.IsType<FilterTracer>(wrappedFilters[0]);
+ }
+
+ [Fact]
+ public void CreateFilterTracers_IFilter_With_IActionFilter_Returns_Single_Wrapped_IActionFilter()
+ {
+ // Arrange
+ Mock<IActionFilter> mockFilter = new Mock<IActionFilter>();
+
+ // Act
+ IFilter[] wrappedFilters = FilterTracer.CreateFilterTracers(mockFilter.Object, new TestTraceWriter()).ToArray();
+
+ // Assert
+ Assert.Equal(1, wrappedFilters.Length);
+ Assert.IsType<ActionFilterTracer>(wrappedFilters[0]);
+ }
+
+ [Fact]
+ public void CreateFilterTracers_IFilter_With_IExceptionFilter_Returns_Single_Wrapped_IExceptionFilter()
+ {
+ // Arrange
+ Mock<IExceptionFilter> mockFilter = new Mock<IExceptionFilter>();
+
+ // Act
+ IFilter[] wrappedFilters = FilterTracer.CreateFilterTracers(mockFilter.Object, new TestTraceWriter()).ToArray();
+
+ // Assert
+ Assert.Equal(1, wrappedFilters.Length);
+ Assert.IsType<ExceptionFilterTracer>(wrappedFilters[0]);
+ }
+
+ [Fact]
+ public void CreateFilterTracers_IFilter_With_IAuthorizationFilter_Returns_Single_Wrapped_IAuthorizationFilter()
+ {
+ // Arrange
+ Mock<IAuthorizationFilter> mockFilter = new Mock<IAuthorizationFilter>();
+
+ // Act
+ IFilter[] wrappedFilters = FilterTracer.CreateFilterTracers(mockFilter.Object, new TestTraceWriter()).ToArray();
+
+ // Assert
+ Assert.Equal(1, wrappedFilters.Length);
+ Assert.IsType<AuthorizationFilterTracer>(wrappedFilters[0]);
+ }
+
+ [Fact]
+ public void CreateFilterTracers_IFilter_With_ActionFilterAttribute_Returns_Single_Wrapped_Filter()
+ {
+ // Arrange
+ Mock<ActionFilterAttribute> mockFilter = new Mock<ActionFilterAttribute>();
+
+ // Act
+ IFilter[] wrappedFilters = FilterTracer.CreateFilterTracers(mockFilter.Object, new TestTraceWriter()).ToArray();
+
+ // Assert
+ Assert.Equal(1, wrappedFilters.Length);
+ Assert.IsType<ActionFilterAttributeTracer>(wrappedFilters[0]);
+ }
+
+ [Fact]
+ public void CreateFilterTracers_IFilter_With_ExceptionFilterAttribute_Returns_Single_Wrapped_Filter()
+ {
+ // Arrange
+ Mock<ExceptionFilterAttribute> mockFilter = new Mock<ExceptionFilterAttribute>();
+
+ // Act
+ IFilter[] wrappedFilters = FilterTracer.CreateFilterTracers(mockFilter.Object, new TestTraceWriter()).ToArray();
+
+ // Assert
+ Assert.Equal(1, wrappedFilters.Length);
+ Assert.IsType<ExceptionFilterAttributeTracer>(wrappedFilters[0]);
+ }
+
+ [Fact]
+ public void CreateFilterTracers_IFilter_With_AuthorizationFilterAttribute_Returns_Single_Wrapped_Filter()
+ {
+ // Arrange
+ Mock<AuthorizationFilterAttribute> mockFilter = new Mock<AuthorizationFilterAttribute>();
+
+ // Act
+ IFilter[] wrappedFilters = FilterTracer.CreateFilterTracers(mockFilter.Object, new TestTraceWriter()).ToArray();
+
+ // Assert
+ Assert.Equal(1, wrappedFilters.Length);
+ Assert.IsType<AuthorizationFilterAttributeTracer>(wrappedFilters[0]);
+ }
+
+ [Fact]
+ public void CreateFilterTracers_IFilter_With_All_Filter_Interfaces_Returns_3_Wrapped_Filters()
+ {
+ // Arrange
+ IFilter filter = new TestFilterAllBehaviors();
+
+ // Act
+ IFilter[] wrappedFilters = FilterTracer.CreateFilterTracers(filter, new TestTraceWriter()).ToArray();
+
+ // Assert
+ Assert.Equal(3, wrappedFilters.Length);
+ Assert.Equal(1, wrappedFilters.OfType<ActionFilterTracer>().Count());
+ Assert.Equal(1, wrappedFilters.OfType<AuthorizationFilterTracer>().Count());
+ Assert.Equal(1, wrappedFilters.OfType<ExceptionFilterTracer>().Count());
+ }
+
+ [Fact]
+ public void CreateFilterTracers_With_IFilter_Returns_Single_Wrapped_IFilter()
+ {
+ // Arrange
+ Mock<IFilter> mockFilter = new Mock<IFilter>();
+ FilterInfo filter = new FilterInfo(mockFilter.Object, FilterScope.First);
+
+ // Act
+ FilterInfo[] wrappedFilters = FilterTracer.CreateFilterTracers(filter, new TestTraceWriter()).ToArray();
+
+ // Assert
+ Assert.Equal(1, wrappedFilters.Length);
+ Assert.IsType<FilterTracer>(wrappedFilters[0].Instance);
+ }
+
+ [Fact]
+ public void CreateFilterTracers_With_IActionFilter_Returns_Single_Wrapped_IActionFilter()
+ {
+ // Arrange
+ Mock<IActionFilter> mockFilter = new Mock<IActionFilter>();
+ FilterInfo filter = new FilterInfo(mockFilter.Object, FilterScope.First);
+
+ // Act
+ FilterInfo[] wrappedFilters = FilterTracer.CreateFilterTracers(filter, new TestTraceWriter()).ToArray();
+
+ // Assert
+ Assert.Equal(1, wrappedFilters.Length);
+ Assert.IsType<ActionFilterTracer>(wrappedFilters[0].Instance);
+ }
+
+ [Fact]
+ public void CreateFilterTracers_With_IExceptionFilter_Returns_Single_Wrapped_IExceptionFilter()
+ {
+ // Arrange
+ Mock<IExceptionFilter> mockFilter = new Mock<IExceptionFilter>();
+ FilterInfo filter = new FilterInfo(mockFilter.Object, FilterScope.First);
+
+ // Act
+ FilterInfo[] wrappedFilters = FilterTracer.CreateFilterTracers(filter, new TestTraceWriter()).ToArray();
+
+ // Assert
+ Assert.Equal(1, wrappedFilters.Length);
+ Assert.IsType<ExceptionFilterTracer>(wrappedFilters[0].Instance);
+ }
+
+ [Fact]
+ public void CreateFilterTracers_With_IAuthorizationFilter_Returns_Single_Wrapped_IAuthorizationFilter()
+ {
+ // Arrange
+ Mock<IAuthorizationFilter> mockFilter = new Mock<IAuthorizationFilter>();
+ FilterInfo filter = new FilterInfo(mockFilter.Object, FilterScope.First);
+
+ // Act
+ FilterInfo[] wrappedFilters = FilterTracer.CreateFilterTracers(filter, new TestTraceWriter()).ToArray();
+
+ // Assert
+ Assert.Equal(1, wrappedFilters.Length);
+ Assert.IsType<AuthorizationFilterTracer>(wrappedFilters[0].Instance);
+ }
+
+ [Fact]
+ public void CreateFilterTracers_With_ActionFilterAttribute_Returns_2_Wrapped_Filters()
+ {
+ // Arrange
+ Mock<ActionFilterAttribute> mockFilter = new Mock<ActionFilterAttribute>();
+ FilterInfo filter = new FilterInfo(mockFilter.Object, FilterScope.First);
+
+ // Act
+ FilterInfo[] wrappedFilters = FilterTracer.CreateFilterTracers(filter, new TestTraceWriter()).ToArray();
+
+ // Assert
+ Assert.Equal(1, wrappedFilters.Length);
+ Assert.IsType<ActionFilterAttributeTracer>(wrappedFilters[0].Instance);
+ }
+
+ [Fact]
+ public void CreateFilterTracers_With_ExceptionFilterAttribute_Returns_2_Wrapped_Filters()
+ {
+ // Arrange
+ Mock<ExceptionFilterAttribute> mockFilter = new Mock<ExceptionFilterAttribute>();
+ FilterInfo filter = new FilterInfo(mockFilter.Object, FilterScope.First);
+
+ // Act
+ FilterInfo[] wrappedFilters = FilterTracer.CreateFilterTracers(filter, new TestTraceWriter()).ToArray();
+
+ // Assert
+ Assert.Equal(1, wrappedFilters.Length);
+ Assert.IsType<ExceptionFilterAttributeTracer>(wrappedFilters[0].Instance);
+ }
+
+ [Fact]
+ public void CreateFilterTracers_With_AuthorizationFilterAttribute_Returns_2_Wrapped_Filters()
+ {
+ // Arrange
+ Mock<AuthorizationFilterAttribute> mockFilter = new Mock<AuthorizationFilterAttribute>();
+ FilterInfo filter = new FilterInfo(mockFilter.Object, FilterScope.First);
+
+ // Act
+ FilterInfo[] wrappedFilters = FilterTracer.CreateFilterTracers(filter, new TestTraceWriter()).ToArray();
+
+ // Assert
+ Assert.Equal(1, wrappedFilters.Length);
+ Assert.IsType<AuthorizationFilterAttributeTracer>(wrappedFilters[0].Instance); ;
+ }
+
+ [Fact]
+ public void CreateFilterTracers_With_All_Filter_Interfaces_Returns_3_Wrapped_Filters()
+ {
+ // Arrange
+ FilterInfo filter = new FilterInfo(new TestFilterAllBehaviors(), FilterScope.First);
+
+ // Act
+ FilterInfo[] wrappedFilters = FilterTracer.CreateFilterTracers(filter, new TestTraceWriter()).ToArray();
+
+ // Assert
+ Assert.Equal(3, wrappedFilters.Length);
+ Assert.Equal(1, wrappedFilters.Where(f => f.Instance.GetType() == typeof(ActionFilterTracer)).Count());
+ Assert.Equal(1, wrappedFilters.Where(f => f.Instance.GetType() == typeof(AuthorizationFilterTracer)).Count());
+ Assert.Equal(1, wrappedFilters.Where(f => f.Instance.GetType() == typeof(ExceptionFilterTracer)).Count());
+ }
+
+ // Test filter class that exposes all filter behaviors will cause separate filters for each
+ class TestFilterAllBehaviors : IActionFilter, IExceptionFilter, IAuthorizationFilter
+ {
+ Task<Net.Http.HttpResponseMessage> IActionFilter.ExecuteActionFilterAsync(Controllers.HttpActionContext actionContext, Threading.CancellationToken cancellationToken, Func<Threading.Tasks.Task<Net.Http.HttpResponseMessage>> continuation)
+ {
+ throw new NotImplementedException();
+ }
+
+ bool IFilter.AllowMultiple
+ {
+ get { throw new NotImplementedException(); }
+ }
+
+ Task IExceptionFilter.ExecuteExceptionFilterAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken)
+ {
+ throw new NotImplementedException();
+ }
+
+ Task<HttpResponseMessage> IAuthorizationFilter.ExecuteAuthorizationFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<Net.Http.HttpResponseMessage>> continuation)
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Tracing/Tracers/FormatterParameterBindingTracerTest.cs b/test/System.Web.Http.Test/Tracing/Tracers/FormatterParameterBindingTracerTest.cs
new file mode 100644
index 00000000..f09aab48
--- /dev/null
+++ b/test/System.Web.Http.Test/Tracing/Tracers/FormatterParameterBindingTracerTest.cs
@@ -0,0 +1,128 @@
+using System.Net.Http;
+using System.Net.Http.Formatting;
+using System.Net.Http.Headers;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Controllers;
+using System.Web.Http.Metadata;
+using System.Web.Http.Metadata.Providers;
+using System.Web.Http.ModelBinding;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Tracing.Tracers
+{
+ public class FormatterParameterBindingTracerTest
+ {
+
+ /// <summary>
+ /// This test verifies that our <see cref="FormatterParameterBindingTracer"/>
+ /// intercepts the async bind request and redirects it to use tracing formatters
+ /// correlated to the request.
+ /// </summary>
+ [Fact]
+ public void ExecuteBindingAsync_Traces_And_Invokes_Inner_ReadAsync()
+ {
+ // Arrange
+ Mock<HttpParameterDescriptor> mockParamDescriptor = new Mock<HttpParameterDescriptor>() { CallBase = true };
+ mockParamDescriptor.Setup(d => d.ParameterName).Returns("paramName");
+ mockParamDescriptor.Setup(d => d.ParameterType).Returns(typeof (string));
+ FormatterParameterBinding binding = new FormatterParameterBinding(mockParamDescriptor.Object, new MediaTypeFormatterCollection(), null);
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ FormatterParameterBindingTracer tracer = new FormatterParameterBindingTracer(binding, traceWriter);
+ HttpActionContext actionContext = ContextUtil.CreateActionContext();
+ actionContext.Request.Content = new StringContent("true");
+ actionContext.Request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
+ ModelMetadataProvider metadataProvider = new EmptyModelMetadataProvider();
+
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(actionContext.Request, TraceCategories.ModelBindingCategory, TraceLevel.Info) { Kind = TraceKind.Begin, Operation = "ExecuteBindingAsync" },
+ new TraceRecord(actionContext.Request, TraceCategories.ModelBindingCategory, TraceLevel.Info) { Kind = TraceKind.End, Operation = "ExecuteBindingAsync" }
+ };
+
+ // Act
+ Task task = tracer.ExecuteBindingAsync(metadataProvider, actionContext, CancellationToken.None);
+ task.Wait();
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ Assert.Equal("True", actionContext.ActionArguments["paramName"]);
+ }
+
+ [Fact]
+ public void ExecuteBindingAsync_Traces_And_Throws_When_Inner_Throws()
+ {
+ // Arrange
+ Mock<HttpParameterDescriptor> mockParamDescriptor = new Mock<HttpParameterDescriptor>() { CallBase = true };
+ mockParamDescriptor.Setup(d => d.ParameterName).Returns("paramName");
+ mockParamDescriptor.Setup(d => d.ParameterType).Returns(typeof(string));
+ Mock<FormatterParameterBinding> mockBinding = new Mock<FormatterParameterBinding>(mockParamDescriptor.Object, new MediaTypeFormatterCollection(), null) { CallBase = true };
+ InvalidOperationException exception = new InvalidOperationException("test");
+ mockBinding.Setup(
+ b =>
+ b.ExecuteBindingAsync(It.IsAny<ModelMetadataProvider>(), It.IsAny<HttpActionContext>(),
+ It.IsAny<CancellationToken>())).Throws(exception);
+
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ FormatterParameterBindingTracer tracer = new FormatterParameterBindingTracer(mockBinding.Object, traceWriter);
+ HttpActionContext actionContext = ContextUtil.CreateActionContext();
+ ModelMetadataProvider metadataProvider = new EmptyModelMetadataProvider();
+
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(actionContext.Request, TraceCategories.ModelBindingCategory, TraceLevel.Info) { Kind = TraceKind.Begin, Operation = "ExecuteBindingAsync" },
+ new TraceRecord(actionContext.Request, TraceCategories.ModelBindingCategory, TraceLevel.Error) { Kind = TraceKind.End, Operation = "ExecuteBindingAsync" }
+ };
+
+ // Act & Assert
+ Exception thrown = Assert.Throws<InvalidOperationException>(() => tracer.ExecuteBindingAsync(metadataProvider, actionContext, CancellationToken.None));
+
+ // Assert
+ Assert.Same(exception, thrown);
+ Assert.Same(exception, traceWriter.Traces[1].Exception);
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ }
+
+ [Fact]
+ public void ExecuteBindingAsync_Traces_And_Faults_When_Inner_Faults()
+ {
+ // Arrange
+
+ Mock<HttpParameterDescriptor> mockParamDescriptor = new Mock<HttpParameterDescriptor>() { CallBase = true };
+ mockParamDescriptor.Setup(d => d.ParameterName).Returns("paramName");
+ mockParamDescriptor.Setup(d => d.ParameterType).Returns(typeof(string));
+ Mock<FormatterParameterBinding> mockBinding = new Mock<FormatterParameterBinding>(mockParamDescriptor.Object, new MediaTypeFormatterCollection(), null) { CallBase = true };
+ InvalidOperationException exception = new InvalidOperationException("test");
+ TaskCompletionSource<object> tcs = new TaskCompletionSource<object>();
+ tcs.TrySetException(exception);
+
+ mockBinding.Setup(
+ b =>
+ b.ExecuteBindingAsync(It.IsAny<ModelMetadataProvider>(), It.IsAny<HttpActionContext>(),
+ It.IsAny<CancellationToken>())).Returns(tcs.Task);
+
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ FormatterParameterBindingTracer tracer = new FormatterParameterBindingTracer(mockBinding.Object, traceWriter);
+ HttpActionContext actionContext = ContextUtil.CreateActionContext();
+ ModelMetadataProvider metadataProvider = new EmptyModelMetadataProvider();
+
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(actionContext.Request, TraceCategories.ModelBindingCategory, TraceLevel.Info) { Kind = TraceKind.Begin, Operation = "ExecuteBindingAsync" },
+ new TraceRecord(actionContext.Request, TraceCategories.ModelBindingCategory, TraceLevel.Error) { Kind = TraceKind.End, Operation = "ExecuteBindingAsync" }
+ };
+
+ // Act & Assert
+ Task task = tracer.ExecuteBindingAsync(metadataProvider, actionContext, CancellationToken.None);
+
+ // Assert
+ Exception thrown = Assert.Throws<InvalidOperationException>(() => task.Wait());
+ Assert.Same(exception, thrown);
+ Assert.Same(exception, traceWriter.Traces[1].Exception);
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ }
+
+ }
+}
diff --git a/test/System.Web.Http.Test/Tracing/Tracers/HttpActionBindingTracerTest.cs b/test/System.Web.Http.Test/Tracing/Tracers/HttpActionBindingTracerTest.cs
new file mode 100644
index 00000000..68efce7c
--- /dev/null
+++ b/test/System.Web.Http.Test/Tracing/Tracers/HttpActionBindingTracerTest.cs
@@ -0,0 +1,102 @@
+using System.Collections.ObjectModel;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Controllers;
+using System.Web.Http.ModelBinding;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Tracing.Tracers
+{
+ public class HttpActionBindingTracerTest
+ {
+ private Mock<HttpActionDescriptor> _mockActionDescriptor;
+ private Mock<HttpParameterDescriptor> _mockParameterDescriptor;
+ private Mock<HttpParameterBinding> _mockParameterBinding;
+ private HttpActionBinding _actionBinding;
+ private HttpActionContext _actionContext;
+ private HttpControllerContext _controllerContext;
+ private HttpControllerDescriptor _controllerDescriptor;
+
+ public HttpActionBindingTracerTest()
+ {
+ _mockActionDescriptor = new Mock<HttpActionDescriptor>() { CallBase = true };
+ _mockActionDescriptor.Setup(a => a.ActionName).Returns("test");
+ _mockActionDescriptor.Setup(a => a.GetParameters()).Returns(new Collection<HttpParameterDescriptor>(new HttpParameterDescriptor[0]));
+
+ _mockParameterDescriptor = new Mock<HttpParameterDescriptor>() { CallBase = true };
+ _mockParameterBinding = new Mock<HttpParameterBinding>(_mockParameterDescriptor.Object) { CallBase = true };
+ _actionBinding = new HttpActionBinding(_mockActionDescriptor.Object, new HttpParameterBinding[] { _mockParameterBinding.Object });
+
+ _controllerDescriptor = new HttpControllerDescriptor(new HttpConfiguration(), "controller", typeof(ApiController));
+
+ _controllerContext = ContextUtil.CreateControllerContext(request: new HttpRequestMessage());
+ _controllerContext.ControllerDescriptor = _controllerDescriptor;
+
+ _actionContext = ContextUtil.CreateActionContext(_controllerContext, actionDescriptor: _mockActionDescriptor.Object);
+
+ }
+
+ [Fact]
+ public void BindValuesAsync_Invokes_Inner_And_Traces()
+ {
+ // Arrange
+ bool wasInvoked = false;
+ Mock<HttpActionBinding> mockBinder = new Mock<HttpActionBinding>() { CallBase = true };
+ mockBinder.Setup(b => b.ExecuteBindingAsync(
+ It.IsAny<HttpActionContext>(),
+ It.IsAny<CancellationToken>())).
+ Callback(() => wasInvoked = true).Returns(TaskHelpers.Completed());
+
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpActionBindingTracer tracer = new HttpActionBindingTracer(mockBinder.Object, traceWriter);
+
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(_actionContext.Request, TraceCategories.ModelBindingCategory, TraceLevel.Info) { Kind = TraceKind.Begin },
+ new TraceRecord(_actionContext.Request, TraceCategories.ModelBindingCategory, TraceLevel.Info) { Kind = TraceKind.End }
+ };
+
+ // Act
+ tracer.ExecuteBindingAsync(_actionContext, CancellationToken.None).Wait();
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ Assert.True(wasInvoked);
+ }
+
+ [Fact]
+ public void ExecuteBindingAsync_Faults_And_Traces_When_Inner_Faults()
+ {
+ // Arrange
+ InvalidOperationException exception = new InvalidOperationException();
+ TaskCompletionSource<object> tcs = new TaskCompletionSource<object>();
+ tcs.TrySetException(exception);
+ Mock<HttpActionBinding> mockBinder = new Mock<HttpActionBinding>() { CallBase = true };
+ mockBinder.Setup(b => b.ExecuteBindingAsync(
+ It.IsAny<HttpActionContext>(),
+ It.IsAny<CancellationToken>())).
+ Returns(tcs.Task);
+
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpActionBindingTracer tracer = new HttpActionBindingTracer(mockBinder.Object, traceWriter);
+
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(_actionContext.Request, TraceCategories.ModelBindingCategory, TraceLevel.Info) { Kind = TraceKind.Begin },
+ new TraceRecord(_actionContext.Request, TraceCategories.ModelBindingCategory, TraceLevel.Error) { Kind = TraceKind.End }
+ };
+
+ // Act
+ Task task = tracer.ExecuteBindingAsync(_actionContext, CancellationToken.None);
+
+ // Assert
+ Exception thrown = Assert.Throws<InvalidOperationException>(() => task.Wait());
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ Assert.Same(exception, thrown);
+ Assert.Same(exception, traceWriter.Traces[1].Exception);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Tracing/Tracers/HttpActionDescriptorTracerTest.cs b/test/System.Web.Http.Test/Tracing/Tracers/HttpActionDescriptorTracerTest.cs
new file mode 100644
index 00000000..3b07adf3
--- /dev/null
+++ b/test/System.Web.Http.Test/Tracing/Tracers/HttpActionDescriptorTracerTest.cs
@@ -0,0 +1,136 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Web.Http.Controllers;
+using System.Web.Http.Filters;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Tracing.Tracers
+{
+ public class HttpActionDescriptorTracerTest
+ {
+ // This test verifies only one kind of filter is wrapped, proving
+ // the static FilterTracer.CreateFilterTracer was called from GetFilterPipeline.
+ // Deeper testing of FilterTracer.CreateFilterTracer is in FilterTracerTest.
+ [Fact]
+ public void GetFilterPipeline_Returns_Wrapped_Filters()
+ {
+ // Arrange
+ Mock<IFilter> mockFilter = new Mock<IFilter>();
+ FilterInfo filter = new FilterInfo(mockFilter.Object, FilterScope.First);
+ Collection<FilterInfo> filterCollection = new Collection<FilterInfo>(new FilterInfo[] { filter });
+ Mock<HttpActionDescriptor> mockActionDescriptor = new Mock<HttpActionDescriptor>() { CallBase = true };
+ mockActionDescriptor.Setup(a => a.ActionName).Returns("test");
+ mockActionDescriptor.Setup(a => a.GetFilterPipeline()).Returns(filterCollection);
+ HttpControllerContext controllerContext = ContextUtil.CreateControllerContext();
+ controllerContext.ControllerDescriptor = new HttpControllerDescriptor(controllerContext.Configuration, "test", typeof(ApiController));
+ HttpActionDescriptorTracer tracer = new HttpActionDescriptorTracer(controllerContext, mockActionDescriptor.Object, new TestTraceWriter());
+
+ // Act
+ Collection<FilterInfo> wrappedFilterCollection = tracer.GetFilterPipeline();
+
+ // Assert
+ Assert.IsType<FilterTracer>(wrappedFilterCollection[0].Instance);
+ }
+
+ // This test verifies only one kind of filter is wrapped, proving
+ // the static FilterTracer.CreateFilterTracer was called from GetFilterPipeline.
+ // Deeper testing of FilterTracer.CreateFilterTracer is in FilterTracerTest.
+ [Fact]
+ public void GetFilters_Returns_Wrapped_IFilters()
+ {
+ // Arrange
+ Mock<IFilter> mockFilter = new Mock<IFilter>();
+ IFilter[] filters = new IFilter[] { mockFilter.Object };
+ Mock<HttpActionDescriptor> mockActionDescriptor = new Mock<HttpActionDescriptor>() { CallBase = true };
+ mockActionDescriptor.Setup(a => a.ActionName).Returns("test");
+ mockActionDescriptor.Setup(a => a.GetFilters()).Returns(filters);
+ HttpControllerContext controllerContext = ContextUtil.CreateControllerContext();
+ controllerContext.ControllerDescriptor = new HttpControllerDescriptor(controllerContext.Configuration, "test", typeof(ApiController));
+ HttpActionDescriptorTracer tracer = new HttpActionDescriptorTracer(controllerContext, mockActionDescriptor.Object, new TestTraceWriter());
+
+ // Act
+ IFilter[] wrappedFilters = tracer.GetFilters().ToArray();
+
+ // Assert
+ Assert.IsType<FilterTracer>(wrappedFilters[0]);
+ }
+
+ [Fact]
+ public void Execute_Invokes_Inner_Execute()
+ {
+ // Arrange
+ bool executeCalled = false;
+ Mock<HttpActionDescriptor> mockActionDescriptor = new Mock<HttpActionDescriptor>() { CallBase = true };
+ mockActionDescriptor.Setup(a => a.ActionName).Returns("test");
+ mockActionDescriptor.Setup(
+ a => a.Execute(It.IsAny<HttpControllerContext>(), It.IsAny<IDictionary<string, object>>())).Callback(() => executeCalled = true);
+ HttpControllerContext controllerContext = ContextUtil.CreateControllerContext();
+ controllerContext.ControllerDescriptor = new HttpControllerDescriptor(controllerContext.Configuration, "test", typeof(ApiController));
+ IDictionary<string, object> arguments = new Dictionary<string, object>();
+ HttpActionDescriptorTracer tracer = new HttpActionDescriptorTracer(controllerContext, mockActionDescriptor.Object, new TestTraceWriter());
+
+ // Act
+ tracer.Execute(controllerContext, arguments);
+
+ // Assert
+ Assert.True(executeCalled);
+ }
+
+ [Fact]
+ public void Execute_Traces()
+ {
+ // Arrange
+ Mock<HttpActionDescriptor> mockActionDescriptor = new Mock<HttpActionDescriptor>() { CallBase = true };
+ mockActionDescriptor.Setup(a => a.ActionName).Returns("test");
+ mockActionDescriptor.Setup(
+ a => a.Execute(It.IsAny<HttpControllerContext>(), It.IsAny<IDictionary<string, object>>())).Callback(() => {});
+ HttpControllerContext controllerContext = ContextUtil.CreateControllerContext();
+ controllerContext.ControllerDescriptor = new HttpControllerDescriptor(controllerContext.Configuration, "test", typeof(ApiController));
+ IDictionary<string, object> arguments = new Dictionary<string, object>();
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpActionDescriptorTracer tracer = new HttpActionDescriptorTracer(controllerContext, mockActionDescriptor.Object, traceWriter);
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(controllerContext.Request, TraceCategories.ActionCategory, TraceLevel.Info) { Kind = TraceKind.Begin },
+ new TraceRecord(controllerContext.Request, TraceCategories.ActionCategory, TraceLevel.Info) { Kind = TraceKind.End }
+ };
+
+ // Act
+ tracer.Execute(controllerContext, arguments);
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ }
+
+ [Fact]
+ public void Execute_Throws_What_Inner_Throws_And_Traces()
+ {
+ // Arrange
+ Mock<HttpActionDescriptor> mockActionDescriptor = new Mock<HttpActionDescriptor>() { CallBase = true };
+ InvalidOperationException exception = new InvalidOperationException("test");
+ mockActionDescriptor.Setup(
+ a => a.Execute(It.IsAny<HttpControllerContext>(), It.IsAny<IDictionary<string, object>>())).Throws(exception);
+ mockActionDescriptor.Setup(a => a.ActionName).Returns("test");
+ HttpControllerContext controllerContext = ContextUtil.CreateControllerContext();
+ controllerContext.ControllerDescriptor = new HttpControllerDescriptor(controllerContext.Configuration, "test", typeof(ApiController));
+ IDictionary<string, object> arguments = new Dictionary<string, object>();
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpActionDescriptorTracer tracer = new HttpActionDescriptorTracer(controllerContext, mockActionDescriptor.Object, traceWriter);
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(controllerContext.Request, TraceCategories.ActionCategory, TraceLevel.Info) { Kind = TraceKind.Begin },
+ new TraceRecord(controllerContext.Request, TraceCategories.ActionCategory, TraceLevel.Error) { Kind = TraceKind.End }
+ };
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(() => tracer.Execute(controllerContext, arguments));
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ Assert.Same(exception, traceWriter.Traces[1].Exception);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Tracing/Tracers/HttpActionInvokerTracerTest.cs b/test/System.Web.Http.Test/Tracing/Tracers/HttpActionInvokerTracerTest.cs
new file mode 100644
index 00000000..ad1443d6
--- /dev/null
+++ b/test/System.Web.Http.Test/Tracing/Tracers/HttpActionInvokerTracerTest.cs
@@ -0,0 +1,224 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Controllers;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Tracing.Tracers
+{
+ public class HttpActionInvokerTracerTest
+ {
+ private HttpActionContext _actionContext;
+ private ApiController _apiController;
+
+ public HttpActionInvokerTracerTest()
+ {
+ UsersController controller = new UsersController();
+ _apiController = controller;
+
+ Func<HttpResponseMessage> actionMethod = controller.Get;
+ _actionContext = ContextUtil.CreateActionContext(
+ ContextUtil.CreateControllerContext(instance: _apiController),
+ new ReflectedHttpActionDescriptor { MethodInfo = actionMethod.Method });
+ HttpRequestMessage request = new HttpRequestMessage();
+ _actionContext.ControllerContext.Request = request;
+ }
+
+ [Fact]
+ public void InvokeActionAsync_Calls_ActionDescriptor_Execute()
+ {
+ // Arrange
+ Mock<HttpActionDescriptor> mockActionDescriptor = new Mock<HttpActionDescriptor>() { CallBase = true };
+ mockActionDescriptor.Setup(a => a.ActionName).Returns("mockAction");
+ mockActionDescriptor.Setup(a => a.GetParameters()).Returns(new Collection<HttpParameterDescriptor>(new HttpParameterDescriptor[0]));
+ mockActionDescriptor.Setup(a => a.ReturnType).Returns(typeof(void));
+ bool executeWasCalled = false;
+ mockActionDescriptor.Setup(
+ a => a.Execute(It.IsAny<HttpControllerContext>(), It.IsAny<IDictionary<string, object>>())).Callback(
+ () => { executeWasCalled = true; });
+
+ HttpActionContext context = ContextUtil.CreateActionContext(
+ ContextUtil.CreateControllerContext(instance: _apiController),
+ mockActionDescriptor.Object);
+
+ HttpActionInvokerTracer tracer = new HttpActionInvokerTracer(new ApiControllerActionInvoker(), new TestTraceWriter());
+
+ // Act
+ ((IHttpActionInvoker)tracer).InvokeActionAsync(context, CancellationToken.None).Wait();
+
+ // Assert
+ Assert.True(executeWasCalled);
+ }
+
+ [Fact]
+ public void InvokeActionAsync_Traces_Begin_And_End_Info()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpActionInvokerTracer tracer = new HttpActionInvokerTracer(new ApiControllerActionInvoker(), traceWriter);
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(_actionContext.Request, TraceCategories.ActionCategory, TraceLevel.Info) { Kind = TraceKind.Begin },
+ new TraceRecord(_actionContext.Request, TraceCategories.ActionCategory, TraceLevel.Info) { Kind = TraceKind.End }
+ };
+
+ // Act
+ Task task = ((IHttpActionInvoker)tracer).InvokeActionAsync(_actionContext, CancellationToken.None);
+ task.Wait();
+
+ // Assert
+ Assert.Equal(2, traceWriter.Traces.Count);
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ }
+
+ [Fact]
+ public void InvokeActionAsync_Returns_Cancelled_Inner_Task()
+ {
+ // Arrange
+ CancellationTokenSource cancellationSource = new CancellationTokenSource();
+ cancellationSource.Cancel();
+
+ HttpActionInvokerTracer tracer = new HttpActionInvokerTracer(new ApiControllerActionInvoker(), new TestTraceWriter());
+
+ // Act
+ var response = ((IHttpActionInvoker)tracer).InvokeActionAsync(_actionContext, cancellationSource.Token);
+
+ // Assert
+ Assert.Throws<TaskCanceledException>(() => { response.Wait(); });
+ Assert.Equal<TaskStatus>(TaskStatus.Canceled, response.Status);
+ }
+
+ [Fact]
+ public void InvokeActionAsync_Traces_Cancelled_Inner_Task()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpActionInvokerTracer tracer = new HttpActionInvokerTracer(new ApiControllerActionInvoker(), traceWriter);
+ CancellationTokenSource cancellationSource = new CancellationTokenSource();
+ cancellationSource.Cancel();
+
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(_actionContext.Request, TraceCategories.ActionCategory, TraceLevel.Info) { Kind = TraceKind.Begin },
+ new TraceRecord(_actionContext.Request, TraceCategories.ActionCategory, TraceLevel.Warn) { Kind = TraceKind.End }
+ };
+
+ // Act
+ var response = ((IHttpActionInvoker)tracer).InvokeActionAsync(_actionContext, cancellationSource.Token);
+
+ // Assert
+ Assert.Throws<TaskCanceledException>(() => { response.Wait(); });
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ }
+
+ [Fact]
+ public void InvokeActionAsync_Returns_Faulted_Inner_Task()
+ {
+ // Arrange
+ Mock<ApiControllerActionInvoker> mockActionInvoker = new Mock<ApiControllerActionInvoker>() { CallBase = true };
+ InvalidOperationException expectedException = new InvalidOperationException("test message");
+ TaskCompletionSource<HttpResponseMessage> tcs = new TaskCompletionSource<HttpResponseMessage>(null);
+ tcs.TrySetException(expectedException);
+ mockActionInvoker.Setup(
+ a => a.InvokeActionAsync(It.IsAny<HttpActionContext>(), It.IsAny<CancellationToken>())).Returns(tcs.Task);
+ HttpActionInvokerTracer tracer = new HttpActionInvokerTracer(mockActionInvoker.Object, new TestTraceWriter());
+
+ // Act
+ var response = ((IHttpActionInvoker)tracer).InvokeActionAsync(_actionContext, CancellationToken.None);
+
+ // Assert
+ Assert.Throws<InvalidOperationException>(() => response.Wait());
+ Assert.Equal<TaskStatus>(TaskStatus.Faulted, response.Status);
+ Assert.Equal(expectedException.Message, response.Exception.GetBaseException().Message);
+ }
+
+ [Fact]
+ public void InvokeActionAsync_Traces_Faulted_Inner_Task()
+ {
+ // Arrange
+ Mock<ApiControllerActionInvoker> mockActionInvoker = new Mock<ApiControllerActionInvoker>() { CallBase = true };
+ InvalidOperationException expectedException = new InvalidOperationException("test message");
+ TaskCompletionSource<HttpResponseMessage> tcs = new TaskCompletionSource<HttpResponseMessage>(null);
+ tcs.TrySetException(expectedException);
+ mockActionInvoker.Setup(
+ a => a.InvokeActionAsync(It.IsAny<HttpActionContext>(), It.IsAny<CancellationToken>())).Returns(tcs.Task);
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpActionInvokerTracer tracer = new HttpActionInvokerTracer(mockActionInvoker.Object, traceWriter);
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(_actionContext.Request, TraceCategories.ActionCategory, TraceLevel.Info) { Kind = TraceKind.Begin },
+ new TraceRecord(_actionContext.Request, TraceCategories.ActionCategory, TraceLevel.Error) { Kind = TraceKind.End }
+ };
+
+ // Act
+ var response = ((IHttpActionInvoker)tracer).InvokeActionAsync(_actionContext, CancellationToken.None);
+
+ // Assert
+ Assert.Throws<InvalidOperationException>(() => response.Wait());
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ Assert.Equal(expectedException, traceWriter.Traces[1].Exception);
+ }
+
+ [Fact]
+ public void InvokeActionAsync_Throws_When_ActionContext_Is_Null()
+ {
+ // Arrange
+ HttpActionInvokerTracer tracer = new HttpActionInvokerTracer(new ApiControllerActionInvoker(), new TestTraceWriter());
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => ((IHttpActionInvoker)tracer).InvokeActionAsync(null, CancellationToken.None),
+ "actionContext");
+ }
+
+ [Fact]
+ public void InvokeActionAsync_Throws_Exception_Thrown_From_Inner()
+ {
+ // Arrange
+ InvalidOperationException expectedException = new InvalidOperationException("test message");
+ Mock<ApiControllerActionInvoker> mockActionInvoker = new Mock<ApiControllerActionInvoker>() {CallBase = true};
+ mockActionInvoker.Setup(
+ a => a.InvokeActionAsync(It.IsAny<HttpActionContext>(), It.IsAny<CancellationToken>())).Throws(expectedException);
+ HttpActionInvokerTracer tracer = new HttpActionInvokerTracer(mockActionInvoker.Object, new TestTraceWriter());
+
+ // Act & Assert
+ InvalidOperationException thrownException = Assert.Throws<InvalidOperationException>(
+ () => ((IHttpActionInvoker)tracer).InvokeActionAsync(_actionContext, CancellationToken.None)
+ );
+
+ // Assert
+ Assert.Equal(expectedException, thrownException);
+ }
+
+ [Fact]
+ public void InvokeActionAsync_Traces_Exception_Thrown_From_Inner()
+ {
+ // Arrange
+ InvalidOperationException expectedException = new InvalidOperationException("test message");
+ Mock<ApiControllerActionInvoker> mockActionInvoker = new Mock<ApiControllerActionInvoker>() { CallBase = true };
+ mockActionInvoker.Setup(
+ a => a.InvokeActionAsync(It.IsAny<HttpActionContext>(), It.IsAny<CancellationToken>())).Throws(expectedException);
+
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpActionInvokerTracer tracer = new HttpActionInvokerTracer(mockActionInvoker.Object, traceWriter);
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(_actionContext.Request, TraceCategories.ActionCategory, TraceLevel.Info) { Kind = TraceKind.Begin },
+ new TraceRecord(_actionContext.Request, TraceCategories.ActionCategory, TraceLevel.Error) { Kind = TraceKind.End }
+ };
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ () => ((IHttpActionInvoker)tracer).InvokeActionAsync(_actionContext, CancellationToken.None)
+ );
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ Assert.Equal(expectedException, traceWriter.Traces[1].Exception);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Tracing/Tracers/HttpActionSelectorTracerTest.cs b/test/System.Web.Http.Test/Tracing/Tracers/HttpActionSelectorTracerTest.cs
new file mode 100644
index 00000000..a68029f4
--- /dev/null
+++ b/test/System.Web.Http.Test/Tracing/Tracers/HttpActionSelectorTracerTest.cs
@@ -0,0 +1,80 @@
+using System.Collections.ObjectModel;
+using System.Net.Http;
+using System.Web.Http.Controllers;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Tracing.Tracers
+{
+ public class HttpActionSelectorTracerTest
+ {
+ private Mock<HttpActionDescriptor> _mockActionDescriptor;
+ private HttpActionContext _actionContext;
+ private HttpControllerContext _controllerContext;
+ private HttpControllerDescriptor _controllerDescriptor;
+
+ public HttpActionSelectorTracerTest()
+ {
+ _mockActionDescriptor = new Mock<HttpActionDescriptor>() { CallBase = true };
+ _mockActionDescriptor.Setup(a => a.ActionName).Returns("test");
+ _mockActionDescriptor.Setup(a => a.GetParameters()).Returns(new Collection<HttpParameterDescriptor>(new HttpParameterDescriptor[0]));
+
+ _controllerDescriptor = new HttpControllerDescriptor(new HttpConfiguration(), "controller", typeof(ApiController));
+
+ _controllerContext = ContextUtil.CreateControllerContext(request: new HttpRequestMessage());
+ _controllerContext.ControllerDescriptor = _controllerDescriptor;
+
+ _actionContext = ContextUtil.CreateActionContext(_controllerContext, actionDescriptor: _mockActionDescriptor.Object);
+ }
+
+ [Fact]
+ public void SelectAction_Traces_And_Returns_ActionDescriptor_Tracer()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ Mock<IHttpActionSelector> mockSelector = new Mock<IHttpActionSelector>();
+ mockSelector.Setup(s => s.SelectAction(_controllerContext)).Returns(_mockActionDescriptor.Object);
+ HttpActionSelectorTracer tracer = new HttpActionSelectorTracer(mockSelector.Object, traceWriter);
+
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(_actionContext.Request, TraceCategories.ActionCategory, TraceLevel.Info) { Kind = TraceKind.Begin },
+ new TraceRecord(_actionContext.Request, TraceCategories.ActionCategory, TraceLevel.Info) { Kind = TraceKind.End }
+ };
+
+ // Act
+ HttpActionDescriptor selectedActionDescriptor = ((IHttpActionSelector)tracer).SelectAction(_controllerContext);
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ Assert.IsAssignableFrom<HttpActionDescriptorTracer>(selectedActionDescriptor);
+ }
+
+
+ [Fact]
+ public void SelectAction_Traces_And_Throws_Exception_Thrown_From_Inner()
+ {
+ // Arrange
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ Mock<IHttpActionSelector> mockSelector = new Mock<IHttpActionSelector>();
+ InvalidOperationException exception = new InvalidOperationException();
+ mockSelector.Setup(s => s.SelectAction(_controllerContext)).Throws(exception);
+ HttpActionSelectorTracer tracer = new HttpActionSelectorTracer(mockSelector.Object, traceWriter);
+
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(_actionContext.Request, TraceCategories.ActionCategory, TraceLevel.Info) { Kind = TraceKind.Begin },
+ new TraceRecord(_actionContext.Request, TraceCategories.ActionCategory, TraceLevel.Error) { Kind = TraceKind.End }
+ };
+
+ // Act
+ Exception thrown = Assert.Throws<InvalidOperationException>(() => ((IHttpActionSelector)tracer).SelectAction(_controllerContext));
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ Assert.Same(exception, thrown);
+ Assert.Same(exception, traceWriter.Traces[1].Exception);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Tracing/Tracers/HttpControllerActivatorTracerTest.cs b/test/System.Web.Http.Test/Tracing/Tracers/HttpControllerActivatorTracerTest.cs
new file mode 100644
index 00000000..2b84b4b3
--- /dev/null
+++ b/test/System.Web.Http.Test/Tracing/Tracers/HttpControllerActivatorTracerTest.cs
@@ -0,0 +1,64 @@
+using System.Net.Http;
+using System.Web.Http.Controllers;
+using System.Web.Http.Dispatcher;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Tracing.Tracers
+{
+ public class HttpControllerActivatorTracerTest
+ {
+ [Fact]
+ public void Create_Invokes_Inner_And_Traces()
+ {
+ // Arrange
+ Mock<ApiController> mockController = new Mock<ApiController>();
+ Mock<IHttpControllerActivator> mockActivator = new Mock<IHttpControllerActivator>() {CallBase = true};
+ mockActivator.Setup(b => b.Create(It.IsAny<HttpControllerContext>(), It.IsAny<Type>())).Returns(mockController.Object);
+ HttpControllerContext controllerContext = ContextUtil.CreateControllerContext(request: new HttpRequestMessage());
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpControllerActivatorTracer tracer = new HttpControllerActivatorTracer(mockActivator.Object, traceWriter);
+
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(controllerContext.Request, TraceCategories.ControllersCategory, TraceLevel.Info) { Kind = TraceKind.Begin, Operation = "Create" },
+ new TraceRecord(controllerContext.Request, TraceCategories.ControllersCategory, TraceLevel.Info) { Kind = TraceKind.End, Operation = "Create" }
+ };
+
+ // Act
+ IHttpController createdController = ((IHttpControllerActivator)tracer).Create(controllerContext, mockController.Object.GetType());
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ Assert.IsAssignableFrom<HttpControllerTracer>(createdController);
+ }
+
+ [Fact]
+ public void Create_Throws_And_Traces_When_Inner_Throws()
+ {
+ // Arrange
+ Mock<ApiController> mockController = new Mock<ApiController>();
+ Mock<IHttpControllerActivator> mockActivator = new Mock<IHttpControllerActivator>() { CallBase = true };
+ InvalidOperationException exception = new InvalidOperationException("test");
+ mockActivator.Setup(b => b.Create(It.IsAny<HttpControllerContext>(), It.IsAny<Type>())).Throws(exception);
+ HttpControllerContext controllerContext = ContextUtil.CreateControllerContext(request: new HttpRequestMessage());
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpControllerActivatorTracer tracer = new HttpControllerActivatorTracer(mockActivator.Object, traceWriter);
+
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(controllerContext.Request, TraceCategories.ControllersCategory, TraceLevel.Info) { Kind = TraceKind.Begin, Operation = "Create" },
+ new TraceRecord(controllerContext.Request, TraceCategories.ControllersCategory, TraceLevel.Error) { Kind = TraceKind.End, Operation = "Create" }
+ };
+
+ // Act & Assert
+ Exception thrown = Assert.Throws<InvalidOperationException>(() => ((IHttpControllerActivator)tracer).Create(controllerContext, mockController.Object.GetType()));
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ Assert.Same(exception, thrown);
+ Assert.Same(exception, traceWriter.Traces[1].Exception);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Tracing/Tracers/HttpControllerFactoryTracerTest.cs b/test/System.Web.Http.Test/Tracing/Tracers/HttpControllerFactoryTracerTest.cs
new file mode 100644
index 00000000..36c3e4f6
--- /dev/null
+++ b/test/System.Web.Http.Test/Tracing/Tracers/HttpControllerFactoryTracerTest.cs
@@ -0,0 +1,63 @@
+using System.Net.Http;
+using System.Web.Http.Controllers;
+using System.Web.Http.Dispatcher;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Tracing.Tracers
+{
+ public class HttpControllerFactoryTracerTest
+ {
+ [Fact]
+ public void CreateController_Invokes_Inner_And_Traces()
+ {
+ // Arrange
+ Mock<ApiController> mockController = new Mock<ApiController>();
+ Mock<IHttpControllerFactory> mockFactory = new Mock<IHttpControllerFactory>() { CallBase = true };
+ mockFactory.Setup(b => b.CreateController(It.IsAny<HttpControllerContext>(), It.IsAny<string>())).Returns(mockController.Object);
+ HttpControllerContext controllerContext = ContextUtil.CreateControllerContext(request: new HttpRequestMessage());
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpControllerFactoryTracer tracer = new HttpControllerFactoryTracer(mockFactory.Object, traceWriter);
+
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(controllerContext.Request, TraceCategories.ControllersCategory, TraceLevel.Info) { Kind = TraceKind.Begin, Operation = "CreateController" },
+ new TraceRecord(controllerContext.Request, TraceCategories.ControllersCategory, TraceLevel.Info) { Kind = TraceKind.End, Operation = "CreateController" }
+ };
+
+ // Act
+ IHttpController createdController = ((IHttpControllerFactory)tracer).CreateController(controllerContext, "anyName");
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ Assert.IsAssignableFrom<HttpControllerTracer>(createdController);
+ }
+
+ [Fact]
+ public void CreateController_Throws_And_Traces_When_Inner_Throws()
+ {
+ // Arrange
+ Mock<IHttpControllerFactory> mockFactory = new Mock<IHttpControllerFactory>() { CallBase = true };
+ InvalidOperationException exception = new InvalidOperationException("test");
+ mockFactory.Setup(b => b.CreateController(It.IsAny<HttpControllerContext>(), It.IsAny<string>())).Throws(exception);
+ HttpControllerContext controllerContext = ContextUtil.CreateControllerContext(request: new HttpRequestMessage());
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpControllerFactoryTracer tracer = new HttpControllerFactoryTracer(mockFactory.Object, traceWriter);
+
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(controllerContext.Request, TraceCategories.ControllersCategory, TraceLevel.Info) { Kind = TraceKind.Begin, Operation = "CreateController" },
+ new TraceRecord(controllerContext.Request, TraceCategories.ControllersCategory, TraceLevel.Error) { Kind = TraceKind.End, Operation = "CreateController" }
+ };
+
+ // Act
+ Exception thrown = Assert.Throws<InvalidOperationException>(() => ((IHttpControllerFactory)tracer).CreateController(controllerContext, "anyName"));
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ Assert.Same(exception, thrown);
+ Assert.Same(exception, traceWriter.Traces[1].Exception);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Tracing/Tracers/HttpControllerTracerTest.cs b/test/System.Web.Http.Test/Tracing/Tracers/HttpControllerTracerTest.cs
new file mode 100644
index 00000000..bd3b50d7
--- /dev/null
+++ b/test/System.Web.Http.Test/Tracing/Tracers/HttpControllerTracerTest.cs
@@ -0,0 +1,121 @@
+using System.Collections.ObjectModel;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Controllers;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Tracing.Tracers
+{
+ public class HttpControllerTracerTest
+ {
+ private Mock<HttpActionDescriptor> _mockActionDescriptor;
+ private HttpControllerDescriptor _controllerDescriptor;
+
+ public HttpControllerTracerTest()
+ {
+ _mockActionDescriptor = new Mock<HttpActionDescriptor>() { CallBase = true };
+ _mockActionDescriptor.Setup(a => a.ActionName).Returns("test");
+ _mockActionDescriptor.Setup(a => a.GetParameters()).Returns(new Collection<HttpParameterDescriptor>(new HttpParameterDescriptor[0]));
+
+ _controllerDescriptor = new HttpControllerDescriptor(new HttpConfiguration(), "controller", typeof(ApiController));
+ }
+
+ [Fact]
+ public void ExecuteAsync_Invokes_Inner_And_Traces()
+ {
+ // Arrange
+ HttpResponseMessage response = new HttpResponseMessage();
+ Mock<ApiController> mockController = new Mock<ApiController>() { CallBase = true };
+ mockController.Setup(b => b.ExecuteAsync(It.IsAny<HttpControllerContext>(), It.IsAny<CancellationToken>())).Returns(TaskHelpers.FromResult<HttpResponseMessage>(response));
+
+ HttpControllerContext controllerContext = ContextUtil.CreateControllerContext(request: new HttpRequestMessage());
+ controllerContext.ControllerDescriptor = _controllerDescriptor;
+ controllerContext.Controller = mockController.Object;
+
+ HttpActionContext actionContext = ContextUtil.CreateActionContext(controllerContext, actionDescriptor: _mockActionDescriptor.Object);
+
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpControllerTracer tracer = new HttpControllerTracer(mockController.Object, traceWriter);
+
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(actionContext.Request, TraceCategories.ControllersCategory, TraceLevel.Info) { Kind = TraceKind.Begin },
+ new TraceRecord(actionContext.Request, TraceCategories.ControllersCategory, TraceLevel.Info) { Kind = TraceKind.End }
+ };
+
+ // Act
+ HttpResponseMessage actualResponse = ((IHttpController)tracer).ExecuteAsync(controllerContext, CancellationToken.None).Result;
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ Assert.Same(response, actualResponse);
+ }
+
+ [Fact]
+ public void ExecuteAsync_Faults_And_Traces_When_Inner_Faults()
+ {
+ // Arrange
+ InvalidOperationException exception = new InvalidOperationException();
+ TaskCompletionSource<HttpResponseMessage> tcs = new TaskCompletionSource<HttpResponseMessage>();
+ tcs.TrySetException(exception);
+ Mock<ApiController> mockController = new Mock<ApiController>() { CallBase = true };
+ mockController.Setup(b => b.ExecuteAsync(It.IsAny<HttpControllerContext>(), It.IsAny<CancellationToken>())).Returns(tcs.Task);
+
+ HttpControllerContext controllerContext = ContextUtil.CreateControllerContext(request: new HttpRequestMessage());
+ controllerContext.ControllerDescriptor = _controllerDescriptor;
+ controllerContext.Controller = mockController.Object;
+
+ HttpActionContext actionContext = ContextUtil.CreateActionContext(controllerContext, actionDescriptor: _mockActionDescriptor.Object);
+
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpControllerTracer tracer = new HttpControllerTracer(mockController.Object, traceWriter);
+
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(actionContext.Request, TraceCategories.ControllersCategory, TraceLevel.Info) { Kind = TraceKind.Begin },
+ new TraceRecord(actionContext.Request, TraceCategories.ControllersCategory, TraceLevel.Error) { Kind = TraceKind.End }
+ };
+
+ // Act
+ Exception thrown = Assert.Throws<InvalidOperationException>(() => ((IHttpController)tracer).ExecuteAsync(controllerContext, CancellationToken.None).Wait());
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ Assert.Same(exception, thrown);
+ Assert.Same(exception, traceWriter.Traces[1].Exception);
+ }
+
+ [Fact]
+ public void ExecuteAsync_IsCancelled_And_Traces_When_Inner_IsCancelled()
+ {
+ // Arrange
+ Mock<ApiController> mockController = new Mock<ApiController>() { CallBase = true };
+ mockController.Setup(b => b.ExecuteAsync(It.IsAny<HttpControllerContext>(), It.IsAny<CancellationToken>())).Returns(TaskHelpers.Canceled<HttpResponseMessage>());
+
+ HttpControllerContext controllerContext = ContextUtil.CreateControllerContext(request: new HttpRequestMessage());
+ controllerContext.ControllerDescriptor = _controllerDescriptor;
+ controllerContext.Controller = mockController.Object;
+
+ HttpActionContext actionContext = ContextUtil.CreateActionContext(controllerContext, actionDescriptor: _mockActionDescriptor.Object);
+
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpControllerTracer tracer = new HttpControllerTracer(mockController.Object, traceWriter);
+
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(actionContext.Request, TraceCategories.ControllersCategory, TraceLevel.Info) { Kind = TraceKind.Begin },
+ new TraceRecord(actionContext.Request, TraceCategories.ControllersCategory, TraceLevel.Warn) { Kind = TraceKind.End }
+ };
+
+ // Act
+ Task task = ((IHttpController) tracer).ExecuteAsync(controllerContext,CancellationToken.None);
+ Exception thrown = Assert.Throws<TaskCanceledException>(() => task.Wait());
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Tracing/Tracers/HttpParameterBindingTracerTest.cs b/test/System.Web.Http.Test/Tracing/Tracers/HttpParameterBindingTracerTest.cs
new file mode 100644
index 00000000..a1d5b7c9
--- /dev/null
+++ b/test/System.Web.Http.Test/Tracing/Tracers/HttpParameterBindingTracerTest.cs
@@ -0,0 +1,122 @@
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Http.Controllers;
+using System.Web.Http.Metadata;
+using System.Web.Http.Metadata.Providers;
+using System.Web.Http.ModelBinding;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Tracing.Tracers
+{
+ public class HttpParameterBindingTracerTest
+ {
+ [Fact]
+ public void ExecuteBindingAsync_Traces_And_Invokes_Inner()
+ {
+ // Arrange
+ Mock<HttpParameterDescriptor> mockParamDescriptor = new Mock<HttpParameterDescriptor>() { CallBase = true };
+ mockParamDescriptor.Setup(d => d.ParameterName).Returns("paramName");
+ mockParamDescriptor.Setup(d => d.ParameterType).Returns(typeof(string));
+ Mock<HttpParameterBinding> mockBinding = new Mock<HttpParameterBinding>(mockParamDescriptor.Object) { CallBase = true };
+ bool innerInvoked = false;
+ mockBinding.Setup(
+ b =>
+ b.ExecuteBindingAsync(It.IsAny<ModelMetadataProvider>(), It.IsAny<HttpActionContext>(),
+ It.IsAny<CancellationToken>())).Returns(TaskHelpers.Completed()).Callback(() => innerInvoked = true);
+
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpParameterBindingTracer tracer = new HttpParameterBindingTracer(mockBinding.Object, traceWriter);
+ HttpActionContext actionContext = ContextUtil.CreateActionContext();
+ ModelMetadataProvider metadataProvider = new EmptyModelMetadataProvider();
+
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(actionContext.Request, TraceCategories.ModelBindingCategory, TraceLevel.Info) { Kind = TraceKind.Begin, Operation = "ExecuteBindingAsync" },
+ new TraceRecord(actionContext.Request, TraceCategories.ModelBindingCategory, TraceLevel.Info) { Kind = TraceKind.End, Operation = "ExecuteBindingAsync" }
+ };
+
+ // Act
+ Task task = tracer.ExecuteBindingAsync(metadataProvider, actionContext, CancellationToken.None);
+ task.Wait();
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ Assert.True(innerInvoked);
+ }
+
+ [Fact]
+ public void ExecuteBindingAsync_Traces_And_Throws_When_Inner_Throws()
+ {
+ // Arrange
+ Mock<HttpParameterDescriptor> mockParamDescriptor = new Mock<HttpParameterDescriptor>() { CallBase = true };
+ mockParamDescriptor.Setup(d => d.ParameterName).Returns("paramName");
+ mockParamDescriptor.Setup(d => d.ParameterType).Returns(typeof(string));
+ Mock<HttpParameterBinding> mockBinding = new Mock<HttpParameterBinding>(mockParamDescriptor.Object) { CallBase = true };
+ InvalidOperationException exception = new InvalidOperationException("test");
+ mockBinding.Setup(
+ b =>
+ b.ExecuteBindingAsync(It.IsAny<ModelMetadataProvider>(), It.IsAny<HttpActionContext>(),
+ It.IsAny<CancellationToken>())).Throws(exception);
+
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpParameterBindingTracer tracer = new HttpParameterBindingTracer(mockBinding.Object, traceWriter);
+ HttpActionContext actionContext = ContextUtil.CreateActionContext();
+ ModelMetadataProvider metadataProvider = new EmptyModelMetadataProvider();
+
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(actionContext.Request, TraceCategories.ModelBindingCategory, TraceLevel.Info) { Kind = TraceKind.Begin, Operation = "ExecuteBindingAsync" },
+ new TraceRecord(actionContext.Request, TraceCategories.ModelBindingCategory, TraceLevel.Error) { Kind = TraceKind.End, Operation = "ExecuteBindingAsync" }
+ };
+
+ // Act & Assert
+ Exception thrown = Assert.Throws<InvalidOperationException>(() => tracer.ExecuteBindingAsync(metadataProvider, actionContext, CancellationToken.None));
+
+ // Assert
+ Assert.Same(exception, thrown);
+ Assert.Same(exception, traceWriter.Traces[1].Exception);
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ }
+
+ [Fact]
+ public void ExecuteBindingAsync_Traces_And_Faults_When_Inner_Faults()
+ {
+ // Arrange
+
+ Mock<HttpParameterDescriptor> mockParamDescriptor = new Mock<HttpParameterDescriptor>() { CallBase = true };
+ mockParamDescriptor.Setup(d => d.ParameterName).Returns("paramName");
+ mockParamDescriptor.Setup(d => d.ParameterType).Returns(typeof(string));
+ Mock<HttpParameterBinding> mockBinding = new Mock<HttpParameterBinding>(mockParamDescriptor.Object) { CallBase = true };
+ InvalidOperationException exception = new InvalidOperationException("test");
+ TaskCompletionSource<object> tcs = new TaskCompletionSource<object>();
+ tcs.TrySetException(exception);
+
+ mockBinding.Setup(
+ b =>
+ b.ExecuteBindingAsync(It.IsAny<ModelMetadataProvider>(), It.IsAny<HttpActionContext>(),
+ It.IsAny<CancellationToken>())).Returns(tcs.Task);
+
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpParameterBindingTracer tracer = new HttpParameterBindingTracer(mockBinding.Object, traceWriter);
+ HttpActionContext actionContext = ContextUtil.CreateActionContext();
+ ModelMetadataProvider metadataProvider = new EmptyModelMetadataProvider();
+
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(actionContext.Request, TraceCategories.ModelBindingCategory, TraceLevel.Info) { Kind = TraceKind.Begin, Operation = "ExecuteBindingAsync" },
+ new TraceRecord(actionContext.Request, TraceCategories.ModelBindingCategory, TraceLevel.Error) { Kind = TraceKind.End, Operation = "ExecuteBindingAsync" }
+ };
+
+ // Act & Assert
+ Task task = tracer.ExecuteBindingAsync(metadataProvider, actionContext, CancellationToken.None);
+
+ // Assert
+ Exception thrown = Assert.Throws<InvalidOperationException>(() => task.Wait());
+ Assert.Same(exception, thrown);
+ Assert.Same(exception, traceWriter.Traces[1].Exception);
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Tracing/Tracers/MediaTypeFormatterTracerTest.cs b/test/System.Web.Http.Test/Tracing/Tracers/MediaTypeFormatterTracerTest.cs
new file mode 100644
index 00000000..35494d7b
--- /dev/null
+++ b/test/System.Web.Http.Test/Tracing/Tracers/MediaTypeFormatterTracerTest.cs
@@ -0,0 +1,248 @@
+using System.IO;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Formatting;
+using System.Net.Http.Headers;
+using System.Threading.Tasks;
+using Moq;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Tracing.Tracers
+{
+ public class MediaTypeFormatterTracerTest
+ {
+ [Fact]
+ public void OnReadFromStreamAsync_Traces()
+ {
+ // Arrange
+ Mock<MediaTypeFormatter> mockFormatter = new Mock<MediaTypeFormatter>() { CallBase = true };
+ mockFormatter.Setup(
+ f => f.ReadFromStreamAsync(It.IsAny<Type>(), It.IsAny<Stream>(), It.IsAny<HttpContentHeaders>(), It.IsAny<IFormatterLogger>())).
+ Returns(TaskHelpers.FromResult<object>("sampleValue"));
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ request.Content = new StringContent("");
+ MediaTypeFormatterTracer tracer = new MediaTypeFormatterTracer(mockFormatter.Object, traceWriter, request);
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(request, TraceCategories.FormattingCategory, TraceLevel.Info) { Kind = TraceKind.Begin, Operation = "ReadFromStreamAsync" },
+ new TraceRecord(request, TraceCategories.FormattingCategory, TraceLevel.Info) { Kind = TraceKind.End, Operation = "ReadFromStreamAsync" }
+ };
+
+ // Act
+ Task<object> task = tracer.ReadFromStreamAsync(typeof (string), new MemoryStream(), request.Content.Headers, null);
+ string result = task.Result as string;
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ Assert.Equal("sampleValue", result);
+ }
+
+ [Fact]
+ public void OnReadFromStreamAsync_Traces_And_Throws_When_Inner_Throws()
+ {
+ // Arrange
+ InvalidOperationException exception = new InvalidOperationException("test");
+ Mock<MediaTypeFormatter> mockFormatter = new Mock<MediaTypeFormatter>() { CallBase = true };
+ mockFormatter.Setup(
+ f => f.ReadFromStreamAsync(It.IsAny<Type>(), It.IsAny<Stream>(), It.IsAny<HttpContentHeaders>(), It.IsAny<IFormatterLogger>())).Throws(exception);
+
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ request.Content = new StringContent("");
+ MediaTypeFormatterTracer tracer = new MediaTypeFormatterTracer(mockFormatter.Object, traceWriter, request);
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(request, TraceCategories.FormattingCategory, TraceLevel.Info) { Kind = TraceKind.Begin, Operation = "ReadFromStreamAsync" },
+ new TraceRecord(request, TraceCategories.FormattingCategory, TraceLevel.Error) { Kind = TraceKind.End, Operation = "ReadFromStreamAsync" }
+ };
+
+ // Act
+ Exception thrown = Assert.Throws<InvalidOperationException>(() => tracer.ReadFromStreamAsync(typeof(string), new MemoryStream(), request.Content.Headers, null));
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ Assert.Same(exception, thrown);
+ Assert.Same(exception, traceWriter.Traces[1].Exception);
+ }
+
+ [Fact]
+ public void OnReadFromStreamAsync_Traces_And_Faults_When_Inner_Faults()
+ {
+ // Arrange
+ InvalidOperationException exception = new InvalidOperationException("test");
+ Mock<MediaTypeFormatter> mockFormatter = new Mock<MediaTypeFormatter>() { CallBase = true };
+ TaskCompletionSource<object> tcs = new TaskCompletionSource<object>();
+ tcs.TrySetException(exception);
+
+ mockFormatter.Setup(
+ f => f.ReadFromStreamAsync(It.IsAny<Type>(), It.IsAny<Stream>(), It.IsAny<HttpContentHeaders>(), It.IsAny<IFormatterLogger>())).
+ Returns(tcs.Task);
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ request.Content = new StringContent("");
+ MediaTypeFormatterTracer tracer = new MediaTypeFormatterTracer(mockFormatter.Object, traceWriter, request);
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(request, TraceCategories.FormattingCategory, TraceLevel.Info) { Kind = TraceKind.Begin, Operation = "ReadFromStreamAsync" },
+ new TraceRecord(request, TraceCategories.FormattingCategory, TraceLevel.Error) { Kind = TraceKind.End, Operation = "ReadFromStreamAsync" }
+ };
+
+ // Act
+ Task<object> task = tracer.ReadFromStreamAsync(typeof (string), new MemoryStream(), request.Content.Headers, null);
+
+ // Assert
+ Exception thrown = Assert.Throws<InvalidOperationException>(() => task.Wait());
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ Assert.Same(exception, thrown);
+ Assert.Same(exception, traceWriter.Traces[1].Exception);
+ }
+
+ [Fact]
+ public void OnWriteToStreamAsync_Traces()
+ {
+ // Arrange
+ Mock<MediaTypeFormatter> mockFormatter = new Mock<MediaTypeFormatter>() { CallBase = true };
+ mockFormatter.Setup(
+ f => f.WriteToStreamAsync(It.IsAny<Type>(), It.IsAny<Object>(), It.IsAny<Stream>(), It.IsAny<HttpContentHeaders>(), It.IsAny<TransportContext>())).
+ Returns(TaskHelpers.Completed());
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ request.Content = new StringContent("");
+ MediaTypeFormatterTracer tracer = new MediaTypeFormatterTracer(mockFormatter.Object, traceWriter, request);
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(request, TraceCategories.FormattingCategory, TraceLevel.Info) { Kind = TraceKind.Begin, Operation = "WriteToStreamAsync" },
+ new TraceRecord(request, TraceCategories.FormattingCategory, TraceLevel.Info) { Kind = TraceKind.End, Operation = "WriteToStreamAsync" }
+ };
+
+ // Act
+ Task task = tracer.WriteToStreamAsync(typeof(string), "sampleValue", new MemoryStream(), request.Content.Headers, transportContext: null);
+ task.Wait();
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ }
+
+ [Fact]
+ public void OnWriteToStreamAsync_Traces_And_Throws_When_Inner_Throws()
+ {
+ // Arrange
+ InvalidOperationException exception = new InvalidOperationException("test");
+ Mock<MediaTypeFormatter> mockFormatter = new Mock<MediaTypeFormatter>() { CallBase = true };
+ mockFormatter.Setup(
+ f =>
+ f.WriteToStreamAsync(It.IsAny<Type>(), It.IsAny<Object>(), It.IsAny<Stream>(),
+ It.IsAny<HttpContentHeaders>(), It.IsAny<TransportContext>())).
+ Throws(exception);
+
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ request.Content = new StringContent("");
+ MediaTypeFormatterTracer tracer = new MediaTypeFormatterTracer(mockFormatter.Object, traceWriter, request);
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(request, TraceCategories.FormattingCategory, TraceLevel.Info) { Kind = TraceKind.Begin, Operation = "WriteToStreamAsync" },
+ new TraceRecord(request, TraceCategories.FormattingCategory, TraceLevel.Error) { Kind = TraceKind.End, Operation = "WriteToStreamAsync" }
+ };
+
+ // Act
+ Exception thrown = Assert.Throws<InvalidOperationException>(() => tracer.WriteToStreamAsync(typeof(string), "sampleValue", new MemoryStream(), request.Content.Headers, transportContext: null));
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ Assert.Same(exception, thrown);
+ Assert.Same(exception, traceWriter.Traces[1].Exception);
+ }
+
+ [Fact]
+ public void OnWriteToStreamAsync_Traces_And_Faults_When_Inner_Faults()
+ {
+ // Arrange
+ InvalidOperationException exception = new InvalidOperationException("test");
+ Mock<MediaTypeFormatter> mockFormatter = new Mock<MediaTypeFormatter>() { CallBase = true };
+ TaskCompletionSource<object> tcs = new TaskCompletionSource<object>();
+ tcs.TrySetException(exception);
+
+ mockFormatter.Setup(
+ f => f.WriteToStreamAsync(It.IsAny<Type>(), It.IsAny<Object>(), It.IsAny<Stream>(), It.IsAny<HttpContentHeaders>(), It.IsAny<TransportContext>())).
+ Returns(tcs.Task);
+
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ request.Content = new StringContent("");
+ MediaTypeFormatterTracer tracer = new MediaTypeFormatterTracer(mockFormatter.Object, traceWriter, request);
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(request, TraceCategories.FormattingCategory, TraceLevel.Info) { Kind = TraceKind.Begin, Operation = "WriteToStreamAsync" },
+ new TraceRecord(request, TraceCategories.FormattingCategory, TraceLevel.Error) { Kind = TraceKind.End, Operation = "WriteToStreamAsync" }
+ };
+
+ // Act
+ Task task = tracer.WriteToStreamAsync(typeof(string), "sampleValue", new MemoryStream(), request.Content.Headers, transportContext: null);
+
+ // Assert
+ Exception thrown = Assert.Throws<InvalidOperationException>(() => task.Wait());
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ Assert.Same(exception, thrown);
+ Assert.Same(exception, traceWriter.Traces[1].Exception);
+ }
+
+ [Fact]
+ public void GetPerRequestFormatterInstance_Returns_Tracing_MediaTypeFormatter()
+ {
+ // Arrange
+ Mock<MediaTypeFormatter> mockReturnFormatter = new Mock<MediaTypeFormatter>() { CallBase = true };
+ Mock<MediaTypeFormatter> mockFormatter = new Mock<MediaTypeFormatter>() { CallBase = true };
+ mockFormatter.Setup(
+ f =>
+ f.GetPerRequestFormatterInstance(It.IsAny<Type>(), It.IsAny<HttpRequestMessage>(),
+ It.IsAny<MediaTypeHeaderValue>())).Returns(mockReturnFormatter.Object);
+
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ HttpRequestMessage request = new HttpRequestMessage();
+ MediaTypeFormatterTracer tracer = new MediaTypeFormatterTracer(mockFormatter.Object, traceWriter, request);
+
+ // Act
+ MediaTypeFormatter actualFormatter = tracer.GetPerRequestFormatterInstance(typeof(string), request, new MediaTypeHeaderValue("application/json"));
+
+ // Assert
+ Assert.IsAssignableFrom<IFormatterTracer>(actualFormatter);
+ }
+
+ [Theory]
+ [InlineDataAttribute(typeof(XmlMediaTypeFormatter))]
+ [InlineDataAttribute(typeof(JsonMediaTypeFormatter))]
+ [InlineDataAttribute(typeof(FormUrlEncodedMediaTypeFormatter))]
+ public void CreateTracer_Returns_Tracing_Formatter(Type formatterType)
+ {
+ // Arrange
+ HttpRequestMessage request = new HttpRequestMessage();
+ MediaTypeFormatter formatter = (MediaTypeFormatter)Activator.CreateInstance(formatterType);
+
+ // Act
+ MediaTypeFormatter tracingFormatter = MediaTypeFormatterTracer.CreateTracer(formatter, new TestTraceWriter(), request);
+
+ // Assert
+ Assert.IsAssignableFrom<IFormatterTracer>(tracingFormatter);
+ Assert.IsAssignableFrom(formatterType, tracingFormatter);
+ }
+
+ [Fact]
+ public void CreateTracer_Returns_Tracing_BufferedFormatter()
+ {
+ // Arrange
+ HttpRequestMessage request = new HttpRequestMessage();
+ MediaTypeFormatter formatter = new Mock<BufferedMediaTypeFormatter>() {CallBase = true}.Object;
+
+ // Act
+ MediaTypeFormatter tracingFormatter = MediaTypeFormatterTracer.CreateTracer(formatter, new TestTraceWriter(), request);
+
+ // Assert
+ Assert.IsAssignableFrom<IFormatterTracer>(tracingFormatter);
+ Assert.IsAssignableFrom<BufferedMediaTypeFormatter>(tracingFormatter);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Tracing/Tracers/MessageHandlerTracerTest.cs b/test/System.Web.Http.Test/Tracing/Tracers/MessageHandlerTracerTest.cs
new file mode 100644
index 00000000..b5ce4cfb
--- /dev/null
+++ b/test/System.Web.Http.Test/Tracing/Tracers/MessageHandlerTracerTest.cs
@@ -0,0 +1,156 @@
+using System.Net.Http;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Tracing.Tracers
+{
+ public class MessageHandlerTracerTest
+ {
+ [Fact]
+ public void SendAsync_Traces_And_Invokes_Inner()
+ {
+ // Arrange
+ HttpResponseMessage response = new HttpResponseMessage();
+ MockDelegatingHandler mockHandler = new MockDelegatingHandler((rqst, cancellation) =>
+ TaskHelpers.FromResult<HttpResponseMessage>(response));
+
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ MessageHandlerTracer tracer = new MessageHandlerTracer(mockHandler, traceWriter);
+ MockHttpMessageHandler mockInnerHandler = new MockHttpMessageHandler((rqst, cancellation) =>
+ TaskHelpers.FromResult<HttpResponseMessage>(response));
+ tracer.InnerHandler = mockInnerHandler;
+
+ HttpRequestMessage request = new HttpRequestMessage();
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(request, TraceCategories.MessageHandlersCategory, TraceLevel.Info) { Kind = TraceKind.Begin, Operation = "SendAsync" },
+ new TraceRecord(request, TraceCategories.MessageHandlersCategory, TraceLevel.Info) { Kind = TraceKind.End, Operation = "SendAsync" }
+ };
+
+ MethodInfo method = typeof (DelegatingHandler).GetMethod("SendAsync",
+ BindingFlags.Public | BindingFlags.NonPublic |
+ BindingFlags.Instance);
+
+ // Act
+ Task<HttpResponseMessage> task = method.Invoke(tracer, new object[] {request, CancellationToken.None}) as Task<HttpResponseMessage>;
+ HttpResponseMessage actualResponse = task.Result;
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ Assert.Same(response, actualResponse);
+ }
+
+ [Fact]
+ public void SendAsync_Traces_And_Throws_When_Inner_Throws()
+ {
+ // Arrange
+ InvalidOperationException exception = new InvalidOperationException("test");
+ MockDelegatingHandler mockHandler = new MockDelegatingHandler((rqst, cancellation) => { throw exception; });
+
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ MessageHandlerTracer tracer = new MessageHandlerTracer(mockHandler, traceWriter);
+
+ // DelegatingHandlers require an InnerHandler to run. We create a mock one to simulate what
+ // would happen when a DelegatingHandler executing after the tracer throws.
+ MockHttpMessageHandler mockInnerHandler = new MockHttpMessageHandler((rqst, cancellation) => { throw exception; });
+ tracer.InnerHandler = mockInnerHandler;
+
+ HttpRequestMessage request = new HttpRequestMessage();
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(request, TraceCategories.MessageHandlersCategory, TraceLevel.Info) { Kind = TraceKind.Begin, Operation = "SendAsync" },
+ new TraceRecord(request, TraceCategories.MessageHandlersCategory, TraceLevel.Error) { Kind = TraceKind.End, Operation = "SendAsync" }
+ };
+
+ MethodInfo method = typeof(DelegatingHandler).GetMethod("SendAsync",
+ BindingFlags.Public | BindingFlags.NonPublic |
+ BindingFlags.Instance);
+
+ // Act
+ Exception thrown =
+ Assert.Throws<TargetInvocationException>(
+ () => method.Invoke(tracer, new object[] {request, CancellationToken.None}));
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ Assert.Same(exception, thrown.InnerException);
+ Assert.Same(exception, traceWriter.Traces[1].Exception);
+ }
+
+ [Fact]
+ public void SendAsync_Traces_And_Faults_When_Inner_Faults()
+ {
+ // Arrange
+ InvalidOperationException exception = new InvalidOperationException("test");
+ TaskCompletionSource<HttpResponseMessage> tcs = new TaskCompletionSource<HttpResponseMessage>();
+ tcs.TrySetException(exception);
+ MockDelegatingHandler mockHandler = new MockDelegatingHandler((rqst, cancellation) => { return tcs.Task; });
+
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ MessageHandlerTracer tracer = new MessageHandlerTracer(mockHandler, traceWriter);
+
+ // DelegatingHandlers require an InnerHandler to run. We create a mock one to simulate what
+ // would happen when a DelegatingHandler executing after the tracer returns a Task that throws.
+ MockHttpMessageHandler mockInnerHandler = new MockHttpMessageHandler((rqst, cancellation) => { return tcs.Task; });
+ tracer.InnerHandler = mockInnerHandler;
+
+ HttpRequestMessage request = new HttpRequestMessage();
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(request, TraceCategories.MessageHandlersCategory, TraceLevel.Info) { Kind = TraceKind.Begin, Operation = "SendAsync" },
+ new TraceRecord(request, TraceCategories.MessageHandlersCategory, TraceLevel.Error) { Kind = TraceKind.End, Operation = "SendAsync" }
+ };
+
+ MethodInfo method = typeof(DelegatingHandler).GetMethod("SendAsync",
+ BindingFlags.Public | BindingFlags.NonPublic |
+ BindingFlags.Instance);
+
+ // Act
+ Task<HttpResponseMessage> task =
+ method.Invoke(tracer, new object[] {request, CancellationToken.None}) as Task<HttpResponseMessage>;
+
+ // Assert
+ Exception thrown = Assert.Throws<InvalidOperationException>(() => task.Wait());
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ Assert.Same(exception, thrown);
+ Assert.Same(exception, traceWriter.Traces[1].Exception);
+ }
+
+
+ // DelegatingHandler cannot be mocked with Moq
+ private class MockDelegatingHandler : DelegatingHandler
+ {
+ private Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _callback;
+
+ public MockDelegatingHandler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> callback) : base()
+ {
+ _callback = callback;
+ }
+
+ protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ return _callback(request, cancellationToken);
+ }
+ }
+
+ // HttpMessageHandler cannot be mocked with Moq
+ private class MockHttpMessageHandler : HttpMessageHandler
+ {
+ private Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _callback;
+
+ public MockHttpMessageHandler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> callback)
+ : base()
+ {
+ _callback = callback;
+ }
+
+ protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ return _callback(request, cancellationToken);
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Tracing/Tracers/RequestMessageHandlerTracerTest.cs b/test/System.Web.Http.Test/Tracing/Tracers/RequestMessageHandlerTracerTest.cs
new file mode 100644
index 00000000..35ffaa25
--- /dev/null
+++ b/test/System.Web.Http.Test/Tracing/Tracers/RequestMessageHandlerTracerTest.cs
@@ -0,0 +1,150 @@
+using System.Net.Http;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Tracing.Tracers
+{
+ public class RequestMessageHandlerTracerTest
+ {
+ [Fact]
+ public void SendAsync_Traces_And_Invokes_Inner()
+ {
+ // Arrange
+ HttpResponseMessage response = new HttpResponseMessage();
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ RequestMessageHandlerTracer tracer = new RequestMessageHandlerTracer(traceWriter);
+ MockHttpMessageHandler mockInnerHandler = new MockHttpMessageHandler((rqst, cancellation) =>
+ TaskHelpers.FromResult<HttpResponseMessage>(response));
+ tracer.InnerHandler = mockInnerHandler;
+
+ HttpRequestMessage request = new HttpRequestMessage();
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(request, TraceCategories.RequestCategory, TraceLevel.Info) { Kind = TraceKind.Begin },
+ new TraceRecord(request, TraceCategories.RequestCategory, TraceLevel.Info) { Kind = TraceKind.End }
+ };
+
+ MethodInfo method = typeof(DelegatingHandler).GetMethod("SendAsync",
+ BindingFlags.Public | BindingFlags.NonPublic |
+ BindingFlags.Instance);
+
+ // Act
+ Task<HttpResponseMessage> task = method.Invoke(tracer, new object[] { request, CancellationToken.None }) as Task<HttpResponseMessage>;
+ HttpResponseMessage actualResponse = task.Result;
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ Assert.Same(response, actualResponse);
+ }
+
+ [Fact]
+ public void SendAsync_Traces_And_Throws_When_Inner_Throws()
+ {
+ // Arrange
+ InvalidOperationException exception = new InvalidOperationException("test");
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ RequestMessageHandlerTracer tracer = new RequestMessageHandlerTracer(traceWriter);
+
+ // DelegatingHandlers require an InnerHandler to run. We create a mock one to simulate what
+ // would happen when a DelegatingHandler executing after the tracer throws.
+ MockHttpMessageHandler mockInnerHandler = new MockHttpMessageHandler((rqst, cancellation) => { throw exception; });
+ tracer.InnerHandler = mockInnerHandler;
+
+ HttpRequestMessage request = new HttpRequestMessage();
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(request, TraceCategories.RequestCategory, TraceLevel.Info) { Kind = TraceKind.Begin },
+ new TraceRecord(request, TraceCategories.RequestCategory, TraceLevel.Error) { Kind = TraceKind.End }
+ };
+
+ MethodInfo method = typeof(DelegatingHandler).GetMethod("SendAsync",
+ BindingFlags.Public | BindingFlags.NonPublic |
+ BindingFlags.Instance);
+
+ // Act
+ Exception thrown =
+ Assert.Throws<TargetInvocationException>(
+ () => method.Invoke(tracer, new object[] { request, CancellationToken.None }));
+
+ // Assert
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ Assert.Same(exception, thrown.InnerException);
+ Assert.Same(exception, traceWriter.Traces[1].Exception);
+ }
+
+ [Fact]
+ public void SendAsync_Traces_And_Faults_When_Inner_Faults()
+ {
+ // Arrange
+ InvalidOperationException exception = new InvalidOperationException("test");
+ TaskCompletionSource<HttpResponseMessage> tcs = new TaskCompletionSource<HttpResponseMessage>();
+ tcs.TrySetException(exception);
+ TestTraceWriter traceWriter = new TestTraceWriter();
+ RequestMessageHandlerTracer tracer = new RequestMessageHandlerTracer(traceWriter);
+
+ // DelegatingHandlers require an InnerHandler to run. We create a mock one to simulate what
+ // would happen when a DelegatingHandler executing after the tracer returns a Task that throws.
+ MockHttpMessageHandler mockInnerHandler = new MockHttpMessageHandler((rqst, cancellation) => { return tcs.Task; });
+ tracer.InnerHandler = mockInnerHandler;
+
+ HttpRequestMessage request = new HttpRequestMessage();
+ TraceRecord[] expectedTraces = new TraceRecord[]
+ {
+ new TraceRecord(request, TraceCategories.RequestCategory, TraceLevel.Info) { Kind = TraceKind.Begin },
+ new TraceRecord(request, TraceCategories.RequestCategory, TraceLevel.Error) { Kind = TraceKind.End }
+ };
+
+ MethodInfo method = typeof(DelegatingHandler).GetMethod("SendAsync",
+ BindingFlags.Public | BindingFlags.NonPublic |
+ BindingFlags.Instance);
+
+ // Act
+ Task<HttpResponseMessage> task =
+ method.Invoke(tracer, new object[] { request, CancellationToken.None }) as Task<HttpResponseMessage>;
+
+ // Assert
+ Exception thrown = Assert.Throws<InvalidOperationException>(() => task.Wait());
+ Assert.Equal<TraceRecord>(expectedTraces, traceWriter.Traces, new TraceRecordComparer());
+ Assert.Same(exception, thrown);
+ Assert.Same(exception, traceWriter.Traces[1].Exception);
+ }
+
+
+ // DelegatingHandler cannot be mocked with Moq
+ private class MockDelegatingHandler : DelegatingHandler
+ {
+ private Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _callback;
+
+ public MockDelegatingHandler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> callback)
+ : base()
+ {
+ _callback = callback;
+ }
+
+ protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ return _callback(request, cancellationToken);
+ }
+ }
+
+ // HttpMessageHandler cannot be mocked with Moq
+ private class MockHttpMessageHandler : HttpMessageHandler
+ {
+ private Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _callback;
+
+ public MockHttpMessageHandler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> callback)
+ : base()
+ {
+ _callback = callback;
+ }
+
+ protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ return _callback(request, cancellationToken);
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Util/ContextUtil.cs b/test/System.Web.Http.Test/Util/ContextUtil.cs
new file mode 100644
index 00000000..e1cc7c68
--- /dev/null
+++ b/test/System.Web.Http.Test/Util/ContextUtil.cs
@@ -0,0 +1,48 @@
+using System.Net.Http;
+using System.Web.Http.Controllers;
+using System.Web.Http.Filters;
+using System.Web.Http.Routing;
+using Moq;
+
+namespace System.Web.Http
+{
+ internal static class ContextUtil
+ {
+ public static HttpControllerContext CreateControllerContext(HttpConfiguration configuration = null, IHttpController instance = null, IHttpRouteData routeData = null, HttpRequestMessage request = null)
+ {
+ HttpConfiguration config = configuration ?? new HttpConfiguration();
+ IHttpRouteData route = routeData ?? new HttpRouteData(new HttpRoute());
+ HttpRequestMessage req = request ?? new HttpRequestMessage();
+
+ HttpControllerContext context = new HttpControllerContext(config, route, req);
+ if (instance != null)
+ {
+ context.Controller = instance;
+ }
+
+ return context;
+ }
+
+ public static HttpActionContext CreateActionContext(HttpControllerContext controllerContext = null, HttpActionDescriptor actionDescriptor = null)
+ {
+ HttpControllerContext context = controllerContext ?? ContextUtil.CreateControllerContext();
+ HttpActionDescriptor descriptor = actionDescriptor ?? new Mock<HttpActionDescriptor>() { CallBase = true }.Object;
+ return new HttpActionContext(context, descriptor);
+ }
+
+ public static HttpActionContext GetHttpActionContext(HttpRequestMessage request)
+ {
+ HttpActionContext actionContext = CreateActionContext();
+ actionContext.ControllerContext.Request = request;
+ return actionContext;
+ }
+
+ public static HttpActionExecutedContext GetActionExecutedContext(HttpRequestMessage request, HttpResponseMessage response)
+ {
+ HttpActionContext actionContext = CreateActionContext();
+ actionContext.ControllerContext.Request = request;
+ HttpActionExecutedContext actionExecutedContext = new HttpActionExecutedContext(actionContext, null) { Result = response };
+ return actionExecutedContext;
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Util/SimpleHttpValueProvider.cs b/test/System.Web.Http.Test/Util/SimpleHttpValueProvider.cs
new file mode 100644
index 00000000..31c1ad8a
--- /dev/null
+++ b/test/System.Web.Http.Test/Util/SimpleHttpValueProvider.cs
@@ -0,0 +1,70 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.Web.Http.ValueProviders;
+
+namespace System.Web.Http.Util
+{
+ public sealed class SimpleHttpValueProvider : Dictionary<string, object>, IValueProvider
+ {
+ private readonly CultureInfo _culture;
+
+ public SimpleHttpValueProvider()
+ : this(null)
+ {
+ }
+
+ public SimpleHttpValueProvider(CultureInfo culture)
+ : base(StringComparer.OrdinalIgnoreCase)
+ {
+ _culture = culture ?? CultureInfo.InvariantCulture;
+ }
+
+ // copied from ValueProviderUtil
+ public bool ContainsPrefix(string prefix)
+ {
+ foreach (string key in Keys)
+ {
+ if (key != null)
+ {
+ if (prefix.Length == 0)
+ {
+ return true; // shortcut - non-null key matches empty prefix
+ }
+
+ if (key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+ {
+ if (key.Length == prefix.Length)
+ {
+ return true; // exact match
+ }
+ else
+ {
+ switch (key[prefix.Length])
+ {
+ case '.': // known separator characters
+ case '[':
+ return true;
+ }
+ }
+ }
+ }
+ }
+
+ return false; // nothing found
+ }
+
+ public ValueProviderResult GetValue(string key)
+ {
+ object rawValue;
+ if (TryGetValue(key, out rawValue))
+ {
+ return new ValueProviderResult(rawValue, Convert.ToString(rawValue, _culture), _culture);
+ }
+ else
+ {
+ // value not found
+ return null;
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Validation/DefaultBodyModelValidatorTest.cs b/test/System.Web.Http.Test/Validation/DefaultBodyModelValidatorTest.cs
new file mode 100644
index 00000000..9d8cfe4e
--- /dev/null
+++ b/test/System.Web.Http.Test/Validation/DefaultBodyModelValidatorTest.cs
@@ -0,0 +1,160 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Web.Http.Controllers;
+using System.Web.Http.Metadata;
+using System.Web.Http.Metadata.Providers;
+using System.Web.Http.ModelBinding;
+using Microsoft.TestCommon;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Validation
+{
+ public class DefaultBodyModelValidatorTest
+ {
+ private static Person LonelyPerson;
+
+ static DefaultBodyModelValidatorTest()
+ {
+ LonelyPerson = new Person() { Name = "Reallllllllly Long Name" };
+ LonelyPerson.Friend = LonelyPerson;
+ }
+
+ public static IEnumerable<object[]> ValidationErrors
+ {
+ get
+ {
+ return new TheoryDataSet<object, Type, Dictionary<string, string>>()
+ {
+ // Primitives
+ { null, typeof(Person), new Dictionary<string, string>() },
+ { 14, typeof(int), new Dictionary<string, string>() },
+ { "foo", typeof(string), new Dictionary<string, string>() },
+
+ // Classes
+ { new Person() { Name = "Rick", Profession = "Astronaut" }, typeof(Person), new Dictionary<string, string>() },
+ { new Person(), typeof(Person), new Dictionary<string, string>()
+ {
+ { "Name", "The Name field is required." },
+ { "Profession", "The Profession field is required." }
+ }
+ },
+
+ { new Person() { Name = "Rick", Friend = new Person() }, typeof(Person), new Dictionary<string, string>()
+ {
+ { "Profession", "The Profession field is required." },
+ { "Friend.Name", "The Name field is required." },
+ { "Friend.Profession", "The Profession field is required." }
+ }
+ },
+
+ // Collections
+ { new Person[] { new Person(), new Person() }, typeof(Person[]), new Dictionary<string, string>()
+ {
+ { "[0].Name", "The Name field is required." },
+ { "[0].Profession", "The Profession field is required." },
+ { "[1].Name", "The Name field is required." },
+ { "[1].Profession", "The Profession field is required." }
+ }
+ },
+
+ { new List<Person> { new Person(), new Person() }, typeof(Person[]), new Dictionary<string, string>()
+ {
+ { "[0].Name", "The Name field is required." },
+ { "[0].Profession", "The Profession field is required." },
+ { "[1].Name", "The Name field is required." },
+ { "[1].Profession", "The Profession field is required." }
+ }
+ },
+
+ { new Dictionary<string, Person> { { "Joe", new Person() } , { "Mark", new Person() } }, typeof(Dictionary<string, Person>), new Dictionary<string, string>()
+ {
+ { "[0].Value.Name", "The Name field is required." },
+ { "[0].Value.Profession", "The Profession field is required." },
+ { "[1].Value.Name", "The Name field is required." },
+ { "[1].Value.Profession", "The Profession field is required." }
+ }
+ },
+
+ // Testing we don't blow up on cycles
+ { LonelyPerson, typeof(Person), new Dictionary<string, string>()
+ {
+ { "Name", "The field Name must be a string with a maximum length of 10." },
+ { "Profession", "The Profession field is required." }
+ }
+ },
+ };
+ }
+ }
+
+ [Theory]
+ [PropertyData("ValidationErrors")]
+ public void ExpectedValidationErrorsRaised(object model, Type type, Dictionary<string, string> expectedErrors)
+ {
+ // Arrange
+ ModelMetadataProvider metadataProvider = new CachedDataAnnotationsModelMetadataProvider();
+ HttpActionContext actionContext = ContextUtil.CreateActionContext();
+
+ // Act
+ Assert.DoesNotThrow(() =>
+ new DefaultBodyModelValidator().Validate(model, type, metadataProvider, actionContext, string.Empty)
+ );
+
+ // Assert
+ Dictionary<string, string> actualErrors = new Dictionary<string, string>();
+ foreach (KeyValuePair<string, ModelState> keyStatePair in actionContext.ModelState)
+ {
+ foreach (ModelError error in keyStatePair.Value.Errors)
+ {
+ actualErrors.Add(keyStatePair.Key, error.ErrorMessage);
+ }
+ }
+
+ Assert.Equal(expectedErrors.Count, actualErrors.Count);
+ foreach (KeyValuePair<string, string> keyErrorPair in expectedErrors)
+ {
+ Assert.Contains(keyErrorPair.Key, actualErrors.Keys);
+ Assert.Equal(keyErrorPair.Value, actualErrors[keyErrorPair.Key]);
+ }
+ }
+
+ [Fact]
+ public void MultipleValidationErrorsOnSameMemberReported()
+ {
+ // Arrange
+ ModelMetadataProvider metadataProvider = new CachedDataAnnotationsModelMetadataProvider();
+ HttpActionContext actionContext = ContextUtil.CreateActionContext();
+ object model = new Address() { Street = "Microsoft Way" };
+
+ // Act
+ Assert.DoesNotThrow(() =>
+ new DefaultBodyModelValidator().Validate(model, typeof(Address), metadataProvider, actionContext, string.Empty)
+ );
+
+ // Assert
+ Assert.Contains("Street", actionContext.ModelState.Keys);
+ ModelState streetState = actionContext.ModelState["Street"];
+ Assert.Equal(2, streetState.Errors.Count);
+ }
+ }
+
+ public class Person
+ {
+ [Required]
+ [StringLength(10)]
+ public string Name { get; set; }
+
+ [Required]
+ public string Profession { get; set; }
+
+ public Person Friend { get; set; }
+ }
+
+ public class Address
+ {
+ [StringLength(5)]
+ [RegularExpression("hehehe")]
+ public string Street { get; set; }
+ }
+}
diff --git a/test/System.Web.Http.Test/Validation/ModelValidationNodeTest.cs b/test/System.Web.Http.Test/Validation/ModelValidationNodeTest.cs
new file mode 100644
index 00000000..9ad5d2d8
--- /dev/null
+++ b/test/System.Web.Http.Test/Validation/ModelValidationNodeTest.cs
@@ -0,0 +1,331 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Web.Http.Controllers;
+using System.Web.Http.Metadata;
+using System.Web.Http.Metadata.Providers;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Validation
+{
+ public class ModelValidationNodeTest
+ {
+ [Fact]
+ public void ConstructorSetsCollectionInstance()
+ {
+ // Arrange
+ ModelMetadata metadata = GetModelMetadata();
+ string modelStateKey = "someKey";
+ ModelValidationNode[] childNodes = new[]
+ {
+ new ModelValidationNode(metadata, "someKey0"),
+ new ModelValidationNode(metadata, "someKey1")
+ };
+
+ // Act
+ ModelValidationNode node = new ModelValidationNode(metadata, modelStateKey, childNodes);
+
+ // Assert
+ Assert.Equal(childNodes, node.ChildNodes.ToArray());
+ }
+
+ [Fact]
+ public void ConstructorThrowsIfModelMetadataIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ () => new ModelValidationNode(null, "someKey"),
+ "modelMetadata");
+ }
+
+ [Fact]
+ public void ConstructorThrowsIfModelStateKeyIsNull()
+ {
+ // Arrange
+ ModelMetadata metadata = GetModelMetadata();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ () => new ModelValidationNode(metadata, null),
+ "modelStateKey");
+ }
+
+ [Fact]
+ public void PropertiesAreSet()
+ {
+ // Arrange
+ ModelMetadata metadata = GetModelMetadata();
+ string modelStateKey = "someKey";
+
+ // Act
+ ModelValidationNode node = new ModelValidationNode(metadata, modelStateKey);
+
+ // Assert
+ Assert.Equal(metadata, node.ModelMetadata);
+ Assert.Equal(modelStateKey, node.ModelStateKey);
+ Assert.NotNull(node.ChildNodes);
+ Assert.Empty(node.ChildNodes);
+ }
+
+ [Fact]
+ public void CombineWith()
+ {
+ // Arrange
+ List<string> log = new List<string>();
+
+ ModelValidationNode[] allChildNodes = new[]
+ {
+ new ModelValidationNode(GetModelMetadata(), "key1"),
+ new ModelValidationNode(GetModelMetadata(), "key2"),
+ new ModelValidationNode(GetModelMetadata(), "key3"),
+ };
+
+ ModelValidationNode parentNode1 = new ModelValidationNode(GetModelMetadata(), "parent1");
+ parentNode1.ChildNodes.Add(allChildNodes[0]);
+ parentNode1.Validating += (sender, e) => log.Add("Validating parent1.");
+ parentNode1.Validated += (sender, e) => log.Add("Validated parent1.");
+
+ ModelValidationNode parentNode2 = new ModelValidationNode(GetModelMetadata(), "parent2");
+ parentNode2.ChildNodes.Add(allChildNodes[1]);
+ parentNode2.ChildNodes.Add(allChildNodes[2]);
+ parentNode2.Validating += (sender, e) => log.Add("Validating parent2.");
+ parentNode2.Validated += (sender, e) => log.Add("Validated parent2.");
+
+ // Act
+ parentNode1.CombineWith(parentNode2);
+ parentNode1.Validate(ContextUtil.CreateActionContext());
+
+ // Assert
+ Assert.Equal(new[] { "Validating parent1.", "Validating parent2.", "Validated parent1.", "Validated parent2." }, log.ToArray());
+ Assert.Equal(allChildNodes, parentNode1.ChildNodes.ToArray());
+ }
+
+ [Fact]
+ public void CombineWith_OtherNodeIsSuppressed_DoesNothing()
+ {
+ // Arrange
+ List<string> log = new List<string>();
+
+ ModelValidationNode[] allChildNodes = new[]
+ {
+ new ModelValidationNode(GetModelMetadata(), "key1"),
+ new ModelValidationNode(GetModelMetadata(), "key2"),
+ new ModelValidationNode(GetModelMetadata(), "key3"),
+ };
+
+ ModelValidationNode[] expectedChildNodes = new[]
+ {
+ allChildNodes[0]
+ };
+
+ ModelValidationNode parentNode1 = new ModelValidationNode(GetModelMetadata(), "parent1");
+ parentNode1.ChildNodes.Add(allChildNodes[0]);
+ parentNode1.Validating += (sender, e) => log.Add("Validating parent1.");
+ parentNode1.Validated += (sender, e) => log.Add("Validated parent1.");
+
+ ModelValidationNode parentNode2 = new ModelValidationNode(GetModelMetadata(), "parent2");
+ parentNode2.ChildNodes.Add(allChildNodes[1]);
+ parentNode2.ChildNodes.Add(allChildNodes[2]);
+ parentNode2.Validating += (sender, e) => log.Add("Validating parent2.");
+ parentNode2.Validated += (sender, e) => log.Add("Validated parent2.");
+ parentNode2.SuppressValidation = true;
+
+ // Act
+ parentNode1.CombineWith(parentNode2);
+ parentNode1.Validate(ContextUtil.CreateActionContext());
+
+ // Assert
+ Assert.Equal(new[] { "Validating parent1.", "Validated parent1." }, log.ToArray());
+ Assert.Equal(expectedChildNodes, parentNode1.ChildNodes.ToArray());
+ }
+
+ [Fact]
+ public void Validate_Ordering()
+ {
+ // Proper order of invocation:
+ // 1. OnValidating()
+ // 2. Child validators
+ // 3. This validator
+ // 4. OnValidated()
+
+ // Arrange
+ List<string> log = new List<string>();
+ LoggingValidatableObject model = new LoggingValidatableObject(log);
+ ModelMetadata modelMetadata = GetModelMetadata(model);
+ ModelMetadata childMetadata = new EmptyModelMetadataProvider().GetMetadataForProperty(() => model, model.GetType(), "ValidStringProperty");
+ ModelValidationNode node = new ModelValidationNode(modelMetadata, "theKey");
+ node.Validating += (sender, e) => log.Add("In OnValidating()");
+ node.Validated += (sender, e) => log.Add("In OnValidated()");
+ node.ChildNodes.Add(new ModelValidationNode(childMetadata, "theKey.ValidStringProperty"));
+
+ // Act
+ node.Validate(ContextUtil.CreateActionContext());
+
+ // Assert
+ Assert.Equal(new[] { "In OnValidating()", "In LoggingValidatonAttribute.IsValid()", "In IValidatableObject.Validate()", "In OnValidated()" }, log.ToArray());
+ }
+
+ [Fact]
+ public void Validate_SkipsRemainingValidationIfModelStateIsInvalid()
+ {
+ // Because a property validator fails, the model validator shouldn't run
+
+ // Arrange
+ List<string> log = new List<string>();
+ LoggingValidatableObject model = new LoggingValidatableObject(log);
+ ModelMetadata modelMetadata = GetModelMetadata(model);
+ ModelMetadata childMetadata = new EmptyModelMetadataProvider().GetMetadataForProperty(() => model, model.GetType(), "InvalidStringProperty");
+ ModelValidationNode node = new ModelValidationNode(modelMetadata, "theKey");
+ node.ChildNodes.Add(new ModelValidationNode(childMetadata, "theKey.InvalidStringProperty"));
+ node.Validating += (sender, e) => log.Add("In OnValidating()");
+ node.Validated += (sender, e) => log.Add("In OnValidated()");
+ HttpActionContext context = ContextUtil.CreateActionContext();
+
+ // Act
+ node.Validate(context);
+
+ // Assert
+ Assert.Equal(new[] { "In OnValidating()", "In IValidatableObject.Validate()", "In OnValidated()" }, log.ToArray());
+ Assert.Equal("Sample error message", context.ModelState["theKey.InvalidStringProperty"].Errors[0].ErrorMessage);
+ }
+
+ [Fact]
+ public void Validate_SkipsValidationIfHandlerCancels()
+ {
+ // Arrange
+ List<string> log = new List<string>();
+ LoggingValidatableObject model = new LoggingValidatableObject(log);
+ ModelMetadata modelMetadata = GetModelMetadata(model);
+ ModelValidationNode node = new ModelValidationNode(modelMetadata, "theKey");
+ node.Validating += (sender, e) =>
+ {
+ log.Add("In OnValidating()");
+ e.Cancel = true;
+ };
+ node.Validated += (sender, e) => log.Add("In OnValidated()");
+
+ // Act
+ node.Validate(ContextUtil.CreateActionContext());
+
+ // Assert
+ Assert.Equal(new[] { "In OnValidating()" }, log.ToArray());
+ }
+
+ [Fact]
+ public void Validate_SkipsValidationIfSuppressed()
+ {
+ // Arrange
+ List<string> log = new List<string>();
+ LoggingValidatableObject model = new LoggingValidatableObject(log);
+ ModelMetadata modelMetadata = GetModelMetadata(model);
+ ModelValidationNode node = new ModelValidationNode(modelMetadata, "theKey")
+ {
+ SuppressValidation = true
+ };
+
+ node.Validating += (sender, e) => log.Add("In OnValidating()");
+ node.Validated += (sender, e) => log.Add("In OnValidated()");
+
+ // Act
+ node.Validate(ContextUtil.CreateActionContext());
+
+ // Assert
+ Assert.Empty(log);
+ }
+
+ [Fact]
+ public void Validate_ThrowsIfControllerContextIsNull()
+ {
+ // Arrange
+ ModelValidationNode node = new ModelValidationNode(GetModelMetadata(), "someKey");
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ () => node.Validate(null),
+ "actionContext");
+ }
+
+ [Fact]
+ public void Validate_ValidateAllProperties_AddsValidationErrors()
+ {
+ // Arrange
+ ValidateAllPropertiesModel model = new ValidateAllPropertiesModel
+ {
+ RequiredString = null /* error */,
+ RangedInt = 0 /* error */,
+ ValidString = "dog"
+ };
+
+ ModelMetadata modelMetadata = GetModelMetadata(model);
+ HttpActionContext context = ContextUtil.CreateActionContext();
+ ModelValidationNode node = new ModelValidationNode(modelMetadata, "theKey")
+ {
+ ValidateAllProperties = true
+ };
+ context.ModelState.AddModelError("theKey.RequiredString.Dummy", "existing Error Text");
+
+ // Act
+ node.Validate(context);
+
+ // Assert
+ Assert.Null(context.ModelState["theKey.RequiredString"]);
+ Assert.Equal("existing Error Text", context.ModelState["theKey.RequiredString.Dummy"].Errors[0].ErrorMessage);
+ Assert.Equal("The field RangedInt must be between 10 and 30.", context.ModelState["theKey.RangedInt"].Errors[0].ErrorMessage);
+ Assert.Null(context.ModelState["theKey.ValidString"]);
+ Assert.Null(context.ModelState["theKey"]);
+ }
+
+ private static ModelMetadata GetModelMetadata()
+ {
+ return new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(object));
+ }
+
+ private static ModelMetadata GetModelMetadata(object o)
+ {
+ return new CachedDataAnnotationsModelMetadataProvider().GetMetadataForType(() => o, o.GetType());
+ }
+
+ private sealed class LoggingValidatableObject : IValidatableObject
+ {
+ private readonly IList<string> _log;
+
+ public LoggingValidatableObject(IList<string> log)
+ {
+ _log = log;
+ }
+
+ [LoggingValidation]
+ public string ValidStringProperty { get; set; }
+ public string InvalidStringProperty { get; set; }
+
+ public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
+ {
+ _log.Add("In IValidatableObject.Validate()");
+ yield return new ValidationResult("Sample error message", new[] { "InvalidStringProperty" });
+ }
+
+ private sealed class LoggingValidationAttribute : ValidationAttribute
+ {
+ protected override ValidationResult IsValid(object value, ValidationContext validationContext)
+ {
+ LoggingValidatableObject lvo = (LoggingValidatableObject)value;
+ lvo._log.Add("In LoggingValidatonAttribute.IsValid()");
+ return ValidationResult.Success;
+ }
+ }
+ }
+
+ private class ValidateAllPropertiesModel
+ {
+ [Required]
+ public string RequiredString { get; set; }
+
+ [Range(10, 30)]
+ public int RangedInt { get; set; }
+
+ [RegularExpression("dog")]
+ public string ValidString { get; set; }
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/Validation/ModelValidationRequiredMemberSelectorTest.cs b/test/System.Web.Http.Test/Validation/ModelValidationRequiredMemberSelectorTest.cs
new file mode 100644
index 00000000..70b8ef20
--- /dev/null
+++ b/test/System.Web.Http.Test/Validation/ModelValidationRequiredMemberSelectorTest.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.ComponentModel.DataAnnotations;
+using System.Runtime.Serialization;
+using System.Net.Http.Formatting;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.Validation
+{
+ public class ModelValidationRequiredMemberSelectorTest
+ {
+ [Theory]
+ [InlineData("Customer", true)]
+ [InlineData("ID", true)]
+ [InlineData("Item", true)]
+ [InlineData("UselessInfo", false)]
+ public void RequiredMembersRecognized(string propertyName, bool isRequired)
+ {
+ IRequiredMemberSelector selector = new ModelValidationRequiredMemberSelector(new HttpConfiguration());
+ Assert.Equal(isRequired, selector.IsRequiredMember(typeof(PurchaseOrder).GetProperty(propertyName)));
+ }
+ }
+
+ [DataContract]
+ public class PurchaseOrder
+ {
+ [Required]
+ public string Customer { get; set; }
+
+ [DataMember(IsRequired=true)]
+ public int ID { get; set; }
+
+ [Required]
+ [DataMember(IsRequired=true)]
+ public string Item { get; set; }
+
+ public string UselessInfo { get; set; }
+ }
+}
diff --git a/test/System.Web.Http.Test/ValueProviders/Providers/KeyValueModelValueProviderTest.cs b/test/System.Web.Http.Test/ValueProviders/Providers/KeyValueModelValueProviderTest.cs
new file mode 100644
index 00000000..5609b3d2
--- /dev/null
+++ b/test/System.Web.Http.Test/ValueProviders/Providers/KeyValueModelValueProviderTest.cs
@@ -0,0 +1,217 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.Net.Http.Formatting;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.ValueProviders.Providers
+{
+ public class KeyValueModelValueProviderTest
+ {
+ private readonly IKeyValueModel _keyValueModel;
+
+ public KeyValueModelValueProviderTest()
+ {
+ Dictionary<string, object> values = new Dictionary<string, object>();
+ values["foo"] = "fooValue";
+ values["int"] = 55;
+ values["bar.baz"] = "someOtherValue";
+
+ _keyValueModel = new KeyValueModel(values);
+ }
+
+ [Fact]
+ public void ContainsPrefix_GuardClauses()
+ {
+ // Arrange
+ var valueProvider = new KeyValueModelValueProvider(_keyValueModel, null);
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(() => valueProvider.ContainsPrefix(null), "prefix");
+ }
+
+ [Fact]
+ public void ContainsPrefix_WithEmptyCollection_ReturnsFalseForEmptyPrefix()
+ {
+ // Arrange
+ var valueProvider = new KeyValueModelValueProvider(new KeyValueModel(new Dictionary<string, object>()), null);
+
+ // Act
+ bool result = valueProvider.ContainsPrefix("");
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void ContainsPrefix_WithNonEmptyCollection_ReturnsTrueForEmptyPrefix()
+ {
+ // Arrange
+ var valueProvider = new KeyValueModelValueProvider(_keyValueModel, null);
+
+ // Act
+ bool result = valueProvider.ContainsPrefix("");
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void ContainsPrefix_WithNonEmptyCollection_ReturnsTrueForKnownPrefixes()
+ {
+ // Arrange
+ var valueProvider = new KeyValueModelValueProvider(_keyValueModel, null);
+
+ // Act & Assert
+ Assert.True(valueProvider.ContainsPrefix("foo"));
+ Assert.True(valueProvider.ContainsPrefix("bar"));
+ Assert.True(valueProvider.ContainsPrefix("bar.baz"));
+ }
+
+ [Fact]
+ public void ContainsPrefix_WithNonEmptyCollection_ReturnsFalseForUnknownPrefix()
+ {
+ // Arrange
+ var valueProvider = new KeyValueModelValueProvider(_keyValueModel, null);
+
+ // Act
+ bool result = valueProvider.ContainsPrefix("biff");
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void GetKeysFromPrefix_GuardClauses()
+ {
+ // Arrange
+ var valueProvider = new KeyValueModelValueProvider(_keyValueModel, null);
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(() => valueProvider.GetKeysFromPrefix(null), "prefix");
+ }
+
+ [Fact]
+ public void GetKeysFromPrefix_EmptyPrefix_ReturnsAllPrefixes()
+ {
+ // Arrange
+ var valueProvider = new KeyValueModelValueProvider(_keyValueModel, null);
+
+ // Act
+ IDictionary<string, string> result = valueProvider.GetKeysFromPrefix("");
+
+ // Assert
+ Assert.Equal(5, result.Count);
+ Assert.Equal("", result[""]);
+ Assert.Equal("foo", result["foo"]);
+ Assert.Equal("int", result["int"]);
+ Assert.Equal("bar", result["bar"]);
+ Assert.Equal("bar.baz", result["bar.baz"]);
+ }
+
+ [Fact]
+ public void GetKeysFromPrefix_UnknownPrefix_ReturnsEmptyDictionary()
+ {
+ // Arrange
+ var valueProvider = new KeyValueModelValueProvider(_keyValueModel, null);
+
+ // Act
+ IDictionary<string, string> result = valueProvider.GetKeysFromPrefix("abc");
+
+ // Assert
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void GetKeysFromPrefix_KnownPrefix_ReturnsMatchingItems()
+ {
+ // Arrange
+ var valueProvider = new KeyValueModelValueProvider(_keyValueModel, null);
+
+ // Act
+ IDictionary<string, string> result = valueProvider.GetKeysFromPrefix("bar");
+
+ // Assert
+ KeyValuePair<string, string> kvp = Assert.Single(result);
+ Assert.Equal("baz", kvp.Key);
+ Assert.Equal("bar.baz", kvp.Value);
+ }
+
+ [Fact]
+ public void GetValue_GuardClauses()
+ {
+ // Arrange
+ var valueProvider = new KeyValueModelValueProvider(_keyValueModel, null);
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(() => valueProvider.GetValue(null), "key"); ;
+ }
+
+ [Fact]
+ public void GetValue_SingleValue_String()
+ {
+ // Arrange
+ var culture = CultureInfo.GetCultureInfo("fr-FR");
+ var valueProvider = new KeyValueModelValueProvider(_keyValueModel, culture);
+
+ // Act
+ ValueProviderResult vpResult = valueProvider.GetValue("bar.baz");
+
+ // Assert
+ Assert.NotNull(vpResult);
+ Assert.Equal("someOtherValue", (string)vpResult.RawValue);
+ Assert.Equal("someOtherValue", vpResult.AttemptedValue);
+ Assert.Equal(culture, vpResult.Culture);
+ }
+
+ [Fact]
+ public void GetValue_SingleValue_Not_String()
+ {
+ // Arrange
+ var culture = CultureInfo.GetCultureInfo("fr-FR");
+ var valueProvider = new KeyValueModelValueProvider(_keyValueModel, culture);
+
+ // Act
+ ValueProviderResult vpResult = valueProvider.GetValue("int");
+
+ // Assert
+ Assert.NotNull(vpResult);
+ Assert.Equal(55, (int)vpResult.RawValue);
+ Assert.Equal("55", vpResult.AttemptedValue);
+ Assert.Equal(culture, vpResult.Culture);
+ }
+
+ [Fact]
+ public void GetValue_ReturnsNullIfKeyNotFound()
+ {
+ // Arrange
+ var valueProvider = new KeyValueModelValueProvider(_keyValueModel, null);
+
+ // Act
+ ValueProviderResult vpResult = valueProvider.GetValue("bar");
+
+ // Assert
+ Assert.Null(vpResult);
+ }
+
+ class KeyValueModel : IKeyValueModel
+ {
+ private Dictionary<string, object> _values;
+
+ public KeyValueModel(Dictionary<string, object> values)
+ {
+ _values = values;
+ }
+
+ public bool TryGetValue(string key, out object value)
+ {
+ return _values.TryGetValue(key, out value);
+ }
+
+ public IEnumerable<string> Keys
+ {
+ get { return _values.Keys; }
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/ValueProviders/Providers/NameValueCollectionValueProviderTest.cs b/test/System.Web.Http.Test/ValueProviders/Providers/NameValueCollectionValueProviderTest.cs
new file mode 100644
index 00000000..eeb89196
--- /dev/null
+++ b/test/System.Web.Http.Test/ValueProviders/Providers/NameValueCollectionValueProviderTest.cs
@@ -0,0 +1,210 @@
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Globalization;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.ValueProviders.Providers
+{
+ public class NameValueCollectionValueProviderTest
+ {
+ private static readonly NameValueCollection _backingStore = new NameValueCollection()
+ {
+ { "foo", "fooValue1" },
+ { "foo", "fooValue2" },
+ { "bar.baz", "someOtherValue" }
+ };
+
+ [Fact]
+ public void Constructor_GuardClauses()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ () => new NameValueCollectionValueProvider(values: null, culture: CultureInfo.InvariantCulture),
+ "values");
+
+ Assert.ThrowsArgumentNull(
+ () => new NameValueCollectionValueProvider(valuesFactory: null, culture: CultureInfo.InvariantCulture),
+ "valuesFactory");
+ }
+
+ [Fact]
+ public void ContainsPrefix_GuardClauses()
+ {
+ // Arrange
+ var valueProvider = new NameValueCollectionValueProvider(_backingStore, null);
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ () => valueProvider.ContainsPrefix(null),
+ "prefix");
+ }
+
+ [Fact]
+ public void ContainsPrefix_WithEmptyCollection_ReturnsFalseForEmptyPrefix()
+ {
+ // Arrange
+ var valueProvider = new NameValueCollectionValueProvider(new NameValueCollection(), null);
+
+ // Act
+ bool result = valueProvider.ContainsPrefix("");
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void ContainsPrefix_WithNonEmptyCollection_ReturnsTrueForEmptyPrefix()
+ {
+ // Arrange
+ var valueProvider = new NameValueCollectionValueProvider(_backingStore, null);
+
+ // Act
+ bool result = valueProvider.ContainsPrefix("");
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void ContainsPrefix_WithNonEmptyCollection_ReturnsTrueForKnownPrefixes()
+ {
+ // Arrange
+ var valueProvider = new NameValueCollectionValueProvider(_backingStore, null);
+
+ // Act & Assert
+ Assert.True(valueProvider.ContainsPrefix("foo"));
+ Assert.True(valueProvider.ContainsPrefix("bar"));
+ Assert.True(valueProvider.ContainsPrefix("bar.baz"));
+ }
+
+ [Fact]
+ public void ContainsPrefix_WithNonEmptyCollection_ReturnsFalseForUnknownPrefix()
+ {
+ // Arrange
+ var valueProvider = new NameValueCollectionValueProvider(_backingStore, null);
+
+ // Act
+ bool result = valueProvider.ContainsPrefix("biff");
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void GetKeysFromPrefix_GuardClauses()
+ {
+ // Arrange
+ var valueProvider = new NameValueCollectionValueProvider(_backingStore, null);
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ () => valueProvider.GetKeysFromPrefix(null),
+ "prefix");
+ }
+
+ [Fact]
+ public void GetKeysFromPrefix_EmptyPrefix_ReturnsAllPrefixes()
+ {
+ // Arrange
+ var valueProvider = new NameValueCollectionValueProvider(_backingStore, null);
+
+ // Act
+ IDictionary<string, string> result = valueProvider.GetKeysFromPrefix("");
+
+ // Assert
+ Assert.Equal(4, result.Count);
+ Assert.Equal("", result[""]);
+ Assert.Equal("foo", result["foo"]);
+ Assert.Equal("bar", result["bar"]);
+ Assert.Equal("bar.baz", result["bar.baz"]);
+ }
+
+ [Fact]
+ public void GetKeysFromPrefix_UnknownPrefix_ReturnsEmptyDictionary()
+ {
+ // Arrange
+ var valueProvider = new NameValueCollectionValueProvider(_backingStore, null);
+
+ // Act
+ IDictionary<string, string> result = valueProvider.GetKeysFromPrefix("abc");
+
+ // Assert
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void GetKeysFromPrefix_KnownPrefix_ReturnsMatchingItems()
+ {
+ // Arrange
+ var valueProvider = new NameValueCollectionValueProvider(_backingStore, null);
+
+ // Act
+ IDictionary<string, string> result = valueProvider.GetKeysFromPrefix("bar");
+
+ // Assert
+ KeyValuePair<string, string> kvp = Assert.Single(result);
+ Assert.Equal("baz", kvp.Key);
+ Assert.Equal("bar.baz", kvp.Value);
+ }
+
+ [Fact]
+ public void GetValue_GuardClauses()
+ {
+ // Arrange
+ var valueProvider = new NameValueCollectionValueProvider(_backingStore, null);
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ () => valueProvider.GetValue(null),
+ "key");
+ }
+
+ [Fact]
+ public void GetValue_SingleValue()
+ {
+ // Arrange
+ var culture = CultureInfo.GetCultureInfo("fr-FR");
+ var valueProvider = new NameValueCollectionValueProvider(_backingStore, culture);
+
+ // Act
+ ValueProviderResult vpResult = valueProvider.GetValue("bar.baz");
+
+ // Assert
+ Assert.NotNull(vpResult);
+ Assert.Equal(new[] { "someOtherValue" }, (string[])vpResult.RawValue);
+ Assert.Equal("someOtherValue", vpResult.AttemptedValue);
+ Assert.Equal(culture, vpResult.Culture);
+ }
+
+ [Fact]
+ public void GetValue_MultiValue()
+ {
+ // Arrange
+ var culture = CultureInfo.GetCultureInfo("fr-FR");
+ var valueProvider = new NameValueCollectionValueProvider(_backingStore, culture);
+
+ // Act
+ ValueProviderResult vpResult = valueProvider.GetValue("foo");
+
+ // Assert
+ Assert.NotNull(vpResult);
+ Assert.Equal(new[] { "fooValue1", "fooValue2" }, (string[])vpResult.RawValue);
+ Assert.Equal("fooValue1,fooValue2", vpResult.AttemptedValue);
+ Assert.Equal(culture, vpResult.Culture);
+ }
+
+ [Fact]
+ public void GetValue_ReturnsNullIfKeyNotFound()
+ {
+ // Arrange
+ var valueProvider = new NameValueCollectionValueProvider(_backingStore, null);
+
+ // Act
+ ValueProviderResult vpResult = valueProvider.GetValue("bar");
+
+ // Assert
+ Assert.Null(vpResult);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/ValueProviders/Providers/QueryStringValueProviderTest.cs b/test/System.Web.Http.Test/ValueProviders/Providers/QueryStringValueProviderTest.cs
new file mode 100644
index 00000000..d5150251
--- /dev/null
+++ b/test/System.Web.Http.Test/ValueProviders/Providers/QueryStringValueProviderTest.cs
@@ -0,0 +1,152 @@
+using System.Collections.Specialized;
+using Xunit;
+
+namespace System.Web.Http.ValueProviders.Providers
+{
+ public class QueryStringValueProviderTest
+ {
+ [Fact]
+ public void ParseQueryString_Null()
+ {
+ // Act
+ NameValueCollection result = QueryStringValueProvider.ParseQueryString(null);
+
+ // Assert
+ Assert.Equal(0, result.Count);
+ }
+
+ [Fact]
+ public void ParseQueryString_SingleNamelessValue()
+ {
+ // Arrange
+ Uri uri = new Uri("http://localhost/?value1");
+
+ // Act
+ NameValueCollection result = QueryStringValueProvider.ParseQueryString(uri);
+
+ // Assert
+ string key = Assert.Single(result) as string;
+ Assert.Equal("", key);
+ Assert.Equal("value1", result[key]);
+ }
+
+ [Fact]
+ public void ParseQueryString_SingleNamedValue()
+ {
+ // Arrange
+ Uri uri = new Uri("http://localhost/?key1=value1");
+
+ // Act
+ NameValueCollection result = QueryStringValueProvider.ParseQueryString(uri);
+
+ // Assert
+ string key = Assert.Single(result) as string;
+ Assert.Equal("key1", key);
+ Assert.Equal("value1", result[key]);
+ }
+
+ [Fact]
+ public void ParseQueryString_TwoNamedValues()
+ {
+ // Arrange
+ Uri uri = new Uri("http://localhost/?key1=value1&key2=value2");
+
+ // Act
+ NameValueCollection result = QueryStringValueProvider.ParseQueryString(uri);
+
+ // Assert
+ Assert.Equal(2, result.Count);
+ Assert.Equal("value1", result["key1"]);
+ Assert.Equal("value2", result["key2"]);
+ }
+
+ [Fact]
+ public void ParseQueryString_MixedNamedAndUnnamedValues()
+ {
+ // Arrange
+ Uri uri = new Uri("http://localhost/?key1=value1&value2");
+
+ // Act
+ NameValueCollection result = QueryStringValueProvider.ParseQueryString(uri);
+
+ // Assert
+ Assert.Equal(2, result.Count);
+ Assert.Equal("value1", result["key1"]);
+ Assert.Equal("value2", result[""]);
+ }
+
+ [Fact]
+ public void ParseQueryString_MultipleValuesForSingleName()
+ {
+ // Arrange
+ Uri uri = new Uri("http://localhost/?key1=value1&key1=value2");
+
+ // Act
+ NameValueCollection result = QueryStringValueProvider.ParseQueryString(uri);
+
+ // Assert
+ Assert.Equal("value1,value2", result["key1"]);
+ Assert.Equal(new[] { "value1", "value2" }, result.GetValues("key1"));
+ }
+
+ [Fact]
+ public void ParseQueryString_LeadingAmpersand()
+ {
+ // Arrange
+ Uri uri = new Uri("http://localhost/?&key1=value1");
+
+ // Act
+ NameValueCollection result = QueryStringValueProvider.ParseQueryString(uri);
+
+ // Assert
+ Assert.Equal(2, result.Count);
+ Assert.Equal("value1", result["key1"]);
+ Assert.Equal("", result[""]);
+ }
+
+ [Fact]
+ public void ParseQueryString_IntermediateDoubleAmpersand()
+ {
+ // Arrange
+ Uri uri = new Uri("http://localhost/?key1=value1&&key2=value2");
+
+ // Act
+ NameValueCollection result = QueryStringValueProvider.ParseQueryString(uri);
+
+ // Assert
+ Assert.Equal(3, result.Count);
+ Assert.Equal("value1", result["key1"]);
+ Assert.Equal("value2", result["key2"]);
+ Assert.Equal("", result[""]);
+ }
+
+ [Fact]
+ public void ParseQueryString_TrailingAmpersand()
+ {
+ // Arrange
+ Uri uri = new Uri("http://localhost/?key1=value1&");
+
+ // Act
+ NameValueCollection result = QueryStringValueProvider.ParseQueryString(uri);
+
+ // Assert
+ Assert.Equal(2, result.Count);
+ Assert.Equal("value1", result["key1"]);
+ Assert.Equal("", result[""]);
+ }
+
+ [Fact]
+ public void ParseQueryString_EncodedUrlValues()
+ {
+ // Arrange
+ Uri uri = new Uri("http://localhost/?key%31=value%31");
+
+ // Act
+ NameValueCollection result = QueryStringValueProvider.ParseQueryString(uri);
+
+ // Assert
+ Assert.Single(result);
+ Assert.Equal("value1", result["key1"]);
+ }
+ }
+}
diff --git a/test/System.Web.Http.Test/packages.config b/test/System.Web.Http.Test/packages.config
new file mode 100644
index 00000000..b9070f9e
--- /dev/null
+++ b/test/System.Web.Http.Test/packages.config
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="Microsoft.Net.Http" version="2.0.20302.1" />
+ <package id="Moq" version="4.0.10827" />
+ <package id="xunit" version="1.9.0.1566" />
+ <package id="xunit.extensions" version="1.9.0.1566" />
+</packages> \ No newline at end of file
diff --git a/test/System.Web.Http.WebHost.Test/HttpControllerHandlerTest.cs b/test/System.Web.Http.WebHost.Test/HttpControllerHandlerTest.cs
new file mode 100644
index 00000000..e289ac6c
--- /dev/null
+++ b/test/System.Web.Http.WebHost.Test/HttpControllerHandlerTest.cs
@@ -0,0 +1,85 @@
+using System.IO;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Threading.Tasks;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http.WebHost
+{
+ public class HttpControllerHandlerTest
+ {
+ [Fact]
+ public void ConvertResponse_IfResponseHasNoCacheControlDefined_SetsNoCacheCacheabilityOnAspNetResponse()
+ {
+ // Arrange
+ Mock<HttpContextBase> contextMock = new Mock<HttpContextBase>() { DefaultValue = DefaultValue.Mock };
+ HttpResponseMessage response = new HttpResponseMessage();
+ HttpRequestMessage request = new HttpRequestMessage();
+
+ // Act
+ HttpControllerHandler.ConvertResponse(contextMock.Object, response, request);
+
+ // Assert
+ contextMock.Verify(c => c.Response.Cache.SetCacheability(HttpCacheability.NoCache));
+ }
+
+ [Fact]
+ public void ConvertResponse_IfResponseHasCacheControlDefined_DoesNotSetCacheCacheabilityOnAspNetResponse()
+ {
+ // Arrange
+ Mock<HttpContextBase> contextMock = new Mock<HttpContextBase>() { DefaultValue = DefaultValue.Mock };
+ HttpResponseMessage response = new HttpResponseMessage();
+ HttpRequestMessage request = new HttpRequestMessage();
+ response.Headers.CacheControl = new CacheControlHeaderValue { Public = true };
+
+ // Act
+ HttpControllerHandler.ConvertResponse(contextMock.Object, response, request);
+
+ // Assert
+ contextMock.Verify(c => c.Response.Cache.SetCacheability(HttpCacheability.NoCache), Times.Never());
+ }
+
+ [Fact]
+ public Task ConvertResponse_DisposesRequestAndResponse()
+ {
+ // Arrange
+ Mock<HttpContextBase> contextMock = new Mock<HttpContextBase>() { DefaultValue = DefaultValue.Mock };
+ contextMock.SetupGet((hcb) => hcb.Response.OutputStream).Returns(Stream.Null);
+
+ HttpRequestMessage request = new HttpRequestMessage();
+ HttpResponseMessage response = new HttpResponseMessage();
+
+ // Act
+ return HttpControllerHandler.ConvertResponse(contextMock.Object, response, request).ContinueWith(
+ _ =>
+ {
+ // Assert
+ Assert.ThrowsObjectDisposed(() => request.Method = HttpMethod.Get, typeof(HttpRequestMessage).FullName);
+ Assert.ThrowsObjectDisposed(() => response.StatusCode = HttpStatusCode.OK, typeof(HttpResponseMessage).FullName);
+ });
+ }
+
+ [Fact]
+ public Task ConvertResponse_DisposesRequestAndResponseWithContent()
+ {
+ // Arrange
+ Mock<HttpContextBase> contextMock = new Mock<HttpContextBase>() { DefaultValue = DefaultValue.Mock };
+ contextMock.SetupGet((hcb) => hcb.Response.OutputStream).Returns(Stream.Null);
+
+ HttpRequestMessage request = new HttpRequestMessage() { Content = new StringContent("request") };
+ HttpResponseMessage response = new HttpResponseMessage() { Content = new StringContent("response") };
+
+ // Act
+ return HttpControllerHandler.ConvertResponse(contextMock.Object, response, request).ContinueWith(
+ _ =>
+ {
+ // Assert
+ Assert.ThrowsObjectDisposed(() => request.Method = HttpMethod.Get, typeof(HttpRequestMessage).FullName);
+ Assert.ThrowsObjectDisposed(() => response.StatusCode = HttpStatusCode.OK, typeof(HttpResponseMessage).FullName);
+ });
+ }
+ }
+}
diff --git a/test/System.Web.Http.WebHost.Test/Properties/AssemblyInfo.cs b/test/System.Web.Http.WebHost.Test/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..9155b469
--- /dev/null
+++ b/test/System.Web.Http.WebHost.Test/Properties/AssemblyInfo.cs
@@ -0,0 +1,3 @@
+using System;
+
+[assembly: CLSCompliant(false)]
diff --git a/test/System.Web.Http.WebHost.Test/RouteCollectionExtensionsTest.cs b/test/System.Web.Http.WebHost.Test/RouteCollectionExtensionsTest.cs
new file mode 100644
index 00000000..fa90c158
--- /dev/null
+++ b/test/System.Web.Http.WebHost.Test/RouteCollectionExtensionsTest.cs
@@ -0,0 +1,67 @@
+using System.Web.Routing;
+using Microsoft.TestCommon;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Http
+{
+ public class RouteCollectionExtensionsTest
+ {
+ [Fact]
+ public void IsCorrectType()
+ {
+ Assert.Type.HasProperties(typeof(RouteCollectionExtensions), TypeAssert.TypeProperties.IsStatic | TypeAssert.TypeProperties.IsPublicVisibleClass);
+ }
+
+ [Fact]
+ public void MapHttpRoute1ThrowsOnNullRouteCollection()
+ {
+ Assert.ThrowsArgumentNull(() => RouteCollectionExtensions.MapHttpRoute(null, "", "", null), "routes");
+ }
+
+ [Fact]
+ public void MapHttpRoute1CreatesRoute()
+ {
+ // Arrange
+ RouteCollection routes = new RouteCollection();
+ object defaults = new { d1 = "D1" };
+
+ // Act
+ Route route = routes.MapHttpRoute("name", "template", defaults);
+
+ // Assert
+ Assert.NotNull(route);
+ Assert.Equal("template", route.Url);
+ Assert.Equal(1, route.Defaults.Count);
+ Assert.Equal("D1", route.Defaults["d1"]);
+ Assert.Same(route, routes["name"]);
+ }
+
+ [Fact]
+ public void MapHttpRoute2ThrowsOnNullRouteCollection()
+ {
+ Assert.ThrowsArgumentNull(() => RouteCollectionExtensions.MapHttpRoute(null, "", "", null, null), "routes");
+ }
+
+ [Fact]
+ public void MapHttpRoute2CreatesRoute()
+ {
+ // Arrange
+ RouteCollection routes = new RouteCollection();
+ object defaults = new { d1 = "D1" };
+ object constraints = new { c1 = "C1" };
+
+ // Act
+ Route route = routes.MapHttpRoute("name", "template", defaults, constraints);
+
+ // Assert
+ Assert.NotNull(route);
+ Assert.Equal("template", route.Url);
+ Assert.Equal(1, route.Defaults.Count);
+ Assert.Equal("D1", route.Defaults["d1"]);
+ Assert.Equal(1, route.Defaults.Count);
+ Assert.Equal("C1", route.Constraints["c1"]);
+ Assert.Same(route, routes["name"]);
+ }
+ }
+}
diff --git a/test/System.Web.Http.WebHost.Test/Routing/HostedUrlHelperTest.cs b/test/System.Web.Http.WebHost.Test/Routing/HostedUrlHelperTest.cs
new file mode 100644
index 00000000..a652e1a0
--- /dev/null
+++ b/test/System.Web.Http.WebHost.Test/Routing/HostedUrlHelperTest.cs
@@ -0,0 +1,137 @@
+using System.Net.Http;
+using System.Web.Http.Controllers;
+using System.Web.Mvc;
+using System.Web.Routing;
+using Moq;
+using Xunit;
+using Xunit.Extensions;
+using UrlHelper = System.Web.Http.Routing.UrlHelper;
+
+namespace System.Web.Http.WebHost.Routing
+{
+ public class HostedUrlHelperTest
+ {
+ [Theory]
+ [InlineData(WhichRoute.ApiRoute1)]
+ [InlineData(WhichRoute.ApiRoute2)]
+ [InlineData(WhichRoute.WebRoute1)]
+ public void UrlHelper_GeneratesApiUrl_ForMatchingData(WhichRoute whichRoute)
+ {
+ // Mixed mode app with Web API generating URLs to other APIs
+ var url = GetUrlHelperForMixedApp(whichRoute);
+
+ string generatedUrl = url.Route("apiroute2", new { controller = "something", action = "someaction", id = 789 });
+
+ Assert.Equal("$APP$/SOMEAPP/api/something/someaction", generatedUrl);
+ }
+
+ [Theory]
+ [InlineData(WhichRoute.ApiRoute1)]
+ [InlineData(WhichRoute.ApiRoute2)]
+ [InlineData(WhichRoute.WebRoute1)]
+ public void UrlHelper_SkipsApiRoutesAndMatchesMvcUrl_ForMatchingData(WhichRoute whichRoute)
+ {
+ // Mixed mode app with MVC generating URLs to other MVC URLs
+ RouteCollection routes;
+ RequestContext requestContext;
+ var url = GetUrlHelperForMixedApp(whichRoute, out routes, out requestContext);
+
+ // Note: This is generating a URL the "hard" way because it's simulating what a regular MVC
+ // app would do when generating a URL. If we went through the Web API functionality it wouldn't
+ // be testing what would really happen in a mixed app.
+ VirtualPathData virtualPathData = routes.GetVirtualPath(requestContext, new RouteValueDictionary(new { controller = "something", action = "someaction", id = 789 }));
+
+ Assert.NotNull(virtualPathData);
+
+ string generatedUrl = virtualPathData.VirtualPath;
+
+ Assert.Equal("$APP$/SOMEAPP/something/someaction/789", generatedUrl);
+ }
+
+ [Theory]
+ [InlineData(WhichRoute.ApiRoute1)]
+ [InlineData(WhichRoute.ApiRoute2)]
+ [InlineData(WhichRoute.WebRoute1)]
+ public void UrlHelper_MvcAppGeneratesApiRoute_WithSpecialHttpRouteKey(WhichRoute whichRoute)
+ {
+ // Mixed mode app with MVC generating URLs to Web APIs
+ RouteCollection routes;
+ RequestContext requestContext;
+ var url = GetUrlHelperForMixedApp(whichRoute, out routes, out requestContext);
+
+ // Note: This is generating a URL the "hard" way because it's simulating what a regular MVC
+ // app would do when generating a URL. If we went through the Web API functionality it wouldn't
+ // be testing what would really happen in a mixed app.
+ VirtualPathData virtualPathData = routes.GetVirtualPath(requestContext, new RouteValueDictionary(new { controller = "something", action = "someotheraction", id = 789, httproute = true }));
+
+ Assert.NotNull(virtualPathData);
+
+ string generatedUrl = virtualPathData.VirtualPath;
+
+ Assert.Equal("$APP$/SOMEAPP/api/something/someotheraction", generatedUrl);
+ }
+
+ private static UrlHelper GetUrlHelperForMixedApp(WhichRoute whichRoute)
+ {
+ RouteCollection routes;
+ RequestContext requestContext;
+ return GetUrlHelperForMixedApp(whichRoute, out routes, out requestContext);
+ }
+
+ private static UrlHelper GetUrlHelperForMixedApp(WhichRoute whichRoute, out RouteCollection routes, out RequestContext requestContext)
+ {
+ routes = new RouteCollection();
+
+ HttpControllerContext cc = new HttpControllerContext();
+ cc.Request = new HttpRequestMessage();
+ var mockHttpContext = new Mock<HttpContextBase>();
+ var mockHttpRequest = new Mock<HttpRequestBase>();
+ mockHttpRequest.SetupGet<string>(x => x.ApplicationPath).Returns("/SOMEAPP/");
+ var mockHttpResponse = new Mock<HttpResponseBase>();
+ mockHttpResponse.Setup<string>(x => x.ApplyAppPathModifier(It.IsAny<string>())).Returns<string>(x => "$APP$" + x);
+ mockHttpContext.SetupGet<HttpRequestBase>(x => x.Request).Returns(mockHttpRequest.Object);
+ mockHttpContext.SetupGet<HttpResponseBase>(x => x.Response).Returns(mockHttpResponse.Object);
+ cc.Request.Properties["MS_HttpContext"] = mockHttpContext.Object;
+
+ // Set up routes
+ var hostedRoutes = new HostedHttpRouteCollection(routes);
+ Route apiRoute1 = routes.MapHttpRoute("apiroute1", "api/{controller}/{id}", new { action = "someaction" });
+ Route apiRoute2 = routes.MapHttpRoute("apiroute2", "api/{controller}/{action}", new { id = 789 });
+ Route webRoute1 = routes.MapRoute("webroute1", "{controller}/{action}/{id}");
+ cc.Configuration = new HttpConfiguration(hostedRoutes);
+
+ RouteData routeData = new RouteData();
+ routeData.Values.Add("controller", "people");
+ routeData.Values.Add("id", "123");
+
+ // Specify which route we came in on (e.g. what request matching the incoming URL) because
+ // it can affect the generated URL due to the ambient route data.
+ switch (whichRoute)
+ {
+ case WhichRoute.ApiRoute1:
+ routeData.Route = apiRoute1;
+ break;
+ case WhichRoute.ApiRoute2:
+ routeData.Route = apiRoute2;
+ break;
+ case WhichRoute.WebRoute1:
+ routeData.Route = webRoute1;
+ break;
+ default:
+ throw new ArgumentException("Invalid route specified.", "whichRoute");
+ }
+ cc.RouteData = new HostedHttpRouteData(routeData);
+
+ requestContext = new RequestContext(mockHttpContext.Object, routeData);
+
+ return cc.Url;
+ }
+
+ public enum WhichRoute
+ {
+ ApiRoute1,
+ ApiRoute2,
+ WebRoute1,
+ }
+ }
+}
diff --git a/test/System.Web.Http.WebHost.Test/System.Web.Http.WebHost.Test.csproj b/test/System.Web.Http.WebHost.Test/System.Web.Http.WebHost.Test.csproj
new file mode 100644
index 00000000..6e02ad94
--- /dev/null
+++ b/test/System.Web.Http.WebHost.Test/System.Web.Http.WebHost.Test.csproj
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProductVersion>9.0.30729</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{EA62944F-BD25-4730-9405-9BE8FF5BEACD}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>System.Web.Http.WebHost</RootNamespace>
+ <AssemblyName>System.Web.Http.WebHost.Test</AssemblyName>
+ <FileAlignment>512</FileAlignment>
+ <ProjectTypeGuids>{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Debug\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Release\Test\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'CodeCoverage' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\CodeCoverage\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="Moq, Version=4.0.10827.0, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL">
+ <HintPath>..\..\packages\Moq.4.0.10827\lib\NET40\Moq.dll</HintPath>
+ </Reference>
+ <Reference Include="System" />
+ <Reference Include="System.Configuration" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.Net.Http">
+ <HintPath>..\..\packages\Microsoft.Net.Http.2.0.20302.1\lib\net40\System.Net.Http.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Net.Http.WebRequest">
+ <HintPath>..\..\packages\Microsoft.Net.Http.2.0.20302.1\lib\net40\System.Net.Http.WebRequest.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Web" />
+ <Reference Include="xunit">
+ <HintPath>..\..\packages\xunit.1.9.0.1566\lib\xunit.dll</HintPath>
+ </Reference>
+ <Reference Include="xunit.extensions">
+ <HintPath>..\..\packages\xunit.extensions.1.9.0.1566\lib\xunit.extensions.dll</HintPath>
+ </Reference>
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="HttpControllerHandlerTest.cs" />
+ <Compile Include="RouteCollectionExtensionsTest.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="Routing\HostedUrlHelperTest.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="packages.config" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\System.Web.Http.WebHost\System.Web.Http.WebHost.csproj">
+ <Project>{A0187BC2-8325-4BB2-8697-7F955CF4173E}</Project>
+ <Name>System.Web.Http.WebHost</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\src\System.Web.Http\System.Web.Http.csproj">
+ <Project>{DDC1CE0C-486E-4E35-BB3B-EAB61F8F9440}</Project>
+ <Name>System.Web.Http</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\src\System.Web.Mvc\System.Web.Mvc.csproj">
+ <Project>{3D3FFD8A-624D-4E9B-954B-E1C105507975}</Project>
+ <Name>System.Web.Mvc</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\Microsoft.TestCommon\Microsoft.TestCommon.csproj">
+ <Project>{FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0}</Project>
+ <Name>Microsoft.TestCommon</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+</Project> \ No newline at end of file
diff --git a/test/System.Web.Http.WebHost.Test/packages.config b/test/System.Web.Http.WebHost.Test/packages.config
new file mode 100644
index 00000000..b9070f9e
--- /dev/null
+++ b/test/System.Web.Http.WebHost.Test/packages.config
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="Microsoft.Net.Http" version="2.0.20302.1" />
+ <package id="Moq" version="4.0.10827" />
+ <package id="xunit" version="1.9.0.1566" />
+ <package id="xunit.extensions" version="1.9.0.1566" />
+</packages> \ No newline at end of file
diff --git a/test/System.Web.Mvc.Test/Ajax/Test/AjaxExtensionsTest.cs b/test/System.Web.Mvc.Test/Ajax/Test/AjaxExtensionsTest.cs
new file mode 100644
index 00000000..f583cb1a
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Ajax/Test/AjaxExtensionsTest.cs
@@ -0,0 +1,1975 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Threading;
+using System.Web.Mvc.Html;
+using System.Web.Routing;
+using Microsoft.Web.UnitTestUtil;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Ajax.Test
+{
+ public class AjaxExtensionsTest
+ {
+ // Guards
+
+ [Fact]
+ public void ActionLinkWithNullOrEmptyLinkTextThrows()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { MvcHtmlString actionLink = ajaxHelper.ActionLink(String.Empty, String.Empty, null, null, null, null); },
+ "linkText");
+ }
+
+ [Fact]
+ public void RouteLinkWithNullOrEmptyLinkTextThrows()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { MvcHtmlString actionLink = ajaxHelper.RouteLink(String.Empty, String.Empty, null, null, null); },
+ "linkText");
+ }
+
+ // Form context setup and cleanup
+
+ [Fact]
+ public void BeginFormSetsAndRestoresToDefault()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper();
+
+ ajaxHelper.ViewContext.FormContext = null;
+ FormContext defaultFormContext = ajaxHelper.ViewContext.FormContext;
+
+ // Act & assert - push
+ MvcForm theForm = ajaxHelper.BeginForm(new AjaxOptions());
+ Assert.NotNull(ajaxHelper.ViewContext.FormContext);
+ Assert.NotEqual(defaultFormContext, ajaxHelper.ViewContext.FormContext);
+
+ // Act & assert - pop
+ theForm.Dispose();
+ Assert.Equal(defaultFormContext, ajaxHelper.ViewContext.FormContext);
+ }
+
+ [Fact]
+ public void DisposeWritesClosingFormTag()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper();
+ AjaxOptions ajaxOptions = new AjaxOptions { UpdateTargetId = "some-id" };
+ StringWriter writer = new StringWriter();
+ ajaxHelper.ViewContext.Writer = writer;
+
+ // Act
+ IDisposable form = ajaxHelper.BeginForm("Action", "Controller", ajaxOptions);
+ form.Dispose();
+
+ // Assert
+ Assert.True(writer.ToString().EndsWith("</form>"));
+ }
+
+ // GlobalizationScript
+
+ [Fact]
+ public void GlobalizationScriptWithNullCultureInfoThrows()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { ajaxHelper.GlobalizationScript(null); },
+ "cultureInfo");
+ }
+
+ [Fact]
+ public void GlobalizationScriptUsesCurrentCultureAsDefault()
+ {
+ CultureInfo currentCulture = Thread.CurrentThread.CurrentCulture;
+
+ try
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper();
+ AjaxHelper.GlobalizationScriptPath = null;
+ Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo("en-GB");
+
+ // Act
+ MvcHtmlString globalizationScript = ajaxHelper.GlobalizationScript();
+
+ // Assert
+ Assert.Equal(@"<script src=""~/Scripts/Globalization/en-GB.js"" type=""text/javascript""></script>", globalizationScript.ToHtmlString());
+ }
+ finally
+ {
+ Thread.CurrentThread.CurrentCulture = currentCulture;
+ }
+ }
+
+ [Fact]
+ public void GlobalizationScriptWithCultureInfo()
+ {
+ CultureInfo currentCulture = Thread.CurrentThread.CurrentCulture;
+
+ try
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper();
+ AjaxHelper.GlobalizationScriptPath = null;
+ Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo("en-GB");
+
+ // Act
+ MvcHtmlString globalizationScript = ajaxHelper.GlobalizationScript(CultureInfo.GetCultureInfo("en-CA"));
+
+ // Assert
+ Assert.Equal(@"<script src=""~/Scripts/Globalization/en-CA.js"" type=""text/javascript""></script>", globalizationScript.ToHtmlString());
+ }
+ finally
+ {
+ Thread.CurrentThread.CurrentCulture = currentCulture;
+ }
+ }
+
+ [Fact]
+ public void GlobalizationScriptEncodesSource()
+ {
+ // Arrange
+ Mock<CultureInfo> xssCulture = new Mock<CultureInfo>("en-US");
+ xssCulture.Setup(culture => culture.Name).Returns("evil.example.com/<script>alert('XSS!')</script>");
+ string globalizationPath = "~/Scripts&Globalization";
+ string expectedScriptTag = @"<script src=""~/Scripts&amp;Globalization/evil.example.com%2f%3cscript%3ealert(%27XSS!%27)%3c%2fscript%3e.js"" type=""text/javascript""></script>";
+
+ // Act
+ MvcHtmlString globalizationScript = AjaxExtensions.GlobalizationScriptHelper(globalizationPath, xssCulture.Object);
+
+ // Assert
+ Assert.Equal(expectedScriptTag, globalizationScript.ToHtmlString());
+ }
+
+ [Fact]
+ public void GlobalizationScriptWithNullCultureName()
+ {
+ // Arrange
+ Mock<CultureInfo> xssCulture = new Mock<CultureInfo>("en-US");
+ xssCulture.Setup(culture => culture.Name).Returns((string)null);
+
+ AjaxHelper ajaxHelper = GetAjaxHelper();
+ AjaxHelper.GlobalizationScriptPath = null;
+
+ // Act
+ MvcHtmlString globalizationScript = ajaxHelper.GlobalizationScript(xssCulture.Object);
+
+ // Assert
+ Assert.Equal(@"<script src=""~/Scripts/Globalization/.js"" type=""text/javascript""></script>", globalizationScript.ToHtmlString());
+ }
+
+ // ActionLink (traditional JavaScript)
+
+ [Fact]
+ public void ActionLinkWithNullActionName()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: false);
+
+ // Act
+ MvcHtmlString actionLink = ajaxHelper.ActionLink("linkText", null, new AjaxOptions());
+
+ // Assert
+ Assert.Equal(@"<a href=""" + MvcHelper.AppPathModifier + @"/app/home/oldaction"" onclick=""Sys.Mvc.AsyncHyperlink.handleClick(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace });"">linkText</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkWithNullActionName_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: true);
+
+ // Act
+ MvcHtmlString actionLink = ajaxHelper.ActionLink("linkText", null, new AjaxOptions());
+
+ // Assert
+ Assert.Equal(@"<a data-ajax=""true"" href=""" + MvcHelper.AppPathModifier + @"/app/home/oldaction"">linkText</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkWithNullActionNameAndNullOptions()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: false);
+
+ // Act
+ MvcHtmlString actionLink = ajaxHelper.ActionLink("linkText", null, null);
+
+ // Assert
+ Assert.Equal(@"<a href=""" + MvcHelper.AppPathModifier + @"/app/home/oldaction"" onclick=""Sys.Mvc.AsyncHyperlink.handleClick(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace });"">linkText</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkWithNullActionNameAndNullOptions_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: true);
+
+ // Act
+ MvcHtmlString actionLink = ajaxHelper.ActionLink("linkText", null, null);
+
+ // Assert
+ Assert.Equal(@"<a data-ajax=""true"" href=""" + MvcHelper.AppPathModifier + @"/app/home/oldaction"">linkText</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLink()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: false);
+
+ // Act
+ MvcHtmlString actionLink = ajaxHelper.ActionLink("linkText", "Action", new AjaxOptions());
+
+ // Assert
+ Assert.Equal(@"<a href=""" + MvcHelper.AppPathModifier + @"/app/home/Action"" onclick=""Sys.Mvc.AsyncHyperlink.handleClick(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace });"">linkText</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLink_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: true);
+
+ // Act
+ MvcHtmlString actionLink = ajaxHelper.ActionLink("linkText", "Action", new AjaxOptions());
+
+ // Assert
+ Assert.Equal(@"<a data-ajax=""true"" href=""" + MvcHelper.AppPathModifier + @"/app/home/Action"">linkText</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkAnonymousValues()
+ {
+ // Arrange
+ AjaxHelper helper = GetAjaxHelper(unobtrusiveJavaScript: false);
+ object values = new { controller = "Controller" };
+ AjaxOptions options = new AjaxOptions { UpdateTargetId = "update-div" };
+
+ // Act
+ MvcHtmlString actionLink = helper.ActionLink("Some Text", "Action", values, options);
+
+ // Assert
+ Assert.Equal(@"<a href=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action"" onclick=""Sys.Mvc.AsyncHyperlink.handleClick(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace, updateTargetId: &#39;update-div&#39; });"">Some Text</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkAnonymousValues_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper helper = GetAjaxHelper(unobtrusiveJavaScript: true);
+ object values = new { controller = "Controller" };
+ AjaxOptions options = new AjaxOptions { UpdateTargetId = "update-div" };
+
+ // Act
+ MvcHtmlString actionLink = helper.ActionLink("Some Text", "Action", values, options);
+
+ // Assert
+ Assert.Equal(@"<a data-ajax=""true"" data-ajax-mode=""replace"" data-ajax-update=""#update-div"" href=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action"">Some Text</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkAnonymousValuesAndAttributes()
+ {
+ // Arrange
+ AjaxHelper helper = GetAjaxHelper(unobtrusiveJavaScript: false);
+ object htmlAttributes = new { foo = "bar", baz = "quux", foo_bar = "baz_quux" };
+ object values = new { controller = "Controller" };
+ AjaxOptions options = new AjaxOptions { UpdateTargetId = "update-div" };
+
+ // Act
+ MvcHtmlString actionLink = helper.ActionLink("Some Text", "Action", values, options, htmlAttributes);
+
+ // Assert
+ Assert.Equal(@"<a baz=""quux"" foo=""bar"" foo-bar=""baz_quux"" href=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action"" onclick=""Sys.Mvc.AsyncHyperlink.handleClick(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace, updateTargetId: &#39;update-div&#39; });"">Some Text</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkAnonymousValuesAndAttributes_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper helper = GetAjaxHelper(unobtrusiveJavaScript: true);
+ object htmlAttributes = new { foo = "bar", baz = "quux", foo_bar = "baz_quux" };
+ object values = new { controller = "Controller" };
+ AjaxOptions options = new AjaxOptions { UpdateTargetId = "update-div" };
+
+ // Act
+ MvcHtmlString actionLink = helper.ActionLink("Some Text", "Action", values, options, htmlAttributes);
+
+ // Assert
+ Assert.Equal(@"<a baz=""quux"" data-ajax=""true"" data-ajax-mode=""replace"" data-ajax-update=""#update-div"" foo=""bar"" foo-bar=""baz_quux"" href=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action"">Some Text</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkTypedValues()
+ {
+ // Arrange
+ AjaxHelper helper = GetAjaxHelper(unobtrusiveJavaScript: false);
+ RouteValueDictionary values = new RouteValueDictionary
+ {
+ { "controller", "Controller" }
+ };
+
+ AjaxOptions options = new AjaxOptions { UpdateTargetId = "update-div" };
+
+ // Act
+ MvcHtmlString actionLink = helper.ActionLink("Some Text", "Action", values, options);
+
+ // Assert
+ Assert.Equal(@"<a href=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action"" onclick=""Sys.Mvc.AsyncHyperlink.handleClick(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace, updateTargetId: &#39;update-div&#39; });"">Some Text</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkTypedValues_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper helper = GetAjaxHelper(unobtrusiveJavaScript: true);
+ RouteValueDictionary values = new RouteValueDictionary
+ {
+ { "controller", "Controller" }
+ };
+
+ AjaxOptions options = new AjaxOptions { UpdateTargetId = "update-div" };
+
+ // Act
+ MvcHtmlString actionLink = helper.ActionLink("Some Text", "Action", values, options);
+
+ // Assert
+ Assert.Equal(@"<a data-ajax=""true"" data-ajax-mode=""replace"" data-ajax-update=""#update-div"" href=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action"">Some Text</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkTypedValuesAndAttributes()
+ {
+ // Arrange
+ AjaxHelper helper = GetAjaxHelper(unobtrusiveJavaScript: false);
+ RouteValueDictionary values = new RouteValueDictionary
+ {
+ { "controller", "Controller" }
+ };
+ Dictionary<string, object> htmlAttributes = new Dictionary<string, object>
+ {
+ { "foo", "bar" },
+ { "baz", "quux" },
+ { "foo_bar", "baz_quux" }
+ };
+ AjaxOptions options = new AjaxOptions { UpdateTargetId = "update-div" };
+
+ // Act
+ MvcHtmlString actionLink = helper.ActionLink("Some Text", "Action", values, options, htmlAttributes);
+
+ // Assert
+ Assert.Equal(@"<a baz=""quux"" foo=""bar"" foo_bar=""baz_quux"" href=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action"" onclick=""Sys.Mvc.AsyncHyperlink.handleClick(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace, updateTargetId: &#39;update-div&#39; });"">Some Text</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkTypedValuesAndAttributes_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper helper = GetAjaxHelper(unobtrusiveJavaScript: true);
+ RouteValueDictionary values = new RouteValueDictionary
+ {
+ { "controller", "Controller" }
+ };
+ Dictionary<string, object> htmlAttributes = new Dictionary<string, object>
+ {
+ { "foo", "bar" },
+ { "baz", "quux" },
+ { "foo_bar", "baz_quux" }
+ };
+ AjaxOptions options = new AjaxOptions { UpdateTargetId = "update-div" };
+
+ // Act
+ MvcHtmlString actionLink = helper.ActionLink("Some Text", "Action", values, options, htmlAttributes);
+
+ // Assert
+ Assert.Equal(@"<a baz=""quux"" data-ajax=""true"" data-ajax-mode=""replace"" data-ajax-update=""#update-div"" foo=""bar"" foo_bar=""baz_quux"" href=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action"">Some Text</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkController()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: false);
+
+ // Act
+ MvcHtmlString actionLink = ajaxHelper.ActionLink("linkText", "Action", "Controller", new AjaxOptions());
+
+ // Assert
+ Assert.Equal(@"<a href=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action"" onclick=""Sys.Mvc.AsyncHyperlink.handleClick(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace });"">linkText</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkController_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: true);
+
+ // Act
+ MvcHtmlString actionLink = ajaxHelper.ActionLink("linkText", "Action", "Controller", new AjaxOptions());
+
+ // Assert
+ Assert.Equal(@"<a data-ajax=""true"" href=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action"">linkText</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkControllerAnonymousValues()
+ {
+ // Arrange
+ AjaxHelper helper = GetAjaxHelper(unobtrusiveJavaScript: false);
+ object values = new { id = 5 };
+ AjaxOptions options = new AjaxOptions { UpdateTargetId = "update-div" };
+
+ // Act
+ MvcHtmlString actionLink = helper.ActionLink("Some Text", "Action", "Controller", values, options);
+
+ // Assert
+ Assert.Equal(@"<a href=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action/5"" onclick=""Sys.Mvc.AsyncHyperlink.handleClick(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace, updateTargetId: &#39;update-div&#39; });"">Some Text</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkControllerAnonymousValues_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper helper = GetAjaxHelper(unobtrusiveJavaScript: true);
+ object values = new { id = 5 };
+ AjaxOptions options = new AjaxOptions { UpdateTargetId = "update-div" };
+
+ // Act
+ MvcHtmlString actionLink = helper.ActionLink("Some Text", "Action", "Controller", values, options);
+
+ // Assert
+ Assert.Equal(@"<a data-ajax=""true"" data-ajax-mode=""replace"" data-ajax-update=""#update-div"" href=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action/5"">Some Text</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkControllerAnonymousValuesAndAttributes()
+ {
+ // Arrange
+ AjaxHelper helper = GetAjaxHelper(unobtrusiveJavaScript: false);
+ object htmlAttributes = new { foo = "bar", baz = "quux", foo_bar = "baz_quux" };
+ object values = new { id = 5 };
+ AjaxOptions options = new AjaxOptions { UpdateTargetId = "update-div" };
+
+ // Act
+ MvcHtmlString actionLink = helper.ActionLink("Some Text", "Action", "Controller", values, options, htmlAttributes);
+
+ // Assert
+ Assert.Equal(@"<a baz=""quux"" foo=""bar"" foo-bar=""baz_quux"" href=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action/5"" onclick=""Sys.Mvc.AsyncHyperlink.handleClick(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace, updateTargetId: &#39;update-div&#39; });"">Some Text</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkControllerAnonymousValuesAndAttributes_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper helper = GetAjaxHelper(unobtrusiveJavaScript: true);
+ object htmlAttributes = new { foo = "bar", baz = "quux", foo_bar = "baz_quux" };
+ object values = new { id = 5 };
+ AjaxOptions options = new AjaxOptions { UpdateTargetId = "update-div" };
+
+ // Act
+ MvcHtmlString actionLink = helper.ActionLink("Some Text", "Action", "Controller", values, options, htmlAttributes);
+
+ // Assert
+ Assert.Equal(@"<a baz=""quux"" data-ajax=""true"" data-ajax-mode=""replace"" data-ajax-update=""#update-div"" foo=""bar"" foo-bar=""baz_quux"" href=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action/5"">Some Text</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkControllerTypedValues()
+ {
+ // Arrange
+ AjaxHelper helper = GetAjaxHelper(unobtrusiveJavaScript: false);
+ RouteValueDictionary values = new RouteValueDictionary
+ {
+ { "id", 5 }
+ };
+ AjaxOptions options = new AjaxOptions { UpdateTargetId = "update-div" };
+
+ // Act
+ MvcHtmlString actionLink = helper.ActionLink("Some Text", "Action", "Controller", values, options);
+
+ // Assert
+ Assert.Equal(@"<a href=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action/5"" onclick=""Sys.Mvc.AsyncHyperlink.handleClick(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace, updateTargetId: &#39;update-div&#39; });"">Some Text</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkControllerTypedValues_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper helper = GetAjaxHelper(unobtrusiveJavaScript: true);
+ RouteValueDictionary values = new RouteValueDictionary
+ {
+ { "id", 5 }
+ };
+ AjaxOptions options = new AjaxOptions { UpdateTargetId = "update-div" };
+
+ // Act
+ MvcHtmlString actionLink = helper.ActionLink("Some Text", "Action", "Controller", values, options);
+
+ // Assert
+ Assert.Equal(@"<a data-ajax=""true"" data-ajax-mode=""replace"" data-ajax-update=""#update-div"" href=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action/5"">Some Text</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkControllerTypedValuesAndAttributes()
+ {
+ // Arrange
+ AjaxHelper helper = GetAjaxHelper(unobtrusiveJavaScript: false);
+ RouteValueDictionary values = new RouteValueDictionary
+ {
+ { "id", 5 }
+ };
+ Dictionary<string, object> htmlAttributes = new Dictionary<string, object>
+ {
+ { "foo", "bar" },
+ { "baz", "quux" },
+ { "foo_bar", "baz_quux" }
+ };
+ AjaxOptions options = new AjaxOptions { UpdateTargetId = "update-div" };
+
+ // Act
+ MvcHtmlString actionLink = helper.ActionLink("Some Text", "Action", "Controller", values, options, htmlAttributes);
+
+ // Assert
+ Assert.Equal(@"<a baz=""quux"" foo=""bar"" foo_bar=""baz_quux"" href=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action/5"" onclick=""Sys.Mvc.AsyncHyperlink.handleClick(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace, updateTargetId: &#39;update-div&#39; });"">Some Text</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkControllerTypedValuesAndAttributes_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper helper = GetAjaxHelper(unobtrusiveJavaScript: true);
+ RouteValueDictionary values = new RouteValueDictionary
+ {
+ { "id", 5 }
+ };
+ Dictionary<string, object> htmlAttributes = new Dictionary<string, object>
+ {
+ { "foo", "bar" },
+ { "baz", "quux" },
+ { "foo_bar", "baz_quux" }
+ };
+ AjaxOptions options = new AjaxOptions { UpdateTargetId = "update-div" };
+
+ // Act
+ MvcHtmlString actionLink = helper.ActionLink("Some Text", "Action", "Controller", values, options, htmlAttributes);
+
+ // Assert
+ Assert.Equal(@"<a baz=""quux"" data-ajax=""true"" data-ajax-mode=""replace"" data-ajax-update=""#update-div"" foo=""bar"" foo_bar=""baz_quux"" href=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action/5"">Some Text</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkWithOptions()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: false);
+
+ // Act
+ MvcHtmlString actionLink = ajaxHelper.ActionLink("linkText", "Action", "Controller", new AjaxOptions { UpdateTargetId = "some-id" });
+
+ // Assert
+ Assert.Equal(@"<a href=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action"" onclick=""Sys.Mvc.AsyncHyperlink.handleClick(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace, updateTargetId: &#39;some-id&#39; });"">linkText</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkWithOptions_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: true);
+ AjaxOptions options = new AjaxOptions { UpdateTargetId = "some-id" };
+
+ // Act
+ MvcHtmlString actionLink = ajaxHelper.ActionLink("linkText", "Action", "Controller", options);
+
+ // Assert
+ Assert.Equal(@"<a data-ajax=""true"" data-ajax-mode=""replace"" data-ajax-update=""#some-id"" href=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action"">linkText</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkWithNullHostName()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: false);
+
+ // Act
+ MvcHtmlString actionLink = ajaxHelper.ActionLink("linkText", "Action", "Controller",
+ null, null, null, null, new AjaxOptions { UpdateTargetId = "some-id" }, null);
+
+ // Assert
+ Assert.Equal(@"<a href=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action"" onclick=""Sys.Mvc.AsyncHyperlink.handleClick(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace, updateTargetId: &#39;some-id&#39; });"">linkText</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkWithNullHostName_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: true);
+
+ // Act
+ MvcHtmlString actionLink = ajaxHelper.ActionLink("linkText", "Action", "Controller",
+ null, null, null, null, new AjaxOptions { UpdateTargetId = "some-id" }, null);
+
+ // Assert
+ Assert.Equal(@"<a data-ajax=""true"" data-ajax-mode=""replace"" data-ajax-update=""#some-id"" href=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action"">linkText</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkWithProtocol()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: false);
+
+ // Act
+ MvcHtmlString actionLink = ajaxHelper.ActionLink("linkText", "Action", "Controller", "https", null, null, null, new AjaxOptions { UpdateTargetId = "some-id" }, null);
+
+ // Assert
+ Assert.Equal(@"<a href=""https://foo.bar.baz" + MvcHelper.AppPathModifier + @"/app/Controller/Action"" onclick=""Sys.Mvc.AsyncHyperlink.handleClick(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace, updateTargetId: &#39;some-id&#39; });"">linkText</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkWithProtocol_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: true);
+
+ // Act
+ MvcHtmlString actionLink = ajaxHelper.ActionLink("linkText", "Action", "Controller", "https", null, null, null, new AjaxOptions { UpdateTargetId = "some-id" }, null);
+
+ // Assert
+ Assert.Equal(@"<a data-ajax=""true"" data-ajax-mode=""replace"" data-ajax-update=""#some-id"" href=""https://foo.bar.baz" + MvcHelper.AppPathModifier + @"/app/Controller/Action"">linkText</a>", actionLink.ToHtmlString());
+ }
+
+ // RouteLink
+
+ [Fact]
+ public void RouteLinkWithNullOptions()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: false);
+
+ // Act
+ MvcHtmlString routeLink = ajaxHelper.RouteLink("Some Text", new RouteValueDictionary(), null);
+
+ // Assert
+ Assert.Equal(@"<a href=""" + MvcHelper.AppPathModifier + @"/app/home/oldaction"" onclick=""Sys.Mvc.AsyncHyperlink.handleClick(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace });"">Some Text</a>", routeLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void RouteLinkWithNullOptions_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: true);
+
+ // Act
+ MvcHtmlString routeLink = ajaxHelper.RouteLink("Some Text", new RouteValueDictionary(), null);
+
+ // Assert
+ Assert.Equal(@"<a data-ajax=""true"" href=""" + MvcHelper.AppPathModifier + @"/app/home/oldaction"">Some Text</a>", routeLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void RouteLinkAnonymousValues()
+ {
+ // Arrange
+ AjaxHelper helper = GetAjaxHelper(unobtrusiveJavaScript: false);
+ object values = new
+ {
+ action = "Action",
+ controller = "Controller"
+ };
+ AjaxOptions options = new AjaxOptions { UpdateTargetId = "update-div" };
+
+ // Act
+ MvcHtmlString routeLink = helper.RouteLink("Some Text", values, options);
+
+ // Assert
+ Assert.Equal(@"<a href=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action"" onclick=""Sys.Mvc.AsyncHyperlink.handleClick(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace, updateTargetId: &#39;update-div&#39; });"">Some Text</a>", routeLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void RouteLinkAnonymousValues_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper helper = GetAjaxHelper(unobtrusiveJavaScript: true);
+ object values = new
+ {
+ action = "Action",
+ controller = "Controller"
+ };
+ AjaxOptions options = new AjaxOptions { UpdateTargetId = "update-div" };
+
+ // Act
+ MvcHtmlString routeLink = helper.RouteLink("Some Text", values, options);
+
+ // Assert
+ Assert.Equal(@"<a data-ajax=""true"" data-ajax-mode=""replace"" data-ajax-update=""#update-div"" href=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action"">Some Text</a>", routeLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void RouteLinkAnonymousValuesAndAttributes()
+ {
+ // Arrange
+ AjaxHelper helper = GetAjaxHelper(unobtrusiveJavaScript: false);
+ object htmlAttributes = new
+ {
+ foo = "bar",
+ baz = "quux",
+ foo_bar = "baz_quux"
+ };
+ object values = new
+ {
+ action = "Action",
+ controller = "Controller"
+ };
+ AjaxOptions options = new AjaxOptions { UpdateTargetId = "update-div" };
+
+ // Act
+ MvcHtmlString actionLink = helper.RouteLink("Some Text", values, options, htmlAttributes);
+
+ // Assert
+ Assert.Equal(@"<a baz=""quux"" foo=""bar"" foo-bar=""baz_quux"" href=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action"" onclick=""Sys.Mvc.AsyncHyperlink.handleClick(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace, updateTargetId: &#39;update-div&#39; });"">Some Text</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void RouteLinkAnonymousValuesAndAttributes_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper helper = GetAjaxHelper(unobtrusiveJavaScript: true);
+ object htmlAttributes = new
+ {
+ foo = "bar",
+ baz = "quux",
+ foo_bar = "baz_quux"
+ };
+ object values = new
+ {
+ action = "Action",
+ controller = "Controller"
+ };
+ AjaxOptions options = new AjaxOptions { UpdateTargetId = "update-div" };
+
+ // Act
+ MvcHtmlString actionLink = helper.RouteLink("Some Text", values, options, htmlAttributes);
+
+ // Assert
+ Assert.Equal(@"<a baz=""quux"" data-ajax=""true"" data-ajax-mode=""replace"" data-ajax-update=""#update-div"" foo=""bar"" foo-bar=""baz_quux"" href=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action"">Some Text</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void RouteLinkTypedValues()
+ {
+ // Arrange
+ AjaxHelper helper = GetAjaxHelper(unobtrusiveJavaScript: false);
+ RouteValueDictionary values = new RouteValueDictionary
+ {
+ { "controller", "Controller" },
+ { "action", "Action" }
+ };
+
+ AjaxOptions options = new AjaxOptions { UpdateTargetId = "update-div" };
+
+ // Act
+ MvcHtmlString actionLink = helper.RouteLink("Some Text", values, options);
+
+ // Assert
+ Assert.Equal(@"<a href=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action"" onclick=""Sys.Mvc.AsyncHyperlink.handleClick(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace, updateTargetId: &#39;update-div&#39; });"">Some Text</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void RouteLinkTypedValues_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper helper = GetAjaxHelper(unobtrusiveJavaScript: true);
+ RouteValueDictionary values = new RouteValueDictionary
+ {
+ { "controller", "Controller" },
+ { "action", "Action" }
+ };
+
+ AjaxOptions options = new AjaxOptions { UpdateTargetId = "update-div" };
+
+ // Act
+ MvcHtmlString actionLink = helper.RouteLink("Some Text", values, options);
+
+ // Assert
+ Assert.Equal(@"<a data-ajax=""true"" data-ajax-mode=""replace"" data-ajax-update=""#update-div"" href=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action"">Some Text</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void RouteLinkTypedValuesAndAttributes()
+ {
+ // Arrange
+ AjaxHelper helper = GetAjaxHelper(unobtrusiveJavaScript: false);
+ RouteValueDictionary values = new RouteValueDictionary
+ {
+ { "controller", "Controller" },
+ { "action", "Action" }
+ };
+ Dictionary<string, object> htmlAttributes = new Dictionary<string, object>
+ {
+ { "foo", "bar" },
+ { "baz", "quux" },
+ { "foo_bar", "baz_quux" }
+ };
+ AjaxOptions options = new AjaxOptions { UpdateTargetId = "update-div" };
+
+ // Act
+ MvcHtmlString actionLink = helper.RouteLink("Some Text", values, options, htmlAttributes);
+
+ // Assert
+ Assert.Equal(@"<a baz=""quux"" foo=""bar"" foo_bar=""baz_quux"" href=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action"" onclick=""Sys.Mvc.AsyncHyperlink.handleClick(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace, updateTargetId: &#39;update-div&#39; });"">Some Text</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void RouteLinkTypedValuesAndAttributes_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper helper = GetAjaxHelper(unobtrusiveJavaScript: true);
+ RouteValueDictionary values = new RouteValueDictionary
+ {
+ { "controller", "Controller" },
+ { "action", "Action" }
+ };
+ Dictionary<string, object> htmlAttributes = new Dictionary<string, object>
+ {
+ { "foo", "bar" },
+ { "baz", "quux" },
+ { "foo_bar", "baz_quux" }
+ };
+ AjaxOptions options = new AjaxOptions { UpdateTargetId = "update-div" };
+
+ // Act
+ MvcHtmlString actionLink = helper.RouteLink("Some Text", values, options, htmlAttributes);
+
+ // Assert
+ Assert.Equal(@"<a baz=""quux"" data-ajax=""true"" data-ajax-mode=""replace"" data-ajax-update=""#update-div"" foo=""bar"" foo_bar=""baz_quux"" href=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action"">Some Text</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void RouteLinkNamedRoute()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: false);
+
+ // Act
+ MvcHtmlString actionLink = ajaxHelper.RouteLink("linkText", "namedroute", new AjaxOptions());
+
+ // Assert
+ Assert.Equal(@"<a href=""" + MvcHelper.AppPathModifier + @"/app/named/home/oldaction"" onclick=""Sys.Mvc.AsyncHyperlink.handleClick(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace });"">linkText</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void RouteLinkNamedRoute_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: true);
+
+ // Act
+ MvcHtmlString actionLink = ajaxHelper.RouteLink("linkText", "namedroute", new AjaxOptions());
+
+ // Assert
+ Assert.Equal(@"<a data-ajax=""true"" href=""" + MvcHelper.AppPathModifier + @"/app/named/home/oldaction"">linkText</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void RouteLinkNamedRouteAnonymousAttributes()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: false);
+ object htmlAttributes = new
+ {
+ foo = "bar",
+ baz = "quux",
+ foo_bar = "baz_quux"
+ };
+ AjaxOptions options = new AjaxOptions { UpdateTargetId = "update-div" };
+
+ // Act
+ MvcHtmlString actionLink = ajaxHelper.RouteLink("Some Text", "namedroute", options, htmlAttributes);
+
+ // Assert
+ Assert.Equal(@"<a baz=""quux"" foo=""bar"" foo-bar=""baz_quux"" href=""" + MvcHelper.AppPathModifier + @"/app/named/home/oldaction"" onclick=""Sys.Mvc.AsyncHyperlink.handleClick(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace, updateTargetId: &#39;update-div&#39; });"">Some Text</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void RouteLinkNamedRouteAnonymousAttributes_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: true);
+ object htmlAttributes = new
+ {
+ foo = "bar",
+ baz = "quux",
+ foo_bar = "baz_quux"
+ };
+ AjaxOptions options = new AjaxOptions { UpdateTargetId = "update-div" };
+
+ // Act
+ MvcHtmlString actionLink = ajaxHelper.RouteLink("Some Text", "namedroute", options, htmlAttributes);
+
+ // Assert
+ Assert.Equal(@"<a baz=""quux"" data-ajax=""true"" data-ajax-mode=""replace"" data-ajax-update=""#update-div"" foo=""bar"" foo-bar=""baz_quux"" href=""" + MvcHelper.AppPathModifier + @"/app/named/home/oldaction"">Some Text</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void RouteLinkNamedRouteTypedAttributes()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: false);
+ Dictionary<string, object> htmlAttributes = new Dictionary<string, object> { { "foo", "bar" }, { "baz", "quux" }, { "foo_bar", "baz_quux" } };
+ AjaxOptions options = new AjaxOptions { UpdateTargetId = "update-div" };
+
+ // Act
+ MvcHtmlString actionLink = ajaxHelper.RouteLink("Some Text", "namedroute", options, htmlAttributes);
+
+ // Assert
+ Assert.Equal(@"<a baz=""quux"" foo=""bar"" foo_bar=""baz_quux"" href=""" + MvcHelper.AppPathModifier + @"/app/named/home/oldaction"" onclick=""Sys.Mvc.AsyncHyperlink.handleClick(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace, updateTargetId: &#39;update-div&#39; });"">Some Text</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void RouteLinkNamedRouteTypedAttributes_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: true);
+ Dictionary<string, object> htmlAttributes = new Dictionary<string, object> { { "foo", "bar" }, { "baz", "quux" }, { "foo_bar", "baz_quux" } };
+ AjaxOptions options = new AjaxOptions { UpdateTargetId = "update-div" };
+
+ // Act
+ MvcHtmlString actionLink = ajaxHelper.RouteLink("Some Text", "namedroute", options, htmlAttributes);
+
+ // Assert
+ Assert.Equal(@"<a baz=""quux"" data-ajax=""true"" data-ajax-mode=""replace"" data-ajax-update=""#update-div"" foo=""bar"" foo_bar=""baz_quux"" href=""" + MvcHelper.AppPathModifier + @"/app/named/home/oldaction"">Some Text</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void RouteLinkNamedRouteWithAnonymousValues()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: false);
+ object values = new
+ {
+ action = "Action",
+ controller = "Controller"
+ };
+
+ // Act
+ MvcHtmlString actionLink = ajaxHelper.RouteLink("linkText", "namedroute", values, new AjaxOptions());
+
+ // Assert
+ Assert.Equal(@"<a href=""" + MvcHelper.AppPathModifier + @"/app/named/Controller/Action"" onclick=""Sys.Mvc.AsyncHyperlink.handleClick(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace });"">linkText</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void RouteLinkNamedRouteWithAnonymousValues_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: true);
+ object values = new
+ {
+ action = "Action",
+ controller = "Controller"
+ };
+
+ // Act
+ MvcHtmlString actionLink = ajaxHelper.RouteLink("linkText", "namedroute", values, new AjaxOptions());
+
+ // Assert
+ Assert.Equal(@"<a data-ajax=""true"" href=""" + MvcHelper.AppPathModifier + @"/app/named/Controller/Action"">linkText</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void RouteLinkNamedRouteAnonymousValuesAndAttributes()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: false);
+ object values = new
+ {
+ action = "Action",
+ controller = "Controller"
+ };
+
+ object htmlAttributes = new
+ {
+ foo = "bar",
+ baz = "quux",
+ foo_bar = "baz_quux"
+ };
+ AjaxOptions options = new AjaxOptions { UpdateTargetId = "update-div" };
+
+ // Act
+ MvcHtmlString actionLink = ajaxHelper.RouteLink("Some Text", "namedroute", values, options, htmlAttributes);
+
+ // Assert
+ Assert.Equal(@"<a baz=""quux"" foo=""bar"" foo-bar=""baz_quux"" href=""" + MvcHelper.AppPathModifier + @"/app/named/Controller/Action"" onclick=""Sys.Mvc.AsyncHyperlink.handleClick(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace, updateTargetId: &#39;update-div&#39; });"">Some Text</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void RouteLinkNamedRouteAnonymousValuesAndAttributes_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: true);
+ object values = new
+ {
+ action = "Action",
+ controller = "Controller"
+ };
+
+ object htmlAttributes = new
+ {
+ foo = "bar",
+ baz = "quux",
+ foo_bar = "baz_quux"
+ };
+ AjaxOptions options = new AjaxOptions { UpdateTargetId = "update-div" };
+
+ // Act
+ MvcHtmlString actionLink = ajaxHelper.RouteLink("Some Text", "namedroute", values, options, htmlAttributes);
+
+ // Assert
+ Assert.Equal(@"<a baz=""quux"" data-ajax=""true"" data-ajax-mode=""replace"" data-ajax-update=""#update-div"" foo=""bar"" foo-bar=""baz_quux"" href=""" + MvcHelper.AppPathModifier + @"/app/named/Controller/Action"">Some Text</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void RouteLinkNamedRouteWithTypedValues()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: false);
+ RouteValueDictionary values = new RouteValueDictionary
+ {
+ { "controller", "Controller" },
+ { "action", "Action" }
+ };
+
+ // Act
+ MvcHtmlString actionLink = ajaxHelper.RouteLink("linkText", "namedroute", values, new AjaxOptions());
+
+ // Assert
+ Assert.Equal(@"<a href=""" + MvcHelper.AppPathModifier + @"/app/named/Controller/Action"" onclick=""Sys.Mvc.AsyncHyperlink.handleClick(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace });"">linkText</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void RouteLinkNamedRouteWithTypedValues_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: true);
+ RouteValueDictionary values = new RouteValueDictionary
+ {
+ { "controller", "Controller" },
+ { "action", "Action" }
+ };
+
+ // Act
+ MvcHtmlString actionLink = ajaxHelper.RouteLink("linkText", "namedroute", values, new AjaxOptions());
+
+ // Assert
+ Assert.Equal(@"<a data-ajax=""true"" href=""" + MvcHelper.AppPathModifier + @"/app/named/Controller/Action"">linkText</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void RouteLinkNamedRouteTypedValuesAndAttributes()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: false);
+ RouteValueDictionary values = new RouteValueDictionary
+ {
+ { "controller", "Controller" },
+ { "action", "Action" }
+ };
+
+ Dictionary<string, object> htmlAttributes = new Dictionary<string, object> { { "foo", "bar" }, { "baz", "quux" }, { "foo_bar", "baz_quux" } };
+ AjaxOptions options = new AjaxOptions { UpdateTargetId = "update-div" };
+
+ // Act
+ MvcHtmlString actionLink = ajaxHelper.RouteLink("Some Text", "namedroute", values, options, htmlAttributes);
+
+ // Assert
+ Assert.Equal(@"<a baz=""quux"" foo=""bar"" foo_bar=""baz_quux"" href=""" + MvcHelper.AppPathModifier + @"/app/named/Controller/Action"" onclick=""Sys.Mvc.AsyncHyperlink.handleClick(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace, updateTargetId: &#39;update-div&#39; });"">Some Text</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void RouteLinkNamedRouteTypedValuesAndAttributes_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: true);
+ RouteValueDictionary values = new RouteValueDictionary
+ {
+ { "controller", "Controller" },
+ { "action", "Action" }
+ };
+
+ Dictionary<string, object> htmlAttributes = new Dictionary<string, object> { { "foo", "bar" }, { "baz", "quux" }, { "foo_bar", "baz_quux" } };
+ AjaxOptions options = new AjaxOptions { UpdateTargetId = "update-div" };
+
+ // Act
+ MvcHtmlString actionLink = ajaxHelper.RouteLink("Some Text", "namedroute", values, options, htmlAttributes);
+
+ // Assert
+ Assert.Equal(@"<a baz=""quux"" data-ajax=""true"" data-ajax-mode=""replace"" data-ajax-update=""#update-div"" foo=""bar"" foo_bar=""baz_quux"" href=""" + MvcHelper.AppPathModifier + @"/app/named/Controller/Action"">Some Text</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void RouteLinkNamedRouteNullValuesAndAttributes()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: false);
+ Dictionary<string, object> htmlAttributes = new Dictionary<string, object> { { "foo", "bar" }, { "baz", "quux" }, { "foo_bar", "baz_quux" } };
+ AjaxOptions options = new AjaxOptions { UpdateTargetId = "update-div" };
+
+ // Act
+ MvcHtmlString actionLink = ajaxHelper.RouteLink("Some Text", "namedroute", null, options, htmlAttributes);
+
+ // Assert
+ Assert.Equal(@"<a baz=""quux"" foo=""bar"" foo_bar=""baz_quux"" href=""" + MvcHelper.AppPathModifier + @"/app/named/home/oldaction"" onclick=""Sys.Mvc.AsyncHyperlink.handleClick(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace, updateTargetId: &#39;update-div&#39; });"">Some Text</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void RouteLinkNamedRouteNullValuesAndAttributes_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: true);
+ Dictionary<string, object> htmlAttributes = new Dictionary<string, object> { { "foo", "bar" }, { "baz", "quux" }, { "foo_bar", "baz_quux" } };
+ AjaxOptions options = new AjaxOptions { UpdateTargetId = "update-div" };
+
+ // Act
+ MvcHtmlString actionLink = ajaxHelper.RouteLink("Some Text", "namedroute", null, options, htmlAttributes);
+
+ // Assert
+ Assert.Equal(@"<a baz=""quux"" data-ajax=""true"" data-ajax-mode=""replace"" data-ajax-update=""#update-div"" foo=""bar"" foo_bar=""baz_quux"" href=""" + MvcHelper.AppPathModifier + @"/app/named/home/oldaction"">Some Text</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void RouteLinkWithHostName()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: false);
+ Dictionary<string, object> htmlAttributes = new Dictionary<string, object> { { "foo", "bar" }, { "baz", "quux" }, { "foo_bar", "baz_quux" } };
+ AjaxOptions options = new AjaxOptions { UpdateTargetId = "update-div" };
+
+ // Act
+ MvcHtmlString actionLink = ajaxHelper.RouteLink("Some Text", "namedroute", null, "baz.bar.foo", null, null, options, htmlAttributes);
+
+ // Assert
+ Assert.Equal(@"<a baz=""quux"" foo=""bar"" foo_bar=""baz_quux"" href=""http://baz.bar.foo" + MvcHelper.AppPathModifier + @"/app/named/home/oldaction"" onclick=""Sys.Mvc.AsyncHyperlink.handleClick(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace, updateTargetId: &#39;update-div&#39; });"">Some Text</a>", actionLink.ToHtmlString());
+ }
+
+ [Fact]
+ public void RouteLinkWithHostName_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: true);
+ Dictionary<string, object> htmlAttributes = new Dictionary<string, object> { { "foo", "bar" }, { "baz", "quux" }, { "foo_bar", "baz_quux" } };
+ AjaxOptions options = new AjaxOptions { UpdateTargetId = "update-div" };
+
+ // Act
+ MvcHtmlString actionLink = ajaxHelper.RouteLink("Some Text", "namedroute", null, "baz.bar.foo", null, null, options, htmlAttributes);
+
+ // Assert
+ Assert.Equal(@"<a baz=""quux"" data-ajax=""true"" data-ajax-mode=""replace"" data-ajax-update=""#update-div"" foo=""bar"" foo_bar=""baz_quux"" href=""http://baz.bar.foo" + MvcHelper.AppPathModifier + @"/app/named/home/oldaction"">Some Text</a>", actionLink.ToHtmlString());
+ }
+
+ // BeginForm
+
+ [Fact]
+ public void BeginFormOnlyWithNullOptions()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: false);
+ StringWriter writer = new StringWriter();
+ ajaxHelper.ViewContext.Writer = writer;
+
+ // Act
+ IDisposable form = ajaxHelper.BeginForm(null);
+
+ // Assert
+ Assert.Equal(@"<form action=""/rawUrl"" method=""post"" onclick=""Sys.Mvc.AsyncForm.handleClick(this, new Sys.UI.DomEvent(event));"" onsubmit=""Sys.Mvc.AsyncForm.handleSubmit(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace });"">", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginFormOnlyWithNullOptions_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: true);
+ StringWriter writer = new StringWriter();
+ ajaxHelper.ViewContext.Writer = writer;
+
+ // Act
+ IDisposable form = ajaxHelper.BeginForm(null);
+
+ // Assert
+ Assert.Equal(@"<form action=""/rawUrl"" data-ajax=""true"" method=""post"">", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginFormWithNullActionName()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: false);
+ AjaxOptions ajaxOptions = new AjaxOptions();
+ StringWriter writer = new StringWriter();
+ ajaxHelper.ViewContext.Writer = writer;
+
+ // Act
+ IDisposable form = ajaxHelper.BeginForm(null, ajaxOptions);
+
+ // Assert
+ Assert.Equal(@"<form action=""" + MvcHelper.AppPathModifier + @"/app/home/oldaction"" method=""post"" onclick=""Sys.Mvc.AsyncForm.handleClick(this, new Sys.UI.DomEvent(event));"" onsubmit=""Sys.Mvc.AsyncForm.handleSubmit(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace });"">", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginFormWithNullActionName_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: true);
+ AjaxOptions ajaxOptions = new AjaxOptions();
+ StringWriter writer = new StringWriter();
+ ajaxHelper.ViewContext.Writer = writer;
+
+ // Act
+ IDisposable form = ajaxHelper.BeginForm(null, ajaxOptions);
+
+ // Assert
+ Assert.Equal(@"<form action=""" + MvcHelper.AppPathModifier + @"/app/home/oldaction"" data-ajax=""true"" method=""post"">", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginFormWithNullOptions()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: false);
+ AjaxOptions ajaxOptions = new AjaxOptions();
+ StringWriter writer = new StringWriter();
+ ajaxHelper.ViewContext.Writer = writer;
+
+ // Act
+ IDisposable form = ajaxHelper.BeginForm("Action", "Controller", null);
+
+ // Assert
+ Assert.Equal(@"<form action=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action"" method=""post"" onclick=""Sys.Mvc.AsyncForm.handleClick(this, new Sys.UI.DomEvent(event));"" onsubmit=""Sys.Mvc.AsyncForm.handleSubmit(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace });"">", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginFormWithNullOptions_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: true);
+ AjaxOptions ajaxOptions = new AjaxOptions();
+ StringWriter writer = new StringWriter();
+ ajaxHelper.ViewContext.Writer = writer;
+
+ // Act
+ IDisposable form = ajaxHelper.BeginForm("Action", "Controller", null);
+
+ // Assert
+ Assert.Equal(@"<form action=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action"" data-ajax=""true"" method=""post"">", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginForm()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: false);
+ AjaxOptions ajaxOptions = new AjaxOptions();
+ StringWriter writer = new StringWriter();
+ ajaxHelper.ViewContext.Writer = writer;
+
+ // Act
+ IDisposable form = ajaxHelper.BeginForm(ajaxOptions);
+
+ // Assert
+ Assert.Equal(@"<form action=""/rawUrl"" method=""post"" onclick=""Sys.Mvc.AsyncForm.handleClick(this, new Sys.UI.DomEvent(event));"" onsubmit=""Sys.Mvc.AsyncForm.handleSubmit(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace });"">", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginForm_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: true);
+ AjaxOptions ajaxOptions = new AjaxOptions();
+ StringWriter writer = new StringWriter();
+ ajaxHelper.ViewContext.Writer = writer;
+
+ // Act
+ IDisposable form = ajaxHelper.BeginForm(ajaxOptions);
+
+ // Assert
+ Assert.Equal(@"<form action=""/rawUrl"" data-ajax=""true"" method=""post"">", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginFormAction()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: false);
+ AjaxOptions ajaxOptions = new AjaxOptions();
+ StringWriter writer = new StringWriter();
+ ajaxHelper.ViewContext.Writer = writer;
+
+ // Act
+ IDisposable form = ajaxHelper.BeginForm("Action", ajaxOptions);
+
+ // Assert
+ Assert.Equal(@"<form action=""" + MvcHelper.AppPathModifier + @"/app/home/Action"" method=""post"" onclick=""Sys.Mvc.AsyncForm.handleClick(this, new Sys.UI.DomEvent(event));"" onsubmit=""Sys.Mvc.AsyncForm.handleSubmit(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace });"">", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginFormAction_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: true);
+ AjaxOptions ajaxOptions = new AjaxOptions();
+ StringWriter writer = new StringWriter();
+ ajaxHelper.ViewContext.Writer = writer;
+
+ // Act
+ IDisposable form = ajaxHelper.BeginForm("Action", ajaxOptions);
+
+ // Assert
+ Assert.Equal(@"<form action=""" + MvcHelper.AppPathModifier + @"/app/home/Action"" data-ajax=""true"" method=""post"">", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginFormAnonymousValues()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: false);
+ AjaxOptions ajaxOptions = new AjaxOptions();
+ object values = new { controller = "Controller" };
+ StringWriter writer = new StringWriter();
+ ajaxHelper.ViewContext.Writer = writer;
+
+ // Act
+ IDisposable form = ajaxHelper.BeginForm("Action", values, ajaxOptions);
+
+ // Assert
+ Assert.Equal(@"<form action=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action"" method=""post"" onclick=""Sys.Mvc.AsyncForm.handleClick(this, new Sys.UI.DomEvent(event));"" onsubmit=""Sys.Mvc.AsyncForm.handleSubmit(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace });"">", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginFormAnonymousValues_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: true);
+ AjaxOptions ajaxOptions = new AjaxOptions();
+ object values = new { controller = "Controller" };
+ StringWriter writer = new StringWriter();
+ ajaxHelper.ViewContext.Writer = writer;
+
+ // Act
+ IDisposable form = ajaxHelper.BeginForm("Action", values, ajaxOptions);
+
+ // Assert
+ Assert.Equal(@"<form action=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action"" data-ajax=""true"" method=""post"">", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginFormAnonymousValuesAndAttributes()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: false);
+ AjaxOptions ajaxOptions = new AjaxOptions { UpdateTargetId = "some-id" };
+ object values = new { controller = "Controller" };
+ object htmlAttributes = new { method = "get", foo_bar = "baz_quux" };
+ StringWriter writer = new StringWriter();
+ ajaxHelper.ViewContext.Writer = writer;
+
+ // Act
+ IDisposable form = ajaxHelper.BeginForm("Action", values, ajaxOptions, htmlAttributes);
+
+ // Assert
+ Assert.Equal(@"<form action=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action"" foo-bar=""baz_quux"" method=""get"" onclick=""Sys.Mvc.AsyncForm.handleClick(this, new Sys.UI.DomEvent(event));"" onsubmit=""Sys.Mvc.AsyncForm.handleSubmit(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace, updateTargetId: &#39;some-id&#39; });"">", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginFormAnonymousValuesAndAttributes_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: true);
+ AjaxOptions ajaxOptions = new AjaxOptions { UpdateTargetId = "some-id" };
+ object values = new { controller = "Controller" };
+ object htmlAttributes = new { method = "get", foo_bar = "baz_quux" };
+ StringWriter writer = new StringWriter();
+ ajaxHelper.ViewContext.Writer = writer;
+
+ // Act
+ IDisposable form = ajaxHelper.BeginForm("Action", values, ajaxOptions, htmlAttributes);
+
+ // Assert
+ Assert.Equal(@"<form action=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action"" data-ajax=""true"" data-ajax-mode=""replace"" data-ajax-update=""#some-id"" foo-bar=""baz_quux"" method=""get"">", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginFormTypedValues()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: false);
+ AjaxOptions ajaxOptions = new AjaxOptions();
+ RouteValueDictionary values = new RouteValueDictionary
+ {
+ { "controller", "Controller" }
+ };
+ StringWriter writer = new StringWriter();
+ ajaxHelper.ViewContext.Writer = writer;
+
+ // Act
+ IDisposable form = ajaxHelper.BeginForm("Action", values, ajaxOptions);
+
+ // Assert
+ Assert.Equal(@"<form action=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action"" method=""post"" onclick=""Sys.Mvc.AsyncForm.handleClick(this, new Sys.UI.DomEvent(event));"" onsubmit=""Sys.Mvc.AsyncForm.handleSubmit(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace });"">", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginFormTypedValues_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: true);
+ AjaxOptions ajaxOptions = new AjaxOptions();
+ RouteValueDictionary values = new RouteValueDictionary
+ {
+ { "controller", "Controller" }
+ };
+ StringWriter writer = new StringWriter();
+ ajaxHelper.ViewContext.Writer = writer;
+
+ // Act
+ IDisposable form = ajaxHelper.BeginForm("Action", values, ajaxOptions);
+
+ // Assert
+ Assert.Equal(@"<form action=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action"" data-ajax=""true"" method=""post"">", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginFormTypedValuesAndAttributes()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: false);
+ AjaxOptions ajaxOptions = new AjaxOptions { UpdateTargetId = "some-id" };
+ RouteValueDictionary values = new RouteValueDictionary
+ {
+ { "controller", "Controller" }
+ };
+ Dictionary<string, object> htmlAttributes = new Dictionary<string, object>
+ {
+ { "method", "get" },
+ { "foo_bar", "baz_quux" }
+ };
+ StringWriter writer = new StringWriter();
+ ajaxHelper.ViewContext.Writer = writer;
+
+ // Act
+ IDisposable form = ajaxHelper.BeginForm("Action", values, ajaxOptions, htmlAttributes);
+
+ // Assert
+ Assert.Equal(@"<form action=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action"" foo_bar=""baz_quux"" method=""get"" onclick=""Sys.Mvc.AsyncForm.handleClick(this, new Sys.UI.DomEvent(event));"" onsubmit=""Sys.Mvc.AsyncForm.handleSubmit(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace, updateTargetId: &#39;some-id&#39; });"">", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginFormTypedValuesAndAttributes_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: true);
+ AjaxOptions ajaxOptions = new AjaxOptions { UpdateTargetId = "some-id" };
+ RouteValueDictionary values = new RouteValueDictionary
+ {
+ { "controller", "Controller" }
+ };
+ Dictionary<string, object> htmlAttributes = new Dictionary<string, object>
+ {
+ { "method", "get" },
+ { "foo_bar", "baz_quux" }
+ };
+ StringWriter writer = new StringWriter();
+ ajaxHelper.ViewContext.Writer = writer;
+
+ // Act
+ IDisposable form = ajaxHelper.BeginForm("Action", values, ajaxOptions, htmlAttributes);
+
+ // Assert
+ Assert.Equal(@"<form action=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action"" data-ajax=""true"" data-ajax-mode=""replace"" data-ajax-update=""#some-id"" foo_bar=""baz_quux"" method=""get"">", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginFormController()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: false);
+ AjaxOptions ajaxOptions = new AjaxOptions();
+ StringWriter writer = new StringWriter();
+ ajaxHelper.ViewContext.Writer = writer;
+
+ // Act
+ IDisposable form = ajaxHelper.BeginForm("Action", "Controller", ajaxOptions);
+
+ // Assert
+ Assert.Equal(@"<form action=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action"" method=""post"" onclick=""Sys.Mvc.AsyncForm.handleClick(this, new Sys.UI.DomEvent(event));"" onsubmit=""Sys.Mvc.AsyncForm.handleSubmit(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace });"">", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginFormController_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: true);
+ AjaxOptions ajaxOptions = new AjaxOptions();
+ StringWriter writer = new StringWriter();
+ ajaxHelper.ViewContext.Writer = writer;
+
+ // Act
+ IDisposable form = ajaxHelper.BeginForm("Action", "Controller", ajaxOptions);
+
+ // Assert
+ Assert.Equal(@"<form action=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action"" data-ajax=""true"" method=""post"">", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginFormControllerAnonymousValues()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: false);
+ AjaxOptions ajaxOptions = new AjaxOptions();
+ object values = new { id = 5 };
+ StringWriter writer = new StringWriter();
+ ajaxHelper.ViewContext.Writer = writer;
+
+ // Act
+ IDisposable form = ajaxHelper.BeginForm("Action", "Controller", values, ajaxOptions);
+
+ // Assert
+ Assert.Equal(@"<form action=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action/5"" method=""post"" onclick=""Sys.Mvc.AsyncForm.handleClick(this, new Sys.UI.DomEvent(event));"" onsubmit=""Sys.Mvc.AsyncForm.handleSubmit(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace });"">", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginFormControllerAnonymousValues_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: true);
+ AjaxOptions ajaxOptions = new AjaxOptions();
+ object values = new { id = 5 };
+ StringWriter writer = new StringWriter();
+ ajaxHelper.ViewContext.Writer = writer;
+
+ // Act
+ IDisposable form = ajaxHelper.BeginForm("Action", "Controller", values, ajaxOptions);
+
+ // Assert
+ Assert.Equal(@"<form action=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action/5"" data-ajax=""true"" method=""post"">", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginFormControllerAnonymousValuesAndAttributes()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: false);
+ AjaxOptions ajaxOptions = new AjaxOptions();
+ object values = new { id = 5 };
+ object htmlAttributes = new { method = "get", foo_bar = "baz_quux" };
+ StringWriter writer = new StringWriter();
+ ajaxHelper.ViewContext.Writer = writer;
+
+ // Act
+ IDisposable form = ajaxHelper.BeginForm("Action", "Controller", values, ajaxOptions, htmlAttributes);
+
+ // Assert
+ Assert.Equal(@"<form action=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action/5"" foo-bar=""baz_quux"" method=""get"" onclick=""Sys.Mvc.AsyncForm.handleClick(this, new Sys.UI.DomEvent(event));"" onsubmit=""Sys.Mvc.AsyncForm.handleSubmit(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace });"">", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginFormControllerAnonymousValuesAndAttributes_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: true);
+ AjaxOptions ajaxOptions = new AjaxOptions();
+ object values = new { id = 5 };
+ object htmlAttributes = new { method = "get", foo_bar = "baz_quux" };
+ StringWriter writer = new StringWriter();
+ ajaxHelper.ViewContext.Writer = writer;
+
+ // Act
+ IDisposable form = ajaxHelper.BeginForm("Action", "Controller", values, ajaxOptions, htmlAttributes);
+
+ // Assert
+ Assert.Equal(@"<form action=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action/5"" data-ajax=""true"" foo-bar=""baz_quux"" method=""get"">", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginFormControllerTypedValues()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: false);
+ AjaxOptions ajaxOptions = new AjaxOptions();
+ RouteValueDictionary values = new RouteValueDictionary
+ {
+ { "id", 5 }
+ };
+ StringWriter writer = new StringWriter();
+ ajaxHelper.ViewContext.Writer = writer;
+
+ // Act
+ IDisposable form = ajaxHelper.BeginForm("Action", "Controller", values, ajaxOptions);
+
+ // Assert
+ Assert.Equal(@"<form action=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action/5"" method=""post"" onclick=""Sys.Mvc.AsyncForm.handleClick(this, new Sys.UI.DomEvent(event));"" onsubmit=""Sys.Mvc.AsyncForm.handleSubmit(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace });"">", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginFormControllerTypedValues_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: true);
+ AjaxOptions ajaxOptions = new AjaxOptions();
+ RouteValueDictionary values = new RouteValueDictionary
+ {
+ { "id", 5 }
+ };
+ StringWriter writer = new StringWriter();
+ ajaxHelper.ViewContext.Writer = writer;
+
+ // Act
+ IDisposable form = ajaxHelper.BeginForm("Action", "Controller", values, ajaxOptions);
+
+ // Assert
+ Assert.Equal(@"<form action=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action/5"" data-ajax=""true"" method=""post"">", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginFormControllerTypedValuesAndAttributes()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: false);
+ AjaxOptions ajaxOptions = new AjaxOptions();
+ RouteValueDictionary values = new RouteValueDictionary
+ {
+ { "id", 5 }
+ };
+ Dictionary<string, object> htmlAttributes = new Dictionary<string, object>
+ {
+ { "method", "get" },
+ { "foo_bar", "baz_quux" }
+ };
+ StringWriter writer = new StringWriter();
+ ajaxHelper.ViewContext.Writer = writer;
+
+ // Act
+ IDisposable form = ajaxHelper.BeginForm("Action", "Controller", values, ajaxOptions, htmlAttributes);
+
+ // Assert
+ Assert.Equal(@"<form action=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action/5"" foo_bar=""baz_quux"" method=""get"" onclick=""Sys.Mvc.AsyncForm.handleClick(this, new Sys.UI.DomEvent(event));"" onsubmit=""Sys.Mvc.AsyncForm.handleSubmit(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace });"">", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginFormControllerTypedValuesAndAttributes_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: true);
+ AjaxOptions ajaxOptions = new AjaxOptions();
+ RouteValueDictionary values = new RouteValueDictionary
+ {
+ { "id", 5 }
+ };
+ Dictionary<string, object> htmlAttributes = new Dictionary<string, object>
+ {
+ { "method", "get" },
+ { "foo_bar", "baz_quux" }
+ };
+ StringWriter writer = new StringWriter();
+ ajaxHelper.ViewContext.Writer = writer;
+
+ // Act
+ IDisposable form = ajaxHelper.BeginForm("Action", "Controller", values, ajaxOptions, htmlAttributes);
+
+ // Assert
+ Assert.Equal(@"<form action=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action/5"" data-ajax=""true"" foo_bar=""baz_quux"" method=""get"">", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginFormWithTargetId()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: false);
+ AjaxOptions ajaxOptions = new AjaxOptions { UpdateTargetId = "some-id" };
+ StringWriter writer = new StringWriter();
+ ajaxHelper.ViewContext.Writer = writer;
+
+ // Act
+ IDisposable form = ajaxHelper.BeginForm("Action", "Controller", ajaxOptions);
+
+ // Assert
+ Assert.Equal(@"<form action=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action"" method=""post"" onclick=""Sys.Mvc.AsyncForm.handleClick(this, new Sys.UI.DomEvent(event));"" onsubmit=""Sys.Mvc.AsyncForm.handleSubmit(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace, updateTargetId: &#39;some-id&#39; });"">", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginFormWithTargetId_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: true);
+ AjaxOptions ajaxOptions = new AjaxOptions { UpdateTargetId = "some-id" };
+ StringWriter writer = new StringWriter();
+ ajaxHelper.ViewContext.Writer = writer;
+
+ // Act
+ IDisposable form = ajaxHelper.BeginForm("Action", "Controller", ajaxOptions);
+
+ // Assert
+ Assert.Equal(@"<form action=""" + MvcHelper.AppPathModifier + @"/app/Controller/Action"" data-ajax=""true"" data-ajax-mode=""replace"" data-ajax-update=""#some-id"" method=""post"">", writer.ToString());
+ }
+
+ // BeginRouteForm
+
+ [Fact]
+ public void BeginRouteForm()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: false);
+ AjaxOptions ajaxOptions = new AjaxOptions();
+ StringWriter writer = new StringWriter();
+ ajaxHelper.ViewContext.Writer = writer;
+
+ // Act
+ IDisposable form = ajaxHelper.BeginRouteForm("namedroute", ajaxOptions);
+
+ // Assert
+ Assert.Equal(@"<form action=""" + MvcHelper.AppPathModifier + @"/app/named/home/oldaction"" method=""post"" onclick=""Sys.Mvc.AsyncForm.handleClick(this, new Sys.UI.DomEvent(event));"" onsubmit=""Sys.Mvc.AsyncForm.handleSubmit(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace });"">", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginRouteForm_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: true);
+ AjaxOptions ajaxOptions = new AjaxOptions();
+ StringWriter writer = new StringWriter();
+ ajaxHelper.ViewContext.Writer = writer;
+
+ // Act
+ IDisposable form = ajaxHelper.BeginRouteForm("namedroute", ajaxOptions);
+
+ // Assert
+ Assert.Equal(@"<form action=""" + MvcHelper.AppPathModifier + @"/app/named/home/oldaction"" data-ajax=""true"" method=""post"">", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginRouteFormAnonymousValues()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: false);
+ AjaxOptions ajaxOptions = new AjaxOptions();
+ AjaxHelper poes = GetAjaxHelper(unobtrusiveJavaScript: false);
+ StringWriter writer = new StringWriter();
+ ajaxHelper.ViewContext.Writer = writer;
+
+ // Act
+ IDisposable form = ajaxHelper.BeginRouteForm("namedroute", null, ajaxOptions);
+
+ // Assert
+ Assert.Equal(@"<form action=""" + MvcHelper.AppPathModifier + @"/app/named/home/oldaction"" method=""post"" onclick=""Sys.Mvc.AsyncForm.handleClick(this, new Sys.UI.DomEvent(event));"" onsubmit=""Sys.Mvc.AsyncForm.handleSubmit(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace });"">", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginRouteFormAnonymousValues_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: true);
+ AjaxOptions ajaxOptions = new AjaxOptions();
+ AjaxHelper poes = GetAjaxHelper(unobtrusiveJavaScript: true);
+ StringWriter writer = new StringWriter();
+ ajaxHelper.ViewContext.Writer = writer;
+
+ // Act
+ IDisposable form = ajaxHelper.BeginRouteForm("namedroute", null, ajaxOptions);
+
+ // Assert
+ Assert.Equal(@"<form action=""" + MvcHelper.AppPathModifier + @"/app/named/home/oldaction"" data-ajax=""true"" method=""post"">", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginRouteFormAnonymousValuesAndAttributes()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: false);
+ AjaxOptions ajaxOptions = new AjaxOptions { UpdateTargetId = "some-id" };
+ object htmlAttributes = new { method = "get", foo_bar = "baz_quux" };
+ StringWriter writer = new StringWriter();
+ ajaxHelper.ViewContext.Writer = writer;
+
+ // Act
+ IDisposable form = ajaxHelper.BeginRouteForm("namedroute", null, ajaxOptions, htmlAttributes);
+
+ // Assert
+ Assert.Equal(@"<form action=""" + MvcHelper.AppPathModifier + @"/app/named/home/oldaction"" foo-bar=""baz_quux"" method=""get"" onclick=""Sys.Mvc.AsyncForm.handleClick(this, new Sys.UI.DomEvent(event));"" onsubmit=""Sys.Mvc.AsyncForm.handleSubmit(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace, updateTargetId: &#39;some-id&#39; });"">", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginRouteFormAnonymousValuesAndAttributes_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: true);
+ AjaxOptions ajaxOptions = new AjaxOptions { UpdateTargetId = "some-id" };
+ object htmlAttributes = new { method = "get", foo_bar = "baz_quux" };
+ StringWriter writer = new StringWriter();
+ ajaxHelper.ViewContext.Writer = writer;
+
+ // Act
+ IDisposable form = ajaxHelper.BeginRouteForm("namedroute", null, ajaxOptions, htmlAttributes);
+
+ // Assert
+ Assert.Equal(@"<form action=""" + MvcHelper.AppPathModifier + @"/app/named/home/oldaction"" data-ajax=""true"" data-ajax-mode=""replace"" data-ajax-update=""#some-id"" foo-bar=""baz_quux"" method=""get"">", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginRouteFormCanUseNamedRouteWithoutSpecifyingDefaults()
+ {
+ // DevDiv 217072: Non-mvc specific helpers should not give default values for controller and action
+
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: false);
+ ajaxHelper.RouteCollection.MapRoute("MyRouteName", "any/url", new { controller = "Charlie" });
+ StringWriter writer = new StringWriter();
+ ajaxHelper.ViewContext.Writer = writer;
+
+ // Act
+ IDisposable form = ajaxHelper.BeginRouteForm("MyRouteName", new AjaxOptions());
+
+ // Assert
+ Assert.Equal(@"<form action=""" + MvcHelper.AppPathModifier + @"/app/any/url"" method=""post"" onclick=""Sys.Mvc.AsyncForm.handleClick(this, new Sys.UI.DomEvent(event));"" onsubmit=""Sys.Mvc.AsyncForm.handleSubmit(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace });"">", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginRouteFormCanUseNamedRouteWithoutSpecifyingDefaults_Unobtrusive()
+ {
+ // DevDiv 217072: Non-mvc specific helpers should not give default values for controller and action
+
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: true);
+ ajaxHelper.RouteCollection.MapRoute("MyRouteName", "any/url", new { controller = "Charlie" });
+ StringWriter writer = new StringWriter();
+ ajaxHelper.ViewContext.Writer = writer;
+
+ // Act
+ IDisposable form = ajaxHelper.BeginRouteForm("MyRouteName", new AjaxOptions());
+
+ // Assert
+ Assert.Equal(@"<form action=""" + MvcHelper.AppPathModifier + @"/app/any/url"" data-ajax=""true"" method=""post"">", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginRouteFormTypedValues()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: false);
+ AjaxOptions ajaxOptions = new AjaxOptions();
+ RouteValueDictionary values = new RouteValueDictionary();
+ StringWriter writer = new StringWriter();
+ ajaxHelper.ViewContext.Writer = writer;
+
+ // Act
+ IDisposable form = ajaxHelper.BeginRouteForm("namedroute", values, ajaxOptions);
+
+ // Assert
+ Assert.Equal(@"<form action=""" + MvcHelper.AppPathModifier + @"/app/named/home/oldaction"" method=""post"" onclick=""Sys.Mvc.AsyncForm.handleClick(this, new Sys.UI.DomEvent(event));"" onsubmit=""Sys.Mvc.AsyncForm.handleSubmit(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace });"">", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginRouteFormTypedValues_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: true);
+ AjaxOptions ajaxOptions = new AjaxOptions();
+ RouteValueDictionary values = new RouteValueDictionary();
+ StringWriter writer = new StringWriter();
+ ajaxHelper.ViewContext.Writer = writer;
+
+ // Act
+ IDisposable form = ajaxHelper.BeginRouteForm("namedroute", values, ajaxOptions);
+
+ // Assert
+ Assert.Equal(@"<form action=""" + MvcHelper.AppPathModifier + @"/app/named/home/oldaction"" data-ajax=""true"" method=""post"">", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginRouteFormTypedValuesAndAttributes()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: false);
+ Dictionary<string, object> htmlAttributes = new Dictionary<string, object> { { "method", "get" }, { "foo_bar", "baz_quux" } };
+ AjaxOptions ajaxOptions = new AjaxOptions { UpdateTargetId = "some-id" };
+ RouteValueDictionary values = new RouteValueDictionary();
+ StringWriter writer = new StringWriter();
+ ajaxHelper.ViewContext.Writer = writer;
+
+ // Act
+ IDisposable form = ajaxHelper.BeginRouteForm("namedroute", values, ajaxOptions, htmlAttributes);
+
+ // Assert
+ Assert.Equal(@"<form action=""" + MvcHelper.AppPathModifier + @"/app/named/home/oldaction"" foo_bar=""baz_quux"" method=""get"" onclick=""Sys.Mvc.AsyncForm.handleClick(this, new Sys.UI.DomEvent(event));"" onsubmit=""Sys.Mvc.AsyncForm.handleSubmit(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace, updateTargetId: &#39;some-id&#39; });"">", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginRouteFormTypedValuesAndAttributes_Unobtrusive()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper(unobtrusiveJavaScript: true);
+ Dictionary<string, object> htmlAttributes = new Dictionary<string, object> { { "method", "get" }, { "foo_bar", "baz_quux" } };
+ AjaxOptions ajaxOptions = new AjaxOptions { UpdateTargetId = "some-id" };
+ RouteValueDictionary values = new RouteValueDictionary();
+ StringWriter writer = new StringWriter();
+ ajaxHelper.ViewContext.Writer = writer;
+
+ // Act
+ IDisposable form = ajaxHelper.BeginRouteForm("namedroute", values, ajaxOptions, htmlAttributes);
+
+ // Assert
+ Assert.Equal(@"<form action=""" + MvcHelper.AppPathModifier + @"/app/named/home/oldaction"" data-ajax=""true"" data-ajax-mode=""replace"" data-ajax-update=""#some-id"" foo_bar=""baz_quux"" method=""get"">", writer.ToString());
+ }
+
+ // Helpers
+
+ private static AjaxHelper GetAjaxHelper(bool unobtrusiveJavaScript = false)
+ {
+ var mockRequest = new Mock<HttpRequestBase>();
+ mockRequest.Setup(o => o.Url).Returns(new Uri("http://foo.bar.baz"));
+ mockRequest.Setup(o => o.RawUrl).Returns("/rawUrl");
+ mockRequest.Setup(o => o.PathInfo).Returns(String.Empty);
+ mockRequest.Setup(o => o.ApplicationPath).Returns("/app/");
+
+ var mockResponse = new Mock<HttpResponseBase>();
+ mockResponse.Setup(o => o.ApplyAppPathModifier(It.IsAny<string>())).Returns<string>(r => MvcHelper.AppPathModifier + r);
+
+ var mockHttpContext = new Mock<HttpContextBase>();
+ mockHttpContext.Setup(o => o.Request).Returns(mockRequest.Object);
+ mockHttpContext.Setup(o => o.Session).Returns((HttpSessionStateBase)null);
+ mockHttpContext.Setup(o => o.Items).Returns(new Hashtable());
+ mockHttpContext.Setup(o => o.Response).Returns(mockResponse.Object);
+
+ var routes = new RouteCollection();
+ routes.MapRoute("default", "{controller}/{action}/{id}", new { id = "defaultid" });
+ routes.MapRoute("namedroute", "named/{controller}/{action}/{id}", new { id = "defaultid" });
+
+ var routeData = new RouteData();
+ routeData.Values.Add("controller", "home");
+ routeData.Values.Add("action", "oldaction");
+
+ var viewContext = new ViewContext()
+ {
+ HttpContext = mockHttpContext.Object,
+ RouteData = routeData,
+ UnobtrusiveJavaScriptEnabled = unobtrusiveJavaScript,
+ Writer = TextWriter.Null
+ };
+
+ return new AjaxHelper(viewContext, new Mock<IViewDataContainer>().Object, routes);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Ajax/Test/AjaxOptionsTest.cs b/test/System.Web.Mvc.Test/Ajax/Test/AjaxOptionsTest.cs
new file mode 100644
index 00000000..8e31ce29
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Ajax/Test/AjaxOptionsTest.cs
@@ -0,0 +1,290 @@
+using System.Collections.Generic;
+using System.Web.TestUtil;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Ajax.Test
+{
+ public class AjaxOptionsTest
+ {
+ [Fact]
+ public void InsertionModeProperty()
+ {
+ // Arrange
+ AjaxOptions options = new AjaxOptions();
+
+ // Act & Assert
+ MemberHelper.TestEnumProperty(options, "InsertionMode", InsertionMode.Replace, false);
+ }
+
+ [Fact]
+ public void InsertionModePropertyExceptionText()
+ {
+ // Arrange
+ AjaxOptions options = new AjaxOptions();
+
+ // Act & Assert
+ Assert.ThrowsArgumentOutOfRange(
+ delegate { options.InsertionMode = (InsertionMode)4; },
+ "value",
+ @"Specified argument was out of the range of valid values.");
+ }
+
+ [Fact]
+ public void InsertionModeStringTests()
+ {
+ // Act & Assert
+ Assert.Equal(new AjaxOptions { InsertionMode = InsertionMode.Replace }.InsertionModeString, "Sys.Mvc.InsertionMode.replace");
+ Assert.Equal(new AjaxOptions { InsertionMode = InsertionMode.InsertAfter }.InsertionModeString, "Sys.Mvc.InsertionMode.insertAfter");
+ Assert.Equal(new AjaxOptions { InsertionMode = InsertionMode.InsertBefore }.InsertionModeString, "Sys.Mvc.InsertionMode.insertBefore");
+ }
+
+ [Fact]
+ public void InsertionModeUnobtrusiveTests()
+ {
+ // Act & Assert
+ Assert.Equal(new AjaxOptions { InsertionMode = InsertionMode.Replace }.InsertionModeUnobtrusive, "replace");
+ Assert.Equal(new AjaxOptions { InsertionMode = InsertionMode.InsertAfter }.InsertionModeUnobtrusive, "after");
+ Assert.Equal(new AjaxOptions { InsertionMode = InsertionMode.InsertBefore }.InsertionModeUnobtrusive, "before");
+ }
+
+ [Fact]
+ public void HttpMethodProperty()
+ {
+ // Arrange
+ AjaxOptions options = new AjaxOptions();
+
+ // Act & Assert
+ MemberHelper.TestStringProperty(options, "HttpMethod", String.Empty);
+ }
+
+ [Fact]
+ public void OnBeginProperty()
+ {
+ // Arrange
+ AjaxOptions options = new AjaxOptions();
+
+ // Act & Assert
+ MemberHelper.TestStringProperty(options, "OnBegin", String.Empty);
+ }
+
+ [Fact]
+ public void OnFailureProperty()
+ {
+ // Arrange
+ AjaxOptions options = new AjaxOptions();
+
+ // Act & Assert
+ MemberHelper.TestStringProperty(options, "OnFailure", String.Empty);
+ }
+
+ [Fact]
+ public void OnSuccessProperty()
+ {
+ // Arrange
+ AjaxOptions options = new AjaxOptions();
+
+ // Act & Assert
+ MemberHelper.TestStringProperty(options, "OnSuccess", String.Empty);
+ }
+
+ [Fact]
+ public void ToJavascriptStringWithEmptyOptions()
+ {
+ string s = (new AjaxOptions()).ToJavascriptString();
+ Assert.Equal("{ insertionMode: Sys.Mvc.InsertionMode.replace }", s);
+ }
+
+ [Fact]
+ public void ToJavascriptString()
+ {
+ // Arrange
+ AjaxOptions options = new AjaxOptions
+ {
+ InsertionMode = InsertionMode.InsertBefore,
+ Confirm = "confirm",
+ HttpMethod = "POST",
+ LoadingElementId = "loadingElement",
+ UpdateTargetId = "someId",
+ Url = "http://someurl.com",
+ OnBegin = "some_begin_function",
+ OnComplete = "some_complete_function",
+ OnFailure = "some_failure_function",
+ OnSuccess = "some_success_function",
+ };
+
+ // Act
+ string s = options.ToJavascriptString();
+
+ // Assert
+ Assert.Equal("{ insertionMode: Sys.Mvc.InsertionMode.insertBefore, " +
+ "confirm: 'confirm', " +
+ "httpMethod: 'POST', " +
+ "loadingElementId: 'loadingElement', " +
+ "updateTargetId: 'someId', " +
+ "url: 'http://someurl.com', " +
+ "onBegin: Function.createDelegate(this, some_begin_function), " +
+ "onComplete: Function.createDelegate(this, some_complete_function), " +
+ "onFailure: Function.createDelegate(this, some_failure_function), " +
+ "onSuccess: Function.createDelegate(this, some_success_function) }", s);
+ }
+
+ [Fact]
+ public void ToJavascriptStringEscapesQuotesCorrectly()
+ {
+ // Arrange
+ AjaxOptions options = new AjaxOptions
+ {
+ InsertionMode = InsertionMode.InsertBefore,
+ Confirm = @"""confirm""",
+ HttpMethod = "POST",
+ LoadingElementId = "loading'Element'",
+ UpdateTargetId = "someId",
+ Url = "http://someurl.com",
+ OnBegin = "some_begin_function",
+ OnComplete = "some_complete_function",
+ OnFailure = "some_failure_function",
+ OnSuccess = "some_success_function",
+ };
+
+ // Act
+ string s = options.ToJavascriptString();
+
+ // Assert
+ Assert.Equal("{ insertionMode: Sys.Mvc.InsertionMode.insertBefore, " +
+ @"confirm: '""confirm""', " +
+ "httpMethod: 'POST', " +
+ @"loadingElementId: 'loading\'Element\'', " +
+ "updateTargetId: 'someId', " +
+ "url: 'http://someurl.com', " +
+ "onBegin: Function.createDelegate(this, some_begin_function), " +
+ "onComplete: Function.createDelegate(this, some_complete_function), " +
+ "onFailure: Function.createDelegate(this, some_failure_function), " +
+ "onSuccess: Function.createDelegate(this, some_success_function) }", s);
+ }
+
+ [Fact]
+ public void ToJavascriptStringWithOnlyUpdateTargetId()
+ {
+ // Arrange
+ AjaxOptions options = new AjaxOptions { UpdateTargetId = "someId" };
+
+ // Act
+ string s = options.ToJavascriptString();
+
+ // Assert
+ Assert.Equal("{ insertionMode: Sys.Mvc.InsertionMode.replace, updateTargetId: 'someId' }", s);
+ }
+
+ [Fact]
+ public void ToJavascriptStringWithUpdateTargetIdAndExplicitInsertionMode()
+ {
+ // Arrange
+ AjaxOptions options = new AjaxOptions { InsertionMode = InsertionMode.InsertAfter, UpdateTargetId = "someId" };
+
+ // Act
+ string s = options.ToJavascriptString();
+
+ // Assert
+ Assert.Equal("{ insertionMode: Sys.Mvc.InsertionMode.insertAfter, updateTargetId: 'someId' }", s);
+ }
+
+ [Fact]
+ public void ToUnobtrusiveHtmlAttributesWithEmptyOptions()
+ {
+ // Arrange
+ AjaxOptions options = new AjaxOptions();
+
+ // Act
+ IDictionary<string, object> attributes = options.ToUnobtrusiveHtmlAttributes();
+
+ // Assert
+ Assert.Single(attributes);
+ Assert.Equal("true", attributes["data-ajax"]);
+ }
+
+ [Fact]
+ public void ToUnobtrusiveHtmlAttributes()
+ {
+ // Arrange
+ AjaxOptions options = new AjaxOptions
+ {
+ InsertionMode = InsertionMode.InsertBefore,
+ Confirm = "confirm",
+ HttpMethod = "POST",
+ LoadingElementId = "loadingElement",
+ LoadingElementDuration = 450,
+ UpdateTargetId = "someId",
+ Url = "http://someurl.com",
+ OnBegin = "some_begin_function",
+ OnComplete = "some_complete_function",
+ OnFailure = "some_failure_function",
+ OnSuccess = "some_success_function",
+ };
+
+ // Act
+ var attributes = options.ToUnobtrusiveHtmlAttributes();
+
+ // Assert
+ Assert.Equal(12, attributes.Count);
+ Assert.Equal("true", attributes["data-ajax"]);
+ Assert.Equal("confirm", attributes["data-ajax-confirm"]);
+ Assert.Equal("POST", attributes["data-ajax-method"]);
+ Assert.Equal("#loadingElement", attributes["data-ajax-loading"]);
+ Assert.Equal(450, attributes["data-ajax-loading-duration"]);
+ Assert.Equal("http://someurl.com", attributes["data-ajax-url"]);
+ Assert.Equal("#someId", attributes["data-ajax-update"]);
+ Assert.Equal("before", attributes["data-ajax-mode"]);
+ Assert.Equal("some_begin_function", attributes["data-ajax-begin"]);
+ Assert.Equal("some_complete_function", attributes["data-ajax-complete"]);
+ Assert.Equal("some_failure_function", attributes["data-ajax-failure"]);
+ Assert.Equal("some_success_function", attributes["data-ajax-success"]);
+ }
+
+ [Fact]
+ public void ToUnobtrusiveHtmlAttributesWithOnlyUpdateTargetId()
+ {
+ // Arrange
+ AjaxOptions options = new AjaxOptions { UpdateTargetId = "someId" };
+
+ // Act
+ var attributes = options.ToUnobtrusiveHtmlAttributes();
+
+ // Assert
+ Assert.Equal(3, attributes.Count);
+ Assert.Equal("true", attributes["data-ajax"]);
+ Assert.Equal("#someId", attributes["data-ajax-update"]);
+ Assert.Equal("replace", attributes["data-ajax-mode"]); // Only added when UpdateTargetId is set
+ }
+
+ [Fact]
+ public void ToUnobtrusiveHtmlAttributesWithUpdateTargetIdAndExplicitInsertionMode()
+ {
+ // Arrange
+ AjaxOptions options = new AjaxOptions
+ {
+ InsertionMode = InsertionMode.InsertAfter,
+ UpdateTargetId = "someId"
+ };
+
+ // Act
+ var attributes = options.ToUnobtrusiveHtmlAttributes();
+
+ // Assert
+ Assert.Equal(3, attributes.Count);
+ Assert.Equal("true", attributes["data-ajax"]);
+ Assert.Equal("#someId", attributes["data-ajax-update"]);
+ Assert.Equal("after", attributes["data-ajax-mode"]);
+ }
+
+ [Fact]
+ public void UpdateTargetIdProperty()
+ {
+ // Arrange
+ AjaxOptions options = new AjaxOptions();
+
+ // Act & Assert
+ MemberHelper.TestStringProperty(options, "UpdateTargetId", String.Empty);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Async/Test/AsyncActionDescriptorTest.cs b/test/System.Web.Mvc.Test/Async/Test/AsyncActionDescriptorTest.cs
new file mode 100644
index 00000000..2bda6573
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Async/Test/AsyncActionDescriptorTest.cs
@@ -0,0 +1,51 @@
+using System.Collections.Generic;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Async.Test
+{
+ public class AsyncActionDescriptorTest
+ {
+ [Fact]
+ public void SynchronousExecuteThrows()
+ {
+ // Arrange
+ AsyncActionDescriptor actionDescriptor = new TestableAsyncActionDescriptor();
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ () => actionDescriptor.Execute(new Mock<ControllerContext>().Object, parameters: null),
+ "The asynchronous action method 'testAction' cannot be executed synchronously."
+ );
+ }
+
+ private class TestableAsyncActionDescriptor : AsyncActionDescriptor
+ {
+ public override string ActionName
+ {
+ get { return "testAction"; }
+ }
+
+ public override ControllerDescriptor ControllerDescriptor
+ {
+ get { throw new NotImplementedException(); }
+ }
+
+ public override IAsyncResult BeginExecute(ControllerContext controllerContext, IDictionary<string, object> parameters, AsyncCallback callback, object state)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override object EndExecute(IAsyncResult asyncResult)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override ParameterDescriptor[] GetParameters()
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Async/Test/AsyncActionMethodSelectorTest.cs b/test/System.Web.Mvc.Test/Async/Test/AsyncActionMethodSelectorTest.cs
new file mode 100644
index 00000000..6a23adde
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Async/Test/AsyncActionMethodSelectorTest.cs
@@ -0,0 +1,377 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Threading.Tasks;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Async.Test
+{
+ public class AsyncActionMethodSelectorTest
+ {
+ [Fact]
+ public void AliasedMethodsProperty()
+ {
+ // Arrange
+ Type controllerType = typeof(MethodLocatorController);
+
+ // Act
+ AsyncActionMethodSelector selector = new AsyncActionMethodSelector(controllerType);
+
+ // Assert
+ Assert.Equal(3, selector.AliasedMethods.Length);
+
+ List<MethodInfo> sortedAliasedMethods = selector.AliasedMethods.OrderBy(methodInfo => methodInfo.Name).ToList();
+ Assert.Equal("Bar", sortedAliasedMethods[0].Name);
+ Assert.Equal("FooRenamed", sortedAliasedMethods[1].Name);
+ Assert.Equal("Renamed", sortedAliasedMethods[2].Name);
+ }
+
+ [Fact]
+ public void ControllerTypeProperty()
+ {
+ // Arrange
+ Type controllerType = typeof(MethodLocatorController);
+ AsyncActionMethodSelector selector = new AsyncActionMethodSelector(controllerType);
+
+ // Act & Assert
+ Assert.Same(controllerType, selector.ControllerType);
+ }
+
+ [Fact]
+ public void FindAction_DoesNotMatchAsyncMethod()
+ {
+ // Arrange
+ Type controllerType = typeof(MethodLocatorController);
+ AsyncActionMethodSelector selector = new AsyncActionMethodSelector(controllerType);
+
+ // Act
+ ActionDescriptorCreator creator = selector.FindAction(null, "EventPatternAsync");
+
+ // Assert
+ Assert.Null(creator);
+ }
+
+ [Fact]
+ public void FindAction_DoesNotMatchCompletedMethod()
+ {
+ // Arrange
+ Type controllerType = typeof(MethodLocatorController);
+ AsyncActionMethodSelector selector = new AsyncActionMethodSelector(controllerType);
+
+ // Act
+ ActionDescriptorCreator creator = selector.FindAction(null, "EventPatternCompleted");
+
+ // Assert
+ Assert.Null(creator);
+ }
+
+ [Fact]
+ public void FindAction_ReturnsMatchingMethodIfOneMethodMatches()
+ {
+ // Arrange
+ Type controllerType = typeof(SelectionAttributeController);
+ AsyncActionMethodSelector selector = new AsyncActionMethodSelector(controllerType);
+
+ // Act
+ ActionDescriptorCreator creator = selector.FindAction(null, "OneMatch");
+ ActionDescriptor actionDescriptor = creator("someName", new Mock<ControllerDescriptor>().Object);
+
+ // Assert
+ var castActionDescriptor = Assert.IsType<ReflectedActionDescriptor>(actionDescriptor);
+ Assert.Equal("OneMatch", castActionDescriptor.MethodInfo.Name);
+ Assert.Equal(typeof(string), castActionDescriptor.MethodInfo.GetParameters()[0].ParameterType);
+ }
+
+ [Fact]
+ public void FindAction_ReturnsMethodWithActionSelectionAttributeIfMultipleMethodsMatchRequest()
+ {
+ // DevDiv Bugs 212062: If multiple action methods match a request, we should match only the methods with an
+ // [ActionMethod] attribute since we assume those methods are more specific.
+
+ // Arrange
+ Type controllerType = typeof(SelectionAttributeController);
+ AsyncActionMethodSelector selector = new AsyncActionMethodSelector(controllerType);
+
+ // Act
+ ActionDescriptorCreator creator = selector.FindAction(null, "ShouldMatchMethodWithSelectionAttribute");
+ ActionDescriptor actionDescriptor = creator("someName", new Mock<ControllerDescriptor>().Object);
+
+ // Assert
+ var castActionDescriptor = Assert.IsType<ReflectedActionDescriptor>(actionDescriptor);
+ Assert.Equal("MethodHasSelectionAttribute1", castActionDescriptor.MethodInfo.Name);
+ }
+
+ [Fact]
+ public void FindAction_ReturnsNullIfNoMethodMatches()
+ {
+ // Arrange
+ Type controllerType = typeof(SelectionAttributeController);
+ AsyncActionMethodSelector selector = new AsyncActionMethodSelector(controllerType);
+
+ // Act
+ ActionDescriptorCreator creator = selector.FindAction(null, "ZeroMatch");
+
+ // Assert
+ Assert.Null(creator);
+ }
+
+ [Fact]
+ public void FindAction_ThrowsIfMultipleMethodsMatch()
+ {
+ // Arrange
+ Type controllerType = typeof(SelectionAttributeController);
+ AsyncActionMethodSelector selector = new AsyncActionMethodSelector(controllerType);
+
+ // Act & veriy
+ Assert.Throws<AmbiguousMatchException>(
+ delegate { selector.FindAction(null, "TwoMatch"); },
+ @"The current request for action 'TwoMatch' on controller type 'SelectionAttributeController' is ambiguous between the following action methods:
+Void TwoMatch2() on type System.Web.Mvc.Async.Test.AsyncActionMethodSelectorTest+SelectionAttributeController
+Void TwoMatch() on type System.Web.Mvc.Async.Test.AsyncActionMethodSelectorTest+SelectionAttributeController");
+ }
+
+ [Fact]
+ public void FindActionMethod_Asynchronous()
+ {
+ // Arrange
+ Type controllerType = typeof(MethodLocatorController);
+ AsyncActionMethodSelector selector = new AsyncActionMethodSelector(controllerType);
+
+ // Act
+ ActionDescriptorCreator creator = selector.FindAction(null, "EventPattern");
+ ActionDescriptor actionDescriptor = creator("someName", new Mock<ControllerDescriptor>().Object);
+
+ // Assert
+ var castActionDescriptor = Assert.IsType<ReflectedAsyncActionDescriptor>(actionDescriptor);
+ Assert.Equal("EventPatternAsync", castActionDescriptor.AsyncMethodInfo.Name);
+ Assert.Equal("EventPatternCompleted", castActionDescriptor.CompletedMethodInfo.Name);
+ }
+
+ [Fact]
+ public void FindActionMethod_Task()
+ {
+ // Arrange
+ Type controllerType = typeof(MethodLocatorController);
+ AsyncActionMethodSelector selector = new AsyncActionMethodSelector(controllerType);
+
+ // Act
+ ActionDescriptorCreator creator = selector.FindAction(null, "TaskPattern");
+ ActionDescriptor actionDescriptor = creator("someName", new Mock<ControllerDescriptor>().Object);
+
+ // Assert
+ var castActionDescriptor = Assert.IsType<TaskAsyncActionDescriptor>(actionDescriptor);
+ Assert.Equal("TaskPattern", castActionDescriptor.TaskMethodInfo.Name);
+ Assert.Equal(typeof(Task), castActionDescriptor.TaskMethodInfo.ReturnType);
+ }
+
+ [Fact]
+ public void FindActionMethod_GenericTask()
+ {
+ // Arrange
+ Type controllerType = typeof(MethodLocatorController);
+ AsyncActionMethodSelector selector = new AsyncActionMethodSelector(controllerType);
+
+ // Act
+ ActionDescriptorCreator creator = selector.FindAction(null, "GenericTaskPattern");
+ ActionDescriptor actionDescriptor = creator("someName", new Mock<ControllerDescriptor>().Object);
+
+ // Assert
+ var castActionDescriptor = Assert.IsType<TaskAsyncActionDescriptor>(actionDescriptor);
+ Assert.Equal("GenericTaskPattern", castActionDescriptor.TaskMethodInfo.Name);
+ Assert.Equal(typeof(Task<string>), castActionDescriptor.TaskMethodInfo.ReturnType);
+ }
+
+ [Fact]
+ public void FindActionMethod_Asynchronous_ThrowsIfCompletionMethodNotFound()
+ {
+ // Arrange
+ Type controllerType = typeof(MethodLocatorController);
+ AsyncActionMethodSelector selector = new AsyncActionMethodSelector(controllerType);
+
+ // Act & assert
+ Assert.Throws<InvalidOperationException>(
+ delegate { ActionDescriptorCreator creator = selector.FindAction(null, "EventPatternWithoutCompletionMethod"); },
+ @"Could not locate a method named 'EventPatternWithoutCompletionMethodCompleted' on controller type System.Web.Mvc.Async.Test.AsyncActionMethodSelectorTest+MethodLocatorController.");
+ }
+
+ [Fact]
+ public void FindActionMethod_Asynchronous_ThrowsIfMultipleCompletedMethodsMatched()
+ {
+ // Arrange
+ Type controllerType = typeof(MethodLocatorController);
+ AsyncActionMethodSelector selector = new AsyncActionMethodSelector(controllerType);
+
+ // Act & assert
+ Assert.Throws<AmbiguousMatchException>(
+ delegate { ActionDescriptorCreator creator = selector.FindAction(null, "EventPatternAmbiguous"); },
+ @"Lookup for method 'EventPatternAmbiguousCompleted' on controller type 'MethodLocatorController' failed because of an ambiguity between the following methods:
+Void EventPatternAmbiguousCompleted(Int32) on type System.Web.Mvc.Async.Test.AsyncActionMethodSelectorTest+MethodLocatorController
+Void EventPatternAmbiguousCompleted(System.String) on type System.Web.Mvc.Async.Test.AsyncActionMethodSelectorTest+MethodLocatorController");
+ }
+
+ [Fact]
+ public void NonAliasedMethodsProperty()
+ {
+ // Arrange
+ Type controllerType = typeof(MethodLocatorController);
+
+ // Act
+ AsyncActionMethodSelector selector = new AsyncActionMethodSelector(controllerType);
+
+ // Assert
+ Assert.Equal(6, selector.NonAliasedMethods.Count);
+
+ List<MethodInfo> sortedMethods = selector.NonAliasedMethods["foo"].OrderBy(methodInfo => methodInfo.GetParameters().Length).ToList();
+ Assert.Equal("Foo", sortedMethods[0].Name);
+ Assert.Empty(sortedMethods[0].GetParameters());
+ Assert.Equal("Foo", sortedMethods[1].Name);
+ Assert.Equal(typeof(string), sortedMethods[1].GetParameters()[0].ParameterType);
+
+ Assert.Equal(1, selector.NonAliasedMethods["EventPattern"].Count());
+ Assert.Equal("EventPatternAsync", selector.NonAliasedMethods["EventPattern"].First().Name);
+ Assert.Equal(1, selector.NonAliasedMethods["EventPatternAmbiguous"].Count());
+ Assert.Equal("EventPatternAmbiguousAsync", selector.NonAliasedMethods["EventPatternAmbiguous"].First().Name);
+ Assert.Equal(1, selector.NonAliasedMethods["EventPatternWithoutCompletionMethod"].Count());
+ Assert.Equal("EventPatternWithoutCompletionMethodAsync", selector.NonAliasedMethods["EventPatternWithoutCompletionMethod"].First().Name);
+
+ Assert.Equal(1, selector.NonAliasedMethods["TaskPattern"].Count());
+ Assert.Equal("TaskPattern", selector.NonAliasedMethods["TaskPattern"].First().Name);
+ Assert.Equal(1, selector.NonAliasedMethods["GenericTaskPattern"].Count());
+ Assert.Equal("GenericTaskPattern", selector.NonAliasedMethods["GenericTaskPattern"].First().Name);
+ }
+
+ private class MethodLocatorController : Controller
+ {
+ public void Foo()
+ {
+ }
+
+ public void Foo(string s)
+ {
+ }
+
+ [ActionName("Foo")]
+ public void FooRenamed()
+ {
+ }
+
+ [ActionName("Bar")]
+ public void Bar()
+ {
+ }
+
+ [ActionName("PrivateVoid")]
+ private void PrivateVoid()
+ {
+ }
+
+ protected void ProtectedVoidAction()
+ {
+ }
+
+ public static void StaticMethod()
+ {
+ }
+
+ public void EventPatternAsync()
+ {
+ }
+
+ public void EventPatternCompleted()
+ {
+ }
+
+ public void EventPatternWithoutCompletionMethodAsync()
+ {
+ }
+
+ public void EventPatternAmbiguousAsync()
+ {
+ }
+
+ public void EventPatternAmbiguousCompleted(int i)
+ {
+ }
+
+ public void EventPatternAmbiguousCompleted(string s)
+ {
+ }
+
+ public Task TaskPattern()
+ {
+ return Task.Factory.StartNew(() => "foo");
+ }
+
+ public Task<string> GenericTaskPattern()
+ {
+ return Task.Factory.StartNew(() => "foo");
+ }
+
+ [ActionName("RenamedCompleted")]
+ public void Renamed()
+ {
+ }
+
+ // ensure that methods inheriting from Controller or a base class are not matched
+ [ActionName("Blah")]
+ protected override void ExecuteCore()
+ {
+ throw new NotImplementedException();
+ }
+
+ public string StringProperty { get; set; }
+
+#pragma warning disable 0067
+ public event EventHandler<EventArgs> SomeEvent;
+#pragma warning restore 0067
+ }
+
+ private class SelectionAttributeController : Controller
+ {
+ [Match(false)]
+ public void OneMatch()
+ {
+ }
+
+ public void OneMatch(string s)
+ {
+ }
+
+ public void TwoMatch()
+ {
+ }
+
+ [ActionName("TwoMatch")]
+ public void TwoMatch2()
+ {
+ }
+
+ [Match(true), ActionName("ShouldMatchMethodWithSelectionAttribute")]
+ public void MethodHasSelectionAttribute1()
+ {
+ }
+
+ [ActionName("ShouldMatchMethodWithSelectionAttribute")]
+ public void MethodDoesNotHaveSelectionAttribute1()
+ {
+ }
+
+ private class MatchAttribute : ActionMethodSelectorAttribute
+ {
+ private bool _match;
+
+ public MatchAttribute(bool match)
+ {
+ _match = match;
+ }
+
+ public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
+ {
+ return _match;
+ }
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Async/Test/AsyncControllerActionInvokerTest.cs b/test/System.Web.Mvc.Test/Async/Test/AsyncControllerActionInvokerTest.cs
new file mode 100644
index 00000000..7905eff0
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Async/Test/AsyncControllerActionInvokerTest.cs
@@ -0,0 +1,916 @@
+using System.Collections.Generic;
+using System.Threading;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Async.Test
+{
+ public class AsyncControllerActionInvokerTest
+ {
+ [Fact]
+ public void InvokeAction_ActionNotFound()
+ {
+ // Arrange
+ ControllerContext controllerContext = GetControllerContext();
+ AsyncControllerActionInvoker invoker = new AsyncControllerActionInvoker();
+
+ // Act
+ IAsyncResult asyncResult = invoker.BeginInvokeAction(controllerContext, "ActionNotFound", null, null);
+ bool retVal = invoker.EndInvokeAction(asyncResult);
+
+ // Assert
+ Assert.False(retVal);
+ }
+
+ [Fact]
+ public void InvokeAction_ActionThrowsException_Handled()
+ {
+ // Arrange
+ ControllerContext controllerContext = GetControllerContext();
+ AsyncControllerActionInvoker invoker = new AsyncControllerActionInvoker();
+
+ // Act & assert
+ IAsyncResult asyncResult = invoker.BeginInvokeAction(controllerContext, "ActionThrowsExceptionAndIsHandled", null, null);
+ Assert.Null(((TestController)controllerContext.Controller).Log); // Result filter shouldn't have executed yet
+
+ bool retVal = invoker.EndInvokeAction(asyncResult);
+ Assert.True(retVal);
+ Assert.Equal("From exception filter", ((TestController)controllerContext.Controller).Log);
+ }
+
+ [Fact]
+ public void InvokeAction_ActionThrowsException_NotHandled()
+ {
+ // Arrange
+ ControllerContext controllerContext = GetControllerContext();
+ AsyncControllerActionInvoker invoker = new AsyncControllerActionInvoker();
+
+ // Act & assert
+ Assert.Throws<Exception>(
+ delegate { invoker.BeginInvokeAction(controllerContext, "ActionThrowsExceptionAndIsNotHandled", null, null); },
+ @"Some exception text.");
+ }
+
+ [Fact]
+ public void InvokeAction_ActionThrowsException_ThreadAbort()
+ {
+ // Arrange
+ ControllerContext controllerContext = GetControllerContext();
+ AsyncControllerActionInvoker invoker = new AsyncControllerActionInvoker();
+
+ // Act & assert
+ Assert.Throws<ThreadAbortException>(
+ delegate { invoker.BeginInvokeAction(controllerContext, "ActionCallsThreadAbort", null, null); });
+ }
+
+ [Fact]
+ public void InvokeAction_AuthorizationFilterShortCircuits()
+ {
+ // Arrange
+ ControllerContext controllerContext = GetControllerContext();
+ AsyncControllerActionInvoker invoker = new AsyncControllerActionInvoker();
+
+ // Act
+ IAsyncResult asyncResult = invoker.BeginInvokeAction(controllerContext, "AuthorizationFilterShortCircuits", null, null);
+ bool retVal = invoker.EndInvokeAction(asyncResult);
+
+ // Assert
+ Assert.True(retVal);
+ Assert.Equal("From authorization filter", ((TestController)controllerContext.Controller).Log);
+ }
+
+ [Fact]
+ public void InvokeAction_NormalAction()
+ {
+ // Arrange
+ ControllerContext controllerContext = GetControllerContext();
+ AsyncControllerActionInvoker invoker = new AsyncControllerActionInvoker();
+
+ // Act
+ IAsyncResult asyncResult = invoker.BeginInvokeAction(controllerContext, "NormalAction", null, null);
+ bool retVal = invoker.EndInvokeAction(asyncResult);
+
+ // Assert
+ Assert.True(retVal);
+ Assert.Equal("From action", ((TestController)controllerContext.Controller).Log);
+ }
+
+ [Fact]
+ public void InvokeAction_OverrideFindAction()
+ {
+ // Arrange
+ ControllerContext controllerContext = GetControllerContext();
+ AsyncControllerActionInvoker invoker = new AsyncControllerActionInvokerWithCustomFindAction();
+
+ // Act
+ IAsyncResult asyncResult = invoker.BeginInvokeAction(controllerContext, actionName: "Non-ExistantAction", callback: null, state: null);
+ bool retVal = invoker.EndInvokeAction(asyncResult);
+
+ // Assert
+ Assert.True(retVal);
+ Assert.Equal("From action", ((TestController)controllerContext.Controller).Log);
+ }
+
+ [Fact]
+ public void InvokeAction_RequestValidationFails()
+ {
+ // Arrange
+ ControllerContext controllerContext = GetControllerContext(passesRequestValidation: false);
+ AsyncControllerActionInvoker invoker = new AsyncControllerActionInvoker();
+
+ // Act & assert
+ Assert.Throws<HttpRequestValidationException>(
+ delegate { invoker.BeginInvokeAction(controllerContext, "NormalAction", null, null); });
+ }
+
+ [Fact]
+ public void InvokeAction_ResultThrowsException_Handled()
+ {
+ // Arrange
+ ControllerContext controllerContext = GetControllerContext();
+ AsyncControllerActionInvoker invoker = new AsyncControllerActionInvoker();
+
+ // Act & assert
+ IAsyncResult asyncResult = invoker.BeginInvokeAction(controllerContext, "ResultThrowsExceptionAndIsHandled", null, null);
+ bool retVal = invoker.EndInvokeAction(asyncResult);
+
+ Assert.True(retVal);
+ Assert.Equal("From exception filter", ((TestController)controllerContext.Controller).Log);
+ }
+
+ [Fact]
+ public void InvokeAction_ResultThrowsException_NotHandled()
+ {
+ // Arrange
+ ControllerContext controllerContext = GetControllerContext();
+ AsyncControllerActionInvoker invoker = new AsyncControllerActionInvoker();
+
+ // Act & assert
+ IAsyncResult asyncResult = invoker.BeginInvokeAction(controllerContext, "ResultThrowsExceptionAndIsNotHandled", null, null);
+ Assert.Throws<Exception>(
+ delegate { invoker.EndInvokeAction(asyncResult); },
+ @"Some exception text.");
+ }
+
+ [Fact]
+ public void InvokeAction_ResultThrowsException_ThreadAbort()
+ {
+ // Arrange
+ ControllerContext controllerContext = GetControllerContext();
+ AsyncControllerActionInvoker invoker = new AsyncControllerActionInvoker();
+
+ // Act & assert
+ IAsyncResult asyncResult = invoker.BeginInvokeAction(controllerContext, "ResultCallsThreadAbort", null, null);
+ Assert.Throws<ThreadAbortException>(
+ delegate { invoker.EndInvokeAction(asyncResult); });
+ }
+
+ [Fact]
+ public void InvokeAction_ThrowsIfActionNameIsEmpty()
+ {
+ // Arrange
+ AsyncControllerActionInvoker invoker = new AsyncControllerActionInvoker();
+
+ // Act & assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { invoker.BeginInvokeAction(new ControllerContext(), "", null, null); }, "actionName");
+ }
+
+ [Fact]
+ public void InvokeAction_ThrowsIfActionNameIsNull()
+ {
+ // Arrange
+ AsyncControllerActionInvoker invoker = new AsyncControllerActionInvoker();
+
+ // Act & assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { invoker.BeginInvokeAction(new ControllerContext(), null, null, null); }, "actionName");
+ }
+
+ [Fact]
+ public void InvokeAction_ThrowsIfControllerContextIsNull()
+ {
+ // Arrange
+ AsyncControllerActionInvoker invoker = new AsyncControllerActionInvoker();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { invoker.BeginInvokeAction(null, "someAction", null, null); }, "controllerContext");
+ }
+
+ [Fact]
+ public void InvokeActionMethod_AsynchronousDescriptor()
+ {
+ // Arrange
+ ControllerContext controllerContext = new ControllerContext();
+ Dictionary<string, object> parameters = new Dictionary<string, object>();
+ IAsyncResult innerAsyncResult = new MockAsyncResult();
+ ActionResult expectedResult = new ViewResult();
+
+ Mock<AsyncActionDescriptor> mockActionDescriptor = new Mock<AsyncActionDescriptor>();
+ mockActionDescriptor.Setup(d => d.BeginExecute(controllerContext, parameters, It.IsAny<AsyncCallback>(), It.IsAny<object>())).Returns(innerAsyncResult);
+ mockActionDescriptor.Setup(d => d.EndExecute(innerAsyncResult)).Returns(expectedResult);
+
+ AsyncControllerActionInvoker invoker = new AsyncControllerActionInvoker();
+
+ // Act
+ IAsyncResult asyncResult = invoker.BeginInvokeActionMethod(controllerContext, mockActionDescriptor.Object, parameters, null, null);
+ ActionResult returnedResult = invoker.EndInvokeActionMethod(asyncResult);
+
+ // Assert
+ Assert.Equal(expectedResult, returnedResult);
+ }
+
+ [Fact]
+ public void InvokeActionMethod_SynchronousDescriptor()
+ {
+ // Arrange
+ ControllerContext controllerContext = new ControllerContext();
+ Dictionary<string, object> parameters = new Dictionary<string, object>();
+ ActionResult expectedResult = new ViewResult();
+
+ Mock<ActionDescriptor> mockActionDescriptor = new Mock<ActionDescriptor>();
+ mockActionDescriptor.Setup(d => d.Execute(controllerContext, parameters)).Returns(expectedResult);
+
+ AsyncControllerActionInvoker invoker = new AsyncControllerActionInvoker();
+
+ // Act
+ IAsyncResult asyncResult = invoker.BeginInvokeActionMethod(controllerContext, mockActionDescriptor.Object, parameters, null, null);
+ ActionResult returnedResult = invoker.EndInvokeActionMethod(asyncResult);
+
+ // Assert
+ Assert.Equal(expectedResult, returnedResult);
+ }
+
+ [Fact]
+ public void InvokeActionMethodFilterAsynchronously_NextInChainThrowsOnActionExecutedException_Handled()
+ {
+ // Arrange
+ ViewResult expectedResult = new ViewResult();
+
+ bool nextInChainWasCalled = false;
+ bool onActionExecutedWasCalled = false;
+
+ ActionExecutingContext preContext = GetActionExecutingContext();
+ ActionFilterImpl actionFilter = new ActionFilterImpl()
+ {
+ OnActionExecutedImpl = filterContext =>
+ {
+ onActionExecutedWasCalled = true;
+ Assert.NotNull(filterContext.Exception);
+ filterContext.ExceptionHandled = true;
+ filterContext.Result = expectedResult;
+ }
+ };
+
+ // Act & assert pre-execution
+ Func<ActionExecutedContext> continuation = AsyncControllerActionInvoker.InvokeActionMethodFilterAsynchronously(
+ actionFilter, preContext,
+ () => () =>
+ {
+ nextInChainWasCalled = true;
+ throw new Exception("Some exception text.");
+ });
+
+ Assert.False(onActionExecutedWasCalled);
+
+ // Act & assert post-execution
+ ActionExecutedContext postContext = continuation();
+
+ Assert.True(nextInChainWasCalled);
+ Assert.True(onActionExecutedWasCalled);
+ Assert.Equal(expectedResult, postContext.Result);
+ }
+
+ [Fact]
+ public void InvokeActionMethodFilterAsynchronously_NextInChainThrowsOnActionExecutedException_NotHandled()
+ {
+ // Arrange
+ ViewResult expectedResult = new ViewResult();
+
+ bool onActionExecutedWasCalled = false;
+
+ ActionExecutingContext preContext = GetActionExecutingContext();
+ ActionFilterImpl actionFilter = new ActionFilterImpl()
+ {
+ OnActionExecutedImpl = filterContext => { onActionExecutedWasCalled = true; }
+ };
+
+ // Act & assert
+ Func<ActionExecutedContext> continuation = AsyncControllerActionInvoker.InvokeActionMethodFilterAsynchronously(actionFilter, preContext,
+ () => () => { throw new Exception("Some exception text."); });
+
+ Assert.Throws<Exception>(
+ delegate { continuation(); },
+ @"Some exception text.");
+
+ // Assert
+ Assert.True(onActionExecutedWasCalled);
+ }
+
+ [Fact]
+ public void InvokeActionMethodFilterAsynchronously_NextInChainThrowsOnActionExecutedException_ThreadAbort()
+ {
+ // Arrange
+ ViewResult expectedResult = new ViewResult();
+
+ bool onActionExecutedWasCalled = false;
+
+ ActionExecutingContext preContext = GetActionExecutingContext();
+ ActionFilterImpl actionFilter = new ActionFilterImpl()
+ {
+ OnActionExecutedImpl = filterContext =>
+ {
+ onActionExecutedWasCalled = true;
+ Thread.ResetAbort();
+ }
+ };
+
+ // Act & assert
+ Func<ActionExecutedContext> continuation = AsyncControllerActionInvoker.InvokeActionMethodFilterAsynchronously(
+ actionFilter, preContext,
+ () => () =>
+ {
+ Thread.CurrentThread.Abort();
+ return null;
+ });
+
+ Assert.Throws<ThreadAbortException>(
+ delegate { continuation(); });
+
+ // Assert
+ Assert.True(onActionExecutedWasCalled);
+ }
+
+ [Fact]
+ public void InvokeActionMethodFilterAsynchronously_NextInChainThrowsOnActionExecutingException_Handled()
+ {
+ // Arrange
+ ViewResult expectedResult = new ViewResult();
+
+ bool nextInChainWasCalled = false;
+ bool onActionExecutingWasCalled = false;
+ bool onActionExecutedWasCalled = false;
+
+ ActionExecutingContext preContext = GetActionExecutingContext();
+ ActionFilterImpl actionFilter = new ActionFilterImpl()
+ {
+ OnActionExecutingImpl = filterContext => { onActionExecutingWasCalled = true; },
+ OnActionExecutedImpl = filterContext =>
+ {
+ onActionExecutedWasCalled = true;
+ Assert.NotNull(filterContext.Exception);
+ filterContext.ExceptionHandled = true;
+ filterContext.Result = expectedResult;
+ }
+ };
+
+ // Act
+ Func<ActionExecutedContext> continuation = AsyncControllerActionInvoker.InvokeActionMethodFilterAsynchronously(
+ actionFilter, preContext,
+ () =>
+ {
+ nextInChainWasCalled = true;
+ throw new Exception("Some exception text.");
+ });
+
+ // Assert
+ Assert.True(nextInChainWasCalled);
+ Assert.True(onActionExecutingWasCalled);
+ Assert.True(onActionExecutedWasCalled);
+
+ ActionExecutedContext postContext = continuation();
+ Assert.Equal(expectedResult, postContext.Result);
+ }
+
+ [Fact]
+ public void InvokeActionMethodFilterAsynchronously_NextInChainThrowsOnActionExecutingException_NotHandled()
+ {
+ // Arrange
+ ViewResult expectedResult = new ViewResult();
+
+ bool nextInChainWasCalled = false;
+ bool onActionExecutingWasCalled = false;
+ bool onActionExecutedWasCalled = false;
+
+ ActionExecutingContext preContext = GetActionExecutingContext();
+ ActionFilterImpl actionFilter = new ActionFilterImpl()
+ {
+ OnActionExecutingImpl = filterContext => { onActionExecutingWasCalled = true; },
+ OnActionExecutedImpl = filterContext => { onActionExecutedWasCalled = true; }
+ };
+
+ // Act & assert
+ Assert.Throws<Exception>(
+ delegate
+ {
+ AsyncControllerActionInvoker.InvokeActionMethodFilterAsynchronously(
+ actionFilter, preContext,
+ () =>
+ {
+ nextInChainWasCalled = true;
+ throw new Exception("Some exception text.");
+ });
+ },
+ @"Some exception text.");
+
+ // Assert
+ Assert.True(nextInChainWasCalled);
+ Assert.True(onActionExecutingWasCalled);
+ Assert.True(onActionExecutedWasCalled);
+ }
+
+ [Fact]
+ public void InvokeActionMethodFilterAsynchronously_NextInChainThrowsOnActionExecutingException_ThreadAbort()
+ {
+ // Arrange
+ ViewResult expectedResult = new ViewResult();
+
+ bool nextInChainWasCalled = false;
+ bool onActionExecutingWasCalled = false;
+ bool onActionExecutedWasCalled = false;
+
+ ActionExecutingContext preContext = GetActionExecutingContext();
+ ActionFilterImpl actionFilter = new ActionFilterImpl()
+ {
+ OnActionExecutingImpl = filterContext => { onActionExecutingWasCalled = true; },
+ OnActionExecutedImpl = filterContext =>
+ {
+ onActionExecutedWasCalled = true;
+ Thread.ResetAbort();
+ }
+ };
+
+ // Act & assert
+ Assert.Throws<ThreadAbortException>(
+ delegate
+ {
+ AsyncControllerActionInvoker.InvokeActionMethodFilterAsynchronously(
+ actionFilter, preContext,
+ () =>
+ {
+ nextInChainWasCalled = true;
+ Thread.CurrentThread.Abort();
+ return null;
+ });
+ });
+
+ // Assert
+ Assert.True(nextInChainWasCalled);
+ Assert.True(onActionExecutingWasCalled);
+ Assert.True(onActionExecutedWasCalled);
+ }
+
+ [Fact]
+ public void InvokeActionMethodFilterAsynchronously_NormalExecutionNotCanceled()
+ {
+ // Arrange
+ bool nextInChainWasCalled = false;
+ bool onActionExecutingWasCalled = false;
+ bool onActionExecutedWasCalled = false;
+
+ ActionExecutingContext preContext = GetActionExecutingContext();
+ ActionFilterImpl actionFilter = new ActionFilterImpl()
+ {
+ OnActionExecutingImpl = _ => { onActionExecutingWasCalled = true; },
+ OnActionExecutedImpl = _ => { onActionExecutedWasCalled = true; }
+ };
+
+ // Act
+ Func<ActionExecutedContext> continuation = AsyncControllerActionInvoker.InvokeActionMethodFilterAsynchronously(
+ actionFilter, preContext,
+ () =>
+ {
+ nextInChainWasCalled = true;
+ return () => new ActionExecutedContext();
+ });
+
+ // Assert
+ Assert.True(nextInChainWasCalled);
+ Assert.True(onActionExecutingWasCalled);
+ Assert.False(onActionExecutedWasCalled);
+
+ continuation();
+ Assert.True(onActionExecutedWasCalled);
+ }
+
+ [Fact]
+ public void InvokeActionMethodFilterAsynchronously_OnActionExecutingSetsResult()
+ {
+ // Arrange
+ ViewResult expectedResult = new ViewResult();
+
+ bool nextInChainWasCalled = false;
+ bool onActionExecutingWasCalled = false;
+ bool onActionExecutedWasCalled = false;
+
+ ActionExecutingContext preContext = GetActionExecutingContext();
+ ActionFilterImpl actionFilter = new ActionFilterImpl()
+ {
+ OnActionExecutingImpl = filterContext =>
+ {
+ onActionExecutingWasCalled = true;
+ filterContext.Result = expectedResult;
+ },
+ OnActionExecutedImpl = _ => { onActionExecutedWasCalled = true; }
+ };
+
+ // Act
+ Func<ActionExecutedContext> continuation = AsyncControllerActionInvoker.InvokeActionMethodFilterAsynchronously(
+ actionFilter, preContext,
+ () =>
+ {
+ nextInChainWasCalled = true;
+ return () => new ActionExecutedContext();
+ });
+
+ // Assert
+ Assert.False(nextInChainWasCalled);
+ Assert.True(onActionExecutingWasCalled);
+ Assert.False(onActionExecutedWasCalled);
+
+ ActionExecutedContext postContext = continuation();
+ Assert.False(onActionExecutedWasCalled);
+ Assert.Equal(expectedResult, postContext.Result);
+ }
+
+ [Fact]
+ public void InvokeActionMethodWithFilters()
+ {
+ // Arrange
+ List<string> actionLog = new List<string>();
+ ControllerContext controllerContext = new ControllerContext();
+ Dictionary<string, object> parameters = new Dictionary<string, object>();
+ MockAsyncResult innerAsyncResult = new MockAsyncResult();
+ ActionResult actionResult = new ViewResult();
+
+ ActionFilterImpl filter1 = new ActionFilterImpl()
+ {
+ OnActionExecutingImpl = delegate(ActionExecutingContext filterContext) { actionLog.Add("OnActionExecuting1"); },
+ OnActionExecutedImpl = delegate(ActionExecutedContext filterContext) { actionLog.Add("OnActionExecuted1"); }
+ };
+ ActionFilterImpl filter2 = new ActionFilterImpl()
+ {
+ OnActionExecutingImpl = delegate(ActionExecutingContext filterContext) { actionLog.Add("OnActionExecuting2"); },
+ OnActionExecutedImpl = delegate(ActionExecutedContext filterContext) { actionLog.Add("OnActionExecuted2"); }
+ };
+
+ Mock<AsyncActionDescriptor> mockActionDescriptor = new Mock<AsyncActionDescriptor>();
+ mockActionDescriptor.Setup(d => d.BeginExecute(controllerContext, parameters, It.IsAny<AsyncCallback>(), It.IsAny<object>())).Returns(innerAsyncResult);
+ mockActionDescriptor.Setup(d => d.EndExecute(innerAsyncResult)).Returns(actionResult);
+
+ AsyncControllerActionInvoker invoker = new AsyncControllerActionInvoker();
+ IActionFilter[] filters = new IActionFilter[] { filter1, filter2 };
+
+ // Act
+ IAsyncResult outerAsyncResult = invoker.BeginInvokeActionMethodWithFilters(controllerContext, filters, mockActionDescriptor.Object, parameters, null, null);
+ ActionExecutedContext postContext = invoker.EndInvokeActionMethodWithFilters(outerAsyncResult);
+
+ // Assert
+ Assert.Equal(new[] { "OnActionExecuting1", "OnActionExecuting2", "OnActionExecuted2", "OnActionExecuted1" }, actionLog.ToArray());
+ Assert.Equal(actionResult, postContext.Result);
+ }
+
+ [Fact]
+ public void InvokeActionMethodWithFilters_ShortCircuited()
+ {
+ // Arrange
+ List<string> actionLog = new List<string>();
+ ControllerContext controllerContext = new ControllerContext();
+ Dictionary<string, object> parameters = new Dictionary<string, object>();
+ ActionResult actionResult = new ViewResult();
+
+ ActionFilterImpl filter1 = new ActionFilterImpl()
+ {
+ OnActionExecutingImpl = delegate(ActionExecutingContext filterContext) { actionLog.Add("OnActionExecuting1"); },
+ OnActionExecutedImpl = delegate(ActionExecutedContext filterContext) { actionLog.Add("OnActionExecuted1"); }
+ };
+ ActionFilterImpl filter2 = new ActionFilterImpl()
+ {
+ OnActionExecutingImpl = delegate(ActionExecutingContext filterContext)
+ {
+ actionLog.Add("OnActionExecuting2");
+ filterContext.Result = actionResult;
+ },
+ OnActionExecutedImpl = delegate(ActionExecutedContext filterContext) { actionLog.Add("OnActionExecuted2"); }
+ };
+
+ Mock<AsyncActionDescriptor> mockActionDescriptor = new Mock<AsyncActionDescriptor>();
+ mockActionDescriptor.Setup(d => d.BeginExecute(controllerContext, parameters, It.IsAny<AsyncCallback>(), It.IsAny<object>())).Throws(new Exception("I shouldn't have been called."));
+ mockActionDescriptor.Setup(d => d.EndExecute(It.IsAny<IAsyncResult>())).Throws(new Exception("I shouldn't have been called."));
+
+ AsyncControllerActionInvoker invoker = new AsyncControllerActionInvoker();
+ IActionFilter[] filters = new IActionFilter[] { filter1, filter2 };
+
+ // Act
+ IAsyncResult outerAsyncResult = invoker.BeginInvokeActionMethodWithFilters(controllerContext, filters, mockActionDescriptor.Object, parameters, null, null);
+ ActionExecutedContext postContext = invoker.EndInvokeActionMethodWithFilters(outerAsyncResult);
+
+ // Assert
+ Assert.Equal(new[] { "OnActionExecuting1", "OnActionExecuting2", "OnActionExecuted1" }, actionLog.ToArray());
+ Assert.Equal(actionResult, postContext.Result);
+ }
+
+ private static ActionExecutingContext GetActionExecutingContext()
+ {
+ return new ActionExecutingContext(new ControllerContext(), new Mock<ActionDescriptor>().Object, new Dictionary<string, object>());
+ }
+
+ private static ControllerContext GetControllerContext(bool passesRequestValidation = true)
+ {
+ Mock<HttpContextBase> mockHttpContext = new Mock<HttpContextBase>();
+ if (passesRequestValidation)
+ {
+#pragma warning disable 618
+ mockHttpContext.Setup(o => o.Request.ValidateInput()).AtMostOnce();
+#pragma warning restore 618
+ }
+ else
+ {
+ mockHttpContext.Setup(o => o.Request.ValidateInput()).Throws(new HttpRequestValidationException());
+ }
+
+ return new ControllerContext()
+ {
+ Controller = new TestController(),
+ HttpContext = mockHttpContext.Object
+ };
+ }
+
+ private class ActionFilterImpl : IActionFilter, IResultFilter
+ {
+ public Action<ActionExecutingContext> OnActionExecutingImpl { get; set; }
+
+ public void OnActionExecuting(ActionExecutingContext filterContext)
+ {
+ if (OnActionExecutingImpl != null)
+ {
+ OnActionExecutingImpl(filterContext);
+ }
+ }
+
+ public Action<ActionExecutedContext> OnActionExecutedImpl { get; set; }
+
+ public void OnActionExecuted(ActionExecutedContext filterContext)
+ {
+ if (OnActionExecutedImpl != null)
+ {
+ OnActionExecutedImpl(filterContext);
+ }
+ }
+
+ public Action<ResultExecutingContext> OnResultExecutingImpl { get; set; }
+
+ public void OnResultExecuting(ResultExecutingContext filterContext)
+ {
+ if (OnResultExecutingImpl != null)
+ {
+ OnResultExecutingImpl(filterContext);
+ }
+ }
+
+ public Action<ResultExecutedContext> OnResultExecutedImpl { get; set; }
+
+ public void OnResultExecuted(ResultExecutedContext filterContext)
+ {
+ if (OnResultExecutedImpl != null)
+ {
+ OnResultExecutedImpl(filterContext);
+ }
+ }
+ }
+
+ public class AsyncControllerActionInvokerHelper : AsyncControllerActionInvoker
+ {
+ public AsyncControllerActionInvokerHelper()
+ {
+ DescriptorCache = new ControllerDescriptorCache();
+ }
+
+ protected override ControllerDescriptor GetControllerDescriptor(ControllerContext controllerContext)
+ {
+ return PublicGetControllerDescriptor(controllerContext);
+ }
+
+ public virtual ControllerDescriptor PublicGetControllerDescriptor(ControllerContext controllerContext)
+ {
+ return base.GetControllerDescriptor(controllerContext);
+ }
+
+ protected override ExceptionContext InvokeExceptionFilters(ControllerContext controllerContext, IList<IExceptionFilter> filters, Exception exception)
+ {
+ return PublicInvokeExceptionFilters(controllerContext, filters, exception);
+ }
+
+ public virtual ExceptionContext PublicInvokeExceptionFilters(ControllerContext controllerContext, IList<IExceptionFilter> filters, Exception exception)
+ {
+ return base.InvokeExceptionFilters(controllerContext, filters, exception);
+ }
+
+ protected override void InvokeActionResult(ControllerContext controllerContext, ActionResult actionResult)
+ {
+ PublicInvokeActionResult(controllerContext, actionResult);
+ }
+
+ public virtual void PublicInvokeActionResult(ControllerContext controllerContext, ActionResult actionResult)
+ {
+ base.InvokeActionResult(controllerContext, actionResult);
+ }
+
+ protected override FilterInfo GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
+ {
+ return PublicGetFilters(controllerContext, actionDescriptor);
+ }
+
+ public virtual FilterInfo PublicGetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
+ {
+ return base.GetFilters(controllerContext, actionDescriptor);
+ }
+
+ protected override AuthorizationContext InvokeAuthorizationFilters(ControllerContext controllerContext, IList<IAuthorizationFilter> filters, ActionDescriptor actionDescriptor)
+ {
+ return PublicInvokeAuthorizationFilters(controllerContext, filters, actionDescriptor);
+ }
+
+ public virtual AuthorizationContext PublicInvokeAuthorizationFilters(ControllerContext controllerContext, IList<IAuthorizationFilter> filters, ActionDescriptor actionDescriptor)
+ {
+ return base.InvokeAuthorizationFilters(controllerContext, filters, actionDescriptor);
+ }
+
+ protected override ActionDescriptor FindAction(ControllerContext controllerContext, ControllerDescriptor controllerDescriptor, string actionName)
+ {
+ return PublicFindAction(controllerContext, controllerDescriptor, actionName);
+ }
+
+ public virtual ActionDescriptor PublicFindAction(ControllerContext controllerContext, ControllerDescriptor controllerDescriptor, string actionName)
+ {
+ return base.FindAction(controllerContext, controllerDescriptor, actionName);
+ }
+
+ protected override IDictionary<string, object> GetParameterValues(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
+ {
+ return PublicGetParameterValues(controllerContext, actionDescriptor);
+ }
+
+ public virtual IDictionary<string, object> PublicGetParameterValues(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
+ {
+ return base.GetParameterValues(controllerContext, actionDescriptor);
+ }
+ }
+
+ public class AsyncControllerActionInvokerWithCustomFindAction : AsyncControllerActionInvoker
+ {
+ protected override ActionDescriptor FindAction(ControllerContext controllerContext, ControllerDescriptor controllerDescriptor, string actionName)
+ {
+ return base.FindAction(controllerContext, controllerDescriptor, "NormalAction");
+ }
+ }
+
+ [ResetThreadAbort]
+ private class TestController : AsyncController
+ {
+ public string Log;
+
+ public ActionResult ActionCallsThreadAbortAsync()
+ {
+ Thread.CurrentThread.Abort();
+ return null;
+ }
+
+ public ActionResult ActionCallsThreadAbortCompleted()
+ {
+ return null;
+ }
+
+ public ActionResult ResultCallsThreadAbort()
+ {
+ return new ActionResultWhichCallsThreadAbort();
+ }
+
+ public ActionResult NormalAction()
+ {
+ return new LoggingActionResult("From action");
+ }
+
+ [AuthorizationFilterReturnsResult]
+ public void AuthorizationFilterShortCircuits()
+ {
+ }
+
+ [CustomExceptionFilterHandlesError]
+ public void ActionThrowsExceptionAndIsHandledAsync()
+ {
+ throw new Exception("Some exception text.");
+ }
+
+ public void ActionThrowsExceptionAndIsHandledCompleted()
+ {
+ }
+
+ [CustomExceptionFilterDoesNotHandleError]
+ public void ActionThrowsExceptionAndIsNotHandledAsync()
+ {
+ throw new Exception("Some exception text.");
+ }
+
+ public void ActionThrowsExceptionAndIsNotHandledCompleted()
+ {
+ }
+
+ [CustomExceptionFilterHandlesError]
+ public ActionResult ResultThrowsExceptionAndIsHandled()
+ {
+ return new ActionResultWhichThrowsException();
+ }
+
+ [CustomExceptionFilterDoesNotHandleError]
+ public ActionResult ResultThrowsExceptionAndIsNotHandled()
+ {
+ return new ActionResultWhichThrowsException();
+ }
+
+ private class AuthorizationFilterReturnsResultAttribute : FilterAttribute, IAuthorizationFilter
+ {
+ public void OnAuthorization(AuthorizationContext filterContext)
+ {
+ filterContext.Result = new LoggingActionResult("From authorization filter");
+ }
+ }
+
+ private class CustomExceptionFilterDoesNotHandleErrorAttribute : FilterAttribute, IExceptionFilter
+ {
+ public void OnException(ExceptionContext filterContext)
+ {
+ }
+ }
+
+ private class CustomExceptionFilterHandlesErrorAttribute : FilterAttribute, IExceptionFilter
+ {
+ public void OnException(ExceptionContext filterContext)
+ {
+ filterContext.ExceptionHandled = true;
+ filterContext.Result = new LoggingActionResult("From exception filter");
+ }
+ }
+
+ private class ActionResultWhichCallsThreadAbort : ActionResult
+ {
+ public override void ExecuteResult(ControllerContext context)
+ {
+ Thread.CurrentThread.Abort();
+ }
+ }
+
+ private class ActionResultWhichThrowsException : ActionResult
+ {
+ public override void ExecuteResult(ControllerContext context)
+ {
+ throw new Exception("Some exception text.");
+ }
+ }
+ }
+
+ private class ResetThreadAbortAttribute : ActionFilterAttribute
+ {
+ public override void OnActionExecuted(ActionExecutedContext filterContext)
+ {
+ try
+ {
+ Thread.ResetAbort();
+ }
+ catch (ThreadStateException)
+ {
+ // thread wasn't being aborted
+ }
+ }
+
+ public override void OnResultExecuted(ResultExecutedContext filterContext)
+ {
+ try
+ {
+ Thread.ResetAbort();
+ }
+ catch (ThreadStateException)
+ {
+ // thread wasn't being aborted
+ }
+ }
+ }
+
+ private class LoggingActionResult : ActionResult
+ {
+ private readonly string _logText;
+
+ public LoggingActionResult(string logText)
+ {
+ _logText = logText;
+ }
+
+ public override void ExecuteResult(ControllerContext context)
+ {
+ ((TestController)context.Controller).Log = _logText;
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Async/Test/AsyncManagerTest.cs b/test/System.Web.Mvc.Test/Async/Test/AsyncManagerTest.cs
new file mode 100644
index 00000000..f57651a0
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Async/Test/AsyncManagerTest.cs
@@ -0,0 +1,113 @@
+using System.Threading;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Async.Test
+{
+ public class AsyncManagerTest
+ {
+ [Fact]
+ public void FinishEvent_ExplicitCallToFinishMethod()
+ {
+ // Arrange
+ AsyncManager helper = new AsyncManager();
+
+ bool delegateCalled = false;
+ helper.Finished += delegate { delegateCalled = true; };
+
+ // Act
+ helper.Finish();
+
+ // Assert
+ Assert.True(delegateCalled);
+ }
+
+ [Fact]
+ public void FinishEvent_LinkedToOutstandingOperationsCompletedEvent()
+ {
+ // Arrange
+ AsyncManager helper = new AsyncManager();
+
+ bool delegateCalled = false;
+ helper.Finished += delegate { delegateCalled = true; };
+
+ // Act
+ helper.OutstandingOperations.Increment();
+ helper.OutstandingOperations.Decrement();
+
+ // Assert
+ Assert.True(delegateCalled);
+ }
+
+ [Fact]
+ public void OutstandingOperationsProperty()
+ {
+ // Act
+ AsyncManager helper = new AsyncManager();
+
+ // Assert
+ Assert.NotNull(helper.OutstandingOperations);
+ }
+
+ [Fact]
+ public void ParametersProperty()
+ {
+ // Act
+ AsyncManager helper = new AsyncManager();
+
+ // Assert
+ Assert.NotNull(helper.Parameters);
+ }
+
+ [Fact]
+ public void Sync()
+ {
+ // Arrange
+ Mock<SynchronizationContext> mockSyncContext = new Mock<SynchronizationContext>();
+ mockSyncContext
+ .Setup(c => c.Send(It.IsAny<SendOrPostCallback>(), null))
+ .Callback(
+ delegate(SendOrPostCallback d, object state) { d(state); });
+
+ AsyncManager helper = new AsyncManager(mockSyncContext.Object);
+ bool wasCalled = false;
+
+ // Act
+ helper.Sync(() => { wasCalled = true; });
+
+ // Assert
+ Assert.True(wasCalled);
+ }
+
+ [Fact]
+ public void TimeoutProperty()
+ {
+ // Arrange
+ int setValue = 50;
+ AsyncManager helper = new AsyncManager();
+
+ // Act
+ int defaultTimeout = helper.Timeout;
+ helper.Timeout = setValue;
+ int newTimeout = helper.Timeout;
+
+ // Assert
+ Assert.Equal(45000, defaultTimeout);
+ Assert.Equal(setValue, newTimeout);
+ }
+
+ [Fact]
+ public void TimeoutPropertyThrowsIfDurationIsOutOfRange()
+ {
+ // Arrange
+ int timeout = -30;
+ AsyncManager helper = new AsyncManager();
+
+ // Act & assert
+ Assert.ThrowsArgumentOutOfRange(
+ delegate { helper.Timeout = timeout; }, "value",
+ @"The timeout value must be non-negative or Timeout.Infinite.");
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Async/Test/AsyncResultWrapperTest.cs b/test/System.Web.Mvc.Test/Async/Test/AsyncResultWrapperTest.cs
new file mode 100644
index 00000000..2323516d
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Async/Test/AsyncResultWrapperTest.cs
@@ -0,0 +1,228 @@
+using System.Threading;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Async.Test
+{
+ public class AsyncResultWrapperTest
+ {
+ [Fact]
+ public void Begin_AsynchronousCompletion()
+ {
+ // Arrange
+ AsyncCallback capturedCallback = null;
+ IAsyncResult resultGivenToCallback = null;
+ IAsyncResult innerResult = new MockAsyncResult();
+
+ // Act
+ IAsyncResult outerResult = AsyncResultWrapper.Begin(
+ ar => { resultGivenToCallback = ar; },
+ null,
+ (callback, state) =>
+ {
+ capturedCallback = callback;
+ return innerResult;
+ },
+ ar => { });
+
+ capturedCallback(innerResult);
+
+ // Assert
+ Assert.Equal(outerResult, resultGivenToCallback);
+ }
+
+ [Fact]
+ public void Begin_ReturnsAsyncResultWhichWrapsInnerResult()
+ {
+ // Arrange
+ IAsyncResult innerResult = new MockAsyncResult()
+ {
+ AsyncState = "inner state",
+ CompletedSynchronously = true,
+ IsCompleted = true
+ };
+
+ // Act
+ IAsyncResult outerResult = AsyncResultWrapper.Begin(
+ null, "outer state",
+ (callback, state) => innerResult,
+ ar => { });
+
+ // Assert
+ Assert.Equal(innerResult.AsyncState, outerResult.AsyncState);
+ Assert.Equal(innerResult.AsyncWaitHandle, outerResult.AsyncWaitHandle);
+ Assert.Equal(innerResult.CompletedSynchronously, outerResult.CompletedSynchronously);
+ Assert.Equal(innerResult.IsCompleted, outerResult.IsCompleted);
+ }
+
+ [Fact]
+ public void Begin_SynchronousCompletion()
+ {
+ // Arrange
+ IAsyncResult resultGivenToCallback = null;
+ IAsyncResult innerResult = new MockAsyncResult();
+
+ // Act
+ IAsyncResult outerResult = AsyncResultWrapper.Begin(
+ ar => { resultGivenToCallback = ar; },
+ null,
+ (callback, state) =>
+ {
+ callback(innerResult);
+ return innerResult;
+ },
+ ar => { });
+
+ // Assert
+ Assert.Equal(outerResult, resultGivenToCallback);
+ }
+
+ [Fact]
+ public void Begin_AsynchronousButAlreadyCompleted()
+ {
+ // Arrange
+ Mock<IAsyncResult> innerResultMock = new Mock<IAsyncResult>();
+ innerResultMock.Setup(ir => ir.CompletedSynchronously).Returns(false);
+ innerResultMock.Setup(ir => ir.IsCompleted).Returns(true);
+
+ // Act
+ IAsyncResult outerResult = AsyncResultWrapper.Begin(
+ null,
+ null,
+ (callback, state) =>
+ {
+ callback(innerResultMock.Object);
+ return innerResultMock.Object;
+ },
+ ar => { });
+
+ // Assert
+ Assert.True(outerResult.CompletedSynchronously);
+ }
+
+ [Fact]
+ public void BeginSynchronous_Action()
+ {
+ // Arrange
+ bool actionCalled = false;
+
+ // Act
+ IAsyncResult asyncResult = AsyncResultWrapper.BeginSynchronous(null, null, delegate { actionCalled = true; });
+ AsyncResultWrapper.End(asyncResult);
+
+ // Assert
+ Assert.True(actionCalled);
+ Assert.True(asyncResult.IsCompleted);
+ Assert.True(asyncResult.CompletedSynchronously);
+ }
+
+ [Fact]
+ public void BeginSynchronous_Func()
+ {
+ // Act
+ IAsyncResult asyncResult = AsyncResultWrapper.BeginSynchronous(null, null, () => 42);
+ int retVal = AsyncResultWrapper.End<int>(asyncResult);
+
+ // Assert
+ Assert.Equal(42, retVal);
+ Assert.True(asyncResult.IsCompleted);
+ Assert.True(asyncResult.CompletedSynchronously);
+ }
+
+ [Fact]
+ public void End_ExecutesStoredDelegateAndReturnsValue()
+ {
+ // Arrange
+ IAsyncResult asyncResult = AsyncResultWrapper.Begin(
+ null, null,
+ (callback, state) => new MockAsyncResult(),
+ ar => 42);
+
+ // Act
+ int returned = AsyncResultWrapper.End<int>(asyncResult);
+
+ // Assert
+ Assert.Equal(42, returned);
+ }
+
+ [Fact]
+ public void End_ThrowsIfAsyncResultIsIncorrectType()
+ {
+ // Arrange
+ IAsyncResult asyncResult = AsyncResultWrapper.Begin(
+ null, null,
+ (callback, state) => new MockAsyncResult(),
+ ar => { });
+
+ // Act & assert
+ Assert.Throws<ArgumentException>(
+ delegate { AsyncResultWrapper.End<int>(asyncResult); },
+ @"The provided IAsyncResult is not valid for this method.
+Parameter name: asyncResult");
+ }
+
+ [Fact]
+ public void End_ThrowsIfAsyncResultIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { AsyncResultWrapper.End(null); }, "asyncResult");
+ }
+
+ [Fact]
+ public void End_ThrowsIfAsyncResultTagMismatch()
+ {
+ // Arrange
+ IAsyncResult asyncResult = AsyncResultWrapper.Begin(
+ null, null,
+ (callback, state) => new MockAsyncResult(),
+ ar => { },
+ "some tag");
+
+ // Act & assert
+ Assert.Throws<ArgumentException>(
+ delegate { AsyncResultWrapper.End(asyncResult, "some other tag"); },
+ @"The provided IAsyncResult is not valid for this method.
+Parameter name: asyncResult");
+ }
+
+ [Fact]
+ public void End_ThrowsIfCalledTwiceOnSameAsyncResult()
+ {
+ // Arrange
+ IAsyncResult asyncResult = AsyncResultWrapper.Begin(
+ null, null,
+ (callback, state) => new MockAsyncResult(),
+ ar => { });
+
+ // Act & assert
+ AsyncResultWrapper.End(asyncResult);
+ Assert.Throws<InvalidOperationException>(
+ delegate { AsyncResultWrapper.End(asyncResult); },
+ @"The provided IAsyncResult has already been consumed.");
+ }
+
+ [Fact]
+ public void TimedOut()
+ {
+ // Arrange
+ ManualResetEvent waitHandle = new ManualResetEvent(false /* initialState */);
+
+ AsyncCallback callback = ar => { waitHandle.Set(); };
+
+ // Act & assert
+ IAsyncResult asyncResult = AsyncResultWrapper.Begin(
+ callback, null,
+ (innerCallback, innerState) => new MockAsyncResult(),
+ ar => { Assert.True(false, "This callback should never execute since we timed out."); },
+ null, 0);
+
+ // wait for the timeout
+ waitHandle.WaitOne();
+
+ Assert.Throws<TimeoutException>(
+ delegate { AsyncResultWrapper.End(asyncResult); });
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Async/Test/AsyncUtilTest.cs b/test/System.Web.Mvc.Test/Async/Test/AsyncUtilTest.cs
new file mode 100644
index 00000000..b4675f9f
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Async/Test/AsyncUtilTest.cs
@@ -0,0 +1,98 @@
+using System.Threading;
+using Xunit;
+
+namespace System.Web.Mvc.Async.Test
+{
+ public class AsyncUtilTest
+ {
+ [Fact]
+ public void WrapCallbackForSynchronizedExecution_CallsSyncIfOperationCompletedAsynchronously()
+ {
+ // Arrange
+ MockAsyncResult asyncResult = new MockAsyncResult()
+ {
+ CompletedSynchronously = false,
+ IsCompleted = true
+ };
+
+ bool originalCallbackCalled = false;
+ AsyncCallback originalCallback = ar =>
+ {
+ Assert.Equal(asyncResult, ar);
+ originalCallbackCalled = true;
+ };
+
+ DummySynchronizationContext syncContext = new DummySynchronizationContext();
+
+ // Act
+ AsyncCallback retVal = AsyncUtil.WrapCallbackForSynchronizedExecution(originalCallback, syncContext);
+ retVal(asyncResult);
+
+ // Assert
+ Assert.True(originalCallbackCalled);
+ Assert.True(syncContext.SendCalled);
+ }
+
+ [Fact]
+ public void WrapCallbackForSynchronizedExecution_DoesNotCallSyncIfOperationCompletedSynchronously()
+ {
+ // Arrange
+ MockAsyncResult asyncResult = new MockAsyncResult()
+ {
+ CompletedSynchronously = true,
+ IsCompleted = true
+ };
+
+ bool originalCallbackCalled = false;
+ AsyncCallback originalCallback = ar =>
+ {
+ Assert.Equal(asyncResult, ar);
+ originalCallbackCalled = true;
+ };
+
+ DummySynchronizationContext syncContext = new DummySynchronizationContext();
+
+ // Act
+ AsyncCallback retVal = AsyncUtil.WrapCallbackForSynchronizedExecution(originalCallback, syncContext);
+ retVal(asyncResult);
+
+ // Assert
+ Assert.True(originalCallbackCalled);
+ Assert.False(syncContext.SendCalled);
+ }
+
+ [Fact]
+ public void WrapCallbackForSynchronizedExecution_ReturnsNullIfCallbackIsNull()
+ {
+ // Act
+ AsyncCallback retVal = AsyncUtil.WrapCallbackForSynchronizedExecution(null, new SynchronizationContext());
+
+ // Assert
+ Assert.Null(retVal);
+ }
+
+ [Fact]
+ public void WrapCallbackForSynchronizedExecution_ReturnsOriginalCallbackIfSyncContextIsNull()
+ {
+ // Arrange
+ AsyncCallback originalCallback = _ => { };
+
+ // Act
+ AsyncCallback retVal = AsyncUtil.WrapCallbackForSynchronizedExecution(originalCallback, null);
+
+ // Assert
+ Assert.Same(originalCallback, retVal);
+ }
+
+ private class DummySynchronizationContext : SynchronizationContext
+ {
+ public bool SendCalled { get; private set; }
+
+ public override void Send(SendOrPostCallback d, object state)
+ {
+ SendCalled = true;
+ base.Send(d, state);
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Async/Test/MockAsyncResult.cs b/test/System.Web.Mvc.Test/Async/Test/MockAsyncResult.cs
new file mode 100644
index 00000000..954e29ea
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Async/Test/MockAsyncResult.cs
@@ -0,0 +1,45 @@
+using System.Threading;
+
+namespace System.Web.Mvc.Async.Test
+{
+ public class MockAsyncResult : IAsyncResult
+ {
+ private volatile object _asyncState;
+ private volatile ManualResetEvent _asyncWaitHandle = new ManualResetEvent(false);
+ private volatile bool _completedSynchronously;
+ private volatile bool _isCompleted;
+
+ public object AsyncState
+ {
+ get { return _asyncState; }
+ set { _asyncState = value; }
+ }
+
+ public ManualResetEvent AsyncWaitHandle
+ {
+ get { return _asyncWaitHandle; }
+ set { _asyncWaitHandle = value; }
+ }
+
+ public bool CompletedSynchronously
+ {
+ get { return _completedSynchronously; }
+ set { _completedSynchronously = value; }
+ }
+
+ public bool IsCompleted
+ {
+ get { return _isCompleted; }
+ set { _isCompleted = value; }
+ }
+
+ #region IAsyncResult Members
+
+ WaitHandle IAsyncResult.AsyncWaitHandle
+ {
+ get { return _asyncWaitHandle; }
+ }
+
+ #endregion
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Async/Test/OperationCounterTest.cs b/test/System.Web.Mvc.Test/Async/Test/OperationCounterTest.cs
new file mode 100644
index 00000000..90827f9b
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Async/Test/OperationCounterTest.cs
@@ -0,0 +1,110 @@
+using Xunit;
+
+namespace System.Web.Mvc.Async.Test
+{
+ public class OperationCounterTest
+ {
+ [Fact]
+ public void CompletedEvent()
+ {
+ // Arrange
+ bool premature = true;
+ bool eventFired = false;
+ OperationCounter ops = new OperationCounter();
+ ops.Completed += (sender, eventArgs) =>
+ {
+ if (premature)
+ {
+ Assert.True(false, "Event fired too early!");
+ }
+ if (eventFired)
+ {
+ Assert.True(false, "Event fired multiple times.");
+ }
+
+ Assert.Equal(ops, sender);
+ Assert.Equal(eventArgs, EventArgs.Empty);
+ eventFired = true;
+ };
+
+ // Act & assert
+ ops.Increment(); // should not fire event (will throw exception)
+ premature = false;
+
+ ops.Decrement(); // should fire event
+ Assert.True(eventFired);
+
+ ops.Increment(); // should not fire event (will throw exception)
+ }
+
+ [Fact]
+ public void CountStartsAtZero()
+ {
+ // Arrange
+ OperationCounter ops = new OperationCounter();
+
+ // Act & assert
+ Assert.Equal(0, ops.Count);
+ }
+
+ [Fact]
+ public void DecrementWithIntegerArgument()
+ {
+ // Arrange
+ OperationCounter ops = new OperationCounter();
+
+ // Act
+ int returned = ops.Decrement(3);
+ int newCount = ops.Count;
+
+ // Assert
+ Assert.Equal(-3, returned);
+ Assert.Equal(-3, newCount);
+ }
+
+ [Fact]
+ public void DecrementWithNoArguments()
+ {
+ // Arrange
+ OperationCounter ops = new OperationCounter();
+
+ // Act
+ int returned = ops.Decrement();
+ int newCount = ops.Count;
+
+ // Assert
+ Assert.Equal(-1, returned);
+ Assert.Equal(-1, newCount);
+ }
+
+ [Fact]
+ public void IncrementWithIntegerArgument()
+ {
+ // Arrange
+ OperationCounter ops = new OperationCounter();
+
+ // Act
+ int returned = ops.Increment(3);
+ int newCount = ops.Count;
+
+ // Assert
+ Assert.Equal(3, returned);
+ Assert.Equal(3, newCount);
+ }
+
+ [Fact]
+ public void IncrementWithNoArguments()
+ {
+ // Arrange
+ OperationCounter ops = new OperationCounter();
+
+ // Act
+ int returned = ops.Increment();
+ int newCount = ops.Count;
+
+ // Assert
+ Assert.Equal(1, returned);
+ Assert.Equal(1, newCount);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Async/Test/ReflectedAsyncActionDescriptorTest.cs b/test/System.Web.Mvc.Test/Async/Test/ReflectedAsyncActionDescriptorTest.cs
new file mode 100644
index 00000000..1420d47c
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Async/Test/ReflectedAsyncActionDescriptorTest.cs
@@ -0,0 +1,314 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Reflection;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Async.Test
+{
+ public class ReflectedAsyncActionDescriptorTest
+ {
+ private readonly MethodInfo _asyncMethod = typeof(ExecuteController).GetMethod("FooAsync");
+ private readonly MethodInfo _completedMethod = typeof(ExecuteController).GetMethod("FooCompleted");
+
+ [Fact]
+ public void Constructor_SetsProperties()
+ {
+ // Arrange
+ string actionName = "SomeAction";
+ ControllerDescriptor cd = new Mock<ControllerDescriptor>().Object;
+
+ // Act
+ ReflectedAsyncActionDescriptor ad = new ReflectedAsyncActionDescriptor(_asyncMethod, _completedMethod, actionName, cd);
+
+ // Assert
+ Assert.Equal(_asyncMethod, ad.AsyncMethodInfo);
+ Assert.Equal(_completedMethod, ad.CompletedMethodInfo);
+ Assert.Equal(actionName, ad.ActionName);
+ Assert.Equal(cd, ad.ControllerDescriptor);
+ }
+
+ [Fact]
+ public void Constructor_ThrowsIfActionNameIsEmpty()
+ {
+ // Arrange
+ ControllerDescriptor cd = new Mock<ControllerDescriptor>().Object;
+
+ // Act & assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { new ReflectedAsyncActionDescriptor(_asyncMethod, _completedMethod, "", cd); }, "actionName");
+ }
+
+ [Fact]
+ public void Constructor_ThrowsIfActionNameIsNull()
+ {
+ // Arrange
+ ControllerDescriptor cd = new Mock<ControllerDescriptor>().Object;
+
+ // Act & assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { new ReflectedAsyncActionDescriptor(_asyncMethod, _completedMethod, null, cd); }, "actionName");
+ }
+
+ [Fact]
+ public void Constructor_ThrowsIfAsyncMethodInfoIsInvalid()
+ {
+ // Arrange
+ ControllerDescriptor cd = new Mock<ControllerDescriptor>().Object;
+ MethodInfo getHashCodeMethod = typeof(object).GetMethod("GetHashCode");
+
+ // Act & assert
+ Assert.Throws<ArgumentException>(
+ delegate { new ReflectedAsyncActionDescriptor(getHashCodeMethod, _completedMethod, "SomeAction", cd); },
+ @"Cannot create a descriptor for instance method 'Int32 GetHashCode()' on type 'System.Object' because the type does not derive from ControllerBase.
+Parameter name: asyncMethodInfo");
+ }
+
+ [Fact]
+ public void Constructor_ThrowsIfAsyncMethodInfoIsNull()
+ {
+ // Arrange
+ ControllerDescriptor cd = new Mock<ControllerDescriptor>().Object;
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new ReflectedAsyncActionDescriptor(null, _completedMethod, "SomeAction", cd); }, "asyncMethodInfo");
+ }
+
+ [Fact]
+ public void Constructor_ThrowsIfCompletedMethodInfoIsInvalid()
+ {
+ // Arrange
+ ControllerDescriptor cd = new Mock<ControllerDescriptor>().Object;
+ MethodInfo getHashCodeMethod = typeof(object).GetMethod("GetHashCode");
+
+ // Act & assert
+ Assert.Throws<ArgumentException>(
+ delegate { new ReflectedAsyncActionDescriptor(_asyncMethod, getHashCodeMethod, "SomeAction", cd); },
+ @"Cannot create a descriptor for instance method 'Int32 GetHashCode()' on type 'System.Object' because the type does not derive from ControllerBase.
+Parameter name: completedMethodInfo");
+ }
+
+ [Fact]
+ public void Constructor_ThrowsIfCompletedMethodInfoIsNull()
+ {
+ // Arrange
+ ControllerDescriptor cd = new Mock<ControllerDescriptor>().Object;
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new ReflectedAsyncActionDescriptor(_asyncMethod, null, "SomeAction", cd); }, "completedMethodInfo");
+ }
+
+ [Fact]
+ public void Constructor_ThrowsIfControllerDescriptorIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new ReflectedAsyncActionDescriptor(_asyncMethod, _completedMethod, "SomeAction", null); }, "controllerDescriptor");
+ }
+
+ [Fact]
+ public void Execute()
+ {
+ // Arrange
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.Setup(c => c.Controller).Returns(new ExecuteController());
+ ControllerContext controllerContext = mockControllerContext.Object;
+
+ Dictionary<string, object> parameters = new Dictionary<string, object>()
+ {
+ { "id1", 42 }
+ };
+
+ ReflectedAsyncActionDescriptor ad = GetActionDescriptor(_asyncMethod, _completedMethod);
+
+ SignalContainer<object> resultContainer = new SignalContainer<object>();
+ AsyncCallback callback = ar =>
+ {
+ object o = ad.EndExecute(ar);
+ resultContainer.Signal(o);
+ };
+
+ // Act
+ ad.BeginExecute(controllerContext, parameters, callback, null);
+ object retVal = resultContainer.Wait();
+
+ // Assert
+ Assert.Equal("Hello world: 42", retVal);
+ }
+
+ [Fact]
+ public void Execute_ThrowsIfControllerContextIsNull()
+ {
+ // Arrange
+ ReflectedAsyncActionDescriptor ad = GetActionDescriptor(_asyncMethod, _completedMethod);
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { ad.BeginExecute(null, new Dictionary<string, object>(), null, null); }, "controllerContext");
+ }
+
+ [Fact]
+ public void Execute_ThrowsIfControllerIsNotAsyncManagerContainer()
+ {
+ // Arrange
+ ReflectedAsyncActionDescriptor ad = GetActionDescriptor(_asyncMethod, _completedMethod);
+ ControllerContext controllerContext = new ControllerContext()
+ {
+ Controller = new RegularSyncController()
+ };
+
+ // Act & assert
+ Assert.Throws<InvalidOperationException>(
+ delegate { ad.BeginExecute(controllerContext, new Dictionary<string, object>(), null, null); },
+ @"The controller of type 'System.Web.Mvc.Async.Test.ReflectedAsyncActionDescriptorTest+RegularSyncController' must subclass AsyncController or implement the IAsyncManagerContainer interface.");
+ }
+
+ [Fact]
+ public void Execute_ThrowsIfParametersIsNull()
+ {
+ // Arrange
+ ReflectedAsyncActionDescriptor ad = GetActionDescriptor(_asyncMethod, _completedMethod);
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { ad.BeginExecute(new ControllerContext(), null, null, null); }, "parameters");
+ }
+
+ [Fact]
+ public void GetCustomAttributes()
+ {
+ // Arrange
+ ReflectedAsyncActionDescriptor ad = GetActionDescriptor(_asyncMethod, _completedMethod);
+
+ // Act
+ object[] attributes = ad.GetCustomAttributes(true /* inherit */);
+
+ // Assert
+ Assert.Single(attributes);
+ Assert.Equal(typeof(AuthorizeAttribute), attributes[0].GetType());
+ }
+
+ [Fact]
+ public void GetCustomAttributes_FilterByType()
+ {
+ // Shouldn't match attributes on the Completed() method, only the Async() method
+
+ // Arrange
+ ReflectedAsyncActionDescriptor ad = GetActionDescriptor(_asyncMethod, _completedMethod);
+
+ // Act
+ object[] attributes = ad.GetCustomAttributes(typeof(OutputCacheAttribute), true /* inherit */);
+
+ // Assert
+ Assert.Empty(attributes);
+ }
+
+ [Fact]
+ public void GetParameters()
+ {
+ // Arrange
+ ParameterInfo pInfo = _asyncMethod.GetParameters()[0];
+ ReflectedAsyncActionDescriptor ad = GetActionDescriptor(_asyncMethod, _completedMethod);
+
+ // Act
+ ParameterDescriptor[] pDescsFirstCall = ad.GetParameters();
+ ParameterDescriptor[] pDescsSecondCall = ad.GetParameters();
+
+ // Assert
+ Assert.NotSame(pDescsFirstCall, pDescsSecondCall);
+ Assert.Equal(pDescsFirstCall, pDescsSecondCall);
+ Assert.Single(pDescsFirstCall);
+
+ ReflectedParameterDescriptor pDesc = pDescsFirstCall[0] as ReflectedParameterDescriptor;
+
+ Assert.NotNull(pDesc);
+ Assert.Same(ad, pDesc.ActionDescriptor);
+ Assert.Same(pInfo, pDesc.ParameterInfo);
+ }
+
+ [Fact]
+ public void GetSelectors()
+ {
+ // Arrange
+ ControllerContext controllerContext = new Mock<ControllerContext>().Object;
+ Mock<MethodInfo> mockMethod = new Mock<MethodInfo>();
+
+ Mock<ActionMethodSelectorAttribute> mockAttr = new Mock<ActionMethodSelectorAttribute>();
+ mockAttr.Setup(attr => attr.IsValidForRequest(controllerContext, mockMethod.Object)).Returns(true).Verifiable();
+ mockMethod.Setup(m => m.GetCustomAttributes(typeof(ActionMethodSelectorAttribute), true)).Returns(new ActionMethodSelectorAttribute[] { mockAttr.Object });
+
+ ReflectedAsyncActionDescriptor ad = GetActionDescriptor(mockMethod.Object, _completedMethod);
+
+ // Act
+ ICollection<ActionSelector> selectors = ad.GetSelectors();
+ bool executedSuccessfully = selectors.All(s => s(controllerContext));
+
+ // Assert
+ Assert.Single(selectors);
+ Assert.True(executedSuccessfully);
+ mockAttr.Verify();
+ }
+
+ [Fact]
+ public void IsDefined()
+ {
+ // Arrange
+ ReflectedAsyncActionDescriptor ad = GetActionDescriptor(_asyncMethod, _completedMethod);
+
+ // Act
+ bool isDefined = ad.IsDefined(typeof(AuthorizeAttribute), true /* inherit */);
+
+ // Assert
+ Assert.True(isDefined);
+ }
+
+ private static ReflectedAsyncActionDescriptor GetActionDescriptor(MethodInfo asyncMethod, MethodInfo completedMethod)
+ {
+ return new ReflectedAsyncActionDescriptor(asyncMethod, completedMethod, "someName", new Mock<ControllerDescriptor>().Object, false /* validateMethod */)
+ {
+ DispatcherCache = new ActionMethodDispatcherCache()
+ };
+ }
+
+ private class ExecuteController : AsyncController
+ {
+ private Func<object, string> _func;
+
+ [Authorize]
+ public void FooAsync(int id1)
+ {
+ _func = o => Convert.ToString(o, CultureInfo.InvariantCulture) + id1.ToString(CultureInfo.InvariantCulture);
+ AsyncManager.Parameters["id2"] = "Hello world: ";
+ AsyncManager.Finish();
+ }
+
+ [OutputCache]
+ public string FooCompleted(string id2)
+ {
+ return _func(id2);
+ }
+
+ public string FooWithBool(bool id2)
+ {
+ return _func(id2);
+ }
+
+ public string FooWithException(Exception id2)
+ {
+ return _func(id2);
+ }
+ }
+
+ private class RegularSyncController : ControllerBase
+ {
+ protected override void ExecuteCore()
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Async/Test/ReflectedAsyncControllerDescriptorTest.cs b/test/System.Web.Mvc.Test/Async/Test/ReflectedAsyncControllerDescriptorTest.cs
new file mode 100644
index 00000000..13586447
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Async/Test/ReflectedAsyncControllerDescriptorTest.cs
@@ -0,0 +1,177 @@
+using System.Reflection;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Async.Test
+{
+ public class ReflectedAsyncControllerDescriptorTest
+ {
+ [Fact]
+ public void ConstructorSetsControllerTypeProperty()
+ {
+ // Arrange
+ Type controllerType = typeof(string);
+
+ // Act
+ ReflectedAsyncControllerDescriptor cd = new ReflectedAsyncControllerDescriptor(controllerType);
+
+ // Assert
+ Assert.Same(controllerType, cd.ControllerType);
+ }
+
+ [Fact]
+ public void ConstructorThrowsIfControllerTypeIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new ReflectedAsyncControllerDescriptor(null); }, "controllerType");
+ }
+
+ [Fact]
+ public void FindActionReturnsActionDescriptorIfFound()
+ {
+ // Arrange
+ Type controllerType = typeof(MyController);
+ MethodInfo asyncMethodInfo = controllerType.GetMethod("FooAsync");
+ MethodInfo completedMethodInfo = controllerType.GetMethod("FooCompleted");
+ ReflectedAsyncControllerDescriptor cd = new ReflectedAsyncControllerDescriptor(controllerType);
+
+ // Act
+ ActionDescriptor ad = cd.FindAction(new Mock<ControllerContext>().Object, "NewName");
+
+ // Assert
+ Assert.Equal("NewName", ad.ActionName);
+ var castAd = Assert.IsType<ReflectedAsyncActionDescriptor>(ad);
+
+ Assert.Same(asyncMethodInfo, castAd.AsyncMethodInfo);
+ Assert.Same(completedMethodInfo, castAd.CompletedMethodInfo);
+ Assert.Same(cd, ad.ControllerDescriptor);
+ }
+
+ [Fact]
+ public void FindActionReturnsNullIfNoActionFound()
+ {
+ // Arrange
+ Type controllerType = typeof(MyController);
+ ReflectedAsyncControllerDescriptor cd = new ReflectedAsyncControllerDescriptor(controllerType);
+
+ // Act
+ ActionDescriptor ad = cd.FindAction(new Mock<ControllerContext>().Object, "NonExistent");
+
+ // Assert
+ Assert.Null(ad);
+ }
+
+ [Fact]
+ public void FindActionThrowsIfActionNameIsEmpty()
+ {
+ // Arrange
+ Type controllerType = typeof(MyController);
+ ReflectedAsyncControllerDescriptor cd = new ReflectedAsyncControllerDescriptor(controllerType);
+
+ // Act & assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { cd.FindAction(new Mock<ControllerContext>().Object, ""); }, "actionName");
+ }
+
+ [Fact]
+ public void FindActionThrowsIfActionNameIsNull()
+ {
+ // Arrange
+ Type controllerType = typeof(MyController);
+ ReflectedAsyncControllerDescriptor cd = new ReflectedAsyncControllerDescriptor(controllerType);
+
+ // Act & assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { cd.FindAction(new Mock<ControllerContext>().Object, null); }, "actionName");
+ }
+
+ [Fact]
+ public void FindActionThrowsIfControllerContextIsNull()
+ {
+ // Arrange
+ Type controllerType = typeof(MyController);
+ ReflectedAsyncControllerDescriptor cd = new ReflectedAsyncControllerDescriptor(controllerType);
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { cd.FindAction(null, "someName"); }, "controllerContext");
+ }
+
+ [Fact]
+ public void GetCanonicalActionsReturnsEmptyArray()
+ {
+ // this method does nothing by default
+
+ // Arrange
+ Type controllerType = typeof(MyController);
+ ReflectedAsyncControllerDescriptor cd = new ReflectedAsyncControllerDescriptor(controllerType);
+
+ // Act
+ ActionDescriptor[] canonicalActions = cd.GetCanonicalActions();
+
+ // Assert
+ Assert.Empty(canonicalActions);
+ }
+
+ [Fact]
+ public void GetCustomAttributesCallsTypeGetCustomAttributes()
+ {
+ // Arrange
+ object[] expected = new object[0];
+ Mock<Type> mockType = new Mock<Type>();
+ mockType.Setup(t => t.GetCustomAttributes(true)).Returns(expected);
+ ReflectedAsyncControllerDescriptor cd = new ReflectedAsyncControllerDescriptor(mockType.Object);
+
+ // Act
+ object[] returned = cd.GetCustomAttributes(true);
+
+ // Assert
+ Assert.Same(expected, returned);
+ }
+
+ [Fact]
+ public void GetCustomAttributesWithAttributeTypeCallsTypeGetCustomAttributes()
+ {
+ // Arrange
+ object[] expected = new object[0];
+ Mock<Type> mockType = new Mock<Type>();
+ mockType.Setup(t => t.GetCustomAttributes(typeof(ObsoleteAttribute), true)).Returns(expected);
+ ReflectedAsyncControllerDescriptor cd = new ReflectedAsyncControllerDescriptor(mockType.Object);
+
+ // Act
+ object[] returned = cd.GetCustomAttributes(typeof(ObsoleteAttribute), true);
+
+ // Assert
+ Assert.Same(expected, returned);
+ }
+
+ [Fact]
+ public void IsDefinedCallsTypeIsDefined()
+ {
+ // Arrange
+ Mock<Type> mockType = new Mock<Type>();
+ mockType.Setup(t => t.IsDefined(typeof(ObsoleteAttribute), true)).Returns(true);
+ ReflectedAsyncControllerDescriptor cd = new ReflectedAsyncControllerDescriptor(mockType.Object);
+
+ // Act
+ bool isDefined = cd.IsDefined(typeof(ObsoleteAttribute), true);
+
+ // Assert
+ Assert.True(isDefined);
+ }
+
+ private class MyController : AsyncController
+ {
+ [ActionName("NewName")]
+ public void FooAsync()
+ {
+ }
+
+ public void FooCompleted()
+ {
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Async/Test/SignalContainer.cs b/test/System.Web.Mvc.Test/Async/Test/SignalContainer.cs
new file mode 100644
index 00000000..8e1f5125
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Async/Test/SignalContainer.cs
@@ -0,0 +1,22 @@
+using System.Threading;
+
+namespace System.Web.Mvc.Async.Test
+{
+ public sealed class SignalContainer<T>
+ {
+ private volatile object _item;
+ private readonly AutoResetEvent _waitHandle = new AutoResetEvent(false /* initialState */);
+
+ public void Signal(T item)
+ {
+ _item = item;
+ _waitHandle.Set();
+ }
+
+ public T Wait()
+ {
+ _waitHandle.WaitOne();
+ return (T)_item;
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Async/Test/SimpleAsyncResultTest.cs b/test/System.Web.Mvc.Test/Async/Test/SimpleAsyncResultTest.cs
new file mode 100644
index 00000000..0c71b598
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Async/Test/SimpleAsyncResultTest.cs
@@ -0,0 +1,101 @@
+using System.Threading;
+using Xunit;
+
+namespace System.Web.Mvc.Async.Test
+{
+ public class SimpleAsyncResultTest
+ {
+ [Fact]
+ public void AsyncStateProperty()
+ {
+ // Arrange
+ string expected = "Hello!";
+ SimpleAsyncResult asyncResult = new SimpleAsyncResult(expected);
+
+ // Act
+ object asyncState = asyncResult.AsyncState;
+
+ // Assert
+ Assert.Equal(expected, asyncState);
+ }
+
+ [Fact]
+ public void AsyncWaitHandleProperty()
+ {
+ // Arrange
+ SimpleAsyncResult asyncResult = new SimpleAsyncResult(null);
+
+ // Act
+ WaitHandle asyncWaitHandle = asyncResult.AsyncWaitHandle;
+
+ // Assert
+ Assert.Null(asyncWaitHandle);
+ }
+
+ [Fact]
+ public void CompletedSynchronouslyProperty()
+ {
+ // Arrange
+ SimpleAsyncResult asyncResult = new SimpleAsyncResult(null);
+
+ // Act
+ bool completedSynchronously = asyncResult.CompletedSynchronously;
+
+ // Assert
+ Assert.False(completedSynchronously);
+ }
+
+ [Fact]
+ public void IsCompletedProperty()
+ {
+ // Arrange
+ SimpleAsyncResult asyncResult = new SimpleAsyncResult(null);
+
+ // Act
+ bool isCompleted = asyncResult.IsCompleted;
+
+ // Assert
+ Assert.False(isCompleted);
+ }
+
+ [Fact]
+ public void MarkCompleted_AsynchronousCompletion()
+ {
+ // Arrange
+ SimpleAsyncResult asyncResult = new SimpleAsyncResult(null);
+
+ bool callbackWasCalled = false;
+ AsyncCallback callback = ar =>
+ {
+ callbackWasCalled = true;
+ Assert.Equal(asyncResult, ar);
+ Assert.True(ar.IsCompleted);
+ Assert.False(ar.CompletedSynchronously);
+ };
+
+ // Act & assert
+ asyncResult.MarkCompleted(false, callback);
+ Assert.True(callbackWasCalled);
+ }
+
+ [Fact]
+ public void MarkCompleted_SynchronousCompletion()
+ {
+ // Arrange
+ SimpleAsyncResult asyncResult = new SimpleAsyncResult(null);
+
+ bool callbackWasCalled = false;
+ AsyncCallback callback = ar =>
+ {
+ callbackWasCalled = true;
+ Assert.Equal(asyncResult, ar);
+ Assert.True(ar.IsCompleted);
+ Assert.True(ar.CompletedSynchronously);
+ };
+
+ // Act & assert
+ asyncResult.MarkCompleted(true, callback);
+ Assert.True(callbackWasCalled);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Async/Test/SingleEntryGateTest.cs b/test/System.Web.Mvc.Test/Async/Test/SingleEntryGateTest.cs
new file mode 100644
index 00000000..2cfdf5c0
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Async/Test/SingleEntryGateTest.cs
@@ -0,0 +1,24 @@
+using Xunit;
+
+namespace System.Web.Mvc.Async.Test
+{
+ public class SingleEntryGateTest
+ {
+ [Fact]
+ public void TryEnterShouldBeTrueForFirstCallAndFalseForSubsequentCalls()
+ {
+ // Arrange
+ SingleEntryGate gate = new SingleEntryGate();
+
+ // Act
+ bool firstCall = gate.TryEnter();
+ bool secondCall = gate.TryEnter();
+ bool thirdCall = gate.TryEnter();
+
+ // Assert
+ Assert.True(firstCall);
+ Assert.False(secondCall);
+ Assert.False(thirdCall);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Async/Test/SynchronizationContextUtilTest.cs b/test/System.Web.Mvc.Test/Async/Test/SynchronizationContextUtilTest.cs
new file mode 100644
index 00000000..0810a2a5
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Async/Test/SynchronizationContextUtilTest.cs
@@ -0,0 +1,89 @@
+using System.Threading;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Async.Test
+{
+ public class SynchronizationContextUtilTest
+ {
+ [Fact]
+ public void SyncWithAction()
+ {
+ // Arrange
+ bool actionWasCalled = false;
+ bool sendWasCalled = false;
+
+ Mock<SynchronizationContext> mockSyncContext = new Mock<SynchronizationContext>();
+ mockSyncContext
+ .Setup(sc => sc.Send(It.IsAny<SendOrPostCallback>(), null))
+ .Callback(
+ delegate(SendOrPostCallback d, object state)
+ {
+ sendWasCalled = true;
+ d(state);
+ });
+
+ // Act
+ SynchronizationContextUtil.Sync(mockSyncContext.Object, () => { actionWasCalled = true; });
+
+ // Assert
+ Assert.True(actionWasCalled);
+ Assert.True(sendWasCalled);
+ }
+
+ [Fact]
+ public void SyncWithActionCapturesException()
+ {
+ // Arrange
+ InvalidOperationException exception = new InvalidOperationException("Some exception text.");
+
+ Mock<SynchronizationContext> mockSyncContext = new Mock<SynchronizationContext>();
+ mockSyncContext
+ .Setup(sc => sc.Send(It.IsAny<SendOrPostCallback>(), null))
+ .Callback(
+ delegate(SendOrPostCallback d, object state)
+ {
+ try
+ {
+ d(state);
+ }
+ catch
+ {
+ // swallow exceptions, just like AspNetSynchronizationContext
+ }
+ });
+
+ // Act & assert
+ SynchronousOperationException thrownException = Assert.Throws<SynchronousOperationException>(
+ delegate { SynchronizationContextUtil.Sync(mockSyncContext.Object, () => { throw exception; }); },
+ @"An operation that crossed a synchronization context failed. See the inner exception for more information.");
+
+ Assert.Equal(exception, thrownException.InnerException);
+ }
+
+ [Fact]
+ public void SyncWithFunc()
+ {
+ // Arrange
+ bool sendWasCalled = false;
+
+ Mock<SynchronizationContext> mockSyncContext = new Mock<SynchronizationContext>();
+ mockSyncContext
+ .Setup(sc => sc.Send(It.IsAny<SendOrPostCallback>(), null))
+ .Callback(
+ delegate(SendOrPostCallback d, object state)
+ {
+ sendWasCalled = true;
+ d(state);
+ });
+
+ // Act
+ int retVal = SynchronizationContextUtil.Sync(mockSyncContext.Object, () => 42);
+
+ // Assert
+ Assert.Equal(42, retVal);
+ Assert.True(sendWasCalled);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Async/Test/SynchronousOperationExceptionTest.cs b/test/System.Web.Mvc.Test/Async/Test/SynchronousOperationExceptionTest.cs
new file mode 100644
index 00000000..32570c22
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Async/Test/SynchronousOperationExceptionTest.cs
@@ -0,0 +1,62 @@
+using System.IO;
+using System.Runtime.Serialization.Formatters.Binary;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Async.Test
+{
+ public class SynchronousOperationExceptionTest
+ {
+ [Fact]
+ public void ConstructorWithMessageAndInnerExceptionParameter()
+ {
+ // Arrange
+ Exception innerException = new Exception();
+
+ // Act
+ SynchronousOperationException ex = new SynchronousOperationException("the message", innerException);
+
+ // Assert
+ Assert.Equal("the message", ex.Message);
+ Assert.Equal(innerException, ex.InnerException);
+ }
+
+ [Fact]
+ public void ConstructorWithMessageParameter()
+ {
+ // Act
+ SynchronousOperationException ex = new SynchronousOperationException("the message");
+
+ // Assert
+ Assert.Equal("the message", ex.Message);
+ }
+
+ [Fact]
+ public void ConstructorWithoutParameters()
+ {
+ // Act & assert
+ Assert.Throws<SynchronousOperationException>(
+ delegate { throw new SynchronousOperationException(); });
+ }
+
+ [Fact]
+ public void TypeIsSerializable()
+ {
+ // Arrange
+ MemoryStream ms = new MemoryStream();
+ BinaryFormatter formatter = new BinaryFormatter();
+ SynchronousOperationException ex = new SynchronousOperationException("the message", new Exception("inner exception"));
+
+ // Act
+ formatter.Serialize(ms, ex);
+ ms.Position = 0;
+ SynchronousOperationException deserialized = formatter.Deserialize(ms) as SynchronousOperationException;
+
+ // Assert
+ Assert.NotNull(deserialized);
+ Assert.Equal("the message", deserialized.Message);
+ Assert.NotNull(deserialized.InnerException);
+ Assert.Equal("inner exception", deserialized.InnerException.Message);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Async/Test/TaskAsyncActionDescriptorTest.cs b/test/System.Web.Mvc.Test/Async/Test/TaskAsyncActionDescriptorTest.cs
new file mode 100644
index 00000000..c2c83232
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Async/Test/TaskAsyncActionDescriptorTest.cs
@@ -0,0 +1,558 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+using Moq;
+using Xunit;
+using Xunit.Sdk;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Async.Test
+{
+ public class TaskAsyncActionDescriptorTest
+ {
+ private readonly MethodInfo _taskMethod = typeof(ExecuteController).GetMethod("SimpleTask");
+
+ [Fact]
+ public void Constructor_SetsProperties()
+ {
+ // Arrange
+ string actionName = "SomeAction";
+ ControllerDescriptor cd = new Mock<ControllerDescriptor>().Object;
+
+ // Act
+ TaskAsyncActionDescriptor ad = new TaskAsyncActionDescriptor(_taskMethod, actionName, cd);
+
+ // Assert
+ Assert.Equal(_taskMethod, ad.TaskMethodInfo);
+ Assert.Equal(actionName, ad.ActionName);
+ Assert.Equal(cd, ad.ControllerDescriptor);
+ }
+
+ [Fact]
+ public void Constructor_ThrowsIfActionNameIsEmpty()
+ {
+ // Arrange
+ ControllerDescriptor cd = new Mock<ControllerDescriptor>().Object;
+
+ // Act & assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { new TaskAsyncActionDescriptor(_taskMethod, "", cd); }, "actionName");
+ }
+
+ [Fact]
+ public void Constructor_ThrowsIfActionNameIsNull()
+ {
+ // Arrange
+ ControllerDescriptor cd = new Mock<ControllerDescriptor>().Object;
+
+ // Act & assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { new TaskAsyncActionDescriptor(_taskMethod, null, cd); }, "actionName");
+ }
+
+ [Fact]
+ public void Constructor_ThrowsIfTaskMethodInfoIsInvalid()
+ {
+ // Arrange
+ ControllerDescriptor cd = new Mock<ControllerDescriptor>().Object;
+ MethodInfo getHashCodeMethod = typeof(object).GetMethod("GetHashCode");
+
+ // Act & assert
+ Assert.Throws<ArgumentException>(
+ delegate { new TaskAsyncActionDescriptor(getHashCodeMethod, "SomeAction", cd); },
+ @"Cannot create a descriptor for instance method 'Int32 GetHashCode()' on type 'System.Object' because the type does not derive from ControllerBase.
+Parameter name: taskMethodInfo");
+ }
+
+ [Fact]
+ public void Constructor_ThrowsIfTaskMethodInfoIsNull()
+ {
+ // Arrange
+ ControllerDescriptor cd = new Mock<ControllerDescriptor>().Object;
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new TaskAsyncActionDescriptor(null, "SomeAction", cd); }, "taskMethodInfo");
+ }
+
+ [Fact]
+ public void Constructor_ThrowsIfControllerDescriptorIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new TaskAsyncActionDescriptor(_taskMethod, "SomeAction", null); }, "controllerDescriptor");
+ }
+
+ [Fact]
+ public void ExecuteTask()
+ {
+ // Arrange
+ TaskAsyncActionDescriptor actionDescriptor = GetActionDescriptor(GetExecuteControllerMethodInfo("SimpleTask"));
+
+ Dictionary<string, object> parameters = new Dictionary<string, object>()
+ {
+ { "doWork", true }
+ };
+
+ ControllerContext controllerContext = GetControllerContext();
+
+ // Act
+ object retVal = ExecuteHelper(actionDescriptor, parameters, controllerContext);
+
+ // Assert
+ Assert.Null(retVal);
+ Assert.True((controllerContext.Controller as ExecuteController).WorkDone);
+ }
+
+ [Fact]
+ public void ExecuteTaskGeneric()
+ {
+ // Arrange
+ TaskAsyncActionDescriptor actionDescriptor = GetActionDescriptor(GetExecuteControllerMethodInfo("GenericTask"));
+
+ Dictionary<string, object> parameters = new Dictionary<string, object>()
+ {
+ { "taskId", "foo" }
+ };
+
+ // Act
+ object retVal = ExecuteHelper(actionDescriptor, parameters);
+
+ // Assert
+ Assert.Equal("foo", retVal);
+ }
+
+ [Fact]
+ public void ExecuteTaskPreservesStackTraceOnException()
+ {
+ // Arrange
+ TaskAsyncActionDescriptor actionDescriptor = GetActionDescriptor(GetExecuteControllerMethodInfo("SimpleTaskException"));
+
+ Dictionary<string, object> parameters = new Dictionary<string, object>()
+ {
+ { "doWork", true }
+ };
+
+ // Act
+ IAsyncResult result = actionDescriptor.BeginExecute(GetControllerContext(), parameters, null, null);
+
+ // Assert
+ InvalidOperationException ex = Assert.Throws<InvalidOperationException>(
+ () => actionDescriptor.EndExecute(result),
+ "Test exception from action"
+ );
+
+ Assert.True(ex.StackTrace.Contains("System.Web.Mvc.Async.Test.TaskAsyncActionDescriptorTest.ExecuteController."));
+ }
+
+ [Fact]
+ public void ExecuteTaskGenericPreservesStackTraceOnException()
+ {
+ // Arrange
+ TaskAsyncActionDescriptor actionDescriptor = GetActionDescriptor(GetExecuteControllerMethodInfo("GenericTaskException"));
+
+ Dictionary<string, object> parameters = new Dictionary<string, object>()
+ {
+ { "taskId", "foo" },
+ { "throwException", true }
+ };
+
+ // Act
+ IAsyncResult result = actionDescriptor.BeginExecute(GetControllerContext(), parameters, null, null);
+
+ // Assert
+ InvalidOperationException ex = Assert.Throws<InvalidOperationException>(
+ () => actionDescriptor.EndExecute(result),
+ "Test exception from action"
+ );
+
+ Assert.True(ex.StackTrace.Contains("System.Web.Mvc.Async.Test.TaskAsyncActionDescriptorTest.ExecuteController."));
+ }
+
+ [Fact]
+ public void ExecuteTaskOfPrivateT()
+ {
+ // Arrange
+ TaskAsyncActionDescriptor actionDescriptor = GetActionDescriptor(GetExecuteControllerMethodInfo("TaskOfPrivateT"));
+ ControllerContext controllerContext = GetControllerContext();
+
+ Dictionary<string, object> parameters = new Dictionary<string, object>();
+
+ // Act
+ object retVal = ExecuteHelper(actionDescriptor, parameters, controllerContext);
+
+ // Assert
+ Assert.Null(retVal);
+ Assert.True((controllerContext.Controller as ExecuteController).WorkDone);
+ }
+
+ [Fact]
+ public void ExecuteTaskPreservesState()
+ {
+ // Arrange
+ TaskAsyncActionDescriptor actionDescriptor = GetActionDescriptor(GetExecuteControllerMethodInfo("SimpleTask"));
+
+ Dictionary<string, object> parameters = new Dictionary<string, object>()
+ {
+ { "doWork", true }
+ };
+
+ ControllerContext controllerContext = GetControllerContext();
+
+ // Act
+ TaskWrapperAsyncResult result = (TaskWrapperAsyncResult)actionDescriptor.BeginExecute(GetControllerContext(), parameters, callback: null, state: "state");
+
+ // Assert
+ Assert.Equal("state", result.AsyncState);
+ }
+
+ [Fact]
+ public void ExecuteTaskWithNullParameterAndTimeout()
+ {
+ // Arrange
+ TaskAsyncActionDescriptor actionDescriptor = GetActionDescriptor(GetExecuteControllerMethodInfo("TaskTimeoutWithNullParam"));
+
+ Dictionary<string, object> token = new Dictionary<string, object>()
+ {
+ { "nullParam", null },
+ { "cancellationToken", new CancellationToken() }
+ };
+
+ // Act & assert
+ Assert.Throws<TimeoutException>(
+ () => actionDescriptor.EndExecute(actionDescriptor.BeginExecute(GetControllerContext(0), parameters: token, callback: null, state: null)),
+ "The operation has timed out."
+ );
+ }
+
+ [Fact]
+ public void ExecuteWithInfiniteTimeout()
+ {
+ // Arrange
+ TaskAsyncActionDescriptor actionDescriptor = GetActionDescriptor(GetExecuteControllerMethodInfo("TaskWithInfiniteTimeout"));
+ ControllerContext controllerContext = GetControllerContext(Timeout.Infinite);
+
+ Dictionary<string, object> parameters = new Dictionary<string, object>()
+ {
+ { "cancellationToken", new CancellationToken() }
+ };
+
+ // Act
+ object retVal = ExecuteHelper(actionDescriptor, parameters);
+
+ // Assert
+ Assert.Equal("Task Completed", retVal);
+ }
+
+ [Fact]
+ public void ExecuteTaskWithImmediateTimeout()
+ {
+ // Arrange
+ TaskAsyncActionDescriptor actionDescriptor = GetActionDescriptor(GetExecuteControllerMethodInfo("TaskTimeout"));
+
+ Dictionary<string, object> token = new Dictionary<string, object>()
+ {
+ { "cancellationToken", new CancellationToken() }
+ };
+
+ // Act & assert
+ Assert.Throws<TimeoutException>(
+ () => actionDescriptor.EndExecute(actionDescriptor.BeginExecute(GetControllerContext(0), parameters: token, callback: null, state: null)),
+ "The operation has timed out."
+ );
+ }
+
+ [Fact]
+ public void ExecuteTaskWithTimeout()
+ {
+ // Arrange
+ TaskAsyncActionDescriptor actionDescriptor = GetActionDescriptor(GetExecuteControllerMethodInfo("TaskTimeout"));
+
+ Dictionary<string, object> token = new Dictionary<string, object>()
+ {
+ { "cancellationToken", new CancellationToken() }
+ };
+
+ // Act & assert
+ Assert.Throws<TimeoutException>(
+ () => actionDescriptor.EndExecute(actionDescriptor.BeginExecute(GetControllerContext(2000), parameters: token, callback: null, state: null)),
+ "The operation has timed out."
+ );
+ }
+
+ [Fact]
+ public void SynchronousExecuteThrows()
+ {
+ // Arrange
+ TaskAsyncActionDescriptor actionDescriptor = GetActionDescriptor(GetExecuteControllerMethodInfo("SimpleTask"));
+
+ // Act & assert
+ Assert.Throws<InvalidOperationException>(
+ delegate { actionDescriptor.Execute(new ControllerContext(), new Dictionary<string, object>()); }, "The asynchronous action method 'someName' returns a Task, which cannot be executed synchronously.");
+ }
+
+ [Fact]
+ public void Execute_ThrowsIfControllerContextIsNull()
+ {
+ // Arrange
+ TaskAsyncActionDescriptor ad = GetActionDescriptor(_taskMethod);
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { ad.BeginExecute(null, new Dictionary<string, object>(), null, null); }, "controllerContext");
+ }
+
+ [Fact]
+ public void Execute_ThrowsIfControllerIsNotAsyncManagerContainer()
+ {
+ // Arrange
+ TaskAsyncActionDescriptor ad = GetActionDescriptor(_taskMethod);
+ ControllerContext controllerContext = new ControllerContext()
+ {
+ Controller = new RegularSyncController()
+ };
+
+ Dictionary<string, object> parameters = new Dictionary<string, object>()
+ {
+ { "doWork", true }
+ };
+
+ // Act & assert
+ Assert.Throws<InvalidOperationException>(
+ delegate { ad.BeginExecute(controllerContext, parameters, null, null); },
+ @"The controller of type 'System.Web.Mvc.Async.Test.TaskAsyncActionDescriptorTest+RegularSyncController' must subclass AsyncController or implement the IAsyncManagerContainer interface.");
+ }
+
+ [Fact]
+ public void Execute_ThrowsIfParametersIsNull()
+ {
+ // Arrange
+ TaskAsyncActionDescriptor ad = GetActionDescriptor(_taskMethod);
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { ad.BeginExecute(new ControllerContext(), null, null, null); }, "parameters");
+ }
+
+ [Fact]
+ public void GetCustomAttributesCallsMethodInfoGetCustomAttributes()
+ {
+ // Arrange
+ object[] expected = new object[0];
+ Mock<MethodInfo> mockMethod = new Mock<MethodInfo>();
+ mockMethod.Setup(mi => mi.GetCustomAttributes(true)).Returns(expected);
+ TaskAsyncActionDescriptor ad = new TaskAsyncActionDescriptor(mockMethod.Object, "someName", new Mock<ControllerDescriptor>().Object, validateMethod: false)
+ {
+ DispatcherCache = new ActionMethodDispatcherCache()
+ };
+
+ // Act
+ object[] returned = ad.GetCustomAttributes(true);
+
+ // Assert
+ Assert.Same(expected, returned);
+ }
+
+ [Fact]
+ public void GetCustomAttributesWithAttributeTypeCallsMethodInfoGetCustomAttributes()
+ {
+ // Arrange
+ object[] expected = new object[0];
+ Mock<MethodInfo> mockMethod = new Mock<MethodInfo>();
+ mockMethod.Setup(mi => mi.GetCustomAttributes(typeof(ObsoleteAttribute), true)).Returns(expected);
+ TaskAsyncActionDescriptor ad = new TaskAsyncActionDescriptor(mockMethod.Object, "someName", new Mock<ControllerDescriptor>().Object, validateMethod: false)
+ {
+ DispatcherCache = new ActionMethodDispatcherCache()
+ };
+
+ // Act
+ object[] returned = ad.GetCustomAttributes(typeof(ObsoleteAttribute), true);
+
+ // Assert
+ Assert.Same(expected, returned);
+ }
+
+ [Fact]
+ public void GetParameters()
+ {
+ // Arrange
+ ParameterInfo pInfo = _taskMethod.GetParameters()[0];
+ TaskAsyncActionDescriptor ad = GetActionDescriptor(_taskMethod);
+
+ // Act
+ ParameterDescriptor[] pDescsFirstCall = ad.GetParameters();
+ ParameterDescriptor[] pDescsSecondCall = ad.GetParameters();
+
+ // Assert
+ Assert.NotSame(pDescsFirstCall, pDescsSecondCall); // Should get a new array every time
+ Assert.Equal(pDescsFirstCall, pDescsSecondCall);
+ Assert.Single(pDescsFirstCall);
+
+ ReflectedParameterDescriptor pDesc = pDescsFirstCall[0] as ReflectedParameterDescriptor;
+
+ Assert.NotNull(pDesc);
+ Assert.Same(ad, pDesc.ActionDescriptor);
+ Assert.Same(pInfo, pDesc.ParameterInfo);
+ }
+
+ [Fact]
+ public void GetSelectors()
+ {
+ // Arrange
+ ControllerContext controllerContext = new Mock<ControllerContext>().Object;
+ Mock<MethodInfo> mockMethod = new Mock<MethodInfo>();
+
+ Mock<ActionMethodSelectorAttribute> mockAttr = new Mock<ActionMethodSelectorAttribute>();
+ mockAttr.Setup(attr => attr.IsValidForRequest(controllerContext, mockMethod.Object)).Returns(true).Verifiable();
+ mockMethod.Setup(m => m.GetCustomAttributes(typeof(ActionMethodSelectorAttribute), true)).Returns(new ActionMethodSelectorAttribute[] { mockAttr.Object });
+
+ TaskAsyncActionDescriptor ad = new TaskAsyncActionDescriptor(mockMethod.Object, "someName", new Mock<ControllerDescriptor>().Object, validateMethod: false)
+ {
+ DispatcherCache = new ActionMethodDispatcherCache()
+ };
+
+ // Act
+ ICollection<ActionSelector> selectors = ad.GetSelectors();
+ bool executedSuccessfully = selectors.All(s => s(controllerContext));
+
+ // Assert
+ Assert.Single(selectors);
+ Assert.True(executedSuccessfully);
+ mockAttr.Verify();
+ }
+
+ [Fact]
+ public void IsDefined()
+ {
+ // Arrange
+ TaskAsyncActionDescriptor ad = GetActionDescriptor(_taskMethod);
+
+ // Act
+ bool isDefined = ad.IsDefined(typeof(AuthorizeAttribute), inherit: true);
+
+ // Assert
+ Assert.True(isDefined);
+ }
+
+ public static object ExecuteHelper(TaskAsyncActionDescriptor actionDescriptor, Dictionary<string, object> parameters, ControllerContext controllerContext = null)
+ {
+ SignalContainer<object> resultContainer = new SignalContainer<object>();
+ AsyncCallback callback = ar =>
+ {
+ object o = actionDescriptor.EndExecute(ar);
+ resultContainer.Signal(o);
+ };
+
+ actionDescriptor.BeginExecute(controllerContext ?? GetControllerContext(), parameters, callback, state: null);
+ return resultContainer.Wait();
+ }
+
+ private static TaskAsyncActionDescriptor GetActionDescriptor(MethodInfo taskMethod)
+ {
+ return new TaskAsyncActionDescriptor(taskMethod, "someName", new Mock<ControllerDescriptor>().Object)
+ {
+ DispatcherCache = new ActionMethodDispatcherCache()
+ };
+ }
+
+ private static ControllerContext GetControllerContext(int timeout = 45 * 1000)
+ {
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ ExecuteController controller = new ExecuteController();
+ controller.AsyncManager.Timeout = timeout;
+ mockControllerContext.Setup(c => c.Controller).Returns(controller);
+ return mockControllerContext.Object;
+ }
+
+ private static MethodInfo GetExecuteControllerMethodInfo(string methodName)
+ {
+ return typeof(ExecuteController).GetMethod(methodName);
+ }
+
+ private class ExecuteController : AsyncController
+ {
+ public bool WorkDone { get; set; }
+
+ public Task<ActionResult> ReturnedTask { get; set; }
+
+ public Task<string> GenericTask(string taskId)
+ {
+ return Task.Factory.StartNew(() => taskId);
+ }
+
+ public Task<string> GenericTaskException(string taskId, bool throwException)
+ {
+ return Task.Factory.StartNew(() =>
+ {
+ if (throwException)
+ {
+ ThrowException();
+ }
+ ;
+ return taskId;
+ });
+ }
+
+ private void ThrowException()
+ {
+ throw new InvalidOperationException("Test exception from action");
+ }
+
+ [Authorize]
+ public Task SimpleTask(bool doWork)
+ {
+ return Task.Factory.StartNew(() => { WorkDone = doWork; });
+ }
+
+ public Task SimpleTaskException(bool doWork)
+ {
+ return Task.Factory.StartNew(() => { ThrowException(); });
+ }
+
+ public Task<ActionResult> TaskTimeoutWithNullParam(Object nullParam, CancellationToken cancellationToken)
+ {
+ return TaskTimeout(cancellationToken);
+ }
+
+ public Task<string> TaskWithInfiniteTimeout(CancellationToken cancellationToken)
+ {
+ return Task.Factory.StartNew(() => "Task Completed");
+ }
+
+ public Task<ActionResult> TaskTimeout(CancellationToken cancellationToken)
+ {
+ TaskCompletionSource<ActionResult> completionSource = new TaskCompletionSource<ActionResult>();
+ cancellationToken.Register(() => completionSource.TrySetCanceled());
+ ReturnedTask = completionSource.Task;
+ return ReturnedTask;
+ }
+
+ public Task TaskOfPrivateT()
+ {
+ var completionSource = new TaskCompletionSource<PrivateObject>();
+ completionSource.SetResult(new PrivateObject());
+ WorkDone = true;
+ return completionSource.Task;
+ }
+
+ private class PrivateObject
+ {
+ public override string ToString()
+ {
+ return "Private Object";
+ }
+ }
+ }
+
+ // Controller is async, so derive from ControllerBase to get sync behavior.
+ private class RegularSyncController : ControllerBase
+ {
+ protected override void ExecuteCore()
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Async/Test/TaskWrapperAsyncResultTest.cs b/test/System.Web.Mvc.Test/Async/Test/TaskWrapperAsyncResultTest.cs
new file mode 100644
index 00000000..e0e74843
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Async/Test/TaskWrapperAsyncResultTest.cs
@@ -0,0 +1,62 @@
+using System.Threading;
+using System.Threading.Tasks;
+using Moq;
+using Xunit;
+
+namespace System.Web.Mvc.Async.Test
+{
+ public class TaskWrapperAsyncResultTest
+ {
+ [Fact]
+ public void PropertiesHaveCorrectValues()
+ {
+ // Arrange
+ Mock<MyTask> mockTask = new Mock<MyTask>();
+ WaitHandle waitHandle = new Mock<WaitHandle>().Object;
+
+ mockTask.Setup(o => o.AsyncState).Returns(10);
+ mockTask.Setup(o => o.AsyncWaitHandle).Returns(waitHandle);
+ mockTask.Setup(o => o.CompletedSynchronously).Returns(true);
+ mockTask.Setup(o => o.IsCompleted).Returns(true);
+
+ // Act
+ TaskWrapperAsyncResult taskWrapper = new TaskWrapperAsyncResult(mockTask.Object, asyncState: 20);
+
+ // Assert
+ Assert.Equal(20, taskWrapper.AsyncState);
+ Assert.Equal(waitHandle, taskWrapper.AsyncWaitHandle);
+ Assert.True(taskWrapper.CompletedSynchronously);
+ Assert.True(taskWrapper.IsCompleted);
+ Assert.Equal(mockTask.Object, taskWrapper.Task);
+ }
+
+ // Assists in mocking a Task by passing a dummy action to the Task constructor [which defers execution]
+ public class MyTask : Task, IAsyncResult
+ {
+ public MyTask()
+ : base(() => { })
+ {
+ }
+
+ public new virtual object AsyncState
+ {
+ get { throw new NotImplementedException(); }
+ }
+
+ public virtual WaitHandle AsyncWaitHandle
+ {
+ get { throw new NotImplementedException(); }
+ }
+
+ public virtual bool CompletedSynchronously
+ {
+ get { throw new NotImplementedException(); }
+ }
+
+ public new virtual bool IsCompleted
+ {
+ get { throw new NotImplementedException(); }
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Async/Test/TriggerListenerTest.cs b/test/System.Web.Mvc.Test/Async/Test/TriggerListenerTest.cs
new file mode 100644
index 00000000..63c48d4e
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Async/Test/TriggerListenerTest.cs
@@ -0,0 +1,30 @@
+using Xunit;
+
+namespace System.Web.Mvc.Async.Test
+{
+ public class TriggerListenerTest
+ {
+ [Fact]
+ public void PerformTest()
+ {
+ // Arrange
+ int count = 0;
+ TriggerListener listener = new TriggerListener();
+ Trigger trigger = listener.CreateTrigger();
+
+ // Act & assert (hasn't fired yet)
+ listener.SetContinuation(() => { count++; });
+ listener.Activate();
+ Assert.Equal(0, count);
+
+ // Act & assert (fire it, get the callback)
+ trigger.Fire();
+ Assert.Equal(1, count);
+
+ // Act & assert (fire again, but no callback since it already fired)
+ Trigger trigger2 = listener.CreateTrigger();
+ trigger2.Fire();
+ Assert.Equal(1, count);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/ExpressionUtil/Test/BinaryExpressionFingerprintTest.cs b/test/System.Web.Mvc.Test/ExpressionUtil/Test/BinaryExpressionFingerprintTest.cs
new file mode 100644
index 00000000..519c5bf3
--- /dev/null
+++ b/test/System.Web.Mvc.Test/ExpressionUtil/Test/BinaryExpressionFingerprintTest.cs
@@ -0,0 +1,91 @@
+using System.Linq.Expressions;
+using System.Reflection;
+using Xunit;
+
+namespace System.Web.Mvc.ExpressionUtil.Test
+{
+ public class BinaryExpressionFingerprintTest
+ {
+ [Fact]
+ public void Properties()
+ {
+ // Arrange
+ ExpressionType expectedNodeType = ExpressionType.Add;
+ Type expectedType = typeof(DateTime);
+ MethodInfo expectedMethod = typeof(DateTime).GetMethod("op_Addition", new Type[] { typeof(DateTime), typeof(TimeSpan) });
+
+ // Act
+ BinaryExpressionFingerprint fingerprint = new BinaryExpressionFingerprint(expectedNodeType, expectedType, expectedMethod);
+
+ // Assert
+ Assert.Equal(expectedNodeType, fingerprint.NodeType);
+ Assert.Equal(expectedType, fingerprint.Type);
+ Assert.Equal(expectedMethod, fingerprint.Method);
+ }
+
+ [Fact]
+ public void Comparison_Equality()
+ {
+ // Arrange
+ ExpressionType nodeType = ExpressionType.Add;
+ Type type = typeof(DateTime);
+ MethodInfo method = typeof(DateTime).GetMethod("op_Addition", new Type[] { typeof(DateTime), typeof(TimeSpan) });
+
+ // Act
+ BinaryExpressionFingerprint fingerprint1 = new BinaryExpressionFingerprint(nodeType, type, method);
+ BinaryExpressionFingerprint fingerprint2 = new BinaryExpressionFingerprint(nodeType, type, method);
+
+ // Assert
+ Assert.Equal(fingerprint1, fingerprint2);
+ Assert.Equal(fingerprint1.GetHashCode(), fingerprint2.GetHashCode());
+ }
+
+ [Fact]
+ public void Comparison_Inequality_FingerprintType()
+ {
+ // Arrange
+ ExpressionType nodeType = ExpressionType.Add;
+ Type type = typeof(DateTime);
+ MethodInfo method = typeof(DateTime).GetMethod("op_Addition", new Type[] { typeof(DateTime), typeof(TimeSpan) });
+
+ // Act
+ BinaryExpressionFingerprint fingerprint1 = new BinaryExpressionFingerprint(nodeType, type, method);
+ DummyExpressionFingerprint fingerprint2 = new DummyExpressionFingerprint(nodeType, type);
+
+ // Assert
+ Assert.NotEqual<ExpressionFingerprint>(fingerprint1, fingerprint2);
+ }
+
+ [Fact]
+ public void Comparison_Inequality_Method()
+ {
+ // Arrange
+ ExpressionType nodeType = ExpressionType.Add;
+ Type type = typeof(DateTime);
+ MethodInfo method = typeof(DateTime).GetMethod("op_Addition", new Type[] { typeof(DateTime), typeof(TimeSpan) });
+
+ // Act
+ BinaryExpressionFingerprint fingerprint1 = new BinaryExpressionFingerprint(nodeType, type, method);
+ BinaryExpressionFingerprint fingerprint2 = new BinaryExpressionFingerprint(nodeType, type, null /* method */);
+
+ // Assert
+ Assert.NotEqual(fingerprint1, fingerprint2);
+ }
+
+ [Fact]
+ public void Comparison_Inequality_Type()
+ {
+ // Arrange
+ ExpressionType nodeType = ExpressionType.Add;
+ Type type = typeof(DateTime);
+ MethodInfo method = typeof(DateTime).GetMethod("op_Addition", new Type[] { typeof(DateTime), typeof(TimeSpan) });
+
+ // Act
+ BinaryExpressionFingerprint fingerprint1 = new BinaryExpressionFingerprint(nodeType, type, method);
+ BinaryExpressionFingerprint fingerprint2 = new BinaryExpressionFingerprint(nodeType, typeof(object), method);
+
+ // Assert
+ Assert.NotEqual(fingerprint1, fingerprint2);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/ExpressionUtil/Test/CachedExpressionCompilerTest.cs b/test/System.Web.Mvc.Test/ExpressionUtil/Test/CachedExpressionCompilerTest.cs
new file mode 100644
index 00000000..b87a0642
--- /dev/null
+++ b/test/System.Web.Mvc.Test/ExpressionUtil/Test/CachedExpressionCompilerTest.cs
@@ -0,0 +1,141 @@
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Text;
+using Xunit;
+
+namespace System.Web.Mvc.ExpressionUtil.Test
+{
+ public class CachedExpressionCompilerTest
+ {
+ private delegate Func<TIn, TOut> Compiler<TIn, TOut>(Expression<Func<TIn, TOut>> expr);
+
+ [Fact]
+ public void Compiler_CompileFromConstLookup()
+ {
+ // Arrange
+ Expression<Func<string, int>> expr = model => 42;
+ var compiler = GetCompilerMethod<string, int>("CompileFromConstLookup");
+
+ // Act
+ var func = compiler(expr);
+ int result = func("any model");
+
+ // Assert
+ Assert.Equal(42, result);
+ }
+
+ [Fact]
+ public void Compiler_CompileFromFingerprint()
+ {
+ // Arrange
+ Expression<Func<string, int>> expr = s => 20 * s.Length;
+ var compiler = GetCompilerMethod<string, int>("CompileFromFingerprint");
+
+ // Act
+ var func = compiler(expr);
+ int result = func("hello");
+
+ // Assert
+ Assert.Equal(100, result);
+ }
+
+ [Fact]
+ public void Compiler_CompileFromIdentityFunc()
+ {
+ // Arrange
+ Expression<Func<string, string>> expr = model => model;
+ var compiler = GetCompilerMethod<string, string>("CompileFromIdentityFunc");
+
+ // Act
+ var func = compiler(expr);
+ string result = func("hello");
+
+ // Assert
+ Assert.Equal("hello", result);
+ }
+
+ [Fact]
+ public void Compiler_CompileFromMemberAccess_CapturedLocal()
+ {
+ // Arrange
+ string capturedLocal = "goodbye";
+ Expression<Func<string, string>> expr = _ => capturedLocal;
+ var compiler = GetCompilerMethod<string, string>("CompileFromMemberAccess");
+
+ // Act
+ var func = compiler(expr);
+ string result = func("hello");
+
+ // Assert
+ Assert.Equal("goodbye", result);
+ }
+
+ [Fact]
+ public void Compiler_CompileFromMemberAccess_ParameterInstanceMember()
+ {
+ // Arrange
+ Expression<Func<string, int>> expr = s => s.Length;
+ var compiler = GetCompilerMethod<string, int>("CompileFromMemberAccess");
+
+ // Act
+ var func = compiler(expr);
+ int result = func("hello");
+
+ // Assert
+ Assert.Equal(5, result);
+ }
+
+ [Fact]
+ public void Compiler_CompileFromMemberAccess_StaticMember()
+ {
+ // Arrange
+ Expression<Func<string, string>> expr = _ => String.Empty;
+ var compiler = GetCompilerMethod<string, string>("CompileFromMemberAccess");
+
+ // Act
+ var func = compiler(expr);
+ string result = func("hello");
+
+ // Assert
+ Assert.Equal("", result);
+ }
+
+ [Fact]
+ public void Compiler_CompileSlow()
+ {
+ // Arrange
+ Expression<Func<string, string>> expr = s => new StringBuilder(s).ToString();
+ var compiler = GetCompilerMethod<string, string>("CompileSlow");
+
+ // Act
+ var func = compiler(expr);
+ string result = func("hello");
+
+ // Assert
+ Assert.Equal("hello", result);
+ }
+
+ [Fact]
+ public void Process()
+ {
+ // Arrange
+ Expression<Func<string, string>> expr = s => new StringBuilder(s).ToString();
+
+ // Act
+ var func = CachedExpressionCompiler.Process(expr);
+ string result = func("hello");
+
+ // Assert
+ Assert.Equal("hello", result);
+ }
+
+ // helper to create a delegate to a private method on the compiler
+ private static Compiler<TIn, TOut> GetCompilerMethod<TIn, TOut>(string methodName)
+ {
+ Type openCompilerType = typeof(CachedExpressionCompiler).GetNestedType("Compiler`2", BindingFlags.NonPublic);
+ Type closedCompilerType = openCompilerType.MakeGenericType(typeof(TIn), typeof(TOut));
+ MethodInfo targetMethod = closedCompilerType.GetMethod(methodName, BindingFlags.Static | BindingFlags.NonPublic);
+ return (Compiler<TIn, TOut>)Delegate.CreateDelegate(typeof(Compiler<TIn, TOut>), targetMethod);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/ExpressionUtil/Test/ConditionalExpressionFingerprintTest.cs b/test/System.Web.Mvc.Test/ExpressionUtil/Test/ConditionalExpressionFingerprintTest.cs
new file mode 100644
index 00000000..27367e43
--- /dev/null
+++ b/test/System.Web.Mvc.Test/ExpressionUtil/Test/ConditionalExpressionFingerprintTest.cs
@@ -0,0 +1,69 @@
+using System.Linq.Expressions;
+using Xunit;
+
+namespace System.Web.Mvc.ExpressionUtil.Test
+{
+ public class ConditionalExpressionFingerprintTest
+ {
+ [Fact]
+ public void Properties()
+ {
+ // Arrange
+ ExpressionType expectedNodeType = ExpressionType.Conditional;
+ Type expectedType = typeof(object);
+
+ // Act
+ ConditionalExpressionFingerprint fingerprint = new ConditionalExpressionFingerprint(expectedNodeType, expectedType);
+
+ // Assert
+ Assert.Equal(expectedNodeType, fingerprint.NodeType);
+ Assert.Equal(expectedType, fingerprint.Type);
+ }
+
+ [Fact]
+ public void Comparison_Equality()
+ {
+ // Arrange
+ ExpressionType nodeType = ExpressionType.Conditional;
+ Type type = typeof(object);
+
+ // Act
+ ConditionalExpressionFingerprint fingerprint1 = new ConditionalExpressionFingerprint(nodeType, type);
+ ConditionalExpressionFingerprint fingerprint2 = new ConditionalExpressionFingerprint(nodeType, type);
+
+ // Assert
+ Assert.Equal(fingerprint1, fingerprint2);
+ Assert.Equal(fingerprint1.GetHashCode(), fingerprint2.GetHashCode());
+ }
+
+ [Fact]
+ public void Comparison_Inequality_FingerprintType()
+ {
+ // Arrange
+ ExpressionType nodeType = ExpressionType.Conditional;
+ Type type = typeof(object);
+
+ // Act
+ ConditionalExpressionFingerprint fingerprint1 = new ConditionalExpressionFingerprint(nodeType, type);
+ DummyExpressionFingerprint fingerprint2 = new DummyExpressionFingerprint(nodeType, type);
+
+ // Assert
+ Assert.NotEqual<ExpressionFingerprint>(fingerprint1, fingerprint2);
+ }
+
+ [Fact]
+ public void Comparison_Inequality_Type()
+ {
+ // Arrange
+ ExpressionType nodeType = ExpressionType.Conditional;
+ Type type = typeof(object);
+
+ // Act
+ ConditionalExpressionFingerprint fingerprint1 = new ConditionalExpressionFingerprint(nodeType, type);
+ ConditionalExpressionFingerprint fingerprint2 = new ConditionalExpressionFingerprint(nodeType, typeof(string));
+
+ // Assert
+ Assert.NotEqual(fingerprint1, fingerprint2);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/ExpressionUtil/Test/ConstantExpressionFingerprintTest.cs b/test/System.Web.Mvc.Test/ExpressionUtil/Test/ConstantExpressionFingerprintTest.cs
new file mode 100644
index 00000000..d71f920e
--- /dev/null
+++ b/test/System.Web.Mvc.Test/ExpressionUtil/Test/ConstantExpressionFingerprintTest.cs
@@ -0,0 +1,69 @@
+using System.Linq.Expressions;
+using Xunit;
+
+namespace System.Web.Mvc.ExpressionUtil.Test
+{
+ public class ConstantExpressionFingerprintTest
+ {
+ [Fact]
+ public void Properties()
+ {
+ // Arrange
+ ExpressionType expectedNodeType = ExpressionType.Constant;
+ Type expectedType = typeof(object);
+
+ // Act
+ ConstantExpressionFingerprint fingerprint = new ConstantExpressionFingerprint(expectedNodeType, expectedType);
+
+ // Assert
+ Assert.Equal(expectedNodeType, fingerprint.NodeType);
+ Assert.Equal(expectedType, fingerprint.Type);
+ }
+
+ [Fact]
+ public void Comparison_Equality()
+ {
+ // Arrange
+ ExpressionType nodeType = ExpressionType.Constant;
+ Type type = typeof(object);
+
+ // Act
+ ConstantExpressionFingerprint fingerprint1 = new ConstantExpressionFingerprint(nodeType, type);
+ ConstantExpressionFingerprint fingerprint2 = new ConstantExpressionFingerprint(nodeType, type);
+
+ // Assert
+ Assert.Equal(fingerprint1, fingerprint2);
+ Assert.Equal(fingerprint1.GetHashCode(), fingerprint2.GetHashCode());
+ }
+
+ [Fact]
+ public void Comparison_Inequality_FingerprintType()
+ {
+ // Arrange
+ ExpressionType nodeType = ExpressionType.Constant;
+ Type type = typeof(object);
+
+ // Act
+ ConstantExpressionFingerprint fingerprint1 = new ConstantExpressionFingerprint(nodeType, type);
+ DummyExpressionFingerprint fingerprint2 = new DummyExpressionFingerprint(nodeType, type);
+
+ // Assert
+ Assert.NotEqual<ExpressionFingerprint>(fingerprint1, fingerprint2);
+ }
+
+ [Fact]
+ public void Comparison_Inequality_Type()
+ {
+ // Arrange
+ ExpressionType nodeType = ExpressionType.Constant;
+ Type type = typeof(object);
+
+ // Act
+ ConstantExpressionFingerprint fingerprint1 = new ConstantExpressionFingerprint(nodeType, type);
+ ConstantExpressionFingerprint fingerprint2 = new ConstantExpressionFingerprint(nodeType, typeof(string));
+
+ // Assert
+ Assert.NotEqual(fingerprint1, fingerprint2);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/ExpressionUtil/Test/DefaultExpressionFingerprintTest.cs b/test/System.Web.Mvc.Test/ExpressionUtil/Test/DefaultExpressionFingerprintTest.cs
new file mode 100644
index 00000000..28bd3b1b
--- /dev/null
+++ b/test/System.Web.Mvc.Test/ExpressionUtil/Test/DefaultExpressionFingerprintTest.cs
@@ -0,0 +1,69 @@
+using System.Linq.Expressions;
+using Xunit;
+
+namespace System.Web.Mvc.ExpressionUtil.Test
+{
+ public class DefaultExpressionFingerprintTest
+ {
+ [Fact]
+ public void Properties()
+ {
+ // Arrange
+ ExpressionType expectedNodeType = ExpressionType.Default;
+ Type expectedType = typeof(object);
+
+ // Act
+ DefaultExpressionFingerprint fingerprint = new DefaultExpressionFingerprint(expectedNodeType, expectedType);
+
+ // Assert
+ Assert.Equal(expectedNodeType, fingerprint.NodeType);
+ Assert.Equal(expectedType, fingerprint.Type);
+ }
+
+ [Fact]
+ public void Comparison_Equality()
+ {
+ // Arrange
+ ExpressionType nodeType = ExpressionType.Default;
+ Type type = typeof(object);
+
+ // Act
+ DefaultExpressionFingerprint fingerprint1 = new DefaultExpressionFingerprint(nodeType, type);
+ DefaultExpressionFingerprint fingerprint2 = new DefaultExpressionFingerprint(nodeType, type);
+
+ // Assert
+ Assert.Equal(fingerprint1, fingerprint2);
+ Assert.Equal(fingerprint1.GetHashCode(), fingerprint2.GetHashCode());
+ }
+
+ [Fact]
+ public void Comparison_Inequality_FingerprintType()
+ {
+ // Arrange
+ ExpressionType nodeType = ExpressionType.Default;
+ Type type = typeof(object);
+
+ // Act
+ DefaultExpressionFingerprint fingerprint1 = new DefaultExpressionFingerprint(nodeType, type);
+ DummyExpressionFingerprint fingerprint2 = new DummyExpressionFingerprint(nodeType, type);
+
+ // Assert
+ Assert.NotEqual<ExpressionFingerprint>(fingerprint1, fingerprint2);
+ }
+
+ [Fact]
+ public void Comparison_Inequality_NodeType()
+ {
+ // Arrange
+ ExpressionType nodeType = ExpressionType.Default;
+ Type type = typeof(object);
+
+ // Act
+ DefaultExpressionFingerprint fingerprint1 = new DefaultExpressionFingerprint(nodeType, type);
+ DefaultExpressionFingerprint fingerprint2 = new DefaultExpressionFingerprint(nodeType, typeof(string));
+
+ // Assert
+ Assert.NotEqual(fingerprint1, fingerprint2);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/ExpressionUtil/Test/DummyExpressionFingerprint.cs b/test/System.Web.Mvc.Test/ExpressionUtil/Test/DummyExpressionFingerprint.cs
new file mode 100644
index 00000000..0d7231ed
--- /dev/null
+++ b/test/System.Web.Mvc.Test/ExpressionUtil/Test/DummyExpressionFingerprint.cs
@@ -0,0 +1,13 @@
+using System.Linq.Expressions;
+
+namespace System.Web.Mvc.ExpressionUtil.Test
+{
+ // Represents an ExpressionFingerprint that is of the wrong type.
+ internal sealed class DummyExpressionFingerprint : ExpressionFingerprint
+ {
+ public DummyExpressionFingerprint(ExpressionType nodeType, Type type)
+ : base(nodeType, type)
+ {
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/ExpressionUtil/Test/ExpressionFingerprintTest.cs b/test/System.Web.Mvc.Test/ExpressionUtil/Test/ExpressionFingerprintTest.cs
new file mode 100644
index 00000000..2e2bbdf0
--- /dev/null
+++ b/test/System.Web.Mvc.Test/ExpressionUtil/Test/ExpressionFingerprintTest.cs
@@ -0,0 +1,42 @@
+using System.Linq.Expressions;
+using Xunit;
+
+namespace System.Web.Mvc.ExpressionUtil.Test
+{
+ public class ExpressionFingerprintTest
+ {
+ [Fact]
+ public void Comparison_Equality()
+ {
+ // Act
+ DummyExpressionFingerprint fingerprint1 = new DummyExpressionFingerprint(ExpressionType.Default, typeof(object));
+ DummyExpressionFingerprint fingerprint2 = new DummyExpressionFingerprint(ExpressionType.Default, typeof(object));
+
+ // Assert
+ Assert.Equal(fingerprint1, fingerprint2);
+ Assert.Equal(fingerprint1.GetHashCode(), fingerprint2.GetHashCode());
+ }
+
+ [Fact]
+ public void Comparison_Inequality_NodeType()
+ {
+ // Act
+ DummyExpressionFingerprint fingerprint1 = new DummyExpressionFingerprint(ExpressionType.Default, typeof(object));
+ DummyExpressionFingerprint fingerprint2 = new DummyExpressionFingerprint(ExpressionType.Parameter, typeof(object));
+
+ // Assert
+ Assert.NotEqual(fingerprint1, fingerprint2);
+ }
+
+ [Fact]
+ public void Comparison_Inequality_Type()
+ {
+ // Act
+ DummyExpressionFingerprint fingerprint1 = new DummyExpressionFingerprint(ExpressionType.Default, typeof(object));
+ DummyExpressionFingerprint fingerprint2 = new DummyExpressionFingerprint(ExpressionType.Default, typeof(string));
+
+ // Assert
+ Assert.NotEqual(fingerprint1, fingerprint2);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/ExpressionUtil/Test/FingerprintingExpressionVisitorTest.cs b/test/System.Web.Mvc.Test/ExpressionUtil/Test/FingerprintingExpressionVisitorTest.cs
new file mode 100644
index 00000000..3e71de49
--- /dev/null
+++ b/test/System.Web.Mvc.Test/ExpressionUtil/Test/FingerprintingExpressionVisitorTest.cs
@@ -0,0 +1,301 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Text;
+using Xunit;
+
+namespace System.Web.Mvc.ExpressionUtil.Test
+{
+ public class FingerprintingExpressionVisitorTest
+ {
+ private const ExpressionFingerprint _nullFingerprint = null;
+
+ [Fact]
+ public void TypeOverridesAllMethods()
+ {
+ // Ensures that the FingerprintingExpressionVisitor type overrides all VisitXxx methods so that
+ // it can properly set the "I gave up" flag when it encounters an Expression it's not familiar
+ // with.
+
+ var methodsOnExpressionVisitorRequiringOverride = typeof(ExpressionVisitor).GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance).Where(mi => mi.IsVirtual).Select(mi => mi.GetBaseDefinition()).Where(mi => mi.DeclaringType == typeof(ExpressionVisitor));
+ var methodsOnFingerprintingExpressionVisitor = typeof(FingerprintingExpressionVisitor).GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance).Where(mi => mi.DeclaringType == typeof(FingerprintingExpressionVisitor));
+
+ var missingMethods = methodsOnExpressionVisitorRequiringOverride.Except(methodsOnFingerprintingExpressionVisitor.Select(mi => mi.GetBaseDefinition())).ToArray();
+ if (missingMethods.Length != 0)
+ {
+ StringBuilder sb = new StringBuilder("The following methods are declared on ExpressionVisitor and must be overridden on FingerprintingExpressionVisitor:");
+ foreach (MethodInfo method in missingMethods)
+ {
+ sb.AppendLine();
+ sb.Append(method);
+ }
+ Assert.True(false, sb.ToString());
+ }
+ }
+
+ [Fact]
+ public void Visit_Null()
+ {
+ // Arrange
+
+ // fingerprints as [ NULL ]
+ Expression expr = null;
+
+ // Act
+ List<object> capturedConstants;
+ ExpressionFingerprintChain fingerprint = FingerprintingExpressionVisitor.GetFingerprintChain(expr, out capturedConstants);
+
+ // Assert
+ Assert.Empty(capturedConstants);
+ AssertChainEquals(fingerprint, _nullFingerprint);
+ }
+
+ [Fact]
+ public void Visit_Unknown()
+ {
+ // Arrange
+
+ // if we fingerprinted ctors, would fingerprint as [ NEW(StringBuilder(int)):StringBuilder, PARAM(0):int ]
+ // but since we don't fingerprint ctors, should just return null (signaling failure)
+ Expression expr = (Expression<Func<int, StringBuilder>>)(capacity => new StringBuilder(capacity));
+
+ // Act
+ List<object> capturedConstants;
+ ExpressionFingerprintChain fingerprint = FingerprintingExpressionVisitor.GetFingerprintChain(expr, out capturedConstants);
+
+ // Assert
+ Assert.Null(fingerprint); // Can't fingerprint ctor
+ Assert.Null(capturedConstants); // Can't fingerprint ctor
+ }
+
+ [Fact]
+ public void VisitBinary()
+ {
+ // Arrange
+
+ // fingerprints as [ OP_GREATERTHAN:bool, CONST:int, CONST:int ]
+ Expression expr = Expression.MakeBinary(ExpressionType.GreaterThan, Expression.Constant(42), Expression.Constant(84));
+
+ // Act
+ List<object> capturedConstants;
+ ExpressionFingerprintChain fingerprint = FingerprintingExpressionVisitor.GetFingerprintChain(expr, out capturedConstants);
+
+ // Assert
+ Assert.Equal(new object[] { 42, 84 }, capturedConstants.ToArray());
+ AssertChainEquals(fingerprint,
+ new BinaryExpressionFingerprint(ExpressionType.GreaterThan, typeof(bool), null /* method */),
+ new ConstantExpressionFingerprint(ExpressionType.Constant, typeof(int)),
+ new ConstantExpressionFingerprint(ExpressionType.Constant, typeof(int)));
+ }
+
+ [Fact]
+ public void VisitConditional()
+ {
+ // Arrange
+
+ // fingerprints as [ CONDITIONAL:int, CONST:bool, CONST:int, CONST:int ]
+ Expression expr = Expression.Condition(Expression.Constant(true), Expression.Constant(42), Expression.Constant(84));
+
+ // Act
+ List<object> capturedConstants;
+ ExpressionFingerprintChain fingerprint = FingerprintingExpressionVisitor.GetFingerprintChain(expr, out capturedConstants);
+
+ // Assert
+ Assert.Equal(new object[] { true, 42, 84 }, capturedConstants.ToArray());
+ AssertChainEquals(fingerprint,
+ new ConditionalExpressionFingerprint(ExpressionType.Conditional, typeof(int)),
+ new ConstantExpressionFingerprint(ExpressionType.Constant, typeof(bool)),
+ new ConstantExpressionFingerprint(ExpressionType.Constant, typeof(int)),
+ new ConstantExpressionFingerprint(ExpressionType.Constant, typeof(int)));
+ }
+
+ [Fact]
+ public void VisitConstant()
+ {
+ // Arrange
+
+ // fingerprints as [ CONST:int ]
+ Expression expr = Expression.Constant(42);
+
+ // Act
+ List<object> capturedConstants;
+ ExpressionFingerprintChain fingerprint = FingerprintingExpressionVisitor.GetFingerprintChain(expr, out capturedConstants);
+
+ // Assert
+ Assert.Equal(new object[] { 42 }, capturedConstants.ToArray());
+ AssertChainEquals(fingerprint,
+ new ConstantExpressionFingerprint(ExpressionType.Constant, typeof(int)));
+ }
+
+ [Fact]
+ public void VisitDefault()
+ {
+ // Arrange
+
+ // fingerprints as [ DEFAULT:int ]
+ Expression expr = Expression.Default(typeof(int));
+
+ // Act
+ List<object> capturedConstants;
+ ExpressionFingerprintChain fingerprint = FingerprintingExpressionVisitor.GetFingerprintChain(expr, out capturedConstants);
+
+ // Assert
+ Assert.Empty(capturedConstants);
+ AssertChainEquals(fingerprint, new DefaultExpressionFingerprint(ExpressionType.Default, typeof(int)));
+ }
+
+ [Fact]
+ public void VisitIndex()
+ {
+ // Arrange
+
+ // fingerprints as [ INDEX:object, PARAM(0):object[], CONST:int ]
+ Expression expr = Expression.MakeIndex(Expression.Parameter(typeof(object[])), null /* indexer */, new Expression[] { Expression.Constant(42) });
+
+ // Act
+ List<object> capturedConstants;
+ ExpressionFingerprintChain fingerprint = FingerprintingExpressionVisitor.GetFingerprintChain(expr, out capturedConstants);
+
+ // Assert
+ Assert.Equal(new object[] { 42 }, capturedConstants.ToArray());
+ AssertChainEquals(fingerprint,
+ new IndexExpressionFingerprint(ExpressionType.Index, typeof(object), null /* indexer */),
+ new ParameterExpressionFingerprint(ExpressionType.Parameter, typeof(object[]), 0 /* parameterIndex */),
+ new ConstantExpressionFingerprint(ExpressionType.Constant, typeof(int)));
+ }
+
+ [Fact]
+ public void VisitLambda()
+ {
+ // Arrange
+
+ // fingerprints as [ LAMBDA:Func<string, int>, CONST:int, PARAM(0):string ]
+ Expression expr = (Expression<Func<string, int>>)(x => 42);
+
+ // Act
+ List<object> capturedConstants;
+ ExpressionFingerprintChain fingerprint = FingerprintingExpressionVisitor.GetFingerprintChain(expr, out capturedConstants);
+
+ // Assert
+ Assert.Equal(new object[] { 42 }, capturedConstants.ToArray());
+ AssertChainEquals(fingerprint,
+ new LambdaExpressionFingerprint(ExpressionType.Lambda, typeof(Func<string, int>)),
+ new ConstantExpressionFingerprint(ExpressionType.Constant, typeof(int)),
+ new ParameterExpressionFingerprint(ExpressionType.Parameter, typeof(string), 0 /* parameterIndex */));
+ }
+
+ [Fact]
+ public void VisitMember()
+ {
+ // Arrange
+
+ // fingerprints as [ MEMBER(String.Empty):string, NULL ]
+ Expression expr = Expression.Field(null, typeof(string), "Empty");
+
+ // Act
+ List<object> capturedConstants;
+ ExpressionFingerprintChain fingerprint = FingerprintingExpressionVisitor.GetFingerprintChain(expr, out capturedConstants);
+
+ // Assert
+ Assert.Empty(capturedConstants);
+ AssertChainEquals(fingerprint,
+ new MemberExpressionFingerprint(ExpressionType.MemberAccess, typeof(string), typeof(string).GetField("Empty")),
+ _nullFingerprint);
+ }
+
+ [Fact]
+ public void VisitMethodCall()
+ {
+ // Arrange
+
+ // fingerprints as [ CALL(GC.KeepAlive):void, NULL, PARAM(0):object ]
+ Expression expr = Expression.Call(typeof(GC).GetMethod("KeepAlive"), Expression.Parameter(typeof(object)));
+
+ // Act
+ List<object> capturedConstants;
+ ExpressionFingerprintChain fingerprint = FingerprintingExpressionVisitor.GetFingerprintChain(expr, out capturedConstants);
+
+ // Assert
+ Assert.Empty(capturedConstants);
+ AssertChainEquals(fingerprint,
+ new MethodCallExpressionFingerprint(ExpressionType.Call, typeof(void), typeof(GC).GetMethod("KeepAlive")),
+ _nullFingerprint,
+ new ParameterExpressionFingerprint(ExpressionType.Parameter, typeof(object), 0 /* parameterIndex */));
+ }
+
+ [Fact]
+ public void VisitParameter()
+ {
+ // Arrange
+
+ // fingerprints as [ LAMBDA:Func<int, int, int>, OP_ADD:int, OP_ADD:int, OP_ADD:int, PARAM(0):int, PARAM(0):int, PARAM(1):int, PARAM(0):int, PARAM(1):int, PARAM(0):int ]
+ // (note that the parameters are out of order since 'y' is used first, but this is ok due
+ // to preservation of alpha equivalence within the VisitParameter method.)
+ Expression expr = (Expression<Func<int, int, int>>)((x, y) => y + y + x + y);
+
+ // Act
+ List<object> capturedConstants;
+ ExpressionFingerprintChain fingerprint = FingerprintingExpressionVisitor.GetFingerprintChain(expr, out capturedConstants);
+
+ // Assert
+ Assert.Empty(capturedConstants);
+ AssertChainEquals(fingerprint,
+ new LambdaExpressionFingerprint(ExpressionType.Lambda, typeof(Func<int, int, int>)),
+ new BinaryExpressionFingerprint(ExpressionType.Add, typeof(int), null /* method */),
+ new BinaryExpressionFingerprint(ExpressionType.Add, typeof(int), null /* method */),
+ new BinaryExpressionFingerprint(ExpressionType.Add, typeof(int), null /* method */),
+ new ParameterExpressionFingerprint(ExpressionType.Parameter, typeof(int), 0 /* parameterIndex */),
+ new ParameterExpressionFingerprint(ExpressionType.Parameter, typeof(int), 0 /* parameterIndex */),
+ new ParameterExpressionFingerprint(ExpressionType.Parameter, typeof(int), 1 /* parameterIndex */),
+ new ParameterExpressionFingerprint(ExpressionType.Parameter, typeof(int), 0 /* parameterIndex */),
+ new ParameterExpressionFingerprint(ExpressionType.Parameter, typeof(int), 1 /* parameterIndex */),
+ new ParameterExpressionFingerprint(ExpressionType.Parameter, typeof(int), 0 /* parameterIndex */));
+ }
+
+ [Fact]
+ public void VisitTypeBinary()
+ {
+ // Arrange
+
+ // fingerprints as [ TYPEIS(DateTime):bool, CONST:string ]
+ Expression expr = Expression.TypeIs(Expression.Constant("hello"), typeof(DateTime));
+
+ // Act
+ List<object> capturedConstants;
+ ExpressionFingerprintChain fingerprint = FingerprintingExpressionVisitor.GetFingerprintChain(expr, out capturedConstants);
+
+ // Assert
+ Assert.Equal(new object[] { "hello" }, capturedConstants.ToArray());
+ AssertChainEquals(fingerprint,
+ new TypeBinaryExpressionFingerprint(ExpressionType.TypeIs, typeof(bool), typeof(DateTime)),
+ new ConstantExpressionFingerprint(ExpressionType.Constant, typeof(string)));
+ }
+
+ [Fact]
+ public void VisitUnary()
+ {
+ // Arrange
+
+ // fingerprints as [ OP_NOT:int, PARAM:int ]
+ Expression expr = Expression.Not(Expression.Parameter(typeof(int)));
+
+ // Act
+ List<object> capturedConstants;
+ ExpressionFingerprintChain fingerprint = FingerprintingExpressionVisitor.GetFingerprintChain(expr, out capturedConstants);
+
+ // Assert
+ Assert.Empty(capturedConstants);
+ AssertChainEquals(fingerprint,
+ new UnaryExpressionFingerprint(ExpressionType.Not, typeof(int), null /* method */),
+ new ParameterExpressionFingerprint(ExpressionType.Parameter, typeof(int), 0 /* parameterIndex */));
+ }
+
+ internal static void AssertChainEquals(ExpressionFingerprintChain fingerprintChain, params ExpressionFingerprint[] expectedElements)
+ {
+ ExpressionFingerprintChain newChain = new ExpressionFingerprintChain();
+ newChain.Elements.AddRange(expectedElements);
+ Assert.Equal(fingerprintChain, newChain);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/ExpressionUtil/Test/HoistingExpressionVisitorTest.cs b/test/System.Web.Mvc.Test/ExpressionUtil/Test/HoistingExpressionVisitorTest.cs
new file mode 100644
index 00000000..ee87d224
--- /dev/null
+++ b/test/System.Web.Mvc.Test/ExpressionUtil/Test/HoistingExpressionVisitorTest.cs
@@ -0,0 +1,45 @@
+using System.Collections.Generic;
+using System.Linq.Expressions;
+using Xunit;
+
+namespace System.Web.Mvc.ExpressionUtil.Test
+{
+ public class HoistingExpressionVisitorTest
+ {
+ [Fact]
+ public void Hoist()
+ {
+ // Arrange
+ Expression<Func<string, int>> expr = s => 2 * s.Length + 1;
+
+ // Act
+ Expression<Hoisted<string, int>> hoisted = HoistingExpressionVisitor<string, int>.Hoist(expr);
+
+ // Assert
+ // new expression should be (s, capturedConstants) => (int)(capturedConstants[0]) * s.Length + (int)(capturedConstants[1])
+ // with fingerprint [ LAMBDA:Hoisted<string, int>, OP_ADD:int, OP_MULTIPLY:int, OP_CAST:int, INDEX(List<object>.get_Item):object, PARAM(0):List<object>, CONST:int, MEMBER(String.Length):int, PARAM(1):string, OP_CAST:int, INDEX(List<object>.get_Item):object, PARAM(0):List<object>, CONST:int, PARAM(1):string, PARAM(0):List<object> ]
+
+ List<object> capturedConstants;
+ ExpressionFingerprintChain fingerprint = FingerprintingExpressionVisitor.GetFingerprintChain(hoisted, out capturedConstants);
+
+ Assert.Equal(new object[] { 0, 1 }, capturedConstants.ToArray()); // these are constants from the hoisted expression (array indexes), not the original expression
+ FingerprintingExpressionVisitorTest.AssertChainEquals(
+ fingerprint,
+ new LambdaExpressionFingerprint(ExpressionType.Lambda, typeof(Hoisted<string, int>)),
+ new BinaryExpressionFingerprint(ExpressionType.Add, typeof(int), null /* method */),
+ new BinaryExpressionFingerprint(ExpressionType.Multiply, typeof(int), null /* method */),
+ new UnaryExpressionFingerprint(ExpressionType.Convert, typeof(int), null /* method */),
+ new IndexExpressionFingerprint(ExpressionType.Index, typeof(object), typeof(List<object>).GetProperty("Item")),
+ new ParameterExpressionFingerprint(ExpressionType.Parameter, typeof(List<object>), 0 /* parameterIndex */),
+ new ConstantExpressionFingerprint(ExpressionType.Constant, typeof(int)),
+ new MemberExpressionFingerprint(ExpressionType.MemberAccess, typeof(int), typeof(string).GetProperty("Length")),
+ new ParameterExpressionFingerprint(ExpressionType.Parameter, typeof(string), 1 /* parameterIndex */),
+ new UnaryExpressionFingerprint(ExpressionType.Convert, typeof(int), null /* method */),
+ new IndexExpressionFingerprint(ExpressionType.Index, typeof(object), typeof(List<object>).GetProperty("Item")),
+ new ParameterExpressionFingerprint(ExpressionType.Parameter, typeof(List<object>), 0 /* parameterIndex */),
+ new ConstantExpressionFingerprint(ExpressionType.Constant, typeof(int)),
+ new ParameterExpressionFingerprint(ExpressionType.Parameter, typeof(string), 1 /* parameterIndex */),
+ new ParameterExpressionFingerprint(ExpressionType.Parameter, typeof(List<object>), 0 /* parameterIndex */));
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/ExpressionUtil/Test/IndexExpressionFingerprintTest.cs b/test/System.Web.Mvc.Test/ExpressionUtil/Test/IndexExpressionFingerprintTest.cs
new file mode 100644
index 00000000..9005b7ab
--- /dev/null
+++ b/test/System.Web.Mvc.Test/ExpressionUtil/Test/IndexExpressionFingerprintTest.cs
@@ -0,0 +1,91 @@
+using System.Linq.Expressions;
+using System.Reflection;
+using Xunit;
+
+namespace System.Web.Mvc.ExpressionUtil.Test
+{
+ public class IndexExpressionFingerprintTest
+ {
+ [Fact]
+ public void Properties()
+ {
+ // Arrange
+ ExpressionType expectedNodeType = ExpressionType.Index;
+ Type expectedType = typeof(char);
+ PropertyInfo expectedIndexer = typeof(string).GetProperty("Chars");
+
+ // Act
+ IndexExpressionFingerprint fingerprint = new IndexExpressionFingerprint(expectedNodeType, expectedType, expectedIndexer);
+
+ // Assert
+ Assert.Equal(expectedNodeType, fingerprint.NodeType);
+ Assert.Equal(expectedType, fingerprint.Type);
+ Assert.Equal(expectedIndexer, fingerprint.Indexer);
+ }
+
+ [Fact]
+ public void Comparison_Equality()
+ {
+ // Arrange
+ ExpressionType nodeType = ExpressionType.Index;
+ Type type = typeof(char);
+ PropertyInfo indexer = typeof(string).GetProperty("Chars");
+
+ // Act
+ IndexExpressionFingerprint fingerprint1 = new IndexExpressionFingerprint(nodeType, type, indexer);
+ IndexExpressionFingerprint fingerprint2 = new IndexExpressionFingerprint(nodeType, type, indexer);
+
+ // Assert
+ Assert.Equal(fingerprint1, fingerprint2);
+ Assert.Equal(fingerprint1.GetHashCode(), fingerprint2.GetHashCode());
+ }
+
+ [Fact]
+ public void Comparison_Inequality_FingerprintType()
+ {
+ // Arrange
+ ExpressionType nodeType = ExpressionType.Index;
+ Type type = typeof(char);
+ PropertyInfo indexer = typeof(string).GetProperty("Chars");
+
+ // Act
+ IndexExpressionFingerprint fingerprint1 = new IndexExpressionFingerprint(nodeType, type, indexer);
+ DummyExpressionFingerprint fingerprint2 = new DummyExpressionFingerprint(nodeType, type);
+
+ // Assert
+ Assert.NotEqual<ExpressionFingerprint>(fingerprint1, fingerprint2);
+ }
+
+ [Fact]
+ public void Comparison_Inequality_Indexer()
+ {
+ // Arrange
+ ExpressionType nodeType = ExpressionType.Index;
+ Type type = typeof(char);
+ PropertyInfo indexer = typeof(string).GetProperty("Chars");
+
+ // Act
+ IndexExpressionFingerprint fingerprint1 = new IndexExpressionFingerprint(nodeType, type, indexer);
+ IndexExpressionFingerprint fingerprint2 = new IndexExpressionFingerprint(nodeType, type, null /* indexer */);
+
+ // Assert
+ Assert.NotEqual(fingerprint1, fingerprint2);
+ }
+
+ [Fact]
+ public void Comparison_Inequality_Type()
+ {
+ // Arrange
+ ExpressionType nodeType = ExpressionType.Index;
+ Type type = typeof(char);
+ PropertyInfo indexer = typeof(string).GetProperty("Chars");
+
+ // Act
+ IndexExpressionFingerprint fingerprint1 = new IndexExpressionFingerprint(nodeType, type, indexer);
+ IndexExpressionFingerprint fingerprint2 = new IndexExpressionFingerprint(nodeType, typeof(object), indexer);
+
+ // Assert
+ Assert.NotEqual(fingerprint1, fingerprint2);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/ExpressionUtil/Test/LambdaExpressionFingerprintTest.cs b/test/System.Web.Mvc.Test/ExpressionUtil/Test/LambdaExpressionFingerprintTest.cs
new file mode 100644
index 00000000..3585da10
--- /dev/null
+++ b/test/System.Web.Mvc.Test/ExpressionUtil/Test/LambdaExpressionFingerprintTest.cs
@@ -0,0 +1,69 @@
+using System.Linq.Expressions;
+using Xunit;
+
+namespace System.Web.Mvc.ExpressionUtil.Test
+{
+ public class LambdaExpressionFingerprintTest
+ {
+ [Fact]
+ public void Properties()
+ {
+ // Arrange
+ ExpressionType expectedNodeType = ExpressionType.Lambda;
+ Type expectedType = typeof(Action<object>);
+
+ // Act
+ LambdaExpressionFingerprint fingerprint = new LambdaExpressionFingerprint(expectedNodeType, expectedType);
+
+ // Assert
+ Assert.Equal(expectedNodeType, fingerprint.NodeType);
+ Assert.Equal(expectedType, fingerprint.Type);
+ }
+
+ [Fact]
+ public void Comparison_Equality()
+ {
+ // Arrange
+ ExpressionType nodeType = ExpressionType.Lambda;
+ Type type = typeof(Action<object>);
+
+ // Act
+ LambdaExpressionFingerprint fingerprint1 = new LambdaExpressionFingerprint(nodeType, type);
+ LambdaExpressionFingerprint fingerprint2 = new LambdaExpressionFingerprint(nodeType, type);
+
+ // Assert
+ Assert.Equal(fingerprint1, fingerprint2);
+ Assert.Equal(fingerprint1.GetHashCode(), fingerprint2.GetHashCode());
+ }
+
+ [Fact]
+ public void Comparison_Inequality_FingerprintType()
+ {
+ // Arrange
+ ExpressionType nodeType = ExpressionType.Lambda;
+ Type type = typeof(Action<object>);
+
+ // Act
+ LambdaExpressionFingerprint fingerprint1 = new LambdaExpressionFingerprint(nodeType, type);
+ DummyExpressionFingerprint fingerprint2 = new DummyExpressionFingerprint(nodeType, type);
+
+ // Assert
+ Assert.NotEqual<ExpressionFingerprint>(fingerprint1, fingerprint2);
+ }
+
+ [Fact]
+ public void Comparison_Inequality_NodeType()
+ {
+ // Arrange
+ ExpressionType nodeType = ExpressionType.Lambda;
+ Type type = typeof(Action<object>);
+
+ // Act
+ LambdaExpressionFingerprint fingerprint1 = new LambdaExpressionFingerprint(nodeType, type);
+ LambdaExpressionFingerprint fingerprint2 = new LambdaExpressionFingerprint(nodeType, typeof(Action));
+
+ // Assert
+ Assert.NotEqual(fingerprint1, fingerprint2);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/ExpressionUtil/Test/MemberExpressionFingerprintTest.cs b/test/System.Web.Mvc.Test/ExpressionUtil/Test/MemberExpressionFingerprintTest.cs
new file mode 100644
index 00000000..bc7e0acc
--- /dev/null
+++ b/test/System.Web.Mvc.Test/ExpressionUtil/Test/MemberExpressionFingerprintTest.cs
@@ -0,0 +1,91 @@
+using System.Linq.Expressions;
+using System.Reflection;
+using Xunit;
+
+namespace System.Web.Mvc.ExpressionUtil.Test
+{
+ public class MemberExpressionFingerprintTest
+ {
+ [Fact]
+ public void Properties()
+ {
+ // Arrange
+ ExpressionType expectedNodeType = ExpressionType.MemberAccess;
+ Type expectedType = typeof(int);
+ MemberInfo expectedMember = typeof(TimeSpan).GetProperty("Seconds");
+
+ // Act
+ MemberExpressionFingerprint fingerprint = new MemberExpressionFingerprint(expectedNodeType, expectedType, expectedMember);
+
+ // Assert
+ Assert.Equal(expectedNodeType, fingerprint.NodeType);
+ Assert.Equal(expectedType, fingerprint.Type);
+ Assert.Equal(expectedMember, fingerprint.Member);
+ }
+
+ [Fact]
+ public void Comparison_Equality()
+ {
+ // Arrange
+ ExpressionType nodeType = ExpressionType.MemberAccess;
+ Type type = typeof(int);
+ MemberInfo member = typeof(TimeSpan).GetProperty("Seconds");
+
+ // Act
+ MemberExpressionFingerprint fingerprint1 = new MemberExpressionFingerprint(nodeType, type, member);
+ MemberExpressionFingerprint fingerprint2 = new MemberExpressionFingerprint(nodeType, type, member);
+
+ // Assert
+ Assert.Equal(fingerprint1, fingerprint2);
+ Assert.Equal(fingerprint1.GetHashCode(), fingerprint2.GetHashCode());
+ }
+
+ [Fact]
+ public void Comparison_Inequality_FingerprintType()
+ {
+ // Arrange
+ ExpressionType nodeType = ExpressionType.MemberAccess;
+ Type type = typeof(int);
+ MemberInfo member = typeof(TimeSpan).GetProperty("Seconds");
+
+ // Act
+ MemberExpressionFingerprint fingerprint1 = new MemberExpressionFingerprint(nodeType, type, member);
+ DummyExpressionFingerprint fingerprint2 = new DummyExpressionFingerprint(nodeType, type);
+
+ // Assert
+ Assert.NotEqual<ExpressionFingerprint>(fingerprint1, fingerprint2);
+ }
+
+ [Fact]
+ public void Comparison_Inequality_Member()
+ {
+ // Arrange
+ ExpressionType nodeType = ExpressionType.MemberAccess;
+ Type type = typeof(int);
+ MemberInfo member = typeof(TimeSpan).GetProperty("Seconds");
+
+ // Act
+ MemberExpressionFingerprint fingerprint1 = new MemberExpressionFingerprint(nodeType, type, member);
+ MemberExpressionFingerprint fingerprint2 = new MemberExpressionFingerprint(nodeType, type, null /* member */);
+
+ // Assert
+ Assert.NotEqual(fingerprint1, fingerprint2);
+ }
+
+ [Fact]
+ public void Comparison_Inequality_Type()
+ {
+ // Arrange
+ ExpressionType nodeType = ExpressionType.MemberAccess;
+ Type type = typeof(int);
+ MemberInfo member = typeof(TimeSpan).GetProperty("Seconds");
+
+ // Act
+ MemberExpressionFingerprint fingerprint1 = new MemberExpressionFingerprint(nodeType, type, member);
+ MemberExpressionFingerprint fingerprint2 = new MemberExpressionFingerprint(nodeType, typeof(object), member);
+
+ // Assert
+ Assert.NotEqual(fingerprint1, fingerprint2);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/ExpressionUtil/Test/MethodCallExpressionFingerprintTest.cs b/test/System.Web.Mvc.Test/ExpressionUtil/Test/MethodCallExpressionFingerprintTest.cs
new file mode 100644
index 00000000..a6243377
--- /dev/null
+++ b/test/System.Web.Mvc.Test/ExpressionUtil/Test/MethodCallExpressionFingerprintTest.cs
@@ -0,0 +1,91 @@
+using System.Linq.Expressions;
+using System.Reflection;
+using Xunit;
+
+namespace System.Web.Mvc.ExpressionUtil.Test
+{
+ public class MethodCallExpressionFingerprintTest
+ {
+ [Fact]
+ public void Properties()
+ {
+ // Arrange
+ ExpressionType expectedNodeType = ExpressionType.Call;
+ Type expectedType = typeof(string);
+ MethodInfo expectedMethod = typeof(string).GetMethod("Intern");
+
+ // Act
+ MethodCallExpressionFingerprint fingerprint = new MethodCallExpressionFingerprint(expectedNodeType, expectedType, expectedMethod);
+
+ // Assert
+ Assert.Equal(expectedNodeType, fingerprint.NodeType);
+ Assert.Equal(expectedType, fingerprint.Type);
+ Assert.Equal(expectedMethod, fingerprint.Method);
+ }
+
+ [Fact]
+ public void Comparison_Equality()
+ {
+ // Arrange
+ ExpressionType nodeType = ExpressionType.Call;
+ Type type = typeof(string);
+ MethodInfo method = typeof(string).GetMethod("Intern");
+
+ // Act
+ MethodCallExpressionFingerprint fingerprint1 = new MethodCallExpressionFingerprint(nodeType, type, method);
+ MethodCallExpressionFingerprint fingerprint2 = new MethodCallExpressionFingerprint(nodeType, type, method);
+
+ // Assert
+ Assert.Equal(fingerprint1, fingerprint2);
+ Assert.Equal(fingerprint1.GetHashCode(), fingerprint2.GetHashCode());
+ }
+
+ [Fact]
+ public void Comparison_Inequality_FingerprintType()
+ {
+ // Arrange
+ ExpressionType nodeType = ExpressionType.Call;
+ Type type = typeof(string);
+ MethodInfo method = typeof(string).GetMethod("Intern");
+
+ // Act
+ MethodCallExpressionFingerprint fingerprint1 = new MethodCallExpressionFingerprint(nodeType, type, method);
+ DummyExpressionFingerprint fingerprint2 = new DummyExpressionFingerprint(nodeType, type);
+
+ // Assert
+ Assert.NotEqual<ExpressionFingerprint>(fingerprint1, fingerprint2);
+ }
+
+ [Fact]
+ public void Comparison_Inequality_Method()
+ {
+ // Arrange
+ ExpressionType nodeType = ExpressionType.Call;
+ Type type = typeof(string);
+ MethodInfo method = typeof(string).GetMethod("Intern");
+
+ // Act
+ MethodCallExpressionFingerprint fingerprint1 = new MethodCallExpressionFingerprint(nodeType, type, method);
+ MethodCallExpressionFingerprint fingerprint2 = new MethodCallExpressionFingerprint(nodeType, type, null /* method */);
+
+ // Assert
+ Assert.NotEqual(fingerprint1, fingerprint2);
+ }
+
+ [Fact]
+ public void Comparison_Inequality_Type()
+ {
+ // Arrange
+ ExpressionType nodeType = ExpressionType.Call;
+ Type type = typeof(string);
+ MethodInfo method = typeof(string).GetMethod("Intern");
+
+ // Act
+ MethodCallExpressionFingerprint fingerprint1 = new MethodCallExpressionFingerprint(nodeType, type, method);
+ MethodCallExpressionFingerprint fingerprint2 = new MethodCallExpressionFingerprint(nodeType, typeof(object), method);
+
+ // Assert
+ Assert.NotEqual(fingerprint1, fingerprint2);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/ExpressionUtil/Test/ParameterExpressionFingerprintTest.cs b/test/System.Web.Mvc.Test/ExpressionUtil/Test/ParameterExpressionFingerprintTest.cs
new file mode 100644
index 00000000..f3acb224
--- /dev/null
+++ b/test/System.Web.Mvc.Test/ExpressionUtil/Test/ParameterExpressionFingerprintTest.cs
@@ -0,0 +1,90 @@
+using System.Linq.Expressions;
+using Xunit;
+
+namespace System.Web.Mvc.ExpressionUtil.Test
+{
+ public class ParameterExpressionFingerprintTest
+ {
+ [Fact]
+ public void Properties()
+ {
+ // Arrange
+ ExpressionType expectedNodeType = ExpressionType.Parameter;
+ Type expectedType = typeof(object);
+ int expectedParameterIndex = 1;
+
+ // Act
+ ParameterExpressionFingerprint fingerprint = new ParameterExpressionFingerprint(expectedNodeType, expectedType, expectedParameterIndex);
+
+ // Assert
+ Assert.Equal(expectedNodeType, fingerprint.NodeType);
+ Assert.Equal(expectedType, fingerprint.Type);
+ Assert.Equal(expectedParameterIndex, fingerprint.ParameterIndex);
+ }
+
+ [Fact]
+ public void Comparison_Equality()
+ {
+ // Arrange
+ ExpressionType nodeType = ExpressionType.Parameter;
+ Type type = typeof(object);
+ int parameterIndex = 1;
+
+ // Act
+ ParameterExpressionFingerprint fingerprint1 = new ParameterExpressionFingerprint(nodeType, type, parameterIndex);
+ ParameterExpressionFingerprint fingerprint2 = new ParameterExpressionFingerprint(nodeType, type, parameterIndex);
+
+ // Assert
+ Assert.Equal(fingerprint1, fingerprint2);
+ Assert.Equal(fingerprint1.GetHashCode(), fingerprint2.GetHashCode());
+ }
+
+ [Fact]
+ public void Comparison_Inequality_FingerprintType()
+ {
+ // Arrange
+ ExpressionType nodeType = ExpressionType.Parameter;
+ Type type = typeof(object);
+ int parameterIndex = 1;
+
+ // Act
+ ParameterExpressionFingerprint fingerprint1 = new ParameterExpressionFingerprint(nodeType, type, parameterIndex);
+ DummyExpressionFingerprint fingerprint2 = new DummyExpressionFingerprint(nodeType, type);
+
+ // Assert
+ Assert.NotEqual<ExpressionFingerprint>(fingerprint1, fingerprint2);
+ }
+
+ [Fact]
+ public void Comparison_Inequality_Method()
+ {
+ // Arrange
+ ExpressionType nodeType = ExpressionType.Parameter;
+ Type type = typeof(object);
+ int parameterIndex = 1;
+
+ // Act
+ ParameterExpressionFingerprint fingerprint1 = new ParameterExpressionFingerprint(nodeType, type, parameterIndex);
+ ParameterExpressionFingerprint fingerprint2 = new ParameterExpressionFingerprint(nodeType, type, -1 /* parameterIndex */);
+
+ // Assert
+ Assert.NotEqual(fingerprint1, fingerprint2);
+ }
+
+ [Fact]
+ public void Comparison_Inequality_Type()
+ {
+ // Arrange
+ ExpressionType nodeType = ExpressionType.Parameter;
+ Type type = typeof(object);
+ int parameterIndex = 1;
+
+ // Act
+ ParameterExpressionFingerprint fingerprint1 = new ParameterExpressionFingerprint(nodeType, type, parameterIndex);
+ ParameterExpressionFingerprint fingerprint2 = new ParameterExpressionFingerprint(nodeType, typeof(string), parameterIndex);
+
+ // Assert
+ Assert.NotEqual(fingerprint1, fingerprint2);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/ExpressionUtil/Test/TypeBinaryExpressionFingerprintTest.cs b/test/System.Web.Mvc.Test/ExpressionUtil/Test/TypeBinaryExpressionFingerprintTest.cs
new file mode 100644
index 00000000..024d3fa2
--- /dev/null
+++ b/test/System.Web.Mvc.Test/ExpressionUtil/Test/TypeBinaryExpressionFingerprintTest.cs
@@ -0,0 +1,90 @@
+using System.Linq.Expressions;
+using Xunit;
+
+namespace System.Web.Mvc.ExpressionUtil.Test
+{
+ public class TypeBinaryExpressionFingerprintTest
+ {
+ [Fact]
+ public void Properties()
+ {
+ // Arrange
+ ExpressionType expectedNodeType = ExpressionType.TypeIs;
+ Type expectedType = typeof(bool);
+ Type expectedTypeOperand = typeof(object);
+
+ // Act
+ TypeBinaryExpressionFingerprint fingerprint = new TypeBinaryExpressionFingerprint(expectedNodeType, expectedType, expectedTypeOperand);
+
+ // Assert
+ Assert.Equal(expectedNodeType, fingerprint.NodeType);
+ Assert.Equal(expectedType, fingerprint.Type);
+ Assert.Equal(expectedTypeOperand, fingerprint.TypeOperand);
+ }
+
+ [Fact]
+ public void Comparison_Equality()
+ {
+ // Arrange
+ ExpressionType nodeType = ExpressionType.TypeIs;
+ Type type = typeof(bool);
+ Type typeOperand = typeof(object);
+
+ // Act
+ TypeBinaryExpressionFingerprint fingerprint1 = new TypeBinaryExpressionFingerprint(nodeType, type, typeOperand);
+ TypeBinaryExpressionFingerprint fingerprint2 = new TypeBinaryExpressionFingerprint(nodeType, type, typeOperand);
+
+ // Assert
+ Assert.Equal(fingerprint1, fingerprint2);
+ Assert.Equal(fingerprint1.GetHashCode(), fingerprint2.GetHashCode());
+ }
+
+ [Fact]
+ public void Comparison_Inequality_FingerprintType()
+ {
+ // Arrange
+ ExpressionType nodeType = ExpressionType.TypeIs;
+ Type type = typeof(bool);
+ Type typeOperand = typeof(object);
+
+ // Act
+ TypeBinaryExpressionFingerprint fingerprint1 = new TypeBinaryExpressionFingerprint(nodeType, type, typeOperand);
+ DummyExpressionFingerprint fingerprint2 = new DummyExpressionFingerprint(nodeType, type);
+
+ // Assert
+ Assert.NotEqual<ExpressionFingerprint>(fingerprint1, fingerprint2);
+ }
+
+ [Fact]
+ public void Comparison_Inequality_TypeOperand()
+ {
+ // Arrange
+ ExpressionType nodeType = ExpressionType.TypeIs;
+ Type type = typeof(bool);
+ Type typeOperand = typeof(object);
+
+ // Act
+ TypeBinaryExpressionFingerprint fingerprint1 = new TypeBinaryExpressionFingerprint(nodeType, type, typeOperand);
+ TypeBinaryExpressionFingerprint fingerprint2 = new TypeBinaryExpressionFingerprint(nodeType, type, typeof(string) /* typeOperand */);
+
+ // Assert
+ Assert.NotEqual(fingerprint1, fingerprint2);
+ }
+
+ [Fact]
+ public void Comparison_Inequality_Type()
+ {
+ // Arrange
+ ExpressionType nodeType = ExpressionType.TypeIs;
+ Type type = typeof(bool);
+ Type typeOperand = typeof(object);
+
+ // Act
+ TypeBinaryExpressionFingerprint fingerprint1 = new TypeBinaryExpressionFingerprint(nodeType, type, typeOperand);
+ TypeBinaryExpressionFingerprint fingerprint2 = new TypeBinaryExpressionFingerprint(nodeType, typeof(object), typeOperand);
+
+ // Assert
+ Assert.NotEqual(fingerprint1, fingerprint2);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/ExpressionUtil/Test/UnaryExpressionFingerprintTest.cs b/test/System.Web.Mvc.Test/ExpressionUtil/Test/UnaryExpressionFingerprintTest.cs
new file mode 100644
index 00000000..de6dc578
--- /dev/null
+++ b/test/System.Web.Mvc.Test/ExpressionUtil/Test/UnaryExpressionFingerprintTest.cs
@@ -0,0 +1,91 @@
+using System.Linq.Expressions;
+using System.Reflection;
+using Xunit;
+
+namespace System.Web.Mvc.ExpressionUtil.Test
+{
+ public class UnaryExpressionFingerprintTest
+ {
+ [Fact]
+ public void Properties()
+ {
+ // Arrange
+ ExpressionType expectedNodeType = ExpressionType.Not;
+ Type expectedType = typeof(int);
+ MethodInfo expectedMethod = typeof(object).GetMethod("GetHashCode");
+
+ // Act
+ UnaryExpressionFingerprint fingerprint = new UnaryExpressionFingerprint(expectedNodeType, expectedType, expectedMethod);
+
+ // Assert
+ Assert.Equal(expectedNodeType, fingerprint.NodeType);
+ Assert.Equal(expectedType, fingerprint.Type);
+ Assert.Equal(expectedMethod, fingerprint.Method);
+ }
+
+ [Fact]
+ public void Comparison_Equality()
+ {
+ // Arrange
+ ExpressionType nodeType = ExpressionType.Not;
+ Type type = typeof(int);
+ MethodInfo method = typeof(object).GetMethod("GetHashCode");
+
+ // Act
+ UnaryExpressionFingerprint fingerprint1 = new UnaryExpressionFingerprint(nodeType, type, method);
+ UnaryExpressionFingerprint fingerprint2 = new UnaryExpressionFingerprint(nodeType, type, method);
+
+ // Assert
+ Assert.Equal(fingerprint1, fingerprint2);
+ Assert.Equal(fingerprint1.GetHashCode(), fingerprint2.GetHashCode());
+ }
+
+ [Fact]
+ public void Comparison_Inequality_FingerprintType()
+ {
+ // Arrange
+ ExpressionType nodeType = ExpressionType.Not;
+ Type type = typeof(int);
+ MethodInfo method = typeof(object).GetMethod("GetHashCode");
+
+ // Act
+ UnaryExpressionFingerprint fingerprint1 = new UnaryExpressionFingerprint(nodeType, type, method);
+ DummyExpressionFingerprint fingerprint2 = new DummyExpressionFingerprint(nodeType, type);
+
+ // Assert
+ Assert.NotEqual<ExpressionFingerprint>(fingerprint1, fingerprint2);
+ }
+
+ [Fact]
+ public void Comparison_Inequality_Method()
+ {
+ // Arrange
+ ExpressionType nodeType = ExpressionType.Not;
+ Type type = typeof(int);
+ MethodInfo method = typeof(object).GetMethod("GetHashCode");
+
+ // Act
+ UnaryExpressionFingerprint fingerprint1 = new UnaryExpressionFingerprint(nodeType, type, method);
+ UnaryExpressionFingerprint fingerprint2 = new UnaryExpressionFingerprint(nodeType, type, null /* method */);
+
+ // Assert
+ Assert.NotEqual(fingerprint1, fingerprint2);
+ }
+
+ [Fact]
+ public void Comparison_Inequality_Type()
+ {
+ // Arrange
+ ExpressionType nodeType = ExpressionType.Not;
+ Type type = typeof(int);
+ MethodInfo method = typeof(object).GetMethod("GetHashCode");
+
+ // Act
+ UnaryExpressionFingerprint fingerprint1 = new UnaryExpressionFingerprint(nodeType, type, method);
+ UnaryExpressionFingerprint fingerprint2 = new UnaryExpressionFingerprint(nodeType, typeof(object), method);
+
+ // Assert
+ Assert.NotEqual(fingerprint1, fingerprint2);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Html/Test/ChildActionExtensionsTest.cs b/test/System.Web.Mvc.Test/Html/Test/ChildActionExtensionsTest.cs
new file mode 100644
index 00000000..c800418e
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Html/Test/ChildActionExtensionsTest.cs
@@ -0,0 +1,256 @@
+using System.IO;
+using System.Web.Mvc.Properties;
+using System.Web.Routing;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Html.Test
+{
+ public class ChildActionExtensionsTest
+ {
+ Mock<HtmlHelper> htmlHelper;
+ Mock<HttpContextBase> httpContext;
+ Mock<RouteBase> route;
+ Mock<IViewDataContainer> viewDataContainer;
+
+ RouteData originalRouteData;
+ RouteCollection routes;
+ ViewContext viewContext;
+ VirtualPathData virtualPathData;
+
+ public ChildActionExtensionsTest()
+ {
+ route = new Mock<RouteBase>();
+ route.Setup(r => r.GetVirtualPath(It.IsAny<RequestContext>(), It.IsAny<RouteValueDictionary>()))
+ .Returns(() => virtualPathData);
+
+ virtualPathData = new VirtualPathData(route.Object, "~/VirtualPath");
+
+ routes = new RouteCollection();
+ routes.Add(route.Object);
+
+ originalRouteData = new RouteData();
+
+ string returnValue = "";
+ httpContext = new Mock<HttpContextBase>();
+ httpContext.Setup(hc => hc.Request.ApplicationPath).Returns("~");
+ httpContext.Setup(hc => hc.Response.ApplyAppPathModifier(It.IsAny<string>()))
+ .Callback<string>(s => returnValue = s)
+ .Returns(() => returnValue);
+ httpContext.Setup(hc => hc.Server.Execute(It.IsAny<IHttpHandler>(), It.IsAny<TextWriter>(), It.IsAny<bool>()));
+
+ viewContext = new ViewContext
+ {
+ RequestContext = new RequestContext(httpContext.Object, originalRouteData)
+ };
+
+ viewDataContainer = new Mock<IViewDataContainer>();
+
+ htmlHelper = new Mock<HtmlHelper>(viewContext, viewDataContainer.Object, routes);
+ }
+
+ [Fact]
+ public void GuardClauses()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => ChildActionExtensions.ActionHelper(null /* htmlHelper */, "abc", null /* controllerName */, null /* routeValues */, null /* textWriter */),
+ "htmlHelper"
+ );
+
+ Assert.ThrowsArgumentNullOrEmpty(
+ () => ChildActionExtensions.ActionHelper(htmlHelper.Object, null /* actionName */, null /* controllerName */, null /* routeValues */, null /* textWriter */),
+ "actionName"
+ );
+
+ Assert.ThrowsArgumentNullOrEmpty(
+ () => ChildActionExtensions.ActionHelper(htmlHelper.Object, String.Empty /* actionName */, null /* controllerName */, null /* routeValues */, null /* textWriter */),
+ "actionName"
+ );
+ }
+
+ [Fact]
+ public void ServerExecuteCalledWithWrappedChildActionMvcHandler()
+ {
+ // Arrange
+ IHttpHandler callbackHandler = null;
+ TextWriter callbackTextWriter = null;
+ bool callbackPreserveForm = false;
+ httpContext.Setup(hc => hc.Server.Execute(It.IsAny<IHttpHandler>(), It.IsAny<TextWriter>(), It.IsAny<bool>()))
+ .Callback<IHttpHandler, TextWriter, bool>(
+ (handler, textWriter, preserveForm) =>
+ {
+ callbackHandler = handler;
+ callbackTextWriter = textWriter;
+ callbackPreserveForm = preserveForm;
+ });
+ TextWriter stringWriter = new StringWriter();
+
+ // Act
+ ChildActionExtensions.ActionHelper(htmlHelper.Object, "actionName", null /* controllerName */, null /* routeValues */, stringWriter);
+
+ // Assert
+ Assert.NotNull(callbackHandler);
+ HttpHandlerUtil.ServerExecuteHttpHandlerWrapper wrapper = callbackHandler as HttpHandlerUtil.ServerExecuteHttpHandlerWrapper;
+ Assert.NotNull(wrapper);
+ Assert.NotNull(wrapper.InnerHandler);
+ ChildActionExtensions.ChildActionMvcHandler childHandler = wrapper.InnerHandler as ChildActionExtensions.ChildActionMvcHandler;
+ Assert.NotNull(childHandler);
+ Assert.Same(stringWriter, callbackTextWriter);
+ Assert.True(callbackPreserveForm);
+ }
+
+ [Fact]
+ public void RouteDataTokensIncludesParentActionViewContext()
+ {
+ // Arrange
+ MvcHandler mvcHandler = null;
+ httpContext.Setup(hc => hc.Server.Execute(It.IsAny<IHttpHandler>(), It.IsAny<TextWriter>(), It.IsAny<bool>()))
+ .Callback<IHttpHandler, TextWriter, bool>((handler, _, __) => mvcHandler = (MvcHandler)((HttpHandlerUtil.ServerExecuteHttpHandlerWrapper)handler).InnerHandler);
+
+ // Act
+ ChildActionExtensions.ActionHelper(htmlHelper.Object, "actionName", null /* controllerName */, null /* routeValues */, null /* textWriter */);
+
+ // Assert
+ Assert.Same(viewContext, mvcHandler.RequestContext.RouteData.DataTokens[ControllerContext.ParentActionViewContextToken]);
+ }
+
+ [Fact]
+ public void RouteValuesIncludeNewActionName()
+ {
+ // Arrange
+ MvcHandler mvcHandler = null;
+ httpContext.Setup(hc => hc.Server.Execute(It.IsAny<IHttpHandler>(), It.IsAny<TextWriter>(), It.IsAny<bool>()))
+ .Callback<IHttpHandler, TextWriter, bool>((handler, _, __) => mvcHandler = (MvcHandler)((HttpHandlerUtil.ServerExecuteHttpHandlerWrapper)handler).InnerHandler);
+
+ // Act
+ ChildActionExtensions.ActionHelper(htmlHelper.Object, "actionName", null /* controllerName */, null /* routeValues */, null /* textWriter */);
+
+ // Assert
+ RouteData routeData = mvcHandler.RequestContext.RouteData;
+ Assert.Equal("actionName", routeData.Values["action"]);
+ }
+
+ [Fact]
+ public void RouteValuesIncludeOldControllerNameWhenControllerNameIsNullOrEmpty()
+ {
+ // Arrange
+ originalRouteData.Values["controller"] = "oldController";
+ MvcHandler mvcHandler = null;
+ httpContext.Setup(hc => hc.Server.Execute(It.IsAny<IHttpHandler>(), It.IsAny<TextWriter>(), It.IsAny<bool>()))
+ .Callback<IHttpHandler, TextWriter, bool>((handler, _, __) => mvcHandler = (MvcHandler)((HttpHandlerUtil.ServerExecuteHttpHandlerWrapper)handler).InnerHandler);
+
+ // Act
+ ChildActionExtensions.ActionHelper(htmlHelper.Object, "actionName", null /* controllerName */, null /* routeValues */, null /* textWriter */);
+
+ // Assert
+ RouteData routeData = mvcHandler.RequestContext.RouteData;
+ Assert.Equal("oldController", routeData.Values["controller"]);
+ }
+
+ [Fact]
+ public void RouteValuesIncludeNewControllerNameWhenControllNameIsNotEmpty()
+ {
+ // Arrange
+ originalRouteData.Values["controller"] = "oldController";
+ MvcHandler mvcHandler = null;
+ httpContext.Setup(hc => hc.Server.Execute(It.IsAny<IHttpHandler>(), It.IsAny<TextWriter>(), It.IsAny<bool>()))
+ .Callback<IHttpHandler, TextWriter, bool>((handler, _, __) => mvcHandler = (MvcHandler)((HttpHandlerUtil.ServerExecuteHttpHandlerWrapper)handler).InnerHandler);
+
+ // Act
+ ChildActionExtensions.ActionHelper(htmlHelper.Object, "actionName", "newController", null /* routeValues */, null /* textWriter */);
+
+ // Assert
+ RouteData routeData = mvcHandler.RequestContext.RouteData;
+ Assert.Equal("newController", routeData.Values["controller"]);
+ }
+
+ [Fact]
+ public void PassedRouteValuesOverrideParentRequestRouteValues()
+ {
+ // Arrange
+ originalRouteData.Values["name1"] = "value1";
+ originalRouteData.Values["name2"] = "value2";
+ MvcHandler mvcHandler = null;
+ httpContext.Setup(hc => hc.Server.Execute(It.IsAny<IHttpHandler>(), It.IsAny<TextWriter>(), It.IsAny<bool>()))
+ .Callback<IHttpHandler, TextWriter, bool>((handler, _, __) => mvcHandler = (MvcHandler)((HttpHandlerUtil.ServerExecuteHttpHandlerWrapper)handler).InnerHandler);
+
+ // Act
+ ChildActionExtensions.ActionHelper(htmlHelper.Object, "actionName", null /* controllerName */, new RouteValueDictionary { { "name2", "newValue2" } }, null /* textWriter */);
+
+ // Assert
+ RouteData routeData = mvcHandler.RequestContext.RouteData;
+ Assert.Equal("value1", routeData.Values["name1"]);
+ Assert.Equal("newValue2", routeData.Values["name2"]);
+
+ Assert.Equal("newValue2", (routeData.Values[ChildActionValueProvider.ChildActionValuesKey] as DictionaryValueProvider<object>).GetValue("name2").RawValue);
+ }
+
+ [Fact]
+ public void NoChildActionValuesDictionaryCreatedIfNoRouteValuesPassed()
+ {
+ // Arrange
+ MvcHandler mvcHandler = null;
+ httpContext.Setup(hc => hc.Server.Execute(It.IsAny<IHttpHandler>(), It.IsAny<TextWriter>(), It.IsAny<bool>()))
+ .Callback<IHttpHandler, TextWriter, bool>((handler, _, __) => mvcHandler = (MvcHandler)((HttpHandlerUtil.ServerExecuteHttpHandlerWrapper)handler).InnerHandler);
+
+ // Act
+ ChildActionExtensions.ActionHelper(htmlHelper.Object, "actionName", null /* controllerName */, null, null /* textWriter */);
+
+ // Assert
+ RouteData routeData = mvcHandler.RequestContext.RouteData;
+ Assert.Null(routeData.Values[ChildActionValueProvider.ChildActionValuesKey]);
+ }
+
+ [Fact]
+ public void RouteValuesDoesNotIncludeExplicitlyPassedAreaName()
+ {
+ // Arrange
+ Route route = routes.MapRoute("my-area", "my-area");
+ route.DataTokens["area"] = "myArea";
+ MvcHandler mvcHandler = null;
+ httpContext.Setup(hc => hc.Server.Execute(It.IsAny<IHttpHandler>(), It.IsAny<TextWriter>(), It.IsAny<bool>()))
+ .Callback<IHttpHandler, TextWriter, bool>((handler, _, __) => mvcHandler = (MvcHandler)((HttpHandlerUtil.ServerExecuteHttpHandlerWrapper)handler).InnerHandler);
+
+ // Act
+ ChildActionExtensions.ActionHelper(htmlHelper.Object, "actionName", null /* controllerName */, new RouteValueDictionary { { "area", "myArea" } }, null /* textWriter */);
+
+ // Assert
+ RouteData routeData = mvcHandler.RequestContext.RouteData;
+ Assert.False(routeData.Values.ContainsKey("area"));
+ Assert.Null((routeData.Values[ChildActionValueProvider.ChildActionValuesKey] as DictionaryValueProvider<object>).GetValue("area"));
+ }
+
+ [Fact]
+ public void RouteValuesIncludeExplicitlyPassedAreaNameIfAreasNotInUse()
+ {
+ // Arrange
+ Route route = routes.MapRoute("my-area", "my-area");
+ MvcHandler mvcHandler = null;
+ httpContext.Setup(hc => hc.Server.Execute(It.IsAny<IHttpHandler>(), It.IsAny<TextWriter>(), It.IsAny<bool>()))
+ .Callback<IHttpHandler, TextWriter, bool>((handler, _, __) => mvcHandler = (MvcHandler)((HttpHandlerUtil.ServerExecuteHttpHandlerWrapper)handler).InnerHandler);
+
+ // Act
+ ChildActionExtensions.ActionHelper(htmlHelper.Object, "actionName", null /* controllerName */, new RouteValueDictionary { { "area", "myArea" } }, null /* textWriter */);
+
+ // Assert
+ RouteData routeData = mvcHandler.RequestContext.RouteData;
+ Assert.True(routeData.Values.ContainsKey("area"));
+ Assert.Equal("myArea", (routeData.Values[ChildActionValueProvider.ChildActionValuesKey] as DictionaryValueProvider<object>).GetValue("area").RawValue);
+ }
+
+ [Fact]
+ public void NoMatchingRouteThrows()
+ {
+ // Arrange
+ routes.Clear();
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ () => ChildActionExtensions.ActionHelper(htmlHelper.Object, "actionName", null /* controllerName */, null /* routeValues */, null /* textWriter */),
+ MvcResources.Common_NoRouteMatched
+ );
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Html/Test/DefaultDisplayTemplatesTest.cs b/test/System.Web.Mvc.Test/Html/Test/DefaultDisplayTemplatesTest.cs
new file mode 100644
index 00000000..3d091c0e
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Html/Test/DefaultDisplayTemplatesTest.cs
@@ -0,0 +1,560 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Data.Objects.DataClasses;
+using System.IO;
+using System.Web.UI.WebControls;
+using Microsoft.Web.UnitTestUtil;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Html.Test
+{
+ public class DefaultDisplayTemplatesTest
+ {
+ // BooleanTemplate
+
+ [Fact]
+ public void BooleanTemplateTests()
+ {
+ // Boolean values
+
+ Assert.Equal(
+ @"<input checked=""checked"" class=""check-box"" disabled=""disabled"" type=""checkbox"" />",
+ DefaultDisplayTemplates.BooleanTemplate(MakeHtmlHelper<bool>(true)));
+
+ Assert.Equal(
+ @"<input class=""check-box"" disabled=""disabled"" type=""checkbox"" />",
+ DefaultDisplayTemplates.BooleanTemplate(MakeHtmlHelper<bool>(false)));
+
+ Assert.Equal(
+ @"<input class=""check-box"" disabled=""disabled"" type=""checkbox"" />",
+ DefaultDisplayTemplates.BooleanTemplate(MakeHtmlHelper<bool>(null)));
+
+ // Nullable<Boolean> values
+
+ Assert.Equal(
+ @"<select class=""tri-state list-box"" disabled=""disabled""><option value="""">Not Set</option><option selected=""selected"" value=""true"">True</option><option value=""false"">False</option></select>",
+ DefaultDisplayTemplates.BooleanTemplate(MakeHtmlHelper<Nullable<bool>>(true)));
+
+ Assert.Equal(
+ @"<select class=""tri-state list-box"" disabled=""disabled""><option value="""">Not Set</option><option value=""true"">True</option><option selected=""selected"" value=""false"">False</option></select>",
+ DefaultDisplayTemplates.BooleanTemplate(MakeHtmlHelper<Nullable<bool>>(false)));
+
+ Assert.Equal(
+ @"<select class=""tri-state list-box"" disabled=""disabled""><option selected=""selected"" value="""">Not Set</option><option value=""true"">True</option><option value=""false"">False</option></select>",
+ DefaultDisplayTemplates.BooleanTemplate(MakeHtmlHelper<Nullable<bool>>(null)));
+ }
+
+ // CollectionTemplate
+
+ private static string CollectionSpyCallback(HtmlHelper html, ModelMetadata metadata, string htmlFieldName, string templateName, DataBoundControlMode mode, object additionalViewData)
+ {
+ return String.Format(Environment.NewLine + "Model = {0}, ModelType = {1}, PropertyName = {2}, HtmlFieldName = {3}, TemplateName = {4}, Mode = {5}, TemplateInfo.HtmlFieldPrefix = {6}, AdditionalViewData = {7}",
+ metadata.Model ?? "(null)",
+ metadata.ModelType == null ? "(null)" : metadata.ModelType.FullName,
+ metadata.PropertyName ?? "(null)",
+ htmlFieldName == String.Empty ? "(empty)" : htmlFieldName ?? "(null)",
+ templateName ?? "(null)",
+ mode,
+ html.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix,
+ AnonymousObject.Inspect(additionalViewData));
+ }
+
+ [Fact]
+ public void CollectionTemplateWithNullModel()
+ {
+ // Arrange
+ HtmlHelper html = MakeHtmlHelper<object>(null);
+
+ // Act
+ string result = DefaultDisplayTemplates.CollectionTemplate(html, CollectionSpyCallback);
+
+ // Assert
+ Assert.Equal(String.Empty, result);
+ }
+
+ [Fact]
+ public void CollectionTemplateNonEnumerableModelThrows()
+ {
+ // Arrange
+ HtmlHelper html = MakeHtmlHelper<object>(new object());
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ () => DefaultDisplayTemplates.CollectionTemplate(html, CollectionSpyCallback),
+ "The Collection template was used with an object of type 'System.Object', which does not implement System.IEnumerable."
+ );
+ }
+
+ [Fact]
+ public void CollectionTemplateWithSingleItemCollectionWithoutPrefix()
+ {
+ // Arrange
+ HtmlHelper html = MakeHtmlHelper<List<string>>(new List<string> { "foo" });
+ html.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = null;
+
+ // Act
+ string result = DefaultDisplayTemplates.CollectionTemplate(html, CollectionSpyCallback);
+
+ // Assert
+ Assert.Equal(@"
+Model = foo, ModelType = System.String, PropertyName = (null), HtmlFieldName = [0], TemplateName = (null), Mode = ReadOnly, TemplateInfo.HtmlFieldPrefix = , AdditionalViewData = (null)",
+ result);
+ }
+
+ [Fact]
+ public void CollectionTemplateWithSingleItemCollectionWithPrefix()
+ {
+ // Arrange
+ HtmlHelper html = MakeHtmlHelper<List<string>>(new List<string> { "foo" });
+ html.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "ModelProperty";
+
+ // Act
+ string result = DefaultDisplayTemplates.CollectionTemplate(html, CollectionSpyCallback);
+
+ // Assert
+ Assert.Equal(@"
+Model = foo, ModelType = System.String, PropertyName = (null), HtmlFieldName = ModelProperty[0], TemplateName = (null), Mode = ReadOnly, TemplateInfo.HtmlFieldPrefix = , AdditionalViewData = (null)",
+ result);
+ }
+
+ [Fact]
+ public void CollectionTemplateWithMultiItemCollection()
+ {
+ // Arrange
+ HtmlHelper html = MakeHtmlHelper<List<string>>(new List<string> { "foo", "bar", "baz" });
+ html.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = null;
+
+ // Act
+ string result = DefaultDisplayTemplates.CollectionTemplate(html, CollectionSpyCallback);
+
+ // Assert
+ Assert.Equal(@"
+Model = foo, ModelType = System.String, PropertyName = (null), HtmlFieldName = [0], TemplateName = (null), Mode = ReadOnly, TemplateInfo.HtmlFieldPrefix = , AdditionalViewData = (null)
+Model = bar, ModelType = System.String, PropertyName = (null), HtmlFieldName = [1], TemplateName = (null), Mode = ReadOnly, TemplateInfo.HtmlFieldPrefix = , AdditionalViewData = (null)
+Model = baz, ModelType = System.String, PropertyName = (null), HtmlFieldName = [2], TemplateName = (null), Mode = ReadOnly, TemplateInfo.HtmlFieldPrefix = , AdditionalViewData = (null)",
+ result);
+ }
+
+ [Fact]
+ public void CollectionTemplateNullITemInWeaklyTypedCollectionUsesModelTypeOfString()
+ {
+ // Arrange
+ HtmlHelper html = MakeHtmlHelper<ArrayList>(new ArrayList { null });
+ html.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = null;
+
+ // Act
+ string result = DefaultDisplayTemplates.CollectionTemplate(html, CollectionSpyCallback);
+
+ // Assert
+ Assert.Equal(@"
+Model = (null), ModelType = System.String, PropertyName = (null), HtmlFieldName = [0], TemplateName = (null), Mode = ReadOnly, TemplateInfo.HtmlFieldPrefix = , AdditionalViewData = (null)",
+ result);
+ }
+
+ [Fact]
+ public void CollectionTemplateNullItemInStronglyTypedCollectionUsesModelTypeFromIEnumerable()
+ {
+ // Arrange
+ HtmlHelper html = MakeHtmlHelper<List<IHttpHandler>>(new List<IHttpHandler> { null });
+ html.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = null;
+
+ // Act
+ string result = DefaultDisplayTemplates.CollectionTemplate(html, CollectionSpyCallback);
+
+ // Assert
+ Assert.Equal(@"
+Model = (null), ModelType = System.Web.IHttpHandler, PropertyName = (null), HtmlFieldName = [0], TemplateName = (null), Mode = ReadOnly, TemplateInfo.HtmlFieldPrefix = , AdditionalViewData = (null)",
+ result);
+ }
+
+ [Fact]
+ public void CollectionTemplateUsesRealObjectTypes()
+ {
+ // Arrange
+ HtmlHelper html = MakeHtmlHelper<List<Object>>(new List<Object> { 1, 2.3, "Hello World" });
+ html.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = null;
+
+ // Act
+ string result = DefaultDisplayTemplates.CollectionTemplate(html, CollectionSpyCallback);
+
+ // Assert
+ Assert.Equal(@"
+Model = 1, ModelType = System.Int32, PropertyName = (null), HtmlFieldName = [0], TemplateName = (null), Mode = ReadOnly, TemplateInfo.HtmlFieldPrefix = , AdditionalViewData = (null)
+Model = 2.3, ModelType = System.Double, PropertyName = (null), HtmlFieldName = [1], TemplateName = (null), Mode = ReadOnly, TemplateInfo.HtmlFieldPrefix = , AdditionalViewData = (null)
+Model = Hello World, ModelType = System.String, PropertyName = (null), HtmlFieldName = [2], TemplateName = (null), Mode = ReadOnly, TemplateInfo.HtmlFieldPrefix = , AdditionalViewData = (null)",
+ result);
+ }
+
+ [Fact]
+ public void CollectionTemplateNullItemInCollectionOfNullableValueTypesDoesNotDiscardNullable()
+ {
+ // Arrange
+ HtmlHelper html = MakeHtmlHelper<List<int?>>(new List<int?> { 1, null, 2 });
+ html.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = null;
+
+ // Act
+ string result = DefaultDisplayTemplates.CollectionTemplate(html, CollectionSpyCallback);
+
+ // Assert
+ Assert.Equal(@"
+Model = 1, ModelType = System.Nullable`1[[System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], PropertyName = (null), HtmlFieldName = [0], TemplateName = (null), Mode = ReadOnly, TemplateInfo.HtmlFieldPrefix = , AdditionalViewData = (null)
+Model = (null), ModelType = System.Nullable`1[[System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], PropertyName = (null), HtmlFieldName = [1], TemplateName = (null), Mode = ReadOnly, TemplateInfo.HtmlFieldPrefix = , AdditionalViewData = (null)
+Model = 2, ModelType = System.Nullable`1[[System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], PropertyName = (null), HtmlFieldName = [2], TemplateName = (null), Mode = ReadOnly, TemplateInfo.HtmlFieldPrefix = , AdditionalViewData = (null)",
+ result);
+ }
+
+ // DecimalTemplate
+
+ [Fact]
+ public void DecimalTemplateTests()
+ {
+ Assert.Equal(
+ @"12.35",
+ DefaultDisplayTemplates.DecimalTemplate(MakeHtmlHelper<decimal>(12.3456M)));
+
+ Assert.Equal(
+ @"Formatted Value",
+ DefaultDisplayTemplates.DecimalTemplate(MakeHtmlHelper<decimal>(12.3456M, "Formatted Value")));
+
+ Assert.Equal(
+ @"&lt;script&gt;alert(&#39;XSS!&#39;)&lt;/script&gt;",
+ DefaultDisplayTemplates.DecimalTemplate(MakeHtmlHelper<decimal>(12.3456M, "<script>alert('XSS!')</script>")));
+ }
+
+ // EmailAddressTemplate
+
+ [Fact]
+ public void EmailAddressTemplateTests()
+ {
+ Assert.Equal(
+ @"<a href=""mailto:foo@bar.com"">foo@bar.com</a>",
+ DefaultDisplayTemplates.EmailAddressTemplate(MakeHtmlHelper<string>("foo@bar.com")));
+
+ Assert.Equal(
+ @"<a href=""mailto:foo@bar.com"">The FooBar User</a>",
+ DefaultDisplayTemplates.EmailAddressTemplate(MakeHtmlHelper<string>("foo@bar.com", "The FooBar User")));
+
+ Assert.Equal(
+ @"<a href=""mailto:&lt;script>alert(&#39;XSS!&#39;)&lt;/script>"">&lt;script&gt;alert(&#39;XSS!&#39;)&lt;/script&gt;</a>",
+ DefaultDisplayTemplates.EmailAddressTemplate(MakeHtmlHelper<string>("<script>alert('XSS!')</script>")));
+
+ Assert.Equal(
+ @"<a href=""mailto:&lt;script>alert(&#39;XSS!&#39;)&lt;/script>"">&lt;b&gt;Encode me!&lt;/b&gt;</a>",
+ DefaultDisplayTemplates.EmailAddressTemplate(MakeHtmlHelper<string>("<script>alert('XSS!')</script>", "<b>Encode me!</b>")));
+ }
+
+ // HiddenInputTemplate
+
+ [Fact]
+ public void HiddenInputTemplateTests()
+ {
+ Assert.Equal(
+ @"Hidden Value",
+ DefaultDisplayTemplates.HiddenInputTemplate(MakeHtmlHelper<string>("Hidden Value")));
+
+ Assert.Equal(
+ @"&lt;b&gt;Encode me!&lt;/b&gt;",
+ DefaultDisplayTemplates.HiddenInputTemplate(MakeHtmlHelper<string>("<script>alert('XSS!')</script>", "<b>Encode me!</b>")));
+
+ var helperWithInvisibleHtml = MakeHtmlHelper<string>("<script>alert('XSS!')</script>", "<b>Encode me!</b>");
+ helperWithInvisibleHtml.ViewData.ModelMetadata.HideSurroundingHtml = true;
+ Assert.Equal(
+ String.Empty,
+ DefaultDisplayTemplates.HiddenInputTemplate(helperWithInvisibleHtml));
+ }
+
+ // HtmlTemplate
+
+ [Fact]
+ public void HtmlTemplateTests()
+ {
+ Assert.Equal(
+ @"Hello, world!",
+ DefaultDisplayTemplates.HtmlTemplate(MakeHtmlHelper<string>("", "Hello, world!")));
+
+ Assert.Equal(
+ @"<b>Hello, world!</b>",
+ DefaultDisplayTemplates.HtmlTemplate(MakeHtmlHelper<string>("", "<b>Hello, world!</b>")));
+ }
+
+ // ObjectTemplate
+
+ private static string SpyCallback(HtmlHelper html, ModelMetadata metadata, string htmlFieldName, string templateName, DataBoundControlMode mode, object additionalViewData)
+ {
+ return String.Format("Model = {0}, ModelType = {1}, PropertyName = {2}, HtmlFieldName = {3}, TemplateName = {4}, Mode = {5}, AdditionalViewData = {6}",
+ metadata.Model ?? "(null)",
+ metadata.ModelType == null ? "(null)" : metadata.ModelType.FullName,
+ metadata.PropertyName ?? "(null)",
+ htmlFieldName == String.Empty ? "(empty)" : htmlFieldName ?? "(null)",
+ templateName ?? "(null)",
+ mode,
+ AnonymousObject.Inspect(additionalViewData));
+ }
+
+ class ObjectTemplateModel
+ {
+ public ObjectTemplateModel()
+ {
+ ComplexInnerModel = new object();
+ }
+
+ public string Property1 { get; set; }
+ public string Property2 { get; set; }
+ public object ComplexInnerModel { get; set; }
+ }
+
+ [Fact]
+ public void ObjectTemplateDisplaysSimplePropertiesOnObjectByDefault()
+ {
+ string expected = @"<div class=""display-label"">Property1</div>
+<div class=""display-field"">Model = p1, ModelType = System.String, PropertyName = Property1, HtmlFieldName = Property1, TemplateName = (null), Mode = ReadOnly, AdditionalViewData = (null)</div>
+<div class=""display-label"">Property2</div>
+<div class=""display-field"">Model = (null), ModelType = System.String, PropertyName = Property2, HtmlFieldName = Property2, TemplateName = (null), Mode = ReadOnly, AdditionalViewData = (null)</div>
+";
+
+ // Arrange
+ ObjectTemplateModel model = new ObjectTemplateModel { Property1 = "p1", Property2 = null };
+ HtmlHelper html = MakeHtmlHelper<ObjectTemplateModel>(model);
+
+ // Act
+ string result = DefaultDisplayTemplates.ObjectTemplate(html, SpyCallback);
+
+ // Assert
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void ObjectTemplateWithDisplayNameMetadata()
+ {
+ string expected = @"<div class=""display-field"">Model = (null), ModelType = System.String, PropertyName = Property1, HtmlFieldName = Property1, TemplateName = (null), Mode = ReadOnly, AdditionalViewData = (null)</div>
+<div class=""display-label"">Custom display name</div>
+<div class=""display-field"">Model = (null), ModelType = System.String, PropertyName = Property2, HtmlFieldName = Property2, TemplateName = (null), Mode = ReadOnly, AdditionalViewData = (null)</div>
+";
+
+ // Arrange
+ ObjectTemplateModel model = new ObjectTemplateModel();
+ HtmlHelper html = MakeHtmlHelper<ObjectTemplateModel>(model);
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+ Func<object> accessor = () => model;
+ Mock<ModelMetadata> metadata = new Mock<ModelMetadata>(provider.Object, null, accessor, typeof(ObjectTemplateModel), null);
+ ModelMetadata prop1Metadata = new ModelMetadata(provider.Object, typeof(ObjectTemplateModel), null, typeof(string), "Property1") { DisplayName = String.Empty };
+ ModelMetadata prop2Metadata = new ModelMetadata(provider.Object, typeof(ObjectTemplateModel), null, typeof(string), "Property2") { DisplayName = "Custom display name" };
+ html.ViewData.ModelMetadata = metadata.Object;
+ metadata.Setup(p => p.Properties).Returns(() => new[] { prop1Metadata, prop2Metadata });
+
+ // Act
+ string result = DefaultDisplayTemplates.ObjectTemplate(html, SpyCallback);
+
+ // Assert
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void ObjectTemplateWithShowForDisplayMetadata()
+ {
+ string expected = @"<div class=""display-label"">Property1</div>
+<div class=""display-field"">Model = (null), ModelType = System.String, PropertyName = Property1, HtmlFieldName = Property1, TemplateName = (null), Mode = ReadOnly, AdditionalViewData = (null)</div>
+";
+
+ // Arrange
+ ObjectTemplateModel model = new ObjectTemplateModel();
+ HtmlHelper html = MakeHtmlHelper<ObjectTemplateModel>(model);
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+ Func<object> accessor = () => model;
+ Mock<ModelMetadata> metadata = new Mock<ModelMetadata>(provider.Object, null, accessor, typeof(ObjectTemplateModel), null);
+ ModelMetadata prop1Metadata = new ModelMetadata(provider.Object, typeof(ObjectTemplateModel), null, typeof(string), "Property1") { ShowForDisplay = true };
+ ModelMetadata prop2Metadata = new ModelMetadata(provider.Object, typeof(ObjectTemplateModel), null, typeof(string), "Property2") { ShowForDisplay = false };
+ html.ViewData.ModelMetadata = metadata.Object;
+ metadata.Setup(p => p.Properties).Returns(() => new[] { prop1Metadata, prop2Metadata });
+
+ // Act
+ string result = DefaultDisplayTemplates.ObjectTemplate(html, SpyCallback);
+
+ // Assert
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void ObjectTemplatePreventsRecursionOnModelValue()
+ {
+ string expected = @"<div class=""display-label"">Property2</div>
+<div class=""display-field"">Model = propValue2, ModelType = System.String, PropertyName = Property2, HtmlFieldName = Property2, TemplateName = (null), Mode = ReadOnly, AdditionalViewData = (null)</div>
+";
+
+ // Arrange
+ ObjectTemplateModel model = new ObjectTemplateModel();
+ HtmlHelper html = MakeHtmlHelper<ObjectTemplateModel>(model);
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+ Func<object> accessor = () => model;
+ Mock<ModelMetadata> metadata = new Mock<ModelMetadata>(provider.Object, null, accessor, typeof(ObjectTemplateModel), null);
+ ModelMetadata prop1Metadata = new ModelMetadata(provider.Object, typeof(ObjectTemplateModel), () => "propValue1", typeof(string), "Property1");
+ ModelMetadata prop2Metadata = new ModelMetadata(provider.Object, typeof(ObjectTemplateModel), () => "propValue2", typeof(string), "Property2");
+ html.ViewData.ModelMetadata = metadata.Object;
+ metadata.Setup(p => p.Properties).Returns(() => new[] { prop1Metadata, prop2Metadata });
+ html.ViewData.TemplateInfo.VisitedObjects.Add("propValue1");
+
+ // Act
+ string result = DefaultDisplayTemplates.ObjectTemplate(html, SpyCallback);
+
+ // Assert
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void ObjectTemplatePreventsRecursionOnModelTypeForNullModelValues()
+ {
+ string expected = @"<div class=""display-label"">Property2</div>
+<div class=""display-field"">Model = propValue2, ModelType = System.String, PropertyName = Property2, HtmlFieldName = Property2, TemplateName = (null), Mode = ReadOnly, AdditionalViewData = (null)</div>
+";
+
+ // Arrange
+ ObjectTemplateModel model = new ObjectTemplateModel();
+ HtmlHelper html = MakeHtmlHelper<ObjectTemplateModel>(model);
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+ Func<object> accessor = () => model;
+ Mock<ModelMetadata> metadata = new Mock<ModelMetadata>(provider.Object, null, accessor, typeof(ObjectTemplateModel), null);
+ ModelMetadata prop1Metadata = new ModelMetadata(provider.Object, typeof(ObjectTemplateModel), null, typeof(string), "Property1");
+ ModelMetadata prop2Metadata = new ModelMetadata(provider.Object, typeof(ObjectTemplateModel), () => "propValue2", typeof(string), "Property2");
+ html.ViewData.ModelMetadata = metadata.Object;
+ metadata.Setup(p => p.Properties).Returns(() => new[] { prop1Metadata, prop2Metadata });
+ html.ViewData.TemplateInfo.VisitedObjects.Add(typeof(string));
+
+ // Act
+ string result = DefaultDisplayTemplates.ObjectTemplate(html, SpyCallback);
+
+ // Assert
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void ObjectTemplateDisplaysNullDisplayTextWhenObjectIsNull()
+ {
+ // Arrange
+ HtmlHelper html = MakeHtmlHelper<ObjectTemplateModel>(null);
+ ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(ObjectTemplateModel));
+ metadata.NullDisplayText = "(null value)";
+ html.ViewData.ModelMetadata = metadata;
+
+ // Act
+ string result = DefaultDisplayTemplates.ObjectTemplate(html, SpyCallback);
+
+ // Assert
+ Assert.Equal(metadata.NullDisplayText, result);
+ }
+
+ [Fact]
+ public void ObjectTemplateDisplaysSimpleDisplayTextWhenTemplateDepthGreaterThanOne()
+ {
+ // Arrange
+ ObjectTemplateModel model = new ObjectTemplateModel();
+ HtmlHelper html = MakeHtmlHelper<ObjectTemplateModel>(model);
+ ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, typeof(ObjectTemplateModel));
+ metadata.SimpleDisplayText = "Simple Display Text";
+ html.ViewData.ModelMetadata = metadata;
+ html.ViewData.TemplateInfo.VisitedObjects.Add("foo");
+ html.ViewData.TemplateInfo.VisitedObjects.Add("bar");
+
+ // Act
+ string result = DefaultDisplayTemplates.ObjectTemplate(html, SpyCallback);
+
+ // Assert
+ Assert.Equal(metadata.SimpleDisplayText, result);
+ }
+
+ [Fact]
+ public void ObjectTemplateWithHiddenHtml()
+ {
+ string expected = @"Model = propValue1, ModelType = System.String, PropertyName = Property1, HtmlFieldName = Property1, TemplateName = (null), Mode = ReadOnly, AdditionalViewData = (null)";
+
+ // Arrange
+ ObjectTemplateModel model = new ObjectTemplateModel();
+ HtmlHelper html = MakeHtmlHelper<ObjectTemplateModel>(model);
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+ Func<object> accessor = () => model;
+ Mock<ModelMetadata> metadata = new Mock<ModelMetadata>(provider.Object, null, accessor, typeof(ObjectTemplateModel), null);
+ ModelMetadata prop1Metadata = new ModelMetadata(provider.Object, typeof(ObjectTemplateModel), () => "propValue1", typeof(string), "Property1") { HideSurroundingHtml = true };
+ html.ViewData.ModelMetadata = metadata.Object;
+ metadata.Setup(p => p.Properties).Returns(() => new[] { prop1Metadata });
+
+ // Act
+ string result = DefaultDisplayTemplates.ObjectTemplate(html, SpyCallback);
+
+ // Assert
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void ObjectTemplateAllPropertiesFromEntityObjectAreHidden()
+ {
+ // Arrange
+ HtmlHelper html = MakeHtmlHelper<ObjectTemplateModel>(new MyEntityObject());
+
+ // Act
+ string result = DefaultDisplayTemplates.ObjectTemplate(html, SpyCallback);
+
+ // Assert
+ Assert.Equal(String.Empty, result);
+ }
+
+ private class MyEntityObject : EntityObject
+ {
+ }
+
+ // StringTemplate
+
+ [Fact]
+ public void StringTemplateTests()
+ {
+ Assert.Equal(
+ @"Hello, world!",
+ DefaultDisplayTemplates.StringTemplate(MakeHtmlHelper<string>("", "Hello, world!")));
+
+ Assert.Equal(
+ @"&lt;b&gt;Hello, world!&lt;/b&gt;",
+ DefaultDisplayTemplates.StringTemplate(MakeHtmlHelper<string>("", "<b>Hello, world!</b>")));
+ }
+
+ // UrlTemplate
+
+ [Fact]
+ public void UrlTemplateTests()
+ {
+ Assert.Equal(
+ @"<a href=""http://www.microsoft.com/testing.aspx?value1=foo&amp;value2=bar"">http://www.microsoft.com/testing.aspx?value1=foo&amp;value2=bar</a>",
+ DefaultDisplayTemplates.UrlTemplate(MakeHtmlHelper<string>("http://www.microsoft.com/testing.aspx?value1=foo&value2=bar")));
+
+ Assert.Equal(
+ @"<a href=""http://www.microsoft.com/testing.aspx?value1=foo&amp;value2=bar"">&lt;b&gt;Microsoft!&lt;/b&gt;</a>",
+ DefaultDisplayTemplates.UrlTemplate(MakeHtmlHelper<string>("http://www.microsoft.com/testing.aspx?value1=foo&value2=bar", "<b>Microsoft!</b>")));
+ }
+
+ // Helpers
+
+ private HtmlHelper MakeHtmlHelper<TModel>(object model)
+ {
+ return MakeHtmlHelper<TModel>(model, model);
+ }
+
+ private HtmlHelper MakeHtmlHelper<TModel>(object model, object formattedModelValue)
+ {
+ ViewDataDictionary viewData = new ViewDataDictionary(model);
+ viewData.TemplateInfo.HtmlFieldPrefix = "FieldPrefix";
+ viewData.TemplateInfo.FormattedModelValue = formattedModelValue;
+ viewData.ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => model, typeof(TModel));
+
+ ViewContext viewContext = new ViewContext(new ControllerContext(), new DummyView(), viewData, new TempDataDictionary(), new StringWriter());
+
+ return new HtmlHelper(viewContext, new SimpleViewDataContainer(viewData));
+ }
+
+ private class DummyView : IView
+ {
+ public void Render(ViewContext viewContext, TextWriter writer)
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Html/Test/DefaultEditorTemplatesTest.cs b/test/System.Web.Mvc.Test/Html/Test/DefaultEditorTemplatesTest.cs
new file mode 100644
index 00000000..25049aca
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Html/Test/DefaultEditorTemplatesTest.cs
@@ -0,0 +1,595 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Data.Linq;
+using System.Data.Objects.DataClasses;
+using System.IO;
+using System.Web.UI.WebControls;
+using Microsoft.Web.UnitTestUtil;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Html.Test
+{
+ public class DefaultEditorTemplatesTest
+ {
+ // BooleanTemplate
+
+ [Fact]
+ public void BooleanTemplateTests()
+ {
+ // Boolean values
+
+ Assert.Equal(
+ @"<input checked=""checked"" class=""check-box"" id=""FieldPrefix"" name=""FieldPrefix"" type=""checkbox"" value=""true"" /><input name=""FieldPrefix"" type=""hidden"" value=""false"" />",
+ DefaultEditorTemplates.BooleanTemplate(MakeHtmlHelper<bool>(true)));
+
+ Assert.Equal(
+ @"<input class=""check-box"" id=""FieldPrefix"" name=""FieldPrefix"" type=""checkbox"" value=""true"" /><input name=""FieldPrefix"" type=""hidden"" value=""false"" />",
+ DefaultEditorTemplates.BooleanTemplate(MakeHtmlHelper<bool>(false)));
+
+ Assert.Equal(
+ @"<input class=""check-box"" id=""FieldPrefix"" name=""FieldPrefix"" type=""checkbox"" value=""true"" /><input name=""FieldPrefix"" type=""hidden"" value=""false"" />",
+ DefaultEditorTemplates.BooleanTemplate(MakeHtmlHelper<bool>(null)));
+
+ // Nullable<Boolean> values
+
+ Assert.Equal(
+ @"<select class=""list-box tri-state"" id=""FieldPrefix"" name=""FieldPrefix""><option value="""">Not Set</option>
+<option selected=""selected"" value=""true"">True</option>
+<option value=""false"">False</option>
+</select>",
+ DefaultEditorTemplates.BooleanTemplate(MakeHtmlHelper<Nullable<bool>>(true)));
+
+ Assert.Equal(
+ @"<select class=""list-box tri-state"" id=""FieldPrefix"" name=""FieldPrefix""><option value="""">Not Set</option>
+<option value=""true"">True</option>
+<option selected=""selected"" value=""false"">False</option>
+</select>",
+ DefaultEditorTemplates.BooleanTemplate(MakeHtmlHelper<Nullable<bool>>(false)));
+
+ Assert.Equal(
+ @"<select class=""list-box tri-state"" id=""FieldPrefix"" name=""FieldPrefix""><option selected=""selected"" value="""">Not Set</option>
+<option value=""true"">True</option>
+<option value=""false"">False</option>
+</select>",
+ DefaultEditorTemplates.BooleanTemplate(MakeHtmlHelper<Nullable<bool>>(null)));
+ }
+
+ // CollectionTemplate
+
+ private static string CollectionSpyCallback(HtmlHelper html, ModelMetadata metadata, string htmlFieldName, string templateName, DataBoundControlMode mode, object additionalViewData)
+ {
+ return String.Format(Environment.NewLine + "Model = {0}, ModelType = {1}, PropertyName = {2}, HtmlFieldName = {3}, TemplateName = {4}, Mode = {5}, TemplateInfo.HtmlFieldPrefix = {6}, AdditionalViewData = {7}",
+ metadata.Model ?? "(null)",
+ metadata.ModelType == null ? "(null)" : metadata.ModelType.FullName,
+ metadata.PropertyName ?? "(null)",
+ htmlFieldName == String.Empty ? "(empty)" : htmlFieldName ?? "(null)",
+ templateName ?? "(null)",
+ mode,
+ html.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix,
+ AnonymousObject.Inspect(additionalViewData));
+ }
+
+ [Fact]
+ public void CollectionTemplateWithNullModel()
+ {
+ // Arrange
+ HtmlHelper html = MakeHtmlHelper<object>(null);
+
+ // Act
+ string result = DefaultEditorTemplates.CollectionTemplate(html, CollectionSpyCallback);
+
+ // Assert
+ Assert.Equal(String.Empty, result);
+ }
+
+ [Fact]
+ public void CollectionTemplateNonEnumerableModelThrows()
+ {
+ // Arrange
+ HtmlHelper html = MakeHtmlHelper<object>(new object());
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ () => DefaultEditorTemplates.CollectionTemplate(html, CollectionSpyCallback),
+ "The Collection template was used with an object of type 'System.Object', which does not implement System.IEnumerable."
+ );
+ }
+
+ [Fact]
+ public void CollectionTemplateWithSingleItemCollectionWithoutPrefix()
+ {
+ // Arrange
+ HtmlHelper html = MakeHtmlHelper<List<string>>(new List<string> { "foo" });
+ html.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = null;
+
+ // Act
+ string result = DefaultEditorTemplates.CollectionTemplate(html, CollectionSpyCallback);
+
+ // Assert
+ Assert.Equal(@"
+Model = foo, ModelType = System.String, PropertyName = (null), HtmlFieldName = [0], TemplateName = (null), Mode = Edit, TemplateInfo.HtmlFieldPrefix = , AdditionalViewData = (null)",
+ result);
+ }
+
+ [Fact]
+ public void CollectionTemplateWithSingleItemCollectionWithPrefix()
+ {
+ // Arrange
+ HtmlHelper html = MakeHtmlHelper<List<string>>(new List<string> { "foo" });
+ html.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "ModelProperty";
+
+ // Act
+ string result = DefaultEditorTemplates.CollectionTemplate(html, CollectionSpyCallback);
+
+ // Assert
+ Assert.Equal(@"
+Model = foo, ModelType = System.String, PropertyName = (null), HtmlFieldName = ModelProperty[0], TemplateName = (null), Mode = Edit, TemplateInfo.HtmlFieldPrefix = , AdditionalViewData = (null)",
+ result);
+ }
+
+ [Fact]
+ public void CollectionTemplateWithMultiItemCollection()
+ {
+ // Arrange
+ HtmlHelper html = MakeHtmlHelper<List<string>>(new List<string> { "foo", "bar", "baz" });
+ html.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = null;
+
+ // Act
+ string result = DefaultEditorTemplates.CollectionTemplate(html, CollectionSpyCallback);
+
+ // Assert
+ Assert.Equal(@"
+Model = foo, ModelType = System.String, PropertyName = (null), HtmlFieldName = [0], TemplateName = (null), Mode = Edit, TemplateInfo.HtmlFieldPrefix = , AdditionalViewData = (null)
+Model = bar, ModelType = System.String, PropertyName = (null), HtmlFieldName = [1], TemplateName = (null), Mode = Edit, TemplateInfo.HtmlFieldPrefix = , AdditionalViewData = (null)
+Model = baz, ModelType = System.String, PropertyName = (null), HtmlFieldName = [2], TemplateName = (null), Mode = Edit, TemplateInfo.HtmlFieldPrefix = , AdditionalViewData = (null)",
+ result);
+ }
+
+ [Fact]
+ public void CollectionTemplateNullITemInWeaklyTypedCollectionUsesModelTypeOfString()
+ {
+ // Arrange
+ HtmlHelper html = MakeHtmlHelper<ArrayList>(new ArrayList { null });
+ html.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = null;
+
+ // Act
+ string result = DefaultEditorTemplates.CollectionTemplate(html, CollectionSpyCallback);
+
+ // Assert
+ Assert.Equal(@"
+Model = (null), ModelType = System.String, PropertyName = (null), HtmlFieldName = [0], TemplateName = (null), Mode = Edit, TemplateInfo.HtmlFieldPrefix = , AdditionalViewData = (null)",
+ result);
+ }
+
+ [Fact]
+ public void CollectionTemplateNullItemInStronglyTypedCollectionUsesModelTypeFromIEnumerable()
+ {
+ // Arrange
+ HtmlHelper html = MakeHtmlHelper<List<IHttpHandler>>(new List<IHttpHandler> { null });
+ html.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = null;
+
+ // Act
+ string result = DefaultEditorTemplates.CollectionTemplate(html, CollectionSpyCallback);
+
+ // Assert
+ Assert.Equal(@"
+Model = (null), ModelType = System.Web.IHttpHandler, PropertyName = (null), HtmlFieldName = [0], TemplateName = (null), Mode = Edit, TemplateInfo.HtmlFieldPrefix = , AdditionalViewData = (null)",
+ result);
+ }
+
+ [Fact]
+ public void CollectionTemplateUsesRealObjectTypes()
+ {
+ // Arrange
+ HtmlHelper html = MakeHtmlHelper<List<Object>>(new List<Object> { 1, 2.3, "Hello World" });
+ html.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = null;
+
+ // Act
+ string result = DefaultEditorTemplates.CollectionTemplate(html, CollectionSpyCallback);
+
+ // Assert
+ Assert.Equal(@"
+Model = 1, ModelType = System.Int32, PropertyName = (null), HtmlFieldName = [0], TemplateName = (null), Mode = Edit, TemplateInfo.HtmlFieldPrefix = , AdditionalViewData = (null)
+Model = 2.3, ModelType = System.Double, PropertyName = (null), HtmlFieldName = [1], TemplateName = (null), Mode = Edit, TemplateInfo.HtmlFieldPrefix = , AdditionalViewData = (null)
+Model = Hello World, ModelType = System.String, PropertyName = (null), HtmlFieldName = [2], TemplateName = (null), Mode = Edit, TemplateInfo.HtmlFieldPrefix = , AdditionalViewData = (null)",
+ result);
+ }
+
+ [Fact]
+ public void CollectionTemplateNullItemInCollectionOfNullableValueTypesDoesNotDiscardNullable()
+ {
+ // Arrange
+ HtmlHelper html = MakeHtmlHelper<List<int?>>(new List<int?> { 1, null, 2 });
+ html.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = null;
+
+ // Act
+ string result = DefaultEditorTemplates.CollectionTemplate(html, CollectionSpyCallback);
+
+ // Assert
+ Assert.Equal(@"
+Model = 1, ModelType = System.Nullable`1[[System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], PropertyName = (null), HtmlFieldName = [0], TemplateName = (null), Mode = Edit, TemplateInfo.HtmlFieldPrefix = , AdditionalViewData = (null)
+Model = (null), ModelType = System.Nullable`1[[System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], PropertyName = (null), HtmlFieldName = [1], TemplateName = (null), Mode = Edit, TemplateInfo.HtmlFieldPrefix = , AdditionalViewData = (null)
+Model = 2, ModelType = System.Nullable`1[[System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], PropertyName = (null), HtmlFieldName = [2], TemplateName = (null), Mode = Edit, TemplateInfo.HtmlFieldPrefix = , AdditionalViewData = (null)",
+ result);
+ }
+
+ // DecimalTemplate
+
+ [Fact]
+ public void DecimalTemplateTests()
+ {
+ Assert.Equal(
+ @"<input class=""text-box single-line"" id=""FieldPrefix"" name=""FieldPrefix"" type=""text"" value=""12.35"" />",
+ DefaultEditorTemplates.DecimalTemplate(MakeHtmlHelper<decimal>(12.3456M)));
+
+ Assert.Equal(
+ @"<input class=""text-box single-line"" id=""FieldPrefix"" name=""FieldPrefix"" type=""text"" value=""Formatted Value"" />",
+ DefaultEditorTemplates.DecimalTemplate(MakeHtmlHelper<decimal>(12.3456M, "Formatted Value")));
+
+ Assert.Equal(
+ @"<input class=""text-box single-line"" id=""FieldPrefix"" name=""FieldPrefix"" type=""text"" value=""&lt;script>alert(&#39;XSS!&#39;)&lt;/script>"" />",
+ DefaultEditorTemplates.DecimalTemplate(MakeHtmlHelper<decimal>(12.3456M, "<script>alert('XSS!')</script>")));
+ }
+
+ // HiddenInputTemplate
+
+ [Fact]
+ public void HiddenInputTemplateTests()
+ {
+ Assert.Equal(
+ @"Hidden Value<input id=""FieldPrefix"" name=""FieldPrefix"" type=""hidden"" value=""Hidden Value"" />",
+ DefaultEditorTemplates.HiddenInputTemplate(MakeHtmlHelper<string>("Hidden Value")));
+
+ Assert.Equal(
+ @"&lt;script&gt;alert(&#39;XSS!&#39;)&lt;/script&gt;<input id=""FieldPrefix"" name=""FieldPrefix"" type=""hidden"" value=""&lt;script>alert(&#39;XSS!&#39;)&lt;/script>"" />",
+ DefaultEditorTemplates.HiddenInputTemplate(MakeHtmlHelper<string>("<script>alert('XSS!')</script>")));
+
+ var helperWithInvisibleHtml = MakeHtmlHelper<string>("<script>alert('XSS!')</script>", "<b>Encode me!</b>");
+ helperWithInvisibleHtml.ViewData.ModelMetadata.HideSurroundingHtml = true;
+ Assert.Equal(
+ @"<input id=""FieldPrefix"" name=""FieldPrefix"" type=""hidden"" value=""&lt;script>alert(&#39;XSS!&#39;)&lt;/script>"" />",
+ DefaultEditorTemplates.HiddenInputTemplate(helperWithInvisibleHtml));
+
+ byte[] byteValues = { 1, 2, 3, 4, 5 };
+
+ Assert.Equal(
+ @"&quot;AQIDBAU=&quot;<input id=""FieldPrefix"" name=""FieldPrefix"" type=""hidden"" value=""AQIDBAU="" />",
+ DefaultEditorTemplates.HiddenInputTemplate(MakeHtmlHelper<Binary>(new Binary(byteValues))));
+
+ Assert.Equal(
+ @"System.Byte[]<input id=""FieldPrefix"" name=""FieldPrefix"" type=""hidden"" value=""AQIDBAU="" />",
+ DefaultEditorTemplates.HiddenInputTemplate(MakeHtmlHelper<byte[]>(byteValues)));
+ }
+
+ // MultilineText
+
+ [Fact]
+ public void MultilineTextTemplateTests()
+ {
+ Assert.Equal(
+ @"<textarea class=""text-box multi-line"" id=""FieldPrefix"" name=""FieldPrefix"">
+Multiple
+Line
+Value!</textarea>",
+ DefaultEditorTemplates.MultilineTextTemplate(MakeHtmlHelper<string>("", @"Multiple
+Line
+Value!")));
+
+ Assert.Equal(
+ @"<textarea class=""text-box multi-line"" id=""FieldPrefix"" name=""FieldPrefix"">
+&lt;script&gt;alert(&#39;XSS!&#39;)&lt;/script&gt;</textarea>",
+ DefaultEditorTemplates.MultilineTextTemplate(MakeHtmlHelper<string>("", "<script>alert('XSS!')</script>")));
+ }
+
+ // ObjectTemplate
+
+ private static string SpyCallback(HtmlHelper html, ModelMetadata metadata, string htmlFieldName, string templateName, DataBoundControlMode mode, object additionalViewData)
+ {
+ return String.Format("Model = {0}, ModelType = {1}, PropertyName = {2}, HtmlFieldName = {3}, TemplateName = {4}, Mode = {5}, AdditionalViewData = {6}",
+ metadata.Model ?? "(null)",
+ metadata.ModelType == null ? "(null)" : metadata.ModelType.FullName,
+ metadata.PropertyName ?? "(null)",
+ htmlFieldName == String.Empty ? "(empty)" : htmlFieldName ?? "(null)",
+ templateName ?? "(null)",
+ mode,
+ AnonymousObject.Inspect(additionalViewData));
+ }
+
+ class ObjectTemplateModel
+ {
+ public ObjectTemplateModel()
+ {
+ ComplexInnerModel = new object();
+ }
+
+ public string Property1 { get; set; }
+ public string Property2 { get; set; }
+ public object ComplexInnerModel { get; set; }
+ }
+
+ [Fact]
+ public void ObjectTemplateEditsSimplePropertiesOnObjectByDefault()
+ {
+ string expected = @"<div class=""editor-label""><label for=""FieldPrefix_Property1"">Property1</label></div>
+<div class=""editor-field"">Model = p1, ModelType = System.String, PropertyName = Property1, HtmlFieldName = Property1, TemplateName = (null), Mode = Edit, AdditionalViewData = (null) </div>
+<div class=""editor-label""><label for=""FieldPrefix_Property2"">Property2</label></div>
+<div class=""editor-field"">Model = (null), ModelType = System.String, PropertyName = Property2, HtmlFieldName = Property2, TemplateName = (null), Mode = Edit, AdditionalViewData = (null) </div>
+";
+
+ // Arrange
+ ObjectTemplateModel model = new ObjectTemplateModel { Property1 = "p1", Property2 = null };
+ HtmlHelper html = MakeHtmlHelper<ObjectTemplateModel>(model);
+
+ // Act
+ string result = DefaultEditorTemplates.ObjectTemplate(html, SpyCallback);
+
+ // Assert
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void ObjectTemplateWithModelError()
+ {
+ string expected = @"<div class=""editor-label""><label for=""FieldPrefix_Property1"">Property1</label></div>
+<div class=""editor-field"">Model = p1, ModelType = System.String, PropertyName = Property1, HtmlFieldName = Property1, TemplateName = (null), Mode = Edit, AdditionalViewData = (null) <span class=""field-validation-error"">Error Message</span></div>
+<div class=""editor-label""><label for=""FieldPrefix_Property2"">Property2</label></div>
+<div class=""editor-field"">Model = (null), ModelType = System.String, PropertyName = Property2, HtmlFieldName = Property2, TemplateName = (null), Mode = Edit, AdditionalViewData = (null) </div>
+";
+
+ // Arrange
+ ObjectTemplateModel model = new ObjectTemplateModel { Property1 = "p1", Property2 = null };
+ HtmlHelper html = MakeHtmlHelper<ObjectTemplateModel>(model);
+ html.ViewData.ModelState.AddModelError("FieldPrefix.Property1", "Error Message");
+
+ // Act
+ string result = DefaultEditorTemplates.ObjectTemplate(html, SpyCallback);
+
+ // Assert
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void ObjectTemplateWithDisplayNameMetadata()
+ {
+ string expected = @"<div class=""editor-field"">Model = (null), ModelType = System.String, PropertyName = Property1, HtmlFieldName = Property1, TemplateName = (null), Mode = Edit, AdditionalViewData = (null) </div>
+<div class=""editor-label""><label for=""FieldPrefix_Property2"">Custom display name</label></div>
+<div class=""editor-field"">Model = (null), ModelType = System.String, PropertyName = Property2, HtmlFieldName = Property2, TemplateName = (null), Mode = Edit, AdditionalViewData = (null) </div>
+";
+
+ // Arrange
+ ObjectTemplateModel model = new ObjectTemplateModel();
+ HtmlHelper html = MakeHtmlHelper<ObjectTemplateModel>(model);
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+ Func<object> accessor = () => model;
+ Mock<ModelMetadata> metadata = new Mock<ModelMetadata>(provider.Object, null, accessor, typeof(ObjectTemplateModel), null);
+ ModelMetadata prop1Metadata = new ModelMetadata(provider.Object, typeof(ObjectTemplateModel), null, typeof(string), "Property1") { DisplayName = String.Empty };
+ ModelMetadata prop2Metadata = new ModelMetadata(provider.Object, typeof(ObjectTemplateModel), null, typeof(string), "Property2") { DisplayName = "Custom display name" };
+ html.ViewData.ModelMetadata = metadata.Object;
+ metadata.Setup(p => p.Properties).Returns(() => new[] { prop1Metadata, prop2Metadata });
+
+ // Act
+ string result = DefaultEditorTemplates.ObjectTemplate(html, SpyCallback);
+
+ // Assert
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void ObjectTemplateWithShowForEditorMetadata()
+ {
+ string expected = @"<div class=""editor-label""><label for=""FieldPrefix_Property1"">Property1</label></div>
+<div class=""editor-field"">Model = (null), ModelType = System.String, PropertyName = Property1, HtmlFieldName = Property1, TemplateName = (null), Mode = Edit, AdditionalViewData = (null) </div>
+";
+
+ // Arrange
+ ObjectTemplateModel model = new ObjectTemplateModel();
+ HtmlHelper html = MakeHtmlHelper<ObjectTemplateModel>(model);
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+ Func<object> accessor = () => model;
+ Mock<ModelMetadata> metadata = new Mock<ModelMetadata>(provider.Object, null, accessor, typeof(ObjectTemplateModel), null);
+ ModelMetadata prop1Metadata = new ModelMetadata(provider.Object, typeof(ObjectTemplateModel), null, typeof(string), "Property1") { ShowForEdit = true };
+ ModelMetadata prop2Metadata = new ModelMetadata(provider.Object, typeof(ObjectTemplateModel), null, typeof(string), "Property2") { ShowForEdit = false };
+ html.ViewData.ModelMetadata = metadata.Object;
+ metadata.Setup(p => p.Properties).Returns(() => new[] { prop1Metadata, prop2Metadata });
+
+ // Act
+ string result = DefaultEditorTemplates.ObjectTemplate(html, SpyCallback);
+
+ // Assert
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void ObjectTemplatePreventsRecursionOnModelValue()
+ {
+ string expected = @"<div class=""editor-label""><label for=""FieldPrefix_Property2"">Property2</label></div>
+<div class=""editor-field"">Model = propValue2, ModelType = System.String, PropertyName = Property2, HtmlFieldName = Property2, TemplateName = (null), Mode = Edit, AdditionalViewData = (null) </div>
+";
+
+ // Arrange
+ ObjectTemplateModel model = new ObjectTemplateModel();
+ HtmlHelper html = MakeHtmlHelper<ObjectTemplateModel>(model);
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+ Func<object> accessor = () => model;
+ Mock<ModelMetadata> metadata = new Mock<ModelMetadata>(provider.Object, null, accessor, typeof(ObjectTemplateModel), null);
+ ModelMetadata prop1Metadata = new ModelMetadata(provider.Object, typeof(ObjectTemplateModel), () => "propValue1", typeof(string), "Property1");
+ ModelMetadata prop2Metadata = new ModelMetadata(provider.Object, typeof(ObjectTemplateModel), () => "propValue2", typeof(string), "Property2");
+ html.ViewData.ModelMetadata = metadata.Object;
+ metadata.Setup(p => p.Properties).Returns(() => new[] { prop1Metadata, prop2Metadata });
+ html.ViewData.TemplateInfo.VisitedObjects.Add("propValue1");
+
+ // Act
+ string result = DefaultEditorTemplates.ObjectTemplate(html, SpyCallback);
+
+ // Assert
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void ObjectTemplatePreventsRecursionOnModelTypeForNullModelValues()
+ {
+ string expected = @"<div class=""editor-label""><label for=""FieldPrefix_Property2"">Property2</label></div>
+<div class=""editor-field"">Model = propValue2, ModelType = System.String, PropertyName = Property2, HtmlFieldName = Property2, TemplateName = (null), Mode = Edit, AdditionalViewData = (null) </div>
+";
+
+ // Arrange
+ ObjectTemplateModel model = new ObjectTemplateModel();
+ HtmlHelper html = MakeHtmlHelper<ObjectTemplateModel>(model);
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+ Func<object> accessor = () => model;
+ Mock<ModelMetadata> metadata = new Mock<ModelMetadata>(provider.Object, null, accessor, typeof(ObjectTemplateModel), null);
+ ModelMetadata prop1Metadata = new ModelMetadata(provider.Object, typeof(ObjectTemplateModel), null, typeof(string), "Property1");
+ ModelMetadata prop2Metadata = new ModelMetadata(provider.Object, typeof(ObjectTemplateModel), () => "propValue2", typeof(string), "Property2");
+ html.ViewData.ModelMetadata = metadata.Object;
+ metadata.Setup(p => p.Properties).Returns(() => new[] { prop1Metadata, prop2Metadata });
+ html.ViewData.TemplateInfo.VisitedObjects.Add(typeof(string));
+
+ // Act
+ string result = DefaultEditorTemplates.ObjectTemplate(html, SpyCallback);
+
+ // Assert
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void ObjectTemplateDisplaysNullDisplayTextWithNullModelAndTemplateDepthGreaterThanOne()
+ {
+ // Arrange
+ HtmlHelper html = MakeHtmlHelper<ObjectTemplateModel>(null);
+ ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(ObjectTemplateModel));
+ metadata.NullDisplayText = "Null Display Text";
+ metadata.SimpleDisplayText = "Simple Display Text";
+ html.ViewData.ModelMetadata = metadata;
+ html.ViewData.TemplateInfo.VisitedObjects.Add("foo");
+ html.ViewData.TemplateInfo.VisitedObjects.Add("bar");
+
+ // Act
+ string result = DefaultEditorTemplates.ObjectTemplate(html, SpyCallback);
+
+ // Assert
+ Assert.Equal(metadata.NullDisplayText, result);
+ }
+
+ [Fact]
+ public void ObjectTemplateDisplaysSimpleDisplayTextWithNonNullModelTemplateDepthGreaterThanOne()
+ {
+ // Arrange
+ ObjectTemplateModel model = new ObjectTemplateModel();
+ HtmlHelper html = MakeHtmlHelper<ObjectTemplateModel>(model);
+ ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, typeof(ObjectTemplateModel));
+ html.ViewData.ModelMetadata = metadata;
+ metadata.NullDisplayText = "Null Display Text";
+ metadata.SimpleDisplayText = "Simple Display Text";
+ html.ViewData.TemplateInfo.VisitedObjects.Add("foo");
+ html.ViewData.TemplateInfo.VisitedObjects.Add("bar");
+
+ // Act
+ string result = DefaultEditorTemplates.ObjectTemplate(html, SpyCallback);
+
+ // Assert
+ Assert.Equal(metadata.SimpleDisplayText, result);
+ }
+
+ // PasswordTemplate
+
+ [Fact]
+ public void PasswordTemplateTests()
+ {
+ Assert.Equal(
+ @"<input class=""text-box single-line password"" id=""FieldPrefix"" name=""FieldPrefix"" type=""password"" value=""Value"" />",
+ DefaultEditorTemplates.PasswordTemplate(MakeHtmlHelper<string>("Value")));
+
+ Assert.Equal(
+ @"<input class=""text-box single-line password"" id=""FieldPrefix"" name=""FieldPrefix"" type=""password"" value=""&lt;script>alert(&#39;XSS!&#39;)&lt;/script>"" />",
+ DefaultEditorTemplates.PasswordTemplate(MakeHtmlHelper<string>("<script>alert('XSS!')</script>")));
+ }
+
+ [Fact]
+ public void ObjectTemplateWithHiddenHtml()
+ {
+ string expected = @"Model = propValue1, ModelType = System.String, PropertyName = Property1, HtmlFieldName = Property1, TemplateName = (null), Mode = Edit, AdditionalViewData = (null)";
+
+ // Arrange
+ ObjectTemplateModel model = new ObjectTemplateModel();
+ HtmlHelper html = MakeHtmlHelper<ObjectTemplateModel>(model);
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+ Func<object> accessor = () => model;
+ Mock<ModelMetadata> metadata = new Mock<ModelMetadata>(provider.Object, null, accessor, typeof(ObjectTemplateModel), null);
+ ModelMetadata prop1Metadata = new ModelMetadata(provider.Object, typeof(ObjectTemplateModel), () => "propValue1", typeof(string), "Property1") { HideSurroundingHtml = true };
+ html.ViewData.ModelMetadata = metadata.Object;
+ metadata.Setup(p => p.Properties).Returns(() => new[] { prop1Metadata });
+
+ // Act
+ string result = DefaultEditorTemplates.ObjectTemplate(html, SpyCallback);
+
+ // Assert
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void ObjectTemplateAllPropertiesFromEntityObjectAreHidden()
+ {
+ // Arrange
+ HtmlHelper html = MakeHtmlHelper<ObjectTemplateModel>(new MyEntityObject());
+
+ // Act
+ string result = DefaultEditorTemplates.ObjectTemplate(html, SpyCallback);
+
+ // Assert
+ Assert.Equal(String.Empty, result);
+ }
+
+ private class MyEntityObject : EntityObject
+ {
+ }
+
+ // StringTemplate
+
+ [Fact]
+ public void StringTemplateTests()
+ {
+ Assert.Equal(
+ @"<input class=""text-box single-line"" id=""FieldPrefix"" name=""FieldPrefix"" type=""text"" value=""Value"" />",
+ DefaultEditorTemplates.StringTemplate(MakeHtmlHelper<string>("Value")));
+
+ Assert.Equal(
+ @"<input class=""text-box single-line"" id=""FieldPrefix"" name=""FieldPrefix"" type=""text"" value=""&lt;script>alert(&#39;XSS!&#39;)&lt;/script>"" />",
+ DefaultEditorTemplates.StringTemplate(MakeHtmlHelper<string>("<script>alert('XSS!')</script>")));
+ }
+
+ // Helpers
+
+ private HtmlHelper MakeHtmlHelper<TModel>(object model)
+ {
+ return MakeHtmlHelper<TModel>(model, model);
+ }
+
+ private HtmlHelper MakeHtmlHelper<TModel>(object model, object formattedModelValue)
+ {
+ ViewDataDictionary viewData = new ViewDataDictionary(model);
+ viewData.TemplateInfo.HtmlFieldPrefix = "FieldPrefix";
+ viewData.TemplateInfo.FormattedModelValue = formattedModelValue;
+ viewData.ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => model, typeof(TModel));
+
+ Mock<HttpContextBase> mockHttpContext = new Mock<HttpContextBase>();
+ mockHttpContext.Setup(o => o.Items).Returns(new Hashtable());
+
+ ViewContext viewContext = new ViewContext(new ControllerContext(), new DummyView(), viewData, new TempDataDictionary(), new StringWriter())
+ {
+ HttpContext = mockHttpContext.Object
+ };
+
+ return new HtmlHelper(viewContext, new SimpleViewDataContainer(viewData));
+ }
+
+ private class DummyView : IView
+ {
+ public void Render(ViewContext viewContext, TextWriter writer)
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Html/Test/DisplayNameExtensionsTest.cs b/test/System.Web.Mvc.Test/Html/Test/DisplayNameExtensionsTest.cs
new file mode 100644
index 00000000..dd2bbca8
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Html/Test/DisplayNameExtensionsTest.cs
@@ -0,0 +1,255 @@
+using System.Collections.Generic;
+using System.Linq.Expressions;
+using Microsoft.Web.UnitTestUtil;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Html.Test
+{
+ public class DisplayNameExtensionsTest
+ {
+ [Fact]
+ public void DisplayNameNullExpressionThrows()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => MvcHelper.GetHtmlHelper().Display(null),
+ "expression");
+ }
+
+ [Fact]
+ public void DisplayNameWithNoModelMetadataDisplayNameOverride()
+ {
+ // Act
+ MvcHtmlString result = MvcHelper.GetHtmlHelper().DisplayNameInternal("PropertyName", new MetadataHelper().MetadataProvider.Object);
+
+ // Assert
+ Assert.Equal("PropertyName", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void DisplayNameUsesMetadataForDisplayText()
+ {
+ // Arrange
+ MetadataHelper metadataHelper = new MetadataHelper();
+ metadataHelper.Metadata.Setup(m => m.DisplayName).Returns("Custom display name from metadata");
+
+ // Act
+ MvcHtmlString result = MvcHelper.GetHtmlHelper().DisplayNameInternal("PropertyName", metadataHelper.MetadataProvider.Object);
+
+ // Assert
+ Assert.Equal("Custom display name from metadata", result.ToHtmlString());
+ }
+
+ private sealed class Model
+ {
+ public string PropertyName { get; set; }
+ }
+
+ [Fact]
+ public void DisplayNameConsultsMetadataProviderForMetadataAboutProperty()
+ {
+ // Arrange
+ Model model = new Model { PropertyName = "propertyValue" };
+
+ ViewDataDictionary viewData = new ViewDataDictionary();
+ Mock<ViewContext> viewContext = new Mock<ViewContext>();
+ viewContext.Setup(c => c.ViewData).Returns(viewData);
+
+ Mock<IViewDataContainer> viewDataContainer = new Mock<IViewDataContainer>();
+ viewDataContainer.Setup(c => c.ViewData).Returns(viewData);
+
+ HtmlHelper<Model> html = new HtmlHelper<Model>(viewContext.Object, viewDataContainer.Object);
+ viewData.Model = model;
+
+ MetadataHelper metadataHelper = new MetadataHelper();
+
+ metadataHelper.MetadataProvider.Setup(p => p.GetMetadataForProperty(It.IsAny<Func<object>>(), typeof(Model), "PropertyName"))
+ .Returns(metadataHelper.Metadata.Object)
+ .Verifiable();
+
+ // Act
+ html.DisplayNameInternal("PropertyName", metadataHelper.MetadataProvider.Object);
+
+ // Assert
+ metadataHelper.MetadataProvider.Verify();
+ }
+
+ [Fact]
+ public void DisplayNameUsesMetadataForPropertyName()
+ {
+ // Arrange
+ MetadataHelper metadataHelper = new MetadataHelper();
+
+ metadataHelper.Metadata = new Mock<ModelMetadata>(metadataHelper.MetadataProvider.Object, null, null, typeof(object), "Custom property name from metadata");
+ metadataHelper.MetadataProvider.Setup(p => p.GetMetadataForType(It.IsAny<Func<object>>(), It.IsAny<Type>()))
+ .Returns(metadataHelper.Metadata.Object);
+
+ // Act
+ MvcHtmlString result = MvcHelper.GetHtmlHelper().DisplayNameInternal("PropertyName", metadataHelper.MetadataProvider.Object);
+
+ // Assert
+ Assert.Equal("Custom property name from metadata", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void DisplayNameForNullExpressionThrows()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => MvcHelper.GetHtmlHelper().DisplayNameFor((Expression<Func<Object, Object>>)null),
+ "expression");
+
+ Assert.ThrowsArgumentNull(
+ () => GetEnumerableHtmlHelper().DisplayNameFor((Expression<Func<Foo, Object>>)null),
+ "expression");
+ }
+
+ [Fact]
+ public void DisplayNameForNonMemberExpressionThrows()
+ {
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ () => MvcHelper.GetHtmlHelper().DisplayNameFor(model => new { foo = "Bar" }),
+ "Templates can be used only with field access, property access, single-dimension array index, or single-parameter custom indexer expressions.");
+
+ Assert.Throws<InvalidOperationException>(
+ () => GetEnumerableHtmlHelper().DisplayNameFor((Expression<Func<IEnumerable<Foo>, object>>)(model => new { foo = "Bar" })),
+ "Templates can be used only with field access, property access, single-dimension array index, or single-parameter custom indexer expressions.");
+ }
+
+ [Fact]
+ public void DisplayNameForWithNoModelMetadataDisplayNameOverride()
+ {
+ // Arrange
+ string unknownKey = "this is a dummy parameter value";
+
+ // Act
+ MvcHtmlString result = MvcHelper.GetHtmlHelper().DisplayNameFor(model => unknownKey);
+ MvcHtmlString enumerableResult = GetEnumerableHtmlHelper().DisplayNameFor((Expression<Func<IEnumerable<Foo>, string>>)(model => unknownKey));
+
+ // Assert
+ Assert.Equal("unknownKey", result.ToHtmlString());
+ Assert.Equal("unknownKey", enumerableResult.ToHtmlString());
+ }
+
+ [Fact]
+ public void DisplayNameForUsesModelMetadata()
+ {
+ // Arrange
+ MetadataHelper metadataHelper = new MetadataHelper();
+
+ metadataHelper.Metadata.Setup(m => m.DisplayName).Returns("Custom display name from metadata");
+ string unknownKey = "this is a dummy parameter value";
+
+ // Act
+ MvcHtmlString result = MvcHelper.GetHtmlHelper().DisplayNameForInternal(model => unknownKey, metadataHelper.MetadataProvider.Object);
+ MvcHtmlString enumerableResult = GetEnumerableHtmlHelper().DisplayNameForInternal(model => model.Bar, metadataHelper.MetadataProvider.Object);
+
+ // Assert
+ Assert.Equal("Custom display name from metadata", result.ToHtmlString());
+ Assert.Equal("Custom display name from metadata", enumerableResult.ToHtmlString());
+ }
+
+ [Fact]
+ public void DisplayNameForEmptyDisplayNameReturnsEmptyName()
+ {
+ // Arrange
+ MetadataHelper metadataHelper = new MetadataHelper();
+
+ metadataHelper.Metadata.Setup(m => m.DisplayName).Returns(String.Empty);
+ string unknownKey = "this is a dummy parameter value";
+
+ // Act
+ MvcHtmlString result = MvcHelper.GetHtmlHelper().DisplayNameForInternal(model => unknownKey, metadataHelper.MetadataProvider.Object);
+ MvcHtmlString enumerableResult = GetEnumerableHtmlHelper().DisplayNameForInternal(model => model.Bar, metadataHelper.MetadataProvider.Object);
+
+ // Assert
+ Assert.Equal(String.Empty, result.ToHtmlString());
+ Assert.Equal(String.Empty, enumerableResult.ToHtmlString());
+ }
+
+ [Fact]
+ public void DisplayNameForModelUsesModelMetadata()
+ {
+ // Arrange
+ ViewDataDictionary viewData = new ViewDataDictionary();
+ Mock<ModelMetadata> metadata = new MetadataHelper().Metadata;
+ metadata.Setup(m => m.DisplayName).Returns("Custom display name from metadata");
+
+ viewData.ModelMetadata = metadata.Object;
+ viewData.TemplateInfo.HtmlFieldPrefix = "Prefix";
+
+ // Act
+ MvcHtmlString result = MvcHelper.GetHtmlHelper(viewData).DisplayNameForModel();
+
+ // Assert
+ Assert.Equal("Custom display name from metadata", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void DisplayNameForWithNestedClass()
+ {
+ // Arrange
+ ViewDataDictionary viewData = new ViewDataDictionary();
+ Mock<ViewContext> viewContext = new Mock<ViewContext>();
+ viewContext.Setup(c => c.ViewData).Returns(viewData);
+
+ Mock<IViewDataContainer> viewDataContainer = new Mock<IViewDataContainer>();
+ viewDataContainer.Setup(c => c.ViewData).Returns(viewData);
+
+ HtmlHelper<NestedProduct> html = new HtmlHelper<NestedProduct>(viewContext.Object, viewDataContainer.Object);
+
+ // Act
+ MvcHtmlString result = html.DisplayNameForInternal(nested => nested.product.Id, new MetadataHelper().MetadataProvider.Object);
+
+ //Assert
+ Assert.Equal("Id", result.ToHtmlString());
+ }
+
+ private class Product
+ {
+ public int Id { get; set; }
+ }
+
+ private class Cart
+ {
+ public Product[] Products { get; set; }
+ }
+
+ private class NestedProduct
+ {
+ public Product product = new Product();
+ }
+
+ private sealed class Foo
+ {
+ public string Bar { get; set; }
+ }
+
+ private static HtmlHelper<IEnumerable<Foo>> GetEnumerableHtmlHelper()
+ {
+ return MvcHelper.GetHtmlHelper(new ViewDataDictionary<IEnumerable<Foo>>());
+ }
+
+ private sealed class MetadataHelper
+ {
+ public Mock<ModelMetadata> Metadata { get; set; }
+ public Mock<ModelMetadataProvider> MetadataProvider { get; set; }
+
+ public MetadataHelper()
+ {
+ MetadataProvider = new Mock<ModelMetadataProvider>();
+ Metadata = new Mock<ModelMetadata>(MetadataProvider.Object, null, null, typeof(object), null);
+
+ MetadataProvider.Setup(p => p.GetMetadataForProperties(It.IsAny<object>(), It.IsAny<Type>()))
+ .Returns(new ModelMetadata[0]);
+ MetadataProvider.Setup(p => p.GetMetadataForProperty(It.IsAny<Func<object>>(), It.IsAny<Type>(), It.IsAny<string>()))
+ .Returns(Metadata.Object);
+ MetadataProvider.Setup(p => p.GetMetadataForType(It.IsAny<Func<object>>(), It.IsAny<Type>()))
+ .Returns(Metadata.Object);
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Html/Test/FormExtensionsTest.cs b/test/System.Web.Mvc.Test/Html/Test/FormExtensionsTest.cs
new file mode 100644
index 00000000..c5bc9781
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Html/Test/FormExtensionsTest.cs
@@ -0,0 +1,423 @@
+using System.Collections;
+using System.Collections.Specialized;
+using System.IO;
+using System.Web.Routing;
+using Microsoft.Web.UnitTestUtil;
+using Moq;
+using Xunit;
+
+namespace System.Web.Mvc.Html.Test
+{
+ public class FormExtensionsTest
+ {
+ private static void BeginFormHelper(Func<HtmlHelper, MvcForm> beginForm, string expectedFormTag)
+ {
+ // Arrange
+ StringWriter writer;
+ HtmlHelper htmlHelper = GetFormHelper(out writer);
+
+ // Act
+ IDisposable formDisposable = beginForm(htmlHelper);
+ formDisposable.Dispose();
+
+ // Assert
+ Assert.Equal(expectedFormTag + "</form>", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginFormParameterDictionaryMerging()
+ {
+ BeginFormHelper(
+ htmlHelper => htmlHelper.BeginForm("bar", "foo", FormMethod.Get, new RouteValueDictionary(new { method = "post" })),
+ @"<form action=""" + MvcHelper.AppPathModifier + @"/foo/bar"" method=""get"">");
+ }
+
+ [Fact]
+ public void BeginFormSetsAndRestoresToDefault()
+ {
+ // Arrange
+ StringWriter writer;
+ HtmlHelper htmlHelper = GetFormHelper(out writer);
+
+ htmlHelper.ViewContext.FormContext = null;
+ FormContext defaultFormContext = htmlHelper.ViewContext.FormContext;
+
+ // Act & assert - push
+ MvcForm theForm = htmlHelper.BeginForm();
+ Assert.NotNull(htmlHelper.ViewContext.FormContext);
+ Assert.NotEqual(defaultFormContext, htmlHelper.ViewContext.FormContext);
+
+ // Act & assert - pop
+ theForm.Dispose();
+ Assert.Equal(defaultFormContext, htmlHelper.ViewContext.FormContext);
+ Assert.Equal(@"<form action=""/some/path"" method=""post""></form>", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginFormWithClientValidationEnabled()
+ {
+ // Arrange
+ StringWriter writer;
+ HtmlHelper htmlHelper = GetFormHelper(out writer);
+
+ htmlHelper.ViewContext.ClientValidationEnabled = true;
+ htmlHelper.ViewContext.FormContext = null;
+ FormContext defaultFormContext = htmlHelper.ViewContext.FormContext;
+
+ // Act & assert - push
+ MvcForm theForm = htmlHelper.BeginForm();
+ Assert.NotNull(htmlHelper.ViewContext.FormContext);
+ Assert.NotEqual(defaultFormContext, htmlHelper.ViewContext.FormContext);
+ Assert.Equal("form_id", htmlHelper.ViewContext.FormContext.FormId);
+
+ // Act & assert - pop
+ theForm.Dispose();
+ Assert.Equal(defaultFormContext, htmlHelper.ViewContext.FormContext);
+ Assert.Equal(@"<form action=""/some/path"" id=""form_id"" method=""post""></form><script type=""text/javascript"">
+//<![CDATA[
+if (!window.mvcClientValidationMetadata) { window.mvcClientValidationMetadata = []; }
+window.mvcClientValidationMetadata.push({""Fields"":[],""FormId"":""form_id"",""ReplaceValidationSummary"":false});
+//]]>
+</script>", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginFormWithClientValidationAndUnobtrusiveJavaScriptEnabled()
+ {
+ // Arrange
+ StringWriter writer;
+ HtmlHelper htmlHelper = GetFormHelper(out writer);
+
+ htmlHelper.ViewContext.ClientValidationEnabled = true;
+ htmlHelper.ViewContext.UnobtrusiveJavaScriptEnabled = true;
+
+ // Act & assert - push
+ MvcForm theForm = htmlHelper.BeginForm();
+ Assert.Null(htmlHelper.ViewContext.FormContext.FormId);
+
+ // Act & assert - pop
+ theForm.Dispose();
+ Assert.Equal(@"<form action=""/some/path"" method=""post""></form>", writer.ToString());
+ }
+
+ [Fact]
+ public void BeginFormWithActionControllerInvalidFormMethodHtmlValues()
+ {
+ BeginFormHelper(
+ htmlHelper => htmlHelper.BeginForm("bar", "foo", (FormMethod)2, new RouteValueDictionary(new { baz = "baz" })),
+ @"<form action=""" + MvcHelper.AppPathModifier + @"/foo/bar"" baz=""baz"" method=""post"">");
+ }
+
+ [Fact]
+ public void BeginFormWithActionController()
+ {
+ BeginFormHelper(
+ htmlHelper => htmlHelper.BeginForm("bar", "foo"),
+ @"<form action=""" + MvcHelper.AppPathModifier + @"/foo/bar"" method=""post"">");
+ }
+
+ [Fact]
+ public void BeginFormWithActionControllerFormMethodHtmlDictionary()
+ {
+ BeginFormHelper(
+ htmlHelper => htmlHelper.BeginForm("bar", "foo", FormMethod.Get, new RouteValueDictionary(new { baz = "baz" })),
+ @"<form action=""" + MvcHelper.AppPathModifier + @"/foo/bar"" baz=""baz"" method=""get"">");
+ }
+
+ [Fact]
+ public void BeginFormWithActionControllerFormMethodHtmlValues()
+ {
+ BeginFormHelper(
+ htmlHelper => htmlHelper.BeginForm("bar", "foo", FormMethod.Get, new { baz = "baz" }),
+ @"<form action=""" + MvcHelper.AppPathModifier + @"/foo/bar"" baz=""baz"" method=""get"">");
+ }
+
+ [Fact]
+ public void BeginFormWithActionControllerFormMethodHtmlValuesWithUnderscores()
+ {
+ BeginFormHelper(
+ htmlHelper => htmlHelper.BeginForm("bar", "foo", FormMethod.Get, new { data_test = "value" }),
+ @"<form action=""" + MvcHelper.AppPathModifier + @"/foo/bar"" data-test=""value"" method=""get"">");
+ }
+
+ [Fact]
+ public void BeginFormWithActionControllerRouteDictionaryFormMethodHtmlDictionary()
+ {
+ BeginFormHelper(
+ htmlHelper => htmlHelper.BeginForm("bar", "foo", new RouteValueDictionary(new { id = "id" }), FormMethod.Get, new RouteValueDictionary(new { baz = "baz" })),
+ @"<form action=""" + MvcHelper.AppPathModifier + @"/foo/bar/id"" baz=""baz"" method=""get"">");
+ }
+
+ [Fact]
+ public void BeginFormWithActionControllerRouteValuesFormMethodHtmlValues()
+ {
+ BeginFormHelper(
+ htmlHelper => htmlHelper.BeginForm("bar", "foo", new { id = "id" }, FormMethod.Get, new { baz = "baz" }),
+ @"<form action=""" + MvcHelper.AppPathModifier + @"/foo/bar/id"" baz=""baz"" method=""get"">");
+ }
+
+ [Fact]
+ public void BeginFormWithActionControllerRouteValuesFormMethodHtmlValuesWithUnderscores()
+ {
+ BeginFormHelper(
+ htmlHelper => htmlHelper.BeginForm("bar", "foo", new { id = "id" }, FormMethod.Get, new { foo_baz = "baz" }),
+ @"<form action=""" + MvcHelper.AppPathModifier + @"/foo/bar/id"" foo-baz=""baz"" method=""get"">");
+ }
+
+ [Fact]
+ public void BeginFormWithActionControllerNullRouteValuesFormMethodNullHtmlValues()
+ {
+ BeginFormHelper(
+ htmlHelper => htmlHelper.BeginForm("bar", "foo", null, FormMethod.Get, null),
+ @"<form action=""" + MvcHelper.AppPathModifier + @"/foo/bar"" method=""get"">");
+ }
+
+ [Fact]
+ public void BeginFormWithRouteValues()
+ {
+ BeginFormHelper(
+ htmlHelper => htmlHelper.BeginForm(new { action = "someOtherAction", id = "id" }),
+ @"<form action=""" + MvcHelper.AppPathModifier + @"/home/someOtherAction/id"" method=""post"">");
+ }
+
+ [Fact]
+ public void BeginFormWithRouteDictionary()
+ {
+ BeginFormHelper(
+ htmlHelper => htmlHelper.BeginForm(new RouteValueDictionary { { "action", "someOtherAction" }, { "id", "id" } }),
+ @"<form action=""" + MvcHelper.AppPathModifier + @"/home/someOtherAction/id"" method=""post"">");
+ }
+
+ [Fact]
+ public void BeginFormWithActionControllerRouteValues()
+ {
+ BeginFormHelper(
+ htmlHelper => htmlHelper.BeginForm("myAction", "myController", new { id = "id", pageNum = "123" }),
+ @"<form action=""" + MvcHelper.AppPathModifier + @"/myController/myAction/id?pageNum=123"" method=""post"">");
+ }
+
+ [Fact]
+ public void BeginFormWithActionControllerRouteDictionary()
+ {
+ BeginFormHelper(
+ htmlHelper => htmlHelper.BeginForm("myAction", "myController", new RouteValueDictionary { { "pageNum", "123" }, { "id", "id" } }),
+ @"<form action=""" + MvcHelper.AppPathModifier + @"/myController/myAction/id?pageNum=123"" method=""post"">");
+ }
+
+ [Fact]
+ public void BeginFormWithActionControllerMethod()
+ {
+ BeginFormHelper(
+ htmlHelper => htmlHelper.BeginForm("myAction", "myController", FormMethod.Get),
+ @"<form action=""" + MvcHelper.AppPathModifier + @"/myController/myAction"" method=""get"">");
+ }
+
+ [Fact]
+ public void BeginFormWithActionControllerRouteValuesMethod()
+ {
+ BeginFormHelper(
+ htmlHelper => htmlHelper.BeginForm("myAction", "myController", new { id = "id", pageNum = "123" }, FormMethod.Get),
+ @"<form action=""" + MvcHelper.AppPathModifier + @"/myController/myAction/id?pageNum=123"" method=""get"">");
+ }
+
+ [Fact]
+ public void BeginFormWithActionControllerRouteDictionaryMethod()
+ {
+ BeginFormHelper(
+ htmlHelper => htmlHelper.BeginForm("myAction", "myController", new RouteValueDictionary { { "pageNum", "123" }, { "id", "id" } }, FormMethod.Get),
+ @"<form action=""" + MvcHelper.AppPathModifier + @"/myController/myAction/id?pageNum=123"" method=""get"">");
+ }
+
+ [Fact]
+ public void BeginFormWithNoParams()
+ {
+ BeginFormHelper(
+ htmlHelper => htmlHelper.BeginForm(),
+ @"<form action=""/some/path"" method=""post"">");
+ }
+
+ [Fact]
+ public void BeginRouteFormWithRouteNameInvalidFormMethodHtmlValues()
+ {
+ BeginFormHelper(
+ htmlHelper => htmlHelper.BeginRouteForm("namedroute", (FormMethod)2, new RouteValueDictionary(new { baz = "baz" })),
+ @"<form action=""" + MvcHelper.AppPathModifier + @"/named/home/oldaction"" baz=""baz"" method=""post"">");
+ }
+
+ [Fact]
+ public void BeginRouteFormWithRouteName()
+ {
+ BeginFormHelper(
+ htmlHelper => htmlHelper.BeginRouteForm("namedroute"),
+ @"<form action=""" + MvcHelper.AppPathModifier + @"/named/home/oldaction"" method=""post"">");
+ }
+
+ [Fact]
+ public void BeginRouteFormWithRouteNameFormMethodHtmlDictionary()
+ {
+ BeginFormHelper(
+ htmlHelper => htmlHelper.BeginRouteForm("namedroute", FormMethod.Get, new RouteValueDictionary(new { baz = "baz" })),
+ @"<form action=""" + MvcHelper.AppPathModifier + @"/named/home/oldaction"" baz=""baz"" method=""get"">");
+ }
+
+ [Fact]
+ public void BeginRouteFormWithRouteNameFormMethodHtmlValues()
+ {
+ BeginFormHelper(
+ htmlHelper => htmlHelper.BeginRouteForm("namedroute", FormMethod.Get, new { baz = "baz" }),
+ @"<form action=""" + MvcHelper.AppPathModifier + @"/named/home/oldaction"" baz=""baz"" method=""get"">");
+ }
+
+ [Fact]
+ public void BeginRouteFormWithRouteNameFormMethodHtmlValuesWithUnderscores()
+ {
+ BeginFormHelper(
+ htmlHelper => htmlHelper.BeginRouteForm("namedroute", FormMethod.Get, new { foo_baz = "baz" }),
+ @"<form action=""" + MvcHelper.AppPathModifier + @"/named/home/oldaction"" foo-baz=""baz"" method=""get"">");
+ }
+
+ [Fact]
+ public void BeginRouteFormWithRouteNameRouteDictionaryFormMethodHtmlDictionary()
+ {
+ BeginFormHelper(
+ htmlHelper => htmlHelper.BeginRouteForm("namedroute", new RouteValueDictionary(new { id = "id" }), FormMethod.Get, new RouteValueDictionary(new { baz = "baz" })),
+ @"<form action=""" + MvcHelper.AppPathModifier + @"/named/home/oldaction/id"" baz=""baz"" method=""get"">");
+ }
+
+ [Fact]
+ public void BeginRouteFormWithRouteNameRouteValuesFormMethodHtmlValues()
+ {
+ BeginFormHelper(
+ htmlHelper => htmlHelper.BeginRouteForm("namedroute", new { id = "id" }, FormMethod.Get, new { baz = "baz" }),
+ @"<form action=""" + MvcHelper.AppPathModifier + @"/named/home/oldaction/id"" baz=""baz"" method=""get"">");
+ }
+
+ [Fact]
+ public void BeginRouteFormWithRouteNameRouteValuesFormMethodHtmlValuesWithUnderscores()
+ {
+ BeginFormHelper(
+ htmlHelper => htmlHelper.BeginRouteForm("namedroute", new { id = "id" }, FormMethod.Get, new { foo_baz = "baz" }),
+ @"<form action=""" + MvcHelper.AppPathModifier + @"/named/home/oldaction/id"" foo-baz=""baz"" method=""get"">");
+ }
+
+ [Fact]
+ public void BeginRouteFormWithRouteNameNullRouteValuesFormMethodNullHtmlValues()
+ {
+ BeginFormHelper(
+ htmlHelper => htmlHelper.BeginRouteForm("namedroute", null, FormMethod.Get, null),
+ @"<form action=""" + MvcHelper.AppPathModifier + @"/named/home/oldaction"" method=""get"">");
+ }
+
+ [Fact]
+ public void BeginRouteFormWithRouteValues()
+ {
+ BeginFormHelper(
+ htmlHelper => htmlHelper.BeginRouteForm(new { action = "someOtherAction", id = "id" }),
+ @"<form action=""" + MvcHelper.AppPathModifier + @"/home/someOtherAction/id"" method=""post"">");
+ }
+
+ [Fact]
+ public void BeginRouteFormWithRouteDictionary()
+ {
+ BeginFormHelper(
+ htmlHelper => htmlHelper.BeginRouteForm(new RouteValueDictionary { { "action", "someOtherAction" }, { "id", "id" } }),
+ @"<form action=""" + MvcHelper.AppPathModifier + @"/home/someOtherAction/id"" method=""post"">");
+ }
+
+ [Fact]
+ public void BeginRouteFormWithRouteNameRouteValues()
+ {
+ BeginFormHelper(
+ htmlHelper => htmlHelper.BeginRouteForm("namedroute", new { id = "id", pageNum = "123" }),
+ @"<form action=""" + MvcHelper.AppPathModifier + @"/named/home/oldaction/id?pageNum=123"" method=""post"">");
+ }
+
+ [Fact]
+ public void BeginRouteFormWithActionControllerRouteDictionary()
+ {
+ BeginFormHelper(
+ htmlHelper => htmlHelper.BeginRouteForm("namedroute", new RouteValueDictionary { { "pageNum", "123" }, { "id", "id" } }),
+ @"<form action=""" + MvcHelper.AppPathModifier + @"/named/home/oldaction/id?pageNum=123"" method=""post"">");
+ }
+
+ [Fact]
+ public void BeginRouteFormCanUseNamedRouteWithoutSpecifyingDefaults()
+ {
+ // DevDiv 217072: Non-mvc specific helpers should not give default values for controller and action
+
+ BeginFormHelper(
+ htmlHelper =>
+ {
+ htmlHelper.RouteCollection.MapRoute("MyRouteName", "any/url", new { controller = "Charlie" });
+ return htmlHelper.BeginRouteForm("MyRouteName");
+ }, @"<form action=""" + MvcHelper.AppPathModifier + @"/any/url"" method=""post"">");
+ }
+
+ [Fact]
+ public void BeginRouteFormWithActionControllerMethod()
+ {
+ BeginFormHelper(
+ htmlHelper => htmlHelper.BeginRouteForm("namedroute", FormMethod.Get),
+ @"<form action=""" + MvcHelper.AppPathModifier + @"/named/home/oldaction"" method=""get"">");
+ }
+
+ [Fact]
+ public void BeginRouteFormWithActionControllerRouteValuesMethod()
+ {
+ BeginFormHelper(
+ htmlHelper => htmlHelper.BeginRouteForm("namedroute", new { id = "id", pageNum = "123" }, FormMethod.Get),
+ @"<form action=""" + MvcHelper.AppPathModifier + @"/named/home/oldaction/id?pageNum=123"" method=""get"">");
+ }
+
+ [Fact]
+ public void BeginRouteFormWithActionControllerRouteDictionaryMethod()
+ {
+ BeginFormHelper(
+ htmlHelper => htmlHelper.BeginRouteForm("namedroute", new RouteValueDictionary { { "pageNum", "123" }, { "id", "id" } }, FormMethod.Get),
+ @"<form action=""" + MvcHelper.AppPathModifier + @"/named/home/oldaction/id?pageNum=123"" method=""get"">");
+ }
+
+ [Fact]
+ public void EndFormWritesCloseTag()
+ {
+ // Arrange
+ StringWriter writer;
+ HtmlHelper htmlHelper = GetFormHelper(out writer);
+
+ // Act
+ htmlHelper.EndForm();
+
+ // Assert
+ Assert.Equal("</form>", writer.ToString());
+ }
+
+ private static HtmlHelper GetFormHelper(out StringWriter writer)
+ {
+ Mock<ViewContext> mockViewContext = new Mock<ViewContext>() { CallBase = true };
+ mockViewContext.Setup(c => c.HttpContext.Request.Url).Returns(new Uri("http://www.contoso.com/some/path"));
+ mockViewContext.Setup(c => c.HttpContext.Request.RawUrl).Returns("/some/path");
+ mockViewContext.Setup(c => c.HttpContext.Request.ApplicationPath).Returns("/");
+ mockViewContext.Setup(c => c.HttpContext.Request.Path).Returns("/");
+ mockViewContext.Setup(c => c.HttpContext.Request.ServerVariables).Returns((NameValueCollection)null);
+ mockViewContext.Setup(c => c.HttpContext.Response.Write(It.IsAny<string>())).Throws(new Exception("Should not be called"));
+ mockViewContext.Setup(c => c.HttpContext.Items).Returns(new Hashtable());
+
+ writer = new StringWriter();
+ mockViewContext.Setup(c => c.Writer).Returns(writer);
+
+ mockViewContext.Setup(c => c.HttpContext.Response.ApplyAppPathModifier(It.IsAny<string>())).Returns<string>(r => MvcHelper.AppPathModifier + r);
+
+ RouteCollection rt = new RouteCollection();
+ rt.Add(new Route("{controller}/{action}/{id}", null) { Defaults = new RouteValueDictionary(new { id = "defaultid" }) });
+ rt.Add("namedroute", new Route("named/{controller}/{action}/{id}", null) { Defaults = new RouteValueDictionary(new { id = "defaultid" }) });
+ RouteData rd = new RouteData();
+ rd.Values.Add("controller", "home");
+ rd.Values.Add("action", "oldaction");
+
+ mockViewContext.Setup(c => c.RouteData).Returns(rd);
+ HtmlHelper helper = new HtmlHelper(mockViewContext.Object, new Mock<IViewDataContainer>().Object, rt);
+ helper.ViewContext.FormIdGenerator = () => "form_id";
+ return helper;
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Html/Test/InputExtensionsTest.cs b/test/System.Web.Mvc.Test/Html/Test/InputExtensionsTest.cs
new file mode 100644
index 00000000..664ae26c
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Html/Test/InputExtensionsTest.cs
@@ -0,0 +1,2376 @@
+using System.ComponentModel.DataAnnotations;
+using System.Data.Linq;
+using System.Web.Mvc.Test;
+using System.Web.Routing;
+using Microsoft.Web.UnitTestUtil;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Html.Test
+{
+ public class InputExtensionsTest
+ {
+ // CheckBox
+
+ [Fact]
+ public void CheckBoxDictionaryOverridesImplicitParameters()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetCheckBoxViewData());
+
+ // Act
+ MvcHtmlString html = helper.CheckBox("baz", new { @checked = "checked", value = "false" });
+
+ // Assert
+ Assert.Equal(@"<input checked=""checked"" id=""baz"" name=""baz"" type=""checkbox"" value=""false"" />" +
+ @"<input name=""baz"" type=""hidden"" value=""false"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void CheckBoxExplicitParametersOverrideDictionary()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ MvcHtmlString html = helper.CheckBox("foo", true /* isChecked */, new { @checked = "unchecked", value = "false" });
+
+ // Assert
+ Assert.Equal(@"<input checked=""checked"" id=""foo"" name=""foo"" type=""checkbox"" value=""false"" />" +
+ @"<input name=""foo"" type=""hidden"" value=""false"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void CheckBoxShouldNotCopyAttributesForHidden()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ MvcHtmlString html = helper.CheckBox("foo", true /* isChecked */, new { id = "myID" });
+
+ // Assert
+ Assert.Equal(@"<input checked=""checked"" id=""myID"" name=""foo"" type=""checkbox"" value=""true"" />" +
+ @"<input name=""foo"" type=""hidden"" value=""false"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void CheckBoxWithEmptyNameThrows()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetCheckBoxViewData());
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { helper.CheckBox(String.Empty); },
+ "name");
+ }
+
+ [Fact]
+ public void CheckBoxWithInvalidBooleanThrows()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetCheckBoxViewData());
+
+ // Act & Assert
+ Assert.Throws<FormatException>(
+ delegate { helper.CheckBox("bar"); },
+ "String was not recognized as a valid Boolean.");
+ }
+
+ [Fact]
+ public void CheckBoxCheckedWithOnlyName()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ MvcHtmlString html = helper.CheckBox("foo", true /* isChecked */);
+
+ // Assert
+ Assert.Equal(@"<input checked=""checked"" id=""foo"" name=""foo"" type=""checkbox"" value=""true"" />" +
+ @"<input name=""foo"" type=""hidden"" value=""false"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void CheckBoxShouldRespectModelStateAttemptedValue()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetCheckBoxViewData());
+ helper.ViewData.ModelState.SetModelValue("foo", HtmlHelperTest.GetValueProviderResult("false", "false"));
+
+ // Act
+ MvcHtmlString html = helper.CheckBox("foo");
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""checkbox"" value=""true"" />" +
+ @"<input name=""foo"" type=""hidden"" value=""false"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void CheckBoxWithOnlyName()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetCheckBoxViewData());
+
+ // Act
+ MvcHtmlString html = helper.CheckBox("foo");
+
+ // Assert
+ Assert.Equal(@"<input checked=""checked"" id=""foo"" name=""foo"" type=""checkbox"" value=""true"" />" +
+ @"<input name=""foo"" type=""hidden"" value=""false"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void CheckBoxWithOnlyName_Unobtrusive()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetCheckBoxViewData());
+ helper.ViewContext.ClientValidationEnabled = true;
+ helper.ViewContext.UnobtrusiveJavaScriptEnabled = true;
+ helper.ViewContext.FormContext = new FormContext();
+ helper.ClientValidationRuleFactory = (name, metadata) => new[] { new ModelClientValidationRule { ValidationType = "type", ErrorMessage = "error" } };
+
+ // Act
+ MvcHtmlString html = helper.CheckBox("foo");
+
+ // Assert
+ Assert.Equal(@"<input checked=""checked"" data-val=""true"" data-val-type=""error"" id=""foo"" name=""foo"" type=""checkbox"" value=""true"" />" +
+ @"<input name=""foo"" type=""hidden"" value=""false"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void CheckBoxWithNameAndObjectAttribute()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetCheckBoxViewData());
+
+ // Act
+ MvcHtmlString html = helper.CheckBox("foo", _attributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazObjValue"" checked=""checked"" id=""foo"" name=""foo"" type=""checkbox"" value=""true"" />" +
+ @"<input name=""foo"" type=""hidden"" value=""false"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void CheckBoxWithNameAndObjectAttributeWithUnderscores()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetCheckBoxViewData());
+
+ // Act
+ MvcHtmlString html = helper.CheckBox("foo", _attributesObjectUnderscoresDictionary);
+
+ // Assert
+ Assert.Equal(@"<input checked=""checked"" foo-baz=""BazObjValue"" id=""foo"" name=""foo"" type=""checkbox"" value=""true"" />" +
+ @"<input name=""foo"" type=""hidden"" value=""false"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void CheckBoxWithObjectAttribute()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ MvcHtmlString html = helper.CheckBox("foo", false /* isChecked */, _attributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazObjValue"" id=""foo"" name=""foo"" type=""checkbox"" value=""true"" />" +
+ @"<input name=""foo"" type=""hidden"" value=""false"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void CheckBoxWithObjectAttributeWithUnderscores()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ MvcHtmlString html = helper.CheckBox("foo", false /* isChecked */, _attributesObjectUnderscoresDictionary);
+
+ // Assert
+ Assert.Equal(@"<input foo-baz=""BazObjValue"" id=""foo"" name=""foo"" type=""checkbox"" value=""true"" />" +
+ @"<input name=""foo"" type=""hidden"" value=""false"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void CheckBoxWithAttributeDictionary()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ MvcHtmlString html = helper.CheckBox("foo", false /* isChecked */, _attributesDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazValue"" id=""foo"" name=""foo"" type=""checkbox"" value=""true"" />" +
+ @"<input name=""foo"" type=""hidden"" value=""false"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void CheckBoxWithPrefix()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper();
+ helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix";
+
+ // Act
+ MvcHtmlString html = helper.CheckBox("foo", false /* isChecked */, _attributesDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazValue"" id=""MyPrefix_foo"" name=""MyPrefix.foo"" type=""checkbox"" value=""true"" />" +
+ @"<input name=""MyPrefix.foo"" type=""hidden"" value=""false"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void CheckBoxWithPrefixAndEmptyName()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper();
+ helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix";
+
+ // Act
+ MvcHtmlString html = helper.CheckBox("", false /* isChecked */, _attributesDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazValue"" id=""MyPrefix"" name=""MyPrefix"" type=""checkbox"" value=""true"" />" +
+ @"<input name=""MyPrefix"" type=""hidden"" value=""false"" />",
+ html.ToHtmlString());
+ }
+
+ // CheckBoxFor
+
+ [Fact]
+ public void CheckBoxForWitNullExpressionThrows()
+ {
+ // Arrange
+ HtmlHelper<FooBarBazModel> helper = MvcHelper.GetHtmlHelper(GetCheckBoxViewData());
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => helper.CheckBoxFor(null),
+ "expression");
+ }
+
+ [Fact]
+ public void CheckBoxForWithInvalidBooleanThrows()
+ {
+ // Arrange
+ HtmlHelper<FooBarBazModel> helper = MvcHelper.GetHtmlHelper(GetCheckBoxViewData());
+
+ // Act & Assert
+ Assert.Throws<FormatException>(
+ () => helper.CheckBoxFor(m => m.bar), // "bar" in ViewData isn't a valid boolean
+ "String was not recognized as a valid Boolean.");
+ }
+
+ [Fact]
+ public void CheckBoxForDictionaryOverridesImplicitParameters()
+ {
+ // Arrange
+ HtmlHelper<FooBarBazModel> helper = MvcHelper.GetHtmlHelper(GetCheckBoxViewData());
+
+ // Act
+ MvcHtmlString html = helper.CheckBoxFor(m => m.baz, new { @checked = "checked", value = "false" });
+
+ // Assert
+ Assert.Equal(@"<input checked=""checked"" id=""baz"" name=""baz"" type=""checkbox"" value=""false"" />" +
+ @"<input name=""baz"" type=""hidden"" value=""false"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void CheckBoxForShouldNotCopyAttributesForHidden()
+ {
+ // Arrange
+ HtmlHelper<FooBarBazModel> helper = MvcHelper.GetHtmlHelper(GetCheckBoxViewData());
+
+ // Act
+ MvcHtmlString html = helper.CheckBoxFor(m => m.foo, new { id = "myID" });
+
+ // Assert
+ Assert.Equal(@"<input checked=""checked"" id=""myID"" name=""foo"" type=""checkbox"" value=""true"" />" +
+ @"<input name=""foo"" type=""hidden"" value=""false"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void CheckBoxForCheckedWithOnlyName()
+ {
+ // Arrange
+ HtmlHelper<FooBarBazModel> helper = MvcHelper.GetHtmlHelper(GetCheckBoxViewData());
+
+ // Act
+ MvcHtmlString html = helper.CheckBoxFor(m => m.foo);
+
+ // Assert
+ Assert.Equal(@"<input checked=""checked"" id=""foo"" name=""foo"" type=""checkbox"" value=""true"" />" +
+ @"<input name=""foo"" type=""hidden"" value=""false"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void CheckBoxForCheckedWithOnlyName_Unobtrusive()
+ {
+ // Arrange
+ HtmlHelper<FooBarBazModel> helper = MvcHelper.GetHtmlHelper(GetCheckBoxViewData());
+ helper.ViewContext.ClientValidationEnabled = true;
+ helper.ViewContext.UnobtrusiveJavaScriptEnabled = true;
+ helper.ViewContext.FormContext = new FormContext();
+ helper.ClientValidationRuleFactory = (name, metadata) => new[] { new ModelClientValidationRule { ValidationType = "type", ErrorMessage = "error" } };
+
+ // Act
+ MvcHtmlString html = helper.CheckBoxFor(m => m.foo);
+
+ // Assert
+ Assert.Equal(@"<input checked=""checked"" data-val=""true"" data-val-type=""error"" id=""foo"" name=""foo"" type=""checkbox"" value=""true"" />" +
+ @"<input name=""foo"" type=""hidden"" value=""false"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void CheckBoxForShouldRespectModelStateAttemptedValue()
+ {
+ // Arrange
+ HtmlHelper<FooBarBazModel> helper = MvcHelper.GetHtmlHelper(GetCheckBoxViewData());
+ helper.ViewContext.ViewData.ModelState.SetModelValue("foo", HtmlHelperTest.GetValueProviderResult("false", "false"));
+
+ // Act
+ MvcHtmlString html = helper.CheckBoxFor(m => m.foo);
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""checkbox"" value=""true"" />" +
+ @"<input name=""foo"" type=""hidden"" value=""false"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void CheckBoxForWithObjectAttribute()
+ {
+ // Arrange
+ HtmlHelper<FooBarBazModel> helper = MvcHelper.GetHtmlHelper(GetCheckBoxViewData());
+
+ // Act
+ MvcHtmlString html = helper.CheckBoxFor(m => m.foo, _attributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazObjValue"" checked=""checked"" id=""foo"" name=""foo"" type=""checkbox"" value=""true"" />" +
+ @"<input name=""foo"" type=""hidden"" value=""false"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void CheckBoxForWithObjectAttributeWithUnderscores()
+ {
+ // Arrange
+ HtmlHelper<FooBarBazModel> helper = MvcHelper.GetHtmlHelper(GetCheckBoxViewData());
+
+ // Act
+ MvcHtmlString html = helper.CheckBoxFor(m => m.foo, _attributesObjectUnderscoresDictionary);
+
+ // Assert
+ Assert.Equal(@"<input checked=""checked"" foo-baz=""BazObjValue"" id=""foo"" name=""foo"" type=""checkbox"" value=""true"" />" +
+ @"<input name=""foo"" type=""hidden"" value=""false"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void CheckBoxForWithAttributeDictionary()
+ {
+ // Arrange
+ HtmlHelper<FooBarBazModel> helper = MvcHelper.GetHtmlHelper(GetCheckBoxViewData());
+
+ // Act
+ MvcHtmlString html = helper.CheckBoxFor(m => m.foo, _attributesDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazValue"" checked=""checked"" id=""foo"" name=""foo"" type=""checkbox"" value=""true"" />" +
+ @"<input name=""foo"" type=""hidden"" value=""false"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void CheckBoxForWithPrefix()
+ {
+ // Arrange
+ HtmlHelper<FooBarBazModel> helper = MvcHelper.GetHtmlHelper(GetCheckBoxViewData());
+ helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix";
+
+ // Act
+ MvcHtmlString html = helper.CheckBoxFor(m => m.foo, _attributesDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazValue"" id=""MyPrefix_foo"" name=""MyPrefix.foo"" type=""checkbox"" value=""true"" />" +
+ @"<input name=""MyPrefix.foo"" type=""hidden"" value=""false"" />",
+ html.ToHtmlString());
+ }
+
+ // Culture tests
+
+ [Fact]
+ public void InputHelpersUseCurrentCultureToConvertValueParameter()
+ {
+ // Arrange
+ DateTime dt = new DateTime(1900, 1, 1, 0, 0, 0);
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary { { "foo", dt } });
+
+ var tests = new[]
+ {
+ // Hidden(name)
+ new
+ {
+ Html = @"<input id=""foo"" name=""foo"" type=""hidden"" value=""1900/01/01 12:00:00 AM"" />",
+ Action = new Func<MvcHtmlString>(() => helper.Hidden("foo"))
+ },
+ // Hidden(name, value)
+ new
+ {
+ Html = @"<input id=""foo"" name=""foo"" type=""hidden"" value=""1900/01/01 12:00:00 AM"" />",
+ Action = new Func<MvcHtmlString>(() => helper.Hidden("foo", dt))
+ },
+ // Hidden(name, value, htmlAttributes)
+ new
+ {
+ Html = @"<input id=""foo"" name=""foo"" type=""hidden"" value=""1900/01/01 12:00:00 AM"" />",
+ Action = new Func<MvcHtmlString>(() => helper.Hidden("foo", dt, null))
+ },
+ // Hidden(name, value, htmlAttributes)
+ new
+ {
+ Html = @"<input id=""foo"" name=""foo"" type=""hidden"" value=""1900/01/01 12:00:00 AM"" />",
+ Action = new Func<MvcHtmlString>(() => helper.Hidden("foo", dt, new RouteValueDictionary()))
+ },
+ // RadioButton(name, value)
+ new
+ {
+ Html = @"<input checked=""checked"" id=""foo"" name=""foo"" type=""radio"" value=""1900/01/01 12:00:00 AM"" />",
+ Action = new Func<MvcHtmlString>(() => helper.RadioButton("foo", dt))
+ },
+ // RadioButton(name, value, isChecked)
+ new
+ {
+ Html = @"<input id=""foo"" name=""foo"" type=""radio"" value=""1900/01/01 12:00:00 AM"" />",
+ Action = new Func<MvcHtmlString>(() => helper.RadioButton("foo", dt, false))
+ },
+ // RadioButton(name, value, htmlAttributes)
+ new
+ {
+ Html = @"<input checked=""checked"" id=""foo"" name=""foo"" type=""radio"" value=""1900/01/01 12:00:00 AM"" />",
+ Action = new Func<MvcHtmlString>(() => helper.RadioButton("foo", dt, null))
+ },
+ // RadioButton(name, value)
+ new
+ {
+ Html = @"<input checked=""checked"" id=""foo"" name=""foo"" type=""radio"" value=""1900/01/01 12:00:00 AM"" />",
+ Action = new Func<MvcHtmlString>(() => helper.RadioButton("foo", dt, new RouteValueDictionary()))
+ },
+ // RadioButton(name, value, isChecked, htmlAttributes)
+ new
+ {
+ Html = @"<input id=""foo"" name=""foo"" type=""radio"" value=""1900/01/01 12:00:00 AM"" />",
+ Action = new Func<MvcHtmlString>(() => helper.RadioButton("foo", dt, false, null))
+ },
+ // RadioButton(name, value, isChecked, htmlAttributes)
+ new
+ {
+ Html = @"<input id=""foo"" name=""foo"" type=""radio"" value=""1900/01/01 12:00:00 AM"" />",
+ Action = new Func<MvcHtmlString>(() => helper.RadioButton("foo", dt, false, new RouteValueDictionary()))
+ },
+ // TextBox(name)
+ new
+ {
+ Html = @"<input id=""foo"" name=""foo"" type=""text"" value=""1900/01/01 12:00:00 AM"" />",
+ Action = new Func<MvcHtmlString>(() => helper.TextBox("foo"))
+ },
+ // TextBox(name, value)
+ new
+ {
+ Html = @"<input id=""foo"" name=""foo"" type=""text"" value=""1900/01/01 12:00:00 AM"" />",
+ Action = new Func<MvcHtmlString>(() => helper.TextBox("foo", dt))
+ },
+ // TextBox(name, value, hmtlAttributes)
+ new
+ {
+ Html = @"<input id=""foo"" name=""foo"" type=""text"" value=""1900/01/01 12:00:00 AM"" />",
+ Action = new Func<MvcHtmlString>(() => helper.TextBox("foo", dt, (object)null))
+ },
+ // TextBox(name, value, hmtlAttributes)
+ new
+ {
+ Html = @"<input id=""foo"" name=""foo"" type=""text"" value=""1900/01/01 12:00:00 AM"" />",
+ Action = new Func<MvcHtmlString>(() => helper.TextBox("foo", dt, new RouteValueDictionary()))
+ }
+ };
+
+ // Act && Assert
+ using (HtmlHelperTest.ReplaceCulture("en-ZA", "en-US"))
+ {
+ foreach (var test in tests)
+ {
+ Assert.Equal(test.Html, test.Action().ToHtmlString());
+ }
+ }
+ }
+
+ // Hidden
+
+ [Fact]
+ public void HiddenWithByteArrayValueRendersBase64EncodedValue()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ MvcHtmlString result = htmlHelper.Hidden("ProductName", ByteArrayModelBinderTest.Base64TestBytes);
+
+ // Assert
+ Assert.Equal("<input id=\"ProductName\" name=\"ProductName\" type=\"hidden\" value=\"Fys1\" />", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void HiddenWithBinaryArrayValueRendersBase64EncodedValue()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ MvcHtmlString result = htmlHelper.Hidden("ProductName", new Binary(new byte[] { 23, 43, 53 }));
+
+ // Assert
+ Assert.Equal("<input id=\"ProductName\" name=\"ProductName\" type=\"hidden\" value=\"Fys1\" />", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void HiddenWithEmptyNameThrows()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetHiddenViewData());
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { helper.Hidden(String.Empty); },
+ "name");
+ }
+
+ [Fact]
+ public void HiddenWithExplicitValue()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetHiddenViewData());
+
+ // Act
+ MvcHtmlString html = helper.Hidden("foo", "DefaultFoo", null);
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""hidden"" value=""DefaultFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void HiddenWithExplicitValueAndAttributesDictionary()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetHiddenViewData());
+
+ // Act
+ MvcHtmlString html = helper.Hidden("foo", "DefaultFoo", _attributesDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazValue"" id=""foo"" name=""foo"" type=""hidden"" value=""DefaultFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void HiddenWithExplicitValueAndAttributesObject()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetHiddenViewData());
+
+ // Act
+ MvcHtmlString html = helper.Hidden("foo", "DefaultFoo", _attributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazObjValue"" id=""foo"" name=""foo"" type=""hidden"" value=""DefaultFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void HiddenWithExplicitValueAndAttributesObjectWithUnderscores()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetHiddenViewData());
+
+ // Act
+ MvcHtmlString html = helper.Hidden("foo", "DefaultFoo", _attributesObjectUnderscoresDictionary);
+
+ // Assert
+ Assert.Equal(@"<input foo-baz=""BazObjValue"" id=""foo"" name=""foo"" type=""hidden"" value=""DefaultFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void HiddenWithExplicitValueNull()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetHiddenViewData());
+
+ // Act
+ MvcHtmlString html = helper.Hidden("foo", (string)null /* value */, (object)null /* htmlAttributes */);
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""hidden"" value=""ViewDataFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void HiddenWithImplicitValue()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetHiddenViewData());
+
+ // Act
+ MvcHtmlString html = helper.Hidden("foo");
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""hidden"" value=""ViewDataFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void HiddenWithImplicitValueAndAttributesDictionary()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetHiddenViewData());
+
+ // Act
+ MvcHtmlString html = helper.Hidden("foo", null, _attributesDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazValue"" id=""foo"" name=""foo"" type=""hidden"" value=""ViewDataFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void HiddenWithImplicitValueAndAttributesDictionaryReturnsEmptyValueIfNotFound()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetHiddenViewData());
+
+ // Act
+ MvcHtmlString html = helper.Hidden("keyNotFound", null, _attributesDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazValue"" id=""keyNotFound"" name=""keyNotFound"" type=""hidden"" value="""" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void HiddenWithImplicitValueAndAttributesObject()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetHiddenViewData());
+
+ // Act
+ MvcHtmlString html = helper.Hidden("foo", null, _attributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazObjValue"" id=""foo"" name=""foo"" type=""hidden"" value=""ViewDataFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void HiddenWithNameAndValue()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetHiddenViewData());
+
+ // Act
+ MvcHtmlString html = helper.Hidden("foo", "fooValue");
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""hidden"" value=""fooValue"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void HiddenWithPrefix()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetHiddenViewData());
+ helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix";
+
+ // Act
+ MvcHtmlString html = helper.Hidden("foo", "fooValue");
+
+ // Assert
+ Assert.Equal(@"<input id=""MyPrefix_foo"" name=""MyPrefix.foo"" type=""hidden"" value=""fooValue"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void HiddenWithPrefixAndEmptyName()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetHiddenViewData());
+ helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix";
+
+ // Act
+ MvcHtmlString html = helper.Hidden("", "fooValue");
+
+ // Assert
+ Assert.Equal(@"<input id=""MyPrefix"" name=""MyPrefix"" type=""hidden"" value=""fooValue"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void HiddenWithNullNameThrows()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetHiddenViewData());
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { helper.Hidden(null /* name */); },
+ "name");
+ }
+
+ [Fact]
+ public void HiddenWithViewDataErrors()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetHiddenViewDataWithErrors());
+
+ // Act
+ MvcHtmlString html = helper.Hidden("foo", null, _attributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazObjValue"" class=""input-validation-error"" id=""foo"" name=""foo"" type=""hidden"" value=""AttemptedValueFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void HiddenWithViewDataErrorsAndCustomClass()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetHiddenViewDataWithErrors());
+
+ // Act
+ MvcHtmlString html = helper.Hidden("foo", null, new { @class = "foo-class" });
+
+ // Assert
+ Assert.Equal(@"<input class=""input-validation-error foo-class"" id=""foo"" name=""foo"" type=""hidden"" value=""AttemptedValueFoo"" />", html.ToHtmlString());
+ }
+
+ // HiddenFor
+
+ [Fact]
+ public void HiddenForWithNullExpressionThrows()
+ {
+ // Arrange
+ HtmlHelper<HiddenModel> helper = MvcHelper.GetHtmlHelper(GetHiddenViewData());
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => helper.HiddenFor<HiddenModel, object>(null),
+ "expression"
+ );
+ }
+
+ [Fact]
+ public void HiddenForWithStringValue()
+ {
+ // Arrange
+ HtmlHelper<HiddenModel> helper = MvcHelper.GetHtmlHelper(GetHiddenViewData());
+ helper.ViewData.Model.foo = "DefaultFoo";
+
+ // Act
+ MvcHtmlString html = helper.HiddenFor(m => m.foo);
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""hidden"" value=""DefaultFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void HiddenForWithByteArrayValueRendersBase64EncodedValue()
+ {
+ // Arrange
+ HtmlHelper<HiddenModel> helper = MvcHelper.GetHtmlHelper(GetHiddenViewData());
+ helper.ViewData.Model.bytes = ByteArrayModelBinderTest.Base64TestBytes;
+
+ // Act
+ MvcHtmlString result = helper.HiddenFor(m => m.bytes);
+
+ // Assert
+ Assert.Equal("<input id=\"bytes\" name=\"bytes\" type=\"hidden\" value=\"Fys1\" />", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void HiddenForWithBinaryValueRendersBase64EncodedValue()
+ {
+ // Arrange
+ HtmlHelper<HiddenModel> helper = MvcHelper.GetHtmlHelper(GetHiddenViewData());
+ helper.ViewData.Model.binary = new Binary(new byte[] { 23, 43, 53 });
+
+ // Act
+ MvcHtmlString result = helper.HiddenFor(m => m.binary);
+
+ // Assert
+ Assert.Equal("<input id=\"binary\" name=\"binary\" type=\"hidden\" value=\"Fys1\" />", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void HiddenForWithAttributesDictionary()
+ {
+ // Arrange
+ HtmlHelper<HiddenModel> helper = MvcHelper.GetHtmlHelper(GetHiddenViewData());
+ helper.ViewData.Model.foo = "DefaultFoo";
+
+ // Act
+ MvcHtmlString html = helper.HiddenFor(m => m.foo, _attributesDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazValue"" id=""foo"" name=""foo"" type=""hidden"" value=""DefaultFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void HiddenForWithAttributesObject()
+ {
+ // Arrange
+ HtmlHelper<HiddenModel> helper = MvcHelper.GetHtmlHelper(GetHiddenViewData());
+ helper.ViewData.Model.foo = "DefaultFoo";
+
+ // Act
+ MvcHtmlString html = helper.HiddenFor(m => m.foo, _attributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazObjValue"" id=""foo"" name=""foo"" type=""hidden"" value=""DefaultFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void HiddenForWithAttributesObjectWithUnderscores()
+ {
+ // Arrange
+ HtmlHelper<HiddenModel> helper = MvcHelper.GetHtmlHelper(GetHiddenViewData());
+ helper.ViewData.Model.foo = "DefaultFoo";
+
+ // Act
+ MvcHtmlString html = helper.HiddenFor(m => m.foo, _attributesObjectUnderscoresDictionary);
+
+ // Assert
+ Assert.Equal(@"<input foo-baz=""BazObjValue"" id=""foo"" name=""foo"" type=""hidden"" value=""DefaultFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void HiddenForWithPrefix()
+ {
+ // Arrange
+ HtmlHelper<HiddenModel> helper = MvcHelper.GetHtmlHelper(GetHiddenViewData());
+ helper.ViewData.Model.foo = "fooValue";
+ helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix";
+
+ // Act
+ MvcHtmlString html = helper.HiddenFor(m => m.foo);
+
+ // Assert
+ Assert.Equal(@"<input id=""MyPrefix_foo"" name=""MyPrefix.foo"" type=""hidden"" value=""fooValue"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void HiddenForWithPrefixAndEmptyName()
+ {
+ // Arrange
+ HtmlHelper<HiddenModel> helper = MvcHelper.GetHtmlHelper(GetHiddenViewData());
+ helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix";
+
+ // Act
+ MvcHtmlString html = helper.HiddenFor(m => m);
+
+ // Assert
+ Assert.Equal(@"<input id=""MyPrefix"" name=""MyPrefix"" type=""hidden"" value=""{ foo = (null) }"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void HiddenForWithViewDataErrors()
+ {
+ // Arrange
+ HtmlHelper<HiddenModel> helper = MvcHelper.GetHtmlHelper(GetHiddenViewDataWithErrors());
+
+ // Act
+ MvcHtmlString html = helper.HiddenFor(m => m.foo, _attributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazObjValue"" class=""input-validation-error"" id=""foo"" name=""foo"" type=""hidden"" value=""AttemptedValueFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void HiddenForWithViewDataErrorsAndCustomClass()
+ {
+ // Arrange
+ HtmlHelper<HiddenModel> helper = MvcHelper.GetHtmlHelper(GetHiddenViewDataWithErrors());
+
+ // Act
+ MvcHtmlString html = helper.HiddenFor(m => m.foo, new { @class = "foo-class" });
+
+ // Assert
+ Assert.Equal(@"<input class=""input-validation-error foo-class"" id=""foo"" name=""foo"" type=""hidden"" value=""AttemptedValueFoo"" />", html.ToHtmlString());
+ }
+
+ // Password
+
+ [Fact]
+ public void PasswordWithEmptyNameThrows()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetPasswordViewData());
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { helper.Password(String.Empty); },
+ "name");
+ }
+
+ [Fact]
+ public void PasswordDictionaryOverridesImplicitParameters()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetPasswordViewData());
+
+ // Act
+ MvcHtmlString html = helper.Password("foo", "Some Value", new { type = "fooType" });
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""fooType"" value=""Some Value"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void PasswordExplicitParametersOverrideDictionary()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetPasswordViewData());
+
+ // Act
+ MvcHtmlString html = helper.Password("foo", "Some Value", new { value = "Another Value", name = "bar" });
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""password"" value=""Some Value"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void PasswordWithExplicitValue()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetPasswordViewData());
+
+ // Act
+ MvcHtmlString html = helper.Password("foo", "DefaultFoo", (object)null);
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""password"" value=""DefaultFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void PasswordWithExplicitValue_Unobtrusive()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetPasswordViewData());
+ helper.ViewContext.ClientValidationEnabled = true;
+ helper.ViewContext.UnobtrusiveJavaScriptEnabled = true;
+ helper.ViewContext.FormContext = new FormContext();
+ helper.ClientValidationRuleFactory = (name, metadata) => new[] { new ModelClientValidationRule { ValidationType = "type", ErrorMessage = "error" } };
+
+ // Act
+ MvcHtmlString html = helper.Password("foo", "DefaultFoo", (object)null);
+
+ // Assert
+ Assert.Equal(@"<input data-val=""true"" data-val-type=""error"" id=""foo"" name=""foo"" type=""password"" value=""DefaultFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void PasswordWithExplicitValueAndAttributesDictionary()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetPasswordViewData());
+
+ // Act
+ MvcHtmlString html = helper.Password("foo", "DefaultFoo", _attributesDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazValue"" id=""foo"" name=""foo"" type=""password"" value=""DefaultFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void PasswordWithExplicitValueAndAttributesObject()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetPasswordViewData());
+
+ // Act
+ MvcHtmlString html = helper.Password("foo", "DefaultFoo", _attributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazObjValue"" id=""foo"" name=""foo"" type=""password"" value=""DefaultFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void PasswordWithExplicitValueNull()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetPasswordViewData());
+
+ // Act
+ MvcHtmlString html = helper.Password("foo", (string)null /* value */, (object)null);
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""password"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void PasswordWithImplicitValue()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetPasswordViewData());
+
+ // Act
+ MvcHtmlString html = helper.Password("foo");
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""password"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void PasswordWithImplicitValueAndAttributesDictionary()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetPasswordViewData());
+
+ // Act
+ MvcHtmlString html = helper.Password("foo", null, _attributesDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazValue"" id=""foo"" name=""foo"" type=""password"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void PasswordWithImplicitValueAndAttributesDictionaryReturnsEmptyValueIfNotFound()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetPasswordViewData());
+
+ // Act
+ MvcHtmlString html = helper.Password("keyNotFound", null, _attributesDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazValue"" id=""keyNotFound"" name=""keyNotFound"" type=""password"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void PasswordWithImplicitValueAndAttributesObject()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetPasswordViewData());
+
+ // Act
+ MvcHtmlString html = helper.Password("foo", null, _attributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazObjValue"" id=""foo"" name=""foo"" type=""password"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void PasswordWithImplicitValueAndAttributesObjectWithUnderscores()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetPasswordViewData());
+
+ // Act
+ MvcHtmlString html = helper.Password("foo", null, _attributesObjectUnderscoresDictionary);
+
+ // Assert
+ Assert.Equal(@"<input foo-baz=""BazObjValue"" id=""foo"" name=""foo"" type=""password"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void PasswordWithNameAndValue()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetHiddenViewData());
+
+ // Act
+ MvcHtmlString html = helper.Password("foo", "fooValue");
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""password"" value=""fooValue"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void PasswordWithPrefix()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetHiddenViewData());
+ helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix";
+
+ // Act
+ MvcHtmlString html = helper.Password("foo", "fooValue");
+
+ // Assert
+ Assert.Equal(@"<input id=""MyPrefix_foo"" name=""MyPrefix.foo"" type=""password"" value=""fooValue"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void PasswordWithPrefixAndEmptyName()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetHiddenViewData());
+ helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix";
+
+ // Act
+ MvcHtmlString html = helper.Password("", "fooValue");
+
+ // Assert
+ Assert.Equal(@"<input id=""MyPrefix"" name=""MyPrefix"" type=""password"" value=""fooValue"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void PasswordWithNullNameThrows()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetPasswordViewData());
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { helper.Password(null /* name */); },
+ "name");
+ }
+
+ [Fact]
+ public void PasswordWithViewDataErrors()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetPasswordViewDataWithErrors());
+
+ // Act
+ MvcHtmlString html = helper.Password("foo", null, _attributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazObjValue"" class=""input-validation-error"" id=""foo"" name=""foo"" type=""password"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void PasswordWithViewDataErrorsAndCustomClass()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetPasswordViewDataWithErrors());
+
+ // Act
+ MvcHtmlString html = helper.Password("foo", null, new { @class = "foo-class" });
+
+ // Assert
+ Assert.Equal(@"<input class=""input-validation-error foo-class"" id=""foo"" name=""foo"" type=""password"" />", html.ToHtmlString());
+ }
+
+ // PasswordFor
+
+ [Fact]
+ public void PasswordForWithNullExpressionThrows()
+ {
+ // Arrange
+ HtmlHelper<FooModel> helper = MvcHelper.GetHtmlHelper(GetPasswordViewData());
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => helper.PasswordFor<FooModel, object>(null),
+ "expression");
+ }
+
+ [Fact]
+ public void PasswordForDictionaryOverridesImplicitParameters()
+ {
+ // Arrange
+ HtmlHelper<FooModel> helper = MvcHelper.GetHtmlHelper(GetPasswordViewData());
+
+ // Act
+ MvcHtmlString html = helper.PasswordFor(m => m.foo, new { type = "fooType" });
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""fooType"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void PasswordForExpressionNameOverridesDictionary()
+ {
+ // Arrange
+ HtmlHelper<FooModel> helper = MvcHelper.GetHtmlHelper(GetPasswordViewData());
+
+ // Act
+ MvcHtmlString html = helper.PasswordFor(m => m.foo, new { name = "bar" });
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""password"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void PasswordForWithImplicitValue()
+ {
+ // Arrange
+ HtmlHelper<FooModel> helper = MvcHelper.GetHtmlHelper(GetPasswordViewData());
+
+ // Act
+ MvcHtmlString html = helper.PasswordFor(m => m.foo);
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""password"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void PasswordForWithImplicitValue_Unobtrusive()
+ {
+ // Arrange
+ HtmlHelper<FooModel> helper = MvcHelper.GetHtmlHelper(GetPasswordViewData());
+ helper.ViewContext.ClientValidationEnabled = true;
+ helper.ViewContext.UnobtrusiveJavaScriptEnabled = true;
+ helper.ViewContext.FormContext = new FormContext();
+ helper.ClientValidationRuleFactory = (name, metadata) => new[] { new ModelClientValidationRule { ValidationType = "type", ErrorMessage = "error" } };
+
+ // Act
+ MvcHtmlString html = helper.PasswordFor(m => m.foo);
+
+ // Assert
+ Assert.Equal(@"<input data-val=""true"" data-val-type=""error"" id=""foo"" name=""foo"" type=""password"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void PasswordForWithDeepValueWithNullModel_Unobtrusive()
+ { // Dev10 Bug #936192
+ // Arrange
+ HtmlHelper<DeepContainerModel> helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary<DeepContainerModel>());
+ helper.ViewContext.ClientValidationEnabled = true;
+ helper.ViewContext.UnobtrusiveJavaScriptEnabled = true;
+ helper.ViewContext.FormContext = new FormContext();
+
+ // Act
+ MvcHtmlString html = helper.PasswordFor(m => m.contained.foo);
+
+ // Assert
+ Assert.Equal(@"<input data-val=""true"" data-val-required=""The foo field is required."" id=""contained_foo"" name=""contained.foo"" type=""password"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void PasswordForWithAttributesDictionary()
+ {
+ // Arrange
+ HtmlHelper<FooModel> helper = MvcHelper.GetHtmlHelper(GetPasswordViewData());
+
+ // Act
+ MvcHtmlString html = helper.PasswordFor(m => m.foo, _attributesDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazValue"" id=""foo"" name=""foo"" type=""password"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void PasswordForWithAttributesObject()
+ {
+ // Arrange
+ HtmlHelper<FooModel> helper = MvcHelper.GetHtmlHelper(GetPasswordViewData());
+
+ // Act
+ MvcHtmlString html = helper.PasswordFor(m => m.foo, _attributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazObjValue"" id=""foo"" name=""foo"" type=""password"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void PasswordForWithAttributesObjectWithUnderscores()
+ {
+ // Arrange
+ HtmlHelper<FooModel> helper = MvcHelper.GetHtmlHelper(GetPasswordViewData());
+
+ // Act
+ MvcHtmlString html = helper.PasswordFor(m => m.foo, _attributesObjectUnderscoresDictionary);
+
+ // Assert
+ Assert.Equal(@"<input foo-baz=""BazObjValue"" id=""foo"" name=""foo"" type=""password"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void PasswordForWithPrefix()
+ {
+ // Arrange
+ HtmlHelper<FooModel> helper = MvcHelper.GetHtmlHelper(GetPasswordViewData());
+ helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix";
+
+ // Act
+ MvcHtmlString html = helper.PasswordFor(m => m.foo);
+
+ // Assert
+ Assert.Equal(@"<input id=""MyPrefix_foo"" name=""MyPrefix.foo"" type=""password"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void PasswordForWithViewDataErrors()
+ {
+ // Arrange
+ HtmlHelper<FooModel> helper = MvcHelper.GetHtmlHelper(GetPasswordViewDataWithErrors());
+
+ // Act
+ MvcHtmlString html = helper.PasswordFor(m => m.foo, _attributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazObjValue"" class=""input-validation-error"" id=""foo"" name=""foo"" type=""password"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void PasswordForWithViewDataErrorsAndCustomClass()
+ {
+ // Arrange
+ HtmlHelper<FooModel> helper = MvcHelper.GetHtmlHelper(GetPasswordViewDataWithErrors());
+
+ // Act
+ MvcHtmlString html = helper.PasswordFor(m => m.foo, new { @class = "foo-class" });
+
+ // Assert
+ Assert.Equal(@"<input class=""input-validation-error foo-class"" id=""foo"" name=""foo"" type=""password"" />", html.ToHtmlString());
+ }
+
+ // RadioButton
+
+ [Fact]
+ public void RadioButtonDictionaryOverridesImplicitParameters()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetRadioButtonViewData());
+
+ // Act
+ MvcHtmlString html = helper.RadioButton("bar", "ViewDataBar", new { @checked = "chucked", value = "baz" });
+
+ // Assert
+ Assert.Equal(@"<input checked=""chucked"" id=""bar"" name=""bar"" type=""radio"" value=""ViewDataBar"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonExplicitParametersOverrideDictionary()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetRadioButtonViewData());
+
+ // Act
+ MvcHtmlString html = helper.RadioButton("bar", "ViewDataBar", false, new { @checked = "checked", value = "baz" });
+
+ // Assert
+ Assert.Equal(@"<input id=""bar"" name=""bar"" type=""radio"" value=""ViewDataBar"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonShouldRespectModelStateAttemptedValue()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetRadioButtonViewData());
+ helper.ViewData.ModelState.SetModelValue("foo", HtmlHelperTest.GetValueProviderResult("ModelStateFoo", "ModelStateFoo"));
+
+ // Act
+ MvcHtmlString html = helper.RadioButton("foo", "ModelStateFoo", false, new { @checked = "checked", value = "baz" });
+
+ // Assert
+ Assert.Equal(@"<input checked=""checked"" id=""foo"" name=""foo"" type=""radio"" value=""ModelStateFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonValueParameterAlwaysRendered()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetRadioButtonViewData());
+
+ // Act
+ MvcHtmlString html = helper.RadioButton("foo", "ViewDataFoo");
+ MvcHtmlString html2 = helper.RadioButton("foo", "fooValue2");
+
+ // Assert
+ Assert.Equal(@"<input checked=""checked"" id=""foo"" name=""foo"" type=""radio"" value=""ViewDataFoo"" />", html.ToHtmlString());
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""radio"" value=""fooValue2"" />", html2.ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonWithEmptyNameThrows()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { helper.RadioButton(String.Empty, "value"); },
+ "name");
+ }
+
+ [Fact]
+ public void RadioButtonWithNullValueThrows()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { helper.RadioButton("foo", null); },
+ "value");
+ }
+
+ [Fact]
+ public void RadioButtonWithNameAndValue()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetRadioButtonViewData());
+
+ // Act
+ MvcHtmlString html = helper.RadioButton("foo", "ViewDataFoo");
+
+ // Assert
+ Assert.Equal(@"<input checked=""checked"" id=""foo"" name=""foo"" type=""radio"" value=""ViewDataFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonWithNameAndValue_Unobtrusive()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetRadioButtonViewData());
+ helper.ViewContext.ClientValidationEnabled = true;
+ helper.ViewContext.UnobtrusiveJavaScriptEnabled = true;
+ helper.ViewContext.FormContext = new FormContext();
+ helper.ClientValidationRuleFactory = (name, metadata) => new[] { new ModelClientValidationRule { ValidationType = "type", ErrorMessage = "error" } };
+
+ // Act
+ MvcHtmlString html = helper.RadioButton("foo", "ViewDataFoo");
+
+ // Assert
+ Assert.Equal(@"<input checked=""checked"" data-val=""true"" data-val-type=""error"" id=""foo"" name=""foo"" type=""radio"" value=""ViewDataFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonWithPrefix()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetRadioButtonViewData());
+ helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix";
+
+ // Act
+ MvcHtmlString html = helper.RadioButton("foo", "ViewDataFoo");
+
+ // Assert
+ Assert.Equal(@"<input checked=""checked"" id=""MyPrefix_foo"" name=""MyPrefix.foo"" type=""radio"" value=""ViewDataFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonWithPrefixAndEmptyName()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetRadioButtonViewData());
+ helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix";
+
+ // Act
+ MvcHtmlString html = helper.RadioButton("", "ViewDataFoo");
+
+ // Assert
+ Assert.Equal(@"<input id=""MyPrefix"" name=""MyPrefix"" type=""radio"" value=""ViewDataFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonWithNameAndValueNotMatched()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetRadioButtonViewData());
+
+ // Act
+ MvcHtmlString html = helper.RadioButton("foo", "fooValue");
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""radio"" value=""fooValue"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonWithNameValueUnchecked()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetRadioButtonViewData());
+
+ // Act
+ MvcHtmlString html = helper.RadioButton("bar", "barValue", false /* isChecked */);
+
+ // Assert
+ Assert.Equal(@"<input id=""bar"" name=""bar"" type=""radio"" value=""barValue"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonWithNameValueChecked()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetRadioButtonViewData());
+
+ // Act
+ MvcHtmlString html = helper.RadioButton("bar", "barValue", true /* isChecked */);
+
+ // Assert
+ Assert.Equal(@"<input checked=""checked"" id=""bar"" name=""bar"" type=""radio"" value=""barValue"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonWithObjectAttribute()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetRadioButtonViewData());
+
+ // Act
+ MvcHtmlString html = helper.RadioButton("foo", "fooValue", _attributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazObjValue"" id=""foo"" name=""foo"" type=""radio"" value=""fooValue"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonWithObjectAttributeWithUnderscores()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetRadioButtonViewData());
+
+ // Act
+ MvcHtmlString html = helper.RadioButton("foo", "fooValue", _attributesObjectUnderscoresDictionary);
+
+ // Assert
+ Assert.Equal(@"<input foo-baz=""BazObjValue"" id=""foo"" name=""foo"" type=""radio"" value=""fooValue"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonWithAttributeDictionary()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetRadioButtonViewData());
+
+ // Act
+ MvcHtmlString html = helper.RadioButton("bar", "barValue", _attributesDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazValue"" id=""bar"" name=""bar"" type=""radio"" value=""barValue"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonWithValueUnchecked()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetRadioButtonViewData());
+
+ // Act
+ MvcHtmlString html = helper.RadioButton("foo", "bar", false /* isChecked */);
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""radio"" value=""bar"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonWithValueAndObjectAttributeUnchecked()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetRadioButtonViewData());
+
+ // Act
+ MvcHtmlString html = helper.RadioButton("foo", "bar", false /* isChecked */, _attributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazObjValue"" id=""foo"" name=""foo"" type=""radio"" value=""bar"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonWithValueAndObjectAttributeWithUnderscoresUnchecked()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetRadioButtonViewData());
+
+ // Act
+ MvcHtmlString html = helper.RadioButton("foo", "bar", false /* isChecked */, _attributesObjectUnderscoresDictionary);
+
+ // Assert
+ Assert.Equal(@"<input foo-baz=""BazObjValue"" id=""foo"" name=""foo"" type=""radio"" value=""bar"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonWithValueAndAttributeDictionaryUnchecked()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetRadioButtonViewData());
+
+ // Act
+ MvcHtmlString html = helper.RadioButton("foo", "bar", false /* isChecked */, _attributesDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazValue"" id=""foo"" name=""foo"" type=""radio"" value=""bar"" />", html.ToHtmlString());
+ }
+
+ // RadioButtonFor
+
+ [Fact]
+ public void RadioButtonForWithNullExpressionThrows()
+ {
+ // Arrange
+ HtmlHelper<FooBarModel> helper = MvcHelper.GetHtmlHelper(GetRadioButtonViewData());
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => helper.RadioButtonFor<FooBarModel, object>(null, "value"),
+ "expression");
+ }
+
+ [Fact]
+ public void RadioButtonForWithNullValueThrows()
+ {
+ // Arrange
+ HtmlHelper<FooBarModel> helper = MvcHelper.GetHtmlHelper(GetRadioButtonViewData());
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => helper.RadioButtonFor(m => m.foo, null),
+ "value");
+ }
+
+ [Fact]
+ public void RadioButtonForDictionaryOverridesImplicitParameters()
+ {
+ // Arrange
+ HtmlHelper<FooBarModel> helper = MvcHelper.GetHtmlHelper(GetRadioButtonViewData());
+
+ // Act
+ MvcHtmlString html = helper.RadioButtonFor(m => m.bar, "ViewDataBar", new { @checked = "chucked", value = "baz" });
+
+ // Assert
+ Assert.Equal(@"<input checked=""chucked"" id=""bar"" name=""bar"" type=""radio"" value=""ViewDataBar"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonForShouldRespectModelStateAttemptedValue()
+ {
+ // Arrange
+ HtmlHelper<FooBarModel> helper = MvcHelper.GetHtmlHelper(GetRadioButtonViewData());
+ helper.ViewData.ModelState.SetModelValue("foo", HtmlHelperTest.GetValueProviderResult("ModelStateFoo", "ModelStateFoo"));
+
+ // Act
+ MvcHtmlString html = helper.RadioButtonFor(m => m.foo, "ModelStateFoo", new { @checked = "checked", value = "baz" });
+
+ // Assert
+ Assert.Equal(@"<input checked=""checked"" id=""foo"" name=""foo"" type=""radio"" value=""ModelStateFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonForValueParameterAlwaysRendered()
+ {
+ // Arrange
+ HtmlHelper<FooBarModel> helper = MvcHelper.GetHtmlHelper(GetRadioButtonViewData());
+
+ // Act & Assert
+ Assert.Equal(@"<input checked=""checked"" id=""foo"" name=""foo"" type=""radio"" value=""ViewDataFoo"" />",
+ helper.RadioButtonFor(m => m.foo, "ViewDataFoo").ToHtmlString());
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""radio"" value=""fooValue2"" />",
+ helper.RadioButtonFor(m => m.foo, "fooValue2").ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonForWithNameAndValue()
+ {
+ // Arrange
+ HtmlHelper<FooBarModel> helper = MvcHelper.GetHtmlHelper(GetRadioButtonViewData());
+
+ // Act
+ MvcHtmlString html = helper.RadioButtonFor(m => m.foo, "ViewDataFoo");
+
+ // Assert
+ Assert.Equal(@"<input checked=""checked"" id=""foo"" name=""foo"" type=""radio"" value=""ViewDataFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonForWithNameAndValue_Unobtrusive()
+ {
+ // Arrange
+ HtmlHelper<FooBarModel> helper = MvcHelper.GetHtmlHelper(GetRadioButtonViewData());
+ helper.ViewContext.ClientValidationEnabled = true;
+ helper.ViewContext.UnobtrusiveJavaScriptEnabled = true;
+ helper.ViewContext.FormContext = new FormContext();
+ helper.ClientValidationRuleFactory = (name, metadata) => new[] { new ModelClientValidationRule { ValidationType = "type", ErrorMessage = "error" } };
+
+ // Act
+ MvcHtmlString html = helper.RadioButtonFor(m => m.foo, "ViewDataFoo");
+
+ // Assert
+ Assert.Equal(@"<input checked=""checked"" data-val=""true"" data-val-type=""error"" id=""foo"" name=""foo"" type=""radio"" value=""ViewDataFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonForWithPrefix()
+ {
+ // Arrange
+ HtmlHelper<FooBarModel> helper = MvcHelper.GetHtmlHelper(GetRadioButtonViewData());
+ helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix";
+
+ // Act
+ MvcHtmlString html = helper.RadioButtonFor(m => m.foo, "ViewDataFoo");
+
+ // Assert
+ Assert.Equal(@"<input id=""MyPrefix_foo"" name=""MyPrefix.foo"" type=""radio"" value=""ViewDataFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonForWithNameAndValueNotMatched()
+ {
+ // Arrange
+ HtmlHelper<FooBarModel> helper = MvcHelper.GetHtmlHelper(GetRadioButtonViewData());
+
+ // Act
+ MvcHtmlString html = helper.RadioButtonFor(m => m.foo, "fooValue");
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""radio"" value=""fooValue"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonForWithObjectAttribute()
+ {
+ // Arrange
+ HtmlHelper<FooBarModel> helper = MvcHelper.GetHtmlHelper(GetRadioButtonViewData());
+
+ // Act
+ MvcHtmlString html = helper.RadioButtonFor(m => m.foo, "fooValue", _attributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazObjValue"" id=""foo"" name=""foo"" type=""radio"" value=""fooValue"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonForWithObjectAttributeWithUnderscores()
+ {
+ // Arrange
+ HtmlHelper<FooBarModel> helper = MvcHelper.GetHtmlHelper(GetRadioButtonViewData());
+
+ // Act
+ MvcHtmlString html = helper.RadioButtonFor(m => m.foo, "fooValue", _attributesObjectUnderscoresDictionary);
+
+ // Assert
+ Assert.Equal(@"<input foo-baz=""BazObjValue"" id=""foo"" name=""foo"" type=""radio"" value=""fooValue"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonForWithAttributeDictionary()
+ {
+ // Arrange
+ HtmlHelper<FooBarModel> helper = MvcHelper.GetHtmlHelper(GetRadioButtonViewData());
+
+ // Act
+ MvcHtmlString html = helper.RadioButtonFor(m => m.bar, "barValue", _attributesDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazValue"" id=""bar"" name=""bar"" type=""radio"" value=""barValue"" />", html.ToHtmlString());
+ }
+
+ // TextBox
+
+ [Fact]
+ public void TextBoxDictionaryOverridesImplicitValues()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetTextBoxViewData());
+
+ // Act
+ MvcHtmlString html = helper.TextBox("foo", "DefaultFoo", new { type = "fooType" });
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""fooType"" value=""DefaultFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextBoxExplicitParametersOverrideDictionaryValues()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetTextBoxViewData());
+
+ // Act
+ MvcHtmlString html = helper.TextBox("foo", "DefaultFoo", new { value = "Some other value" });
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""text"" value=""DefaultFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextBoxWithDotReplacementForId()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetTextBoxViewData());
+
+ // Act
+ MvcHtmlString html = helper.TextBox("foo.bar.baz", null);
+
+ // Assert
+ Assert.Equal(@"<input id=""foo_bar_baz"" name=""foo.bar.baz"" type=""text"" value="""" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextBoxWithEmptyNameThrows()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetTextBoxViewData());
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { helper.TextBox(String.Empty); },
+ "name");
+ }
+
+ [Fact]
+ public void TextBoxWithExplicitValue()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetTextBoxViewData());
+
+ // Act
+ MvcHtmlString html = helper.TextBox("foo", "DefaultFoo", (object)null);
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""text"" value=""DefaultFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextBoxWithExplicitValue_Unobtrusive()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetTextBoxViewData());
+ helper.ViewContext.ClientValidationEnabled = true;
+ helper.ViewContext.UnobtrusiveJavaScriptEnabled = true;
+ helper.ViewContext.FormContext = new FormContext();
+ helper.ClientValidationRuleFactory = (name, metadata) => new[] { new ModelClientValidationRule { ValidationType = "type", ErrorMessage = "error" } };
+
+ // Act
+ MvcHtmlString html = helper.TextBox("foo", "DefaultFoo", (object)null);
+
+ // Assert
+ Assert.Equal(@"<input data-val=""true"" data-val-type=""error"" id=""foo"" name=""foo"" type=""text"" value=""DefaultFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextBoxWithExplicitValueAndAttributesDictionary()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetTextBoxViewData());
+
+ // Act
+ MvcHtmlString html = helper.TextBox("foo", "DefaultFoo", _attributesDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazValue"" id=""foo"" name=""foo"" type=""text"" value=""DefaultFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextBoxWithExplicitValueAndAttributesObject()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetTextBoxViewData());
+
+ // Act
+ MvcHtmlString html = helper.TextBox("foo", "DefaultFoo", _attributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazObjValue"" id=""foo"" name=""foo"" type=""text"" value=""DefaultFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextBoxWithExplicitValueNull()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetTextBoxViewData());
+
+ // Act
+ MvcHtmlString html = helper.TextBox("foo", (string)null /* value */, (object)null);
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""text"" value=""ViewDataFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextBoxWithImplicitValue()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetTextBoxViewData());
+
+ // Act
+ MvcHtmlString html = helper.TextBox("foo");
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""text"" value=""ViewDataFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextBoxWithImplicitValueAndAttributesDictionary()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetTextBoxViewData());
+
+ // Act
+ MvcHtmlString html = helper.TextBox("foo", null, _attributesDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazValue"" id=""foo"" name=""foo"" type=""text"" value=""ViewDataFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextBoxWithImplicitValueAndAttributesDictionaryReturnsEmptyValueIfNotFound()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetTextBoxViewData());
+
+ // Act
+ MvcHtmlString html = helper.TextBox("keyNotFound", null, _attributesDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazValue"" id=""keyNotFound"" name=""keyNotFound"" type=""text"" value="""" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextBoxWithImplicitValueAndAttributesObject()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetTextBoxViewData());
+
+ // Act
+ MvcHtmlString html = helper.TextBox("foo", null, _attributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazObjValue"" id=""foo"" name=""foo"" type=""text"" value=""ViewDataFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextBoxWithImplicitValueAndAttributesObjectWithUnderscores()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetTextBoxViewData());
+
+ // Act
+ MvcHtmlString html = helper.TextBox("foo", null, _attributesObjectUnderscoresDictionary);
+
+ // Assert
+ Assert.Equal(@"<input foo-baz=""BazObjValue"" id=""foo"" name=""foo"" type=""text"" value=""ViewDataFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextBoxWithNullNameThrows()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetTextBoxViewData());
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { helper.TextBox(null /* name */); },
+ "name");
+ }
+
+ [Fact]
+ public void TextBoxWithNameAndValue()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetHiddenViewData());
+
+ // Act
+ MvcHtmlString html = helper.TextBox("foo", "fooValue");
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""text"" value=""fooValue"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextBoxWithPrefix()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetHiddenViewData());
+ helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix";
+
+ // Act
+ MvcHtmlString html = helper.TextBox("foo", "fooValue");
+
+ // Assert
+ Assert.Equal(@"<input id=""MyPrefix_foo"" name=""MyPrefix.foo"" type=""text"" value=""fooValue"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextBoxWithPrefixAndEmptyName()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetHiddenViewData());
+ helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix";
+
+ // Act
+ MvcHtmlString html = helper.TextBox("", "fooValue");
+
+ // Assert
+ Assert.Equal(@"<input id=""MyPrefix"" name=""MyPrefix"" type=""text"" value=""fooValue"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextBoxWithViewDataErrors()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetTextBoxViewDataWithErrors());
+
+ // Act
+ MvcHtmlString html = helper.TextBox("foo", null, _attributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazObjValue"" class=""input-validation-error"" id=""foo"" name=""foo"" type=""text"" value=""AttemptedValueFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextBoxWithViewDataErrorsAndCustomClass()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetTextBoxViewDataWithErrors());
+
+ // Act
+ MvcHtmlString html = helper.TextBox("foo", null, new { @class = "foo-class" });
+
+ // Assert
+ Assert.Equal(@"<input class=""input-validation-error foo-class"" id=""foo"" name=""foo"" type=""text"" value=""AttemptedValueFoo"" />", html.ToHtmlString());
+ }
+
+ // TextBoxFor
+
+ [Fact]
+ public void TextBoxForWithNullExpressionThrows()
+ {
+ // Arrange
+ HtmlHelper<FooBarModel> helper = MvcHelper.GetHtmlHelper(GetTextBoxViewData());
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => helper.TextBoxFor<FooBarModel, object>(null /* expression */),
+ "expression"
+ );
+ }
+
+ [Fact]
+ public void TextBoxForWithSimpleExpression()
+ {
+ // Arrange
+ HtmlHelper<FooBarModel> helper = MvcHelper.GetHtmlHelper(GetTextBoxViewData());
+
+ // Act
+ MvcHtmlString html = helper.TextBoxFor(m => m.foo);
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""text"" value=""ViewItemFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextBoxForWithSimpleExpression_Unobtrusive()
+ {
+ // Arrange
+ HtmlHelper<FooBarModel> helper = MvcHelper.GetHtmlHelper(GetTextBoxViewData());
+ helper.ViewContext.ClientValidationEnabled = true;
+ helper.ViewContext.UnobtrusiveJavaScriptEnabled = true;
+ helper.ViewContext.FormContext = new FormContext();
+ helper.ClientValidationRuleFactory = (name, metadata) => new[] { new ModelClientValidationRule { ValidationType = "type", ErrorMessage = "error" } };
+
+ // Act
+ MvcHtmlString html = helper.TextBoxFor(m => m.foo);
+
+ // Assert
+ Assert.Equal(@"<input data-val=""true"" data-val-type=""error"" id=""foo"" name=""foo"" type=""text"" value=""ViewItemFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextBoxForWithAttributesDictionary()
+ {
+ // Arrange
+ HtmlHelper<FooBarModel> helper = MvcHelper.GetHtmlHelper(GetTextBoxViewData());
+
+ // Act
+ MvcHtmlString html = helper.TextBoxFor(m => m.foo, _attributesDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazValue"" id=""foo"" name=""foo"" type=""text"" value=""ViewItemFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextBoxForWithAttributesObject()
+ {
+ // Arrange
+ HtmlHelper<FooBarModel> helper = MvcHelper.GetHtmlHelper(GetTextBoxViewData());
+
+ // Act
+ MvcHtmlString html = helper.TextBoxFor(m => m.foo, _attributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazObjValue"" id=""foo"" name=""foo"" type=""text"" value=""ViewItemFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextBoxForWithAttributesObjectWithUnderscores()
+ {
+ // Arrange
+ HtmlHelper<FooBarModel> helper = MvcHelper.GetHtmlHelper(GetTextBoxViewData());
+
+ // Act
+ MvcHtmlString html = helper.TextBoxFor(m => m.foo, _attributesObjectUnderscoresDictionary);
+
+ // Assert
+ Assert.Equal(@"<input foo-baz=""BazObjValue"" id=""foo"" name=""foo"" type=""text"" value=""ViewItemFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextBoxForWithPrefix()
+ {
+ // Arrange
+ HtmlHelper<FooBarModel> helper = MvcHelper.GetHtmlHelper(GetTextBoxViewData());
+ helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix";
+
+ // Act
+ MvcHtmlString html = helper.TextBoxFor(m => m.foo);
+
+ // Assert
+ Assert.Equal(@"<input id=""MyPrefix_foo"" name=""MyPrefix.foo"" type=""text"" value=""ViewItemFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextBoxForWithPrefixAndEmptyName()
+ {
+ // Arrange
+ HtmlHelper<FooBarModel> helper = MvcHelper.GetHtmlHelper(GetTextBoxViewData());
+ helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix";
+
+ // Act
+ MvcHtmlString html = helper.TextBoxFor(m => m);
+
+ // Assert
+ Assert.Equal(@"<input id=""MyPrefix"" name=""MyPrefix"" type=""text"" value=""{ foo = ViewItemFoo, bar = ViewItemBar }"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextBoxForWithErrors()
+ {
+ // Arrange
+ HtmlHelper<FooBarModel> helper = MvcHelper.GetHtmlHelper(GetTextBoxViewDataWithErrors());
+
+ // Act
+ MvcHtmlString html = helper.TextBoxFor(m => m.foo, _attributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazObjValue"" class=""input-validation-error"" id=""foo"" name=""foo"" type=""text"" value=""AttemptedValueFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextBoxForWithErrorsAndCustomClass()
+ {
+ // Arrange
+ HtmlHelper<FooBarModel> helper = MvcHelper.GetHtmlHelper(GetTextBoxViewDataWithErrors());
+
+ // Act
+ MvcHtmlString html = helper.TextBoxFor(m => m.foo, new { @class = "foo-class" });
+
+ // Assert
+ Assert.Equal(@"<input class=""input-validation-error foo-class"" id=""foo"" name=""foo"" type=""text"" value=""AttemptedValueFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextBoxHelpersFormatValue()
+ {
+ // Arrange
+ DateTime dt = new DateTime(1900, 1, 1, 0, 0, 0);
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary { { "viewDataDate", dt } });
+
+ ViewDataDictionary<DateModel> viewData = new ViewDataDictionary<DateModel>() { Model = new DateModel { date = dt } };
+ HtmlHelper<DateModel> dateModelhelper = MvcHelper.GetHtmlHelper(viewData);
+
+ var tests = new[]
+ {
+ // TextBox(name, value, format)
+ new
+ {
+ Html = @"<input id=""viewDataDate"" name=""viewDataDate"" type=""text"" value=""-1900/01/01 12:00:00 AM-"" />",
+ Action = new Func<MvcHtmlString>(() => helper.TextBox("viewDataDate", null, "-{0}-"))
+ },
+ // TextBox(name, value, format)
+ new
+ {
+ Html = @"<input id=""date"" name=""date"" type=""text"" value=""-1900/01/01 12:00:00 AM-"" />",
+ Action = new Func<MvcHtmlString>(() => helper.TextBox("date", dt, "-{0}-"))
+ },
+ // TextBox(name, value, format, hmtlAttributes)
+ new
+ {
+ Html = @"<input id=""date"" name=""date"" type=""text"" value=""-1900/01/01 12:00:00 AM-"" />",
+ Action = new Func<MvcHtmlString>(() => helper.TextBox("date", dt, "-{0}-", (object)null))
+ },
+ // TextBox(name, value, format, hmtlAttributes)
+ new
+ {
+ Html = @"<input id=""date"" name=""date"" type=""text"" value=""-1900/01/01 12:00:00 AM-"" />",
+ Action = new Func<MvcHtmlString>(() => helper.TextBox("date", dt, "-{0}-", new RouteValueDictionary()))
+ },
+ // TextBoxFor(expression, format)
+ new
+ {
+ Html = @"<input id=""date"" name=""date"" type=""text"" value=""-1900/01/01 12:00:00 AM-"" />",
+ Action = new Func<MvcHtmlString>(() => dateModelhelper.TextBoxFor(m => m.date, "-{0}-"))
+ },
+ // TextBoxFor(expression, format, hmtlAttributes)
+ new
+ {
+ Html = @"<input id=""date"" name=""date"" type=""text"" value=""-1900/01/01 12:00:00 AM-"" />",
+ Action = new Func<MvcHtmlString>(() => dateModelhelper.TextBoxFor(m => m.date, "-{0}-", (object)null))
+ },
+ // TextBoxFor(expression, format, hmtlAttributes)
+ new
+ {
+ Html = @"<input id=""date"" name=""date"" type=""text"" value=""-1900/01/01 12:00:00 AM-"" />",
+ Action = new Func<MvcHtmlString>(() => dateModelhelper.TextBoxFor(m => m.date, "-{0}-", new RouteValueDictionary()))
+ }
+ };
+
+ // Act && Assert
+ using (HtmlHelperTest.ReplaceCulture("en-ZA", "en-US"))
+ {
+ foreach (var test in tests)
+ {
+ Assert.Equal(test.Html, test.Action().ToHtmlString());
+ }
+ }
+ }
+
+ // MODELS
+ private class FooModel
+ {
+ public string foo { get; set; }
+
+ public override string ToString()
+ {
+ return String.Format("{{ foo = {0} }}", foo ?? "(null)");
+ }
+ }
+
+ private class FooBarModel : FooModel
+ {
+ public string bar { get; set; }
+
+ public override string ToString()
+ {
+ return String.Format("{{ foo = {0}, bar = {1} }}", foo ?? "(null)", bar ?? "(null)");
+ }
+ }
+
+ private class FooBarBazModel
+ {
+ public bool foo { get; set; }
+ public bool bar { get; set; }
+ public bool baz { get; set; }
+
+ public override string ToString()
+ {
+ return String.Format("{{ foo = {0}, bar = {1}, baz = {2} }}", foo, bar, baz);
+ }
+ }
+
+ private class ShallowModel
+ {
+ [Required]
+ public string foo { get; set; }
+ }
+
+ private class DeepContainerModel
+ {
+ public ShallowModel contained { get; set; }
+ }
+
+ private class HiddenModel : FooModel
+ {
+ public byte[] bytes { get; set; }
+ public Binary binary { get; set; }
+ }
+
+ private class DateModel
+ {
+ public DateTime date { get; set; }
+ }
+
+ // CHECKBOX
+ private static ViewDataDictionary<FooBarBazModel> GetCheckBoxViewData()
+ {
+ ViewDataDictionary<FooBarBazModel> viewData = new ViewDataDictionary<FooBarBazModel> { { "foo", true }, { "bar", "NotTrue" }, { "baz", false } };
+ return viewData;
+ }
+
+ // HIDDEN
+ private static ViewDataDictionary<HiddenModel> GetHiddenViewData()
+ {
+ return new ViewDataDictionary<HiddenModel>(new HiddenModel()) { { "foo", "ViewDataFoo" } };
+ }
+
+ private static ViewDataDictionary<HiddenModel> GetHiddenViewDataWithErrors()
+ {
+ ViewDataDictionary<HiddenModel> viewData = new ViewDataDictionary<HiddenModel> { { "foo", "ViewDataFoo" } };
+ viewData.Model = new HiddenModel();
+ ModelState modelStateFoo = new ModelState();
+ modelStateFoo.Errors.Add(new ModelError("foo error 1"));
+ modelStateFoo.Errors.Add(new ModelError("foo error 2"));
+ viewData.ModelState["foo"] = modelStateFoo;
+ modelStateFoo.Value = HtmlHelperTest.GetValueProviderResult("AttemptedValueFoo", "AttemptedValueFoo");
+
+ return viewData;
+ }
+
+ // PASSWORD
+ private static ViewDataDictionary<FooModel> GetPasswordViewData()
+ {
+ return new ViewDataDictionary<FooModel> { { "foo", "ViewDataFoo" } };
+ }
+
+ private static ViewDataDictionary<FooModel> GetPasswordViewDataWithErrors()
+ {
+ ViewDataDictionary<FooModel> viewData = new ViewDataDictionary<FooModel> { { "foo", "ViewDataFoo" } };
+ ModelState modelStateFoo = new ModelState();
+ modelStateFoo.Errors.Add(new ModelError("foo error 1"));
+ modelStateFoo.Errors.Add(new ModelError("foo error 2"));
+ viewData.ModelState["foo"] = modelStateFoo;
+ modelStateFoo.Value = HtmlHelperTest.GetValueProviderResult("AttemptedValueFoo", "AttemptedValueFoo");
+
+ return viewData;
+ }
+
+ // RADIO
+ private static ViewDataDictionary<FooBarModel> GetRadioButtonViewData()
+ {
+ ViewDataDictionary<FooBarModel> viewData = new ViewDataDictionary<FooBarModel> { { "foo", "ViewDataFoo" } };
+ viewData.Model = new FooBarModel { foo = "ViewItemFoo", bar = "ViewItemBar" };
+ ModelState modelState = new ModelState();
+ modelState.Value = HtmlHelperTest.GetValueProviderResult("ViewDataFoo", "ViewDataFoo");
+ viewData.ModelState["foo"] = modelState;
+
+ return viewData;
+ }
+
+ // TEXTBOX
+ private static readonly RouteValueDictionary _attributesDictionary = new RouteValueDictionary(new { baz = "BazValue" });
+ private static readonly object _attributesObjectDictionary = new { baz = "BazObjValue" };
+ private static readonly object _attributesObjectUnderscoresDictionary = new { foo_baz = "BazObjValue" };
+
+ private static ViewDataDictionary<FooBarModel> GetTextBoxViewData()
+ {
+ ViewDataDictionary<FooBarModel> viewData = new ViewDataDictionary<FooBarModel> { { "foo", "ViewDataFoo" } };
+ viewData.Model = new FooBarModel { foo = "ViewItemFoo", bar = "ViewItemBar" };
+
+ return viewData;
+ }
+
+ private static ViewDataDictionary<FooBarModel> GetTextBoxViewDataWithErrors()
+ {
+ ViewDataDictionary<FooBarModel> viewData = new ViewDataDictionary<FooBarModel> { { "foo", "ViewDataFoo" } };
+ viewData.Model = new FooBarModel { foo = "ViewItemFoo", bar = "ViewItemBar" };
+ ModelState modelStateFoo = new ModelState();
+ modelStateFoo.Errors.Add(new ModelError("foo error 1"));
+ modelStateFoo.Errors.Add(new ModelError("foo error 2"));
+ viewData.ModelState["foo"] = modelStateFoo;
+ modelStateFoo.Value = HtmlHelperTest.GetValueProviderResult(new string[] { "AttemptedValueFoo" }, "AttemptedValueFoo");
+
+ return viewData;
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Html/Test/LabelExtensionsTest.cs b/test/System.Web.Mvc.Test/Html/Test/LabelExtensionsTest.cs
new file mode 100644
index 00000000..fddfdd6e
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Html/Test/LabelExtensionsTest.cs
@@ -0,0 +1,516 @@
+using System.Collections.Generic;
+using System.Linq.Expressions;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Html.Test
+{
+ public class LabelExtensionsTest
+ {
+ Mock<ModelMetadataProvider> metadataProvider;
+ Mock<ModelMetadata> metadata;
+ ViewDataDictionary viewData;
+ Mock<ViewContext> viewContext;
+ Mock<IViewDataContainer> viewDataContainer;
+ HtmlHelper<object> html;
+
+ public LabelExtensionsTest()
+ {
+ metadataProvider = new Mock<ModelMetadataProvider>();
+ metadata = new Mock<ModelMetadata>(metadataProvider.Object, null, null, typeof(object), null);
+ viewData = new ViewDataDictionary();
+
+ viewContext = new Mock<ViewContext>();
+ viewContext.Setup(c => c.ViewData).Returns(viewData);
+
+ viewDataContainer = new Mock<IViewDataContainer>();
+ viewDataContainer.Setup(c => c.ViewData).Returns(viewData);
+
+ html = new HtmlHelper<object>(viewContext.Object, viewDataContainer.Object);
+
+ metadataProvider.Setup(p => p.GetMetadataForProperties(It.IsAny<object>(), It.IsAny<Type>()))
+ .Returns(new ModelMetadata[0]);
+ metadataProvider.Setup(p => p.GetMetadataForProperty(It.IsAny<Func<object>>(), It.IsAny<Type>(), It.IsAny<string>()))
+ .Returns(metadata.Object);
+ metadataProvider.Setup(p => p.GetMetadataForType(It.IsAny<Func<object>>(), It.IsAny<Type>()))
+ .Returns(metadata.Object);
+ }
+
+ // Label tests
+
+ [Fact]
+ public void LabelNullExpressionThrows()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => html.Label(null),
+ "expression");
+ }
+
+ [Fact]
+ public void LabelViewDataNotFound()
+ {
+ // Act
+ MvcHtmlString result = html.Label("PropertyName", null, null, metadataProvider.Object);
+
+ // Assert
+ Assert.Equal(@"<label for=""PropertyName"">PropertyName</label>", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void LabelViewDataNull()
+ {
+ // Act
+ viewData["PropertyName"] = null;
+ MvcHtmlString result = html.Label("PropertyName", null, null, metadataProvider.Object);
+
+ // Assert
+ Assert.Equal(@"<label for=""PropertyName"">PropertyName</label>", result.ToHtmlString());
+ }
+
+ class Model
+ {
+ public string PropertyName { get; set; }
+ }
+
+ [Fact]
+ public void LabelViewDataFromPropertyGetsActualPropertyType()
+ {
+ // Arrange
+ Model model = new Model { PropertyName = "propertyValue" };
+ HtmlHelper<Model> html = new HtmlHelper<Model>(viewContext.Object, viewDataContainer.Object);
+ viewData.Model = model;
+ metadataProvider.Setup(p => p.GetMetadataForProperty(It.IsAny<Func<object>>(), typeof(Model), "PropertyName"))
+ .Returns(metadata.Object)
+ .Verifiable();
+
+ // Act
+ html.Label("PropertyName", null, null, metadataProvider.Object);
+
+ // Assert
+ metadataProvider.Verify();
+ }
+
+ [Fact]
+ public void LabelUsesTemplateInfoPrefix()
+ {
+ // Arrange
+ viewData.TemplateInfo.HtmlFieldPrefix = "Prefix";
+
+ // Act
+ MvcHtmlString result = html.Label("PropertyName", null, null, metadataProvider.Object);
+
+ // Assert
+ Assert.Equal(@"<label for=""Prefix_PropertyName"">PropertyName</label>", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void LabelUsesLabelTextBeforeMetadata()
+ {
+ // Arrange
+ metadata = new Mock<ModelMetadata>(metadataProvider.Object, null, null, typeof(object), "Custom property name from metadata");
+ metadataProvider.Setup(p => p.GetMetadataForType(It.IsAny<Func<object>>(), It.IsAny<Type>()))
+ .Returns(metadata.Object);
+
+ //Act
+ MvcHtmlString result = html.Label("PropertyName", "Label Text", null, metadataProvider.Object);
+
+ // Assert
+ Assert.Equal(@"<label for=""PropertyName"">Label Text</label>", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void LabelUsesMetadataForDisplayTextWhenLabelTextIsNull()
+ {
+ // Arrange
+ metadata.Setup(m => m.DisplayName).Returns("Custom display name from metadata");
+
+ // Act
+ MvcHtmlString result = html.Label("PropertyName", null, null, metadataProvider.Object);
+
+ // Assert
+ Assert.Equal(@"<label for=""PropertyName"">Custom display name from metadata</label>", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void LabelUsesMetadataForPropertyNameWhenDisplayNameIsNull()
+ {
+ // Arrange
+ metadata = new Mock<ModelMetadata>(metadataProvider.Object, null, null, typeof(object), "Custom property name from metadata");
+ metadataProvider.Setup(p => p.GetMetadataForType(It.IsAny<Func<object>>(), It.IsAny<Type>()))
+ .Returns(metadata.Object);
+
+ // Act
+ MvcHtmlString result = html.Label("PropertyName", null, null, metadataProvider.Object);
+
+ // Assert
+ Assert.Equal(@"<label for=""PropertyName"">Custom property name from metadata</label>", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void LabelEmptyDisplayNameReturnsEmptyLabelText()
+ {
+ // Arrange
+ metadata.Setup(m => m.DisplayName).Returns(String.Empty);
+
+ // Act
+ MvcHtmlString result = html.Label("PropertyName", null, null, metadataProvider.Object);
+
+ // Assert
+ Assert.Equal(String.Empty, result.ToHtmlString());
+ }
+
+ [Fact]
+ public void LabelWithAnonymousValues()
+ {
+ // Act
+ MvcHtmlString result = html.Label("PropertyName", null, new { @for = "attrFor" }, metadataProvider.Object);
+
+ // Assert
+ Assert.Equal(@"<label for=""attrFor"">PropertyName</label>", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void LabelWithAnonymousValuesAndLabelText()
+ {
+ // Act
+ MvcHtmlString result = html.Label("PropertyName", "Label Text", new { @for = "attrFor" }, metadataProvider.Object);
+
+ // Assert
+ Assert.Equal(@"<label for=""attrFor"">Label Text</label>", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void LabelWithTypedAttributes()
+ {
+ // Arrange
+ Dictionary<string, object> htmlAttributes = new Dictionary<string, object>
+ {
+ { "foo", "bar" },
+ { "quux", "baz" }
+ };
+
+ // Act
+ MvcHtmlString result = html.Label("PropertyName", null, htmlAttributes, metadataProvider.Object);
+
+ // Assert
+ Assert.Equal(@"<label foo=""bar"" for=""PropertyName"" quux=""baz"">PropertyName</label>", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void LabelWithTypedAttributesAndLabelText()
+ {
+ // Arrange
+ Dictionary<string, object> htmlAttributes = new Dictionary<string, object>
+ {
+ { "foo", "bar" },
+ { "quux", "baz" }
+ };
+
+ // Act
+ MvcHtmlString result = html.Label("PropertyName", "Label Text", htmlAttributes, metadataProvider.Object);
+
+ // Assert
+ Assert.Equal(@"<label foo=""bar"" for=""PropertyName"" quux=""baz"">Label Text</label>", result.ToHtmlString());
+ }
+
+ // LabelFor tests
+
+ [Fact]
+ public void LabelForNullExpressionThrows()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => html.LabelFor((Expression<Func<Object, Object>>)null),
+ "expression");
+ }
+
+ [Fact]
+ public void LabelForNonMemberExpressionThrows()
+ {
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ () => html.LabelFor(model => new { foo = "Bar" }, null, metadataProvider.Object),
+ "Templates can be used only with field access, property access, single-dimension array index, or single-parameter custom indexer expressions.");
+ }
+
+ [Fact]
+ public void LabelForViewDataNotFound()
+ {
+ // Arrange
+ string unknownKey = "this is a dummy parameter value";
+
+ // Act
+ MvcHtmlString result = html.LabelFor(model => unknownKey, null, null, metadataProvider.Object);
+
+ // Assert
+ Assert.Equal(@"<label for=""unknownKey"">unknownKey</label>", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void LabelForUsesTemplateInfoPrefix()
+ {
+ // Arrange
+ viewData.TemplateInfo.HtmlFieldPrefix = "Prefix";
+ string unknownKey = "this is a dummy parameter value";
+
+ // Act
+ MvcHtmlString result = html.LabelFor(model => unknownKey, null, null, metadataProvider.Object);
+
+ // Assert
+ Assert.Equal(@"<label for=""Prefix_unknownKey"">unknownKey</label>", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void LabelForUsesLabelTextBeforeModelMetadata()
+ {
+ // Arrange
+ metadata.Setup(m => m.DisplayName).Returns("Custom display name from metadata");
+ string unknownKey = "this is a dummy parameter value";
+
+ //Act
+ MvcHtmlString result = html.LabelFor(model => unknownKey, "Label Text", null, metadataProvider.Object);
+
+ // Assert
+ Assert.Equal(@"<label for=""unknownKey"">Label Text</label>", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void LabelForUsesModelMetadata()
+ {
+ // Arrange
+ metadata.Setup(m => m.DisplayName).Returns("Custom display name from metadata");
+ string unknownKey = "this is a dummy parameter value";
+
+ // Act
+ MvcHtmlString result = html.LabelFor(model => unknownKey, null, null, metadataProvider.Object);
+
+ // Assert
+ Assert.Equal(@"<label for=""unknownKey"">Custom display name from metadata</label>", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void LabelForEmptyDisplayNameReturnsEmptyLabelText()
+ {
+ // Arrange
+ metadata.Setup(m => m.DisplayName).Returns(String.Empty);
+ string unknownKey = "this is a dummy parameter value";
+
+ // Act
+ MvcHtmlString result = html.LabelFor(model => unknownKey, null, null, metadataProvider.Object);
+
+ // Assert
+ Assert.Equal(String.Empty, result.ToHtmlString());
+ }
+
+ [Fact]
+ public void LabelForWithAnonymousValues()
+ {
+ //Arrange
+ string unknownKey = "this is a dummy parameter value";
+
+ // Act
+ MvcHtmlString result = html.LabelFor(model => unknownKey, null, new { @for = "attrFor" }, metadataProvider.Object);
+
+ // Assert
+ Assert.Equal(@"<label for=""attrFor"">unknownKey</label>", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void LabelForWithAnonymousValuesAndLabelText()
+ {
+ //Arrange
+ string unknownKey = "this is a dummy parameter value";
+
+ // Act
+ MvcHtmlString result = html.LabelFor(model => unknownKey, "Label Text", new { @for = "attrFor" }, metadataProvider.Object);
+
+ // Assert
+ Assert.Equal(@"<label for=""attrFor"">Label Text</label>", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void LabelForWithTypedAttributes()
+ {
+ //Arrange
+ string unknownKey = "this is a dummy parameter value";
+
+ Dictionary<string, object> htmlAttributes = new Dictionary<string, object>
+ {
+ { "foo", "bar" },
+ { "quux", "baz" }
+ };
+
+ // Act
+ MvcHtmlString result = html.LabelFor(model => unknownKey, null, htmlAttributes, metadataProvider.Object);
+
+ // Assert
+ Assert.Equal(@"<label foo=""bar"" for=""unknownKey"" quux=""baz"">unknownKey</label>", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void LabelForWithTypedAttributesAndLabelText()
+ {
+ //Arrange
+ string unknownKey = "this is a dummy parameter value";
+
+ Dictionary<string, object> htmlAttributes = new Dictionary<string, object>
+ {
+ { "foo", "bar" },
+ { "quux", "baz" }
+ };
+
+ // Act
+ MvcHtmlString result = html.LabelFor(model => unknownKey, "Label Text", htmlAttributes, metadataProvider.Object);
+
+ // Assert
+ Assert.Equal(@"<label foo=""bar"" for=""unknownKey"" quux=""baz"">Label Text</label>", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void LabelForWithNestedClass()
+ { // Dev10 Bug #936323
+ // Arrange
+ HtmlHelper<NestedProduct> html = new HtmlHelper<NestedProduct>(viewContext.Object, viewDataContainer.Object);
+
+ // Act
+ MvcHtmlString result = html.LabelFor(nested => nested.product.Id, null, null, metadataProvider.Object);
+
+ //Assert
+ Assert.Equal(@"<label for=""product_Id"">Id</label>", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void LabelForWithArrayExpression()
+ { // Dev10 Bug #905780
+ // Arrange
+ HtmlHelper<Cart> html = new HtmlHelper<Cart>(viewContext.Object, viewDataContainer.Object);
+
+ // Act
+ MvcHtmlString result = html.LabelFor(cart => cart.Products[0].Id, null, null, metadataProvider.Object);
+
+ // Assert
+ Assert.Equal(@"<label for=""Products_0__Id"">Id</label>", result.ToHtmlString());
+ }
+
+ private class Product
+ {
+ public int Id { get; set; }
+ }
+
+ private class Cart
+ {
+ public Product[] Products { get; set; }
+ }
+
+ private class NestedProduct
+ {
+ public Product product = new Product();
+ }
+
+ // LabelForModel tests
+
+ [Fact]
+ public void LabelForModelUsesLabelTextBeforeModelMetadata()
+ {
+ // Arrange
+ viewData.ModelMetadata = metadata.Object;
+ viewData.TemplateInfo.HtmlFieldPrefix = "Prefix";
+ metadata.Setup(m => m.DisplayName).Returns("Custom display name from metadata");
+
+ // Act
+ MvcHtmlString result = html.LabelForModel("Label Text");
+
+ // Assert
+ Assert.Equal(@"<label for=""Prefix"">Label Text</label>", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void LabelForModelUsesModelMetadata()
+ {
+ // Arrange
+ viewData.ModelMetadata = metadata.Object;
+ viewData.TemplateInfo.HtmlFieldPrefix = "Prefix";
+ metadata.Setup(m => m.DisplayName).Returns("Custom display name from metadata");
+
+ // Act
+ MvcHtmlString result = html.LabelForModel();
+
+ // Assert
+ Assert.Equal(@"<label for=""Prefix"">Custom display name from metadata</label>", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void LabelForModelWithAnonymousValues()
+ {
+ //Arrange
+ viewData.ModelMetadata = metadata.Object;
+ viewData.TemplateInfo.HtmlFieldPrefix = "Prefix";
+ metadata.Setup(m => m.DisplayName).Returns("Custom display name from metadata");
+
+ // Act
+ MvcHtmlString result = html.LabelForModel(new { @for = "attrFor" });
+
+ // Assert
+ Assert.Equal(@"<label for=""attrFor"">Custom display name from metadata</label>", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void LabelForModelWithAnonymousValuesAndLabelText()
+ {
+ //Arrange
+ viewData.ModelMetadata = metadata.Object;
+ viewData.TemplateInfo.HtmlFieldPrefix = "Prefix";
+ metadata.Setup(m => m.DisplayName).Returns("Custom display name from metadata");
+
+ // Act
+ MvcHtmlString result = html.LabelForModel("Label Text", new { @for = "attrFor" });
+
+ // Assert
+ Assert.Equal(@"<label for=""attrFor"">Label Text</label>", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void LabelForModelWithTypedAttributes()
+ {
+ //Arrange
+ viewData.ModelMetadata = metadata.Object;
+ viewData.TemplateInfo.HtmlFieldPrefix = "Prefix";
+ metadata.Setup(m => m.DisplayName).Returns("Custom display name from metadata");
+
+ Dictionary<string, object> htmlAttributes = new Dictionary<string, object>
+ {
+ { "foo", "bar" },
+ { "quux", "baz" }
+ };
+
+ // Act
+ MvcHtmlString result = html.LabelForModel(htmlAttributes);
+
+ // Assert
+ Assert.Equal(@"<label foo=""bar"" for=""Prefix"" quux=""baz"">Custom display name from metadata</label>", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void LabelForModelWithTypedAttributesAndLabelText()
+ {
+ //Arrange
+ viewData.ModelMetadata = metadata.Object;
+ viewData.TemplateInfo.HtmlFieldPrefix = "Prefix";
+ metadata.Setup(m => m.DisplayName).Returns("Custom display name from metadata");
+
+ Dictionary<string, object> htmlAttributes = new Dictionary<string, object>
+ {
+ { "foo", "bar" },
+ { "quux", "baz" }
+ };
+
+ // Act
+ MvcHtmlString result = html.LabelForModel("Label Text", htmlAttributes);
+
+ // Assert
+ Assert.Equal(@"<label foo=""bar"" for=""Prefix"" quux=""baz"">Label Text</label>", result.ToHtmlString());
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Html/Test/LinkExtensionsTest.cs b/test/System.Web.Mvc.Test/Html/Test/LinkExtensionsTest.cs
new file mode 100644
index 00000000..eb182421
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Html/Test/LinkExtensionsTest.cs
@@ -0,0 +1,562 @@
+using System.Web.Routing;
+using Microsoft.Web.UnitTestUtil;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Html.Test
+{
+ public class LinkExtensionsTest
+ {
+ private const string AppPathModifier = MvcHelper.AppPathModifier;
+
+ [Fact]
+ public void ActionLink()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ MvcHtmlString html = htmlHelper.ActionLink("linktext", "newaction");
+
+ // Assert
+ Assert.Equal(@"<a href=""" + AppPathModifier + @"/app/home/newaction"">linktext</a>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkDictionaryOverridesImplicitValues()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ MvcHtmlString html = htmlHelper.ActionLink("linktext", "newaction", null, new { href = "http://foo.com" });
+
+ // Assert
+ Assert.Equal(@"<a href=""http://foo.com"">linktext</a>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkExplictValuesOverrideDictionary()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ MvcHtmlString html = htmlHelper.ActionLink("linktext", "explicitAction", new { action = "dictionaryAction" }, null);
+
+ // Assert
+ Assert.Equal(@"<a href=""" + AppPathModifier + @"/app/home/explicitAction"">linktext</a>", html.ToHtmlString());
+ }
+
+ [Fact(Skip = "External bug DevDiv 356125 -- does not work correctly on 4.5")]
+ public void ActionLinkParametersNeedEscaping()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ MvcHtmlString html = htmlHelper.ActionLink("linktext<&>\"", "new action<&>\"");
+
+ // Assert
+ Assert.Equal(@"<a href=""" + AppPathModifier + @"/app/home/new%20action%3C%26%3E%22"">linktext&lt;&amp;&gt;&quot;</a>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkWithActionNameAndValueDictionary()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ MvcHtmlString html = htmlHelper.ActionLink("linktext", "newaction", new RouteValueDictionary(new { controller = "home2" }));
+
+ // Assert
+ Assert.Equal(@"<a href=""" + AppPathModifier + @"/app/home2/newaction"">linktext</a>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkWithActionNameAndValueObject()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ MvcHtmlString html = htmlHelper.ActionLink("linktext", "newaction", new { controller = "home2" });
+
+ // Assert
+ Assert.Equal(@"<a href=""" + AppPathModifier + @"/app/home2/newaction"">linktext</a>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkWithControllerName()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ MvcHtmlString html = htmlHelper.ActionLink("linktext", "newaction", "home2");
+
+ // Assert
+ Assert.Equal(@"<a href=""" + AppPathModifier + @"/app/home2/newaction"">linktext</a>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkWithControllerNameAndDictionary()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ MvcHtmlString html = htmlHelper.ActionLink("linktext", "newaction", "home2", new RouteValueDictionary(new { id = "someid" }), new RouteValueDictionary(new { baz = "baz" }));
+
+ // Assert
+ Assert.Equal(@"<a baz=""baz"" href=""" + AppPathModifier + @"/app/home2/newaction/someid"">linktext</a>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkWithControllerNameAndObjectProperties()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ MvcHtmlString html = htmlHelper.ActionLink("linktext", "newaction", "home2", new { id = "someid" }, new { baz = "baz" });
+
+ // Assert
+ Assert.Equal(@"<a baz=""baz"" href=""" + AppPathModifier + @"/app/home2/newaction/someid"">linktext</a>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkWithControllerNameAndObjectPropertiesWithUnderscores()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ MvcHtmlString html = htmlHelper.ActionLink("linktext", "newaction", "home2", new { id = "someid" }, new { foo_baz = "baz" });
+
+ // Assert
+ Assert.Equal(@"<a foo-baz=""baz"" href=""" + AppPathModifier + @"/app/home2/newaction/someid"">linktext</a>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkWithDictionary()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ MvcHtmlString html = htmlHelper.ActionLink("linktext", "newaction", new RouteValueDictionary(new { Controller = "home2", id = "someid" }), new RouteValueDictionary(new { baz = "baz" }));
+
+ // Assert
+ Assert.Equal(@"<a baz=""baz"" href=""" + AppPathModifier + @"/app/home2/newaction/someid"">linktext</a>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkWithFragment()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ MvcHtmlString html = htmlHelper.ActionLink("linktext", "newaction", "home2", "http", "foo.bar.com", "foo", new { id = "someid" }, new { baz = "baz" });
+
+ // Assert
+ Assert.Equal(@"<a baz=""baz"" href=""http://foo.bar.com" + AppPathModifier + @"/app/home2/newaction/someid#foo"">linktext</a>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkWithFragmentAndAttributesWithUnderscores()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ MvcHtmlString html = htmlHelper.ActionLink("linktext", "newaction", "home2", "http", "foo.bar.com", "foo", new { id = "someid" }, new { foo_baz = "baz" });
+
+ // Assert
+ Assert.Equal(@"<a foo-baz=""baz"" href=""http://foo.bar.com" + AppPathModifier + @"/app/home2/newaction/someid#foo"">linktext</a>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkWithNullHostname()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ MvcHtmlString html = htmlHelper.ActionLink("linktext", "newaction", "home2", "https", null /* hostName */, "foo", new { id = "someid" }, new { baz = "baz" });
+
+ // Assert
+ Assert.Equal(@"<a baz=""baz"" href=""https://localhost" + AppPathModifier + @"/app/home2/newaction/someid#foo"">linktext</a>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkWithNullProtocolAndFragment()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ MvcHtmlString html = htmlHelper.ActionLink("linktext", "newaction", "home2", null /* protocol */, "foo.bar.com", null /* fragment */, new { id = "someid" }, new { baz = "baz" });
+
+ // Assert
+ Assert.Equal(@"<a baz=""baz"" href=""http://foo.bar.com" + AppPathModifier + @"/app/home2/newaction/someid"">linktext</a>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkWithNullProtocolNullHostNameAndNullFragment()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ MvcHtmlString html = htmlHelper.ActionLink("linktext", "newaction", "home2", null /* protocol */, null /* hostName */, null /* fragment */, new { id = "someid" }, new { baz = "baz" });
+
+ // Assert
+ Assert.Equal(@"<a baz=""baz"" href=""" + AppPathModifier + @"/app/home2/newaction/someid"">linktext</a>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkWithObjectProperties()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ MvcHtmlString html = htmlHelper.ActionLink("linktext", "newaction", new { Controller = "home2", id = "someid" }, new { baz = "baz" });
+
+ // Assert
+ Assert.Equal(@"<a baz=""baz"" href=""" + AppPathModifier + @"/app/home2/newaction/someid"">linktext</a>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkWithObjectPropertiesWithUnderscores()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ MvcHtmlString html = htmlHelper.ActionLink("linktext", "newaction", new { Controller = "home2", id = "someid" }, new { foo_baz = "baz" });
+
+ // Assert
+ Assert.Equal(@"<a foo-baz=""baz"" href=""" + AppPathModifier + @"/app/home2/newaction/someid"">linktext</a>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkWithProtocol()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ MvcHtmlString html = htmlHelper.ActionLink("linktext", "newaction", "home2", "https", "foo.bar.com", null /* fragment */, new { id = "someid" }, new { baz = "baz" });
+
+ // Assert
+ Assert.Equal(@"<a baz=""baz"" href=""https://foo.bar.com" + AppPathModifier + @"/app/home2/newaction/someid"">linktext</a>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkWithProtocolAndFragment()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ MvcHtmlString html = htmlHelper.ActionLink("linktext", "newaction", "home2", "https", "foo.bar.com", "foo", new { id = "someid" }, new { baz = "baz" });
+
+ // Assert
+ Assert.Equal(@"<a baz=""baz"" href=""https://foo.bar.com" + AppPathModifier + @"/app/home2/newaction/someid#foo"">linktext</a>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkWithDefaultPort()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(Uri.UriSchemeHttps, -1);
+
+ // Act
+ MvcHtmlString html = htmlHelper.ActionLink("linktext", "newaction", "home2", "https", "foo.bar.com", "foo", new { id = "someid" }, new { baz = "baz" });
+
+ // Assert
+ Assert.Equal(@"<a baz=""baz"" href=""https://foo.bar.com" + AppPathModifier + @"/app/home2/newaction/someid#foo"">linktext</a>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkWithDifferentPortProtocols()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(Uri.UriSchemeHttp, -1);
+
+ // Act
+ MvcHtmlString html = htmlHelper.ActionLink("linktext", "newaction", "home2", "https", "foo.bar.com", "foo", new { id = "someid" }, new { baz = "baz" });
+
+ // Assert
+ Assert.Equal(@"<a baz=""baz"" href=""https://foo.bar.com" + AppPathModifier + @"/app/home2/newaction/someid#foo"">linktext</a>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkWithNonDefaultPortAndDifferentProtocol()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(Uri.UriSchemeHttp, 32768);
+
+ // Act
+ MvcHtmlString html = htmlHelper.ActionLink("linktext", "newaction", "home2", "https", "foo.bar.com", "foo", new { id = "someid" }, new { baz = "baz" });
+
+ // Assert
+ Assert.Equal(@"<a baz=""baz"" href=""https://foo.bar.com" + AppPathModifier + @"/app/home2/newaction/someid#foo"">linktext</a>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ActionLinkWithNonDefaultPortAndSameProtocol()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(Uri.UriSchemeHttp, 32768);
+
+ // Act
+ MvcHtmlString html = htmlHelper.ActionLink("linktext", "newaction", "home2", "http", "foo.bar.com", "foo", new { id = "someid" }, new { baz = "baz" });
+
+ // Assert
+ Assert.Equal(@"<a baz=""baz"" href=""http://foo.bar.com:32768" + AppPathModifier + @"/app/home2/newaction/someid#foo"">linktext</a>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void LinkGenerationDoesNotChangeProvidedDictionary()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+ RouteValueDictionary valuesDictionary = new RouteValueDictionary();
+
+ // Act
+ htmlHelper.ActionLink("linkText", "actionName", valuesDictionary, new RouteValueDictionary());
+
+ // Assert
+ Assert.Empty(valuesDictionary);
+ Assert.False(valuesDictionary.ContainsKey("action"));
+ }
+
+ [Fact]
+ public void NullOrEmptyStringParameterThrows()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+ var tests = new[]
+ {
+ // ActionLink(string linkText, string actionName)
+ new { Parameter = "linkText", Action = new Action(() => htmlHelper.ActionLink(String.Empty, "actionName")) },
+ // ActionLink(string linkText, string actionName, object routeValues, object htmlAttributes)
+ new { Parameter = "linkText", Action = new Action(() => htmlHelper.ActionLink(String.Empty, "actionName", new Object(), null /* htmlAttributes */)) },
+ // ActionLink(string linkText, string actionName, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes)
+ new { Parameter = "linkText", Action = new Action(() => htmlHelper.ActionLink(String.Empty, "actionName", new RouteValueDictionary(), new RouteValueDictionary())) },
+ // ActionLink(string linkText, string actionName, string controllerName)
+ new { Parameter = "linkText", Action = new Action(() => htmlHelper.ActionLink(String.Empty, "actionName", "controllerName")) },
+ // ActionLink(string linkText, string actionName, string controllerName, object routeValues, object htmlAttributes)
+ new { Parameter = "linkText", Action = new Action(() => htmlHelper.ActionLink(String.Empty, "actionName", "controllerName", new Object(), null /* htmlAttributes */)) },
+ // ActionLink(string linkText, string actionName, string controllerName, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes)
+ new { Parameter = "linkText", Action = new Action(() => htmlHelper.ActionLink(String.Empty, "actionName", "controllerName", new RouteValueDictionary(), new RouteValueDictionary())) },
+ // ActionLink(string linkText, string actionName, string controllerName, string protocol, string hostName, string fragment, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes)
+ new { Parameter = "linkText", Action = new Action(() => htmlHelper.ActionLink(String.Empty, "actionName", "controllerName", null, null, null, new RouteValueDictionary(), new RouteValueDictionary())) },
+ // RouteLink(string linkText, object routeValues, object htmlAttributes)
+ new { Parameter = "linkText", Action = new Action(() => htmlHelper.RouteLink(String.Empty, new Object(), null /* htmlAttributes */)) },
+ // RouteLink(string linkText, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes)
+ new { Parameter = "linkText", Action = new Action(() => htmlHelper.RouteLink(String.Empty, new RouteValueDictionary(), new RouteValueDictionary())) },
+ // RouteLink(string linkText, string routeName, object routeValues)
+ new { Parameter = "linkText", Action = new Action(() => htmlHelper.RouteLink(String.Empty, "routeName", null /* routeValues */)) },
+ // RouteLink(string linkText, string routeName, RouteValueDictionary routeValues)
+ new { Parameter = "linkText", Action = new Action(() => htmlHelper.RouteLink(String.Empty, "routeName", new RouteValueDictionary() /* routeValues */)) },
+ // RouteLink(string linkText, string routeName)
+ new { Parameter = "linkText", Action = new Action(() => htmlHelper.RouteLink(String.Empty, (string)null /* routeName */)) },
+ // RouteLink(string linkText, object routeValues)
+ new { Parameter = "linkText", Action = new Action(() => htmlHelper.RouteLink(String.Empty, (object)null /* routeValues */)) },
+ // RouteLink(string linkText, RouteValueDictionary routeValues)
+ new { Parameter = "linkText", Action = new Action(() => htmlHelper.RouteLink(String.Empty, new RouteValueDictionary() /* routeValues */)) },
+ // RouteLink(string linkText, string routeName, object routeValues, object htmlAttributes)
+ new { Parameter = "linkText", Action = new Action(() => htmlHelper.RouteLink(String.Empty, "routeName", new Object(), null /* htmlAttributes */)) },
+ // RouteLink(string linkText, string routeName, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes)
+ new { Parameter = "linkText", Action = new Action(() => htmlHelper.RouteLink(String.Empty, "routeName", new RouteValueDictionary(), new RouteValueDictionary())) },
+ // RouteLink(string linkText, string routeName, string protocol, string hostName, string fragment, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes)
+ new { Parameter = "linkText", Action = new Action(() => htmlHelper.RouteLink(String.Empty, "routeName", null, null, null, new RouteValueDictionary(), new RouteValueDictionary())) },
+ };
+
+ // Act & Assert
+ foreach (var test in tests)
+ {
+ Assert.ThrowsArgumentNullOrEmpty(test.Action, test.Parameter);
+ }
+ }
+
+ [Fact]
+ public void RouteLinkCanUseNamedRouteWithoutSpecifyingDefaults()
+ {
+ // DevDiv 217072: Non-mvc specific helpers should not give default values for controller and action
+
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+ htmlHelper.RouteCollection.MapRoute("MyRouteName", "any/url", new { controller = "Charlie" });
+
+ // Act
+ MvcHtmlString html = htmlHelper.RouteLink("linktext", "MyRouteName", null /* routeValues */);
+
+ // Assert
+ Assert.Equal(@"<a href=""" + AppPathModifier + @"/app/any/url"">linktext</a>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RouteLinkWithDictionary()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ MvcHtmlString html = htmlHelper.RouteLink("linktext", new RouteValueDictionary(new { Action = "newaction", Controller = "home2", id = "someid" }), new RouteValueDictionary(new { baz = "baz" }));
+
+ // Assert
+ Assert.Equal(@"<a baz=""baz"" href=""" + AppPathModifier + @"/app/home2/newaction/someid"">linktext</a>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RouteLinkWithFragment()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ MvcHtmlString html = htmlHelper.RouteLink("linktext", "namedroute", "http", "foo.bar.com", "foo", new { Action = "newaction", Controller = "home2", id = "someid" }, new { baz = "baz" });
+
+ // Assert
+ Assert.Equal(@"<a baz=""baz"" href=""http://foo.bar.com" + AppPathModifier + @"/app/named/home2/newaction/someid#foo"">linktext</a>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RouteLinkWithFragmentAndAttributesWithUnderscores()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ MvcHtmlString html = htmlHelper.RouteLink("linktext", "namedroute", "http", "foo.bar.com", "foo", new { Action = "newaction", Controller = "home2", id = "someid" }, new { foo_baz = "baz" });
+
+ // Assert
+ Assert.Equal(@"<a foo-baz=""baz"" href=""http://foo.bar.com" + AppPathModifier + @"/app/named/home2/newaction/someid#foo"">linktext</a>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RouteLinkWithLinkTextAndRouteName()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+ htmlHelper.RouteCollection.MapRoute("MyRouteName", "any/url", new { controller = "Charlie" });
+
+ // Act
+ MvcHtmlString html = htmlHelper.RouteLink("linktext", "MyRouteName");
+
+ // Assert
+ Assert.Equal(@"<a href=""" + AppPathModifier + @"/app/any/url"">linktext</a>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RouteLinkWithObjectProperties()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ MvcHtmlString html = htmlHelper.RouteLink("linktext", new { Action = "newaction", Controller = "home2", id = "someid" }, new { baz = "baz" });
+
+ // Assert
+ Assert.Equal(@"<a baz=""baz"" href=""" + AppPathModifier + @"/app/home2/newaction/someid"">linktext</a>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RouteLinkWithObjectPropertiesWithUnderscores()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ MvcHtmlString html = htmlHelper.RouteLink("linktext", new { Action = "newaction", Controller = "home2", id = "someid" }, new { foo_baz = "baz" });
+
+ // Assert
+ Assert.Equal(@"<a foo-baz=""baz"" href=""" + AppPathModifier + @"/app/home2/newaction/someid"">linktext</a>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RouteLinkWithProtocol()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ MvcHtmlString html = htmlHelper.RouteLink("linktext", "namedroute", "https", "foo.bar.com", null /* fragment */, new { Action = "newaction", Controller = "home2", id = "someid" }, new { baz = "baz" });
+
+ // Assert
+ Assert.Equal(@"<a baz=""baz"" href=""https://foo.bar.com" + AppPathModifier + @"/app/named/home2/newaction/someid"">linktext</a>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RouteLinkWithProtocolAndFragment()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ MvcHtmlString html = htmlHelper.RouteLink("linktext", "namedroute", "https", "foo.bar.com", "foo", new { Action = "newaction", Controller = "home2", id = "someid" }, new { baz = "baz" });
+
+ // Assert
+ Assert.Equal(@"<a baz=""baz"" href=""https://foo.bar.com" + AppPathModifier + @"/app/named/home2/newaction/someid#foo"">linktext</a>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RouteLinkWithRouteNameAndDefaults()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ MvcHtmlString html = htmlHelper.RouteLink("linktext", "namedroute", new { Action = "newaction" });
+
+ // Assert
+ Assert.Equal(@"<a href=""" + AppPathModifier + @"/app/named/home/newaction"">linktext</a>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RouteLinkWithRouteNameAndDictionary()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ MvcHtmlString html = htmlHelper.RouteLink("linktext", "namedroute", new RouteValueDictionary(new { Action = "newaction", Controller = "home2", id = "someid" }), new RouteValueDictionary());
+
+ // Assert
+ Assert.Equal(@"<a href=""" + AppPathModifier + @"/app/named/home2/newaction/someid"">linktext</a>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RouteLinkWithRouteNameAndObjectProperties()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ MvcHtmlString html = htmlHelper.RouteLink("linktext", "namedroute", new { Action = "newaction", Controller = "home2", id = "someid" }, new { baz = "baz" });
+
+ // Assert
+ Assert.Equal(@"<a baz=""baz"" href=""" + AppPathModifier + @"/app/named/home2/newaction/someid"">linktext</a>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RouteLinkWithRouteNameAndObjectPropertiesWithUnderscores()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ MvcHtmlString html = htmlHelper.RouteLink("linktext", "namedroute", new { Action = "newaction", Controller = "home2", id = "someid" }, new { foo_baz = "baz" });
+
+ // Assert
+ Assert.Equal(@"<a foo-baz=""baz"" href=""" + AppPathModifier + @"/app/named/home2/newaction/someid"">linktext</a>", html.ToHtmlString());
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Html/Test/MvcFormTest.cs b/test/System.Web.Mvc.Test/Html/Test/MvcFormTest.cs
new file mode 100644
index 00000000..721b7fee
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Html/Test/MvcFormTest.cs
@@ -0,0 +1,97 @@
+using System.Collections;
+using System.IO;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Html.Test
+{
+ public class MvcFormTest
+ {
+ [Fact]
+ public void ConstructorWithNullViewContextThrows()
+ {
+ Assert.ThrowsArgumentNull(
+ delegate { new MvcForm((ViewContext)null); },
+ "viewContext");
+ }
+
+ [Fact]
+ public void DisposeRendersCloseFormTag()
+ {
+ // Arrange
+ StringWriter writer = new StringWriter();
+ ViewContext viewContext = GetViewContext(writer);
+
+ MvcForm form = new MvcForm(viewContext);
+
+ // Act
+ form.Dispose();
+
+ // Assert
+ Assert.Equal("</form>", writer.ToString());
+ }
+
+ [Fact]
+ public void EndFormRendersCloseFormTag()
+ {
+ // Arrange
+ StringWriter writer = new StringWriter();
+ ViewContext viewContext = GetViewContext(writer);
+
+ MvcForm form = new MvcForm(viewContext);
+
+ // Act
+ form.EndForm();
+
+ // Assert
+ Assert.Equal("</form>", writer.ToString());
+ }
+
+ [Fact]
+ public void DisposeTwiceRendersCloseFormTagOnce()
+ {
+ // Arrange
+ StringWriter writer = new StringWriter();
+ ViewContext viewContext = GetViewContext(writer);
+
+ MvcForm form = new MvcForm(viewContext);
+
+ // Act
+ form.Dispose();
+ form.Dispose();
+
+ // Assert
+ Assert.Equal("</form>", writer.ToString());
+ }
+
+ [Fact]
+ public void EndFormTwiceRendersCloseFormTagOnce()
+ {
+ // Arrange
+ StringWriter writer = new StringWriter();
+ ViewContext viewContext = GetViewContext(writer);
+
+ MvcForm form = new MvcForm(viewContext);
+
+ // Act
+ form.EndForm();
+ form.EndForm();
+
+ // Assert
+ Assert.Equal("</form>", writer.ToString());
+ }
+
+ private static ViewContext GetViewContext(TextWriter writer)
+ {
+ Mock<HttpContextBase> mockHttpContext = new Mock<HttpContextBase>();
+ mockHttpContext.Setup(o => o.Items).Returns(new Hashtable());
+
+ return new ViewContext()
+ {
+ HttpContext = mockHttpContext.Object,
+ Writer = writer
+ };
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Html/Test/NameExtensionsTest.cs b/test/System.Web.Mvc.Test/Html/Test/NameExtensionsTest.cs
new file mode 100644
index 00000000..55a4ba5b
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Html/Test/NameExtensionsTest.cs
@@ -0,0 +1,83 @@
+using Microsoft.Web.UnitTestUtil;
+using Xunit;
+
+namespace System.Web.Mvc.Html.Test
+{
+ public class NameExtensionsTest
+ {
+ [Fact]
+ public void NonStronglyTypedWithNoPrefix()
+ {
+ // Arrange
+ HtmlHelper html = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+
+ // Act & Assert
+ Assert.Equal("", html.IdForModel().ToHtmlString());
+ Assert.Equal("foo", html.Id("foo").ToHtmlString());
+ Assert.Equal("foo_bar", html.Id("foo.bar").ToHtmlString());
+ Assert.Equal(String.Empty, html.Id("<script>alert(\"XSS!\")</script>").ToHtmlString());
+
+ Assert.Equal("", html.NameForModel().ToHtmlString());
+ Assert.Equal("foo", html.Name("foo").ToHtmlString());
+ Assert.Equal("foo.bar", html.Name("foo.bar").ToHtmlString());
+ Assert.Equal("&lt;script>alert(&quot;XSS!&quot;)&lt;/script>", html.Name("<script>alert(\"XSS!\")</script>").ToHtmlString());
+ }
+
+ [Fact]
+ public void NonStronglyTypedWithPrefix()
+ {
+ // Arrange
+ HtmlHelper html = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+ html.ViewData.TemplateInfo.HtmlFieldPrefix = "prefix";
+
+ // Act & Assert
+ Assert.Equal("prefix", html.IdForModel().ToHtmlString());
+ Assert.Equal("prefix_foo", html.Id("foo").ToHtmlString());
+ Assert.Equal("prefix_foo_bar", html.Id("foo.bar").ToHtmlString());
+
+ Assert.Equal("prefix", html.NameForModel().ToHtmlString());
+ Assert.Equal("prefix.foo", html.Name("foo").ToHtmlString());
+ Assert.Equal("prefix.foo.bar", html.Name("foo.bar").ToHtmlString());
+ }
+
+ [Fact]
+ public void StronglyTypedWithNoPrefix()
+ {
+ // Arrange
+ HtmlHelper<OuterClass> html = MvcHelper.GetHtmlHelper(new ViewDataDictionary<OuterClass>());
+
+ // Act & Assert
+ Assert.Equal("IntValue", html.IdFor(m => m.IntValue).ToHtmlString());
+ Assert.Equal("Inner_StringValue", html.IdFor(m => m.Inner.StringValue).ToHtmlString());
+
+ Assert.Equal("IntValue", html.NameFor(m => m.IntValue).ToHtmlString());
+ Assert.Equal("Inner.StringValue", html.NameFor(m => m.Inner.StringValue).ToHtmlString());
+ }
+
+ [Fact]
+ public void StronglyTypedWithPrefix()
+ {
+ // Arrange
+ HtmlHelper<OuterClass> html = MvcHelper.GetHtmlHelper(new ViewDataDictionary<OuterClass>());
+ html.ViewData.TemplateInfo.HtmlFieldPrefix = "prefix";
+
+ // Act & Assert
+ Assert.Equal("prefix_IntValue", html.IdFor(m => m.IntValue).ToHtmlString());
+ Assert.Equal("prefix_Inner_StringValue", html.IdFor(m => m.Inner.StringValue).ToHtmlString());
+
+ Assert.Equal("prefix.IntValue", html.NameFor(m => m.IntValue).ToHtmlString());
+ Assert.Equal("prefix.Inner.StringValue", html.NameFor(m => m.Inner.StringValue).ToHtmlString());
+ }
+
+ private sealed class OuterClass
+ {
+ public InnerClass Inner { get; set; }
+ public int IntValue { get; set; }
+ }
+
+ private sealed class InnerClass
+ {
+ public string StringValue { get; set; }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Html/Test/PartialExtensionsTest.cs b/test/System.Web.Mvc.Test/Html/Test/PartialExtensionsTest.cs
new file mode 100644
index 00000000..2380bd80
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Html/Test/PartialExtensionsTest.cs
@@ -0,0 +1,84 @@
+using System.IO;
+using Xunit;
+
+namespace System.Web.Mvc.Html.Test
+{
+ public class PartialExtensionsTest
+ {
+ [Fact]
+ public void PartialWithViewName()
+ {
+ // Arrange
+ RenderPartialExtensionsTest.SpyHtmlHelper helper = RenderPartialExtensionsTest.SpyHtmlHelper.Create();
+
+ // Act
+ MvcHtmlString result = helper.Partial("partial-view");
+
+ // Assert
+ Assert.Equal("partial-view", helper.RenderPartialInternal_PartialViewName);
+ Assert.Same(helper.ViewData, helper.RenderPartialInternal_ViewData);
+ Assert.Null(helper.RenderPartialInternal_Model);
+ Assert.IsType<StringWriter>(helper.RenderPartialInternal_Writer);
+ Assert.Same(ViewEngines.Engines, helper.RenderPartialInternal_ViewEngineCollection);
+ Assert.Equal("This is the result of the view", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void PartialWithViewNameAndViewData()
+ {
+ // Arrange
+ RenderPartialExtensionsTest.SpyHtmlHelper helper = RenderPartialExtensionsTest.SpyHtmlHelper.Create();
+ ViewDataDictionary viewData = new ViewDataDictionary();
+
+ // Act
+ MvcHtmlString result = helper.Partial("partial-view", viewData);
+
+ // Assert
+ Assert.Equal("partial-view", helper.RenderPartialInternal_PartialViewName);
+ Assert.Same(viewData, helper.RenderPartialInternal_ViewData);
+ Assert.Null(helper.RenderPartialInternal_Model);
+ Assert.IsType<StringWriter>(helper.RenderPartialInternal_Writer);
+ Assert.Same(ViewEngines.Engines, helper.RenderPartialInternal_ViewEngineCollection);
+ Assert.Equal("This is the result of the view", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void PartialWithViewNameAndModel()
+ {
+ // Arrange
+ RenderPartialExtensionsTest.SpyHtmlHelper helper = RenderPartialExtensionsTest.SpyHtmlHelper.Create();
+ object model = new object();
+
+ // Act
+ MvcHtmlString result = helper.Partial("partial-view", model);
+
+ // Assert
+ Assert.Equal("partial-view", helper.RenderPartialInternal_PartialViewName);
+ Assert.Same(helper.ViewData, helper.RenderPartialInternal_ViewData);
+ Assert.Same(model, helper.RenderPartialInternal_Model);
+ Assert.IsType<StringWriter>(helper.RenderPartialInternal_Writer);
+ Assert.Same(ViewEngines.Engines, helper.RenderPartialInternal_ViewEngineCollection);
+ Assert.Equal("This is the result of the view", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void PartialWithViewNameAndModelAndViewData()
+ {
+ // Arrange
+ RenderPartialExtensionsTest.SpyHtmlHelper helper = RenderPartialExtensionsTest.SpyHtmlHelper.Create();
+ object model = new object();
+ ViewDataDictionary viewData = new ViewDataDictionary();
+
+ // Act
+ MvcHtmlString result = helper.Partial("partial-view", model, viewData);
+
+ // Assert
+ Assert.Equal("partial-view", helper.RenderPartialInternal_PartialViewName);
+ Assert.Same(viewData, helper.RenderPartialInternal_ViewData);
+ Assert.Same(model, helper.RenderPartialInternal_Model);
+ Assert.IsType<StringWriter>(helper.RenderPartialInternal_Writer);
+ Assert.Same(ViewEngines.Engines, helper.RenderPartialInternal_ViewEngineCollection);
+ Assert.Equal("This is the result of the view", result.ToHtmlString());
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Html/Test/RenderPartialExtensionsTest.cs b/test/System.Web.Mvc.Test/Html/Test/RenderPartialExtensionsTest.cs
new file mode 100644
index 00000000..4aba780e
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Html/Test/RenderPartialExtensionsTest.cs
@@ -0,0 +1,122 @@
+using System.IO;
+using Moq;
+using Xunit;
+
+namespace System.Web.Mvc.Html.Test
+{
+ public class RenderPartialExtensionsTest
+ {
+ [Fact]
+ public void RenderPartialWithViewName()
+ {
+ // Arrange
+ SpyHtmlHelper helper = SpyHtmlHelper.Create();
+
+ // Act
+ helper.RenderPartial("partial-view");
+
+ // Assert
+ Assert.Equal("partial-view", helper.RenderPartialInternal_PartialViewName);
+ Assert.Same(helper.ViewData, helper.RenderPartialInternal_ViewData);
+ Assert.Null(helper.RenderPartialInternal_Model);
+ Assert.Same(helper.ViewContext.Writer, helper.RenderPartialInternal_Writer);
+ Assert.Same(ViewEngines.Engines, helper.RenderPartialInternal_ViewEngineCollection);
+ }
+
+ [Fact]
+ public void RenderPartialWithViewNameAndViewData()
+ {
+ // Arrange
+ SpyHtmlHelper helper = SpyHtmlHelper.Create();
+ ViewDataDictionary viewData = new ViewDataDictionary();
+
+ // Act
+ helper.RenderPartial("partial-view", viewData);
+
+ // Assert
+ Assert.Equal("partial-view", helper.RenderPartialInternal_PartialViewName);
+ Assert.Same(viewData, helper.RenderPartialInternal_ViewData);
+ Assert.Null(helper.RenderPartialInternal_Model);
+ Assert.Same(helper.ViewContext.Writer, helper.RenderPartialInternal_Writer);
+ Assert.Same(ViewEngines.Engines, helper.RenderPartialInternal_ViewEngineCollection);
+ }
+
+ [Fact]
+ public void RenderPartialWithViewNameAndModel()
+ {
+ // Arrange
+ SpyHtmlHelper helper = SpyHtmlHelper.Create();
+ object model = new object();
+
+ // Act
+ helper.RenderPartial("partial-view", model);
+
+ // Assert
+ Assert.Equal("partial-view", helper.RenderPartialInternal_PartialViewName);
+ Assert.Same(helper.ViewData, helper.RenderPartialInternal_ViewData);
+ Assert.Same(model, helper.RenderPartialInternal_Model);
+ Assert.Same(helper.ViewContext.Writer, helper.RenderPartialInternal_Writer);
+ Assert.Same(ViewEngines.Engines, helper.RenderPartialInternal_ViewEngineCollection);
+ }
+
+ [Fact]
+ public void RenderPartialWithViewNameAndModelAndViewData()
+ {
+ // Arrange
+ SpyHtmlHelper helper = SpyHtmlHelper.Create();
+ object model = new object();
+ ViewDataDictionary viewData = new ViewDataDictionary();
+
+ // Act
+ helper.RenderPartial("partial-view", model, viewData);
+
+ // Assert
+ Assert.Equal("partial-view", helper.RenderPartialInternal_PartialViewName);
+ Assert.Same(viewData, helper.RenderPartialInternal_ViewData);
+ Assert.Same(model, helper.RenderPartialInternal_Model);
+ Assert.Same(helper.ViewContext.Writer, helper.RenderPartialInternal_Writer);
+ Assert.Same(ViewEngines.Engines, helper.RenderPartialInternal_ViewEngineCollection);
+ }
+
+ internal class SpyHtmlHelper : HtmlHelper
+ {
+ public string RenderPartialInternal_PartialViewName;
+ public ViewDataDictionary RenderPartialInternal_ViewData;
+ public object RenderPartialInternal_Model;
+ public TextWriter RenderPartialInternal_Writer;
+ public ViewEngineCollection RenderPartialInternal_ViewEngineCollection;
+
+ SpyHtmlHelper(ViewContext viewContext, IViewDataContainer viewDataContainer)
+ : base(viewContext, viewDataContainer)
+ {
+ }
+
+ public static SpyHtmlHelper Create()
+ {
+ ViewDataDictionary viewData = new ViewDataDictionary();
+
+ Mock<ViewContext> mockViewContext = new Mock<ViewContext>() { DefaultValue = DefaultValue.Mock };
+ mockViewContext.Setup(c => c.HttpContext.Response.Output).Throws(new Exception("Response.Output should never be called."));
+ mockViewContext.Setup(c => c.ViewData).Returns(viewData);
+ mockViewContext.Setup(c => c.Writer).Returns(new StringWriter());
+
+ Mock<IViewDataContainer> container = new Mock<IViewDataContainer>();
+ container.Setup(c => c.ViewData).Returns(viewData);
+
+ return new SpyHtmlHelper(mockViewContext.Object, container.Object);
+ }
+
+ internal override void RenderPartialInternal(string partialViewName, ViewDataDictionary viewData, object model,
+ TextWriter writer, ViewEngineCollection viewEngineCollection)
+ {
+ RenderPartialInternal_PartialViewName = partialViewName;
+ RenderPartialInternal_ViewData = viewData;
+ RenderPartialInternal_Model = model;
+ RenderPartialInternal_Writer = writer;
+ RenderPartialInternal_ViewEngineCollection = viewEngineCollection;
+
+ writer.Write("This is the result of the view");
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Html/Test/SelectExtensionsTest.cs b/test/System.Web.Mvc.Test/Html/Test/SelectExtensionsTest.cs
new file mode 100644
index 00000000..25454fb2
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Html/Test/SelectExtensionsTest.cs
@@ -0,0 +1,1857 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Globalization;
+using System.Linq;
+using System.Web.Mvc.Test;
+using Microsoft.Web.UnitTestUtil;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Html.Test
+{
+ public class SelectExtensionsTest
+ {
+ private static readonly ViewDataDictionary<FooModel> _listBoxViewData = new ViewDataDictionary<FooModel> { { "foo", new[] { "Bravo" } } };
+ private static readonly ViewDataDictionary<FooModel> _dropDownListViewData = new ViewDataDictionary<FooModel> { { "foo", "Bravo" } };
+ private static readonly ViewDataDictionary<NonIEnumerableModel> _nonIEnumerableViewData = new ViewDataDictionary<NonIEnumerableModel> { { "foo", 1 } };
+
+ private static ViewDataDictionary GetViewDataWithSelectList()
+ {
+ ViewDataDictionary viewData = new ViewDataDictionary();
+ SelectList selectList = new SelectList(MultiSelectListTest.GetSampleAnonymousObjects(), "Letter", "FullWord", "C");
+ viewData["foo"] = selectList;
+ viewData["foo.bar"] = selectList;
+ return viewData;
+ }
+
+ // DropDownList
+
+ [Fact]
+ public void DropDownListUsesExplicitValueIfNotProvidedInViewData()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+ SelectList selectList = new SelectList(MultiSelectListTest.GetSampleAnonymousObjects(), "Letter", "FullWord", "C");
+
+ // Act
+ MvcHtmlString html = helper.DropDownList("foo", selectList, (string)null /* optionLabel */);
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""foo"" name=""foo""><option value=""A"">Alpha</option>
+<option value=""B"">Bravo</option>
+<option selected=""selected"" value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListUsesExplicitValueIfNotProvidedInViewData_Unobtrusive()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+ helper.ViewContext.ClientValidationEnabled = true;
+ helper.ViewContext.UnobtrusiveJavaScriptEnabled = true;
+ helper.ViewContext.FormContext = new FormContext();
+ helper.ClientValidationRuleFactory = (name, metadata) => new[] { new ModelClientValidationRule { ValidationType = "type", ErrorMessage = "error" } };
+ SelectList selectList = new SelectList(MultiSelectListTest.GetSampleAnonymousObjects(), "Letter", "FullWord", "C");
+
+ // Act
+ MvcHtmlString html = helper.DropDownList("foo", selectList, (string)null /* optionLabel */);
+
+ // Assert
+ Assert.Equal(
+ @"<select data-val=""true"" data-val-type=""error"" id=""foo"" name=""foo""><option value=""A"">Alpha</option>
+<option value=""B"">Bravo</option>
+<option selected=""selected"" value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListUsesViewDataDefaultValue()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(_dropDownListViewData);
+ SelectList selectList = new SelectList(MultiSelectListTest.GetSampleStrings(), "Charlie");
+
+ // Act
+ MvcHtmlString html = helper.DropDownList("foo", selectList, (string)null /* optionLabel */);
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""foo"" name=""foo""><option>Alpha</option>
+<option selected=""selected"">Bravo</option>
+<option>Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListUsesViewDataDefaultValueNoOptionLabel()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(_dropDownListViewData);
+ SelectList selectList = new SelectList(MultiSelectListTest.GetSampleStrings(), "Charlie");
+
+ // Act
+ MvcHtmlString html = helper.DropDownList("foo", selectList);
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""foo"" name=""foo""><option>Alpha</option>
+<option selected=""selected"">Bravo</option>
+<option>Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListWithAttributesDictionary()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+ SelectList selectList = new SelectList(MultiSelectListTest.GetSampleStrings());
+
+ // Act
+ MvcHtmlString html = helper.DropDownList("foo", selectList, null /* optionLabel */, HtmlHelperTest.AttributesDictionary);
+
+ // Assert
+ Assert.Equal(
+ @"<select baz=""BazValue"" id=""foo"" name=""foo""><option>Alpha</option>
+<option>Bravo</option>
+<option>Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListWithEmptyNameThrows()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { helper.DropDownList(String.Empty, (SelectList)null /* selectList */, (string)null /* optionLabel */); },
+ "name");
+ }
+
+ [Fact]
+ public void DropDownListWithErrors()
+ {
+ // Arrange
+ SelectList selectList = new SelectList(MultiSelectListTest.GetSampleStrings(), new[] { "Charlie" });
+ ViewDataDictionary viewData = GetViewDataWithErrors();
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(viewData);
+
+ // Act
+ MvcHtmlString html = helper.DropDownList("foo", selectList, null /* optionLabel */, HtmlHelperTest.AttributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(
+ @"<select baz=""BazObjValue"" class=""input-validation-error"" id=""foo"" name=""foo""><option>Alpha</option>
+<option selected=""selected"">Bravo</option>
+<option>Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListWithErrorsAndCustomClass()
+ {
+ // Arrange
+ SelectList selectList = new SelectList(MultiSelectListTest.GetSampleStrings());
+ ViewDataDictionary viewData = GetViewDataWithErrors();
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(viewData);
+
+ // Act
+ MvcHtmlString html = helper.DropDownList("foo", selectList, null /* optionLabel */, new { @class = "foo-class" });
+
+ // Assert
+ Assert.Equal(
+ @"<select class=""input-validation-error foo-class"" id=""foo"" name=""foo""><option>Alpha</option>
+<option selected=""selected"">Bravo</option>
+<option>Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListWithNullNameThrows()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { helper.DropDownList(null /* name */, (SelectList)null /* selectList */, (string)null /* optionLabel */); },
+ "name");
+ }
+
+ [Fact]
+ public void DropDownListWithNullSelectListUsesViewData()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper();
+ helper.ViewData["foo"] = new MultiSelectList(MultiSelectListTest.GetSampleStrings(), new[] { "Charlie" });
+
+ // Act
+ MvcHtmlString html = helper.DropDownList("foo");
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""foo"" name=""foo""><option>Alpha</option>
+<option>Bravo</option>
+<option selected=""selected"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListWithObjectDictionary()
+ {
+ // Arrange
+ SelectList selectList = new SelectList(MultiSelectListTest.GetSampleStrings());
+ ViewDataDictionary viewData = new ViewDataDictionary();
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(viewData);
+
+ // Act
+ MvcHtmlString html = helper.DropDownList("foo", selectList, null /* optionLabel */, HtmlHelperTest.AttributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(
+ @"<select baz=""BazObjValue"" id=""foo"" name=""foo""><option>Alpha</option>
+<option>Bravo</option>
+<option>Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListWithObjectDictionaryWithUnderscores()
+ {
+ // Arrange
+ SelectList selectList = new SelectList(MultiSelectListTest.GetSampleStrings());
+ ViewDataDictionary viewData = new ViewDataDictionary();
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(viewData);
+
+ // Act
+ MvcHtmlString html = helper.DropDownList("foo", selectList, null /* optionLabel */, HtmlHelperTest.AttributesObjectUnderscoresDictionary);
+
+ // Assert
+ Assert.Equal(
+ @"<select foo-baz=""BazObjValue"" id=""foo"" name=""foo""><option>Alpha</option>
+<option>Bravo</option>
+<option>Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListWithObjectDictionaryAndSelectList()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+ SelectList selectList = new SelectList(MultiSelectListTest.GetSampleStrings());
+
+ // Act
+ MvcHtmlString html = helper.DropDownList("foo", selectList, null /* optionLabel */, HtmlHelperTest.AttributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(
+ @"<select baz=""BazObjValue"" id=""foo"" name=""foo""><option>Alpha</option>
+<option>Bravo</option>
+<option>Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListWithObjectDictionaryAndSelectListNoOptionLabel()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+ SelectList selectList = new SelectList(MultiSelectListTest.GetSampleStrings());
+
+ // Act
+ MvcHtmlString html = helper.DropDownList("foo", selectList, HtmlHelperTest.AttributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(
+ @"<select baz=""BazObjValue"" id=""foo"" name=""foo""><option>Alpha</option>
+<option>Bravo</option>
+<option>Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListWithObjectDictionaryWithUnderscoresAndSelectListNoOptionLabel()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+ SelectList selectList = new SelectList(MultiSelectListTest.GetSampleStrings());
+
+ // Act
+ MvcHtmlString html = helper.DropDownList("foo", selectList, HtmlHelperTest.AttributesObjectUnderscoresDictionary);
+
+ // Assert
+ Assert.Equal(
+ @"<select foo-baz=""BazObjValue"" id=""foo"" name=""foo""><option>Alpha</option>
+<option>Bravo</option>
+<option>Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListWithObjectDictionaryAndEmptyOptionLabel()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+ SelectList selectList = new SelectList(MultiSelectListTest.GetSampleStrings());
+
+ // Act
+ MvcHtmlString html = helper.DropDownList("foo", selectList, String.Empty /* optionLabel */, HtmlHelperTest.AttributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(
+ @"<select baz=""BazObjValue"" id=""foo"" name=""foo""><option value=""""></option>
+<option>Alpha</option>
+<option>Bravo</option>
+<option>Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListWithObjectDictionaryAndTitle()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+ SelectList selectList = new SelectList(MultiSelectListTest.GetSampleStrings());
+
+ // Act
+ MvcHtmlString html = helper.DropDownList("foo", selectList, "[Select Something]", HtmlHelperTest.AttributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(
+ @"<select baz=""BazObjValue"" id=""foo"" name=""foo""><option value="""">[Select Something]</option>
+<option>Alpha</option>
+<option>Bravo</option>
+<option>Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListUsesViewDataSelectList()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetViewDataWithSelectList());
+
+ // Act
+ MvcHtmlString html = helper.DropDownList("foo", (string)null /* optionLabel */);
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""foo"" name=""foo""><option value=""A"">Alpha</option>
+<option value=""B"">Bravo</option>
+<option selected=""selected"" value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListUsesModelState()
+ {
+ // Arrange
+ SelectList selectList = new SelectList(MultiSelectListTest.GetSampleStrings());
+ ViewDataDictionary viewData = GetViewDataWithErrors();
+ viewData["foo"] = selectList;
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(viewData);
+
+ // Act
+ MvcHtmlString html = helper.DropDownList("foo");
+
+ // Assert
+ Assert.Equal(
+ @"<select class=""input-validation-error"" id=""foo"" name=""foo""><option>Alpha</option>
+<option selected=""selected"">Bravo</option>
+<option>Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListUsesViewDataSelectListNoOptionLabel()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetViewDataWithSelectList());
+
+ // Act
+ MvcHtmlString html = helper.DropDownList("foo");
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""foo"" name=""foo""><option value=""A"">Alpha</option>
+<option value=""B"">Bravo</option>
+<option selected=""selected"" value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListWithDotReplacementForId()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetViewDataWithSelectList());
+
+ // Act
+ MvcHtmlString html = helper.DropDownList("foo.bar");
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""foo_bar"" name=""foo.bar""><option value=""A"">Alpha</option>
+<option value=""B"">Bravo</option>
+<option selected=""selected"" value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListWithIEnumerableSelectListItem()
+ {
+ // Arrange
+ ViewDataDictionary vdd = new ViewDataDictionary { { "foo", MultiSelectListTest.GetSampleIEnumerableObjects() } };
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(vdd);
+
+ // Act
+ MvcHtmlString html = helper.DropDownList("foo");
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""foo"" name=""foo""><option value=""123456789"">John</option>
+<option value=""987654321"">Jane</option>
+<option selected=""selected"" value=""111111111"">Joe</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListWithIEnumerableSelectListItemSelectsDefaultFromViewData()
+ {
+ // Arrange
+ ViewDataDictionary vdd = new ViewDataDictionary { { "foo", "123456789" } };
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(vdd);
+
+ // Act
+ MvcHtmlString html = helper.DropDownList("foo", MultiSelectListTest.GetSampleIEnumerableObjects());
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""foo"" name=""foo""><option selected=""selected"" value=""123456789"">John</option>
+<option value=""987654321"">Jane</option>
+<option value=""111111111"">Joe</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListWithListOfSelectListItemSelectsDefaultFromViewData()
+ {
+ // Arrange
+ ViewDataDictionary vdd = new ViewDataDictionary { { "foo", "123456789" } };
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(vdd);
+
+ // Act
+ MvcHtmlString html = helper.DropDownList("foo", MultiSelectListTest.GetSampleListObjects());
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""foo"" name=""foo""><option selected=""selected"" value=""123456789"">John</option>
+<option value=""987654321"">Jane</option>
+<option value=""111111111"">Joe</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListWithListOfSelectListItem()
+ {
+ // Arrange
+ ViewDataDictionary vdd = new ViewDataDictionary { { "foo", MultiSelectListTest.GetSampleListObjects() } };
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(vdd);
+
+ // Act
+ MvcHtmlString html = helper.DropDownList("foo");
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""foo"" name=""foo""><option value=""123456789"">John</option>
+<option value=""987654321"">Jane</option>
+<option selected=""selected"" value=""111111111"">Joe</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListWithNullViewDataValueThrows()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+
+ // Act
+ Assert.Throws<InvalidOperationException>(
+ delegate { helper.DropDownList("foo", (string)null /* optionLabel */); },
+ "There is no ViewData item of type 'IEnumerable<SelectListItem>' that has the key 'foo'.");
+ }
+
+ [Fact]
+ public void DropDownListWithWrongViewDataTypeValueThrows()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary { { "foo", 123 } });
+
+ // Act
+ Assert.Throws<InvalidOperationException>(
+ delegate { helper.DropDownList("foo", (string)null /* optionLabel */); },
+ "The ViewData item that has the key 'foo' is of type 'System.Int32' but must be of type 'IEnumerable<SelectListItem>'.");
+ }
+
+ [Fact]
+ public void DropDownListWithPrefix()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+ SelectList selectList = new SelectList(MultiSelectListTest.GetSampleStrings());
+ helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix";
+
+ // Act
+ MvcHtmlString html = helper.DropDownList("foo", selectList, HtmlHelperTest.AttributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(
+ @"<select baz=""BazObjValue"" id=""MyPrefix_foo"" name=""MyPrefix.foo""><option>Alpha</option>
+<option>Bravo</option>
+<option>Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListWithPrefixAndEmptyName()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+ SelectList selectList = new SelectList(MultiSelectListTest.GetSampleStrings());
+ helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix";
+
+ // Act
+ MvcHtmlString html = helper.DropDownList("", selectList, HtmlHelperTest.AttributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(
+ @"<select baz=""BazObjValue"" id=""MyPrefix"" name=""MyPrefix""><option>Alpha</option>
+<option>Bravo</option>
+<option>Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListWithPrefixAndNullSelectListUsesViewData()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper();
+ helper.ViewData["foo"] = new MultiSelectList(MultiSelectListTest.GetSampleStrings(), new[] { "Charlie" });
+ helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix";
+
+ // Act
+ MvcHtmlString html = helper.DropDownList("foo");
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""MyPrefix_foo"" name=""MyPrefix.foo""><option>Alpha</option>
+<option>Bravo</option>
+<option selected=""selected"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ // DropDownListFor
+
+ [Fact]
+ public void DropDownListForWithNullExpressionThrows()
+ {
+ // Arrange
+ HtmlHelper<object> helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary<object>());
+ SelectList selectList = new SelectList(MultiSelectListTest.GetSampleAnonymousObjects(), "Letter", "FullWord", "C");
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => helper.DropDownListFor<object, object>(null /* expression */, selectList),
+ "expression"
+ );
+ }
+
+ [Fact]
+ public void DropDownListForUsesExplicitValueIfNotProvidedInViewData()
+ {
+ // Arrange
+ HtmlHelper<FooModel> helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary<FooModel>());
+ SelectList selectList = new SelectList(MultiSelectListTest.GetSampleAnonymousObjects(), "Letter", "FullWord", "C");
+
+ // Act
+ MvcHtmlString html = helper.DropDownListFor(m => m.foo, selectList, (string)null /* optionLabel */);
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""foo"" name=""foo""><option value=""A"">Alpha</option>
+<option value=""B"">Bravo</option>
+<option selected=""selected"" value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListForUsesExplicitValueIfNotProvidedInViewData_Unobtrusive()
+ {
+ // Arrange
+ HtmlHelper<FooModel> helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary<FooModel>());
+ helper.ViewContext.ClientValidationEnabled = true;
+ helper.ViewContext.UnobtrusiveJavaScriptEnabled = true;
+ helper.ViewContext.FormContext = new FormContext();
+ helper.ClientValidationRuleFactory = (name, metadata) => new[] { new ModelClientValidationRule { ValidationType = "type", ErrorMessage = "error" } };
+ SelectList selectList = new SelectList(MultiSelectListTest.GetSampleAnonymousObjects(), "Letter", "FullWord", "C");
+
+ // Act
+ MvcHtmlString html = helper.DropDownListFor(m => m.foo, selectList, (string)null /* optionLabel */);
+
+ // Assert
+ Assert.Equal(
+ @"<select data-val=""true"" data-val-type=""error"" id=""foo"" name=""foo""><option value=""A"">Alpha</option>
+<option value=""B"">Bravo</option>
+<option selected=""selected"" value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListForWithEnumerableModel_Unobtrusive()
+ {
+ // Arrange
+ HtmlHelper<IEnumerable<RequiredModel>> helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary<IEnumerable<RequiredModel>>());
+ helper.ViewContext.ClientValidationEnabled = true;
+ helper.ViewContext.UnobtrusiveJavaScriptEnabled = true;
+ helper.ViewContext.FormContext = new FormContext();
+ helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix";
+
+ SelectList selectList = new SelectList(MultiSelectListTest.GetSampleAnonymousObjects(), "Letter", "FullWord", "C");
+
+ // Act
+ MvcHtmlString html = helper.DropDownListFor(m => m.ElementAt(0).foo, selectList);
+
+ // Assert
+ Assert.Equal(
+ @"<select data-val=""true"" data-val-required=""The foo field is required."" id=""MyPrefix_foo"" name=""MyPrefix.foo""><option value=""A"">Alpha</option>
+<option value=""B"">Bravo</option>
+<option selected=""selected"" value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListForUsesViewDataDefaultValue()
+ {
+ // Arrange
+ HtmlHelper<FooModel> helper = MvcHelper.GetHtmlHelper(_dropDownListViewData);
+ SelectList selectList = new SelectList(MultiSelectListTest.GetSampleStrings(), "Charlie");
+
+ // Act
+ MvcHtmlString html = helper.DropDownListFor(m => m.foo, selectList, (string)null /* optionLabel */);
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""foo"" name=""foo""><option>Alpha</option>
+<option selected=""selected"">Bravo</option>
+<option>Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListForUsesViewDataDefaultValueNoOptionLabel()
+ {
+ // Arrange
+ HtmlHelper<FooModel> helper = MvcHelper.GetHtmlHelper(_dropDownListViewData);
+ SelectList selectList = new SelectList(MultiSelectListTest.GetSampleStrings(), "Charlie");
+
+ // Act
+ MvcHtmlString html = helper.DropDownListFor(m => m.foo, selectList);
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""foo"" name=""foo""><option>Alpha</option>
+<option selected=""selected"">Bravo</option>
+<option>Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListForWithAttributesDictionary()
+ {
+ // Arrange
+ HtmlHelper<FooModel> helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary<FooModel>());
+ SelectList selectList = new SelectList(MultiSelectListTest.GetSampleStrings());
+
+ // Act
+ MvcHtmlString html = helper.DropDownListFor(m => m.foo, selectList, null /* optionLabel */, HtmlHelperTest.AttributesDictionary);
+
+ // Assert
+ Assert.Equal(
+ @"<select baz=""BazValue"" id=""foo"" name=""foo""><option>Alpha</option>
+<option>Bravo</option>
+<option>Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListForWithErrors()
+ {
+ // Arrange
+ HtmlHelper<FooBarModel> helper = MvcHelper.GetHtmlHelper(GetViewDataWithErrors());
+ SelectList selectList = new SelectList(MultiSelectListTest.GetSampleStrings(), new[] { "Charlie" });
+
+ // Act
+ MvcHtmlString html = helper.DropDownListFor(m => m.foo, selectList, null /* optionLabel */, HtmlHelperTest.AttributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(
+ @"<select baz=""BazObjValue"" class=""input-validation-error"" id=""foo"" name=""foo""><option>Alpha</option>
+<option selected=""selected"">Bravo</option>
+<option>Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListForWithErrorsAndCustomClass()
+ {
+ // Arrange
+ HtmlHelper<FooBarModel> helper = MvcHelper.GetHtmlHelper(GetViewDataWithErrors());
+ SelectList selectList = new SelectList(MultiSelectListTest.GetSampleStrings());
+
+ // Act
+ MvcHtmlString html = helper.DropDownListFor(m => m.foo, selectList, null /* optionLabel */, new { @class = "foo-class" });
+
+ // Assert
+ Assert.Equal(
+ @"<select class=""input-validation-error foo-class"" id=""foo"" name=""foo""><option>Alpha</option>
+<option selected=""selected"">Bravo</option>
+<option>Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListForWithObjectDictionary()
+ {
+ // Arrange
+ HtmlHelper<FooModel> helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary<FooModel>());
+ SelectList selectList = new SelectList(MultiSelectListTest.GetSampleStrings());
+
+ // Act
+ MvcHtmlString html = helper.DropDownListFor(m => m.foo, selectList, null /* optionLabel */, HtmlHelperTest.AttributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(
+ @"<select baz=""BazObjValue"" id=""foo"" name=""foo""><option>Alpha</option>
+<option>Bravo</option>
+<option>Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListForWithObjectDictionaryWithUnderscores()
+ {
+ // Arrange
+ HtmlHelper<FooModel> helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary<FooModel>());
+ SelectList selectList = new SelectList(MultiSelectListTest.GetSampleStrings());
+
+ // Act
+ MvcHtmlString html = helper.DropDownListFor(m => m.foo, selectList, null /* optionLabel */, HtmlHelperTest.AttributesObjectUnderscoresDictionary);
+
+ // Assert
+ Assert.Equal(
+ @"<select foo-baz=""BazObjValue"" id=""foo"" name=""foo""><option>Alpha</option>
+<option>Bravo</option>
+<option>Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListForWithObjectDictionaryAndSelectListNoOptionLabel()
+ {
+ // Arrange
+ HtmlHelper<FooModel> helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary<FooModel>());
+ SelectList selectList = new SelectList(MultiSelectListTest.GetSampleStrings());
+
+ // Act
+ MvcHtmlString html = helper.DropDownListFor(m => m.foo, selectList, HtmlHelperTest.AttributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(
+ @"<select baz=""BazObjValue"" id=""foo"" name=""foo""><option>Alpha</option>
+<option>Bravo</option>
+<option>Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListForWithObjectDictionaryWithUnderscoresAndSelectListNoOptionLabel()
+ {
+ // Arrange
+ HtmlHelper<FooModel> helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary<FooModel>());
+ SelectList selectList = new SelectList(MultiSelectListTest.GetSampleStrings());
+
+ // Act
+ MvcHtmlString html = helper.DropDownListFor(m => m.foo, selectList, HtmlHelperTest.AttributesObjectUnderscoresDictionary);
+
+ // Assert
+ Assert.Equal(
+ @"<select foo-baz=""BazObjValue"" id=""foo"" name=""foo""><option>Alpha</option>
+<option>Bravo</option>
+<option>Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListForWithObjectDictionaryAndEmptyOptionLabel()
+ {
+ // Arrange
+ HtmlHelper<FooModel> helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary<FooModel>());
+ SelectList selectList = new SelectList(MultiSelectListTest.GetSampleStrings());
+
+ // Act
+ MvcHtmlString html = helper.DropDownListFor(m => m.foo, selectList, String.Empty /* optionLabel */, HtmlHelperTest.AttributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(
+ @"<select baz=""BazObjValue"" id=""foo"" name=""foo""><option value=""""></option>
+<option>Alpha</option>
+<option>Bravo</option>
+<option>Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListForWithObjectDictionaryAndTitle()
+ {
+ // Arrange
+ HtmlHelper<FooModel> helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary<FooModel>());
+ SelectList selectList = new SelectList(MultiSelectListTest.GetSampleStrings());
+
+ // Act
+ MvcHtmlString html = helper.DropDownListFor(m => m.foo, selectList, "[Select Something]", HtmlHelperTest.AttributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(
+ @"<select baz=""BazObjValue"" id=""foo"" name=""foo""><option value="""">[Select Something]</option>
+<option>Alpha</option>
+<option>Bravo</option>
+<option>Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListForWithIEnumerableSelectListItemSelectsDefaultFromViewData()
+ {
+ // Arrange
+ ViewDataDictionary<FooModel> vdd = new ViewDataDictionary<FooModel> { { "foo", "123456789" } };
+ HtmlHelper<FooModel> helper = MvcHelper.GetHtmlHelper(vdd);
+
+ // Act
+ MvcHtmlString html = helper.DropDownListFor(m => m.foo, MultiSelectListTest.GetSampleIEnumerableObjects());
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""foo"" name=""foo""><option selected=""selected"" value=""123456789"">John</option>
+<option value=""987654321"">Jane</option>
+<option value=""111111111"">Joe</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListForWithListOfSelectListItemSelectsDefaultFromViewData()
+ {
+ // Arrange
+ ViewDataDictionary<FooModel> vdd = new ViewDataDictionary<FooModel> { { "foo", "123456789" } };
+ HtmlHelper<FooModel> helper = MvcHelper.GetHtmlHelper(vdd);
+
+ // Act
+ MvcHtmlString html = helper.DropDownListFor(m => m.foo, MultiSelectListTest.GetSampleListObjects());
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""foo"" name=""foo""><option selected=""selected"" value=""123456789"">John</option>
+<option value=""987654321"">Jane</option>
+<option value=""111111111"">Joe</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListForWithPrefix()
+ {
+ // Arrange
+ HtmlHelper<FooModel> helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary<FooModel>());
+ SelectList selectList = new SelectList(MultiSelectListTest.GetSampleStrings());
+ helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix";
+
+ // Act
+ MvcHtmlString html = helper.DropDownListFor(m => m.foo, selectList, HtmlHelperTest.AttributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(
+ @"<select baz=""BazObjValue"" id=""MyPrefix_foo"" name=""MyPrefix.foo""><option>Alpha</option>
+<option>Bravo</option>
+<option>Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListForWithPrefixAndEmptyName()
+ {
+ // Arrange
+ HtmlHelper<FooModel> helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary<FooModel>());
+ SelectList selectList = new SelectList(MultiSelectListTest.GetSampleStrings());
+ helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix";
+
+ // Act
+ MvcHtmlString html = helper.DropDownListFor(m => m, selectList, HtmlHelperTest.AttributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(
+ @"<select baz=""BazObjValue"" id=""MyPrefix"" name=""MyPrefix""><option>Alpha</option>
+<option>Bravo</option>
+<option>Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListForWithPrefixAndIEnumerableSelectListItemSelectsDefaultFromViewData()
+ {
+ // Arrange
+ ViewDataDictionary<FooModel> vdd = new ViewDataDictionary<FooModel> { { "foo", "123456789" } };
+ vdd.TemplateInfo.HtmlFieldPrefix = "MyPrefix";
+ HtmlHelper<FooModel> helper = MvcHelper.GetHtmlHelper(vdd);
+
+ // Act
+ MvcHtmlString html = helper.DropDownListFor(m => m.foo, MultiSelectListTest.GetSampleIEnumerableObjects());
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""MyPrefix_foo"" name=""MyPrefix.foo""><option selected=""selected"" value=""123456789"">John</option>
+<option value=""987654321"">Jane</option>
+<option value=""111111111"">Joe</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ // ListBox
+
+ [Fact]
+ public void ListBoxUsesExplicitValueIfNotProvidedInViewData()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+ MultiSelectList selectList = new MultiSelectList(MultiSelectListTest.GetSampleAnonymousObjects(), "Letter", "FullWord", new[] { "A", "C" });
+
+ // Act
+ MvcHtmlString html = helper.ListBox("foo", selectList);
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""foo"" multiple=""multiple"" name=""foo""><option selected=""selected"" value=""A"">Alpha</option>
+<option value=""B"">Bravo</option>
+<option selected=""selected"" value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxUsesExplicitValueIfNotProvidedInViewData_Unobtrusive()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+ helper.ViewContext.ClientValidationEnabled = true;
+ helper.ViewContext.UnobtrusiveJavaScriptEnabled = true;
+ helper.ViewContext.FormContext = new FormContext();
+ helper.ClientValidationRuleFactory = (name, metadata) => new[] { new ModelClientValidationRule { ValidationType = "type", ErrorMessage = "error" } };
+ MultiSelectList selectList = new MultiSelectList(MultiSelectListTest.GetSampleAnonymousObjects(), "Letter", "FullWord", new[] { "A", "C" });
+
+ // Act
+ MvcHtmlString html = helper.ListBox("foo", selectList);
+
+ // Assert
+ Assert.Equal(
+ @"<select data-val=""true"" data-val-type=""error"" id=""foo"" multiple=""multiple"" name=""foo""><option selected=""selected"" value=""A"">Alpha</option>
+<option value=""B"">Bravo</option>
+<option selected=""selected"" value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxUsesViewDataDefaultValue()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(_listBoxViewData);
+ MultiSelectList selectList = new MultiSelectList(MultiSelectListTest.GetSampleStrings(), new[] { "Charlie" });
+
+ // Act
+ MvcHtmlString html = helper.ListBox("foo", selectList);
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""foo"" multiple=""multiple"" name=""foo""><option>Alpha</option>
+<option selected=""selected"">Bravo</option>
+<option>Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxWithErrors()
+ {
+ // Arrange
+ ViewDataDictionary viewData = GetViewDataWithErrors();
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(viewData);
+ MultiSelectList list = new MultiSelectList(MultiSelectListTest.GetSampleStrings(), new[] { "Charlie" });
+
+ // Act
+ MvcHtmlString html = helper.ListBox("foo", list);
+
+ // Assert
+ Assert.Equal(
+ @"<select class=""input-validation-error"" id=""foo"" multiple=""multiple"" name=""foo""><option>Alpha</option>
+<option selected=""selected"">Bravo</option>
+<option selected=""selected"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxWithErrorsAndCustomClass()
+ {
+ // Arrange
+ ViewDataDictionary viewData = GetViewDataWithErrors();
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(viewData);
+ MultiSelectList selectList = new MultiSelectList(MultiSelectListTest.GetSampleStrings(), new[] { "Charlie" });
+
+ // Act
+ MvcHtmlString html = helper.ListBox("foo", selectList, new { @class = "foo-class" });
+
+ // Assert
+ Assert.Equal(
+ @"<select class=""input-validation-error foo-class"" id=""foo"" multiple=""multiple"" name=""foo""><option>Alpha</option>
+<option selected=""selected"">Bravo</option>
+<option selected=""selected"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxWithNameOnly()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper();
+ helper.ViewData["foo"] = new MultiSelectList(MultiSelectListTest.GetSampleStrings(), new[] { "Charlie" });
+
+ // Act
+ MvcHtmlString html = helper.ListBox("foo");
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""foo"" multiple=""multiple"" name=""foo""><option>Alpha</option>
+<option>Bravo</option>
+<option selected=""selected"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxWithAttributesDictionary()
+ {
+ // Arrange
+ ViewDataDictionary viewData = new ViewDataDictionary();
+ MultiSelectList selectList = new MultiSelectList(MultiSelectListTest.GetSampleStrings());
+ //viewData["foo"] = selectList;
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(viewData);
+
+ // Act
+ MvcHtmlString html = helper.ListBox("foo", selectList, HtmlHelperTest.AttributesDictionary);
+
+ // Assert
+ Assert.Equal(
+ @"<select baz=""BazValue"" id=""foo"" multiple=""multiple"" name=""foo""><option>Alpha</option>
+<option>Bravo</option>
+<option>Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxWithAttributesDictionaryAndMultiSelectList()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+ MultiSelectList selectList = new MultiSelectList(MultiSelectListTest.GetSampleStrings());
+
+ // Act
+ MvcHtmlString html = helper.ListBox("foo", selectList, HtmlHelperTest.AttributesDictionary);
+
+ // Assert
+ Assert.Equal(
+ @"<select baz=""BazValue"" id=""foo"" multiple=""multiple"" name=""foo""><option>Alpha</option>
+<option>Bravo</option>
+<option>Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxWithAttributesDictionaryOverridesName()
+ {
+ // DevDiv Bugs #217602:
+ // SelectInternal() should override the user-provided 'name' attribute
+
+ // Arrange
+ ViewDataDictionary viewData = new ViewDataDictionary();
+ MultiSelectList selectList = new MultiSelectList(MultiSelectListTest.GetSampleStrings());
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(viewData);
+
+ // Act
+ MvcHtmlString html = helper.ListBox("foo", selectList, new { myAttr = "myValue", name = "theName" });
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""foo"" multiple=""multiple"" myAttr=""myValue"" name=""foo""><option>Alpha</option>
+<option>Bravo</option>
+<option>Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxWithEmptyNameThrows()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { helper.ListBox(String.Empty, (MultiSelectList)null /* selectList */); },
+ "name");
+ }
+
+ [Fact]
+ public void ListBoxWithNullNameThrows()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { helper.ListBox(null /* name */, (MultiSelectList)null /* selectList */); },
+ "name");
+ }
+
+ [Fact]
+ public void ListBoxWithNullSelectListUsesViewData()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper();
+ helper.ViewData["foo"] = new MultiSelectList(MultiSelectListTest.GetSampleStrings(), new[] { "Charlie" });
+
+ // Act
+ MvcHtmlString html = helper.ListBox("foo", null);
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""foo"" multiple=""multiple"" name=""foo""><option>Alpha</option>
+<option>Bravo</option>
+<option selected=""selected"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxWithObjectDictionary()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+ MultiSelectList selectList = new MultiSelectList(MultiSelectListTest.GetSampleStrings());
+
+ // Act
+ MvcHtmlString html = helper.ListBox("foo", selectList, HtmlHelperTest.AttributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(
+ @"<select baz=""BazObjValue"" id=""foo"" multiple=""multiple"" name=""foo""><option>Alpha</option>
+<option>Bravo</option>
+<option>Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxWithObjectDictionaryWithUnderscores()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+ MultiSelectList selectList = new MultiSelectList(MultiSelectListTest.GetSampleStrings());
+
+ // Act
+ MvcHtmlString html = helper.ListBox("foo", selectList, HtmlHelperTest.AttributesObjectUnderscoresDictionary);
+
+ // Assert
+ Assert.Equal(
+ @"<select foo-baz=""BazObjValue"" id=""foo"" multiple=""multiple"" name=""foo""><option>Alpha</option>
+<option>Bravo</option>
+<option>Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxWithIEnumerableSelectListItem()
+ {
+ // Arrange
+ ViewDataDictionary vdd = new ViewDataDictionary { { "foo", MultiSelectListTest.GetSampleIEnumerableObjects() } };
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(vdd);
+
+ // Act
+ MvcHtmlString html = helper.ListBox("foo");
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""foo"" multiple=""multiple"" name=""foo""><option value=""123456789"">John</option>
+<option value=""987654321"">Jane</option>
+<option selected=""selected"" value=""111111111"">Joe</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxThrowsWhenExpressionDoesNotEvaluateToIEnumerable()
+ {
+ // Arrange
+ ViewDataDictionary vdd = new ViewDataDictionary { { "foo", 123456789 } };
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(vdd);
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ () => helper.ListBox("foo", MultiSelectListTest.GetSampleIEnumerableObjects()),
+ @"The parameter 'expression' must evaluate to an IEnumerable when multiple selection is allowed."
+ );
+ }
+
+ [Fact]
+ public void ListBoxThrowsWhenExpressionEvaluatesToString()
+ {
+ // Arrange
+ ViewDataDictionary vdd = new ViewDataDictionary { { "foo", "123456789" } };
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(vdd);
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ () => helper.ListBox("foo", MultiSelectListTest.GetSampleIEnumerableObjects()),
+ @"The parameter 'expression' must evaluate to an IEnumerable when multiple selection is allowed."
+ );
+ }
+
+ [Fact]
+ public void ListBoxWithListOfSelectListItemSelectsDefaultFromViewData()
+ {
+ // Arrange
+ ViewDataDictionary vdd = new ViewDataDictionary { { "foo", new string[] { "123456789", "111111111" } } };
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(vdd);
+
+ // Act
+ MvcHtmlString html = helper.ListBox("foo", MultiSelectListTest.GetSampleListObjects());
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""foo"" multiple=""multiple"" name=""foo""><option selected=""selected"" value=""123456789"">John</option>
+<option value=""987654321"">Jane</option>
+<option selected=""selected"" value=""111111111"">Joe</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxWithListOfSelectListItem()
+ {
+ // Arrange
+ ViewDataDictionary vdd = new ViewDataDictionary { { "foo", MultiSelectListTest.GetSampleListObjects() } };
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(vdd);
+
+ // Act
+ MvcHtmlString html = helper.ListBox("foo");
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""foo"" multiple=""multiple"" name=""foo""><option value=""123456789"">John</option>
+<option value=""987654321"">Jane</option>
+<option selected=""selected"" value=""111111111"">Joe</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxWithPrefix()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+ MultiSelectList selectList = new MultiSelectList(MultiSelectListTest.GetSampleStrings());
+ helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix";
+
+ // Act
+ MvcHtmlString html = helper.ListBox("foo", selectList, HtmlHelperTest.AttributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(
+ @"<select baz=""BazObjValue"" id=""MyPrefix_foo"" multiple=""multiple"" name=""MyPrefix.foo""><option>Alpha</option>
+<option>Bravo</option>
+<option>Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxWithPrefixAndEmptyName()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+ MultiSelectList selectList = new MultiSelectList(MultiSelectListTest.GetSampleStrings());
+ helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix";
+
+ // Act
+ MvcHtmlString html = helper.ListBox("", selectList, HtmlHelperTest.AttributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(
+ @"<select baz=""BazObjValue"" id=""MyPrefix"" multiple=""multiple"" name=""MyPrefix""><option>Alpha</option>
+<option>Bravo</option>
+<option>Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxWithPrefixAndNullSelectListUsesViewData()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper();
+ helper.ViewData["foo"] = new MultiSelectList(MultiSelectListTest.GetSampleStrings(), new[] { "Charlie" });
+ helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix";
+
+ // Act
+ MvcHtmlString html = helper.ListBox("foo");
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""MyPrefix_foo"" multiple=""multiple"" name=""MyPrefix.foo""><option>Alpha</option>
+<option>Bravo</option>
+<option selected=""selected"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ // ListBoxFor
+
+ [Fact]
+ public void ListBoxForWithNullExpressionThrows()
+ {
+ // Arrange
+ HtmlHelper<FooModel> helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary<FooModel>());
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => helper.ListBoxFor<FooModel, object>(null, null),
+ "expression");
+ }
+
+ [Fact]
+ public void ListBoxForUsesExplicitValueIfNotProvidedInViewData()
+ {
+ // Arrange
+ HtmlHelper<FooModel> helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary<FooModel>());
+ MultiSelectList selectList = new MultiSelectList(MultiSelectListTest.GetSampleAnonymousObjects(), "Letter", "FullWord", new[] { "A", "C" });
+
+ // Act
+ MvcHtmlString html = helper.ListBoxFor(m => m.foo, selectList);
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""foo"" multiple=""multiple"" name=""foo""><option selected=""selected"" value=""A"">Alpha</option>
+<option value=""B"">Bravo</option>
+<option selected=""selected"" value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxForUsesExplicitValueIfNotProvidedInViewData_Unobtrusive()
+ {
+ // Arrange
+ HtmlHelper<FooModel> helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary<FooModel>());
+ helper.ViewContext.ClientValidationEnabled = true;
+ helper.ViewContext.UnobtrusiveJavaScriptEnabled = true;
+ helper.ViewContext.FormContext = new FormContext();
+ helper.ClientValidationRuleFactory = (name, metadata) => new[] { new ModelClientValidationRule { ValidationType = "type", ErrorMessage = "error" } };
+ MultiSelectList selectList = new MultiSelectList(MultiSelectListTest.GetSampleAnonymousObjects(), "Letter", "FullWord", new[] { "A", "C" });
+
+ // Act
+ MvcHtmlString html = helper.ListBoxFor(m => m.foo, selectList);
+
+ // Assert
+ Assert.Equal(
+ @"<select data-val=""true"" data-val-type=""error"" id=""foo"" multiple=""multiple"" name=""foo""><option selected=""selected"" value=""A"">Alpha</option>
+<option value=""B"">Bravo</option>
+<option selected=""selected"" value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxForWithEnumerableModel_Unobtrusive()
+ {
+ // Arrange
+ HtmlHelper<IEnumerable<RequiredModel>> helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary<IEnumerable<RequiredModel>>());
+ helper.ViewContext.ClientValidationEnabled = true;
+ helper.ViewContext.UnobtrusiveJavaScriptEnabled = true;
+ helper.ViewContext.FormContext = new FormContext();
+ helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix";
+
+ MultiSelectList selectList = new MultiSelectList(MultiSelectListTest.GetSampleAnonymousObjects(), "Letter", "FullWord", "C");
+
+ // Act
+ MvcHtmlString html = helper.ListBoxFor(m => m.ElementAt(0).foo, selectList);
+
+ // Assert
+ Assert.Equal(
+ @"<select data-val=""true"" data-val-required=""The foo field is required."" id=""MyPrefix_foo"" multiple=""multiple"" name=""MyPrefix.foo""><option value=""A"">Alpha</option>
+<option value=""B"">Bravo</option>
+<option selected=""selected"" value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxForUsesViewDataDefaultValue()
+ {
+ // Arrange
+ HtmlHelper<FooModel> helper = MvcHelper.GetHtmlHelper(_listBoxViewData);
+ MultiSelectList selectList = new MultiSelectList(MultiSelectListTest.GetSampleStrings(), new[] { "Charlie" });
+
+ // Act
+ MvcHtmlString html = helper.ListBoxFor(m => m.foo, selectList);
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""foo"" multiple=""multiple"" name=""foo""><option>Alpha</option>
+<option selected=""selected"">Bravo</option>
+<option>Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxForWithErrors()
+ {
+ // Arrange
+ HtmlHelper<FooBarModel> helper = MvcHelper.GetHtmlHelper(GetViewDataWithErrors());
+ MultiSelectList list = new MultiSelectList(MultiSelectListTest.GetSampleStrings(), new[] { "Charlie" });
+
+ // Act
+ MvcHtmlString html = helper.ListBoxFor(m => m.foo, list);
+
+ // Assert
+ Assert.Equal(
+ @"<select class=""input-validation-error"" id=""foo"" multiple=""multiple"" name=""foo""><option>Alpha</option>
+<option selected=""selected"">Bravo</option>
+<option selected=""selected"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxForWithErrorsAndCustomClass()
+ {
+ // Arrange
+ HtmlHelper<FooBarModel> helper = MvcHelper.GetHtmlHelper(GetViewDataWithErrors());
+ MultiSelectList selectList = new MultiSelectList(MultiSelectListTest.GetSampleStrings(), new[] { "Charlie" });
+
+ // Act
+ MvcHtmlString html = helper.ListBoxFor(m => m.foo, selectList, new { @class = "foo-class" });
+
+ // Assert
+ Assert.Equal(
+ @"<select class=""input-validation-error foo-class"" id=""foo"" multiple=""multiple"" name=""foo""><option>Alpha</option>
+<option selected=""selected"">Bravo</option>
+<option selected=""selected"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxForWithAttributesDictionary()
+ {
+ // Arrange
+ MultiSelectList selectList = new MultiSelectList(MultiSelectListTest.GetSampleStrings());
+ HtmlHelper<FooModel> helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary<FooModel>());
+
+ // Act
+ MvcHtmlString html = helper.ListBoxFor(m => m.foo, selectList, HtmlHelperTest.AttributesDictionary);
+
+ // Assert
+ Assert.Equal(
+ @"<select baz=""BazValue"" id=""foo"" multiple=""multiple"" name=""foo""><option>Alpha</option>
+<option>Bravo</option>
+<option>Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxForWithAttributesDictionaryOverridesName()
+ {
+ // DevDiv Bugs #217602:
+ // SelectInternal() should override the user-provided 'name' attribute
+
+ // Arrange
+ MultiSelectList selectList = new MultiSelectList(MultiSelectListTest.GetSampleStrings());
+ HtmlHelper<FooModel> helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary<FooModel>());
+
+ // Act
+ MvcHtmlString html = helper.ListBoxFor(m => m.foo, selectList, new { myAttr = "myValue", name = "theName" });
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""foo"" multiple=""multiple"" myAttr=""myValue"" name=""foo""><option>Alpha</option>
+<option>Bravo</option>
+<option>Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxForWithNullSelectListUsesViewData()
+ {
+ // Arrange
+ HtmlHelper<FooModel> helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary<FooModel>());
+ helper.ViewContext.ViewData["foo"] = new MultiSelectList(MultiSelectListTest.GetSampleStrings(), new[] { "Charlie" });
+
+ // Act
+ MvcHtmlString html = helper.ListBoxFor(m => m.foo, null);
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""foo"" multiple=""multiple"" name=""foo""><option>Alpha</option>
+<option>Bravo</option>
+<option selected=""selected"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxForWithObjectDictionary()
+ {
+ // Arrange
+ HtmlHelper<FooModel> helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary<FooModel>());
+ MultiSelectList selectList = new MultiSelectList(MultiSelectListTest.GetSampleStrings());
+
+ // Act
+ MvcHtmlString html = helper.ListBoxFor(m => m.foo, selectList, HtmlHelperTest.AttributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(
+ @"<select baz=""BazObjValue"" id=""foo"" multiple=""multiple"" name=""foo""><option>Alpha</option>
+<option>Bravo</option>
+<option>Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxForWithObjectDictionaryWithUnderscores()
+ {
+ // Arrange
+ HtmlHelper<FooModel> helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary<FooModel>());
+ MultiSelectList selectList = new MultiSelectList(MultiSelectListTest.GetSampleStrings());
+
+ // Act
+ MvcHtmlString html = helper.ListBoxFor(m => m.foo, selectList, HtmlHelperTest.AttributesObjectUnderscoresDictionary);
+
+ // Assert
+ Assert.Equal(
+ @"<select foo-baz=""BazObjValue"" id=""foo"" multiple=""multiple"" name=""foo""><option>Alpha</option>
+<option>Bravo</option>
+<option>Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxForWithIEnumerableSelectListItem()
+ {
+ // Arrange
+ ViewDataDictionary<FooModel> vdd = new ViewDataDictionary<FooModel> { { "foo", MultiSelectListTest.GetSampleIEnumerableObjects() } };
+ HtmlHelper<FooModel> helper = MvcHelper.GetHtmlHelper(vdd);
+
+ // Act
+ MvcHtmlString html = helper.ListBoxFor(m => m.foo, null);
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""foo"" multiple=""multiple"" name=""foo""><option value=""123456789"">John</option>
+<option value=""987654321"">Jane</option>
+<option selected=""selected"" value=""111111111"">Joe</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxForThrowsWhenExpressionDoesNotEvaluateToIEnumerable()
+ {
+ // Arrange
+ ViewDataDictionary<NonIEnumerableModel> vdd = new ViewDataDictionary<NonIEnumerableModel> { { "foo", 123456789 } };
+ HtmlHelper<NonIEnumerableModel> helper = MvcHelper.GetHtmlHelper(vdd);
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ () => helper.ListBoxFor(m => m.foo, MultiSelectListTest.GetSampleIEnumerableObjects()),
+ @"The parameter 'expression' must evaluate to an IEnumerable when multiple selection is allowed."
+ );
+ }
+
+ [Fact]
+ public void ListBoxForThrowsWhenExpressionEvaluatesToString()
+ {
+ // Arrange
+ ViewDataDictionary<FooModel> vdd = new ViewDataDictionary<FooModel> { { "foo", "123456789" } };
+ HtmlHelper<FooModel> helper = MvcHelper.GetHtmlHelper(vdd);
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ () => helper.ListBoxFor(m => m.foo, MultiSelectListTest.GetSampleIEnumerableObjects()),
+ @"The parameter 'expression' must evaluate to an IEnumerable when multiple selection is allowed."
+ );
+ }
+
+ [Fact]
+ public void ListBoxForWithListOfSelectListItemSelectsDefaultFromViewData()
+ {
+ // Arrange
+ ViewDataDictionary<FooModel> vdd = new ViewDataDictionary<FooModel> { { "foo", new string[] { "123456789", "111111111" } } };
+ HtmlHelper<FooModel> helper = MvcHelper.GetHtmlHelper(vdd);
+
+ // Act
+ MvcHtmlString html = helper.ListBoxFor(m => m.foo, MultiSelectListTest.GetSampleListObjects());
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""foo"" multiple=""multiple"" name=""foo""><option selected=""selected"" value=""123456789"">John</option>
+<option value=""987654321"">Jane</option>
+<option selected=""selected"" value=""111111111"">Joe</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxForWithListOfSelectListItem()
+ {
+ // Arrange
+ ViewDataDictionary<FooModel> vdd = new ViewDataDictionary<FooModel> { { "foo", MultiSelectListTest.GetSampleListObjects() } };
+ HtmlHelper<FooModel> helper = MvcHelper.GetHtmlHelper(vdd);
+
+ // Act
+ MvcHtmlString html = helper.ListBoxFor(m => m.foo, null);
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""foo"" multiple=""multiple"" name=""foo""><option value=""123456789"">John</option>
+<option value=""987654321"">Jane</option>
+<option selected=""selected"" value=""111111111"">Joe</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxForWithPrefix()
+ {
+ // Arrange
+ HtmlHelper<FooModel> helper = MvcHelper.GetHtmlHelper(new ViewDataDictionary<FooModel>());
+ MultiSelectList selectList = new MultiSelectList(MultiSelectListTest.GetSampleStrings());
+ helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix";
+
+ // Act
+ MvcHtmlString html = helper.ListBoxFor(m => m.foo, selectList, HtmlHelperTest.AttributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(
+ @"<select baz=""BazObjValue"" id=""MyPrefix_foo"" multiple=""multiple"" name=""MyPrefix.foo""><option>Alpha</option>
+<option>Bravo</option>
+<option>Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxForWithPrefixAndListOfSelectListItemSelectsDefaultFromViewData()
+ {
+ // Arrange
+ ViewDataDictionary<FooModel> vdd = new ViewDataDictionary<FooModel> { { "foo", new string[] { "123456789", "111111111" } } };
+ HtmlHelper<FooModel> helper = MvcHelper.GetHtmlHelper(vdd);
+ helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix";
+
+ // Act
+ MvcHtmlString html = helper.ListBoxFor(m => m.foo, MultiSelectListTest.GetSampleListObjects());
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""MyPrefix_foo"" multiple=""multiple"" name=""MyPrefix.foo""><option selected=""selected"" value=""123456789"">John</option>
+<option value=""987654321"">Jane</option>
+<option selected=""selected"" value=""111111111"">Joe</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ // Culture tests
+
+ [Fact]
+ public void SelectHelpersUseCurrentCultureToConvertValues()
+ {
+ // Arrange
+ HtmlHelper defaultValueHelper = MvcHelper.GetHtmlHelper(new ViewDataDictionary
+ {
+ { "foo", new[] { new DateTime(1900, 1, 1, 0, 0, 1) } },
+ { "bar", new DateTime(1900, 1, 1, 0, 0, 1) }
+ });
+ HtmlHelper helper = MvcHelper.GetHtmlHelper();
+ SelectList selectList = new SelectList(GetSampleCultureAnonymousObjects(), "Date", "FullWord", new DateTime(1900, 1, 1, 0, 0, 0));
+
+ var tests = new[]
+ {
+ // DropDownList(name, selectList, optionLabel)
+ new
+ {
+ Html = @"<select id=""foo"" name=""foo""><option selected=""selected"" value=""1900/01/01 12:00:00 AM"">Alpha</option>
+<option value=""1900/01/01 12:00:01 AM"">Bravo</option>
+<option value=""1900/01/01 12:00:02 AM"">Charlie</option>
+</select>",
+ Action = new Func<MvcHtmlString>(() => helper.DropDownList("foo", selectList, (string)null))
+ },
+ // DropDownList(name, selectList, optionLabel) (With default value selected from ViewData)
+ new
+ {
+ Html = @"<select id=""bar"" name=""bar""><option value=""1900/01/01 12:00:00 AM"">Alpha</option>
+<option selected=""selected"" value=""1900/01/01 12:00:01 AM"">Bravo</option>
+<option value=""1900/01/01 12:00:02 AM"">Charlie</option>
+</select>",
+ Action = new Func<MvcHtmlString>(() => defaultValueHelper.DropDownList("bar", selectList, (string)null))
+ },
+ // ListBox(name, selectList)
+ new
+ {
+ Html = @"<select id=""foo"" multiple=""multiple"" name=""foo""><option selected=""selected"" value=""1900/01/01 12:00:00 AM"">Alpha</option>
+<option value=""1900/01/01 12:00:01 AM"">Bravo</option>
+<option value=""1900/01/01 12:00:02 AM"">Charlie</option>
+</select>",
+ Action = new Func<MvcHtmlString>(() => helper.ListBox("foo", selectList))
+ },
+ // ListBox(name, selectList) (With default value selected from ViewData)
+ new
+ {
+ Html = @"<select id=""foo"" multiple=""multiple"" name=""foo""><option value=""1900/01/01 12:00:00 AM"">Alpha</option>
+<option selected=""selected"" value=""1900/01/01 12:00:01 AM"">Bravo</option>
+<option value=""1900/01/01 12:00:02 AM"">Charlie</option>
+</select>",
+ Action = new Func<MvcHtmlString>(() => defaultValueHelper.ListBox("foo", selectList))
+ }
+ };
+
+ // Act && Assert
+ using (HtmlHelperTest.ReplaceCulture("en-ZA", "en-US"))
+ {
+ foreach (var test in tests)
+ {
+ Assert.Equal(test.Html, test.Action().ToHtmlString());
+ }
+ }
+ }
+
+ // Helpers
+
+ private class FooModel
+ {
+ public string foo { get; set; }
+ }
+
+ private class FooBarModel : FooModel
+ {
+ public string bar { get; set; }
+ }
+
+ private class NonIEnumerableModel
+ {
+ public int foo { get; set; }
+ }
+
+ private static ViewDataDictionary<FooBarModel> GetViewDataWithErrors()
+ {
+ ViewDataDictionary<FooBarModel> viewData = new ViewDataDictionary<FooBarModel> { { "foo", "ViewDataFoo" } };
+ viewData.Model = new FooBarModel { foo = "ViewItemFoo", bar = "ViewItemBar" };
+
+ ModelState modelStateFoo = new ModelState();
+ modelStateFoo.Errors.Add(new ModelError("foo error 1"));
+ modelStateFoo.Errors.Add(new ModelError("foo error 2"));
+ viewData.ModelState["foo"] = modelStateFoo;
+ modelStateFoo.Value = new ValueProviderResult(new string[] { "Bravo", "Charlie" }, "Bravo", CultureInfo.InvariantCulture);
+
+ return viewData;
+ }
+
+ internal static IEnumerable GetSampleCultureAnonymousObjects()
+ {
+ return new[]
+ {
+ new { Date = new DateTime(1900, 1, 1, 0, 0, 0), FullWord = "Alpha" },
+ new { Date = new DateTime(1900, 1, 1, 0, 0, 1), FullWord = "Bravo" },
+ new { Date = new DateTime(1900, 1, 1, 0, 0, 2), FullWord = "Charlie" }
+ };
+ }
+
+ private class RequiredModel
+ {
+ [Required]
+ public string foo { get; set; }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Html/Test/TemplateHelpersTest.cs b/test/System.Web.Mvc.Test/Html/Test/TemplateHelpersTest.cs
new file mode 100644
index 00000000..e0a795d9
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Html/Test/TemplateHelpersTest.cs
@@ -0,0 +1,1158 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Threading;
+using System.Web.Routing;
+using System.Web.UI.WebControls;
+using Microsoft.Web.UnitTestUtil;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Html.Test
+{
+ public class TemplateHelpersTest
+ {
+ // ExecuteTemplate
+
+ private class ExecuteTemplateModel
+ {
+ public string MyProperty { get; set; }
+ public Nullable<int> MyNullableProperty { get; set; }
+ }
+
+ [Fact]
+ public void ExecuteTemplateCallsGetViewNamesWithProvidedTemplateNameAndMetadataInformation()
+ {
+ using (new MockViewEngine())
+ {
+ // Arrange
+ HtmlHelper html = MakeHtmlHelper(new ExecuteTemplateModel());
+ ModelMetadata metadata = ModelMetadata.FromStringExpression("MyProperty", html.ViewData);
+ metadata.TemplateHint = "templateHint";
+ metadata.DataTypeName = "dataType";
+ string[] hints = null;
+
+ // Act
+ TemplateHelpers.ExecuteTemplate(
+ html, MakeViewData(html, metadata), "templateName", DataBoundControlMode.ReadOnly,
+ (_metadata, _hints) =>
+ {
+ hints = _hints;
+ return new[] { "String" };
+ },
+ TemplateHelpers.GetDefaultActions);
+
+ // Assert
+ Assert.NotNull(hints);
+ Assert.Equal(3, hints.Length);
+ Assert.Equal("templateName", hints[0]);
+ Assert.Equal("templateHint", hints[1]);
+ Assert.Equal("dataType", hints[2]);
+ }
+ }
+
+ [Fact]
+ public void ExecuteTemplateUsesViewFromViewEngineInReadOnlyMode()
+ {
+ using (MockViewEngine engine = new MockViewEngine())
+ {
+ // Arrange
+ HtmlHelper html = MakeHtmlHelper(new ExecuteTemplateModel { MyProperty = "Hello" });
+ ModelMetadata metadata = ModelMetadata.FromStringExpression("MyProperty", html.ViewData);
+ ViewContext callbackViewContext = null;
+ engine.Engine.Setup(e => e.FindPartialView(html.ViewContext, "DisplayTemplates/String", true))
+ .Returns(new ViewEngineResult(engine.View.Object, engine.Engine.Object))
+ .Verifiable();
+ engine.View.Setup(v => v.Render(It.IsAny<ViewContext>(), It.IsAny<TextWriter>()))
+ .Callback<ViewContext, TextWriter>((vc, tw) =>
+ {
+ callbackViewContext = vc;
+ tw.Write("View Text");
+ })
+ .Verifiable();
+ ViewDataDictionary viewData = MakeViewData(html, metadata);
+
+ // Act
+ string result = TemplateHelpers.ExecuteTemplate(
+ html, viewData, "templateName", DataBoundControlMode.ReadOnly,
+ delegate { return new[] { "String" }; },
+ TemplateHelpers.GetDefaultActions);
+
+ // Assert
+ engine.Engine.Verify();
+ engine.View.Verify();
+ Assert.Equal("View Text", result);
+ Assert.Same(engine.View.Object, callbackViewContext.View);
+ Assert.Same(viewData, callbackViewContext.ViewData);
+ Assert.Same(html.ViewContext.TempData, callbackViewContext.TempData);
+ TemplateHelpers.ActionCacheViewItem cacheItem = TemplateHelpers.GetActionCache(html)["DisplayTemplates/String"] as TemplateHelpers.ActionCacheViewItem;
+ Assert.NotNull(cacheItem);
+ Assert.Equal("DisplayTemplates/String", cacheItem.ViewName);
+ }
+ }
+
+ [Fact]
+ public void ExecuteTemplateUsesViewFromViewEngineInEditMode()
+ {
+ using (MockViewEngine engine = new MockViewEngine())
+ {
+ // Arrange
+ HtmlHelper html = MakeHtmlHelper(new ExecuteTemplateModel { MyProperty = "Hello" });
+ ModelMetadata metadata = ModelMetadata.FromStringExpression("MyProperty", html.ViewData);
+ ViewContext callbackViewContext = null;
+ engine.Engine.Setup(e => e.FindPartialView(html.ViewContext, "EditorTemplates/String", true))
+ .Returns(new ViewEngineResult(engine.View.Object, engine.Engine.Object))
+ .Verifiable();
+ engine.View.Setup(v => v.Render(It.IsAny<ViewContext>(), It.IsAny<TextWriter>()))
+ .Callback<ViewContext, TextWriter>((vc, tw) =>
+ {
+ callbackViewContext = vc;
+ tw.Write("View Text");
+ })
+ .Verifiable();
+ ViewDataDictionary viewData = MakeViewData(html, metadata);
+
+ // Act
+ string result = TemplateHelpers.ExecuteTemplate(
+ html, viewData, "templateName", DataBoundControlMode.Edit,
+ delegate { return new[] { "String" }; },
+ TemplateHelpers.GetDefaultActions);
+
+ // Assert
+ engine.Engine.Verify();
+ engine.View.Verify();
+ Assert.Equal("View Text", result);
+ Assert.Same(engine.View.Object, callbackViewContext.View);
+ Assert.Same(viewData, callbackViewContext.ViewData);
+ Assert.Same(html.ViewContext.TempData, callbackViewContext.TempData);
+ TemplateHelpers.ActionCacheViewItem cacheItem = TemplateHelpers.GetActionCache(html)["EditorTemplates/String"] as TemplateHelpers.ActionCacheViewItem;
+ Assert.NotNull(cacheItem);
+ Assert.Equal("EditorTemplates/String", cacheItem.ViewName);
+ }
+ }
+
+ [Fact]
+ public void ExecuteTemplateUsesViewFromDefaultActionsInReadOnlyMode()
+ {
+ using (MockViewEngine engine = new MockViewEngine())
+ {
+ // Arrange
+ HtmlHelper html = MakeHtmlHelper(new ExecuteTemplateModel { MyProperty = "Hello" });
+ ModelMetadata metadata = ModelMetadata.FromStringExpression("MyProperty", html.ViewData);
+ engine.Engine.Setup(e => e.FindPartialView(html.ViewContext, "DisplayTemplates/String", It.IsAny<bool>()))
+ .Returns(new ViewEngineResult(new string[0]))
+ .Verifiable();
+ ViewDataDictionary viewData = MakeViewData(html, metadata);
+
+ // Act
+ TemplateHelpers.ExecuteTemplate(
+ html, viewData, "templateName", DataBoundControlMode.ReadOnly,
+ delegate { return new[] { "String" }; },
+ TemplateHelpers.GetDefaultActions);
+
+ // Assert
+ engine.Engine.Verify();
+ TemplateHelpers.ActionCacheCodeItem cacheItem = TemplateHelpers.GetActionCache(html)["DisplayTemplates/String"] as TemplateHelpers.ActionCacheCodeItem;
+ Assert.NotNull(cacheItem);
+ Assert.Equal(DefaultDisplayTemplates.StringTemplate, cacheItem.Action);
+ }
+ }
+
+ [Fact]
+ public void ExecuteTemplateUsesViewFromDefaultActionsInEditMode()
+ {
+ using (MockViewEngine engine = new MockViewEngine())
+ {
+ // Arrange
+ HtmlHelper html = MakeHtmlHelper(new ExecuteTemplateModel { MyProperty = "Hello" });
+ ModelMetadata metadata = ModelMetadata.FromStringExpression("MyProperty", html.ViewData);
+ engine.Engine.Setup(e => e.FindPartialView(html.ViewContext, "EditorTemplates/String", It.IsAny<bool>()))
+ .Returns(new ViewEngineResult(new string[0]))
+ .Verifiable();
+ ViewDataDictionary viewData = MakeViewData(html, metadata);
+
+ // Act
+ TemplateHelpers.ExecuteTemplate(
+ html, viewData, "templateName", DataBoundControlMode.Edit,
+ delegate { return new[] { "String" }; },
+ TemplateHelpers.GetDefaultActions);
+
+ // Assert
+ engine.Engine.Verify();
+ TemplateHelpers.ActionCacheCodeItem cacheItem = TemplateHelpers.GetActionCache(html)["EditorTemplates/String"] as TemplateHelpers.ActionCacheCodeItem;
+ Assert.NotNull(cacheItem);
+ Assert.Equal(DefaultEditorTemplates.StringTemplate, cacheItem.Action);
+ }
+ }
+
+ [Fact]
+ public void ExecuteTemplatePrefersExistingActionCacheItem()
+ {
+ using (MockViewEngine engine = new MockViewEngine())
+ {
+ // Arrange
+ HtmlHelper html = MakeHtmlHelper(new ExecuteTemplateModel { MyProperty = "Hello" });
+ ModelMetadata metadata = ModelMetadata.FromStringExpression("MyProperty", html.ViewData);
+ ViewDataDictionary viewData = MakeViewData(html, metadata);
+ TemplateHelpers.GetActionCache(html).Add("EditorTemplates/String",
+ new TemplateHelpers.ActionCacheCodeItem { Action = _ => "Action Text" });
+
+ // Act
+ string result = TemplateHelpers.ExecuteTemplate(
+ html, viewData, "templateName", DataBoundControlMode.Edit,
+ delegate { return new[] { "String" }; },
+ TemplateHelpers.GetDefaultActions);
+
+ // Assert
+ engine.Engine.Verify();
+ engine.Engine.Verify(e => e.FindPartialView(It.IsAny<ControllerContext>(), It.IsAny<string>(), It.IsAny<bool>()), Times.Never());
+ Assert.Equal("Action Text", result);
+ }
+ }
+
+ [Fact]
+ public void ExecuteTemplateThrowsWhenNoTemplatesMatch()
+ {
+ using (new MockViewEngine())
+ {
+ // Arrange
+ HtmlHelper html = MakeHtmlHelper(new ExecuteTemplateModel { MyProperty = "Hello" });
+ ModelMetadata metadata = ModelMetadata.FromStringExpression("MyProperty", html.ViewData);
+ ViewDataDictionary viewData = MakeViewData(html, metadata);
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ () => TemplateHelpers.ExecuteTemplate(html, viewData, "templateName", DataBoundControlMode.Edit, delegate { return new string[0]; }, TemplateHelpers.GetDefaultActions),
+ "Unable to locate an appropriate template for type System.String.");
+ }
+ }
+
+ [Fact]
+ public void ExecuteTemplateCreatesNewHtmlHelperWithCorrectViewDataForDefaultAction()
+ {
+ using (MockViewEngine engine = new MockViewEngine(false))
+ {
+ // Arrange
+ HtmlHelper html = MakeHtmlHelper(new ExecuteTemplateModel { MyProperty = "Hello" });
+ ModelMetadata metadata = ModelMetadata.FromStringExpression("MyProperty", html.ViewData);
+ ViewDataDictionary viewData = MakeViewData(html, metadata);
+ HtmlHelper passedHtmlHelper = null;
+
+ // Act
+ TemplateHelpers.ExecuteTemplate(
+ html, viewData, "templateName", DataBoundControlMode.Edit,
+ delegate { return new[] { "String" }; },
+ delegate
+ {
+ return new Dictionary<string, Func<HtmlHelper, string>>
+ {
+ {
+ "String", _htmlHelper =>
+ {
+ passedHtmlHelper = _htmlHelper;
+ return "content";
+ }
+ }
+ };
+ });
+
+ // Assert
+ Assert.NotNull(passedHtmlHelper);
+ Assert.Same(passedHtmlHelper.ViewData, passedHtmlHelper.ViewContext.ViewData);
+ Assert.NotSame(html.ViewData, passedHtmlHelper.ViewData);
+ }
+ }
+
+ [Fact]
+ public void ExecuteTemplateCreatesNewHtmlHelperWithCorrectViewDataForCachedAction()
+ {
+ using (MockViewEngine engine = new MockViewEngine())
+ {
+ // Arrange
+ HtmlHelper html = MakeHtmlHelper(new ExecuteTemplateModel { MyProperty = "Hello" });
+ ModelMetadata metadata = ModelMetadata.FromStringExpression("MyProperty", html.ViewData);
+ ViewDataDictionary viewData = MakeViewData(html, metadata);
+ HtmlHelper passedHtmlHelper = null;
+ TemplateHelpers.GetActionCache(html).Add(
+ "EditorTemplates/String",
+ new TemplateHelpers.ActionCacheCodeItem
+ {
+ Action = _htmlHelper =>
+ {
+ passedHtmlHelper = _htmlHelper;
+ return "content";
+ }
+ });
+
+ // Act
+ string result = TemplateHelpers.ExecuteTemplate(
+ html, viewData, "templateName", DataBoundControlMode.Edit,
+ delegate { return new[] { "String" }; },
+ TemplateHelpers.GetDefaultActions);
+
+ // Assert
+ Assert.NotNull(passedHtmlHelper);
+ Assert.Same(passedHtmlHelper.ViewData, passedHtmlHelper.ViewContext.ViewData);
+ Assert.NotSame(html.ViewData, passedHtmlHelper.ViewData);
+ }
+ }
+
+ // GetActionCache
+
+ [Fact]
+ public void CacheIsCreatedIfNotPresent()
+ {
+ // Arrange
+ Hashtable items = new Hashtable();
+ Mock<HttpContextBase> context = new Mock<HttpContextBase>();
+ context.Setup(c => c.Items).Returns(items);
+ Mock<ViewContext> viewContext = new Mock<ViewContext>();
+ viewContext.Setup(c => c.HttpContext).Returns(context.Object);
+ Mock<IViewDataContainer> viewDataContainer = new Mock<IViewDataContainer>();
+ HtmlHelper helper = new HtmlHelper(viewContext.Object, viewDataContainer.Object);
+
+ // Act
+ Dictionary<string, TemplateHelpers.ActionCacheItem> cache = TemplateHelpers.GetActionCache(helper);
+
+ // Assert
+ Assert.NotNull(cache);
+ Assert.Empty(cache);
+ Assert.Contains((object)cache, items.Values.OfType<object>());
+ }
+
+ [Fact]
+ public void CacheIsReusedIfPresent()
+ {
+ // Arrange
+ Hashtable items = new Hashtable();
+ Mock<HttpContextBase> context = new Mock<HttpContextBase>();
+ context.Setup(c => c.Items).Returns(items);
+ Mock<ViewContext> viewContext = new Mock<ViewContext>();
+ viewContext.Setup(c => c.HttpContext).Returns(context.Object);
+ Mock<IViewDataContainer> viewDataContainer = new Mock<IViewDataContainer>();
+ HtmlHelper helper = new HtmlHelper(viewContext.Object, viewDataContainer.Object);
+
+ // Act
+ Dictionary<string, TemplateHelpers.ActionCacheItem> cache1 = TemplateHelpers.GetActionCache(helper);
+ Dictionary<string, TemplateHelpers.ActionCacheItem> cache2 = TemplateHelpers.GetActionCache(helper);
+
+ // Assert
+ Assert.NotNull(cache1);
+ Assert.Same(cache1, cache2);
+ }
+
+ // GetViewNames
+
+ [Fact]
+ public void GetViewNamesFullOrderingOfBuiltInValueType()
+ {
+ // Arrange
+ ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(double));
+
+ // Act
+ List<string> result = TemplateHelpers.GetViewNames(metadata, "UIHint", "DataType").ToList();
+
+ // Assert
+ Assert.Equal(4, result.Count);
+ Assert.Equal("UIHint", result[0]);
+ Assert.Equal("DataType", result[1]);
+ Assert.Equal("Double", result[2]);
+ Assert.Equal("String", result[3]);
+ }
+
+ [Fact]
+ public void GetViewNamesFullOrderingOfComplexType()
+ {
+ // Arrange
+ ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(HttpWebRequest));
+
+ // Act
+ List<string> result = TemplateHelpers.GetViewNames(metadata, "UIHint", "DataType").ToList();
+
+ // Assert
+ Assert.Equal(6, result.Count);
+ Assert.Equal("UIHint", result[0]);
+ Assert.Equal("DataType", result[1]);
+ Assert.Equal("HttpWebRequest", result[2]);
+ Assert.Equal("WebRequest", result[3]);
+ Assert.Equal("MarshalByRefObject", result[4]);
+ Assert.Equal("Object", result[5]);
+ }
+
+ [Fact]
+ public void GetViewNamesFullOrderingOfInterface()
+ {
+ // Arrange
+ ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(IDisposable));
+
+ // Act
+ List<string> result = TemplateHelpers.GetViewNames(metadata, "UIHint", "DataType").ToList();
+
+ // Assert
+ Assert.Equal(4, result.Count);
+ Assert.Equal("UIHint", result[0]);
+ Assert.Equal("DataType", result[1]);
+ Assert.Equal("IDisposable", result[2]);
+ Assert.Equal("Object", result[3]);
+ }
+
+ [Fact]
+ public void GetViewNamesFullOrderingOfComplexTypeThatImplementsIEnumerable()
+ {
+ // Arrange
+ ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(List<int>));
+
+ // Act
+ List<string> result = TemplateHelpers.GetViewNames(metadata, "UIHint", "DataType").ToList();
+
+ // Assert
+ Assert.Equal(5, result.Count);
+ Assert.Equal("UIHint", result[0]);
+ Assert.Equal("DataType", result[1]);
+ Assert.Equal("List`1", result[2]);
+ Assert.Equal("Collection", result[3]);
+ Assert.Equal("Object", result[4]);
+ }
+
+ [Fact]
+ public void GetViewNamesFullOrderingOfInterfaceThatRequiresIEnumerable()
+ {
+ // Arrange
+ ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(IList<int>));
+
+ // Act
+ List<string> result = TemplateHelpers.GetViewNames(metadata, "UIHint", "DataType").ToList();
+
+ // Assert
+ Assert.Equal(5, result.Count);
+ Assert.Equal("UIHint", result[0]);
+ Assert.Equal("DataType", result[1]);
+ Assert.Equal("IList`1", result[2]);
+ Assert.Equal("Collection", result[3]);
+ Assert.Equal("Object", result[4]);
+ }
+
+ [Fact]
+ public void GetViewNamesNullUIHintNotIncludedInList()
+ {
+ // Arrange
+ ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(Object));
+
+ // Act
+ List<string> result = TemplateHelpers.GetViewNames(metadata, null, "DataType").ToList();
+
+ // Assert
+ Assert.Equal(2, result.Count);
+ Assert.Equal("DataType", result[0]);
+ Assert.Equal("Object", result[1]);
+ }
+
+ [Fact]
+ public void GetViewNamesNullDataTypeNotIncludedInList()
+ {
+ // Arrange
+ ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(Object));
+
+ // Act
+ List<string> result = TemplateHelpers.GetViewNames(metadata, "UIHint", null).ToList();
+
+ // Assert
+ Assert.Equal(2, result.Count);
+ Assert.Equal("UIHint", result[0]);
+ Assert.Equal("Object", result[1]);
+ }
+
+ [Fact]
+ public void GetViewNamesConvertsNullableOfTIntoT()
+ {
+ // Arrange
+ ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(Nullable<int>));
+
+ // Act
+ List<string> result = TemplateHelpers.GetViewNames(metadata, null, null).ToList();
+
+ // Assert
+ Assert.Equal(2, result.Count);
+ Assert.Equal("Int32", result[0]);
+ Assert.Equal("String", result[1]);
+ }
+
+ // Template
+
+ private class TemplateModel
+ {
+ public object MyProperty { get; set; }
+ }
+
+ [Fact]
+ public void TemplateNullExpressionThrows()
+ {
+ // Arrange
+ HtmlHelper<object> html = MakeHtmlHelper<object>(null);
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => TemplateHelpers.Template(html, null, "templateName", "htmlFieldName", DataBoundControlMode.ReadOnly,
+ null /* additionalViewData */, TemplateHelperSpy),
+ "expression");
+ }
+
+ [Fact]
+ public void TemplateDataNotFound()
+ {
+ // Arrange
+ HtmlHelper<object> html = MakeHtmlHelper<object>(null);
+
+ // Act
+ string result = TemplateHelpers.Template(html, "UnknownObject", "templateName", null, DataBoundControlMode.ReadOnly,
+ null /* additionalViewData */, TemplateHelperSpy);
+
+ // Assert
+ Assert.Equal("Model = (null), ModelType = System.String, RealModelType = System.String, PropertyName = (null), HtmlFieldName = UnknownObject, TemplateName = templateName, Mode = ReadOnly, AdditionalViewData = (null)", result);
+ }
+
+ [Fact]
+ public void TemplateHtmlFieldNameReplacesExpression()
+ {
+ // Arrange
+ HtmlHelper<object> html = MakeHtmlHelper<object>(null);
+
+ // Act
+ string result = TemplateHelpers.Template(html, "UnknownObject", "templateName", "htmlFieldName", DataBoundControlMode.ReadOnly,
+ new { foo = "Bar" }, TemplateHelperSpy);
+
+ // Assert
+ Assert.Equal("Model = (null), ModelType = System.String, RealModelType = System.String, PropertyName = (null), HtmlFieldName = htmlFieldName, TemplateName = templateName, Mode = ReadOnly, AdditionalViewData = { foo: Bar }", result);
+ }
+
+ [Fact]
+ public void TemplateDataFoundInViewDataDictionaryHasNoPropertyName()
+ {
+ // Arrange
+ HtmlHelper<object> html = MakeHtmlHelper<object>(null);
+ html.ViewContext.ViewData["Key"] = 42;
+
+ // Act
+ string result = TemplateHelpers.Template(html, "Key", null, null, DataBoundControlMode.Edit, null /* additionalViewData */, TemplateHelperSpy);
+
+ // Assert
+ Assert.Equal("Model = 42, ModelType = System.Int32, RealModelType = System.Int32, PropertyName = (null), HtmlFieldName = Key, TemplateName = (null), Mode = Edit, AdditionalViewData = (null)", result);
+ }
+
+ [Fact]
+ public void TemplateDataFoundInViewDataDictionarySubPropertyHasPropertyName()
+ {
+ // Arrange
+ HtmlHelper<object> html = MakeHtmlHelper<object>(null);
+ html.ViewContext.ViewData["Key"] = new TemplateModel { MyProperty = "Hello!" };
+
+ // Act
+ string result = TemplateHelpers.Template(html, "Key.MyProperty", null, null, DataBoundControlMode.ReadOnly,
+ null /* additionalViewData */, TemplateHelperSpy);
+
+ // Assert
+ Assert.Equal("Model = Hello!, ModelType = System.Object, RealModelType = System.String, PropertyName = MyProperty, HtmlFieldName = Key.MyProperty, TemplateName = (null), Mode = ReadOnly, AdditionalViewData = (null)", result);
+ }
+
+ [Fact]
+ public void TemplateDataFoundInModelHasPropertyName()
+ {
+ // Arrange
+ HtmlHelper<TemplateModel> html = MakeHtmlHelper<TemplateModel>(new TemplateModel { MyProperty = "Hello!" });
+
+ // Act
+ string result = TemplateHelpers.Template(html, "MyProperty", null, null, DataBoundControlMode.ReadOnly,
+ null /* additionalViewData */, TemplateHelperSpy);
+
+ // Assert
+ Assert.Equal("Model = Hello!, ModelType = System.Object, RealModelType = System.String, PropertyName = MyProperty, HtmlFieldName = MyProperty, TemplateName = (null), Mode = ReadOnly, AdditionalViewData = (null)", result);
+ }
+
+ [Fact]
+ public void TemplateNullDataFoundInModelHasPropertyTypeInsteadOfActualModelType()
+ {
+ // Arrange
+ HtmlHelper<TemplateModel> html = MakeHtmlHelper<TemplateModel>(new TemplateModel());
+
+ // Act
+ string result = TemplateHelpers.Template(html, "MyProperty", null, null, DataBoundControlMode.ReadOnly,
+ null /* additionalViewData */, TemplateHelperSpy);
+
+ // Assert
+ Assert.Equal("Model = (null), ModelType = System.Object, RealModelType = System.Object, PropertyName = MyProperty, HtmlFieldName = MyProperty, TemplateName = (null), Mode = ReadOnly, AdditionalViewData = (null)", result);
+ }
+
+ // TemplateFor
+
+ private class TemplateForModel
+ {
+ public int MyField = 0;
+ public object MyProperty { get; set; }
+ public Nullable<int> MyNullableProperty { get; set; }
+ }
+
+ [Fact]
+ public void TemplateForNonUnsupportedExpressionTypeThrows()
+ {
+ // Arrange
+ HtmlHelper<TemplateForModel> html = MakeHtmlHelper<TemplateForModel>(null);
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ () => TemplateHelpers.TemplateFor(html, model => new Object(), "templateName", "htmlFieldName", DataBoundControlMode.ReadOnly,
+ null /* additionalViewData */, TemplateHelperSpy),
+ "Templates can be used only with field access, property access, single-dimension array index, or single-parameter custom indexer expressions.");
+ }
+
+ [Fact]
+ public void TemplateForWithNonNullExpression()
+ {
+ // Arrange
+ HtmlHelper<TemplateForModel> html = MakeHtmlHelper(new TemplateForModel { MyProperty = "Hello!" });
+
+ // Act
+ string result = TemplateHelpers.TemplateFor(html, model => model.MyProperty, "templateName", null, DataBoundControlMode.ReadOnly,
+ new { foo = "Bar" }, TemplateHelperSpy);
+
+ // Assert
+ Assert.Equal("Model = Hello!, ModelType = System.Object, RealModelType = System.String, PropertyName = MyProperty, HtmlFieldName = MyProperty, TemplateName = templateName, Mode = ReadOnly, AdditionalViewData = { foo: Bar }", result);
+ }
+
+ [Fact]
+ public void TemplateForHtmlFieldNameReplacesExpression()
+ {
+ // Arrange
+ HtmlHelper<TemplateForModel> html = MakeHtmlHelper(new TemplateForModel { MyProperty = "Hello!" });
+
+ // Act
+ string result = TemplateHelpers.TemplateFor(html, model => model.MyProperty, "templateName", "htmlFieldName",
+ DataBoundControlMode.ReadOnly, null /* additionalViewData */, TemplateHelperSpy);
+
+ // Assert
+ Assert.Equal("Model = Hello!, ModelType = System.Object, RealModelType = System.String, PropertyName = MyProperty, HtmlFieldName = htmlFieldName, TemplateName = templateName, Mode = ReadOnly, AdditionalViewData = (null)", result);
+ }
+
+ [Fact]
+ public void TemplateForNullModelStillRetainsTypeInformation()
+ {
+ // Arrange
+ HtmlHelper<TemplateForModel> html = MakeHtmlHelper<TemplateForModel>(null);
+
+ // Act
+ string result = TemplateHelpers.TemplateFor(html, model => model.MyProperty, null, null, DataBoundControlMode.ReadOnly,
+ null /* additionalViewData */, TemplateHelperSpy);
+
+ // Assert
+ Assert.Equal("Model = (null), ModelType = System.Object, RealModelType = System.Object, PropertyName = MyProperty, HtmlFieldName = MyProperty, TemplateName = (null), Mode = ReadOnly, AdditionalViewData = (null)", result);
+ }
+
+ [Fact]
+ public void TemplateForNullPropertyStillRetainsTypeInformation()
+ {
+ // Arrange
+ HtmlHelper<TemplateForModel> html = MakeHtmlHelper(new TemplateForModel());
+
+ // Act
+ string result = TemplateHelpers.TemplateFor(html, model => model.MyProperty, null, null, DataBoundControlMode.ReadOnly,
+ null /* additionalViewData */, TemplateHelperSpy);
+
+ // Assert
+ Assert.Equal("Model = (null), ModelType = System.Object, RealModelType = System.Object, PropertyName = MyProperty, HtmlFieldName = MyProperty, TemplateName = (null), Mode = ReadOnly, AdditionalViewData = (null)", result);
+ }
+
+ [Fact]
+ public void TemplateForNullableValueTYpePropertyRetainsNullableValueTYpeForNullPropertyValue()
+ {
+ // Arrange
+ HtmlHelper<TemplateForModel> html = MakeHtmlHelper(new TemplateForModel());
+
+ // Act
+ string result = TemplateHelpers.TemplateFor(html, model => model.MyNullableProperty, null, null, DataBoundControlMode.ReadOnly,
+ null /* additionalViewData */, TemplateHelperSpy);
+
+ // Assert
+ Assert.Equal("Model = (null), ModelType = System.Nullable`1[[System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], RealModelType = System.Nullable`1[[System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], PropertyName = MyNullableProperty, HtmlFieldName = MyNullableProperty, TemplateName = (null), Mode = ReadOnly, AdditionalViewData = (null)", result);
+ }
+
+ [Fact]
+ public void TemplateForNullableValueTypePropertyRetainsNullableValueTypeForNonNullPropertyValue()
+ {
+ // Arrange
+ HtmlHelper<TemplateForModel> html = MakeHtmlHelper(new TemplateForModel { MyNullableProperty = 42 });
+
+ // Act
+ string result = TemplateHelpers.TemplateFor(html, model => model.MyNullableProperty, null, null, DataBoundControlMode.ReadOnly,
+ null /* additionalViewData */, TemplateHelperSpy);
+
+ // Assert
+ Assert.Equal("Model = 42, ModelType = System.Nullable`1[[System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], RealModelType = System.Nullable`1[[System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], PropertyName = MyNullableProperty, HtmlFieldName = MyNullableProperty, TemplateName = (null), Mode = ReadOnly, AdditionalViewData = (null)", result);
+ }
+
+ [Fact]
+ public void TemplateForWithParameterExpression()
+ {
+ // Arrange
+ HtmlHelper<TemplateForModel> html = MakeHtmlHelper(new TemplateForModel());
+
+ // Act
+ string result = TemplateHelpers.TemplateFor(html, model => model, null, null, DataBoundControlMode.ReadOnly,
+ null /* additionalViewData */, TemplateHelperSpy);
+
+ // Assert
+ Assert.Equal("Model = System.Web.Mvc.Html.Test.TemplateHelpersTest+TemplateForModel, ModelType = System.Web.Mvc.Html.Test.TemplateHelpersTest+TemplateForModel, RealModelType = System.Web.Mvc.Html.Test.TemplateHelpersTest+TemplateForModel, PropertyName = (null), HtmlFieldName = (empty), TemplateName = (null), Mode = ReadOnly, AdditionalViewData = (null)", result);
+ }
+
+ [Fact]
+ public void TemplateForWithFieldExpression()
+ {
+ // Arrange
+ HtmlHelper<TemplateForModel> html = MakeHtmlHelper(new TemplateForModel { MyField = 42 });
+
+ // Act
+ string result = TemplateHelpers.TemplateFor(html, model => model.MyField, null, null, DataBoundControlMode.ReadOnly,
+ null /* additionalViewData */, TemplateHelperSpy);
+
+ // Assert
+ Assert.Equal("Model = 42, ModelType = System.Int32, RealModelType = System.Int32, PropertyName = (null), HtmlFieldName = MyField, TemplateName = (null), Mode = ReadOnly, AdditionalViewData = (null)", result);
+ }
+
+ // TemplateHelper
+
+ private class TemplateHelperModel
+ {
+ public string MyProperty { get; set; }
+ }
+
+ [Fact]
+ public void TemplateHelperNonNullNonEmptyStringModel()
+ {
+ // Arrange
+ HtmlHelper html = MakeHtmlHelper(new TemplateHelperModel { MyProperty = "Hello" });
+ ModelMetadata metadata = ModelMetadata.FromStringExpression("MyProperty", html.ViewData);
+
+ // Act
+ string result = TemplateHelpers.TemplateHelper(html, metadata, "htmlFieldName", "templateName", DataBoundControlMode.ReadOnly,
+ null /* additionalViewData */, ExecuteTemplateSpy);
+
+ // Assert
+ Assert.Equal("Model = Hello, ModelType = System.String, RealModelType = System.String, PropertyName = MyProperty, FormattedModelValue = Hello, HtmlFieldPrefix = FieldPrefix.htmlFieldName, TemplateName = templateName, Mode = ReadOnly", result);
+ }
+
+ [Fact]
+ public void TemplateHelperEmptyStringModel()
+ {
+ // Arrange
+ HtmlHelper html = MakeHtmlHelper(new TemplateHelperModel { MyProperty = "" });
+ ModelMetadata metadata = ModelMetadata.FromStringExpression("MyProperty", html.ViewData);
+ metadata.ConvertEmptyStringToNull = false;
+
+ // Act
+ string result = TemplateHelpers.TemplateHelper(html, metadata, "htmlFieldName", "templateName", DataBoundControlMode.ReadOnly,
+ null /* additionalViewData */, ExecuteTemplateSpy);
+
+ // Assert
+ Assert.Equal("Model = , ModelType = System.String, RealModelType = System.String, PropertyName = MyProperty, FormattedModelValue = , HtmlFieldPrefix = FieldPrefix.htmlFieldName, TemplateName = templateName, Mode = ReadOnly", result);
+ }
+
+ [Fact]
+ public void TemplateHelperConvertsEmptyStringsToNull()
+ {
+ // Arrange
+ HtmlHelper html = MakeHtmlHelper(new TemplateHelperModel { MyProperty = "" });
+ ModelMetadata metadata = ModelMetadata.FromStringExpression("MyProperty", html.ViewData);
+ metadata.ConvertEmptyStringToNull = true;
+
+ // Act
+ string result = TemplateHelpers.TemplateHelper(html, metadata, "htmlFieldName", "templateName", DataBoundControlMode.ReadOnly,
+ null /* additionalViewData */, ExecuteTemplateSpy);
+
+ // Assert
+ Assert.Equal("Model = (null), ModelType = System.String, RealModelType = System.String, PropertyName = MyProperty, FormattedModelValue = , HtmlFieldPrefix = FieldPrefix.htmlFieldName, TemplateName = templateName, Mode = ReadOnly", result);
+ }
+
+ [Fact]
+ public void TemplateHelperConvertsNullModelsToNullDisplayTextInReadOnlyMode()
+ {
+ // Arrange
+ HtmlHelper html = MakeHtmlHelper(new TemplateHelperModel());
+ ModelMetadata metadata = ModelMetadata.FromStringExpression("MyProperty", html.ViewData);
+ metadata.NullDisplayText = "NullDisplayText";
+
+ // Act
+ string result = TemplateHelpers.TemplateHelper(html, metadata, "htmlFieldName", "templateName", DataBoundControlMode.ReadOnly,
+ null /* additionalViewData */, ExecuteTemplateSpy);
+
+ // Assert
+ Assert.Equal("Model = (null), ModelType = System.String, RealModelType = System.String, PropertyName = MyProperty, FormattedModelValue = NullDisplayText, HtmlFieldPrefix = FieldPrefix.htmlFieldName, TemplateName = templateName, Mode = ReadOnly", result);
+ }
+
+ [Fact]
+ public void TemplateHelperDoesNotConvertNullModelsToNullDisplayTextInEditMode()
+ {
+ // Arrange
+ HtmlHelper html = MakeHtmlHelper(new TemplateHelperModel());
+ ModelMetadata metadata = ModelMetadata.FromStringExpression("MyProperty", html.ViewData);
+ metadata.NullDisplayText = "NullDisplayText";
+
+ // Act
+ string result = TemplateHelpers.TemplateHelper(html, metadata, "htmlFieldName", "templateName", DataBoundControlMode.Edit,
+ null /* additionalViewData */, ExecuteTemplateSpy);
+
+ // Assert
+ Assert.Equal("Model = (null), ModelType = System.String, RealModelType = System.String, PropertyName = MyProperty, FormattedModelValue = , HtmlFieldPrefix = FieldPrefix.htmlFieldName, TemplateName = templateName, Mode = Edit", result);
+ }
+
+ [Fact]
+ public void TemplateHelperAppliesDisplayFormatStringInReadOnlyMode()
+ {
+ // Arrange
+ HtmlHelper html = MakeHtmlHelper(new TemplateHelperModel { MyProperty = "Hello" });
+ ModelMetadata metadata = ModelMetadata.FromStringExpression("MyProperty", html.ViewData);
+ metadata.DisplayFormatString = "{0} world!";
+
+ // Act
+ string result = TemplateHelpers.TemplateHelper(html, metadata, "htmlFieldName", "templateName", DataBoundControlMode.ReadOnly,
+ null /* additionalViewData */, ExecuteTemplateSpy);
+
+ // Assert
+ Assert.Equal("Model = Hello, ModelType = System.String, RealModelType = System.String, PropertyName = MyProperty, FormattedModelValue = Hello world!, HtmlFieldPrefix = FieldPrefix.htmlFieldName, TemplateName = templateName, Mode = ReadOnly", result);
+ }
+
+ [Fact]
+ public void TemplateHelperDoesNotApplyDisplayFormatStringInReadOnlyModeForNullModel()
+ {
+ // Arrange
+ HtmlHelper html = MakeHtmlHelper(new TemplateHelperModel());
+ ModelMetadata metadata = ModelMetadata.FromStringExpression("MyProperty", html.ViewData);
+ metadata.DisplayFormatString = "{0} world!";
+
+ // Act
+ string result = TemplateHelpers.TemplateHelper(html, metadata, "htmlFieldName", "templateName", DataBoundControlMode.ReadOnly,
+ null /* additionalViewData */, ExecuteTemplateSpy);
+
+ // Assert
+ Assert.Equal("Model = (null), ModelType = System.String, RealModelType = System.String, PropertyName = MyProperty, FormattedModelValue = , HtmlFieldPrefix = FieldPrefix.htmlFieldName, TemplateName = templateName, Mode = ReadOnly", result);
+ }
+
+ [Fact]
+ public void TemplateHelperAppliesEditFormatStringInEditMode()
+ {
+ HtmlHelper html = MakeHtmlHelper(new TemplateHelperModel { MyProperty = "Hello" });
+ ModelMetadata metadata = ModelMetadata.FromStringExpression("MyProperty", html.ViewData);
+ metadata.EditFormatString = "{0} world!";
+
+ // Act
+ string result = TemplateHelpers.TemplateHelper(html, metadata, "htmlFieldName", "templateName", DataBoundControlMode.Edit,
+ null /* additionalViewData */, ExecuteTemplateSpy);
+
+ // Assert
+ Assert.Equal("Model = Hello, ModelType = System.String, RealModelType = System.String, PropertyName = MyProperty, FormattedModelValue = Hello world!, HtmlFieldPrefix = FieldPrefix.htmlFieldName, TemplateName = templateName, Mode = Edit", result);
+ }
+
+ [Fact]
+ public void TemplateHelperDoesNotApplyEditFormatStringInEditModeForNullModel()
+ {
+ // Arrange
+ HtmlHelper html = MakeHtmlHelper(new TemplateHelperModel());
+ ModelMetadata metadata = ModelMetadata.FromStringExpression("MyProperty", html.ViewData);
+ metadata.EditFormatString = "{0} world!";
+
+ // Act
+ string result = TemplateHelpers.TemplateHelper(html, metadata, "htmlFieldName", "templateName", DataBoundControlMode.Edit,
+ null /* additionalViewData */, ExecuteTemplateSpy);
+
+ // Assert
+ Assert.Equal("Model = (null), ModelType = System.String, RealModelType = System.String, PropertyName = MyProperty, FormattedModelValue = , HtmlFieldPrefix = FieldPrefix.htmlFieldName, TemplateName = templateName, Mode = Edit", result);
+ }
+
+ [Fact]
+ public void TemplateHelperAddsNonNullModelToVisitedObjects()
+ { // DDB #224750
+ HtmlHelper html = MakeHtmlHelper(new TemplateHelperModel { MyProperty = "Hello" });
+ ModelMetadata metadata = ModelMetadata.FromStringExpression("MyProperty", html.ViewData);
+ ViewDataDictionary viewData = null;
+
+ // Act
+ TemplateHelpers.TemplateHelper(
+ html, metadata, "htmlFieldName", "templateName", DataBoundControlMode.ReadOnly, null /* additionalViewData */,
+ (_html, _viewData, _templateName, _mode, _getViews, _getDefaultActions) =>
+ {
+ viewData = _viewData;
+ return String.Empty;
+ });
+
+ // Assert
+ Assert.NotNull(viewData);
+ Assert.True(viewData.TemplateInfo.VisitedObjects.Contains("Hello"));
+ }
+
+ [Fact]
+ public void TemplateHelperAddsNullModelsTypeToVisitedObjects()
+ { // DDB #224750
+ HtmlHelper html = MakeHtmlHelper(new TemplateHelperModel());
+ ModelMetadata metadata = ModelMetadata.FromStringExpression("MyProperty", html.ViewData);
+ ViewDataDictionary viewData = null;
+
+ // Act
+ TemplateHelpers.TemplateHelper(
+ html, metadata, "htmlFieldName", "templateName", DataBoundControlMode.ReadOnly, null /* additionalViewData */,
+ (_html, _viewData, _templateName, _mode, _getViews, _getDefaultActions) =>
+ {
+ viewData = _viewData;
+ return String.Empty;
+ });
+
+ // Assert
+ Assert.NotNull(viewData);
+ Assert.True(viewData.TemplateInfo.VisitedObjects.Contains(typeof(string)));
+ }
+
+ [Fact]
+ public void TemplateHelperReturnsEmptyStringForAlreadyVisitedObject()
+ { // DDB #224750
+ HtmlHelper html = MakeHtmlHelper(new TemplateHelperModel { MyProperty = "Hello" });
+ ModelMetadata metadata = ModelMetadata.FromStringExpression("MyProperty", html.ViewData);
+ html.ViewData.TemplateInfo.VisitedObjects.Add("Hello");
+
+ // Act
+ string result = TemplateHelpers.TemplateHelper(html, metadata, "htmlFieldName", "templateName", DataBoundControlMode.ReadOnly,
+ null /* additionalViewData */);
+
+ // Assert
+ Assert.Equal(String.Empty, result);
+ }
+
+ [Fact]
+ public void TemplateHelperReturnsEmptyStringForAlreadyVisitedType()
+ { // DDB #224750
+ HtmlHelper html = MakeHtmlHelper(new TemplateHelperModel());
+ ModelMetadata metadata = ModelMetadata.FromStringExpression("MyProperty", html.ViewData);
+ html.ViewData.TemplateInfo.VisitedObjects.Add(typeof(string));
+
+ // Act
+ string result = TemplateHelpers.TemplateHelper(html, metadata, "htmlFieldName", "templateName", DataBoundControlMode.ReadOnly,
+ null /* additionalViewData */);
+
+ // Assert
+ Assert.Equal(String.Empty, result);
+ }
+
+ [Fact]
+ public void TemplateHelperPreservesSameInstanceOfModelMetadata()
+ { // DDB #225858
+ HtmlHelper html = MakeHtmlHelper(new TemplateHelperModel());
+ ModelMetadata metadata = ModelMetadata.FromStringExpression("MyProperty", html.ViewData);
+ ViewDataDictionary callbackViewData = null;
+
+ // Act
+ string result = TemplateHelpers.TemplateHelper(html, metadata, "htmlFieldName", "templateName", DataBoundControlMode.ReadOnly,
+ null /* additionalViewData */,
+ (_html, _viewData, _templateName, _mode, _getViewNames, _getDefaultActions) =>
+ {
+ callbackViewData = _viewData;
+ return String.Empty;
+ });
+
+ // Assert
+ Assert.NotNull(callbackViewData);
+ Assert.Same(metadata, callbackViewData.ModelMetadata);
+ }
+
+ [Fact]
+ public void TemplateHelperFormatsValuesUsingCurrentCulture()
+ {
+ CultureInfo existingCulture = Thread.CurrentThread.CurrentCulture;
+
+ try
+ {
+ // Arrange
+ Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo("es-PR");
+ HtmlHelper html = MakeHtmlHelper(new { MyProperty = new DateTime(2009, 11, 18, 16, 12, 8, DateTimeKind.Utc) });
+ ModelMetadata metadata = ModelMetadata.FromStringExpression("MyProperty", html.ViewData);
+ metadata.DisplayFormatString = "{0:F}";
+
+ // Act
+ string result = TemplateHelpers.TemplateHelper(html, metadata, "htmlFieldName", "templateName", DataBoundControlMode.ReadOnly,
+ null /* additionalViewData */, ExecuteTemplateSpy);
+
+ // Assert
+ Assert.Equal("Model = 18/11/2009 04:12:08 p.m., ModelType = System.DateTime, RealModelType = System.DateTime, PropertyName = MyProperty, FormattedModelValue = miércoles, 18 de noviembre de 2009 04:12:08 p.m., HtmlFieldPrefix = FieldPrefix.htmlFieldName, TemplateName = templateName, Mode = ReadOnly", result);
+ }
+ finally
+ {
+ Thread.CurrentThread.CurrentCulture = existingCulture;
+ }
+ }
+
+ [Fact]
+ public void TemplateHelperPreservesExistingViewData()
+ {
+ HtmlHelper html = MakeHtmlHelper(new TemplateHelperModel());
+ html.ViewData["Foo"] = "Bar";
+ html.ViewData["Baz"] = 42;
+ ModelMetadata metadata = ModelMetadata.FromStringExpression("MyProperty", html.ViewData);
+ ViewDataDictionary callbackViewData = null;
+
+ // Act
+ string result = TemplateHelpers.TemplateHelper(html, metadata, "htmlFieldName", "templateName", DataBoundControlMode.ReadOnly,
+ null /* additionalViewData */,
+ (_html, _viewData, _templateName, _mode, _getViewNames, _getDefaultActions) =>
+ {
+ callbackViewData = _viewData;
+ return String.Empty;
+ });
+
+ // Assert
+ Assert.NotSame(html.ViewData, callbackViewData);
+ Assert.Equal(2, callbackViewData.Count);
+ Assert.Equal("Bar", callbackViewData["Foo"]);
+ Assert.Equal(42, callbackViewData["Baz"]);
+ }
+
+ [Fact]
+ public void TemplateHelperMergesAdditionalViewData()
+ {
+ HtmlHelper html = MakeHtmlHelper(new TemplateHelperModel());
+ html.ViewData["Foo"] = "Bar";
+ html.ViewData["Baz"] = 42;
+ ModelMetadata metadata = ModelMetadata.FromStringExpression("MyProperty", html.ViewData);
+ ViewDataDictionary callbackViewData = null;
+
+ // Act
+ string result = TemplateHelpers.TemplateHelper(html, metadata, "htmlFieldName", "templateName", DataBoundControlMode.ReadOnly,
+ new { foo = "New Foo", hello = "World!" },
+ (_html, _viewData, _templateName, _mode, _getViewNames, _getDefaultActions) =>
+ {
+ callbackViewData = _viewData;
+ return String.Empty;
+ });
+
+ // Assert
+ Assert.NotSame(html.ViewData, callbackViewData);
+ Assert.Equal(3, callbackViewData.Count);
+ Assert.Equal("New Foo", callbackViewData["Foo"]);
+ Assert.Equal(42, callbackViewData["Baz"]);
+ Assert.Equal("World!", callbackViewData["Hello"]);
+ }
+
+ // Helpers
+
+ private static string ExecuteTemplateSpy(HtmlHelper html, ViewDataDictionary viewData, string templateName, DataBoundControlMode mode,
+ TemplateHelpers.GetViewNamesDelegate getViewNames, TemplateHelpers.GetDefaultActionsDelegate getDefaultActions)
+ {
+ Assert.Same(viewData.Model, viewData.ModelMetadata.Model);
+ Assert.Equal<TemplateHelpers.GetViewNamesDelegate>(TemplateHelpers.GetViewNames, getViewNames);
+ Assert.Equal<TemplateHelpers.GetDefaultActionsDelegate>(TemplateHelpers.GetDefaultActions, getDefaultActions);
+
+ return String.Format("Model = {0}, ModelType = {1}, RealModelType = {2}, PropertyName = {3}, FormattedModelValue = {4}, HtmlFieldPrefix = {5}, TemplateName = {6}, Mode = {7}",
+ viewData.ModelMetadata.Model ?? "(null)",
+ viewData.ModelMetadata.ModelType == null ? "(null)" : viewData.ModelMetadata.ModelType.FullName,
+ viewData.ModelMetadata.RealModelType == null ? "(null)" : viewData.ModelMetadata.RealModelType.FullName,
+ viewData.ModelMetadata.PropertyName ?? "(null)",
+ viewData.TemplateInfo.FormattedModelValue ?? "(null)",
+ viewData.TemplateInfo.HtmlFieldPrefix == "" ? "(empty)" : viewData.TemplateInfo.HtmlFieldPrefix ?? "(null)",
+ templateName ?? "(null)",
+ mode);
+ }
+
+ private static string TemplateHelperSpy(HtmlHelper html, ModelMetadata metadata, string htmlFieldName, string templateName, DataBoundControlMode mode,
+ object additionalViewData)
+ {
+ return String.Format("Model = {0}, ModelType = {1}, RealModelType = {2}, PropertyName = {3}, HtmlFieldName = {4}, TemplateName = {5}, Mode = {6}, AdditionalViewData = {7}",
+ metadata.Model ?? "(null)",
+ metadata.ModelType == null ? "(null)" : metadata.ModelType.FullName,
+ metadata.RealModelType == null ? "(null)" : metadata.RealModelType.FullName,
+ metadata.PropertyName ?? "(null)",
+ htmlFieldName == String.Empty ? "(empty)" : htmlFieldName ?? "(null)",
+ templateName ?? "(null)",
+ mode,
+ AnonymousObject.Inspect(additionalViewData));
+ }
+
+ private HtmlHelper<TModel> MakeHtmlHelper<TModel>(TModel model)
+ {
+ return MakeHtmlHelper<TModel>(model, model);
+ }
+
+ private HtmlHelper<TModel> MakeHtmlHelper<TModel>(TModel model, object formattedModelValue)
+ {
+ ViewDataDictionary viewData = new ViewDataDictionary(model);
+ viewData.TemplateInfo.HtmlFieldPrefix = "FieldPrefix";
+ viewData.TemplateInfo.FormattedModelValue = formattedModelValue;
+ viewData.ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => model, typeof(TModel));
+
+ Mock<HttpContextBase> httpContext = new Mock<HttpContextBase>();
+ httpContext.Setup(c => c.Items).Returns(new Hashtable());
+
+ Mock<ViewContext> viewContext = new Mock<ViewContext> { CallBase = true };
+ viewContext.Setup(c => c.View).Returns(new DummyView());
+ viewContext.Setup(c => c.ViewData).Returns(viewData);
+ viewContext.Setup(c => c.HttpContext).Returns(httpContext.Object);
+ viewContext.Setup(c => c.RouteData).Returns(new RouteData());
+ viewContext.Setup(c => c.TempData).Returns(new TempDataDictionary());
+ viewContext.Setup(c => c.Writer).Returns(TextWriter.Null);
+
+ return new HtmlHelper<TModel>(viewContext.Object, new SimpleViewDataContainer(viewData));
+ }
+
+ private ViewDataDictionary MakeViewData(HtmlHelper html, ModelMetadata metadata)
+ {
+ return new ViewDataDictionary(html.ViewDataContainer.ViewData)
+ {
+ Model = metadata.Model,
+ ModelMetadata = metadata,
+ TemplateInfo = new TemplateInfo
+ {
+ FormattedModelValue = metadata.Model,
+ HtmlFieldPrefix = "FieldPrefix",
+ }
+ };
+ }
+
+ private class DummyView : IView
+ {
+ public void Render(ViewContext viewContext, TextWriter writer)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ private class MockViewEngine : IDisposable
+ {
+ List<IViewEngine> oldEngines;
+
+ public MockViewEngine(bool returnView = true)
+ {
+ oldEngines = ViewEngines.Engines.ToList();
+
+ View = new Mock<IView>();
+
+ Engine = new Mock<IViewEngine>();
+
+ Engine.Setup(e => e.FindPartialView(It.IsAny<ControllerContext>(), It.IsAny<string>(), It.IsAny<bool>()))
+ .Returns(returnView ? new ViewEngineResult(View.Object, Engine.Object) : new ViewEngineResult(new string[0]));
+
+ ViewEngines.Engines.Clear();
+ ViewEngines.Engines.Add(Engine.Object);
+ }
+
+ public void Dispose()
+ {
+ ViewEngines.Engines.Clear();
+
+ foreach (IViewEngine engine in oldEngines)
+ {
+ ViewEngines.Engines.Add(engine);
+ }
+ }
+
+ public Mock<IViewEngine> Engine { get; set; }
+
+ public Mock<IView> View { get; set; }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Html/Test/TextAreaExtensionsTest.cs b/test/System.Web.Mvc.Test/Html/Test/TextAreaExtensionsTest.cs
new file mode 100644
index 00000000..dadf5e58
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Html/Test/TextAreaExtensionsTest.cs
@@ -0,0 +1,629 @@
+using System.Web.Mvc.Test;
+using System.Web.Routing;
+using Microsoft.Web.UnitTestUtil;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Html.Test
+{
+ public class TextAreaExtensionsTest
+ {
+ private static readonly RouteValueDictionary _textAreaAttributesDictionary = new RouteValueDictionary(new { rows = "15", cols = "12" });
+ private static readonly object _textAreaAttributesObjectDictionary = new { rows = "15", cols = "12" };
+ private static readonly object _textAreaAttributesObjectUnderscoresDictionary = new { rows = "15", cols = "12", foo_bar = "baz" };
+
+ private class TextAreaModel
+ {
+ public string foo { get; set; }
+ public string bar { get; set; }
+ }
+
+ private static ViewDataDictionary<TextAreaModel> GetTextAreaViewData()
+ {
+ ViewDataDictionary<TextAreaModel> viewData = new ViewDataDictionary<TextAreaModel> { { "foo", "ViewDataFoo" } };
+ viewData.Model = new TextAreaModel { foo = "ViewItemFoo", bar = "ViewItemBar" };
+ return viewData;
+ }
+
+ private static ViewDataDictionary<TextAreaModel> GetTextAreaViewDataWithErrors()
+ {
+ ViewDataDictionary<TextAreaModel> viewData = new ViewDataDictionary<TextAreaModel> { { "foo", "ViewDataFoo" } };
+ viewData.Model = new TextAreaModel { foo = "ViewItemFoo", bar = "ViewItemBar" };
+
+ ModelState modelStateFoo = new ModelState();
+ modelStateFoo.Errors.Add(new ModelError("foo error 1"));
+ modelStateFoo.Errors.Add(new ModelError("foo error 2"));
+ viewData.ModelState["foo"] = modelStateFoo;
+ modelStateFoo.Value = HtmlHelperTest.GetValueProviderResult(new string[] { "AttemptedValueFoo" }, "AttemptedValueFoo");
+
+ return viewData;
+ }
+
+ // TextArea
+
+ [Fact]
+ public void TextAreaParameterDictionaryMerging()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ MvcHtmlString html = helper.TextArea("foo", new { rows = "30" });
+
+ // Assert
+ Assert.Equal(@"<textarea cols=""20"" id=""foo"" name=""foo"" rows=""30"">
+</textarea>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextAreaParameterDictionaryMerging_Unobtrusive()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper();
+ helper.ViewContext.ClientValidationEnabled = true;
+ helper.ViewContext.UnobtrusiveJavaScriptEnabled = true;
+ helper.ViewContext.FormContext = new FormContext();
+ helper.ClientValidationRuleFactory = (name, metadata) => new[] { new ModelClientValidationRule { ValidationType = "type", ErrorMessage = "error" } };
+
+ // Act
+ MvcHtmlString html = helper.TextArea("foo", new { rows = "30" });
+
+ // Assert
+ Assert.Equal(@"<textarea cols=""20"" data-val=""true"" data-val-type=""error"" id=""foo"" name=""foo"" rows=""30"">
+</textarea>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextAreaParameterDictionaryMergingExplicitParameters()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ MvcHtmlString html = helper.TextArea("foo", "bar", 10, 25, new { rows = "30" });
+
+ // Assert
+ Assert.Equal(@"<textarea cols=""25"" id=""foo"" name=""foo"" rows=""10"">
+bar</textarea>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextAreaParameterDictionaryMergingExplicitParametersWithUnderscores()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ MvcHtmlString html = helper.TextArea("foo", "bar", 10, 25, new { rows = "30", foo_bar = "baz" });
+
+ // Assert
+ Assert.Equal(@"<textarea cols=""25"" foo-bar=""baz"" id=""foo"" name=""foo"" rows=""10"">
+bar</textarea>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextAreaWithEmptyNameThrows()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { helper.TextArea(String.Empty); },
+ "name");
+ }
+
+ [Fact]
+ public void TextAreaWithOutOfRangeColsThrows()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper();
+
+ // Act & Assert
+ Assert.ThrowsArgumentOutOfRange(
+ delegate { helper.TextArea("Foo", null /* value */, 0, -1, null /* htmlAttributes */); },
+ "columns",
+ @"The value must be greater than or equal to zero.");
+ }
+
+ [Fact]
+ public void TextAreaWithOutOfRangeRowsThrows()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper();
+
+ // Act & Assert
+ Assert.ThrowsArgumentOutOfRange(
+ delegate { helper.TextArea("Foo", null /* value */, -1, 0, null /* htmlAttributes */); },
+ "rows",
+ @"The value must be greater than or equal to zero.");
+ }
+
+ [Fact]
+ public void TextAreaWithExplicitValue()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ MvcHtmlString html = helper.TextArea("foo", "bar");
+
+ // Assert
+ Assert.Equal(@"<textarea cols=""20"" id=""foo"" name=""foo"" rows=""2"">
+bar</textarea>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextAreaWithDefaultAttributes()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetTextAreaViewData());
+
+ // Act
+ MvcHtmlString html = helper.TextArea("foo");
+
+ // Assert
+ Assert.Equal(@"<textarea cols=""20"" id=""foo"" name=""foo"" rows=""2"">
+ViewDataFoo</textarea>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextAreaWithZeroRowsAndColumns()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetTextAreaViewData());
+
+ // Act
+ MvcHtmlString html = helper.TextArea("foo", null, 0, 0, null);
+
+ // Assert
+ Assert.Equal(@"<textarea id=""foo"" name=""foo"">
+ViewDataFoo</textarea>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextAreaWithDotReplacementForId()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetTextAreaViewData());
+
+ // Act
+ MvcHtmlString html = helper.TextArea("foo.bar.baz");
+
+ // Assert
+ Assert.Equal(@"<textarea cols=""20"" id=""foo_bar_baz"" name=""foo.bar.baz"" rows=""2"">
+</textarea>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextAreaWithObjectAttributes()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetTextAreaViewData());
+
+ // Act
+ MvcHtmlString html = helper.TextArea("foo", _textAreaAttributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(@"<textarea cols=""12"" id=""foo"" name=""foo"" rows=""15"">
+ViewDataFoo</textarea>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextAreaWithObjectAttributesWithUnderscores()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetTextAreaViewData());
+
+ // Act
+ MvcHtmlString html = helper.TextArea("foo", _textAreaAttributesObjectUnderscoresDictionary);
+
+ // Assert
+ Assert.Equal(@"<textarea cols=""12"" foo-bar=""baz"" id=""foo"" name=""foo"" rows=""15"">
+ViewDataFoo</textarea>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextAreaWithDictionaryAttributes()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetTextAreaViewData());
+
+ // Act
+ MvcHtmlString html = helper.TextArea("foo", _textAreaAttributesDictionary);
+
+ // Assert
+ Assert.Equal(@"<textarea cols=""12"" id=""foo"" name=""foo"" rows=""15"">
+ViewDataFoo</textarea>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextAreaWithExplicitValueAndObjectAttributes()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetTextAreaViewData());
+
+ // Act
+ MvcHtmlString html = helper.TextArea("foo", "Hello World", _textAreaAttributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(@"<textarea cols=""12"" id=""foo"" name=""foo"" rows=""15"">
+Hello World</textarea>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextAreaWithExplicitValueAndObjectAttributesWithUnderscores()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetTextAreaViewData());
+
+ // Act
+ MvcHtmlString html = helper.TextArea("foo", "Hello World", _textAreaAttributesObjectUnderscoresDictionary);
+
+ // Assert
+ Assert.Equal(@"<textarea cols=""12"" foo-bar=""baz"" id=""foo"" name=""foo"" rows=""15"">
+Hello World</textarea>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextAreaWithExplicitValueAndDictionaryAttributes()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetTextAreaViewData());
+
+ // Act
+ MvcHtmlString html = helper.TextArea("foo", "<Hello World>", _textAreaAttributesDictionary);
+
+ // Assert
+ Assert.Equal(@"<textarea cols=""12"" id=""foo"" name=""foo"" rows=""15"">
+&lt;Hello World&gt;</textarea>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextAreaWithNoValueAndObjectAttributes()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetTextAreaViewData());
+
+ // Act
+ MvcHtmlString html = helper.TextArea("baz", _textAreaAttributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(@"<textarea cols=""12"" id=""baz"" name=""baz"" rows=""15"">
+</textarea>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextAreaWithNullValue()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetTextAreaViewData());
+
+ // Act
+ MvcHtmlString html = helper.TextArea("foo", null, null);
+
+ // Assert
+ Assert.Equal(@"<textarea cols=""20"" id=""foo"" name=""foo"" rows=""2"">
+ViewDataFoo</textarea>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextAreaWithViewDataErrors()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetTextAreaViewDataWithErrors());
+
+ // Act
+ MvcHtmlString html = helper.TextArea("foo", _textAreaAttributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(@"<textarea class=""input-validation-error"" cols=""12"" id=""foo"" name=""foo"" rows=""15"">
+AttemptedValueFoo</textarea>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextAreaWithViewDataErrorsAndCustomClass()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper(GetTextAreaViewDataWithErrors());
+
+ // Act
+ MvcHtmlString html = helper.TextArea("foo", new { @class = "foo-class" });
+
+ // Assert
+ Assert.Equal(@"<textarea class=""input-validation-error foo-class"" cols=""20"" id=""foo"" name=""foo"" rows=""2"">
+AttemptedValueFoo</textarea>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextAreaWithPrefix()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper();
+ helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix";
+
+ // Act
+ MvcHtmlString html = helper.TextArea("foo", "bar");
+
+ // Assert
+ Assert.Equal(@"<textarea cols=""20"" id=""MyPrefix_foo"" name=""MyPrefix.foo"" rows=""2"">
+bar</textarea>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextAreaWithPrefixAndEmptyName()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper();
+ helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix";
+
+ // Act
+ MvcHtmlString html = helper.TextArea("", "bar");
+
+ // Assert
+ Assert.Equal(@"<textarea cols=""20"" id=""MyPrefix"" name=""MyPrefix"" rows=""2"">
+bar</textarea>", html.ToHtmlString());
+ }
+
+ // TextAreaFor
+
+ [Fact]
+ public void TextAreaForWithNullExpression()
+ {
+ // Arrange
+ HtmlHelper<TextAreaModel> helper = MvcHelper.GetHtmlHelper(GetTextAreaViewData());
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => helper.TextAreaFor<TextAreaModel, object>(null),
+ "expression"
+ );
+ }
+
+ [Fact]
+ public void TextAreaForWithOutOfRangeColsThrows()
+ {
+ // Arrange
+ HtmlHelper<TextAreaModel> helper = MvcHelper.GetHtmlHelper(GetTextAreaViewData());
+
+ // Act & Assert
+ Assert.ThrowsArgumentOutOfRange(
+ () => helper.TextAreaFor(m => m.foo, 0, -1, null /* htmlAttributes */),
+ "columns",
+ "The value must be greater than or equal to zero."
+ );
+ }
+
+ [Fact]
+ public void TextAreaForWithOutOfRangeRowsThrows()
+ {
+ // Arrange
+ HtmlHelper<TextAreaModel> helper = MvcHelper.GetHtmlHelper(GetTextAreaViewData());
+
+ // Act & Assert
+ Assert.ThrowsArgumentOutOfRange(
+ () => helper.TextAreaFor(m => m.foo, -1, 0, null /* htmlAttributes */),
+ "rows",
+ "The value must be greater than or equal to zero."
+ );
+ }
+
+ [Fact]
+ public void TextAreaForParameterDictionaryMerging()
+ {
+ // Arrange
+ HtmlHelper<TextAreaModel> helper = MvcHelper.GetHtmlHelper(GetTextAreaViewData());
+
+ // Act
+ MvcHtmlString html = helper.TextAreaFor(m => m.foo, new { rows = "30" });
+
+ // Assert
+ Assert.Equal(@"<textarea cols=""20"" id=""foo"" name=""foo"" rows=""30"">
+ViewItemFoo</textarea>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextAreaForParameterDictionaryMerging_Unobtrusive()
+ {
+ // Arrange
+ HtmlHelper<TextAreaModel> helper = MvcHelper.GetHtmlHelper(GetTextAreaViewData());
+ helper.ViewContext.ClientValidationEnabled = true;
+ helper.ViewContext.UnobtrusiveJavaScriptEnabled = true;
+ helper.ViewContext.FormContext = new FormContext();
+ helper.ClientValidationRuleFactory = (name, metadata) => new[] { new ModelClientValidationRule { ValidationType = "type", ErrorMessage = "error" } };
+
+ // Act
+ MvcHtmlString html = helper.TextAreaFor(m => m.foo, new { rows = "30" });
+
+ // Assert
+ Assert.Equal(@"<textarea cols=""20"" data-val=""true"" data-val-type=""error"" id=""foo"" name=""foo"" rows=""30"">
+ViewItemFoo</textarea>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextAreaForWithDefaultAttributes()
+ {
+ // Arrange
+ HtmlHelper<TextAreaModel> helper = MvcHelper.GetHtmlHelper(GetTextAreaViewData());
+
+ // Act
+ MvcHtmlString html = helper.TextAreaFor(m => m.foo);
+
+ // Assert
+ Assert.Equal(@"<textarea cols=""20"" id=""foo"" name=""foo"" rows=""2"">
+ViewItemFoo</textarea>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextAreaForWithZeroRowsAndColumns()
+ {
+ // Arrange
+ HtmlHelper<TextAreaModel> helper = MvcHelper.GetHtmlHelper(GetTextAreaViewData());
+
+ // Act
+ MvcHtmlString html = helper.TextAreaFor(m => m.foo, 0, 0, null);
+
+ // Assert
+ Assert.Equal(@"<textarea id=""foo"" name=""foo"">
+ViewItemFoo</textarea>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextAreaForWithObjectAttributes()
+ {
+ // Arrange
+ HtmlHelper<TextAreaModel> helper = MvcHelper.GetHtmlHelper(GetTextAreaViewData());
+
+ // Act
+ MvcHtmlString html = helper.TextAreaFor(m => m.foo, _textAreaAttributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(@"<textarea cols=""12"" id=""foo"" name=""foo"" rows=""15"">
+ViewItemFoo</textarea>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextAreaForWithObjectAttributesWithUnderscores()
+ {
+ // Arrange
+ HtmlHelper<TextAreaModel> helper = MvcHelper.GetHtmlHelper(GetTextAreaViewData());
+
+ // Act
+ MvcHtmlString html = helper.TextAreaFor(m => m.foo, _textAreaAttributesObjectUnderscoresDictionary);
+
+ // Assert
+ Assert.Equal(@"<textarea cols=""12"" foo-bar=""baz"" id=""foo"" name=""foo"" rows=""15"">
+ViewItemFoo</textarea>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextAreaForWithDictionaryAttributes()
+ {
+ // Arrange
+ HtmlHelper<TextAreaModel> helper = MvcHelper.GetHtmlHelper(GetTextAreaViewData());
+
+ // Act
+ MvcHtmlString html = helper.TextAreaFor(m => m.foo, _textAreaAttributesDictionary);
+
+ // Assert
+ Assert.Equal(@"<textarea cols=""12"" id=""foo"" name=""foo"" rows=""15"">
+ViewItemFoo</textarea>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextAreaForWithViewDataErrors()
+ {
+ // Arrange
+ HtmlHelper<TextAreaModel> helper = MvcHelper.GetHtmlHelper(GetTextAreaViewDataWithErrors());
+
+ // Act
+ MvcHtmlString html = helper.TextAreaFor(m => m.foo, _textAreaAttributesObjectDictionary);
+
+ // Assert
+ Assert.Equal(@"<textarea class=""input-validation-error"" cols=""12"" id=""foo"" name=""foo"" rows=""15"">
+AttemptedValueFoo</textarea>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextAreaForWithViewDataErrorsAndCustomClass()
+ {
+ // Arrange
+ HtmlHelper<TextAreaModel> helper = MvcHelper.GetHtmlHelper(GetTextAreaViewDataWithErrors());
+
+ // Act
+ MvcHtmlString html = helper.TextAreaFor(m => m.foo, new { @class = "foo-class" });
+
+ // Assert
+ Assert.Equal(@"<textarea class=""input-validation-error foo-class"" cols=""20"" id=""foo"" name=""foo"" rows=""2"">
+AttemptedValueFoo</textarea>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextAreaForWithPrefix()
+ {
+ // Arrange
+ HtmlHelper<TextAreaModel> helper = MvcHelper.GetHtmlHelper(GetTextAreaViewData());
+ helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix";
+
+ // Act
+ MvcHtmlString html = helper.TextAreaFor(m => m.foo);
+
+ // Assert
+ Assert.Equal(@"<textarea cols=""20"" id=""MyPrefix_foo"" name=""MyPrefix.foo"" rows=""2"">
+ViewItemFoo</textarea>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextAreaForWithPrefixAndEmptyName()
+ {
+ // Arrange
+ HtmlHelper<TextAreaModel> helper = MvcHelper.GetHtmlHelper(GetTextAreaViewData());
+ helper.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = "MyPrefix";
+
+ // Act
+ MvcHtmlString html = helper.TextAreaFor(m => m);
+
+ // Assert
+ Assert.Equal(@"<textarea cols=""20"" id=""MyPrefix"" name=""MyPrefix"" rows=""2"">
+System.Web.Mvc.Html.Test.TextAreaExtensionsTest+TextAreaModel</textarea>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextAreaForParameterDictionaryMergingWithObjectValues()
+ {
+ // Arrange
+ HtmlHelper<TextAreaModel> helper = MvcHelper.GetHtmlHelper(GetTextAreaViewData());
+
+ // Act
+ MvcHtmlString html = helper.TextAreaFor(m => m.foo, 10, 25, new { rows = "30" });
+
+ // Assert
+ Assert.Equal(@"<textarea cols=""25"" id=""foo"" name=""foo"" rows=""10"">
+ViewItemFoo</textarea>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextAreaForParameterDictionaryMergingWithObjectValuesWithUnderscores()
+ {
+ // Arrange
+ HtmlHelper<TextAreaModel> helper = MvcHelper.GetHtmlHelper(GetTextAreaViewData());
+
+ // Act
+ MvcHtmlString html = helper.TextAreaFor(m => m.foo, 10, 25, new { rows = "30", foo_bar = "baz" });
+
+ // Assert
+ Assert.Equal(@"<textarea cols=""25"" foo-bar=""baz"" id=""foo"" name=""foo"" rows=""10"">
+ViewItemFoo</textarea>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextAreaForParameterDictionaryMergingWithDictionaryValues()
+ {
+ // Arrange
+ HtmlHelper<TextAreaModel> helper = MvcHelper.GetHtmlHelper(GetTextAreaViewData());
+
+ // Act
+ MvcHtmlString html = helper.TextAreaFor(m => m.foo, 10, 25, new RouteValueDictionary(new { rows = "30" }));
+
+ // Assert
+ Assert.Equal(@"<textarea cols=""25"" id=""foo"" name=""foo"" rows=""10"">
+ViewItemFoo</textarea>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextAreaHelperDoesNotEncodeInnerHtmlPrefix()
+ {
+ // Arrange
+ HtmlHelper helper = MvcHelper.GetHtmlHelper();
+ ModelMetadata metadata = ModelMetadata.FromStringExpression("foo", helper.ViewData);
+ metadata.Model = "<model>";
+
+ // Act
+ MvcHtmlString html = TextAreaExtensions.TextAreaHelper(helper, metadata, "testEncoding", rowsAndColumns: null,
+ htmlAttributes: null, innerHtmlPrefix: "<prefix>");
+
+ // Assert
+ Assert.Equal(@"<textarea id=""testEncoding"" name=""testEncoding""><prefix>&lt;model&gt;</textarea>", html.ToHtmlString());
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Html/Test/ValidationExtensionsTest.cs b/test/System.Web.Mvc.Test/Html/Test/ValidationExtensionsTest.cs
new file mode 100644
index 00000000..0683e60f
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Html/Test/ValidationExtensionsTest.cs
@@ -0,0 +1,1086 @@
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Web.Routing;
+using Microsoft.Web.UnitTestUtil;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Html.Test
+{
+ public class ValidationExtensionsTest
+ {
+ // Validate
+
+ [Fact]
+ public void Validate_AddsClientValidationMetadata()
+ {
+ var originalProviders = ModelValidatorProviders.Providers.ToArray();
+ ModelValidatorProviders.Providers.Clear();
+
+ try
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(GetViewDataWithModelErrors());
+ FormContext formContext = new FormContext()
+ {
+ FormId = "form_id"
+ };
+ htmlHelper.ViewContext.ClientValidationEnabled = true;
+ htmlHelper.ViewContext.FormContext = formContext;
+
+ ModelClientValidationRule[] expectedValidationRules = new ModelClientValidationRule[]
+ {
+ new ModelClientValidationRule() { ValidationType = "ValidationRule1" },
+ new ModelClientValidationRule() { ValidationType = "ValidationRule2" }
+ };
+
+ Mock<ModelValidator> mockValidator = new Mock<ModelValidator>(ModelMetadata.FromStringExpression("", htmlHelper.ViewContext.ViewData), htmlHelper.ViewContext);
+ mockValidator.Setup(v => v.GetClientValidationRules())
+ .Returns(expectedValidationRules);
+ Mock<ModelValidatorProvider> mockValidatorProvider = new Mock<ModelValidatorProvider>();
+ mockValidatorProvider.Setup(vp => vp.GetValidators(It.IsAny<ModelMetadata>(), It.IsAny<ControllerContext>()))
+ .Returns(new[] { mockValidator.Object });
+ ModelValidatorProviders.Providers.Add(mockValidatorProvider.Object);
+
+ // Act
+ htmlHelper.Validate("baz");
+
+ // Assert
+ Assert.NotNull(formContext.GetValidationMetadataForField("baz"));
+ Assert.Equal(expectedValidationRules, formContext.FieldValidators["baz"].ValidationRules.ToArray());
+ }
+ finally
+ {
+ ModelValidatorProviders.Providers.Clear();
+ foreach (var provider in originalProviders)
+ {
+ ModelValidatorProviders.Providers.Add(provider);
+ }
+ }
+ }
+
+ [Fact]
+ public void Validate_DoesNothingIfClientValidationIsNotEnabled()
+ {
+ // Arrange
+ HtmlHelper<ValidationModel> htmlHelper = MvcHelper.GetHtmlHelper(GetViewDataWithModelErrors());
+ htmlHelper.ViewContext.FormContext = new FormContext();
+ htmlHelper.ViewContext.ClientValidationEnabled = false;
+
+ // Act
+ htmlHelper.Validate("foo");
+
+ // Assert
+ Assert.Empty(htmlHelper.ViewContext.FormContext.FieldValidators);
+ }
+
+ [Fact]
+ public void Validate_DoesNothingIfUnobtrusiveJavaScriptIsEnabled()
+ {
+ // Arrange
+ HtmlHelper<ValidationModel> htmlHelper = MvcHelper.GetHtmlHelper(GetViewDataWithModelErrors());
+ htmlHelper.ViewContext.FormContext = new FormContext();
+ htmlHelper.ViewContext.ClientValidationEnabled = true;
+ htmlHelper.ViewContext.UnobtrusiveJavaScriptEnabled = true;
+
+ // Act
+ htmlHelper.Validate("foo");
+
+ // Assert
+ Assert.Empty(htmlHelper.ViewContext.FormContext.FieldValidators);
+ }
+
+ [Fact]
+ public void Validate_ThrowsIfModelNameIsNull()
+ {
+ // Arrange
+ HtmlHelper<ValidationModel> htmlHelper = MvcHelper.GetHtmlHelper(GetViewDataWithModelErrors());
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { htmlHelper.Validate((string)null /* modelName */); }, "modelName");
+ }
+
+ [Fact]
+ public void ValidateFor_AddsClientValidationMetadata()
+ {
+ var originalProviders = ModelValidatorProviders.Providers.ToArray();
+ ModelValidatorProviders.Providers.Clear();
+
+ try
+ {
+ // Arrange
+ HtmlHelper<ValidationModel> htmlHelper = MvcHelper.GetHtmlHelper(GetViewDataWithModelErrors());
+ FormContext formContext = new FormContext()
+ {
+ FormId = "form_id"
+ };
+ htmlHelper.ViewContext.ClientValidationEnabled = true;
+ htmlHelper.ViewContext.FormContext = formContext;
+
+ ModelClientValidationRule[] expectedValidationRules = new ModelClientValidationRule[]
+ {
+ new ModelClientValidationRule() { ValidationType = "ValidationRule1" },
+ new ModelClientValidationRule() { ValidationType = "ValidationRule2" }
+ };
+
+ Mock<ModelValidator> mockValidator = new Mock<ModelValidator>(ModelMetadata.FromStringExpression("", htmlHelper.ViewContext.ViewData), htmlHelper.ViewContext);
+ mockValidator.Setup(v => v.GetClientValidationRules())
+ .Returns(expectedValidationRules);
+ Mock<ModelValidatorProvider> mockValidatorProvider = new Mock<ModelValidatorProvider>();
+ mockValidatorProvider.Setup(vp => vp.GetValidators(It.IsAny<ModelMetadata>(), It.IsAny<ControllerContext>()))
+ .Returns(new[] { mockValidator.Object });
+ ModelValidatorProviders.Providers.Add(mockValidatorProvider.Object);
+
+ // Act
+ htmlHelper.ValidateFor(m => m.baz);
+
+ // Assert
+ Assert.NotNull(formContext.GetValidationMetadataForField("baz"));
+ Assert.Equal(expectedValidationRules, formContext.FieldValidators["baz"].ValidationRules.ToArray());
+ }
+ finally
+ {
+ ModelValidatorProviders.Providers.Clear();
+ foreach (var provider in originalProviders)
+ {
+ ModelValidatorProviders.Providers.Add(provider);
+ }
+ }
+ }
+
+ // ValidationMessage
+
+ [Fact]
+ public void ValidationMessageAllowsEmptyModelName()
+ {
+ // Arrange
+ ViewDataDictionary vdd = new ViewDataDictionary();
+ vdd.ModelState.AddModelError("", "some error text");
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(vdd);
+
+ // Act
+ MvcHtmlString html = htmlHelper.ValidationMessage("");
+
+ // Assert
+ Assert.Equal(@"<span class=""field-validation-error"">some error text</span>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ValidationMessageReturnsNullForNullModelState()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(GetViewDataWithNullModelState());
+
+ // Act
+ MvcHtmlString html = htmlHelper.ValidationMessage("foo");
+
+ // Assert
+ Assert.Null(html);
+ }
+
+ [Fact]
+ public void ValidationMessageReturnsFirstErrorWithMessage()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(GetViewDataWithModelErrors());
+
+ // Act
+ MvcHtmlString html = htmlHelper.ValidationMessage("foo");
+
+ // Assert
+ Assert.Equal(@"<span class=""field-validation-error"">foo error &lt;1&gt;</span>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ValidationMessageReturnsGenericMessageInsteadOfExceptionText()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(GetViewDataWithModelErrors());
+
+ // Act
+ MvcHtmlString html = htmlHelper.ValidationMessage("quux");
+
+ // Assert
+ Assert.Equal(@"<span class=""field-validation-error"">The value &#39;quuxValue&#39; is invalid.</span>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ValidationMessageReturnsNullForInvalidName()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(GetViewDataWithModelErrors());
+
+ // Act
+ MvcHtmlString html = htmlHelper.ValidationMessage("boo");
+
+ // Assert
+ Assert.Null(html);
+ }
+
+ [Fact]
+ public void ValidationMessageReturnsWithObjectAttributes()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(GetViewDataWithModelErrors());
+
+ // Act
+ MvcHtmlString html = htmlHelper.ValidationMessage("foo", new { bar = "bar" });
+
+ // Assert
+ Assert.Equal(@"<span bar=""bar"" class=""field-validation-error"">foo error &lt;1&gt;</span>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ValidationMessageReturnsWithObjectAttributesWithUnderscores()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(GetViewDataWithModelErrors());
+
+ // Act
+ MvcHtmlString html = htmlHelper.ValidationMessage("foo", new { foo_bar = "bar" });
+
+ // Assert
+ Assert.Equal(@"<span class=""field-validation-error"" foo-bar=""bar"">foo error &lt;1&gt;</span>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ValidationMessageReturnsWithCustomMessage()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(GetViewDataWithModelErrors());
+
+ // Act
+ MvcHtmlString html = htmlHelper.ValidationMessage("foo", "bar error");
+
+ // Assert
+ Assert.Equal(@"<span class=""field-validation-error"">bar error</span>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ValidationMessageReturnsWithCustomMessageAndObjectAttributes()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(GetViewDataWithModelErrors());
+
+ // Act
+ MvcHtmlString html = htmlHelper.ValidationMessage("foo", "bar error", new { baz = "baz" });
+
+ // Assert
+ Assert.Equal(@"<span baz=""baz"" class=""field-validation-error"">bar error</span>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ValidationMessageReturnsWithCustomMessageAndObjectAttributesWithUnderscores()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(GetViewDataWithModelErrors());
+
+ // Act
+ MvcHtmlString html = htmlHelper.ValidationMessage("foo", "bar error", new { foo_baz = "baz" });
+
+ // Assert
+ Assert.Equal(@"<span class=""field-validation-error"" foo-baz=""baz"">bar error</span>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ValidationMessageThrowsIfModelNameIsNull()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { htmlHelper.ValidationMessage(null); }, "modelName");
+ }
+
+ [Fact]
+ public void ValidationMessageWithClientValidation_DefaultMessage_Valid()
+ {
+ var originalProviders = ModelValidatorProviders.Providers.ToArray();
+ ModelValidatorProviders.Providers.Clear();
+
+ try
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(GetViewDataWithModelErrors());
+ FormContext formContext = new FormContext();
+ htmlHelper.ViewContext.ClientValidationEnabled = true;
+ htmlHelper.ViewContext.FormContext = formContext;
+
+ ModelClientValidationRule[] expectedValidationRules = new ModelClientValidationRule[]
+ {
+ new ModelClientValidationRule() { ValidationType = "ValidationRule1" },
+ new ModelClientValidationRule() { ValidationType = "ValidationRule2" }
+ };
+
+ Mock<ModelValidator> mockValidator = new Mock<ModelValidator>(ModelMetadata.FromStringExpression("", htmlHelper.ViewContext.ViewData), htmlHelper.ViewContext);
+ mockValidator.Setup(v => v.GetClientValidationRules())
+ .Returns(expectedValidationRules);
+ Mock<ModelValidatorProvider> mockValidatorProvider = new Mock<ModelValidatorProvider>();
+ mockValidatorProvider.Setup(vp => vp.GetValidators(It.IsAny<ModelMetadata>(), It.IsAny<ControllerContext>()))
+ .Returns(new[] { mockValidator.Object });
+ ModelValidatorProviders.Providers.Add(mockValidatorProvider.Object);
+
+ // Act
+ MvcHtmlString html = htmlHelper.ValidationMessage("baz"); // 'baz' is valid
+
+ // Assert
+ Assert.Equal(@"<span class=""field-validation-valid"" id=""baz_validationMessage""></span>", html.ToHtmlString());
+ Assert.NotNull(formContext.GetValidationMetadataForField("baz"));
+ Assert.Equal("baz_validationMessage", formContext.FieldValidators["baz"].ValidationMessageId);
+ Assert.True(formContext.FieldValidators["baz"].ReplaceValidationMessageContents);
+ Assert.Equal(expectedValidationRules, formContext.FieldValidators["baz"].ValidationRules.ToArray());
+ }
+ finally
+ {
+ ModelValidatorProviders.Providers.Clear();
+ foreach (var provider in originalProviders)
+ {
+ ModelValidatorProviders.Providers.Add(provider);
+ }
+ }
+ }
+
+ [Fact]
+ public void ValidationMessageWithClientValidation_DefaultMessage_Valid_Unobtrusive()
+ {
+ var originalProviders = ModelValidatorProviders.Providers.ToArray();
+ ModelValidatorProviders.Providers.Clear();
+
+ try
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(GetViewDataWithModelErrors());
+ FormContext formContext = new FormContext();
+ htmlHelper.ViewContext.ClientValidationEnabled = true;
+ htmlHelper.ViewContext.UnobtrusiveJavaScriptEnabled = true;
+ htmlHelper.ViewContext.FormContext = formContext;
+
+ ModelClientValidationRule[] expectedValidationRules = new ModelClientValidationRule[]
+ {
+ new ModelClientValidationRule() { ValidationType = "ValidationRule1" },
+ new ModelClientValidationRule() { ValidationType = "ValidationRule2" }
+ };
+
+ Mock<ModelValidator> mockValidator = new Mock<ModelValidator>(ModelMetadata.FromStringExpression("", htmlHelper.ViewContext.ViewData), htmlHelper.ViewContext);
+ mockValidator.Setup(v => v.GetClientValidationRules())
+ .Returns(expectedValidationRules);
+ Mock<ModelValidatorProvider> mockValidatorProvider = new Mock<ModelValidatorProvider>();
+ mockValidatorProvider.Setup(vp => vp.GetValidators(It.IsAny<ModelMetadata>(), It.IsAny<ControllerContext>()))
+ .Returns(new[] { mockValidator.Object });
+ ModelValidatorProviders.Providers.Add(mockValidatorProvider.Object);
+
+ // Act
+ MvcHtmlString html = htmlHelper.ValidationMessage("baz"); // 'baz' is valid
+
+ // Assert
+ Assert.Equal(@"<span class=""field-validation-valid"" data-valmsg-for=""baz"" data-valmsg-replace=""true""></span>", html.ToHtmlString());
+ }
+ finally
+ {
+ ModelValidatorProviders.Providers.Clear();
+ foreach (var provider in originalProviders)
+ {
+ ModelValidatorProviders.Providers.Add(provider);
+ }
+ }
+ }
+
+ [Fact]
+ public void ValidationMessageWithClientValidation_ExplicitMessage_Valid()
+ {
+ var originalProviders = ModelValidatorProviders.Providers.ToArray();
+ ModelValidatorProviders.Providers.Clear();
+
+ try
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(GetViewDataWithModelErrors());
+ FormContext formContext = new FormContext();
+ htmlHelper.ViewContext.ClientValidationEnabled = true;
+ htmlHelper.ViewContext.FormContext = formContext;
+
+ ModelClientValidationRule[] expectedValidationRules = new ModelClientValidationRule[]
+ {
+ new ModelClientValidationRule() { ValidationType = "ValidationRule1" },
+ new ModelClientValidationRule() { ValidationType = "ValidationRule2" }
+ };
+
+ Mock<ModelValidator> mockValidator = new Mock<ModelValidator>(ModelMetadata.FromStringExpression("", htmlHelper.ViewContext.ViewData), htmlHelper.ViewContext);
+ mockValidator.Setup(v => v.GetClientValidationRules())
+ .Returns(expectedValidationRules);
+ Mock<ModelValidatorProvider> mockValidatorProvider = new Mock<ModelValidatorProvider>();
+ mockValidatorProvider.Setup(vp => vp.GetValidators(It.IsAny<ModelMetadata>(), It.IsAny<ControllerContext>()))
+ .Returns(new[] { mockValidator.Object });
+ ModelValidatorProviders.Providers.Add(mockValidatorProvider.Object);
+
+ // Act
+ MvcHtmlString html = htmlHelper.ValidationMessage("baz", "some explicit message"); // 'baz' is valid
+
+ // Assert
+ Assert.Equal(@"<span class=""field-validation-valid"" id=""baz_validationMessage"">some explicit message</span>", html.ToHtmlString());
+ Assert.NotNull(formContext.GetValidationMetadataForField("baz"));
+ Assert.Equal("baz_validationMessage", formContext.FieldValidators["baz"].ValidationMessageId);
+ Assert.False(formContext.FieldValidators["baz"].ReplaceValidationMessageContents);
+ Assert.Equal(expectedValidationRules, formContext.FieldValidators["baz"].ValidationRules.ToArray());
+ }
+ finally
+ {
+ ModelValidatorProviders.Providers.Clear();
+ foreach (var provider in originalProviders)
+ {
+ ModelValidatorProviders.Providers.Add(provider);
+ }
+ }
+ }
+
+ [Fact]
+ public void ValidationMessageWithClientValidation_ExplicitMessage_Valid_Unobtrusive()
+ {
+ var originalProviders = ModelValidatorProviders.Providers.ToArray();
+ ModelValidatorProviders.Providers.Clear();
+
+ try
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(GetViewDataWithModelErrors());
+ FormContext formContext = new FormContext();
+ htmlHelper.ViewContext.ClientValidationEnabled = true;
+ htmlHelper.ViewContext.UnobtrusiveJavaScriptEnabled = true;
+ htmlHelper.ViewContext.FormContext = formContext;
+
+ ModelClientValidationRule[] expectedValidationRules = new ModelClientValidationRule[]
+ {
+ new ModelClientValidationRule() { ValidationType = "ValidationRule1" },
+ new ModelClientValidationRule() { ValidationType = "ValidationRule2" }
+ };
+
+ Mock<ModelValidator> mockValidator = new Mock<ModelValidator>(ModelMetadata.FromStringExpression("", htmlHelper.ViewContext.ViewData), htmlHelper.ViewContext);
+ mockValidator.Setup(v => v.GetClientValidationRules())
+ .Returns(expectedValidationRules);
+ Mock<ModelValidatorProvider> mockValidatorProvider = new Mock<ModelValidatorProvider>();
+ mockValidatorProvider.Setup(vp => vp.GetValidators(It.IsAny<ModelMetadata>(), It.IsAny<ControllerContext>()))
+ .Returns(new[] { mockValidator.Object });
+ ModelValidatorProviders.Providers.Add(mockValidatorProvider.Object);
+
+ // Act
+ MvcHtmlString html = htmlHelper.ValidationMessage("baz", "some explicit message"); // 'baz' is valid
+
+ // Assert
+ Assert.Equal(@"<span class=""field-validation-valid"" data-valmsg-for=""baz"" data-valmsg-replace=""false"">some explicit message</span>", html.ToHtmlString());
+ }
+ finally
+ {
+ ModelValidatorProviders.Providers.Clear();
+ foreach (var provider in originalProviders)
+ {
+ ModelValidatorProviders.Providers.Add(provider);
+ }
+ }
+ }
+
+ [Fact]
+ public void ValidationMessageWithModelStateAndNoErrors()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(GetViewDataWithModelErrors());
+
+ // Act
+ MvcHtmlString html = htmlHelper.ValidationMessage("baz");
+
+ // Assert
+ Assert.Null(html);
+ }
+
+ // ValidationMessageFor
+
+ [Fact]
+ public void ValidationMessageForThrowsIfExpressionIsNull()
+ {
+ // Arrange
+ HtmlHelper<object> htmlHelper = MvcHelper.GetHtmlHelper();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => htmlHelper.ValidationMessageFor<object, object>(null),
+ "expression"
+ );
+ }
+
+ [Fact]
+ public void ValidationMessageForReturnsNullIfModelStateIsNull()
+ {
+ // Arrange
+ HtmlHelper<ValidationModel> htmlHelper = MvcHelper.GetHtmlHelper(GetViewDataWithNullModelState());
+
+ // Act
+ MvcHtmlString html = htmlHelper.ValidationMessageFor(m => m.foo);
+
+ // Assert
+ Assert.Null(html);
+ }
+
+ [Fact]
+ public void ValidationMessageForReturnsFirstErrorWithErrorMessage()
+ {
+ // Arrange
+ HtmlHelper<ValidationModel> htmlHelper = MvcHelper.GetHtmlHelper(GetViewDataWithModelErrors());
+
+ // Act
+ MvcHtmlString html = htmlHelper.ValidationMessageFor(m => m.foo);
+
+ // Assert
+ Assert.Equal(@"<span class=""field-validation-error"">foo error &lt;1&gt;</span>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ValidationMessageForReturnsGenericMessageInsteadOfExceptionText()
+ {
+ // Arrange
+ HtmlHelper<ValidationModel> htmlHelper = MvcHelper.GetHtmlHelper(GetViewDataWithModelErrors());
+
+ // Act
+ MvcHtmlString html = htmlHelper.ValidationMessageFor(m => m.quux);
+
+ // Assert
+ Assert.Equal(@"<span class=""field-validation-error"">The value &#39;quuxValue&#39; is invalid.</span>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ValidationMessageForReturnsWithObjectAttributes()
+ {
+ // Arrange
+ HtmlHelper<ValidationModel> htmlHelper = MvcHelper.GetHtmlHelper(GetViewDataWithModelErrors());
+
+ // Act
+ MvcHtmlString html = htmlHelper.ValidationMessageFor(m => m.foo, null /* validationMessage */, new { bar = "bar" });
+
+ // Assert
+ Assert.Equal(@"<span bar=""bar"" class=""field-validation-error"">foo error &lt;1&gt;</span>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ValidationMessageForReturnsWithObjectAttributesWithUnderscores()
+ {
+ // Arrange
+ HtmlHelper<ValidationModel> htmlHelper = MvcHelper.GetHtmlHelper(GetViewDataWithModelErrors());
+
+ // Act
+ MvcHtmlString html = htmlHelper.ValidationMessageFor(m => m.foo, null /* validationMessage */, new { foo_bar = "bar" });
+
+ // Assert
+ Assert.Equal(@"<span class=""field-validation-error"" foo-bar=""bar"">foo error &lt;1&gt;</span>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ValidationMessageForReturnsWithCustomMessage()
+ {
+ // Arrange
+ HtmlHelper<ValidationModel> htmlHelper = MvcHelper.GetHtmlHelper(GetViewDataWithModelErrors());
+
+ // Act
+ MvcHtmlString html = htmlHelper.ValidationMessageFor(m => m.foo, "bar error");
+
+ // Assert
+ Assert.Equal(@"<span class=""field-validation-error"">bar error</span>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ValidationMessageForReturnsWithCustomMessageAndObjectAttributes()
+ {
+ // Arrange
+ HtmlHelper<ValidationModel> htmlHelper = MvcHelper.GetHtmlHelper(GetViewDataWithModelErrors());
+
+ // Act
+ MvcHtmlString html = htmlHelper.ValidationMessageFor(m => m.foo, "bar error", new { baz = "baz" });
+
+ // Assert
+ Assert.Equal(@"<span baz=""baz"" class=""field-validation-error"">bar error</span>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ValidationMessageForWithClientValidation()
+ {
+ var originalProviders = ModelValidatorProviders.Providers.ToArray();
+ ModelValidatorProviders.Providers.Clear();
+
+ try
+ {
+ // Arrange
+ HtmlHelper<ValidationModel> htmlHelper = MvcHelper.GetHtmlHelper(GetViewDataWithModelErrors());
+ FormContext formContext = new FormContext();
+ htmlHelper.ViewContext.ClientValidationEnabled = true;
+ htmlHelper.ViewContext.FormContext = formContext;
+
+ ModelClientValidationRule[] expectedValidationRules = new ModelClientValidationRule[]
+ {
+ new ModelClientValidationRule() { ValidationType = "ValidationRule1" },
+ new ModelClientValidationRule() { ValidationType = "ValidationRule2" }
+ };
+
+ Mock<ModelValidator> mockValidator = new Mock<ModelValidator>(ModelMetadata.FromStringExpression("", htmlHelper.ViewContext.ViewData), htmlHelper.ViewContext);
+ mockValidator.Setup(v => v.GetClientValidationRules())
+ .Returns(expectedValidationRules);
+ Mock<ModelValidatorProvider> mockValidatorProvider = new Mock<ModelValidatorProvider>();
+ mockValidatorProvider.Setup(vp => vp.GetValidators(It.IsAny<ModelMetadata>(), It.IsAny<ControllerContext>()))
+ .Returns(new[] { mockValidator.Object });
+ ModelValidatorProviders.Providers.Add(mockValidatorProvider.Object);
+
+ // Act
+ MvcHtmlString html = htmlHelper.ValidationMessageFor(m => m.baz);
+
+ // Assert
+ Assert.Equal(@"<span class=""field-validation-valid"" id=""baz_validationMessage""></span>", html.ToHtmlString());
+ Assert.NotNull(formContext.GetValidationMetadataForField("baz"));
+ Assert.Equal("baz_validationMessage", formContext.FieldValidators["baz"].ValidationMessageId);
+ Assert.Equal(expectedValidationRules, formContext.FieldValidators["baz"].ValidationRules.ToArray());
+ }
+ finally
+ {
+ ModelValidatorProviders.Providers.Clear();
+ foreach (var provider in originalProviders)
+ {
+ ModelValidatorProviders.Providers.Add(provider);
+ }
+ }
+ }
+
+ [Fact]
+ public void ValidationMessageForWithClientValidation_Unobtrusive()
+ {
+ var originalProviders = ModelValidatorProviders.Providers.ToArray();
+ ModelValidatorProviders.Providers.Clear();
+
+ try
+ {
+ // Arrange
+ HtmlHelper<ValidationModel> htmlHelper = MvcHelper.GetHtmlHelper(GetViewDataWithModelErrors());
+ FormContext formContext = new FormContext();
+ htmlHelper.ViewContext.ClientValidationEnabled = true;
+ htmlHelper.ViewContext.UnobtrusiveJavaScriptEnabled = true;
+ htmlHelper.ViewContext.FormContext = formContext;
+
+ ModelClientValidationRule[] expectedValidationRules = new ModelClientValidationRule[]
+ {
+ new ModelClientValidationRule() { ValidationType = "ValidationRule1" },
+ new ModelClientValidationRule() { ValidationType = "ValidationRule2" }
+ };
+
+ Mock<ModelValidator> mockValidator = new Mock<ModelValidator>(ModelMetadata.FromStringExpression("", htmlHelper.ViewContext.ViewData), htmlHelper.ViewContext);
+ mockValidator.Setup(v => v.GetClientValidationRules())
+ .Returns(expectedValidationRules);
+ Mock<ModelValidatorProvider> mockValidatorProvider = new Mock<ModelValidatorProvider>();
+ mockValidatorProvider.Setup(vp => vp.GetValidators(It.IsAny<ModelMetadata>(), It.IsAny<ControllerContext>()))
+ .Returns(new[] { mockValidator.Object });
+ ModelValidatorProviders.Providers.Add(mockValidatorProvider.Object);
+
+ // Act
+ MvcHtmlString html = htmlHelper.ValidationMessageFor(m => m.baz);
+
+ // Assert
+ Assert.Equal(@"<span class=""field-validation-valid"" data-valmsg-for=""baz"" data-valmsg-replace=""true""></span>", html.ToHtmlString());
+ }
+ finally
+ {
+ ModelValidatorProviders.Providers.Clear();
+ foreach (var provider in originalProviders)
+ {
+ ModelValidatorProviders.Providers.Add(provider);
+ }
+ }
+ }
+
+ [Fact]
+ public void ValidationMessageForWithModelStateAndNoErrors()
+ {
+ // Arrange
+ HtmlHelper<ValidationModel> htmlHelper = MvcHelper.GetHtmlHelper(GetViewDataWithModelErrors());
+
+ // Act
+ MvcHtmlString html = htmlHelper.ValidationMessageFor(m => m.baz);
+
+ // Assert
+ Assert.Null(html);
+ }
+
+ // ValidationSummary
+
+ [Fact]
+ public void ValidationSummary()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(GetViewDataWithModelErrors());
+
+ // Act
+ MvcHtmlString html = htmlHelper.ValidationSummary();
+
+ // Assert
+ Assert.Equal(@"<div class=""validation-summary-errors""><ul><li>foo error &lt;1&gt;</li>
+<li>foo error 2</li>
+<li>bar error &lt;1&gt;</li>
+<li>bar error 2</li>
+</ul></div>"
+ , html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ValidationSummaryAddsIdIfClientValidationEnabled()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(GetViewDataWithModelErrors());
+ htmlHelper.ViewContext.FormContext = new FormContext();
+ htmlHelper.ViewContext.ClientValidationEnabled = true;
+
+ // Act
+ MvcHtmlString html = htmlHelper.ValidationSummary();
+
+ // Assert
+ Assert.Equal(@"<div class=""validation-summary-errors"" id=""validationSummary""><ul><li>foo error &lt;1&gt;</li>
+<li>foo error 2</li>
+<li>bar error &lt;1&gt;</li>
+<li>bar error 2</li>
+</ul></div>"
+ , html.ToHtmlString());
+ Assert.Equal("validationSummary", htmlHelper.ViewContext.FormContext.ValidationSummaryId);
+ }
+
+ [Fact]
+ public void ValidationSummaryDoesNotAddIdIfUnobtrusiveJavaScriptEnabled()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(GetViewDataWithModelErrors());
+ htmlHelper.ViewContext.FormContext = new FormContext();
+ htmlHelper.ViewContext.ClientValidationEnabled = true;
+ htmlHelper.ViewContext.UnobtrusiveJavaScriptEnabled = true;
+
+ // Act
+ MvcHtmlString html = htmlHelper.ValidationSummary();
+
+ // Assert
+ Assert.Equal(@"<div class=""validation-summary-errors"" data-valmsg-summary=""true""><ul><li>foo error &lt;1&gt;</li>
+<li>foo error 2</li>
+<li>bar error &lt;1&gt;</li>
+<li>bar error 2</li>
+</ul></div>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ValidationSummaryWithDictionary()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(GetViewDataWithModelErrors());
+ RouteValueDictionary htmlAttributes = new RouteValueDictionary();
+ htmlAttributes["class"] = "my-class";
+
+ // Act
+ MvcHtmlString html = htmlHelper.ValidationSummary(null /* message */, htmlAttributes);
+
+ // Assert
+ Assert.Equal(@"<div class=""validation-summary-errors my-class""><ul><li>foo error &lt;1&gt;</li>
+<li>foo error 2</li>
+<li>bar error &lt;1&gt;</li>
+<li>bar error 2</li>
+</ul></div>"
+ , html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ValidationSummaryWithDictionaryAndMessage()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(GetViewDataWithModelErrors());
+ RouteValueDictionary htmlAttributes = new RouteValueDictionary();
+ htmlAttributes["class"] = "my-class";
+
+ // Act
+ MvcHtmlString html = htmlHelper.ValidationSummary("This is my message.", htmlAttributes);
+
+ // Assert
+ Assert.Equal(@"<div class=""validation-summary-errors my-class""><span>This is my message.</span>
+<ul><li>foo error &lt;1&gt;</li>
+<li>foo error 2</li>
+<li>bar error &lt;1&gt;</li>
+<li>bar error 2</li>
+</ul></div>"
+ , html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ValidationSummaryWithNoErrors_ReturnsNullIfClientValidationDisabled()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+
+ // Act
+ MvcHtmlString html = htmlHelper.ValidationSummary();
+
+ // Assert
+ Assert.Null(html);
+ }
+
+ [Fact]
+ public void ValidationSummaryWithNoErrors_EmptyUlIfClientValidationEnabled()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+ htmlHelper.ViewContext.ClientValidationEnabled = true;
+ htmlHelper.ViewContext.FormContext = new FormContext();
+
+ // Act
+ MvcHtmlString html = htmlHelper.ValidationSummary();
+
+ // Assert
+ Assert.Equal(@"<div class=""validation-summary-valid"" id=""validationSummary""><ul><li style=""display:none""></li>
+</ul></div>"
+ , html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ValidationSummaryWithObjectAttributes()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(GetViewDataWithModelErrors());
+
+ // Act
+ MvcHtmlString html = htmlHelper.ValidationSummary(null /* message */, new { baz = "baz" });
+
+ // Assert
+ Assert.Equal(@"<div baz=""baz"" class=""validation-summary-errors""><ul><li>foo error &lt;1&gt;</li>
+<li>foo error 2</li>
+<li>bar error &lt;1&gt;</li>
+<li>bar error 2</li>
+</ul></div>"
+ , html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ValidationSummaryWithObjectAttributesWithUnderscores()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(GetViewDataWithModelErrors());
+
+ // Act
+ MvcHtmlString html = htmlHelper.ValidationSummary(null /* message */, new { foo_baz = "baz" });
+
+ // Assert
+ Assert.Equal(@"<div class=""validation-summary-errors"" foo-baz=""baz""><ul><li>foo error &lt;1&gt;</li>
+<li>foo error 2</li>
+<li>bar error &lt;1&gt;</li>
+<li>bar error 2</li>
+</ul></div>"
+ , html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ValidationSummaryWithObjectAttributesAndMessage()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(GetViewDataWithModelErrors());
+
+ // Act
+ MvcHtmlString html = htmlHelper.ValidationSummary("This is my message.", new { baz = "baz" });
+
+ // Assert
+ Assert.Equal(@"<div baz=""baz"" class=""validation-summary-errors""><span>This is my message.</span>
+<ul><li>foo error &lt;1&gt;</li>
+<li>foo error 2</li>
+<li>bar error &lt;1&gt;</li>
+<li>bar error 2</li>
+</ul></div>"
+ , html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ValidationSummaryWithNoModelErrors()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(GetViewDataWithModelErrors());
+
+ // Act
+ MvcHtmlString html = htmlHelper.ValidationSummary(true /* excludePropertyErrors */, "This is my message.");
+
+ // Assert
+ Assert.Equal(@"<div class=""validation-summary-errors""><span>This is my message.</span>
+<ul><li style=""display:none""></li>
+</ul></div>"
+ , html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ValidationSummaryWithOnlyModelErrors()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(GetViewDataWithModelAndPropertyErrors());
+
+ // Act
+ MvcHtmlString html = htmlHelper.ValidationSummary(true /* excludePropertyErrors */, "This is my message.");
+
+ // Assert
+ Assert.Equal(@"<div class=""validation-summary-errors""><span>This is my message.</span>
+<ul><li>Something is wrong.</li>
+<li>Something else is also wrong.</li>
+</ul></div>"
+ , html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ValidationSummaryWithOnlyModelErrorsAndPrefix()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(GetViewDataWithModelErrors("MyPrefix"));
+
+ // Act
+ MvcHtmlString html = htmlHelper.ValidationSummary(true /* excludePropertyErrors */, "This is my message.");
+
+ // Assert
+ Assert.Equal(@"<div class=""validation-summary-errors""><span>This is my message.</span>
+<ul><li style=""display:none""></li>
+</ul></div>"
+ , html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ValidationMessageWithPrefix()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(GetViewDataWithModelErrors("MyPrefix"));
+
+ // Act
+ MvcHtmlString html = htmlHelper.ValidationMessage("foo");
+
+ // Assert
+ Assert.Equal(@"<span class=""field-validation-error"">foo error &lt;1&gt;</span>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ValidationErrorOrdering()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(GetViewDataWithModelWithDisplayOrderErrors());
+
+ // Act
+ MvcHtmlString html = htmlHelper.ValidationSummary();
+
+ // Assert
+ Assert.Equal(@"<div class=""validation-summary-errors""><ul><li>Error 1</li>
+<li>Error 2</li>
+<li>Error 3</li>
+<li>Error 4</li>
+</ul></div>", html.ToHtmlString());
+
+ }
+
+ private class ValidationModel
+ {
+ public string foo { get; set; }
+ public string bar { get; set; }
+ public string baz { get; set; }
+ public string quux { get; set; }
+ }
+
+ public class ModelWithOrdering
+ {
+ [Required]
+ [Display(Order = 2)]
+ public int Second { get; set; }
+
+ [Required]
+ [Display(Order = 1)]
+ public string First { get; set; }
+
+ [Required]
+ [Display(Order = 4)]
+ public string Fourth { get; set; }
+
+ [Required]
+ [Display(Order = 3)]
+ public string Third { get; set; }
+ }
+
+ private static ViewDataDictionary<ValidationModel> GetViewDataWithNullModelState()
+ {
+ ViewDataDictionary<ValidationModel> viewData = new ViewDataDictionary<ValidationModel>();
+ viewData.ModelState["foo"] = null;
+ return viewData;
+ }
+
+ private static ViewDataDictionary<ValidationModel> GetViewDataWithModelErrors()
+ {
+ ViewDataDictionary<ValidationModel> viewData = new ViewDataDictionary<ValidationModel>();
+ ModelState modelStateFoo = new ModelState();
+ ModelState modelStateBar = new ModelState();
+ ModelState modelStateBaz = new ModelState();
+
+ modelStateFoo.Errors.Add(new ModelError(new InvalidOperationException("foo error from exception")));
+ modelStateFoo.Errors.Add(new ModelError("foo error <1>"));
+ modelStateFoo.Errors.Add(new ModelError("foo error 2"));
+ modelStateBar.Errors.Add(new ModelError("bar error <1>"));
+ modelStateBar.Errors.Add(new ModelError("bar error 2"));
+
+ viewData.ModelState["foo"] = modelStateFoo;
+ viewData.ModelState["bar"] = modelStateBar;
+ viewData.ModelState["baz"] = modelStateBaz;
+
+ viewData.ModelState.SetModelValue("quux", new ValueProviderResult(null, "quuxValue", null));
+ viewData.ModelState.AddModelError("quux", new InvalidOperationException("Some error text."));
+ return viewData;
+ }
+
+ private static ViewDataDictionary<ValidationModel> GetViewDataWithModelAndPropertyErrors()
+ {
+ ViewDataDictionary<ValidationModel> viewData = new ViewDataDictionary<ValidationModel>();
+ ModelState modelStateFoo = new ModelState();
+ ModelState modelStateBar = new ModelState();
+ ModelState modelStateBaz = new ModelState();
+ modelStateFoo.Errors.Add(new ModelError("foo error <1>"));
+ modelStateFoo.Errors.Add(new ModelError("foo error 2"));
+ modelStateBar.Errors.Add(new ModelError("bar error <1>"));
+ modelStateBar.Errors.Add(new ModelError("bar error 2"));
+ viewData.ModelState["foo"] = modelStateFoo;
+ viewData.ModelState["bar"] = modelStateBar;
+ viewData.ModelState["baz"] = modelStateBaz;
+ viewData.ModelState.SetModelValue("quux", new ValueProviderResult(null, "quuxValue", null));
+ viewData.ModelState.AddModelError("quux", new InvalidOperationException("Some error text."));
+ viewData.ModelState.AddModelError(String.Empty, "Something is wrong.");
+ viewData.ModelState.AddModelError(String.Empty, "Something else is also wrong.");
+ return viewData;
+ }
+
+ private static ViewDataDictionary<ValidationModel> GetViewDataWithModelErrors(string prefix)
+ {
+ ViewDataDictionary<ValidationModel> viewData = new ViewDataDictionary<ValidationModel>();
+ viewData.TemplateInfo.HtmlFieldPrefix = prefix;
+ ModelState modelStateFoo = new ModelState();
+ ModelState modelStateBar = new ModelState();
+ ModelState modelStateBaz = new ModelState();
+ modelStateFoo.Errors.Add(new ModelError("foo error <1>"));
+ modelStateFoo.Errors.Add(new ModelError("foo error 2"));
+ modelStateBar.Errors.Add(new ModelError("bar error <1>"));
+ modelStateBar.Errors.Add(new ModelError("bar error 2"));
+ viewData.ModelState[prefix + ".foo"] = modelStateFoo;
+ viewData.ModelState[prefix + ".bar"] = modelStateBar;
+ viewData.ModelState[prefix + ".baz"] = modelStateBaz;
+ viewData.ModelState.SetModelValue(prefix + ".quux", new ValueProviderResult(null, "quuxValue", null));
+ viewData.ModelState.AddModelError(prefix + ".quux", new InvalidOperationException("Some error text."));
+ return viewData;
+ }
+
+
+ private static ViewDataDictionary<ModelWithOrdering> GetViewDataWithModelWithDisplayOrderErrors()
+ {
+ ViewDataDictionary<ModelWithOrdering> viewData = new ViewDataDictionary<ModelWithOrdering>();
+
+ var model = new ModelWithOrdering();
+
+ // Error names for each property on ModelWithOrdering.
+ viewData.ModelState.AddModelError("First", "Error 1");
+ viewData.ModelState.AddModelError("Second", "Error 2");
+ viewData.ModelState.AddModelError("Third", "Error 3");
+ viewData.ModelState.AddModelError("Fourth", "Error 4");
+
+ return viewData;
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Html/Test/ValueExtensionsTest.cs b/test/System.Web.Mvc.Test/Html/Test/ValueExtensionsTest.cs
new file mode 100644
index 00000000..6262f479
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Html/Test/ValueExtensionsTest.cs
@@ -0,0 +1,164 @@
+using System.Web.Mvc.Test;
+using Microsoft.Web.UnitTestUtil;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Html.Test
+{
+ public class ValueExtensionsTest
+ {
+ // Value
+
+ [Fact]
+ public void ValueWithNullNameThrows()
+ {
+ // Arrange
+ HtmlHelper<FooBarModel> helper = MvcHelper.GetHtmlHelper(GetValueViewData());
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => helper.Value(name: null),
+ "name"
+ );
+ }
+
+ [Fact]
+ public void ValueGetsValueFromViewData()
+ {
+ // Arrange
+ HtmlHelper<FooBarModel> helper = MvcHelper.GetHtmlHelper(GetValueViewData());
+
+ // Act
+ MvcHtmlString html = helper.Value("foo");
+
+ // Assert
+ Assert.Equal("ViewDataFoo", html.ToHtmlString());
+ }
+
+ // ValueFor
+
+ [Fact]
+ public void ValueForWithNullExpressionThrows()
+ {
+ // Arrange
+ HtmlHelper<FooBarModel> helper = MvcHelper.GetHtmlHelper(GetValueViewData());
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => helper.ValueFor<FooBarModel, object>(expression: null),
+ "expression"
+ );
+ }
+
+ [Fact]
+ public void ValueForGetsExpressionValueFromViewDataModel()
+ {
+ // Arrange
+ HtmlHelper<FooBarModel> helper = MvcHelper.GetHtmlHelper(GetValueViewData());
+
+ // Act
+ MvcHtmlString html = helper.ValueFor(m => m.foo);
+
+ // Assert
+ Assert.Equal("ViewItemFoo", html.ToHtmlString());
+ }
+
+ // All Value Helpers including ValueForModel
+
+ [Fact]
+ public void ValueHelpersWithErrorsGetValueFromModelState()
+ {
+ // Arrange
+ ViewDataDictionary<FooBarModel> viewDataWithErrors = new ViewDataDictionary<FooBarModel> { { "foo", "ViewDataFoo" } };
+ viewDataWithErrors.Model = new FooBarModel() { foo = "ViewItemFoo", bar = "ViewItemBar" };
+ viewDataWithErrors.TemplateInfo.HtmlFieldPrefix = "FieldPrefix";
+
+ ModelState modelStateFoo = new ModelState();
+ modelStateFoo.Value = HtmlHelperTest.GetValueProviderResult(new string[] { "AttemptedValueFoo" }, "AttemptedValueFoo");
+ viewDataWithErrors.ModelState["FieldPrefix.foo"] = modelStateFoo;
+
+ ModelState modelStateFooBar = new ModelState();
+ modelStateFooBar.Value = HtmlHelperTest.GetValueProviderResult(new string[] { "AttemptedValueFooBar" }, "AttemptedValueFooBar");
+ viewDataWithErrors.ModelState["FieldPrefix"] = modelStateFooBar;
+
+ HtmlHelper<FooBarModel> helper = MvcHelper.GetHtmlHelper(viewDataWithErrors);
+
+ // Act & Assert
+ Assert.Equal("AttemptedValueFoo", helper.Value("foo").ToHtmlString());
+ Assert.Equal("AttemptedValueFoo", helper.ValueFor(m => m.foo).ToHtmlString());
+ Assert.Equal("AttemptedValueFooBar", helper.ValueForModel().ToHtmlString());
+ }
+
+ [Fact]
+ public void ValueHelpersWithEmptyNameConvertModelValueUsingCurrentCulture()
+ {
+ // Arrange
+ HtmlHelper<FooBarModel> helper = MvcHelper.GetHtmlHelper(GetValueViewData());
+ string expectedModelValue = "{ foo = ViewItemFoo, bar = 1900/01/01 12:00:00 AM }";
+
+ // Act & Assert
+ using (HtmlHelperTest.ReplaceCulture("en-ZA", "en-US"))
+ {
+ Assert.Equal(expectedModelValue, helper.Value(name: String.Empty).ToHtmlString());
+ Assert.Equal(expectedModelValue, helper.ValueFor(m => m).ToHtmlString());
+ Assert.Equal(expectedModelValue, helper.ValueForModel().ToHtmlString());
+ }
+ }
+
+ [Fact]
+ public void ValueHelpersFormatValue()
+ {
+ // Arrange
+ HtmlHelper<FooBarModel> helper = MvcHelper.GetHtmlHelper(GetValueViewData());
+ string expectedModelValue = "-{ foo = ViewItemFoo, bar = 1900/01/01 12:00:00 AM }-";
+ string expectedBarValue = "-1900/01/01 12:00:00 AM-";
+
+ // Act & Assert
+ using (HtmlHelperTest.ReplaceCulture("en-ZA", "en-US"))
+ {
+ Assert.Equal(expectedModelValue, helper.ValueForModel("-{0}-").ToHtmlString());
+ Assert.Equal(expectedBarValue, helper.Value("bar", "-{0}-").ToHtmlString());
+ Assert.Equal(expectedBarValue, helper.ValueFor(m => m.bar, "-{0}-").ToHtmlString());
+ }
+ }
+
+ [Fact]
+ public void ValueHelpersEncodeValue()
+ {
+ // Arrange
+ ViewDataDictionary<FooBarModel> viewData = new ViewDataDictionary<FooBarModel> { { "foo", @"ViewDataFoo <"">" } };
+ viewData.Model = new FooBarModel { foo = @"ViewItemFoo <"">" };
+
+ ModelState modelStateFoo = new ModelState();
+ modelStateFoo.Value = HtmlHelperTest.GetValueProviderResult(new string[] { @"AttemptedValueBar <"">" }, @"AttemptedValueBar <"">");
+ viewData.ModelState["bar"] = modelStateFoo;
+
+ HtmlHelper<FooBarModel> helper = MvcHelper.GetHtmlHelper(viewData);
+
+ // Act & Assert
+ Assert.Equal("&lt;{ foo = ViewItemFoo &lt;&quot;>, bar = (null) }", helper.ValueForModel("<{0}").ToHtmlString());
+ Assert.Equal("&lt;ViewDataFoo &lt;&quot;>", helper.Value("foo", "<{0}").ToHtmlString());
+ Assert.Equal("&lt;ViewItemFoo &lt;&quot;>", helper.ValueFor(m => m.foo, "<{0}").ToHtmlString());
+ Assert.Equal("AttemptedValueBar &lt;&quot;>", helper.ValueFor(m => m.bar).ToHtmlString());
+ }
+
+ private sealed class FooBarModel
+ {
+ public string foo { get; set; }
+ public object bar { get; set; }
+
+ public override string ToString()
+ {
+ return String.Format("{{ foo = {0}, bar = {1} }}", foo ?? "(null)", bar ?? "(null)");
+ }
+ }
+
+ private static ViewDataDictionary<FooBarModel> GetValueViewData()
+ {
+ ViewDataDictionary<FooBarModel> viewData = new ViewDataDictionary<FooBarModel> { { "foo", "ViewDataFoo" } };
+ viewData.Model = new FooBarModel { foo = "ViewItemFoo", bar = new DateTime(1900, 1, 1, 0, 0, 0) };
+
+ return viewData;
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Properties/AssemblyInfo.cs b/test/System.Web.Mvc.Test/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..8c114539
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Properties/AssemblyInfo.cs
@@ -0,0 +1,3 @@
+using System;
+
+[assembly: CLSCompliant(true)]
diff --git a/test/System.Web.Mvc.Test/Razor/Test/MvcCSharpRazorCodeGeneratorTest.cs b/test/System.Web.Mvc.Test/Razor/Test/MvcCSharpRazorCodeGeneratorTest.cs
new file mode 100644
index 00000000..0991c16b
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Razor/Test/MvcCSharpRazorCodeGeneratorTest.cs
@@ -0,0 +1,69 @@
+using System.Collections.Generic;
+using System.Web.Razor;
+using Moq;
+using Xunit;
+
+namespace System.Web.Mvc.Razor.Test
+{
+ public class MvcCSharpRazorCodeGeneratorTest
+ {
+ [Fact]
+ public void Constructor()
+ {
+ // Arrange
+ Mock<RazorEngineHost> mockHost = new Mock<RazorEngineHost>();
+
+ // Act
+ var generator = new MvcCSharpRazorCodeGenerator("FooClass", "Root.Namespace", "SomeSourceFile.cshtml", mockHost.Object);
+
+ // Assert
+ Assert.Equal("FooClass", generator.ClassName);
+ Assert.Equal("Root.Namespace", generator.RootNamespaceName);
+ Assert.Equal("SomeSourceFile.cshtml", generator.SourceFileName);
+ Assert.Same(mockHost.Object, generator.Host);
+ }
+
+ [Fact]
+ public void Constructor_DoesNotSetBaseTypeForNonMvcHost()
+ {
+ // Arrange
+ Mock<RazorEngineHost> mockHost = new Mock<RazorEngineHost>();
+ mockHost.SetupGet(h => h.NamespaceImports).Returns(new HashSet<string>());
+
+ // Act
+ var generator = new MvcCSharpRazorCodeGenerator("FooClass", "Root.Namespace", "SomeSourceFile.cshtml", mockHost.Object);
+
+ // Assert
+ Assert.Equal(0, generator.Context.GeneratedClass.BaseTypes.Count);
+ }
+
+ [Fact]
+ public void Constructor_DoesNotSetBaseTypeForSpecialPage()
+ {
+ // Arrange
+ Mock<MvcWebPageRazorHost> mockHost = new Mock<MvcWebPageRazorHost>("_viewStart.cshtml", "_viewStart.cshtml");
+ mockHost.SetupGet(h => h.NamespaceImports).Returns(new HashSet<string>());
+
+ // Act
+ var generator = new MvcCSharpRazorCodeGenerator("FooClass", "Root.Namespace", "_viewStart.cshtml", mockHost.Object);
+
+ // Assert
+ Assert.Equal(0, generator.Context.GeneratedClass.BaseTypes.Count);
+ }
+
+ [Fact]
+ public void Constructor_SetsBaseTypeForRegularPage()
+ {
+ // Arrange
+ Mock<MvcWebPageRazorHost> mockHost = new Mock<MvcWebPageRazorHost>("SomeSourceFile.cshtml", "SomeSourceFile.cshtml") { CallBase = true };
+ mockHost.SetupGet(h => h.NamespaceImports).Returns(new HashSet<string>());
+
+ // Act
+ var generator = new MvcCSharpRazorCodeGenerator("FooClass", "Root.Namespace", "SomeSourceFile.cshtml", mockHost.Object);
+
+ // Assert
+ Assert.Equal(1, generator.Context.GeneratedClass.BaseTypes.Count);
+ Assert.Equal("System.Web.Mvc.WebViewPage<dynamic>", generator.Context.GeneratedClass.BaseTypes[0].BaseType);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Razor/Test/MvcCSharpRazorCodeParserTest.cs b/test/System.Web.Mvc.Test/Razor/Test/MvcCSharpRazorCodeParserTest.cs
new file mode 100644
index 00000000..280e1a50
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Razor/Test/MvcCSharpRazorCodeParserTest.cs
@@ -0,0 +1,284 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Web.Razor;
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Test.Framework;
+using System.Web.Razor.Text;
+using Xunit;
+
+namespace System.Web.Mvc.Razor.Test
+{
+ public class MvcCSharpRazorCodeParserTest
+ {
+ [Fact]
+ public void Constructor_AddsModelKeyword()
+ {
+ var parser = new TestMvcCSharpRazorCodeParser();
+
+ Assert.True(parser.HasDirective("model"));
+ }
+
+ [Fact]
+ public void ParseModelKeyword_HandlesSingleInstance()
+ {
+ // Arrange + Act
+ var document = "@model Foo";
+ var spans = ParseDocument(document);
+
+ // Assert
+ var factory = SpanFactory.CreateCsHtml();
+ var expectedSpans = new Span[]
+ {
+ factory.EmptyHtml(),
+ factory.CodeTransition(SyntaxConstants.TransitionString)
+ .Accepts(AcceptedCharacters.None),
+ factory.MetaCode("model ")
+ .Accepts(AcceptedCharacters.None),
+ factory.Code(" Foo")
+ .As(new SetModelTypeCodeGenerator("Foo", "{0}<{1}>"))
+ };
+ Assert.Equal(expectedSpans, spans.ToArray());
+ }
+
+ [Fact]
+ public void ParseModelKeyword_HandlesNullableTypes()
+ {
+ // Arrange + Act
+ var document = "@model Foo?\r\nBar";
+ var spans = ParseDocument(document);
+
+ // Assert
+ var factory = SpanFactory.CreateCsHtml();
+ var expectedSpans = new Span[]
+ {
+ factory.EmptyHtml(),
+ factory.CodeTransition(SyntaxConstants.TransitionString)
+ .Accepts(AcceptedCharacters.None),
+ factory.MetaCode("model ")
+ .Accepts(AcceptedCharacters.None),
+ factory.Code("Foo?\r\n")
+ .As(new SetModelTypeCodeGenerator("Foo?", "{0}<{1}>")),
+ factory.Markup("Bar")
+ .With(new MarkupCodeGenerator())
+ };
+ Assert.Equal(expectedSpans, spans.ToArray());
+ }
+
+ [Fact]
+ public void ParseModelKeyword_HandlesArrays()
+ {
+ // Arrange + Act
+ var document = "@model Foo[[]][]\r\nBar";
+ var spans = ParseDocument(document);
+
+ // Assert
+ var factory = SpanFactory.CreateCsHtml();
+ var expectedSpans = new Span[]
+ {
+ factory.EmptyHtml(),
+ factory.CodeTransition(SyntaxConstants.TransitionString)
+ .Accepts(AcceptedCharacters.None),
+ factory.MetaCode("model ")
+ .Accepts(AcceptedCharacters.None),
+ factory.Code("Foo[[]][]\r\n")
+ .As(new SetModelTypeCodeGenerator("Foo[[]][]", "{0}<{1}>")),
+ factory.Markup("Bar")
+ .With(new MarkupCodeGenerator())
+ };
+ Assert.Equal(expectedSpans, spans.ToArray());
+ }
+
+ [Fact]
+ public void ParseModelKeyword_HandlesVSTemplateSyntax()
+ {
+ // Arrange + Act
+ var document = "@model $rootnamespace$.MyModel";
+ var spans = ParseDocument(document);
+
+ // Assert
+ var factory = SpanFactory.CreateCsHtml();
+ var expectedSpans = new Span[]
+ {
+ factory.EmptyHtml(),
+ factory.CodeTransition(SyntaxConstants.TransitionString)
+ .Accepts(AcceptedCharacters.None),
+ factory.MetaCode("model ")
+ .Accepts(AcceptedCharacters.None),
+ factory.Code("$rootnamespace$.MyModel")
+ .As(new SetModelTypeCodeGenerator("$rootnamespace$.MyModel", "{0}<{1}>"))
+ };
+ Assert.Equal(expectedSpans, spans.ToArray());
+ }
+
+ [Fact]
+ public void ParseModelKeyword_ErrorOnMissingModelType()
+ {
+ // Arrange + Act
+ List<RazorError> errors = new List<RazorError>();
+ var document = "@model ";
+ var spans = ParseDocument(document, errors);
+
+ // Assert
+ var factory = SpanFactory.CreateCsHtml();
+ var expectedSpans = new Span[]
+ {
+ factory.EmptyHtml(),
+ factory.CodeTransition(SyntaxConstants.TransitionString)
+ .Accepts(AcceptedCharacters.None),
+ factory.MetaCode("model ")
+ .Accepts(AcceptedCharacters.None),
+ factory.Code(" ")
+ .As(new SetModelTypeCodeGenerator(String.Empty, "{0}<{1}>")),
+ };
+ var expectedErrors = new[]
+ {
+ new RazorError("The 'model' keyword must be followed by a type name on the same line.", new SourceLocation(9, 0, 9), 1)
+ };
+ Assert.Equal(expectedSpans, spans.ToArray());
+ Assert.Equal(expectedErrors, errors.ToArray());
+ }
+
+ [Fact]
+ public void ParseModelKeyword_ErrorOnMultipleModelStatements()
+ {
+ // Arrange + Act
+ List<RazorError> errors = new List<RazorError>();
+ var document =
+ @"@model Foo
+@model Bar";
+ var spans = ParseDocument(document, errors);
+
+ // Assert
+ var factory = SpanFactory.CreateCsHtml();
+ var expectedSpans = new Span[]
+ {
+ factory.EmptyHtml(),
+ factory.CodeTransition(SyntaxConstants.TransitionString)
+ .Accepts(AcceptedCharacters.None),
+ factory.MetaCode("model ")
+ .Accepts(AcceptedCharacters.None),
+ factory.Code("Foo\r\n")
+ .As(new SetModelTypeCodeGenerator("Foo", "{0}<{1}>")),
+ factory.CodeTransition(SyntaxConstants.TransitionString)
+ .Accepts(AcceptedCharacters.None),
+ factory.MetaCode("model ")
+ .Accepts(AcceptedCharacters.None),
+ factory.Code("Bar")
+ .As(new SetModelTypeCodeGenerator("Bar", "{0}<{1}>"))
+ };
+
+ var expectedErrors = new[]
+ {
+ new RazorError("Only one 'model' statement is allowed in a file.", new SourceLocation(18, 1, 6), 1)
+ };
+ expectedSpans.Zip(spans, (exp, span) => new { expected = exp, span = span }).ToList().ForEach(i => Assert.Equal(i.expected, i.span));
+ Assert.Equal(expectedSpans, spans.ToArray());
+ Assert.Equal(expectedErrors, errors.ToArray());
+ }
+
+ [Fact]
+ public void ParseModelKeyword_ErrorOnModelFollowedByInherits()
+ {
+ // Arrange + Act
+ List<RazorError> errors = new List<RazorError>();
+ var document =
+ @"@model Foo
+@inherits Bar";
+ var spans = ParseDocument(document, errors);
+
+ // Assert
+ var factory = SpanFactory.CreateCsHtml();
+ var expectedSpans = new Span[]
+ {
+ factory.EmptyHtml(),
+ factory.CodeTransition(SyntaxConstants.TransitionString)
+ .Accepts(AcceptedCharacters.None),
+ factory.MetaCode("model ")
+ .Accepts(AcceptedCharacters.None),
+ factory.Code("Foo\r\n")
+ .As(new SetModelTypeCodeGenerator("Foo", "{0}<{1}>")),
+ factory.CodeTransition(SyntaxConstants.TransitionString)
+ .Accepts(AcceptedCharacters.None),
+ factory.MetaCode("inherits ")
+ .Accepts(AcceptedCharacters.None),
+ factory.Code("Bar")
+ .As(new SetBaseTypeCodeGenerator("Bar"))
+ };
+
+ var expectedErrors = new[]
+ {
+ new RazorError("The 'inherits' keyword is not allowed when a 'model' keyword is used.", new SourceLocation(21, 1, 9), 1)
+ };
+ expectedSpans.Zip(spans, (exp, span) => new { expected = exp, span = span }).ToList().ForEach(i => Assert.Equal(i.expected, i.span));
+ Assert.Equal(expectedSpans, spans.ToArray());
+ Assert.Equal(expectedErrors, errors.ToArray());
+ }
+
+ [Fact]
+ public void ParseModelKeyword_ErrorOnInheritsFollowedByModel()
+ {
+ // Arrange + Act
+ List<RazorError> errors = new List<RazorError>();
+ var document =
+ @"@inherits Bar
+@model Foo";
+ var spans = ParseDocument(document, errors);
+
+ // Assert
+ var factory = SpanFactory.CreateCsHtml();
+ var expectedSpans = new Span[]
+ {
+ factory.EmptyHtml(),
+ factory.CodeTransition(SyntaxConstants.TransitionString)
+ .Accepts(AcceptedCharacters.None),
+ factory.MetaCode("inherits ")
+ .Accepts(AcceptedCharacters.None),
+ factory.Code("Bar\r\n")
+ .As(new SetBaseTypeCodeGenerator("Bar")),
+ factory.CodeTransition(SyntaxConstants.TransitionString)
+ .Accepts(AcceptedCharacters.None),
+ factory.MetaCode("model ")
+ .Accepts(AcceptedCharacters.None),
+ factory.Code("Foo")
+ .As(new SetModelTypeCodeGenerator("Foo", "{0}<{1}>"))
+ };
+
+ var expectedErrors = new[]
+ {
+ new RazorError("The 'inherits' keyword is not allowed when a 'model' keyword is used.", new SourceLocation(9, 0, 9), 1)
+ };
+ expectedSpans.Zip(spans, (exp, span) => new { expected = exp, span = span }).ToList().ForEach(i => Assert.Equal(i.expected, i.span));
+ Assert.Equal(expectedSpans, spans.ToArray());
+ Assert.Equal(expectedErrors, errors.ToArray());
+ }
+
+ private static List<Span> ParseDocument(string documentContents, IList<RazorError> errors = null)
+ {
+ errors = errors ?? new List<RazorError>();
+ var markupParser = new HtmlMarkupParser();
+ var codeParser = new TestMvcCSharpRazorCodeParser();
+ var context = new ParserContext(new SeekableTextReader(documentContents), codeParser, markupParser, markupParser);
+ codeParser.Context = context;
+ markupParser.Context = context;
+ markupParser.ParseDocument();
+
+ ParserResults results = context.CompleteParse();
+ foreach (RazorError error in results.ParserErrors)
+ {
+ errors.Add(error);
+ }
+ return results.Document.Flatten().ToList();
+ }
+
+ private sealed class TestMvcCSharpRazorCodeParser : MvcCSharpRazorCodeParser
+ {
+ public bool HasDirective(string directive)
+ {
+ Action handler;
+ return TryGetDirectiveHandler(directive, out handler);
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Razor/Test/MvcVBRazorCodeParserTest.cs b/test/System.Web.Mvc.Test/Razor/Test/MvcVBRazorCodeParserTest.cs
new file mode 100644
index 00000000..8826f0fe
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Razor/Test/MvcVBRazorCodeParserTest.cs
@@ -0,0 +1,301 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Web.Razor;
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Test.Framework;
+using System.Web.Razor.Text;
+using Xunit;
+
+namespace System.Web.Mvc.Razor.Test
+{
+ public class MvcVBRazorCodeParserTest
+ {
+ [Fact]
+ public void Constructor_AddsModelKeyword()
+ {
+ var parser = new MvcVBRazorCodeParser();
+
+ Assert.True(parser.IsDirectiveDefined(MvcVBRazorCodeParser.ModelTypeKeyword));
+ }
+
+ [Fact]
+ public void ParseModelKeyword_HandlesSingleInstance()
+ {
+ // Arrange + Act
+ var document = "@ModelType Foo";
+ var spans = ParseDocument(document);
+
+ // Assert
+ var factory = SpanFactory.CreateVbHtml();
+ var expectedSpans = new Span[]
+ {
+ factory.EmptyHtml(),
+ factory.CodeTransition(SyntaxConstants.TransitionString)
+ .Accepts(AcceptedCharacters.None),
+ factory.MetaCode("ModelType ")
+ .Accepts(AcceptedCharacters.None),
+ factory.Code("Foo")
+ .As(new SetModelTypeCodeGenerator("Foo", "{0}(Of {1})"))
+ };
+ Assert.Equal(expectedSpans, spans.ToArray());
+ }
+
+ [Fact]
+ public void ParseModelKeyword_HandlesNullableTypes()
+ {
+ // Arrange + Act
+ var document = "@ModelType Foo?\r\nBar";
+ var spans = ParseDocument(document);
+
+ // Assert
+ var factory = SpanFactory.CreateVbHtml();
+ var expectedSpans = new Span[]
+ {
+ factory.EmptyHtml(),
+ factory.CodeTransition(SyntaxConstants.TransitionString)
+ .Accepts(AcceptedCharacters.None),
+ factory.MetaCode("ModelType ")
+ .Accepts(AcceptedCharacters.None),
+ factory.Code("Foo?\r\n")
+ .As(new SetModelTypeCodeGenerator("Foo?", "{0}(Of {1})")),
+ factory.Markup("Bar")
+ };
+ Assert.Equal(expectedSpans, spans.ToArray());
+ }
+
+ [Fact]
+ public void ParseModelKeyword_HandlesArrays()
+ {
+ // Arrange + Act
+ var document = "@ModelType Foo(())()\r\nBar";
+ var spans = ParseDocument(document);
+
+ // Assert
+ var factory = SpanFactory.CreateVbHtml();
+ var expectedSpans = new Span[]
+ {
+ factory.EmptyHtml(),
+ factory.CodeTransition(SyntaxConstants.TransitionString)
+ .Accepts(AcceptedCharacters.None),
+ factory.MetaCode("ModelType ")
+ .Accepts(AcceptedCharacters.None),
+ factory.Code("Foo(())()\r\n")
+ .As(new SetModelTypeCodeGenerator("Foo(())()", "{0}(Of {1})")),
+ factory.Markup("Bar")
+ };
+ Assert.Equal(expectedSpans, spans.ToArray());
+ }
+
+ [Fact]
+ public void ParseModelKeyword_HandlesVSTemplateSyntax()
+ {
+ // Arrange + Act
+ var document = "@ModelType $rootnamespace$.MyModel";
+ var spans = ParseDocument(document);
+
+ // Assert
+ var factory = SpanFactory.CreateVbHtml();
+ var expectedSpans = new Span[]
+ {
+ factory.EmptyHtml(),
+ factory.CodeTransition(SyntaxConstants.TransitionString)
+ .Accepts(AcceptedCharacters.None),
+ factory.MetaCode("ModelType ")
+ .Accepts(AcceptedCharacters.None),
+ factory.Code("$rootnamespace$.MyModel")
+ .As(new SetModelTypeCodeGenerator("$rootnamespace$.MyModel", "{0}(Of {1})"))
+ };
+ Assert.Equal(expectedSpans, spans.ToArray());
+ }
+
+ [Fact]
+ public void ParseModelKeyword_ErrorOnMissingModelType()
+ {
+ // Arrange + Act
+ List<RazorError> errors = new List<RazorError>();
+ var document = "@ModelType ";
+ var spans = ParseDocument(document, errors);
+
+ // Assert
+ var factory = SpanFactory.CreateVbHtml();
+ var expectedSpans = new Span[]
+ {
+ factory.EmptyHtml(),
+ factory.CodeTransition(SyntaxConstants.TransitionString)
+ .Accepts(AcceptedCharacters.None),
+ factory.MetaCode("ModelType ")
+ .Accepts(AcceptedCharacters.None),
+ factory.EmptyVB()
+ .As(new SetModelTypeCodeGenerator(String.Empty, "{0}(Of {1})"))
+ .Accepts(AcceptedCharacters.Any)
+ };
+ var expectedErrors = new[]
+ {
+ new RazorError("The 'ModelType' keyword must be followed by a type name on the same line.", new SourceLocation(10, 0, 10), 1)
+ };
+ Assert.Equal(expectedSpans, spans.ToArray());
+ Assert.Equal(expectedErrors, errors.ToArray());
+ }
+
+ [Fact]
+ public void ParseModelKeyword_DoesNotAcceptNewlineIfInDesignTimeMode()
+ {
+ // Arrange + Act
+ List<RazorError> errors = new List<RazorError>();
+ var document = "@ModelType foo\r\n";
+ var spans = ParseDocument(document, errors, designTimeMode: true);
+
+ // Assert
+ var factory = SpanFactory.CreateVbHtml();
+ var expectedSpans = new Span[]
+ {
+ factory.EmptyHtml(),
+ factory.CodeTransition(SyntaxConstants.TransitionString)
+ .Accepts(AcceptedCharacters.None),
+ factory.MetaCode("ModelType ")
+ .Accepts(AcceptedCharacters.None),
+ factory.Code("foo")
+ .As(new SetModelTypeCodeGenerator("foo", "{0}(Of {1})"))
+ .Accepts(AcceptedCharacters.Any),
+ factory.Markup("\r\n")
+ };
+ Assert.Equal(expectedSpans, spans.ToArray());
+ Assert.Equal(0, errors.Count);
+ }
+
+ [Fact]
+ public void ParseModelKeyword_ErrorOnMultipleModelStatements()
+ {
+ // Arrange + Act
+ List<RazorError> errors = new List<RazorError>();
+ var document =
+ @"@ModelType Foo
+@ModelType Bar";
+ var spans = ParseDocument(document, errors);
+
+ // Assert
+ var factory = SpanFactory.CreateVbHtml();
+ var expectedSpans = new Span[]
+ {
+ factory.EmptyHtml(),
+ factory.CodeTransition(SyntaxConstants.TransitionString)
+ .Accepts(AcceptedCharacters.None),
+ factory.MetaCode("ModelType ")
+ .Accepts(AcceptedCharacters.None),
+ factory.Code("Foo\r\n")
+ .As(new SetModelTypeCodeGenerator("Foo", "{0}(Of {1})")),
+ factory.CodeTransition(SyntaxConstants.TransitionString)
+ .Accepts(AcceptedCharacters.None),
+ factory.MetaCode("ModelType ")
+ .Accepts(AcceptedCharacters.None),
+ factory.Code("Bar")
+ .As(new SetModelTypeCodeGenerator("Bar", "{0}(Of {1})"))
+ };
+
+ var expectedErrors = new[]
+ {
+ new RazorError("Only one 'ModelType' statement is allowed in a file.", new SourceLocation(26, 1, 10), 1)
+ };
+ expectedSpans.Zip(spans, (exp, span) => new { expected = exp, span = span }).ToList().ForEach(i => Assert.Equal(i.expected, i.span));
+ Assert.Equal(expectedSpans, spans.ToArray());
+ Assert.Equal(expectedErrors, errors.ToArray());
+ }
+
+ [Fact]
+ public void ParseModelKeyword_ErrorOnModelFollowedByInherits()
+ {
+ // Arrange + Act
+ List<RazorError> errors = new List<RazorError>();
+ var document =
+ @"@ModelType Foo
+@Inherits Bar";
+ var spans = ParseDocument(document, errors);
+
+ // Assert
+ var factory = SpanFactory.CreateVbHtml();
+ var expectedSpans = new Span[]
+ {
+ factory.EmptyHtml(),
+ factory.CodeTransition(SyntaxConstants.TransitionString)
+ .Accepts(AcceptedCharacters.None),
+ factory.MetaCode("ModelType ")
+ .Accepts(AcceptedCharacters.None),
+ factory.Code("Foo\r\n")
+ .As(new SetModelTypeCodeGenerator("Foo", "{0}(Of {1})")),
+ factory.CodeTransition(SyntaxConstants.TransitionString)
+ .Accepts(AcceptedCharacters.None),
+ factory.MetaCode("Inherits ")
+ .Accepts(AcceptedCharacters.None),
+ factory.Code("Bar")
+ .As(new SetBaseTypeCodeGenerator("Bar"))
+ };
+
+ var expectedErrors = new[]
+ {
+ new RazorError("The 'inherits' keyword is not allowed when a 'ModelType' keyword is used.", new SourceLocation(25, 1, 9), 1)
+ };
+ expectedSpans.Zip(spans, (exp, span) => new { expected = exp, span = span }).ToList().ForEach(i => Assert.Equal(i.expected, i.span));
+ Assert.Equal(expectedSpans, spans.ToArray());
+ Assert.Equal(expectedErrors, errors.ToArray());
+ }
+
+ [Fact]
+ public void ParseModelKeyword_ErrorOnInheritsFollowedByModel()
+ {
+ // Arrange + Act
+ List<RazorError> errors = new List<RazorError>();
+ var document =
+ @"@Inherits Bar
+@ModelType Foo";
+ var spans = ParseDocument(document, errors);
+
+ // Assert
+ var factory = SpanFactory.CreateVbHtml();
+ var expectedSpans = new Span[]
+ {
+ factory.EmptyHtml(),
+ factory.CodeTransition(SyntaxConstants.TransitionString)
+ .Accepts(AcceptedCharacters.None),
+ factory.MetaCode("Inherits ")
+ .Accepts(AcceptedCharacters.None),
+ factory.Code("Bar\r\n")
+ .AsBaseType("Bar"),
+ factory.CodeTransition(SyntaxConstants.TransitionString)
+ .Accepts(AcceptedCharacters.None),
+ factory.MetaCode("ModelType ")
+ .Accepts(AcceptedCharacters.None),
+ factory.Code("Foo")
+ .As(new SetModelTypeCodeGenerator("Foo", "{0}(Of {1})"))
+ };
+
+ var expectedErrors = new[]
+ {
+ new RazorError("The 'inherits' keyword is not allowed when a 'ModelType' keyword is used.", new SourceLocation(9, 0, 9), 1)
+ };
+ expectedSpans.Zip(spans, (exp, span) => new { expected = exp, span = span }).ToList().ForEach(i => Assert.Equal(i.expected, i.span));
+ Assert.Equal(expectedSpans, spans.ToArray());
+ Assert.Equal(expectedErrors, errors.ToArray());
+ }
+
+ private static List<Span> ParseDocument(string documentContents, List<RazorError> errors = null, bool designTimeMode = false)
+ {
+ errors = errors ?? new List<RazorError>();
+ var markupParser = new HtmlMarkupParser();
+ var codeParser = new MvcVBRazorCodeParser();
+ var context = new ParserContext(new SeekableTextReader(documentContents), codeParser, markupParser, markupParser);
+ context.DesignTimeMode = designTimeMode;
+ codeParser.Context = context;
+ markupParser.Context = context;
+ markupParser.ParseDocument();
+
+ ParserResults results = context.CompleteParse();
+ foreach (RazorError error in results.ParserErrors)
+ {
+ errors.Add(error);
+ }
+ return results.Document.Flatten().ToList();
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Razor/Test/MvcWebPageRazorHostTest.cs b/test/System.Web.Mvc.Test/Razor/Test/MvcWebPageRazorHostTest.cs
new file mode 100644
index 00000000..9354cbe7
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Razor/Test/MvcWebPageRazorHostTest.cs
@@ -0,0 +1,107 @@
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Razor.Test
+{
+ public class MvcWebPageRazorHostTest
+ {
+ [Fact]
+ public void Constructor()
+ {
+ MvcWebPageRazorHost host = new MvcWebPageRazorHost("foo.cshtml", "bar");
+
+ Assert.Equal("foo.cshtml", host.VirtualPath);
+ Assert.Equal("bar", host.PhysicalPath);
+ Assert.Equal(typeof(WebViewPage).FullName, host.DefaultBaseClass);
+ }
+
+ [Fact]
+ public void ConstructorRemovesUnwantedNamespaceImports()
+ {
+ MvcWebPageRazorHost host = new MvcWebPageRazorHost("foo.cshtml", "bar");
+
+ Assert.False(host.NamespaceImports.Contains("System.Web.WebPages.Html"));
+
+ // Even though MVC no longer needs to remove the following two namespaces
+ // (because they are no longer imported by System.Web.WebPages), we want
+ // to make sure that they don't get introduced again by default.
+ Assert.False(host.NamespaceImports.Contains("WebMatrix.Data"));
+ Assert.False(host.NamespaceImports.Contains("WebMatrix.WebData"));
+ }
+
+#if VB_ENABLED
+ [Fact]
+ public void DecorateGodeGenerator_ReplacesVBCodeGeneratorWithMvcSpecificOne() {
+ // Arrange
+ MvcWebPageRazorHost host = new MvcWebPageRazorHost("foo.vbhtml", "bar");
+ var generator = new VBRazorCodeGenerator("someClass", "root.name", "foo.vbhtml", host);
+
+ // Act
+ var result = host.DecorateCodeGenerator(generator);
+
+ // Assert
+ Assert.IsType<MvcVBRazorCodeGenerator>(result);
+ Assert.Equal("someClass", result.ClassName);
+ Assert.Equal("root.name", result.RootNamespaceName);
+ Assert.Equal("foo.vbhtml", result.SourceFileName);
+ Assert.Same(host, result.Host);
+ }
+#endif
+
+ [Fact]
+ public void DecorateGodeGenerator_ReplacesCSharpCodeGeneratorWithMvcSpecificOne()
+ {
+ // Arrange
+ MvcWebPageRazorHost host = new MvcWebPageRazorHost("foo.cshtml", "bar");
+ var generator = new CSharpRazorCodeGenerator("someClass", "root.name", "foo.cshtml", host);
+
+ // Act
+ var result = host.DecorateCodeGenerator(generator);
+
+ // Assert
+ Assert.IsType<MvcCSharpRazorCodeGenerator>(result);
+ Assert.Equal("someClass", result.ClassName);
+ Assert.Equal("root.name", result.RootNamespaceName);
+ Assert.Equal("foo.cshtml", result.SourceFileName);
+ Assert.Same(host, result.Host);
+ }
+
+ [Fact]
+ public void DecorateCodeParser_ThrowsOnNull()
+ {
+ MvcWebPageRazorHost host = new MvcWebPageRazorHost("foo.cshtml", "bar");
+ Assert.ThrowsArgumentNull(delegate() { host.DecorateCodeParser(null); }, "incomingCodeParser");
+ }
+
+ [Fact]
+ public void DecorateCodeParser_ReplacesCSharpCodeParserWithMvcSpecificOne()
+ {
+ // Arrange
+ MvcWebPageRazorHost host = new MvcWebPageRazorHost("foo.cshtml", "bar");
+ var parser = new CSharpCodeParser();
+
+ // Act
+ var result = host.DecorateCodeParser(parser);
+
+ // Assert
+ Assert.IsType<MvcCSharpRazorCodeParser>(result);
+ }
+
+#if VB_ENABLED
+ [Fact]
+ public void DecorateCodeParser_ReplacesVBCodeParserWithMvcSpecificOne() {
+ // Arrange
+ MvcWebPageRazorHost host = new MvcWebPageRazorHost("foo.vbhtml", "bar");
+ var parser = new VBCodeParser();
+
+ // Act
+ var result = host.DecorateCodeParser(parser);
+
+ // Assert
+ Assert.IsType<MvcVBRazorCodeParser>(result);
+ }
+#endif
+ }
+}
diff --git a/test/System.Web.Mvc.Test/System.Web.Mvc.Test.csproj b/test/System.Web.Mvc.Test/System.Web.Mvc.Test.csproj
new file mode 100644
index 00000000..23692c17
--- /dev/null
+++ b/test/System.Web.Mvc.Test/System.Web.Mvc.Test.csproj
@@ -0,0 +1,356 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <!-- Temporarily disable Obsolete Warnings as Errors -->
+ <WarningsNotAsErrors>618</WarningsNotAsErrors>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProductVersion>9.0.30729</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{8AC2A2E4-2F11-4D40-A887-62E2583A65E6}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>System.Web</RootNamespace>
+ <AssemblyName>System.Web.Mvc.Test</AssemblyName>
+ <FileAlignment>512</FileAlignment>
+ <ProjectTypeGuids>{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Debug\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Release\Test\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'CodeCoverage' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\CodeCoverage\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="Microsoft.CSharp" />
+ <Reference Include="Moq, Version=4.0.10827.0, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL">
+ <HintPath>..\..\packages\Moq.4.0.10827\lib\NET40\Moq.dll</HintPath>
+ </Reference>
+ <Reference Include="System" />
+ <Reference Include="System.ComponentModel.DataAnnotations" />
+ <Reference Include="System.Configuration" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.Data.Entity" />
+ <Reference Include="System.Data.Linq" />
+ <Reference Include="System.Runtime.Caching" />
+ <Reference Include="System.Web" />
+ <Reference Include="System.Web.Abstractions" />
+ <Reference Include="System.Web.Routing" />
+ <Reference Include="xunit">
+ <HintPath>..\..\packages\xunit.1.9.0.1566\lib\xunit.dll</HintPath>
+ </Reference>
+ <Reference Include="xunit.extensions">
+ <HintPath>..\..\packages\xunit.extensions.1.9.0.1566\lib\xunit.extensions.dll</HintPath>
+ </Reference>
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="Async\Test\TaskAsyncActionDescriptorTest.cs" />
+ <Compile Include="Async\Test\TaskWrapperAsyncResultTest.cs" />
+ <Compile Include="Html\Test\DisplayNameExtensionsTest.cs" />
+ <Compile Include="Html\Test\NameExtensionsTest.cs" />
+ <Compile Include="Html\Test\ValueExtensionsTest.cs" />
+ <Compile Include="Razor\Test\MvcCSharpRazorCodeGeneratorTest.cs" />
+ <Compile Include="Test\AjaxHelper`1Test.cs" />
+ <Compile Include="Test\CancellationTokenModelBinderTest.cs" />
+ <Compile Include="Test\CachedAssociatedMetadataProviderTest.cs" />
+ <Compile Include="Test\CachedDataAnnotationsModelMetadataProviderTest.cs" />
+ <Compile Include="Test\ChildActionValueProviderFactoryTest.cs" />
+ <Compile Include="ExpressionUtil\Test\BinaryExpressionFingerprintTest.cs" />
+ <Compile Include="ExpressionUtil\Test\CachedExpressionCompilerTest.cs" />
+ <Compile Include="ExpressionUtil\Test\ConditionalExpressionFingerprintTest.cs" />
+ <Compile Include="ExpressionUtil\Test\ConstantExpressionFingerprintTest.cs" />
+ <Compile Include="ExpressionUtil\Test\DefaultExpressionFingerprintTest.cs" />
+ <Compile Include="ExpressionUtil\Test\DummyExpressionFingerprint.cs" />
+ <Compile Include="ExpressionUtil\Test\ExpressionFingerprintTest.cs" />
+ <Compile Include="ExpressionUtil\Test\FingerprintingExpressionVisitorTest.cs" />
+ <Compile Include="ExpressionUtil\Test\HoistingExpressionVisitorTest.cs" />
+ <Compile Include="ExpressionUtil\Test\IndexExpressionFingerprintTest.cs" />
+ <Compile Include="ExpressionUtil\Test\LambdaExpressionFingerprintTest.cs" />
+ <Compile Include="ExpressionUtil\Test\MemberExpressionFingerprintTest.cs" />
+ <Compile Include="ExpressionUtil\Test\MethodCallExpressionFingerprintTest.cs" />
+ <Compile Include="ExpressionUtil\Test\ParameterExpressionFingerprintTest.cs" />
+ <Compile Include="ExpressionUtil\Test\TypeBinaryExpressionFingerprintTest.cs" />
+ <Compile Include="ExpressionUtil\Test\UnaryExpressionFingerprintTest.cs" />
+ <Compile Include="Test\AdditionalMetadataAttributeTest.cs" />
+ <Compile Include="Test\BuildManagerCompiledViewTest.cs" />
+ <Compile Include="Test\BuildManagerViewEngineTest.cs" />
+ <Compile Include="Test\CompareAttributeTest.cs" />
+ <Compile Include="Test\DataAnnotationsModelMetadataProviderTestBase.cs" />
+ <Compile Include="Test\DataTypeUtilTest.cs" />
+ <Compile Include="Test\FilterInfoTest.cs" />
+ <Compile Include="Test\AllowHtmlAttributeTest.cs" />
+ <Compile Include="Test\HtmlHelper`1Test.cs" />
+ <Compile Include="Test\MockableUnvalidatedRequestValues.cs" />
+ <Compile Include="Test\DescriptorUtilTest.cs" />
+ <Compile Include="Razor\Test\MvcVBRazorCodeParserTest.cs" />
+ <Compile Include="Test\ModelBinderProviderCollectionTest.cs" />
+ <Compile Include="Test\ModelBinderProvidersTest.cs" />
+ <Compile Include="Razor\Test\MvcWebPageRazorHostTest.cs" />
+ <Compile Include="Ajax\Test\AjaxExtensionsTest.cs" />
+ <Compile Include="Ajax\Test\AjaxOptionsTest.cs" />
+ <Compile Include="Async\Test\AsyncActionMethodSelectorTest.cs" />
+ <Compile Include="Async\Test\AsyncControllerActionInvokerTest.cs" />
+ <Compile Include="Async\Test\AsyncUtilTest.cs" />
+ <Compile Include="Async\Test\AsyncActionDescriptorTest.cs" />
+ <Compile Include="Test\AsyncControllerTest.cs" />
+ <Compile Include="Async\Test\SynchronousOperationExceptionTest.cs" />
+ <Compile Include="Async\Test\AsyncManagerTest.cs" />
+ <Compile Include="Async\Test\AsyncResultWrapperTest.cs" />
+ <Compile Include="Test\AsyncTimeoutAttributeTest.cs" />
+ <Compile Include="Test\ClientDataTypeModelValidatorProviderTest.cs" />
+ <Compile Include="Test\ControllerInstanceFilterProviderTest.cs" />
+ <Compile Include="Test\PreApplicationStartCodeTest.cs" />
+ <Compile Include="Test\RemoteAttributeTest.cs" />
+ <Compile Include="Test\UrlParameterTest.cs" />
+ <Compile Include="Test\UrlRewriterHelperTest.cs" />
+ <Compile Include="Test\ViewStartPageTest.cs" />
+ <Compile Include="Test\RazorViewEngineTest.cs" />
+ <Compile Include="Test\RazorViewTest.cs" />
+ <Compile Include="Test\DynamicViewDataDictionaryTest.cs" />
+ <Compile Include="Test\FilterAttributeFilterProviderTest.cs" />
+ <Compile Include="Test\FilterProviderCollectionTest.cs" />
+ <Compile Include="Test\FilterProvidersTest.cs" />
+ <Compile Include="Test\FilterTest.cs" />
+ <Compile Include="Test\GlobalFilterCollectionTest.cs" />
+ <Compile Include="Test\HttpFileCollectionValueProviderFactoryTest.cs" />
+ <Compile Include="Test\HttpNotFoundResultTest.cs" />
+ <Compile Include="Test\HttpStatusCodeResultTest.cs" />
+ <Compile Include="Test\JsonValueProviderFactoryTest.cs" />
+ <Compile Include="Razor\Test\MvcCSharpRazorCodeParserTest.cs" />
+ <Compile Include="Test\MultiServiceResolverTest.cs" />
+ <Compile Include="Test\DependencyResolverTest.cs" />
+ <Compile Include="Test\MvcWebRazorHostFactoryTest.cs" />
+ <Compile Include="Test\RangeAttributeAdapterTest.cs" />
+ <Compile Include="Test\RegularExpressionAttributeAdapterTest.cs" />
+ <Compile Include="Test\RequiredAttributeAdapterTest.cs" />
+ <Compile Include="Test\RouteDataValueProviderFactoryTest.cs" />
+ <Compile Include="Test\QueryStringValueProviderFactoryTest.cs" />
+ <Compile Include="Test\FormValueProviderFactoryTest.cs" />
+ <Compile Include="Test\SingleServiceResolverTest.cs" />
+ <Compile Include="Test\StringLengthAttributeAdapterTest.cs" />
+ <Compile Include="Test\ValidatableObjectAdapterTest.cs" />
+ <Compile Include="Test\ValueProviderFactoryCollectionTest.cs" />
+ <Compile Include="Test\TypeCacheUtilTest.cs" />
+ <Compile Include="Test\TypeCacheSerializerTest.cs" />
+ <Compile Include="Test\NoAsyncTimeoutAttributeTest.cs" />
+ <Compile Include="Async\Test\OperationCounterTest.cs" />
+ <Compile Include="Async\Test\ReflectedAsyncActionDescriptorTest.cs" />
+ <Compile Include="Async\Test\ReflectedAsyncControllerDescriptorTest.cs" />
+ <Compile Include="Async\Test\SignalContainer.cs" />
+ <Compile Include="Async\Test\TriggerListenerTest.cs" />
+ <Compile Include="Async\Test\MockAsyncResult.cs" />
+ <Compile Include="Async\Test\SimpleAsyncResultTest.cs" />
+ <Compile Include="Async\Test\SynchronizationContextUtilTest.cs" />
+ <Compile Include="Html\Test\ChildActionExtensionsTest.cs" />
+ <Compile Include="Html\Test\DefaultDisplayTemplatesTest.cs" />
+ <Compile Include="Html\Test\DefaultEditorTemplatesTest.cs" />
+ <Compile Include="Html\Test\FormExtensionsTest.cs" />
+ <Compile Include="Html\Test\InputExtensionsTest.cs" />
+ <Compile Include="Html\Test\LabelExtensionsTest.cs" />
+ <Compile Include="Html\Test\LinkExtensionsTest.cs" />
+ <Compile Include="Html\Test\MvcFormTest.cs" />
+ <Compile Include="Html\Test\PartialExtensionsTest.cs" />
+ <Compile Include="Html\Test\RenderPartialExtensionsTest.cs" />
+ <Compile Include="Html\Test\SelectExtensionsTest.cs" />
+ <Compile Include="Html\Test\TemplateHelpersTest.cs" />
+ <Compile Include="Html\Test\TextAreaExtensionsTest.cs" />
+ <Compile Include="Html\Test\ValidationExtensionsTest.cs" />
+ <Compile Include="Test\ActionExecutedContextTest.cs" />
+ <Compile Include="Test\ActionExecutingContextTest.cs" />
+ <Compile Include="Test\AssociatedValidatorProviderTest.cs" />
+ <Compile Include="Test\BindAttributeTest.cs" />
+ <Compile Include="Test\AssociatedMetadataProviderTest.cs" />
+ <Compile Include="Test\ByteArrayModelBinderTest.cs" />
+ <Compile Include="Test\ChildActionOnlyAttributeTest.cs" />
+ <Compile Include="Test\ControllerBaseTest.cs" />
+ <Compile Include="Test\ActionMethodSelectorTest.cs" />
+ <Compile Include="Test\ActionNameAttributeTest.cs" />
+ <Compile Include="Test\AcceptVerbsAttributeTest.cs" />
+ <Compile Include="Test\ControllerContextTest.cs" />
+ <Compile Include="Test\AuthorizationContextTest.cs" />
+ <Compile Include="Test\ParameterInfoUtilTest.cs" />
+ <Compile Include="Test\ModelValidationResultTest.cs" />
+ <Compile Include="Test\HttpHandlerUtilTest.cs" />
+ <Compile Include="Test\ValueProviderFactoriesTest.cs" />
+ <Compile Include="Test\ValueProviderCollectionTest.cs" />
+ <Compile Include="Test\HttpFileCollectionValueProviderTest.cs" />
+ <Compile Include="Test\DictionaryValueProviderTest.cs" />
+ <Compile Include="Test\NameValueCollectionValueProviderTest.cs" />
+ <Compile Include="Test\ValueProviderUtilTest.cs" />
+ <Compile Include="Test\DataErrorInfoModelValidatorProviderTest.cs" />
+ <Compile Include="Test\ModelValidatorProviderCollectionTest.cs" />
+ <Compile Include="Test\HttpVerbAttributeHelper.cs" />
+ <Compile Include="Test\HttpDeleteAttributeTest.cs" />
+ <Compile Include="Test\HttpPutAttributeTest.cs" />
+ <Compile Include="Test\HttpGetAttributeTest.cs" />
+ <Compile Include="Test\DataAnnotationsModelValidatorTest.cs" />
+ <Compile Include="Test\EmptyModelValidatorProviderTest.cs" />
+ <Compile Include="Test\ModelValidatorProvidersTest.cs" />
+ <Compile Include="Test\ModelValidatorTest.cs" />
+ <Compile Include="Test\MvcHtmlStringTest.cs" />
+ <Compile Include="Test\DataAnnotationsModelValidatorProviderTest.cs" />
+ <Compile Include="Test\ExpressionHelperTest.cs" />
+ <Compile Include="Test\FormContextTest.cs" />
+ <Compile Include="Test\FieldValidationMetadataTest.cs" />
+ <Compile Include="Test\ModelClientValidationRuleTest.cs" />
+ <Compile Include="Test\RequireHttpsAttributeTest.cs" />
+ <Compile Include="Test\HttpRequestExtensionsTest.cs" />
+ <Compile Include="Test\DataAnnotationsModelMetadataProviderTest.cs" />
+ <Compile Include="Test\ModelMetadataProvidersTest.cs" />
+ <Compile Include="Test\ModelMetadataTest.cs" />
+ <Compile Include="Test\AreaHelpersTest.cs" />
+ <Compile Include="Test\AreaRegistrationContextTest.cs" />
+ <Compile Include="Test\AreaRegistrationTest.cs" />
+ <Compile Include="Async\Test\SingleEntryGateTest.cs" />
+ <Compile Include="Test\HttpPostAttributeTest.cs" />
+ <Compile Include="Test\LinqBinaryModelBinderTest.cs" />
+ <Compile Include="Test\MockHelpers.cs" />
+ <Compile Include="Test\PathHelpersTest.cs" />
+ <Compile Include="Test\ExceptionContextTest.cs" />
+ <Compile Include="Test\ModelBindingContextTest.cs" />
+ <Compile Include="Test\ResultExecutedContextTest.cs" />
+ <Compile Include="Test\ResultExecutingContextTest.cs" />
+ <Compile Include="Test\ValidateAntiForgeryTokenAttributeTest.cs" />
+ <Compile Include="Test\DictionaryHelpersTest.cs" />
+ <Compile Include="Test\AjaxRequestExtensionsTest.cs" />
+ <Compile Include="Test\JavaScriptResultTest.cs" />
+ <Compile Include="Test\ModelBinderDictionaryTest.cs" />
+ <Compile Include="Test\DefaultViewLocationCacheTest.cs" />
+ <Compile Include="Test\FormCollectionTest.cs" />
+ <Compile Include="Test\HttpPostedFileBaseModelBinderTest.cs" />
+ <Compile Include="Test\ValidateInputAttributeTest.cs" />
+ <Compile Include="Test\FileContentResultTest.cs" />
+ <Compile Include="Test\FilePathResultTest.cs" />
+ <Compile Include="Test\FileResultTest.cs" />
+ <Compile Include="Test\FileStreamResultTest.cs" />
+ <Compile Include="Test\ControllerDescriptorTest.cs" />
+ <Compile Include="Test\ControllerDescriptorCacheTest.cs" />
+ <Compile Include="Test\ReaderWriterCacheTest.cs" />
+ <Compile Include="Test\ReflectedParameterBindingInfoTest.cs" />
+ <Compile Include="Test\ParameterBindingInfoTest.cs" />
+ <Compile Include="Test\ReflectedControllerDescriptorTest.cs" />
+ <Compile Include="Test\ActionDescriptorTest.cs" />
+ <Compile Include="Test\ReflectedActionDescriptorTest.cs" />
+ <Compile Include="Test\ReflectedParameterDescriptorTest.cs" />
+ <Compile Include="Test\ParameterDescriptorTest.cs" />
+ <Compile Include="Test\DefaultModelBinderTest.cs" />
+ <Compile Include="Test\ValueProviderDictionaryTest.cs" />
+ <Compile Include="Test\ValueProviderResultTest.cs" />
+ <Compile Include="Test\ModelBinderAttributeTest.cs" />
+ <Compile Include="Test\ModelBindersTest.cs" />
+ <Compile Include="Test\ViewContextTest.cs" />
+ <Compile Include="Test\ViewDataInfoTest.cs" />
+ <Compile Include="Test\ViewEngineResultTest.cs" />
+ <Compile Include="Test\NonActionAttributeTest.cs" />
+ <Compile Include="Test\ModelStateDictionaryTest.cs" />
+ <Compile Include="Test\ModelStateTest.cs" />
+ <Compile Include="Test\ModelErrorCollectionTest.cs" />
+ <Compile Include="Test\ModelErrorTest.cs" />
+ <Compile Include="Test\AjaxHelperTest.cs" />
+ <Compile Include="Test\AuthorizeAttributeTest.cs" />
+ <Compile Include="Test\ControllerActionInvokerTest.cs" />
+ <Compile Include="Test\ActionFilterAttributeTest.cs" />
+ <Compile Include="Test\ControllerBuilderTest.cs" />
+ <Compile Include="Test\ControllerTest.cs" />
+ <Compile Include="Test\ContentResultTest.cs" />
+ <Compile Include="Test\ActionMethodDispatcherTest.cs" />
+ <Compile Include="Test\ActionMethodDispatcherCacheTest.cs" />
+ <Compile Include="Test\PartialViewResultTest.cs" />
+ <Compile Include="Test\ViewEngineCollectionTest.cs" />
+ <Compile Include="Test\ViewMasterPageControlBuilderTest.cs" />
+ <Compile Include="Test\ViewPageControlBuilderTest.cs" />
+ <Compile Include="Test\ViewTypeParserFilterTest.cs" />
+ <Compile Include="Test\ViewResultBaseTest.cs" />
+ <Compile Include="Test\ViewUserControlControlBuilderTest.cs" />
+ <Compile Include="Test\VirtualPathProviderViewEngineTest.cs" />
+ <Compile Include="Test\WebFormViewTest.cs" />
+ <Compile Include="Test\MvcHttpHandlerTest.cs" />
+ <Compile Include="Test\HandleErrorAttributeTest.cs" />
+ <Compile Include="Test\HandleErrorInfoTest.cs" />
+ <Compile Include="Test\HttpUnauthorizedResultTest.cs" />
+ <Compile Include="Test\SessionStateTempDataProviderTest.cs" />
+ <Compile Include="Test\OutputCacheAttributeTest.cs" />
+ <Compile Include="Test\JsonResultTest.cs" />
+ <Compile Include="Test\NameValueCollectionExtensionsTest.cs" />
+ <Compile Include="Test\MockBuildManager.cs" />
+ <Compile Include="Test\DefaultControllerFactoryTest.cs" />
+ <Compile Include="Test\HtmlHelperTest.cs" />
+ <Compile Include="Test\MultiSelectListTest.cs" />
+ <Compile Include="Test\MvcHandlerTest.cs" />
+ <Compile Include="Test\MvcRouteHandlerTest.cs" />
+ <Compile Include="Test\MvcTestHelper.cs" />
+ <Compile Include="Test\RedirectResultTest.cs" />
+ <Compile Include="Test\RedirectToRouteResultTest.cs" />
+ <Compile Include="Test\RouteCollectionExtensionsTest.cs" />
+ <Compile Include="Test\SelectListTest.cs" />
+ <Compile Include="Test\TempDataDictionaryTest.cs" />
+ <Compile Include="Test\TypeHelpersTest.cs" />
+ <Compile Include="Test\UrlHelperTest.cs" />
+ <Compile Include="Test\ViewDataDictionaryTest.cs" />
+ <Compile Include="Test\ViewEnginesTest.cs" />
+ <Compile Include="Test\ViewMasterPageTest.cs" />
+ <Compile Include="Test\ViewPageTest.cs" />
+ <Compile Include="Test\ViewResultTest.cs" />
+ <Compile Include="Test\ViewUserControlTest.cs" />
+ <Compile Include="Test\WebFormViewEngineTest.cs" />
+ <Compile Include="Util\AnonymousObject.cs" />
+ <Compile Include="Util\DictionaryHelper.cs" />
+ <Compile Include="Util\HttpContextHelpers.cs" />
+ <Compile Include="Util\MvcHelper.cs" />
+ <Compile Include="Util\Resolver.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="Util\SimpleValueProvider.cs" />
+ <Compile Include="Util\SimpleViewDataContainer.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\System.Web.Mvc\System.Web.Mvc.csproj">
+ <Project>{3D3FFD8A-624D-4E9B-954B-E1C105507975}</Project>
+ <Name>System.Web.Mvc</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\src\System.Web.Razor\System.Web.Razor.csproj">
+ <Project>{8F18041B-9410-4C36-A9C5-067813DF5F31}</Project>
+ <Name>System.Web.Razor</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\src\System.Web.WebPages.Razor\System.Web.WebPages.Razor.csproj">
+ <Project>{0939B11A-FE4E-4BA1-8AD6-D97741EE314F}</Project>
+ <Name>System.Web.WebPages.Razor</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\src\System.Web.WebPages\System.Web.WebPages.csproj">
+ <Project>{76EFA9C5-8D7E-4FDF-B710-E20F8B6B00D2}</Project>
+ <Name>System.Web.WebPages</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\Microsoft.TestCommon\Microsoft.TestCommon.csproj">
+ <Project>{FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0}</Project>
+ <Name>Microsoft.TestCommon</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\System.Web.Razor.Test\System.Web.Razor.Test.csproj">
+ <Project>{0BB62A1D-E6B5-49FA-9E3C-6AF679A66DFE}</Project>
+ <Name>System.Web.Razor.Test</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="packages.config" />
+ </ItemGroup>
+ <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+</Project> \ No newline at end of file
diff --git a/test/System.Web.Mvc.Test/Test/AcceptVerbsAttributeTest.cs b/test/System.Web.Mvc.Test/Test/AcceptVerbsAttributeTest.cs
new file mode 100644
index 00000000..483befba
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/AcceptVerbsAttributeTest.cs
@@ -0,0 +1,214 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.Linq;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class AcceptVerbsAttributeTest
+ {
+ private const string _invalidEnumFormatString = @"The enum '{0}' did not produce the correct array.
+Expected: {1}
+Actual: {2}";
+
+ [Fact]
+ public void ConstructorThrowsIfVerbsIsEmpty()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { new AcceptVerbsAttribute(new string[0]); }, "verbs");
+ }
+
+ [Fact]
+ public void ConstructorThrowsIfVerbsIsNull()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { new AcceptVerbsAttribute((string[])null); }, "verbs");
+ }
+
+ [Fact]
+ public void EnumToArray()
+ {
+ // Arrange
+ IDictionary<string, HttpVerbs> enumValues = EnumToDictionary<HttpVerbs>();
+ var allCombinations = EnumerableToCombinations(enumValues);
+
+ // Act & assert
+ foreach (var combination in allCombinations)
+ {
+ // generate all the names + values in this combination
+ List<string> aggrNames = new List<string>();
+ HttpVerbs aggrValues = (HttpVerbs)0;
+ foreach (var entry in combination)
+ {
+ aggrNames.Add(entry.Key);
+ aggrValues |= entry.Value;
+ }
+
+ // get the resulting array
+ string[] array = AcceptVerbsAttribute.EnumToArray(aggrValues);
+ var aggrNamesOrdered = aggrNames.OrderBy(name => name, StringComparer.OrdinalIgnoreCase);
+ var arrayOrdered = array.OrderBy(name => name, StringComparer.OrdinalIgnoreCase);
+ bool match = aggrNamesOrdered.SequenceEqual(arrayOrdered, StringComparer.OrdinalIgnoreCase);
+
+ if (!match)
+ {
+ string message = String.Format(_invalidEnumFormatString, aggrValues,
+ aggrNames.Aggregate((a, b) => a + ", " + b),
+ array.Aggregate((a, b) => a + ", " + b));
+ Assert.True(false, message);
+ }
+ }
+ }
+
+ [Fact]
+ public void IsValidForRequestReturnsFalseIfHttpVerbIsNotInVerbsCollection()
+ {
+ // Arrange
+ AcceptVerbsAttribute attr = new AcceptVerbsAttribute("get", "post");
+ ControllerContext context = GetControllerContextWithHttpVerb("HEAD");
+
+ // Act
+ bool result = attr.IsValidForRequest(context, null);
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void IsValidForRequestReturnsTrueIfHttpVerbIsInVerbsCollection()
+ {
+ // Arrange
+ AcceptVerbsAttribute attr = new AcceptVerbsAttribute("get", "post");
+ ControllerContext context = GetControllerContextWithHttpVerb("POST");
+
+ // Act
+ bool result = attr.IsValidForRequest(context, null);
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void IsValidForRequestReturnsTrueIfHttpVerbIsOverridden()
+ {
+ // Arrange
+ AcceptVerbsAttribute attr = new AcceptVerbsAttribute("put");
+ ControllerContext context = GetControllerContextWithHttpVerb("POST", "PUT", null, null);
+
+ // Act
+ bool result = attr.IsValidForRequest(context, null);
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void IsValidForRequestThrowsIfControllerContextIsNull()
+ {
+ // Arrange
+ AcceptVerbsAttribute attr = new AcceptVerbsAttribute("get", "post");
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { attr.IsValidForRequest(null, null); }, "controllerContext");
+ }
+
+ [Fact]
+ public void VerbsPropertyFromEnumConstructor()
+ {
+ // Arrange
+ AcceptVerbsAttribute attr = new AcceptVerbsAttribute(HttpVerbs.Get | HttpVerbs.Post);
+
+ // Act
+ ReadOnlyCollection<string> collection = attr.Verbs as ReadOnlyCollection<string>;
+
+ // Assert
+ Assert.NotNull(collection);
+ Assert.Equal(2, collection.Count);
+ Assert.Equal("GET", collection[0]);
+ Assert.Equal("POST", collection[1]);
+ }
+
+ [Fact]
+ public void VerbsPropertyFromStringArrayConstructor()
+ {
+ // Arrange
+ AcceptVerbsAttribute attr = new AcceptVerbsAttribute("get", "post");
+
+ // Act
+ ReadOnlyCollection<string> collection = attr.Verbs as ReadOnlyCollection<string>;
+
+ // Assert
+ Assert.NotNull(collection);
+ Assert.Equal(2, collection.Count);
+ Assert.Equal("get", collection[0]);
+ Assert.Equal("post", collection[1]);
+ }
+
+ internal static ControllerContext GetControllerContextWithHttpVerb(string httpRequestVerb)
+ {
+ return GetControllerContextWithHttpVerb(httpRequestVerb, null, null, null);
+ }
+
+ internal static ControllerContext GetControllerContextWithHttpVerb(string httpRequestVerb, string httpHeaderVerb, string httpFormVerb, string httpQueryStringVerb)
+ {
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.Setup(c => c.HttpContext.Request.HttpMethod).Returns(httpRequestVerb);
+
+ NameValueCollection headers = new NameValueCollection();
+ if (!String.IsNullOrEmpty(httpHeaderVerb))
+ {
+ headers.Add(HttpRequestExtensions.XHttpMethodOverrideKey, httpHeaderVerb);
+ }
+ mockControllerContext.Setup(c => c.HttpContext.Request.Headers).Returns(headers);
+
+ NameValueCollection form = new NameValueCollection();
+ if (!String.IsNullOrEmpty(httpFormVerb))
+ {
+ form.Add(HttpRequestExtensions.XHttpMethodOverrideKey, httpFormVerb);
+ }
+ mockControllerContext.Setup(c => c.HttpContext.Request.Form).Returns(form);
+
+ NameValueCollection queryString = new NameValueCollection();
+ if (!String.IsNullOrEmpty(httpQueryStringVerb))
+ {
+ queryString.Add(HttpRequestExtensions.XHttpMethodOverrideKey, httpQueryStringVerb);
+ }
+ mockControllerContext.Setup(c => c.HttpContext.Request.QueryString).Returns(queryString);
+
+ return mockControllerContext.Object;
+ }
+
+ private static IDictionary<string, TEnum> EnumToDictionary<TEnum>()
+ {
+ // Arrange
+ var values = Enum.GetValues(typeof(TEnum)).Cast<TEnum>();
+ return values.ToDictionary(value => Enum.GetName(typeof(TEnum), value), value => value);
+ }
+
+ private static IEnumerable<ICollection<T>> EnumerableToCombinations<T>(IEnumerable<T> elements)
+ {
+ List<T> allElements = elements.ToList();
+
+ int maxCount = 1 << allElements.Count;
+ for (int idxCombination = 0; idxCombination < maxCount; idxCombination++)
+ {
+ List<T> thisCollection = new List<T>();
+ for (int idxBit = 0; idxBit < 32; idxBit++)
+ {
+ bool bitActive = (((uint)idxCombination >> idxBit) & 1) != 0;
+ if (bitActive)
+ {
+ thisCollection.Add(allElements[idxBit]);
+ }
+ }
+ yield return thisCollection;
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ActionDescriptorTest.cs b/test/System.Web.Mvc.Test/Test/ActionDescriptorTest.cs
new file mode 100644
index 00000000..d1abfbd6
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ActionDescriptorTest.cs
@@ -0,0 +1,280 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Reflection;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ActionDescriptorTest
+ {
+ [Fact]
+ public void ExtractParameterOrDefaultFromDictionary_ReturnsDefaultParameterValueIfMismatch()
+ {
+ // Arrange
+ Dictionary<string, object> dictionary = new Dictionary<string, object>()
+ {
+ { "stringParameterWithDefaultValue", 42 }
+ };
+
+ // Act
+ object value = ActionDescriptor.ExtractParameterOrDefaultFromDictionary(ParameterExtractionController.StringParameterWithDefaultValue, dictionary);
+
+ // Assert
+ Assert.Equal("hello", value);
+ }
+
+ [Fact]
+ public void ExtractParameterOrDefaultFromDictionary_ReturnsDefaultTypeValueIfNoMatchAndNoDefaultParameterValue()
+ {
+ // Arrange
+ Dictionary<string, object> dictionary = new Dictionary<string, object>();
+
+ // Act
+ object value = ActionDescriptor.ExtractParameterOrDefaultFromDictionary(ParameterExtractionController.IntParameter, dictionary);
+
+ // Assert
+ Assert.Equal(0, value);
+ }
+
+ [Fact]
+ public void ExtractParameterOrDefaultFromDictionary_ReturnsDictionaryValueIfTypeMatch()
+ {
+ // Arrange
+ Dictionary<string, object> dictionary = new Dictionary<string, object>()
+ {
+ { "stringParameterNoDefaultValue", "someValue" }
+ };
+
+ // Act
+ object value = ActionDescriptor.ExtractParameterOrDefaultFromDictionary(ParameterExtractionController.StringParameterNoDefaultValue, dictionary);
+
+ // Assert
+ Assert.Equal("someValue", value);
+ }
+
+ [Fact]
+ public void GetCustomAttributesReturnsEmptyArrayOfAttributeType()
+ {
+ // Arrange
+ ActionDescriptor ad = GetActionDescriptor();
+
+ // Act
+ ObsoleteAttribute[] attrs = (ObsoleteAttribute[])ad.GetCustomAttributes(typeof(ObsoleteAttribute), true);
+
+ // Assert
+ Assert.Empty(attrs);
+ }
+
+ [Fact]
+ public void GetCustomAttributesThrowsIfAttributeTypeIsNull()
+ {
+ // Arrange
+ ActionDescriptor ad = GetActionDescriptor();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { ad.GetCustomAttributes(null /* attributeType */, true); }, "attributeType");
+ }
+
+ [Fact]
+ public void GetCustomAttributesWithoutAttributeTypeCallsGetCustomAttributesWithAttributeType()
+ {
+ // Arrange
+ object[] expected = new object[0];
+ Mock<ActionDescriptor> mockDescriptor = new Mock<ActionDescriptor>() { CallBase = true };
+ mockDescriptor.Setup(d => d.GetCustomAttributes(typeof(object), true)).Returns(expected);
+ ActionDescriptor ad = mockDescriptor.Object;
+
+ // Act
+ object[] returned = ad.GetCustomAttributes(true /* inherit */);
+
+ // Assert
+ Assert.Same(expected, returned);
+ }
+
+ [Fact]
+ public void GetFilterAttributes_CallsGetCustomAttributes()
+ {
+ // Arrange
+ var mockDescriptor = new Mock<ActionDescriptor>() { CallBase = true };
+ mockDescriptor.Setup(d => d.GetCustomAttributes(typeof(FilterAttribute), true)).Returns(new object[] { new Mock<FilterAttribute>().Object }).Verifiable();
+
+ // Act
+ var result = mockDescriptor.Object.GetFilterAttributes(true).ToList();
+
+ // Assert
+ mockDescriptor.Verify();
+ Assert.Single(result);
+ }
+
+ [Fact]
+ public void GetSelectorsReturnsEmptyCollection()
+ {
+ // Arrange
+ ActionDescriptor ad = GetActionDescriptor();
+
+ // Act
+ ICollection<ActionSelector> selectors = ad.GetSelectors();
+
+ // Assert
+ Assert.IsType<ActionSelector[]>(selectors);
+ Assert.Empty(selectors);
+ }
+
+ [Fact]
+ public void IsDefinedReturnsFalse()
+ {
+ // Arrange
+ ActionDescriptor ad = GetActionDescriptor();
+
+ // Act
+ bool isDefined = ad.IsDefined(typeof(object), true);
+
+ // Assert
+ Assert.False(isDefined);
+ }
+
+ [Fact]
+ public void IsDefinedThrowsIfAttributeTypeIsNull()
+ {
+ // Arrange
+ ActionDescriptor ad = GetActionDescriptor();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { ad.IsDefined(null /* attributeType */, true); }, "attributeType");
+ }
+
+ [Fact]
+ public void UniqueId_SameTypeControllerDescriptorAndActionName_SameID()
+ {
+ // Arrange
+ var controllerDescriptor = new Mock<ControllerDescriptor>().Object;
+
+ var descriptor1 = new Mock<ActionDescriptor> { CallBase = true };
+ descriptor1.SetupGet(d => d.ControllerDescriptor).Returns(controllerDescriptor);
+ descriptor1.SetupGet(d => d.ActionName).Returns("Action1");
+
+ var descriptor2 = new Mock<ActionDescriptor> { CallBase = true };
+ descriptor2.SetupGet(d => d.ControllerDescriptor).Returns(controllerDescriptor);
+ descriptor2.SetupGet(d => d.ActionName).Returns("Action1");
+
+ // Act
+ var id1 = descriptor1.Object.UniqueId;
+ var id2 = descriptor2.Object.UniqueId;
+
+ // Assert
+ Assert.Equal(id1, id2);
+ }
+
+ [Fact]
+ public void UniqueId_VariesWithActionName()
+ {
+ // Arrange
+ var controllerDescriptor = new Mock<ControllerDescriptor>().Object;
+
+ var descriptor1 = new Mock<ActionDescriptor> { CallBase = true };
+ descriptor1.SetupGet(d => d.ControllerDescriptor).Returns(controllerDescriptor);
+ descriptor1.SetupGet(d => d.ActionName).Returns("Action1");
+
+ var descriptor2 = new Mock<ActionDescriptor> { CallBase = true };
+ descriptor2.SetupGet(d => d.ControllerDescriptor).Returns(controllerDescriptor);
+ descriptor2.SetupGet(d => d.ActionName).Returns("Action2");
+
+ // Act
+ var id1 = descriptor1.Object.UniqueId;
+ var id2 = descriptor2.Object.UniqueId;
+
+ // Assert
+ Assert.NotEqual(id1, id2);
+ }
+
+ [Fact]
+ public void UniqueId_VariesWithControllerDescriptorsUniqueId()
+ {
+ // Arrange
+ var controllerDescriptor1 = new Mock<ControllerDescriptor>();
+ controllerDescriptor1.SetupGet(cd => cd.UniqueId).Returns("1");
+ var descriptor1 = new Mock<ActionDescriptor> { CallBase = true };
+ descriptor1.SetupGet(d => d.ControllerDescriptor).Returns(controllerDescriptor1.Object);
+ descriptor1.SetupGet(d => d.ActionName).Returns("Action1");
+
+ var controllerDescriptor2 = new Mock<ControllerDescriptor>();
+ controllerDescriptor2.SetupGet(cd => cd.UniqueId).Returns("2");
+ var descriptor2 = new Mock<ActionDescriptor> { CallBase = true };
+ descriptor2.SetupGet(d => d.ControllerDescriptor).Returns(controllerDescriptor2.Object);
+ descriptor2.SetupGet(d => d.ActionName).Returns("Action1");
+
+ // Act
+ var id1 = descriptor1.Object.UniqueId;
+ var id2 = descriptor2.Object.UniqueId;
+
+ // Assert
+ Assert.NotEqual(id1, id2);
+ }
+
+ [Fact]
+ public void UniqueId_VariesWithActionDescriptorType()
+ {
+ // Arrange
+ var descriptor1 = new BaseDescriptor();
+ var descriptor2 = new DerivedDescriptor();
+
+ // Act
+ var id1 = descriptor1.UniqueId;
+ var id2 = descriptor2.UniqueId;
+
+ // Assert
+ Assert.NotEqual(id1, id2);
+ }
+
+ class BaseDescriptor : ActionDescriptor
+ {
+ static ControllerDescriptor controllerDescriptor = new Mock<ControllerDescriptor>().Object;
+
+ public override string ActionName
+ {
+ get { return "ActionName"; }
+ }
+
+ public override ControllerDescriptor ControllerDescriptor
+ {
+ get { return controllerDescriptor; }
+ }
+
+ public override object Execute(ControllerContext controllerContext, IDictionary<string, object> parameters)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override ParameterDescriptor[] GetParameters()
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ class DerivedDescriptor : BaseDescriptor
+ {
+ }
+
+ private static ActionDescriptor GetActionDescriptor()
+ {
+ Mock<ActionDescriptor> mockDescriptor = new Mock<ActionDescriptor>() { CallBase = true };
+ return mockDescriptor.Object;
+ }
+
+ private class ParameterExtractionController : Controller
+ {
+ public static readonly ParameterInfo IntParameter = typeof(ParameterExtractionController).GetMethod("SomeMethod").GetParameters()[0];
+ public static readonly ParameterInfo StringParameterNoDefaultValue = typeof(ParameterExtractionController).GetMethod("SomeMethod").GetParameters()[1];
+ public static readonly ParameterInfo StringParameterWithDefaultValue = typeof(ParameterExtractionController).GetMethod("SomeMethod").GetParameters()[2];
+
+ public void SomeMethod(int intParameter, string stringParameterNoDefaultValue, [DefaultValue("hello")] string stringParameterWithDefaultValue)
+ {
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ActionExecutedContextTest.cs b/test/System.Web.Mvc.Test/Test/ActionExecutedContextTest.cs
new file mode 100644
index 00000000..9f98ad4c
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ActionExecutedContextTest.cs
@@ -0,0 +1,52 @@
+using System.Web.TestUtil;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ActionExecutedContextTest
+ {
+ [Fact]
+ public void ConstructorThrowsIfActionDescriptorIsNull()
+ {
+ // Arrange
+ ControllerContext controllerContext = new Mock<ControllerContext>().Object;
+ ActionDescriptor actionDescriptor = null;
+ bool canceled = true;
+ Exception exception = null;
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new ActionExecutedContext(controllerContext, actionDescriptor, canceled, exception); }, "actionDescriptor");
+ }
+
+ [Fact]
+ public void PropertiesAreSetByConstructor()
+ {
+ // Arrange
+ ControllerContext controllerContext = new Mock<ControllerContext>().Object;
+ ActionDescriptor actionDescriptor = new Mock<ActionDescriptor>().Object;
+ bool canceled = true;
+ Exception exception = new Exception();
+
+ // Act
+ ActionExecutedContext actionExecutedContext = new ActionExecutedContext(controllerContext, actionDescriptor, canceled, exception);
+
+ // Assert
+ Assert.Equal(actionDescriptor, actionExecutedContext.ActionDescriptor);
+ Assert.Equal(canceled, actionExecutedContext.Canceled);
+ Assert.Equal(exception, actionExecutedContext.Exception);
+ }
+
+ [Fact]
+ public void ResultProperty()
+ {
+ // Arrange
+ ActionExecutedContext actionExecutedContext = new Mock<ActionExecutedContext>().Object;
+
+ // Act & assert
+ MemberHelper.TestPropertyWithDefaultInstance(actionExecutedContext, "Result", new ViewResult(), EmptyResult.Instance);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ActionExecutingContextTest.cs b/test/System.Web.Mvc.Test/Test/ActionExecutingContextTest.cs
new file mode 100644
index 00000000..db992447
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ActionExecutingContextTest.cs
@@ -0,0 +1,52 @@
+using System.Collections.Generic;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ActionExecutingContextTest
+ {
+ [Fact]
+ public void ConstructorThrowsIfActionDescriptorIsNull()
+ {
+ // Arrange
+ ControllerContext controllerContext = new Mock<ControllerContext>().Object;
+ ActionDescriptor actionDescriptor = null;
+ Dictionary<string, object> actionParameters = new Dictionary<string, object>();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new ActionExecutingContext(controllerContext, actionDescriptor, actionParameters); }, "actionDescriptor");
+ }
+
+ [Fact]
+ public void ConstructorThrowsIfActionParametersIsNull()
+ {
+ // Arrange
+ ControllerContext controllerContext = new Mock<ControllerContext>().Object;
+ ActionDescriptor actionDescriptor = new Mock<ActionDescriptor>().Object;
+ Dictionary<string, object> actionParameters = null;
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new ActionExecutingContext(controllerContext, actionDescriptor, actionParameters); }, "actionParameters");
+ }
+
+ [Fact]
+ public void PropertiesAreSetByConstructor()
+ {
+ // Arrange
+ ControllerContext controllerContext = new Mock<ControllerContext>().Object;
+ ActionDescriptor actionDescriptor = new Mock<ActionDescriptor>().Object;
+ Dictionary<string, object> actionParameters = new Dictionary<string, object>();
+
+ // Act
+ ActionExecutingContext actionExecutingContext = new ActionExecutingContext(controllerContext, actionDescriptor, actionParameters);
+
+ // Assert
+ Assert.Equal(actionDescriptor, actionExecutingContext.ActionDescriptor);
+ Assert.Equal(actionParameters, actionExecutingContext.ActionParameters);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ActionFilterAttributeTest.cs b/test/System.Web.Mvc.Test/Test/ActionFilterAttributeTest.cs
new file mode 100644
index 00000000..2d19fd13
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ActionFilterAttributeTest.cs
@@ -0,0 +1,42 @@
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ActionFilterAttributeTest
+ {
+ [Fact]
+ public void DefaultOrderIsNegativeOne()
+ {
+ // Act
+ var attr = new EmptyActionFilterAttribute();
+
+ // Assert
+ Assert.Equal(-1, attr.Order);
+ }
+
+ [Fact]
+ public void OrderIsSetCorrectly()
+ {
+ // Act
+ var attr = new EmptyActionFilterAttribute() { Order = 98052 };
+
+ // Assert
+ Assert.Equal(98052, attr.Order);
+ }
+
+ [Fact]
+ public void SpecifyingInvalidOrderThrows()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentOutOfRange(
+ delegate { new EmptyActionFilterAttribute() { Order = -2 }; },
+ "value",
+ "Order must be greater than or equal to -1.");
+ }
+
+ private class EmptyActionFilterAttribute : ActionFilterAttribute
+ {
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ActionMethodDispatcherCacheTest.cs b/test/System.Web.Mvc.Test/Test/ActionMethodDispatcherCacheTest.cs
new file mode 100644
index 00000000..d46c8b4d
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ActionMethodDispatcherCacheTest.cs
@@ -0,0 +1,24 @@
+using System.Reflection;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class ActionMethodDispatcherCacheTest
+ {
+ [Fact]
+ public void GetDispatcher()
+ {
+ // Arrange
+ MethodInfo methodInfo = typeof(object).GetMethod("ToString");
+ ActionMethodDispatcherCache cache = new ActionMethodDispatcherCache();
+
+ // Act
+ ActionMethodDispatcher dispatcher1 = cache.GetDispatcher(methodInfo);
+ ActionMethodDispatcher dispatcher2 = cache.GetDispatcher(methodInfo);
+
+ // Assert
+ Assert.Same(methodInfo, dispatcher1.MethodInfo);
+ Assert.Same(dispatcher1, dispatcher2);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ActionMethodDispatcherTest.cs b/test/System.Web.Mvc.Test/Test/ActionMethodDispatcherTest.cs
new file mode 100644
index 00000000..74242f1c
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ActionMethodDispatcherTest.cs
@@ -0,0 +1,126 @@
+using System.Reflection;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class ActionMethodDispatcherTest
+ {
+ [Fact]
+ public void ExecuteWithNormalActionMethod()
+ {
+ // Arrange
+ DispatcherController controller = new DispatcherController();
+ object[] parameters = new object[] { 5, "some string", new DateTime(2001, 1, 1) };
+ MethodInfo methodInfo = typeof(DispatcherController).GetMethod("NormalAction");
+ ActionMethodDispatcher dispatcher = new ActionMethodDispatcher(methodInfo);
+
+ // Act
+ object returnValue = dispatcher.Execute(controller, parameters);
+
+ // Assert
+ var stringResult = Assert.IsType<string>(returnValue);
+ Assert.Equal("Hello from NormalAction!", stringResult);
+
+ Assert.Equal(5, controller._i);
+ Assert.Equal("some string", controller._s);
+ Assert.Equal(new DateTime(2001, 1, 1), controller._dt);
+ }
+
+ [Fact]
+ public void ExecuteWithParameterlessActionMethod()
+ {
+ // Arrange
+ DispatcherController controller = new DispatcherController();
+ object[] parameters = new object[0];
+ MethodInfo methodInfo = typeof(DispatcherController).GetMethod("ParameterlessAction");
+ ActionMethodDispatcher dispatcher = new ActionMethodDispatcher(methodInfo);
+
+ // Act
+ object returnValue = dispatcher.Execute(controller, parameters);
+
+ // Assert
+ var intResult = Assert.IsType<int>(returnValue);
+ Assert.Equal(53, intResult);
+ }
+
+ [Fact]
+ public void ExecuteWithStaticActionMethod()
+ {
+ // Arrange
+ DispatcherController controller = new DispatcherController();
+ object[] parameters = new object[0];
+ MethodInfo methodInfo = typeof(DispatcherController).GetMethod("StaticAction");
+ ActionMethodDispatcher dispatcher = new ActionMethodDispatcher(methodInfo);
+
+ // Act
+ object returnValue = dispatcher.Execute(controller, parameters);
+
+ // Assert
+ var intResult = Assert.IsType<int>(returnValue);
+ Assert.Equal(89, intResult);
+ }
+
+ [Fact]
+ public void ExecuteWithVoidActionMethod()
+ {
+ // Arrange
+ DispatcherController controller = new DispatcherController();
+ object[] parameters = new object[] { 5, "some string", new DateTime(2001, 1, 1) };
+ MethodInfo methodInfo = typeof(DispatcherController).GetMethod("VoidAction");
+ ActionMethodDispatcher dispatcher = new ActionMethodDispatcher(methodInfo);
+
+ // Act
+ object returnValue = dispatcher.Execute(controller, parameters);
+
+ // Assert
+ Assert.Null(returnValue);
+ Assert.Equal(5, controller._i);
+ Assert.Equal("some string", controller._s);
+ Assert.Equal(new DateTime(2001, 1, 1), controller._dt);
+ }
+
+ [Fact]
+ public void MethodInfoProperty()
+ {
+ // Arrange
+ MethodInfo original = typeof(object).GetMethod("ToString");
+ ActionMethodDispatcher dispatcher = new ActionMethodDispatcher(original);
+
+ // Act
+ MethodInfo returned = dispatcher.MethodInfo;
+
+ // Assert
+ Assert.Same(original, returned);
+ }
+
+ private class DispatcherController : Controller
+ {
+ public int _i;
+ public string _s;
+ public DateTime _dt;
+
+ public object NormalAction(int i, string s, DateTime dt)
+ {
+ VoidAction(i, s, dt);
+ return "Hello from NormalAction!";
+ }
+
+ public int ParameterlessAction()
+ {
+ return 53;
+ }
+
+ public void VoidAction(int i, string s, DateTime dt)
+ {
+ _i = i;
+ _s = s;
+ _dt = dt;
+ }
+
+ public static int StaticAction()
+ {
+ return 89;
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ActionMethodSelectorTest.cs b/test/System.Web.Mvc.Test/Test/ActionMethodSelectorTest.cs
new file mode 100644
index 00000000..3f988766
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ActionMethodSelectorTest.cs
@@ -0,0 +1,212 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ActionMethodSelectorTest
+ {
+ [Fact]
+ public void AliasedMethodsProperty()
+ {
+ // Arrange
+ Type controllerType = typeof(MethodLocatorController);
+
+ // Act
+ ActionMethodSelector selector = new ActionMethodSelector(controllerType);
+
+ // Assert
+ Assert.Equal(2, selector.AliasedMethods.Length);
+
+ List<MethodInfo> sortedAliasedMethods = selector.AliasedMethods.OrderBy(methodInfo => methodInfo.Name).ToList();
+ Assert.Equal("Bar", sortedAliasedMethods[0].Name);
+ Assert.Equal("FooRenamed", sortedAliasedMethods[1].Name);
+ }
+
+ [Fact]
+ public void ControllerTypeProperty()
+ {
+ // Arrange
+ Type controllerType = typeof(MethodLocatorController);
+ ActionMethodSelector selector = new ActionMethodSelector(controllerType);
+
+ // Act & Assert
+ Assert.Same(controllerType, selector.ControllerType);
+ }
+
+ [Fact]
+ public void FindActionMethodReturnsMatchingMethodIfOneMethodMatches()
+ {
+ // Arrange
+ Type controllerType = typeof(SelectionAttributeController);
+ ActionMethodSelector selector = new ActionMethodSelector(controllerType);
+
+ // Act
+ MethodInfo matchedMethod = selector.FindActionMethod(null, "OneMatch");
+
+ // Assert
+ Assert.Equal("OneMatch", matchedMethod.Name);
+ Assert.Equal(typeof(string), matchedMethod.GetParameters()[0].ParameterType);
+ }
+
+ [Fact]
+ public void FindActionMethodReturnsMethodWithActionSelectionAttributeIfMultipleMethodsMatchRequest()
+ {
+ // DevDiv Bugs 212062: If multiple action methods match a request, we should match only the methods with an
+ // [ActionMethod] attribute since we assume those methods are more specific.
+
+ // Arrange
+ Type controllerType = typeof(SelectionAttributeController);
+ ActionMethodSelector selector = new ActionMethodSelector(controllerType);
+
+ // Act
+ MethodInfo matchedMethod = selector.FindActionMethod(null, "ShouldMatchMethodWithSelectionAttribute");
+
+ // Assert
+ Assert.Equal("MethodHasSelectionAttribute1", matchedMethod.Name);
+ }
+
+ [Fact]
+ public void FindActionMethodReturnsNullIfNoMethodMatches()
+ {
+ // Arrange
+ Type controllerType = typeof(SelectionAttributeController);
+ ActionMethodSelector selector = new ActionMethodSelector(controllerType);
+
+ // Act
+ MethodInfo matchedMethod = selector.FindActionMethod(null, "ZeroMatch");
+
+ // Assert
+ Assert.Null(matchedMethod);
+ }
+
+ [Fact]
+ public void FindActionMethodThrowsIfMultipleMethodsMatch()
+ {
+ // Arrange
+ Type controllerType = typeof(SelectionAttributeController);
+ ActionMethodSelector selector = new ActionMethodSelector(controllerType);
+
+ // Act & veriy
+ Assert.Throws<AmbiguousMatchException>(
+ delegate { selector.FindActionMethod(null, "TwoMatch"); },
+ @"The current request for action 'TwoMatch' on controller type 'SelectionAttributeController' is ambiguous between the following action methods:
+Void TwoMatch2() on type System.Web.Mvc.Test.ActionMethodSelectorTest+SelectionAttributeController
+Void TwoMatch() on type System.Web.Mvc.Test.ActionMethodSelectorTest+SelectionAttributeController");
+ }
+
+ [Fact]
+ public void NonAliasedMethodsProperty()
+ {
+ // Arrange
+ Type controllerType = typeof(MethodLocatorController);
+
+ // Act
+ ActionMethodSelector selector = new ActionMethodSelector(controllerType);
+
+ // Assert
+ Assert.Single(selector.NonAliasedMethods);
+
+ List<MethodInfo> sortedMethods = selector.NonAliasedMethods["foo"].OrderBy(methodInfo => methodInfo.GetParameters().Length).ToList();
+ Assert.Equal("Foo", sortedMethods[0].Name);
+ Assert.Empty(sortedMethods[0].GetParameters());
+ Assert.Equal("Foo", sortedMethods[1].Name);
+ Assert.Equal(typeof(string), sortedMethods[1].GetParameters()[0].ParameterType);
+ }
+
+ private class MethodLocatorController : Controller
+ {
+ public void Foo()
+ {
+ }
+
+ public void Foo(string s)
+ {
+ }
+
+ [ActionName("Foo")]
+ public void FooRenamed()
+ {
+ }
+
+ [ActionName("Bar")]
+ public void Bar()
+ {
+ }
+
+ [ActionName("PrivateVoid")]
+ private void PrivateVoid()
+ {
+ }
+
+ protected void ProtectedVoidAction()
+ {
+ }
+
+ public static void StaticMethod()
+ {
+ }
+
+ // ensure that methods inheriting from Controller or a base class are not matched
+ [ActionName("Blah")]
+ protected override void ExecuteCore()
+ {
+ throw new NotImplementedException();
+ }
+
+ public string StringProperty { get; set; }
+
+#pragma warning disable 0067
+ public event EventHandler<EventArgs> SomeEvent;
+#pragma warning restore 0067
+ }
+
+ private class SelectionAttributeController : Controller
+ {
+ [Match(false)]
+ public void OneMatch()
+ {
+ }
+
+ public void OneMatch(string s)
+ {
+ }
+
+ public void TwoMatch()
+ {
+ }
+
+ [ActionName("TwoMatch")]
+ public void TwoMatch2()
+ {
+ }
+
+ [Match(true), ActionName("ShouldMatchMethodWithSelectionAttribute")]
+ public void MethodHasSelectionAttribute1()
+ {
+ }
+
+ [ActionName("ShouldMatchMethodWithSelectionAttribute")]
+ public void MethodDoesNotHaveSelectionAttribute1()
+ {
+ }
+
+ private class MatchAttribute : ActionMethodSelectorAttribute
+ {
+ private bool _match;
+
+ public MatchAttribute(bool match)
+ {
+ _match = match;
+ }
+
+ public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
+ {
+ return _match;
+ }
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ActionNameAttributeTest.cs b/test/System.Web.Mvc.Test/Test/ActionNameAttributeTest.cs
new file mode 100644
index 00000000..1c394163
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ActionNameAttributeTest.cs
@@ -0,0 +1,60 @@
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ActionNameAttributeTest
+ {
+ [Fact]
+ public void ConstructorThrowsIfNameIsEmpty()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { new ActionNameAttribute(String.Empty); }, "name");
+ }
+
+ [Fact]
+ public void ConstructorThrowsIfNameIsNull()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { new ActionNameAttribute(null); }, "name");
+ }
+
+ [Fact]
+ public void IsValidForRequestReturnsFalseIfGivenNameDoesNotMatch()
+ {
+ // Arrange
+ ActionNameAttribute attr = new ActionNameAttribute("Bar");
+
+ // Act
+ bool returned = attr.IsValidName(null, "foo", null);
+
+ // Assert
+ Assert.False(returned);
+ }
+
+ [Fact]
+ public void IsValidForRequestReturnsTrueIfGivenNameMatches()
+ {
+ // Arrange
+ ActionNameAttribute attr = new ActionNameAttribute("Bar");
+
+ // Act
+ bool returned = attr.IsValidName(null, "bar", null);
+
+ // Assert
+ Assert.True(returned);
+ }
+
+ [Fact]
+ public void NameProperty()
+ {
+ // Arrange
+ ActionNameAttribute attr = new ActionNameAttribute("someName");
+
+ // Act & Assert
+ Assert.Equal("someName", attr.Name);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/AdditionalMetadataAttributeTest.cs b/test/System.Web.Mvc.Test/Test/AdditionalMetadataAttributeTest.cs
new file mode 100644
index 00000000..2f67dee1
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/AdditionalMetadataAttributeTest.cs
@@ -0,0 +1,73 @@
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class AdditionalMetadataAttributeTest
+ {
+ [Fact]
+ public void GuardClauses()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ () => new AdditionalMetadataAttribute(null, new object()),
+ "name");
+
+ AdditionalMetadataAttribute attr = new AdditionalMetadataAttribute("key", null);
+ Assert.ThrowsArgumentNull(
+ () => attr.OnMetadataCreated(null),
+ "metadata");
+ }
+
+ [Fact]
+ public void OnMetaDataCreatedSetsAdditionalValue()
+ {
+ // Arrange
+ string name = "name";
+ object value = new object();
+
+ ModelMetadata modelMetadata = new ModelMetadata(new Mock<ModelMetadataProvider>().Object, null, null, typeof(object), null);
+ AdditionalMetadataAttribute attr = new AdditionalMetadataAttribute(name, value);
+
+ // Act
+ attr.OnMetadataCreated(modelMetadata);
+
+ // Assert
+ Assert.Equal(modelMetadata.AdditionalValues[name], value);
+ Assert.Equal(attr.Name, name);
+ Assert.Equal(attr.Value, value);
+ }
+
+ [Fact]
+ public void MultipleAttributesCanSetValuesOnMetadata()
+ {
+ // Arrange
+ string name1 = "name1";
+ string name2 = "name2";
+
+ object value1 = new object();
+ object value2 = new object();
+ object value3 = new object();
+
+ ModelMetadata modelMetadata = new ModelMetadata(new Mock<ModelMetadataProvider>().Object, null, null, typeof(object), null);
+ AdditionalMetadataAttribute attr1 = new AdditionalMetadataAttribute(name1, value1);
+ AdditionalMetadataAttribute attr2 = new AdditionalMetadataAttribute(name2, value2);
+ AdditionalMetadataAttribute attr3 = new AdditionalMetadataAttribute(name1, value3);
+
+ // Act
+ attr1.OnMetadataCreated(modelMetadata);
+ attr2.OnMetadataCreated(modelMetadata);
+ attr3.OnMetadataCreated(modelMetadata);
+
+ // Assert
+ Assert.Equal(2, modelMetadata.AdditionalValues.Count);
+ Assert.Equal(modelMetadata.AdditionalValues[name1], value3);
+ Assert.Equal(modelMetadata.AdditionalValues[name2], value2);
+
+ Assert.NotEqual(attr1.TypeId, attr2.TypeId);
+ Assert.NotEqual(attr2.TypeId, attr3.TypeId);
+ Assert.NotEqual(attr3.TypeId, attr1.TypeId);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/AjaxHelperTest.cs b/test/System.Web.Mvc.Test/Test/AjaxHelperTest.cs
new file mode 100644
index 00000000..3cfa0b36
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/AjaxHelperTest.cs
@@ -0,0 +1,236 @@
+using System.Web.Routing;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class AjaxHelperTest
+ {
+ [Fact]
+ public void ConstructorWithNullViewContextThrows()
+ {
+ // Assert
+ Assert.ThrowsArgumentNull(
+ delegate { AjaxHelper ajaxHelper = new AjaxHelper(null, new Mock<IViewDataContainer>().Object); },
+ "viewContext");
+ }
+
+ [Fact]
+ public void ConstructorWithNullViewDataContainerThrows()
+ {
+ // Assert
+ Assert.ThrowsArgumentNull(
+ delegate { AjaxHelper ajaxHelper = new AjaxHelper(new Mock<ViewContext>().Object, null); },
+ "viewDataContainer");
+ }
+
+ [Fact]
+ public void ConstructorSetsProperties1()
+ {
+ // Arrange
+ ViewContext viewContext = new Mock<ViewContext>().Object;
+ IViewDataContainer vdc = new Mock<IViewDataContainer>().Object;
+
+ // Act
+ AjaxHelper ajaxHelper = new AjaxHelper(viewContext, vdc);
+
+ // Assert
+ Assert.Equal(viewContext, ajaxHelper.ViewContext);
+ Assert.Equal(vdc, ajaxHelper.ViewDataContainer);
+ Assert.Equal(RouteTable.Routes, ajaxHelper.RouteCollection);
+ }
+
+ [Fact]
+ public void ConstructorSetsProperties2()
+ {
+ // Arrange
+ ViewContext viewContext = new Mock<ViewContext>().Object;
+ IViewDataContainer vdc = new Mock<IViewDataContainer>().Object;
+ RouteCollection rc = new RouteCollection();
+
+ // Act
+ AjaxHelper ajaxHelper = new AjaxHelper(viewContext, vdc, rc);
+
+ // Assert
+ Assert.Equal(viewContext, ajaxHelper.ViewContext);
+ Assert.Equal(vdc, ajaxHelper.ViewDataContainer);
+ Assert.Equal(rc, ajaxHelper.RouteCollection);
+ }
+
+ [Fact]
+ public void GenericHelperConstructorSetsProperties1()
+ {
+ // Arrange
+ ViewContext viewContext = new Mock<ViewContext>().Object;
+ ViewDataDictionary<Controller> vdd = new ViewDataDictionary<Controller>(new Mock<Controller>().Object);
+ Mock<IViewDataContainer> vdc = new Mock<IViewDataContainer>();
+ vdc.Setup(v => v.ViewData).Returns(vdd);
+
+ // Act
+ AjaxHelper<Controller> ajaxHelper = new AjaxHelper<Controller>(viewContext, vdc.Object);
+
+ // Assert
+ Assert.Equal(viewContext, ajaxHelper.ViewContext);
+ Assert.Equal(vdc.Object, ajaxHelper.ViewDataContainer);
+ Assert.Equal(RouteTable.Routes, ajaxHelper.RouteCollection);
+ Assert.Equal(vdd.Model, ajaxHelper.ViewData.Model);
+ }
+
+ [Fact]
+ public void GenericHelperConstructorSetsProperties2()
+ {
+ // Arrange
+ ViewContext viewContext = new Mock<ViewContext>().Object;
+ ViewDataDictionary<Controller> vdd = new ViewDataDictionary<Controller>(new Mock<Controller>().Object);
+ Mock<IViewDataContainer> vdc = new Mock<IViewDataContainer>();
+ vdc.Setup(v => v.ViewData).Returns(vdd);
+ RouteCollection rc = new RouteCollection();
+
+ // Act
+ AjaxHelper<Controller> ajaxHelper = new AjaxHelper<Controller>(viewContext, vdc.Object, rc);
+
+ // Assert
+ Assert.Equal(viewContext, ajaxHelper.ViewContext);
+ Assert.Equal(vdc.Object, ajaxHelper.ViewDataContainer);
+ Assert.Equal(rc, ajaxHelper.RouteCollection);
+ Assert.Equal(vdd.Model, ajaxHelper.ViewData.Model);
+ }
+
+ [Fact]
+ public void GlobalizationScriptPathPropertyDefault()
+ {
+ try
+ {
+ // Act
+ AjaxHelper.GlobalizationScriptPath = null;
+
+ // Assert
+ Assert.Equal("~/Scripts/Globalization", AjaxHelper.GlobalizationScriptPath);
+ }
+ finally
+ {
+ AjaxHelper.GlobalizationScriptPath = null;
+ }
+ }
+
+ [Fact]
+ public void GlobalizationScriptPathPropertySet()
+ {
+ try
+ {
+ // Act
+ AjaxHelper.GlobalizationScriptPath = "/Foo/Bar";
+
+ // Assert
+ Assert.Equal("/Foo/Bar", AjaxHelper.GlobalizationScriptPath);
+ }
+ finally
+ {
+ AjaxHelper.GlobalizationScriptPath = null;
+ }
+ }
+
+ [Fact]
+ public void JavaScriptStringEncodeReturnsEmptyStringIfMessageIsEmpty()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper();
+
+ // Act
+ string encoded = ajaxHelper.JavaScriptStringEncode(String.Empty);
+
+ // Assert
+ Assert.Equal(String.Empty, encoded);
+ }
+
+ [Fact]
+ public void JavaScriptStringEncodeReturnsEncodedMessage()
+ {
+ // Arrange
+ string message = "I said, \"Hello, world!\"\nHow are you?";
+ AjaxHelper ajaxHelper = GetAjaxHelper();
+
+ // Act
+ string encoded = ajaxHelper.JavaScriptStringEncode(message);
+
+ // Assert
+ Assert.Equal(@"I said, \""Hello, world!\""\nHow are you?", encoded);
+ }
+
+ [Fact]
+ public void JavaScriptStringEncodeReturnsNullIfMessageIsNull()
+ {
+ // Arrange
+ AjaxHelper ajaxHelper = GetAjaxHelper();
+
+ // Act
+ string encoded = ajaxHelper.JavaScriptStringEncode(null /* message */);
+
+ // Assert
+ Assert.Null(encoded);
+ }
+
+ [Fact]
+ public void ViewBagProperty_ReflectsViewData()
+ {
+ // Arrange
+ ViewDataDictionary viewDataDictionary = new ViewDataDictionary() { { "A", 1 } };
+ Mock<IViewDataContainer> viewDataContainer = new Mock<IViewDataContainer>();
+ viewDataContainer.Setup(container => container.ViewData).Returns(viewDataDictionary);
+
+ // Act
+ AjaxHelper ajaxHelper = new AjaxHelper(new Mock<ViewContext>().Object, viewDataContainer.Object);
+
+ // Assert
+ Assert.Equal(1, ajaxHelper.ViewBag.A);
+ }
+
+ [Fact]
+ public void ViewBagProperty_ReflectsNewViewDataContainerInstance()
+ {
+ // Arrange
+ ViewDataDictionary viewDataDictionary = new ViewDataDictionary() { { "A", 1 } };
+ Mock<IViewDataContainer> viewDataContainer = new Mock<IViewDataContainer>();
+ viewDataContainer.Setup(container => container.ViewData).Returns(viewDataDictionary);
+
+ ViewDataDictionary otherViewDataDictionary = new ViewDataDictionary() { { "A", 2 } };
+ Mock<IViewDataContainer> otherViewDataContainer = new Mock<IViewDataContainer>();
+ otherViewDataContainer.Setup(container => container.ViewData).Returns(otherViewDataDictionary);
+
+ AjaxHelper ajaxHelper = new AjaxHelper(new Mock<ViewContext>().Object, viewDataContainer.Object, new RouteCollection());
+
+ // Act
+ ajaxHelper.ViewDataContainer = otherViewDataContainer.Object;
+
+ // Assert
+ Assert.Equal(2, ajaxHelper.ViewBag.A);
+ }
+
+ [Fact]
+ public void ViewBag_PropagatesChangesToViewData()
+ {
+ // Arrange
+ ViewDataDictionary viewDataDictionary = new ViewDataDictionary() { { "A", 1 } };
+ Mock<IViewDataContainer> viewDataContainer = new Mock<IViewDataContainer>();
+ viewDataContainer.Setup(container => container.ViewData).Returns(viewDataDictionary);
+
+ AjaxHelper ajaxHelper = new AjaxHelper(new Mock<ViewContext>().Object, viewDataContainer.Object, new RouteCollection());
+
+ // Act
+ ajaxHelper.ViewBag.A = "foo";
+ ajaxHelper.ViewBag.B = 2;
+
+ // Assert
+ Assert.Equal("foo", ajaxHelper.ViewData["A"]);
+ Assert.Equal(2, ajaxHelper.ViewData["B"]);
+ }
+
+ private static AjaxHelper GetAjaxHelper()
+ {
+ ViewContext viewContext = new Mock<ViewContext>().Object;
+ IViewDataContainer viewDataContainer = new Mock<IViewDataContainer>().Object;
+ return new AjaxHelper(viewContext, viewDataContainer);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/AjaxHelper`1Test.cs b/test/System.Web.Mvc.Test/Test/AjaxHelper`1Test.cs
new file mode 100644
index 00000000..41209a71
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/AjaxHelper`1Test.cs
@@ -0,0 +1,41 @@
+using Moq;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class AjaxHelper_1Test
+ {
+ [Fact]
+ public void ViewBagAndViewDataStayInSync()
+ {
+ // Arrange
+ Mock<IViewDataContainer> viewDataContainer = new Mock<IViewDataContainer>();
+ ViewDataDictionary viewDataDictionary = new ViewDataDictionary() { { "A", 1 } };
+ viewDataContainer.Setup(container => container.ViewData).Returns(viewDataDictionary);
+
+ // Act
+ AjaxHelper<object> ajaxHelper = new AjaxHelper<object>(new Mock<ViewContext>().Object, viewDataContainer.Object);
+ ajaxHelper.ViewData["B"] = 2;
+ ajaxHelper.ViewBag.C = 3;
+
+ // Assert
+
+ // Original ViewData should not be modified by redfined ViewData and ViewBag
+ AjaxHelper nonGenericAjaxHelper = ajaxHelper;
+ Assert.Single(nonGenericAjaxHelper.ViewData.Keys);
+ Assert.Equal(1, nonGenericAjaxHelper.ViewData["A"]);
+ Assert.Equal(1, nonGenericAjaxHelper.ViewBag.A);
+
+ // Redefined ViewData and ViewBag should be in sync
+ Assert.Equal(3, ajaxHelper.ViewData.Keys.Count);
+
+ Assert.Equal(1, ajaxHelper.ViewData["A"]);
+ Assert.Equal(2, ajaxHelper.ViewData["B"]);
+ Assert.Equal(3, ajaxHelper.ViewData["C"]);
+
+ Assert.Equal(1, ajaxHelper.ViewBag.A);
+ Assert.Equal(2, ajaxHelper.ViewBag.B);
+ Assert.Equal(3, ajaxHelper.ViewBag.C);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/AjaxRequestExtensionsTest.cs b/test/System.Web.Mvc.Test/Test/AjaxRequestExtensionsTest.cs
new file mode 100644
index 00000000..fbd3455b
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/AjaxRequestExtensionsTest.cs
@@ -0,0 +1,70 @@
+using System.Collections.Specialized;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class AjaxRequestExtensionsTest
+ {
+ [Fact]
+ public void IsAjaxRequestWithNullRequestThrows()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { AjaxRequestExtensions.IsAjaxRequest(null); }, "request");
+ }
+
+ [Fact]
+ public void IsAjaxRequestWithKeyIsTrue()
+ {
+ // Arrange
+ Mock<HttpRequestBase> mockRequest = new Mock<HttpRequestBase>();
+ mockRequest.Setup(r => r["X-Requested-With"]).Returns("XMLHttpRequest").Verifiable();
+ HttpRequestBase request = mockRequest.Object;
+
+ // Act
+ bool retVal = AjaxRequestExtensions.IsAjaxRequest(request);
+
+ // Assert
+ Assert.True(retVal);
+ mockRequest.Verify();
+ }
+
+ [Fact]
+ public void IsAjaxRequestWithoutKeyOrHeaderIsFalse()
+ {
+ // Arrange
+ Mock<HttpRequestBase> mockRequest = new Mock<HttpRequestBase>();
+ NameValueCollection headerCollection = new NameValueCollection();
+ mockRequest.Setup(r => r.Headers).Returns(headerCollection).Verifiable();
+ mockRequest.Setup(r => r["X-Requested-With"]).Returns((string)null).Verifiable();
+ HttpRequestBase request = mockRequest.Object;
+
+ // Act
+ bool retVal = AjaxRequestExtensions.IsAjaxRequest(request);
+
+ // Assert
+ Assert.False(retVal);
+ mockRequest.Verify();
+ }
+
+ [Fact]
+ public void IsAjaxRequestReturnsTrueIfHeaderSet()
+ {
+ // Arrange
+ Mock<HttpRequestBase> mockRequest = new Mock<HttpRequestBase>();
+ NameValueCollection headerCollection = new NameValueCollection();
+ headerCollection["X-Requested-With"] = "XMLHttpRequest";
+ mockRequest.Setup(r => r.Headers).Returns(headerCollection).Verifiable();
+ HttpRequestBase request = mockRequest.Object;
+
+ // Act
+ bool retVal = AjaxRequestExtensions.IsAjaxRequest(request);
+
+ // Assert
+ Assert.True(retVal);
+ mockRequest.Verify();
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/AllowHtmlAttributeTest.cs b/test/System.Web.Mvc.Test/Test/AllowHtmlAttributeTest.cs
new file mode 100644
index 00000000..959326a2
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/AllowHtmlAttributeTest.cs
@@ -0,0 +1,37 @@
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class AllowHtmlAttributeTest
+ {
+ [Fact]
+ public void OnMetadataCreated_ThrowsIfMetadataIsNull()
+ {
+ // Arrange
+ AllowHtmlAttribute attr = new AllowHtmlAttribute();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { attr.OnMetadataCreated(null); }, "metadata");
+ }
+
+ [Fact]
+ public void OnMetadataCreated()
+ {
+ // Arrange
+ ModelMetadata modelMetadata = new ModelMetadata(new Mock<ModelMetadataProvider>().Object, null, null, typeof(object), "SomeProperty");
+ AllowHtmlAttribute attr = new AllowHtmlAttribute();
+
+ // Act
+ bool originalValue = modelMetadata.RequestValidationEnabled;
+ attr.OnMetadataCreated(modelMetadata);
+ bool newValue = modelMetadata.RequestValidationEnabled;
+
+ // Assert
+ Assert.True(originalValue);
+ Assert.False(newValue);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/AreaHelpersTest.cs b/test/System.Web.Mvc.Test/Test/AreaHelpersTest.cs
new file mode 100644
index 00000000..ef342afb
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/AreaHelpersTest.cs
@@ -0,0 +1,97 @@
+using System.Web.Routing;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class AreaHelpersTest
+ {
+ [Fact]
+ public void GetAreaNameFromAreaRouteCollectionRoute()
+ {
+ // Arrange
+ RouteCollection routes = new RouteCollection();
+ AreaRegistrationContext context = new AreaRegistrationContext("area_name", routes);
+ Route route = context.MapRoute(null, "the_url");
+
+ // Act
+ string areaName = AreaHelpers.GetAreaName(route);
+
+ // Assert
+ Assert.Equal("area_name", areaName);
+ }
+
+ [Fact]
+ public void GetAreaNameFromIAreaAssociatedItem()
+ {
+ // Arrange
+ CustomRouteWithArea route = new CustomRouteWithArea();
+
+ // Act
+ string areaName = AreaHelpers.GetAreaName(route);
+
+ // Assert
+ Assert.Equal("area_name", areaName);
+ }
+
+ [Fact]
+ public void GetAreaNameFromRouteData()
+ {
+ // Arrange
+ RouteData routeData = new RouteData();
+ routeData.DataTokens["area"] = "area_name";
+
+ // Act
+ string areaName = AreaHelpers.GetAreaName(routeData);
+
+ // Assert
+ Assert.Equal("area_name", areaName);
+ }
+
+ [Fact]
+ public void GetAreaNameFromRouteDataFallsBackToRoute()
+ {
+ // Arrange
+ RouteCollection routes = new RouteCollection();
+ AreaRegistrationContext context = new AreaRegistrationContext("area_name", routes);
+ Route route = context.MapRoute(null, "the_url");
+ RouteData routeData = new RouteData(route, new MvcRouteHandler());
+
+ // Act
+ string areaName = AreaHelpers.GetAreaName(routeData);
+
+ // Assert
+ Assert.Equal("area_name", areaName);
+ }
+
+ [Fact]
+ public void GetAreaNameReturnsNullIfRouteNotAreaAware()
+ {
+ // Arrange
+ Route route = new Route("the_url", new MvcRouteHandler());
+
+ // Act
+ string areaName = AreaHelpers.GetAreaName(route);
+
+ // Assert
+ Assert.Null(areaName);
+ }
+
+ private class CustomRouteWithArea : RouteBase, IRouteWithArea
+ {
+ public string Area
+ {
+ get { return "area_name"; }
+ }
+
+ public override RouteData GetRouteData(HttpContextBase httpContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/AreaRegistrationContextTest.cs b/test/System.Web.Mvc.Test/Test/AreaRegistrationContextTest.cs
new file mode 100644
index 00000000..f9e3e7e9
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/AreaRegistrationContextTest.cs
@@ -0,0 +1,138 @@
+using System.Collections.Generic;
+using System.Web.Routing;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class AreaRegistrationContextTest
+ {
+ [Fact]
+ public void ConstructorSetsProperties()
+ {
+ // Arrange
+ string areaName = "the_area";
+ RouteCollection routes = new RouteCollection();
+
+ // Act
+ AreaRegistrationContext context = new AreaRegistrationContext(areaName, routes);
+
+ // Assert
+ Assert.Equal(areaName, context.AreaName);
+ Assert.Same(routes, context.Routes);
+ }
+
+ [Fact]
+ public void ConstructorThrowsIfAreaNameIsEmpty()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { new AreaRegistrationContext("", new RouteCollection()); }, "areaName");
+ }
+
+ [Fact]
+ public void ConstructorThrowsIfAreaNameIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { new AreaRegistrationContext(null, new RouteCollection()); }, "areaName");
+ }
+
+ [Fact]
+ public void ConstructorThrowsIfRoutesIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new AreaRegistrationContext("the_area", null); }, "routes");
+ }
+
+ [Fact]
+ public void MapRouteWithEmptyStringNamespaces()
+ {
+ // Arrange
+ string[] implicitNamespaces = new string[] { "implicit_1", "implicit_2" };
+ string[] explicitNamespaces = new string[0];
+
+ RouteCollection routes = new RouteCollection();
+ AreaRegistrationContext context = new AreaRegistrationContext("the_area", routes);
+ ReplaceCollectionContents(context.Namespaces, implicitNamespaces);
+
+ // Act
+ Route route = context.MapRoute("the_name", "the_url", explicitNamespaces);
+
+ // Assert
+ Assert.Equal(route, routes["the_name"]);
+ Assert.Equal("the_area", route.DataTokens["area"]);
+ Assert.Equal(true, route.DataTokens["UseNamespaceFallback"]);
+ Assert.Null(route.DataTokens["namespaces"]);
+ }
+
+ [Fact]
+ public void MapRouteWithExplicitNamespaces()
+ {
+ // Arrange
+ string[] implicitNamespaces = new string[] { "implicit_1", "implicit_2" };
+ string[] explicitNamespaces = new string[] { "explicit_1", "explicit_2" };
+
+ RouteCollection routes = new RouteCollection();
+ AreaRegistrationContext context = new AreaRegistrationContext("the_area", routes);
+ ReplaceCollectionContents(context.Namespaces, implicitNamespaces);
+
+ // Act
+ Route route = context.MapRoute("the_name", "the_url", explicitNamespaces);
+
+ // Assert
+ Assert.Equal(route, routes["the_name"]);
+ Assert.Equal("the_area", route.DataTokens["area"]);
+ Assert.Equal(false, route.DataTokens["UseNamespaceFallback"]);
+ Assert.Equal(explicitNamespaces, (string[])route.DataTokens["namespaces"]);
+ }
+
+ [Fact]
+ public void MapRouteWithImplicitNamespaces()
+ {
+ // Arrange
+ string[] implicitNamespaces = new string[] { "implicit_1", "implicit_2" };
+ string[] explicitNamespaces = new string[] { "explicit_1", "explicit_2" };
+
+ RouteCollection routes = new RouteCollection();
+ AreaRegistrationContext context = new AreaRegistrationContext("the_area", routes);
+ ReplaceCollectionContents(context.Namespaces, implicitNamespaces);
+
+ // Act
+ Route route = context.MapRoute("the_name", "the_url");
+
+ // Assert
+ Assert.Equal(route, routes["the_name"]);
+ Assert.Equal("the_area", route.DataTokens["area"]);
+ Assert.Equal(false, route.DataTokens["UseNamespaceFallback"]);
+ Assert.Equal(implicitNamespaces, (string[])route.DataTokens["namespaces"]);
+ }
+
+ [Fact]
+ public void MapRouteWithoutNamespaces()
+ {
+ // Arrange
+ RouteCollection routes = new RouteCollection();
+ AreaRegistrationContext context = new AreaRegistrationContext("the_area", routes);
+
+ // Act
+ Route route = context.MapRoute("the_name", "the_url");
+
+ // Assert
+ Assert.Equal(route, routes["the_name"]);
+ Assert.Equal("the_area", route.DataTokens["area"]);
+ Assert.Null(route.DataTokens["namespaces"]);
+ Assert.Equal(true, route.DataTokens["UseNamespaceFallback"]);
+ }
+
+ private static void ReplaceCollectionContents(ICollection<string> collectionToReplace, IEnumerable<string> newContents)
+ {
+ collectionToReplace.Clear();
+ foreach (string item in newContents)
+ {
+ collectionToReplace.Add(item);
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/AreaRegistrationTest.cs b/test/System.Web.Mvc.Test/Test/AreaRegistrationTest.cs
new file mode 100644
index 00000000..2bdd879b
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/AreaRegistrationTest.cs
@@ -0,0 +1,99 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Web.Routing;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class AreaRegistrationTest
+ {
+ [Fact]
+ public void CreateContextAndRegister()
+ {
+ // Arrange
+ string[] expectedNamespaces = new string[] { "System.Web.Mvc.Test.*" };
+
+ RouteCollection routes = new RouteCollection();
+ MyAreaRegistration registration = new MyAreaRegistration();
+
+ // Act
+ registration.CreateContextAndRegister(routes, "some state");
+
+ // Assert
+ Assert.Equal(expectedNamespaces, registration.Namespaces);
+ Assert.Equal("some state", registration.State);
+ }
+
+ [Fact]
+ public void RegisterAllAreas()
+ {
+ // Arrange
+ string[] expectedLoadedAreas = new string[] { "AreaRegistrationTest_AreaRegistration" };
+ AnnotatedRouteCollection routes = new AnnotatedRouteCollection();
+ MockBuildManager buildManager = new MockBuildManager(new Assembly[] { typeof(AreaRegistrationTest).Assembly });
+
+ // Act
+ AreaRegistration.RegisterAllAreas(routes, buildManager, null);
+
+ // Assert
+ Assert.Equal(expectedLoadedAreas, routes._areasLoaded.ToArray());
+ }
+
+ private class MyAreaRegistration : AreaRegistration
+ {
+ public string[] Namespaces;
+ public object State;
+
+ public override string AreaName
+ {
+ get { return "my_area"; }
+ }
+
+ public override void RegisterArea(AreaRegistrationContext context)
+ {
+ Namespaces = context.Namespaces.ToArray();
+ State = context.State;
+ }
+ }
+ }
+
+ [CLSCompliant(false)]
+ public class AnnotatedRouteCollection : RouteCollection
+ {
+ public List<string> _areasLoaded = new List<string>();
+ }
+
+ public abstract class AreaRegistrationTest_AbstractAreaRegistration : AreaRegistration
+ {
+ public override string AreaName
+ {
+ get { return "the_area"; }
+ }
+
+ public override void RegisterArea(AreaRegistrationContext context)
+ {
+ ((AnnotatedRouteCollection)context.Routes)._areasLoaded.Add("AreaRegistrationTest_AbstractAreaRegistration");
+ }
+ }
+
+ public class AreaRegistrationTest_AreaRegistration : AreaRegistrationTest_AbstractAreaRegistration
+ {
+ public override void RegisterArea(AreaRegistrationContext context)
+ {
+ ((AnnotatedRouteCollection)context.Routes)._areasLoaded.Add("AreaRegistrationTest_AreaRegistration");
+ }
+ }
+
+ public class AreaRegistrationTest_NoConstructorAreaRegistration : AreaRegistrationTest_AreaRegistration
+ {
+ private AreaRegistrationTest_NoConstructorAreaRegistration()
+ {
+ }
+
+ public override void RegisterArea(AreaRegistrationContext context)
+ {
+ ((AnnotatedRouteCollection)context.Routes)._areasLoaded.Add("AreaRegistrationTest_NoConstructorAreaRegistration");
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/AssociatedMetadataProviderTest.cs b/test/System.Web.Mvc.Test/Test/AssociatedMetadataProviderTest.cs
new file mode 100644
index 00000000..6e0d0f58
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/AssociatedMetadataProviderTest.cs
@@ -0,0 +1,346 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class AssociatedMetadataProviderTest
+ {
+ // FilterAttributes
+
+ [Fact]
+ public void ReadOnlyAttributeIsFilteredOffWhenContainerTypeIsViewPage()
+ {
+ // Arrange
+ TestableAssociatedMetadataProvider provider = new TestableAssociatedMetadataProvider();
+
+ // Act
+ provider.GetMetadataForProperty(() => null, typeof(ViewPage<PropertyModel>), "Model");
+
+ // Assert
+ CreateMetadataParams parms = provider.CreateMetadataLog.Single();
+ Assert.False(parms.Attributes.Any(a => a is ReadOnlyAttribute));
+ }
+
+ [Fact]
+ public void ReadOnlyAttributeIsFilteredOffWhenContainerTypeIsViewUserControl()
+ {
+ // Arrange
+ TestableAssociatedMetadataProvider provider = new TestableAssociatedMetadataProvider();
+
+ // Act
+ provider.GetMetadataForProperty(() => null, typeof(ViewUserControl<PropertyModel>), "Model");
+
+ // Assert
+ CreateMetadataParams parms = provider.CreateMetadataLog.Single();
+ Assert.False(parms.Attributes.Any(a => a is ReadOnlyAttribute));
+ }
+
+ [Fact]
+ public void ReadOnlyAttributeIsPreservedForReadOnlyModelProperties()
+ {
+ // Arrange
+ TestableAssociatedMetadataProvider provider = new TestableAssociatedMetadataProvider();
+
+ // Act
+ provider.GetMetadataForProperty(() => null, typeof(ModelWithReadOnlyProperty), "ReadOnlyProperty");
+
+ // Assert
+ CreateMetadataParams parms = provider.CreateMetadataLog.Single();
+ Assert.True(parms.Attributes.Any(a => a is ReadOnlyAttribute));
+ }
+
+ // GetMetadataForProperties
+
+ [Fact]
+ public void GetMetadataForPropertiesNullContainerTypeThrows()
+ {
+ // Arrange
+ TestableAssociatedMetadataProvider provider = new TestableAssociatedMetadataProvider();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => provider.GetMetadataForProperties(new Object(), null),
+ "containerType");
+ }
+
+ [Fact]
+ public void GetMetadataForPropertiesCreatesMetadataForAllPropertiesOnModelWithPropertyValues()
+ {
+ // Arrange
+ PropertyModel model = new PropertyModel { LocalAttributes = 42, MetadataAttributes = "hello", MixedAttributes = 21.12 };
+ TestableAssociatedMetadataProvider provider = new TestableAssociatedMetadataProvider();
+
+ // Act
+ provider.GetMetadataForProperties(model, typeof(PropertyModel)).ToList(); // Call ToList() to force the lazy evaluation to evaluate
+
+ // Assert
+ CreateMetadataParams local =
+ provider.CreateMetadataLog.Single(m => m.ContainerType == typeof(PropertyModel) &&
+ m.PropertyName == "LocalAttributes");
+ Assert.Equal(typeof(int), local.ModelType);
+ Assert.Equal(42, local.Model);
+ Assert.True(local.Attributes.Any(a => a is RequiredAttribute));
+
+ CreateMetadataParams metadata =
+ provider.CreateMetadataLog.Single(m => m.ContainerType == typeof(PropertyModel) &&
+ m.PropertyName == "MetadataAttributes");
+ Assert.Equal(typeof(string), metadata.ModelType);
+ Assert.Equal("hello", metadata.Model);
+ Assert.True(metadata.Attributes.Any(a => a is RangeAttribute));
+
+ CreateMetadataParams mixed =
+ provider.CreateMetadataLog.Single(m => m.ContainerType == typeof(PropertyModel) &&
+ m.PropertyName == "MixedAttributes");
+ Assert.Equal(typeof(double), mixed.ModelType);
+ Assert.Equal(21.12, mixed.Model);
+ Assert.True(mixed.Attributes.Any(a => a is RequiredAttribute));
+ Assert.True(mixed.Attributes.Any(a => a is RangeAttribute));
+ }
+
+ [Fact]
+ public void GetMetadataForPropertyWithNullContainerReturnsMetadataWithNullValuesForProperties()
+ {
+ // Arrange
+ TestableAssociatedMetadataProvider provider = new TestableAssociatedMetadataProvider();
+
+ // Act
+ provider.GetMetadataForProperties(null, typeof(PropertyModel)).ToList(); // Call ToList() to force the lazy evaluation to evaluate
+
+ // Assert
+ Assert.True(provider.CreateMetadataLog.Any());
+ foreach (var parms in provider.CreateMetadataLog)
+ {
+ Assert.Null(parms.Model);
+ }
+ }
+
+ // GetMetadataForProperty
+
+ [Fact]
+ public void GetMetadataForPropertyNullContainerTypeThrows()
+ {
+ // Arrange
+ TestableAssociatedMetadataProvider provider = new TestableAssociatedMetadataProvider();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => provider.GetMetadataForProperty(null /* model */, null /* containerType */, "propertyName"),
+ "containerType");
+ }
+
+ [Fact]
+ public void GetMetadataForPropertyNullOrEmptyPropertyNameThrows()
+ {
+ // Arrange
+ TestableAssociatedMetadataProvider provider = new TestableAssociatedMetadataProvider();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ () => provider.GetMetadataForProperty(null /* model */, typeof(object), null /* propertyName */),
+ "propertyName");
+ Assert.ThrowsArgumentNullOrEmpty(
+ () => provider.GetMetadataForProperty(null, typeof(object), String.Empty),
+ "propertyName");
+ }
+
+ [Fact]
+ public void GetMetadataForPropertyInvalidPropertyNameThrows()
+ {
+ // Arrange
+ TestableAssociatedMetadataProvider provider = new TestableAssociatedMetadataProvider();
+
+ // Act & Assert
+ Assert.Throws<ArgumentException>(
+ () => provider.GetMetadataForProperty(null, typeof(object), "BadPropertyName"),
+ "The property System.Object.BadPropertyName could not be found.");
+ }
+
+ [Fact]
+ public void GetMetadataForPropertyWithLocalAttributes()
+ {
+ // Arrange
+ TestableAssociatedMetadataProvider provider = new TestableAssociatedMetadataProvider();
+ ModelMetadata metadata = new ModelMetadata(provider, typeof(PropertyModel), null, typeof(int), "LocalAttributes");
+ provider.CreateMetadataReturnValue = metadata;
+
+ // Act
+ ModelMetadata result = provider.GetMetadataForProperty(null, typeof(PropertyModel), "LocalAttributes");
+
+ // Assert
+ Assert.Same(metadata, result);
+ Assert.True(provider.CreateMetadataLog.Single().Attributes.Any(a => a is RequiredAttribute));
+ }
+
+ [Fact]
+ public void GetMetadataForPropertyWithMetadataAttributes()
+ {
+ // Arrange
+ TestableAssociatedMetadataProvider provider = new TestableAssociatedMetadataProvider();
+ ModelMetadata metadata = new ModelMetadata(provider, typeof(PropertyModel), null, typeof(string), "MetadataAttributes");
+ provider.CreateMetadataReturnValue = metadata;
+
+ // Act
+ ModelMetadata result = provider.GetMetadataForProperty(null, typeof(PropertyModel), "MetadataAttributes");
+
+ // Assert
+ Assert.Same(metadata, result);
+ CreateMetadataParams parms = provider.CreateMetadataLog.Single(p => p.PropertyName == "MetadataAttributes");
+ Assert.True(parms.Attributes.Any(a => a is RangeAttribute));
+ }
+
+ [Fact]
+ public void GetMetadataForPropertyWithMixedAttributes()
+ {
+ // Arrange
+ TestableAssociatedMetadataProvider provider = new TestableAssociatedMetadataProvider();
+ ModelMetadata metadata = new ModelMetadata(provider, typeof(PropertyModel), null, typeof(double), "MixedAttributes");
+ provider.CreateMetadataReturnValue = metadata;
+
+ // Act
+ ModelMetadata result = provider.GetMetadataForProperty(null, typeof(PropertyModel), "MixedAttributes");
+
+ // Assert
+ Assert.Same(metadata, result);
+ CreateMetadataParams parms = provider.CreateMetadataLog.Single(p => p.PropertyName == "MixedAttributes");
+ Assert.True(parms.Attributes.Any(a => a is RequiredAttribute));
+ Assert.True(parms.Attributes.Any(a => a is RangeAttribute));
+ }
+
+ // GetMetadataForType
+
+ [Fact]
+ public void GetMetadataForTypeNullModelTypeThrows()
+ {
+ // Arrange
+ TestableAssociatedMetadataProvider provider = new TestableAssociatedMetadataProvider();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => provider.GetMetadataForType(() => new Object(), null),
+ "modelType");
+ }
+
+ [Fact]
+ public void GetMetadataForTypeIncludesAttributesOnType()
+ {
+ TestableAssociatedMetadataProvider provider = new TestableAssociatedMetadataProvider();
+ ModelMetadata metadata = new ModelMetadata(provider, null, null, typeof(TypeModel), null);
+ provider.CreateMetadataReturnValue = metadata;
+
+ // Act
+ ModelMetadata result = provider.GetMetadataForType(null, typeof(TypeModel));
+
+ // Assert
+ Assert.Same(metadata, result);
+ CreateMetadataParams parms = provider.CreateMetadataLog.Single(p => p.ModelType == typeof(TypeModel));
+ Assert.True(parms.Attributes.Any(a => a is ReadOnlyAttribute));
+ }
+
+ [AdditionalMetadata("ClassName", "ClassValue")]
+ class ClassWithAdditionalMetadata
+ {
+ [AdditionalMetadata("PropertyName", "PropertyValue")]
+ public int MyProperty { get; set; }
+ }
+
+ [Fact]
+ public void MetadataAwareAttributeCanModifyTypeMetadata()
+ {
+ // Arrange
+ TestableAssociatedMetadataProvider provider = new TestableAssociatedMetadataProvider();
+ provider.CreateMetadataReturnValue = new ModelMetadata(provider, null, null, typeof(ClassWithAdditionalMetadata), null);
+
+ // Act
+ ModelMetadata metadata = provider.GetMetadataForType(null, typeof(ClassWithAdditionalMetadata));
+
+ // Assert
+ var kvp = metadata.AdditionalValues.Single();
+ Assert.Equal("ClassName", kvp.Key);
+ Assert.Equal("ClassValue", kvp.Value);
+ }
+
+ [Fact]
+ public void MetadataAwareAttributeCanModifyPropertyMetadata()
+ {
+ // Arrange
+ TestableAssociatedMetadataProvider provider = new TestableAssociatedMetadataProvider();
+ provider.CreateMetadataReturnValue = new ModelMetadata(provider, typeof(ClassWithAdditionalMetadata), null, typeof(int), "MyProperty");
+
+ // Act
+ ModelMetadata metadata = provider.GetMetadataForProperty(null, typeof(ClassWithAdditionalMetadata), "MyProperty");
+
+ // Assert
+ var kvp = metadata.AdditionalValues.Single();
+ Assert.Equal("PropertyName", kvp.Key);
+ Assert.Equal("PropertyValue", kvp.Value);
+ }
+
+ // Helpers
+
+ [MetadataType(typeof(Metadata))]
+ private class PropertyModel
+ {
+ [Required]
+ public int LocalAttributes { get; set; }
+
+ public string MetadataAttributes { get; set; }
+
+ [Required]
+ public double MixedAttributes { get; set; }
+
+ private class Metadata
+ {
+ [Range(10, 100)]
+ public object MetadataAttributes { get; set; }
+
+ [Range(10, 100)]
+ public object MixedAttributes { get; set; }
+ }
+ }
+
+ private class ModelWithReadOnlyProperty
+ {
+ public int ReadOnlyProperty { get; private set; }
+ }
+
+ [ReadOnly(true)]
+ private class TypeModel
+ {
+ }
+
+ class TestableAssociatedMetadataProvider : AssociatedMetadataProvider
+ {
+ public List<CreateMetadataParams> CreateMetadataLog = new List<CreateMetadataParams>();
+ public ModelMetadata CreateMetadataReturnValue = null;
+
+ protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType,
+ Func<object> modelAccessor, Type modelType,
+ string propertyName)
+ {
+ CreateMetadataLog.Add(new CreateMetadataParams
+ {
+ Attributes = attributes,
+ ContainerType = containerType,
+ Model = modelAccessor == null ? null : modelAccessor(),
+ ModelType = modelType,
+ PropertyName = propertyName
+ });
+
+ return CreateMetadataReturnValue;
+ }
+ }
+
+ class CreateMetadataParams
+ {
+ public IEnumerable<Attribute> Attributes { get; set; }
+ public Type ContainerType { get; set; }
+ public object Model { get; set; }
+ public Type ModelType { get; set; }
+ public string PropertyName { get; set; }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/AssociatedValidatorProviderTest.cs b/test/System.Web.Mvc.Test/Test/AssociatedValidatorProviderTest.cs
new file mode 100644
index 00000000..f687f543
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/AssociatedValidatorProviderTest.cs
@@ -0,0 +1,124 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class AssociatedValidatorProviderTest
+ {
+ [Fact]
+ public void GetValidatorsGuardClauses()
+ {
+ // Arrange
+ ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(object));
+ Mock<AssociatedValidatorProvider> provider = new Mock<AssociatedValidatorProvider> { CallBase = true };
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => provider.Object.GetValidators(null, new ControllerContext()),
+ "metadata");
+ Assert.ThrowsArgumentNull(
+ () => provider.Object.GetValidators(metadata, null),
+ "context");
+ }
+
+ [Fact]
+ public void GetValidatorsForPropertyWithLocalAttributes()
+ {
+ // Arrange
+ IEnumerable<Attribute> callbackAttributes = null;
+ ControllerContext context = new ControllerContext();
+ ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(null, typeof(PropertyModel), "LocalAttributes");
+ Mock<TestableAssociatedValidatorProvider> provider = new Mock<TestableAssociatedValidatorProvider> { CallBase = true };
+ provider.Setup(p => p.AbstractGetValidators(metadata, context, It.IsAny<IEnumerable<Attribute>>()))
+ .Callback<ModelMetadata, ControllerContext, IEnumerable<Attribute>>((m, c, attributes) => callbackAttributes = attributes)
+ .Returns(() => null)
+ .Verifiable();
+
+ // Act
+ provider.Object.GetValidators(metadata, context);
+
+ // Assert
+ provider.Verify();
+ Assert.True(callbackAttributes.Any(a => a is RequiredAttribute));
+ }
+
+ [Fact]
+ public void GetValidatorsForPropertyWithMetadataAttributes()
+ {
+ // Arrange
+ IEnumerable<Attribute> callbackAttributes = null;
+ ControllerContext context = new ControllerContext();
+ ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(null, typeof(PropertyModel), "MetadataAttributes");
+ Mock<TestableAssociatedValidatorProvider> provider = new Mock<TestableAssociatedValidatorProvider> { CallBase = true };
+ provider.Setup(p => p.AbstractGetValidators(metadata, context, It.IsAny<IEnumerable<Attribute>>()))
+ .Callback<ModelMetadata, ControllerContext, IEnumerable<Attribute>>((m, c, attributes) => callbackAttributes = attributes)
+ .Returns(() => null)
+ .Verifiable();
+
+ // Act
+ provider.Object.GetValidators(metadata, context);
+
+ // Assert
+ provider.Verify();
+ Assert.True(callbackAttributes.Any(a => a is RangeAttribute));
+ }
+
+ [Fact]
+ public void GetValidatorsForPropertyWithMixedAttributes()
+ {
+ // Arrange
+ IEnumerable<Attribute> callbackAttributes = null;
+ ControllerContext context = new ControllerContext();
+ ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(null, typeof(PropertyModel), "MixedAttributes");
+ Mock<TestableAssociatedValidatorProvider> provider = new Mock<TestableAssociatedValidatorProvider> { CallBase = true };
+ provider.Setup(p => p.AbstractGetValidators(metadata, context, It.IsAny<IEnumerable<Attribute>>()))
+ .Callback<ModelMetadata, ControllerContext, IEnumerable<Attribute>>((m, c, attributes) => callbackAttributes = attributes)
+ .Returns(() => null)
+ .Verifiable();
+
+ // Act
+ provider.Object.GetValidators(metadata, context);
+
+ // Assert
+ provider.Verify();
+ Assert.True(callbackAttributes.Any(a => a is RangeAttribute));
+ Assert.True(callbackAttributes.Any(a => a is RequiredAttribute));
+ }
+
+ [MetadataType(typeof(Metadata))]
+ private class PropertyModel
+ {
+ [Required]
+ public int LocalAttributes { get; set; }
+
+ public string MetadataAttributes { get; set; }
+
+ [Required]
+ public double MixedAttributes { get; set; }
+
+ private class Metadata
+ {
+ [Range(10, 100)]
+ public object MetadataAttributes { get; set; }
+
+ [Range(10, 100)]
+ public object MixedAttributes { get; set; }
+ }
+ }
+
+ public abstract class TestableAssociatedValidatorProvider : AssociatedValidatorProvider
+ {
+ protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes)
+ {
+ return AbstractGetValidators(metadata, context, attributes);
+ }
+
+ // Hoist access
+ public abstract IEnumerable<ModelValidator> AbstractGetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/AsyncControllerTest.cs b/test/System.Web.Mvc.Test/Test/AsyncControllerTest.cs
new file mode 100644
index 00000000..50805c85
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/AsyncControllerTest.cs
@@ -0,0 +1,263 @@
+using System.Collections.Generic;
+using System.Web.Mvc.Async;
+using System.Web.Mvc.Async.Test;
+using System.Web.Routing;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class AsyncControllerTest
+ {
+ [Fact]
+ public void ActionInvokerProperty()
+ {
+ // Arrange
+ EmptyController controller = new EmptyController();
+
+ // Act
+ IActionInvoker invoker = controller.ActionInvoker;
+
+ // Assert
+ Assert.IsType<AsyncControllerActionInvoker>(invoker);
+ }
+
+ [Fact]
+ public void AsyncManagerProperty()
+ {
+ // Arrange
+ EmptyController controller = new EmptyController();
+
+ // Act
+ AsyncManager asyncManager = controller.AsyncManager;
+
+ // Assert
+ Assert.NotNull(asyncManager);
+ }
+
+ [Fact]
+ public void Execute_ThrowsIfCalledMoreThanOnce()
+ {
+ // Arrange
+ IAsyncController controller = new EmptyController();
+ RequestContext requestContext = GetRequestContext("SomeAction");
+
+ // Act & assert
+ controller.BeginExecute(requestContext, null, null);
+ Assert.Throws<InvalidOperationException>(
+ delegate { controller.BeginExecute(requestContext, null, null); },
+ @"A single instance of controller 'System.Web.Mvc.Test.AsyncControllerTest+EmptyController' cannot be used to handle multiple requests. If a custom controller factory is in use, make sure that it creates a new instance of the controller for each request.");
+ }
+
+ [Fact]
+ public void Execute_ThrowsIfRequestContextIsNull()
+ {
+ // Arrange
+ IAsyncController controller = new EmptyController();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { controller.BeginExecute(null, null, null); }, "requestContext");
+ }
+
+ [Fact]
+ public void ExecuteCore_Asynchronous_ActionFound()
+ {
+ // Arrange
+ MockAsyncResult innerAsyncResult = new MockAsyncResult();
+
+ Mock<IAsyncActionInvoker> mockActionInvoker = new Mock<IAsyncActionInvoker>();
+ mockActionInvoker.Setup(o => o.BeginInvokeAction(It.IsAny<ControllerContext>(), "SomeAction", It.IsAny<AsyncCallback>(), It.IsAny<object>())).Returns(innerAsyncResult);
+ mockActionInvoker.Setup(o => o.EndInvokeAction(innerAsyncResult)).Returns(true);
+
+ RequestContext requestContext = GetRequestContext("SomeAction");
+ EmptyController controller = new EmptyController()
+ {
+ ActionInvoker = mockActionInvoker.Object
+ };
+
+ // Act & assert
+ IAsyncResult outerAsyncResult = ((IAsyncController)controller).BeginExecute(requestContext, null, null);
+ Assert.False(controller.TempDataSaved);
+
+ ((IAsyncController)controller).EndExecute(outerAsyncResult);
+ Assert.True(controller.TempDataSaved);
+ Assert.False(controller.HandleUnknownActionCalled);
+ }
+
+ [Fact]
+ public void ExecuteCore_Asynchronous_ActionNotFound()
+ {
+ // Arrange
+ MockAsyncResult innerAsyncResult = new MockAsyncResult();
+
+ Mock<IAsyncActionInvoker> mockActionInvoker = new Mock<IAsyncActionInvoker>();
+ mockActionInvoker.Setup(o => o.BeginInvokeAction(It.IsAny<ControllerContext>(), "SomeAction", It.IsAny<AsyncCallback>(), It.IsAny<object>())).Returns(innerAsyncResult);
+ mockActionInvoker.Setup(o => o.EndInvokeAction(innerAsyncResult)).Returns(false);
+
+ RequestContext requestContext = GetRequestContext("SomeAction");
+ EmptyController controller = new EmptyController()
+ {
+ ActionInvoker = mockActionInvoker.Object
+ };
+
+ // Act & assert
+ IAsyncResult outerAsyncResult = ((IAsyncController)controller).BeginExecute(requestContext, null, null);
+ Assert.False(controller.TempDataSaved);
+
+ ((IAsyncController)controller).EndExecute(outerAsyncResult);
+ Assert.True(controller.TempDataSaved);
+ Assert.True(controller.HandleUnknownActionCalled);
+ }
+
+ [Fact]
+ public void ExecuteCore_Synchronous_ActionFound()
+ {
+ // Arrange
+ MockAsyncResult innerAsyncResult = new MockAsyncResult();
+
+ Mock<IActionInvoker> mockActionInvoker = new Mock<IActionInvoker>();
+ mockActionInvoker.Setup(o => o.InvokeAction(It.IsAny<ControllerContext>(), "SomeAction")).Returns(true);
+
+ RequestContext requestContext = GetRequestContext("SomeAction");
+ EmptyController controller = new EmptyController()
+ {
+ ActionInvoker = mockActionInvoker.Object
+ };
+
+ // Act & assert
+ IAsyncResult outerAsyncResult = ((IAsyncController)controller).BeginExecute(requestContext, null, null);
+ Assert.False(controller.TempDataSaved);
+
+ ((IAsyncController)controller).EndExecute(outerAsyncResult);
+ Assert.True(controller.TempDataSaved);
+ Assert.False(controller.HandleUnknownActionCalled);
+ }
+
+ [Fact]
+ public void ExecuteCore_Synchronous_ActionNotFound()
+ {
+ // Arrange
+ MockAsyncResult innerAsyncResult = new MockAsyncResult();
+
+ Mock<IActionInvoker> mockActionInvoker = new Mock<IActionInvoker>();
+ mockActionInvoker.Setup(o => o.InvokeAction(It.IsAny<ControllerContext>(), "SomeAction")).Returns(false);
+
+ RequestContext requestContext = GetRequestContext("SomeAction");
+ EmptyController controller = new EmptyController()
+ {
+ ActionInvoker = mockActionInvoker.Object
+ };
+
+ // Act & assert
+ IAsyncResult outerAsyncResult = ((IAsyncController)controller).BeginExecute(requestContext, null, null);
+ Assert.False(controller.TempDataSaved);
+
+ ((IAsyncController)controller).EndExecute(outerAsyncResult);
+ Assert.True(controller.TempDataSaved);
+ Assert.True(controller.HandleUnknownActionCalled);
+ }
+
+ [Fact]
+ public void ExecuteCore_SavesTempDataOnException()
+ {
+ // Arrange
+ Mock<IAsyncActionInvoker> mockActionInvoker = new Mock<IAsyncActionInvoker>();
+ mockActionInvoker
+ .Setup(o => o.BeginInvokeAction(It.IsAny<ControllerContext>(), "SomeAction", It.IsAny<AsyncCallback>(), It.IsAny<object>()))
+ .Throws(new Exception("Some exception text."));
+
+ RequestContext requestContext = GetRequestContext("SomeAction");
+ EmptyController controller = new EmptyController()
+ {
+ ActionInvoker = mockActionInvoker.Object
+ };
+
+ // Act & assert
+ Assert.Throws<Exception>(
+ delegate { ((IAsyncController)controller).BeginExecute(requestContext, null, null); },
+ @"Some exception text.");
+ Assert.True(controller.TempDataSaved);
+ }
+
+ [Fact]
+ public void CreateActionInvokerCallsIntoResolverInstance()
+ {
+ // Controller uses an IDependencyResolver to create an IActionInvoker.
+ var controller = new EmptyController();
+ Mock<IDependencyResolver> resolverMock = new Mock<IDependencyResolver>();
+ Mock<IAsyncActionInvoker> actionInvokerMock = new Mock<IAsyncActionInvoker>();
+ resolverMock.Setup(r => r.GetService(typeof(IAsyncActionInvoker))).Returns(actionInvokerMock.Object);
+ controller.Resolver = resolverMock.Object;
+
+ var ai = controller.CreateActionInvoker();
+
+ resolverMock.Verify(r => r.GetService(typeof(IAsyncActionInvoker)), Times.Once());
+ Assert.Same(actionInvokerMock.Object, ai);
+ }
+
+ [Fact]
+ public void CreateActionInvokerCallsIntoResolverInstanceAndCreatesANewOneIfNecessary()
+ {
+ // If IDependencyResolver is set, but empty, falls back and still creates.
+ var controller = new EmptyController();
+ Mock<IDependencyResolver> resolverMock = new Mock<IDependencyResolver>();
+ resolverMock.Setup(r => r.GetService(typeof(IAsyncActionInvoker))).Returns(null);
+ resolverMock.Setup(r => r.GetService(typeof(IActionInvoker))).Returns(null);
+ controller.Resolver = resolverMock.Object;
+
+ var ai = controller.CreateActionInvoker();
+
+ resolverMock.Verify(r => r.GetService(typeof(IAsyncActionInvoker)), Times.Once());
+ resolverMock.Verify(r => r.GetService(typeof(IActionInvoker)), Times.Once());
+ Assert.NotNull(ai);
+ }
+
+
+ private static RequestContext GetRequestContext(string actionName)
+ {
+ Mock<HttpContextBase> mockHttpContext = new Mock<HttpContextBase>();
+ RouteData routeData = new RouteData();
+ routeData.Values["action"] = actionName;
+
+ return new RequestContext(mockHttpContext.Object, routeData);
+ }
+
+ private class EmptyController : AsyncController
+ {
+ public bool TempDataSaved;
+ public bool HandleUnknownActionCalled;
+
+ protected override ITempDataProvider CreateTempDataProvider()
+ {
+ return new DummyTempDataProvider();
+ }
+
+ protected override void HandleUnknownAction(string actionName)
+ {
+ HandleUnknownActionCalled = true;
+ }
+
+ // Test can expose protected method as public.
+ public new IActionInvoker CreateActionInvoker()
+ {
+ return base.CreateActionInvoker();
+ }
+
+
+ private class DummyTempDataProvider : ITempDataProvider
+ {
+ public IDictionary<string, object> LoadTempData(ControllerContext controllerContext)
+ {
+ return new TempDataDictionary();
+ }
+
+ public void SaveTempData(ControllerContext controllerContext, IDictionary<string, object> values)
+ {
+ ((EmptyController)controllerContext.Controller).TempDataSaved = true;
+ }
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/AsyncTimeoutAttributeTest.cs b/test/System.Web.Mvc.Test/Test/AsyncTimeoutAttributeTest.cs
new file mode 100644
index 00000000..581bbf1e
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/AsyncTimeoutAttributeTest.cs
@@ -0,0 +1,87 @@
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class AsyncTimeoutAttributeTest
+ {
+ [Fact]
+ public void ConstructorThrowsIfDurationIsOutOfRange()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentOutOfRange(() => new AsyncTimeoutAttribute(-1000), "duration",
+ @"The timeout value must be non-negative or Timeout.Infinite.");
+ }
+
+ [Fact]
+ public void DurationProperty()
+ {
+ // Act
+ AsyncTimeoutAttribute attr = new AsyncTimeoutAttribute(45);
+
+ // Assert
+ Assert.Equal(45, attr.Duration);
+ }
+
+ [Fact]
+ public void OnActionExecutingSetsTimeoutPropertyOnController()
+ {
+ // Arrange
+ AsyncTimeoutAttribute attr = new AsyncTimeoutAttribute(45);
+
+ MyAsyncController controller = new MyAsyncController();
+ controller.AsyncManager.Timeout = 0;
+
+ ActionExecutingContext filterContext = new ActionExecutingContext()
+ {
+ Controller = controller
+ };
+
+ // Act
+ attr.OnActionExecuting(filterContext);
+
+ // Assert
+ Assert.Equal(45, controller.AsyncManager.Timeout);
+ }
+
+ [Fact]
+ public void OnActionExecutingThrowsIfControllerIsNotAsyncManagerContainer()
+ {
+ // Arrange
+ AsyncTimeoutAttribute attr = new AsyncTimeoutAttribute(45);
+
+ ActionExecutingContext filterContext = new ActionExecutingContext()
+ {
+ Controller = new MyController()
+ };
+
+ // Act & assert
+ Assert.Throws<InvalidOperationException>(
+ delegate { attr.OnActionExecuting(filterContext); },
+ @"The controller of type 'System.Web.Mvc.Test.AsyncTimeoutAttributeTest+MyController' must subclass AsyncController or implement the IAsyncManagerContainer interface.");
+ }
+
+ [Fact]
+ public void OnActionExecutingThrowsIfFilterContextIsNull()
+ {
+ // Arrange
+ AsyncTimeoutAttribute attr = new AsyncTimeoutAttribute(45);
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { attr.OnActionExecuting(null); }, "filterContext");
+ }
+
+ private class MyController : ControllerBase
+ {
+ protected override void ExecuteCore()
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ private class MyAsyncController : AsyncController
+ {
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/AuthorizationContextTest.cs b/test/System.Web.Mvc.Test/Test/AuthorizationContextTest.cs
new file mode 100644
index 00000000..3f51a25d
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/AuthorizationContextTest.cs
@@ -0,0 +1,35 @@
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class AuthorizationContextTest
+ {
+ [Fact]
+ public void ConstructorThrowsIfActionDescriptorIsNull()
+ {
+ // Arrange
+ ControllerContext controllerContext = new Mock<ControllerContext>().Object;
+ ActionDescriptor actionDescriptor = null;
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new AuthorizationContext(controllerContext, actionDescriptor); }, "actionDescriptor");
+ }
+
+ [Fact]
+ public void PropertiesAreSetByConstructor()
+ {
+ // Arrange
+ ControllerContext controllerContext = new Mock<ControllerContext>().Object;
+ ActionDescriptor actionDescriptor = new Mock<ActionDescriptor>().Object;
+
+ // Act
+ AuthorizationContext authorizationContext = new AuthorizationContext(controllerContext, actionDescriptor);
+
+ // Assert
+ Assert.Equal(actionDescriptor, authorizationContext.ActionDescriptor);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/AuthorizeAttributeTest.cs b/test/System.Web.Mvc.Test/Test/AuthorizeAttributeTest.cs
new file mode 100644
index 00000000..1605b773
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/AuthorizeAttributeTest.cs
@@ -0,0 +1,362 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Reflection;
+using System.Security.Principal;
+using System.Web.TestUtil;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class AuthorizeAttributeTest
+ {
+ [Fact]
+ public void AuthorizeAttributeReturnsUniqueTypeIDs()
+ {
+ // Arrange
+ AuthorizeAttribute attr1 = new AuthorizeAttribute();
+ AuthorizeAttribute attr2 = new AuthorizeAttribute();
+
+ // Assert
+ Assert.NotEqual(attr1.TypeId, attr2.TypeId);
+ }
+
+ [Authorize(Roles = "foo")]
+ [Authorize(Roles = "bar")]
+ private class ClassWithMultipleAuthorizeAttributes
+ {
+ }
+
+ [Fact]
+ public void CanRetrieveMultipleAuthorizeAttributesFromOneClass()
+ {
+ // Arrange
+ ClassWithMultipleAuthorizeAttributes @class = new ClassWithMultipleAuthorizeAttributes();
+
+ // Act
+ IEnumerable<AuthorizeAttribute> attributes = TypeDescriptor.GetAttributes(@class).OfType<AuthorizeAttribute>();
+
+ // Assert
+ Assert.Equal(2, attributes.Count());
+ Assert.True(attributes.Any(a => a.Roles == "foo"));
+ Assert.True(attributes.Any(a => a.Roles == "bar"));
+ }
+
+ [Fact]
+ public void AuthorizeCoreReturnsFalseIfNameDoesNotMatch()
+ {
+ // Arrange
+ AuthorizeAttributeHelper helper = new AuthorizeAttributeHelper() { Users = "SomeName" };
+
+ Mock<HttpContextBase> mockHttpContext = new Mock<HttpContextBase>();
+ mockHttpContext.Setup(c => c.User.Identity.IsAuthenticated).Returns(true);
+ mockHttpContext.Setup(c => c.User.Identity.Name).Returns("SomeOtherName");
+
+ // Act
+ bool retVal = helper.PublicAuthorizeCore(mockHttpContext.Object);
+
+ // Assert
+ Assert.False(retVal);
+ }
+
+ [Fact]
+ public void AuthorizeCoreReturnsFalseIfRoleDoesNotMatch()
+ {
+ // Arrange
+ AuthorizeAttributeHelper helper = new AuthorizeAttributeHelper() { Roles = "SomeRole" };
+
+ Mock<HttpContextBase> mockHttpContext = new Mock<HttpContextBase>();
+ mockHttpContext.Setup(c => c.User.Identity.IsAuthenticated).Returns(true);
+ mockHttpContext.Setup(c => c.User.IsInRole("SomeRole")).Returns(false).Verifiable();
+
+ // Act
+ bool retVal = helper.PublicAuthorizeCore(mockHttpContext.Object);
+
+ // Assert
+ Assert.False(retVal);
+ mockHttpContext.Verify();
+ }
+
+ [Fact]
+ public void AuthorizeCoreReturnsFalseIfUserIsUnauthenticated()
+ {
+ // Arrange
+ AuthorizeAttributeHelper helper = new AuthorizeAttributeHelper();
+
+ Mock<HttpContextBase> mockHttpContext = new Mock<HttpContextBase>();
+ mockHttpContext.Setup(c => c.User.Identity.IsAuthenticated).Returns(false);
+
+ // Act
+ bool retVal = helper.PublicAuthorizeCore(mockHttpContext.Object);
+
+ // Assert
+ Assert.False(retVal);
+ }
+
+ [Fact]
+ public void AuthorizeCoreReturnsTrueIfUserIsAuthenticatedAndNamesOrRolesSpecified()
+ {
+ // Arrange
+ AuthorizeAttributeHelper helper = new AuthorizeAttributeHelper() { Users = "SomeUser, SomeOtherUser", Roles = "SomeRole, SomeOtherRole" };
+
+ Mock<HttpContextBase> mockHttpContext = new Mock<HttpContextBase>();
+ mockHttpContext.Setup(c => c.User.Identity.IsAuthenticated).Returns(true);
+ mockHttpContext.Setup(c => c.User.Identity.Name).Returns("SomeUser");
+ mockHttpContext.Setup(c => c.User.IsInRole("SomeRole")).Returns(false).Verifiable();
+ mockHttpContext.Setup(c => c.User.IsInRole("SomeOtherRole")).Returns(true).Verifiable();
+
+ // Act
+ bool retVal = helper.PublicAuthorizeCore(mockHttpContext.Object);
+
+ // Assert
+ Assert.True(retVal);
+ mockHttpContext.Verify();
+ }
+
+ [Fact]
+ public void AuthorizeCoreReturnsTrueIfUserIsAuthenticatedAndNoNamesOrRolesSpecified()
+ {
+ // Arrange
+ AuthorizeAttributeHelper helper = new AuthorizeAttributeHelper();
+
+ Mock<HttpContextBase> mockHttpContext = new Mock<HttpContextBase>();
+ mockHttpContext.Setup(c => c.User.Identity.IsAuthenticated).Returns(true);
+
+ // Act
+ bool retVal = helper.PublicAuthorizeCore(mockHttpContext.Object);
+
+ // Assert
+ Assert.True(retVal);
+ }
+
+ [Fact]
+ public void AuthorizeCoreThrowsIfHttpContextIsNull()
+ {
+ // Arrange
+ AuthorizeAttributeHelper helper = new AuthorizeAttributeHelper();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { helper.PublicAuthorizeCore((HttpContextBase)null); }, "httpContext");
+ }
+
+ [Fact]
+ public void OnAuthorizationCallsHandleUnauthorizedRequestIfUserUnauthorized()
+ {
+ // Arrange
+ CustomFailAuthorizeAttribute attr = new CustomFailAuthorizeAttribute();
+
+ Mock<AuthorizationContext> mockAuthContext = new Mock<AuthorizationContext>();
+ mockAuthContext.Setup(c => c.HttpContext.User.Identity.IsAuthenticated).Returns(false);
+ mockAuthContext.Setup(c => c.HttpContext.Items).Returns(new Hashtable());
+ mockAuthContext.Setup(c => c.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(AllowAnonymousAttribute), true)).Returns(false);
+ AuthorizationContext authContext = mockAuthContext.Object;
+
+ // Act
+ attr.OnAuthorization(authContext);
+
+ // Assert
+ Assert.Equal(CustomFailAuthorizeAttribute.ExpectedResult, authContext.Result);
+ }
+
+ [Fact]
+ public void OnAuthorizationFailedSetsHttpUnauthorizedResultIfUserUnauthorized()
+ {
+ // Arrange
+ Mock<AuthorizeAttributeHelper> mockHelper = new Mock<AuthorizeAttributeHelper>() { CallBase = true };
+ mockHelper.Setup(h => h.PublicAuthorizeCore(It.IsAny<HttpContextBase>())).Returns(false);
+ AuthorizeAttributeHelper helper = mockHelper.Object;
+
+ AuthorizationContext filterContext = new Mock<AuthorizationContext>() { DefaultValue = DefaultValue.Mock }.Object;
+
+ // Act
+ helper.OnAuthorization(filterContext);
+
+ // Assert
+ Assert.IsType<HttpUnauthorizedResult>(filterContext.Result);
+ }
+
+ [Fact]
+ public void OnAuthorizationHooksCacheValidationIfUserAuthorized()
+ {
+ // Arrange
+ Mock<AuthorizeAttributeHelper> mockHelper = new Mock<AuthorizeAttributeHelper>() { CallBase = true };
+ mockHelper.Setup(h => h.PublicAuthorizeCore(It.IsAny<HttpContextBase>())).Returns(true);
+ AuthorizeAttributeHelper helper = mockHelper.Object;
+
+ MethodInfo callbackMethod = typeof(AuthorizeAttribute).GetMethod("CacheValidateHandler", BindingFlags.Instance | BindingFlags.NonPublic);
+ Mock<AuthorizationContext> mockFilterContext = new Mock<AuthorizationContext>();
+ mockFilterContext.Setup(c => c.HttpContext.Response.Cache.SetProxyMaxAge(new TimeSpan(0))).Verifiable();
+ mockFilterContext.Setup(c => c.HttpContext.Items).Returns(new Hashtable());
+ mockFilterContext
+ .Setup(c => c.HttpContext.Response.Cache.AddValidationCallback(It.IsAny<HttpCacheValidateHandler>(), null /* data */))
+ .Callback(
+ delegate(HttpCacheValidateHandler handler, object data)
+ {
+ Assert.Equal(helper, handler.Target);
+ Assert.Equal(callbackMethod, handler.Method);
+ })
+ .Verifiable();
+ mockFilterContext.Setup(c => c.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(AllowAnonymousAttribute), true)).Returns(false);
+ AuthorizationContext filterContext = mockFilterContext.Object;
+
+ // Act
+ helper.OnAuthorization(filterContext);
+
+ // Assert
+ mockFilterContext.Verify();
+ }
+
+ [Fact]
+ public void OnAuthorizationThrowsIfFilterContextIsNull()
+ {
+ // Arrange
+ AuthorizeAttribute attr = new AuthorizeAttribute();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { attr.OnAuthorization(null); }, "filterContext");
+ }
+
+ [Fact]
+ public void OnAuthorizationReturnsWithNoResultIfAllowAnonymousAttributeIsDefinedOnAction()
+ {
+ // Arrange
+ Mock<AuthorizeAttributeHelper> mockHelper = new Mock<AuthorizeAttributeHelper>() { CallBase = true };
+ AuthorizeAttributeHelper helper = mockHelper.Object;
+
+ Mock<AuthorizationContext> mockFilterContext = new Mock<AuthorizationContext>();
+ mockFilterContext.Setup(c => c.HttpContext.Items).Returns(new Hashtable());
+ mockFilterContext.Setup(c => c.ActionDescriptor.IsDefined(typeof(AllowAnonymousAttribute), true)).Returns(true);
+
+ // Act
+ helper.OnAuthorization(mockFilterContext.Object);
+
+ // Assert
+ Assert.Null(mockFilterContext.Object.Result);
+ mockHelper.Verify(h => h.PublicAuthorizeCore(It.IsAny<HttpContextBase>()), Times.Never());
+ }
+
+ [Fact]
+ public void OnAuthorizationReturnsWithNoResultIfAllowAnonymousAttributeIsDefinedOnController()
+ {
+ // Arrange
+ Mock<AuthorizeAttributeHelper> mockHelper = new Mock<AuthorizeAttributeHelper>() { CallBase = true };
+ AuthorizeAttributeHelper helper = mockHelper.Object;
+
+ Mock<AuthorizationContext> mockFilterContext = new Mock<AuthorizationContext>();
+ mockFilterContext.Setup(c => c.HttpContext.Items).Returns(new Hashtable());
+ mockFilterContext.Setup(c => c.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(AllowAnonymousAttribute), true)).Returns(true);
+
+ // Act
+ helper.OnAuthorization(mockFilterContext.Object);
+
+ // Assert
+ Assert.Null(mockFilterContext.Object.Result);
+ mockHelper.Verify(h => h.PublicAuthorizeCore(It.IsAny<HttpContextBase>()), Times.Never());
+ }
+
+ [Fact]
+ public void OnCacheAuthorizationReturnsIgnoreRequestIfUserIsUnauthorized()
+ {
+ // Arrange
+ Mock<AuthorizeAttributeHelper> mockHelper = new Mock<AuthorizeAttributeHelper>() { CallBase = true };
+ mockHelper.Setup(h => h.PublicAuthorizeCore(It.IsAny<HttpContextBase>())).Returns(false);
+ AuthorizeAttributeHelper helper = mockHelper.Object;
+
+ Mock<HttpContextBase> mockHttpContext = new Mock<HttpContextBase>();
+ mockHttpContext.Setup(c => c.User).Returns(new Mock<IPrincipal>().Object);
+
+ // Act
+ HttpValidationStatus validationStatus = helper.PublicOnCacheAuthorization(mockHttpContext.Object);
+
+ // Assert
+ Assert.Equal(HttpValidationStatus.IgnoreThisRequest, validationStatus);
+ }
+
+ [Fact]
+ public void OnCacheAuthorizationReturnsValidIfUserIsAuthorized()
+ {
+ // Arrange
+ Mock<AuthorizeAttributeHelper> mockHelper = new Mock<AuthorizeAttributeHelper>() { CallBase = true };
+ mockHelper.Setup(h => h.PublicAuthorizeCore(It.IsAny<HttpContextBase>())).Returns(true);
+ AuthorizeAttributeHelper helper = mockHelper.Object;
+
+ Mock<HttpContextBase> mockHttpContext = new Mock<HttpContextBase>();
+ mockHttpContext.Setup(c => c.User).Returns(new Mock<IPrincipal>().Object);
+
+ // Act
+ HttpValidationStatus validationStatus = helper.PublicOnCacheAuthorization(mockHttpContext.Object);
+
+ // Assert
+ Assert.Equal(HttpValidationStatus.Valid, validationStatus);
+ }
+
+ [Fact]
+ public void OnCacheAuthorizationThrowsIfHttpContextIsNull()
+ {
+ // Arrange
+ AuthorizeAttributeHelper helper = new AuthorizeAttributeHelper();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { helper.PublicOnCacheAuthorization(null); }, "httpContext");
+ }
+
+ [Fact]
+ public void RolesProperty()
+ {
+ // Arrange
+ AuthorizeAttribute attr = new AuthorizeAttribute();
+
+ // Act & assert
+ MemberHelper.TestStringProperty(attr, "Roles", String.Empty);
+ }
+
+ [Fact]
+ public void UsersProperty()
+ {
+ // Arrange
+ AuthorizeAttribute attr = new AuthorizeAttribute();
+
+ // Act & assert
+ MemberHelper.TestStringProperty(attr, "Users", String.Empty);
+ }
+
+ public class AuthorizeAttributeHelper : AuthorizeAttribute
+ {
+ public virtual bool PublicAuthorizeCore(HttpContextBase httpContext)
+ {
+ return base.AuthorizeCore(httpContext);
+ }
+
+ protected override bool AuthorizeCore(HttpContextBase httpContext)
+ {
+ return PublicAuthorizeCore(httpContext);
+ }
+
+ public virtual HttpValidationStatus PublicOnCacheAuthorization(HttpContextBase httpContext)
+ {
+ return base.OnCacheAuthorization(httpContext);
+ }
+
+ protected override HttpValidationStatus OnCacheAuthorization(HttpContextBase httpContext)
+ {
+ return PublicOnCacheAuthorization(httpContext);
+ }
+ }
+
+ public class CustomFailAuthorizeAttribute : AuthorizeAttribute
+ {
+ public static readonly ActionResult ExpectedResult = new ContentResult();
+
+ protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
+ {
+ filterContext.Result = ExpectedResult;
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/BindAttributeTest.cs b/test/System.Web.Mvc.Test/Test/BindAttributeTest.cs
new file mode 100644
index 00000000..625e2513
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/BindAttributeTest.cs
@@ -0,0 +1,96 @@
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class BindAttributeTest
+ {
+ [Fact]
+ public void PrefixProperty()
+ {
+ // Arrange
+ BindAttribute attr = new BindAttribute { Prefix = "somePrefix" };
+
+ // Act & assert
+ Assert.Equal("somePrefix", attr.Prefix);
+ }
+
+ [Fact]
+ public void PrefixPropertyDefaultsToNull()
+ {
+ // Arrange
+ BindAttribute attr = new BindAttribute();
+
+ // Act & assert
+ Assert.Null(attr.Prefix);
+ }
+
+ [Fact]
+ public void IncludePropertyDefaultsToEmptyString()
+ {
+ // Arrange
+ BindAttribute attr = new BindAttribute { Include = null };
+
+ // Act & assert
+ Assert.Equal(String.Empty, attr.Include);
+ }
+
+ [Fact]
+ public void ExcludePropertyDefaultsToEmptyString()
+ {
+ // Arrange
+ BindAttribute attr = new BindAttribute { Exclude = null };
+
+ // Act & assert
+ Assert.Equal(String.Empty, attr.Exclude);
+ }
+
+ [Fact]
+ public void IsPropertyAllowedReturnsFalseForBlacklistedPropertiesIfBindPropertiesIsExclude()
+ {
+ // Setup
+ BindAttribute attr = new BindAttribute { Exclude = "FOO,BAZ" };
+
+ // Act & assert
+ Assert.False(attr.IsPropertyAllowed("foo"));
+ Assert.True(attr.IsPropertyAllowed("bar"));
+ Assert.False(attr.IsPropertyAllowed("baz"));
+ }
+
+ [Fact]
+ public void IsPropertyAllowedReturnsTrueAlwaysIfBindPropertiesIsAll()
+ {
+ // Setup
+ BindAttribute attr = new BindAttribute();
+
+ // Act & assert
+ Assert.True(attr.IsPropertyAllowed("foo"));
+ Assert.True(attr.IsPropertyAllowed("bar"));
+ Assert.True(attr.IsPropertyAllowed("baz"));
+ }
+
+ [Fact]
+ public void IsPropertyAllowedReturnsTrueForWhitelistedPropertiesIfBindPropertiesIsInclude()
+ {
+ // Setup
+ BindAttribute attr = new BindAttribute { Include = "FOO,BAR" };
+
+ // Act & assert
+ Assert.True(attr.IsPropertyAllowed("foo"));
+ Assert.True(attr.IsPropertyAllowed("bar"));
+ Assert.False(attr.IsPropertyAllowed("baz"));
+ }
+
+ [Fact]
+ public void IsPropertyAllowedReturnsFalseForBlacklistOverridingWhitelistedProperties()
+ {
+ // Setup
+ BindAttribute attr = new BindAttribute { Include = "FOO,BAR", Exclude = "bar,QUx" };
+
+ // Act & assert
+ Assert.True(attr.IsPropertyAllowed("foo"));
+ Assert.False(attr.IsPropertyAllowed("bar"));
+ Assert.False(attr.IsPropertyAllowed("baz"));
+ Assert.False(attr.IsPropertyAllowed("qux"));
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/BuildManagerCompiledViewTest.cs b/test/System.Web.Mvc.Test/Test/BuildManagerCompiledViewTest.cs
new file mode 100644
index 00000000..8e0278fb
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/BuildManagerCompiledViewTest.cs
@@ -0,0 +1,145 @@
+using System.IO;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class CompiledTypeViewTest
+ {
+ [Fact]
+ public void GuardClauses()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ () => new TestableBuildManagerCompiledView(new ControllerContext(), String.Empty),
+ "viewPath"
+ );
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ () => new TestableBuildManagerCompiledView(new ControllerContext(), null),
+ "viewPath"
+ );
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => new TestableBuildManagerCompiledView(null, "view path"),
+ "controllerContext"
+ );
+ }
+
+ [Fact]
+ public void RenderWithNullContextThrows()
+ {
+ // Arrange
+ TestableBuildManagerCompiledView view = new TestableBuildManagerCompiledView(new ControllerContext(), "~/view");
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => view.Render(null, new Mock<TextWriter>().Object),
+ "viewContext"
+ );
+ }
+
+ [Fact]
+ public void RenderWithNullViewInstanceThrows()
+ {
+ // Arrange
+ ViewContext context = new Mock<ViewContext>().Object;
+ MockBuildManager buildManager = new MockBuildManager("view path", compiledType: null);
+ TestableBuildManagerCompiledView view = new TestableBuildManagerCompiledView(new ControllerContext(), "view path");
+ view.BuildManager = buildManager;
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ () => view.Render(context, new Mock<TextWriter>().Object),
+ "The view found at 'view path' was not created."
+ );
+ }
+
+ [Fact]
+ public void ViewPathProperty()
+ {
+ // Act
+ BuildManagerCompiledView view = new TestableBuildManagerCompiledView(new ControllerContext(), "view path");
+
+ // Assert
+ Assert.Equal("view path", view.ViewPath);
+ }
+
+ [Fact]
+ public void ViewCreationConsultsSetActivator()
+ {
+ // Arrange
+ object viewInstance = new object();
+ Mock<IViewPageActivator> activator = new Mock<IViewPageActivator>(MockBehavior.Strict);
+ ControllerContext controllerContext = new ControllerContext();
+ activator.Setup(a => a.Create(controllerContext, typeof(object))).Returns(viewInstance).Verifiable();
+ MockBuildManager buildManager = new MockBuildManager("view path", typeof(object));
+ BuildManagerCompiledView view = new TestableBuildManagerCompiledView(controllerContext, "view path", activator.Object) { BuildManager = buildManager };
+
+ // Act
+ view.Render(new Mock<ViewContext>().Object, new Mock<TextWriter>().Object);
+
+ // Assert
+ activator.Verify();
+ }
+
+ [Fact]
+ public void ViewCreationDelegatesToDependencyResolverWhenActivatorIsNull()
+ {
+ // Arrange
+ var viewInstance = new object();
+ var controllerContext = new ControllerContext();
+ var buildManager = new MockBuildManager("view path", typeof(object));
+ var dependencyResolver = new Mock<IDependencyResolver>(MockBehavior.Strict);
+ dependencyResolver.Setup(dr => dr.GetService(typeof(object))).Returns(viewInstance).Verifiable();
+ var view = new TestableBuildManagerCompiledView(controllerContext, "view path", dependencyResolver: dependencyResolver.Object) { BuildManager = buildManager };
+
+ // Act
+ view.Render(new Mock<ViewContext>().Object, new Mock<TextWriter>().Object);
+
+ // Assert
+ dependencyResolver.Verify();
+ }
+
+ [Fact]
+ public void ViewCreationDelegatesToActivatorCreateInstanceWhenDependencyResolverReturnsNull()
+ {
+ // Arrange
+ var controllerContext = new ControllerContext();
+ var buildManager = new MockBuildManager("view path", typeof(NoParameterlessCtor));
+ var dependencyResolver = new Mock<IDependencyResolver>();
+ var view = new TestableBuildManagerCompiledView(controllerContext, "view path", dependencyResolver: dependencyResolver.Object) { BuildManager = buildManager };
+
+ // Act
+ MissingMethodException ex = Assert.Throws<MissingMethodException>( // Depend on the fact that Activator.CreateInstance cannot create an object without a parameterless ctor
+ () => view.Render(new Mock<ViewContext>().Object, new Mock<TextWriter>().Object)
+ );
+
+ // Assert
+ Assert.Contains("System.Activator.CreateInstance(", ex.StackTrace);
+ }
+
+ private class NoParameterlessCtor
+ {
+ public NoParameterlessCtor(int x)
+ {
+ }
+ }
+
+ private sealed class TestableBuildManagerCompiledView : BuildManagerCompiledView
+ {
+ public TestableBuildManagerCompiledView(ControllerContext controllerContext, string viewPath, IViewPageActivator viewPageActivator = null, IDependencyResolver dependencyResolver = null)
+ : base(controllerContext, viewPath, viewPageActivator, dependencyResolver)
+ {
+ }
+
+ protected override void RenderView(ViewContext viewContext, TextWriter writer, object instance)
+ {
+ return;
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/BuildManagerViewEngineTest.cs b/test/System.Web.Mvc.Test/Test/BuildManagerViewEngineTest.cs
new file mode 100644
index 00000000..59640cc0
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/BuildManagerViewEngineTest.cs
@@ -0,0 +1,182 @@
+using Moq;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class BuildManagerViewEngineTest
+ {
+ [Fact]
+ public void BuildManagerProperty()
+ {
+ // Arrange
+ var engine = new TestableBuildManagerViewEngine();
+ var buildManagerMock = new MockBuildManager(expectedVirtualPath: null, compiledType: null);
+
+ // Act
+ engine.BuildManager = buildManagerMock;
+
+ // Assert
+ Assert.Same(engine.BuildManager, buildManagerMock);
+ }
+
+ [Fact]
+ public void FileExistsReturnsTrueForExistingPath()
+ {
+ // Arrange
+ var engine = new TestableBuildManagerViewEngine();
+ var buildManagerMock = new MockBuildManager("some path", typeof(object));
+ engine.BuildManager = buildManagerMock;
+
+ // Act
+ bool result = engine.FileExists("some path");
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void FileExistsReturnsFalseWhenBuildManagerFileExistsReturnsFalse()
+ {
+ // Arrange
+ var engine = new TestableBuildManagerViewEngine();
+ var buildManagerMock = new MockBuildManager("some path", false);
+ engine.BuildManager = buildManagerMock;
+
+ // Act
+ bool result = engine.FileExists("some path");
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void ViewPageActivatorConsultsSetActivatorResolver()
+ {
+ // Arrange
+ Mock<IViewPageActivator> activator = new Mock<IViewPageActivator>();
+
+ // Act
+ TestableBuildManagerViewEngine engine = new TestableBuildManagerViewEngine(activator.Object);
+
+ //Assert
+ Assert.Equal(activator.Object, engine.ViewPageActivator);
+ }
+
+ [Fact]
+ public void ViewPageActivatorDelegatesToActivatorResolver()
+ {
+ // Arrange
+ var activator = new Mock<IViewPageActivator>();
+ var activatorResolver = new Resolver<IViewPageActivator> { Current = activator.Object };
+
+ // Act
+ TestableBuildManagerViewEngine engine = new TestableBuildManagerViewEngine(activatorResolver: activatorResolver);
+
+ // Assert
+ Assert.Equal(activator.Object, engine.ViewPageActivator);
+ }
+
+ [Fact]
+ public void ViewPageActivatorDelegatesToDependencyResolverWhenActivatorResolverIsNull()
+ {
+ // Arrange
+ var viewInstance = new object();
+ var controllerContext = new ControllerContext();
+ var buildManager = new MockBuildManager("view path", typeof(object));
+ var dependencyResolver = new Mock<IDependencyResolver>(MockBehavior.Strict);
+ dependencyResolver.Setup(dr => dr.GetService(typeof(object))).Returns(viewInstance).Verifiable();
+
+ // Act
+ TestableBuildManagerViewEngine engine = new TestableBuildManagerViewEngine(dependencyResolver: dependencyResolver.Object);
+ engine.ViewPageActivator.Create(controllerContext, typeof(object));
+
+ // Assert
+ dependencyResolver.Verify();
+ }
+
+ [Fact]
+ public void ViewPageActivatorDelegatesToActivatorCreateInstanceWhenDependencyResolverReturnsNull()
+ {
+ // Arrange
+ var controllerContext = new ControllerContext();
+ var buildManager = new MockBuildManager("view path", typeof(NoParameterlessCtor));
+ var dependencyResolver = new Mock<IDependencyResolver>();
+
+ var engine = new TestableBuildManagerViewEngine(dependencyResolver: dependencyResolver.Object);
+
+ // Act
+ MissingMethodException ex = Assert.Throws<MissingMethodException>( // Depend on the fact that Activator.CreateInstance cannot create an object without a parameterless ctor
+ () => engine.ViewPageActivator.Create(controllerContext, typeof(NoParameterlessCtor))
+ );
+
+ // Assert
+ Assert.Contains("System.Activator.CreateInstance(", ex.StackTrace);
+ }
+
+ [Fact]
+ public void ActivatorResolverAndDependencyResolverAreNeverCalledWhenViewPageActivatorIsPassedInContstructor()
+ {
+ // Arrange
+ var controllerContext = new ControllerContext();
+ var expectedController = new Goodcontroller();
+
+ Mock<IViewPageActivator> activator = new Mock<IViewPageActivator>();
+
+ var resolverActivator = new Mock<IViewPageActivator>(MockBehavior.Strict);
+ var activatorResolver = new Resolver<IViewPageActivator> { Current = resolverActivator.Object };
+
+ var dependencyResolver = new Mock<IDependencyResolver>(MockBehavior.Strict);
+
+ //Act
+ var engine = new TestableBuildManagerViewEngine(activator.Object, activatorResolver, dependencyResolver.Object);
+
+ //Assert
+ Assert.Same(activator.Object, engine.ViewPageActivator);
+ }
+
+ private class NoParameterlessCtor
+ {
+ public NoParameterlessCtor(int x)
+ {
+ }
+ }
+
+ private class TestableBuildManagerViewEngine : BuildManagerViewEngine
+ {
+ public TestableBuildManagerViewEngine()
+ : base()
+ {
+ }
+
+ public TestableBuildManagerViewEngine(IViewPageActivator viewPageActivator)
+ : base(viewPageActivator)
+ {
+ }
+
+ public TestableBuildManagerViewEngine(IViewPageActivator viewPageActivator = null, IResolver<IViewPageActivator> activatorResolver = null, IDependencyResolver dependencyResolver = null)
+ : base(viewPageActivator, activatorResolver, dependencyResolver)
+ {
+ }
+
+ public new IViewPageActivator ViewPageActivator
+ {
+ get { return base.ViewPageActivator; }
+ }
+
+ protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
+ {
+ throw new NotImplementedException();
+ }
+
+ protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
+ {
+ throw new NotImplementedException();
+ }
+
+ public bool FileExists(string virtualPath)
+ {
+ return base.FileExists(null, virtualPath);
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ByteArrayModelBinderTest.cs b/test/System.Web.Mvc.Test/Test/ByteArrayModelBinderTest.cs
new file mode 100644
index 00000000..2aaf6b53
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ByteArrayModelBinderTest.cs
@@ -0,0 +1,121 @@
+using Microsoft.Web.UnitTestUtil;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ByteArrayModelBinderTest
+ {
+ internal const string Base64TestString = "Fys1";
+ internal static readonly byte[] Base64TestBytes = new byte[] { 23, 43, 53 };
+
+ [Fact]
+ public void BindModelWithNonExistentValueReturnsNull()
+ {
+ // Arrange
+ SimpleValueProvider valueProvider = new SimpleValueProvider()
+ {
+ { "foo", null }
+ };
+
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelName = "foo",
+ ValueProvider = valueProvider
+ };
+
+ ByteArrayModelBinder binder = new ByteArrayModelBinder();
+
+ // Act
+ object binderResult = binder.BindModel(null, bindingContext);
+
+ // Assert
+ Assert.Null(binderResult);
+ }
+
+ [Fact]
+ public void BinderWithEmptyStringValueReturnsNull()
+ {
+ // Arrange
+ SimpleValueProvider valueProvider = new SimpleValueProvider()
+ {
+ { "foo", "" }
+ };
+
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelName = "foo",
+ ValueProvider = valueProvider
+ };
+
+ ByteArrayModelBinder binder = new ByteArrayModelBinder();
+
+ // Act
+ object binderResult = binder.BindModel(null, bindingContext);
+
+ // Assert
+ Assert.Null(binderResult);
+ }
+
+ [Fact]
+ public void BindModelThrowsIfBindingContextIsNull()
+ {
+ // Arrange
+ ByteArrayModelBinder binder = new ByteArrayModelBinder();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { binder.BindModel(null, null); }, "bindingContext");
+ }
+
+ [Fact]
+ public void BindModelWithBase64QuotedValueReturnsByteArray()
+ {
+ // Arrange
+ string base64Value = Base64TestString;
+ SimpleValueProvider valueProvider = new SimpleValueProvider()
+ {
+ { "foo", "\"" + base64Value + "\"" }
+ };
+
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelName = "foo",
+ ValueProvider = valueProvider
+ };
+
+ ByteArrayModelBinder binder = new ByteArrayModelBinder();
+
+ // Act
+ byte[] boundValue = binder.BindModel(null, bindingContext) as byte[];
+
+ // Assert
+ Assert.Equal(Base64TestBytes, boundValue);
+ }
+
+ [Fact]
+ public void BindModelWithBase64UnquotedValueReturnsByteArray()
+ {
+ // Arrange
+ string base64Value = Base64TestString;
+ SimpleValueProvider valueProvider = new SimpleValueProvider()
+ {
+ { "foo", base64Value }
+ };
+
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelName = "foo",
+ ValueProvider = valueProvider
+ };
+
+ ByteArrayModelBinder binder = new ByteArrayModelBinder();
+
+ // Act
+ byte[] boundValue = binder.BindModel(null, bindingContext) as byte[];
+
+ // Assert
+ Assert.Equal(Base64TestBytes, boundValue);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/CachedAssociatedMetadataProviderTest.cs b/test/System.Web.Mvc.Test/Test/CachedAssociatedMetadataProviderTest.cs
new file mode 100644
index 00000000..38388b3d
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/CachedAssociatedMetadataProviderTest.cs
@@ -0,0 +1,275 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.Caching;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class CachedAssociatedMetadataProviderTest
+ {
+ [Fact]
+ public void GetMetadataForPropertyInvalidPropertyNameThrows()
+ {
+ // Arrange
+ MockableCachedAssociatedMetadataProvider provider = new MockableCachedAssociatedMetadataProvider();
+
+ // Act & Assert
+ Assert.Throws<ArgumentException>(
+ () => provider.GetMetadataForProperty(null, typeof(object), "BadPropertyName"),
+ "The property System.Object.BadPropertyName could not be found.");
+ }
+
+ [Fact]
+ public void GetCacheKey_ResultsForTypesDoNotCollide()
+ {
+ // Arrange
+ var provider = new MockableCachedAssociatedMetadataProvider();
+ var keys = new List<string>();
+
+ // Act
+ keys.Add(provider.GetCacheKey(typeof(string)));
+ keys.Add(provider.GetCacheKey(typeof(int)));
+ keys.Add(provider.GetCacheKey(typeof(Nullable<int>)));
+ keys.Add(provider.GetCacheKey(typeof(Nullable<bool>)));
+ keys.Add(provider.GetCacheKey(typeof(List<string>)));
+ keys.Add(provider.GetCacheKey(typeof(List<bool>)));
+
+ // Assert
+ Assert.Equal(keys.Distinct().Count(), keys.Count);
+ }
+
+ [Fact]
+ public void GetCacheKey_ResultsForTypesAndPropertiesDoNotCollide()
+ {
+ // Arrange
+ var provider = new MockableCachedAssociatedMetadataProvider();
+ var keys = new List<string>();
+
+ // Act
+ keys.Add(provider.GetCacheKey(typeof(string), "Foo"));
+ keys.Add(provider.GetCacheKey(typeof(string), "Bar"));
+ keys.Add(provider.GetCacheKey(typeof(int), "Foo"));
+ keys.Add(provider.GetCacheKey(typeof(Nullable<int>), "Foo"));
+ keys.Add(provider.GetCacheKey(typeof(Nullable<bool>), "Foo"));
+ keys.Add(provider.GetCacheKey(typeof(List<string>), "Count"));
+ keys.Add(provider.GetCacheKey(typeof(List<bool>), "Count"));
+ keys.Add(provider.GetCacheKey(typeof(Foo), "BarBaz"));
+ keys.Add(provider.GetCacheKey(typeof(FooBar), "Baz"));
+
+ // Assert
+ Assert.Equal(keys.Distinct().Count(), keys.Count);
+ }
+
+ private class Foo
+ {
+ }
+
+ private class FooBar
+ {
+ }
+
+ // GetMetadataForProperty
+
+ [Fact]
+ public void GetMetadataForPropertyCreatesPrototypeMetadataAndAddsItToCache()
+ {
+ // Arrange
+ var provider = new Mock<MockableCachedAssociatedMetadataProvider> { CallBase = true };
+
+ // Act
+ provider.Object.GetMetadataForProperty(() => 3, typeof(string), "Length");
+
+ // Assert
+ provider.Verify(p => p.CreateMetadataPrototypeImpl(It.IsAny<IEnumerable<Attribute>>(),
+ typeof(string) /* containerType */,
+ typeof(int) /* modelType */,
+ "Length" /* propertyName */));
+ provider.Object.Cache.Verify(c => c.Add(provider.Object.GetCacheKey(typeof(string), "Length"),
+ provider.Object.PrototypeMetadata,
+ provider.Object.CacheItemPolicy, null));
+ }
+
+ [Fact]
+ public void GetMetadataForPropertyCreatesRealMetadataFromPrototype()
+ {
+ // Arrange
+ Func<object> accessor = () => 3;
+ var provider = new Mock<MockableCachedAssociatedMetadataProvider> { CallBase = true };
+
+ // Act
+ provider.Object.GetMetadataForProperty(accessor, typeof(string), "Length");
+
+ // Assert
+ provider.Verify(p => p.CreateMetadataFromPrototypeImpl(provider.Object.PrototypeMetadata, accessor));
+ }
+
+ [Fact]
+ public void MetaDataAwareAttributesForPropertyAreAppliedToMetadata()
+ {
+ // Arrange
+ MemoryCache memoryCache = new MemoryCache("testCache");
+ MockableCachedAssociatedMetadataProvider provider = new MockableCachedAssociatedMetadataProvider(memoryCache);
+
+ // Act
+ ModelMetadata metadata = provider.GetMetadataForProperty(null, typeof(ClassWithMetaDataAwareAttributes), "PropertyWithAdditionalValue");
+
+ // Assert
+ Assert.True(metadata.AdditionalValues["baz"].Equals("biz"));
+ }
+
+ [Fact]
+ public void GetMetadataForPropertyTwiceOnlyCreatesAndCachesPrototypeOnce()
+ {
+ // Arrange
+ Func<object> accessor = () => 3;
+ var provider = new Mock<MockableCachedAssociatedMetadataProvider> { CallBase = true };
+
+ // Act
+ provider.Object.GetMetadataForProperty(accessor, typeof(string), "Length");
+ provider.Object.GetMetadataForProperty(accessor, typeof(string), "Length");
+
+ // Assert
+ provider.Verify(p => p.CreateMetadataPrototypeImpl(It.IsAny<IEnumerable<Attribute>>(),
+ typeof(string) /* containerType */,
+ typeof(int) /* modelType */,
+ "Length" /* propertyName */),
+ Times.Once());
+
+ provider.Verify(p => p.CreateMetadataFromPrototypeImpl(provider.Object.PrototypeMetadata, accessor),
+ Times.Exactly(2));
+
+ provider.Object.Cache.Verify(c => c.Add(provider.Object.GetCacheKey(typeof(string), "Length"),
+ provider.Object.PrototypeMetadata,
+ provider.Object.CacheItemPolicy, null),
+ Times.Once());
+ }
+
+ // GetMetadataForType
+
+ [Fact]
+ public void GetMetadataForTypeCreatesPrototypeMetadataAndAddsItToCache()
+ {
+ // Arrange
+ var provider = new Mock<MockableCachedAssociatedMetadataProvider> { CallBase = true };
+
+ // Act
+ provider.Object.GetMetadataForType(() => "foo", typeof(string));
+
+ // Assert
+ provider.Verify(p => p.CreateMetadataPrototypeImpl(It.IsAny<IEnumerable<Attribute>>(),
+ null /* containerType */,
+ typeof(string) /* modelType */,
+ null /* propertyName */));
+ provider.Object.Cache.Verify(c => c.Add(provider.Object.GetCacheKey(typeof(string), null),
+ provider.Object.PrototypeMetadata,
+ provider.Object.CacheItemPolicy, null));
+ }
+
+ [Fact]
+ public void GetMetadataForTypeCreatesRealMetadataFromPrototype()
+ {
+ // Arrange
+ Func<object> accessor = () => "foo";
+ var provider = new Mock<MockableCachedAssociatedMetadataProvider> { CallBase = true };
+
+ // Act
+ provider.Object.GetMetadataForType(accessor, typeof(string));
+
+ // Assert
+ provider.Verify(p => p.CreateMetadataFromPrototypeImpl(provider.Object.PrototypeMetadata, accessor));
+ }
+
+ [Fact]
+ public void MetaDataAwareAttributesForTypeAreAppliedToMetadata()
+ {
+ // Arrange
+ MemoryCache memoryCache = new MemoryCache("testCache");
+ MockableCachedAssociatedMetadataProvider provider = new MockableCachedAssociatedMetadataProvider(memoryCache);
+
+ // Act
+ ModelMetadata metadata = provider.GetMetadataForType(null, typeof(ClassWithMetaDataAwareAttributes));
+
+ // Assert
+ Assert.True(metadata.AdditionalValues["foo"].Equals("bar"));
+ }
+
+ [Fact]
+ public void GetMetadataForTypeTwiceOnlyCreatesAndCachesPrototypeOnce()
+ {
+ // Arrange
+ Func<object> accessor = () => "foo";
+ var provider = new Mock<MockableCachedAssociatedMetadataProvider> { CallBase = true };
+
+ // Act
+ provider.Object.GetMetadataForType(accessor, typeof(string));
+ provider.Object.GetMetadataForType(accessor, typeof(string));
+
+ // Assert
+ provider.Verify(p => p.CreateMetadataPrototypeImpl(It.IsAny<IEnumerable<Attribute>>(),
+ null /* containerType */,
+ typeof(string) /* modelType */,
+ null /* propertyName */),
+ Times.Once());
+
+ provider.Verify(p => p.CreateMetadataFromPrototypeImpl(provider.Object.PrototypeMetadata, accessor),
+ Times.Exactly(2));
+
+ provider.Object.Cache.Verify(c => c.Add(provider.Object.GetCacheKey(typeof(string), null),
+ provider.Object.PrototypeMetadata,
+ provider.Object.CacheItemPolicy, null),
+ Times.Once());
+ }
+
+ // Helpers
+
+ public class MockableCachedAssociatedMetadataProvider : CachedAssociatedMetadataProvider<ModelMetadata>
+ {
+ public Mock<MemoryCache> Cache;
+ public ModelMetadata PrototypeMetadata;
+ public ModelMetadata RealMetadata;
+
+ public MockableCachedAssociatedMetadataProvider()
+ : this(null)
+ {
+ }
+
+ public MockableCachedAssociatedMetadataProvider(MemoryCache memoryCache = null)
+ {
+ Cache = new Mock<MemoryCache>("MockMemoryCache", null) { CallBase = true };
+ PrototypeMetadata = new ModelMetadata(this, null, null, typeof(string), null);
+ RealMetadata = new ModelMetadata(this, null, null, typeof(string), null);
+
+ PrototypeCache = memoryCache ?? Cache.Object;
+ }
+
+ public virtual ModelMetadata CreateMetadataPrototypeImpl(IEnumerable<Attribute> attributes, Type containerType, Type modelType, string propertyName)
+ {
+ return PrototypeMetadata;
+ }
+
+ public virtual ModelMetadata CreateMetadataFromPrototypeImpl(ModelMetadata prototype, Func<object> modelAccessor)
+ {
+ return RealMetadata;
+ }
+
+ protected override ModelMetadata CreateMetadataPrototype(IEnumerable<Attribute> attributes, Type containerType, Type modelType, string propertyName)
+ {
+ return CreateMetadataPrototypeImpl(attributes, containerType, modelType, propertyName);
+ }
+
+ protected override ModelMetadata CreateMetadataFromPrototype(ModelMetadata prototype, Func<object> modelAccessor)
+ {
+ return CreateMetadataFromPrototypeImpl(prototype, modelAccessor);
+ }
+ }
+
+ [AdditionalMetadata("foo", "bar")]
+ private class ClassWithMetaDataAwareAttributes
+ {
+ [AdditionalMetadata("baz", "biz")]
+ public string PropertyWithAdditionalValue { get; set; }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/CachedDataAnnotationsModelMetadataProviderTest.cs b/test/System.Web.Mvc.Test/Test/CachedDataAnnotationsModelMetadataProviderTest.cs
new file mode 100644
index 00000000..0de0b039
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/CachedDataAnnotationsModelMetadataProviderTest.cs
@@ -0,0 +1,10 @@
+namespace System.Web.Mvc.Test
+{
+ public class CachedDataAnnotationsModelMetadataProviderTest : DataAnnotationsModelMetadataProviderTestBase
+ {
+ protected override AssociatedMetadataProvider MakeProvider()
+ {
+ return new CachedDataAnnotationsModelMetadataProvider();
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/CancellationTokenModelBinderTest.cs b/test/System.Web.Mvc.Test/Test/CancellationTokenModelBinderTest.cs
new file mode 100644
index 00000000..52ee5649
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/CancellationTokenModelBinderTest.cs
@@ -0,0 +1,21 @@
+using System.Threading;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class CancellationTokenModelBinderTest
+ {
+ [Fact]
+ public void BinderReturnsDefaultCancellationToken()
+ {
+ // Arrange
+ CancellationTokenModelBinder binder = new CancellationTokenModelBinder();
+
+ // Act
+ object binderResult = binder.BindModel(controllerContext: null, bindingContext: null);
+
+ // Assert
+ Assert.Equal(default(CancellationToken), binderResult);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ChildActionOnlyAttributeTest.cs b/test/System.Web.Mvc.Test/Test/ChildActionOnlyAttributeTest.cs
new file mode 100644
index 00000000..1df26168
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ChildActionOnlyAttributeTest.cs
@@ -0,0 +1,52 @@
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ChildActionOnlyAttributeTest
+ {
+ [Fact]
+ public void GuardClause()
+ {
+ // Arrange
+ ChildActionOnlyAttribute attr = new ChildActionOnlyAttribute();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => attr.OnAuthorization(null /* filterContext */),
+ "filterContext"
+ );
+ }
+
+ [Fact]
+ public void DoesNothingForChildRequest()
+ {
+ // Arrange
+ ChildActionOnlyAttribute attr = new ChildActionOnlyAttribute();
+ Mock<AuthorizationContext> context = new Mock<AuthorizationContext>();
+ context.Setup(c => c.IsChildAction).Returns(true);
+
+ // Act
+ attr.OnAuthorization(context.Object);
+
+ // Assert
+ Assert.Null(context.Object.Result);
+ }
+
+ [Fact]
+ public void ThrowsIfNotChildRequest()
+ {
+ // Arrange
+ ChildActionOnlyAttribute attr = new ChildActionOnlyAttribute();
+ Mock<AuthorizationContext> context = new Mock<AuthorizationContext>();
+ context.Setup(c => c.IsChildAction).Returns(false);
+ context.Setup(c => c.ActionDescriptor.ActionName).Returns("some name");
+
+ // Act & assert
+ Assert.Throws<InvalidOperationException>(
+ delegate { attr.OnAuthorization(context.Object); },
+ @"The action 'some name' is accessible only by a child request.");
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ChildActionValueProviderFactoryTest.cs b/test/System.Web.Mvc.Test/Test/ChildActionValueProviderFactoryTest.cs
new file mode 100644
index 00000000..2ea59353
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ChildActionValueProviderFactoryTest.cs
@@ -0,0 +1,93 @@
+using System.Globalization;
+using System.Web.Routing;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ExpiicitRouteDataValueProviderFactoryTest
+ {
+ [Fact]
+ public void GetValueProviderReturnsChildActionValue()
+ {
+ // Arrange
+ ChildActionValueProviderFactory factory = new ChildActionValueProviderFactory();
+
+ ControllerContext controllerContext = new ControllerContext();
+ controllerContext.RouteData = new RouteData();
+
+ string conflictingKey = "conflictingKey";
+
+ controllerContext.RouteData.Values["conflictingKey"] = 43;
+
+ DictionaryValueProvider<object> explictValueDictionary = new DictionaryValueProvider<object>(new RouteValueDictionary { { conflictingKey, 42 } }, CultureInfo.InvariantCulture);
+ controllerContext.RouteData.Values[ChildActionValueProvider.ChildActionValuesKey] = explictValueDictionary;
+
+ // Act
+ IValueProvider valueProvider = factory.GetValueProvider(controllerContext);
+
+ // Assert
+ Assert.Equal(typeof(ChildActionValueProvider), valueProvider.GetType());
+ ValueProviderResult vpResult = valueProvider.GetValue(conflictingKey);
+
+ Assert.NotNull(vpResult);
+ Assert.Equal(42, vpResult.RawValue);
+ Assert.Equal("42", vpResult.AttemptedValue);
+ Assert.Equal(CultureInfo.InvariantCulture, vpResult.Culture);
+ }
+
+ [Fact]
+ public void GetValueProviderReturnsNullIfNoChildActionDictionary()
+ {
+ // Arrange
+ ChildActionValueProviderFactory factory = new ChildActionValueProviderFactory();
+
+ ControllerContext controllerContext = new ControllerContext();
+ controllerContext.RouteData = new RouteData();
+ controllerContext.RouteData.Values["forty-two"] = 42;
+
+ // Act
+ IValueProvider valueProvider = factory.GetValueProvider(controllerContext);
+
+ // Assert
+ Assert.Equal(typeof(ChildActionValueProvider), valueProvider.GetType());
+ ValueProviderResult vpResult = valueProvider.GetValue("forty-two");
+
+ Assert.Null(vpResult);
+ }
+
+ [Fact]
+ public void GetValueProviderReturnsNullIfKeyIsNotInChildActionDictionary()
+ {
+ // Arrange
+ ChildActionValueProviderFactory factory = new ChildActionValueProviderFactory();
+
+ ControllerContext controllerContext = new ControllerContext();
+ controllerContext.RouteData = new RouteData();
+ controllerContext.RouteData.Values["forty-two"] = 42;
+
+ DictionaryValueProvider<object> explictValueDictionary = new DictionaryValueProvider<object>(new RouteValueDictionary { { "forty-three", 42 } }, CultureInfo.CurrentUICulture);
+ controllerContext.RouteData.Values[ChildActionValueProvider.ChildActionValuesKey] = explictValueDictionary;
+
+ // Act
+ IValueProvider valueProvider = factory.GetValueProvider(controllerContext);
+
+ // Assert
+ Assert.Equal(typeof(ChildActionValueProvider), valueProvider.GetType());
+ ValueProviderResult vpResult = valueProvider.GetValue("forty-two");
+
+ Assert.Null(vpResult);
+ }
+
+ [Fact]
+ public void GetValueProvider_ThrowsIfControllerContextIsNull()
+ {
+ // Arrange
+ RouteDataValueProviderFactory factory = new RouteDataValueProviderFactory();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { factory.GetValueProvider(null); }, "controllerContext");
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ClientDataTypeModelValidatorProviderTest.cs b/test/System.Web.Mvc.Test/Test/ClientDataTypeModelValidatorProviderTest.cs
new file mode 100644
index 00000000..524bfa77
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ClientDataTypeModelValidatorProviderTest.cs
@@ -0,0 +1,215 @@
+using System.Collections.Generic;
+using System.Linq;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ [CLSCompliant(false)]
+ public class ClientDataTypeModelValidatorProviderTest
+ {
+ private static readonly EmptyModelMetadataProvider _metadataProvider = new EmptyModelMetadataProvider();
+ private static readonly ClientDataTypeModelValidatorProvider _validatorProvider = new ClientDataTypeModelValidatorProvider();
+
+ private bool ReturnsValidator<TValidator>(string propertyName)
+ {
+ ModelMetadata metadata = _metadataProvider.GetMetadataForProperty(null, typeof(SampleModel), propertyName);
+ IEnumerable<ModelValidator> validators = _validatorProvider.GetValidators(metadata, new ControllerContext());
+ return validators.Any(v => v is TValidator);
+ }
+
+ [Theory]
+ [InlineData("Byte"), InlineData("SByte"), InlineData("Int16"), InlineData("UInt16")]
+ [InlineData("Int32"), InlineData("UInt32"), InlineData("Int64"), InlineData("UInt64")]
+ [InlineData("Single"), InlineData("Double"), InlineData("Decimal"), InlineData("NullableInt32")]
+ public void GetValidators_NumericValidatorTypes(string propertyName)
+ {
+ // Act & assert
+ Assert.True(ReturnsValidator<ClientDataTypeModelValidatorProvider.NumericModelValidator>(propertyName));
+ }
+
+ [Theory]
+ [InlineData("String"), InlineData("Object"), InlineData("DateTime"), InlineData("NullableDateTime")]
+ public void GetValidators_NonNumericValidatorTypes(string propertyName)
+ {
+ // Act & assert
+ Assert.False(ReturnsValidator<ClientDataTypeModelValidatorProvider.NumericModelValidator>(propertyName));
+ }
+
+ [Theory]
+ [InlineData("DateTime"), InlineData("NullableDateTime")]
+ public void GetValidators_DateTimeValidatorTypes(string propertyName)
+ {
+ // Act & assert
+ Assert.True(ReturnsValidator<ClientDataTypeModelValidatorProvider.DateModelValidator>(propertyName));
+ }
+
+ [Theory]
+ [InlineData("Int32"), InlineData("NullableInt32"), InlineData("String"), InlineData("Object")]
+ public void GetValidators_NonDateTimeValidatorTypes(string propertyName)
+ {
+ // Act & assert
+ Assert.False(ReturnsValidator<ClientDataTypeModelValidatorProvider.DateModelValidator>(propertyName));
+ }
+
+ [Fact]
+ public void GuardClauses()
+ {
+ // Arrange
+ ModelMetadata metadata = _metadataProvider.GetMetadataForType(null, typeof(SampleModel));
+
+ // Act & assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ () => new ClientDataTypeModelValidatorProvider.ClientModelValidator(metadata, new ControllerContext(), "testValidationType", errorMessage: null),
+ "errorMessage");
+
+ Assert.ThrowsArgumentNullOrEmpty(
+ () => new ClientDataTypeModelValidatorProvider.ClientModelValidator(metadata, new ControllerContext(), "testValidationType", errorMessage: String.Empty),
+ "errorMessage");
+
+ Assert.ThrowsArgumentNullOrEmpty(
+ () => new ClientDataTypeModelValidatorProvider.ClientModelValidator(metadata, new ControllerContext(), validationType: null, errorMessage: "testErrorMessage"),
+ "validationType");
+
+ Assert.ThrowsArgumentNullOrEmpty(
+ () => new ClientDataTypeModelValidatorProvider.ClientModelValidator(metadata, new ControllerContext(), validationType: String.Empty, errorMessage: "testErrorMessage"),
+ "validationType");
+ }
+
+ [Fact]
+ public void GetValidators_ThrowsIfContextIsNull()
+ {
+ // Arrange
+ ModelMetadata metadata = _metadataProvider.GetMetadataForType(null, typeof(SampleModel));
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ () => _validatorProvider.GetValidators(metadata, null),
+ "context");
+ }
+
+ [Fact]
+ public void GetValidators_ThrowsIfMetadataIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ () => _validatorProvider.GetValidators(null, new ControllerContext()),
+ "metadata");
+ }
+
+ [Fact]
+ public void NumericValidator_GetClientValidationRules()
+ {
+ // Arrange
+ ModelMetadata metadata = _metadataProvider.GetMetadataForProperty(null, typeof(SampleModel), "Int32");
+ var validator = new ClientDataTypeModelValidatorProvider.NumericModelValidator(metadata, new ControllerContext());
+
+ // Act
+ ModelClientValidationRule[] rules = validator.GetClientValidationRules().ToArray();
+
+ // Assert
+ ModelClientValidationRule rule = Assert.Single(rules);
+ Assert.Equal("number", rule.ValidationType);
+ Assert.Empty(rule.ValidationParameters);
+ Assert.Equal("The field Int32 must be a number.", rule.ErrorMessage);
+ }
+
+ [Fact]
+ public void DateValidator_GetClientValidationRules()
+ {
+ // Arrange
+ ModelMetadata metadata = _metadataProvider.GetMetadataForProperty(null, typeof(SampleModel), "DateTime");
+ var validator = new ClientDataTypeModelValidatorProvider.DateModelValidator(metadata, new ControllerContext());
+
+ // Act
+ ModelClientValidationRule[] rules = validator.GetClientValidationRules().ToArray();
+
+ // Assert
+ ModelClientValidationRule rule = Assert.Single(rules);
+ Assert.Equal("date", rule.ValidationType);
+ Assert.Empty(rule.ValidationParameters);
+ Assert.Equal("The field DateTime must be a date.", rule.ErrorMessage);
+ }
+
+ [Fact]
+ public void ClientModelValidator_Validate_DoesNotReadPropertyValue()
+ {
+ // Arrange
+ ObservableModel model = new ObservableModel();
+ ModelMetadata metadata = _metadataProvider.GetMetadataForProperty(() => model.TheProperty, typeof(ObservableModel), "TheProperty");
+ ControllerContext controllerContext = new ControllerContext();
+
+ // Act
+ ModelValidator[] validators = new ClientDataTypeModelValidatorProvider().GetValidators(metadata, controllerContext).ToArray();
+ ModelValidationResult[] results = validators.SelectMany(o => o.Validate(model)).ToArray();
+
+ // Assert
+ Assert.Equal(new Type[] { typeof(ClientDataTypeModelValidatorProvider.NumericModelValidator) }, Array.ConvertAll(validators, o => o.GetType()));
+ Assert.Empty(results);
+ Assert.False(model.PropertyWasRead());
+ }
+
+ [Fact]
+ public void ClientModelValidator_Validate_ReturnsEmptyCollection()
+ {
+ // Arrange
+ ModelMetadata metadata = _metadataProvider.GetMetadataForType(null, typeof(object));
+ var validator = new ClientDataTypeModelValidatorProvider.ClientModelValidator(metadata, new ControllerContext(), "testValidationType", "testErrorMessage");
+
+ // Act
+ IEnumerable<ModelValidationResult> result = validator.Validate(null);
+
+ // Assert
+ Assert.Empty(result);
+ }
+
+ private class SampleModel
+ {
+ // these should have 'numeric' validators associated with them
+ public byte Byte { get; set; }
+ public sbyte SByte { get; set; }
+ public short Int16 { get; set; }
+ public ushort UInt16 { get; set; }
+ public int Int32 { get; set; }
+ public uint UInt32 { get; set; }
+ public long Int64 { get; set; }
+ public ulong UInt64 { get; set; }
+ public float Single { get; set; }
+ public double Double { get; set; }
+ public decimal Decimal { get; set; }
+
+ // this should also have a 'numeric' validator
+ public int? NullableInt32 { get; set; }
+
+ // this should have a 'date' validator associated with it
+ public DateTime DateTime { get; set; }
+
+ // this should also have a 'date' validator associated with it
+ public DateTime? NullableDateTime { get; set; }
+
+ // these shouldn't have any validators
+ public string String { get; set; }
+ public object Object { get; set; }
+ }
+
+ private class ObservableModel
+ {
+ private bool _propertyWasRead;
+
+ public int TheProperty
+ {
+ get
+ {
+ _propertyWasRead = true;
+ return 42;
+ }
+ }
+
+ public bool PropertyWasRead()
+ {
+ return _propertyWasRead;
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/CompareAttributeTest.cs b/test/System.Web.Mvc.Test/Test/CompareAttributeTest.cs
new file mode 100644
index 00000000..1a3a76e8
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/CompareAttributeTest.cs
@@ -0,0 +1,198 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class CompareAttributeTest
+ {
+ [Fact]
+ public void GuardClauses()
+ {
+ //Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { new CompareAttribute(null); }, "otherProperty");
+
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { CompareAttribute.FormatPropertyForClientValidation(null); }, "property");
+ }
+
+ [Fact]
+ public void FormatPropertyForClientValidationPrependsStarDot()
+ {
+ string prepended = CompareAttribute.FormatPropertyForClientValidation("test");
+ Assert.Equal(prepended, "*.test");
+ }
+
+ [Fact]
+ public void ValidateDoesNotThrowWhenComparedObjectsAreEqual()
+ {
+ object otherObject = new CompareObject("test");
+ CompareObject currentObject = new CompareObject("test");
+ ValidationContext testContext = new ValidationContext(otherObject, null, null);
+
+ CompareAttribute attr = new CompareAttribute("CompareProperty");
+ attr.Validate(currentObject.CompareProperty, testContext);
+ }
+
+ [Fact]
+ public void ValidateThrowsWhenComparedObjectsAreNotEqual()
+ {
+ CompareObject currentObject = new CompareObject("a");
+ object otherObject = new CompareObject("b");
+
+ ValidationContext testContext = new ValidationContext(otherObject, null, null);
+ testContext.DisplayName = "CurrentProperty";
+
+ CompareAttribute attr = new CompareAttribute("CompareProperty");
+ Assert.Throws<ValidationException>(
+ delegate { attr.Validate(currentObject.CompareProperty, testContext); }, "'CurrentProperty' and 'CompareProperty' do not match.");
+ }
+
+ [Fact]
+ public void ValidateThrowsWithOtherPropertyDisplayName()
+ {
+ CompareObject currentObject = new CompareObject("a");
+ object otherObject = new CompareObject("b");
+
+ ValidationContext testContext = new ValidationContext(otherObject, null, null);
+ testContext.DisplayName = "CurrentProperty";
+
+ CompareAttribute attr = new CompareAttribute("ComparePropertyWithDisplayName");
+ Assert.Throws<ValidationException>(
+ delegate { attr.Validate(currentObject.CompareProperty, testContext); }, "'CurrentProperty' and 'DisplayName' do not match.");
+ }
+
+ [Fact]
+ public void ValidateUsesSetDisplayName()
+ {
+ CompareObject currentObject = new CompareObject("a");
+ object otherObject = new CompareObject("b");
+
+ ValidationContext testContext = new ValidationContext(otherObject, null, null);
+ testContext.DisplayName = "CurrentProperty";
+
+ CompareAttribute attr = new CompareAttribute("ComparePropertyWithDisplayName");
+ attr.OtherPropertyDisplayName = "SetDisplayName";
+
+ Assert.Throws<ValidationException>(
+ delegate { attr.Validate(currentObject.CompareProperty, testContext); }, "'CurrentProperty' and 'SetDisplayName' do not match.");
+ }
+
+ [Fact]
+ public void ValidateThrowsWhenPropertyNameIsUnknown()
+ {
+ CompareObject currentObject = new CompareObject("a");
+ object otherObject = new CompareObject("b");
+
+ ValidationContext testContext = new ValidationContext(otherObject, null, null);
+ testContext.DisplayName = "CurrentProperty";
+
+ CompareAttribute attr = new CompareAttribute("UnknownPropertyName");
+ Assert.Throws<ValidationException>(
+ () => attr.Validate(currentObject.CompareProperty, testContext),
+ "Could not find a property named UnknownPropertyName."
+ );
+ }
+
+ [Fact]
+ public void GetClientValidationRulesReturnsModelClientValidationEqualToRule()
+ {
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+ Mock<ModelMetadata> metadata = new Mock<ModelMetadata>(provider.Object, null, null, typeof(string), null);
+ metadata.Setup(m => m.DisplayName).Returns("CurrentProperty");
+
+ CompareAttribute attr = new CompareAttribute("CompareProperty");
+ List<ModelClientValidationRule> ruleList = new List<ModelClientValidationRule>(attr.GetClientValidationRules(metadata.Object, null));
+
+ Assert.Equal(ruleList.Count, 1);
+
+ ModelClientValidationEqualToRule actualRule = ruleList[0] as ModelClientValidationEqualToRule;
+
+ Assert.Equal("'CurrentProperty' and 'CompareProperty' do not match.", actualRule.ErrorMessage);
+ Assert.Equal("equalto", actualRule.ValidationType);
+ Assert.Equal("*.CompareProperty", actualRule.ValidationParameters["other"]);
+ }
+
+ [Fact]
+ public void ModelClientValidationEqualToRuleErrorMessageUsesOtherPropertyDisplayName()
+ {
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+ ModelMetadata metadata = new ModelMetadata(provider.Object, typeof(CompareObject), null, typeof(string), null);
+ metadata.DisplayName = "CurrentProperty";
+
+ CompareAttribute attr = new CompareAttribute("ComparePropertyWithDisplayName");
+ List<ModelClientValidationRule> ruleList = new List<ModelClientValidationRule>(attr.GetClientValidationRules(metadata, null));
+
+ Assert.Equal(ruleList.Count, 1);
+
+ ModelClientValidationEqualToRule actualRule = ruleList[0] as ModelClientValidationEqualToRule;
+
+ Assert.Equal("'CurrentProperty' and 'DisplayName' do not match.", actualRule.ErrorMessage);
+ Assert.Equal("equalto", actualRule.ValidationType);
+ Assert.Equal("*.ComparePropertyWithDisplayName", actualRule.ValidationParameters["other"]);
+ }
+
+ [Fact]
+ public void ModelClientValidationEqualToRuleUsesSetDisplayName()
+ {
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+ ModelMetadata metadata = new ModelMetadata(provider.Object, typeof(CompareObject), null, typeof(string), null);
+ metadata.DisplayName = "CurrentProperty";
+
+ CompareAttribute attr = new CompareAttribute("ComparePropertyWithDisplayName");
+ attr.OtherPropertyDisplayName = "SetDisplayName";
+
+ List<ModelClientValidationRule> ruleList = new List<ModelClientValidationRule>(attr.GetClientValidationRules(metadata, null));
+ Assert.Equal(ruleList.Count, 1);
+ ModelClientValidationEqualToRule actualRule = ruleList[0] as ModelClientValidationEqualToRule;
+
+ Assert.Equal("'CurrentProperty' and 'SetDisplayName' do not match.", actualRule.ErrorMessage);
+ }
+
+ [Fact]
+ public void CompareAttributeCanBeDerivedFromAndOverrideIsValid()
+ {
+ object otherObject = new CompareObject("a");
+ CompareObject currentObject = new CompareObject("b");
+ ValidationContext testContext = new ValidationContext(otherObject, null, null);
+
+ DerivedCompareAttribute attr = new DerivedCompareAttribute("CompareProperty");
+ attr.Validate(currentObject.CompareProperty, testContext);
+ }
+
+ private class DerivedCompareAttribute : CompareAttribute
+ {
+ public DerivedCompareAttribute(string otherProperty)
+ : base(otherProperty)
+ {
+ }
+
+ public override bool IsValid(object value)
+ {
+ return false;
+ }
+
+ protected override ValidationResult IsValid(object value, ValidationContext context)
+ {
+ return null;
+ }
+ }
+
+ private class CompareObject
+ {
+ public string CompareProperty { get; set; }
+
+ [Display(Name = "DisplayName")]
+ public string ComparePropertyWithDisplayName { get; set; }
+
+ public CompareObject(string otherValue)
+ {
+ CompareProperty = otherValue;
+ ComparePropertyWithDisplayName = otherValue;
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ContentResultTest.cs b/test/System.Web.Mvc.Test/Test/ContentResultTest.cs
new file mode 100644
index 00000000..888c16aa
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ContentResultTest.cs
@@ -0,0 +1,158 @@
+using System.Text;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ContentResultTest
+ {
+ [Fact]
+ public void AllPropertiesDefaultToNull()
+ {
+ // Act
+ ContentResult result = new ContentResult();
+
+ // Assert
+ Assert.Null(result.Content);
+ Assert.Null(result.ContentEncoding);
+ Assert.Null(result.ContentType);
+ }
+
+ [Fact]
+ public void EmptyContentTypeIsNotOutput()
+ {
+ // Arrange
+ string content = "Some content.";
+ Encoding contentEncoding = Encoding.UTF8;
+
+ // Arrange expectations
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>(MockBehavior.Strict);
+ mockControllerContext.SetupSet(c => c.HttpContext.Response.ContentEncoding = contentEncoding).Verifiable();
+ mockControllerContext.Setup(c => c.HttpContext.Response.Write(content)).Verifiable();
+
+ ContentResult result = new ContentResult
+ {
+ Content = content,
+ ContentType = String.Empty,
+ ContentEncoding = contentEncoding
+ };
+
+ // Act
+ result.ExecuteResult(mockControllerContext.Object);
+
+ // Assert
+ mockControllerContext.Verify();
+ }
+
+ [Fact]
+ public void ExecuteResult()
+ {
+ // Arrange
+ string content = "Some content.";
+ string contentType = "Some content type.";
+ Encoding contentEncoding = Encoding.UTF8;
+
+ // Arrange expectations
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>(MockBehavior.Strict);
+ mockControllerContext.SetupSet(c => c.HttpContext.Response.ContentType = contentType).Verifiable();
+ mockControllerContext.SetupSet(c => c.HttpContext.Response.ContentEncoding = contentEncoding).Verifiable();
+ mockControllerContext.Setup(c => c.HttpContext.Response.Write(content)).Verifiable();
+
+ ContentResult result = new ContentResult
+ {
+ Content = content,
+ ContentType = contentType,
+ ContentEncoding = contentEncoding
+ };
+
+ // Act
+ result.ExecuteResult(mockControllerContext.Object);
+
+ // Assert
+ mockControllerContext.Verify();
+ }
+
+ [Fact]
+ public void ExecuteResultWithNullContextThrows()
+ {
+ Assert.ThrowsArgumentNull(
+ delegate { new ContentResult().ExecuteResult(null /* context */); }, "context");
+ }
+
+ [Fact]
+ public void NullContentIsNotOutput()
+ {
+ // Arrange
+ string contentType = "Some content type.";
+ Encoding contentEncoding = Encoding.UTF8;
+
+ // Arrange expectations
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.SetupSet(c => c.HttpContext.Response.ContentType = contentType).Verifiable();
+ mockControllerContext.SetupSet(c => c.HttpContext.Response.ContentEncoding = contentEncoding).Verifiable();
+
+ ContentResult result = new ContentResult
+ {
+ ContentType = contentType,
+ ContentEncoding = contentEncoding
+ };
+
+ // Act
+ result.ExecuteResult(mockControllerContext.Object);
+
+ // Assert
+ mockControllerContext.Verify();
+ }
+
+ [Fact]
+ public void NullContentEncodingIsNotOutput()
+ {
+ // Arrange
+ string content = "Some content.";
+ string contentType = "Some content type.";
+
+ // Arrange expectations
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>(MockBehavior.Strict);
+ mockControllerContext.SetupSet(c => c.HttpContext.Response.ContentType = contentType).Verifiable();
+ mockControllerContext.Setup(c => c.HttpContext.Response.Write(content)).Verifiable();
+
+ ContentResult result = new ContentResult
+ {
+ Content = content,
+ ContentType = contentType,
+ };
+
+ // Act
+ result.ExecuteResult(mockControllerContext.Object);
+
+ // Assert
+ mockControllerContext.Verify();
+ }
+
+ [Fact]
+ public void NullContentTypeIsNotOutput()
+ {
+ // Arrange
+ string content = "Some content.";
+ Encoding contentEncoding = Encoding.UTF8;
+
+ // Arrange expectations
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>(MockBehavior.Strict);
+ mockControllerContext.SetupSet(c => c.HttpContext.Response.ContentEncoding = contentEncoding).Verifiable();
+ mockControllerContext.Setup(c => c.HttpContext.Response.Write(content)).Verifiable();
+
+ ContentResult result = new ContentResult
+ {
+ Content = content,
+ ContentEncoding = contentEncoding
+ };
+
+ // Act
+ result.ExecuteResult(mockControllerContext.Object);
+
+ // Assert
+ mockControllerContext.Verify();
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ControllerActionInvokerTest.cs b/test/System.Web.Mvc.Test/Test/ControllerActionInvokerTest.cs
new file mode 100644
index 00000000..5327961a
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ControllerActionInvokerTest.cs
@@ -0,0 +1,2427 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Globalization;
+using System.Linq;
+using System.Reflection;
+using System.Threading;
+using Microsoft.Web.UnitTestUtil;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ [CLSCompliant(false)]
+ public class ControllerActionInvokerTest
+ {
+ [Fact]
+ public void CreateActionResultWithActionResultParameterReturnsParameterUnchanged()
+ {
+ // Arrange
+ ControllerActionInvokerHelper invoker = new ControllerActionInvokerHelper();
+ ActionResult originalResult = new JsonResult();
+
+ // Act
+ ActionResult returnedActionResult = invoker.PublicCreateActionResult(null, null, originalResult);
+
+ // Assert
+ Assert.Same(originalResult, returnedActionResult);
+ }
+
+ [Fact]
+ public void CreateActionResultWithNullParameterReturnsEmptyResult()
+ {
+ // Arrange
+ ControllerActionInvokerHelper invoker = new ControllerActionInvokerHelper();
+
+ // Act
+ ActionResult returnedActionResult = invoker.PublicCreateActionResult(null, null, null);
+
+ // Assert
+ Assert.IsType<EmptyResult>(returnedActionResult);
+ }
+
+ [Fact]
+ public void CreateActionResultWithObjectParameterReturnsContentResult()
+ {
+ // Arrange
+ ControllerActionInvokerHelper invoker = new ControllerActionInvokerHelper();
+ object originalReturnValue = new CultureReflector();
+
+ // Act
+ ActionResult returnedActionResult = invoker.PublicCreateActionResult(null, null, originalReturnValue);
+
+ // Assert
+ ContentResult contentResult = Assert.IsType<ContentResult>(returnedActionResult);
+ Assert.Equal("ivl", contentResult.Content);
+ }
+
+ [Fact]
+ public void FindAction()
+ {
+ // Arrange
+ EmptyController controller = new EmptyController();
+ ControllerContext controllerContext = GetControllerContext(controller);
+ ControllerActionInvokerHelper helper = new ControllerActionInvokerHelper();
+
+ ActionDescriptor expectedAd = new Mock<ActionDescriptor>().Object;
+ Mock<ControllerDescriptor> mockCd = new Mock<ControllerDescriptor>();
+ mockCd.Setup(cd => cd.FindAction(controllerContext, "someAction")).Returns(expectedAd);
+
+ // Act
+ ActionDescriptor returnedAd = helper.PublicFindAction(controllerContext, mockCd.Object, "someAction");
+
+ // Assert
+ Assert.Equal(expectedAd, returnedAd);
+ }
+
+ [Fact]
+ public void FindActionDoesNotMatchConstructor()
+ {
+ // FindActionMethod() shouldn't match special-named methods like type constructors.
+
+ // Arrange
+ Controller controller = new FindMethodController();
+ ControllerContext context = GetControllerContext(controller);
+ ControllerDescriptor cd = new ReflectedControllerDescriptor(typeof(FindMethodController));
+
+ ControllerActionInvokerHelper helper = new ControllerActionInvokerHelper();
+
+ // Act
+ ActionDescriptor ad = helper.PublicFindAction(context, cd, ".ctor");
+ ActionDescriptor ad2 = helper.PublicFindAction(context, cd, "FindMethodController");
+
+ // Assert
+ Assert.Null(ad);
+ Assert.Null(ad2);
+ }
+
+ [Fact]
+ public void FindActionDoesNotMatchEvent()
+ {
+ // FindActionMethod() should skip methods that aren't publicly visible.
+
+ // Arrange
+ Controller controller = new FindMethodController();
+ ControllerContext context = GetControllerContext(controller);
+ ControllerDescriptor cd = new ReflectedControllerDescriptor(typeof(FindMethodController));
+
+ ControllerActionInvokerHelper helper = new ControllerActionInvokerHelper();
+
+ // Act
+ ActionDescriptor ad = helper.PublicFindAction(context, cd, "add_Event");
+
+ // Assert
+ Assert.Null(ad);
+ }
+
+ [Fact]
+ public void FindActionDoesNotMatchInternalMethod()
+ {
+ // FindActionMethod() should skip methods that aren't publicly visible.
+
+ // Arrange
+ Controller controller = new FindMethodController();
+ ControllerContext context = GetControllerContext(controller);
+ ControllerDescriptor cd = new ReflectedControllerDescriptor(typeof(FindMethodController));
+
+ ControllerActionInvokerHelper helper = new ControllerActionInvokerHelper();
+
+ // Act
+ ActionDescriptor ad = helper.PublicFindAction(context, cd, "InternalMethod");
+
+ // Assert
+ Assert.Null(ad);
+ }
+
+ [Fact]
+ public void FindActionDoesNotMatchMethodsDefinedOnControllerType()
+ {
+ // FindActionMethod() shouldn't match methods originally defined on the Controller type, e.g. Dispose().
+
+ // Arrange
+ Controller controller = new BlankController();
+ ControllerDescriptor cd = new ReflectedControllerDescriptor(typeof(BlankController));
+ ControllerContext context = GetControllerContext(controller);
+ ControllerActionInvokerHelper helper = new ControllerActionInvokerHelper();
+ var methods = typeof(Controller).GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
+
+ // Act & Assert
+ foreach (var method in methods)
+ {
+ bool wasFound = true;
+ try
+ {
+ ActionDescriptor ad = helper.PublicFindAction(context, cd, method.Name);
+ wasFound = (ad != null);
+ }
+ finally
+ {
+ Assert.False(wasFound, "FindAction() should return false for methods defined on the Controller class: " + method);
+ }
+ }
+ }
+
+ [Fact]
+ public void FindActionDoesNotMatchMethodsDefinedOnObjectType()
+ {
+ // FindActionMethod() shouldn't match methods originally defined on the Object type, e.g. ToString().
+
+ // Arrange
+ Controller controller = new FindMethodController();
+ ControllerContext context = GetControllerContext(controller);
+ ControllerDescriptor cd = new ReflectedControllerDescriptor(typeof(FindMethodController));
+
+ ControllerActionInvokerHelper helper = new ControllerActionInvokerHelper();
+
+ // Act
+ ActionDescriptor ad = helper.PublicFindAction(context, cd, "ToString");
+
+ // Assert
+ Assert.Null(ad);
+ }
+
+ [Fact]
+ public void FindActionDoesNotMatchNonActionMethod()
+ {
+ // FindActionMethod() should respect the [NonAction] attribute.
+
+ // Arrange
+ Controller controller = new FindMethodController();
+ ControllerContext context = GetControllerContext(controller);
+ ControllerDescriptor cd = new ReflectedControllerDescriptor(typeof(FindMethodController));
+
+ ControllerActionInvokerHelper helper = new ControllerActionInvokerHelper();
+
+ // Act
+ ActionDescriptor ad = helper.PublicFindAction(context, cd, "NonActionMethod");
+
+ // Assert
+ Assert.Null(ad);
+ }
+
+ [Fact]
+ public void FindActionDoesNotMatchOverriddenNonActionMethod()
+ {
+ // FindActionMethod() should trace the method's inheritance chain looking for the [NonAction] attribute.
+
+ // Arrange
+ Controller controller = new DerivedFindMethodController();
+ ControllerContext context = GetControllerContext(controller);
+ ControllerDescriptor cd = new ReflectedControllerDescriptor(typeof(DerivedFindMethodController));
+
+ ControllerActionInvokerHelper helper = new ControllerActionInvokerHelper();
+
+ // Act
+ ActionDescriptor ad = helper.PublicFindAction(context, cd, "InternalMethod");
+
+ // Assert
+ Assert.Null(ad);
+ }
+
+ [Fact]
+ public void FindActionDoesNotMatchPrivateMethod()
+ {
+ // FindActionMethod() should skip methods that aren't publicly visible.
+
+ // Arrange
+ Controller controller = new FindMethodController();
+ ControllerContext context = GetControllerContext(controller);
+ ControllerDescriptor cd = new ReflectedControllerDescriptor(typeof(FindMethodController));
+
+ ControllerActionInvokerHelper helper = new ControllerActionInvokerHelper();
+
+ // Act
+ ActionDescriptor ad = helper.PublicFindAction(context, cd, "PrivateMethod");
+
+ // Assert
+ Assert.Null(ad);
+ }
+
+ [Fact]
+ public void FindActionDoesNotMatchProperty()
+ {
+ // FindActionMethod() shouldn't match special-named methods like property getters.
+
+ // Arrange
+ Controller controller = new FindMethodController();
+ ControllerContext context = GetControllerContext(controller);
+ ControllerDescriptor cd = new ReflectedControllerDescriptor(typeof(FindMethodController));
+
+ ControllerActionInvokerHelper helper = new ControllerActionInvokerHelper();
+
+ // Act
+ ActionDescriptor ad = helper.PublicFindAction(context, cd, "get_Property");
+
+ // Assert
+ Assert.Null(ad);
+ }
+
+ [Fact]
+ public void FindActionDoesNotMatchProtectedMethod()
+ {
+ // FindActionMethod() should skip methods that aren't publicly visible.
+
+ // Arrange
+ Controller controller = new FindMethodController();
+ ControllerContext context = GetControllerContext(controller);
+ ControllerDescriptor cd = new ReflectedControllerDescriptor(typeof(FindMethodController));
+
+ ControllerActionInvokerHelper helper = new ControllerActionInvokerHelper();
+
+ // Act
+ ActionDescriptor ad = helper.PublicFindAction(context, cd, "ProtectedMethod");
+
+ // Assert
+ Assert.Null(ad);
+ }
+
+ [Fact]
+ public void FindActionIsCaseInsensitive()
+ {
+ // Arrange
+ Controller controller = new FindMethodController();
+ ControllerContext context = GetControllerContext(controller);
+ ControllerDescriptor cd = new ReflectedControllerDescriptor(typeof(FindMethodController));
+ MethodInfo expectedMethodInfo = typeof(FindMethodController).GetMethod("ValidActionMethod");
+
+ ControllerActionInvokerHelper helper = new ControllerActionInvokerHelper();
+
+ // Act
+ ActionDescriptor ad1 = helper.PublicFindAction(context, cd, "validactionmethod");
+ ActionDescriptor ad2 = helper.PublicFindAction(context, cd, "VALIDACTIONMETHOD");
+
+ // Assert
+ ReflectedActionDescriptor rad1 = Assert.IsType<ReflectedActionDescriptor>(ad1);
+ Assert.Same(expectedMethodInfo, rad1.MethodInfo);
+ ReflectedActionDescriptor rad2 = Assert.IsType<ReflectedActionDescriptor>(ad2);
+ Assert.Same(expectedMethodInfo, rad2.MethodInfo);
+ }
+
+ [Fact]
+ public void FindActionMatchesActionMethodWithClosedGenerics()
+ {
+ // FindActionMethod() should work with generic methods as long as there are no open types.
+
+ // Arrange
+ Controller controller = new GenericFindMethodController<int>();
+ ControllerContext context = GetControllerContext(controller);
+ ControllerDescriptor cd = new ReflectedControllerDescriptor(typeof(GenericFindMethodController<int>));
+ MethodInfo expectedMethodInfo = typeof(GenericFindMethodController<int>).GetMethod("ClosedGenericMethod");
+
+ ControllerActionInvokerHelper helper = new ControllerActionInvokerHelper();
+
+ // Act
+ ActionDescriptor ad = helper.PublicFindAction(context, cd, "ClosedGenericMethod");
+
+ // Assert
+ ReflectedActionDescriptor rad = Assert.IsType<ReflectedActionDescriptor>(ad);
+ Assert.Same(expectedMethodInfo, rad.MethodInfo);
+ }
+
+ [Fact]
+ public void FindActionMatchesNewActionMethodsHidingNonActionMethods()
+ {
+ // FindActionMethod() should stop looking for [NonAction] in the method's inheritance chain when it sees
+ // that a method in a derived class hides the a method in the base class.
+
+ // Arrange
+ Controller controller = new DerivedFindMethodController();
+ ControllerContext context = GetControllerContext(controller);
+ ControllerDescriptor cd = new ReflectedControllerDescriptor(typeof(DerivedFindMethodController));
+ MethodInfo expectedMethodInfo = typeof(DerivedFindMethodController).GetMethod("DerivedIsActionMethod");
+
+ ControllerActionInvokerHelper helper = new ControllerActionInvokerHelper();
+
+ // Act
+ ActionDescriptor ad = helper.PublicFindAction(context, cd, "DerivedIsActionMethod");
+
+ // Assert
+ ReflectedActionDescriptor rad = Assert.IsType<ReflectedActionDescriptor>(ad);
+ Assert.Same(expectedMethodInfo, rad.MethodInfo);
+ }
+
+ [Fact]
+ public void GetControllerDescriptor()
+ {
+ // Arrange
+ EmptyController controller = new EmptyController();
+ ControllerContext controllerContext = GetControllerContext(controller);
+ ControllerActionInvokerHelper helper = new ControllerActionInvokerHelper();
+
+ // Act
+ ControllerDescriptor cd = helper.PublicGetControllerDescriptor(controllerContext);
+
+ // Assert
+ Assert.IsType<ReflectedControllerDescriptor>(cd);
+ Assert.Equal(typeof(EmptyController), cd.ControllerType);
+ }
+
+ [Fact]
+ public void GetFiltersSplitsFilterObjectsIntoFilterInfo()
+ {
+ // Arrange
+ IActionFilter actionFilter = new Mock<IActionFilter>().Object;
+ IResultFilter resultFilter = new Mock<IResultFilter>().Object;
+ IAuthorizationFilter authFilter = new Mock<IAuthorizationFilter>().Object;
+ IExceptionFilter exFilter = new Mock<IExceptionFilter>().Object;
+ object noneOfTheAbove = new object();
+ ControllerActionInvokerHelper invoker = new ControllerActionInvokerHelper(actionFilter, authFilter, exFilter, resultFilter, noneOfTheAbove);
+ ControllerContext context = new ControllerContext();
+ ActionDescriptor descriptor = new Mock<ActionDescriptor>().Object;
+
+ // Act
+ FilterInfo result = invoker.PublicGetFilters(context, descriptor);
+
+ // Assert
+ Assert.Same(actionFilter, result.ActionFilters.Single());
+ Assert.Same(authFilter, result.AuthorizationFilters.Single());
+ Assert.Same(exFilter, result.ExceptionFilters.Single());
+ Assert.Same(resultFilter, result.ResultFilters.Single());
+ }
+
+ [Fact]
+ public void GetParameterValueAllowsAllSubpropertiesIfBindAttributeNotSpecified()
+ {
+ // Arrange
+ CustomConverterController controller = new CustomConverterController();
+ ControllerContext controllerContext = GetControllerContext(controller);
+ ControllerActionInvokerHelper helper = new ControllerActionInvokerHelper();
+
+ ParameterInfo paramWithoutBindAttribute = typeof(CustomConverterController).GetMethod("ParameterWithoutBindAttribute").GetParameters()[0];
+ ReflectedParameterDescriptor pd = new ReflectedParameterDescriptor(paramWithoutBindAttribute, new Mock<ActionDescriptor>().Object);
+
+ // Act
+ object valueWithoutBindAttribute = helper.PublicGetParameterValue(controllerContext, pd);
+
+ // Assert
+ Assert.Equal("foo=True&bar=True", valueWithoutBindAttribute);
+ }
+
+ [Fact]
+ public void GetParameterValueResolvesConvertersInCorrectOrderOfPrecedence()
+ {
+ // Order of precedence:
+ // 1. Attributes on the parameter itself
+ // 2. Query the global converter provider
+
+ // Arrange
+ CustomConverterController controller = new CustomConverterController();
+ Dictionary<string, object> values = new Dictionary<string, object> { { "foo", "fooValue" } };
+ ControllerContext controllerContext = GetControllerContext(controller, values);
+ controller.ControllerContext = controllerContext;
+ ControllerActionInvokerHelper helper = new ControllerActionInvokerHelper();
+
+ ParameterInfo paramWithOneConverter = typeof(CustomConverterController).GetMethod("ParameterHasOneConverter").GetParameters()[0];
+ ReflectedParameterDescriptor pdOneConverter = new ReflectedParameterDescriptor(paramWithOneConverter, new Mock<ActionDescriptor>().Object);
+ ParameterInfo paramWithNoConverters = typeof(CustomConverterController).GetMethod("ParameterHasNoConverters").GetParameters()[0];
+ ReflectedParameterDescriptor pdNoConverters = new ReflectedParameterDescriptor(paramWithNoConverters, new Mock<ActionDescriptor>().Object);
+
+ // Act
+ object valueWithOneConverter = helper.PublicGetParameterValue(controllerContext, pdOneConverter);
+ object valueWithNoConverters = helper.PublicGetParameterValue(controllerContext, pdNoConverters);
+
+ // Assert
+ Assert.Equal("foo_String", valueWithOneConverter);
+ Assert.Equal("fooValue", valueWithNoConverters);
+ }
+
+ [Fact]
+ public void GetParameterValueRespectsBindAttribute()
+ {
+ // Arrange
+ CustomConverterController controller = new CustomConverterController();
+ ControllerContext controllerContext = GetControllerContext(controller);
+ ControllerActionInvokerHelper helper = new ControllerActionInvokerHelper();
+
+ ParameterInfo paramWithBindAttribute = typeof(CustomConverterController).GetMethod("ParameterHasBindAttribute").GetParameters()[0];
+ ReflectedParameterDescriptor pd = new ReflectedParameterDescriptor(paramWithBindAttribute, new Mock<ActionDescriptor>().Object);
+
+ // Act
+ object valueWithBindAttribute = helper.PublicGetParameterValue(controllerContext, pd);
+
+ // Assert
+ Assert.Equal("foo=True&bar=False", valueWithBindAttribute);
+ }
+
+ [Fact]
+ public void GetParameterValueRespectsBindAttributePrefix()
+ {
+ // Arrange
+ CustomConverterController controller = new CustomConverterController();
+ Dictionary<string, object> values = new Dictionary<string, object> { { "foo", "fooValue" }, { "bar", "barValue" } };
+ ControllerContext controllerContext = GetControllerContext(controller, values);
+ controller.ControllerContext = controllerContext;
+
+ ControllerActionInvokerHelper helper = new ControllerActionInvokerHelper();
+
+ ParameterInfo paramWithFieldPrefix = typeof(CustomConverterController).GetMethod("ParameterHasFieldPrefix").GetParameters()[0];
+ ReflectedParameterDescriptor pd = new ReflectedParameterDescriptor(paramWithFieldPrefix, new Mock<ActionDescriptor>().Object);
+
+ // Act
+ object parameterValue = helper.PublicGetParameterValue(controllerContext, pd);
+
+ // Assert
+ Assert.Equal("barValue", parameterValue);
+ }
+
+ [Fact]
+ public void GetParameterValueRespectsBindAttributePrefixOnComplexType()
+ {
+ // Arrange
+ CustomConverterController controller = new CustomConverterController();
+ Dictionary<string, object> values = new Dictionary<string, object> { { "intprop", "123" }, { "stringprop", "hello" } };
+ ControllerContext controllerContext = GetControllerContext(controller, values);
+ controller.ControllerContext = controllerContext;
+
+ ControllerActionInvokerHelper helper = new ControllerActionInvokerHelper();
+
+ ParameterInfo paramWithFieldPrefix = typeof(CustomConverterController).GetMethod("ParameterHasPrefixAndComplexType").GetParameters()[0];
+ ReflectedParameterDescriptor pd = new ReflectedParameterDescriptor(paramWithFieldPrefix, new Mock<ActionDescriptor>().Object);
+
+ // Act
+ MySimpleModel parameterValue = helper.PublicGetParameterValue(controllerContext, pd) as MySimpleModel;
+
+ // Assert
+ Assert.Null(parameterValue);
+ }
+
+ [Fact]
+ public void GetParameterValueRespectsBindAttributeNullPrefix()
+ {
+ // Arrange
+ CustomConverterController controller = new CustomConverterController();
+ Dictionary<string, object> values = new Dictionary<string, object> { { "foo", "fooValue" }, { "bar", "barValue" } };
+ ControllerContext controllerContext = GetControllerContext(controller, values);
+ controller.ControllerContext = controllerContext;
+
+ ControllerActionInvokerHelper helper = new ControllerActionInvokerHelper();
+
+ ParameterInfo paramWithFieldPrefix = typeof(CustomConverterController).GetMethod("ParameterHasNullFieldPrefix").GetParameters()[0];
+ ReflectedParameterDescriptor pd = new ReflectedParameterDescriptor(paramWithFieldPrefix, new Mock<ActionDescriptor>().Object);
+
+ // Act
+ object parameterValue = helper.PublicGetParameterValue(controllerContext, pd);
+
+ // Assert
+ Assert.Equal("fooValue", parameterValue);
+ }
+
+ [Fact]
+ public void GetParameterValueRespectsBindAttributeNullPrefixOnComplexType()
+ {
+ // Arrange
+ CustomConverterController controller = new CustomConverterController();
+ Dictionary<string, object> values = new Dictionary<string, object> { { "intprop", "123" }, { "stringprop", "hello" } };
+ ControllerContext controllerContext = GetControllerContext(controller, values);
+ controller.ControllerContext = controllerContext;
+
+ ControllerActionInvokerHelper helper = new ControllerActionInvokerHelper();
+
+ ParameterInfo paramWithFieldPrefix = typeof(CustomConverterController).GetMethod("ParameterHasNoPrefixAndComplexType").GetParameters()[0];
+ ReflectedParameterDescriptor pd = new ReflectedParameterDescriptor(paramWithFieldPrefix, new Mock<ActionDescriptor>().Object);
+
+ // Act
+ MySimpleModel parameterValue = helper.PublicGetParameterValue(controllerContext, pd) as MySimpleModel;
+
+ // Assert
+ Assert.NotNull(parameterValue);
+ Assert.Equal(123, parameterValue.IntProp);
+ Assert.Equal("hello", parameterValue.StringProp);
+ }
+
+ [Fact]
+ public void GetParameterValueRespectsBindAttributeEmptyPrefix()
+ {
+ // Arrange
+ CustomConverterController controller = new CustomConverterController();
+ Dictionary<string, object> values = new Dictionary<string, object> { { "foo", "fooValue" }, { "bar", "barValue" }, { "intprop", "123" }, { "stringprop", "hello" } };
+ ControllerContext controllerContext = GetControllerContext(controller, values);
+ controller.ControllerContext = controllerContext;
+
+ ControllerActionInvokerHelper helper = new ControllerActionInvokerHelper();
+
+ ParameterInfo paramWithFieldPrefix = typeof(CustomConverterController).GetMethod("ParameterHasEmptyFieldPrefix").GetParameters()[0];
+ ReflectedParameterDescriptor pd = new ReflectedParameterDescriptor(paramWithFieldPrefix, new Mock<ActionDescriptor>().Object);
+
+ // Act
+ MySimpleModel parameterValue = helper.PublicGetParameterValue(controllerContext, pd) as MySimpleModel;
+
+ // Assert
+ Assert.NotNull(parameterValue);
+ Assert.Equal(123, parameterValue.IntProp);
+ Assert.Equal("hello", parameterValue.StringProp);
+ }
+
+ [Fact]
+ public void GetParameterValueRespectsDefaultValueAttribute()
+ {
+ // Arrange
+ CustomConverterController controller = new CustomConverterController();
+ ControllerContext controllerContext = GetControllerContext(controller);
+ ControllerActionInvokerHelper helper = new ControllerActionInvokerHelper();
+ controller.ValueProvider = new SimpleValueProvider();
+
+ ParameterInfo paramWithDefaultValueAttribute = typeof(CustomConverterController).GetMethod("ParameterHasDefaultValueAttribute").GetParameters()[0];
+ ReflectedParameterDescriptor pd = new ReflectedParameterDescriptor(paramWithDefaultValueAttribute, new Mock<ActionDescriptor>().Object);
+
+ // Act
+ object valueWithDefaultValueAttribute = helper.PublicGetParameterValue(controllerContext, pd);
+
+ // Assert
+ Assert.Equal(42, valueWithDefaultValueAttribute);
+ }
+
+ [Fact]
+ public void GetParameterValueReturnsNullIfCannotConvertNonRequiredParameter()
+ {
+ // Arrange
+ Dictionary<string, object> dict = new Dictionary<string, object>()
+ {
+ { "id", DateTime.Now } // cannot convert DateTime to Nullable<int>
+ };
+ var controller = new ParameterTestingController();
+ ControllerContext context = GetControllerContext(controller, dict);
+ controller.ControllerContext = context;
+
+ ControllerActionInvokerHelper helper = new ControllerActionInvokerHelper();
+ MethodInfo mi = typeof(ParameterTestingController).GetMethod("TakesNullableInt");
+ ParameterInfo[] pis = mi.GetParameters();
+ ReflectedParameterDescriptor pd = new ReflectedParameterDescriptor(pis[0], new Mock<ActionDescriptor>().Object);
+
+ // Act
+ object oValue = helper.PublicGetParameterValue(context, pd);
+
+ // Assert
+ Assert.Null(oValue);
+ }
+
+ [Fact]
+ public void GetParameterValueReturnsNullIfNullableTypeValueNotFound()
+ {
+ // Arrange
+ var controller = new ParameterTestingController();
+ ControllerContext context = GetControllerContext(controller);
+ controller.ControllerContext = context;
+ controller.ValueProvider = new SimpleValueProvider();
+
+ ControllerActionInvokerHelper helper = new ControllerActionInvokerHelper();
+ MethodInfo mi = typeof(ParameterTestingController).GetMethod("TakesNullableInt");
+ ParameterInfo[] pis = mi.GetParameters();
+ ReflectedParameterDescriptor pd = new ReflectedParameterDescriptor(pis[0], new Mock<ActionDescriptor>().Object);
+
+ // Act
+ object oValue = helper.PublicGetParameterValue(context, pd);
+
+ // Assert
+ Assert.Null(oValue);
+ }
+
+ [Fact]
+ public void GetParameterValueReturnsNullIfReferenceTypeValueNotFound()
+ {
+ // Arrange
+ var controller = new ParameterTestingController();
+ ControllerContext context = GetControllerContext(controller);
+ controller.ControllerContext = context;
+ controller.ValueProvider = new SimpleValueProvider();
+
+ ControllerActionInvokerHelper helper = new ControllerActionInvokerHelper();
+ MethodInfo mi = typeof(ParameterTestingController).GetMethod("Foo");
+ ParameterInfo[] pis = mi.GetParameters();
+ ReflectedParameterDescriptor pd = new ReflectedParameterDescriptor(pis[0], new Mock<ActionDescriptor>().Object);
+
+ // Act
+ object oValue = helper.PublicGetParameterValue(context, pd);
+
+ // Assert
+ Assert.Null(oValue);
+ }
+
+ [Fact]
+ public void GetParameterValuesCallsGetParameterValue()
+ {
+ // Arrange
+ ControllerBase controller = new ParameterTestingController();
+ IDictionary<string, object> dict = new Dictionary<string, object>();
+ ControllerContext context = GetControllerContext(controller);
+ MethodInfo mi = typeof(ParameterTestingController).GetMethod("Foo");
+ ReflectedActionDescriptor ad = new ReflectedActionDescriptor(mi, "Foo", new Mock<ControllerDescriptor>().Object);
+ ParameterDescriptor[] pds = ad.GetParameters();
+
+ Mock<ControllerActionInvokerHelper> mockHelper = new Mock<ControllerActionInvokerHelper>() { CallBase = true };
+ mockHelper.Setup(h => h.PublicGetParameterValue(context, pds[0])).Returns("Myfoo").Verifiable();
+ mockHelper.Setup(h => h.PublicGetParameterValue(context, pds[1])).Returns("Mybar").Verifiable();
+ mockHelper.Setup(h => h.PublicGetParameterValue(context, pds[2])).Returns("Mybaz").Verifiable();
+ ControllerActionInvokerHelper helper = mockHelper.Object;
+
+ // Act
+ IDictionary<string, object> parameters = helper.PublicGetParameterValues(context, ad);
+
+ // Assert
+ Assert.Equal(3, parameters.Count);
+ Assert.Equal("Myfoo", parameters["foo"]);
+ Assert.Equal("Mybar", parameters["bar"]);
+ Assert.Equal("Mybaz", parameters["baz"]);
+ mockHelper.Verify();
+ }
+
+ [Fact]
+ public void GetParameterValuesReturnsEmptyDictionaryForParameterlessMethod()
+ {
+ // Arrange
+ var controller = new ParameterTestingController();
+ ControllerContext context = GetControllerContext(controller);
+ ControllerActionInvokerHelper helper = new ControllerActionInvokerHelper();
+ MethodInfo mi = typeof(ParameterTestingController).GetMethod("Parameterless");
+ ReflectedActionDescriptor ad = new ReflectedActionDescriptor(mi, "Parameterless", new Mock<ControllerDescriptor>().Object);
+
+ // Act
+ IDictionary<string, object> parameters = helper.PublicGetParameterValues(context, ad);
+
+ // Assert
+ Assert.Empty(parameters);
+ }
+
+ [Fact]
+ public void GetParameterValuesReturnsValuesForParametersInOrder()
+ {
+ // We need to hook into GetParameterValue() to make sure that GetParameterValues() is calling it.
+
+ // Arrange
+ var controller = new ParameterTestingController();
+ Dictionary<string, object> dict = new Dictionary<string, object>()
+ {
+ { "foo", "MyFoo" },
+ { "bar", "MyBar" },
+ { "baz", "MyBaz" }
+ };
+ ControllerContext context = GetControllerContext(controller, dict);
+ controller.ControllerContext = context;
+
+ ControllerActionInvokerHelper helper = new ControllerActionInvokerHelper();
+ MethodInfo mi = typeof(ParameterTestingController).GetMethod("Foo");
+ ReflectedActionDescriptor ad = new ReflectedActionDescriptor(mi, "Foo", new Mock<ControllerDescriptor>().Object);
+
+ // Act
+ IDictionary<string, object> parameters = helper.PublicGetParameterValues(context, ad);
+
+ // Assert
+ Assert.Equal(3, parameters.Count);
+ Assert.Equal("MyFoo", parameters["foo"]);
+ Assert.Equal("MyBar", parameters["bar"]);
+ Assert.Equal("MyBaz", parameters["baz"]);
+ }
+
+ [Fact]
+ public void GetParameterValueUsesControllerValueProviderAsValueProvider()
+ {
+ // Arrange
+ Dictionary<string, object> values = new Dictionary<string, object>()
+ {
+ { "foo", "fooValue" }
+ };
+
+ CustomConverterController controller = new CustomConverterController();
+ ControllerContext controllerContext = GetControllerContext(controller, values);
+ ControllerActionInvokerHelper helper = new ControllerActionInvokerHelper();
+
+ ParameterInfo parameter = typeof(CustomConverterController).GetMethod("ParameterHasNoConverters").GetParameters()[0];
+ ReflectedParameterDescriptor pd = new ReflectedParameterDescriptor(parameter, new Mock<ActionDescriptor>().Object);
+
+ // Act
+ object parameterValue = helper.PublicGetParameterValue(controllerContext, pd);
+
+ // Assert
+ Assert.Equal("fooValue", parameterValue);
+ }
+
+ [Fact]
+ public void InvokeAction()
+ {
+ // Arrange
+ ControllerBase controller = new Mock<ControllerBase>().Object;
+
+ ControllerContext context = GetControllerContext(controller);
+ ControllerDescriptor cd = new Mock<ControllerDescriptor>().Object;
+ ActionDescriptor ad = new Mock<ActionDescriptor>().Object;
+ FilterInfo filterInfo = new FilterInfo();
+
+ IDictionary<string, object> parameters = new Dictionary<string, object>();
+ MethodInfo methodInfo = typeof(object).GetMethod("ToString");
+ ActionResult actionResult = new EmptyResult();
+ ActionExecutedContext postContext = new ActionExecutedContext(context, ad, false /* canceled */, null /* exception */)
+ {
+ Result = actionResult
+ };
+ AuthorizationContext authContext = new AuthorizationContext();
+
+ Mock<ControllerActionInvokerHelper> mockHelper = new Mock<ControllerActionInvokerHelper>() { CallBase = true };
+ mockHelper.Setup(h => h.PublicGetControllerDescriptor(context)).Returns(cd).Verifiable();
+ mockHelper.Setup(h => h.PublicFindAction(context, cd, "SomeMethod")).Returns(ad).Verifiable();
+ mockHelper.Setup(h => h.PublicGetFilters(context, ad)).Returns(filterInfo).Verifiable();
+ mockHelper.Setup(h => h.PublicInvokeAuthorizationFilters(context, filterInfo.AuthorizationFilters, ad)).Returns(authContext).Verifiable();
+ mockHelper.Setup(h => h.PublicGetParameterValues(context, ad)).Returns(parameters).Verifiable();
+ mockHelper.Setup(h => h.PublicInvokeActionMethodWithFilters(context, filterInfo.ActionFilters, ad, parameters)).Returns(postContext).Verifiable();
+ mockHelper.Setup(h => h.PublicInvokeActionResultWithFilters(context, filterInfo.ResultFilters, actionResult)).Returns((ResultExecutedContext)null).Verifiable();
+ ControllerActionInvokerHelper helper = mockHelper.Object;
+
+ // Act
+ bool retVal = helper.InvokeAction(context, "SomeMethod");
+ Assert.True(retVal);
+ mockHelper.Verify();
+ }
+
+ [Fact]
+ public void InvokeActionCallsValidateRequestIfAsked()
+ {
+ // Arrange
+ ControllerBase controller = new Mock<ControllerBase>().Object;
+ controller.ValidateRequest = true;
+ bool validateInputWasCalled = false;
+
+ ControllerContext context = GetControllerContext(controller, null, validateInputCallback: () => { validateInputWasCalled = true; });
+ ControllerDescriptor cd = new Mock<ControllerDescriptor>().Object;
+ ActionDescriptor ad = new Mock<ActionDescriptor>().Object;
+ FilterInfo filterInfo = new FilterInfo();
+ AuthorizationContext authContext = new AuthorizationContext();
+
+ Mock<ControllerActionInvokerHelper> mockHelper = new Mock<ControllerActionInvokerHelper>();
+ mockHelper.CallBase = true;
+ mockHelper.Setup(h => h.PublicGetControllerDescriptor(context)).Returns(cd).Verifiable();
+ mockHelper.Setup(h => h.PublicFindAction(context, cd, "SomeMethod")).Returns(ad).Verifiable();
+ mockHelper.Setup(h => h.PublicGetFilters(context, ad)).Returns(filterInfo).Verifiable();
+ mockHelper.Setup(h => h.PublicInvokeAuthorizationFilters(context, filterInfo.AuthorizationFilters, ad)).Returns(authContext).Verifiable();
+ ControllerActionInvokerHelper helper = mockHelper.Object;
+
+ // Act
+ helper.InvokeAction(context, "SomeMethod");
+
+ // Assert
+ Assert.True(validateInputWasCalled);
+ mockHelper.Verify();
+ }
+
+ [Fact]
+ public void InvokeActionDoesNotCallValidateRequestForChildActions()
+ {
+ // Arrange
+ ControllerBase controller = new Mock<ControllerBase>().Object;
+ controller.ValidateRequest = true;
+
+ ControllerContext context = GetControllerContext(controller, null);
+ Mock.Get<ControllerContext>(context).SetupGet(c => c.IsChildAction).Returns(true);
+ ControllerDescriptor cd = new Mock<ControllerDescriptor>().Object;
+ ActionDescriptor ad = new Mock<ActionDescriptor>().Object;
+ FilterInfo filterInfo = new FilterInfo();
+ AuthorizationContext authContext = new AuthorizationContext();
+
+ Mock<ControllerActionInvokerHelper> mockHelper = new Mock<ControllerActionInvokerHelper>();
+ mockHelper.CallBase = true;
+ mockHelper.Setup(h => h.PublicGetControllerDescriptor(context)).Returns(cd).Verifiable();
+ mockHelper.Setup(h => h.PublicFindAction(context, cd, "SomeMethod")).Returns(ad).Verifiable();
+ mockHelper.Setup(h => h.PublicGetFilters(context, ad)).Returns(filterInfo).Verifiable();
+ mockHelper.Setup(h => h.PublicInvokeAuthorizationFilters(context, filterInfo.AuthorizationFilters, ad)).Returns(authContext).Verifiable();
+ ControllerActionInvokerHelper helper = mockHelper.Object;
+
+ // Act
+ helper.InvokeAction(context, "SomeMethod"); // No exception thrown
+
+ // Assert
+ mockHelper.Verify();
+ }
+
+ [Fact]
+ public void InvokeActionMethodFilterWhereContinuationThrowsExceptionAndIsHandled()
+ {
+ // Arrange
+ List<string> actions = new List<string>();
+ MethodInfo mi = typeof(object).GetMethod("ToString");
+ Dictionary<string, object> parameters = new Dictionary<string, object>();
+ Exception exception = new Exception();
+ ActionDescriptor action = new Mock<ActionDescriptor>().Object;
+
+ ActionFilterImpl filter = new ActionFilterImpl()
+ {
+ OnActionExecutingImpl = delegate(ActionExecutingContext filterContext) { actions.Add("OnActionExecuting"); },
+ OnActionExecutedImpl = delegate(ActionExecutedContext filterContext)
+ {
+ actions.Add("OnActionExecuted");
+ Assert.Same(exception, filterContext.Exception);
+ Assert.Same(action, filterContext.ActionDescriptor);
+ Assert.False(filterContext.ExceptionHandled);
+ filterContext.ExceptionHandled = true;
+ }
+ };
+ Func<ActionExecutedContext> continuation = delegate
+ {
+ actions.Add("Continuation");
+ throw exception;
+ };
+
+ ActionExecutingContext context = new ActionExecutingContext(GetControllerContext(new EmptyController()), action, parameters);
+
+ // Act
+ ActionExecutedContext result = ControllerActionInvoker.InvokeActionMethodFilter(filter, context, continuation);
+
+ // Assert
+ Assert.Equal(3, actions.Count);
+ Assert.Equal("OnActionExecuting", actions[0]);
+ Assert.Equal("Continuation", actions[1]);
+ Assert.Equal("OnActionExecuted", actions[2]);
+ Assert.Same(exception, result.Exception);
+ Assert.Same(action, result.ActionDescriptor);
+ Assert.True(result.ExceptionHandled);
+ }
+
+ [Fact]
+ public void InvokeActionMethodFilterWhereContinuationThrowsExceptionAndIsNotHandled()
+ {
+ // Arrange
+ List<string> actions = new List<string>();
+ Dictionary<string, object> parameters = new Dictionary<string, object>();
+ ActionDescriptor action = new Mock<ActionDescriptor>().Object;
+
+ ActionFilterImpl filter = new ActionFilterImpl()
+ {
+ OnActionExecutingImpl = delegate(ActionExecutingContext filterContext) { actions.Add("OnActionExecuting"); },
+ OnActionExecutedImpl = delegate(ActionExecutedContext filterContext)
+ {
+ Assert.NotNull(filterContext.Exception);
+ Assert.Equal("Some exception message.", filterContext.Exception.Message);
+ Assert.Same(action, filterContext.ActionDescriptor);
+ actions.Add("OnActionExecuted");
+ }
+ };
+ Func<ActionExecutedContext> continuation = delegate
+ {
+ actions.Add("Continuation");
+ throw new Exception("Some exception message.");
+ };
+
+ ActionExecutingContext context = new ActionExecutingContext(GetControllerContext(new EmptyController()), action, parameters);
+
+ // Act & Assert
+ Assert.Throws<Exception>(
+ delegate { ControllerActionInvoker.InvokeActionMethodFilter(filter, context, continuation); },
+ "Some exception message.");
+ Assert.Equal(3, actions.Count);
+ Assert.Equal("OnActionExecuting", actions[0]);
+ Assert.Equal("Continuation", actions[1]);
+ Assert.Equal("OnActionExecuted", actions[2]);
+ }
+
+ [Fact]
+ public void InvokeActionMethodFilterWhereContinuationThrowsThreadAbortException()
+ {
+ // Arrange
+ List<string> actions = new List<string>();
+ ActionResult actionResult = new EmptyResult();
+ ActionDescriptor action = new Mock<ActionDescriptor>().Object;
+
+ ActionFilterImpl filter = new ActionFilterImpl()
+ {
+ OnActionExecutingImpl = delegate(ActionExecutingContext filterContext) { actions.Add("OnActionExecuting"); },
+ OnActionExecutedImpl = delegate(ActionExecutedContext filterContext)
+ {
+ Thread.ResetAbort();
+ actions.Add("OnActionExecuted");
+ Assert.Null(filterContext.Exception);
+ Assert.False(filterContext.ExceptionHandled);
+ Assert.Same(action, filterContext.ActionDescriptor);
+ }
+ };
+ Func<ActionExecutedContext> continuation = delegate
+ {
+ actions.Add("Continuation");
+ Thread.CurrentThread.Abort();
+ return null;
+ };
+
+ ActionExecutingContext context = new ActionExecutingContext(new Mock<ControllerContext>().Object, action, new Dictionary<string, object>());
+
+ // Act & Assert
+ Assert.Throws<ThreadAbortException>(
+ delegate { ControllerActionInvoker.InvokeActionMethodFilter(filter, context, continuation); },
+ "Thread was being aborted.");
+ Assert.Equal(3, actions.Count);
+ Assert.Equal("OnActionExecuting", actions[0]);
+ Assert.Equal("Continuation", actions[1]);
+ Assert.Equal("OnActionExecuted", actions[2]);
+ }
+
+ [Fact]
+ public void InvokeActionMethodFilterWhereOnActionExecutingCancels()
+ {
+ // Arrange
+ bool wasCalled = false;
+ ActionDescriptor ad = new Mock<ActionDescriptor>().Object;
+ Dictionary<string, object> parameters = new Dictionary<string, object>();
+
+ ActionResult actionResult = new EmptyResult();
+ ActionDescriptor action = new Mock<ActionDescriptor>().Object;
+
+ ActionFilterImpl filter = new ActionFilterImpl()
+ {
+ OnActionExecutingImpl = delegate(ActionExecutingContext filterContext)
+ {
+ Assert.False(wasCalled);
+ wasCalled = true;
+ filterContext.Result = actionResult;
+ },
+ };
+ Func<ActionExecutedContext> continuation = delegate
+ {
+ Assert.True(false, "The continuation should not be called.");
+ return null;
+ };
+
+ ActionExecutingContext context = new ActionExecutingContext(GetControllerContext(new EmptyController()), action, parameters);
+
+ // Act
+ ActionExecutedContext result = ControllerActionInvoker.InvokeActionMethodFilter(filter, context, continuation);
+
+ // Assert
+ Assert.True(wasCalled);
+ Assert.Null(result.Exception);
+ Assert.True(result.Canceled);
+ Assert.Same(actionResult, result.Result);
+ Assert.Same(action, result.ActionDescriptor);
+ }
+
+ [Fact]
+ public void InvokeActionMethodFilterWithNormalControlFlow()
+ {
+ // Arrange
+ List<string> actions = new List<string>();
+ Dictionary<string, object> parameters = new Dictionary<string, object>();
+ ActionDescriptor action = new Mock<ActionDescriptor>().Object;
+
+ ActionExecutingContext preContext = new ActionExecutingContext(GetControllerContext(new EmptyController()), action, parameters);
+ Mock<ActionExecutedContext> mockPostContext = new Mock<ActionExecutedContext>();
+
+ ActionFilterImpl filter = new ActionFilterImpl()
+ {
+ OnActionExecutingImpl = delegate(ActionExecutingContext filterContext)
+ {
+ Assert.Same(parameters, filterContext.ActionParameters);
+ Assert.Null(filterContext.Result);
+ actions.Add("OnActionExecuting");
+ },
+ OnActionExecutedImpl = delegate(ActionExecutedContext filterContext)
+ {
+ Assert.Equal(mockPostContext.Object, filterContext);
+ actions.Add("OnActionExecuted");
+ }
+ };
+ Func<ActionExecutedContext> continuation = delegate
+ {
+ actions.Add("Continuation");
+ return mockPostContext.Object;
+ };
+
+ // Act
+ ActionExecutedContext result = ControllerActionInvoker.InvokeActionMethodFilter(filter, preContext, continuation);
+
+ // Assert
+ Assert.Equal(3, actions.Count);
+ Assert.Equal("OnActionExecuting", actions[0]);
+ Assert.Equal("Continuation", actions[1]);
+ Assert.Equal("OnActionExecuted", actions[2]);
+ Assert.Same(result, mockPostContext.Object);
+ }
+
+ [Fact]
+ public void InvokeActionInvokesExceptionFiltersAndExecutesResultIfExceptionHandled()
+ {
+ // Arrange
+ ControllerBase controller = new Mock<ControllerBase>().Object;
+
+ ControllerContext context = GetControllerContext(controller);
+ ControllerDescriptor cd = new Mock<ControllerDescriptor>().Object;
+ ActionDescriptor ad = new Mock<ActionDescriptor>().Object;
+ FilterInfo filterInfo = new FilterInfo();
+
+ Exception exception = new Exception();
+ ActionResult actionResult = new EmptyResult();
+ ExceptionContext exContext = new ExceptionContext(context, exception)
+ {
+ ExceptionHandled = true,
+ Result = actionResult
+ };
+
+ Mock<ControllerActionInvokerHelper> mockHelper = new Mock<ControllerActionInvokerHelper>() { CallBase = true };
+ mockHelper.Setup(h => h.PublicGetControllerDescriptor(context)).Returns(cd).Verifiable();
+ mockHelper.Setup(h => h.PublicFindAction(context, cd, "SomeMethod")).Returns(ad).Verifiable();
+ mockHelper.Setup(h => h.PublicGetFilters(context, ad)).Returns(filterInfo).Verifiable();
+ mockHelper.Setup(h => h.PublicInvokeAuthorizationFilters(context, filterInfo.AuthorizationFilters, ad)).Throws(exception).Verifiable();
+ mockHelper.Setup(h => h.PublicInvokeExceptionFilters(context, filterInfo.ExceptionFilters, exception)).Returns(exContext).Verifiable();
+ mockHelper.Setup(h => h.PublicInvokeActionResult(context, actionResult)).Verifiable();
+ ControllerActionInvokerHelper helper = mockHelper.Object;
+
+ // Act
+ bool retVal = helper.InvokeAction(context, "SomeMethod");
+ Assert.True(retVal);
+ mockHelper.Verify();
+ }
+
+ [Fact]
+ public void InvokeActionInvokesExceptionFiltersAndRethrowsExceptionIfNotHandled()
+ {
+ // Arrange
+ ControllerBase controller = new Mock<ControllerBase>().Object;
+
+ ControllerContext context = GetControllerContext(controller);
+ ControllerDescriptor cd = new Mock<ControllerDescriptor>().Object;
+ ActionDescriptor ad = new Mock<ActionDescriptor>().Object;
+ FilterInfo filterInfo = new FilterInfo();
+
+ Exception exception = new Exception();
+ ExceptionContext exContext = new ExceptionContext(context, exception);
+
+ Mock<ControllerActionInvokerHelper> mockHelper = new Mock<ControllerActionInvokerHelper>() { CallBase = true };
+ mockHelper.Setup(h => h.PublicGetControllerDescriptor(context)).Returns(cd).Verifiable();
+ mockHelper.Setup(h => h.PublicFindAction(context, cd, "SomeMethod")).Returns(ad).Verifiable();
+ mockHelper.Setup(h => h.PublicGetFilters(context, ad)).Returns(filterInfo).Verifiable();
+ mockHelper.Setup(h => h.PublicInvokeAuthorizationFilters(context, filterInfo.AuthorizationFilters, ad)).Throws(exception).Verifiable();
+ mockHelper.Setup(h => h.PublicInvokeExceptionFilters(context, filterInfo.ExceptionFilters, exception)).Returns(exContext).Verifiable();
+ mockHelper.Setup(h => h.PublicInvokeActionResult(context, It.IsAny<ActionResult>())).Callback(delegate { Assert.True(false, "InvokeActionResult() shouldn't be called if the exception was unhandled by filters."); });
+ ControllerActionInvokerHelper helper = mockHelper.Object;
+
+ // Act
+ Exception thrownException = Assert.Throws<Exception>(
+ delegate { helper.InvokeAction(context, "SomeMethod"); });
+
+ // Assert
+ Assert.Same(exception, thrownException);
+ mockHelper.Verify();
+ }
+
+ [Fact]
+ public void InvokeActionInvokesResultIfAuthorizationFilterReturnsResult()
+ {
+ // Arrange
+ ControllerBase controller = new Mock<ControllerBase>().Object;
+
+ ControllerContext context = GetControllerContext(controller);
+ ControllerDescriptor cd = new Mock<ControllerDescriptor>().Object;
+ ActionDescriptor ad = new Mock<ActionDescriptor>().Object;
+ FilterInfo filterInfo = new FilterInfo();
+
+ ActionResult actionResult = new EmptyResult();
+ ActionExecutedContext postContext = new ActionExecutedContext(context, ad, false /* canceled */, null /* exception */)
+ {
+ Result = actionResult
+ };
+ AuthorizationContext authContext = new AuthorizationContext() { Result = actionResult };
+
+ Mock<ControllerActionInvokerHelper> mockHelper = new Mock<ControllerActionInvokerHelper>() { CallBase = true };
+ mockHelper.Setup(h => h.PublicGetControllerDescriptor(context)).Returns(cd).Verifiable();
+ mockHelper.Setup(h => h.PublicFindAction(context, cd, "SomeMethod")).Returns(ad).Verifiable();
+ mockHelper.Setup(h => h.PublicGetFilters(context, ad)).Returns(filterInfo).Verifiable();
+ mockHelper.Setup(h => h.PublicInvokeAuthorizationFilters(context, filterInfo.AuthorizationFilters, ad)).Returns(authContext).Verifiable();
+ mockHelper.Setup(h => h.PublicInvokeActionResult(context, actionResult)).Verifiable();
+ ControllerActionInvokerHelper helper = mockHelper.Object;
+
+ // Act
+ bool retVal = helper.InvokeAction(context, "SomeMethod");
+ Assert.True(retVal);
+ mockHelper.Verify();
+ }
+
+ [Fact]
+ public void InvokeActionMethod()
+ {
+ // Arrange
+ EmptyController controller = new EmptyController();
+ ControllerContext controllerContext = GetControllerContext(controller);
+ Dictionary<string, object> parameters = new Dictionary<string, object>();
+
+ ActionResult expectedResult = new Mock<ActionResult>().Object;
+
+ Mock<ActionDescriptor> mockAd = new Mock<ActionDescriptor>();
+ mockAd.Setup(ad => ad.Execute(controllerContext, parameters)).Returns("hello world");
+
+ Mock<ControllerActionInvokerHelper> mockHelper = new Mock<ControllerActionInvokerHelper>() { CallBase = true };
+ mockHelper.Setup(h => h.PublicCreateActionResult(controllerContext, mockAd.Object, "hello world")).Returns(expectedResult);
+ ControllerActionInvokerHelper helper = mockHelper.Object;
+
+ // Act
+ ActionResult returnedResult = helper.PublicInvokeActionMethod(controllerContext, mockAd.Object, parameters);
+
+ // Assert
+ Assert.Same(expectedResult, returnedResult);
+ }
+
+ [Fact]
+ public void InvokeActionMethodWithFiltersOrdersFiltersCorrectly()
+ {
+ // Arrange
+ List<string> actions = new List<string>();
+ Dictionary<string, object> parameters = new Dictionary<string, object>();
+ ActionResult actionResult = new EmptyResult();
+
+ ActionFilterImpl filter1 = new ActionFilterImpl()
+ {
+ OnActionExecutingImpl = delegate(ActionExecutingContext filterContext) { actions.Add("OnActionExecuting1"); },
+ OnActionExecutedImpl = delegate(ActionExecutedContext filterContext) { actions.Add("OnActionExecuted1"); }
+ };
+ ActionFilterImpl filter2 = new ActionFilterImpl()
+ {
+ OnActionExecutingImpl = delegate(ActionExecutingContext filterContext) { actions.Add("OnActionExecuting2"); },
+ OnActionExecutedImpl = delegate(ActionExecutedContext filterContext) { actions.Add("OnActionExecuted2"); }
+ };
+ Func<ActionResult> continuation = delegate
+ {
+ actions.Add("Continuation");
+ return new EmptyResult();
+ };
+ ControllerBase controller = new ContinuationController(continuation);
+ ControllerContext context = GetControllerContext(controller);
+ ActionDescriptor actionDescriptor = new ReflectedActionDescriptor(ContinuationController.GoMethod, "someName", new Mock<ControllerDescriptor>().Object);
+ ControllerActionInvokerHelper helper = new ControllerActionInvokerHelper();
+ List<IActionFilter> filters = new List<IActionFilter>() { filter1, filter2 };
+
+ // Act
+ helper.PublicInvokeActionMethodWithFilters(context, filters, actionDescriptor, parameters);
+
+ // Assert
+ Assert.Equal(5, actions.Count);
+ Assert.Equal("OnActionExecuting1", actions[0]);
+ Assert.Equal("OnActionExecuting2", actions[1]);
+ Assert.Equal("Continuation", actions[2]);
+ Assert.Equal("OnActionExecuted2", actions[3]);
+ Assert.Equal("OnActionExecuted1", actions[4]);
+ }
+
+ [Fact]
+ public void InvokeActionMethodWithFiltersPassesArgumentsCorrectly()
+ {
+ // Arrange
+ bool wasCalled = false;
+ MethodInfo mi = ContinuationController.GoMethod;
+ Dictionary<string, object> parameters = new Dictionary<string, object>();
+ ActionResult actionResult = new EmptyResult();
+ ActionFilterImpl filter = new ActionFilterImpl()
+ {
+ OnActionExecutingImpl = delegate(ActionExecutingContext filterContext)
+ {
+ Assert.Same(parameters, filterContext.ActionParameters);
+ Assert.False(wasCalled);
+ wasCalled = true;
+ filterContext.Result = actionResult;
+ }
+ };
+ Func<ActionResult> continuation = delegate
+ {
+ Assert.True(false, "Continuation should not be called.");
+ return null;
+ };
+ ControllerBase controller = new ContinuationController(continuation);
+ ControllerContext context = GetControllerContext(controller);
+ ControllerActionInvokerHelper helper = new ControllerActionInvokerHelper();
+ ActionDescriptor actionDescriptor = new ReflectedActionDescriptor(ContinuationController.GoMethod, "someName", new Mock<ControllerDescriptor>().Object);
+ List<IActionFilter> filters = new List<IActionFilter>() { filter };
+
+ // Act
+ ActionExecutedContext result = helper.PublicInvokeActionMethodWithFilters(context, filters, actionDescriptor, parameters);
+
+ // Assert
+ Assert.True(wasCalled);
+ Assert.Null(result.Exception);
+ Assert.False(result.ExceptionHandled);
+ Assert.Same(actionResult, result.Result);
+ Assert.Same(actionDescriptor, result.ActionDescriptor);
+ }
+
+ [Fact]
+ public void InvokeActionPropagatesThreadAbortException()
+ {
+ // Arrange
+ ControllerBase controller = new Mock<ControllerBase>().Object;
+
+ ControllerContext context = GetControllerContext(controller);
+ ControllerDescriptor cd = new Mock<ControllerDescriptor>().Object;
+ ActionDescriptor ad = new Mock<ActionDescriptor>().Object;
+ FilterInfo filterInfo = new FilterInfo();
+
+ ActionResult actionResult = new EmptyResult();
+ ActionExecutedContext postContext = new ActionExecutedContext(context, ad, false /* canceled */, null /* exception */)
+ {
+ Result = actionResult
+ };
+ AuthorizationContext authContext = new AuthorizationContext() { Result = actionResult };
+
+ Mock<ControllerActionInvokerHelper> mockHelper = new Mock<ControllerActionInvokerHelper>() { CallBase = true };
+ mockHelper.Setup(h => h.PublicGetControllerDescriptor(context)).Returns(cd).Verifiable();
+ mockHelper.Setup(h => h.PublicFindAction(context, cd, "SomeMethod")).Returns(ad).Verifiable();
+ mockHelper.Setup(h => h.PublicGetFilters(context, ad)).Returns(filterInfo).Verifiable();
+ mockHelper
+ .Setup(h => h.PublicInvokeAuthorizationFilters(context, filterInfo.AuthorizationFilters, ad))
+ .Returns(
+ delegate(ControllerContext cc, IList<IAuthorizationFilter> f, ActionDescriptor a)
+ {
+ Thread.CurrentThread.Abort();
+ return null;
+ });
+ ControllerActionInvokerHelper helper = mockHelper.Object;
+
+ bool wasAborted = false;
+
+ // Act
+ try
+ {
+ helper.InvokeAction(context, "SomeMethod");
+ }
+ catch (ThreadAbortException)
+ {
+ wasAborted = true;
+ Thread.ResetAbort();
+ }
+
+ // Assert
+ Assert.True(wasAborted);
+ mockHelper.Verify();
+ }
+
+ [Fact]
+ public void InvokeActionResultWithFiltersPassesSameContextObjectToInnerFilters()
+ {
+ // Arrange
+ ControllerBase controller = new Mock<ControllerBase>().Object;
+ ControllerContext context = GetControllerContext(controller);
+
+ ResultExecutingContext storedContext = null;
+ ActionResult result = new EmptyResult();
+ List<IResultFilter> filters = new List<IResultFilter>()
+ {
+ new ActionFilterImpl()
+ {
+ OnResultExecutingImpl = delegate(ResultExecutingContext ctx) { storedContext = ctx; },
+ OnResultExecutedImpl = delegate { }
+ },
+ new ActionFilterImpl()
+ {
+ OnResultExecutingImpl = delegate(ResultExecutingContext ctx) { Assert.Same(storedContext, ctx); },
+ OnResultExecutedImpl = delegate { }
+ },
+ };
+ ControllerActionInvokerHelper helper = new ControllerActionInvokerHelper();
+
+ // Act
+ ResultExecutedContext postContext = helper.PublicInvokeActionResultWithFilters(context, filters, result);
+
+ // Assert
+ Assert.Same(result, postContext.Result);
+ }
+
+ [Fact]
+ public void InvokeActionReturnsFalseIfMethodNotFound()
+ {
+ // Arrange
+ var controller = new BlankController();
+ ControllerContext context = GetControllerContext(controller);
+ ControllerActionInvoker invoker = new ControllerActionInvoker();
+
+ // Act
+ bool retVal = invoker.InvokeAction(context, "foo");
+
+ // Assert
+ Assert.False(retVal);
+ }
+
+ [Fact]
+ public void InvokeActionThrowsIfControllerContextIsNull()
+ {
+ // Arrange
+ ControllerActionInvoker invoker = new ControllerActionInvoker();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { invoker.InvokeAction(null, "actionName"); }, "controllerContext");
+ }
+
+ [Fact]
+ public void InvokeActionWithEmptyActionNameThrows()
+ {
+ // Arrange
+ var controller = new BasicMethodInvokeController();
+ ControllerContext context = GetControllerContext(controller);
+ ControllerActionInvoker invoker = new ControllerActionInvoker();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { invoker.InvokeAction(context, String.Empty); },
+ "actionName");
+ }
+
+ [Fact]
+ public void InvokeActionWithNullActionNameThrows()
+ {
+ // Arrange
+ var controller = new BasicMethodInvokeController();
+ ControllerContext context = GetControllerContext(controller);
+ ControllerActionInvoker invoker = new ControllerActionInvoker();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { invoker.InvokeAction(context, null /* actionName */); },
+ "actionName");
+ }
+
+ [Fact]
+ public void InvokeActionWithResultExceptionInvokesExceptionFiltersAndExecutesResultIfExceptionHandled()
+ {
+ // Arrange
+ ControllerBase controller = new Mock<ControllerBase>().Object;
+
+ ControllerContext context = GetControllerContext(controller);
+ ControllerDescriptor cd = new Mock<ControllerDescriptor>().Object;
+ ActionDescriptor ad = new Mock<ActionDescriptor>().Object;
+ IDictionary<string, object> parameters = new Dictionary<string, object>();
+ FilterInfo filterInfo = new FilterInfo();
+ AuthorizationContext authContext = new AuthorizationContext();
+
+ Exception exception = new Exception();
+ ActionResult actionResult = new EmptyResult();
+ ActionExecutedContext postContext = new ActionExecutedContext(context, ad, false /* canceled */, null /* exception */)
+ {
+ Result = actionResult
+ };
+ ExceptionContext exContext = new ExceptionContext(context, exception)
+ {
+ ExceptionHandled = true,
+ Result = actionResult
+ };
+
+ Mock<ControllerActionInvokerHelper> mockHelper = new Mock<ControllerActionInvokerHelper>() { CallBase = true };
+ mockHelper.Setup(h => h.PublicGetControllerDescriptor(context)).Returns(cd).Verifiable();
+ mockHelper.Setup(h => h.PublicFindAction(context, cd, "SomeMethod")).Returns(ad).Verifiable();
+ mockHelper.Setup(h => h.PublicGetFilters(context, ad)).Returns(filterInfo).Verifiable();
+ mockHelper.Setup(h => h.PublicInvokeAuthorizationFilters(context, filterInfo.AuthorizationFilters, ad)).Returns(authContext).Verifiable();
+ mockHelper.Setup(h => h.PublicGetParameterValues(context, ad)).Returns(parameters).Verifiable();
+ mockHelper.Setup(h => h.PublicInvokeActionMethodWithFilters(context, filterInfo.ActionFilters, ad, parameters)).Returns(postContext).Verifiable();
+ mockHelper.Setup(h => h.PublicInvokeActionResultWithFilters(context, filterInfo.ResultFilters, actionResult)).Throws(exception).Verifiable();
+ mockHelper.Setup(h => h.PublicInvokeExceptionFilters(context, filterInfo.ExceptionFilters, exception)).Returns(exContext).Verifiable();
+ mockHelper.Setup(h => h.PublicInvokeActionResult(context, actionResult)).Verifiable();
+ ControllerActionInvokerHelper helper = mockHelper.Object;
+
+ // Act
+ bool retVal = helper.InvokeAction(context, "SomeMethod");
+ Assert.True(retVal, "InvokeAction() should return True on success.");
+ mockHelper.Verify();
+ }
+
+ [Fact]
+ public void InvokeAuthorizationFilters()
+ {
+ // Arrange
+ ControllerBase controller = new Mock<ControllerBase>().Object;
+ ActionDescriptor ad = new Mock<ActionDescriptor>().Object;
+ ControllerContext controllerContext = GetControllerContext(controller);
+ ControllerActionInvokerHelper helper = new ControllerActionInvokerHelper();
+
+ List<AuthorizationFilterHelper> callQueue = new List<AuthorizationFilterHelper>();
+ AuthorizationFilterHelper filter1 = new AuthorizationFilterHelper(callQueue);
+ AuthorizationFilterHelper filter2 = new AuthorizationFilterHelper(callQueue);
+ IAuthorizationFilter[] filters = new IAuthorizationFilter[] { filter1, filter2 };
+
+ // Act
+ AuthorizationContext postContext = helper.PublicInvokeAuthorizationFilters(controllerContext, filters, ad);
+
+ // Assert
+ Assert.Equal(ad, postContext.ActionDescriptor);
+ Assert.Equal(2, callQueue.Count);
+ Assert.Same(filter1, callQueue[0]);
+ Assert.Same(filter2, callQueue[1]);
+ }
+
+ [Fact]
+ public void InvokeAuthorizationFiltersStopsExecutingIfResultProvided()
+ {
+ // Arrange
+ ControllerBase controller = new Mock<ControllerBase>().Object;
+ ActionDescriptor ad = new Mock<ActionDescriptor>().Object;
+ ControllerContext controllerContext = GetControllerContext(controller);
+ ControllerActionInvokerHelper helper = new ControllerActionInvokerHelper();
+ ActionResult result = new EmptyResult();
+
+ List<AuthorizationFilterHelper> callQueue = new List<AuthorizationFilterHelper>();
+ AuthorizationFilterHelper filter1 = new AuthorizationFilterHelper(callQueue) { ShortCircuitResult = result };
+ AuthorizationFilterHelper filter2 = new AuthorizationFilterHelper(callQueue);
+ IAuthorizationFilter[] filters = new IAuthorizationFilter[] { filter1, filter2 };
+
+ // Act
+ AuthorizationContext postContext = helper.PublicInvokeAuthorizationFilters(controllerContext, filters, ad);
+
+ // Assert
+ Assert.Equal(ad, postContext.ActionDescriptor);
+ Assert.Same(result, postContext.Result);
+ Assert.Single(callQueue);
+ Assert.Same(filter1, callQueue[0]);
+ }
+
+ [Fact]
+ public void InvokeExceptionFilters()
+ {
+ // Arrange
+ ControllerBase controller = new Mock<ControllerBase>().Object;
+ Exception exception = new Exception();
+ ControllerContext controllerContext = GetControllerContext(controller);
+ ControllerActionInvokerHelper helper = new ControllerActionInvokerHelper();
+
+ List<ExceptionFilterHelper> callQueue = new List<ExceptionFilterHelper>();
+ ExceptionFilterHelper filter1 = new ExceptionFilterHelper(callQueue);
+ ExceptionFilterHelper filter2 = new ExceptionFilterHelper(callQueue);
+ IExceptionFilter[] filters = new IExceptionFilter[] { filter1, filter2 };
+
+ // Act
+ ExceptionContext postContext = helper.PublicInvokeExceptionFilters(controllerContext, filters, exception);
+
+ // Assert
+ Assert.Same(exception, postContext.Exception);
+ Assert.False(postContext.ExceptionHandled);
+ Assert.Same(filter1.ContextPassed, filter2.ContextPassed);
+ Assert.Equal(2, callQueue.Count);
+ Assert.Same(filter2, callQueue[0]); // Exception filters are executed in reverse order
+ Assert.Same(filter1, callQueue[1]);
+ }
+
+ [Fact]
+ public void InvokeExceptionFiltersContinuesExecutingIfExceptionHandled()
+ {
+ // Arrange
+ ControllerBase controller = new Mock<ControllerBase>().Object;
+ Exception exception = new Exception();
+ ControllerContext controllerContext = GetControllerContext(controller);
+ ControllerActionInvokerHelper helper = new ControllerActionInvokerHelper();
+
+ List<ExceptionFilterHelper> callQueue = new List<ExceptionFilterHelper>();
+ ExceptionFilterHelper filter1 = new ExceptionFilterHelper(callQueue) { ShouldHandleException = true };
+ ExceptionFilterHelper filter2 = new ExceptionFilterHelper(callQueue);
+ IExceptionFilter[] filters = new IExceptionFilter[] { filter1, filter2 };
+
+ // Act
+ ExceptionContext postContext = helper.PublicInvokeExceptionFilters(controllerContext, filters, exception);
+
+ // Assert
+ Assert.Same(exception, postContext.Exception);
+ Assert.True(postContext.ExceptionHandled);
+ Assert.Same(filter1.ContextPassed, filter2.ContextPassed);
+ Assert.Equal(2, callQueue.Count);
+ Assert.Same(filter2, callQueue[0]); // Exception filters are executed in reverse order
+ Assert.Same(filter1, callQueue[1]);
+ }
+
+ [Fact]
+ public void InvokeResultFiltersOrdersFiltersCorrectly()
+ {
+ // Arrange
+ List<string> actions = new List<string>();
+ ActionFilterImpl filter1 = new ActionFilterImpl()
+ {
+ OnResultExecutingImpl = delegate(ResultExecutingContext filterContext) { actions.Add("OnResultExecuting1"); },
+ OnResultExecutedImpl = delegate(ResultExecutedContext filterContext) { actions.Add("OnResultExecuted1"); }
+ };
+ ActionFilterImpl filter2 = new ActionFilterImpl()
+ {
+ OnResultExecutingImpl = delegate(ResultExecutingContext filterContext) { actions.Add("OnResultExecuting2"); },
+ OnResultExecutedImpl = delegate(ResultExecutedContext filterContext) { actions.Add("OnResultExecuted2"); }
+ };
+ Action continuation = delegate { actions.Add("Continuation"); };
+ ActionResult actionResult = new ContinuationResult(continuation);
+ ControllerBase controller = new Mock<ControllerBase>().Object;
+ ControllerContext context = GetControllerContext(controller);
+ ControllerActionInvokerHelper helper = new ControllerActionInvokerHelper();
+ List<IResultFilter> filters = new List<IResultFilter>() { filter1, filter2 };
+
+ // Act
+ helper.PublicInvokeActionResultWithFilters(context, filters, actionResult);
+
+ // Assert
+ Assert.Equal(5, actions.Count);
+ Assert.Equal("OnResultExecuting1", actions[0]);
+ Assert.Equal("OnResultExecuting2", actions[1]);
+ Assert.Equal("Continuation", actions[2]);
+ Assert.Equal("OnResultExecuted2", actions[3]);
+ Assert.Equal("OnResultExecuted1", actions[4]);
+ }
+
+ [Fact]
+ public void InvokeResultFiltersPassesArgumentsCorrectly()
+ {
+ // Arrange
+ bool wasCalled = false;
+ Action continuation = delegate { Assert.True(false, "Continuation should not be called."); };
+ ActionResult actionResult = new ContinuationResult(continuation);
+ ControllerBase controller = new Mock<ControllerBase>().Object;
+ ControllerContext context = GetControllerContext(controller);
+ ControllerActionInvokerHelper helper = new ControllerActionInvokerHelper();
+ ActionFilterImpl filter = new ActionFilterImpl()
+ {
+ OnResultExecutingImpl = delegate(ResultExecutingContext filterContext)
+ {
+ Assert.Same(actionResult, filterContext.Result);
+ Assert.False(wasCalled);
+ wasCalled = true;
+ filterContext.Cancel = true;
+ }
+ };
+
+ List<IResultFilter> filters = new List<IResultFilter>() { filter };
+
+ // Act
+ ResultExecutedContext result = helper.PublicInvokeActionResultWithFilters(context, filters, actionResult);
+
+ // Assert
+ Assert.True(wasCalled);
+ Assert.Null(result.Exception);
+ Assert.False(result.ExceptionHandled);
+ Assert.Same(actionResult, result.Result);
+ }
+
+ [Fact]
+ public void InvokeResultFilterWhereContinuationThrowsExceptionAndIsHandled()
+ {
+ // Arrange
+ List<string> actions = new List<string>();
+ ActionResult actionResult = new EmptyResult();
+ Exception exception = new Exception();
+ ActionFilterImpl filter = new ActionFilterImpl()
+ {
+ OnResultExecutingImpl = delegate(ResultExecutingContext filterContext) { actions.Add("OnResultExecuting"); },
+ OnResultExecutedImpl = delegate(ResultExecutedContext filterContext)
+ {
+ actions.Add("OnResultExecuted");
+ Assert.Same(actionResult, filterContext.Result);
+ Assert.Same(exception, filterContext.Exception);
+ Assert.False(filterContext.ExceptionHandled);
+ filterContext.ExceptionHandled = true;
+ }
+ };
+ Func<ResultExecutedContext> continuation = delegate
+ {
+ actions.Add("Continuation");
+ throw exception;
+ };
+
+ Mock<ResultExecutingContext> mockResultExecutingContext = new Mock<ResultExecutingContext>() { DefaultValue = DefaultValue.Mock };
+ mockResultExecutingContext.Setup(c => c.Result).Returns(actionResult);
+
+ // Act
+ ResultExecutedContext result = ControllerActionInvoker.InvokeActionResultFilter(filter, mockResultExecutingContext.Object, continuation);
+
+ // Assert
+ Assert.Equal(3, actions.Count);
+ Assert.Equal("OnResultExecuting", actions[0]);
+ Assert.Equal("Continuation", actions[1]);
+ Assert.Equal("OnResultExecuted", actions[2]);
+ Assert.Same(exception, result.Exception);
+ Assert.True(result.ExceptionHandled);
+ Assert.Same(actionResult, result.Result);
+ }
+
+ [Fact]
+ public void InvokeResultFilterWhereContinuationThrowsExceptionAndIsNotHandled()
+ {
+ // Arrange
+ List<string> actions = new List<string>();
+ ActionFilterImpl filter = new ActionFilterImpl()
+ {
+ OnResultExecutingImpl = delegate(ResultExecutingContext filterContext) { actions.Add("OnResultExecuting"); },
+ OnResultExecutedImpl = delegate(ResultExecutedContext filterContext) { actions.Add("OnResultExecuted"); }
+ };
+ Func<ResultExecutedContext> continuation = delegate
+ {
+ actions.Add("Continuation");
+ throw new Exception("Some exception message.");
+ };
+
+ // Act & Assert
+ Assert.Throws<Exception>(
+ delegate { ControllerActionInvoker.InvokeActionResultFilter(filter, new Mock<ResultExecutingContext>() { DefaultValue = DefaultValue.Mock }.Object, continuation); },
+ "Some exception message.");
+ Assert.Equal(3, actions.Count);
+ Assert.Equal("OnResultExecuting", actions[0]);
+ Assert.Equal("Continuation", actions[1]);
+ Assert.Equal("OnResultExecuted", actions[2]);
+ }
+
+ [Fact]
+ public void InvokeResultFilterWhereContinuationThrowsThreadAbortException()
+ {
+ // Arrange
+ List<string> actions = new List<string>();
+ ActionResult actionResult = new EmptyResult();
+
+ Mock<ResultExecutingContext> mockPreContext = new Mock<ResultExecutingContext>() { DefaultValue = DefaultValue.Mock };
+ mockPreContext.Setup(c => c.Result).Returns(actionResult);
+
+ ActionFilterImpl filter = new ActionFilterImpl()
+ {
+ OnResultExecutingImpl = delegate(ResultExecutingContext filterContext) { actions.Add("OnResultExecuting"); },
+ OnResultExecutedImpl = delegate(ResultExecutedContext filterContext)
+ {
+ Thread.ResetAbort();
+ actions.Add("OnResultExecuted");
+ Assert.Same(actionResult, filterContext.Result);
+ Assert.Null(filterContext.Exception);
+ Assert.False(filterContext.ExceptionHandled);
+ }
+ };
+ Func<ResultExecutedContext> continuation = delegate
+ {
+ actions.Add("Continuation");
+ Thread.CurrentThread.Abort();
+ return null;
+ };
+
+ // Act & Assert
+ Assert.Throws<ThreadAbortException>(
+ delegate { ControllerActionInvoker.InvokeActionResultFilter(filter, mockPreContext.Object, continuation); },
+ "Thread was being aborted.");
+ Assert.Equal(3, actions.Count);
+ Assert.Equal("OnResultExecuting", actions[0]);
+ Assert.Equal("Continuation", actions[1]);
+ Assert.Equal("OnResultExecuted", actions[2]);
+ }
+
+ [Fact]
+ public void InvokeResultFilterWhereOnResultExecutingCancels()
+ {
+ // Arrange
+ bool wasCalled = false;
+ MethodInfo mi = typeof(object).GetMethod("ToString");
+ object[] paramValues = new object[0];
+ ActionResult actionResult = new EmptyResult();
+ ActionFilterImpl filter = new ActionFilterImpl()
+ {
+ OnResultExecutingImpl = delegate(ResultExecutingContext filterContext)
+ {
+ Assert.False(wasCalled);
+ wasCalled = true;
+ filterContext.Cancel = true;
+ },
+ };
+ Func<ResultExecutedContext> continuation = delegate
+ {
+ Assert.True(false, "The continuation should not be called.");
+ return null;
+ };
+
+ Mock<ResultExecutingContext> mockResultExecutingContext = new Mock<ResultExecutingContext>() { DefaultValue = DefaultValue.Mock };
+ mockResultExecutingContext.Setup(c => c.Result).Returns(actionResult);
+
+ // Act
+ ResultExecutedContext result = ControllerActionInvoker.InvokeActionResultFilter(filter, mockResultExecutingContext.Object, continuation);
+
+ // Assert
+ Assert.True(wasCalled);
+ Assert.Null(result.Exception);
+ Assert.True(result.Canceled);
+ Assert.Same(actionResult, result.Result);
+ }
+
+ [Fact]
+ public void InvokeResultFilterWithNormalControlFlow()
+ {
+ // Arrange
+ List<string> actions = new List<string>();
+ ActionResult actionResult = new EmptyResult();
+
+ Mock<ResultExecutedContext> mockPostContext = new Mock<ResultExecutedContext>();
+ mockPostContext.Setup(c => c.Result).Returns(actionResult);
+
+ ActionFilterImpl filter = new ActionFilterImpl()
+ {
+ OnResultExecutingImpl = delegate(ResultExecutingContext filterContext)
+ {
+ Assert.Same(actionResult, filterContext.Result);
+ Assert.False(filterContext.Cancel);
+ actions.Add("OnResultExecuting");
+ },
+ OnResultExecutedImpl = delegate(ResultExecutedContext filterContext)
+ {
+ Assert.Equal(mockPostContext.Object, filterContext);
+ actions.Add("OnResultExecuted");
+ }
+ };
+ Func<ResultExecutedContext> continuation = delegate
+ {
+ actions.Add("Continuation");
+ return mockPostContext.Object;
+ };
+
+ Mock<ResultExecutingContext> mockResultExecutingContext = new Mock<ResultExecutingContext>();
+ mockResultExecutingContext.Setup(c => c.Result).Returns(actionResult);
+
+ // Act
+ ResultExecutedContext result = ControllerActionInvoker.InvokeActionResultFilter(filter, mockResultExecutingContext.Object, continuation);
+
+ // Assert
+ Assert.Equal(3, actions.Count);
+ Assert.Equal("OnResultExecuting", actions[0]);
+ Assert.Equal("Continuation", actions[1]);
+ Assert.Equal("OnResultExecuted", actions[2]);
+ Assert.Same(result, mockPostContext.Object);
+ }
+
+ [Fact]
+ public void InvokeMethodCallsOverriddenCreateActionResult()
+ {
+ // Arrange
+ CustomResultInvokerController controller = new CustomResultInvokerController();
+ ControllerContext context = GetControllerContext(controller);
+ CustomResultInvoker helper = new CustomResultInvoker();
+ MethodInfo mi = typeof(CustomResultInvokerController).GetMethod("ReturnCustomResult");
+ ReflectedActionDescriptor ad = new ReflectedActionDescriptor(mi, "ReturnCustomResult", new Mock<ControllerDescriptor>().Object);
+ IDictionary<string, object> parameters = new Dictionary<string, object>();
+
+ // Act
+ ActionResult actionResult = helper.PublicInvokeActionMethod(context, ad, parameters);
+
+ // Assert (arg got passed to method + back correctly)
+ CustomResult customResult = Assert.IsType<CustomResult>(actionResult);
+ Assert.Equal("abc123", customResult.ReturnValue);
+ }
+
+ private static ControllerContext GetControllerContext(ControllerBase controller)
+ {
+ return GetControllerContext(controller, null);
+ }
+
+ private static ControllerContext GetControllerContext(ControllerBase controller, IDictionary<string, object> values, Action validateInputCallback = null)
+ {
+ SimpleValueProvider valueProvider = new SimpleValueProvider();
+ controller.ValueProvider = valueProvider;
+ if (values != null)
+ {
+ foreach (var entry in values)
+ {
+ valueProvider[entry.Key] = entry.Value;
+ }
+ }
+
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>() { DefaultValue = DefaultValue.Mock };
+
+ mockControllerContext.Setup(c => c.HttpContext.Request.ValidateInput()).Callback(() =>
+ {
+ if (!controller.ValidateRequest)
+ {
+ Assert.True(false, "ValidateRequest() should not be called if the controller opted out.");
+ }
+ if (validateInputCallback != null)
+ {
+ // signal to caller that ValidateInput was called
+ validateInputCallback();
+ }
+ });
+
+ mockControllerContext.Setup(c => c.HttpContext.Session).Returns((HttpSessionStateBase)null);
+ mockControllerContext.Setup(c => c.Controller).Returns(controller);
+ return mockControllerContext.Object;
+ }
+
+ private class EmptyActionFilterAttribute : ActionFilterAttribute
+ {
+ }
+
+ private abstract class KeyedFilterAttribute : FilterAttribute
+ {
+ public string Key { get; set; }
+ }
+
+ private class KeyedAuthorizationFilterAttribute : KeyedFilterAttribute, IAuthorizationFilter
+ {
+ public void OnAuthorization(AuthorizationContext filterContext)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
+ private class KeyedActionFilterAttribute : KeyedFilterAttribute, IActionFilter, IResultFilter
+ {
+ public void OnActionExecuting(ActionExecutingContext filterContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void OnActionExecuted(ActionExecutedContext filterContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void OnResultExecuting(ResultExecutingContext filterContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void OnResultExecuted(ResultExecutedContext filterContext)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ private class ActionFilterImpl : IActionFilter, IResultFilter
+ {
+ public Action<ActionExecutingContext> OnActionExecutingImpl { get; set; }
+
+ public void OnActionExecuting(ActionExecutingContext filterContext)
+ {
+ OnActionExecutingImpl(filterContext);
+ }
+
+ public Action<ActionExecutedContext> OnActionExecutedImpl { get; set; }
+
+ public void OnActionExecuted(ActionExecutedContext filterContext)
+ {
+ OnActionExecutedImpl(filterContext);
+ }
+
+ public Action<ResultExecutingContext> OnResultExecutingImpl { get; set; }
+
+ public void OnResultExecuting(ResultExecutingContext filterContext)
+ {
+ OnResultExecutingImpl(filterContext);
+ }
+
+ public Action<ResultExecutedContext> OnResultExecutedImpl { get; set; }
+
+ public void OnResultExecuted(ResultExecutedContext filterContext)
+ {
+ OnResultExecutedImpl(filterContext);
+ }
+ }
+
+ [KeyedActionFilter(Key = "BaseClass", Order = 0)]
+ [KeyedAuthorizationFilter(Key = "BaseClass", Order = 0)]
+ private class GetMemberChainController : Controller
+ {
+ [KeyedActionFilter(Key = "BaseMethod", Order = 0)]
+ [KeyedAuthorizationFilter(Key = "BaseMethod", Order = 0)]
+ public virtual void SomeVirtual()
+ {
+ }
+ }
+
+ [KeyedActionFilter(Key = "DerivedClass", Order = 1)]
+ private class GetMemberChainDerivedController : GetMemberChainController
+ {
+ }
+
+ [KeyedActionFilter(Key = "SubderivedClass", Order = 2)]
+ private class GetMemberChainSubderivedController : GetMemberChainDerivedController
+ {
+ [KeyedActionFilter(Key = "SubderivedMethod", Order = 2)]
+ public override void SomeVirtual()
+ {
+ }
+ }
+
+ // This controller serves only to test vanilla method invocation - nothing exciting here
+ private class BasicMethodInvokeController : Controller
+ {
+ public ActionResult ReturnsRenderView(object viewItem)
+ {
+ return View("ReturnsRenderView", viewItem);
+ }
+ }
+
+ private class BlankController : Controller
+ {
+ }
+
+ private sealed class CustomResult : ActionResult
+ {
+ public object ReturnValue { get; set; }
+
+ public override void ExecuteResult(ControllerContext context)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ private sealed class CustomResultInvokerController : Controller
+ {
+ public object ReturnCustomResult()
+ {
+ return "abc123";
+ }
+ }
+
+ private sealed class CustomResultInvoker : ControllerActionInvokerHelper
+ {
+ protected override ActionResult CreateActionResult(ControllerContext controllerContext, ActionDescriptor actionDescriptor, object actionReturnValue)
+ {
+ return new CustomResult
+ {
+ ReturnValue = actionReturnValue
+ };
+ }
+ }
+
+ private class ContinuationController : Controller
+ {
+ private Func<ActionResult> _continuation;
+
+ public ContinuationController(Func<ActionResult> continuation)
+ {
+ _continuation = continuation;
+ }
+
+ public ActionResult Go()
+ {
+ return _continuation();
+ }
+
+ public static MethodInfo GoMethod
+ {
+ get { return typeof(ContinuationController).GetMethod("Go"); }
+ }
+ }
+
+ private class ContinuationResult : ActionResult
+ {
+ private Action _continuation;
+
+ public ContinuationResult(Action continuation)
+ {
+ _continuation = continuation;
+ }
+
+ public override void ExecuteResult(ControllerContext context)
+ {
+ _continuation();
+ }
+ }
+
+ private class EmptyController : Controller
+ {
+ }
+
+ // This controller serves to test the default action method matching mechanism
+ private class FindMethodController : Controller
+ {
+ public ActionResult ValidActionMethod()
+ {
+ return null;
+ }
+
+ [NonAction]
+ public virtual ActionResult NonActionMethod()
+ {
+ return null;
+ }
+
+ [NonAction]
+ public ActionResult DerivedIsActionMethod()
+ {
+ return null;
+ }
+
+ public ActionResult MethodOverloaded()
+ {
+ return null;
+ }
+
+ public ActionResult MethodOverloaded(string s)
+ {
+ return null;
+ }
+
+ public void WrongReturnType()
+ {
+ }
+
+ protected ActionResult ProtectedMethod()
+ {
+ return null;
+ }
+
+ private ActionResult PrivateMethod()
+ {
+ return null;
+ }
+
+ internal ActionResult InternalMethod()
+ {
+ return null;
+ }
+
+ public override string ToString()
+ {
+ // originally defined on Object
+ return base.ToString();
+ }
+
+ public ActionResult Property
+ {
+ get { return null; }
+ }
+
+#pragma warning disable 0067
+ // CS0067: Event declared but never used. We use reflection to access this member.
+ public event EventHandler Event;
+#pragma warning restore 0067
+ }
+
+ private class DerivedFindMethodController : FindMethodController
+ {
+ public override ActionResult NonActionMethod()
+ {
+ return base.NonActionMethod();
+ }
+
+ // FindActionMethod() should accept this as a valid method since [NonAction] doesn't appear
+ // in its inheritance chain.
+ public new ActionResult DerivedIsActionMethod()
+ {
+ return base.DerivedIsActionMethod();
+ }
+ }
+
+ // Similar to FindMethodController, but tests generics support specifically
+ private class GenericFindMethodController<T> : Controller
+ {
+ public ActionResult ClosedGenericMethod(T t)
+ {
+ return null;
+ }
+
+ public ActionResult OpenGenericMethod<U>(U t)
+ {
+ return null;
+ }
+ }
+
+ // Allows for testing parameter conversions, etc.
+ private class ParameterTestingController : Controller
+ {
+ public ParameterTestingController()
+ {
+ Values = new Dictionary<string, object>();
+ }
+
+ public IDictionary<string, object> Values { get; private set; }
+
+ public void Foo(string foo, string bar, string baz)
+ {
+ Values["foo"] = foo;
+ Values["bar"] = bar;
+ Values["baz"] = baz;
+ }
+
+ public void HasOutParam(out string foo)
+ {
+ foo = null;
+ }
+
+ public void HasRefParam(ref string foo)
+ {
+ }
+
+ public void Parameterless()
+ {
+ }
+
+ public void TakesInt(int id)
+ {
+ Values["id"] = id;
+ }
+
+ public ActionResult TakesNullableInt(int? id)
+ {
+ Values["id"] = id;
+ return null;
+ }
+
+ public void TakesString(string id)
+ {
+ }
+
+ public void TakesDateTime(DateTime id)
+ {
+ }
+ }
+
+ // Provides access to the protected members of ControllerActionInvoker
+ public class ControllerActionInvokerHelper : ControllerActionInvoker
+ {
+ public ControllerActionInvokerHelper()
+ {
+ // set instance caches to prevent modifying global test application state
+ DescriptorCache = new ControllerDescriptorCache();
+ }
+
+ public ControllerActionInvokerHelper(params object[] filters)
+ : base(filters)
+ {
+ // set instance caches to prevent modifying global test application state
+ DescriptorCache = new ControllerDescriptorCache();
+ }
+
+ public virtual ActionResult PublicCreateActionResult(ControllerContext controllerContext, ActionDescriptor actionDescriptor, object actionReturnValue)
+ {
+ return base.CreateActionResult(controllerContext, actionDescriptor, actionReturnValue);
+ }
+
+ protected override ActionResult CreateActionResult(ControllerContext controllerContext, ActionDescriptor actionDescriptor, object actionReturnValue)
+ {
+ return PublicCreateActionResult(controllerContext, actionDescriptor, actionReturnValue);
+ }
+
+ public virtual ActionDescriptor PublicFindAction(ControllerContext controllerContext, ControllerDescriptor controllerDescriptor, string actionName)
+ {
+ return base.FindAction(controllerContext, controllerDescriptor, actionName);
+ }
+
+ protected override ActionDescriptor FindAction(ControllerContext controllerContext, ControllerDescriptor controllerDescriptor, string actionName)
+ {
+ return PublicFindAction(controllerContext, controllerDescriptor, actionName);
+ }
+
+ public virtual ControllerDescriptor PublicGetControllerDescriptor(ControllerContext controllerContext)
+ {
+ return base.GetControllerDescriptor(controllerContext);
+ }
+
+ protected override ControllerDescriptor GetControllerDescriptor(ControllerContext controllerContext)
+ {
+ return PublicGetControllerDescriptor(controllerContext);
+ }
+
+ public virtual FilterInfo PublicGetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
+ {
+ return base.GetFilters(controllerContext, actionDescriptor);
+ }
+
+ protected override FilterInfo GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
+ {
+ return PublicGetFilters(controllerContext, actionDescriptor);
+ }
+
+ public virtual object PublicGetParameterValue(ControllerContext controllerContext, ParameterDescriptor parameterDescriptor)
+ {
+ return base.GetParameterValue(controllerContext, parameterDescriptor);
+ }
+
+ protected override object GetParameterValue(ControllerContext controllerContext, ParameterDescriptor parameterDescriptor)
+ {
+ return PublicGetParameterValue(controllerContext, parameterDescriptor);
+ }
+
+ public virtual IDictionary<string, object> PublicGetParameterValues(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
+ {
+ return base.GetParameterValues(controllerContext, actionDescriptor);
+ }
+
+ protected override IDictionary<string, object> GetParameterValues(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
+ {
+ return PublicGetParameterValues(controllerContext, actionDescriptor);
+ }
+
+ public virtual ActionResult PublicInvokeActionMethod(ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary<string, object> parameters)
+ {
+ return base.InvokeActionMethod(controllerContext, actionDescriptor, parameters);
+ }
+
+ protected override ActionResult InvokeActionMethod(ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary<string, object> parameters)
+ {
+ return PublicInvokeActionMethod(controllerContext, actionDescriptor, parameters);
+ }
+
+ public virtual ActionExecutedContext PublicInvokeActionMethodWithFilters(ControllerContext controllerContext, IList<IActionFilter> filters, ActionDescriptor actionDescriptor, IDictionary<string, object> parameters)
+ {
+ return base.InvokeActionMethodWithFilters(controllerContext, filters, actionDescriptor, parameters);
+ }
+
+ protected override ActionExecutedContext InvokeActionMethodWithFilters(ControllerContext controllerContext, IList<IActionFilter> filters, ActionDescriptor actionDescriptor, IDictionary<string, object> parameters)
+ {
+ return PublicInvokeActionMethodWithFilters(controllerContext, filters, actionDescriptor, parameters);
+ }
+
+ public virtual void PublicInvokeActionResult(ControllerContext controllerContext, ActionResult actionResult)
+ {
+ base.InvokeActionResult(controllerContext, actionResult);
+ }
+
+ protected override void InvokeActionResult(ControllerContext controllerContext, ActionResult actionResult)
+ {
+ PublicInvokeActionResult(controllerContext, actionResult);
+ }
+
+ public virtual ResultExecutedContext PublicInvokeActionResultWithFilters(ControllerContext controllerContext, IList<IResultFilter> filters, ActionResult actionResult)
+ {
+ return base.InvokeActionResultWithFilters(controllerContext, filters, actionResult);
+ }
+
+ protected override ResultExecutedContext InvokeActionResultWithFilters(ControllerContext controllerContext, IList<IResultFilter> filters, ActionResult actionResult)
+ {
+ return PublicInvokeActionResultWithFilters(controllerContext, filters, actionResult);
+ }
+
+ public virtual AuthorizationContext PublicInvokeAuthorizationFilters(ControllerContext controllerContext, IList<IAuthorizationFilter> filters, ActionDescriptor actionDescriptor)
+ {
+ return base.InvokeAuthorizationFilters(controllerContext, filters, actionDescriptor);
+ }
+
+ protected override AuthorizationContext InvokeAuthorizationFilters(ControllerContext controllerContext, IList<IAuthorizationFilter> filters, ActionDescriptor actionDescriptor)
+ {
+ return PublicInvokeAuthorizationFilters(controllerContext, filters, actionDescriptor);
+ }
+
+ public virtual ExceptionContext PublicInvokeExceptionFilters(ControllerContext controllerContext, IList<IExceptionFilter> filters, Exception exception)
+ {
+ return base.InvokeExceptionFilters(controllerContext, filters, exception);
+ }
+
+ protected override ExceptionContext InvokeExceptionFilters(ControllerContext controllerContext, IList<IExceptionFilter> filters, Exception exception)
+ {
+ return PublicInvokeExceptionFilters(controllerContext, filters, exception);
+ }
+ }
+
+ public class AuthorizationFilterHelper : IAuthorizationFilter
+ {
+ private IList<AuthorizationFilterHelper> _callQueue;
+ public ActionResult ShortCircuitResult;
+
+ public AuthorizationFilterHelper(IList<AuthorizationFilterHelper> callQueue)
+ {
+ _callQueue = callQueue;
+ }
+
+ public void OnAuthorization(AuthorizationContext filterContext)
+ {
+ _callQueue.Add(this);
+ if (ShortCircuitResult != null)
+ {
+ filterContext.Result = ShortCircuitResult;
+ }
+ }
+ }
+
+ public class ExceptionFilterHelper : IExceptionFilter
+ {
+ private IList<ExceptionFilterHelper> _callQueue;
+ public bool ShouldHandleException;
+ public ExceptionContext ContextPassed;
+
+ public ExceptionFilterHelper(IList<ExceptionFilterHelper> callQueue)
+ {
+ _callQueue = callQueue;
+ }
+
+ public void OnException(ExceptionContext filterContext)
+ {
+ _callQueue.Add(this);
+ if (ShouldHandleException)
+ {
+ filterContext.ExceptionHandled = true;
+ }
+ ContextPassed = filterContext;
+ }
+ }
+
+ private class CustomConverterController : Controller
+ {
+ public void ParameterWithoutBindAttribute([PredicateReflector] string someParam)
+ {
+ }
+
+ public void ParameterHasBindAttribute([Bind(Include = "foo"), PredicateReflector] string someParam)
+ {
+ }
+
+ public void ParameterHasDefaultValueAttribute([DefaultValue(42)] int foo)
+ {
+ }
+
+ public void ParameterHasFieldPrefix([Bind(Prefix = "bar")] string foo)
+ {
+ }
+
+ public void ParameterHasNullFieldPrefix([Bind(Include = "whatever")] string foo)
+ {
+ }
+
+ public void ParameterHasEmptyFieldPrefix([Bind(Prefix = "")] MySimpleModel foo)
+ {
+ }
+
+ public void ParameterHasNoPrefixAndComplexType(MySimpleModel foo)
+ {
+ }
+
+ public void ParameterHasPrefixAndComplexType([Bind(Prefix = "badprefix")] MySimpleModel foo)
+ {
+ }
+
+ public void ParameterHasNoConverters(string foo)
+ {
+ }
+
+ public void ParameterHasOneConverter([MyCustomConverter] string foo)
+ {
+ }
+
+ public void ParameterHasTwoConverters([MyCustomConverter, MyCustomConverter] string foo)
+ {
+ }
+ }
+
+ public class MySimpleModel
+ {
+ public int IntProp { get; set; }
+ public string StringProp { get; set; }
+ }
+
+ [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true, Inherited = false)]
+ private class PredicateReflectorAttribute : CustomModelBinderAttribute
+ {
+ public override IModelBinder GetBinder()
+ {
+ return new MyConverter();
+ }
+
+ private class MyConverter : IModelBinder
+ {
+ public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
+ {
+ string s = String.Format("foo={0}&bar={1}", bindingContext.PropertyFilter("foo"), bindingContext.PropertyFilter("bar"));
+ return s;
+ }
+ }
+ }
+
+ [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true, Inherited = false)]
+ private class MyCustomConverterAttribute : CustomModelBinderAttribute
+ {
+ public override IModelBinder GetBinder()
+ {
+ return new MyConverter();
+ }
+
+ private class MyConverter : IModelBinder
+ {
+ public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
+ {
+ string s = bindingContext.ModelName + "_" + bindingContext.ModelType.Name;
+ return s;
+ }
+ }
+ }
+
+
+ // helper class for making sure that we're performing culture-invariant string conversions
+ public class CultureReflector : IFormattable
+ {
+ string IFormattable.ToString(string format, IFormatProvider formatProvider)
+ {
+ CultureInfo cInfo = (CultureInfo)formatProvider;
+ return cInfo.ThreeLetterISOLanguageName;
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ControllerBaseTest.cs b/test/System.Web.Mvc.Test/Test/ControllerBaseTest.cs
new file mode 100644
index 00000000..98ac2df7
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ControllerBaseTest.cs
@@ -0,0 +1,234 @@
+using System.Linq;
+using System.Web.Routing;
+using System.Web.TestUtil;
+using Microsoft.Web.UnitTestUtil;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ControllerBaseTest
+ {
+ [Fact]
+ public void ExecuteCallsControllerBaseExecute()
+ {
+ // Arrange
+ RequestContext requestContext = new RequestContext(HttpContextHelpers.GetMockHttpContext().Object, new RouteData());
+
+ Mock<ControllerBaseHelper> mockController = new Mock<ControllerBaseHelper>() { CallBase = true };
+ mockController.Setup(c => c.PublicInitialize(requestContext)).Verifiable();
+ mockController.Setup(c => c.PublicExecuteCore()).Verifiable();
+ IController controller = mockController.Object;
+
+ // Act
+ controller.Execute(requestContext);
+
+ // Assert
+ mockController.Verify();
+ }
+
+ [Fact]
+ public void ExecuteThrowsIfCalledTwice()
+ {
+ // Arrange
+ EmptyControllerBase controller = new EmptyControllerBase();
+ RequestContext requestContext = new RequestContext(HttpContextHelpers.GetMockHttpContext().Object, new RouteData());
+
+ // Act
+ ((IController)controller).Execute(requestContext); // first call
+ Assert.Throws<InvalidOperationException>(
+ delegate
+ {
+ ((IController)controller).Execute(requestContext); // second call
+ },
+ @"A single instance of controller 'System.Web.Mvc.Test.ControllerBaseTest+EmptyControllerBase' cannot be used to handle multiple requests. If a custom controller factory is in use, make sure that it creates a new instance of the controller for each request.");
+
+ // Assert
+ Assert.Equal(1, controller.NumTimesExecuteCoreCalled);
+ }
+
+ [Fact]
+ public void ExecuteThrowsIfRequestContextIsNull()
+ {
+ // Arrange
+ IController controller = new ControllerBaseHelper();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { controller.Execute(null); }, "requestContext");
+ }
+
+ [Fact]
+ public void ExecuteThrowsIfRequestContextHttpContextIsNull()
+ {
+ //Arrange
+ IController controller = new ControllerBaseHelper();
+
+ //Act & Assert
+ Assert.Throws<ArgumentException>(
+ delegate { controller.Execute(new Mock<RequestContext>().Object); }, "Cannot execute Controller with a null HttpContext.\r\nParameter name: requestContext");
+ }
+
+ [Fact]
+ public void InitializeSetsControllerContext()
+ {
+ // Arrange
+ ControllerBaseHelper helper = new ControllerBaseHelper();
+ RequestContext requestContext = new RequestContext(new Mock<HttpContextBase>().Object, new RouteData());
+
+ // Act
+ helper.PublicInitialize(requestContext);
+
+ // Assert
+ Assert.Same(requestContext.HttpContext, helper.ControllerContext.HttpContext);
+ Assert.Same(requestContext.RouteData, helper.ControllerContext.RouteData);
+ Assert.Same(helper, helper.ControllerContext.Controller);
+ }
+
+ [Fact]
+ public void TempDataProperty()
+ {
+ // Arrange
+ ControllerBase controller = new ControllerBaseHelper();
+
+ // Act & Assert
+ MemberHelper.TestPropertyWithDefaultInstance(controller, "TempData", new TempDataDictionary());
+ }
+
+ [Fact]
+ public void TempDataReturnsParentTempDataWhenInChildRequest()
+ {
+ // Arrange
+ TempDataDictionary tempData = new TempDataDictionary();
+ ViewContext viewContext = new ViewContext { TempData = tempData };
+ RouteData routeData = new RouteData();
+ routeData.DataTokens[ControllerContext.ParentActionViewContextToken] = viewContext;
+ RequestContext requestContext = new RequestContext(new Mock<HttpContextBase>().Object, routeData);
+ ControllerBaseHelper controller = new ControllerBaseHelper();
+ controller.PublicInitialize(requestContext);
+
+ // Act
+ TempDataDictionary result = controller.TempData;
+
+ // Assert
+ Assert.Same(result, tempData);
+ }
+
+ [Fact]
+ public void ValidateRequestProperty()
+ {
+ // Arrange
+ ControllerBase controller = new ControllerBaseHelper();
+
+ // Act & assert
+ MemberHelper.TestBooleanProperty(controller, "ValidateRequest", true /* initialValue */, false /* testDefaultValue */);
+ }
+
+ [Fact]
+ public void ValueProviderProperty()
+ {
+ // Arrange
+ ControllerBase controller = new ControllerBaseHelper();
+ IValueProvider valueProvider = new SimpleValueProvider();
+
+ // Act & assert
+ ValueProviderFactory[] originalFactories = ValueProviderFactories.Factories.ToArray();
+ try
+ {
+ ValueProviderFactories.Factories.Clear();
+ MemberHelper.TestPropertyWithDefaultInstance(controller, "ValueProvider", valueProvider);
+ }
+ finally
+ {
+ foreach (ValueProviderFactory factory in originalFactories)
+ {
+ ValueProviderFactories.Factories.Add(factory);
+ }
+ }
+ }
+
+ [Fact]
+ public void ViewDataProperty()
+ {
+ // Arrange
+ ControllerBase controller = new ControllerBaseHelper();
+
+ // Act & Assert
+ MemberHelper.TestPropertyWithDefaultInstance(controller, "ViewData", new ViewDataDictionary());
+ }
+
+ [Fact]
+ public void ViewBagProperty_ReflectsViewData()
+ {
+ // Arrange
+ ControllerBase controller = new ControllerBaseHelper();
+ controller.ViewData["A"] = 1;
+
+ // Act & Assert
+ Assert.NotNull(controller.ViewBag);
+ Assert.Equal(1, controller.ViewBag.A);
+ }
+
+ [Fact]
+ public void ViewBagProperty_ReflectsNewViewDataInstance()
+ {
+ // Arrange
+ ControllerBase controller = new ControllerBaseHelper();
+ controller.ViewData["A"] = 1;
+ controller.ViewData = new ViewDataDictionary() { { "A", "bar" } };
+
+ // Act & Assert
+ Assert.Equal("bar", controller.ViewBag.A);
+ }
+
+ [Fact]
+ public void ViewBag_PropagatesChangesToViewData()
+ {
+ // Arrange
+ ControllerBase controller = new ControllerBaseHelper();
+ controller.ViewData["A"] = 1;
+
+ // Act
+ controller.ViewBag.A = "foo";
+ controller.ViewBag.B = 2;
+
+ // Assert
+ Assert.Equal("foo", controller.ViewData["A"]);
+ Assert.Equal(2, controller.ViewData["B"]);
+ }
+
+ public class ControllerBaseHelper : ControllerBase
+ {
+ protected override void Initialize(RequestContext requestContext)
+ {
+ PublicInitialize(requestContext);
+ }
+
+ public virtual void PublicInitialize(RequestContext requestContext)
+ {
+ base.Initialize(requestContext);
+ }
+
+ protected override void ExecuteCore()
+ {
+ PublicExecuteCore();
+ }
+
+ public virtual void PublicExecuteCore()
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ private class EmptyControllerBase : ControllerBase
+ {
+ public int NumTimesExecuteCoreCalled = 0;
+
+ protected override void ExecuteCore()
+ {
+ NumTimesExecuteCoreCalled++;
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ControllerBuilderTest.cs b/test/System.Web.Mvc.Test/Test/ControllerBuilderTest.cs
new file mode 100644
index 00000000..f3c41197
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ControllerBuilderTest.cs
@@ -0,0 +1,274 @@
+using System.Web.Routing;
+using System.Web.SessionState;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ControllerBuilderTest
+ {
+ [Fact]
+ public void ControllerBuilderReturnsDefaultControllerBuilderByDefault()
+ {
+ // Arrange
+ ControllerBuilder cb = new ControllerBuilder();
+
+ // Act
+ IControllerFactory cf = cb.GetControllerFactory();
+
+ // Assert
+ Assert.IsType<DefaultControllerFactory>(cf);
+ }
+
+ [Fact]
+ public void CreateControllerWithFactoryThatCannotBeCreatedThrows()
+ {
+ // Arrange
+ ControllerBuilder cb = new ControllerBuilder();
+ cb.SetControllerFactory(typeof(ControllerFactoryThrowsFromConstructor));
+
+ // Act
+ Assert.Throws<InvalidOperationException>(
+ delegate
+ {
+ RequestContext reqContext = new RequestContext(new Mock<HttpContextBase>().Object, new RouteData());
+ reqContext.RouteData.Values["controller"] = "foo";
+ MvcHandlerWithNoVersionHeader handler = new MvcHandlerWithNoVersionHeader(reqContext)
+ {
+ ControllerBuilder = cb
+ };
+ handler.ProcessRequest(reqContext.HttpContext);
+ },
+ "An error occurred when trying to create the IControllerFactory 'System.Web.Mvc.Test.ControllerBuilderTest+ControllerFactoryThrowsFromConstructor'. Make sure that the controller factory has a public parameterless constructor.");
+ }
+
+ [Fact]
+ public void CreateControllerWithFactoryThatReturnsNullThrows()
+ {
+ // Arrange
+ ControllerBuilder cb = new ControllerBuilder();
+ cb.SetControllerFactory(typeof(ControllerFactoryReturnsNull));
+
+ // Act
+ Assert.Throws<InvalidOperationException>(
+ delegate
+ {
+ RequestContext reqContext = new RequestContext(new Mock<HttpContextBase>().Object, new RouteData());
+ reqContext.RouteData.Values["controller"] = "boo";
+ MvcHandlerWithNoVersionHeader handler = new MvcHandlerWithNoVersionHeader(reqContext)
+ {
+ ControllerBuilder = cb
+ };
+ handler.ProcessRequest(reqContext.HttpContext);
+ },
+ "The IControllerFactory 'System.Web.Mvc.Test.ControllerBuilderTest+ControllerFactoryReturnsNull' did not return a controller for the name 'boo'.");
+ }
+
+ [Fact]
+ public void CreateControllerWithFactoryThatThrowsDoesNothingSpecial()
+ {
+ // Arrange
+ ControllerBuilder cb = new ControllerBuilder();
+ cb.SetControllerFactory(typeof(ControllerFactoryThrows));
+
+ // Act
+ Assert.Throws<Exception>(
+ delegate
+ {
+ RequestContext reqContext = new RequestContext(new Mock<HttpContextBase>().Object, new RouteData());
+ reqContext.RouteData.Values["controller"] = "foo";
+ MvcHandlerWithNoVersionHeader handler = new MvcHandlerWithNoVersionHeader(reqContext)
+ {
+ ControllerBuilder = cb
+ };
+ handler.ProcessRequest(reqContext.HttpContext);
+ },
+ "ControllerFactoryThrows");
+ }
+
+ [Fact]
+ public void CreateControllerWithFactoryInstanceReturnsInstance()
+ {
+ // Arrange
+ ControllerBuilder cb = new ControllerBuilder();
+ DefaultControllerFactory factory = new DefaultControllerFactory();
+ cb.SetControllerFactory(factory);
+
+ // Act
+ IControllerFactory cf = cb.GetControllerFactory();
+
+ // Assert
+ Assert.Same(factory, cf);
+ }
+
+ [Fact]
+ public void CreateControllerWithFactoryTypeReturnsValidType()
+ {
+ // Arrange
+ ControllerBuilder cb = new ControllerBuilder();
+ cb.SetControllerFactory(typeof(MockControllerFactory));
+
+ // Act
+ IControllerFactory cf = cb.GetControllerFactory();
+
+ // Assert
+ Assert.IsType<MockControllerFactory>(cf);
+ }
+
+ [Fact]
+ public void SetControllerFactoryInstanceWithNullThrows()
+ {
+ ControllerBuilder cb = new ControllerBuilder();
+ Assert.ThrowsArgumentNull(
+ delegate { cb.SetControllerFactory((IControllerFactory)null); },
+ "controllerFactory");
+ }
+
+ [Fact]
+ public void SetControllerFactoryTypeWithNullThrows()
+ {
+ ControllerBuilder cb = new ControllerBuilder();
+ Assert.ThrowsArgumentNull(
+ delegate { cb.SetControllerFactory((Type)null); },
+ "controllerFactoryType");
+ }
+
+ [Fact]
+ public void SetControllerFactoryTypeWithNonFactoryTypeThrows()
+ {
+ ControllerBuilder cb = new ControllerBuilder();
+ Assert.Throws<ArgumentException>(
+ delegate { cb.SetControllerFactory(typeof(int)); },
+ "The controller factory type 'System.Int32' must implement the IControllerFactory interface.\r\nParameter name: controllerFactoryType");
+ }
+
+ [Fact]
+ public void DefaultControllerFactoryIsDefaultControllerFactory()
+ {
+ // Arrange
+ ControllerBuilder builder = new ControllerBuilder();
+
+ // Act
+ IControllerFactory returnedControllerFactory = builder.GetControllerFactory();
+
+ //Assert
+ Assert.Equal(typeof(DefaultControllerFactory), returnedControllerFactory.GetType());
+ }
+
+ [Fact]
+ public void SettingControllerFactoryReturnsSetFactory()
+ {
+ // Arrange
+ ControllerBuilder builder = new ControllerBuilder();
+ Mock<IControllerFactory> setFactory = new Mock<IControllerFactory>();
+
+ // Act
+ builder.SetControllerFactory(setFactory.Object);
+
+ // Assert
+ Assert.Same(setFactory.Object, builder.GetControllerFactory());
+ }
+
+ [Fact]
+ public void ControllerBuilderGetControllerFactoryDelegatesToResolver()
+ {
+ //Arrange
+ Mock<IControllerFactory> factory = new Mock<IControllerFactory>();
+ Resolver<IControllerFactory> resolver = new Resolver<IControllerFactory> { Current = factory.Object };
+ ControllerBuilder builder = new ControllerBuilder(resolver);
+
+ //Act
+ IControllerFactory result = builder.GetControllerFactory();
+
+ //Assert
+ Assert.Same(factory.Object, result);
+ }
+
+ public class ControllerFactoryThrowsFromConstructor : IControllerFactory
+ {
+ public ControllerFactoryThrowsFromConstructor()
+ {
+ throw new Exception("ControllerFactoryThrowsFromConstructor");
+ }
+
+ public IController CreateController(RequestContext context, string controllerName)
+ {
+ return null;
+ }
+
+ public SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName)
+ {
+ return SessionStateBehavior.Default;
+ }
+
+ public void ReleaseController(IController controller)
+ {
+ }
+ }
+
+ public class ControllerFactoryReturnsNull : IControllerFactory
+ {
+ public IController CreateController(RequestContext context, string controllerName)
+ {
+ return null;
+ }
+
+ public SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName)
+ {
+ return SessionStateBehavior.Default;
+ }
+
+ public void ReleaseController(IController controller)
+ {
+ }
+ }
+
+ public class ControllerFactoryThrows : IControllerFactory
+ {
+ public IController CreateController(RequestContext context, string controllerName)
+ {
+ throw new Exception("ControllerFactoryThrows");
+ }
+
+ public SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName)
+ {
+ return SessionStateBehavior.Default;
+ }
+
+ public void ReleaseController(IController controller)
+ {
+ }
+ }
+
+ public class MockControllerFactory : IControllerFactory
+ {
+ public IController CreateController(RequestContext context, string controllerName)
+ {
+ throw new NotImplementedException();
+ }
+
+ public SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName)
+ {
+ return SessionStateBehavior.Default;
+ }
+
+ public void ReleaseController(IController controller)
+ {
+ }
+ }
+
+ private sealed class MvcHandlerWithNoVersionHeader : MvcHandler
+ {
+ public MvcHandlerWithNoVersionHeader(RequestContext requestContext)
+ : base(requestContext)
+ {
+ }
+
+ protected internal override void AddVersionHeader(HttpContextBase httpContext)
+ {
+ // Don't try to set the version header for the unit tests
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ControllerContextTest.cs b/test/System.Web.Mvc.Test/Test/ControllerContextTest.cs
new file mode 100644
index 00000000..5ce8ea0c
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ControllerContextTest.cs
@@ -0,0 +1,296 @@
+using System.Collections;
+using System.Web.Routing;
+using System.Web.TestUtil;
+using System.Web.WebPages;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ControllerContextTest
+ {
+ [Fact]
+ public void ConstructorThrowsIfControllerIsNull()
+ {
+ // Arrange
+ RequestContext requestContext = new RequestContext(new Mock<HttpContextBase>().Object, new RouteData());
+ Controller controller = null;
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new ControllerContext(requestContext, controller); }, "controller");
+ }
+
+ [Fact]
+ public void ConstructorThrowsIfRequestContextIsNull()
+ {
+ // Arrange
+ RequestContext requestContext = null;
+ Controller controller = new Mock<Controller>().Object;
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new ControllerContext(requestContext, controller); }, "requestContext");
+ }
+
+ [Fact]
+ public void ConstructorWithHttpContextAndRouteData()
+ {
+ // Arrange
+ HttpContextBase httpContext = new Mock<HttpContextBase>().Object;
+ RouteData routeData = new RouteData();
+ Controller controller = new Mock<Controller>().Object;
+
+ // Act
+ ControllerContext controllerContext = new ControllerContext(httpContext, routeData, controller);
+
+ // Assert
+ Assert.Equal(httpContext, controllerContext.HttpContext);
+ Assert.Equal(routeData, controllerContext.RouteData);
+ Assert.Equal(controller, controllerContext.Controller);
+ }
+
+ [Fact]
+ public void ControllerProperty()
+ {
+ // Arrange
+ HttpContextBase httpContext = new Mock<HttpContextBase>().Object;
+ RouteData routeData = new RouteData();
+ Controller controller = new Mock<Controller>().Object;
+
+ // Act
+ ControllerContext controllerContext = new ControllerContext(httpContext, routeData, controller);
+
+ // Assert
+ Assert.Equal(controller, controllerContext.Controller);
+ }
+
+ [Fact]
+ public void CopyConstructorSetsProperties()
+ {
+ // Arrange
+ Mock<HttpContextBase> httpContext = new Mock<HttpContextBase>();
+ httpContext.Setup(c => c.Items).Returns(new Hashtable());
+
+ RequestContext requestContext = new RequestContext(httpContext.Object, new RouteData());
+ Controller controller = new Mock<Controller>().Object;
+ var displayMode = new DefaultDisplayMode("test");
+
+ ControllerContext innerControllerContext = new ControllerContext(requestContext, controller);
+ innerControllerContext.DisplayMode = displayMode;
+
+ // Act
+ ControllerContext outerControllerContext = new SubclassedControllerContext(innerControllerContext);
+
+ // Assert
+ Assert.Equal(requestContext, outerControllerContext.RequestContext);
+ Assert.Equal(controller, outerControllerContext.Controller);
+
+ // We don't actually set DisplayMode but verify it is identical to the inner controller context.
+ Assert.Equal(displayMode, outerControllerContext.DisplayMode);
+ }
+
+ [Fact]
+ public void DisplayModeDelegatesToHttpContext()
+ {
+ // Arrange
+ IDisplayMode testDisplayMode = new DefaultDisplayMode("test");
+ Mock<HttpContextBase> httpContext = new Mock<HttpContextBase>();
+ httpContext.Setup(c => c.Items).Returns(new Hashtable());
+ Controller controller = new Mock<Controller>().Object;
+ ControllerContext controllerContext = new ControllerContext(httpContext.Object, new RouteData(), controller);
+ ControllerContext controllerContextWithIdenticalContext = new ControllerContext(httpContext.Object, new RouteData(), controller);
+
+ // Act
+ controllerContext.DisplayMode = testDisplayMode;
+
+ // Assert
+ Assert.Same(testDisplayMode, controllerContext.DisplayMode);
+ Assert.Same(testDisplayMode, controllerContextWithIdenticalContext.DisplayMode);
+ }
+
+ [Fact]
+ public void CopyConstructorThrowsIfControllerContextIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new SubclassedControllerContext(null); }, "controllerContext");
+ }
+
+ [Fact]
+ public void HttpContextPropertyGetSetBehavior()
+ {
+ // Arrange
+ HttpContextBase httpContext = new Mock<HttpContextBase>().Object;
+ ControllerContext controllerContext = new ControllerContext();
+
+ // Act & assert
+ MemberHelper.TestPropertyValue(controllerContext, "HttpContext", httpContext);
+ }
+
+ [Fact]
+ public void HttpContextPropertyReturnsEmptyHttpContextIfRequestContextNotPresent()
+ {
+ // Arrange
+ ControllerContext controllerContext = new ControllerContext();
+
+ // Act
+ HttpContextBase httpContext = controllerContext.HttpContext;
+ HttpContextBase httpContext2 = controllerContext.HttpContext;
+
+ // Assert
+ Assert.NotNull(httpContext);
+ Assert.Equal(httpContext, httpContext2);
+ }
+
+ [Fact]
+ public void HttpContextPropertyReturnsRequestContextHttpContextIfPresent()
+ {
+ // Arrange
+ HttpContextBase httpContext = new Mock<HttpContextBase>().Object;
+ RouteData routeData = new RouteData();
+ RequestContext requestContext = new RequestContext(httpContext, routeData);
+ Controller controller = new Mock<Controller>().Object;
+
+ // Act
+ ControllerContext controllerContext = new ControllerContext(requestContext, controller);
+
+ // Assert
+ Assert.Equal(httpContext, controllerContext.HttpContext);
+ }
+
+ [Fact]
+ public void RequestContextPropertyCreatesDummyHttpContextAndRouteDataIfNecessary()
+ {
+ // Arrange
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.Setup(c => c.HttpContext).Returns((HttpContextBase)null);
+ mockControllerContext.Setup(c => c.RouteData).Returns((RouteData)null);
+ ControllerContext controllerContext = mockControllerContext.Object;
+
+ // Act
+ RequestContext requestContext = controllerContext.RequestContext;
+ RequestContext requestContext2 = controllerContext.RequestContext;
+
+ // Assert
+ Assert.Equal(requestContext, requestContext2);
+ Assert.NotNull(requestContext.HttpContext);
+ Assert.NotNull(requestContext.RouteData);
+ }
+
+ [Fact]
+ public void RequestContextPropertyUsesExistingHttpContextAndRouteData()
+ {
+ // Arrange
+ HttpContextBase httpContext = new Mock<HttpContextBase>().Object;
+ RouteData routeData = new RouteData();
+
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.Setup(c => c.HttpContext).Returns(httpContext);
+ mockControllerContext.Setup(c => c.RouteData).Returns(routeData);
+ ControllerContext controllerContext = mockControllerContext.Object;
+
+ // Act
+ RequestContext requestContext = controllerContext.RequestContext;
+ RequestContext requestContext2 = controllerContext.RequestContext;
+
+ // Assert
+ Assert.Equal(requestContext, requestContext2);
+ Assert.Equal(httpContext, requestContext.HttpContext);
+ Assert.Equal(routeData, requestContext.RouteData);
+ }
+
+ [Fact]
+ public void RouteDataPropertyGetSetBehavior()
+ {
+ // Arrange
+ RouteData routeData = new RouteData();
+ ControllerContext controllerContext = new ControllerContext();
+
+ // Act & assert
+ MemberHelper.TestPropertyValue(controllerContext, "RouteData", routeData);
+ }
+
+ [Fact]
+ public void RouteDataPropertyReturnsEmptyRouteDataIfRequestContextNotPresent()
+ {
+ // Arrange
+ ControllerContext controllerContext = new ControllerContext();
+
+ // Act
+ RouteData routeData = controllerContext.RouteData;
+ RouteData routeData2 = controllerContext.RouteData;
+
+ // Assert
+ Assert.Equal(routeData, routeData2);
+ Assert.Empty(routeData.Values);
+ }
+
+ [Fact]
+ public void RouteDataPropertyReturnsRequestContextRouteDataIfPresent()
+ {
+ // Arrange
+ HttpContextBase httpContext = new Mock<HttpContextBase>().Object;
+ RouteData routeData = new RouteData();
+ RequestContext requestContext = new RequestContext(httpContext, routeData);
+ Controller controller = new Mock<Controller>().Object;
+
+ // Act
+ ControllerContext controllerContext = new ControllerContext(requestContext, controller);
+
+ // Assert
+ Assert.Equal(routeData, controllerContext.RouteData);
+ }
+
+ [Fact]
+ public void IsChildActionReturnsFalseByDefault()
+ {
+ // Arrange
+ HttpContextBase httpContext = new Mock<HttpContextBase>().Object;
+ RouteData routeData = new RouteData();
+ RequestContext requestContext = new RequestContext(httpContext, routeData);
+ Controller controller = new Mock<Controller>().Object;
+ ControllerContext controllerContext = new ControllerContext(requestContext, controller);
+
+ // Act
+ bool result = controllerContext.IsChildAction;
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void IsChildActionReturnsTrueWhenRouteDataTokenIsSet()
+ {
+ // Arrange
+ HttpContextBase httpContext = new Mock<HttpContextBase>().Object;
+ ViewContext viewContext = new ViewContext();
+ RouteData routeData = new RouteData();
+ routeData.DataTokens[ControllerContext.ParentActionViewContextToken] = viewContext;
+ RequestContext requestContext = new RequestContext(httpContext, routeData);
+ Controller controller = new Mock<Controller>().Object;
+ ControllerContext controllerContext = new ControllerContext(requestContext, controller);
+
+ // Act
+ bool result = controllerContext.IsChildAction;
+
+ // Assert
+ Assert.True(result);
+ Assert.Same(viewContext, controllerContext.ParentActionViewContext);
+ }
+
+ public static ControllerContext CreateEmptyContext()
+ {
+ return new ControllerContext(new Mock<HttpContextBase>().Object, new RouteData(), new Mock<Controller>().Object);
+ }
+
+ private class SubclassedControllerContext : ControllerContext
+ {
+ public SubclassedControllerContext(ControllerContext controllerContext)
+ : base(controllerContext)
+ {
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ControllerDescriptorCacheTest.cs b/test/System.Web.Mvc.Test/Test/ControllerDescriptorCacheTest.cs
new file mode 100644
index 00000000..6f700a20
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ControllerDescriptorCacheTest.cs
@@ -0,0 +1,23 @@
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class ControllerDescriptorCacheTest
+ {
+ [Fact]
+ public void GetDescriptor()
+ {
+ // Arrange
+ Type controllerType = typeof(object);
+ ControllerDescriptorCache cache = new ControllerDescriptorCache();
+
+ // Act
+ ControllerDescriptor descriptor1 = cache.GetDescriptor(controllerType, () => new ReflectedControllerDescriptor(controllerType));
+ ControllerDescriptor descriptor2 = cache.GetDescriptor(controllerType, () => new ReflectedControllerDescriptor(controllerType));
+
+ // Assert
+ Assert.Same(controllerType, descriptor1.ControllerType);
+ Assert.Same(descriptor1, descriptor2);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ControllerDescriptorTest.cs b/test/System.Web.Mvc.Test/Test/ControllerDescriptorTest.cs
new file mode 100644
index 00000000..334ffcbe
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ControllerDescriptorTest.cs
@@ -0,0 +1,129 @@
+using System.Linq;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ControllerDescriptorTest
+ {
+ [Fact]
+ public void ControllerNamePropertyReturnsControllerTypeName()
+ {
+ // Arrange
+ ControllerDescriptor cd = GetControllerDescriptor(typeof(object));
+
+ // Act
+ string name = cd.ControllerName;
+
+ // Assert
+ Assert.Equal("Object", name);
+ }
+
+ [Fact]
+ public void ControllerNamePropertyReturnsControllerTypeNameWithoutControllerSuffix()
+ {
+ // Arrange
+ Mock<Type> mockType = new Mock<Type>();
+ mockType.Setup(t => t.Name).Returns("somecontroller");
+ ControllerDescriptor cd = GetControllerDescriptor(mockType.Object);
+
+ // Act
+ string name = cd.ControllerName;
+
+ // Assert
+ Assert.Equal("some", name);
+ }
+
+ [Fact]
+ public void GetCustomAttributesReturnsEmptyArrayOfAttributeType()
+ {
+ // Arrange
+ ControllerDescriptor cd = GetControllerDescriptor();
+
+ // Act
+ ObsoleteAttribute[] attrs = (ObsoleteAttribute[])cd.GetCustomAttributes(typeof(ObsoleteAttribute), true);
+
+ // Assert
+ Assert.Empty(attrs);
+ }
+
+ [Fact]
+ public void GetCustomAttributesThrowsIfAttributeTypeIsNull()
+ {
+ // Arrange
+ ControllerDescriptor cd = GetControllerDescriptor();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { cd.GetCustomAttributes(null /* attributeType */, true); }, "attributeType");
+ }
+
+ [Fact]
+ public void GetCustomAttributesWithoutAttributeTypeCallsGetCustomAttributesWithAttributeType()
+ {
+ // Arrange
+ object[] expected = new object[0];
+ Mock<ControllerDescriptor> mockDescriptor = new Mock<ControllerDescriptor>() { CallBase = true };
+ mockDescriptor.Setup(d => d.GetCustomAttributes(typeof(object), true)).Returns(expected);
+ ControllerDescriptor cd = mockDescriptor.Object;
+
+ // Act
+ object[] returned = cd.GetCustomAttributes(true /* inherit */);
+
+ // Assert
+ Assert.Same(expected, returned);
+ }
+
+ [Fact]
+ public void GetFilterAttributes_CallsGetCustomAttributes()
+ {
+ // Arrange
+ var mockDescriptor = new Mock<ControllerDescriptor>() { CallBase = true };
+ mockDescriptor.Setup(d => d.GetCustomAttributes(typeof(FilterAttribute), true)).Returns(new object[] { new Mock<FilterAttribute>().Object }).Verifiable();
+
+ // Act
+ var result = mockDescriptor.Object.GetFilterAttributes(true).ToList();
+
+ // Assert
+ mockDescriptor.Verify();
+ Assert.Single(result);
+ }
+
+ [Fact]
+ public void IsDefinedReturnsFalse()
+ {
+ // Arrange
+ ControllerDescriptor cd = GetControllerDescriptor();
+
+ // Act
+ bool isDefined = cd.IsDefined(typeof(object), true);
+
+ // Assert
+ Assert.False(isDefined);
+ }
+
+ [Fact]
+ public void IsDefinedThrowsIfAttributeTypeIsNull()
+ {
+ // Arrange
+ ControllerDescriptor cd = GetControllerDescriptor();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { cd.IsDefined(null /* attributeType */, true); }, "attributeType");
+ }
+
+ private static ControllerDescriptor GetControllerDescriptor()
+ {
+ return GetControllerDescriptor(null);
+ }
+
+ private static ControllerDescriptor GetControllerDescriptor(Type controllerType)
+ {
+ Mock<ControllerDescriptor> mockDescriptor = new Mock<ControllerDescriptor>() { CallBase = true };
+ mockDescriptor.Setup(d => d.ControllerType).Returns(controllerType);
+ return mockDescriptor.Object;
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ControllerInstanceFilterProviderTest.cs b/test/System.Web.Mvc.Test/Test/ControllerInstanceFilterProviderTest.cs
new file mode 100644
index 00000000..81f9685c
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ControllerInstanceFilterProviderTest.cs
@@ -0,0 +1,44 @@
+using System.Collections.Generic;
+using System.Linq;
+using Moq;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class ControllerInstanceFilterProviderTest
+ {
+ [Fact]
+ public void GetFiltersWithNullControllerReturnsEmptyCollection()
+ {
+ // Arrange
+ var context = new ControllerContext();
+ var descriptor = new Mock<ActionDescriptor>().Object;
+ var provider = new ControllerInstanceFilterProvider();
+
+ // Act
+ IEnumerable<Filter> result = provider.GetFilters(context, descriptor);
+
+ // Assert
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void GetFiltersWithControllerReturnsWrappedController()
+ {
+ // Arrange
+ var controller = new Mock<ControllerBase>().Object;
+ var context = new ControllerContext { Controller = controller };
+ var descriptor = new Mock<ActionDescriptor>().Object;
+ var provider = new ControllerInstanceFilterProvider();
+
+ // Act
+ IEnumerable<Filter> result = provider.GetFilters(context, descriptor);
+
+ // Assert
+ Filter filter = result.Single();
+ Assert.Same(controller, filter.Instance);
+ Assert.Equal(Int32.MinValue, filter.Order);
+ Assert.Equal(FilterScope.First, filter.Scope);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ControllerTest.cs b/test/System.Web.Mvc.Test/Test/ControllerTest.cs
new file mode 100644
index 00000000..cc748821
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ControllerTest.cs
@@ -0,0 +1,2047 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.IO;
+using System.Reflection;
+using System.Security.Principal;
+using System.Text;
+using System.Web.Mvc.Async;
+using System.Web.Profile;
+using System.Web.Routing;
+using System.Web.TestUtil;
+using Microsoft.Web.UnitTestUtil;
+using Moq;
+using Moq.Protected;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ControllerTest
+ {
+ [Fact]
+ public void ActionInvokerProperty()
+ {
+ // Arrange
+ Controller controller = new EmptyController();
+
+ // Act & Assert
+ MemberHelper.TestPropertyWithDefaultInstance(controller, "ActionInvoker", new ControllerActionInvoker());
+ }
+
+ [Fact]
+ public void ContentWithContentString()
+ {
+ // Arrange
+ Controller controller = new EmptyController();
+ string content = "Some content";
+
+ // Act
+ ContentResult result = controller.Content(content);
+
+ // Assert
+ Assert.Equal(content, result.Content);
+ }
+
+ [Fact]
+ public void ContentWithContentStringAndContentType()
+ {
+ // Arrange
+ Controller controller = new EmptyController();
+ string content = "Some content";
+ string contentType = "Some content type";
+
+ // Act
+ ContentResult result = controller.Content(content, contentType);
+
+ // Assert
+ Assert.Equal(content, result.Content);
+ Assert.Equal(contentType, result.ContentType);
+ }
+
+ [Fact]
+ public void ContentWithContentStringAndContentTypeAndEncoding()
+ {
+ // Arrange
+ Controller controller = new EmptyController();
+ string content = "Some content";
+ string contentType = "Some content type";
+ Encoding contentEncoding = Encoding.UTF8;
+
+ // Act
+ ContentResult result = controller.Content(content, contentType, contentEncoding);
+
+ // Assert
+ Assert.Equal(content, result.Content);
+ Assert.Equal(contentType, result.ContentType);
+ Assert.Same(contentEncoding, result.ContentEncoding);
+ }
+
+ [Fact]
+ public void ContextProperty()
+ {
+ var controller = new EmptyController();
+ MemberHelper.TestPropertyValue(controller, "ControllerContext", new Mock<ControllerContext>().Object);
+ }
+
+ [Fact]
+ public void HttpContextProperty()
+ {
+ var c = new EmptyController();
+ Assert.Null(c.HttpContext);
+
+ Mock<HttpContextBase> mockHttpContext = new Mock<HttpContextBase>();
+
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.Setup(cc => cc.Controller).Returns(c);
+ mockControllerContext.Setup(cc => cc.HttpContext).Returns(mockHttpContext.Object);
+
+ c.ControllerContext = mockControllerContext.Object;
+ Assert.Equal(mockHttpContext.Object, c.HttpContext);
+ }
+
+ [Fact]
+ public void HttpNotFound()
+ {
+ // Arrange
+ var c = new EmptyController();
+
+ // Act
+ HttpNotFoundResult result = c.HttpNotFound();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Null(result.StatusDescription);
+ Assert.Equal(404, result.StatusCode);
+ }
+
+ [Fact]
+ public void HttpNotFoundWithNullStatusDescription()
+ {
+ // Arrange
+ var c = new EmptyController();
+
+ // Act
+ HttpNotFoundResult result = c.HttpNotFound(statusDescription: null);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Null(result.StatusDescription);
+ Assert.Equal(404, result.StatusCode);
+ }
+
+ [Fact]
+ public void HttpNotFoundWithStatusDescription()
+ {
+ // Arrange
+ var c = new EmptyController();
+
+ // Act
+ HttpNotFoundResult result = c.HttpNotFound(statusDescription: "I lost it");
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal("I lost it", result.StatusDescription);
+ Assert.Equal(404, result.StatusCode);
+ }
+
+ [Fact]
+ public void ModelStateProperty()
+ {
+ // Arrange
+ Controller controller = new EmptyController();
+
+ // Act & assert
+ Assert.Same(controller.ViewData.ModelState, controller.ModelState);
+ }
+
+ [Fact]
+ public void ProfileProperty()
+ {
+ var c = new EmptyController();
+ Assert.Null(c.Profile);
+
+ Mock<ProfileBase> mockProfile = new Mock<ProfileBase>();
+
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.Setup(cc => cc.Controller).Returns(c);
+ mockControllerContext.Setup(cc => cc.HttpContext.Profile).Returns(mockProfile.Object);
+
+ c.ControllerContext = mockControllerContext.Object;
+ Assert.Equal(mockProfile.Object, c.Profile);
+ }
+
+ [Fact]
+ public void RequestProperty()
+ {
+ var c = new EmptyController();
+ Assert.Null(c.Request);
+
+ Mock<HttpRequestBase> mockHttpRequest = new Mock<HttpRequestBase>();
+
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.Setup(cc => cc.Controller).Returns(c);
+ mockControllerContext.Setup(cc => cc.HttpContext.Request).Returns(mockHttpRequest.Object);
+
+ c.ControllerContext = mockControllerContext.Object;
+ Assert.Equal(mockHttpRequest.Object, c.Request);
+ }
+
+ [Fact]
+ public void ResponseProperty()
+ {
+ var c = new EmptyController();
+ Assert.Null(c.Request);
+
+ Mock<HttpResponseBase> mockHttpResponse = new Mock<HttpResponseBase>();
+
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.Setup(cc => cc.Controller).Returns(c);
+ mockControllerContext.Setup(cc => cc.HttpContext.Response).Returns(mockHttpResponse.Object);
+
+ c.ControllerContext = mockControllerContext.Object;
+ Assert.Equal(mockHttpResponse.Object, c.Response);
+ }
+
+ [Fact]
+ public void ServerProperty()
+ {
+ var c = new EmptyController();
+ Assert.Null(c.Request);
+
+ Mock<HttpServerUtilityBase> mockServerUtility = new Mock<HttpServerUtilityBase>();
+
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.Setup(cc => cc.Controller).Returns(c);
+ mockControllerContext.Setup(cc => cc.HttpContext.Server).Returns(mockServerUtility.Object);
+
+ c.ControllerContext = mockControllerContext.Object;
+ Assert.Equal(mockServerUtility.Object, c.Server);
+ }
+
+ [Fact]
+ public void SessionProperty()
+ {
+ var c = new EmptyController();
+ Assert.Null(c.Request);
+
+ Mock<HttpSessionStateBase> mockSessionState = new Mock<HttpSessionStateBase>();
+
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.Setup(cc => cc.Controller).Returns(c);
+ mockControllerContext.Setup(cc => cc.HttpContext.Session).Returns(mockSessionState.Object);
+
+ c.ControllerContext = mockControllerContext.Object;
+ Assert.Same(mockSessionState.Object, c.Session);
+ }
+
+ [Fact]
+ public void UrlProperty()
+ {
+ // Arrange
+ EmptyController controller = new EmptyController();
+ RequestContext requestContext = new RequestContext(new Mock<HttpContextBase>().Object, new RouteData());
+
+ // Act
+ controller.PublicInitialize(requestContext);
+
+ // Assert
+ Assert.NotNull(controller.Url);
+ }
+
+ [Fact]
+ public void UserProperty()
+ {
+ var c = new EmptyController();
+ Assert.Null(c.Request);
+
+ Mock<IPrincipal> mockUser = new Mock<IPrincipal>();
+
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.Setup(cc => cc.Controller).Returns(c);
+ mockControllerContext.Setup(cc => cc.HttpContext.User).Returns(mockUser.Object);
+
+ c.ControllerContext = mockControllerContext.Object;
+ Assert.Equal(mockUser.Object, c.User);
+ }
+
+ [Fact]
+ public void RouteDataProperty()
+ {
+ var c = new EmptyController();
+ Assert.Null(c.Request);
+
+ RouteData rd = new RouteData();
+
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.Setup(cc => cc.Controller).Returns(c);
+ mockControllerContext.Setup(cc => cc.RouteData).Returns(rd);
+
+ c.ControllerContext = mockControllerContext.Object;
+ Assert.Equal(rd, c.RouteData);
+ }
+
+ [Fact]
+ public void ControllerMethodsDoNotHaveNonActionAttribute()
+ {
+ var methods = typeof(Controller).GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
+ foreach (var method in methods)
+ {
+ var attrs = method.GetCustomAttributes(typeof(NonActionAttribute), true /* inherit */);
+ Assert.True(attrs.Length == 0, "Methods on the Controller class should not be marked [NonAction]: " + method);
+ }
+ }
+
+ [Fact]
+ public void DisposeCallsProtectedDisposingMethod()
+ {
+ // Arrange
+ Mock<Controller> mockController = new Mock<Controller>();
+ mockController.Protected().Setup("Dispose", true).Verifiable();
+ Controller controller = mockController.Object;
+
+ // Act
+ controller.Dispose();
+
+ // Assert
+ mockController.Verify();
+ }
+
+ [Fact]
+ public void ExecuteWithUnknownAction()
+ {
+ // Arrange
+ UnknownActionController controller = new UnknownActionController();
+ // We need a provider since Controller.Execute is called
+ controller.TempDataProvider = new EmptyTempDataProvider();
+ ControllerContext context = GetControllerContext("Foo");
+
+ Mock<IActionInvoker> mockInvoker = new Mock<IActionInvoker>();
+ mockInvoker.Setup(o => o.InvokeAction(context, "Foo")).Returns(false);
+ controller.ActionInvoker = mockInvoker.Object;
+
+ // Act
+ ((IController)controller).Execute(context.RequestContext);
+
+ // Assert
+ Assert.True(controller.WasCalled);
+ }
+
+ [Fact]
+ public void FileWithContents()
+ {
+ // Arrange
+ EmptyController controller = new EmptyController();
+ byte[] fileContents = new byte[0];
+
+ // Act
+ FileContentResult result = controller.File(fileContents, "someContentType");
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Same(fileContents, result.FileContents);
+ Assert.Equal("someContentType", result.ContentType);
+ Assert.Equal(String.Empty, result.FileDownloadName);
+ }
+
+ [Fact]
+ public void FileWithContentsAndFileDownloadName()
+ {
+ // Arrange
+ EmptyController controller = new EmptyController();
+ byte[] fileContents = new byte[0];
+
+ // Act
+ FileContentResult result = controller.File(fileContents, "someContentType", "someDownloadName");
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Same(fileContents, result.FileContents);
+ Assert.Equal("someContentType", result.ContentType);
+ Assert.Equal("someDownloadName", result.FileDownloadName);
+ }
+
+ [Fact]
+ public void FileWithPath()
+ {
+ // Arrange
+ EmptyController controller = new EmptyController();
+
+ // Act
+ FilePathResult result = controller.File("somePath", "someContentType");
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal("somePath", result.FileName);
+ Assert.Equal("someContentType", result.ContentType);
+ Assert.Equal(String.Empty, result.FileDownloadName);
+ }
+
+ [Fact]
+ public void FileWithPathAndFileDownloadName()
+ {
+ // Arrange
+ EmptyController controller = new EmptyController();
+
+ // Act
+ FilePathResult result = controller.File("somePath", "someContentType", "someDownloadName");
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal("somePath", result.FileName);
+ Assert.Equal("someContentType", result.ContentType);
+ Assert.Equal("someDownloadName", result.FileDownloadName);
+ }
+
+ [Fact]
+ public void FileWithStream()
+ {
+ // Arrange
+ EmptyController controller = new EmptyController();
+ Stream fileStream = Stream.Null;
+
+ // Act
+ FileStreamResult result = controller.File(fileStream, "someContentType");
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Same(fileStream, result.FileStream);
+ Assert.Equal("someContentType", result.ContentType);
+ Assert.Equal(String.Empty, result.FileDownloadName);
+ }
+
+ [Fact]
+ public void FileWithStreamAndFileDownloadName()
+ {
+ // Arrange
+ EmptyController controller = new EmptyController();
+ Stream fileStream = Stream.Null;
+
+ // Act
+ FileStreamResult result = controller.File(fileStream, "someContentType", "someDownloadName");
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Same(fileStream, result.FileStream);
+ Assert.Equal("someContentType", result.ContentType);
+ Assert.Equal("someDownloadName", result.FileDownloadName);
+ }
+
+ [Fact]
+ public void HandleUnknownActionThrows()
+ {
+ var controller = new EmptyController();
+ Assert.Throws<HttpException>(
+ delegate { controller.HandleUnknownAction("UnknownAction"); },
+ "A public action method 'UnknownAction' was not found on controller 'System.Web.Mvc.Test.ControllerTest+EmptyController'.");
+ }
+
+ [Fact]
+ public void JavaScript()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+ string script = "alert('foo');";
+
+ // Act
+ JavaScriptResult result = controller.JavaScript(script);
+
+ // Assert
+ Assert.Equal(script, result.Script);
+ }
+
+ [Fact]
+ public void PartialView()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+
+ // Act
+ PartialViewResult result = controller.PartialView();
+
+ // Assert
+ Assert.Same(controller.TempData, result.TempData);
+ Assert.Same(controller.ViewData, result.ViewData);
+ Assert.Same(ViewEngines.Engines, result.ViewEngineCollection);
+ }
+
+ [Fact]
+ public void PartialView_Model()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+ object model = new object();
+
+ // Act
+ PartialViewResult result = controller.PartialView(model);
+
+ // Assert
+ Assert.Same(model, result.ViewData.Model);
+ Assert.Same(controller.TempData, result.TempData);
+ Assert.Same(controller.ViewData, result.ViewData);
+ }
+
+ [Fact]
+ public void PartialView_ViewName()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+
+ // Act
+ PartialViewResult result = controller.PartialView("Some partial view");
+
+ // Assert
+ Assert.Equal("Some partial view", result.ViewName);
+ Assert.Same(controller.TempData, result.TempData);
+ Assert.Same(controller.ViewData, result.ViewData);
+ }
+
+ [Fact]
+ public void PartialView_ViewName_Model()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+ object model = new object();
+
+ // Act
+ PartialViewResult result = controller.PartialView("Some partial view", model);
+
+ // Assert
+ Assert.Equal("Some partial view", result.ViewName);
+ Assert.Same(model, result.ViewData.Model);
+ Assert.Same(controller.TempData, result.TempData);
+ Assert.Same(controller.ViewData, result.ViewData);
+ }
+
+ [Fact]
+ public void PartialView_ViewEngineCollection()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+ ViewEngineCollection viewEngines = new ViewEngineCollection();
+ controller.ViewEngineCollection = viewEngines;
+
+ // Act
+ PartialViewResult result = controller.PartialView();
+
+ // Assert
+ Assert.Same(viewEngines, result.ViewEngineCollection);
+ }
+
+ [Fact]
+ public void RedirectToActionClonesRouteValueDictionary()
+ {
+ // The RedirectToAction() method should clone the provided dictionary, then operate on the clone.
+ // The original dictionary should remain unmodified throughout the helper's execution.
+
+ // Arrange
+ Controller controller = GetEmptyController();
+ RouteValueDictionary values = new RouteValueDictionary(new { Action = "SomeAction", Controller = "SomeController" });
+
+ // Act
+ controller.RedirectToAction("SomeOtherAction", "SomeOtherController", values);
+
+ // Assert
+ Assert.Equal(2, values.Count);
+ Assert.Equal("SomeAction", values["action"]);
+ Assert.Equal("SomeController", values["controller"]);
+ }
+
+ [Fact]
+ public void RedirectToActionOverwritesActionDictionaryKey()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+ object values = new { Action = "SomeAction" };
+
+ // Act
+ RedirectToRouteResult result = controller.RedirectToAction("SomeOtherAction", values);
+ RouteValueDictionary newValues = result.RouteValues;
+
+ // Assert
+ Assert.Equal("SomeOtherAction", newValues["action"]);
+ }
+
+ [Fact]
+ public void RedirectToActionOverwritesControllerDictionaryKeyIfSpecified()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+ object values = new { Action = "SomeAction", Controller = "SomeController" };
+
+ // Act
+ RedirectToRouteResult result = controller.RedirectToAction("SomeOtherAction", "SomeOtherController", values);
+ RouteValueDictionary newValues = result.RouteValues;
+
+ // Assert
+ Assert.Equal("SomeOtherController", newValues["controller"]);
+ }
+
+ [Fact]
+ public void RedirectToActionPreservesControllerDictionaryKeyIfNotSpecified()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+ object values = new { Controller = "SomeController" };
+
+ // Act
+ RedirectToRouteResult result = controller.RedirectToAction("SomeOtherAction", values);
+ RouteValueDictionary newValues = result.RouteValues;
+
+ // Assert
+ Assert.Equal("SomeController", newValues["controller"]);
+ }
+
+ [Fact]
+ public void RedirectToActionWithActionName()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+
+ // Act
+ RedirectToRouteResult result = controller.RedirectToAction("SomeOtherAction");
+
+ // Assert
+ Assert.Equal("", result.RouteName);
+ Assert.Equal("SomeOtherAction", result.RouteValues["action"]);
+ Assert.False(result.Permanent);
+ }
+
+ [Fact]
+ public void RedirectToActionWithActionNameAndControllerName()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+
+ // Act
+ RedirectToRouteResult result = controller.RedirectToAction("SomeOtherAction", "SomeOtherController");
+
+ // Assert
+ Assert.Equal("", result.RouteName);
+ Assert.Equal("SomeOtherAction", result.RouteValues["action"]);
+ Assert.Equal("SomeOtherController", result.RouteValues["controller"]);
+ Assert.False(result.Permanent);
+ }
+
+ [Fact]
+ public void RedirectToActionWithActionNameAndControllerNameAndValuesDictionary()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+ RouteValueDictionary values = new RouteValueDictionary(new { Foo = "SomeFoo" });
+
+ // Act
+ RedirectToRouteResult result = controller.RedirectToAction("SomeOtherAction", "SomeOtherController", values);
+
+ // Assert
+ Assert.Equal("", result.RouteName);
+ Assert.Equal("SomeOtherAction", result.RouteValues["action"]);
+ Assert.Equal("SomeOtherController", result.RouteValues["controller"]);
+ Assert.Equal("SomeFoo", result.RouteValues["foo"]);
+ Assert.False(result.Permanent);
+ }
+
+ [Fact]
+ public void RedirectToActionWithActionNameAndControllerNameAndValuesObject()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+ object values = new { Foo = "SomeFoo" };
+
+ // Act
+ RedirectToRouteResult result = controller.RedirectToAction("SomeOtherAction", "SomeOtherController", values);
+
+ // Assert
+ Assert.Equal("", result.RouteName);
+ Assert.Equal("SomeOtherAction", result.RouteValues["action"]);
+ Assert.Equal("SomeOtherController", result.RouteValues["controller"]);
+ Assert.Equal("SomeFoo", result.RouteValues["foo"]);
+ Assert.False(result.Permanent);
+ }
+
+ [Fact]
+ public void RedirectToActionSelectsCurrentControllerByDefault()
+ {
+ // Arrange
+ TestRouteController controller = new TestRouteController();
+ controller.ControllerContext = GetControllerContext("SomeAction", "TestRoute");
+
+ // Act
+ RedirectToRouteResult route = controller.Index() as RedirectToRouteResult;
+
+ // Assert
+ Assert.Equal("SomeAction", route.RouteValues["action"]);
+ Assert.Equal("TestRoute", route.RouteValues["controller"]);
+ }
+
+ [Fact]
+ public void RedirectToActionDictionaryOverridesDefaultControllerName()
+ {
+ // Arrange
+ TestRouteController controller = new TestRouteController();
+ object values = new { controller = "SomeOtherController" };
+ controller.ControllerContext = GetControllerContext("SomeAction", "TestRoute");
+
+ // Act
+ RedirectToRouteResult route = controller.RedirectToAction("SomeOtherAction", values);
+
+ // Assert
+ Assert.Equal("SomeOtherAction", route.RouteValues["action"]);
+ Assert.Equal("SomeOtherController", route.RouteValues["controller"]);
+ }
+
+ [Fact]
+ public void RedirectToActionSimpleOverridesCallLegacyMethod()
+ {
+ // The simple overrides need to call RedirectToAction(string, string, RouteValueDictionary) to maintain backwards compat
+
+ // Arrange
+ int invocationCount = 0;
+ Mock<Controller> controllerMock = new Mock<Controller>();
+ controllerMock.Setup(c => c.RedirectToAction(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<RouteValueDictionary>())).Callback(() => { invocationCount++; });
+
+ Controller controller = controllerMock.Object;
+
+ // Act
+ controller.RedirectToAction("SomeAction");
+ controller.RedirectToAction("SomeAction", (object)null);
+ controller.RedirectToAction("SomeAction", (RouteValueDictionary)null);
+ controller.RedirectToAction("SomeAction", "SomeController");
+ controller.RedirectToAction("SomeAction", "SomeController", (object)null);
+
+ // Assert
+ Assert.Equal(5, invocationCount);
+ }
+
+ [Fact]
+ public void RedirectToActionWithActionNameAndValuesDictionary()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+ RouteValueDictionary values = new RouteValueDictionary(new { Foo = "SomeFoo" });
+
+ // Act
+ RedirectToRouteResult result = controller.RedirectToAction("SomeOtherAction", values);
+
+ // Assert
+ Assert.Equal("", result.RouteName);
+ Assert.Equal("SomeOtherAction", result.RouteValues["action"]);
+ Assert.Equal("SomeFoo", result.RouteValues["foo"]);
+ Assert.False(result.Permanent);
+ }
+
+ [Fact]
+ public void RedirectToActionWithActionNameAndValuesObject()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+ object values = new { Foo = "SomeFoo" };
+
+ // Act
+ RedirectToRouteResult result = controller.RedirectToAction("SomeOtherAction", values);
+
+ // Assert
+ Assert.Equal("", result.RouteName);
+ Assert.Equal("SomeOtherAction", result.RouteValues["action"]);
+ Assert.Equal("SomeFoo", result.RouteValues["foo"]);
+ Assert.False(result.Permanent);
+ }
+
+ [Fact]
+ public void RedirectToActionWithNullRouteValueDictionary()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+
+ // Act
+ RedirectToRouteResult result = controller.RedirectToAction("SomeOtherAction", (RouteValueDictionary)null);
+ RouteValueDictionary newValues = result.RouteValues;
+
+ // Assert
+ Assert.Single(newValues);
+ Assert.Equal("SomeOtherAction", newValues["action"]);
+ Assert.False(result.Permanent);
+ }
+
+ [Fact]
+ public void RedirectToActionPermanent()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+
+ // Act
+ RedirectToRouteResult result = controller.RedirectToActionPermanent("SomeOtherAction");
+
+ // Assert
+ Assert.True(result.Permanent);
+ Assert.Equal("SomeOtherAction", result.RouteValues["action"]);
+ Assert.Null(result.RouteValues["controller"]);
+ }
+
+ [Fact]
+ public void RedirectToActionPermanentWithObjectDictionary()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+
+ // Act
+ RedirectToRouteResult result = controller.RedirectToActionPermanent("SomeOtherAction", controllerName: "SomeController", routeValues: new { foo = "bar" });
+
+ // Assert
+ Assert.True(result.Permanent);
+ Assert.Equal("SomeOtherAction", result.RouteValues["action"]);
+ Assert.Equal("bar", result.RouteValues["foo"]);
+ Assert.Equal("SomeController", result.RouteValues["controller"]);
+ }
+
+ [Fact]
+ public void RedirectToActionPermanentWithRouteValueDictionary()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+
+ // Act
+ RedirectToRouteResult result = controller.RedirectToActionPermanent("SomeOtherAction", routeValues: new RouteValueDictionary(new { foo = "bar" }));
+
+ // Assert
+ Assert.True(result.Permanent);
+ Assert.Equal("SomeOtherAction", result.RouteValues["action"]);
+ Assert.Equal("bar", result.RouteValues["foo"]);
+ }
+
+ [Fact]
+ public void RedirectToRouteSimpleOverridesCallLegacyMethod()
+ {
+ // The simple overrides need to call RedirectToRoute(string, RouteValueDictionary) to maintain backwards compat
+
+ // Arrange
+ int invocationCount = 0;
+ Mock<Controller> controllerMock = new Mock<Controller>();
+ controllerMock.Setup(c => c.RedirectToRoute(It.IsAny<string>(), It.IsAny<RouteValueDictionary>())).Callback(() => { invocationCount++; });
+
+ Controller controller = controllerMock.Object;
+
+ // Act
+ controller.RedirectToRoute("SomeRoute");
+ controller.RedirectToRoute("SomeRoute", (object)null);
+ controller.RedirectToRoute((object)null);
+ controller.RedirectToRoute((RouteValueDictionary)null);
+
+ // Assert
+ Assert.Equal(4, invocationCount);
+ }
+
+ [Fact]
+ public void RedirectToRouteWithNullRouteValueDictionary()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+
+ // Act
+ RedirectToRouteResult result = controller.RedirectToRoute((RouteValueDictionary)null);
+
+ // Assert
+ Assert.Empty(result.RouteValues);
+ Assert.False(result.Permanent);
+ }
+
+ [Fact]
+ public void RedirectToRouteWithObjectDictionary()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+ var values = new { Foo = "MyFoo" };
+
+ // Act
+ RedirectToRouteResult result = controller.RedirectToRoute(values);
+
+ // Assert
+ Assert.Single(result.RouteValues);
+ Assert.Equal("MyFoo", result.RouteValues["Foo"]);
+ Assert.False(result.Permanent);
+ }
+
+ [Fact]
+ public void RedirectToRouteWithRouteValueDictionary()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+ RouteValueDictionary values = new RouteValueDictionary() { { "Foo", "MyFoo" } };
+
+ // Act
+ RedirectToRouteResult result = controller.RedirectToRoute(values);
+
+ // Assert
+ Assert.Single(result.RouteValues);
+ Assert.Equal("MyFoo", result.RouteValues["Foo"]);
+ Assert.NotSame(values, result.RouteValues);
+ Assert.False(result.Permanent);
+ }
+
+ [Fact]
+ public void RedirectToRouteWithName()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+
+ // Act
+ RedirectToRouteResult result = controller.RedirectToRoute("foo");
+
+ // Assert
+ Assert.Empty(result.RouteValues);
+ Assert.Equal("foo", result.RouteName);
+ Assert.False(result.Permanent);
+ }
+
+ [Fact]
+ public void RedirectToRouteWithNameAndNullRouteValueDictionary()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+
+ // Act
+ RedirectToRouteResult result = controller.RedirectToRoute("foo", (RouteValueDictionary)null);
+
+ // Assert
+ Assert.Empty(result.RouteValues);
+ Assert.Equal("foo", result.RouteName);
+ Assert.False(result.Permanent);
+ }
+
+ [Fact]
+ public void RedirectToRouteWithNullNameAndNullRouteValueDictionary()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+
+ // Act
+ RedirectToRouteResult result = controller.RedirectToRoute(null, (RouteValueDictionary)null);
+
+ // Assert
+ Assert.Empty(result.RouteValues);
+ Assert.Equal(String.Empty, result.RouteName);
+ Assert.False(result.Permanent);
+ }
+
+ [Fact]
+ public void RedirectToRouteWithNameAndObjectDictionary()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+ var values = new { Foo = "MyFoo" };
+
+ // Act
+ RedirectToRouteResult result = controller.RedirectToRoute("foo", values);
+
+ // Assert
+ Assert.Single(result.RouteValues);
+ Assert.Equal("MyFoo", result.RouteValues["Foo"]);
+ Assert.Equal("foo", result.RouteName);
+ Assert.False(result.Permanent);
+ }
+
+ [Fact]
+ public void RedirectToRouteWithNameAndRouteValueDictionary()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+ RouteValueDictionary values = new RouteValueDictionary() { { "Foo", "MyFoo" } };
+
+ // Act
+ RedirectToRouteResult result = controller.RedirectToRoute("foo", values);
+
+ // Assert
+ Assert.Single(result.RouteValues);
+ Assert.Equal("MyFoo", result.RouteValues["Foo"]);
+ Assert.NotSame(values, result.RouteValues);
+ Assert.Equal("foo", result.RouteName);
+ Assert.False(result.Permanent);
+ }
+
+ [Fact]
+ public void RedirectToRoutePermanentWithObjectDictionary()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+
+ // Act
+ RedirectToRouteResult result = controller.RedirectToRoutePermanent(routeValues: new { Foo = "Bar" });
+
+ // Assert
+ Assert.True(result.Permanent);
+ Assert.Equal("Bar", result.RouteValues["Foo"]);
+ }
+
+ [Fact]
+ public void RedirectToRoutePermanentWithRouteValueDictionary()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+
+ // Act
+ RedirectToRouteResult result = controller.RedirectToRoutePermanent(routeValues: new RouteValueDictionary(new { Foo = "Bar" }));
+
+ // Assert
+ Assert.True(result.Permanent);
+ Assert.Equal("Bar", result.RouteValues["Foo"]);
+ }
+
+ [Fact]
+ public void RedirectReturnsCorrectActionResult()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+
+ // Act & Assert
+ var result = controller.Redirect("http://www.contoso.com/");
+
+ // Assert
+ Assert.Equal("http://www.contoso.com/", result.Url);
+ Assert.False(result.Permanent);
+ }
+
+ [Fact]
+ public void RedirectPermanentReturnsCorrectActionResult()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+
+ // Act & Assert
+ var result = controller.RedirectPermanent("http://www.contoso.com/");
+
+ // Assert
+ Assert.Equal("http://www.contoso.com/", result.Url);
+ Assert.True(result.Permanent);
+ }
+
+ [Fact]
+ public void RedirectWithEmptyUrlThrows()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { controller.Redirect(String.Empty); },
+ "url");
+ }
+
+ [Fact]
+ public void RedirectPermanentWithEmptyUrlThrows()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { controller.RedirectPermanent(String.Empty); },
+ "url");
+ }
+
+ [Fact]
+ public void RedirectWithNullUrlThrows()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { controller.Redirect(url: null); },
+ "url");
+ }
+
+ [Fact]
+ public void RedirectPermanentWithNullUrlThrows()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { controller.RedirectPermanent(url: null); },
+ "url");
+ }
+
+ [Fact]
+ public void View()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+
+ // Act
+ ViewResult result = controller.View();
+
+ // Assert
+ Assert.Same(controller.ViewData, result.ViewData);
+ Assert.Same(controller.TempData, result.TempData);
+ Assert.Same(ViewEngines.Engines, result.ViewEngineCollection);
+ }
+
+ [Fact]
+ public void View_Model()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+ object viewItem = new object();
+
+ // Act
+ ViewResult result = controller.View(viewItem);
+
+ // Assert
+ Assert.Same(viewItem, result.ViewData.Model);
+ Assert.Same(controller.TempData, result.TempData);
+ }
+
+ [Fact]
+ public void View_ViewName()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+
+ // Act
+ ViewResult result = controller.View("Foo");
+
+ // Assert
+ Assert.Equal("Foo", result.ViewName);
+ Assert.Same(controller.ViewData, result.ViewData);
+ Assert.Same(controller.TempData, result.TempData);
+ }
+
+ [Fact]
+ public void View_ViewName_Model()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+ object viewItem = new object();
+
+ // Act
+ ViewResult result = controller.View("Foo", viewItem);
+
+ // Assert
+ Assert.Equal("Foo", result.ViewName);
+ Assert.Same(viewItem, result.ViewData.Model);
+ Assert.Same(controller.TempData, result.TempData);
+ }
+
+ [Fact]
+ public void View_ViewName_MasterViewName()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+
+ // Act
+ ViewResult result = controller.View("Foo", "Bar");
+
+ // Assert
+ Assert.Equal("Foo", result.ViewName);
+ Assert.Equal("Bar", result.MasterName);
+ Assert.Same(controller.ViewData, result.ViewData);
+ Assert.Same(controller.TempData, result.TempData);
+ }
+
+ [Fact]
+ public void View_ViewName_MasterViewName_Model()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+ object viewItem = new object();
+
+ // Act
+ ViewResult result = controller.View("Foo", "Bar", viewItem);
+
+ // Assert
+ Assert.Equal("Foo", result.ViewName);
+ Assert.Equal("Bar", result.MasterName);
+ Assert.Same(viewItem, result.ViewData.Model);
+ Assert.Same(controller.TempData, result.TempData);
+ }
+
+ [Fact]
+ public void View_View()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+ IView view = new Mock<IView>().Object;
+
+ // Act
+ ViewResult result = controller.View(view);
+
+ // Assert
+ Assert.Same(result.View, view);
+ Assert.Same(controller.ViewData, result.ViewData);
+ Assert.Same(controller.TempData, result.TempData);
+ }
+
+ [Fact]
+ public void View_View_Model()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+ IView view = new Mock<IView>().Object;
+ object model = new object();
+
+ // Act
+ ViewResult result = controller.View(view, model);
+
+ // Assert
+ Assert.Same(result.View, view);
+ Assert.Same(controller.ViewData, result.ViewData);
+ Assert.Same(controller.TempData, result.TempData);
+ Assert.Same(model, result.ViewData.Model);
+ }
+
+ [Fact]
+ public void View_ViewEngineCollection()
+ {
+ // Arrange
+ Controller controller = GetEmptyController();
+ ViewEngineCollection viewEngines = new ViewEngineCollection();
+ controller.ViewEngineCollection = viewEngines;
+
+ // Act
+ ViewResult result = controller.View();
+
+ // Assert
+ Assert.Same(viewEngines, result.ViewEngineCollection);
+ }
+
+ internal static void AddRequestParams(Mock<HttpRequestBase> requestMock, object paramValues)
+ {
+ PropertyDescriptorCollection props = TypeDescriptor.GetProperties(paramValues);
+ foreach (PropertyDescriptor prop in props)
+ {
+ requestMock.Setup(o => o[It.Is<string>(item => String.Equals(prop.Name, item, StringComparison.OrdinalIgnoreCase))]).Returns((string)prop.GetValue(paramValues));
+ }
+ }
+
+ [Fact]
+ public void TempDataGreetUserWithNoUserIDRedirects()
+ {
+ // Arrange
+ TempDataHomeController tempDataHomeController = new TempDataHomeController();
+
+ // Act
+ RedirectToRouteResult result = tempDataHomeController.GreetUser() as RedirectToRouteResult;
+ RouteValueDictionary values = result.RouteValues;
+
+ // Assert
+ Assert.True(values.ContainsKey("action"));
+ Assert.Equal("ErrorPage", values["action"]);
+ Assert.Empty(tempDataHomeController.TempData);
+ }
+
+ [Fact]
+ public void TempDataGreetUserWithUserIDCopiesToViewDataAndRenders()
+ {
+ // Arrange
+ TempDataHomeController tempDataHomeController = new TempDataHomeController();
+ tempDataHomeController.TempData["UserID"] = "TestUserID";
+
+ // Act
+ ViewResult result = tempDataHomeController.GreetUser() as ViewResult;
+ ViewDataDictionary viewData = tempDataHomeController.ViewData;
+
+ // Assert
+ Assert.Equal("GreetUser", result.ViewName);
+ Assert.NotNull(viewData);
+ Assert.True(viewData.ContainsKey("NewUserID"));
+ Assert.Equal("TestUserID", viewData["NewUserID"]);
+ }
+
+ [Fact]
+ public void TempDataIndexSavesUserIDAndRedirects()
+ {
+ // Arrange
+ TempDataHomeController tempDataHomeController = new TempDataHomeController();
+
+ // Act
+ RedirectToRouteResult result = tempDataHomeController.Index() as RedirectToRouteResult;
+ RouteValueDictionary values = result.RouteValues;
+
+ // Assert
+ Assert.True(values.ContainsKey("action"));
+ Assert.Equal("GreetUser", values["action"]);
+
+ Assert.True(tempDataHomeController.TempData.ContainsKey("UserID"));
+ Assert.Equal("user123", tempDataHomeController.TempData["UserID"]);
+ }
+
+ [Fact]
+ public void TempDataSavedWhenControllerThrows()
+ {
+ // Arrange
+ BrokenController controller = new BrokenController() { ValidateRequest = false };
+ Mock<HttpContextBase> mockContext = HttpContextHelpers.GetMockHttpContext();
+ HttpSessionStateBase session = GetEmptySession();
+ mockContext.Setup(o => o.Session).Returns(session);
+ RouteData rd = new RouteData();
+ rd.Values.Add("action", "Crash");
+ controller.ControllerContext = new ControllerContext(mockContext.Object, rd, controller);
+
+ // Assert
+ Assert.Throws<InvalidOperationException>(
+ delegate { ((IController)controller).Execute(controller.ControllerContext.RequestContext); });
+ Assert.NotEqual(mockContext.Object.Session[SessionStateTempDataProvider.TempDataSessionStateKey], null);
+ TempDataDictionary tempData = new TempDataDictionary();
+ tempData.Load(controller.ControllerContext, controller.TempDataProvider);
+ Assert.Equal(tempData["Key1"], "Value1");
+ }
+
+ [Fact]
+ public void TempDataMovedToPreviousTempDataInDestinationController()
+ {
+ // Arrange
+ Mock<Controller> mockController = new Mock<Controller>() { CallBase = true };
+ Mock<HttpContextBase> mockContext = new Mock<HttpContextBase>();
+ HttpSessionStateBase session = GetEmptySession();
+ mockContext.Setup(o => o.Session).Returns(session);
+ mockController.Object.ControllerContext = new ControllerContext(mockContext.Object, new RouteData(), mockController.Object);
+
+ // Act
+ mockController.Object.TempData.Add("Key", "Value");
+ mockController.Object.TempData.Save(mockController.Object.ControllerContext, mockController.Object.TempDataProvider);
+
+ // Assert
+ Assert.True(mockController.Object.TempData.ContainsKey("Key"));
+ Assert.True(mockController.Object.TempData.ContainsValue("Value"));
+
+ // Instantiate "destination" controller with the same session state and see that it gets the temp data
+ Mock<Controller> mockDestinationController = new Mock<Controller>() { CallBase = true };
+ Mock<HttpContextBase> mockDestinationContext = new Mock<HttpContextBase>();
+ mockDestinationContext.Setup(o => o.Session).Returns(session);
+ mockDestinationController.Object.ControllerContext = new ControllerContext(mockDestinationContext.Object, new RouteData(), mockDestinationController.Object);
+ mockDestinationController.Object.TempData.Load(mockDestinationController.Object.ControllerContext, mockDestinationController.Object.TempDataProvider);
+
+ // Assert
+ Assert.True(mockDestinationController.Object.TempData.ContainsKey("Key"));
+
+ // Act
+ mockDestinationController.Object.TempData["NewKey"] = "NewValue";
+ Assert.True(mockDestinationController.Object.TempData.ContainsKey("NewKey"));
+ mockDestinationController.Object.TempData.Save(mockDestinationController.Object.ControllerContext, mockDestinationController.Object.TempDataProvider);
+
+ // Instantiate "second destination" controller with the same session state and see that it gets the temp data
+ Mock<Controller> mockSecondDestinationController = new Mock<Controller>() { CallBase = true };
+ Mock<HttpContextBase> mockSecondDestinationContext = new Mock<HttpContextBase>();
+ mockSecondDestinationContext.Setup(o => o.Session).Returns(session);
+ mockSecondDestinationController.Object.ControllerContext = new ControllerContext(mockSecondDestinationContext.Object, new RouteData(), mockSecondDestinationController.Object);
+ mockSecondDestinationController.Object.TempData.Load(mockSecondDestinationController.Object.ControllerContext, mockSecondDestinationController.Object.TempDataProvider);
+
+ // Assert
+ Assert.True(mockSecondDestinationController.Object.TempData.ContainsKey("Key"));
+ Assert.True(mockSecondDestinationController.Object.TempData.ContainsKey("NewKey"));
+ }
+
+ [Fact]
+ public void TempDataRemovesKeyWhenRead()
+ {
+ // Arrange
+ Mock<Controller> mockController = new Mock<Controller>() { CallBase = true };
+ Mock<HttpContextBase> mockContext = new Mock<HttpContextBase>();
+ HttpSessionStateBase session = GetEmptySession();
+ mockContext.Setup(o => o.Session).Returns(session);
+ mockController.Object.ControllerContext = new ControllerContext(mockContext.Object, new RouteData(), mockController.Object);
+
+ // Act
+ mockController.Object.TempData.Add("Key", "Value");
+ mockController.Object.TempData.Save(mockController.Object.ControllerContext, mockController.Object.TempDataProvider);
+
+ // Assert
+ Assert.True(mockController.Object.TempData.ContainsKey("Key"));
+ Assert.True(mockController.Object.TempData.ContainsValue("Value"));
+
+ // Instantiate "destination" controller with the same session state and see that it gets the temp data
+ Mock<Controller> mockDestinationController = new Mock<Controller>() { CallBase = true };
+ Mock<HttpContextBase> mockDestinationContext = new Mock<HttpContextBase>();
+ mockDestinationContext.Setup(o => o.Session).Returns(session);
+ mockDestinationController.Object.ControllerContext = new ControllerContext(mockDestinationContext.Object, new RouteData(), mockDestinationController.Object);
+ mockDestinationController.Object.TempData.Load(mockDestinationController.Object.ControllerContext, mockDestinationController.Object.TempDataProvider);
+
+ // Assert
+ Assert.True(mockDestinationController.Object.TempData.ContainsKey("Key"));
+
+ // Act
+ object value = mockDestinationController.Object.TempData["Key"];
+ mockDestinationController.Object.TempData.Save(mockDestinationController.Object.ControllerContext, mockDestinationController.Object.TempDataProvider);
+
+ // Instantiate "second destination" controller with the same session state and see that it gets the temp data
+ Mock<Controller> mockSecondDestinationController = new Mock<Controller>() { CallBase = true };
+ Mock<HttpContextBase> mockSecondDestinationContext = new Mock<HttpContextBase>();
+ mockSecondDestinationContext.Setup(o => o.Session).Returns(session);
+ mockSecondDestinationController.Object.ControllerContext = new ControllerContext(mockSecondDestinationContext.Object, new RouteData(), mockSecondDestinationController.Object);
+ mockSecondDestinationController.Object.TempData.Load(mockSecondDestinationController.Object.ControllerContext, mockSecondDestinationController.Object.TempDataProvider);
+
+ // Assert
+ Assert.False(mockSecondDestinationController.Object.TempData.ContainsKey("Key"));
+ }
+
+ [Fact]
+ public void TempDataValidForSingleControllerWhenSessionStateDisabled()
+ {
+ // Arrange
+ Mock<Controller> mockController = new Mock<Controller>();
+ Mock<HttpContextBase> mockContext = new Mock<HttpContextBase>();
+ HttpSessionStateBase session = null;
+ mockContext.Setup(o => o.Session).Returns(session);
+ mockController.Object.ControllerContext = new ControllerContext(mockContext.Object, new RouteData(), mockController.Object);
+ mockController.Object.TempData = new TempDataDictionary();
+
+ // Act
+ mockController.Object.TempData["Key"] = "Value";
+
+ // Assert
+ Assert.True(mockController.Object.TempData.ContainsKey("Key"));
+ }
+
+ [Fact]
+ public void TryUpdateModelCallsModelBinderForModel()
+ {
+ // Arrange
+ MyModel myModel = new MyModelSubclassed();
+ IValueProvider valueProvider = new SimpleValueProvider();
+
+ Controller controller = new EmptyController();
+ controller.ControllerContext = GetControllerContext("someAction");
+
+ // Act
+ bool returned = controller.TryUpdateModel(myModel, "somePrefix", new[] { "prop1", "prop2" }, null, valueProvider);
+
+ // Assert
+ Assert.True(returned);
+ Assert.Equal(valueProvider, myModel.BindingContext.ValueProvider);
+ Assert.Equal("somePrefix", myModel.BindingContext.ModelName);
+ Assert.Equal(controller.ModelState, myModel.BindingContext.ModelState);
+ Assert.Equal(typeof(MyModel), myModel.BindingContext.ModelType);
+ Assert.True(myModel.BindingContext.PropertyFilter("prop1"));
+ Assert.True(myModel.BindingContext.PropertyFilter("prop2"));
+ Assert.False(myModel.BindingContext.PropertyFilter("prop3"));
+ }
+
+ [Fact]
+ public void TryUpdateModelReturnsFalseIfModelStateInvalid()
+ {
+ // Arrange
+ MyModel myModel = new MyModelSubclassed();
+
+ Controller controller = new EmptyController();
+ controller.ModelState.AddModelError("key", "some exception message");
+
+ // Act
+ bool returned = controller.TryUpdateModel(myModel, new SimpleValueProvider());
+
+ // Assert
+ Assert.False(returned);
+ }
+
+ [Fact]
+ public void TryUpdateModelSuppliesControllerValueProviderIfNoValueProviderSpecified()
+ {
+ // Arrange
+ MyModel myModel = new MyModelSubclassed();
+ IValueProvider valueProvider = new SimpleValueProvider();
+
+ Controller controller = new EmptyController();
+ controller.ValueProvider = valueProvider;
+
+ // Act
+ bool returned = controller.TryUpdateModel(myModel, "somePrefix", new[] { "prop1", "prop2" });
+
+ // Assert
+ Assert.True(returned);
+ Assert.Equal(valueProvider, myModel.BindingContext.ValueProvider);
+ }
+
+ [Fact]
+ public void TryUpdateModelSuppliesEmptyModelNameIfNoPrefixSpecified()
+ {
+ // Arrange
+ MyModel myModel = new MyModelSubclassed();
+ Controller controller = new EmptyController();
+
+ // Act
+ bool returned = controller.TryUpdateModel(myModel, new[] { "prop1", "prop2" }, new SimpleValueProvider());
+
+ // Assert
+ Assert.True(returned);
+ Assert.Equal(String.Empty, myModel.BindingContext.ModelName);
+ }
+
+ [Fact]
+ public void TryUpdateModelThrowsIfModelIsNull()
+ {
+ // Arrange
+ Controller controller = new EmptyController();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { controller.TryUpdateModel<object>(null, new SimpleValueProvider()); }, "model");
+ }
+
+ [Fact]
+ public void TryUpdateModelThrowsIfValueProviderIsNull()
+ {
+ // Arrange
+ Controller controller = new EmptyController();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { controller.TryUpdateModel(new object(), null, null, null, null); }, "valueProvider");
+ }
+
+ [Fact]
+ public void UpdateModelReturnsIfModelStateValid()
+ {
+ // Arrange
+ MyModel myModel = new MyModelSubclassed();
+ Controller controller = new EmptyController();
+
+ // Act
+ controller.UpdateModel(myModel, new SimpleValueProvider());
+
+ // Assert
+ // nothing to do - if we got here, the test passed
+ }
+
+ [Fact]
+ public void TryUpdateModelWithoutBindPropertiesImpliesAllPropertiesAreUpdateable()
+ {
+ // Arrange
+ MyModel myModel = new MyModelSubclassed();
+ Controller controller = new EmptyController();
+
+ // Act
+ bool returned = controller.TryUpdateModel(myModel, "somePrefix", new SimpleValueProvider());
+
+ // Assert
+ Assert.True(returned);
+ Assert.True(myModel.BindingContext.PropertyFilter("prop1"));
+ Assert.True(myModel.BindingContext.PropertyFilter("prop2"));
+ Assert.True(myModel.BindingContext.PropertyFilter("prop3"));
+ }
+
+ [Fact]
+ public void UpdateModelSuppliesControllerValueProviderIfNoValueProviderSpecified()
+ {
+ // Arrange
+ MyModel myModel = new MyModelSubclassed();
+ IValueProvider valueProvider = new SimpleValueProvider();
+
+ Controller controller = new EmptyController() { ValueProvider = valueProvider };
+
+ // Act
+ controller.UpdateModel(myModel, "somePrefix", new[] { "prop1", "prop2" });
+
+ // Assert
+ Assert.Equal(valueProvider, myModel.BindingContext.ValueProvider);
+ }
+
+ [Fact]
+ public void UpdateModelThrowsIfModelStateInvalid()
+ {
+ // Arrange
+ MyModel myModel = new MyModelSubclassed();
+
+ Controller controller = new EmptyController();
+ controller.ModelState.AddModelError("key", "some exception message");
+
+ // Act & assert
+ Assert.Throws<InvalidOperationException>(
+ delegate { controller.UpdateModel(myModel, new SimpleValueProvider()); },
+ "The model of type 'System.Web.Mvc.Test.ControllerTest+MyModel' could not be updated.");
+ }
+
+ [Fact]
+ public void UpdateModelWithoutBindPropertiesImpliesAllPropertiesAreUpdateable()
+ {
+ // Arrange
+ MyModel myModel = new MyModelSubclassed();
+ Controller controller = new EmptyController();
+
+ // Act
+ controller.UpdateModel(myModel, "somePrefix", new SimpleValueProvider());
+
+ // Assert
+ Assert.True(myModel.BindingContext.PropertyFilter("prop1"));
+ Assert.True(myModel.BindingContext.PropertyFilter("prop2"));
+ Assert.True(myModel.BindingContext.PropertyFilter("prop3"));
+ }
+
+ [Fact]
+ public void Json()
+ {
+ // Arrange
+ MyModel model = new MyModel();
+ Controller controller = new EmptyController();
+
+ // Act
+ JsonResult result = controller.Json(model);
+
+ // Assert
+ Assert.Same(model, result.Data);
+ Assert.Null(result.ContentType);
+ Assert.Null(result.ContentEncoding);
+ Assert.Equal(JsonRequestBehavior.DenyGet, result.JsonRequestBehavior);
+ }
+
+ [Fact]
+ public void JsonWithContentType()
+ {
+ // Arrange
+ MyModel model = new MyModel();
+ Controller controller = new EmptyController();
+
+ // Act
+ JsonResult result = controller.Json(model, "text/xml");
+
+ // Assert
+ Assert.Same(model, result.Data);
+ Assert.Equal("text/xml", result.ContentType);
+ Assert.Null(result.ContentEncoding);
+ Assert.Equal(JsonRequestBehavior.DenyGet, result.JsonRequestBehavior);
+ }
+
+ [Fact]
+ public void JsonWithContentTypeAndEncoding()
+ {
+ // Arrange
+ MyModel model = new MyModel();
+ Controller controller = new EmptyController();
+
+ // Act
+ JsonResult result = controller.Json(model, "text/xml", Encoding.UTF32);
+
+ // Assert
+ Assert.Same(model, result.Data);
+ Assert.Equal("text/xml", result.ContentType);
+ Assert.Equal(Encoding.UTF32, result.ContentEncoding);
+ Assert.Equal(JsonRequestBehavior.DenyGet, result.JsonRequestBehavior);
+ }
+
+ [Fact]
+ public void JsonWithBehavior()
+ {
+ // Arrange
+ MyModel model = new MyModel();
+ Controller controller = new EmptyController();
+
+ // Act
+ JsonResult result = controller.Json(model, JsonRequestBehavior.AllowGet);
+
+ // Assert
+ Assert.Same(model, result.Data);
+ Assert.Null(result.ContentType);
+ Assert.Null(result.ContentEncoding);
+ Assert.Equal(JsonRequestBehavior.AllowGet, result.JsonRequestBehavior);
+ }
+
+ [Fact]
+ public void JsonWithContentTypeAndBehavior()
+ {
+ // Arrange
+ MyModel model = new MyModel();
+ Controller controller = new EmptyController();
+
+ // Act
+ JsonResult result = controller.Json(model, "text/xml", JsonRequestBehavior.AllowGet);
+
+ // Assert
+ Assert.Same(model, result.Data);
+ Assert.Equal("text/xml", result.ContentType);
+ Assert.Null(result.ContentEncoding);
+ Assert.Equal(JsonRequestBehavior.AllowGet, result.JsonRequestBehavior);
+ }
+
+ [Fact]
+ public void JsonWithContentTypeAndEncodingAndBehavior()
+ {
+ // Arrange
+ MyModel model = new MyModel();
+ Controller controller = new EmptyController();
+
+ // Act
+ JsonResult result = controller.Json(model, "text/xml", Encoding.UTF32, JsonRequestBehavior.AllowGet);
+
+ // Assert
+ Assert.Same(model, result.Data);
+ Assert.Equal("text/xml", result.ContentType);
+ Assert.Equal(Encoding.UTF32, result.ContentEncoding);
+ Assert.Equal(JsonRequestBehavior.AllowGet, result.JsonRequestBehavior);
+ }
+
+ [Fact]
+ public void ExecuteDoesNotCallTempDataLoadOrSave()
+ {
+ // Arrange
+ TempDataDictionary tempData = new TempDataDictionary();
+ ViewContext viewContext = new ViewContext { TempData = tempData };
+ RouteData routeData = new RouteData();
+ routeData.DataTokens[ControllerContext.ParentActionViewContextToken] = viewContext;
+ routeData.Values["action"] = "SimpleAction";
+ RequestContext requestContext = new RequestContext(HttpContextHelpers.GetMockHttpContext().Object, routeData);
+ // Strict == no default implementations == calls to Load & Save are not allowed
+ Mock<ITempDataProvider> tempDataProvider = new Mock<ITempDataProvider>(MockBehavior.Strict);
+ SimpleController controller = new SimpleController();
+ controller.ValidateRequest = false;
+
+ // Act
+ ((IController)controller).Execute(requestContext);
+
+ // Assert
+ tempDataProvider.Verify();
+ }
+
+ // Model validation
+
+ [Fact]
+ public void TryValidateModelGuardClauses()
+ {
+ // Arrange
+ Controller controller = new SimpleController();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => controller.TryValidateModel(null),
+ "model");
+ }
+
+ [Fact]
+ public void TryValidateModelWithValidModel()
+ {
+ // Arrange
+ Controller controller = new SimpleController();
+ TryValidateModelModel model = new TryValidateModelModel { IntegerProperty = 15 };
+
+ // Act
+ bool result = controller.TryValidateModel(model);
+
+ // Assert
+ Assert.True(result);
+ Assert.True(controller.ModelState.IsValid);
+ }
+
+ [Fact]
+ public void TryValidateModelWithInvalidModel()
+ {
+ // Arrange
+ Controller controller = new SimpleController();
+ TryValidateModelModel model = new TryValidateModelModel { IntegerProperty = 5 };
+
+ // Act
+ bool result = controller.TryValidateModel(model, "Prefix");
+
+ // Assert
+ Assert.False(result);
+ Assert.Equal("Out of range!", controller.ModelState["Prefix.IntegerProperty"].Errors[0].ErrorMessage);
+ }
+
+ [Fact]
+ public void ValidateModelGuardClauses()
+ {
+ // Arrange
+ Controller controller = new SimpleController();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => controller.ValidateModel(null),
+ "model");
+ }
+
+ [Fact]
+ public void ValidateModelWithValidModel()
+ {
+ // Arrange
+ Controller controller = new SimpleController();
+ TryValidateModelModel model = new TryValidateModelModel { IntegerProperty = 15 };
+
+ // Act
+ controller.ValidateModel(model);
+
+ // Assert
+ Assert.True(controller.ModelState.IsValid);
+ }
+
+ [Fact]
+ public void ValidateModelWithInvalidModel()
+ {
+ // Arrange
+ Controller controller = new SimpleController();
+ TryValidateModelModel model = new TryValidateModelModel { IntegerProperty = 5 };
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ () => controller.ValidateModel(model, "Prefix"),
+ "The model of type '" + model.GetType().FullName + "' is not valid.");
+
+ Assert.Equal("Out of range!", controller.ModelState["Prefix.IntegerProperty"].Errors[0].ErrorMessage);
+ }
+
+ [Fact]
+ public void ValidateControllerUsesCachedResolver()
+ {
+ var controller = new EmptyController();
+
+ var resolver = controller.Resolver;
+
+ Assert.Equal(DependencyResolver.CurrentCache.GetType(), resolver.GetType());
+ }
+
+
+ [Fact]
+ public void CreateActionInvokerCallsIntoResolverInstance()
+ {
+ // Controller uses an IDependencyResolver to create an IActionInvoker.
+ var controller = new EmptyController();
+ Mock<IDependencyResolver> resolverMock = new Mock<IDependencyResolver>();
+ Mock<IAsyncActionInvoker> actionInvokerMock = new Mock<IAsyncActionInvoker>();
+ resolverMock.Setup(r => r.GetService(typeof(IAsyncActionInvoker))).Returns(actionInvokerMock.Object);
+ controller.Resolver = resolverMock.Object;
+
+ var ai = controller.CreateActionInvoker();
+
+ resolverMock.Verify(r => r.GetService(typeof(IAsyncActionInvoker)), Times.Once());
+ Assert.Same(actionInvokerMock.Object, ai);
+ }
+
+ [Fact]
+ public void CreateActionInvokerCallsIntoResolverInstanceAndCreatesANewOneIfNecessary()
+ {
+ // If IDependencyResolver is set, but empty, falls back and still creates.
+ var controller = new EmptyController();
+ Mock<IDependencyResolver> resolverMock = new Mock<IDependencyResolver>();
+ resolverMock.Setup(r => r.GetService(typeof(IAsyncActionInvoker))).Returns(null);
+ controller.Resolver = resolverMock.Object;
+
+ IActionInvoker ai = controller.CreateActionInvoker();
+
+ resolverMock.Verify(r => r.GetService(typeof(IAsyncActionInvoker)), Times.Once());
+ Assert.NotNull(ai);
+ }
+
+ [Fact]
+ public void CreateTempProviderWithResolver()
+ {
+ // Controller uses an IDependencyResolver to create an IActionInvoker.
+ var controller = new EmptyController();
+ Mock<IDependencyResolver> resolverMock = new Mock<IDependencyResolver>();
+ Mock<ITempDataProvider> tempMock = new Mock<ITempDataProvider>();
+ resolverMock.Setup(r => r.GetService(typeof(ITempDataProvider))).Returns(tempMock.Object);
+ controller.Resolver = resolverMock.Object;
+
+ ITempDataProvider temp = controller.CreateTempDataProvider();
+
+ resolverMock.Verify(r => r.GetService(typeof(ITempDataProvider)), Times.Once());
+ Assert.Same(tempMock.Object, temp);
+ }
+
+ private class TryValidateModelModel
+ {
+ [Range(10, 20, ErrorMessage = "Out of range!")]
+ public int IntegerProperty { get; set; }
+ }
+
+ // Helpers
+
+ private class SimpleController : Controller
+ {
+ public SimpleController()
+ {
+ ControllerContext = new ControllerContext { Controller = this };
+ }
+
+ public void SimpleAction()
+ {
+ }
+ }
+
+ private static ControllerContext GetControllerContext(string actionName)
+ {
+ RouteData rd = new RouteData();
+ rd.Values["action"] = actionName;
+
+ Mock<HttpContextBase> mockHttpContext = HttpContextHelpers.GetMockHttpContext();
+ mockHttpContext.Setup(c => c.Session).Returns((HttpSessionStateBase)null);
+
+ return new ControllerContext(mockHttpContext.Object, rd, new Mock<Controller>().Object);
+ }
+
+ private static ControllerContext GetControllerContext(string actionName, string controllerName)
+ {
+ RouteData rd = new RouteData();
+ rd.Values["action"] = actionName;
+ rd.Values["controller"] = controllerName;
+
+ Mock<HttpContextBase> mockHttpContext = new Mock<HttpContextBase>();
+ mockHttpContext.Setup(c => c.Session).Returns((HttpSessionStateBase)null);
+
+ return new ControllerContext(mockHttpContext.Object, rd, new Mock<Controller>().Object);
+ }
+
+ private static Controller GetEmptyController()
+ {
+ ControllerContext context = GetControllerContext("Foo");
+ var controller = new EmptyController()
+ {
+ ControllerContext = context,
+ RouteCollection = new RouteCollection(),
+ TempData = new TempDataDictionary(),
+ TempDataProvider = new SessionStateTempDataProvider()
+ };
+ return controller;
+ }
+
+ private static HttpSessionStateBase GetEmptySession()
+ {
+ HttpSessionStateMock mockSession = new HttpSessionStateMock();
+ return mockSession;
+ }
+
+ private sealed class HttpSessionStateMock : HttpSessionStateBase
+ {
+ private Hashtable _sessionData = new Hashtable(StringComparer.OrdinalIgnoreCase);
+
+ public override void Remove(string name)
+ {
+ Assert.Equal<string>(SessionStateTempDataProvider.TempDataSessionStateKey, name);
+ _sessionData.Remove(name);
+ }
+
+ public override object this[string name]
+ {
+ get
+ {
+ Assert.Equal<string>(SessionStateTempDataProvider.TempDataSessionStateKey, name);
+ return _sessionData[name];
+ }
+ set
+ {
+ Assert.Equal<string>(SessionStateTempDataProvider.TempDataSessionStateKey, name);
+ _sessionData[name] = value;
+ }
+ }
+ }
+
+ public class Person
+ {
+ public string Name { get; set; }
+ public int Age { get; set; }
+ }
+
+ private class EmptyController : Controller
+ {
+ public new void HandleUnknownAction(string actionName)
+ {
+ base.HandleUnknownAction(actionName);
+ }
+
+ public void PublicInitialize(RequestContext requestContext)
+ {
+ base.Initialize(requestContext);
+ }
+
+ // Test can expose protected method as public.
+ public new IActionInvoker CreateActionInvoker()
+ {
+ return base.CreateActionInvoker();
+ }
+
+ public new ITempDataProvider CreateTempDataProvider()
+ {
+ return base.CreateTempDataProvider();
+ }
+ }
+
+
+ private sealed class UnknownActionController : Controller
+ {
+ public bool WasCalled;
+
+ protected override void HandleUnknownAction(string actionName)
+ {
+ WasCalled = true;
+ }
+ }
+
+ private sealed class TempDataHomeController : Controller
+ {
+ public ActionResult Index()
+ {
+ // Save UserID into TempData and redirect to greeting page
+ TempData["UserID"] = "user123";
+ return RedirectToAction("GreetUser");
+ }
+
+ public ActionResult GreetUser()
+ {
+ // Check that the UserID is present. If it's not
+ // there, redirect to error page. If it is, show
+ // the greet user view.
+ if (!TempData.ContainsKey("UserID"))
+ {
+ return RedirectToAction("ErrorPage");
+ }
+ ViewData["NewUserID"] = TempData["UserID"];
+ return View("GreetUser");
+ }
+ }
+
+ public class BrokenController : Controller
+ {
+ public BrokenController()
+ {
+ ActionInvoker = new ControllerActionInvoker()
+ {
+ DescriptorCache = new ControllerDescriptorCache()
+ };
+ }
+
+ public ActionResult Crash()
+ {
+ TempData["Key1"] = "Value1";
+ throw new InvalidOperationException("Crashing....");
+ }
+ }
+
+ private sealed class TestRouteController : Controller
+ {
+ public ActionResult Index()
+ {
+ return RedirectToAction("SomeAction");
+ }
+ }
+
+ [ModelBinder(typeof(MyModelBinder))]
+ private class MyModel
+ {
+ public ControllerContext ControllerContext;
+ public ModelBindingContext BindingContext;
+ }
+
+ private class MyModelSubclassed : MyModel
+ {
+ }
+
+ private class MyModelBinder : IModelBinder
+ {
+ public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
+ {
+ MyModel myModel = (MyModel)bindingContext.Model;
+ myModel.ControllerContext = controllerContext;
+ myModel.BindingContext = bindingContext;
+ return myModel;
+ }
+ }
+ }
+
+ internal class EmptyTempDataProvider : ITempDataProvider
+ {
+ public void SaveTempData(ControllerContext controllerContext, IDictionary<string, object> values)
+ {
+ }
+
+ public IDictionary<string, object> LoadTempData(ControllerContext controllerContext)
+ {
+ return new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/DataAnnotationsModelMetadataProviderTest.cs b/test/System.Web.Mvc.Test/Test/DataAnnotationsModelMetadataProviderTest.cs
new file mode 100644
index 00000000..f7c4f69c
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/DataAnnotationsModelMetadataProviderTest.cs
@@ -0,0 +1,10 @@
+namespace System.Web.Mvc.Test
+{
+ public class DataAnnotationsModelMetadataProviderTest : DataAnnotationsModelMetadataProviderTestBase
+ {
+ protected override AssociatedMetadataProvider MakeProvider()
+ {
+ return new DataAnnotationsModelMetadataProvider();
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/DataAnnotationsModelMetadataProviderTestBase.cs b/test/System.Web.Mvc.Test/Test/DataAnnotationsModelMetadataProviderTestBase.cs
new file mode 100644
index 00000000..90958788
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/DataAnnotationsModelMetadataProviderTestBase.cs
@@ -0,0 +1,614 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public abstract class DataAnnotationsModelMetadataProviderTestBase
+ {
+ protected abstract AssociatedMetadataProvider MakeProvider();
+
+ [Fact]
+ public void GetMetadataForPropertiesSetTypesAndPropertyNames()
+ {
+ // Arrange
+ var provider = MakeProvider();
+
+ // Act
+ IEnumerable<ModelMetadata> result = provider.GetMetadataForProperties("foo", typeof(string));
+
+ // Assert
+ Assert.True(result.Any(m => m.ModelType == typeof(int)
+ && m.PropertyName == "Length"
+ && (int)m.Model == 3));
+ }
+
+ [Fact]
+ public void GetMetadataForPropertySetsTypeAndPropertyName()
+ {
+ // Arrange
+ var provider = MakeProvider();
+
+ // Act
+ ModelMetadata result = provider.GetMetadataForProperty(null, typeof(string), "Length");
+
+ // Assert
+ Assert.Equal(typeof(int), result.ModelType);
+ Assert.Equal("Length", result.PropertyName);
+ }
+
+ [Fact]
+ public void GetMetadataForTypeSetsTypeWithNullPropertyName()
+ {
+ // Arrange
+ var provider = MakeProvider();
+
+ // Act
+ ModelMetadata result = provider.GetMetadataForType(null, typeof(string));
+
+ // Assert
+ Assert.Equal(typeof(string), result.ModelType);
+ Assert.Null(result.PropertyName);
+ }
+
+ // [HiddenInput] tests
+
+ class HiddenModel
+ {
+ public int NoAttribute { get; set; }
+
+ [HiddenInput]
+ public int DefaultHidden { get; set; }
+
+ [HiddenInput(DisplayValue = false)]
+ public int HiddenWithDisplayValueFalse { get; set; }
+
+ [HiddenInput]
+ [UIHint("CustomUIHint")]
+ public int HiddenAndUIHint { get; set; }
+ }
+
+ [Fact]
+ public void HiddenAttributeSetsTemplateHintAndHideSurroundingHtml()
+ {
+ // Arrange
+ var provider = MakeProvider();
+
+ // Act & Assert
+ ModelMetadata noAttributeMetadata = provider.GetMetadataForProperty(null, typeof(HiddenModel), "NoAttribute");
+ Assert.Null(noAttributeMetadata.TemplateHint);
+ Assert.False(noAttributeMetadata.HideSurroundingHtml);
+
+ ModelMetadata defaultHiddenMetadata = provider.GetMetadataForProperty(null, typeof(HiddenModel), "DefaultHidden");
+ Assert.Equal("HiddenInput", defaultHiddenMetadata.TemplateHint);
+ Assert.False(defaultHiddenMetadata.HideSurroundingHtml);
+
+ ModelMetadata hiddenWithDisplayValueFalseMetadata = provider.GetMetadataForProperty(null, typeof(HiddenModel), "HiddenWithDisplayValueFalse");
+ Assert.Equal("HiddenInput", hiddenWithDisplayValueFalseMetadata.TemplateHint);
+ Assert.True(hiddenWithDisplayValueFalseMetadata.HideSurroundingHtml);
+
+ // [UIHint] overrides the template hint from [Hidden]
+ Assert.Equal("CustomUIHint", provider.GetMetadataForProperty(null, typeof(HiddenModel), "HiddenAndUIHint").TemplateHint);
+ }
+
+ // [UIHint] tests
+
+ class UIHintModel
+ {
+ public int NoAttribute { get; set; }
+
+ [UIHint("MyCustomTemplate")]
+ public int DefaultUIHint { get; set; }
+
+ [UIHint("MyMvcTemplate", "MVC")]
+ public int MvcUIHint { get; set; }
+
+ [UIHint("MyWebFormsTemplate", "WebForms")]
+ public int NoMvcUIHint { get; set; }
+
+ [UIHint("MyDefaultTemplate")]
+ [UIHint("MyWebFormsTemplate", "WebForms")]
+ [UIHint("MyMvcTemplate", "MVC")]
+ public int MultipleUIHint { get; set; }
+ }
+
+ [Fact]
+ public void UIHintAttributeSetsTemplateHint()
+ {
+ // Arrange
+ var provider = MakeProvider();
+
+ // Act & Assert
+ Assert.Null(provider.GetMetadataForProperty(null, typeof(UIHintModel), "NoAttribute").TemplateHint);
+ Assert.Equal("MyCustomTemplate", provider.GetMetadataForProperty(null, typeof(UIHintModel), "DefaultUIHint").TemplateHint);
+ Assert.Equal("MyMvcTemplate", provider.GetMetadataForProperty(null, typeof(UIHintModel), "MvcUIHint").TemplateHint);
+ Assert.Null(provider.GetMetadataForProperty(null, typeof(UIHintModel), "NoMvcUIHint").TemplateHint);
+
+ Assert.Equal("MyMvcTemplate", provider.GetMetadataForProperty(null, typeof(UIHintModel), "MultipleUIHint").TemplateHint);
+ }
+
+ // [DataType] tests
+
+ class DataTypeModel
+ {
+ public int NoAttribute { get; set; }
+
+ [DataType(DataType.EmailAddress)]
+ public int EmailAddressProperty { get; set; }
+
+ [DataType("CustomDataType")]
+ public int CustomDataTypeProperty { get; set; }
+ }
+
+ [Fact]
+ public void DataTypeAttributeSetsDataTypeName()
+ {
+ // Arrange
+ var provider = MakeProvider();
+
+ // Act & Assert
+ Assert.Null(provider.GetMetadataForProperty(null, typeof(DataTypeModel), "NoAttribute").DataTypeName);
+ Assert.Equal("EmailAddress", provider.GetMetadataForProperty(null, typeof(DataTypeModel), "EmailAddressProperty").DataTypeName);
+ Assert.Equal("CustomDataType", provider.GetMetadataForProperty(null, typeof(DataTypeModel), "CustomDataTypeProperty").DataTypeName);
+ }
+
+ // [ReadOnly] & [Editable] tests
+
+ class ReadOnlyModel
+ {
+ public int NoAttributes { get; set; }
+
+ [ReadOnly(true)]
+ public int ReadOnlyAttribute { get; set; }
+
+ [Editable(false)]
+ public int EditableAttribute { get; set; }
+
+ [ReadOnly(true)]
+ [Editable(true)]
+ public int BothAttributes { get; set; }
+
+ // Editable trumps ReadOnly
+ }
+
+ [Fact]
+ public void ReadOnlyTests()
+ {
+ // Arrange
+ var provider = MakeProvider();
+
+ // Act & Assert
+ Assert.False(provider.GetMetadataForProperty(null, typeof(ReadOnlyModel), "NoAttributes").IsReadOnly);
+ Assert.True(provider.GetMetadataForProperty(null, typeof(ReadOnlyModel), "ReadOnlyAttribute").IsReadOnly);
+ Assert.True(provider.GetMetadataForProperty(null, typeof(ReadOnlyModel), "EditableAttribute").IsReadOnly);
+ Assert.False(provider.GetMetadataForProperty(null, typeof(ReadOnlyModel), "BothAttributes").IsReadOnly);
+ }
+
+ // [DisplayFormat] tests
+
+ class DisplayFormatModel
+ {
+ public int NoAttribute { get; set; }
+
+ [DisplayFormat(NullDisplayText = "(null value)")]
+ public int NullDisplayText { get; set; }
+
+ [DisplayFormat(DataFormatString = "Data {0} format")]
+ public int DisplayFormatString { get; set; }
+
+ [DisplayFormat(DataFormatString = "Data {0} format", ApplyFormatInEditMode = true)]
+ public int DisplayAndEditFormatString { get; set; }
+
+ [DisplayFormat(ConvertEmptyStringToNull = true)]
+ public int ConvertEmptyStringToNullTrue { get; set; }
+
+ [DisplayFormat(ConvertEmptyStringToNull = false)]
+ public int ConvertEmptyStringToNullFalse { get; set; }
+
+ [DataType(DataType.Currency)]
+ public int DataTypeWithoutDisplayFormatOverride { get; set; }
+
+ [DataType(DataType.Currency)]
+ [DisplayFormat(DataFormatString = "format override")]
+ public int DataTypeWithDisplayFormatOverride { get; set; }
+
+ [DisplayFormat(HtmlEncode = true)]
+ public int HtmlEncodeTrue { get; set; }
+
+ [DisplayFormat(HtmlEncode = false)]
+ public int HtmlEncodeFalse { get; set; }
+
+ [DataType(DataType.Currency)]
+ [DisplayFormat(HtmlEncode = false)]
+ public int HtmlEncodeFalseWithDataType { get; set; }
+
+ // DataType trumps DisplayFormat.HtmlEncode
+ }
+
+ [Fact]
+ public void DisplayFormatAttributetSetsNullDisplayText()
+ {
+ // Arrange
+ var provider = MakeProvider();
+
+ // Act & Assert
+ Assert.Null(provider.GetMetadataForProperty(null, typeof(DisplayFormatModel), "NoAttribute").NullDisplayText);
+ Assert.Equal("(null value)", provider.GetMetadataForProperty(null, typeof(DisplayFormatModel), "NullDisplayText").NullDisplayText);
+ }
+
+ [Fact]
+ public void DisplayFormatAttributeSetsDisplayFormatString()
+ {
+ // Arrange
+ var provider = MakeProvider();
+
+ // Act & Assert
+ Assert.Null(provider.GetMetadataForProperty(null, typeof(DisplayFormatModel), "NoAttribute").DisplayFormatString);
+ Assert.Equal("Data {0} format", provider.GetMetadataForProperty(null, typeof(DisplayFormatModel), "DisplayFormatString").DisplayFormatString);
+ Assert.Equal("Data {0} format", provider.GetMetadataForProperty(null, typeof(DisplayFormatModel), "DisplayAndEditFormatString").DisplayFormatString);
+ }
+
+ [Fact]
+ public void DisplayFormatAttributeSetEditFormatString()
+ {
+ // Arrange
+ var provider = MakeProvider();
+
+ // Act & Assert
+ Assert.Null(provider.GetMetadataForProperty(null, typeof(DisplayFormatModel), "NoAttribute").EditFormatString);
+ Assert.Null(provider.GetMetadataForProperty(null, typeof(DisplayFormatModel), "DisplayFormatString").EditFormatString);
+ Assert.Equal("Data {0} format", provider.GetMetadataForProperty(null, typeof(DisplayFormatModel), "DisplayAndEditFormatString").EditFormatString);
+ }
+
+ [Fact]
+ public void DisplayFormatAttributeSetsConvertEmptyStringToNull()
+ {
+ // Arrange
+ var provider = MakeProvider();
+
+ // Act & Assert
+ Assert.True(provider.GetMetadataForProperty(null, typeof(DisplayFormatModel), "NoAttribute").ConvertEmptyStringToNull);
+ Assert.True(provider.GetMetadataForProperty(null, typeof(DisplayFormatModel), "ConvertEmptyStringToNullTrue").ConvertEmptyStringToNull);
+ Assert.False(provider.GetMetadataForProperty(null, typeof(DisplayFormatModel), "ConvertEmptyStringToNullFalse").ConvertEmptyStringToNull);
+ }
+
+ [Fact]
+ public void DataTypeWithoutDisplayFormatOverrideUsesDataTypesDisplayFormat()
+ {
+ // Arrange
+ var provider = MakeProvider();
+
+ // Act
+ string result = provider.GetMetadataForProperty(null, typeof(DisplayFormatModel), "DataTypeWithoutDisplayFormatOverride").DisplayFormatString;
+
+ // Assert
+ Assert.Equal("{0:C}", result); // Currency's default format string
+ }
+
+ [Fact]
+ public void DataTypeWithDisplayFormatOverrideUsesDisplayFormatOverride()
+ {
+ // Arrange
+ var provider = MakeProvider();
+
+ // Act
+ string result = provider.GetMetadataForProperty(null, typeof(DisplayFormatModel), "DataTypeWithDisplayFormatOverride").DisplayFormatString;
+
+ // Assert
+ Assert.Equal("format override", result);
+ }
+
+ [Fact]
+ public void DataTypeInfluencedByDisplayFormatAttributeHtmlEncode()
+ {
+ // Arrange
+ var provider = MakeProvider();
+
+ // Act & Assert
+ Assert.Null(provider.GetMetadataForProperty(null, typeof(DisplayFormatModel), "NoAttribute").DataTypeName);
+ Assert.Null(provider.GetMetadataForProperty(null, typeof(DisplayFormatModel), "HtmlEncodeTrue").DataTypeName);
+ Assert.Equal("Html", provider.GetMetadataForProperty(null, typeof(DisplayFormatModel), "HtmlEncodeFalse").DataTypeName);
+ Assert.Equal("Currency", provider.GetMetadataForProperty(null, typeof(DisplayFormatModel), "HtmlEncodeFalseWithDataType").DataTypeName);
+ }
+
+ // [ScaffoldColumn] tests
+
+ class ScaffoldColumnModel
+ {
+ public int NoAttribute { get; set; }
+
+ [ScaffoldColumn(true)]
+ public int ScaffoldColumnTrue { get; set; }
+
+ [ScaffoldColumn(false)]
+ public int ScaffoldColumnFalse { get; set; }
+ }
+
+ [Fact]
+ public void ScaffoldColumnAttributeSetsShowForDisplay()
+ {
+ // Arrange
+ var provider = MakeProvider();
+
+ // Act & Assert
+ Assert.True(provider.GetMetadataForProperty(null, typeof(ScaffoldColumnModel), "NoAttribute").ShowForDisplay);
+ Assert.True(provider.GetMetadataForProperty(null, typeof(ScaffoldColumnModel), "ScaffoldColumnTrue").ShowForDisplay);
+ Assert.False(provider.GetMetadataForProperty(null, typeof(ScaffoldColumnModel), "ScaffoldColumnFalse").ShowForDisplay);
+ }
+
+ [Fact]
+ public void ScaffoldColumnAttributeSetsShowForEdit()
+ {
+ // Arrange
+ var provider = MakeProvider();
+
+ // Act & Assert
+ Assert.True(provider.GetMetadataForProperty(null, typeof(ScaffoldColumnModel), "NoAttribute").ShowForEdit);
+ Assert.True(provider.GetMetadataForProperty(null, typeof(ScaffoldColumnModel), "ScaffoldColumnTrue").ShowForEdit);
+ Assert.False(provider.GetMetadataForProperty(null, typeof(ScaffoldColumnModel), "ScaffoldColumnFalse").ShowForEdit);
+ }
+
+ // [DisplayColumn] tests
+
+ [DisplayColumn("NoPropertyWithThisName")]
+ class UnknownDisplayColumnModel
+ {
+ }
+
+ [Fact]
+ public void SimpleDisplayNameWithUnknownDisplayColumnThrows()
+ {
+ // Arrange
+ var provider = MakeProvider();
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ () => provider.GetMetadataForType(() => new UnknownDisplayColumnModel(), typeof(UnknownDisplayColumnModel)).SimpleDisplayText,
+ typeof(UnknownDisplayColumnModel).FullName + " has a DisplayColumn attribute for NoPropertyWithThisName, but property NoPropertyWithThisName does not exist.");
+ }
+
+ [DisplayColumn("WriteOnlyProperty")]
+ class WriteOnlyDisplayColumnModel
+ {
+ public int WriteOnlyProperty
+ {
+ set { }
+ }
+ }
+
+ [DisplayColumn("PrivateReadPublicWriteProperty")]
+ class PrivateReadPublicWriteDisplayColumnModel
+ {
+ public int PrivateReadPublicWriteProperty { private get; set; }
+ }
+
+ [Fact]
+ public void SimpleDisplayTextForTypeWithWriteOnlyDisplayColumnThrows()
+ {
+ // Arrange
+ var provider = MakeProvider();
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ () => provider.GetMetadataForType(() => new WriteOnlyDisplayColumnModel(), typeof(WriteOnlyDisplayColumnModel)).SimpleDisplayText,
+ typeof(WriteOnlyDisplayColumnModel).FullName + " has a DisplayColumn attribute for WriteOnlyProperty, but property WriteOnlyProperty does not have a public getter.");
+
+ Assert.Throws<InvalidOperationException>(
+ () => provider.GetMetadataForType(() => new PrivateReadPublicWriteDisplayColumnModel(), typeof(PrivateReadPublicWriteDisplayColumnModel)).SimpleDisplayText,
+ typeof(PrivateReadPublicWriteDisplayColumnModel).FullName + " has a DisplayColumn attribute for PrivateReadPublicWriteProperty, but property PrivateReadPublicWriteProperty does not have a public getter.");
+ }
+
+ [DisplayColumn("DisplayColumnProperty")]
+ class SimpleDisplayTextAttributeModel
+ {
+ public int FirstProperty
+ {
+ get { return 42; }
+ }
+
+ [ScaffoldColumn(false)]
+ public string DisplayColumnProperty { get; set; }
+ }
+
+ class SimpleDisplayTextAttributeModelContainer
+ {
+ [DisplayFormat(NullDisplayText = "This is the null display text")]
+ public SimpleDisplayTextAttributeModel Inner { get; set; }
+ }
+
+ [Fact]
+ public void SimpleDisplayTextForNonNullClassWithNonNullDisplayColumnValue()
+ {
+ // Arrange
+ string expected = "Custom property display value";
+ var provider = MakeProvider();
+ var model = new SimpleDisplayTextAttributeModel { DisplayColumnProperty = expected };
+ var metadata = provider.GetMetadataForType(() => model, typeof(SimpleDisplayTextAttributeModel));
+
+ // Act
+ string result = metadata.SimpleDisplayText;
+
+ // Assert
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void SimpleDisplayTextForNullClassRevertsToDefaultBehavior()
+ {
+ // Arrange
+ var provider = MakeProvider();
+ var metadata = provider.GetMetadataForProperty(null, typeof(SimpleDisplayTextAttributeModelContainer), "Inner");
+
+ // Act
+ string result = metadata.SimpleDisplayText;
+
+ // Assert
+ Assert.Equal("This is the null display text", result);
+ }
+
+ [Fact]
+ public void SimpleDisplayTextForNonNullClassWithNullDisplayColumnValueRevertsToDefaultBehavior()
+ {
+ // Arrange
+ var provider = MakeProvider();
+ var model = new SimpleDisplayTextAttributeModel();
+ var metadata = provider.GetMetadataForType(() => model, typeof(SimpleDisplayTextAttributeModel));
+
+ // Act
+ string result = metadata.SimpleDisplayText;
+
+ // Assert
+ Assert.Equal("42", result); // Falls back to the default logic of first property value
+ }
+
+ // [Required] tests
+
+ class IsRequiredModel
+ {
+ public int NonNullableWithout { get; set; }
+
+ public string NullableWithout { get; set; }
+
+ [Required]
+ public string NullableWith { get; set; }
+ }
+
+ [Fact]
+ public void IsRequiredTests()
+ {
+ // Arrange
+ var provider = MakeProvider();
+
+ // Act & Assert
+ Assert.True(provider.GetMetadataForProperty(null, typeof(IsRequiredModel), "NonNullableWithout").IsRequired);
+ Assert.False(provider.GetMetadataForProperty(null, typeof(IsRequiredModel), "NullableWithout").IsRequired);
+ Assert.True(provider.GetMetadataForProperty(null, typeof(IsRequiredModel), "NullableWith").IsRequired);
+ }
+
+ // [Display] & [DisplayName] tests
+
+ class DisplayModel
+ {
+ public int NoAttribute { get; set; }
+
+ // Description
+
+ [Display]
+ public int DescriptionNotSet { get; set; }
+
+ [Display(Description = "Description text")]
+ public int DescriptionSet { get; set; }
+
+ // DisplayName
+
+ [DisplayName("Value from DisplayName")]
+ public int DisplayNameAttributeNoDisplayAttribute { get; set; }
+
+ [Display]
+ public int DisplayAttributeNameNotSet { get; set; }
+
+ [Display(Name = "Non empty name")]
+ public int DisplayAttributeNonEmptyName { get; set; }
+
+ [Display]
+ [DisplayName("Value from DisplayName")]
+ public int BothAttributesNameNotSet { get; set; }
+
+ [Display(Name = "Value from Display")]
+ [DisplayName("Value from DisplayName")]
+ public int BothAttributes { get; set; }
+
+ // Display trumps DisplayName
+
+ // Order
+
+ [Display]
+ public int OrderNotSet { get; set; }
+
+ [Display(Order = 2112)]
+ public int OrderSet { get; set; }
+
+ // ShortDisplayName
+
+ [Display]
+ public int ShortNameNotSet { get; set; }
+
+ [Display(ShortName = "Short name")]
+ public int ShortNameSet { get; set; }
+
+ // Watermark
+
+ [Display]
+ public int PromptNotSet { get; set; }
+
+ [Display(Prompt = "Enter stuff here")]
+ public int PromptSet { get; set; }
+ }
+
+ [Fact]
+ public void DescriptionTests()
+ {
+ // Arrange
+ var provider = MakeProvider();
+
+ // Act & Assert
+ Assert.Null(provider.GetMetadataForProperty(null, typeof(DisplayModel), "NoAttribute").Description);
+ Assert.Null(provider.GetMetadataForProperty(null, typeof(DisplayModel), "DescriptionNotSet").Description);
+ Assert.Equal("Description text", provider.GetMetadataForProperty(null, typeof(DisplayModel), "DescriptionSet").Description);
+ }
+
+ [Fact]
+ public void DisplayNameTests()
+ {
+ // Arrange
+ var provider = MakeProvider();
+
+ // Act & Assert
+ Assert.Null(provider.GetMetadataForProperty(null, typeof(DisplayModel), "NoAttribute").DisplayName);
+ Assert.Equal("Value from DisplayName", provider.GetMetadataForProperty(null, typeof(DisplayModel), "DisplayNameAttributeNoDisplayAttribute").DisplayName);
+ Assert.Null(provider.GetMetadataForProperty(null, typeof(DisplayModel), "DisplayAttributeNameNotSet").DisplayName);
+ Assert.Equal("Non empty name", provider.GetMetadataForProperty(null, typeof(DisplayModel), "DisplayAttributeNonEmptyName").DisplayName);
+ Assert.Equal("Value from DisplayName", provider.GetMetadataForProperty(null, typeof(DisplayModel), "BothAttributesNameNotSet").DisplayName);
+ Assert.Equal("Value from Display", provider.GetMetadataForProperty(null, typeof(DisplayModel), "BothAttributes").DisplayName);
+ }
+
+ [Fact]
+ public void OrderTests()
+ {
+ // Arrange
+ var provider = MakeProvider();
+
+ // Act & Assert
+ Assert.Equal(10000, provider.GetMetadataForProperty(null, typeof(DisplayModel), "NoAttribute").Order);
+ Assert.Equal(10000, provider.GetMetadataForProperty(null, typeof(DisplayModel), "OrderNotSet").Order);
+ Assert.Equal(2112, provider.GetMetadataForProperty(null, typeof(DisplayModel), "OrderSet").Order);
+ }
+
+ [Fact]
+ public void ShortDisplayNameTests()
+ {
+ // Arrange
+ var provider = MakeProvider();
+
+ // Act & Assert
+ Assert.Null(provider.GetMetadataForProperty(null, typeof(DisplayModel), "NoAttribute").ShortDisplayName);
+ Assert.Null(provider.GetMetadataForProperty(null, typeof(DisplayModel), "ShortNameNotSet").ShortDisplayName);
+ Assert.Equal("Short name", provider.GetMetadataForProperty(null, typeof(DisplayModel), "ShortNameSet").ShortDisplayName);
+ }
+
+ [Fact]
+ public void WatermarkTests()
+ {
+ // Arrange
+ var provider = MakeProvider();
+
+ // Act & Assert
+ Assert.Null(provider.GetMetadataForProperty(null, typeof(DisplayModel), "NoAttribute").Watermark);
+ Assert.Null(provider.GetMetadataForProperty(null, typeof(DisplayModel), "PromptNotSet").Watermark);
+ Assert.Equal("Enter stuff here", provider.GetMetadataForProperty(null, typeof(DisplayModel), "PromptSet").Watermark);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/DataAnnotationsModelValidatorProviderTest.cs b/test/System.Web.Mvc.Test/Test/DataAnnotationsModelValidatorProviderTest.cs
new file mode 100644
index 00000000..ab8c0342
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/DataAnnotationsModelValidatorProviderTest.cs
@@ -0,0 +1,725 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class DataAnnotationsModelValidatorProviderTest
+ {
+ // Validation attribute adapter registration
+
+ private class MyValidationAttribute : ValidationAttribute
+ {
+ public override bool IsValid(object value)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ private class MyValidationAttributeAdapter : DataAnnotationsModelValidator<MyValidationAttribute>
+ {
+ public MyValidationAttributeAdapter(ModelMetadata metadata, ControllerContext context, MyValidationAttribute attribute)
+ : base(metadata, context, attribute)
+ {
+ }
+ }
+
+ private class MyValidationAttributeAdapterBadCtor : ModelValidator
+ {
+ public MyValidationAttributeAdapterBadCtor(ModelMetadata metadata, ControllerContext context)
+ : base(metadata, context)
+ {
+ }
+
+ public override IEnumerable<ModelValidationResult> Validate(object container)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ private class MyDefaultValidationAttributeAdapter : DataAnnotationsModelValidator
+ {
+ public MyDefaultValidationAttributeAdapter(ModelMetadata metadata, ControllerContext context, ValidationAttribute attribute)
+ : base(metadata, context, attribute)
+ {
+ }
+ }
+
+ [MyValidation]
+ private class MyValidatedClass
+ {
+ }
+
+ [Fact]
+ public void RegisterAdapter()
+ {
+ var oldFactories = DataAnnotationsModelValidatorProvider.AttributeFactories;
+
+ try
+ {
+ // Arrange
+ DataAnnotationsModelValidatorProvider.AttributeFactories = new Dictionary<Type, DataAnnotationsModelValidationFactory>();
+
+ // Act
+ DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(MyValidationAttribute), typeof(MyValidationAttributeAdapter));
+
+ // Assert
+ var type = DataAnnotationsModelValidatorProvider.AttributeFactories.Keys.Single();
+ Assert.Equal(typeof(MyValidationAttribute), type);
+
+ var factory = DataAnnotationsModelValidatorProvider.AttributeFactories.Values.Single();
+ var metadata = ModelMetadataProviders.Current.GetMetadataForType(() => null, typeof(object));
+ var context = new ControllerContext();
+ var attribute = new MyValidationAttribute();
+ var validator = factory(metadata, context, attribute);
+ Assert.IsType<MyValidationAttributeAdapter>(validator);
+ }
+ finally
+ {
+ DataAnnotationsModelValidatorProvider.AttributeFactories = oldFactories;
+ }
+ }
+
+ [Fact]
+ public void RegisterAdapterGuardClauses()
+ {
+ // Attribute type cannot be null
+ Assert.ThrowsArgumentNull(
+ () => DataAnnotationsModelValidatorProvider.RegisterAdapter(null, typeof(MyValidationAttributeAdapter)),
+ "attributeType");
+
+ // Adapter type cannot be null
+ Assert.ThrowsArgumentNull(
+ () => DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(MyValidationAttribute), null),
+ "adapterType");
+
+ // Validation attribute must derive from ValidationAttribute
+ Assert.Throws<ArgumentException>(
+ () => DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(object), typeof(MyValidationAttributeAdapter)),
+ "The type System.Object must derive from System.ComponentModel.DataAnnotations.ValidationAttribute\r\nParameter name: attributeType");
+
+ // Adapter must derive from ModelValidator
+ Assert.Throws<ArgumentException>(
+ () => DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(MyValidationAttribute), typeof(object)),
+ "The type System.Object must derive from System.Web.Mvc.ModelValidator\r\nParameter name: adapterType");
+
+ // Adapter must have the expected constructor
+ Assert.Throws<ArgumentException>(
+ () => DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(MyValidationAttribute), typeof(MyValidationAttributeAdapterBadCtor)),
+ "The type System.Web.Mvc.Test.DataAnnotationsModelValidatorProviderTest+MyValidationAttributeAdapterBadCtor must have a public constructor which accepts three parameters of types System.Web.Mvc.ModelMetadata, System.Web.Mvc.ControllerContext, and System.Web.Mvc.Test.DataAnnotationsModelValidatorProviderTest+MyValidationAttribute\r\nParameter name: adapterType");
+ }
+
+ [Fact]
+ public void RegisterAdapterFactory()
+ {
+ var oldFactories = DataAnnotationsModelValidatorProvider.AttributeFactories;
+
+ try
+ {
+ // Arrange
+ DataAnnotationsModelValidatorProvider.AttributeFactories = new Dictionary<Type, DataAnnotationsModelValidationFactory>();
+ DataAnnotationsModelValidationFactory factory = delegate { return null; };
+
+ // Act
+ DataAnnotationsModelValidatorProvider.RegisterAdapterFactory(typeof(MyValidationAttribute), factory);
+
+ // Assert
+ var type = DataAnnotationsModelValidatorProvider.AttributeFactories.Keys.Single();
+ Assert.Equal(typeof(MyValidationAttribute), type);
+ Assert.Same(factory, DataAnnotationsModelValidatorProvider.AttributeFactories.Values.Single());
+ }
+ finally
+ {
+ DataAnnotationsModelValidatorProvider.AttributeFactories = oldFactories;
+ }
+ }
+
+ [Fact]
+ public void RegisterAdapterFactoryGuardClauses()
+ {
+ DataAnnotationsModelValidationFactory factory = (metadata, context, attribute) => null;
+
+ // Attribute type cannot be null
+ Assert.ThrowsArgumentNull(
+ () => DataAnnotationsModelValidatorProvider.RegisterAdapterFactory(null, factory),
+ "attributeType");
+
+ // Factory cannot be null
+ Assert.ThrowsArgumentNull(
+ () => DataAnnotationsModelValidatorProvider.RegisterAdapterFactory(typeof(MyValidationAttribute), null),
+ "factory");
+
+ // Validation attribute must derive from ValidationAttribute
+ Assert.Throws<ArgumentException>(
+ () => DataAnnotationsModelValidatorProvider.RegisterAdapterFactory(typeof(object), factory),
+ "The type System.Object must derive from System.ComponentModel.DataAnnotations.ValidationAttribute\r\nParameter name: attributeType");
+ }
+
+ [Fact]
+ public void RegisterDefaultAdapter()
+ {
+ var oldFactory = DataAnnotationsModelValidatorProvider.DefaultAttributeFactory;
+
+ try
+ {
+ // Arrange
+ var metadata = ModelMetadataProviders.Current.GetMetadataForType(() => null, typeof(MyValidatedClass));
+ var context = new ControllerContext();
+ DataAnnotationsModelValidatorProvider.RegisterDefaultAdapter(typeof(MyDefaultValidationAttributeAdapter));
+
+ // Act
+ var result = new DataAnnotationsModelValidatorProvider().GetValidators(metadata, context).Single();
+
+ // Assert
+ Assert.IsType<MyDefaultValidationAttributeAdapter>(result);
+ }
+ finally
+ {
+ DataAnnotationsModelValidatorProvider.DefaultAttributeFactory = oldFactory;
+ }
+ }
+
+ [Fact]
+ public void RegisterDefaultAdapterGuardClauses()
+ {
+ // Adapter type cannot be null
+ Assert.ThrowsArgumentNull(
+ () => DataAnnotationsModelValidatorProvider.RegisterDefaultAdapter(null),
+ "adapterType");
+
+ // Adapter must derive from ModelValidator
+ Assert.Throws<ArgumentException>(
+ () => DataAnnotationsModelValidatorProvider.RegisterDefaultAdapter(typeof(object)),
+ "The type System.Object must derive from System.Web.Mvc.ModelValidator\r\nParameter name: adapterType");
+
+ // Adapter must have the expected constructor
+ Assert.Throws<ArgumentException>(
+ () => DataAnnotationsModelValidatorProvider.RegisterDefaultAdapter(typeof(MyValidationAttributeAdapterBadCtor)),
+ "The type System.Web.Mvc.Test.DataAnnotationsModelValidatorProviderTest+MyValidationAttributeAdapterBadCtor must have a public constructor which accepts three parameters of types System.Web.Mvc.ModelMetadata, System.Web.Mvc.ControllerContext, and System.ComponentModel.DataAnnotations.ValidationAttribute\r\nParameter name: adapterType");
+ }
+
+ [Fact]
+ public void RegisterDefaultAdapterFactory()
+ {
+ var oldFactory = DataAnnotationsModelValidatorProvider.DefaultAttributeFactory;
+
+ try
+ {
+ // Arrange
+ var metadata = ModelMetadataProviders.Current.GetMetadataForType(() => null, typeof(MyValidatedClass));
+ var context = new ControllerContext();
+ ModelValidator validator = new Mock<ModelValidator>(metadata, context).Object;
+ DataAnnotationsModelValidationFactory factory = delegate { return validator; };
+ DataAnnotationsModelValidatorProvider.RegisterDefaultAdapterFactory(factory);
+
+ // Act
+ var result = new DataAnnotationsModelValidatorProvider().GetValidators(metadata, context).Single();
+
+ // Assert
+ Assert.Same(validator, result);
+ }
+ finally
+ {
+ DataAnnotationsModelValidatorProvider.DefaultAttributeFactory = oldFactory;
+ }
+ }
+
+ [Fact]
+ public void RegisterDefaultAdapterFactoryGuardClauses()
+ {
+ // Factory cannot be null
+ Assert.ThrowsArgumentNull(
+ () => DataAnnotationsModelValidatorProvider.RegisterDefaultAdapterFactory(null),
+ "factory");
+ }
+
+ // IValidatableObject adapter registration
+
+ private class MyValidatableAdapter : ModelValidator
+ {
+ public MyValidatableAdapter(ModelMetadata metadata, ControllerContext context)
+ : base(metadata, context)
+ {
+ }
+
+ public override IEnumerable<ModelValidationResult> Validate(object container)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ private class MyValidatableAdapterBadCtor : ModelValidator
+ {
+ public MyValidatableAdapterBadCtor(ModelMetadata metadata, ControllerContext context, int unused)
+ : base(metadata, context)
+ {
+ }
+
+ public override IEnumerable<ModelValidationResult> Validate(object container)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ private class MyValidatableClass : IValidatableObject
+ {
+ public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ [Fact]
+ public void RegisterValidatableObjectAdapter()
+ {
+ var oldFactories = DataAnnotationsModelValidatorProvider.ValidatableFactories;
+
+ try
+ {
+ // Arrange
+ DataAnnotationsModelValidatorProvider.ValidatableFactories = new Dictionary<Type, DataAnnotationsValidatableObjectAdapterFactory>();
+ IValidatableObject validatable = new Mock<IValidatableObject>().Object;
+
+ // Act
+ DataAnnotationsModelValidatorProvider.RegisterValidatableObjectAdapter(validatable.GetType(), typeof(MyValidatableAdapter));
+
+ // Assert
+ var type = DataAnnotationsModelValidatorProvider.ValidatableFactories.Keys.Single();
+ Assert.Equal(validatable.GetType(), type);
+
+ var factory = DataAnnotationsModelValidatorProvider.ValidatableFactories.Values.Single();
+ var metadata = ModelMetadataProviders.Current.GetMetadataForType(() => null, typeof(object));
+ var context = new ControllerContext();
+ var validator = factory(metadata, context);
+ Assert.IsType<MyValidatableAdapter>(validator);
+ }
+ finally
+ {
+ DataAnnotationsModelValidatorProvider.ValidatableFactories = oldFactories;
+ }
+ }
+
+ [Fact]
+ public void RegisterValidatableObjectAdapterGuardClauses()
+ {
+ // Attribute type cannot be null
+ Assert.ThrowsArgumentNull(
+ () => DataAnnotationsModelValidatorProvider.RegisterValidatableObjectAdapter(null, typeof(MyValidatableAdapter)),
+ "modelType");
+
+ // Adapter type cannot be null
+ Assert.ThrowsArgumentNull(
+ () => DataAnnotationsModelValidatorProvider.RegisterValidatableObjectAdapter(typeof(MyValidatableClass), null),
+ "adapterType");
+
+ // Validation attribute must derive from ValidationAttribute
+ Assert.Throws<ArgumentException>(
+ () => DataAnnotationsModelValidatorProvider.RegisterValidatableObjectAdapter(typeof(object), typeof(MyValidatableAdapter)),
+ "The type System.Object must derive from System.ComponentModel.DataAnnotations.IValidatableObject\r\nParameter name: modelType");
+
+ // Adapter must derive from ModelValidator
+ Assert.Throws<ArgumentException>(
+ () => DataAnnotationsModelValidatorProvider.RegisterValidatableObjectAdapter(typeof(MyValidatableClass), typeof(object)),
+ "The type System.Object must derive from System.Web.Mvc.ModelValidator\r\nParameter name: adapterType");
+
+ // Adapter must have the expected constructor
+ Assert.Throws<ArgumentException>(
+ () => DataAnnotationsModelValidatorProvider.RegisterValidatableObjectAdapter(typeof(MyValidatableClass), typeof(MyValidatableAdapterBadCtor)),
+ "The type System.Web.Mvc.Test.DataAnnotationsModelValidatorProviderTest+MyValidatableAdapterBadCtor must have a public constructor which accepts two parameters of types System.Web.Mvc.ModelMetadata and System.Web.Mvc.ControllerContext.\r\nParameter name: adapterType");
+ }
+
+ [Fact]
+ public void RegisterValidatableObjectAdapterFactory()
+ {
+ var oldFactories = DataAnnotationsModelValidatorProvider.ValidatableFactories;
+
+ try
+ {
+ // Arrange
+ DataAnnotationsModelValidatorProvider.ValidatableFactories = new Dictionary<Type, DataAnnotationsValidatableObjectAdapterFactory>();
+ DataAnnotationsValidatableObjectAdapterFactory factory = delegate { return null; };
+
+ // Act
+ DataAnnotationsModelValidatorProvider.RegisterValidatableObjectAdapterFactory(typeof(MyValidatableClass), factory);
+
+ // Assert
+ var type = DataAnnotationsModelValidatorProvider.ValidatableFactories.Keys.Single();
+ Assert.Equal(typeof(MyValidatableClass), type);
+ Assert.Same(factory, DataAnnotationsModelValidatorProvider.ValidatableFactories.Values.Single());
+ }
+ finally
+ {
+ DataAnnotationsModelValidatorProvider.ValidatableFactories = oldFactories;
+ }
+ }
+
+ [Fact]
+ public void RegisterValidatableObjectAdapterFactoryGuardClauses()
+ {
+ DataAnnotationsValidatableObjectAdapterFactory factory = (metadata, context) => null;
+
+ // Attribute type cannot be null
+ Assert.ThrowsArgumentNull(
+ () => DataAnnotationsModelValidatorProvider.RegisterValidatableObjectAdapterFactory(null, factory),
+ "modelType");
+
+ // Factory cannot be null
+ Assert.ThrowsArgumentNull(
+ () => DataAnnotationsModelValidatorProvider.RegisterValidatableObjectAdapterFactory(typeof(MyValidatableClass), null),
+ "factory");
+
+ // Validation attribute must derive from ValidationAttribute
+ Assert.Throws<ArgumentException>(
+ () => DataAnnotationsModelValidatorProvider.RegisterValidatableObjectAdapterFactory(typeof(object), factory),
+ "The type System.Object must derive from System.ComponentModel.DataAnnotations.IValidatableObject\r\nParameter name: modelType");
+ }
+
+ [Fact]
+ public void RegisterDefaultValidatableObjectAdapter()
+ {
+ var oldFactory = DataAnnotationsModelValidatorProvider.DefaultValidatableFactory;
+
+ try
+ {
+ // Arrange
+ var metadata = ModelMetadataProviders.Current.GetMetadataForType(() => null, typeof(MyValidatableClass));
+ var context = new ControllerContext();
+ DataAnnotationsModelValidatorProvider.RegisterDefaultValidatableObjectAdapter(typeof(MyValidatableAdapter));
+
+ // Act
+ var result = new DataAnnotationsModelValidatorProvider().GetValidators(metadata, context).Single();
+
+ // Assert
+ Assert.IsType<MyValidatableAdapter>(result);
+ }
+ finally
+ {
+ DataAnnotationsModelValidatorProvider.DefaultValidatableFactory = oldFactory;
+ }
+ }
+
+ [Fact]
+ public void RegisterDefaultValidatableObjectAdapterGuardClauses()
+ {
+ // Adapter type cannot be null
+ Assert.ThrowsArgumentNull(
+ () => DataAnnotationsModelValidatorProvider.RegisterDefaultValidatableObjectAdapter(null),
+ "adapterType");
+
+ // Adapter must derive from ModelValidator
+ Assert.Throws<ArgumentException>(
+ () => DataAnnotationsModelValidatorProvider.RegisterDefaultValidatableObjectAdapter(typeof(object)),
+ "The type System.Object must derive from System.Web.Mvc.ModelValidator\r\nParameter name: adapterType");
+
+ // Adapter must have the expected constructor
+ Assert.Throws<ArgumentException>(
+ () => DataAnnotationsModelValidatorProvider.RegisterDefaultValidatableObjectAdapter(typeof(MyValidatableAdapterBadCtor)),
+ "The type System.Web.Mvc.Test.DataAnnotationsModelValidatorProviderTest+MyValidatableAdapterBadCtor must have a public constructor which accepts two parameters of types System.Web.Mvc.ModelMetadata and System.Web.Mvc.ControllerContext.\r\nParameter name: adapterType");
+ }
+
+ [Fact]
+ public void RegisterDefaultValidatableObjectAdapterFactory()
+ {
+ var oldFactory = DataAnnotationsModelValidatorProvider.DefaultValidatableFactory;
+
+ try
+ {
+ // Arrange
+ var metadata = ModelMetadataProviders.Current.GetMetadataForType(() => null, typeof(MyValidatableClass));
+ var context = new ControllerContext();
+ ModelValidator validator = new Mock<ModelValidator>(metadata, context).Object;
+ DataAnnotationsValidatableObjectAdapterFactory factory = delegate { return validator; };
+ DataAnnotationsModelValidatorProvider.RegisterDefaultValidatableObjectAdapterFactory(factory);
+
+ // Act
+ var result = new DataAnnotationsModelValidatorProvider().GetValidators(metadata, context).Single();
+
+ // Assert
+ Assert.Same(validator, result);
+ }
+ finally
+ {
+ DataAnnotationsModelValidatorProvider.DefaultValidatableFactory = oldFactory;
+ }
+ }
+
+ [Fact]
+ public void RegisterDefaultValidatableObjectAdapterFactoryGuardClauses()
+ {
+ // Factory cannot be null
+ Assert.ThrowsArgumentNull(
+ () => DataAnnotationsModelValidatorProvider.RegisterDefaultValidatableObjectAdapterFactory(null),
+ "factory");
+ }
+
+ // Pre-configured adapters
+
+ [Fact]
+ public void AdapterForRangeAttributeRegistered()
+ {
+ // Arrange
+ var metadata = ModelMetadataProviders.Current.GetMetadataForType(() => null, typeof(object));
+ var context = new ControllerContext();
+ var adapters = DataAnnotationsModelValidatorProvider.AttributeFactories;
+ var adapterFactory = adapters.Single(kvp => kvp.Key == typeof(RangeAttribute)).Value;
+
+ // Act
+ var adapter = adapterFactory(metadata, context, new RangeAttribute(1, 100));
+
+ // Assert
+ Assert.IsType<RangeAttributeAdapter>(adapter);
+ }
+
+ [Fact]
+ public void AdapterForRegularExpressionAttributeRegistered()
+ {
+ // Arrange
+ var metadata = ModelMetadataProviders.Current.GetMetadataForType(() => null, typeof(object));
+ var context = new ControllerContext();
+ var adapters = DataAnnotationsModelValidatorProvider.AttributeFactories;
+ var adapterFactory = adapters.Single(kvp => kvp.Key == typeof(RegularExpressionAttribute)).Value;
+
+ // Act
+ var adapter = adapterFactory(metadata, context, new RegularExpressionAttribute("abc"));
+
+ // Assert
+ Assert.IsType<RegularExpressionAttributeAdapter>(adapter);
+ }
+
+ [Fact]
+ public void AdapterForRequiredAttributeRegistered()
+ {
+ // Arrange
+ var metadata = ModelMetadataProviders.Current.GetMetadataForType(() => null, typeof(object));
+ var context = new ControllerContext();
+ var adapters = DataAnnotationsModelValidatorProvider.AttributeFactories;
+ var adapterFactory = adapters.Single(kvp => kvp.Key == typeof(RequiredAttribute)).Value;
+
+ // Act
+ var adapter = adapterFactory(metadata, context, new RequiredAttribute());
+
+ // Assert
+ Assert.IsType<RequiredAttributeAdapter>(adapter);
+ }
+
+ [Fact]
+ public void AdapterForStringLengthAttributeRegistered()
+ {
+ // Arrange
+ var metadata = ModelMetadataProviders.Current.GetMetadataForType(() => null, typeof(object));
+ var context = new ControllerContext();
+ var adapters = DataAnnotationsModelValidatorProvider.AttributeFactories;
+ var adapterFactory = adapters.Single(kvp => kvp.Key == typeof(StringLengthAttribute)).Value;
+
+ // Act
+ var adapter = adapterFactory(metadata, context, new StringLengthAttribute(6));
+
+ // Assert
+ Assert.IsType<StringLengthAttributeAdapter>(adapter);
+ }
+
+ // Default adapter factory for unknown attribute type
+
+ [Fact]
+ public void UnknownValidationAttributeGetsDefaultAdapter()
+ {
+ // Arrange
+ var provider = new DataAnnotationsModelValidatorProvider();
+ var context = new ControllerContext();
+ var metadata = ModelMetadataProviders.Current.GetMetadataForType(() => null, typeof(DummyClassWithDummyValidationAttribute));
+
+ // Act
+ IEnumerable<ModelValidator> validators = provider.GetValidators(metadata, context);
+
+ // Assert
+ var validator = validators.Single();
+ Assert.IsType<DataAnnotationsModelValidator>(validator);
+ }
+
+ private class DummyValidationAttribute : ValidationAttribute
+ {
+ }
+
+ [DummyValidation]
+ private class DummyClassWithDummyValidationAttribute
+ {
+ }
+
+ // Default IValidatableObject adapter factory
+
+ [Fact]
+ public void IValidatableObjectGetsAValidator()
+ {
+ // Arrange
+ var provider = new DataAnnotationsModelValidatorProvider();
+ var mockValidatable = new Mock<IValidatableObject>();
+ var context = new ControllerContext();
+ var metadata = ModelMetadataProviders.Current.GetMetadataForType(() => null, mockValidatable.Object.GetType());
+
+ // Act
+ IEnumerable<ModelValidator> validators = provider.GetValidators(metadata, context);
+
+ // Assert
+ Assert.Single(validators);
+ }
+
+ // Implicit [Required] attribute
+
+ [Fact]
+ public void ReferenceTypesDontGetImplicitRequiredAttribute()
+ {
+ // Arrange
+ var provider = new DataAnnotationsModelValidatorProvider();
+ var context = new ControllerContext();
+ var metadata = ModelMetadataProviders.Current.GetMetadataForType(() => null, typeof(string));
+
+ // Act
+ IEnumerable<ModelValidator> validators = provider.GetValidators(metadata, context);
+
+ // Assert
+ Assert.Empty(validators);
+ }
+
+ [Fact]
+ public void NonNullableValueTypesGetImplicitRequiredAttribute()
+ {
+ // Arrange
+ var provider = new DataAnnotationsModelValidatorProvider();
+ var context = new ControllerContext();
+ var metadata = ModelMetadataProviders.Current.GetMetadataForProperty(() => null, typeof(DummyRequiredAttributeHelperClass), "WithoutAttribute");
+
+ // Act
+ IEnumerable<ModelValidator> validators = provider.GetValidators(metadata, context);
+
+ // Assert
+ ModelValidator validator = validators.Single();
+ ModelClientValidationRule rule = validator.GetClientValidationRules().Single();
+ Assert.IsType<ModelClientValidationRequiredRule>(rule);
+ }
+
+ [Fact]
+ public void NonNullableValueTypesWithExplicitRequiredAttributeDoesntGetImplictRequiredAttribute()
+ {
+ // Arrange
+ var provider = new DataAnnotationsModelValidatorProvider();
+ var context = new ControllerContext();
+ var metadata = ModelMetadataProviders.Current.GetMetadataForProperty(() => null, typeof(DummyRequiredAttributeHelperClass), "WithAttribute");
+
+ // Act
+ IEnumerable<ModelValidator> validators = provider.GetValidators(metadata, context);
+
+ // Assert
+ ModelValidator validator = validators.Single();
+ ModelClientValidationRule rule = validator.GetClientValidationRules().Single();
+ Assert.IsType<ModelClientValidationRequiredRule>(rule);
+ Assert.Equal("Custom Required Message", rule.ErrorMessage);
+ }
+
+ [Fact]
+ public void NonNullableValueTypeDoesntGetImplicitRequiredAttributeWhenFlagIsOff()
+ {
+ DataAnnotationsModelValidatorProvider.AddImplicitRequiredAttributeForValueTypes = false;
+
+ try
+ {
+ // Arrange
+ var provider = new DataAnnotationsModelValidatorProvider();
+ var context = new ControllerContext();
+ var metadata = ModelMetadataProviders.Current.GetMetadataForProperty(() => null, typeof(DummyRequiredAttributeHelperClass), "WithoutAttribute");
+
+ // Act
+ IEnumerable<ModelValidator> validators = provider.GetValidators(metadata, context);
+
+ // Assert
+ Assert.Empty(validators);
+ }
+ finally
+ {
+ DataAnnotationsModelValidatorProvider.AddImplicitRequiredAttributeForValueTypes = true;
+ }
+ }
+
+ private class DummyRequiredAttributeHelperClass
+ {
+ [Required(ErrorMessage = "Custom Required Message")]
+ public int WithAttribute { get; set; }
+
+ public int WithoutAttribute { get; set; }
+ }
+
+ // Integration with metadata system
+
+ [Fact]
+ public void DoesNotReadPropertyValue()
+ {
+ // Arrange
+ ObservableModel model = new ObservableModel();
+ ModelMetadata metadata = new DataAnnotationsModelMetadataProvider().GetMetadataForProperty(() => model.TheProperty, typeof(ObservableModel), "TheProperty");
+ ControllerContext controllerContext = new ControllerContext();
+
+ // Act
+ ModelValidator[] validators = new DataAnnotationsModelValidatorProvider().GetValidators(metadata, controllerContext).ToArray();
+ ModelValidationResult[] results = validators.SelectMany(o => o.Validate(model)).ToArray();
+
+ // Assert
+ Assert.Empty(validators);
+ Assert.False(model.PropertyWasRead());
+ }
+
+ private class ObservableModel
+ {
+ private bool _propertyWasRead;
+
+ public string TheProperty
+ {
+ get
+ {
+ _propertyWasRead = true;
+ return "Hello";
+ }
+ }
+
+ public bool PropertyWasRead()
+ {
+ return _propertyWasRead;
+ }
+ }
+
+ private class BaseModel
+ {
+ public virtual string MyProperty { get; set; }
+ }
+
+ private class DerivedModel : BaseModel
+ {
+ [StringLength(10)]
+ public override string MyProperty
+ {
+ get { return base.MyProperty; }
+ set { base.MyProperty = value; }
+ }
+ }
+
+ [Fact]
+ public void GetValidatorsReturnsClientValidatorForDerivedTypeAppliedAgainstBaseTypeViaFromLambdaExpression()
+ { // Dev10 Bug #868619
+ // Arrange
+ var provider = new DataAnnotationsModelValidatorProvider();
+ var context = new ControllerContext();
+ var viewdata = new ViewDataDictionary<DerivedModel>();
+ var metadata = ModelMetadata.FromLambdaExpression(m => m.MyProperty, viewdata); // Bug is in FromLambdaExpression
+
+ // Act
+ IEnumerable<ModelValidator> validators = provider.GetValidators(metadata, context);
+
+ // Assert
+ ModelValidator validator = validators.Single();
+ ModelClientValidationRule clientRule = validator.GetClientValidationRules().Single();
+ Assert.IsType<ModelClientValidationStringLengthRule>(clientRule);
+ Assert.Equal(10, clientRule.ValidationParameters["max"]);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/DataAnnotationsModelValidatorTest.cs b/test/System.Web.Mvc.Test/Test/DataAnnotationsModelValidatorTest.cs
new file mode 100644
index 00000000..092fa2b1
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/DataAnnotationsModelValidatorTest.cs
@@ -0,0 +1,158 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using Moq;
+using Moq.Protected;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class DataAnnotationsModelValidatorTest
+ {
+ [Fact]
+ public void ConstructorGuards()
+ {
+ // Arrange
+ ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(object));
+ ControllerContext context = new ControllerContext();
+ RequiredAttribute attribute = new RequiredAttribute();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => new DataAnnotationsModelValidator(null, context, attribute),
+ "metadata");
+ Assert.ThrowsArgumentNull(
+ () => new DataAnnotationsModelValidator(metadata, null, attribute),
+ "controllerContext");
+ Assert.ThrowsArgumentNull(
+ () => new DataAnnotationsModelValidator(metadata, context, null),
+ "attribute");
+ }
+
+ [Fact]
+ public void ValuesSet()
+ {
+ // Arrange
+ ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(() => 15, typeof(string), "Length");
+ ControllerContext context = new ControllerContext();
+ RequiredAttribute attribute = new RequiredAttribute();
+
+ // Act
+ DataAnnotationsModelValidator validator = new DataAnnotationsModelValidator(metadata, context, attribute);
+
+ // Assert
+ Assert.Same(attribute, validator.Attribute);
+ Assert.Equal(attribute.FormatErrorMessage("Length"), validator.ErrorMessage);
+ }
+
+ [Fact]
+ public void NoClientRulesByDefault()
+ {
+ // Arrange
+ ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(() => 15, typeof(string), "Length");
+ ControllerContext context = new ControllerContext();
+ RequiredAttribute attribute = new RequiredAttribute();
+
+ // Act
+ DataAnnotationsModelValidator validator = new DataAnnotationsModelValidator(metadata, context, attribute);
+
+ // Assert
+ Assert.Empty(validator.GetClientValidationRules());
+ }
+
+ [Fact]
+ public void ValidateWithIsValidTrue()
+ {
+ // Arrange
+ ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(() => 15, typeof(string), "Length");
+ ControllerContext context = new ControllerContext();
+ Mock<ValidationAttribute> attribute = new Mock<ValidationAttribute> { CallBase = true };
+ attribute.Setup(a => a.IsValid(metadata.Model)).Returns(true);
+ DataAnnotationsModelValidator validator = new DataAnnotationsModelValidator(metadata, context, attribute.Object);
+
+ // Act
+ IEnumerable<ModelValidationResult> result = validator.Validate(null);
+
+ // Assert
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void ValidateWithIsValidFalse()
+ {
+ // Arrange
+ ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(() => 15, typeof(string), "Length");
+ ControllerContext context = new ControllerContext();
+ Mock<ValidationAttribute> attribute = new Mock<ValidationAttribute> { CallBase = true };
+ attribute.Setup(a => a.IsValid(metadata.Model)).Returns(false);
+ DataAnnotationsModelValidator validator = new DataAnnotationsModelValidator(metadata, context, attribute.Object);
+
+ // Act
+ IEnumerable<ModelValidationResult> result = validator.Validate(null);
+
+ // Assert
+ var validationResult = result.Single();
+ Assert.Equal("", validationResult.MemberName);
+ Assert.Equal(attribute.Object.FormatErrorMessage("Length"), validationResult.Message);
+ }
+
+ [Fact]
+ public void ValidatateWithValidationResultSuccess()
+ {
+ // Arrange
+ ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(() => 15, typeof(string), "Length");
+ ControllerContext context = new ControllerContext();
+ Mock<ValidationAttribute> attribute = new Mock<ValidationAttribute> { CallBase = true };
+ attribute.Protected()
+ .Setup<ValidationResult>("IsValid", ItExpr.IsAny<object>(), ItExpr.IsAny<ValidationContext>())
+ .Returns(ValidationResult.Success);
+ DataAnnotationsModelValidator validator = new DataAnnotationsModelValidator(metadata, context, attribute.Object);
+
+ // Act
+ IEnumerable<ModelValidationResult> result = validator.Validate(null);
+
+ // Assert
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void IsRequiredTests()
+ {
+ // Arrange
+ ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(() => 15, typeof(string), "Length");
+ ControllerContext context = new ControllerContext();
+
+ // Act & Assert
+ Assert.False(new DataAnnotationsModelValidator(metadata, context, new RangeAttribute(10, 20)).IsRequired);
+ Assert.True(new DataAnnotationsModelValidator(metadata, context, new RequiredAttribute()).IsRequired);
+ Assert.True(new DataAnnotationsModelValidator(metadata, context, new DerivedRequiredAttribute()).IsRequired);
+ }
+
+ class DerivedRequiredAttribute : RequiredAttribute
+ {
+ }
+
+ [Fact]
+ public void AttributeWithIClientValidatableGetsClientValidationRules()
+ {
+ // Arrange
+ var expected = new ModelClientValidationStringLengthRule("Error", 1, 10);
+ var context = new ControllerContext();
+ var metadata = ModelMetadataProviders.Current.GetMetadataForType(() => null, typeof(string));
+ var attribute = new Mock<ValidationAttribute> { CallBase = true };
+ attribute.As<IClientValidatable>()
+ .Setup(cv => cv.GetClientValidationRules(metadata, context))
+ .Returns(new[] { expected })
+ .Verifiable();
+ var validator = new DataAnnotationsModelValidator(metadata, context, attribute.Object);
+
+ // Act
+ ModelClientValidationRule actual = validator.GetClientValidationRules().Single();
+
+ // Assert
+ attribute.Verify();
+ Assert.Same(expected, actual);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/DataErrorInfoModelValidatorProviderTest.cs b/test/System.Web.Mvc.Test/Test/DataErrorInfoModelValidatorProviderTest.cs
new file mode 100644
index 00000000..b65ca64e
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/DataErrorInfoModelValidatorProviderTest.cs
@@ -0,0 +1,287 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class DataErrorInfoModelValidatorProviderTest
+ {
+ private static readonly EmptyModelMetadataProvider _metadataProvider = new EmptyModelMetadataProvider();
+
+ [Fact]
+ public void GetValidatorsReturnsEmptyCollectionIfTypeNotIDataErrorInfo()
+ {
+ // Arrange
+ DataErrorInfoModelValidatorProvider validatorProvider = new DataErrorInfoModelValidatorProvider();
+ object model = new object();
+ ModelMetadata metadata = _metadataProvider.GetMetadataForType(() => model, typeof(object));
+
+ // Act
+ ModelValidator[] validators = validatorProvider.GetValidators(metadata, new ControllerContext()).ToArray();
+
+ // Assert
+ Assert.Empty(validators);
+ }
+
+ [Fact]
+ public void GetValidatorsReturnsValidatorForIDataErrorInfoProperty()
+ {
+ // Arrange
+ DataErrorInfoModelValidatorProvider validatorProvider = new DataErrorInfoModelValidatorProvider();
+ DataErrorInfo1 model = new DataErrorInfo1();
+ ModelMetadata metadata = _metadataProvider.GetMetadataForProperty(() => model, typeof(DataErrorInfo1), "SomeStringProperty");
+ Type[] expectedTypes = new Type[]
+ {
+ typeof(DataErrorInfoModelValidatorProvider.DataErrorInfoPropertyModelValidator)
+ };
+
+ // Act
+ Type[] actualTypes = validatorProvider.GetValidators(metadata, new ControllerContext()).Select(v => v.GetType()).ToArray();
+
+ // Assert
+ Assert.Equal(expectedTypes, actualTypes);
+ }
+
+ [Fact]
+ public void GetValidatorsReturnsValidatorForIDataErrorInfoRootType()
+ {
+ // Arrange
+ DataErrorInfoModelValidatorProvider validatorProvider = new DataErrorInfoModelValidatorProvider();
+ DataErrorInfo1 model = new DataErrorInfo1();
+ ModelMetadata metadata = _metadataProvider.GetMetadataForType(() => model, typeof(DataErrorInfo1));
+ Type[] expectedTypes = new Type[]
+ {
+ typeof(DataErrorInfoModelValidatorProvider.DataErrorInfoClassModelValidator)
+ };
+
+ // Act
+ Type[] actualTypes = validatorProvider.GetValidators(metadata, new ControllerContext()).Select(v => v.GetType()).ToArray();
+
+ // Assert
+ Assert.Equal(expectedTypes, actualTypes);
+ }
+
+ [Fact]
+ public void GetValidatorsThrowsIfContextIsNull()
+ {
+ // Arrange
+ DataErrorInfoModelValidatorProvider validatorProvider = new DataErrorInfoModelValidatorProvider();
+ ModelMetadata metadata = _metadataProvider.GetMetadataForType(null, typeof(DataErrorInfo1));
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { validatorProvider.GetValidators(metadata, null); }, "context");
+ }
+
+ [Fact]
+ public void GetValidatorsThrowsIfMetadataIsNull()
+ {
+ // Arrange
+ DataErrorInfoModelValidatorProvider validatorProvider = new DataErrorInfoModelValidatorProvider();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { validatorProvider.GetValidators(null, new ControllerContext()); }, "metadata");
+ }
+
+ [Fact]
+ public void ClassValidator_Validate_IDataErrorInfoModelWithError()
+ {
+ // Arrange
+ DataErrorInfo1 model = new DataErrorInfo1()
+ {
+ Error = "This is an error message."
+ };
+ ModelMetadata metadata = _metadataProvider.GetMetadataForType(() => model, typeof(DataErrorInfo1));
+
+ var validator = new DataErrorInfoModelValidatorProvider.DataErrorInfoClassModelValidator(metadata, new ControllerContext());
+
+ // Act
+ ModelValidationResult[] result = validator.Validate(null).ToArray();
+
+ // Assert
+ ModelValidationResult modelValidationResult = Assert.Single(result);
+ Assert.Equal("This is an error message.", modelValidationResult.Message);
+ }
+
+ [Fact]
+ public void ClassValidator_Validate_IDataErrorInfoModelWithNoErrorReturnsEmptyResults()
+ {
+ // Arrange
+ DataErrorInfo1 model = new DataErrorInfo1();
+ ModelMetadata metadata = _metadataProvider.GetMetadataForType(() => model, typeof(DataErrorInfo1));
+
+ var validator = new DataErrorInfoModelValidatorProvider.DataErrorInfoClassModelValidator(metadata, new ControllerContext());
+
+ // Act
+ ModelValidationResult[] result = validator.Validate(null).ToArray();
+
+ // Assert
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void ClassValidator_Validate_NonIDataErrorInfoModelReturnsEmptyResults()
+ {
+ // Arrange
+ object model = new object();
+ ModelMetadata metadata = _metadataProvider.GetMetadataForType(() => model, typeof(object));
+
+ var validator = new DataErrorInfoModelValidatorProvider.DataErrorInfoClassModelValidator(metadata, new ControllerContext());
+
+ // Act
+ ModelValidationResult[] result = validator.Validate(null).ToArray();
+
+ // Assert
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void PropertyValidator_Validate_IDataErrorInfoSkipsErrorProperty()
+ {
+ // Arrange
+ DataErrorInfo1 container = new DataErrorInfo1();
+ container["Error"] = "This should never be shown.";
+ ModelMetadata metadata = _metadataProvider.GetMetadataForProperty(() => container, typeof(DataErrorInfo1), "Error");
+
+ var validator = new DataErrorInfoModelValidatorProvider.DataErrorInfoPropertyModelValidator(metadata, new ControllerContext());
+
+ // Act
+ ModelValidationResult[] result = validator.Validate(container).ToArray();
+
+ // Assert
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void PropertyValidator_Validate_DoesNotReadPropertyValue()
+ {
+ // Arrange
+ ObservableModel model = new ObservableModel();
+ ModelMetadata metadata = _metadataProvider.GetMetadataForProperty(() => model.TheProperty, typeof(ObservableModel), "TheProperty");
+ ControllerContext controllerContext = new ControllerContext();
+
+ // Act
+ ModelValidator[] validators = new DataErrorInfoModelValidatorProvider().GetValidators(metadata, controllerContext).ToArray();
+ ModelValidationResult[] results = validators.SelectMany(o => o.Validate(model)).ToArray();
+
+ // Assert
+ Assert.Equal(new[] { typeof(DataErrorInfoModelValidatorProvider.DataErrorInfoPropertyModelValidator) }, Array.ConvertAll(validators, o => o.GetType()));
+ Assert.Equal(new[] { "TheProperty" }, model.GetColumnNamesPassed().ToArray());
+ Assert.Empty(results);
+ Assert.False(model.PropertyWasRead());
+ }
+
+ [Fact]
+ public void PropertyValidator_Validate_IDataErrorInfoContainerWithError()
+ {
+ // Arrange
+ DataErrorInfo1 container = new DataErrorInfo1();
+ container["SomeStringProperty"] = "This is an error message.";
+ ModelMetadata metadata = _metadataProvider.GetMetadataForProperty(() => container, typeof(DataErrorInfo1), "SomeStringProperty");
+
+ var validator = new DataErrorInfoModelValidatorProvider.DataErrorInfoPropertyModelValidator(metadata, new ControllerContext());
+
+ // Act
+ ModelValidationResult[] result = validator.Validate(container).ToArray();
+
+ // Assert
+ ModelValidationResult modelValidationResult = Assert.Single(result);
+ Assert.Equal("This is an error message.", modelValidationResult.Message);
+ }
+
+ [Fact]
+ public void PropertyValidator_Validate_IDataErrorInfoContainerWithNoErrorReturnsEmptyResults()
+ {
+ // Arrange
+ DataErrorInfo1 container = new DataErrorInfo1();
+ ModelMetadata metadata = _metadataProvider.GetMetadataForProperty(() => container, typeof(DataErrorInfo1), "SomeStringProperty");
+
+ var validator = new DataErrorInfoModelValidatorProvider.DataErrorInfoPropertyModelValidator(metadata, new ControllerContext());
+
+ // Act
+ ModelValidationResult[] result = validator.Validate(container).ToArray();
+
+ // Assert
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void PropertyValidator_Validate_NonIDataErrorInfoContainerReturnsEmptyResults()
+ {
+ // Arrange
+ DataErrorInfo1 container = new DataErrorInfo1();
+ container["SomeStringProperty"] = "This is an error message.";
+ ModelMetadata metadata = _metadataProvider.GetMetadataForProperty(() => container, typeof(DataErrorInfo1), "SomeStringProperty");
+
+ var validator = new DataErrorInfoModelValidatorProvider.DataErrorInfoPropertyModelValidator(metadata, new ControllerContext());
+
+ // Act
+ ModelValidationResult[] result = validator.Validate(new object()).ToArray();
+
+ // Assert
+ Assert.Empty(result);
+ }
+
+ private class DataErrorInfo1 : IDataErrorInfo
+ {
+ private readonly Dictionary<string, string> _errors = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+
+ public string SomeStringProperty { get; set; }
+
+ public string Error { get; set; }
+
+ public string this[string columnName]
+ {
+ get
+ {
+ string outVal;
+ _errors.TryGetValue(columnName, out outVal);
+ return outVal;
+ }
+ set { _errors[columnName] = value; }
+ }
+ }
+
+ private class ObservableModel : IDataErrorInfo
+ {
+ private bool _propertyWasRead;
+ private readonly List<string> _columnNamesPassed = new List<string>();
+
+ public int TheProperty
+ {
+ get
+ {
+ _propertyWasRead = true;
+ return 42;
+ }
+ }
+
+ public bool PropertyWasRead()
+ {
+ return _propertyWasRead;
+ }
+
+ public string Error
+ {
+ get { throw new NotImplementedException(); }
+ }
+
+ public string this[string columnName]
+ {
+ get
+ {
+ _columnNamesPassed.Add(columnName);
+ return null;
+ }
+ }
+
+ public List<string> GetColumnNamesPassed()
+ {
+ return _columnNamesPassed;
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/DataTypeUtilTest.cs b/test/System.Web.Mvc.Test/Test/DataTypeUtilTest.cs
new file mode 100644
index 00000000..a468d540
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/DataTypeUtilTest.cs
@@ -0,0 +1,75 @@
+using System.ComponentModel.DataAnnotations;
+using Moq;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class DataTypeUtilTest
+ {
+ private class DerivedDataTypeAttribute : DataTypeAttribute
+ {
+ public DerivedDataTypeAttribute(DataType dataType)
+ : base(dataType)
+ {
+ }
+
+ public override string GetDataTypeName()
+ {
+ return "DerivedTypeName";
+ }
+ }
+
+ [Fact]
+ public void VirtualDataTypeNameCallsAttributeGetDataTypeName()
+ {
+ // Arrange
+ DataTypeAttribute derivedAttr = new DerivedDataTypeAttribute(DataType.Html);
+ string expectedTypeName = derivedAttr.GetDataTypeName();
+
+ // Act
+ string actualTypeName = DataTypeUtil.ToDataTypeName(derivedAttr);
+
+ // Assert
+ Assert.Equal(expectedTypeName, actualTypeName);
+ }
+
+ [Fact]
+ public void DataTypeAttributeDoesNotCallAttributeGetDataTypeName()
+ {
+ // Arrange
+ Func<DataTypeAttribute, Boolean> isDataTypeAttribute = t => (t as DataTypeAttribute) != null;
+
+ foreach (DataType dataTypeValue in Enum.GetValues(typeof(DataType)))
+ {
+ if (dataTypeValue != DataType.Custom)
+ {
+ Mock<DataTypeAttribute> dataType = new Mock<DataTypeAttribute>(dataTypeValue);
+
+ // Act
+ string actualTypeName = DataTypeUtil.ToDataTypeName(dataType.Object, dta => dta as DataTypeAttribute != null);
+
+ // Assert
+ Assert.Equal(dataTypeValue.ToString(), actualTypeName);
+ dataType.Verify(dt => dt.GetDataTypeName(), Times.Never());
+ }
+ }
+ }
+
+ [Fact]
+ public void CustomDataTypeNameCallsAttributeGetDataTypeName()
+ {
+ // Arrange
+ Func<DataTypeAttribute, Boolean> isDataTypeAttribute = t => (t as DataTypeAttribute) != null;
+
+ Mock<DataTypeAttribute> customDataType = new Mock<DataTypeAttribute>(DataType.Custom);
+ customDataType.Setup(c => c.GetDataTypeName()).Returns("CustomTypeName").Verifiable();
+
+ // Act
+ string actualTypeName = DataTypeUtil.ToDataTypeName(customDataType.Object);
+
+ // Assert
+ customDataType.Verify(c => c.GetDataTypeName(), Times.Once());
+ Assert.Equal("CustomTypeName", actualTypeName);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/DefaultControllerFactoryTest.cs b/test/System.Web.Mvc.Test/Test/DefaultControllerFactoryTest.cs
new file mode 100644
index 00000000..b95ebb42
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/DefaultControllerFactoryTest.cs
@@ -0,0 +1,766 @@
+using System.Reflection;
+using System.Web.Routing;
+using System.Web.SessionState;
+using Moq;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ [CLSCompliant(false)]
+ public class DefaultControllerFactoryTest
+ {
+ static DefaultControllerFactoryTest()
+ {
+ MvcTestHelper.CreateMvcAssemblies();
+ }
+
+ [Fact]
+ public void CreateAmbiguousControllerException_RouteWithoutUrl()
+ {
+ // Arrange
+ RouteBase route = new Mock<RouteBase>().Object;
+
+ Type[] matchingTypes = new Type[]
+ {
+ typeof(object),
+ typeof(string)
+ };
+
+ // Act
+ InvalidOperationException exception = DefaultControllerFactory.CreateAmbiguousControllerException(route, "Foo", matchingTypes);
+
+ // Assert
+ Assert.Equal(@"Multiple types were found that match the controller named 'Foo'. This can happen if the route that services this request does not specify namespaces to search for a controller that matches the request. If this is the case, register this route by calling an overload of the 'MapRoute' method that takes a 'namespaces' parameter.
+
+The request for 'Foo' has found the following matching controllers:
+System.Object
+System.String", exception.Message);
+ }
+
+ [Fact]
+ public void CreateAmbiguousControllerException_RouteWithUrl()
+ {
+ // Arrange
+ RouteBase route = new Route("{controller}/blah", new Mock<IRouteHandler>().Object);
+
+ Type[] matchingTypes = new Type[]
+ {
+ typeof(object),
+ typeof(string)
+ };
+
+ // Act
+ InvalidOperationException exception = DefaultControllerFactory.CreateAmbiguousControllerException(route, "Foo", matchingTypes);
+
+ // Assert
+ Assert.Equal(@"Multiple types were found that match the controller named 'Foo'. This can happen if the route that services this request ('{controller}/blah') does not specify namespaces to search for a controller that matches the request. If this is the case, register this route by calling an overload of the 'MapRoute' method that takes a 'namespaces' parameter.
+
+The request for 'Foo' has found the following matching controllers:
+System.Object
+System.String", exception.Message);
+ }
+
+ [Fact]
+ public void CreateControllerWithNullContextThrows()
+ {
+ // Arrange
+ DefaultControllerFactory factory = new DefaultControllerFactory();
+
+ // Act
+ Assert.ThrowsArgumentNull(
+ delegate
+ {
+ ((IControllerFactory)factory).CreateController(
+ null,
+ "foo");
+ },
+ "requestContext");
+ }
+
+ [Fact]
+ public void CreateControllerWithEmptyControllerNameThrows()
+ {
+ // Arrange
+ DefaultControllerFactory factory = new DefaultControllerFactory();
+
+ // Act
+ Assert.Throws<ArgumentException>(
+ delegate
+ {
+ ((IControllerFactory)factory).CreateController(
+ new RequestContext(new Mock<HttpContextBase>().Object, new RouteData()),
+ String.Empty);
+ },
+ "Value cannot be null or empty.\r\nParameter name: controllerName");
+ }
+
+ [Fact]
+ public void CreateControllerReturnsControllerInstance()
+ {
+ // Arrange
+ RequestContext requestContext = new RequestContext(new Mock<HttpContextBase>().Object, new RouteData());
+ Mock<DefaultControllerFactory> factoryMock = new Mock<DefaultControllerFactory>();
+ factoryMock.CallBase = true;
+ factoryMock.Setup(o => o.GetControllerType(requestContext, "moo")).Returns(typeof(DummyController));
+
+ // Act
+ IController controller = ((IControllerFactory)factoryMock.Object).CreateController(requestContext, "moo");
+
+ // Assert
+ Assert.IsType<DummyController>(controller);
+ }
+
+ [Fact]
+ public void CreateControllerCanReturnNull()
+ {
+ // Arrange
+ RequestContext requestContext = new RequestContext(new Mock<HttpContextBase>().Object, new RouteData());
+ Mock<DefaultControllerFactory> factoryMock = new Mock<DefaultControllerFactory>();
+ factoryMock.Setup(o => o.GetControllerType(requestContext, "moo")).Returns(typeof(DummyController));
+ factoryMock.Setup(o => o.GetControllerInstance(requestContext, typeof(DummyController))).Returns((ControllerBase)null);
+
+ // Act
+ IController controller = ((IControllerFactory)factoryMock.Object).CreateController(requestContext, "moo");
+
+ // Assert
+ Assert.Null(controller);
+ }
+
+ [Fact]
+ public void DisposeControllerFactoryWithDisposableController()
+ {
+ // Arrange
+ IControllerFactory factory = new DefaultControllerFactory();
+ Mock<ControllerBase> mockController = new Mock<ControllerBase>();
+ Mock<IDisposable> mockDisposable = mockController.As<IDisposable>();
+ mockDisposable.Setup(d => d.Dispose()).Verifiable();
+
+ // Act
+ factory.ReleaseController(mockController.Object);
+
+ // Assert
+ mockDisposable.Verify();
+ }
+
+ [Fact]
+ public void GetControllerInstanceThrowsIfControllerTypeIsNull()
+ {
+ // Arrange
+ Mock<HttpContextBase> contextMock = new Mock<HttpContextBase>();
+ Mock<HttpRequestBase> requestMock = new Mock<HttpRequestBase>();
+ contextMock.Setup(o => o.Request).Returns(requestMock.Object);
+ requestMock.Setup(o => o.Path).Returns("somepath");
+ RequestContext requestContext = new RequestContext(contextMock.Object, new RouteData());
+ Mock<DefaultControllerFactory> factoryMock = new Mock<DefaultControllerFactory> { CallBase = true };
+ factoryMock.Setup(o => o.GetControllerType(requestContext, "moo")).Returns((Type)null);
+
+ // Act
+ Assert.ThrowsHttpException(
+ delegate { ((IControllerFactory)factoryMock.Object).CreateController(requestContext, "moo"); },
+ "The controller for path 'somepath' was not found or does not implement IController.",
+ 404);
+ }
+
+ [Fact]
+ public void GetControllerInstanceThrowsIfControllerTypeIsNotControllerBase()
+ {
+ // Arrange
+ RequestContext requestContext = new RequestContext(new Mock<HttpContextBase>().Object, new RouteData());
+ DefaultControllerFactory factory = new DefaultControllerFactory();
+
+ // Act
+ Assert.Throws<ArgumentException>(
+ delegate { factory.GetControllerInstance(requestContext, typeof(int)); },
+ "The controller type 'System.Int32' must implement IController.\r\nParameter name: controllerType");
+ }
+
+ [Fact]
+ public void GetControllerInstanceWithBadConstructorThrows()
+ {
+ // Arrange
+ Mock<HttpContextBase> contextMock = new Mock<HttpContextBase>();
+ RequestContext requestContext = new RequestContext(contextMock.Object, new RouteData());
+ Mock<DefaultControllerFactory> factoryMock = new Mock<DefaultControllerFactory>();
+ factoryMock.CallBase = true;
+ factoryMock.Setup(o => o.GetControllerType(requestContext, "moo")).Returns(typeof(DummyControllerThrows));
+
+ // Act
+ Exception ex = Assert.Throws<InvalidOperationException>(
+ delegate { ((IControllerFactory)factoryMock.Object).CreateController(requestContext, "moo"); },
+ "An error occurred when trying to create a controller of type 'System.Web.Mvc.Test.DefaultControllerFactoryTest+DummyControllerThrows'. Make sure that the controller has a parameterless public constructor.");
+
+ Assert.Equal("constructor", ex.InnerException.InnerException.Message);
+ }
+
+ [Fact]
+ public void GetControllerSessionBehaviorGuardClauses()
+ {
+ // Arrange
+ RequestContext requestContext = new RequestContext(new Mock<HttpContextBase>().Object, new RouteData());
+ IControllerFactory factory = new DefaultControllerFactory();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => factory.GetControllerSessionBehavior(null, "controllerName"),
+ "requestContext"
+ );
+ Assert.ThrowsArgumentNullOrEmpty(
+ () => factory.GetControllerSessionBehavior(requestContext, null),
+ "controllerName"
+ );
+ Assert.ThrowsArgumentNullOrEmpty(
+ () => factory.GetControllerSessionBehavior(requestContext, ""),
+ "controllerName"
+ );
+ }
+
+ [Fact]
+ public void GetControllerSessionBehaviorReturnsDefaultForNullControllerType()
+ {
+ // Arrange
+ var factory = new DefaultControllerFactory();
+
+ // Act
+ SessionStateBehavior result = factory.GetControllerSessionBehavior(null, null);
+
+ // Assert
+ Assert.Equal(SessionStateBehavior.Default, result);
+ }
+
+ [Fact]
+ public void GetControllerSessionBehaviorReturnsDefaultForControllerWithoutAttribute()
+ {
+ // Arrange
+ var factory = new DefaultControllerFactory();
+
+ // Act
+ SessionStateBehavior result = factory.GetControllerSessionBehavior(null, typeof(object));
+
+ // Assert
+ Assert.Equal(SessionStateBehavior.Default, result);
+ }
+
+ [Fact]
+ public void GetControllerSessionBehaviorReturnsAttributeValueFromController()
+ {
+ // Arrange
+ var factory = new DefaultControllerFactory();
+
+ // Act
+ SessionStateBehavior result = factory.GetControllerSessionBehavior(null, typeof(MyReadOnlyController));
+
+ // Assert
+ Assert.Equal(SessionStateBehavior.ReadOnly, result);
+ }
+
+ [SessionState(SessionStateBehavior.ReadOnly)]
+ class MyReadOnlyController
+ {
+ }
+
+ [Fact]
+ public void GetControllerTypeWithEmptyControllerNameThrows()
+ {
+ // Arrange
+ RequestContext requestContext = new RequestContext(new Mock<HttpContextBase>().Object, new RouteData());
+ DefaultControllerFactory factory = new DefaultControllerFactory();
+
+ // Act
+ Assert.Throws<ArgumentException>(
+ delegate { factory.GetControllerType(requestContext, String.Empty); },
+ "Value cannot be null or empty.\r\nParameter name: controllerName");
+ }
+
+ [Fact]
+ public void GetControllerTypeForNoAssemblies()
+ {
+ // Arrange
+ RequestContext requestContext = new RequestContext(new Mock<HttpContextBase>().Object, new RouteData());
+ DefaultControllerFactory factory = new DefaultControllerFactory();
+ MockBuildManager buildManagerMock = new MockBuildManager(new Assembly[] { });
+ ControllerTypeCache controllerTypeCache = new ControllerTypeCache();
+
+ factory.BuildManager = buildManagerMock;
+ factory.ControllerTypeCache = controllerTypeCache;
+
+ // Act
+ Type controllerType = factory.GetControllerType(requestContext, "sometype");
+
+ // Assert
+ Assert.Null(controllerType);
+ Assert.Equal(0, controllerTypeCache.Count);
+ }
+
+ [Fact]
+ public void GetControllerTypeForOneAssembly()
+ {
+ // Arrange
+ RequestContext requestContext = new RequestContext(new Mock<HttpContextBase>().Object, new RouteData());
+ DefaultControllerFactory factory = GetDefaultControllerFactory("ns1a.ns1b", "ns2a.ns2b");
+ MockBuildManager buildManagerMock = new MockBuildManager(new Assembly[] { Assembly.Load("MvcAssembly1") });
+ ControllerTypeCache controllerTypeCache = new ControllerTypeCache();
+
+ factory.BuildManager = buildManagerMock;
+ factory.ControllerTypeCache = controllerTypeCache;
+
+ // Act
+ Type c1Type = factory.GetControllerType(requestContext, "C1");
+ Type c2Type = factory.GetControllerType(requestContext, "c2");
+
+ // Assert
+ Assembly asm1 = Assembly.Load("MvcAssembly1");
+ Type verifiedC1 = asm1.GetType("NS1a.NS1b.C1Controller");
+ Type verifiedC2 = asm1.GetType("NS2a.NS2b.C2Controller");
+ Assert.Equal(verifiedC1, c1Type);
+ Assert.Equal(verifiedC2, c2Type);
+ Assert.Equal(2, controllerTypeCache.Count);
+ }
+
+ [Fact]
+ public void GetControllerTypeForManyAssemblies()
+ {
+ // Arrange
+ RequestContext requestContext = new RequestContext(new Mock<HttpContextBase>().Object, new RouteData());
+ DefaultControllerFactory factory = GetDefaultControllerFactory("ns1a.ns1b", "ns2a.ns2b", "ns3a.ns3b", "ns4a.ns4b");
+ MockBuildManager buildManagerMock = new MockBuildManager(new Assembly[] { Assembly.Load("MvcAssembly1"), Assembly.Load("MvcAssembly2") });
+ ControllerTypeCache controllerTypeCache = new ControllerTypeCache();
+
+ factory.BuildManager = buildManagerMock;
+ factory.ControllerTypeCache = controllerTypeCache;
+
+ // Act
+ Type c1Type = factory.GetControllerType(requestContext, "C1");
+ Type c2Type = factory.GetControllerType(requestContext, "C2");
+ Type c3Type = factory.GetControllerType(requestContext, "c3"); // lower case
+ Type c4Type = factory.GetControllerType(requestContext, "c4"); // lower case
+
+ // Assert
+ Assembly asm1 = Assembly.Load("MvcAssembly1");
+ Type verifiedC1 = asm1.GetType("NS1a.NS1b.C1Controller");
+ Type verifiedC2 = asm1.GetType("NS2a.NS2b.C2Controller");
+ Assembly asm2 = Assembly.Load("MvcAssembly2");
+ Type verifiedC3 = asm2.GetType("NS3a.NS3b.C3Controller");
+ Type verifiedC4 = asm2.GetType("NS4a.NS4b.C4Controller");
+ Assert.NotNull(verifiedC1);
+ Assert.NotNull(verifiedC2);
+ Assert.NotNull(verifiedC3);
+ Assert.NotNull(verifiedC4);
+ Assert.Equal(verifiedC1, c1Type);
+ Assert.Equal(verifiedC2, c2Type);
+ Assert.Equal(verifiedC3, c3Type);
+ Assert.Equal(verifiedC4, c4Type);
+ Assert.Equal(4, controllerTypeCache.Count);
+ }
+
+ [Fact]
+ public void GetControllerTypeDoesNotThrowIfSameControllerMatchedMultipleNamespaces()
+ {
+ // both namespaces "ns3a" and "ns3a.ns3b" will match a controller type, but it's actually
+ // the same type. in this case, we shouldn't throw.
+
+ // Arrange
+ RequestContext requestContext = GetRequestContextWithNamespaces("ns3a", "ns3a.ns3b");
+ requestContext.RouteData.DataTokens["UseNamespaceFallback"] = false;
+ DefaultControllerFactory factory = GetDefaultControllerFactory("ns1a.ns1b", "ns2a.ns2b");
+ MockBuildManager buildManagerMock = new MockBuildManager(new Assembly[] { Assembly.Load("MvcAssembly3") });
+ ControllerTypeCache controllerTypeCache = new ControllerTypeCache();
+
+ factory.BuildManager = buildManagerMock;
+ factory.ControllerTypeCache = controllerTypeCache;
+
+ // Act
+ Type c1Type = factory.GetControllerType(requestContext, "C1");
+
+ // Assert
+ Assembly asm3 = Assembly.Load("MvcAssembly3");
+ Type verifiedC1 = asm3.GetType("NS3a.NS3b.C1Controller");
+ Assert.NotNull(verifiedC1);
+ Assert.Equal(verifiedC1, c1Type);
+ }
+
+ [Fact]
+ public void GetControllerTypeForAssembliesWithSameTypeNamesInDifferentNamespaces()
+ {
+ // Arrange
+ RequestContext requestContext = new RequestContext(new Mock<HttpContextBase>().Object, new RouteData());
+ DefaultControllerFactory factory = GetDefaultControllerFactory("ns1a.ns1b", "ns2a.ns2b");
+ MockBuildManager buildManagerMock = new MockBuildManager(new Assembly[] { Assembly.Load("MvcAssembly1"), Assembly.Load("MvcAssembly3") });
+ ControllerTypeCache controllerTypeCache = new ControllerTypeCache();
+
+ factory.BuildManager = buildManagerMock;
+ factory.ControllerTypeCache = controllerTypeCache;
+
+ // Act
+ Type c1Type = factory.GetControllerType(requestContext, "C1");
+ Type c2Type = factory.GetControllerType(requestContext, "C2");
+
+ // Assert
+ Assembly asm1 = Assembly.Load("MvcAssembly1");
+ Type verifiedC1 = asm1.GetType("NS1a.NS1b.C1Controller");
+ Type verifiedC2 = asm1.GetType("NS2a.NS2b.C2Controller");
+ Assert.NotNull(verifiedC1);
+ Assert.NotNull(verifiedC2);
+ Assert.Equal(verifiedC1, c1Type);
+ Assert.Equal(verifiedC2, c2Type);
+ Assert.Equal(4, controllerTypeCache.Count);
+ }
+
+ [Fact]
+ public void GetControllerTypeForAssembliesWithSameTypeNamesInDifferentNamespacesThrowsIfAmbiguous()
+ {
+ // Arrange
+ RequestContext requestContext = new RequestContext(new Mock<HttpContextBase>().Object, new RouteData());
+ DefaultControllerFactory factory = GetDefaultControllerFactory("ns1a.ns1b", "ns3a.ns3b");
+ MockBuildManager buildManagerMock = new MockBuildManager(new Assembly[] { Assembly.Load("MvcAssembly1"), Assembly.Load("MvcAssembly3") });
+ ControllerTypeCache controllerTypeCache = new ControllerTypeCache();
+
+ factory.BuildManager = buildManagerMock;
+ factory.ControllerTypeCache = controllerTypeCache;
+
+ // Act
+ Assert.Throws<InvalidOperationException>(
+ delegate { factory.GetControllerType(requestContext, "C1"); },
+ @"Multiple types were found that match the controller named 'C1'. This can happen if the route that services this request does not specify namespaces to search for a controller that matches the request. If this is the case, register this route by calling an overload of the 'MapRoute' method that takes a 'namespaces' parameter.
+
+The request for 'C1' has found the following matching controllers:
+NS1a.NS1b.C1Controller
+NS3a.NS3b.C1Controller");
+
+ // Assert
+ Assert.Equal(4, controllerTypeCache.Count);
+ }
+
+ [Fact]
+ public void GetControllerTypeForAssembliesWithSameTypeNamesInSameNamespaceThrows()
+ {
+ // Arrange
+ RequestContext requestContext = new RequestContext(new Mock<HttpContextBase>().Object, new RouteData());
+ DefaultControllerFactory factory = GetDefaultControllerFactory("ns1a.ns1b");
+ MockBuildManager buildManagerMock = new MockBuildManager(new Assembly[] { Assembly.Load("MvcAssembly1"), Assembly.Load("MvcAssembly4") });
+ ControllerTypeCache controllerTypeCache = new ControllerTypeCache();
+
+ factory.BuildManager = buildManagerMock;
+ factory.ControllerTypeCache = controllerTypeCache;
+
+ // Act
+ Assert.Throws<InvalidOperationException>(
+ delegate { factory.GetControllerType(requestContext, "C1"); },
+ @"Multiple types were found that match the controller named 'C1'. This can happen if the route that services this request does not specify namespaces to search for a controller that matches the request. If this is the case, register this route by calling an overload of the 'MapRoute' method that takes a 'namespaces' parameter.
+
+The request for 'C1' has found the following matching controllers:
+NS1a.NS1b.C1Controller
+NS1a.NS1b.C1Controller");
+
+ // Assert
+ Assert.Equal(4, controllerTypeCache.Count);
+ }
+
+ [Fact]
+ public void GetControllerTypeSearchesAllNamespacesAsLastResort()
+ {
+ // Arrange
+ RequestContext requestContext = GetRequestContextWithNamespaces("ns3a.ns3b");
+ DefaultControllerFactory factory = GetDefaultControllerFactory("ns1a.ns1b");
+ MockBuildManager buildManagerMock = new MockBuildManager(new Assembly[] { Assembly.Load("MvcAssembly1") });
+ ControllerTypeCache controllerTypeCache = new ControllerTypeCache();
+
+ factory.BuildManager = buildManagerMock;
+ factory.ControllerTypeCache = controllerTypeCache;
+
+ // Act
+ Type c2Type = factory.GetControllerType(requestContext, "C2");
+
+ // Assert
+ Assembly asm1 = Assembly.Load("MvcAssembly1");
+ Type verifiedC2 = asm1.GetType("NS2a.NS2b.C2Controller");
+ Assert.NotNull(verifiedC2);
+ Assert.Equal(verifiedC2, c2Type);
+ Assert.Equal(2, controllerTypeCache.Count);
+ }
+
+ [Fact]
+ public void GetControllerTypeSearchesOnlyRouteDefinedNamespacesIfRequested()
+ {
+ // Arrange
+ RequestContext requestContext = GetRequestContextWithNamespaces("ns3a.ns3b");
+ requestContext.RouteData.DataTokens["UseNamespaceFallback"] = false;
+ DefaultControllerFactory factory = GetDefaultControllerFactory("ns1a.ns1b", "ns2a.ns2b");
+ MockBuildManager buildManagerMock = new MockBuildManager(new Assembly[] { Assembly.Load("MvcAssembly1"), Assembly.Load("MvcAssembly3") });
+ ControllerTypeCache controllerTypeCache = new ControllerTypeCache();
+
+ factory.BuildManager = buildManagerMock;
+ factory.ControllerTypeCache = controllerTypeCache;
+
+ // Act
+ Type c1Type = factory.GetControllerType(requestContext, "C1");
+ Type c2Type = factory.GetControllerType(requestContext, "C2");
+
+ // Assert
+ Assembly asm3 = Assembly.Load("MvcAssembly3");
+ Type verifiedC1 = asm3.GetType("NS3a.NS3b.C1Controller");
+ Assert.NotNull(verifiedC1);
+ Assert.Equal(verifiedC1, c1Type);
+ Assert.Null(c2Type);
+ }
+
+ [Fact]
+ public void GetControllerTypeSearchesRouteDefinedNamespacesBeforeApplicationDefinedNamespaces()
+ {
+ // Arrange
+ RequestContext requestContext = GetRequestContextWithNamespaces("ns3a.ns3b");
+ DefaultControllerFactory factory = GetDefaultControllerFactory("ns1a.ns1b", "ns2a.ns2b");
+ MockBuildManager buildManagerMock = new MockBuildManager(new Assembly[] { Assembly.Load("MvcAssembly1"), Assembly.Load("MvcAssembly3") });
+ ControllerTypeCache controllerTypeCache = new ControllerTypeCache();
+
+ factory.BuildManager = buildManagerMock;
+ factory.ControllerTypeCache = controllerTypeCache;
+
+ // Act
+ Type c1Type = factory.GetControllerType(requestContext, "C1");
+ Type c2Type = factory.GetControllerType(requestContext, "C2");
+
+ // Assert
+ Assembly asm1 = Assembly.Load("MvcAssembly1");
+ Type verifiedC2 = asm1.GetType("NS2a.NS2b.C2Controller");
+ Assembly asm3 = Assembly.Load("MvcAssembly3");
+ Type verifiedC1 = asm3.GetType("NS3a.NS3b.C1Controller");
+ Assert.NotNull(verifiedC1);
+ Assert.NotNull(verifiedC2);
+ Assert.Equal(verifiedC1, c1Type);
+ Assert.Equal(verifiedC2, c2Type);
+ Assert.Equal(4, controllerTypeCache.Count);
+ }
+
+ [Fact]
+ public void GetControllerTypeThatDoesntExist()
+ {
+ // Arrange
+ RequestContext requestContext = new RequestContext(new Mock<HttpContextBase>().Object, new RouteData());
+ DefaultControllerFactory factory = GetDefaultControllerFactory("ns1a.ns1b", "ns2a.ns2b", "ns3a.ns3b", "ns4a.ns4b");
+ MockBuildManager buildManagerMock = new MockBuildManager(new Assembly[] { Assembly.Load("MvcAssembly1"), Assembly.Load("MvcAssembly2"), Assembly.Load("MvcAssembly3"), Assembly.Load("MvcAssembly4") });
+ ControllerTypeCache controllerTypeCache = new ControllerTypeCache();
+
+ factory.BuildManager = buildManagerMock;
+ factory.ControllerTypeCache = controllerTypeCache;
+
+ // Act
+ Type randomType1 = factory.GetControllerType(requestContext, "Cx");
+ Type randomType2 = factory.GetControllerType(requestContext, "Cy");
+ Type randomType3 = factory.GetControllerType(requestContext, "Foo.Bar");
+ Type randomType4 = factory.GetControllerType(requestContext, "C1Controller");
+
+ // Assert
+ Assert.Null(randomType1);
+ Assert.Null(randomType2);
+ Assert.Null(randomType3);
+ Assert.Null(randomType4);
+ Assert.Equal(8, controllerTypeCache.Count);
+ }
+
+ [Fact]
+ public void IsControllerType()
+ {
+ // Act
+ bool isController1 = ControllerTypeCache.IsControllerType(null);
+ bool isController2 = ControllerTypeCache.IsControllerType(typeof(NonPublicController));
+ bool isController3 = ControllerTypeCache.IsControllerType(typeof(MisspelledKontroller));
+ bool isController4 = ControllerTypeCache.IsControllerType(typeof(AbstractController));
+ bool isController5 = ControllerTypeCache.IsControllerType(typeof(NonIControllerController));
+ bool isController6 = ControllerTypeCache.IsControllerType(typeof(Goodcontroller));
+
+ // Assert
+ Assert.False(isController1);
+ Assert.False(isController2);
+ Assert.False(isController3);
+ Assert.False(isController4);
+ Assert.False(isController5);
+ Assert.True(isController6);
+ }
+
+ [Theory]
+ [InlineData(null, false)]
+ [InlineData("", true)]
+ [InlineData("Dummy", false)]
+ [InlineData("Dummy.*", true)]
+ [InlineData("Dummy.Controller.*", false)]
+ [InlineData("Dummy.Controllers", true)]
+ [InlineData("Dummy.Controllers.*", true)]
+ [InlineData("Dummy.Controllers*", false)]
+ public void IsNamespaceMatch(string testNamespace, bool expectedResult)
+ {
+ // Act & Assert
+ Assert.Equal(expectedResult, ControllerTypeCache.IsNamespaceMatch(testNamespace, "Dummy.Controllers"));
+ }
+
+ [Fact]
+ public void GetControllerInstanceConsultsSetControllerActivator()
+ {
+ //Arrange
+ Mock<IControllerActivator> activator = new Mock<IControllerActivator>();
+ DefaultControllerFactory factory = new DefaultControllerFactory(activator.Object);
+ RequestContext context = new RequestContext();
+
+ //Act
+ factory.GetControllerInstance(context, typeof(Goodcontroller));
+
+ //Assert
+ activator.Verify(l => l.Create(context, typeof(Goodcontroller)));
+ }
+
+ [Fact]
+ public void GetControllerDelegatesToActivatorResolver()
+ {
+ //Arrange
+ var context = new RequestContext();
+ var expectedController = new Goodcontroller();
+ var resolverActivator = new Mock<IControllerActivator>();
+ resolverActivator.Setup(a => a.Create(context, typeof(Goodcontroller))).Returns(expectedController);
+ var activatorResolver = new Resolver<IControllerActivator> { Current = resolverActivator.Object };
+ var factory = new DefaultControllerFactory(null, activatorResolver, null);
+
+ //Act
+ IController returnedController = factory.GetControllerInstance(context, typeof(Goodcontroller));
+
+ //Assert
+ Assert.Same(returnedController, expectedController);
+ }
+
+ [Fact]
+ public void GetControllerDelegatesToDependencyResolveWhenActivatorResolverIsNull()
+ {
+ // Arrange
+ var context = new RequestContext();
+ var expectedController = new Goodcontroller();
+ var dependencyResolver = new Mock<IDependencyResolver>(MockBehavior.Strict);
+ dependencyResolver.Setup(dr => dr.GetService(typeof(Goodcontroller))).Returns(expectedController);
+ var factory = new DefaultControllerFactory(null, null, dependencyResolver.Object);
+
+ // Act
+ IController returnedController = factory.GetControllerInstance(context, typeof(Goodcontroller));
+
+ // Assert
+ Assert.Same(returnedController, expectedController);
+ }
+
+ [Fact]
+ public void GetControllerDelegatesToActivatorCreateInstanceWhenDependencyResolverReturnsNull()
+ {
+ // Arrange
+ var context = new RequestContext();
+ var dependencyResolver = new Mock<IDependencyResolver>();
+ var factory = new DefaultControllerFactory(null, null, dependencyResolver.Object);
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ () => factory.GetControllerInstance(context, typeof(NoParameterlessCtor)),
+ "An error occurred when trying to create a controller of type 'System.Web.Mvc.Test.DefaultControllerFactoryTest+NoParameterlessCtor'. Make sure that the controller has a parameterless public constructor."
+ );
+ }
+
+ [Fact]
+ public void ActivatorResolverAndDependencyResolverAreNeverCalledWhenControllerActivatorIsPassedInConstructor()
+ {
+ // Arrange
+ var context = new RequestContext();
+ var expectedController = new Goodcontroller();
+
+ Mock<IControllerActivator> activator = new Mock<IControllerActivator>();
+ activator.Setup(a => a.Create(context, typeof(Goodcontroller))).Returns(expectedController);
+
+ var resolverActivator = new Mock<IControllerActivator>(MockBehavior.Strict);
+ var activatorResolver = new Resolver<IControllerActivator> { Current = resolverActivator.Object };
+
+ var dependencyResolver = new Mock<IDependencyResolver>(MockBehavior.Strict);
+
+ //Act
+ var factory = new DefaultControllerFactory(activator.Object, activatorResolver, dependencyResolver.Object);
+ IController returnedController = factory.GetControllerInstance(context, typeof(Goodcontroller));
+
+ //Assert
+ Assert.Same(returnedController, expectedController);
+ }
+
+ class NoParameterlessCtor : IController
+ {
+ public NoParameterlessCtor(int x)
+ {
+ }
+
+ public void Execute(RequestContext requestContext)
+ {
+ }
+ }
+
+ private static DefaultControllerFactory GetDefaultControllerFactory(params string[] namespaces)
+ {
+ ControllerBuilder builder = new ControllerBuilder();
+ builder.DefaultNamespaces.UnionWith(namespaces);
+ return new DefaultControllerFactory() { ControllerBuilder = builder };
+ }
+
+ private static RequestContext GetRequestContextWithNamespaces(params string[] namespaces)
+ {
+ RouteData routeData = new RouteData();
+ routeData.DataTokens["namespaces"] = namespaces;
+ Mock<HttpContextBase> mockHttpContext = new Mock<HttpContextBase>();
+ RequestContext requestContext = new RequestContext(mockHttpContext.Object, routeData);
+ return requestContext;
+ }
+
+ private sealed class DummyController : ControllerBase
+ {
+ protected override void ExecuteCore()
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ private sealed class DummyControllerThrows : IController
+ {
+ public DummyControllerThrows()
+ {
+ throw new Exception("constructor");
+ }
+
+ #region IController Members
+
+ void IController.Execute(RequestContext requestContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ #endregion
+ }
+
+ public interface IDisposableController : IController, IDisposable
+ {
+ }
+ }
+
+ // BAD: type isn't public
+ internal class NonPublicController : Controller
+ {
+ }
+
+ // BAD: type doesn't end with 'Controller'
+ public class MisspelledKontroller : Controller
+ {
+ }
+
+ // BAD: type is abstract
+ public abstract class AbstractController : Controller
+ {
+ }
+
+ // BAD: type doesn't implement IController
+ public class NonIControllerController
+ {
+ }
+
+ // GOOD: 'Controller' suffix should be case-insensitive
+ public class Goodcontroller : Controller
+ {
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/DefaultModelBinderTest.cs b/test/System.Web.Mvc.Test/Test/DefaultModelBinderTest.cs
new file mode 100644
index 00000000..d4c4ddc6
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/DefaultModelBinderTest.cs
@@ -0,0 +1,2904 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using Microsoft.Web.UnitTestUtil;
+using Moq;
+using Moq.Protected;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ [CLSCompliant(false)]
+ public class DefaultModelBinderTest
+ {
+ [Fact]
+ public void BindComplexElementalModelReturnsIfOnModelUpdatingReturnsFalse()
+ {
+ // Arrange
+ ControllerContext controllerContext = new Mock<ControllerContext>().Object;
+
+ MyModel model = new MyModel() { ReadWriteProperty = 3 };
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, model.GetType()),
+ };
+
+ Mock<DefaultModelBinderHelper> mockHelper = new Mock<DefaultModelBinderHelper>() { CallBase = true };
+ mockHelper.Setup(b => b.PublicOnModelUpdating(controllerContext, It.IsAny<ModelBindingContext>())).Returns(false);
+ DefaultModelBinderHelper helper = mockHelper.Object;
+
+ // Act
+ helper.BindComplexElementalModel(controllerContext, bindingContext, model);
+
+ // Assert
+ Assert.Equal(3, model.ReadWriteProperty);
+ mockHelper.Verify();
+ mockHelper.Verify(b => b.PublicGetModelProperties(controllerContext, It.IsAny<ModelBindingContext>()), Times.Never());
+ mockHelper.Verify(b => b.PublicBindProperty(controllerContext, It.IsAny<ModelBindingContext>(), It.IsAny<PropertyDescriptor>()), Times.Never());
+ }
+
+ [Fact]
+ public void BindComplexModelCanBindArrays()
+ {
+ // Arrange
+ ControllerContext controllerContext = new Mock<ControllerContext>().Object;
+
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(int[])),
+ ModelName = "foo",
+ PropertyFilter = _ => false,
+ ValueProvider = new SimpleValueProvider()
+ {
+ { "foo[0]", null },
+ { "foo[1]", null },
+ { "foo[2]", null }
+ }
+ };
+
+ Mock<IModelBinder> mockInnerBinder = new Mock<IModelBinder>();
+ mockInnerBinder
+ .Setup(b => b.BindModel(It.IsAny<ControllerContext>(), It.IsAny<ModelBindingContext>()))
+ .Returns(
+ delegate(ControllerContext cc, ModelBindingContext bc)
+ {
+ Assert.Equal(controllerContext, cc);
+ Assert.Equal(typeof(int), bc.ModelType);
+ Assert.Equal(bindingContext.ModelState, bc.ModelState);
+ Assert.Equal(bindingContext.PropertyFilter, bc.PropertyFilter);
+ Assert.Equal(bindingContext.ValueProvider, bc.ValueProvider);
+ return Int32.Parse(bc.ModelName.Substring(4, 1), CultureInfo.InvariantCulture);
+ });
+
+ DefaultModelBinder binder = new DefaultModelBinder()
+ {
+ Binders = new ModelBinderDictionary()
+ {
+ { typeof(int), mockInnerBinder.Object }
+ }
+ };
+
+ // Act
+ object newModel = binder.BindComplexModel(controllerContext, bindingContext);
+
+ // Assert
+ var newIntArray = Assert.IsType<int[]>(newModel);
+ Assert.Equal(new[] { 0, 1, 2 }, newIntArray);
+ }
+
+ [Fact]
+ public void BindComplexModelCanBindCollections()
+ {
+ // Arrange
+ ControllerContext controllerContext = new Mock<ControllerContext>().Object;
+
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(IList<int>)),
+ ModelName = "foo",
+ PropertyFilter = _ => false,
+ ValueProvider = new SimpleValueProvider()
+ {
+ { "foo[0]", null },
+ { "foo[1]", null },
+ { "foo[2]", null }
+ }
+ };
+
+ Mock<IModelBinder> mockInnerBinder = new Mock<IModelBinder>();
+ mockInnerBinder
+ .Setup(b => b.BindModel(It.IsAny<ControllerContext>(), It.IsAny<ModelBindingContext>()))
+ .Returns(
+ delegate(ControllerContext cc, ModelBindingContext bc)
+ {
+ Assert.Equal(controllerContext, cc);
+ Assert.Equal(typeof(int), bc.ModelType);
+ Assert.Equal(bindingContext.ModelState, bc.ModelState);
+ Assert.Equal(bindingContext.PropertyFilter, bc.PropertyFilter);
+ Assert.Equal(bindingContext.ValueProvider, bc.ValueProvider);
+ return Int32.Parse(bc.ModelName.Substring(4, 1), CultureInfo.InvariantCulture);
+ });
+
+ DefaultModelBinder binder = new DefaultModelBinder()
+ {
+ Binders = new ModelBinderDictionary()
+ {
+ { typeof(int), mockInnerBinder.Object }
+ }
+ };
+
+ // Act
+ object newModel = binder.BindComplexModel(controllerContext, bindingContext);
+
+ // Assert
+ var modelAsList = Assert.IsAssignableFrom<IList<int>>(newModel);
+ Assert.Equal(new[] { 0, 1, 2 }, modelAsList.ToArray());
+ }
+
+ [Fact]
+ public void BindComplexModelCanBindDictionariesWithDotsNotation()
+ {
+ // Arrange
+ ControllerContext controllerContext = new Mock<ControllerContext>().Object;
+
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(IDictionary<string, CountryState>)),
+ ModelName = "countries",
+ PropertyFilter = _ => true,
+ ValueProvider = new DictionaryValueProvider<object>(new Dictionary<string, object>()
+ {
+ { "countries.CA.Name", "Canada" },
+ { "countries.CA.States[0]", "Québec" },
+ { "countries.CA.States[1]", "British Columbia" },
+ { "countries.US.Name", "United States" },
+ { "countries.US.States[0]", "Washington" },
+ { "countries.US.States[1]", "Oregon" }
+ }, CultureInfo.CurrentCulture)
+ };
+
+ DefaultModelBinder binder = new DefaultModelBinder();
+
+ // Act
+ object newModel = binder.BindComplexModel(controllerContext, bindingContext);
+
+ // Assert
+ var modelAsDictionary = Assert.IsAssignableFrom<IDictionary<string, CountryState>>(newModel);
+ Assert.Equal(2, modelAsDictionary.Count);
+ Assert.Equal("Canada", modelAsDictionary["CA"].Name);
+ Assert.Equal("United States", modelAsDictionary["US"].Name);
+ Assert.Equal(2, modelAsDictionary["CA"].States.Count());
+ Assert.True(modelAsDictionary["CA"].States.Contains("Québec"));
+ Assert.True(modelAsDictionary["CA"].States.Contains("British Columbia"));
+ Assert.Equal(2, modelAsDictionary["US"].States.Count());
+ Assert.True(modelAsDictionary["US"].States.Contains("Washington"));
+ Assert.True(modelAsDictionary["US"].States.Contains("Oregon"));
+ }
+
+ [Fact]
+ public void BindComplexModelCanBindDictionariesWithBracketsNotation()
+ {
+ // Arrange
+ ControllerContext controllerContext = new Mock<ControllerContext>().Object;
+
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(IDictionary<string, CountryState>)),
+ ModelName = "countries",
+ PropertyFilter = _ => true,
+ ValueProvider = new DictionaryValueProvider<object>(new Dictionary<string, object>()
+ {
+ { "countries[CA].Name", "Canada" },
+ { "countries[CA].States[0]", "Québec" },
+ { "countries[CA].States[1]", "British Columbia" },
+ { "countries[US].Name", "United States" },
+ { "countries[US].States[0]", "Washington" },
+ { "countries[US].States[1]", "Oregon" }
+ }, CultureInfo.CurrentCulture)
+ };
+
+ DefaultModelBinder binder = new DefaultModelBinder();
+
+ // Act
+ object newModel = binder.BindComplexModel(controllerContext, bindingContext);
+
+ // Assert
+ var modelAsDictionary = Assert.IsAssignableFrom<IDictionary<string, CountryState>>(newModel);
+ Assert.Equal(2, modelAsDictionary.Count);
+ Assert.Equal("Canada", modelAsDictionary["CA"].Name);
+ Assert.Equal("United States", modelAsDictionary["US"].Name);
+ Assert.Equal(2, modelAsDictionary["CA"].States.Count());
+ Assert.True(modelAsDictionary["CA"].States.Contains("Québec"));
+ Assert.True(modelAsDictionary["CA"].States.Contains("British Columbia"));
+ Assert.Equal(2, modelAsDictionary["US"].States.Count());
+ Assert.True(modelAsDictionary["US"].States.Contains("Washington"));
+ Assert.True(modelAsDictionary["US"].States.Contains("Oregon"));
+ }
+
+ [Fact]
+ public void BindComplexModelCanBindDictionariesWithBracketsAndDotsNotation()
+ {
+ // Arrange
+ ControllerContext controllerContext = new Mock<ControllerContext>().Object;
+
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(IDictionary<string, CountryState>)),
+ ModelName = "countries",
+ PropertyFilter = _ => true,
+ ValueProvider = new DictionaryValueProvider<object>(new Dictionary<string, object>()
+ {
+ { "countries[CA].Name", "Canada" },
+ { "countries[CA].States[0]", "Québec" },
+ { "countries.CA.States[1]", "British Columbia" },
+ { "countries.US.Name", "United States" },
+ { "countries.US.States[0]", "Washington" },
+ { "countries.US.States[1]", "Oregon" }
+ }, CultureInfo.CurrentCulture)
+ };
+
+ DefaultModelBinder binder = new DefaultModelBinder();
+
+ // Act
+ object newModel = binder.BindComplexModel(controllerContext, bindingContext);
+
+ // Assert
+ var modelAsDictionary = Assert.IsAssignableFrom<IDictionary<string, CountryState>>(newModel);
+ Assert.Equal(2, modelAsDictionary.Count);
+ Assert.Equal("Canada", modelAsDictionary["CA"].Name);
+ Assert.Equal("United States", modelAsDictionary["US"].Name);
+ Assert.Equal(1, modelAsDictionary["CA"].States.Count());
+ Assert.True(modelAsDictionary["CA"].States.Contains("Québec"));
+
+ // We do not accept double notation for a same entry, so we can't find that state.
+ Assert.False(modelAsDictionary["CA"].States.Contains("British Columbia"));
+ Assert.Equal(2, modelAsDictionary["US"].States.Count());
+ Assert.True(modelAsDictionary["US"].States.Contains("Washington"));
+ Assert.True(modelAsDictionary["US"].States.Contains("Oregon"));
+ }
+
+ [Fact]
+ public void BindComplexModelCanBindDictionaries()
+ {
+ // Arrange
+ ControllerContext controllerContext = new Mock<ControllerContext>().Object;
+
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(IDictionary<int, string>)),
+ ModelName = "foo",
+ PropertyFilter = _ => false,
+ ValueProvider = new SimpleValueProvider()
+ {
+ { "foo[0].key", null }, { "foo[0].value", null },
+ { "foo[1].key", null }, { "foo[1].value", null },
+ { "foo[2].key", null }, { "foo[2].value", null }
+ }
+ };
+
+ Mock<IModelBinder> mockIntBinder = new Mock<IModelBinder>();
+ mockIntBinder
+ .Setup(b => b.BindModel(It.IsAny<ControllerContext>(), It.IsAny<ModelBindingContext>()))
+ .Returns(
+ delegate(ControllerContext cc, ModelBindingContext bc)
+ {
+ Assert.Equal(controllerContext, cc);
+ Assert.Equal(typeof(int), bc.ModelType);
+ Assert.Equal(bindingContext.ModelState, bc.ModelState);
+ Assert.Equal(new ModelBindingContext().PropertyFilter, bc.PropertyFilter);
+ Assert.Equal(bindingContext.ValueProvider, bc.ValueProvider);
+ return Int32.Parse(bc.ModelName.Substring(4, 1), CultureInfo.InvariantCulture) + 10;
+ });
+
+ Mock<IModelBinder> mockStringBinder = new Mock<IModelBinder>();
+ mockStringBinder
+ .Setup(b => b.BindModel(It.IsAny<ControllerContext>(), It.IsAny<ModelBindingContext>()))
+ .Returns(
+ delegate(ControllerContext cc, ModelBindingContext bc)
+ {
+ Assert.Equal(controllerContext, cc);
+ Assert.Equal(typeof(string), bc.ModelType);
+ Assert.Equal(bindingContext.ModelState, bc.ModelState);
+ Assert.Equal(bindingContext.PropertyFilter, bc.PropertyFilter);
+ Assert.Equal(bindingContext.ValueProvider, bc.ValueProvider);
+ return (Int32.Parse(bc.ModelName.Substring(4, 1), CultureInfo.InvariantCulture) + 10) + "Value";
+ });
+
+ DefaultModelBinder binder = new DefaultModelBinder()
+ {
+ Binders = new ModelBinderDictionary()
+ {
+ { typeof(int), mockIntBinder.Object },
+ { typeof(string), mockStringBinder.Object }
+ }
+ };
+
+ // Act
+ object newModel = binder.BindComplexModel(controllerContext, bindingContext);
+
+ // Assert
+ var modelAsDictionary = Assert.IsAssignableFrom<IDictionary<int, string>>(newModel);
+ Assert.Equal(3, modelAsDictionary.Count);
+ Assert.Equal("10Value", modelAsDictionary[10]);
+ Assert.Equal("11Value", modelAsDictionary[11]);
+ Assert.Equal("12Value", modelAsDictionary[12]);
+ }
+
+ [Fact]
+ public void BindComplexModelCanBindObjects()
+ {
+ // Arrange
+ ControllerContext controllerContext = new Mock<ControllerContext>().Object;
+
+ ModelWithoutBindAttribute model = new ModelWithoutBindAttribute()
+ {
+ Foo = "FooPreValue",
+ Bar = "BarPreValue",
+ Baz = "BazPreValue",
+ };
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, model.GetType()),
+ ValueProvider = new SimpleValueProvider() { { "Foo", null }, { "Bar", null } }
+ };
+
+ Mock<IModelBinder> mockInnerBinder = new Mock<IModelBinder>();
+ mockInnerBinder
+ .Setup(b => b.BindModel(It.IsAny<ControllerContext>(), It.IsAny<ModelBindingContext>()))
+ .Returns(
+ delegate(ControllerContext cc, ModelBindingContext bc)
+ {
+ Assert.Equal(controllerContext, cc);
+ Assert.Equal(bindingContext.ValueProvider, bc.ValueProvider);
+ return bc.ModelName + "PostValue";
+ });
+
+ DefaultModelBinder binder = new DefaultModelBinder()
+ {
+ Binders = new ModelBinderDictionary()
+ {
+ { typeof(string), mockInnerBinder.Object }
+ }
+ };
+
+ // Act
+ object updatedModel = binder.BindComplexModel(controllerContext, bindingContext);
+
+ // Assert
+ Assert.Same(model, updatedModel);
+ Assert.Equal("FooPostValue", model.Foo);
+ Assert.Equal("BarPostValue", model.Bar);
+ Assert.Equal("BazPreValue", model.Baz);
+ }
+
+ [Fact]
+ public void BindComplexModelReturnsNullArrayIfNoValuesProvided()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(int[])),
+ ModelName = "foo",
+ ValueProvider = new SimpleValueProvider() { { "foo", null } }
+ };
+
+ Mock<IModelBinder> mockInnerBinder = new Mock<IModelBinder>();
+ mockInnerBinder
+ .Setup(b => b.BindModel(It.IsAny<ControllerContext>(), It.IsAny<ModelBindingContext>()))
+ .Returns(
+ delegate(ControllerContext cc, ModelBindingContext bc) { return Int32.Parse(bc.ModelName.Substring(4, 1), CultureInfo.InvariantCulture); });
+
+ DefaultModelBinder binder = new DefaultModelBinder()
+ {
+ Binders = new ModelBinderDictionary()
+ {
+ { typeof(int), mockInnerBinder.Object }
+ }
+ };
+
+ // Act
+ object newModel = binder.BindComplexModel(null, bindingContext);
+
+ // Assert
+ Assert.Null(newModel);
+ }
+
+ [Fact]
+ public void BindComplexModelWhereModelTypeContainsBindAttribute()
+ {
+ // Arrange
+ ControllerContext controllerContext = new Mock<ControllerContext>().Object;
+
+ ModelWithBindAttribute model = new ModelWithBindAttribute()
+ {
+ Foo = "FooPreValue",
+ Bar = "BarPreValue",
+ Baz = "BazPreValue",
+ };
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, model.GetType()),
+ ValueProvider = new SimpleValueProvider() { { "Foo", null }, { "Bar", null } }
+ };
+
+ Mock<IModelBinder> mockInnerBinder = new Mock<IModelBinder>();
+ mockInnerBinder
+ .Setup(b => b.BindModel(It.IsAny<ControllerContext>(), It.IsAny<ModelBindingContext>()))
+ .Returns(
+ delegate(ControllerContext cc, ModelBindingContext bc)
+ {
+ Assert.Equal(controllerContext, cc);
+ Assert.Equal(bindingContext.ValueProvider, bc.ValueProvider);
+ return bc.ModelName + "PostValue";
+ });
+
+ DefaultModelBinder binder = new DefaultModelBinder()
+ {
+ Binders = new ModelBinderDictionary()
+ {
+ { typeof(string), mockInnerBinder.Object }
+ }
+ };
+
+ // Act
+ binder.BindComplexModel(controllerContext, bindingContext);
+
+ // Assert
+ Assert.Equal("FooPreValue", model.Foo);
+ Assert.Equal("BarPostValue", model.Bar);
+ Assert.Equal("BazPreValue", model.Baz);
+ }
+
+ [Fact]
+ public void BindComplexModelWhereModelTypeDoesNotContainBindAttribute()
+ {
+ // Arrange
+ ControllerContext controllerContext = new Mock<ControllerContext>().Object;
+
+ ModelWithoutBindAttribute model = new ModelWithoutBindAttribute()
+ {
+ Foo = "FooPreValue",
+ Bar = "BarPreValue",
+ Baz = "BazPreValue",
+ };
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, model.GetType()),
+ ValueProvider = new SimpleValueProvider() { { "Foo", null }, { "Bar", null } }
+ };
+
+ Mock<IModelBinder> mockInnerBinder = new Mock<IModelBinder>();
+ mockInnerBinder
+ .Setup(b => b.BindModel(It.IsAny<ControllerContext>(), It.IsAny<ModelBindingContext>()))
+ .Returns(
+ delegate(ControllerContext cc, ModelBindingContext bc)
+ {
+ Assert.Equal(controllerContext, cc);
+ Assert.Equal(bindingContext.ValueProvider, bc.ValueProvider);
+ return bc.ModelName + "PostValue";
+ });
+
+ DefaultModelBinder binder = new DefaultModelBinder()
+ {
+ Binders = new ModelBinderDictionary()
+ {
+ { typeof(string), mockInnerBinder.Object }
+ }
+ };
+
+ // Act
+ binder.BindComplexModel(controllerContext, bindingContext);
+
+ // Assert
+ Assert.Equal("FooPostValue", model.Foo);
+ Assert.Equal("BarPostValue", model.Bar);
+ Assert.Equal("BazPreValue", model.Baz);
+ }
+
+ // BindModel tests
+
+ [Fact]
+ public void BindModelCanBindObjects()
+ {
+ // Arrange
+ ControllerContext controllerContext = new Mock<ControllerContext>().Object;
+
+ ModelWithoutBindAttribute model = new ModelWithoutBindAttribute()
+ {
+ Foo = "FooPreValue",
+ Bar = "BarPreValue",
+ Baz = "BazPreValue",
+ };
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, model.GetType()),
+ ValueProvider = new SimpleValueProvider() { { "Foo", null }, { "Bar", null } }
+ };
+
+ Mock<IModelBinder> mockInnerBinder = new Mock<IModelBinder>();
+ mockInnerBinder
+ .Setup(b => b.BindModel(It.IsAny<ControllerContext>(), It.IsAny<ModelBindingContext>()))
+ .Returns(
+ delegate(ControllerContext cc, ModelBindingContext bc)
+ {
+ Assert.Equal(controllerContext, cc);
+ Assert.Equal(bindingContext.ValueProvider, bc.ValueProvider);
+ return bc.ModelName + "PostValue";
+ });
+
+ DefaultModelBinder binder = new DefaultModelBinder()
+ {
+ Binders = new ModelBinderDictionary()
+ {
+ { typeof(string), mockInnerBinder.Object }
+ }
+ };
+
+ // Act
+ object updatedModel = binder.BindModel(controllerContext, bindingContext);
+
+ // Assert
+ Assert.Same(model, updatedModel);
+ Assert.Equal("FooPostValue", model.Foo);
+ Assert.Equal("BarPostValue", model.Bar);
+ Assert.Equal("BazPreValue", model.Baz);
+ }
+
+ [Fact]
+ public void BindModelCanBindSimpleTypes()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(int)),
+ ModelName = "foo",
+ ValueProvider = new SimpleValueProvider()
+ {
+ { "foo", "42" }
+ }
+ };
+
+ DefaultModelBinder binder = new DefaultModelBinder();
+
+ // Act
+ object updatedModel = binder.BindModel(new ControllerContext(), bindingContext);
+
+ // Assert
+ Assert.Equal(42, updatedModel);
+ }
+
+ [Fact]
+ public void BindModel_PerformsValidationByDefault()
+ {
+ // Arrange
+ ModelMetadata metadata = new DataAnnotationsModelMetadataProvider().GetMetadataForType(null, typeof(string));
+
+ ControllerContext controllerContext = new ControllerContext();
+ controllerContext.Controller = new SimpleController();
+
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = metadata,
+ ModelName = "foo",
+ ValueProvider = new CustomUnvalidatedValueProvider()
+ };
+
+ DefaultModelBinder binder = new DefaultModelBinder();
+
+ // Act
+ object updatedModel = binder.BindModel(controllerContext, bindingContext);
+
+ // Assert
+ Assert.Equal("fooValidated", updatedModel);
+ }
+
+ [Fact]
+ public void BindModel_SkipsValidationIfControllerOptsOut()
+ {
+ // Arrange
+ ModelMetadata metadata = new DataAnnotationsModelMetadataProvider().GetMetadataForType(null, typeof(string));
+
+ ControllerContext controllerContext = new ControllerContext();
+ controllerContext.Controller = new SimpleController();
+ controllerContext.Controller.ValidateRequest = false;
+
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = metadata,
+ ModelName = "foo",
+ ValueProvider = new CustomUnvalidatedValueProvider()
+ };
+
+ DefaultModelBinder binder = new DefaultModelBinder();
+
+ // Act
+ object updatedModel = binder.BindModel(controllerContext, bindingContext);
+
+ // Assert
+ Assert.Equal("fooUnvalidated", updatedModel);
+ }
+
+ [Fact]
+ public void BindModel_SkipsValidationIfModelOptsOut()
+ {
+ // Arrange
+ ModelMetadata metadata = new DataAnnotationsModelMetadataProvider().GetMetadataForType(null, typeof(string));
+ metadata.RequestValidationEnabled = false;
+
+ ControllerContext controllerContext = new ControllerContext();
+ controllerContext.Controller = new SimpleController();
+
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = metadata,
+ ModelName = "foo",
+ ValueProvider = new CustomUnvalidatedValueProvider()
+ };
+
+ DefaultModelBinder binder = new DefaultModelBinder();
+
+ // Act
+ object updatedModel = binder.BindModel(controllerContext, bindingContext);
+
+ // Assert
+ Assert.Equal("fooUnvalidated", updatedModel);
+ }
+
+ [Fact]
+ public void BindModelReturnsNullIfKeyNotFound()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(int)),
+ ModelName = "foo",
+ ValueProvider = new SimpleValueProvider()
+ };
+
+ DefaultModelBinder binder = new DefaultModelBinder();
+
+ // Act
+ object returnedModel = binder.BindModel(new ControllerContext(), bindingContext);
+
+ // Assert
+ Assert.Null(returnedModel);
+ }
+
+ [Fact]
+ public void BindModelThrowsIfBindingContextIsNull()
+ {
+ // Arrange
+ DefaultModelBinder binder = new DefaultModelBinder();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { binder.BindModel(new ControllerContext(), null); }, "bindingContext");
+ }
+
+ [Fact]
+ public void BindModelValuesCanBeOverridden()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => new ModelWithoutBindAttribute(), typeof(ModelWithoutBindAttribute)),
+ ModelName = "",
+ ValueProvider = new SimpleValueProvider()
+ {
+ { "foo", "FooPostValue" },
+ { "bar", "BarPostValue" },
+ { "baz", "BazPostValue" }
+ }
+ };
+ Mock<DefaultModelBinder> binder = new Mock<DefaultModelBinder> { CallBase = true };
+ binder.Protected().Setup<object>("GetPropertyValue",
+ ItExpr.IsAny<ControllerContext>(), ItExpr.IsAny<ModelBindingContext>(),
+ ItExpr.IsAny<PropertyDescriptor>(), ItExpr.IsAny<IModelBinder>())
+ .Returns("Hello, world!");
+
+ // Act
+ ModelWithoutBindAttribute model = (ModelWithoutBindAttribute)binder.Object.BindModel(new ControllerContext(), bindingContext);
+
+ // Assert
+ Assert.Equal("Hello, world!", model.Bar);
+ Assert.Equal("Hello, world!", model.Baz);
+ Assert.Equal("Hello, world!", model.Foo);
+ }
+
+ [Fact]
+ public void BindModelWithTypeConversionErrorUpdatesModelStateMessage()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => new PropertyTestingModel(), typeof(PropertyTestingModel)),
+ ModelName = "",
+ ValueProvider = new SimpleValueProvider()
+ {
+ { "IntReadWrite", "foo" }
+ },
+ };
+ DefaultModelBinder binder = new DefaultModelBinder();
+
+ // Act
+ PropertyTestingModel model = (PropertyTestingModel)binder.BindModel(new ControllerContext(), bindingContext);
+
+ // Assert
+ ModelState modelState = bindingContext.ModelState["IntReadWrite"];
+ Assert.NotNull(modelState);
+ Assert.Single(modelState.Errors);
+ Assert.Equal("The value 'foo' is not valid for IntReadWrite.", modelState.Errors[0].ErrorMessage);
+ }
+
+ [Fact]
+ public void BindModelWithPrefix()
+ {
+ // Arrange
+ ModelWithoutBindAttribute model = new ModelWithoutBindAttribute()
+ {
+ Foo = "FooPreValue",
+ Bar = "BarPreValue",
+ Baz = "BazPreValue",
+ };
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, model.GetType()),
+ ModelName = "prefix",
+ ValueProvider = new SimpleValueProvider()
+ {
+ { "prefix.foo", "FooPostValue" },
+ { "prefix.bar", "BarPostValue" }
+ }
+ };
+
+ DefaultModelBinder binder = new DefaultModelBinder();
+
+ // Act
+ object updatedModel = binder.BindModel(new ControllerContext(), bindingContext);
+
+ // Assert
+ Assert.Same(model, updatedModel);
+ Assert.Equal("FooPostValue", model.Foo);
+ Assert.Equal("BarPostValue", model.Bar);
+ Assert.Equal("BazPreValue", model.Baz);
+ }
+
+ [Fact]
+ public void BindModelWithPrefixAndFallback()
+ {
+ // Arrange
+ ModelWithoutBindAttribute model = new ModelWithoutBindAttribute()
+ {
+ Foo = "FooPreValue",
+ Bar = "BarPreValue",
+ Baz = "BazPreValue",
+ };
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ FallbackToEmptyPrefix = true,
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, model.GetType()),
+ ModelName = "prefix",
+ ValueProvider = new SimpleValueProvider()
+ {
+ { "foo", "FooPostValue" },
+ { "bar", "BarPostValue" }
+ }
+ };
+
+ DefaultModelBinder binder = new DefaultModelBinder();
+
+ // Act
+ object updatedModel = binder.BindModel(new ControllerContext(), bindingContext);
+
+ // Assert
+ Assert.Same(model, updatedModel);
+ Assert.Equal("FooPostValue", model.Foo);
+ Assert.Equal("BarPostValue", model.Bar);
+ Assert.Equal("BazPreValue", model.Baz);
+ }
+
+ [Fact]
+ public void BindModelWithPrefixReturnsNullIfFallbackNotSpecifiedAndValueProviderContainsNoEntries()
+ {
+ // Arrange
+ ModelWithoutBindAttribute model = new ModelWithoutBindAttribute()
+ {
+ Foo = "FooPreValue",
+ Bar = "BarPreValue",
+ Baz = "BazPreValue",
+ };
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, model.GetType()),
+ ModelName = "prefix",
+ ValueProvider = new SimpleValueProvider()
+ {
+ { "foo", "FooPostValue" },
+ { "bar", "BarPostValue" }
+ }
+ };
+
+ DefaultModelBinder binder = new DefaultModelBinder();
+
+ // Act
+ object updatedModel = binder.BindModel(new ControllerContext(), bindingContext);
+
+ // Assert
+ Assert.Null(updatedModel);
+ }
+
+ [Fact]
+ public void BindModelReturnsNullIfSimpleTypeNotFound()
+ {
+ // DevDiv 216165: ModelBinders should not try and instantiate simple types
+
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(string)),
+ ModelName = "prefix",
+ ValueProvider = new SimpleValueProvider()
+ {
+ { "prefix.foo", "foo" },
+ { "prefix.bar", "bar" }
+ }
+ };
+
+ DefaultModelBinder binder = new DefaultModelBinder();
+
+ // Act
+ object updatedModel = binder.BindModel(new ControllerContext(), bindingContext);
+
+ // Assert
+ Assert.Null(updatedModel);
+ }
+
+ // BindProperty tests
+
+ [Fact]
+ public void BindPropertyCanUpdateComplexReadOnlyProperties()
+ {
+ // Arrange
+ // the Customer type contains a single read-only Address property
+ Customer model = new Customer();
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, model.GetType()),
+ ValueProvider = new SimpleValueProvider() { { "Address", null } }
+ };
+
+ Mock<IModelBinder> mockInnerBinder = new Mock<IModelBinder>();
+ mockInnerBinder
+ .Setup(b => b.BindModel(It.IsAny<ControllerContext>(), It.IsAny<ModelBindingContext>()))
+ .Returns(
+ delegate(ControllerContext cc, ModelBindingContext bc)
+ {
+ Address address = (Address)bc.Model;
+ address.Street = "1 Microsoft Way";
+ address.Zip = "98052";
+ return address;
+ });
+
+ PropertyDescriptor pd = TypeDescriptor.GetProperties(model)["Address"];
+ DefaultModelBinderHelper helper = new DefaultModelBinderHelper()
+ {
+ Binders = new ModelBinderDictionary()
+ {
+ { typeof(Address), mockInnerBinder.Object }
+ }
+ };
+
+ // Act
+ helper.PublicBindProperty(new ControllerContext(), bindingContext, pd);
+
+ // Assert
+ Assert.Equal("1 Microsoft Way", model.Address.Street);
+ Assert.Equal("98052", model.Address.Zip);
+ }
+
+ [Fact]
+ public void BindPropertyDoesNothingIfValueProviderContainsNoEntryForProperty()
+ {
+ // Arrange
+ MyModel2 model = new MyModel2() { IntReadWrite = 3 };
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, model.GetType()),
+ ValueProvider = new SimpleValueProvider()
+ };
+
+ PropertyDescriptor pd = TypeDescriptor.GetProperties(model)["IntReadWrite"];
+ DefaultModelBinderHelper helper = new DefaultModelBinderHelper();
+
+ // Act
+ helper.PublicBindProperty(new ControllerContext(), bindingContext, pd);
+
+ // Assert
+ Assert.Equal(3, model.IntReadWrite);
+ }
+
+ [Fact]
+ public void BindProperty()
+ {
+ // Arrange
+ ControllerContext controllerContext = new Mock<ControllerContext>().Object;
+ MyModel2 model = new MyModel2() { IntReadWrite = 3 };
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, model.GetType()),
+ ValueProvider = new SimpleValueProvider()
+ {
+ { "IntReadWrite", "42" }
+ }
+ };
+
+ PropertyDescriptor pd = TypeDescriptor.GetProperties(model)["IntReadWrite"];
+
+ Mock<DefaultModelBinderHelper> mockHelper = new Mock<DefaultModelBinderHelper>() { CallBase = true };
+ mockHelper.Setup(b => b.PublicOnPropertyValidating(controllerContext, bindingContext, pd, 42)).Returns(true).Verifiable();
+ mockHelper.Setup(b => b.PublicSetProperty(controllerContext, bindingContext, pd, 42)).Verifiable();
+ mockHelper.Setup(b => b.PublicOnPropertyValidated(controllerContext, bindingContext, pd, 42)).Verifiable();
+ DefaultModelBinderHelper helper = mockHelper.Object;
+
+ // Act
+ helper.PublicBindProperty(controllerContext, bindingContext, pd);
+
+ // Assert
+ mockHelper.Verify();
+ }
+
+ [Fact]
+ public void BindPropertyReturnsIfOnPropertyValidatingReturnsFalse()
+ {
+ // Arrange
+ ControllerContext controllerContext = new Mock<ControllerContext>().Object;
+ MyModel2 model = new MyModel2() { IntReadWrite = 3 };
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, model.GetType()),
+ ValueProvider = new SimpleValueProvider()
+ {
+ { "IntReadWrite", "42" }
+ }
+ };
+
+ PropertyDescriptor pd = TypeDescriptor.GetProperties(model)["IntReadWrite"];
+
+ Mock<DefaultModelBinderHelper> mockHelper = new Mock<DefaultModelBinderHelper>() { CallBase = true };
+ mockHelper.Setup(b => b.PublicOnPropertyValidating(controllerContext, bindingContext, pd, 42)).Returns(false);
+ DefaultModelBinderHelper helper = mockHelper.Object;
+
+ // Act
+ helper.PublicBindProperty(controllerContext, bindingContext, pd);
+
+ // Assert
+ Assert.Equal(3, model.IntReadWrite);
+ mockHelper.Verify();
+ mockHelper.Verify(b => b.PublicSetProperty(controllerContext, bindingContext, pd, 42), Times.Never());
+ mockHelper.Verify(b => b.PublicOnPropertyValidated(controllerContext, bindingContext, pd, 42), Times.Never());
+ }
+
+ [Fact]
+ public void BindPropertySetsPropertyToNullIfUserLeftTextEntryFieldBlankForOptionalValue()
+ {
+ // Arrange
+ MyModel2 model = new MyModel2() { NullableIntReadWrite = 8 };
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, model.GetType()),
+ ValueProvider = new SimpleValueProvider() { { "NullableIntReadWrite", null } }
+ };
+
+ Mock<IModelBinder> mockInnerBinder = new Mock<IModelBinder>();
+ mockInnerBinder.Setup(b => b.BindModel(new ControllerContext(), It.IsAny<ModelBindingContext>())).Returns((object)null);
+
+ PropertyDescriptor pd = TypeDescriptor.GetProperties(model)["NullableIntReadWrite"];
+ DefaultModelBinderHelper helper = new DefaultModelBinderHelper()
+ {
+ Binders = new ModelBinderDictionary()
+ {
+ { typeof(int?), mockInnerBinder.Object }
+ }
+ };
+
+ // Act
+ helper.PublicBindProperty(new ControllerContext(), bindingContext, pd);
+
+ // Assert
+ Assert.Empty(bindingContext.ModelState);
+ Assert.Null(model.NullableIntReadWrite);
+ }
+
+ [Fact]
+ public void BindPropertyUpdatesPropertyOnFailureIfInnerBinderReturnsNonNullObject()
+ {
+ // Arrange
+ MyModel2 model = new MyModel2() { IntReadWriteNonNegative = 8 };
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, model.GetType()),
+ ValueProvider = new SimpleValueProvider() { { "IntReadWriteNonNegative", null } }
+ };
+
+ Mock<IModelBinder> mockInnerBinder = new Mock<IModelBinder>();
+ mockInnerBinder
+ .Setup(b => b.BindModel(It.IsAny<ControllerContext>(), It.IsAny<ModelBindingContext>()))
+ .Returns(
+ delegate(ControllerContext cc, ModelBindingContext bc)
+ {
+ bc.ModelState.AddModelError("IntReadWriteNonNegative", "Some error text.");
+ return 4;
+ });
+
+ PropertyDescriptor pd = TypeDescriptor.GetProperties(model)["IntReadWriteNonNegative"];
+ DefaultModelBinderHelper helper = new DefaultModelBinderHelper()
+ {
+ Binders = new ModelBinderDictionary()
+ {
+ { typeof(int), mockInnerBinder.Object }
+ }
+ };
+
+ // Act
+ helper.PublicBindProperty(new ControllerContext(), bindingContext, pd);
+
+ // Assert
+ Assert.Equal(false, bindingContext.ModelState.IsValidField("IntReadWriteNonNegative"));
+ var error = Assert.Single(bindingContext.ModelState["IntReadWriteNonNegative"].Errors);
+ Assert.Equal("Some error text.", error.ErrorMessage);
+ Assert.Equal(4, model.IntReadWriteNonNegative);
+ }
+
+ [Fact]
+ public void BindPropertyUpdatesPropertyOnSuccess()
+ {
+ // Arrange
+ // Effectively, this is just testing updating a single property named "IntReadWrite"
+ ControllerContext controllerContext = new Mock<ControllerContext>().Object;
+
+ MyModel2 model = new MyModel2() { IntReadWrite = 3 };
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, model.GetType()),
+ ModelName = "foo",
+ ModelState = new ModelStateDictionary() { { "blah", new ModelState() } },
+ ValueProvider = new SimpleValueProvider() { { "foo.IntReadWrite", null } }
+ };
+
+ Mock<IModelBinder> mockInnerBinder = new Mock<IModelBinder>();
+ mockInnerBinder
+ .Setup(b => b.BindModel(It.IsAny<ControllerContext>(), It.IsAny<ModelBindingContext>()))
+ .Returns(
+ delegate(ControllerContext cc, ModelBindingContext bc)
+ {
+ Assert.Equal(controllerContext, cc);
+ Assert.Equal(3, bc.Model);
+ Assert.Equal(typeof(int), bc.ModelType);
+ Assert.Equal("foo.IntReadWrite", bc.ModelName);
+ Assert.Equal(new ModelBindingContext().PropertyFilter, bc.PropertyFilter);
+ Assert.Equal(bindingContext.ModelState, bc.ModelState);
+ Assert.Equal(bindingContext.ValueProvider, bc.ValueProvider);
+ return 4;
+ });
+
+ PropertyDescriptor pd = TypeDescriptor.GetProperties(model)["IntReadWrite"];
+ DefaultModelBinderHelper helper = new DefaultModelBinderHelper()
+ {
+ Binders = new ModelBinderDictionary()
+ {
+ { typeof(int), mockInnerBinder.Object }
+ }
+ };
+
+ // Act
+ helper.PublicBindProperty(controllerContext, bindingContext, pd);
+
+ // Assert
+ Assert.Equal(4, model.IntReadWrite);
+ }
+
+ // BindSimpleModel tests
+
+ [Fact]
+ public void BindSimpleModelCanReturnArrayTypes()
+ {
+ // Arrange
+ ValueProviderResult result = new ValueProviderResult(42, null, null);
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(int[])),
+ ModelName = "foo",
+ };
+
+ DefaultModelBinder binder = new DefaultModelBinder();
+
+ // Act
+ object returnedValue = binder.BindSimpleModel(null, bindingContext, result);
+
+ // Assert
+ var returnedValueAsIntArray = Assert.IsType<int[]>(returnedValue);
+ Assert.Single(returnedValueAsIntArray);
+ Assert.Equal(42, returnedValueAsIntArray[0]);
+ }
+
+ [Fact]
+ public void BindSimpleModelCanReturnCollectionTypes()
+ {
+ // Arrange
+ ValueProviderResult result = new ValueProviderResult(new string[] { "42", "82" }, null, null);
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(IEnumerable<int>)),
+ ModelName = "foo",
+ };
+
+ DefaultModelBinder binder = new DefaultModelBinder();
+
+ // Act
+ object returnedValue = binder.BindSimpleModel(null, bindingContext, result);
+
+ // Assert
+ var returnedValueAsList = Assert.IsAssignableFrom<IEnumerable<int>>(returnedValue).ToList();
+ Assert.Equal(2, returnedValueAsList.Count);
+ Assert.Equal(42, returnedValueAsList[0]);
+ Assert.Equal(82, returnedValueAsList[1]);
+ }
+
+ [Fact]
+ public void BindSimpleModelCanReturnElementalTypes()
+ {
+ // Arrange
+ ValueProviderResult result = new ValueProviderResult("42", null, null);
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(int)),
+ ModelName = "foo",
+ };
+
+ DefaultModelBinder binder = new DefaultModelBinder();
+
+ // Act
+ object returnedValue = binder.BindSimpleModel(null, bindingContext, result);
+
+ // Assert
+ Assert.Equal(42, returnedValue);
+ }
+
+ [Fact]
+ public void BindSimpleModelCanReturnStrings()
+ {
+ // Arrange
+ ValueProviderResult result = new ValueProviderResult(new object[] { "42" }, null, null);
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(string)),
+ ModelName = "foo",
+ };
+
+ DefaultModelBinder binder = new DefaultModelBinder();
+
+ // Act
+ object returnedValue = binder.BindSimpleModel(null, bindingContext, result);
+
+ // Assert
+ Assert.Equal("42", returnedValue);
+ }
+
+ [Fact]
+ public void BindSimpleModelChecksValueProviderResultRawValueType()
+ {
+ // Arrange
+ ValueProviderResult result = new ValueProviderResult(new MemoryStream(), null, null);
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(Stream)),
+ ModelName = "foo",
+ };
+
+ DefaultModelBinder binder = new DefaultModelBinder();
+
+ // Act
+ object returnedValue = binder.BindSimpleModel(null, bindingContext, result);
+
+ // Assert
+ Assert.Equal(result, bindingContext.ModelState["foo"].Value);
+ Assert.Same(result.RawValue, returnedValue);
+ }
+
+ [Fact]
+ public void BindSimpleModelPropagatesErrorsOnFailure()
+ {
+ // Arrange
+ ValueProviderResult result = new ValueProviderResult("invalid", null, null);
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(int)),
+ ModelName = "foo",
+ };
+
+ DefaultModelBinder binder = new DefaultModelBinder();
+
+ // Act
+ object returnedValue = binder.BindSimpleModel(null, bindingContext, result);
+
+ // Assert
+ Assert.False(bindingContext.ModelState.IsValidField("foo"));
+ Assert.IsType<InvalidOperationException>(bindingContext.ModelState["foo"].Errors[0].Exception);
+ Assert.Equal("The parameter conversion from type 'System.String' to type 'System.Int32' failed. See the inner exception for more information.", bindingContext.ModelState["foo"].Errors[0].Exception.Message);
+ Assert.Null(returnedValue);
+ }
+
+ [Fact]
+ public void CreateComplexElementalModelBindingContext_ReadsBindAttributeFromBuddyClass()
+ {
+ // Arrange
+ ModelBindingContext originalBindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(CreateComplexElementalModelBindingContext_ReadsBindAttributeFromBuddyClass_Model)),
+ ModelName = "someName",
+ ValueProvider = new SimpleValueProvider()
+ };
+
+ DefaultModelBinder binder = new DefaultModelBinder();
+
+ // Act
+ ModelBindingContext newBindingContext = binder.CreateComplexElementalModelBindingContext(new ControllerContext(), originalBindingContext, null);
+
+ // Assert
+ Assert.True(newBindingContext.PropertyFilter("foo"));
+ Assert.False(newBindingContext.PropertyFilter("bar"));
+ }
+
+ [MetadataType(typeof(CreateComplexElementalModelBindingContext_ReadsBindAttributeFromBuddyClass_Model_BuddyClass))]
+ private class CreateComplexElementalModelBindingContext_ReadsBindAttributeFromBuddyClass_Model
+ {
+ [Bind(Include = "foo")]
+ private class CreateComplexElementalModelBindingContext_ReadsBindAttributeFromBuddyClass_Model_BuddyClass
+ {
+ }
+ }
+
+ [Fact]
+ public void CreateInstanceCreatesModelInstance()
+ {
+ // Arrange
+ DefaultModelBinderHelper helper = new DefaultModelBinderHelper();
+
+ // Act
+ object modelObj = helper.PublicCreateModel(null, null, typeof(Guid));
+
+ // Assert
+ Assert.Equal(Guid.Empty, modelObj);
+ }
+
+ [Fact]
+ public void CreateInstanceCreatesModelInstanceForGenericICollection()
+ {
+ // Arrange
+ DefaultModelBinderHelper helper = new DefaultModelBinderHelper();
+
+ // Act
+ object modelObj = helper.PublicCreateModel(null, null, typeof(ICollection<Guid>));
+
+ // Assert
+ Assert.IsAssignableFrom<ICollection<Guid>>(modelObj);
+ }
+
+ [Fact]
+ public void CreateInstanceCreatesModelInstanceForGenericIDictionary()
+ {
+ // Arrange
+ DefaultModelBinderHelper helper = new DefaultModelBinderHelper();
+
+ // Act
+ object modelObj = helper.PublicCreateModel(null, null, typeof(IDictionary<string, Guid>));
+
+ // Assert
+ Assert.IsAssignableFrom<IDictionary<string, Guid>>(modelObj);
+ }
+
+ [Fact]
+ public void CreateInstanceCreatesModelInstanceForGenericIEnumerable()
+ {
+ // Arrange
+ DefaultModelBinderHelper helper = new DefaultModelBinderHelper();
+
+ // Act
+ object modelObj = helper.PublicCreateModel(null, null, typeof(IEnumerable<Guid>));
+
+ // Assert
+ Assert.IsAssignableFrom<ICollection<Guid>>(modelObj);
+ }
+
+ [Fact]
+ public void CreateInstanceCreatesModelInstanceForGenericIList()
+ {
+ // Arrange
+ DefaultModelBinderHelper helper = new DefaultModelBinderHelper();
+
+ // Act
+ object modelObj = helper.PublicCreateModel(null, null, typeof(IList<Guid>));
+
+ // Assert
+ Assert.IsAssignableFrom<IList<Guid>>(modelObj);
+ }
+
+ [Fact]
+ public void CreateSubIndexNameReturnsPrefixPlusIndex()
+ {
+ // Arrange
+ DefaultModelBinderHelper helper = new DefaultModelBinderHelper();
+
+ // Act
+ string newName = helper.PublicCreateSubIndexName("somePrefix", 2);
+
+ // Assert
+ Assert.Equal("somePrefix[2]", newName);
+ }
+
+ [Fact]
+ public void CreateSubPropertyNameReturnsPrefixPlusPropertyName()
+ {
+ // Arrange
+ DefaultModelBinderHelper helper = new DefaultModelBinderHelper();
+
+ // Act
+ string newName = helper.PublicCreateSubPropertyName("somePrefix", "someProperty");
+
+ // Assert
+ Assert.Equal("somePrefix.someProperty", newName);
+ }
+
+ [Fact]
+ public void CreateSubPropertyNameReturnsPropertyNameIfPrefixIsEmpty()
+ {
+ // Arrange
+ DefaultModelBinderHelper helper = new DefaultModelBinderHelper();
+
+ // Act
+ string newName = helper.PublicCreateSubPropertyName(String.Empty, "someProperty");
+
+ // Assert
+ Assert.Equal("someProperty", newName);
+ }
+
+ [Fact]
+ public void CreateSubPropertyNameReturnsPropertyNameIfPrefixIsNull()
+ {
+ // Arrange
+ DefaultModelBinderHelper helper = new DefaultModelBinderHelper();
+
+ // Act
+ string newName = helper.PublicCreateSubPropertyName(null, "someProperty");
+
+ // Assert
+ Assert.Equal("someProperty", newName);
+ }
+
+ [Fact]
+ public void GetFilteredModelPropertiesFiltersNonUpdateableProperties()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(PropertyTestingModel)),
+ PropertyFilter = new BindAttribute() { Exclude = "Blacklisted" }.IsPropertyAllowed
+ };
+
+ DefaultModelBinderHelper helper = new DefaultModelBinderHelper();
+
+ // Act
+ PropertyDescriptorCollection properties = new PropertyDescriptorCollection(helper.PublicGetFilteredModelProperties(null, bindingContext).ToArray());
+
+ // Assert
+ Assert.NotNull(properties["StringReadWrite"]);
+ Assert.Null(properties["StringReadOnly"]);
+ Assert.NotNull(properties["IntReadWrite"]);
+ Assert.Null(properties["IntReadOnly"]);
+ Assert.NotNull(properties["ArrayReadWrite"]);
+ Assert.Null(properties["ArrayReadOnly"]);
+ Assert.NotNull(properties["AddressReadWrite"]);
+ Assert.NotNull(properties["AddressReadOnly"]);
+ Assert.NotNull(properties["Whitelisted"]);
+ Assert.Null(properties["Blacklisted"]);
+ Assert.Equal(6, properties.Count);
+ }
+
+ [Fact]
+ public void GetModelPropertiesReturnsUnfilteredPropertyList()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(PropertyTestingModel)),
+ PropertyFilter = new BindAttribute() { Exclude = "Blacklisted" }.IsPropertyAllowed
+ };
+
+ DefaultModelBinderHelper helper = new DefaultModelBinderHelper();
+
+ // Act
+ PropertyDescriptorCollection properties = helper.PublicGetModelProperties(null, bindingContext);
+
+ // Assert
+ Assert.NotNull(properties["StringReadWrite"]);
+ Assert.NotNull(properties["StringReadOnly"]);
+ Assert.NotNull(properties["IntReadWrite"]);
+ Assert.NotNull(properties["IntReadOnly"]);
+ Assert.NotNull(properties["ArrayReadWrite"]);
+ Assert.NotNull(properties["ArrayReadOnly"]);
+ Assert.NotNull(properties["AddressReadWrite"]);
+ Assert.NotNull(properties["AddressReadOnly"]);
+ Assert.NotNull(properties["Whitelisted"]);
+ Assert.NotNull(properties["Blacklisted"]);
+ Assert.Equal(10, properties.Count);
+ }
+
+ [Fact]
+ public void IsModelValidWithNullBindingContextThrows()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => DefaultModelBinderHelper.PublicIsModelValid(null),
+ "bindingContext");
+ }
+
+ [Fact]
+ public void IsModelValidReturnsModelStateIsValidWhenModelNameIsEmpty()
+ {
+ // Arrange
+ ModelBindingContext contextWithNoErrors = new ModelBindingContext { ModelName = "" };
+ ModelBindingContext contextWithErrors = new ModelBindingContext { ModelName = "" };
+ contextWithErrors.ModelState.AddModelError("foo", "bar");
+
+ // Act & Assert
+ Assert.True(DefaultModelBinderHelper.PublicIsModelValid(contextWithNoErrors));
+ Assert.False(DefaultModelBinderHelper.PublicIsModelValid(contextWithErrors));
+ }
+
+ [Fact]
+ public void IsModelValidReturnsValidityOfSubModelStateWhenModelNameIsNotEmpty()
+ {
+ // Arrange
+ ModelBindingContext contextWithNoErrors = new ModelBindingContext { ModelName = "foo" };
+ ModelBindingContext contextWithErrors = new ModelBindingContext { ModelName = "foo" };
+ contextWithErrors.ModelState.AddModelError("foo.bar", "baz");
+ ModelBindingContext contextWithUnrelatedErrors = new ModelBindingContext { ModelName = "foo" };
+ contextWithUnrelatedErrors.ModelState.AddModelError("biff", "baz");
+
+ // Act & Assert
+ Assert.True(DefaultModelBinderHelper.PublicIsModelValid(contextWithNoErrors));
+ Assert.False(DefaultModelBinderHelper.PublicIsModelValid(contextWithErrors));
+ Assert.True(DefaultModelBinderHelper.PublicIsModelValid(contextWithUnrelatedErrors));
+ }
+
+ [Fact]
+ public void OnModelUpdatingReturnsTrue()
+ {
+ // By default, this method does nothing, so we just want to make sure it returns true
+
+ // Arrange
+ DefaultModelBinderHelper helper = new DefaultModelBinderHelper();
+
+ // Act
+ bool returned = helper.PublicOnModelUpdating(null, null);
+
+ // Arrange
+ Assert.True(returned);
+ }
+
+ // OnModelUpdated tests
+
+ [Fact]
+ public void OnModelUpdatedCalledWhenOnModelUpdatingReturnsTrue()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => new ModelWithoutBindAttribute(), typeof(ModelWithoutBindAttribute)),
+ ModelName = "",
+ ValueProvider = new SimpleValueProvider()
+ };
+ Mock<DefaultModelBinder> binder = new Mock<DefaultModelBinder> { CallBase = true };
+ binder.Protected().Setup<bool>("OnModelUpdating",
+ ItExpr.IsAny<ControllerContext>(), ItExpr.IsAny<ModelBindingContext>())
+ .Returns(true);
+ binder.Protected().Setup("OnModelUpdated",
+ ItExpr.IsAny<ControllerContext>(), ItExpr.IsAny<ModelBindingContext>())
+ .Verifiable();
+
+ // Act
+ binder.Object.BindModel(new ControllerContext(), bindingContext);
+
+ // Assert
+ binder.Verify();
+ }
+
+ [Fact]
+ public void OnModelUpdatedNotCalledWhenOnModelUpdatingReturnsFalse()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => new ModelWithoutBindAttribute(), typeof(ModelWithoutBindAttribute)),
+ ModelName = "",
+ ValueProvider = new SimpleValueProvider()
+ };
+ Mock<DefaultModelBinder> binder = new Mock<DefaultModelBinder> { CallBase = true };
+ binder.Protected().Setup<bool>("OnModelUpdating",
+ ItExpr.IsAny<ControllerContext>(), ItExpr.IsAny<ModelBindingContext>())
+ .Returns(false);
+
+ // Act
+ binder.Object.BindModel(new ControllerContext(), bindingContext);
+
+ // Assert
+ binder.Verify();
+ binder.Protected().Verify("OnModelUpdated", Times.Never(),
+ ItExpr.IsAny<ControllerContext>(), ItExpr.IsAny<ModelBindingContext>());
+ }
+
+ [Fact]
+ public void OnModelUpdatedDoesntAddNewMessagesWhenMessagesAlreadyExist()
+ {
+ // Arrange
+ var binder = new TestableDefaultModelBinder<SetPropertyModel>();
+ binder.Context.ModelState.AddModelError(BASE_MODEL_NAME + ".NonNullableStringWithAttribute", "Some pre-existing error");
+
+ // Act
+ binder.OnModelUpdated();
+
+ // Assert
+ var modelState = binder.Context.ModelState[BASE_MODEL_NAME + ".NonNullableStringWithAttribute"];
+ var error = Assert.Single(modelState.Errors);
+ Assert.Equal("Some pre-existing error", error.ErrorMessage);
+ }
+
+ [Fact]
+ public void OnPropertyValidatingNotCalledOnPropertiesWithErrors()
+ {
+ // Arrange
+ ModelWithoutBindAttribute model = new ModelWithoutBindAttribute();
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, model.GetType()),
+ ModelName = "",
+ ValueProvider = new SimpleValueProvider()
+ {
+ { "foo", "foo" }
+ },
+ };
+ bindingContext.ModelState.AddModelError("foo", "Pre-existing error");
+ Mock<DefaultModelBinder> binder = new Mock<DefaultModelBinder> { CallBase = true };
+
+ // Act
+ binder.Object.BindModel(new ControllerContext(), bindingContext);
+
+ // Assert
+ binder.Verify();
+ binder.Protected().Verify("OnPropertyValidating", Times.Never(),
+ ItExpr.IsAny<ControllerContext>(), ItExpr.IsAny<ModelBindingContext>(),
+ ItExpr.IsAny<PropertyDescriptor>(), ItExpr.IsAny<object>());
+ }
+
+ public class ExtraValueModel
+ {
+ public int RequiredValue { get; set; }
+ }
+
+ [Fact]
+ public void ExtraValueRequiredMessageNotAddedForAlreadyInvalidProperty()
+ {
+ // Arrange
+ DefaultModelBinder binder = new DefaultModelBinder();
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(MyModel)),
+ ModelName = "theModel",
+ ValueProvider = new SimpleValueProvider()
+ };
+ bindingContext.ModelState.AddModelError("theModel.ReadWriteProperty", "Existing Error Message");
+
+ // Act
+ binder.BindModel(new ControllerContext(), bindingContext);
+
+ // Assert
+ ModelState modelState = bindingContext.ModelState["theModel.ReadWriteProperty"];
+ var error = Assert.Single(modelState.Errors);
+ Assert.Equal("Existing Error Message", error.ErrorMessage);
+ }
+
+ [Fact]
+ public void OnPropertyValidatingReturnsTrueOnSuccess()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(MyModel)),
+ ModelName = "theModel"
+ };
+
+ PropertyDescriptor property = TypeDescriptor.GetProperties(typeof(MyModel))["ReadWriteProperty"];
+ DefaultModelBinderHelper helper = new DefaultModelBinderHelper();
+ bindingContext.PropertyMetadata["ReadWriteProperty"].Model = 42;
+
+ // Act
+ bool returned = helper.PublicOnPropertyValidating(new ControllerContext(), bindingContext, property, 42);
+
+ // Assert
+ Assert.True(returned);
+ Assert.Empty(bindingContext.ModelState);
+ }
+
+ [Fact]
+ public void UpdateCollectionCreatesDefaultEntriesForInvalidElements()
+ {
+ // Arrange
+ List<int> model = new List<int>() { 4, 5, 6, 7, 8 };
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, model.GetType()),
+ ModelName = "foo",
+ ValueProvider = new SimpleValueProvider()
+ {
+ { "foo[0]", null },
+ { "foo[1]", null },
+ { "foo[2]", null }
+ }
+ };
+
+ Mock<IModelBinder> mockInnerBinder = new Mock<IModelBinder>();
+ mockInnerBinder
+ .Setup(b => b.BindModel(It.IsAny<ControllerContext>(), It.IsAny<ModelBindingContext>()))
+ .Returns(
+ delegate(ControllerContext cc, ModelBindingContext bc)
+ {
+ int fooIdx = Int32.Parse(bc.ModelName.Substring(4, 1), CultureInfo.InvariantCulture);
+ return (fooIdx == 1) ? (object)null : fooIdx;
+ });
+
+ DefaultModelBinder binder = new DefaultModelBinder()
+ {
+ Binders = new ModelBinderDictionary()
+ {
+ { typeof(int), mockInnerBinder.Object }
+ }
+ };
+
+ // Act
+ object updatedModel = binder.UpdateCollection(null, bindingContext, typeof(int));
+
+ // Assert
+ Assert.Equal(3, model.Count);
+ Assert.Equal(false, bindingContext.ModelState.IsValidField("foo[1]"));
+ Assert.Equal("A value is required.", bindingContext.ModelState["foo[1]"].Errors[0].ErrorMessage);
+ Assert.Equal(0, model[0]);
+ Assert.Equal(0, model[1]);
+ Assert.Equal(2, model[2]);
+ }
+
+ [Fact]
+ public void UpdateCollectionReturnsModifiedCollectionOnSuccess_ExplicitIndex()
+ {
+ // Arrange
+ ControllerContext controllerContext = new Mock<ControllerContext>().Object;
+
+ List<int> model = new List<int>() { 4, 5, 6, 7, 8 };
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, model.GetType()),
+ ModelName = "foo",
+ PropertyFilter = _ => false,
+ ValueProvider = new SimpleValueProvider()
+ {
+ { "foo.index", new string[] { "alpha", "bravo", "charlie" } }, // 'bravo' will be skipped
+ { "foo[alpha]", "10" },
+ { "foo[charlie]", "30" }
+ }
+ };
+
+ DefaultModelBinder binder = new DefaultModelBinder();
+
+ // Act
+ object updatedModel = binder.UpdateCollection(controllerContext, bindingContext, typeof(int));
+
+ // Assert
+ Assert.Same(model, updatedModel);
+ Assert.Equal(2, model.Count);
+ Assert.Equal(10, model[0]);
+ Assert.Equal(30, model[1]);
+ }
+
+ [Fact]
+ public void UpdateCollectionReturnsModifiedCollectionOnSuccess_ZeroBased()
+ {
+ // Arrange
+ ControllerContext controllerContext = new Mock<ControllerContext>().Object;
+
+ List<int> model = new List<int>() { 4, 5, 6, 7, 8 };
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, model.GetType()),
+ ModelName = "foo",
+ PropertyFilter = _ => false,
+ ValueProvider = new SimpleValueProvider()
+ {
+ { "foo[0]", null },
+ { "foo[1]", null },
+ { "foo[2]", null }
+ }
+ };
+
+ Mock<IModelBinder> mockInnerBinder = new Mock<IModelBinder>();
+ mockInnerBinder
+ .Setup(b => b.BindModel(It.IsAny<ControllerContext>(), It.IsAny<ModelBindingContext>()))
+ .Returns(
+ delegate(ControllerContext cc, ModelBindingContext bc)
+ {
+ Assert.Equal(controllerContext, cc);
+ Assert.Equal(typeof(int), bc.ModelType);
+ Assert.Equal(bindingContext.ModelState, bc.ModelState);
+ Assert.Equal(bindingContext.PropertyFilter, bc.PropertyFilter);
+ Assert.Equal(bindingContext.ValueProvider, bc.ValueProvider);
+ return Int32.Parse(bc.ModelName.Substring(4, 1), CultureInfo.InvariantCulture);
+ });
+
+ DefaultModelBinder binder = new DefaultModelBinder()
+ {
+ Binders = new ModelBinderDictionary()
+ {
+ { typeof(int), mockInnerBinder.Object }
+ }
+ };
+
+ // Act
+ object updatedModel = binder.UpdateCollection(controllerContext, bindingContext, typeof(int));
+
+ // Assert
+ Assert.Same(model, updatedModel);
+ Assert.Equal(3, model.Count);
+ Assert.Equal(0, model[0]);
+ Assert.Equal(1, model[1]);
+ Assert.Equal(2, model[2]);
+ }
+
+ [Fact]
+ public void UpdateCollectionReturnsNullIfZeroIndexNotFound()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ValueProvider = new SimpleValueProvider()
+ };
+ DefaultModelBinder binder = new DefaultModelBinder();
+
+ // Act
+ object updatedModel = binder.UpdateCollection(null, bindingContext, typeof(object));
+
+ // Assert
+ Assert.Null(updatedModel);
+ }
+
+ [Fact]
+ public void UpdateDictionaryCreatesDefaultEntriesForInvalidValues()
+ {
+ // Arrange
+ Dictionary<string, int> model = new Dictionary<string, int>
+ {
+ { "one", 1 },
+ { "two", 2 }
+ };
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, model.GetType()),
+ ModelName = "foo",
+ ValueProvider = new SimpleValueProvider()
+ {
+ { "foo[0].key", null }, { "foo[0].value", null },
+ { "foo[1].key", null }, { "foo[1].value", null },
+ { "foo[2].key", null }, { "foo[2].value", null }
+ }
+ };
+
+ Mock<IModelBinder> mockStringBinder = new Mock<IModelBinder>();
+ mockStringBinder
+ .Setup(b => b.BindModel(It.IsAny<ControllerContext>(), It.IsAny<ModelBindingContext>()))
+ .Returns(
+ delegate(ControllerContext cc, ModelBindingContext bc) { return (Int32.Parse(bc.ModelName.Substring(4, 1), CultureInfo.InvariantCulture) + 10) + "Value"; });
+
+ Mock<IModelBinder> mockIntBinder = new Mock<IModelBinder>();
+ mockIntBinder
+ .Setup(b => b.BindModel(It.IsAny<ControllerContext>(), It.IsAny<ModelBindingContext>()))
+ .Returns(
+ delegate(ControllerContext cc, ModelBindingContext bc)
+ {
+ int fooIdx = Int32.Parse(bc.ModelName.Substring(4, 1), CultureInfo.InvariantCulture);
+ return (fooIdx == 1) ? (object)null : fooIdx;
+ });
+
+ DefaultModelBinder binder = new DefaultModelBinder()
+ {
+ Binders = new ModelBinderDictionary()
+ {
+ { typeof(string), mockStringBinder.Object },
+ { typeof(int), mockIntBinder.Object }
+ }
+ };
+
+ // Act
+ object updatedModel = binder.UpdateDictionary(null, bindingContext, typeof(string), typeof(int));
+
+ // Assert
+ Assert.Equal(3, model.Count);
+ Assert.False(bindingContext.ModelState.IsValidField("foo[1].value"));
+ Assert.Equal("A value is required.", bindingContext.ModelState["foo[1].value"].Errors[0].ErrorMessage);
+ Assert.Equal(0, model["10Value"]);
+ Assert.Equal(0, model["11Value"]);
+ Assert.Equal(2, model["12Value"]);
+ }
+
+ [Fact]
+ public void UpdateDictionaryReturnsModifiedDictionaryOnSuccess_ExplicitIndex()
+ {
+ // Arrange
+ ControllerContext controllerContext = new Mock<ControllerContext>().Object;
+
+ Dictionary<int, string> model = new Dictionary<int, string>
+ {
+ { 1, "one" },
+ { 2, "two" }
+ };
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, model.GetType()),
+ ModelName = "foo",
+ PropertyFilter = _ => false,
+ ValueProvider = new SimpleValueProvider()
+ {
+ { "foo.index", new string[] { "alpha", "bravo", "charlie" } }, // 'bravo' will be skipped
+ { "foo[alpha].key", "10" }, { "foo[alpha].value", "ten" },
+ { "foo[charlie].key", "30" }, { "foo[charlie].value", "thirty" }
+ }
+ };
+
+ DefaultModelBinder binder = new DefaultModelBinder();
+
+ // Act
+ object updatedModel = binder.UpdateDictionary(controllerContext, bindingContext, typeof(int), typeof(string));
+
+ // Assert
+ Assert.Same(model, updatedModel);
+ Assert.Equal(2, model.Count);
+ Assert.Equal("ten", model[10]);
+ Assert.Equal("thirty", model[30]);
+ }
+
+ [Fact]
+ public void UpdateDictionaryReturnsModifiedDictionaryOnSuccess_ZeroBased()
+ {
+ // Arrange
+ ControllerContext controllerContext = new Mock<ControllerContext>().Object;
+
+ Dictionary<int, string> model = new Dictionary<int, string>
+ {
+ { 1, "one" },
+ { 2, "two" }
+ };
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, model.GetType()),
+ ModelName = "foo",
+ PropertyFilter = _ => false,
+ ValueProvider = new SimpleValueProvider()
+ {
+ { "foo[0].key", null }, { "foo[0].value", null },
+ { "foo[1].key", null }, { "foo[1].value", null },
+ { "foo[2].key", null }, { "foo[2].value", null }
+ }
+ };
+
+ Mock<IModelBinder> mockIntBinder = new Mock<IModelBinder>();
+ mockIntBinder
+ .Setup(b => b.BindModel(It.IsAny<ControllerContext>(), It.IsAny<ModelBindingContext>()))
+ .Returns(
+ delegate(ControllerContext cc, ModelBindingContext bc)
+ {
+ Assert.Equal(controllerContext, cc);
+ Assert.Equal(typeof(int), bc.ModelType);
+ Assert.Equal(bindingContext.ModelState, bc.ModelState);
+ Assert.Equal(new ModelBindingContext().PropertyFilter, bc.PropertyFilter);
+ Assert.Equal(bindingContext.ValueProvider, bc.ValueProvider);
+ return Int32.Parse(bc.ModelName.Substring(4, 1), CultureInfo.InvariantCulture) + 10;
+ });
+
+ Mock<IModelBinder> mockStringBinder = new Mock<IModelBinder>();
+ mockStringBinder
+ .Setup(b => b.BindModel(It.IsAny<ControllerContext>(), It.IsAny<ModelBindingContext>()))
+ .Returns(
+ delegate(ControllerContext cc, ModelBindingContext bc)
+ {
+ Assert.Equal(controllerContext, cc);
+ Assert.Equal(typeof(string), bc.ModelType);
+ Assert.Equal(bindingContext.ModelState, bc.ModelState);
+ Assert.Equal(bindingContext.PropertyFilter, bc.PropertyFilter);
+ Assert.Equal(bindingContext.ValueProvider, bc.ValueProvider);
+ return (Int32.Parse(bc.ModelName.Substring(4, 1), CultureInfo.InvariantCulture) + 10) + "Value";
+ });
+
+ DefaultModelBinder binder = new DefaultModelBinder()
+ {
+ Binders = new ModelBinderDictionary()
+ {
+ { typeof(int), mockIntBinder.Object },
+ { typeof(string), mockStringBinder.Object }
+ }
+ };
+
+ // Act
+ object updatedModel = binder.UpdateDictionary(controllerContext, bindingContext, typeof(int), typeof(string));
+
+ // Assert
+ Assert.Same(model, updatedModel);
+ Assert.Equal(3, model.Count);
+ Assert.Equal("10Value", model[10]);
+ Assert.Equal("11Value", model[11]);
+ Assert.Equal("12Value", model[12]);
+ }
+
+ [Fact]
+ public void UpdateDictionaryReturnsNullIfNoValidElementsFound()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ValueProvider = new SimpleValueProvider()
+ };
+ DefaultModelBinder binder = new DefaultModelBinder();
+
+ // Act
+ object updatedModel = binder.UpdateDictionary(null, bindingContext, typeof(object), typeof(object));
+
+ // Assert
+ Assert.Null(updatedModel);
+ }
+
+ [Fact]
+ public void UpdateDictionarySkipsInvalidKeys()
+ {
+ // Arrange
+ Dictionary<int, string> model = new Dictionary<int, string>
+ {
+ { 1, "one" },
+ { 2, "two" }
+ };
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, model.GetType()),
+ ModelName = "foo",
+ ValueProvider = new SimpleValueProvider()
+ {
+ { "foo[0].key", null }, { "foo[0].value", null },
+ { "foo[1].key", null }, { "foo[1].value", null },
+ { "foo[2].key", null }, { "foo[2].value", null }
+ }
+ };
+
+ Mock<IModelBinder> mockIntBinder = new Mock<IModelBinder>();
+ mockIntBinder
+ .Setup(b => b.BindModel(It.IsAny<ControllerContext>(), It.IsAny<ModelBindingContext>()))
+ .Returns(
+ delegate(ControllerContext cc, ModelBindingContext bc)
+ {
+ int fooIdx = Int32.Parse(bc.ModelName.Substring(4, 1), CultureInfo.InvariantCulture);
+ return (fooIdx == 1) ? (object)null : fooIdx;
+ });
+
+ Mock<IModelBinder> mockStringBinder = new Mock<IModelBinder>();
+ mockStringBinder
+ .Setup(b => b.BindModel(It.IsAny<ControllerContext>(), It.IsAny<ModelBindingContext>()))
+ .Returns(
+ delegate(ControllerContext cc, ModelBindingContext bc) { return (Int32.Parse(bc.ModelName.Substring(4, 1), CultureInfo.InvariantCulture) + 10) + "Value"; });
+
+ DefaultModelBinder binder = new DefaultModelBinder()
+ {
+ Binders = new ModelBinderDictionary()
+ {
+ { typeof(int), mockIntBinder.Object },
+ { typeof(string), mockStringBinder.Object }
+ }
+ };
+
+ // Act
+ object updatedModel = binder.UpdateDictionary(null, bindingContext, typeof(int), typeof(string));
+
+ // Assert
+ Assert.Equal(2, model.Count);
+ Assert.False(bindingContext.ModelState.IsValidField("foo[1].key"));
+ Assert.Equal("A value is required.", bindingContext.ModelState["foo[1].key"].Errors[0].ErrorMessage);
+ Assert.Equal("10Value", model[0]);
+ Assert.Equal("12Value", model[2]);
+ }
+
+ [ModelBinder(typeof(DefaultModelBinder))]
+ private class MyModel
+ {
+ public int ReadOnlyProperty
+ {
+ get { return 4; }
+ }
+
+ public int ReadWriteProperty { get; set; }
+ public int ReadWriteProperty2 { get; set; }
+ }
+
+ private class MyClassWithoutConverter
+ {
+ }
+
+ [Bind(Exclude = "Alpha,Echo")]
+ private class MyOtherModel
+ {
+ public string Alpha { get; set; }
+ public string Bravo { get; set; }
+ public string Charlie { get; set; }
+ public string Delta { get; set; }
+ public string Echo { get; set; }
+ public string Foxtrot { get; set; }
+ }
+
+ public class Customer
+ {
+ private Address _address = new Address();
+
+ public Address Address
+ {
+ get { return _address; }
+ }
+ }
+
+ public class Address
+ {
+ public string Street { get; set; }
+ public string Zip { get; set; }
+ }
+
+ public class IntegerContainer
+ {
+ public int Integer { get; set; }
+ public int? NullableInteger { get; set; }
+ }
+
+ [TypeConverter(typeof(CultureAwareConverter))]
+ public class StringContainer
+ {
+ public StringContainer(string value)
+ {
+ Value = value;
+ }
+
+ public string Value { get; private set; }
+ }
+
+ private class CultureAwareConverter : TypeConverter
+ {
+ public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
+ {
+ return (sourceType == typeof(string));
+ }
+
+ public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
+ {
+ return (destinationType == typeof(string));
+ }
+
+ public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
+ {
+ string stringValue = value as string;
+ if (stringValue == null || stringValue.Length < 3)
+ {
+ throw new Exception("Value must have at least 3 characters.");
+ }
+ return new StringContainer(AppendCultureName(stringValue, culture));
+ }
+
+ public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
+ {
+ StringContainer container = value as StringContainer;
+ if (container.Value == null || container.Value.Length < 3)
+ {
+ throw new Exception("Value must have at least 3 characters.");
+ }
+
+ return AppendCultureName(container.Value, culture);
+ }
+
+ private static string AppendCultureName(string value, CultureInfo culture)
+ {
+ string cultureName = (!String.IsNullOrEmpty(culture.Name)) ? culture.Name : culture.ThreeLetterWindowsLanguageName;
+ return value + " (" + cultureName + ")";
+ }
+ }
+
+ [ModelBinder(typeof(MyStringModelBinder))]
+ private class MyStringModel
+ {
+ public string Value { get; set; }
+ }
+
+ private class MyStringModelBinder : IModelBinder
+ {
+ public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
+ {
+ MyStringModel castModel = bindingContext.Model as MyStringModel;
+ if (castModel != null)
+ {
+ castModel.Value += "_Update";
+ }
+ else
+ {
+ castModel = new MyStringModel() { Value = bindingContext.ModelName + "_Create" };
+ }
+ return castModel;
+ }
+ }
+
+ private class CustomUnvalidatedValueProvider : IUnvalidatedValueProvider
+ {
+ public ValueProviderResult GetValue(string key, bool skipValidation)
+ {
+ string newValue = key + ((skipValidation) ? "Unvalidated" : "Validated");
+ return new ValueProviderResult(newValue, newValue, null);
+ }
+
+ public bool ContainsPrefix(string prefix)
+ {
+ return true;
+ }
+
+ public ValueProviderResult GetValue(string key)
+ {
+ return GetValue(key, skipValidation: false);
+ }
+ }
+
+ private class SimpleController : Controller
+ {
+ }
+
+ public class DefaultModelBinderHelper : DefaultModelBinder
+ {
+ public virtual void PublicBindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor property)
+ {
+ base.BindProperty(controllerContext, bindingContext, property);
+ }
+
+ protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor property)
+ {
+ PublicBindProperty(controllerContext, bindingContext, property);
+ }
+
+ public virtual object PublicCreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
+ {
+ return base.CreateModel(controllerContext, bindingContext, modelType);
+ }
+
+ protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
+ {
+ return PublicCreateModel(controllerContext, bindingContext, modelType);
+ }
+
+ public virtual IEnumerable<PropertyDescriptor> PublicGetFilteredModelProperties(ControllerContext controllerContext, ModelBindingContext bindingContext)
+ {
+ return base.GetFilteredModelProperties(controllerContext, bindingContext);
+ }
+
+ public virtual PropertyDescriptorCollection PublicGetModelProperties(ControllerContext controllerContext, ModelBindingContext bindingContext)
+ {
+ return base.GetModelProperties(controllerContext, bindingContext);
+ }
+
+ protected override PropertyDescriptorCollection GetModelProperties(ControllerContext controllerContext, ModelBindingContext bindingContext)
+ {
+ return PublicGetModelProperties(controllerContext, bindingContext);
+ }
+
+ public string PublicCreateSubIndexName(string prefix, int indexName)
+ {
+ return CreateSubIndexName(prefix, indexName);
+ }
+
+ public string PublicCreateSubPropertyName(string prefix, string propertyName)
+ {
+ return CreateSubPropertyName(prefix, propertyName);
+ }
+
+ public static bool PublicIsModelValid(ModelBindingContext bindingContext)
+ {
+ return IsModelValid(bindingContext);
+ }
+
+ public virtual bool PublicOnModelUpdating(ControllerContext controllerContext, ModelBindingContext bindingContext)
+ {
+ return base.OnModelUpdating(controllerContext, bindingContext);
+ }
+
+ protected override bool OnModelUpdating(ControllerContext controllerContext, ModelBindingContext bindingContext)
+ {
+ return PublicOnModelUpdating(controllerContext, bindingContext);
+ }
+
+ public virtual void PublicOnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
+ {
+ base.OnModelUpdated(controllerContext, bindingContext);
+ }
+
+ protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
+ {
+ PublicOnModelUpdated(controllerContext, bindingContext);
+ }
+
+ public virtual bool PublicOnPropertyValidating(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor property, object value)
+ {
+ return base.OnPropertyValidating(controllerContext, bindingContext, property, value);
+ }
+
+ protected override bool OnPropertyValidating(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor property, object value)
+ {
+ return PublicOnPropertyValidating(controllerContext, bindingContext, property, value);
+ }
+
+ public virtual void PublicOnPropertyValidated(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor property, object value)
+ {
+ base.OnPropertyValidated(controllerContext, bindingContext, property, value);
+ }
+
+ protected override void OnPropertyValidated(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor property, object value)
+ {
+ PublicOnPropertyValidated(controllerContext, bindingContext, property, value);
+ }
+
+ public virtual void PublicSetProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor property, object value)
+ {
+ base.SetProperty(controllerContext, bindingContext, property, value);
+ }
+
+ protected override void SetProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor property, object value)
+ {
+ PublicSetProperty(controllerContext, bindingContext, property, value);
+ }
+ }
+
+ private class MyModel2
+ {
+ private int _intReadWriteNonNegative;
+
+ public int IntReadOnly
+ {
+ get { return 4; }
+ }
+
+ public int IntReadWrite { get; set; }
+
+ public int IntReadWriteNonNegative
+ {
+ get { return _intReadWriteNonNegative; }
+ set
+ {
+ if (value < 0)
+ {
+ throw new ArgumentOutOfRangeException("value", "Value must be non-negative.");
+ }
+ _intReadWriteNonNegative = value;
+ }
+ }
+
+ public int? NullableIntReadWrite { get; set; }
+ }
+
+ [Bind(Exclude = "Foo")]
+ private class ModelWithBindAttribute : ModelWithoutBindAttribute
+ {
+ }
+
+ private class ModelWithoutBindAttribute
+ {
+ public string Foo { get; set; }
+ public string Bar { get; set; }
+ public string Baz { get; set; }
+ }
+
+ private class PropertyTestingModel
+ {
+ public string StringReadWrite { get; set; }
+ public string StringReadOnly { get; private set; }
+ public int IntReadWrite { get; set; }
+ public int IntReadOnly { get; private set; }
+ public object[] ArrayReadWrite { get; set; }
+ public object[] ArrayReadOnly { get; private set; }
+ public Address AddressReadWrite { get; set; }
+ public Address AddressReadOnly { get; private set; }
+ public string Whitelisted { get; set; }
+ public string Blacklisted { get; set; }
+ }
+
+ // --------------------------------------------------------------------------------
+ // DataAnnotations tests
+
+ public const string BASE_MODEL_NAME = "BaseModelName";
+
+ // GetModelProperties tests
+
+ [MetadataType(typeof(Metadata))]
+ class GetModelPropertiesModel
+ {
+ [Required]
+ public int LocalAttributes { get; set; }
+
+ public int MetadataAttributes { get; set; }
+
+ [Required]
+ public int MixedAttributes { get; set; }
+
+ class Metadata
+ {
+ [Range(10, 100)]
+ public int MetadataAttributes { get; set; }
+
+ [Range(10, 100)]
+ public int MixedAttributes { get; set; }
+ }
+ }
+
+ [Fact]
+ public void GetModelPropertiesWithLocalAttributes()
+ {
+ // Arrange
+ TestableDefaultModelBinder<GetModelPropertiesModel> modelBinder = new TestableDefaultModelBinder<GetModelPropertiesModel>();
+
+ // Act
+ PropertyDescriptor property = modelBinder.GetModelProperties()
+ .Cast<PropertyDescriptor>()
+ .Where(pd => pd.Name == "LocalAttributes")
+ .Single();
+
+ // Assert
+ Assert.True(property.Attributes.Cast<Attribute>().Any(a => a is RequiredAttribute));
+ }
+
+ [Fact]
+ public void GetModelPropertiesWithMetadataAttributes()
+ {
+ // Arrange
+ TestableDefaultModelBinder<GetModelPropertiesModel> modelBinder = new TestableDefaultModelBinder<GetModelPropertiesModel>();
+
+ // Act
+ PropertyDescriptor property = modelBinder.GetModelProperties()
+ .Cast<PropertyDescriptor>()
+ .Where(pd => pd.Name == "MetadataAttributes")
+ .Single();
+
+ // Assert
+ Assert.True(property.Attributes.Cast<Attribute>().Any(a => a is RangeAttribute));
+ }
+
+ [Fact]
+ public void GetModelPropertiesWithMixedAttributes()
+ {
+ // Arrange
+ TestableDefaultModelBinder<GetModelPropertiesModel> modelBinder = new TestableDefaultModelBinder<GetModelPropertiesModel>();
+
+ // Act
+ PropertyDescriptor property = modelBinder.GetModelProperties()
+ .Cast<PropertyDescriptor>()
+ .Where(pd => pd.Name == "MixedAttributes")
+ .Single();
+
+ // Assert
+ Assert.True(property.Attributes.Cast<Attribute>().Any(a => a is RequiredAttribute));
+ Assert.True(property.Attributes.Cast<Attribute>().Any(a => a is RangeAttribute));
+ }
+
+ // GetPropertyValue tests
+
+ class GetPropertyValueModel
+ {
+ public string NoAttribute { get; set; }
+
+ [DisplayFormat(ConvertEmptyStringToNull = false)]
+ public string AttributeWithoutConversion { get; set; }
+
+ [DisplayFormat(ConvertEmptyStringToNull = true)]
+ public string AttributeWithConversion { get; set; }
+ }
+
+ [Fact]
+ public void GetPropertyValueWithNoAttributeConvertsEmptyStringToNull()
+ {
+ // Arrange
+ TestableDefaultModelBinder<GetPropertyValueModel> binder = new TestableDefaultModelBinder<GetPropertyValueModel>();
+ binder.Context.ModelMetadata = binder.Context.PropertyMetadata["NoAttribute"];
+
+ // Act
+ object result = binder.GetPropertyValue("NoAttribute", String.Empty);
+
+ // Assert
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void GetPropertyValueWithFalseAttributeDoesNotConvertEmptyStringToNull()
+ {
+ // Arrange
+ TestableDefaultModelBinder<GetPropertyValueModel> binder = new TestableDefaultModelBinder<GetPropertyValueModel>();
+ binder.Context.ModelMetadata = binder.Context.PropertyMetadata["AttributeWithoutConversion"];
+
+ // Act
+ object result = binder.GetPropertyValue("AttributeWithoutConversion", String.Empty);
+
+ // Assert
+ Assert.Equal(String.Empty, result);
+ }
+
+ [Fact]
+ public void GetPropertyValueWithTrueAttributeConvertsEmptyStringToNull()
+ {
+ // Arrange
+ TestableDefaultModelBinder<GetPropertyValueModel> binder = new TestableDefaultModelBinder<GetPropertyValueModel>();
+ binder.Context.ModelMetadata = binder.Context.PropertyMetadata["AttributeWithConversion"];
+
+ // Act
+ object result = binder.GetPropertyValue("AttributeWithConversion", String.Empty);
+
+ // Assert
+ Assert.Null(result);
+ }
+
+ // OnModelUpdated tests
+
+ [Fact]
+ public void OnModelUpdatedPassesNullContainerToValidate()
+ {
+ Mock<ModelValidatorProvider> provider = null;
+
+ try
+ {
+ // Arrange
+ ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(object));
+ ControllerContext context = new ControllerContext();
+ Mock<ModelValidator> validator = new Mock<ModelValidator>(metadata, context);
+ provider = new Mock<ModelValidatorProvider>();
+ provider.Setup(p => p.GetValidators(It.IsAny<ModelMetadata>(), It.IsAny<ControllerContext>()))
+ .Returns(new ModelValidator[] { validator.Object });
+ ModelValidatorProviders.Providers.Add(provider.Object);
+ object model = new object();
+ TestableDefaultModelBinder<object> modelBinder = new TestableDefaultModelBinder<object>(model);
+
+ // Act
+ modelBinder.OnModelUpdated();
+
+ // Assert
+ validator.Verify(v => v.Validate(null));
+ }
+ finally
+ {
+ if (provider != null)
+ {
+ ModelValidatorProviders.Providers.Remove(provider.Object);
+ }
+ }
+ }
+
+ public class MinMaxValidationAttribute : ValidationAttribute
+ {
+ public MinMaxValidationAttribute()
+ : base("Minimum must be less than or equal to Maximum")
+ {
+ }
+
+ public override bool IsValid(object value)
+ {
+ OnModelUpdatedModelMultipleParameters model = (OnModelUpdatedModelMultipleParameters)value;
+ return model.Minimum <= model.Maximum;
+ }
+ }
+
+ [MinMaxValidation]
+ public class OnModelUpdatedModelMultipleParameters
+ {
+ public int Minimum { get; set; }
+ public int Maximum { get; set; }
+ }
+
+ [Fact]
+ public void OnModelUpdatedWithValidationAttributeMultipleParameters()
+ {
+ // Arrange
+ OnModelUpdatedModelMultipleParameters model = new OnModelUpdatedModelMultipleParameters { Minimum = 250, Maximum = 100 };
+ TestableDefaultModelBinder<OnModelUpdatedModelMultipleParameters> modelBinder = new TestableDefaultModelBinder<OnModelUpdatedModelMultipleParameters>(model);
+
+ // Act
+ modelBinder.OnModelUpdated();
+
+ // Assert
+ Assert.Single(modelBinder.ModelState);
+ ModelState stateModel = modelBinder.ModelState[BASE_MODEL_NAME];
+ Assert.NotNull(stateModel);
+ Assert.Equal("Minimum must be less than or equal to Maximum", stateModel.Errors.Single().ErrorMessage);
+ }
+
+ [Fact]
+ public void OnModelUpdatedWithInvalidPropertyValidationWillNotRunEntityLevelValidation()
+ {
+ // Arrange
+ OnModelUpdatedModelMultipleParameters model = new OnModelUpdatedModelMultipleParameters { Minimum = 250, Maximum = 100 };
+ TestableDefaultModelBinder<OnModelUpdatedModelMultipleParameters> modelBinder = new TestableDefaultModelBinder<OnModelUpdatedModelMultipleParameters>(model);
+ modelBinder.ModelState.AddModelError(BASE_MODEL_NAME + ".Minimum", "The minimum value was invalid.");
+
+ // Act
+ modelBinder.OnModelUpdated();
+
+ // Assert
+ Assert.Null(modelBinder.ModelState[BASE_MODEL_NAME]);
+ }
+
+ public class AlwaysInvalidAttribute : ValidationAttribute
+ {
+ public AlwaysInvalidAttribute()
+ {
+ }
+
+ public AlwaysInvalidAttribute(string message)
+ : base(message)
+ {
+ }
+
+ public override bool IsValid(object value)
+ {
+ return false;
+ }
+ }
+
+ [AlwaysInvalid("The object just isn't right")]
+ public class OnModelUpdatedModelNoParameters
+ {
+ }
+
+ [Fact]
+ public void OnModelUpdatedWithValidationAttributeNoParameters()
+ {
+ // Arrange
+ TestableDefaultModelBinder<OnModelUpdatedModelNoParameters> modelBinder = new TestableDefaultModelBinder<OnModelUpdatedModelNoParameters>();
+
+ // Act
+ modelBinder.OnModelUpdated();
+
+ // Assert
+ Assert.Single(modelBinder.ModelState);
+ ModelState stateModel = modelBinder.ModelState[BASE_MODEL_NAME];
+ Assert.NotNull(stateModel);
+ Assert.Equal("The object just isn't right", stateModel.Errors.Single().ErrorMessage);
+ }
+
+ [AlwaysInvalid]
+ public class OnModelUpdatedModelNoValidationResult
+ {
+ }
+
+ [Fact]
+ public void OnModelUpdatedWithValidationAttributeNoValidationMessage()
+ {
+ // Arrange
+ TestableDefaultModelBinder<OnModelUpdatedModelNoValidationResult> modelBinder = new TestableDefaultModelBinder<OnModelUpdatedModelNoValidationResult>();
+
+ // Act
+ modelBinder.OnModelUpdated();
+
+ // Assert
+ Assert.Single(modelBinder.ModelState);
+ ModelState stateModel = modelBinder.ModelState[BASE_MODEL_NAME];
+ Assert.NotNull(stateModel);
+ Assert.Equal("The field OnModelUpdatedModelNoValidationResult is invalid.", stateModel.Errors.Single().ErrorMessage);
+ }
+
+ [Fact]
+ public void OnModelUpdatedDoesNotPlaceErrorMessagesInModelStateWhenSubPropertiesHaveErrors()
+ {
+ // Arrange
+ TestableDefaultModelBinder<OnModelUpdatedModelNoValidationResult> modelBinder = new TestableDefaultModelBinder<OnModelUpdatedModelNoValidationResult>();
+ modelBinder.ModelState.AddModelError("Foo.Bar", "Foo.Bar is invalid");
+ modelBinder.Context.ModelName = "Foo";
+
+ // Act
+ modelBinder.OnModelUpdated();
+
+ // Assert
+ Assert.Null(modelBinder.ModelState["Foo"]);
+ }
+
+ // OnPropertyValidating tests
+
+ class OnPropertyValidatingModel
+ {
+ public string NotValidated { get; set; }
+
+ [Range(10, 65)]
+ public int RangedInteger { get; set; }
+
+ [Required(ErrorMessage = "Custom Required Message")]
+ public int RequiredInteger { get; set; }
+ }
+
+ [Fact]
+ public void OnPropertyValidatingWithoutValidationAttribute()
+ {
+ // Arrange
+ TestableDefaultModelBinder<OnPropertyValidatingModel> modelBinder = new TestableDefaultModelBinder<OnPropertyValidatingModel>();
+
+ // Act
+ modelBinder.OnPropertyValidating("NotValidated", 42);
+
+ // Assert
+ Assert.Empty(modelBinder.ModelState);
+ }
+
+ [Fact]
+ public void OnPropertyValidatingWithValidationAttributePassing()
+ {
+ // Arrange
+ TestableDefaultModelBinder<OnPropertyValidatingModel> modelBinder = new TestableDefaultModelBinder<OnPropertyValidatingModel>();
+ modelBinder.Context.PropertyMetadata["RangedInteger"].Model = 42;
+
+ // Act
+ bool result = modelBinder.OnPropertyValidating("RangedInteger", 42);
+
+ // Assert
+ Assert.True(result);
+ Assert.Empty(modelBinder.ModelState);
+ }
+
+ // SetProperty tests
+
+ [Fact]
+ public void SetPropertyWithRequiredOnValueTypeOnlyResultsInSingleMessage()
+ { // DDB #225150
+ // Arrange
+ TestableDefaultModelBinder<OnPropertyValidatingModel> modelBinder = new TestableDefaultModelBinder<OnPropertyValidatingModel>();
+ modelBinder.Context.ModelMetadata.Model = new OnPropertyValidatingModel();
+
+ // Act
+ modelBinder.SetProperty("RequiredInteger", null);
+
+ // Assert
+ ModelState modelState = modelBinder.ModelState[BASE_MODEL_NAME + ".RequiredInteger"];
+ ModelError modelStateError = modelState.Errors.Single();
+ Assert.Equal("Custom Required Message", modelStateError.ErrorMessage);
+ }
+
+ [Fact]
+ public void SetPropertyAddsDefaultMessageForNonBindableNonNullableValueTypes()
+ {
+ DataAnnotationsModelValidatorProvider.AddImplicitRequiredAttributeForValueTypes = false;
+
+ try
+ {
+ // Arrange
+ TestableDefaultModelBinder<List<String>> modelBinder = new TestableDefaultModelBinder<List<String>>();
+ modelBinder.Context.ModelMetadata.Model = null;
+
+ // Act
+ modelBinder.SetProperty("Count", null);
+
+ // Assert
+ ModelState modelState = modelBinder.ModelState[BASE_MODEL_NAME + ".Count"];
+ ModelError modelStateError = modelState.Errors.Single();
+ Assert.Equal("A value is required.", modelStateError.ErrorMessage);
+ }
+ finally
+ {
+ DataAnnotationsModelValidatorProvider.AddImplicitRequiredAttributeForValueTypes = true;
+ }
+ }
+
+ [Fact]
+ public void SetPropertyCreatesValueRequiredErrorIfNecessary()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => new MyModel(), typeof(MyModel)),
+ ModelName = "theModel",
+ };
+
+ PropertyDescriptor property = TypeDescriptor.GetProperties(typeof(MyModel))["ReadWriteProperty"];
+ DefaultModelBinderHelper helper = new DefaultModelBinderHelper();
+
+ // Act
+ helper.PublicSetProperty(new ControllerContext(), bindingContext, property, null);
+
+ // Assert
+ Assert.Equal("The ReadWriteProperty field is required.", bindingContext.ModelState["theModel.ReadWriteProperty"].Errors[0].ErrorMessage);
+ }
+
+ [Fact]
+ public void SetPropertyWithThrowingSetter()
+ {
+ // Arrange
+ TestableDefaultModelBinder<SetPropertyModel> binder = new TestableDefaultModelBinder<SetPropertyModel>();
+
+ // Act
+ binder.SetProperty("NonNullableString", null);
+
+ // Assert
+ ModelState modelState = binder.Context.ModelState[BASE_MODEL_NAME + ".NonNullableString"];
+ Assert.Single(modelState.Errors);
+ Assert.IsType<ArgumentNullException>(modelState.Errors[0].Exception);
+ }
+
+ [Fact]
+ public void SetPropertyWithNullValueAndThrowingSetterWithRequiredAttribute()
+ { // DDB #227809
+ // Arrange
+ TestableDefaultModelBinder<SetPropertyModel> binder = new TestableDefaultModelBinder<SetPropertyModel>();
+
+ // Act
+ binder.SetProperty("NonNullableStringWithAttribute", null);
+
+ // Assert
+ ModelState modelState = binder.Context.ModelState[BASE_MODEL_NAME + ".NonNullableStringWithAttribute"];
+ var error = Assert.Single(modelState.Errors);
+ Assert.Equal("My custom required message", error.ErrorMessage);
+ }
+
+ [Fact]
+ public void SetPropertyDoesNothingIfPropertyIsReadOnly()
+ {
+ // Arrange
+ MyModel model = new MyModel();
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, model.GetType()),
+ ModelName = "theModel"
+ };
+
+ PropertyDescriptor property = TypeDescriptor.GetProperties(model)["ReadOnlyProperty"];
+ DefaultModelBinderHelper helper = new DefaultModelBinderHelper();
+
+ // Act
+ helper.PublicSetProperty(new ControllerContext(), bindingContext, property, 42);
+
+ // Assert
+ Assert.Empty(bindingContext.ModelState);
+ }
+
+ [Fact]
+ public void SetPropertySuccess()
+ {
+ // Arrange
+ TestableDefaultModelBinder<SetPropertyModel> binder = new TestableDefaultModelBinder<SetPropertyModel>();
+
+ // Act
+ binder.SetProperty("NullableString", "The new value");
+
+ // Assert
+ Assert.Equal("The new value", ((SetPropertyModel)binder.Context.Model).NullableString);
+ Assert.Empty(binder.Context.ModelState);
+ }
+
+ class SetPropertyModel
+ {
+ public string NullableString { get; set; }
+
+ public string NonNullableString
+ {
+ get { return null; }
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException("value");
+ }
+ }
+ }
+
+ [Required(ErrorMessage = "My custom required message")]
+ public string NonNullableStringWithAttribute
+ {
+ get { return null; }
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException("value");
+ }
+ }
+ }
+ }
+
+ // Helper methods
+
+ static PropertyDescriptor GetProperty<T>(string propertyName)
+ {
+ return TypeDescriptor.GetProperties(typeof(T))
+ .Cast<PropertyDescriptor>()
+ .Where(p => p.Name == propertyName)
+ .Single();
+ }
+
+ static ICustomTypeDescriptor GetType<T>()
+ {
+ return TypeDescriptor.GetProvider(typeof(T)).GetTypeDescriptor(typeof(T));
+ }
+
+ // Helper classes
+
+ class TestableDefaultModelBinder<TModel> : DefaultModelBinder
+ where TModel : new()
+ {
+ public TestableDefaultModelBinder()
+ : this(new TModel())
+ {
+ }
+
+ public TestableDefaultModelBinder(TModel model)
+ {
+ ModelState = new ModelStateDictionary();
+
+ Context = new ModelBindingContext();
+ Context.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, typeof(TModel));
+ Context.ModelName = BASE_MODEL_NAME;
+ Context.ModelState = ModelState;
+ }
+
+ public ModelBindingContext Context { get; private set; }
+
+ public ModelStateDictionary ModelState { get; set; }
+
+ public void BindProperty(string propertyName)
+ {
+ base.BindProperty(new ControllerContext(), Context, GetProperty<TModel>(propertyName));
+ }
+
+ public PropertyDescriptorCollection GetModelProperties()
+ {
+ return base.GetModelProperties(new ControllerContext(), Context);
+ }
+
+ public object GetPropertyValue(string propertyName, object existingValue)
+ {
+ Mock<IModelBinder> mockModelBinder = new Mock<IModelBinder>();
+ mockModelBinder.Setup(b => b.BindModel(It.IsAny<ControllerContext>(), Context)).Returns(existingValue);
+ return base.GetPropertyValue(new ControllerContext(), Context, GetProperty<TModel>(propertyName), mockModelBinder.Object);
+ }
+
+ public bool OnPropertyValidating(string propertyName, object value)
+ {
+ return base.OnPropertyValidating(new ControllerContext(), Context, GetProperty<TModel>(propertyName), value);
+ }
+
+ public void OnPropertyValidated(string propertyName, object value)
+ {
+ base.OnPropertyValidated(new ControllerContext(), Context, GetProperty<TModel>(propertyName), value);
+ }
+
+ public void OnModelUpdated()
+ {
+ base.OnModelUpdated(new ControllerContext(), Context);
+ }
+
+ public void SetProperty(string propertyName, object value)
+ {
+ base.SetProperty(new ControllerContext(), Context, GetProperty<TModel>(propertyName), value);
+ }
+ }
+
+ private class CountryState
+ {
+ public string Name { get; set; }
+
+ public IEnumerable<string> States { get; set; }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/DefaultViewLocationCacheTest.cs b/test/System.Web.Mvc.Test/Test/DefaultViewLocationCacheTest.cs
new file mode 100644
index 00000000..90fbf950
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/DefaultViewLocationCacheTest.cs
@@ -0,0 +1,73 @@
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class DefaultViewLocationCacheTest
+ {
+ [Fact]
+ public void TimeSpanProperty()
+ {
+ // Arrange
+ TimeSpan timeSpan = new TimeSpan(0, 20, 0);
+ DefaultViewLocationCache viewCache = new DefaultViewLocationCache(timeSpan);
+
+ // Assert
+ Assert.Equal(timeSpan.Ticks, viewCache.TimeSpan.Ticks);
+ }
+
+ [Fact]
+ public void ConstructorAssignsDefaultTimeSpan()
+ {
+ // Arrange
+ DefaultViewLocationCache viewLocationCache = new DefaultViewLocationCache();
+ TimeSpan timeSpan = new TimeSpan(0, 15, 0);
+
+ // Assert
+ Assert.Equal(timeSpan.Ticks, viewLocationCache.TimeSpan.Ticks);
+ }
+
+ [Fact]
+ public void ConstructorWithNegativeTimeSpanThrows()
+ {
+ // Assert
+ Assert.Throws<InvalidOperationException>(
+ delegate { new DefaultViewLocationCache(new TimeSpan(-1, 0, 0)); },
+ "The number of ticks for the TimeSpan value must be greater than or equal to 0.");
+ }
+
+ [Fact]
+ public void GetViewLocationThrowsWithNullHttpContext()
+ {
+ // Arrange
+ DefaultViewLocationCache viewLocationCache = new DefaultViewLocationCache();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { string viewLocation = viewLocationCache.GetViewLocation(null /* httpContext */, "foo"); },
+ "httpContext");
+ }
+
+ [Fact]
+ public void InsertViewLocationThrowsWithNullHttpContext()
+ {
+ // Arrange
+ DefaultViewLocationCache viewLocationCache = new DefaultViewLocationCache();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { viewLocationCache.InsertViewLocation(null /* httpContext */, "foo", "fooPath"); },
+ "httpContext");
+ }
+
+ [Fact]
+ public void NullViewLocationCacheReturnsNullLocations()
+ {
+ // Act
+ DefaultViewLocationCache.Null.InsertViewLocation(null /* httpContext */, "foo", "fooPath");
+
+ // Assert
+ Assert.Equal(null, DefaultViewLocationCache.Null.GetViewLocation(null /* httpContext */, "foo"));
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/DependencyResolverTest.cs b/test/System.Web.Mvc.Test/Test/DependencyResolverTest.cs
new file mode 100644
index 00000000..ebc65e9d
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/DependencyResolverTest.cs
@@ -0,0 +1,241 @@
+using System.Collections.Generic;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class DependencyResolverTest
+ {
+ [Fact]
+ public void GuardClauses()
+ {
+ // Arrange
+ var resolver = new DependencyResolver();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => resolver.InnerSetResolver((IDependencyResolver)null),
+ "resolver"
+ );
+ Assert.ThrowsArgumentNull(
+ () => resolver.InnerSetResolver((object)null),
+ "commonServiceLocator"
+ );
+ Assert.ThrowsArgumentNull(
+ () => resolver.InnerSetResolver(null, type => null),
+ "getService"
+ );
+ Assert.ThrowsArgumentNull(
+ () => resolver.InnerSetResolver(type => null, null),
+ "getServices"
+ );
+ }
+
+ [Fact]
+ public void DefaultServiceLocatorBehaviorTests()
+ {
+ // Arrange
+ var resolver = new DependencyResolver();
+
+ // Act & Assert
+ Assert.NotNull(resolver.InnerCurrent.GetService<object>()); // Concrete type
+ Assert.Null(resolver.InnerCurrent.GetService<ModelMetadataProvider>()); // Abstract type
+ Assert.Null(resolver.InnerCurrent.GetService<IDisposable>()); // Interface
+ Assert.Null(resolver.InnerCurrent.GetService(typeof(List<>))); // Open generic
+ }
+
+ [Fact]
+ public void DefaultServiceLocatorResolvesNewInstances()
+ {
+ // Arrange
+ var resolver = new DependencyResolver();
+
+ // Act
+ object obj1 = resolver.InnerCurrent.GetService<object>();
+ object obj2 = resolver.InnerCurrent.GetService<object>();
+
+ // Assert
+ Assert.NotSame(obj1, obj2);
+ }
+
+ public class MockableResolver
+ {
+ public virtual object Get(Type type)
+ {
+ throw new NotImplementedException();
+ }
+
+ public virtual IEnumerable<object> GetAll(Type type)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ [Fact]
+ public void ResolverPassesCallsToDelegateBasedResolver()
+ {
+ // Arrange
+ var resolver = new DependencyResolver();
+ var mockResolver = new Mock<MockableResolver>();
+ resolver.InnerSetResolver(mockResolver.Object.Get, mockResolver.Object.GetAll);
+
+ // Act & Assert
+ resolver.InnerCurrent.GetService(typeof(object));
+ mockResolver.Verify(r => r.Get(typeof(object)));
+
+ resolver.InnerCurrent.GetServices(typeof(string));
+ mockResolver.Verify(r => r.GetAll(typeof(string)));
+ }
+
+ public class MockableCommonServiceLocator
+ {
+ public virtual object GetInstance(Type type)
+ {
+ throw new NotImplementedException();
+ }
+
+ public virtual IEnumerable<object> GetAllInstances(Type type)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ [Fact]
+ public void ResolverPassesCallsToICommonServiceLocator()
+ {
+ // Arrange
+ var resolver = new DependencyResolver();
+ var mockResolver = new Mock<MockableCommonServiceLocator>();
+ resolver.InnerSetResolver(mockResolver.Object);
+
+ // Act & Assert
+ resolver.InnerCurrent.GetService(typeof(object));
+ mockResolver.Verify(r => r.GetInstance(typeof(object)));
+
+ resolver.InnerCurrent.GetServices(typeof(string));
+ mockResolver.Verify(r => r.GetAllInstances(typeof(string)));
+ }
+
+ class MissingGetInstance
+ {
+ public IEnumerable<object> GetAllInstances(Type type)
+ {
+ return null;
+ }
+ }
+
+ class MissingGetAllInstances
+ {
+ public object GetInstance(Type type)
+ {
+ return null;
+ }
+ }
+
+ class GetInstanceHasWrongSignature
+ {
+ public string GetInstance(Type type)
+ {
+ return null;
+ }
+
+ public IEnumerable<object> GetAllInstances(Type type)
+ {
+ return null;
+ }
+ }
+
+ class GetAllInstancesHasWrongSignature
+ {
+ public object GetInstance(Type type)
+ {
+ return null;
+ }
+
+ public IEnumerable<string> GetAllInstances(Type type)
+ {
+ return null;
+ }
+ }
+
+ [Fact]
+ public void ValidationOfCommonServiceLocatorTests()
+ {
+ // Arrange
+ var resolver = new DependencyResolver();
+
+ // Act & Assert
+ Assert.Throws<ArgumentException>(
+ () => resolver.InnerSetResolver(new MissingGetInstance()),
+ @"The type System.Web.Mvc.Test.DependencyResolverTest+MissingGetInstance does not appear to implement Microsoft.Practices.ServiceLocation.IServiceLocator.
+Parameter name: commonServiceLocator"
+ );
+ Assert.Throws<ArgumentException>(
+ () => resolver.InnerSetResolver(new MissingGetAllInstances()),
+ @"The type System.Web.Mvc.Test.DependencyResolverTest+MissingGetAllInstances does not appear to implement Microsoft.Practices.ServiceLocation.IServiceLocator.
+Parameter name: commonServiceLocator"
+ );
+ Assert.Throws<ArgumentException>(
+ () => resolver.InnerSetResolver(new GetInstanceHasWrongSignature()),
+ @"The type System.Web.Mvc.Test.DependencyResolverTest+GetInstanceHasWrongSignature does not appear to implement Microsoft.Practices.ServiceLocation.IServiceLocator.
+Parameter name: commonServiceLocator"
+ );
+ Assert.Throws<ArgumentException>(
+ () => resolver.InnerSetResolver(new GetAllInstancesHasWrongSignature()),
+ @"The type System.Web.Mvc.Test.DependencyResolverTest+GetAllInstancesHasWrongSignature does not appear to implement Microsoft.Practices.ServiceLocation.IServiceLocator.
+Parameter name: commonServiceLocator"
+ );
+ }
+
+
+
+ [Fact]
+ public void DependencyResolverCache()
+ {
+ // Verify that when we ask for an interface twice, it only queries the underlying resolver once.
+ var resolver = new DependencyResolver();
+
+ Mock<IDependencyResolver> resolverMock = new Mock<IDependencyResolver>();
+ resolverMock.Setup(r => r.GetService(typeof(object))).Returns(() => new object());
+ resolverMock.Setup(r => r.GetService(typeof(int))).Returns(15);
+
+ resolver.InnerSetResolver(resolverMock.Object);
+
+ object result1 = resolver.InnerCurrentCache.GetService(typeof(object)); // 1st call
+ object otherResult = resolver.InnerCurrentCache.GetService(typeof(int)); // 2nd call
+ object result2 = resolver.InnerCurrentCache.GetService(typeof(object)); // Cached result from 1st call
+
+
+ resolverMock.Verify(r => r.GetService(typeof(object)), Times.Once());
+ resolverMock.Verify(r => r.GetService(typeof(int)), Times.Once());
+ Assert.Same(result1, result2);
+ Assert.Equal(15, otherResult);
+ }
+
+ [Fact]
+ public void ClearDependencyResolverCache()
+ {
+ // Verify that when we ask for an interface twice, it only queries the underlying resolver once.
+ var resolver = new DependencyResolver();
+
+ Mock<IDependencyResolver> resolverMock = new Mock<IDependencyResolver>();
+ resolverMock.Setup(r => r.GetService(typeof(object))).Returns(() => new object());
+ resolverMock.Setup(r => r.GetService(typeof(int))).Returns(15);
+
+ resolver.InnerSetResolver(resolverMock.Object);
+
+ object result1 = resolver.InnerCurrentCache.GetService(typeof(object)); // 1st call
+ object otherResult = resolver.InnerCurrentCache.GetService(typeof(int)); // 2nd call
+ resolver.InnerSetResolver(resolverMock.Object); // This will clear the cache
+ object result2 = resolver.InnerCurrentCache.GetService(typeof(object)); // 3rd call
+
+
+ resolverMock.Verify(r => r.GetService(typeof(object)), Times.Exactly(2));
+ resolverMock.Verify(r => r.GetService(typeof(int)), Times.Once());
+ Assert.NotSame(result1, result2);
+ Assert.Equal(15, otherResult);
+ }
+
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/DescriptorUtilTest.cs b/test/System.Web.Mvc.Test/Test/DescriptorUtilTest.cs
new file mode 100644
index 00000000..22a155a4
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/DescriptorUtilTest.cs
@@ -0,0 +1,55 @@
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class DescriptorUtilTest
+ {
+ [Fact]
+ public void CreateUniqueId_FromIUniquelyIdentifiable()
+ {
+ // Arrange
+ CustomUniquelyIdentifiable custom = new CustomUniquelyIdentifiable("hello-world");
+
+ // Act
+ string retVal = DescriptorUtil.CreateUniqueId(custom);
+
+ // Assert
+ Assert.Equal("[11]hello-world", retVal);
+ }
+
+ [Fact]
+ public void CreateUniqueId_FromMemberInfo()
+ {
+ // Arrange
+ string moduleVersionId = typeof(DescriptorUtilTest).Module.ModuleVersionId.ToString();
+ string metadataToken = typeof(DescriptorUtilTest).MetadataToken.ToString();
+ string expected = String.Format("[{0}]{1}[{2}]{3}", moduleVersionId.Length, moduleVersionId, metadataToken.Length, metadataToken);
+
+ // Act
+ string retVal = DescriptorUtil.CreateUniqueId(typeof(DescriptorUtilTest));
+
+ // Assert
+ Assert.Equal(expected, retVal);
+ }
+
+ [Fact]
+ public void CreateUniqueId_FromSimpleTypes()
+ {
+ // Act
+ string retVal = DescriptorUtil.CreateUniqueId("foo", null, 12345);
+
+ // Assert
+ Assert.Equal("[3]foo[-1][5]12345", retVal);
+ }
+
+ private sealed class CustomUniquelyIdentifiable : IUniquelyIdentifiable
+ {
+ public CustomUniquelyIdentifiable(string uniqueId)
+ {
+ UniqueId = uniqueId;
+ }
+
+ public string UniqueId { get; private set; }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/DictionaryHelpersTest.cs b/test/System.Web.Mvc.Test/Test/DictionaryHelpersTest.cs
new file mode 100644
index 00000000..6df4c367
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/DictionaryHelpersTest.cs
@@ -0,0 +1,89 @@
+using System.Collections.Generic;
+using System.Linq;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class DictionaryHelpersTest
+ {
+ [Fact]
+ public void DoesAnyKeyHavePrefixFailure()
+ {
+ // Arrange
+ Dictionary<string, object> dict = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
+ {
+ { "FOOBAR", 42 }
+ };
+
+ // Act
+ bool wasPrefixFound = DictionaryHelpers.DoesAnyKeyHavePrefix(dict, "foo");
+
+ // Assert
+ Assert.False(wasPrefixFound);
+ }
+
+ [Fact]
+ public void DoesAnyKeyHavePrefixSuccess()
+ {
+ // Arrange
+ Dictionary<string, object> dict = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
+ {
+ { "FOO.BAR", 42 }
+ };
+
+ // Act
+ bool wasPrefixFound = DictionaryHelpers.DoesAnyKeyHavePrefix(dict, "foo");
+
+ // Assert
+ Assert.True(wasPrefixFound);
+ }
+
+ [Fact]
+ public void FindKeysWithPrefix()
+ {
+ // Arrange
+ Dictionary<string, string> dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
+ {
+ { "FOO", "fooValue" },
+ { "FOOBAR", "foobarValue" },
+ { "FOO.BAR", "foo.barValue" },
+ { "FOO[0]", "foo[0]Value" },
+ { "BAR", "barValue" }
+ };
+
+ // Act
+ var matchingEntries = DictionaryHelpers.FindKeysWithPrefix(dict, "foo");
+
+ // Assert
+ var matchingEntriesList = matchingEntries.OrderBy(entry => entry.Key).ToList();
+ Assert.Equal(3, matchingEntriesList.Count);
+ Assert.Equal("foo", matchingEntriesList[0].Key);
+ Assert.Equal("fooValue", matchingEntriesList[0].Value);
+ Assert.Equal("FOO.BAR", matchingEntriesList[1].Key);
+ Assert.Equal("foo.barValue", matchingEntriesList[1].Value);
+ Assert.Equal("FOO[0]", matchingEntriesList[2].Key);
+ Assert.Equal("foo[0]Value", matchingEntriesList[2].Value);
+ }
+
+ [Fact]
+ public void GetOrDefaultMissing()
+ {
+ Dictionary<string, int> dict = new Dictionary<string, int>();
+ int @default = 15;
+
+ int value = dict.GetOrDefault("two", @default);
+
+ Assert.Equal(15, @default);
+ }
+
+ [Fact]
+ public void GetOrDefaultPresent()
+ {
+ Dictionary<string, int> dict = new Dictionary<string, int>() { { "one", 1 } };
+
+ int value = dict.GetOrDefault("one", -999);
+
+ Assert.Equal(1, 1);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/DictionaryValueProviderTest.cs b/test/System.Web.Mvc.Test/Test/DictionaryValueProviderTest.cs
new file mode 100644
index 00000000..9ebf55df
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/DictionaryValueProviderTest.cs
@@ -0,0 +1,102 @@
+using System.Collections.Generic;
+using System.Globalization;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class DictionaryValueProviderTest
+ {
+ private static readonly Dictionary<string, object> _backingStore = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
+ {
+ { "forty.two", 42 },
+ { "nineteen.eighty.four", new DateTime(1984, 1, 1) }
+ };
+
+ [Fact]
+ public void Constructor_ThrowsIfDictionaryIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new DictionaryValueProvider<object>(null, CultureInfo.InvariantCulture); }, "dictionary");
+ }
+
+ [Fact]
+ public void ContainsPrefix()
+ {
+ // Arrange
+ DictionaryValueProvider<object> valueProvider = new DictionaryValueProvider<object>(_backingStore, null);
+
+ // Act
+ bool result = valueProvider.ContainsPrefix("forty");
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void ContainsPrefix_DoesNotContainEmptyPrefixIfBackingStoreIsEmpty()
+ {
+ // Arrange
+ DictionaryValueProvider<object> valueProvider = new DictionaryValueProvider<object>(new Dictionary<string, object>(), null);
+
+ // Act
+ bool result = valueProvider.ContainsPrefix("");
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void ContainsPrefix_ThrowsIfPrefixIsNull()
+ {
+ // Arrange
+ DictionaryValueProvider<object> valueProvider = new DictionaryValueProvider<object>(_backingStore, null);
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { valueProvider.ContainsPrefix(null); }, "prefix");
+ }
+
+ [Fact]
+ public void GetValue()
+ {
+ // Arrange
+ CultureInfo culture = CultureInfo.GetCultureInfo("fr-FR");
+ DictionaryValueProvider<object> valueProvider = new DictionaryValueProvider<object>(_backingStore, culture);
+
+ // Act
+ ValueProviderResult vpResult = valueProvider.GetValue("nineteen.eighty.four");
+
+ // Assert
+ Assert.NotNull(vpResult);
+ Assert.Equal(new DateTime(1984, 1, 1), vpResult.RawValue);
+ Assert.Equal("01/01/1984 00:00:00", vpResult.AttemptedValue);
+ Assert.Equal(culture, vpResult.Culture);
+ }
+
+ [Fact]
+ public void GetValue_ReturnsNullIfKeyNotFound()
+ {
+ // Arrange
+ DictionaryValueProvider<object> valueProvider = new DictionaryValueProvider<object>(_backingStore, null);
+
+ // Act
+ ValueProviderResult vpResult = valueProvider.GetValue("nineteen.eighty");
+
+ // Assert
+ Assert.Null(vpResult);
+ }
+
+ [Fact]
+ public void GetValue_ThrowsIfKeyIsNull()
+ {
+ // Arrange
+ DictionaryValueProvider<object> valueProvider = new DictionaryValueProvider<object>(_backingStore, null);
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { valueProvider.GetValue(null); }, "key");
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/DynamicViewDataDictionaryTest.cs b/test/System.Web.Mvc.Test/Test/DynamicViewDataDictionaryTest.cs
new file mode 100644
index 00000000..55430a0f
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/DynamicViewDataDictionaryTest.cs
@@ -0,0 +1,214 @@
+using System.Collections.Generic;
+using System.Dynamic;
+using System.Linq;
+using Moq;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class DynamicViewDataDictionaryTest
+ {
+ [Fact]
+ public void Get_OnExistingProperty_ReturnsValue()
+ {
+ // Arrange
+ ViewDataDictionary vd = new ViewDataDictionary()
+ {
+ { "Prop", "Value" }
+ };
+ dynamic dynamicVD = new DynamicViewDataDictionary(() => vd);
+
+ // Act
+ object value = dynamicVD.Prop;
+
+ // Assert
+ Assert.IsType<string>(value);
+ Assert.Equal("Value", value);
+ }
+
+ [Fact]
+ public void Get_OnNonExistentProperty_ReturnsNull()
+ {
+ // Arrange
+ ViewDataDictionary vd = new ViewDataDictionary();
+ dynamic dynamicVD = new DynamicViewDataDictionary(() => vd);
+
+ // Act
+ object value = dynamicVD.Prop;
+
+ // Assert
+ Assert.Null(value);
+ }
+
+ [Fact]
+ public void Set_OnExistingProperty_OverridesValue()
+ {
+ // Arrange
+ ViewDataDictionary vd = new ViewDataDictionary()
+ {
+ { "Prop", "Value" }
+ };
+ dynamic dynamicVD = new DynamicViewDataDictionary(() => vd);
+
+ // Act
+ dynamicVD.Prop = "NewValue";
+
+ // Assert
+ Assert.Equal("NewValue", dynamicVD.Prop);
+ Assert.Equal("NewValue", vd["Prop"]);
+ }
+
+ [Fact]
+ public void Set_OnNonExistentProperty_SetsValue()
+ {
+ // Arrange
+ ViewDataDictionary vd = new ViewDataDictionary();
+ dynamic dynamicVD = new DynamicViewDataDictionary(() => vd);
+
+ // Act
+ dynamicVD.Prop = "NewValue";
+
+ // Assert
+ Assert.Equal("NewValue", dynamicVD.Prop);
+ Assert.Equal("NewValue", vd["Prop"]);
+ }
+
+ [Fact]
+ public void TryGetMember_OnExistingProperty_ReturnsValueAndSucceeds()
+ {
+ // Arrange
+ object result = null;
+ ViewDataDictionary vd = new ViewDataDictionary()
+ {
+ { "Prop", "Value" }
+ };
+ DynamicViewDataDictionary dynamicVD = new DynamicViewDataDictionary(() => vd);
+ Mock<GetMemberBinder> binderMock = new Mock<GetMemberBinder>("Prop", /* ignoreCase */ false);
+
+ // Act
+ bool success = dynamicVD.TryGetMember(binderMock.Object, out result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal("Value", result);
+ }
+
+ [Fact]
+ public void TryGetMember_OnNonExistentProperty_ReturnsNullAndSucceeds()
+ {
+ // Arrange
+ object result = null;
+ ViewDataDictionary vd = new ViewDataDictionary();
+ DynamicViewDataDictionary dynamicVD = new DynamicViewDataDictionary(() => vd);
+ Mock<GetMemberBinder> binderMock = new Mock<GetMemberBinder>("Prop", /* ignoreCase */ false);
+
+ // Act
+ bool success = dynamicVD.TryGetMember(binderMock.Object, out result);
+
+ // Assert
+ Assert.True(success);
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void TrySetMember_OnExistingProperty_OverridesValueAndSucceeds()
+ {
+ // Arrange
+ ViewDataDictionary vd = new ViewDataDictionary()
+ {
+ { "Prop", "Value" }
+ };
+ DynamicViewDataDictionary dynamicVD = new DynamicViewDataDictionary(() => vd);
+ Mock<SetMemberBinder> binderMock = new Mock<SetMemberBinder>("Prop", /* ignoreCase */ false);
+
+ // Act
+ bool success = dynamicVD.TrySetMember(binderMock.Object, "NewValue");
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal("NewValue", ((dynamic)dynamicVD).Prop);
+ Assert.Equal("NewValue", vd["Prop"]);
+ }
+
+ [Fact]
+ public void TrySetMember_OnNonExistentProperty_SetsValueAndSucceeds()
+ {
+ // Arrange
+ ViewDataDictionary vd = new ViewDataDictionary();
+ DynamicViewDataDictionary dynamicVD = new DynamicViewDataDictionary(() => vd);
+ Mock<SetMemberBinder> binderMock = new Mock<SetMemberBinder>("Prop", /* ignoreCase */ false);
+
+ // Act
+ bool success = dynamicVD.TrySetMember(binderMock.Object, "NewValue");
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal("NewValue", ((dynamic)dynamicVD).Prop);
+ Assert.Equal("NewValue", vd["Prop"]);
+ }
+
+ [Fact]
+ public void GetDynamicMemberNames_ReturnsEmptyListForEmptyViewDataDictionary()
+ {
+ // Arrange
+ ViewDataDictionary vd = new ViewDataDictionary();
+ DynamicViewDataDictionary dvd = new DynamicViewDataDictionary(() => vd);
+
+ // Act
+ IEnumerable<string> result = dvd.GetDynamicMemberNames();
+
+ // Assert
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void GetDynamicMemberNames_ReturnsKeyNamesForFilledViewDataDictionary()
+ {
+ // Arrange
+ ViewDataDictionary vd = new ViewDataDictionary()
+ {
+ { "Prop1", 1 },
+ { "Prop2", 2 }
+ };
+ DynamicViewDataDictionary dvd = new DynamicViewDataDictionary(() => vd);
+
+ // Act
+ var result = dvd.GetDynamicMemberNames();
+
+ // Assert
+ Assert.Equal(new[] { "Prop1", "Prop2" }, result.OrderBy(s => s).ToArray());
+ }
+
+ [Fact]
+ public void GetDynamicMemberNames_ReflectsChangesToUnderlyingViewDataDictionary()
+ {
+ // Arrange
+ ViewDataDictionary vd = new ViewDataDictionary();
+ vd["OldProp"] = 123;
+ DynamicViewDataDictionary dvd = new DynamicViewDataDictionary(() => vd);
+ vd["NewProp"] = "somevalue";
+
+ // Act
+ var result = dvd.GetDynamicMemberNames();
+
+ // Assert
+ Assert.Equal(new[] { "NewProp", "OldProp" }, result.OrderBy(s => s).ToArray());
+ }
+
+ [Fact]
+ public void GetDynamicMemberNames_ReflectsChangesToDynamicObject()
+ {
+ // Arrange
+ ViewDataDictionary vd = new ViewDataDictionary();
+ vd["OldProp"] = 123;
+ DynamicViewDataDictionary dvd = new DynamicViewDataDictionary(() => vd);
+ ((dynamic)dvd).NewProp = "foo";
+
+ // Act
+ var result = dvd.GetDynamicMemberNames();
+
+ // Assert
+ Assert.Equal(new[] { "NewProp", "OldProp" }, result.OrderBy(s => s).ToArray());
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/EmptyModelValidatorProviderTest.cs b/test/System.Web.Mvc.Test/Test/EmptyModelValidatorProviderTest.cs
new file mode 100644
index 00000000..d03598ae
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/EmptyModelValidatorProviderTest.cs
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class EmptyModelValidatorProviderTest
+ {
+ [Fact]
+ public void ReturnsNoValidators()
+ {
+ // Arrange
+ EmptyModelValidatorProvider provider = new EmptyModelValidatorProvider();
+
+ // Act
+ IEnumerable<ModelValidator> result = provider.GetValidators(null, null);
+
+ // Assert
+ Assert.Empty(result);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ExceptionContextTest.cs b/test/System.Web.Mvc.Test/Test/ExceptionContextTest.cs
new file mode 100644
index 00000000..79222697
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ExceptionContextTest.cs
@@ -0,0 +1,46 @@
+using System.Web.TestUtil;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ExceptionContextTest
+ {
+ [Fact]
+ public void ConstructorThrowsIfExceptionIsNull()
+ {
+ // Arrange
+ ControllerContext controllerContext = new Mock<ControllerContext>().Object;
+ Exception exception = null;
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new ExceptionContext(controllerContext, exception); }, "exception");
+ }
+
+ [Fact]
+ public void ExceptionProperty()
+ {
+ // Arrange
+ ControllerContext controllerContext = new Mock<ControllerContext>().Object;
+ Exception exception = new Exception();
+
+ // Act
+ ExceptionContext exceptionContext = new ExceptionContext(controllerContext, exception);
+
+ // Assert
+ Assert.Equal(exception, exceptionContext.Exception);
+ }
+
+ [Fact]
+ public void ResultProperty()
+ {
+ // Arrange
+ ExceptionContext exceptionContext = new Mock<ExceptionContext>().Object;
+
+ // Act & assert
+ MemberHelper.TestPropertyWithDefaultInstance(exceptionContext, "Result", new ViewResult(), EmptyResult.Instance);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ExpressionHelperTest.cs b/test/System.Web.Mvc.Test/Test/ExpressionHelperTest.cs
new file mode 100644
index 00000000..a51aa27f
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ExpressionHelperTest.cs
@@ -0,0 +1,120 @@
+using System.Linq.Expressions;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ExpressionHelperTest
+ {
+ [Fact]
+ public void StringBasedExpressionTests()
+ {
+ ViewDataDictionary vdd = new ViewDataDictionary();
+
+ // Uses the given expression as the expression text
+ Assert.Equal("?", ExpressionHelper.GetExpressionText("?"));
+
+ // Exactly "Model" (case-insensitive) is turned into empty string
+ Assert.Equal(String.Empty, ExpressionHelper.GetExpressionText("Model"));
+ Assert.Equal(String.Empty, ExpressionHelper.GetExpressionText("mOdEl"));
+
+ // Beginning with "Model" is untouched
+ Assert.Equal("Model.Foo", ExpressionHelper.GetExpressionText("Model.Foo"));
+ }
+
+ [Fact]
+ public void LambdaBasedExpressionTextTests()
+ {
+ // "Model" at the front of the expression is excluded (case insensitively)
+ DummyContactModel Model = null;
+ Assert.Equal(String.Empty, ExpressionHelper.GetExpressionText(Lambda<object, DummyContactModel>(m => Model)));
+ Assert.Equal("FirstName", ExpressionHelper.GetExpressionText(Lambda<object, string>(m => Model.FirstName)));
+
+ DummyContactModel mOdeL = null;
+ Assert.Equal(String.Empty, ExpressionHelper.GetExpressionText(Lambda<object, DummyContactModel>(m => mOdeL)));
+ Assert.Equal("FirstName", ExpressionHelper.GetExpressionText(Lambda<object, string>(m => mOdeL.FirstName)));
+
+ // Model property of model is passed through
+ Assert.Equal("Model", ExpressionHelper.GetExpressionText(Lambda<DummyModelContainer, DummyContactModel>(m => m.Model)));
+
+ // "Model" in the middle of the expression is not excluded
+ DummyModelContainer container = null;
+ Assert.Equal("container.Model", ExpressionHelper.GetExpressionText(Lambda<object, DummyContactModel>(m => container.Model)));
+ Assert.Equal("container.Model.FirstName", ExpressionHelper.GetExpressionText(Lambda<object, string>(m => container.Model.FirstName)));
+
+ // The parameter is excluded
+ Assert.Equal(String.Empty, ExpressionHelper.GetExpressionText(Lambda<DummyContactModel, DummyContactModel>(m => m)));
+ Assert.Equal("FirstName", ExpressionHelper.GetExpressionText(Lambda<DummyContactModel, string>(m => m.FirstName)));
+
+ // Integer indexer is included and properly computed from captured values
+ int x = 2;
+ Assert.Equal("container.Model[42].Length", ExpressionHelper.GetExpressionText(Lambda<object, int>(m => container.Model[x * 21].Length)));
+ Assert.Equal("[42]", ExpressionHelper.GetExpressionText(Lambda<int[], int>(m => m[x * 21])));
+
+ // String indexer is included and properly computed from captured values
+ string y = "Hello world";
+ Assert.Equal("container.Model[Hello].Length", ExpressionHelper.GetExpressionText(Lambda<object, int>(m => container.Model[y.Substring(0, 5)].Length)));
+
+ // Back to back indexer is included
+ Assert.Equal("container.Model[1024][2]", ExpressionHelper.GetExpressionText(Lambda<object, char>(m => container.Model[x * 512][x])));
+
+ // Multi-parameter indexer is excluded
+ Assert.Equal("Length", ExpressionHelper.GetExpressionText(Lambda<object, int>(m => container.Model[42, "Hello World"].Length)));
+
+ // Single array indexer is included
+ Assert.Equal("container.Model.Array[1024]", ExpressionHelper.GetExpressionText(Lambda<object, int>(m => container.Model.Array[x * 512])));
+
+ // Double array indexer is excluded
+ Assert.Equal("", ExpressionHelper.GetExpressionText(Lambda<object, int>(m => container.Model.DoubleArray[1, 2])));
+
+ // Non-indexer method call is excluded
+ Assert.Equal("Length", ExpressionHelper.GetExpressionText(Lambda<object, int>(m => container.Model.Method().Length)));
+
+ // Lambda expression which involves indexer which references lambda parameter throws
+ Assert.Throws<InvalidOperationException>(
+ () => ExpressionHelper.GetExpressionText(Lambda<string, char>(s => s[s.Length - 4])),
+ "The expression compiler was unable to evaluate the indexer expression '(s.Length - 4)' because it references the model parameter 's' which is unavailable.");
+ }
+
+ // Helpers
+
+ private LambdaExpression Lambda<T1, T2>(Expression<Func<T1, T2>> expression)
+ {
+ return expression;
+ }
+
+ class DummyContactModel
+ {
+ public string FirstName { get; set; }
+
+ public string this[int index]
+ {
+ get { return index.ToString(); }
+ }
+
+ public string this[string index]
+ {
+ get { return index; }
+ }
+
+ public string this[int index, string index2]
+ {
+ get { return index2; }
+ }
+
+ public int[] Array { get; set; }
+
+ public int[,] DoubleArray { get; set; }
+
+ public string Method()
+ {
+ return String.Empty;
+ }
+ }
+
+ class DummyModelContainer
+ {
+ public DummyContactModel Model { get; set; }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/FieldValidationMetadataTest.cs b/test/System.Web.Mvc.Test/Test/FieldValidationMetadataTest.cs
new file mode 100644
index 00000000..bd79d04f
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/FieldValidationMetadataTest.cs
@@ -0,0 +1,33 @@
+using System.Collections.Generic;
+using System.Web.TestUtil;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class FieldValidationMetadataTest
+ {
+ [Fact]
+ public void FieldNameProperty()
+ {
+ // Arrange
+ FieldValidationMetadata metadata = new FieldValidationMetadata();
+
+ // Act & assert
+ MemberHelper.TestStringProperty(metadata, "FieldName", String.Empty);
+ }
+
+ [Fact]
+ public void ValidationRulesProperty()
+ {
+ // Arrange
+ FieldValidationMetadata metadata = new FieldValidationMetadata();
+
+ // Act
+ ICollection<ModelClientValidationRule> validationRules = metadata.ValidationRules;
+
+ // Assert
+ Assert.NotNull(validationRules);
+ Assert.Empty(validationRules);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/FileContentResultTest.cs b/test/System.Web.Mvc.Test/Test/FileContentResultTest.cs
new file mode 100644
index 00000000..74f9f7a0
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/FileContentResultTest.cs
@@ -0,0 +1,65 @@
+using System.IO;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class FileContentResultTest
+ {
+ [Fact]
+ public void ConstructorSetsFileContentsProperty()
+ {
+ // Arrange
+ byte[] fileContents = new byte[0];
+
+ // Act
+ FileContentResult result = new FileContentResult(fileContents, "contentType");
+
+ // Assert
+ Assert.Same(fileContents, result.FileContents);
+ }
+
+ [Fact]
+ public void ConstructorThrowsIfFileContentsIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new FileContentResult(null, "contentType"); }, "fileContents");
+ }
+
+ [Fact]
+ public void WriteFileCopiesBufferToOutputStream()
+ {
+ // Arrange
+ byte[] buffer = new byte[] { 1, 2, 3, 4, 5 };
+
+ Mock<Stream> mockOutputStream = new Mock<Stream>();
+ mockOutputStream.Setup(s => s.Write(buffer, 0, buffer.Length)).Verifiable();
+ Mock<HttpResponseBase> mockResponse = new Mock<HttpResponseBase>();
+ mockResponse.Setup(r => r.OutputStream).Returns(mockOutputStream.Object);
+
+ FileContentResultHelper helper = new FileContentResultHelper(buffer, "application/octet-stream");
+
+ // Act
+ helper.PublicWriteFile(mockResponse.Object);
+
+ // Assert
+ mockOutputStream.Verify();
+ mockResponse.Verify();
+ }
+
+ private class FileContentResultHelper : FileContentResult
+ {
+ public FileContentResultHelper(byte[] fileContents, string contentType)
+ : base(fileContents, contentType)
+ {
+ }
+
+ public void PublicWriteFile(HttpResponseBase response)
+ {
+ WriteFile(response);
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/FilePathResultTest.cs b/test/System.Web.Mvc.Test/Test/FilePathResultTest.cs
new file mode 100644
index 00000000..e7fb4f01
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/FilePathResultTest.cs
@@ -0,0 +1,64 @@
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class FilePathResultTest
+ {
+ [Fact]
+ public void ConstructorSetsFileNameProperty()
+ {
+ // Act
+ FilePathResult result = new FilePathResult("someFile", "contentType");
+
+ // Assert
+ Assert.Equal("someFile", result.FileName);
+ }
+
+ [Fact]
+ public void ConstructorThrowsIfFileNameIsEmpty()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { new FilePathResult(String.Empty, "contentType"); }, "fileName");
+ }
+
+ [Fact]
+ public void ConstructorThrowsIfFileNameIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { new FilePathResult(null, "contentType"); }, "fileName");
+ }
+
+ [Fact]
+ public void WriteFileTransmitsFileToOutputStream()
+ {
+ // Arrange
+ Mock<HttpResponseBase> mockResponse = new Mock<HttpResponseBase>();
+ mockResponse.Setup(r => r.TransmitFile("someFile")).Verifiable();
+
+ FilePathResultHelper helper = new FilePathResultHelper("someFile", "application/octet-stream");
+
+ // Act
+ helper.PublicWriteFile(mockResponse.Object);
+
+ // Assert
+ mockResponse.Verify();
+ }
+
+ private class FilePathResultHelper : FilePathResult
+ {
+ public FilePathResultHelper(string fileName, string contentType)
+ : base(fileName, contentType)
+ {
+ }
+
+ public void PublicWriteFile(HttpResponseBase response)
+ {
+ WriteFile(response);
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/FileResultTest.cs b/test/System.Web.Mvc.Test/Test/FileResultTest.cs
new file mode 100644
index 00000000..69cd8f12
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/FileResultTest.cs
@@ -0,0 +1,160 @@
+using System.Net.Mime;
+using System.Web.TestUtil;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class FileResultTest
+ {
+ [Fact]
+ public void ConstructorSetsContentTypeProperty()
+ {
+ // Act
+ FileResult result = new EmptyFileResult("someContentType");
+
+ // Assert
+ Assert.Equal("someContentType", result.ContentType);
+ }
+
+ [Fact]
+ public void ConstructorThrowsIfContentTypeIsEmpty()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { new EmptyFileResult(String.Empty); }, "contentType");
+ }
+
+ [Fact]
+ public void ConstructorThrowsIfContentTypeIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { new EmptyFileResult(null); }, "contentType");
+ }
+
+ [Fact]
+ public void ContentDispositionHeaderIsEncodedCorrectly()
+ {
+ // See comment in FileResult.cs detailing how the FileDownloadName should be encoded.
+
+ // Arrange
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>(MockBehavior.Strict);
+ mockControllerContext.SetupSet(c => c.HttpContext.Response.ContentType = "application/my-type").Verifiable();
+ mockControllerContext.Setup(c => c.HttpContext.Response.AddHeader("Content-Disposition", @"attachment; filename=""some\\file""")).Verifiable();
+
+ EmptyFileResult result = new EmptyFileResult("application/my-type")
+ {
+ FileDownloadName = @"some\file"
+ };
+
+ // Act
+ result.ExecuteResult(mockControllerContext.Object);
+
+ // Assert
+ Assert.True(result.WasWriteFileCalled);
+ mockControllerContext.Verify();
+ }
+
+ [Fact(Skip="Pending fix to DevDiv 356181 -- 4.5 changed ContentDisposition.ToString()")]
+ public void ContentDispositionHeaderIsEncodedCorrectlyForUnicodeCharacters()
+ {
+ // Arrange
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>(MockBehavior.Strict);
+ mockControllerContext.SetupSet(c => c.HttpContext.Response.ContentType = "application/my-type").Verifiable();
+ mockControllerContext.Setup(c => c.HttpContext.Response.AddHeader("Content-Disposition", @"attachment; filename*=UTF-8''ABCXYZabcxyz012789!%40%23$%25%5E&%2A%28%29-%3D_+.:~%CE%94")).Verifiable();
+
+ EmptyFileResult result = new EmptyFileResult("application/my-type")
+ {
+ FileDownloadName = "ABCXYZabcxyz012789!@#$%^&*()-=_+.:~Δ"
+ };
+
+ // Act
+ result.ExecuteResult(mockControllerContext.Object);
+
+ // Assert
+ Assert.True(result.WasWriteFileCalled);
+ mockControllerContext.Verify();
+ }
+
+ [Fact]
+ public void ExecuteResultDoesNotSetContentDispositionIfNotSpecified()
+ {
+ // Arrange
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.SetupSet(c => c.HttpContext.Response.ContentType = "application/my-type").Verifiable();
+
+ EmptyFileResult result = new EmptyFileResult("application/my-type");
+
+ // Act
+ result.ExecuteResult(mockControllerContext.Object);
+
+ // Assert
+ Assert.True(result.WasWriteFileCalled);
+ mockControllerContext.Verify();
+ }
+
+ [Fact]
+ public void ExecuteResultSetsContentDispositionIfSpecified()
+ {
+ // Arrange
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>(MockBehavior.Strict);
+ mockControllerContext.SetupSet(c => c.HttpContext.Response.ContentType = "application/my-type").Verifiable();
+ mockControllerContext.Setup(c => c.HttpContext.Response.AddHeader("Content-Disposition", "attachment; filename=filename.ext")).Verifiable();
+
+ EmptyFileResult result = new EmptyFileResult("application/my-type")
+ {
+ FileDownloadName = "filename.ext"
+ };
+
+ // Act
+ result.ExecuteResult(mockControllerContext.Object);
+
+ // Assert
+ Assert.True(result.WasWriteFileCalled);
+ mockControllerContext.Verify();
+ }
+
+ [Fact]
+ public void ExecuteResultThrowsIfContextIsNull()
+ {
+ // Arrange
+ FileResult result = new EmptyFileResult();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { result.ExecuteResult(null); }, "context");
+ }
+
+ [Fact]
+ public void FileDownloadNameProperty()
+ {
+ // Arrange
+ FileResult result = new EmptyFileResult();
+
+ // Act & assert
+ MemberHelper.TestStringProperty(result, "FileDownloadName", String.Empty);
+ }
+
+ private class EmptyFileResult : FileResult
+ {
+ public bool WasWriteFileCalled;
+
+ public EmptyFileResult()
+ : this(MediaTypeNames.Application.Octet)
+ {
+ }
+
+ public EmptyFileResult(string contentType)
+ : base(contentType)
+ {
+ }
+
+ protected override void WriteFile(HttpResponseBase response)
+ {
+ WasWriteFileCalled = true;
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/FileStreamResultTest.cs b/test/System.Web.Mvc.Test/Test/FileStreamResultTest.cs
new file mode 100644
index 00000000..2f560d42
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/FileStreamResultTest.cs
@@ -0,0 +1,77 @@
+using System.IO;
+using System.Linq;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class FileStreamResultTest
+ {
+ private static readonly Random _random = new Random();
+
+ [Fact]
+ public void ConstructorSetsFileStreamProperty()
+ {
+ // Arrange
+ Stream stream = new MemoryStream();
+
+ // Act
+ FileStreamResult result = new FileStreamResult(stream, "contentType");
+
+ // Assert
+ Assert.Same(stream, result.FileStream);
+ }
+
+ [Fact]
+ public void ConstructorThrowsIfFileStreamIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new FileStreamResult(null, "contentType"); }, "fileStream");
+ }
+
+ [Fact]
+ public void WriteFileCopiesProvidedStreamToOutputStream()
+ {
+ // Arrange
+ int byteLen = 0x1234;
+ byte[] originalBytes = GetRandomByteArray(byteLen);
+ MemoryStream originalStream = new MemoryStream(originalBytes);
+ MemoryStream outStream = new MemoryStream();
+
+ Mock<HttpResponseBase> mockResponse = new Mock<HttpResponseBase>();
+ mockResponse.Setup(r => r.OutputStream).Returns(outStream);
+
+ FileStreamResultHelper helper = new FileStreamResultHelper(originalStream, "application/octet-stream");
+
+ // Act
+ helper.PublicWriteFile(mockResponse.Object);
+
+ // Assert
+ byte[] outBytes = outStream.ToArray();
+ Assert.True(originalBytes.SequenceEqual(outBytes));
+ mockResponse.Verify();
+ }
+
+ private static byte[] GetRandomByteArray(int length)
+ {
+ byte[] bytes = new byte[length];
+ _random.NextBytes(bytes);
+ return bytes;
+ }
+
+ private class FileStreamResultHelper : FileStreamResult
+ {
+ public FileStreamResultHelper(Stream fileStream, string contentType)
+ : base(fileStream, contentType)
+ {
+ }
+
+ public void PublicWriteFile(HttpResponseBase response)
+ {
+ WriteFile(response);
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/FilterAttributeFilterProviderTest.cs b/test/System.Web.Mvc.Test/Test/FilterAttributeFilterProviderTest.cs
new file mode 100644
index 00000000..b482d128
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/FilterAttributeFilterProviderTest.cs
@@ -0,0 +1,155 @@
+using System.Collections.Generic;
+using System.Linq;
+using Moq;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class FilterAttributeFilterProviderTest
+ {
+ [Fact]
+ public void GetFilters_WithNullController_ReturnsEmptyList()
+ {
+ // Arrange
+ var context = new ControllerContext();
+ var descriptor = new Mock<ActionDescriptor>().Object;
+ var provider = new FilterAttributeFilterProvider();
+
+ // Act
+ IEnumerable<Filter> result = provider.GetFilters(context, descriptor);
+
+ // Assert
+ Assert.Empty(result);
+ }
+
+ [MyFilter(Order = 2112)]
+ private class ControllerWithTypeAttribute : Controller
+ {
+ }
+
+ [Fact]
+ public void GetFilters_IncludesAttributesOnControllerType()
+ {
+ // Arrange
+ var context = new ControllerContext { Controller = new ControllerWithTypeAttribute() };
+ var controllerDescriptorMock = new Mock<ControllerDescriptor>();
+ controllerDescriptorMock.Setup(cd => cd.GetFilterAttributes(It.IsAny<bool>()))
+ .Returns(new FilterAttribute[] { new MyFilterAttribute { Order = 2112 } });
+ var actionDescriptorMock = new Mock<ActionDescriptor>();
+ actionDescriptorMock.Setup(ad => ad.ControllerDescriptor).Returns(controllerDescriptorMock.Object);
+ var provider = new FilterAttributeFilterProvider();
+
+ // Act
+ Filter filter = provider.GetFilters(context, actionDescriptorMock.Object).Single();
+
+ // Assert
+ MyFilterAttribute attrib = filter.Instance as MyFilterAttribute;
+ Assert.NotNull(attrib);
+ Assert.Equal(FilterScope.Controller, filter.Scope);
+ Assert.Equal(2112, filter.Order);
+ }
+
+ private class ControllerWithActionAttribute : Controller
+ {
+ [MyFilter(Order = 1234)]
+ public ActionResult MyActionMethod()
+ {
+ return null;
+ }
+ }
+
+ [Fact]
+ public void GetFilters_IncludesAttributesOnActionMethod()
+ {
+ // Arrange
+ var context = new ControllerContext { Controller = new ControllerWithActionAttribute() };
+ var controllerDescriptor = new ReflectedControllerDescriptor(context.Controller.GetType());
+ var action = context.Controller.GetType().GetMethod("MyActionMethod");
+ var actionDescriptor = new ReflectedActionDescriptor(action, "MyActionMethod", controllerDescriptor);
+ var provider = new FilterAttributeFilterProvider();
+
+ // Act
+ Filter filter = provider.GetFilters(context, actionDescriptor).Single();
+
+ // Assert
+ MyFilterAttribute attrib = filter.Instance as MyFilterAttribute;
+ Assert.NotNull(attrib);
+ Assert.Equal(FilterScope.Action, filter.Scope);
+ Assert.Equal(1234, filter.Order);
+ }
+
+ private abstract class BaseController : Controller
+ {
+ public ActionResult MyActionMethod()
+ {
+ return null;
+ }
+ }
+
+ [MyFilter]
+ private class DerivedController : BaseController
+ {
+ }
+
+ [Fact]
+ public void GetFilters_IncludesTypeAttributesFromDerivedTypeWhenMethodIsOnBaseClass()
+ { // DDB #208062
+ // Arrange
+ var context = new ControllerContext { Controller = new DerivedController() };
+ var controllerDescriptor = new ReflectedControllerDescriptor(context.Controller.GetType());
+ var action = context.Controller.GetType().GetMethod("MyActionMethod");
+ var actionDescriptor = new ReflectedActionDescriptor(action, "MyActionMethod", controllerDescriptor);
+ var provider = new FilterAttributeFilterProvider();
+
+ // Act
+ IEnumerable<Filter> filters = provider.GetFilters(context, actionDescriptor);
+
+ // Assert
+ Assert.NotNull(filters.Select(f => f.Instance).Cast<MyFilterAttribute>().Single());
+ }
+
+ private class MyFilterAttribute : FilterAttribute
+ {
+ }
+
+ [Fact]
+ public void GetFilters_RetrievesCachedAttributesByDefault()
+ {
+ // Arrange
+ var provider = new FilterAttributeFilterProvider();
+ var context = new ControllerContext { Controller = new DerivedController() };
+ var controllerDescriptorMock = new Mock<ControllerDescriptor>();
+ controllerDescriptorMock.Setup(cd => cd.GetFilterAttributes(true)).Returns(Enumerable.Empty<FilterAttribute>()).Verifiable();
+ var actionDescriptorMock = new Mock<ActionDescriptor>();
+ actionDescriptorMock.Setup(ad => ad.GetFilterAttributes(true)).Returns(Enumerable.Empty<FilterAttribute>()).Verifiable();
+ actionDescriptorMock.Setup(ad => ad.ControllerDescriptor).Returns(controllerDescriptorMock.Object);
+
+ // Act
+ var result = provider.GetFilters(context, actionDescriptorMock.Object);
+
+ // Assert
+ controllerDescriptorMock.Verify();
+ actionDescriptorMock.Verify();
+ }
+
+ [Fact]
+ public void GetFilters_RetrievesNonCachedAttributesWhenConfiguredNotTo()
+ {
+ // Arrange
+ var provider = new FilterAttributeFilterProvider(false);
+ var context = new ControllerContext { Controller = new DerivedController() };
+ var controllerDescriptorMock = new Mock<ControllerDescriptor>();
+ controllerDescriptorMock.Setup(cd => cd.GetFilterAttributes(false)).Returns(Enumerable.Empty<FilterAttribute>()).Verifiable();
+ var actionDescriptorMock = new Mock<ActionDescriptor>();
+ actionDescriptorMock.Setup(ad => ad.GetFilterAttributes(false)).Returns(Enumerable.Empty<FilterAttribute>()).Verifiable();
+ actionDescriptorMock.Setup(ad => ad.ControllerDescriptor).Returns(controllerDescriptorMock.Object);
+
+ // Act
+ var result = provider.GetFilters(context, actionDescriptorMock.Object);
+
+ // Assert
+ controllerDescriptorMock.Verify();
+ actionDescriptorMock.Verify();
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/FilterInfoTest.cs b/test/System.Web.Mvc.Test/Test/FilterInfoTest.cs
new file mode 100644
index 00000000..d5286d27
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/FilterInfoTest.cs
@@ -0,0 +1,69 @@
+using System.Collections.Generic;
+using System.Linq;
+using Moq;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class FilterInfoTest
+ {
+ [Fact]
+ public void Constructor_Default()
+ {
+ // Arrange + Act
+ FilterInfo filterInfo = new FilterInfo();
+
+ // Assert
+ Assert.Empty(filterInfo.ActionFilters);
+ Assert.Empty(filterInfo.AuthorizationFilters);
+ Assert.Empty(filterInfo.ExceptionFilters);
+ Assert.Empty(filterInfo.ResultFilters);
+ }
+
+ [Fact]
+ public void Constructor_PopulatesFilterCollections()
+ {
+ // Arrange
+ Mock<IActionFilter> actionFilterMock = new Mock<IActionFilter>();
+ Mock<IAuthorizationFilter> authorizationFilterMock = new Mock<IAuthorizationFilter>();
+ Mock<IExceptionFilter> exceptionFilterMock = new Mock<IExceptionFilter>();
+ Mock<IResultFilter> resultFilterMock = new Mock<IResultFilter>();
+
+ List<Filter> filters = new List<Filter>()
+ {
+ CreateFilter(actionFilterMock),
+ CreateFilter(authorizationFilterMock),
+ CreateFilter(exceptionFilterMock),
+ CreateFilter(resultFilterMock),
+ };
+
+ // Act
+ FilterInfo filterInfo = new FilterInfo(filters);
+
+ // Assert
+ Assert.Equal(actionFilterMock.Object, filterInfo.ActionFilters.SingleOrDefault());
+ Assert.Equal(authorizationFilterMock.Object, filterInfo.AuthorizationFilters.SingleOrDefault());
+ Assert.Equal(exceptionFilterMock.Object, filterInfo.ExceptionFilters.SingleOrDefault());
+ Assert.Equal(resultFilterMock.Object, filterInfo.ResultFilters.SingleOrDefault());
+ }
+
+ [Fact]
+ public void Constructor_IteratesOverFiltersOnlyOnce()
+ {
+ // Arrange
+ var filtersMock = new Mock<IEnumerable<Filter>>();
+ filtersMock.Setup(f => f.GetEnumerator()).Returns(new List<Filter>().GetEnumerator());
+
+ // Act
+ FilterInfo filterInfo = new FilterInfo(filtersMock.Object);
+
+ // Assert
+ filtersMock.Verify(f => f.GetEnumerator(), Times.Once());
+ }
+
+ private static Filter CreateFilter(Mock instanceMock)
+ {
+ return new Filter(instanceMock.Object, FilterScope.Global, null);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/FilterProviderCollectionTest.cs b/test/System.Web.Mvc.Test/Test/FilterProviderCollectionTest.cs
new file mode 100644
index 00000000..46b8d117
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/FilterProviderCollectionTest.cs
@@ -0,0 +1,202 @@
+using System.Collections.Generic;
+using System.Linq;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class FilterProviderCollectionTest
+ {
+ [Fact]
+ public void GuardClauses()
+ {
+ // Arrange
+ var context = new ControllerContext();
+ var descriptor = new Mock<ActionDescriptor>().Object;
+ var collection = new FilterProviderCollection();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => collection.GetFilters(null, descriptor),
+ "controllerContext"
+ );
+ Assert.ThrowsArgumentNull(
+ () => collection.GetFilters(context, null),
+ "actionDescriptor"
+ );
+ }
+
+ [Fact]
+ public void GetFiltersUsesRegisteredProviders()
+ {
+ // Arrange
+ var context = new ControllerContext();
+ var descriptor = new Mock<ActionDescriptor>().Object;
+ var filter = new Filter(new Object(), FilterScope.Action, null);
+ var provider = new Mock<IFilterProvider>(MockBehavior.Strict);
+ var collection = new FilterProviderCollection(new[] { provider.Object });
+ provider.Setup(p => p.GetFilters(context, descriptor)).Returns(new[] { filter });
+
+ // Act
+ IEnumerable<Filter> result = collection.GetFilters(context, descriptor);
+
+ // Assert
+ Assert.Same(filter, result.Single());
+ }
+
+ [Fact]
+ public void GetFiltersDelegatesToResolver()
+ {
+ // Arrange
+ var context = new ControllerContext();
+ var descriptor = new Mock<ActionDescriptor>().Object;
+ var filter = new Filter(new Object(), FilterScope.Action, null);
+ var provider = new Mock<IFilterProvider>(MockBehavior.Strict);
+ var resolver = new Resolver<IEnumerable<IFilterProvider>> { Current = new[] { provider.Object } };
+ var collection = new FilterProviderCollection(resolver);
+
+ provider.Setup(p => p.GetFilters(context, descriptor)).Returns(new[] { filter });
+
+ // Act
+ IEnumerable<Filter> result = collection.GetFilters(context, descriptor);
+
+ // Assert
+ Assert.Same(filter, result.Single());
+ }
+
+ [Fact]
+ public void GetFiltersSortsFiltersByOrderFirstThenScope()
+ {
+ // Arrange
+ var context = new ControllerContext();
+ var descriptor = new Mock<ActionDescriptor>().Object;
+ var actionFilter = new Filter(new Object(), FilterScope.Action, null);
+ var controllerFilter = new Filter(new Object(), FilterScope.Controller, null);
+ var globalFilter = new Filter(new Object(), FilterScope.Global, null);
+ var earlyActionFilter = new Filter(new Object(), FilterScope.Action, -100);
+ var lateGlobalFilter = new Filter(new Object(), FilterScope.Global, 100);
+ var provider = new Mock<IFilterProvider>(MockBehavior.Strict);
+ var collection = new FilterProviderCollection(new[] { provider.Object });
+ provider.Setup(p => p.GetFilters(context, descriptor))
+ .Returns(new[] { actionFilter, controllerFilter, globalFilter, earlyActionFilter, lateGlobalFilter });
+
+ // Act
+ Filter[] result = collection.GetFilters(context, descriptor).ToArray();
+
+ // Assert
+ Assert.Equal(5, result.Length);
+ Assert.Same(earlyActionFilter, result[0]);
+ Assert.Same(globalFilter, result[1]);
+ Assert.Same(controllerFilter, result[2]);
+ Assert.Same(actionFilter, result[3]);
+ Assert.Same(lateGlobalFilter, result[4]);
+ }
+
+ [AttributeUsage(AttributeTargets.All, AllowMultiple = false)]
+ private class AllowMultipleFalseAttribute : FilterAttribute
+ {
+ }
+
+ [Fact]
+ public void GetFiltersIncludesLastFilterOnlyWithAttributeUsageAllowMultipleFalse()
+ { // DDB #222988
+ // Arrange
+ var context = new ControllerContext();
+ var descriptor = new Mock<ActionDescriptor>().Object;
+ var globalFilter = new Filter(new AllowMultipleFalseAttribute(), FilterScope.Global, null);
+ var controllerFilter = new Filter(new AllowMultipleFalseAttribute(), FilterScope.Controller, null);
+ var actionFilter = new Filter(new AllowMultipleFalseAttribute(), FilterScope.Action, null);
+ var provider = new Mock<IFilterProvider>(MockBehavior.Strict);
+ var collection = new FilterProviderCollection(new[] { provider.Object });
+ provider.Setup(p => p.GetFilters(context, descriptor))
+ .Returns(new[] { controllerFilter, actionFilter, globalFilter });
+
+ // Act
+ IEnumerable<Filter> result = collection.GetFilters(context, descriptor);
+
+ // Assert
+ Assert.Same(actionFilter, result.Single());
+ }
+
+ [AttributeUsage(AttributeTargets.All, AllowMultiple = true)]
+ private class AllowMultipleTrueAttribute : FilterAttribute
+ {
+ }
+
+ [Fact]
+ public void GetFiltersIncludesAllFiltersWithAttributeUsageAllowMultipleTrue()
+ { // DDB #222988
+ // Arrange
+ var context = new ControllerContext();
+ var descriptor = new Mock<ActionDescriptor>().Object;
+ var globalFilter = new Filter(new AllowMultipleTrueAttribute(), FilterScope.Global, null);
+ var controllerFilter = new Filter(new AllowMultipleTrueAttribute(), FilterScope.Controller, null);
+ var actionFilter = new Filter(new AllowMultipleTrueAttribute(), FilterScope.Action, null);
+ var provider = new Mock<IFilterProvider>(MockBehavior.Strict);
+ var collection = new FilterProviderCollection(new[] { provider.Object });
+ provider.Setup(p => p.GetFilters(context, descriptor))
+ .Returns(new[] { controllerFilter, actionFilter, globalFilter });
+
+ // Act
+ List<Filter> result = collection.GetFilters(context, descriptor).ToList();
+
+ // Assert
+ Assert.Same(globalFilter, result[0]);
+ Assert.Same(controllerFilter, result[1]);
+ Assert.Same(actionFilter, result[2]);
+ }
+
+ private class AllowMultipleCustomFilter : MvcFilter
+ {
+ public AllowMultipleCustomFilter(bool allowMultiple)
+ : base(allowMultiple, -1)
+ {
+ }
+ }
+
+ [Fact]
+ public void GetFiltersIncludesLastFilterOnlyWithCustomFilterAllowMultipleFalse()
+ { // DDB #222988
+ // Arrange
+ var context = new ControllerContext();
+ var descriptor = new Mock<ActionDescriptor>().Object;
+ var globalFilter = new Filter(new AllowMultipleCustomFilter(false), FilterScope.Global, null);
+ var controllerFilter = new Filter(new AllowMultipleCustomFilter(false), FilterScope.Controller, null);
+ var actionFilter = new Filter(new AllowMultipleCustomFilter(false), FilterScope.Action, null);
+ var provider = new Mock<IFilterProvider>(MockBehavior.Strict);
+ var collection = new FilterProviderCollection(new[] { provider.Object });
+ provider.Setup(p => p.GetFilters(context, descriptor))
+ .Returns(new[] { controllerFilter, actionFilter, globalFilter });
+
+ // Act
+ IEnumerable<Filter> result = collection.GetFilters(context, descriptor);
+
+ // Assert
+ Assert.Same(actionFilter, result.Single());
+ }
+
+ [Fact]
+ public void GetFiltersIncludesAllFiltersWithCustomFilterAllowMultipleTrue()
+ { // DDB #222988
+ // Arrange
+ var context = new ControllerContext();
+ var descriptor = new Mock<ActionDescriptor>().Object;
+ var globalFilter = new Filter(new AllowMultipleCustomFilter(true), FilterScope.Global, null);
+ var controllerFilter = new Filter(new AllowMultipleCustomFilter(true), FilterScope.Controller, null);
+ var actionFilter = new Filter(new AllowMultipleCustomFilter(true), FilterScope.Action, null);
+ var provider = new Mock<IFilterProvider>(MockBehavior.Strict);
+ var collection = new FilterProviderCollection(new[] { provider.Object });
+ provider.Setup(p => p.GetFilters(context, descriptor))
+ .Returns(new[] { controllerFilter, actionFilter, globalFilter });
+
+ // Act
+ List<Filter> result = collection.GetFilters(context, descriptor).ToList();
+
+ // Assert
+ Assert.Same(globalFilter, result[0]);
+ Assert.Same(controllerFilter, result[1]);
+ Assert.Same(actionFilter, result[2]);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/FilterProvidersTest.cs b/test/System.Web.Mvc.Test/Test/FilterProvidersTest.cs
new file mode 100644
index 00000000..cfff049a
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/FilterProvidersTest.cs
@@ -0,0 +1,17 @@
+using System.Linq;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class FilterProvidersTest
+ {
+ [Fact]
+ public void DefaultFilterProviders()
+ {
+ // Assert
+ Assert.NotNull(FilterProviders.Providers.Single(fp => fp is GlobalFilterCollection));
+ Assert.NotNull(FilterProviders.Providers.Single(fp => fp is FilterAttributeFilterProvider));
+ Assert.NotNull(FilterProviders.Providers.Single(fp => fp is ControllerInstanceFilterProvider));
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/FilterTest.cs b/test/System.Web.Mvc.Test/Test/FilterTest.cs
new file mode 100644
index 00000000..50c5a60e
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/FilterTest.cs
@@ -0,0 +1,66 @@
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class FilterTest
+ {
+ [Fact]
+ public void GuardClause()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => new Filter(null, FilterScope.Action, null),
+ "instance"
+ );
+ }
+
+ [Fact]
+ public void FilterDoesNotImplementIOrderedFilter()
+ {
+ // Arrange
+ var filterInstance = new object();
+
+ // Act
+ var filter = new Filter(filterInstance, FilterScope.Action, null);
+
+ // Assert
+ Assert.Same(filterInstance, filter.Instance);
+ Assert.Equal(FilterScope.Action, filter.Scope);
+ Assert.Equal(Filter.DefaultOrder, filter.Order);
+ }
+
+ [Fact]
+ public void FilterImplementsIOrderedFilter()
+ {
+ // Arrange
+ var filterInstance = new Mock<IMvcFilter>();
+ filterInstance.SetupGet(f => f.Order).Returns(42);
+
+ // Act
+ var filter = new Filter(filterInstance.Object, FilterScope.Controller, null);
+
+ // Assert
+ Assert.Same(filterInstance.Object, filter.Instance);
+ Assert.Equal(FilterScope.Controller, filter.Scope);
+ Assert.Equal(42, filter.Order);
+ }
+
+ [Fact]
+ public void ExplicitOrderOverridesIOrderedFilter()
+ {
+ // Arrange
+ var filterInstance = new Mock<IMvcFilter>();
+ filterInstance.SetupGet(f => f.Order).Returns(42);
+
+ // Act
+ var filter = new Filter(filterInstance.Object, FilterScope.Controller, 2112);
+
+ // Assert
+ Assert.Same(filterInstance.Object, filter.Instance);
+ Assert.Equal(FilterScope.Controller, filter.Scope);
+ Assert.Equal(2112, filter.Order);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/FormCollectionTest.cs b/test/System.Web.Mvc.Test/Test/FormCollectionTest.cs
new file mode 100644
index 00000000..5c8a45ff
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/FormCollectionTest.cs
@@ -0,0 +1,180 @@
+using System.Collections.Specialized;
+using System.Globalization;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class FormCollectionTest
+ {
+ [Fact]
+ public void ConstructorCopiesProvidedCollection()
+ {
+ // Arrange
+ NameValueCollection nvc = new NameValueCollection()
+ {
+ { "foo", "fooValue" },
+ { "bar", "barValue" }
+ };
+
+ // Act
+ FormCollection formCollection = new FormCollection(nvc);
+
+ // Assert
+ Assert.Equal(2, formCollection.Count);
+ Assert.Equal("fooValue", formCollection["foo"]);
+ Assert.Equal("barValue", formCollection["bar"]);
+ }
+
+ [Fact]
+ public void ConstructorThrowsIfCollectionIsNull()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { new FormCollection(null); }, "collection");
+ }
+
+ [Fact]
+ public void ConstructorUsesValidatedValuesWhenControllerIsNull()
+ {
+ // Arrange
+ var values = new NameValueCollection()
+ {
+ { "foo", "fooValue" },
+ { "bar", "barValue" }
+ };
+
+ // Act
+ var result = new FormCollection(controller: null,
+ validatedValuesThunk: () => values,
+ unvalidatedValuesThunk: () => { throw new NotImplementedException(); });
+
+ // Assert
+ Assert.Equal(2, result.Count);
+ Assert.Equal("fooValue", result["foo"]);
+ Assert.Equal("barValue", result["bar"]);
+ }
+
+ [Fact]
+ public void ConstructorUsesValidatedValuesWhenControllerValidateRequestIsTrue()
+ {
+ // Arrange
+ var values = new NameValueCollection()
+ {
+ { "foo", "fooValue" },
+ { "bar", "barValue" }
+ };
+ var controller = new Mock<ControllerBase>().Object;
+ controller.ValidateRequest = true;
+
+ // Act
+ var result = new FormCollection(controller,
+ validatedValuesThunk: () => values,
+ unvalidatedValuesThunk: () => { throw new NotImplementedException(); });
+
+ // Assert
+ Assert.Equal(2, result.Count);
+ Assert.Equal("fooValue", result["foo"]);
+ Assert.Equal("barValue", result["bar"]);
+ }
+
+ [Fact]
+ public void ConstructorUsesUnvalidatedValuesWhenControllerValidateRequestIsFalse()
+ {
+ // Arrange
+ var values = new NameValueCollection()
+ {
+ { "foo", "fooValue" },
+ { "bar", "barValue" }
+ };
+ var controller = new Mock<ControllerBase>().Object;
+ controller.ValidateRequest = false;
+
+ // Act
+ var result = new FormCollection(controller,
+ validatedValuesThunk: () => { throw new NotImplementedException(); },
+ unvalidatedValuesThunk: () => values);
+
+ // Assert
+ Assert.Equal(2, result.Count);
+ Assert.Equal("fooValue", result["foo"]);
+ Assert.Equal("barValue", result["bar"]);
+ }
+
+ [Fact]
+ public void CustomBinderBindModelReturnsFormCollection()
+ {
+ // Arrange
+ NameValueCollection nvc = new NameValueCollection() { { "foo", "fooValue" }, { "bar", "barValue" } };
+ IModelBinder binder = ModelBinders.Binders.GetBinder(typeof(FormCollection));
+
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.Setup(c => c.HttpContext.Request.Form).Returns(nvc);
+
+ // Act
+ FormCollection formCollection = (FormCollection)binder.BindModel(mockControllerContext.Object, null);
+
+ // Assert
+ Assert.NotNull(formCollection);
+ Assert.Equal(2, formCollection.Count);
+ Assert.Equal("fooValue", nvc["foo"]);
+ Assert.Equal("barValue", nvc["bar"]);
+ }
+
+ [Fact]
+ public void CustomBinderBindModelThrowsIfControllerContextIsNull()
+ {
+ // Arrange
+ IModelBinder binder = ModelBinders.Binders.GetBinder(typeof(FormCollection));
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { binder.BindModel(null, null); }, "controllerContext");
+ }
+
+ [Fact]
+ public void GetValue_ThrowsIfNameIsNull()
+ {
+ // Arrange
+ FormCollection formCollection = new FormCollection();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { formCollection.GetValue(null); }, "name");
+ }
+
+ [Fact]
+ public void GetValue_KeyDoesNotExist_ReturnsNull()
+ {
+ // Arrange
+ FormCollection formCollection = new FormCollection();
+
+ // Act
+ ValueProviderResult vpResult = formCollection.GetValue("");
+
+ // Assert
+ Assert.Null(vpResult);
+ }
+
+ [Fact]
+ public void GetValue_KeyExists_ReturnsResult()
+ {
+ // Arrange
+ FormCollection formCollection = new FormCollection()
+ {
+ { "foo", "1" },
+ { "foo", "2" }
+ };
+
+ // Act
+ ValueProviderResult vpResult = formCollection.GetValue("foo");
+
+ // Assert
+ Assert.NotNull(vpResult);
+ Assert.Equal(new[] { "1", "2" }, (string[])vpResult.RawValue);
+ Assert.Equal("1,2", vpResult.AttemptedValue);
+ Assert.Equal(CultureInfo.CurrentCulture, vpResult.Culture);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/FormContextTest.cs b/test/System.Web.Mvc.Test/Test/FormContextTest.cs
new file mode 100644
index 00000000..4551d967
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/FormContextTest.cs
@@ -0,0 +1,172 @@
+using System.Collections.Generic;
+using System.Web.TestUtil;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class FormContextTest
+ {
+ [Fact]
+ public void FieldValidatorsProperty()
+ {
+ // Arrange
+ FormContext context = new FormContext();
+
+ // Act
+ IDictionary<String, FieldValidationMetadata> fieldValidators = context.FieldValidators;
+
+ // Assert
+ Assert.NotNull(fieldValidators);
+ Assert.Empty(fieldValidators);
+ }
+
+ [Fact]
+ public void ReplaceValidationSummaryProperty()
+ {
+ // Arrange
+ FormContext context = new FormContext();
+
+ // Act & Assert
+ MemberHelper.TestBooleanProperty(context, "ReplaceValidationSummary", false, false);
+ }
+
+ [Fact]
+ public void GetJsonValidationMetadata_NoValidationSummary()
+ {
+ // Arrange
+ FormContext context = new FormContext() { FormId = "theFormId" };
+
+ ModelClientValidationRule rule = new ModelClientValidationRule() { ValidationType = "ValidationType1", ErrorMessage = "Error Message" };
+ rule.ValidationParameters["theParam"] = new { FirstName = "John", LastName = "Doe", Age = 32 };
+ FieldValidationMetadata metadata = new FieldValidationMetadata() { FieldName = "theFieldName", ValidationMessageId = "theFieldName_ValidationMessage" };
+ metadata.ValidationRules.Add(rule);
+ context.FieldValidators["theFieldName"] = metadata;
+
+ // Act
+ string jsonMetadata = context.GetJsonValidationMetadata();
+
+ // Assert
+ string expected = @"{""Fields"":[{""FieldName"":""theFieldName"",""ReplaceValidationMessageContents"":false,""ValidationMessageId"":""theFieldName_ValidationMessage"",""ValidationRules"":[{""ErrorMessage"":""Error Message"",""ValidationParameters"":{""theParam"":{""FirstName"":""John"",""LastName"":""Doe"",""Age"":32}},""ValidationType"":""ValidationType1""}]}],""FormId"":""theFormId"",""ReplaceValidationSummary"":false}";
+ Assert.Equal(expected, jsonMetadata);
+ }
+
+ [Fact]
+ public void GetJsonValidationMetadata_ValidationSummary()
+ {
+ // Arrange
+ FormContext context = new FormContext() { FormId = "theFormId", ValidationSummaryId = "validationSummary" };
+
+ ModelClientValidationRule rule = new ModelClientValidationRule() { ValidationType = "ValidationType1", ErrorMessage = "Error Message" };
+ rule.ValidationParameters["theParam"] = new { FirstName = "John", LastName = "Doe", Age = 32 };
+ FieldValidationMetadata metadata = new FieldValidationMetadata() { FieldName = "theFieldName", ValidationMessageId = "theFieldName_ValidationMessage" };
+ metadata.ValidationRules.Add(rule);
+ context.FieldValidators["theFieldName"] = metadata;
+
+ // Act
+ string jsonMetadata = context.GetJsonValidationMetadata();
+
+ // Assert
+ string expected = @"{""Fields"":[{""FieldName"":""theFieldName"",""ReplaceValidationMessageContents"":false,""ValidationMessageId"":""theFieldName_ValidationMessage"",""ValidationRules"":[{""ErrorMessage"":""Error Message"",""ValidationParameters"":{""theParam"":{""FirstName"":""John"",""LastName"":""Doe"",""Age"":32}},""ValidationType"":""ValidationType1""}]}],""FormId"":""theFormId"",""ReplaceValidationSummary"":false,""ValidationSummaryId"":""validationSummary""}";
+ Assert.Equal(expected, jsonMetadata);
+ }
+
+ [Fact]
+ public void GetValidationMetadataForField_Create_CreatesNewMetadataIfNotFound()
+ {
+ // Arrange
+ FormContext context = new FormContext();
+
+ // Act
+ FieldValidationMetadata result = context.GetValidationMetadataForField("fieldName", true /* createIfNotFound */);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal("fieldName", result.FieldName);
+
+ Assert.Single(context.FieldValidators);
+ Assert.Equal(result, context.FieldValidators["fieldName"]);
+ }
+
+ [Fact]
+ public void GetValidationMetadataForField_NoCreate_ReturnsMetadataIfFound()
+ {
+ // Arrange
+ FormContext context = new FormContext();
+ FieldValidationMetadata metadata = new FieldValidationMetadata();
+ context.FieldValidators["fieldName"] = metadata;
+
+ // Act
+ FieldValidationMetadata result = context.GetValidationMetadataForField("fieldName");
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(metadata, result);
+ }
+
+ [Fact]
+ public void GetValidationMetadataForField_NoCreate_ReturnsNullIfNotFound()
+ {
+ // Arrange
+ FormContext context = new FormContext();
+
+ // Act
+ FieldValidationMetadata result = context.GetValidationMetadataForField("fieldName");
+
+ // Assert
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void GetValidationMetadataForFieldThrowsIfFieldNameIsEmpty()
+ {
+ // Arrange
+ FormContext context = new FormContext();
+
+ // Act & assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { context.GetValidationMetadataForField(String.Empty); }, "fieldName");
+ }
+
+ [Fact]
+ public void GetValidationMetadataForFieldThrowsIfFieldNameIsNull()
+ {
+ // Arrange
+ FormContext context = new FormContext();
+
+ // Act & assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { context.GetValidationMetadataForField(null); }, "fieldName");
+ }
+
+ // RenderedField
+
+ [Fact]
+ public void RenderedFieldIsFalseByDefault()
+ {
+ // Arrange
+ var context = new FormContext();
+
+ // Act
+ bool result = context.RenderedField(Guid.NewGuid().ToString());
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void CanSetRenderedFieldToBeTrue()
+ {
+ // Arrange
+ var context = new FormContext();
+ var name = Guid.NewGuid().ToString();
+ context.RenderedField(name, true);
+
+ // Act
+ bool result = context.RenderedField(name);
+
+ // Assert
+ Assert.True(result);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/FormValueProviderFactoryTest.cs b/test/System.Web.Mvc.Test/Test/FormValueProviderFactoryTest.cs
new file mode 100644
index 00000000..1bbe2ca0
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/FormValueProviderFactoryTest.cs
@@ -0,0 +1,77 @@
+using System.Collections.Specialized;
+using System.Globalization;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class FormValueProviderFactoryTest
+ {
+ private static readonly NameValueCollection _backingStore = new NameValueCollection()
+ {
+ { "foo", "fooValue" }
+ };
+
+ private static readonly NameValueCollection _unvalidatedBackingStore = new NameValueCollection()
+ {
+ { "foo", "fooUnvalidated" }
+ };
+
+ [Fact]
+ public void GetValueProvider()
+ {
+ // Arrange
+ Mock<MockableUnvalidatedRequestValues> mockUnvalidatedValues = new Mock<MockableUnvalidatedRequestValues>();
+ FormValueProviderFactory factory = new FormValueProviderFactory(_ => mockUnvalidatedValues.Object);
+
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.Setup(o => o.HttpContext.Request.Form).Returns(_backingStore);
+
+ // Act
+ IValueProvider valueProvider = factory.GetValueProvider(mockControllerContext.Object);
+
+ // Assert
+ Assert.Equal(typeof(FormValueProvider), valueProvider.GetType());
+ ValueProviderResult vpResult = valueProvider.GetValue("foo");
+
+ Assert.NotNull(vpResult);
+ Assert.Equal("fooValue", vpResult.AttemptedValue);
+ Assert.Equal(CultureInfo.CurrentCulture, vpResult.Culture);
+ }
+
+ [Fact]
+ public void GetValueProvider_GetValue_SkipValidation()
+ {
+ // Arrange
+ Mock<MockableUnvalidatedRequestValues> mockUnvalidatedValues = new Mock<MockableUnvalidatedRequestValues>();
+ mockUnvalidatedValues.Setup(o => o.Form).Returns(_unvalidatedBackingStore);
+ FormValueProviderFactory factory = new FormValueProviderFactory(_ => mockUnvalidatedValues.Object);
+
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.Setup(o => o.HttpContext.Request.Form).Returns(_backingStore);
+
+ // Act
+ IUnvalidatedValueProvider valueProvider = (IUnvalidatedValueProvider)factory.GetValueProvider(mockControllerContext.Object);
+
+ // Assert
+ Assert.Equal(typeof(FormValueProvider), valueProvider.GetType());
+ ValueProviderResult vpResult = valueProvider.GetValue("foo", skipValidation: true);
+
+ Assert.NotNull(vpResult);
+ Assert.Equal("fooUnvalidated", vpResult.AttemptedValue);
+ Assert.Equal(CultureInfo.CurrentCulture, vpResult.Culture);
+ }
+
+ [Fact]
+ public void GetValueProvider_ThrowsIfControllerContextIsNull()
+ {
+ // Arrange
+ FormValueProviderFactory factory = new FormValueProviderFactory();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { factory.GetValueProvider(null); }, "controllerContext");
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/GlobalFilterCollectionTest.cs b/test/System.Web.Mvc.Test/Test/GlobalFilterCollectionTest.cs
new file mode 100644
index 00000000..44d2455c
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/GlobalFilterCollectionTest.cs
@@ -0,0 +1,92 @@
+using System.Collections.Generic;
+using Moq;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class GlobalFilterCollectionTest
+ {
+ [Fact]
+ public void AddPlacesFilterInGlobalScope()
+ {
+ // Arrange
+ var filterInstance = new object();
+ var collection = new GlobalFilterCollection();
+
+ // Act
+ collection.Add(filterInstance);
+
+ // Assert
+ Filter filter = Assert.Single(collection);
+ Assert.Same(filterInstance, filter.Instance);
+ Assert.Equal(FilterScope.Global, filter.Scope);
+ Assert.Equal(-1, filter.Order);
+ }
+
+ [Fact]
+ public void AddWithOrderPlacesFilterInGlobalScope()
+ {
+ // Arrange
+ var filterInstance = new object();
+ var collection = new GlobalFilterCollection();
+
+ // Act
+ collection.Add(filterInstance, 42);
+
+ // Assert
+ Filter filter = Assert.Single(collection);
+ Assert.Same(filterInstance, filter.Instance);
+ Assert.Equal(FilterScope.Global, filter.Scope);
+ Assert.Equal(42, filter.Order);
+ }
+
+ [Fact]
+ public void ContainsFindsFilterByInstance()
+ {
+ // Arrange
+ var filterInstance = new object();
+ var collection = new GlobalFilterCollection();
+ collection.Add(filterInstance);
+
+ // Act
+ bool result = collection.Contains(filterInstance);
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void RemoveDeletesFilterByInstance()
+ {
+ // Arrange
+ var filterInstance = new object();
+ var collection = new GlobalFilterCollection();
+ collection.Add(filterInstance);
+
+ // Act
+ collection.Remove(filterInstance);
+
+ // Assert
+ Assert.Empty(collection);
+ }
+
+ [Fact]
+ public void CollectionIsIFilterProviderWhichReturnsAllFilters()
+ {
+ // Arrange
+ var context = new ControllerContext();
+ var descriptor = new Mock<ActionDescriptor>().Object;
+ var filterInstance = new object();
+ var collection = new GlobalFilterCollection();
+ collection.Add(filterInstance);
+ var provider = (IFilterProvider)collection;
+
+ // Act
+ IEnumerable<Filter> result = provider.GetFilters(context, descriptor);
+
+ // Assert
+ Filter filter = Assert.Single(result);
+ Assert.Same(filterInstance, filter.Instance);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/HandleErrorAttributeTest.cs b/test/System.Web.Mvc.Test/Test/HandleErrorAttributeTest.cs
new file mode 100644
index 00000000..59ee5638
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/HandleErrorAttributeTest.cs
@@ -0,0 +1,251 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Web.Routing;
+using System.Web.TestUtil;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class HandleErrorAttributeTest
+ {
+ [Fact]
+ public void HandleErrorAttributeReturnsUniqueTypeIDs()
+ {
+ // Arrange
+ HandleErrorAttribute attr1 = new HandleErrorAttribute();
+ HandleErrorAttribute attr2 = new HandleErrorAttribute();
+
+ // Assert
+ Assert.NotEqual(attr1.TypeId, attr2.TypeId);
+ }
+
+ [HandleError(View = "foo")]
+ [HandleError(View = "bar")]
+ private class ClassWithMultipleHandleErrorAttributes
+ {
+ }
+
+ [Fact]
+ public void CanRetrieveMultipleAuthorizeAttributesFromOneClass()
+ {
+ // Arrange
+ ClassWithMultipleHandleErrorAttributes @class = new ClassWithMultipleHandleErrorAttributes();
+
+ // Act
+ IEnumerable<HandleErrorAttribute> attributes = TypeDescriptor.GetAttributes(@class).OfType<HandleErrorAttribute>();
+
+ // Assert
+ Assert.Equal(2, attributes.Count());
+ Assert.True(attributes.Any(a => a.View == "foo"));
+ Assert.True(attributes.Any(a => a.View == "bar"));
+ }
+
+ [Fact]
+ public void ExceptionTypeProperty()
+ {
+ // Arrange
+ HandleErrorAttribute attr = new HandleErrorAttribute();
+
+ // Act
+ Type origType = attr.ExceptionType;
+ attr.ExceptionType = typeof(SystemException);
+ Type newType = attr.ExceptionType;
+
+ // Assert
+ Assert.Equal(typeof(Exception), origType);
+ Assert.Equal(typeof(SystemException), attr.ExceptionType);
+ }
+
+ [Fact]
+ public void ExceptionTypePropertyWithNonExceptionTypeThrows()
+ {
+ // Arrange
+ HandleErrorAttribute attr = new HandleErrorAttribute();
+
+ // Act & Assert
+ Assert.Throws<ArgumentException>(
+ delegate { attr.ExceptionType = typeof(string); },
+ "The type 'System.String' does not inherit from Exception.");
+ }
+
+ [Fact]
+ public void ExceptionTypePropertyWithNullValueThrows()
+ {
+ // Arrange
+ HandleErrorAttribute attr = new HandleErrorAttribute();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { attr.ExceptionType = null; }, "value");
+ }
+
+ [Fact]
+ public void MasterProperty()
+ {
+ // Arrange
+ HandleErrorAttribute attr = new HandleErrorAttribute();
+
+ // Act & Assert
+ MemberHelper.TestStringProperty(attr, "Master", String.Empty);
+ }
+
+ [Fact]
+ public void OnException()
+ {
+ // Arrange
+ HandleErrorAttribute attr = new HandleErrorAttribute()
+ {
+ View = "SomeView",
+ Master = "SomeMaster",
+ ExceptionType = typeof(ArgumentException)
+ };
+ Exception exception = new ArgumentNullException();
+
+ Mock<HttpContextBase> mockHttpContext = new Mock<HttpContextBase>(MockBehavior.Strict);
+ mockHttpContext.Setup(c => c.IsCustomErrorEnabled).Returns(true);
+ mockHttpContext.Setup(c => c.Session).Returns((HttpSessionStateBase)null);
+ mockHttpContext.Setup(c => c.Response.Clear()).Verifiable();
+ mockHttpContext.SetupSet(c => c.Response.StatusCode = 500).Verifiable();
+ mockHttpContext.SetupSet(c => c.Response.TrySkipIisCustomErrors = true).Verifiable();
+
+ TempDataDictionary tempData = new TempDataDictionary();
+ IViewEngine viewEngine = new Mock<IViewEngine>().Object;
+ Controller controller = new Mock<Controller>().Object;
+ controller.TempData = tempData;
+
+ ExceptionContext context = GetExceptionContext(mockHttpContext.Object, controller, exception);
+
+ // Exception
+ attr.OnException(context);
+
+ // Assert
+ mockHttpContext.Verify();
+ ViewResult viewResult = context.Result as ViewResult;
+ Assert.NotNull(viewResult);
+ Assert.Equal(tempData, viewResult.TempData);
+ Assert.Equal("SomeView", viewResult.ViewName);
+ Assert.Equal("SomeMaster", viewResult.MasterName);
+
+ HandleErrorInfo viewData = viewResult.ViewData.Model as HandleErrorInfo;
+ Assert.NotNull(viewData);
+ Assert.Same(exception, viewData.Exception);
+ Assert.Equal("SomeController", viewData.ControllerName);
+ Assert.Equal("SomeAction", viewData.ActionName);
+ }
+
+ [Fact]
+ public void OnExceptionWithCustomErrorsDisabledDoesNothing()
+ {
+ // Arrange
+ HandleErrorAttribute attr = new HandleErrorAttribute();
+ ActionResult result = new EmptyResult();
+ ExceptionContext context = GetExceptionContext(GetHttpContext(false), null, new Exception());
+ context.Result = result;
+
+ // Exception
+ attr.OnException(context);
+
+ // Assert
+ Assert.Same(result, context.Result);
+ }
+
+ [Fact]
+ public void OnExceptionWithExceptionHandledDoesNothing()
+ {
+ // Arrange
+ HandleErrorAttribute attr = new HandleErrorAttribute();
+ ActionResult result = new EmptyResult();
+ ExceptionContext context = GetExceptionContext(GetHttpContext(), null, new Exception());
+ context.Result = result;
+ context.ExceptionHandled = true;
+
+ // Exception
+ attr.OnException(context);
+
+ // Assert
+ Assert.Same(result, context.Result);
+ }
+
+ [Fact]
+ public void OnExceptionWithNon500ExceptionDoesNothing()
+ {
+ // Arrange
+ HandleErrorAttribute attr = new HandleErrorAttribute();
+ ActionResult result = new EmptyResult();
+ ExceptionContext context = GetExceptionContext(GetHttpContext(), null, new HttpException(404, "Some Exception"));
+ context.Result = result;
+
+ // Exception
+ attr.OnException(context);
+
+ // Assert
+ Assert.Same(result, context.Result);
+ }
+
+ [Fact]
+ public void OnExceptionWithNullFilterContextThrows()
+ {
+ // Arrange
+ HandleErrorAttribute attr = new HandleErrorAttribute();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { attr.OnException(null /* filterContext */); }, "filterContext");
+ }
+
+ [Fact]
+ public void OnExceptionWithWrongExceptionTypeDoesNothing()
+ {
+ // Arrange
+ HandleErrorAttribute attr = new HandleErrorAttribute() { ExceptionType = typeof(ArgumentException) };
+ ActionResult result = new EmptyResult();
+ ExceptionContext context = GetExceptionContext(GetHttpContext(), null, new InvalidCastException());
+ context.Result = result;
+
+ // Exception
+ attr.OnException(context);
+
+ // Assert
+ Assert.Same(result, context.Result);
+ }
+
+ [Fact]
+ public void ViewProperty()
+ {
+ // Arrange
+ HandleErrorAttribute attr = new HandleErrorAttribute();
+
+ // Act & Assert
+ MemberHelper.TestStringProperty(attr, "View", "Error", nullAndEmptyReturnValue: "Error");
+ }
+
+ private static ExceptionContext GetExceptionContext(HttpContextBase httpContext, ControllerBase controller, Exception exception)
+ {
+ RouteData rd = new RouteData();
+ rd.Values["controller"] = "SomeController";
+ rd.Values["action"] = "SomeAction";
+
+ Mock<ExceptionContext> mockExceptionContext = new Mock<ExceptionContext>();
+ mockExceptionContext.Setup(c => c.Controller).Returns(controller);
+ mockExceptionContext.Setup(c => c.Exception).Returns(exception);
+ mockExceptionContext.Setup(c => c.RouteData).Returns(rd);
+ mockExceptionContext.Setup(c => c.HttpContext).Returns(httpContext);
+ return mockExceptionContext.Object;
+ }
+
+ private static HttpContextBase GetHttpContext()
+ {
+ return GetHttpContext(true);
+ }
+
+ private static HttpContextBase GetHttpContext(bool isCustomErrorEnabled)
+ {
+ Mock<HttpContextBase> mockContext = new Mock<HttpContextBase>(MockBehavior.Strict);
+ mockContext.Setup(c => c.IsCustomErrorEnabled).Returns(isCustomErrorEnabled);
+ return mockContext.Object;
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/HandleErrorInfoTest.cs b/test/System.Web.Mvc.Test/Test/HandleErrorInfoTest.cs
new file mode 100644
index 00000000..e68b7d94
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/HandleErrorInfoTest.cs
@@ -0,0 +1,76 @@
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class HandleErrorInfoTest
+ {
+ [Fact]
+ public void ConstructorSetsProperties()
+ {
+ // Arrange
+ Exception exception = new Exception();
+ string controller = "SomeController";
+ string action = "SomeAction";
+
+ // Act
+ HandleErrorInfo viewData = new HandleErrorInfo(exception, controller, action);
+
+ // Assert
+ Assert.Same(exception, viewData.Exception);
+ Assert.Equal(controller, viewData.ControllerName);
+ Assert.Equal(action, viewData.ActionName);
+ }
+
+ [Fact]
+ public void ConstructorWithEmptyActionThrows()
+ {
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { new HandleErrorInfo(new Exception(), "SomeController", String.Empty); }, "actionName");
+ }
+
+ [Fact]
+ public void ConstructorWithEmptyControllerThrows()
+ {
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { new HandleErrorInfo(new Exception(), String.Empty, "SomeAction"); }, "controllerName");
+ }
+
+ [Fact]
+ public void ConstructorWithNullActionThrows()
+ {
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { new HandleErrorInfo(new Exception(), "SomeController", null /* action */); }, "actionName");
+ }
+
+ [Fact]
+ public void ConstructorWithNullControllerThrows()
+ {
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { new HandleErrorInfo(new Exception(), null /* controller */, "SomeAction"); }, "controllerName");
+ }
+
+ [Fact]
+ public void ConstructorWithNullExceptionThrows()
+ {
+ Assert.ThrowsArgumentNull(
+ delegate { new HandleErrorInfo(null /* exception */, "SomeController", "SomeAction"); }, "exception");
+ }
+
+ [Fact]
+ public void ErrorHandlingDoesNotFireIfCalledInChildAction()
+ {
+ // Arrange
+ HandleErrorAttribute attr = new HandleErrorAttribute();
+ Mock<ExceptionContext> context = new Mock<ExceptionContext>();
+ context.Setup(c => c.IsChildAction).Returns(true);
+
+ // Act
+ attr.OnException(context.Object);
+
+ // Assert
+ Assert.IsType<EmptyResult>(context.Object.Result);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/HtmlHelperTest.cs b/test/System.Web.Mvc.Test/Test/HtmlHelperTest.cs
new file mode 100644
index 00000000..1e577dc8
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/HtmlHelperTest.cs
@@ -0,0 +1,1172 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Web.Routing;
+using Microsoft.Web.UnitTestUtil;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class HtmlHelperTest
+ {
+ public static readonly RouteValueDictionary AttributesDictionary = new RouteValueDictionary(new { baz = "BazValue" });
+ public static readonly object AttributesObjectDictionary = new { baz = "BazObjValue" };
+ public static readonly object AttributesObjectUnderscoresDictionary = new { foo_baz = "BazObjValue" };
+
+ // Constructor
+
+ [Fact]
+ public void ConstructorGuardClauses()
+ {
+ // Arrange
+ var viewContext = new Mock<ViewContext>().Object;
+ var viewDataContainer = MvcHelper.GetViewDataContainer(null);
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => new HtmlHelper(null, viewDataContainer),
+ "viewContext"
+ );
+ Assert.ThrowsArgumentNull(
+ () => new HtmlHelper(viewContext, null),
+ "viewDataContainer"
+ );
+ Assert.ThrowsArgumentNull(
+ () => new HtmlHelper(viewContext, viewDataContainer, null),
+ "routeCollection"
+ );
+ }
+
+ [Fact]
+ public void PropertiesAreSet()
+ {
+ // Arrange
+ var viewContext = new Mock<ViewContext>().Object;
+ var viewData = new ViewDataDictionary<String>("The Model");
+ var routes = new RouteCollection();
+ var mockViewDataContainer = new Mock<IViewDataContainer>();
+ mockViewDataContainer.Setup(vdc => vdc.ViewData).Returns(viewData);
+
+ // Act
+ var htmlHelper = new HtmlHelper(viewContext, mockViewDataContainer.Object, routes);
+
+ // Assert
+ Assert.Equal(viewContext, htmlHelper.ViewContext);
+ Assert.Equal(mockViewDataContainer.Object, htmlHelper.ViewDataContainer);
+ Assert.Equal(routes, htmlHelper.RouteCollection);
+ Assert.Equal(viewData.Model, htmlHelper.ViewData.Model);
+ }
+
+ [Fact]
+ public void DefaultRouteCollectionIsRouteTableRoutes()
+ {
+ // Arrange
+ var viewContext = new Mock<ViewContext>().Object;
+ var viewDataContainer = new Mock<IViewDataContainer>().Object;
+
+ // Act
+ var htmlHelper = new HtmlHelper(viewContext, viewDataContainer);
+
+ // Assert
+ Assert.Equal(RouteTable.Routes, htmlHelper.RouteCollection);
+ }
+
+ // AnonymousObjectToHtmlAttributes tests
+
+ [Fact]
+ public void ConvertsUnderscoresInNamesToDashes()
+ {
+ // Arrange
+ var attributes = new { foo = "Bar", baz_bif = "pow_wow" };
+
+ // Act
+ RouteValueDictionary result = HtmlHelper.AnonymousObjectToHtmlAttributes(attributes);
+
+ // Assert
+ Assert.Equal(2, result.Count);
+ Assert.Equal("Bar", result["foo"]);
+ Assert.Equal("pow_wow", result["baz-bif"]);
+ }
+
+ // AttributeEncode
+
+ [Fact]
+ public void AttributeEncodeObject()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ string encodedHtml = htmlHelper.AttributeEncode((object)@"<"">");
+
+ // Assert
+ Assert.Equal(encodedHtml, "&lt;&quot;>");
+ }
+
+ [Fact]
+ public void AttributeEncodeObjectNull()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ string encodedHtml = htmlHelper.AttributeEncode((object)null);
+
+ // Assert
+ Assert.Equal("", encodedHtml);
+ }
+
+ [Fact]
+ public void AttributeEncodeString()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ string encodedHtml = htmlHelper.AttributeEncode(@"<"">");
+
+ // Assert
+ Assert.Equal(encodedHtml, "&lt;&quot;>");
+ }
+
+ [Fact]
+ public void AttributeEncodeStringNull()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ string encodedHtml = htmlHelper.AttributeEncode((string)null);
+
+ // Assert
+ Assert.Equal("", encodedHtml);
+ }
+
+ // EnableClientValidation
+
+ [Fact]
+ public void EnableClientValidation()
+ {
+ // Arrange
+ var mockViewContext = new Mock<ViewContext>();
+ var viewDataContainer = new Mock<IViewDataContainer>().Object;
+ var htmlHelper = new HtmlHelper(mockViewContext.Object, viewDataContainer);
+
+ // Act
+ htmlHelper.EnableClientValidation();
+
+ // Act & assert
+ mockViewContext.VerifySet(vc => vc.ClientValidationEnabled = true);
+ }
+
+ // EnableUnobtrusiveJavaScript
+
+ [Fact]
+ public void EnableUnobtrusiveJavaScript()
+ {
+ // Arrange
+ var mockViewContext = new Mock<ViewContext>();
+ var viewDataContainer = new Mock<IViewDataContainer>().Object;
+ var htmlHelper = new HtmlHelper(mockViewContext.Object, viewDataContainer);
+
+ // Act
+ htmlHelper.EnableUnobtrusiveJavaScript();
+
+ // Act & assert
+ mockViewContext.VerifySet(vc => vc.UnobtrusiveJavaScriptEnabled = true);
+ }
+
+ // Encode
+
+ [Fact]
+ public void EncodeObject()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ string encodedHtml = htmlHelper.Encode((object)"<br />");
+
+ // Assert
+ Assert.Equal(encodedHtml, "&lt;br /&gt;");
+ }
+
+ [Fact]
+ public void EncodeObjectNull()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ string encodedHtml = htmlHelper.Encode((object)null);
+
+ // Assert
+ Assert.Equal("", encodedHtml);
+ }
+
+ [Fact]
+ public void EncodeString()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ string encodedHtml = htmlHelper.Encode("<br />");
+
+ // Assert
+ Assert.Equal(encodedHtml, "&lt;br /&gt;");
+ }
+
+ [Fact]
+ public void EncodeStringNull()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper();
+
+ // Act
+ string encodedHtml = htmlHelper.Encode((string)null);
+
+ // Assert
+ Assert.Equal("", encodedHtml);
+ }
+
+ // GetModelStateValue
+
+ [Fact]
+ public void GetModelStateValueReturnsNullIfModelStateHasNoValue()
+ {
+ // Arrange
+ ViewDataDictionary vdd = new ViewDataDictionary();
+ vdd.ModelState.AddModelError("foo", "some error text"); // didn't call SetModelValue()
+
+ HtmlHelper helper = new HtmlHelper(new ViewContext(), new SimpleViewDataContainer(vdd));
+
+ // Act
+ object retVal = helper.GetModelStateValue("foo", typeof(object));
+
+ // Assert
+ Assert.Null(retVal);
+ }
+
+ [Fact]
+ public void GetModelStateValueReturnsNullIfModelStateKeyNotPresent()
+ {
+ // Arrange
+ ViewDataDictionary vdd = new ViewDataDictionary();
+ HtmlHelper helper = new HtmlHelper(new ViewContext(), new SimpleViewDataContainer(vdd));
+
+ // Act
+ object retVal = helper.GetModelStateValue("key_not_present", typeof(object));
+
+ // Assert
+ Assert.Null(retVal);
+ }
+
+ // GenerateIdFromName
+
+ [Fact]
+ public void GenerateIdFromNameTests()
+ {
+ // Guard clauses
+ Assert.ThrowsArgumentNull(
+ () => HtmlHelper.GenerateIdFromName(null),
+ "name"
+ );
+ Assert.ThrowsArgumentNull(
+ () => HtmlHelper.GenerateIdFromName(null, "?"),
+ "name"
+ );
+ Assert.ThrowsArgumentNull(
+ () => HtmlHelper.GenerateIdFromName("?", null),
+ "idAttributeDotReplacement"
+ );
+
+ // Default replacement tests
+ Assert.Equal("", HtmlHelper.GenerateIdFromName(""));
+ Assert.Equal("Foo", HtmlHelper.GenerateIdFromName("Foo"));
+ Assert.Equal("Foo_Bar", HtmlHelper.GenerateIdFromName("Foo.Bar"));
+ Assert.Equal("Foo_Bar_Baz", HtmlHelper.GenerateIdFromName("Foo.Bar.Baz"));
+ Assert.Null(HtmlHelper.GenerateIdFromName("1Foo"));
+ Assert.Equal("Foo_0_", HtmlHelper.GenerateIdFromName("Foo[0]"));
+
+ // Custom replacement tests
+ Assert.Equal("", HtmlHelper.GenerateIdFromName("", "?"));
+ Assert.Equal("Foo", HtmlHelper.GenerateIdFromName("Foo", "?"));
+ Assert.Equal("Foo?Bar", HtmlHelper.GenerateIdFromName("Foo.Bar", "?"));
+ Assert.Equal("Foo?Bar?Baz", HtmlHelper.GenerateIdFromName("Foo.Bar.Baz", "?"));
+ Assert.Equal("FooBarBaz", HtmlHelper.GenerateIdFromName("Foo.Bar.Baz", ""));
+ Assert.Null(HtmlHelper.GenerateIdFromName("1Foo", "?"));
+ Assert.Equal("Foo?0?", HtmlHelper.GenerateIdFromName("Foo[0]", "?"));
+ }
+
+ // RenderPartialInternal
+
+ [Fact]
+ public void NullPartialViewNameThrows()
+ {
+ // Arrange
+ TestableHtmlHelper helper = TestableHtmlHelper.Create();
+ ViewDataDictionary viewData = new ViewDataDictionary();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ () => helper.RenderPartialInternal(null /* partialViewName */, null /* viewData */, null /* model */, TextWriter.Null),
+ "partialViewName");
+ }
+
+ [Fact]
+ public void EmptyPartialViewNameThrows()
+ {
+ // Arrange
+ TestableHtmlHelper helper = TestableHtmlHelper.Create();
+ ViewDataDictionary viewData = new ViewDataDictionary();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ () => helper.RenderPartialInternal(String.Empty /* partialViewName */, null /* viewData */, null /* model */, TextWriter.Null),
+ "partialViewName");
+ }
+
+ [Fact]
+ public void EngineLookupSuccessCallsRender()
+ {
+ // Arrange
+ TestableHtmlHelper helper = TestableHtmlHelper.Create();
+ TextWriter writer = helper.ViewContext.Writer;
+ Mock<IViewEngine> engine = new Mock<IViewEngine>(MockBehavior.Strict);
+ Mock<IView> view = new Mock<IView>(MockBehavior.Strict);
+ engine
+ .Setup(e => e.FindPartialView(It.IsAny<ControllerContext>(), "partial-view", It.IsAny<bool>()))
+ .Returns(new ViewEngineResult(view.Object, engine.Object))
+ .Verifiable();
+ view
+ .Setup(v => v.Render(It.IsAny<ViewContext>(), writer))
+ .Callback<ViewContext, TextWriter>(
+ (viewContext, _) =>
+ {
+ Assert.Same(helper.ViewContext.View, viewContext.View);
+ Assert.Same(helper.ViewContext.TempData, viewContext.TempData);
+ })
+ .Verifiable();
+
+ // Act
+ helper.RenderPartialInternal("partial-view", null /* viewData */, null /* model */, writer, engine.Object);
+
+ // Assert
+ engine.Verify();
+ view.Verify();
+ }
+
+ [Fact]
+ public void EngineLookupFailureThrows()
+ {
+ // Arrange
+ TestableHtmlHelper helper = TestableHtmlHelper.Create();
+ Mock<IViewEngine> engine = new Mock<IViewEngine>(MockBehavior.Strict);
+ engine
+ .Setup(e => e.FindPartialView(It.IsAny<ControllerContext>(), "partial-view", It.IsAny<bool>()))
+ .Returns(new ViewEngineResult(new[] { "location1", "location2" }))
+ .Verifiable();
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ () => helper.RenderPartialInternal("partial-view", null /* viewData */, null /* model */, TextWriter.Null, engine.Object),
+ @"The partial view 'partial-view' was not found or no view engine supports the searched locations. The following locations were searched:
+location1
+location2");
+
+ engine.Verify();
+ }
+
+ [Fact]
+ public void RenderPartialInternalWithNullModelAndNullViewData()
+ {
+ // Arrange
+ object model = new object();
+ TestableHtmlHelper helper = TestableHtmlHelper.Create();
+ helper.ViewData["Foo"] = "Bar";
+ helper.ViewData.Model = model;
+ Mock<IViewEngine> engine = new Mock<IViewEngine>(MockBehavior.Strict);
+ Mock<IView> view = new Mock<IView>(MockBehavior.Strict);
+ engine
+ .Setup(e => e.FindPartialView(It.IsAny<ControllerContext>(), "partial-view", It.IsAny<bool>()))
+ .Returns(new ViewEngineResult(view.Object, engine.Object))
+ .Verifiable();
+ view
+ .Setup(v => v.Render(It.IsAny<ViewContext>(), TextWriter.Null))
+ .Callback<ViewContext, TextWriter>(
+ (viewContext, writer) =>
+ {
+ Assert.NotSame(helper.ViewData, viewContext.ViewData); // New view data instance
+ Assert.Equal("Bar", viewContext.ViewData["Foo"]); // Copy of the existing view data
+ Assert.Same(model, viewContext.ViewData.Model); // Keep existing model
+ })
+ .Verifiable();
+
+ // Act
+ helper.RenderPartialInternal("partial-view", null /* viewData */, null /* model */, TextWriter.Null, engine.Object);
+
+ // Assert
+ engine.Verify();
+ view.Verify();
+ }
+
+ [Fact]
+ public void RenderPartialInternalWithNonNullModelAndNullViewData()
+ {
+ // Arrange
+ object model = new object();
+ object newModel = new object();
+ TestableHtmlHelper helper = TestableHtmlHelper.Create();
+ helper.ViewData["Foo"] = "Bar";
+ helper.ViewData.Model = model;
+ Mock<IViewEngine> engine = new Mock<IViewEngine>(MockBehavior.Strict);
+ Mock<IView> view = new Mock<IView>(MockBehavior.Strict);
+ engine
+ .Setup(e => e.FindPartialView(It.IsAny<ControllerContext>(), "partial-view", It.IsAny<bool>()))
+ .Returns(new ViewEngineResult(view.Object, engine.Object))
+ .Verifiable();
+ view
+ .Setup(v => v.Render(It.IsAny<ViewContext>(), TextWriter.Null))
+ .Callback<ViewContext, TextWriter>(
+ (viewContext, writer) =>
+ {
+ Assert.NotSame(helper.ViewData, viewContext.ViewData); // New view data instance
+ Assert.Empty(viewContext.ViewData); // Empty (not copied)
+ Assert.Same(newModel, viewContext.ViewData.Model); // New model
+ })
+ .Verifiable();
+
+ // Act
+ helper.RenderPartialInternal("partial-view", null /* viewData */, newModel, TextWriter.Null, engine.Object);
+
+ // Assert
+ engine.Verify();
+ view.Verify();
+ }
+
+ [Fact]
+ public void RenderPartialInternalWithNullModelAndNonNullViewData()
+ {
+ // Arrange
+ object model = new object();
+ object vddModel = new object();
+ ViewDataDictionary vdd = new ViewDataDictionary();
+ vdd["Baz"] = "Biff";
+ vdd.Model = vddModel;
+ TestableHtmlHelper helper = TestableHtmlHelper.Create();
+ helper.ViewData["Foo"] = "Bar";
+ helper.ViewData.Model = model;
+ Mock<IViewEngine> engine = new Mock<IViewEngine>(MockBehavior.Strict);
+ Mock<IView> view = new Mock<IView>(MockBehavior.Strict);
+ engine
+ .Setup(e => e.FindPartialView(It.IsAny<ControllerContext>(), "partial-view", It.IsAny<bool>()))
+ .Returns(new ViewEngineResult(view.Object, engine.Object))
+ .Verifiable();
+ view
+ .Setup(v => v.Render(It.IsAny<ViewContext>(), TextWriter.Null))
+ .Callback<ViewContext, TextWriter>(
+ (viewContext, writer) =>
+ {
+ Assert.NotSame(helper.ViewData, viewContext.ViewData); // New view data instance
+ Assert.Single(viewContext.ViewData); // Copy of the passed view data, not original view data
+ Assert.Equal("Biff", viewContext.ViewData["Baz"]);
+ Assert.Same(vddModel, viewContext.ViewData.Model); // Keep model from passed view data, not original view data
+ })
+ .Verifiable();
+
+ // Act
+ helper.RenderPartialInternal("partial-view", vdd, null /* model */, TextWriter.Null, engine.Object);
+
+ // Assert
+ engine.Verify();
+ view.Verify();
+ }
+
+ [Fact]
+ public void RenderPartialInternalWithNonNullModelAndNonNullViewData()
+ {
+ // Arrange
+ object model = new object();
+ object vddModel = new object();
+ object newModel = new object();
+ ViewDataDictionary vdd = new ViewDataDictionary();
+ vdd["Baz"] = "Biff";
+ vdd.Model = vddModel;
+ TestableHtmlHelper helper = TestableHtmlHelper.Create();
+ helper.ViewData["Foo"] = "Bar";
+ helper.ViewData.Model = model;
+ Mock<IViewEngine> engine = new Mock<IViewEngine>(MockBehavior.Strict);
+ Mock<IView> view = new Mock<IView>(MockBehavior.Strict);
+ engine
+ .Setup(e => e.FindPartialView(It.IsAny<ControllerContext>(), "partial-view", It.IsAny<bool>()))
+ .Returns(new ViewEngineResult(view.Object, engine.Object))
+ .Verifiable();
+ view
+ .Setup(v => v.Render(It.IsAny<ViewContext>(), TextWriter.Null))
+ .Callback<ViewContext, TextWriter>(
+ (viewContext, writer) =>
+ {
+ Assert.NotSame(helper.ViewData, viewContext.ViewData); // New view data instance
+ Assert.Single(viewContext.ViewData); // Copy of the passed view data, not original view data
+ Assert.Equal("Biff", viewContext.ViewData["Baz"]);
+ Assert.Same(newModel, viewContext.ViewData.Model); // New model
+ })
+ .Verifiable();
+
+ // Act
+ helper.RenderPartialInternal("partial-view", vdd, newModel, TextWriter.Null, engine.Object);
+
+ // Assert
+ engine.Verify();
+ view.Verify();
+ }
+
+ // HttpMethodOverride
+
+ [Fact]
+ public void HttpMethodOverrideGuardClauses()
+ {
+ // Arrange
+ var viewContext = new Mock<ViewContext>().Object;
+ var viewDataContainer = MvcHelper.GetViewDataContainer(null);
+ var htmlHelper = new HtmlHelper(viewContext, viewDataContainer);
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ () => htmlHelper.HttpMethodOverride(null),
+ "httpMethod"
+ );
+ Assert.Throws<ArgumentException>(
+ () => htmlHelper.HttpMethodOverride((HttpVerbs)10000),
+ @"The specified HttpVerbs value is not supported. The supported values are Delete, Head, and Put.
+Parameter name: httpVerb"
+ );
+ Assert.Throws<ArgumentException>(
+ () => htmlHelper.HttpMethodOverride(HttpVerbs.Get),
+ @"The specified HttpVerbs value is not supported. The supported values are Delete, Head, and Put.
+Parameter name: httpVerb"
+ );
+ Assert.Throws<ArgumentException>(
+ () => htmlHelper.HttpMethodOverride(HttpVerbs.Post),
+ @"The specified HttpVerbs value is not supported. The supported values are Delete, Head, and Put.
+Parameter name: httpVerb"
+ );
+ Assert.Throws<ArgumentException>(
+ () => htmlHelper.HttpMethodOverride("gEt"),
+ @"The GET and POST HTTP methods are not supported.
+Parameter name: httpMethod"
+ );
+ Assert.Throws<ArgumentException>(
+ () => htmlHelper.HttpMethodOverride("pOsT"),
+ @"The GET and POST HTTP methods are not supported.
+Parameter name: httpMethod"
+ );
+ }
+
+ [Fact]
+ public void HttpMethodOverrideWithMethodRendersHiddenField()
+ {
+ // Arrange
+ var viewContext = new Mock<ViewContext>().Object;
+ var viewDataContainer = MvcHelper.GetViewDataContainer(null);
+ var htmlHelper = new HtmlHelper(viewContext, viewDataContainer);
+
+ // Act
+ MvcHtmlString hiddenField = htmlHelper.HttpMethodOverride("PUT");
+
+ // Assert
+ Assert.Equal(@"<input name=""X-HTTP-Method-Override"" type=""hidden"" value=""PUT"" />", hiddenField.ToHtmlString());
+ }
+
+ [Fact]
+ public void HttpMethodOverrideWithVerbRendersHiddenField()
+ {
+ // Arrange
+ var viewContext = new Mock<ViewContext>().Object;
+ var viewDataContainer = MvcHelper.GetViewDataContainer(null);
+ var htmlHelper = new HtmlHelper(viewContext, viewDataContainer);
+
+ // Act
+ MvcHtmlString hiddenField = htmlHelper.HttpMethodOverride(HttpVerbs.Delete);
+
+ // Assert
+ Assert.Equal(@"<input name=""X-HTTP-Method-Override"" type=""hidden"" value=""DELETE"" />", hiddenField.ToHtmlString());
+ }
+
+ [Fact]
+ public void ViewBagProperty_ReflectsViewData()
+ {
+ // Arrange
+ Mock<IViewDataContainer> viewDataContainer = new Mock<IViewDataContainer>();
+ ViewDataDictionary viewDataDictionary = new ViewDataDictionary() { { "A", 1 } };
+ viewDataContainer.Setup(container => container.ViewData).Returns(viewDataDictionary);
+
+ // Act
+ HtmlHelper htmlHelper = new HtmlHelper(new Mock<ViewContext>().Object, viewDataContainer.Object);
+
+ // Assert
+ Assert.Equal(1, htmlHelper.ViewBag.A);
+ }
+
+ [Fact]
+ public void ViewBagProperty_ReflectsNewViewDataContainerInstance()
+ {
+ // Arrange
+ ViewDataDictionary viewDataDictionary = new ViewDataDictionary() { { "A", 1 } };
+ Mock<IViewDataContainer> viewDataContainer = new Mock<IViewDataContainer>();
+ viewDataContainer.Setup(container => container.ViewData).Returns(viewDataDictionary);
+
+ ViewDataDictionary otherViewDataDictionary = new ViewDataDictionary() { { "A", 2 } };
+ Mock<IViewDataContainer> otherViewDataContainer = new Mock<IViewDataContainer>();
+ otherViewDataContainer.Setup(container => container.ViewData).Returns(otherViewDataDictionary);
+
+ HtmlHelper htmlHelper = new HtmlHelper(new Mock<ViewContext>().Object, viewDataContainer.Object, new RouteCollection());
+
+ // Act
+ htmlHelper.ViewDataContainer = otherViewDataContainer.Object;
+
+ // Assert
+ Assert.Equal(2, htmlHelper.ViewBag.A);
+ }
+
+ [Fact]
+ public void ViewBag_PropagatesChangesToViewData()
+ {
+ // Arrange
+ ViewDataDictionary viewDataDictionary = new ViewDataDictionary() { { "A", 1 } };
+ Mock<IViewDataContainer> viewDataContainer = new Mock<IViewDataContainer>();
+ viewDataContainer.Setup(container => container.ViewData).Returns(viewDataDictionary);
+
+ HtmlHelper htmlHelper = new HtmlHelper(new Mock<ViewContext>().Object, viewDataContainer.Object, new RouteCollection());
+
+ // Act
+ htmlHelper.ViewBag.A = "foo";
+ htmlHelper.ViewBag.B = 2;
+
+ // Assert
+ Assert.Equal("foo", htmlHelper.ViewData["A"]);
+ Assert.Equal(2, htmlHelper.ViewData["B"]);
+ }
+
+ // Unobtrusive validation attributes
+
+ [Fact]
+ public void GetUnobtrusiveValidationAttributesReturnsEmptySetWhenClientValidationIsNotEnabled()
+ {
+ // Arrange
+ var formContext = new FormContext();
+ formContext.RenderedField("foobar", true);
+ var viewContext = new Mock<ViewContext>();
+ viewContext.SetupGet(vc => vc.FormContext).Returns(formContext);
+ var viewDataContainer = MvcHelper.GetViewDataContainer(new ViewDataDictionary());
+ var htmlHelper = new HtmlHelper(viewContext.Object, viewDataContainer);
+
+ // Act
+ IDictionary<string, object> result = htmlHelper.GetUnobtrusiveValidationAttributes("foobar");
+
+ // Assert
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void GetUnobtrusiveValidationAttributesReturnsEmptySetWhenUnobtrusiveJavaScriptIsNotEnabled()
+ {
+ // Arrange
+ var formContext = new FormContext();
+ formContext.RenderedField("foobar", true);
+ var viewContext = new Mock<ViewContext>();
+ viewContext.SetupGet(vc => vc.FormContext).Returns(formContext);
+ viewContext.SetupGet(vc => vc.ClientValidationEnabled).Returns(true);
+ var viewDataContainer = MvcHelper.GetViewDataContainer(new ViewDataDictionary());
+ var htmlHelper = new HtmlHelper(viewContext.Object, viewDataContainer);
+
+ // Act
+ IDictionary<string, object> result = htmlHelper.GetUnobtrusiveValidationAttributes("foobar");
+
+ // Assert
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void GetUnobtrusiveValidationAttributesReturnsEmptySetWhenFieldHasAlreadyBeenRendered()
+ {
+ // Arrange
+ var formContext = new FormContext();
+ formContext.RenderedField("foobar", true);
+ var viewContext = new Mock<ViewContext>();
+ viewContext.SetupGet(vc => vc.FormContext).Returns(formContext);
+ viewContext.SetupGet(vc => vc.ClientValidationEnabled).Returns(true);
+ viewContext.SetupGet(vc => vc.UnobtrusiveJavaScriptEnabled).Returns(true);
+ var viewDataContainer = MvcHelper.GetViewDataContainer(new ViewDataDictionary());
+ var htmlHelper = new HtmlHelper(viewContext.Object, viewDataContainer);
+
+ // Act
+ IDictionary<string, object> result = htmlHelper.GetUnobtrusiveValidationAttributes("foobar");
+
+ // Assert
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void GetUnobtrusiveValidationAttributesReturnsEmptySetAndSetsFieldAsRenderedForFieldWithNoClientRules()
+ {
+ // Arrange
+ var formContext = new FormContext();
+ var viewContext = new Mock<ViewContext>();
+ viewContext.SetupGet(vc => vc.FormContext).Returns(formContext);
+ viewContext.SetupGet(vc => vc.ClientValidationEnabled).Returns(true);
+ viewContext.SetupGet(vc => vc.UnobtrusiveJavaScriptEnabled).Returns(true);
+ var viewDataContainer = MvcHelper.GetViewDataContainer(new ViewDataDictionary());
+ var htmlHelper = new HtmlHelper(viewContext.Object, viewDataContainer);
+ htmlHelper.ClientValidationRuleFactory = delegate { return Enumerable.Empty<ModelClientValidationRule>(); };
+
+ // Act
+ IDictionary<string, object> result = htmlHelper.GetUnobtrusiveValidationAttributes("foobar");
+
+ // Assert
+ Assert.Empty(result);
+ Assert.True(formContext.RenderedField("foobar"));
+ }
+
+ [Fact]
+ public void GetUnobtrusiveValidationAttributesIncludesDataValTrueWithNonEmptyClientRuleList()
+ {
+ // Arrange
+ var formContext = new FormContext();
+ var viewContext = new Mock<ViewContext>();
+ viewContext.SetupGet(vc => vc.FormContext).Returns(formContext);
+ viewContext.SetupGet(vc => vc.ClientValidationEnabled).Returns(true);
+ viewContext.SetupGet(vc => vc.UnobtrusiveJavaScriptEnabled).Returns(true);
+ var viewDataContainer = MvcHelper.GetViewDataContainer(new ViewDataDictionary());
+ var htmlHelper = new HtmlHelper(viewContext.Object, viewDataContainer);
+ htmlHelper.ClientValidationRuleFactory = delegate { return new[] { new ModelClientValidationRule { ValidationType = "type" } }; };
+
+ // Act
+ IDictionary<string, object> result = htmlHelper.GetUnobtrusiveValidationAttributes("foobar");
+
+ // Assert
+ Assert.Equal("true", result["data-val"]);
+ }
+
+ [Fact]
+ public void GetUnobtrusiveValidationAttributesWithEmptyMessage()
+ {
+ // Arrange
+ var formContext = new FormContext();
+ var viewContext = new Mock<ViewContext>();
+ viewContext.SetupGet(vc => vc.FormContext).Returns(formContext);
+ viewContext.SetupGet(vc => vc.ClientValidationEnabled).Returns(true);
+ viewContext.SetupGet(vc => vc.UnobtrusiveJavaScriptEnabled).Returns(true);
+ var viewDataContainer = MvcHelper.GetViewDataContainer(new ViewDataDictionary());
+ var htmlHelper = new HtmlHelper(viewContext.Object, viewDataContainer);
+ htmlHelper.ClientValidationRuleFactory = delegate { return new[] { new ModelClientValidationRule { ValidationType = "type" } }; };
+
+ // Act
+ IDictionary<string, object> result = htmlHelper.GetUnobtrusiveValidationAttributes("foobar");
+
+ // Assert
+ Assert.Equal("", result["data-val-type"]);
+ }
+
+ [Fact]
+ public void GetUnobtrusiveValidationAttributesMessageIsHtmlEncoded()
+ {
+ // Arrange
+ var formContext = new FormContext();
+ var viewContext = new Mock<ViewContext>();
+ viewContext.SetupGet(vc => vc.FormContext).Returns(formContext);
+ viewContext.SetupGet(vc => vc.ClientValidationEnabled).Returns(true);
+ viewContext.SetupGet(vc => vc.UnobtrusiveJavaScriptEnabled).Returns(true);
+ var viewDataContainer = MvcHelper.GetViewDataContainer(new ViewDataDictionary());
+ var htmlHelper = new HtmlHelper(viewContext.Object, viewDataContainer);
+ htmlHelper.ClientValidationRuleFactory = delegate { return new[] { new ModelClientValidationRule { ValidationType = "type", ErrorMessage = "<script>alert('xss')</script>" } }; };
+
+ // Act
+ IDictionary<string, object> result = htmlHelper.GetUnobtrusiveValidationAttributes("foobar");
+
+ // Assert
+ Assert.Equal("&lt;script&gt;alert(&#39;xss&#39;)&lt;/script&gt;", result["data-val-type"]);
+ }
+
+ [Fact]
+ public void GetUnobtrusiveValidationAttributesWithMessageAndParameters()
+ {
+ // Arrange
+ var formContext = new FormContext();
+ var viewContext = new Mock<ViewContext>();
+ viewContext.SetupGet(vc => vc.FormContext).Returns(formContext);
+ viewContext.SetupGet(vc => vc.ClientValidationEnabled).Returns(true);
+ viewContext.SetupGet(vc => vc.UnobtrusiveJavaScriptEnabled).Returns(true);
+ var viewDataContainer = MvcHelper.GetViewDataContainer(new ViewDataDictionary());
+ var htmlHelper = new HtmlHelper(viewContext.Object, viewDataContainer);
+ htmlHelper.ClientValidationRuleFactory = delegate
+ {
+ ModelClientValidationRule rule = new ModelClientValidationRule { ValidationType = "type", ErrorMessage = "error" };
+ rule.ValidationParameters["foo"] = "bar";
+ rule.ValidationParameters["baz"] = "biff";
+ return new[] { rule };
+ };
+
+ // Act
+ IDictionary<string, object> result = htmlHelper.GetUnobtrusiveValidationAttributes("foobar");
+
+ // Assert
+ Assert.Equal("error", result["data-val-type"]);
+ Assert.Equal("bar", result["data-val-type-foo"]);
+ Assert.Equal("biff", result["data-val-type-baz"]);
+ }
+
+ [Fact]
+ public void GetUnobtrusiveValidationAttributesWithTwoClientRules()
+ {
+ // Arrange
+ var formContext = new FormContext();
+ var viewContext = new Mock<ViewContext>();
+ viewContext.SetupGet(vc => vc.FormContext).Returns(formContext);
+ viewContext.SetupGet(vc => vc.ClientValidationEnabled).Returns(true);
+ viewContext.SetupGet(vc => vc.UnobtrusiveJavaScriptEnabled).Returns(true);
+ var viewDataContainer = MvcHelper.GetViewDataContainer(new ViewDataDictionary());
+ var htmlHelper = new HtmlHelper(viewContext.Object, viewDataContainer);
+ htmlHelper.ClientValidationRuleFactory = delegate
+ {
+ ModelClientValidationRule rule1 = new ModelClientValidationRule { ValidationType = "type", ErrorMessage = "error" };
+ rule1.ValidationParameters["foo"] = "bar";
+ rule1.ValidationParameters["baz"] = "biff";
+ ModelClientValidationRule rule2 = new ModelClientValidationRule { ValidationType = "othertype", ErrorMessage = "othererror" };
+ rule2.ValidationParameters["true3"] = "false4";
+ return new[] { rule1, rule2 };
+ };
+
+ // Act
+ IDictionary<string, object> result = htmlHelper.GetUnobtrusiveValidationAttributes("foobar");
+
+ // Assert
+ Assert.Equal("error", result["data-val-type"]);
+ Assert.Equal("bar", result["data-val-type-foo"]);
+ Assert.Equal("biff", result["data-val-type-baz"]);
+ Assert.Equal("othererror", result["data-val-othertype"]);
+ Assert.Equal("false4", result["data-val-othertype-true3"]);
+ }
+
+ [Fact]
+ public void GetUnobtrusiveValidationAttributesUsesShortNameForModelMetadataLookup()
+ {
+ // Arrange
+ string passedName = null;
+ var formContext = new FormContext();
+ var viewContext = new Mock<ViewContext>();
+ var viewData = new ViewDataDictionary();
+ viewContext.SetupGet(vc => vc.FormContext).Returns(formContext);
+ viewContext.SetupGet(vc => vc.ClientValidationEnabled).Returns(true);
+ viewContext.SetupGet(vc => vc.UnobtrusiveJavaScriptEnabled).Returns(true);
+ viewData.TemplateInfo.HtmlFieldPrefix = "Prefix";
+ var viewDataContainer = MvcHelper.GetViewDataContainer(viewData);
+ var htmlHelper = new HtmlHelper(viewContext.Object, viewDataContainer);
+ htmlHelper.ClientValidationRuleFactory = (name, _) =>
+ {
+ passedName = name;
+ return Enumerable.Empty<ModelClientValidationRule>();
+ };
+
+ // Act
+ htmlHelper.GetUnobtrusiveValidationAttributes("foobar");
+
+ // Assert
+ Assert.Equal("foobar", passedName);
+ }
+
+ [Fact]
+ public void GetUnobtrusiveValidationAttributeUsesViewDataForModelMetadataLookup()
+ {
+ // Arrange
+ var formContext = new FormContext();
+ var viewContext = new Mock<ViewContext>();
+ var viewData = new ViewDataDictionary<MyModel>();
+ viewContext.SetupGet(vc => vc.FormContext).Returns(formContext);
+ viewContext.SetupGet(vc => vc.ClientValidationEnabled).Returns(true);
+ viewContext.SetupGet(vc => vc.UnobtrusiveJavaScriptEnabled).Returns(true);
+ viewData.TemplateInfo.HtmlFieldPrefix = "Prefix";
+ var viewDataContainer = MvcHelper.GetViewDataContainer(viewData);
+ var htmlHelper = new HtmlHelper(viewContext.Object, viewDataContainer);
+
+ // Act
+ IDictionary<string, object> result = htmlHelper.GetUnobtrusiveValidationAttributes("MyProperty");
+
+ // Assert
+ Assert.Equal(2, result.Count);
+ Assert.Equal("true", result["data-val"]);
+ Assert.Equal("My required message", result["data-val-required"]);
+ }
+
+ class MyModel
+ {
+ [Required(ErrorMessage = "My required message")]
+ public object MyProperty { get; set; }
+ }
+
+ [Fact]
+ public void GetUnobtrusiveValidationAttributesMarksRenderedFieldsWithFullName()
+ {
+ // Arrange
+ var formContext = new FormContext();
+ var viewContext = new Mock<ViewContext>();
+ var viewData = new ViewDataDictionary();
+ viewContext.SetupGet(vc => vc.FormContext).Returns(formContext);
+ viewContext.SetupGet(vc => vc.ClientValidationEnabled).Returns(true);
+ viewContext.SetupGet(vc => vc.UnobtrusiveJavaScriptEnabled).Returns(true);
+ viewData.TemplateInfo.HtmlFieldPrefix = "Prefix";
+ var viewDataContainer = MvcHelper.GetViewDataContainer(viewData);
+ var htmlHelper = new HtmlHelper(viewContext.Object, viewDataContainer);
+
+ // Act
+ htmlHelper.GetUnobtrusiveValidationAttributes("foobar");
+
+ // Assert
+ Assert.False(formContext.RenderedField("foobar"));
+ Assert.True(formContext.RenderedField("Prefix.foobar"));
+ }
+
+ [Fact]
+ public void GetUnobtrusiveValidationAttributesGuardClauses()
+ {
+ // Arrange
+ var formContext = new FormContext();
+ var viewContext = new Mock<ViewContext>();
+ viewContext.SetupGet(vc => vc.FormContext).Returns(formContext);
+ viewContext.SetupGet(vc => vc.ClientValidationEnabled).Returns(true);
+ viewContext.SetupGet(vc => vc.UnobtrusiveJavaScriptEnabled).Returns(true);
+ var viewDataContainer = MvcHelper.GetViewDataContainer(new ViewDataDictionary());
+ var htmlHelper = new HtmlHelper(viewContext.Object, viewDataContainer);
+
+ // Act & Assert
+ AssertBadClientValidationRule(htmlHelper, "Validation type names in unobtrusive client validation rules cannot be empty. Client rule type: System.Web.Mvc.ModelClientValidationRule", new ModelClientValidationRule());
+ AssertBadClientValidationRule(htmlHelper, "Validation type names in unobtrusive client validation rules must consist of only lowercase letters. Invalid name: \"OnlyLowerCase\", client rule type: System.Web.Mvc.ModelClientValidationRule", new ModelClientValidationRule { ValidationType = "OnlyLowerCase" });
+ AssertBadClientValidationRule(htmlHelper, "Validation type names in unobtrusive client validation rules must consist of only lowercase letters. Invalid name: \"nonumb3rs\", client rule type: System.Web.Mvc.ModelClientValidationRule", new ModelClientValidationRule { ValidationType = "nonumb3rs" });
+ AssertBadClientValidationRule(htmlHelper, "Validation type names in unobtrusive client validation rules must be unique. The following validation type was seen more than once: rule", new ModelClientValidationRule { ValidationType = "rule" }, new ModelClientValidationRule { ValidationType = "rule" });
+
+ var emptyParamName = new ModelClientValidationRule { ValidationType = "type" };
+ emptyParamName.ValidationParameters[""] = "foo";
+ AssertBadClientValidationRule(htmlHelper, "Validation parameter names in unobtrusive client validation rules cannot be empty. Client rule type: System.Web.Mvc.ModelClientValidationRule", emptyParamName);
+
+ var paramNameMixedCase = new ModelClientValidationRule { ValidationType = "type" };
+ paramNameMixedCase.ValidationParameters["MixedCase"] = "foo";
+ AssertBadClientValidationRule(htmlHelper, "Validation parameter names in unobtrusive client validation rules must start with a lowercase letter and consist of only lowercase letters or digits. Validation parameter name: MixedCase, client rule type: System.Web.Mvc.ModelClientValidationRule", paramNameMixedCase);
+
+ var paramNameStartsWithNumber = new ModelClientValidationRule { ValidationType = "type" };
+ paramNameStartsWithNumber.ValidationParameters["2112"] = "foo";
+ AssertBadClientValidationRule(htmlHelper, "Validation parameter names in unobtrusive client validation rules must start with a lowercase letter and consist of only lowercase letters or digits. Validation parameter name: 2112, client rule type: System.Web.Mvc.ModelClientValidationRule", paramNameStartsWithNumber);
+ }
+
+ [Fact]
+ public void RawReturnsWrapperMarkup()
+ {
+ // Arrange
+ var viewContext = new Mock<ViewContext>().Object;
+ var viewDataContainer = new Mock<IViewDataContainer>().Object;
+ var htmlHelper = new HtmlHelper(viewContext, viewDataContainer);
+ string markup = "<b>bold</b>";
+
+ // Act
+ IHtmlString markupHtml = htmlHelper.Raw(markup);
+
+ // Assert
+ Assert.Equal("<b>bold</b>", markupHtml.ToString());
+ Assert.Equal("<b>bold</b>", markupHtml.ToHtmlString());
+ }
+
+ [Fact]
+ public void RawAllowsNullValue()
+ {
+ // Arrange
+ var viewContext = new Mock<ViewContext>().Object;
+ var viewDataContainer = new Mock<IViewDataContainer>().Object;
+ var htmlHelper = new HtmlHelper(viewContext, viewDataContainer);
+
+ // Act
+ IHtmlString markupHtml = htmlHelper.Raw(null);
+
+ // Assert
+ Assert.Equal(null, markupHtml.ToString());
+ Assert.Equal(null, markupHtml.ToHtmlString());
+ }
+
+ [Fact]
+ public void RawAllowsNullObjectValue()
+ {
+ // Arrange
+ var viewContext = new Mock<ViewContext>().Object;
+ var viewDataContainer = new Mock<IViewDataContainer>().Object;
+ var htmlHelper = new HtmlHelper(viewContext, viewDataContainer);
+
+ // Act
+ IHtmlString markupHtml = htmlHelper.Raw((object)null);
+
+ // Assert
+ Assert.Equal(null, markupHtml.ToString());
+ Assert.Equal(null, markupHtml.ToHtmlString());
+ }
+
+ [Fact]
+ public void RawAllowsEmptyValue()
+ {
+ // Arrange
+ var viewContext = new Mock<ViewContext>().Object;
+ var viewDataContainer = new Mock<IViewDataContainer>().Object;
+ var htmlHelper = new HtmlHelper(viewContext, viewDataContainer);
+
+ // Act
+ IHtmlString markupHtml = htmlHelper.Raw("");
+
+ // Assert
+ Assert.Equal("", markupHtml.ToString());
+ Assert.Equal("", markupHtml.ToHtmlString());
+ }
+
+ [Fact]
+ public void RawReturnsWrapperMarkupOfObject()
+ {
+ // Arrange
+ var viewContext = new Mock<ViewContext>().Object;
+ var viewDataContainer = new Mock<IViewDataContainer>().Object;
+ var htmlHelper = new HtmlHelper(viewContext, viewDataContainer);
+ ObjectWithWrapperMarkup obj = new ObjectWithWrapperMarkup();
+
+ // Act
+ IHtmlString markupHtml = htmlHelper.Raw(obj);
+
+ // Assert
+ Assert.Equal("<b>boldFromObject</b>", markupHtml.ToString());
+ Assert.Equal("<b>boldFromObject</b>", markupHtml.ToHtmlString());
+ }
+
+ [Fact]
+ public void EvalStringAndFormatValueWithNullValueReturnsEmptyString()
+ {
+ // Arrange
+ var htmlHelper = MvcHelper.GetHtmlHelper(new ViewDataDictionary());
+
+ // Act & Assert
+ Assert.Equal(String.Empty, htmlHelper.FormatValue(null, "-{0}-"));
+ Assert.Equal(String.Empty, htmlHelper.EvalString("nonExistant"));
+ Assert.Equal(String.Empty, htmlHelper.EvalString("nonExistant", "-{0}-"));
+ }
+
+ [Fact]
+ public void EvalStringAndFormatValueUseCurrentCulture()
+ {
+ // Arrange
+ DateTime dt = new DateTime(1900, 1, 1, 0, 0, 0);
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(new ViewDataDictionary() { { "date", dt } });
+ string expectedFormattedDate = "-1900/01/01 12:00:00 AM-";
+
+ // Act && Assert
+ using (ReplaceCulture("en-ZA", "en-US"))
+ {
+ Assert.Equal(expectedFormattedDate, htmlHelper.FormatValue(dt, "-{0}-"));
+ Assert.Equal(expectedFormattedDate, htmlHelper.EvalString("date", "-{0}-"));
+ }
+ }
+
+ [Fact]
+ public void EvalStringAndFormatValueWithEmptyFormatConvertsValueToString()
+ {
+ // Arrange
+ DateTime dt = new DateTime(1900, 1, 1, 0, 0, 0);
+ HtmlHelper htmlHelper = MvcHelper.GetHtmlHelper(new ViewDataDictionary() { { "date", dt } });
+ string expectedUnformattedDate = "1900/01/01 12:00:00 AM";
+
+ // Act && Assert
+ using (ReplaceCulture("en-ZA", "en-US"))
+ {
+ Assert.Equal(expectedUnformattedDate, htmlHelper.FormatValue(dt, String.Empty));
+ Assert.Equal(expectedUnformattedDate, htmlHelper.EvalString("date", String.Empty));
+ Assert.Equal(expectedUnformattedDate, htmlHelper.EvalString("date"));
+ }
+ }
+
+ private class ObjectWithWrapperMarkup
+ {
+ public override string ToString()
+ {
+ return "<b>boldFromObject</b>";
+ }
+ }
+
+ // Helpers
+
+ private static void AssertBadClientValidationRule(HtmlHelper htmlHelper, string expectedMessage, params ModelClientValidationRule[] rules)
+ {
+ htmlHelper.ClientValidationRuleFactory = delegate { return rules; };
+ Assert.Throws<InvalidOperationException>(
+ () => htmlHelper.GetUnobtrusiveValidationAttributes(Guid.NewGuid().ToString()),
+ expectedMessage
+ );
+ }
+
+ internal static ValueProviderResult GetValueProviderResult(object rawValue, string attemptedValue)
+ {
+ return new ValueProviderResult(rawValue, attemptedValue, CultureInfo.InvariantCulture);
+ }
+
+ internal static IDisposable ReplaceCulture(string currentCulture, string currentUICulture)
+ {
+ CultureInfo newCulture = CultureInfo.GetCultureInfo(currentCulture);
+ CultureInfo newUICulture = CultureInfo.GetCultureInfo(currentUICulture);
+ CultureInfo originalCulture = Thread.CurrentThread.CurrentCulture;
+ CultureInfo originalUICulture = Thread.CurrentThread.CurrentUICulture;
+ Thread.CurrentThread.CurrentCulture = newCulture;
+ Thread.CurrentThread.CurrentUICulture = newUICulture;
+ return new CultureReplacement { OriginalCulture = originalCulture, OriginalUICulture = originalUICulture };
+ }
+
+ private class CultureReplacement : IDisposable
+ {
+ public CultureInfo OriginalCulture;
+ public CultureInfo OriginalUICulture;
+
+ public void Dispose()
+ {
+ Thread.CurrentThread.CurrentCulture = OriginalCulture;
+ Thread.CurrentThread.CurrentUICulture = OriginalUICulture;
+ }
+ }
+
+ private class TestableHtmlHelper : HtmlHelper
+ {
+ TestableHtmlHelper(ViewContext viewContext, IViewDataContainer viewDataContainer)
+ : base(viewContext, viewDataContainer)
+ {
+ }
+
+ public static TestableHtmlHelper Create()
+ {
+ ViewDataDictionary viewData = new ViewDataDictionary();
+
+ Mock<ViewContext> mockViewContext = new Mock<ViewContext>() { DefaultValue = DefaultValue.Mock };
+ mockViewContext.Setup(c => c.HttpContext.Response.Output).Throws(new Exception("Response.Output should never be called."));
+ mockViewContext.Setup(c => c.ViewData).Returns(viewData);
+ mockViewContext.Setup(c => c.Writer).Returns(new StringWriter());
+
+ Mock<IViewDataContainer> container = new Mock<IViewDataContainer>();
+ container.Setup(c => c.ViewData).Returns(viewData);
+
+ return new TestableHtmlHelper(mockViewContext.Object, container.Object);
+ }
+
+ public void RenderPartialInternal(string partialViewName,
+ ViewDataDictionary viewData,
+ object model,
+ TextWriter writer,
+ params IViewEngine[] engines)
+ {
+ base.RenderPartialInternal(partialViewName, viewData, model, writer, new ViewEngineCollection(engines));
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/HtmlHelper`1Test.cs b/test/System.Web.Mvc.Test/Test/HtmlHelper`1Test.cs
new file mode 100644
index 00000000..101a5dbb
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/HtmlHelper`1Test.cs
@@ -0,0 +1,40 @@
+using Moq;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class HtmlHelper_1Test
+ {
+ [Fact]
+ public void StronglyTypedViewBagAndStronglyTypedViewDataStayInSync()
+ {
+ // Arrange
+ Mock<IViewDataContainer> viewDataContainer = new Mock<IViewDataContainer>();
+ ViewDataDictionary viewDataDictionary = new ViewDataDictionary() { { "A", 1 } };
+ viewDataContainer.Setup(container => container.ViewData).Returns(viewDataDictionary);
+
+ // Act
+ HtmlHelper<object> htmlHelper = new HtmlHelper<object>(new Mock<ViewContext>().Object, viewDataContainer.Object);
+ htmlHelper.ViewData["B"] = 2;
+ htmlHelper.ViewBag.C = 3;
+
+ // Assert
+
+ // Original ViewData should not be modified by redfined ViewData and ViewBag
+ Assert.Single((htmlHelper as HtmlHelper).ViewData.Keys);
+ Assert.Equal(1, (htmlHelper as HtmlHelper).ViewData["A"]);
+ Assert.Equal(1, (htmlHelper as HtmlHelper).ViewBag.A);
+
+ // Redefined ViewData and ViewBag should be in sync
+ Assert.Equal(3, htmlHelper.ViewData.Keys.Count);
+
+ Assert.Equal(1, htmlHelper.ViewData["A"]);
+ Assert.Equal(2, htmlHelper.ViewData["B"]);
+ Assert.Equal(3, htmlHelper.ViewData["C"]);
+
+ Assert.Equal(1, htmlHelper.ViewBag.A);
+ Assert.Equal(2, htmlHelper.ViewBag.B);
+ Assert.Equal(3, htmlHelper.ViewBag.C);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/HttpDeleteAttributeTest.cs b/test/System.Web.Mvc.Test/Test/HttpDeleteAttributeTest.cs
new file mode 100644
index 00000000..f52eda78
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/HttpDeleteAttributeTest.cs
@@ -0,0 +1,25 @@
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class HttpDeleteAttributeTest
+ {
+ [Fact]
+ public void IsValidForRequestReturnsFalseIfHttpVerbIsNotPost()
+ {
+ HttpVerbAttributeHelper.TestHttpVerbAttributeWithInvalidVerb<HttpDeleteAttribute>("POST");
+ }
+
+ [Fact]
+ public void IsValidForRequestReturnsTrueIfHttpVerbIsPost()
+ {
+ HttpVerbAttributeHelper.TestHttpVerbAttributeWithValidVerb<HttpDeleteAttribute>("DELETE");
+ }
+
+ [Fact]
+ public void IsValidForRequestThrowsIfControllerContextIsNull()
+ {
+ HttpVerbAttributeHelper.TestHttpVerbAttributeNullControllerContext<HttpDeleteAttribute>();
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/HttpFileCollectionValueProviderFactoryTest.cs b/test/System.Web.Mvc.Test/Test/HttpFileCollectionValueProviderFactoryTest.cs
new file mode 100644
index 00000000..36ba8ade
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/HttpFileCollectionValueProviderFactoryTest.cs
@@ -0,0 +1,36 @@
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class HttpFileCollectionValueProviderFactoryTest
+ {
+ [Fact]
+ public void GetValueProvider()
+ {
+ // Arrange
+ HttpFileCollectionValueProviderFactory factory = new HttpFileCollectionValueProviderFactory();
+
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.Setup(o => o.HttpContext.Request.Files.Count).Returns(0);
+
+ // Act
+ IValueProvider valueProvider = factory.GetValueProvider(mockControllerContext.Object);
+
+ // Assert
+ Assert.IsType<HttpFileCollectionValueProvider>(valueProvider);
+ }
+
+ [Fact]
+ public void GetValueProvider_ThrowsIfControllerContextIsNull()
+ {
+ // Arrange
+ HttpFileCollectionValueProviderFactory factory = new HttpFileCollectionValueProviderFactory();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { factory.GetValueProvider(null); }, "controllerContext");
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/HttpFileCollectionValueProviderTest.cs b/test/System.Web.Mvc.Test/Test/HttpFileCollectionValueProviderTest.cs
new file mode 100644
index 00000000..dcb0a7f0
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/HttpFileCollectionValueProviderTest.cs
@@ -0,0 +1,147 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class HttpFileCollectionValueProviderTest
+ {
+ private static readonly KeyValuePair<string, HttpPostedFileBase>[] _allFiles = new KeyValuePair<string, HttpPostedFileBase>[]
+ {
+ new KeyValuePair<string, HttpPostedFileBase>("foo", new MockHttpPostedFile(42, "fooFile1")),
+ new KeyValuePair<string, HttpPostedFileBase>("foo", null),
+ new KeyValuePair<string, HttpPostedFileBase>("foo", new MockHttpPostedFile(0, "") /* empty */),
+ new KeyValuePair<string, HttpPostedFileBase>("foo", new MockHttpPostedFile(100, "fooFile2")),
+ new KeyValuePair<string, HttpPostedFileBase>("bar.baz", new MockHttpPostedFile(200, "barBazFile"))
+ };
+
+ [Fact]
+ public void ContainsPrefix()
+ {
+ // Arrange
+ HttpFileCollectionValueProvider valueProvider = GetValueProvider();
+
+ // Act
+ bool result = valueProvider.ContainsPrefix("bar");
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void ContainsPrefix_DoesNotContainEmptyPrefixIfBackingStoreIsEmpty()
+ {
+ // Arrange
+ HttpFileCollectionValueProvider valueProvider = GetEmptyValueProvider();
+
+ // Act
+ bool result = valueProvider.ContainsPrefix("");
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void ContainsPrefix_ThrowsIfPrefixIsNull()
+ {
+ // Arrange
+ HttpFileCollectionValueProvider valueProvider = GetValueProvider();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { valueProvider.ContainsPrefix(null); }, "prefix");
+ }
+
+ [Fact]
+ public void GetValue()
+ {
+ // Arrange
+ HttpFileCollectionValueProvider valueProvider = GetValueProvider();
+
+ // Act
+ ValueProviderResult vpResult = valueProvider.GetValue("foo");
+
+ // Assert
+ Assert.NotNull(vpResult);
+
+ HttpPostedFileBase[] expectedRawValues = (from el in _allFiles
+ where el.Key == "foo"
+ let file = el.Value
+ let hasContent = (file != null && file.ContentLength > 0 && !String.IsNullOrEmpty(file.FileName))
+ select (hasContent) ? file : null).ToArray();
+ Assert.Equal(expectedRawValues, (HttpPostedFileBase[])vpResult.RawValue);
+ Assert.Equal("System.Web.HttpPostedFileBase[]", vpResult.AttemptedValue);
+ Assert.Equal(CultureInfo.InvariantCulture, vpResult.Culture);
+ }
+
+ [Fact]
+ public void GetValue_ReturnsNullIfKeyNotFound()
+ {
+ // Arrange
+ HttpFileCollectionValueProvider valueProvider = GetValueProvider();
+
+ // Act
+ ValueProviderResult vpResult = valueProvider.GetValue("bar");
+
+ // Assert
+ Assert.Null(vpResult);
+ }
+
+ [Fact]
+ public void GetValue_ThrowsIfKeyIsNull()
+ {
+ // Arrange
+ HttpFileCollectionValueProvider valueProvider = GetValueProvider();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { valueProvider.GetValue(null); }, "key");
+ }
+
+ private static HttpFileCollectionValueProvider GetEmptyValueProvider()
+ {
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.Setup(o => o.HttpContext.Request.Files.Count).Returns(0);
+ return new HttpFileCollectionValueProvider(mockControllerContext.Object);
+ }
+
+ private static HttpFileCollectionValueProvider GetValueProvider()
+ {
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.Setup(o => o.HttpContext.Request.Files.Count).Returns(_allFiles.Length);
+ mockControllerContext.Setup(o => o.HttpContext.Request.Files.AllKeys).Returns(_allFiles.Select(f => f.Key).ToArray());
+ for (int i = 0; i < _allFiles.Length; i++)
+ {
+ int j = i;
+ mockControllerContext.Setup(o => o.HttpContext.Request.Files[j]).Returns(_allFiles[j].Value);
+ }
+
+ return new HttpFileCollectionValueProvider(mockControllerContext.Object);
+ }
+
+ private sealed class MockHttpPostedFile : HttpPostedFileBase
+ {
+ private readonly int _contentLength;
+ private readonly string _fileName;
+
+ public MockHttpPostedFile(int contentLength, string fileName)
+ {
+ _contentLength = contentLength;
+ _fileName = fileName;
+ }
+
+ public override int ContentLength
+ {
+ get { return _contentLength; }
+ }
+
+ public override string FileName
+ {
+ get { return _fileName; }
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/HttpGetAttributeTest.cs b/test/System.Web.Mvc.Test/Test/HttpGetAttributeTest.cs
new file mode 100644
index 00000000..cb4e09a0
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/HttpGetAttributeTest.cs
@@ -0,0 +1,25 @@
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class HttpGetAttributeTest
+ {
+ [Fact]
+ public void IsValidForRequestReturnsFalseIfHttpVerbIsNotPost()
+ {
+ HttpVerbAttributeHelper.TestHttpVerbAttributeWithInvalidVerb<HttpGetAttribute>("DELETE");
+ }
+
+ [Fact]
+ public void IsValidForRequestReturnsTrueIfHttpVerbIsPost()
+ {
+ HttpVerbAttributeHelper.TestHttpVerbAttributeWithValidVerb<HttpGetAttribute>("GET");
+ }
+
+ [Fact]
+ public void IsValidForRequestThrowsIfControllerContextIsNull()
+ {
+ HttpVerbAttributeHelper.TestHttpVerbAttributeNullControllerContext<HttpGetAttribute>();
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/HttpHandlerUtilTest.cs b/test/System.Web.Mvc.Test/Test/HttpHandlerUtilTest.cs
new file mode 100644
index 00000000..fdb16d51
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/HttpHandlerUtilTest.cs
@@ -0,0 +1,153 @@
+using System.IO;
+using System.Web.Hosting;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class HttpHandlerUtilTest
+ {
+ [Fact]
+ public void WrapForServerExecute_BeginProcessRequest_DelegatesCorrectly()
+ {
+ // Arrange
+ IAsyncResult expectedResult = new Mock<IAsyncResult>().Object;
+ AsyncCallback cb = delegate { };
+
+ HttpContext httpContext = GetHttpContext();
+ Mock<IHttpAsyncHandler> mockHttpHandler = new Mock<IHttpAsyncHandler>();
+ mockHttpHandler.Setup(o => o.BeginProcessRequest(httpContext, cb, "extraData")).Returns(expectedResult);
+
+ IHttpAsyncHandler wrapper = (IHttpAsyncHandler)HttpHandlerUtil.WrapForServerExecute(mockHttpHandler.Object);
+
+ // Act
+ IAsyncResult actualResult = wrapper.BeginProcessRequest(httpContext, cb, "extraData");
+
+ // Assert
+ Assert.Equal(expectedResult, actualResult);
+ }
+
+ [Fact]
+ public void WrapForServerExecute_EndProcessRequest_DelegatesCorrectly()
+ {
+ // Arrange
+ IAsyncResult asyncResult = new Mock<IAsyncResult>().Object;
+
+ HttpContext httpContext = GetHttpContext();
+ Mock<IHttpAsyncHandler> mockHttpHandler = new Mock<IHttpAsyncHandler>();
+ mockHttpHandler.Setup(o => o.EndProcessRequest(asyncResult)).Verifiable();
+
+ IHttpAsyncHandler wrapper = (IHttpAsyncHandler)HttpHandlerUtil.WrapForServerExecute(mockHttpHandler.Object);
+
+ // Act
+ wrapper.EndProcessRequest(asyncResult);
+
+ // Assert
+ mockHttpHandler.Verify();
+ }
+
+ [Fact]
+ public void WrapForServerExecute_ProcessRequest_DelegatesCorrectly()
+ {
+ // Arrange
+ HttpContext httpContext = GetHttpContext();
+ Mock<IHttpHandler> mockHttpHandler = new Mock<IHttpHandler>();
+ mockHttpHandler.Setup(o => o.ProcessRequest(httpContext)).Verifiable();
+
+ IHttpHandler wrapper = HttpHandlerUtil.WrapForServerExecute(mockHttpHandler.Object);
+
+ // Act
+ wrapper.ProcessRequest(httpContext);
+
+ // Assert
+ mockHttpHandler.Verify();
+ }
+
+ [Fact]
+ public void WrapForServerExecute_ProcessRequest_PropagatesExceptionsIfNotHttpException()
+ {
+ // Arrange
+ HttpContext httpContext = GetHttpContext();
+ Mock<IHttpHandler> mockHttpHandler = new Mock<IHttpHandler>();
+ mockHttpHandler.Setup(o => o.ProcessRequest(httpContext)).Throws(new InvalidOperationException("Some exception."));
+
+ IHttpHandler wrapper = HttpHandlerUtil.WrapForServerExecute(mockHttpHandler.Object);
+
+ // Act & assert
+ Assert.Throws<InvalidOperationException>(
+ delegate { wrapper.ProcessRequest(httpContext); },
+ @"Some exception.");
+ }
+
+ [Fact]
+ public void WrapForServerExecute_ProcessRequest_PropagatesHttpExceptionIfStatusCode500()
+ {
+ // Arrange
+ HttpContext httpContext = GetHttpContext();
+ Mock<IHttpHandler> mockHttpHandler = new Mock<IHttpHandler>();
+ mockHttpHandler.Setup(o => o.ProcessRequest(httpContext)).Throws(new HttpException(500, "Some exception."));
+
+ IHttpHandler wrapper = HttpHandlerUtil.WrapForServerExecute(mockHttpHandler.Object);
+
+ // Act & assert
+ Assert.ThrowsHttpException(
+ delegate { wrapper.ProcessRequest(httpContext); },
+ @"Some exception.",
+ 500);
+ }
+
+ [Fact]
+ public void WrapForServerExecute_ProcessRequest_WrapsHttpExceptionIfStatusCodeNot500()
+ {
+ // Arrange
+ HttpContext httpContext = GetHttpContext();
+ Mock<IHttpHandler> mockHttpHandler = new Mock<IHttpHandler>();
+ mockHttpHandler.Setup(o => o.ProcessRequest(httpContext)).Throws(new HttpException(404, "Some exception."));
+
+ IHttpHandler wrapper = HttpHandlerUtil.WrapForServerExecute(mockHttpHandler.Object);
+
+ // Act & assert
+ HttpException outerException = Assert.ThrowsHttpException(
+ delegate { wrapper.ProcessRequest(httpContext); },
+ @"Execution of the child request failed. Please examine the InnerException for more information.",
+ 500);
+
+ HttpException innerException = outerException.InnerException as HttpException;
+ Assert.NotNull(innerException);
+ Assert.Equal(404, innerException.GetHttpCode());
+ Assert.Equal("Some exception.", innerException.Message);
+ }
+
+ [Fact]
+ public void WrapForServerExecute_ReturnsIHttpAsyncHandler()
+ {
+ // Arrange
+ IHttpAsyncHandler httpHandler = new Mock<IHttpAsyncHandler>().Object;
+
+ // Act
+ IHttpHandler wrapper = HttpHandlerUtil.WrapForServerExecute(httpHandler);
+
+ // Assert
+ Assert.True(wrapper is IHttpAsyncHandler);
+ }
+
+ [Fact]
+ public void WrapForServerExecute_ReturnsIHttpHandler()
+ {
+ // Arrange
+ IHttpHandler httpHandler = new Mock<IHttpHandler>().Object;
+
+ // Act
+ IHttpHandler wrapper = HttpHandlerUtil.WrapForServerExecute(httpHandler);
+
+ // Assert
+ Assert.False(wrapper is IHttpAsyncHandler);
+ }
+
+ private static HttpContext GetHttpContext()
+ {
+ return new HttpContext(new SimpleWorkerRequest("/", "/", "Page", "Query", TextWriter.Null));
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/HttpNotFoundResultTest.cs b/test/System.Web.Mvc.Test/Test/HttpNotFoundResultTest.cs
new file mode 100644
index 00000000..0e9007e3
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/HttpNotFoundResultTest.cs
@@ -0,0 +1,31 @@
+using Moq;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class HttpNotFoundResultTest
+ {
+ [Fact]
+ public void ExecuteResult()
+ {
+ // Arrange
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.SetupSet(c => c.HttpContext.Response.StatusCode = 404).Verifiable();
+ mockControllerContext.SetupSet(c => c.HttpContext.Response.StatusDescription = "Some description").Verifiable();
+
+ HttpNotFoundResult result = new HttpNotFoundResult("Some description");
+
+ // Act
+ result.ExecuteResult(mockControllerContext.Object);
+
+ // Assert
+ mockControllerContext.Verify();
+ }
+
+ [Fact]
+ public void StatusCode()
+ {
+ Assert.Equal(404, new HttpNotFoundResult().StatusCode);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/HttpPostAttributeTest.cs b/test/System.Web.Mvc.Test/Test/HttpPostAttributeTest.cs
new file mode 100644
index 00000000..9c9fd4f0
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/HttpPostAttributeTest.cs
@@ -0,0 +1,25 @@
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class HttpPostAttributeTest
+ {
+ [Fact]
+ public void IsValidForRequestReturnsFalseIfHttpVerbIsNotPost()
+ {
+ HttpVerbAttributeHelper.TestHttpVerbAttributeWithInvalidVerb<HttpPostAttribute>("DELETE");
+ }
+
+ [Fact]
+ public void IsValidForRequestReturnsTrueIfHttpVerbIsPost()
+ {
+ HttpVerbAttributeHelper.TestHttpVerbAttributeWithValidVerb<HttpPostAttribute>("POST");
+ }
+
+ [Fact]
+ public void IsValidForRequestThrowsIfControllerContextIsNull()
+ {
+ HttpVerbAttributeHelper.TestHttpVerbAttributeNullControllerContext<HttpPostAttribute>();
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/HttpPostedFileBaseModelBinderTest.cs b/test/System.Web.Mvc.Test/Test/HttpPostedFileBaseModelBinderTest.cs
new file mode 100644
index 00000000..9bb1ff81
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/HttpPostedFileBaseModelBinderTest.cs
@@ -0,0 +1,90 @@
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class HttpPostedFileBaseModelBinderTest
+ {
+ [Fact]
+ public void BindModelReturnsEmptyResultIfEmptyFileInputElementInPost()
+ {
+ // Arrange
+ Mock<HttpPostedFileBase> mockFile = new Mock<HttpPostedFileBase>();
+ mockFile.Setup(f => f.ContentLength).Returns(0);
+ mockFile.Setup(f => f.FileName).Returns(String.Empty);
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.SetupGet(c => c.HttpContext.Request.Files["fileName"]).Returns(mockFile.Object);
+
+ HttpPostedFileBaseModelBinder binder = new HttpPostedFileBaseModelBinder();
+ ModelBindingContext bindingContext = new ModelBindingContext() { ModelName = "fileName" };
+
+ // Act
+ object result = binder.BindModel(mockControllerContext.Object, bindingContext);
+
+ // Assert
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void BindModelReturnsNullIfNoFileInputElementInPost()
+ {
+ // Arrange
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.Setup(c => c.HttpContext.Request.Files["fileName"]).Returns((HttpPostedFileBase)null);
+
+ HttpPostedFileBaseModelBinder binder = new HttpPostedFileBaseModelBinder();
+ ModelBindingContext bindingContext = new ModelBindingContext() { ModelName = "fileName" };
+
+ // Act
+ object result = binder.BindModel(mockControllerContext.Object, bindingContext);
+
+ // Assert
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void BindModelReturnsResultIfFileFound()
+ {
+ // Arrange
+ Mock<HttpPostedFileBase> mockFile = new Mock<HttpPostedFileBase>();
+ mockFile.Setup(f => f.ContentLength).Returns(1234);
+ mockFile.Setup(f => f.FileName).Returns("somefile");
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.SetupGet(c => c.HttpContext.Request.Files["fileName"]).Returns(mockFile.Object);
+
+ HttpPostedFileBaseModelBinder binder = new HttpPostedFileBaseModelBinder();
+ ModelBindingContext bindingContext = new ModelBindingContext() { ModelName = "fileName" };
+
+ // Act
+ object result = binder.BindModel(mockControllerContext.Object, bindingContext);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Same(mockFile.Object, result);
+ }
+
+ [Fact]
+ public void BindModelThrowsIfBindingContextIsNull()
+ {
+ // Arrange
+ ControllerContext controllerContext = new Mock<ControllerContext>().Object;
+ HttpPostedFileBaseModelBinder binder = new HttpPostedFileBaseModelBinder();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { binder.BindModel(controllerContext, null); }, "bindingContext");
+ }
+
+ [Fact]
+ public void BindModelThrowsIfControllerContextIsNull()
+ {
+ // Arrange
+ HttpPostedFileBaseModelBinder binder = new HttpPostedFileBaseModelBinder();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { binder.BindModel(null, null); }, "controllerContext");
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/HttpPutAttributeTest.cs b/test/System.Web.Mvc.Test/Test/HttpPutAttributeTest.cs
new file mode 100644
index 00000000..f7894e9e
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/HttpPutAttributeTest.cs
@@ -0,0 +1,25 @@
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class HttpPutAttributeTest
+ {
+ [Fact]
+ public void IsValidForRequestReturnsFalseIfHttpVerbIsNotPost()
+ {
+ HttpVerbAttributeHelper.TestHttpVerbAttributeWithInvalidVerb<HttpPutAttribute>("GET");
+ }
+
+ [Fact]
+ public void IsValidForRequestReturnsTrueIfHttpVerbIsPost()
+ {
+ HttpVerbAttributeHelper.TestHttpVerbAttributeWithValidVerb<HttpPutAttribute>("PUT");
+ }
+
+ [Fact]
+ public void IsValidForRequestThrowsIfControllerContextIsNull()
+ {
+ HttpVerbAttributeHelper.TestHttpVerbAttributeNullControllerContext<HttpPutAttribute>();
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/HttpRequestExtensionsTest.cs b/test/System.Web.Mvc.Test/Test/HttpRequestExtensionsTest.cs
new file mode 100644
index 00000000..4ffbf9a5
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/HttpRequestExtensionsTest.cs
@@ -0,0 +1,50 @@
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ [CLSCompliant(false)]
+ public class HttpRequestExtensionsTest
+ {
+ [Fact]
+ public void GetHttpMethodOverrideWithNullRequestThrows()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => HttpRequestExtensions.GetHttpMethodOverride(null),
+ "request"
+ );
+ }
+
+ [Theory]
+ [InlineData("GET", "PUT", null, null, "GET")] // Cannot override GET with header PUT
+ [InlineData("GET", null, "PUT", null, "GET")] // Cannot override GET with form PUT
+ [InlineData("GET", null, null, "PUT", "GET")] // Cannot override GET with query string PUT
+ [InlineData("PUT", "GET", null, null, "PUT")] // Cannot override PUT with GET
+ [InlineData("PUT", "POST", null, null, "PUT")] // Cannot override PUT with POST
+ [InlineData("POST", "GET", null, null, "POST")] // Cannot override POST with GET
+ [InlineData("POST", "POST", null, null, "POST")] // Cannot override POST with POST
+ [InlineData("POST", "PUT", null, null, "PUT")] // Can override POST with header PUT
+ [InlineData("POST", null, "PUT", null, "PUT")] // Can override POST with form PUT
+ [InlineData("POST", null, null, "PUT", "PUT")] // Can override POST with query string PUT
+ [InlineData("POST", "PUT", "BOGUS", null, "PUT")] // Header override wins over form override
+ [InlineData("POST", "PUT", null, "BOGUS", "PUT")] // Header override wins over query string override
+ [InlineData("POST", null, "PUT", "BOGUS", "PUT")] // Form override wins over query string override
+ public void TestHttpMethodOverride(string httpRequestVerb,
+ string httpHeaderVerb,
+ string httpFormVerb,
+ string httpQueryStringVerb,
+ string expectedMethod)
+ {
+ // Arrange
+ ControllerContext context = AcceptVerbsAttributeTest.GetControllerContextWithHttpVerb(httpRequestVerb, httpHeaderVerb, httpFormVerb, httpQueryStringVerb);
+
+ // Act
+ string methodOverride = context.RequestContext.HttpContext.Request.GetHttpMethodOverride();
+
+ // Assert
+ Assert.Equal(expectedMethod, methodOverride);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/HttpStatusCodeResultTest.cs b/test/System.Web.Mvc.Test/Test/HttpStatusCodeResultTest.cs
new file mode 100644
index 00000000..c099044b
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/HttpStatusCodeResultTest.cs
@@ -0,0 +1,96 @@
+using System.Net;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class HttpStatusCodeResultTest
+ {
+ [Fact]
+ public void ExecuteResult()
+ {
+ // Arrange
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.SetupSet(c => c.HttpContext.Response.StatusCode = 666).Verifiable();
+
+ HttpStatusCodeResult result = new HttpStatusCodeResult(666);
+
+ // Act
+ result.ExecuteResult(mockControllerContext.Object);
+
+ // Assert
+ mockControllerContext.Verify();
+ }
+
+ [Fact]
+ public void ExecuteResultWithDescription()
+ {
+ // Arrange
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.SetupSet(c => c.HttpContext.Response.StatusCode = 666).Verifiable();
+ mockControllerContext.SetupSet(c => c.HttpContext.Response.StatusDescription = "Foo Bar").Verifiable();
+ HttpStatusCodeResult result = new HttpStatusCodeResult(666, "Foo Bar");
+
+ // Act
+ result.ExecuteResult(mockControllerContext.Object);
+
+ // Assert
+ mockControllerContext.Verify();
+ }
+
+ [Fact]
+ public void ExecuteResultWithNullContextThrows()
+ {
+ // Act and Assert
+ Assert.ThrowsArgumentNull(delegate { new HttpStatusCodeResult(1).ExecuteResult(context: null); }, "context");
+ }
+
+ [Fact]
+ public void StatusCode()
+ {
+ // Assert
+ Assert.Equal(123, new HttpStatusCodeResult(123).StatusCode);
+ Assert.Equal(234, new HttpStatusCodeResult(234, "foobar").StatusCode);
+ }
+
+ [Fact]
+ public void StatusDescription()
+ {
+ // Assert
+ Assert.Null(new HttpStatusCodeResult(123).StatusDescription);
+ Assert.Equal("foobar", new HttpStatusCodeResult(234, "foobar").StatusDescription);
+ }
+
+ [Fact]
+ public void HttpStatusCodeAndStatusDescription()
+ {
+ // Arrange
+ int unusedStatusCode = 306;
+
+ // Act
+ HttpStatusCodeResult result = new HttpStatusCodeResult(HttpStatusCode.Unused, "foobar");
+
+ // Assert
+ Assert.Equal(unusedStatusCode, result.StatusCode);
+ Assert.Equal("foobar", result.StatusDescription);
+ }
+
+ [Fact]
+ public void ExecuteResultWithHttpStatusCode()
+ {
+ // Arrange
+ int unusedStatusCode = 306;
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.SetupSet(c => c.HttpContext.Response.StatusCode = unusedStatusCode).Verifiable();
+
+ HttpStatusCodeResult result = new HttpStatusCodeResult(HttpStatusCode.Unused);
+
+ // Act
+ result.ExecuteResult(mockControllerContext.Object);
+
+ // Assert
+ mockControllerContext.Verify();
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/HttpUnauthorizedResultTest.cs b/test/System.Web.Mvc.Test/Test/HttpUnauthorizedResultTest.cs
new file mode 100644
index 00000000..70cba3ee
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/HttpUnauthorizedResultTest.cs
@@ -0,0 +1,31 @@
+using Moq;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class HttpUnauthorizedResultTest
+ {
+ [Fact]
+ public void ExecuteResult()
+ {
+ // Arrange
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.SetupSet(c => c.HttpContext.Response.StatusCode = 401).Verifiable();
+ mockControllerContext.SetupSet(c => c.HttpContext.Response.StatusDescription = "Some description").Verifiable();
+
+ HttpUnauthorizedResult result = new HttpUnauthorizedResult("Some description");
+
+ // Act
+ result.ExecuteResult(mockControllerContext.Object);
+
+ // Assert
+ mockControllerContext.Verify();
+ }
+
+ [Fact]
+ public void StatusCode()
+ {
+ Assert.Equal(401, new HttpUnauthorizedResult().StatusCode);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/HttpVerbAttributeHelper.cs b/test/System.Web.Mvc.Test/Test/HttpVerbAttributeHelper.cs
new file mode 100644
index 00000000..3436db1c
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/HttpVerbAttributeHelper.cs
@@ -0,0 +1,46 @@
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ internal static class HttpVerbAttributeHelper
+ {
+ internal static void TestHttpVerbAttributeNullControllerContext<THttpVerb>()
+ where THttpVerb : ActionMethodSelectorAttribute, new()
+ {
+ // Arrange
+ ActionMethodSelectorAttribute attribute = new THttpVerb();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { attribute.IsValidForRequest(null, null); }, "controllerContext");
+ }
+
+ internal static void TestHttpVerbAttributeWithValidVerb<THttpVerb>(string validVerb)
+ where THttpVerb : ActionMethodSelectorAttribute, new()
+ {
+ // Arrange
+ ActionMethodSelectorAttribute attribute = new THttpVerb();
+ ControllerContext context = AcceptVerbsAttributeTest.GetControllerContextWithHttpVerb(validVerb);
+
+ // Act
+ bool result = attribute.IsValidForRequest(context, null);
+
+ // Assert
+ Assert.True(result);
+ }
+
+ internal static void TestHttpVerbAttributeWithInvalidVerb<THttpVerb>(string invalidVerb)
+ where THttpVerb : ActionMethodSelectorAttribute, new()
+ {
+ // Arrange
+ ActionMethodSelectorAttribute attribute = new THttpVerb();
+ ControllerContext context = AcceptVerbsAttributeTest.GetControllerContextWithHttpVerb(invalidVerb);
+
+ // Act
+ bool result = attribute.IsValidForRequest(context, null);
+
+ // Assert
+ Assert.False(result);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/JavaScriptResultTest.cs b/test/System.Web.Mvc.Test/Test/JavaScriptResultTest.cs
new file mode 100644
index 00000000..8902b9f2
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/JavaScriptResultTest.cs
@@ -0,0 +1,69 @@
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class JavaScriptResultTest
+ {
+ [Fact]
+ public void AllPropertiesDefaultToNull()
+ {
+ // Act
+ JavaScriptResult result = new JavaScriptResult();
+
+ // Assert
+ Assert.Null(result.Script);
+ }
+
+ [Fact]
+ public void ExecuteResult()
+ {
+ // Arrange
+ string script = "alert('foo');";
+ string contentType = "application/x-javascript";
+
+ // Arrange expectations
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>(MockBehavior.Strict);
+ mockControllerContext.SetupSet(c => c.HttpContext.Response.ContentType = contentType).Verifiable();
+ mockControllerContext.Setup(c => c.HttpContext.Response.Write(script)).Verifiable();
+
+ JavaScriptResult result = new JavaScriptResult
+ {
+ Script = script
+ };
+
+ // Act
+ result.ExecuteResult(mockControllerContext.Object);
+
+ // Assert
+ mockControllerContext.Verify();
+ }
+
+ [Fact]
+ public void ExecuteResultWithNullContextThrows()
+ {
+ Assert.ThrowsArgumentNull(
+ delegate { new JavaScriptResult().ExecuteResult(null /* context */); }, "context");
+ }
+
+ [Fact]
+ public void NullScriptIsNotOutput()
+ {
+ // Arrange
+ string contentType = "application/x-javascript";
+
+ // Arrange expectations
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.SetupSet(c => c.HttpContext.Response.ContentType = contentType).Verifiable();
+
+ JavaScriptResult result = new JavaScriptResult();
+
+ // Act
+ result.ExecuteResult(mockControllerContext.Object);
+
+ // Assert
+ mockControllerContext.Verify();
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/JsonResultTest.cs b/test/System.Web.Mvc.Test/Test/JsonResultTest.cs
new file mode 100644
index 00000000..de43d1fc
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/JsonResultTest.cs
@@ -0,0 +1,292 @@
+using System.Text;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class JsonResultTest
+ {
+ private static readonly object _jsonData = new object[] { 1, 2, "three", "four" };
+ private static readonly string _jsonSerializedData = "[1,2,\"three\",\"four\"]";
+
+ [Fact]
+ public void PropertyDefaults()
+ {
+ // Act
+ JsonResult result = new JsonResult();
+
+ // Assert
+ Assert.Null(result.Data);
+ Assert.Null(result.ContentEncoding);
+ Assert.Null(result.ContentType);
+ Assert.Null(result.MaxJsonLength);
+ Assert.Null(result.RecursionLimit);
+ Assert.Equal(JsonRequestBehavior.DenyGet, result.JsonRequestBehavior);
+ }
+
+ [Fact]
+ public void EmptyContentTypeRendersDefault()
+ {
+ // Arrange
+ object data = _jsonData;
+ Encoding contentEncoding = Encoding.UTF8;
+
+ // Arrange expectations
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>(MockBehavior.Strict);
+ mockControllerContext.SetupGet(c => c.HttpContext.Request.HttpMethod).Returns("POST").Verifiable();
+ mockControllerContext.SetupSet(c => c.HttpContext.Response.ContentType = "application/json").Verifiable();
+ mockControllerContext.SetupSet(c => c.HttpContext.Response.ContentEncoding = contentEncoding).Verifiable();
+ mockControllerContext.Setup(c => c.HttpContext.Response.Write(_jsonSerializedData)).Verifiable();
+
+ JsonResult result = new JsonResult
+ {
+ Data = data,
+ ContentType = String.Empty,
+ ContentEncoding = contentEncoding
+ };
+
+ // Act
+ result.ExecuteResult(mockControllerContext.Object);
+
+ // Assert
+ mockControllerContext.Verify();
+ }
+
+ [Fact]
+ public void ExecuteResult()
+ {
+ // Arrange
+ object data = _jsonData;
+ string contentType = "Some content type.";
+ Encoding contentEncoding = Encoding.UTF8;
+
+ // Arrange expectations
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>(MockBehavior.Strict);
+ mockControllerContext.SetupGet(c => c.HttpContext.Request.HttpMethod).Returns("POST").Verifiable();
+ mockControllerContext.SetupSet(c => c.HttpContext.Response.ContentType = contentType).Verifiable();
+ mockControllerContext.SetupSet(c => c.HttpContext.Response.ContentEncoding = contentEncoding).Verifiable();
+ mockControllerContext.Setup(c => c.HttpContext.Response.Write(_jsonSerializedData)).Verifiable();
+
+ JsonResult result = new JsonResult
+ {
+ Data = data,
+ ContentType = contentType,
+ ContentEncoding = contentEncoding
+ };
+
+ // Act
+ result.ExecuteResult(mockControllerContext.Object);
+
+ // Assert
+ mockControllerContext.Verify();
+ }
+
+ [Fact]
+ public void ExecuteResultWithNullContextThrows()
+ {
+ Assert.ThrowsArgumentNull(
+ delegate { new JsonResult().ExecuteResult(null /* context */); }, "context");
+ }
+
+ [Fact]
+ public void NullContentIsNotOutput()
+ {
+ // Arrange
+ string contentType = "Some content type.";
+ Encoding contentEncoding = Encoding.UTF8;
+
+ // Arrange expectations
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.SetupGet(c => c.HttpContext.Request.HttpMethod).Returns("POST").Verifiable();
+ mockControllerContext.SetupSet(c => c.HttpContext.Response.ContentType = contentType).Verifiable();
+ mockControllerContext.SetupSet(c => c.HttpContext.Response.ContentEncoding = contentEncoding).Verifiable();
+
+ JsonResult result = new JsonResult
+ {
+ ContentType = contentType,
+ ContentEncoding = contentEncoding
+ };
+
+ // Act
+ result.ExecuteResult(mockControllerContext.Object);
+
+ // Assert
+ mockControllerContext.Verify();
+ }
+
+ [Fact]
+ public void NullContentEncodingIsNotOutput()
+ {
+ // Arrange
+ object data = _jsonData;
+ string contentType = "Some content type.";
+
+ // Arrange expectations
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>(MockBehavior.Strict);
+ mockControllerContext.SetupGet(c => c.HttpContext.Request.HttpMethod).Returns("POST").Verifiable();
+ mockControllerContext.SetupSet(c => c.HttpContext.Response.ContentType = contentType).Verifiable();
+ mockControllerContext.Setup(c => c.HttpContext.Response.Write(_jsonSerializedData)).Verifiable();
+
+ JsonResult result = new JsonResult
+ {
+ Data = data,
+ ContentType = contentType,
+ };
+
+ // Act
+ result.ExecuteResult(mockControllerContext.Object);
+
+ // Assert
+ mockControllerContext.Verify();
+ }
+
+ [Fact]
+ public void NullContentTypeRendersDefault()
+ {
+ // Arrange
+ object data = _jsonData;
+ Encoding contentEncoding = Encoding.UTF8;
+
+ // Arrange expectations
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>(MockBehavior.Strict);
+ mockControllerContext.SetupGet(c => c.HttpContext.Request.HttpMethod).Returns("POST").Verifiable();
+ mockControllerContext.SetupSet(c => c.HttpContext.Response.ContentType = "application/json").Verifiable();
+ mockControllerContext.SetupSet(c => c.HttpContext.Response.ContentEncoding = contentEncoding).Verifiable();
+ mockControllerContext.Setup(c => c.HttpContext.Response.Write(_jsonSerializedData)).Verifiable();
+
+ JsonResult result = new JsonResult
+ {
+ Data = data,
+ ContentEncoding = contentEncoding
+ };
+
+ // Act
+ result.ExecuteResult(mockControllerContext.Object);
+
+ // Assert
+ mockControllerContext.Verify();
+ }
+
+ [Fact]
+ public void NullMaxJsonLengthDefaultIsUsed()
+ {
+ // Arrange
+ string data = new String('1', 2100000);
+
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.SetupGet(c => c.HttpContext.Request.HttpMethod).Returns("POST").Verifiable();
+ mockControllerContext.SetupSet(c => c.HttpContext.Response.ContentType = "application/json").Verifiable();
+
+ JsonResult result = new JsonResult
+ {
+ Data = data
+ };
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ () => result.ExecuteResult(mockControllerContext.Object),
+ "Error during serialization or deserialization using the JSON JavaScriptSerializer. The length of the string exceeds the value set on the maxJsonLength property.");
+ }
+
+ [Fact]
+ public void MaxJsonLengthIsPassedToSerializer()
+ {
+ // Arrange
+ string data = new String('1', 2100000);
+ string jsonData = "\"" + data + "\"";
+
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.SetupGet(c => c.HttpContext.Request.HttpMethod).Returns("POST").Verifiable();
+ mockControllerContext.SetupSet(c => c.HttpContext.Response.ContentType = "application/json").Verifiable();
+ mockControllerContext.Setup(c => c.HttpContext.Response.Write(jsonData)).Verifiable();
+
+ JsonResult result = new JsonResult
+ {
+ Data = data,
+ MaxJsonLength = 2200000
+ };
+
+ // Act
+ result.ExecuteResult(mockControllerContext.Object);
+
+ // Assert
+ mockControllerContext.Verify();
+ }
+
+ [Fact]
+ public void RecursionLimitIsPassedToSerilizer()
+ {
+ // Arrange
+ Tuple<string, Tuple<string, Tuple<string, string>>> data =
+ new Tuple<string, Tuple<string, Tuple<string, string>>>("key1",
+ new Tuple<string, Tuple<string, string>>("key2",
+ new Tuple<string, string>("key3", "value")
+ )
+ );
+ string jsonData = "{\"Item1\":\"key1\",\"Item2\":{\"Item1\":\"key2\",\"Item2\":{\"Item1\":\"key3\",\"Item2\":\"value\"}}}";
+
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.SetupGet(c => c.HttpContext.Request.HttpMethod).Returns("POST").Verifiable();
+ mockControllerContext.SetupSet(c => c.HttpContext.Response.ContentType = "application/json").Verifiable();
+ mockControllerContext.Setup(c => c.HttpContext.Response.Write(jsonData)).Verifiable();
+
+ JsonResult result = new JsonResult
+ {
+ Data = data,
+ RecursionLimit = 2
+ };
+
+ // Act & Assert
+ Assert.Throws<ArgumentException>(
+ () => result.ExecuteResult(mockControllerContext.Object),
+ "RecursionLimit exceeded.");
+ }
+
+ [Fact]
+ public void NullRecursionLimitDefaultIsUsed()
+ {
+ // Arrange
+ Tuple<string, Tuple<string, Tuple<string, string>>> data =
+ new Tuple<string, Tuple<string, Tuple<string, string>>>("key1",
+ new Tuple<string, Tuple<string, string>>("key2",
+ new Tuple<string, string>("key3", "value")
+ )
+ );
+ string jsonData = "{\"Item1\":\"key1\",\"Item2\":{\"Item1\":\"key2\",\"Item2\":{\"Item1\":\"key3\",\"Item2\":\"value\"}}}";
+
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.SetupGet(c => c.HttpContext.Request.HttpMethod).Returns("POST").Verifiable();
+ mockControllerContext.SetupSet(c => c.HttpContext.Response.ContentType = "application/json").Verifiable();
+ mockControllerContext.Setup(c => c.HttpContext.Response.Write(jsonData)).Verifiable();
+
+ JsonResult result = new JsonResult
+ {
+ Data = data
+ };
+
+ // Act
+ result.ExecuteResult(mockControllerContext.Object);
+
+ // Assert
+ mockControllerContext.Verify();
+ }
+
+ [Fact]
+ public void GetRequestBlocked()
+ {
+ // Arrange expectations
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>(MockBehavior.Strict);
+ mockControllerContext.SetupGet(c => c.HttpContext.Request.HttpMethod).Returns("GET").Verifiable();
+
+ JsonResult result = new JsonResult();
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ () => result.ExecuteResult(mockControllerContext.Object),
+ "This request has been blocked because sensitive information could be disclosed to third party web sites when this is used in a GET request. To allow GET requests, set JsonRequestBehavior to AllowGet.");
+
+ mockControllerContext.Verify();
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/JsonValueProviderFactoryTest.cs b/test/System.Web.Mvc.Test/Test/JsonValueProviderFactoryTest.cs
new file mode 100644
index 00000000..69a51679
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/JsonValueProviderFactoryTest.cs
@@ -0,0 +1,152 @@
+using System.Globalization;
+using System.IO;
+using System.Text;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class JsonValueProviderFactoryTest
+ {
+ [Fact]
+ public void GetValueProvider_NullControllerContext_ThrowsException()
+ {
+ JsonValueProviderFactory factory = new JsonValueProviderFactory();
+
+ Assert.ThrowsArgumentNull(delegate() { factory.GetValueProvider(controllerContext: null); }, "controllerContext");
+ }
+
+ [Fact]
+ public void GetValueProvider_SimpleArrayJsonObject()
+ {
+ const string jsonString = @"
+[ ""abc"", null, ""foobar"" ]
+";
+ ControllerContext cc = GetJsonEnabledControllerContext(jsonString);
+ JsonValueProviderFactory factory = new JsonValueProviderFactory();
+
+ // Act & assert
+ IValueProvider valueProvider = factory.GetValueProvider(cc);
+ Assert.True(valueProvider.ContainsPrefix("[0]"));
+ Assert.True(valueProvider.ContainsPrefix("[2]"));
+ Assert.False(valueProvider.ContainsPrefix("[3]"));
+
+ ValueProviderResult vpResult1 = valueProvider.GetValue("[0]");
+ Assert.Equal("abc", vpResult1.AttemptedValue);
+ Assert.Equal(CultureInfo.CurrentCulture, vpResult1.Culture);
+
+ // null values should exist in the backing store as actual entries
+ ValueProviderResult vpResult2 = valueProvider.GetValue("[1]");
+ Assert.NotNull(vpResult2);
+ Assert.Null(vpResult2.RawValue);
+ }
+
+ [Fact]
+ public void GetValueProvider_SimpleDictionaryJsonObject()
+ {
+ const string jsonString = @"
+{ ""FirstName"":""John"",
+ ""LastName"": ""Doe""
+}";
+
+ ControllerContext cc = GetJsonEnabledControllerContext(jsonString);
+ JsonValueProviderFactory factory = new JsonValueProviderFactory();
+
+ // Act & assert
+ IValueProvider valueProvider = factory.GetValueProvider(cc);
+ Assert.True(valueProvider.ContainsPrefix("firstname"));
+
+ ValueProviderResult vpResult1 = valueProvider.GetValue("firstname");
+ Assert.Equal("John", vpResult1.AttemptedValue);
+ Assert.Equal(CultureInfo.CurrentCulture, vpResult1.Culture);
+ }
+
+ [Fact]
+ public void GetValueProvider_ComplexJsonObject()
+ {
+ // Arrange
+ const string jsonString = @"
+[
+ {
+ ""BillingAddress"": {
+ ""Street"": ""1 Microsoft Way"",
+ ""City"": ""Redmond"",
+ ""State"": ""WA"",
+ ""ZIP"": 98052 },
+ ""ShippingAddress"": {
+ ""Street"": ""123 Anywhere Ln"",
+ ""City"": ""Anytown"",
+ ""State"": ""ZZ"",
+ ""ZIP"": 99999 }
+ },
+ {
+ ""Enchiladas"": [ ""Delicious"", ""Nutritious""]
+ }
+]
+";
+
+ ControllerContext cc = GetJsonEnabledControllerContext(jsonString);
+ JsonValueProviderFactory factory = new JsonValueProviderFactory();
+
+ // Act & assert
+ IValueProvider valueProvider = factory.GetValueProvider(cc);
+ Assert.NotNull(valueProvider);
+
+ Assert.True(valueProvider.ContainsPrefix("[0].billingaddress"));
+ Assert.Null(valueProvider.GetValue("[0].billingaddress"));
+
+ Assert.True(valueProvider.ContainsPrefix("[0].billingaddress.street"));
+ Assert.NotNull(valueProvider.GetValue("[0].billingaddress.street"));
+
+ ValueProviderResult vpResult1 = valueProvider.GetValue("[1].enchiladas[0]");
+ Assert.NotNull(vpResult1);
+ Assert.Equal("Delicious", vpResult1.AttemptedValue);
+ Assert.Equal(CultureInfo.CurrentCulture, vpResult1.Culture);
+ }
+
+ [Fact]
+ public void GetValueProvider_NoJsonBody_ReturnsNull()
+ {
+ // Arrange
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.Setup(o => o.HttpContext.Request.ContentType).Returns("application/json");
+ mockControllerContext.Setup(o => o.HttpContext.Request.InputStream).Returns(new MemoryStream());
+
+ JsonValueProviderFactory factory = new JsonValueProviderFactory();
+
+ // Act
+ IValueProvider valueProvider = factory.GetValueProvider(mockControllerContext.Object);
+
+ // Assert
+ Assert.Null(valueProvider);
+ }
+
+ [Fact]
+ public void GetValueProvider_NotJsonRequest_ReturnsNull()
+ {
+ // Arrange
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.Setup(o => o.HttpContext.Request.ContentType).Returns("not JSON");
+
+ JsonValueProviderFactory factory = new JsonValueProviderFactory();
+
+ // Act
+ IValueProvider valueProvider = factory.GetValueProvider(mockControllerContext.Object);
+
+ // Assert
+ Assert.Null(valueProvider);
+ }
+
+ private static ControllerContext GetJsonEnabledControllerContext(string jsonString)
+ {
+ byte[] jsonBytes = Encoding.UTF8.GetBytes(jsonString);
+ MemoryStream jsonStream = new MemoryStream(jsonBytes);
+
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.Setup(o => o.HttpContext.Request.ContentType).Returns("application/json");
+ mockControllerContext.Setup(o => o.HttpContext.Request.InputStream).Returns(jsonStream);
+ return mockControllerContext.Object;
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/LinqBinaryModelBinderTest.cs b/test/System.Web.Mvc.Test/Test/LinqBinaryModelBinderTest.cs
new file mode 100644
index 00000000..70dd4651
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/LinqBinaryModelBinderTest.cs
@@ -0,0 +1,120 @@
+using System.Data.Linq;
+using Microsoft.Web.UnitTestUtil;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class LinqBinaryModelBinderTest
+ {
+ [Fact]
+ public void BindModelWithNonExistentValueReturnsNull()
+ {
+ // Arrange
+ SimpleValueProvider valueProvider = new SimpleValueProvider()
+ {
+ { "foo", null }
+ };
+
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelName = "foo",
+ ValueProvider = valueProvider
+ };
+
+ LinqBinaryModelBinder binder = new LinqBinaryModelBinder();
+
+ // Act
+ object binderResult = binder.BindModel(null, bindingContext);
+
+ // Assert
+ Assert.Null(binderResult);
+ }
+
+ [Fact]
+ public void BinderWithEmptyStringValueReturnsNull()
+ {
+ // Arrange
+ SimpleValueProvider valueProvider = new SimpleValueProvider()
+ {
+ { "foo", "" }
+ };
+
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelName = "foo",
+ ValueProvider = valueProvider
+ };
+
+ LinqBinaryModelBinder binder = new LinqBinaryModelBinder();
+
+ // Act
+ object binderResult = binder.BindModel(null, bindingContext);
+
+ // Assert
+ Assert.Null(binderResult);
+ }
+
+ [Fact]
+ public void BindModelThrowsIfBindingContextIsNull()
+ {
+ // Arrange
+ LinqBinaryModelBinder binder = new LinqBinaryModelBinder();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { binder.BindModel(null, null); }, "bindingContext");
+ }
+
+ [Fact]
+ public void BindModelWithBase64QuotedValueReturnsBinary()
+ {
+ // Arrange
+ string base64Value = ByteArrayModelBinderTest.Base64TestString;
+
+ SimpleValueProvider valueProvider = new SimpleValueProvider()
+ {
+ { "foo", "\"" + base64Value + "\"" }
+ };
+
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelName = "foo",
+ ValueProvider = valueProvider
+ };
+
+ LinqBinaryModelBinder binder = new LinqBinaryModelBinder();
+
+ // Act
+ Binary boundValue = binder.BindModel(null, bindingContext) as Binary;
+
+ // Assert
+ Assert.Equal(ByteArrayModelBinderTest.Base64TestBytes, boundValue);
+ }
+
+ [Fact]
+ public void BindModelWithBase64UnquotedValueReturnsBinary()
+ {
+ // Arrange
+ string base64Value = ByteArrayModelBinderTest.Base64TestString;
+ SimpleValueProvider valueProvider = new SimpleValueProvider()
+ {
+ { "foo", base64Value }
+ };
+
+ ModelBindingContext bindingContext = new ModelBindingContext()
+ {
+ ModelName = "foo",
+ ValueProvider = valueProvider
+ };
+
+ LinqBinaryModelBinder binder = new LinqBinaryModelBinder();
+
+ // Act
+ Binary boundValue = binder.BindModel(null, bindingContext) as Binary;
+
+ // Assert
+ Assert.Equal(ByteArrayModelBinderTest.Base64TestBytes, boundValue);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/MockBuildManager.cs b/test/System.Web.Mvc.Test/Test/MockBuildManager.cs
new file mode 100644
index 00000000..a5f6732c
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/MockBuildManager.cs
@@ -0,0 +1,80 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+
+namespace System.Web.Mvc.Test
+{
+ // Custom mock IBuildManager since the mock framework doesn't support mocking internal types
+ public class MockBuildManager : IBuildManager
+ {
+ private Assembly[] _referencedAssemblies;
+
+ private Type _compiledType;
+ private string _expectedVirtualPath;
+ private bool _fileExists = true;
+
+ public readonly Dictionary<string, Stream> CachedFileStore = new Dictionary<string, Stream>(StringComparer.OrdinalIgnoreCase);
+
+ public MockBuildManager()
+ : this(new Assembly[] { typeof(MockBuildManager).Assembly })
+ {
+ }
+
+ public MockBuildManager(Assembly[] referencedAssemblies)
+ {
+ _referencedAssemblies = referencedAssemblies;
+ }
+
+ public MockBuildManager(string expectedVirtualPath, bool fileExists)
+ {
+ _expectedVirtualPath = expectedVirtualPath;
+ _fileExists = fileExists;
+ }
+
+ public MockBuildManager(string expectedVirtualPath, Type compiledType)
+ {
+ _expectedVirtualPath = expectedVirtualPath;
+ _compiledType = compiledType;
+ }
+
+ bool IBuildManager.FileExists(string virtualPath)
+ {
+ if (_expectedVirtualPath == virtualPath)
+ {
+ return _fileExists;
+ }
+
+ throw new InvalidOperationException("Unexpected call to IBuildManager.FileExists()");
+ }
+
+ public Type GetCompiledType(string virtualPath)
+ {
+ if (_expectedVirtualPath == virtualPath)
+ {
+ return _compiledType;
+ }
+
+ throw new InvalidOperationException("Unexpected call to IBuildManager.GetCompiledType()");
+ }
+
+ ICollection IBuildManager.GetReferencedAssemblies()
+ {
+ return _referencedAssemblies;
+ }
+
+ Stream IBuildManager.ReadCachedFile(string fileName)
+ {
+ Stream stream;
+ CachedFileStore.TryGetValue(fileName, out stream);
+ return stream;
+ }
+
+ Stream IBuildManager.CreateCachedFile(string fileName)
+ {
+ MemoryStream stream = new MemoryStream();
+ CachedFileStore[fileName] = stream;
+ return stream;
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/MockHelpers.cs b/test/System.Web.Mvc.Test/Test/MockHelpers.cs
new file mode 100644
index 00000000..307601e3
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/MockHelpers.cs
@@ -0,0 +1,13 @@
+using Moq;
+using Moq.Language.Flow;
+
+namespace System.Web.Mvc.Test
+{
+ public static class MockHelpers
+ {
+ public static ISetup<HttpContextBase> ExpectMvcVersionResponseHeader(this Mock<HttpContextBase> mock)
+ {
+ return mock.Setup(r => r.Response.AppendHeader(MvcHandler.MvcVersionHeaderName, "4.0"));
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/MockableUnvalidatedRequestValues.cs b/test/System.Web.Mvc.Test/Test/MockableUnvalidatedRequestValues.cs
new file mode 100644
index 00000000..8de57b53
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/MockableUnvalidatedRequestValues.cs
@@ -0,0 +1,11 @@
+using System.Collections.Specialized;
+
+namespace System.Web.Mvc.Test
+{
+ public abstract class MockableUnvalidatedRequestValues : IUnvalidatedRequestValues
+ {
+ public abstract NameValueCollection Form { get; }
+ public abstract NameValueCollection QueryString { get; }
+ public abstract string this[string key] { get; }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ModelBinderAttributeTest.cs b/test/System.Web.Mvc.Test/Test/ModelBinderAttributeTest.cs
new file mode 100644
index 00000000..3bfeef21
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ModelBinderAttributeTest.cs
@@ -0,0 +1,85 @@
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ModelBinderAttributeTest
+ {
+ [Fact]
+ public void ConstructorWithInvalidBinderTypeThrows()
+ {
+ // Arrange
+ Type badType = typeof(string);
+
+ // Act & Assert
+ Assert.Throws<ArgumentException>(
+ delegate { new ModelBinderAttribute(badType); },
+ "The type 'System.String' does not implement the IModelBinder interface.\r\nParameter name: binderType");
+ }
+
+ [Fact]
+ public void ConstructorWithNullBinderTypeThrows()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { new ModelBinderAttribute(null); }, "binderType");
+ }
+
+ [Fact]
+ public void BinderTypeProperty()
+ {
+ // Arrange
+ Type binderType = typeof(GoodConverter);
+ ModelBinderAttribute attr = new ModelBinderAttribute(binderType);
+
+ // Act & Assert
+ Assert.Same(binderType, attr.BinderType);
+ }
+
+ [Fact]
+ public void GetBinder()
+ {
+ // Arrange
+ ModelBinderAttribute attr = new ModelBinderAttribute(typeof(GoodConverter));
+
+ // Act
+ IModelBinder binder = attr.GetBinder();
+
+ // Assert
+ Assert.IsType<GoodConverter>(binder);
+ }
+
+ [Fact]
+ public void GetBinderWithBadConstructorThrows()
+ {
+ // Arrange
+ ModelBinderAttribute attr = new ModelBinderAttribute(typeof(BadConverter));
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ delegate { attr.GetBinder(); },
+ "An error occurred when trying to create the IModelBinder 'System.Web.Mvc.Test.ModelBinderAttributeTest+BadConverter'. Make sure that the binder has a public parameterless constructor.");
+ }
+
+ private class GoodConverter : IModelBinder
+ {
+ public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ private class BadConverter : IModelBinder
+ {
+ // no public parameterless constructor
+ public BadConverter(string s)
+ {
+ }
+
+ public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ModelBinderDictionaryTest.cs b/test/System.Web.Mvc.Test/Test/ModelBinderDictionaryTest.cs
new file mode 100644
index 00000000..3a4e57fa
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ModelBinderDictionaryTest.cs
@@ -0,0 +1,191 @@
+using System.Web.TestUtil;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ModelBinderDictionaryTest
+ {
+ [Fact]
+ public void DefaultBinderIsInstanceOfDefaultModelBinder()
+ {
+ // Arrange
+ ModelBinderDictionary binders = new ModelBinderDictionary();
+
+ // Act
+ IModelBinder defaultBinder = binders.DefaultBinder;
+
+ // Assert
+ Assert.IsType<DefaultModelBinder>(defaultBinder);
+ }
+
+ [Fact]
+ public void DefaultBinderProperty()
+ {
+ // Arrange
+ ModelBinderDictionary binders = new ModelBinderDictionary();
+ IModelBinder binder = new Mock<IModelBinder>().Object;
+
+ // Act & assert
+ MemberHelper.TestPropertyWithDefaultInstance(binders, "DefaultBinder", binder);
+ }
+
+ [Fact]
+ public void DictionaryInterface()
+ {
+ // Arrange
+ DictionaryHelper<Type, IModelBinder> helper = new DictionaryHelper<Type, IModelBinder>()
+ {
+ Creator = () => new ModelBinderDictionary(),
+ SampleKeys = new Type[] { typeof(object), typeof(string), typeof(int), typeof(long), typeof(long) },
+ SampleValues = new IModelBinder[] { new DefaultModelBinder(), new DefaultModelBinder(), new DefaultModelBinder(), new DefaultModelBinder(), new DefaultModelBinder() },
+ ThrowOnKeyNotFound = false
+ };
+
+ // Act & assert
+ helper.Execute();
+ }
+
+ [Fact]
+ public void GetBinderConsultsProviders()
+ {
+ // Arrange
+ Type modelType = typeof(string);
+ IModelBinder expectedBinderFromProvider = new Mock<IModelBinder>().Object;
+
+ Mock<IModelBinderProvider> locatedProvider = new Mock<IModelBinderProvider>();
+ locatedProvider.Setup(p => p.GetBinder(modelType))
+ .Returns(expectedBinderFromProvider);
+
+ Mock<IModelBinderProvider> secondProvider = new Mock<IModelBinderProvider>();
+
+ ModelBinderProviderCollection providers = new ModelBinderProviderCollection(new IModelBinderProvider[] { locatedProvider.Object, secondProvider.Object });
+ ModelBinderDictionary binders = new ModelBinderDictionary(providers);
+
+ // Act
+ IModelBinder returnedBinder = binders.GetBinder(modelType);
+
+ // Assert
+ Assert.Same(expectedBinderFromProvider, returnedBinder);
+ }
+
+ [Fact]
+ public void GetBinderDoesNotReturnDefaultBinderIfAskedNotTo()
+ {
+ // Proper order of precedence:
+ // 1. Binder registered in the global table
+ // 2. Binder attribute defined on the type
+ // 3. <null>
+
+ // Arrange
+ IModelBinder registeredFirstBinder = new Mock<IModelBinder>().Object;
+ ModelBinderDictionary binders = new ModelBinderDictionary()
+ {
+ { typeof(MyFirstConvertibleType), registeredFirstBinder }
+ };
+
+ // Act
+ IModelBinder binder1 = binders.GetBinder(typeof(MyFirstConvertibleType), false /* fallbackToDefault */);
+ IModelBinder binder2 = binders.GetBinder(typeof(MySecondConvertibleType), false /* fallbackToDefault */);
+ IModelBinder binder3 = binders.GetBinder(typeof(object), false /* fallbackToDefault */);
+
+ // Assert
+ Assert.Same(registeredFirstBinder, binder1);
+ Assert.IsType<MySecondBinder>(binder2);
+ Assert.Null(binder3);
+ }
+
+ [Fact]
+ public void GetBinderResolvesBindersWithCorrectPrecedence()
+ {
+ // Proper order of precedence:
+ // 1. Binder registered in the global table
+ // 2. Binder attribute defined on the type
+ // 3. Default binder
+
+ // Arrange
+ IModelBinder registeredFirstBinder = new Mock<IModelBinder>().Object;
+ ModelBinderDictionary binders = new ModelBinderDictionary()
+ {
+ { typeof(MyFirstConvertibleType), registeredFirstBinder }
+ };
+
+ IModelBinder defaultBinder = new Mock<IModelBinder>().Object;
+ binders.DefaultBinder = defaultBinder;
+
+ // Act
+ IModelBinder binder1 = binders.GetBinder(typeof(MyFirstConvertibleType));
+ IModelBinder binder2 = binders.GetBinder(typeof(MySecondConvertibleType));
+ IModelBinder binder3 = binders.GetBinder(typeof(object));
+
+ // Assert
+ Assert.Same(registeredFirstBinder, binder1);
+ Assert.IsType<MySecondBinder>(binder2);
+ Assert.Same(defaultBinder, binder3);
+ }
+
+ [Fact]
+ public void GetBinderThrowsIfModelTypeContainsMultipleAttributes()
+ {
+ // Arrange
+ ModelBinderDictionary binders = new ModelBinderDictionary();
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ delegate { binders.GetBinder(typeof(ConvertibleTypeWithSeveralBinders), true /* fallbackToDefault */); },
+ "The type 'System.Web.Mvc.Test.ModelBinderDictionaryTest+ConvertibleTypeWithSeveralBinders' contains multiple attributes that inherit from CustomModelBinderAttribute.");
+ }
+
+ [Fact]
+ public void GetBinderThrowsIfModelTypeIsNull()
+ {
+ // Arrange
+ ModelBinderDictionary binders = new ModelBinderDictionary();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { binders.GetBinder(null); }, "modelType");
+ }
+
+ [ModelBinder(typeof(MyFirstBinder))]
+ private class MyFirstConvertibleType
+ {
+ }
+
+ private class MyFirstBinder : IModelBinder
+ {
+ public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ [ModelBinder(typeof(MySecondBinder))]
+ private class MySecondConvertibleType
+ {
+ }
+
+ private class MySecondBinder : IModelBinder
+ {
+ public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ [ModelBinder(typeof(MySecondBinder))]
+ [MySubclassedBinder]
+ private class ConvertibleTypeWithSeveralBinders
+ {
+ }
+
+ private class MySubclassedBinderAttribute : CustomModelBinderAttribute
+ {
+ public override IModelBinder GetBinder()
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ModelBinderProviderCollectionTest.cs b/test/System.Web.Mvc.Test/Test/ModelBinderProviderCollectionTest.cs
new file mode 100644
index 00000000..99fb73d9
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ModelBinderProviderCollectionTest.cs
@@ -0,0 +1,113 @@
+using System.Collections.Generic;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ModelBinderProviderCollectionTest
+ {
+ [Fact]
+ public void GuardClause()
+ {
+ // Arrange
+ var collection = new ModelBinderProviderCollection();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => collection.GetBinder(null),
+ "modelType"
+ );
+ }
+
+ [Fact]
+ public void GetBinderUsesRegisteredProviders()
+ {
+ // Arrange
+ var testType = typeof(string);
+ var expectedBinder = new Mock<IModelBinder>().Object;
+
+ var provider = new Mock<IModelBinderProvider>(MockBehavior.Strict);
+ provider.Setup(p => p.GetBinder(testType)).Returns(expectedBinder);
+ var collection = new ModelBinderProviderCollection(new[] { provider.Object });
+
+ // Act
+ IModelBinder returnedBinder = collection.GetBinder(testType);
+
+ // Assert
+ Assert.Same(expectedBinder, returnedBinder);
+ }
+
+ [Fact]
+ public void GetBinderReturnsValueFromFirstSuccessfulBinderProvider()
+ {
+ // Arrange
+ var testType = typeof(string);
+ IModelBinder nullModelBinder = null;
+ IModelBinder expectedBinder = new Mock<IModelBinder>().Object;
+ IModelBinder secondMatchingBinder = new Mock<IModelBinder>().Object;
+
+ var provider1 = new Mock<IModelBinderProvider>();
+ provider1.Setup(p => p.GetBinder(testType)).Returns(nullModelBinder);
+
+ var provider2 = new Mock<IModelBinderProvider>(MockBehavior.Strict);
+ provider2.Setup(p => p.GetBinder(testType)).Returns(expectedBinder);
+
+ var provider3 = new Mock<IModelBinderProvider>(MockBehavior.Strict);
+ provider3.Setup(p => p.GetBinder(testType)).Returns(secondMatchingBinder);
+
+ var collection = new ModelBinderProviderCollection(new[] { provider1.Object, provider2.Object, provider3.Object });
+
+ // Act
+ IModelBinder returnedBinder = collection.GetBinder(testType);
+
+ // Assert
+ Assert.Same(expectedBinder, returnedBinder);
+ }
+
+ [Fact]
+ public void GetBinderReturnsNullWhenNoSuccessfulBinderProviders()
+ {
+ // Arrange
+ var testType = typeof(string);
+ IModelBinder nullModelBinder = null;
+
+ var provider1 = new Mock<IModelBinderProvider>();
+ provider1.Setup(p => p.GetBinder(testType)).Returns(nullModelBinder);
+
+ var provider2 = new Mock<IModelBinderProvider>(MockBehavior.Strict);
+ provider2.Setup(p => p.GetBinder(testType)).Returns(nullModelBinder);
+
+ var collection = new ModelBinderProviderCollection(new[] { provider1.Object, provider2.Object });
+
+ // Act
+ IModelBinder returnedBinder = collection.GetBinder(testType);
+
+ // Assert
+ Assert.Null(returnedBinder);
+ }
+
+ [Fact]
+ public void GetBinderDelegatesToResolver()
+ {
+ // Arrange
+ Type modelType = typeof(string);
+ IModelBinder expectedBinder = new Mock<IModelBinder>().Object;
+
+ Mock<IModelBinderProvider> locatedProvider = new Mock<IModelBinderProvider>();
+ locatedProvider.Setup(p => p.GetBinder(modelType))
+ .Returns(expectedBinder);
+
+ Mock<IModelBinderProvider> secondProvider = new Mock<IModelBinderProvider>();
+ Resolver<IEnumerable<IModelBinderProvider>> resolver = new Resolver<IEnumerable<IModelBinderProvider>> { Current = new IModelBinderProvider[] { locatedProvider.Object, secondProvider.Object } };
+
+ ModelBinderProviderCollection providers = new ModelBinderProviderCollection(resolver);
+
+ // Act
+ IModelBinder returnedBinder = providers.GetBinder(modelType);
+
+ // Assert
+ Assert.Same(expectedBinder, returnedBinder);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ModelBinderProvidersTest.cs b/test/System.Web.Mvc.Test/Test/ModelBinderProvidersTest.cs
new file mode 100644
index 00000000..18ebdd53
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ModelBinderProvidersTest.cs
@@ -0,0 +1,18 @@
+using System.Linq;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class ModelBinderProvidersTest
+ {
+ [Fact]
+ public void CollectionDefaults()
+ {
+ // Act
+ Type[] actualTypes = ModelBinderProviders.BinderProviders.Select(b => b.GetType()).ToArray();
+
+ // Assert
+ Assert.Equal(Enumerable.Empty<Type>(), actualTypes);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ModelBindersTest.cs b/test/System.Web.Mvc.Test/Test/ModelBindersTest.cs
new file mode 100644
index 00000000..b2d31388
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ModelBindersTest.cs
@@ -0,0 +1,65 @@
+using System.ComponentModel.DataAnnotations;
+using System.Data.Linq;
+using System.Threading;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class ModelBindersTest
+ {
+ [Fact]
+ public void BindersPropertyIsNotNull()
+ {
+ // Arrange & Act
+ ModelBinderDictionary binders = ModelBinders.Binders;
+
+ // Assert
+ Assert.NotNull(binders);
+ }
+
+ [Fact]
+ public void DefaultModelBinders()
+ {
+ // Act
+ ModelBinderDictionary binders = ModelBinders.Binders;
+
+ // Assert
+ Assert.Equal(4, binders.Count);
+ Assert.True(binders.ContainsKey(typeof(byte[])));
+ Assert.IsType<ByteArrayModelBinder>(binders[typeof(byte[])]);
+ Assert.True(binders.ContainsKey(typeof(HttpPostedFileBase)));
+ Assert.IsType<HttpPostedFileBaseModelBinder>(binders[typeof(HttpPostedFileBase)]);
+ Assert.True(binders.ContainsKey(typeof(Binary)));
+ Assert.IsType<LinqBinaryModelBinder>(binders[typeof(Binary)]);
+ Assert.True(binders.ContainsKey(typeof(CancellationToken)));
+ Assert.IsType<CancellationTokenModelBinder>(binders[typeof(CancellationToken)]);
+ }
+
+ [Fact]
+ public void GetBindersFromAttributes_ReadsModelBinderAttributeFromBuddyClass()
+ {
+ // Act
+ IModelBinder binder = ModelBinders.GetBinderFromAttributes(typeof(SampleModel), null);
+
+ // Assert
+ Assert.IsType<SampleModelBinder>(binder);
+ }
+
+ [MetadataType(typeof(SampleModel_Buddy))]
+ private class SampleModel
+ {
+ [ModelBinder(typeof(SampleModelBinder))]
+ private class SampleModel_Buddy
+ {
+ }
+ }
+
+ private class SampleModelBinder : IModelBinder
+ {
+ public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ModelBindingContextTest.cs b/test/System.Web.Mvc.Test/Test/ModelBindingContextTest.cs
new file mode 100644
index 00000000..d5e32e25
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ModelBindingContextTest.cs
@@ -0,0 +1,122 @@
+using System.Web.TestUtil;
+using Microsoft.Web.UnitTestUtil;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ModelBindingContextTest
+ {
+ [Fact]
+ public void CopyConstructor()
+ {
+ // Arrange
+ ModelBindingContext originalBindingContext = new ModelBindingContext()
+ {
+ FallbackToEmptyPrefix = true,
+ ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(object)),
+ ModelName = "theName",
+ ModelState = new ModelStateDictionary(),
+ PropertyFilter = _ => false,
+ ValueProvider = new SimpleValueProvider()
+ };
+
+ // Act
+ ModelBindingContext newBindingContext = new ModelBindingContext(originalBindingContext);
+
+ // Assert
+ Assert.False(newBindingContext.FallbackToEmptyPrefix);
+ Assert.Null(newBindingContext.ModelMetadata);
+ Assert.Equal("", newBindingContext.ModelName);
+ Assert.Equal(originalBindingContext.ModelState, newBindingContext.ModelState);
+ Assert.True(newBindingContext.PropertyFilter("foo"));
+ Assert.Equal(originalBindingContext.ValueProvider, newBindingContext.ValueProvider);
+ }
+
+ [Fact]
+ public void ModelNameProperty()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext();
+
+ // Act & assert
+ MemberHelper.TestStringProperty(bindingContext, "ModelName", String.Empty);
+ }
+
+ [Fact]
+ public void ModelStateProperty()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext();
+ ModelStateDictionary modelState = new ModelStateDictionary();
+
+ // Act & assert
+ MemberHelper.TestPropertyWithDefaultInstance(bindingContext, "ModelState", modelState);
+ }
+
+ [Fact]
+ public void PropertyFilterPropertyDefaultInstanceReturnsTrueForAnyInput()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext();
+
+ // Act
+ Predicate<string> propertyFilter = bindingContext.PropertyFilter;
+
+ // Assert
+ // We can't test all inputs, but at least this gives us high confidence that we ignore the parameter by default
+ Assert.True(propertyFilter(null));
+ Assert.True(propertyFilter(String.Empty));
+ Assert.True(propertyFilter("Foo"));
+ }
+
+ [Fact]
+ public void PropertyFilterPropertyReturnsDefaultInstance()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext();
+ Predicate<string> propertyFilter = _ => true;
+
+ // Act & assert
+ MemberHelper.TestPropertyWithDefaultInstance(bindingContext, "PropertyFilter", propertyFilter);
+ }
+
+ [Fact]
+ public void ModelAndModelTypeAreFedFromModelMetadata()
+ {
+ // Act
+ ModelBindingContext bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => 42, typeof(int))
+ };
+
+ // Assert
+ Assert.Equal(42, bindingContext.Model);
+ Assert.Equal(typeof(int), bindingContext.ModelType);
+ }
+
+ [Fact]
+ public void ModelIsNotSettable()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext();
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ () => bindingContext.Model = "foo",
+ "This property setter is obsolete, because its value is derived from ModelMetadata.Model now.");
+ }
+
+ [Fact]
+ public void ModelTypeIsNotSettable()
+ {
+ // Arrange
+ ModelBindingContext bindingContext = new ModelBindingContext();
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ () => bindingContext.ModelType = typeof(string),
+ "This property setter is obsolete, because its value is derived from ModelMetadata.Model now.");
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ModelClientValidationRuleTest.cs b/test/System.Web.Mvc.Test/Test/ModelClientValidationRuleTest.cs
new file mode 100644
index 00000000..40d21dd2
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ModelClientValidationRuleTest.cs
@@ -0,0 +1,33 @@
+using System.Collections.Generic;
+using System.Web.TestUtil;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class ModelClientValidationRuleTest
+ {
+ [Fact]
+ public void ValidationParametersProperty()
+ {
+ // Arrange
+ ModelClientValidationRule rule = new ModelClientValidationRule();
+
+ // Act
+ IDictionary<string, object> parameters = rule.ValidationParameters;
+
+ // Assert
+ Assert.NotNull(parameters);
+ Assert.Empty(parameters);
+ }
+
+ [Fact]
+ public void ValidationTypeProperty()
+ {
+ // Arrange
+ ModelClientValidationRule rule = new ModelClientValidationRule();
+
+ // Act & assert
+ MemberHelper.TestStringProperty(rule, "ValidationType", String.Empty);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ModelErrorCollectionTest.cs b/test/System.Web.Mvc.Test/Test/ModelErrorCollectionTest.cs
new file mode 100644
index 00000000..dbc7afdb
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ModelErrorCollectionTest.cs
@@ -0,0 +1,37 @@
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class ModelErrorCollectionTest
+ {
+ [Fact]
+ public void AddWithExceptionArgument()
+ {
+ // Arrange
+ ModelErrorCollection collection = new ModelErrorCollection();
+ Exception ex = new Exception("some message");
+
+ // Act
+ collection.Add(ex);
+
+ // Assert
+ ModelError modelError = Assert.Single(collection);
+ Assert.Same(ex, modelError.Exception);
+ }
+
+ [Fact]
+ public void AddWithStringArgument()
+ {
+ // Arrange
+ ModelErrorCollection collection = new ModelErrorCollection();
+
+ // Act
+ collection.Add("some message");
+
+ // Assert
+ ModelError modelError = Assert.Single(collection);
+ Assert.Equal("some message", modelError.ErrorMessage);
+ Assert.Null(modelError.Exception);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ModelErrorTest.cs b/test/System.Web.Mvc.Test/Test/ModelErrorTest.cs
new file mode 100644
index 00000000..a2048008
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ModelErrorTest.cs
@@ -0,0 +1,65 @@
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ModelErrorTest
+ {
+ [Fact]
+ public void ConstructorThrowsIfExceptionIsNull()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { new ModelError((Exception)null); }, "exception");
+ }
+
+ [Fact]
+ public void ConstructorWithExceptionAndStringArguments()
+ {
+ // Arrange
+ Exception ex = new Exception("some message");
+
+ // Act
+ ModelError modelError = new ModelError(ex, "some other message");
+
+ // Assert
+ Assert.Equal("some other message", modelError.ErrorMessage);
+ Assert.Same(ex, modelError.Exception);
+ }
+
+ [Fact]
+ public void ConstructorWithExceptionArgument()
+ {
+ // Arrange
+ Exception ex = new Exception("some message");
+
+ // Act
+ ModelError modelError = new ModelError(ex);
+
+ // Assert
+ Assert.Equal(String.Empty, modelError.ErrorMessage);
+ Assert.Same(ex, modelError.Exception);
+ }
+
+ [Fact]
+ public void ConstructorWithNullStringArgumentCreatesEmptyStringErrorMessage()
+ {
+ // Act
+ ModelError modelError = new ModelError((string)null);
+
+ // Assert
+ Assert.Equal(String.Empty, modelError.ErrorMessage);
+ }
+
+ [Fact]
+ public void ConstructorWithStringArgument()
+ {
+ // Act
+ ModelError modelError = new ModelError("some message");
+
+ // Assert
+ Assert.Equal("some message", modelError.ErrorMessage);
+ Assert.Null(modelError.Exception);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ModelMetadataProvidersTest.cs b/test/System.Web.Mvc.Test/Test/ModelMetadataProvidersTest.cs
new file mode 100644
index 00000000..a0f6509d
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ModelMetadataProvidersTest.cs
@@ -0,0 +1,68 @@
+using Moq;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class ModelMetadataProvidersTest
+ {
+ [Fact]
+ public void DefaultModelMetadataProviderIsCachedDataAnnotations()
+ {
+ // Arrange
+ ModelMetadataProviders providers = new ModelMetadataProviders();
+
+ // Act
+ ModelMetadataProvider provider = providers.CurrentInternal;
+
+ // Assert
+ Assert.IsType<CachedDataAnnotationsModelMetadataProvider>(provider);
+ }
+
+ [Fact]
+ public void SettingModelMetadataProviderReturnsSetProvider()
+ {
+ // Arrange
+ ModelMetadataProviders providers = new ModelMetadataProviders();
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+
+ // Act
+ providers.CurrentInternal = provider.Object;
+
+ // Assert
+ Assert.Same(provider.Object, providers.CurrentInternal);
+ }
+
+ [Fact]
+ public void SettingNullModelMetadataProviderUsesEmptyModelMetadataProvider()
+ {
+ // Arrange
+ ModelMetadataProviders providers = new ModelMetadataProviders();
+
+ // Act
+ providers.CurrentInternal = null;
+
+ // Assert
+ Assert.IsType<EmptyModelMetadataProvider>(providers.CurrentInternal);
+ }
+
+ [Fact]
+ public void ModelMetadataProvidersCurrentDelegatesToResolver()
+ {
+ // Arrange
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+ Resolver<ModelMetadataProvider> resolver = new Resolver<ModelMetadataProvider> { Current = provider.Object };
+ ModelMetadataProviders providers = new ModelMetadataProviders(resolver);
+
+ // Act
+ ModelMetadataProvider result = providers.CurrentInternal;
+
+ // Assert
+ Assert.Same(provider.Object, result);
+ }
+
+ private class Resolver<T> : IResolver<T>
+ {
+ public T Current { get; set; }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ModelMetadataTest.cs b/test/System.Web.Mvc.Test/Test/ModelMetadataTest.cs
new file mode 100644
index 00000000..78244edd
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ModelMetadataTest.cs
@@ -0,0 +1,892 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Linq.Expressions;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ModelMetadataTest
+ {
+ // Guard clauses
+
+ [Fact]
+ public void NullProviderThrows()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => new ModelMetadata(null /* provider */, null /* containerType */, null /* model */, typeof(object), null /* propertyName */),
+ "provider");
+ }
+
+ [Fact]
+ public void NullTypeThrows()
+ {
+ // Arrange
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => new ModelMetadata(provider.Object, null /* containerType */, null /* model */, null /* modelType */, null /* propertyName */),
+ "modelType");
+ }
+
+ // Constructor
+
+ [Fact]
+ public void DefaultValues()
+ {
+ // Arrange
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+
+ // Act
+ ModelMetadata metadata = new ModelMetadata(provider.Object, typeof(Exception), () => "model", typeof(string), "propertyName");
+
+ // Assert
+ Assert.Equal(typeof(Exception), metadata.ContainerType);
+ Assert.True(metadata.ConvertEmptyStringToNull);
+ Assert.Null(metadata.DataTypeName);
+ Assert.Null(metadata.Description);
+ Assert.Null(metadata.DisplayFormatString);
+ Assert.Null(metadata.DisplayName);
+ Assert.Null(metadata.EditFormatString);
+ Assert.False(metadata.HideSurroundingHtml);
+ Assert.Equal("model", metadata.Model);
+ Assert.Equal(typeof(string), metadata.ModelType);
+ Assert.Null(metadata.NullDisplayText);
+ Assert.Equal(10000, metadata.Order);
+ Assert.Equal("propertyName", metadata.PropertyName);
+ Assert.False(metadata.IsReadOnly);
+ Assert.True(metadata.RequestValidationEnabled);
+ Assert.Null(metadata.ShortDisplayName);
+ Assert.True(metadata.ShowForDisplay);
+ Assert.True(metadata.ShowForEdit);
+ Assert.Null(metadata.TemplateHint);
+ Assert.Null(metadata.Watermark);
+ }
+
+ // IsComplexType
+
+ struct IsComplexTypeModel
+ {
+ }
+
+ [Fact]
+ public void IsComplexTypeTests()
+ {
+ // Arrange
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+
+ // Act & Assert
+ Assert.True(new ModelMetadata(provider.Object, null, null, typeof(Object), null).IsComplexType);
+ Assert.False(new ModelMetadata(provider.Object, null, null, typeof(string), null).IsComplexType);
+ Assert.True(new ModelMetadata(provider.Object, null, null, typeof(IDisposable), null).IsComplexType);
+ Assert.False(new ModelMetadata(provider.Object, null, null, typeof(Nullable<int>), null).IsComplexType);
+ Assert.False(new ModelMetadata(provider.Object, null, null, typeof(int), null).IsComplexType);
+ Assert.True(new ModelMetadata(provider.Object, null, null, typeof(IsComplexTypeModel), null).IsComplexType);
+ Assert.True(new ModelMetadata(provider.Object, null, null, typeof(Nullable<IsComplexTypeModel>), null).IsComplexType);
+ }
+
+ // IsNullableValueType
+
+ [Fact]
+ public void IsNullableValueTypeTests()
+ {
+ // Arrange
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+
+ // Act & Assert
+ Assert.False(new ModelMetadata(provider.Object, null, null, typeof(string), null).IsNullableValueType);
+ Assert.False(new ModelMetadata(provider.Object, null, null, typeof(IDisposable), null).IsNullableValueType);
+ Assert.True(new ModelMetadata(provider.Object, null, null, typeof(Nullable<int>), null).IsNullableValueType);
+ Assert.False(new ModelMetadata(provider.Object, null, null, typeof(int), null).IsNullableValueType);
+ }
+
+ // IsRequired
+
+ [Fact]
+ public void IsRequiredTests()
+ {
+ // Arrange
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+
+ // Act & Assert
+ Assert.False(new ModelMetadata(provider.Object, null, null, typeof(string), null).IsRequired); // Reference type not required
+ Assert.False(new ModelMetadata(provider.Object, null, null, typeof(IDisposable), null).IsRequired); // Interface not required
+ Assert.False(new ModelMetadata(provider.Object, null, null, typeof(Nullable<int>), null).IsRequired); // Nullable value type not required
+ Assert.True(new ModelMetadata(provider.Object, null, null, typeof(int), null).IsRequired); // Value type required
+ Assert.True(new ModelMetadata(provider.Object, null, null, typeof(DayOfWeek), null).IsRequired); // Enum (implicit value type) is required
+ }
+
+ // Properties
+
+ [Fact]
+ public void PropertiesCallsProvider()
+ {
+ // Arrange
+ Type modelType = typeof(string);
+ List<ModelMetadata> propertyMetadata = new List<ModelMetadata>();
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+ ModelMetadata metadata = new ModelMetadata(provider.Object, null, null, modelType, null);
+ provider.Setup(p => p.GetMetadataForProperties(null, modelType))
+ .Returns(propertyMetadata)
+ .Verifiable();
+
+ // Act
+ IEnumerable<ModelMetadata> result = metadata.Properties;
+
+ // Assert
+ Assert.Equal(propertyMetadata, result.ToList());
+ provider.Verify();
+ }
+
+ [Fact]
+ public void PropertiesUsesRealModelTypeRatherThanPassedModelType()
+ {
+ // Arrange
+ string model = "String Value";
+ Expression<Func<object, object>> accessor = _ => model;
+ ModelMetadata metadata = ModelMetadata.FromLambdaExpression(accessor, new ViewDataDictionary<object>());
+
+ // Act
+ IEnumerable<ModelMetadata> result = metadata.Properties;
+
+ // Assert
+ Assert.Equal("Length", result.Single().PropertyName);
+ }
+
+ [Fact]
+ public void PropertiesAreSortedByOrder()
+ {
+ // Arrange
+ Type modelType = typeof(string);
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+ List<ModelMetadata> propertyMetadata = new List<ModelMetadata>
+ {
+ new ModelMetadata(provider.Object, null, () => 1, typeof(int), null) { Order = 20 },
+ new ModelMetadata(provider.Object, null, () => 2, typeof(int), null) { Order = 30 },
+ new ModelMetadata(provider.Object, null, () => 3, typeof(int), null) { Order = 10 },
+ };
+ ModelMetadata metadata = new ModelMetadata(provider.Object, null, null, modelType, null);
+ provider.Setup(p => p.GetMetadataForProperties(null, modelType))
+ .Returns(propertyMetadata)
+ .Verifiable();
+
+ // Act
+ List<ModelMetadata> result = metadata.Properties.ToList();
+
+ // Assert
+ Assert.Equal(3, result.Count);
+ Assert.Equal(3, result[0].Model);
+ Assert.Equal(1, result[1].Model);
+ Assert.Equal(2, result[2].Model);
+ }
+
+ [Fact]
+ public void PropertiesListGetsResetWhenModelGetsReset()
+ { // Dev10 Bug #923263
+ // Arrange
+ var provider = new DataAnnotationsModelMetadataProvider();
+ var metadata = new ModelMetadata(provider, null, () => new Class1(), typeof(Class1), null);
+
+ // Act
+ ModelMetadata[] originalProps = metadata.Properties.ToArray();
+ metadata.Model = new Class2();
+ ModelMetadata[] newProps = metadata.Properties.ToArray();
+
+ // Assert
+ ModelMetadata originalProp = Assert.Single(originalProps);
+ Assert.Equal(typeof(string), originalProp.ModelType);
+ Assert.Equal("Prop1", originalProp.PropertyName);
+ ModelMetadata newProp = Assert.Single(newProps);
+ Assert.Equal(typeof(int), newProp.ModelType);
+ Assert.Equal("Prop2", newProp.PropertyName);
+ }
+
+ class Class1
+ {
+ public string Prop1 { get; set; }
+ }
+
+ class Class2
+ {
+ public int Prop2 { get; set; }
+ }
+
+ // SimpleDisplayText
+
+ [Fact]
+ public void SimpleDisplayTextReturnsNullDisplayTextForNullModel()
+ {
+ // Arrange
+ string nullText = "(null)";
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+ ModelMetadata metadata = new ModelMetadata(provider.Object, null, null, typeof(object), null) { NullDisplayText = nullText };
+
+ // Act
+ string result = metadata.SimpleDisplayText;
+
+ // Assert
+ Assert.Equal(nullText, result);
+ }
+
+ private class SimpleDisplayTextModelWithToString
+ {
+ public override string ToString()
+ {
+ return "Custom ToString Value";
+ }
+ }
+
+ [Fact]
+ public void SimpleDisplayTextReturnsToStringValueWhenOverridden()
+ {
+ // Arrange
+ SimpleDisplayTextModelWithToString model = new SimpleDisplayTextModelWithToString();
+ EmptyModelMetadataProvider provider = new EmptyModelMetadataProvider();
+ ModelMetadata metadata = new ModelMetadata(provider, null, () => model, typeof(SimpleDisplayTextModelWithToString), null);
+
+ // Act
+ string result = metadata.SimpleDisplayText;
+
+ // Assert
+ Assert.Equal(model.ToString(), result);
+ }
+
+ private class SimpleDisplayTextModelWithoutToString
+ {
+ public string FirstProperty { get; set; }
+
+ public int SecondProperty { get; set; }
+ }
+
+ [Fact]
+ public void SimpleDisplayTextReturnsFirstPropertyValueForNonNullModel()
+ {
+ // Arrange
+ SimpleDisplayTextModelWithoutToString model = new SimpleDisplayTextModelWithoutToString
+ {
+ FirstProperty = "First Property Value"
+ };
+ EmptyModelMetadataProvider provider = new EmptyModelMetadataProvider();
+ ModelMetadata metadata = new ModelMetadata(provider, null, () => model, typeof(SimpleDisplayTextModelWithoutToString), null);
+
+ // Act
+ string result = metadata.SimpleDisplayText;
+
+ // Assert
+ Assert.Equal(model.FirstProperty, result);
+ }
+
+ [Fact]
+ public void SimpleDisplayTextReturnsFirstPropertyNullDisplayTextForNonNullModelWithNullDisplayColumnPropertyValue()
+ {
+ // Arrange
+ SimpleDisplayTextModelWithoutToString model = new SimpleDisplayTextModelWithoutToString();
+ EmptyModelMetadataProvider propertyProvider = new EmptyModelMetadataProvider();
+ ModelMetadata propertyMetadata = propertyProvider.GetMetadataForProperty(() => model.FirstProperty, typeof(SimpleDisplayTextModelWithoutToString), "FirstProperty");
+ propertyMetadata.NullDisplayText = "Null Display Text";
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+ provider.Setup(p => p.GetMetadataForProperties(model, typeof(SimpleDisplayTextModelWithoutToString)))
+ .Returns(new[] { propertyMetadata });
+ ModelMetadata metadata = new ModelMetadata(provider.Object, null, () => model, typeof(SimpleDisplayTextModelWithoutToString), null);
+
+ // Act
+ string result = metadata.SimpleDisplayText;
+
+ // Assert
+ Assert.Equal(propertyMetadata.NullDisplayText, result);
+ }
+
+ private class SimpleDisplayTextModelWithNoProperties
+ {
+ }
+
+ [Fact]
+ public void SimpleDisplayTextReturnsEmptyStringForNonNullModelWithNoVisibleProperties()
+ {
+ // Arrange
+ SimpleDisplayTextModelWithNoProperties model = new SimpleDisplayTextModelWithNoProperties();
+ EmptyModelMetadataProvider provider = new EmptyModelMetadataProvider();
+ ModelMetadata metadata = new ModelMetadata(provider, null, () => model, typeof(SimpleDisplayTextModelWithNoProperties), null);
+
+ // Act
+ string result = metadata.SimpleDisplayText;
+
+ // Assert
+ Assert.Equal(String.Empty, result);
+ }
+
+ private class ObjectWithToStringOverride
+ {
+ private string _toStringValue;
+
+ public ObjectWithToStringOverride(string toStringValue)
+ {
+ _toStringValue = toStringValue;
+ }
+
+ public override string ToString()
+ {
+ return _toStringValue;
+ }
+ }
+
+ [Fact]
+ public void SimpleDisplayTextReturnsToStringOfModelForNonNullModel()
+ {
+ // Arrange
+ string toStringText = "text from ToString()";
+ ObjectWithToStringOverride model = new ObjectWithToStringOverride(toStringText);
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+ ModelMetadata metadata = new ModelMetadata(provider.Object, null, () => model, typeof(ObjectWithToStringOverride), null);
+
+ // Act
+ string result = metadata.SimpleDisplayText;
+
+ // Assert
+ Assert.Equal(toStringText, result);
+ }
+
+ [Fact]
+ public void SimpleDisplayTextReturnsEmptyStringForNonNullModelWithToStringNull()
+ {
+ // Arrange
+ string toStringText = null;
+ ObjectWithToStringOverride model = new ObjectWithToStringOverride(toStringText);
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+ ModelMetadata metadata = new ModelMetadata(provider.Object, null, () => model, typeof(ObjectWithToStringOverride), null);
+
+ // Act
+ string result = metadata.SimpleDisplayText;
+
+ // Assert
+ Assert.Equal(String.Empty, result);
+ }
+
+ // FromStringExpression()
+
+ [Fact]
+ public void FromStringExpressionGuardClauses()
+ {
+ // Null expression throws
+ Assert.ThrowsArgumentNull(
+ () => ModelMetadata.FromStringExpression(null, new ViewDataDictionary()),
+ "expression");
+
+ // Null view data dictionary throws
+ Assert.ThrowsArgumentNull(
+ () => ModelMetadata.FromStringExpression("expression", null),
+ "viewData");
+ }
+
+ [Fact]
+ public void FromStringExpressionEmptyExpressionReturnsExistingModelMetadata()
+ {
+ // Arrange
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+ ModelMetadata metadata = new ModelMetadata(provider.Object, null, null, typeof(object), null);
+ ViewDataDictionary viewData = new ViewDataDictionary();
+ viewData.ModelMetadata = metadata;
+
+ // Act
+ ModelMetadata result = ModelMetadata.FromStringExpression(String.Empty, viewData, provider.Object);
+
+ // Assert
+ Assert.Same(metadata, result);
+ }
+
+ [Fact]
+ public void FromStringExpressionItemNotFoundInViewData()
+ {
+ // Arrange
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+ ViewDataDictionary viewData = new ViewDataDictionary();
+ provider.Setup(p => p.GetMetadataForType(It.IsAny<Func<object>>(), It.IsAny<Type>()))
+ .Callback<Func<object>, Type>((accessor, type) =>
+ {
+ Assert.Null(accessor);
+ Assert.Equal(typeof(string), type); // Don't know the type, must fall back on string
+ })
+ .Returns(() => null)
+ .Verifiable();
+
+ // Act
+ ModelMetadata.FromStringExpression("UnknownObject", viewData, provider.Object);
+
+ // Assert
+ provider.Verify();
+ }
+
+ [Fact]
+ public void FromStringExpressionNullItemFoundAtRootOfViewData()
+ {
+ // Arrange
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+ ViewDataDictionary viewData = new ViewDataDictionary();
+ viewData["Object"] = null;
+ provider.Setup(p => p.GetMetadataForType(It.IsAny<Func<object>>(), It.IsAny<Type>()))
+ .Callback<Func<object>, Type>((accessor, type) =>
+ {
+ Assert.Null(accessor());
+ Assert.Equal(typeof(string), type); // Don't know the type, must fall back on string
+ })
+ .Returns(() => null)
+ .Verifiable();
+
+ // Act
+ ModelMetadata.FromStringExpression("Object", viewData, provider.Object);
+
+ // Assert
+ provider.Verify();
+ }
+
+ [Fact]
+ public void FromStringExpressionNonNullItemFoundAtRootOfViewData()
+ {
+ // Arrange
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+ object model = new object();
+ ViewDataDictionary viewData = new ViewDataDictionary();
+ viewData["Object"] = model;
+ provider.Setup(p => p.GetMetadataForType(It.IsAny<Func<object>>(), It.IsAny<Type>()))
+ .Callback<Func<object>, Type>((accessor, type) =>
+ {
+ Assert.Same(model, accessor());
+ Assert.Equal(typeof(object), type);
+ })
+ .Returns(() => null)
+ .Verifiable();
+
+ // Act
+ ModelMetadata.FromStringExpression("Object", viewData, provider.Object);
+
+ // Assert
+ provider.Verify();
+ }
+
+ [Fact]
+ public void FromStringExpressionNullItemFoundOnPropertyOfItemInViewData()
+ {
+ // Arrange
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+ DummyModelContainer model = new DummyModelContainer();
+ ViewDataDictionary viewData = new ViewDataDictionary();
+ viewData["Object"] = model;
+ provider.Setup(p => p.GetMetadataForProperty(It.IsAny<Func<object>>(), It.IsAny<Type>(), It.IsAny<string>()))
+ .Callback<Func<object>, Type, string>((accessor, type, propertyName) =>
+ {
+ Assert.Null(accessor());
+ Assert.Equal(typeof(DummyModelContainer), type);
+ Assert.Equal("Model", propertyName);
+ })
+ .Returns(() => null)
+ .Verifiable();
+
+ // Act
+ ModelMetadata.FromStringExpression("Object.Model", viewData, provider.Object);
+
+ // Assert
+ provider.Verify();
+ }
+
+ [Fact]
+ public void FromStringExpressionNonNullItemFoundOnPropertyOfItemInViewData()
+ {
+ // Arrange
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+ DummyModelContainer model = new DummyModelContainer { Model = new DummyContactModel() };
+ ViewDataDictionary viewData = new ViewDataDictionary();
+ viewData["Object"] = model;
+ provider.Setup(p => p.GetMetadataForProperty(It.IsAny<Func<object>>(), It.IsAny<Type>(), It.IsAny<string>()))
+ .Callback<Func<object>, Type, string>((accessor, type, propertyName) =>
+ {
+ Assert.Same(model.Model, accessor());
+ Assert.Equal(typeof(DummyModelContainer), type);
+ Assert.Equal("Model", propertyName);
+ })
+ .Returns(() => null)
+ .Verifiable();
+
+ // Act
+ ModelMetadata.FromStringExpression("Object.Model", viewData, provider.Object);
+
+ // Assert
+ provider.Verify();
+ }
+
+ [Fact]
+ public void FromStringExpressionWithNullModelButValidModelMetadataShouldReturnProperPropertyMetadata()
+ {
+ // Arrange
+ ViewDataDictionary viewData = new ViewDataDictionary();
+ viewData.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(DummyContactModel));
+
+ // Act
+ ModelMetadata result = ModelMetadata.FromStringExpression("NullableIntValue", viewData);
+
+ // Assert
+ Assert.Null(result.Model);
+ Assert.Equal(typeof(Nullable<int>), result.ModelType);
+ Assert.Equal("NullableIntValue", result.PropertyName);
+ Assert.Equal(typeof(DummyContactModel), result.ContainerType);
+ }
+
+ [Fact]
+ public void FromStringExpressionValueInModelProperty()
+ {
+ // Arrange
+ DummyContactModel model = new DummyContactModel { FirstName = "John" };
+ ViewDataDictionary viewData = new ViewDataDictionary(model);
+
+ // Act
+ ModelMetadata metadata = ModelMetadata.FromStringExpression("FirstName", viewData);
+
+ // Assert
+ Assert.Equal("John", metadata.Model);
+ }
+
+ [Fact]
+ public void FromStringExpressionValueInViewDataOverridesValueFromModelProperty()
+ {
+ // Arrange
+ DummyContactModel model = new DummyContactModel { FirstName = "John" };
+ ViewDataDictionary viewData = new ViewDataDictionary(model);
+ viewData["FirstName"] = "Jim";
+
+ // Act
+ ModelMetadata metadata = ModelMetadata.FromStringExpression("FirstName", viewData);
+
+ // Assert
+ Assert.Equal("Jim", metadata.Model);
+ }
+
+ // FromLambdaExpression()
+
+ [Fact]
+ public void FromLambdaExpressionGuardClauseTests()
+ {
+ // Null expression throws
+ Assert.ThrowsArgumentNull(
+ () => ModelMetadata.FromLambdaExpression<string, object>(null, new ViewDataDictionary<string>()),
+ "expression");
+
+ // Null view data throws
+ Assert.ThrowsArgumentNull(
+ () => ModelMetadata.FromLambdaExpression<string, object>(m => m, null),
+ "viewData");
+
+ // Unsupported expression type throws
+ Assert.Throws<InvalidOperationException>(
+ () => ModelMetadata.FromLambdaExpression<string, object>(m => new Object(), new ViewDataDictionary<string>()),
+ "Templates can be used only with field access, property access, single-dimension array index, or single-parameter custom indexer expressions.");
+ }
+
+ [Fact]
+ public void FromLambdaExpressionModelIdentityExpressionReturnsExistingModelMetadata()
+ {
+ // Arrange
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+ ModelMetadata metadata = new ModelMetadata(provider.Object, null, null, typeof(object), null);
+ ViewDataDictionary<object> viewData = new ViewDataDictionary<object>();
+ viewData.ModelMetadata = metadata;
+
+ // Act
+ ModelMetadata result = ModelMetadata.FromLambdaExpression<object, object>(m => m, viewData, provider.Object);
+
+ // Assert
+ Assert.Same(metadata, result);
+ }
+
+ [Fact]
+ public void FromLambdaExpressionPropertyExpressionFromParameter()
+ {
+ // Arrange
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+ DummyContactModel model = new DummyContactModel { FirstName = "Test" };
+ ViewDataDictionary<DummyContactModel> viewData = new ViewDataDictionary<DummyContactModel>(model);
+ provider.Setup(p => p.GetMetadataForProperty(It.IsAny<Func<object>>(), It.IsAny<Type>(), It.IsAny<string>()))
+ .Callback<Func<object>, Type, string>((accessor, type, propertyName) =>
+ {
+ Assert.Equal("Test", accessor());
+ Assert.Equal(typeof(DummyContactModel), type);
+ Assert.Equal("FirstName", propertyName);
+ })
+ .Returns(() => null)
+ .Verifiable();
+
+ // Act
+ ModelMetadata.FromLambdaExpression<DummyContactModel, string>(m => m.FirstName, viewData, provider.Object);
+
+ // Assert
+ provider.Verify();
+ }
+
+ [Fact]
+ public void FromLambdaExpressionPropertyExpressionFromClosureValue()
+ {
+ // Arrange
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+ DummyContactModel model = new DummyContactModel { FirstName = "Test" };
+ ViewDataDictionary<object> viewData = new ViewDataDictionary<object>();
+ provider.Setup(p => p.GetMetadataForProperty(It.IsAny<Func<object>>(), It.IsAny<Type>(), It.IsAny<string>()))
+ .Callback<Func<object>, Type, string>((accessor, type, propertyName) =>
+ {
+ Assert.Equal("Test", accessor());
+ Assert.Equal(typeof(DummyContactModel), type);
+ Assert.Equal("FirstName", propertyName);
+ })
+ .Returns(() => null)
+ .Verifiable();
+
+ // Act
+ ModelMetadata.FromLambdaExpression<object, string>(m => model.FirstName, viewData, provider.Object);
+
+ // Assert
+ provider.Verify();
+ }
+
+ [Fact]
+ public void FromLambdaExpressionFieldExpressionFromParameter()
+ {
+ // Arrange
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+ DummyContactModel model = new DummyContactModel { IntField = 42 };
+ ViewDataDictionary<DummyContactModel> viewData = new ViewDataDictionary<DummyContactModel>(model);
+ provider.Setup(p => p.GetMetadataForType(It.IsAny<Func<object>>(), It.IsAny<Type>()))
+ .Callback<Func<object>, Type>((accessor, type) =>
+ {
+ Assert.Equal(42, accessor());
+ Assert.Equal(typeof(int), type);
+ })
+ .Returns(() => null)
+ .Verifiable();
+
+ // Act
+ ModelMetadata.FromLambdaExpression<DummyContactModel, int>(m => m.IntField, viewData, provider.Object);
+
+ // Assert
+ provider.Verify();
+ }
+
+ [Fact]
+ public void FromLambdaExpressionFieldExpressionFromFieldOfClosureValue()
+ {
+ // Arrange
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+ DummyContactModel model = new DummyContactModel { IntField = 42 };
+ ViewDataDictionary<object> viewData = new ViewDataDictionary<object>();
+ provider.Setup(p => p.GetMetadataForType(It.IsAny<Func<object>>(), It.IsAny<Type>()))
+ .Callback<Func<object>, Type>((accessor, type) =>
+ {
+ Assert.Equal(42, accessor());
+ Assert.Equal(typeof(int), type);
+ })
+ .Returns(() => null)
+ .Verifiable();
+
+ // Act
+ ModelMetadata.FromLambdaExpression<object, int>(m => model.IntField, viewData, provider.Object);
+
+ // Assert
+ provider.Verify();
+ }
+
+ [Fact]
+ public void FromLambdaExpressionFieldExpressionFromClosureValue()
+ {
+ // Arrange
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+ DummyContactModel model = new DummyContactModel();
+ ViewDataDictionary<object> viewData = new ViewDataDictionary<object>();
+ provider.Setup(p => p.GetMetadataForType(It.IsAny<Func<object>>(), It.IsAny<Type>()))
+ .Callback<Func<object>, Type>((accessor, type) =>
+ {
+ Assert.Same(model, accessor());
+ Assert.Equal(typeof(DummyContactModel), type);
+ })
+ .Returns(() => null)
+ .Verifiable();
+
+ // Act
+ ModelMetadata.FromLambdaExpression<object, DummyContactModel>(m => model, viewData, provider.Object);
+
+ // Assert
+ provider.Verify();
+ }
+
+ [Fact]
+ public void FromLambdaExpressionSingleParameterClassIndexer()
+ {
+ // Arrange
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+ DummyContactModel model = new DummyContactModel();
+ ViewDataDictionary<DummyContactModel> viewData = new ViewDataDictionary<DummyContactModel>(model);
+ provider.Setup(p => p.GetMetadataForType(It.IsAny<Func<object>>(), It.IsAny<Type>()))
+ .Callback<Func<object>, Type>((accessor, type) =>
+ {
+ Assert.Equal("Indexed into 42", accessor());
+ Assert.Equal(typeof(string), type);
+ })
+ .Returns(() => null)
+ .Verifiable();
+
+ // Act
+ ModelMetadata.FromLambdaExpression<DummyContactModel, string>(m => m[42], viewData, provider.Object);
+
+ // Assert
+ provider.Verify();
+ }
+
+ [Fact]
+ public void FromLambdaExpressionSingleDimensionArrayIndex()
+ {
+ // Arrange
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+ DummyContactModel model = new DummyContactModel { Array = new[] { 4, 8, 15, 16, 23, 42 } };
+ ViewDataDictionary<DummyContactModel> viewData = new ViewDataDictionary<DummyContactModel>(model);
+ provider.Setup(p => p.GetMetadataForType(It.IsAny<Func<object>>(), It.IsAny<Type>()))
+ .Callback<Func<object>, Type>((accessor, type) =>
+ {
+ Assert.Equal(16, accessor());
+ Assert.Equal(typeof(int), type);
+ })
+ .Returns(() => null)
+ .Verifiable();
+
+ // Act
+ ModelMetadata.FromLambdaExpression<DummyContactModel, int>(m => m.Array[3], viewData, provider.Object);
+
+ // Assert
+ provider.Verify();
+ }
+
+ [Fact]
+ public void FromLambdaExpressionNullReferenceExceptionsInPropertyExpressionPreserveAllExpressionInformation()
+ {
+ // Arrange
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+ ViewDataDictionary<DummyContactModel> viewData = new ViewDataDictionary<DummyContactModel>();
+ provider.Setup(p => p.GetMetadataForProperty(It.IsAny<Func<object>>(), It.IsAny<Type>(), It.IsAny<string>()))
+ .Callback<Func<object>, Type, string>((accessor, type, propertyName) =>
+ {
+ Assert.Null(accessor());
+ Assert.Equal(typeof(DummyContactModel), type);
+ Assert.Equal("FirstName", propertyName);
+ })
+ .Returns(() => null)
+ .Verifiable();
+
+ // Act
+ ModelMetadata.FromLambdaExpression<DummyContactModel, string>(m => m.FirstName, viewData, provider.Object);
+
+ // Assert
+ provider.Verify();
+ }
+
+ [Fact]
+ public void FromLambdaExpressionSetsContainerTypeToDerivedMostType()
+ { // Dev10 Bug #868619
+ // Arrange
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+ ViewDataDictionary<DerivedModel> viewData = new ViewDataDictionary<DerivedModel>();
+ provider.Setup(p => p.GetMetadataForProperty(It.IsAny<Func<object>>(), It.IsAny<Type>(), It.IsAny<string>()))
+ .Callback<Func<object>, Type, string>((accessor, type, propertyName) =>
+ {
+ Assert.Null(accessor());
+ Assert.Equal(typeof(DerivedModel), type);
+ Assert.Equal("MyProperty", propertyName);
+ })
+ .Returns(() => null)
+ .Verifiable();
+
+ // Act
+ ModelMetadata.FromLambdaExpression<DerivedModel, string>(m => m.MyProperty, viewData, provider.Object);
+
+ // Assert
+ provider.Verify();
+ }
+
+ private class BaseModel
+ {
+ public virtual string MyProperty { get; set; }
+ }
+
+ private class DerivedModel : BaseModel
+ {
+ [Required]
+ public override string MyProperty
+ {
+ get { return base.MyProperty; }
+ set { base.MyProperty = value; }
+ }
+ }
+
+ // GetDisplayName()
+
+ [Fact]
+ public void ReturnsDisplayNameWhenSet()
+ {
+ // Arrange
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+ ModelMetadata metadata = new ModelMetadata(provider.Object, null, null, typeof(object), "PropertyName") { DisplayName = "Display Name" };
+
+ // Act
+ string result = metadata.GetDisplayName();
+
+ // Assert
+ Assert.Equal("Display Name", result);
+ }
+
+ [Fact]
+ public void ReturnsPropertyNameWhenSetAndDisplayNameIsNull()
+ {
+ // Arrange
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+ ModelMetadata metadata = new ModelMetadata(provider.Object, null, null, typeof(object), "PropertyName");
+
+ // Act
+ string result = metadata.GetDisplayName();
+
+ // Assert
+ Assert.Equal("PropertyName", result);
+ }
+
+ [Fact]
+ public void ReturnsTypeNameWhenPropertyNameAndDisplayNameAreNull()
+ {
+ // Arrange
+ Mock<ModelMetadataProvider> provider = new Mock<ModelMetadataProvider>();
+ ModelMetadata metadata = new ModelMetadata(provider.Object, null, null, typeof(object), null);
+
+ // Act
+ string result = metadata.GetDisplayName();
+
+ // Assert
+ Assert.Equal("Object", result);
+ }
+
+ // Helpers
+
+ private class DummyContactModel
+ {
+ public int IntField = 0;
+ public string FirstName { get; set; }
+ public string LastName { get; set; }
+ public Nullable<int> NullableIntValue { get; set; }
+ public int[] Array { get; set; }
+
+ public string this[int index]
+ {
+ get { return "Indexed into " + index; }
+ }
+ }
+
+ private class DummyModelContainer
+ {
+ public DummyContactModel Model { get; set; }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ModelStateDictionaryTest.cs b/test/System.Web.Mvc.Test/Test/ModelStateDictionaryTest.cs
new file mode 100644
index 00000000..deb9f014
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ModelStateDictionaryTest.cs
@@ -0,0 +1,309 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Runtime.Serialization.Formatters.Binary;
+using System.Web.TestUtil;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ModelStateDictionaryTest
+ {
+ [Fact]
+ public void AddModelErrorCreatesModelStateIfNotPresent()
+ {
+ // Arrange
+ ModelStateDictionary dictionary = new ModelStateDictionary();
+
+ // Act
+ dictionary.AddModelError("some key", "some error");
+
+ // Assert
+ KeyValuePair<string, ModelState> kvp = Assert.Single(dictionary);
+ Assert.Equal("some key", kvp.Key);
+ ModelError error = Assert.Single(kvp.Value.Errors);
+ Assert.Equal("some error", error.ErrorMessage);
+ }
+
+ [Fact]
+ public void AddModelErrorThrowsIfKeyIsNull()
+ {
+ // Arrange
+ ModelStateDictionary dictionary = new ModelStateDictionary();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { dictionary.AddModelError(null, (string)null); }, "key");
+ }
+
+ [Fact]
+ public void AddModelErrorUsesExistingModelStateIfPresent()
+ {
+ // Arrange
+ ModelStateDictionary dictionary = new ModelStateDictionary();
+ dictionary.AddModelError("some key", "some error");
+ Exception ex = new Exception();
+
+ // Act
+ dictionary.AddModelError("some key", ex);
+
+ // Assert
+ KeyValuePair<string, ModelState> kvp = Assert.Single(dictionary);
+ Assert.Equal("some key", kvp.Key);
+
+ Assert.Equal(2, kvp.Value.Errors.Count);
+ Assert.Equal("some error", kvp.Value.Errors[0].ErrorMessage);
+ Assert.Same(ex, kvp.Value.Errors[1].Exception);
+ }
+
+ [Fact]
+ public void ConstructorThrowsIfDictionaryIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new ModelStateDictionary((ModelStateDictionary)null); }, "dictionary");
+ }
+
+ [Fact]
+ public void ConstructorWithDictionaryParameter()
+ {
+ // Arrange
+ ModelStateDictionary oldDictionary = new ModelStateDictionary()
+ {
+ { "foo", new ModelState() { Value = HtmlHelperTest.GetValueProviderResult("bar", "bar") } }
+ };
+
+ // Act
+ ModelStateDictionary newDictionary = new ModelStateDictionary(oldDictionary);
+
+ // Assert
+ Assert.Single(newDictionary);
+ Assert.Equal("bar", newDictionary["foo"].Value.ConvertTo(typeof(string)));
+ }
+
+ [Fact]
+ public void DictionaryInterface()
+ {
+ // Arrange
+ DictionaryHelper<string, ModelState> helper = new DictionaryHelper<string, ModelState>()
+ {
+ Creator = () => new ModelStateDictionary(),
+ Comparer = StringComparer.OrdinalIgnoreCase,
+ SampleKeys = new string[] { "foo", "bar", "baz", "quux", "QUUX" },
+ SampleValues = new ModelState[] { new ModelState(), new ModelState(), new ModelState(), new ModelState(), new ModelState() },
+ ThrowOnKeyNotFound = false
+ };
+
+ // Act & assert
+ helper.Execute();
+ }
+
+ [Fact]
+ public void DictionaryIsSerializable()
+ {
+ // Arrange
+ MemoryStream stream = new MemoryStream();
+ BinaryFormatter formatter = new BinaryFormatter();
+
+ ModelStateDictionary originalDict = new ModelStateDictionary();
+ originalDict.AddModelError("foo", new InvalidOperationException("Some invalid operation."));
+ originalDict.AddModelError("foo", new InvalidOperationException("Some other invalid operation."));
+ originalDict.AddModelError("bar", "Some exception text.");
+ originalDict.SetModelValue("baz", new ValueProviderResult("rawValue", "attemptedValue", CultureInfo.GetCultureInfo("fr-FR")));
+
+ // Act
+ formatter.Serialize(stream, originalDict);
+ stream.Position = 0;
+ ModelStateDictionary deserializedDict = formatter.Deserialize(stream) as ModelStateDictionary;
+
+ // Assert
+ Assert.NotNull(deserializedDict);
+ Assert.Equal(3, deserializedDict.Count);
+
+ ModelState foo = deserializedDict["FOO"];
+ Assert.IsType<InvalidOperationException>(foo.Errors[0].Exception);
+ Assert.Equal("Some invalid operation.", foo.Errors[0].Exception.Message);
+ Assert.IsType<InvalidOperationException>(foo.Errors[1].Exception);
+ Assert.Equal("Some other invalid operation.", foo.Errors[1].Exception.Message);
+
+ ModelState bar = deserializedDict["BAR"];
+ Assert.Equal("Some exception text.", bar.Errors[0].ErrorMessage);
+
+ ModelState baz = deserializedDict["BAZ"];
+ Assert.Equal("rawValue", baz.Value.RawValue);
+ Assert.Equal("attemptedValue", baz.Value.AttemptedValue);
+ Assert.Equal(CultureInfo.GetCultureInfo("fr-FR"), baz.Value.Culture);
+ }
+
+ [Fact]
+ public void IsValidFieldReturnsFalseIfDictionaryDoesNotContainKey()
+ {
+ // Arrange
+ ModelStateDictionary msd = new ModelStateDictionary();
+
+ // Act
+ bool isValid = msd.IsValidField("foo");
+
+ // Assert
+ Assert.True(isValid);
+ }
+
+ [Fact]
+ public void IsValidFieldReturnsFalseIfKeyChildContainsErrors()
+ {
+ // Arrange
+ ModelStateDictionary msd = new ModelStateDictionary();
+ msd.AddModelError("foo.bar", "error text");
+
+ // Act
+ bool isValid = msd.IsValidField("foo");
+
+ // Assert
+ Assert.False(isValid);
+ }
+
+ [Fact]
+ public void IsValidFieldReturnsFalseIfKeyContainsErrors()
+ {
+ // Arrange
+ ModelStateDictionary msd = new ModelStateDictionary();
+ msd.AddModelError("foo", "error text");
+
+ // Act
+ bool isValid = msd.IsValidField("foo");
+
+ // Assert
+ Assert.False(isValid);
+ }
+
+ [Fact]
+ public void IsValidFieldReturnsTrueIfModelStateDoesNotContainErrors()
+ {
+ // Arrange
+ ModelStateDictionary msd = new ModelStateDictionary()
+ {
+ { "foo", new ModelState() { Value = new ValueProviderResult(null, null, null) } }
+ };
+
+ // Act
+ bool isValid = msd.IsValidField("foo");
+
+ // Assert
+ Assert.True(isValid);
+ }
+
+ [Fact]
+ public void IsValidFieldThrowsIfKeyIsNull()
+ {
+ // Arrange
+ ModelStateDictionary msd = new ModelStateDictionary();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { msd.IsValidField(null); }, "key");
+ }
+
+ [Fact]
+ public void IsValidPropertyReturnsFalseIfErrors()
+ {
+ // Arrange
+ ModelState errorState = new ModelState() { Value = HtmlHelperTest.GetValueProviderResult("quux", "quux") };
+ errorState.Errors.Add("some error");
+ ModelStateDictionary dictionary = new ModelStateDictionary()
+ {
+ { "foo", new ModelState() { Value = HtmlHelperTest.GetValueProviderResult("bar", "bar") } },
+ { "baz", errorState }
+ };
+
+ // Act
+ bool isValid = dictionary.IsValid;
+
+ // Assert
+ Assert.False(isValid);
+ }
+
+ [Fact]
+ public void IsValidPropertyReturnsTrueIfNoErrors()
+ {
+ // Arrange
+ ModelStateDictionary dictionary = new ModelStateDictionary()
+ {
+ { "foo", new ModelState() { Value = HtmlHelperTest.GetValueProviderResult("bar", "bar") } },
+ { "baz", new ModelState() { Value = HtmlHelperTest.GetValueProviderResult("quux", "bar") } }
+ };
+
+ // Act
+ bool isValid = dictionary.IsValid;
+
+ // Assert
+ Assert.True(isValid);
+ }
+
+ [Fact]
+ public void MergeCopiesDictionaryEntries()
+ {
+ // Arrange
+ ModelStateDictionary fooDict = new ModelStateDictionary() { { "foo", new ModelState() } };
+ ModelStateDictionary barDict = new ModelStateDictionary() { { "bar", new ModelState() } };
+
+ // Act
+ fooDict.Merge(barDict);
+
+ // Assert
+ Assert.Equal(2, fooDict.Count);
+ Assert.Equal(barDict["bar"], fooDict["bar"]);
+ }
+
+ [Fact]
+ public void MergeDoesNothingIfParameterIsNull()
+ {
+ // Arrange
+ ModelStateDictionary fooDict = new ModelStateDictionary() { { "foo", new ModelState() } };
+
+ // Act
+ fooDict.Merge(null);
+
+ // Assert
+ Assert.Single(fooDict);
+ Assert.True(fooDict.ContainsKey("foo"));
+ }
+
+ [Fact]
+ public void SetAttemptedValueCreatesModelStateIfNotPresent()
+ {
+ // Arrange
+ ModelStateDictionary dictionary = new ModelStateDictionary();
+
+ // Act
+ dictionary.SetModelValue("some key", HtmlHelperTest.GetValueProviderResult("some value", "some value"));
+
+ // Assert
+ Assert.Single(dictionary);
+ ModelState modelState = dictionary["some key"];
+
+ Assert.Empty(modelState.Errors);
+ Assert.Equal("some value", modelState.Value.ConvertTo(typeof(string)));
+ }
+
+ [Fact]
+ public void SetAttemptedValueUsesExistingModelStateIfPresent()
+ {
+ // Arrange
+ ModelStateDictionary dictionary = new ModelStateDictionary();
+ dictionary.AddModelError("some key", "some error");
+ Exception ex = new Exception();
+
+ // Act
+ dictionary.SetModelValue("some key", HtmlHelperTest.GetValueProviderResult("some value", "some value"));
+
+ // Assert
+ Assert.Single(dictionary);
+ ModelState modelState = dictionary["some key"];
+
+ Assert.Single(modelState.Errors);
+ Assert.Equal("some error", modelState.Errors[0].ErrorMessage);
+ Assert.Equal("some value", modelState.Value.ConvertTo(typeof(string)));
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ModelStateTest.cs b/test/System.Web.Mvc.Test/Test/ModelStateTest.cs
new file mode 100644
index 00000000..d3e1df95
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ModelStateTest.cs
@@ -0,0 +1,17 @@
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class ModelStateTest
+ {
+ [Fact]
+ public void ErrorsProperty()
+ {
+ // Arrange
+ ModelState modelState = new ModelState();
+
+ // Act & Assert
+ Assert.NotNull(modelState.Errors);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ModelValidationResultTest.cs b/test/System.Web.Mvc.Test/Test/ModelValidationResultTest.cs
new file mode 100644
index 00000000..18a51cd8
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ModelValidationResultTest.cs
@@ -0,0 +1,28 @@
+using System.Web.TestUtil;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class ModelValidationResultTest
+ {
+ [Fact]
+ public void MemberNameProperty()
+ {
+ // Arrange
+ ModelValidationResult result = new ModelValidationResult();
+
+ // Act & assert
+ MemberHelper.TestStringProperty(result, "MemberName", String.Empty);
+ }
+
+ [Fact]
+ public void MessageProperty()
+ {
+ // Arrange
+ ModelValidationResult result = new ModelValidationResult();
+
+ // Act & assert
+ MemberHelper.TestStringProperty(result, "Message", String.Empty);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ModelValidatorProviderCollectionTest.cs b/test/System.Web.Mvc.Test/Test/ModelValidatorProviderCollectionTest.cs
new file mode 100644
index 00000000..e9a1d58b
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ModelValidatorProviderCollectionTest.cs
@@ -0,0 +1,185 @@
+using System.Collections.Generic;
+using System.Linq;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ModelValidatorProviderCollectionTest
+ {
+ [Fact]
+ public void ListWrappingConstructor()
+ {
+ // Arrange
+ List<ModelValidatorProvider> list = new List<ModelValidatorProvider>()
+ {
+ new Mock<ModelValidatorProvider>().Object, new Mock<ModelValidatorProvider>().Object
+ };
+
+ // Act
+ ModelValidatorProviderCollection collection = new ModelValidatorProviderCollection(list);
+
+ // Assert
+ Assert.Equal(list, collection.ToList());
+ }
+
+ [Fact]
+ public void ListWrappingConstructorThrowsIfListIsNull()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { new ModelValidatorProviderCollection((IList<ModelValidatorProvider>)null); },
+ "list");
+ }
+
+ [Fact]
+ public void DefaultConstructor()
+ {
+ // Act
+ ModelValidatorProviderCollection collection = new ModelValidatorProviderCollection();
+
+ // Assert
+ Assert.Empty(collection);
+ }
+
+ [Fact]
+ public void AddNullModelValidatorProviderThrows()
+ {
+ // Arrange
+ ModelValidatorProviderCollection collection = new ModelValidatorProviderCollection();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { collection.Add(null); },
+ "item");
+ }
+
+ [Fact]
+ public void SetItem()
+ {
+ // Arrange
+ ModelValidatorProviderCollection collection = new ModelValidatorProviderCollection();
+ collection.Add(new Mock<ModelValidatorProvider>().Object);
+
+ ModelValidatorProvider newProvider = new Mock<ModelValidatorProvider>().Object;
+
+ // Act
+ collection[0] = newProvider;
+
+ // Assert
+ ModelValidatorProvider provider = Assert.Single(collection);
+ Assert.Equal(newProvider, provider);
+ }
+
+ [Fact]
+ public void SetNullModelValidatorProviderThrows()
+ {
+ // Arrange
+ ModelValidatorProviderCollection collection = new ModelValidatorProviderCollection();
+ collection.Add(new Mock<ModelValidatorProvider>().Object);
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { collection[0] = null; },
+ "item");
+ }
+
+ [Fact]
+ public void GetValidators()
+ {
+ // Arrange
+ ModelMetadata metadata = GetMetadata();
+ ControllerContext controllerContext = new ControllerContext();
+
+ ModelValidator[] allValidators = new ModelValidator[]
+ {
+ new SimpleModelValidator(),
+ new SimpleModelValidator(),
+ new SimpleModelValidator(),
+ new SimpleModelValidator(),
+ new SimpleModelValidator()
+ };
+
+ Mock<ModelValidatorProvider> provider1 = new Mock<ModelValidatorProvider>();
+ provider1.Setup(p => p.GetValidators(metadata, controllerContext)).Returns(new ModelValidator[]
+ {
+ allValidators[0], allValidators[1]
+ });
+
+ Mock<ModelValidatorProvider> provider2 = new Mock<ModelValidatorProvider>();
+ provider2.Setup(p => p.GetValidators(metadata, controllerContext)).Returns(new ModelValidator[]
+ {
+ allValidators[2], allValidators[3], allValidators[4]
+ });
+
+ ModelValidatorProviderCollection collection = new ModelValidatorProviderCollection();
+ collection.Add(provider1.Object);
+ collection.Add(provider2.Object);
+
+ // Act
+ IEnumerable<ModelValidator> returnedValidators = collection.GetValidators(metadata, controllerContext);
+
+ // Assert
+ Assert.Equal(allValidators, returnedValidators.ToArray());
+ }
+
+ [Fact]
+ public void GetValidatorsDelegatesToResolver()
+ {
+ // Arrange
+ ModelValidator[] allValidators = new ModelValidator[]
+ {
+ new SimpleModelValidator(),
+ new SimpleModelValidator(),
+ new SimpleModelValidator(),
+ new SimpleModelValidator()
+ };
+
+ ModelMetadata metadata = GetMetadata();
+ ControllerContext controllerContext = new ControllerContext();
+
+ Mock<ModelValidatorProvider> resolverProvider1 = new Mock<ModelValidatorProvider>();
+ resolverProvider1.Setup(p => p.GetValidators(metadata, controllerContext)).Returns(new ModelValidator[]
+ {
+ allValidators[0], allValidators[1]
+ });
+
+ Mock<ModelValidatorProvider> resolverprovider2 = new Mock<ModelValidatorProvider>();
+ resolverprovider2.Setup(p => p.GetValidators(metadata, controllerContext)).Returns(new ModelValidator[]
+ {
+ allValidators[2], allValidators[3]
+ });
+
+ Resolver<IEnumerable<ModelValidatorProvider>> resolver = new Resolver<IEnumerable<ModelValidatorProvider>>();
+ resolver.Current = new ModelValidatorProvider[] { resolverProvider1.Object, resolverprovider2.Object };
+
+ ModelValidatorProviderCollection collection = new ModelValidatorProviderCollection(resolver);
+
+ // Act
+ IEnumerable<ModelValidator> returnedValidators = collection.GetValidators(metadata, controllerContext);
+
+ // Assert
+ Assert.Equal(allValidators, returnedValidators.ToArray());
+ }
+
+ private static ModelMetadata GetMetadata()
+ {
+ ModelMetadataProvider provider = new EmptyModelMetadataProvider();
+ return provider.GetMetadataForType(null, typeof(object));
+ }
+
+ private sealed class SimpleModelValidator : ModelValidator
+ {
+ public SimpleModelValidator()
+ : base(GetMetadata(), new ControllerContext())
+ {
+ }
+
+ public override IEnumerable<ModelValidationResult> Validate(object container)
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ModelValidatorProvidersTest.cs b/test/System.Web.Mvc.Test/Test/ModelValidatorProvidersTest.cs
new file mode 100644
index 00000000..175ac7bb
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ModelValidatorProvidersTest.cs
@@ -0,0 +1,26 @@
+using System.Linq;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class ModelValidatorProvidersTest
+ {
+ [Fact]
+ public void CollectionDefaults()
+ {
+ // Arrange
+ Type[] expectedTypes = new Type[]
+ {
+ typeof(DataAnnotationsModelValidatorProvider),
+ typeof(DataErrorInfoModelValidatorProvider),
+ typeof(ClientDataTypeModelValidatorProvider)
+ };
+
+ // Act
+ Type[] actualTypes = ModelValidatorProviders.Providers.Select(p => p.GetType()).ToArray();
+
+ // Assert
+ Assert.Equal(expectedTypes, actualTypes);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ModelValidatorTest.cs b/test/System.Web.Mvc.Test/Test/ModelValidatorTest.cs
new file mode 100644
index 00000000..663de14e
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ModelValidatorTest.cs
@@ -0,0 +1,233 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ModelValidatorTest
+ {
+ [Fact]
+ public void ConstructorGuards()
+ {
+ // Arrange
+ ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(object));
+ ControllerContext context = new ControllerContext();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => new TestableModelValidator(null, context),
+ "metadata");
+ Assert.ThrowsArgumentNull(
+ () => new TestableModelValidator(metadata, null),
+ "controllerContext");
+ }
+
+ [Fact]
+ public void ValuesSet()
+ {
+ // Arrange
+ ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(() => 15, typeof(string), "Length");
+ ControllerContext context = new ControllerContext();
+
+ // Act
+ TestableModelValidator validator = new TestableModelValidator(metadata, context);
+
+ // Assert
+ Assert.Same(context, validator.ControllerContext);
+ Assert.Same(metadata, validator.Metadata);
+ }
+
+ [Fact]
+ public void NoClientRulesByDefault()
+ {
+ // Arrange
+ ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(() => 15, typeof(string), "Length");
+ ControllerContext context = new ControllerContext();
+
+ // Act
+ TestableModelValidator validator = new TestableModelValidator(metadata, context);
+
+ // Assert
+ Assert.Empty(validator.GetClientValidationRules());
+ }
+
+ [Fact]
+ public void IsRequiredFalseByDefault()
+ {
+ // Arrange
+ ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(() => 15, typeof(string), "Length");
+ ControllerContext context = new ControllerContext();
+
+ // Act
+ TestableModelValidator validator = new TestableModelValidator(metadata, context);
+
+ // Assert
+ Assert.False(validator.IsRequired);
+ }
+
+ [Fact]
+ public void GetModelValidator_DoesNotReadPropertyValues()
+ {
+ ModelValidatorProvider[] originalProviders = ModelValidatorProviders.Providers.ToArray();
+ try
+ {
+ // Arrange
+ ModelValidatorProviders.Providers.Clear();
+ ModelValidatorProviders.Providers.Add(new ObservableModelValidatorProvider());
+
+ ObservableModel model = new ObservableModel();
+ ModelMetadata metadata = new EmptyModelMetadataProvider().GetMetadataForType(() => model, typeof(ObservableModel));
+ ControllerContext controllerContext = new ControllerContext();
+
+ // Act
+ ModelValidator validator = ModelValidator.GetModelValidator(metadata, controllerContext);
+ ModelValidationResult[] results = validator.Validate(model).ToArray();
+
+ // Assert
+ Assert.False(model.PropertyWasRead());
+ }
+ finally
+ {
+ ModelValidatorProviders.Providers.Clear();
+ foreach (ModelValidatorProvider provider in originalProviders)
+ {
+ ModelValidatorProviders.Providers.Add(provider);
+ }
+ }
+ }
+
+ private class ObservableModelValidatorProvider : ModelValidatorProvider
+ {
+ public override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context)
+ {
+ return new ModelValidator[] { new ObservableModelValidator(metadata, context) };
+ }
+
+ private class ObservableModelValidator : ModelValidator
+ {
+ public ObservableModelValidator(ModelMetadata metadata, ControllerContext controllerContext)
+ : base(metadata, controllerContext)
+ {
+ }
+
+ public override IEnumerable<ModelValidationResult> Validate(object container)
+ {
+ return Enumerable.Empty<ModelValidationResult>();
+ }
+ }
+ }
+
+ private class ObservableModel
+ {
+ private bool _propertyWasRead;
+
+ public int TheProperty
+ {
+ get
+ {
+ _propertyWasRead = true;
+ return 42;
+ }
+ }
+
+ public bool PropertyWasRead()
+ {
+ return _propertyWasRead;
+ }
+ }
+
+ [Fact]
+ public void GetModelValidatorWithTypeLevelValidator()
+ {
+ // Arrange
+ ControllerContext context = new ControllerContext();
+ DataErrorInfo1 model = new DataErrorInfo1 { Error = "Some Type Error" };
+ ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, model.GetType());
+ ModelValidator validator = ModelValidator.GetModelValidator(metadata, context);
+
+ // Act
+ ModelValidationResult result = validator.Validate(null).Single();
+
+ // Assert
+ Assert.Equal(String.Empty, result.MemberName);
+ Assert.Equal("Some Type Error", result.Message);
+ }
+
+ [Fact]
+ public void GetModelValidatorWithPropertyLevelValidator()
+ {
+ // Arrange
+ ControllerContext context = new ControllerContext();
+ DataErrorInfo1 model = new DataErrorInfo1();
+ model["SomeStringProperty"] = "Some Property Error";
+ ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, model.GetType());
+ ModelValidator validator = ModelValidator.GetModelValidator(metadata, context);
+
+ // Act
+ ModelValidationResult result = validator.Validate(null).Single();
+
+ // Assert
+ Assert.Equal("SomeStringProperty", result.MemberName);
+ Assert.Equal("Some Property Error", result.Message);
+ }
+
+ [Fact]
+ public void GetModelValidatorWithFailedPropertyValidatorsPreventsTypeValidatorFromRunning()
+ {
+ // Arrange
+ ControllerContext context = new ControllerContext();
+ DataErrorInfo1 model = new DataErrorInfo1 { Error = "Some Type Error" };
+ model["SomeStringProperty"] = "Some Property Error";
+ model["SomeOtherStringProperty"] = "Some Other Property Error";
+ ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, model.GetType());
+ ModelValidator validator = ModelValidator.GetModelValidator(metadata, context);
+
+ // Act
+ List<ModelValidationResult> result = validator.Validate(null).ToList();
+
+ // Assert
+ Assert.Equal(2, result.Count);
+ Assert.Equal("SomeStringProperty", result[0].MemberName);
+ Assert.Equal("Some Property Error", result[0].Message);
+ Assert.Equal("SomeOtherStringProperty", result[1].MemberName);
+ Assert.Equal("Some Other Property Error", result[1].Message);
+ }
+
+ private class TestableModelValidator : ModelValidator
+ {
+ public TestableModelValidator(ModelMetadata metadata, ControllerContext context)
+ : base(metadata, context)
+ {
+ }
+
+ public override IEnumerable<ModelValidationResult> Validate(object container)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ private class DataErrorInfo1 : IDataErrorInfo
+ {
+ private readonly Dictionary<string, string> _errors = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+
+ public string SomeStringProperty { get; set; }
+
+ public string SomeOtherStringProperty { get; set; }
+
+ public string Error { get; set; }
+
+ public string this[string columnName]
+ {
+ get
+ {
+ string outVal;
+ _errors.TryGetValue(columnName, out outVal);
+ return outVal;
+ }
+ set { _errors[columnName] = value; }
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/MultiSelectListTest.cs b/test/System.Web.Mvc.Test/Test/MultiSelectListTest.cs
new file mode 100644
index 00000000..13173cf4
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/MultiSelectListTest.cs
@@ -0,0 +1,307 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class MultiSelectListTest
+ {
+ [Fact]
+ public void Constructor1SetsProperties()
+ {
+ // Arrange
+ IEnumerable items = new object[0];
+
+ // Act
+ MultiSelectList multiSelect = new MultiSelectList(items);
+
+ // Assert
+ Assert.Same(items, multiSelect.Items);
+ Assert.Null(multiSelect.DataValueField);
+ Assert.Null(multiSelect.DataTextField);
+ Assert.Null(multiSelect.SelectedValues);
+ }
+
+ [Fact]
+ public void Constructor2SetsProperties()
+ {
+ // Arrange
+ IEnumerable items = new object[0];
+ IEnumerable selectedValues = new object[0];
+
+ // Act
+ MultiSelectList multiSelect = new MultiSelectList(items, selectedValues);
+
+ // Assert
+ Assert.Same(items, multiSelect.Items);
+ Assert.Null(multiSelect.DataValueField);
+ Assert.Null(multiSelect.DataTextField);
+ Assert.Same(selectedValues, multiSelect.SelectedValues);
+ }
+
+ [Fact]
+ public void Constructor3SetsProperties()
+ {
+ // Arrange
+ IEnumerable items = new object[0];
+
+ // Act
+ MultiSelectList multiSelect = new MultiSelectList(items, "SomeValueField", "SomeTextField");
+
+ // Assert
+ Assert.Same(items, multiSelect.Items);
+ Assert.Equal("SomeValueField", multiSelect.DataValueField);
+ Assert.Equal("SomeTextField", multiSelect.DataTextField);
+ Assert.Null(multiSelect.SelectedValues);
+ }
+
+ [Fact]
+ public void Constructor4SetsProperties()
+ {
+ // Arrange
+ IEnumerable items = new object[0];
+ IEnumerable selectedValues = new object[0];
+
+ // Act
+ MultiSelectList multiSelect = new MultiSelectList(items, "SomeValueField", "SomeTextField", selectedValues);
+
+ // Assert
+ Assert.Same(items, multiSelect.Items);
+ Assert.Equal("SomeValueField", multiSelect.DataValueField);
+ Assert.Equal("SomeTextField", multiSelect.DataTextField);
+ Assert.Same(selectedValues, multiSelect.SelectedValues);
+ }
+
+ [Fact]
+ public void ConstructorWithNullItemsThrows()
+ {
+ Assert.ThrowsArgumentNull(
+ delegate { new MultiSelectList(null /* items */, "dataValueField", "dataTextField", null /* selectedValues */); }, "items");
+ }
+
+ [Fact]
+ public void GetListItemsThrowsOnBindingFailure()
+ {
+ // Arrange
+ MultiSelectList multiSelect = new MultiSelectList(GetSampleFieldObjects(),
+ "Text", "Value", new string[] { "A", "C", "T" });
+
+ // Assert
+ Assert.ThrowsHttpException(
+ delegate { IList<SelectListItem> listItems = multiSelect.GetListItems(); }, "DataBinding: 'System.Web.Mvc.Test.MultiSelectListTest+Item' does not contain a property with the name 'Text'.", 500);
+ }
+
+ [Fact]
+ public void GetListItemsWithoutValueField()
+ {
+ // Arrange
+ MultiSelectList multiSelect = new MultiSelectList(GetSampleStrings());
+
+ // Act
+ IList<SelectListItem> listItems = multiSelect.GetListItems();
+
+ // Assert
+ Assert.Equal(3, listItems.Count);
+ Assert.Null(listItems[0].Value);
+ Assert.Equal("Alpha", listItems[0].Text);
+ Assert.False(listItems[0].Selected);
+ Assert.Null(listItems[1].Value);
+ Assert.Equal("Bravo", listItems[1].Text);
+ Assert.False(listItems[1].Selected);
+ Assert.Null(listItems[2].Value);
+ Assert.Equal("Charlie", listItems[2].Text);
+ Assert.False(listItems[2].Selected);
+ }
+
+ [Fact]
+ public void GetListItemsWithoutValueFieldWithSelections()
+ {
+ // Arrange
+ MultiSelectList multiSelect = new MultiSelectList(GetSampleStrings(), new string[] { "Alpha", "Charlie", "Tango" });
+
+ // Act
+ IList<SelectListItem> listItems = multiSelect.GetListItems();
+
+ // Assert
+ Assert.Equal(3, listItems.Count);
+ Assert.Null(listItems[0].Value);
+ Assert.Equal("Alpha", listItems[0].Text);
+ Assert.True(listItems[0].Selected);
+ Assert.Null(listItems[1].Value);
+ Assert.Equal("Bravo", listItems[1].Text);
+ Assert.False(listItems[1].Selected);
+ Assert.Null(listItems[2].Value);
+ Assert.Equal("Charlie", listItems[2].Text);
+ Assert.True(listItems[2].Selected);
+ }
+
+ [Fact]
+ public void GetListItemsWithValueField()
+ {
+ // Arrange
+ MultiSelectList multiSelect = new MultiSelectList(GetSampleAnonymousObjects(), "Letter", "FullWord");
+
+ // Act
+ IList<SelectListItem> listItems = multiSelect.GetListItems();
+
+ // Assert
+ Assert.Equal(3, listItems.Count);
+ Assert.Equal("A", listItems[0].Value);
+ Assert.Equal("Alpha", listItems[0].Text);
+ Assert.False(listItems[0].Selected);
+ Assert.Equal("B", listItems[1].Value);
+ Assert.Equal("Bravo", listItems[1].Text);
+ Assert.False(listItems[1].Selected);
+ Assert.Equal("C", listItems[2].Value);
+ Assert.Equal("Charlie", listItems[2].Text);
+ Assert.False(listItems[2].Selected);
+ }
+
+ [Fact]
+ public void GetListItemsWithValueFieldWithSelections()
+ {
+ // Arrange
+ MultiSelectList multiSelect = new MultiSelectList(GetSampleAnonymousObjects(),
+ "Letter", "FullWord", new string[] { "A", "C", "T" });
+
+ // Act
+ IList<SelectListItem> listItems = multiSelect.GetListItems();
+
+ // Assert
+ Assert.Equal(3, listItems.Count);
+ Assert.Equal("A", listItems[0].Value);
+ Assert.Equal("Alpha", listItems[0].Text);
+ Assert.True(listItems[0].Selected);
+ Assert.Equal("B", listItems[1].Value);
+ Assert.Equal("Bravo", listItems[1].Text);
+ Assert.False(listItems[1].Selected);
+ Assert.Equal("C", listItems[2].Value);
+ Assert.Equal("Charlie", listItems[2].Text);
+ Assert.True(listItems[2].Selected);
+ }
+
+ [Fact]
+ public void IEnumerableWithAnonymousObjectsAndTextValueFields()
+ {
+ // Arrange
+ MultiSelectList multiSelect = new MultiSelectList(GetSampleAnonymousObjects(),
+ "Letter", "FullWord", new string[] { "A", "C", "T" });
+
+ // Act
+ IEnumerator enumerator = multiSelect.GetEnumerator();
+ enumerator.MoveNext();
+ SelectListItem firstItem = enumerator.Current as SelectListItem;
+ SelectListItem lastItem = null;
+
+ while (enumerator.MoveNext())
+ {
+ lastItem = enumerator.Current as SelectListItem;
+ }
+
+ // Assert
+ Assert.True(firstItem != null);
+ Assert.Equal("Alpha", firstItem.Text);
+ Assert.Equal("A", firstItem.Value);
+ Assert.True(firstItem.Selected);
+
+ Assert.True(lastItem != null);
+ Assert.Equal("Charlie", lastItem.Text);
+ Assert.Equal("C", lastItem.Value);
+ Assert.True(lastItem.Selected);
+ }
+
+ internal static IEnumerable GetSampleAnonymousObjects()
+ {
+ return new[]
+ {
+ new { Letter = 'A', FullWord = "Alpha" },
+ new { Letter = 'B', FullWord = "Bravo" },
+ new { Letter = 'C', FullWord = "Charlie" }
+ };
+ }
+
+ internal static IEnumerable GetSampleFieldObjects()
+ {
+ return new[]
+ {
+ new Item { Text = "A", Value = "Alpha" },
+ new Item { Text = "B", Value = "Bravo" },
+ new Item { Text = "C", Value = "Charlie" }
+ };
+ }
+
+ internal static List<SelectListItem> GetSampleListObjects()
+ {
+ List<SelectListItem> list = new List<SelectListItem>();
+ string selectedSSN = "111111111";
+
+ foreach (Person person in GetSamplePeople())
+ {
+ list.Add(new SelectListItem
+ {
+ Text = person.FirstName,
+ Value = person.SSN,
+ Selected = String.Equals(person.SSN, selectedSSN)
+ });
+ }
+ return list;
+ }
+
+ internal static IEnumerable<SelectListItem> GetSampleIEnumerableObjects()
+ {
+ Person[] people = GetSamplePeople();
+
+ string selectedSSN = "111111111";
+ IEnumerable<SelectListItem> list = from person in people
+ select new SelectListItem
+ {
+ Text = person.FirstName,
+ Value = person.SSN,
+ Selected = String.Equals(person.SSN, selectedSSN)
+ };
+ return list;
+ }
+
+ internal static IEnumerable GetSampleStrings()
+ {
+ return new string[] { "Alpha", "Bravo", "Charlie" };
+ }
+
+ internal static Person[] GetSamplePeople()
+ {
+ return new Person[]
+ {
+ new Person
+ {
+ FirstName = "John",
+ SSN = "123456789"
+ },
+ new Person
+ {
+ FirstName = "Jane",
+ SSN = "987654321"
+ },
+ new Person
+ {
+ FirstName = "Joe",
+ SSN = "111111111"
+ }
+ };
+ }
+
+ internal class Item
+ {
+ public string Text;
+ public string Value;
+ }
+
+ internal class Person
+ {
+ public string FirstName { get; set; }
+
+ public string SSN { get; set; }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/MultiServiceResolverTest.cs b/test/System.Web.Mvc.Test/Test/MultiServiceResolverTest.cs
new file mode 100644
index 00000000..22084ddb
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/MultiServiceResolverTest.cs
@@ -0,0 +1,136 @@
+using System.Collections.Generic;
+using System.Linq;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class MultiServiceResolverTest
+ {
+ [Fact]
+ public void ConstructorWithNullThunkArgumentThrows()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { new MultiServiceResolver<TestProvider>(null); },
+ "itemsThunk");
+ }
+
+ [Fact]
+ public void CurrentPrependsFromResolver()
+ {
+ // Arrange
+ IEnumerable<TestProvider> providersFromServiceLocation = GetProvidersFromService();
+ IEnumerable<TestProvider> providersFromItemsThunk = GetProvidersFromItemsThunk();
+ IEnumerable<TestProvider> expectedProviders = providersFromServiceLocation.Concat(providersFromItemsThunk);
+
+ Mock<IDependencyResolver> resolver = new Mock<IDependencyResolver>();
+ resolver.Setup(r => r.GetServices(typeof(TestProvider)))
+ .Returns(providersFromServiceLocation);
+
+ MultiServiceResolver<TestProvider> multiResolver = new MultiServiceResolver<TestProvider>(() => providersFromItemsThunk, resolver.Object);
+
+ // Act
+ IEnumerable<TestProvider> returnedProviders = multiResolver.Current;
+
+ // Assert
+ Assert.Equal(expectedProviders.ToList(), returnedProviders.ToList());
+ }
+
+ [Fact]
+ public void CurrentCachesResolverResult()
+ {
+ // Arrange
+ IEnumerable<TestProvider> providersFromServiceLocation = GetProvidersFromService();
+ IEnumerable<TestProvider> providersFromItemsThunk = GetProvidersFromItemsThunk();
+ IEnumerable<TestProvider> expectedProviders = providersFromServiceLocation.Concat(providersFromItemsThunk);
+
+ Mock<IDependencyResolver> resolver = new Mock<IDependencyResolver>();
+ resolver.Setup(r => r.GetServices(typeof(TestProvider)))
+ .Returns(providersFromServiceLocation);
+
+ MultiServiceResolver<TestProvider> multiResolver = new MultiServiceResolver<TestProvider>(() => providersFromItemsThunk, resolver.Object);
+
+ // Act
+ IEnumerable<TestProvider> returnedProviders = multiResolver.Current;
+ IEnumerable<TestProvider> cachedProviders = multiResolver.Current;
+
+ // Assert
+ Assert.Equal(expectedProviders.ToList(), returnedProviders.ToList());
+ Assert.Equal(expectedProviders.ToList(), cachedProviders.ToList());
+ resolver.Verify(r => r.GetServices(typeof(TestProvider)), Times.Exactly(1));
+ }
+
+ [Fact]
+ public void CurrentReturnsCurrentItemsWhenResolverReturnsNoInstances()
+ {
+ // Arrange
+ IEnumerable<TestProvider> providersFromItemsThunk = GetProvidersFromItemsThunk();
+ MultiServiceResolver<TestProvider> resolver = new MultiServiceResolver<TestProvider>(() => providersFromItemsThunk);
+
+ // Act
+ IEnumerable<TestProvider> returnedProviders = resolver.Current;
+
+ // Assert
+ Assert.Equal(providersFromItemsThunk.ToList(), returnedProviders.ToList());
+ }
+
+ [Fact]
+ public void CurrentDoesNotQueryResolverAfterNoInstancesAreReturned()
+ {
+ // Arrange
+ IEnumerable<TestProvider> providersFromItemsThunk = GetProvidersFromItemsThunk();
+ Mock<IDependencyResolver> resolver = new Mock<IDependencyResolver>();
+ resolver.Setup(r => r.GetServices(typeof(TestProvider)))
+ .Returns(new TestProvider[0]);
+ MultiServiceResolver<TestProvider> multiResolver = new MultiServiceResolver<TestProvider>(() => providersFromItemsThunk, resolver.Object);
+
+ // Act
+ IEnumerable<TestProvider> returnedProviders = multiResolver.Current;
+ IEnumerable<TestProvider> cachedProviders = multiResolver.Current;
+
+ // Assert
+ Assert.Equal(providersFromItemsThunk.ToList(), returnedProviders.ToList());
+ Assert.Equal(providersFromItemsThunk.ToList(), cachedProviders.ToList());
+ resolver.Verify(r => r.GetServices(typeof(TestProvider)), Times.Exactly(1));
+ }
+
+ [Fact]
+ public void CurrentPropagatesExceptionWhenResolverThrowsNonActivationException()
+ {
+ // Arrange
+ Mock<IDependencyResolver> resolver = new Mock<IDependencyResolver>(MockBehavior.Strict);
+ MultiServiceResolver<TestProvider> multiResolver = new MultiServiceResolver<TestProvider>(() => null, resolver.Object);
+
+ // Act & Assert
+ Assert.Throws<MockException>(
+ () => multiResolver.Current,
+ @"IDependencyResolver.GetServices(System.Web.Mvc.Test.MultiServiceResolverTest+TestProvider) invocation failed with mock behavior Strict.
+All invocations on the mock must have a corresponding setup."
+ );
+ }
+
+ private class TestProvider
+ {
+ }
+
+ private IEnumerable<TestProvider> GetProvidersFromService()
+ {
+ return new TestProvider[]
+ {
+ new TestProvider(),
+ new TestProvider()
+ };
+ }
+
+ private IEnumerable<TestProvider> GetProvidersFromItemsThunk()
+ {
+ return new TestProvider[]
+ {
+ new TestProvider(),
+ new TestProvider()
+ };
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/MvcHandlerTest.cs b/test/System.Web.Mvc.Test/Test/MvcHandlerTest.cs
new file mode 100644
index 00000000..7b874fb4
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/MvcHandlerTest.cs
@@ -0,0 +1,360 @@
+using System.Web.Mvc.Async;
+using System.Web.Mvc.Async.Test;
+using System.Web.Routing;
+using System.Web.SessionState;
+using Moq;
+using Moq.Protected;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class MvcHandlerTest
+ {
+ [Fact]
+ public void ConstructorWithNullRequestContextThrows()
+ {
+ Assert.ThrowsArgumentNull(
+ delegate { new MvcHandler(null); },
+ "requestContext");
+ }
+
+ [Fact]
+ public void ProcessRequestWithRouteWithoutControllerThrows()
+ {
+ // Arrange
+ Mock<HttpContextBase> contextMock = new Mock<HttpContextBase>();
+ contextMock.ExpectMvcVersionResponseHeader().Verifiable();
+ RouteData rd = new RouteData();
+ MvcHandler mvcHandler = new MvcHandler(new RequestContext(contextMock.Object, rd));
+
+ // Act
+ Assert.Throws<InvalidOperationException>(
+ delegate { mvcHandler.ProcessRequest(contextMock.Object); },
+ "The RouteData must contain an item named 'controller' with a non-empty string value.");
+
+ // Assert
+ contextMock.Verify();
+ }
+
+ [Fact]
+ public void ProcessRequestAddsServerHeaderCallsExecute()
+ {
+ // Arrange
+ Mock<HttpContextBase> contextMock = new Mock<HttpContextBase>();
+ contextMock.ExpectMvcVersionResponseHeader().Verifiable();
+
+ RouteData rd = new RouteData();
+ rd.Values.Add("controller", "foo");
+ RequestContext requestContext = new RequestContext(contextMock.Object, rd);
+ MvcHandler mvcHandler = new MvcHandler(requestContext);
+
+ Mock<ControllerBase> controllerMock = new Mock<ControllerBase>();
+ controllerMock.Protected().Setup("Execute", requestContext).Verifiable();
+
+ ControllerBuilder cb = new ControllerBuilder();
+ Mock<IControllerFactory> controllerFactoryMock = new Mock<IControllerFactory>();
+ controllerFactoryMock.Setup(o => o.CreateController(requestContext, "foo")).Returns(controllerMock.Object);
+ controllerFactoryMock.Setup(o => o.ReleaseController(controllerMock.Object));
+ cb.SetControllerFactory(controllerFactoryMock.Object);
+ mvcHandler.ControllerBuilder = cb;
+
+ // Act
+ mvcHandler.ProcessRequest(contextMock.Object);
+
+ // Assert
+ contextMock.Verify();
+ controllerMock.Verify();
+ }
+
+ [Fact]
+ public void ProcessRequestRemovesOptionalParametersFromRouteValueDictionary()
+ {
+ // Arrange
+ Mock<HttpContextBase> contextMock = new Mock<HttpContextBase>();
+ contextMock.ExpectMvcVersionResponseHeader();
+
+ RouteData rd = new RouteData();
+ rd.Values.Add("controller", "foo");
+ rd.Values.Add("optional", UrlParameter.Optional);
+ RequestContext requestContext = new RequestContext(contextMock.Object, rd);
+ MvcHandler mvcHandler = new MvcHandler(requestContext);
+
+ Mock<ControllerBase> controllerMock = new Mock<ControllerBase>();
+ controllerMock.Protected().Setup("Execute", requestContext).Verifiable();
+
+ ControllerBuilder cb = new ControllerBuilder();
+ Mock<IControllerFactory> controllerFactoryMock = new Mock<IControllerFactory>();
+ controllerFactoryMock.Setup(o => o.CreateController(requestContext, "foo")).Returns(controllerMock.Object);
+ controllerFactoryMock.Setup(o => o.ReleaseController(controllerMock.Object));
+ cb.SetControllerFactory(controllerFactoryMock.Object);
+ mvcHandler.ControllerBuilder = cb;
+
+ // Act
+ mvcHandler.ProcessRequest(contextMock.Object);
+
+ // Assert
+ controllerMock.Verify();
+ Assert.False(rd.Values.ContainsKey("optional"));
+ }
+
+ [Fact]
+ public void ProcessRequestWithDisabledServerHeaderOnlyCallsExecute()
+ {
+ bool oldResponseHeaderValue = MvcHandler.DisableMvcResponseHeader;
+ try
+ {
+ // Arrange
+ MvcHandler.DisableMvcResponseHeader = true;
+ Mock<HttpContextBase> contextMock = new Mock<HttpContextBase>();
+
+ RouteData rd = new RouteData();
+ rd.Values.Add("controller", "foo");
+ RequestContext requestContext = new RequestContext(contextMock.Object, rd);
+ MvcHandler mvcHandler = new MvcHandler(requestContext);
+
+ Mock<ControllerBase> controllerMock = new Mock<ControllerBase>();
+ controllerMock.Protected().Setup("Execute", requestContext).Verifiable();
+
+ ControllerBuilder cb = new ControllerBuilder();
+ Mock<IControllerFactory> controllerFactoryMock = new Mock<IControllerFactory>();
+ controllerFactoryMock.Setup(o => o.CreateController(requestContext, "foo")).Returns(controllerMock.Object);
+ controllerFactoryMock.Setup(o => o.ReleaseController(controllerMock.Object));
+ cb.SetControllerFactory(controllerFactoryMock.Object);
+ mvcHandler.ControllerBuilder = cb;
+
+ // Act
+ mvcHandler.ProcessRequest(contextMock.Object);
+
+ // Assert
+ controllerMock.Verify();
+ }
+ finally
+ {
+ MvcHandler.DisableMvcResponseHeader = oldResponseHeaderValue;
+ }
+ }
+
+ [Fact]
+ public void ProcessRequestDisposesControllerIfExecuteDoesNotThrowException()
+ {
+ // Arrange
+ Mock<ControllerBase> mockController = new Mock<ControllerBase>();
+ mockController.As<IDisposable>(); // so that Verify can be called on Dispose later
+ mockController.Protected().Setup("Execute", ItExpr.IsAny<RequestContext>()).Verifiable();
+
+ ControllerBuilder builder = new ControllerBuilder();
+ builder.SetControllerFactory(new SimpleControllerFactory(mockController.Object));
+
+ Mock<HttpContextBase> contextMock = new Mock<HttpContextBase>();
+ contextMock.ExpectMvcVersionResponseHeader().Verifiable();
+ RequestContext requestContext = new RequestContext(contextMock.Object, new RouteData());
+ requestContext.RouteData.Values["controller"] = "fooController";
+ MvcHandler handler = new MvcHandler(requestContext)
+ {
+ ControllerBuilder = builder
+ };
+
+ // Act
+ handler.ProcessRequest(requestContext.HttpContext);
+
+ // Assert
+ mockController.Verify();
+ contextMock.Verify();
+ mockController.As<IDisposable>().Verify(d => d.Dispose(), Times.AtMostOnce());
+ }
+
+ [Fact]
+ public void ProcessRequestDisposesControllerIfExecuteThrowsException()
+ {
+ // Arrange
+ Mock<ControllerBase> mockController = new Mock<ControllerBase>(MockBehavior.Strict);
+ mockController.As<IDisposable>().Setup(d => d.Dispose()); // so that Verify can be called on Dispose later
+ mockController.Protected().Setup("Execute", ItExpr.IsAny<RequestContext>()).Throws(new Exception("some exception"));
+
+ ControllerBuilder builder = new ControllerBuilder();
+ builder.SetControllerFactory(new SimpleControllerFactory(mockController.Object));
+
+ Mock<HttpContextBase> contextMock = new Mock<HttpContextBase>();
+ contextMock.ExpectMvcVersionResponseHeader().Verifiable();
+ RequestContext requestContext = new RequestContext(contextMock.Object, new RouteData());
+ requestContext.RouteData.Values["controller"] = "fooController";
+ MvcHandler handler = new MvcHandler(requestContext)
+ {
+ ControllerBuilder = builder
+ };
+
+ // Act
+ Assert.Throws<Exception>(
+ delegate { handler.ProcessRequest(requestContext.HttpContext); },
+ "some exception");
+
+ // Assert
+ mockController.Verify();
+ contextMock.Verify();
+ mockController.As<IDisposable>().Verify(d => d.Dispose(), Times.AtMostOnce());
+ }
+
+ [Fact]
+ public void ProcessRequestAsync_AsyncController_DisposesControllerOnException()
+ {
+ // Arrange
+ Mock<IAsyncController> mockController = new Mock<IAsyncController>();
+ mockController.Setup(o => o.BeginExecute(It.IsAny<RequestContext>(), It.IsAny<AsyncCallback>(), It.IsAny<object>())).Throws(new Exception("Some exception text."));
+ mockController.As<IDisposable>().Setup(o => o.Dispose()).Verifiable();
+
+ MvcHandler handler = GetMvcHandler(mockController.Object);
+
+ // Act & assert
+ Assert.Throws<Exception>(
+ delegate { handler.BeginProcessRequest(handler.RequestContext.HttpContext, null, null); },
+ @"Some exception text.");
+
+ mockController.Verify();
+ }
+
+ [Fact]
+ public void ProcessRequestAsync_AsyncController_NormalExecution()
+ {
+ // Arrange
+ MockAsyncResult innerAsyncResult = new MockAsyncResult();
+ bool disposeWasCalled = false;
+
+ Mock<IAsyncController> mockController = new Mock<IAsyncController>();
+ mockController.Setup(o => o.BeginExecute(It.IsAny<RequestContext>(), It.IsAny<AsyncCallback>(), It.IsAny<object>())).Returns(innerAsyncResult);
+ mockController.As<IDisposable>().Setup(o => o.Dispose()).Callback(delegate { disposeWasCalled = true; });
+
+ MvcHandler handler = GetMvcHandler(mockController.Object);
+
+ // Act & assert
+ IAsyncResult outerAsyncResult = handler.BeginProcessRequest(handler.RequestContext.HttpContext, null, null);
+ Assert.False(disposeWasCalled);
+
+ handler.EndProcessRequest(outerAsyncResult);
+ Assert.True(disposeWasCalled);
+ mockController.Verify(o => o.EndExecute(innerAsyncResult), Times.AtMostOnce());
+ }
+
+ [Fact]
+ public void ProcessRequestAsync_SyncController_NormalExecution()
+ {
+ // Arrange
+ bool executeWasCalled = false;
+ bool disposeWasCalled = false;
+
+ Mock<IController> mockController = new Mock<IController>();
+ mockController.Setup(o => o.Execute(It.IsAny<RequestContext>())).Callback(delegate { executeWasCalled = true; });
+ mockController.As<IDisposable>().Setup(o => o.Dispose()).Callback(delegate { disposeWasCalled = true; });
+
+ MvcHandler handler = GetMvcHandler(mockController.Object);
+
+ // Act & assert
+ IAsyncResult outerAsyncResult = handler.BeginProcessRequest(handler.RequestContext.HttpContext, null, null);
+ Assert.False(executeWasCalled);
+ Assert.False(disposeWasCalled);
+
+ handler.EndProcessRequest(outerAsyncResult);
+ Assert.True(executeWasCalled);
+ Assert.True(disposeWasCalled);
+ }
+
+ // Test that execute is called on a user controller that derives from Controller
+ [Fact]
+ public void ProcessRequestAsync_SyncController_NormalExecution2()
+ {
+ // Arrange
+ MyCustomerController controller = new MyCustomerController();
+
+ MvcHandler handler = GetMvcHandler(controller);
+ handler.RequestContext.RouteData.Values["action"] = "Widget";
+
+ // Act
+ IAsyncResult outerAsyncResult = handler.BeginProcessRequest(handler.RequestContext.HttpContext, null, null);
+ handler.EndProcessRequest(outerAsyncResult);
+
+ // Assert
+ Assert.Equal(1, controller.Called);
+ }
+
+ private class EmptyActionInvoker : IActionInvoker
+ {
+ public bool InvokeAction(ControllerContext controllerContext, string actionName)
+ {
+ return true; // action handled.
+ }
+ }
+
+ // Class that captures how an end-user would override and use a Controller
+ private class MyCustomerController : Controller
+ {
+ public int Called { get; set; }
+
+ protected override void ExecuteCore()
+ {
+ this.Called++;
+ }
+
+ protected override IActionInvoker CreateActionInvoker()
+ {
+ // The default action invoker relies on too much state (like having an HttpRequestContext), so stub it out.
+ return new EmptyActionInvoker();
+ }
+
+ // Workaround.
+ protected override bool DisableAsyncSupport
+ {
+ get
+ {
+ return true; // ensure ExecuteCore still gets called.
+ }
+ }
+ }
+
+ private static MvcHandler GetMvcHandler(IController controller)
+ {
+ Mock<HttpContextBase> mockHttpContext = new Mock<HttpContextBase>();
+ mockHttpContext.Setup(o => o.Response.AddHeader("X-AspNetMvc-Version", "2.0"));
+
+ RouteData routeData = new RouteData();
+ routeData.Values["controller"] = "SomeController";
+ RequestContext requestContext = new RequestContext(mockHttpContext.Object, routeData);
+
+ ControllerBuilder controllerBuilder = new ControllerBuilder();
+ controllerBuilder.SetControllerFactory(new SimpleControllerFactory(controller));
+
+ return new MvcHandler(requestContext)
+ {
+ ControllerBuilder = controllerBuilder
+ };
+ }
+
+ private class SimpleControllerFactory : IControllerFactory
+ {
+ private IController _instance;
+
+ public SimpleControllerFactory(IController instance)
+ {
+ _instance = instance;
+ }
+
+ public IController CreateController(RequestContext context, string controllerName)
+ {
+ return _instance;
+ }
+
+ public SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName)
+ {
+ return SessionStateBehavior.Default;
+ }
+
+ public void ReleaseController(IController controller)
+ {
+ IDisposable disposable = controller as IDisposable;
+ if (disposable != null)
+ {
+ disposable.Dispose();
+ }
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/MvcHtmlStringTest.cs b/test/System.Web.Mvc.Test/Test/MvcHtmlStringTest.cs
new file mode 100644
index 00000000..519d0e50
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/MvcHtmlStringTest.cs
@@ -0,0 +1,62 @@
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class MvcHtmlStringTest
+ {
+ // IsNullOrEmpty
+
+ [Fact]
+ public void IsNullOrEmptyTests()
+ {
+ // Act & Assert
+ Assert.True(MvcHtmlString.IsNullOrEmpty(null));
+ Assert.True(MvcHtmlString.IsNullOrEmpty(MvcHtmlString.Empty));
+ Assert.True(MvcHtmlString.IsNullOrEmpty(MvcHtmlString.Create("")));
+ Assert.False(MvcHtmlString.IsNullOrEmpty(MvcHtmlString.Create(" ")));
+ }
+
+ // ToHtmlString
+
+ [Fact]
+ public void ToHtmlStringReturnsOriginalString()
+ {
+ // Arrange
+ MvcHtmlString htmlString = MvcHtmlString.Create("some value");
+
+ // Act
+ string retVal = htmlString.ToHtmlString();
+
+ // Assert
+ Assert.Equal("some value", retVal);
+ }
+
+ // ToString
+
+ [Fact]
+ public void ToStringReturnsOriginalString()
+ {
+ // Arrange
+ MvcHtmlString htmlString = MvcHtmlString.Create("some value");
+
+ // Act
+ string retVal = htmlString.ToString();
+
+ // Assert
+ Assert.Equal("some value", retVal);
+ }
+
+ [Fact]
+ public void ToStringReturnsEmptyStringIfOriginalStringWasNull()
+ {
+ // Arrange
+ MvcHtmlString htmlString = MvcHtmlString.Create(null);
+
+ // Act
+ string retVal = htmlString.ToString();
+
+ // Assert
+ Assert.Equal("", retVal);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/MvcHttpHandlerTest.cs b/test/System.Web.Mvc.Test/Test/MvcHttpHandlerTest.cs
new file mode 100644
index 00000000..14ccc5f0
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/MvcHttpHandlerTest.cs
@@ -0,0 +1,63 @@
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class MvcHttpHandlerTest
+ {
+ [Fact]
+ public void ConstructorDoesNothing()
+ {
+ new MvcHttpHandler();
+ }
+
+ [Fact]
+ public void VerifyAndProcessRequestWithNullHandlerThrows()
+ {
+ // Arrange
+ PublicMvcHttpHandler handler = new PublicMvcHttpHandler();
+
+ // Act
+ Assert.ThrowsArgumentNull(
+ delegate { handler.PublicVerifyAndProcessRequest(null, null); },
+ "httpHandler");
+ }
+
+ [Fact]
+ public void ProcessRequestCallsExecute()
+ {
+ // Arrange
+ PublicMvcHttpHandler handler = new PublicMvcHttpHandler();
+ Mock<IHttpHandler> mockTargetHandler = new Mock<IHttpHandler>();
+ mockTargetHandler.Setup(h => h.ProcessRequest(It.IsAny<HttpContext>())).Verifiable();
+
+ // Act
+ handler.PublicVerifyAndProcessRequest(mockTargetHandler.Object, null);
+
+ // Assert
+ mockTargetHandler.Verify();
+ }
+
+ private sealed class DummyHttpHandler : IHttpHandler
+ {
+ bool IHttpHandler.IsReusable
+ {
+ get { throw new NotImplementedException(); }
+ }
+
+ void IHttpHandler.ProcessRequest(HttpContext context)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ private sealed class PublicMvcHttpHandler : MvcHttpHandler
+ {
+ public void PublicVerifyAndProcessRequest(IHttpHandler httpHandler, HttpContextBase httpContext)
+ {
+ base.VerifyAndProcessRequest(httpHandler, httpContext);
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/MvcRouteHandlerTest.cs b/test/System.Web.Mvc.Test/Test/MvcRouteHandlerTest.cs
new file mode 100644
index 00000000..c5d9d8a8
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/MvcRouteHandlerTest.cs
@@ -0,0 +1,71 @@
+using System.Web.Routing;
+using System.Web.SessionState;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class MvcRouteHandlerTest
+ {
+ [Fact]
+ public void GetHttpHandlerReturnsMvcHandlerWithRouteData()
+ {
+ // Arrange
+ var routeData = new RouteData();
+ routeData.Values["controller"] = "controllerName";
+ var context = new RequestContext(new Mock<HttpContextBase>().Object, routeData);
+ var controllerFactory = new Mock<IControllerFactory>();
+ controllerFactory.Setup(f => f.GetControllerSessionBehavior(context, "controllerName"))
+ .Returns(SessionStateBehavior.Default)
+ .Verifiable();
+ IRouteHandler rh = new MvcRouteHandler(controllerFactory.Object);
+
+ // Act
+ IHttpHandler httpHandler = rh.GetHttpHandler(context);
+
+ // Assert
+ MvcHandler h = httpHandler as MvcHandler;
+ Assert.NotNull(h);
+ Assert.Equal(context, h.RequestContext);
+ }
+
+ [Fact]
+ public void GetHttpHandlerAsksControllerFactoryForSessionBehaviorOfController()
+ {
+ // Arrange
+ var httpContext = new Mock<HttpContextBase>();
+ var routeData = new RouteData();
+ routeData.Values["controller"] = "controllerName";
+ var requestContext = new RequestContext(httpContext.Object, routeData);
+ var controllerFactory = new Mock<IControllerFactory>();
+ controllerFactory.Setup(f => f.GetControllerSessionBehavior(requestContext, "controllerName"))
+ .Returns(SessionStateBehavior.ReadOnly)
+ .Verifiable();
+ IRouteHandler routeHandler = new MvcRouteHandler(controllerFactory.Object);
+
+ // Act
+ routeHandler.GetHttpHandler(requestContext);
+
+ // Assert
+ controllerFactory.Verify();
+ httpContext.Verify(c => c.SetSessionStateBehavior(SessionStateBehavior.ReadOnly));
+ }
+
+ [Fact]
+ public void GetHttpHandlerThrowsIfTheRouteValuesDoesNotIncludeAControllerName()
+ {
+ // Arrange
+ var httpContext = new Mock<HttpContextBase>();
+ var routeData = new RouteData();
+ var requestContext = new RequestContext(httpContext.Object, routeData);
+ IRouteHandler routeHandler = new MvcRouteHandler();
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ () => routeHandler.GetHttpHandler(requestContext),
+ "The matched route does not include a 'controller' route value, which is required."
+ );
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/MvcTestHelper.cs b/test/System.Web.Mvc.Test/Test/MvcTestHelper.cs
new file mode 100644
index 00000000..ff714aaa
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/MvcTestHelper.cs
@@ -0,0 +1,102 @@
+using System.Reflection;
+using System.Reflection.Emit;
+
+namespace System.Web.Mvc.Test
+{
+ internal static class MvcTestHelper
+ {
+ private static bool _mvcAssembliesCreated;
+
+ public static void CreateMvcAssemblies()
+ {
+ // Only create MVC assemblies once per appdomain. This method is called from the static ctor of several
+ // test classes.
+ if (_mvcAssembliesCreated)
+ {
+ return;
+ }
+
+ CreateMvcTestAssembly1();
+ CreateMvcTestAssembly2();
+ CreateMvcTestAssembly3();
+ CreateMvcTestAssembly4();
+
+ _mvcAssembliesCreated = true;
+ }
+
+ private static void CreateMvcTestAssembly1()
+ {
+ AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(
+ new AssemblyName("MvcAssembly1"),
+ AssemblyBuilderAccess.Save);
+ ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule(
+ "MvcAssembly1", "MvcAssembly1.dll");
+
+ CreateController(moduleBuilder, "NS1a.NS1b.C1Controller");
+ CreateController(moduleBuilder, "NS2a.NS2b.C2Controller");
+
+ assemblyBuilder.Save("MvcAssembly1.dll");
+ }
+
+ private static void CreateMvcTestAssembly2()
+ {
+ AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(
+ new AssemblyName("MvcAssembly2"),
+ AssemblyBuilderAccess.Save);
+ ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule(
+ "MvcAssembly2", "MvcAssembly2.dll");
+
+ CreateController(moduleBuilder, "NS3a.NS3b.C3Controller");
+ CreateController(moduleBuilder, "NS4a.NS4b.C4Controller");
+
+ assemblyBuilder.Save("MvcAssembly2.dll");
+ }
+
+ private static void CreateMvcTestAssembly3()
+ {
+ AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(
+ new AssemblyName("MvcAssembly3"),
+ AssemblyBuilderAccess.Save);
+ ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule(
+ "MvcAssembly3", "MvcAssembly3.dll");
+
+ // Type names (but not namespaces) are the same as those in TestAssembly1
+ CreateController(moduleBuilder, "NS3a.NS3b.C1Controller");
+ CreateController(moduleBuilder, "NS4a.NS4b.C2Controller");
+
+ assemblyBuilder.Save("MvcAssembly3.dll");
+ }
+
+ private static void CreateMvcTestAssembly4()
+ {
+ AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(
+ new AssemblyName("MvcAssembly4"),
+ AssemblyBuilderAccess.Save);
+ ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule(
+ "MvcAssembly4", "MvcAssembly4.dll");
+
+ // Namespaces and type names are the same as those in TestAssembly1
+ CreateController(moduleBuilder, "NS1a.NS1b.C1Controller");
+ CreateController(moduleBuilder, "NS2a.NS2b.C2Controller");
+
+ assemblyBuilder.Save("MvcAssembly4.dll");
+ }
+
+ private static void CreateController(ModuleBuilder moduleBuilder, string typeName)
+ {
+ //namespace {namespace} {
+ // public class {typename} : ControllerBase {
+ // protected virtual void ExecuteCore() {
+ // return;
+ // }
+ // }
+ //}
+
+ TypeBuilder controllerTypeBuilder = moduleBuilder.DefineType(typeName, TypeAttributes.Class | TypeAttributes.Public, typeof(ControllerBase));
+ MethodBuilder executeMethodBuilder = controllerTypeBuilder.DefineMethod("ExecuteCore", MethodAttributes.Family | MethodAttributes.Virtual, typeof(void), Type.EmptyTypes);
+ executeMethodBuilder.GetILGenerator().Emit(OpCodes.Ret);
+ controllerTypeBuilder.DefineMethodOverride(executeMethodBuilder, typeof(ControllerBase).GetMethod("ExecuteCore", BindingFlags.Instance | BindingFlags.NonPublic));
+ controllerTypeBuilder.CreateType();
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/MvcWebRazorHostFactoryTest.cs b/test/System.Web.Mvc.Test/Test/MvcWebRazorHostFactoryTest.cs
new file mode 100644
index 00000000..0d23f365
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/MvcWebRazorHostFactoryTest.cs
@@ -0,0 +1,56 @@
+using System.Web.Mvc.Razor;
+using System.Web.WebPages.Razor;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class MvcWebRazorHostFactoryTest
+ {
+ [Fact]
+ public void Constructor()
+ {
+ new MvcWebRazorHostFactory();
+
+ // All is cool
+ }
+
+ [Fact]
+ public void CreateHost_ReplacesRegularHostWithMvcSpecificOne()
+ {
+ // Arrange
+ MvcWebRazorHostFactory factory = new MvcWebRazorHostFactory();
+
+ // Act
+ WebPageRazorHost result = factory.CreateHost("foo.cshtml", null);
+
+ // Assert
+ Assert.IsType<MvcWebPageRazorHost>(result);
+ }
+
+ [Fact]
+ public void CreateHost_DoesNotChangeAppStartFileHost()
+ {
+ // Arrange
+ MvcWebRazorHostFactory factory = new MvcWebRazorHostFactory();
+
+ // Act
+ WebPageRazorHost result = factory.CreateHost("_appstart.cshtml", null);
+
+ // Assert
+ Assert.IsNotType<MvcWebPageRazorHost>(result);
+ }
+
+ [Fact]
+ public void CreateHost_DoesNotChangePageStartFileHost()
+ {
+ // Arrange
+ MvcWebRazorHostFactory factory = new MvcWebRazorHostFactory();
+
+ // Act
+ WebPageRazorHost result = factory.CreateHost("_pagestart.cshtml", null);
+
+ // Assert
+ Assert.IsNotType<MvcWebPageRazorHost>(result);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/NameValueCollectionExtensionsTest.cs b/test/System.Web.Mvc.Test/Test/NameValueCollectionExtensionsTest.cs
new file mode 100644
index 00000000..cff63b64
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/NameValueCollectionExtensionsTest.cs
@@ -0,0 +1,76 @@
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Web.Routing;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class NameValueCollectionExtensionsTest
+ {
+ [Fact]
+ public void CopyTo()
+ {
+ // Arrange
+ NameValueCollection collection = GetCollection();
+ IDictionary<string, object> dictionary = GetDictionary();
+
+ // Act
+ collection.CopyTo(dictionary);
+
+ // Assert
+ Assert.Equal(3, dictionary.Count);
+ Assert.Equal("FooDictionary", dictionary["foo"]);
+ Assert.Equal("BarDictionary", dictionary["bar"]);
+ Assert.Equal("BazCollection", dictionary["baz"]);
+ }
+
+ public void CopyToReplaceExisting()
+ {
+ // Arrange
+ NameValueCollection collection = GetCollection();
+ IDictionary<string, object> dictionary = GetDictionary();
+
+ // Act
+ collection.CopyTo(dictionary, true /* replaceExisting */);
+
+ // Assert
+ Assert.Equal(3, dictionary.Count);
+ Assert.Equal("FooCollection", dictionary["foo"]);
+ Assert.Equal("BarDictionary", dictionary["bar"]);
+ Assert.Equal("BazCollection", dictionary["baz"]);
+ }
+
+ [Fact]
+ public void CopyToWithNullCollectionThrows()
+ {
+ Assert.ThrowsArgumentNull(
+ delegate { NameValueCollectionExtensions.CopyTo(null /* collection */, null /* destination */); }, "collection");
+ }
+
+ [Fact]
+ public void CopyToWithNullDestinationThrows()
+ {
+ // Arrange
+ NameValueCollection collection = GetCollection();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { collection.CopyTo(null /* destination */); }, "destination");
+ }
+
+ private static NameValueCollection GetCollection()
+ {
+ return new NameValueCollection
+ {
+ { "Foo", "FooCollection" },
+ { "Baz", "BazCollection" }
+ };
+ }
+
+ private static IDictionary<string, object> GetDictionary()
+ {
+ return new RouteValueDictionary(new { Foo = "FooDictionary", Bar = "BarDictionary" });
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/NameValueCollectionValueProviderTest.cs b/test/System.Web.Mvc.Test/Test/NameValueCollectionValueProviderTest.cs
new file mode 100644
index 00000000..49f5aaf9
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/NameValueCollectionValueProviderTest.cs
@@ -0,0 +1,143 @@
+using System.Collections.Specialized;
+using System.Globalization;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class NameValueCollectionValueProviderTest
+ {
+ private static readonly NameValueCollection _backingStore = new NameValueCollection()
+ {
+ { "foo", "fooValue1" },
+ { "foo", "fooValue2" },
+ { "bar.baz", "someOtherValue" }
+ };
+
+ [Fact]
+ public void Constructor_ThrowsIfCollectionIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new NameValueCollectionValueProvider(null, CultureInfo.InvariantCulture); }, "collection");
+ }
+
+ [Fact]
+ public void ContainsPrefix()
+ {
+ // Arrange
+ NameValueCollectionValueProvider valueProvider = new NameValueCollectionValueProvider(_backingStore, null);
+
+ // Act
+ bool result = valueProvider.ContainsPrefix("bar");
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void ContainsPrefix_DoesNotContainEmptyPrefixIfBackingStoreIsEmpty()
+ {
+ // Arrange
+ NameValueCollectionValueProvider valueProvider = new NameValueCollectionValueProvider(new NameValueCollection(), null);
+
+ // Act
+ bool result = valueProvider.ContainsPrefix("");
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void ContainsPrefix_ThrowsIfPrefixIsNull()
+ {
+ // Arrange
+ NameValueCollectionValueProvider valueProvider = new NameValueCollectionValueProvider(_backingStore, null);
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { valueProvider.ContainsPrefix(null); }, "prefix");
+ }
+
+ [Fact]
+ public void GetValue()
+ {
+ // Arrange
+ CultureInfo culture = CultureInfo.GetCultureInfo("fr-FR");
+ NameValueCollectionValueProvider valueProvider = new NameValueCollectionValueProvider(_backingStore, culture);
+
+ // Act
+ ValueProviderResult vpResult = valueProvider.GetValue("foo");
+
+ // Assert
+ Assert.NotNull(vpResult);
+ Assert.Equal(_backingStore.GetValues("foo"), (string[])vpResult.RawValue);
+ Assert.Equal("fooValue1,fooValue2", vpResult.AttemptedValue);
+ Assert.Equal(culture, vpResult.Culture);
+ }
+
+ [Fact]
+ public void GetValue_NonValidating()
+ {
+ // Arrange
+ NameValueCollection unvalidatedCollection = new NameValueCollection()
+ {
+ { "foo", "fooValue3" },
+ { "foo", "fooValue4" }
+ };
+
+ CultureInfo culture = CultureInfo.GetCultureInfo("fr-FR");
+ NameValueCollectionValueProvider valueProvider = new NameValueCollectionValueProvider(_backingStore, unvalidatedCollection, culture);
+
+ // Act
+ ValueProviderResult vpResult = valueProvider.GetValue("foo", skipValidation: true);
+
+ // Assert
+ Assert.NotNull(vpResult);
+ Assert.Equal(new[] { "fooValue3", "fooValue4" }, (string[])vpResult.RawValue);
+ Assert.Equal("fooValue3,fooValue4", vpResult.AttemptedValue);
+ Assert.Equal(culture, vpResult.Culture);
+ }
+
+ [Fact]
+ public void GetValue_NonValidating_NoUnvalidatedCollectionSpecified_UsesDefaultCollectionValue()
+ {
+ // Arrange
+ CultureInfo culture = CultureInfo.GetCultureInfo("fr-FR");
+ NameValueCollectionValueProvider valueProvider = new NameValueCollectionValueProvider(_backingStore, null, culture);
+
+ // Act
+ ValueProviderResult vpResult = valueProvider.GetValue("foo", skipValidation: true);
+
+ // Assert
+ Assert.NotNull(vpResult);
+ Assert.Equal(_backingStore.GetValues("foo"), (string[])vpResult.RawValue);
+ Assert.Equal("fooValue1,fooValue2", vpResult.AttemptedValue);
+ Assert.Equal(culture, vpResult.Culture);
+ }
+
+ [Fact]
+ public void GetValue_ReturnsNullIfKeyNotFound()
+ {
+ // Arrange
+ NameValueCollectionValueProvider valueProvider = new NameValueCollectionValueProvider(_backingStore, null);
+
+ // Act
+ ValueProviderResult vpResult = valueProvider.GetValue("bar");
+
+ // Assert
+ Assert.Null(vpResult);
+ }
+
+ [Fact]
+ public void GetValue_ThrowsIfKeyIsNull()
+ {
+ // Arrange
+ NameValueCollectionValueProvider valueProvider = new NameValueCollectionValueProvider(_backingStore, null);
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { valueProvider.GetValue(null); }, "key");
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/NoAsyncTimeoutAttributeTest.cs b/test/System.Web.Mvc.Test/Test/NoAsyncTimeoutAttributeTest.cs
new file mode 100644
index 00000000..aaaace5d
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/NoAsyncTimeoutAttributeTest.cs
@@ -0,0 +1,18 @@
+using System.Threading;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class NoAsyncTimeoutAttributeTest
+ {
+ [Fact]
+ public void DurationPropertyIsZero()
+ {
+ // Act
+ AsyncTimeoutAttribute attr = new NoAsyncTimeoutAttribute();
+
+ // Assert
+ Assert.Equal(Timeout.Infinite, attr.Duration);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/NonActionAttributeTest.cs b/test/System.Web.Mvc.Test/Test/NonActionAttributeTest.cs
new file mode 100644
index 00000000..60f55cf2
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/NonActionAttributeTest.cs
@@ -0,0 +1,17 @@
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class NonActionAttributeTest
+ {
+ [Fact]
+ public void InValidActionForRequestReturnsFalse()
+ {
+ // Arrange
+ NonActionAttribute attr = new NonActionAttribute();
+
+ // Act & Assert
+ Assert.False(attr.IsValidForRequest(null, null));
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/OutputCacheAttributeTest.cs b/test/System.Web.Mvc.Test/Test/OutputCacheAttributeTest.cs
new file mode 100644
index 00000000..32c8479c
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/OutputCacheAttributeTest.cs
@@ -0,0 +1,279 @@
+using System.Collections.Generic;
+using System.Web.TestUtil;
+using System.Web.UI;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class OutputCacheAttributeTest
+ {
+ [Fact]
+ public void CacheProfileProperty()
+ {
+ // Arrange
+ OutputCacheAttribute attr = new OutputCacheAttribute();
+
+ // Act & assert
+ MemberHelper.TestStringProperty(attr, "CacheProfile", String.Empty);
+ }
+
+ [Fact]
+ public void CacheSettingsProperty()
+ {
+ // Arrange
+ OutputCacheAttribute attr = new OutputCacheAttribute()
+ {
+ CacheProfile = "SomeProfile",
+ Duration = 50,
+ Location = OutputCacheLocation.Downstream,
+ NoStore = true,
+ SqlDependency = "SomeSqlDependency",
+ VaryByContentEncoding = "SomeContentEncoding",
+ VaryByCustom = "SomeCustom",
+ VaryByHeader = "SomeHeader",
+ VaryByParam = "SomeParam",
+ };
+
+ // Act
+ OutputCacheParameters cacheSettings = attr.CacheSettings;
+
+ // Assert
+ Assert.Equal("SomeProfile", cacheSettings.CacheProfile);
+ Assert.Equal(50, cacheSettings.Duration);
+ Assert.Equal(OutputCacheLocation.Downstream, cacheSettings.Location);
+ Assert.Equal(true, cacheSettings.NoStore);
+ Assert.Equal("SomeSqlDependency", cacheSettings.SqlDependency);
+ Assert.Equal("SomeContentEncoding", cacheSettings.VaryByContentEncoding);
+ Assert.Equal("SomeCustom", cacheSettings.VaryByCustom);
+ Assert.Equal("SomeHeader", cacheSettings.VaryByHeader);
+ Assert.Equal("SomeParam", cacheSettings.VaryByParam);
+ }
+
+ [Fact]
+ public void DurationProperty()
+ {
+ // Arrange
+ OutputCacheAttribute attr = new OutputCacheAttribute();
+
+ // Act & assert
+ MemberHelper.TestInt32Property(attr, "Duration", 10, 20);
+ }
+
+ [Fact]
+ public void LocationProperty()
+ {
+ // Arrange
+ OutputCacheAttribute attr = new OutputCacheAttribute();
+
+ // Act & assert
+ MemberHelper.TestPropertyValue(attr, "Location", OutputCacheLocation.ServerAndClient);
+ }
+
+ [Fact]
+ public void NoStoreProperty()
+ {
+ // Arrange
+ OutputCacheAttribute attr = new OutputCacheAttribute();
+
+ // Act & assert
+ MemberHelper.TestBooleanProperty(attr, "NoStore", false /* initialValue */, false /* testDefaultValue */);
+ }
+
+ [Fact]
+ public void OnResultExecutingThrowsIfFilterContextIsNull()
+ {
+ // Arrange
+ OutputCacheAttribute attr = new OutputCacheAttribute();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { attr.OnResultExecuting(null); }, "filterContext");
+ }
+
+ [Fact]
+ public void SqlDependencyProperty()
+ {
+ // Arrange
+ OutputCacheAttribute attr = new OutputCacheAttribute();
+
+ // Act & assert
+ MemberHelper.TestStringProperty(attr, "SqlDependency", String.Empty);
+ }
+
+ [Fact]
+ public void VaryByContentEncodingProperty()
+ {
+ // Arrange
+ OutputCacheAttribute attr = new OutputCacheAttribute();
+
+ // Act & assert
+ MemberHelper.TestStringProperty(attr, "VaryByContentEncoding", String.Empty);
+ }
+
+ [Fact]
+ public void VaryByCustomProperty()
+ {
+ // Arrange
+ OutputCacheAttribute attr = new OutputCacheAttribute();
+
+ // Act & assert
+ MemberHelper.TestStringProperty(attr, "VaryByCustom", String.Empty);
+ }
+
+ [Fact]
+ public void VaryByHeaderProperty()
+ {
+ // Arrange
+ OutputCacheAttribute attr = new OutputCacheAttribute();
+
+ // Act & assert
+ MemberHelper.TestStringProperty(attr, "VaryByHeader", String.Empty);
+ }
+
+ [Fact]
+ public void VaryByParamProperty()
+ {
+ // Arrange
+ OutputCacheAttribute attr = new OutputCacheAttribute();
+
+ // Act & assert
+ MemberHelper.TestStringProperty(attr, "VaryByParam", "*");
+ }
+
+ [Fact]
+ public void OutputCacheDoesNotExecuteIfInChildAction()
+ {
+ // Arrange
+ OutputCacheAttribute attr = new OutputCacheAttribute();
+ Mock<ResultExecutingContext> context = new Mock<ResultExecutingContext>();
+ context.Setup(c => c.IsChildAction).Returns(true);
+
+ // Act
+ attr.OnResultExecuting(context.Object);
+
+ // Assert
+ context.Verify();
+ context.Verify(c => c.Result, Times.Never());
+ }
+
+ // GetChildActionUniqueId
+
+ [Fact]
+ public void GetChildActionUniqueId_ReturnsRepeatableValueForIdenticalContext()
+ {
+ // Arrange
+ var attr = new OutputCacheAttribute();
+ var context = new MockActionExecutingContext();
+
+ // Act
+ string result1 = attr.GetChildActionUniqueId(context.Object);
+ string result2 = attr.GetChildActionUniqueId(context.Object);
+
+ // Assert
+ Assert.Equal(result1, result2);
+ }
+
+ [Fact]
+ public void GetChildActionUniqueId_VariesByActionDescriptorsUniqueId()
+ {
+ // Arrange
+ var attr = new OutputCacheAttribute();
+ var context1 = new MockActionExecutingContext();
+ context1.Setup(c => c.ActionDescriptor.UniqueId).Returns("1");
+ var context2 = new MockActionExecutingContext();
+ context2.Setup(c => c.ActionDescriptor.UniqueId).Returns("2");
+
+ // Act
+ string result1 = attr.GetChildActionUniqueId(context1.Object);
+ string result2 = attr.GetChildActionUniqueId(context2.Object);
+
+ // Assert
+ Assert.NotEqual(result1, result2);
+ }
+
+ [Fact]
+ public void GetChildActionUniqueId_VariesByCustom()
+ {
+ // Arrange
+ var attr = new OutputCacheAttribute { VaryByCustom = "foo" };
+ var context1 = new MockActionExecutingContext();
+ context1.Setup(c => c.HttpContext.ApplicationInstance.GetVaryByCustomString(It.IsAny<HttpContext>(), "foo")).Returns("1");
+ var context2 = new MockActionExecutingContext();
+ context2.Setup(c => c.HttpContext.ApplicationInstance.GetVaryByCustomString(It.IsAny<HttpContext>(), "foo")).Returns("2");
+
+ // Act
+ string result1 = attr.GetChildActionUniqueId(context1.Object);
+ string result2 = attr.GetChildActionUniqueId(context2.Object);
+
+ // Assert
+ Assert.NotEqual(result1, result2);
+ }
+
+ [Fact]
+ public void GetChildActionUniqueId_VariesByActionParameters_AllParametersByDefault()
+ {
+ // Arrange
+ var attr = new OutputCacheAttribute();
+ var context1 = new MockActionExecutingContext();
+ context1.ActionParameters["foo"] = "1";
+ var context2 = new MockActionExecutingContext();
+ context2.ActionParameters["foo"] = "2";
+
+ // Act
+ string result1 = attr.GetChildActionUniqueId(context1.Object);
+ string result2 = attr.GetChildActionUniqueId(context2.Object);
+
+ // Assert
+ Assert.NotEqual(result1, result2);
+ }
+
+ [Fact]
+ public void GetChildActionUniqueId_DoesNotVaryByActionParametersWhenVaryByParamIsNone()
+ {
+ // Arrange
+ var attr = new OutputCacheAttribute { VaryByParam = "none" };
+ var context1 = new MockActionExecutingContext();
+ context1.ActionParameters["foo"] = "1";
+ var context2 = new MockActionExecutingContext();
+ context2.ActionParameters["foo"] = "2";
+
+ // Act
+ string result1 = attr.GetChildActionUniqueId(context1.Object);
+ string result2 = attr.GetChildActionUniqueId(context2.Object);
+
+ // Assert
+ Assert.Equal(result1, result2);
+ }
+
+ [Fact]
+ public void GetChildActionUniqueId_VariesByActionParameters_OnlyVariesByTheGivenParameters()
+ {
+ // Arrange
+ var attr = new OutputCacheAttribute { VaryByParam = "bar" };
+ var context1 = new MockActionExecutingContext();
+ context1.ActionParameters["foo"] = "1";
+ var context2 = new MockActionExecutingContext();
+ context2.ActionParameters["foo"] = "2";
+
+ // Act
+ string result1 = attr.GetChildActionUniqueId(context1.Object);
+ string result2 = attr.GetChildActionUniqueId(context2.Object);
+
+ // Assert
+ Assert.Equal(result1, result2);
+ }
+
+ class MockActionExecutingContext : Mock<ActionExecutingContext>
+ {
+ public Dictionary<string, object> ActionParameters = new Dictionary<string, object>();
+
+ public MockActionExecutingContext()
+ {
+ Setup(c => c.ActionDescriptor.UniqueId).Returns("abc123");
+ Setup(c => c.ActionParameters).Returns(() => ActionParameters);
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ParameterBindingInfoTest.cs b/test/System.Web.Mvc.Test/Test/ParameterBindingInfoTest.cs
new file mode 100644
index 00000000..0f6b6b61
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ParameterBindingInfoTest.cs
@@ -0,0 +1,60 @@
+using System.Collections.Generic;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class ParameterBindingInfoTest
+ {
+ [Fact]
+ public void BinderProperty()
+ {
+ // Arrange
+ ParameterBindingInfo bindingInfo = new ParameterBindingInfoHelper();
+
+ // Act & assert
+ Assert.Null(bindingInfo.Binder);
+ }
+
+ [Fact]
+ public void ExcludeProperty()
+ {
+ // Arrange
+ ParameterBindingInfo bindingInfo = new ParameterBindingInfoHelper();
+
+ // Act
+ ICollection<string> exclude = bindingInfo.Exclude;
+
+ // Assert
+ Assert.NotNull(exclude);
+ Assert.Empty(exclude);
+ }
+
+ [Fact]
+ public void IncludeProperty()
+ {
+ // Arrange
+ ParameterBindingInfo bindingInfo = new ParameterBindingInfoHelper();
+
+ // Act
+ ICollection<string> include = bindingInfo.Include;
+
+ // Assert
+ Assert.NotNull(include);
+ Assert.Empty(include);
+ }
+
+ [Fact]
+ public void PrefixProperty()
+ {
+ // Arrange
+ ParameterBindingInfo bindingInfo = new ParameterBindingInfoHelper();
+
+ // Act & assert
+ Assert.Null(bindingInfo.Prefix);
+ }
+
+ private class ParameterBindingInfoHelper : ParameterBindingInfo
+ {
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ParameterDescriptorTest.cs b/test/System.Web.Mvc.Test/Test/ParameterDescriptorTest.cs
new file mode 100644
index 00000000..a537b9d7
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ParameterDescriptorTest.cs
@@ -0,0 +1,114 @@
+using System.Reflection;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ParameterDescriptorTest
+ {
+ [Fact]
+ public void BindingInfoProperty()
+ {
+ // Arrange
+ ParameterDescriptor pd = GetParameterDescriptor(typeof(object), "someName");
+
+ // Act
+ ParameterBindingInfo bindingInfo = pd.BindingInfo;
+
+ // Assert
+ Assert.IsType(typeof(ParameterDescriptor).GetNestedType("EmptyParameterBindingInfo", BindingFlags.NonPublic),
+ bindingInfo);
+ }
+
+ [Fact]
+ public void DefaultValuePropertyDefaultsToNull()
+ {
+ // Arrange
+ ParameterDescriptor pd = GetParameterDescriptor();
+
+ // Act
+ object defaultValue = pd.DefaultValue;
+
+ // Assert
+ Assert.Null(defaultValue);
+ }
+
+ [Fact]
+ public void GetCustomAttributesReturnsEmptyArrayOfAttributeType()
+ {
+ // Arrange
+ ParameterDescriptor pd = GetParameterDescriptor();
+
+ // Act
+ ObsoleteAttribute[] attrs = (ObsoleteAttribute[])pd.GetCustomAttributes(typeof(ObsoleteAttribute), true);
+
+ // Assert
+ Assert.Empty(attrs);
+ }
+
+ [Fact]
+ public void GetCustomAttributesThrowsIfAttributeTypeIsNull()
+ {
+ // Arrange
+ ParameterDescriptor pd = GetParameterDescriptor();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { pd.GetCustomAttributes(null /* attributeType */, true); }, "attributeType");
+ }
+
+ [Fact]
+ public void GetCustomAttributesWithoutAttributeTypeCallsGetCustomAttributesWithAttributeType()
+ {
+ // Arrange
+ object[] expected = new object[0];
+ Mock<ParameterDescriptor> mockDescriptor = new Mock<ParameterDescriptor>() { CallBase = true };
+ mockDescriptor.Setup(d => d.GetCustomAttributes(typeof(object), true)).Returns(expected);
+ ParameterDescriptor pd = mockDescriptor.Object;
+
+ // Act
+ object[] returned = pd.GetCustomAttributes(true /* inherit */);
+
+ // Assert
+ Assert.Same(expected, returned);
+ }
+
+ [Fact]
+ public void IsDefinedReturnsFalse()
+ {
+ // Arrange
+ ParameterDescriptor pd = GetParameterDescriptor();
+
+ // Act
+ bool isDefined = pd.IsDefined(typeof(object), true);
+
+ // Assert
+ Assert.False(isDefined);
+ }
+
+ [Fact]
+ public void IsDefinedThrowsIfAttributeTypeIsNull()
+ {
+ // Arrange
+ ParameterDescriptor pd = GetParameterDescriptor();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { pd.IsDefined(null /* attributeType */, true); }, "attributeType");
+ }
+
+ private static ParameterDescriptor GetParameterDescriptor()
+ {
+ return GetParameterDescriptor(typeof(object), "someName");
+ }
+
+ private static ParameterDescriptor GetParameterDescriptor(Type type, string name)
+ {
+ Mock<ParameterDescriptor> mockDescriptor = new Mock<ParameterDescriptor>() { CallBase = true };
+ mockDescriptor.Setup(d => d.ParameterType).Returns(type);
+ mockDescriptor.Setup(d => d.ParameterName).Returns(name);
+ return mockDescriptor.Object;
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ParameterInfoUtilTest.cs b/test/System.Web.Mvc.Test/Test/ParameterInfoUtilTest.cs
new file mode 100644
index 00000000..2a406a6f
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ParameterInfoUtilTest.cs
@@ -0,0 +1,182 @@
+using System.ComponentModel;
+using System.Linq;
+using System.Reflection;
+using Moq;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class ParameterInfoUtilTest
+ {
+ [Fact]
+ public void TryGetDefaultValue_FirstChecksDefaultValue()
+ {
+ // Arrange
+ Mock<ParameterInfo> mockPInfo = new Mock<ParameterInfo>() { DefaultValue = DefaultValue.Mock };
+ mockPInfo.Setup(p => p.DefaultValue).Returns(42);
+ mockPInfo.Setup(p => p.Name).Returns("someParameter");
+
+ // Act
+ object defaultValue;
+ bool retVal = ParameterInfoUtil.TryGetDefaultValue(mockPInfo.Object, out defaultValue);
+
+ // Assert
+ Assert.True(retVal);
+ Assert.Equal(42, defaultValue);
+ }
+
+ [Fact]
+ public void TryGetDefaultValue_SecondChecksDefaultValueAttribute()
+ {
+ // Arrange
+ ParameterInfo pInfo = typeof(MyController).GetMethod("DefaultValues").GetParameters()[1]; // hasDefaultValue
+
+ // Act
+ object defaultValue;
+ bool retVal = ParameterInfoUtil.TryGetDefaultValue(pInfo, out defaultValue);
+
+ // Assert
+ Assert.True(retVal);
+ Assert.Equal("someValue", defaultValue);
+ }
+
+ [Fact]
+ public void TryGetDefaultValue_RespectsNullDefaultValue()
+ {
+ // Arrange
+ Mock<ParameterInfo> mockPInfo = new Mock<ParameterInfo>() { DefaultValue = DefaultValue.Mock };
+ mockPInfo.Setup(p => p.DefaultValue).Returns(null);
+ mockPInfo.Setup(p => p.Name).Returns("someParameter");
+ mockPInfo
+ .Setup(p => p.GetCustomAttributes(typeof(DefaultValueAttribute[]), false))
+ .Returns(new DefaultValueAttribute[] { new DefaultValueAttribute(42) });
+
+ // Act
+ object defaultValue;
+ bool retVal = ParameterInfoUtil.TryGetDefaultValue(mockPInfo.Object, out defaultValue);
+
+ // Assert
+ Assert.True(retVal);
+ Assert.Null(defaultValue);
+ }
+
+ [Fact]
+ public void TryGetDefaultValue_ReturnsFalseIfNoDefaultValue()
+ {
+ // Arrange
+ ParameterInfo pInfo = typeof(MyController).GetMethod("DefaultValues").GetParameters()[0]; // noDefaultValue
+
+ // Act
+ object defaultValue;
+ bool retVal = ParameterInfoUtil.TryGetDefaultValue(pInfo, out defaultValue);
+
+ // Assert
+ Assert.False(retVal);
+ Assert.Equal(default(object), defaultValue);
+ }
+
+ [Fact]
+ public void TryGetDefaultValue_DefaultValueAttributeParameters()
+ {
+ DefaultValueAttributeHelper<bool>(true, "boolParam");
+ DefaultValueAttributeHelper<byte>(42, "byteParam");
+ DefaultValueAttributeHelper<char>('a', "charParam");
+ DefaultValueAttributeHelper<double>(1.0, "doubleParam");
+ DefaultValueAttributeHelper<MyEnum>(MyEnum.All, "enumParam");
+ DefaultValueAttributeHelper<float>((float)1.0, "floatParam");
+ DefaultValueAttributeHelper<int>(42, "intParam");
+ DefaultValueAttributeHelper<long>(42, "longParam");
+ DefaultValueAttributeHelper<object>(null, "objectParam");
+ DefaultValueAttributeHelper<short>(42, "shortParam");
+ DefaultValueAttributeHelper<string>("abc", "stringParam");
+ DefaultValueAttributeHelper<DateTime>(new DateTime(2010, 09, 27), "customParam");
+ }
+
+ [Fact]
+ public void TryGetDefaultValue_OptionalParameters()
+ {
+ OptionalParamHelper<bool>(true, "boolParam");
+ OptionalParamHelper<byte>(42, "byteParam");
+ OptionalParamHelper<char>('a', "charParam");
+ OptionalParamHelper<double>(1.0, "doubleParam");
+ OptionalParamHelper<MyEnum>(MyEnum.All, "enumParam");
+ OptionalParamHelper<float>((float)1.0, "floatParam");
+ OptionalParamHelper<int>(42, "intParam");
+ OptionalParamHelper<long>(42, "longParam");
+ OptionalParamHelper<object>(null, "objectParam");
+ OptionalParamHelper<short>(42, "shortParam");
+ OptionalParamHelper<string>("abc", "stringParam");
+ }
+
+ private static void DefaultValueAttributeHelper<TParam>(TParam expectedValue, string paramName)
+ {
+ ParameterTestHelper<TParam>(expectedValue, paramName, "AttributeDefaultValues");
+ }
+
+ private static void OptionalParamHelper<TParam>(TParam expectedValue, string paramName)
+ {
+ ParameterTestHelper<TParam>(expectedValue, paramName, "OptionalParamDefaultValues");
+ }
+
+ private static void ParameterTestHelper<TParam>(TParam expectedValue, string paramName, string actionMethodName)
+ {
+ ParameterInfo pInfo = typeof(MyController).GetMethod(actionMethodName).GetParameters().Single(p => p.Name == paramName);
+ object returnValueObject;
+ bool result = ParameterInfoUtil.TryGetDefaultValue(pInfo, out returnValueObject);
+
+ Assert.True(result);
+ if (expectedValue != null)
+ {
+ Assert.IsType<TParam>(returnValueObject);
+ }
+ TParam returnValue = (TParam)returnValueObject;
+ Assert.Equal<TParam>(expectedValue, returnValue);
+ }
+
+ private class MyController : Controller
+ {
+ public void DefaultValues(string noDefaultValue, [DefaultValue("someValue")] string hasDefaultValue)
+ {
+ }
+
+ public void AttributeDefaultValues(
+ [DefaultValue(true)] bool boolParam,
+ [DefaultValue((byte)42)] byte byteParam,
+ [DefaultValue('a')] char charParam,
+ [DefaultValue(typeof(DateTime), "2010-09-27")] object customParam,
+ [DefaultValue((double)1.0)] double doubleParam,
+ [DefaultValue(MyEnum.All)] MyEnum enumParam,
+ [DefaultValue((float)1.0)] float floatParam,
+ [DefaultValue(42)] int intParam,
+ [DefaultValue((long)42)] long longParam,
+ [DefaultValue(null)] object objectParam,
+ [DefaultValue((short)42)] short shortParam,
+ [DefaultValue("abc")] string stringParam
+ )
+ {
+ }
+
+ public void OptionalParamDefaultValues(
+ bool boolParam = true,
+ byte byteParam = 42,
+ char charParam = 'a',
+ double doubleParam = 1.0,
+ MyEnum enumParam = MyEnum.All,
+ float floatParam = (float)1.0,
+ int intParam = 42,
+ long longParam = 42,
+ object objectParam = null,
+ short shortParam = 42,
+ string stringParam = "abc"
+ )
+ {
+ }
+ }
+
+ private enum MyEnum
+ {
+ None = 0,
+ All = 1
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/PartialViewResultTest.cs b/test/System.Web.Mvc.Test/Test/PartialViewResultTest.cs
new file mode 100644
index 00000000..561a0bd9
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/PartialViewResultTest.cs
@@ -0,0 +1,197 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Web.Routing;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class PartialViewResultTest
+ {
+ private const string _viewName = "My cool partial view.";
+
+ [Fact]
+ public void EmptyViewNameUsesActionNameAsViewName()
+ {
+ // Arrange
+ ControllerBase controller = new Mock<ControllerBase>().Object;
+ HttpContextBase httpContext = CreateHttpContext();
+ RouteData routeData = new RouteData();
+ routeData.Values["action"] = _viewName;
+ ControllerContext context = new ControllerContext(httpContext, routeData, controller);
+ Mock<IViewEngine> viewEngine = new Mock<IViewEngine>(MockBehavior.Strict);
+ Mock<IView> view = new Mock<IView>(MockBehavior.Strict);
+ List<IViewEngine> viewEngines = new List<IViewEngine>();
+ viewEngines.Add(viewEngine.Object);
+ Mock<ViewEngineCollection> viewEngineCollection = new Mock<ViewEngineCollection>(MockBehavior.Strict, viewEngines);
+ PartialViewResult result = new PartialViewResultHelper { ViewEngineCollection = viewEngineCollection.Object };
+ viewEngine
+ .Setup(e => e.FindPartialView(It.IsAny<ControllerContext>(), _viewName, It.IsAny<bool>()))
+ .Callback<ControllerContext, string, bool>(
+ (controllerContext, viewName, useCache) =>
+ {
+ Assert.Same(httpContext, controllerContext.HttpContext);
+ Assert.Same(routeData, controllerContext.RouteData);
+ })
+ .Returns(new ViewEngineResult(view.Object, viewEngine.Object));
+ viewEngineCollection
+ .Setup(e => e.FindPartialView(It.IsAny<ControllerContext>(), _viewName))
+ .Returns(new ViewEngineResult(view.Object, viewEngine.Object));
+ view
+ .Setup(o => o.Render(It.IsAny<ViewContext>(), httpContext.Response.Output))
+ .Callback<ViewContext, TextWriter>(
+ (viewContext, writer) =>
+ {
+ Assert.Same(view.Object, viewContext.View);
+ Assert.Same(result.ViewData, viewContext.ViewData);
+ Assert.Same(result.TempData, viewContext.TempData);
+ Assert.Same(controller, viewContext.Controller);
+ });
+ viewEngine
+ .Setup(e => e.ReleaseView(context, It.IsAny<IView>()))
+ .Callback<ControllerContext, IView>(
+ (controllerContext, releasedView) => { Assert.Same(releasedView, view.Object); });
+
+ // Act
+ result.ExecuteResult(context);
+
+ // Assert
+ viewEngine.Verify();
+ viewEngineCollection.Verify();
+ view.Verify();
+ }
+
+ [Fact]
+ public void EngineLookupFailureThrows()
+ {
+ // Arrange
+ ControllerBase controller = new Mock<ControllerBase>().Object;
+ HttpContextBase httpContext = CreateHttpContext();
+ RouteData routeData = new RouteData();
+ routeData.Values["action"] = _viewName;
+ ControllerContext context = new ControllerContext(httpContext, routeData, controller);
+ Mock<IViewEngine> viewEngine = new Mock<IViewEngine>(MockBehavior.Strict);
+ List<IViewEngine> viewEngines = new List<IViewEngine>();
+ viewEngines.Add(viewEngine.Object);
+ Mock<ViewEngineCollection> viewEngineCollection = new Mock<ViewEngineCollection>(MockBehavior.Strict, viewEngines);
+ PartialViewResult result = new PartialViewResultHelper { ViewEngineCollection = viewEngineCollection.Object };
+ viewEngineCollection
+ .Setup(e => e.FindPartialView(It.IsAny<ControllerContext>(), _viewName))
+ .Returns(new ViewEngineResult(new[] { "location1", "location2" }));
+ viewEngine
+ .Setup(e => e.FindPartialView(It.IsAny<ControllerContext>(), _viewName, It.IsAny<bool>()))
+ .Callback<ControllerContext, string, bool>(
+ (controllerContext, viewName, useCache) =>
+ {
+ Assert.Same(httpContext, controllerContext.HttpContext);
+ Assert.Same(routeData, controllerContext.RouteData);
+ })
+ .Returns(new ViewEngineResult(new[] { "location1", "location2" }));
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ () => result.ExecuteResult(context),
+ @"The partial view '" + _viewName + @"' was not found or no view engine supports the searched locations. The following locations were searched:
+location1
+location2");
+
+ viewEngine.Verify();
+ viewEngineCollection.Verify();
+ }
+
+ [Fact]
+ public void EngineLookupSuccessRendersView()
+ {
+ // Arrange
+ ControllerBase controller = new Mock<ControllerBase>().Object;
+ HttpContextBase httpContext = CreateHttpContext();
+ RouteData routeData = new RouteData();
+ ControllerContext context = new ControllerContext(httpContext, routeData, controller);
+ Mock<IViewEngine> viewEngine = new Mock<IViewEngine>(MockBehavior.Strict);
+ List<IViewEngine> viewEngines = new List<IViewEngine>();
+ viewEngines.Add(viewEngine.Object);
+ Mock<ViewEngineCollection> viewEngineCollection = new Mock<ViewEngineCollection>(MockBehavior.Strict, viewEngines);
+ Mock<IView> view = new Mock<IView>(MockBehavior.Strict);
+ PartialViewResult result = new PartialViewResultHelper { ViewEngineCollection = viewEngineCollection.Object, ViewName = _viewName };
+ view
+ .Setup(o => o.Render(It.IsAny<ViewContext>(), httpContext.Response.Output))
+ .Callback<ViewContext, TextWriter>(
+ (viewContext, writer) =>
+ {
+ Assert.Same(view.Object, viewContext.View);
+ Assert.Same(result.ViewData, viewContext.ViewData);
+ Assert.Same(result.TempData, viewContext.TempData);
+ Assert.Same(controller, viewContext.Controller);
+ });
+ viewEngineCollection
+ .Setup(e => e.FindPartialView(It.IsAny<ControllerContext>(), _viewName))
+ .Returns(new ViewEngineResult(view.Object, viewEngine.Object));
+ viewEngine
+ .Setup(e => e.FindPartialView(It.IsAny<ControllerContext>(), _viewName, It.IsAny<bool>()))
+ .Callback<ControllerContext, string, bool>(
+ (controllerContext, viewName, useCache) =>
+ {
+ Assert.Same(httpContext, controllerContext.HttpContext);
+ Assert.Same(routeData, controllerContext.RouteData);
+ })
+ .Returns(new ViewEngineResult(view.Object, viewEngine.Object));
+ viewEngine
+ .Setup(e => e.ReleaseView(context, It.IsAny<IView>()))
+ .Callback<ControllerContext, IView>(
+ (controllerContext, releasedView) => { Assert.Same(releasedView, view.Object); });
+
+ // Act
+ result.ExecuteResult(context);
+
+ // Assert
+ viewEngineCollection.Verify();
+ viewEngine.Verify();
+ view.Verify();
+ }
+
+ [Fact]
+ public void ExecuteResultWithExplicitViewObject()
+ {
+ // Arrange
+ ControllerBase controller = new Mock<ControllerBase>().Object;
+ HttpContextBase httpContext = CreateHttpContext();
+ RouteData routeData = new RouteData();
+ routeData.Values["action"] = _viewName;
+ ControllerContext context = new ControllerContext(httpContext, routeData, controller);
+ Mock<IView> view = new Mock<IView>(MockBehavior.Strict);
+ PartialViewResult result = new PartialViewResultHelper { View = view.Object };
+ view
+ .Setup(o => o.Render(It.IsAny<ViewContext>(), httpContext.Response.Output))
+ .Callback<ViewContext, TextWriter>(
+ (viewContext, writer) =>
+ {
+ Assert.Same(view.Object, viewContext.View);
+ Assert.Same(result.ViewData, viewContext.ViewData);
+ Assert.Same(result.TempData, viewContext.TempData);
+ Assert.Same(controller, viewContext.Controller);
+ });
+
+ // Act
+ result.ExecuteResult(context);
+
+ // Assert
+ view.Verify();
+ }
+
+ private static HttpContextBase CreateHttpContext()
+ {
+ Mock<HttpContextBase> mockHttpContext = new Mock<HttpContextBase>();
+ mockHttpContext.Setup(c => c.Response.Output).Returns(TextWriter.Null);
+ return mockHttpContext.Object;
+ }
+
+ private class PartialViewResultHelper : PartialViewResult
+ {
+ public PartialViewResultHelper()
+ {
+ ViewEngineCollection = new ViewEngineCollection(new IViewEngine[] { new WebFormViewEngine() });
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/PathHelpersTest.cs b/test/System.Web.Mvc.Test/Test/PathHelpersTest.cs
new file mode 100644
index 00000000..da773723
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/PathHelpersTest.cs
@@ -0,0 +1,247 @@
+using System.Collections.Specialized;
+using Moq;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class PathHelpersTest
+ {
+ [Fact]
+ public void GenerateClientUrlWithAbsoluteContentPathAndRewritingDisabled()
+ {
+ // Arrange
+ Mock<HttpContextBase> mockHttpContext = GetMockHttpContext(includeRewriterServerVar: false);
+
+ // Act
+ string returnedUrl = PathHelpers.GenerateClientUrl(mockHttpContext.Object, "should remain unchanged");
+
+ // Assert
+ Assert.Equal("should remain unchanged", returnedUrl);
+ }
+
+ [Fact]
+ public void GenerateClientUrlWithAbsoluteContentPathAndRewritingEnabled()
+ {
+ PathHelpers.ResetUrlRewriterHelper(); // Reset the "is URL rewriting enabled?" cache
+
+ // Arrange
+ Mock<HttpContextBase> mockHttpContext = GetMockHttpContext(includeRewriterServerVar: true);
+ mockHttpContext.Setup(c => c.Request.RawUrl).Returns("/quux/foo/bar/baz");
+ mockHttpContext.Setup(c => c.Request.Path).Returns("/myapp/foo/bar/baz");
+
+ // Act
+ string returnedUrl = PathHelpers.GenerateClientUrl(mockHttpContext.Object, "/myapp/some/absolute/path?alpha=bravo");
+
+ // Assert
+ Assert.Equal("/quux/some/absolute/path?alpha=bravo", returnedUrl);
+ }
+
+ [Fact]
+ public void GenerateClientUrlWithAppRelativeContentPathAndRewritingDisabled()
+ {
+ // Arrange
+ Mock<HttpContextBase> mockHttpContext = GetMockHttpContext(includeRewriterServerVar: false);
+
+ // Act
+ string returnedUrl = PathHelpers.GenerateClientUrl(mockHttpContext.Object, "~/foo/bar?alpha=bravo");
+
+ // Assert
+ Assert.Equal("/myapp/(S(session))/foo/bar?alpha=bravo", returnedUrl);
+ }
+
+ [Fact]
+ public void GenerateClientUrlWithAppRelativeContentPathAndRewritingEnabled()
+ {
+ PathHelpers.ResetUrlRewriterHelper(); // Reset the "is URL rewriting enabled?" cache
+
+ // Arrange
+ Mock<HttpContextBase> mockHttpContext = GetMockHttpContext(includeRewriterServerVar: true);
+ mockHttpContext.Setup(c => c.Request.RawUrl).Returns("/quux/foo/baz");
+ mockHttpContext.Setup(c => c.Request.Path).Returns("/myapp/foo/baz");
+
+ // Act
+ string returnedUrl = PathHelpers.GenerateClientUrl(mockHttpContext.Object, "~/foo/bar?alpha=bravo");
+
+ // Assert
+ Assert.Equal("/quux/foo/bar?alpha=bravo", returnedUrl);
+ }
+
+ [Fact]
+ public void GenerateClientUrlWithEmptyContentPathReturnsEmptyString()
+ {
+ // Act
+ string returnedUrl = PathHelpers.GenerateClientUrl(null, "");
+
+ // Assert
+ Assert.Equal("", returnedUrl);
+ }
+
+ [Fact]
+ public void GenerateClientUrlWithNullContentPathReturnsNull()
+ {
+ // Act
+ string returnedUrl = PathHelpers.GenerateClientUrl(null, null);
+
+ // Assert
+ Assert.Null(returnedUrl);
+ }
+
+ [Fact]
+ public void GenerateClientUrlWithOnlyQueryStringForContentPathReturnsOriginalContentPath()
+ {
+ // Act
+ string returnedUrl = PathHelpers.GenerateClientUrl(null, "?foo=bar");
+
+ // Assert
+ Assert.Equal("?foo=bar", returnedUrl);
+ }
+
+ [Fact]
+ public void MakeAbsoluteFromDirectoryToParent()
+ {
+ // Act
+ string returnedUrl = PathHelpers.MakeAbsolute("/Account/Register", "../Account");
+
+ // Assert
+ Assert.Equal("/Account", returnedUrl);
+ }
+
+ [Fact]
+ public void MakeAbsoluteFromDirectoryToSelf()
+ {
+ // Act
+ string returnedUrl = PathHelpers.MakeAbsolute("/foo/", "./");
+
+ // Assert
+ Assert.Equal("/foo/", returnedUrl);
+ }
+
+ [Fact]
+ public void MakeAbsoluteFromFileToFile()
+ {
+ // Act
+ string returnedUrl = PathHelpers.MakeAbsolute("/foo", "bar");
+
+ // Assert
+ Assert.Equal("/bar", returnedUrl);
+ }
+
+ [Fact]
+ public void MakeAbsoluteFromFileWithQueryToFile()
+ {
+ // Act
+ string returnedUrl = PathHelpers.MakeAbsolute("/foo/bar?alpha=bravo", "baz");
+
+ // Assert
+ Assert.Equal("/foo/baz", returnedUrl);
+ }
+
+ [Fact]
+ public void MakeAbsoluteFromRootToSelf()
+ {
+ // Act
+ string returnedUrl = PathHelpers.MakeAbsolute("/", "./");
+
+ // Assert
+ Assert.Equal("/", returnedUrl);
+ }
+
+ [Fact]
+ public void MakeRelativeFromFileToDirectory()
+ {
+ // Act
+ string returnedUrl = PathHelpers.MakeRelative("/foo/bar", "/foo/");
+
+ // Assert
+ Assert.Equal("./", returnedUrl);
+ }
+
+ [Fact]
+ public void MakeRelativeFromFileToDirectoryWithQueryString()
+ {
+ // Act
+ string returnedUrl = PathHelpers.MakeRelative("/foo/bar", "/foo/?alpha=bravo");
+
+ // Assert
+ Assert.Equal("./?alpha=bravo", returnedUrl);
+ }
+
+ [Fact]
+ public void MakeRelativeFromFileToFile()
+ {
+ // Act
+ string returnedUrl = PathHelpers.MakeRelative("/foo/bar", "/baz/quux");
+
+ // Assert
+ Assert.Equal("../baz/quux", returnedUrl);
+ }
+
+ [Fact]
+ public void MakeRelativeFromFileToFileWithQuery()
+ {
+ // Act
+ string returnedUrl = PathHelpers.MakeRelative("/foo/bar", "/baz/quux?alpha=bravo");
+
+ // Assert
+ Assert.Equal("../baz/quux?alpha=bravo", returnedUrl);
+ }
+
+ [Fact]
+ public void MakeRelativeFromFileWithQueryToFileWithQuery()
+ {
+ // Act
+ string returnedUrl = PathHelpers.MakeRelative("/foo/bar?charlie=delta", "/baz/quux?alpha=bravo");
+
+ // Assert
+ Assert.Equal("../baz/quux?alpha=bravo", returnedUrl);
+ }
+
+ [Fact]
+ public void MakeRelativeFromRootToRoot()
+ {
+ // Act
+ string returnedUrl = PathHelpers.MakeRelative("/", "/");
+
+ // Assert
+ Assert.Equal("./", returnedUrl);
+ }
+
+ [Fact]
+ public void MakeRelativeFromRootToRootWithQueryString()
+ {
+ // Act
+ string returnedUrl = PathHelpers.MakeRelative("/", "/?foo=bar");
+
+ // Assert
+ Assert.Equal("./?foo=bar", returnedUrl);
+ }
+
+ internal static Mock<HttpContextBase> GetMockHttpContext(bool includeRewriterServerVar)
+ {
+ Mock<HttpContextBase> mockContext = new Mock<HttpContextBase>();
+
+ NameValueCollection serverVars = new NameValueCollection();
+ serverVars["IIS_UrlRewriteModule"] = "I'm on!";
+ mockContext.Setup(c => c.Request.ServerVariables).Returns(serverVars);
+ mockContext.Setup(c => c.Request.ApplicationPath).Returns("/myapp");
+
+ if (includeRewriterServerVar)
+ {
+ serverVars["IIS_WasUrlRewritten"] = "Got rewritten!";
+ mockContext
+ .Setup(c => c.Response.ApplyAppPathModifier(It.IsAny<string>()))
+ .Returns(
+ delegate(string input) { return input; });
+ }
+ else
+ {
+ mockContext
+ .Setup(c => c.Response.ApplyAppPathModifier(It.IsAny<string>()))
+ .Returns(
+ delegate(string input) { return "/myapp/(S(session))" + input.Substring("/myapp".Length); });
+ }
+
+ return mockContext;
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/PreApplicationStartCodeTest.cs b/test/System.Web.Mvc.Test/Test/PreApplicationStartCodeTest.cs
new file mode 100644
index 00000000..d71f42e7
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/PreApplicationStartCodeTest.cs
@@ -0,0 +1,26 @@
+using System.Linq;
+using System.Reflection;
+using System.Web.WebPages.TestUtils;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class PreApplicationStartCodeTest
+ {
+ [Fact]
+ public void PreApplicationStartCodeIsNotBrowsableTest()
+ {
+ PreAppStartTestHelper.TestPreAppStartClass(typeof(PreApplicationStartCode));
+ }
+
+ [Fact]
+ public void PreApplicationStartMethodAttributeTest()
+ {
+ Assembly assembly = typeof(Controller).Assembly;
+ object[] attributes = assembly.GetCustomAttributes(typeof(PreApplicationStartMethodAttribute), true);
+ var preAppStartMethodAttribute = Assert.Single(attributes.Cast<PreApplicationStartMethodAttribute>());
+ Type preAppStartMethodType = preAppStartMethodAttribute.Type;
+ Assert.Equal(typeof(PreApplicationStartCode), preAppStartMethodType);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/QueryStringValueProviderFactoryTest.cs b/test/System.Web.Mvc.Test/Test/QueryStringValueProviderFactoryTest.cs
new file mode 100644
index 00000000..34072f1e
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/QueryStringValueProviderFactoryTest.cs
@@ -0,0 +1,77 @@
+using System.Collections.Specialized;
+using System.Globalization;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class QueryStringValueProviderFactoryTest
+ {
+ private static readonly NameValueCollection _backingStore = new NameValueCollection()
+ {
+ { "foo", "fooValue" }
+ };
+
+ private static readonly NameValueCollection _unvalidatedBackingStore = new NameValueCollection()
+ {
+ { "foo", "fooUnvalidated" }
+ };
+
+ [Fact]
+ public void GetValueProvider()
+ {
+ // Arrange
+ Mock<MockableUnvalidatedRequestValues> mockUnvalidatedValues = new Mock<MockableUnvalidatedRequestValues>();
+ QueryStringValueProviderFactory factory = new QueryStringValueProviderFactory(_ => mockUnvalidatedValues.Object);
+
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.Setup(o => o.HttpContext.Request.QueryString).Returns(_backingStore);
+
+ // Act
+ IValueProvider valueProvider = factory.GetValueProvider(mockControllerContext.Object);
+
+ // Assert
+ Assert.Equal(typeof(QueryStringValueProvider), valueProvider.GetType());
+ ValueProviderResult vpResult = valueProvider.GetValue("foo");
+
+ Assert.NotNull(vpResult);
+ Assert.Equal("fooValue", vpResult.AttemptedValue);
+ Assert.Equal(CultureInfo.InvariantCulture, vpResult.Culture);
+ }
+
+ [Fact]
+ public void GetValueProvider_GetValue_SkipValidation()
+ {
+ // Arrange
+ Mock<MockableUnvalidatedRequestValues> mockUnvalidatedValues = new Mock<MockableUnvalidatedRequestValues>();
+ mockUnvalidatedValues.Setup(o => o.QueryString).Returns(_unvalidatedBackingStore);
+ QueryStringValueProviderFactory factory = new QueryStringValueProviderFactory(_ => mockUnvalidatedValues.Object);
+
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.Setup(o => o.HttpContext.Request.QueryString).Returns(_backingStore);
+
+ // Act
+ IUnvalidatedValueProvider valueProvider = (IUnvalidatedValueProvider)factory.GetValueProvider(mockControllerContext.Object);
+
+ // Assert
+ Assert.Equal(typeof(QueryStringValueProvider), valueProvider.GetType());
+ ValueProviderResult vpResult = valueProvider.GetValue("foo", skipValidation: true);
+
+ Assert.NotNull(vpResult);
+ Assert.Equal("fooUnvalidated", vpResult.AttemptedValue);
+ Assert.Equal(CultureInfo.InvariantCulture, vpResult.Culture);
+ }
+
+ [Fact]
+ public void GetValueProvider_ThrowsIfControllerContextIsNull()
+ {
+ // Arrange
+ QueryStringValueProviderFactory factory = new QueryStringValueProviderFactory();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { factory.GetValueProvider(null); }, "controllerContext");
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/RangeAttributeAdapterTest.cs b/test/System.Web.Mvc.Test/Test/RangeAttributeAdapterTest.cs
new file mode 100644
index 00000000..bdcbfa3b
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/RangeAttributeAdapterTest.cs
@@ -0,0 +1,32 @@
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class RangeAttributeAdapterTest
+ {
+ [Fact]
+ public void ClientRulesWithRangeAttribute()
+ {
+ // Arrange
+ var metadata = ModelMetadataProviders.Current.GetMetadataForProperty(() => null, typeof(string), "Length");
+ var context = new ControllerContext();
+ var attribute = new RangeAttribute(typeof(decimal), "0", "100");
+ var adapter = new RangeAttributeAdapter(metadata, context, attribute);
+
+ // Act
+ var rules = adapter.GetClientValidationRules()
+ .OrderBy(r => r.ValidationType)
+ .ToArray();
+
+ // Assert
+ ModelClientValidationRule rule = Assert.Single(rules);
+ Assert.Equal("range", rule.ValidationType);
+ Assert.Equal(2, rule.ValidationParameters.Count);
+ Assert.Equal(0m, rule.ValidationParameters["min"]);
+ Assert.Equal(100m, rule.ValidationParameters["max"]);
+ Assert.Equal(@"The field Length must be between 0 and 100.", rule.ErrorMessage);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/RazorViewEngineTest.cs b/test/System.Web.Mvc.Test/Test/RazorViewEngineTest.cs
new file mode 100644
index 00000000..1ec5c498
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/RazorViewEngineTest.cs
@@ -0,0 +1,253 @@
+using System.Linq;
+using Moq;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class RazorViewEngineTest
+ {
+ [Fact]
+ public void AreaMasterLocationFormats()
+ {
+ // Arrange
+ string[] expected = new[]
+ {
+ "~/Areas/{2}/Views/{1}/{0}.cshtml",
+ "~/Areas/{2}/Views/{1}/{0}.vbhtml",
+ "~/Areas/{2}/Views/Shared/{0}.cshtml",
+ "~/Areas/{2}/Views/Shared/{0}.vbhtml"
+ };
+
+ // Act
+ RazorViewEngine viewEngine = new RazorViewEngine();
+
+ // Assert
+ Assert.Equal(expected, viewEngine.AreaMasterLocationFormats);
+ }
+
+ [Fact]
+ public void AreaPartialViewLocationFormats()
+ {
+ // Arrange
+ string[] expected = new[]
+ {
+ "~/Areas/{2}/Views/{1}/{0}.cshtml",
+ "~/Areas/{2}/Views/{1}/{0}.vbhtml",
+ "~/Areas/{2}/Views/Shared/{0}.cshtml",
+ "~/Areas/{2}/Views/Shared/{0}.vbhtml"
+ };
+
+ // Act
+ RazorViewEngine viewEngine = new RazorViewEngine();
+
+ // Assert
+ Assert.Equal(expected, viewEngine.AreaPartialViewLocationFormats);
+ }
+
+ [Fact]
+ public void AreaViewLocationFormats()
+ {
+ // Arrange
+ string[] expected = new[]
+ {
+ "~/Areas/{2}/Views/{1}/{0}.cshtml",
+ "~/Areas/{2}/Views/{1}/{0}.vbhtml",
+ "~/Areas/{2}/Views/Shared/{0}.cshtml",
+ "~/Areas/{2}/Views/Shared/{0}.vbhtml"
+ };
+
+ // Act
+ RazorViewEngine viewEngine = new RazorViewEngine();
+
+ // Assert
+ Assert.Equal(expected, viewEngine.AreaViewLocationFormats);
+ }
+
+ [Fact]
+ public void RazorViewEngineSetsViewPageActivator()
+ {
+ // Arrange
+ Mock<IViewPageActivator> viewPageActivator = new Mock<IViewPageActivator>();
+ TestableRazorViewEngine viewEngine = new TestableRazorViewEngine(viewPageActivator.Object);
+
+ //Act & Assert
+ Assert.Equal(viewPageActivator.Object, viewEngine.ViewPageActivator);
+ }
+
+ [Fact]
+ public void CreatePartialView_PassesViewPageActivator()
+ {
+ // Arrange
+ Mock<IViewPageActivator> viewPageActivator = new Mock<IViewPageActivator>();
+ TestableRazorViewEngine viewEngine = new TestableRazorViewEngine(viewPageActivator.Object);
+
+ // Act
+ RazorView result = (RazorView)viewEngine.CreatePartialView("partial path");
+
+ // Assert
+ Assert.Equal(viewEngine.ViewPageActivator, result.ViewPageActivator);
+ }
+
+ [Fact]
+ public void CreateView_PassesViewPageActivator()
+ {
+ // Arrange
+ Mock<IViewPageActivator> viewPageActivator = new Mock<IViewPageActivator>();
+ TestableRazorViewEngine viewEngine = new TestableRazorViewEngine(viewPageActivator.Object);
+
+ // Act
+ RazorView result = (RazorView)viewEngine.CreateView("partial path", "master path");
+
+ // Assert
+ Assert.Equal(viewEngine.ViewPageActivator, result.ViewPageActivator);
+ }
+
+ [Fact]
+ public void CreatePartialView_ReturnsRazorView()
+ {
+ // Arrange
+ TestableRazorViewEngine viewEngine = new TestableRazorViewEngine();
+
+ // Act
+ RazorView result = (RazorView)viewEngine.CreatePartialView("partial path");
+
+ // Assert
+ Assert.Equal("partial path", result.ViewPath);
+ Assert.Equal(String.Empty, result.LayoutPath);
+ Assert.False(result.RunViewStartPages);
+ }
+
+ [Fact]
+ public void CreateView_ReturnsRazorView()
+ {
+ // Arrange
+ TestableRazorViewEngine viewEngine = new TestableRazorViewEngine()
+ {
+ FileExtensions = new[] { "cshtml", "vbhtml", "razor" }
+ };
+
+ // Act
+ RazorView result = (RazorView)viewEngine.CreateView("partial path", "master path");
+
+ // Assert
+ Assert.Equal("partial path", result.ViewPath);
+ Assert.Equal("master path", result.LayoutPath);
+ Assert.Equal(new[] { "cshtml", "vbhtml", "razor" }, result.ViewStartFileExtensions.ToArray());
+ Assert.True(result.RunViewStartPages);
+ }
+
+ [Fact]
+ public void FileExtensionsProperty()
+ {
+ // Arrange
+ string[] expected = new[]
+ {
+ "cshtml",
+ "vbhtml",
+ };
+
+ // Act
+ RazorViewEngine viewEngine = new RazorViewEngine();
+
+ // Assert
+ Assert.Equal(expected, viewEngine.FileExtensions);
+ }
+
+ [Fact]
+ public void MasterLocationFormats()
+ {
+ // Arrange
+ string[] expected = new[]
+ {
+ "~/Views/{1}/{0}.cshtml",
+ "~/Views/{1}/{0}.vbhtml",
+ "~/Views/Shared/{0}.cshtml",
+ "~/Views/Shared/{0}.vbhtml"
+ };
+
+ // Act
+ RazorViewEngine viewEngine = new RazorViewEngine();
+
+ // Assert
+ Assert.Equal(expected, viewEngine.MasterLocationFormats);
+ }
+
+ [Fact]
+ public void PartialViewLocationFormats()
+ {
+ // Arrange
+ string[] expected = new[]
+ {
+ "~/Views/{1}/{0}.cshtml",
+ "~/Views/{1}/{0}.vbhtml",
+ "~/Views/Shared/{0}.cshtml",
+ "~/Views/Shared/{0}.vbhtml"
+ };
+
+ // Act
+ RazorViewEngine viewEngine = new RazorViewEngine();
+
+ // Assert
+ Assert.Equal(expected, viewEngine.PartialViewLocationFormats);
+ }
+
+ [Fact]
+ public void ViewLocationFormats()
+ {
+ // Arrange
+ string[] expected = new[]
+ {
+ "~/Views/{1}/{0}.cshtml",
+ "~/Views/{1}/{0}.vbhtml",
+ "~/Views/Shared/{0}.cshtml",
+ "~/Views/Shared/{0}.vbhtml"
+ };
+
+ // Act
+ RazorViewEngine viewEngine = new RazorViewEngine();
+
+ // Assert
+ Assert.Equal(expected, viewEngine.ViewLocationFormats);
+ }
+
+ [Fact]
+ public void ViewStartFileName()
+ {
+ Assert.Equal("_ViewStart", RazorViewEngine.ViewStartFileName);
+ }
+
+ private sealed class TestableRazorViewEngine : RazorViewEngine
+ {
+ public TestableRazorViewEngine()
+ : base()
+ {
+ }
+
+ public TestableRazorViewEngine(IViewPageActivator viewPageActivator)
+ : base(viewPageActivator)
+ {
+ }
+
+ public new IViewPageActivator ViewPageActivator
+ {
+ get { return base.ViewPageActivator; }
+ }
+
+ public IView CreatePartialView(string partialPath)
+ {
+ return base.CreatePartialView(new ControllerContext(), partialPath);
+ }
+
+ public IView CreateView(string viewPath, string masterPath)
+ {
+ return base.CreateView(new ControllerContext(), viewPath, masterPath);
+ }
+
+ // This method should remain overridable in derived view engines
+ protected override bool FileExists(ControllerContext controllerContext, string virtualPath)
+ {
+ return base.FileExists(controllerContext, virtualPath);
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/RazorViewTest.cs b/test/System.Web.Mvc.Test/Test/RazorViewTest.cs
new file mode 100644
index 00000000..fa644a9c
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/RazorViewTest.cs
@@ -0,0 +1,248 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Web.WebPages;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class RazorViewTest
+ {
+ [Fact]
+ public void Constructor_RunViewStartPagesParam()
+ {
+ var context = new ControllerContext();
+ Assert.True(new RazorView(context, "~/view", "~/master", runViewStartPages: true, viewStartFileExtensions: null).RunViewStartPages);
+ Assert.False(new RazorView(context, "~/view", "~/master", runViewStartPages: false, viewStartFileExtensions: null).RunViewStartPages);
+ Assert.True(new RazorView(context, "~/view", "~/master", runViewStartPages: true, viewStartFileExtensions: null, viewPageActivator: new Mock<IViewPageActivator>().Object).RunViewStartPages);
+ Assert.False(new RazorView(context, "~/view", "~/master", runViewStartPages: false, viewStartFileExtensions: null, viewPageActivator: new Mock<IViewPageActivator>().Object).RunViewStartPages);
+ }
+
+ [Fact]
+ public void ConstructorWithEmptyViewPathThrows()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ () => new RazorView(new ControllerContext(), String.Empty, "~/master", false, Enumerable.Empty<string>()),
+ "viewPath"
+ );
+ }
+
+ [Fact]
+ public void ConstructorWithNullViewPathThrows()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ () => new RazorView(new ControllerContext(), null, "~/master", false, Enumerable.Empty<string>()),
+ "viewPath"
+ );
+ }
+
+ [Fact]
+ public void ConstructorWithNullControllerContextThrows()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => new RazorView(null, "view path", "~/master", false, Enumerable.Empty<string>()),
+ "controllerContext"
+ );
+ }
+
+ [Fact]
+ public void LayoutPathProperty()
+ {
+ //Arrange
+ ControllerContext controllerContext = new ControllerContext();
+
+ // Act
+ RazorView view = new RazorView(new ControllerContext(), "view path", "master path", false, Enumerable.Empty<string>());
+
+ // Assert
+ Assert.Equal("master path", view.LayoutPath);
+ }
+
+ [Fact]
+ public void LayoutPathPropertyReturnsEmptyStringIfNullLayoutSpecified()
+ {
+ // Act
+ RazorView view = new RazorView(new ControllerContext(), "view path", null, false, Enumerable.Empty<string>());
+
+ // Assert
+ Assert.Equal(String.Empty, view.LayoutPath);
+ }
+
+ [Fact]
+ public void LayoutPathPropertyReturnsEmptyStringIfLayoutNotSpecified()
+ {
+ // Act
+ RazorView view = new RazorView(new ControllerContext(), "view path", null, false, Enumerable.Empty<string>());
+
+ // Assert
+ Assert.Equal(String.Empty, view.LayoutPath);
+ }
+
+ [Fact]
+ public void RenderWithNullWriterThrows()
+ {
+ // Arrange
+ RazorView view = new RazorView(new ControllerContext(), "~/viewPath", null, false, Enumerable.Empty<string>());
+ Mock<ViewContext> viewContextMock = new Mock<ViewContext>();
+
+ MockBuildManager buildManager = new MockBuildManager("~/viewPath", typeof(object));
+ view.BuildManager = buildManager;
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => view.Render(viewContextMock.Object, null),
+ "writer"
+ );
+ }
+
+ [Fact]
+ public void RenderWithUnsupportedTypeThrows()
+ {
+ // Arrange
+ ViewContext context = new Mock<ViewContext>().Object;
+ MockBuildManager buildManagerMock = new MockBuildManager("view path", typeof(object));
+ RazorView view = new RazorView(new ControllerContext(), "view path", null, false, Enumerable.Empty<string>());
+ view.BuildManager = buildManagerMock;
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ () => view.Render(context, new Mock<TextWriter>().Object),
+ "The view at 'view path' must derive from WebViewPage, or WebViewPage<TModel>."
+ );
+ }
+
+ [Fact]
+ public void RenderWithViewPageAndNoStartPageLookupRendersView()
+ {
+ // Arrange
+ StubWebViewPage viewPage = new StubWebViewPage();
+ Mock<ViewContext> viewContextMock = new Mock<ViewContext>();
+ viewContextMock.Setup(vc => vc.HttpContext.Items).Returns(new Dictionary<object, object>());
+ viewContextMock.Setup(vc => vc.HttpContext.Request.IsLocal).Returns(false);
+ MockBuildManager buildManager = new MockBuildManager("~/viewPath", typeof(object));
+ Mock<IViewPageActivator> activator = new Mock<IViewPageActivator>(MockBehavior.Strict);
+ ControllerContext controllerContext = new ControllerContext();
+ activator.Setup(l => l.Create(controllerContext, typeof(object))).Returns(viewPage);
+ RazorView view = new RazorView(controllerContext, "~/viewPath", null, false, Enumerable.Empty<string>(), activator.Object);
+ view.StartPageLookup = (WebPageRenderingBase p, string n, IEnumerable<string> e) =>
+ {
+ Assert.True(false, "ViewStart page lookup should not be called");
+ return null;
+ };
+ view.BuildManager = buildManager;
+
+ // Act
+ view.Render(viewContextMock.Object, new Mock<TextWriter>().Object);
+
+ // Assert
+ Assert.Null(viewPage.Layout);
+ Assert.Equal("", viewPage.OverridenLayoutPath);
+ Assert.Same(viewContextMock.Object, viewPage.ViewContext);
+ Assert.Equal("~/viewPath", viewPage.VirtualPath);
+ }
+
+ [Fact]
+ public void RenderWithViewPageAndStartPageLookupExecutesStartPage()
+ {
+ // Arrange
+ StubWebViewPage viewPage = new StubWebViewPage();
+ Mock<ViewContext> viewContextMock = new Mock<ViewContext>();
+ viewContextMock.Setup(vc => vc.HttpContext.Items).Returns(new Dictionary<object, object>());
+ MockBuildManager buildManager = new MockBuildManager("~/viewPath", typeof(object));
+ Mock<IViewPageActivator> activator = new Mock<IViewPageActivator>(MockBehavior.Strict);
+ ControllerContext controllerContext = new ControllerContext();
+ activator.Setup(l => l.Create(controllerContext, typeof(object))).Returns(viewPage);
+ RazorView view = new RazorView(controllerContext, "~/viewPath", null, true, new[] { "cshtml" }, activator.Object);
+ Mock<ViewStartPage> startPage = new Mock<ViewStartPage>();
+ startPage.Setup(sp => sp.ExecutePageHierarchy()).Verifiable();
+ view.StartPageLookup = (WebPageRenderingBase page, string fileName, IEnumerable<string> extensions) =>
+ {
+ Assert.Equal(viewPage, page);
+ Assert.Equal("_ViewStart", fileName);
+ Assert.Equal(new[] { "cshtml" }, extensions.ToArray());
+ return startPage.Object;
+ };
+ view.BuildManager = buildManager;
+
+ // Act
+ view.Render(viewContextMock.Object, new Mock<TextWriter>().Object);
+
+ // Assert
+ startPage.Verify(sp => sp.ExecutePageHierarchy(), Times.Once());
+ }
+
+ // TODO: This throws in WebPages and needs to be tracked down.
+ [Fact]
+ public void RenderWithViewPageAndLayoutPageRendersView()
+ {
+ // Arrange
+ StubWebViewPage viewPage = new StubWebViewPage();
+ Mock<ViewContext> viewContext = new Mock<ViewContext>();
+ Mock<HttpContextBase> httpContext = new Mock<HttpContextBase>();
+ Mock<HttpRequestBase> httpRequest = new Mock<HttpRequestBase>();
+
+ httpRequest.SetupGet(r => r.IsLocal).Returns(false);
+ httpRequest.SetupGet(r => r.Browser.IsMobileDevice).Returns(false);
+ httpRequest.SetupGet(r => r.Cookies).Returns(new HttpCookieCollection());
+
+ httpContext.SetupGet(c => c.Request).Returns(httpRequest.Object);
+ httpContext.SetupGet(c => c.Response.Cookies).Returns(new HttpCookieCollection());
+ httpContext.SetupGet(c => c.Items).Returns(new Hashtable());
+
+ viewContext.SetupGet(v => v.HttpContext).Returns(httpContext.Object);
+
+ MockBuildManager buildManager = new MockBuildManager("~/viewPath", typeof(object));
+ Mock<IViewPageActivator> activator = new Mock<IViewPageActivator>(MockBehavior.Strict);
+
+ Mock<WebPage> layoutPage = new Mock<WebPage> { CallBase = true };
+ layoutPage.Setup(c => c.Execute()).Callback(() => layoutPage.Object.RenderBody());
+ Mock<IVirtualPathFactory> virtualPathFactory = new Mock<IVirtualPathFactory>(MockBehavior.Strict);
+ virtualPathFactory.Setup(f => f.Exists("~/layoutPath")).Returns(true);
+ virtualPathFactory.Setup(f => f.CreateInstance("~/layoutPath")).Returns(layoutPage.Object);
+ ControllerContext controllerContext = new ControllerContext();
+ activator.Setup(l => l.Create(controllerContext, typeof(object))).Returns(viewPage);
+ RazorView view = new RazorView(controllerContext, "~/viewPath", "~/layoutPath", false, Enumerable.Empty<string>(), activator.Object);
+ view.BuildManager = buildManager;
+ view.VirtualPathFactory = virtualPathFactory.Object;
+ view.DisplayModeProvider = DisplayModeProvider.Instance;
+
+ // Act
+ view.Render(viewContext.Object, TextWriter.Null);
+
+ // Assert
+ Assert.Equal("~/layoutPath", viewPage.Layout);
+ Assert.Equal("~/layoutPath", viewPage.OverridenLayoutPath);
+ Assert.Same(viewContext.Object, viewPage.ViewContext);
+ Assert.Equal("~/viewPath", viewPage.VirtualPath);
+ }
+
+ public class StubWebViewPage : WebViewPage
+ {
+ public bool InitHelpersCalled;
+ public string ResultLayoutPage;
+ public string ResultOverridenLayoutPath;
+ public ViewContext ResultViewContext;
+ public string ResultVirtualPath;
+
+ public override void Execute()
+ {
+ ResultLayoutPage = Layout;
+ ResultOverridenLayoutPath = OverridenLayoutPath;
+ ResultViewContext = ViewContext;
+ ResultVirtualPath = VirtualPath;
+ }
+
+ public override void InitHelpers()
+ {
+ base.InitHelpers();
+ InitHelpersCalled = true;
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ReaderWriterCacheTest.cs b/test/System.Web.Mvc.Test/Test/ReaderWriterCacheTest.cs
new file mode 100644
index 00000000..58625fdc
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ReaderWriterCacheTest.cs
@@ -0,0 +1,76 @@
+using System.Collections.Generic;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class ReaderWriterCacheTest
+ {
+ [Fact]
+ public void PublicFetchOrCreateItemCreatesItemIfNotAlreadyInCache()
+ {
+ // Arrange
+ ReaderWriterCacheHelper<int, string> helper = new ReaderWriterCacheHelper<int, string>();
+ Dictionary<int, string> cache = helper.PublicCache;
+
+ // Act
+ string item = helper.PublicFetchOrCreateItem(42, () => "new");
+
+ // Assert
+ Assert.Equal("new", cache[42]);
+ Assert.Equal("new", item);
+ }
+
+ [Fact]
+ public void PublicFetchOrCreateItemReturnsExistingItemIfFound()
+ {
+ // Arrange
+ ReaderWriterCacheHelper<int, string> helper = new ReaderWriterCacheHelper<int, string>();
+ Dictionary<int, string> cache = helper.PublicCache;
+ helper.PublicCache[42] = "original";
+
+ // Act
+ string item = helper.PublicFetchOrCreateItem(42, () => "new");
+
+ // Assert
+ Assert.Equal("original", cache[42]);
+ Assert.Equal("original", item);
+ }
+
+ [Fact]
+ public void PublicFetchOrCreateItemReturnsFirstItemIfTwoThreadsUpdateCacheSimultaneously()
+ {
+ // Arrange
+ ReaderWriterCacheHelper<int, string> helper = new ReaderWriterCacheHelper<int, string>();
+ Dictionary<int, string> cache = helper.PublicCache;
+ Func<string> creator = delegate()
+ {
+ // fake a second thread coming along when we weren't looking
+ string firstItem = helper.PublicFetchOrCreateItem(42, () => "original");
+
+ Assert.Equal("original", cache[42]);
+ Assert.Equal("original", firstItem);
+ return "new";
+ };
+
+ // Act
+ string secondItem = helper.PublicFetchOrCreateItem(42, creator);
+
+ // Assert
+ Assert.Equal("original", cache[42]);
+ Assert.Equal("original", secondItem);
+ }
+
+ private class ReaderWriterCacheHelper<TKey, TValue> : ReaderWriterCache<TKey, TValue>
+ {
+ public Dictionary<TKey, TValue> PublicCache
+ {
+ get { return Cache; }
+ }
+
+ public TValue PublicFetchOrCreateItem(TKey key, Func<TValue> creator)
+ {
+ return FetchOrCreateItem(key, creator);
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/RedirectResultTest.cs b/test/System.Web.Mvc.Test/Test/RedirectResultTest.cs
new file mode 100644
index 00000000..2882bc38
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/RedirectResultTest.cs
@@ -0,0 +1,125 @@
+using System.Web.Mvc.Properties;
+using System.Web.Routing;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class RedirectResultTest
+ {
+ private static string _baseUrl = "http://www.contoso.com/";
+
+ [Fact]
+ public void ConstructorSetsUrl()
+ {
+ // Act
+ var result = new RedirectResult(_baseUrl);
+
+ // Assert
+ Assert.Same(_baseUrl, result.Url);
+ Assert.False(result.Permanent);
+ }
+
+ [Fact]
+ public void ConstructorSetsUrlAndPermanent()
+ {
+ // Act
+ var result = new RedirectResult(_baseUrl, permanent: true);
+
+ // Assert
+ Assert.Same(_baseUrl, result.Url);
+ Assert.True(result.Permanent);
+ }
+
+ [Fact]
+ public void ConstructorWithEmptyUrlThrows()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { new RedirectResult(String.Empty); },
+ "url");
+
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { new RedirectResult(String.Empty, true); },
+ "url");
+ }
+
+ [Fact]
+ public void ConstructorWithNullUrlThrows()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { new RedirectResult(url: null); },
+ "url");
+
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { new RedirectResult(url: null, permanent: true); },
+ "url");
+ }
+
+ [Fact]
+ public void ExecuteResultCallsResponseRedirect()
+ {
+ // Arrange
+ Mock<HttpResponseBase> mockResponse = new Mock<HttpResponseBase>();
+ mockResponse.Setup(o => o.Redirect(_baseUrl, false /* endResponse */)).Verifiable();
+ Mock<HttpContextBase> mockContext = new Mock<HttpContextBase>();
+ mockContext.Setup(o => o.Response).Returns(mockResponse.Object);
+ ControllerContext context = new ControllerContext(mockContext.Object, new RouteData(), new Mock<ControllerBase>().Object);
+ var result = new RedirectResult(_baseUrl);
+
+ // Act
+ result.ExecuteResult(context);
+
+ // Assert
+ mockResponse.Verify();
+ }
+
+ [Fact]
+ public void ExecuteResultWithPermanentCallsResponseRedirectPermanent()
+ {
+ // Arrange
+ Mock<HttpResponseBase> mockResponse = new Mock<HttpResponseBase>();
+ mockResponse.Setup(o => o.RedirectPermanent(_baseUrl, false /* endResponse */)).Verifiable();
+ Mock<HttpContextBase> mockContext = new Mock<HttpContextBase>();
+ mockContext.Setup(o => o.Response).Returns(mockResponse.Object);
+ ControllerContext context = new ControllerContext(mockContext.Object, new RouteData(), new Mock<ControllerBase>().Object);
+ var result = new RedirectResult(_baseUrl, permanent: true);
+
+ // Act
+ result.ExecuteResult(context);
+
+ // Assert
+ mockResponse.Verify();
+ }
+
+ [Fact]
+ public void ExecuteResultWithNullControllerContextThrows()
+ {
+ // Arrange
+ var result = new RedirectResult(_baseUrl);
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { result.ExecuteResult(null /* context */); },
+ "context");
+ }
+
+ [Fact]
+ public void RedirectInChildActionThrows()
+ {
+ // Arrange
+ RouteData routeData = new RouteData();
+ routeData.DataTokens[ControllerContext.ParentActionViewContextToken] = new ViewContext();
+ ControllerContext context = new ControllerContext(new Mock<HttpContextBase>().Object, routeData, new Mock<ControllerBase>().Object);
+ RedirectResult result = new RedirectResult(_baseUrl);
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ () => result.ExecuteResult(context),
+ MvcResources.RedirectAction_CannotRedirectInChildAction
+ );
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/RedirectToRouteResultTest.cs b/test/System.Web.Mvc.Test/Test/RedirectToRouteResultTest.cs
new file mode 100644
index 00000000..eb097c7d
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/RedirectToRouteResultTest.cs
@@ -0,0 +1,209 @@
+using System.Web.Routing;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class RedirectToRouteResultTest
+ {
+ [Fact]
+ public void ConstructorWithNullValuesDictionary()
+ {
+ // Act
+ var result = new RedirectToRouteResult(routeValues: null);
+
+ // Assert
+ Assert.NotNull(result.RouteValues);
+ Assert.Empty(result.RouteValues);
+ Assert.Equal(String.Empty, result.RouteName);
+ Assert.False(result.Permanent);
+ }
+
+ [Fact]
+ public void ConstructorSetsValuesDictionary()
+ {
+ // Arrange
+ RouteValueDictionary dict = new RouteValueDictionary();
+
+ // Act
+ var result = new RedirectToRouteResult(dict);
+
+ // Assert
+ Assert.Same(dict, result.RouteValues);
+ Assert.Equal(String.Empty, result.RouteName);
+ Assert.False(result.Permanent);
+ }
+
+ [Fact]
+ public void ConstructorSetsValuesDictionaryAndEmptyName()
+ {
+ // Arrange
+ RouteValueDictionary dict = new RouteValueDictionary();
+
+ // Act
+ var result = new RedirectToRouteResult(null, dict);
+
+ // Assert
+ Assert.Same(dict, result.RouteValues);
+ Assert.Equal(String.Empty, result.RouteName);
+ Assert.False(result.Permanent);
+ }
+
+ [Fact]
+ public void ConstructorSetsValuesDictionaryAndName()
+ {
+ // Arrange
+ RouteValueDictionary dict = new RouteValueDictionary();
+
+ // Act
+ var result = new RedirectToRouteResult("foo", dict);
+
+ // Assert
+ Assert.Same(dict, result.RouteValues);
+ Assert.Equal("foo", result.RouteName);
+ Assert.False(result.Permanent);
+ }
+
+ [Fact]
+ public void ConstructorSetsPermanent()
+ {
+ // Act
+ var result = new RedirectToRouteResult(null, null, true);
+
+ // Assert
+ Assert.True(result.Permanent);
+ }
+
+ [Fact]
+ public void ExecuteResultCallsResponseRedirect()
+ {
+ // Arrange
+ Mock<Controller> mockController = new Mock<Controller>();
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.Setup(c => c.HttpContext.Request.ApplicationPath).Returns("/somepath");
+ mockControllerContext.Setup(c => c.HttpContext.Response.ApplyAppPathModifier(It.IsAny<string>())).Returns((string s) => s);
+ mockControllerContext.Setup(c => c.HttpContext.Response.Redirect("/somepath/c/a/i", false)).Verifiable();
+ mockControllerContext.Setup(c => c.Controller).Returns(mockController.Object);
+
+ var values = new { Controller = "c", Action = "a", Id = "i" };
+ RedirectToRouteResult result = new RedirectToRouteResult(new RouteValueDictionary(values))
+ {
+ Routes = new RouteCollection() { new Route("{controller}/{action}/{id}", null) },
+ };
+
+ // Act
+ result.ExecuteResult(mockControllerContext.Object);
+
+ // Assert
+ mockControllerContext.Verify();
+ }
+
+ [Fact]
+ public void ExecuteResultWithPermanentCallsResponseRedirectPermanent()
+ {
+ // Arrange
+ Mock<Controller> mockController = new Mock<Controller>();
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.Setup(c => c.HttpContext.Request.ApplicationPath).Returns("/somepath");
+ mockControllerContext.Setup(c => c.HttpContext.Response.ApplyAppPathModifier(It.IsAny<string>())).Returns((string s) => s);
+ mockControllerContext.Setup(c => c.HttpContext.Response.RedirectPermanent("/somepath/c/a/i", false)).Verifiable();
+ mockControllerContext.Setup(c => c.Controller).Returns(mockController.Object);
+
+ var values = new { Controller = "c", Action = "a", Id = "i" };
+ RedirectToRouteResult result = new RedirectToRouteResult(null, new RouteValueDictionary(values), permanent: true)
+ {
+ Routes = new RouteCollection() { new Route("{controller}/{action}/{id}", null) },
+ };
+
+ // Act
+ result.ExecuteResult(mockControllerContext.Object);
+
+ // Assert
+ mockControllerContext.Verify();
+ }
+
+ [Fact]
+ public void ExecuteResultPreservesTempData()
+ {
+ // Arrange
+ TempDataDictionary tempData = new TempDataDictionary();
+ tempData["Foo"] = "Foo";
+ tempData["Bar"] = "Bar";
+ Mock<Controller> mockController = new Mock<Controller>() { CallBase = true };
+ mockController.Object.TempData = tempData;
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.Setup(c => c.HttpContext.Request.ApplicationPath).Returns("/somepath");
+ mockControllerContext.Setup(c => c.HttpContext.Response.ApplyAppPathModifier(It.IsAny<string>())).Returns((string s) => s);
+ mockControllerContext.Setup(c => c.HttpContext.Response.Redirect("/somepath/c/a/i", false)).Verifiable();
+ mockControllerContext.Setup(c => c.Controller).Returns(mockController.Object);
+
+ var values = new { Controller = "c", Action = "a", Id = "i" };
+ RedirectToRouteResult result = new RedirectToRouteResult(new RouteValueDictionary(values))
+ {
+ Routes = new RouteCollection() { new Route("{controller}/{action}/{id}", null) },
+ };
+
+ // Act
+ object value = tempData["Foo"];
+ result.ExecuteResult(mockControllerContext.Object);
+ mockController.Object.TempData.Save(mockControllerContext.Object, new Mock<ITempDataProvider>().Object);
+
+ // Assert
+ Assert.True(tempData.ContainsKey("Foo"));
+ Assert.True(tempData.ContainsKey("Bar"));
+ }
+
+ [Fact]
+ public void ExecuteResultThrowsIfVirtualPathDataIsNull()
+ {
+ // Arrange
+ var result = new RedirectToRouteResult(null)
+ {
+ Routes = new RouteCollection()
+ };
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ delegate { result.ExecuteResult(ControllerContextTest.CreateEmptyContext()); },
+ "No route in the route table matches the supplied values.");
+ }
+
+ [Fact]
+ public void ExecuteResultWithNullControllerContextThrows()
+ {
+ // Arrange
+ var result = new RedirectToRouteResult(null);
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { result.ExecuteResult(null /* context */); },
+ "context");
+ }
+
+ [Fact]
+ public void RoutesPropertyDefaultsToGlobalRouteTable()
+ {
+ // Act
+ var result = new RedirectToRouteResult(new RouteValueDictionary());
+
+ // Assert
+ Assert.Same(RouteTable.Routes, result.Routes);
+ }
+
+ [Fact]
+ public void RedirectInChildActionThrows()
+ {
+ // Arrange
+ RouteData routeData = new RouteData();
+ routeData.DataTokens[ControllerContext.ParentActionViewContextToken] = new ViewContext();
+ ControllerContext context = new ControllerContext(new Mock<HttpContextBase>().Object, routeData, new Mock<ControllerBase>().Object);
+ RedirectToRouteResult result = new RedirectToRouteResult(new RouteValueDictionary());
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ () => result.ExecuteResult(context),
+ "Child actions are not allowed to perform redirect actions.");
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ReflectedActionDescriptorTest.cs b/test/System.Web.Mvc.Test/Test/ReflectedActionDescriptorTest.cs
new file mode 100644
index 00000000..eaf418f4
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ReflectedActionDescriptorTest.cs
@@ -0,0 +1,491 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ReflectedActionDescriptorTest
+ {
+ private static readonly MethodInfo _int32EqualsIntMethod = typeof(int).GetMethod("Equals", new Type[] { typeof(int) });
+
+ [Fact]
+ public void ConstructorSetsActionNameProperty()
+ {
+ // Arrange
+ string name = "someName";
+
+ // Act
+ ReflectedActionDescriptor ad = new ReflectedActionDescriptor(new Mock<MethodInfo>().Object, "someName", new Mock<ControllerDescriptor>().Object, false /* validateMethod */);
+
+ // Assert
+ Assert.Equal(name, ad.ActionName);
+ }
+
+ [Fact]
+ public void ConstructorSetsControllerDescriptorProperty()
+ {
+ // Arrange
+ ControllerDescriptor cd = new Mock<ControllerDescriptor>().Object;
+
+ // Act
+ ReflectedActionDescriptor ad = new ReflectedActionDescriptor(new Mock<MethodInfo>().Object, "someName", cd, false /* validateMethod */);
+
+ // Assert
+ Assert.Same(cd, ad.ControllerDescriptor);
+ }
+
+ [Fact]
+ public void ConstructorSetsMethodInfoProperty()
+ {
+ // Arrange
+ MethodInfo methodInfo = new Mock<MethodInfo>().Object;
+
+ // Act
+ ReflectedActionDescriptor ad = new ReflectedActionDescriptor(methodInfo, "someName", new Mock<ControllerDescriptor>().Object, false /* validateMethod */);
+
+ // Assert
+ Assert.Same(methodInfo, ad.MethodInfo);
+ }
+
+ [Fact]
+ public void ConstructorThrowsIfActionNameIsEmpty()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { new ReflectedActionDescriptor(new Mock<MethodInfo>().Object, "", new Mock<ControllerDescriptor>().Object); }, "actionName");
+ }
+
+ [Fact]
+ public void ConstructorThrowsIfActionNameIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { new ReflectedActionDescriptor(new Mock<MethodInfo>().Object, null, new Mock<ControllerDescriptor>().Object); }, "actionName");
+ }
+
+ [Fact]
+ public void ConstructorThrowsIfControllerDescriptorIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new ReflectedActionDescriptor(new Mock<MethodInfo>().Object, "someName", null); }, "controllerDescriptor");
+ }
+
+ [Fact]
+ public void ConstructorThrowsIfMethodInfoHasRefParameters()
+ {
+ // Arrange
+ MethodInfo methodInfo = typeof(MyController).GetMethod("MethodHasRefParameter");
+
+ // Act & assert
+ Assert.Throws<ArgumentException>(
+ delegate { new ReflectedActionDescriptor(methodInfo, "someName", new Mock<ControllerDescriptor>().Object); },
+ @"Cannot call action method 'Void MethodHasRefParameter(Int32 ByRef)' on controller 'System.Web.Mvc.Test.ReflectedActionDescriptorTest+MyController' because the parameter 'Int32& i' is passed by reference.
+Parameter name: methodInfo");
+ }
+
+ [Fact]
+ public void ConstructorThrowsIfMethodInfoHasOutParameters()
+ {
+ // Arrange
+ MethodInfo methodInfo = typeof(MyController).GetMethod("MethodHasOutParameter");
+
+ // Act & assert
+ Assert.Throws<ArgumentException>(
+ delegate { new ReflectedActionDescriptor(methodInfo, "someName", new Mock<ControllerDescriptor>().Object); },
+ @"Cannot call action method 'Void MethodHasOutParameter(Int32 ByRef)' on controller 'System.Web.Mvc.Test.ReflectedActionDescriptorTest+MyController' because the parameter 'Int32& i' is passed by reference.
+Parameter name: methodInfo");
+ }
+
+ [Fact]
+ public void ConstructorThrowsIfMethodInfoIsInstanceMethodOnNonControllerBaseType()
+ {
+ // Arrange
+ MethodInfo methodInfo = typeof(string).GetMethod("Clone");
+
+ // Act & assert
+ Assert.Throws<ArgumentException>(
+ delegate { new ReflectedActionDescriptor(methodInfo, "someName", new Mock<ControllerDescriptor>().Object); },
+ @"Cannot create a descriptor for instance method 'System.Object Clone()' on type 'System.String' because the type does not derive from ControllerBase.
+Parameter name: methodInfo");
+ }
+
+ [Fact]
+ public void ConstructorThrowsIfMethodIsStatic()
+ {
+ // Arrange
+ MethodInfo methodInfo = typeof(MyController).GetMethod("StaticMethod");
+
+ // Act & assert
+ Assert.Throws<ArgumentException>(
+ delegate { new ReflectedActionDescriptor(methodInfo, "someName", new Mock<ControllerDescriptor>().Object); },
+ @"Cannot call action method 'Void StaticMethod()' on controller 'System.Web.Mvc.Test.ReflectedActionDescriptorTest+MyController' because the action method is a static method.
+Parameter name: methodInfo");
+ }
+
+ [Fact]
+ public void ConstructorThrowsIfMethodInfoIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new ReflectedActionDescriptor(null, "someName", new Mock<ControllerDescriptor>().Object); }, "methodInfo");
+ }
+
+ [Fact]
+ public void ConstructorThrowsIfMethodInfoIsOpenGenericType()
+ {
+ // Arrange
+ MethodInfo methodInfo = typeof(MyController).GetMethod("OpenGenericMethod");
+
+ // Act & assert
+ Assert.Throws<ArgumentException>(
+ delegate { new ReflectedActionDescriptor(methodInfo, "someName", new Mock<ControllerDescriptor>().Object); },
+ @"Cannot call action method 'Void OpenGenericMethod[T]()' on controller 'System.Web.Mvc.Test.ReflectedActionDescriptorTest+MyController' because the action method is a generic method.
+Parameter name: methodInfo");
+ }
+
+ [Fact]
+ public void ExecuteCallsMethodInfoOnSuccess()
+ {
+ // Arrange
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.Setup(c => c.Controller).Returns(new ConcatController());
+ Dictionary<string, object> parameters = new Dictionary<string, object>()
+ {
+ { "a", "hello " },
+ { "b", "world" }
+ };
+
+ ReflectedActionDescriptor ad = GetActionDescriptor(typeof(ConcatController).GetMethod("Concat"));
+
+ // Act
+ object result = ad.Execute(mockControllerContext.Object, parameters);
+
+ // Assert
+ Assert.Equal("hello world", result);
+ }
+
+ [Fact]
+ public void ExecuteThrowsIfControllerContextIsNull()
+ {
+ // Arrange
+ ReflectedActionDescriptor ad = GetActionDescriptor();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { ad.Execute(null, new Dictionary<string, object>()); }, "controllerContext");
+ }
+
+ [Fact]
+ public void ExecuteThrowsIfParametersContainsNullForNonNullableParameter()
+ {
+ // Arrange
+ ReflectedActionDescriptor ad = GetActionDescriptor(_int32EqualsIntMethod);
+ Dictionary<string, object> parameters = new Dictionary<string, object>() { { "obj", null } };
+
+ // Act & assert
+ Assert.Throws<ArgumentException>(
+ delegate { ad.Execute(new Mock<ControllerContext>().Object, parameters); },
+ @"The parameters dictionary contains a null entry for parameter 'obj' of non-nullable type 'System.Int32' for method 'Boolean Equals(Int32)' in 'System.Int32'. An optional parameter must be a reference type, a nullable type, or be declared as an optional parameter.
+Parameter name: parameters");
+ }
+
+ [Fact]
+ public void ExecuteThrowsIfParametersContainsValueOfWrongTypeForParameter()
+ {
+ // Arrange
+ ReflectedActionDescriptor ad = GetActionDescriptor(_int32EqualsIntMethod);
+ Dictionary<string, object> parameters = new Dictionary<string, object>() { { "obj", "notAnInteger" } };
+
+ // Act & assert
+ Assert.Throws<ArgumentException>(
+ delegate { ad.Execute(new Mock<ControllerContext>().Object, parameters); },
+ @"The parameters dictionary contains an invalid entry for parameter 'obj' for method 'Boolean Equals(Int32)' in 'System.Int32'. The dictionary contains a value of type 'System.String', but the parameter requires a value of type 'System.Int32'.
+Parameter name: parameters");
+ }
+
+ [Fact]
+ public void ExecuteThrowsIfParametersIsMissingAValue()
+ {
+ // Arrange
+ ReflectedActionDescriptor ad = GetActionDescriptor(_int32EqualsIntMethod);
+ Dictionary<string, object> parameters = new Dictionary<string, object>();
+
+ // Act & assert
+ Assert.Throws<ArgumentException>(
+ delegate { ad.Execute(new Mock<ControllerContext>().Object, parameters); },
+ @"The parameters dictionary does not contain an entry for parameter 'obj' of type 'System.Int32' for method 'Boolean Equals(Int32)' in 'System.Int32'. The dictionary must contain an entry for each parameter, including parameters that have null values.
+Parameter name: parameters");
+ }
+
+ [Fact]
+ public void ExecuteThrowsIfParametersIsNull()
+ {
+ // Arrange
+ ReflectedActionDescriptor ad = GetActionDescriptor();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { ad.Execute(new Mock<ControllerContext>().Object, null); }, "parameters");
+ }
+
+ [Fact]
+ public void GetCustomAttributesCallsMethodInfoGetCustomAttributes()
+ {
+ // Arrange
+ object[] expected = new object[0];
+ Mock<MethodInfo> mockMethod = new Mock<MethodInfo>();
+ mockMethod.Setup(mi => mi.GetCustomAttributes(true)).Returns(expected);
+ ReflectedActionDescriptor ad = GetActionDescriptor(mockMethod.Object);
+
+ // Act
+ object[] returned = ad.GetCustomAttributes(true);
+
+ // Assert
+ Assert.Same(expected, returned);
+ }
+
+ [Fact]
+ public void GetCustomAttributesWithAttributeTypeCallsMethodInfoGetCustomAttributes()
+ {
+ // Arrange
+ object[] expected = new object[0];
+ Mock<MethodInfo> mockMethod = new Mock<MethodInfo>();
+ mockMethod.Setup(mi => mi.GetCustomAttributes(typeof(ObsoleteAttribute), true)).Returns(expected);
+ ReflectedActionDescriptor ad = GetActionDescriptor(mockMethod.Object);
+
+ // Act
+ object[] returned = ad.GetCustomAttributes(typeof(ObsoleteAttribute), true);
+
+ // Assert
+ Assert.Same(expected, returned);
+ }
+
+ [Fact]
+ public void GetParametersWrapsParameterInfos()
+ {
+ // Arrange
+ ParameterInfo pInfo0 = typeof(ConcatController).GetMethod("Concat").GetParameters()[0];
+ ParameterInfo pInfo1 = typeof(ConcatController).GetMethod("Concat").GetParameters()[1];
+ ReflectedActionDescriptor ad = GetActionDescriptor(typeof(ConcatController).GetMethod("Concat"));
+
+ // Act
+ ParameterDescriptor[] pDescsFirstCall = ad.GetParameters();
+ ParameterDescriptor[] pDescsSecondCall = ad.GetParameters();
+
+ // Assert
+ Assert.NotSame(pDescsFirstCall, pDescsSecondCall);
+ Assert.True(pDescsFirstCall.SequenceEqual(pDescsSecondCall));
+ Assert.Equal(2, pDescsFirstCall.Length);
+
+ ReflectedParameterDescriptor pDesc0 = pDescsFirstCall[0] as ReflectedParameterDescriptor;
+ ReflectedParameterDescriptor pDesc1 = pDescsFirstCall[1] as ReflectedParameterDescriptor;
+
+ Assert.NotNull(pDesc0);
+ Assert.Same(ad, pDesc0.ActionDescriptor);
+ Assert.Same(pInfo0, pDesc0.ParameterInfo);
+ Assert.NotNull(pDesc1);
+ Assert.Same(ad, pDesc1.ActionDescriptor);
+ Assert.Same(pInfo1, pDesc1.ParameterInfo);
+ }
+
+ [Fact]
+ public void GetSelectorsWrapsSelectorAttributes()
+ {
+ // Arrange
+ ControllerContext controllerContext = new Mock<ControllerContext>().Object;
+ Mock<MethodInfo> mockMethod = new Mock<MethodInfo>();
+
+ Mock<ActionMethodSelectorAttribute> mockAttr = new Mock<ActionMethodSelectorAttribute>();
+ mockAttr.Setup(attr => attr.IsValidForRequest(controllerContext, mockMethod.Object)).Returns(true).Verifiable();
+ mockMethod.Setup(m => m.GetCustomAttributes(typeof(ActionMethodSelectorAttribute), true)).Returns(new ActionMethodSelectorAttribute[] { mockAttr.Object });
+
+ ReflectedActionDescriptor ad = GetActionDescriptor(mockMethod.Object);
+
+ // Act
+ ICollection<ActionSelector> selectors = ad.GetSelectors();
+ bool executedSuccessfully = selectors.All(s => s(controllerContext));
+
+ // Assert
+ Assert.Single(selectors);
+ Assert.True(executedSuccessfully);
+ mockAttr.Verify();
+ }
+
+ [Fact]
+ public void IsDefinedCallsMethodInfoIsDefined()
+ {
+ // Arrange
+ Mock<MethodInfo> mockMethod = new Mock<MethodInfo>();
+ mockMethod.Setup(mi => mi.IsDefined(typeof(ObsoleteAttribute), true)).Returns(true);
+ ReflectedActionDescriptor ad = GetActionDescriptor(mockMethod.Object);
+
+ // Act
+ bool isDefined = ad.IsDefined(typeof(ObsoleteAttribute), true);
+
+ // Assert
+ Assert.True(isDefined);
+ }
+
+ [Fact]
+ public void TryCreateDescriptorReturnsDescriptorOnSuccess()
+ {
+ // Arrange
+ MethodInfo methodInfo = typeof(MyController).GetMethod("GoodActionMethod");
+ ControllerDescriptor cd = new Mock<ControllerDescriptor>().Object;
+
+ // Act
+ ReflectedActionDescriptor ad = ReflectedActionDescriptor.TryCreateDescriptor(methodInfo, "someName", cd);
+
+ // Assert
+ Assert.NotNull(ad);
+ Assert.Same(methodInfo, ad.MethodInfo);
+ Assert.Equal("someName", ad.ActionName);
+ Assert.Same(cd, ad.ControllerDescriptor);
+ }
+
+ [Fact]
+ public void TryCreateDescriptorReturnsNullOnFailure()
+ {
+ // Arrange
+ MethodInfo methodInfo = typeof(MyController).GetMethod("OpenGenericMethod");
+ ControllerDescriptor cd = new Mock<ControllerDescriptor>().Object;
+
+ // Act
+ ReflectedActionDescriptor ad = ReflectedActionDescriptor.TryCreateDescriptor(methodInfo, "someName", cd);
+
+ // Assert
+ Assert.Null(ad);
+ }
+
+ private static ReflectedActionDescriptor GetActionDescriptor()
+ {
+ return GetActionDescriptor(new Mock<MethodInfo>().Object);
+ }
+
+ private static ReflectedActionDescriptor GetActionDescriptor(MethodInfo methodInfo)
+ {
+ return new ReflectedActionDescriptor(methodInfo, "someName", new Mock<ControllerDescriptor>().Object, false /* validateMethod */)
+ {
+ DispatcherCache = new ActionMethodDispatcherCache()
+ };
+ }
+
+ private class ConcatController : Controller
+ {
+ public string Concat(string a, string b)
+ {
+ return a + b;
+ }
+ }
+
+ [OutputCache(VaryByParam = "Class")]
+ private class OverriddenAttributeController : Controller
+ {
+ [OutputCache(VaryByParam = "Method")]
+ public void SomeMethod()
+ {
+ }
+ }
+
+ [KeyedActionFilter(Key = "BaseClass", Order = 0)]
+ [KeyedAuthorizationFilter(Key = "BaseClass", Order = 0)]
+ [KeyedExceptionFilter(Key = "BaseClass", Order = 0)]
+ private class GetMemberChainController : Controller
+ {
+ [KeyedActionFilter(Key = "BaseMethod", Order = 0)]
+ [KeyedAuthorizationFilter(Key = "BaseMethod", Order = 0)]
+ public virtual void SomeVirtual()
+ {
+ }
+ }
+
+ [KeyedActionFilter(Key = "DerivedClass", Order = 1)]
+ private class GetMemberChainDerivedController : GetMemberChainController
+ {
+ }
+
+ [KeyedActionFilter(Key = "SubderivedClass", Order = 2)]
+ private class GetMemberChainSubderivedController : GetMemberChainDerivedController
+ {
+ [KeyedActionFilter(Key = "SubderivedMethod", Order = 2)]
+ public override void SomeVirtual()
+ {
+ }
+ }
+
+ private abstract class KeyedFilterAttribute : FilterAttribute
+ {
+ public string Key { get; set; }
+ }
+
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
+ private class KeyedAuthorizationFilterAttribute : KeyedFilterAttribute, IAuthorizationFilter
+ {
+ public void OnAuthorization(AuthorizationContext filterContext)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
+ private class KeyedExceptionFilterAttribute : KeyedFilterAttribute, IExceptionFilter
+ {
+ public void OnException(ExceptionContext filterContext)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
+ private class KeyedActionFilterAttribute : KeyedFilterAttribute, IActionFilter, IResultFilter
+ {
+ public void OnActionExecuting(ActionExecutingContext filterContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void OnActionExecuted(ActionExecutedContext filterContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void OnResultExecuting(ResultExecutingContext filterContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void OnResultExecuted(ResultExecutedContext filterContext)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ private class MyController : Controller
+ {
+ public void GoodActionMethod()
+ {
+ }
+
+ public static void StaticMethod()
+ {
+ }
+
+ public void OpenGenericMethod<T>()
+ {
+ }
+
+ public void MethodHasOutParameter(out int i)
+ {
+ i = 0;
+ }
+
+ public void MethodHasRefParameter(ref int i)
+ {
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ReflectedControllerDescriptorTest.cs b/test/System.Web.Mvc.Test/Test/ReflectedControllerDescriptorTest.cs
new file mode 100644
index 00000000..5f5ca11f
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ReflectedControllerDescriptorTest.cs
@@ -0,0 +1,198 @@
+using System.Linq;
+using System.Reflection;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ReflectedControllerDescriptorTest
+ {
+ [Fact]
+ public void ConstructorSetsControllerTypeProperty()
+ {
+ // Arrange
+ Type controllerType = typeof(string);
+
+ // Act
+ ReflectedControllerDescriptor cd = new ReflectedControllerDescriptor(controllerType);
+
+ // Assert
+ Assert.Same(controllerType, cd.ControllerType);
+ }
+
+ [Fact]
+ public void ConstructorThrowsIfControllerTypeIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new ReflectedControllerDescriptor(null); }, "controllerType");
+ }
+
+ [Fact]
+ public void FindActionReturnsActionDescriptorIfFound()
+ {
+ // Arrange
+ Type controllerType = typeof(MyController);
+ MethodInfo targetMethod = controllerType.GetMethod("AliasedMethod");
+ ReflectedControllerDescriptor cd = new ReflectedControllerDescriptor(controllerType);
+
+ // Act
+ ActionDescriptor ad = cd.FindAction(new Mock<ControllerContext>().Object, "NewName");
+
+ // Assert
+ Assert.Equal("NewName", ad.ActionName);
+ Assert.IsType<ReflectedActionDescriptor>(ad);
+ Assert.Same(targetMethod, ((ReflectedActionDescriptor)ad).MethodInfo);
+ Assert.Same(cd, ad.ControllerDescriptor);
+ }
+
+ [Fact]
+ public void FindActionReturnsNullIfNoActionFound()
+ {
+ // Arrange
+ Type controllerType = typeof(MyController);
+ ReflectedControllerDescriptor cd = new ReflectedControllerDescriptor(controllerType);
+
+ // Act
+ ActionDescriptor ad = cd.FindAction(new Mock<ControllerContext>().Object, "NonExistent");
+
+ // Assert
+ Assert.Null(ad);
+ }
+
+ [Fact]
+ public void FindActionThrowsIfActionNameIsEmpty()
+ {
+ // Arrange
+ Type controllerType = typeof(MyController);
+ ReflectedControllerDescriptor cd = new ReflectedControllerDescriptor(controllerType);
+
+ // Act & assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { cd.FindAction(new Mock<ControllerContext>().Object, ""); }, "actionName");
+ }
+
+ [Fact]
+ public void FindActionThrowsIfActionNameIsNull()
+ {
+ // Arrange
+ Type controllerType = typeof(MyController);
+ ReflectedControllerDescriptor cd = new ReflectedControllerDescriptor(controllerType);
+
+ // Act & assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { cd.FindAction(new Mock<ControllerContext>().Object, null); }, "actionName");
+ }
+
+ [Fact]
+ public void FindActionThrowsIfControllerContextIsNull()
+ {
+ // Arrange
+ Type controllerType = typeof(MyController);
+ ReflectedControllerDescriptor cd = new ReflectedControllerDescriptor(controllerType);
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { cd.FindAction(null, "someName"); }, "controllerContext");
+ }
+
+ [Fact]
+ public void GetCanonicalActionsWrapsMethodInfos()
+ {
+ // Arrange
+ Type controllerType = typeof(MyController);
+ MethodInfo mInfo0 = controllerType.GetMethod("AliasedMethod");
+ MethodInfo mInfo1 = controllerType.GetMethod("NonAliasedMethod");
+ ReflectedControllerDescriptor cd = new ReflectedControllerDescriptor(controllerType);
+
+ // Act
+ ActionDescriptor[] aDescsFirstCall = cd.GetCanonicalActions();
+ ActionDescriptor[] aDescsSecondCall = cd.GetCanonicalActions();
+
+ // Assert
+ Assert.NotSame(aDescsFirstCall, aDescsSecondCall);
+ Assert.True(aDescsFirstCall.SequenceEqual(aDescsSecondCall));
+ Assert.Equal(2, aDescsFirstCall.Length);
+
+ ReflectedActionDescriptor aDesc0 = aDescsFirstCall[0] as ReflectedActionDescriptor;
+ ReflectedActionDescriptor aDesc1 = aDescsFirstCall[1] as ReflectedActionDescriptor;
+
+ Assert.NotNull(aDesc0);
+ Assert.Equal("AliasedMethod", aDesc0.ActionName);
+ Assert.Same(mInfo0, aDesc0.MethodInfo);
+ Assert.Same(cd, aDesc0.ControllerDescriptor);
+ Assert.NotNull(aDesc1);
+ Assert.Equal("NonAliasedMethod", aDesc1.ActionName);
+ Assert.Same(mInfo1, aDesc1.MethodInfo);
+ Assert.Same(cd, aDesc1.ControllerDescriptor);
+ }
+
+ [Fact]
+ public void GetCustomAttributesCallsTypeGetCustomAttributes()
+ {
+ // Arrange
+ object[] expected = new object[0];
+ Mock<Type> mockType = new Mock<Type>();
+ mockType.Setup(t => t.GetCustomAttributes(true)).Returns(expected);
+ ReflectedControllerDescriptor cd = new ReflectedControllerDescriptor(mockType.Object);
+
+ // Act
+ object[] returned = cd.GetCustomAttributes(true);
+
+ // Assert
+ Assert.Same(expected, returned);
+ }
+
+ [Fact]
+ public void GetCustomAttributesWithAttributeTypeCallsTypeGetCustomAttributes()
+ {
+ // Arrange
+ object[] expected = new object[0];
+ Mock<Type> mockType = new Mock<Type>();
+ mockType.Setup(t => t.GetCustomAttributes(typeof(ObsoleteAttribute), true)).Returns(expected);
+ ReflectedControllerDescriptor cd = new ReflectedControllerDescriptor(mockType.Object);
+
+ // Act
+ object[] returned = cd.GetCustomAttributes(typeof(ObsoleteAttribute), true);
+
+ // Assert
+ Assert.Same(expected, returned);
+ }
+
+ [Fact]
+ public void IsDefinedCallsTypeIsDefined()
+ {
+ // Arrange
+ Mock<Type> mockType = new Mock<Type>();
+ mockType.Setup(t => t.IsDefined(typeof(ObsoleteAttribute), true)).Returns(true);
+ ReflectedControllerDescriptor cd = new ReflectedControllerDescriptor(mockType.Object);
+
+ // Act
+ bool isDefined = cd.IsDefined(typeof(ObsoleteAttribute), true);
+
+ // Assert
+ Assert.True(isDefined);
+ }
+
+ private class MyController : Controller
+ {
+ [ActionName("NewName")]
+ public void AliasedMethod()
+ {
+ }
+
+ public void NonAliasedMethod()
+ {
+ }
+
+ public void GenericMethod<T>()
+ {
+ }
+
+ private void PrivateMethod()
+ {
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ReflectedParameterBindingInfoTest.cs b/test/System.Web.Mvc.Test/Test/ReflectedParameterBindingInfoTest.cs
new file mode 100644
index 00000000..f64238bf
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ReflectedParameterBindingInfoTest.cs
@@ -0,0 +1,172 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Reflection;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ReflectedParameterBindingInfoTest
+ {
+ [Fact]
+ public void BinderProperty()
+ {
+ // Arrange
+ ParameterInfo pInfo = typeof(MyController).GetMethod("ParameterHasSingleModelBinderAttribute").GetParameters()[0];
+ ReflectedParameterBindingInfo bindingInfo = new ReflectedParameterBindingInfo(pInfo);
+
+ // Act
+ IModelBinder binder = bindingInfo.Binder;
+
+ // Assert
+ Assert.IsType<MyModelBinder>(binder);
+ }
+
+ [Fact]
+ public void BinderPropertyThrowsIfMultipleBinderAttributesFound()
+ {
+ // Arrange
+ ParameterInfo pInfo = typeof(MyController).GetMethod("ParameterHasMultipleModelBinderAttributes").GetParameters()[0];
+ ReflectedParameterBindingInfo bindingInfo = new ReflectedParameterBindingInfo(pInfo);
+
+ // Act & assert
+ Assert.Throws<InvalidOperationException>(
+ delegate { IModelBinder binder = bindingInfo.Binder; },
+ "The parameter 'p1' on method 'Void ParameterHasMultipleModelBinderAttributes(System.Object)' contains multiple attributes that inherit from CustomModelBinderAttribute.");
+ }
+
+ [Fact]
+ public void ExcludeProperty()
+ {
+ // Arrange
+ ParameterInfo pInfo = typeof(MyController).GetMethod("ParameterHasBindAttribute").GetParameters()[0];
+ ReflectedParameterBindingInfo bindingInfo = new ReflectedParameterBindingInfo(pInfo);
+
+ // Act
+ ICollection<string> excludes = bindingInfo.Exclude;
+
+ // Assert
+ Assert.IsType<ReadOnlyCollection<string>>(excludes);
+
+ string[] excludesArray = excludes.ToArray();
+ Assert.Equal(2, excludesArray.Length);
+ Assert.Equal("excl_a", excludesArray[0]);
+ Assert.Equal("excl_b", excludesArray[1]);
+ }
+
+ [Fact]
+ public void ExcludePropertyReturnsEmptyArrayIfNoBindAttributeSpecified()
+ {
+ // Arrange
+ ParameterInfo pInfo = typeof(MyController).GetMethod("ParameterHasNoBindAttributes").GetParameters()[0];
+ ReflectedParameterBindingInfo bindingInfo = new ReflectedParameterBindingInfo(pInfo);
+
+ // Act
+ ICollection<string> excludes = bindingInfo.Exclude;
+
+ // Assert
+ Assert.NotNull(excludes);
+ Assert.Empty(excludes);
+ }
+
+ [Fact]
+ public void IncludeProperty()
+ {
+ // Arrange
+ ParameterInfo pInfo = typeof(MyController).GetMethod("ParameterHasBindAttribute").GetParameters()[0];
+ ReflectedParameterBindingInfo bindingInfo = new ReflectedParameterBindingInfo(pInfo);
+
+ // Act
+ ICollection<string> includes = bindingInfo.Include;
+
+ // Assert
+ Assert.IsType<ReadOnlyCollection<string>>(includes);
+
+ string[] includesArray = includes.ToArray();
+ Assert.Equal(2, includesArray.Length);
+ Assert.Equal("incl_a", includesArray[0]);
+ Assert.Equal("incl_b", includesArray[1]);
+ }
+
+ [Fact]
+ public void IncludePropertyReturnsEmptyArrayIfNoBindAttributeSpecified()
+ {
+ // Arrange
+ ParameterInfo pInfo = typeof(MyController).GetMethod("ParameterHasNoBindAttributes").GetParameters()[0];
+ ReflectedParameterBindingInfo bindingInfo = new ReflectedParameterBindingInfo(pInfo);
+
+ // Act
+ ICollection<string> includes = bindingInfo.Include;
+
+ // Assert
+ Assert.NotNull(includes);
+ Assert.Empty(includes);
+ }
+
+ [Fact]
+ public void PrefixProperty()
+ {
+ // Arrange
+ ParameterInfo pInfo = typeof(MyController).GetMethod("ParameterHasBindAttribute").GetParameters()[0];
+ ReflectedParameterBindingInfo bindingInfo = new ReflectedParameterBindingInfo(pInfo);
+
+ // Act
+ string prefix = bindingInfo.Prefix;
+
+ // Assert
+ Assert.Equal("some prefix", prefix);
+ }
+
+ [Fact]
+ public void PrefixPropertyReturnsNullIfNoBindAttributeSpecified()
+ {
+ // Arrange
+ ParameterInfo pInfo = typeof(MyController).GetMethod("ParameterHasNoBindAttributes").GetParameters()[0];
+ ReflectedParameterBindingInfo bindingInfo = new ReflectedParameterBindingInfo(pInfo);
+
+ // Act
+ string prefix = bindingInfo.Prefix;
+
+ // Assert
+ Assert.Null(prefix);
+ }
+
+ private class MyController : Controller
+ {
+ public void ParameterHasBindAttribute(
+ [Bind(Prefix = "some prefix", Include = "incl_a, incl_b", Exclude = "excl_a, excl_b")] object p1)
+ {
+ }
+
+ public void ParameterHasNoBindAttributes(object p1)
+ {
+ }
+
+ public void ParameterHasSingleModelBinderAttribute([ModelBinder(typeof(MyModelBinder))] object p1)
+ {
+ }
+
+ public void ParameterHasMultipleModelBinderAttributes([MyCustomModelBinder, MyCustomModelBinder] object p1)
+ {
+ }
+ }
+
+ [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true, Inherited = true)]
+ private class MyCustomModelBinderAttribute : CustomModelBinderAttribute
+ {
+ public override IModelBinder GetBinder()
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ private class MyModelBinder : IModelBinder
+ {
+ public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ReflectedParameterDescriptorTest.cs b/test/System.Web.Mvc.Test/Test/ReflectedParameterDescriptorTest.cs
new file mode 100644
index 00000000..ec949366
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ReflectedParameterDescriptorTest.cs
@@ -0,0 +1,159 @@
+using System.ComponentModel;
+using System.Reflection;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ReflectedParameterDescriptorTest
+ {
+ [Fact]
+ public void ConstructorSetsActionDescriptorProperty()
+ {
+ // Arrange
+ ParameterInfo pInfo = typeof(MyController).GetMethod("Foo").GetParameters()[0];
+ ActionDescriptor ad = new Mock<ActionDescriptor>().Object;
+
+ // Act
+ ReflectedParameterDescriptor pd = new ReflectedParameterDescriptor(pInfo, ad);
+
+ // Assert
+ Assert.Same(ad, pd.ActionDescriptor);
+ }
+
+ [Fact]
+ public void ConstructorSetsParameterInfo()
+ {
+ // Arrange
+ ParameterInfo pInfo = typeof(MyController).GetMethod("Foo").GetParameters()[0];
+
+ // Act
+ ReflectedParameterDescriptor pd = new ReflectedParameterDescriptor(pInfo, new Mock<ActionDescriptor>().Object);
+
+ // Assert
+ Assert.Same(pInfo, pd.ParameterInfo);
+ }
+
+ [Fact]
+ public void ConstructorThrowsIfActionDescriptorIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new ReflectedParameterDescriptor(new Mock<ParameterInfo>().Object, null); }, "actionDescriptor");
+ }
+
+ [Fact]
+ public void ConstructorThrowsIfParameterInfoIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new ReflectedParameterDescriptor(null, new Mock<ActionDescriptor>().Object); }, "parameterInfo");
+ }
+
+ [Fact]
+ public void DefaultValuePropertyDefaultsToNull()
+ {
+ // Arrange
+ ParameterInfo pInfo = typeof(MyController).GetMethod("DefaultValues").GetParameters()[0]; // noDefaultValue
+
+ // Act
+ ReflectedParameterDescriptor pd = GetParameterDescriptor(pInfo);
+
+ // Assert
+ Assert.Null(pd.DefaultValue);
+ }
+
+ [Fact]
+ public void GetCustomAttributesCallsParameterInfoGetCustomAttributes()
+ {
+ // Arrange
+ object[] expected = new object[0];
+ Mock<ParameterInfo> mockParameter = new Mock<ParameterInfo>();
+ mockParameter.Setup(pi => pi.Member).Returns(new Mock<MemberInfo>().Object);
+ mockParameter.Setup(pi => pi.GetCustomAttributes(true)).Returns(expected);
+ ReflectedParameterDescriptor pd = GetParameterDescriptor(mockParameter.Object);
+
+ // Act
+ object[] returned = pd.GetCustomAttributes(true);
+
+ // Assert
+ Assert.Same(expected, returned);
+ }
+
+ [Fact]
+ public void GetCustomAttributesWithAttributeTypeCallsParameterInfoGetCustomAttributes()
+ {
+ // Arrange
+ object[] expected = new object[0];
+ Mock<ParameterInfo> mockParameter = new Mock<ParameterInfo>();
+ mockParameter.Setup(pi => pi.Member).Returns(new Mock<MemberInfo>().Object);
+ mockParameter.Setup(pi => pi.GetCustomAttributes(typeof(ObsoleteAttribute), true)).Returns(expected);
+ ReflectedParameterDescriptor pd = GetParameterDescriptor(mockParameter.Object);
+
+ // Act
+ object[] returned = pd.GetCustomAttributes(typeof(ObsoleteAttribute), true);
+
+ // Assert
+ Assert.Same(expected, returned);
+ }
+
+ [Fact]
+ public void IsDefinedCallsParameterInfoIsDefined()
+ {
+ // Arrange
+ Mock<ParameterInfo> mockParameter = new Mock<ParameterInfo>();
+ mockParameter.Setup(pi => pi.Member).Returns(new Mock<MemberInfo>().Object);
+ mockParameter.Setup(pi => pi.IsDefined(typeof(ObsoleteAttribute), true)).Returns(true);
+ ReflectedParameterDescriptor pd = GetParameterDescriptor(mockParameter.Object);
+
+ // Act
+ bool isDefined = pd.IsDefined(typeof(ObsoleteAttribute), true);
+
+ // Assert
+ Assert.True(isDefined);
+ }
+
+ [Fact]
+ public void ParameterNameProperty()
+ {
+ // Arrange
+ ParameterInfo pInfo = typeof(MyController).GetMethod("Foo").GetParameters()[0];
+
+ // Act
+ ReflectedParameterDescriptor pd = GetParameterDescriptor(pInfo);
+
+ // Assert
+ Assert.Equal("s1", pd.ParameterName);
+ }
+
+ [Fact]
+ public void ParameterTypeProperty()
+ {
+ // Arrange
+ ParameterInfo pInfo = typeof(MyController).GetMethod("Foo").GetParameters()[0];
+
+ // Act
+ ReflectedParameterDescriptor pd = GetParameterDescriptor(pInfo);
+
+ // Assert
+ Assert.Equal(typeof(string), pd.ParameterType);
+ }
+
+ private static ReflectedParameterDescriptor GetParameterDescriptor(ParameterInfo parameterInfo)
+ {
+ return new ReflectedParameterDescriptor(parameterInfo, new Mock<ActionDescriptor>().Object);
+ }
+
+ private class MyController : Controller
+ {
+ public void Foo(string s1)
+ {
+ }
+
+ public void DefaultValues(string noDefaultValue, [DefaultValue("someValue")] string hasDefaultValue)
+ {
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/RegularExpressionAttributeAdapterTest.cs b/test/System.Web.Mvc.Test/Test/RegularExpressionAttributeAdapterTest.cs
new file mode 100644
index 00000000..cfba012e
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/RegularExpressionAttributeAdapterTest.cs
@@ -0,0 +1,31 @@
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class RegularExpressionAttributeAdapterTest
+ {
+ [Fact]
+ public void ClientRulesWithRegexAttribute()
+ {
+ // Arrange
+ var metadata = ModelMetadataProviders.Current.GetMetadataForProperty(() => null, typeof(string), "Length");
+ var context = new ControllerContext();
+ var attribute = new RegularExpressionAttribute("the_pattern");
+ var adapter = new RegularExpressionAttributeAdapter(metadata, context, attribute);
+
+ // Act
+ var rules = adapter.GetClientValidationRules()
+ .OrderBy(r => r.ValidationType)
+ .ToArray();
+
+ // Assert
+ ModelClientValidationRule rule = Assert.Single(rules);
+ Assert.Equal("regex", rule.ValidationType);
+ Assert.Single(rule.ValidationParameters);
+ Assert.Equal("the_pattern", rule.ValidationParameters["pattern"]);
+ Assert.Equal(@"The field Length must match the regular expression 'the_pattern'.", rule.ErrorMessage);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/RemoteAttributeTest.cs b/test/System.Web.Mvc.Test/Test/RemoteAttributeTest.cs
new file mode 100644
index 00000000..019066d2
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/RemoteAttributeTest.cs
@@ -0,0 +1,172 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Web.Routing;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class RemoteAttributeTest
+ {
+ // Good route name, bad route name
+ // Controller + Action
+
+ [Fact]
+ public void GuardClauses()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ () => new RemoteAttribute(null, "controller"),
+ "action");
+ Assert.ThrowsArgumentNullOrEmpty(
+ () => new RemoteAttribute("action", null),
+ "controller");
+ Assert.ThrowsArgumentNullOrEmpty(
+ () => new RemoteAttribute(null),
+ "routeName");
+ Assert.ThrowsArgumentNullOrEmpty(
+ () => RemoteAttribute.FormatPropertyForClientValidation(String.Empty),
+ "property");
+ Assert.ThrowsArgumentNullOrEmpty(
+ () => new RemoteAttribute("foo").FormatAdditionalFieldsForClientValidation(String.Empty),
+ "property");
+ }
+
+ [Fact]
+ public void IsValidAlwaysReturnsTrue()
+ {
+ // Act & Assert
+ Assert.True(new RemoteAttribute("RouteName", "ParameterName").IsValid(null));
+ Assert.True(new RemoteAttribute("ActionName", "ControllerName", "ParameterName").IsValid(null));
+ }
+
+ [Fact]
+ public void BadRouteNameThrows()
+ {
+ // Arrange
+ ControllerContext context = new ControllerContext();
+ ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(object));
+ TestableRemoteAttribute attribute = new TestableRemoteAttribute("RouteName");
+
+ // Act & Assert
+ Assert.Throws<ArgumentException>(
+ () => new List<ModelClientValidationRule>(attribute.GetClientValidationRules(metadata, context)),
+ "A route named 'RouteName' could not be found in the route collection.\r\nParameter name: name");
+ }
+
+ [Fact]
+ public void NoRouteWithActionControllerThrows()
+ {
+ // Arrange
+ ControllerContext context = new ControllerContext();
+ ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(null, typeof(string), "Length");
+ TestableRemoteAttribute attribute = new TestableRemoteAttribute("Action", "Controller");
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ () => new List<ModelClientValidationRule>(attribute.GetClientValidationRules(metadata, context)),
+ "No url for remote validation could be found.");
+ }
+
+ [Fact]
+ public void GoodRouteNameReturnsCorrectClientData()
+ {
+ // Arrange
+ string url = null;
+ ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(null, typeof(string), "Length");
+ TestableRemoteAttribute attribute = new TestableRemoteAttribute("RouteName");
+ attribute.RouteTable.Add("RouteName", new Route("my/url", new MvcRouteHandler()));
+
+ // Act
+ ModelClientValidationRule rule = attribute.GetClientValidationRules(metadata, GetMockControllerContext(url)).Single();
+
+ // Assert
+ Assert.Equal("remote", rule.ValidationType);
+ Assert.Equal("'Length' is invalid.", rule.ErrorMessage);
+ Assert.Equal(2, rule.ValidationParameters.Count);
+ Assert.Equal("/my/url", rule.ValidationParameters["url"]);
+ }
+
+ [Fact]
+ public void ActionControllerReturnsCorrectClientDataWithoutNamedParameters()
+ {
+ // Arrange
+ string url = null;
+
+ ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(null, typeof(string), "Length");
+ TestableRemoteAttribute attribute = new TestableRemoteAttribute("Action", "Controller");
+ attribute.RouteTable.Add(new Route("{controller}/{action}", new MvcRouteHandler()));
+
+ // Act
+ ModelClientValidationRule rule = attribute.GetClientValidationRules(metadata, GetMockControllerContext(url)).Single();
+
+ // Assert
+ Assert.Equal("remote", rule.ValidationType);
+ Assert.Equal("'Length' is invalid.", rule.ErrorMessage);
+ Assert.Equal(2, rule.ValidationParameters.Count);
+ Assert.Equal("/Controller/Action", rule.ValidationParameters["url"]);
+ Assert.Equal("*.Length", rule.ValidationParameters["additionalfields"]);
+ Assert.Throws<KeyNotFoundException>(
+ () => rule.ValidationParameters["type"],
+ "The given key was not present in the dictionary.");
+ }
+
+ [Fact]
+ public void ActionControllerReturnsCorrectClientDataWithNamedParameters()
+ {
+ // Arrange
+ string url = null;
+
+ ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(null, typeof(string), "Length");
+ TestableRemoteAttribute attribute = new TestableRemoteAttribute("Action", "Controller");
+ attribute.HttpMethod = "POST";
+ attribute.AdditionalFields = "Password,ConfirmPassword";
+
+ attribute.RouteTable.Add(new Route("{controller}/{action}", new MvcRouteHandler()));
+
+ // Act
+ ModelClientValidationRule rule = attribute.GetClientValidationRules(metadata, GetMockControllerContext(url)).Single();
+
+ // Assert
+ Assert.Equal("remote", rule.ValidationType);
+ Assert.Equal("'Length' is invalid.", rule.ErrorMessage);
+ Assert.Equal(3, rule.ValidationParameters.Count);
+ Assert.Equal("/Controller/Action", rule.ValidationParameters["url"]);
+ Assert.Equal("*.Length,*.Password,*.ConfirmPassword", rule.ValidationParameters["additionalfields"]);
+ Assert.Equal("POST", rule.ValidationParameters["type"]);
+ }
+
+ private ControllerContext GetMockControllerContext(string url)
+ {
+ Mock<ControllerContext> context = new Mock<ControllerContext>();
+ context.Setup(c => c.HttpContext.Request.ApplicationPath)
+ .Returns("/");
+ context.Setup(c => c.HttpContext.Response.ApplyAppPathModifier(It.IsAny<string>()))
+ .Callback<string>(vpath => url = vpath)
+ .Returns(() => url);
+
+ return context.Object;
+ }
+
+ private class TestableRemoteAttribute : RemoteAttribute
+ {
+ public RouteCollection RouteTable = new RouteCollection();
+
+ public TestableRemoteAttribute(string action, string controller)
+ : base(action, controller)
+ {
+ }
+
+ public TestableRemoteAttribute(string routeName)
+ : base(routeName)
+ {
+ }
+
+ protected override RouteCollection Routes
+ {
+ get { return RouteTable; }
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/RequireHttpsAttributeTest.cs b/test/System.Web.Mvc.Test/Test/RequireHttpsAttributeTest.cs
new file mode 100644
index 00000000..38d623ac
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/RequireHttpsAttributeTest.cs
@@ -0,0 +1,106 @@
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class RequireHttpsAttributeTest
+ {
+ [Fact]
+ public void HandleNonHttpsRequestExtensibility()
+ {
+ // Arrange
+ Mock<AuthorizationContext> mockAuthContext = new Mock<AuthorizationContext>();
+ mockAuthContext.Setup(c => c.HttpContext.Request.IsSecureConnection).Returns(false);
+ AuthorizationContext authContext = mockAuthContext.Object;
+
+ RequireHttpsAttribute attr = new MyRequireHttpsAttribute();
+
+ // Act
+ attr.OnAuthorization(authContext);
+ ContentResult result = authContext.Result as ContentResult;
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal("Custom HandleNonHttpsRequest", result.Content);
+ }
+
+ [Fact]
+ public void OnAuthorizationDoesNothingIfRequestIsSecure()
+ {
+ // Arrange
+ Mock<AuthorizationContext> mockAuthContext = new Mock<AuthorizationContext>();
+ mockAuthContext.Setup(c => c.HttpContext.Request.IsSecureConnection).Returns(true);
+ AuthorizationContext authContext = mockAuthContext.Object;
+
+ ViewResult result = new ViewResult();
+ authContext.Result = result;
+
+ RequireHttpsAttribute attr = new RequireHttpsAttribute();
+
+ // Act
+ attr.OnAuthorization(authContext);
+
+ // Assert
+ Assert.Same(result, authContext.Result);
+ }
+
+ [Fact]
+ public void OnAuthorizationRedirectsIfRequestIsNotSecureAndMethodIsGet()
+ {
+ // Arrange
+ Mock<AuthorizationContext> mockAuthContext = new Mock<AuthorizationContext>();
+ mockAuthContext.Setup(c => c.HttpContext.Request.HttpMethod).Returns("get");
+ mockAuthContext.Setup(c => c.HttpContext.Request.IsSecureConnection).Returns(false);
+ mockAuthContext.Setup(c => c.HttpContext.Request.RawUrl).Returns("/alpha/bravo/charlie?q=quux");
+ mockAuthContext.Setup(c => c.HttpContext.Request.Url).Returns(new Uri("http://www.example.com:8080/foo/bar/baz"));
+ AuthorizationContext authContext = mockAuthContext.Object;
+
+ RequireHttpsAttribute attr = new RequireHttpsAttribute();
+
+ // Act
+ attr.OnAuthorization(authContext);
+ RedirectResult result = authContext.Result as RedirectResult;
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal("https://www.example.com/alpha/bravo/charlie?q=quux", result.Url);
+ }
+
+ [Fact]
+ public void OnAuthorizationThrowsIfFilterContextIsNull()
+ {
+ // Arrange
+ RequireHttpsAttribute attr = new RequireHttpsAttribute();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { attr.OnAuthorization(null); }, "filterContext");
+ }
+
+ [Fact]
+ public void OnAuthorizationThrowsIfRequestIsNotSecureAndMethodIsNotGet()
+ {
+ // Arrange
+ Mock<AuthorizationContext> mockAuthContext = new Mock<AuthorizationContext>();
+ mockAuthContext.Setup(c => c.HttpContext.Request.HttpMethod).Returns("post");
+ mockAuthContext.Setup(c => c.HttpContext.Request.IsSecureConnection).Returns(false);
+ AuthorizationContext authContext = mockAuthContext.Object;
+
+ RequireHttpsAttribute attr = new RequireHttpsAttribute();
+
+ // Act & assert
+ Assert.Throws<InvalidOperationException>(
+ delegate { attr.OnAuthorization(authContext); },
+ @"The requested resource can only be accessed via SSL.");
+ }
+
+ private class MyRequireHttpsAttribute : RequireHttpsAttribute
+ {
+ protected override void HandleNonHttpsRequest(AuthorizationContext filterContext)
+ {
+ filterContext.Result = new ContentResult() { Content = "Custom HandleNonHttpsRequest" };
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/RequiredAttributeAdapterTest.cs b/test/System.Web.Mvc.Test/Test/RequiredAttributeAdapterTest.cs
new file mode 100644
index 00000000..62c041eb
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/RequiredAttributeAdapterTest.cs
@@ -0,0 +1,30 @@
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class RequiredAttributeAdapterTest
+ {
+ [Fact]
+ public void ClientRulesWithRequiredAttribute()
+ {
+ // Arrange
+ var metadata = ModelMetadataProviders.Current.GetMetadataForProperty(() => null, typeof(string), "Length");
+ var context = new ControllerContext();
+ var attribute = new RequiredAttribute();
+ var adapter = new RequiredAttributeAdapter(metadata, context, attribute);
+
+ // Act
+ var rules = adapter.GetClientValidationRules()
+ .OrderBy(r => r.ValidationType)
+ .ToArray();
+
+ // Assert
+ ModelClientValidationRule rule = Assert.Single(rules);
+ Assert.Equal("required", rule.ValidationType);
+ Assert.Empty(rule.ValidationParameters);
+ Assert.Equal(@"The Length field is required.", rule.ErrorMessage);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ResultExecutedContextTest.cs b/test/System.Web.Mvc.Test/Test/ResultExecutedContextTest.cs
new file mode 100644
index 00000000..7852b7eb
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ResultExecutedContextTest.cs
@@ -0,0 +1,55 @@
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ResultExecutedContextTest
+ {
+ [Fact]
+ public void ConstructorThrowsIfControllerDescriptorIsNull()
+ {
+ // Arrange
+ ControllerContext controllerContext = null;
+ ActionResult result = new ViewResult();
+ bool canceled = true;
+ Exception exception = null;
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new ResultExecutedContext(controllerContext, result, canceled, exception); }, "controllerContext");
+ }
+
+ [Fact]
+ public void ConstructorThrowsIfResultIsNull()
+ {
+ // Arrange
+ ControllerContext controllerContext = new Mock<ControllerContext>().Object;
+ ActionResult result = null;
+ bool canceled = true;
+ Exception exception = null;
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new ResultExecutedContext(controllerContext, result, canceled, exception); }, "result");
+ }
+
+ [Fact]
+ public void PropertiesAreSetByConstructor()
+ {
+ // Arrange
+ ControllerContext controllerContext = new Mock<ControllerContext>().Object;
+ ActionResult result = new ViewResult();
+ bool canceled = true;
+ Exception exception = new Exception();
+
+ // Act
+ ResultExecutedContext resultExecutedContext = new ResultExecutedContext(controllerContext, result, canceled, exception);
+
+ // Assert
+ Assert.Equal(result, resultExecutedContext.Result);
+ Assert.Equal(canceled, resultExecutedContext.Canceled);
+ Assert.Equal(exception, resultExecutedContext.Exception);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ResultExecutingContextTest.cs b/test/System.Web.Mvc.Test/Test/ResultExecutingContextTest.cs
new file mode 100644
index 00000000..2881e2c0
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ResultExecutingContextTest.cs
@@ -0,0 +1,47 @@
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ResultExecutingContextTest
+ {
+ [Fact]
+ public void ConstructorThrowsIfControllerContextIsNull()
+ {
+ // Arrange
+ ControllerContext controllerContext = null;
+ ActionResult result = new ViewResult();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new ResultExecutingContext(controllerContext, result); }, "controllerContext");
+ }
+
+ [Fact]
+ public void ConstructorThrowsIfResultIsNull()
+ {
+ // Arrange
+ ControllerContext controllerContext = new Mock<ControllerContext>().Object;
+ ActionResult result = null;
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new ResultExecutingContext(controllerContext, result); }, "result");
+ }
+
+ [Fact]
+ public void ResultProperty()
+ {
+ // Arrange
+ ControllerContext controllerContext = new Mock<ControllerContext>().Object;
+ ActionResult result = new ViewResult();
+
+ // Act
+ ResultExecutingContext resultExecutingContext = new ResultExecutingContext(controllerContext, result);
+
+ // Assert
+ Assert.Equal(result, resultExecutingContext.Result);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/RouteCollectionExtensionsTest.cs b/test/System.Web.Mvc.Test/Test/RouteCollectionExtensionsTest.cs
new file mode 100644
index 00000000..300de646
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/RouteCollectionExtensionsTest.cs
@@ -0,0 +1,388 @@
+using System.Linq;
+using System.Web.Routing;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class RouteCollectionExtensionsTest
+ {
+ private static string[] _nameSpaces = new string[] { "nsA.nsB.nsC", "ns1.ns2.ns3" };
+
+ [Fact]
+ public void GetVirtualPathForAreaDoesNotStripAreaTokenIfAreasNotInUse()
+ {
+ // Arrange
+ RouteCollection routes = new RouteCollection();
+ routes.MapRoute(
+ "Default",
+ "no-area/{controller}/{action}/{id}",
+ new { controller = "Home", action = "Index", id = "" }
+ );
+
+ RequestContext requestContext = GetRequestContext(null);
+ RouteValueDictionary values = new RouteValueDictionary()
+ {
+ { "controller", "home" },
+ { "action", "about" },
+ { "area", "some-area" }
+ };
+
+ // Act
+ VirtualPathData vpd = routes.GetVirtualPathForArea(requestContext, values);
+
+ // Assert
+ Assert.NotNull(vpd);
+ Assert.Equal(routes["Default"], vpd.Route);
+
+ // note presence of 'area' query string parameter; RVD should not be modified if areas not in use
+ Assert.Equal("/app/no-area/home/about?area=some-area", vpd.VirtualPath);
+ }
+
+ [Fact]
+ public void GetVirtualPathForAreaForwardsCallIfRouteNameSpecified()
+ {
+ // Arrange
+ RouteCollection routes = GetRouteCollection();
+ RequestContext requestContext = GetRequestContext(null);
+ RouteValueDictionary values = new RouteValueDictionary()
+ {
+ { "controller", "home" },
+ { "action", "index" },
+ { "area", "some-area" }
+ };
+
+ // Act
+ VirtualPathData vpd = routes.GetVirtualPathForArea(requestContext, "admin_default", values);
+
+ // Assert
+ Assert.NotNull(vpd);
+ Assert.Equal(routes["admin_default"], vpd.Route);
+
+ // note presence of 'area' query string parameter; RVD should not be modified if route name was provided
+ Assert.Equal("/app/admin-area?area=some-area", vpd.VirtualPath);
+ }
+
+ [Fact]
+ public void GetVirtualPathForAreaThrowsIfRoutesIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { RouteCollectionExtensions.GetVirtualPathForArea(null, null, null); }, "routes");
+ }
+
+ [Fact]
+ public void GetVirtualPathForAreaWillJumpBetweenAreasExplicitly()
+ {
+ // Arrange
+ RouteCollection routes = GetRouteCollection();
+ RequestContext requestContext = GetRequestContext(null);
+ RouteValueDictionary values = new RouteValueDictionary()
+ {
+ { "controller", "home" },
+ { "action", "tenmostrecent" },
+ { "tag", "some-tag" },
+ { "area", "blog" }
+ };
+
+ // Act
+ VirtualPathData vpd = routes.GetVirtualPathForArea(requestContext, values);
+
+ // Assert
+ Assert.NotNull(vpd);
+ Assert.Equal(routes["blog_whatsnew"], vpd.Route);
+ Assert.Equal("/app/whats-new/some-tag", vpd.VirtualPath);
+ }
+
+ [Fact]
+ public void GetVirtualPathForAreaWillNotJumpBetweenAreasImplicitly()
+ {
+ // Arrange
+ RouteCollection routes = GetRouteCollection();
+ RequestContext requestContext = GetRequestContext("admin");
+ RouteValueDictionary values = new RouteValueDictionary()
+ {
+ { "controller", "home" },
+ { "action", "tenmostrecent" },
+ { "tag", "some-tag" }
+ };
+
+ // Act
+ VirtualPathData vpd = routes.GetVirtualPathForArea(requestContext, values);
+
+ // Assert
+ Assert.NotNull(vpd);
+ Assert.Equal(routes["admin_default"], vpd.Route);
+ Assert.Equal("/app/admin-area/home/tenmostrecent?tag=some-tag", vpd.VirtualPath);
+ }
+
+ [Fact]
+ public void MapRoute3()
+ {
+ // Arrange
+ RouteCollection routes = new RouteCollection();
+
+ // Act
+ routes.MapRoute("RouteName", "SomeUrl");
+
+ // Assert
+ Route route = Assert.Single(routes.Cast<Route>());
+ Assert.NotNull(route);
+ Assert.Same(route, routes["RouteName"]);
+ Assert.Equal("SomeUrl", route.Url);
+ Assert.IsType<MvcRouteHandler>(route.RouteHandler);
+ Assert.Empty(route.Defaults);
+ Assert.Empty(route.Constraints);
+ Assert.Empty(route.DataTokens);
+ }
+
+ [Fact]
+ public void MapRoute3WithNameSpaces()
+ {
+ // Arrange
+ RouteCollection routes = new RouteCollection();
+ //string[] namespaces = new string[] { "nsA.nsB.nsC", "ns1.ns2.ns3" };
+
+ // Act
+ routes.MapRoute("RouteName", "SomeUrl", _nameSpaces);
+
+ // Assert
+ Route route = Assert.Single(routes.Cast<Route>());
+ Assert.NotNull(route);
+ Assert.NotNull(route.DataTokens);
+ Assert.NotNull(route.DataTokens["Namespaces"]);
+ string[] routeNameSpaces = route.DataTokens["Namespaces"] as string[];
+ Assert.Equal(routeNameSpaces.Length, 2);
+ Assert.Same(route, routes["RouteName"]);
+ Assert.Same(routeNameSpaces, _nameSpaces);
+ Assert.Equal("SomeUrl", route.Url);
+ Assert.IsType<MvcRouteHandler>(route.RouteHandler);
+ Assert.Empty(route.Defaults);
+ Assert.Empty(route.Constraints);
+ }
+
+ [Fact]
+ public void MapRoute3WithEmptyNameSpaces()
+ {
+ // Arrange
+ RouteCollection routes = new RouteCollection();
+
+ // Act
+ routes.MapRoute("RouteName", "SomeUrl", new string[] { });
+
+ // Assert
+ Route route = Assert.Single(routes.Cast<Route>());
+ Assert.NotNull(route);
+ Assert.Same(route, routes["RouteName"]);
+ Assert.Equal("SomeUrl", route.Url);
+ Assert.IsType<MvcRouteHandler>(route.RouteHandler);
+ Assert.Empty(route.Defaults);
+ Assert.Empty(route.Constraints);
+ Assert.Empty(route.DataTokens);
+ }
+
+ [Fact]
+ public void MapRoute4()
+ {
+ // Arrange
+ RouteCollection routes = new RouteCollection();
+ var defaults = new { Foo = "DefaultFoo" };
+
+ // Act
+ routes.MapRoute("RouteName", "SomeUrl", defaults);
+
+ // Assert
+ Route route = Assert.Single(routes.Cast<Route>());
+ Assert.NotNull(route);
+ Assert.Same(route, routes["RouteName"]);
+ Assert.Equal("SomeUrl", route.Url);
+ Assert.IsType<MvcRouteHandler>(route.RouteHandler);
+ Assert.Equal("DefaultFoo", route.Defaults["Foo"]);
+ Assert.Empty(route.Constraints);
+ Assert.Empty(route.DataTokens);
+ }
+
+ [Fact]
+ public void MapRoute4WithNameSpaces()
+ {
+ // Arrange
+ RouteCollection routes = new RouteCollection();
+ var defaults = new { Foo = "DefaultFoo" };
+
+ // Act
+ routes.MapRoute("RouteName", "SomeUrl", defaults, _nameSpaces);
+
+ // Assert
+ Route route = Assert.Single(routes.Cast<Route>());
+ Assert.NotNull(route);
+ Assert.NotNull(route.DataTokens);
+ Assert.NotNull(route.DataTokens["Namespaces"]);
+ string[] routeNameSpaces = route.DataTokens["Namespaces"] as string[];
+ Assert.Equal(routeNameSpaces.Length, 2);
+ Assert.Same(route, routes["RouteName"]);
+ Assert.Same(routeNameSpaces, _nameSpaces);
+ Assert.Equal("SomeUrl", route.Url);
+ Assert.IsType<MvcRouteHandler>(route.RouteHandler);
+ Assert.Equal("DefaultFoo", route.Defaults["Foo"]);
+ Assert.Empty(route.Constraints);
+ }
+
+ [Fact]
+ public void MapRoute5()
+ {
+ // Arrange
+ RouteCollection routes = new RouteCollection();
+ var defaults = new { Foo = "DefaultFoo" };
+ var constraints = new { Foo = "ConstraintFoo" };
+
+ // Act
+ routes.MapRoute("RouteName", "SomeUrl", defaults, constraints);
+
+ // Assert
+ Route route = Assert.Single(routes.Cast<Route>());
+ Assert.NotNull(route);
+ Assert.Same(route, routes["RouteName"]);
+ Assert.Equal("SomeUrl", route.Url);
+ Assert.IsType<MvcRouteHandler>(route.RouteHandler);
+ Assert.Equal("DefaultFoo", route.Defaults["Foo"]);
+ Assert.Equal("ConstraintFoo", route.Constraints["Foo"]);
+ Assert.Empty(route.DataTokens);
+ }
+
+ [Fact]
+ public void MapRoute5WithNullRouteCollectionThrows()
+ {
+ Assert.ThrowsArgumentNull(
+ delegate { RouteCollectionExtensions.MapRoute(null, null, null, null, null); },
+ "routes");
+ }
+
+ [Fact]
+ public void MapRoute5WithNullUrlThrows()
+ {
+ // Arrange
+ RouteCollection routes = new RouteCollection();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { routes.MapRoute(null, null /* url */, null, null); },
+ "url");
+ }
+
+ [Fact]
+ public void IgnoreRoute1WithNullRouteCollectionThrows()
+ {
+ Assert.ThrowsArgumentNull(
+ delegate { RouteCollectionExtensions.IgnoreRoute(null, "foo"); },
+ "routes");
+ }
+
+ [Fact]
+ public void IgnoreRoute1WithNullUrlThrows()
+ {
+ // Arrange
+ RouteCollection routes = new RouteCollection();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { routes.IgnoreRoute(null); },
+ "url");
+ }
+
+ [Fact]
+ public void IgnoreRoute3()
+ {
+ // Arrange
+ RouteCollection routes = new RouteCollection();
+
+ // Act
+ routes.IgnoreRoute("SomeUrl");
+
+ // Assert
+ Route route = Assert.Single(routes.Cast<Route>());
+ Assert.NotNull(route);
+ Assert.Equal("SomeUrl", route.Url);
+ Assert.IsType<StopRoutingHandler>(route.RouteHandler);
+ Assert.Null(route.Defaults);
+ Assert.Empty(route.Constraints);
+ }
+
+ [Fact]
+ public void IgnoreRoute4()
+ {
+ // Arrange
+ RouteCollection routes = new RouteCollection();
+ var constraints = new { Foo = "DefaultFoo" };
+
+ // Act
+ routes.IgnoreRoute("SomeUrl", constraints);
+
+ // Assert
+ Route route = Assert.Single(routes.Cast<Route>());
+ Assert.NotNull(route);
+ Assert.Equal("SomeUrl", route.Url);
+ Assert.IsType<StopRoutingHandler>(route.RouteHandler);
+ Assert.Null(route.Defaults);
+ Assert.Single(route.Constraints);
+ Assert.Equal("DefaultFoo", route.Constraints["Foo"]);
+ }
+
+ [Fact]
+ public void IgnoreRouteInternalNeverMatchesUrlGeneration()
+ {
+ // Arrange
+ RouteCollection routes = new RouteCollection();
+ routes.IgnoreRoute("SomeUrl");
+ Route route = routes[0] as Route;
+
+ // Act
+ VirtualPathData vpd = route.GetVirtualPath(new RequestContext(new Mock<HttpContextBase>().Object, new RouteData()), null);
+
+ // Assert
+ Assert.Null(vpd);
+ }
+
+ private static RequestContext GetRequestContext(string currentAreaName)
+ {
+ Mock<HttpContextBase> mockHttpContext = new Mock<HttpContextBase>();
+ mockHttpContext.Setup(c => c.Request.ApplicationPath).Returns("/app");
+ mockHttpContext.Setup(c => c.Response.ApplyAppPathModifier(It.IsAny<string>())).Returns<string>(virtualPath => virtualPath);
+
+ RouteData routeData = new RouteData();
+ routeData.DataTokens["area"] = currentAreaName;
+ return new RequestContext(mockHttpContext.Object, routeData);
+ }
+
+ private static RouteCollection GetRouteCollection()
+ {
+ RouteCollection routes = new RouteCollection();
+ routes.MapRoute(
+ "Default",
+ "no-area/{controller}/{action}/{id}",
+ new { controller = "Home", action = "Index", id = "" }
+ );
+
+ AreaRegistrationContext blogContext = new AreaRegistrationContext("blog", routes);
+ blogContext.MapRoute(
+ "Blog_WhatsNew",
+ "whats-new/{tag}",
+ new { controller = "Home", action = "TenMostRecent", tag = "" }
+ );
+ blogContext.MapRoute(
+ "Blog_Default",
+ "blog-area/{controller}/{action}/{id}",
+ new { controller = "Home", action = "Index", id = "" }
+ );
+
+ AreaRegistrationContext adminContext = new AreaRegistrationContext("admin", routes);
+ adminContext.MapRoute(
+ "Admin_Default",
+ "admin-area/{controller}/{action}/{id}",
+ new { controller = "Home", action = "Index", id = "" }
+ );
+
+ return routes;
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/RouteDataValueProviderFactoryTest.cs b/test/System.Web.Mvc.Test/Test/RouteDataValueProviderFactoryTest.cs
new file mode 100644
index 00000000..dc66d1eb
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/RouteDataValueProviderFactoryTest.cs
@@ -0,0 +1,44 @@
+using System.Globalization;
+using System.Web.Routing;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class RouteDataValueProviderFactoryTest
+ {
+ [Fact]
+ public void GetValueProvider()
+ {
+ // Arrange
+ RouteDataValueProviderFactory factory = new RouteDataValueProviderFactory();
+
+ ControllerContext controllerContext = new ControllerContext();
+ controllerContext.RouteData = new RouteData();
+ controllerContext.RouteData.Values["forty-two"] = 42;
+
+ // Act
+ IValueProvider valueProvider = factory.GetValueProvider(controllerContext);
+
+ // Assert
+ Assert.IsType<RouteDataValueProvider>(valueProvider);
+ ValueProviderResult vpResult = valueProvider.GetValue("forty-two");
+
+ Assert.NotNull(vpResult);
+ Assert.Equal(42, vpResult.RawValue);
+ Assert.Equal("42", vpResult.AttemptedValue);
+ Assert.Equal(CultureInfo.InvariantCulture, vpResult.Culture);
+ }
+
+ [Fact]
+ public void GetValueProvider_ThrowsIfControllerContextIsNull()
+ {
+ // Arrange
+ RouteDataValueProviderFactory factory = new RouteDataValueProviderFactory();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { factory.GetValueProvider(null); }, "controllerContext");
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/SelectListTest.cs b/test/System.Web.Mvc.Test/Test/SelectListTest.cs
new file mode 100644
index 00000000..1691291c
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/SelectListTest.cs
@@ -0,0 +1,84 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class SelectListTest
+ {
+ [Fact]
+ public void Constructor1SetsProperties()
+ {
+ // Arrange
+ IEnumerable items = new object[0];
+
+ // Act
+ SelectList selectList = new SelectList(items);
+
+ // Assert
+ Assert.Same(items, selectList.Items);
+ Assert.Null(selectList.DataValueField);
+ Assert.Null(selectList.DataTextField);
+ Assert.Null(selectList.SelectedValues);
+ Assert.Null(selectList.SelectedValue);
+ }
+
+ [Fact]
+ public void Constructor2SetsProperties()
+ {
+ // Arrange
+ IEnumerable items = new object[0];
+ object selectedValue = new object();
+
+ // Act
+ SelectList selectList = new SelectList(items, selectedValue);
+ List<object> selectedValues = selectList.SelectedValues.Cast<object>().ToList();
+
+ // Assert
+ Assert.Same(items, selectList.Items);
+ Assert.Null(selectList.DataValueField);
+ Assert.Null(selectList.DataTextField);
+ Assert.Same(selectedValue, selectList.SelectedValue);
+ Assert.Single(selectedValues);
+ Assert.Same(selectedValue, selectedValues[0]);
+ }
+
+ [Fact]
+ public void Constructor3SetsProperties()
+ {
+ // Arrange
+ IEnumerable items = new object[0];
+
+ // Act
+ SelectList selectList = new SelectList(items, "SomeValueField", "SomeTextField");
+
+ // Assert
+ Assert.Same(items, selectList.Items);
+ Assert.Equal("SomeValueField", selectList.DataValueField);
+ Assert.Equal("SomeTextField", selectList.DataTextField);
+ Assert.Null(selectList.SelectedValues);
+ Assert.Null(selectList.SelectedValue);
+ }
+
+ [Fact]
+ public void Constructor4SetsProperties()
+ {
+ // Arrange
+ IEnumerable items = new object[0];
+ object selectedValue = new object();
+
+ // Act
+ SelectList selectList = new SelectList(items, "SomeValueField", "SomeTextField", selectedValue);
+ List<object> selectedValues = selectList.SelectedValues.Cast<object>().ToList();
+
+ // Assert
+ Assert.Same(items, selectList.Items);
+ Assert.Equal("SomeValueField", selectList.DataValueField);
+ Assert.Equal("SomeTextField", selectList.DataTextField);
+ Assert.Same(selectedValue, selectList.SelectedValue);
+ Assert.Single(selectedValues);
+ Assert.Same(selectedValue, selectedValues[0]);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/SessionStateTempDataProviderTest.cs b/test/System.Web.Mvc.Test/Test/SessionStateTempDataProviderTest.cs
new file mode 100644
index 00000000..252ff007
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/SessionStateTempDataProviderTest.cs
@@ -0,0 +1,166 @@
+using System.Collections.Generic;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class SessionStateTempDataProviderTest
+ {
+ [Fact]
+ public void Load_NullSession_ReturnsEmptyDictionary()
+ {
+ // Arrange
+ SessionStateTempDataProvider testProvider = new SessionStateTempDataProvider();
+
+ // Act
+ IDictionary<string, object> tempDataDictionary = testProvider.LoadTempData(GetControllerContext());
+
+ // Assert
+ Assert.Empty(tempDataDictionary);
+ }
+
+ [Fact]
+ public void Load_NonNullSession_NoSessionData_ReturnsEmptyDictionary()
+ {
+ // Arrange
+ SessionStateTempDataProvider testProvider = new SessionStateTempDataProvider();
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ Mock<HttpSessionStateBase> mockSessionStateBase = new Mock<HttpSessionStateBase>();
+ mockControllerContext.Setup(c => c.HttpContext.Session).Returns(mockSessionStateBase.Object);
+
+ // Act
+ IDictionary<string, object> result = testProvider.LoadTempData(mockControllerContext.Object);
+
+ // Assert
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void Load_NonNullSession_IncorrectSessionDataType_ReturnsEmptyDictionary()
+ {
+ // Arrange
+ SessionStateTempDataProvider testProvider = new SessionStateTempDataProvider();
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ Mock<HttpSessionStateBase> mockSessionStateBase = new Mock<HttpSessionStateBase>();
+ mockControllerContext.Setup(c => c.HttpContext.Session).Returns(mockSessionStateBase.Object);
+ mockSessionStateBase.Setup(ssb => ssb[SessionStateTempDataProvider.TempDataSessionStateKey]).Returns(42);
+
+ // Act
+ IDictionary<string, object> result = testProvider.LoadTempData(mockControllerContext.Object);
+
+ // Assert
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void Load_NonNullSession_CorrectSessionDataType_ReturnsSessionData()
+ {
+ // Arrange
+ SessionStateTempDataProvider testProvider = new SessionStateTempDataProvider();
+ Dictionary<string, object> tempData = new Dictionary<string, object> { { "foo", "bar" } };
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ Mock<HttpSessionStateBase> mockSessionStateBase = new Mock<HttpSessionStateBase>();
+ mockControllerContext.Setup(c => c.HttpContext.Session).Returns(mockSessionStateBase.Object);
+ mockSessionStateBase.Setup(ssb => ssb[SessionStateTempDataProvider.TempDataSessionStateKey]).Returns(tempData);
+
+ // Act
+ var result = testProvider.LoadTempData(mockControllerContext.Object);
+
+ // Assert
+ Assert.Same(tempData, result);
+ }
+
+ [Fact]
+ public void Save_NullSession_NullDictionary_DoesNotThrow()
+ {
+ // Arrange
+ SessionStateTempDataProvider testProvider = new SessionStateTempDataProvider();
+
+ // Act
+ testProvider.SaveTempData(GetControllerContext(), null);
+ }
+
+ [Fact]
+ public void Save_NullSession_EmptyDictionary_DoesNotThrow()
+ {
+ // Arrange
+ SessionStateTempDataProvider testProvider = new SessionStateTempDataProvider();
+
+ // Act
+ testProvider.SaveTempData(GetControllerContext(), new Dictionary<string, object>());
+ }
+
+ [Fact]
+ public void Save_NullSession_NonEmptyDictionary_Throws()
+ {
+ // Arrange
+ SessionStateTempDataProvider testProvider = new SessionStateTempDataProvider();
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ delegate { testProvider.SaveTempData(GetControllerContext(), new Dictionary<string, object> { { "foo", "bar" } }); },
+ "The SessionStateTempDataProvider class requires session state to be enabled.");
+ }
+
+ [Fact]
+ public void Save_NonNullSession_TempDataIsDirty_AssignsTempDataDictionaryIntoSession()
+ {
+ // Arrange
+ SessionStateTempDataProvider testProvider = new SessionStateTempDataProvider();
+ Dictionary<string, object> tempData = new Dictionary<string, object> { { "foo", "bar" } };
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ Mock<HttpSessionStateBase> mockSessionStateBase = new Mock<HttpSessionStateBase>();
+ mockControllerContext.Setup(c => c.HttpContext.Session).Returns(mockSessionStateBase.Object);
+ mockSessionStateBase.SetupSet(ssb => ssb[SessionStateTempDataProvider.TempDataSessionStateKey] = tempData);
+
+ // Act
+ testProvider.SaveTempData(mockControllerContext.Object, tempData);
+
+ // Assert
+ mockSessionStateBase.VerifyAll();
+ }
+
+ [Fact]
+ public void Save_NonNullSession_TempDataIsNotDirty_KeyDoesNotExistInSession_SessionRemainsUntouched()
+ {
+ // Arrange
+ SessionStateTempDataProvider testProvider = new SessionStateTempDataProvider();
+ Dictionary<string, object> tempData = new Dictionary<string, object>();
+
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>(MockBehavior.Strict);
+ mockControllerContext.Setup(o => o.HttpContext.Session[SessionStateTempDataProvider.TempDataSessionStateKey]).Returns(null);
+
+ // Act
+ testProvider.SaveTempData(mockControllerContext.Object, tempData);
+
+ // Assert
+ mockControllerContext.Verify();
+ }
+
+ [Fact]
+ public void Save_NonNullSession_TempDataIsNotDirty_KeyExistsInSession_KeyRemovedFromSession()
+ {
+ // Arrange
+ SessionStateTempDataProvider testProvider = new SessionStateTempDataProvider();
+ Dictionary<string, object> tempData = new Dictionary<string, object>();
+
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>(MockBehavior.Strict);
+ mockControllerContext.Setup(o => o.HttpContext.Session[SessionStateTempDataProvider.TempDataSessionStateKey]).Returns(new object());
+ mockControllerContext.Setup(o => o.HttpContext.Session.Remove(SessionStateTempDataProvider.TempDataSessionStateKey)).Verifiable();
+
+ // Act
+ testProvider.SaveTempData(mockControllerContext.Object, tempData);
+
+ // Assert
+ mockControllerContext.Verify();
+ }
+
+ private static ControllerContext GetControllerContext()
+ {
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.Setup(c => c.HttpContext.Session).Returns((HttpSessionStateBase)null);
+ return mockControllerContext.Object;
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/SingleServiceResolverTest.cs b/test/System.Web.Mvc.Test/Test/SingleServiceResolverTest.cs
new file mode 100644
index 00000000..6c8b6aed
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/SingleServiceResolverTest.cs
@@ -0,0 +1,163 @@
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class SingleServiceResolverTest
+ {
+ [Fact]
+ public void ConstructorWithNullThunkArgumentThrows()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { new SingleServiceResolver<TestProvider>(null, null, "TestProvider.Current"); },
+ "currentValueThunk");
+
+ Assert.ThrowsArgumentNull(
+ delegate { new SingleServiceResolver<TestProvider>(null, null, "TestProvider.Current"); },
+ "currentValueThunk");
+
+ Assert.ThrowsArgumentNull(
+ delegate { new SingleServiceResolver<TestProvider>(() => null, null, "TestProvider.Current"); },
+ "defaultValue");
+ }
+
+ [Fact]
+ public void CurrentConsultsResolver()
+ {
+ // Arrange
+ TestProvider providerFromDefaultValue = new TestProvider();
+ TestProvider providerFromServiceLocation = new TestProvider();
+
+ Mock<IDependencyResolver> resolver = new Mock<IDependencyResolver>();
+ resolver.Setup(r => r.GetService(typeof(TestProvider)))
+ .Returns(providerFromServiceLocation);
+
+ SingleServiceResolver<TestProvider> singleResolver = new SingleServiceResolver<TestProvider>(() => null, providerFromDefaultValue, resolver.Object, "TestProvider.Current");
+
+ // Act
+ TestProvider returnedProvider = singleResolver.Current;
+
+ // Assert
+ Assert.Equal(providerFromServiceLocation, returnedProvider);
+ }
+
+ [Fact]
+ public void CurrentReturnsCurrentProviderNotDefaultIfSet()
+ {
+ // Arrange
+ TestProvider providerFromDefaultValue = new TestProvider();
+ TestProvider providerFromCurrentValueThunk = null;
+ Mock<IDependencyResolver> resolver = new Mock<IDependencyResolver>();
+ SingleServiceResolver<TestProvider> singleResolver = new SingleServiceResolver<TestProvider>(() => providerFromCurrentValueThunk, providerFromDefaultValue, resolver.Object, "TestProvider.Current");
+
+ // Act
+ providerFromCurrentValueThunk = new TestProvider();
+ TestProvider returnedProvider = singleResolver.Current;
+
+ // Assert
+ Assert.Equal(providerFromCurrentValueThunk, returnedProvider);
+ resolver.Verify(r => r.GetService(typeof(TestProvider)));
+ }
+
+ [Fact]
+ public void CurrentCachesResolverResult()
+ {
+ // Arrange
+ TestProvider providerFromDefaultValue = new TestProvider();
+ TestProvider providerFromServiceLocation = new TestProvider();
+
+ Mock<IDependencyResolver> resolver = new Mock<IDependencyResolver>();
+ resolver.Setup(r => r.GetService(typeof(TestProvider)))
+ .Returns(providerFromServiceLocation);
+
+ SingleServiceResolver<TestProvider> singleResolver = new SingleServiceResolver<TestProvider>(() => null, providerFromDefaultValue, resolver.Object, "TestProvider.Current");
+
+ // Act
+ TestProvider returnedProvider = singleResolver.Current;
+ TestProvider cachedProvider = singleResolver.Current;
+
+ // Assert
+ Assert.Equal(providerFromServiceLocation, returnedProvider);
+ Assert.Equal(providerFromServiceLocation, cachedProvider);
+ resolver.Verify(r => r.GetService(typeof(TestProvider)), Times.Exactly(1));
+ }
+
+ [Fact]
+ public void CurrentDoesNotQueryResolverAfterReceivingNull()
+ {
+ // Arrange
+ TestProvider providerFromDefaultValue = new TestProvider();
+ TestProvider providerFromCurrentValueThunk = new TestProvider();
+ Mock<IDependencyResolver> resolver = new Mock<IDependencyResolver>();
+ SingleServiceResolver<TestProvider> singleResolver = new SingleServiceResolver<TestProvider>(() => providerFromCurrentValueThunk, providerFromDefaultValue, resolver.Object, "TestProvider.Current");
+
+ // Act
+ TestProvider returnedProvider = singleResolver.Current;
+ TestProvider cachedProvider = singleResolver.Current;
+
+ // Assert
+ Assert.Equal(providerFromCurrentValueThunk, returnedProvider);
+ Assert.Equal(providerFromCurrentValueThunk, cachedProvider);
+ resolver.Verify(r => r.GetService(typeof(TestProvider)), Times.Exactly(1));
+ }
+
+ [Fact]
+ public void CurrentReturnsDefaultIfCurrentNotSet()
+ {
+ //Arrange
+ TestProvider providerFromDefaultValue = new TestProvider();
+ Mock<IDependencyResolver> resolver = new Mock<IDependencyResolver>();
+ SingleServiceResolver<TestProvider> singleResolver = new SingleServiceResolver<TestProvider>(() => null, providerFromDefaultValue, resolver.Object, "TestProvider.Current");
+
+ //Act
+ TestProvider returnedProvider = singleResolver.Current;
+
+ // Assert
+ Assert.Equal(returnedProvider, providerFromDefaultValue);
+ resolver.Verify(l => l.GetService(typeof(TestProvider)));
+ }
+
+ [Fact]
+ public void CurrentThrowsIfCurrentSetThroughServiceAndSetter()
+ {
+ // Arrange
+ TestProvider providerFromCurrentValueThunk = new TestProvider();
+ TestProvider providerFromServiceLocation = new TestProvider();
+ TestProvider providerFromDefaultValue = new TestProvider();
+ Mock<IDependencyResolver> resolver = new Mock<IDependencyResolver>();
+
+ resolver.Setup(r => r.GetService(typeof(TestProvider)))
+ .Returns(providerFromServiceLocation);
+
+ SingleServiceResolver<TestProvider> singleResolver = new SingleServiceResolver<TestProvider>(() => providerFromCurrentValueThunk, providerFromDefaultValue, resolver.Object, "TestProvider.Current");
+
+ //Act & assert
+ Assert.Throws<InvalidOperationException>(
+ () => singleResolver.Current,
+ "An instance of TestProvider was found in the resolver as well as a custom registered provider in TestProvider.Current. Please set only one or the other."
+ );
+ }
+
+ [Fact]
+ public void CurrentPropagatesExceptionWhenResolverThrowsNonActivationException()
+ {
+ // Arrange
+ TestProvider providerFromDefaultValue = new TestProvider();
+ Mock<IDependencyResolver> resolver = new Mock<IDependencyResolver>(MockBehavior.Strict);
+ SingleServiceResolver<TestProvider> singleResolver = new SingleServiceResolver<TestProvider>(() => null, providerFromDefaultValue, resolver.Object, "TestProvider.Current");
+
+ // Act & Assert
+ Assert.Throws<MockException>(
+ () => singleResolver.Current,
+ @"IDependencyResolver.GetService(System.Web.Mvc.Test.SingleServiceResolverTest+TestProvider) invocation failed with mock behavior Strict.
+All invocations on the mock must have a corresponding setup."
+ );
+ }
+
+ private class TestProvider
+ {
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/StringLengthAttributeAdapterTest.cs b/test/System.Web.Mvc.Test/Test/StringLengthAttributeAdapterTest.cs
new file mode 100644
index 00000000..027eab5c
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/StringLengthAttributeAdapterTest.cs
@@ -0,0 +1,32 @@
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class StringLengthAttributeAdapterTest
+ {
+ [Fact]
+ public void ClientRulesWithStringLengthAttribute()
+ {
+ // Arrange
+ var metadata = ModelMetadataProviders.Current.GetMetadataForProperty(() => null, typeof(string), "Length");
+ var context = new ControllerContext();
+ var attribute = new StringLengthAttribute(10) { MinimumLength = 3 };
+ var adapter = new StringLengthAttributeAdapter(metadata, context, attribute);
+
+ // Act
+ var rules = adapter.GetClientValidationRules()
+ .OrderBy(r => r.ValidationType)
+ .ToArray();
+
+ // Assert
+ ModelClientValidationRule rule = Assert.Single(rules);
+ Assert.Equal("length", rule.ValidationType);
+ Assert.Equal(2, rule.ValidationParameters.Count);
+ Assert.Equal(3, rule.ValidationParameters["min"]);
+ Assert.Equal(10, rule.ValidationParameters["max"]);
+ Assert.Equal("The field Length must be a string with a minimum length of 3 and a maximum length of 10.", rule.ErrorMessage);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/TempDataDictionaryTest.cs b/test/System.Web.Mvc.Test/Test/TempDataDictionaryTest.cs
new file mode 100644
index 00000000..78a6404c
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/TempDataDictionaryTest.cs
@@ -0,0 +1,340 @@
+using System.Collections;
+using System.Collections.Generic;
+using Moq;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class TempDataDictionaryTest
+ {
+ [Fact]
+ public void CompareIsOrdinalIgnoreCase()
+ {
+ // Arrange
+ TempDataDictionary tempData = new TempDataDictionary();
+ object item = new object();
+
+ // Act
+ tempData["Foo"] = item;
+ object value = tempData["FOO"];
+
+ // Assert
+ Assert.Same(item, value);
+ }
+
+ [Fact]
+ public void EnumeratingDictionaryMarksValuesForDeletion()
+ {
+ // Arrange
+ NullTempDataProvider provider = new NullTempDataProvider();
+ TempDataDictionary tempData = new TempDataDictionary();
+ Mock<ControllerContext> controllerContext = new Mock<ControllerContext>();
+ tempData["Foo"] = "Foo";
+ tempData["Bar"] = "Bar";
+
+ // Act
+ IEnumerator<KeyValuePair<string, object>> enumerator = tempData.GetEnumerator();
+ while (enumerator.MoveNext())
+ {
+ object value = enumerator.Current;
+ }
+ tempData.Save(controllerContext.Object, provider);
+
+ // Assert
+ Assert.False(tempData.ContainsKey("Foo"));
+ Assert.False(tempData.ContainsKey("Bar"));
+ }
+
+ [Fact]
+ public void EnumeratingTempDataAsIEnmerableMarksValuesForDeletion()
+ {
+ // Arrange
+ NullTempDataProvider provider = new NullTempDataProvider();
+ TempDataDictionary tempData = new TempDataDictionary();
+ Mock<ControllerContext> controllerContext = new Mock<ControllerContext>();
+ tempData["Foo"] = "Foo";
+ tempData["Bar"] = "Bar";
+
+ // Act
+ IEnumerator enumerator = ((IEnumerable)tempData).GetEnumerator();
+ while (enumerator.MoveNext())
+ {
+ object value = enumerator.Current;
+ }
+ tempData.Save(controllerContext.Object, provider);
+
+ // Assert
+ Assert.False(tempData.ContainsKey("Foo"));
+ Assert.False(tempData.ContainsKey("Bar"));
+ }
+
+ [Fact]
+ public void KeepRetainsAllKeysWhenSavingDictionary()
+ {
+ // Arrange
+ NullTempDataProvider provider = new NullTempDataProvider();
+ TempDataDictionary tempData = new TempDataDictionary();
+ Mock<ControllerContext> controllerContext = new Mock<ControllerContext>();
+ controllerContext.Setup(c => c.HttpContext.Request).Returns(new Mock<HttpRequestBase>().Object);
+ tempData["Foo"] = "Foo";
+ tempData["Bar"] = "Bar";
+
+ // Act
+ tempData.Keep();
+ tempData.Save(controllerContext.Object, provider);
+
+ // Assert
+ Assert.True(tempData.ContainsKey("Foo"));
+ Assert.True(tempData.ContainsKey("Bar"));
+ }
+
+ [Fact]
+ public void KeepRetainsSpecificKeysWhenSavingDictionary()
+ {
+ // Arrange
+ NullTempDataProvider provider = new NullTempDataProvider();
+ TempDataDictionary tempData = new TempDataDictionary();
+ Mock<ControllerContext> controllerContext = new Mock<ControllerContext>();
+ controllerContext.Setup(c => c.HttpContext.Request).Returns(new Mock<HttpRequestBase>().Object);
+ tempData["Foo"] = "Foo";
+ tempData["Bar"] = "Bar";
+
+ // Act
+ tempData.Keep("Foo");
+ object value = tempData["Bar"];
+ tempData.Save(controllerContext.Object, provider);
+
+ // Assert
+ Assert.True(tempData.ContainsKey("Foo"));
+ Assert.False(tempData.ContainsKey("Bar"));
+ }
+
+ [Fact]
+ public void LoadAndSaveAreCaseInsensitive()
+ {
+ // Arrange
+ Dictionary<string, object> data = new Dictionary<string, object>();
+ data["Foo"] = "Foo";
+ data["Bar"] = "Bar";
+ TestTempDataProvider provider = new TestTempDataProvider(data);
+ Mock<ControllerContext> controllerContext = new Mock<ControllerContext>();
+ TempDataDictionary tempData = new TempDataDictionary();
+
+ // Act
+ tempData.Load(controllerContext.Object, provider);
+ object value = tempData["FOO"];
+ tempData.Save(controllerContext.Object, provider);
+
+ // Assert
+ Assert.False(tempData.ContainsKey("foo"));
+ Assert.True(tempData.ContainsKey("bar"));
+ }
+
+ [Fact]
+ public void PeekDoesNotMarkKeyAsRead()
+ {
+ // Arrange
+ NullTempDataProvider provider = new NullTempDataProvider();
+ TempDataDictionary tempData = new TempDataDictionary();
+ Mock<ControllerContext> controllerContext = new Mock<ControllerContext>();
+ tempData["Bar"] = "barValue";
+
+ // Act
+ object value = tempData.Peek("bar");
+ tempData.Save(controllerContext.Object, provider);
+
+ // Assert
+ Assert.Equal("barValue", value);
+ Assert.True(tempData.ContainsKey("Bar"));
+ }
+
+ [Fact]
+ public void RemovalOfKeysAreCaseInsensitive()
+ {
+ NullTempDataProvider provider = new NullTempDataProvider();
+ TempDataDictionary tempData = new TempDataDictionary();
+ Mock<ControllerContext> controllerContext = new Mock<ControllerContext>();
+ object fooValue;
+ tempData["Foo"] = "Foo";
+ tempData["Bar"] = "Bar";
+
+ // Act
+ tempData.TryGetValue("foo", out fooValue);
+ object barValue = tempData["bar"];
+ tempData.Save(controllerContext.Object, provider);
+
+ // Assert
+ Assert.False(tempData.ContainsKey("Foo"));
+ Assert.False(tempData.ContainsKey("Boo"));
+ }
+
+ [Fact]
+ public void SaveRetainsAllKeys()
+ {
+ // Arrange
+ NullTempDataProvider provider = new NullTempDataProvider();
+ TempDataDictionary tempData = new TempDataDictionary();
+ Mock<ControllerContext> controllerContext = new Mock<ControllerContext>();
+ tempData["Foo"] = "Foo";
+ tempData["Bar"] = "Bar";
+
+ // Act
+ tempData.Save(controllerContext.Object, provider);
+
+ // Assert
+ Assert.True(tempData.ContainsKey("Foo"));
+ Assert.True(tempData.ContainsKey("Bar"));
+ }
+
+ [Fact]
+ public void SaveRemovesKeysThatWereRead()
+ {
+ // Arrange
+ NullTempDataProvider provider = new NullTempDataProvider();
+ TempDataDictionary tempData = new TempDataDictionary();
+ Mock<ControllerContext> controllerContext = new Mock<ControllerContext>();
+ tempData["Foo"] = "Foo";
+ tempData["Bar"] = "Bar";
+
+ // Act
+ object value = tempData["Foo"];
+ tempData.Save(controllerContext.Object, provider);
+
+ // Assert
+ Assert.False(tempData.ContainsKey("Foo"));
+ Assert.True(tempData.ContainsKey("Bar"));
+ }
+
+ [Fact]
+ public void TempDataIsADictionary()
+ {
+ // Arrange
+ TempDataDictionary tempData = new TempDataDictionary();
+
+ // Act
+ tempData["Key1"] = "Value1";
+ tempData.Add("Key2", "Value2");
+ ((ICollection<KeyValuePair<string, object>>)tempData).Add(new KeyValuePair<string, object>("Key3", "Value3"));
+
+ // Assert (IDictionary)
+ Assert.Equal(3, tempData.Count);
+ Assert.True(tempData.Remove("Key1"));
+ Assert.False(tempData.Remove("Key4"));
+ Assert.True(tempData.ContainsValue("Value2"));
+ Assert.False(tempData.ContainsValue("Value1"));
+ Assert.Null(tempData["Key6"]);
+
+ IEnumerator tempDataEnumerator = tempData.GetEnumerator();
+ tempDataEnumerator.Reset();
+ while (tempDataEnumerator.MoveNext())
+ {
+ KeyValuePair<string, object> pair = (KeyValuePair<string, object>)tempDataEnumerator.Current;
+ Assert.True(((ICollection<KeyValuePair<string, object>>)tempData).Contains(pair));
+ }
+
+ // Assert (ICollection)
+ foreach (string key in tempData.Keys)
+ {
+ Assert.True(((ICollection<KeyValuePair<string, object>>)tempData).Contains(new KeyValuePair<string, object>(key, tempData[key])));
+ }
+
+ foreach (string value in tempData.Values)
+ {
+ Assert.True(tempData.ContainsValue(value));
+ }
+
+ foreach (string key in ((IDictionary<string, object>)tempData).Keys)
+ {
+ Assert.True(tempData.ContainsKey(key));
+ }
+
+ foreach (string value in ((IDictionary<string, object>)tempData).Values)
+ {
+ Assert.True(tempData.ContainsValue(value));
+ }
+
+ KeyValuePair<string, object>[] keyValuePairArray = new KeyValuePair<string, object>[tempData.Count];
+ ((ICollection<KeyValuePair<string, object>>)tempData).CopyTo(keyValuePairArray, 0);
+
+ Assert.False(((ICollection<KeyValuePair<string, object>>)tempData).IsReadOnly);
+
+ Assert.False(((ICollection<KeyValuePair<string, object>>)tempData).Remove(new KeyValuePair<string, object>("Key5", "Value5")));
+
+ IEnumerator<KeyValuePair<string, object>> keyValuePairEnumerator = ((ICollection<KeyValuePair<string, object>>)tempData).GetEnumerator();
+ keyValuePairEnumerator.Reset();
+ while (keyValuePairEnumerator.MoveNext())
+ {
+ KeyValuePair<string, object> pair = keyValuePairEnumerator.Current;
+ Assert.True(((ICollection<KeyValuePair<string, object>>)tempData).Contains(pair));
+ }
+
+ // Act
+ tempData.Clear();
+
+ // Assert
+ Assert.Empty(tempData);
+ }
+
+ [Fact]
+ public void TempDataDictionaryCreatesEmptyDictionaryIfProviderReturnsNull()
+ {
+ // Arrange
+ TempDataDictionary tempDataDictionary = new TempDataDictionary();
+ NullTempDataProvider provider = new NullTempDataProvider();
+
+ // Act
+ tempDataDictionary.Load(null /* controllerContext */, provider);
+
+ // Assert
+ Assert.Empty(tempDataDictionary);
+ }
+
+ [Fact]
+ public void TryGetValueMarksKeyForDeletion()
+ {
+ NullTempDataProvider provider = new NullTempDataProvider();
+ TempDataDictionary tempData = new TempDataDictionary();
+ Mock<ControllerContext> controllerContext = new Mock<ControllerContext>();
+ object value;
+ tempData["Foo"] = "Foo";
+
+ // Act
+ tempData.TryGetValue("Foo", out value);
+ tempData.Save(controllerContext.Object, provider);
+
+ // Assert
+ Assert.False(tempData.ContainsKey("Foo"));
+ }
+
+ internal class NullTempDataProvider : ITempDataProvider
+ {
+ public void SaveTempData(ControllerContext controllerContext, IDictionary<string, object> values)
+ {
+ }
+
+ public IDictionary<string, object> LoadTempData(ControllerContext controllerContext)
+ {
+ return null;
+ }
+ }
+
+ internal class TestTempDataProvider : ITempDataProvider
+ {
+ private IDictionary<string, object> _data;
+
+ public TestTempDataProvider(IDictionary<string, object> data)
+ {
+ _data = data;
+ }
+
+ public void SaveTempData(ControllerContext controllerContext, IDictionary<string, object> values)
+ {
+ }
+
+ public IDictionary<string, object> LoadTempData(ControllerContext controllerContext)
+ {
+ return _data;
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/TypeCacheSerializerTest.cs b/test/System.Web.Mvc.Test/Test/TypeCacheSerializerTest.cs
new file mode 100644
index 00000000..d7447021
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/TypeCacheSerializerTest.cs
@@ -0,0 +1,165 @@
+using System.Collections.Generic;
+using System.IO;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class TypeCacheSerializerTest
+ {
+ private const string _expectedDeserializationFormat = @"<?xml version=""1.0"" encoding=""utf-16""?>
+<!--This file is automatically generated. Please do not modify the contents of this file.-->
+<typeCache lastModified=""__IGNORED__"" mvcVersionId=""{0}"">
+ <assembly name=""{1}"">
+ <module versionId=""{2}"">
+ <type>System.String</type>
+ <type>System.Object</type>
+ </module>
+ </assembly>
+</typeCache>";
+
+ private static readonly string _mscorlibAsmFullName = typeof(object).Assembly.FullName;
+
+ [Fact]
+ public void DeserializeTypes_ReturnsNullIfModuleVersionIdDoesNotMatch()
+ {
+ // Arrange
+ string expected = String.Format(_expectedDeserializationFormat,
+ GetMvidForType(typeof(TypeCacheSerializer)) /* mvcVersionId */,
+ _mscorlibAsmFullName /* assembly.name */,
+ Guid.Empty /* module.versionId */
+ );
+
+ TypeCacheSerializer serializer = new TypeCacheSerializer();
+ StringReader input = new StringReader(expected);
+
+ // Act
+ List<Type> deserializedTypes = serializer.DeserializeTypes(input);
+
+ // Assert
+ Assert.Null(deserializedTypes);
+ }
+
+ [Fact]
+ public void DeserializeTypes_ReturnsNullIfMvcVersionIdDoesNotMatch()
+ {
+ // Arrange
+ string expected = String.Format(_expectedDeserializationFormat,
+ Guid.Empty /* mvcVersionId */,
+ _mscorlibAsmFullName /* assembly.name */,
+ GetMvidForType(typeof(object)) /* module.versionId */
+ );
+
+ TypeCacheSerializer serializer = new TypeCacheSerializer();
+ StringReader input = new StringReader(expected);
+
+ // Act
+ List<Type> deserializedTypes = serializer.DeserializeTypes(input);
+
+ // Assert
+ Assert.Null(deserializedTypes);
+ }
+
+ [Fact]
+ public void DeserializeTypes_ReturnsNullIfTypeNotFound()
+ {
+ string expectedFormat = @"<?xml version=""1.0"" encoding=""utf-16""?>
+<!--This file is automatically generated. Please do not modify the contents of this file.-->
+<typeCache lastModified=""__IGNORED__"" mvcVersionId=""{0}"">
+ <assembly name=""{1}"">
+ <module versionId=""{2}"">
+ <type>System.String</type>
+ <type>This.Type.Does.Not.Exist</type>
+ </module>
+ </assembly>
+</typeCache>";
+
+ // Arrange
+ string expected = String.Format(expectedFormat,
+ GetMvidForType(typeof(TypeCacheSerializer)) /* mvcVersionId */,
+ _mscorlibAsmFullName /* assembly.name */,
+ GetMvidForType(typeof(object)) /* module.versionId */
+ );
+
+ TypeCacheSerializer serializer = new TypeCacheSerializer();
+ StringReader input = new StringReader(expected);
+
+ // Act
+ List<Type> deserializedTypes = serializer.DeserializeTypes(input);
+
+ // Assert
+ Assert.Null(deserializedTypes);
+ }
+
+ [Fact]
+ public void DeserializeTypes_Success()
+ {
+ // Arrange
+ string expected = String.Format(_expectedDeserializationFormat,
+ GetMvidForType(typeof(TypeCacheSerializer)) /* mvcVersionId */,
+ _mscorlibAsmFullName /* assembly.name */,
+ GetMvidForType(typeof(object)) /* module.versionId */
+ );
+
+ TypeCacheSerializer serializer = new TypeCacheSerializer();
+ StringReader input = new StringReader(expected);
+
+ Type[] expectedTypes = new Type[]
+ {
+ typeof(string),
+ typeof(object)
+ };
+
+ // Act
+ List<Type> deserializedTypes = serializer.DeserializeTypes(input);
+
+ // Assert
+ Assert.Equal(expectedTypes, deserializedTypes.ToArray());
+ }
+
+ [Fact]
+ public void SerializeTypes()
+ {
+ // Arrange
+ DateTime expectedDate = new DateTime(2001, 1, 1, 0, 0, 0, DateTimeKind.Utc); // Jan 1, 2001 midnight UTC
+ string expectedFormat = @"<?xml version=""1.0"" encoding=""utf-16""?>
+<!--This file is automatically generated. Please do not modify the contents of this file.-->
+<typeCache lastModified=""{0}"" mvcVersionId=""{1}"">
+ <assembly name=""{2}"">
+ <module versionId=""{3}"">
+ <type>System.String</type>
+ <type>System.Object</type>
+ </module>
+ </assembly>
+</typeCache>";
+ string expected = String.Format(expectedFormat,
+ expectedDate /* lastModified */,
+ GetMvidForType(typeof(TypeCacheSerializer)) /* mvcVersionId */,
+ _mscorlibAsmFullName /* assembly.name */,
+ GetMvidForType(typeof(object)) /* module.versionId */
+ );
+
+ Type[] typesToSerialize = new Type[]
+ {
+ typeof(string),
+ typeof(object)
+ };
+
+ TypeCacheSerializer serializer = new TypeCacheSerializer();
+ serializer.CurrentDateOverride = expectedDate;
+
+ StringWriter output = new StringWriter();
+
+ // Act
+ serializer.SerializeTypes(typesToSerialize, output);
+ string outputString = output.ToString();
+
+ // Assert
+ Assert.Equal(expected, outputString);
+ }
+
+ private static Guid GetMvidForType(Type type)
+ {
+ return type.Module.ModuleVersionId;
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/TypeCacheUtilTest.cs b/test/System.Web.Mvc.Test/Test/TypeCacheUtilTest.cs
new file mode 100644
index 00000000..7d60f20b
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/TypeCacheUtilTest.cs
@@ -0,0 +1,149 @@
+using System.Collections.Generic;
+using System.IO;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class TypeCacheUtilTest
+ {
+ [Fact]
+ public void GetFilteredTypesFromAssemblies_FallThrough()
+ {
+ // Arrange
+ Type[] expectedTypes = new Type[]
+ {
+ typeof(TypeCacheValidFoo),
+ typeof(TypeCacheValidBar)
+ };
+
+ string cacheName = "testCache";
+ MockBuildManager buildManager = new MockBuildManager();
+ Predicate<Type> predicate = type => type.IsDefined(typeof(TypeCacheMarkerAttribute), true);
+
+ // Act
+ List<Type> returnedTypes = TypeCacheUtil.GetFilteredTypesFromAssemblies(cacheName, predicate, buildManager);
+
+ // Assert
+ Assert.Equal(expectedTypes, returnedTypes.ToArray());
+
+ MemoryStream cachedStream = buildManager.CachedFileStore[cacheName] as MemoryStream;
+ Assert.NotNull(cachedStream);
+ Assert.NotEqual(0, cachedStream.ToArray().Length);
+ }
+
+ [Fact]
+ public void SaveToCache_ReadFromCache_ReturnsNullIfTypesAreInvalid()
+ {
+ //
+ // SAVING
+ //
+
+ // Arrange
+ Type[] expectedTypes = new Type[]
+ {
+ typeof(object),
+ typeof(string)
+ };
+
+ TypeCacheSerializer serializer = new TypeCacheSerializer();
+ string cacheName = "testCache";
+ MockBuildManager buildManager = new MockBuildManager();
+
+ // Act
+ TypeCacheUtil.SaveTypesToCache(cacheName, expectedTypes, buildManager, serializer);
+
+ // Assert
+ MemoryStream writeStream = buildManager.CachedFileStore[cacheName] as MemoryStream;
+ Assert.NotNull(writeStream);
+
+ byte[] streamContents = writeStream.ToArray();
+ Assert.NotEqual(0, streamContents.Length);
+
+ //
+ // READING
+ //
+
+ // Arrange
+ MemoryStream readStream = new MemoryStream(streamContents);
+ buildManager.CachedFileStore[cacheName] = readStream;
+
+ // Act
+ List<Type> returnedTypes = TypeCacheUtil.ReadTypesFromCache(cacheName, _ => false /* all types are invalid */, buildManager, serializer);
+
+ // Assert
+ Assert.Null(returnedTypes);
+ }
+
+ [Fact]
+ public void SaveToCache_ReadFromCache_Success()
+ {
+ //
+ // SAVING
+ //
+
+ // Arrange
+ Type[] expectedTypes = new Type[]
+ {
+ typeof(object),
+ typeof(string)
+ };
+
+ TypeCacheSerializer serializer = new TypeCacheSerializer();
+ string cacheName = "testCache";
+ MockBuildManager buildManager = new MockBuildManager();
+
+ // Act
+ TypeCacheUtil.SaveTypesToCache(cacheName, expectedTypes, buildManager, serializer);
+
+ // Assert
+ MemoryStream writeStream = buildManager.CachedFileStore[cacheName] as MemoryStream;
+ Assert.NotNull(writeStream);
+
+ byte[] streamContents = writeStream.ToArray();
+ Assert.NotEqual(0, streamContents.Length);
+
+ //
+ // READING
+ //
+
+ // Arrange
+ MemoryStream readStream = new MemoryStream(streamContents);
+ buildManager.CachedFileStore[cacheName] = readStream;
+
+ // Act
+ List<Type> returnedTypes = TypeCacheUtil.ReadTypesFromCache(cacheName, _ => true /* all types are valid */, buildManager, serializer);
+
+ // Assert
+ Assert.Equal(expectedTypes, returnedTypes.ToArray());
+ }
+ }
+
+ public class TypeCacheMarkerAttribute : Attribute
+ {
+ }
+
+ [TypeCacheMarker]
+ public class TypeCacheValidFoo
+ {
+ }
+
+ [TypeCacheMarker]
+ public class TypeCacheValidBar
+ {
+ }
+
+ [TypeCacheMarker]
+ internal class TypeCacheInvalidInternal
+ {
+ }
+
+ [TypeCacheMarker]
+ public abstract class TypeCacheInvalidAbstract
+ {
+ }
+
+ [TypeCacheMarker]
+ public struct TypeCacheInvalidStruct
+ {
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/TypeHelpersTest.cs b/test/System.Web.Mvc.Test/Test/TypeHelpersTest.cs
new file mode 100644
index 00000000..9815c9d5
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/TypeHelpersTest.cs
@@ -0,0 +1,242 @@
+using System.Collections;
+using System.Collections.Generic;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class TypeHelpersTest
+ {
+ [Fact]
+ public void CreateDelegateBindsInstanceMethod()
+ {
+ // Act
+ string s = "Hello, world!";
+ Func<string, bool> endsWith = TypeHelpers.CreateDelegate<Func<string, bool>>(TypeHelpers.MsCorLibAssembly, "System.String", "EndsWith", s);
+
+ // Assert
+ Assert.NotNull(endsWith);
+ Assert.True(endsWith("world!"));
+ }
+
+ [Fact]
+ public void CreateDelegateBindsStaticMethod()
+ {
+ // Act
+ Func<object, object, string> concat = TypeHelpers.CreateDelegate<Func<object, object, string>>(TypeHelpers.MsCorLibAssembly, "System.String", "Concat", null);
+
+ // Assert
+ Assert.NotNull(concat);
+ Assert.Equal("45", concat(4, 5));
+ }
+
+ [Fact]
+ public void CreateDelegateReturnsNullIfTypeDoesNotExist()
+ {
+ // Act
+ Action d = TypeHelpers.CreateDelegate<Action>(TypeHelpers.MsCorLibAssembly, "System.xyz.TypeDoesNotExist", "SomeMethod", null);
+
+ // Assert
+ Assert.Null(d);
+ }
+
+ [Fact]
+ public void CreateDelegateReturnsNullIfMethodDoesNotExist()
+ {
+ // Act
+ Action d = TypeHelpers.CreateDelegate<Action>(TypeHelpers.MsCorLibAssembly, "System.String", "MethodDoesNotExist", null);
+
+ // Assert
+ Assert.Null(d);
+ }
+
+ [Fact]
+ public void CreateTryGetValueDelegateReturnsNullForNonDictionaries()
+ {
+ // Arrange
+ object notDictionary = "Hello, world";
+
+ // Act
+ TryGetValueDelegate d = TypeHelpers.CreateTryGetValueDelegate(notDictionary.GetType());
+
+ // Assert
+ Assert.Null(d);
+ }
+
+ [Fact]
+ public void CreateTryGetValueDelegateWrapsGenericObjectDictionaries()
+ {
+ // Arrange
+ object dictionary = new Dictionary<object, int>()
+ {
+ { "theKey", 42 }
+ };
+
+ // Act
+ TryGetValueDelegate d = TypeHelpers.CreateTryGetValueDelegate(dictionary.GetType());
+
+ object value;
+ bool found = d(dictionary, "theKey", out value);
+
+ // Assert
+ Assert.True(found);
+ Assert.Equal(42, value);
+ }
+
+ [Fact]
+ public void CreateTryGetValueDelegateWrapsGenericStringDictionaries()
+ {
+ // Arrange
+ object dictionary = new Dictionary<string, int>()
+ {
+ { "theKey", 42 }
+ };
+
+ // Act
+ TryGetValueDelegate d = TypeHelpers.CreateTryGetValueDelegate(dictionary.GetType());
+
+ object value;
+ bool found = d(dictionary, "theKey", out value);
+
+ // Assert
+ Assert.True(found);
+ Assert.Equal(42, value);
+ }
+
+ [Fact]
+ public void CreateTryGetValueDelegateWrapsNonGenericDictionaries()
+ {
+ // Arrange
+ object dictionary = new Hashtable()
+ {
+ { "foo", 42 }
+ };
+
+ // Act
+ TryGetValueDelegate d = TypeHelpers.CreateTryGetValueDelegate(dictionary.GetType());
+
+ object fooValue;
+ bool fooFound = d(dictionary, "foo", out fooValue);
+
+ object barValue;
+ bool barFound = d(dictionary, "bar", out barValue);
+
+ // Assert
+ Assert.True(fooFound);
+ Assert.Equal(42, fooValue);
+ Assert.False(barFound);
+ Assert.Null(barValue);
+ }
+
+ [Fact]
+ public void GetDefaultValue_NullableValueType()
+ {
+ // Act
+ object defaultValue = TypeHelpers.GetDefaultValue(typeof(int?));
+
+ // Assert
+ Assert.Equal(default(int?), defaultValue);
+ }
+
+ [Fact]
+ public void GetDefaultValue_ReferenceType()
+ {
+ // Act
+ object defaultValue = TypeHelpers.GetDefaultValue(typeof(object));
+
+ // Assert
+ Assert.Equal(default(object), defaultValue);
+ }
+
+ [Fact]
+ public void GetDefaultValue_ValueType()
+ {
+ // Act
+ object defaultValue = TypeHelpers.GetDefaultValue(typeof(int));
+
+ // Assert
+ Assert.Equal(default(int), defaultValue);
+ }
+
+ [Fact]
+ public void IsCompatibleObjectReturnsTrueIfTypeIsNotNullableAndValueIsNull()
+ {
+ // Act
+ bool retVal = TypeHelpers.IsCompatibleObject<int>(null);
+
+ // Assert
+ Assert.False(retVal);
+ }
+
+ [Fact]
+ public void IsCompatibleObjectReturnsFalseIfValueIsIncorrectType()
+ {
+ // Arrange
+ object value = new string[] { "Hello", "world" };
+
+ // Act
+ bool retVal = TypeHelpers.IsCompatibleObject<int>(value);
+
+ // Assert
+ Assert.False(retVal);
+ }
+
+ [Fact]
+ public void IsCompatibleObjectReturnsTrueIfTypeIsNullableAndValueIsNull()
+ {
+ // Act
+ bool retVal = TypeHelpers.IsCompatibleObject<int?>(null);
+
+ // Assert
+ Assert.True(retVal);
+ }
+
+ [Fact]
+ public void IsCompatibleObjectReturnsTrueIfValueIsOfCorrectType()
+ {
+ // Arrange
+ object value = new string[] { "Hello", "world" };
+
+ // Act
+ bool retVal = TypeHelpers.IsCompatibleObject<IEnumerable<string>>(value);
+
+ // Assert
+ Assert.True(retVal);
+ }
+
+ [Fact]
+ public void TypeAllowsNullValueReturnsFalseForNonNullableGenericValueType()
+ {
+ Assert.False(TypeHelpers.TypeAllowsNullValue(typeof(KeyValuePair<int, string>)));
+ }
+
+ [Fact]
+ public void TypeAllowsNullValueReturnsFalseForNonNullableGenericValueTypeDefinition()
+ {
+ Assert.False(TypeHelpers.TypeAllowsNullValue(typeof(KeyValuePair<,>)));
+ }
+
+ [Fact]
+ public void TypeAllowsNullValueReturnsFalseForNonNullableValueType()
+ {
+ Assert.False(TypeHelpers.TypeAllowsNullValue(typeof(int)));
+ }
+
+ [Fact]
+ public void TypeAllowsNullValueReturnsTrueForInterfaceType()
+ {
+ Assert.True(TypeHelpers.TypeAllowsNullValue(typeof(IDisposable)));
+ }
+
+ [Fact]
+ public void TypeAllowsNullValueReturnsTrueForNullableType()
+ {
+ Assert.True(TypeHelpers.TypeAllowsNullValue(typeof(int?)));
+ }
+
+ [Fact]
+ public void TypeAllowsNullValueReturnsTrueForReferenceType()
+ {
+ Assert.True(TypeHelpers.TypeAllowsNullValue(typeof(object)));
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/UrlHelperTest.cs b/test/System.Web.Mvc.Test/Test/UrlHelperTest.cs
new file mode 100644
index 00000000..b93e1e70
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/UrlHelperTest.cs
@@ -0,0 +1,694 @@
+using System.Web.Routing;
+using Microsoft.Web.UnitTestUtil;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class UrlHelperTest
+ {
+ [Fact]
+ public void IsLocalUrl_RejectsNull()
+ {
+ UrlHelper helper = GetUrlHelperForIsLocalUrl();
+
+ Assert.False(helper.IsLocalUrl(null));
+ }
+
+ [Fact]
+ public void IsLocalUrl_RejectsEmptyString()
+ {
+ UrlHelper helper = GetUrlHelperForIsLocalUrl();
+
+ Assert.False(helper.IsLocalUrl(String.Empty));
+ Assert.False(helper.IsLocalUrl(" "));
+ }
+
+ [Fact]
+ public void IsLocalUrl_AcceptsRootedUrls()
+ {
+ UrlHelper helper = GetUrlHelperForIsLocalUrl();
+ Assert.True(helper.IsLocalUrl("/fooo"));
+ Assert.True(helper.IsLocalUrl("/www.hackerz.com"));
+ Assert.True(helper.IsLocalUrl("/"));
+ }
+
+ [Fact]
+ public void IsLocalUrl_AcceptsAppRelativeUrls()
+ {
+ UrlHelper helper = GetUrlHelperForIsLocalUrl();
+ Assert.True(helper.IsLocalUrl("~/"));
+ Assert.True(helper.IsLocalUrl("~/foobar.html"));
+ }
+
+ [Fact]
+ public void IsLocalUrl_RejectsRelativeUrls()
+ {
+ UrlHelper helper = GetUrlHelperForIsLocalUrl();
+ Assert.False(helper.IsLocalUrl("foobar.html"));
+ Assert.False(helper.IsLocalUrl("../foobar.html"));
+ Assert.False(helper.IsLocalUrl("fold/foobar.html"));
+ }
+
+ [Fact]
+ public void IsLocalUrl_RejectValidButUnsafeRelativeUrls()
+ {
+ UrlHelper helper = GetUrlHelperForIsLocalUrl();
+
+ Assert.False(helper.IsLocalUrl("http:/foobar.html"));
+ Assert.False(helper.IsLocalUrl("hTtP:foobar.html"));
+ Assert.False(helper.IsLocalUrl("http:/www.hackerz.com"));
+ Assert.False(helper.IsLocalUrl("HtTpS:/www.hackerz.com"));
+ }
+
+ [Fact]
+ public void IsLocalUrl_RejectsUrlsOnTheSameHost()
+ {
+ UrlHelper helper = GetUrlHelperForIsLocalUrl();
+
+ Assert.False(helper.IsLocalUrl("http://www.mysite.com/appDir/foobar.html"));
+ Assert.False(helper.IsLocalUrl("http://WWW.MYSITE.COM"));
+ }
+
+ [Fact]
+ public void IsLocalUrl_RejectsUrlsOnLocalHost()
+ {
+ UrlHelper helper = GetUrlHelperForIsLocalUrl();
+
+ Assert.False(helper.IsLocalUrl("http://localhost/foobar.html"));
+ Assert.False(helper.IsLocalUrl("http://127.0.0.1/foobar.html"));
+ }
+
+ [Fact]
+ public void IsLocalUrl_RejectsUrlsOnTheSameHostButDifferentScheme()
+ {
+ UrlHelper helper = GetUrlHelperForIsLocalUrl();
+
+ Assert.False(helper.IsLocalUrl("https://www.mysite.com/"));
+ }
+
+ [Fact]
+ public void IsLocalUrl_RejectsUrlsOnDifferentHost()
+ {
+ UrlHelper helper = GetUrlHelperForIsLocalUrl();
+
+ Assert.False(helper.IsLocalUrl("http://www.hackerz.com"));
+ Assert.False(helper.IsLocalUrl("https://www.hackerz.com"));
+ Assert.False(helper.IsLocalUrl("hTtP://www.hackerz.com"));
+ Assert.False(helper.IsLocalUrl("HtTpS://www.hackerz.com"));
+ }
+
+ [Fact]
+ public void IsLocalUrl_RejectsUrlsWithTooManySchemeSeparatorCharacters()
+ {
+ UrlHelper helper = GetUrlHelperForIsLocalUrl();
+
+ Assert.False(helper.IsLocalUrl("http://///www.hackerz.com/foobar.html"));
+ Assert.False(helper.IsLocalUrl("https://///www.hackerz.com/foobar.html"));
+ Assert.False(helper.IsLocalUrl("HtTpS://///www.hackerz.com/foobar.html"));
+
+ Assert.False(helper.IsLocalUrl("http:///www.hackerz.com/foobar.html"));
+ Assert.False(helper.IsLocalUrl("http:////www.hackerz.com/foobar.html"));
+ Assert.False(helper.IsLocalUrl("http://///www.hackerz.com/foobar.html"));
+ }
+
+ [Fact]
+ public void IsLocalUrl_RejectsUrlsWithMissingSchemeName()
+ {
+ UrlHelper helper = GetUrlHelperForIsLocalUrl();
+
+ Assert.False(helper.IsLocalUrl("//www.hackerz.com"));
+ Assert.False(helper.IsLocalUrl("//www.hackerz.com/foobar.html"));
+ Assert.False(helper.IsLocalUrl("///www.hackerz.com"));
+ Assert.False(helper.IsLocalUrl("//////www.hackerz.com"));
+ }
+
+ [Fact]
+ public void IsLocalUrl_RejectsInvalidUrls()
+ {
+ UrlHelper helper = GetUrlHelperForIsLocalUrl();
+
+ Assert.False(helper.IsLocalUrl(@"http:\\www.hackerz.com"));
+ Assert.False(helper.IsLocalUrl(@"http:\\www.hackerz.com\"));
+ }
+
+ [Fact]
+ public void RequestContextProperty()
+ {
+ // Arrange
+ RequestContext requestContext = new RequestContext(new Mock<HttpContextBase>().Object, new RouteData());
+ UrlHelper urlHelper = new UrlHelper(requestContext);
+
+ // Assert
+ Assert.Equal(requestContext, urlHelper.RequestContext);
+ }
+
+ [Fact]
+ public void ConstructorWithNullRequestContextThrows()
+ {
+ // Assert
+ Assert.ThrowsArgumentNull(
+ delegate { new UrlHelper(null); },
+ "requestContext");
+ }
+
+ [Fact]
+ public void ConstructorWithNullRouteCollectionThrows()
+ {
+ // Assert
+ Assert.ThrowsArgumentNull(
+ delegate { new UrlHelper(GetRequestContext(), null); },
+ "routeCollection");
+ }
+
+ [Fact]
+ public void Action()
+ {
+ // Arrange
+ UrlHelper urlHelper = GetUrlHelper();
+
+ // Act
+ string url = urlHelper.Action("newaction");
+
+ // Assert
+ Assert.Equal(MvcHelper.AppPathModifier + "/app/home/newaction", url);
+ }
+
+ [Fact]
+ public void ActionWithControllerName()
+ {
+ // Arrange
+ UrlHelper urlHelper = GetUrlHelper();
+
+ // Act
+ string url = urlHelper.Action("newaction", "home2");
+
+ // Assert
+ Assert.Equal(MvcHelper.AppPathModifier + "/app/home2/newaction", url);
+ }
+
+ [Fact]
+ public void ActionWithControllerNameAndDictionary()
+ {
+ // Arrange
+ UrlHelper urlHelper = GetUrlHelper();
+
+ // Act
+ string url = urlHelper.Action("newaction", "home2", new RouteValueDictionary(new { id = "someid" }));
+
+ // Assert
+ Assert.Equal(MvcHelper.AppPathModifier + "/app/home2/newaction/someid", url);
+ }
+
+ [Fact]
+ public void ActionWithControllerNameAndObjectProperties()
+ {
+ // Arrange
+ UrlHelper urlHelper = GetUrlHelper();
+
+ // Act
+ string url = urlHelper.Action("newaction", "home2", new { id = "someid" });
+
+ // Assert
+ Assert.Equal(MvcHelper.AppPathModifier + "/app/home2/newaction/someid", url);
+ }
+
+ [Fact]
+ public void ActionWithDictionary()
+ {
+ // Arrange
+ UrlHelper urlHelper = GetUrlHelper();
+
+ // Act
+ string url = urlHelper.Action("newaction", new RouteValueDictionary(new { Controller = "home2", id = "someid" }));
+
+ // Assert
+ Assert.Equal(MvcHelper.AppPathModifier + "/app/home2/newaction/someid", url);
+ }
+
+ [Fact]
+ public void ActionWithNullActionName()
+ {
+ // Arrange
+ UrlHelper urlHelper = GetUrlHelper();
+
+ // Act
+ string url = urlHelper.Action(null);
+
+ // Assert
+ Assert.Equal(MvcHelper.AppPathModifier + "/app/home/oldaction", url);
+ }
+
+ [Fact]
+ public void ActionWithNullProtocol()
+ {
+ // Arrange
+ UrlHelper urlHelper = GetUrlHelper();
+
+ // Act
+ string url = urlHelper.Action("newaction", "home2", new { id = "someid" }, null /* protocol */);
+
+ // Assert
+ Assert.Equal(MvcHelper.AppPathModifier + "/app/home2/newaction/someid", url);
+ }
+
+ [Fact]
+ public void ActionParameterOverridesObjectProperties()
+ {
+ // Arrange
+ UrlHelper urlHelper = GetUrlHelper();
+
+ // Act
+ string url = urlHelper.Action("newaction", new { Action = "action" });
+
+ // Assert
+ Assert.Equal(MvcHelper.AppPathModifier + "/app/home/newaction", url);
+ }
+
+ [Fact]
+ public void ActionWithObjectProperties()
+ {
+ // Arrange
+ UrlHelper urlHelper = GetUrlHelper();
+
+ // Act
+ string url = urlHelper.Action("newaction", new { Controller = "home2", id = "someid" });
+
+ // Assert
+ Assert.Equal(MvcHelper.AppPathModifier + "/app/home2/newaction/someid", url);
+ }
+
+ [Fact]
+ public void ActionWithProtocol()
+ {
+ // Arrange
+ UrlHelper urlHelper = GetUrlHelper();
+
+ // Act
+ string url = urlHelper.Action("newaction", "home2", new { id = "someid" }, "https");
+
+ // Assert
+ Assert.Equal("https://localhost" + MvcHelper.AppPathModifier + "/app/home2/newaction/someid", url);
+ }
+
+ [Fact]
+ public void ContentWithAbsolutePath()
+ {
+ // Arrange
+ UrlHelper urlHelper = GetUrlHelper();
+
+ // Act
+ string url = urlHelper.Content("/Content/Image.jpg");
+
+ // Assert
+ Assert.Equal("/Content/Image.jpg", url);
+ }
+
+ [Fact]
+ public void ContentWithAppRelativePath()
+ {
+ // Arrange
+ UrlHelper urlHelper = GetUrlHelper();
+
+ // Act
+ string url = urlHelper.Content("~/Content/Image.jpg");
+
+ // Assert
+ Assert.Equal(MvcHelper.AppPathModifier + "/app/Content/Image.jpg", url);
+ }
+
+ [Fact]
+ public void ContentWithEmptyPathThrows()
+ {
+ // Arrange
+ UrlHelper urlHelper = GetUrlHelper();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate() { urlHelper.Content(String.Empty); },
+ "contentPath");
+ }
+
+ [Fact]
+ public void ContentWithRelativePath()
+ {
+ // Arrange
+ UrlHelper urlHelper = GetUrlHelper();
+
+ // Act
+ string url = urlHelper.Content("Content/Image.jpg");
+
+ // Assert
+ Assert.Equal("Content/Image.jpg", url);
+ }
+
+ [Fact]
+ public void ContentWithExternalUrl()
+ {
+ // Arrange
+ UrlHelper urlHelper = GetUrlHelper();
+
+ // Act
+ string url = urlHelper.Content("http://www.asp.net/App_Themes/Standard/i/logo.png");
+
+ // Assert
+ Assert.Equal("http://www.asp.net/App_Themes/Standard/i/logo.png", url);
+ }
+
+ [Fact]
+ public void Encode()
+ {
+ // Arrange
+ UrlHelper urlHelper = GetUrlHelper();
+
+ // Act
+ string encodedUrl = urlHelper.Encode(@"SomeUrl /+\");
+
+ // Assert
+ Assert.Equal(encodedUrl, "SomeUrl+%2f%2b%5c");
+ }
+
+ [Fact]
+ public void GenerateContentUrlWithNullContentPathThrows()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate() { UrlHelper.GenerateContentUrl(null, null); },
+ "contentPath");
+ }
+
+ [Fact]
+ public void GenerateContentUrlWithNullContextThrows()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate() { UrlHelper.GenerateContentUrl("Content/foo.png", null); },
+ "httpContext");
+ }
+
+ [Fact]
+ public void GenerateUrlWithNullRequestContextThrows()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate() { UrlHelper.GenerateUrl(null /* routeName */, null /* actionName */, null /* controllerName */, null /* routeValues */, new RouteCollection(), null /* requestContext */, false); },
+ "requestContext");
+ }
+
+ [Fact]
+ public void GenerateUrlWithNullRouteCollectionThrows()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate() { UrlHelper.GenerateUrl(null /* routeName */, null /* actionName */, null /* controllerName */, null /* routeValues */, null /* routeCollection */, null /* requestContext */, false); },
+ "routeCollection");
+ }
+
+ [Fact]
+ public void GenerateUrlWithEmptyCollectionsReturnsNull()
+ {
+ // Arrange
+ RequestContext requestContext = GetRequestContext();
+
+ // Act
+ string url = UrlHelper.GenerateUrl(null, null, null, null, new RouteCollection(), requestContext, true);
+
+ // Assert
+ Assert.Null(url);
+ }
+
+ [Fact]
+ public void GenerateUrlWithAction()
+ {
+ // Arrange
+ RequestContext requestContext = GetRequestContext(GetRouteData());
+
+ // Act
+ string url = UrlHelper.GenerateUrl(null, "newaction", null, null, GetRouteCollection(), requestContext, true);
+
+ // Assert
+ Assert.Equal(MvcHelper.AppPathModifier + "/app/home/newaction", url);
+ }
+
+ [Fact]
+ public void GenerateUrlWithActionAndController()
+ {
+ // Arrange
+ RequestContext requestContext = GetRequestContext(GetRouteData());
+
+ // Act
+ string url = UrlHelper.GenerateUrl(null, "newaction", "newcontroller", null, GetRouteCollection(), requestContext, true);
+
+ // Assert
+ Assert.Equal(MvcHelper.AppPathModifier + "/app/newcontroller/newaction", url);
+ }
+
+ [Fact]
+ public void GenerateUrlWithImplicitValues()
+ {
+ // Arrange
+ RequestContext requestContext = GetRequestContext(GetRouteData());
+
+ // Act
+ string url = UrlHelper.GenerateUrl(null, null, null, null, GetRouteCollection(), requestContext, true);
+
+ // Assert
+ Assert.Equal(MvcHelper.AppPathModifier + "/app/home/oldaction", url);
+ }
+
+ [Fact]
+ public void RouteUrlCanUseNamedRouteWithoutSpecifyingDefaults()
+ {
+ // DevDiv 217072: Non-mvc specific helpers should not give default values for controller and action
+
+ // Arrange
+ UrlHelper urlHelper = GetUrlHelper();
+ urlHelper.RouteCollection.MapRoute("MyRouteName", "any/url", new { controller = "Charlie" });
+
+ // Act
+ string result = urlHelper.RouteUrl("MyRouteName");
+
+ // Assert
+ Assert.Equal(MvcHelper.AppPathModifier + "/app/any/url", result);
+ }
+
+ [Fact]
+ public void RouteUrlWithDictionary()
+ {
+ // Arrange
+ UrlHelper urlHelper = GetUrlHelper();
+
+ // Act
+ string url = urlHelper.RouteUrl(new RouteValueDictionary(new { Action = "newaction", Controller = "home2", id = "someid" }));
+
+ // Assert
+ Assert.Equal(MvcHelper.AppPathModifier + "/app/home2/newaction/someid", url);
+ }
+
+ [Fact]
+ public void RouteUrlWithEmptyHostName()
+ {
+ // Arrange
+ UrlHelper urlHelper = GetUrlHelper();
+
+ // Act
+ string url = urlHelper.RouteUrl("namedroute", new RouteValueDictionary(new { Action = "newaction", Controller = "home2", id = "someid" }), "http", String.Empty /* hostName */);
+
+ // Assert
+ Assert.Equal("http://localhost" + MvcHelper.AppPathModifier + "/app/named/home2/newaction/someid", url);
+ }
+
+ [Fact]
+ public void RouteUrlWithEmptyProtocol()
+ {
+ // Arrange
+ UrlHelper urlHelper = GetUrlHelper();
+
+ // Act
+ string url = urlHelper.RouteUrl("namedroute", new RouteValueDictionary(new { Action = "newaction", Controller = "home2", id = "someid" }), String.Empty /* protocol */, "foo.bar.com");
+
+ // Assert
+ Assert.Equal("http://foo.bar.com" + MvcHelper.AppPathModifier + "/app/named/home2/newaction/someid", url);
+ }
+
+ [Fact]
+ public void RouteUrlWithNullProtocol()
+ {
+ // Arrange
+ UrlHelper urlHelper = GetUrlHelper();
+
+ // Act
+ string url = urlHelper.RouteUrl("namedroute", new RouteValueDictionary(new { Action = "newaction", Controller = "home2", id = "someid" }), null /* protocol */, "foo.bar.com");
+
+ // Assert
+ Assert.Equal("http://foo.bar.com" + MvcHelper.AppPathModifier + "/app/named/home2/newaction/someid", url);
+ }
+
+ [Fact]
+ public void RouteUrlWithNullProtocolAndNullHostName()
+ {
+ // Arrange
+ UrlHelper urlHelper = GetUrlHelper();
+
+ // Act
+ string url = urlHelper.RouteUrl("namedroute", new RouteValueDictionary(new { Action = "newaction", Controller = "home2", id = "someid" }), null /* protocol */, null /* hostName */);
+
+ // Assert
+ Assert.Equal(MvcHelper.AppPathModifier + "/app/named/home2/newaction/someid", url);
+ }
+
+ [Fact]
+ public void RouteUrlWithObjectProperties()
+ {
+ // Arrange
+ UrlHelper urlHelper = GetUrlHelper();
+
+ // Act
+ string url = urlHelper.RouteUrl(new { Action = "newaction", Controller = "home2", id = "someid" });
+
+ // Assert
+ Assert.Equal(MvcHelper.AppPathModifier + "/app/home2/newaction/someid", url);
+ }
+
+ [Fact]
+ public void RouteUrlWithProtocol()
+ {
+ // Arrange
+ UrlHelper urlHelper = GetUrlHelper();
+
+ // Act
+ string url = urlHelper.RouteUrl("namedroute", new { Action = "newaction", Controller = "home2", id = "someid" }, "https");
+
+ // Assert
+ Assert.Equal("https://localhost" + MvcHelper.AppPathModifier + "/app/named/home2/newaction/someid", url);
+ }
+
+ [Fact]
+ public void RouteUrlWithRouteNameAndDefaults()
+ {
+ // Arrange
+ UrlHelper urlHelper = GetUrlHelper();
+
+ // Act
+ string url = urlHelper.RouteUrl("namedroute");
+
+ // Assert
+ Assert.Equal(MvcHelper.AppPathModifier + "/app/named/home/oldaction", url);
+ }
+
+ [Fact]
+ public void RouteUrlWithRouteNameAndDictionary()
+ {
+ // Arrange
+ UrlHelper urlHelper = GetUrlHelper();
+
+ // Act
+ string url = urlHelper.RouteUrl("namedroute", new RouteValueDictionary(new { Action = "newaction", Controller = "home2", id = "someid" }));
+
+ // Assert
+ Assert.Equal(MvcHelper.AppPathModifier + "/app/named/home2/newaction/someid", url);
+ }
+
+ [Fact]
+ public void RouteUrlWithRouteNameAndObjectProperties()
+ {
+ // Arrange
+ UrlHelper urlHelper = GetUrlHelper();
+
+ // Act
+ string url = urlHelper.RouteUrl("namedroute", new { Action = "newaction", Controller = "home2", id = "someid" });
+
+ // Assert
+ Assert.Equal(MvcHelper.AppPathModifier + "/app/named/home2/newaction/someid", url);
+ }
+
+ [Fact]
+ public void UrlGenerationDoesNotChangeProvidedDictionary()
+ {
+ // Arrange
+ UrlHelper urlHelper = GetUrlHelper();
+ RouteValueDictionary valuesDictionary = new RouteValueDictionary();
+
+ // Act
+ urlHelper.Action("actionName", valuesDictionary);
+
+ // Assert
+ Assert.Empty(valuesDictionary);
+ Assert.False(valuesDictionary.ContainsKey("action"));
+ }
+
+ [Fact]
+ public void UrlGenerationReturnsNullWhenSubsequentSegmentHasValue()
+ { // Dev10 Bug #924729
+ // Arrange
+ RouteCollection routes = new RouteCollection();
+ routes.MapRoute("SampleRoute", "testing/{a}/{b}/{c}",
+ new
+ {
+ controller = "controller",
+ action = "action",
+ b = UrlParameter.Optional,
+ c = UrlParameter.Optional
+ });
+
+ UrlHelper helper = GetUrlHelper(routeCollection: routes);
+
+ // Act
+ string url = helper.Action("action", "controller", new { a = 42, c = 2112 });
+
+ // Assert
+ Assert.Null(url);
+ }
+
+ private static RequestContext GetRequestContext()
+ {
+ HttpContextBase httpcontext = MvcHelper.GetHttpContext("/app/", null, null);
+ RouteData rd = new RouteData();
+ return new RequestContext(httpcontext, rd);
+ }
+
+ private static RequestContext GetRequestContext(RouteData routeData)
+ {
+ HttpContextBase httpcontext = MvcHelper.GetHttpContext("/app/", null, null);
+ return new RequestContext(httpcontext, routeData);
+ }
+
+ private static RouteCollection GetRouteCollection()
+ {
+ RouteCollection rt = new RouteCollection();
+ rt.Add(new Route("{controller}/{action}/{id}", null) { Defaults = new RouteValueDictionary(new { id = "defaultid" }) });
+ rt.Add("namedroute", new Route("named/{controller}/{action}/{id}", null) { Defaults = new RouteValueDictionary(new { id = "defaultid" }) });
+ return rt;
+ }
+
+ private static RouteData GetRouteData()
+ {
+ RouteData rd = new RouteData();
+ rd.Values.Add("controller", "home");
+ rd.Values.Add("action", "oldaction");
+ return rd;
+ }
+
+ private static UrlHelper GetUrlHelper()
+ {
+ return GetUrlHelper(GetRouteData(), GetRouteCollection());
+ }
+
+ private static UrlHelper GetUrlHelper(RouteData routeData = null, RouteCollection routeCollection = null)
+ {
+ HttpContextBase httpcontext = MvcHelper.GetHttpContext("/app/", null, null);
+ UrlHelper urlHelper = new UrlHelper(new RequestContext(httpcontext, routeData ?? new RouteData()), routeCollection ?? new RouteCollection());
+ return urlHelper;
+ }
+
+ private static UrlHelper GetUrlHelperForIsLocalUrl()
+ {
+ Mock<HttpContextBase> contextMock = new Mock<HttpContextBase>();
+ contextMock.SetupGet(context => context.Request.Url).Returns(new Uri("http://www.mysite.com/"));
+ RequestContext requestContext = new RequestContext(contextMock.Object, new RouteData());
+ UrlHelper helper = new UrlHelper(requestContext);
+ return helper;
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/UrlParameterTest.cs b/test/System.Web.Mvc.Test/Test/UrlParameterTest.cs
new file mode 100644
index 00000000..89f8e8df
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/UrlParameterTest.cs
@@ -0,0 +1,14 @@
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class UrlParameterTest
+ {
+ [Fact]
+ public void UrlParameterOptionalToStringReturnsEmptyString()
+ {
+ // Act & Assert
+ Assert.Empty(UrlParameter.Optional.ToString());
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/UrlRewriterHelperTest.cs b/test/System.Web.Mvc.Test/Test/UrlRewriterHelperTest.cs
new file mode 100644
index 00000000..8c8f8740
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/UrlRewriterHelperTest.cs
@@ -0,0 +1,83 @@
+using System.Collections.Specialized;
+using Moq;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class UrlRewriterHelperTest
+ {
+ private const string _urlWasRewrittenServerVar = "IIS_WasUrlRewritten";
+ private const string _urlRewriterEnabledServerVar = "IIS_UrlRewriteModule";
+
+ [Fact]
+ public void WasRequestRewritten_FalseIfUrlRewriterIsTurnedOff()
+ {
+ // Arrange
+ UrlRewriterHelper helper = new UrlRewriterHelper();
+ Mock<HttpContextBase> requestMock = new Mock<HttpContextBase>();
+ requestMock.Setup(c => c.Request.ServerVariables.Get(_urlRewriterEnabledServerVar)).Returns((string)null).Verifiable();
+
+ // Act
+ bool result = helper.WasRequestRewritten(requestMock.Object);
+
+ // Assert
+ Assert.False(result);
+ requestMock.Verify();
+ requestMock.Verify(c => c.Request.ServerVariables.Get(_urlWasRewrittenServerVar), Times.Never());
+ }
+
+ [Fact]
+ public void WasRequestRewritten_FalseIfUrlRewriterIsTurnedOnButRequestWasNotRewritten()
+ {
+ // Arrange
+ UrlRewriterHelper helper = new UrlRewriterHelper();
+ Mock<HttpContextBase> requestMock = new Mock<HttpContextBase>();
+ requestMock.Setup(c => c.Request.ServerVariables.Get(_urlRewriterEnabledServerVar)).Returns("yes").Verifiable();
+ requestMock.Setup(c => c.Request.ServerVariables.Get(_urlWasRewrittenServerVar)).Returns((string)null).Verifiable();
+
+ // Act
+ bool result = helper.WasRequestRewritten(requestMock.Object);
+
+ // Assert
+ Assert.False(result);
+ requestMock.Verify();
+ }
+
+ [Fact]
+ public void WasRequestRewritten_TrueIfUrlRewriterIsTurnedOnAndRequestWasRewritten()
+ {
+ // Arrange
+ UrlRewriterHelper helper = new UrlRewriterHelper();
+ Mock<HttpContextBase> requestMock = new Mock<HttpContextBase>();
+ requestMock.Setup(c => c.Request.ServerVariables.Get(_urlRewriterEnabledServerVar)).Returns("yes").Verifiable();
+ requestMock.Setup(c => c.Request.ServerVariables.Get(_urlWasRewrittenServerVar)).Returns("yes").Verifiable();
+
+ // Act
+ bool result = helper.WasRequestRewritten(requestMock.Object);
+
+ // Assert
+ Assert.True(result);
+ requestMock.Verify();
+ }
+
+ [Fact]
+ public void WasRequestRewritten_ChecksIfUrlRewriterIsTurnedOnOnlyOnce()
+ {
+ // Arrange
+ UrlRewriterHelper helper = new UrlRewriterHelper();
+ Mock<HttpContextBase> request1Mock = new Mock<HttpContextBase>();
+ request1Mock.Setup(c => c.Request.ServerVariables).Returns(new NameValueCollection());
+ Mock<HttpContextBase> request2Mock = new Mock<HttpContextBase>();
+
+ // Act
+ bool result1 = helper.WasRequestRewritten(request1Mock.Object);
+ bool result2 = helper.WasRequestRewritten(request2Mock.Object);
+
+ // Assert
+ request1Mock.Verify(c => c.Request.ServerVariables, Times.Once());
+ request2Mock.Verify(c => c.Request.ServerVariables, Times.Never());
+ Assert.False(result1);
+ Assert.False(result2);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ValidatableObjectAdapterTest.cs b/test/System.Web.Mvc.Test/Test/ValidatableObjectAdapterTest.cs
new file mode 100644
index 00000000..4941de2f
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ValidatableObjectAdapterTest.cs
@@ -0,0 +1,172 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ValidatableObjectAdapterTest
+ {
+ // IValidatableObject support
+
+ [Fact]
+ public void NonIValidatableObjectInsideMetadataThrows()
+ {
+ // Arrange
+ var context = new ControllerContext();
+ var validatable = new Mock<IValidatableObject>();
+ var metadata = ModelMetadataProviders.Current.GetMetadataForType(() => 42, typeof(IValidatableObject));
+ var validator = new ValidatableObjectAdapter(metadata, context);
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ () => validator.Validate(null),
+ "The model object inside the metadata claimed to be compatible with System.ComponentModel.DataAnnotations.IValidatableObject, but was actually System.Int32.");
+ }
+
+ [Fact]
+ public void IValidatableObjectGetsAProperlyPopulatedValidationContext()
+ {
+ // Arrange
+ var context = new ControllerContext();
+ var validatable = new Mock<IValidatableObject>();
+ var metadata = ModelMetadataProviders.Current.GetMetadataForType(() => validatable.Object, validatable.Object.GetType());
+ var validator = new ValidatableObjectAdapter(metadata, context);
+ ValidationContext validationContext = null;
+ validatable.Setup(vo => vo.Validate(It.IsAny<ValidationContext>()))
+ .Callback<ValidationContext>(vc => validationContext = vc)
+ .Returns(Enumerable.Empty<ValidationResult>())
+ .Verifiable();
+
+ // Act
+ validator.Validate(null);
+
+ // Assert
+ validatable.Verify();
+ Assert.Same(validatable.Object, validationContext.ObjectInstance);
+ Assert.Null(validationContext.MemberName);
+ }
+
+ [Fact]
+ public void IValidatableObjectWithNoErrors()
+ {
+ // Arrange
+ var context = new ControllerContext();
+ var validatable = new Mock<IValidatableObject>();
+ var metadata = ModelMetadataProviders.Current.GetMetadataForType(() => validatable.Object, validatable.Object.GetType());
+ var validator = new ValidatableObjectAdapter(metadata, context);
+ validatable.Setup(vo => vo.Validate(It.IsAny<ValidationContext>()))
+ .Returns(Enumerable.Empty<ValidationResult>());
+
+ // Act
+ IEnumerable<ModelValidationResult> results = validator.Validate(null);
+
+ // Assert
+ Assert.Empty(results);
+ }
+
+ [Fact]
+ public void IValidatableObjectWithModelLevelError()
+ {
+ // Arrange
+ var context = new ControllerContext();
+ var validatable = new Mock<IValidatableObject>();
+ var metadata = ModelMetadataProviders.Current.GetMetadataForType(() => validatable.Object, validatable.Object.GetType());
+ var validator = new ValidatableObjectAdapter(metadata, context);
+ validatable.Setup(vo => vo.Validate(It.IsAny<ValidationContext>()))
+ .Returns(new ValidationResult[] { new ValidationResult("Error Message") });
+
+ // Act
+ ModelValidationResult result = validator.Validate(null).Single();
+
+ // Assert
+ Assert.Equal("Error Message", result.Message);
+ Assert.Equal(String.Empty, result.MemberName);
+ }
+
+ [Fact]
+ public void IValidatableObjectWithMultipleModelLevelErrors()
+ {
+ // Arrange
+ var context = new ControllerContext();
+ var validatable = new Mock<IValidatableObject>();
+ var metadata = ModelMetadataProviders.Current.GetMetadataForType(() => validatable.Object, validatable.Object.GetType());
+ var validator = new ValidatableObjectAdapter(metadata, context);
+ validatable.Setup(vo => vo.Validate(It.IsAny<ValidationContext>()))
+ .Returns(new ValidationResult[]
+ {
+ new ValidationResult("Error Message 1"),
+ new ValidationResult("Error Message 2")
+ });
+
+ // Act
+ ModelValidationResult[] results = validator.Validate(null).ToArray();
+
+ // Assert
+ Assert.Equal(2, results.Length);
+ Assert.Equal("Error Message 1", results[0].Message);
+ Assert.Equal("Error Message 2", results[1].Message);
+ }
+
+ [Fact]
+ public void IValidatableObjectWithMultiPropertyValidationFailure()
+ {
+ // Arrange
+ var context = new ControllerContext();
+ var validatable = new Mock<IValidatableObject>();
+ var metadata = ModelMetadataProviders.Current.GetMetadataForType(() => validatable.Object, validatable.Object.GetType());
+ var validator = new ValidatableObjectAdapter(metadata, context);
+ validatable.Setup(vo => vo.Validate(It.IsAny<ValidationContext>()))
+ .Returns(new[] { new ValidationResult("Error Message", new[] { "Property1", "Property2" }) })
+ .Verifiable();
+
+ // Act
+ ModelValidationResult[] results = validator.Validate(null).ToArray();
+
+ // Assert
+ validatable.Verify();
+ Assert.Equal(2, results.Length);
+ Assert.Equal("Error Message", results[0].Message);
+ Assert.Equal("Property1", results[0].MemberName);
+ Assert.Equal("Error Message", results[1].Message);
+ Assert.Equal("Property2", results[1].MemberName);
+ }
+
+ [Fact]
+ public void IValidatableObjectWhichIsNullReturnsNoErrors()
+ {
+ // Arrange
+ var context = new ControllerContext();
+ var metadata = ModelMetadataProviders.Current.GetMetadataForType(() => null, typeof(IValidatableObject));
+ var validator = new ValidatableObjectAdapter(metadata, context);
+
+ // Act
+ IEnumerable<ModelValidationResult> results = validator.Validate(null);
+
+ // Assert
+ Assert.Empty(results);
+ }
+
+ [Fact]
+ public void IValidatableObjectWhichReturnsValidationResultSuccessReturnsNoErrors()
+ {
+ // Arrange
+ var context = new ControllerContext();
+ var validatable = new Mock<IValidatableObject>();
+ var metadata = ModelMetadataProviders.Current.GetMetadataForType(() => validatable.Object, validatable.Object.GetType());
+ var validator = new ValidatableObjectAdapter(metadata, context);
+ validatable.Setup(vo => vo.Validate(It.IsAny<ValidationContext>()))
+ .Returns(new[] { ValidationResult.Success })
+ .Verifiable();
+
+ // Act
+ ModelValidationResult[] results = validator.Validate(null).ToArray();
+
+ // Assert
+ validatable.Verify();
+ Assert.Empty(results);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ValidateAntiForgeryTokenAttributeTest.cs b/test/System.Web.Mvc.Test/Test/ValidateAntiForgeryTokenAttributeTest.cs
new file mode 100644
index 00000000..0b9464cf
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ValidateAntiForgeryTokenAttributeTest.cs
@@ -0,0 +1,68 @@
+using System.Web.Helpers;
+using System.Web.TestUtil;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ValidateAntiForgeryTokenAttributeTest
+ {
+ [Fact]
+ public void OnAuthorization_ThrowsIfFilterContextIsNull()
+ {
+ // Arrange
+ ValidateAntiForgeryTokenAttribute attribute = new ValidateAntiForgeryTokenAttribute();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { attribute.OnAuthorization(null); }, "filterContext");
+ }
+
+ [Fact]
+ public void OnAuthorization_ForwardsAttributes()
+ {
+ // Arrange
+ HttpContextBase context = new Mock<HttpContextBase>().Object;
+ Mock<AuthorizationContext> authorizationContextMock = new Mock<AuthorizationContext>();
+ authorizationContextMock.SetupGet(ac => ac.HttpContext).Returns(context);
+ bool validateCalled = false;
+ Action<HttpContextBase, string> validateMethod = (c, s) =>
+ {
+ Assert.Same(context, c);
+ Assert.Equal("some salt", s);
+ validateCalled = true;
+ };
+ ValidateAntiForgeryTokenAttribute attribute = new ValidateAntiForgeryTokenAttribute(validateMethod)
+ {
+ Salt = "some salt"
+ };
+
+ // Act
+ attribute.OnAuthorization(authorizationContextMock.Object);
+
+ // Assert
+ Assert.True(validateCalled);
+ }
+
+ [Fact]
+ public void SaltProperty()
+ {
+ // Arrange
+ ValidateAntiForgeryTokenAttribute attribute = new ValidateAntiForgeryTokenAttribute();
+
+ // Act & Assert
+ MemberHelper.TestStringProperty(attribute, "Salt", String.Empty);
+ }
+
+ [Fact]
+ public void ValidateThunk_DefaultsToAntiForgeryMethod()
+ {
+ // Arrange
+ ValidateAntiForgeryTokenAttribute attribute = new ValidateAntiForgeryTokenAttribute();
+
+ // Act & Assert
+ Assert.Equal(AntiForgery.Validate, attribute.ValidateAction);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ValidateInputAttributeTest.cs b/test/System.Web.Mvc.Test/Test/ValidateInputAttributeTest.cs
new file mode 100644
index 00000000..0f94efc6
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ValidateInputAttributeTest.cs
@@ -0,0 +1,73 @@
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ValidateInputAttributeTest
+ {
+ [Fact]
+ public void EnableValidationProperty()
+ {
+ // Act
+ ValidateInputAttribute attrTrue = new ValidateInputAttribute(true);
+ ValidateInputAttribute attrFalse = new ValidateInputAttribute(false);
+
+ // Assert
+ Assert.True(attrTrue.EnableValidation);
+ Assert.False(attrFalse.EnableValidation);
+ }
+
+ [Fact]
+ public void OnAuthorizationSetsControllerValidateRequestToFalse()
+ {
+ // Arrange
+ Controller controller = new EmptyController() { ValidateRequest = true };
+ ValidateInputAttribute attr = new ValidateInputAttribute(enableValidation: false);
+ AuthorizationContext authContext = GetAuthorizationContext(controller);
+
+ // Act
+ attr.OnAuthorization(authContext);
+
+ // Assert
+ Assert.False(controller.ValidateRequest);
+ }
+
+ [Fact]
+ public void OnAuthorizationSetsControllerValidateRequestToTrue()
+ {
+ // Arrange
+ Controller controller = new EmptyController() { ValidateRequest = false };
+ ValidateInputAttribute attr = new ValidateInputAttribute(enableValidation: true);
+ AuthorizationContext authContext = GetAuthorizationContext(controller);
+
+ // Act
+ attr.OnAuthorization(authContext);
+
+ // Assert
+ Assert.True(controller.ValidateRequest);
+ }
+
+ [Fact]
+ public void OnAuthorizationThrowsIfFilterContextIsNull()
+ {
+ // Arrange
+ ValidateInputAttribute attr = new ValidateInputAttribute(true);
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { attr.OnAuthorization(null); }, "filterContext");
+ }
+
+ private static AuthorizationContext GetAuthorizationContext(ControllerBase controller)
+ {
+ Mock<AuthorizationContext> mockAuthContext = new Mock<AuthorizationContext>();
+ mockAuthContext.Setup(c => c.Controller).Returns(controller);
+ return mockAuthContext.Object;
+ }
+
+ private class EmptyController : Controller
+ {
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ValueProviderCollectionTest.cs b/test/System.Web.Mvc.Test/Test/ValueProviderCollectionTest.cs
new file mode 100644
index 00000000..950722fa
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ValueProviderCollectionTest.cs
@@ -0,0 +1,246 @@
+using System.Collections.Generic;
+using System.Linq;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ValueProviderCollectionTest
+ {
+ [Fact]
+ public void ListWrappingConstructor()
+ {
+ // Arrange
+ List<IValueProvider> list = new List<IValueProvider>()
+ {
+ new Mock<IValueProvider>().Object, new Mock<IValueProvider>().Object
+ };
+
+ // Act
+ ValueProviderCollection collection = new ValueProviderCollection(list);
+
+ // Assert
+ Assert.Equal(list, collection.ToList());
+ }
+
+ [Fact]
+ public void ListWrappingConstructorThrowsIfListIsNull()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { new ValueProviderCollection(null); },
+ "list");
+ }
+
+ [Fact]
+ public void DefaultConstructor()
+ {
+ // Act
+ ValueProviderCollection collection = new ValueProviderCollection();
+
+ // Assert
+ Assert.Empty(collection);
+ }
+
+ [Fact]
+ public void AddNullValueProviderThrows()
+ {
+ // Arrange
+ ValueProviderCollection collection = new ValueProviderCollection();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { collection.Add(null); },
+ "item");
+ }
+
+ [Fact]
+ public void SetItem()
+ {
+ // Arrange
+ ValueProviderCollection collection = new ValueProviderCollection();
+ collection.Add(new Mock<IValueProvider>().Object);
+
+ IValueProvider newProvider = new Mock<IValueProvider>().Object;
+
+ // Act
+ collection[0] = newProvider;
+
+ // Assert
+ IValueProvider provider = Assert.Single(collection);
+ Assert.Equal(newProvider, provider);
+ }
+
+ [Fact]
+ public void SetNullValueProviderThrows()
+ {
+ // Arrange
+ ValueProviderCollection collection = new ValueProviderCollection();
+ collection.Add(new Mock<IValueProvider>().Object);
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { collection[0] = null; },
+ "item");
+ }
+
+ [Fact]
+ public void ContainsPrefix()
+ {
+ // Arrange
+ string prefix = "somePrefix";
+
+ Mock<IValueProvider> mockProvider1 = new Mock<IValueProvider>();
+ mockProvider1.Setup(p => p.ContainsPrefix(prefix)).Returns(false);
+ Mock<IValueProvider> mockProvider2 = new Mock<IValueProvider>();
+ mockProvider2.Setup(p => p.ContainsPrefix(prefix)).Returns(true);
+ Mock<IValueProvider> mockProvider3 = new Mock<IValueProvider>();
+ mockProvider3.Setup(p => p.ContainsPrefix(prefix)).Returns(false);
+
+ ValueProviderCollection collection = new ValueProviderCollection()
+ {
+ mockProvider1.Object, mockProvider2.Object, mockProvider3.Object
+ };
+
+ // Act
+ bool retVal = collection.ContainsPrefix(prefix);
+
+ // Assert
+ Assert.True(retVal);
+ }
+
+ [Fact]
+ public void GetValue()
+ {
+ // Arrange
+ string key = "someKey";
+
+ Mock<IValueProvider> mockProvider1 = new Mock<IValueProvider>();
+ mockProvider1.Setup(p => p.GetValue(key)).Returns((ValueProviderResult)null);
+ Mock<IValueProvider> mockProvider2 = new Mock<IValueProvider>();
+ mockProvider2.Setup(p => p.GetValue(key)).Returns(new ValueProviderResult("2", "2", null));
+ Mock<IValueProvider> mockProvider3 = new Mock<IValueProvider>();
+ mockProvider3.Setup(p => p.GetValue(key)).Returns(new ValueProviderResult("3", "3", null));
+
+ ValueProviderCollection collection = new ValueProviderCollection()
+ {
+ mockProvider1.Object, mockProvider2.Object, mockProvider3.Object
+ };
+
+ // Act
+ ValueProviderResult result = collection.GetValue(key);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(2, result.ConvertTo(typeof(int)));
+ }
+
+ [Fact]
+ public void GetValueFromProvider_NormalProvider_DoNotSkipValidation()
+ {
+ // Arrange
+ ValueProviderResult expectedResult = new ValueProviderResult("Success", "Success", null);
+
+ Mock<IValueProvider> mockProvider = new Mock<IValueProvider>();
+ mockProvider.Setup(o => o.GetValue("key")).Returns(expectedResult);
+
+ // Act
+ ValueProviderResult actualResult = ValueProviderCollection.GetValueFromProvider(mockProvider.Object, "key", skipValidation: false);
+
+ // Assert
+ Assert.Equal(expectedResult, actualResult);
+ }
+
+ [Fact]
+ public void GetValueFromProvider_NormalProvider_SkipValidation()
+ {
+ // Arrange
+ ValueProviderResult expectedResult = new ValueProviderResult("Success", "Success", null);
+
+ Mock<IValueProvider> mockProvider = new Mock<IValueProvider>();
+ mockProvider.Setup(o => o.GetValue("key")).Returns(expectedResult);
+
+ // Act
+ ValueProviderResult actualResult = ValueProviderCollection.GetValueFromProvider(mockProvider.Object, "key", skipValidation: true);
+
+ // Assert
+ Assert.Equal(expectedResult, actualResult);
+ }
+
+ [Fact]
+ public void GetValueFromProvider_UnvalidatedProvider_DoNotSkipValidation()
+ {
+ // Arrange
+ ValueProviderResult expectedResult = new ValueProviderResult("Success", "Success", null);
+
+ Mock<IUnvalidatedValueProvider> mockProvider = new Mock<IUnvalidatedValueProvider>();
+ mockProvider.Setup(o => o.GetValue("key", false)).Returns(expectedResult);
+
+ // Act
+ ValueProviderResult actualResult = ValueProviderCollection.GetValueFromProvider(mockProvider.Object, "key", skipValidation: false);
+
+ // Assert
+ Assert.Equal(expectedResult, actualResult);
+ }
+
+ [Fact]
+ public void GetValueFromProvider_UnvalidatedProvider_SkipValidation()
+ {
+ // Arrange
+ ValueProviderResult expectedResult = new ValueProviderResult("Success", "Success", null);
+
+ Mock<IUnvalidatedValueProvider> mockProvider = new Mock<IUnvalidatedValueProvider>();
+ mockProvider.Setup(o => o.GetValue("key", true)).Returns(expectedResult);
+
+ // Act
+ ValueProviderResult actualResult = ValueProviderCollection.GetValueFromProvider(mockProvider.Object, "key", skipValidation: true);
+
+ // Assert
+ Assert.Equal(expectedResult, actualResult);
+ }
+
+ [Fact]
+ public void GetValueFromProvider_EnumeratedProvider_GoodPrefix()
+ {
+ // Arrange
+ IDictionary<string, string> expectedResult = new Dictionary<string, string>()
+ {
+ { "random", "random.hello" }
+ };
+
+ Mock<IEnumerableValueProvider> mockProvider = new Mock<IEnumerableValueProvider>();
+ mockProvider.Setup(o => o.GetKeysFromPrefix("prefix")).Returns(expectedResult);
+
+ ValueProviderCollection providerCollection = new ValueProviderCollection(new List<IValueProvider>() { mockProvider.Object });
+
+ // Act
+ IDictionary<string, string> actualResult = providerCollection.GetKeysFromPrefix("prefix");
+
+ // Assert
+ Assert.Equal(expectedResult, actualResult);
+ }
+
+ [Fact]
+ public void GetValueFromProvider_EnumeratedProvider_NotFound_DoNotReturnNull()
+ {
+ // Arrange
+ IDictionary<string, string> expectedResult = new Dictionary<string, string>()
+ {
+ { "random", "random.hello" }
+ };
+
+ Mock<IEnumerableValueProvider> mockProvider = new Mock<IEnumerableValueProvider>();
+ mockProvider.Setup(o => o.GetKeysFromPrefix("notfound")).Returns(expectedResult);
+
+ ValueProviderCollection providerCollection = new ValueProviderCollection(new List<IValueProvider>() { mockProvider.Object });
+
+ // Act
+ IDictionary<string, string> actualResult = providerCollection.GetKeysFromPrefix("prefix");
+
+ // Assert
+ Assert.NotNull(actualResult);
+ Assert.Empty(actualResult);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ValueProviderDictionaryTest.cs b/test/System.Web.Mvc.Test/Test/ValueProviderDictionaryTest.cs
new file mode 100644
index 00000000..7bc74b78
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ValueProviderDictionaryTest.cs
@@ -0,0 +1,204 @@
+using System.Collections.Specialized;
+using System.Globalization;
+using System.Threading;
+using System.Web.Routing;
+using System.Web.TestUtil;
+using Moq;
+using Xunit;
+
+#pragma warning disable 0618 // ValueProviderDictionary is now obsolete
+
+namespace System.Web.Mvc.Test
+{
+ public class ValueProviderDictionaryTest
+ {
+ [Fact]
+ public void ConstructorCreatesEmptyDictionaryIfControllerContextIsNull()
+ {
+ // Act
+ ValueProviderDictionary dict = new ValueProviderDictionary(null);
+
+ // Assert
+ Assert.Empty(dict);
+ }
+
+ [Fact]
+ public void ControllerContextProperty()
+ {
+ // Arrange
+ ControllerContext expected = GetControllerContext();
+ ValueProviderDictionary dict = new ValueProviderDictionary(expected);
+
+ // Act
+ ControllerContext returned = dict.ControllerContext;
+
+ // Assert
+ Assert.Equal(expected, returned);
+ }
+
+ [Fact]
+ public void DictionaryInterface()
+ {
+ // Arrange
+ DictionaryHelper<string, ValueProviderResult> helper = new DictionaryHelper<string, ValueProviderResult>()
+ {
+ Creator = () => new ValueProviderDictionary(null),
+ Comparer = StringComparer.OrdinalIgnoreCase,
+ SampleKeys = new string[] { "foo", "bar", "baz", "quux", "QUUX" },
+ SampleValues = new ValueProviderResult[]
+ {
+ new ValueProviderResult(null, null, null),
+ new ValueProviderResult(null, null, null),
+ new ValueProviderResult(null, null, null),
+ new ValueProviderResult(null, null, null),
+ new ValueProviderResult(null, null, null)
+ },
+ ThrowOnKeyNotFound = false
+ };
+
+ // Act & assert
+ helper.Execute();
+ }
+
+ [Fact]
+ public void AddWithRawValueUsesInvariantCulture()
+ {
+ // Arrange
+ ValueProviderDictionary dict = new ValueProviderDictionary(null);
+
+ // Act
+ dict.Add("foo", 42);
+
+ // Assert
+ ValueProviderResult vpResult = dict["foo"];
+ Assert.Equal("42", vpResult.AttemptedValue);
+ Assert.Equal(42, vpResult.RawValue);
+ Assert.Equal(CultureInfo.InvariantCulture, vpResult.Culture);
+ }
+
+ [Fact]
+ public void NullAndEmptyKeysAreIgnored()
+ {
+ // DevDiv Bugs #216667: Exception thrown when querystring contains name without value
+
+ // Arrange
+ ValueProviderDictionary dict = GetAndPopulateDictionary();
+
+ // Act
+ bool emptyKeyFound = dict.ContainsKey(String.Empty);
+
+ // Assert
+ Assert.False(emptyKeyFound);
+ }
+
+ [Fact]
+ public void ValueFromForm()
+ {
+ // Arrange
+ ValueProviderDictionary dict;
+
+ // Act
+ using (ReplaceCurrentCulture("fr-FR"))
+ {
+ dict = GetAndPopulateDictionary();
+ }
+ ValueProviderResult result = dict["foo"];
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal("fooFromForm", result.AttemptedValue);
+ string[] stringValue = Assert.IsType<string[]>(result.RawValue);
+ Assert.Single(stringValue);
+ Assert.Equal("fooFromForm", stringValue[0]);
+ Assert.Equal(CultureInfo.GetCultureInfo("fr-FR"), result.Culture);
+ }
+
+ [Fact]
+ public void ValueFromQueryString()
+ {
+ // Arrange
+ ValueProviderDictionary dict;
+
+ // Act
+ using (ReplaceCurrentCulture("fr-FR"))
+ {
+ dict = GetAndPopulateDictionary();
+ }
+ ValueProviderResult result = dict["baz"];
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal("bazFromQueryString", result.AttemptedValue);
+ string[] stringValue = Assert.IsType<string[]>(result.RawValue);
+ Assert.Single(stringValue);
+ Assert.Equal("bazFromQueryString", stringValue[0]);
+ Assert.Equal(CultureInfo.InvariantCulture, result.Culture);
+ }
+
+ public void ValueFromRoute()
+ {
+ // Arrange
+ ValueProviderDictionary dict;
+
+ // Act
+ using (ReplaceCurrentCulture("fr-FR"))
+ {
+ dict = GetAndPopulateDictionary();
+ }
+ ValueProviderResult result = dict["bar"];
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal("barFromRoute", result.AttemptedValue);
+ Assert.Equal("barFromRoute", result.RawValue);
+ Assert.Equal(CultureInfo.InvariantCulture, result.Culture);
+ }
+
+ private static ValueProviderDictionary GetAndPopulateDictionary()
+ {
+ return new ValueProviderDictionary(GetControllerContext());
+ }
+
+ private static ControllerContext GetControllerContext()
+ {
+ NameValueCollection form = new NameValueCollection() { { "foo", "fooFromForm" } };
+
+ RouteData rd = new RouteData();
+ rd.Values["foo"] = "fooFromRoute";
+ rd.Values["bar"] = "barFromRoute";
+
+ NameValueCollection queryString = new NameValueCollection()
+ {
+ { "foo", "fooFromQueryString" },
+ { "bar", "barFromQueryString" },
+ { "baz", "bazFromQueryString" },
+ { null, "nullValue" },
+ { "", "emptyStringValue" }
+ };
+
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.Setup(c => c.HttpContext.Request.Form).Returns(form);
+ mockControllerContext.Setup(c => c.HttpContext.Request.QueryString).Returns(queryString);
+ mockControllerContext.Setup(c => c.RouteData).Returns(rd);
+ return mockControllerContext.Object;
+ }
+
+ public static IDisposable ReplaceCurrentCulture(string culture)
+ {
+ CultureInfo newCulture = CultureInfo.GetCultureInfo(culture);
+ CultureInfo originalCulture = Thread.CurrentThread.CurrentCulture;
+ Thread.CurrentThread.CurrentCulture = newCulture;
+ return new CultureReplacement { OriginalCulture = originalCulture };
+ }
+
+ private class CultureReplacement : IDisposable
+ {
+ public CultureInfo OriginalCulture;
+
+ public void Dispose()
+ {
+ Thread.CurrentThread.CurrentCulture = OriginalCulture;
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ValueProviderFactoriesTest.cs b/test/System.Web.Mvc.Test/Test/ValueProviderFactoriesTest.cs
new file mode 100644
index 00000000..a4a2ac56
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ValueProviderFactoriesTest.cs
@@ -0,0 +1,29 @@
+using System.Linq;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class ValueProviderFactoriesTest
+ {
+ [Fact]
+ public void CollectionDefaults()
+ {
+ // Arrange
+ Type[] expectedTypes = new[]
+ {
+ typeof(ChildActionValueProviderFactory),
+ typeof(FormValueProviderFactory),
+ typeof(JsonValueProviderFactory),
+ typeof(RouteDataValueProviderFactory),
+ typeof(QueryStringValueProviderFactory),
+ typeof(HttpFileCollectionValueProviderFactory),
+ };
+
+ // Act
+ Type[] actualTypes = ValueProviderFactories.Factories.Select(p => p.GetType()).ToArray();
+
+ // Assert
+ Assert.Equal(expectedTypes, actualTypes);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ValueProviderFactoryCollectionTest.cs b/test/System.Web.Mvc.Test/Test/ValueProviderFactoryCollectionTest.cs
new file mode 100644
index 00000000..eedba8c0
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ValueProviderFactoryCollectionTest.cs
@@ -0,0 +1,143 @@
+using System.Collections.Generic;
+using System.Linq;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ValueProviderFactoryCollectionTest
+ {
+ [Fact]
+ public void ListWrappingConstructor()
+ {
+ // Arrange
+ List<ValueProviderFactory> list = new List<ValueProviderFactory>()
+ {
+ new FormValueProviderFactory()
+ };
+
+ // Act
+ ValueProviderFactoryCollection collection = new ValueProviderFactoryCollection(list);
+
+ // Assert
+ Assert.Equal(list, collection.ToList());
+ }
+
+ [Fact]
+ public void ListWrappingConstructorThrowsIfListIsNull()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { new ValueProviderFactoryCollection(null, null); },
+ "list");
+ }
+
+ [Fact]
+ public void DefaultConstructor()
+ {
+ // Act
+ ValueProviderFactoryCollection collection = new ValueProviderFactoryCollection();
+
+ // Assert
+ Assert.Empty(collection);
+ }
+
+ [Fact]
+ public void AddNullValueProviderFactoryThrows()
+ {
+ // Arrange
+ ValueProviderFactoryCollection collection = new ValueProviderFactoryCollection();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { collection.Add(null); },
+ "item");
+ }
+
+ [Fact]
+ public void GetValueProvider()
+ {
+ // Arrange
+ ControllerContext controllerContext = new ControllerContext();
+ IValueProvider[] expectedValueProviders = new[]
+ {
+ new Mock<IValueProvider>().Object,
+ new Mock<IValueProvider>().Object
+ };
+
+ Mock<ValueProviderFactory> mockFactory1 = new Mock<ValueProviderFactory>();
+ mockFactory1.Setup(o => o.GetValueProvider(controllerContext)).Returns(expectedValueProviders[0]);
+ Mock<ValueProviderFactory> mockFactory2 = new Mock<ValueProviderFactory>();
+ mockFactory2.Setup(o => o.GetValueProvider(controllerContext)).Returns(expectedValueProviders[1]);
+
+ ValueProviderFactoryCollection factories = new ValueProviderFactoryCollection()
+ {
+ mockFactory1.Object,
+ mockFactory2.Object
+ };
+
+ // Act
+ ValueProviderCollection valueProviders = (ValueProviderCollection)factories.GetValueProvider(controllerContext);
+
+ // Assert
+ Assert.Equal(expectedValueProviders, valueProviders.ToArray());
+ }
+
+ [Fact]
+ public void GetValueProviderDelegatesToResolver()
+ {
+ //Arrange
+ ControllerContext controllerContext = new ControllerContext();
+ IValueProvider[] expectedValueProviders = new[]
+ {
+ new Mock<IValueProvider>().Object,
+ new Mock<IValueProvider>().Object
+ };
+
+ Mock<ValueProviderFactory> mockFactory1 = new Mock<ValueProviderFactory>();
+ mockFactory1.Setup(o => o.GetValueProvider(controllerContext)).Returns(expectedValueProviders[0]);
+ Mock<ValueProviderFactory> mockFactory2 = new Mock<ValueProviderFactory>();
+ mockFactory2.Setup(o => o.GetValueProvider(controllerContext)).Returns(expectedValueProviders[1]);
+
+ Resolver<IEnumerable<ValueProviderFactory>> resolver = new Resolver<IEnumerable<ValueProviderFactory>> { Current = new[] { mockFactory1.Object, mockFactory2.Object } };
+ ValueProviderFactoryCollection factories = new ValueProviderFactoryCollection(resolver);
+
+ // Act
+ ValueProviderCollection valueProviders = (ValueProviderCollection)factories.GetValueProvider(controllerContext);
+
+ // Assert
+ Assert.Equal(expectedValueProviders, valueProviders.ToArray());
+ }
+
+ [Fact]
+ public void SetItem()
+ {
+ // Arrange
+ ValueProviderFactoryCollection collection = new ValueProviderFactoryCollection();
+ collection.Add(new Mock<ValueProviderFactory>().Object);
+
+ ValueProviderFactory newFactory = new Mock<ValueProviderFactory>().Object;
+
+ // Act
+ collection[0] = newFactory;
+
+ // Assert
+ Assert.Single(collection);
+ Assert.Equal(newFactory, collection[0]);
+ }
+
+ [Fact]
+ public void SetNullValueProviderFactoryThrows()
+ {
+ // Arrange
+ ValueProviderFactoryCollection collection = new ValueProviderFactoryCollection();
+ collection.Add(new Mock<ValueProviderFactory>().Object);
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { collection[0] = null; },
+ "item");
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ValueProviderResultTest.cs b/test/System.Web.Mvc.Test/Test/ValueProviderResultTest.cs
new file mode 100644
index 00000000..cae550d8
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ValueProviderResultTest.cs
@@ -0,0 +1,398 @@
+using System.Globalization;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ValueProviderResultTest
+ {
+ [Fact]
+ public void ConstructorSetsProperties()
+ {
+ // Arrange
+ object rawValue = new object();
+ string attemptedValue = "some string";
+ CultureInfo culture = CultureInfo.GetCultureInfo("fr-FR");
+
+ // Act
+ ValueProviderResult result = new ValueProviderResult(rawValue, attemptedValue, culture);
+
+ // Assert
+ Assert.Same(rawValue, result.RawValue);
+ Assert.Same(attemptedValue, result.AttemptedValue);
+ Assert.Same(culture, result.Culture);
+ }
+
+ [Fact]
+ public void ConvertToCanConvertArraysToArrays()
+ {
+ // Arrange
+ ValueProviderResult vpr = new ValueProviderResult(new int[] { 1, 20, 42 }, "", CultureInfo.InvariantCulture);
+
+ // Act
+ string[] converted = (string[])vpr.ConvertTo(typeof(string[]));
+
+ // Assert
+ Assert.NotNull(converted);
+ Assert.Equal(3, converted.Length);
+ Assert.Equal("1", converted[0]);
+ Assert.Equal("20", converted[1]);
+ Assert.Equal("42", converted[2]);
+ }
+
+ [Fact]
+ public void ConvertToCanConvertArraysToSingleElements()
+ {
+ // Arrange
+ ValueProviderResult vpr = new ValueProviderResult(new int[] { 1, 20, 42 }, "", CultureInfo.InvariantCulture);
+
+ // Act
+ string converted = (string)vpr.ConvertTo(typeof(string));
+
+ // Assert
+ Assert.Equal("1", converted);
+ }
+
+ [Fact]
+ public void ConvertToCanConvertSingleElementsToArrays()
+ {
+ // Arrange
+ ValueProviderResult vpr = new ValueProviderResult(42, "", CultureInfo.InvariantCulture);
+
+ // Act
+ string[] converted = (string[])vpr.ConvertTo(typeof(string[]));
+
+ // Assert
+ Assert.NotNull(converted);
+ Assert.Single(converted);
+ Assert.Equal("42", converted[0]);
+ }
+
+ [Fact]
+ public void ConvertToCanConvertSingleElementsToSingleElements()
+ {
+ // Arrange
+ ValueProviderResult vpr = new ValueProviderResult(42, "", CultureInfo.InvariantCulture);
+
+ // Act
+ string converted = (string)vpr.ConvertTo(typeof(string));
+
+ // Assert
+ Assert.NotNull(converted);
+ Assert.Equal("42", converted);
+ }
+
+ [Fact]
+ public void ConvertToChecksTypeConverterCanConvertFrom()
+ {
+ // Arrange
+ object original = "someValue";
+ ValueProviderResult vpr = new ValueProviderResult(original, null, CultureInfo.GetCultureInfo("fr-FR"));
+
+ // Act
+ DefaultModelBinderTest.StringContainer returned = (DefaultModelBinderTest.StringContainer)vpr.ConvertTo(typeof(DefaultModelBinderTest.StringContainer));
+
+ // Assert
+ Assert.Equal(returned.Value, "someValue (fr-FR)");
+ }
+
+ [Fact]
+ public void ConvertToChecksTypeConverterCanConvertTo()
+ {
+ // Arrange
+ object original = new DefaultModelBinderTest.StringContainer("someValue");
+ ValueProviderResult vpr = new ValueProviderResult(original, "", CultureInfo.GetCultureInfo("en-US"));
+
+ // Act
+ string returned = (string)vpr.ConvertTo(typeof(string));
+
+ // Assert
+ Assert.Equal(returned, "someValue (en-US)");
+ }
+
+ [Fact]
+ public void ConvertToReturnsNullIfArrayElementValueIsNull()
+ {
+ // Arrange
+ ValueProviderResult vpr = new ValueProviderResult(new string[] { null }, null, CultureInfo.InvariantCulture);
+
+ // Act
+ object outValue = vpr.ConvertTo(typeof(int));
+
+ // Assert
+ Assert.Null(outValue);
+ }
+
+ [Fact]
+ public void ConvertToReturnsNullIfTryingToConvertEmptyArrayToSingleElement()
+ {
+ // Arrange
+ ValueProviderResult vpr = new ValueProviderResult(new int[0], "", CultureInfo.InvariantCulture);
+
+ // Act
+ object outValue = vpr.ConvertTo(typeof(int));
+
+ // Assert
+ Assert.Null(outValue);
+ }
+
+ [Fact]
+ public void ConvertToReturnsNullIfValueIsEmptyString()
+ {
+ // Arrange
+ ValueProviderResult vpr = new ValueProviderResult("", null, CultureInfo.InvariantCulture);
+
+ // Act
+ object outValue = vpr.ConvertTo(typeof(int));
+
+ // Assert
+ Assert.Null(outValue);
+ }
+
+ [Fact]
+ public void ConvertToReturnsNullIfTrimmedValueIsEmptyString()
+ {
+ // Arrange
+ ValueProviderResult vpr = new ValueProviderResult(" \t \r\n ", null, CultureInfo.InvariantCulture);
+
+ // Act
+ object outValue = vpr.ConvertTo(typeof(int));
+
+ // Assert
+ Assert.Null(outValue);
+ }
+
+ [Fact]
+ public void ConvertToReturnsNullIfValueIsNull()
+ {
+ // Arrange
+ ValueProviderResult vpr = new ValueProviderResult(null /* rawValue */, null /* attemptedValue */, CultureInfo.InvariantCulture);
+
+ // Act
+ object outValue = vpr.ConvertTo(typeof(int[]));
+
+ // Assert
+ Assert.Null(outValue);
+ }
+
+ [Fact]
+ public void ConvertToReturnsValueIfArrayElementIsIntegerAndDestinationTypeIsEnum()
+ {
+ // Arrange
+ ValueProviderResult vpr = new ValueProviderResult(new object[] { 1 }, null, CultureInfo.InvariantCulture);
+
+ // Act
+ object outValue = vpr.ConvertTo(typeof(MyEnum));
+
+ // Assert
+ Assert.Equal(outValue, MyEnum.Value1);
+ }
+
+ [Fact]
+ public void ConvertToReturnsValueIfArrayElementIsStringValueAndDestinationTypeIsEnum()
+ {
+ // Arrange
+ ValueProviderResult vpr = new ValueProviderResult(new object[] { "1" }, null, CultureInfo.InvariantCulture);
+
+ // Act
+ object outValue = vpr.ConvertTo(typeof(MyEnum));
+
+ // Assert
+ Assert.Equal(outValue, MyEnum.Value1);
+ }
+
+ [Fact]
+ public void ConvertToReturnsValueIfArrayElementIsStringKeyAndDestinationTypeIsEnum()
+ {
+ // Arrange
+ ValueProviderResult vpr = new ValueProviderResult(new object[] { "Value1" }, null, CultureInfo.InvariantCulture);
+
+ // Act
+ object outValue = vpr.ConvertTo(typeof(MyEnum));
+
+ // Assert
+ Assert.Equal(outValue, MyEnum.Value1);
+ }
+
+ [Fact]
+ public void ConvertToReturnsValueIfElementIsStringAndDestionationIsNullableInteger()
+ {
+ // Arrange
+ ValueProviderResult vpr = new ValueProviderResult("12", null, CultureInfo.InvariantCulture);
+
+ // Act
+ object outValue = vpr.ConvertTo(typeof(int?));
+
+ // Assert
+ Assert.Equal(12, outValue);
+ }
+
+ [Fact]
+ public void ConvertToReturnsValueIfElementIsStringAndDestionationIsNullableDouble()
+ {
+ // Arrange
+ ValueProviderResult vpr = new ValueProviderResult("12.5", null, CultureInfo.InvariantCulture);
+
+ // Act
+ object outValue = vpr.ConvertTo(typeof(double?));
+
+ // Assert
+ Assert.Equal(12.5, outValue);
+ }
+
+ [Fact]
+ public void ConvertToReturnsValueIfElementIsDecimalAndDestionationIsNullableInteger()
+ {
+ // Arrange
+ ValueProviderResult vpr = new ValueProviderResult(12M, null, CultureInfo.InvariantCulture);
+
+ // Act
+ object outValue = vpr.ConvertTo(typeof(int?));
+
+ // Assert
+ Assert.Equal(12, outValue);
+ }
+
+ [Fact]
+ public void ConvertToReturnsValueIfElementIsDecimalAndDestionationIsNullableDouble()
+ {
+ // Arrange
+ ValueProviderResult vpr = new ValueProviderResult(12.5M, null, CultureInfo.InvariantCulture);
+
+ // Act
+ object outValue = vpr.ConvertTo(typeof(double?));
+
+ // Assert
+ Assert.Equal(12.5, outValue);
+ }
+
+ [Fact]
+ public void ConvertToReturnsValueIfElementIsDecimalDoubleAndDestionationIsNullableInteger()
+ {
+ // Arrange
+ ValueProviderResult vpr = new ValueProviderResult(12.5M, null, CultureInfo.InvariantCulture);
+
+ // Act
+ object outValue = vpr.ConvertTo(typeof(int?));
+
+ // Assert
+ Assert.Equal(12, outValue);
+ }
+
+ [Fact]
+ public void ConvertToReturnsValueIfElementIsDecimalDoubleAndDestionationIsNullableLong()
+ {
+ // Arrange
+ ValueProviderResult vpr = new ValueProviderResult(12.5M, null, CultureInfo.InvariantCulture);
+
+ // Act
+ object outValue = vpr.ConvertTo(typeof(long?));
+
+ // Assert
+ Assert.Equal(12L, outValue);
+ }
+
+ [Fact]
+ public void ConvertToReturnsValueIfArrayElementInstanceOfDestinationType()
+ {
+ // Arrange
+ ValueProviderResult vpr = new ValueProviderResult(new object[] { "some string" }, null, CultureInfo.InvariantCulture);
+
+ // Act
+ object outValue = vpr.ConvertTo(typeof(string));
+
+ // Assert
+ Assert.Equal("some string", outValue);
+ }
+
+ [Fact]
+ public void ConvertToReturnsValueIfInstanceOfDestinationType()
+ {
+ // Arrange
+ string[] original = new string[] { "some string" };
+ ValueProviderResult vpr = new ValueProviderResult(original, null, CultureInfo.InvariantCulture);
+
+ // Act
+ object outValue = vpr.ConvertTo(typeof(string[]));
+
+ // Assert
+ Assert.Same(original, outValue);
+ }
+
+ [Fact]
+ public void ConvertToThrowsIfConverterThrows()
+ {
+ // Arrange
+ ValueProviderResult vpr = new ValueProviderResult("x", null, CultureInfo.InvariantCulture);
+ Type destinationType = typeof(DefaultModelBinderTest.StringContainer);
+
+ // Act & Assert
+ // Will throw since the custom converter assumes the first 5 characters to be digits
+ InvalidOperationException exception = Assert.Throws<InvalidOperationException>(
+ delegate { vpr.ConvertTo(destinationType); },
+ "The parameter conversion from type 'System.String' to type 'System.Web.Mvc.Test.DefaultModelBinderTest+StringContainer' failed. See the inner exception for more information.");
+
+ Exception innerException = exception.InnerException;
+ Assert.Equal("Value must have at least 3 characters.", innerException.Message);
+ }
+
+ [Fact]
+ public void ConvertToThrowsIfNoConverterExists()
+ {
+ // Arrange
+ ValueProviderResult vpr = new ValueProviderResult("x", null, CultureInfo.InvariantCulture);
+ Type destinationType = typeof(MyClassWithoutConverter);
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ delegate { vpr.ConvertTo(destinationType); },
+ "The parameter conversion from type 'System.String' to type 'System.Web.Mvc.Test.ValueProviderResultTest+MyClassWithoutConverter' failed because no type converter can convert between these types.");
+ }
+
+ [Fact]
+ public void ConvertToThrowsIfTypeIsNull()
+ {
+ // Arrange
+ ValueProviderResult vpr = new ValueProviderResult("x", null, CultureInfo.InvariantCulture);
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { vpr.ConvertTo(null); }, "type");
+ }
+
+ [Fact]
+ public void ConvertToUsesProvidedCulture()
+ {
+ // Arrange
+ object original = "someValue";
+ CultureInfo gbCulture = CultureInfo.GetCultureInfo("en-GB");
+ ValueProviderResult vpr = new ValueProviderResult(original, null, CultureInfo.GetCultureInfo("fr-FR"));
+
+ // Act
+ DefaultModelBinderTest.StringContainer returned = (DefaultModelBinderTest.StringContainer)vpr.ConvertTo(typeof(DefaultModelBinderTest.StringContainer), gbCulture);
+
+ // Assert
+ Assert.Equal(returned.Value, "someValue (en-GB)");
+ }
+
+ [Fact]
+ public void CulturePropertyDefaultsToInvariantCulture()
+ {
+ // Arrange
+ ValueProviderResult result = new ValueProviderResult(null, null, null);
+
+ // Act & assert
+ Assert.Same(CultureInfo.InvariantCulture, result.Culture);
+ }
+
+ private class MyClassWithoutConverter
+ {
+ }
+
+ private enum MyEnum
+ {
+ Value0 = 0,
+ Value1 = 1
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ValueProviderUtilTest.cs b/test/System.Web.Mvc.Test/Test/ValueProviderUtilTest.cs
new file mode 100644
index 00000000..9ceec6b5
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ValueProviderUtilTest.cs
@@ -0,0 +1,185 @@
+using System.Collections.Generic;
+using System.Linq;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class ValueProviderUtilTest
+ {
+ [Fact]
+ public void CollectionContainsPrefix_EmptyCollectionReturnsFalse()
+ {
+ // Arrange
+ string[] collection = new string[0];
+
+ // Act
+ bool retVal = ValueProviderUtil.CollectionContainsPrefix(collection, "");
+
+ // Assert
+ Assert.False(retVal);
+ }
+
+ [Fact]
+ public void CollectionContainsPrefix_ExactMatch()
+ {
+ // Arrange
+ string[] collection = new string[] { "Hello" };
+
+ // Act
+ bool retVal = ValueProviderUtil.CollectionContainsPrefix(collection, "Hello");
+
+ // Assert
+ Assert.True(retVal);
+ }
+
+ [Fact]
+ public void CollectionContainsPrefix_MatchIsCaseInsensitive()
+ {
+ // Arrange
+ string[] collection = new string[] { "Hello" };
+
+ // Act
+ bool retVal = ValueProviderUtil.CollectionContainsPrefix(collection, "hello");
+
+ // Assert
+ Assert.True(retVal);
+ }
+
+ [Fact]
+ public void CollectionContainsPrefix_MatchIsNotSimpleSubstringMatch()
+ {
+ // Arrange
+ string[] collection = new string[] { "Hello" };
+
+ // Act
+ bool retVal = ValueProviderUtil.CollectionContainsPrefix(collection, "He");
+
+ // Assert
+ Assert.False(retVal);
+ }
+
+ [Fact]
+ public void CollectionContainsPrefix_NonEmptyCollectionReturnsTrueIfPrefixIsEmptyString()
+ {
+ // Arrange
+ string[] collection = new string[] { "Hello" };
+
+ // Act
+ bool retVal = ValueProviderUtil.CollectionContainsPrefix(collection, "");
+
+ // Assert
+ Assert.True(retVal);
+ }
+
+ [Fact]
+ public void CollectionContainsPrefix_PrefixBoundaries()
+ {
+ // Arrange
+ string[] collection = new string[] { "Hello.There[0]" };
+
+ // Act
+ bool retVal1 = ValueProviderUtil.CollectionContainsPrefix(collection, "hello");
+ bool retVal2 = ValueProviderUtil.CollectionContainsPrefix(collection, "hello.there");
+
+ // Assert
+ Assert.True(retVal1);
+ Assert.True(retVal2);
+ }
+
+ [Fact]
+ public void GetPrefixes()
+ {
+ // Arrange
+ string key = "foo.bar[baz].quux";
+ string[] expected = new string[]
+ {
+ "foo.bar[baz].quux",
+ "foo.bar[baz]",
+ "foo.bar",
+ "foo"
+ };
+
+ // Act
+ string[] result = ValueProviderUtil.GetPrefixes(key).ToArray();
+
+ // Assert
+ Assert.Equal(expected, result);
+ }
+
+ public void GetKeysFromPrefixWithDotsNotation()
+ {
+ // Arrange
+ IList<string> collection = new List<string>()
+ {
+ "foo.bar", "something.other", "foo.baz", "foot.hello", "fo.nothing", "foo"
+ };
+ string prefix = "foo";
+
+ // Act
+ IDictionary<string, string> result = ValueProviderUtil.GetKeysFromPrefix(collection, prefix);
+
+ // Assert
+ Assert.Equal(2, result.Count());
+ Assert.True(result.ContainsKey("bar"));
+ Assert.True(result.ContainsKey("baz"));
+ Assert.Equal("foo.bar", result["bar"]);
+ Assert.Equal("foo.baz", result["baz"]);
+ }
+
+ public void GetKeysFromPrefixWithBracketsNotation()
+ {
+ // Arrange
+ IList<string> collection = new List<string>()
+ {
+ "foo[bar]", "something[other]", "foo[baz]", "foot[hello]", "fo[nothing]", "foo"
+ };
+ string prefix = "foo";
+
+ // Act
+ IDictionary<string, string> result = ValueProviderUtil.GetKeysFromPrefix(collection, prefix);
+
+ // Assert
+ Assert.Equal(2, result.Count());
+ Assert.True(result.ContainsKey("bar"));
+ Assert.True(result.ContainsKey("baz"));
+ Assert.Equal("foo[bar]", result["bar"]);
+ Assert.Equal("foo[baz]", result["baz"]);
+ }
+
+ public void GetKeysFromPrefixWithDotsAndBracketsNotation()
+ {
+ // Arrange
+ IList<string> collection = new List<string>()
+ {
+ "foo[bar]", "something[other]", "foo.baz", "foot[hello]", "fo[nothing]", "foo"
+ };
+ string prefix = "foo";
+
+ // Act
+ IDictionary<string, string> result = ValueProviderUtil.GetKeysFromPrefix(collection, prefix);
+
+ // Assert
+ Assert.Equal(2, result.Count());
+ Assert.True(result.ContainsKey("bar"));
+ Assert.True(result.ContainsKey("baz"));
+ Assert.Equal("foo[bar]", result["bar"]);
+ Assert.Equal("foo.baz", result["baz"]);
+ }
+
+ public void GetKeysFromPrefixWithPrefixNotFound()
+ {
+ // Arrange
+ IList<string> collection = new List<string>()
+ {
+ "foo[bar]", "something[other]", "foo.baz", "foot[hello]", "fo[nothing]", "foo"
+ };
+ string prefix = "notfound";
+
+ // Act
+ IDictionary<string, string> result = ValueProviderUtil.GetKeysFromPrefix(collection, prefix);
+
+ // Assert
+ Assert.Empty(result);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ViewContextTest.cs b/test/System.Web.Mvc.Test/Test/ViewContextTest.cs
new file mode 100644
index 00000000..b8230fb2
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ViewContextTest.cs
@@ -0,0 +1,182 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ViewContextTest
+ {
+ [Fact]
+ public void GuardClauses()
+ {
+ // Arrange
+ var controllerContext = new Mock<ControllerContext>().Object;
+ var view = new Mock<IView>().Object;
+ var viewData = new ViewDataDictionary();
+ var tempData = new TempDataDictionary();
+ var writer = new StringWriter();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => new ViewContext(null, view, viewData, tempData, writer),
+ "controllerContext"
+ );
+ Assert.ThrowsArgumentNull(
+ () => new ViewContext(controllerContext, null, viewData, tempData, writer),
+ "view"
+ );
+ Assert.ThrowsArgumentNull(
+ () => new ViewContext(controllerContext, view, null, tempData, writer),
+ "viewData"
+ );
+ Assert.ThrowsArgumentNull(
+ () => new ViewContext(controllerContext, view, viewData, null, writer),
+ "tempData"
+ );
+ Assert.ThrowsArgumentNull(
+ () => new ViewContext(controllerContext, view, viewData, tempData, null),
+ "writer"
+ );
+ }
+
+ [Fact]
+ public void FormIdGeneratorProperty()
+ {
+ // Arrange
+ var mockHttpContext = new Mock<HttpContextBase>();
+ mockHttpContext.Setup(o => o.Items).Returns(new Hashtable());
+ var viewContext = new ViewContext
+ {
+ HttpContext = mockHttpContext.Object
+ };
+
+ // Act
+ string form0Name = viewContext.FormIdGenerator();
+ string form1Name = viewContext.FormIdGenerator();
+ string form2Name = viewContext.FormIdGenerator();
+
+ // Assert
+ Assert.Equal("form0", form0Name);
+ Assert.Equal("form1", form1Name);
+ Assert.Equal("form2", form2Name);
+ }
+
+ [Fact]
+ public void PropertiesAreSet()
+ {
+ // Arrange
+ var mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.Setup(o => o.HttpContext.Items).Returns(new Hashtable());
+ var view = new Mock<IView>().Object;
+ var viewData = new ViewDataDictionary();
+ var tempData = new TempDataDictionary();
+ var writer = new StringWriter();
+
+ // Act
+ ViewContext viewContext = new ViewContext(mockControllerContext.Object, view, viewData, tempData, writer);
+
+ // Setting FormContext to null will return the default one later
+ viewContext.FormContext = null;
+
+ // Assert
+ Assert.Equal(view, viewContext.View);
+ Assert.Equal(viewData, viewContext.ViewData);
+ Assert.Equal(tempData, viewContext.TempData);
+ Assert.Equal(writer, viewContext.Writer);
+ Assert.False(viewContext.UnobtrusiveJavaScriptEnabled); // Unobtrusive JavaScript should be off by default
+ Assert.NotNull(viewContext.FormContext); // We get the default FormContext
+ }
+
+ [Fact]
+ public void ViewContextUsesScopeThunkForInstanceClientValidationFlag()
+ {
+ // Arrange
+ var scope = new Dictionary<object, object>();
+ var httpContext = new Mock<HttpContextBase>();
+ var viewContext = new ViewContext { ScopeThunk = () => scope, HttpContext = httpContext.Object };
+ httpContext.Setup(c => c.Items).Returns(new Hashtable());
+
+ // Act & Assert
+ Assert.False(viewContext.ClientValidationEnabled);
+ viewContext.ClientValidationEnabled = true;
+ Assert.True(viewContext.ClientValidationEnabled);
+ Assert.Equal(true, scope[ViewContext.ClientValidationKeyName]);
+ viewContext.ClientValidationEnabled = false;
+ Assert.False(viewContext.ClientValidationEnabled);
+ Assert.Equal(false, scope[ViewContext.ClientValidationKeyName]);
+ }
+
+ [Fact]
+ public void ViewContextUsesScopeThunkForInstanceUnobstrusiveJavaScriptFlag()
+ {
+ // Arrange
+ var scope = new Dictionary<object, object>();
+ var httpContext = new Mock<HttpContextBase>();
+ var viewContext = new ViewContext { ScopeThunk = () => scope, HttpContext = httpContext.Object };
+ httpContext.Setup(c => c.Items).Returns(new Hashtable());
+
+ // Act & Assert
+ Assert.False(viewContext.UnobtrusiveJavaScriptEnabled);
+ viewContext.UnobtrusiveJavaScriptEnabled = true;
+ Assert.True(viewContext.UnobtrusiveJavaScriptEnabled);
+ Assert.Equal(true, scope[ViewContext.UnobtrusiveJavaScriptKeyName]);
+ viewContext.UnobtrusiveJavaScriptEnabled = false;
+ Assert.False(viewContext.UnobtrusiveJavaScriptEnabled);
+ Assert.Equal(false, scope[ViewContext.UnobtrusiveJavaScriptKeyName]);
+ }
+
+ [Fact]
+ public void ViewBagProperty_ReflectsViewData()
+ {
+ // Arrange
+ var mockControllerContext = new Mock<ControllerContext>();
+ var view = new Mock<IView>().Object;
+ var viewData = new ViewDataDictionary() { { "A", 1 } };
+
+ // Act
+ ViewContext viewContext = new ViewContext(mockControllerContext.Object, view, viewData, new TempDataDictionary(), new StringWriter());
+
+ // Assert
+ Assert.Equal(1, viewContext.ViewBag.A);
+ }
+
+ [Fact]
+ public void ViewBagProperty_ReflectsNewViewDataInstance()
+ {
+ // Arrange
+ var mockControllerContext = new Mock<ControllerContext>();
+ var view = new Mock<IView>().Object;
+ var viewData = new ViewDataDictionary() { { "A", 1 } };
+
+ ViewContext viewContext = new ViewContext(mockControllerContext.Object, view, viewData, new TempDataDictionary(), new StringWriter());
+
+ // Act
+ viewContext.ViewData = new ViewDataDictionary() { { "A", "bar" } };
+
+ // Assert
+ Assert.Equal("bar", viewContext.ViewBag.A);
+ }
+
+ [Fact]
+ public void ViewBag_PropagatesChangesToViewData()
+ {
+ // Arrange
+ var mockControllerContext = new Mock<ControllerContext>();
+ var view = new Mock<IView>().Object;
+ var viewData = new ViewDataDictionary() { { "A", 1 } };
+
+ ViewContext viewContext = new ViewContext(mockControllerContext.Object, view, viewData, new TempDataDictionary(), new StringWriter());
+
+ // Act
+ viewContext.ViewBag.A = "foo";
+ viewContext.ViewBag.B = 2;
+
+ // Assert
+ Assert.Equal("foo", viewContext.ViewData["A"]);
+ Assert.Equal(2, viewContext.ViewData["B"]);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ViewDataDictionaryTest.cs b/test/System.Web.Mvc.Test/Test/ViewDataDictionaryTest.cs
new file mode 100644
index 00000000..3321af13
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ViewDataDictionaryTest.cs
@@ -0,0 +1,536 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Web.TestUtil;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ViewDataDictionaryTest
+ {
+ [Fact]
+ public void ConstructorThrowsIfDictionaryIsNull()
+ {
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ delegate { new ViewDataDictionary((ViewDataDictionary)null); }, "dictionary");
+ }
+
+ [Fact]
+ public void ConstructorWithViewDataDictionaryCopiesModelAndModelState()
+ {
+ // Arrange
+ ViewDataDictionary originalVdd = new ViewDataDictionary();
+ object model = new object();
+ originalVdd.Model = model;
+ originalVdd["foo"] = "bar";
+ originalVdd.ModelState.AddModelError("key", "error");
+
+ // Act
+ ViewDataDictionary newVdd = new ViewDataDictionary(originalVdd);
+
+ // Assert
+ Assert.Equal(model, newVdd.Model);
+ Assert.True(newVdd.ModelState.ContainsKey("key"));
+ Assert.Equal("error", newVdd.ModelState["key"].Errors[0].ErrorMessage);
+ Assert.Equal("bar", newVdd["foo"]);
+ }
+
+ [Fact]
+ public void DictionaryInterface()
+ {
+ // Arrange
+ DictionaryHelper<string, object> helper = new DictionaryHelper<string, object>()
+ {
+ Creator = () => new ViewDataDictionary(),
+ Comparer = StringComparer.OrdinalIgnoreCase,
+ SampleKeys = new string[] { "foo", "bar", "baz", "quux", "QUUX" },
+ SampleValues = new object[] { 42, "string value", new DateTime(2001, 1, 1), new object(), 32m },
+ ThrowOnKeyNotFound = false
+ };
+
+ // Act & assert
+ helper.Execute();
+ }
+
+ [Fact]
+ public void EvalReturnsSimplePropertyValue()
+ {
+ var obj = new { Foo = "Bar" };
+ ViewDataDictionary vdd = new ViewDataDictionary(obj);
+
+ Assert.Equal("Bar", vdd.Eval("Foo"));
+ }
+
+ [Fact]
+ public void EvalWithModelAndDictionaryPropertyEvaluatesDictionaryValue()
+ {
+ var obj = new { Foo = new Dictionary<string, object> { { "Bar", "Baz" } } };
+ ViewDataDictionary vdd = new ViewDataDictionary(obj);
+
+ Assert.Equal("Baz", vdd.Eval("Foo.Bar"));
+ }
+
+ [Fact]
+ public void EvalEvaluatesDictionaryThenModel()
+ {
+ var obj = new { Foo = "NotBar" };
+ ViewDataDictionary vdd = new ViewDataDictionary(obj);
+ vdd.Add("Foo", "Bar");
+
+ Assert.Equal("Bar", vdd.Eval("Foo"));
+ }
+
+ [Fact]
+ public void EvalReturnsValueOfCompoundExpressionByFollowingObjectPath()
+ {
+ var obj = new { Foo = new { Bar = "Baz" } };
+ ViewDataDictionary vdd = new ViewDataDictionary(obj);
+
+ Assert.Equal("Baz", vdd.Eval("Foo.Bar"));
+ }
+
+ [Fact]
+ public void EvalReturnsNullIfExpressionDoesNotMatch()
+ {
+ var obj = new { Foo = new { Biz = "Baz" } };
+ ViewDataDictionary vdd = new ViewDataDictionary(obj);
+
+ Assert.Equal(null, vdd.Eval("Foo.Bar"));
+ }
+
+ [Fact]
+ public void EvalReturnsValueJustAdded()
+ {
+ ViewDataDictionary vdd = new ViewDataDictionary();
+ vdd.Add("Foo", "Blah");
+
+ Assert.Equal("Blah", vdd.Eval("Foo"));
+ }
+
+ [Fact]
+ public void EvalWithCompoundExpressionReturnsIndexedValue()
+ {
+ ViewDataDictionary vdd = new ViewDataDictionary();
+ vdd.Add("Foo.Bar", "Baz");
+
+ Assert.Equal("Baz", vdd.Eval("Foo.Bar"));
+ }
+
+ [Fact]
+ public void EvalWithCompoundExpressionReturnsPropertyOfAddedObject()
+ {
+ ViewDataDictionary vdd = new ViewDataDictionary();
+ vdd.Add("Foo", new { Bar = "Baz" });
+
+ Assert.Equal("Baz", vdd.Eval("Foo.Bar"));
+ }
+
+ [Fact]
+ public void EvalWithCompoundIndexExpressionReturnsEval()
+ {
+ ViewDataDictionary vdd = new ViewDataDictionary();
+ vdd.Add("Foo.Bar", new { Baz = "Quux" });
+
+ Assert.Equal("Quux", vdd.Eval("Foo.Bar.Baz"));
+ }
+
+ [Fact]
+ public void EvalWithCompoundIndexAndCompoundExpressionReturnsValue()
+ {
+ ViewDataDictionary vdd = new ViewDataDictionary();
+ vdd.Add("Foo.Bar", new { Baz = new { Blah = "Quux" } });
+
+ Assert.Equal("Quux", vdd.Eval("Foo.Bar.Baz.Blah"));
+ }
+
+ /// <summary>
+ /// Make sure that dict["foo.bar"] gets chosen before dict["foo"]["bar"]
+ /// </summary>
+ [Fact]
+ public void EvalChoosesValueInDictionaryOverOtherValue()
+ {
+ ViewDataDictionary vdd = new ViewDataDictionary();
+ vdd.Add("Foo", new { Bar = "Not Baz" });
+ vdd.Add("Foo.Bar", "Baz");
+
+ Assert.Equal("Baz", vdd.Eval("Foo.Bar"));
+ }
+
+ /// <summary>
+ /// Make sure that dict["foo.bar"]["baz"] gets chosen before dict["foo"]["bar"]["baz"]
+ /// </summary>
+ [Fact]
+ public void EvalChoosesCompoundValueInDictionaryOverOtherValues()
+ {
+ ViewDataDictionary vdd = new ViewDataDictionary();
+ vdd.Add("Foo", new { Bar = new { Baz = "Not Quux" } });
+ vdd.Add("Foo.Bar", new { Baz = "Quux" });
+
+ Assert.Equal("Quux", vdd.Eval("Foo.Bar.Baz"));
+ }
+
+ /// <summary>
+ /// Make sure that dict["foo.bar"]["baz"] gets chosen before dict["foo"]["bar.baz"]
+ /// </summary>
+ [Fact]
+ public void EvalChoosesCompoundValueInDictionaryOverOtherValuesWithCompoundProperty()
+ {
+ ViewDataDictionary vdd = new ViewDataDictionary();
+ vdd.Add("Foo", new Person());
+ vdd.Add("Foo.Bar", new { Baz = "Quux" });
+
+ Assert.Equal("Quux", vdd.Eval("Foo.Bar.Baz"));
+ }
+
+ [Fact]
+ public void EvalThrowsIfExpressionIsEmpty()
+ {
+ // Arrange
+ ViewDataDictionary vdd = new ViewDataDictionary();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { vdd.Eval(String.Empty); }, "expression");
+ }
+
+ [Fact]
+ public void EvalThrowsIfExpressionIsNull()
+ {
+ // Arrange
+ ViewDataDictionary vdd = new ViewDataDictionary();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ delegate { vdd.Eval(null); }, "expression");
+ }
+
+ [Fact]
+ public void EvalWithCompoundExpressionAndDictionarySubExpressionChoosesDictionaryValue()
+ {
+ ViewDataDictionary vdd = new ViewDataDictionary();
+ vdd.Add("Foo", new Dictionary<string, object> { { "Bar", "Baz" } });
+
+ Assert.Equal("Baz", vdd.Eval("Foo.Bar"));
+ }
+
+ [Fact]
+ public void EvalWithDictionaryAndNoMatchReturnsNull()
+ {
+ ViewDataDictionary vdd = new ViewDataDictionary();
+ vdd.Add("Foo", new Dictionary<string, object> { { "NotBar", "Baz" } });
+
+ object result = vdd.Eval("Foo.Bar");
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void EvalWithNestedDictionariesEvalCorrectly()
+ {
+ ViewDataDictionary vdd = new ViewDataDictionary();
+ vdd.Add("Foo", new Dictionary<string, object> { { "Bar", new Hashtable { { "Baz", "Quux" } } } });
+
+ Assert.Equal("Quux", vdd.Eval("Foo.Bar.Baz"));
+ }
+
+ [Fact]
+ public void EvalFormatWithNullValueReturnsEmptyString()
+ {
+ // Arrange
+ ViewDataDictionary vdd = new ViewDataDictionary();
+
+ // Act
+ string formattedValue = vdd.Eval("foo", "for{0}mat");
+
+ // Assert
+ Assert.Equal(String.Empty, formattedValue);
+ }
+
+ [Fact]
+ public void EvalFormatWithEmptyFormatReturnsViewData()
+ {
+ // Arrange
+ ViewDataDictionary vdd = new ViewDataDictionary();
+ vdd["foo"] = "value";
+
+ // Act
+ string formattedValue = vdd.Eval("foo", "");
+
+ // Assert
+ Assert.Equal("value", formattedValue);
+ }
+
+ [Fact]
+ public void EvalFormatWithFormatReturnsFormattedViewData()
+ {
+ // Arrange
+ ViewDataDictionary vdd = new ViewDataDictionary();
+ vdd["foo"] = "value";
+
+ // Act
+ string formattedValue = vdd.Eval("foo", "for{0}mat");
+
+ // Assert
+ Assert.Equal("forvaluemat", formattedValue);
+ }
+
+ [Fact]
+ public void EvalPropertyNamedModel()
+ {
+ // Arrange
+ ViewDataDictionary vdd = new ViewDataDictionary();
+ vdd["Title"] = "Home Page";
+ vdd["Message"] = "Welcome to ASP.NET MVC!";
+ vdd.Model = new TheQueryStringParam
+ {
+ Name = "The Name",
+ Value = "The Value",
+ Model = "The Model",
+ };
+
+ // Act
+ object o = vdd.Eval("Model");
+
+ // Assert
+ Assert.Equal("The Model", o);
+ }
+
+ [Fact]
+ public void EvalSubPropertyNamedValueInModel()
+ {
+ // Arrange
+ ViewDataDictionary vdd = new ViewDataDictionary();
+ vdd["Title"] = "Home Page";
+ vdd["Message"] = "Welcome to ASP.NET MVC!";
+ vdd.Model = new TheQueryStringParam
+ {
+ Name = "The Name",
+ Value = "The Value",
+ Model = "The Model",
+ };
+
+ // Act
+ object o = vdd.Eval("Value");
+
+ // Assert
+ Assert.Equal("The Value", o);
+ }
+
+ [Fact]
+ public void GetViewDataInfoFromDictionary()
+ {
+ // Arrange
+ ViewDataDictionary fooVdd = new ViewDataDictionary()
+ {
+ { "Bar", "barValue" }
+ };
+ ViewDataDictionary vdd = new ViewDataDictionary()
+ {
+ { "Foo", fooVdd }
+ };
+
+ // Act
+ ViewDataInfo info = vdd.GetViewDataInfo("foo.bar");
+
+ // Assert
+ Assert.NotNull(info);
+ Assert.Equal(fooVdd, info.Container);
+ Assert.Equal("barValue", info.Value);
+ }
+
+ [Fact]
+ public void GetViewDataInfoFromDictionaryWithMissingEntry()
+ {
+ // Arrange
+ ViewDataDictionary vdd = new ViewDataDictionary();
+
+ // Act
+ ViewDataInfo info = vdd.GetViewDataInfo("foo");
+
+ // Assert
+ Assert.Null(info);
+ }
+
+ [Fact]
+ public void GetViewDataInfoFromDictionaryWithNullEntry()
+ {
+ // Arrange
+ ViewDataDictionary vdd = new ViewDataDictionary()
+ {
+ { "Foo", null }
+ };
+
+ // Act
+ ViewDataInfo info = vdd.GetViewDataInfo("foo");
+
+ // Assert
+ Assert.NotNull(info);
+ Assert.Equal(vdd, info.Container);
+ Assert.Null(info.Value);
+ }
+
+ [Fact]
+ public void GetViewDataInfoFromModel()
+ {
+ // Arrange
+ object model = new { foo = "fooValue" };
+ ViewDataDictionary vdd = new ViewDataDictionary(model);
+
+ PropertyDescriptor propDesc = TypeDescriptor.GetProperties(model).Find("foo", true /* ignoreCase */);
+
+ // Act
+ ViewDataInfo info = vdd.GetViewDataInfo("foo");
+
+ // Assert
+ Assert.NotNull(info);
+ Assert.Equal(model, info.Container);
+ Assert.Equal(propDesc, info.PropertyDescriptor);
+ Assert.Equal("fooValue", info.Value);
+ }
+
+ [Fact]
+ public void FabricatesModelMetadataFromModelWhenModelMetadataHasNotBeenSet()
+ {
+ // Arrange
+ object model = new { foo = "fooValue", bar = "barValue" };
+ ViewDataDictionary vdd = new ViewDataDictionary(model);
+
+ // Act
+ ModelMetadata metadata = vdd.ModelMetadata;
+
+ // Assert
+ Assert.NotNull(metadata);
+ Assert.Equal(model.GetType(), metadata.ModelType);
+ }
+
+ [Fact]
+ public void ReturnsExistingModelMetadata()
+ {
+ // Arrange
+ object model = new { foo = "fooValue", bar = "barValue" };
+ ModelMetadata originalMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, model.GetType());
+ ViewDataDictionary vdd = new ViewDataDictionary(model) { ModelMetadata = originalMetadata };
+
+ // Act
+ ModelMetadata metadata = vdd.ModelMetadata;
+
+ // Assert
+ Assert.Same(originalMetadata, metadata);
+ }
+
+ [Fact]
+ public void ModelMetadataIsNullIfModelMetadataHasNotBeenSetAndModelIsNull()
+ {
+ // Arrange
+ ViewDataDictionary vdd = new ViewDataDictionary();
+
+ // Act
+ ModelMetadata metadata = vdd.ModelMetadata;
+
+ // Assert
+ Assert.Null(metadata);
+ }
+
+ [Fact]
+ public void ModelMetadataCanBeFabricatedWithNullModelAndGenericViewDataDictionary()
+ {
+ // Arrange
+ ViewDataDictionary vdd = new ViewDataDictionary<Exception>();
+
+ // Act
+ ModelMetadata metadata = vdd.ModelMetadata;
+
+ // Assert
+ Assert.NotNull(metadata);
+ Assert.Equal(typeof(Exception), metadata.ModelType);
+ }
+
+ [Fact]
+ public void ModelSetterThrowsIfValueIsNullAndModelTypeIsNonNullable()
+ {
+ // Arrange
+ ViewDataDictionary vdd = new ViewDataDictionary<int>();
+
+ // Act & assert
+ Assert.Throws<InvalidOperationException>(
+ delegate { vdd.Model = null; },
+ @"The model item passed into the dictionary is null, but this dictionary requires a non-null model item of type 'System.Int32'.");
+ }
+
+ [Fact]
+ public void ChangingModelReplacesModelMetadata()
+ {
+ // Arrange
+ ViewDataDictionary vdd = new ViewDataDictionary(new Object());
+ ModelMetadata originalMetadata = vdd.ModelMetadata;
+
+ // Act
+ vdd.Model = "New Model";
+
+ // Assert
+ Assert.NotSame(originalMetadata, vdd.ModelMetadata);
+ }
+
+ public class TheQueryStringParam
+ {
+ public string Name { get; set; }
+ public string Value { get; set; }
+ public string Model { get; set; }
+ }
+
+ public class Person : CustomTypeDescriptor
+ {
+ public override PropertyDescriptorCollection GetProperties()
+ {
+ return new PropertyDescriptorCollection(new PersonPropertyDescriptor[] { new PersonPropertyDescriptor() });
+ }
+ }
+
+ public class PersonPropertyDescriptor : PropertyDescriptor
+ {
+ public PersonPropertyDescriptor()
+ : base("Bar.Baz", null)
+ {
+ }
+
+ public override object GetValue(object component)
+ {
+ return "Quux";
+ }
+
+ public override bool CanResetValue(object component)
+ {
+ return false;
+ }
+
+ public override Type ComponentType
+ {
+ get { return typeof(Person); }
+ }
+
+ public override bool IsReadOnly
+ {
+ get { return false; }
+ }
+
+ public override Type PropertyType
+ {
+ get { return typeof(string); }
+ }
+
+ public override void ResetValue(object component)
+ {
+ }
+
+ public override void SetValue(object component, object value)
+ {
+ }
+
+ public override bool ShouldSerializeValue(object component)
+ {
+ return true;
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ViewDataInfoTest.cs b/test/System.Web.Mvc.Test/Test/ViewDataInfoTest.cs
new file mode 100644
index 00000000..f2293550
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ViewDataInfoTest.cs
@@ -0,0 +1,64 @@
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class ViewDataInfoTest
+ {
+ [Fact]
+ public void ViewDataInfoDoesNotCallAccessorUntilValuePropertyAccessed()
+ {
+ // Arrange
+ bool called = false;
+ ViewDataInfo vdi = new ViewDataInfo(() =>
+ {
+ called = true;
+ return 21;
+ });
+
+ // Act & Assert
+ Assert.False(called);
+ object result = vdi.Value;
+ Assert.True(called);
+ Assert.Equal(21, result);
+ }
+
+ [Fact]
+ public void AccessorIsOnlyCalledOnce()
+ {
+ // Arrange
+ int callCount = 0;
+ ViewDataInfo vdi = new ViewDataInfo(() =>
+ {
+ ++callCount;
+ return null;
+ });
+
+ // Act & Assert
+ Assert.Equal(0, callCount);
+ object unused;
+ unused = vdi.Value;
+ unused = vdi.Value;
+ unused = vdi.Value;
+ Assert.Equal(1, callCount);
+ }
+
+ [Fact]
+ public void SettingExplicitValueOverridesAccessorMethod()
+ {
+ // Arrange
+ bool called = false;
+ ViewDataInfo vdi = new ViewDataInfo(() =>
+ {
+ called = true;
+ return null;
+ });
+
+ // Act & Assert
+ Assert.False(called);
+ vdi.Value = 42;
+ object result = vdi.Value;
+ Assert.False(called);
+ Assert.Equal(42, result);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ViewEngineCollectionTest.cs b/test/System.Web.Mvc.Test/Test/ViewEngineCollectionTest.cs
new file mode 100644
index 00000000..331e37f7
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ViewEngineCollectionTest.cs
@@ -0,0 +1,631 @@
+using System.Collections.Generic;
+using System.Linq;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ViewEngineCollectionTest
+ {
+ [Fact]
+ public void ListWrappingConstructor()
+ {
+ // Arrange
+ List<IViewEngine> list = new List<IViewEngine>() { new Mock<IViewEngine>().Object, new Mock<IViewEngine>().Object };
+
+ // Act
+ ViewEngineCollection collection = new ViewEngineCollection(list);
+
+ // Assert
+ Assert.Equal(2, collection.Count);
+ Assert.Same(list[0], collection[0]);
+ Assert.Same(list[1], collection[1]);
+ }
+
+ [Fact]
+ public void ListWrappingConstructorThrowsIfListIsNull()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { new ViewEngineCollection((IList<IViewEngine>)null); },
+ "list");
+ }
+
+ [Fact]
+ public void DefaultConstructor()
+ {
+ // Act
+ ViewEngineCollection collection = new ViewEngineCollection();
+
+ // Assert
+ Assert.Empty(collection);
+ }
+
+ [Fact]
+ public void AddNullViewEngineThrows()
+ {
+ // Arrange
+ ViewEngineCollection collection = new ViewEngineCollection();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { collection.Add(null); },
+ "item");
+ }
+
+ [Fact]
+ public void SetNullViewEngineThrows()
+ {
+ // Arrange
+ ViewEngineCollection collection = new ViewEngineCollection();
+ collection.Add(new Mock<IViewEngine>().Object);
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { collection[0] = null; },
+ "item");
+ }
+
+ [Fact]
+ public void FindPartialViewAggregatesAllSearchedLocationsIfAllEnginesFail()
+ {
+ // Arrange
+ ControllerContext context = new Mock<ControllerContext>().Object;
+ ViewEngineCollection viewEngineCollection = new ViewEngineCollection();
+ Mock<IViewEngine> engine1 = new Mock<IViewEngine>();
+ ViewEngineResult engine1Result = new ViewEngineResult(new[] { "location1", "location2" });
+ engine1.Setup(e => e.FindPartialView(context, "partial", It.IsAny<bool>())).Returns(engine1Result);
+ Mock<IViewEngine> engine2 = new Mock<IViewEngine>();
+ ViewEngineResult engine2Result = new ViewEngineResult(new[] { "location3", "location4" });
+ engine2.Setup(e => e.FindPartialView(context, "partial", It.IsAny<bool>())).Returns(engine2Result);
+ viewEngineCollection.Add(engine1.Object);
+ viewEngineCollection.Add(engine2.Object);
+
+ // Act
+ ViewEngineResult result = viewEngineCollection.FindPartialView(context, "partial");
+
+ // Assert
+ Assert.Null(result.View);
+ Assert.Equal(4, result.SearchedLocations.Count());
+ Assert.True(result.SearchedLocations.Contains("location1"));
+ Assert.True(result.SearchedLocations.Contains("location2"));
+ Assert.True(result.SearchedLocations.Contains("location3"));
+ Assert.True(result.SearchedLocations.Contains("location4"));
+ }
+
+ [Fact]
+ public void FindPartialViewFailureWithOneEngine()
+ {
+ // Arrange
+ ControllerContext context = new Mock<ControllerContext>().Object;
+ ViewEngineCollection collection = new ViewEngineCollection();
+ Mock<IViewEngine> engine = new Mock<IViewEngine>();
+ ViewEngineResult engineResult = new ViewEngineResult(new[] { "location1", "location2" });
+ engine.Setup(e => e.FindPartialView(context, "partial", It.IsAny<bool>())).Returns(engineResult);
+ collection.Add(engine.Object);
+
+ // Act
+ ViewEngineResult result = collection.FindPartialView(context, "partial");
+
+ // Assert
+ Assert.Null(result.View);
+ Assert.Equal(2, result.SearchedLocations.Count());
+ Assert.True(result.SearchedLocations.Contains("location1"));
+ Assert.True(result.SearchedLocations.Contains("location2"));
+ }
+
+ [Fact]
+ public void FindPartialViewLooksAtCacheFirst()
+ {
+ // Arrange
+ ControllerContext context = new Mock<ControllerContext>().Object;
+ Mock<IViewEngine> engine = new Mock<IViewEngine>();
+ ViewEngineResult engineResult = new ViewEngineResult(new Mock<IView>().Object, engine.Object);
+ engine.Setup(e => e.FindPartialView(context, "partial", true)).Returns(engineResult);
+ ViewEngineCollection collection = new ViewEngineCollection()
+ {
+ engine.Object,
+ };
+
+ // Act
+ ViewEngineResult result = collection.FindPartialView(context, "partial");
+
+ // Assert
+ Assert.Same(engineResult, result);
+ engine.Verify(e => e.FindPartialView(context, "partial", true), Times.Once());
+ engine.Verify(e => e.FindPartialView(context, "partial", false), Times.Never());
+ }
+
+ [Fact]
+ public void FindPartialViewLooksAtLocatorIfCacheEmpty()
+ {
+ // Arrange
+ ControllerContext context = new Mock<ControllerContext>().Object;
+ Mock<IViewEngine> engine = new Mock<IViewEngine>();
+ ViewEngineResult engineResult = new ViewEngineResult(new Mock<IView>().Object, engine.Object);
+ engine.Setup(e => e.FindPartialView(context, "partial", true)).Returns(new ViewEngineResult(new[] { "path" }));
+ engine.Setup(e => e.FindPartialView(context, "partial", false)).Returns(engineResult);
+ ViewEngineCollection collection = new ViewEngineCollection()
+ {
+ engine.Object,
+ };
+
+ // Act
+ ViewEngineResult result = collection.FindPartialView(context, "partial");
+
+ // Assert
+ Assert.Same(engineResult, result);
+ engine.Verify(e => e.FindPartialView(context, "partial", true), Times.Once());
+ engine.Verify(e => e.FindPartialView(context, "partial", false), Times.Once());
+ }
+
+ [Fact]
+ public void FindPartialViewIgnoresSearchLocationsFromCache()
+ {
+ // Arrange
+ ControllerContext context = new Mock<ControllerContext>().Object;
+ Mock<IViewEngine> engine = new Mock<IViewEngine>();
+ engine.Setup(e => e.FindPartialView(context, "partial", true)).Returns(new ViewEngineResult(new[] { "cachePath" }));
+ engine.Setup(e => e.FindPartialView(context, "partial", false)).Returns(new ViewEngineResult(new[] { "locatorPath" }));
+ ViewEngineCollection collection = new ViewEngineCollection()
+ {
+ engine.Object,
+ };
+
+ // Act
+ ViewEngineResult result = collection.FindPartialView(context, "partial");
+
+ // Assert
+ string searchedLocation = Assert.Single(result.SearchedLocations);
+ Assert.Equal("locatorPath", searchedLocation);
+ engine.Verify(e => e.FindPartialView(context, "partial", true), Times.Once());
+ engine.Verify(e => e.FindPartialView(context, "partial", false), Times.Once());
+ }
+
+ [Fact]
+ public void FindPartialViewIteratesThroughCollectionUntilFindsSuccessfulEngine()
+ {
+ // Arrange
+ ControllerContext context = new Mock<ControllerContext>().Object;
+ ViewEngineCollection collection = new ViewEngineCollection();
+ Mock<IViewEngine> engine1 = new Mock<IViewEngine>();
+ ViewEngineResult engine1Result = new ViewEngineResult(new[] { "location1", "location2" });
+ engine1.Setup(e => e.FindPartialView(context, "partial", It.IsAny<bool>())).Returns(engine1Result);
+ Mock<IViewEngine> engine2 = new Mock<IViewEngine>();
+ ViewEngineResult engine2Result = new ViewEngineResult(new Mock<IView>().Object, engine2.Object);
+ engine2.Setup(e => e.FindPartialView(context, "partial", It.IsAny<bool>())).Returns(engine2Result);
+ collection.Add(engine1.Object);
+ collection.Add(engine2.Object);
+
+ // Act
+ ViewEngineResult result = collection.FindPartialView(context, "partial");
+
+ // Assert
+ Assert.Same(engine2Result, result);
+ }
+
+ [Fact]
+ public void FindPartialViewRemovesDuplicateSearchedLocationsFromMultipleEngines()
+ {
+ // Arrange
+ ControllerContext context = new Mock<ControllerContext>().Object;
+ Mock<IViewEngine> engine1 = new Mock<IViewEngine>();
+ ViewEngineResult engine1Result = new ViewEngineResult(new[] { "repeatLocation", "location1" });
+ engine1.Setup(e => e.FindPartialView(context, "partial", It.IsAny<bool>())).Returns(engine1Result);
+ Mock<IViewEngine> engine2 = new Mock<IViewEngine>();
+ ViewEngineResult engine2Result = new ViewEngineResult(new[] { "location2", "repeatLocation" });
+ engine2.Setup(e => e.FindPartialView(context, "partial", It.IsAny<bool>())).Returns(engine2Result);
+ ViewEngineCollection viewEngineCollection = new ViewEngineCollection()
+ {
+ engine1.Object,
+ engine2.Object,
+ };
+
+ // Act
+ ViewEngineResult result = viewEngineCollection.FindPartialView(context, "partial");
+
+ // Assert
+ var expectedLocations = new[] { "repeatLocation", "location1", "location2" };
+ Assert.Null(result.View);
+ Assert.Equal(expectedLocations, result.SearchedLocations.ToArray());
+ }
+
+ [Fact]
+ public void FindPartialViewReturnsNoViewAndEmptySearchedLocationsIfCollectionEmpty()
+ {
+ // Arrange
+ ControllerContext context = new Mock<ControllerContext>().Object;
+ ViewEngineCollection collection = new ViewEngineCollection();
+
+ // Act
+ ViewEngineResult result = collection.FindPartialView(context, "partial");
+
+ // Assert
+ Assert.Null(result.View);
+ Assert.Empty(result.SearchedLocations);
+ }
+
+ [Fact]
+ public void FindPartialViewReturnsValueFromFirstSuccessfulEngine()
+ {
+ // Arrange
+ ControllerContext context = new Mock<ControllerContext>().Object;
+ ViewEngineCollection collection = new ViewEngineCollection();
+ Mock<IViewEngine> engine1 = new Mock<IViewEngine>();
+ ViewEngineResult engine1Result = new ViewEngineResult(new Mock<IView>().Object, engine1.Object);
+ engine1.Setup(e => e.FindPartialView(context, "partial", It.IsAny<bool>())).Returns(engine1Result);
+ Mock<IViewEngine> engine2 = new Mock<IViewEngine>();
+ ViewEngineResult engine2Result = new ViewEngineResult(new Mock<IView>().Object, engine2.Object);
+ engine2.Setup(e => e.FindPartialView(context, "partial", It.IsAny<bool>())).Returns(engine2Result);
+ collection.Add(engine1.Object);
+ collection.Add(engine2.Object);
+
+ // Act
+ ViewEngineResult result = collection.FindPartialView(context, "partial");
+
+ // Assert
+ Assert.Same(engine1Result, result);
+ }
+
+ [Fact]
+ public void FindPartialViewSuccessWithOneEngine()
+ {
+ // Arrange
+ ControllerContext context = new Mock<ControllerContext>().Object;
+ ViewEngineCollection collection = new ViewEngineCollection();
+ Mock<IViewEngine> engine = new Mock<IViewEngine>();
+ ViewEngineResult engineResult = new ViewEngineResult(new Mock<IView>().Object, engine.Object);
+ engine.Setup(e => e.FindPartialView(context, "partial", It.IsAny<bool>())).Returns(engineResult);
+ collection.Add(engine.Object);
+
+ // Act
+ ViewEngineResult result = collection.FindPartialView(context, "partial");
+
+ // Assert
+ Assert.Same(engineResult, result);
+ }
+
+ [Fact]
+ public void FindPartialViewThrowsIfPartialViewNameIsEmpty()
+ {
+ // Arrange
+ ControllerContext context = new Mock<ControllerContext>().Object;
+ ViewEngineCollection collection = new ViewEngineCollection();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ () => collection.FindPartialView(context, ""),
+ "partialViewName");
+ }
+
+ [Fact]
+ public void FindPartialViewThrowsIfPartialViewNameIsNull()
+ {
+ // Arrange
+ ControllerContext context = new Mock<ControllerContext>().Object;
+ ViewEngineCollection collection = new ViewEngineCollection();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ () => collection.FindPartialView(context, null),
+ "partialViewName");
+ }
+
+ [Fact]
+ public void FindPartialViewThrowsIfControllerContextIsNull()
+ {
+ // Arrange
+ ViewEngineCollection collection = new ViewEngineCollection();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => collection.FindPartialView(null, "partial"),
+ "controllerContext");
+ }
+
+ [Fact]
+ public void FindViewAggregatesAllSearchedLocationsIfAllEnginesFail()
+ {
+ // Arrange
+ ControllerContext context = new Mock<ControllerContext>().Object;
+ ViewEngineCollection collection = new ViewEngineCollection();
+ Mock<IViewEngine> engine1 = new Mock<IViewEngine>();
+ ViewEngineResult engine1Result = new ViewEngineResult(new[] { "location1", "location2" });
+ engine1.Setup(e => e.FindView(context, "view", "master", It.IsAny<bool>())).Returns(engine1Result);
+ Mock<IViewEngine> engine2 = new Mock<IViewEngine>();
+ ViewEngineResult engine2Result = new ViewEngineResult(new[] { "location3", "location4" });
+ engine2.Setup(e => e.FindView(context, "view", "master", It.IsAny<bool>())).Returns(engine2Result);
+ collection.Add(engine1.Object);
+ collection.Add(engine2.Object);
+
+ // Act
+ ViewEngineResult result = collection.FindView(context, "view", "master");
+
+ // Assert
+ Assert.Null(result.View);
+ Assert.Equal(4, result.SearchedLocations.Count());
+ Assert.True(result.SearchedLocations.Contains("location1"));
+ Assert.True(result.SearchedLocations.Contains("location2"));
+ Assert.True(result.SearchedLocations.Contains("location3"));
+ Assert.True(result.SearchedLocations.Contains("location4"));
+ }
+
+ [Fact]
+ public void FindViewFailureWithOneEngine()
+ {
+ // Arrange
+ ControllerContext context = new Mock<ControllerContext>().Object;
+ ViewEngineCollection collection = new ViewEngineCollection();
+ Mock<IViewEngine> engine = new Mock<IViewEngine>();
+ ViewEngineResult engineResult = new ViewEngineResult(new[] { "location1", "location2" });
+ engine.Setup(e => e.FindView(context, "view", "master", It.IsAny<bool>())).Returns(engineResult);
+ collection.Add(engine.Object);
+
+ // Act
+ ViewEngineResult result = collection.FindView(context, "view", "master");
+
+ // Assert
+ Assert.Null(result.View);
+ Assert.Equal(2, result.SearchedLocations.Count());
+ Assert.True(result.SearchedLocations.Contains("location1"));
+ Assert.True(result.SearchedLocations.Contains("location2"));
+ }
+
+ [Fact]
+ public void FindViewLooksAtCacheFirst()
+ {
+ // Arrange
+ ControllerContext context = new Mock<ControllerContext>().Object;
+ Mock<IViewEngine> engine = new Mock<IViewEngine>();
+ ViewEngineResult engineResult = new ViewEngineResult(new Mock<IView>().Object, engine.Object);
+ engine.Setup(e => e.FindView(context, "view", "master", true)).Returns(engineResult);
+ ViewEngineCollection collection = new ViewEngineCollection()
+ {
+ engine.Object,
+ };
+
+ // Act
+ ViewEngineResult result = collection.FindView(context, "view", "master");
+
+ // Assert
+ Assert.Same(engineResult, result);
+ engine.Verify(e => e.FindView(context, "view", "master", true), Times.Once());
+ engine.Verify(e => e.FindView(context, "view", "master", false), Times.Never());
+ }
+
+ [Fact]
+ public void FindViewLooksAtLocatorIfCacheEmpty()
+ {
+ // Arrange
+ ControllerContext context = new Mock<ControllerContext>().Object;
+ Mock<IViewEngine> engine = new Mock<IViewEngine>();
+ ViewEngineResult engineResult = new ViewEngineResult(new Mock<IView>().Object, engine.Object);
+ engine.Setup(e => e.FindView(context, "view", "master", true)).Returns(new ViewEngineResult(new[] { "path" }));
+ engine.Setup(e => e.FindView(context, "view", "master", false)).Returns(engineResult);
+ ViewEngineCollection collection = new ViewEngineCollection()
+ {
+ engine.Object,
+ };
+
+ // Act
+ ViewEngineResult result = collection.FindView(context, "view", "master");
+
+ // Assert
+ Assert.Same(engineResult, result);
+ engine.Verify(e => e.FindView(context, "view", "master", true), Times.Once());
+ engine.Verify(e => e.FindView(context, "view", "master", false), Times.Once());
+ }
+
+ [Fact]
+ public void FindViewIgnoresSearchLocationsFromCache()
+ {
+ // Arrange
+ ControllerContext context = new Mock<ControllerContext>().Object;
+ Mock<IViewEngine> engine = new Mock<IViewEngine>();
+ engine.Setup(e => e.FindView(context, "view", "master", true)).Returns(new ViewEngineResult(new[] { "cachePath" }));
+ engine.Setup(e => e.FindView(context, "view", "master", false)).Returns(new ViewEngineResult(new[] { "locatorPath" }));
+ ViewEngineCollection collection = new ViewEngineCollection()
+ {
+ engine.Object,
+ };
+
+ // Act
+ ViewEngineResult result = collection.FindView(context, "view", "master");
+
+ // Assert
+ string searchedLocation = Assert.Single(result.SearchedLocations);
+ Assert.Equal("locatorPath", searchedLocation);
+ engine.Verify(e => e.FindView(context, "view", "master", true), Times.Once());
+ engine.Verify(e => e.FindView(context, "view", "master", false), Times.Once());
+ }
+
+ [Fact]
+ public void FindViewIteratesThroughCollectionUntilFindsSuccessfulEngine()
+ {
+ // Arrange
+ ControllerContext context = new Mock<ControllerContext>().Object;
+ ViewEngineCollection collection = new ViewEngineCollection();
+ Mock<IViewEngine> engine1 = new Mock<IViewEngine>();
+ ViewEngineResult engine1Result = new ViewEngineResult(new[] { "location1", "location2" });
+ engine1.Setup(e => e.FindView(context, "view", "master", It.IsAny<bool>())).Returns(engine1Result);
+ Mock<IViewEngine> engine2 = new Mock<IViewEngine>();
+ ViewEngineResult engine2Result = new ViewEngineResult(new Mock<IView>().Object, engine2.Object);
+ engine2.Setup(e => e.FindView(context, "view", "master", It.IsAny<bool>())).Returns(engine2Result);
+ collection.Add(engine1.Object);
+ collection.Add(engine2.Object);
+
+ // Act
+ ViewEngineResult result = collection.FindView(context, "view", "master");
+
+ // Assert
+ Assert.Same(engine2Result, result);
+ }
+
+ [Fact]
+ public void FindViewRemovesDuplicateSearchedLocationsFromMultipleEngines()
+ {
+ // Arrange
+ ControllerContext context = new Mock<ControllerContext>().Object;
+ Mock<IViewEngine> engine1 = new Mock<IViewEngine>();
+ ViewEngineResult engine1Result = new ViewEngineResult(new[] { "repeatLocation", "location1" });
+ engine1.Setup(e => e.FindView(context, "view", "master", It.IsAny<bool>())).Returns(engine1Result);
+ Mock<IViewEngine> engine2 = new Mock<IViewEngine>();
+ ViewEngineResult engine2Result = new ViewEngineResult(new[] { "location2", "repeatLocation" });
+ engine2.Setup(e => e.FindView(context, "view", "master", It.IsAny<bool>())).Returns(engine2Result);
+ ViewEngineCollection collection = new ViewEngineCollection()
+ {
+ engine1.Object,
+ engine2.Object,
+ };
+
+ // Act
+ ViewEngineResult result = collection.FindView(context, "view", "master");
+
+ // Assert
+ Assert.Null(result.View);
+ var expectedLocations = new[] { "repeatLocation", "location1", "location2" };
+ Assert.Equal(expectedLocations, result.SearchedLocations.ToArray());
+ }
+
+ [Fact]
+ public void FindViewReturnsNoViewAndEmptySearchedLocationsIfCollectionEmpty()
+ {
+ // Arrange
+ ControllerContext context = new Mock<ControllerContext>().Object;
+ ViewEngineCollection collection = new ViewEngineCollection();
+
+ // Act
+ ViewEngineResult result = collection.FindView(context, "view", null);
+
+ // Assert
+ Assert.Null(result.View);
+ Assert.Empty(result.SearchedLocations);
+ }
+
+ [Fact]
+ public void FindViewReturnsValueFromFirstSuccessfulEngine()
+ {
+ // Arrange
+ ControllerContext context = new Mock<ControllerContext>().Object;
+ ViewEngineCollection collection = new ViewEngineCollection();
+ Mock<IViewEngine> engine1 = new Mock<IViewEngine>();
+ ViewEngineResult engine1Result = new ViewEngineResult(new Mock<IView>().Object, engine1.Object);
+ engine1.Setup(e => e.FindView(context, "view", "master", It.IsAny<bool>())).Returns(engine1Result);
+ Mock<IViewEngine> engine2 = new Mock<IViewEngine>();
+ ViewEngineResult engine2Result = new ViewEngineResult(new Mock<IView>().Object, engine2.Object);
+ engine2.Setup(e => e.FindView(context, "view", "master", It.IsAny<bool>())).Returns(engine2Result);
+ collection.Add(engine1.Object);
+ collection.Add(engine2.Object);
+
+ // Act
+ ViewEngineResult result = collection.FindView(context, "view", "master");
+
+ // Assert
+ Assert.Same(engine1Result, result);
+ }
+
+ [Fact]
+ public void FindViewSuccessWithOneEngine()
+ {
+ // Arrange
+ ControllerContext context = new Mock<ControllerContext>().Object;
+ ViewEngineCollection collection = new ViewEngineCollection();
+ Mock<IViewEngine> engine = new Mock<IViewEngine>();
+ ViewEngineResult engineResult = new ViewEngineResult(new Mock<IView>().Object, engine.Object);
+ engine.Setup(e => e.FindView(context, "view", "master", It.IsAny<bool>())).Returns(engineResult);
+ collection.Add(engine.Object);
+
+ // Act
+ ViewEngineResult result = collection.FindView(context, "view", "master");
+
+ // Assert
+ Assert.Same(engineResult, result);
+ }
+
+ [Fact]
+ public void FindViewThrowsIfControllerContextIsNull()
+ {
+ // Arrange
+ ViewEngineCollection collection = new ViewEngineCollection();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => collection.FindView(null, "view", null),
+ "controllerContext"
+ );
+ }
+
+ [Fact]
+ public void FindViewThrowsIfViewNameIsEmpty()
+ {
+ // Arrange
+ ControllerContext context = new Mock<ControllerContext>().Object;
+ ViewEngineCollection collection = new ViewEngineCollection();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ () => collection.FindView(context, "", null),
+ "viewName"
+ );
+ }
+
+ [Fact]
+ public void FindViewThrowsIfViewNameIsNull()
+ {
+ // Arrange
+ ControllerContext context = new Mock<ControllerContext>().Object;
+ ViewEngineCollection collection = new ViewEngineCollection();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ () => collection.FindView(context, null, null),
+ "viewName"
+ );
+ }
+
+ [Fact]
+ public void FindViewDelegatesToResolver()
+ {
+ // Arrange
+ Mock<IView> view = new Mock<IView>();
+ ControllerContext context = new ControllerContext();
+ Mock<IViewEngine> locatedEngine = new Mock<IViewEngine>();
+ ViewEngineResult engineResult = new ViewEngineResult(view.Object, locatedEngine.Object);
+ locatedEngine.Setup(e => e.FindView(context, "ViewName", "MasterName", true))
+ .Returns(engineResult);
+ Mock<IViewEngine> secondEngine = new Mock<IViewEngine>();
+ Resolver<IEnumerable<IViewEngine>> resolver = new Resolver<IEnumerable<IViewEngine>> { Current = new IViewEngine[] { locatedEngine.Object, secondEngine.Object } };
+ ViewEngineCollection engines = new ViewEngineCollection(resolver);
+
+ // Act
+ ViewEngineResult result = engines.FindView(context, "ViewName", "MasterName");
+
+ // Assert
+ Assert.Same(engineResult, result);
+ secondEngine.Verify(e => e.FindView(context, "ViewName", "MasterName", It.IsAny<bool>()), Times.Never());
+ }
+
+ [Fact]
+ public void FindPartialViewDelegatesToResolver()
+ {
+ // Arrange
+ Mock<IView> view = new Mock<IView>();
+ ControllerContext context = new ControllerContext();
+ Mock<IViewEngine> locatedEngine = new Mock<IViewEngine>();
+ ViewEngineResult engineResult = new ViewEngineResult(view.Object, locatedEngine.Object);
+ locatedEngine.Setup(e => e.FindPartialView(context, "ViewName", true))
+ .Returns(engineResult);
+ Mock<IViewEngine> secondEngine = new Mock<IViewEngine>();
+ Resolver<IEnumerable<IViewEngine>> resolver = new Resolver<IEnumerable<IViewEngine>> { Current = new IViewEngine[] { locatedEngine.Object, secondEngine.Object } };
+ ViewEngineCollection engines = new ViewEngineCollection(resolver);
+
+ // Act
+ ViewEngineResult result = engines.FindPartialView(context, "ViewName");
+
+ // Assert
+ Assert.Same(engineResult, result);
+ secondEngine.Verify(e => e.FindPartialView(context, "ViewName", It.IsAny<bool>()), Times.Never());
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ViewEngineResultTest.cs b/test/System.Web.Mvc.Test/Test/ViewEngineResultTest.cs
new file mode 100644
index 00000000..9791a683
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ViewEngineResultTest.cs
@@ -0,0 +1,80 @@
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ViewEngineResultTest
+ {
+ [Fact]
+ public void ConstructorThrowsIfSearchedLocationsIsNull()
+ {
+ // Arrange
+ string[] searchedLocations = null;
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { new ViewEngineResult(searchedLocations); }, "searchedLocations");
+ }
+
+ [Fact]
+ public void ConstructorThrowsIfViewIsNull()
+ {
+ // Arrange
+ IView view = null;
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { new ViewEngineResult(view, null); }, "view");
+ }
+
+ [Fact]
+ public void ConstructorThrowsIfViewEngineIsNull()
+ {
+ // Arrange
+ IView view = new Mock<IView>().Object;
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { new ViewEngineResult(view, null); }, "viewEngine");
+ }
+
+ [Fact]
+ public void SearchedLocationsProperty()
+ {
+ // Arrange
+ string[] searchedLocations = new string[0];
+ ViewEngineResult result = new ViewEngineResult(searchedLocations);
+
+ // Act & Assert
+ Assert.Same(searchedLocations, result.SearchedLocations);
+ Assert.Null(result.View);
+ }
+
+ [Fact]
+ public void ViewProperty()
+ {
+ // Arrange
+ IView view = new Mock<IView>().Object;
+ IViewEngine viewEngine = new Mock<IViewEngine>().Object;
+ ViewEngineResult result = new ViewEngineResult(view, viewEngine);
+
+ // Act & Assert
+ Assert.Same(view, result.View);
+ Assert.Null(result.SearchedLocations);
+ }
+
+ [Fact]
+ public void ViewEngineProperty()
+ {
+ // Arrange
+ IView view = new Mock<IView>().Object;
+ IViewEngine viewEngine = new Mock<IViewEngine>().Object;
+ ViewEngineResult result = new ViewEngineResult(view, viewEngine);
+
+ // Act & Assert
+ Assert.Same(viewEngine, result.ViewEngine);
+ Assert.Null(result.SearchedLocations);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ViewEnginesTest.cs b/test/System.Web.Mvc.Test/Test/ViewEnginesTest.cs
new file mode 100644
index 00000000..966d3b43
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ViewEnginesTest.cs
@@ -0,0 +1,19 @@
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class ViewEnginesTest
+ {
+ [Fact]
+ public void EnginesProperty()
+ {
+ // Act
+ ViewEngineCollection collection = ViewEngines.Engines;
+
+ // Assert
+ Assert.Equal(2, collection.Count);
+ Assert.IsType<WebFormViewEngine>(collection[0]);
+ Assert.IsType<RazorViewEngine>(collection[1]);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ViewMasterPageControlBuilderTest.cs b/test/System.Web.Mvc.Test/Test/ViewMasterPageControlBuilderTest.cs
new file mode 100644
index 00000000..c55d22d1
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ViewMasterPageControlBuilderTest.cs
@@ -0,0 +1,39 @@
+using System.CodeDom;
+using System.Linq;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class ViewMasterPageControlBuilderTest
+ {
+ [Fact]
+ public void BuilderWithoutInheritsDoesNothing()
+ {
+ // Arrange
+ var builder = new ViewMasterPageControlBuilder();
+ var derivedType = new CodeTypeDeclaration();
+ derivedType.BaseTypes.Add("basetype");
+
+ // Act
+ builder.ProcessGeneratedCode(null, null, derivedType, null, null);
+
+ // Assert
+ Assert.Equal("basetype", derivedType.BaseTypes.Cast<CodeTypeReference>().Single().BaseType);
+ }
+
+ [Fact]
+ public void BuilderWithInheritsSetsBaseType()
+ {
+ // Arrange
+ var builder = new ViewMasterPageControlBuilder { Inherits = "inheritedtype" };
+ var derivedType = new CodeTypeDeclaration();
+ derivedType.BaseTypes.Add("basetype");
+
+ // Act
+ builder.ProcessGeneratedCode(null, null, derivedType, null, null);
+
+ // Assert
+ Assert.Equal("inheritedtype", derivedType.BaseTypes.Cast<CodeTypeReference>().Single().BaseType);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ViewMasterPageTest.cs b/test/System.Web.Mvc.Test/Test/ViewMasterPageTest.cs
new file mode 100644
index 00000000..cfa0b3d6
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ViewMasterPageTest.cs
@@ -0,0 +1,245 @@
+using System.IO;
+using System.Web.Routing;
+using System.Web.UI;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ViewMasterPageTest
+ {
+ [Fact]
+ public void GetModelFromViewPage()
+ {
+ // Arrange
+ ViewMasterPage vmp = new ViewMasterPage();
+ ViewPage vp = new ViewPage();
+ vmp.Page = vp;
+ object model = new object();
+ vp.ViewData = new ViewDataDictionary(model);
+
+ // Assert
+ Assert.Equal(model, vmp.Model);
+ }
+
+ [Fact]
+ public void GetModelFromViewPageStronglyTyped()
+ {
+ // Arrange
+ ViewMasterPage<FooModel> vmp = new ViewMasterPage<FooModel>();
+ ViewPage vp = new ViewPage();
+ vmp.Page = vp;
+ FooModel model = new FooModel();
+ vp.ViewData = new ViewDataDictionary(model);
+
+ // Assert
+ Assert.Equal(model, vmp.Model);
+ }
+
+ [Fact]
+ public void GetViewDataFromViewPage()
+ {
+ // Arrange
+ ViewMasterPage vmp = new ViewMasterPage();
+ ViewPage vp = new ViewPage();
+ vmp.Page = vp;
+ vp.ViewData = new ViewDataDictionary { { "a", "123" }, { "b", "456" } };
+
+ // Assert
+ Assert.Equal("123", vmp.ViewData.Eval("a"));
+ Assert.Equal("456", vmp.ViewData.Eval("b"));
+ }
+
+ [Fact]
+ public void GetViewItemFromViewPageTViewData()
+ {
+ // Arrange
+ MockViewMasterPageDummyViewData vmp = new MockViewMasterPageDummyViewData();
+ MockViewPageDummyViewData vp = new MockViewPageDummyViewData();
+ vmp.Page = vp;
+ vp.ViewData.Model = new DummyViewData { MyInt = 123, MyString = "abc" };
+
+ // Assert
+ Assert.Equal(123, vmp.ViewData.Model.MyInt);
+ Assert.Equal("abc", vmp.ViewData.Model.MyString);
+ }
+
+ [Fact]
+ public void GetWriterFromViewPage()
+ {
+ // Arrange
+ bool triggered = false;
+ HtmlTextWriter writer = new HtmlTextWriter(TextWriter.Null);
+ ViewMasterPage vmp = new ViewMasterPage();
+ MockViewPage vp = new MockViewPage();
+ vp.RenderCallback = delegate()
+ {
+ triggered = true;
+ Assert.Equal(writer, vmp.Writer);
+ };
+ vmp.Page = vp;
+
+ // Act & Assert
+ Assert.Null(vmp.Writer);
+ vp.RenderControl(writer);
+ Assert.Null(vmp.Writer);
+ Assert.True(triggered);
+ }
+
+ [Fact]
+ public void GetViewDataFromPageThrows()
+ {
+ // Arrange
+ ViewMasterPage vmp = new ViewMasterPage();
+ vmp.Page = new Page();
+
+ // Assert
+ Assert.Throws<InvalidOperationException>(
+ delegate { object foo = vmp.ViewData; },
+ "A ViewMasterPage can be used only with content pages that derive from ViewPage or ViewPage<TModel>.");
+ }
+
+ [Fact]
+ public void GetViewItemFromWrongGenericViewPageType()
+ {
+ // Arrange
+ MockViewMasterPageDummyViewData vmp = new MockViewMasterPageDummyViewData();
+ MockViewPageBogusViewData vp = new MockViewPageBogusViewData();
+ vmp.Page = vp;
+ vp.ViewData.Model = new SelectListItem();
+
+ // Assert
+ Assert.Throws<InvalidOperationException>(
+ delegate { object foo = vmp.ViewData.Model; },
+ "The model item passed into the dictionary is of type 'System.Web.Mvc.SelectListItem', but this dictionary requires a model item of type 'System.Web.Mvc.Test.ViewMasterPageTest+DummyViewData'.");
+ }
+
+ [Fact]
+ public void GetViewDataFromNullPageThrows()
+ {
+ // Arrange
+ MockViewMasterPageDummyViewData vmp = new MockViewMasterPageDummyViewData();
+
+ // Assert
+ Assert.Throws<InvalidOperationException>(
+ delegate { object foo = vmp.ViewData; },
+ "A ViewMasterPage can be used only with content pages that derive from ViewPage or ViewPage<TModel>.");
+ }
+
+ [Fact]
+ public void GetViewDataFromRegularPageThrows()
+ {
+ // Arrange
+ MockViewMasterPageDummyViewData vmp = new MockViewMasterPageDummyViewData();
+ vmp.Page = new Page();
+
+ // Assert
+ Assert.Throws<InvalidOperationException>(
+ delegate { object foo = vmp.ViewData; },
+ "A ViewMasterPage can be used only with content pages that derive from ViewPage or ViewPage<TModel>.");
+ }
+
+ [Fact]
+ public void GetHtmlHelperFromViewPage()
+ {
+ // Arrange
+ ViewMasterPage vmp = new ViewMasterPage();
+ ViewPage vp = new ViewPage();
+ vmp.Page = vp;
+ ViewContext vc = new Mock<ViewContext>().Object;
+
+ HtmlHelper<object> htmlHelper = new HtmlHelper<object>(vc, vp);
+ vp.Html = htmlHelper;
+
+ // Assert
+ Assert.Equal(vmp.Html, htmlHelper);
+ }
+
+ [Fact]
+ public void GetUrlHelperFromViewPage()
+ {
+ // Arrange
+ ViewMasterPage vmp = new ViewMasterPage();
+ ViewPage vp = new ViewPage();
+ vmp.Page = vp;
+ RequestContext rc = new RequestContext(new Mock<HttpContextBase>().Object, new RouteData());
+ UrlHelper urlHelper = new UrlHelper(rc);
+ vp.Url = urlHelper;
+
+ // Assert
+ Assert.Equal(vmp.Url, urlHelper);
+ }
+
+ [Fact]
+ public void ViewBagProperty_ReflectsViewData()
+ {
+ // Arrange
+ ViewPage page = new ViewPage();
+ ViewMasterPage masterPage = new ViewMasterPage();
+ masterPage.Page = page;
+ masterPage.ViewData["A"] = 1;
+
+ // Act & Assert
+ Assert.NotNull(masterPage.ViewBag);
+ Assert.Equal(1, masterPage.ViewBag.A);
+ }
+
+ [Fact]
+ public void ViewBagProperty_PropagatesChangesToViewData()
+ {
+ // Arrange
+ ViewPage page = new ViewPage();
+ ViewMasterPage masterPage = new ViewMasterPage();
+ masterPage.Page = page;
+ masterPage.ViewData["A"] = 1;
+
+ // Act
+ masterPage.ViewBag.A = "foo";
+ masterPage.ViewBag.B = 2;
+
+ // Assert
+ Assert.Equal("foo", masterPage.ViewData["A"]);
+ Assert.Equal(2, masterPage.ViewData["B"]);
+ }
+
+ // Master page types
+ private sealed class MockViewMasterPageDummyViewData : ViewMasterPage<DummyViewData>
+ {
+ }
+
+ // View data types
+ private sealed class DummyViewData
+ {
+ public int MyInt { get; set; }
+ public string MyString { get; set; }
+ }
+
+ // Page types
+ private sealed class MockViewPageBogusViewData : ViewPage<SelectListItem>
+ {
+ }
+
+ private sealed class MockViewPageDummyViewData : ViewPage<DummyViewData>
+ {
+ }
+
+ private sealed class MockViewPage : ViewPage
+ {
+ public Action RenderCallback { get; set; }
+
+ protected override void RenderChildren(HtmlTextWriter writer)
+ {
+ if (RenderCallback != null)
+ {
+ RenderCallback();
+ }
+ base.RenderChildren(writer);
+ }
+ }
+
+ private sealed class FooModel
+ {
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ViewPageControlBuilderTest.cs b/test/System.Web.Mvc.Test/Test/ViewPageControlBuilderTest.cs
new file mode 100644
index 00000000..1ae597a1
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ViewPageControlBuilderTest.cs
@@ -0,0 +1,39 @@
+using System.CodeDom;
+using System.Linq;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class ViewPageControlBuilderTest
+ {
+ [Fact]
+ public void BuilderWithoutInheritsDoesNothing()
+ {
+ // Arrange
+ var builder = new ViewPageControlBuilder();
+ var derivedType = new CodeTypeDeclaration();
+ derivedType.BaseTypes.Add("basetype");
+
+ // Act
+ builder.ProcessGeneratedCode(null, null, derivedType, null, null);
+
+ // Assert
+ Assert.Equal("basetype", derivedType.BaseTypes.Cast<CodeTypeReference>().Single().BaseType);
+ }
+
+ [Fact]
+ public void BuilderWithInheritsSetsBaseType()
+ {
+ // Arrange
+ var builder = new ViewPageControlBuilder { Inherits = "inheritedtype" };
+ var derivedType = new CodeTypeDeclaration();
+ derivedType.BaseTypes.Add("basetype");
+
+ // Act
+ builder.ProcessGeneratedCode(null, null, derivedType, null, null);
+
+ // Assert
+ Assert.Equal("inheritedtype", derivedType.BaseTypes.Cast<CodeTypeReference>().Single().BaseType);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ViewPageTest.cs b/test/System.Web.Mvc.Test/Test/ViewPageTest.cs
new file mode 100644
index 00000000..f7ae263f
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ViewPageTest.cs
@@ -0,0 +1,262 @@
+using System.IO;
+using System.Web.UI;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ViewPageTest
+ {
+ [Fact]
+ public void ModelProperty()
+ {
+ // Arrange
+ object model = new object();
+ ViewDataDictionary viewData = new ViewDataDictionary(model);
+ ViewPage viewPage = new ViewPage();
+ viewPage.ViewData = viewData;
+
+ // Act
+ object viewPageModel = viewPage.Model;
+
+ // Assert
+ Assert.Equal(model, viewPageModel);
+ Assert.Equal(model, viewPage.ViewData.Model);
+ }
+
+ [Fact]
+ public void ModelPropertyStronglyTypedViewPage()
+ {
+ // Arrange
+ FooModel model = new FooModel();
+ ViewDataDictionary<FooModel> viewData = new ViewDataDictionary<FooModel>(model);
+ ViewPage<FooModel> viewPage = new ViewPage<FooModel>();
+ viewPage.ViewData = viewData;
+
+ // Act
+ object viewPageModelObject = ((ViewPage)viewPage).Model;
+ FooModel viewPageModelPerson = viewPage.Model;
+
+ // Assert
+ Assert.Equal(model, viewPageModelObject);
+ Assert.Equal(model, viewPageModelPerson);
+ }
+
+ [Fact]
+ public void SetViewItemOnBaseClassPropagatesToDerivedClass()
+ {
+ // Arrange
+ ViewPage<object> vpInt = new ViewPage<object>();
+ ViewPage vp = vpInt;
+ object o = new object();
+
+ // Act
+ vp.ViewData.Model = o;
+
+ // Assert
+ Assert.Equal(o, vpInt.ViewData.Model);
+ Assert.Equal(o, vp.ViewData.Model);
+ }
+
+ [Fact]
+ public void SetViewItemOnDerivedClassPropagatesToBaseClass()
+ {
+ // Arrange
+ ViewPage<object> vpInt = new ViewPage<object>();
+ ViewPage vp = vpInt;
+ object o = new object();
+
+ // Act
+ vpInt.ViewData.Model = o;
+
+ // Assert
+ Assert.Equal(o, vpInt.ViewData.Model);
+ Assert.Equal(o, vp.ViewData.Model);
+ }
+
+ [Fact]
+ public void SetViewItemToWrongTypeThrows()
+ {
+ // Arrange
+ ViewPage vp = new ViewPage<string>();
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ delegate { vp.ViewData.Model = 50; },
+ "The model item passed into the dictionary is of type 'System.Int32', but this dictionary requires a model item of type 'System.String'.");
+ }
+
+ [Fact]
+ public void RenderInitsHelpersAndSetsID()
+ {
+ // Arrange
+ ViewPageWithNoProcessRequest viewPage = new ViewPageWithNoProcessRequest();
+ TextWriter writer = new StringWriter();
+
+ Mock<ViewContext> mockViewContext = new Mock<ViewContext>();
+ mockViewContext.Setup(c => c.Writer).Returns(writer);
+ mockViewContext.Setup(c => c.HttpContext.Response.Output).Returns(TextWriter.Null);
+ mockViewContext.Setup(c => c.HttpContext.Server.Execute(It.IsAny<IHttpHandler>(), It.IsAny<TextWriter>(), true))
+ .Callback<IHttpHandler, TextWriter, bool>((_h, _w, _pf) =>
+ {
+ ViewPage.SwitchWriter switchWriter = _w as ViewPage.SwitchWriter;
+ Assert.NotNull(switchWriter);
+ Assert.Same(writer, switchWriter.InnerWriter);
+ })
+ .Verifiable();
+
+ // Act
+ viewPage.RenderView(mockViewContext.Object);
+
+ // Assert
+ mockViewContext.Verify();
+ Assert.NotNull(viewPage.Ajax);
+ Assert.NotNull(viewPage.Html);
+ Assert.NotNull(viewPage.Url);
+ }
+
+ [Fact]
+ public void GenericPageRenderInitsHelpersAndSetsID()
+ {
+ // Arrange
+ Mock<ViewContext> mockViewContext = new Mock<ViewContext>();
+ mockViewContext.Setup(c => c.Writer).Returns(new StringWriter());
+ mockViewContext.Setup(c => c.HttpContext.Response.Output).Returns(TextWriter.Null);
+ mockViewContext.Setup(c => c.HttpContext.Server).Returns(new Mock<HttpServerUtilityBase>().Object);
+
+ ViewPageWithNoProcessRequest<Controller> viewPage = new ViewPageWithNoProcessRequest<Controller>();
+
+ // Act
+ viewPage.RenderView(mockViewContext.Object);
+
+ // Assert
+ Assert.NotNull(viewPage.Ajax);
+ Assert.NotNull(viewPage.Html);
+ Assert.NotNull(viewPage.Url);
+ Assert.NotNull(((ViewPage)viewPage).Html);
+ Assert.NotNull(((ViewPage)viewPage).Url);
+ }
+
+ private static void WriterSetCorrectlyInternal(bool throwException)
+ {
+ // Arrange
+ bool triggered = false;
+ HtmlTextWriter writer = new HtmlTextWriter(TextWriter.Null);
+ MockViewPage vp = new MockViewPage();
+ vp.RenderCallback = delegate()
+ {
+ triggered = true;
+ Assert.Equal(writer, vp.Writer);
+ if (throwException)
+ {
+ throw new CallbackException();
+ }
+ };
+
+ // Act & Assert
+ Assert.Null(vp.Writer);
+ try
+ {
+ vp.RenderControl(writer);
+ }
+ catch (CallbackException)
+ {
+ }
+ Assert.Null(vp.Writer);
+ Assert.True(triggered);
+ }
+
+ [Fact]
+ public void WriterSetCorrectly()
+ {
+ WriterSetCorrectlyInternal(false /* throwException */);
+ }
+
+ [Fact]
+ public void WriterSetCorrectlyThrowException()
+ {
+ WriterSetCorrectlyInternal(true /* throwException */);
+ }
+
+ private sealed class ViewPageWithNoProcessRequest : ViewPage
+ {
+ public override void ProcessRequest(HttpContext context)
+ {
+ }
+ }
+
+ private sealed class ViewPageWithNoProcessRequest<TModel> : ViewPage<TModel>
+ {
+ public override void ProcessRequest(HttpContext context)
+ {
+ }
+ }
+
+ [Fact]
+ public void ViewBagProperty_ReflectsViewData()
+ {
+ // Arrange
+ ViewPage page = new ViewPage();
+ page.ViewData["A"] = 1;
+
+ // Act & Assert
+ Assert.NotNull(page.ViewBag);
+ Assert.Equal(1, page.ViewBag.A);
+ }
+
+ [Fact]
+ public void ViewBagProperty_ReflectsNewViewDataInstance()
+ {
+ // Arrange
+ ViewPage page = new ViewPage();
+ page.ViewData["A"] = 1;
+ page.ViewData = new ViewDataDictionary() { { "A", "bar" } };
+
+ // Act & Assert
+ Assert.Equal("bar", page.ViewBag.A);
+ }
+
+ [Fact]
+ public void ViewBagProperty_PropagatesChangesToViewData()
+ {
+ // Arrange
+ ViewPage page = new ViewPage();
+ page.ViewData["A"] = 1;
+
+ // Act
+ page.ViewBag.A = "foo";
+ page.ViewBag.B = 2;
+
+ // Assert
+ Assert.Equal("foo", page.ViewData["A"]);
+ Assert.Equal(2, page.ViewData["B"]);
+ }
+
+ private sealed class MockViewPage : ViewPage
+ {
+ public MockViewPage()
+ {
+ }
+
+ public Action RenderCallback { get; set; }
+
+ protected override void RenderChildren(HtmlTextWriter writer)
+ {
+ if (RenderCallback != null)
+ {
+ RenderCallback();
+ }
+ base.RenderChildren(writer);
+ }
+ }
+
+ private sealed class FooModel
+ {
+ }
+
+ private sealed class CallbackException : Exception
+ {
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ViewResultBaseTest.cs b/test/System.Web.Mvc.Test/Test/ViewResultBaseTest.cs
new file mode 100644
index 00000000..3dc3bedf
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ViewResultBaseTest.cs
@@ -0,0 +1,72 @@
+using System.Web.TestUtil;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ViewResultBaseTest
+ {
+ [Fact]
+ public void ExecuteResultWithNullControllerContextThrows()
+ {
+ // Arrange
+ ViewResultBaseHelper result = new ViewResultBaseHelper();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => result.ExecuteResult(null),
+ "context");
+ }
+
+ [Fact]
+ public void TempDataProperty()
+ {
+ // Arrange
+ TempDataDictionary newDict = new TempDataDictionary();
+ ViewResultBaseHelper result = new ViewResultBaseHelper();
+
+ // Act & Assert
+ MemberHelper.TestPropertyWithDefaultInstance(result, "TempData", newDict);
+ }
+
+ [Fact]
+ public void ViewDataProperty()
+ {
+ // Arrange
+ ViewDataDictionary newDict = new ViewDataDictionary();
+ ViewResultBaseHelper result = new ViewResultBaseHelper();
+
+ // Act & Assert
+ MemberHelper.TestPropertyWithDefaultInstance(result, "ViewData", newDict);
+ }
+
+ [Fact]
+ public void ViewEngineCollectionProperty()
+ {
+ // Arrange
+ ViewEngineCollection viewEngineCollection = new ViewEngineCollection();
+ ViewResultBaseHelper result = new ViewResultBaseHelper();
+
+ // Act & Assert
+ MemberHelper.TestPropertyWithDefaultInstance(result, "ViewEngineCollection", viewEngineCollection);
+ }
+
+ [Fact]
+ public void ViewNameProperty()
+ {
+ // Arrange
+ ViewResultBaseHelper result = new ViewResultBaseHelper();
+
+ // Act & Assert
+ MemberHelper.TestStringProperty(result, "ViewName", String.Empty);
+ }
+
+ public class ViewResultBaseHelper : ViewResultBase
+ {
+ protected override ViewEngineResult FindView(ControllerContext context)
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ViewResultTest.cs b/test/System.Web.Mvc.Test/Test/ViewResultTest.cs
new file mode 100644
index 00000000..e78a366c
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ViewResultTest.cs
@@ -0,0 +1,222 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Web.Routing;
+using System.Web.TestUtil;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ViewResultTest
+ {
+ private const string _viewName = "My cool view.";
+ private const string _masterName = "My cool master.";
+
+ [Fact]
+ public void EmptyViewNameUsesActionNameAsViewName()
+ {
+ // Arrange
+ ControllerBase controller = new Mock<ControllerBase>().Object;
+ HttpContextBase httpContext = CreateHttpContext();
+ RouteData routeData = new RouteData();
+ routeData.Values["action"] = _viewName;
+ ControllerContext context = new ControllerContext(httpContext, routeData, controller);
+ Mock<IViewEngine> viewEngine = new Mock<IViewEngine>(MockBehavior.Strict);
+ Mock<IView> view = new Mock<IView>(MockBehavior.Strict);
+ List<IViewEngine> viewEngines = new List<IViewEngine>();
+ viewEngines.Add(viewEngine.Object);
+ Mock<ViewEngineCollection> viewEngineCollection = new Mock<ViewEngineCollection>(MockBehavior.Strict, viewEngines);
+ ViewResult result = new ViewResultHelper { ViewEngineCollection = viewEngineCollection.Object };
+ viewEngineCollection
+ .Setup(e => e.FindView(It.IsAny<ControllerContext>(), _viewName, _masterName))
+ .Returns(new ViewEngineResult(view.Object, viewEngine.Object));
+ viewEngine
+ .Setup(e => e.FindView(It.IsAny<ControllerContext>(), _viewName, _masterName, It.IsAny<bool>()))
+ .Callback<ControllerContext, string, string, bool>(
+ (controllerContext, viewName, masterName, useCache) =>
+ {
+ Assert.Same(httpContext, controllerContext.HttpContext);
+ Assert.Same(routeData, controllerContext.RouteData);
+ })
+ .Returns(new ViewEngineResult(view.Object, viewEngine.Object));
+ view
+ .Setup(o => o.Render(It.IsAny<ViewContext>(), httpContext.Response.Output))
+ .Callback<ViewContext, TextWriter>(
+ (viewContext, writer) =>
+ {
+ Assert.Same(view.Object, viewContext.View);
+ Assert.Same(result.ViewData, viewContext.ViewData);
+ Assert.Same(result.TempData, viewContext.TempData);
+ Assert.Same(controller, viewContext.Controller);
+ });
+ viewEngine
+ .Setup(e => e.ReleaseView(context, It.IsAny<IView>()))
+ .Callback<ControllerContext, IView>(
+ (controllerContext, releasedView) => { Assert.Same(releasedView, view.Object); });
+
+ // Act
+ result.ExecuteResult(context);
+
+ // Assert
+ viewEngine.Verify();
+ viewEngineCollection.Verify();
+ view.Verify();
+ }
+
+ [Fact]
+ public void EngineLookupFailureThrows()
+ {
+ // Arrange
+ ControllerBase controller = new Mock<ControllerBase>().Object;
+ HttpContextBase httpContext = CreateHttpContext();
+ RouteData routeData = new RouteData();
+ routeData.Values["action"] = _viewName;
+ ControllerContext context = new ControllerContext(httpContext, routeData, controller);
+ Mock<IViewEngine> viewEngine = new Mock<IViewEngine>(MockBehavior.Strict);
+ List<IViewEngine> viewEngines = new List<IViewEngine>();
+ viewEngines.Add(viewEngine.Object);
+ Mock<ViewEngineCollection> viewEngineCollection = new Mock<ViewEngineCollection>(MockBehavior.Strict, viewEngines);
+ ViewResult result = new ViewResultHelper { ViewEngineCollection = viewEngineCollection.Object };
+ viewEngineCollection
+ .Setup(e => e.FindView(It.IsAny<ControllerContext>(), _viewName, _masterName))
+ .Returns(new ViewEngineResult(new[] { "location1", "location2" }));
+ viewEngine
+ .Setup(e => e.FindView(It.IsAny<ControllerContext>(), _viewName, _masterName, It.IsAny<bool>()))
+ .Callback<ControllerContext, string, string, bool>(
+ (controllerContext, viewName, masterName, useCache) =>
+ {
+ Assert.Same(httpContext, controllerContext.HttpContext);
+ Assert.Same(routeData, controllerContext.RouteData);
+ })
+ .Returns(new ViewEngineResult(new[] { "location1", "location2" }));
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ () => result.ExecuteResult(context),
+ @"The view '" + _viewName + @"' or its master was not found or no view engine supports the searched locations. The following locations were searched:
+location1
+location2");
+
+ viewEngine.Verify();
+ viewEngineCollection.Verify();
+ }
+
+ [Fact]
+ public void EngineLookupSuccessRendersView()
+ {
+ // Arrange
+ ControllerBase controller = new Mock<ControllerBase>().Object;
+ HttpContextBase httpContext = CreateHttpContext();
+ RouteData routeData = new RouteData();
+ ControllerContext context = new ControllerContext(httpContext, routeData, controller);
+ Mock<IViewEngine> viewEngine = new Mock<IViewEngine>(MockBehavior.Strict);
+ Mock<IView> view = new Mock<IView>(MockBehavior.Strict);
+ List<IViewEngine> viewEngines = new List<IViewEngine>();
+ viewEngines.Add(viewEngine.Object);
+ Mock<ViewEngineCollection> viewEngineCollection = new Mock<ViewEngineCollection>(MockBehavior.Strict, viewEngines);
+ ViewResult result = new ViewResultHelper { ViewName = _viewName, ViewEngineCollection = viewEngineCollection.Object };
+ view
+ .Setup(o => o.Render(It.IsAny<ViewContext>(), httpContext.Response.Output))
+ .Callback<ViewContext, TextWriter>(
+ (viewContext, writer) =>
+ {
+ Assert.Same(view.Object, viewContext.View);
+ Assert.Same(result.ViewData, viewContext.ViewData);
+ Assert.Same(result.TempData, viewContext.TempData);
+ Assert.Same(controller, viewContext.Controller);
+ });
+ viewEngineCollection
+ .Setup(e => e.FindView(It.IsAny<ControllerContext>(), _viewName, _masterName))
+ .Returns(new ViewEngineResult(view.Object, viewEngine.Object));
+ viewEngine
+ .Setup(e => e.FindView(It.IsAny<ControllerContext>(), _viewName, _masterName, It.IsAny<bool>()))
+ .Callback<ControllerContext, string, string, bool>(
+ (controllerContext, viewName, masterName, useCache) =>
+ {
+ Assert.Same(httpContext, controllerContext.HttpContext);
+ Assert.Same(routeData, controllerContext.RouteData);
+ })
+ .Returns(new ViewEngineResult(view.Object, viewEngine.Object));
+ viewEngine
+ .Setup(e => e.ReleaseView(context, It.IsAny<IView>()))
+ .Callback<ControllerContext, IView>(
+ (controllerContext, releasedView) => { Assert.Same(releasedView, view.Object); });
+
+ // Act
+ result.ExecuteResult(context);
+
+ // Assert
+ viewEngine.Verify();
+ viewEngineCollection.Verify();
+ view.Verify();
+ }
+
+ [Fact]
+ public void ExecuteResultWithExplicitViewObject()
+ {
+ // Arrange
+ ControllerBase controller = new Mock<ControllerBase>().Object;
+ HttpContextBase httpContext = CreateHttpContext();
+ RouteData routeData = new RouteData();
+ routeData.Values["action"] = _viewName;
+ ControllerContext context = new ControllerContext(httpContext, routeData, controller);
+ Mock<IView> view = new Mock<IView>(MockBehavior.Strict);
+ ViewResult result = new ViewResultHelper { View = view.Object };
+ view
+ .Setup(o => o.Render(It.IsAny<ViewContext>(), httpContext.Response.Output))
+ .Callback<ViewContext, TextWriter>(
+ (viewContext, writer) =>
+ {
+ Assert.Same(view.Object, viewContext.View);
+ Assert.Same(result.ViewData, viewContext.ViewData);
+ Assert.Same(result.TempData, viewContext.TempData);
+ Assert.Same(controller, viewContext.Controller);
+ });
+
+ // Act
+ result.ExecuteResult(context);
+
+ // Assert
+ view.Verify();
+ }
+
+ [Fact]
+ public void ExecuteResultWithNullControllerContextThrows()
+ {
+ // Arrange
+ ViewResult result = new ViewResultHelper();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => result.ExecuteResult(null),
+ "context");
+ }
+
+ [Fact]
+ public void MasterNameProperty()
+ {
+ // Arrange
+ ViewResult result = new ViewResult();
+
+ // Act & Assert
+ MemberHelper.TestStringProperty(result, "MasterName", String.Empty);
+ }
+
+ private static HttpContextBase CreateHttpContext()
+ {
+ Mock<HttpContextBase> mockHttpContext = new Mock<HttpContextBase>();
+ mockHttpContext.Setup(c => c.Response.Output).Returns(TextWriter.Null);
+ return mockHttpContext.Object;
+ }
+
+ private class ViewResultHelper : ViewResult
+ {
+ public ViewResultHelper()
+ {
+ ViewEngineCollection = new ViewEngineCollection(new IViewEngine[] { new WebFormViewEngine() });
+ MasterName = _masterName;
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ViewStartPageTest.cs b/test/System.Web.Mvc.Test/Test/ViewStartPageTest.cs
new file mode 100644
index 00000000..7bcdf8a0
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ViewStartPageTest.cs
@@ -0,0 +1,110 @@
+using System.Web.Routing;
+using System.Web.WebPages;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ViewStartPageTest
+ {
+ [Fact]
+ public void Html_DelegatesToChildPage()
+ {
+ // Arrange
+ MockViewStartPage viewStart = new MockViewStartPage();
+ var viewPage = new Mock<WebViewPage>() { CallBase = true };
+ var helper = new HtmlHelper<object>(new ViewContext() { ViewData = new ViewDataDictionary() }, viewPage.Object, new RouteCollection());
+ viewPage.Object.Html = helper;
+ viewStart.ChildPage = viewPage.Object;
+
+ // Act
+ var result = viewStart.Html;
+
+ // Assert
+ Assert.Same(helper, result);
+ }
+
+ [Fact]
+ public void Url_DelegatesToChildPage()
+ {
+ // Arrange
+ MockViewStartPage viewStart = new MockViewStartPage();
+ var viewPage = new Mock<WebViewPage>() { CallBase = true };
+ var helper = new UrlHelper(new RequestContext());
+ viewPage.Object.Url = helper;
+ viewStart.ChildPage = viewPage.Object;
+
+ // Act
+ var result = viewStart.Url;
+
+ // Assert
+ Assert.Same(helper, result);
+ }
+
+ [Fact]
+ public void ViewContext_DelegatesToChildPage()
+ {
+ // Arrange
+ MockViewStartPage viewStart = new MockViewStartPage();
+ var viewPage = new Mock<WebViewPage>() { CallBase = true };
+ var viewContext = new ViewContext();
+ viewPage.Object.ViewContext = viewContext;
+ viewStart.ChildPage = viewPage.Object;
+
+ // Act
+ var result = viewStart.ViewContext;
+
+ // Assert
+ Assert.Same(viewContext, result);
+ }
+
+ [Fact]
+ public void ViewStartPageChild_ThrowsOnNonMvcChildPage()
+ {
+ // Arrange
+ MockViewStartPage viewStart = new MockViewStartPage();
+ viewStart.ChildPage = new Mock<WebPage>().Object;
+
+ // Act + Assert
+ Assert.Throws<InvalidOperationException>(delegate() { var c = viewStart.ViewStartPageChild; }, "A ViewStartPage can be used only with with a page that derives from WebViewPage or another ViewStartPage.");
+ }
+
+ [Fact]
+ public void ViewStartPageChild_WorksWithWebViewPage()
+ {
+ // Arrange
+ MockViewStartPage viewStart = new MockViewStartPage();
+ var viewPage = new Mock<WebViewPage>();
+ viewStart.ChildPage = viewPage.Object;
+
+ // Act
+ var result = viewStart.ViewStartPageChild;
+
+ // Assert
+ Assert.Same(viewPage.Object, result);
+ }
+
+ [Fact]
+ public void ViewStartPageChild_WorksWithAnotherRazorStartPage()
+ {
+ // Arrange
+ MockViewStartPage viewStart = new MockViewStartPage();
+ var anotherViewStart = new Mock<ViewStartPage>();
+ viewStart.ChildPage = anotherViewStart.Object;
+
+ // Act
+ var result = viewStart.ViewStartPageChild;
+
+ // Assert
+ Assert.Same(anotherViewStart.Object, result);
+ }
+
+ class MockViewStartPage : ViewStartPage
+ {
+ public override void Execute()
+ {
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ViewTypeParserFilterTest.cs b/test/System.Web.Mvc.Test/Test/ViewTypeParserFilterTest.cs
new file mode 100644
index 00000000..92c3c03e
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ViewTypeParserFilterTest.cs
@@ -0,0 +1,208 @@
+using System.Collections.Generic;
+using System.Web.UI;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class ViewTypeParserFilterTest
+ {
+ // Non-generic directives
+
+ [Fact]
+ public void NonGenericPageDirectiveDoesNotChangeInheritsDirective()
+ {
+ var filter = new ViewTypeParserFilter();
+ var attributes = new Dictionary<string, string> { { "inherits", "foobar" } };
+ var builder = new MvcBuilder();
+
+ filter.PreprocessDirective("page", attributes);
+ filter.ParseComplete(builder);
+
+ Assert.Equal("foobar", attributes["inherits"]);
+ Assert.Null(builder.Inherits);
+ }
+
+ [Fact]
+ public void NonGenericControlDirectiveDoesNotChangeInheritsDirective()
+ {
+ var filter = new ViewTypeParserFilter();
+ var attributes = new Dictionary<string, string> { { "inherits", "foobar" } };
+ var builder = new MvcBuilder();
+
+ filter.PreprocessDirective("control", attributes);
+ filter.ParseComplete(builder);
+
+ Assert.Equal("foobar", attributes["inherits"]);
+ Assert.Null(builder.Inherits);
+ }
+
+ [Fact]
+ public void NonGenericMasterDirectiveDoesNotChangeInheritsDirective()
+ {
+ var filter = new ViewTypeParserFilter();
+ var attributes = new Dictionary<string, string> { { "inherits", "foobar" } };
+ var builder = new MvcBuilder();
+
+ filter.PreprocessDirective("master", attributes);
+ filter.ParseComplete(builder);
+
+ Assert.Equal("foobar", attributes["inherits"]);
+ Assert.Null(builder.Inherits);
+ }
+
+ // C#-style generic directives
+
+ [Fact]
+ public void CSGenericUnknownDirectiveDoesNotChangeInheritsDirective()
+ {
+ var filter = new ViewTypeParserFilter();
+ var attributes = new Dictionary<string, string> { { "inherits", "foobar<baz>" } };
+ var builder = new MvcBuilder();
+
+ filter.PreprocessDirective("unknown", attributes);
+ filter.ParseComplete(builder);
+
+ Assert.Equal("foobar<baz>", attributes["inherits"]);
+ Assert.Null(builder.Inherits);
+ }
+
+ [Fact]
+ public void CSGenericPageDirectiveChangesInheritsDirective()
+ {
+ var filter = new ViewTypeParserFilter();
+ var attributes = new Dictionary<string, string> { { "inherits", "foobar<baz>" } };
+ var builder = new MvcBuilder();
+
+ filter.PreprocessDirective("page", attributes);
+ filter.ParseComplete(builder);
+
+ Assert.Equal(typeof(ViewPage).FullName, attributes["inherits"]);
+ Assert.Equal("foobar<baz>", builder.Inherits);
+ }
+
+ [Fact]
+ public void CSGenericControlDirectiveChangesInheritsDirective()
+ {
+ var filter = new ViewTypeParserFilter();
+ var attributes = new Dictionary<string, string> { { "inherits", "foobar<baz>" } };
+ var builder = new MvcBuilder();
+
+ filter.PreprocessDirective("control", attributes);
+ filter.ParseComplete(builder);
+
+ Assert.Equal(typeof(ViewUserControl).FullName, attributes["inherits"]);
+ Assert.Equal("foobar<baz>", builder.Inherits);
+ }
+
+ [Fact]
+ public void CSGenericMasterDirectiveChangesInheritsDirective()
+ {
+ var filter = new ViewTypeParserFilter();
+ var attributes = new Dictionary<string, string> { { "inherits", "foobar<baz>" } };
+ var builder = new MvcBuilder();
+
+ filter.PreprocessDirective("master", attributes);
+ filter.ParseComplete(builder);
+
+ Assert.Equal(typeof(ViewMasterPage).FullName, attributes["inherits"]);
+ Assert.Equal("foobar<baz>", builder.Inherits);
+ }
+
+ [Fact]
+ public void CSDirectivesAfterPageDirectiveProperlyPreserveInheritsDirective()
+ {
+ var filter = new ViewTypeParserFilter();
+ var pageAttributes = new Dictionary<string, string> { { "inherits", "foobar<baz>" } };
+ var importAttributes = new Dictionary<string, string> { { "inherits", "dummyvalue<baz>" } };
+ var builder = new MvcBuilder();
+
+ filter.PreprocessDirective("page", pageAttributes);
+ filter.PreprocessDirective("import", importAttributes);
+ filter.ParseComplete(builder);
+
+ Assert.Equal(typeof(ViewPage).FullName, pageAttributes["inherits"]);
+ Assert.Equal("foobar<baz>", builder.Inherits);
+ }
+
+ // VB.NET-style generic directives
+
+ [Fact]
+ public void VBGenericUnknownDirectiveDoesNotChangeInheritsDirective()
+ {
+ var filter = new ViewTypeParserFilter();
+ var attributes = new Dictionary<string, string> { { "inherits", "foobar(of baz)" } };
+ var builder = new MvcBuilder();
+
+ filter.PreprocessDirective("unknown", attributes);
+ filter.ParseComplete(builder);
+
+ Assert.Equal("foobar(of baz)", attributes["inherits"]);
+ Assert.Null(builder.Inherits);
+ }
+
+ [Fact]
+ public void VBGenericPageDirectiveChangesInheritsDirective()
+ {
+ var filter = new ViewTypeParserFilter();
+ var attributes = new Dictionary<string, string> { { "inherits", "foobar(of baz)" } };
+ var builder = new MvcBuilder();
+
+ filter.PreprocessDirective("page", attributes);
+ filter.ParseComplete(builder);
+
+ Assert.Equal(typeof(ViewPage).FullName, attributes["inherits"]);
+ Assert.Equal("foobar(of baz)", builder.Inherits);
+ }
+
+ [Fact]
+ public void VBGenericControlDirectiveChangesInheritsDirective()
+ {
+ var filter = new ViewTypeParserFilter();
+ var attributes = new Dictionary<string, string> { { "inherits", "foobar(of baz)" } };
+ var builder = new MvcBuilder();
+
+ filter.PreprocessDirective("control", attributes);
+ filter.ParseComplete(builder);
+
+ Assert.Equal(typeof(ViewUserControl).FullName, attributes["inherits"]);
+ Assert.Equal("foobar(of baz)", builder.Inherits);
+ }
+
+ [Fact]
+ public void VBGenericMasterDirectiveChangesInheritsDirective()
+ {
+ var filter = new ViewTypeParserFilter();
+ var attributes = new Dictionary<string, string> { { "inherits", "foobar(of baz)" } };
+ var builder = new MvcBuilder();
+
+ filter.PreprocessDirective("master", attributes);
+ filter.ParseComplete(builder);
+
+ Assert.Equal(typeof(ViewMasterPage).FullName, attributes["inherits"]);
+ Assert.Equal("foobar(of baz)", builder.Inherits);
+ }
+
+ [Fact]
+ public void VBDirectivesAfterPageDirectiveProperlyPreserveInheritsDirective()
+ {
+ var filter = new ViewTypeParserFilter();
+ var pageAttributes = new Dictionary<string, string> { { "inherits", "foobar(of baz)" } };
+ var importAttributes = new Dictionary<string, string> { { "inherits", "dummyvalue(of baz)" } };
+ var builder = new MvcBuilder();
+
+ filter.PreprocessDirective("page", pageAttributes);
+ filter.PreprocessDirective("import", importAttributes);
+ filter.ParseComplete(builder);
+
+ Assert.Equal(typeof(ViewPage).FullName, pageAttributes["inherits"]);
+ Assert.Equal("foobar(of baz)", builder.Inherits);
+ }
+
+ // Helpers
+
+ private class MvcBuilder : RootBuilder, IMvcControlBuilder
+ {
+ public string Inherits { get; set; }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ViewUserControlControlBuilderTest.cs b/test/System.Web.Mvc.Test/Test/ViewUserControlControlBuilderTest.cs
new file mode 100644
index 00000000..94fbcf39
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ViewUserControlControlBuilderTest.cs
@@ -0,0 +1,39 @@
+using System.CodeDom;
+using System.Linq;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class ViewUserControlControlBuilderTest
+ {
+ [Fact]
+ public void BuilderWithoutInheritsDoesNothing()
+ {
+ // Arrange
+ var builder = new ViewUserControlControlBuilder();
+ var derivedType = new CodeTypeDeclaration();
+ derivedType.BaseTypes.Add("basetype");
+
+ // Act
+ builder.ProcessGeneratedCode(null, null, derivedType, null, null);
+
+ // Assert
+ Assert.Equal("basetype", derivedType.BaseTypes.Cast<CodeTypeReference>().Single().BaseType);
+ }
+
+ [Fact]
+ public void BuilderWithInheritsSetsBaseType()
+ {
+ // Arrange
+ var builder = new ViewUserControlControlBuilder { Inherits = "inheritedtype" };
+ var derivedType = new CodeTypeDeclaration();
+ derivedType.BaseTypes.Add("basetype");
+
+ // Act
+ builder.ProcessGeneratedCode(null, null, derivedType, null, null);
+
+ // Assert
+ Assert.Equal("inheritedtype", derivedType.BaseTypes.Cast<CodeTypeReference>().Single().BaseType);
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/ViewUserControlTest.cs b/test/System.Web.Mvc.Test/Test/ViewUserControlTest.cs
new file mode 100644
index 00000000..37e8bdd1
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/ViewUserControlTest.cs
@@ -0,0 +1,538 @@
+using System.IO;
+using System.Web.Routing;
+using System.Web.TestUtil;
+using System.Web.UI;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class ViewUserControlTest
+ {
+ [Fact]
+ public void ModelProperty()
+ {
+ // Arrange
+ object model = new object();
+ ViewDataDictionary viewData = new ViewDataDictionary(model);
+ ViewUserControl viewUserControl = new ViewUserControl();
+ viewUserControl.ViewData = viewData;
+
+ // Act
+ object viewPageModel = viewUserControl.Model;
+
+ // Assert
+ Assert.Equal(model, viewPageModel);
+ Assert.Equal(model, viewUserControl.ViewData.Model);
+ }
+
+ [Fact]
+ public void ModelPropertyStronglyTyped()
+ {
+ // Arrange
+ FooModel model = new FooModel();
+ ViewDataDictionary<FooModel> viewData = new ViewDataDictionary<FooModel>(model);
+ ViewUserControl<FooModel> viewUserControl = new ViewUserControl<FooModel>();
+ viewUserControl.ViewData = viewData;
+
+ // Act
+ object viewPageModelObject = ((ViewUserControl)viewUserControl).Model;
+ FooModel viewPageModelPerson = viewUserControl.Model;
+
+ // Assert
+ Assert.Equal(model, viewPageModelObject);
+ Assert.Equal(model, viewPageModelPerson);
+ }
+
+ [Fact]
+ public void RenderViewAndRestoreContentType()
+ {
+ // Arrange
+ Mock<ViewContext> mockViewContext = new Mock<ViewContext>();
+ mockViewContext.SetupProperty(c => c.HttpContext.Response.ContentType);
+ ViewContext vc = mockViewContext.Object;
+
+ Mock<ViewPage> mockViewPage = new Mock<ViewPage>();
+ mockViewPage.Setup(vp => vp.RenderView(vc)).Callback(() => vc.HttpContext.Response.ContentType = "newContentType");
+
+ // Act
+ vc.HttpContext.Response.ContentType = "oldContentType";
+ ViewUserControl.RenderViewAndRestoreContentType(mockViewPage.Object, vc);
+ string postContentType = vc.HttpContext.Response.ContentType;
+
+ // Assert
+ Assert.Equal("oldContentType", postContentType);
+ }
+
+ [Fact]
+ public void SetViewItem()
+ {
+ // Arrange
+ ViewUserControl vuc = new ViewUserControl();
+ object viewItem = new object();
+ vuc.ViewData = new ViewDataDictionary(viewItem);
+
+ // Act
+ vuc.ViewData.Model = viewItem;
+ object newViewItem = vuc.ViewData.Model;
+
+ // Assert
+ Assert.Same(viewItem, newViewItem);
+ }
+
+ [Fact]
+ public void SetViewItemOnBaseClassPropagatesToDerivedClass()
+ {
+ // Arrange
+ ViewUserControl<object> vucInt = new ViewUserControl<object>();
+ ViewUserControl vuc = vucInt;
+ vuc.ViewData = new ViewDataDictionary();
+ object o = new object();
+
+ // Act
+ vuc.ViewData.Model = o;
+
+ // Assert
+ Assert.Equal(o, vucInt.ViewData.Model);
+ Assert.Equal(o, vuc.ViewData.Model);
+ }
+
+ [Fact]
+ public void SetViewItemOnDerivedClassPropagatesToBaseClass()
+ {
+ // Arrange
+ ViewUserControl<object> vucInt = new ViewUserControl<object>();
+ ViewUserControl vuc = vucInt;
+ vucInt.ViewData = new ViewDataDictionary<object>();
+ object o = new object();
+
+ // Act
+ vucInt.ViewData.Model = o;
+
+ // Assert
+ Assert.Equal(o, vucInt.ViewData.Model);
+ Assert.Equal(o, vuc.ViewData.Model);
+ }
+
+ [Fact]
+ public void SetViewItemToWrongTypeThrows()
+ {
+ // Arrange
+ ViewUserControl<string> vucString = new ViewUserControl<string>();
+ vucString.ViewData = new ViewDataDictionary<string>();
+ ViewUserControl vuc = vucString;
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ delegate { vuc.ViewData.Model = 50; },
+ "The model item passed into the dictionary is of type 'System.Int32', but this dictionary requires a model item of type 'System.String'.");
+ }
+
+ [Fact]
+ public void GetViewDataWhenNoPageSetThrows()
+ {
+ ViewUserControl vuc = new ViewUserControl();
+ vuc.AppRelativeVirtualPath = "~/Foo.ascx";
+
+ Assert.Throws<InvalidOperationException>(
+ delegate { var foo = vuc.ViewData["Foo"]; },
+ "The ViewUserControl '~/Foo.ascx' cannot find an IViewDataContainer object. The ViewUserControl must be inside a ViewPage, a ViewMasterPage, or another ViewUserControl.");
+ }
+
+ [Fact]
+ public void GetViewDataWhenRegularPageSetThrows()
+ {
+ Page p = new Page();
+ p.Controls.Add(new Control());
+ ViewUserControl vuc = new ViewUserControl();
+ p.Controls[0].Controls.Add(vuc);
+ vuc.AppRelativeVirtualPath = "~/Foo.ascx";
+
+ Assert.Throws<InvalidOperationException>(
+ delegate { var foo = vuc.ViewData["Foo"]; },
+ "The ViewUserControl '~/Foo.ascx' cannot find an IViewDataContainer object. The ViewUserControl must be inside a ViewPage, a ViewMasterPage, or another ViewUserControl.");
+ }
+
+ [Fact]
+ public void GetViewDataFromViewPage()
+ {
+ // Arrange
+ ViewPage p = new ViewPage();
+ p.Controls.Add(new Control());
+ ViewUserControl vuc = new ViewUserControl();
+ p.Controls[0].Controls.Add(vuc);
+ p.ViewData = new ViewDataDictionary { { "FirstName", "Joe" }, { "LastName", "Schmoe" } };
+
+ // Act
+ object firstName = vuc.ViewData.Eval("FirstName");
+ object lastName = vuc.ViewData.Eval("LastName");
+
+ // Assert
+ Assert.Equal("Joe", firstName);
+ Assert.Equal("Schmoe", lastName);
+ }
+
+ [Fact]
+ public void GetViewDataFromViewPageWithViewDataKeyPointingToObject()
+ {
+ // Arrange
+ ViewDataDictionary vdd = new ViewDataDictionary()
+ {
+ { "Foo", "FooParent" },
+ { "Bar", "BarParent" },
+ { "Child", new object() }
+ };
+
+ ViewPage p = new ViewPage();
+ p.Controls.Add(new Control());
+ ViewUserControl vuc = new ViewUserControl() { ViewDataKey = "Child" };
+ p.Controls[0].Controls.Add(vuc);
+ p.ViewData = vdd;
+
+ // Act
+ object oFoo = vuc.ViewData.Eval("Foo");
+ object oBar = vuc.ViewData.Eval("Bar");
+
+ // Assert
+ Assert.Equal(vdd["Child"], vuc.ViewData.Model);
+ Assert.Equal("FooParent", oFoo);
+ Assert.Equal("BarParent", oBar);
+ }
+
+ [Fact]
+ public void GetViewDataFromViewPageWithViewDataKeyPointingToViewDataDictionary()
+ {
+ // Arrange
+ ViewDataDictionary vdd = new ViewDataDictionary()
+ {
+ { "Foo", "FooParent" },
+ { "Bar", "BarParent" },
+ {
+ "Child",
+ new ViewDataDictionary()
+ {
+ { "Foo", "FooChild" },
+ { "Bar", "BarChild" }
+ }
+ }
+ };
+
+ ViewPage p = new ViewPage();
+ p.Controls.Add(new Control());
+ ViewUserControl vuc = new ViewUserControl() { ViewDataKey = "Child" };
+ p.Controls[0].Controls.Add(vuc);
+ p.ViewData = vdd;
+
+ // Act
+ object oFoo = vuc.ViewData.Eval("Foo");
+ object oBar = vuc.ViewData.Eval("Bar");
+
+ // Assert
+ Assert.Equal(vdd["Child"], vuc.ViewData);
+ Assert.Equal("FooChild", oFoo);
+ Assert.Equal("BarChild", oBar);
+ }
+
+ [Fact]
+ public void GetViewDataFromViewUserControl()
+ {
+ // Arrange
+ ViewPage p = new ViewPage();
+ p.Controls.Add(new Control());
+ ViewUserControl outerVuc = new ViewUserControl();
+ p.Controls[0].Controls.Add(outerVuc);
+ outerVuc.Controls.Add(new Control());
+ ViewUserControl vuc = new ViewUserControl();
+ outerVuc.Controls[0].Controls.Add(vuc);
+
+ p.ViewData = new ViewDataDictionary { { "FirstName", "Joe" }, { "LastName", "Schmoe" } };
+
+ // Act
+ object firstName = vuc.ViewData.Eval("FirstName");
+ object lastName = vuc.ViewData.Eval("LastName");
+
+ // Assert
+ Assert.Equal("Joe", firstName);
+ Assert.Equal("Schmoe", lastName);
+ }
+
+ [Fact]
+ public void GetViewDataFromViewUserControlWithViewDataKeyOnInnerControl()
+ {
+ // Arrange
+ ViewPage p = new ViewPage();
+ p.Controls.Add(new Control());
+ ViewUserControl outerVuc = new ViewUserControl();
+ p.Controls[0].Controls.Add(outerVuc);
+ outerVuc.Controls.Add(new Control());
+ ViewUserControl vuc = new ViewUserControl() { ViewDataKey = "SubData" };
+ outerVuc.Controls[0].Controls.Add(vuc);
+
+ p.ViewData = new ViewDataDictionary { { "FirstName", "Joe" }, { "LastName", "Schmoe" } };
+ p.ViewData["SubData"] = new ViewDataDictionary { { "FirstName", "SubJoe" }, { "LastName", "SubSchmoe" } };
+
+ // Act
+ object firstName = vuc.ViewData.Eval("FirstName");
+ object lastName = vuc.ViewData.Eval("LastName");
+
+ // Assert
+ Assert.Equal("SubJoe", firstName);
+ Assert.Equal("SubSchmoe", lastName);
+ }
+
+ [Fact]
+ public void GetViewDataFromViewUserControlWithViewDataKeyOnOuterControl()
+ {
+ // Arrange
+ ViewPage p = new ViewPage();
+ p.Controls.Add(new Control());
+ ViewUserControl outerVuc = new ViewUserControl() { ViewDataKey = "SubData" };
+ p.Controls[0].Controls.Add(outerVuc);
+ outerVuc.Controls.Add(new Control());
+ ViewUserControl vuc = new ViewUserControl();
+ outerVuc.Controls[0].Controls.Add(vuc);
+
+ p.ViewData = new ViewDataDictionary { { "FirstName", "Joe" }, { "LastName", "Schmoe" } };
+ p.ViewData["SubData"] = new ViewDataDictionary { { "FirstName", "SubJoe" }, { "LastName", "SubSchmoe" } };
+
+ // Act
+ object firstName = vuc.ViewData.Eval("FirstName");
+ object lastName = vuc.ViewData.Eval("LastName");
+
+ // Assert
+ Assert.Equal("SubJoe", firstName);
+ Assert.Equal("SubSchmoe", lastName);
+ }
+
+ [Fact]
+ public void ViewDataKeyProperty()
+ {
+ MemberHelper.TestStringProperty(new ViewUserControl(), "ViewDataKey", String.Empty, testDefaultValueAttribute: true);
+ }
+
+ [Fact]
+ public void GetWrongGenericViewItemTypeThrows()
+ {
+ // Arrange
+ ViewPage p = new ViewPage();
+ p.ViewData = new ViewDataDictionary();
+ p.ViewData["Foo"] = new DummyViewData { MyInt = 123, MyString = "Whatever" };
+
+ MockViewUserControl<MyViewData> vuc = new MockViewUserControl<MyViewData>() { ViewDataKey = "FOO" };
+ vuc.AppRelativeVirtualPath = "~/Foo.aspx";
+ p.Controls.Add(new Control());
+ p.Controls[0].Controls.Add(vuc);
+
+ // Act
+ Assert.Throws<InvalidOperationException>(
+ delegate { var foo = vuc.ViewData.Model.IntProp; },
+ @"The model item passed into the dictionary is of type 'System.Web.Mvc.Test.ViewUserControlTest+DummyViewData', but this dictionary requires a model item of type 'System.Web.Mvc.Test.ViewUserControlTest+MyViewData'.");
+ }
+
+ [Fact]
+ public void GetGenericViewItemType()
+ {
+ // Arrange
+ ViewPage p = new ViewPage();
+ p.Controls.Add(new Control());
+ MockViewUserControl<MyViewData> vuc = new MockViewUserControl<MyViewData>() { ViewDataKey = "FOO" };
+ p.Controls[0].Controls.Add(vuc);
+ p.ViewData = new ViewDataDictionary();
+ p.ViewData["Foo"] = new MyViewData { IntProp = 123, StringProp = "miao" };
+
+ // Act
+ int intProp = vuc.ViewData.Model.IntProp;
+ string stringProp = vuc.ViewData.Model.StringProp;
+
+ // Assert
+ Assert.Equal(123, intProp);
+ Assert.Equal("miao", stringProp);
+ }
+
+ [Fact]
+ public void GetHtmlHelperFromViewPage()
+ {
+ // Arrange
+ ViewUserControl vuc = new ViewUserControl();
+ ViewPage containerPage = new ViewPage();
+ containerPage.Controls.Add(vuc);
+ ViewContext vc = new Mock<ViewContext>().Object;
+ vuc.ViewContext = vc;
+
+ // Act
+ HtmlHelper htmlHelper = vuc.Html;
+
+ // Assert
+ Assert.Equal(vc, htmlHelper.ViewContext);
+ Assert.Equal(vuc, htmlHelper.ViewDataContainer);
+ }
+
+ [Fact]
+ public void GetHtmlHelperFromRegularPage()
+ {
+ // Arrange
+ ViewUserControl vuc = new ViewUserControl();
+ Page containerPage = new Page();
+ containerPage.Controls.Add(vuc);
+
+ // Assert
+ Assert.Throws<InvalidOperationException>(
+ delegate { HtmlHelper foo = vuc.Html; },
+ "A ViewUserControl can be used only in pages that derive from ViewPage or ViewPage<TModel>.");
+ }
+
+ [Fact]
+ public void GetUrlHelperFromViewPage()
+ {
+ // Arrange
+ ViewUserControl vuc = new ViewUserControl();
+ ViewPage containerPage = new ViewPage();
+ containerPage.Controls.Add(vuc);
+ RequestContext rc = new RequestContext(new Mock<HttpContextBase>().Object, new RouteData());
+ UrlHelper urlHelper = new UrlHelper(rc);
+ containerPage.Url = urlHelper;
+
+ // Assert
+ Assert.Equal(vuc.Url, urlHelper);
+ }
+
+ [Fact]
+ public void GetUrlHelperFromRegularPage()
+ {
+ // Arrange
+ ViewUserControl vuc = new ViewUserControl();
+ Page containerPage = new Page();
+ containerPage.Controls.Add(vuc);
+
+ // Assert
+ Assert.Throws<InvalidOperationException>(
+ delegate { UrlHelper foo = vuc.Url; },
+ "A ViewUserControl can be used only in pages that derive from ViewPage or ViewPage<TModel>.");
+ }
+
+ [Fact]
+ public void GetWriterFromViewPage()
+ {
+ // Arrange
+ MockViewUserControl vuc = new MockViewUserControl();
+ MockViewUserControlContainerPage containerPage = new MockViewUserControlContainerPage(vuc);
+ bool triggered = false;
+ HtmlTextWriter writer = new HtmlTextWriter(TextWriter.Null);
+ containerPage.RenderCallback = delegate()
+ {
+ triggered = true;
+ Assert.Equal(writer, vuc.Writer);
+ };
+
+ // Act & Assert
+ Assert.Null(vuc.Writer);
+ containerPage.RenderControl(writer);
+ Assert.Null(vuc.Writer);
+ Assert.True(triggered);
+ }
+
+ [Fact]
+ public void GetWriterFromRegularPageThrows()
+ {
+ // Arrange
+ MockViewUserControl vuc = new MockViewUserControl();
+ Page containerPage = new Page();
+ containerPage.Controls.Add(vuc);
+
+ // Act
+ Assert.Throws<InvalidOperationException>(
+ delegate { HtmlTextWriter writer = vuc.Writer; },
+ "A ViewUserControl can be used only in pages that derive from ViewPage or ViewPage<TModel>.");
+ }
+
+ [Fact]
+ public void ViewBagProperty_ReflectsViewData()
+ {
+ // Arrange
+ ViewPage containerPage = new ViewPage();
+ ViewUserControl userControl = new ViewUserControl();
+ containerPage.Controls.Add(userControl);
+ userControl.ViewData["A"] = 1;
+
+ // Act & Assert
+ Assert.NotNull(userControl.ViewBag);
+ Assert.Equal(1, userControl.ViewBag.A);
+ }
+
+ [Fact]
+ public void ViewBagProperty_ReflectsNewViewDataInstance()
+ {
+ // Arrange
+ ViewPage containerPage = new ViewPage();
+ ViewUserControl userControl = new ViewUserControl();
+ containerPage.Controls.Add(userControl);
+ userControl.ViewData["A"] = 1;
+ userControl.ViewData = new ViewDataDictionary() { { "A", "bar" } };
+
+ // Act & Assert
+ Assert.Equal("bar", userControl.ViewBag.A);
+ }
+
+ [Fact]
+ public void ViewBagProperty_PropagatesChangesToViewData()
+ {
+ // Arrange
+ ViewPage containerPage = new ViewPage();
+ ViewUserControl userControl = new ViewUserControl();
+ containerPage.Controls.Add(userControl);
+ userControl.ViewData["A"] = 1;
+
+ // Act
+ userControl.ViewBag.A = "foo";
+ userControl.ViewBag.B = 2;
+
+ // Assert
+ Assert.Equal("foo", userControl.ViewData["A"]);
+ Assert.Equal(2, userControl.ViewData["B"]);
+ }
+
+ private sealed class DummyViewData
+ {
+ public int MyInt { get; set; }
+ public string MyString { get; set; }
+ }
+
+ private sealed class MockViewUserControlContainerPage : ViewPage
+ {
+ public Action RenderCallback { get; set; }
+
+ public MockViewUserControlContainerPage(ViewUserControl userControl)
+ {
+ Controls.Add(userControl);
+ }
+
+ protected override void RenderChildren(HtmlTextWriter writer)
+ {
+ if (RenderCallback != null)
+ {
+ RenderCallback();
+ }
+ base.RenderChildren(writer);
+ }
+ }
+
+ private sealed class MockViewUserControl : ViewUserControl
+ {
+ }
+
+ private sealed class MockViewUserControl<TViewData> : ViewUserControl<TViewData>
+ {
+ }
+
+ private sealed class MyViewData
+ {
+ public int IntProp { get; set; }
+ public string StringProp { get; set; }
+ }
+
+ private sealed class FooModel
+ {
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/VirtualPathProviderViewEngineTest.cs b/test/System.Web.Mvc.Test/Test/VirtualPathProviderViewEngineTest.cs
new file mode 100644
index 00000000..555988df
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/VirtualPathProviderViewEngineTest.cs
@@ -0,0 +1,1412 @@
+using System.Collections;
+using System.IO;
+using System.Linq;
+using System.Web.Hosting;
+using System.Web.Routing;
+using System.Web.WebPages;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class VirtualPathProviderViewEngineTest
+ {
+ [Fact]
+ public void FindView_NullControllerContext_Throws()
+ {
+ // Arrange
+ TestableVirtualPathProviderViewEngine engine = new TestableVirtualPathProviderViewEngine();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => engine.FindView(null, "view name", null, false),
+ "controllerContext"
+ );
+ }
+
+ [Fact]
+ public void FindView_NullViewName_Throws()
+ {
+ // Arrange
+ ControllerContext context = CreateContext();
+ TestableVirtualPathProviderViewEngine engine = new TestableVirtualPathProviderViewEngine();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ () => engine.FindView(context, null, null, false),
+ "viewName"
+ );
+ }
+
+ [Fact]
+ public void FindView_EmptyViewName_Throws()
+ {
+ // Arrange
+ ControllerContext context = CreateContext();
+ TestableVirtualPathProviderViewEngine engine = new TestableVirtualPathProviderViewEngine();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ () => engine.FindView(context, "", null, false),
+ "viewName"
+ );
+ }
+
+ [Fact]
+ public void FindView_ControllerNameNotInRequestContext_Throws()
+ {
+ // Arrange
+ TestableVirtualPathProviderViewEngine engine = new TestableVirtualPathProviderViewEngine();
+ ControllerContext context = CreateContext();
+ context.RouteData.Values.Remove("controller");
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ () => engine.FindView(context, "viewName", null, false),
+ "The RouteData must contain an item named 'controller' with a non-empty string value."
+ );
+ }
+
+ [Fact]
+ public void FindView_EmptyViewLocations_Throws()
+ {
+ // Arrange
+ ControllerContext context = CreateContext();
+ TestableVirtualPathProviderViewEngine engine = new TestableVirtualPathProviderViewEngine();
+ engine.ClearViewLocations();
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ () => engine.FindView(context, "viewName", null, false),
+ "The property 'ViewLocationFormats' cannot be null or empty."
+ );
+ }
+
+ [Fact]
+ public void FindView_ViewDoesNotExistAndNoMaster_ReturnsSearchedLocationsResult()
+ {
+ // Arrange
+ ControllerContext context = CreateContext();
+ TestableVirtualPathProviderViewEngine engine = new TestableVirtualPathProviderViewEngine();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/vpath/controllerName/viewName.view"))
+ .Returns(false)
+ .Verifiable();
+
+ // Act
+ ViewEngineResult result = engine.FindView(context, "viewName", null, false);
+
+ // Assert
+ Assert.Null(result.View);
+ Assert.Single(result.SearchedLocations);
+ Assert.True(result.SearchedLocations.Contains("~/vpath/controllerName/viewName.view"));
+ engine.MockPathProvider.Verify();
+ }
+
+ [Fact]
+ public void FindView_VirtualPathViewDoesNotExistAndNoMaster_ReturnsSearchedLocationsResult()
+ {
+ // Arrange
+ ControllerContext context = CreateContext();
+ TestableVirtualPathProviderViewEngine engine = new TestableVirtualPathProviderViewEngine();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/foo/bar.view"))
+ .Returns(false)
+ .Verifiable();
+ engine.MockCache
+ .Setup(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), ""))
+ .Verifiable();
+
+ // Act
+ ViewEngineResult result = engine.FindView(context, "~/foo/bar.view", null, false);
+
+ // Assert
+ Assert.Null(result.View);
+ Assert.Single(result.SearchedLocations);
+ Assert.True(result.SearchedLocations.Contains("~/foo/bar.view"));
+ engine.MockPathProvider.Verify();
+ engine.MockCache.Verify();
+ }
+
+ [Fact]
+ public void FindView_VirtualPathViewNotSupportedAndNoMaster_ReturnsSearchedLocationsResult()
+ {
+ // Arrange
+ ControllerContext context = CreateContext();
+ TestableVirtualPathProviderViewEngine engine = new TestableVirtualPathProviderViewEngine();
+ engine.MockCache
+ .Setup(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), ""))
+ .Verifiable();
+
+ // Act
+ ViewEngineResult result = engine.FindView(context, "~/foo/bar.unsupported", null, false);
+
+ // Assert
+ Assert.Null(result.View);
+ Assert.Single(result.SearchedLocations);
+ Assert.True(result.SearchedLocations.Contains("~/foo/bar.unsupported"));
+ engine.MockPathProvider.Verify(vpp => vpp.FileExists("~/foo/bar.unsupported"), Times.Never());
+ engine.MockCache.Verify();
+ }
+
+ [Fact]
+ public void FindView_AbsolutePathViewDoesNotExistAndNoMaster_ReturnsSearchedLocationsResult()
+ {
+ // Arrange
+ ControllerContext context = CreateContext();
+ TestableVirtualPathProviderViewEngine engine = new TestableVirtualPathProviderViewEngine();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("/foo/bar.view"))
+ .Returns(false)
+ .Verifiable();
+ engine.MockCache
+ .Setup(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), ""))
+ .Verifiable();
+
+ // Act
+ ViewEngineResult result = engine.FindView(context, "/foo/bar.view", null, false);
+
+ // Assert
+ Assert.Null(result.View);
+ Assert.Single(result.SearchedLocations);
+ Assert.True(result.SearchedLocations.Contains("/foo/bar.view"));
+ engine.MockPathProvider.Verify();
+ engine.MockCache.Verify();
+ }
+
+ [Fact]
+ public void FindView_AbsolutePathViewNotSupportedAndNoMaster_ReturnsSearchedLocationsResult()
+ {
+ // Arrange
+ ControllerContext context = CreateContext();
+ TestableVirtualPathProviderViewEngine engine = new TestableVirtualPathProviderViewEngine();
+ engine.MockCache
+ .Setup(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), ""))
+ .Verifiable();
+
+ // Act
+ ViewEngineResult result = engine.FindView(context, "/foo/bar.unsupported", null, false);
+
+ // Assert
+ Assert.Null(result.View);
+ Assert.Single(result.SearchedLocations);
+ Assert.True(result.SearchedLocations.Contains("/foo/bar.unsupported"));
+ engine.MockPathProvider.Verify(vpp => vpp.FileExists("/foo/bar.unsupported"), Times.Never());
+ engine.MockCache.Verify();
+ }
+
+ [Fact]
+ public void FindView_ViewExistsAndNoMaster_ReturnsView()
+ {
+ // Arrange
+ ControllerContext context = CreateContext();
+ TestableVirtualPathProviderViewEngine engine = new TestableVirtualPathProviderViewEngine();
+ engine.ClearMasterLocations(); // If master is not provided, master locations can be empty
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/vpath/controllerName/viewName.view"))
+ .Returns(true)
+ .Verifiable();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/vpath/controllerName/viewName.Mobile.view"))
+ .Returns(false)
+ .Verifiable();
+ engine.MockCache
+ .Setup(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), "~/vpath/controllerName/viewName.view"))
+ .Verifiable();
+
+ // Act
+ ViewEngineResult result = engine.FindView(context, "viewName", null, false);
+
+ // Assert
+ Assert.Same(engine.CreateViewResult, result.View);
+ Assert.Null(result.SearchedLocations);
+ Assert.Same(context, engine.CreateViewControllerContext);
+ Assert.Equal("~/vpath/controllerName/viewName.view", engine.CreateViewViewPath);
+ Assert.Equal(String.Empty, engine.CreateViewMasterPath);
+ engine.MockPathProvider.Verify();
+ engine.MockCache.Verify();
+ }
+
+ [Fact]
+ public void FindView_VirtualPathViewExistsAndNoMaster_ReturnsView()
+ {
+ // Arrange
+ ControllerContext context = CreateContext();
+ TestableVirtualPathProviderViewEngine engine = new TestableVirtualPathProviderViewEngine();
+ engine.ClearMasterLocations();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/foo/bar.view"))
+ .Returns(true)
+ .Verifiable();
+ engine.MockCache
+ .Setup(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), "~/foo/bar.view"))
+ .Verifiable();
+
+ // Act
+ ViewEngineResult result = engine.FindView(context, "~/foo/bar.view", null, false);
+
+ // Assert
+ Assert.Same(engine.CreateViewResult, result.View);
+ Assert.Null(result.SearchedLocations);
+ Assert.Same(context, engine.CreateViewControllerContext);
+ Assert.Equal("~/foo/bar.view", engine.CreateViewViewPath);
+ Assert.Equal(String.Empty, engine.CreateViewMasterPath);
+ engine.MockPathProvider.Verify();
+ engine.MockCache.Verify();
+ }
+
+ [Fact]
+ public void FindView_VirtualPathViewExistsAndNoMaster_Legacy_ReturnsView()
+ {
+ // Arrange
+ ControllerContext context = CreateContext();
+ TestableVirtualPathProviderViewEngine engine = new TestableVirtualPathProviderViewEngine()
+ {
+ FileExtensions = null, // Set FileExtensions to null to simulate View Engines that do not set this property
+ };
+ engine.ClearMasterLocations();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/foo/bar.unsupported"))
+ .Returns(true)
+ .Verifiable();
+ engine.MockCache
+ .Setup(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), "~/foo/bar.unsupported"))
+ .Verifiable();
+
+ // Act
+ ViewEngineResult result = engine.FindView(context, "~/foo/bar.unsupported", null, false);
+
+ // Assert
+ Assert.Same(engine.CreateViewResult, result.View);
+ Assert.Null(result.SearchedLocations);
+ Assert.Same(context, engine.CreateViewControllerContext);
+ Assert.Equal("~/foo/bar.unsupported", engine.CreateViewViewPath);
+ Assert.Equal(String.Empty, engine.CreateViewMasterPath);
+ engine.MockPathProvider.Verify();
+ engine.MockCache.Verify();
+ }
+
+ [Fact]
+ public void FindView_AbsolutePathViewExistsAndNoMaster_ReturnsView()
+ {
+ // Arrange
+ ControllerContext context = CreateContext();
+ TestableVirtualPathProviderViewEngine engine = new TestableVirtualPathProviderViewEngine();
+ engine.ClearMasterLocations();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("/foo/bar.view"))
+ .Returns(true)
+ .Verifiable();
+ engine.MockCache
+ .Setup(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), "/foo/bar.view"))
+ .Verifiable();
+
+ // Act
+ ViewEngineResult result = engine.FindView(context, "/foo/bar.view", null, false);
+
+ // Assert
+ Assert.Same(engine.CreateViewResult, result.View);
+ Assert.Null(result.SearchedLocations);
+ Assert.Same(context, engine.CreateViewControllerContext);
+ Assert.Equal("/foo/bar.view", engine.CreateViewViewPath);
+ Assert.Equal(String.Empty, engine.CreateViewMasterPath);
+ engine.MockPathProvider.Verify();
+ engine.MockCache.Verify();
+ }
+
+ [Fact]
+ public void FindView_AbsolutePathViewExistsAndNoMaster_Legacy_ReturnsView()
+ {
+ // Arrange
+ ControllerContext context = CreateContext();
+ TestableVirtualPathProviderViewEngine engine = new TestableVirtualPathProviderViewEngine()
+ {
+ FileExtensions = null, // Set FileExtensions to null to simulate View Engines that do not set this property
+ };
+ engine.ClearMasterLocations();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("/foo/bar.unsupported"))
+ .Returns(true)
+ .Verifiable();
+ engine.MockCache
+ .Setup(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), "/foo/bar.unsupported"))
+ .Verifiable();
+
+ // Act
+ ViewEngineResult result = engine.FindView(context, "/foo/bar.unsupported", null, false);
+
+ // Assert
+ Assert.Same(engine.CreateViewResult, result.View);
+ Assert.Null(result.SearchedLocations);
+ Assert.Same(context, engine.CreateViewControllerContext);
+ Assert.Equal("/foo/bar.unsupported", engine.CreateViewViewPath);
+ Assert.Equal(String.Empty, engine.CreateViewMasterPath);
+ engine.MockPathProvider.Verify();
+ engine.MockCache.Verify();
+ }
+
+ [Fact]
+ public void FindView_ViewExistsAndMasterNameProvidedButEmptyMasterLocations_Throws()
+ {
+ // Arrange
+ ControllerContext context = CreateContext();
+ TestableVirtualPathProviderViewEngine engine = new TestableVirtualPathProviderViewEngine();
+ engine.ClearMasterLocations();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/vpath/controllerName/viewName.view"))
+ .Returns(true)
+ .Verifiable();
+ engine.MockCache
+ .Setup(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), "~/vpath/controllerName/viewName.view"))
+ .Verifiable();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/vpath/controllerName/viewName.Mobile.view"))
+ .Returns(false)
+ .Verifiable();
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ () => engine.FindView(context, "viewName", "masterName", false),
+ "The property 'MasterLocationFormats' cannot be null or empty."
+ );
+ engine.MockPathProvider.Verify();
+ engine.MockCache.Verify();
+ }
+
+ [Fact]
+ public void FindView_ViewDoesNotExistAndMasterDoesNotExist_ReturnsSearchedLocationsResult()
+ {
+ // Arrange
+ ControllerContext context = CreateContext();
+ TestableVirtualPathProviderViewEngine engine = new TestableVirtualPathProviderViewEngine();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/vpath/controllerName/viewName.view"))
+ .Returns(false)
+ .Verifiable();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/vpath/controllerName/masterName.master"))
+ .Returns(false)
+ .Verifiable();
+
+ // Act
+ ViewEngineResult result = engine.FindView(context, "viewName", "masterName", false);
+
+ // Assert
+ Assert.Null(result.View);
+ Assert.Equal(2, result.SearchedLocations.Count()); // Both view and master locations
+ Assert.True(result.SearchedLocations.Contains("~/vpath/controllerName/viewName.view"));
+ Assert.True(result.SearchedLocations.Contains("~/vpath/controllerName/masterName.master"));
+ engine.MockPathProvider.Verify();
+ }
+
+ [Fact]
+ public void FindView_ViewExistsButMasterDoesNotExist_ReturnsSearchedLocationsResult()
+ {
+ // Arrange
+ ControllerContext context = CreateContext();
+ TestableVirtualPathProviderViewEngine engine = new TestableVirtualPathProviderViewEngine();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/vpath/controllerName/viewName.view"))
+ .Returns(true)
+ .Verifiable();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/vpath/controllerName/viewName.Mobile.view"))
+ .Returns(false)
+ .Verifiable();
+ engine.MockCache
+ .Setup(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), "~/vpath/controllerName/viewName.view"))
+ .Verifiable();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/vpath/controllerName/masterName.master"))
+ .Returns(false)
+ .Verifiable();
+
+ // Act
+ ViewEngineResult result = engine.FindView(context, "viewName", "masterName", false);
+
+ // Assert
+ Assert.Null(result.View);
+ Assert.Single(result.SearchedLocations); // View was found, not included in 'searched locations'
+ Assert.True(result.SearchedLocations.Contains("~/vpath/controllerName/masterName.master"));
+ engine.MockPathProvider.Verify();
+ engine.MockCache.Verify();
+ }
+
+ [Fact]
+ public void FindView_MasterInAreaDoesNotExist_ReturnsSearchedLocationsResult()
+ {
+ // Arrange
+ ControllerContext context = CreateContext();
+ context.RouteData.DataTokens["area"] = "areaName";
+
+ TestableVirtualPathProviderViewEngine engine = new TestableVirtualPathProviderViewEngine();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/vpath/areaName/controllerName/viewName.view"))
+ .Returns(true)
+ .Verifiable();
+ engine.MockCache
+ .Setup(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), "~/vpath/areaName/controllerName/viewName.view"))
+ .Verifiable();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/vpath/areaName/controllerName/viewName.Mobile.view"))
+ .Returns(false)
+ .Verifiable();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/vpath/areaName/controllerName/masterName.master"))
+ .Returns(false)
+ .Verifiable();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/vpath/controllerName/masterName.master"))
+ .Returns(false)
+ .Verifiable();
+
+ // Act
+ ViewEngineResult result = engine.FindView(context, "viewName", "masterName", false);
+
+ // Assert
+ Assert.Null(result.View);
+ Assert.Equal(2, result.SearchedLocations.Count()); // View was found, not included in 'searched locations'
+ Assert.True(result.SearchedLocations.Contains("~/vpath/areaName/controllerName/masterName.master"));
+ Assert.True(result.SearchedLocations.Contains("~/vpath/controllerName/masterName.master"));
+ engine.MockPathProvider.Verify();
+ engine.MockCache.Verify();
+ }
+
+ [Fact]
+ public void FindView_ViewExistsAndMasterExists_ReturnsView()
+ {
+ // Arrange
+ ControllerContext context = CreateContext();
+ TestableVirtualPathProviderViewEngine engine = new TestableVirtualPathProviderViewEngine();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/vpath/controllerName/viewName.view"))
+ .Returns(true)
+ .Verifiable();
+ engine.MockCache
+ .Setup(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), "~/vpath/controllerName/viewName.view"))
+ .Verifiable();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/vpath/controllerName/viewName.Mobile.view"))
+ .Returns(false)
+ .Verifiable();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/vpath/controllerName/masterName.master"))
+ .Returns(true)
+ .Verifiable();
+ engine.MockCache
+ .Setup(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), "~/vpath/controllerName/masterName.master"))
+ .Verifiable();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/vpath/controllerName/masterName.Mobile.master"))
+ .Returns(false)
+ .Verifiable();
+
+ // Act
+ ViewEngineResult result = engine.FindView(context, "viewName", "masterName", false);
+
+ // Assert
+ Assert.Same(engine.CreateViewResult, result.View);
+ Assert.Null(result.SearchedLocations);
+ Assert.Same(context, engine.CreateViewControllerContext);
+ Assert.Equal("~/vpath/controllerName/viewName.view", engine.CreateViewViewPath);
+ Assert.Equal("~/vpath/controllerName/masterName.master", engine.CreateViewMasterPath);
+ engine.MockPathProvider.Verify();
+ engine.MockCache.Verify();
+ }
+
+ [Fact]
+ public void FindView_ViewInAreaExistsAndMasterExists_ReturnsView()
+ {
+ // Arrange
+ ControllerContext context = CreateContext();
+ context.RouteData.DataTokens["area"] = "areaName";
+
+ TestableVirtualPathProviderViewEngine engine = new TestableVirtualPathProviderViewEngine();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/vpath/areaName/controllerName/viewName.view"))
+ .Returns(true)
+ .Verifiable();
+ engine.MockCache
+ .Setup(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), "~/vpath/areaName/controllerName/viewName.view"))
+ .Verifiable();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/vpath/areaName/controllerName/viewName.Mobile.view"))
+ .Returns(false)
+ .Verifiable();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/vpath/areaName/controllerName/masterName.master"))
+ .Returns(false)
+ .Verifiable();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/vpath/controllerName/masterName.master"))
+ .Returns(true)
+ .Verifiable();
+ engine.MockCache
+ .Setup(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), "~/vpath/controllerName/masterName.master"))
+ .Verifiable();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/vpath/controllerName/masterName.Mobile.master"))
+ .Returns(false)
+ .Verifiable();
+
+ // Act
+ ViewEngineResult result = engine.FindView(context, "viewName", "masterName", false);
+
+ // Assert
+ Assert.Same(engine.CreateViewResult, result.View);
+ Assert.Null(result.SearchedLocations);
+ Assert.Same(context, engine.CreateViewControllerContext);
+ Assert.Equal("~/vpath/areaName/controllerName/viewName.view", engine.CreateViewViewPath);
+ Assert.Equal("~/vpath/controllerName/masterName.master", engine.CreateViewMasterPath);
+ engine.MockPathProvider.Verify();
+ engine.MockCache.Verify();
+ }
+
+ [Fact]
+ public void FindView_ViewInAreaExistsAndMasterExists_ReturnsView_Mobile()
+ {
+ // Arrange
+ ControllerContext context = CreateContext(isMobileDevice: true);
+ context.RouteData.DataTokens["area"] = "areaName";
+
+ TestableVirtualPathProviderViewEngine engine = new TestableVirtualPathProviderViewEngine();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/vpath/areaName/controllerName/viewName.view"))
+ .Returns(true)
+ .Verifiable();
+ engine.MockCache
+ .Setup(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), "~/vpath/areaName/controllerName/viewName.view"))
+ .Verifiable();
+
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/vpath/areaName/controllerName/viewName.Mobile.view"))
+ .Returns(true)
+ .Verifiable();
+ engine.MockCache
+ .Setup(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), "~/vpath/areaName/controllerName/viewName.Mobile.view"))
+ .Verifiable();
+
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/vpath/areaName/controllerName/masterName.master"))
+ .Returns(false)
+ .Verifiable();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/vpath/areaName/controllerName/masterName.Mobile.master"))
+ .Returns(false)
+ .Verifiable();
+
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/vpath/controllerName/masterName.master"))
+ .Returns(true)
+ .Verifiable();
+ engine.MockCache
+ .Setup(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), "~/vpath/controllerName/masterName.master"))
+ .Verifiable();
+
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/vpath/controllerName/masterName.Mobile.master"))
+ .Returns(true)
+ .Verifiable();
+ engine.MockCache
+ .Setup(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), "~/vpath/controllerName/masterName.Mobile.master"))
+ .Verifiable();
+
+ // Act
+ ViewEngineResult result = engine.FindView(context, "viewName", "masterName", false);
+
+ // Assert
+ Assert.Same(engine.CreateViewResult, result.View);
+ Assert.Null(result.SearchedLocations);
+ Assert.Same(context, engine.CreateViewControllerContext);
+ Assert.Equal("~/vpath/areaName/controllerName/viewName.Mobile.view", engine.CreateViewViewPath);
+ Assert.Equal("~/vpath/controllerName/masterName.Mobile.master", engine.CreateViewMasterPath);
+ engine.MockPathProvider.Verify();
+ engine.MockCache.Verify();
+ }
+
+ [Fact]
+ public void FindPartialView_NullControllerContext_Throws()
+ {
+ // Arrange
+ TestableVirtualPathProviderViewEngine engine = new TestableVirtualPathProviderViewEngine();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => engine.FindPartialView(null, "view name", false),
+ "controllerContext"
+ );
+ }
+
+ [Fact]
+ public void FindPartialView_NullPartialViewName_Throws()
+ {
+ // Arrange
+ ControllerContext context = CreateContext();
+ TestableVirtualPathProviderViewEngine engine = new TestableVirtualPathProviderViewEngine();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ () => engine.FindPartialView(context, null, false),
+ "partialViewName"
+ );
+ }
+
+ [Fact]
+ public void FindPartialView_EmptyPartialViewName_Throws()
+ {
+ // Arrange
+ ControllerContext context = CreateContext();
+ TestableVirtualPathProviderViewEngine engine = new TestableVirtualPathProviderViewEngine();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ () => engine.FindPartialView(context, "", false),
+ "partialViewName"
+ );
+ }
+
+ [Fact]
+ public void FindPartialView_ControllerNameNotInRequestContext_Throws()
+ {
+ // Arrange
+ TestableVirtualPathProviderViewEngine engine = new TestableVirtualPathProviderViewEngine();
+ ControllerContext context = CreateContext();
+ context.RouteData.Values.Remove("controller");
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ () => engine.FindPartialView(context, "partialName", false),
+ "The RouteData must contain an item named 'controller' with a non-empty string value."
+ );
+ }
+
+ [Fact]
+ public void FindPartialView_EmptyPartialViewLocations_Throws()
+ {
+ // Arrange
+ ControllerContext context = CreateContext();
+ TestableVirtualPathProviderViewEngine engine = new TestableVirtualPathProviderViewEngine();
+ engine.ClearPartialViewLocations();
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ () => engine.FindPartialView(context, "partialName", false),
+ "The property 'PartialViewLocationFormats' cannot be null or empty."
+ );
+ }
+
+ [Fact]
+ public void FindPartialView_ViewDoesNotExist_ReturnsSearchLocationsResult()
+ {
+ // Arrange
+ ControllerContext context = CreateContext();
+ TestableVirtualPathProviderViewEngine engine = new TestableVirtualPathProviderViewEngine();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/vpath/controllerName/partialName.partial"))
+ .Returns(false)
+ .Verifiable();
+
+ // Act
+ ViewEngineResult result = engine.FindPartialView(context, "partialName", false);
+
+ // Assert
+ Assert.Null(result.View);
+ Assert.Single(result.SearchedLocations);
+ Assert.True(result.SearchedLocations.Contains("~/vpath/controllerName/partialName.partial"));
+ engine.MockPathProvider.Verify();
+ }
+
+ [Fact]
+ public void FindPartialView_VirtualPathViewExists_Legacy_ReturnsView()
+ {
+ // Arrange
+ ControllerContext context = CreateContext();
+ TestableVirtualPathProviderViewEngine engine = new TestableVirtualPathProviderViewEngine()
+ {
+ FileExtensions = null, // Set FileExtensions to null to simulate View Engines that do not set this property
+ };
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/foo/bar.unsupported"))
+ .Returns(true)
+ .Verifiable();
+ engine.MockCache
+ .Setup(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), "~/foo/bar.unsupported"))
+ .Verifiable();
+
+ // Act
+ ViewEngineResult result = engine.FindPartialView(context, "~/foo/bar.unsupported", false);
+
+ // Assert
+ Assert.Same(engine.CreatePartialViewResult, result.View);
+ Assert.Null(result.SearchedLocations);
+ Assert.Same(context, engine.CreatePartialViewControllerContext);
+ Assert.Equal("~/foo/bar.unsupported", engine.CreatePartialViewPartialPath);
+ engine.MockPathProvider.Verify();
+ engine.MockCache.Verify();
+ }
+
+ [Fact]
+ public void FindPartialView_VirtualPathViewDoesNotExist_ReturnsSearchedLocationsResult()
+ {
+ // Arrange
+ ControllerContext context = CreateContext();
+ TestableVirtualPathProviderViewEngine engine = new TestableVirtualPathProviderViewEngine();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/foo/bar.partial"))
+ .Returns(false)
+ .Verifiable();
+ engine.MockCache
+ .Setup(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), ""))
+ .Verifiable();
+
+ // Act
+ ViewEngineResult result = engine.FindPartialView(context, "~/foo/bar.partial", false);
+
+ // Assert
+ Assert.Null(result.View);
+ Assert.Single(result.SearchedLocations);
+ Assert.True(result.SearchedLocations.Contains("~/foo/bar.partial"));
+ engine.MockPathProvider.Verify();
+ engine.MockCache.Verify();
+ }
+
+ [Fact]
+ public void FindPartialView_VirtualPathViewNotSupported_ReturnsSearchedLocationsResult()
+ {
+ // Arrange
+ ControllerContext context = CreateContext();
+ TestableVirtualPathProviderViewEngine engine = new TestableVirtualPathProviderViewEngine();
+ engine.MockCache
+ .Setup(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), ""))
+ .Verifiable();
+
+ // Act
+ ViewEngineResult result = engine.FindPartialView(context, "~/foo/bar.unsupported", false);
+
+ // Assert
+ Assert.Null(result.View);
+ Assert.Single(result.SearchedLocations);
+ Assert.True(result.SearchedLocations.Contains("~/foo/bar.unsupported"));
+ engine.MockPathProvider.Verify(vpp => vpp.FileExists("~/foo/bar.unsupported"), Times.Never());
+ engine.MockCache.Verify();
+ }
+
+ [Fact]
+ public void FindPartialView_AbsolutePathViewDoesNotExist_ReturnsSearchedLocationsResult()
+ {
+ // Arrange
+ ControllerContext context = CreateContext();
+ TestableVirtualPathProviderViewEngine engine = new TestableVirtualPathProviderViewEngine();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("/foo/bar.partial"))
+ .Returns(false)
+ .Verifiable();
+ engine.MockCache
+ .Setup(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), ""))
+ .Verifiable();
+
+ // Act
+ ViewEngineResult result = engine.FindPartialView(context, "/foo/bar.partial", false);
+
+ // Assert
+ Assert.Null(result.View);
+ Assert.Single(result.SearchedLocations);
+ Assert.True(result.SearchedLocations.Contains("/foo/bar.partial"));
+ engine.MockPathProvider.Verify();
+ engine.MockCache.Verify();
+ }
+
+ [Fact]
+ public void FindPartialView_AbsolutePathViewNotSupported_ReturnsSearchedLocationsResult()
+ {
+ // Arrange
+ ControllerContext context = CreateContext();
+ TestableVirtualPathProviderViewEngine engine = new TestableVirtualPathProviderViewEngine();
+ engine.MockCache
+ .Setup(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), ""))
+ .Verifiable();
+
+ // Act
+ ViewEngineResult result = engine.FindPartialView(context, "/foo/bar.unsupported", false);
+
+ // Assert
+ Assert.Null(result.View);
+ Assert.Single(result.SearchedLocations);
+ Assert.True(result.SearchedLocations.Contains("/foo/bar.unsupported"));
+ engine.MockPathProvider.Verify<bool>(vpp => vpp.FileExists("/foo/bar.unsupported"), Times.Never());
+ engine.MockCache.Verify();
+ }
+
+ [Fact]
+ public void FindPartialView_AbsolutePathViewExists_Legacy_ReturnsView()
+ {
+ // Arrange
+ ControllerContext context = CreateContext();
+ TestableVirtualPathProviderViewEngine engine = new TestableVirtualPathProviderViewEngine()
+ {
+ FileExtensions = null, // Set FileExtensions to null to simulate View Engines that do not set this property
+ };
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("/foo/bar.unsupported"))
+ .Returns(true)
+ .Verifiable();
+ engine.MockCache
+ .Setup(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), "/foo/bar.unsupported"))
+ .Verifiable();
+
+ // Act
+ ViewEngineResult result = engine.FindPartialView(context, "/foo/bar.unsupported", false);
+
+ // Assert
+ Assert.Same(engine.CreatePartialViewResult, result.View);
+ Assert.Null(result.SearchedLocations);
+ Assert.Same(context, engine.CreatePartialViewControllerContext);
+ Assert.Equal("/foo/bar.unsupported", engine.CreatePartialViewPartialPath);
+ engine.MockPathProvider.Verify();
+ engine.MockCache.Verify();
+ }
+
+ [Fact]
+ public void FindPartialView_ViewExists_ReturnsView()
+ {
+ // Arrange
+ ControllerContext context = CreateContext();
+ TestableVirtualPathProviderViewEngine engine = new TestableVirtualPathProviderViewEngine();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/vpath/controllerName/partialName.partial"))
+ .Returns(true)
+ .Verifiable();
+ engine.MockCache
+ .Setup(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), "~/vpath/controllerName/partialName.partial"))
+ .Verifiable();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/vpath/controllerName/partialName.Mobile.partial"))
+ .Returns(false)
+ .Verifiable();
+
+ // Act
+ ViewEngineResult result = engine.FindPartialView(context, "partialName", false);
+
+ // Assert
+ Assert.Same(engine.CreatePartialViewResult, result.View);
+ Assert.Null(result.SearchedLocations);
+ Assert.Same(context, engine.CreatePartialViewControllerContext);
+ Assert.Equal("~/vpath/controllerName/partialName.partial", engine.CreatePartialViewPartialPath);
+ engine.MockPathProvider.Verify();
+ engine.MockCache.Verify();
+ }
+
+ [Fact]
+ public void FindPartialView_VirtualPathViewExists_ReturnsView()
+ {
+ // Arrange
+ ControllerContext context = CreateContext();
+ TestableVirtualPathProviderViewEngine engine = new TestableVirtualPathProviderViewEngine();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/foo/bar.partial"))
+ .Returns(true)
+ .Verifiable();
+ engine.MockCache
+ .Setup(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), "~/foo/bar.partial"))
+ .Verifiable();
+
+ // Act
+ ViewEngineResult result = engine.FindPartialView(context, "~/foo/bar.partial", false);
+
+ // Assert
+ Assert.Same(engine.CreatePartialViewResult, result.View);
+ Assert.Null(result.SearchedLocations);
+ Assert.Same(context, engine.CreatePartialViewControllerContext);
+ Assert.Equal("~/foo/bar.partial", engine.CreatePartialViewPartialPath);
+ engine.MockPathProvider.Verify();
+ engine.MockCache.Verify();
+ }
+
+ [Fact]
+ public void FindPartialView_AbsolutePathViewExists_ReturnsView()
+ {
+ // Arrange
+ ControllerContext context = CreateContext();
+ TestableVirtualPathProviderViewEngine engine = new TestableVirtualPathProviderViewEngine();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("/foo/bar.partial"))
+ .Returns(true)
+ .Verifiable();
+ engine.MockCache
+ .Setup(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), "/foo/bar.partial"))
+ .Verifiable();
+
+ // Act
+ ViewEngineResult result = engine.FindPartialView(context, "/foo/bar.partial", false);
+
+ // Assert
+ Assert.Same(engine.CreatePartialViewResult, result.View);
+ Assert.Null(result.SearchedLocations);
+ Assert.Same(context, engine.CreatePartialViewControllerContext);
+ Assert.Equal("/foo/bar.partial", engine.CreatePartialViewPartialPath);
+ engine.MockPathProvider.Verify();
+ engine.MockCache.Verify();
+ }
+
+ [Fact]
+ public void FileExtensions()
+ {
+ // Arrange + Assert
+ Assert.Null(new Mock<VirtualPathProviderViewEngine>().Object.FileExtensions);
+ }
+
+ [Fact]
+ public void GetExtensionThunk()
+ {
+ // Arrange and Assert
+ Assert.Equal(VirtualPathUtility.GetExtension, new Mock<VirtualPathProviderViewEngine>().Object.GetExtensionThunk);
+ }
+
+ [Fact]
+ public void DisplayModeSetOncePerRequest()
+ {
+ // Arrange
+ RouteData routeData = new RouteData();
+ routeData.Values["controller"] = "controllerName";
+ routeData.Values["action"] = "actionName";
+
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.Setup(c => c.HttpContext.Request.Browser.IsMobileDevice).Returns(true);
+ mockControllerContext.Setup(c => c.HttpContext.Request.Cookies).Returns(new HttpCookieCollection());
+ mockControllerContext.Setup(c => c.HttpContext.Response.Cookies).Returns(new HttpCookieCollection());
+ mockControllerContext.Setup(c => c.HttpContext.Items).Returns(new Hashtable());
+ mockControllerContext.Setup(c => c.RouteData).Returns(routeData);
+
+ ControllerContext context = mockControllerContext.Object;
+ TestableVirtualPathProviderViewEngine engine = new TestableVirtualPathProviderViewEngine();
+
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/vpath/controllerName/viewName.view"))
+ .Returns(true)
+ .Verifiable();
+ engine.MockCache
+ .Setup(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), "~/vpath/controllerName/viewName.view"))
+ .Verifiable();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/vpath/controllerName/viewName.Mobile.view"))
+ .Returns(false)
+ .Verifiable();
+
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/vpath/controllerName/partialName.partial"))
+ .Returns(false)
+ .Verifiable();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/vpath/controllerName/partialName.Mobile.partial"))
+ .Returns(true)
+ .Verifiable();
+ engine.MockCache
+ .Setup(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), "~/vpath/controllerName/partialName.Mobile.partial"))
+ .Callback<HttpContextBase, string, string>((httpContext, key, virtualPath) =>
+ {
+ engine.MockCache
+ .Setup(c => c.GetViewLocation(It.IsAny<HttpContextBase>(), key))
+ .Returns("~/vpath/controllerName/partialName.Mobile.partial")
+ .Verifiable();
+ })
+ .Verifiable();
+
+ // Act
+ ViewEngineResult viewResult = engine.FindView(context, "viewName", masterName: null, useCache: false);
+
+ // Mobile display mode will be used to locate the view with and without the cache.
+ // In neither case should this set the DisplayModeId to Mobile because it has already been set.
+ ViewEngineResult partialResult = engine.FindPartialView(context, "partialName", useCache: false);
+ ViewEngineResult cachedPartialResult = engine.FindPartialView(context, "partialName", useCache: true);
+
+ // Assert
+ engine.MockPathProvider.Verify();
+ engine.MockCache.Verify();
+ Assert.Same(engine.CreateViewResult, viewResult.View);
+ Assert.Same(engine.CreatePartialViewResult, partialResult.View);
+ Assert.Same(engine.CreatePartialViewResult, cachedPartialResult.View);
+
+ Assert.Equal(DisplayModeProvider.DefaultDisplayModeId, context.DisplayMode.DisplayModeId);
+ }
+
+ // The core caching scenarios are covered in the FindView/FindPartialView tests. These
+ // extra tests deal with the cache itself, rather than specifics around finding views.
+
+ private const string MASTER_VIRTUAL = "~/vpath/controllerName/name.master";
+ private const string PARTIAL_VIRTUAL = "~/vpath/controllerName/name.partial";
+ private const string VIEW_VIRTUAL = "~/vpath/controllerName/name.view";
+ private const string MOBILE_VIEW_VIRTUAL = "~/vpath/controllerName/name.Mobile.view";
+
+ [Fact]
+ public void UsesDifferentKeysForViewMasterAndPartial()
+ {
+ string keyMaster = null;
+ string keyPartial = null;
+ string keyView = null;
+
+ // Arrange
+ ControllerContext context = CreateContext();
+ TestableVirtualPathProviderViewEngine engine = new TestableVirtualPathProviderViewEngine();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists(VIEW_VIRTUAL))
+ .Returns(true)
+ .Verifiable();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists(MOBILE_VIEW_VIRTUAL))
+ .Returns(false)
+ .Verifiable();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists(MASTER_VIRTUAL))
+ .Returns(true)
+ .Verifiable();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/vpath/controllerName/name.Mobile.master"))
+ .Returns(false)
+ .Verifiable();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists(PARTIAL_VIRTUAL))
+ .Returns(true)
+ .Verifiable();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists("~/vpath/controllerName/name.Mobile.partial"))
+ .Returns(false)
+ .Verifiable();
+ engine.MockCache
+ .Setup(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), VIEW_VIRTUAL))
+ .Callback<HttpContextBase, string, string>((httpContext, key, path) => keyView = key)
+ .Verifiable();
+ engine.MockCache
+ .Setup(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), MASTER_VIRTUAL))
+ .Callback<HttpContextBase, string, string>((httpContext, key, path) => keyMaster = key)
+ .Verifiable();
+ engine.MockCache
+ .Setup(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), PARTIAL_VIRTUAL))
+ .Callback<HttpContextBase, string, string>((httpContext, key, path) => keyPartial = key)
+ .Verifiable();
+
+ // Act
+ engine.FindView(context, "name", "name", false);
+ engine.FindPartialView(context, "name", false);
+
+ // Assert
+ Assert.NotNull(keyMaster);
+ Assert.NotNull(keyPartial);
+ Assert.NotNull(keyView);
+ Assert.NotEqual(keyMaster, keyPartial);
+ Assert.NotEqual(keyMaster, keyView);
+ Assert.NotEqual(keyPartial, keyView);
+ engine.MockPathProvider.Verify();
+ engine.MockCache.Verify();
+ engine.MockPathProvider
+ .Verify(vpp => vpp.FileExists(VIEW_VIRTUAL), Times.AtMostOnce());
+ engine.MockPathProvider
+ .Verify(vpp => vpp.FileExists(MASTER_VIRTUAL), Times.AtMostOnce());
+ engine.MockPathProvider
+ .Verify(vpp => vpp.FileExists(PARTIAL_VIRTUAL), Times.AtMostOnce());
+ engine.MockCache
+ .Verify(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), VIEW_VIRTUAL), Times.AtMostOnce());
+ engine.MockCache
+ .Verify(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), MASTER_VIRTUAL), Times.AtMostOnce());
+ engine.MockCache
+ .Verify(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), PARTIAL_VIRTUAL), Times.AtMostOnce());
+ }
+
+ // This tests the protocol involved with two calls to FindView for the same view name
+ // where the request succeeds. The calls happen in this order:
+ //
+ // FindView("view")
+ // Cache.GetViewLocation(key for "view") -> returns null (not found)
+ // VirtualPathProvider.FileExists(virtual path for "view") -> returns true
+ // Cache.InsertViewLocation(key for "view", virtual path for "view")
+ // FindView("view")
+ // Cache.GetViewLocation(key for "view") -> returns virtual path for "view"
+ //
+ // The mocking code is written as it is because we don't want to make any assumptions
+ // about the format of the cache key. So we intercept the first call to Cache.GetViewLocation and
+ // take the key they gave us to set up the rest of the mock expectations.
+ // The ViewCollection class will typically place to successive calls to FindView and FindPartialView and
+ // set the useCache parameter to true/false respectively. To simulate this, both calls to FindView are executed
+ // with useCache set to true. This mimics the behavior of always going to the cache first and after finding a
+ // view, ensuring that subsequent calls from the cache are successful.
+
+ [Fact]
+ public void ValueInCacheBypassesVirtualPathProvider()
+ {
+ // Arrange
+ string cacheKey = null;
+ ControllerContext context = CreateContext();
+ ControllerContext mobileContext = CreateContext(isMobileDevice: true);
+
+ TestableVirtualPathProviderViewEngine engine = new TestableVirtualPathProviderViewEngine();
+
+ engine.MockPathProvider // It wasn't found, so they call vpp.FileExists
+ .Setup(vpp => vpp.FileExists(VIEW_VIRTUAL))
+ .Returns(true)
+ .Verifiable();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists(MOBILE_VIEW_VIRTUAL))
+ .Returns(false)
+ .Verifiable();
+ engine.MockCache // Then they set the value into the cache
+ .Setup(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), VIEW_VIRTUAL))
+ .Callback<HttpContextBase, string, string>((httpContext, key, virtualPath) =>
+ {
+ cacheKey = key;
+ engine.MockCache // Second time through, we give them a cache hit
+ .Setup(c => c.GetViewLocation(It.IsAny<HttpContextBase>(), key))
+ .Returns(VIEW_VIRTUAL)
+ .Verifiable();
+ })
+ .Verifiable();
+
+ // Act
+ engine.FindView(context, "name", null, false); // Call it once with false to seed the cache
+ engine.FindView(context, "name", null, true); // Call it once with true to check the cache
+
+ // Assert
+ engine.MockPathProvider.Verify();
+ engine.MockCache.Verify();
+
+ engine.MockPathProvider.Verify(vpp => vpp.FileExists(VIEW_VIRTUAL), Times.AtMostOnce());
+ engine.MockCache.Verify(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), VIEW_VIRTUAL), Times.AtMostOnce());
+ engine.MockCache.Verify(c => c.GetViewLocation(It.IsAny<HttpContextBase>(), cacheKey), Times.AtMostOnce());
+
+ // We seed the cache with all possible display modes but since the mobile view does not exist we don't insert it into the cache.
+ engine.MockPathProvider.Verify(vpp => vpp.FileExists(MOBILE_VIEW_VIRTUAL), Times.Exactly(1));
+ engine.MockCache.Verify(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), MOBILE_VIEW_VIRTUAL), Times.Never());
+ engine.MockCache.Verify(c => c.GetViewLocation(It.IsAny<HttpContextBase>(), VirtualPathProviderViewEngine.AppendDisplayModeToCacheKey(cacheKey, DisplayModeProvider.MobileDisplayModeId)), Times.Never());
+ }
+
+ [Fact]
+ public void ValueInCacheBypassesVirtualPathProviderForAllAvailableDisplayModesForContext()
+ {
+ // Arrange
+ string cacheKey = null;
+ string mobileCacheKey = null;
+
+ ControllerContext mobileContext = CreateContext(isMobileDevice: true);
+ ControllerContext desktopContext = CreateContext(isMobileDevice: false);
+
+ TestableVirtualPathProviderViewEngine engine = new TestableVirtualPathProviderViewEngine();
+
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists(VIEW_VIRTUAL))
+ .Returns(true)
+ .Verifiable();
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists(MOBILE_VIEW_VIRTUAL))
+ .Returns(true)
+ .Verifiable();
+
+ engine.MockCache
+ .Setup(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), VIEW_VIRTUAL))
+ .Callback<HttpContextBase, string, string>((httpContext, key, virtualPath) =>
+ {
+ cacheKey = key;
+ engine.MockCache
+ .Setup(c => c.GetViewLocation(It.IsAny<HttpContextBase>(), key))
+ .Returns(MOBILE_VIEW_VIRTUAL)
+ .Verifiable();
+ })
+ .Verifiable();
+ engine.MockCache
+ .Setup(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), MOBILE_VIEW_VIRTUAL))
+ .Callback<HttpContextBase, string, string>((httpContext, key, virtualPath) =>
+ {
+ mobileCacheKey = key;
+ engine.MockCache
+ .Setup(c => c.GetViewLocation(It.IsAny<HttpContextBase>(), key))
+ .Returns(MOBILE_VIEW_VIRTUAL)
+ .Verifiable();
+ })
+ .Verifiable();
+
+ // Act
+ engine.FindView(mobileContext, "name", null, false);
+ engine.FindView(mobileContext, "name", null, true);
+
+ // Assert
+ engine.MockPathProvider.Verify();
+
+ // DefaultDisplayMode with Mobile substitution is cached and hit on the second call to FindView
+ engine.MockPathProvider.Verify(vpp => vpp.FileExists(MOBILE_VIEW_VIRTUAL), Times.AtMostOnce());
+ engine.MockCache.Verify(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), MOBILE_VIEW_VIRTUAL), Times.AtMostOnce());
+ engine.MockCache.Verify(c => c.GetViewLocation(It.IsAny<HttpContextBase>(), VirtualPathProviderViewEngine.AppendDisplayModeToCacheKey(cacheKey, DisplayModeProvider.MobileDisplayModeId)), Times.AtMostOnce());
+
+ engine.MockPathProvider.Verify(vpp => vpp.FileExists(VIEW_VIRTUAL), Times.AtMostOnce());
+ engine.MockCache.Verify(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), VIEW_VIRTUAL), Times.Exactly(1));
+
+ Assert.NotEqual(cacheKey, mobileCacheKey);
+
+ // Act
+ engine.FindView(desktopContext, "name", null, true);
+
+ // Assert
+ engine.MockCache.Verify();
+
+ // The first call to FindView without a mobile browser results in a cache hit
+ engine.MockPathProvider.Verify(vpp => vpp.FileExists(VIEW_VIRTUAL), Times.AtMostOnce());
+ engine.MockCache.Verify(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), VIEW_VIRTUAL), Times.Exactly(1));
+ engine.MockCache.Verify(c => c.GetViewLocation(It.IsAny<HttpContextBase>(), cacheKey), Times.Exactly(1));
+ }
+
+ [Fact]
+ public void NoValueInCacheButFileExistsReturnsNullIfUsingCache()
+ {
+ // Arrange
+ ControllerContext mobileContext = CreateContext(isMobileDevice: true);
+ TestableVirtualPathProviderViewEngine engine = new TestableVirtualPathProviderViewEngine();
+
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists(MOBILE_VIEW_VIRTUAL))
+ .Returns(true);
+ engine.MockPathProvider
+ .Setup(vpp => vpp.FileExists(VIEW_VIRTUAL))
+ .Returns(true);
+
+ engine.MockCache
+ .Setup(c => c.GetViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>()))
+ .Returns((string)null)
+ .Verifiable();
+ engine.MockCache
+ .Setup(c => c.InsertViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>(), It.IsAny<string>()));
+
+ // Act
+ IView viewNotInCacheResult = engine.FindView(mobileContext, "name", masterName: null, useCache: true).View;
+
+ // Assert
+ Assert.Null(viewNotInCacheResult);
+
+ // On a cache miss we should never check the file system. FindView will be called on a second pass
+ // without using the cache.
+ engine.MockPathProvider.Verify(vpp => vpp.FileExists(MOBILE_VIEW_VIRTUAL), Times.Never());
+ engine.MockPathProvider.Verify(vpp => vpp.FileExists(VIEW_VIRTUAL), Times.Never());
+
+ // Act & Assert
+
+ //At this point the view on disk should be found and cached.
+ Assert.NotNull(engine.FindView(mobileContext, "name", masterName: null, useCache: false).View);
+ }
+
+ [Fact]
+ public void ReleaseViewCallsDispose()
+ {
+ // Arrange
+ TestableVirtualPathProviderViewEngine engine = new TestableVirtualPathProviderViewEngine();
+ ControllerContext context = CreateContext();
+ IView view = engine.CreateViewResult;
+
+ // Act
+ engine.ReleaseView(context, view);
+
+ // Assert
+ Assert.True(((TestView)view).Disposed);
+ }
+
+ private static ControllerContext CreateContext(bool isMobileDevice = false)
+ {
+ RouteData routeData = new RouteData();
+ routeData.Values["controller"] = "controllerName";
+ routeData.Values["action"] = "actionName";
+
+ Mock<ControllerContext> mockControllerContext = new Mock<ControllerContext>();
+ mockControllerContext.Setup(c => c.HttpContext.Request.Browser.IsMobileDevice).Returns(isMobileDevice);
+ mockControllerContext.Setup(c => c.HttpContext.Items).Returns(new Hashtable());
+ mockControllerContext.Setup(c => c.RouteData).Returns(routeData);
+
+ mockControllerContext.Setup(c => c.HttpContext.Response.Cookies).Returns(new HttpCookieCollection());
+ mockControllerContext.Setup(c => c.HttpContext.Request.Cookies).Returns(new HttpCookieCollection());
+
+ return mockControllerContext.Object;
+ }
+
+ private class TestView : IView, IDisposable
+ {
+ public bool Disposed { get; set; }
+
+ void IDisposable.Dispose()
+ {
+ Disposed = true;
+ }
+
+ void IView.Render(ViewContext viewContext, TextWriter writer)
+ {
+ }
+ }
+
+ private class TestableVirtualPathProviderViewEngine : VirtualPathProviderViewEngine
+ {
+ public IView CreatePartialViewResult = new Mock<IView>().Object;
+ public string CreatePartialViewPartialPath;
+ public ControllerContext CreatePartialViewControllerContext;
+
+ //public IView CreateViewResult = new Mock<IView>().Object;
+ public IView CreateViewResult = new TestView();
+ public string CreateViewMasterPath;
+ public ControllerContext CreateViewControllerContext;
+ public string CreateViewViewPath;
+
+ public Mock<IViewLocationCache> MockCache = new Mock<IViewLocationCache>(MockBehavior.Strict);
+ public Mock<VirtualPathProvider> MockPathProvider = new Mock<VirtualPathProvider>(MockBehavior.Strict);
+
+ public TestableVirtualPathProviderViewEngine()
+ {
+ MasterLocationFormats = new[] { "~/vpath/{1}/{0}.master" };
+ ViewLocationFormats = new[] { "~/vpath/{1}/{0}.view" };
+ PartialViewLocationFormats = new[] { "~/vpath/{1}/{0}.partial" };
+ AreaMasterLocationFormats = new[] { "~/vpath/{2}/{1}/{0}.master" };
+ AreaViewLocationFormats = new[] { "~/vpath/{2}/{1}/{0}.view" };
+ AreaPartialViewLocationFormats = new[] { "~/vpath/{2}/{1}/{0}.partial" };
+ FileExtensions = new[] { "view", "partial", "master" };
+
+ ViewLocationCache = MockCache.Object;
+ VirtualPathProvider = MockPathProvider.Object;
+
+ MockCache
+ .Setup(c => c.GetViewLocation(It.IsAny<HttpContextBase>(), It.IsAny<string>()))
+ .Returns((string)null);
+
+ GetExtensionThunk = GetExtension;
+ }
+
+ public void ClearViewLocations()
+ {
+ ViewLocationFormats = new string[0];
+ }
+
+ public void ClearMasterLocations()
+ {
+ MasterLocationFormats = new string[0];
+ }
+
+ public void ClearPartialViewLocations()
+ {
+ PartialViewLocationFormats = new string[0];
+ }
+
+ protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
+ {
+ CreatePartialViewControllerContext = controllerContext;
+ CreatePartialViewPartialPath = partialPath;
+
+ return CreatePartialViewResult;
+ }
+
+ protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
+ {
+ CreateViewControllerContext = controllerContext;
+ CreateViewViewPath = viewPath;
+ CreateViewMasterPath = masterPath;
+
+ return CreateViewResult;
+ }
+
+ private static string GetExtension(string virtualPath)
+ {
+ var extension = virtualPath.Substring(virtualPath.LastIndexOf('.'));
+ return extension;
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/WebFormViewEngineTest.cs b/test/System.Web.Mvc.Test/Test/WebFormViewEngineTest.cs
new file mode 100644
index 00000000..8b14bdbd
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/WebFormViewEngineTest.cs
@@ -0,0 +1,244 @@
+using System.Web.Hosting;
+using Moq;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class WebFormViewEngineTest
+ {
+ [Fact]
+ public void CreatePartialViewCreatesWebFormView()
+ {
+ // Arrange
+ TestableWebFormViewEngine engine = new TestableWebFormViewEngine();
+
+ // Act
+ WebFormView result = (WebFormView)engine.CreatePartialView("partial path");
+
+ // Assert
+ Assert.Equal("partial path", result.ViewPath);
+ Assert.Equal(String.Empty, result.MasterPath);
+ }
+
+ [Fact]
+ public void CreateViewCreatesWebFormView()
+ {
+ // Arrange
+ TestableWebFormViewEngine engine = new TestableWebFormViewEngine();
+
+ // Act
+ WebFormView result = (WebFormView)engine.CreateView("view path", "master path");
+
+ // Assert
+ Assert.Equal("view path", result.ViewPath);
+ Assert.Equal("master path", result.MasterPath);
+ }
+
+ [Fact]
+ public void WebFormViewEngineSetsViewPageActivator()
+ {
+ // Arrange
+ Mock<IViewPageActivator> viewPageActivator = new Mock<IViewPageActivator>();
+ TestableWebFormViewEngine viewEngine = new TestableWebFormViewEngine(viewPageActivator.Object);
+
+ //Act & Assert
+ Assert.Equal(viewPageActivator.Object, viewEngine.ViewPageActivator);
+ }
+
+ [Fact]
+ public void CreatePartialView_PassesViewPageActivator()
+ {
+ // Arrange
+ Mock<IViewPageActivator> viewPageActivator = new Mock<IViewPageActivator>();
+ TestableWebFormViewEngine viewEngine = new TestableWebFormViewEngine(viewPageActivator.Object);
+
+ // Act
+ WebFormView result = (WebFormView)viewEngine.CreatePartialView("partial path");
+
+ // Assert
+ Assert.Equal(viewEngine.ViewPageActivator, result.ViewPageActivator);
+ }
+
+ [Fact]
+ public void CreateView_PassesViewPageActivator()
+ {
+ // Arrange
+ Mock<IViewPageActivator> viewPageActivator = new Mock<IViewPageActivator>();
+ TestableWebFormViewEngine viewEngine = new TestableWebFormViewEngine(viewPageActivator.Object);
+
+ // Act
+ WebFormView result = (WebFormView)viewEngine.CreateView("partial path", "master path");
+
+ // Assert
+ Assert.Equal(viewEngine.ViewPageActivator, result.ViewPageActivator);
+ }
+
+ [Fact]
+ public void MasterLocationFormatsProperty()
+ {
+ // Arrange
+ string[] expected = new string[]
+ {
+ "~/Views/{1}/{0}.master",
+ "~/Views/Shared/{0}.master"
+ };
+
+ // Act
+ TestableWebFormViewEngine engine = new TestableWebFormViewEngine();
+
+ // Assert
+ Assert.Equal(expected, engine.MasterLocationFormats);
+ }
+
+ [Fact]
+ public void AreaMasterLocationFormatsProperty()
+ {
+ // Arrange
+ string[] expected = new string[]
+ {
+ "~/Areas/{2}/Views/{1}/{0}.master",
+ "~/Areas/{2}/Views/Shared/{0}.master",
+ };
+
+ // Act
+ TestableWebFormViewEngine engine = new TestableWebFormViewEngine();
+
+ // Assert
+ Assert.Equal(expected, engine.AreaMasterLocationFormats);
+ }
+
+ [Fact]
+ public void PartialViewLocationFormatsProperty()
+ {
+ // Arrange
+ string[] expected = new string[]
+ {
+ "~/Views/{1}/{0}.aspx",
+ "~/Views/{1}/{0}.ascx",
+ "~/Views/Shared/{0}.aspx",
+ "~/Views/Shared/{0}.ascx"
+ };
+
+ // Act
+ TestableWebFormViewEngine engine = new TestableWebFormViewEngine();
+
+ // Assert
+ Assert.Equal(expected, engine.PartialViewLocationFormats);
+ }
+
+ [Fact]
+ public void AreaPartialViewLocationFormatsProperty()
+ {
+ // Arrange
+ string[] expected = new string[]
+ {
+ "~/Areas/{2}/Views/{1}/{0}.aspx",
+ "~/Areas/{2}/Views/{1}/{0}.ascx",
+ "~/Areas/{2}/Views/Shared/{0}.aspx",
+ "~/Areas/{2}/Views/Shared/{0}.ascx",
+ };
+
+ // Act
+ TestableWebFormViewEngine engine = new TestableWebFormViewEngine();
+
+ // Assert
+ Assert.Equal(expected, engine.AreaPartialViewLocationFormats);
+ }
+
+ [Fact]
+ public void ViewLocationFormatsProperty()
+ {
+ // Arrange
+ string[] expected = new string[]
+ {
+ "~/Views/{1}/{0}.aspx",
+ "~/Views/{1}/{0}.ascx",
+ "~/Views/Shared/{0}.aspx",
+ "~/Views/Shared/{0}.ascx"
+ };
+
+ // Act
+ TestableWebFormViewEngine engine = new TestableWebFormViewEngine();
+
+ // Assert
+ Assert.Equal(expected, engine.ViewLocationFormats);
+ }
+
+ [Fact]
+ public void AreaViewLocationFormatsProperty()
+ {
+ // Arrange
+ string[] expected = new string[]
+ {
+ "~/Areas/{2}/Views/{1}/{0}.aspx",
+ "~/Areas/{2}/Views/{1}/{0}.ascx",
+ "~/Areas/{2}/Views/Shared/{0}.aspx",
+ "~/Areas/{2}/Views/Shared/{0}.ascx",
+ };
+
+ // Act
+ TestableWebFormViewEngine engine = new TestableWebFormViewEngine();
+
+ // Assert
+ Assert.Equal(expected, engine.AreaViewLocationFormats);
+ }
+
+ [Fact]
+ public void FileExtensionsProperty()
+ {
+ // Arrange
+ string[] expected = new string[]
+ {
+ "aspx",
+ "ascx",
+ "master",
+ };
+
+ // Act
+ TestableWebFormViewEngine engine = new TestableWebFormViewEngine();
+
+ // Assert
+ Assert.Equal(expected, engine.FileExtensions);
+ }
+
+ private sealed class TestableWebFormViewEngine : WebFormViewEngine
+ {
+ public TestableWebFormViewEngine()
+ : base()
+ {
+ }
+
+ public TestableWebFormViewEngine(IViewPageActivator viewPageActivator)
+ : base(viewPageActivator)
+ {
+ }
+
+ public new IViewPageActivator ViewPageActivator
+ {
+ get { return base.ViewPageActivator; }
+ }
+
+ public new VirtualPathProvider VirtualPathProvider
+ {
+ get { return base.VirtualPathProvider; }
+ set { base.VirtualPathProvider = value; }
+ }
+
+ public IView CreatePartialView(string partialPath)
+ {
+ return base.CreatePartialView(new ControllerContext(), partialPath);
+ }
+
+ public IView CreateView(string viewPath, string masterPath)
+ {
+ return base.CreateView(new ControllerContext(), viewPath, masterPath);
+ }
+
+ // This method should remain overridable in derived view engines
+ protected override bool FileExists(ControllerContext controllerContext, string virtualPath)
+ {
+ return base.FileExists(controllerContext, virtualPath);
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Test/WebFormViewTest.cs b/test/System.Web.Mvc.Test/Test/WebFormViewTest.cs
new file mode 100644
index 00000000..83e9cb96
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Test/WebFormViewTest.cs
@@ -0,0 +1,164 @@
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class WebFormViewTest
+ {
+ [Fact]
+ public void GuardClauses()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ () => new WebFormView(new ControllerContext(), String.Empty, "~/master"),
+ "viewPath"
+ );
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmpty(
+ () => new WebFormView(new ControllerContext(), null, "~/master"),
+ "viewPath"
+ );
+
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => new WebFormView(null, "view path", "~/master"),
+ "controllerContext"
+ );
+ }
+
+ [Fact]
+ public void MasterPathProperty()
+ {
+ // Act
+ WebFormView view = new WebFormView(new ControllerContext(), "view path", "master path");
+
+ // Assert
+ Assert.Equal("master path", view.MasterPath);
+ }
+
+ [Fact]
+ public void MasterPathPropertyReturnsEmptyStringIfMasterNotSpecified()
+ {
+ // Act
+ WebFormView view = new WebFormView(new ControllerContext(), "view path", null);
+
+ // Assert
+ Assert.Equal(String.Empty, view.MasterPath);
+ }
+
+ [Fact]
+ public void RenderWithUnsupportedTypeThrows()
+ {
+ // Arrange
+ ViewContext context = new Mock<ViewContext>().Object;
+ MockBuildManager buildManagerMock = new MockBuildManager("view path", typeof(int));
+ WebFormView view = new WebFormView(new ControllerContext(), "view path", null);
+ view.BuildManager = buildManagerMock;
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ () => view.Render(context, null),
+ "The view at 'view path' must derive from ViewPage, ViewPage<TModel>, ViewUserControl, or ViewUserControl<TModel>."
+ );
+ }
+
+ [Fact]
+ public void RenderWithViewPageAndMasterRendersView()
+ {
+ // Arrange
+ ViewContext context = new Mock<ViewContext>().Object;
+ MockBuildManager buildManager = new MockBuildManager("view path", typeof(object));
+ Mock<IViewPageActivator> activator = new Mock<IViewPageActivator>(MockBehavior.Strict);
+ ControllerContext controllerContext = new ControllerContext();
+ StubViewPage viewPage = new StubViewPage();
+ activator.Setup(l => l.Create(controllerContext, typeof(object))).Returns(viewPage);
+ WebFormView view = new WebFormView(controllerContext, "view path", "master path", activator.Object);
+ view.BuildManager = buildManager;
+
+ // Act
+ view.Render(context, null);
+
+ // Assert
+ Assert.Equal(context, viewPage.ResultViewContext);
+ Assert.Equal("master path", viewPage.MasterLocation);
+ }
+
+ [Fact]
+ public void RenderWithViewPageRendersView()
+ {
+ // Arrange
+ ViewContext context = new Mock<ViewContext>().Object;
+ MockBuildManager buildManager = new MockBuildManager("view path", typeof(object));
+ Mock<IViewPageActivator> activator = new Mock<IViewPageActivator>(MockBehavior.Strict);
+ ControllerContext controllerContext = new ControllerContext();
+ StubViewPage viewPage = new StubViewPage();
+ activator.Setup(l => l.Create(controllerContext, typeof(object))).Returns(viewPage);
+ WebFormView view = new WebFormView(controllerContext, "view path", null, activator.Object);
+ view.BuildManager = buildManager;
+
+ // Act
+ view.Render(context, null);
+
+ // Assert
+ Assert.Equal(context, viewPage.ResultViewContext);
+ Assert.Equal(String.Empty, viewPage.MasterLocation);
+ }
+
+ [Fact]
+ public void RenderWithViewUserControlAndMasterThrows()
+ {
+ // Arrange
+ ViewContext context = new Mock<ViewContext>().Object;
+ MockBuildManager buildManagerMock = new MockBuildManager("view path", typeof(StubViewUserControl));
+ WebFormView view = new WebFormView(new ControllerContext(), "view path", "master path");
+ view.BuildManager = buildManagerMock;
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(
+ () => view.Render(context, null),
+ "A master name cannot be specified when the view is a ViewUserControl."
+ );
+ }
+
+ [Fact]
+ public void RenderWithViewUserControlRendersView()
+ {
+ // Arrange
+ ViewContext context = new Mock<ViewContext>().Object;
+ MockBuildManager buildManager = new MockBuildManager("view path", typeof(object));
+ Mock<IViewPageActivator> activator = new Mock<IViewPageActivator>(MockBehavior.Strict);
+ ControllerContext controllerContext = new ControllerContext();
+ StubViewUserControl viewUserControl = new StubViewUserControl();
+ activator.Setup(l => l.Create(controllerContext, typeof(object))).Returns(viewUserControl);
+ WebFormView view = new WebFormView(controllerContext, "view path", null, activator.Object) { BuildManager = buildManager };
+
+ // Act
+ view.Render(context, null);
+
+ // Assert
+ Assert.Equal(context, viewUserControl.ResultViewContext);
+ }
+
+ public sealed class StubViewPage : ViewPage
+ {
+ public ViewContext ResultViewContext;
+
+ public override void RenderView(ViewContext viewContext)
+ {
+ ResultViewContext = viewContext;
+ }
+ }
+
+ public sealed class StubViewUserControl : ViewUserControl
+ {
+ public ViewContext ResultViewContext;
+
+ public override void RenderView(ViewContext viewContext)
+ {
+ ResultViewContext = viewContext;
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Util/AnonymousObject.cs b/test/System.Web.Mvc.Test/Util/AnonymousObject.cs
new file mode 100644
index 00000000..ec99ac82
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Util/AnonymousObject.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Microsoft.Web.UnitTestUtil
+{
+ public static class AnonymousObject
+ {
+ public static string Inspect(object obj)
+ {
+ if (obj == null)
+ {
+ return "(null)";
+ }
+
+ object[] args = Enumerable.Empty<Object>().ToArray();
+ IEnumerable<string> values = obj.GetType()
+ .GetProperties()
+ .Select(prop => String.Format("{0}: {1}", prop.Name, prop.GetValue(obj, args)));
+
+ if (!values.Any())
+ {
+ return "(no properties)";
+ }
+
+ return "{ " + values.Aggregate((left, right) => left + ", " + right) + " }";
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Util/DictionaryHelper.cs b/test/System.Web.Mvc.Test/Util/DictionaryHelper.cs
new file mode 100644
index 00000000..253f2292
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Util/DictionaryHelper.cs
@@ -0,0 +1,517 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.TestUtil
+{
+ public class DictionaryHelper<TKey, TValue>
+ {
+ public IEqualityComparer<TKey> Comparer { get; set; }
+
+ public Func<IDictionary<TKey, TValue>> Creator { get; set; }
+
+ public IList<TKey> SampleKeys { get; set; }
+
+ public IList<TValue> SampleValues { get; set; }
+
+ public bool SkipItemPropertyTest { get; set; }
+
+ public bool ThrowOnKeyNotFound { get; set; }
+
+ public void Execute()
+ {
+ ValidateProperties();
+
+ Executor executor = new Executor()
+ {
+ Comparer = Comparer,
+ Creator = Creator,
+ ThrowOnKeyNotFound = ThrowOnKeyNotFound,
+ Values = SampleValues.ToArray()
+ };
+ SeparateKeys(out executor.ExcludedKey, out executor.ConflictingKeys, out executor.NonConflictingKeys);
+
+ executor.TestAdd1();
+ executor.TestAdd1ThrowsArgumentExceptionIfKeyAlreadyInDictionary();
+ executor.TestAdd2();
+ executor.TestClear();
+ executor.TestContains();
+ executor.TestContainsKey();
+ executor.TestCopyTo();
+ executor.TestCountProperty();
+ executor.TestGetEnumerator();
+ executor.TestGetEnumeratorGeneric();
+ executor.TestIsReadOnlyProperty();
+
+ if (!SkipItemPropertyTest)
+ {
+ executor.TestItemProperty();
+ }
+
+ executor.TestKeysProperty();
+ executor.TestRemove1();
+ executor.TestRemove2();
+ executor.TestTryGetValue();
+ executor.TestValuesProperty();
+ }
+
+ private void SeparateKeys(out TKey excludedKey, out TKey[] conflictingKeys, out TKey[] nonConflictingKeys)
+ {
+ List<TKey> nonConflictingKeyList = new List<TKey>();
+ TKey[] newConflictingKeys = null;
+
+ var keyLookup = SampleKeys.ToLookup(key => key, Comparer);
+ foreach (var entry in keyLookup)
+ {
+ if (entry.Count() == 1)
+ {
+ // not a conflict
+ nonConflictingKeyList.AddRange(entry);
+ }
+ else
+ {
+ // conflict
+ newConflictingKeys = entry.ToArray();
+ }
+ }
+
+ excludedKey = nonConflictingKeyList[nonConflictingKeyList.Count - 1];
+ nonConflictingKeyList.RemoveAt(nonConflictingKeyList.Count - 1);
+ conflictingKeys = newConflictingKeys;
+ nonConflictingKeys = nonConflictingKeyList.ToArray();
+ }
+
+ private void ValidateProperties()
+ {
+ if (Creator == null)
+ {
+ throw new InvalidOperationException("The Creator property must not be null.");
+ }
+ if (SampleKeys == null || SampleKeys.Count < 4)
+ {
+ throw new InvalidOperationException("The SampleKeys property must contain at least 4 elements.");
+ }
+ if (SampleValues == null || SampleValues.Count != SampleKeys.Count)
+ {
+ throw new InvalidOperationException("The SampleValues property must contain as many elements as the SampleKeys property.");
+ }
+
+ HashSet<TKey> keys = new HashSet<TKey>(SampleKeys, Comparer);
+ if (keys.Count != SampleKeys.Count - 1)
+ {
+ throw new InvalidOperationException("The SampleKeys property must contain exactly one colliding keypair using the given comparer.");
+ }
+ }
+
+ private class Executor
+ {
+ public IEqualityComparer<TKey> Comparer;
+ public Func<IDictionary<TKey, TValue>> Creator;
+ public TKey ExcludedKey;
+ public TKey[] ConflictingKeys;
+ public TKey[] NonConflictingKeys;
+ public bool ThrowOnKeyNotFound;
+ public TValue[] Values;
+
+ private IEnumerable<KeyValuePair<TKey, TValue>> MakeKeyValuePairs()
+ {
+ return MakeKeyValuePairs(false /* includeConflictingKeys */);
+ }
+
+ private IEnumerable<KeyValuePair<TKey, TValue>> MakeKeyValuePairs(bool includeConflictingKeys)
+ {
+ for (int i = 0; i < NonConflictingKeys.Length; i++)
+ {
+ TKey key = NonConflictingKeys[i];
+ TValue value = Values[i];
+ yield return new KeyValuePair<TKey, TValue>(key, value);
+ }
+ if (includeConflictingKeys)
+ {
+ for (int i = 0; i < 2; i++)
+ {
+ TKey key = ConflictingKeys[i];
+ TValue value = Values[NonConflictingKeys.Length + i];
+ yield return new KeyValuePair<TKey, TValue>(key, value);
+ }
+ }
+ }
+
+ public void TestAdd1()
+ {
+ // Arrange
+ Dictionary<TKey, TValue> controlDictionary = new Dictionary<TKey, TValue>(Comparer);
+ IDictionary<TKey, TValue> testDictionary = Creator();
+
+ // Act
+ foreach (var entry in MakeKeyValuePairs())
+ {
+ controlDictionary.Add(entry.Key, entry.Value);
+ testDictionary.Add(entry.Key, entry.Value);
+ }
+
+ // Assert
+ VerifyDictionaryEntriesEqual(controlDictionary, testDictionary);
+ }
+
+ public void TestAdd1ThrowsArgumentExceptionIfKeyAlreadyInDictionary()
+ {
+ // Arrange
+ IDictionary<TKey, TValue> testDictionary = Creator();
+
+ // Act & assert
+ var pairs = MakeKeyValuePairs(true /* includeConflictingKeys */).Skip(NonConflictingKeys.Length).ToArray();
+ testDictionary.Add(pairs[0].Key, pairs[1].Value);
+
+ Assert.Throws<ArgumentException>(
+ delegate { testDictionary.Add(pairs[1].Key, pairs[1].Value); },
+ "An item with the same key has already been added."
+ );
+ }
+
+ public void TestAdd2()
+ {
+ // Arrange
+ Dictionary<TKey, TValue> controlDictionary = new Dictionary<TKey, TValue>(Comparer);
+ IDictionary<TKey, TValue> testDictionary = Creator();
+
+ // Act
+ foreach (var entry in MakeKeyValuePairs())
+ {
+ ((IDictionary<TKey, TValue>)controlDictionary).Add(entry);
+ testDictionary.Add(entry);
+ }
+
+ // Assert
+ VerifyDictionaryEntriesEqual(controlDictionary, testDictionary);
+ }
+
+ public void TestClear()
+ {
+ // Arrange
+ IDictionary<TKey, TValue> testDictionary = Creator();
+
+ // Act
+ foreach (var entry in MakeKeyValuePairs())
+ {
+ testDictionary.Add(entry);
+ }
+ testDictionary.Clear();
+
+ // Assert
+ Assert.Empty(testDictionary);
+ }
+
+ public void TestCountProperty()
+ {
+ // Arrange
+ Dictionary<TKey, TValue> controlDictionary = new Dictionary<TKey, TValue>(Comparer);
+ IDictionary<TKey, TValue> testDictionary = Creator();
+
+ // Act & assert
+ foreach (var entry in MakeKeyValuePairs())
+ {
+ controlDictionary.Add(entry.Key, entry.Value);
+ testDictionary.Add(entry.Key, entry.Value);
+ Assert.Equal(controlDictionary.Count, testDictionary.Count);
+ }
+ }
+
+ public void TestContains()
+ {
+ // Arrange
+ IDictionary<TKey, TValue> testDictionary = Creator();
+
+ // Act
+ foreach (var entry in MakeKeyValuePairs())
+ {
+ testDictionary.Add(entry);
+ }
+
+ // Assert
+ var shouldBeFound = MakeKeyValuePairs().First();
+ var shouldNotBeFound = new KeyValuePair<TKey, TValue>(ExcludedKey, Values[Values.Length - 1]);
+ Assert.True(testDictionary.Contains(shouldBeFound), String.Format("Test dictionary should have contained entry for KVP '{0}'.", shouldBeFound));
+ Assert.False(testDictionary.Contains(shouldNotBeFound), String.Format("Test dictionary should not have contained entry for KVP '{0}'.", shouldNotBeFound));
+ }
+
+ public void TestContainsKey()
+ {
+ // Arrange
+ IDictionary<TKey, TValue> testDictionary = Creator();
+
+ // Act
+ foreach (var entry in MakeKeyValuePairs())
+ {
+ testDictionary.Add(entry);
+ }
+
+ // Assert
+ Assert.True(testDictionary.ContainsKey(NonConflictingKeys[0]), String.Format("Test dictionary should have contained entry for key '{0}'.", NonConflictingKeys[0]));
+ Assert.False(testDictionary.ContainsKey(ExcludedKey), String.Format("Test dictionary should not have contained entry for key '{0}'.", ExcludedKey));
+ }
+
+ public void TestCopyTo()
+ {
+ // Arrange
+ IDictionary<TKey, TValue> controlDictionary = new Dictionary<TKey, TValue>(Comparer);
+ IDictionary<TKey, TValue> testDictionary = Creator();
+
+ foreach (var entry in MakeKeyValuePairs())
+ {
+ controlDictionary.Add(entry.Key, entry.Value);
+ testDictionary.Add(entry.Key, entry.Value);
+ }
+ KeyValuePair<TKey, TValue>[] testKvps = new KeyValuePair<TKey, TValue>[testDictionary.Count + 2];
+
+ // Act
+ testDictionary.CopyTo(testKvps, 2);
+
+ // Assert
+ for (int i = 0; i < 2; i++)
+ {
+ var defaultValue = default(KeyValuePair<TKey, TValue>);
+ var entry = testKvps[i];
+ Assert.Equal(defaultValue, entry);
+ }
+ for (int i = 2; i < testKvps.Length; i++)
+ {
+ var entry = testKvps[i];
+ Assert.True(controlDictionary.Contains(entry), String.Format("The value '{0}' wasn't present in the control dictionary.", entry));
+ controlDictionary.Remove(entry);
+ }
+
+ Assert.Empty(controlDictionary);
+ }
+
+ public void TestGetEnumerator()
+ {
+ // Arrange
+ Dictionary<TKey, TValue> controlDictionary = new Dictionary<TKey, TValue>(Comparer);
+ IDictionary<TKey, TValue> testDictionary = Creator();
+
+ foreach (var entry in MakeKeyValuePairs())
+ {
+ controlDictionary.Add(entry.Key, entry.Value);
+ testDictionary.Add(entry.Key, entry.Value);
+ }
+
+ IEnumerable testDictionaryAsEnumerable = (IEnumerable)testDictionary;
+
+ // Act
+ Dictionary<TKey, TValue> newTestDictionary = new Dictionary<TKey, TValue>(Comparer);
+ foreach (object entry in testDictionaryAsEnumerable)
+ {
+ var kvp = Assert.IsType<KeyValuePair<TKey, TValue>>(entry);
+ newTestDictionary.Add(kvp.Key, kvp.Value);
+ }
+
+ // Assert
+ VerifyDictionaryEntriesEqual(controlDictionary, newTestDictionary);
+ }
+
+ public void TestGetEnumeratorGeneric()
+ {
+ // Arrange
+ Dictionary<TKey, TValue> controlDictionary = new Dictionary<TKey, TValue>(Comparer);
+ IDictionary<TKey, TValue> testDictionary = Creator();
+
+ foreach (var entry in MakeKeyValuePairs())
+ {
+ controlDictionary.Add(entry.Key, entry.Value);
+ testDictionary.Add(entry.Key, entry.Value);
+ }
+
+ // Act & assert
+ VerifyDictionaryEntriesEqual(controlDictionary, testDictionary);
+ }
+
+ public void TestIsReadOnlyProperty()
+ {
+ // Arrange
+ IDictionary<TKey, TValue> testDictionary = Creator();
+
+ // Act & assert
+ Assert.False(testDictionary.IsReadOnly, "Dictionary should not be read only.");
+ }
+
+ public void TestItemProperty()
+ {
+ // Arrange
+ Dictionary<TKey, TValue> controlDictionary = new Dictionary<TKey, TValue>(Comparer);
+ IDictionary<TKey, TValue> testDictionary = Creator();
+
+ var shouldBeFound = MakeKeyValuePairs().First();
+ var shouldNotBeFound = new KeyValuePair<TKey, TValue>(ExcludedKey, Values[Values.Length - 1]);
+
+ // Act & assert
+ foreach (var entry in MakeKeyValuePairs())
+ {
+ controlDictionary.Add(entry.Key, entry.Value);
+ testDictionary[entry.Key] = entry.Value;
+ }
+ VerifyDictionaryEntriesEqual(controlDictionary, testDictionary);
+
+ TValue value = testDictionary[shouldBeFound.Key];
+ Assert.Equal(shouldBeFound.Value, value);
+
+ if (ThrowOnKeyNotFound)
+ {
+ Assert.Throws<KeyNotFoundException>(
+ delegate { TValue valueNotFound = testDictionary[shouldNotBeFound.Key]; }, allowDerivedExceptions: true);
+ }
+ else
+ {
+ TValue valueNotFound = testDictionary[shouldNotBeFound.Key];
+ Assert.Equal(default(TValue), valueNotFound); // Should not throw
+ }
+ }
+
+ public void TestKeysProperty()
+ {
+ // Arrange
+ Dictionary<TKey, TValue> controlDictionary = new Dictionary<TKey, TValue>(Comparer);
+ IDictionary<TKey, TValue> testDictionary = Creator();
+
+ foreach (var entry in MakeKeyValuePairs())
+ {
+ controlDictionary.Add(entry.Key, entry.Value);
+ testDictionary.Add(entry.Key, entry.Value);
+ }
+
+ // Act
+ HashSet<TKey> controlKeys = new HashSet<TKey>(controlDictionary.Keys, Comparer);
+ HashSet<TKey> testKeys = new HashSet<TKey>(testDictionary.Keys, Comparer);
+
+ // Assert
+ Assert.True(controlKeys.SetEquals(testKeys), "Control dictionary and test dictionary key sets were not equal.");
+ }
+
+ public void TestRemove1()
+ {
+ // Arrange
+ Dictionary<TKey, TValue> controlDictionary = new Dictionary<TKey, TValue>(Comparer);
+ IDictionary<TKey, TValue> testDictionary = Creator();
+
+ foreach (var entry in MakeKeyValuePairs())
+ {
+ controlDictionary.Add(entry.Key, entry.Value);
+ testDictionary.Add(entry.Key, entry.Value);
+ }
+
+ // Act
+ bool removalSuccess = testDictionary.Remove(NonConflictingKeys[0]);
+ bool removalFailure = testDictionary.Remove(ExcludedKey);
+
+ // Assert
+ Assert.True(removalSuccess, "Remove() should return true if the key was removed.");
+ Assert.False(removalFailure, "Remove() should return false if the key was not removed.");
+
+ controlDictionary.Remove(NonConflictingKeys[0]);
+ VerifyDictionaryEntriesEqual(controlDictionary, testDictionary);
+ }
+
+ public void TestRemove2()
+ {
+ // Arrange
+ Dictionary<TKey, TValue> controlDictionary = new Dictionary<TKey, TValue>(Comparer);
+ IDictionary<TKey, TValue> testDictionary = Creator();
+
+ foreach (var entry in MakeKeyValuePairs())
+ {
+ ((IDictionary<TKey, TValue>)controlDictionary).Add(entry);
+ testDictionary.Add(entry);
+ }
+
+ // Act
+ var shouldBeFound = MakeKeyValuePairs().First();
+ var shouldNotBeFound = new KeyValuePair<TKey, TValue>(ExcludedKey, Values[Values.Length - 1]);
+ bool removalSuccess = testDictionary.Remove(shouldBeFound);
+ bool removalFailure = testDictionary.Remove(shouldNotBeFound);
+
+ // Assert
+ Assert.True(removalSuccess, "Remove() should return true if the key was removed.");
+ Assert.False(removalFailure, "Remove() should return false if the key was not removed.");
+
+ ((IDictionary<TKey, TValue>)controlDictionary).Remove(shouldBeFound);
+ VerifyDictionaryEntriesEqual(controlDictionary, testDictionary);
+ }
+
+ public void TestTryGetValue()
+ {
+ // Arrange
+ Dictionary<TKey, TValue> controlDictionary = new Dictionary<TKey, TValue>(Comparer);
+ IDictionary<TKey, TValue> testDictionary = Creator();
+
+ foreach (var entry in MakeKeyValuePairs())
+ {
+ controlDictionary.Add(entry.Key, entry.Value);
+ testDictionary.Add(entry.Key, entry.Value);
+ }
+
+ var shouldBeFound = MakeKeyValuePairs().First();
+ var shouldNotBeFound = new KeyValuePair<TKey, TValue>(ExcludedKey, Values[Values.Length - 1]);
+
+ // Act
+ TValue value1;
+ bool returned1 = testDictionary.TryGetValue(shouldBeFound.Key, out value1);
+ TValue value2;
+ bool returned2 = testDictionary.TryGetValue(shouldNotBeFound.Key, out value2);
+
+ // Assert
+ Assert.True(returned1, String.Format("The entry '{0}' should have been found.", shouldBeFound));
+ Assert.Equal(shouldBeFound.Value, value1);
+ Assert.False(returned2, String.Format("The entry '{0}' should not have been found.", shouldNotBeFound));
+ Assert.Equal(default(TValue), value2);
+ }
+
+ public void TestValuesProperty()
+ {
+ // Arrange
+ Dictionary<TKey, TValue> controlDictionary = new Dictionary<TKey, TValue>(Comparer);
+ IDictionary<TKey, TValue> testDictionary = Creator();
+
+ foreach (var entry in MakeKeyValuePairs())
+ {
+ controlDictionary.Add(entry.Key, entry.Value);
+ testDictionary.Add(entry.Key, entry.Value);
+ }
+
+ // Act
+ List<TValue> controlValues = controlDictionary.Values.ToList();
+ List<TValue> testValues = testDictionary.Values.ToList();
+
+ // Assert
+ foreach (var entry in controlValues)
+ {
+ Assert.True(testValues.Contains(entry), String.Format("Test dictionary did not contain value '{0}'.", entry));
+ }
+ foreach (var entry in testValues)
+ {
+ Assert.True(controlValues.Contains(entry), String.Format("Control dictionary did not contain value '{0}'.", entry));
+ }
+ }
+
+ private void VerifyDictionaryEntriesEqual(Dictionary<TKey, TValue> controlDictionary, IDictionary<TKey, TValue> testDictionary)
+ {
+ Assert.Equal(controlDictionary.Count, testDictionary.Count);
+
+ Dictionary<TKey, TValue> clonedControlDictionary = new Dictionary<TKey, TValue>(controlDictionary, controlDictionary.Comparer);
+
+ foreach (var entry in testDictionary)
+ {
+ var key = entry.Key;
+ Assert.True(clonedControlDictionary.ContainsKey(entry.Key), String.Format("Control dictionary did not contain key '{0}'.", key));
+ clonedControlDictionary.Remove(key);
+ }
+
+ foreach (var entry in clonedControlDictionary)
+ {
+ var key = entry.Key;
+ Assert.True(false, String.Format("Test dictionary did not contain key '{0}'.", key));
+ }
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Util/HttpContextHelpers.cs b/test/System.Web.Mvc.Test/Util/HttpContextHelpers.cs
new file mode 100644
index 00000000..de2778a2
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Util/HttpContextHelpers.cs
@@ -0,0 +1,15 @@
+using System.Collections.Generic;
+using Moq;
+
+namespace System.Web.Mvc.Test
+{
+ public static class HttpContextHelpers
+ {
+ public static Mock<HttpContextBase> GetMockHttpContext()
+ {
+ Mock<HttpContextBase> mockContext = new Mock<HttpContextBase>();
+ mockContext.Setup(m => m.Items).Returns(new Dictionary<object, object>());
+ return mockContext;
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Util/MvcHelper.cs b/test/System.Web.Mvc.Test/Util/MvcHelper.cs
new file mode 100644
index 00000000..db87dd35
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Util/MvcHelper.cs
@@ -0,0 +1,174 @@
+using System;
+using System.Collections;
+using System.IO;
+using System.Web;
+using System.Web.Mvc;
+using System.Web.Routing;
+using Moq;
+
+namespace Microsoft.Web.UnitTestUtil
+{
+ public static class MvcHelper
+ {
+ public const string AppPathModifier = "/$(SESSION)";
+
+ public static HtmlHelper<object> GetHtmlHelper()
+ {
+ HttpContextBase httpcontext = GetHttpContext("/app/", null, null);
+ RouteCollection rt = new RouteCollection();
+ rt.Add(new Route("{controller}/{action}/{id}", null) { Defaults = new RouteValueDictionary(new { id = "defaultid" }) });
+ rt.Add("namedroute", new Route("named/{controller}/{action}/{id}", null) { Defaults = new RouteValueDictionary(new { id = "defaultid" }) });
+ RouteData rd = new RouteData();
+ rd.Values.Add("controller", "home");
+ rd.Values.Add("action", "oldaction");
+
+ ViewDataDictionary vdd = new ViewDataDictionary();
+
+ ViewContext viewContext = new ViewContext()
+ {
+ HttpContext = httpcontext,
+ RouteData = rd,
+ ViewData = vdd
+ };
+ Mock<IViewDataContainer> mockVdc = new Mock<IViewDataContainer>();
+ mockVdc.Setup(vdc => vdc.ViewData).Returns(vdd);
+
+ HtmlHelper<object> htmlHelper = new HtmlHelper<object>(viewContext, mockVdc.Object, rt);
+ return htmlHelper;
+ }
+
+ public static HtmlHelper GetHtmlHelper(string protocol, int port)
+ {
+ HttpContextBase httpcontext = GetHttpContext("/app/", null, null, protocol, port);
+ RouteCollection rt = new RouteCollection();
+ rt.Add(new Route("{controller}/{action}/{id}", null) { Defaults = new RouteValueDictionary(new { id = "defaultid" }) });
+ rt.Add("namedroute", new Route("named/{controller}/{action}/{id}", null) { Defaults = new RouteValueDictionary(new { id = "defaultid" }) });
+ RouteData rd = new RouteData();
+ rd.Values.Add("controller", "home");
+ rd.Values.Add("action", "oldaction");
+
+ ViewDataDictionary vdd = new ViewDataDictionary();
+
+ Mock<ViewContext> mockViewContext = new Mock<ViewContext>();
+ mockViewContext.Setup(c => c.HttpContext).Returns(httpcontext);
+ mockViewContext.Setup(c => c.RouteData).Returns(rd);
+ mockViewContext.Setup(c => c.ViewData).Returns(vdd);
+ Mock<IViewDataContainer> mockVdc = new Mock<IViewDataContainer>();
+ mockVdc.Setup(vdc => vdc.ViewData).Returns(vdd);
+
+ HtmlHelper htmlHelper = new HtmlHelper(mockViewContext.Object, mockVdc.Object, rt);
+ return htmlHelper;
+ }
+
+ public static HtmlHelper GetHtmlHelper(ViewDataDictionary viewData)
+ {
+ Mock<ViewContext> mockViewContext = new Mock<ViewContext>() { CallBase = true };
+ mockViewContext.Setup(c => c.ViewData).Returns(viewData);
+ mockViewContext.Setup(c => c.HttpContext.Items).Returns(new Hashtable());
+ IViewDataContainer container = GetViewDataContainer(viewData);
+ return new HtmlHelper(mockViewContext.Object, container);
+ }
+
+ public static HtmlHelper<TModel> GetHtmlHelper<TModel>(ViewDataDictionary<TModel> viewData)
+ {
+ Mock<ViewContext> mockViewContext = new Mock<ViewContext>() { CallBase = true };
+ mockViewContext.Setup(c => c.ViewData).Returns(viewData);
+ mockViewContext.Setup(c => c.HttpContext.Items).Returns(new Hashtable());
+ IViewDataContainer container = GetViewDataContainer(viewData);
+ return new HtmlHelper<TModel>(mockViewContext.Object, container);
+ }
+
+ public static HtmlHelper GetHtmlHelperWithPath(ViewDataDictionary viewData)
+ {
+ return GetHtmlHelperWithPath(viewData, "/");
+ }
+
+ public static HtmlHelper GetHtmlHelperWithPath(ViewDataDictionary viewData, string appPath)
+ {
+ ViewContext viewContext = GetViewContextWithPath(appPath, viewData);
+ Mock<IViewDataContainer> mockContainer = new Mock<IViewDataContainer>();
+ mockContainer.Setup(c => c.ViewData).Returns(viewData);
+ IViewDataContainer container = mockContainer.Object;
+ return new HtmlHelper(viewContext, container, new RouteCollection());
+ }
+
+ public static HtmlHelper<TModel> GetHtmlHelperWithPath<TModel>(ViewDataDictionary<TModel> viewData, string appPath)
+ {
+ ViewContext viewContext = GetViewContextWithPath(appPath, viewData);
+ Mock<IViewDataContainer> mockContainer = new Mock<IViewDataContainer>();
+ mockContainer.Setup(c => c.ViewData).Returns(viewData);
+ IViewDataContainer container = mockContainer.Object;
+ return new HtmlHelper<TModel>(viewContext, container, new RouteCollection());
+ }
+
+ public static HtmlHelper<TModel> GetHtmlHelperWithPath<TModel>(ViewDataDictionary<TModel> viewData)
+ {
+ return GetHtmlHelperWithPath(viewData, "/");
+ }
+
+ public static HttpContextBase GetHttpContext(string appPath, string requestPath, string httpMethod, string protocol, int port)
+ {
+ Mock<HttpContextBase> mockHttpContext = new Mock<HttpContextBase>();
+
+ if (!String.IsNullOrEmpty(appPath))
+ {
+ mockHttpContext.Setup(o => o.Request.ApplicationPath).Returns(appPath);
+ }
+ if (!String.IsNullOrEmpty(requestPath))
+ {
+ mockHttpContext.Setup(o => o.Request.AppRelativeCurrentExecutionFilePath).Returns(requestPath);
+ }
+
+ Uri uri;
+
+ if (port >= 0)
+ {
+ uri = new Uri(protocol + "://localhost" + ":" + Convert.ToString(port));
+ }
+ else
+ {
+ uri = new Uri(protocol + "://localhost");
+ }
+ mockHttpContext.Setup(o => o.Request.Url).Returns(uri);
+
+ mockHttpContext.Setup(o => o.Request.PathInfo).Returns(String.Empty);
+ if (!String.IsNullOrEmpty(httpMethod))
+ {
+ mockHttpContext.Setup(o => o.Request.HttpMethod).Returns(httpMethod);
+ }
+
+ mockHttpContext.Setup(o => o.Session).Returns((HttpSessionStateBase)null);
+ mockHttpContext.Setup(o => o.Response.ApplyAppPathModifier(It.IsAny<string>())).Returns<string>(r => AppPathModifier + r);
+ mockHttpContext.Setup(o => o.Items).Returns(new Hashtable());
+ return mockHttpContext.Object;
+ }
+
+ public static HttpContextBase GetHttpContext(string appPath, string requestPath, string httpMethod)
+ {
+ return GetHttpContext(appPath, requestPath, httpMethod, Uri.UriSchemeHttp.ToString(), -1);
+ }
+
+ public static ViewContext GetViewContextWithPath(string appPath, ViewDataDictionary viewData)
+ {
+ HttpContextBase httpContext = GetHttpContext(appPath, "/request", "GET");
+
+ Mock<ViewContext> mockViewContext = new Mock<ViewContext>() { DefaultValue = DefaultValue.Mock };
+ mockViewContext.Setup(c => c.HttpContext).Returns(httpContext);
+ mockViewContext.Setup(c => c.ViewData).Returns(viewData);
+ mockViewContext.Setup(c => c.Writer).Returns(new StringWriter());
+ return mockViewContext.Object;
+ }
+
+ public static ViewContext GetViewContextWithPath(ViewDataDictionary viewData)
+ {
+ return GetViewContextWithPath("/", viewData);
+ }
+
+ public static IViewDataContainer GetViewDataContainer(ViewDataDictionary viewData)
+ {
+ Mock<IViewDataContainer> mockContainer = new Mock<IViewDataContainer>();
+ mockContainer.Setup(c => c.ViewData).Returns(viewData);
+ return mockContainer.Object;
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Util/Resolver.cs b/test/System.Web.Mvc.Test/Util/Resolver.cs
new file mode 100644
index 00000000..f83ec1bc
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Util/Resolver.cs
@@ -0,0 +1,7 @@
+namespace System.Web.Mvc.Test
+{
+ public class Resolver<T> : IResolver<T>
+ {
+ public T Current { get; set; }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Util/SimpleValueProvider.cs b/test/System.Web.Mvc.Test/Util/SimpleValueProvider.cs
new file mode 100644
index 00000000..c25be79c
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Util/SimpleValueProvider.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Web.Mvc;
+
+namespace Microsoft.Web.UnitTestUtil
+{
+ // just a simple value provider used for unit testing
+
+ public sealed class SimpleValueProvider : Dictionary<string, object>, IValueProvider
+ {
+ private readonly CultureInfo _culture;
+
+ public SimpleValueProvider()
+ : this(null)
+ {
+ }
+
+ public SimpleValueProvider(CultureInfo culture)
+ : base(StringComparer.OrdinalIgnoreCase)
+ {
+ _culture = culture ?? CultureInfo.InvariantCulture;
+ }
+
+ // copied from ValueProviderUtil
+ public bool ContainsPrefix(string prefix)
+ {
+ foreach (string key in Keys)
+ {
+ if (key != null)
+ {
+ if (prefix.Length == 0)
+ {
+ return true; // shortcut - non-null key matches empty prefix
+ }
+
+ if (key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+ {
+ if (key.Length == prefix.Length)
+ {
+ return true; // exact match
+ }
+ else
+ {
+ switch (key[prefix.Length])
+ {
+ case '.': // known separator characters
+ case '[':
+ return true;
+ }
+ }
+ }
+ }
+ }
+
+ return false; // nothing found
+ }
+
+ public ValueProviderResult GetValue(string key)
+ {
+ object rawValue;
+ if (TryGetValue(key, out rawValue))
+ {
+ return new ValueProviderResult(rawValue, Convert.ToString(rawValue, _culture), _culture);
+ }
+ else
+ {
+ // value not found
+ return null;
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/Util/SimpleViewDataContainer.cs b/test/System.Web.Mvc.Test/Util/SimpleViewDataContainer.cs
new file mode 100644
index 00000000..304e6d8f
--- /dev/null
+++ b/test/System.Web.Mvc.Test/Util/SimpleViewDataContainer.cs
@@ -0,0 +1,14 @@
+using System.Web.Mvc;
+
+namespace Microsoft.Web.UnitTestUtil
+{
+ public class SimpleViewDataContainer : IViewDataContainer
+ {
+ public SimpleViewDataContainer(ViewDataDictionary viewData)
+ {
+ ViewData = viewData;
+ }
+
+ public ViewDataDictionary ViewData { get; set; }
+ }
+}
diff --git a/test/System.Web.Mvc.Test/packages.config b/test/System.Web.Mvc.Test/packages.config
new file mode 100644
index 00000000..d5aa6401
--- /dev/null
+++ b/test/System.Web.Mvc.Test/packages.config
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="Moq" version="4.0.10827" />
+ <package id="xunit" version="1.9.0.1566" />
+ <package id="xunit.extensions" version="1.9.0.1566" />
+</packages> \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/CSharpRazorCodeLanguageTest.cs b/test/System.Web.Razor.Test/CSharpRazorCodeLanguageTest.cs
new file mode 100644
index 00000000..587eb6f7
--- /dev/null
+++ b/test/System.Web.Razor.Test/CSharpRazorCodeLanguageTest.cs
@@ -0,0 +1,53 @@
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser;
+using Microsoft.CSharp;
+using Xunit;
+
+namespace System.Web.Razor.Test
+{
+ public class CSharpRazorCodeLanguageTest
+ {
+ [Fact]
+ public void CreateCodeParserReturnsNewCSharpCodeParser()
+ {
+ // Arrange
+ RazorCodeLanguage service = new CSharpRazorCodeLanguage();
+
+ // Act
+ ParserBase parser = service.CreateCodeParser();
+
+ // Assert
+ Assert.NotNull(parser);
+ Assert.IsType<CSharpCodeParser>(parser);
+ }
+
+ [Fact]
+ public void CreateCodeGeneratorParserListenerReturnsNewCSharpCodeGeneratorParserListener()
+ {
+ // Arrange
+ RazorCodeLanguage service = new CSharpRazorCodeLanguage();
+
+ // Act
+ RazorEngineHost host = new RazorEngineHost(service);
+ RazorCodeGenerator generator = service.CreateCodeGenerator("Foo", "Bar", "Baz", host);
+
+ // Assert
+ Assert.NotNull(generator);
+ Assert.IsType<CSharpRazorCodeGenerator>(generator);
+ Assert.Equal("Foo", generator.ClassName);
+ Assert.Equal("Bar", generator.RootNamespaceName);
+ Assert.Equal("Baz", generator.SourceFileName);
+ Assert.Same(host, generator.Host);
+ }
+
+ [Fact]
+ public void CodeDomProviderTypeReturnsVBCodeProvider()
+ {
+ // Arrange
+ RazorCodeLanguage service = new CSharpRazorCodeLanguage();
+
+ // Assert
+ Assert.Equal(typeof(CSharpCodeProvider), service.CodeDomProviderType);
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/CodeCompileUnitExtensions.cs b/test/System.Web.Razor.Test/CodeCompileUnitExtensions.cs
new file mode 100644
index 00000000..49f4a59f
--- /dev/null
+++ b/test/System.Web.Razor.Test/CodeCompileUnitExtensions.cs
@@ -0,0 +1,22 @@
+using System.CodeDom;
+using System.CodeDom.Compiler;
+using System.IO;
+using System.Text;
+
+namespace System.Web.Razor.Test
+{
+ internal static class CodeCompileUnitExtensions
+ {
+ public static string GenerateCode<T>(this CodeCompileUnit ccu) where T : CodeDomProvider, new()
+ {
+ StringBuilder output = new StringBuilder();
+ using (StringWriter writer = new StringWriter(output))
+ {
+ T provider = new T();
+ provider.GenerateCodeFromCompileUnit(ccu, writer, new CodeGeneratorOptions() { IndentString = " " });
+ }
+
+ return output.ToString();
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Editor/RazorEditorParserTest.cs b/test/System.Web.Razor.Test/Editor/RazorEditorParserTest.cs
new file mode 100644
index 00000000..c7dccca5
--- /dev/null
+++ b/test/System.Web.Razor.Test/Editor/RazorEditorParserTest.cs
@@ -0,0 +1,216 @@
+using System.Threading;
+using System.Web.Razor.Editor;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Test.Framework;
+using System.Web.Razor.Test.Utils;
+using System.Web.Razor.Text;
+using System.Web.WebPages.TestUtils;
+using Microsoft.CSharp;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Razor.Test.Editor
+{
+ public class RazorEditorParserTest
+ {
+ private static readonly TestFile SimpleCSHTMLDocument = TestFile.Create("DesignTime.Simple.cshtml");
+ private static readonly TestFile SimpleCSHTMLDocumentGenerated = TestFile.Create("DesignTime.Simple.txt");
+ private const string TestLinePragmaFileName = "C:\\This\\Path\\Is\\Just\\For\\Line\\Pragmas.cshtml";
+
+ [Fact]
+ public void ConstructorRequiresNonNullHost()
+ {
+ Assert.ThrowsArgumentNull(() => new RazorEditorParser(null, TestLinePragmaFileName),
+ "host");
+ }
+
+ [Fact]
+ public void ConstructorRequiresNonNullPhysicalPath()
+ {
+ Assert.ThrowsArgumentNullOrEmptyString(() => new RazorEditorParser(CreateHost(), null),
+ "sourceFileName");
+ }
+
+ [Fact]
+ public void ConstructorRequiresNonEmptyPhysicalPath()
+ {
+ Assert.ThrowsArgumentNullOrEmptyString(() => new RazorEditorParser(CreateHost(), String.Empty),
+ "sourceFileName");
+ }
+
+ [Fact]
+ public void TreesAreDifferentReturnsTrueIfTreeStructureIsDifferent()
+ {
+ var factory = SpanFactory.CreateCsHtml();
+ Block original = new MarkupBlock(
+ factory.Markup("<p>"),
+ new ExpressionBlock(
+ factory.CodeTransition()),
+ factory.Markup("</p>"));
+ Block modified = new MarkupBlock(
+ factory.Markup("<p>"),
+ new ExpressionBlock(
+ factory.CodeTransition("@"),
+ factory.Code("f")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: false)),
+ factory.Markup("</p>"));
+ ITextBuffer oldBuffer = new StringTextBuffer("<p>@</p>");
+ ITextBuffer newBuffer = new StringTextBuffer("<p>@f</p>");
+ Assert.True(RazorEditorParser.TreesAreDifferent(
+ original, modified, new[] {
+ new TextChange(position: 4, oldLength: 0, oldBuffer: oldBuffer, newLength: 1, newBuffer: newBuffer)
+ }));
+ }
+
+ [Fact]
+ public void TreesAreDifferentReturnsFalseIfTreeStructureIsSame()
+ {
+ var factory = SpanFactory.CreateCsHtml();
+ Block original = new MarkupBlock(
+ factory.Markup("<p>"),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("f")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: false)),
+ factory.Markup("</p>"));
+ factory.Reset();
+ Block modified = new MarkupBlock(
+ factory.Markup("<p>"),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("foo")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: false)),
+ factory.Markup("</p>"));
+ original.LinkNodes();
+ modified.LinkNodes();
+ ITextBuffer oldBuffer = new StringTextBuffer("<p>@f</p>");
+ ITextBuffer newBuffer = new StringTextBuffer("<p>@foo</p>");
+ Assert.False(RazorEditorParser.TreesAreDifferent(
+ original, modified, new[] {
+ new TextChange(position: 5, oldLength: 0, oldBuffer: oldBuffer, newLength: 2, newBuffer: newBuffer)
+ }));
+ }
+
+ [Fact]
+ public void CheckForStructureChangesRequiresNonNullBufferInChange()
+ {
+ TextChange change = new TextChange();
+ Assert.ThrowsArgument(
+ () => new RazorEditorParser(
+ CreateHost(),
+ "C:\\Foo.cshtml").CheckForStructureChanges(change),
+ "change",
+ String.Format(RazorResources.Structure_Member_CannotBeNull, "Buffer", "TextChange"));
+ }
+
+ private static RazorEngineHost CreateHost()
+ {
+ return new RazorEngineHost(new CSharpRazorCodeLanguage()) { DesignTimeMode = true };
+ }
+
+ [Fact]
+ public void CheckForStructureChangesStartsReparseAndFiresDocumentParseCompletedEventIfNoAdditionalChangesQueued()
+ {
+ // Arrange
+ using (RazorEditorParser parser = CreateClientParser())
+ {
+ StringTextBuffer input = new StringTextBuffer(SimpleCSHTMLDocument.ReadAllText());
+
+ DocumentParseCompleteEventArgs capturedArgs = null;
+ ManualResetEventSlim parseComplete = new ManualResetEventSlim(false);
+
+ parser.DocumentParseComplete += (sender, args) =>
+ {
+ capturedArgs = args;
+ parseComplete.Set();
+ };
+
+ // Act
+ parser.CheckForStructureChanges(new TextChange(0, 0, new StringTextBuffer(String.Empty), input.Length, input));
+
+ // Assert
+ MiscUtils.DoWithTimeoutIfNotDebugging(parseComplete.Wait);
+ Assert.Equal(
+ SimpleCSHTMLDocumentGenerated.ReadAllText(),
+ MiscUtils.StripRuntimeVersion(
+ capturedArgs.GeneratorResults.GeneratedCode.GenerateCode<CSharpCodeProvider>()
+ ));
+ }
+ }
+
+ [Fact]
+ public void CheckForStructureChangesStartsFullReparseIfChangeOverlapsMultipleSpans()
+ {
+ // Arrange
+ RazorEditorParser parser = new RazorEditorParser(CreateHost(), TestLinePragmaFileName);
+ ITextBuffer original = new StringTextBuffer("Foo @bar Baz");
+ ITextBuffer changed = new StringTextBuffer("Foo @bap Daz");
+ TextChange change = new TextChange(7, 3, original, 3, changed);
+
+ ManualResetEventSlim parseComplete = new ManualResetEventSlim();
+ int parseCount = 0;
+ parser.DocumentParseComplete += (sender, args) =>
+ {
+ Interlocked.Increment(ref parseCount);
+ parseComplete.Set();
+ };
+
+ Assert.Equal(PartialParseResult.Rejected, parser.CheckForStructureChanges(new TextChange(0, 0, new StringTextBuffer(String.Empty), 12, original)));
+ MiscUtils.DoWithTimeoutIfNotDebugging(parseComplete.Wait); // Wait for the parse to finish
+ parseComplete.Reset();
+
+ // Act
+ PartialParseResult result = parser.CheckForStructureChanges(change);
+
+ // Assert
+ Assert.Equal(PartialParseResult.Rejected, result);
+ MiscUtils.DoWithTimeoutIfNotDebugging(parseComplete.Wait);
+ Assert.Equal(2, parseCount);
+ }
+
+ private static void SetupMockWorker(Mock<RazorEditorParser> parser, ManualResetEventSlim running)
+ {
+ SetUpMockWorker(parser, running, null);
+ }
+
+ private static void SetUpMockWorker(Mock<RazorEditorParser> parser, ManualResetEventSlim running, Action<int> backgroundThreadIdReceiver)
+ {
+ parser.Setup(p => p.CreateBackgroundTask(It.IsAny<RazorEngineHost>(), It.IsAny<string>(), It.IsAny<TextChange>()))
+ .Returns<RazorEngineHost, string, TextChange>((host, fileName, change) =>
+ {
+ var task = new Mock<BackgroundParseTask>(new RazorTemplateEngine(host), fileName, change) { CallBase = true };
+ task.Setup(t => t.Run(It.IsAny<CancellationToken>()))
+ .Callback<CancellationToken>((token) =>
+ {
+ if (backgroundThreadIdReceiver != null)
+ {
+ backgroundThreadIdReceiver(Thread.CurrentThread.ManagedThreadId);
+ }
+ running.Set();
+ while (!token.IsCancellationRequested)
+ {
+ }
+ });
+ return task.Object;
+ });
+ }
+
+ private TextChange CreateDummyChange()
+ {
+ return new TextChange(0, 0, new StringTextBuffer(String.Empty), 3, new StringTextBuffer("foo"));
+ }
+
+ private static Mock<RazorEditorParser> CreateMockParser()
+ {
+ return new Mock<RazorEditorParser>(CreateHost(), TestLinePragmaFileName) { CallBase = true };
+ }
+
+ private static RazorEditorParser CreateClientParser()
+ {
+ return new RazorEditorParser(CreateHost(), TestLinePragmaFileName);
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Framework/BlockExtensions.cs b/test/System.Web.Razor.Test/Framework/BlockExtensions.cs
new file mode 100644
index 00000000..2effe832
--- /dev/null
+++ b/test/System.Web.Razor.Test/Framework/BlockExtensions.cs
@@ -0,0 +1,27 @@
+using System.Web.Razor.Parser.SyntaxTree;
+
+namespace System.Web.Razor.Test.Framework
+{
+ public static class BlockExtensions
+ {
+ public static void LinkNodes(this Block self)
+ {
+ Span first = null;
+ Span previous = null;
+ foreach (Span span in self.Flatten())
+ {
+ if (first == null)
+ {
+ first = span;
+ }
+ span.Previous = previous;
+
+ if (previous != null)
+ {
+ previous.Next = span;
+ }
+ previous = span;
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Framework/BlockTypes.cs b/test/System.Web.Razor.Test/Framework/BlockTypes.cs
new file mode 100644
index 00000000..c0cde64f
--- /dev/null
+++ b/test/System.Web.Razor.Test/Framework/BlockTypes.cs
@@ -0,0 +1,233 @@
+using System.Collections.Generic;
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser.SyntaxTree;
+
+namespace System.Web.Razor.Test.Framework
+{
+ // The product code doesn't need this, but having subclasses for the block types makes tests much cleaner :)
+
+ public class StatementBlock : Block
+ {
+ private const BlockType ThisBlockType = BlockType.Statement;
+
+ public StatementBlock(IBlockCodeGenerator codeGenerator, IEnumerable<SyntaxTreeNode> children)
+ : base(ThisBlockType, children, codeGenerator)
+ {
+ }
+
+ public StatementBlock(IBlockCodeGenerator codeGenerator, params SyntaxTreeNode[] children)
+ : this(codeGenerator, (IEnumerable<SyntaxTreeNode>)children)
+ {
+ }
+
+ public StatementBlock(params SyntaxTreeNode[] children)
+ : this(BlockCodeGenerator.Null, children)
+ {
+ }
+
+ public StatementBlock(IEnumerable<SyntaxTreeNode> children)
+ : this(BlockCodeGenerator.Null, children)
+ {
+ }
+ }
+
+ public class DirectiveBlock : Block
+ {
+ private const BlockType ThisBlockType = BlockType.Directive;
+
+ public DirectiveBlock(IBlockCodeGenerator codeGenerator, IEnumerable<SyntaxTreeNode> children)
+ : base(ThisBlockType, children, codeGenerator)
+ {
+ }
+
+ public DirectiveBlock(IBlockCodeGenerator codeGenerator, params SyntaxTreeNode[] children)
+ : this(codeGenerator, (IEnumerable<SyntaxTreeNode>)children)
+ {
+ }
+
+ public DirectiveBlock(params SyntaxTreeNode[] children)
+ : this(BlockCodeGenerator.Null, children)
+ {
+ }
+
+ public DirectiveBlock(IEnumerable<SyntaxTreeNode> children)
+ : this(BlockCodeGenerator.Null, children)
+ {
+ }
+ }
+
+ public class FunctionsBlock : Block
+ {
+ private const BlockType ThisBlockType = BlockType.Functions;
+
+ public FunctionsBlock(IBlockCodeGenerator codeGenerator, IEnumerable<SyntaxTreeNode> children)
+ : base(ThisBlockType, children, codeGenerator)
+ {
+ }
+
+ public FunctionsBlock(IBlockCodeGenerator codeGenerator, params SyntaxTreeNode[] children)
+ : this(codeGenerator, (IEnumerable<SyntaxTreeNode>)children)
+ {
+ }
+
+ public FunctionsBlock(params SyntaxTreeNode[] children)
+ : this(BlockCodeGenerator.Null, children)
+ {
+ }
+
+ public FunctionsBlock(IEnumerable<SyntaxTreeNode> children)
+ : this(BlockCodeGenerator.Null, children)
+ {
+ }
+ }
+
+ public class ExpressionBlock : Block
+ {
+ private const BlockType ThisBlockType = BlockType.Expression;
+
+ public ExpressionBlock(IBlockCodeGenerator codeGenerator, IEnumerable<SyntaxTreeNode> children)
+ : base(ThisBlockType, children, codeGenerator)
+ {
+ }
+
+ public ExpressionBlock(IBlockCodeGenerator codeGenerator, params SyntaxTreeNode[] children)
+ : this(codeGenerator, (IEnumerable<SyntaxTreeNode>)children)
+ {
+ }
+
+ public ExpressionBlock(params SyntaxTreeNode[] children)
+ : this(new ExpressionCodeGenerator(), children)
+ {
+ }
+
+ public ExpressionBlock(IEnumerable<SyntaxTreeNode> children)
+ : this(new ExpressionCodeGenerator(), children)
+ {
+ }
+ }
+
+ public class HelperBlock : Block
+ {
+ private const BlockType ThisBlockType = BlockType.Helper;
+
+ public HelperBlock(IBlockCodeGenerator codeGenerator, IEnumerable<SyntaxTreeNode> children)
+ : base(ThisBlockType, children, codeGenerator)
+ {
+ }
+
+ public HelperBlock(IBlockCodeGenerator codeGenerator, params SyntaxTreeNode[] children)
+ : this(codeGenerator, (IEnumerable<SyntaxTreeNode>)children)
+ {
+ }
+
+ public HelperBlock(params SyntaxTreeNode[] children)
+ : this(BlockCodeGenerator.Null, children)
+ {
+ }
+
+ public HelperBlock(IEnumerable<SyntaxTreeNode> children)
+ : this(BlockCodeGenerator.Null, children)
+ {
+ }
+ }
+
+ public class MarkupBlock : Block
+ {
+ private const BlockType ThisBlockType = BlockType.Markup;
+
+ public MarkupBlock(IBlockCodeGenerator codeGenerator, IEnumerable<SyntaxTreeNode> children)
+ : base(ThisBlockType, children, codeGenerator)
+ {
+ }
+
+ public MarkupBlock(IBlockCodeGenerator codeGenerator, params SyntaxTreeNode[] children)
+ : this(codeGenerator, (IEnumerable<SyntaxTreeNode>)children)
+ {
+ }
+
+ public MarkupBlock(params SyntaxTreeNode[] children)
+ : this(BlockCodeGenerator.Null, children)
+ {
+ }
+
+ public MarkupBlock(IEnumerable<SyntaxTreeNode> children)
+ : this(BlockCodeGenerator.Null, children)
+ {
+ }
+ }
+
+ public class SectionBlock : Block
+ {
+ private const BlockType ThisBlockType = BlockType.Section;
+
+ public SectionBlock(IBlockCodeGenerator codeGenerator, IEnumerable<SyntaxTreeNode> children)
+ : base(ThisBlockType, children, codeGenerator)
+ {
+ }
+
+ public SectionBlock(IBlockCodeGenerator codeGenerator, params SyntaxTreeNode[] children)
+ : this(codeGenerator, (IEnumerable<SyntaxTreeNode>)children)
+ {
+ }
+
+ public SectionBlock(params SyntaxTreeNode[] children)
+ : this(BlockCodeGenerator.Null, children)
+ {
+ }
+
+ public SectionBlock(IEnumerable<SyntaxTreeNode> children)
+ : this(BlockCodeGenerator.Null, children)
+ {
+ }
+ }
+
+ public class TemplateBlock : Block
+ {
+ private const BlockType ThisBlockType = BlockType.Template;
+
+ public TemplateBlock(IBlockCodeGenerator codeGenerator, IEnumerable<SyntaxTreeNode> children)
+ : base(ThisBlockType, children, codeGenerator)
+ {
+ }
+
+ public TemplateBlock(IBlockCodeGenerator codeGenerator, params SyntaxTreeNode[] children)
+ : this(codeGenerator, (IEnumerable<SyntaxTreeNode>)children)
+ {
+ }
+
+ public TemplateBlock(params SyntaxTreeNode[] children)
+ : this(new TemplateBlockCodeGenerator(), children)
+ {
+ }
+
+ public TemplateBlock(IEnumerable<SyntaxTreeNode> children)
+ : this(new TemplateBlockCodeGenerator(), children)
+ {
+ }
+ }
+
+ public class CommentBlock : Block
+ {
+ private const BlockType ThisBlockType = BlockType.Comment;
+
+ public CommentBlock(IBlockCodeGenerator codeGenerator, IEnumerable<SyntaxTreeNode> children)
+ : base(ThisBlockType, children, codeGenerator)
+ {
+ }
+
+ public CommentBlock(IBlockCodeGenerator codeGenerator, params SyntaxTreeNode[] children)
+ : this(codeGenerator, (IEnumerable<SyntaxTreeNode>)children)
+ {
+ }
+
+ public CommentBlock(params SyntaxTreeNode[] children)
+ : this(new RazorCommentCodeGenerator(), children)
+ {
+ }
+
+ public CommentBlock(IEnumerable<SyntaxTreeNode> children)
+ : this(new RazorCommentCodeGenerator(), children)
+ {
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Framework/CodeParserTestBase.cs b/test/System.Web.Razor.Test/Framework/CodeParserTestBase.cs
new file mode 100644
index 00000000..42dd4934
--- /dev/null
+++ b/test/System.Web.Razor.Test/Framework/CodeParserTestBase.cs
@@ -0,0 +1,74 @@
+using System.Collections.Generic;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+
+namespace System.Web.Razor.Test.Framework
+{
+ public abstract class CodeParserTestBase : ParserTestBase
+ {
+ protected abstract ISet<string> KeywordSet { get; }
+
+ protected override ParserBase SelectActiveParser(ParserBase codeParser, ParserBase markupParser)
+ {
+ return codeParser;
+ }
+
+ protected void ImplicitExpressionTest(string input, params RazorError[] errors)
+ {
+ ImplicitExpressionTest(input, AcceptedCharacters.NonWhiteSpace, errors);
+ }
+
+ protected void ImplicitExpressionTest(string input, AcceptedCharacters acceptedCharacters, params RazorError[] errors)
+ {
+ ImplicitExpressionTest(input, input, acceptedCharacters, errors);
+ }
+
+ protected void ImplicitExpressionTest(string input, string expected, params RazorError[] errors)
+ {
+ ImplicitExpressionTest(input, expected, AcceptedCharacters.NonWhiteSpace, errors);
+ }
+
+ protected override void SingleSpanBlockTest(string document, BlockType blockType, SpanKind spanType, AcceptedCharacters acceptedCharacters = AcceptedCharacters.Any)
+ {
+ SingleSpanBlockTest(document, blockType, spanType, acceptedCharacters, expectedError: null);
+ }
+
+ protected override void SingleSpanBlockTest(string document, string spanContent, BlockType blockType, SpanKind spanType, AcceptedCharacters acceptedCharacters = AcceptedCharacters.Any)
+ {
+ SingleSpanBlockTest(document, spanContent, blockType, spanType, acceptedCharacters, expectedErrors: null);
+ }
+
+ protected override void SingleSpanBlockTest(string document, BlockType blockType, SpanKind spanType, params RazorError[] expectedError)
+ {
+ SingleSpanBlockTest(document, document, blockType, spanType, expectedError);
+ }
+
+ protected override void SingleSpanBlockTest(string document, string spanContent, BlockType blockType, SpanKind spanType, params RazorError[] expectedErrors)
+ {
+ SingleSpanBlockTest(document, spanContent, blockType, spanType, AcceptedCharacters.Any, expectedErrors ?? new RazorError[0]);
+ }
+
+ protected override void SingleSpanBlockTest(string document, BlockType blockType, SpanKind spanType, AcceptedCharacters acceptedCharacters, params RazorError[] expectedError)
+ {
+ SingleSpanBlockTest(document, document, blockType, spanType, acceptedCharacters, expectedError);
+ }
+
+ protected override void SingleSpanBlockTest(string document, string spanContent, BlockType blockType, SpanKind spanType, AcceptedCharacters acceptedCharacters, params RazorError[] expectedErrors)
+ {
+ Block b = CreateSimpleBlockAndSpan(spanContent, blockType, spanType, acceptedCharacters);
+ ParseBlockTest(document, b, expectedErrors ?? new RazorError[0]);
+ }
+
+ protected void ImplicitExpressionTest(string input, string expected, AcceptedCharacters acceptedCharacters, params RazorError[] errors)
+ {
+ var factory = CreateSpanFactory();
+ ParseBlockTest(SyntaxConstants.TransitionString + input,
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code(expected)
+ .AsImplicitExpression(KeywordSet)
+ .Accepts(acceptedCharacters)),
+ errors);
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Framework/CsHtmlCodeParserTestBase.cs b/test/System.Web.Razor.Test/Framework/CsHtmlCodeParserTestBase.cs
new file mode 100644
index 00000000..db9ff8bf
--- /dev/null
+++ b/test/System.Web.Razor.Test/Framework/CsHtmlCodeParserTestBase.cs
@@ -0,0 +1,28 @@
+using System.Collections.Generic;
+using System.Web.Razor.Parser;
+
+namespace System.Web.Razor.Test.Framework
+{
+ public abstract class CsHtmlCodeParserTestBase : CodeParserTestBase
+ {
+ protected override ISet<string> KeywordSet
+ {
+ get { return CSharpCodeParser.DefaultKeywords; }
+ }
+
+ protected override SpanFactory CreateSpanFactory()
+ {
+ return SpanFactory.CreateCsHtml();
+ }
+
+ public override ParserBase CreateMarkupParser()
+ {
+ return new HtmlMarkupParser();
+ }
+
+ public override ParserBase CreateCodeParser()
+ {
+ return new CSharpCodeParser();
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Framework/CsHtmlMarkupParserTestBase.cs b/test/System.Web.Razor.Test/Framework/CsHtmlMarkupParserTestBase.cs
new file mode 100644
index 00000000..181abf9c
--- /dev/null
+++ b/test/System.Web.Razor.Test/Framework/CsHtmlMarkupParserTestBase.cs
@@ -0,0 +1,28 @@
+using System.Collections.Generic;
+using System.Web.Razor.Parser;
+
+namespace System.Web.Razor.Test.Framework
+{
+ public abstract class CsHtmlMarkupParserTestBase : MarkupParserTestBase
+ {
+ protected override ISet<string> KeywordSet
+ {
+ get { return CSharpCodeParser.DefaultKeywords; }
+ }
+
+ protected override SpanFactory CreateSpanFactory()
+ {
+ return SpanFactory.CreateCsHtml();
+ }
+
+ public override ParserBase CreateMarkupParser()
+ {
+ return new HtmlMarkupParser();
+ }
+
+ public override ParserBase CreateCodeParser()
+ {
+ return new CSharpCodeParser();
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Framework/ErrorCollector.cs b/test/System.Web.Razor.Test/Framework/ErrorCollector.cs
new file mode 100644
index 00000000..64a8e461
--- /dev/null
+++ b/test/System.Web.Razor.Test/Framework/ErrorCollector.cs
@@ -0,0 +1,54 @@
+using System.Text;
+using System.Web.Razor.Utils;
+
+namespace System.Web.Razor.Test.Framework
+{
+ public class ErrorCollector
+ {
+ private StringBuilder _message = new StringBuilder();
+ private int _indent = 0;
+
+ public bool Success { get; private set; }
+
+ public string Message
+ {
+ get { return _message.ToString(); }
+ }
+
+ public ErrorCollector()
+ {
+ Success = true;
+ }
+
+ public void AddError(string msg, params object[] args)
+ {
+ Append("F", msg, args);
+ Success = false;
+ }
+
+ public void AddMessage(string msg, params object[] args)
+ {
+ Append("P", msg, args);
+ }
+
+ public IDisposable Indent()
+ {
+ _indent++;
+ return new DisposableAction(Unindent);
+ }
+
+ public void Unindent()
+ {
+ _indent--;
+ }
+
+ private void Append(string prefix, string msg, object[] args)
+ {
+ _message.Append(prefix);
+ _message.Append(":");
+ _message.Append(new String('\t', _indent));
+ _message.AppendFormat(msg, args);
+ _message.AppendLine();
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Framework/MarkupParserTestBase.cs b/test/System.Web.Razor.Test/Framework/MarkupParserTestBase.cs
new file mode 100644
index 00000000..5149cdec
--- /dev/null
+++ b/test/System.Web.Razor.Test/Framework/MarkupParserTestBase.cs
@@ -0,0 +1,59 @@
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+
+namespace System.Web.Razor.Test.Framework
+{
+ public abstract class MarkupParserTestBase : CodeParserTestBase
+ {
+ protected override ParserBase SelectActiveParser(ParserBase codeParser, ParserBase markupParser)
+ {
+ return markupParser;
+ }
+
+ protected virtual void SingleSpanDocumentTest(string document, BlockType blockType, SpanKind spanType)
+ {
+ Block b = CreateSimpleBlockAndSpan(document, blockType, spanType);
+ ParseDocumentTest(document, b);
+ }
+
+ protected virtual void ParseDocumentTest(string document)
+ {
+ ParseDocumentTest(document, null, false);
+ }
+
+ protected virtual void ParseDocumentTest(string document, Block expectedRoot)
+ {
+ ParseDocumentTest(document, expectedRoot, false, null);
+ }
+
+ protected virtual void ParseDocumentTest(string document, Block expectedRoot, params RazorError[] expectedErrors)
+ {
+ ParseDocumentTest(document, expectedRoot, false, expectedErrors);
+ }
+
+ protected virtual void ParseDocumentTest(string document, bool designTimeParser)
+ {
+ ParseDocumentTest(document, null, designTimeParser);
+ }
+
+ protected virtual void ParseDocumentTest(string document, Block expectedRoot, bool designTimeParser)
+ {
+ ParseDocumentTest(document, expectedRoot, designTimeParser, null);
+ }
+
+ protected virtual void ParseDocumentTest(string document, Block expectedRoot, bool designTimeParser, params RazorError[] expectedErrors)
+ {
+ RunParseTest(document, parser => parser.ParseDocument, expectedRoot, expectedErrors, designTimeParser);
+ }
+
+ protected virtual ParserResults ParseDocument(string document)
+ {
+ return ParseDocument(document, designTimeParser: false);
+ }
+
+ protected virtual ParserResults ParseDocument(string document, bool designTimeParser)
+ {
+ return RunParse(document, parser => parser.ParseDocument, designTimeParser);
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Framework/ParserTestBase.cs b/test/System.Web.Razor.Test/Framework/ParserTestBase.cs
new file mode 100644
index 00000000..58f5dccb
--- /dev/null
+++ b/test/System.Web.Razor.Test/Framework/ParserTestBase.cs
@@ -0,0 +1,381 @@
+//#define PARSER_TRACE
+
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Text;
+using Xunit;
+
+namespace System.Web.Razor.Test.Framework
+{
+ public abstract class ParserTestBase
+ {
+ protected static Block IgnoreOutput = new IgnoreOutputBlock();
+
+ public SpanFactory Factory { get; private set; }
+
+ protected ParserTestBase()
+ {
+ Factory = CreateSpanFactory();
+ }
+
+ public abstract ParserBase CreateMarkupParser();
+ public abstract ParserBase CreateCodeParser();
+
+ protected abstract ParserBase SelectActiveParser(ParserBase codeParser, ParserBase markupParser);
+
+ public virtual ParserContext CreateParserContext(ITextDocument input, ParserBase codeParser, ParserBase markupParser)
+ {
+ return new ParserContext(input, codeParser, markupParser, SelectActiveParser(codeParser, markupParser));
+ }
+
+ protected abstract SpanFactory CreateSpanFactory();
+
+ protected virtual void ParseBlockTest(string document)
+ {
+ ParseBlockTest(document, null, false, new RazorError[0]);
+ }
+
+ protected virtual void ParseBlockTest(string document, bool designTimeParser)
+ {
+ ParseBlockTest(document, null, designTimeParser, new RazorError[0]);
+ }
+
+ protected virtual void ParseBlockTest(string document, params RazorError[] expectedErrors)
+ {
+ ParseBlockTest(document, false, expectedErrors);
+ }
+
+ protected virtual void ParseBlockTest(string document, bool designTimeParser, params RazorError[] expectedErrors)
+ {
+ ParseBlockTest(document, null, designTimeParser, expectedErrors);
+ }
+
+ protected virtual void ParseBlockTest(string document, Block expectedRoot)
+ {
+ ParseBlockTest(document, expectedRoot, false, null);
+ }
+
+ protected virtual void ParseBlockTest(string document, Block expectedRoot, bool designTimeParser)
+ {
+ ParseBlockTest(document, expectedRoot, designTimeParser, null);
+ }
+
+ protected virtual void ParseBlockTest(string document, Block expectedRoot, params RazorError[] expectedErrors)
+ {
+ ParseBlockTest(document, expectedRoot, false, expectedErrors);
+ }
+
+ protected virtual void ParseBlockTest(string document, Block expectedRoot, bool designTimeParser, params RazorError[] expectedErrors)
+ {
+ RunParseTest(document, parser => parser.ParseBlock, expectedRoot, (expectedErrors ?? new RazorError[0]).ToList(), designTimeParser);
+ }
+
+ protected virtual void SingleSpanBlockTest(string document, BlockType blockType, SpanKind spanType, AcceptedCharacters acceptedCharacters = AcceptedCharacters.Any)
+ {
+ SingleSpanBlockTest(document, blockType, spanType, acceptedCharacters, expectedError: null);
+ }
+
+ protected virtual void SingleSpanBlockTest(string document, string spanContent, BlockType blockType, SpanKind spanType, AcceptedCharacters acceptedCharacters = AcceptedCharacters.Any)
+ {
+ SingleSpanBlockTest(document, spanContent, blockType, spanType, acceptedCharacters, expectedErrors: null);
+ }
+
+ protected virtual void SingleSpanBlockTest(string document, BlockType blockType, SpanKind spanType, params RazorError[] expectedError)
+ {
+ SingleSpanBlockTest(document, document, blockType, spanType, expectedError);
+ }
+
+ protected virtual void SingleSpanBlockTest(string document, string spanContent, BlockType blockType, SpanKind spanType, params RazorError[] expectedErrors)
+ {
+ SingleSpanBlockTest(document, spanContent, blockType, spanType, AcceptedCharacters.Any, expectedErrors ?? new RazorError[0]);
+ }
+
+ protected virtual void SingleSpanBlockTest(string document, BlockType blockType, SpanKind spanType, AcceptedCharacters acceptedCharacters, params RazorError[] expectedError)
+ {
+ SingleSpanBlockTest(document, document, blockType, spanType, acceptedCharacters, expectedError);
+ }
+
+ protected virtual void SingleSpanBlockTest(string document, string spanContent, BlockType blockType, SpanKind spanType, AcceptedCharacters acceptedCharacters, params RazorError[] expectedErrors)
+ {
+ BlockBuilder builder = new BlockBuilder();
+ builder.Type = blockType;
+ ParseBlockTest(
+ document,
+ ConfigureAndAddSpanToBlock(
+ builder,
+ Factory.Span(spanType, spanContent, spanType == SpanKind.Markup)
+ .Accepts(acceptedCharacters)),
+ expectedErrors ?? new RazorError[0]);
+ }
+
+ protected virtual ParserResults RunParse(string document, Func<ParserBase, Action> parserActionSelector, bool designTimeParser)
+ {
+ // Create the source
+ ParserResults results = null;
+ using (SeekableTextReader reader = new SeekableTextReader(document))
+ {
+ try
+ {
+ ParserBase codeParser = CreateCodeParser();
+ ParserBase markupParser = CreateMarkupParser();
+ ParserContext context = CreateParserContext(reader, codeParser, markupParser);
+ context.DesignTimeMode = designTimeParser;
+
+ codeParser.Context = context;
+ markupParser.Context = context;
+
+ // Run the parser
+ parserActionSelector(context.ActiveParser)();
+ results = context.CompleteParse();
+ }
+ finally
+ {
+ if (results != null && results.Document != null)
+ {
+ WriteTraceLine(String.Empty);
+ WriteTraceLine("Actual Parse Tree:");
+ WriteNode(0, results.Document);
+ }
+ }
+ }
+ return results;
+ }
+
+ protected virtual void RunParseTest(string document, Func<ParserBase, Action> parserActionSelector, Block expectedRoot, IList<RazorError> expectedErrors, bool designTimeParser)
+ {
+ // Create the source
+ ParserResults results = RunParse(document, parserActionSelector, designTimeParser);
+
+ // Evaluate the results
+ if (!ReferenceEquals(expectedRoot, IgnoreOutput))
+ {
+ EvaluateResults(results, expectedRoot, expectedErrors);
+ }
+ }
+
+ [Conditional("PARSER_TRACE")]
+ private void WriteNode(int indent, SyntaxTreeNode node)
+ {
+ string content = node.ToString().Replace("\r", "\\r")
+ .Replace("\n", "\\n")
+ .Replace("{", "{{")
+ .Replace("}", "}}");
+ if (indent > 0)
+ {
+ content = new String('.', indent * 2) + content;
+ }
+ WriteTraceLine(content);
+ Block block = node as Block;
+ if (block != null)
+ {
+ foreach (SyntaxTreeNode child in block.Children)
+ {
+ WriteNode(indent + 1, child);
+ }
+ }
+ }
+
+ public static void EvaluateResults(ParserResults results, Block expectedRoot)
+ {
+ EvaluateResults(results, expectedRoot, null);
+ }
+
+ public static void EvaluateResults(ParserResults results, Block expectedRoot, IList<RazorError> expectedErrors)
+ {
+ EvaluateParseTree(results.Document, expectedRoot);
+ EvaluateRazorErrors(results.ParserErrors, expectedErrors);
+ }
+
+ public static void EvaluateParseTree(Block actualRoot, Block expectedRoot)
+ {
+ // Evaluate the result
+ ErrorCollector collector = new ErrorCollector();
+
+ // Link all the nodes
+ expectedRoot.LinkNodes();
+
+ if (expectedRoot == null)
+ {
+ Assert.Null(actualRoot);
+ }
+ else
+ {
+ Assert.NotNull(actualRoot);
+ EvaluateSyntaxTreeNode(collector, actualRoot, expectedRoot);
+ if (collector.Success)
+ {
+ WriteTraceLine("Parse Tree Validation Succeeded:\r\n{0}", collector.Message);
+ }
+ else
+ {
+ Assert.True(false, String.Format("\r\n{0}", collector.Message));
+ }
+ }
+ }
+
+ private static void EvaluateSyntaxTreeNode(ErrorCollector collector, SyntaxTreeNode actual, SyntaxTreeNode expected)
+ {
+ if (actual == null)
+ {
+ AddNullActualError(collector, actual, expected);
+ }
+
+ if (actual.IsBlock != expected.IsBlock)
+ {
+ AddMismatchError(collector, actual, expected);
+ }
+ else
+ {
+ if (expected.IsBlock)
+ {
+ EvaluateBlock(collector, (Block)actual, (Block)expected);
+ }
+ else
+ {
+ EvaluateSpan(collector, (Span)actual, (Span)expected);
+ }
+ }
+ }
+
+ private static void EvaluateSpan(ErrorCollector collector, Span actual, Span expected)
+ {
+ if (!Equals(expected, actual))
+ {
+ AddMismatchError(collector, actual, expected);
+ }
+ else
+ {
+ AddPassedMessage(collector, expected);
+ }
+ }
+
+ private static void EvaluateBlock(ErrorCollector collector, Block actual, Block expected)
+ {
+ if (actual.Type != expected.Type || !expected.CodeGenerator.Equals(actual.CodeGenerator))
+ {
+ AddMismatchError(collector, actual, expected);
+ }
+ else
+ {
+ AddPassedMessage(collector, expected);
+ using (collector.Indent())
+ {
+ IEnumerator<SyntaxTreeNode> expectedNodes = expected.Children.GetEnumerator();
+ IEnumerator<SyntaxTreeNode> actualNodes = actual.Children.GetEnumerator();
+ while (expectedNodes.MoveNext())
+ {
+ if (!actualNodes.MoveNext())
+ {
+ collector.AddError("{0} - FAILED :: No more elements at this node", expectedNodes.Current);
+ }
+ else
+ {
+ EvaluateSyntaxTreeNode(collector, actualNodes.Current, expectedNodes.Current);
+ }
+ }
+ while (actualNodes.MoveNext())
+ {
+ collector.AddError("End of Node - FAILED :: Found Node: {0}", actualNodes.Current);
+ }
+ }
+ }
+ }
+
+ private static void AddPassedMessage(ErrorCollector collector, SyntaxTreeNode expected)
+ {
+ collector.AddMessage("{0} - PASSED", expected);
+ }
+
+ private static void AddMismatchError(ErrorCollector collector, SyntaxTreeNode actual, SyntaxTreeNode expected)
+ {
+ collector.AddError("{0} - FAILED :: Actual: {1}", expected, actual);
+ }
+
+ private static void AddNullActualError(ErrorCollector collector, SyntaxTreeNode actual, SyntaxTreeNode expected)
+ {
+ collector.AddError("{0} - FAILED :: Actual: << Null >>", expected);
+ }
+
+ public static void EvaluateRazorErrors(IList<RazorError> actualErrors, IList<RazorError> expectedErrors)
+ {
+ // Evaluate the errors
+ if (expectedErrors == null || expectedErrors.Count == 0)
+ {
+ Assert.True(actualErrors.Count == 0,
+ String.Format("Expected that no errors would be raised, but the following errors were:\r\n{0}", FormatErrors(actualErrors)));
+ }
+ else
+ {
+ Assert.True(expectedErrors.Count == actualErrors.Count,
+ String.Format("Expected that {0} errors would be raised, but {1} errors were.\r\nExpected Errors: \r\n{2}\r\nActual Errors: \r\n{3}",
+ expectedErrors.Count,
+ actualErrors.Count,
+ FormatErrors(expectedErrors),
+ FormatErrors(actualErrors)));
+ Assert.Equal(expectedErrors.ToArray(), actualErrors.ToArray());
+ }
+ WriteTraceLine("Expected Errors were raised:\r\n{0}", FormatErrors(expectedErrors));
+ }
+
+ public static string FormatErrors(IList<RazorError> errors)
+ {
+ if (errors == null)
+ {
+ return "\t<< No Errors >>";
+ }
+
+ StringBuilder builder = new StringBuilder();
+ foreach (RazorError err in errors)
+ {
+ builder.AppendFormat("\t{0}", err);
+ builder.AppendLine();
+ }
+ return builder.ToString();
+ }
+
+ [Conditional("PARSER_TRACE")]
+ private static void WriteTraceLine(string format, params object[] args)
+ {
+ Trace.WriteLine(String.Format(format, args));
+ }
+
+ protected virtual Block CreateSimpleBlockAndSpan(string spanContent, BlockType blockType, SpanKind spanType, AcceptedCharacters acceptedCharacters = AcceptedCharacters.Any)
+ {
+ SpanConstructor span = Factory.Span(spanType, spanContent, spanType == SpanKind.Markup).Accepts(acceptedCharacters);
+ BlockBuilder b = new BlockBuilder()
+ {
+ Type = blockType
+ };
+ return ConfigureAndAddSpanToBlock(b, span);
+ }
+
+ protected virtual Block ConfigureAndAddSpanToBlock(BlockBuilder block, SpanConstructor span)
+ {
+ switch (block.Type)
+ {
+ case BlockType.Markup:
+ span.With(new MarkupCodeGenerator());
+ break;
+ case BlockType.Statement:
+ span.With(new StatementCodeGenerator());
+ break;
+ case BlockType.Expression:
+ block.CodeGenerator = new ExpressionCodeGenerator();
+ span.With(new ExpressionCodeGenerator());
+ break;
+ }
+ block.Children.Add(span);
+ return block.Build();
+ }
+
+ private class IgnoreOutputBlock : Block
+ {
+ public IgnoreOutputBlock() : base(BlockType.Template, Enumerable.Empty<SyntaxTreeNode>(), null) { }
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Framework/RawTextSymbol.cs b/test/System.Web.Razor.Test/Framework/RawTextSymbol.cs
new file mode 100644
index 00000000..abb02fd2
--- /dev/null
+++ b/test/System.Web.Razor.Test/Framework/RawTextSymbol.cs
@@ -0,0 +1,71 @@
+using System.Globalization;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Text;
+using System.Web.Razor.Tokenizer.Symbols;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.Razor.Test.Framework
+{
+ internal class RawTextSymbol : ISymbol
+ {
+ public SourceLocation Start { get; private set; }
+ public string Content { get; private set; }
+
+ public RawTextSymbol(SourceLocation start, string content)
+ {
+ if (content == null)
+ {
+ throw new ArgumentNullException("content");
+ }
+
+ Start = start;
+ Content = content;
+ }
+
+ public override bool Equals(object obj)
+ {
+ RawTextSymbol other = obj as RawTextSymbol;
+ return Equals(Start, other.Start) && Equals(Content, other.Content);
+ }
+
+ internal bool EquivalentTo(ISymbol sym)
+ {
+ return Equals(Start, sym.Start) && Equals(Content, sym.Content);
+ }
+
+ public override int GetHashCode()
+ {
+ return HashCodeCombiner.Start()
+ .Add(Start)
+ .Add(Content)
+ .CombinedHash;
+ }
+
+ public void OffsetStart(SourceLocation documentStart)
+ {
+ Start = documentStart + Start;
+ }
+
+ public void ChangeStart(SourceLocation newStart)
+ {
+ Start = newStart;
+ }
+
+ public override string ToString()
+ {
+ return String.Format(CultureInfo.InvariantCulture, "{0} RAW - [{1}]", Start, Content);
+ }
+
+ internal void CalculateStart(Span prev)
+ {
+ if (prev == null)
+ {
+ Start = SourceLocation.Zero;
+ }
+ else
+ {
+ Start = new SourceLocationTracker(prev.Start).UpdateLocation(prev.Content).CurrentLocation;
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Framework/TestSpanBuilder.cs b/test/System.Web.Razor.Test/Framework/TestSpanBuilder.cs
new file mode 100644
index 00000000..27a4776d
--- /dev/null
+++ b/test/System.Web.Razor.Test/Framework/TestSpanBuilder.cs
@@ -0,0 +1,405 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Web.Razor.Editor;
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Text;
+using System.Web.Razor.Tokenizer;
+using System.Web.Razor.Tokenizer.Symbols;
+
+namespace System.Web.Razor.Test.Framework
+{
+ public static class SpanFactoryExtensions
+ {
+ public static UnclassifiedCodeSpanConstructor EmptyCSharp(this SpanFactory self)
+ {
+ return new UnclassifiedCodeSpanConstructor(
+ self.Span(SpanKind.Code, new CSharpSymbol(self.LocationTracker.CurrentLocation, String.Empty, CSharpSymbolType.Unknown)));
+ }
+
+ public static UnclassifiedCodeSpanConstructor EmptyVB(this SpanFactory self)
+ {
+ return new UnclassifiedCodeSpanConstructor(
+ self.Span(SpanKind.Code, new VBSymbol(self.LocationTracker.CurrentLocation, String.Empty, VBSymbolType.Unknown)));
+ }
+
+ public static SpanConstructor EmptyHtml(this SpanFactory self)
+ {
+ return self.Span(SpanKind.Markup, new HtmlSymbol(self.LocationTracker.CurrentLocation, String.Empty, HtmlSymbolType.Unknown))
+ .With(new MarkupCodeGenerator());
+ }
+
+ public static UnclassifiedCodeSpanConstructor Code(this SpanFactory self, string content)
+ {
+ return new UnclassifiedCodeSpanConstructor(
+ self.Span(SpanKind.Code, content, markup: false));
+ }
+
+ public static SpanConstructor CodeTransition(this SpanFactory self)
+ {
+ return self.Span(SpanKind.Transition, SyntaxConstants.TransitionString, markup: false).Accepts(AcceptedCharacters.None);
+ }
+
+ public static SpanConstructor CodeTransition(this SpanFactory self, string content)
+ {
+ return self.Span(SpanKind.Transition, content, markup: false).Accepts(AcceptedCharacters.None);
+ }
+
+ public static SpanConstructor CodeTransition(this SpanFactory self, CSharpSymbolType type)
+ {
+ return self.Span(SpanKind.Transition, SyntaxConstants.TransitionString, type).Accepts(AcceptedCharacters.None);
+ }
+
+ public static SpanConstructor CodeTransition(this SpanFactory self, string content, CSharpSymbolType type)
+ {
+ return self.Span(SpanKind.Transition, content, type).Accepts(AcceptedCharacters.None);
+ }
+
+ public static SpanConstructor CodeTransition(this SpanFactory self, VBSymbolType type)
+ {
+ return self.Span(SpanKind.Transition, SyntaxConstants.TransitionString, type).Accepts(AcceptedCharacters.None);
+ }
+
+ public static SpanConstructor CodeTransition(this SpanFactory self, string content, VBSymbolType type)
+ {
+ return self.Span(SpanKind.Transition, content, type).Accepts(AcceptedCharacters.None);
+ }
+
+ public static SpanConstructor MarkupTransition(this SpanFactory self)
+ {
+ return self.Span(SpanKind.Transition, SyntaxConstants.TransitionString, markup: true).Accepts(AcceptedCharacters.None);
+ }
+
+ public static SpanConstructor MarkupTransition(this SpanFactory self, string content)
+ {
+ return self.Span(SpanKind.Transition, content, markup: true).Accepts(AcceptedCharacters.None);
+ }
+
+ public static SpanConstructor MarkupTransition(this SpanFactory self, HtmlSymbolType type)
+ {
+ return self.Span(SpanKind.Transition, SyntaxConstants.TransitionString, type).Accepts(AcceptedCharacters.None);
+ }
+
+ public static SpanConstructor MarkupTransition(this SpanFactory self, string content, HtmlSymbolType type)
+ {
+ return self.Span(SpanKind.Transition, content, type).Accepts(AcceptedCharacters.None);
+ }
+
+ public static SpanConstructor MetaCode(this SpanFactory self, string content)
+ {
+ return self.Span(SpanKind.MetaCode, content, markup: false);
+ }
+
+ public static SpanConstructor MetaCode(this SpanFactory self, string content, CSharpSymbolType type)
+ {
+ return self.Span(SpanKind.MetaCode, content, type);
+ }
+
+ public static SpanConstructor MetaCode(this SpanFactory self, string content, VBSymbolType type)
+ {
+ return self.Span(SpanKind.MetaCode, content, type);
+ }
+
+ public static SpanConstructor MetaMarkup(this SpanFactory self, string content)
+ {
+ return self.Span(SpanKind.MetaCode, content, markup: true);
+ }
+
+ public static SpanConstructor MetaMarkup(this SpanFactory self, string content, HtmlSymbolType type)
+ {
+ return self.Span(SpanKind.MetaCode, content, type);
+ }
+
+ public static SpanConstructor Comment(this SpanFactory self, string content, CSharpSymbolType type)
+ {
+ return self.Span(SpanKind.Comment, content, type);
+ }
+
+ public static SpanConstructor Comment(this SpanFactory self, string content, VBSymbolType type)
+ {
+ return self.Span(SpanKind.Comment, content, type);
+ }
+
+ public static SpanConstructor Comment(this SpanFactory self, string content, HtmlSymbolType type)
+ {
+ return self.Span(SpanKind.Comment, content, type);
+ }
+
+ public static SpanConstructor Markup(this SpanFactory self, string content)
+ {
+ return self.Span(SpanKind.Markup, content, markup: true).With(new MarkupCodeGenerator());
+ }
+
+ public static SpanConstructor Markup(this SpanFactory self, params string[] content)
+ {
+ return self.Span(SpanKind.Markup, content, markup: true).With(new MarkupCodeGenerator());
+ }
+
+ public static SourceLocation GetLocationAndAdvance(this SourceLocationTracker self, string content)
+ {
+ SourceLocation ret = self.CurrentLocation;
+ self.UpdateLocation(content);
+ return ret;
+ }
+ }
+
+ public class SpanFactory
+ {
+ public Func<ITextDocument, ITokenizer> MarkupTokenizerFactory { get; set; }
+ public Func<ITextDocument, ITokenizer> CodeTokenizerFactory { get; set; }
+ public SourceLocationTracker LocationTracker { get; private set; }
+
+ public static SpanFactory CreateCsHtml()
+ {
+ return new SpanFactory()
+ {
+ MarkupTokenizerFactory = doc => new HtmlTokenizer(doc),
+ CodeTokenizerFactory = doc => new CSharpTokenizer(doc)
+ };
+ }
+
+ public static SpanFactory CreateVbHtml()
+ {
+ return new SpanFactory()
+ {
+ MarkupTokenizerFactory = doc => new HtmlTokenizer(doc),
+ CodeTokenizerFactory = doc => new VBTokenizer(doc)
+ };
+ }
+
+ public SpanFactory()
+ {
+ LocationTracker = new SourceLocationTracker();
+ }
+
+ public SpanConstructor Span(SpanKind kind, string content, CSharpSymbolType type)
+ {
+ return CreateSymbolSpan(kind, content, st => new CSharpSymbol(st, content, type));
+ }
+
+ public SpanConstructor Span(SpanKind kind, string content, VBSymbolType type)
+ {
+ return CreateSymbolSpan(kind, content, st => new VBSymbol(st, content, type));
+ }
+
+ public SpanConstructor Span(SpanKind kind, string content, HtmlSymbolType type)
+ {
+ return CreateSymbolSpan(kind, content, st => new HtmlSymbol(st, content, type));
+ }
+
+ public SpanConstructor Span(SpanKind kind, string content, bool markup)
+ {
+ return new SpanConstructor(kind, Tokenize(new[] { content }, markup));
+ }
+
+ public SpanConstructor Span(SpanKind kind, string[] content, bool markup)
+ {
+ return new SpanConstructor(kind, Tokenize(content, markup));
+ }
+
+ public SpanConstructor Span(SpanKind kind, params ISymbol[] symbols)
+ {
+ return new SpanConstructor(kind, symbols);
+ }
+
+ private SpanConstructor CreateSymbolSpan(SpanKind kind, string content, Func<SourceLocation, ISymbol> ctor)
+ {
+ SourceLocation start = LocationTracker.CurrentLocation;
+ LocationTracker.UpdateLocation(content);
+ return new SpanConstructor(kind, new[] { ctor(start) });
+ }
+
+ public void Reset()
+ {
+ LocationTracker.CurrentLocation = SourceLocation.Zero;
+ }
+
+ private IEnumerable<ISymbol> Tokenize(IEnumerable<string> contentFragments, bool markup)
+ {
+ return contentFragments.SelectMany(fragment => Tokenize(fragment, markup));
+ }
+
+ private IEnumerable<ISymbol> Tokenize(string content, bool markup)
+ {
+ ITokenizer tok = MakeTokenizer(markup, new SeekableTextReader(content));
+ ISymbol sym;
+ ISymbol last = null;
+ while ((sym = tok.NextSymbol()) != null)
+ {
+ OffsetStart(sym, LocationTracker.CurrentLocation);
+ last = sym;
+ yield return sym;
+ }
+ LocationTracker.UpdateLocation(content);
+ }
+
+ private ITokenizer MakeTokenizer(bool markup, SeekableTextReader seekableTextReader)
+ {
+ if (markup)
+ {
+ return MarkupTokenizerFactory(seekableTextReader);
+ }
+ else
+ {
+ return CodeTokenizerFactory(seekableTextReader);
+ }
+ }
+
+ private void OffsetStart(ISymbol sym, SourceLocation sourceLocation)
+ {
+ sym.OffsetStart(sourceLocation);
+ }
+ }
+
+ public static class SpanConstructorExtensions
+ {
+ public static SpanConstructor Accepts(this SpanConstructor self, AcceptedCharacters accepted)
+ {
+ return self.With(eh => eh.AcceptedCharacters = accepted);
+ }
+
+ public static SpanConstructor AutoCompleteWith(this SpanConstructor self, string autoCompleteString)
+ {
+ return AutoCompleteWith(self, autoCompleteString, atEndOfSpan: false);
+ }
+
+ public static SpanConstructor AutoCompleteWith(this SpanConstructor self, string autoCompleteString, bool atEndOfSpan)
+ {
+ return self.With(new AutoCompleteEditHandler(SpanConstructor.TestTokenizer) { AutoCompleteString = autoCompleteString, AutoCompleteAtEndOfSpan = atEndOfSpan });
+ }
+
+ public static SpanConstructor WithEditorHints(this SpanConstructor self, EditorHints hints)
+ {
+ return self.With(eh => eh.EditorHints = hints);
+ }
+ }
+
+ public class UnclassifiedCodeSpanConstructor
+ {
+ SpanConstructor _self;
+
+ public UnclassifiedCodeSpanConstructor(SpanConstructor self)
+ {
+ _self = self;
+ }
+
+ public SpanConstructor AsMetaCode()
+ {
+ _self.Builder.Kind = SpanKind.MetaCode;
+ return _self;
+ }
+
+ public SpanConstructor AsStatement()
+ {
+ return _self.With(new StatementCodeGenerator());
+ }
+
+ public SpanConstructor AsExpression()
+ {
+ return _self.With(new ExpressionCodeGenerator());
+ }
+
+ public SpanConstructor AsImplicitExpression(ISet<string> keywords)
+ {
+ return AsImplicitExpression(keywords, acceptTrailingDot: false);
+ }
+
+ public SpanConstructor AsImplicitExpression(ISet<string> keywords, bool acceptTrailingDot)
+ {
+ return _self.With(new ImplicitExpressionEditHandler(SpanConstructor.TestTokenizer, keywords, acceptTrailingDot))
+ .With(new ExpressionCodeGenerator());
+ }
+
+ public SpanConstructor AsFunctionsBody()
+ {
+ return _self.With(new TypeMemberCodeGenerator());
+ }
+
+ public SpanConstructor AsNamespaceImport(string ns, int namespaceKeywordLength)
+ {
+ return _self.With(new AddImportCodeGenerator(ns, namespaceKeywordLength));
+ }
+
+ public SpanConstructor Hidden()
+ {
+ return _self.With(SpanCodeGenerator.Null);
+ }
+
+ public SpanConstructor AsBaseType(string baseType)
+ {
+ return _self.With(new SetBaseTypeCodeGenerator(baseType));
+ }
+
+ public SpanConstructor AsRazorDirectiveAttribute(string key, string value)
+ {
+ return _self.With(new RazorDirectiveAttributeCodeGenerator(key, value));
+ }
+
+ public SpanConstructor As(ISpanCodeGenerator codeGenerator)
+ {
+ return _self.With(codeGenerator);
+ }
+ }
+
+ public class SpanConstructor
+ {
+ public SpanBuilder Builder { get; private set; }
+
+ internal static IEnumerable<ISymbol> TestTokenizer(string str)
+ {
+ yield return new RawTextSymbol(SourceLocation.Zero, str);
+ }
+
+ public SpanConstructor(SpanKind kind, IEnumerable<ISymbol> symbols)
+ {
+ Builder = new SpanBuilder();
+ Builder.Kind = kind;
+ Builder.EditHandler = SpanEditHandler.CreateDefault(TestTokenizer);
+ foreach (ISymbol sym in symbols)
+ {
+ Builder.Accept(sym);
+ }
+ }
+
+ private Span Build()
+ {
+ return Builder.Build();
+ }
+
+ public SpanConstructor With(ISpanCodeGenerator generator)
+ {
+ Builder.CodeGenerator = generator;
+ return this;
+ }
+
+ public SpanConstructor With(SpanEditHandler handler)
+ {
+ Builder.EditHandler = handler;
+ return this;
+ }
+
+ public SpanConstructor With(Action<ISpanCodeGenerator> generatorConfigurer)
+ {
+ generatorConfigurer(Builder.CodeGenerator);
+ return this;
+ }
+
+ public SpanConstructor With(Action<SpanEditHandler> handlerConfigurer)
+ {
+ handlerConfigurer(Builder.EditHandler);
+ return this;
+ }
+
+ public static implicit operator Span(SpanConstructor self)
+ {
+ return self.Build();
+ }
+
+ public SpanConstructor Hidden()
+ {
+ Builder.CodeGenerator = SpanCodeGenerator.Null;
+ return this;
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Framework/VBHtmlCodeParserTestBase.cs b/test/System.Web.Razor.Test/Framework/VBHtmlCodeParserTestBase.cs
new file mode 100644
index 00000000..65336ab7
--- /dev/null
+++ b/test/System.Web.Razor.Test/Framework/VBHtmlCodeParserTestBase.cs
@@ -0,0 +1,28 @@
+using System.Collections.Generic;
+using System.Web.Razor.Parser;
+
+namespace System.Web.Razor.Test.Framework
+{
+ public abstract class VBHtmlCodeParserTestBase : CodeParserTestBase
+ {
+ protected override ISet<string> KeywordSet
+ {
+ get { return VBCodeParser.DefaultKeywords; }
+ }
+
+ protected override SpanFactory CreateSpanFactory()
+ {
+ return SpanFactory.CreateVbHtml();
+ }
+
+ public override ParserBase CreateMarkupParser()
+ {
+ return new HtmlMarkupParser();
+ }
+
+ public override ParserBase CreateCodeParser()
+ {
+ return new VBCodeParser();
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Framework/VBHtmlMarkupParserTestBase.cs b/test/System.Web.Razor.Test/Framework/VBHtmlMarkupParserTestBase.cs
new file mode 100644
index 00000000..a336f048
--- /dev/null
+++ b/test/System.Web.Razor.Test/Framework/VBHtmlMarkupParserTestBase.cs
@@ -0,0 +1,28 @@
+using System.Collections.Generic;
+using System.Web.Razor.Parser;
+
+namespace System.Web.Razor.Test.Framework
+{
+ public abstract class VBHtmlMarkupParserTestBase : MarkupParserTestBase
+ {
+ protected override ISet<string> KeywordSet
+ {
+ get { return VBCodeParser.DefaultKeywords; }
+ }
+
+ protected override SpanFactory CreateSpanFactory()
+ {
+ return SpanFactory.CreateVbHtml();
+ }
+
+ public override ParserBase CreateMarkupParser()
+ {
+ return new HtmlMarkupParser();
+ }
+
+ public override ParserBase CreateCodeParser()
+ {
+ return new VBCodeParser();
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Generator/CSharpRazorCodeGeneratorTest.cs b/test/System.Web.Razor.Test/Generator/CSharpRazorCodeGeneratorTest.cs
new file mode 100644
index 00000000..996aee4c
--- /dev/null
+++ b/test/System.Web.Razor.Test/Generator/CSharpRazorCodeGeneratorTest.cs
@@ -0,0 +1,290 @@
+using System.Collections.Generic;
+using System.Web.Razor.Generator;
+using System.Web.WebPages.TestUtils;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Razor.Test.Generator
+{
+ public class CSharpRazorCodeGeneratorTest : RazorCodeGeneratorTest<CSharpRazorCodeLanguage>
+ {
+ protected override string FileExtension
+ {
+ get { return "cshtml"; }
+ }
+
+ protected override string LanguageName
+ {
+ get { return "CS"; }
+ }
+
+ protected override string BaselineExtension
+ {
+ get { return "cs"; }
+ }
+
+ private const string TestPhysicalPath = @"C:\Bar.cshtml";
+ private const string TestVirtualPath = "~/Foo/Bar.cshtml";
+
+ [Fact]
+ public void ConstructorRequiresNonNullClassName()
+ {
+ Assert.ThrowsArgumentNullOrEmptyString(() => new CSharpRazorCodeGenerator(null, TestRootNamespaceName, TestPhysicalPath, CreateHost()), "className");
+ }
+
+ [Fact]
+ public void ConstructorRequiresNonEmptyClassName()
+ {
+ Assert.ThrowsArgumentNullOrEmptyString(() => new CSharpRazorCodeGenerator(String.Empty, TestRootNamespaceName, TestPhysicalPath, CreateHost()), "className");
+ }
+
+ [Fact]
+ public void ConstructorRequiresNonNullRootNamespaceName()
+ {
+ Assert.ThrowsArgumentNull(() => new CSharpRazorCodeGenerator("Foo", null, TestPhysicalPath, CreateHost()), "rootNamespaceName");
+ }
+
+ [Fact]
+ public void ConstructorAllowsEmptyRootNamespaceName()
+ {
+ new CSharpRazorCodeGenerator("Foo", String.Empty, TestPhysicalPath, CreateHost());
+ }
+
+ [Fact]
+ public void ConstructorRequiresNonNullHost()
+ {
+ Assert.ThrowsArgumentNull(() => new CSharpRazorCodeGenerator("Foo", TestRootNamespaceName, TestPhysicalPath, null), "host");
+ }
+
+ [Theory]
+ [InlineData("NestedCodeBlocks")]
+ [InlineData("CodeBlock")]
+ [InlineData("ExplicitExpression")]
+ [InlineData("MarkupInCodeBlock")]
+ [InlineData("Blocks")]
+ [InlineData("ImplicitExpression")]
+ [InlineData("Imports")]
+ [InlineData("ExpressionsInCode")]
+ [InlineData("FunctionsBlock")]
+ [InlineData("Templates")]
+ [InlineData("Sections")]
+ [InlineData("RazorComments")]
+ [InlineData("Helpers")]
+ [InlineData("HelpersMissingCloseParen")]
+ [InlineData("HelpersMissingOpenBrace")]
+ [InlineData("HelpersMissingOpenParen")]
+ [InlineData("NestedHelpers")]
+ [InlineData("InlineBlocks")]
+ [InlineData("NestedHelpers")]
+ [InlineData("LayoutDirective")]
+ [InlineData("ConditionalAttributes")]
+ [InlineData("ResolveUrl")]
+ public void CSharpCodeGeneratorCorrectlyGeneratesRunTimeCode(string testType)
+ {
+ RunTest(testType);
+ }
+
+ //// To regenerate individual baselines, uncomment this and set the appropriate test name in the Inline Data.
+ //// Please comment out again after regenerating.
+ //// TODO: Remove this when we go to a Source Control system that doesn't lock files, thus requiring we unlock them to regenerate them :(
+ //[Theory]
+ //[InlineData("ConditionalAttributes")]
+ //public void CSharpCodeGeneratorCorrectlyGeneratesRunTimeCode2(string testType)
+ //{
+ // RunTest(testType);
+ //}
+
+ [Fact]
+ public void CSharpCodeGeneratorCorrectlyGeneratesMappingsForRazorCommentsAtDesignTime()
+ {
+ RunTest("RazorComments", "RazorComments.DesignTime", designTimeMode: true, expectedDesignTimePragmas: new List<GeneratedCodeMapping>()
+ {
+ /* 01 */ new GeneratedCodeMapping(4, 3, 3, 6),
+ /* 02 */ new GeneratedCodeMapping(5, 40, 39, 22),
+ /* 03 */ new GeneratedCodeMapping(6, 50, 49, 58),
+ /* 04 */ new GeneratedCodeMapping(12, 3, 3, 24),
+ /* 05 */ new GeneratedCodeMapping(13, 46, 46, 3),
+ /* 06 */ new GeneratedCodeMapping(15, 3, 7, 1),
+ /* 07 */ new GeneratedCodeMapping(15, 8, 8, 1)
+ });
+ }
+
+ [Fact]
+ public void CSharpCodeGeneratorCorrectlyGeneratesImportStatementsAtDesignTime()
+ {
+ RunTest("Imports", "Imports.DesignTime", designTimeMode: true, expectedDesignTimePragmas: new List<GeneratedCodeMapping>()
+ {
+ /* 01 */ new GeneratedCodeMapping(1, 2, 1, 15),
+ /* 02 */ new GeneratedCodeMapping(2, 2, 1, 32),
+ /* 03 */ new GeneratedCodeMapping(3, 2, 1, 12),
+ /* 04 */ new GeneratedCodeMapping(5, 30, 30, 21),
+ /* 05 */ new GeneratedCodeMapping(6, 36, 36, 20),
+ });
+ }
+
+ [Fact]
+ public void CSharpCodeGeneratorCorrectlyGeneratesFunctionsBlocksAtDesignTime()
+ {
+ RunTest("FunctionsBlock", "FunctionsBlock.DesignTime", designTimeMode: true, expectedDesignTimePragmas: new List<GeneratedCodeMapping>()
+ {
+ /* 01 */ new GeneratedCodeMapping(1, 13, 13, 4),
+ /* 02 */ new GeneratedCodeMapping(5, 13, 13, 104),
+ /* 03 */ new GeneratedCodeMapping(12, 26, 26, 11)
+ });
+ }
+
+ [Fact]
+ public void CSharpCodeGeneratorCorrectlyGeneratesHiddenSpansWithinCode()
+ {
+ RunTest("HiddenSpansInCode", designTimeMode: true, expectedDesignTimePragmas: new List<GeneratedCodeMapping>
+ {
+ /* 01 */ new GeneratedCodeMapping(1, 3, 3, 6),
+ /* 02 */ new GeneratedCodeMapping(2, 6, 6, 5)
+ });
+ }
+
+ [Fact]
+ public void CSharpCodeGeneratorGeneratesCodeWithParserErrorsInDesignTimeMode()
+ {
+ RunTest("ParserError", designTimeMode: true, expectedDesignTimePragmas: new List<GeneratedCodeMapping>()
+ {
+ /* 01 */ new GeneratedCodeMapping(1, 3, 3, 31)
+ });
+ }
+
+ [Fact]
+ public void CSharpCodeGeneratorCorrectlyGeneratesInheritsAtRuntime()
+ {
+ RunTest("Inherits", baselineName: "Inherits.Runtime");
+ }
+
+ [Fact]
+ public void CSharpCodeGeneratorCorrectlyGeneratesInheritsAtDesigntime()
+ {
+ RunTest("Inherits", baselineName: "Inherits.Designtime", designTimeMode: true, expectedDesignTimePragmas: new List<GeneratedCodeMapping>()
+ {
+ /* 01 */ new GeneratedCodeMapping(1, 2, 7, 5),
+ /* 02 */ new GeneratedCodeMapping(3, 11, 11, 25),
+ });
+ }
+
+ [Fact]
+ public void CSharpCodeGeneratorCorrectlyGeneratesDesignTimePragmasForUnfinishedExpressionsInCode()
+ {
+ RunTest("UnfinishedExpressionInCode", designTimeMode: true, expectedDesignTimePragmas: new List<GeneratedCodeMapping>()
+ {
+ /* 01 */ new GeneratedCodeMapping(1, 3, 3, 2),
+ /* 02 */ new GeneratedCodeMapping(2, 2, 7, 9),
+ /* 03 */ new GeneratedCodeMapping(2, 11, 11, 2)
+ });
+ }
+
+ [Fact]
+ public void CSharpCodeGeneratorCorrectlyGeneratesDesignTimePragmasMarkupAndExpressions()
+ {
+ RunTest("DesignTime", designTimeMode: true, expectedDesignTimePragmas: new List<GeneratedCodeMapping>()
+ {
+ /* 01 */ new GeneratedCodeMapping(2, 14, 13, 36),
+ /* 02 */ new GeneratedCodeMapping(3, 23, 23, 1),
+ /* 03 */ new GeneratedCodeMapping(3, 28, 28, 15),
+ /* 04 */ new GeneratedCodeMapping(8, 3, 7, 12),
+ /* 05 */ new GeneratedCodeMapping(9, 2, 7, 4),
+ /* 06 */ new GeneratedCodeMapping(9, 15, 15, 3),
+ /* 07 */ new GeneratedCodeMapping(9, 26, 26, 1),
+ /* 08 */ new GeneratedCodeMapping(14, 6, 7, 3),
+ /* 09 */ new GeneratedCodeMapping(17, 9, 24, 7),
+ /* 10 */ new GeneratedCodeMapping(17, 16, 16, 26),
+ /* 11 */ new GeneratedCodeMapping(19, 19, 19, 9),
+ /* 12 */ new GeneratedCodeMapping(21, 1, 1, 1)
+ });
+ }
+
+ [Fact]
+ public void CSharpCodeGeneratorCorrectlyGeneratesDesignTimePragmasForImplicitExpressionStartedAtEOF()
+ {
+ RunTest("ImplicitExpressionAtEOF", designTimeMode: true, expectedDesignTimePragmas: new List<GeneratedCodeMapping>()
+ {
+ /* 01 */ new GeneratedCodeMapping(3, 2, 7, 0)
+ });
+ }
+
+ [Fact]
+ public void CSharpCodeGeneratorCorrectlyGeneratesDesignTimePragmasForExplicitExpressionStartedAtEOF()
+ {
+ RunTest("ExplicitExpressionAtEOF", designTimeMode: true, expectedDesignTimePragmas: new List<GeneratedCodeMapping>()
+ {
+ /* 01 */ new GeneratedCodeMapping(3, 3, 7, 0)
+ });
+ }
+
+ [Fact]
+ public void CSharpCodeGeneratorCorrectlyGeneratesDesignTimePragmasForCodeBlockStartedAtEOF()
+ {
+ RunTest("CodeBlockAtEOF", designTimeMode: true, expectedDesignTimePragmas: new List<GeneratedCodeMapping>()
+ {
+ /* 01 */ new GeneratedCodeMapping(1, 3, 3, 0)
+ });
+ }
+
+ [Fact]
+ public void CSharpCodeGeneratorCorrectlyGeneratesDesignTimePragmasForEmptyImplicitExpression()
+ {
+ RunTest("EmptyImplicitExpression", designTimeMode: true, expectedDesignTimePragmas: new List<GeneratedCodeMapping>()
+ {
+ /* 01 */ new GeneratedCodeMapping(3, 2, 7, 0)
+ });
+ }
+
+ [Fact]
+ public void CSharpCodeGeneratorCorrectlyGeneratesDesignTimePragmasForEmptyImplicitExpressionInCode()
+ {
+ RunTest("EmptyImplicitExpressionInCode", designTimeMode: true, expectedDesignTimePragmas: new List<GeneratedCodeMapping>()
+ {
+ /* 01 */ new GeneratedCodeMapping(1, 3, 3, 6),
+ /* 02 */ new GeneratedCodeMapping(2, 6, 7, 0),
+ /* 03 */ new GeneratedCodeMapping(2, 6, 6, 2)
+ });
+ }
+
+ [Fact]
+ public void CSharpCodeGeneratorCorrectlyGeneratesDesignTimePragmasForEmptyExplicitExpression()
+ {
+ RunTest("EmptyExplicitExpression", designTimeMode: true, expectedDesignTimePragmas: new List<GeneratedCodeMapping>()
+ {
+ /* 01 */ new GeneratedCodeMapping(3, 3, 7, 0)
+ });
+ }
+
+ [Fact]
+ public void CSharpCodeGeneratorCorrectlyGeneratesDesignTimePragmasForEmptyCodeBlock()
+ {
+ RunTest("EmptyCodeBlock", designTimeMode: true, expectedDesignTimePragmas: new List<GeneratedCodeMapping>()
+ {
+ /* 01 */ new GeneratedCodeMapping(3, 3, 3, 0)
+ });
+ }
+
+ [Fact]
+ public void CSharpCodeGeneratorDoesNotRenderLinePragmasIfGenerateLinePragmasIsSetToFalse()
+ {
+ RunTest("NoLinePragmas", generatePragmas: false);
+ }
+
+ [Fact]
+ public void CSharpCodeGeneratorRendersHelpersBlockCorrectlyWhenInstanceHelperRequested()
+ {
+ RunTest("Helpers", baselineName: "Helpers.Instance", hostConfig: h => h.StaticHelpers = false);
+ }
+
+ [Fact]
+ public void CSharpCodeGeneratorCorrectlyInstrumentsRazorCodeWhenInstrumentationRequested()
+ {
+ RunTest("Instrumented", hostConfig: host =>
+ {
+ host.EnableInstrumentation = true;
+ host.InstrumentedSourceFilePath = String.Format("~/{0}.cshtml", host.DefaultClassName);
+ });
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Generator/GeneratedCodeMappingTest.cs b/test/System.Web.Razor.Test/Generator/GeneratedCodeMappingTest.cs
new file mode 100644
index 00000000..b0abdef4
--- /dev/null
+++ b/test/System.Web.Razor.Test/Generator/GeneratedCodeMappingTest.cs
@@ -0,0 +1,63 @@
+using System.Web.Razor.Generator;
+using Xunit;
+
+namespace System.Web.Razor.Test.Generator
+{
+ public class GeneratedCodeMappingTest
+ {
+ [Fact]
+ public void GeneratedCodeMappingsAreEqualIfDataIsEqual()
+ {
+ GeneratedCodeMapping left = new GeneratedCodeMapping(12, 34, 56, 78);
+ GeneratedCodeMapping right = new GeneratedCodeMapping(12, 34, 56, 78);
+ Assert.True(left == right);
+ Assert.True(left.Equals(right));
+ Assert.True(right.Equals(left));
+ Assert.True(Equals(left, right));
+ }
+
+ [Fact]
+ public void GeneratedCodeMappingsAreNotEqualIfCodeLengthIsNotEqual()
+ {
+ GeneratedCodeMapping left = new GeneratedCodeMapping(12, 34, 56, 87);
+ GeneratedCodeMapping right = new GeneratedCodeMapping(12, 34, 56, 78);
+ Assert.False(left == right);
+ Assert.False(left.Equals(right));
+ Assert.False(right.Equals(left));
+ Assert.False(Equals(left, right));
+ }
+
+ [Fact]
+ public void GeneratedCodeMappingsAreNotEqualIfStartGeneratedColumnIsNotEqual()
+ {
+ GeneratedCodeMapping left = new GeneratedCodeMapping(12, 34, 56, 87);
+ GeneratedCodeMapping right = new GeneratedCodeMapping(12, 34, 65, 87);
+ Assert.False(left == right);
+ Assert.False(left.Equals(right));
+ Assert.False(right.Equals(left));
+ Assert.False(Equals(left, right));
+ }
+
+ [Fact]
+ public void GeneratedCodeMappingsAreNotEqualIfStartColumnIsNotEqual()
+ {
+ GeneratedCodeMapping left = new GeneratedCodeMapping(12, 34, 56, 87);
+ GeneratedCodeMapping right = new GeneratedCodeMapping(12, 43, 56, 87);
+ Assert.False(left == right);
+ Assert.False(left.Equals(right));
+ Assert.False(right.Equals(left));
+ Assert.False(Equals(left, right));
+ }
+
+ [Fact]
+ public void GeneratedCodeMappingsAreNotEqualIfStartLineIsNotEqual()
+ {
+ GeneratedCodeMapping left = new GeneratedCodeMapping(12, 34, 56, 87);
+ GeneratedCodeMapping right = new GeneratedCodeMapping(21, 34, 56, 87);
+ Assert.False(left == right);
+ Assert.False(left.Equals(right));
+ Assert.False(right.Equals(left));
+ Assert.False(Equals(left, right));
+ }
+ }
+} \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/Generator/RazorCodeGeneratorTest.cs b/test/System.Web.Razor.Test/Generator/RazorCodeGeneratorTest.cs
new file mode 100644
index 00000000..0a130d95
--- /dev/null
+++ b/test/System.Web.Razor.Test/Generator/RazorCodeGeneratorTest.cs
@@ -0,0 +1,146 @@
+//#define GENERATE_BASELINES
+
+using System.CodeDom;
+using System.CodeDom.Compiler;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Web.Razor.Generator;
+using System.Web.Razor.Test.Utils;
+using System.Web.WebPages.TestUtils;
+using Xunit;
+
+namespace System.Web.Razor.Test.Generator
+{
+ public abstract class RazorCodeGeneratorTest<TLanguage>
+ where TLanguage : RazorCodeLanguage, new()
+ {
+ protected static readonly string TestRootNamespaceName = "TestOutput";
+
+ protected abstract string FileExtension { get; }
+ protected abstract string LanguageName { get; }
+ protected abstract string BaselineExtension { get; }
+
+ protected RazorEngineHost CreateHost()
+ {
+ return new RazorEngineHost(new TLanguage());
+ }
+
+ protected void RunTest(string name, string baselineName = null, bool generatePragmas = true, bool designTimeMode = false, IList<GeneratedCodeMapping> expectedDesignTimePragmas = null, Action<RazorEngineHost> hostConfig = null)
+ {
+ // Load the test files
+ if (baselineName == null)
+ {
+ baselineName = name;
+ }
+ string source = TestFile.Create(String.Format("CodeGenerator.{1}.Source.{0}.{2}", name, LanguageName, FileExtension)).ReadAllText();
+ string expectedOutput = TestFile.Create(String.Format("CodeGenerator.{1}.Output.{0}.{2}", baselineName, LanguageName, BaselineExtension)).ReadAllText();
+
+ // Set up the host and engine
+ RazorEngineHost host = CreateHost();
+ host.NamespaceImports.Add("System");
+ host.DesignTimeMode = designTimeMode;
+ host.StaticHelpers = true;
+ host.DefaultClassName = name;
+
+ // Add support for templates, etc.
+ host.GeneratedClassContext = new GeneratedClassContext(GeneratedClassContext.DefaultExecuteMethodName,
+ GeneratedClassContext.DefaultWriteMethodName,
+ GeneratedClassContext.DefaultWriteLiteralMethodName,
+ "WriteTo",
+ "WriteLiteralTo",
+ "Template",
+ "DefineSection",
+ "BeginContext",
+ "EndContext")
+ {
+ LayoutPropertyName = "Layout",
+ ResolveUrlMethodName = "Href"
+ };
+ if (hostConfig != null)
+ {
+ hostConfig(host);
+ }
+
+ RazorTemplateEngine engine = new RazorTemplateEngine(host);
+
+ // Generate code for the file
+ GeneratorResults results = null;
+ using (StringTextBuffer buffer = new StringTextBuffer(source))
+ {
+ results = engine.GenerateCode(buffer, className: name, rootNamespace: TestRootNamespaceName, sourceFileName: generatePragmas ? String.Format("{0}.{1}", name, FileExtension) : null);
+ }
+
+ // Generate code
+ CodeCompileUnit ccu = results.GeneratedCode;
+ CodeDomProvider codeProvider = (CodeDomProvider)Activator.CreateInstance(host.CodeLanguage.CodeDomProviderType);
+
+ CodeGeneratorOptions options = new CodeGeneratorOptions();
+
+ // Both run-time and design-time use these settings. See:
+ // * $/Dev10/pu/SP_WebTools/venus/html/Razor/Impl/RazorCodeGenerator.cs:204
+ // * $/Dev10/Releases/RTMRel/ndp/fx/src/xsp/System/Web/Compilation/BuildManagerHost.cs:373
+ options.BlankLinesBetweenMembers = false;
+ options.IndentString = String.Empty;
+
+ StringBuilder output = new StringBuilder();
+ using (StringWriter writer = new StringWriter(output))
+ {
+ codeProvider.GenerateCodeFromCompileUnit(ccu, writer, options);
+ }
+
+ WriteBaseline(String.Format(@"test\System.Web.Razor.Test\TestFiles\CodeGenerator\{0}\Output\{1}.{2}", LanguageName, baselineName, BaselineExtension), MiscUtils.StripRuntimeVersion(output.ToString()));
+
+ // Verify code against baseline
+#if !GENERATE_BASELINES
+ Assert.Equal(expectedOutput, MiscUtils.StripRuntimeVersion(output.ToString()));
+#endif
+
+ // Verify design-time pragmas
+ if (designTimeMode)
+ {
+ Assert.True(expectedDesignTimePragmas != null || results.DesignTimeLineMappings == null || results.DesignTimeLineMappings.Count == 0);
+ Assert.True(expectedDesignTimePragmas == null || (results.DesignTimeLineMappings != null && results.DesignTimeLineMappings.Count > 0));
+ if (expectedDesignTimePragmas != null)
+ {
+ Assert.Equal(
+ expectedDesignTimePragmas.ToArray(),
+ results.DesignTimeLineMappings
+ .OrderBy(p => p.Key)
+ .Select(p => p.Value)
+ .ToArray());
+ }
+ }
+ }
+
+ [Conditional("GENERATE_BASELINES")]
+ private void WriteBaseline(string baselineFile, string output)
+ {
+ string root = RecursiveFind("Runtime.sln", Path.GetFullPath("."));
+ string baselinePath = Path.Combine(root, baselineFile);
+
+ // Update baseline
+ // IMPORTANT! Replace this path with the local path on your machine to the baseline files!
+ if (File.Exists(baselinePath))
+ {
+ File.Delete(baselinePath);
+ }
+ File.WriteAllText(baselinePath, output.ToString());
+ }
+
+ private string RecursiveFind(string path, string start)
+ {
+ string test = Path.Combine(start, path);
+ if (File.Exists(test))
+ {
+ return start;
+ }
+ else
+ {
+ return RecursiveFind(path, new DirectoryInfo(start).Parent.FullName);
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Generator/VBRazorCodeGeneratorTest.cs b/test/System.Web.Razor.Test/Generator/VBRazorCodeGeneratorTest.cs
new file mode 100644
index 00000000..debdd8d3
--- /dev/null
+++ b/test/System.Web.Razor.Test/Generator/VBRazorCodeGeneratorTest.cs
@@ -0,0 +1,279 @@
+using System.Collections.Generic;
+using System.Web.Razor.Generator;
+using System.Web.WebPages.TestUtils;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Razor.Test.Generator
+{
+ public class VBRazorCodeGeneratorTest : RazorCodeGeneratorTest<VBRazorCodeLanguage>
+ {
+ private const string TestPhysicalPath = @"C:\Bar.vbhtml";
+ private const string TestVirtualPath = "~/Foo/Bar.vbhtml";
+
+ protected override string FileExtension
+ {
+ get { return "vbhtml"; }
+ }
+
+ protected override string LanguageName
+ {
+ get { return "VB"; }
+ }
+
+ protected override string BaselineExtension
+ {
+ get { return "vb"; }
+ }
+
+ [Fact]
+ public void ConstructorRequiresNonNullClassName()
+ {
+ Assert.ThrowsArgumentNullOrEmptyString(() => new VBRazorCodeGenerator(null, TestRootNamespaceName, TestPhysicalPath, CreateHost()), "className");
+ }
+
+ [Fact]
+ public void ConstructorRequiresNonEmptyClassName()
+ {
+ Assert.ThrowsArgumentNullOrEmptyString(() => new VBRazorCodeGenerator(String.Empty, TestRootNamespaceName, TestPhysicalPath, CreateHost()), "className");
+ }
+
+ [Fact]
+ public void ConstructorRequiresNonNullRootNamespaceName()
+ {
+ Assert.ThrowsArgumentNull(() => new VBRazorCodeGenerator("Foo", null, TestPhysicalPath, CreateHost()), "rootNamespaceName");
+ }
+
+ [Fact]
+ public void ConstructorAllowsEmptyRootNamespaceName()
+ {
+ new VBRazorCodeGenerator("Foo", String.Empty, TestPhysicalPath, CreateHost());
+ }
+
+ [Fact]
+ public void ConstructorRequiresNonNullHost()
+ {
+ Assert.ThrowsArgumentNull(() => new VBRazorCodeGenerator("Foo", TestRootNamespaceName, TestPhysicalPath, null), "host");
+ }
+
+ [Theory]
+ [InlineData("NestedCodeBlocks")]
+ [InlineData("NestedCodeBlocks")]
+ [InlineData("CodeBlock")]
+ [InlineData("ExplicitExpression")]
+ [InlineData("MarkupInCodeBlock")]
+ [InlineData("Blocks")]
+ [InlineData("ImplicitExpression")]
+ [InlineData("Imports")]
+ [InlineData("ExpressionsInCode")]
+ [InlineData("FunctionsBlock")]
+ [InlineData("Options")]
+ [InlineData("Templates")]
+ [InlineData("RazorComments")]
+ [InlineData("Sections")]
+ [InlineData("Helpers")]
+ [InlineData("HelpersMissingCloseParen")]
+ [InlineData("HelpersMissingOpenParen")]
+ [InlineData("NestedHelpers")]
+ [InlineData("LayoutDirective")]
+ [InlineData("ConditionalAttributes")]
+ [InlineData("ResolveUrl")]
+ public void VBCodeGeneratorCorrectlyGeneratesRunTimeCode(string testName)
+ {
+ RunTest(testName);
+ }
+
+ //// To regenerate individual baselines, uncomment this and set the appropriate test name in the Inline Data.
+ //// Please comment out again after regenerating.
+ //// TODO: Remove this when we go to a Source Control system that doesn't lock files, thus requiring we unlock them to regenerate them :(
+ //[Theory]
+ //[InlineData("RazorComments")]
+ //public void VBCodeGeneratorCorrectlyGeneratesRunTimeCode2(string testType)
+ //{
+ // RunTest(testType);
+ //}
+
+ [Fact]
+ public void VBCodeGeneratorCorrectlyGeneratesMappingsForRazorCommentsAtDesignTime()
+ {
+ // (4, 6) -> (?, 6) [6]
+ // ( 5, 40) -> (?, 39) [2]
+ // ( 8, 6) -> (?, 6) [33]
+ // ( 9, 46) -> (?, 46) [3]
+ // ( 12, 3) -> (?, 7) [3]
+ // ( 12, 8) -> (?, 8) [1]
+ RunTest("RazorComments", "RazorComments.DesignTime", designTimeMode: true, expectedDesignTimePragmas: new List<GeneratedCodeMapping>()
+ {
+ /* 01 */ new GeneratedCodeMapping(4, 6, 6, 6),
+ /* 02 */ new GeneratedCodeMapping(5, 40, 39, 2),
+ /* 03 */ new GeneratedCodeMapping(8, 6, 6, 33),
+ /* 04 */ new GeneratedCodeMapping(9, 46, 46, 3),
+ /* 05 */ new GeneratedCodeMapping(12, 3, 7, 1),
+ /* 06 */ new GeneratedCodeMapping(12, 8, 8, 1)
+ });
+ }
+
+ [Fact]
+ public void VBCodeGeneratorCorrectlyGeneratesHelperMissingNameAtDesignTime()
+ {
+ RunTest("HelpersMissingName", designTimeMode: true);
+ }
+
+ [Fact]
+ public void VBCodeGeneratorCorrectlyGeneratesImportStatementsAtDesignTimeButCannotWrapPragmasAroundImportStatement()
+ {
+ RunTest("Imports", "Imports.DesignTime", designTimeMode: true, expectedDesignTimePragmas: new List<GeneratedCodeMapping>()
+ {
+ /* 01 */ new GeneratedCodeMapping(1, 2, 1, 19),
+ /* 02 */ new GeneratedCodeMapping(2, 2, 1, 36),
+ /* 03 */ new GeneratedCodeMapping(3, 2, 1, 16),
+ /* 04 */ new GeneratedCodeMapping(5, 30, 30, 22),
+ /* 05 */ new GeneratedCodeMapping(6, 36, 36, 21),
+ });
+ }
+
+ [Fact]
+ public void VBCodeGeneratorCorrectlyGeneratesFunctionsBlocksAtDesignTime()
+ {
+ RunTest("FunctionsBlock", "FunctionsBlock.DesignTime", designTimeMode: true, expectedDesignTimePragmas: new List<GeneratedCodeMapping>()
+ {
+ /* 01 */ new GeneratedCodeMapping(1, 11, 11, 4),
+ /* 02 */ new GeneratedCodeMapping(5, 11, 11, 129),
+ /* 03 */ new GeneratedCodeMapping(12, 26, 26, 11)
+ });
+ }
+
+ [Fact]
+ public void VBCodeGeneratorGeneratesCodeWithParserErrorsInDesignTimeMode()
+ {
+ RunTest("ParserError", designTimeMode: true, expectedDesignTimePragmas: new List<GeneratedCodeMapping>()
+ {
+ /* 01 */ new GeneratedCodeMapping(1, 6, 6, 16)
+ });
+ }
+
+ [Fact]
+ public void VBCodeGeneratorCorrectlyGeneratesInheritsAtRuntime()
+ {
+ RunTest("Inherits", baselineName: "Inherits.Runtime");
+ }
+
+ [Fact]
+ public void VBCodeGeneratorCorrectlyGeneratesInheritsAtDesigntime()
+ {
+ RunTest("Inherits", baselineName: "Inherits.Designtime", designTimeMode: true, expectedDesignTimePragmas: new List<GeneratedCodeMapping>()
+ {
+ /* 01 */ new GeneratedCodeMapping(1, 11, 25, 27)
+ });
+ }
+
+ [Fact]
+ public void VBCodeGeneratorCorrectlyGeneratesDesignTimePragmasForUnfinishedExpressionsInCode()
+ {
+ RunTest("UnfinishedExpressionInCode", designTimeMode: true, expectedDesignTimePragmas: new List<GeneratedCodeMapping>()
+ {
+ /* 01 */ new GeneratedCodeMapping(1, 6, 6, 2),
+ /* 02 */ new GeneratedCodeMapping(2, 2, 7, 9),
+ /* 03 */ new GeneratedCodeMapping(2, 11, 11, 2)
+ });
+ }
+
+ [Fact]
+ public void VBCodeGeneratorCorrectlyGeneratesDesignTimePragmasMarkupAndExpressions()
+ {
+ RunTest("DesignTime", designTimeMode: true, expectedDesignTimePragmas: new List<GeneratedCodeMapping>()
+ {
+ /* 01 */ new GeneratedCodeMapping(2, 14, 13, 17),
+ /* 02 */ new GeneratedCodeMapping(3, 20, 20, 1),
+ /* 03 */ new GeneratedCodeMapping(3, 25, 25, 20),
+ /* 04 */ new GeneratedCodeMapping(8, 3, 7, 12),
+ /* 05 */ new GeneratedCodeMapping(9, 2, 7, 4),
+ /* 06 */ new GeneratedCodeMapping(9, 16, 16, 3),
+ /* 07 */ new GeneratedCodeMapping(9, 27, 27, 1),
+ /* 08 */ new GeneratedCodeMapping(14, 6, 7, 3),
+ /* 09 */ new GeneratedCodeMapping(17, 9, 24, 5),
+ /* 10 */ new GeneratedCodeMapping(17, 14, 14, 28),
+ /* 11 */ new GeneratedCodeMapping(19, 20, 20, 14)
+ });
+ }
+
+ [Fact]
+ public void VBCodeGeneratorCorrectlyGeneratesDesignTimePragmasForImplicitExpressionStartedAtEOF()
+ {
+ RunTest("ImplicitExpressionAtEOF", designTimeMode: true, expectedDesignTimePragmas: new List<GeneratedCodeMapping>()
+ {
+ /* 01 */ new GeneratedCodeMapping(3, 2, 7, 0)
+ });
+ }
+
+ [Fact]
+ public void VBCodeGeneratorCorrectlyGeneratesDesignTimePragmasForExplicitExpressionStartedAtEOF()
+ {
+ RunTest("ExplicitExpressionAtEOF", designTimeMode: true, expectedDesignTimePragmas: new List<GeneratedCodeMapping>()
+ {
+ /* 01 */ new GeneratedCodeMapping(3, 3, 7, 0)
+ });
+ }
+
+ [Fact]
+ public void VBCodeGeneratorCorrectlyGeneratesDesignTimePragmasForCodeBlockStartedAtEOF()
+ {
+ RunTest("CodeBlockAtEOF", designTimeMode: true, expectedDesignTimePragmas: new List<GeneratedCodeMapping>()
+ {
+ /* 01 */ new GeneratedCodeMapping(3, 6, 6, 0)
+ });
+ }
+
+ [Fact]
+ public void VBCodeGeneratorCorrectlyGeneratesDesignTimePragmasForEmptyImplicitExpression()
+ {
+ RunTest("EmptyImplicitExpression", designTimeMode: true, expectedDesignTimePragmas: new List<GeneratedCodeMapping>()
+ {
+ /* 01 */ new GeneratedCodeMapping(3, 2, 7, 0)
+ });
+ }
+
+ [Fact]
+ public void VBCodeGeneratorCorrectlyGeneratesDesignTimePragmasForEmptyImplicitExpressionInCode()
+ {
+ RunTest("EmptyImplicitExpressionInCode", designTimeMode: true, expectedDesignTimePragmas: new List<GeneratedCodeMapping>()
+ {
+ /* 01 */ new GeneratedCodeMapping(1, 6, 6, 6),
+ /* 02 */ new GeneratedCodeMapping(2, 6, 7, 0),
+ /* 03 */ new GeneratedCodeMapping(2, 6, 6, 2)
+ });
+ }
+
+ [Fact]
+ public void VBCodeGeneratorCorrectlyGeneratesDesignTimePragmasForEmptyExplicitExpression()
+ {
+ RunTest("EmptyExplicitExpression", designTimeMode: true, expectedDesignTimePragmas: new List<GeneratedCodeMapping>()
+ {
+ /* 01 */ new GeneratedCodeMapping(3, 3, 7, 0)
+ });
+ }
+
+ [Fact]
+ public void VBCodeGeneratorDoesNotRenderLinePragmasIfGenerateLinePragmasIsSetToFalse()
+ {
+ RunTest("NoLinePragmas", generatePragmas: false);
+ }
+
+ [Fact]
+ public void VBCodeGeneratorRendersHelpersBlockCorrectlyWhenInstanceHelperRequested()
+ {
+ RunTest("Helpers", baselineName: "Helpers.Instance", hostConfig: h => h.StaticHelpers = false);
+ }
+
+ [Fact]
+ public void VBCodeGeneratorCorrectlyInstrumentsRazorCodeWhenInstrumentationRequested()
+ {
+ RunTest("Instrumented", hostConfig: host =>
+ {
+ host.EnableInstrumentation = true;
+ host.InstrumentedSourceFilePath = String.Format("~/{0}.vbhtml", host.DefaultClassName);
+ });
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/BlockTest.cs b/test/System.Web.Razor.Test/Parser/BlockTest.cs
new file mode 100644
index 00000000..92561fa0
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/BlockTest.cs
@@ -0,0 +1,122 @@
+using System.Linq;
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Test.Framework;
+using System.Web.Razor.Text;
+using System.Web.WebPages.TestUtils;
+using Xunit;
+
+namespace System.Web.Razor.Test.Parser
+{
+ public class BlockTest
+ {
+ [Fact]
+ public void ConstructorWithBlockBuilderSetsParent()
+ {
+ // Arrange
+ BlockBuilder builder = new BlockBuilder() { Type = BlockType.Comment };
+ Span span = new SpanBuilder() { Kind = SpanKind.Code }.Build();
+ builder.Children.Add(span);
+
+ // Act
+ Block block = builder.Build();
+
+ // Assert
+ Assert.Same(block, span.Parent);
+ }
+
+ [Fact]
+ public void ConstructorCopiesBasicValuesFromBlockBuilder()
+ {
+ // Arrange
+ BlockBuilder builder = new BlockBuilder()
+ {
+ Name = "Foo",
+ Type = BlockType.Helper
+ };
+
+ // Act
+ Block actual = builder.Build();
+
+ // Assert
+ Assert.Equal("Foo", actual.Name);
+ Assert.Equal(BlockType.Helper, actual.Type);
+ }
+
+ [Fact]
+ public void ConstructorTransfersInstanceOfCodeGeneratorFromBlockBuilder()
+ {
+ // Arrange
+ IBlockCodeGenerator expected = new ExpressionCodeGenerator();
+ BlockBuilder builder = new BlockBuilder()
+ {
+ Type = BlockType.Helper,
+ CodeGenerator = expected
+ };
+
+ // Act
+ Block actual = builder.Build();
+
+ // Assert
+ Assert.Same(expected, actual.CodeGenerator);
+ }
+
+ [Fact]
+ public void ConstructorTransfersChildrenFromBlockBuilder()
+ {
+ // Arrange
+ Span expected = new SpanBuilder() { Kind = SpanKind.Code }.Build();
+ BlockBuilder builder = new BlockBuilder()
+ {
+ Type = BlockType.Functions
+ };
+ builder.Children.Add(expected);
+
+ // Act
+ Block block = builder.Build();
+
+ // Assert
+ Assert.Same(expected, block.Children.Single());
+ }
+
+ [Fact]
+ public void LocateOwnerReturnsNullIfNoSpanReturnsTrueForOwnsSpan()
+ {
+ // Arrange
+ var factory = SpanFactory.CreateCsHtml();
+ Block block = new MarkupBlock(
+ factory.Markup("Foo "),
+ new StatementBlock(
+ factory.CodeTransition(),
+ factory.Code("bar").AsStatement()),
+ factory.Markup(" Baz"));
+ TextChange change = new TextChange(128, 1, new StringTextBuffer("Foo @bar Baz"), 1, new StringTextBuffer("Foo @bor Baz"));
+
+ // Act
+ Span actual = block.LocateOwner(change);
+
+ // Assert
+ Assert.Null(actual);
+ }
+
+ [Fact]
+ public void LocateOwnerReturnsNullIfChangeCrossesMultipleSpans()
+ {
+ // Arrange
+ var factory = SpanFactory.CreateCsHtml();
+ Block block = new MarkupBlock(
+ factory.Markup("Foo "),
+ new StatementBlock(
+ factory.CodeTransition(),
+ factory.Code("bar").AsStatement()),
+ factory.Markup(" Baz"));
+ TextChange change = new TextChange(4, 10, new StringTextBuffer("Foo @bar Baz"), 10, new StringTextBuffer("Foo @bor Baz"));
+
+ // Act
+ Span actual = block.LocateOwner(change);
+
+ // Assert
+ Assert.Null(actual);
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/CSharp/CSharpAutoCompleteTest.cs b/test/System.Web.Razor.Test/Parser/CSharp/CSharpAutoCompleteTest.cs
new file mode 100644
index 00000000..ed10c56f
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/CSharp/CSharpAutoCompleteTest.cs
@@ -0,0 +1,172 @@
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Test.Framework;
+using System.Web.Razor.Text;
+using System.Web.Razor.Tokenizer.Symbols;
+using Xunit;
+
+namespace System.Web.Razor.Test.Parser.CSharp
+{
+ public class CSharpAutoCompleteTest : CsHtmlCodeParserTestBase
+ {
+ [Fact]
+ public void FunctionsDirectiveAutoCompleteAtEOF()
+ {
+ ParseBlockTest("@functions{",
+ new FunctionsBlock(
+ Factory.CodeTransition("@")
+ .Accepts(AcceptedCharacters.None),
+ Factory.MetaCode("functions{")
+ .Accepts(AcceptedCharacters.None),
+ Factory.EmptyCSharp()
+ .AsFunctionsBody()
+ .With(new AutoCompleteEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString)
+ {
+ AutoCompleteString = "}"
+ })),
+ new RazorError(String.Format(RazorResources.ParseError_Expected_EndOfBlock_Before_EOF, "functions", "}", "{"),
+ 1, 0, 1));
+ }
+
+ [Fact]
+ public void HelperDirectiveAutoCompleteAtEOF()
+ {
+ ParseBlockTest("@helper Strong(string value) {",
+ new HelperBlock(new HelperCodeGenerator(new LocationTagged<string>("Strong(string value) {", 8, 0, 8), headerComplete: true),
+ Factory.CodeTransition(),
+ Factory.MetaCode("helper ")
+ .Accepts(AcceptedCharacters.None),
+ Factory.Code("Strong(string value) {")
+ .Hidden()
+ .Accepts(AcceptedCharacters.None),
+ new StatementBlock(
+ Factory.EmptyCSharp()
+ .AsStatement()
+ .With(new AutoCompleteEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString) { AutoCompleteString = "}" })
+ )
+ ),
+ new RazorError(String.Format(RazorResources.ParseError_Expected_EndOfBlock_Before_EOF, "helper", "}", "{"),
+ 1, 0, 1));
+ }
+
+ [Fact]
+ public void SectionDirectiveAutoCompleteAtEOF()
+ {
+ ParseBlockTest("@section Header {",
+ new SectionBlock(new SectionCodeGenerator("Header"),
+ Factory.CodeTransition(),
+ Factory.MetaCode("section Header {")
+ .AutoCompleteWith("}", atEndOfSpan: true)
+ .Accepts(AcceptedCharacters.Any),
+ new MarkupBlock()),
+ new RazorError(
+ String.Format(RazorResources.ParseError_Expected_X, "}"),
+ 17, 0, 17));
+ }
+
+ [Fact]
+ public void VerbatimBlockAutoCompleteAtEOF()
+ {
+ ParseBlockTest("@{",
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ Factory.EmptyCSharp()
+ .AsStatement()
+ .With(new AutoCompleteEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString) { AutoCompleteString = "}" })
+ ),
+ new RazorError(String.Format(RazorResources.ParseError_Expected_EndOfBlock_Before_EOF, RazorResources.BlockName_Code, "}", "{"),
+ 1, 0, 1));
+ }
+
+ [Fact]
+ public void FunctionsDirectiveAutoCompleteAtStartOfFile()
+ {
+ ParseBlockTest(@"@functions{
+foo",
+ new FunctionsBlock(
+ Factory.CodeTransition("@")
+ .Accepts(AcceptedCharacters.None),
+ Factory.MetaCode("functions{")
+ .Accepts(AcceptedCharacters.None),
+ Factory.Code("\r\nfoo")
+ .AsFunctionsBody()
+ .With(new AutoCompleteEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString)
+ {
+ AutoCompleteString = "}"
+ })),
+ new RazorError(String.Format(RazorResources.ParseError_Expected_EndOfBlock_Before_EOF, "functions", "}", "{"),
+ 1, 0, 1));
+ }
+
+ [Fact]
+ public void HelperDirectiveAutoCompleteAtStartOfFile()
+ {
+ ParseBlockTest(@"@helper Strong(string value) {
+<p></p>",
+ new HelperBlock(new HelperCodeGenerator(new LocationTagged<string>("Strong(string value) {", 8, 0, 8), headerComplete: true),
+ Factory.CodeTransition(),
+ Factory.MetaCode("helper ")
+ .Accepts(AcceptedCharacters.None),
+ Factory.Code("Strong(string value) {")
+ .Hidden()
+ .Accepts(AcceptedCharacters.None),
+ new StatementBlock(
+ Factory.Code("\r\n")
+ .AsStatement()
+ .With(new AutoCompleteEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString) { AutoCompleteString = "}" }),
+ new MarkupBlock(
+ Factory.Markup(@"<p></p>")
+ .With(new MarkupCodeGenerator())
+ .Accepts(AcceptedCharacters.None)
+ ),
+ Factory.Span(SpanKind.Code, new CSharpSymbol(Factory.LocationTracker.CurrentLocation, String.Empty, CSharpSymbolType.Unknown))
+ .With(new StatementCodeGenerator())
+ )
+ ),
+ new RazorError(String.Format(RazorResources.ParseError_Expected_EndOfBlock_Before_EOF, "helper", "}", "{"),
+ 1, 0, 1));
+ }
+
+ [Fact]
+ public void SectionDirectiveAutoCompleteAtStartOfFile()
+ {
+ ParseBlockTest(@"@section Header {
+<p>Foo</p>",
+ new SectionBlock(new SectionCodeGenerator("Header"),
+ Factory.CodeTransition(),
+ Factory.MetaCode("section Header {")
+ .AutoCompleteWith("}", atEndOfSpan: true)
+ .Accepts(AcceptedCharacters.Any),
+ new MarkupBlock(
+ Factory.Markup("\r\n<p>Foo</p>"))),
+ new RazorError(String.Format(RazorResources.ParseError_Expected_X, "}"),
+ 29, 1, 10));
+ }
+
+ [Fact]
+ public void VerbatimBlockAutoCompleteAtStartOfFile()
+ {
+ ParseBlockTest(@"@{
+<p></p>",
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ Factory.Code("\r\n")
+ .AsStatement()
+ .With(new AutoCompleteEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString) { AutoCompleteString = "}" }),
+ new MarkupBlock(
+ Factory.Markup(@"<p></p>")
+ .With(new MarkupCodeGenerator())
+ .Accepts(AcceptedCharacters.None)
+ ),
+ Factory.Span(SpanKind.Code, new CSharpSymbol(Factory.LocationTracker.CurrentLocation, String.Empty, CSharpSymbolType.Unknown))
+ .With(new StatementCodeGenerator())
+ ),
+ new RazorError(String.Format(RazorResources.ParseError_Expected_EndOfBlock_Before_EOF, RazorResources.BlockName_Code, "}", "{"),
+ 1, 0, 1));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/CSharp/CSharpBlockTest.cs b/test/System.Web.Razor.Test/Parser/CSharp/CSharpBlockTest.cs
new file mode 100644
index 00000000..addf3951
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/CSharp/CSharpBlockTest.cs
@@ -0,0 +1,731 @@
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Test.Framework;
+using System.Web.Razor.Text;
+using System.Web.Razor.Tokenizer.Symbols;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Razor.Test.Parser.CSharp
+{
+ public class CSharpBlockTest : CsHtmlCodeParserTestBase
+ {
+ [Fact]
+ public void ParseBlockMethodThrowsArgNullExceptionOnNullContext()
+ {
+ // Arrange
+ CSharpCodeParser parser = new CSharpCodeParser();
+
+ // Act and Assert
+ Assert.Throws<InvalidOperationException>(() => parser.ParseBlock(), RazorResources.Parser_Context_Not_Set);
+ }
+
+ [Fact]
+ public void BalancingBracketsIgnoresStringLiteralCharactersAndBracketsInsideSingleLineComments()
+ {
+ SingleSpanBlockTest(@"if(foo) {
+ // bar } "" baz '
+ zoop();
+}", BlockType.Statement, SpanKind.Code);
+ }
+
+ [Fact]
+ public void NestedCodeBlockWithAtCausesError()
+ {
+ ParseBlockTest("if (true) { @if(false) { } }",
+ new StatementBlock(
+ Factory.Code("if (true) { ").AsStatement(),
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("if(false) { }").AsStatement()
+ ),
+ Factory.Code(" }").AsStatement()),
+ new RazorError(
+ String.Format(RazorResources.ParseError_Unexpected_Keyword_After_At,
+ "if"),
+ new SourceLocation(13, 0, 13)));
+ }
+
+ [Fact]
+ public void BalancingBracketsIgnoresStringLiteralCharactersAndBracketsInsideBlockComments()
+ {
+ SingleSpanBlockTest(
+ @"if(foo) {
+ /* bar } "" */ ' baz } '
+ zoop();
+}", BlockType.Statement, SpanKind.Code);
+ }
+
+ [Fact]
+ public void ParseBlockSkipsParenthesisedExpressionAndThenBalancesBracesIfFirstIdentifierIsForKeyword()
+ {
+ SingleSpanBlockTest("for(int i = 0; i < 10; new Foo { Bar = \"baz\" }) { Debug.WriteLine(@\"foo } bar\"); }", BlockType.Statement, SpanKind.Code, acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockSkipsParenthesisedExpressionAndThenBalancesBracesIfFirstIdentifierIsForeachKeyword()
+ {
+ SingleSpanBlockTest("foreach(int i = 0; i < 10; new Foo { Bar = \"baz\" }) { Debug.WriteLine(@\"foo } bar\"); }", BlockType.Statement, SpanKind.Code, acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockSkipsParenthesisedExpressionAndThenBalancesBracesIfFirstIdentifierIsWhileKeyword()
+ {
+ SingleSpanBlockTest("while(int i = 0; i < 10; new Foo { Bar = \"baz\" }) { Debug.WriteLine(@\"foo } bar\"); }", BlockType.Statement, SpanKind.Code, acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockSkipsParenthesisedExpressionAndThenBalancesBracesIfFirstIdentifierIsUsingKeywordFollowedByParen()
+ {
+ SingleSpanBlockTest("using(int i = 0; i < 10; new Foo { Bar = \"baz\" }) { Debug.WriteLine(@\"foo } bar\"); }", BlockType.Statement, SpanKind.Code, acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockSupportsUsingsNestedWithinOtherBlocks()
+ {
+ SingleSpanBlockTest("if(foo) { using(int i = 0; i < 10; new Foo { Bar = \"baz\" }) { Debug.WriteLine(@\"foo } bar\"); } }", BlockType.Statement, SpanKind.Code);
+ }
+
+ [Fact]
+ public void ParseBlockSkipsParenthesisedExpressionAndThenBalancesBracesIfFirstIdentifierIsIfKeywordWithNoElseBranches()
+ {
+ SingleSpanBlockTest("if(int i = 0; i < 10; new Foo { Bar = \"baz\" }) { Debug.WriteLine(@\"foo } bar\"); }", BlockType.Statement, SpanKind.Code);
+ }
+
+ [Fact]
+ public void ParseBlockAllowsEmptyBlockStatement()
+ {
+ SingleSpanBlockTest("if(false) { }", BlockType.Statement, SpanKind.Code);
+ }
+
+ [Fact]
+ public void ParseBlockTerminatesParenBalancingAtEOF()
+ {
+ ImplicitExpressionTest("Html.En(code()", "Html.En(code()",
+ AcceptedCharacters.Any,
+ new RazorError(
+ String.Format(RazorResources.ParseError_Expected_CloseBracket_Before_EOF,
+ "(", ")"),
+ new SourceLocation(8, 0, 8)));
+ }
+
+ [Fact]
+ public void ParseBlockSupportsBlockCommentBetweenIfAndElseClause()
+ {
+ SingleSpanBlockTest("if(foo) { bar(); } /* Foo */ /* Bar */ else { baz(); }", BlockType.Statement, SpanKind.Code, acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockSupportsRazorCommentBetweenIfAndElseClause()
+ {
+ RunRazorCommentBetweenClausesTest("if(foo) { bar(); } ", " else { baz(); }", acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockSupportsBlockCommentBetweenElseIfAndElseClause()
+ {
+ SingleSpanBlockTest("if(foo) { bar(); } else if(bar) { baz(); } /* Foo */ /* Bar */ else { biz(); }", BlockType.Statement, SpanKind.Code, acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockSupportsRazorCommentBetweenElseIfAndElseClause()
+ {
+ RunRazorCommentBetweenClausesTest("if(foo) { bar(); } else if(bar) { baz(); } ", " else { baz(); }", acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockSupportsBlockCommentBetweenIfAndElseIfClause()
+ {
+ SingleSpanBlockTest("if(foo) { bar(); } /* Foo */ /* Bar */ else if(bar) { baz(); }", BlockType.Statement, SpanKind.Code);
+ }
+
+ [Fact]
+ public void ParseBlockSupportsRazorCommentBetweenIfAndElseIfClause()
+ {
+ RunRazorCommentBetweenClausesTest("if(foo) { bar(); } ", " else if(bar) { baz(); }");
+ }
+
+ [Fact]
+ public void ParseBlockSupportsLineCommentBetweenIfAndElseClause()
+ {
+ SingleSpanBlockTest(@"if(foo) { bar(); }
+// Foo
+// Bar
+else { baz(); }", BlockType.Statement, SpanKind.Code, acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockSupportsLineCommentBetweenElseIfAndElseClause()
+ {
+ SingleSpanBlockTest(@"if(foo) { bar(); } else if(bar) { baz(); }
+// Foo
+// Bar
+else { biz(); }", BlockType.Statement, SpanKind.Code, acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockSupportsLineCommentBetweenIfAndElseIfClause()
+ {
+ SingleSpanBlockTest(@"if(foo) { bar(); }
+// Foo
+// Bar
+else if(bar) { baz(); }", BlockType.Statement, SpanKind.Code);
+ }
+
+ [Fact]
+ public void ParseBlockParsesElseIfBranchesOfIfStatement()
+ {
+ const string ifStatement = @"if(int i = 0; i < 10; new Foo { Bar = ""baz"" }) {
+ Debug.WriteLine(@""foo } bar"");
+}";
+ const string elseIfBranch = @" else if(int i = 0; i < 10; new Foo { Bar = ""baz"" }) {
+ Debug.WriteLine(@""bar } baz"");
+}";
+ const string document = ifStatement + elseIfBranch;
+
+ SingleSpanBlockTest(document, BlockType.Statement, SpanKind.Code);
+ }
+
+ [Fact]
+ public void ParseBlockParsesMultipleElseIfBranchesOfIfStatement()
+ {
+ const string ifStatement = @"if(int i = 0; i < 10; new Foo { Bar = ""baz"" }) {
+ Debug.WriteLine(@""foo } bar"");
+}";
+ const string elseIfBranch = @" else if(int i = 0; i < 10; new Foo { Bar = ""baz"" }) {
+ Debug.WriteLine(@""bar } baz"");
+}";
+ const string document = ifStatement + elseIfBranch + elseIfBranch + elseIfBranch + elseIfBranch;
+ SingleSpanBlockTest(document, BlockType.Statement, SpanKind.Code);
+ }
+
+ [Fact]
+ public void ParseBlockParsesMultipleElseIfBranchesOfIfStatementFollowedByOneElseBranch()
+ {
+ const string ifStatement = @"if(int i = 0; i < 10; new Foo { Bar = ""baz"" }) {
+ Debug.WriteLine(@""foo } bar"");
+}";
+ const string elseIfBranch = @" else if(int i = 0; i < 10; new Foo { Bar = ""baz"" }) {
+ Debug.WriteLine(@""bar } baz"");
+}";
+ const string elseBranch = @" else { Debug.WriteLine(@""bar } baz""); }";
+ const string document = ifStatement + elseIfBranch + elseIfBranch + elseBranch;
+
+ SingleSpanBlockTest(document, BlockType.Statement, SpanKind.Code, acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockStopsParsingCodeAfterElseBranch()
+ {
+ const string ifStatement = @"if(int i = 0; i < 10; new Foo { Bar = ""baz"" }) {
+ Debug.WriteLine(@""foo } bar"");
+}";
+ const string elseIfBranch = @" else if(int i = 0; i < 10; new Foo { Bar = ""baz"" }) {
+ Debug.WriteLine(@""bar } baz"");
+}";
+ const string elseBranch = @" else { Debug.WriteLine(@""bar } baz""); }";
+ const string document = ifStatement + elseIfBranch + elseBranch + elseIfBranch;
+ const string expected = ifStatement + elseIfBranch + elseBranch;
+
+ ParseBlockTest(document, new StatementBlock(Factory.Code(expected).AsStatement().Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void ParseBlockStopsParsingIfIfStatementNotFollowedByElse()
+ {
+ const string document = @"if(int i = 0; i < 10; new Foo { Bar = ""baz"" }) {
+ Debug.WriteLine(@""foo } bar"");
+}";
+
+ SingleSpanBlockTest(document, BlockType.Statement, SpanKind.Code);
+ }
+
+ [Fact]
+ public void ParseBlockAcceptsElseIfWithNoCondition()
+ {
+ // We don't want to be a full C# parser - If the else if is missing it's condition, the C# compiler can handle that, we have all the info we need to keep parsing
+ const string ifBranch = @"if(int i = 0; i < 10; new Foo { Bar = ""baz"" }) {
+ Debug.WriteLine(@""foo } bar"");
+}";
+ const string elseIfBranch = @" else if { foo(); }";
+ const string document = ifBranch + elseIfBranch;
+
+ SingleSpanBlockTest(document, BlockType.Statement, SpanKind.Code);
+ }
+
+ [Fact]
+ public void ParseBlockCorrectlyParsesDoWhileBlock()
+ {
+ SingleSpanBlockTest("do { var foo = bar; } while(foo != bar);", BlockType.Statement, SpanKind.Code, acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockCorrectlyParsesDoWhileBlockMissingSemicolon()
+ {
+ SingleSpanBlockTest("do { var foo = bar; } while(foo != bar)", BlockType.Statement, SpanKind.Code);
+ }
+
+ [Fact]
+ public void ParseBlockCorrectlyParsesDoWhileBlockMissingWhileCondition()
+ {
+ SingleSpanBlockTest("do { var foo = bar; } while", BlockType.Statement, SpanKind.Code);
+ }
+
+ [Fact]
+ public void ParseBlockCorrectlyParsesDoWhileBlockMissingWhileConditionWithSemicolon()
+ {
+ SingleSpanBlockTest("do { var foo = bar; } while;", BlockType.Statement, SpanKind.Code, acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockCorrectlyParsesDoWhileBlockMissingWhileClauseEntirely()
+ {
+ SingleSpanBlockTest("do { var foo = bar; } narf;", "do { var foo = bar; }", BlockType.Statement, SpanKind.Code);
+ }
+
+ [Fact]
+ public void ParseBlockSupportsBlockCommentBetweenDoAndWhileClause()
+ {
+ SingleSpanBlockTest("do { var foo = bar; } /* Foo */ /* Bar */ while(true);", BlockType.Statement, SpanKind.Code, acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockSupportsLineCommentBetweenDoAndWhileClause()
+ {
+ SingleSpanBlockTest(@"do { var foo = bar; }
+// Foo
+// Bar
+while(true);", BlockType.Statement, SpanKind.Code, acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockSupportsRazorCommentBetweenDoAndWhileClause()
+ {
+ RunRazorCommentBetweenClausesTest("do { var foo = bar; } ", " while(true);", acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockCorrectlyParsesMarkupInDoWhileBlock()
+ {
+ ParseBlockTest("@do { var foo = bar; <p>Foo</p> foo++; } while (foo<bar>);",
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("do { var foo = bar;").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" <p>Foo</p> ").Accepts(AcceptedCharacters.None)
+ ),
+ Factory.Code("foo++; } while (foo<bar>);").AsStatement().Accepts(AcceptedCharacters.None)
+ ));
+ }
+
+ [Fact]
+ public void ParseBlockSkipsParenthesisedExpressionAndThenBalancesBracesIfFirstIdentifierIsSwitchKeyword()
+ {
+ SingleSpanBlockTest(@"switch(foo) {
+ case 0:
+ break;
+ case 1:
+ {
+ break;
+ }
+ case 2:
+ return;
+ default:
+ return;
+}", BlockType.Statement, SpanKind.Code, acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockSkipsParenthesisedExpressionAndThenBalancesBracesIfFirstIdentifierIsLockKeyword()
+ {
+ SingleSpanBlockTest("lock(foo) { Debug.WriteLine(@\"foo } bar\"); }", BlockType.Statement, SpanKind.Code, acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockHasErrorsIfNamespaceImportMissingSemicolon()
+ {
+ NamespaceImportTest("using Foo.Bar.Baz", " Foo.Bar.Baz", acceptedCharacters: AcceptedCharacters.NonWhiteSpace | AcceptedCharacters.WhiteSpace, location: new SourceLocation(17, 0, 17));
+ }
+
+ [Fact]
+ public void ParseBlockHasErrorsIfNamespaceAliasMissingSemicolon()
+ {
+ NamespaceImportTest("using Foo.Bar.Baz = FooBarBaz", " Foo.Bar.Baz = FooBarBaz", acceptedCharacters: AcceptedCharacters.NonWhiteSpace | AcceptedCharacters.WhiteSpace, location: new SourceLocation(29, 0, 29));
+ }
+
+ [Fact]
+ public void ParseBlockParsesNamespaceImportWithSemicolonForUsingKeywordIfIsInValidFormat()
+ {
+ NamespaceImportTest("using Foo.Bar.Baz;", " Foo.Bar.Baz", AcceptedCharacters.NonWhiteSpace | AcceptedCharacters.WhiteSpace);
+ }
+
+ [Fact]
+ public void ParseBlockDoesntCaptureWhitespaceAfterUsing()
+ {
+ ParseBlockTest("using Foo ",
+ new DirectiveBlock(
+ Factory.Code("using Foo")
+ .AsNamespaceImport(" Foo", CSharpCodeParser.UsingKeywordLength)
+ .Accepts(AcceptedCharacters.NonWhiteSpace | AcceptedCharacters.WhiteSpace)));
+ }
+
+ [Fact]
+ public void ParseBlockParsesNamespaceAliasWithSemicolonForUsingKeywordIfIsInValidFormat()
+ {
+ NamespaceImportTest("using FooBarBaz = FooBarBaz;", " FooBarBaz = FooBarBaz", AcceptedCharacters.NonWhiteSpace | AcceptedCharacters.WhiteSpace);
+ }
+
+ [Fact]
+ public void ParseBlockTerminatesUsingKeywordAtEOFAndOutputsFileCodeBlock()
+ {
+ SingleSpanBlockTest("using ", BlockType.Statement, SpanKind.Code);
+ }
+
+ [Fact]
+ public void ParseBlockTerminatesSingleLineCommentAtEndOfFile()
+ {
+ const string document = "foreach(var f in Foo) { // foo bar baz";
+ SingleSpanBlockTest(document, document, BlockType.Statement, SpanKind.Code,
+ new RazorError(String.Format(RazorResources.ParseError_Expected_EndOfBlock_Before_EOF, "foreach", '}', '{'), SourceLocation.Zero));
+ }
+
+ [Fact]
+ public void ParseBlockTerminatesBlockCommentAtEndOfFile()
+ {
+ const string document = "foreach(var f in Foo) { /* foo bar baz";
+ SingleSpanBlockTest(document, document, BlockType.Statement, SpanKind.Code,
+ new RazorError(String.Format(RazorResources.ParseError_BlockComment_Not_Terminated), 24, 0, 24),
+ new RazorError(String.Format(RazorResources.ParseError_Expected_EndOfBlock_Before_EOF, "foreach", '}', '{'), SourceLocation.Zero));
+ }
+
+ [Fact]
+ public void ParseBlockTerminatesSingleSlashAtEndOfFile()
+ {
+ const string document = "foreach(var f in Foo) { / foo bar baz";
+ SingleSpanBlockTest(document, document, BlockType.Statement, SpanKind.Code,
+ new RazorError(String.Format(RazorResources.ParseError_Expected_EndOfBlock_Before_EOF, "foreach", '}', '{'), SourceLocation.Zero));
+ }
+
+ [Fact]
+ public void ParseBlockSupportsBlockCommentBetweenTryAndFinallyClause()
+ {
+ SingleSpanBlockTest("try { bar(); } /* Foo */ /* Bar */ finally { baz(); }", BlockType.Statement, SpanKind.Code, acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockSupportsRazorCommentBetweenTryAndFinallyClause()
+ {
+ RunRazorCommentBetweenClausesTest("try { bar(); } ", " finally { biz(); }", acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockSupportsBlockCommentBetweenCatchAndFinallyClause()
+ {
+ SingleSpanBlockTest("try { bar(); } catch(bar) { baz(); } /* Foo */ /* Bar */ finally { biz(); }", BlockType.Statement, SpanKind.Code, acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockSupportsRazorCommentBetweenCatchAndFinallyClause()
+ {
+ RunRazorCommentBetweenClausesTest("try { bar(); } catch(bar) { baz(); } ", " finally { biz(); }", acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockSupportsBlockCommentBetweenTryAndCatchClause()
+ {
+ SingleSpanBlockTest("try { bar(); } /* Foo */ /* Bar */ catch(bar) { baz(); }", BlockType.Statement, SpanKind.Code);
+ }
+
+ [Fact]
+ public void ParseBlockSupportsRazorCommentBetweenTryAndCatchClause()
+ {
+ RunRazorCommentBetweenClausesTest("try { bar(); }", " catch(bar) { baz(); }");
+ }
+
+ [Fact]
+ public void ParseBlockSupportsLineCommentBetweenTryAndFinallyClause()
+ {
+ SingleSpanBlockTest(@"try { bar(); }
+// Foo
+// Bar
+finally { baz(); }", BlockType.Statement, SpanKind.Code, acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockSupportsLineCommentBetweenCatchAndFinallyClause()
+ {
+ SingleSpanBlockTest(@"try { bar(); } catch(bar) { baz(); }
+// Foo
+// Bar
+finally { biz(); }", BlockType.Statement, SpanKind.Code, acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockSupportsLineCommentBetweenTryAndCatchClause()
+ {
+ SingleSpanBlockTest(@"try { bar(); }
+// Foo
+// Bar
+catch(bar) { baz(); }", BlockType.Statement, SpanKind.Code);
+ }
+
+ [Fact]
+ public void ParseBlockSupportsTryStatementWithNoAdditionalClauses()
+ {
+ SingleSpanBlockTest("try { var foo = new { } }", BlockType.Statement, SpanKind.Code);
+ }
+
+ [Fact]
+ public void ParseBlockSupportsMarkupWithinTryClause()
+ {
+ RunSimpleWrappedMarkupTest("try {", " <p>Foo</p> ", "}");
+ }
+
+ [Fact]
+ public void ParseBlockSupportsTryStatementWithOneCatchClause()
+ {
+ SingleSpanBlockTest("try { var foo = new { } } catch(Foo Bar Baz) { var foo = new { } }", BlockType.Statement, SpanKind.Code);
+ }
+
+ [Fact]
+ public void ParseBlockSupportsMarkupWithinCatchClause()
+ {
+ RunSimpleWrappedMarkupTest("try { var foo = new { } } catch(Foo Bar Baz) {", " <p>Foo</p> ", "}");
+ }
+
+ [Fact]
+ public void ParseBlockSupportsTryStatementWithMultipleCatchClause()
+ {
+ SingleSpanBlockTest("try { var foo = new { } } catch(Foo Bar Baz) { var foo = new { } } catch(Foo Bar Baz) { var foo = new { } } catch(Foo Bar Baz) { var foo = new { } }", BlockType.Statement, SpanKind.Code);
+ }
+
+ [Fact]
+ public void ParseBlockSupportsExceptionLessCatchClauses()
+ {
+ SingleSpanBlockTest("try { var foo = new { } } catch { var foo = new { } }", BlockType.Statement, SpanKind.Code);
+ }
+
+ [Fact]
+ public void ParseBlockSupportsMarkupWithinAdditionalCatchClauses()
+ {
+ RunSimpleWrappedMarkupTest("try { var foo = new { } } catch(Foo Bar Baz) { var foo = new { } } catch(Foo Bar Baz) { var foo = new { } } catch(Foo Bar Baz) {", " <p>Foo</p> ", "}");
+ }
+
+ [Fact]
+ public void ParseBlockSupportsTryStatementWithFinallyClause()
+ {
+ SingleSpanBlockTest("try { var foo = new { } } finally { var foo = new { } }", BlockType.Statement, SpanKind.Code, acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockSupportsMarkupWithinFinallyClause()
+ {
+ RunSimpleWrappedMarkupTest("try { var foo = new { } } finally {", " <p>Foo</p> ", "}", acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockStopsParsingCatchClausesAfterFinallyBlock()
+ {
+ string expectedContent = "try { var foo = new { } } finally { var foo = new { } }";
+ SingleSpanBlockTest(expectedContent + " catch(Foo Bar Baz) { }", expectedContent, BlockType.Statement, SpanKind.Code, acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockDoesNotAllowMultipleFinallyBlocks()
+ {
+ string expectedContent = "try { var foo = new { } } finally { var foo = new { } }";
+ SingleSpanBlockTest(expectedContent + " finally { }", expectedContent, BlockType.Statement, SpanKind.Code, acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockAcceptsTrailingDotIntoImplicitExpressionWhenEmbeddedInCode()
+ {
+ // Arrange
+ ParseBlockTest(@"if(foo) { @foo. }",
+ new StatementBlock(
+ Factory.Code("if(foo) { ").AsStatement(),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("foo.")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)
+ ),
+ Factory.Code(" }").AsStatement()
+ ));
+ }
+
+ [Fact]
+ public void ParseBlockParsesExpressionOnSwitchCharacterFollowedByOpenParen()
+ {
+ // Arrange
+ ParseBlockTest(@"if(foo) { @(foo + bar) }",
+ new StatementBlock(
+ Factory.Code("if(foo) { ").AsStatement(),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("(").Accepts(AcceptedCharacters.None),
+ Factory.Code("foo + bar").AsExpression(),
+ Factory.MetaCode(")").Accepts(AcceptedCharacters.None)
+ ),
+ Factory.Code(" }").AsStatement()
+ ));
+ }
+
+ [Fact]
+ public void ParseBlockParsesExpressionOnSwitchCharacterFollowedByIdentifierStart()
+ {
+ // Arrange
+ ParseBlockTest(@"if(foo) { @foo[4].bar() }",
+ new StatementBlock(
+ Factory.Code("if(foo) { ").AsStatement(),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("foo[4].bar()")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)
+ ),
+ Factory.Code(" }").AsStatement()
+ ));
+ }
+
+ [Fact]
+ public void ParseBlockTreatsDoubleAtSignAsEscapeSequenceIfAtStatementStart()
+ {
+ // Arrange
+ ParseBlockTest(@"if(foo) { @@class.Foo() }",
+ new StatementBlock(
+ Factory.Code("if(foo) { ").AsStatement(),
+ Factory.Code("@").Hidden(),
+ Factory.Code("@class.Foo() }").AsStatement()
+ ));
+ }
+
+ [Fact]
+ public void ParseBlockTreatsAtSignsAfterFirstPairAsPartOfCSharpStatement()
+ {
+ // Arrange
+ ParseBlockTest(@"if(foo) { @@@@class.Foo() }",
+ new StatementBlock(
+ Factory.Code("if(foo) { ").AsStatement(),
+ Factory.Code("@").Hidden(),
+ Factory.Code("@@@class.Foo() }").AsStatement()
+ ));
+ }
+
+ [Fact]
+ public void ParseBlockDoesNotParseMarkupStatementOrExpressionOnSwitchCharacterNotFollowedByOpenAngleOrColon()
+ {
+ // Arrange
+ ParseBlockTest("if(foo) { @\"Foo\".ToString(); }",
+ new StatementBlock(
+ Factory.Code("if(foo) { @\"Foo\".ToString(); }").AsStatement()));
+ }
+
+ [Fact]
+ public void ParsersCanNestRecursively()
+ {
+ // Arrange
+ ParseBlockTest(@"foreach(var c in db.Categories) {
+ <div>
+ <h1>@c.Name</h1>
+ <ul>
+ @foreach(var p in c.Products) {
+ <li><a href=""@Html.ActionUrl(""Products"", ""Detail"", new { id = p.Id })"">@p.Name</a></li>
+ }
+ </ul>
+ </div>
+ }",
+ new StatementBlock(
+ Factory.Code("foreach(var c in db.Categories) {\r\n").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" <div>\r\n <h1>"),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("c.Name")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ Factory.Markup("</h1>\r\n <ul>\r\n"),
+ new StatementBlock(
+ Factory.Code(@" ").AsStatement(),
+ Factory.CodeTransition(),
+ Factory.Code("foreach(var p in c.Products) {\r\n").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" <li><a"),
+ new MarkupBlock(new AttributeBlockCodeGenerator("href", new LocationTagged<string>(" href=\"", 193, 5, 30), new LocationTagged<string>("\"", 256, 5, 93)),
+ Factory.Markup(" href=\"").With(SpanCodeGenerator.Null),
+ new MarkupBlock(new DynamicAttributeBlockCodeGenerator(new LocationTagged<string>(String.Empty, 200, 5, 37), 200, 5, 37),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("Html.ActionUrl(\"Products\", \"Detail\", new { id = p.Id })")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace))),
+ Factory.Markup("\"").With(SpanCodeGenerator.Null)),
+ Factory.Markup(">"),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("p.Name")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ Factory.Markup("</a></li>\r\n").Accepts(AcceptedCharacters.None)),
+ Factory.Code(" }\r\n").AsStatement().Accepts(AcceptedCharacters.None)),
+ Factory.Markup(" </ul>\r\n </div>\r\n")
+ .Accepts(AcceptedCharacters.None)),
+ Factory.Code(" }").AsStatement().Accepts(AcceptedCharacters.None)));
+ }
+
+ private void RunRazorCommentBetweenClausesTest(string preComment, string postComment, AcceptedCharacters acceptedCharacters = AcceptedCharacters.Any)
+ {
+ ParseBlockTest(preComment + "@* Foo *@ @* Bar *@" + postComment,
+ new StatementBlock(
+ Factory.Code(preComment).AsStatement(),
+ new CommentBlock(
+ Factory.CodeTransition(CSharpSymbolType.RazorCommentTransition),
+ Factory.MetaCode("*", CSharpSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None),
+ Factory.Comment(" Foo ", CSharpSymbolType.RazorComment),
+ Factory.MetaCode("*", CSharpSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None),
+ Factory.CodeTransition(CSharpSymbolType.RazorCommentTransition)
+ ),
+ Factory.Code(" ").AsStatement(),
+ new CommentBlock(
+ Factory.CodeTransition(CSharpSymbolType.RazorCommentTransition),
+ Factory.MetaCode("*", CSharpSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None),
+ Factory.Comment(" Bar ", CSharpSymbolType.RazorComment),
+ Factory.MetaCode("*", CSharpSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None),
+ Factory.CodeTransition(CSharpSymbolType.RazorCommentTransition)
+ ),
+ Factory.Code(postComment).AsStatement().Accepts(acceptedCharacters)));
+ }
+
+ private void RunSimpleWrappedMarkupTest(string prefix, string markup, string suffix, AcceptedCharacters acceptedCharacters = AcceptedCharacters.Any)
+ {
+ ParseBlockTest(prefix + markup + suffix,
+ new StatementBlock(
+ Factory.Code(prefix).AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(markup).Accepts(AcceptedCharacters.None)
+ ),
+ Factory.Code(suffix).AsStatement().Accepts(acceptedCharacters)
+ ));
+ }
+
+ private void NamespaceImportTest(string content, string expectedNS, AcceptedCharacters acceptedCharacters = AcceptedCharacters.None, string errorMessage = null, SourceLocation? location = null)
+ {
+ var errors = new RazorError[0];
+ if (!String.IsNullOrEmpty(errorMessage) && location.HasValue)
+ {
+ errors = new RazorError[]
+ {
+ new RazorError(errorMessage, location.Value)
+ };
+ }
+ ParseBlockTest(content,
+ new DirectiveBlock(
+ Factory.Code(content)
+ .AsNamespaceImport(expectedNS, CSharpCodeParser.UsingKeywordLength)
+ .Accepts(acceptedCharacters)),
+ errors);
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/CSharp/CSharpDirectivesTest.cs b/test/System.Web.Razor.Test/Parser/CSharp/CSharpDirectivesTest.cs
new file mode 100644
index 00000000..00bdb167
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/CSharp/CSharpDirectivesTest.cs
@@ -0,0 +1,161 @@
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Test.Framework;
+using System.Web.Razor.Text;
+using Xunit;
+
+namespace System.Web.Razor.Test.Parser.CSharp
+{
+ public class CSharpDirectivesTest : CsHtmlCodeParserTestBase
+ {
+ [Fact]
+ public void InheritsDirective()
+ {
+ ParseBlockTest("@inherits System.Web.WebPages.WebPage",
+ new DirectiveBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode(SyntaxConstants.CSharp.InheritsKeyword + " ")
+ .Accepts(AcceptedCharacters.None),
+ Factory.Code("System.Web.WebPages.WebPage")
+ .AsBaseType("System.Web.WebPages.WebPage")));
+ }
+
+ [Fact]
+ public void InheritsDirectiveSupportsArrays()
+ {
+ ParseBlockTest("@inherits string[[]][]",
+ new DirectiveBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode(SyntaxConstants.CSharp.InheritsKeyword + " ")
+ .Accepts(AcceptedCharacters.None),
+ Factory.Code("string[[]][]")
+ .AsBaseType("string[[]][]")));
+ }
+
+ [Fact]
+ public void InheritsDirectiveSupportsNestedGenerics()
+ {
+ ParseBlockTest("@inherits System.Web.Mvc.WebViewPage<IEnumerable<MvcApplication2.Models.RegisterModel>>",
+ new DirectiveBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode(SyntaxConstants.CSharp.InheritsKeyword + " ")
+ .Accepts(AcceptedCharacters.None),
+ Factory.Code("System.Web.Mvc.WebViewPage<IEnumerable<MvcApplication2.Models.RegisterModel>>")
+ .AsBaseType("System.Web.Mvc.WebViewPage<IEnumerable<MvcApplication2.Models.RegisterModel>>")));
+ }
+
+ [Fact]
+ public void InheritsDirectiveSupportsTypeKeywords()
+ {
+ ParseBlockTest("@inherits string",
+ new DirectiveBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode(SyntaxConstants.CSharp.InheritsKeyword + " ")
+ .Accepts(AcceptedCharacters.None),
+ Factory.Code("string")
+ .AsBaseType("string")));
+ }
+
+ [Fact]
+ public void InheritsDirectiveSupportsVSTemplateTokens()
+ {
+ ParseBlockTest("@inherits $rootnamespace$.MyBase",
+ new DirectiveBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode(SyntaxConstants.CSharp.InheritsKeyword + " ")
+ .Accepts(AcceptedCharacters.None),
+ Factory.Code("$rootnamespace$.MyBase")
+ .AsBaseType("$rootnamespace$.MyBase")));
+ }
+
+ [Fact]
+ public void SessionStateDirectiveWorks()
+ {
+ ParseBlockTest("@sessionstate InProc",
+ new DirectiveBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode(SyntaxConstants.CSharp.SessionStateKeyword + " ")
+ .Accepts(AcceptedCharacters.None),
+ Factory.Code("InProc")
+ .AsRazorDirectiveAttribute("sessionstate", "InProc")
+ ));
+ }
+
+ [Fact]
+ public void SessionStateDirectiveParsesInvalidSessionValue()
+ {
+ ParseBlockTest("@sessionstate Blah",
+ new DirectiveBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode(SyntaxConstants.CSharp.SessionStateKeyword + " ")
+ .Accepts(AcceptedCharacters.None),
+ Factory.Code("Blah")
+ .AsRazorDirectiveAttribute("sessionstate", "Blah")
+ ));
+ }
+
+ [Fact]
+ public void FunctionsDirective()
+ {
+ ParseBlockTest("@functions { foo(); bar(); }",
+ new FunctionsBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode(SyntaxConstants.CSharp.FunctionsKeyword + " {")
+ .Accepts(AcceptedCharacters.None),
+ Factory.Code(" foo(); bar(); ")
+ .AsFunctionsBody(),
+ Factory.MetaCode("}")
+ .Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void EmptyFunctionsDirective()
+ {
+ ParseBlockTest("@functions { }",
+ new FunctionsBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode(SyntaxConstants.CSharp.FunctionsKeyword + " {")
+ .Accepts(AcceptedCharacters.None),
+ Factory.Code(" ")
+ .AsFunctionsBody(),
+ Factory.MetaCode("}")
+ .Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void SectionDirective()
+ {
+ ParseBlockTest("@section Header { <p>F{o}o</p> }",
+ new SectionBlock(new SectionCodeGenerator("Header"),
+ Factory.CodeTransition(),
+ Factory.MetaCode("section Header {")
+ .AutoCompleteWith(null, atEndOfSpan: true)
+ .Accepts(AcceptedCharacters.Any),
+ new MarkupBlock(
+ Factory.Markup(" <p>F", "{", "o", "}", "o", "</p> ")),
+ Factory.MetaCode("}")
+ .Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void HelperDirective()
+ {
+ ParseBlockTest("@helper Strong(string value) { foo(); }",
+ new HelperBlock(new HelperCodeGenerator(new LocationTagged<string>("Strong(string value) {", new SourceLocation(8, 0, 8)), headerComplete: true),
+ Factory.CodeTransition(),
+ Factory.MetaCode("helper ")
+ .Accepts(AcceptedCharacters.None),
+ Factory.Code("Strong(string value) {")
+ .Hidden()
+ .Accepts(AcceptedCharacters.None),
+ new StatementBlock(
+ Factory.Code(" foo(); ")
+ .AsStatement()
+ .With(new StatementCodeGenerator())),
+ Factory.Code("}")
+ .Hidden()
+ .Accepts(AcceptedCharacters.None)));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/CSharp/CSharpErrorTest.cs b/test/System.Web.Razor.Test/Parser/CSharp/CSharpErrorTest.cs
new file mode 100644
index 00000000..37fd73b6
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/CSharp/CSharpErrorTest.cs
@@ -0,0 +1,604 @@
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Test.Framework;
+using System.Web.Razor.Text;
+using Xunit;
+
+namespace System.Web.Razor.Test.Parser.CSharp
+{
+ public class CSharpErrorTest : CsHtmlCodeParserTestBase
+ {
+ [Fact]
+ public void ParseBlockHandlesQuotesAfterTransition()
+ {
+ ParseBlockTest("@\"",
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.EmptyCSharp()
+ .AsImplicitExpression(KeywordSet)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)
+ ),
+ new RazorError(
+ String.Format(RazorResources.ParseError_Unexpected_Character_At_Start_Of_CodeBlock_CS, '"'),
+ 1, 0, 1));
+ }
+
+ [Fact]
+ public void ParseBlockCapturesWhitespaceToEndOfLineInInvalidUsingStatementAndTreatsAsFileCode()
+ {
+ ParseBlockTest(@"using
+
+",
+ new StatementBlock(
+ Factory.Code("using \r\n").AsStatement()
+ ));
+ }
+
+ [Fact]
+ public void ParseBlockMethodOutputsOpenCurlyAsCodeSpanIfEofFoundAfterOpenCurlyBrace()
+ {
+ ParseBlockTest("{",
+ new StatementBlock(
+ Factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ Factory.EmptyCSharp()
+ .AsStatement()
+ .With(new AutoCompleteEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString) { AutoCompleteString = "}" })
+ ),
+ new RazorError(
+ String.Format(RazorResources.ParseError_Expected_EndOfBlock_Before_EOF,
+ RazorResources.BlockName_Code,
+ "}", "{"),
+ SourceLocation.Zero));
+ }
+
+ [Fact]
+ public void ParseBlockMethodOutputsZeroLengthCodeSpanIfStatementBlockEmpty()
+ {
+ ParseBlockTest("{}",
+ new StatementBlock(
+ Factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ Factory.EmptyCSharp().AsStatement(),
+ Factory.MetaCode("}").Accepts(AcceptedCharacters.None)
+ ));
+ }
+
+ [Fact]
+ public void ParseBlockMethodProducesErrorIfNewlineFollowsTransition()
+ {
+ ParseBlockTest(@"@
+",
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.EmptyCSharp()
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ new RazorError(RazorResources.ParseError_Unexpected_WhiteSpace_At_Start_Of_CodeBlock_CS, new SourceLocation(1, 0, 1)));
+ }
+
+ [Fact]
+ public void ParseBlockMethodProducesErrorIfWhitespaceBetweenTransitionAndBlockStartInEmbeddedExpression()
+ {
+ ParseBlockTest(@"{
+ @ {}
+}",
+ new StatementBlock(
+ Factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ Factory.Code("\r\n ").AsStatement(),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.EmptyCSharp()
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ Factory.Code(" {}\r\n").AsStatement(),
+ Factory.MetaCode("}").Accepts(AcceptedCharacters.None)
+ ),
+ new RazorError(RazorResources.ParseError_Unexpected_WhiteSpace_At_Start_Of_CodeBlock_CS, 8, 1, 5));
+ }
+
+ [Fact]
+ public void ParseBlockMethodProducesErrorIfEOFAfterTransitionInEmbeddedExpression()
+ {
+ ParseBlockTest(@"{
+ @",
+ new StatementBlock(
+ Factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ Factory.Code("\r\n ").AsStatement(),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.EmptyCSharp()
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ Factory.EmptyCSharp().AsStatement()
+ ),
+ new RazorError(RazorResources.ParseError_Unexpected_EndOfFile_At_Start_Of_CodeBlock, 8, 1, 5),
+ new RazorError(
+ String.Format(RazorResources.ParseError_Expected_EndOfBlock_Before_EOF, RazorResources.BlockName_Code, "}", "{"),
+ SourceLocation.Zero));
+ }
+
+ [Fact]
+ public void ParseBlockMethodParsesNothingIfFirstCharacterIsNotIdentifierStartOrParenOrBrace()
+ {
+ ParseBlockTest("@!!!",
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.EmptyCSharp()
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ new RazorError(
+ String.Format(RazorResources.ParseError_Unexpected_Character_At_Start_Of_CodeBlock_CS, "!"),
+ 1, 0, 1));
+ }
+
+ [Fact]
+ public void ParseBlockShouldReportErrorAndTerminateAtEOFIfIfParenInExplicitExpressionUnclosed()
+ {
+ ParseBlockTest(@"(foo bar
+baz",
+ new ExpressionBlock(
+ Factory.MetaCode("(").Accepts(AcceptedCharacters.None),
+ Factory.Code("foo bar\r\nbaz").AsExpression()
+ ),
+ new RazorError(
+ String.Format(RazorResources.ParseError_Expected_EndOfBlock_Before_EOF,
+ RazorResources.BlockName_ExplicitExpression, ')', '('),
+ new SourceLocation(0, 0, 0)));
+ }
+
+ [Fact]
+ public void ParseBlockShouldReportErrorAndTerminateAtMarkupIfIfParenInExplicitExpressionUnclosed()
+ {
+ ParseBlockTest(@"(foo bar
+<html>
+baz
+</html",
+ new ExpressionBlock(
+ Factory.MetaCode("(").Accepts(AcceptedCharacters.None),
+ Factory.Code("foo bar\r\n").AsExpression()
+ ),
+ new RazorError(
+ String.Format(RazorResources.ParseError_Expected_EndOfBlock_Before_EOF,
+ RazorResources.BlockName_ExplicitExpression, ')', '('),
+ new SourceLocation(0, 0, 0)));
+ }
+
+ [Fact]
+ public void ParseBlockCorrectlyHandlesInCorrectTransitionsIfImplicitExpressionParensUnclosed()
+ {
+ ParseBlockTest(@"Href(
+<h1>@Html.Foo(Bar);</h1>
+",
+ new ExpressionBlock(
+ Factory.Code("Href(\r\n")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ ),
+ new RazorError(
+ String.Format(RazorResources.ParseError_Expected_CloseBracket_Before_EOF,
+ "(", ")"),
+ new SourceLocation(4, 0, 4)));
+ }
+
+ [Fact]
+ // Test for fix to Dev10 884975 - Incorrect Error Messaging
+ public void ParseBlockShouldReportErrorAndTerminateAtEOFIfParenInImplicitExpressionUnclosed()
+ {
+ ParseBlockTest(@"Foo(Bar(Baz)
+Biz
+Boz",
+ new ExpressionBlock(
+ Factory.Code("Foo(Bar(Baz)\r\nBiz\r\nBoz")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ ),
+ new RazorError(String.Format(RazorResources.ParseError_Expected_CloseBracket_Before_EOF,
+ "(", ")"),
+ new SourceLocation(3, 0, 3)));
+ }
+
+ [Fact]
+ // Test for fix to Dev10 884975 - Incorrect Error Messaging
+ public void ParseBlockShouldReportErrorAndTerminateAtMarkupIfParenInImplicitExpressionUnclosed()
+ {
+ ParseBlockTest(@"Foo(Bar(Baz)
+Biz
+<html>
+Boz
+</html>",
+ new ExpressionBlock(
+ Factory.Code("Foo(Bar(Baz)\r\nBiz\r\n")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ ),
+ new RazorError(String.Format(RazorResources.ParseError_Expected_CloseBracket_Before_EOF,
+ "(", ")"),
+ new SourceLocation(3, 0, 3)));
+ }
+
+ [Fact]
+ // Test for fix to Dev10 884975 - Incorrect Error Messaging
+ public void ParseBlockShouldReportErrorAndTerminateAtEOFIfBracketInImplicitExpressionUnclosed()
+ {
+ ParseBlockTest(@"Foo[Bar[Baz]
+Biz
+Boz",
+ new ExpressionBlock(
+ Factory.Code("Foo[Bar[Baz]\r\nBiz\r\nBoz")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ ),
+ new RazorError(
+ String.Format(RazorResources.ParseError_Expected_CloseBracket_Before_EOF,
+ "[", "]"),
+ new SourceLocation(3, 0, 3)));
+ }
+
+ [Fact]
+ // Test for fix to Dev10 884975 - Incorrect Error Messaging
+ public void ParseBlockShouldReportErrorAndTerminateAtMarkupIfBracketInImplicitExpressionUnclosed()
+ {
+ ParseBlockTest(@"Foo[Bar[Baz]
+Biz
+<b>
+Boz
+</b>",
+ new ExpressionBlock(
+ Factory.Code("Foo[Bar[Baz]\r\nBiz\r\n")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ ),
+ new RazorError(
+ String.Format(RazorResources.ParseError_Expected_CloseBracket_Before_EOF,
+ "[", "]"),
+ new SourceLocation(3, 0, 3)));
+ }
+
+ // Simple EOF handling errors:
+ [Fact]
+ public void ParseBlockReportsErrorIfExplicitCodeBlockUnterminatedAtEOF()
+ {
+ ParseBlockTest("{ var foo = bar; if(foo != null) { bar(); } ",
+ new StatementBlock(
+ Factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ Factory.Code(" var foo = bar; if(foo != null) { bar(); } ").AsStatement()
+ ),
+ new RazorError(
+ String.Format(RazorResources.ParseError_Expected_EndOfBlock_Before_EOF,
+ RazorResources.BlockName_Code, '}', '{'),
+ SourceLocation.Zero));
+ }
+
+ [Fact]
+ public void ParseBlockReportsErrorIfClassBlockUnterminatedAtEOF()
+ {
+ ParseBlockTest("functions { var foo = bar; if(foo != null) { bar(); } ",
+ new FunctionsBlock(
+ Factory.MetaCode("functions {").Accepts(AcceptedCharacters.None),
+ Factory.Code(" var foo = bar; if(foo != null) { bar(); } ").AsFunctionsBody()
+ ),
+ new RazorError(
+ String.Format(RazorResources.ParseError_Expected_EndOfBlock_Before_EOF,
+ "functions", '}', '{'),
+ SourceLocation.Zero));
+ }
+
+ [Fact]
+ public void ParseBlockReportsErrorIfIfBlockUnterminatedAtEOF()
+ {
+ RunUnterminatedSimpleKeywordBlock("if");
+ }
+
+ [Fact]
+ public void ParseBlockReportsErrorIfElseBlockUnterminatedAtEOF()
+ {
+ ParseBlockTest("if(foo) { baz(); } else { var foo = bar; if(foo != null) { bar(); } ",
+ new StatementBlock(
+ Factory.Code("if(foo) { baz(); } else { var foo = bar; if(foo != null) { bar(); } ").AsStatement()
+ ),
+ new RazorError(
+ String.Format(RazorResources.ParseError_Expected_EndOfBlock_Before_EOF,
+ "else", '}', '{'),
+ new SourceLocation(19, 0, 19)));
+ }
+
+ [Fact]
+ public void ParseBlockReportsErrorIfElseIfBlockUnterminatedAtEOF()
+ {
+ ParseBlockTest("if(foo) { baz(); } else if { var foo = bar; if(foo != null) { bar(); } ",
+ new StatementBlock(
+ Factory.Code("if(foo) { baz(); } else if { var foo = bar; if(foo != null) { bar(); } ").AsStatement()
+ ),
+ new RazorError(
+ String.Format(RazorResources.ParseError_Expected_EndOfBlock_Before_EOF,
+ "else if", '}', '{'),
+ new SourceLocation(19, 0, 19)));
+ }
+
+ [Fact]
+ public void ParseBlockReportsErrorIfDoBlockUnterminatedAtEOF()
+ {
+ ParseBlockTest("do { var foo = bar; if(foo != null) { bar(); } ",
+ new StatementBlock(
+ Factory.Code("do { var foo = bar; if(foo != null) { bar(); } ").AsStatement()
+ ),
+ new RazorError(
+ String.Format(RazorResources.ParseError_Expected_EndOfBlock_Before_EOF,
+ "do", '}', '{'),
+ SourceLocation.Zero));
+ }
+
+ [Fact]
+ public void ParseBlockReportsErrorIfTryBlockUnterminatedAtEOF()
+ {
+ ParseBlockTest("try { var foo = bar; if(foo != null) { bar(); } ",
+ new StatementBlock(
+ Factory.Code("try { var foo = bar; if(foo != null) { bar(); } ").AsStatement()
+ ),
+ new RazorError(
+ String.Format(RazorResources.ParseError_Expected_EndOfBlock_Before_EOF,
+ "try", '}', '{'),
+ SourceLocation.Zero));
+ }
+
+ [Fact]
+ public void ParseBlockReportsErrorIfCatchBlockUnterminatedAtEOF()
+ {
+ ParseBlockTest("try { baz(); } catch(Foo) { var foo = bar; if(foo != null) { bar(); } ",
+ new StatementBlock(
+ Factory.Code("try { baz(); } catch(Foo) { var foo = bar; if(foo != null) { bar(); } ").AsStatement()
+ ),
+ new RazorError(
+ String.Format(RazorResources.ParseError_Expected_EndOfBlock_Before_EOF,
+ "catch", '}', '{'),
+ new SourceLocation(15, 0, 15)));
+ }
+
+ [Fact]
+ public void ParseBlockReportsErrorIfFinallyBlockUnterminatedAtEOF()
+ {
+ ParseBlockTest("try { baz(); } finally { var foo = bar; if(foo != null) { bar(); } ",
+ new StatementBlock(
+ Factory.Code("try { baz(); } finally { var foo = bar; if(foo != null) { bar(); } ").AsStatement()
+ ),
+ new RazorError(
+ String.Format(RazorResources.ParseError_Expected_EndOfBlock_Before_EOF,
+ "finally", '}', '{'),
+ new SourceLocation(15, 0, 15)));
+ }
+
+ [Fact]
+ public void ParseBlockReportsErrorIfForBlockUnterminatedAtEOF()
+ {
+ RunUnterminatedSimpleKeywordBlock("for");
+ }
+
+ [Fact]
+ public void ParseBlockReportsErrorIfForeachBlockUnterminatedAtEOF()
+ {
+ RunUnterminatedSimpleKeywordBlock("foreach");
+ }
+
+ [Fact]
+ public void ParseBlockReportsErrorIfWhileBlockUnterminatedAtEOF()
+ {
+ RunUnterminatedSimpleKeywordBlock("while");
+ }
+
+ [Fact]
+ public void ParseBlockReportsErrorIfSwitchBlockUnterminatedAtEOF()
+ {
+ RunUnterminatedSimpleKeywordBlock("switch");
+ }
+
+ [Fact]
+ public void ParseBlockReportsErrorIfLockBlockUnterminatedAtEOF()
+ {
+ RunUnterminatedSimpleKeywordBlock("lock");
+ }
+
+ [Fact]
+ public void ParseBlockReportsErrorIfUsingBlockUnterminatedAtEOF()
+ {
+ RunUnterminatedSimpleKeywordBlock("using");
+ }
+
+ [Fact]
+ public void ParseBlockRequiresControlFlowStatementsToHaveBraces()
+ {
+ string expectedMessage = String.Format(RazorResources.ParseError_SingleLine_ControlFlowStatements_Not_Allowed, "{", "<");
+ ParseBlockTest("if(foo) <p>Bar</p> else if(bar) <p>Baz</p> else <p>Boz</p>",
+ new StatementBlock(
+ Factory.Code("if(foo) ").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup("<p>Bar</p> ").Accepts(AcceptedCharacters.None)
+ ),
+ Factory.Code("else if(bar) ").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup("<p>Baz</p> ").Accepts(AcceptedCharacters.None)
+ ),
+ Factory.Code("else ").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup("<p>Boz</p>").Accepts(AcceptedCharacters.None)
+ ),
+ Factory.EmptyCSharp().AsStatement()
+ ),
+ new RazorError(expectedMessage, 8, 0, 8),
+ new RazorError(expectedMessage, 32, 0, 32),
+ new RazorError(expectedMessage, 48, 0, 48));
+ }
+
+ [Fact]
+ public void ParseBlockIncludesUnexpectedCharacterInSingleStatementControlFlowStatementError()
+ {
+ ParseBlockTest("if(foo)) { var bar = foo; }",
+ new StatementBlock(
+ Factory.Code("if(foo)) { var bar = foo; }").AsStatement()
+ ),
+ new RazorError(
+ String.Format(RazorResources.ParseError_SingleLine_ControlFlowStatements_Not_Allowed,
+ "{", ")"),
+ new SourceLocation(7, 0, 7)));
+ }
+
+ [Fact]
+ public void ParseBlockOutputsErrorIfAtSignFollowedByLessThanSignAtStatementStart()
+ {
+ ParseBlockTest("if(foo) { @<p>Bar</p> }",
+ new StatementBlock(
+ Factory.Code("if(foo) {").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" "),
+ Factory.MarkupTransition(),
+ Factory.Markup("<p>Bar</p> ").Accepts(AcceptedCharacters.None)
+ ),
+ Factory.Code("}").AsStatement()
+ ),
+ new RazorError(
+ RazorResources.ParseError_AtInCode_Must_Be_Followed_By_Colon_Paren_Or_Identifier_Start,
+ 10, 0, 10));
+ }
+
+ [Fact]
+ public void ParseBlockTerminatesIfBlockAtEOLWhenRecoveringFromMissingCloseParen()
+ {
+ ParseBlockTest(@"if(foo bar
+baz",
+ new StatementBlock(
+ Factory.Code("if(foo bar\r\n").AsStatement()
+ ),
+ new RazorError(
+ String.Format(RazorResources.ParseError_Expected_CloseBracket_Before_EOF,
+ "(", ")"),
+ new SourceLocation(2, 0, 2)));
+ }
+
+ [Fact]
+ public void ParseBlockTerminatesForeachBlockAtEOLWhenRecoveringFromMissingCloseParen()
+ {
+ ParseBlockTest(@"foreach(foo bar
+baz",
+ new StatementBlock(
+ Factory.Code("foreach(foo bar\r\n").AsStatement()
+ ),
+ new RazorError(
+ String.Format(RazorResources.ParseError_Expected_CloseBracket_Before_EOF,
+ "(", ")"),
+ new SourceLocation(7, 0, 7)));
+ }
+
+ [Fact]
+ public void ParseBlockTerminatesWhileClauseInDoStatementAtEOLWhenRecoveringFromMissingCloseParen()
+ {
+ ParseBlockTest(@"do { } while(foo bar
+baz",
+ new StatementBlock(
+ Factory.Code("do { } while(foo bar\r\n").AsStatement()
+ ),
+ new RazorError(
+ String.Format(RazorResources.ParseError_Expected_CloseBracket_Before_EOF,
+ "(", ")"),
+ new SourceLocation(12, 0, 12)));
+ }
+
+ [Fact]
+ public void ParseBlockTerminatesUsingBlockAtEOLWhenRecoveringFromMissingCloseParen()
+ {
+ ParseBlockTest(@"using(foo bar
+baz",
+ new StatementBlock(
+ Factory.Code("using(foo bar\r\n").AsStatement()
+ ),
+ new RazorError(
+ String.Format(RazorResources.ParseError_Expected_CloseBracket_Before_EOF,
+ "(", ")"),
+ new SourceLocation(5, 0, 5)));
+ }
+
+ [Fact]
+ public void ParseBlockResumesIfStatementAfterOpenParen()
+ {
+ ParseBlockTest(@"if(
+else { <p>Foo</p> }",
+ new StatementBlock(
+ Factory.Code("if(\r\nelse {").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" <p>Foo</p> ").Accepts(AcceptedCharacters.None)
+ ),
+ Factory.Code("}").AsStatement().Accepts(AcceptedCharacters.None)
+ ),
+ new RazorError(
+ String.Format(RazorResources.ParseError_Expected_CloseBracket_Before_EOF,
+ "(", ")"),
+ new SourceLocation(2, 0, 2)));
+ }
+
+ [Fact]
+ public void ParseBlockTerminatesNormalCSharpStringsAtEOLIfEndQuoteMissing()
+ {
+ SingleSpanBlockTest(@"if(foo) {
+ var p = ""foo bar baz
+;
+}",
+ BlockType.Statement, SpanKind.Code,
+ new RazorError(RazorResources.ParseError_Unterminated_String_Literal, 23, 1, 12));
+ }
+
+ [Fact]
+ public void ParseBlockTerminatesNormalStringAtEndOfFile()
+ {
+ SingleSpanBlockTest("if(foo) { var foo = \"blah blah blah blah blah", BlockType.Statement, SpanKind.Code,
+ new RazorError(RazorResources.ParseError_Unterminated_String_Literal, 20, 0, 20),
+ new RazorError(String.Format(RazorResources.ParseError_Expected_EndOfBlock_Before_EOF, "if", '}', '{'), SourceLocation.Zero));
+ }
+
+ [Fact]
+ public void ParseBlockTerminatesVerbatimStringAtEndOfFile()
+ {
+ SingleSpanBlockTest(@"if(foo) { var foo = @""blah
+blah;
+<p>Foo</p>
+blah
+blah",
+ BlockType.Statement, SpanKind.Code,
+ new RazorError(RazorResources.ParseError_Unterminated_String_Literal, 20, 0, 20),
+ new RazorError(String.Format(RazorResources.ParseError_Expected_EndOfBlock_Before_EOF, "if", '}', '{'), SourceLocation.Zero));
+ }
+
+ [Fact]
+ public void ParseBlockCorrectlyParsesMarkupIncorrectyAssumedToBeWithinAStatement()
+ {
+ ParseBlockTest(@"if(foo) {
+ var foo = ""foo bar baz
+ <p>Foo is @foo</p>
+}",
+ new StatementBlock(
+ Factory.Code("if(foo) {\r\n var foo = \"foo bar baz\r\n ").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup("<p>Foo is "),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("foo")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ Factory.Markup("</p>\r\n").Accepts(AcceptedCharacters.None)),
+ Factory.Code("}").AsStatement()
+ ),
+ new RazorError(
+ RazorResources.ParseError_Unterminated_String_Literal,
+ 25, 1, 14));
+ }
+
+ [Fact]
+ public void ParseBlockCorrectlyParsesAtSignInDelimitedBlock()
+ {
+ ParseBlockTest("(Request[\"description\"] ?? @photo.Description)",
+ new ExpressionBlock(
+ Factory.MetaCode("(").Accepts(AcceptedCharacters.None),
+ Factory.Code("Request[\"description\"] ?? @photo.Description").AsExpression(),
+ Factory.MetaCode(")").Accepts(AcceptedCharacters.None)
+ ));
+ }
+
+ private void RunUnterminatedSimpleKeywordBlock(string keyword)
+ {
+ SingleSpanBlockTest(keyword + " (foo) { var foo = bar; if(foo != null) { bar(); } ", BlockType.Statement, SpanKind.Code,
+ new RazorError(String.Format(RazorResources.ParseError_Expected_EndOfBlock_Before_EOF, keyword, '}', '{'), SourceLocation.Zero));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/CSharp/CSharpExplicitExpressionTest.cs b/test/System.Web.Razor.Test/Parser/CSharp/CSharpExplicitExpressionTest.cs
new file mode 100644
index 00000000..24156f30
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/CSharp/CSharpExplicitExpressionTest.cs
@@ -0,0 +1,139 @@
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Test.Framework;
+using System.Web.Razor.Text;
+using Xunit;
+
+namespace System.Web.Razor.Test.Parser.CSharp
+{
+ public class CSharpExplicitExpressionTest : CsHtmlCodeParserTestBase
+ {
+ [Fact]
+ public void ParseBlockShouldOutputZeroLengthCodeSpanIfExplicitExpressionIsEmpty()
+ {
+ ParseBlockTest("@()",
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("(").Accepts(AcceptedCharacters.None),
+ Factory.EmptyCSharp().AsExpression(),
+ Factory.MetaCode(")").Accepts(AcceptedCharacters.None)
+ ));
+ }
+
+ [Fact]
+ public void ParseBlockShouldOutputZeroLengthCodeSpanIfEOFOccursAfterStartOfExplicitExpression()
+ {
+ ParseBlockTest("@(",
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("(").Accepts(AcceptedCharacters.None),
+ Factory.EmptyCSharp().AsExpression()
+ ),
+ new RazorError(
+ String.Format(RazorResources.ParseError_Expected_EndOfBlock_Before_EOF,
+ RazorResources.BlockName_ExplicitExpression,
+ ")", "("),
+ new SourceLocation(1, 0, 1)));
+ }
+
+ [Fact]
+ public void ParseBlockShouldAcceptEscapedQuoteInNonVerbatimStrings()
+ {
+ ParseBlockTest("@(\"\\\"\")",
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("(").Accepts(AcceptedCharacters.None),
+ Factory.Code("\"\\\"\"").AsExpression(),
+ Factory.MetaCode(")").Accepts(AcceptedCharacters.None)
+ ));
+ }
+
+ [Fact]
+ public void ParseBlockShouldAcceptEscapedQuoteInVerbatimStrings()
+ {
+ ParseBlockTest("@(@\"\"\"\")",
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("(").Accepts(AcceptedCharacters.None),
+ Factory.Code("@\"\"\"\"").AsExpression(),
+ Factory.MetaCode(")").Accepts(AcceptedCharacters.None)
+ ));
+ }
+
+ [Fact]
+ public void ParseBlockShouldAcceptMultipleRepeatedEscapedQuoteInVerbatimStrings()
+ {
+ ParseBlockTest("@(@\"\"\"\"\"\")",
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("(").Accepts(AcceptedCharacters.None),
+ Factory.Code("@\"\"\"\"\"\"").AsExpression(),
+ Factory.MetaCode(")").Accepts(AcceptedCharacters.None)
+ ));
+ }
+
+ [Fact]
+ public void ParseBlockShouldAcceptMultiLineVerbatimStrings()
+ {
+ ParseBlockTest(@"@(@""
+Foo
+Bar
+Baz
+"")",
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("(").Accepts(AcceptedCharacters.None),
+ Factory.Code("@\"\r\nFoo\r\nBar\r\nBaz\r\n\"").AsExpression(),
+ Factory.MetaCode(")").Accepts(AcceptedCharacters.None)
+ ));
+ }
+
+ [Fact]
+ public void ParseBlockShouldAcceptMultipleEscapedQuotesInNonVerbatimStrings()
+ {
+ ParseBlockTest("@(\"\\\"hello, world\\\"\")",
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("(").Accepts(AcceptedCharacters.None),
+ Factory.Code("\"\\\"hello, world\\\"\"").AsExpression(),
+ Factory.MetaCode(")").Accepts(AcceptedCharacters.None)
+ ));
+ }
+
+ [Fact]
+ public void ParseBlockShouldAcceptMultipleEscapedQuotesInVerbatimStrings()
+ {
+ ParseBlockTest("@(@\"\"\"hello, world\"\"\")",
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("(").Accepts(AcceptedCharacters.None),
+ Factory.Code("@\"\"\"hello, world\"\"\"").AsExpression(),
+ Factory.MetaCode(")").Accepts(AcceptedCharacters.None)
+ ));
+ }
+
+ [Fact]
+ public void ParseBlockShouldAcceptConsecutiveEscapedQuotesInNonVerbatimStrings()
+ {
+ ParseBlockTest("@(\"\\\"\\\"\")",
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("(").Accepts(AcceptedCharacters.None),
+ Factory.Code("\"\\\"\\\"\"").AsExpression(),
+ Factory.MetaCode(")").Accepts(AcceptedCharacters.None)
+ ));
+ }
+
+ [Fact]
+ public void ParseBlockShouldAcceptConsecutiveEscapedQuotesInVerbatimStrings()
+ {
+ ParseBlockTest("@(@\"\"\"\"\"\")",
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("(").Accepts(AcceptedCharacters.None),
+ Factory.Code("@\"\"\"\"\"\"").AsExpression(),
+ Factory.MetaCode(")").Accepts(AcceptedCharacters.None)
+ ));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/CSharp/CSharpHelperTest.cs b/test/System.Web.Razor.Test/Parser/CSharp/CSharpHelperTest.cs
new file mode 100644
index 00000000..0bb99afa
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/CSharp/CSharpHelperTest.cs
@@ -0,0 +1,345 @@
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Test.Framework;
+using System.Web.Razor.Text;
+using Xunit;
+
+namespace System.Web.Razor.Test.Parser.CSharp
+{
+ public class CSharpHelperTest : CsHtmlMarkupParserTestBase
+ {
+ [Fact]
+ public void ParseHelperCorrectlyParsesHelperWithNoSpaceInBody()
+ {
+ ParseDocumentTest("@helper Foo(){@Bar()}",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new HelperBlock(new HelperCodeGenerator(new LocationTagged<string>("Foo(){", 8, 0, 8), headerComplete: true),
+ Factory.CodeTransition(),
+ Factory.MetaCode("helper ").Accepts(AcceptedCharacters.None),
+ Factory.Code("Foo(){").Hidden().Accepts(AcceptedCharacters.None),
+ new StatementBlock(
+ Factory.EmptyCSharp().AsStatement(),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("Bar()")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ Factory.EmptyCSharp().AsStatement()),
+ Factory.Code("}").Hidden().Accepts(AcceptedCharacters.None)),
+ Factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void ParseHelperCorrectlyParsesIncompleteHelperPreceedingCodeBlock()
+ {
+ ParseDocumentTest(@"@helper
+@{}",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new HelperBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("helper")),
+ Factory.Markup("\r\n"),
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ Factory.EmptyCSharp().AsStatement(),
+ Factory.MetaCode("}").Accepts(AcceptedCharacters.None)),
+ Factory.EmptyHtml()),
+ new RazorError(
+ String.Format(
+ RazorResources.ParseError_Unexpected_Character_At_Helper_Name_Start,
+ RazorResources.ErrorComponent_Newline),
+ 7, 0, 7));
+ }
+
+ [Fact]
+ public void ParseHelperRequiresSpaceBeforeSignature()
+ {
+ ParseDocumentTest("@helper{",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new HelperBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("helper")),
+ Factory.Markup("{")),
+ new RazorError(
+ String.Format(
+ RazorResources.ParseError_Unexpected_Character_At_Helper_Name_Start,
+ String.Format(RazorResources.ErrorComponent_Character, "{")),
+ 7, 0, 7));
+ }
+
+ [Fact]
+ public void ParseHelperOutputsErrorButContinuesIfLParenFoundAfterHelperKeyword()
+ {
+ ParseDocumentTest("@helper () {",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new HelperBlock(new HelperCodeGenerator(new LocationTagged<string>("() {", 8, 0, 8), headerComplete: true),
+ Factory.CodeTransition(),
+ Factory.MetaCode("helper ").Accepts(AcceptedCharacters.None),
+ Factory.Code("() {").Hidden().Accepts(AcceptedCharacters.None),
+ new StatementBlock(
+ Factory.EmptyCSharp()
+ .AsStatement()
+ .AutoCompleteWith("}")))),
+ new RazorError(
+ String.Format(
+ RazorResources.ParseError_Unexpected_Character_At_Helper_Name_Start,
+ String.Format(RazorResources.ErrorComponent_Character, "(")),
+ 8, 0, 8),
+ new RazorError(
+ String.Format(
+ RazorResources.ParseError_Expected_EndOfBlock_Before_EOF,
+ "helper", "}", "{"),
+ 1, 0, 1));
+ }
+
+ [Fact]
+ public void ParseHelperStatementOutputsMarkerHelperHeaderSpanOnceKeywordComplete()
+ {
+ ParseDocumentTest("@helper ",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new HelperBlock(new HelperCodeGenerator(new LocationTagged<string>(String.Empty, 8, 0, 8), headerComplete: false),
+ Factory.CodeTransition(),
+ Factory.MetaCode("helper ").Accepts(AcceptedCharacters.None),
+ Factory.EmptyCSharp().Hidden())),
+ new RazorError(
+ String.Format(
+ RazorResources.ParseError_Unexpected_Character_At_Helper_Name_Start,
+ RazorResources.ErrorComponent_EndOfFile),
+ 8, 0, 8));
+ }
+
+ [Fact]
+ public void ParseHelperStatementMarksHelperSpanAsCanGrowIfMissingTrailingSpace()
+ {
+ ParseDocumentTest("@helper",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new HelperBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("helper").Accepts(AcceptedCharacters.Any))),
+ new RazorError(
+ String.Format(
+ RazorResources.ParseError_Unexpected_Character_At_Helper_Name_Start,
+ RazorResources.ErrorComponent_EndOfFile),
+ 7, 0, 7));
+ }
+
+ [Fact]
+ public void ParseHelperStatementCapturesWhitespaceToEndOfLineIfHelperStatementMissingName()
+ {
+ ParseDocumentTest(@"@helper
+ ",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new HelperBlock(new HelperCodeGenerator(new LocationTagged<string>(" ", 8, 0, 8), headerComplete: false),
+ Factory.CodeTransition(),
+ Factory.MetaCode("helper ").Accepts(AcceptedCharacters.None),
+ Factory.Code(" \r\n").Hidden()),
+ Factory.Markup(@" ")),
+ new RazorError(
+ String.Format(
+ RazorResources.ParseError_Unexpected_Character_At_Helper_Name_Start,
+ RazorResources.ErrorComponent_Newline),
+ 30, 0, 30));
+ }
+
+ [Fact]
+ public void ParseHelperStatementCapturesWhitespaceToEndOfLineIfHelperStatementMissingOpenParen()
+ {
+ ParseDocumentTest(@"@helper Foo
+ ",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new HelperBlock(new HelperCodeGenerator(new LocationTagged<string>("Foo ", 8, 0, 8), headerComplete: false),
+ Factory.CodeTransition(),
+ Factory.MetaCode("helper ").Accepts(AcceptedCharacters.None),
+ Factory.Code("Foo \r\n").Hidden()),
+ Factory.Markup(" ")),
+ new RazorError(
+ String.Format(RazorResources.ParseError_MissingCharAfterHelperName, "("),
+ 15, 0, 15));
+ }
+
+ [Fact]
+ public void ParseHelperStatementCapturesAllContentToEndOfFileIfHelperStatementMissingCloseParenInParameterList()
+ {
+ ParseDocumentTest(@"@helper Foo(Foo Bar
+Biz
+Boz",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new HelperBlock(new HelperCodeGenerator(new LocationTagged<string>("Foo(Foo Bar\r\nBiz\r\nBoz", 8, 0, 8), headerComplete: false),
+ Factory.CodeTransition(),
+ Factory.MetaCode("helper ").Accepts(AcceptedCharacters.None),
+ Factory.Code("Foo(Foo Bar\r\nBiz\r\nBoz").Hidden())),
+ new RazorError(
+ RazorResources.ParseError_UnterminatedHelperParameterList,
+ 11, 0, 11));
+ }
+
+ [Fact]
+ public void ParseHelperStatementCapturesWhitespaceToEndOfLineIfHelperStatementMissingOpenBraceAfterParameterList()
+ {
+ ParseDocumentTest(@"@helper Foo(string foo)
+",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new HelperBlock(new HelperCodeGenerator(new LocationTagged<string>("Foo(string foo) ", 8, 0, 8), headerComplete: false),
+ Factory.CodeTransition(),
+ Factory.MetaCode("helper ").Accepts(AcceptedCharacters.None),
+ Factory.Code("Foo(string foo) \r\n").Hidden())),
+ new RazorError(
+ String.Format(RazorResources.ParseError_MissingCharAfterHelperParameters, "{"),
+ 29, 1, 0));
+ }
+
+ [Fact]
+ public void ParseHelperStatementContinuesParsingHelperUntilEOF()
+ {
+ ParseDocumentTest(@"@helper Foo(string foo) {
+ <p>Foo</p>",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new HelperBlock(new HelperCodeGenerator(new LocationTagged<string>("Foo(string foo) {", 8, 0, 8), headerComplete: true),
+ Factory.CodeTransition(),
+ Factory.MetaCode("helper ").Accepts(AcceptedCharacters.None),
+ Factory.Code(@"Foo(string foo) {").Hidden().Accepts(AcceptedCharacters.None),
+ new StatementBlock(
+ Factory.Code(" \r\n")
+ .AsStatement()
+ .AutoCompleteWith("}"),
+ new MarkupBlock(
+ Factory.Markup(" <p>Foo</p>").Accepts(AcceptedCharacters.None)),
+ Factory.EmptyCSharp().AsStatement()))),
+ new RazorError(
+ String.Format(
+ RazorResources.ParseError_Expected_EndOfBlock_Before_EOF,
+ "helper", "}", "{"),
+ 1, 0, 1));
+ }
+
+ [Fact]
+ public void ParseHelperStatementCorrectlyParsesHelperWithEmbeddedCode()
+ {
+ ParseDocumentTest(@"@helper Foo(string foo) {
+ <p>@foo</p>
+}",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new HelperBlock(new HelperCodeGenerator(new LocationTagged<string>("Foo(string foo) {", 8, 0, 8), headerComplete: true),
+ Factory.CodeTransition(),
+ Factory.MetaCode("helper ").Accepts(AcceptedCharacters.None),
+ Factory.Code(@"Foo(string foo) {").Hidden().Accepts(AcceptedCharacters.None),
+ new StatementBlock(
+ Factory.Code(" \r\n").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" <p>"),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("foo")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ Factory.Markup("</p>\r\n").Accepts(AcceptedCharacters.None)),
+ Factory.EmptyCSharp().AsStatement()),
+ Factory.Code("}").Hidden().Accepts(AcceptedCharacters.None)),
+ Factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void ParseHelperStatementCorrectlyParsesHelperWithNewlinesBetweenCloseParenAndOpenBrace()
+ {
+ ParseDocumentTest(@"@helper Foo(string foo)
+
+
+
+{
+ <p>@foo</p>
+}",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new HelperBlock(new HelperCodeGenerator(new LocationTagged<string>("Foo(string foo)\r\n\r\n\r\n\r\n{", 8, 0, 8), headerComplete: true),
+ Factory.CodeTransition(),
+ Factory.MetaCode("helper ").Accepts(AcceptedCharacters.None),
+ Factory.Code("Foo(string foo)\r\n\r\n\r\n\r\n{").Hidden().Accepts(AcceptedCharacters.None),
+ new StatementBlock(
+ Factory.Code(" \r\n").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(@" <p>"),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("foo")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ Factory.Markup("</p>\r\n").Accepts(AcceptedCharacters.None)),
+ Factory.EmptyCSharp().AsStatement()),
+ Factory.Code("}").Hidden().Accepts(AcceptedCharacters.None)),
+ Factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void ParseHelperStatementGivesWhitespaceAfterOpenBraceToMarkupInDesignMode()
+ {
+ ParseDocumentTest(@"@helper Foo(string foo) {
+ ",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new HelperBlock(new HelperCodeGenerator(new LocationTagged<string>("Foo(string foo) {", 8, 0, 8), headerComplete: true),
+ Factory.CodeTransition(),
+ Factory.MetaCode("helper ").Accepts(AcceptedCharacters.None),
+ Factory.Code(@"Foo(string foo) {").Hidden().Accepts(AcceptedCharacters.None),
+ new StatementBlock(
+ Factory.Code(" \r\n ")
+ .AsStatement()
+ .AutoCompleteWith("}")))),
+ designTimeParser: true,
+ expectedErrors: new[]
+ {
+ new RazorError(
+ String.Format(
+ RazorResources.ParseError_Expected_EndOfBlock_Before_EOF,
+ "helper", "}", "{"),
+ new SourceLocation(1, 0, 1))
+ });
+ }
+
+ [Fact]
+ public void ParseHelperAcceptsNestedHelpersButOutputsError()
+ {
+ ParseDocumentTest(@"@helper Foo(string foo) {
+ @helper Bar(string baz) {
+ }
+}",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new HelperBlock(new HelperCodeGenerator(new LocationTagged<string>("Foo(string foo) {", 8, 0, 8), headerComplete: true),
+ Factory.CodeTransition(),
+ Factory.MetaCode("helper ").Accepts(AcceptedCharacters.None),
+ Factory.Code(@"Foo(string foo) {").Hidden().Accepts(AcceptedCharacters.None),
+ new StatementBlock(
+ Factory.Code("\r\n ").AsStatement(),
+ new HelperBlock(new HelperCodeGenerator(new LocationTagged<string>("Bar(string baz) {", 39, 1, 12), headerComplete: true),
+ Factory.CodeTransition(),
+ Factory.MetaCode("helper ").Accepts(AcceptedCharacters.None),
+ Factory.Code(@"Bar(string baz) {").Hidden().Accepts(AcceptedCharacters.None),
+ new StatementBlock(
+ Factory.Code("\r\n ").AsStatement()),
+ Factory.Code("}").Hidden().Accepts(AcceptedCharacters.None)),
+ Factory.Code("\r\n").AsStatement()),
+ Factory.Code("}").Hidden().Accepts(AcceptedCharacters.None)),
+ Factory.EmptyHtml()),
+ designTimeParser: true,
+ expectedErrors: new[]
+ {
+ new RazorError(RazorResources.ParseError_Helpers_Cannot_Be_Nested, 38, 1, 11)
+ });
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/CSharp/CSharpImplicitExpressionTest.cs b/test/System.Web.Razor.Test/Parser/CSharp/CSharpImplicitExpressionTest.cs
new file mode 100644
index 00000000..44a375c1
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/CSharp/CSharpImplicitExpressionTest.cs
@@ -0,0 +1,201 @@
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Test.Framework;
+using System.Web.Razor.Text;
+using Xunit;
+
+namespace System.Web.Razor.Test.Parser.CSharp
+{
+ public class CSharpImplicitExpressionTest : CsHtmlCodeParserTestBase
+ {
+ private const string TestExtraKeyword = "model";
+
+ public override ParserBase CreateCodeParser()
+ {
+ return new CSharpCodeParser();
+ }
+
+ [Fact]
+ public void NestedImplicitExpression()
+ {
+ ParseBlockTest("if (true) { @foo }",
+ new StatementBlock(
+ Factory.Code("if (true) { ").AsStatement(),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("foo")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ Factory.Code(" }").AsStatement()));
+ }
+
+ [Fact]
+ public void ParseBlockAcceptsNonEnglishCharactersThatAreValidIdentifiers()
+ {
+ ImplicitExpressionTest("हळूँजद॔.", "हळूँजद॔");
+ }
+
+ [Fact]
+ public void ParseBlockOutputsZeroLengthCodeSpanIfInvalidCharacterFollowsTransition()
+ {
+ ParseBlockTest("@/",
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.EmptyCSharp()
+ .AsImplicitExpression(KeywordSet)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ new RazorError(
+ String.Format(RazorResources.ParseError_Unexpected_Character_At_Start_Of_CodeBlock_CS, "/"),
+ new SourceLocation(1, 0, 1)));
+ }
+
+ [Fact]
+ public void ParseBlockOutputsZeroLengthCodeSpanIfEOFOccursAfterTransition()
+ {
+ ParseBlockTest("@",
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.EmptyCSharp()
+ .AsImplicitExpression(KeywordSet)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ new RazorError(
+ RazorResources.ParseError_Unexpected_EndOfFile_At_Start_Of_CodeBlock,
+ new SourceLocation(1, 0, 1)));
+ }
+
+ [Fact]
+ public void ParseBlockSupportsSlashesWithinComplexImplicitExpressions()
+ {
+ ImplicitExpressionTest("DataGridColumn.Template(\"Years of Service\", e => (int)Math.Round((DateTime.Now - dt).TotalDays / 365))");
+ }
+
+ [Fact]
+ public void ParseBlockMethodParsesSingleIdentifierAsImplicitExpression()
+ {
+ ImplicitExpressionTest("foo");
+ }
+
+ [Fact]
+ public void ParseBlockMethodDoesNotAcceptSemicolonIfExpressionTerminatedByWhitespace()
+ {
+ ImplicitExpressionTest("foo ;", "foo");
+ }
+
+ [Fact]
+ public void ParseBlockMethodIgnoresSemicolonAtEndOfSimpleImplicitExpression()
+ {
+ RunTrailingSemicolonTest("foo");
+ }
+
+ [Fact]
+ public void ParseBlockMethodParsesDottedIdentifiersAsImplicitExpression()
+ {
+ ImplicitExpressionTest("foo.bar.baz");
+ }
+
+ [Fact]
+ public void ParseBlockMethodIgnoresSemicolonAtEndOfDottedIdentifiers()
+ {
+ RunTrailingSemicolonTest("foo.bar.baz");
+ }
+
+ [Fact]
+ public void ParseBlockMethodDoesNotIncludeDotAtEOFInImplicitExpression()
+ {
+ ImplicitExpressionTest("foo.bar.", "foo.bar");
+ }
+
+ [Fact]
+ public void ParseBlockMethodDoesNotIncludeDotFollowedByInvalidIdentifierCharacterInImplicitExpression()
+ {
+ ImplicitExpressionTest("foo.bar.0", "foo.bar");
+ ImplicitExpressionTest("foo.bar.</p>", "foo.bar");
+ }
+
+ [Fact]
+ public void ParseBlockMethodDoesNotIncludeSemicolonAfterDot()
+ {
+ ImplicitExpressionTest("foo.bar.;", "foo.bar");
+ }
+
+ [Fact]
+ public void ParseBlockMethodTerminatesAfterIdentifierUnlessFollowedByDotOrParenInImplicitExpression()
+ {
+ ImplicitExpressionTest("foo.bar</p>", "foo.bar");
+ }
+
+ [Fact]
+ public void ParseBlockProperlyParsesParenthesesAndBalancesThemInImplicitExpression()
+ {
+ ImplicitExpressionTest(@"foo().bar(""bi\""z"", 4)(""chained method; call"").baz(@""bo""""z"", '\'', () => { return 4; }, (4+5+new { foo = bar[4] }))");
+ }
+
+ [Fact]
+ public void ParseBlockProperlyParsesBracketsAndBalancesThemInImplicitExpression()
+ {
+ ImplicitExpressionTest(@"foo.bar[4 * (8 + 7)][""fo\""o""].baz");
+ }
+
+ [Fact]
+ public void ParseBlockTerminatesImplicitExpressionAtHtmlEndTag()
+ {
+ ImplicitExpressionTest("foo().bar.baz</p>zoop", "foo().bar.baz");
+ }
+
+ [Fact]
+ public void ParseBlockTerminatesImplicitExpressionAtHtmlStartTag()
+ {
+ ImplicitExpressionTest("foo().bar.baz<p>zoop", "foo().bar.baz");
+ }
+
+ [Fact]
+ public void ParseBlockTerminatesImplicitExpressionBeforeDotIfDotNotFollowedByIdentifierStartCharacter()
+ {
+ ImplicitExpressionTest("foo().bar.baz.42", "foo().bar.baz");
+ }
+
+ [Fact]
+ public void ParseBlockStopsBalancingParenthesesAtEOF()
+ {
+ ImplicitExpressionTest("foo(()", "foo(()",
+ acceptedCharacters: AcceptedCharacters.Any,
+ errors: new RazorError(String.Format(RazorResources.ParseError_Expected_CloseBracket_Before_EOF, "(", ")"), new SourceLocation(4, 0, 4)));
+ }
+
+ [Fact]
+ public void ParseBlockTerminatesImplicitExpressionIfCloseParenFollowedByAnyWhiteSpace()
+ {
+ ImplicitExpressionTest("foo.bar() (baz)", "foo.bar()");
+ }
+
+ [Fact]
+ public void ParseBlockTerminatesImplicitExpressionIfIdentifierFollowedByAnyWhiteSpace()
+ {
+ ImplicitExpressionTest("foo .bar() (baz)", "foo");
+ }
+
+ [Fact]
+ public void ParseBlockTerminatesImplicitExpressionAtLastValidPointIfDotFollowedByWhitespace()
+ {
+ ImplicitExpressionTest("foo. bar() (baz)", "foo");
+ }
+
+ [Fact]
+ public void ParseBlockOutputExpressionIfModuleTokenNotFollowedByBrace()
+ {
+ ImplicitExpressionTest("module.foo()");
+ }
+
+ private void RunTrailingSemicolonTest(string expr)
+ {
+ ParseBlockTest(SyntaxConstants.TransitionString + expr + ";",
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code(expr)
+ .AsImplicitExpression(KeywordSet)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)
+ ));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/CSharp/CSharpLayoutDirectiveTest.cs b/test/System.Web.Razor.Test/Parser/CSharp/CSharpLayoutDirectiveTest.cs
new file mode 100644
index 00000000..ad444764
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/CSharp/CSharpLayoutDirectiveTest.cs
@@ -0,0 +1,84 @@
+using System.Web.Razor.Editor;
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Test.Framework;
+using Xunit;
+using Xunit.Extensions;
+
+namespace System.Web.Razor.Test.Parser.CSharp
+{
+ public class CSharpLayoutDirectiveTest : CsHtmlCodeParserTestBase
+ {
+ [Theory]
+ [InlineData("Layout")]
+ [InlineData("LAYOUT")]
+ [InlineData("layOut")]
+ [InlineData("LayOut")]
+ private void LayoutKeywordIsCaseSensitive(string word)
+ {
+ ParseBlockTest(word,
+ new ExpressionBlock(
+ Factory.Code(word)
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)
+ ));
+ }
+
+ [Fact]
+ public void LayoutDirectiveAcceptsAllTextToEndOfLine()
+ {
+ ParseBlockTest(@"@layout Foo Bar Baz",
+ new DirectiveBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("layout ").Accepts(AcceptedCharacters.None),
+ Factory.MetaCode("Foo Bar Baz")
+ .With(new SetLayoutCodeGenerator("Foo Bar Baz"))
+ .WithEditorHints(EditorHints.VirtualPath | EditorHints.LayoutPage)
+ )
+ );
+ }
+
+ [Fact]
+ public void LayoutDirectiveAcceptsAnyIfNoWhitespaceFollowingLayoutKeyword()
+ {
+ ParseBlockTest(@"@layout",
+ new DirectiveBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("layout")
+ )
+ );
+ }
+
+ [Fact]
+ public void LayoutDirectiveOutputsMarkerSpanIfAnyWhitespaceAfterLayoutKeyword()
+ {
+ ParseBlockTest(@"@layout ",
+ new DirectiveBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("layout ").Accepts(AcceptedCharacters.None),
+ Factory.EmptyCSharp()
+ .AsMetaCode()
+ .With(new SetLayoutCodeGenerator(String.Empty))
+ .WithEditorHints(EditorHints.VirtualPath | EditorHints.LayoutPage)
+ )
+ );
+ }
+
+ [Fact]
+ public void LayoutDirectiveAcceptsTrailingNewlineButDoesNotIncludeItInLayoutPath()
+ {
+ ParseBlockTest(@"@layout Foo
+",
+ new DirectiveBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("layout ").Accepts(AcceptedCharacters.None),
+ Factory.MetaCode("Foo\r\n")
+ .With(new SetLayoutCodeGenerator("Foo"))
+ .Accepts(AcceptedCharacters.None)
+ .WithEditorHints(EditorHints.VirtualPath | EditorHints.LayoutPage)
+ )
+ );
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/CSharp/CSharpNestedStatementsTest.cs b/test/System.Web.Razor.Test/Parser/CSharp/CSharpNestedStatementsTest.cs
new file mode 100644
index 00000000..7e8f2bba
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/CSharp/CSharpNestedStatementsTest.cs
@@ -0,0 +1,100 @@
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Test.Framework;
+using Xunit;
+
+namespace System.Web.Razor.Test.Parser.CSharp
+{
+ public class CSharpNestedStatementsTest : CsHtmlCodeParserTestBase
+ {
+ [Fact]
+ public void NestedSimpleStatement()
+ {
+ ParseBlockTest("@while(true) { foo(); }",
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("while(true) { foo(); }")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void NestedKeywordStatement()
+ {
+ ParseBlockTest("@while(true) { for(int i = 0; i < 10; i++) { foo(); } }",
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("while(true) { for(int i = 0; i < 10; i++) { foo(); } }")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void NestedCodeBlock()
+ {
+ ParseBlockTest("@while(true) { { { { foo(); } } } }",
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("while(true) { { { { foo(); } } } }")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void NestedImplicitExpression()
+ {
+ ParseBlockTest("@while(true) { @foo }",
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("while(true) { ")
+ .AsStatement(),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("foo")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ Factory.Code(" }")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void NestedExplicitExpression()
+ {
+ ParseBlockTest("@while(true) { @(foo) }",
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("while(true) { ")
+ .AsStatement(),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("(")
+ .Accepts(AcceptedCharacters.None),
+ Factory.Code("foo")
+ .AsExpression(),
+ Factory.MetaCode(")")
+ .Accepts(AcceptedCharacters.None)),
+ Factory.Code(" }")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void NestedMarkupBlock()
+ {
+ ParseBlockTest("@while(true) { <p>Hello</p> }",
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("while(true) {")
+ .AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" <p>Hello</p> ")
+ .With(new MarkupCodeGenerator())
+ .Accepts(AcceptedCharacters.None)),
+ Factory.Code("}")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/CSharp/CSharpRazorCommentsTest.cs b/test/System.Web.Razor.Test/Parser/CSharp/CSharpRazorCommentsTest.cs
new file mode 100644
index 00000000..7942c9e6
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/CSharp/CSharpRazorCommentsTest.cs
@@ -0,0 +1,172 @@
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Test.Framework;
+using System.Web.Razor.Tokenizer.Symbols;
+using Xunit;
+
+namespace System.Web.Razor.Test.Parser.CSharp
+{
+ public class CSharpRazorCommentsTest : CsHtmlMarkupParserTestBase
+ {
+ [Fact]
+ public void UnterminatedRazorComment()
+ {
+ ParseDocumentTest("@*",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new CommentBlock(
+ Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition)
+ .Accepts(AcceptedCharacters.None),
+ Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar)
+ .Accepts(AcceptedCharacters.None),
+ Factory.Span(SpanKind.Comment, new HtmlSymbol(
+ Factory.LocationTracker.CurrentLocation,
+ String.Empty,
+ HtmlSymbolType.Unknown))
+ .Accepts(AcceptedCharacters.Any))),
+ new RazorError(RazorResources.ParseError_RazorComment_Not_Terminated, 0, 0, 0));
+ }
+
+ [Fact]
+ public void EmptyRazorComment()
+ {
+ ParseDocumentTest("@**@",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new CommentBlock(
+ Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition)
+ .Accepts(AcceptedCharacters.None),
+ Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar)
+ .Accepts(AcceptedCharacters.None),
+ Factory.Span(SpanKind.Comment, new HtmlSymbol(
+ Factory.LocationTracker.CurrentLocation,
+ String.Empty,
+ HtmlSymbolType.Unknown))
+ .Accepts(AcceptedCharacters.Any),
+ Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar)
+ .Accepts(AcceptedCharacters.None),
+ Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition)
+ .Accepts(AcceptedCharacters.None)),
+ Factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void RazorCommentInImplicitExpressionMethodCall()
+ {
+ ParseDocumentTest(@"@foo(
+@**@
+",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("foo(\r\n")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords),
+ new CommentBlock(
+ Factory.CodeTransition(CSharpSymbolType.RazorCommentTransition)
+ .Accepts(AcceptedCharacters.None),
+ Factory.MetaCode("*", CSharpSymbolType.RazorCommentStar)
+ .Accepts(AcceptedCharacters.None),
+ Factory.Span(SpanKind.Comment, new CSharpSymbol(
+ Factory.LocationTracker.CurrentLocation,
+ String.Empty,
+ CSharpSymbolType.Unknown))
+ .Accepts(AcceptedCharacters.Any),
+ Factory.MetaCode("*", CSharpSymbolType.RazorCommentStar)
+ .Accepts(AcceptedCharacters.None),
+ Factory.CodeTransition(CSharpSymbolType.RazorCommentTransition)
+ .Accepts(AcceptedCharacters.None)),
+ Factory.Code("\r\n")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords))),
+ new RazorError(
+ String.Format(RazorResources.ParseError_Expected_CloseBracket_Before_EOF, "(", ")"),
+ 4, 0, 4));
+ }
+
+ [Fact]
+ public void UnterminatedRazorCommentInImplicitExpressionMethodCall()
+ {
+ ParseDocumentTest("@foo(@*",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("foo(")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords),
+ new CommentBlock(
+ Factory.CodeTransition(CSharpSymbolType.RazorCommentTransition)
+ .Accepts(AcceptedCharacters.None),
+ Factory.MetaCode("*", CSharpSymbolType.RazorCommentStar)
+ .Accepts(AcceptedCharacters.None),
+ Factory.Span(SpanKind.Comment, new CSharpSymbol(
+ Factory.LocationTracker.CurrentLocation,
+ String.Empty,
+ CSharpSymbolType.Unknown))
+ .Accepts(AcceptedCharacters.Any)))),
+ new RazorError(RazorResources.ParseError_RazorComment_Not_Terminated, 5, 0, 5),
+ new RazorError(String.Format(RazorResources.ParseError_Expected_CloseBracket_Before_EOF, "(", ")"), 4, 0, 4));
+ }
+
+ [Fact]
+ public void RazorCommentInVerbatimBlock()
+ {
+ ParseDocumentTest(@"@{
+ <text
+ @**@
+}",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ Factory.Code("\r\n").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" "),
+ Factory.MarkupTransition("<text").Accepts(AcceptedCharacters.Any),
+ Factory.Markup("\r\n "),
+ new CommentBlock(
+ Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition)
+ .Accepts(AcceptedCharacters.None),
+ Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar)
+ .Accepts(AcceptedCharacters.None),
+ Factory.Span(SpanKind.Comment, new HtmlSymbol(
+ Factory.LocationTracker.CurrentLocation,
+ String.Empty,
+ HtmlSymbolType.Unknown))
+ .Accepts(AcceptedCharacters.Any),
+ Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar)
+ .Accepts(AcceptedCharacters.None),
+ Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition)
+ .Accepts(AcceptedCharacters.None)),
+ Factory.Markup("\r\n}")))),
+ new RazorError(RazorResources.ParseError_TextTagCannotContainAttributes, 8, 1, 4),
+ new RazorError(String.Format(RazorResources.ParseError_MissingEndTag, "text"), 8, 1, 4),
+ new RazorError(String.Format(RazorResources.ParseError_Expected_EndOfBlock_Before_EOF, RazorResources.BlockName_Code, "}", "{"), 1, 0, 1));
+ }
+
+ [Fact]
+ public void UnterminatedRazorCommentInVerbatimBlock()
+ {
+ ParseDocumentTest("@{@*",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ Factory.EmptyCSharp()
+ .AsStatement(),
+ new CommentBlock(
+ Factory.CodeTransition(CSharpSymbolType.RazorCommentTransition)
+ .Accepts(AcceptedCharacters.None),
+ Factory.MetaCode("*", CSharpSymbolType.RazorCommentStar)
+ .Accepts(AcceptedCharacters.None),
+ Factory.Span(SpanKind.Comment, new CSharpSymbol(Factory.LocationTracker.CurrentLocation,
+ String.Empty,
+ CSharpSymbolType.Unknown))
+ .Accepts(AcceptedCharacters.Any)))),
+ new RazorError(RazorResources.ParseError_RazorComment_Not_Terminated, 2, 0, 2),
+ new RazorError(String.Format(RazorResources.ParseError_Expected_EndOfBlock_Before_EOF, RazorResources.BlockName_Code, "}", "{"), 1, 0, 1));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/CSharp/CSharpReservedWordsTest.cs b/test/System.Web.Razor.Test/Parser/CSharp/CSharpReservedWordsTest.cs
new file mode 100644
index 00000000..cc6bfdde
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/CSharp/CSharpReservedWordsTest.cs
@@ -0,0 +1,41 @@
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Test.Framework;
+using System.Web.Razor.Text;
+using Xunit.Extensions;
+
+namespace System.Web.Razor.Test.Parser.CSharp
+{
+ public class CSharpReservedWordsTest : CsHtmlCodeParserTestBase
+ {
+ [Theory]
+ [InlineData("namespace")]
+ [InlineData("class")]
+ public void ReservedWords(string word)
+ {
+ ParseBlockTest(word,
+ new DirectiveBlock(
+ Factory.MetaCode(word).Accepts(AcceptedCharacters.None)
+ ),
+ new RazorError(String.Format(RazorResources.ParseError_ReservedWord, word), SourceLocation.Zero));
+ }
+
+ [Theory]
+ [InlineData("Namespace")]
+ [InlineData("Class")]
+ [InlineData("NAMESPACE")]
+ [InlineData("CLASS")]
+ [InlineData("nameSpace")]
+ [InlineData("NameSpace")]
+ private void ReservedWordsAreCaseSensitive(string word)
+ {
+ ParseBlockTest(word,
+ new ExpressionBlock(
+ Factory.Code(word)
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)
+ ));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/CSharp/CSharpSectionTest.cs b/test/System.Web.Razor.Test/Parser/CSharp/CSharpSectionTest.cs
new file mode 100644
index 00000000..4239354c
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/CSharp/CSharpSectionTest.cs
@@ -0,0 +1,298 @@
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Test.Framework;
+using Xunit;
+
+namespace System.Web.Razor.Test.Parser.CSharp
+{
+ public class CSharpSectionTest : CsHtmlMarkupParserTestBase
+ {
+ [Fact]
+ public void ParseSectionBlockCapturesNewlineImmediatelyFollowing()
+ {
+ ParseDocumentTest(@"@section
+",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new SectionBlock(new SectionCodeGenerator(String.Empty),
+ Factory.CodeTransition(),
+ Factory.MetaCode("section\r\n"))),
+ new RazorError(
+ String.Format(
+ RazorResources.ParseError_Unexpected_Character_At_Section_Name_Start,
+ RazorResources.ErrorComponent_EndOfFile),
+ 10, 1, 0));
+ }
+
+ [Fact]
+ public void ParseSectionBlockCapturesWhitespaceToEndOfLineInSectionStatementMissingOpenBrace()
+ {
+ ParseDocumentTest(@"@section Foo
+ ",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new SectionBlock(new SectionCodeGenerator("Foo"),
+ Factory.CodeTransition(),
+ Factory.MetaCode("section Foo \r\n")),
+ Factory.Markup(@" ")),
+ new RazorError(RazorResources.ParseError_MissingOpenBraceAfterSection, 12, 0, 12));
+ }
+
+ [Fact]
+ public void ParseSectionBlockCapturesWhitespaceToEndOfLineInSectionStatementMissingName()
+ {
+ ParseDocumentTest(@"@section
+ ",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new SectionBlock(new SectionCodeGenerator(String.Empty),
+ Factory.CodeTransition(),
+ Factory.MetaCode("section \r\n")),
+ Factory.Markup(@" ")),
+ new RazorError(
+ String.Format(
+ RazorResources.ParseError_Unexpected_Character_At_Section_Name_Start,
+ RazorResources.ErrorComponent_EndOfFile),
+ 23, 1, 4));
+ }
+
+ [Fact]
+ public void ParseSectionBlockIgnoresSectionUnlessAllLowerCase()
+ {
+ ParseDocumentTest("@Section foo",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("Section")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ Factory.Markup(" foo")));
+ }
+
+ [Fact]
+ public void ParseSectionBlockReportsErrorAndTerminatesSectionBlockIfKeywordNotFollowedByIdentifierStartCharacter()
+ {
+ ParseDocumentTest("@section 9 { <p>Foo</p> }",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new SectionBlock(new SectionCodeGenerator(String.Empty),
+ Factory.CodeTransition(),
+ Factory.MetaCode("section ")),
+ Factory.Markup("9 { <p>Foo</p> }")),
+ new RazorError(
+ String.Format(
+ RazorResources.ParseError_Unexpected_Character_At_Section_Name_Start,
+ String.Format(RazorResources.ErrorComponent_Character, "9")),
+ 9, 0, 9));
+ }
+
+ [Fact]
+ public void ParseSectionBlockReportsErrorAndTerminatesSectionBlockIfNameNotFollowedByOpenBrace()
+ {
+ ParseDocumentTest("@section foo-bar { <p>Foo</p> }",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new SectionBlock(new SectionCodeGenerator("foo"),
+ Factory.CodeTransition(),
+ Factory.MetaCode("section foo")),
+ Factory.Markup("-bar { <p>Foo</p> }")),
+ new RazorError(RazorResources.ParseError_MissingOpenBraceAfterSection, 12, 0, 12));
+ }
+
+ [Fact]
+ public void ParserOutputsErrorOnNestedSections()
+ {
+ ParseDocumentTest("@section foo { @section bar { <p>Foo</p> } }",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new SectionBlock(new SectionCodeGenerator("foo"),
+ Factory.CodeTransition(),
+ Factory.MetaCode("section foo {")
+ .AutoCompleteWith(null, atEndOfSpan: true),
+ new MarkupBlock(
+ Factory.Markup(" "),
+ new SectionBlock(new SectionCodeGenerator("bar"),
+ Factory.CodeTransition(),
+ Factory.MetaCode("section bar {")
+ .AutoCompleteWith(null, atEndOfSpan: true),
+ new MarkupBlock(
+ Factory.Markup(" <p>Foo</p> ")),
+ Factory.MetaCode("}").Accepts(AcceptedCharacters.None)),
+ Factory.Markup(" ")),
+ Factory.MetaCode("}").Accepts(AcceptedCharacters.None)),
+ Factory.EmptyHtml()),
+ new RazorError(
+ String.Format(
+ RazorResources.ParseError_Sections_Cannot_Be_Nested,
+ RazorResources.SectionExample_CS),
+ 23, 0, 23));
+ }
+
+ [Fact]
+ public void ParseSectionBlockHandlesEOFAfterOpenBrace()
+ {
+ ParseDocumentTest("@section foo {",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new SectionBlock(new SectionCodeGenerator("foo"),
+ Factory.CodeTransition(),
+ Factory.MetaCode("section foo {")
+ .AutoCompleteWith("}", atEndOfSpan: true),
+ new MarkupBlock())),
+ new RazorError(
+ String.Format(RazorResources.ParseError_Expected_X, "}"),
+ 14, 0, 14));
+ }
+
+ [Fact]
+ public void ParseSectionBlockHandlesUnterminatedSection()
+ {
+ ParseDocumentTest("@section foo { <p>Foo{}</p>",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new SectionBlock(new SectionCodeGenerator("foo"),
+ Factory.CodeTransition(),
+ Factory.MetaCode("section foo {")
+ .AutoCompleteWith("}", atEndOfSpan: true),
+ new MarkupBlock(
+ // Need to provide the markup span as fragments, since the parser will split the {} into separate symbols.
+ Factory.Markup(" <p>Foo", "{", "}", "</p>")))),
+ new RazorError(
+ String.Format(RazorResources.ParseError_Expected_X, "}"),
+ 27, 0, 27));
+ }
+
+ [Fact]
+ public void ParseSectionBlockReportsErrorAndAcceptsWhitespaceToEndOfLineIfSectionNotFollowedByOpenBrace()
+ {
+ ParseDocumentTest(@"@section foo
+",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new SectionBlock(new SectionCodeGenerator("foo"),
+ Factory.CodeTransition(),
+ Factory.MetaCode("section foo \r\n"))),
+ new RazorError(RazorResources.ParseError_MissingOpenBraceAfterSection, 12, 0, 12));
+ }
+
+ [Fact]
+ public void ParseSectionBlockAcceptsOpenBraceMultipleLinesBelowSectionName()
+ {
+ ParseDocumentTest(@"@section foo
+
+
+
+
+
+{
+<p>Foo</p>
+}",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new SectionBlock(new SectionCodeGenerator("foo"),
+ Factory.CodeTransition(),
+ Factory.MetaCode("section foo \r\n\r\n\r\n\r\n\r\n\r\n{")
+ .AutoCompleteWith(null, atEndOfSpan: true),
+ new MarkupBlock(
+ Factory.Markup("\r\n<p>Foo</p>\r\n")),
+ Factory.MetaCode("}").Accepts(AcceptedCharacters.None)),
+ Factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void ParseSectionBlockParsesNamedSectionCorrectly()
+ {
+ ParseDocumentTest("@section foo { <p>Foo</p> }",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new SectionBlock(new SectionCodeGenerator("foo"),
+ Factory.CodeTransition(),
+ Factory.MetaCode("section foo {")
+ .AutoCompleteWith(null, atEndOfSpan: true),
+ new MarkupBlock(
+ Factory.Markup(" <p>Foo</p> ")),
+ Factory.MetaCode("}").Accepts(AcceptedCharacters.None)),
+ Factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void ParseSectionBlockDoesNotRequireSpaceBetweenSectionNameAndOpenBrace()
+ {
+ ParseDocumentTest("@section foo{ <p>Foo</p> }",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new SectionBlock(new SectionCodeGenerator("foo"),
+ Factory.CodeTransition(),
+ Factory.MetaCode("section foo{")
+ .AutoCompleteWith(null, atEndOfSpan: true),
+ new MarkupBlock(
+ Factory.Markup(" <p>Foo</p> ")),
+ Factory.MetaCode("}").Accepts(AcceptedCharacters.None)),
+ Factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void ParseSectionBlockBalancesBraces()
+ {
+ ParseDocumentTest("@section foo { <script>(function foo() { return 1; })();</script> }",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new SectionBlock(new SectionCodeGenerator("foo"),
+ Factory.CodeTransition(),
+ Factory.MetaCode("section foo {")
+ .AutoCompleteWith(null, atEndOfSpan: true),
+ new MarkupBlock(
+ Factory.Markup(" <script>(function foo() ", "{", " return 1; ", "}", ")();</script> ")),
+ Factory.MetaCode("}").Accepts(AcceptedCharacters.None)),
+ Factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void ParseSectionBlockAllowsBracesInCSharpExpression()
+ {
+ ParseDocumentTest("@section foo { I really want to render a close brace, so here I go: @(\"}\") }",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new SectionBlock(new SectionCodeGenerator("foo"),
+ Factory.CodeTransition(),
+ Factory.MetaCode("section foo {")
+ .AutoCompleteWith(null, atEndOfSpan: true),
+ new MarkupBlock(
+ Factory.Markup(" I really want to render a close brace, so here I go: "),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("(").Accepts(AcceptedCharacters.None),
+ Factory.Code("\"}\"").AsExpression(),
+ Factory.MetaCode(")").Accepts(AcceptedCharacters.None)),
+ Factory.Markup(" ")),
+ Factory.MetaCode("}").Accepts(AcceptedCharacters.None)),
+ Factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void SectionIsCorrectlyTerminatedWhenCloseBraceImmediatelyFollowsCodeBlock()
+ {
+ ParseDocumentTest(@"@section Foo {
+@if(true) {
+}
+}",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new SectionBlock(new SectionCodeGenerator("Foo"),
+ Factory.CodeTransition(),
+ Factory.MetaCode("section Foo {")
+ .AutoCompleteWith(null, atEndOfSpan: true),
+ new MarkupBlock(
+ Factory.Markup("\r\n"),
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("if(true) {\r\n}\r\n").AsStatement()
+ )),
+ Factory.MetaCode("}").Accepts(AcceptedCharacters.None)),
+ Factory.EmptyHtml()));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/CSharp/CSharpSpecialBlockTest.cs b/test/System.Web.Razor.Test/Parser/CSharp/CSharpSpecialBlockTest.cs
new file mode 100644
index 00000000..1f10433e
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/CSharp/CSharpSpecialBlockTest.cs
@@ -0,0 +1,195 @@
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Test.Framework;
+using System.Web.Razor.Text;
+using Xunit;
+
+namespace System.Web.Razor.Test.Parser.CSharp
+{
+ public class CSharpSpecialBlockTest : CsHtmlCodeParserTestBase
+ {
+ [Fact]
+ public void ParseInheritsStatementMarksInheritsSpanAsCanGrowIfMissingTrailingSpace()
+ {
+ ParseBlockTest("inherits",
+ new DirectiveBlock(
+ Factory.MetaCode("inherits").Accepts(AcceptedCharacters.Any)
+ ),
+ new RazorError(
+ RazorResources.ParseError_InheritsKeyword_Must_Be_Followed_By_TypeName,
+ new SourceLocation(8, 0, 8)));
+ }
+
+ [Fact]
+ public void InheritsBlockAcceptsMultipleGenericArguments()
+ {
+ ParseBlockTest("inherits Foo.Bar<Biz<Qux>, string, int>.Baz",
+ new DirectiveBlock(
+ Factory.MetaCode("inherits ").Accepts(AcceptedCharacters.None),
+ Factory.Code("Foo.Bar<Biz<Qux>, string, int>.Baz")
+ .AsBaseType("Foo.Bar<Biz<Qux>, string, int>.Baz")
+ ));
+ }
+
+ [Fact]
+ public void InheritsBlockOutputsErrorIfInheritsNotFollowedByTypeButAcceptsEntireLineAsCode()
+ {
+ ParseBlockTest(@"inherits
+foo",
+ new DirectiveBlock(
+ Factory.MetaCode("inherits ").Accepts(AcceptedCharacters.None),
+ Factory.Code(" \r\n")
+ .AsBaseType(String.Empty)
+ ),
+ new RazorError(RazorResources.ParseError_InheritsKeyword_Must_Be_Followed_By_TypeName, 24, 0, 24));
+ }
+
+ [Fact]
+ public void NamespaceImportInsideCodeBlockCausesError()
+ {
+ ParseBlockTest("{ using Foo.Bar.Baz; var foo = bar; }",
+ new StatementBlock(
+ Factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ Factory.Code(" using Foo.Bar.Baz; var foo = bar; ").AsStatement(),
+ Factory.MetaCode("}").Accepts(AcceptedCharacters.None)
+ ),
+ new RazorError(
+ RazorResources.ParseError_NamespaceImportAndTypeAlias_Cannot_Exist_Within_CodeBlock,
+ new SourceLocation(2, 0, 2)));
+ }
+
+ [Fact]
+ public void TypeAliasInsideCodeBlockIsNotHandledSpecially()
+ {
+ ParseBlockTest("{ using Foo = Bar.Baz; var foo = bar; }",
+ new StatementBlock(
+ Factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ Factory.Code(" using Foo = Bar.Baz; var foo = bar; ").AsStatement(),
+ Factory.MetaCode("}").Accepts(AcceptedCharacters.None)
+ ),
+ new RazorError(
+ RazorResources.ParseError_NamespaceImportAndTypeAlias_Cannot_Exist_Within_CodeBlock,
+ new SourceLocation(2, 0, 2)));
+ }
+
+ [Fact]
+ public void Plan9FunctionsKeywordInsideCodeBlockIsNotHandledSpecially()
+ {
+ ParseBlockTest("{ functions Foo; }",
+ new StatementBlock(
+ Factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ Factory.Code(" functions Foo; ").AsStatement(),
+ Factory.MetaCode("}").Accepts(AcceptedCharacters.None)
+ ));
+ }
+
+ [Fact]
+ public void NonKeywordStatementInCodeBlockIsHandledCorrectly()
+ {
+ ParseBlockTest(@"{
+ List<dynamic> photos = gallery.Photo.ToList();
+}",
+ new StatementBlock(
+ Factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ Factory.Code("\r\n List<dynamic> photos = gallery.Photo.ToList();\r\n").AsStatement(),
+ Factory.MetaCode("}").Accepts(AcceptedCharacters.None)
+ ));
+ }
+
+ [Fact]
+ public void ParseBlockBalancesBracesOutsideStringsIfFirstCharacterIsBraceAndReturnsSpanOfTypeCode()
+ {
+ // Arrange
+ const string code = "foo\"b}ar\" if(condition) { String.Format(\"{0}\"); } ";
+
+ // Act/Assert
+ ParseBlockTest("{" + code + "}",
+ new StatementBlock(
+ Factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ Factory.Code(code).AsStatement(),
+ Factory.MetaCode("}").Accepts(AcceptedCharacters.None)
+ ));
+ }
+
+ [Fact]
+ public void ParseBlockBalancesParensOutsideStringsIfFirstCharacterIsParenAndReturnsSpanOfTypeExpression()
+ {
+ // Arrange
+ const string code = "foo\"b)ar\" if(condition) { String.Format(\"{0}\"); } ";
+
+ // Act/Assert
+ ParseBlockTest("(" + code + ")",
+ new ExpressionBlock(
+ Factory.MetaCode("(").Accepts(AcceptedCharacters.None),
+ Factory.Code(code).AsExpression(),
+ Factory.MetaCode(")").Accepts(AcceptedCharacters.None)
+ ));
+ }
+
+ [Fact]
+ public void ParseBlockBalancesBracesAndOutputsContentAsClassLevelCodeSpanIfFirstIdentifierIsFunctionsKeyword()
+ {
+ const string code = " foo(); \"bar}baz\" ";
+ ParseBlockTest("functions {" + code + "} zoop",
+ new FunctionsBlock(
+ Factory.MetaCode("functions {").Accepts(AcceptedCharacters.None),
+ Factory.Code(code).AsFunctionsBody(),
+ Factory.MetaCode("}").Accepts(AcceptedCharacters.None)
+ ));
+ }
+
+ [Fact]
+ public void ParseBlockDoesNoErrorRecoveryForFunctionsBlock()
+ {
+ ParseBlockTest("functions { { { { { } zoop",
+ new FunctionsBlock(
+ Factory.MetaCode("functions {").Accepts(AcceptedCharacters.None),
+ Factory.Code(" { { { { } zoop").AsFunctionsBody()
+ ),
+ new RazorError(
+ String.Format(RazorResources.ParseError_Expected_EndOfBlock_Before_EOF, "functions", "}", "{"),
+ SourceLocation.Zero));
+ }
+
+ [Fact]
+ public void ParseBlockIgnoresFunctionsUnlessAllLowerCase()
+ {
+ ParseBlockTest("Functions { foo() }",
+ new ExpressionBlock(
+ Factory.Code("Functions")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)));
+ }
+
+ [Fact]
+ public void ParseBlockIgnoresSingleSlashAtStart()
+ {
+ ParseBlockTest("@/ foo",
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.EmptyCSharp()
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ new RazorError(
+ String.Format(RazorResources.ParseError_Unexpected_Character_At_Start_Of_CodeBlock_CS, "/"),
+ 1, 0, 1));
+ }
+
+ [Fact]
+ public void ParseBlockTerminatesSingleLineCommentAtEndOfLine()
+ {
+ ParseBlockTest(@"if(!false) {
+ // Foo
+ <p>A real tag!</p>
+}",
+ new StatementBlock(
+ Factory.Code("if(!false) {\r\n // Foo\r\n").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" <p>A real tag!</p>\r\n")
+ .Accepts(AcceptedCharacters.None)),
+ Factory.Code("}").AsStatement()
+ ));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/CSharp/CSharpStatementTest.cs b/test/System.Web.Razor.Test/Parser/CSharp/CSharpStatementTest.cs
new file mode 100644
index 00000000..fb2629f7
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/CSharp/CSharpStatementTest.cs
@@ -0,0 +1,208 @@
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Test.Framework;
+using Xunit;
+
+namespace System.Web.Razor.Test.Parser.CSharp
+{
+ // Basic Tests for C# Statements:
+ // * Basic case for each statement
+ // * Basic case for ALL clauses
+
+ // This class DOES NOT contain
+ // * Error cases
+ // * Tests for various types of nested statements
+ // * Comment tests
+
+ public class CSharpStatementTest : CsHtmlCodeParserTestBase
+ {
+ [Fact]
+ public void ForStatement()
+ {
+ ParseBlockTest("@for(int i = 0; i++; i < length) { foo(); }",
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("for(int i = 0; i++; i < length) { foo(); }")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)
+ ));
+ }
+
+ [Fact]
+ public void ForEachStatement()
+ {
+ ParseBlockTest("@foreach(var foo in bar) { foo(); }",
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("foreach(var foo in bar) { foo(); }")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)
+ ));
+ }
+
+ [Fact]
+ public void WhileStatement()
+ {
+ ParseBlockTest("@while(true) { foo(); }",
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("while(true) { foo(); }")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)
+ ));
+ }
+
+ [Fact]
+ public void SwitchStatement()
+ {
+ ParseBlockTest("@switch(foo) { foo(); }",
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("switch(foo) { foo(); }")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)
+ ));
+ }
+
+ [Fact]
+ public void LockStatement()
+ {
+ ParseBlockTest("@lock(baz) { foo(); }",
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("lock(baz) { foo(); }")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)
+ ));
+ }
+
+ [Fact]
+ public void IfStatement()
+ {
+ ParseBlockTest("@if(true) { foo(); }",
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("if(true) { foo(); }")
+ .AsStatement()
+ ));
+ }
+
+ [Fact]
+ public void ElseIfClause()
+ {
+ ParseBlockTest("@if(true) { foo(); } else if(false) { foo(); } else if(!false) { foo(); }",
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("if(true) { foo(); } else if(false) { foo(); } else if(!false) { foo(); }")
+ .AsStatement()
+ ));
+ }
+
+ [Fact]
+ public void ElseClause()
+ {
+ ParseBlockTest("@if(true) { foo(); } else { foo(); }",
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("if(true) { foo(); } else { foo(); }")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)
+ ));
+ }
+
+ [Fact]
+ public void TryStatement()
+ {
+ ParseBlockTest("@try { foo(); }",
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("try { foo(); }")
+ .AsStatement()
+ ));
+ }
+
+ [Fact]
+ public void CatchClause()
+ {
+ ParseBlockTest("@try { foo(); } catch(IOException ioex) { handleIO(); } catch(Exception ex) { handleOther(); }",
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("try { foo(); } catch(IOException ioex) { handleIO(); } catch(Exception ex) { handleOther(); }")
+ .AsStatement()
+ ));
+ }
+
+ [Fact]
+ public void FinallyClause()
+ {
+ ParseBlockTest("@try { foo(); } finally { Dispose(); }",
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("try { foo(); } finally { Dispose(); }")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)
+ ));
+ }
+
+ [Fact]
+ public void UsingStatement()
+ {
+ ParseBlockTest("@using(var foo = new Foo()) { foo.Bar(); }",
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("using(var foo = new Foo()) { foo.Bar(); }")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)
+ ));
+ }
+
+ [Fact]
+ public void UsingTypeAlias()
+ {
+ ParseBlockTest("@using StringDictionary = System.Collections.Generic.Dictionary<string, string>",
+ new DirectiveBlock(
+ Factory.CodeTransition(),
+ Factory.Code("using StringDictionary = System.Collections.Generic.Dictionary<string, string>")
+ .AsNamespaceImport(" StringDictionary = System.Collections.Generic.Dictionary<string, string>", 5)
+ .Accepts(AcceptedCharacters.AnyExceptNewline)
+ ));
+ }
+
+ [Fact]
+ public void UsingNamespaceImport()
+ {
+ ParseBlockTest("@using System.Text.Encoding.ASCIIEncoding",
+ new DirectiveBlock(
+ Factory.CodeTransition(),
+ Factory.Code("using System.Text.Encoding.ASCIIEncoding")
+ .AsNamespaceImport(" System.Text.Encoding.ASCIIEncoding", 5)
+ .Accepts(AcceptedCharacters.AnyExceptNewline)
+ ));
+ }
+
+ [Fact]
+ public void DoStatement()
+ {
+ ParseBlockTest("@do { foo(); } while(true);",
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("do { foo(); } while(true);")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)
+ ));
+ }
+
+ [Fact]
+ public void NonBlockKeywordTreatedAsImplicitExpression()
+ {
+ ParseBlockTest("@is foo",
+ new ExpressionBlock(new ExpressionCodeGenerator(),
+ Factory.CodeTransition(),
+ Factory.Code("is")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)
+ ));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/CSharp/CSharpTemplateTest.cs b/test/System.Web.Razor.Test/Parser/CSharp/CSharpTemplateTest.cs
new file mode 100644
index 00000000..4b797d3e
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/CSharp/CSharpTemplateTest.cs
@@ -0,0 +1,258 @@
+using System.Web.Razor.Editor;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Test.Framework;
+using System.Web.Razor.Text;
+using System.Web.Razor.Tokenizer.Symbols;
+using Xunit;
+
+namespace System.Web.Razor.Test.Parser.CSharp
+{
+ public class CSharpTemplateTest : CsHtmlCodeParserTestBase
+ {
+ private const string TestTemplateCode = " @<p>Foo #@item</p>";
+
+ private TemplateBlock TestTemplate()
+ {
+ return new TemplateBlock(
+ new MarkupBlock(
+ Factory.MarkupTransition(),
+ Factory.Markup("<p>Foo #"),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("item")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)
+ ),
+ Factory.Markup("</p>").Accepts(AcceptedCharacters.None)
+ )
+ );
+ }
+
+ private const string TestNestedTemplateCode = " @<p>Foo #@Html.Repeat(10, @<p>@item</p>)</p>";
+
+ private TemplateBlock TestNestedTemplate()
+ {
+ return new TemplateBlock(
+ new MarkupBlock(
+ Factory.MarkupTransition(),
+ Factory.Markup("<p>Foo #"),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("Html.Repeat(10, ")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords),
+ new TemplateBlock(
+ new MarkupBlock(
+ Factory.MarkupTransition(),
+ Factory.Markup("<p>"),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("item")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)
+ ),
+ Factory.Markup("</p>").Accepts(AcceptedCharacters.None)
+ )
+ ),
+ Factory.Code(")")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)
+ ),
+ Factory.Markup("</p>").Accepts(AcceptedCharacters.None)
+ )
+ );
+ }
+
+ [Fact]
+ public void ParseBlockHandlesSingleLineTemplate()
+ {
+ ParseBlockTest(@"{ var foo = @: bar
+; }",
+ new StatementBlock(
+ Factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ Factory.Code(" var foo = ").AsStatement(),
+ new TemplateBlock(
+ new MarkupBlock(
+ Factory.MarkupTransition(),
+ Factory.MetaMarkup(":", HtmlSymbolType.Colon),
+ Factory.Markup(" bar\r\n")
+ .With(new SingleLineMarkupEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString))
+ .Accepts(AcceptedCharacters.None)
+ )
+ ),
+ Factory.Code("; ").AsStatement(),
+ Factory.MetaCode("}").Accepts(AcceptedCharacters.None)
+ ));
+ }
+
+ [Fact]
+ public void ParseBlockHandlesSingleLineImmediatelyFollowingStatementChar()
+ {
+ ParseBlockTest(@"{i@: bar
+}",
+ new StatementBlock(
+ Factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ Factory.Code("i").AsStatement(),
+ new TemplateBlock(
+ new MarkupBlock(
+ Factory.MarkupTransition(),
+ Factory.MetaMarkup(":", HtmlSymbolType.Colon),
+ Factory.Markup(" bar\r\n")
+ .With(new SingleLineMarkupEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString))
+ .Accepts(AcceptedCharacters.None)
+ )
+ ),
+ Factory.EmptyCSharp().AsStatement(),
+ Factory.MetaCode("}").Accepts(AcceptedCharacters.None)
+ ));
+ }
+
+ [Fact]
+ public void ParseBlockHandlesSimpleTemplateInExplicitExpressionParens()
+ {
+ ParseBlockTest("(Html.Repeat(10," + TestTemplateCode + "))",
+ new ExpressionBlock(
+ Factory.MetaCode("(").Accepts(AcceptedCharacters.None),
+ Factory.Code("Html.Repeat(10, ").AsExpression(),
+ TestTemplate(),
+ Factory.Code(")").AsExpression(),
+ Factory.MetaCode(")").Accepts(AcceptedCharacters.None)
+ ));
+ }
+
+ [Fact]
+ public void ParseBlockHandlesSimpleTemplateInImplicitExpressionParens()
+ {
+ ParseBlockTest("Html.Repeat(10," + TestTemplateCode + ")",
+ new ExpressionBlock(
+ Factory.Code("Html.Repeat(10, ")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords),
+ TestTemplate(),
+ Factory.Code(")")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)
+ ));
+ }
+
+ [Fact]
+ public void ParseBlockHandlesTwoTemplatesInImplicitExpressionParens()
+ {
+ ParseBlockTest("Html.Repeat(10," + TestTemplateCode + "," + TestTemplateCode + ")",
+ new ExpressionBlock(
+ Factory.Code("Html.Repeat(10, ")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords),
+ TestTemplate(),
+ Factory.Code(", ")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords),
+ TestTemplate(),
+ Factory.Code(")")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharacters.NonWhiteSpace)
+ ));
+ }
+
+ [Fact]
+ public void ParseBlockProducesErrorButCorrectlyParsesNestedTemplateInImplicitExpressionParens()
+ {
+ ParseBlockTest("Html.Repeat(10," + TestNestedTemplateCode + ")",
+ new ExpressionBlock(
+ Factory.Code("Html.Repeat(10, ")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords),
+ TestNestedTemplate(),
+ Factory.Code(")")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)
+ ),
+ GetNestedTemplateError(42));
+ }
+
+ [Fact]
+ public void ParseBlockHandlesSimpleTemplateInStatementWithinCodeBlock()
+ {
+ ParseBlockTest("foreach(foo in Bar) { Html.ExecuteTemplate(foo," + TestTemplateCode + "); }",
+ new StatementBlock(
+ Factory.Code("foreach(foo in Bar) { Html.ExecuteTemplate(foo, ").AsStatement(),
+ TestTemplate(),
+ Factory.Code("); }")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)
+ ));
+ }
+
+ [Fact]
+ public void ParseBlockHandlesTwoTemplatesInStatementWithinCodeBlock()
+ {
+ ParseBlockTest("foreach(foo in Bar) { Html.ExecuteTemplate(foo," + TestTemplateCode + "," + TestTemplateCode + "); }",
+ new StatementBlock(
+ Factory.Code("foreach(foo in Bar) { Html.ExecuteTemplate(foo, ").AsStatement(),
+ TestTemplate(),
+ Factory.Code(", ").AsStatement(),
+ TestTemplate(),
+ Factory.Code("); }")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)
+ ));
+ }
+
+ [Fact]
+ public void ParseBlockProducesErrorButCorrectlyParsesNestedTemplateInStatementWithinCodeBlock()
+ {
+ ParseBlockTest("foreach(foo in Bar) { Html.ExecuteTemplate(foo," + TestNestedTemplateCode + "); }",
+ new StatementBlock(
+ Factory.Code("foreach(foo in Bar) { Html.ExecuteTemplate(foo, ").AsStatement(),
+ TestNestedTemplate(),
+ Factory.Code("); }")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)
+ ),
+ GetNestedTemplateError(74));
+ }
+
+ [Fact]
+ public void ParseBlockHandlesSimpleTemplateInStatementWithinStatementBlock()
+ {
+ ParseBlockTest("{ var foo = bar; Html.ExecuteTemplate(foo," + TestTemplateCode + "); }",
+ new StatementBlock(
+ Factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ Factory.Code(" var foo = bar; Html.ExecuteTemplate(foo, ").AsStatement(),
+ TestTemplate(),
+ Factory.Code("); ").AsStatement(),
+ Factory.MetaCode("}").Accepts(AcceptedCharacters.None)
+ ));
+ }
+
+ [Fact]
+ public void ParseBlockHandlessTwoTemplatesInStatementWithinStatementBlock()
+ {
+ ParseBlockTest("{ var foo = bar; Html.ExecuteTemplate(foo," + TestTemplateCode + "," + TestTemplateCode + "); }",
+ new StatementBlock(
+ Factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ Factory.Code(" var foo = bar; Html.ExecuteTemplate(foo, ").AsStatement(),
+ TestTemplate(),
+ Factory.Code(", ").AsStatement(),
+ TestTemplate(),
+ Factory.Code("); ").AsStatement(),
+ Factory.MetaCode("}").Accepts(AcceptedCharacters.None)
+ ));
+ }
+
+ [Fact]
+ public void ParseBlockProducesErrorButCorrectlyParsesNestedTemplateInStatementWithinStatementBlock()
+ {
+ ParseBlockTest("{ var foo = bar; Html.ExecuteTemplate(foo," + TestNestedTemplateCode + "); }",
+ new StatementBlock(
+ Factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ Factory.Code(" var foo = bar; Html.ExecuteTemplate(foo, ").AsStatement(),
+ TestNestedTemplate(),
+ Factory.Code("); ").AsStatement(),
+ Factory.MetaCode("}").Accepts(AcceptedCharacters.None)
+ ),
+ GetNestedTemplateError(69));
+ }
+
+ private static RazorError GetNestedTemplateError(int characterIndex)
+ {
+ return new RazorError(RazorResources.ParseError_InlineMarkup_Blocks_Cannot_Be_Nested, new SourceLocation(characterIndex, 0, characterIndex));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/CSharp/CSharpToMarkupSwitchTest.cs b/test/System.Web.Razor.Test/Parser/CSharp/CSharpToMarkupSwitchTest.cs
new file mode 100644
index 00000000..c84ff737
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/CSharp/CSharpToMarkupSwitchTest.cs
@@ -0,0 +1,491 @@
+using System.Web.Razor.Editor;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Test.Framework;
+using System.Web.Razor.Tokenizer.Symbols;
+using Xunit;
+
+namespace System.Web.Razor.Test.Parser.CSharp
+{
+ public class CSharpToMarkupSwitchTest : CsHtmlCodeParserTestBase
+ {
+ [Fact]
+ public void SingleAngleBracketDoesNotCauseSwitchIfOuterBlockIsTerminated()
+ {
+ ParseBlockTest("{ List< }",
+ new StatementBlock(
+ Factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ Factory.Code(" List< ").AsStatement(),
+ Factory.MetaCode("}").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void ParseBlockGivesSpacesToCodeOnAtTagTemplateTransitionInDesignTimeMode()
+ {
+ ParseBlockTest(@"Foo( @<p>Foo</p> )",
+ new ExpressionBlock(
+ Factory.Code(@"Foo( ")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.Any),
+ new TemplateBlock(
+ new MarkupBlock(
+ Factory.MarkupTransition(),
+ Factory.Markup("<p>Foo</p>").Accepts(AcceptedCharacters.None)
+ )
+ ),
+ Factory.Code(@" )")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)
+ ), designTimeParser: true);
+ }
+
+ [Fact]
+ public void ParseBlockGivesSpacesToCodeOnAtColonTemplateTransitionInDesignTimeMode()
+ {
+ ParseBlockTest(@"Foo(
+@:<p>Foo</p>
+)",
+ new ExpressionBlock(
+ Factory.Code("Foo( \r\n").AsImplicitExpression(CSharpCodeParser.DefaultKeywords),
+ new TemplateBlock(
+ new MarkupBlock(
+ Factory.MarkupTransition(),
+ Factory.MetaMarkup(":", HtmlSymbolType.Colon),
+ Factory.Markup("<p>Foo</p> \r\n")
+ .With(new SingleLineMarkupEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString, AcceptedCharacters.None))
+ )
+ ),
+ Factory.Code(@")")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)
+ ), designTimeParser: true);
+ }
+
+ [Fact]
+ public void ParseBlockGivesSpacesToCodeOnTagTransitionInDesignTimeMode()
+ {
+ ParseBlockTest(@"{
+ <p>Foo</p>
+}",
+ new StatementBlock(
+ Factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ Factory.Code("\r\n ").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup("<p>Foo</p>").Accepts(AcceptedCharacters.None)
+ ),
+ Factory.Code(" \r\n").AsStatement(),
+ Factory.MetaCode("}").Accepts(AcceptedCharacters.None)
+ ), designTimeParser: true);
+ }
+
+ [Fact]
+ public void ParseBlockGivesSpacesToCodeOnInvalidAtTagTransitionInDesignTimeMode()
+ {
+ ParseBlockTest(@"{
+ @<p>Foo</p>
+}",
+ new StatementBlock(
+ Factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ Factory.Code("\r\n ").AsStatement(),
+ new MarkupBlock(
+ Factory.MarkupTransition(),
+ Factory.Markup("<p>Foo</p>").Accepts(AcceptedCharacters.None)
+ ),
+ Factory.Code(" \r\n").AsStatement(),
+ Factory.MetaCode("}").Accepts(AcceptedCharacters.None)
+ ), true,
+ new RazorError(RazorResources.ParseError_AtInCode_Must_Be_Followed_By_Colon_Paren_Or_Identifier_Start, 7, 1, 4));
+ }
+
+ [Fact]
+ public void ParseBlockGivesSpacesToCodeOnAtColonTransitionInDesignTimeMode()
+ {
+ ParseBlockTest(@"{
+ @:<p>Foo</p>
+}",
+ new StatementBlock(
+ Factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ Factory.Code("\r\n ").AsStatement(),
+ new MarkupBlock(
+ Factory.MarkupTransition(),
+ Factory.MetaMarkup(":", HtmlSymbolType.Colon),
+ Factory.Markup("<p>Foo</p> \r\n")
+ .With(new SingleLineMarkupEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString, AcceptedCharacters.None))
+ ),
+ Factory.EmptyCSharp().AsStatement(),
+ Factory.MetaCode("}").Accepts(AcceptedCharacters.None)
+ ), designTimeParser: true);
+ }
+
+ [Fact]
+ public void ParseBlockShouldSupportSingleLineMarkupContainingStatementBlock()
+ {
+ ParseBlockTest(@"Repeat(10,
+ @: @{}
+)",
+ new ExpressionBlock(
+ Factory.Code("Repeat(10,\r\n ")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords),
+ new TemplateBlock(
+ new MarkupBlock(
+ Factory.MarkupTransition(),
+ Factory.MetaMarkup(":", HtmlSymbolType.Colon),
+ Factory.Markup(" ")
+ .With(new SingleLineMarkupEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString)),
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ Factory.EmptyCSharp().AsStatement(),
+ Factory.MetaCode("}").Accepts(AcceptedCharacters.None)
+ ),
+ Factory.Markup("\r\n")
+ .With(new SingleLineMarkupEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString, AcceptedCharacters.None))
+ )
+ ),
+ Factory.Code(")")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)
+ ));
+ }
+
+ [Fact]
+ public void ParseBlockShouldSupportMarkupWithoutPreceedingWhitespace()
+ {
+ ParseBlockTest(@"foreach(var file in files){
+
+
+@:Baz
+<br/>
+<a>Foo</a>
+@:Bar
+}",
+ new StatementBlock(
+ Factory.Code("foreach(var file in files){\r\n\r\n\r\n").AsStatement(),
+ new MarkupBlock(
+ Factory.MarkupTransition(),
+ Factory.MetaMarkup(":", HtmlSymbolType.Colon),
+ Factory.Markup("Baz\r\n")
+ .With(new SingleLineMarkupEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString, AcceptedCharacters.None))
+ ),
+ new MarkupBlock(
+ Factory.Markup("<br/>\r\n")
+ .Accepts(AcceptedCharacters.None)
+ ),
+ new MarkupBlock(
+ Factory.Markup("<a>Foo</a>\r\n")
+ .Accepts(AcceptedCharacters.None)
+ ),
+ new MarkupBlock(
+ Factory.MarkupTransition(),
+ Factory.MetaMarkup(":", HtmlSymbolType.Colon),
+ Factory.Markup("Bar\r\n")
+ .With(new SingleLineMarkupEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString, AcceptedCharacters.None))
+ ),
+ Factory.Code("}").AsStatement().Accepts(AcceptedCharacters.None)
+ ));
+ }
+
+ [Fact]
+ public void ParseBlockGivesAllWhitespaceOnSameLineExcludingPreceedingNewlineButIncludingTrailingNewLineToMarkup()
+ {
+ ParseBlockTest(@"if(foo) {
+ var foo = ""After this statement there are 10 spaces"";
+ <p>
+ Foo
+ @bar
+ </p>
+ @:Hello!
+ var biz = boz;
+}",
+ new StatementBlock(
+ Factory.Code("if(foo) {\r\n var foo = \"After this statement there are 10 spaces\"; \r\n").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" <p>\r\n Foo\r\n"),
+ new ExpressionBlock(
+ Factory.Code(" ").AsStatement(),
+ Factory.CodeTransition(),
+ Factory.Code(@"bar").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharacters.NonWhiteSpace)
+ ),
+ Factory.Markup("\r\n </p>\r\n").Accepts(AcceptedCharacters.None)
+ ),
+ new MarkupBlock(
+ Factory.Markup(@" "),
+ Factory.MarkupTransition(),
+ Factory.MetaMarkup(":", HtmlSymbolType.Colon),
+ Factory.Markup("Hello!\r\n").With(new SingleLineMarkupEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString, AcceptedCharacters.None))
+ ),
+ Factory.Code(" var biz = boz;\r\n}").AsStatement()));
+ }
+
+ [Fact]
+ public void ParseBlockAllowsMarkupInIfBodyWithBraces()
+ {
+ ParseBlockTest("if(foo) { <p>Bar</p> } else if(bar) { <p>Baz</p> } else { <p>Boz</p> }",
+ new StatementBlock(
+ Factory.Code("if(foo) {").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" <p>Bar</p> ").Accepts(AcceptedCharacters.None)
+ ),
+ Factory.Code("} else if(bar) {").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" <p>Baz</p> ").Accepts(AcceptedCharacters.None)
+ ),
+ Factory.Code("} else {").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" <p>Boz</p> ").Accepts(AcceptedCharacters.None)
+ ),
+ Factory.Code("}").AsStatement().Accepts(AcceptedCharacters.None)
+ ));
+ }
+
+ [Fact]
+ public void ParseBlockAllowsMarkupInIfBodyWithBracesWithinCodeBlock()
+ {
+ ParseBlockTest("{ if(foo) { <p>Bar</p> } else if(bar) { <p>Baz</p> } else { <p>Boz</p> } }",
+ new StatementBlock(
+ Factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ Factory.Code(" if(foo) {").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" <p>Bar</p> ").Accepts(AcceptedCharacters.None)
+ ),
+ Factory.Code("} else if(bar) {").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" <p>Baz</p> ").Accepts(AcceptedCharacters.None)
+ ),
+ Factory.Code("} else {").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" <p>Boz</p> ").Accepts(AcceptedCharacters.None)
+ ),
+ Factory.Code("} ").AsStatement(),
+ Factory.MetaCode("}").Accepts(AcceptedCharacters.None)
+ ));
+ }
+
+ [Fact]
+ public void ParseBlockSupportsMarkupInCaseAndDefaultBranchesOfSwitch()
+ {
+ // Arrange
+ ParseBlockTest(@"switch(foo) {
+ case 0:
+ <p>Foo</p>
+ break;
+ case 1:
+ <p>Bar</p>
+ return;
+ case 2:
+ {
+ <p>Baz</p>
+ <p>Boz</p>
+ }
+ default:
+ <p>Biz</p>
+}",
+ new StatementBlock(
+ Factory.Code("switch(foo) {\r\n case 0:\r\n").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" <p>Foo</p>\r\n").Accepts(AcceptedCharacters.None)
+ ),
+ Factory.Code(" break;\r\n case 1:\r\n").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" <p>Bar</p>\r\n").Accepts(AcceptedCharacters.None)
+ ),
+ Factory.Code(" return;\r\n case 2:\r\n {\r\n").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" <p>Baz</p>\r\n").Accepts(AcceptedCharacters.None)
+ ),
+ new MarkupBlock(
+ Factory.Markup(" <p>Boz</p>\r\n").Accepts(AcceptedCharacters.None)
+ ),
+ Factory.Code(" }\r\n default:\r\n").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" <p>Biz</p>\r\n").Accepts(AcceptedCharacters.None)
+ ),
+ Factory.Code("}").AsStatement().Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void ParseBlockSupportsMarkupInCaseAndDefaultBranchesOfSwitchInCodeBlock()
+ {
+ // Arrange
+ ParseBlockTest(@"{ switch(foo) {
+ case 0:
+ <p>Foo</p>
+ break;
+ case 1:
+ <p>Bar</p>
+ return;
+ case 2:
+ {
+ <p>Baz</p>
+ <p>Boz</p>
+ }
+ default:
+ <p>Biz</p>
+} }",
+ new StatementBlock(
+ Factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ Factory.Code(" switch(foo) {\r\n case 0:\r\n").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" <p>Foo</p>\r\n").Accepts(AcceptedCharacters.None)
+ ),
+ Factory.Code(" break;\r\n case 1:\r\n").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" <p>Bar</p>\r\n").Accepts(AcceptedCharacters.None)
+ ),
+ Factory.Code(" return;\r\n case 2:\r\n {\r\n").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" <p>Baz</p>\r\n").Accepts(AcceptedCharacters.None)
+ ),
+ new MarkupBlock(
+ Factory.Markup(" <p>Boz</p>\r\n").Accepts(AcceptedCharacters.None)
+ ),
+ Factory.Code(" }\r\n default:\r\n").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" <p>Biz</p>\r\n").Accepts(AcceptedCharacters.None)
+ ),
+ Factory.Code("} ").AsStatement(),
+ Factory.MetaCode("}").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void ParseBlockParsesMarkupStatementOnOpenAngleBracket()
+ {
+ ParseBlockTest("for(int i = 0; i < 10; i++) { <p>Foo</p> }",
+ new StatementBlock(
+ Factory.Code("for(int i = 0; i < 10; i++) {").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" <p>Foo</p> ").Accepts(AcceptedCharacters.None)
+ ),
+ Factory.Code("}").AsStatement().Accepts(AcceptedCharacters.None)
+ ));
+ }
+
+ [Fact]
+ public void ParseBlockParsesMarkupStatementOnOpenAngleBracketInCodeBlock()
+ {
+ ParseBlockTest("{ for(int i = 0; i < 10; i++) { <p>Foo</p> } }",
+ new StatementBlock(
+ Factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ Factory.Code(" for(int i = 0; i < 10; i++) {").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" <p>Foo</p> ").Accepts(AcceptedCharacters.None)
+ ),
+ Factory.Code("} ").AsStatement(),
+ Factory.MetaCode("}").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void ParseBlockParsesMarkupStatementOnSwitchCharacterFollowedByColon()
+ {
+ // Arrange
+ ParseBlockTest(@"if(foo) { @:Bar
+} zoop",
+ new StatementBlock(
+ Factory.Code("if(foo) {").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" "),
+ Factory.MarkupTransition(),
+ Factory.MetaMarkup(":", HtmlSymbolType.Colon),
+ Factory.Markup("Bar\r\n").With(new SingleLineMarkupEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString, AcceptedCharacters.None))
+ ),
+ Factory.Code("}").AsStatement()));
+ }
+
+ [Fact]
+ public void ParseBlockParsesMarkupStatementOnSwitchCharacterFollowedByColonInCodeBlock()
+ {
+ // Arrange
+ ParseBlockTest(@"{ if(foo) { @:Bar
+} } zoop",
+ new StatementBlock(
+ Factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ Factory.Code(" if(foo) {").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" "),
+ Factory.MarkupTransition(),
+ Factory.MetaMarkup(":", HtmlSymbolType.Colon),
+ Factory.Markup("Bar\r\n").Accepts(AcceptedCharacters.None)
+ ),
+ Factory.Code("} ").AsStatement(),
+ Factory.MetaCode("}").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void ParseBlockCorrectlyReturnsFromMarkupBlockWithPseudoTag()
+ {
+ ParseBlockTest(@"if (i > 0) { <text>;</text> }",
+ new StatementBlock(
+ Factory.Code(@"if (i > 0) {").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" "),
+ Factory.MarkupTransition("<text>").Accepts(AcceptedCharacters.None),
+ Factory.Markup(";"),
+ Factory.MarkupTransition("</text>").Accepts(AcceptedCharacters.None),
+ Factory.Markup(" ").Accepts(AcceptedCharacters.None)
+ ),
+ Factory.Code(@"}").AsStatement()));
+ }
+
+ [Fact]
+ public void ParseBlockCorrectlyReturnsFromMarkupBlockWithPseudoTagInCodeBlock()
+ {
+ ParseBlockTest(@"{ if (i > 0) { <text>;</text> } }",
+ new StatementBlock(
+ Factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ Factory.Code(@" if (i > 0) {").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" "),
+ Factory.MarkupTransition("<text>").Accepts(AcceptedCharacters.None),
+ Factory.Markup(";"),
+ Factory.MarkupTransition("</text>").Accepts(AcceptedCharacters.None),
+ Factory.Markup(" ").Accepts(AcceptedCharacters.None)
+ ),
+ Factory.Code(@"} ").AsStatement(),
+ Factory.MetaCode("}").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void ParseBlockSupportsAllKindsOfImplicitMarkupInCodeBlock()
+ {
+ ParseBlockTest(@"{
+ if(true) {
+ @:Single Line Markup
+ }
+ foreach (var p in Enumerable.Range(1, 10)) {
+ <text>The number is @p</text>
+ }
+ if(!false) {
+ <p>A real tag!</p>
+ }
+}",
+ new StatementBlock(
+ Factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ Factory.Code("\r\n if(true) {\r\n").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" "),
+ Factory.MarkupTransition(),
+ Factory.MetaMarkup(":", HtmlSymbolType.Colon),
+ Factory.Markup("Single Line Markup\r\n").With(new SingleLineMarkupEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString, AcceptedCharacters.None))
+ ),
+ Factory.Code(" }\r\n foreach (var p in Enumerable.Range(1, 10)) {\r\n").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(@" "),
+ Factory.MarkupTransition("<text>").Accepts(AcceptedCharacters.None),
+ Factory.Markup("The number is "),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("p").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharacters.NonWhiteSpace)
+ ),
+ Factory.MarkupTransition("</text>").Accepts(AcceptedCharacters.None),
+ Factory.Markup("\r\n").Accepts(AcceptedCharacters.None)
+ ),
+ Factory.Code(" }\r\n if(!false) {\r\n").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" <p>A real tag!</p>\r\n").Accepts(AcceptedCharacters.None)
+ ),
+ Factory.Code(" }\r\n").AsStatement(),
+ Factory.MetaCode("}").Accepts(AcceptedCharacters.None)));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/CSharp/CSharpVerbatimBlockTest.cs b/test/System.Web.Razor.Test/Parser/CSharp/CSharpVerbatimBlockTest.cs
new file mode 100644
index 00000000..dcc20cba
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/CSharp/CSharpVerbatimBlockTest.cs
@@ -0,0 +1,118 @@
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Test.Framework;
+using System.Web.Razor.Text;
+using Xunit;
+
+namespace System.Web.Razor.Test.Parser.CSharp
+{
+ public class CSharpVerbatimBlockTest : CsHtmlCodeParserTestBase
+ {
+ private const string TestExtraKeyword = "model";
+
+ [Fact]
+ public void VerbatimBlock()
+ {
+ ParseBlockTest("@{ foo(); }",
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("{")
+ .Accepts(AcceptedCharacters.None),
+ Factory.Code(" foo(); ")
+ .AsStatement(),
+ Factory.MetaCode("}")
+ .Accepts(AcceptedCharacters.None)
+ ));
+ }
+
+ [Fact]
+ public void InnerImplicitExpressionWithOnlySingleAtOutputsZeroLengthCodeSpan()
+ {
+ ParseBlockTest(@"{@}",
+ new StatementBlock(
+ Factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ Factory.EmptyCSharp().AsStatement(),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.EmptyCSharp().AsImplicitExpression(KeywordSet, acceptTrailingDot: true).Accepts(AcceptedCharacters.NonWhiteSpace)
+ ),
+ Factory.EmptyCSharp().AsStatement(),
+ Factory.MetaCode("}").Accepts(AcceptedCharacters.None)),
+ designTimeParser: true,
+ expectedErrors: new[]
+ {
+ new RazorError(String.Format(RazorResources.ParseError_Unexpected_Character_At_Start_Of_CodeBlock_CS, "}"), new SourceLocation(2, 0, 2))
+ });
+ }
+
+ [Fact]
+ public void InnerImplicitExpressionDoesNotAcceptDotAfterAt()
+ {
+ ParseBlockTest(@"{@.}",
+ new StatementBlock(
+ Factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ Factory.EmptyCSharp().AsStatement(),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.EmptyCSharp().AsImplicitExpression(KeywordSet, acceptTrailingDot: true).Accepts(AcceptedCharacters.NonWhiteSpace)
+ ),
+ Factory.Code(".").AsStatement(),
+ Factory.MetaCode("}").Accepts(AcceptedCharacters.None)),
+ designTimeParser: true,
+ expectedErrors: new[]
+ {
+ new RazorError(String.Format(RazorResources.ParseError_Unexpected_Character_At_Start_Of_CodeBlock_CS, "."), new SourceLocation(2, 0, 2))
+ });
+ }
+
+ [Fact]
+ public void InnerImplicitExpressionWithOnlySingleAtAcceptsSingleSpaceOrNewlineAtDesignTime()
+ {
+ ParseBlockTest(@"{
+ @
+}",
+ new StatementBlock(
+ Factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ Factory.Code("\r\n ").AsStatement(),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.EmptyCSharp().AsImplicitExpression(KeywordSet, acceptTrailingDot: true).Accepts(AcceptedCharacters.NonWhiteSpace)
+ ),
+ Factory.Code("\r\n").AsStatement(),
+ Factory.MetaCode("}").Accepts(AcceptedCharacters.None)),
+ /* designTimeParser */ true,
+ new RazorError(RazorResources.ParseError_Unexpected_WhiteSpace_At_Start_Of_CodeBlock_CS, 8, 1, 5));
+ }
+
+ [Fact]
+ public void InnerImplicitExpressionDoesNotAcceptTrailingNewlineInRunTimeMode()
+ {
+ ParseBlockTest(@"{@foo.
+}",
+ new StatementBlock(
+ Factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ Factory.EmptyCSharp().AsStatement(),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code(@"foo.").AsImplicitExpression(KeywordSet, acceptTrailingDot: true).Accepts(AcceptedCharacters.NonWhiteSpace)),
+ Factory.Code("\r\n").AsStatement(),
+ Factory.MetaCode("}").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void InnerImplicitExpressionAcceptsTrailingNewlineInDesignTimeMode()
+ {
+ ParseBlockTest(@"{@foo.
+}",
+ new StatementBlock(
+ Factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ Factory.EmptyCSharp().AsStatement(),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code(@"foo.").AsImplicitExpression(KeywordSet, acceptTrailingDot: true).Accepts(AcceptedCharacters.NonWhiteSpace)),
+ Factory.Code("\r\n").AsStatement(),
+ Factory.MetaCode("}").Accepts(AcceptedCharacters.None)),
+ designTimeParser: true);
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/CSharp/CSharpWhitespaceHandlingTest.cs b/test/System.Web.Razor.Test/Parser/CSharp/CSharpWhitespaceHandlingTest.cs
new file mode 100644
index 00000000..92a5c2b9
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/CSharp/CSharpWhitespaceHandlingTest.cs
@@ -0,0 +1,30 @@
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Test.Framework;
+using System.Web.Razor.Tokenizer.Symbols;
+using Xunit;
+
+namespace System.Web.Razor.Test.Parser.CSharp
+{
+ public class CSharpWhitespaceHandlingTest : CsHtmlMarkupParserTestBase
+ {
+ [Fact]
+ public void StatementBlockDoesNotAcceptTrailingNewlineIfNewlinesAreSignificantToAncestor()
+ {
+ ParseBlockTest(@"@: @if (true) { }
+}",
+ new MarkupBlock(
+ Factory.MarkupTransition()
+ .Accepts(AcceptedCharacters.None),
+ Factory.MetaMarkup(":", HtmlSymbolType.Colon),
+ Factory.Markup(" "),
+ new StatementBlock(
+ Factory.CodeTransition()
+ .Accepts(AcceptedCharacters.None),
+ Factory.Code("if (true) { }")
+ .AsStatement()
+ ),
+ Factory.Markup("\r\n")
+ .Accepts(AcceptedCharacters.None)));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/CSharp/CsHtmlDocumentTest.cs b/test/System.Web.Razor.Test/Parser/CSharp/CsHtmlDocumentTest.cs
new file mode 100644
index 00000000..b7c17d70
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/CSharp/CsHtmlDocumentTest.cs
@@ -0,0 +1,286 @@
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Test.Framework;
+using System.Web.Razor.Text;
+using System.Web.Razor.Tokenizer.Symbols;
+using Xunit;
+
+namespace System.Web.Razor.Test.Parser.CSharp
+{
+ public class CsHtmlDocumentTest : CsHtmlMarkupParserTestBase
+ {
+ [Fact]
+ public void UnterminatedBlockCommentCausesRazorError()
+ {
+ ParseDocumentTest(@"@* Foo Bar",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new CommentBlock(
+ Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition),
+ Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None),
+ Factory.Comment(" Foo Bar", HtmlSymbolType.RazorComment)
+ )
+ ),
+ new RazorError(RazorResources.ParseError_RazorComment_Not_Terminated, SourceLocation.Zero));
+ }
+
+ [Fact]
+ public void BlockCommentInMarkupDocumentIsHandledCorrectly()
+ {
+ ParseDocumentTest(@"<ul>
+ @* This is a block comment </ul> *@ foo",
+ new MarkupBlock(
+ Factory.Markup("<ul>\r\n "),
+ new CommentBlock(
+ Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition),
+ Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None),
+ Factory.Comment(" This is a block comment </ul> ", HtmlSymbolType.RazorComment),
+ Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None),
+ Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition)
+ ),
+ Factory.Markup(" foo")
+ ));
+ }
+
+ [Fact]
+ public void BlockCommentInMarkupBlockIsHandledCorrectly()
+ {
+ ParseBlockTest(@"<ul>
+ @* This is a block comment </ul> *@ foo </ul>",
+ new MarkupBlock(
+ Factory.Markup("<ul>\r\n "),
+ new CommentBlock(
+ Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition),
+ Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None),
+ Factory.Comment(" This is a block comment </ul> ", HtmlSymbolType.RazorComment),
+ Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None),
+ Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition)
+ ),
+ Factory.Markup(" foo </ul>").Accepts(AcceptedCharacters.None)
+ ));
+ }
+
+ [Fact]
+ public void BlockCommentAtStatementStartInCodeBlockIsHandledCorrectly()
+ {
+ ParseDocumentTest(@"@if(Request.IsAuthenticated) {
+ @* User is logged in! } *@
+ Write(""Hello friend!"");
+}",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("if(Request.IsAuthenticated) {\r\n ").AsStatement(),
+ new CommentBlock(
+ Factory.CodeTransition(CSharpSymbolType.RazorCommentTransition),
+ Factory.MetaCode("*", CSharpSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None),
+ Factory.Comment(" User is logged in! } ", CSharpSymbolType.RazorComment),
+ Factory.MetaCode("*", CSharpSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None),
+ Factory.CodeTransition(CSharpSymbolType.RazorCommentTransition)
+ ),
+ Factory.Code("\r\n Write(\"Hello friend!\");\r\n}").AsStatement())));
+ }
+
+ [Fact]
+ public void BlockCommentInStatementInCodeBlockIsHandledCorrectly()
+ {
+ ParseDocumentTest(@"@if(Request.IsAuthenticated) {
+ var foo = @* User is logged in! ; *@;
+ Write(""Hello friend!"");
+}",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("if(Request.IsAuthenticated) {\r\n var foo = ").AsStatement(),
+ new CommentBlock(
+ Factory.CodeTransition(CSharpSymbolType.RazorCommentTransition),
+ Factory.MetaCode("*", CSharpSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None),
+ Factory.Comment(" User is logged in! ; ", CSharpSymbolType.RazorComment),
+ Factory.MetaCode("*", CSharpSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None),
+ Factory.CodeTransition(CSharpSymbolType.RazorCommentTransition)
+ ),
+ Factory.Code(";\r\n Write(\"Hello friend!\");\r\n}").AsStatement())));
+ }
+
+ [Fact]
+ public void BlockCommentInStringIsIgnored()
+ {
+ ParseDocumentTest(@"@if(Request.IsAuthenticated) {
+ var foo = ""@* User is logged in! ; *@"";
+ Write(""Hello friend!"");
+}",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code(@"if(Request.IsAuthenticated) {
+ var foo = ""@* User is logged in! ; *@"";
+ Write(""Hello friend!"");
+}").AsStatement())));
+ }
+
+ [Fact]
+ public void BlockCommentInCSharpBlockCommentIsIgnored()
+ {
+ ParseDocumentTest(@"@if(Request.IsAuthenticated) {
+ var foo = /*@* User is logged in! */ *@ */;
+ Write(""Hello friend!"");
+}",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code(@"if(Request.IsAuthenticated) {
+ var foo = /*@* User is logged in! */ *@ */;
+ Write(""Hello friend!"");
+}").AsStatement())));
+ }
+
+ [Fact]
+ public void BlockCommentInCSharpLineCommentIsIgnored()
+ {
+ ParseDocumentTest(@"@if(Request.IsAuthenticated) {
+ var foo = //@* User is logged in! */ *@;
+ Write(""Hello friend!"");
+}",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code(@"if(Request.IsAuthenticated) {
+ var foo = //@* User is logged in! */ *@;
+ Write(""Hello friend!"");
+}").AsStatement())));
+ }
+
+ [Fact]
+ public void BlockCommentInImplicitExpressionIsHandledCorrectly()
+ {
+ ParseDocumentTest(@"@Html.Foo@*bar*@",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("Html.Foo").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharacters.NonWhiteSpace)
+ ),
+ Factory.EmptyHtml(),
+ new CommentBlock(
+ Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition),
+ Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None),
+ Factory.Comment("bar", HtmlSymbolType.RazorComment),
+ Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None),
+ Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition)
+ ),
+ Factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void BlockCommentAfterDotOfImplicitExpressionIsHandledCorrectly()
+ {
+ ParseDocumentTest(@"@Html.@*bar*@",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code(@"Html").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharacters.NonWhiteSpace)
+ ),
+ Factory.Markup("."),
+ new CommentBlock(
+ Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition),
+ Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None),
+ Factory.Comment("bar", HtmlSymbolType.RazorComment),
+ Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None),
+ Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition)
+ ),
+ Factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void BlockCommentInParensOfImplicitExpressionIsHandledCorrectly()
+ {
+ ParseDocumentTest(@"@Html.Foo(@*bar*@ 4)",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code(@"Html.Foo(").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharacters.Any),
+ new CommentBlock(
+ Factory.CodeTransition(CSharpSymbolType.RazorCommentTransition),
+ Factory.MetaCode("*", CSharpSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None),
+ Factory.Comment("bar", CSharpSymbolType.RazorComment),
+ Factory.MetaCode("*", CSharpSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None),
+ Factory.CodeTransition(CSharpSymbolType.RazorCommentTransition)
+ ),
+ Factory.Code(" 4)").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharacters.NonWhiteSpace)
+ ),
+ Factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void BlockCommentInBracketsOfImplicitExpressionIsHandledCorrectly()
+ {
+ ParseDocumentTest(@"@Html.Foo[@*bar*@ 4]",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code(@"Html.Foo[").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharacters.Any),
+ new CommentBlock(
+ Factory.CodeTransition(CSharpSymbolType.RazorCommentTransition),
+ Factory.MetaCode("*", CSharpSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None),
+ Factory.Comment("bar", CSharpSymbolType.RazorComment),
+ Factory.MetaCode("*", CSharpSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None),
+ Factory.CodeTransition(CSharpSymbolType.RazorCommentTransition)
+ ),
+ Factory.Code(" 4]").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharacters.NonWhiteSpace)
+ ),
+ Factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void BlockCommentInParensOfConditionIsHandledCorrectly()
+ {
+ ParseDocumentTest(@"@if(@*bar*@) {}",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code(@"if(").AsStatement(),
+ new CommentBlock(
+ Factory.CodeTransition(CSharpSymbolType.RazorCommentTransition),
+ Factory.MetaCode("*", CSharpSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None),
+ Factory.Comment("bar", CSharpSymbolType.RazorComment),
+ Factory.MetaCode("*", CSharpSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None),
+ Factory.CodeTransition(CSharpSymbolType.RazorCommentTransition)
+ ),
+ Factory.Code(") {}").AsStatement()
+ )));
+ }
+
+ [Fact]
+ public void BlockCommentInExplicitExpressionIsHandledCorrectly()
+ {
+ ParseDocumentTest(@"@(1 + @*bar*@ 1)",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("(").Accepts(AcceptedCharacters.None),
+ Factory.Code(@"1 + ").AsExpression(),
+ new CommentBlock(
+ Factory.CodeTransition(CSharpSymbolType.RazorCommentTransition),
+ Factory.MetaCode("*", CSharpSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None),
+ Factory.Comment("bar", CSharpSymbolType.RazorComment),
+ Factory.MetaCode("*", CSharpSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None),
+ Factory.CodeTransition(CSharpSymbolType.RazorCommentTransition)
+ ),
+ Factory.Code(" 1").AsExpression(),
+ Factory.MetaCode(")").Accepts(AcceptedCharacters.None)
+ ),
+ Factory.EmptyHtml()));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/CallbackParserListenerTest.cs b/test/System.Web.Razor.Test/Parser/CallbackParserListenerTest.cs
new file mode 100644
index 00000000..499a84ef
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/CallbackParserListenerTest.cs
@@ -0,0 +1,162 @@
+using System.Threading;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Test.Framework;
+using System.Web.Razor.Text;
+using Moq;
+using Xunit;
+
+namespace System.Web.Razor.Test.Parser
+{
+ public class CallbackParserListenerTest
+ {
+ [Fact]
+ public void ListenerConstructedWithSpanCallbackCallsCallbackOnEndSpan()
+ {
+ RunOnEndSpanTest(callback => new CallbackVisitor(callback));
+ }
+
+ [Fact]
+ public void ListenerConstructedWithSpanCallbackDoesNotThrowOnStartBlockEndBlockOrError()
+ {
+ // Arrange
+ Action<Span> spanCallback = _ => { };
+ CallbackVisitor listener = new CallbackVisitor(spanCallback);
+
+ // Act/Assert
+ listener.VisitStartBlock(new FunctionsBlock());
+ listener.VisitError(new RazorError("Error", SourceLocation.Zero));
+ listener.VisitEndBlock(new FunctionsBlock());
+ }
+
+ [Fact]
+ public void ListenerConstructedWithSpanAndErrorCallbackCallsCallbackOnEndSpan()
+ {
+ RunOnEndSpanTest(spanCallback => new CallbackVisitor(spanCallback, _ => { }));
+ }
+
+ [Fact]
+ public void ListenerConstructedWithSpanAndErrorCallbackCallsCallbackOnError()
+ {
+ RunOnErrorTest(errorCallback => new CallbackVisitor(_ => { }, errorCallback));
+ }
+
+ [Fact]
+ public void ListenerConstructedWithAllCallbacksCallsCallbackOnEndSpan()
+ {
+ RunOnEndSpanTest(spanCallback => new CallbackVisitor(spanCallback, _ => { }, _ => { }, _ => { }));
+ }
+
+ [Fact]
+ public void ListenerConstructedWithAllCallbacksCallsCallbackOnError()
+ {
+ RunOnErrorTest(errorCallback => new CallbackVisitor(_ => { }, errorCallback, _ => { }, _ => { }));
+ }
+
+ [Fact]
+ public void ListenerConstructedWithAllCallbacksCallsCallbackOnStartBlock()
+ {
+ RunOnStartBlockTest(startBlockCallback => new CallbackVisitor(_ => { }, _ => { }, startBlockCallback, _ => { }));
+ }
+
+ [Fact]
+ public void ListenerConstructedWithAllCallbacksCallsCallbackOnEndBlock()
+ {
+ RunOnEndBlockTest(endBlockCallback => new CallbackVisitor(_ => { }, _ => { }, _ => { }, endBlockCallback));
+ }
+
+ [Fact]
+ public void ListenerCallsOnEndSpanCallbackUsingSynchronizationContextIfSpecified()
+ {
+ RunSyncContextTest(new SpanBuilder().Build(),
+ spanCallback => new CallbackVisitor(spanCallback, _ => { }, _ => { }, _ => { }),
+ (listener, expected) => listener.VisitSpan(expected));
+ }
+
+ [Fact]
+ public void ListenerCallsOnStartBlockCallbackUsingSynchronizationContextIfSpecified()
+ {
+ RunSyncContextTest(BlockType.Template,
+ startBlockCallback => new CallbackVisitor(_ => { }, _ => { }, startBlockCallback, _ => { }),
+ (listener, expected) => listener.VisitStartBlock(new BlockBuilder() { Type = expected }.Build()));
+ }
+
+ [Fact]
+ public void ListenerCallsOnEndBlockCallbackUsingSynchronizationContextIfSpecified()
+ {
+ RunSyncContextTest(BlockType.Template,
+ endBlockCallback => new CallbackVisitor(_ => { }, _ => { }, _ => { }, endBlockCallback),
+ (listener, expected) => listener.VisitEndBlock(new BlockBuilder() { Type = expected }.Build()));
+ }
+
+ [Fact]
+ public void ListenerCallsOnErrorCallbackUsingSynchronizationContextIfSpecified()
+ {
+ RunSyncContextTest(new RazorError("Bar", 42, 42, 42),
+ errorCallback => new CallbackVisitor(_ => { }, errorCallback, _ => { }, _ => { }),
+ (listener, expected) => listener.VisitError(expected));
+ }
+
+ private static void RunSyncContextTest<T>(T expected, Func<Action<T>, CallbackVisitor> ctor, Action<CallbackVisitor, T> call)
+ {
+ // Arrange
+ Mock<SynchronizationContext> mockContext = new Mock<SynchronizationContext>();
+ mockContext.Setup(c => c.Post(It.IsAny<SendOrPostCallback>(), It.IsAny<object>()))
+ .Callback<SendOrPostCallback, object>((callback, state) => { callback(expected); });
+
+ // Act/Assert
+ RunCallbackTest<T>(default(T), callback =>
+ {
+ CallbackVisitor listener = ctor(callback);
+ listener.SynchronizationContext = mockContext.Object;
+ return listener;
+ }, call, (original, actual) =>
+ {
+ Assert.NotEqual(original, actual);
+ Assert.Equal(expected, actual);
+ });
+ }
+
+ private static void RunOnStartBlockTest(Func<Action<BlockType>, CallbackVisitor> ctor, Action<BlockType, BlockType> verifyResults = null)
+ {
+ RunCallbackTest(BlockType.Markup, ctor, (listener, expected) => listener.VisitStartBlock(new BlockBuilder() { Type = expected }.Build()), verifyResults);
+ }
+
+ private static void RunOnEndBlockTest(Func<Action<BlockType>, CallbackVisitor> ctor, Action<BlockType, BlockType> verifyResults = null)
+ {
+ RunCallbackTest(BlockType.Markup, ctor, (listener, expected) => listener.VisitEndBlock(new BlockBuilder() { Type = expected }.Build()), verifyResults);
+ }
+
+ private static void RunOnErrorTest(Func<Action<RazorError>, CallbackVisitor> ctor, Action<RazorError, RazorError> verifyResults = null)
+ {
+ RunCallbackTest(new RazorError("Foo", SourceLocation.Zero), ctor, (listener, expected) => listener.VisitError(expected), verifyResults);
+ }
+
+ private static void RunOnEndSpanTest(Func<Action<Span>, CallbackVisitor> ctor, Action<Span, Span> verifyResults = null)
+ {
+ RunCallbackTest(new SpanBuilder().Build(), ctor, (listener, expected) => listener.VisitSpan(expected), verifyResults);
+ }
+
+ private static void RunCallbackTest<T>(T expected, Func<Action<T>, CallbackVisitor> ctor, Action<CallbackVisitor, T> call, Action<T, T> verifyResults = null)
+ {
+ // Arrange
+ object actual = null;
+ Action<T> callback = t => actual = t;
+
+ CallbackVisitor listener = ctor(callback);
+
+ // Act
+ call(listener, expected);
+
+ // Assert
+ if (verifyResults == null)
+ {
+ Assert.Equal(expected, actual);
+ }
+ else
+ {
+ verifyResults(expected, (T)actual);
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/Html/HtmlAttributeTest.cs b/test/System.Web.Razor.Test/Parser/Html/HtmlAttributeTest.cs
new file mode 100644
index 00000000..65fac9e5
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/Html/HtmlAttributeTest.cs
@@ -0,0 +1,268 @@
+using System.Web.Razor.Editor;
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Test.Framework;
+using System.Web.Razor.Text;
+using Xunit;
+
+namespace System.Web.Razor.Test.Parser.Html
+{
+ public class HtmlAttributeTest : CsHtmlMarkupParserTestBase
+ {
+ [Fact]
+ public void SimpleLiteralAttribute()
+ {
+ ParseBlockTest("<a href='Foo' />",
+ new MarkupBlock(
+ Factory.Markup("<a"),
+ new MarkupBlock(new AttributeBlockCodeGenerator(name: "href", prefix: new LocationTagged<string>(" href='", 2, 0, 2), suffix: new LocationTagged<string>("'", 12, 0, 12)),
+ Factory.Markup(" href='").With(SpanCodeGenerator.Null),
+ Factory.Markup("Foo").With(new LiteralAttributeCodeGenerator(prefix: new LocationTagged<string>(String.Empty, 9, 0, 9), value: new LocationTagged<string>("Foo", 9, 0, 9))),
+ Factory.Markup("'").With(SpanCodeGenerator.Null)),
+ Factory.Markup(" />").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void MultiPartLiteralAttribute()
+ {
+ ParseBlockTest("<a href='Foo Bar Baz' />",
+ new MarkupBlock(
+ Factory.Markup("<a"),
+ new MarkupBlock(new AttributeBlockCodeGenerator(name: "href", prefix: new LocationTagged<string>(" href='", 2, 0, 2), suffix: new LocationTagged<string>("'", 20, 0, 20)),
+ Factory.Markup(" href='").With(SpanCodeGenerator.Null),
+ Factory.Markup("Foo").With(new LiteralAttributeCodeGenerator(prefix: new LocationTagged<string>(String.Empty, 9, 0, 9), value: new LocationTagged<string>("Foo", 9, 0, 9))),
+ Factory.Markup(" Bar").With(new LiteralAttributeCodeGenerator(prefix: new LocationTagged<string>(" ", 12, 0, 12), value: new LocationTagged<string>("Bar", 13, 0, 13))),
+ Factory.Markup(" Baz").With(new LiteralAttributeCodeGenerator(prefix: new LocationTagged<string>(" ", 16, 0, 16), value: new LocationTagged<string>("Baz", 17, 0, 17))),
+ Factory.Markup("'").With(SpanCodeGenerator.Null)),
+ Factory.Markup(" />").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void DoubleQuotedLiteralAttribute()
+ {
+ ParseBlockTest("<a href=\"Foo Bar Baz\" />",
+ new MarkupBlock(
+ Factory.Markup("<a"),
+ new MarkupBlock(new AttributeBlockCodeGenerator(name: "href", prefix: new LocationTagged<string>(" href=\"", 2, 0, 2), suffix: new LocationTagged<string>("\"", 20, 0, 20)),
+ Factory.Markup(" href=\"").With(SpanCodeGenerator.Null),
+ Factory.Markup("Foo").With(new LiteralAttributeCodeGenerator(prefix: new LocationTagged<string>(String.Empty, 9, 0, 9), value: new LocationTagged<string>("Foo", 9, 0, 9))),
+ Factory.Markup(" Bar").With(new LiteralAttributeCodeGenerator(prefix: new LocationTagged<string>(" ", 12, 0, 12), value: new LocationTagged<string>("Bar", 13, 0, 13))),
+ Factory.Markup(" Baz").With(new LiteralAttributeCodeGenerator(prefix: new LocationTagged<string>(" ", 16, 0, 16), value: new LocationTagged<string>("Baz", 17, 0, 17))),
+ Factory.Markup("\"").With(SpanCodeGenerator.Null)),
+ Factory.Markup(" />").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void UnquotedLiteralAttribute()
+ {
+ ParseBlockTest("<a href=Foo Bar Baz />",
+ new MarkupBlock(
+ Factory.Markup("<a"),
+ new MarkupBlock(new AttributeBlockCodeGenerator(name: "href", prefix: new LocationTagged<string>(" href=", 2, 0, 2), suffix: new LocationTagged<string>(String.Empty, 11, 0, 11)),
+ Factory.Markup(" href=").With(SpanCodeGenerator.Null),
+ Factory.Markup("Foo").With(new LiteralAttributeCodeGenerator(prefix: new LocationTagged<string>(String.Empty, 8, 0, 8), value: new LocationTagged<string>("Foo", 8, 0, 8)))),
+ Factory.Markup(" Bar Baz />").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void SimpleExpressionAttribute()
+ {
+ ParseBlockTest("<a href='@foo' />",
+ new MarkupBlock(
+ Factory.Markup("<a"),
+ new MarkupBlock(new AttributeBlockCodeGenerator(name: "href", prefix: new LocationTagged<string>(" href='", 2, 0, 2), suffix: new LocationTagged<string>("'", 13, 0, 13)),
+ Factory.Markup(" href='").With(SpanCodeGenerator.Null),
+ new MarkupBlock(new DynamicAttributeBlockCodeGenerator(new LocationTagged<string>(String.Empty, 9, 0, 9), 9, 0, 9),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("foo")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace))),
+ Factory.Markup("'").With(SpanCodeGenerator.Null)),
+ Factory.Markup(" />").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void MultiValueExpressionAttribute()
+ {
+ ParseBlockTest("<a href='@foo bar @baz' />",
+ new MarkupBlock(
+ Factory.Markup("<a"),
+ new MarkupBlock(new AttributeBlockCodeGenerator(name: "href", prefix: new LocationTagged<string>(" href='", 2, 0, 2), suffix: new LocationTagged<string>("'", 22, 0, 22)),
+ Factory.Markup(" href='").With(SpanCodeGenerator.Null),
+ new MarkupBlock(new DynamicAttributeBlockCodeGenerator(new LocationTagged<string>(String.Empty, 9, 0, 9), 9, 0, 9),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("foo")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace))),
+ Factory.Markup(" bar").With(new LiteralAttributeCodeGenerator(new LocationTagged<string>(" ", 13, 0, 13), new LocationTagged<string>("bar", 14, 0, 14))),
+ new MarkupBlock(new DynamicAttributeBlockCodeGenerator(new LocationTagged<string>(" ", 17, 0, 17), 18, 0, 18),
+ Factory.Markup(" ").With(SpanCodeGenerator.Null),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("baz")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace))),
+ Factory.Markup("'").With(SpanCodeGenerator.Null)),
+ Factory.Markup(" />").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void VirtualPathAttributesWorkWithConditionalAttributes()
+ {
+ ParseBlockTest("<a href='@foo ~/Foo/Bar' />",
+ new MarkupBlock(
+ Factory.Markup("<a"),
+ new MarkupBlock(new AttributeBlockCodeGenerator(name: "href", prefix: new LocationTagged<string>(" href='", 2, 0, 2), suffix: new LocationTagged<string>("'", 23, 0, 23)),
+ Factory.Markup(" href='").With(SpanCodeGenerator.Null),
+ new MarkupBlock(new DynamicAttributeBlockCodeGenerator(new LocationTagged<string>(String.Empty, 9, 0, 9), 9, 0, 9),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("foo")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace))),
+ Factory.Markup(" ~/Foo/Bar")
+ .WithEditorHints(EditorHints.VirtualPath)
+ .With(new LiteralAttributeCodeGenerator(
+ new LocationTagged<string>(" ", 13, 0, 13),
+ new LocationTagged<SpanCodeGenerator>(new ResolveUrlCodeGenerator(), 14, 0, 14))),
+ Factory.Markup("'").With(SpanCodeGenerator.Null)),
+ Factory.Markup(" />").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void UnquotedAttributeWithCodeWithSpacesInBlock()
+ {
+ ParseBlockTest("<input value=@foo />",
+ new MarkupBlock(
+ Factory.Markup("<input"),
+ new MarkupBlock(new AttributeBlockCodeGenerator(name: "value", prefix: new LocationTagged<string>(" value=", 6, 0, 6), suffix: new LocationTagged<string>(String.Empty, 17, 0, 17)),
+ Factory.Markup(" value=").With(SpanCodeGenerator.Null),
+ new MarkupBlock(new DynamicAttributeBlockCodeGenerator(new LocationTagged<string>(String.Empty, 13, 0, 13), 13, 0, 13),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("foo")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)))),
+ Factory.Markup(" />").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void UnquotedAttributeWithCodeWithSpacesInDocument()
+ {
+ ParseDocumentTest("<input value=@foo />",
+ new MarkupBlock(
+ Factory.Markup("<input"),
+ new MarkupBlock(new AttributeBlockCodeGenerator(name: "value", prefix: new LocationTagged<string>(" value=", 6, 0, 6), suffix: new LocationTagged<string>(String.Empty, 17, 0, 17)),
+ Factory.Markup(" value=").With(SpanCodeGenerator.Null),
+ new MarkupBlock(new DynamicAttributeBlockCodeGenerator(new LocationTagged<string>(String.Empty, 13, 0, 13), 13, 0, 13),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("foo")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)))),
+ Factory.Markup(" />")));
+ }
+
+ [Fact]
+ public void ConditionalAttributeCollapserDoesNotRemoveUrlAttributeValues()
+ {
+ // Act
+ ParserResults results = ParseDocument("<a href='~/Foo/Bar' />");
+ Block rewritten = new ConditionalAttributeCollapser(new HtmlMarkupParser().BuildSpan).Rewrite(results.Document);
+ rewritten = new MarkupCollapser(new HtmlMarkupParser().BuildSpan).Rewrite(rewritten);
+
+ // Assert
+ Assert.Equal(0, results.ParserErrors.Count);
+ EvaluateParseTree(rewritten,
+ new MarkupBlock(
+ Factory.Markup("<a"),
+ new MarkupBlock(new AttributeBlockCodeGenerator(name: "href", prefix: new LocationTagged<string>(" href='", 2, 0, 2), suffix: new LocationTagged<string>("'", 18, 0, 18)),
+ Factory.Markup(" href='").With(SpanCodeGenerator.Null),
+ Factory.Markup("~/Foo/Bar")
+ .WithEditorHints(EditorHints.VirtualPath)
+ .With(new LiteralAttributeCodeGenerator(
+ new LocationTagged<string>(String.Empty, 9, 0, 9),
+ new LocationTagged<SpanCodeGenerator>(new ResolveUrlCodeGenerator(), 9, 0, 9))),
+ Factory.Markup("'").With(SpanCodeGenerator.Null)),
+ Factory.Markup(" />")));
+ }
+
+ [Fact]
+ public void ConditionalAttributesDoNotCreateExtraDataForEntirelyLiteralAttribute()
+ {
+ // Arrange
+ const string code =
+ #region Big Block o' code
+ @"<div class=""sidebar"">
+ <h1>Title</h1>
+ <p>
+ As the author, you can <a href=""/Photo/Edit/photoId"">edit</a>
+ or <a href=""/Photo/Remove/photoId"">remove</a> this photo.
+ </p>
+ <dl>
+ <dt class=""description"">Description</dt>
+ <dd class=""description"">
+ The uploader did not provide a description for this photo.
+ </dd>
+ <dt class=""uploaded-by"">Uploaded by</dt>
+ <dd class=""uploaded-by""><a href=""/User/View/user.UserId"">user.DisplayName</a></dd>
+ <dt class=""upload-date"">Upload date</dt>
+ <dd class=""upload-date"">photo.UploadDate</dd>
+ <dt class=""part-of-gallery"">Gallery</dt>
+ <dd><a href=""/View/gallery.Id"" title=""View gallery.Name gallery"">gallery.Name</a></dd>
+ <dt class=""tags"">Tags</dt>
+ <dd class=""tags"">
+ <ul class=""tags"">
+ <li>This photo has no tags.</li>
+ </ul>
+ <a href=""/Photo/EditTags/photoId"">edit tags</a>
+ </dd>
+ </dl>
+
+ <p>
+ <a class=""download"" href=""/Photo/Full/photoId"" title=""Download: (photo.FileTitle + photo.FileExtension)"">Download full photo</a> ((photo.FileSize / 1024) KB)
+ </p>
+</div>
+<div class=""main"">
+ <img class=""large-photo"" alt=""photo.FileTitle"" src=""/Photo/Thumbnail"" />
+ <h2>Nobody has commented on this photo</h2>
+ <ol class=""comments"">
+ <li>
+ <h3 class=""comment-header"">
+ <a href=""/User/View/comment.UserId"" title=""View comment.DisplayName's profile"">comment.DisplayName</a> commented at comment.CommentDate:
+ </h3>
+ <p class=""comment-body"">comment.CommentText</p>
+ </li>
+ </ol>
+
+ <form method=""post"" action="""">
+ <fieldset id=""addComment"">
+ <legend>Post new comment</legend>
+ <ol>
+ <li>
+ <label for=""newComment"">Comment</label>
+ <textarea id=""newComment"" name=""newComment"" title=""Your comment"" rows=""6"" cols=""70""></textarea>
+ </li>
+ </ol>
+ <p class=""form-actions"">
+ <input type=""submit"" title=""Add comment"" value=""Add comment"" />
+ </p>
+ </fieldset>
+ </form>
+</div>";
+ #endregion
+
+ // Act
+ ParserResults results = ParseDocument(code);
+ Block rewritten = new ConditionalAttributeCollapser(new HtmlMarkupParser().BuildSpan).Rewrite(results.Document);
+ rewritten = new MarkupCollapser(new HtmlMarkupParser().BuildSpan).Rewrite(rewritten);
+
+ // Assert
+ Assert.Equal(0, results.ParserErrors.Count);
+ EvaluateParseTree(rewritten, new MarkupBlock(Factory.Markup(code)));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/Html/HtmlBlockTest.cs b/test/System.Web.Razor.Test/Parser/Html/HtmlBlockTest.cs
new file mode 100644
index 00000000..d69b367c
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/Html/HtmlBlockTest.cs
@@ -0,0 +1,388 @@
+using System.Web.Razor.Editor;
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Test.Framework;
+using System.Web.Razor.Text;
+using System.Web.Razor.Tokenizer.Symbols;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Razor.Test.Parser.Html
+{
+ public class HtmlBlockTest : CsHtmlMarkupParserTestBase
+ {
+ [Fact]
+ public void ParseBlockMethodThrowsArgNullExceptionOnNullContext()
+ {
+ // Arrange
+ HtmlMarkupParser parser = new HtmlMarkupParser();
+
+ // Act and Assert
+ Assert.Throws<InvalidOperationException>(() => parser.ParseBlock(), RazorResources.Parser_Context_Not_Set);
+ }
+
+ [Fact]
+ public void ParseBlockHandlesOpenAngleAtEof()
+ {
+ ParseDocumentTest(@"@{
+<",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ Factory.Code("\r\n").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup("<")))),
+ new RazorError(
+ String.Format(RazorResources.ParseError_Expected_EndOfBlock_Before_EOF, RazorResources.BlockName_Code, "}", "{"),
+ 1, 0, 1));
+ }
+
+ [Fact]
+ public void ParseBlockHandlesOpenAngleWithProperTagFollowingIt()
+ {
+ ParseDocumentTest(@"@{
+<
+</html>",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ Factory.Code("\r\n").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup("<\r\n")
+ ),
+ new MarkupBlock(
+ Factory.Markup(@"</html>").Accepts(AcceptedCharacters.None)
+ ),
+ Factory.EmptyCSharp().AsStatement()
+ )
+ ),
+ designTimeParser: true,
+ expectedErrors: new[]
+ {
+ new RazorError(String.Format(RazorResources.ParseError_UnexpectedEndTag, "html"), 7, 2, 0),
+ new RazorError(String.Format(RazorResources.ParseError_Expected_EndOfBlock_Before_EOF, "code", "}", "{"), 1, 0, 1)
+ });
+ }
+
+ [Fact]
+ public void TagWithoutCloseAngleDoesNotTerminateBlock()
+ {
+ ParseBlockTest(@"<
+ ",
+ new MarkupBlock(
+ Factory.Markup("< \r\n ")),
+ designTimeParser: true,
+ expectedErrors: new RazorError(String.Format(RazorResources.ParseError_UnfinishedTag, String.Empty), 0, 0, 0));
+ }
+
+ [Fact]
+ public void ParseBlockAllowsStartAndEndTagsToDifferInCase()
+ {
+ SingleSpanBlockTest("<li><p>Foo</P></lI>", BlockType.Markup, SpanKind.Markup, acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockReadsToEndOfLineIfFirstCharacterAfterTransitionIsColon()
+ {
+ ParseBlockTest(@"@:<li>Foo Bar Baz
+bork",
+ new MarkupBlock(
+ Factory.MarkupTransition(),
+ Factory.MetaMarkup(":", HtmlSymbolType.Colon),
+ Factory.Markup("<li>Foo Bar Baz\r\n")
+ .With(new SingleLineMarkupEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString, AcceptedCharacters.None))
+ ));
+ }
+
+ [Fact]
+ public void ParseBlockStopsParsingSingleLineBlockAtEOFIfNoEOLReached()
+ {
+ ParseBlockTest("@:foo bar",
+ new MarkupBlock(
+ Factory.MarkupTransition(),
+ Factory.MetaMarkup(":", HtmlSymbolType.Colon),
+ Factory.Markup(@"foo bar")
+ .With(new SingleLineMarkupEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString))
+ ));
+ }
+
+ [Fact]
+ public void ParseBlockTreatsTwoAtSignsAsEscapeSequence()
+ {
+ HtmlParserTestUtils.RunSingleAtEscapeTest(ParseBlockTest);
+ }
+
+ [Fact]
+ public void ParseBlockTreatsPairsOfAtSignsAsEscapeSequence()
+ {
+ HtmlParserTestUtils.RunMultiAtEscapeTest(ParseBlockTest);
+ }
+
+ [Fact]
+ public void ParseBlockStopsAtMatchingCloseTagToStartTag()
+ {
+ SingleSpanBlockTest("<a><b></b></a><c></c>", "<a><b></b></a>", BlockType.Markup, SpanKind.Markup, acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockParsesUntilMatchingEndTagIfFirstNonWhitespaceCharacterIsStartTag()
+ {
+ SingleSpanBlockTest("<baz><boz><biz></biz></boz></baz>", BlockType.Markup, SpanKind.Markup, acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockAllowsUnclosedTagsAsLongAsItCanRecoverToAnExpectedEndTag()
+ {
+ SingleSpanBlockTest("<foo><bar><baz></foo>", BlockType.Markup, SpanKind.Markup, acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockWithSelfClosingTagJustEmitsTag()
+ {
+ SingleSpanBlockTest("<foo />", BlockType.Markup, SpanKind.Markup, acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockCanHandleSelfClosingTagsWithinBlock()
+ {
+ SingleSpanBlockTest("<foo><bar /></foo>", BlockType.Markup, SpanKind.Markup, acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockSupportsTagsWithAttributes()
+ {
+ ParseBlockTest("<foo bar=\"baz\"><biz><boz zoop=zork/></biz></foo>",
+ new MarkupBlock(
+ Factory.Markup("<foo"),
+ new MarkupBlock(new AttributeBlockCodeGenerator("bar", new LocationTagged<string>(" bar=\"", 4, 0, 4), new LocationTagged<string>("\"", 13, 0, 13)),
+ Factory.Markup(" bar=\"").With(SpanCodeGenerator.Null),
+ Factory.Markup("baz").With(new LiteralAttributeCodeGenerator(new LocationTagged<string>(String.Empty, 10, 0, 10), new LocationTagged<string>("baz", 10, 0, 10))),
+ Factory.Markup("\"").With(SpanCodeGenerator.Null)),
+ Factory.Markup("><biz><boz"),
+ new MarkupBlock(new AttributeBlockCodeGenerator("zoop", new LocationTagged<string>(" zoop=", 24, 0, 24), new LocationTagged<string>(String.Empty, 34, 0, 34)),
+ Factory.Markup(" zoop=").With(SpanCodeGenerator.Null),
+ Factory.Markup("zork").With(new LiteralAttributeCodeGenerator(new LocationTagged<string>(String.Empty, 30, 0, 30), new LocationTagged<string>("zork", 30, 0, 30)))),
+ Factory.Markup("/></biz></foo>").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void ParseBlockAllowsCloseAngleBracketInAttributeValueIfDoubleQuoted()
+ {
+ ParseBlockTest("<foo><bar baz=\">\" /></foo>",
+ new MarkupBlock(
+ Factory.Markup("<foo><bar"),
+ new MarkupBlock(new AttributeBlockCodeGenerator("baz", new LocationTagged<string>(" baz=\"", 9, 0, 9), new LocationTagged<string>("\"", 16, 0, 16)),
+ Factory.Markup(" baz=\"").With(SpanCodeGenerator.Null),
+ Factory.Markup(">").With(new LiteralAttributeCodeGenerator(new LocationTagged<string>(String.Empty, 15, 0, 15), new LocationTagged<string>(">", 15, 0, 15))),
+ Factory.Markup("\"").With(SpanCodeGenerator.Null)),
+ Factory.Markup(" /></foo>").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void ParseBlockAllowsCloseAngleBracketInAttributeValueIfSingleQuoted()
+ {
+ ParseBlockTest("<foo><bar baz=\'>\' /></foo>",
+ new MarkupBlock(
+ Factory.Markup("<foo><bar"),
+ new MarkupBlock(new AttributeBlockCodeGenerator("baz", new LocationTagged<string>(" baz='", 9, 0, 9), new LocationTagged<string>("'", 16, 0, 16)),
+ Factory.Markup(" baz='").With(SpanCodeGenerator.Null),
+ Factory.Markup(">").With(new LiteralAttributeCodeGenerator(new LocationTagged<string>(String.Empty, 15, 0, 15), new LocationTagged<string>(">", 15, 0, 15))),
+ Factory.Markup("'").With(SpanCodeGenerator.Null)),
+ Factory.Markup(" /></foo>").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void ParseBlockAllowsSlashInAttributeValueIfDoubleQuoted()
+ {
+ ParseBlockTest("<foo><bar baz=\"/\"></bar></foo>",
+ new MarkupBlock(
+ Factory.Markup("<foo><bar"),
+ new MarkupBlock(new AttributeBlockCodeGenerator("baz", new LocationTagged<string>(" baz=\"", 9, 0, 9), new LocationTagged<string>("\"", 16, 0, 16)),
+ Factory.Markup(" baz=\"").With(SpanCodeGenerator.Null),
+ Factory.Markup("/").With(new LiteralAttributeCodeGenerator(new LocationTagged<string>(String.Empty, 15, 0, 15), new LocationTagged<string>("/", 15, 0, 15))),
+ Factory.Markup("\"").With(SpanCodeGenerator.Null)),
+ Factory.Markup("></bar></foo>").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void ParseBlockAllowsSlashInAttributeValueIfSingleQuoted()
+ {
+ ParseBlockTest("<foo><bar baz=\'/\'></bar></foo>",
+ new MarkupBlock(
+ Factory.Markup("<foo><bar"),
+ new MarkupBlock(new AttributeBlockCodeGenerator("baz", new LocationTagged<string>(" baz='", 9, 0, 9), new LocationTagged<string>("'", 16, 0, 16)),
+ Factory.Markup(" baz='").With(SpanCodeGenerator.Null),
+ Factory.Markup("/").With(new LiteralAttributeCodeGenerator(new LocationTagged<string>(String.Empty, 15, 0, 15), new LocationTagged<string>("/", 15, 0, 15))),
+ Factory.Markup("'").With(SpanCodeGenerator.Null)),
+ Factory.Markup("></bar></foo>").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void ParseBlockTerminatesAtEOF()
+ {
+ SingleSpanBlockTest("<foo>", "<foo>", BlockType.Markup, SpanKind.Markup,
+ new RazorError(String.Format(RazorResources.ParseError_MissingEndTag, "foo"), new SourceLocation(0, 0, 0)));
+ }
+
+ [Fact]
+ public void ParseBlockSupportsCommentAsBlock()
+ {
+ SingleSpanBlockTest("<!-- foo -->", BlockType.Markup, SpanKind.Markup, acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockSupportsCommentWithinBlock()
+ {
+ SingleSpanBlockTest("<foo>bar<!-- zoop -->baz</foo>", BlockType.Markup, SpanKind.Markup, acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockProperlyBalancesCommentStartAndEndTags()
+ {
+ SingleSpanBlockTest("<!--<foo></bar>-->", BlockType.Markup, SpanKind.Markup, acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockTerminatesAtEOFWhenParsingComment()
+ {
+ SingleSpanBlockTest("<!--<foo>", "<!--<foo>", BlockType.Markup, SpanKind.Markup);
+ }
+
+ [Fact]
+ public void ParseBlockOnlyTerminatesCommentOnFullEndSequence()
+ {
+ SingleSpanBlockTest("<!--<foo>--</bar>-->", BlockType.Markup, SpanKind.Markup, acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockTerminatesCommentAtFirstOccurrenceOfEndSequence()
+ {
+ SingleSpanBlockTest("<foo><!--<foo></bar-->--></foo>", BlockType.Markup, SpanKind.Markup, acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockTreatsMalformedTagsAsContent()
+ {
+ SingleSpanBlockTest(
+ "<foo></!-- bar --></foo>",
+ "<foo></!-- bar -->",
+ BlockType.Markup,
+ SpanKind.Markup,
+ AcceptedCharacters.None,
+ new RazorError(String.Format(RazorResources.ParseError_MissingEndTag, "foo"), 0, 0, 0));
+ }
+
+
+ [Fact]
+ public void ParseBlockParsesSGMLDeclarationAsEmptyTag()
+ {
+ SingleSpanBlockTest("<foo><!DOCTYPE foo bar baz></foo>", BlockType.Markup, SpanKind.Markup, acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockTerminatesSGMLDeclarationAtFirstCloseAngle()
+ {
+ SingleSpanBlockTest("<foo><!DOCTYPE foo bar> baz></foo>", BlockType.Markup, SpanKind.Markup, acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockParsesXMLProcessingInstructionAsEmptyTag()
+ {
+ SingleSpanBlockTest("<foo><?xml foo bar baz?></foo>", BlockType.Markup, SpanKind.Markup, acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockTerminatesXMLProcessingInstructionAtQuestionMarkCloseAnglePair()
+ {
+ SingleSpanBlockTest("<foo><?xml foo bar?> baz</foo>", BlockType.Markup, SpanKind.Markup, acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockDoesNotTerminateXMLProcessingInstructionAtCloseAngleUnlessPreceededByQuestionMark()
+ {
+ SingleSpanBlockTest("<foo><?xml foo bar> baz?></foo>", BlockType.Markup, SpanKind.Markup, acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockSupportsScriptTagsWithLessThanSignsInThem()
+ {
+ SingleSpanBlockTest(@"<script>if(foo<bar) { alert(""baz"");)</script>", BlockType.Markup, SpanKind.Markup, acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockSupportsScriptTagsWithSpacedLessThanSignsInThem()
+ {
+ SingleSpanBlockTest(@"<script>if(foo < bar) { alert(""baz"");)</script>", BlockType.Markup, SpanKind.Markup, acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockAcceptsEmptyTextTag()
+ {
+ ParseBlockTest("<text/>",
+ new MarkupBlock(
+ Factory.MarkupTransition("<text/>")
+ ));
+ }
+
+ [Fact]
+ public void ParseBlockAcceptsTextTagAsOuterTagButDoesNotRender()
+ {
+ ParseBlockTest("<text>Foo Bar <foo> Baz</text> zoop",
+ new MarkupBlock(
+ Factory.MarkupTransition("<text>"),
+ Factory.Markup("Foo Bar <foo> Baz"),
+ Factory.MarkupTransition("</text>"),
+ Factory.Markup(" ").Accepts(AcceptedCharacters.None)
+ ));
+ }
+
+ [Fact]
+ public void ParseBlockRendersLiteralTextTagIfDoubled()
+ {
+ ParseBlockTest("<text><text>Foo Bar <foo> Baz</text></text> zoop",
+ new MarkupBlock(
+ Factory.MarkupTransition("<text>"),
+ Factory.Markup("<text>Foo Bar <foo> Baz</text>"),
+ Factory.MarkupTransition("</text>"),
+ Factory.Markup(" ").Accepts(AcceptedCharacters.None)
+ ));
+ }
+
+ [Fact]
+ public void ParseBlockDoesNotConsiderPsuedoTagWithinMarkupBlock()
+ {
+ ParseBlockTest("<foo><text><bar></bar></foo>",
+ new MarkupBlock(
+ Factory.Markup("<foo><text><bar></bar></foo>").Accepts(AcceptedCharacters.None)
+ ));
+ }
+
+ [Fact]
+ public void ParseBlockStopsParsingMidEmptyTagIfEOFReached()
+ {
+ ParseBlockTest("<br/",
+ new MarkupBlock(
+ Factory.Markup("<br/")
+ ),
+ new RazorError(String.Format(RazorResources.ParseError_UnfinishedTag, "br"), SourceLocation.Zero));
+ }
+
+ [Fact]
+ public void ParseBlockCorrectlyHandlesSingleLineOfMarkupWithEmbeddedStatement()
+ {
+ ParseBlockTest("<div>Foo @if(true) {} Bar</div>",
+ new MarkupBlock(
+ Factory.Markup("<div>Foo "),
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("if(true) {}").AsStatement()),
+ Factory.Markup(" Bar</div>").Accepts(AcceptedCharacters.None)));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/Html/HtmlDocumentTest.cs b/test/System.Web.Razor.Test/Parser/Html/HtmlDocumentTest.cs
new file mode 100644
index 00000000..58a18819
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/Html/HtmlDocumentTest.cs
@@ -0,0 +1,214 @@
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Test.Framework;
+using System.Web.Razor.Text;
+using System.Web.WebPages.TestUtils;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Razor.Test.Parser.Html
+{
+ public class HtmlDocumentTest : CsHtmlMarkupParserTestBase
+ {
+ private static readonly TestFile Nested1000 = TestFile.Create("nested-1000.html");
+
+ [Fact]
+ public void ParseDocumentMethodThrowsArgNullExceptionOnNullContext()
+ {
+ // Arrange
+ HtmlMarkupParser parser = new HtmlMarkupParser();
+
+ // Act and Assert
+ Assert.Throws<InvalidOperationException>(() => parser.ParseDocument(), RazorResources.Parser_Context_Not_Set);
+ }
+
+ [Fact]
+ public void ParseSectionMethodThrowsArgNullExceptionOnNullContext()
+ {
+ // Arrange
+ HtmlMarkupParser parser = new HtmlMarkupParser();
+
+ // Act and Assert
+ Assert.Throws<InvalidOperationException>(() => parser.ParseSection(null, true), RazorResources.Parser_Context_Not_Set);
+ }
+
+ [Fact]
+ public void ParseDocumentOutputsEmptyBlockWithEmptyMarkupSpanIfContentIsEmptyString()
+ {
+ ParseDocumentTest(String.Empty, new MarkupBlock(Factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void ParseDocumentOutputsWhitespaceOnlyContentAsSingleWhitespaceMarkupSpan()
+ {
+ SingleSpanDocumentTest(" ", BlockType.Markup, SpanKind.Markup);
+ }
+
+ [Fact]
+ public void ParseDocumentAcceptsSwapTokenAtEndOfFileAndOutputsZeroLengthCodeSpan()
+ {
+ ParseDocumentTest("@",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.EmptyCSharp()
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ Factory.EmptyHtml()),
+ new RazorError(RazorResources.ParseError_Unexpected_EndOfFile_At_Start_Of_CodeBlock, 1, 0, 1));
+ }
+
+ [Fact]
+ public void ParseDocumentCorrectlyHandlesSingleLineOfMarkupWithEmbeddedStatement()
+ {
+ ParseDocumentTest("<div>Foo @if(true) {} Bar</div>",
+ new MarkupBlock(
+ Factory.Markup("<div>Foo "),
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("if(true) {}").AsStatement()),
+ Factory.Markup(" Bar</div>")));
+ }
+
+ [Fact]
+ public void ParseDocumentWithinSectionDoesNotCreateDocumentLevelSpan()
+ {
+ ParseDocumentTest(@"@section Foo {
+ <html></html>
+}",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new SectionBlock(new SectionCodeGenerator("Foo"),
+ Factory.CodeTransition(),
+ Factory.MetaCode("section Foo {")
+ .AutoCompleteWith(null, atEndOfSpan: true),
+ new MarkupBlock(
+ Factory.Markup("\r\n <html></html>\r\n")),
+ Factory.MetaCode("}").Accepts(AcceptedCharacters.None)),
+ Factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void ParseDocumentParsesWholeContentAsOneSpanIfNoSwapCharacterEncountered()
+ {
+ SingleSpanDocumentTest("foo <bar>baz</bar>", BlockType.Markup, SpanKind.Markup);
+ }
+
+ [Fact]
+ public void ParseDocumentHandsParsingOverToCodeParserWhenAtSignEncounteredAndEmitsOutput()
+ {
+ ParseDocumentTest("foo @bar baz",
+ new MarkupBlock(
+ Factory.Markup("foo "),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("bar")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ Factory.Markup(" baz")));
+ }
+
+ [Fact]
+ public void ParseDocumentEmitsAtSignAsMarkupIfAtEndOfFile()
+ {
+ ParseDocumentTest("foo @",
+ new MarkupBlock(
+ Factory.Markup("foo "),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.EmptyCSharp()
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ Factory.EmptyHtml()),
+ new RazorError(RazorResources.ParseError_Unexpected_EndOfFile_At_Start_Of_CodeBlock, 5, 0, 5));
+ }
+
+ [Fact]
+ public void ParseDocumentEmitsCodeBlockIfFirstCharacterIsSwapCharacter()
+ {
+ ParseDocumentTest("@bar",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("bar")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ Factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void ParseDocumentDoesNotSwitchToCodeOnEmailAddressInText()
+ {
+ SingleSpanDocumentTest("<foo>anurse@microsoft.com</foo>", BlockType.Markup, SpanKind.Markup);
+ }
+
+ [Fact]
+ public void ParseDocumentDoesNotSwitchToCodeOnEmailAddressInAttribute()
+ {
+ ParseDocumentTest("<a href=\"mailto:anurse@microsoft.com\">Email me</a>",
+ new MarkupBlock(
+ Factory.Markup("<a"),
+ new MarkupBlock(new AttributeBlockCodeGenerator("href", new LocationTagged<string>(" href=\"", 2, 0, 2), new LocationTagged<string>("\"", 36, 0, 36)),
+ Factory.Markup(" href=\"").With(SpanCodeGenerator.Null),
+ Factory.Markup("mailto:anurse@microsoft.com")
+ .With(new LiteralAttributeCodeGenerator(new LocationTagged<string>(String.Empty, 9, 0, 9), new LocationTagged<string>("mailto:anurse@microsoft.com", 9, 0, 9))),
+ Factory.Markup("\"").With(SpanCodeGenerator.Null)),
+ Factory.Markup(">Email me</a>")));
+ }
+
+ [Fact]
+ public void ParseDocumentDoesNotReturnErrorOnMismatchedTags()
+ {
+ SingleSpanDocumentTest("Foo <div><p></p></p> Baz", BlockType.Markup, SpanKind.Markup);
+ }
+
+ [Fact]
+ public void ParseDocumentReturnsOneMarkupSegmentIfNoCodeBlocksEncountered()
+ {
+ SingleSpanDocumentTest("Foo <p>Baz<!--Foo-->Bar<!-F> Qux", BlockType.Markup, SpanKind.Markup);
+ }
+
+ [Fact]
+ public void ParseDocumentRendersTextPseudoTagAsMarkup()
+ {
+ SingleSpanDocumentTest("Foo <text>Foo</text>", BlockType.Markup, SpanKind.Markup);
+ }
+
+ [Fact]
+ public void ParseDocumentAcceptsEndTagWithNoMatchingStartTag()
+ {
+ SingleSpanDocumentTest("Foo </div> Bar", BlockType.Markup, SpanKind.Markup);
+ }
+
+ [Fact]
+ public void ParseDocumentNoLongerSupportsDollarOpenBraceCombination()
+ {
+ ParseDocumentTest("<foo>${bar}</foo>",
+ new MarkupBlock(
+ Factory.Markup("<foo>${bar}</foo>")));
+ }
+
+ [Fact]
+ public void ParseDocumentTreatsTwoAtSignsAsEscapeSequence()
+ {
+ HtmlParserTestUtils.RunSingleAtEscapeTest(ParseDocumentTest, lastSpanAcceptedCharacters: AcceptedCharacters.Any);
+ }
+
+ [Fact]
+ public void ParseDocumentTreatsPairsOfAtSignsAsEscapeSequence()
+ {
+ HtmlParserTestUtils.RunMultiAtEscapeTest(ParseDocumentTest, lastSpanAcceptedCharacters: AcceptedCharacters.Any);
+ }
+
+ [Fact]
+ public void ParseBlockCanParse1000NestedElements()
+ {
+ string content = Nested1000.ReadAllText();
+ SingleSpanDocumentTest(content, BlockType.Markup, SpanKind.Markup);
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/Html/HtmlErrorTest.cs b/test/System.Web.Razor.Test/Parser/Html/HtmlErrorTest.cs
new file mode 100644
index 00000000..191a3f92
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/Html/HtmlErrorTest.cs
@@ -0,0 +1,87 @@
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Test.Framework;
+using System.Web.Razor.Text;
+using Xunit;
+
+namespace System.Web.Razor.Test.Parser.Html
+{
+ public class HtmlErrorTest : CsHtmlMarkupParserTestBase
+ {
+ [Fact]
+ public void ParseBlockAllowsInvalidTagNamesAsLongAsParserCanIdentifyEndTag()
+ {
+ SingleSpanBlockTest("<1-foo+bar>foo</1-foo+bar>", BlockType.Markup, SpanKind.Markup, acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockThrowsErrorIfStartTextTagContainsTextAfterName()
+ {
+ ParseBlockTest("<text foo bar></text>",
+ new MarkupBlock(
+ Factory.MarkupTransition("<text").Accepts(AcceptedCharacters.Any),
+ Factory.Markup(" foo bar>"),
+ Factory.MarkupTransition("</text>")),
+ new RazorError(RazorResources.ParseError_TextTagCannotContainAttributes, SourceLocation.Zero));
+ }
+
+ [Fact]
+ public void ParseBlockThrowsErrorIfEndTextTagContainsTextAfterName()
+ {
+ ParseBlockTest("<text></text foo bar>",
+ new MarkupBlock(
+ Factory.MarkupTransition("<text>"),
+ Factory.MarkupTransition("</text").Accepts(AcceptedCharacters.Any),
+ Factory.Markup(" ")),
+ new RazorError(RazorResources.ParseError_TextTagCannotContainAttributes, 6, 0, 6));
+ }
+
+ [Fact]
+ public void ParseBlockThrowsExceptionIfBlockDoesNotStartWithTag()
+ {
+ ParseBlockTest("foo bar <baz>",
+ new MarkupBlock(),
+ new RazorError(RazorResources.ParseError_MarkupBlock_Must_Start_With_Tag, SourceLocation.Zero));
+ }
+
+ [Fact]
+ public void ParseBlockStartingWithEndTagProducesRazorErrorThenOutputsMarkupSegmentAndEndsBlock()
+ {
+ ParseBlockTest("</foo> bar baz",
+ new MarkupBlock(
+ Factory.Markup("</foo> ").Accepts(AcceptedCharacters.None)),
+ new RazorError(String.Format(RazorResources.ParseError_UnexpectedEndTag, "foo"), SourceLocation.Zero));
+ }
+
+ [Fact]
+ public void ParseBlockWithUnclosedTopLevelTagThrowsMissingEndTagParserExceptionOnOutermostUnclosedTag()
+ {
+ ParseBlockTest("<p><foo></bar>",
+ new MarkupBlock(
+ Factory.Markup("<p><foo></bar>").Accepts(AcceptedCharacters.None)),
+ new RazorError(String.Format(RazorResources.ParseError_MissingEndTag, "p"), new SourceLocation(0, 0, 0)));
+ }
+
+ [Fact]
+ public void ParseBlockWithUnclosedTagAtEOFThrowsMissingEndTagException()
+ {
+ ParseBlockTest("<foo>blah blah blah blah blah",
+ new MarkupBlock(
+ Factory.Markup("<foo>blah blah blah blah blah")),
+ new RazorError(String.Format(RazorResources.ParseError_MissingEndTag, "foo"), new SourceLocation(0, 0, 0)));
+ }
+
+ [Fact]
+ public void ParseBlockWithUnfinishedTagAtEOFThrowsIncompleteTagException()
+ {
+ ParseBlockTest("<foo bar=baz",
+ new MarkupBlock(
+ Factory.Markup("<foo"),
+ new MarkupBlock(new AttributeBlockCodeGenerator("bar", new LocationTagged<string>(" bar=", 4, 0, 4), new LocationTagged<string>(String.Empty, 12, 0, 12)),
+ Factory.Markup(" bar=").With(SpanCodeGenerator.Null),
+ Factory.Markup("baz").With(new LiteralAttributeCodeGenerator(new LocationTagged<string>(String.Empty, 9, 0, 9), new LocationTagged<string>("baz", 9, 0, 9))))),
+ new RazorError(String.Format(RazorResources.ParseError_UnfinishedTag, "foo"), new SourceLocation(0, 0, 0)));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/Html/HtmlParserTestUtils.cs b/test/System.Web.Razor.Test/Parser/Html/HtmlParserTestUtils.cs
new file mode 100644
index 00000000..595651fb
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/Html/HtmlParserTestUtils.cs
@@ -0,0 +1,37 @@
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Test.Framework;
+
+namespace System.Web.Razor.Test.Parser.Html
+{
+ internal class HtmlParserTestUtils
+ {
+ public static void RunSingleAtEscapeTest(Action<string, Block> testMethod, AcceptedCharacters lastSpanAcceptedCharacters = AcceptedCharacters.None)
+ {
+ var factory = SpanFactory.CreateCsHtml();
+ testMethod("<foo>@@bar</foo>",
+ new MarkupBlock(
+ factory.Markup("<foo>"),
+ factory.Markup("@").Hidden(),
+ factory.Markup("@bar</foo>").Accepts(lastSpanAcceptedCharacters)));
+ }
+
+ public static void RunMultiAtEscapeTest(Action<string, Block> testMethod, AcceptedCharacters lastSpanAcceptedCharacters = AcceptedCharacters.None)
+ {
+ var factory = SpanFactory.CreateCsHtml();
+ testMethod("<foo>@@@@@bar</foo>",
+ new MarkupBlock(
+ factory.Markup("<foo>"),
+ factory.Markup("@").Hidden(),
+ factory.Markup("@"),
+ factory.Markup("@").Hidden(),
+ factory.Markup("@"),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("bar")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ factory.Markup("</foo>").Accepts(lastSpanAcceptedCharacters)));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/Html/HtmlTagsTest.cs b/test/System.Web.Razor.Test/Parser/Html/HtmlTagsTest.cs
new file mode 100644
index 00000000..465666e1
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/Html/HtmlTagsTest.cs
@@ -0,0 +1,150 @@
+using System.Collections.Generic;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Test.Framework;
+using Xunit;
+using Xunit.Extensions;
+
+namespace System.Web.Razor.Test.Parser.Html
+{
+ public class HtmlTagsTest : CsHtmlMarkupParserTestBase
+ {
+ public static IEnumerable<string[]> VoidElementNames
+ {
+ get
+ {
+ yield return new[] { "area" };
+ yield return new[] { "base" };
+ yield return new[] { "br" };
+ yield return new[] { "col" };
+ yield return new[] { "command" };
+ yield return new[] { "embed" };
+ yield return new[] { "hr" };
+ yield return new[] { "img" };
+ yield return new[] { "input" };
+ yield return new[] { "keygen" };
+ yield return new[] { "link" };
+ yield return new[] { "meta" };
+ yield return new[] { "param" };
+ yield return new[] { "source" };
+ yield return new[] { "track" };
+ yield return new[] { "wbr" };
+ }
+ }
+
+ [Fact]
+ public void EmptyTagNestsLikeNormalTag()
+ {
+ ParseBlockTest("<p></> Bar",
+ new MarkupBlock(
+ Factory.Markup("<p></> ").Accepts(AcceptedCharacters.None)),
+ new RazorError(String.Format(RazorResources.ParseError_MissingEndTag, "p"), 0, 0, 0));
+ }
+
+ [Fact]
+ public void EmptyTag()
+ {
+ ParseBlockTest("<></> Bar",
+ new MarkupBlock(
+ Factory.Markup("<></> ").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void CommentTag()
+ {
+ ParseBlockTest("<!--Foo--> Bar",
+ new MarkupBlock(
+ Factory.Markup("<!--Foo--> ").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void DocTypeTag()
+ {
+ ParseBlockTest("<!DOCTYPE html> foo",
+ new MarkupBlock(
+ Factory.Markup("<!DOCTYPE html> ").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void ProcessingInstructionTag()
+ {
+ ParseBlockTest("<?xml version=\"1.0\" ?> foo",
+ new MarkupBlock(
+ Factory.Markup("<?xml version=\"1.0\" ?> ").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void ElementTags()
+ {
+ ParseBlockTest("<p>Foo</p> Bar",
+ new MarkupBlock(
+ Factory.Markup("<p>Foo</p> ").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void TextTags()
+ {
+ ParseBlockTest("<text>Foo</text>}",
+ new MarkupBlock(
+ Factory.MarkupTransition("<text>"),
+ Factory.Markup("Foo"),
+ Factory.MarkupTransition("</text>")));
+ }
+
+ [Fact]
+ public void CDataTag()
+ {
+ ParseBlockTest("<![CDATA[Foo]]> Bar",
+ new MarkupBlock(
+ Factory.Markup("<![CDATA[Foo]]> ").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void ScriptTag()
+ {
+ ParseDocumentTest("<script>foo < bar && quantity.toString() !== orderQty.val()</script>",
+ new MarkupBlock(
+ Factory.Markup("<script>foo < bar && quantity.toString() !== orderQty.val()</script>")));
+ }
+
+ [Theory]
+ [PropertyData("VoidElementNames")]
+ public void VoidElementFollowedByContent(string tagName)
+ {
+ ParseBlockTest("<" + tagName + ">foo",
+ new MarkupBlock(
+ Factory.Markup("<" + tagName + ">")
+ .Accepts(AcceptedCharacters.None)));
+ }
+
+ [Theory]
+ [PropertyData("VoidElementNames")]
+ public void VoidElementFollowedByOtherTag(string tagName)
+ {
+ ParseBlockTest("<" + tagName + "><other>foo",
+ new MarkupBlock(
+ Factory.Markup("<" + tagName + ">")
+ .Accepts(AcceptedCharacters.None)));
+ }
+
+ [Theory]
+ [PropertyData("VoidElementNames")]
+ public void VoidElementFollowedByCloseTag(string tagName)
+ {
+ ParseBlockTest("<" + tagName + "> </" + tagName + ">foo",
+ new MarkupBlock(
+ Factory.Markup("<" + tagName + "> </" + tagName + ">")
+ .Accepts(AcceptedCharacters.None)));
+ }
+
+ [Theory]
+ [PropertyData("VoidElementNames")]
+ public void IncompleteVoidElementEndTag(string tagName)
+ {
+ ParseBlockTest("<" + tagName + "></" + tagName,
+ new MarkupBlock(
+ Factory.Markup("<" + tagName + "></" + tagName)
+ .Accepts(AcceptedCharacters.Any)));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/Html/HtmlToCodeSwitchTest.cs b/test/System.Web.Razor.Test/Parser/Html/HtmlToCodeSwitchTest.cs
new file mode 100644
index 00000000..67275318
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/Html/HtmlToCodeSwitchTest.cs
@@ -0,0 +1,222 @@
+using System.Web.Razor.Editor;
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Test.Framework;
+using System.Web.Razor.Text;
+using System.Web.Razor.Tokenizer.Symbols;
+using Xunit;
+
+namespace System.Web.Razor.Test.Parser.Html
+{
+ public class HtmlToCodeSwitchTest : CsHtmlMarkupParserTestBase
+ {
+ [Fact]
+ public void ParseBlockSwitchesWhenCharacterBeforeSwapIsNonAlphanumeric()
+ {
+ ParseBlockTest("<p>foo#@i</p>",
+ new MarkupBlock(
+ Factory.Markup("<p>foo#"),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("i").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharacters.NonWhiteSpace)),
+ Factory.Markup("</p>").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void ParseBlockSwitchesToCodeWhenSwapCharacterEncounteredMidTag()
+ {
+ ParseBlockTest("<foo @bar />",
+ new MarkupBlock(
+ Factory.Markup("<foo "),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("bar")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ Factory.Markup(" />").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void ParseBlockSwitchesToCodeWhenSwapCharacterEncounteredInAttributeValue()
+ {
+ ParseBlockTest("<foo bar=\"@baz\" />",
+ new MarkupBlock(
+ Factory.Markup("<foo"),
+ new MarkupBlock(new AttributeBlockCodeGenerator("bar", new LocationTagged<string>(" bar=\"", 4, 0, 4), new LocationTagged<string>("\"", 14, 0, 14)),
+ Factory.Markup(" bar=\"").With(SpanCodeGenerator.Null),
+ new MarkupBlock(new DynamicAttributeBlockCodeGenerator(new LocationTagged<string>(String.Empty, 10, 0, 10), 10, 0, 10),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("baz")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace))),
+ Factory.Markup("\"").With(SpanCodeGenerator.Null)),
+ Factory.Markup(" />").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void ParseBlockSwitchesToCodeWhenSwapCharacterEncounteredInTagContent()
+ {
+ ParseBlockTest("<foo>@bar<baz>@boz</baz></foo>",
+ new MarkupBlock(
+ Factory.Markup("<foo>"),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("bar")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ Factory.Markup("<baz>"),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("boz")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ Factory.Markup("</baz></foo>").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void ParseBlockParsesCodeWithinSingleLineMarkup()
+ {
+ ParseBlockTest(@"@:<li>Foo @Bar Baz
+bork",
+ new MarkupBlock(
+ Factory.MarkupTransition(),
+ Factory.MetaMarkup(":", HtmlSymbolType.Colon),
+ Factory.Markup("<li>Foo ").With(new SingleLineMarkupEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString)),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("Bar")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ Factory.Markup(" Baz\r\n")
+ .With(new SingleLineMarkupEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString, AcceptedCharacters.None))));
+ }
+
+ [Fact]
+ public void ParseBlockSupportsCodeWithinComment()
+ {
+ ParseBlockTest("<foo><!-- @foo --></foo>",
+ new MarkupBlock(
+ Factory.Markup("<foo><!-- "),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("foo")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ Factory.Markup(" --></foo>").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void ParseBlockSupportsCodeWithinSGMLDeclaration()
+ {
+ ParseBlockTest("<foo><!DOCTYPE foo @bar baz></foo>",
+ new MarkupBlock(
+ Factory.Markup("<foo><!DOCTYPE foo "),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("bar")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ Factory.Markup(" baz></foo>").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void ParseBlockSupportsCodeWithinCDataDeclaration()
+ {
+ ParseBlockTest("<foo><![CDATA[ foo @bar baz]]></foo>",
+ new MarkupBlock(
+ Factory.Markup("<foo><![CDATA[ foo "),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("bar")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ Factory.Markup(" baz]]></foo>").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void ParseBlockSupportsCodeWithinXMLProcessingInstruction()
+ {
+ ParseBlockTest("<foo><?xml foo @bar baz?></foo>",
+ new MarkupBlock(
+ Factory.Markup("<foo><?xml foo "),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("bar")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ Factory.Markup(" baz?></foo>").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void ParseBlockDoesNotSwitchToCodeOnEmailAddressInText()
+ {
+ SingleSpanBlockTest("<foo>anurse@microsoft.com</foo>", BlockType.Markup, SpanKind.Markup, acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockDoesNotSwitchToCodeOnEmailAddressInAttribute()
+ {
+ ParseBlockTest("<a href=\"mailto:anurse@microsoft.com\">Email me</a>",
+ new MarkupBlock(
+ Factory.Markup("<a"),
+ new MarkupBlock(new AttributeBlockCodeGenerator("href", new LocationTagged<string>(" href=\"", 2, 0, 2), new LocationTagged<string>("\"", 36, 0, 36)),
+ Factory.Markup(" href=\"").With(SpanCodeGenerator.Null),
+ Factory.Markup("mailto:anurse@microsoft.com")
+ .With(new LiteralAttributeCodeGenerator(new LocationTagged<string>(String.Empty, 9, 0, 9), new LocationTagged<string>("mailto:anurse@microsoft.com", 9, 0, 9))),
+ Factory.Markup("\"").With(SpanCodeGenerator.Null)),
+ Factory.Markup(">Email me</a>").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void ParseBlockGivesWhitespacePreceedingAtToCodeIfThereIsNoMarkupOnThatLine()
+ {
+ ParseBlockTest(@" <ul>
+ @foreach(var p in Products) {
+ <li>Product: @p.Name</li>
+ }
+ </ul>",
+ new MarkupBlock(
+ Factory.Markup(" <ul>\r\n"),
+ new StatementBlock(
+ Factory.Code(" ").AsStatement(),
+ Factory.CodeTransition(),
+ Factory.Code("foreach(var p in Products) {\r\n").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" <li>Product: "),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("p.Name")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ Factory.Markup("</li>\r\n").Accepts(AcceptedCharacters.None)),
+ Factory.Code(" }\r\n").AsStatement().Accepts(AcceptedCharacters.None)),
+ Factory.Markup(" </ul>").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void CSharpCodeParserDoesNotAcceptLeadingOrTrailingWhitespaceInDesignMode()
+ {
+ ParseBlockTest(@" <ul>
+ @foreach(var p in Products) {
+ <li>Product: @p.Name</li>
+ }
+ </ul>",
+ new MarkupBlock(
+ Factory.Markup(" <ul>\r\n "),
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("foreach(var p in Products) {\r\n ").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup("<li>Product: "),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("p.Name").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharacters.NonWhiteSpace)),
+ Factory.Markup("</li>").Accepts(AcceptedCharacters.None)),
+ Factory.Code("\r\n }").AsStatement().Accepts(AcceptedCharacters.None)),
+ Factory.Markup("\r\n </ul>").Accepts(AcceptedCharacters.None)),
+ designTimeParser: true);
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/Html/HtmlUrlAttributeTest.cs b/test/System.Web.Razor.Test/Parser/Html/HtmlUrlAttributeTest.cs
new file mode 100644
index 00000000..ad5fe9fa
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/Html/HtmlUrlAttributeTest.cs
@@ -0,0 +1,270 @@
+using System.Web.Razor.Editor;
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Test.Framework;
+using System.Web.Razor.Text;
+using Xunit;
+
+namespace System.Web.Razor.Test.Parser.Html
+{
+ public class HtmlUrlAttributeTest : CsHtmlMarkupParserTestBase
+ {
+ [Fact]
+ public void SimpleUrlInAttributeInMarkupBlock()
+ {
+ ParseBlockTest("<a href='~/Foo/Bar/Baz' />",
+ new MarkupBlock(
+ Factory.Markup("<a"),
+ new MarkupBlock(new AttributeBlockCodeGenerator("href", new LocationTagged<string>(" href='", 2, 0, 2), new LocationTagged<string>("'", 22, 0, 22)),
+ Factory.Markup(" href='").With(SpanCodeGenerator.Null),
+ Factory.Markup("~/Foo/Bar/Baz")
+ .WithEditorHints(EditorHints.VirtualPath)
+ .With(new LiteralAttributeCodeGenerator(
+ new LocationTagged<string>(String.Empty, 9, 0, 9),
+ new LocationTagged<SpanCodeGenerator>(new ResolveUrlCodeGenerator(), 9, 0, 9))),
+ Factory.Markup("'").With(SpanCodeGenerator.Null)),
+ Factory.Markup(" />").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void SimpleUrlInAttributeInMarkupDocument()
+ {
+ ParseDocumentTest("<a href='~/Foo/Bar/Baz' />",
+ new MarkupBlock(
+ Factory.Markup("<a"),
+ new MarkupBlock(new AttributeBlockCodeGenerator("href", new LocationTagged<string>(" href='", 2, 0, 2), new LocationTagged<string>("'", 22, 0, 22)),
+ Factory.Markup(" href='").With(SpanCodeGenerator.Null),
+ Factory.Markup("~/Foo/Bar/Baz")
+ .WithEditorHints(EditorHints.VirtualPath)
+ .With(new LiteralAttributeCodeGenerator(
+ new LocationTagged<string>(String.Empty, 9, 0, 9),
+ new LocationTagged<SpanCodeGenerator>(new ResolveUrlCodeGenerator(), 9, 0, 9))),
+ Factory.Markup("'").With(SpanCodeGenerator.Null)),
+ Factory.Markup(" />")));
+ }
+
+ [Fact]
+ public void SimpleUrlInAttributeInMarkupSection()
+ {
+ ParseDocumentTest("@section Foo { <a href='~/Foo/Bar/Baz' /> }",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new SectionBlock(new SectionCodeGenerator("Foo"),
+ Factory.CodeTransition(),
+ Factory.MetaCode("section Foo {")
+ .AutoCompleteWith(null, atEndOfSpan: true)
+ .Accepts(AcceptedCharacters.Any),
+ new MarkupBlock(
+ Factory.Markup(" <a"),
+ new MarkupBlock(new AttributeBlockCodeGenerator("href", new LocationTagged<string>(" href='", 17, 0, 17), new LocationTagged<string>("'", 37, 0, 37)),
+ Factory.Markup(" href='").With(SpanCodeGenerator.Null),
+ Factory.Markup("~/Foo/Bar/Baz")
+ .WithEditorHints(EditorHints.VirtualPath)
+ .With(new LiteralAttributeCodeGenerator(
+ new LocationTagged<string>(String.Empty, 24, 0, 24),
+ new LocationTagged<SpanCodeGenerator>(new ResolveUrlCodeGenerator(), 24, 0, 24))),
+ Factory.Markup("'").With(SpanCodeGenerator.Null)),
+ Factory.Markup(" /> ")),
+ Factory.MetaCode("}").Accepts(AcceptedCharacters.None)),
+ Factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void UrlWithExpressionsInAttributeInMarkupBlock()
+ {
+ ParseBlockTest("<a href='~/Foo/@id/Baz' />",
+ new MarkupBlock(
+ Factory.Markup("<a"),
+ new MarkupBlock(new AttributeBlockCodeGenerator("href", new LocationTagged<string>(" href='", 2, 0, 2), new LocationTagged<string>("'", 22, 0, 22)),
+ Factory.Markup(" href='").With(SpanCodeGenerator.Null),
+ Factory.Markup("~/Foo/")
+ .WithEditorHints(EditorHints.VirtualPath)
+ .With(new LiteralAttributeCodeGenerator(
+ new LocationTagged<string>(String.Empty, 9, 0, 9),
+ new LocationTagged<SpanCodeGenerator>(new ResolveUrlCodeGenerator(), 9, 0, 9))),
+ new MarkupBlock(new DynamicAttributeBlockCodeGenerator(new LocationTagged<string>(String.Empty, 15, 0, 15), 15, 0, 15),
+ new ExpressionBlock(
+ Factory.CodeTransition().Accepts(AcceptedCharacters.None),
+ Factory.Code("id")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace))),
+ Factory.Markup("/Baz")
+ .With(new LiteralAttributeCodeGenerator(new LocationTagged<string>(String.Empty, 18, 0, 18), new LocationTagged<string>("/Baz", 18, 0, 18))),
+ Factory.Markup("'").With(SpanCodeGenerator.Null)),
+ Factory.Markup(" />").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void UrlWithExpressionsInAttributeInMarkupDocument()
+ {
+ ParseDocumentTest("<a href='~/Foo/@id/Baz' />",
+ new MarkupBlock(
+ Factory.Markup("<a"),
+ new MarkupBlock(new AttributeBlockCodeGenerator("href", new LocationTagged<string>(" href='", 2, 0, 2), new LocationTagged<string>("'", 22, 0, 22)),
+ Factory.Markup(" href='").With(SpanCodeGenerator.Null),
+ Factory.Markup("~/Foo/")
+ .WithEditorHints(EditorHints.VirtualPath)
+ .With(new LiteralAttributeCodeGenerator(
+ new LocationTagged<string>(String.Empty, 9, 0, 9),
+ new LocationTagged<SpanCodeGenerator>(new ResolveUrlCodeGenerator(), 9, 0, 9))),
+ new MarkupBlock(new DynamicAttributeBlockCodeGenerator(new LocationTagged<string>(String.Empty, 15, 0, 15), 15, 0, 15),
+ new ExpressionBlock(
+ Factory.CodeTransition().Accepts(AcceptedCharacters.None),
+ Factory.Code("id")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace))),
+ Factory.Markup("/Baz")
+ .With(new LiteralAttributeCodeGenerator(new LocationTagged<string>(String.Empty, 18, 0, 18), new LocationTagged<string>("/Baz", 18, 0, 18))),
+ Factory.Markup("'").With(SpanCodeGenerator.Null)),
+ Factory.Markup(" />")));
+ }
+
+ [Fact]
+ public void UrlWithExpressionsInAttributeInMarkupSection()
+ {
+ ParseDocumentTest("@section Foo { <a href='~/Foo/@id/Baz' /> }",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new SectionBlock(new SectionCodeGenerator("Foo"),
+ Factory.CodeTransition(),
+ Factory.MetaCode("section Foo {")
+ .AutoCompleteWith(null, atEndOfSpan: true),
+ new MarkupBlock(
+ Factory.Markup(" <a"),
+ new MarkupBlock(new AttributeBlockCodeGenerator("href", new LocationTagged<string>(" href='", 17, 0, 17), new LocationTagged<string>("'", 37, 0, 37)),
+ Factory.Markup(" href='").With(SpanCodeGenerator.Null),
+ Factory.Markup("~/Foo/")
+ .WithEditorHints(EditorHints.VirtualPath)
+ .With(new LiteralAttributeCodeGenerator(
+ new LocationTagged<string>(String.Empty, 24, 0, 24),
+ new LocationTagged<SpanCodeGenerator>(new ResolveUrlCodeGenerator(), 24, 0, 24))),
+ new MarkupBlock(new DynamicAttributeBlockCodeGenerator(new LocationTagged<string>(String.Empty, 30, 0, 30), 30, 0, 30),
+ new ExpressionBlock(
+ Factory.CodeTransition().Accepts(AcceptedCharacters.None),
+ Factory.Code("id")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace))),
+ Factory.Markup("/Baz")
+ .With(new LiteralAttributeCodeGenerator(new LocationTagged<string>(String.Empty, 33, 0, 33), new LocationTagged<string>("/Baz", 33, 0, 33))),
+ Factory.Markup("'").With(SpanCodeGenerator.Null)),
+ Factory.Markup(" /> ")),
+ Factory.MetaCode("}").Accepts(AcceptedCharacters.None)),
+ Factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void UrlWithComplexCharactersInAttributeInMarkupBlock()
+ {
+ ParseBlockTest("<a href='~/Foo+Bar:Baz(Biz),Boz' />",
+ new MarkupBlock(
+ Factory.Markup("<a"),
+ new MarkupBlock(new AttributeBlockCodeGenerator("href", new LocationTagged<string>(" href='", 2, 0, 2), new LocationTagged<string>("'", 31, 0, 31)),
+ Factory.Markup(" href='").With(SpanCodeGenerator.Null),
+ Factory.Markup("~/Foo+Bar:Baz(Biz),Boz")
+ .WithEditorHints(EditorHints.VirtualPath)
+ .With(new LiteralAttributeCodeGenerator(
+ new LocationTagged<string>(String.Empty, 9, 0, 9),
+ new LocationTagged<SpanCodeGenerator>(new ResolveUrlCodeGenerator(), 9, 0, 9))),
+ Factory.Markup("'").With(SpanCodeGenerator.Null)),
+ Factory.Markup(" />").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void UrlWithComplexCharactersInAttributeInMarkupDocument()
+ {
+ ParseDocumentTest("<a href='~/Foo+Bar:Baz(Biz),Boz' />",
+ new MarkupBlock(
+ Factory.Markup("<a"),
+ new MarkupBlock(new AttributeBlockCodeGenerator("href", new LocationTagged<string>(" href='", 2, 0, 2), new LocationTagged<string>("'", 31, 0, 31)),
+ Factory.Markup(" href='").With(SpanCodeGenerator.Null),
+ Factory.Markup("~/Foo+Bar:Baz(Biz),Boz")
+ .WithEditorHints(EditorHints.VirtualPath)
+ .With(new LiteralAttributeCodeGenerator(
+ new LocationTagged<string>(String.Empty, 9, 0, 9),
+ new LocationTagged<SpanCodeGenerator>(new ResolveUrlCodeGenerator(), 9, 0, 9))),
+ Factory.Markup("'").With(SpanCodeGenerator.Null)),
+ Factory.Markup(" />")));
+ }
+
+ [Fact]
+ public void UrlInUnquotedAttributeValueInMarkupBlock()
+ {
+ ParseBlockTest("<a href=~/Foo+Bar:Baz(Biz),Boz/@id/Boz />",
+ new MarkupBlock(
+ Factory.Markup("<a"),
+ new MarkupBlock(new AttributeBlockCodeGenerator("href", new LocationTagged<string>(" href=", 2, 0, 2), new LocationTagged<string>(String.Empty, 38, 0, 38)),
+ Factory.Markup(" href=").With(SpanCodeGenerator.Null),
+ Factory.Markup("~/Foo+Bar:Baz(Biz),Boz/")
+ .WithEditorHints(EditorHints.VirtualPath)
+ .With(new LiteralAttributeCodeGenerator(
+ new LocationTagged<string>(String.Empty, 8, 0, 8),
+ new LocationTagged<SpanCodeGenerator>(new ResolveUrlCodeGenerator(), 8, 0, 8))),
+ new MarkupBlock(new DynamicAttributeBlockCodeGenerator(new LocationTagged<string>(String.Empty, 31, 0, 31), 31, 0, 31),
+ new ExpressionBlock(
+ Factory.CodeTransition()
+ .Accepts(AcceptedCharacters.None),
+ Factory.Code("id")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace))),
+ Factory.Markup("/Boz").With(new LiteralAttributeCodeGenerator(new LocationTagged<string>(String.Empty, 34, 0, 34), new LocationTagged<string>("/Boz", 34, 0, 34)))),
+ Factory.Markup(" />").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void UrlInUnquotedAttributeValueInMarkupDocument()
+ {
+ ParseDocumentTest("<a href=~/Foo+Bar:Baz(Biz),Boz/@id/Boz />",
+ new MarkupBlock(
+ Factory.Markup("<a"),
+ new MarkupBlock(new AttributeBlockCodeGenerator("href", new LocationTagged<string>(" href=", 2, 0, 2), new LocationTagged<string>(String.Empty, 38, 0, 38)),
+ Factory.Markup(" href=").With(SpanCodeGenerator.Null),
+ Factory.Markup("~/Foo+Bar:Baz(Biz),Boz/")
+ .WithEditorHints(EditorHints.VirtualPath)
+ .With(new LiteralAttributeCodeGenerator(
+ new LocationTagged<string>(String.Empty, 8, 0, 8),
+ new LocationTagged<SpanCodeGenerator>(new ResolveUrlCodeGenerator(), 8, 0, 8))),
+ new MarkupBlock(new DynamicAttributeBlockCodeGenerator(new LocationTagged<string>(String.Empty, 31, 0, 31), 31, 0, 31),
+ new ExpressionBlock(
+ Factory.CodeTransition()
+ .Accepts(AcceptedCharacters.None),
+ Factory.Code("id")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace))),
+ Factory.Markup("/Boz").With(new LiteralAttributeCodeGenerator(new LocationTagged<string>(String.Empty, 34, 0, 34), new LocationTagged<string>("/Boz", 34, 0, 34)))),
+ Factory.Markup(" />")));
+ }
+
+ [Fact]
+ public void UrlInUnquotedAttributeValueInMarkupSection()
+ {
+ ParseDocumentTest("@section Foo { <a href=~/Foo+Bar:Baz(Biz),Boz/@id/Boz /> }",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new SectionBlock(new SectionCodeGenerator("Foo"),
+ Factory.CodeTransition(),
+ Factory.MetaCode("section Foo {")
+ .AutoCompleteWith(null, atEndOfSpan: true),
+ new MarkupBlock(
+ Factory.Markup(" <a"),
+ new MarkupBlock(new AttributeBlockCodeGenerator("href", new LocationTagged<string>(" href=", 17, 0, 17), new LocationTagged<string>(String.Empty, 53, 0, 53)),
+ Factory.Markup(" href=").With(SpanCodeGenerator.Null),
+ Factory.Markup("~/Foo+Bar:Baz(Biz),Boz/")
+ .WithEditorHints(EditorHints.VirtualPath)
+ .With(new LiteralAttributeCodeGenerator(
+ new LocationTagged<string>(String.Empty, 23, 0, 23),
+ new LocationTagged<SpanCodeGenerator>(new ResolveUrlCodeGenerator(), 23, 0, 23))),
+ new MarkupBlock(new DynamicAttributeBlockCodeGenerator(new LocationTagged<string>(String.Empty, 46, 0, 46), 46, 0, 46),
+ new ExpressionBlock(
+ Factory.CodeTransition()
+ .Accepts(AcceptedCharacters.None),
+ Factory.Code("id")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace))),
+ Factory.Markup("/Boz").With(new LiteralAttributeCodeGenerator(new LocationTagged<string>(String.Empty, 49, 0, 49), new LocationTagged<string>("/Boz", 49, 0, 49)))),
+ Factory.Markup(" /> ")),
+ Factory.MetaCode("}").Accepts(AcceptedCharacters.None)),
+ Factory.EmptyHtml()));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/Old/CsHtmlDocumentTest.cs b/test/System.Web.Razor.Test/Parser/Old/CsHtmlDocumentTest.cs
new file mode 100644
index 00000000..7480d2ba
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/Old/CsHtmlDocumentTest.cs
@@ -0,0 +1,267 @@
+using System.Web.Razor.Parser;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Test.Framework;
+using System.Web.Razor.Text;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using System.Web.Razor.Parser.SyntaxTree;
+
+namespace System.Web.Razor.Test.Parser.CSharp {
+ [TestClass]
+ public class CsHtmlDocumentTest : CsHtmlMarkupParserTestBase {
+ [TestMethod]
+ public void UnterminatedBlockCommentCausesRazorError() {
+ ParseDocumentTest(@"@* Foo Bar",
+ new MarkupBlock(
+ new MarkupSpan(String.Empty),
+ new CommentBlock(
+ new TransitionSpan(RazorParser.TransitionString, hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new MetaCodeSpan("*", hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new CommentSpan(" Foo Bar")
+ )
+ ),
+ new RazorError(RazorResources.ParseError_RazorComment_Not_Terminated, SourceLocation.Zero));
+ }
+
+ [TestMethod]
+ public void BlockCommentInMarkupDocumentIsHandledCorrectly() {
+ ParseDocumentTest(@"<ul>
+ @* This is a block comment </ul> *@ foo",
+ new MarkupBlock(
+ new MarkupSpan(@"<ul>
+ "),
+ new CommentBlock(
+ new TransitionSpan(RazorParser.TransitionString, hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new MetaCodeSpan("*", hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new CommentSpan(" This is a block comment </ul> "),
+ new MetaCodeSpan("*", hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new TransitionSpan(RazorParser.TransitionString, hidden: false, acceptedCharacters: AcceptedCharacters.None)
+ ),
+ new MarkupSpan(" foo")
+ ));
+ }
+
+ [TestMethod]
+ public void BlockCommentInMarkupBlockIsHandledCorrectly() {
+ ParseBlockTest(@"<ul>
+ @* This is a block comment </ul> *@ foo </ul>",
+ new MarkupBlock(
+ new MarkupSpan(@"<ul>
+ "),
+ new CommentBlock(
+ new TransitionSpan(RazorParser.TransitionString, hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new MetaCodeSpan("*", hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new CommentSpan(" This is a block comment </ul> "),
+ new MetaCodeSpan("*", hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new TransitionSpan(RazorParser.TransitionString, hidden: false, acceptedCharacters: AcceptedCharacters.None)
+ ),
+ new MarkupSpan(" foo </ul>", hidden: false, acceptedCharacters: AcceptedCharacters.None)
+ ));
+ }
+
+ [TestMethod]
+ public void BlockCommentAtStatementStartInCodeBlockIsHandledCorrectly() {
+ ParseDocumentTest(@"@if(Request.IsAuthenticated) {
+ @* User is logged in! } *@
+ Write(""Hello friend!"");
+}",
+ new MarkupBlock(
+ new StatementBlock(
+ new TransitionSpan(RazorParser.TransitionString, hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new CodeSpan(@"if(Request.IsAuthenticated) {
+ "),
+ new CommentBlock(
+ new TransitionSpan(RazorParser.TransitionString, hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new MetaCodeSpan("*", hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new CommentSpan(" User is logged in! } "),
+ new MetaCodeSpan("*", hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new TransitionSpan(RazorParser.TransitionString, hidden: false, acceptedCharacters: AcceptedCharacters.None)
+ ),
+ new CodeSpan(@"
+ Write(""Hello friend!"");
+}"))));
+ }
+
+ [TestMethod]
+ public void BlockCommentInStatementInCodeBlockIsHandledCorrectly() {
+ ParseDocumentTest(@"@if(Request.IsAuthenticated) {
+ var foo = @* User is logged in! ; *@;
+ Write(""Hello friend!"");
+}",
+ new MarkupBlock(
+ new StatementBlock(
+ new TransitionSpan(RazorParser.TransitionString, hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new CodeSpan(@"if(Request.IsAuthenticated) {
+ var foo = "),
+ new CommentBlock(
+ new TransitionSpan(RazorParser.TransitionString, hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new MetaCodeSpan("*", hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new CommentSpan(" User is logged in! ; "),
+ new MetaCodeSpan("*", hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new TransitionSpan(RazorParser.TransitionString, hidden: false, acceptedCharacters: AcceptedCharacters.None)
+ ),
+ new CodeSpan(@";
+ Write(""Hello friend!"");
+}"))));
+ }
+
+ [TestMethod]
+ public void BlockCommentInStringIsIgnored() {
+ ParseDocumentTest(@"@if(Request.IsAuthenticated) {
+ var foo = ""@* User is logged in! ; *@"";
+ Write(""Hello friend!"");
+}",
+ new MarkupBlock(
+ new StatementBlock(
+ new TransitionSpan(RazorParser.TransitionString, hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new CodeSpan(@"if(Request.IsAuthenticated) {
+ var foo = ""@* User is logged in! ; *@"";
+ Write(""Hello friend!"");
+}"))));
+ }
+
+ [TestMethod]
+ public void BlockCommentInCSharpBlockCommentIsIgnored() {
+ ParseDocumentTest(@"@if(Request.IsAuthenticated) {
+ var foo = /*@* User is logged in! */ *@ */;
+ Write(""Hello friend!"");
+}",
+ new MarkupBlock(
+ new StatementBlock(
+ new TransitionSpan(RazorParser.TransitionString, hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new CodeSpan(@"if(Request.IsAuthenticated) {
+ var foo = /*@* User is logged in! */ *@ */;
+ Write(""Hello friend!"");
+}"))));
+ }
+
+ [TestMethod]
+ public void BlockCommentInCSharpLineCommentIsIgnored() {
+ ParseDocumentTest(@"@if(Request.IsAuthenticated) {
+ var foo = //@* User is logged in! */ *@;
+ Write(""Hello friend!"");
+}",
+ new MarkupBlock(
+ new StatementBlock(
+ new TransitionSpan(RazorParser.TransitionString, hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new CodeSpan(@"if(Request.IsAuthenticated) {
+ var foo = //@* User is logged in! */ *@;
+ Write(""Hello friend!"");
+}"))));
+ }
+
+ [TestMethod]
+ public void BlockCommentInImplicitExpressionIsHandledCorrectly() {
+ ParseDocumentTest(@"@Html.Foo@*bar*@",
+ new MarkupBlock(
+ new ExpressionBlock(
+ new TransitionSpan(RazorParser.TransitionString, hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new ImplicitExpressionSpan(@"Html.Foo", CSharpCodeParser.DefaultKeywords, acceptTrailingDot: false, acceptedCharacters: AcceptedCharacters.NonWhiteSpace)
+ ),
+ new MarkupSpan(String.Empty),
+ new CommentBlock(
+ new TransitionSpan(RazorParser.TransitionString, hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new MetaCodeSpan("*", hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new CommentSpan("bar"),
+ new MetaCodeSpan("*", hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new TransitionSpan(RazorParser.TransitionString, hidden: false, acceptedCharacters: AcceptedCharacters.None)
+ ),
+ new MarkupSpan(String.Empty)));
+ }
+
+ [TestMethod]
+ public void BlockCommentAfterDotOfImplicitExpressionIsHandledCorrectly() {
+ ParseDocumentTest(@"@Html.@*bar*@",
+ new MarkupBlock(
+ new ExpressionBlock(
+ new TransitionSpan(RazorParser.TransitionString, hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new ImplicitExpressionSpan(@"Html", CSharpCodeParser.DefaultKeywords, acceptTrailingDot: false, acceptedCharacters: AcceptedCharacters.NonWhiteSpace)
+ ),
+ new MarkupSpan("."),
+ new CommentBlock(
+ new TransitionSpan(RazorParser.TransitionString, hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new MetaCodeSpan("*", hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new CommentSpan("bar"),
+ new MetaCodeSpan("*", hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new TransitionSpan(RazorParser.TransitionString, hidden: false, acceptedCharacters: AcceptedCharacters.None)
+ ),
+ new MarkupSpan(String.Empty)));
+ }
+
+ [TestMethod]
+ public void BlockCommentInParensOfImplicitExpressionIsHandledCorrectly() {
+ ParseDocumentTest(@"@Html.Foo(@*bar*@ 4)",
+ new MarkupBlock(
+ new ExpressionBlock(
+ new TransitionSpan(RazorParser.TransitionString, hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new ImplicitExpressionSpan(@"Html.Foo(", CSharpCodeParser.DefaultKeywords, acceptTrailingDot: false, acceptedCharacters: AcceptedCharacters.Any),
+ new CommentBlock(
+ new TransitionSpan(RazorParser.TransitionString, hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new MetaCodeSpan("*", hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new CommentSpan("bar"),
+ new MetaCodeSpan("*", hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new TransitionSpan(RazorParser.TransitionString, hidden: false, acceptedCharacters: AcceptedCharacters.None)
+ ),
+ new ImplicitExpressionSpan(" 4)", CSharpCodeParser.DefaultKeywords, acceptTrailingDot: false, acceptedCharacters: AcceptedCharacters.NonWhiteSpace)
+ ),
+ new MarkupSpan(String.Empty)));
+ }
+
+ [TestMethod]
+ public void BlockCommentInBracketsOfImplicitExpressionIsHandledCorrectly() {
+ ParseDocumentTest(@"@Html.Foo[@*bar*@ 4]",
+ new MarkupBlock(
+ new ExpressionBlock(
+ new TransitionSpan(RazorParser.TransitionString, hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new ImplicitExpressionSpan(@"Html.Foo[", CSharpCodeParser.DefaultKeywords, acceptTrailingDot: false, acceptedCharacters: AcceptedCharacters.Any),
+ new CommentBlock(
+ new TransitionSpan(RazorParser.TransitionString, hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new MetaCodeSpan("*", hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new CommentSpan("bar"),
+ new MetaCodeSpan("*", hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new TransitionSpan(RazorParser.TransitionString, hidden: false, acceptedCharacters: AcceptedCharacters.None)
+ ),
+ new ImplicitExpressionSpan(" 4]", CSharpCodeParser.DefaultKeywords, acceptTrailingDot: false, acceptedCharacters: AcceptedCharacters.NonWhiteSpace)
+ ),
+ new MarkupSpan(String.Empty)));
+ }
+
+ [TestMethod]
+ public void BlockCommentInParensOfConditionIsHandledCorrectly() {
+ ParseDocumentTest(@"@if(@*bar*@) {}",
+ new MarkupBlock(
+ new StatementBlock(
+ new TransitionSpan(RazorParser.TransitionString, hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new CodeSpan(@"if("),
+ new CommentBlock(
+ new TransitionSpan(RazorParser.TransitionString, hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new MetaCodeSpan("*", hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new CommentSpan("bar"),
+ new MetaCodeSpan("*", hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new TransitionSpan(RazorParser.TransitionString, hidden: false, acceptedCharacters: AcceptedCharacters.None)
+ ),
+ new CodeSpan(") {}")
+ )));
+ }
+
+ [TestMethod]
+ public void BlockCommentInExplicitExpressionIsHandledCorrectly() {
+ ParseDocumentTest(@"@(1 + @*bar*@ 1)",
+ new MarkupBlock(
+ new ExpressionBlock(
+ new TransitionSpan(RazorParser.TransitionString, hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new MetaCodeSpan("(", hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new CodeSpan(@"1 + "),
+ new CommentBlock(
+ new TransitionSpan(RazorParser.TransitionString, hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new MetaCodeSpan("*", hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new CommentSpan("bar"),
+ new MetaCodeSpan("*", hidden: false, acceptedCharacters: AcceptedCharacters.None),
+ new TransitionSpan(RazorParser.TransitionString, hidden: false, acceptedCharacters: AcceptedCharacters.None)
+ ),
+ new CodeSpan(" 1"),
+ new MetaCodeSpan(")", hidden: false, acceptedCharacters: AcceptedCharacters.None)
+ ),
+ new MarkupSpan(String.Empty)));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/ParserContextTest.cs b/test/System.Web.Razor.Test/Parser/ParserContextTest.cs
new file mode 100644
index 00000000..bd7e5ab6
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/ParserContextTest.cs
@@ -0,0 +1,240 @@
+using System.IO;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Test.Framework;
+using System.Web.Razor.Text;
+using System.Web.Razor.Tokenizer.Symbols;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Razor.Test.Parser
+{
+ public class ParserContextTest
+ {
+ [Fact]
+ public void ConstructorRequiresNonNullSource()
+ {
+ var codeParser = new CSharpCodeParser();
+ Assert.ThrowsArgumentNull(() => new ParserContext(null, codeParser, new HtmlMarkupParser(), codeParser), "source");
+ }
+
+ [Fact]
+ public void ConstructorRequiresNonNullCodeParser()
+ {
+ var codeParser = new CSharpCodeParser();
+ Assert.ThrowsArgumentNull(() => new ParserContext(new SeekableTextReader(TextReader.Null), null, new HtmlMarkupParser(), codeParser), "codeParser");
+ }
+
+ [Fact]
+ public void ConstructorRequiresNonNullMarkupParser()
+ {
+ var codeParser = new CSharpCodeParser();
+ Assert.ThrowsArgumentNull(() => new ParserContext(new SeekableTextReader(TextReader.Null), codeParser, null, codeParser), "markupParser");
+ }
+
+ [Fact]
+ public void ConstructorRequiresNonNullActiveParser()
+ {
+ Assert.ThrowsArgumentNull(() => new ParserContext(new SeekableTextReader(TextReader.Null), new CSharpCodeParser(), new HtmlMarkupParser(), null), "activeParser");
+ }
+
+ [Fact]
+ public void ConstructorThrowsIfActiveParserIsNotCodeOrMarkupParser()
+ {
+ Assert.ThrowsArgument(() => new ParserContext(new SeekableTextReader(TextReader.Null), new CSharpCodeParser(), new HtmlMarkupParser(), new CSharpCodeParser()),
+ "activeParser",
+ RazorResources.ActiveParser_Must_Be_Code_Or_Markup_Parser);
+ }
+
+ [Fact]
+ public void ConstructorAcceptsActiveParserIfIsSameAsEitherCodeOrMarkupParser()
+ {
+ var codeParser = new CSharpCodeParser();
+ var markupParser = new HtmlMarkupParser();
+ new ParserContext(new SeekableTextReader(TextReader.Null), codeParser, markupParser, codeParser);
+ new ParserContext(new SeekableTextReader(TextReader.Null), codeParser, markupParser, markupParser);
+ }
+
+ [Fact]
+ public void ConstructorInitializesProperties()
+ {
+ // Arrange
+ SeekableTextReader expectedBuffer = new SeekableTextReader(TextReader.Null);
+ CSharpCodeParser expectedCodeParser = new CSharpCodeParser();
+ HtmlMarkupParser expectedMarkupParser = new HtmlMarkupParser();
+
+ // Act
+ ParserContext context = new ParserContext(expectedBuffer, expectedCodeParser, expectedMarkupParser, expectedCodeParser);
+
+ // Assert
+ Assert.NotNull(context.Source);
+ Assert.Same(expectedCodeParser, context.CodeParser);
+ Assert.Same(expectedMarkupParser, context.MarkupParser);
+ Assert.Same(expectedCodeParser, context.ActiveParser);
+ }
+
+ [Fact]
+ public void CurrentCharacterReturnsCurrentCharacterInTextBuffer()
+ {
+ // Arrange
+ ParserContext context = SetupTestContext("bar", b => b.Read());
+
+ // Act
+ char actual = context.CurrentCharacter;
+
+ // Assert
+ Assert.Equal('a', actual);
+ }
+
+ [Fact]
+ public void CurrentCharacterReturnsNulCharacterIfTextBufferAtEOF()
+ {
+ // Arrange
+ ParserContext context = SetupTestContext("bar", b => b.ReadToEnd());
+
+ // Act
+ char actual = context.CurrentCharacter;
+
+ // Assert
+ Assert.Equal('\0', actual);
+ }
+
+ [Fact]
+ public void EndOfFileReturnsFalseIfTextBufferNotAtEOF()
+ {
+ // Arrange
+ ParserContext context = SetupTestContext("bar");
+
+ // Act/Assert
+ Assert.False(context.EndOfFile);
+ }
+
+ [Fact]
+ public void EndOfFileReturnsTrueIfTextBufferAtEOF()
+ {
+ // Arrange
+ ParserContext context = SetupTestContext("bar", b => b.ReadToEnd());
+
+ // Act/Assert
+ Assert.True(context.EndOfFile);
+ }
+
+ [Fact]
+ public void StartBlockCreatesNewBlock()
+ {
+ // Arrange
+ ParserContext context = SetupTestContext("phoo");
+
+ // Act
+ context.StartBlock(BlockType.Expression);
+
+ // Assert
+ Assert.Equal(1, context.BlockStack.Count);
+ Assert.Equal(BlockType.Expression, context.BlockStack.Peek().Type);
+ }
+
+ [Fact]
+ public void EndBlockAddsCurrentBlockToParentBlock()
+ {
+ // Arrange
+ Mock<ParserVisitor> mockListener = new Mock<ParserVisitor>();
+ ParserContext context = SetupTestContext("phoo");
+
+ // Act
+ context.StartBlock(BlockType.Expression);
+ context.StartBlock(BlockType.Statement);
+ context.EndBlock();
+
+ // Assert
+ Assert.Equal(1, context.BlockStack.Count);
+ Assert.Equal(BlockType.Expression, context.BlockStack.Peek().Type);
+ Assert.Equal(1, context.BlockStack.Peek().Children.Count);
+ Assert.Equal(BlockType.Statement, ((Block)context.BlockStack.Peek().Children[0]).Type);
+ }
+
+ [Fact]
+ public void AddSpanAddsSpanToCurrentBlockBuilder()
+ {
+ // Arrange
+ var factory = SpanFactory.CreateCsHtml();
+ Mock<ParserVisitor> mockListener = new Mock<ParserVisitor>();
+ ParserContext context = SetupTestContext("phoo");
+
+ SpanBuilder builder = new SpanBuilder()
+ {
+ Kind = SpanKind.Code
+ };
+ builder.Accept(new CSharpSymbol(1, 0, 1, "foo", CSharpSymbolType.Identifier));
+ Span added = builder.Build();
+
+ using (context.StartBlock(BlockType.Functions))
+ {
+ context.AddSpan(added);
+ }
+
+ BlockBuilder expected = new BlockBuilder()
+ {
+ Type = BlockType.Functions,
+ };
+ expected.Children.Add(added);
+
+ // Assert
+ ParserTestBase.EvaluateResults(context.CompleteParse(), expected.Build());
+ }
+
+ [Fact]
+ public void SwitchActiveParserSetsMarkupParserAsActiveIfCodeParserCurrentlyActive()
+ {
+ // Arrange
+ var codeParser = new CSharpCodeParser();
+ var markupParser = new HtmlMarkupParser();
+ ParserContext context = SetupTestContext("barbazbiz", b => b.Read(), codeParser, markupParser, codeParser);
+ Assert.Same(codeParser, context.ActiveParser);
+
+ // Act
+ context.SwitchActiveParser();
+
+ // Assert
+ Assert.Same(markupParser, context.ActiveParser);
+ }
+
+ [Fact]
+ public void SwitchActiveParserSetsCodeParserAsActiveIfMarkupParserCurrentlyActive()
+ {
+ // Arrange
+ var codeParser = new CSharpCodeParser();
+ var markupParser = new HtmlMarkupParser();
+ ParserContext context = SetupTestContext("barbazbiz", b => b.Read(), codeParser, markupParser, markupParser);
+ Assert.Same(markupParser, context.ActiveParser);
+
+ // Act
+ context.SwitchActiveParser();
+
+ // Assert
+ Assert.Same(codeParser, context.ActiveParser);
+ }
+
+ private ParserContext SetupTestContext(string document)
+ {
+ var codeParser = new CSharpCodeParser();
+ var markupParser = new HtmlMarkupParser();
+ return SetupTestContext(document, b => { }, codeParser, markupParser, codeParser);
+ }
+
+ private ParserContext SetupTestContext(string document, Action<TextReader> positioningAction)
+ {
+ var codeParser = new CSharpCodeParser();
+ var markupParser = new HtmlMarkupParser();
+ return SetupTestContext(document, positioningAction, codeParser, markupParser, codeParser);
+ }
+
+ private ParserContext SetupTestContext(string document, Action<TextReader> positioningAction, ParserBase codeParser, ParserBase markupParser, ParserBase activeParser)
+ {
+ ParserContext context = new ParserContext(new SeekableTextReader(new StringReader(document)), codeParser, markupParser, activeParser);
+ positioningAction(context.Source);
+ return context;
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/ParserVisitorExtensionsTest.cs b/test/System.Web.Razor.Test/Parser/ParserVisitorExtensionsTest.cs
new file mode 100644
index 00000000..903354ec
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/ParserVisitorExtensionsTest.cs
@@ -0,0 +1,82 @@
+using System.Collections.Generic;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Razor.Test.Parser
+{
+ public class ParserVisitorExtensionsTest
+ {
+ [Fact]
+ public void VisitThrowsOnNullVisitor()
+ {
+ ParserVisitor target = null;
+ ParserResults results = new ParserResults(new BlockBuilder() { Type = BlockType.Comment }.Build(), new List<RazorError>());
+
+ Assert.ThrowsArgumentNull(() => target.Visit(results), "self");
+ }
+
+ [Fact]
+ public void VisitThrowsOnNullResults()
+ {
+ ParserVisitor target = new Mock<ParserVisitor>().Object;
+ Assert.ThrowsArgumentNull(() => target.Visit(null), "result");
+ }
+
+ [Fact]
+ public void VisitSendsDocumentToVisitor()
+ {
+ // Arrange
+ Mock<ParserVisitor> targetMock = new Mock<ParserVisitor>();
+ Block root = new BlockBuilder() { Type = BlockType.Comment }.Build();
+ ParserResults results = new ParserResults(root, new List<RazorError>());
+
+ // Act
+ targetMock.Object.Visit(results);
+
+ // Assert
+ targetMock.Verify(v => v.VisitBlock(root));
+ }
+
+ [Fact]
+ public void VisitSendsErrorsToVisitor()
+ {
+ // Arrange
+ Mock<ParserVisitor> targetMock = new Mock<ParserVisitor>();
+ Block root = new BlockBuilder() { Type = BlockType.Comment }.Build();
+ List<RazorError> errors = new List<RazorError>() {
+ new RazorError("Foo", 1, 0, 1),
+ new RazorError("Bar", 2, 0, 2)
+ };
+ ParserResults results = new ParserResults(root, errors);
+
+ // Act
+ targetMock.Object.Visit(results);
+
+ // Assert
+ targetMock.Verify(v => v.VisitError(errors[0]));
+ targetMock.Verify(v => v.VisitError(errors[1]));
+ }
+
+ [Fact]
+ public void VisitCallsOnCompleteWhenAllNodesHaveBeenVisited()
+ {
+ // Arrange
+ Mock<ParserVisitor> targetMock = new Mock<ParserVisitor>();
+ Block root = new BlockBuilder() { Type = BlockType.Comment }.Build();
+ List<RazorError> errors = new List<RazorError>() {
+ new RazorError("Foo", 1, 0, 1),
+ new RazorError("Bar", 2, 0, 2)
+ };
+ ParserResults results = new ParserResults(root, errors);
+
+ // Act
+ targetMock.Object.Visit(results);
+
+ // Assert
+ targetMock.Verify(v => v.OnComplete());
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/PartialParsing/CSharpPartialParsingTest.cs b/test/System.Web.Razor.Test/Parser/PartialParsing/CSharpPartialParsingTest.cs
new file mode 100644
index 00000000..b57dbd19
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/PartialParsing/CSharpPartialParsingTest.cs
@@ -0,0 +1,400 @@
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Test.Framework;
+using System.Web.Razor.Text;
+using System.Web.WebPages.TestUtils;
+using Xunit;
+
+namespace System.Web.Razor.Test.Parser.PartialParsing
+{
+ public class CSharpPartialParsingTest : PartialParsingTestBase<CSharpRazorCodeLanguage>
+ {
+ [Fact]
+ public void ImplicitExpressionProvisionallyAcceptsDeleteOfIdentifierPartsIfDotRemains()
+ {
+ var factory = SpanFactory.CreateCsHtml();
+ StringTextBuffer changed = new StringTextBuffer("foo @User. baz");
+ StringTextBuffer old = new StringTextBuffer("foo @User.Name baz");
+ RunPartialParseTest(new TextChange(10, 4, old, 0, changed),
+ new MarkupBlock(
+ factory.Markup("foo "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("User.").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharacters.NonWhiteSpace)),
+ factory.Markup(" baz")),
+ additionalFlags: PartialParseResult.Provisional);
+ }
+
+ [Fact]
+ public void ImplicitExpressionAcceptsDeleteOfIdentifierPartsIfSomeOfIdentifierRemains()
+ {
+ var factory = SpanFactory.CreateCsHtml();
+ StringTextBuffer changed = new StringTextBuffer("foo @Us baz");
+ StringTextBuffer old = new StringTextBuffer("foo @User baz");
+ RunPartialParseTest(new TextChange(7, 2, old, 0, changed),
+ new MarkupBlock(
+ factory.Markup("foo "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("Us").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharacters.NonWhiteSpace)),
+ factory.Markup(" baz")));
+ }
+
+ [Fact]
+ public void ImplicitExpressionProvisionallyAcceptsMultipleInsertionIfItCausesIdentifierExpansionAndTrailingDot()
+ {
+ var factory = SpanFactory.CreateCsHtml();
+ StringTextBuffer changed = new StringTextBuffer("foo @User. baz");
+ StringTextBuffer old = new StringTextBuffer("foo @U baz");
+ RunPartialParseTest(new TextChange(6, 0, old, 4, changed),
+ new MarkupBlock(
+ factory.Markup("foo "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("User.").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharacters.NonWhiteSpace)),
+ factory.Markup(" baz")),
+ additionalFlags: PartialParseResult.Provisional);
+ }
+
+ [Fact]
+ public void ImplicitExpressionAcceptsMultipleInsertionIfItOnlyCausesIdentifierExpansion()
+ {
+ var factory = SpanFactory.CreateCsHtml();
+ StringTextBuffer changed = new StringTextBuffer("foo @barbiz baz");
+ StringTextBuffer old = new StringTextBuffer("foo @bar baz");
+ RunPartialParseTest(new TextChange(8, 0, old, 3, changed),
+ new MarkupBlock(
+ factory.Markup("foo "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("barbiz").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharacters.NonWhiteSpace)),
+ factory.Markup(" baz")));
+ }
+
+ [Fact]
+ public void ImplicitExpressionAcceptsIdentifierExpansionAtEndOfNonWhitespaceCharacters()
+ {
+ var factory = SpanFactory.CreateCsHtml();
+ StringTextBuffer changed = new StringTextBuffer(@"@{
+ @food
+}");
+ StringTextBuffer old = new StringTextBuffer(@"@{
+ @foo
+}");
+ RunPartialParseTest(new TextChange(12, 0, old, 1, changed),
+ new MarkupBlock(
+ factory.EmptyHtml(),
+ new StatementBlock(
+ factory.CodeTransition(),
+ factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ factory.Code("\r\n ").AsStatement(),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("food")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ factory.Code("\r\n").AsStatement(),
+ factory.MetaCode("}").Accepts(AcceptedCharacters.None)),
+ factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void ImplicitExpressionAcceptsIdentifierAfterDotAtEndOfNonWhitespaceCharacters()
+ {
+ var factory = SpanFactory.CreateCsHtml();
+ StringTextBuffer changed = new StringTextBuffer(@"@{
+ @foo.d
+}");
+ StringTextBuffer old = new StringTextBuffer(@"@{
+ @foo.
+}");
+ RunPartialParseTest(new TextChange(13, 0, old, 1, changed),
+ new MarkupBlock(
+ factory.EmptyHtml(),
+ new StatementBlock(
+ factory.CodeTransition(),
+ factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ factory.Code("\r\n ").AsStatement(),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("foo.d")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ factory.Code("\r\n").AsStatement(),
+ factory.MetaCode("}").Accepts(AcceptedCharacters.None)),
+ factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void ImplicitExpressionAcceptsDotAtEndOfNonWhitespaceCharacters()
+ {
+ var factory = SpanFactory.CreateCsHtml();
+ StringTextBuffer changed = new StringTextBuffer(@"@{
+ @foo.
+}");
+ StringTextBuffer old = new StringTextBuffer(@"@{
+ @foo
+}");
+ RunPartialParseTest(new TextChange(12, 0, old, 1, changed),
+ new MarkupBlock(
+ factory.EmptyHtml(),
+ new StatementBlock(
+ factory.CodeTransition(),
+ factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ factory.Code("\r\n ").AsStatement(),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code(@"foo.")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ factory.Code("\r\n").AsStatement(),
+ factory.MetaCode("}").Accepts(AcceptedCharacters.None)),
+ factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void ImplicitExpressionRejectsChangeWhichWouldHaveBeenAcceptedIfLastChangeWasProvisionallyAcceptedOnDifferentSpan()
+ {
+ var factory = SpanFactory.CreateCsHtml();
+
+ // Arrange
+ TextChange dotTyped = new TextChange(8, 0, new StringTextBuffer("foo @foo @bar"), 1, new StringTextBuffer("foo @foo. @bar"));
+ TextChange charTyped = new TextChange(14, 0, new StringTextBuffer("foo @foo. @bar"), 1, new StringTextBuffer("foo @foo. @barb"));
+ TestParserManager manager = CreateParserManager();
+ manager.InitializeWithDocument(dotTyped.OldBuffer);
+
+ // Apply the dot change
+ Assert.Equal(PartialParseResult.Provisional | PartialParseResult.Accepted, manager.CheckForStructureChangesAndWait(dotTyped));
+
+ // Act (apply the identifier start char change)
+ PartialParseResult result = manager.CheckForStructureChangesAndWait(charTyped);
+
+ // Assert
+ Assert.Equal(PartialParseResult.Rejected, result);
+ Assert.False(manager.Parser.LastResultProvisional, "LastResultProvisional flag should have been cleared but it was not");
+ ParserTestBase.EvaluateParseTree(manager.Parser.CurrentParseTree,
+ new MarkupBlock(
+ factory.Markup("foo "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("foo")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ factory.Markup(". "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("barb")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void ImplicitExpressionAcceptsIdentifierTypedAfterDotIfLastChangeWasProvisionalAcceptanceOfDot()
+ {
+ var factory = SpanFactory.CreateCsHtml();
+
+ // Arrange
+ TextChange dotTyped = new TextChange(8, 0, new StringTextBuffer("foo @foo bar"), 1, new StringTextBuffer("foo @foo. bar"));
+ TextChange charTyped = new TextChange(9, 0, new StringTextBuffer("foo @foo. bar"), 1, new StringTextBuffer("foo @foo.b bar"));
+ TestParserManager manager = CreateParserManager();
+ manager.InitializeWithDocument(dotTyped.OldBuffer);
+
+ // Apply the dot change
+ Assert.Equal(PartialParseResult.Provisional | PartialParseResult.Accepted, manager.CheckForStructureChangesAndWait(dotTyped));
+
+ // Act (apply the identifier start char change)
+ PartialParseResult result = manager.CheckForStructureChangesAndWait(charTyped);
+
+ // Assert
+ Assert.Equal(PartialParseResult.Accepted, result);
+ Assert.False(manager.Parser.LastResultProvisional, "LastResultProvisional flag should have been cleared but it was not");
+ ParserTestBase.EvaluateParseTree(manager.Parser.CurrentParseTree,
+ new MarkupBlock(
+ factory.Markup("foo "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("foo.b")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ factory.Markup(" bar")));
+ }
+
+ [Fact]
+ public void ImplicitExpressionProvisionallyAcceptsDotAfterIdentifierInMarkup()
+ {
+ var factory = SpanFactory.CreateCsHtml();
+ StringTextBuffer changed = new StringTextBuffer("foo @foo. bar");
+ StringTextBuffer old = new StringTextBuffer("foo @foo bar");
+ RunPartialParseTest(new TextChange(8, 0, old, 1, changed),
+ new MarkupBlock(
+ factory.Markup("foo "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("foo.")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ factory.Markup(" bar")),
+ additionalFlags: PartialParseResult.Provisional);
+ }
+
+ [Fact]
+ public void ImplicitExpressionAcceptsAdditionalIdentifierCharactersIfEndOfSpanIsIdentifier()
+ {
+ var factory = SpanFactory.CreateCsHtml();
+ StringTextBuffer changed = new StringTextBuffer("foo @foob bar");
+ StringTextBuffer old = new StringTextBuffer("foo @foo bar");
+ RunPartialParseTest(new TextChange(8, 0, old, 1, changed),
+ new MarkupBlock(
+ factory.Markup("foo "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("foob")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ factory.Markup(" bar")));
+ }
+
+ [Fact]
+ public void ImplicitExpressionAcceptsAdditionalIdentifierStartCharactersIfEndOfSpanIsDot()
+ {
+ var factory = SpanFactory.CreateCsHtml();
+ StringTextBuffer changed = new StringTextBuffer("@{@foo.b}");
+ StringTextBuffer old = new StringTextBuffer("@{@foo.}");
+ RunPartialParseTest(new TextChange(7, 0, old, 1, changed),
+ new MarkupBlock(
+ factory.EmptyHtml(),
+ new StatementBlock(
+ factory.CodeTransition(),
+ factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ factory.EmptyCSharp().AsStatement(),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("foo.b")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ factory.EmptyCSharp().AsStatement(),
+ factory.MetaCode("}").Accepts(AcceptedCharacters.None)),
+ factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void ImplicitExpressionAcceptsDotIfTrailingDotsAreAllowed()
+ {
+ var factory = SpanFactory.CreateCsHtml();
+ StringTextBuffer changed = new StringTextBuffer("@{@foo.}");
+ StringTextBuffer old = new StringTextBuffer("@{@foo}");
+ RunPartialParseTest(new TextChange(6, 0, old, 1, changed),
+ new MarkupBlock(
+ factory.EmptyHtml(),
+ new StatementBlock(
+ factory.CodeTransition(),
+ factory.MetaCode("{").Accepts(AcceptedCharacters.None),
+ factory.EmptyCSharp().AsStatement(),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("foo.")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ factory.EmptyCSharp().AsStatement(),
+ factory.MetaCode("}").Accepts(AcceptedCharacters.None)),
+ factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfIfKeywordTyped()
+ {
+ RunTypeKeywordTest("if");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfDoKeywordTyped()
+ {
+ RunTypeKeywordTest("do");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfTryKeywordTyped()
+ {
+ RunTypeKeywordTest("try");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfForKeywordTyped()
+ {
+ RunTypeKeywordTest("for");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfForEachKeywordTyped()
+ {
+ RunTypeKeywordTest("foreach");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfWhileKeywordTyped()
+ {
+ RunTypeKeywordTest("while");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfSwitchKeywordTyped()
+ {
+ RunTypeKeywordTest("switch");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfLockKeywordTyped()
+ {
+ RunTypeKeywordTest("lock");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfUsingKeywordTyped()
+ {
+ RunTypeKeywordTest("using");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfSectionKeywordTyped()
+ {
+ RunTypeKeywordTest("section");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfInheritsKeywordTyped()
+ {
+ RunTypeKeywordTest("inherits");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfHelperKeywordTyped()
+ {
+ RunTypeKeywordTest("helper");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfFunctionsKeywordTyped()
+ {
+ RunTypeKeywordTest("functions");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfNamespaceKeywordTyped()
+ {
+ RunTypeKeywordTest("namespace");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfClassKeywordTyped()
+ {
+ RunTypeKeywordTest("class");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfLayoutKeywordTyped()
+ {
+ RunTypeKeywordTest("layout");
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/PartialParsing/PartialParsingTestBase.cs b/test/System.Web.Razor.Test/Parser/PartialParsing/PartialParsingTestBase.cs
new file mode 100644
index 00000000..34b63736
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/PartialParsing/PartialParsingTestBase.cs
@@ -0,0 +1,111 @@
+using System.Threading;
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Test.Framework;
+using System.Web.Razor.Test.Utils;
+using System.Web.Razor.Text;
+using System.Web.WebPages.TestUtils;
+using Xunit;
+
+namespace System.Web.Razor.Test.Parser.PartialParsing
+{
+ public abstract class PartialParsingTestBase<TLanguage>
+ where TLanguage : RazorCodeLanguage, new()
+ {
+ private const string TestLinePragmaFileName = "C:\\This\\Path\\Is\\Just\\For\\Line\\Pragmas.cshtml";
+
+ protected static void RunFullReparseTest(TextChange change, PartialParseResult additionalFlags = (PartialParseResult)0)
+ {
+ // Arrange
+ TestParserManager manager = CreateParserManager();
+ manager.InitializeWithDocument(change.OldBuffer);
+
+ // Act
+ PartialParseResult result = manager.CheckForStructureChangesAndWait(change);
+
+ // Assert
+ Assert.Equal(PartialParseResult.Rejected | additionalFlags, result);
+ Assert.Equal(2, manager.ParseCount);
+ }
+
+ protected static void RunPartialParseTest(TextChange change, Block newTreeRoot, PartialParseResult additionalFlags = (PartialParseResult)0)
+ {
+ // Arrange
+ TestParserManager manager = CreateParserManager();
+ manager.InitializeWithDocument(change.OldBuffer);
+
+ // Act
+ PartialParseResult result = manager.CheckForStructureChangesAndWait(change);
+
+ // Assert
+ Assert.Equal(PartialParseResult.Accepted | additionalFlags, result);
+ Assert.Equal(1, manager.ParseCount);
+ ParserTestBase.EvaluateParseTree(manager.Parser.CurrentParseTree, newTreeRoot);
+ }
+
+ protected static TestParserManager CreateParserManager()
+ {
+ RazorEngineHost host = CreateHost();
+ RazorEditorParser parser = new RazorEditorParser(host, TestLinePragmaFileName);
+ return new TestParserManager(parser);
+ }
+
+ protected static RazorEngineHost CreateHost()
+ {
+ return new RazorEngineHost(new TLanguage())
+ {
+ GeneratedClassContext = new GeneratedClassContext("Execute", "Write", "WriteLiteral", "WriteTo", "WriteLiteralTo", "Template", "DefineSection"),
+ DesignTimeMode = true
+ };
+ }
+
+ protected static void RunTypeKeywordTest(string keyword)
+ {
+ string before = "@" + keyword.Substring(0, keyword.Length - 1);
+ string after = "@" + keyword;
+ StringTextBuffer changed = new StringTextBuffer(after);
+ StringTextBuffer old = new StringTextBuffer(before);
+ RunFullReparseTest(new TextChange(keyword.Length, 0, old, 1, changed), additionalFlags: PartialParseResult.SpanContextChanged);
+ }
+
+ protected class TestParserManager
+ {
+ public RazorEditorParser Parser;
+ public ManualResetEventSlim ParserComplete;
+ public int ParseCount;
+
+ public TestParserManager(RazorEditorParser parser)
+ {
+ ParserComplete = new ManualResetEventSlim();
+ ParseCount = 0;
+ Parser = parser;
+ parser.DocumentParseComplete += (sender, args) =>
+ {
+ Interlocked.Increment(ref ParseCount);
+ ParserComplete.Set();
+ };
+ }
+
+ public void InitializeWithDocument(ITextBuffer startDocument)
+ {
+ CheckForStructureChangesAndWait(new TextChange(0, 0, new StringTextBuffer(String.Empty), startDocument.Length, startDocument));
+ }
+
+ public PartialParseResult CheckForStructureChangesAndWait(TextChange change)
+ {
+ PartialParseResult result = Parser.CheckForStructureChanges(change);
+ if (result.HasFlag(PartialParseResult.Rejected))
+ {
+ WaitForParse();
+ }
+ return result;
+ }
+
+ public void WaitForParse()
+ {
+ MiscUtils.DoWithTimeoutIfNotDebugging(ParserComplete.Wait); // Wait for the parse to finish
+ ParserComplete.Reset();
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/PartialParsing/VBPartialParsingTest.cs b/test/System.Web.Razor.Test/Parser/PartialParsing/VBPartialParsingTest.cs
new file mode 100644
index 00000000..7cf7738d
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/PartialParsing/VBPartialParsingTest.cs
@@ -0,0 +1,372 @@
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Test.Framework;
+using System.Web.Razor.Text;
+using System.Web.WebPages.TestUtils;
+using Xunit;
+
+namespace System.Web.Razor.Test.Parser.PartialParsing
+{
+ public class VBPartialParsingTest : PartialParsingTestBase<VBRazorCodeLanguage>
+ {
+ [Fact]
+ public void ImplicitExpressionProvisionallyAcceptsDeleteOfIdentifierPartsIfDotRemains()
+ {
+ var factory = SpanFactory.CreateVbHtml();
+ StringTextBuffer changed = new StringTextBuffer("foo @User. baz");
+ StringTextBuffer old = new StringTextBuffer("foo @User.Name baz");
+ RunPartialParseTest(new TextChange(10, 4, old, 0, changed),
+ new MarkupBlock(
+ factory.Markup("foo "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("User.")
+ .AsImplicitExpression(VBCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ factory.Markup(" baz")),
+ additionalFlags: PartialParseResult.Provisional);
+ }
+
+ [Fact]
+ public void ImplicitExpressionAcceptsDeleteOfIdentifierPartsIfSomeOfIdentifierRemains()
+ {
+ var factory = SpanFactory.CreateVbHtml();
+ StringTextBuffer changed = new StringTextBuffer("foo @Us baz");
+ StringTextBuffer old = new StringTextBuffer("foo @User baz");
+ RunPartialParseTest(new TextChange(7, 2, old, 0, changed),
+ new MarkupBlock(
+ factory.Markup("foo "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("Us")
+ .AsImplicitExpression(VBCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ factory.Markup(" baz")));
+ }
+
+ [Fact]
+ public void ImplicitExpressionProvisionallyAcceptsMultipleInsertionIfItCausesIdentifierExpansionAndTrailingDot()
+ {
+ var factory = SpanFactory.CreateVbHtml();
+ StringTextBuffer changed = new StringTextBuffer("foo @User. baz");
+ StringTextBuffer old = new StringTextBuffer("foo @U baz");
+ RunPartialParseTest(new TextChange(6, 0, old, 4, changed),
+ new MarkupBlock(
+ factory.Markup("foo "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("User.")
+ .AsImplicitExpression(VBCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ factory.Markup(" baz")),
+ additionalFlags: PartialParseResult.Provisional);
+ }
+
+ [Fact]
+ public void ImplicitExpressionAcceptsMultipleInsertionIfItOnlyCausesIdentifierExpansion()
+ {
+ var factory = SpanFactory.CreateVbHtml();
+ StringTextBuffer changed = new StringTextBuffer("foo @barbiz baz");
+ StringTextBuffer old = new StringTextBuffer("foo @bar baz");
+ RunPartialParseTest(new TextChange(8, 0, old, 3, changed),
+ new MarkupBlock(
+ factory.Markup("foo "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("barbiz")
+ .AsImplicitExpression(VBCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ factory.Markup(" baz")));
+ }
+
+ [Fact]
+ public void ImplicitExpressionRejectsChangeWhichWouldHaveBeenAcceptedIfLastChangeWasProvisionallyAcceptedOnDifferentSpan()
+ {
+ var factory = SpanFactory.CreateVbHtml();
+
+ // Arrange
+ TextChange dotTyped = new TextChange(8, 0, new StringTextBuffer("foo @foo @bar"), 1, new StringTextBuffer("foo @foo. @bar"));
+ TextChange charTyped = new TextChange(14, 0, new StringTextBuffer("foo @foo. @barb"), 1, new StringTextBuffer("foo @foo. @barb"));
+ TestParserManager manager = CreateParserManager();
+ manager.InitializeWithDocument(dotTyped.OldBuffer);
+
+ // Apply the dot change
+ Assert.Equal(PartialParseResult.Provisional | PartialParseResult.Accepted, manager.CheckForStructureChangesAndWait(dotTyped));
+
+ // Act (apply the identifier start char change)
+ PartialParseResult result = manager.CheckForStructureChangesAndWait(charTyped);
+
+ // Assert
+ Assert.Equal(PartialParseResult.Rejected, result);
+ Assert.False(manager.Parser.LastResultProvisional, "LastResultProvisional flag should have been cleared but it was not");
+ ParserTestBase.EvaluateParseTree(manager.Parser.CurrentParseTree,
+ new MarkupBlock(
+ factory.Markup("foo "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("foo")
+ .AsImplicitExpression(VBCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ factory.Markup(". "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("barb")
+ .AsImplicitExpression(VBCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void ImplicitExpressionAcceptsIdentifierTypedAfterDotIfLastChangeWasProvisionalAcceptanceOfDot()
+ {
+ var factory = SpanFactory.CreateVbHtml();
+
+ // Arrange
+ TextChange dotTyped = new TextChange(8, 0, new StringTextBuffer("foo @foo bar"), 1, new StringTextBuffer("foo @foo. bar"));
+ TextChange charTyped = new TextChange(9, 0, new StringTextBuffer("foo @foo. bar"), 1, new StringTextBuffer("foo @foo.b bar"));
+ TestParserManager manager = CreateParserManager();
+ manager.InitializeWithDocument(dotTyped.OldBuffer);
+
+ // Apply the dot change
+ Assert.Equal(PartialParseResult.Provisional | PartialParseResult.Accepted, manager.CheckForStructureChangesAndWait(dotTyped));
+
+ // Act (apply the identifier start char change)
+ PartialParseResult result = manager.CheckForStructureChangesAndWait(charTyped);
+
+ // Assert
+ Assert.Equal(PartialParseResult.Accepted, result);
+ Assert.False(manager.Parser.LastResultProvisional, "LastResultProvisional flag should have been cleared but it was not");
+ ParserTestBase.EvaluateParseTree(manager.Parser.CurrentParseTree,
+ new MarkupBlock(
+ factory.Markup("foo "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("foo.b")
+ .AsImplicitExpression(VBCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ factory.Markup(" bar")));
+ }
+ [Fact]
+ public void ImplicitExpressionAcceptsIdentifierExpansionAtEndOfNonWhitespaceCharacters()
+ {
+ var factory = SpanFactory.CreateVbHtml();
+ StringTextBuffer changed = new StringTextBuffer(@"@Code
+ @food
+End Code");
+ StringTextBuffer old = new StringTextBuffer(@"@Code
+ @foo
+End Code");
+ RunPartialParseTest(new TextChange(15, 0, old, 1, changed),
+ new MarkupBlock(
+ factory.EmptyHtml(),
+ new StatementBlock(
+ factory.CodeTransition(),
+ factory.MetaCode("Code")
+ .Accepts(AcceptedCharacters.None),
+ factory.Code("\r\n ").AsStatement(),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("food")
+ .AsImplicitExpression(VBCodeParser.DefaultKeywords, acceptTrailingDot: true)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ factory.Code("\r\n").AsStatement(),
+ factory.MetaCode("End Code").Accepts(AcceptedCharacters.None)),
+ factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void ImplicitExpressionProvisionallyAcceptsDotAfterIdentifierInMarkup()
+ {
+ var factory = SpanFactory.CreateVbHtml();
+ StringTextBuffer changed = new StringTextBuffer("foo @foo. bar");
+ StringTextBuffer old = new StringTextBuffer("foo @foo bar");
+ RunPartialParseTest(new TextChange(8, 0, old, 1, changed),
+ new MarkupBlock(
+ factory.Markup("foo "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("foo.")
+ .AsImplicitExpression(VBCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ factory.Markup(" bar")),
+ additionalFlags: PartialParseResult.Provisional);
+ }
+
+ [Fact]
+ public void ImplicitExpressionAcceptsAdditionalIdentifierCharactersIfEndOfSpanIsIdentifier()
+ {
+ var factory = SpanFactory.CreateVbHtml();
+ StringTextBuffer changed = new StringTextBuffer("foo @foob baz");
+ StringTextBuffer old = new StringTextBuffer("foo @foo bar");
+ RunPartialParseTest(new TextChange(8, 0, old, 1, changed),
+ new MarkupBlock(
+ factory.Markup("foo "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("foob")
+ .AsImplicitExpression(VBCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ factory.Markup(" bar")));
+ }
+
+ [Fact]
+ public void ImplicitExpressionAcceptsAdditionalIdentifierStartCharactersIfEndOfSpanIsDot()
+ {
+ var factory = SpanFactory.CreateVbHtml();
+ StringTextBuffer changed = new StringTextBuffer("@Code @foo.b End Code");
+ StringTextBuffer old = new StringTextBuffer("@Code @foo. End Code");
+ RunPartialParseTest(new TextChange(11, 0, old, 1, changed),
+ new MarkupBlock(
+ factory.EmptyHtml(),
+ new StatementBlock(
+ factory.CodeTransition(),
+ factory.MetaCode("Code").Accepts(AcceptedCharacters.None),
+ factory.Code(" ").AsStatement(),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("foo.b")
+ .AsImplicitExpression(VBCodeParser.DefaultKeywords, acceptTrailingDot: true)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ factory.Code(" ").AsStatement(),
+ factory.MetaCode("End Code").Accepts(AcceptedCharacters.None)),
+ factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void ImplicitExpressionAcceptsDotIfTrailingDotsAreAllowed()
+ {
+ var factory = SpanFactory.CreateVbHtml();
+ StringTextBuffer changed = new StringTextBuffer("@Code @foo. End Code");
+ StringTextBuffer old = new StringTextBuffer("@Code @foo End Code");
+ RunPartialParseTest(new TextChange(10, 0, old, 1, changed),
+ new MarkupBlock(
+ factory.EmptyHtml(),
+ new StatementBlock(
+ factory.CodeTransition(),
+ factory.MetaCode("Code").Accepts(AcceptedCharacters.None),
+ factory.Code(" ").AsStatement(),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("foo.")
+ .AsImplicitExpression(VBCodeParser.DefaultKeywords, acceptTrailingDot: true)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ factory.Code(" ").AsStatement(),
+ factory.MetaCode("End Code").Accepts(AcceptedCharacters.None)),
+ factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfFunctionsKeywordTyped()
+ {
+ RunTypeKeywordTest("functions");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfCodeKeywordTyped()
+ {
+ RunTypeKeywordTest("code");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfSectionKeywordTyped()
+ {
+ RunTypeKeywordTest("section");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfDoKeywordTyped()
+ {
+ RunTypeKeywordTest("do");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfWhileKeywordTyped()
+ {
+ RunTypeKeywordTest("while");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfIfKeywordTyped()
+ {
+ RunTypeKeywordTest("if");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfSelectKeywordTyped()
+ {
+ RunTypeKeywordTest("select");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfForKeywordTyped()
+ {
+ RunTypeKeywordTest("for");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfTryKeywordTyped()
+ {
+ RunTypeKeywordTest("try");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfWithKeywordTyped()
+ {
+ RunTypeKeywordTest("with");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfSyncLockKeywordTyped()
+ {
+ RunTypeKeywordTest("synclock");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfUsingKeywordTyped()
+ {
+ RunTypeKeywordTest("using");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfImportsKeywordTyped()
+ {
+ RunTypeKeywordTest("imports");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfInheritsKeywordTyped()
+ {
+ RunTypeKeywordTest("inherits");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfOptionKeywordTyped()
+ {
+ RunTypeKeywordTest("option");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfHelperKeywordTyped()
+ {
+ RunTypeKeywordTest("helper");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfNamespaceKeywordTyped()
+ {
+ RunTypeKeywordTest("namespace");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfClassKeywordTyped()
+ {
+ RunTypeKeywordTest("class");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfLayoutKeywordTyped()
+ {
+ RunTypeKeywordTest("layout");
+ }
+ }
+} \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/Parser/RazorParserTest.cs b/test/System.Web.Razor.Test/Parser/RazorParserTest.cs
new file mode 100644
index 00000000..69c0ff33
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/RazorParserTest.cs
@@ -0,0 +1,134 @@
+using System.IO;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Test.Framework;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Razor.Test.Parser
+{
+ public class RazorParserTest
+ {
+ [Fact]
+ public void ConstructorRequiresNonNullCodeParser()
+ {
+ Assert.ThrowsArgumentNull(() => new RazorParser(null, new HtmlMarkupParser()), "codeParser");
+ }
+
+ [Fact]
+ public void ConstructorRequiresNonNullMarkupParser()
+ {
+ Assert.ThrowsArgumentNull(() => new RazorParser(new CSharpCodeParser(), null), "markupParser");
+ }
+
+ [Fact]
+ public void ParseMethodCallsParseDocumentOnMarkupParserAndReturnsResults()
+ {
+ var factory = SpanFactory.CreateCsHtml();
+
+ // Arrange
+ RazorParser parser = new RazorParser(new CSharpCodeParser(), new HtmlMarkupParser());
+
+ // Act/Assert
+ ParserTestBase.EvaluateResults(parser.Parse(new StringReader("foo @bar baz")),
+ new MarkupBlock(
+ factory.Markup("foo "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("bar")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ factory.Markup(" baz")));
+ }
+
+ [Fact]
+ public void ParseMethodUsesProvidedParserListenerIfSpecified()
+ {
+ var factory = SpanFactory.CreateCsHtml();
+
+ // Arrange
+ RazorParser parser = new RazorParser(new CSharpCodeParser(), new HtmlMarkupParser());
+
+ // Act
+ ParserResults results = parser.Parse(new StringReader("foo @bar baz"));
+
+ // Assert
+ ParserTestBase.EvaluateResults(results,
+ new MarkupBlock(
+ factory.Markup("foo "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("bar")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ factory.Markup(" baz")));
+ }
+
+ [Fact]
+ public void ParseMethodSetsUpRunWithSpecifiedCodeParserMarkupParserAndListenerAndPassesToMarkupParser()
+ {
+ RunParseWithListenerTest((parser, reader) => parser.Parse(reader));
+ }
+
+ private static void RunParseWithListenerTest(Action<RazorParser, TextReader> parserAction)
+ {
+ // Arrange
+ ParserBase markupParser = new MockMarkupParser();
+ ParserBase codeParser = new CSharpCodeParser();
+ RazorParser parser = new RazorParser(codeParser, markupParser);
+ TextReader expectedReader = new StringReader("foo");
+
+ // Act
+ parserAction(parser, expectedReader);
+
+ // Assert
+ ParserContext actualContext = markupParser.Context;
+ Assert.NotNull(actualContext);
+ Assert.Same(markupParser, actualContext.MarkupParser);
+ Assert.Same(markupParser, actualContext.ActiveParser);
+ Assert.Same(codeParser, actualContext.CodeParser);
+ }
+
+ private class MockMarkupParser : ParserBase
+ {
+ public override bool IsMarkupParser
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ public override void ParseDocument()
+ {
+ using (Context.StartBlock(BlockType.Markup))
+ {
+ }
+ }
+
+ public override void ParseSection(Tuple<string, string> nestingSequences, bool caseSensitive = true)
+ {
+ using (Context.StartBlock(BlockType.Markup))
+ {
+ }
+ }
+
+ public override void ParseBlock()
+ {
+ using (Context.StartBlock(BlockType.Markup))
+ {
+ }
+ }
+
+ protected override ParserBase OtherParser
+ {
+ get { return Context.CodeParser; }
+ }
+
+ public override void BuildSpan(SpanBuilder span, Razor.Text.SourceLocation start, string content)
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/VB/VBAutoCompleteTest.cs b/test/System.Web.Razor.Test/Parser/VB/VBAutoCompleteTest.cs
new file mode 100644
index 00000000..dc557bf2
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/VB/VBAutoCompleteTest.cs
@@ -0,0 +1,153 @@
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Test.Framework;
+using System.Web.Razor.Text;
+using System.Web.Razor.Tokenizer.Symbols;
+using Xunit;
+
+namespace System.Web.Razor.Test.Parser.VB
+{
+ public class VBAutoCompleteTest : VBHtmlCodeParserTestBase
+ {
+ [Fact]
+ public void FunctionsDirective_AutoComplete_At_EOF()
+ {
+ ParseBlockTest("@Functions",
+ new FunctionsBlock(
+ Factory.CodeTransition("@")
+ .Accepts(AcceptedCharacters.None),
+ Factory.MetaCode("Functions")
+ .Accepts(AcceptedCharacters.None),
+ Factory.EmptyVB()
+ .AsFunctionsBody()
+ .With(new AutoCompleteEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString)
+ {
+ AutoCompleteString = SyntaxConstants.VB.EndFunctionsKeyword
+ })),
+ new RazorError(
+ String.Format(RazorResources.ParseError_BlockNotTerminated, "Functions", "End Functions"),
+ 1, 0, 1));
+ }
+
+ [Fact]
+ public void HelperDirective_AutoComplete_At_EOF()
+ {
+ ParseBlockTest("@Helper Strong(value As String)",
+ new HelperBlock(new HelperCodeGenerator(new LocationTagged<string>("Strong(value As String)", 8, 0, 8), headerComplete: true),
+ Factory.CodeTransition(),
+ Factory.MetaCode("Helper ")
+ .Accepts(AcceptedCharacters.None),
+ Factory.Code("Strong(value As String)")
+ .Hidden()
+ .Accepts(AcceptedCharacters.None)
+ .With(new AutoCompleteEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString) { AutoCompleteString = SyntaxConstants.VB.EndHelperKeyword }),
+ new StatementBlock()),
+ new RazorError(
+ String.Format(RazorResources.ParseError_BlockNotTerminated, "Helper", "End Helper"),
+ 1, 0, 1));
+ }
+
+ [Fact]
+ public void SectionDirective_AutoComplete_At_EOF()
+ {
+ ParseBlockTest("@Section Header",
+ new SectionBlock(new SectionCodeGenerator("Header"),
+ Factory.CodeTransition(),
+ Factory.MetaCode("Section Header")
+ .With(new AutoCompleteEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString) { AutoCompleteString = SyntaxConstants.VB.EndSectionKeyword }),
+ new MarkupBlock()),
+ new RazorError(
+ String.Format(RazorResources.ParseError_BlockNotTerminated, "Section", "End Section"),
+ 1, 0, 1));
+ }
+
+ [Fact]
+ public void VerbatimBlock_AutoComplete_At_EOF()
+ {
+ ParseBlockTest("@Code",
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("Code").Accepts(AcceptedCharacters.None),
+ Factory.Span(SpanKind.Code, new VBSymbol(5, 0, 5, String.Empty, VBSymbolType.Unknown))
+ .With(new StatementCodeGenerator())
+ .With(new AutoCompleteEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString) { AutoCompleteString = SyntaxConstants.VB.EndCodeKeyword })),
+ new RazorError(
+ String.Format(RazorResources.ParseError_BlockNotTerminated, "Code", "End Code"),
+ 1, 0, 1));
+ }
+
+ [Fact]
+ public void FunctionsDirective_AutoComplete_At_StartOfFile()
+ {
+ ParseBlockTest(@"@Functions
+foo",
+ new FunctionsBlock(
+ Factory.CodeTransition("@").Accepts(AcceptedCharacters.None),
+ Factory.MetaCode("Functions").Accepts(AcceptedCharacters.None),
+ Factory.Code("\r\nfoo")
+ .AsFunctionsBody()
+ .With(new AutoCompleteEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString)
+ {
+ AutoCompleteString = SyntaxConstants.VB.EndFunctionsKeyword
+ })),
+ new RazorError(
+ String.Format(RazorResources.ParseError_BlockNotTerminated, "Functions", "End Functions"),
+ 1, 0, 1));
+ }
+
+ [Fact]
+ public void HelperDirective_AutoComplete_At_StartOfFile()
+ {
+ ParseBlockTest(@"@Helper Strong(value As String)
+Foo",
+ new HelperBlock(new HelperCodeGenerator(new LocationTagged<string>("Strong(value As String)", 8, 0, 8), headerComplete: true),
+ Factory.CodeTransition(),
+ Factory.MetaCode("Helper ")
+ .Accepts(AcceptedCharacters.None),
+ Factory.Code("Strong(value As String)")
+ .Hidden()
+ .Accepts(AcceptedCharacters.None)
+ .With(new AutoCompleteEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString) { AutoCompleteString = SyntaxConstants.VB.EndHelperKeyword }),
+ new StatementBlock(
+ Factory.Code("\r\nFoo").AsStatement())),
+ new RazorError(
+ String.Format(RazorResources.ParseError_BlockNotTerminated, "Helper", "End Helper"),
+ 1, 0, 1));
+ }
+
+ [Fact]
+ public void SectionDirective_AutoComplete_At_StartOfFile()
+ {
+ ParseBlockTest(@"@Section Header
+Foo",
+ new SectionBlock(new SectionCodeGenerator("Header"),
+ Factory.CodeTransition(),
+ Factory.MetaCode("Section Header")
+ .With(new AutoCompleteEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString) { AutoCompleteString = SyntaxConstants.VB.EndSectionKeyword }),
+ new MarkupBlock(
+ Factory.Markup("\r\nFoo")
+ .With(new MarkupCodeGenerator()))),
+ new RazorError(
+ String.Format(RazorResources.ParseError_BlockNotTerminated, "Section", "End Section"),
+ 1, 0, 1));
+ }
+
+ [Fact]
+ public void VerbatimBlock_AutoComplete_At_StartOfFile()
+ {
+ ParseBlockTest(@"@Code
+Foo",
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("Code").Accepts(AcceptedCharacters.None),
+ Factory.Code("\r\nFoo")
+ .AsStatement()
+ .With(new AutoCompleteEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString) { AutoCompleteString = SyntaxConstants.VB.EndCodeKeyword })),
+ new RazorError(
+ String.Format(RazorResources.ParseError_BlockNotTerminated, "Code", "End Code"),
+ 1, 0, 1));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/VB/VBBlockTest.cs b/test/System.Web.Razor.Test/Parser/VB/VBBlockTest.cs
new file mode 100644
index 00000000..a346dce5
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/VB/VBBlockTest.cs
@@ -0,0 +1,372 @@
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Test.Framework;
+using System.Web.Razor.Text;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Razor.Test.Parser.VB
+{
+ public class VBBlockTest : VBHtmlCodeParserTestBase
+ {
+ [Fact]
+ public void ParseBlockMethodThrowsArgNullExceptionOnNullContext()
+ {
+ // Arrange
+ VBCodeParser parser = new VBCodeParser();
+
+ // Act and Assert
+ Assert.Throws<InvalidOperationException>(() => parser.ParseBlock(), RazorResources.Parser_Context_Not_Set);
+ }
+
+ [Fact]
+ public void ParseBlockAcceptsImplicitExpression()
+ {
+ ParseBlockTest(@"If True Then
+ @foo
+End If",
+ new StatementBlock(
+ Factory.Code("If True Then\r\n ").AsStatement(),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("foo")
+ .AsImplicitExpression(VBCodeParser.DefaultKeywords, acceptTrailingDot: true)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ Factory.Code("\r\nEnd If")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void ParseBlockAcceptsIfStatementWithinCodeBlockIfInDesignTimeMode()
+ {
+ ParseBlockTest(@"If True Then
+ @If True Then
+ End If
+End If",
+ new StatementBlock(
+ Factory.Code("If True Then\r\n ").AsStatement(),
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("If True Then\r\n End If\r\n")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)),
+ Factory.Code(@"End If")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void ParseBlockSupportsSpacesInStrings()
+ {
+ ParseBlockTest(@"for each p in db.Query(""SELECT * FROM PRODUCTS"")
+ @<p>@p.Name</p>
+next",
+ new StatementBlock(
+ Factory.Code("for each p in db.Query(\"SELECT * FROM PRODUCTS\")\r\n")
+ .AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" "),
+ Factory.MarkupTransition(),
+ Factory.Markup("<p>"),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("p.Name")
+ .AsImplicitExpression(VBCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ Factory.Markup("</p>\r\n").Accepts(AcceptedCharacters.None)),
+ Factory.Code("next")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.WhiteSpace | AcceptedCharacters.NonWhiteSpace)));
+ }
+
+ [Fact]
+ public void ParseBlockSupportsSimpleCodeBlock()
+ {
+ ParseBlockTest(@"Code
+ If foo IsNot Nothing
+ Bar(foo)
+ End If
+End Code",
+ new StatementBlock(
+ Factory.MetaCode("Code").Accepts(AcceptedCharacters.None),
+ Factory.Code("\r\n If foo IsNot Nothing\r\n Bar(foo)\r\n End If\r\n")
+ .AsStatement(),
+ Factory.MetaCode("End Code").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void ParseBlockRejectsNewlineBetweenEndAndCodeIfNotPrefixedWithUnderscore()
+ {
+ ParseBlockTest(@"Code
+ If foo IsNot Nothing
+ Bar(foo)
+ End If
+End
+Code",
+ new StatementBlock(
+ Factory.MetaCode("Code").Accepts(AcceptedCharacters.None),
+ Factory.Code("\r\n If foo IsNot Nothing\r\n Bar(foo)\r\n End If\r\nEnd\r\nCode")
+ .AsStatement()),
+ new RazorError(
+ String.Format(RazorResources.ParseError_BlockNotTerminated, "Code", "End Code"),
+ SourceLocation.Zero));
+ }
+
+ [Fact]
+ public void ParseBlockAcceptsNewlineBetweenEndAndCodeIfPrefixedWithUnderscore()
+ {
+ ParseBlockTest(@"Code
+ If foo IsNot Nothing
+ Bar(foo)
+ End If
+End _
+_
+ _
+Code",
+ new StatementBlock(
+ Factory.MetaCode("Code").Accepts(AcceptedCharacters.None),
+ Factory.Code("\r\n If foo IsNot Nothing\r\n Bar(foo)\r\n End If\r\n")
+ .AsStatement(),
+ Factory.MetaCode("End _\r\n_\r\n _\r\nCode").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void ParseBlockSupportsSimpleFunctionsBlock()
+ {
+ ParseBlockTest(@"Functions
+ Public Sub Foo()
+ Bar()
+ End Sub
+
+ Private Function Bar() As Object
+ Return Nothing
+ End Function
+End Functions",
+ new FunctionsBlock(
+ Factory.MetaCode("Functions").Accepts(AcceptedCharacters.None),
+ Factory.Code("\r\n Public Sub Foo()\r\n Bar()\r\n End Sub\r\n\r\n Private Function Bar() As Object\r\n Return Nothing\r\n End Function\r\n")
+ .AsFunctionsBody(),
+ Factory.MetaCode("End Functions").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void ParseBlockRejectsNewlineBetweenEndAndFunctionsIfNotPrefixedWithUnderscore()
+ {
+ ParseBlockTest(@"Functions
+ If foo IsNot Nothing
+ Bar(foo)
+ End If
+End
+Functions",
+ new FunctionsBlock(
+ Factory.MetaCode("Functions").Accepts(AcceptedCharacters.None),
+ Factory.Code("\r\n If foo IsNot Nothing\r\n Bar(foo)\r\n End If\r\nEnd\r\nFunctions")
+ .AsFunctionsBody()),
+ new RazorError(
+ String.Format(RazorResources.ParseError_BlockNotTerminated, "Functions", "End Functions"),
+ SourceLocation.Zero));
+ }
+
+ [Fact]
+ public void ParseBlockAcceptsNewlineBetweenEndAndFunctionsIfPrefixedWithUnderscore()
+ {
+ ParseBlockTest(@"Functions
+ If foo IsNot Nothing
+ Bar(foo)
+ End If
+End _
+_
+ _
+Functions",
+ new FunctionsBlock(
+ Factory.MetaCode("Functions").Accepts(AcceptedCharacters.None),
+ Factory.Code("\r\n If foo IsNot Nothing\r\n Bar(foo)\r\n End If\r\n")
+ .AsFunctionsBody(),
+ Factory.MetaCode("End _\r\n_\r\n _\r\nFunctions").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void ParseBlockCorrectlyHandlesExtraEndsInEndCode()
+ {
+ ParseBlockTest(@"Code
+ Bar End
+End Code",
+ new StatementBlock(
+ Factory.MetaCode("Code").Accepts(AcceptedCharacters.None),
+ Factory.Code("\r\n Bar End\r\n").AsStatement(),
+ Factory.MetaCode("End Code").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void ParseBlockCorrectlyHandlesExtraEndsInEndFunctions()
+ {
+ ParseBlockTest(@"Functions
+ Bar End
+End Functions",
+ new FunctionsBlock(
+ Factory.MetaCode("Functions").Accepts(AcceptedCharacters.None),
+ Factory.Code("\r\n Bar End\r\n").AsFunctionsBody().AutoCompleteWith(null, atEndOfSpan: false),
+ Factory.MetaCode("End Functions").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Theory]
+ [InlineData("If", "End", "If")]
+ [InlineData("Try", "End", "Try")]
+ [InlineData("While", "End", "While")]
+ [InlineData("Using", "End", "Using")]
+ [InlineData("With", "End", "With")]
+ public void KeywordAllowsNewlinesIfPrefixedByUnderscore(string startKeyword, string endKeyword1, string endKeyword2)
+ {
+ string code = startKeyword + @"
+ ' In the block
+" + endKeyword1 + @" _
+_
+_
+_
+_
+_
+ " + endKeyword2 + @"
+";
+ ParseBlockTest(code + "foo bar baz",
+ new StatementBlock(
+ Factory.Code(code)
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)));
+ }
+
+ [Theory]
+ [InlineData("While", "EndWhile", "End While")]
+ [InlineData("If", "EndIf", "End If")]
+ [InlineData("Select", "EndSelect", "End Select")]
+ [InlineData("Try", "EndTry", "End Try")]
+ [InlineData("With", "EndWith", "End With")]
+ [InlineData("Using", "EndUsing", "End Using")]
+ public void EndTerminatedKeywordRequiresSpaceBetweenEndAndKeyword(string startKeyword, string wrongEndKeyword, string endKeyword)
+ {
+ string code = startKeyword + @"
+ ' This should not end the code
+ " + wrongEndKeyword + @"
+ ' But this should
+" + endKeyword;
+ ParseBlockTest(code,
+ new StatementBlock(
+ Factory.Code(code)
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)));
+ }
+
+ [Theory]
+ [InlineData("While", "End While", false)]
+ [InlineData("Do", "Loop", true)]
+ [InlineData("If", "End If", false)]
+ [InlineData("Select", "End Select", false)]
+ [InlineData("For", "Next", true)]
+ [InlineData("Try", "End Try", false)]
+ [InlineData("With", "End With", false)]
+ [InlineData("Using", "End Using", false)]
+ public void EndSequenceInString(string keyword, string endSequence, bool acceptToEndOfLine)
+ {
+ string code = keyword + @"
+ """ + endSequence + @"""
+" + endSequence + (acceptToEndOfLine ? @" foo bar baz" : "") + @"
+";
+ ParseBlockTest(code + "biz boz",
+ new StatementBlock(
+ Factory.Code(code).AsStatement().Accepts(GetAcceptedCharacters(acceptToEndOfLine))));
+ }
+
+ [Theory]
+ [InlineData("While", "End While", false)]
+ [InlineData("Do", "Loop", true)]
+ [InlineData("If", "End If", false)]
+ [InlineData("Select", "End Select", false)]
+ [InlineData("For", "Next", true)]
+ [InlineData("Try", "End Try", false)]
+ [InlineData("With", "End With", false)]
+ [InlineData("Using", "End Using", false)]
+ private void CommentedEndSequence(string keyword, string endSequence, bool acceptToEndOfLine)
+ {
+ string code = keyword + @"
+ '" + endSequence + @"
+" + endSequence + (acceptToEndOfLine ? @" foo bar baz" : "") + @"
+";
+ ParseBlockTest(code + "biz boz",
+ new StatementBlock(
+ Factory.Code(code).AsStatement().Accepts(GetAcceptedCharacters(acceptToEndOfLine))));
+ }
+
+ [Theory]
+ [InlineData("While", "End While", false)]
+ [InlineData("Do", "Loop", true)]
+ [InlineData("If", "End If", false)]
+ [InlineData("Select", "End Select", false)]
+ [InlineData("For", "Next", true)]
+ [InlineData("Try", "End Try", false)]
+ [InlineData("With", "End With", false)]
+ [InlineData("SyncLock", "End SyncLock", false)]
+ [InlineData("Using", "End Using", false)]
+ private void NestedKeywordBlock(string keyword, string endSequence, bool acceptToEndOfLine)
+ {
+ string code = keyword + @"
+ " + keyword + @"
+ Bar(foo)
+ " + endSequence + @"
+" + endSequence + (acceptToEndOfLine ? @" foo bar baz" : "") + @"
+";
+ ParseBlockTest(code + "biz boz",
+ new StatementBlock(
+ Factory.Code(code).AsStatement().Accepts(GetAcceptedCharacters(acceptToEndOfLine))));
+ }
+
+ [Theory]
+ [InlineData("While True", "End While", false)]
+ [InlineData("Do", "Loop", true)]
+ [InlineData("If foo IsNot Nothing", "End If", false)]
+ [InlineData("Select Case foo", "End Select", false)]
+ [InlineData("For Each p in Products", "Next", true)]
+ [InlineData("Try", "End Try", false)]
+ [InlineData("With", "End With", false)]
+ [InlineData("SyncLock", "End SyncLock", false)]
+ [InlineData("Using", "End Using", false)]
+ private void SimpleKeywordBlock(string keyword, string endSequence, bool acceptToEndOfLine)
+ {
+ string code = keyword + @"
+ Bar(foo)
+" + endSequence + (acceptToEndOfLine ? @" foo bar baz" : "") + @"
+";
+ ParseBlockTest(code + "biz boz",
+ new StatementBlock(
+ Factory.Code(code).AsStatement().Accepts(GetAcceptedCharacters(acceptToEndOfLine))));
+ }
+
+ [Theory]
+ [InlineData("While True", "Exit While", "End While", false)]
+ [InlineData("Do", "Exit Do", "Loop", true)]
+ [InlineData("For Each p in Products", "Exit For", "Next", true)]
+ [InlineData("While True", "Continue While", "End While", false)]
+ [InlineData("Do", "Continue Do", "Loop", true)]
+ [InlineData("For Each p in Products", "Continue For", "Next", true)]
+ private void KeywordWithExitOrContinue(string startKeyword, string exitKeyword, string endKeyword, bool acceptToEndOfLine)
+ {
+ string code = startKeyword + @"
+ ' This is before the exit
+ " + exitKeyword + @"
+ ' This is after the exit
+" + endKeyword + @"
+";
+ ParseBlockTest(code + "foo bar baz",
+ new StatementBlock(
+ Factory.Code(code).AsStatement().Accepts(GetAcceptedCharacters(acceptToEndOfLine))));
+ }
+
+ private AcceptedCharacters GetAcceptedCharacters(bool acceptToEndOfLine)
+ {
+ return acceptToEndOfLine ?
+ AcceptedCharacters.WhiteSpace | AcceptedCharacters.NonWhiteSpace :
+ AcceptedCharacters.None;
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/VB/VBContinueStatementTest.cs b/test/System.Web.Razor.Test/Parser/VB/VBContinueStatementTest.cs
new file mode 100644
index 00000000..b342c8ee
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/VB/VBContinueStatementTest.cs
@@ -0,0 +1,56 @@
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Test.Framework;
+using Xunit;
+
+namespace System.Web.Razor.Test.Parser.VB
+{
+ // VB Continue Statement: http://msdn.microsoft.com/en-us/library/801hyx6f.aspx
+ public class VBContinueStatementTest : VBHtmlCodeParserTestBase
+ {
+ [Fact]
+ public void VB_Do_Statement_With_Continue()
+ {
+ ParseBlockTest(@"@Do While True
+ Continue Do
+Loop
+' Not in the block!",
+ new StatementBlock(
+ Factory.CodeTransition(SyntaxConstants.TransitionString)
+ .Accepts(AcceptedCharacters.None),
+ Factory.Code("Do While True\r\n Continue Do\r\nLoop\r\n")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.AnyExceptNewline)));
+ }
+
+ [Fact]
+ public void VB_For_Statement_With_Continue()
+ {
+ ParseBlockTest(@"@For i = 1 To 12
+ Continue For
+Next i
+' Not in the block!",
+ new StatementBlock(
+ Factory.CodeTransition(SyntaxConstants.TransitionString)
+ .Accepts(AcceptedCharacters.None),
+ Factory.Code("For i = 1 To 12\r\n Continue For\r\nNext i\r\n")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.AnyExceptNewline)));
+ }
+
+ [Fact]
+ public void VB_While_Statement_With_Continue()
+ {
+ ParseBlockTest(@"@While True
+ Continue While
+End While
+' Not in the block!",
+ new StatementBlock(
+ Factory.CodeTransition(SyntaxConstants.TransitionString)
+ .Accepts(AcceptedCharacters.None),
+ Factory.Code("While True\r\n Continue While\r\nEnd While\r\n")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/VB/VBDirectiveTest.cs b/test/System.Web.Razor.Test/Parser/VB/VBDirectiveTest.cs
new file mode 100644
index 00000000..ad212625
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/VB/VBDirectiveTest.cs
@@ -0,0 +1,127 @@
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Test.Framework;
+using System.Web.Razor.Text;
+using Xunit;
+
+namespace System.Web.Razor.Test.Parser.VB
+{
+ public class VBDirectiveTest : VBHtmlCodeParserTestBase
+ {
+ [Fact]
+ public void VB_Code_Directive()
+ {
+ ParseBlockTest(@"@Code
+ foo()
+End Code
+' Not part of the block",
+ new StatementBlock(
+ Factory.CodeTransition(SyntaxConstants.TransitionString)
+ .Accepts(AcceptedCharacters.None),
+ Factory.MetaCode("Code")
+ .Accepts(AcceptedCharacters.None),
+ Factory.Code("\r\n foo()\r\n")
+ .AsStatement()
+ .With(new AutoCompleteEditHandler(VBLanguageCharacteristics.Instance.TokenizeString)),
+ Factory.MetaCode("End Code")
+ .Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void VB_Functions_Directive()
+ {
+ ParseBlockTest(@"@Functions
+ Public Function Foo() As String
+ Return ""Foo""
+ End Function
+
+ Public Sub Bar()
+ End Sub
+End Functions
+' Not part of the block",
+ new FunctionsBlock(
+ Factory.CodeTransition(SyntaxConstants.TransitionString)
+ .Accepts(AcceptedCharacters.None),
+ Factory.MetaCode("Functions")
+ .Accepts(AcceptedCharacters.None),
+ Factory.Code("\r\n Public Function Foo() As String\r\n Return \"Foo\"\r\n End Function\r\n\r\n Public Sub Bar()\r\n End Sub\r\n")
+ .AsFunctionsBody(),
+ Factory.MetaCode("End Functions")
+ .Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void VB_Section_Directive()
+ {
+ ParseBlockTest(@"@Section Header
+ <p>Foo</p>
+End Section",
+ new SectionBlock(new SectionCodeGenerator("Header"),
+ Factory.CodeTransition(SyntaxConstants.TransitionString),
+ Factory.MetaCode(@"Section Header"),
+ new MarkupBlock(
+ Factory.Markup("\r\n <p>Foo</p>\r\n")),
+ Factory.MetaCode("End Section")
+ .Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void SessionStateDirectiveWorks()
+ {
+ ParseBlockTest(@"@SessionState InProc
+",
+ new DirectiveBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("SessionState ")
+ .Accepts(AcceptedCharacters.None),
+ Factory.MetaCode("InProc\r\n")
+ .Accepts(AcceptedCharacters.None)
+ .With(new RazorDirectiveAttributeCodeGenerator("SessionState", "InProc"))
+ )
+ );
+ }
+
+ [Fact]
+ public void SessionStateDirectiveIsCaseInsensitive()
+ {
+ ParseBlockTest(@"@sessionstate disabled
+",
+ new DirectiveBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("sessionstate ")
+ .Accepts(AcceptedCharacters.None),
+ Factory.MetaCode("disabled\r\n")
+ .Accepts(AcceptedCharacters.None)
+ .With(new RazorDirectiveAttributeCodeGenerator("SessionState", "disabled"))
+ )
+ );
+ }
+
+ [Fact]
+ public void VB_Helper_Directive()
+ {
+ ParseBlockTest(@"@Helper Strong(s as String)
+ s = s.ToUpperCase()
+ @<strong>s</strong>
+End Helper",
+ new HelperBlock(new HelperCodeGenerator(new LocationTagged<string>("Strong(s as String)", 8, 0, 8), headerComplete: true),
+ Factory.CodeTransition(SyntaxConstants.TransitionString),
+ Factory.MetaCode("Helper ")
+ .Accepts(AcceptedCharacters.None),
+ Factory.Code("Strong(s as String)").Hidden(),
+ new StatementBlock(
+ Factory.Code("\r\n s = s.ToUpperCase()\r\n")
+ .AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" "),
+ Factory.MarkupTransition(SyntaxConstants.TransitionString),
+ Factory.Markup("<strong>s</strong>\r\n")
+ .Accepts(AcceptedCharacters.None)),
+ Factory.EmptyVB()
+ .AsStatement(),
+ Factory.MetaCode("End Helper")
+ .Accepts(AcceptedCharacters.None))));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/VB/VBErrorTest.cs b/test/System.Web.Razor.Test/Parser/VB/VBErrorTest.cs
new file mode 100644
index 00000000..d0f1bdc7
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/VB/VBErrorTest.cs
@@ -0,0 +1,172 @@
+using System.Web.Razor.Editor;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Test.Framework;
+using System.Web.Razor.Text;
+using Xunit;
+using Xunit.Extensions;
+
+namespace System.Web.Razor.Test.Parser.VB
+{
+ public class VBErrorTest : VBHtmlCodeParserTestBase
+ {
+ [Fact]
+ public void ParserOutputsErrorAndRecoversToEndOfLineIfExplicitExpressionUnterminated()
+ {
+ ParseBlockTest(@"(foo
+bar",
+ new ExpressionBlock(
+ Factory.MetaCode("(").Accepts(AcceptedCharacters.None),
+ Factory.Code("foo").AsExpression()),
+ new RazorError(
+ String.Format(
+ RazorResources.ParseError_Expected_EndOfBlock_Before_EOF,
+ RazorResources.BlockName_ExplicitExpression,
+ ")", "("),
+ SourceLocation.Zero));
+ }
+
+ [Fact]
+ public void ParserOutputsZeroLengthCodeSpanIfEofReachedAfterStartOfExplicitExpression()
+ {
+ ParseBlockTest("(",
+ new ExpressionBlock(
+ Factory.MetaCode("(").Accepts(AcceptedCharacters.None),
+ Factory.EmptyVB().AsExpression()),
+ new RazorError(
+ String.Format(RazorResources.ParseError_Expected_EndOfBlock_Before_EOF, "explicit expression", ")", "("),
+ SourceLocation.Zero));
+ }
+
+ [Fact]
+ public void ParserOutputsZeroLengthCodeSpanIfEofReachedAfterAtSign()
+ {
+ ParseBlockTest(String.Empty,
+ new ExpressionBlock(
+ Factory.EmptyVB().AsImplicitExpression(KeywordSet).Accepts(AcceptedCharacters.NonWhiteSpace)),
+ new RazorError(
+ RazorResources.ParseError_Unexpected_EndOfFile_At_Start_Of_CodeBlock,
+ SourceLocation.Zero));
+ }
+
+ [Fact]
+ public void ParserOutputsZeroLengthCodeSpanIfOnlyWhitespaceFoundAfterAtSign()
+ {
+ ParseBlockTest(" ",
+ new ExpressionBlock(
+ Factory.EmptyVB().AsImplicitExpression(KeywordSet).Accepts(AcceptedCharacters.NonWhiteSpace)),
+ new RazorError(
+ RazorResources.ParseError_Unexpected_WhiteSpace_At_Start_Of_CodeBlock_VB,
+ SourceLocation.Zero));
+ }
+
+ [Fact]
+ public void ParserOutputsZeroLengthCodeSpanIfInvalidCharacterFoundAfterAtSign()
+ {
+ ParseBlockTest("!!!",
+ new ExpressionBlock(
+ Factory.EmptyVB().AsImplicitExpression(KeywordSet).Accepts(AcceptedCharacters.NonWhiteSpace)),
+ new RazorError(
+ String.Format(RazorResources.ParseError_Unexpected_Character_At_Start_Of_CodeBlock_VB, "!"),
+ SourceLocation.Zero));
+ }
+
+ [Theory]
+ [InlineData("Code", "End Code", true, true)]
+ [InlineData("Do", "Loop", false, false)]
+ [InlineData("While", "End While", false, false)]
+ [InlineData("If", "End If", false, false)]
+ [InlineData("Select Case", "End Select", false, false)]
+ [InlineData("For", "Next", false, false)]
+ [InlineData("Try", "End Try", false, false)]
+ [InlineData("With", "End With", false, false)]
+ [InlineData("Using", "End Using", false, false)]
+ public void EofBlock(string keyword, string expectedTerminator, bool autoComplete, bool keywordIsMetaCode)
+ {
+ EofBlockCore(keyword, expectedTerminator, autoComplete, BlockType.Statement, keywordIsMetaCode, c => c.AsStatement());
+ }
+
+ [Fact]
+ public void EofFunctionsBlock()
+ {
+ EofBlockCore("Functions", "End Functions", true, BlockType.Functions, true, c => c.AsFunctionsBody());
+ }
+
+ private void EofBlockCore(string keyword, string expectedTerminator, bool autoComplete, BlockType blockType, bool keywordIsMetaCode, Func<UnclassifiedCodeSpanConstructor, SpanConstructor> classifier)
+ {
+ BlockBuilder expected = new BlockBuilder();
+ expected.Type = blockType;
+ if (keywordIsMetaCode)
+ {
+ expected.Children.Add(Factory.MetaCode(keyword).Accepts(AcceptedCharacters.None));
+ expected.Children.Add(
+ classifier(Factory.EmptyVB())
+ .With((SpanEditHandler)(
+ autoComplete ?
+ new AutoCompleteEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString) { AutoCompleteString = expectedTerminator } :
+ SpanEditHandler.CreateDefault())));
+ }
+ else
+ {
+ expected.Children.Add(
+ classifier(Factory.Code(keyword))
+ .With((SpanEditHandler)(
+ autoComplete ?
+ new AutoCompleteEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString) { AutoCompleteString = expectedTerminator } :
+ SpanEditHandler.CreateDefault())));
+ }
+
+ ParseBlockTest(keyword,
+ expected.Build(),
+ new RazorError(
+ String.Format(RazorResources.ParseError_BlockNotTerminated, keyword, expectedTerminator),
+ SourceLocation.Zero));
+ }
+
+ [Theory]
+ [InlineData("Code", "End Code", true)]
+ [InlineData("Do", "Loop", false)]
+ [InlineData("While", "End While", false)]
+ [InlineData("If", "End If", false)]
+ [InlineData("Select Case", "End Select", false)]
+ [InlineData("For", "Next", false)]
+ [InlineData("Try", "End Try", false)]
+ [InlineData("With", "End With", false)]
+ [InlineData("Using", "End Using", false)]
+ public void UnterminatedBlock(string keyword, string expectedTerminator, bool keywordIsMetaCode)
+ {
+ UnterminatedBlockCore(keyword, expectedTerminator, BlockType.Statement, keywordIsMetaCode, c => c.AsStatement());
+ }
+
+ [Fact]
+ public void UnterminatedFunctionsBlock()
+ {
+ UnterminatedBlockCore("Functions", "End Functions", BlockType.Functions, true, c => c.AsFunctionsBody());
+ }
+
+ private void UnterminatedBlockCore(string keyword, string expectedTerminator, BlockType blockType, bool keywordIsMetaCode, Func<UnclassifiedCodeSpanConstructor, SpanConstructor> classifier)
+ {
+ const string blockBody = @"
+ ' This block is not correctly terminated!";
+
+ BlockBuilder expected = new BlockBuilder();
+ expected.Type = blockType;
+ if (keywordIsMetaCode)
+ {
+ expected.Children.Add(Factory.MetaCode(keyword).Accepts(AcceptedCharacters.None));
+ expected.Children.Add(classifier(Factory.Code(blockBody)));
+ }
+ else
+ {
+ expected.Children.Add(classifier(Factory.Code(keyword + blockBody)));
+ }
+
+ ParseBlockTest(keyword + blockBody,
+ expected.Build(),
+ new RazorError(
+ String.Format(RazorResources.ParseError_BlockNotTerminated, keyword, expectedTerminator),
+ SourceLocation.Zero));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/VB/VBExitStatementTest.cs b/test/System.Web.Razor.Test/Parser/VB/VBExitStatementTest.cs
new file mode 100644
index 00000000..c7968f58
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/VB/VBExitStatementTest.cs
@@ -0,0 +1,94 @@
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Test.Framework;
+using Xunit;
+
+namespace System.Web.Razor.Test.Parser.VB
+{
+ // VB Exit Statement: http://msdn.microsoft.com/en-us/library/t2at9t47.aspx
+ public class VBExitStatementTest : VBHtmlCodeParserTestBase
+ {
+ [Fact]
+ public void VB_Do_Statement_With_Exit()
+ {
+ ParseBlockTest(@"@Do While True
+ Exit Do
+Loop
+' Not in the block!",
+ new StatementBlock(
+ Factory.CodeTransition(SyntaxConstants.TransitionString)
+ .Accepts(AcceptedCharacters.None),
+ Factory.Code("Do While True\r\n Exit Do\r\nLoop\r\n")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.AnyExceptNewline)));
+ }
+
+ [Fact]
+ public void VB_For_Statement_With_Exit()
+ {
+ ParseBlockTest(@"@For i = 1 To 12
+ Exit For
+Next i
+' Not in the block!",
+ new StatementBlock(
+ Factory.CodeTransition(SyntaxConstants.TransitionString)
+ .Accepts(AcceptedCharacters.None),
+ Factory.Code("For i = 1 To 12\r\n Exit For\r\nNext i\r\n")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.AnyExceptNewline)));
+ }
+
+ [Fact]
+ public void VB_Select_Statement_With_Exit()
+ {
+ ParseBlockTest(@"@Select Case Foo
+ Case 1
+ Exit Select
+ Case 2
+ Exit Select
+End Select
+' Not in the block!",
+ new StatementBlock(
+ Factory.CodeTransition(SyntaxConstants.TransitionString)
+ .Accepts(AcceptedCharacters.None),
+ Factory.Code("Select Case Foo\r\n Case 1\r\n Exit Select\r\n Case 2\r\n Exit Select\r\nEnd Select\r\n")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void VB_Try_Statement_With_Exit()
+ {
+ ParseBlockTest(@"@Try
+ Foo()
+ Exit Try
+Catch Bar
+ Throw Bar
+Finally
+ Baz()
+End Try
+' Not in the block!",
+ new StatementBlock(
+ Factory.CodeTransition(SyntaxConstants.TransitionString)
+ .Accepts(AcceptedCharacters.None),
+ Factory.Code("Try\r\n Foo()\r\n Exit Try\r\nCatch Bar\r\n Throw Bar\r\nFinally\r\n Baz()\r\nEnd Try\r\n")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void VB_While_Statement_With_Exit()
+ {
+ ParseBlockTest(@"@While True
+ Exit While
+End While
+' Not in the block!",
+ new StatementBlock(
+ Factory.CodeTransition(SyntaxConstants.TransitionString)
+ .Accepts(AcceptedCharacters.None),
+ Factory.Code("While True\r\n Exit While\r\nEnd While\r\n")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/VB/VBExplicitExpressionTest.cs b/test/System.Web.Razor.Test/Parser/VB/VBExplicitExpressionTest.cs
new file mode 100644
index 00000000..bd41e3e4
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/VB/VBExplicitExpressionTest.cs
@@ -0,0 +1,20 @@
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Test.Framework;
+using Xunit;
+
+namespace System.Web.Razor.Test.Parser.VB
+{
+ public class VBExplicitExpressionTest : VBHtmlCodeParserTestBase
+ {
+ [Fact]
+ public void VB_Simple_ExplicitExpression()
+ {
+ ParseBlockTest("@(foo)",
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("(").Accepts(AcceptedCharacters.None),
+ Factory.Code("foo").AsExpression(),
+ Factory.MetaCode(")").Accepts(AcceptedCharacters.None)));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/VB/VBExpressionTest.cs b/test/System.Web.Razor.Test/Parser/VB/VBExpressionTest.cs
new file mode 100644
index 00000000..6b29123b
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/VB/VBExpressionTest.cs
@@ -0,0 +1,109 @@
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Test.Framework;
+using System.Web.Razor.Text;
+using Xunit;
+using Xunit.Extensions;
+
+namespace System.Web.Razor.Test.Parser.VB
+{
+ public class VBExpressionTest : VBHtmlCodeParserTestBase
+ {
+ [Fact]
+ public void ParseBlockCorrectlyHandlesCodeBlockInBodyOfExplicitExpressionDueToUnclosedExpression()
+ {
+ ParseBlockTest(@"(
+@Code
+ Dim foo = bar
+End Code",
+ new ExpressionBlock(
+ Factory.MetaCode("(").Accepts(AcceptedCharacters.None),
+ Factory.EmptyVB().AsExpression()),
+ new RazorError(
+ String.Format(
+ RazorResources.ParseError_Expected_EndOfBlock_Before_EOF,
+ RazorResources.BlockName_ExplicitExpression,
+ ")", "("),
+ SourceLocation.Zero));
+ }
+
+ [Fact]
+ public void ParseBlockAcceptsNonEnglishCharactersThatAreValidIdentifiers()
+ {
+ ImplicitExpressionTest("हळूँजद॔.", "हळूँजद॔");
+ }
+
+ [Fact]
+ public void ParseBlockDoesNotTreatXmlAxisPropertyAsTransitionToMarkup()
+ {
+ SingleSpanBlockTest(
+ @"If foo Is Nothing Then
+ Dim bar As XElement
+ Dim foo = bar.<foo>
+End If",
+ BlockType.Statement,
+ SpanKind.Code,
+ acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockDoesNotTreatXmlAttributePropertyAsTransitionToMarkup()
+ {
+ SingleSpanBlockTest(
+ @"If foo Is Nothing Then
+ Dim bar As XElement
+ Dim foo = bar.@foo
+End If",
+ BlockType.Statement,
+ SpanKind.Code,
+ acceptedCharacters: AcceptedCharacters.None);
+ }
+
+ [Fact]
+ public void ParseBlockSupportsSimpleImplicitExpression()
+ {
+ ImplicitExpressionTest("Foo");
+ }
+
+ [Fact]
+ public void ParseBlockSupportsImplicitExpressionWithDots()
+ {
+ ImplicitExpressionTest("Foo.Bar.Baz");
+ }
+
+ [Fact]
+ public void ParseBlockSupportsImplicitExpressionWithParens()
+ {
+ ImplicitExpressionTest("Foo().Bar().Baz()");
+ }
+
+ [Fact]
+ public void ParseBlockSupportsImplicitExpressionWithStuffInParens()
+ {
+ ImplicitExpressionTest("Foo().Bar(sdfkhj sdfksdfjs \")\" sjdfkjsdf).Baz()");
+ }
+
+ [Fact]
+ public void ParseBlockSupportsImplicitExpressionWithCommentInParens()
+ {
+ ImplicitExpressionTest("Foo().Bar(sdfkhj sdfksdfjs \")\" '))))))))\r\nsjdfkjsdf).Baz()");
+ }
+
+ [Theory]
+ [InlineData("Foo")]
+ [InlineData("Foo(Of String).Bar(1, 2, 3).Biz")]
+ [InlineData("Foo(Of String).Bar(\")\").Biz")]
+ [InlineData("Foo(Of String).Bar(\"Foo\"\"Bar)\"\"Baz\").Biz")]
+ [InlineData("\"foo\r\nbar")]
+ [InlineData("Foo.Bar. _\r\nREM )\r\nBaz()\r\n")]
+ [InlineData("Foo.Bar. _\r\n' )\r\nBaz()\r\n")]
+ public void ValidExplicitExpressions(string body)
+ {
+ ParseBlockTest("(" + body + ")",
+ new ExpressionBlock(
+ Factory.MetaCode("(").Accepts(AcceptedCharacters.None),
+ Factory.Code(body).AsExpression(),
+ Factory.MetaCode(")").Accepts(AcceptedCharacters.None)));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/VB/VBExpressionsInCodeTest.cs b/test/System.Web.Razor.Test/Parser/VB/VBExpressionsInCodeTest.cs
new file mode 100644
index 00000000..8be07bca
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/VB/VBExpressionsInCodeTest.cs
@@ -0,0 +1,119 @@
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Test.Framework;
+using Xunit;
+using Xunit.Extensions;
+
+namespace System.Web.Razor.Test.Parser.VB
+{
+ public class VBExpressionsInCodeTest : VBHtmlCodeParserTestBase
+ {
+ [Fact]
+ public void InnerImplicitExpressionWithOnlySingleAtAcceptsSingleSpaceOrNewlineAtDesignTime()
+ {
+ ParseBlockTest(@"Code
+ @
+End Code",
+ new StatementBlock(
+ Factory.MetaCode("Code").Accepts(AcceptedCharacters.None),
+ Factory.Code("\r\n ").AsStatement(),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.EmptyVB()
+ .AsImplicitExpression(VBCodeParser.DefaultKeywords, acceptTrailingDot: true)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ Factory.Code("\r\n").AsStatement(),
+ Factory.MetaCode("End Code").Accepts(AcceptedCharacters.None)),
+ designTimeParser: true,
+ expectedErrors: new[]
+ {
+ new RazorError(RazorResources.ParseError_Unexpected_WhiteSpace_At_Start_Of_CodeBlock_VB, 11, 1, 5)
+ });
+ }
+
+ [Fact]
+ public void InnerImplicitExpressionDoesNotAcceptDotAfterAt()
+ {
+ ParseBlockTest(@"Code
+ @.
+End Code",
+ new StatementBlock(
+ Factory.MetaCode("Code").Accepts(AcceptedCharacters.None),
+ Factory.Code("\r\n ").AsStatement(),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.EmptyVB()
+ .AsImplicitExpression(VBCodeParser.DefaultKeywords, acceptTrailingDot: true)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ Factory.Code(".\r\n").AsStatement(),
+ Factory.MetaCode("End Code").Accepts(AcceptedCharacters.None)),
+ designTimeParser: true,
+ expectedErrors: new[]
+ {
+ new RazorError(
+ String.Format(RazorResources.ParseError_Unexpected_Character_At_Start_Of_CodeBlock_VB, "."),
+ 11, 1, 5)
+ });
+ }
+
+ [Theory]
+ [InlineData("Foo.Bar.", true)]
+ [InlineData("Foo", true)]
+ [InlineData("Foo.Bar.Baz", true)]
+ [InlineData("Foo().Bar().Baz()", true)]
+ [InlineData("Foo().Bar(sdfkhj sdfksdfjs \")\" sjdfkjsdf).Baz()", true)]
+ [InlineData("Foo().Bar(sdfkhj sdfksdfjs \")\" '))))))))\r\nsjdfkjsdf).Baz()", true)]
+ [InlineData("Foo", false)]
+ [InlineData("Foo(Of String).Bar(1, 2, 3).Biz", false)]
+ [InlineData("Foo(Of String).Bar(\")\").Biz", false)]
+ [InlineData("Foo(Of String).Bar(\"Foo\"\"Bar)\"\"Baz\").Biz", false)]
+ [InlineData("Foo.Bar. _\r\nREM )\r\nBaz()\r\n", false)]
+ [InlineData("Foo.Bar. _\r\n' )\r\nBaz()\r\n", false)]
+ public void ExpressionInCode(string expression, bool isImplicit)
+ {
+ ExpressionBlock expressionBlock;
+ if (isImplicit)
+ {
+ expressionBlock =
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code(expression)
+ .AsImplicitExpression(VBCodeParser.DefaultKeywords, acceptTrailingDot: true)
+ .Accepts(AcceptedCharacters.NonWhiteSpace));
+ }
+ else
+ {
+ expressionBlock =
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("(").Accepts(AcceptedCharacters.None),
+ Factory.Code(expression).AsExpression(),
+ Factory.MetaCode(")").Accepts(AcceptedCharacters.None));
+ }
+
+ string code;
+ if (isImplicit)
+ {
+ code = @"If foo IsNot Nothing Then
+ @" + expression + @"
+End If";
+ }
+ else
+ {
+ code = @"If foo IsNot Nothing Then
+ @(" + expression + @")
+End If";
+ }
+
+ ParseBlockTest(code,
+ new StatementBlock(
+ Factory.Code("If foo IsNot Nothing Then\r\n ")
+ .AsStatement(),
+ expressionBlock,
+ Factory.Code("\r\nEnd If")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/VB/VBHelperTest.cs b/test/System.Web.Razor.Test/Parser/VB/VBHelperTest.cs
new file mode 100644
index 00000000..9b0f501f
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/VB/VBHelperTest.cs
@@ -0,0 +1,322 @@
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Test.Framework;
+using System.Web.Razor.Text;
+using Xunit;
+
+namespace System.Web.Razor.Test.Parser.VB
+{
+ public class VBHelperTest : VBHtmlMarkupParserTestBase
+ {
+ [Fact]
+ public void ParseHelperOutputsErrorButContinuesIfLParenFoundAfterHelperKeyword()
+ {
+ ParseDocumentTest("@Helper ()",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new HelperBlock(new HelperCodeGenerator(new LocationTagged<string>("()", 8, 0, 8), headerComplete: true),
+ Factory.CodeTransition(),
+ Factory.MetaCode("Helper ").Accepts(AcceptedCharacters.None),
+ Factory.Code("()").Hidden().AutoCompleteWith(SyntaxConstants.VB.EndHelperKeyword),
+ new StatementBlock())),
+ new RazorError(
+ String.Format(
+ RazorResources.ParseError_Unexpected_Character_At_Helper_Name_Start,
+ String.Format(RazorResources.ErrorComponent_Character, "(")),
+ 8, 0, 8),
+ new RazorError(
+ String.Format(RazorResources.ParseError_BlockNotTerminated, "Helper", "End Helper"),
+ 1, 0, 1));
+ }
+
+ [Fact]
+ public void ParseHelperStatementOutputsMarkerHelperHeaderSpanOnceKeywordComplete()
+ {
+ ParseDocumentTest("@Helper ",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new HelperBlock(new HelperCodeGenerator(new LocationTagged<string>(String.Empty, 8, 0, 8), headerComplete: false),
+ Factory.CodeTransition(),
+ Factory.MetaCode("Helper ").Accepts(AcceptedCharacters.None),
+ Factory.EmptyVB().Hidden())),
+ new RazorError(
+ String.Format(RazorResources.ParseError_Unexpected_Character_At_Helper_Name_Start, RazorResources.ErrorComponent_EndOfFile),
+ 8, 0, 8));
+ }
+
+ [Fact]
+ public void ParseHelperStatementMarksHelperSpanAsCanGrowIfMissingTrailingSpace()
+ {
+ ParseDocumentTest("@Helper",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new HelperBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("Helper"))),
+ new RazorError(
+ String.Format(RazorResources.ParseError_Unexpected_Character_At_Helper_Name_Start, RazorResources.ErrorComponent_EndOfFile),
+ 7, 0, 7));
+ }
+
+ [Fact]
+ public void ParseHelperStatementTerminatesEarlyIfHeaderNotComplete()
+ {
+ ParseDocumentTest(@"@Helper
+@Helper",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new HelperBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("Helper\r\n").Accepts(AcceptedCharacters.None),
+ Factory.EmptyVB().Hidden()),
+ new HelperBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("Helper"))),
+ designTimeParser: true,
+ expectedErrors: new[]
+ {
+ new RazorError(
+ String.Format(
+ RazorResources.ParseError_Unexpected_Character_At_Helper_Name_Start,
+ String.Format(RazorResources.ErrorComponent_Character, "@")),
+ 9, 1, 0),
+ new RazorError(
+ String.Format(
+ RazorResources.ParseError_Unexpected_Character_At_Helper_Name_Start,
+ RazorResources.ErrorComponent_EndOfFile),
+ 16, 1, 7)
+ });
+ }
+
+ [Fact]
+ public void ParseHelperStatementTerminatesEarlyIfHeaderNotCompleteWithSpace()
+ {
+ ParseDocumentTest(@"@Helper @Helper",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new HelperBlock(new HelperCodeGenerator(new LocationTagged<string>(String.Empty, 8, 0, 8), headerComplete: false),
+ Factory.CodeTransition(),
+ Factory.MetaCode(@"Helper ").Accepts(AcceptedCharacters.None),
+ Factory.EmptyVB().Hidden()),
+ new HelperBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("Helper").Accepts(AcceptedCharacters.Any))),
+ designTimeParser: true,
+ expectedErrors: new[]
+ {
+ new RazorError(
+ String.Format(
+ RazorResources.ParseError_Unexpected_Character_At_Helper_Name_Start,
+ String.Format(RazorResources.ErrorComponent_Character, "@")),
+ 8, 0, 8),
+ new RazorError(
+ String.Format(
+ RazorResources.ParseError_Unexpected_Character_At_Helper_Name_Start,
+ RazorResources.ErrorComponent_EndOfFile),
+ 15, 0, 15)
+ });
+ }
+
+ [Fact]
+ public void ParseHelperStatementAllowsDifferentlyCasedEndHelperKeyword()
+ {
+ ParseDocumentTest(@"@Helper Foo()
+end helper",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new HelperBlock(new HelperCodeGenerator(new LocationTagged<string>("Foo()", 8, 0, 8), headerComplete: true),
+ Factory.CodeTransition(),
+ Factory.MetaCode("Helper ").Accepts(AcceptedCharacters.None),
+ Factory.Code("Foo()").Hidden(),
+ new StatementBlock(
+ Factory.Code("\r\n").AsStatement(),
+ Factory.MetaCode("end helper").Accepts(AcceptedCharacters.None))),
+ Factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void ParseHelperStatementCapturesWhitespaceToEndOfLineIfHelperStatementMissingName()
+ {
+ ParseDocumentTest(@"@Helper
+ ",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new HelperBlock(new HelperCodeGenerator(new LocationTagged<string>(" ", 8, 0, 8), headerComplete: false),
+ Factory.CodeTransition(),
+ Factory.MetaCode("Helper ").Accepts(AcceptedCharacters.None),
+ Factory.Code(" ").Hidden()),
+ Factory.Markup("\r\n ")),
+ new RazorError(
+ String.Format(
+ RazorResources.ParseError_Unexpected_Character_At_Helper_Name_Start,
+ RazorResources.ErrorComponent_Newline),
+ 30, 0, 30));
+ }
+
+ [Fact]
+ public void ParseHelperStatementCapturesWhitespaceToEndOfLineIfHelperStatementMissingOpenParen()
+ {
+ ParseDocumentTest(@"@Helper Foo
+ ",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new HelperBlock(new HelperCodeGenerator(new LocationTagged<string>("Foo ", 8, 0, 8), headerComplete: false),
+ Factory.CodeTransition(),
+ Factory.MetaCode("Helper ").Accepts(AcceptedCharacters.None),
+ Factory.Code("Foo ").Hidden()),
+ Factory.Markup("\r\n ")),
+ new RazorError(
+ String.Format(RazorResources.ParseError_MissingCharAfterHelperName, "("),
+ 15, 0, 15));
+ }
+
+ [Fact]
+ public void ParseHelperStatementCapturesAllContentToEndOfFileIfHelperStatementMissingCloseParenInParameterList()
+ {
+ ParseDocumentTest(@"@Helper Foo(Foo Bar
+Biz
+Boz",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new HelperBlock(new HelperCodeGenerator(new LocationTagged<string>("Foo(Foo Bar\r\nBiz\r\nBoz", 8, 0, 8), headerComplete: false),
+ Factory.CodeTransition(),
+ Factory.MetaCode("Helper ").Accepts(AcceptedCharacters.None),
+ Factory.Code("Foo(Foo Bar\r\nBiz\r\nBoz").Hidden())),
+ new RazorError(RazorResources.ParseError_UnterminatedHelperParameterList, 11, 0, 11));
+ }
+
+ [Fact]
+ public void ParseHelperStatementCapturesWhitespaceToEndOfLineIfHelperStatementMissingOpenBraceAfterParameterList()
+ {
+ ParseDocumentTest(@"@Helper Foo(foo as String)
+",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new HelperBlock(new HelperCodeGenerator(new LocationTagged<string>("Foo(foo as String)", 8, 0, 8), headerComplete: true),
+ Factory.CodeTransition(),
+ Factory.MetaCode("Helper ").Accepts(AcceptedCharacters.None),
+ Factory.Code("Foo(foo as String)")
+ .Hidden()
+ .AutoCompleteWith(SyntaxConstants.VB.EndHelperKeyword),
+ new StatementBlock(
+ Factory.Code(" \r\n").AsStatement()))),
+ new RazorError(
+ String.Format(RazorResources.ParseError_BlockNotTerminated, "Helper", "End Helper"),
+ 1, 0, 1));
+ }
+
+ [Fact]
+ public void ParseHelperStatementContinuesParsingHelperUntilEOF()
+ {
+ ParseDocumentTest(@"@Helper Foo(foo as String)
+ @<p>Foo</p>",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new HelperBlock(new HelperCodeGenerator(new LocationTagged<string>("Foo(foo as String)", 8, 0, 8), headerComplete: true),
+ Factory.CodeTransition(),
+ Factory.MetaCode("Helper ").Accepts(AcceptedCharacters.None),
+ Factory.Code("Foo(foo as String)")
+ .Hidden()
+ .AutoCompleteWith(SyntaxConstants.VB.EndHelperKeyword),
+ new StatementBlock(
+ Factory.Code("\r\n").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" "),
+ Factory.MarkupTransition(),
+ Factory.Markup("<p>Foo</p>").Accepts(AcceptedCharacters.None)),
+ Factory.EmptyVB().AsStatement()))),
+ new RazorError(
+ String.Format(RazorResources.ParseError_BlockNotTerminated, "Helper", "End Helper"),
+ 1, 0, 1));
+ }
+
+ [Fact]
+ public void ParseHelperStatementCorrectlyParsesHelperWithEmbeddedCode()
+ {
+ ParseDocumentTest(@"@Helper Foo(foo as String, bar as String)
+ @<p>@foo</p>
+End Helper",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new HelperBlock(new HelperCodeGenerator(new LocationTagged<string>("Foo(foo as String, bar as String)", 8, 0, 8), headerComplete: true),
+ Factory.CodeTransition(),
+ Factory.MetaCode("Helper ").Accepts(AcceptedCharacters.None),
+ Factory.Code("Foo(foo as String, bar as String)").Hidden(),
+ new StatementBlock(
+ Factory.Code("\r\n").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" "),
+ Factory.MarkupTransition(),
+ Factory.Markup("<p>"),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("foo")
+ .AsImplicitExpression(VBCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ Factory.Markup("</p>\r\n").Accepts(AcceptedCharacters.None)),
+ Factory.EmptyVB().AsStatement(),
+ Factory.MetaCode("End Helper").Accepts(AcceptedCharacters.None))),
+ Factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void ParseHelperStatementGivesWhitespaceAfterCloseParenToMarkup()
+ {
+ ParseDocumentTest(@"@Helper Foo(string foo)
+ ",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new HelperBlock(new HelperCodeGenerator(new LocationTagged<string>("Foo(string foo)", 8, 0, 8), headerComplete: true),
+ Factory.CodeTransition(),
+ Factory.MetaCode("Helper ").Accepts(AcceptedCharacters.None),
+ Factory.Code("Foo(string foo)")
+ .Hidden()
+ .AutoCompleteWith(SyntaxConstants.VB.EndHelperKeyword),
+ new StatementBlock(
+ Factory.Code(" \r\n ").AsStatement()))),
+ designTimeParser: true,
+ expectedErrors:
+ new RazorError(
+ String.Format(
+ RazorResources.ParseError_BlockNotTerminated,
+ "Helper", "End Helper"),
+ 1, 0, 1));
+ }
+
+ [Fact]
+ public void ParseHelperAcceptsNestedHelpersButOutputsError()
+ {
+ ParseDocumentTest(@"@Helper Foo(string foo)
+ @Helper Bar(string baz)
+ End Helper
+End Helper",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new HelperBlock(new HelperCodeGenerator(new LocationTagged<string>("Foo(string foo)", 8, 0, 8), headerComplete: true),
+ Factory.CodeTransition(),
+ Factory.MetaCode("Helper ").Accepts(AcceptedCharacters.None),
+ Factory.Code("Foo(string foo)").Hidden(),
+ new StatementBlock(
+ Factory.Code("\r\n ").AsStatement(),
+ new HelperBlock(new HelperCodeGenerator(new LocationTagged<string>("Bar(string baz)", 37, 1, 12), headerComplete: true),
+ Factory.CodeTransition(),
+ Factory.MetaCode("Helper ").Accepts(AcceptedCharacters.None),
+ Factory.Code("Bar(string baz)").Hidden(),
+ new StatementBlock(
+ Factory.Code("\r\n ").AsStatement(),
+ Factory.MetaCode("End Helper").Accepts(AcceptedCharacters.None))),
+ Factory.Code("\r\n").AsStatement(),
+ Factory.MetaCode("End Helper").Accepts(AcceptedCharacters.None))),
+ Factory.EmptyHtml()),
+ designTimeParser: true,
+ expectedErrors: new[]
+ {
+ new RazorError(
+ RazorResources.ParseError_Helpers_Cannot_Be_Nested,
+ 30, 1, 5)
+ });
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/VB/VBHtmlDocumentTest.cs b/test/System.Web.Razor.Test/Parser/VB/VBHtmlDocumentTest.cs
new file mode 100644
index 00000000..8bf1a066
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/VB/VBHtmlDocumentTest.cs
@@ -0,0 +1,248 @@
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Test.Framework;
+using System.Web.Razor.Tokenizer.Symbols;
+using Xunit;
+
+namespace System.Web.Razor.Test.Parser.VB
+{
+ public class VBHtmlDocumentTest : VBHtmlMarkupParserTestBase
+ {
+ [Fact]
+ public void BlockCommentInMarkupDocumentIsHandledCorrectly()
+ {
+ ParseDocumentTest(@"<ul>
+ @* This is a block comment </ul> *@ foo",
+ new MarkupBlock(
+ Factory.Markup("<ul>\r\n "),
+ new CommentBlock(
+ Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition),
+ Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None),
+ Factory.Comment(" This is a block comment </ul> ", HtmlSymbolType.RazorComment),
+ Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None),
+ Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition)),
+ Factory.Markup(" foo")));
+ }
+
+ [Fact]
+ public void BlockCommentInMarkupBlockIsHandledCorrectly()
+ {
+ ParseBlockTest(@"<ul>
+ @* This is a block comment </ul> *@ foo </ul>",
+ new MarkupBlock(
+ Factory.Markup("<ul>\r\n "),
+ new CommentBlock(
+ Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition),
+ Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None),
+ Factory.Comment(" This is a block comment </ul> ", HtmlSymbolType.RazorComment),
+ Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None),
+ Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition)),
+ Factory.Markup(" foo </ul>").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void BlockCommentAtStatementStartInCodeBlockIsHandledCorrectly()
+ {
+ ParseDocumentTest(@"@If Request.IsAuthenticated Then
+ @* User is logged in! End If *@
+ Write(""Hello friend!"")
+End If",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("If Request.IsAuthenticated Then\r\n ").AsStatement(),
+ new CommentBlock(
+ Factory.CodeTransition(VBSymbolType.RazorCommentTransition),
+ Factory.MetaCode("*", VBSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None),
+ Factory.Comment(" User is logged in! End If ", VBSymbolType.RazorComment),
+ Factory.MetaCode("*", VBSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None),
+ Factory.CodeTransition(VBSymbolType.RazorCommentTransition)),
+ Factory.Code("\r\n Write(\"Hello friend!\")\r\nEnd If")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)),
+ Factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void BlockCommentInStatementInCodeBlockIsHandledCorrectly()
+ {
+ ParseDocumentTest(@"@If Request.IsAuthenticated Then
+ Dim foo = @* User is logged in! End If *@ bar
+ Write(""Hello friend!"")
+End If",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("If Request.IsAuthenticated Then\r\n Dim foo = ").AsStatement(),
+ new CommentBlock(
+ Factory.CodeTransition(VBSymbolType.RazorCommentTransition),
+ Factory.MetaCode("*", VBSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None),
+ Factory.Comment(" User is logged in! End If ", VBSymbolType.RazorComment),
+ Factory.MetaCode("*", VBSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None),
+ Factory.CodeTransition(VBSymbolType.RazorCommentTransition)),
+ Factory.Code(" bar\r\n Write(\"Hello friend!\")\r\nEnd If")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)),
+ Factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void BlockCommentInStringInCodeBlockIsIgnored()
+ {
+ ParseDocumentTest(@"@If Request.IsAuthenticated Then
+ Dim foo = ""@* User is logged in! End If *@ bar""
+ Write(""Hello friend!"")
+End If",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("If Request.IsAuthenticated Then\r\n Dim foo = \"@* User is logged in! End If *@ bar\"\r\n Write(\"Hello friend!\")\r\nEnd If")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)),
+ Factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void BlockCommentInTickCommentInCodeBlockIsIgnored()
+ {
+ ParseDocumentTest(@"@If Request.IsAuthenticated Then
+ Dim foo = '@* User is logged in! End If *@ bar
+ Write(""Hello friend!"")
+End If",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("If Request.IsAuthenticated Then\r\n Dim foo = '@* User is logged in! End If *@ bar\r\n Write(\"Hello friend!\")\r\nEnd If")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)),
+ Factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void BlockCommentInRemCommentInCodeBlockIsIgnored()
+ {
+ ParseDocumentTest(@"@If Request.IsAuthenticated Then
+ Dim foo = REM @* User is logged in! End If *@ bar
+ Write(""Hello friend!"")
+End If",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("If Request.IsAuthenticated Then\r\n Dim foo = REM @* User is logged in! End If *@ bar\r\n Write(\"Hello friend!\")\r\nEnd If")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)),
+ Factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void BlockCommentInImplicitExpressionIsHandledCorrectly()
+ {
+ ParseDocumentTest("@Html.Foo@*bar*@",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("Html.Foo")
+ .AsImplicitExpression(KeywordSet)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ Factory.EmptyHtml(),
+ new CommentBlock(
+ Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition),
+ Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None),
+ Factory.Comment("bar", HtmlSymbolType.RazorComment),
+ Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None),
+ Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition)),
+ Factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void BlockCommentAfterDotOfImplicitExpressionIsHandledCorrectly()
+ {
+ ParseDocumentTest("@Html.@*bar*@",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("Html")
+ .AsImplicitExpression(KeywordSet)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ Factory.Markup("."),
+ new CommentBlock(
+ Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition),
+ Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None),
+ Factory.Comment("bar", HtmlSymbolType.RazorComment),
+ Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None),
+ Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition)),
+ Factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void BlockCommentInParensOfImplicitExpressionIsHandledCorrectly()
+ {
+ ParseDocumentTest("@Html.Foo(@*bar*@ 4)",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("Html.Foo(")
+ .AsImplicitExpression(KeywordSet)
+ .Accepts(AcceptedCharacters.Any),
+ new CommentBlock(
+ Factory.CodeTransition(VBSymbolType.RazorCommentTransition),
+ Factory.MetaCode("*", VBSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None),
+ Factory.Comment("bar", VBSymbolType.RazorComment),
+ Factory.MetaCode("*", VBSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None),
+ Factory.CodeTransition(VBSymbolType.RazorCommentTransition)),
+ Factory.Code(" 4)")
+ .AsImplicitExpression(KeywordSet)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ Factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void BlockCommentInConditionIsHandledCorrectly()
+ {
+ ParseDocumentTest("@If @*bar*@ Then End If",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("If ").AsStatement(),
+ new CommentBlock(
+ Factory.CodeTransition(VBSymbolType.RazorCommentTransition),
+ Factory.MetaCode("*", VBSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None),
+ Factory.Comment("bar", VBSymbolType.RazorComment),
+ Factory.MetaCode("*", VBSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None),
+ Factory.CodeTransition(VBSymbolType.RazorCommentTransition)),
+ Factory.Code(" Then End If").AsStatement().Accepts(AcceptedCharacters.None)),
+ Factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void BlockCommentInExplicitExpressionIsHandledCorrectly()
+ {
+ ParseDocumentTest(@"@(1 + @*bar*@ 1)",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("(").Accepts(AcceptedCharacters.None),
+ Factory.Code(@"1 + ").AsExpression(),
+ new CommentBlock(
+ Factory.CodeTransition(VBSymbolType.RazorCommentTransition),
+ Factory.MetaCode("*", VBSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None),
+ Factory.Comment("bar", VBSymbolType.RazorComment),
+ Factory.MetaCode("*", VBSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None),
+ Factory.CodeTransition(VBSymbolType.RazorCommentTransition)
+ ),
+ Factory.Code(" 1").AsExpression(),
+ Factory.MetaCode(")").Accepts(AcceptedCharacters.None)
+ ),
+ Factory.EmptyHtml()));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/VB/VBImplicitExpressionTest.cs b/test/System.Web.Razor.Test/Parser/VB/VBImplicitExpressionTest.cs
new file mode 100644
index 00000000..1ec7528d
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/VB/VBImplicitExpressionTest.cs
@@ -0,0 +1,77 @@
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Test.Framework;
+using Xunit;
+
+namespace System.Web.Razor.Test.Parser.VB
+{
+ public class VBImplicitExpressionTest : VBHtmlCodeParserTestBase
+ {
+ [Fact]
+ public void VB_Simple_ImplicitExpression()
+ {
+ ParseBlockTest("@foo not-part-of-the-block",
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("foo")
+ .AsImplicitExpression(VBCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)));
+ }
+
+ [Fact]
+ public void VB_ImplicitExpression_With_Keyword_At_Start()
+ {
+ ParseBlockTest("@Partial",
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("Partial")
+ .AsImplicitExpression(VBCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)));
+ }
+
+ [Fact]
+ public void VB_ImplicitExpression_With_Keyword_In_Body()
+ {
+ ParseBlockTest("@Html.Partial",
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("Html.Partial")
+ .AsImplicitExpression(VBCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)));
+ }
+
+ [Fact]
+ public void VB_ImplicitExpression_With_MethodCallOrArrayIndex()
+ {
+ ParseBlockTest("@foo(42) not-part-of-the-block",
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("foo(42)")
+ .AsImplicitExpression(VBCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)));
+ }
+
+ [Fact]
+ public void VB_ImplicitExpression_Terminates_If_Trailing_Dot_Not_Followed_By_Valid_Token()
+ {
+ ParseBlockTest("@foo(42). ",
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("foo(42)")
+ .AsImplicitExpression(VBCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)));
+ }
+
+ [Fact]
+ public void VB_ImplicitExpression_Supports_Complex_Expressions()
+ {
+ ParseBlockTest("@foo(42).bar(Biz.Boz / 42 * 8)(1).Burf not part of the block",
+ new ExpressionBlock(
+ Factory.CodeTransition()
+ .Accepts(AcceptedCharacters.None),
+ Factory.Code("foo(42).bar(Biz.Boz / 42 * 8)(1).Burf")
+ .AsImplicitExpression(VBCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/VB/VBLayoutDirectiveTest.cs b/test/System.Web.Razor.Test/Parser/VB/VBLayoutDirectiveTest.cs
new file mode 100644
index 00000000..a1d58b86
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/VB/VBLayoutDirectiveTest.cs
@@ -0,0 +1,86 @@
+using System.Web.Razor.Editor;
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Test.Framework;
+using Xunit;
+using Xunit.Extensions;
+
+namespace System.Web.Razor.Test.Parser.VB
+{
+ public class VBLayoutDirectiveTest : VBHtmlCodeParserTestBase
+ {
+ [Theory]
+ [InlineData("layout")]
+ [InlineData("Layout")]
+ [InlineData("LAYOUT")]
+ [InlineData("layOut")]
+ [InlineData("LayOut")]
+ [InlineData("LaYoUt")]
+ [InlineData("lAyOuT")]
+ public void LayoutDirectiveSupportsAnyCasingOfKeyword(string keyword)
+ {
+ ParseBlockTest("@" + keyword,
+ new DirectiveBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode(keyword)
+ )
+ );
+ }
+
+ [Fact]
+ public void LayoutDirectiveAcceptsAllTextToEndOfLine()
+ {
+ ParseBlockTest(@"@Layout Foo Bar Baz",
+ new DirectiveBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("Layout ").Accepts(AcceptedCharacters.None),
+ Factory.MetaCode("Foo Bar Baz")
+ .With(new SetLayoutCodeGenerator("Foo Bar Baz"))
+ .WithEditorHints(EditorHints.VirtualPath | EditorHints.LayoutPage)
+ )
+ );
+ }
+
+ [Fact]
+ public void LayoutDirectiveAcceptsAnyIfNoWhitespaceFollowingLayoutKeyword()
+ {
+ ParseBlockTest(@"@Layout",
+ new DirectiveBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("Layout")
+ )
+ );
+ }
+
+ [Fact]
+ public void LayoutDirectiveOutputsMarkerSpanIfAnyWhitespaceAfterLayoutKeyword()
+ {
+ ParseBlockTest(@"@Layout ",
+ new DirectiveBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("Layout ").Accepts(AcceptedCharacters.None),
+ Factory.EmptyVB()
+ .AsMetaCode()
+ .With(new SetLayoutCodeGenerator(String.Empty))
+ .WithEditorHints(EditorHints.VirtualPath | EditorHints.LayoutPage)
+ )
+ );
+ }
+
+ [Fact]
+ public void LayoutDirectiveAcceptsTrailingNewlineButDoesNotIncludeItInLayoutPath()
+ {
+ ParseBlockTest(@"@Layout Foo
+",
+ new DirectiveBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("Layout ").Accepts(AcceptedCharacters.None),
+ Factory.MetaCode("Foo\r\n")
+ .With(new SetLayoutCodeGenerator("Foo"))
+ .Accepts(AcceptedCharacters.None)
+ .WithEditorHints(EditorHints.VirtualPath | EditorHints.LayoutPage)
+ )
+ );
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/VB/VBNestedStatementsTest.cs b/test/System.Web.Razor.Test/Parser/VB/VBNestedStatementsTest.cs
new file mode 100644
index 00000000..8d4f8fca
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/VB/VBNestedStatementsTest.cs
@@ -0,0 +1,167 @@
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Test.Framework;
+using System.Web.Razor.Tokenizer.Symbols;
+using Xunit;
+
+namespace System.Web.Razor.Test.Parser.VB
+{
+ public class VBNestedStatementsTest : VBHtmlCodeParserTestBase
+ {
+ [Fact]
+ public void VB_Nested_If_Statement()
+ {
+ ParseBlockTest(@"@If True Then
+ If False Then
+ End If
+End If",
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("If True Then\r\n If False Then\r\n End If\r\nEnd If")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void VB_Nested_Do_Statement()
+ {
+ ParseBlockTest(@"@Do While True
+ Do
+ Loop Until False
+Loop",
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("Do While True\r\n Do\r\n Loop Until False\r\nLoop")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.AnyExceptNewline)));
+ }
+
+ [Fact]
+ public void VB_Nested_Markup_Statement_In_If()
+ {
+ ParseBlockTest(@"@If True Then
+ @<p>Tag</p>
+End If",
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("If True Then\r\n")
+ .AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" "),
+ Factory.MarkupTransition(),
+ Factory.Markup("<p>Tag</p>\r\n")
+ .Accepts(AcceptedCharacters.None)),
+ Factory.Code("End If")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void VB_Nested_Markup_Statement_In_Code()
+ {
+ ParseBlockTest(@"@Code
+ Foo()
+ @<p>Tag</p>
+ Bar()
+End Code",
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("Code")
+ .Accepts(AcceptedCharacters.None),
+ Factory.Code("\r\n Foo()\r\n")
+ .AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" "),
+ Factory.MarkupTransition(),
+ Factory.Markup("<p>Tag</p>\r\n")
+ .Accepts(AcceptedCharacters.None)),
+ Factory.Code(" Bar()\r\n")
+ .AsStatement(),
+ Factory.MetaCode("End Code")
+ .Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void VB_Nested_Markup_Statement_In_Do()
+ {
+ ParseBlockTest(@"@Do
+ @<p>Tag</p>
+Loop While True",
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("Do\r\n")
+ .AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" "),
+ Factory.MarkupTransition(),
+ Factory.Markup("<p>Tag</p>\r\n")
+ .Accepts(AcceptedCharacters.None)),
+ Factory.Code("Loop While True")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.AnyExceptNewline)));
+ }
+
+ [Fact]
+ public void VB_Nested_Single_Line_Markup_Statement_In_Do()
+ {
+ ParseBlockTest(@"@Do
+ @:<p>Tag
+Loop While True",
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("Do\r\n")
+ .AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" "),
+ Factory.MarkupTransition(),
+ Factory.MetaMarkup(":", HtmlSymbolType.Colon),
+ Factory.Markup("<p>Tag\r\n")
+ .Accepts(AcceptedCharacters.None)),
+ Factory.Code("Loop While True")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.AnyExceptNewline)));
+ }
+
+ [Fact]
+ public void VB_Nested_Implicit_Expression_In_If()
+ {
+ ParseBlockTest(@"@If True Then
+ @Foo.Bar
+End If",
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("If True Then\r\n ")
+ .AsStatement(),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("Foo.Bar")
+ .AsExpression()
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ Factory.Code("\r\nEnd If")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void VB_Nested_Explicit_Expression_In_If()
+ {
+ ParseBlockTest(@"@If True Then
+ @(Foo.Bar + 42)
+End If",
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("If True Then\r\n ")
+ .AsStatement(),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("(")
+ .Accepts(AcceptedCharacters.None),
+ Factory.Code("Foo.Bar + 42")
+ .AsExpression(),
+ Factory.MetaCode(")")
+ .Accepts(AcceptedCharacters.None)),
+ Factory.Code("\r\nEnd If")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/VB/VBRazorCommentsTest.cs b/test/System.Web.Razor.Test/Parser/VB/VBRazorCommentsTest.cs
new file mode 100644
index 00000000..e90c5a2d
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/VB/VBRazorCommentsTest.cs
@@ -0,0 +1,172 @@
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Test.Framework;
+using System.Web.Razor.Tokenizer.Symbols;
+using Xunit;
+
+namespace System.Web.Razor.Test.Parser.VB
+{
+ public class VBRazorCommentsTest : VBHtmlMarkupParserTestBase
+ {
+ [Fact]
+ public void UnterminatedRazorComment()
+ {
+ ParseDocumentTest("@*",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new CommentBlock(
+ Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition)
+ .Accepts(AcceptedCharacters.None),
+ Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar)
+ .Accepts(AcceptedCharacters.None),
+ Factory.Span(SpanKind.Comment, new HtmlSymbol(
+ Factory.LocationTracker.CurrentLocation,
+ String.Empty,
+ HtmlSymbolType.Unknown))
+ .Accepts(AcceptedCharacters.Any))),
+ new RazorError(RazorResources.ParseError_RazorComment_Not_Terminated, 0, 0, 0));
+ }
+
+ [Fact]
+ public void EmptyRazorComment()
+ {
+ ParseDocumentTest("@**@",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new CommentBlock(
+ Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition)
+ .Accepts(AcceptedCharacters.None),
+ Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar)
+ .Accepts(AcceptedCharacters.None),
+ Factory.Span(SpanKind.Comment, new HtmlSymbol(
+ Factory.LocationTracker.CurrentLocation,
+ String.Empty,
+ HtmlSymbolType.Unknown))
+ .Accepts(AcceptedCharacters.Any),
+ Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar)
+ .Accepts(AcceptedCharacters.None),
+ Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition)
+ .Accepts(AcceptedCharacters.None)),
+ Factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void RazorCommentInImplicitExpressionMethodCall()
+ {
+ ParseDocumentTest(@"@foo(@**@",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("foo(")
+ .AsImplicitExpression(VBCodeParser.DefaultKeywords),
+ new CommentBlock(
+ Factory.CodeTransition(VBSymbolType.RazorCommentTransition)
+ .Accepts(AcceptedCharacters.None),
+ Factory.MetaCode("*", VBSymbolType.RazorCommentStar)
+ .Accepts(AcceptedCharacters.None),
+ Factory.Span(SpanKind.Comment, new VBSymbol(
+ Factory.LocationTracker.CurrentLocation,
+ String.Empty,
+ VBSymbolType.Unknown))
+ .Accepts(AcceptedCharacters.Any),
+ Factory.MetaCode("*", VBSymbolType.RazorCommentStar)
+ .Accepts(AcceptedCharacters.None),
+ Factory.CodeTransition(VBSymbolType.RazorCommentTransition)
+ .Accepts(AcceptedCharacters.None)),
+ Factory.EmptyVB()
+ .AsImplicitExpression(VBCodeParser.DefaultKeywords))),
+ new RazorError(
+ String.Format(RazorResources.ParseError_Expected_CloseBracket_Before_EOF, "(", ")"),
+ 4, 0, 4));
+ }
+
+ [Fact]
+ public void UnterminatedRazorCommentInImplicitExpressionMethodCall()
+ {
+ ParseDocumentTest("@foo(@*",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("foo(")
+ .AsImplicitExpression(VBCodeParser.DefaultKeywords),
+ new CommentBlock(
+ Factory.CodeTransition(VBSymbolType.RazorCommentTransition)
+ .Accepts(AcceptedCharacters.None),
+ Factory.MetaCode("*", VBSymbolType.RazorCommentStar)
+ .Accepts(AcceptedCharacters.None),
+ Factory.Span(SpanKind.Comment, new VBSymbol(
+ Factory.LocationTracker.CurrentLocation,
+ String.Empty,
+ VBSymbolType.Unknown))
+ .Accepts(AcceptedCharacters.Any)))),
+ new RazorError(RazorResources.ParseError_RazorComment_Not_Terminated, 5, 0, 5),
+ new RazorError(String.Format(RazorResources.ParseError_Expected_CloseBracket_Before_EOF, "(", ")"), 4, 0, 4));
+ }
+
+ [Fact]
+ public void RazorCommentInVerbatimBlock()
+ {
+ ParseDocumentTest(@"@Code
+ @<text
+ @**@
+End Code",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("Code").Accepts(AcceptedCharacters.None),
+ Factory.Code("\r\n").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" "),
+ Factory.MarkupTransition("@"),
+ Factory.MarkupTransition("<text").Accepts(AcceptedCharacters.Any),
+ Factory.Markup("\r\n "),
+ new CommentBlock(
+ Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition)
+ .Accepts(AcceptedCharacters.None),
+ Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar)
+ .Accepts(AcceptedCharacters.None),
+ Factory.Span(SpanKind.Comment, new HtmlSymbol(
+ Factory.LocationTracker.CurrentLocation,
+ String.Empty,
+ HtmlSymbolType.Unknown))
+ .Accepts(AcceptedCharacters.Any),
+ Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar)
+ .Accepts(AcceptedCharacters.None),
+ Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition)
+ .Accepts(AcceptedCharacters.None)),
+ Factory.Markup("\r\nEnd Code")))),
+ new RazorError(RazorResources.ParseError_TextTagCannotContainAttributes, 12, 1, 5),
+ new RazorError(String.Format(RazorResources.ParseError_MissingEndTag, "text"), 12, 1, 5),
+ new RazorError(String.Format(RazorResources.ParseError_BlockNotTerminated, SyntaxConstants.VB.CodeKeyword, SyntaxConstants.VB.EndCodeKeyword), 1, 0, 1));
+ }
+
+ [Fact]
+ public void UnterminatedRazorCommentInVerbatimBlock()
+ {
+ ParseDocumentTest(@"@Code
+@*",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("Code").Accepts(AcceptedCharacters.None),
+ Factory.Code("\r\n")
+ .AsStatement(),
+ new CommentBlock(
+ Factory.CodeTransition(VBSymbolType.RazorCommentTransition)
+ .Accepts(AcceptedCharacters.None),
+ Factory.MetaCode("*", VBSymbolType.RazorCommentStar)
+ .Accepts(AcceptedCharacters.None),
+ Factory.Span(SpanKind.Comment, new VBSymbol(Factory.LocationTracker.CurrentLocation,
+ String.Empty,
+ VBSymbolType.Unknown))
+ .Accepts(AcceptedCharacters.Any)))),
+ new RazorError(RazorResources.ParseError_RazorComment_Not_Terminated, 7, 1, 0),
+ new RazorError(String.Format(RazorResources.ParseError_BlockNotTerminated, SyntaxConstants.VB.CodeKeyword, SyntaxConstants.VB.EndCodeKeyword), 1, 0, 1));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/VB/VBReservedWordsTest.cs b/test/System.Web.Razor.Test/Parser/VB/VBReservedWordsTest.cs
new file mode 100644
index 00000000..fe0c1f87
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/VB/VBReservedWordsTest.cs
@@ -0,0 +1,28 @@
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Test.Framework;
+using System.Web.Razor.Text;
+using Xunit.Extensions;
+
+namespace System.Web.Razor.Test.Parser.VB
+{
+ public class VBReservedWordsTest : VBHtmlCodeParserTestBase
+ {
+ [Theory]
+ [InlineData("Namespace")]
+ [InlineData("Class")]
+ [InlineData("NAMESPACE")]
+ [InlineData("CLASS")]
+ [InlineData("NameSpace")]
+ [InlineData("nameSpace")]
+ private void ReservedWords(string word)
+ {
+ ParseBlockTest(word,
+ new DirectiveBlock(
+ Factory.MetaCode(word).Accepts(AcceptedCharacters.None)),
+ new RazorError(
+ String.Format(RazorResources.ParseError_ReservedWord, word),
+ SourceLocation.Zero));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/VB/VBSectionTest.cs b/test/System.Web.Razor.Test/Parser/VB/VBSectionTest.cs
new file mode 100644
index 00000000..e0052527
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/VB/VBSectionTest.cs
@@ -0,0 +1,225 @@
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Test.Framework;
+using Xunit;
+
+namespace System.Web.Razor.Test.Parser.VB
+{
+ public class VBSectionTest : VBHtmlMarkupParserTestBase
+ {
+ [Fact]
+ public void ParseSectionBlockCapturesNewlineImmediatelyFollowing()
+ {
+ ParseDocumentTest(@"@Section
+",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new SectionBlock(new SectionCodeGenerator(String.Empty),
+ Factory.CodeTransition(),
+ Factory.MetaCode("Section\r\n"),
+ new MarkupBlock())),
+ new RazorError(
+ String.Format(
+ RazorResources.ParseError_Unexpected_Character_At_Section_Name_Start,
+ RazorResources.ErrorComponent_EndOfFile),
+ 10, 1, 0),
+ new RazorError(
+ String.Format(
+ RazorResources.ParseError_BlockNotTerminated,
+ "Section", "End Section"),
+ 1, 0, 1));
+ }
+
+ [Fact]
+ public void ParseSectionRequiresNameBeOnSameLineAsSectionKeyword()
+ {
+ ParseDocumentTest(@"@Section
+Foo
+ <p>Body</p>
+End Section",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new SectionBlock(new SectionCodeGenerator(String.Empty),
+ Factory.CodeTransition(),
+ Factory.MetaCode("Section "),
+ new MarkupBlock(
+ Factory.Markup("\r\nFoo\r\n <p>Body</p>\r\n")),
+ Factory.MetaCode("End Section").Accepts(AcceptedCharacters.None)),
+ Factory.EmptyHtml()),
+ new RazorError(
+ String.Format(
+ RazorResources.ParseError_Unexpected_Character_At_Section_Name_Start,
+ RazorResources.ErrorComponent_Newline),
+ 9, 0, 9));
+ }
+
+ [Fact]
+ public void ParseSectionAllowsNameToBeOnDifferentLineAsSectionKeywordIfUnderscoresUsed()
+ {
+ ParseDocumentTest(@"@Section _
+_
+Foo
+ <p>Body</p>
+End Section",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new SectionBlock(new SectionCodeGenerator("Foo"),
+ Factory.CodeTransition(),
+ Factory.MetaCode("Section _\r\n_\r\nFoo"),
+ new MarkupBlock(
+ Factory.Markup("\r\n <p>Body</p>\r\n")),
+ Factory.MetaCode("End Section").Accepts(AcceptedCharacters.None)),
+ Factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void ParseSectionReportsErrorAndTerminatesSectionBlockIfKeywordNotFollowedByIdentifierStartCharacter()
+ {
+ ParseDocumentTest(@"@Section 9
+ <p>Foo</p>
+End Section",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new SectionBlock(new SectionCodeGenerator(String.Empty),
+ Factory.CodeTransition(),
+ Factory.MetaCode("Section "),
+ new MarkupBlock(
+ Factory.Markup("9\r\n <p>Foo</p>\r\n")),
+ Factory.MetaCode("End Section").Accepts(AcceptedCharacters.None)),
+ Factory.EmptyHtml()),
+ new RazorError(
+ String.Format(
+ RazorResources.ParseError_Unexpected_Character_At_Section_Name_Start,
+ String.Format(RazorResources.ErrorComponent_Character, "9")),
+ 9, 0, 9));
+ }
+
+ [Fact]
+ public void ParserOutputsErrorOnNestedSections()
+ {
+ ParseDocumentTest(@"@Section foo
+ @Section bar
+ <p>Foo</p>
+ End Section
+End Section",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new SectionBlock(new SectionCodeGenerator("foo"),
+ Factory.CodeTransition(),
+ Factory.MetaCode("Section foo"),
+ new MarkupBlock(
+ Factory.Markup("\r\n"),
+ new SectionBlock(new SectionCodeGenerator("bar"),
+ Factory.Code(" ").AsStatement(),
+ Factory.CodeTransition(),
+ Factory.MetaCode("Section bar"),
+ new MarkupBlock(
+ Factory.Markup("\r\n <p>Foo</p>\r\n ")),
+ Factory.MetaCode("End Section").Accepts(AcceptedCharacters.None)),
+ Factory.Markup("\r\n")),
+ Factory.MetaCode("End Section").Accepts(AcceptedCharacters.None)),
+ Factory.EmptyHtml()),
+ new RazorError(
+ String.Format(
+ RazorResources.ParseError_Sections_Cannot_Be_Nested,
+ RazorResources.SectionExample_VB),
+ 26, 1, 12));
+ }
+
+ [Fact]
+ public void ParseSectionHandlesEOFAfterIdentifier()
+ {
+ ParseDocumentTest("@Section foo",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new SectionBlock(new SectionCodeGenerator("foo"),
+ Factory.CodeTransition(),
+ Factory.MetaCode("Section foo")
+ .AutoCompleteWith(SyntaxConstants.VB.EndSectionKeyword),
+ new MarkupBlock())),
+ new RazorError(
+ String.Format(
+ RazorResources.ParseError_BlockNotTerminated,
+ "Section", "End Section"),
+ 1, 0, 1));
+ }
+
+ [Fact]
+ public void ParseSectionHandlesUnterminatedSection()
+ {
+ ParseDocumentTest(@"@Section foo
+ <p>Foo</p>",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new SectionBlock(new SectionCodeGenerator("foo"),
+ Factory.CodeTransition(),
+ Factory.MetaCode("Section foo")
+ .AutoCompleteWith(SyntaxConstants.VB.EndSectionKeyword),
+ new MarkupBlock(
+ Factory.Markup("\r\n <p>Foo</p>")))),
+ new RazorError(
+ String.Format(
+ RazorResources.ParseError_BlockNotTerminated,
+ "Section", "End Section"),
+ 1, 0, 1));
+ }
+
+ [Fact]
+ public void ParseDocumentParsesNamedSectionCorrectly()
+ {
+ ParseDocumentTest(@"@Section foo
+ <p>Foo</p>
+End Section",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new SectionBlock(new SectionCodeGenerator("foo"),
+ Factory.CodeTransition(),
+ Factory.MetaCode("Section foo"),
+ new MarkupBlock(
+ Factory.Markup("\r\n <p>Foo</p>\r\n")),
+ Factory.MetaCode("End Section").Accepts(AcceptedCharacters.None)),
+ Factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void ParseSectionTerminatesOnFirstEndSection()
+ {
+ ParseDocumentTest(@"@Section foo
+ <p>End Section</p>",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new SectionBlock(new SectionCodeGenerator("foo"),
+ Factory.CodeTransition(),
+ Factory.MetaCode("Section foo"),
+ new MarkupBlock(
+ Factory.Markup("\r\n <p>")),
+ Factory.MetaCode("End Section").Accepts(AcceptedCharacters.None)),
+ Factory.Markup("</p>")));
+ }
+
+ [Fact]
+ public void ParseSectionAllowsEndSectionInVBExpression()
+ {
+ ParseDocumentTest(@"@Section foo
+ I really want to render the word @(""End Section""), so this is how I do it
+End Section",
+ new MarkupBlock(
+ Factory.EmptyHtml(),
+ new SectionBlock(new SectionCodeGenerator("foo"),
+ Factory.CodeTransition(),
+ Factory.MetaCode("Section foo"),
+ new MarkupBlock(
+ Factory.Markup("\r\n I really want to render the word "),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("(").Accepts(AcceptedCharacters.None),
+ Factory.Code("\"End Section\"").AsExpression(),
+ Factory.MetaCode(")").Accepts(AcceptedCharacters.None)),
+ Factory.Markup(", so this is how I do it\r\n")),
+ Factory.MetaCode("End Section").Accepts(AcceptedCharacters.None)),
+ Factory.EmptyHtml()));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/VB/VBSpecialKeywordsTest.cs b/test/System.Web.Razor.Test/Parser/VB/VBSpecialKeywordsTest.cs
new file mode 100644
index 00000000..9c4b17bd
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/VB/VBSpecialKeywordsTest.cs
@@ -0,0 +1,169 @@
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Test.Framework;
+using Xunit;
+
+namespace System.Web.Razor.Test.Parser.VB
+{
+ public class VBSpecialKeywordsTest : VBHtmlCodeParserTestBase
+ {
+ [Fact]
+ public void ParseInheritsStatementMarksInheritsSpanAsCanGrowIfMissingTrailingSpace()
+ {
+ ParseBlockTest("inherits",
+ new DirectiveBlock(
+ Factory.MetaCode("inherits")),
+ new RazorError(
+ RazorResources.ParseError_InheritsKeyword_Must_Be_Followed_By_TypeName,
+ 8, 0, 8));
+ }
+
+ [Fact]
+ public void InheritsBlockAcceptsMultipleGenericArguments()
+ {
+ ParseBlockTest("inherits Foo.Bar(Of Biz(Of Qux), String, Integer).Baz",
+ new DirectiveBlock(
+ Factory.MetaCode("inherits ").Accepts(AcceptedCharacters.None),
+ Factory.Code("Foo.Bar(Of Biz(Of Qux), String, Integer).Baz")
+ .AsBaseType("Foo.Bar(Of Biz(Of Qux), String, Integer).Baz")));
+ }
+
+ [Fact]
+ public void InheritsDirectiveSupportsVSTemplateTokens()
+ {
+ ParseBlockTest("@Inherits $rootnamespace$.MyBase",
+ new DirectiveBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("Inherits ")
+ .Accepts(AcceptedCharacters.None),
+ Factory.Code("$rootnamespace$.MyBase")
+ .AsBaseType("$rootnamespace$.MyBase")));
+ }
+
+ [Fact]
+ public void InheritsBlockOutputsErrorIfInheritsNotFollowedByTypeButAcceptsEntireLineAsCode()
+ {
+ ParseBlockTest(@"inherits
+foo",
+ new DirectiveBlock(
+ Factory.MetaCode("inherits ").Accepts(AcceptedCharacters.None),
+ Factory.Code("\r\n").AsBaseType(String.Empty)),
+ new RazorError(
+ RazorResources.ParseError_InheritsKeyword_Must_Be_Followed_By_TypeName,
+ 8, 0, 8));
+ }
+
+ [Fact]
+ public void ParseBlockShouldSupportNamespaceImports()
+ {
+ ParseBlockTest("Imports Foo.Bar.Baz.Biz.Boz",
+ new DirectiveBlock(
+ Factory.MetaCode("Imports Foo.Bar.Baz.Biz.Boz")
+ .With(new AddImportCodeGenerator(
+ ns: " Foo.Bar.Baz.Biz.Boz",
+ namespaceKeywordLength: SyntaxConstants.VB.ImportsKeywordLength))));
+ }
+
+ [Fact]
+ public void ParseBlockShowsErrorIfNamespaceNotOnSameLineAsImportsKeyword()
+ {
+ ParseBlockTest(@"Imports
+Foo",
+ new DirectiveBlock(
+ Factory.MetaCode("Imports\r\n")
+ .With(new AddImportCodeGenerator(
+ ns: "\r\n",
+ namespaceKeywordLength: SyntaxConstants.VB.ImportsKeywordLength))),
+ new RazorError(
+ RazorResources.ParseError_NamespaceOrTypeAliasExpected,
+ 7, 0, 7));
+ }
+
+ [Fact]
+ public void ParseBlockShowsErrorIfTypeBeingAliasedNotOnSameLineAsImportsKeyword()
+ {
+ ParseBlockTest(@"Imports Foo =
+System.Bar",
+ new DirectiveBlock(
+ Factory.MetaCode("Imports Foo =\r\n")
+ .With(new AddImportCodeGenerator(
+ ns: " Foo =\r\n",
+ namespaceKeywordLength: SyntaxConstants.VB.ImportsKeywordLength))));
+ }
+
+ [Fact]
+ public void ParseBlockShouldSupportTypeAliases()
+ {
+ ParseBlockTest("Imports Foo = Bar.Baz.Biz.Boz",
+ new DirectiveBlock(
+ Factory.MetaCode("Imports Foo = Bar.Baz.Biz.Boz")
+ .With(new AddImportCodeGenerator(
+ ns: " Foo = Bar.Baz.Biz.Boz",
+ namespaceKeywordLength: SyntaxConstants.VB.ImportsKeywordLength))));
+ }
+
+ [Fact]
+ public void ParseBlockThrowsErrorIfOptionIsNotFollowedByStrictOrExplicit()
+ {
+ ParseBlockTest("Option FizzBuzz On",
+ new DirectiveBlock(
+ Factory.MetaCode("Option FizzBuzz On")
+ .With(new SetVBOptionCodeGenerator(optionName: null, value: true))),
+ new RazorError(
+ String.Format(RazorResources.ParseError_UnknownOption, "FizzBuzz"),
+ 7, 0, 7));
+ }
+
+ [Fact]
+ public void ParseBlockThrowsErrorIfOptionStrictIsNotFollowedByOnOrOff()
+ {
+ ParseBlockTest("Option Strict Yes",
+ new DirectiveBlock(
+ Factory.MetaCode("Option Strict Yes")
+ .With(SetVBOptionCodeGenerator.Strict(true))),
+ new RazorError(
+ String.Format(
+ RazorResources.ParseError_InvalidOptionValue,
+ "Strict", "Yes"),
+ 14, 0, 14));
+ }
+
+ [Fact]
+ public void ParseBlockReadsToAfterOnKeywordIfOptionStrictBlock()
+ {
+ ParseBlockTest("Option Strict On Foo Bar Baz",
+ new DirectiveBlock(
+ Factory.MetaCode("Option Strict On")
+ .With(SetVBOptionCodeGenerator.Strict(true))));
+ }
+
+ [Fact]
+ public void ParseBlockReadsToAfterOffKeywordIfOptionStrictBlock()
+ {
+ ParseBlockTest("Option Strict Off Foo Bar Baz",
+ new DirectiveBlock(
+ Factory.MetaCode("Option Strict Off")
+ .With(SetVBOptionCodeGenerator.Strict(false))));
+ }
+
+ [Fact]
+ public void ParseBlockReadsToAfterOnKeywordIfOptionExplicitBlock()
+ {
+ ParseBlockTest("Option Explicit On Foo Bar Baz",
+ new DirectiveBlock(
+ Factory.MetaCode("Option Explicit On")
+ .With(SetVBOptionCodeGenerator.Explicit(true))));
+ }
+
+ [Fact]
+ public void ParseBlockReadsToAfterOffKeywordIfOptionExplicitBlock()
+ {
+ ParseBlockTest("Option Explicit Off Foo Bar Baz",
+ new DirectiveBlock(
+ Factory.MetaCode("Option Explicit Off")
+ .With(SetVBOptionCodeGenerator.Explicit(false))));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/VB/VBStatementTest.cs b/test/System.Web.Razor.Test/Parser/VB/VBStatementTest.cs
new file mode 100644
index 00000000..73423af5
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/VB/VBStatementTest.cs
@@ -0,0 +1,218 @@
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Test.Framework;
+using Xunit;
+
+namespace System.Web.Razor.Test.Parser.VB
+{
+ public class VBStatementTest : VBHtmlCodeParserTestBase
+ {
+ [Fact]
+ public void VB_Inherits_Statement()
+ {
+ ParseBlockTest(@"@Inherits System.Foo.Bar(Of Baz)",
+ new DirectiveBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("Inherits ").Accepts(AcceptedCharacters.None),
+ Factory.Code("System.Foo.Bar(Of Baz)")
+ .AsBaseType("System.Foo.Bar(Of Baz)")));
+ }
+
+ [Fact]
+ public void InheritsDirectiveSupportsArrays()
+ {
+ ParseBlockTest("@Inherits System.String(())()",
+ new DirectiveBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("Inherits ")
+ .Accepts(AcceptedCharacters.None),
+ Factory.Code("System.String(())()")
+ .AsBaseType("System.String(())()")));
+ }
+
+ [Fact]
+ public void InheritsDirectiveSupportsNestedGenerics()
+ {
+ ParseBlockTest("@Inherits System.Web.Mvc.WebViewPage(Of IEnumerable(Of MvcApplication2.Models.RegisterModel))",
+ new DirectiveBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("Inherits ")
+ .Accepts(AcceptedCharacters.None),
+ Factory.Code("System.Web.Mvc.WebViewPage(Of IEnumerable(Of MvcApplication2.Models.RegisterModel))")
+ .AsBaseType("System.Web.Mvc.WebViewPage(Of IEnumerable(Of MvcApplication2.Models.RegisterModel))")));
+ }
+
+ [Fact]
+ public void InheritsDirectiveSupportsTypeKeywords()
+ {
+ ParseBlockTest("@Inherits String",
+ new DirectiveBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("Inherits ").Accepts(AcceptedCharacters.None),
+ Factory.Code("String").AsBaseType("String")));
+ }
+
+ [Fact]
+ public void VB_Option_Strict_Statement()
+ {
+ ParseBlockTest(@"@Option Strict Off",
+ new DirectiveBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("Option Strict Off")
+ .With(SetVBOptionCodeGenerator.Strict(false))));
+ }
+
+ [Fact]
+ public void VB_Option_Explicit_Statement()
+ {
+ ParseBlockTest(@"@Option Explicit Off",
+ new DirectiveBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("Option Explicit Off")
+ .With(SetVBOptionCodeGenerator.Explicit(false))));
+ }
+
+ [Fact]
+ public void VB_Imports_Statement()
+ {
+ ParseBlockTest("@Imports Biz = System.Foo.Bar(Of Boz.Baz(Of Qux))",
+ new DirectiveBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode("Imports Biz = System.Foo.Bar(Of Boz.Baz(Of Qux))")
+ .With(new AddImportCodeGenerator(
+ ns: " Biz = System.Foo.Bar(Of Boz.Baz(Of Qux))",
+ namespaceKeywordLength: SyntaxConstants.VB.ImportsKeywordLength))));
+ }
+
+ [Fact]
+ public void VB_Using_Statement()
+ {
+ ParseBlockTest(@"@Using foo as Bar
+ foo()
+End Using",
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("Using foo as Bar\r\n foo()\r\nEnd Using")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void VB_Do_Loop_Statement()
+ {
+ ParseBlockTest(@"@Do
+ foo()
+Loop While True",
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("Do\r\n foo()\r\nLoop While True")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.AnyExceptNewline)));
+ }
+
+ [Fact]
+ public void VB_While_Statement()
+ {
+ ParseBlockTest(@"@While True
+ foo()
+End While",
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("While True\r\n foo()\r\nEnd While")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void VB_If_Statement()
+ {
+ ParseBlockTest(@"@If True Then
+ foo()
+ElseIf False Then
+ bar()
+Else
+ baz()
+End If",
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("If True Then\r\n foo()\r\nElseIf False Then\r\n bar()\r\nElse\r\n baz()\r\nEnd If")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void VB_Select_Statement()
+ {
+ ParseBlockTest(@"@Select Case foo
+ Case 1
+ foo()
+ Case 2
+ bar()
+ Case Else
+ baz()
+End Select",
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("Select Case foo\r\n Case 1\r\n foo()\r\n Case 2\r\n bar()\r\n Case Else\r\n baz()\r\nEnd Select")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void VB_For_Statement()
+ {
+ ParseBlockTest(@"@For Each foo In bar
+ baz()
+Next",
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("For Each foo In bar\r\n baz()\r\nNext")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.AnyExceptNewline)));
+ }
+
+ [Fact]
+ public void VB_Try_Statement()
+ {
+ ParseBlockTest(@"@Try
+ foo()
+Catch ex as Exception
+ bar()
+Finally
+ baz()
+End Try",
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("Try\r\n foo()\r\nCatch ex as Exception\r\n bar()\r\nFinally\r\n baz()\r\nEnd Try")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void VB_With_Statement()
+ {
+ ParseBlockTest(@"@With foo
+ .bar()
+End With",
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("With foo\r\n .bar()\r\nEnd With")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void VB_SyncLock_Statement()
+ {
+ ParseBlockTest(@"@SyncLock foo
+ foo.bar()
+End SyncLock",
+ new StatementBlock(
+ Factory.CodeTransition(),
+ Factory.Code("SyncLock foo\r\n foo.bar()\r\nEnd SyncLock")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.None)));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/VB/VBTemplateTest.cs b/test/System.Web.Razor.Test/Parser/VB/VBTemplateTest.cs
new file mode 100644
index 00000000..76992485
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/VB/VBTemplateTest.cs
@@ -0,0 +1,211 @@
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Test.Framework;
+using Xunit;
+
+namespace System.Web.Razor.Test.Parser.VB
+{
+ public class VBTemplateTest : VBHtmlCodeParserTestBase
+ {
+ private const string TestTemplateCode = "@@<p>Foo #@item</p>";
+
+ private TemplateBlock TestTemplate()
+ {
+ return new TemplateBlock(new TemplateBlockCodeGenerator(),
+ new MarkupBlock(
+ Factory.MarkupTransition(),
+ Factory.MetaMarkup("@"),
+ Factory.Markup("<p>Foo #"),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("item")
+ .AsImplicitExpression(VBCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ Factory.Markup("</p>").Accepts(AcceptedCharacters.None)));
+ }
+
+ private const string TestNestedTemplateCode = "@@<p>Foo #@Html.Repeat(10,@@<p>@item</p>)</p>";
+
+ private TemplateBlock TestNestedTemplate()
+ {
+ return new TemplateBlock(new TemplateBlockCodeGenerator(),
+ new MarkupBlock(
+ Factory.MarkupTransition(),
+ Factory.MetaMarkup("@"),
+ Factory.Markup("<p>Foo #"),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("Html.Repeat(10,")
+ .AsImplicitExpression(VBCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.Any),
+ new TemplateBlock(new TemplateBlockCodeGenerator(),
+ new MarkupBlock(
+ Factory.MarkupTransition(),
+ Factory.MetaMarkup("@"),
+ Factory.Markup("<p>"),
+ new ExpressionBlock(
+ Factory.CodeTransition(),
+ Factory.Code("item")
+ .AsImplicitExpression(VBCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ Factory.Markup("</p>").Accepts(AcceptedCharacters.None))),
+ Factory.Code(")")
+ .AsImplicitExpression(VBCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharacters.NonWhiteSpace)),
+ Factory.Markup("</p>").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void ParseBlockHandlesSimpleAnonymousSectionInExplicitExpressionParens()
+ {
+ ParseBlockTest("(Html.Repeat(10," + TestTemplateCode + "))",
+ new ExpressionBlock(
+ Factory.MetaCode("(").Accepts(AcceptedCharacters.None),
+ Factory.Code("Html.Repeat(10,").AsExpression(),
+ TestTemplate(),
+ Factory.Code(")").AsExpression(),
+ Factory.MetaCode(")").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void ParseBlockHandlesSimpleAnonymousSectionInImplicitExpressionParens()
+ {
+ ParseBlockTest("Html.Repeat(10," + TestTemplateCode + ")",
+ new ExpressionBlock(
+ Factory.Code("Html.Repeat(10,").AsImplicitExpression(KeywordSet),
+ TestTemplate(),
+ Factory.Code(")").AsImplicitExpression(KeywordSet).Accepts(AcceptedCharacters.NonWhiteSpace)));
+ }
+
+ [Fact]
+ public void ParseBlockHandlesTwoAnonymousSectionsInImplicitExpressionParens()
+ {
+ ParseBlockTest("Html.Repeat(10," + TestTemplateCode + "," + TestTemplateCode + ")",
+ new ExpressionBlock(
+ Factory.Code("Html.Repeat(10,").AsImplicitExpression(KeywordSet),
+ TestTemplate(),
+ Factory.Code(",").AsImplicitExpression(KeywordSet),
+ TestTemplate(),
+ Factory.Code(")").AsImplicitExpression(KeywordSet).Accepts(AcceptedCharacters.NonWhiteSpace)));
+ }
+
+ [Fact]
+ public void ParseBlockProducesErrorButCorrectlyParsesNestedAnonymousSectionInImplicitExpressionParens()
+ {
+ ParseBlockTest("Html.Repeat(10," + TestNestedTemplateCode + ")",
+ new ExpressionBlock(
+ Factory.Code("Html.Repeat(10,").AsImplicitExpression(KeywordSet),
+ TestNestedTemplate(),
+ Factory.Code(")").AsImplicitExpression(KeywordSet).Accepts(AcceptedCharacters.NonWhiteSpace)),
+ GetNestedSectionError(41, 0, 41));
+ }
+
+ [Fact]
+ public void ParseBlockHandlesSimpleAnonymousSectionInStatementWithinCodeBlock()
+ {
+ ParseBlockTest(@"For Each foo in Bar
+ Html.ExecuteTemplate(foo," + TestTemplateCode + @")
+Next foo",
+ new StatementBlock(
+ Factory.Code("For Each foo in Bar \r\n Html.ExecuteTemplate(foo,")
+ .AsStatement(),
+ TestTemplate(),
+ Factory.Code(")\r\nNext foo")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.WhiteSpace | AcceptedCharacters.NonWhiteSpace)));
+ }
+
+ [Fact]
+ public void ParseBlockHandlesTwoAnonymousSectionsInStatementWithinCodeBlock()
+ {
+ ParseBlockTest(@"For Each foo in Bar
+ Html.ExecuteTemplate(foo," + TestTemplateCode + "," + TestTemplateCode + @")
+Next foo",
+ new StatementBlock(
+ Factory.Code("For Each foo in Bar \r\n Html.ExecuteTemplate(foo,")
+ .AsStatement(),
+ TestTemplate(),
+ Factory.Code(",").AsStatement(),
+ TestTemplate(),
+ Factory.Code(")\r\nNext foo")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.WhiteSpace | AcceptedCharacters.NonWhiteSpace)));
+ }
+
+ [Fact]
+ public void ParseBlockProducesErrorButCorrectlyParsesNestedAnonymousSectionInStatementWithinCodeBlock()
+ {
+ ParseBlockTest(@"For Each foo in Bar
+ Html.ExecuteTemplate(foo," + TestNestedTemplateCode + @")
+Next foo",
+ new StatementBlock(
+ Factory.Code("For Each foo in Bar \r\n Html.ExecuteTemplate(foo,")
+ .AsStatement(),
+ TestNestedTemplate(),
+ Factory.Code(")\r\nNext foo")
+ .AsStatement()
+ .Accepts(AcceptedCharacters.WhiteSpace | AcceptedCharacters.NonWhiteSpace)),
+ GetNestedSectionError(77, 1, 55));
+ }
+
+ [Fact]
+ public void ParseBlockHandlesSimpleAnonymousSectionInStatementWithinStatementBlock()
+ {
+ ParseBlockTest(@"Code
+ Dim foo = bar
+ Html.ExecuteTemplate(foo," + TestTemplateCode + @")
+End Code",
+ new StatementBlock(
+ Factory.MetaCode("Code").Accepts(AcceptedCharacters.None),
+ Factory.Code(" \r\n Dim foo = bar\r\n Html.ExecuteTemplate(foo,")
+ .AsStatement(),
+ TestTemplate(),
+ Factory.Code(")\r\n").AsStatement(),
+ Factory.MetaCode("End Code").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void ParseBlockHandlessTwoAnonymousSectionsInStatementWithinStatementBlock()
+ {
+ ParseBlockTest(@"Code
+ Dim foo = bar
+ Html.ExecuteTemplate(foo," + TestTemplateCode + "," + TestTemplateCode + @")
+End Code",
+ new StatementBlock(
+ Factory.MetaCode("Code").Accepts(AcceptedCharacters.None),
+ Factory.Code("\r\n Dim foo = bar\r\n Html.ExecuteTemplate(foo,")
+ .AsStatement(),
+ TestTemplate(),
+ Factory.Code(",").AsStatement(),
+ TestTemplate(),
+ Factory.Code(")\r\n").AsStatement(),
+ Factory.MetaCode("End Code").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void ParseBlockProducesErrorButCorrectlyParsesNestedAnonymousSectionInStatementWithinStatementBlock()
+ {
+ ParseBlockTest(@"Code
+ Dim foo = bar
+ Html.ExecuteTemplate(foo," + TestNestedTemplateCode + @")
+End Code",
+ new StatementBlock(
+ Factory.MetaCode("Code").Accepts(AcceptedCharacters.None),
+ Factory.Code("\r\n Dim foo = bar\r\n Html.ExecuteTemplate(foo,")
+ .AsStatement(),
+ TestNestedTemplate(),
+ Factory.Code(")\r\n").AsStatement(),
+ Factory.MetaCode("End Code").Accepts(AcceptedCharacters.None)),
+ GetNestedSectionError(80, 2, 55));
+ }
+
+ private static RazorError GetNestedSectionError(int absoluteIndex, int lineIndex, int characterIndex)
+ {
+ return new RazorError(
+ RazorResources.ParseError_InlineMarkup_Blocks_Cannot_Be_Nested,
+ absoluteIndex, lineIndex, characterIndex);
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/VB/VBToMarkupSwitchTest.cs b/test/System.Web.Razor.Test/Parser/VB/VBToMarkupSwitchTest.cs
new file mode 100644
index 00000000..b8568626
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/VB/VBToMarkupSwitchTest.cs
@@ -0,0 +1,103 @@
+using System.Web.Razor.Editor;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Test.Framework;
+using System.Web.Razor.Tokenizer.Symbols;
+using Xunit;
+using Xunit.Extensions;
+
+namespace System.Web.Razor.Test.Parser.VB
+{
+ public class VBToMarkupSwitchTest : VBHtmlCodeParserTestBase
+ {
+ [Fact]
+ public void ParseBlockSwitchesToMarkupWhenAtSignFollowedByLessThanInStatementBlock()
+ {
+ ParseBlockTest(@"Code
+ If True Then
+ @<p>It's True!</p>
+ End If
+End Code",
+ new StatementBlock(
+ Factory.MetaCode("Code").Accepts(AcceptedCharacters.None),
+ Factory.Code("\r\n If True Then\r\n").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" "),
+ Factory.MarkupTransition(),
+ Factory.Markup("<p>It's True!</p>\r\n").Accepts(AcceptedCharacters.None)),
+ Factory.Code(" End If\r\n").AsStatement(),
+ Factory.MetaCode("End Code").Accepts(AcceptedCharacters.None)));
+ }
+
+ [Fact]
+ public void ParseBlockGivesWhiteSpacePreceedingMarkupBlockToCodeInDesignTimeMode()
+ {
+ ParseBlockTest(@"Code
+ @<p>Foo</p>
+End Code",
+ new StatementBlock(
+ Factory.MetaCode("Code").Accepts(AcceptedCharacters.None),
+ Factory.Code("\r\n ").AsStatement(),
+ new MarkupBlock(
+ Factory.MarkupTransition(),
+ Factory.Markup("<p>Foo</p>").Accepts(AcceptedCharacters.None)),
+ Factory.Code("\r\n").AsStatement(),
+ Factory.MetaCode("End Code").Accepts(AcceptedCharacters.None)),
+ designTimeParser: true);
+ }
+
+ [Theory]
+ [InlineData("While", "End While", AcceptedCharacters.None)]
+ [InlineData("If", "End If", AcceptedCharacters.None)]
+ [InlineData("Select", "End Select", AcceptedCharacters.None)]
+ [InlineData("For", "Next", AcceptedCharacters.WhiteSpace | AcceptedCharacters.NonWhiteSpace)]
+ [InlineData("Try", "End Try", AcceptedCharacters.None)]
+ [InlineData("With", "End With", AcceptedCharacters.None)]
+ [InlineData("Using", "End Using", AcceptedCharacters.None)]
+ public void SimpleMarkupSwitch(string keyword, string endSequence, AcceptedCharacters acceptedCharacters)
+ {
+ ParseBlockTest(keyword + @"
+ If True Then
+ @<p>It's True!</p>
+ End If
+" + endSequence,
+ new StatementBlock(
+ Factory.Code(keyword + "\r\n If True Then\r\n").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" "),
+ Factory.MarkupTransition(),
+ Factory.Markup("<p>It's True!</p>\r\n").Accepts(AcceptedCharacters.None)),
+ Factory.Code(" End If\r\n" + endSequence).AsStatement().Accepts(acceptedCharacters)));
+ }
+
+ [Theory]
+ [InlineData("While", "End While", AcceptedCharacters.None)]
+ [InlineData("If", "End If", AcceptedCharacters.None)]
+ [InlineData("Select", "End Select", AcceptedCharacters.None)]
+ [InlineData("For", "Next", AcceptedCharacters.WhiteSpace | AcceptedCharacters.NonWhiteSpace)]
+ [InlineData("Try", "End Try", AcceptedCharacters.None)]
+ [InlineData("With", "End With", AcceptedCharacters.None)]
+ [InlineData("Using", "End Using", AcceptedCharacters.None)]
+ public void SingleLineMarkupSwitch(string keyword, string endSequence, AcceptedCharacters acceptedCharacters)
+ {
+ ParseBlockTest(keyword + @"
+ If True Then
+ @:<p>It's True!</p>
+ This is code!
+ End If
+" + endSequence,
+ new StatementBlock(
+ Factory.Code(keyword + "\r\n If True Then\r\n").AsStatement(),
+ new MarkupBlock(
+ Factory.Markup(" "),
+ Factory.MarkupTransition(),
+ Factory.MetaMarkup(":", HtmlSymbolType.Colon),
+ Factory.Markup("<p>It's True!</p>\r\n")
+ .With(new SingleLineMarkupEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString))
+ .Accepts(AcceptedCharacters.None)),
+ Factory.Code(" This is code!\r\n End If\r\n" + endSequence)
+ .AsStatement()
+ .Accepts(acceptedCharacters)));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Parser/WhitespaceRewriterTest.cs b/test/System.Web.Razor.Test/Parser/WhitespaceRewriterTest.cs
new file mode 100644
index 00000000..146312a4
--- /dev/null
+++ b/test/System.Web.Razor.Test/Parser/WhitespaceRewriterTest.cs
@@ -0,0 +1,50 @@
+using System.Web.Razor.Parser;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Test.Framework;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Razor.Test.Parser
+{
+ public class WhitespaceRewriterTest
+ {
+ [Fact]
+ public void Constructor_Requires_NonNull_SymbolConverter()
+ {
+ Assert.ThrowsArgumentNull(() => new WhiteSpaceRewriter(null), "markupSpanFactory");
+ }
+
+ [Fact]
+ public void Rewrite_Moves_Whitespace_Preceeding_ExpressionBlock_To_Parent_Block()
+ {
+ // Arrange
+ var factory = SpanFactory.CreateCsHtml();
+ Block start = new MarkupBlock(
+ factory.Markup("test"),
+ new ExpressionBlock(
+ factory.Code(" ").AsExpression(),
+ factory.CodeTransition(SyntaxConstants.TransitionString),
+ factory.Code("foo").AsExpression()
+ ),
+ factory.Markup("test")
+ );
+ WhiteSpaceRewriter rewriter = new WhiteSpaceRewriter(new HtmlMarkupParser().BuildSpan);
+
+ // Act
+ Block actual = rewriter.Rewrite(start);
+
+ factory.Reset();
+
+ // Assert
+ ParserTestBase.EvaluateParseTree(actual, new MarkupBlock(
+ factory.Markup("test"),
+ factory.Markup(" "),
+ new ExpressionBlock(
+ factory.CodeTransition(SyntaxConstants.TransitionString),
+ factory.Code("foo").AsExpression()
+ ),
+ factory.Markup("test")
+ ));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Properties/AssemblyInfo.cs b/test/System.Web.Razor.Test/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..7256e213
--- /dev/null
+++ b/test/System.Web.Razor.Test/Properties/AssemblyInfo.cs
@@ -0,0 +1,34 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+
+[assembly: AssemblyTitle("System.Web.Razor.Test")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("Microsoft")]
+[assembly: AssemblyProduct("System.Web.Razor.Test")]
+[assembly: AssemblyCopyright("Copyright © Microsoft 2009")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM componenets. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+
+[assembly: ComVisible(false)]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Revision and Build Numbers
+// by using the '*' as shown below:
+
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/test/System.Web.Razor.Test/RazorCodeLanguageTest.cs b/test/System.Web.Razor.Test/RazorCodeLanguageTest.cs
new file mode 100644
index 00000000..2dab4b84
--- /dev/null
+++ b/test/System.Web.Razor.Test/RazorCodeLanguageTest.cs
@@ -0,0 +1,47 @@
+using Xunit;
+
+namespace System.Web.Razor.Test
+{
+ public class RazorCodeLanguageTest
+ {
+ [Fact]
+ public void ServicesPropertyContainsEntriesForCSharpCodeLanguageService()
+ {
+ // Assert
+ Assert.Equal(2, RazorCodeLanguage.Languages.Count);
+ Assert.IsType<CSharpRazorCodeLanguage>(RazorCodeLanguage.Languages["cshtml"]);
+ Assert.IsType<VBRazorCodeLanguage>(RazorCodeLanguage.Languages["vbhtml"]);
+ }
+
+ [Fact]
+ public void GetServiceByExtensionReturnsEntryMatchingExtensionWithoutPreceedingDot()
+ {
+ Assert.IsType<CSharpRazorCodeLanguage>(RazorCodeLanguage.GetLanguageByExtension("cshtml"));
+ }
+
+ [Fact]
+ public void GetServiceByExtensionReturnsEntryMatchingExtensionWithPreceedingDot()
+ {
+ Assert.IsType<CSharpRazorCodeLanguage>(RazorCodeLanguage.GetLanguageByExtension(".cshtml"));
+ }
+
+ [Fact]
+ public void GetServiceByExtensionReturnsNullIfNoServiceForSpecifiedExtension()
+ {
+ Assert.Null(RazorCodeLanguage.GetLanguageByExtension("foobar"));
+ }
+
+ [Fact]
+ public void MultipleCallsToGetServiceWithSameExtensionReturnSameObject()
+ {
+ // Arrange
+ RazorCodeLanguage expected = RazorCodeLanguage.GetLanguageByExtension("cshtml");
+
+ // Act
+ RazorCodeLanguage actual = RazorCodeLanguage.GetLanguageByExtension("cshtml");
+
+ // Assert
+ Assert.Same(expected, actual);
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/RazorDirectiveAttributeTest.cs b/test/System.Web.Razor.Test/RazorDirectiveAttributeTest.cs
new file mode 100644
index 00000000..d26ec878
--- /dev/null
+++ b/test/System.Web.Razor.Test/RazorDirectiveAttributeTest.cs
@@ -0,0 +1,79 @@
+using System.Linq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Razor.Test
+{
+ public class RazorDirectiveAttributeTest
+ {
+ [Fact]
+ public void ConstructorThrowsIfNameIsNullOrEmpty()
+ {
+ // Act and Assert
+ Assert.ThrowsArgumentNullOrEmptyString(() => new RazorDirectiveAttribute(name: null, value: "blah"), "name");
+ Assert.ThrowsArgumentNullOrEmptyString(() => new RazorDirectiveAttribute(name: "", value: "blah"), "name");
+ }
+
+ [Fact]
+ public void EnsureRazorDirectiveProperties()
+ {
+ // Arrange
+ var attribute = (AttributeUsageAttribute)typeof(RazorDirectiveAttribute).GetCustomAttributes(typeof(AttributeUsageAttribute), inherit: false)
+ .SingleOrDefault();
+
+ // Assert
+ Assert.True(attribute.AllowMultiple);
+ Assert.True(attribute.ValidOn == AttributeTargets.Class);
+ Assert.True(attribute.Inherited);
+ }
+
+ [Fact]
+ public void EqualsAndGetHashCodeIgnoresCase()
+ {
+ // Arrange
+ var attribute1 = new RazorDirectiveAttribute("foo", "bar");
+ var attribute2 = new RazorDirectiveAttribute("fOo", "BAr");
+
+ // Act
+ var hashCode1 = attribute1.GetHashCode();
+ var hashCode2 = attribute2.GetHashCode();
+
+ // Assert
+ Assert.Equal(attribute1, attribute2);
+ Assert.Equal(hashCode1, hashCode2);
+ }
+
+ [Fact]
+ public void EqualsAndGetHashCodeDoNotThrowIfValueIsNullOrEmpty()
+ {
+ // Arrange
+ var attribute1 = new RazorDirectiveAttribute("foo", null);
+ var attribute2 = new RazorDirectiveAttribute("foo", "BAr");
+
+ // Act
+ bool result = attribute1.Equals(attribute2);
+ var hashCode = attribute1.GetHashCode();
+
+ // Assert
+ Assert.False(result);
+ // If we've got this far, GetHashCode did not throw
+ }
+
+ [Fact]
+ public void EqualsAndGetHashCodeReturnDifferentValuesForNullAndEmpty()
+ {
+ // Arrange
+ var attribute1 = new RazorDirectiveAttribute("foo", null);
+ var attribute2 = new RazorDirectiveAttribute("foo", "");
+
+ // Act
+ bool result = attribute1.Equals(attribute2);
+ var hashCode1 = attribute1.GetHashCode();
+ var hashCode2 = attribute2.GetHashCode();
+
+ // Assert
+ Assert.False(result);
+ Assert.NotEqual(hashCode1, hashCode2);
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/RazorEngineHostTest.cs b/test/System.Web.Razor.Test/RazorEngineHostTest.cs
new file mode 100644
index 00000000..1385fec8
--- /dev/null
+++ b/test/System.Web.Razor.Test/RazorEngineHostTest.cs
@@ -0,0 +1,186 @@
+using System.CodeDom;
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Razor.Test
+{
+ public class RazorEngineHostTest
+ {
+ [Fact]
+ public void ConstructorRequiresNonNullCodeLanguage()
+ {
+ Assert.ThrowsArgumentNull(() => new RazorEngineHost(null), "codeLanguage");
+ Assert.ThrowsArgumentNull(() => new RazorEngineHost(null, () => new HtmlMarkupParser()), "codeLanguage");
+ }
+
+ [Fact]
+ public void ConstructorRequiresNonNullMarkupParser()
+ {
+ Assert.ThrowsArgumentNull(() => new RazorEngineHost(new CSharpRazorCodeLanguage(), null), "markupParserFactory");
+ }
+
+ [Fact]
+ public void ConstructorWithCodeLanguageSetsPropertiesAppropriately()
+ {
+ // Arrange
+ RazorCodeLanguage language = new CSharpRazorCodeLanguage();
+
+ // Act
+ RazorEngineHost host = new RazorEngineHost(language);
+
+ // Assert
+ VerifyCommonDefaults(host);
+ Assert.Same(language, host.CodeLanguage);
+ Assert.IsType<HtmlMarkupParser>(host.CreateMarkupParser());
+ }
+
+ [Fact]
+ public void ConstructorWithCodeLanguageAndMarkupParserSetsPropertiesAppropriately()
+ {
+ // Arrange
+ RazorCodeLanguage language = new CSharpRazorCodeLanguage();
+ ParserBase expected = new HtmlMarkupParser();
+
+ // Act
+ RazorEngineHost host = new RazorEngineHost(language, () => expected);
+
+ // Assert
+ VerifyCommonDefaults(host);
+ Assert.Same(language, host.CodeLanguage);
+ Assert.Same(expected, host.CreateMarkupParser());
+ }
+
+ [Fact]
+ public void DecorateCodeParserRequiresNonNullCodeParser()
+ {
+ Assert.ThrowsArgumentNull(() => CreateHost().DecorateCodeParser(null), "incomingCodeParser");
+ }
+
+ [Fact]
+ public void DecorateMarkupParserRequiresNonNullMarkupParser()
+ {
+ Assert.ThrowsArgumentNull(() => CreateHost().DecorateMarkupParser(null), "incomingMarkupParser");
+ }
+
+ [Fact]
+ public void DecorateCodeGeneratorRequiresNonNullCodeGenerator()
+ {
+ Assert.ThrowsArgumentNull(() => CreateHost().DecorateCodeGenerator(null), "incomingCodeGenerator");
+ }
+
+ [Fact]
+ public void PostProcessGeneratedCodeRequiresNonNullCompileUnit()
+ {
+ Assert.ThrowsArgumentNull(() => CreateHost().PostProcessGeneratedCode(codeCompileUnit: null,
+ generatedNamespace: new CodeNamespace(),
+ generatedClass: new CodeTypeDeclaration(),
+ executeMethod: new CodeMemberMethod()),
+ "codeCompileUnit");
+ }
+
+ [Fact]
+ public void PostProcessGeneratedCodeRequiresNonNullGeneratedNamespace()
+ {
+ Assert.ThrowsArgumentNull(() => CreateHost().PostProcessGeneratedCode(codeCompileUnit: new CodeCompileUnit(),
+ generatedNamespace: null,
+ generatedClass: new CodeTypeDeclaration(),
+ executeMethod: new CodeMemberMethod()),
+ "generatedNamespace");
+ }
+
+ [Fact]
+ public void PostProcessGeneratedCodeRequiresNonNullGeneratedClass()
+ {
+ Assert.ThrowsArgumentNull(() => CreateHost().PostProcessGeneratedCode(codeCompileUnit: new CodeCompileUnit(),
+ generatedNamespace: new CodeNamespace(),
+ generatedClass: null,
+ executeMethod: new CodeMemberMethod()),
+ "generatedClass");
+ }
+
+ [Fact]
+ public void PostProcessGeneratedCodeRequiresNonNullExecuteMethod()
+ {
+ Assert.ThrowsArgumentNull(() => CreateHost().PostProcessGeneratedCode(codeCompileUnit: new CodeCompileUnit(),
+ generatedNamespace: new CodeNamespace(),
+ generatedClass: new CodeTypeDeclaration(),
+ executeMethod: null),
+ "executeMethod");
+ }
+
+ [Fact]
+ public void DecorateCodeParserDoesNotModifyIncomingParser()
+ {
+ // Arrange
+ ParserBase expected = new CSharpCodeParser();
+
+ // Act
+ ParserBase actual = CreateHost().DecorateCodeParser(expected);
+
+ // Assert
+ Assert.Same(expected, actual);
+ }
+
+ [Fact]
+ public void DecorateMarkupParserReturnsIncomingParser()
+ {
+ // Arrange
+ ParserBase expected = new HtmlMarkupParser();
+
+ // Act
+ ParserBase actual = CreateHost().DecorateMarkupParser(expected);
+
+ // Assert
+ Assert.Same(expected, actual);
+ }
+
+ [Fact]
+ public void DecorateCodeGeneratorReturnsIncomingCodeGenerator()
+ {
+ // Arrange
+ RazorCodeGenerator expected = new CSharpRazorCodeGenerator("Foo", "Bar", "Baz", CreateHost());
+
+ // Act
+ RazorCodeGenerator actual = CreateHost().DecorateCodeGenerator(expected);
+
+ // Assert
+ Assert.Same(expected, actual);
+ }
+
+ [Fact]
+ public void PostProcessGeneratedCodeDoesNotModifyCode()
+ {
+ // Arrange
+ CodeCompileUnit compileUnit = new CodeCompileUnit();
+ CodeNamespace ns = new CodeNamespace();
+ CodeTypeDeclaration typeDecl = new CodeTypeDeclaration();
+ CodeMemberMethod execMethod = new CodeMemberMethod();
+
+ // Act
+ CreateHost().PostProcessGeneratedCode(compileUnit, ns, typeDecl, execMethod);
+
+ // Assert
+ Assert.Empty(compileUnit.Namespaces);
+ Assert.Empty(ns.Imports);
+ Assert.Empty(ns.Types);
+ Assert.Empty(typeDecl.Members);
+ Assert.Empty(execMethod.Statements);
+ }
+
+ private static RazorEngineHost CreateHost()
+ {
+ return new RazorEngineHost(new CSharpRazorCodeLanguage());
+ }
+
+ private static void VerifyCommonDefaults(RazorEngineHost host)
+ {
+ Assert.Equal(GeneratedClassContext.Default, host.GeneratedClassContext);
+ Assert.Empty(host.NamespaceImports);
+ Assert.False(host.DesignTimeMode);
+ Assert.Equal(RazorEngineHost.InternalDefaultClassName, host.DefaultClassName);
+ Assert.Equal(RazorEngineHost.InternalDefaultNamespace, host.DefaultNamespace);
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/RazorTemplateEngineTest.cs b/test/System.Web.Razor.Test/RazorTemplateEngineTest.cs
new file mode 100644
index 00000000..1f3d4fff
--- /dev/null
+++ b/test/System.Web.Razor.Test/RazorTemplateEngineTest.cs
@@ -0,0 +1,197 @@
+using System.IO;
+using System.Threading;
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser;
+using System.Web.Razor.Text;
+using System.Web.WebPages.TestUtils;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Razor.Test
+{
+ public class RazorTemplateEngineTest
+ {
+ [Fact]
+ public void ConstructorRequiresNonNullHost()
+ {
+ Assert.ThrowsArgumentNull(() => new RazorTemplateEngine(null), "host");
+ }
+
+ [Fact]
+ public void ConstructorInitializesHost()
+ {
+ // Arrange
+ RazorEngineHost host = new RazorEngineHost(new CSharpRazorCodeLanguage());
+
+ // Act
+ RazorTemplateEngine engine = new RazorTemplateEngine(host);
+
+ // Assert
+ Assert.Same(host, engine.Host);
+ }
+
+ [Fact]
+ public void CreateParserMethodIsConstructedFromHost()
+ {
+ // Arrange
+ RazorEngineHost host = CreateHost();
+ RazorTemplateEngine engine = new RazorTemplateEngine(host);
+
+ // Act
+ RazorParser parser = engine.CreateParser();
+
+ // Assert
+ Assert.IsType<CSharpCodeParser>(parser.CodeParser);
+ Assert.IsType<HtmlMarkupParser>(parser.MarkupParser);
+ }
+
+ [Fact]
+ public void CreateParserMethodSetsParserContextToDesignTimeModeIfHostSetToDesignTimeMode()
+ {
+ // Arrange
+ RazorEngineHost host = CreateHost();
+ RazorTemplateEngine engine = new RazorTemplateEngine(host);
+ host.DesignTimeMode = true;
+
+ // Act
+ RazorParser parser = engine.CreateParser();
+
+ // Assert
+ Assert.True(parser.DesignTimeMode);
+ }
+
+ [Fact]
+ public void CreateParserMethodPassesParsersThroughDecoratorMethodsOnHost()
+ {
+ // Arrange
+ ParserBase expectedCode = new Mock<ParserBase>().Object;
+ ParserBase expectedMarkup = new Mock<ParserBase>().Object;
+
+ var mockHost = new Mock<RazorEngineHost>(new CSharpRazorCodeLanguage()) { CallBase = true };
+ mockHost.Setup(h => h.DecorateCodeParser(It.IsAny<CSharpCodeParser>()))
+ .Returns(expectedCode);
+ mockHost.Setup(h => h.DecorateMarkupParser(It.IsAny<HtmlMarkupParser>()))
+ .Returns(expectedMarkup);
+ RazorTemplateEngine engine = new RazorTemplateEngine(mockHost.Object);
+
+ // Act
+ RazorParser actual = engine.CreateParser();
+
+ // Assert
+ Assert.Equal(expectedCode, actual.CodeParser);
+ Assert.Equal(expectedMarkup, actual.MarkupParser);
+ }
+
+ [Fact]
+ public void CreateCodeGeneratorMethodPassesCodeGeneratorThroughDecorateMethodOnHost()
+ {
+ // Arrange
+ var mockHost = new Mock<RazorEngineHost>(new CSharpRazorCodeLanguage()) { CallBase = true };
+
+ RazorCodeGenerator expected = new Mock<RazorCodeGenerator>("Foo", "Bar", "Baz", mockHost.Object).Object;
+
+ mockHost.Setup(h => h.DecorateCodeGenerator(It.IsAny<CSharpRazorCodeGenerator>()))
+ .Returns(expected);
+ RazorTemplateEngine engine = new RazorTemplateEngine(mockHost.Object);
+
+ // Act
+ RazorCodeGenerator actual = engine.CreateCodeGenerator("Foo", "Bar", "Baz");
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void ParseTemplateCopiesTextReaderContentToSeekableTextReaderAndPassesToParseTemplateCore()
+ {
+ // Arrange
+ Mock<RazorTemplateEngine> mockEngine = new Mock<RazorTemplateEngine>(CreateHost());
+ TextReader reader = new StringReader("foo");
+ CancellationTokenSource source = new CancellationTokenSource();
+
+ // Act
+ mockEngine.Object.ParseTemplate(reader, cancelToken: source.Token);
+
+ // Assert
+ mockEngine.Verify(e => e.ParseTemplateCore(It.Is<SeekableTextReader>(l => l.ReadToEnd() == "foo"),
+ source.Token));
+ }
+
+ [Fact]
+ public void GenerateCodeCopiesTextReaderContentToSeekableTextReaderAndPassesToGenerateCodeCore()
+ {
+ // Arrange
+ Mock<RazorTemplateEngine> mockEngine = new Mock<RazorTemplateEngine>(CreateHost());
+ TextReader reader = new StringReader("foo");
+ CancellationTokenSource source = new CancellationTokenSource();
+ string className = "Foo";
+ string ns = "Bar";
+ string src = "Baz";
+
+ // Act
+ mockEngine.Object.GenerateCode(reader, className: className, rootNamespace: ns, sourceFileName: src, cancelToken: source.Token);
+
+ // Assert
+ mockEngine.Verify(e => e.GenerateCodeCore(It.Is<SeekableTextReader>(l => l.ReadToEnd() == "foo"),
+ className, ns, src, source.Token));
+ }
+
+ [Fact]
+ public void ParseTemplateOutputsResultsOfParsingProvidedTemplateSource()
+ {
+ // Arrange
+ RazorTemplateEngine engine = new RazorTemplateEngine(CreateHost());
+
+ // Act
+ ParserResults results = engine.ParseTemplate(new StringTextBuffer("foo @bar("));
+
+ // Assert
+ Assert.False(results.Success);
+ Assert.Single(results.ParserErrors);
+ Assert.NotNull(results.Document);
+ }
+
+ [Fact]
+ public void GenerateOutputsResultsOfParsingAndGeneration()
+ {
+ // Arrange
+ RazorTemplateEngine engine = new RazorTemplateEngine(CreateHost());
+
+ // Act
+ GeneratorResults results = engine.GenerateCode(new StringTextBuffer("foo @bar("));
+
+ // Assert
+ Assert.False(results.Success);
+ Assert.Single(results.ParserErrors);
+ Assert.NotNull(results.Document);
+ Assert.NotNull(results.GeneratedCode);
+ Assert.Null(results.DesignTimeLineMappings);
+ }
+
+ [Fact]
+ public void GenerateOutputsDesignTimeMappingsIfDesignTimeSetOnHost()
+ {
+ // Arrange
+ RazorTemplateEngine engine = new RazorTemplateEngine(CreateHost(designTime: true));
+
+ // Act
+ GeneratorResults results = engine.GenerateCode(new StringTextBuffer("foo @bar()"), className: null, rootNamespace: null, sourceFileName: "foo.cshtml");
+
+ // Assert
+ Assert.True(results.Success);
+ Assert.Empty(results.ParserErrors);
+ Assert.NotNull(results.Document);
+ Assert.NotNull(results.GeneratedCode);
+ Assert.NotNull(results.DesignTimeLineMappings);
+ }
+
+ private static RazorEngineHost CreateHost(bool designTime = false)
+ {
+ return new RazorEngineHost(new CSharpRazorCodeLanguage())
+ {
+ DesignTimeMode = designTime
+ };
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/StringTextBuffer.cs b/test/System.Web.Razor.Test/StringTextBuffer.cs
new file mode 100644
index 00000000..d94ddfba
--- /dev/null
+++ b/test/System.Web.Razor.Test/StringTextBuffer.cs
@@ -0,0 +1,50 @@
+using System.Web.Razor.Text;
+
+namespace System.Web.WebPages.TestUtils
+{
+ public class StringTextBuffer : ITextBuffer, IDisposable
+ {
+ private string _buffer;
+ public bool Disposed { get; set; }
+
+ public StringTextBuffer(string buffer)
+ {
+ _buffer = buffer;
+ }
+
+ public int Length
+ {
+ get { return _buffer.Length; }
+ }
+
+ public int Position { get; set; }
+
+ public int Read()
+ {
+ if (Position >= _buffer.Length)
+ {
+ return -1;
+ }
+ return _buffer[Position++];
+ }
+
+ public int Peek()
+ {
+ if (Position >= _buffer.Length)
+ {
+ return -1;
+ }
+ return _buffer[Position];
+ }
+
+ public void Dispose()
+ {
+ Disposed = true;
+ }
+
+ public object VersionToken
+ {
+ get { return _buffer; }
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/System.Web.Razor.Test.csproj b/test/System.Web.Razor.Test/System.Web.Razor.Test.csproj
new file mode 100644
index 00000000..f4dedfeb
--- /dev/null
+++ b/test/System.Web.Razor.Test/System.Web.Razor.Test.csproj
@@ -0,0 +1,464 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <!-- Temporarily disable Obsolete Warnings as Errors -->
+ <WarningsNotAsErrors>618</WarningsNotAsErrors>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProductVersion>9.0.30729</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{0BB62A1D-E6B5-49FA-9E3C-6AF679A66DFE}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>System.Web.Razor.Test</RootNamespace>
+ <AssemblyName>System.Web.Razor.Test</AssemblyName>
+ <FileAlignment>512</FileAlignment>
+ <ProjectTypeGuids>{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Debug\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ <NoWarn>0618</NoWarn>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Release\Test\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ <NoWarn>0618</NoWarn>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'CodeCoverage' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\CodeCoverage\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ <NoWarn>0618</NoWarn>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="Microsoft.CSharp" />
+ <Reference Include="Moq">
+ <HintPath>..\..\packages\Moq.4.0.10827\lib\NET40\Moq.dll</HintPath>
+ </Reference>
+ <Reference Include="System" />
+ <Reference Include="System.Core">
+ <RequiredTargetFramework>3.5</RequiredTargetFramework>
+ </Reference>
+ <Reference Include="xunit">
+ <HintPath>..\..\packages\xunit.1.9.0.1566\lib\xunit.dll</HintPath>
+ </Reference>
+ <Reference Include="xunit.extensions">
+ <HintPath>..\..\packages\xunit.extensions.1.9.0.1566\lib\xunit.extensions.dll</HintPath>
+ </Reference>
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="CodeCompileUnitExtensions.cs" />
+ <Compile Include="Framework\BlockExtensions.cs" />
+ <Compile Include="Framework\BlockTypes.cs" />
+ <Compile Include="Framework\CodeParserTestBase.cs" />
+ <Compile Include="Framework\CsHtmlCodeParserTestBase.cs" />
+ <Compile Include="Framework\CsHtmlMarkupParserTestBase.cs" />
+ <Compile Include="Framework\ErrorCollector.cs" />
+ <Compile Include="Framework\MarkupParserTestBase.cs" />
+ <Compile Include="Framework\ParserTestBase.cs" />
+ <Compile Include="Framework\RawTextSymbol.cs" />
+ <Compile Include="Framework\TestSpanBuilder.cs" />
+ <Compile Include="Framework\VBHtmlCodeParserTestBase.cs" />
+ <Compile Include="Framework\VBHtmlMarkupParserTestBase.cs" />
+ <Compile Include="Generator\GeneratedCodeMappingTest.cs" />
+ <Compile Include="Parser\CSharp\CSharpAutoCompleteTest.cs" />
+ <Compile Include="Parser\CSharp\CSharpDirectivesTest.cs" />
+ <Compile Include="Parser\CSharp\CSharpLayoutDirectiveTest.cs" />
+ <Compile Include="Parser\CSharp\CSharpNestedStatementsTest.cs" />
+ <Compile Include="Parser\CSharp\CSharpRazorCommentsTest.cs" />
+ <Compile Include="Parser\CSharp\CSharpStatementTest.cs" />
+ <Compile Include="Parser\CSharp\CSharpTemplateTest.cs" />
+ <Compile Include="Parser\CSharp\CSharpToMarkupSwitchTest.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="Parser\CSharp\CSharpVerbatimBlockTest.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="Parser\CSharp\CSharpWhitespaceHandlingTest.cs" />
+ <Compile Include="Parser\CSharp\CsHtmlDocumentTest.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="Parser\Html\HtmlAttributeTest.cs" />
+ <Compile Include="Parser\Html\HtmlUrlAttributeTest.cs" />
+ <Compile Include="Parser\ParserVisitorExtensionsTest.cs" />
+ <Compile Include="Parser\VB\VBRazorCommentsTest.cs" />
+ <Compile Include="Parser\VB\VBLayoutDirectiveTest.cs" />
+ <Compile Include="Parser\VB\VBAutoCompleteTest.cs" />
+ <Compile Include="Parser\Html\HtmlTagsTest.cs" />
+ <Compile Include="Parser\VB\VBContinueStatementTest.cs" />
+ <Compile Include="Parser\VB\VBDirectiveTest.cs" />
+ <Compile Include="Parser\VB\VBExitStatementTest.cs" />
+ <Compile Include="Parser\VB\VBExplicitExpressionTest.cs" />
+ <Compile Include="Parser\VB\VBImplicitExpressionTest.cs" />
+ <Compile Include="Parser\VB\VBReservedWordsTest.cs" />
+ <Compile Include="Parser\CSharp\CSharpReservedWordsTest.cs" />
+ <Compile Include="Parser\PartialParsing\VBPartialParsingTest.cs" />
+ <Compile Include="Parser\PartialParsing\CSharpPartialParsingTest.cs" />
+ <Compile Include="Parser\VB\VBHelperTest.cs" />
+ <Compile Include="Parser\CSharp\CSharpHelperTest.cs" />
+ <Compile Include="Generator\RazorCodeGeneratorTest.cs" />
+ <Compile Include="Parser\PartialParsing\PartialParsingTestBase.cs" />
+ <Compile Include="Parser\VB\VBNestedStatementsTest.cs" />
+ <Compile Include="Parser\VB\VBStatementTest.cs" />
+ <Compile Include="Parser\WhitespaceRewriterTest.cs" />
+ <Compile Include="RazorCodeLanguageTest.cs" />
+ <Compile Include="Parser\VB\VBHtmlDocumentTest.cs" />
+ <Compile Include="Parser\VB\VBErrorTest.cs" />
+ <Compile Include="Parser\VB\VBSectionTest.cs" />
+ <Compile Include="Parser\VB\VBTemplateTest.cs" />
+ <Compile Include="Parser\VB\VBExpressionsInCodeTest.cs" />
+ <Compile Include="Parser\VB\VBSpecialKeywordsTest.cs" />
+ <Compile Include="Parser\VB\VBBlockTest.cs" />
+ <Compile Include="Parser\VB\VBToMarkupSwitchTest.cs" />
+ <Compile Include="Editor\RazorEditorParserTest.cs" />
+ <Compile Include="RazorDirectiveAttributeTest.cs" />
+ <Compile Include="RazorEngineHostTest.cs" />
+ <Compile Include="RazorTemplateEngineTest.cs" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Output\Blocks.cs" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Output\CodeBlock.cs" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Output\CodeBlockAtEOF.cs" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Output\Comments.cs" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Output\ConditionalAttributes.cs" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Output\DesignTime.cs" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Output\EmptyCodeBlock.cs" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Output\EmptyExplicitExpression.cs" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Output\EmptyImplicitExpression.cs" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Output\EmptyImplicitExpressionInCode.cs" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Output\ExplicitExpression.cs" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Output\ExplicitExpressionAtEOF.cs" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Output\ExpressionsInCode.cs" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Output\FunctionsBlock.cs" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Output\FunctionsBlock.DesignTime.cs" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Output\Helpers.cs" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Output\Helpers.Instance.cs" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Output\HelpersMissingCloseParen.cs" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Output\HelpersMissingName.cs" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Output\HelpersMissingOpenBrace.cs" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Output\HelpersMissingOpenParen.cs" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Output\HiddenSpansInCode.cs" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Output\ImplicitExpression.cs" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Output\ImplicitExpressionAtEOF.cs" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Output\Imports.cs" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Output\Imports.DesignTime.cs" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Output\Inherits.Designtime.cs" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Output\Inherits.Runtime.cs" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Output\InlineBlocks.cs" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Output\Instrumented.cs" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Output\LayoutDirective.cs" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Output\MarkupInCodeBlock.cs" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Output\NestedCodeBlocks.cs" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Output\NestedHelpers.cs" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Output\NoLinePragmas.cs" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Output\ParserError.cs" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Output\RazorComments.cs" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Output\ResolveUrl.cs" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Output\Sections.cs" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Output\Templates.cs" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Output\UnfinishedExpressionInCode.cs" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Output\RazorComments.DesignTime.cs" />
+ <Compile Include="StringTextBuffer.cs" />
+ <Compile Include="Text\LineTrackingStringBufferTest.cs" />
+ <Compile Include="Tokenizer\VBTokenizerLiteralTest.cs" />
+ <Compile Include="Tokenizer\VBTokenizerIdentifierTest.cs" />
+ <Compile Include="Tokenizer\VBTokenizerCommentTest.cs" />
+ <Compile Include="Tokenizer\VBTokenizerOperatorsTest.cs" />
+ <Compile Include="Tokenizer\VBTokenizerTestBase.cs" />
+ <Compile Include="Tokenizer\VBTokenizerTest.cs" />
+ <Compile Include="Tokenizer\CSharpTokenizerCommentTest.cs" />
+ <Compile Include="Tokenizer\CSharpTokenizerLiteralTest.cs" />
+ <Compile Include="Tokenizer\CSharpTokenizerIdentifierTest.cs" />
+ <Compile Include="Tokenizer\CSharpTokenizerOperatorsTest.cs" />
+ <Compile Include="Tokenizer\CSharpTokenizerTestBase.cs" />
+ <Compile Include="Tokenizer\CSharpTokenizerTest.cs" />
+ <Compile Include="Tokenizer\TokenizerLookaheadTest.cs" />
+ <Compile Include="Tokenizer\HtmlTokenizerTest.cs" />
+ <Compile Include="Tokenizer\HtmlTokenizerTestBase.cs" />
+ <Compile Include="Tokenizer\TokenizerTestBase.cs" />
+ <Compile Include="Utils\MiscUtils.cs" />
+ <Compile Include="Generator\VBRazorCodeGeneratorTest.cs" />
+ <Compile Include="Generator\CSharpRazorCodeGeneratorTest.cs" />
+ <Compile Include="CSharpRazorCodeLanguageTest.cs" />
+ <Compile Include="Parser\CallbackParserListenerTest.cs" />
+ <Compile Include="Parser\CSharp\CSharpExplicitExpressionTest.cs" />
+ <Compile Include="Parser\CSharp\CSharpSectionTest.cs" />
+ <Compile Include="Parser\BlockTest.cs" />
+ <Compile Include="Parser\VB\VBExpressionTest.cs" />
+ <Compile Include="Text\BufferingTextReaderTest.cs" />
+ <Compile Include="Parser\CSharp\CSharpErrorTest.cs" />
+ <Compile Include="Parser\CSharp\CSharpImplicitExpressionTest.cs" />
+ <Compile Include="Parser\CSharp\CSharpSpecialBlockTest.cs" />
+ <Compile Include="Parser\Html\HtmlBlockTest.cs" />
+ <Compile Include="Parser\CSharp\CSharpBlockTest.cs" />
+ <Compile Include="Text\LookaheadTextReaderTestBase.cs" />
+ <Compile Include="Text\SourceLocationTrackerTest.cs" />
+ <Compile Include="Text\TextBufferReaderTest.cs" />
+ <Compile Include="Utils\DisposableActionTest.cs" />
+ <Compile Include="Parser\Html\HtmlDocumentTest.cs" />
+ <Compile Include="Parser\ParserContextTest.cs" />
+ <Compile Include="Parser\Html\HtmlErrorTest.cs" />
+ <Compile Include="Parser\Html\HtmlParserTestUtils.cs" />
+ <Compile Include="Parser\Html\HtmlToCodeSwitchTest.cs" />
+ <Compile Include="Parser\RazorParserTest.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="Text\SourceLocationTest.cs" />
+ <Compile Include="Utils\SpanAssert.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="VBRazorCodeLanguageTest.cs" />
+ <Compile Include="Text\TextChangeTest.cs" />
+ <Compile Include="Text\TextReaderExtensionsTest.cs" />
+ <Compile Include="Utils\EnumerableUtils.cs" />
+ <Compile Include="Utils\MiscAssert.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\System.Web.Razor\System.Web.Razor.csproj">
+ <Project>{8F18041B-9410-4C36-A9C5-067813DF5F31}</Project>
+ <Name>System.Web.Razor</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\Microsoft.TestCommon\Microsoft.TestCommon.csproj">
+ <Project>{FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0}</Project>
+ <Name>Microsoft.TestCommon</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\nested-1000.html" />
+ </ItemGroup>
+ <ItemGroup>
+ <Service Include="{508349B6-6B84-4DF5-91F0-309BEEBAD82D}" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Source\CodeBlock.vbhtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Source\ExplicitExpression.vbhtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Source\MarkupInCodeBlock.vbhtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Source\Blocks.vbhtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Source\ImplicitExpression.vbhtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Source\NoLinePragmas.vbhtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Source\Imports.vbhtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Source\ExpressionsInCode.vbhtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Source\FunctionsBlock.vbhtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Source\Options.vbhtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Source\Templates.vbhtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Source\Sections.vbhtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Source\DesignTime.vbhtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Source\Templates.cshtml" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Source\Blocks.cshtml" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Source\CodeBlock.cshtml" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Source\NoLinePragmas.cshtml" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Source\ExplicitExpression.cshtml" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Source\ImplicitExpression.cshtml" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Source\Imports.cshtml" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Source\MarkupInCodeBlock.cshtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Source\DesignTime.cshtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Source\ExpressionsInCode.cshtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Source\FunctionsBlock.cshtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Source\Sections.cshtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Source\Inherits.cshtml" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Source\Inherits.vbhtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Source\NestedHelpers.cshtml" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Source\NestedHelpers.vbhtml" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Source\Instrumented.cshtml" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Source\Instrumented.vbhtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Source\RazorComments.cshtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Source\RazorComments.vbhtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Source\ParserError.cshtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Source\ParserError.vbhtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Source\ImplicitExpressionAtEOF.cshtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Source\CodeBlockAtEOF.cshtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Source\ExplicitExpressionAtEOF.cshtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Source\EmptyCodeBlock.cshtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Source\EmptyExplicitExpression.cshtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Source\EmptyImplicitExpression.cshtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Source\CodeBlockAtEOF.vbhtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Source\EmptyExplicitExpression.vbhtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Source\EmptyImplicitExpression.vbhtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Source\ExplicitExpressionAtEOF.vbhtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Source\ImplicitExpressionAtEOF.vbhtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Source\UnfinishedExpressionInCode.cshtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Source\UnfinishedExpressionInCode.vbhtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\DesignTime\Simple.cshtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\DesignTime\Simple.txt" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Source\Helpers.cshtml" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Source\HelpersMissingCloseParen.cshtml" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Source\HelpersMissingName.cshtml" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Source\HelpersMissingOpenBrace.cshtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Source\HelpersMissingOpenParen.cshtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Source\HelpersMissingOpenParen.vbhtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Source\Helpers.vbhtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Source\HelpersMissingCloseParen.vbhtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Source\HelpersMissingName.vbhtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Source\NestedCodeBlocks.cshtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Source\NestedCodeBlocks.vbhtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Source\InlineBlocks.cshtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Source\EmptyImplicitExpressionInCode.cshtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Source\EmptyImplicitExpressionInCode.vbhtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Source\HiddenSpansInCode.cshtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="packages.config" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Source\ResolveUrl.cshtml" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Source\ResolveUrl.vbhtml" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Source\LayoutDirective.cshtml" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Source\LayoutDirective.vbhtml" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\CS\Source\ConditionalAttributes.cshtml" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Source\ConditionalAttributes.vbhtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Output\Blocks.vb" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Output\CodeBlock.vb" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Output\CodeBlockAtEOF.vb" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Output\Comments.vb" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Output\ConditionalAttributes.vb" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Output\DesignTime.vb" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Output\EmptyExplicitExpression.vb" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Output\EmptyImplicitExpression.vb" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Output\EmptyImplicitExpressionInCode.vb" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Output\ExplicitExpression.vb" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Output\ExplicitExpressionAtEOF.vb" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Output\ExpressionsInCode.vb" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Output\FunctionsBlock.DesignTime.vb" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Output\FunctionsBlock.vb" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Output\Helpers.Instance.vb" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Output\Helpers.vb" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Output\HelpersMissingCloseParen.vb" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Output\HelpersMissingName.vb" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Output\HelpersMissingOpenParen.vb" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Output\ImplicitExpression.vb" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Output\ImplicitExpressionAtEOF.vb" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Output\Imports.DesignTime.vb" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Output\Imports.vb" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Output\Inherits.Designtime.vb" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Output\Inherits.Runtime.vb" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Output\Instrumented.vb" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Output\LayoutDirective.vb" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Output\MarkupInCodeBlock.vb" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Output\NestedCodeBlocks.vb" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Output\NestedHelpers.vb" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Output\NoLinePragmas.vb" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Output\Options.vb" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Output\ParserError.vb" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Output\RazorComments.vb" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Output\ResolveUrl.vb" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Output\Sections.vb" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Output\Templates.vb" />
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Output\UnfinishedExpressionInCode.vb" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\CodeGenerator\VB\Output\RazorComments.DesignTime.vb" />
+ </ItemGroup>
+ <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+</Project> \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Blocks.cs b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Blocks.cs
new file mode 100644
index 00000000..f92286bf
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Blocks.cs
@@ -0,0 +1,194 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:N.N.NNNNN.N
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace TestOutput {
+using System;
+
+public class Blocks {
+#line hidden
+public Blocks() {
+}
+public override void Execute() {
+
+#line 1 "Blocks.cshtml"
+
+ int i = 1;
+
+
+#line default
+#line hidden
+WriteLiteral("\r\n\r\n");
+
+
+#line 5 "Blocks.cshtml"
+ while(i <= 10) {
+
+
+#line default
+#line hidden
+WriteLiteral(" <p>Hello from C#, #");
+
+
+#line 6 "Blocks.cshtml"
+ Write(i);
+
+
+#line default
+#line hidden
+WriteLiteral("</p>\r\n");
+
+
+#line 7 "Blocks.cshtml"
+ i += 1;
+}
+
+
+#line default
+#line hidden
+WriteLiteral("\r\n");
+
+
+#line 10 "Blocks.cshtml"
+ if(i == 11) {
+
+
+#line default
+#line hidden
+WriteLiteral(" <p>We wrote 10 lines!</p>\r\n");
+
+
+#line 12 "Blocks.cshtml"
+}
+
+
+#line default
+#line hidden
+WriteLiteral("\r\n");
+
+
+#line 14 "Blocks.cshtml"
+ switch(i) {
+ case 11:
+
+
+#line default
+#line hidden
+WriteLiteral(" <p>No really, we wrote 10 lines!</p>\r\n");
+
+
+#line 17 "Blocks.cshtml"
+ break;
+ default:
+
+
+#line default
+#line hidden
+WriteLiteral(" <p>Actually, we didn\'t...</p>\r\n");
+
+
+#line 20 "Blocks.cshtml"
+ break;
+}
+
+
+#line default
+#line hidden
+WriteLiteral("\r\n");
+
+
+#line 23 "Blocks.cshtml"
+ for(int j = 1; j <= 10; j += 2) {
+
+
+#line default
+#line hidden
+WriteLiteral(" <p>Hello again from C#, #");
+
+
+#line 24 "Blocks.cshtml"
+ Write(j);
+
+
+#line default
+#line hidden
+WriteLiteral("</p>\r\n");
+
+
+#line 25 "Blocks.cshtml"
+}
+
+
+#line default
+#line hidden
+WriteLiteral("\r\n");
+
+
+#line 27 "Blocks.cshtml"
+ try {
+
+
+#line default
+#line hidden
+WriteLiteral(" <p>That time, we wrote 5 lines!</p>\r\n");
+
+
+#line 29 "Blocks.cshtml"
+} catch(Exception ex) {
+
+
+#line default
+#line hidden
+WriteLiteral(" <p>Oh no! An error occurred: ");
+
+
+#line 30 "Blocks.cshtml"
+ Write(ex.Message);
+
+
+#line default
+#line hidden
+WriteLiteral("</p>\r\n");
+
+
+#line 31 "Blocks.cshtml"
+}
+
+
+#line default
+#line hidden
+WriteLiteral("\r\n<p>i is now ");
+
+
+#line 33 "Blocks.cshtml"
+ Write(i);
+
+
+#line default
+#line hidden
+WriteLiteral("</p>\r\n\r\n");
+
+
+#line 35 "Blocks.cshtml"
+ lock(new object()) {
+
+
+#line default
+#line hidden
+WriteLiteral(" <p>This block is locked, for your security!</p>\r\n");
+
+
+#line 37 "Blocks.cshtml"
+}
+
+#line default
+#line hidden
+}
+}
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/CodeBlock.cs b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/CodeBlock.cs
new file mode 100644
index 00000000..a47b997e
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/CodeBlock.cs
@@ -0,0 +1,31 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:N.N.NNNNN.N
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace TestOutput {
+using System;
+
+public class CodeBlock {
+#line hidden
+public CodeBlock() {
+}
+public override void Execute() {
+
+#line 1 "CodeBlock.cshtml"
+
+ for(int i = 1; i <= 10; i++) {
+ Output.Write("<p>Hello from C#, #" + i.ToString() + "</p>");
+ }
+
+
+#line default
+#line hidden
+}
+}
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/CodeBlockAtEOF.cs b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/CodeBlockAtEOF.cs
new file mode 100644
index 00000000..1563d2e0
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/CodeBlockAtEOF.cs
@@ -0,0 +1,27 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:N.N.NNNNN.N
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace TestOutput {
+using System;
+
+public class CodeBlockAtEOF {
+#line hidden
+public CodeBlockAtEOF() {
+}
+public override void Execute() {
+
+#line 1 "CodeBlockAtEOF.cshtml"
+
+
+#line default
+#line hidden
+}
+}
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Comments.cs b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Comments.cs
new file mode 100644
index 00000000..61e70fcb
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Comments.cs
@@ -0,0 +1,38 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:N.N.NNNNN.N
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace TestOutput {
+using System;
+
+public class Comments {
+public override void Execute() {
+
+
+#line 1 "Comments.cshtml"
+ //This is not going to be rendered
+
+
+#line default
+#line hidden
+WriteLiteral("<p>This is going to be rendered</p>\r\n");
+
+
+
+#line 3 "Comments.cshtml"
+ /* Neither is this
+ nor this
+ nor this */
+
+#line default
+#line hidden
+
+}
+}
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/ConditionalAttributes.cs b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/ConditionalAttributes.cs
new file mode 100644
index 00000000..90d8a9a5
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/ConditionalAttributes.cs
@@ -0,0 +1,197 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:N.N.NNNNN.N
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace TestOutput {
+using System;
+
+public class ConditionalAttributes {
+#line hidden
+public ConditionalAttributes() {
+}
+public override void Execute() {
+
+#line 1 "ConditionalAttributes.cshtml"
+
+ var ch = true;
+ var cls = "bar";
+
+
+#line default
+#line hidden
+WriteLiteral(" <a");
+
+WriteLiteral(" href=\"Foo\"");
+
+WriteLiteral(" />\r\n");
+
+WriteLiteral(" <p");
+
+WriteAttribute("class", Tuple.Create(" class=\"", 74), Tuple.Create("\"", 86)
+
+#line 5 "ConditionalAttributes.cshtml"
+, Tuple.Create(Tuple.Create("", 82), Tuple.Create<System.Object, System.Int32>(cls
+
+#line default
+#line hidden
+, 82), false)
+);
+
+WriteLiteral(" />\r\n");
+
+WriteLiteral(" <p");
+
+WriteAttribute("class", Tuple.Create(" class=\"", 98), Tuple.Create("\"", 114)
+, Tuple.Create(Tuple.Create("", 106), Tuple.Create("foo", 106), true)
+
+#line 6 "ConditionalAttributes.cshtml"
+, Tuple.Create(Tuple.Create(" ", 109), Tuple.Create<System.Object, System.Int32>(cls
+
+#line default
+#line hidden
+, 110), false)
+);
+
+WriteLiteral(" />\r\n");
+
+WriteLiteral(" <p");
+
+WriteAttribute("class", Tuple.Create(" class=\"", 126), Tuple.Create("\"", 142)
+
+#line 7 "ConditionalAttributes.cshtml"
+, Tuple.Create(Tuple.Create("", 134), Tuple.Create<System.Object, System.Int32>(cls
+
+#line default
+#line hidden
+, 134), false)
+, Tuple.Create(Tuple.Create(" ", 138), Tuple.Create("foo", 139), true)
+);
+
+WriteLiteral(" />\r\n");
+
+WriteLiteral(" <input");
+
+WriteLiteral(" type=\"checkbox\"");
+
+WriteAttribute("checked", Tuple.Create(" checked=\"", 174), Tuple.Create("\"", 187)
+
+#line 8 "ConditionalAttributes.cshtml"
+, Tuple.Create(Tuple.Create("", 184), Tuple.Create<System.Object, System.Int32>(ch
+
+#line default
+#line hidden
+, 184), false)
+);
+
+WriteLiteral(" />\r\n");
+
+WriteLiteral(" <input");
+
+WriteLiteral(" type=\"checkbox\"");
+
+WriteAttribute("checked", Tuple.Create(" checked=\"", 219), Tuple.Create("\"", 236)
+, Tuple.Create(Tuple.Create("", 229), Tuple.Create("foo", 229), true)
+
+#line 9 "ConditionalAttributes.cshtml"
+, Tuple.Create(Tuple.Create(" ", 232), Tuple.Create<System.Object, System.Int32>(ch
+
+#line default
+#line hidden
+, 233), false)
+);
+
+WriteLiteral(" />\r\n");
+
+WriteLiteral(" <p");
+
+WriteAttribute("class", Tuple.Create(" class=\"", 248), Tuple.Create("\"", 281)
+, Tuple.Create(Tuple.Create("", 256), Tuple.Create<System.Object, System.Int32>(new Template(__razor_attribute_value_writer => {
+
+
+#line 10 "ConditionalAttributes.cshtml"
+ if(cls != null) {
+
+#line default
+#line hidden
+
+#line 10 "ConditionalAttributes.cshtml"
+WriteTo(__razor_attribute_value_writer, cls);
+
+
+#line default
+#line hidden
+
+#line 10 "ConditionalAttributes.cshtml"
+ }
+
+#line default
+#line hidden
+}), 256), false)
+);
+
+WriteLiteral(" />\r\n");
+
+WriteLiteral(" <a");
+
+WriteAttribute("href", Tuple.Create(" href=\"", 293), Tuple.Create("\"", 305)
+, Tuple.Create(Tuple.Create("", 300), Tuple.Create<System.Object, System.Int32>(Href("~/Foo")
+, 300), false)
+);
+
+WriteLiteral(" />\r\n");
+
+WriteLiteral(" <script");
+
+WriteAttribute("src", Tuple.Create(" src=\"", 322), Tuple.Create("\"", 373)
+
+#line 12 "ConditionalAttributes.cshtml"
+, Tuple.Create(Tuple.Create("", 328), Tuple.Create<System.Object, System.Int32>(Url.Content("~/Scripts/jquery-1.6.2.min.js")
+
+#line default
+#line hidden
+, 328), false)
+);
+
+WriteLiteral(" type=\"text/javascript\"");
+
+WriteLiteral("></script>\r\n");
+
+WriteLiteral(" <script");
+
+WriteAttribute("src", Tuple.Create(" src=\"", 420), Tuple.Create("\"", 487)
+
+#line 13 "ConditionalAttributes.cshtml"
+, Tuple.Create(Tuple.Create("", 426), Tuple.Create<System.Object, System.Int32>(Url.Content("~/Scripts/modernizr-2.0.6-development-only.js")
+
+#line default
+#line hidden
+, 426), false)
+);
+
+WriteLiteral(" type=\"text/javascript\"");
+
+WriteLiteral("></script>\r\n");
+
+WriteLiteral(" <script");
+
+WriteLiteral(" src=\"http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.16/jquery-ui.min.js\"");
+
+WriteLiteral(" type=\"text/javascript\"");
+
+WriteLiteral("></script>\r\n");
+
+
+#line 15 "ConditionalAttributes.cshtml"
+
+
+#line default
+#line hidden
+}
+}
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/DesignTime.cs b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/DesignTime.cs
new file mode 100644
index 00000000..794a3661
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/DesignTime.cs
@@ -0,0 +1,108 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:N.N.NNNNN.N
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace TestOutput {
+using System;
+
+public class DesignTime {
+private static object @__o;
+#line hidden
+#line 9 "DesignTime.cshtml"
+public static Template Foo() {
+#line default
+#line hidden
+return new Template(__razor_helper_writer => {
+
+#line 10 "DesignTime.cshtml"
+
+ if(true) {
+
+#line default
+#line hidden
+
+#line 11 "DesignTime.cshtml"
+
+ }
+
+#line default
+#line hidden
+});
+
+#line 12 "DesignTime.cshtml"
+}
+#line default
+#line hidden
+
+public DesignTime() {
+}
+public override void Execute() {
+
+#line 1 "DesignTime.cshtml"
+ for(int i = 1; i <= 10; i++) {
+
+
+#line default
+#line hidden
+
+#line 2 "DesignTime.cshtml"
+ __o = i;
+
+
+#line default
+#line hidden
+
+#line 3 "DesignTime.cshtml"
+
+ }
+
+#line default
+#line hidden
+
+#line 4 "DesignTime.cshtml"
+__o = Foo(Bar.Baz);
+
+
+#line default
+#line hidden
+
+#line 5 "DesignTime.cshtml"
+__o = Foo(item => new Template(__razor_template_writer => {
+
+
+#line default
+#line hidden
+
+#line 6 "DesignTime.cshtml"
+ __o = baz;
+
+
+#line default
+#line hidden
+
+#line 7 "DesignTime.cshtml"
+ }));
+
+
+#line default
+#line hidden
+DefineSection("Footer", () => {
+
+
+#line 8 "DesignTime.cshtml"
+__o = bar;
+
+
+#line default
+#line hidden
+});
+
+}
+}
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/EmptyCodeBlock.cs b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/EmptyCodeBlock.cs
new file mode 100644
index 00000000..5aa97216
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/EmptyCodeBlock.cs
@@ -0,0 +1,27 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:N.N.NNNNN.N
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace TestOutput {
+using System;
+
+public class EmptyCodeBlock {
+#line hidden
+public EmptyCodeBlock() {
+}
+public override void Execute() {
+
+#line 1 "EmptyCodeBlock.cshtml"
+
+
+#line default
+#line hidden
+}
+}
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/EmptyExplicitExpression.cs b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/EmptyExplicitExpression.cs
new file mode 100644
index 00000000..bac2bc5d
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/EmptyExplicitExpression.cs
@@ -0,0 +1,29 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:N.N.NNNNN.N
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace TestOutput {
+using System;
+
+public class EmptyExplicitExpression {
+private static object @__o;
+#line hidden
+public EmptyExplicitExpression() {
+}
+public override void Execute() {
+
+#line 1 "EmptyExplicitExpression.cshtml"
+__o = ;
+
+
+#line default
+#line hidden
+}
+}
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/EmptyImplicitExpression.cs b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/EmptyImplicitExpression.cs
new file mode 100644
index 00000000..bf8cdefa
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/EmptyImplicitExpression.cs
@@ -0,0 +1,29 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:N.N.NNNNN.N
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace TestOutput {
+using System;
+
+public class EmptyImplicitExpression {
+private static object @__o;
+#line hidden
+public EmptyImplicitExpression() {
+}
+public override void Execute() {
+
+#line 1 "EmptyImplicitExpression.cshtml"
+__o = ;
+
+
+#line default
+#line hidden
+}
+}
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/EmptyImplicitExpressionInCode.cs b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/EmptyImplicitExpressionInCode.cs
new file mode 100644
index 00000000..3c8deb41
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/EmptyImplicitExpressionInCode.cs
@@ -0,0 +1,43 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:N.N.NNNNN.N
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace TestOutput {
+using System;
+
+public class EmptyImplicitExpressionInCode {
+private static object @__o;
+#line hidden
+public EmptyImplicitExpressionInCode() {
+}
+public override void Execute() {
+
+#line 1 "EmptyImplicitExpressionInCode.cshtml"
+
+
+
+#line default
+#line hidden
+
+#line 2 "EmptyImplicitExpressionInCode.cshtml"
+__o = ;
+
+
+#line default
+#line hidden
+
+#line 3 "EmptyImplicitExpressionInCode.cshtml"
+
+
+
+#line default
+#line hidden
+}
+}
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/ExplicitExpression.cs b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/ExplicitExpression.cs
new file mode 100644
index 00000000..95651766
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/ExplicitExpression.cs
@@ -0,0 +1,30 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:N.N.NNNNN.N
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace TestOutput {
+using System;
+
+public class ExplicitExpression {
+#line hidden
+public ExplicitExpression() {
+}
+public override void Execute() {
+WriteLiteral("1 + 1 = ");
+
+
+#line 1 "ExplicitExpression.cshtml"
+ Write(1+1);
+
+
+#line default
+#line hidden
+}
+}
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/ExplicitExpressionAtEOF.cs b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/ExplicitExpressionAtEOF.cs
new file mode 100644
index 00000000..941af4e0
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/ExplicitExpressionAtEOF.cs
@@ -0,0 +1,29 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:N.N.NNNNN.N
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace TestOutput {
+using System;
+
+public class ExplicitExpressionAtEOF {
+private static object @__o;
+#line hidden
+public ExplicitExpressionAtEOF() {
+}
+public override void Execute() {
+
+#line 1 "ExplicitExpressionAtEOF.cshtml"
+__o = ;
+
+
+#line default
+#line hidden
+}
+}
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/ExpressionsInCode.cs b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/ExpressionsInCode.cs
new file mode 100644
index 00000000..4171f401
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/ExpressionsInCode.cs
@@ -0,0 +1,89 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:N.N.NNNNN.N
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace TestOutput {
+using System;
+
+public class ExpressionsInCode {
+#line hidden
+public ExpressionsInCode() {
+}
+public override void Execute() {
+
+#line 1 "ExpressionsInCode.cshtml"
+
+ object foo = null;
+ string bar = "Foo";
+
+
+#line default
+#line hidden
+WriteLiteral("\r\n\r\n");
+
+
+#line 6 "ExpressionsInCode.cshtml"
+ if(foo != null) {
+
+
+#line default
+#line hidden
+
+#line 7 "ExpressionsInCode.cshtml"
+Write(foo);
+
+
+#line default
+#line hidden
+
+#line 7 "ExpressionsInCode.cshtml"
+
+} else {
+
+
+#line default
+#line hidden
+WriteLiteral(" <p>Foo is Null!</p>\r\n");
+
+
+#line 10 "ExpressionsInCode.cshtml"
+}
+
+
+#line default
+#line hidden
+WriteLiteral("\r\n<p>\r\n");
+
+
+#line 13 "ExpressionsInCode.cshtml"
+ if(!String.IsNullOrEmpty(bar)) {
+
+
+#line default
+#line hidden
+
+#line 14 "ExpressionsInCode.cshtml"
+Write(bar.Replace("F", "B"));
+
+
+#line default
+#line hidden
+
+#line 14 "ExpressionsInCode.cshtml"
+
+}
+
+
+#line default
+#line hidden
+WriteLiteral("</p>");
+
+}
+}
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/FunctionsBlock.DesignTime.cs b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/FunctionsBlock.DesignTime.cs
new file mode 100644
index 00000000..534039f8
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/FunctionsBlock.DesignTime.cs
@@ -0,0 +1,46 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:N.N.NNNNN.N
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace TestOutput {
+using System;
+
+public class FunctionsBlock {
+private static object @__o;
+#line hidden
+#line 1 "FunctionsBlock.cshtml"
+
+
+
+#line default
+#line hidden
+
+#line 2 "FunctionsBlock.cshtml"
+
+ Random _rand = new Random();
+ private int RandomInt() {
+ return _rand.Next();
+ }
+
+#line default
+#line hidden
+
+public FunctionsBlock() {
+}
+public override void Execute() {
+
+#line 3 "FunctionsBlock.cshtml"
+ __o = RandomInt();
+
+
+#line default
+#line hidden
+}
+}
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/FunctionsBlock.cs b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/FunctionsBlock.cs
new file mode 100644
index 00000000..0b9f2a37
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/FunctionsBlock.cs
@@ -0,0 +1,49 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:N.N.NNNNN.N
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace TestOutput {
+using System;
+
+public class FunctionsBlock {
+#line hidden
+#line 1 "FunctionsBlock.cshtml"
+
+
+
+#line default
+#line hidden
+
+#line 5 "FunctionsBlock.cshtml"
+
+ Random _rand = new Random();
+ private int RandomInt() {
+ return _rand.Next();
+ }
+
+#line default
+#line hidden
+
+public FunctionsBlock() {
+}
+public override void Execute() {
+WriteLiteral("\r\n");
+
+WriteLiteral("\r\nHere\'s a random number: ");
+
+
+#line 12 "FunctionsBlock.cshtml"
+ Write(RandomInt());
+
+
+#line default
+#line hidden
+}
+}
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Helpers.Instance.cs b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Helpers.Instance.cs
new file mode 100644
index 00000000..ebe067e4
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Helpers.Instance.cs
@@ -0,0 +1,96 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:N.N.NNNNN.N
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace TestOutput {
+using System;
+
+public class Helpers {
+#line hidden
+#line 1 "Helpers.cshtml"
+public Template Bold(string s) {
+#line default
+#line hidden
+return new Template(__razor_helper_writer => {
+
+#line 1 "Helpers.cshtml"
+
+ s = s.ToUpper();
+
+#line default
+#line hidden
+WriteLiteralTo(__razor_helper_writer, " <strong>");
+
+#line 3 "Helpers.cshtml"
+WriteTo(__razor_helper_writer, s);
+
+#line default
+#line hidden
+WriteLiteralTo(__razor_helper_writer, "</strong>\r\n");
+
+#line 4 "Helpers.cshtml"
+
+#line default
+#line hidden
+});
+
+#line 4 "Helpers.cshtml"
+}
+#line default
+#line hidden
+
+#line 6 "Helpers.cshtml"
+public Template Italic(string s) {
+#line default
+#line hidden
+return new Template(__razor_helper_writer => {
+
+#line 6 "Helpers.cshtml"
+
+ s = s.ToUpper();
+
+#line default
+#line hidden
+WriteLiteralTo(__razor_helper_writer, " <em>");
+
+#line 8 "Helpers.cshtml"
+WriteTo(__razor_helper_writer, s);
+
+#line default
+#line hidden
+WriteLiteralTo(__razor_helper_writer, "</em>\r\n");
+
+#line 9 "Helpers.cshtml"
+
+#line default
+#line hidden
+});
+
+#line 9 "Helpers.cshtml"
+}
+#line default
+#line hidden
+
+public Helpers() {
+}
+public override void Execute() {
+WriteLiteral("\r\n");
+
+WriteLiteral("\r\n");
+
+
+#line 11 "Helpers.cshtml"
+Write(Bold("Hello"));
+
+
+#line default
+#line hidden
+}
+}
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Helpers.cs b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Helpers.cs
new file mode 100644
index 00000000..3afbaca2
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Helpers.cs
@@ -0,0 +1,96 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:N.N.NNNNN.N
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace TestOutput {
+using System;
+
+public class Helpers {
+#line hidden
+#line 1 "Helpers.cshtml"
+public static Template Bold(string s) {
+#line default
+#line hidden
+return new Template(__razor_helper_writer => {
+
+#line 1 "Helpers.cshtml"
+
+ s = s.ToUpper();
+
+#line default
+#line hidden
+WriteLiteralTo(__razor_helper_writer, " <strong>");
+
+#line 3 "Helpers.cshtml"
+WriteTo(__razor_helper_writer, s);
+
+#line default
+#line hidden
+WriteLiteralTo(__razor_helper_writer, "</strong>\r\n");
+
+#line 4 "Helpers.cshtml"
+
+#line default
+#line hidden
+});
+
+#line 4 "Helpers.cshtml"
+}
+#line default
+#line hidden
+
+#line 6 "Helpers.cshtml"
+public static Template Italic(string s) {
+#line default
+#line hidden
+return new Template(__razor_helper_writer => {
+
+#line 6 "Helpers.cshtml"
+
+ s = s.ToUpper();
+
+#line default
+#line hidden
+WriteLiteralTo(__razor_helper_writer, " <em>");
+
+#line 8 "Helpers.cshtml"
+WriteTo(__razor_helper_writer, s);
+
+#line default
+#line hidden
+WriteLiteralTo(__razor_helper_writer, "</em>\r\n");
+
+#line 9 "Helpers.cshtml"
+
+#line default
+#line hidden
+});
+
+#line 9 "Helpers.cshtml"
+}
+#line default
+#line hidden
+
+public Helpers() {
+}
+public override void Execute() {
+WriteLiteral("\r\n");
+
+WriteLiteral("\r\n");
+
+
+#line 11 "Helpers.cshtml"
+Write(Bold("Hello"));
+
+
+#line default
+#line hidden
+}
+}
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/HelpersMissingCloseParen.cs b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/HelpersMissingCloseParen.cs
new file mode 100644
index 00000000..5e340f70
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/HelpersMissingCloseParen.cs
@@ -0,0 +1,61 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:N.N.NNNNN.N
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace TestOutput {
+using System;
+
+public class HelpersMissingCloseParen {
+#line hidden
+#line 1 "HelpersMissingCloseParen.cshtml"
+public static Template Bold(string s) {
+#line default
+#line hidden
+return new Template(__razor_helper_writer => {
+
+#line 1 "HelpersMissingCloseParen.cshtml"
+
+ s = s.ToUpper();
+
+#line default
+#line hidden
+WriteLiteralTo(__razor_helper_writer, " <strong>");
+
+#line 3 "HelpersMissingCloseParen.cshtml"
+WriteTo(__razor_helper_writer, s);
+
+#line default
+#line hidden
+WriteLiteralTo(__razor_helper_writer, "</strong>\r\n");
+
+#line 4 "HelpersMissingCloseParen.cshtml"
+
+#line default
+#line hidden
+});
+
+#line 4 "HelpersMissingCloseParen.cshtml"
+}
+#line default
+#line hidden
+
+#line 6 "HelpersMissingCloseParen.cshtml"
+public static Template Italic(string s
+@Bold("Hello")
+#line default
+#line hidden
+
+public HelpersMissingCloseParen() {
+}
+public override void Execute() {
+WriteLiteral("\r\n");
+
+}
+}
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/HelpersMissingName.cs b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/HelpersMissingName.cs
new file mode 100644
index 00000000..aa0ea146
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/HelpersMissingName.cs
@@ -0,0 +1,21 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:N.N.NNNNN.N
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace TestOutput {
+using System;
+
+public class HelpersMissingName {
+#line hidden
+public HelpersMissingName() {
+}
+public override void Execute() {
+}
+}
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/HelpersMissingOpenBrace.cs b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/HelpersMissingOpenBrace.cs
new file mode 100644
index 00000000..c464f666
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/HelpersMissingOpenBrace.cs
@@ -0,0 +1,67 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:N.N.NNNNN.N
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace TestOutput {
+using System;
+
+public class HelpersMissingOpenBrace {
+#line hidden
+#line 1 "HelpersMissingOpenBrace.cshtml"
+public static Template Bold(string s) {
+#line default
+#line hidden
+return new Template(__razor_helper_writer => {
+
+#line 1 "HelpersMissingOpenBrace.cshtml"
+
+ s = s.ToUpper();
+
+#line default
+#line hidden
+WriteLiteralTo(__razor_helper_writer, " <strong>");
+
+#line 3 "HelpersMissingOpenBrace.cshtml"
+WriteTo(__razor_helper_writer, s);
+
+#line default
+#line hidden
+WriteLiteralTo(__razor_helper_writer, "</strong>\r\n");
+
+#line 4 "HelpersMissingOpenBrace.cshtml"
+
+#line default
+#line hidden
+});
+
+#line 4 "HelpersMissingOpenBrace.cshtml"
+}
+#line default
+#line hidden
+
+#line 6 "HelpersMissingOpenBrace.cshtml"
+public static Template Italic(string s)
+#line default
+#line hidden
+
+public HelpersMissingOpenBrace() {
+}
+public override void Execute() {
+WriteLiteral("\r\n");
+
+
+#line 7 "HelpersMissingOpenBrace.cshtml"
+Write(Italic(s));
+
+
+#line default
+#line hidden
+}
+}
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/HelpersMissingOpenParen.cs b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/HelpersMissingOpenParen.cs
new file mode 100644
index 00000000..136d52c9
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/HelpersMissingOpenParen.cs
@@ -0,0 +1,67 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:N.N.NNNNN.N
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace TestOutput {
+using System;
+
+public class HelpersMissingOpenParen {
+#line hidden
+#line 1 "HelpersMissingOpenParen.cshtml"
+public static Template Bold(string s) {
+#line default
+#line hidden
+return new Template(__razor_helper_writer => {
+
+#line 1 "HelpersMissingOpenParen.cshtml"
+
+ s = s.ToUpper();
+
+#line default
+#line hidden
+WriteLiteralTo(__razor_helper_writer, " <strong>");
+
+#line 3 "HelpersMissingOpenParen.cshtml"
+WriteTo(__razor_helper_writer, s);
+
+#line default
+#line hidden
+WriteLiteralTo(__razor_helper_writer, "</strong>\r\n");
+
+#line 4 "HelpersMissingOpenParen.cshtml"
+
+#line default
+#line hidden
+});
+
+#line 4 "HelpersMissingOpenParen.cshtml"
+}
+#line default
+#line hidden
+
+#line 6 "HelpersMissingOpenParen.cshtml"
+public static Template Italic
+#line default
+#line hidden
+
+public HelpersMissingOpenParen() {
+}
+public override void Execute() {
+WriteLiteral("\r\n");
+
+
+#line 7 "HelpersMissingOpenParen.cshtml"
+Write(Bold("Hello"));
+
+
+#line default
+#line hidden
+}
+}
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/HiddenSpansInCode.cs b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/HiddenSpansInCode.cs
new file mode 100644
index 00000000..31c1da09
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/HiddenSpansInCode.cs
@@ -0,0 +1,35 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:N.N.NNNNN.N
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace TestOutput {
+using System;
+
+public class HiddenSpansInCode {
+#line hidden
+public HiddenSpansInCode() {
+}
+public override void Execute() {
+
+#line 1 "HiddenSpansInCode.cshtml"
+
+
+
+#line default
+#line hidden
+
+#line 2 "HiddenSpansInCode.cshtml"
+ @Da
+
+
+#line default
+#line hidden
+}
+}
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/ImplicitExpression.cs b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/ImplicitExpression.cs
new file mode 100644
index 00000000..6d987037
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/ImplicitExpression.cs
@@ -0,0 +1,45 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:N.N.NNNNN.N
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace TestOutput {
+using System;
+
+public class ImplicitExpression {
+#line hidden
+public ImplicitExpression() {
+}
+public override void Execute() {
+
+#line 1 "ImplicitExpression.cshtml"
+ for(int i = 1; i <= 10; i++) {
+
+
+#line default
+#line hidden
+WriteLiteral(" <p>This is item #");
+
+
+#line 2 "ImplicitExpression.cshtml"
+ Write(i);
+
+
+#line default
+#line hidden
+WriteLiteral("</p>\r\n");
+
+
+#line 3 "ImplicitExpression.cshtml"
+}
+
+#line default
+#line hidden
+}
+}
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/ImplicitExpressionAtEOF.cs b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/ImplicitExpressionAtEOF.cs
new file mode 100644
index 00000000..0eff69ae
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/ImplicitExpressionAtEOF.cs
@@ -0,0 +1,29 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:N.N.NNNNN.N
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace TestOutput {
+using System;
+
+public class ImplicitExpressionAtEOF {
+private static object @__o;
+#line hidden
+public ImplicitExpressionAtEOF() {
+}
+public override void Execute() {
+
+#line 1 "ImplicitExpressionAtEOF.cshtml"
+__o = ;
+
+
+#line default
+#line hidden
+}
+}
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Imports.DesignTime.cs b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Imports.DesignTime.cs
new file mode 100644
index 00000000..082da73b
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Imports.DesignTime.cs
@@ -0,0 +1,53 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:N.N.NNNNN.N
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace TestOutput {
+
+#line 3 "Imports.cshtml"
+using System;
+
+#line default
+#line hidden
+
+#line 1 "Imports.cshtml"
+using System.IO;
+
+#line default
+#line hidden
+
+#line 2 "Imports.cshtml"
+using Foo = System.Text.Encoding;
+
+#line default
+#line hidden
+
+public class Imports {
+private static object @__o;
+#line hidden
+public Imports() {
+}
+public override void Execute() {
+
+#line 4 "Imports.cshtml"
+ __o = typeof(Path).FullName;
+
+
+#line default
+#line hidden
+
+#line 5 "Imports.cshtml"
+ __o = typeof(Foo).FullName;
+
+
+#line default
+#line hidden
+}
+}
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Imports.cs b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Imports.cs
new file mode 100644
index 00000000..58105a3b
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Imports.cs
@@ -0,0 +1,58 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:N.N.NNNNN.N
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace TestOutput {
+
+#line 3 "Imports.cshtml"
+using System;
+
+#line default
+#line hidden
+
+#line 1 "Imports.cshtml"
+using System.IO;
+
+#line default
+#line hidden
+
+#line 2 "Imports.cshtml"
+using Foo = System.Text.Encoding;
+
+#line default
+#line hidden
+
+public class Imports {
+#line hidden
+public Imports() {
+}
+public override void Execute() {
+WriteLiteral("\r\n<p>Path\'s full type name is ");
+
+
+#line 5 "Imports.cshtml"
+ Write(typeof(Path).FullName);
+
+
+#line default
+#line hidden
+WriteLiteral("</p>\r\n<p>Foo\'s actual full type name is ");
+
+
+#line 6 "Imports.cshtml"
+ Write(typeof(Foo).FullName);
+
+
+#line default
+#line hidden
+WriteLiteral("</p>");
+
+}
+}
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Inherits.Designtime.cs b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Inherits.Designtime.cs
new file mode 100644
index 00000000..44af46d9
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Inherits.Designtime.cs
@@ -0,0 +1,40 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:N.N.NNNNN.N
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace TestOutput {
+using System;
+
+public class Inherits : foo.bar<baz<biz>>.boz bar {
+private static object @__o;
+#line hidden
+public Inherits() {
+}
+private void @__RazorDesignTimeHelpers__() {
+#pragma warning disable 219
+
+#line 2 "Inherits.cshtml"
+ foo.bar<baz<biz>>.boz bar __inheritsHelper = null;
+
+
+#line default
+#line hidden
+#pragma warning restore 219
+}
+public override void Execute() {
+
+#line 1 "Inherits.cshtml"
+__o = foo();
+
+
+#line default
+#line hidden
+}
+}
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Inherits.Runtime.cs b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Inherits.Runtime.cs
new file mode 100644
index 00000000..77e3633e
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Inherits.Runtime.cs
@@ -0,0 +1,30 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:N.N.NNNNN.N
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace TestOutput {
+using System;
+
+public class Inherits : foo.bar<baz<biz>>.boz bar {
+#line hidden
+public Inherits() {
+}
+public override void Execute() {
+
+#line 1 "Inherits.cshtml"
+Write(foo());
+
+
+#line default
+#line hidden
+WriteLiteral("\r\n\r\n");
+
+}
+}
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/InlineBlocks.cs b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/InlineBlocks.cs
new file mode 100644
index 00000000..3f24dcb0
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/InlineBlocks.cs
@@ -0,0 +1,72 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:N.N.NNNNN.N
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace TestOutput {
+using System;
+
+public class InlineBlocks {
+#line hidden
+#line 1 "InlineBlocks.cshtml"
+public static Template Link(string link) {
+#line default
+#line hidden
+return new Template(__razor_helper_writer => {
+
+#line 1 "InlineBlocks.cshtml"
+
+
+#line default
+#line hidden
+WriteLiteralTo(__razor_helper_writer, " <a");
+WriteAttributeTo(__razor_helper_writer, "href", Tuple.Create(" href=\"", 35), Tuple.Create("\"", 93), Tuple.Create(Tuple.Create("", 42), Tuple.Create<System.Object, System.Int32>(new Template(__razor_attribute_value_writer => {
+
+#line 2 "InlineBlocks.cshtml"
+ if(link != null) {
+#line default
+#line hidden
+
+#line 2 "InlineBlocks.cshtml"
+WriteTo(__razor_attribute_value_writer, link);
+
+#line default
+#line hidden
+
+#line 2 "InlineBlocks.cshtml"
+ } else {
+#line default
+#line hidden
+WriteLiteralTo(__razor_attribute_value_writer, " ");
+WriteLiteralTo(__razor_attribute_value_writer, "#");
+WriteLiteralTo(__razor_attribute_value_writer, " ");
+
+#line 2 "InlineBlocks.cshtml"
+ }
+#line default
+#line hidden
+}), 42), false));
+WriteLiteralTo(__razor_helper_writer, " />\r\n");
+
+#line 3 "InlineBlocks.cshtml"
+
+#line default
+#line hidden
+});
+
+#line 3 "InlineBlocks.cshtml"
+}
+#line default
+#line hidden
+
+public InlineBlocks() {
+}
+public override void Execute() {
+}
+}
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Instrumented.cs b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Instrumented.cs
new file mode 100644
index 00000000..c0a7cc97
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Instrumented.cs
@@ -0,0 +1,348 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:N.N.NNNNN.N
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace TestOutput {
+using System;
+
+public class Instrumented {
+#line hidden
+#line 1 "Instrumented.cshtml"
+public static Template Strong(string s) {
+#line default
+#line hidden
+return new Template(__razor_helper_writer => {
+
+#line 1 "Instrumented.cshtml"
+
+
+#line default
+#line hidden
+BeginContext(__razor_helper_writer, "~/Instrumented.cshtml", 28, 12, true);
+WriteLiteralTo(__razor_helper_writer, " <strong>");
+EndContext(__razor_helper_writer, "~/Instrumented.cshtml", 28, 12, true);
+BeginContext(__razor_helper_writer, "~/Instrumented.cshtml", 41, 1, false);
+
+#line 2 "Instrumented.cshtml"
+WriteTo(__razor_helper_writer, s);
+
+#line default
+#line hidden
+EndContext(__razor_helper_writer, "~/Instrumented.cshtml", 41, 1, false);
+BeginContext(__razor_helper_writer, "~/Instrumented.cshtml", 42, 11, true);
+WriteLiteralTo(__razor_helper_writer, "</strong>\r\n");
+EndContext(__razor_helper_writer, "~/Instrumented.cshtml", 42, 11, true);
+
+#line 3 "Instrumented.cshtml"
+
+#line default
+#line hidden
+});
+
+#line 3 "Instrumented.cshtml"
+}
+#line default
+#line hidden
+
+public Instrumented() {
+}
+public override void Execute() {
+BeginContext("~/Instrumented.cshtml", 56, 2, true);
+
+WriteLiteral("\r\n");
+
+EndContext("~/Instrumented.cshtml", 56, 2, true);
+
+
+#line 5 "Instrumented.cshtml"
+
+ int i = 1;
+ var foo =
+
+#line default
+#line hidden
+item => new Template(__razor_template_writer => {
+
+BeginContext(__razor_template_writer, "~/Instrumented.cshtml", 93, 10, true);
+
+WriteLiteralTo(__razor_template_writer, "<p>Bar</p>");
+
+EndContext(__razor_template_writer, "~/Instrumented.cshtml", 93, 10, true);
+
+})
+
+#line 7 "Instrumented.cshtml"
+ ;
+
+
+#line default
+#line hidden
+BeginContext("~/Instrumented.cshtml", 106, 4, true);
+
+WriteLiteral(" ");
+
+EndContext("~/Instrumented.cshtml", 106, 4, true);
+
+BeginContext("~/Instrumented.cshtml", 112, 14, true);
+
+WriteLiteral("Hello, World\r\n");
+
+EndContext("~/Instrumented.cshtml", 112, 14, true);
+
+BeginContext("~/Instrumented.cshtml", 126, 25, true);
+
+WriteLiteral(" <p>Hello, World</p>\r\n");
+
+EndContext("~/Instrumented.cshtml", 126, 25, true);
+
+
+#line 10 "Instrumented.cshtml"
+
+
+#line default
+#line hidden
+BeginContext("~/Instrumented.cshtml", 152, 4, true);
+
+WriteLiteral("\r\n\r\n");
+
+EndContext("~/Instrumented.cshtml", 152, 4, true);
+
+
+#line 12 "Instrumented.cshtml"
+ while(i <= 10) {
+
+
+#line default
+#line hidden
+BeginContext("~/Instrumented.cshtml", 175, 23, true);
+
+WriteLiteral(" <p>Hello from C#, #");
+
+EndContext("~/Instrumented.cshtml", 175, 23, true);
+
+BeginContext("~/Instrumented.cshtml", 200, 1, false);
+
+
+#line 13 "Instrumented.cshtml"
+ Write(i);
+
+
+#line default
+#line hidden
+EndContext("~/Instrumented.cshtml", 200, 1, false);
+
+BeginContext("~/Instrumented.cshtml", 202, 6, true);
+
+WriteLiteral("</p>\r\n");
+
+EndContext("~/Instrumented.cshtml", 202, 6, true);
+
+
+#line 14 "Instrumented.cshtml"
+ i += 1;
+}
+
+
+#line default
+#line hidden
+BeginContext("~/Instrumented.cshtml", 224, 2, true);
+
+WriteLiteral("\r\n");
+
+EndContext("~/Instrumented.cshtml", 224, 2, true);
+
+
+#line 17 "Instrumented.cshtml"
+ if(i == 11) {
+
+
+#line default
+#line hidden
+BeginContext("~/Instrumented.cshtml", 242, 31, true);
+
+WriteLiteral(" <p>We wrote 10 lines!</p>\r\n");
+
+EndContext("~/Instrumented.cshtml", 242, 31, true);
+
+
+#line 19 "Instrumented.cshtml"
+}
+
+
+#line default
+#line hidden
+BeginContext("~/Instrumented.cshtml", 276, 2, true);
+
+WriteLiteral("\r\n");
+
+EndContext("~/Instrumented.cshtml", 276, 2, true);
+
+
+#line 21 "Instrumented.cshtml"
+ switch(i) {
+ case 11:
+
+
+#line default
+#line hidden
+BeginContext("~/Instrumented.cshtml", 306, 46, true);
+
+WriteLiteral(" <p>No really, we wrote 10 lines!</p>\r\n");
+
+EndContext("~/Instrumented.cshtml", 306, 46, true);
+
+
+#line 24 "Instrumented.cshtml"
+ break;
+ default:
+
+
+#line default
+#line hidden
+BeginContext("~/Instrumented.cshtml", 382, 39, true);
+
+WriteLiteral(" <p>Actually, we didn\'t...</p>\r\n");
+
+EndContext("~/Instrumented.cshtml", 382, 39, true);
+
+
+#line 27 "Instrumented.cshtml"
+ break;
+}
+
+
+#line default
+#line hidden
+BeginContext("~/Instrumented.cshtml", 440, 2, true);
+
+WriteLiteral("\r\n");
+
+EndContext("~/Instrumented.cshtml", 440, 2, true);
+
+
+#line 30 "Instrumented.cshtml"
+ for(int j = 1; j <= 10; j += 2) {
+
+
+#line default
+#line hidden
+BeginContext("~/Instrumented.cshtml", 478, 29, true);
+
+WriteLiteral(" <p>Hello again from C#, #");
+
+EndContext("~/Instrumented.cshtml", 478, 29, true);
+
+BeginContext("~/Instrumented.cshtml", 509, 1, false);
+
+
+#line 31 "Instrumented.cshtml"
+ Write(j);
+
+
+#line default
+#line hidden
+EndContext("~/Instrumented.cshtml", 509, 1, false);
+
+BeginContext("~/Instrumented.cshtml", 511, 6, true);
+
+WriteLiteral("</p>\r\n");
+
+EndContext("~/Instrumented.cshtml", 511, 6, true);
+
+
+#line 32 "Instrumented.cshtml"
+}
+
+
+#line default
+#line hidden
+BeginContext("~/Instrumented.cshtml", 520, 2, true);
+
+WriteLiteral("\r\n");
+
+EndContext("~/Instrumented.cshtml", 520, 2, true);
+
+
+#line 34 "Instrumented.cshtml"
+ try {
+
+
+#line default
+#line hidden
+BeginContext("~/Instrumented.cshtml", 530, 41, true);
+
+WriteLiteral(" <p>That time, we wrote 5 lines!</p>\r\n");
+
+EndContext("~/Instrumented.cshtml", 530, 41, true);
+
+
+#line 36 "Instrumented.cshtml"
+} catch(Exception ex) {
+
+
+#line default
+#line hidden
+BeginContext("~/Instrumented.cshtml", 596, 33, true);
+
+WriteLiteral(" <p>Oh no! An error occurred: ");
+
+EndContext("~/Instrumented.cshtml", 596, 33, true);
+
+BeginContext("~/Instrumented.cshtml", 631, 10, false);
+
+
+#line 37 "Instrumented.cshtml"
+ Write(ex.Message);
+
+
+#line default
+#line hidden
+EndContext("~/Instrumented.cshtml", 631, 10, false);
+
+BeginContext("~/Instrumented.cshtml", 642, 6, true);
+
+WriteLiteral("</p>\r\n");
+
+EndContext("~/Instrumented.cshtml", 642, 6, true);
+
+
+#line 38 "Instrumented.cshtml"
+}
+
+
+#line default
+#line hidden
+BeginContext("~/Instrumented.cshtml", 651, 2, true);
+
+WriteLiteral("\r\n");
+
+EndContext("~/Instrumented.cshtml", 651, 2, true);
+
+
+#line 40 "Instrumented.cshtml"
+ lock(new object()) {
+
+
+#line default
+#line hidden
+BeginContext("~/Instrumented.cshtml", 676, 53, true);
+
+WriteLiteral(" <p>This block is locked, for your security!</p>\r\n");
+
+EndContext("~/Instrumented.cshtml", 676, 53, true);
+
+
+#line 42 "Instrumented.cshtml"
+}
+
+#line default
+#line hidden
+}
+}
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/LayoutDirective.cs b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/LayoutDirective.cs
new file mode 100644
index 00000000..86c0e0f6
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/LayoutDirective.cs
@@ -0,0 +1,22 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:N.N.NNNNN.N
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace TestOutput {
+using System;
+
+public class LayoutDirective {
+#line hidden
+public LayoutDirective() {
+}
+public override void Execute() {
+Layout = "~/Foo/Bar/Baz";
+}
+}
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/MarkupInCodeBlock.cs b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/MarkupInCodeBlock.cs
new file mode 100644
index 00000000..14a1f096
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/MarkupInCodeBlock.cs
@@ -0,0 +1,49 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:N.N.NNNNN.N
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace TestOutput {
+using System;
+
+public class MarkupInCodeBlock {
+#line hidden
+public MarkupInCodeBlock() {
+}
+public override void Execute() {
+
+#line 1 "MarkupInCodeBlock.cshtml"
+
+ for(int i = 1; i <= 10; i++) {
+
+
+#line default
+#line hidden
+WriteLiteral(" <p>Hello from C#, #");
+
+
+#line 3 "MarkupInCodeBlock.cshtml"
+ Write(i.ToString());
+
+
+#line default
+#line hidden
+WriteLiteral("</p>\r\n");
+
+
+#line 4 "MarkupInCodeBlock.cshtml"
+ }
+
+
+#line default
+#line hidden
+WriteLiteral("\r\n");
+
+}
+}
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/NestedCodeBlocks.cs b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/NestedCodeBlocks.cs
new file mode 100644
index 00000000..424e26c4
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/NestedCodeBlocks.cs
@@ -0,0 +1,42 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:N.N.NNNNN.N
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace TestOutput {
+using System;
+
+public class NestedCodeBlocks {
+#line hidden
+public NestedCodeBlocks() {
+}
+public override void Execute() {
+
+#line 1 "NestedCodeBlocks.cshtml"
+ if(foo) {
+
+
+#line default
+#line hidden
+
+#line 2 "NestedCodeBlocks.cshtml"
+ if(bar) {
+ }
+
+#line default
+#line hidden
+
+#line 3 "NestedCodeBlocks.cshtml"
+
+}
+
+#line default
+#line hidden
+}
+}
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/NestedHelpers.cs b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/NestedHelpers.cs
new file mode 100644
index 00000000..76d02f53
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/NestedHelpers.cs
@@ -0,0 +1,100 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:N.N.NNNNN.N
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace TestOutput {
+using System;
+
+public class NestedHelpers {
+#line hidden
+#line 3 "NestedHelpers.cshtml"
+public static Template Bold(string s) {
+#line default
+#line hidden
+return new Template(__razor_helper_writer => {
+
+#line 3 "NestedHelpers.cshtml"
+
+ s = s.ToUpper();
+
+#line default
+#line hidden
+WriteLiteralTo(__razor_helper_writer, " <strong>");
+
+#line 5 "NestedHelpers.cshtml"
+WriteTo(__razor_helper_writer, s);
+
+#line default
+#line hidden
+WriteLiteralTo(__razor_helper_writer, "</strong>\r\n");
+
+#line 6 "NestedHelpers.cshtml"
+
+#line default
+#line hidden
+});
+
+#line 6 "NestedHelpers.cshtml"
+}
+#line default
+#line hidden
+
+#line 1 "NestedHelpers.cshtml"
+public static Template Italic(string s) {
+#line default
+#line hidden
+return new Template(__razor_helper_writer => {
+
+#line 1 "NestedHelpers.cshtml"
+
+ s = s.ToUpper();
+
+#line default
+#line hidden
+
+#line 6 "NestedHelpers.cshtml"
+
+
+#line default
+#line hidden
+WriteLiteralTo(__razor_helper_writer, " <em>");
+
+#line 7 "NestedHelpers.cshtml"
+WriteTo(__razor_helper_writer, Bold(s));
+
+#line default
+#line hidden
+WriteLiteralTo(__razor_helper_writer, "</em>\r\n");
+
+#line 8 "NestedHelpers.cshtml"
+
+#line default
+#line hidden
+});
+
+#line 8 "NestedHelpers.cshtml"
+}
+#line default
+#line hidden
+
+public NestedHelpers() {
+}
+public override void Execute() {
+WriteLiteral("\r\n");
+
+
+#line 10 "NestedHelpers.cshtml"
+Write(Italic("Hello"));
+
+
+#line default
+#line hidden
+}
+}
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/NoLinePragmas.cs b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/NoLinePragmas.cs
new file mode 100644
index 00000000..3fd500a5
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/NoLinePragmas.cs
@@ -0,0 +1,102 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:N.N.NNNNN.N
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace TestOutput {
+using System;
+
+public class NoLinePragmas {
+#line hidden
+public NoLinePragmas() {
+}
+public override void Execute() {
+
+ int i = 1;
+
+WriteLiteral("\r\n\r\n");
+
+ while(i <= 10) {
+
+WriteLiteral(" <p>Hello from C#, #");
+
+ Write(i);
+
+WriteLiteral("</p>\r\n");
+
+ i += 1;
+}
+
+WriteLiteral("\r\n");
+
+ if(i == 11) {
+
+WriteLiteral(" <p>We wrote 10 lines!</p>\r\n");
+
+}
+
+WriteLiteral("\r\n");
+
+ switch(i) {
+ case 11:
+
+WriteLiteral(" <p>No really, we wrote 10 lines!</p>\r\n");
+
+ break;
+ default:
+
+WriteLiteral(" <p>Actually, we didn\'t...</p>\r\n");
+
+ break;
+}
+
+WriteLiteral("\r\n");
+
+ for(int j = 1; j <= 10; j += 2) {
+
+WriteLiteral(" <p>Hello again from C#, #");
+
+ Write(j);
+
+WriteLiteral("</p>\r\n");
+
+}
+
+WriteLiteral("\r\n");
+
+ try {
+
+WriteLiteral(" <p>That time, we wrote 5 lines!</p>\r\n");
+
+} catch(Exception ex) {
+
+WriteLiteral(" <p>Oh no! An error occurred: ");
+
+ Write(ex.Message);
+
+WriteLiteral("</p>\r\n");
+
+}
+
+
+
+
+WriteLiteral("<p>i is now ");
+
+ Write(i);
+
+WriteLiteral("</p>\r\n\r\n");
+
+ lock(new object()) {
+
+WriteLiteral(" <p>This block is locked, for your security!</p>\r\n");
+
+}
+}
+}
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/ParserError.cs b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/ParserError.cs
new file mode 100644
index 00000000..79d25e62
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/ParserError.cs
@@ -0,0 +1,31 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:N.N.NNNNN.N
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace TestOutput {
+using System;
+
+public class ParserError {
+#line hidden
+public ParserError() {
+}
+public override void Execute() {
+
+#line 1 "ParserError.cshtml"
+
+/*
+int i =10;
+int j =20;
+}
+
+#line default
+#line hidden
+}
+}
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/RazorComments.DesignTime.cs b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/RazorComments.DesignTime.cs
new file mode 100644
index 00000000..dc732228
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/RazorComments.DesignTime.cs
@@ -0,0 +1,72 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:N.N.NNNNN.N
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace TestOutput {
+using System;
+
+public class RazorComments {
+private static object @__o;
+#line hidden
+public RazorComments() {
+}
+public override void Execute() {
+
+#line 1 "RazorComments.cshtml"
+
+
+
+#line default
+#line hidden
+
+#line 2 "RazorComments.cshtml"
+
+ Exception foo =
+
+#line default
+#line hidden
+
+#line 3 "RazorComments.cshtml"
+ null;
+ if(foo != null) {
+ throw foo;
+ }
+
+
+#line default
+#line hidden
+
+#line 4 "RazorComments.cshtml"
+ var bar = "@* bar *@";
+
+#line default
+#line hidden
+
+#line 5 "RazorComments.cshtml"
+ __o = bar;
+
+
+#line default
+#line hidden
+
+#line 6 "RazorComments.cshtml"
+__o = a
+
+#line default
+#line hidden
+
+#line 7 "RazorComments.cshtml"
+ b;
+
+
+#line default
+#line hidden
+}
+}
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/RazorComments.cs b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/RazorComments.cs
new file mode 100644
index 00000000..72d45d60
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/RazorComments.cs
@@ -0,0 +1,83 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:N.N.NNNNN.N
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace TestOutput {
+using System;
+
+public class RazorComments {
+#line hidden
+public RazorComments() {
+}
+public override void Execute() {
+WriteLiteral("\r\n<p>This should ");
+
+WriteLiteral(" be shown</p>\r\n\r\n");
+
+
+#line 4 "RazorComments.cshtml"
+
+
+
+#line default
+#line hidden
+
+#line 5 "RazorComments.cshtml"
+
+ Exception foo =
+
+#line default
+#line hidden
+
+#line 6 "RazorComments.cshtml"
+ null;
+ if(foo != null) {
+ throw foo;
+ }
+
+
+#line default
+#line hidden
+WriteLiteral("\r\n\r\n");
+
+
+#line 12 "RazorComments.cshtml"
+ var bar = "@* bar *@";
+
+#line default
+#line hidden
+WriteLiteral("\r\n<p>But this should show the comment syntax: ");
+
+
+#line 13 "RazorComments.cshtml"
+ Write(bar);
+
+
+#line default
+#line hidden
+WriteLiteral("</p>\r\n\r\n");
+
+
+#line 15 "RazorComments.cshtml"
+Write(a
+
+#line default
+#line hidden
+
+#line 15 "RazorComments.cshtml"
+ b);
+
+
+#line default
+#line hidden
+WriteLiteral("\r\n");
+
+}
+}
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/ResolveUrl.cs b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/ResolveUrl.cs
new file mode 100644
index 00000000..f99f1ec1
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/ResolveUrl.cs
@@ -0,0 +1,230 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:N.N.NNNNN.N
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace TestOutput {
+using System;
+
+public class ResolveUrl {
+#line hidden
+public ResolveUrl() {
+}
+public override void Execute() {
+WriteLiteral("<a");
+
+WriteAttribute("href", Tuple.Create(" href=\"", 2), Tuple.Create("\"", 14)
+, Tuple.Create(Tuple.Create("", 9), Tuple.Create<System.Object, System.Int32>(Href("~/Foo")
+, 9), false)
+);
+
+WriteLiteral(">Foo</a>\r\n<a");
+
+WriteAttribute("href", Tuple.Create(" href=\"", 27), Tuple.Create("\"", 56)
+, Tuple.Create(Tuple.Create("", 34), Tuple.Create<System.Object, System.Int32>(Href("~/Products/")
+, 34), false)
+
+#line 2 "ResolveUrl.cshtml"
+, Tuple.Create(Tuple.Create("", 45), Tuple.Create<System.Object, System.Int32>(product.id
+
+#line default
+#line hidden
+, 45), false)
+);
+
+WriteLiteral(">");
+
+
+#line 2 "ResolveUrl.cshtml"
+ Write(product.Name);
+
+
+#line default
+#line hidden
+WriteLiteral("</a>\r\n<a");
+
+WriteAttribute("href", Tuple.Create(" href=\"", 79), Tuple.Create("\"", 115)
+, Tuple.Create(Tuple.Create("", 86), Tuple.Create<System.Object, System.Int32>(Href("~/Products/")
+, 86), false)
+
+#line 3 "ResolveUrl.cshtml"
+, Tuple.Create(Tuple.Create("", 97), Tuple.Create<System.Object, System.Int32>(product.id
+
+#line default
+#line hidden
+, 97), false)
+, Tuple.Create(Tuple.Create("", 108), Tuple.Create("/Detail", 108), true)
+);
+
+WriteLiteral(">Details</a>\r\n<a");
+
+WriteAttribute("href", Tuple.Create(" href=\"", 132), Tuple.Create("\"", 187)
+, Tuple.Create(Tuple.Create("", 139), Tuple.Create<System.Object, System.Int32>(Href("~/A+Really(Crazy),Url.Is:This/")
+, 139), false)
+
+#line 4 "ResolveUrl.cshtml"
+, Tuple.Create(Tuple.Create("", 169), Tuple.Create<System.Object, System.Int32>(product.id
+
+#line default
+#line hidden
+, 169), false)
+, Tuple.Create(Tuple.Create("", 180), Tuple.Create("/Detail", 180), true)
+);
+
+WriteLiteral(">Crazy Url!</a>\r\n\r\n");
+
+
+#line 6 "ResolveUrl.cshtml"
+
+
+
+#line default
+#line hidden
+WriteLiteral(" ");
+
+WriteLiteral("\r\n <a");
+
+WriteAttribute("href", Tuple.Create(" href=\"", 233), Tuple.Create("\"", 245)
+, Tuple.Create(Tuple.Create("", 240), Tuple.Create<System.Object, System.Int32>(Href("~/Foo")
+, 240), false)
+);
+
+WriteLiteral(">Foo</a>\r\n <a");
+
+WriteAttribute("href", Tuple.Create(" href=\"", 266), Tuple.Create("\"", 295)
+, Tuple.Create(Tuple.Create("", 273), Tuple.Create<System.Object, System.Int32>(Href("~/Products/")
+, 273), false)
+
+#line 9 "ResolveUrl.cshtml"
+, Tuple.Create(Tuple.Create("", 284), Tuple.Create<System.Object, System.Int32>(product.id
+
+#line default
+#line hidden
+, 284), false)
+);
+
+WriteLiteral(">");
+
+
+#line 9 "ResolveUrl.cshtml"
+ Write(product.Name);
+
+
+#line default
+#line hidden
+WriteLiteral("</a>\r\n <a");
+
+WriteAttribute("href", Tuple.Create(" href=\"", 326), Tuple.Create("\"", 362)
+, Tuple.Create(Tuple.Create("", 333), Tuple.Create<System.Object, System.Int32>(Href("~/Products/")
+, 333), false)
+
+#line 10 "ResolveUrl.cshtml"
+, Tuple.Create(Tuple.Create("", 344), Tuple.Create<System.Object, System.Int32>(product.id
+
+#line default
+#line hidden
+, 344), false)
+, Tuple.Create(Tuple.Create("", 355), Tuple.Create("/Detail", 355), true)
+);
+
+WriteLiteral(">Details</a>\r\n <a");
+
+WriteAttribute("href", Tuple.Create(" href=\"", 387), Tuple.Create("\"", 442)
+, Tuple.Create(Tuple.Create("", 394), Tuple.Create<System.Object, System.Int32>(Href("~/A+Really(Crazy),Url.Is:This/")
+, 394), false)
+
+#line 11 "ResolveUrl.cshtml"
+, Tuple.Create(Tuple.Create("", 424), Tuple.Create<System.Object, System.Int32>(product.id
+
+#line default
+#line hidden
+, 424), false)
+, Tuple.Create(Tuple.Create("", 435), Tuple.Create("/Detail", 435), true)
+);
+
+WriteLiteral(">Crazy Url!</a>\r\n ");
+
+WriteLiteral("\r\n");
+
+
+#line 13 "ResolveUrl.cshtml"
+
+
+#line default
+#line hidden
+WriteLiteral("\r\n\r\n");
+
+DefineSection("Foo", () => {
+
+WriteLiteral("\r\n <a");
+
+WriteAttribute("href", Tuple.Create(" href=\"", 500), Tuple.Create("\"", 512)
+, Tuple.Create(Tuple.Create("", 507), Tuple.Create<System.Object, System.Int32>(Href("~/Foo")
+, 507), false)
+);
+
+WriteLiteral(">Foo</a>\r\n <a");
+
+WriteAttribute("href", Tuple.Create(" href=\"", 529), Tuple.Create("\"", 558)
+, Tuple.Create(Tuple.Create("", 536), Tuple.Create<System.Object, System.Int32>(Href("~/Products/")
+, 536), false)
+
+#line 17 "ResolveUrl.cshtml"
+, Tuple.Create(Tuple.Create("", 547), Tuple.Create<System.Object, System.Int32>(product.id
+
+#line default
+#line hidden
+, 547), false)
+);
+
+WriteLiteral(">");
+
+
+#line 17 "ResolveUrl.cshtml"
+ Write(product.Name);
+
+
+#line default
+#line hidden
+WriteLiteral("</a>\r\n <a");
+
+WriteAttribute("href", Tuple.Create(" href=\"", 585), Tuple.Create("\"", 621)
+, Tuple.Create(Tuple.Create("", 592), Tuple.Create<System.Object, System.Int32>(Href("~/Products/")
+, 592), false)
+
+#line 18 "ResolveUrl.cshtml"
+, Tuple.Create(Tuple.Create("", 603), Tuple.Create<System.Object, System.Int32>(product.id
+
+#line default
+#line hidden
+, 603), false)
+, Tuple.Create(Tuple.Create("", 614), Tuple.Create("/Detail", 614), true)
+);
+
+WriteLiteral(">Details</a>\r\n <a");
+
+WriteAttribute("href", Tuple.Create(" href=\"", 642), Tuple.Create("\"", 697)
+, Tuple.Create(Tuple.Create("", 649), Tuple.Create<System.Object, System.Int32>(Href("~/A+Really(Crazy),Url.Is:This/")
+, 649), false)
+
+#line 19 "ResolveUrl.cshtml"
+, Tuple.Create(Tuple.Create("", 679), Tuple.Create<System.Object, System.Int32>(product.id
+
+#line default
+#line hidden
+, 679), false)
+, Tuple.Create(Tuple.Create("", 690), Tuple.Create("/Detail", 690), true)
+);
+
+WriteLiteral(">Crazy Url!</a>\r\n");
+
+});
+
+}
+}
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Sections.cs b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Sections.cs
new file mode 100644
index 00000000..13f24b73
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Sections.cs
@@ -0,0 +1,45 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:N.N.NNNNN.N
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace TestOutput {
+using System;
+
+public class Sections {
+#line hidden
+public Sections() {
+}
+public override void Execute() {
+
+#line 1 "Sections.cshtml"
+
+ Layout = "_SectionTestLayout.cshtml"
+
+
+#line default
+#line hidden
+WriteLiteral("\r\n\r\n<div>This is in the Body>\r\n\r\n");
+
+DefineSection("Section2", () => {
+
+WriteLiteral("\r\n <div>This is in Section 2</div>\r\n");
+
+});
+
+WriteLiteral("\r\n");
+
+DefineSection("Section1", () => {
+
+WriteLiteral("\r\n <div>This is in Section 1</div>\r\n");
+
+});
+
+}
+}
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Templates.cs b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Templates.cs
new file mode 100644
index 00000000..8131acf7
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/Templates.cs
@@ -0,0 +1,180 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:N.N.NNNNN.N
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace TestOutput {
+using System;
+
+public class Templates {
+#line hidden
+#line 1 "Templates.cshtml"
+
+ public HelperResult Repeat(int times, Func<int, object> template) {
+ return new HelperResult((writer) => {
+ for(int i = 0; i < times; i++) {
+ ((HelperResult)template(i)).WriteTo(writer);
+ }
+ });
+ }
+
+#line default
+#line hidden
+
+public Templates() {
+}
+public override void Execute() {
+WriteLiteral("\r\n");
+
+
+#line 11 "Templates.cshtml"
+
+ Func<dynamic, object> foo =
+
+#line default
+#line hidden
+item => new Template(__razor_template_writer => {
+
+WriteLiteralTo(__razor_template_writer, "This works ");
+
+
+#line 12 "Templates.cshtml"
+ WriteTo(__razor_template_writer, item);
+
+
+#line default
+#line hidden
+WriteLiteralTo(__razor_template_writer, "!");
+
+})
+
+#line 12 "Templates.cshtml"
+ ;
+
+
+#line default
+#line hidden
+
+#line 13 "Templates.cshtml"
+Write(foo(""));
+
+
+#line default
+#line hidden
+
+#line 13 "Templates.cshtml"
+
+
+
+#line default
+#line hidden
+WriteLiteral("\r\n\r\n<ul>\r\n");
+
+
+#line 17 "Templates.cshtml"
+Write(Repeat(10, item => new Template(__razor_template_writer => {
+
+
+#line default
+#line hidden
+WriteLiteralTo(__razor_template_writer, "<li>Item #");
+
+
+#line 17 "Templates.cshtml"
+WriteTo(__razor_template_writer, item);
+
+
+#line default
+#line hidden
+WriteLiteralTo(__razor_template_writer, "</li>");
+
+
+#line 17 "Templates.cshtml"
+ })));
+
+
+#line default
+#line hidden
+WriteLiteral("\r\n</ul>\r\n\r\n<p>\r\n");
+
+
+#line 21 "Templates.cshtml"
+Write(Repeat(10,
+ item => new Template(__razor_template_writer => {
+
+
+#line default
+#line hidden
+WriteLiteralTo(__razor_template_writer, " This is line#");
+
+
+#line 22 "Templates.cshtml"
+WriteTo(__razor_template_writer, item);
+
+
+#line default
+#line hidden
+WriteLiteralTo(__razor_template_writer, " of markup<br/>\r\n");
+
+
+#line 23 "Templates.cshtml"
+})));
+
+
+#line default
+#line hidden
+WriteLiteral("\r\n</p>\r\n\r\n<ul>\r\n");
+
+WriteLiteral(" ");
+
+
+#line 27 "Templates.cshtml"
+Write(Repeat(10, item => new Template(__razor_template_writer => {
+
+
+#line default
+#line hidden
+WriteLiteralTo(__razor_template_writer, "<li>\r\n Item #");
+
+
+#line 28 "Templates.cshtml"
+WriteTo(__razor_template_writer, item);
+
+
+#line default
+#line hidden
+WriteLiteralTo(__razor_template_writer, "\r\n");
+
+
+#line 29 "Templates.cshtml"
+
+
+#line default
+#line hidden
+
+#line 29 "Templates.cshtml"
+ var parent = item;
+
+#line default
+#line hidden
+WriteLiteralTo(__razor_template_writer, "\r\n <ul>\r\n <li>Child Items... ?</li>\r\n ");
+
+WriteLiteralTo(__razor_template_writer, "\r\n </ul>\r\n </li>");
+
+
+#line 34 "Templates.cshtml"
+ })));
+
+
+#line default
+#line hidden
+WriteLiteral("\r\n</ul> ");
+
+}
+}
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/UnfinishedExpressionInCode.cs b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/UnfinishedExpressionInCode.cs
new file mode 100644
index 00000000..2a469eea
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Output/UnfinishedExpressionInCode.cs
@@ -0,0 +1,43 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:N.N.NNNNN.N
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace TestOutput {
+using System;
+
+public class UnfinishedExpressionInCode {
+private static object @__o;
+#line hidden
+public UnfinishedExpressionInCode() {
+}
+public override void Execute() {
+
+#line 1 "UnfinishedExpressionInCode.cshtml"
+
+
+
+#line default
+#line hidden
+
+#line 2 "UnfinishedExpressionInCode.cshtml"
+__o = DateTime.;
+
+
+#line default
+#line hidden
+
+#line 3 "UnfinishedExpressionInCode.cshtml"
+
+
+
+#line default
+#line hidden
+}
+}
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/Blocks.cshtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/Blocks.cshtml
new file mode 100644
index 00000000..8d27de89
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/Blocks.cshtml
@@ -0,0 +1,37 @@
+@{
+ int i = 1;
+}
+
+@while(i <= 10) {
+ <p>Hello from C#, #@(i)</p>
+ i += 1;
+}
+
+@if(i == 11) {
+ <p>We wrote 10 lines!</p>
+}
+
+@switch(i) {
+ case 11:
+ <p>No really, we wrote 10 lines!</p>
+ break;
+ default:
+ <p>Actually, we didn't...</p>
+ break;
+}
+
+@for(int j = 1; j <= 10; j += 2) {
+ <p>Hello again from C#, #@(j)</p>
+}
+
+@try {
+ <p>That time, we wrote 5 lines!</p>
+} catch(Exception ex) {
+ <p>Oh no! An error occurred: @(ex.Message)</p>
+}
+
+<p>i is now @i</p>
+
+@lock(new object()) {
+ <p>This block is locked, for your security!</p>
+} \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/CodeBlock.cshtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/CodeBlock.cshtml
new file mode 100644
index 00000000..1c78883a
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/CodeBlock.cshtml
@@ -0,0 +1,5 @@
+@{
+ for(int i = 1; i <= 10; i++) {
+ Output.Write("<p>Hello from C#, #" + i.ToString() + "</p>");
+ }
+} \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/CodeBlockAtEOF.cshtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/CodeBlockAtEOF.cshtml
new file mode 100644
index 00000000..38417d48
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/CodeBlockAtEOF.cshtml
@@ -0,0 +1 @@
+@{ \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/ConditionalAttributes.cshtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/ConditionalAttributes.cshtml
new file mode 100644
index 00000000..be1a9c20
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/ConditionalAttributes.cshtml
@@ -0,0 +1,15 @@
+@{
+ var ch = true;
+ var cls = "bar";
+ <a href="Foo" />
+ <p class="@cls" />
+ <p class="foo @cls" />
+ <p class="@cls foo" />
+ <input type="checkbox" checked="@ch" />
+ <input type="checkbox" checked="foo @ch" />
+ <p class="@if(cls != null) { @cls }" />
+ <a href="~/Foo" />
+ <script src="@Url.Content("~/Scripts/jquery-1.6.2.min.js")" type="text/javascript"></script>
+ <script src="@Url.Content("~/Scripts/modernizr-2.0.6-development-only.js")" type="text/javascript"></script>
+ <script src="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.16/jquery-ui.min.js" type="text/javascript"></script>
+} \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/DesignTime.cshtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/DesignTime.cshtml
new file mode 100644
index 00000000..581b164d
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/DesignTime.cshtml
@@ -0,0 +1,21 @@
+<div>
+ @for(int i = 1; i <= 10; i++) {
+ <p>This is item #@i</p>
+ }
+</div>
+
+<p>
+@(Foo(Bar.Baz))
+@Foo(@<p>Bar @baz Biz</p>)
+</p>
+
+@section Footer {
+ <p>Foo</p>
+ @bar
+}
+
+@helper Foo() {
+ if(true) {
+ <p>Foo</p>
+ }
+} \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/EmptyCodeBlock.cshtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/EmptyCodeBlock.cshtml
new file mode 100644
index 00000000..0366199c
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/EmptyCodeBlock.cshtml
@@ -0,0 +1,3 @@
+This is markup
+
+@{} \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/EmptyExplicitExpression.cshtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/EmptyExplicitExpression.cshtml
new file mode 100644
index 00000000..6790c7eb
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/EmptyExplicitExpression.cshtml
@@ -0,0 +1,3 @@
+This is markup
+
+@() \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/EmptyImplicitExpression.cshtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/EmptyImplicitExpression.cshtml
new file mode 100644
index 00000000..021306da
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/EmptyImplicitExpression.cshtml
@@ -0,0 +1,3 @@
+This is markup
+
+@! \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/EmptyImplicitExpressionInCode.cshtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/EmptyImplicitExpressionInCode.cshtml
new file mode 100644
index 00000000..a1db8cd6
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/EmptyImplicitExpressionInCode.cshtml
@@ -0,0 +1,3 @@
+@{
+ @
+} \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/ExplicitExpression.cshtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/ExplicitExpression.cshtml
new file mode 100644
index 00000000..10730f11
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/ExplicitExpression.cshtml
@@ -0,0 +1 @@
+1 + 1 = @(1+1) \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/ExplicitExpressionAtEOF.cshtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/ExplicitExpressionAtEOF.cshtml
new file mode 100644
index 00000000..a0fdfc9a
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/ExplicitExpressionAtEOF.cshtml
@@ -0,0 +1,3 @@
+This is markup
+
+@( \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/ExpressionsInCode.cshtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/ExpressionsInCode.cshtml
new file mode 100644
index 00000000..a4d4caa0
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/ExpressionsInCode.cshtml
@@ -0,0 +1,16 @@
+@{
+ object foo = null;
+ string bar = "Foo";
+}
+
+@if(foo != null) {
+ @foo
+} else {
+ <p>Foo is Null!</p>
+}
+
+<p>
+@if(!String.IsNullOrEmpty(bar)) {
+ @(bar.Replace("F", "B"))
+}
+</p> \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/FunctionsBlock.cshtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/FunctionsBlock.cshtml
new file mode 100644
index 00000000..5d06b372
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/FunctionsBlock.cshtml
@@ -0,0 +1,12 @@
+@functions {
+
+}
+
+@functions {
+ Random _rand = new Random();
+ private int RandomInt() {
+ return _rand.Next();
+ }
+}
+
+Here's a random number: @RandomInt() \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/Helpers.cshtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/Helpers.cshtml
new file mode 100644
index 00000000..7ad443fe
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/Helpers.cshtml
@@ -0,0 +1,11 @@
+@helper Bold(string s) {
+ s = s.ToUpper();
+ <strong>@s</strong>
+}
+
+@helper Italic(string s) {
+ s = s.ToUpper();
+ <em>@s</em>
+}
+
+@Bold("Hello") \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/HelpersMissingCloseParen.cshtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/HelpersMissingCloseParen.cshtml
new file mode 100644
index 00000000..e787dea2
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/HelpersMissingCloseParen.cshtml
@@ -0,0 +1,7 @@
+@helper Bold(string s) {
+ s = s.ToUpper();
+ <strong>@s</strong>
+}
+
+@helper Italic(string s
+@Bold("Hello") \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/HelpersMissingName.cshtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/HelpersMissingName.cshtml
new file mode 100644
index 00000000..504cbee6
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/HelpersMissingName.cshtml
@@ -0,0 +1 @@
+@helper \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/HelpersMissingOpenBrace.cshtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/HelpersMissingOpenBrace.cshtml
new file mode 100644
index 00000000..b9702e38
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/HelpersMissingOpenBrace.cshtml
@@ -0,0 +1,7 @@
+@helper Bold(string s) {
+ s = s.ToUpper();
+ <strong>@s</strong>
+}
+
+@helper Italic(string s)
+@Italic(s) \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/HelpersMissingOpenParen.cshtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/HelpersMissingOpenParen.cshtml
new file mode 100644
index 00000000..f3825f7e
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/HelpersMissingOpenParen.cshtml
@@ -0,0 +1,7 @@
+@helper Bold(string s) {
+ s = s.ToUpper();
+ <strong>@s</strong>
+}
+
+@helper Italic
+@Bold("Hello") \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/HiddenSpansInCode.cshtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/HiddenSpansInCode.cshtml
new file mode 100644
index 00000000..a6addbe9
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/HiddenSpansInCode.cshtml
@@ -0,0 +1,3 @@
+@{
+ @@Da
+} \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/ImplicitExpression.cshtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/ImplicitExpression.cshtml
new file mode 100644
index 00000000..dcce7fa5
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/ImplicitExpression.cshtml
@@ -0,0 +1,3 @@
+@for(int i = 1; i <= 10; i++) {
+ <p>This is item #@i</p>
+} \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/ImplicitExpressionAtEOF.cshtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/ImplicitExpressionAtEOF.cshtml
new file mode 100644
index 00000000..365d20e0
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/ImplicitExpressionAtEOF.cshtml
@@ -0,0 +1,3 @@
+This is markup
+
+@ \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/Imports.cshtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/Imports.cshtml
new file mode 100644
index 00000000..332e26f0
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/Imports.cshtml
@@ -0,0 +1,6 @@
+@using System.IO
+@using Foo = System.Text.Encoding
+@using System
+
+<p>Path's full type name is @typeof(Path).FullName</p>
+<p>Foo's actual full type name is @typeof(Foo).FullName</p> \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/Inherits.cshtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/Inherits.cshtml
new file mode 100644
index 00000000..b449937d
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/Inherits.cshtml
@@ -0,0 +1,3 @@
+@foo()
+
+@inherits foo.bar<baz<biz>>.boz bar
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/InlineBlocks.cshtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/InlineBlocks.cshtml
new file mode 100644
index 00000000..0a8b3d8f
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/InlineBlocks.cshtml
@@ -0,0 +1,3 @@
+@helper Link(string link) {
+ <a href="@if(link != null) { @link } else { <text>#</text> }" />
+} \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/Instrumented.cshtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/Instrumented.cshtml
new file mode 100644
index 00000000..4d9b03eb
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/Instrumented.cshtml
@@ -0,0 +1,42 @@
+@helper Strong(string s) {
+ <strong>@s</strong>
+}
+
+@{
+ int i = 1;
+ var foo = @<p>Bar</p>;
+ @:Hello, World
+ <p>Hello, World</p>
+}
+
+@while(i <= 10) {
+ <p>Hello from C#, #@(i)</p>
+ i += 1;
+}
+
+@if(i == 11) {
+ <p>We wrote 10 lines!</p>
+}
+
+@switch(i) {
+ case 11:
+ <p>No really, we wrote 10 lines!</p>
+ break;
+ default:
+ <p>Actually, we didn't...</p>
+ break;
+}
+
+@for(int j = 1; j <= 10; j += 2) {
+ <p>Hello again from C#, #@(j)</p>
+}
+
+@try {
+ <p>That time, we wrote 5 lines!</p>
+} catch(Exception ex) {
+ <p>Oh no! An error occurred: @(ex.Message)</p>
+}
+
+@lock(new object()) {
+ <p>This block is locked, for your security!</p>
+} \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/LayoutDirective.cshtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/LayoutDirective.cshtml
new file mode 100644
index 00000000..155c550b
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/LayoutDirective.cshtml
@@ -0,0 +1 @@
+@layout ~/Foo/Bar/Baz \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/MarkupInCodeBlock.cshtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/MarkupInCodeBlock.cshtml
new file mode 100644
index 00000000..712ef428
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/MarkupInCodeBlock.cshtml
@@ -0,0 +1,5 @@
+@{
+ for(int i = 1; i <= 10; i++) {
+ <p>Hello from C#, #@(i.ToString())</p>
+ }
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/NestedCodeBlocks.cshtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/NestedCodeBlocks.cshtml
new file mode 100644
index 00000000..070875f5
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/NestedCodeBlocks.cshtml
@@ -0,0 +1,4 @@
+@if(foo) {
+ @if(bar) {
+ }
+} \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/NestedHelpers.cshtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/NestedHelpers.cshtml
new file mode 100644
index 00000000..63158ff0
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/NestedHelpers.cshtml
@@ -0,0 +1,10 @@
+@helper Italic(string s) {
+ s = s.ToUpper();
+ @helper Bold(string s) {
+ s = s.ToUpper();
+ <strong>@s</strong>
+ }
+ <em>@Bold(s)</em>
+}
+
+@Italic("Hello") \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/NoLinePragmas.cshtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/NoLinePragmas.cshtml
new file mode 100644
index 00000000..36e96c46
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/NoLinePragmas.cshtml
@@ -0,0 +1,38 @@
+@{
+ int i = 1;
+}
+
+@while(i <= 10) {
+ <p>Hello from C#, #@(i)</p>
+ i += 1;
+}
+
+@if(i == 11) {
+ <p>We wrote 10 lines!</p>
+}
+
+@switch(i) {
+ case 11:
+ <p>No really, we wrote 10 lines!</p>
+ break;
+ default:
+ <p>Actually, we didn't...</p>
+ break;
+}
+
+@for(int j = 1; j <= 10; j += 2) {
+ <p>Hello again from C#, #@(j)</p>
+}
+
+@try {
+ <p>That time, we wrote 5 lines!</p>
+} catch(Exception ex) {
+ <p>Oh no! An error occurred: @(ex.Message)</p>
+}
+
+@* With has no equivalent in C# *@
+<p>i is now @i</p>
+
+@lock(new object()) {
+ <p>This block is locked, for your security!</p>
+} \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/ParserError.cshtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/ParserError.cshtml
new file mode 100644
index 00000000..ab30e853
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/ParserError.cshtml
@@ -0,0 +1,5 @@
+@{
+/*
+int i =10;
+int j =20;
+} \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/RazorComments.cshtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/RazorComments.cshtml
new file mode 100644
index 00000000..bd4820f9
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/RazorComments.cshtml
@@ -0,0 +1,15 @@
+@*This is not going to be rendered*@
+<p>This should @* not *@ be shown</p>
+
+@{
+ @* throw new Exception("Oh no!") *@
+ Exception foo = @* new Exception("Oh no!") *@ null;
+ if(foo != null) {
+ throw foo;
+ }
+}
+
+@{ var bar = "@* bar *@"; }
+<p>But this should show the comment syntax: @bar</p>
+
+@(a@**@b)
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/ResolveUrl.cshtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/ResolveUrl.cshtml
new file mode 100644
index 00000000..fc8c77cf
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/ResolveUrl.cshtml
@@ -0,0 +1,20 @@
+<a href="~/Foo">Foo</a>
+<a href="~/Products/@product.id">@product.Name</a>
+<a href="~/Products/@product.id/Detail">Details</a>
+<a href="~/A+Really(Crazy),Url.Is:This/@product.id/Detail">Crazy Url!</a>
+
+@{
+ <text>
+ <a href="~/Foo">Foo</a>
+ <a href="~/Products/@product.id">@product.Name</a>
+ <a href="~/Products/@product.id/Detail">Details</a>
+ <a href="~/A+Really(Crazy),Url.Is:This/@product.id/Detail">Crazy Url!</a>
+ </text>
+}
+
+@section Foo {
+ <a href="~/Foo">Foo</a>
+ <a href="~/Products/@product.id">@product.Name</a>
+ <a href="~/Products/@product.id/Detail">Details</a>
+ <a href="~/A+Really(Crazy),Url.Is:This/@product.id/Detail">Crazy Url!</a>
+} \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/Sections.cshtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/Sections.cshtml
new file mode 100644
index 00000000..1d8cbefe
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/Sections.cshtml
@@ -0,0 +1,13 @@
+@{
+ Layout = "_SectionTestLayout.cshtml"
+}
+
+<div>This is in the Body>
+
+@section Section2 {
+ <div>This is in Section 2</div>
+}
+
+@section Section1 {
+ <div>This is in Section 1</div>
+} \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/Templates.cshtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/Templates.cshtml
new file mode 100644
index 00000000..c9523f0b
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/Templates.cshtml
@@ -0,0 +1,35 @@
+@functions {
+ public HelperResult Repeat(int times, Func<int, object> template) {
+ return new HelperResult((writer) => {
+ for(int i = 0; i < times; i++) {
+ ((HelperResult)template(i)).WriteTo(writer);
+ }
+ });
+ }
+}
+
+@{
+ Func<dynamic, object> foo = @<text>This works @item!</text>;
+ @foo("")
+}
+
+<ul>
+@(Repeat(10, @<li>Item #@item</li>))
+</ul>
+
+<p>
+@Repeat(10,
+ @: This is line#@item of markup<br/>
+)
+</p>
+
+<ul>
+ @Repeat(10, @<li>
+ Item #@item
+ @{var parent = item;}
+ <ul>
+ <li>Child Items... ?</li>
+ @*Repeat(10, @<li>Item #@(parent).@item</li>)*@
+ </ul>
+ </li>)
+</ul> \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/UnfinishedExpressionInCode.cshtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/UnfinishedExpressionInCode.cshtml
new file mode 100644
index 00000000..bf1d7964
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/CS/Source/UnfinishedExpressionInCode.cshtml
@@ -0,0 +1,3 @@
+@{
+@DateTime.
+} \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Blocks.vb b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Blocks.vb
new file mode 100644
index 00000000..d0dc84d5
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Blocks.vb
@@ -0,0 +1,255 @@
+'------------------------------------------------------------------------------
+' <auto-generated>
+' This code was generated by a tool.
+' Runtime Version:N.N.NNNNN.N
+'
+' Changes to this file may cause incorrect behavior and will be lost if
+' the code is regenerated.
+' </auto-generated>
+'------------------------------------------------------------------------------
+
+Option Strict Off
+Option Explicit On
+
+Imports System
+
+Namespace TestOutput
+Public Class Blocks
+Public Sub New()
+MyBase.New
+End Sub
+Public Overrides Sub Execute()
+
+#ExternalSource("Blocks.vbhtml",1)
+
+ Dim i as Integer = 1
+
+
+#End ExternalSource
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+
+#ExternalSource("Blocks.vbhtml",5)
+ While i <= 10
+
+
+#End ExternalSource
+WriteLiteral(" ")
+
+WriteLiteral("<p>Hello from VB.Net, #")
+
+
+#ExternalSource("Blocks.vbhtml",6)
+ Write(i)
+
+
+#End ExternalSource
+WriteLiteral("</p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+
+#ExternalSource("Blocks.vbhtml",7)
+ i += 1
+End While
+
+
+#End ExternalSource
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+
+#ExternalSource("Blocks.vbhtml",10)
+ If i = 11 Then
+
+
+#End ExternalSource
+WriteLiteral(" ")
+
+WriteLiteral("<p>We wrote 10 lines!</p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+
+#ExternalSource("Blocks.vbhtml",12)
+End If
+
+
+#End ExternalSource
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+
+#ExternalSource("Blocks.vbhtml",14)
+ Do
+
+
+#End ExternalSource
+WriteLiteral(" ")
+
+WriteLiteral("<p>Hello again: ")
+
+
+#ExternalSource("Blocks.vbhtml",15)
+ Write(i)
+
+
+#End ExternalSource
+WriteLiteral("</p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+
+#ExternalSource("Blocks.vbhtml",16)
+ i -= 1
+Loop While i > 0
+
+
+#End ExternalSource
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+
+#ExternalSource("Blocks.vbhtml",19)
+ Select Case i
+ Case 11
+
+
+#End ExternalSource
+WriteLiteral(" ")
+
+WriteLiteral("<p>No really, we wrote 10 lines!</p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+
+#ExternalSource("Blocks.vbhtml",22)
+ Case Else
+
+
+#End ExternalSource
+WriteLiteral(" ")
+
+WriteLiteral("<p>We wrote a bunch more lines too!</p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+
+#ExternalSource("Blocks.vbhtml",24)
+End Select
+
+
+#End ExternalSource
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+
+#ExternalSource("Blocks.vbhtml",26)
+ For j as Integer = 1 to 10 Step 2
+
+
+#End ExternalSource
+WriteLiteral(" ")
+
+WriteLiteral("<p>Hello again from VB.Net, #")
+
+
+#ExternalSource("Blocks.vbhtml",27)
+ Write(j)
+
+
+#End ExternalSource
+WriteLiteral("</p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+
+#ExternalSource("Blocks.vbhtml",28)
+Next j
+
+
+#End ExternalSource
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+
+#ExternalSource("Blocks.vbhtml",30)
+ Try
+
+
+#End ExternalSource
+WriteLiteral(" ")
+
+WriteLiteral("<p>That time, we wrote 5 lines!</p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+
+#ExternalSource("Blocks.vbhtml",32)
+Catch ex as Exception
+
+
+#End ExternalSource
+WriteLiteral(" ")
+
+WriteLiteral("<p>Oh no! An error occurred: ")
+
+
+#ExternalSource("Blocks.vbhtml",33)
+ Write(ex.Message)
+
+
+#End ExternalSource
+WriteLiteral("</p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+
+#ExternalSource("Blocks.vbhtml",34)
+End Try
+
+
+#End ExternalSource
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+
+#ExternalSource("Blocks.vbhtml",36)
+ With i
+
+
+#End ExternalSource
+WriteLiteral(" ")
+
+WriteLiteral("<p>i is now ")
+
+
+#ExternalSource("Blocks.vbhtml",37)
+ Write(.ToString())
+
+
+#End ExternalSource
+WriteLiteral("</p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+
+#ExternalSource("Blocks.vbhtml",38)
+End With
+
+
+#End ExternalSource
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+
+#ExternalSource("Blocks.vbhtml",40)
+ SyncLock New Object()
+
+
+#End ExternalSource
+WriteLiteral(" ")
+
+WriteLiteral("<p>This block is locked, for your security!</p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+
+#ExternalSource("Blocks.vbhtml",42)
+End SyncLock
+
+
+#End ExternalSource
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+
+#ExternalSource("Blocks.vbhtml",44)
+ Using New System.IO.MemoryStream()
+
+
+#End ExternalSource
+WriteLiteral(" ")
+
+WriteLiteral("<p>Some random memory stream will be disposed after rendering this block</p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+
+#ExternalSource("Blocks.vbhtml",46)
+End Using
+
+#End ExternalSource
+End Sub
+End Class
+End Namespace
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/CodeBlock.vb b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/CodeBlock.vb
new file mode 100644
index 00000000..b77904b3
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/CodeBlock.vb
@@ -0,0 +1,31 @@
+'------------------------------------------------------------------------------
+' <auto-generated>
+' This code was generated by a tool.
+' Runtime Version:N.N.NNNNN.N
+'
+' Changes to this file may cause incorrect behavior and will be lost if
+' the code is regenerated.
+' </auto-generated>
+'------------------------------------------------------------------------------
+
+Option Strict Off
+Option Explicit On
+
+Imports System
+
+Namespace TestOutput
+Public Class CodeBlock
+Public Sub New()
+MyBase.New
+End Sub
+Public Overrides Sub Execute()
+
+#ExternalSource("CodeBlock.vbhtml",1)
+
+ Test()
+
+
+#End ExternalSource
+End Sub
+End Class
+End Namespace
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/CodeBlockAtEOF.vb b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/CodeBlockAtEOF.vb
new file mode 100644
index 00000000..4550554d
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/CodeBlockAtEOF.vb
@@ -0,0 +1,29 @@
+'------------------------------------------------------------------------------
+' <auto-generated>
+' This code was generated by a tool.
+' Runtime Version:N.N.NNNNN.N
+'
+' Changes to this file may cause incorrect behavior and will be lost if
+' the code is regenerated.
+' </auto-generated>
+'------------------------------------------------------------------------------
+
+Option Strict Off
+Option Explicit On
+
+Imports System
+
+Namespace TestOutput
+Public Class CodeBlockAtEOF
+Public Sub New()
+MyBase.New
+End Sub
+Public Overrides Sub Execute()
+
+#ExternalSource("CodeBlockAtEOF.vbhtml",1)
+
+
+#End ExternalSource
+End Sub
+End Class
+End Namespace
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Comments.vb b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Comments.vb
new file mode 100644
index 00000000..b6cabd32
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Comments.vb
@@ -0,0 +1,51 @@
+'------------------------------------------------------------------------------
+' <auto-generated>
+' This code was generated by a tool.
+' Runtime Version:N.N.NNNNN.N
+'
+' Changes to this file may cause incorrect behavior and will be lost if
+' the code is regenerated.
+' </auto-generated>
+'------------------------------------------------------------------------------
+
+Option Strict Off
+Option Explicit On
+
+Imports System
+
+Namespace TestOutput
+Public Class Comments
+Public Overrides Sub Execute()
+
+
+#ExternalSource("Comments.vbhtml",1)
+ 'This is not going to be rendered
+
+
+#End ExternalSource
+WriteLiteral("<p>This is going to be rendered</p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+
+
+#ExternalSource("Comments.vbhtml",3)
+ REM Neither is this
+
+
+#End ExternalSource
+
+
+#ExternalSource("Comments.vbhtml",4)
+ rem nor this
+
+
+#End ExternalSource
+
+
+#ExternalSource("Comments.vbhtml",5)
+ rEm nor this
+
+#End ExternalSource
+
+End Sub
+End Class
+End Namespace
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/ConditionalAttributes.vb b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/ConditionalAttributes.vb
new file mode 100644
index 00000000..6abcc142
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/ConditionalAttributes.vb
@@ -0,0 +1,146 @@
+'------------------------------------------------------------------------------
+' <auto-generated>
+' This code was generated by a tool.
+' Runtime Version:N.N.NNNNN.N
+'
+' Changes to this file may cause incorrect behavior and will be lost if
+' the code is regenerated.
+' </auto-generated>
+'------------------------------------------------------------------------------
+
+Option Strict Off
+Option Explicit On
+
+Imports System
+
+Namespace TestOutput
+Public Class ConditionalAttributes
+Public Sub New()
+MyBase.New
+End Sub
+Public Overrides Sub Execute()
+
+#ExternalSource("ConditionalAttributes.vbhtml",1)
+
+ Dim ch = True
+ Dim cls = "bar"
+
+
+#End ExternalSource
+WriteLiteral(" ")
+
+WriteLiteral("<a")
+
+WriteLiteral(" href=""Foo""")
+
+WriteLiteral(" />"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+WriteLiteral(" ")
+
+WriteLiteral("<p")
+
+WriteAttribute("class", Tuple.Create(" class=""", 77), Tuple.Create("""", 89) _
+, Tuple.Create(Tuple.Create("", 85), Tuple.Create(Of System.Object, System.Int32)(cls _
+, 85), False) _
+)
+
+WriteLiteral(" />"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+WriteLiteral(" ")
+
+WriteLiteral("<p")
+
+WriteAttribute("class", Tuple.Create(" class=""", 102), Tuple.Create("""", 118) _
+, Tuple.Create(Tuple.Create("", 110), Tuple.Create("foo", 110), True) _
+, Tuple.Create(Tuple.Create(" ", 113), Tuple.Create(Of System.Object, System.Int32)(cls _
+, 114), False) _
+)
+
+WriteLiteral(" />"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+WriteLiteral(" ")
+
+WriteLiteral("<p")
+
+WriteAttribute("class", Tuple.Create(" class=""", 131), Tuple.Create("""", 147) _
+, Tuple.Create(Tuple.Create("", 139), Tuple.Create(Of System.Object, System.Int32)(cls _
+, 139), False) _
+, Tuple.Create(Tuple.Create(" ", 143), Tuple.Create("foo", 144), True) _
+)
+
+WriteLiteral(" />"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+WriteLiteral(" ")
+
+WriteLiteral("<input")
+
+WriteLiteral(" type=""checkbox""")
+
+WriteAttribute("checked", Tuple.Create(" checked=""", 180), Tuple.Create("""", 193) _
+, Tuple.Create(Tuple.Create("", 190), Tuple.Create(Of System.Object, System.Int32)(ch _
+, 190), False) _
+)
+
+WriteLiteral(" />"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+WriteLiteral(" ")
+
+WriteLiteral("<input")
+
+WriteLiteral(" type=""checkbox""")
+
+WriteAttribute("checked", Tuple.Create(" checked=""", 226), Tuple.Create("""", 243) _
+, Tuple.Create(Tuple.Create("", 236), Tuple.Create("foo", 236), True) _
+, Tuple.Create(Tuple.Create(" ", 239), Tuple.Create(Of System.Object, System.Int32)(ch _
+, 240), False) _
+)
+
+WriteLiteral(" />"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+WriteLiteral(" ")
+
+WriteLiteral("<p")
+
+WriteAttribute("class", Tuple.Create(" class=""", 256), Tuple.Create("""", 302) _
+, Tuple.Create(Tuple.Create("", 264), Tuple.Create(Of System.Object, System.Int32)(New Template(Sub (__razor_attribute_value_writer)
+
+
+#ExternalSource("ConditionalAttributes.vbhtml",10)
+ If cls IsNot Nothing Then
+
+#End ExternalSource
+
+#ExternalSource("ConditionalAttributes.vbhtml",10)
+ WriteTo(__razor_attribute_value_writer, cls)
+
+
+#End ExternalSource
+
+#ExternalSource("ConditionalAttributes.vbhtml",10)
+ End If
+
+#End ExternalSource
+End Sub), 264), False) _
+)
+
+WriteLiteral(" />"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+WriteLiteral(" ")
+
+WriteLiteral("<a")
+
+WriteAttribute("href", Tuple.Create(" href=""", 315), Tuple.Create("""", 327) _
+, Tuple.Create(Tuple.Create("", 322), Tuple.Create(Of System.Object, System.Int32)(Href("~/Foo") _
+, 322), False) _
+)
+
+WriteLiteral(" />"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+
+#ExternalSource("ConditionalAttributes.vbhtml",12)
+
+
+#End ExternalSource
+End Sub
+End Class
+End Namespace
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/DesignTime.vb b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/DesignTime.vb
new file mode 100644
index 00000000..8c2f832c
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/DesignTime.vb
@@ -0,0 +1,99 @@
+'------------------------------------------------------------------------------
+' <auto-generated>
+' This code was generated by a tool.
+' Runtime Version:N.N.NNNNN.N
+'
+' Changes to this file may cause incorrect behavior and will be lost if
+' the code is regenerated.
+' </auto-generated>
+'------------------------------------------------------------------------------
+
+Option Strict Off
+Option Explicit On
+
+Imports System
+
+Namespace TestOutput
+Public Class DesignTime
+Private Shared __o As Object
+
+#ExternalSource("DesignTime.vbhtml", 9)
+Public Shared Function Foo() As Template
+
+#End ExternalSource
+Return New Template(Sub (__razor_helper_writer)
+
+#ExternalSource("DesignTime.vbhtml", 10)
+
+ If True Then
+
+#End ExternalSource
+
+#ExternalSource("DesignTime.vbhtml", 11)
+
+ End If
+
+#End ExternalSource
+End Sub)
+End Function
+
+Public Sub New()
+MyBase.New
+End Sub
+Public Overrides Sub Execute()
+
+#ExternalSource("DesignTime.vbhtml",1)
+ For i = 1 to 10
+
+
+#End ExternalSource
+
+#ExternalSource("DesignTime.vbhtml",2)
+ __o = i
+
+
+#End ExternalSource
+
+#ExternalSource("DesignTime.vbhtml",3)
+
+ Next
+
+
+#End ExternalSource
+
+#ExternalSource("DesignTime.vbhtml",4)
+__o = Foo(Bar.Baz)
+
+
+#End ExternalSource
+
+#ExternalSource("DesignTime.vbhtml",5)
+__o = Foo(Function (item) New Template(Sub (__razor_template_writer)
+
+
+#End ExternalSource
+
+#ExternalSource("DesignTime.vbhtml",6)
+ __o = baz
+
+
+#End ExternalSource
+
+#ExternalSource("DesignTime.vbhtml",7)
+ End Sub))
+
+
+#End ExternalSource
+DefineSection("Footer", Sub ()
+
+
+#ExternalSource("DesignTime.vbhtml",8)
+__o = bar
+
+
+#End ExternalSource
+End Sub)
+
+End Sub
+End Class
+End Namespace
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/EmptyExplicitExpression.vb b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/EmptyExplicitExpression.vb
new file mode 100644
index 00000000..ce8fc4c1
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/EmptyExplicitExpression.vb
@@ -0,0 +1,31 @@
+'------------------------------------------------------------------------------
+' <auto-generated>
+' This code was generated by a tool.
+' Runtime Version:N.N.NNNNN.N
+'
+' Changes to this file may cause incorrect behavior and will be lost if
+' the code is regenerated.
+' </auto-generated>
+'------------------------------------------------------------------------------
+
+Option Strict Off
+Option Explicit On
+
+Imports System
+
+Namespace TestOutput
+Public Class EmptyExplicitExpression
+Private Shared __o As Object
+Public Sub New()
+MyBase.New
+End Sub
+Public Overrides Sub Execute()
+
+#ExternalSource("EmptyExplicitExpression.vbhtml",1)
+__o =
+
+
+#End ExternalSource
+End Sub
+End Class
+End Namespace
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/EmptyImplicitExpression.vb b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/EmptyImplicitExpression.vb
new file mode 100644
index 00000000..786c049d
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/EmptyImplicitExpression.vb
@@ -0,0 +1,31 @@
+'------------------------------------------------------------------------------
+' <auto-generated>
+' This code was generated by a tool.
+' Runtime Version:N.N.NNNNN.N
+'
+' Changes to this file may cause incorrect behavior and will be lost if
+' the code is regenerated.
+' </auto-generated>
+'------------------------------------------------------------------------------
+
+Option Strict Off
+Option Explicit On
+
+Imports System
+
+Namespace TestOutput
+Public Class EmptyImplicitExpression
+Private Shared __o As Object
+Public Sub New()
+MyBase.New
+End Sub
+Public Overrides Sub Execute()
+
+#ExternalSource("EmptyImplicitExpression.vbhtml",1)
+__o =
+
+
+#End ExternalSource
+End Sub
+End Class
+End Namespace
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/EmptyImplicitExpressionInCode.vb b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/EmptyImplicitExpressionInCode.vb
new file mode 100644
index 00000000..9e4b2b1c
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/EmptyImplicitExpressionInCode.vb
@@ -0,0 +1,43 @@
+'------------------------------------------------------------------------------
+' <auto-generated>
+' This code was generated by a tool.
+' Runtime Version:N.N.NNNNN.N
+'
+' Changes to this file may cause incorrect behavior and will be lost if
+' the code is regenerated.
+' </auto-generated>
+'------------------------------------------------------------------------------
+
+Option Strict Off
+Option Explicit On
+
+Imports System
+
+Namespace TestOutput
+Public Class EmptyImplicitExpressionInCode
+Private Shared __o As Object
+Public Sub New()
+MyBase.New
+End Sub
+Public Overrides Sub Execute()
+
+#ExternalSource("EmptyImplicitExpressionInCode.vbhtml",1)
+
+
+
+#End ExternalSource
+
+#ExternalSource("EmptyImplicitExpressionInCode.vbhtml",2)
+__o =
+
+
+#End ExternalSource
+
+#ExternalSource("EmptyImplicitExpressionInCode.vbhtml",3)
+
+
+
+#End ExternalSource
+End Sub
+End Class
+End Namespace
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/ExplicitExpression.vb b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/ExplicitExpression.vb
new file mode 100644
index 00000000..d2e7a9fd
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/ExplicitExpression.vb
@@ -0,0 +1,30 @@
+'------------------------------------------------------------------------------
+' <auto-generated>
+' This code was generated by a tool.
+' Runtime Version:N.N.NNNNN.N
+'
+' Changes to this file may cause incorrect behavior and will be lost if
+' the code is regenerated.
+' </auto-generated>
+'------------------------------------------------------------------------------
+
+Option Strict Off
+Option Explicit On
+
+Imports System
+
+Namespace TestOutput
+Public Class ExplicitExpression
+Public Sub New()
+MyBase.New
+End Sub
+Public Overrides Sub Execute()
+
+#ExternalSource("ExplicitExpression.vbhtml",1)
+Write(Foo(Bar.Baz))
+
+
+#End ExternalSource
+End Sub
+End Class
+End Namespace
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/ExplicitExpressionAtEOF.vb b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/ExplicitExpressionAtEOF.vb
new file mode 100644
index 00000000..7fb5bd77
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/ExplicitExpressionAtEOF.vb
@@ -0,0 +1,31 @@
+'------------------------------------------------------------------------------
+' <auto-generated>
+' This code was generated by a tool.
+' Runtime Version:N.N.NNNNN.N
+'
+' Changes to this file may cause incorrect behavior and will be lost if
+' the code is regenerated.
+' </auto-generated>
+'------------------------------------------------------------------------------
+
+Option Strict Off
+Option Explicit On
+
+Imports System
+
+Namespace TestOutput
+Public Class ExplicitExpressionAtEOF
+Private Shared __o As Object
+Public Sub New()
+MyBase.New
+End Sub
+Public Overrides Sub Execute()
+
+#ExternalSource("ExplicitExpressionAtEOF.vbhtml",1)
+__o =
+
+
+#End ExternalSource
+End Sub
+End Class
+End Namespace
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/ExpressionsInCode.vb b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/ExpressionsInCode.vb
new file mode 100644
index 00000000..bbbd62b2
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/ExpressionsInCode.vb
@@ -0,0 +1,86 @@
+'------------------------------------------------------------------------------
+' <auto-generated>
+' This code was generated by a tool.
+' Runtime Version:N.N.NNNNN.N
+'
+' Changes to this file may cause incorrect behavior and will be lost if
+' the code is regenerated.
+' </auto-generated>
+'------------------------------------------------------------------------------
+
+Option Strict Off
+Option Explicit On
+
+Imports System
+
+Namespace TestOutput
+Public Class ExpressionsInCode
+Public Sub New()
+MyBase.New
+End Sub
+Public Overrides Sub Execute()
+
+#ExternalSource("ExpressionsInCode.vbhtml",1)
+
+ Dim foo As Object = Nothing
+ Dim bar as String = "Foo"
+
+
+#End ExternalSource
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+
+#ExternalSource("ExpressionsInCode.vbhtml",6)
+ If foo IsNot Nothing Then
+
+
+#End ExternalSource
+
+#ExternalSource("ExpressionsInCode.vbhtml",7)
+Write(foo)
+
+
+#End ExternalSource
+
+#ExternalSource("ExpressionsInCode.vbhtml",7)
+
+Else
+
+
+#End ExternalSource
+WriteLiteral(" ")
+
+WriteLiteral("<p>Foo is Null!</p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+
+#ExternalSource("ExpressionsInCode.vbhtml",10)
+End If
+
+
+#End ExternalSource
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&"<p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+
+#ExternalSource("ExpressionsInCode.vbhtml",13)
+ If Not String.IsNullOrEmpty(bar) Then
+
+
+#End ExternalSource
+
+#ExternalSource("ExpressionsInCode.vbhtml",14)
+Write(bar.Replace("F", "B"))
+
+
+#End ExternalSource
+
+#ExternalSource("ExpressionsInCode.vbhtml",14)
+
+End If
+
+
+#End ExternalSource
+WriteLiteral("</p>")
+
+End Sub
+End Class
+End Namespace
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/FunctionsBlock.DesignTime.vb b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/FunctionsBlock.DesignTime.vb
new file mode 100644
index 00000000..3ade6739
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/FunctionsBlock.DesignTime.vb
@@ -0,0 +1,47 @@
+'------------------------------------------------------------------------------
+' <auto-generated>
+' This code was generated by a tool.
+' Runtime Version:N.N.NNNNN.N
+'
+' Changes to this file may cause incorrect behavior and will be lost if
+' the code is regenerated.
+' </auto-generated>
+'------------------------------------------------------------------------------
+
+Option Strict Off
+Option Explicit On
+
+Imports System
+
+Namespace TestOutput
+Public Class FunctionsBlock
+Private Shared __o As Object
+
+#ExternalSource("FunctionsBlock.vbhtml",1)
+
+
+
+#End ExternalSource
+
+#ExternalSource("FunctionsBlock.vbhtml",2)
+
+ Private _rand as New Random()
+ Private Function RandomInt() as Integer
+ Return _rand.Next()
+ End Function
+
+#End ExternalSource
+
+Public Sub New()
+MyBase.New
+End Sub
+Public Overrides Sub Execute()
+
+#ExternalSource("FunctionsBlock.vbhtml",3)
+ __o = RandomInt()
+
+
+#End ExternalSource
+End Sub
+End Class
+End Namespace
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/FunctionsBlock.vb b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/FunctionsBlock.vb
new file mode 100644
index 00000000..b14fedc3
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/FunctionsBlock.vb
@@ -0,0 +1,50 @@
+'------------------------------------------------------------------------------
+' <auto-generated>
+' This code was generated by a tool.
+' Runtime Version:N.N.NNNNN.N
+'
+' Changes to this file may cause incorrect behavior and will be lost if
+' the code is regenerated.
+' </auto-generated>
+'------------------------------------------------------------------------------
+
+Option Strict Off
+Option Explicit On
+
+Imports System
+
+Namespace TestOutput
+Public Class FunctionsBlock
+
+#ExternalSource("FunctionsBlock.vbhtml",1)
+
+
+
+#End ExternalSource
+
+#ExternalSource("FunctionsBlock.vbhtml",5)
+
+ Private _rand as New Random()
+ Private Function RandomInt() as Integer
+ Return _rand.Next()
+ End Function
+
+#End ExternalSource
+
+Public Sub New()
+MyBase.New
+End Sub
+Public Overrides Sub Execute()
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&"Here's a random number: ")
+
+
+#ExternalSource("FunctionsBlock.vbhtml",12)
+ Write(RandomInt())
+
+
+#End ExternalSource
+End Sub
+End Class
+End Namespace
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Helpers.Instance.vb b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Helpers.Instance.vb
new file mode 100644
index 00000000..5669e5e7
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Helpers.Instance.vb
@@ -0,0 +1,87 @@
+'------------------------------------------------------------------------------
+' <auto-generated>
+' This code was generated by a tool.
+' Runtime Version:N.N.NNNNN.N
+'
+' Changes to this file may cause incorrect behavior and will be lost if
+' the code is regenerated.
+' </auto-generated>
+'------------------------------------------------------------------------------
+
+Option Strict Off
+Option Explicit On
+
+Imports System
+
+Namespace TestOutput
+Public Class Helpers
+
+#ExternalSource("Helpers.vbhtml", 1)
+Public Function Bold(s as String) As Template
+
+#End ExternalSource
+Return New Template(Sub (__razor_helper_writer)
+
+#ExternalSource("Helpers.vbhtml", 1)
+
+ s = s.ToUpper()
+
+#End ExternalSource
+WriteLiteralTo(__razor_helper_writer, " ")
+WriteLiteralTo(__razor_helper_writer, "<strong>")
+
+#ExternalSource("Helpers.vbhtml", 3)
+WriteTo(__razor_helper_writer, s)
+
+#End ExternalSource
+WriteLiteralTo(__razor_helper_writer, "</strong>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+#ExternalSource("Helpers.vbhtml", 4)
+
+#End ExternalSource
+End Sub)
+End Function
+
+#ExternalSource("Helpers.vbhtml", 6)
+Public Function Italic(s as String) As Template
+
+#End ExternalSource
+Return New Template(Sub (__razor_helper_writer)
+
+#ExternalSource("Helpers.vbhtml", 6)
+
+ s = s.ToUpper()
+
+#End ExternalSource
+WriteLiteralTo(__razor_helper_writer, " ")
+WriteLiteralTo(__razor_helper_writer, "<em>")
+
+#ExternalSource("Helpers.vbhtml", 8)
+WriteTo(__razor_helper_writer, s)
+
+#End ExternalSource
+WriteLiteralTo(__razor_helper_writer, "</em>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+#ExternalSource("Helpers.vbhtml", 9)
+
+#End ExternalSource
+End Sub)
+End Function
+
+Public Sub New()
+MyBase.New
+End Sub
+Public Overrides Sub Execute()
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+
+#ExternalSource("Helpers.vbhtml",11)
+Write(Bold("Hello"))
+
+
+#End ExternalSource
+End Sub
+End Class
+End Namespace
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Helpers.vb b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Helpers.vb
new file mode 100644
index 00000000..b8e2ec46
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Helpers.vb
@@ -0,0 +1,87 @@
+'------------------------------------------------------------------------------
+' <auto-generated>
+' This code was generated by a tool.
+' Runtime Version:N.N.NNNNN.N
+'
+' Changes to this file may cause incorrect behavior and will be lost if
+' the code is regenerated.
+' </auto-generated>
+'------------------------------------------------------------------------------
+
+Option Strict Off
+Option Explicit On
+
+Imports System
+
+Namespace TestOutput
+Public Class Helpers
+
+#ExternalSource("Helpers.vbhtml", 1)
+Public Shared Function Bold(s as String) As Template
+
+#End ExternalSource
+Return New Template(Sub (__razor_helper_writer)
+
+#ExternalSource("Helpers.vbhtml", 1)
+
+ s = s.ToUpper()
+
+#End ExternalSource
+WriteLiteralTo(__razor_helper_writer, " ")
+WriteLiteralTo(__razor_helper_writer, "<strong>")
+
+#ExternalSource("Helpers.vbhtml", 3)
+WriteTo(__razor_helper_writer, s)
+
+#End ExternalSource
+WriteLiteralTo(__razor_helper_writer, "</strong>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+#ExternalSource("Helpers.vbhtml", 4)
+
+#End ExternalSource
+End Sub)
+End Function
+
+#ExternalSource("Helpers.vbhtml", 6)
+Public Shared Function Italic(s as String) As Template
+
+#End ExternalSource
+Return New Template(Sub (__razor_helper_writer)
+
+#ExternalSource("Helpers.vbhtml", 6)
+
+ s = s.ToUpper()
+
+#End ExternalSource
+WriteLiteralTo(__razor_helper_writer, " ")
+WriteLiteralTo(__razor_helper_writer, "<em>")
+
+#ExternalSource("Helpers.vbhtml", 8)
+WriteTo(__razor_helper_writer, s)
+
+#End ExternalSource
+WriteLiteralTo(__razor_helper_writer, "</em>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+#ExternalSource("Helpers.vbhtml", 9)
+
+#End ExternalSource
+End Sub)
+End Function
+
+Public Sub New()
+MyBase.New
+End Sub
+Public Overrides Sub Execute()
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+
+#ExternalSource("Helpers.vbhtml",11)
+Write(Bold("Hello"))
+
+
+#End ExternalSource
+End Sub
+End Class
+End Namespace
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/HelpersMissingCloseParen.vb b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/HelpersMissingCloseParen.vb
new file mode 100644
index 00000000..9a7337cb
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/HelpersMissingCloseParen.vb
@@ -0,0 +1,60 @@
+'------------------------------------------------------------------------------
+' <auto-generated>
+' This code was generated by a tool.
+' Runtime Version:N.N.NNNNN.N
+'
+' Changes to this file may cause incorrect behavior and will be lost if
+' the code is regenerated.
+' </auto-generated>
+'------------------------------------------------------------------------------
+
+Option Strict Off
+Option Explicit On
+
+Imports System
+
+Namespace TestOutput
+Public Class HelpersMissingCloseParen
+
+#ExternalSource("HelpersMissingCloseParen.vbhtml", 1)
+Public Shared Function Bold(s as String) As Template
+
+#End ExternalSource
+Return New Template(Sub (__razor_helper_writer)
+
+#ExternalSource("HelpersMissingCloseParen.vbhtml", 1)
+
+ s = s.ToUpper()
+
+#End ExternalSource
+WriteLiteralTo(__razor_helper_writer, " ")
+WriteLiteralTo(__razor_helper_writer, "<strong>")
+
+#ExternalSource("HelpersMissingCloseParen.vbhtml", 3)
+WriteTo(__razor_helper_writer, s)
+
+#End ExternalSource
+WriteLiteralTo(__razor_helper_writer, "</strong>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+#ExternalSource("HelpersMissingCloseParen.vbhtml", 4)
+
+#End ExternalSource
+End Sub)
+End Function
+
+#ExternalSource("HelpersMissingCloseParen.vbhtml", 6)
+Public Shared Function Italic(s as String
+
+@Bold("Hello")
+#End ExternalSource
+End Function
+
+Public Sub New()
+MyBase.New
+End Sub
+Public Overrides Sub Execute()
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+End Sub
+End Class
+End Namespace
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/HelpersMissingName.vb b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/HelpersMissingName.vb
new file mode 100644
index 00000000..79c3c00d
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/HelpersMissingName.vb
@@ -0,0 +1,24 @@
+'------------------------------------------------------------------------------
+' <auto-generated>
+' This code was generated by a tool.
+' Runtime Version:N.N.NNNNN.N
+'
+' Changes to this file may cause incorrect behavior and will be lost if
+' the code is regenerated.
+' </auto-generated>
+'------------------------------------------------------------------------------
+
+Option Strict Off
+Option Explicit On
+
+Imports System
+
+Namespace TestOutput
+Public Class HelpersMissingName
+Public Sub New()
+MyBase.New
+End Sub
+Public Overrides Sub Execute()
+End Sub
+End Class
+End Namespace
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/HelpersMissingOpenParen.vb b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/HelpersMissingOpenParen.vb
new file mode 100644
index 00000000..62fedca0
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/HelpersMissingOpenParen.vb
@@ -0,0 +1,66 @@
+'------------------------------------------------------------------------------
+' <auto-generated>
+' This code was generated by a tool.
+' Runtime Version:N.N.NNNNN.N
+'
+' Changes to this file may cause incorrect behavior and will be lost if
+' the code is regenerated.
+' </auto-generated>
+'------------------------------------------------------------------------------
+
+Option Strict Off
+Option Explicit On
+
+Imports System
+
+Namespace TestOutput
+Public Class HelpersMissingOpenParen
+
+#ExternalSource("HelpersMissingOpenParen.vbhtml", 1)
+Public Shared Function Bold(s as String) As Template
+
+#End ExternalSource
+Return New Template(Sub (__razor_helper_writer)
+
+#ExternalSource("HelpersMissingOpenParen.vbhtml", 1)
+
+ s = s.ToUpper()
+
+#End ExternalSource
+WriteLiteralTo(__razor_helper_writer, " ")
+WriteLiteralTo(__razor_helper_writer, "<strong>")
+
+#ExternalSource("HelpersMissingOpenParen.vbhtml", 3)
+WriteTo(__razor_helper_writer, s)
+
+#End ExternalSource
+WriteLiteralTo(__razor_helper_writer, "</strong>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+#ExternalSource("HelpersMissingOpenParen.vbhtml", 4)
+
+#End ExternalSource
+End Sub)
+End Function
+
+#ExternalSource("HelpersMissingOpenParen.vbhtml", 6)
+Public Shared Function Italic
+#End ExternalSource
+End Function
+
+Public Sub New()
+MyBase.New
+End Sub
+Public Overrides Sub Execute()
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+
+#ExternalSource("HelpersMissingOpenParen.vbhtml",8)
+Write(Bold("Hello"))
+
+
+#End ExternalSource
+End Sub
+End Class
+End Namespace
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/ImplicitExpression.vb b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/ImplicitExpression.vb
new file mode 100644
index 00000000..dd9e756f
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/ImplicitExpression.vb
@@ -0,0 +1,56 @@
+'------------------------------------------------------------------------------
+' <auto-generated>
+' This code was generated by a tool.
+' Runtime Version:N.N.NNNNN.N
+'
+' Changes to this file may cause incorrect behavior and will be lost if
+' the code is regenerated.
+' </auto-generated>
+'------------------------------------------------------------------------------
+
+Option Strict Off
+Option Explicit On
+
+Imports System
+
+Namespace TestOutput
+Public Class ImplicitExpression
+Public Sub New()
+MyBase.New
+End Sub
+Public Overrides Sub Execute()
+
+#ExternalSource("ImplicitExpression.vbhtml",1)
+ For i = 1 To 10
+
+
+#End ExternalSource
+WriteLiteral(" ")
+
+WriteLiteral("<p>This is item #")
+
+
+#ExternalSource("ImplicitExpression.vbhtml",2)
+ Write(i)
+
+
+#End ExternalSource
+WriteLiteral("</p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+
+#ExternalSource("ImplicitExpression.vbhtml",3)
+Next
+
+
+#End ExternalSource
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+
+#ExternalSource("ImplicitExpression.vbhtml",5)
+Write(SyntaxSampleHelpers.CodeForLink(Me))
+
+
+#End ExternalSource
+End Sub
+End Class
+End Namespace
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/ImplicitExpressionAtEOF.vb b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/ImplicitExpressionAtEOF.vb
new file mode 100644
index 00000000..debff90d
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/ImplicitExpressionAtEOF.vb
@@ -0,0 +1,31 @@
+'------------------------------------------------------------------------------
+' <auto-generated>
+' This code was generated by a tool.
+' Runtime Version:N.N.NNNNN.N
+'
+' Changes to this file may cause incorrect behavior and will be lost if
+' the code is regenerated.
+' </auto-generated>
+'------------------------------------------------------------------------------
+
+Option Strict Off
+Option Explicit On
+
+Imports System
+
+Namespace TestOutput
+Public Class ImplicitExpressionAtEOF
+Private Shared __o As Object
+Public Sub New()
+MyBase.New
+End Sub
+Public Overrides Sub Execute()
+
+#ExternalSource("ImplicitExpressionAtEOF.vbhtml",1)
+__o =
+
+
+#End ExternalSource
+End Sub
+End Class
+End Namespace
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Imports.DesignTime.vb b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Imports.DesignTime.vb
new file mode 100644
index 00000000..5868d5ab
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Imports.DesignTime.vb
@@ -0,0 +1,41 @@
+'------------------------------------------------------------------------------
+' <auto-generated>
+' This code was generated by a tool.
+' Runtime Version:N.N.NNNNN.N
+'
+' Changes to this file may cause incorrect behavior and will be lost if
+' the code is regenerated.
+' </auto-generated>
+'------------------------------------------------------------------------------
+
+Option Strict Off
+Option Explicit On
+
+Imports Foo = System.Text.Encoding
+
+Imports System
+Imports System.IO
+
+
+Namespace TestOutput
+Public Class [Imports]
+Private Shared __o As Object
+Public Sub New()
+MyBase.New
+End Sub
+Public Overrides Sub Execute()
+
+#ExternalSource("Imports.vbhtml",4)
+ __o = GetType(Path).FullName
+
+
+#End ExternalSource
+
+#ExternalSource("Imports.vbhtml",5)
+ __o = GetType(Foo).FullName
+
+
+#End ExternalSource
+End Sub
+End Class
+End Namespace
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Imports.vb b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Imports.vb
new file mode 100644
index 00000000..d35ce2ab
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Imports.vb
@@ -0,0 +1,46 @@
+'------------------------------------------------------------------------------
+' <auto-generated>
+' This code was generated by a tool.
+' Runtime Version:N.N.NNNNN.N
+'
+' Changes to this file may cause incorrect behavior and will be lost if
+' the code is regenerated.
+' </auto-generated>
+'------------------------------------------------------------------------------
+
+Option Strict Off
+Option Explicit On
+
+Imports Foo = System.Text.Encoding
+
+Imports System
+Imports System.IO
+
+
+Namespace TestOutput
+Public Class [Imports]
+Public Sub New()
+MyBase.New
+End Sub
+Public Overrides Sub Execute()
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&"<p>Path's full type name is ")
+
+
+#ExternalSource("Imports.vbhtml",5)
+ Write(GetType(Path).FullName)
+
+
+#End ExternalSource
+WriteLiteral("</p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&"<p>Foo's actual full type name is ")
+
+
+#ExternalSource("Imports.vbhtml",6)
+ Write(GetType(Foo).FullName)
+
+
+#End ExternalSource
+WriteLiteral("</p>")
+
+End Sub
+End Class
+End Namespace
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Inherits.Designtime.vb b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Inherits.Designtime.vb
new file mode 100644
index 00000000..fc62324b
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Inherits.Designtime.vb
@@ -0,0 +1,35 @@
+'------------------------------------------------------------------------------
+' <auto-generated>
+' This code was generated by a tool.
+' Runtime Version:N.N.NNNNN.N
+'
+' Changes to this file may cause incorrect behavior and will be lost if
+' the code is regenerated.
+' </auto-generated>
+'------------------------------------------------------------------------------
+
+Option Strict Off
+Option Explicit On
+
+Imports System
+
+Namespace TestOutput
+Public Class [Inherits]
+Inherits System.Web.WebPages.WebPage
+Public Sub New()
+MyBase.New
+End Sub
+Private Sub __RazorDesignTimeHelpers__()
+
+
+#ExternalSource("Inherits.vbhtml",1)
+Dim __inheritsHelper As System.Web.WebPages.WebPage = Nothing
+
+
+#End ExternalSource
+
+End Sub
+Public Overrides Sub Execute()
+End Sub
+End Class
+End Namespace
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Inherits.Runtime.vb b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Inherits.Runtime.vb
new file mode 100644
index 00000000..1395dac1
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Inherits.Runtime.vb
@@ -0,0 +1,25 @@
+'------------------------------------------------------------------------------
+' <auto-generated>
+' This code was generated by a tool.
+' Runtime Version:N.N.NNNNN.N
+'
+' Changes to this file may cause incorrect behavior and will be lost if
+' the code is regenerated.
+' </auto-generated>
+'------------------------------------------------------------------------------
+
+Option Strict Off
+Option Explicit On
+
+Imports System
+
+Namespace TestOutput
+Public Class [Inherits]
+Inherits System.Web.WebPages.WebPage
+Public Sub New()
+MyBase.New
+End Sub
+Public Overrides Sub Execute()
+End Sub
+End Class
+End Namespace
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Instrumented.vb b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Instrumented.vb
new file mode 100644
index 00000000..22eadaf0
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Instrumented.vb
@@ -0,0 +1,497 @@
+'------------------------------------------------------------------------------
+' <auto-generated>
+' This code was generated by a tool.
+' Runtime Version:N.N.NNNNN.N
+'
+' Changes to this file may cause incorrect behavior and will be lost if
+' the code is regenerated.
+' </auto-generated>
+'------------------------------------------------------------------------------
+
+Option Strict Off
+Option Explicit On
+
+Imports System
+
+Namespace TestOutput
+Public Class Instrumented
+
+#ExternalSource("Instrumented.vbhtml", 1)
+Public Shared Function Strong(s As String) As Template
+
+#End ExternalSource
+Return New Template(Sub (__razor_helper_writer)
+
+#ExternalSource("Instrumented.vbhtml", 1)
+
+
+#End ExternalSource
+BeginContext(__razor_helper_writer, "~/Instrumented.vbhtml", 29, 4, true)
+WriteLiteralTo(__razor_helper_writer, " ")
+EndContext(__razor_helper_writer, "~/Instrumented.vbhtml", 29, 4, true)
+BeginContext(__razor_helper_writer, "~/Instrumented.vbhtml", 34, 8, true)
+WriteLiteralTo(__razor_helper_writer, "<strong>")
+EndContext(__razor_helper_writer, "~/Instrumented.vbhtml", 34, 8, true)
+BeginContext(__razor_helper_writer, "~/Instrumented.vbhtml", 43, 1, false)
+
+#ExternalSource("Instrumented.vbhtml", 2)
+WriteTo(__razor_helper_writer, s)
+
+#End ExternalSource
+EndContext(__razor_helper_writer, "~/Instrumented.vbhtml", 43, 1, false)
+BeginContext(__razor_helper_writer, "~/Instrumented.vbhtml", 44, 11, true)
+WriteLiteralTo(__razor_helper_writer, "</strong>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+EndContext(__razor_helper_writer, "~/Instrumented.vbhtml", 44, 11, true)
+
+#ExternalSource("Instrumented.vbhtml", 3)
+
+#End ExternalSource
+End Sub)
+End Function
+
+Public Sub New()
+MyBase.New
+End Sub
+Public Overrides Sub Execute()
+BeginContext("~/Instrumented.vbhtml", 65, 4, true)
+
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+EndContext("~/Instrumented.vbhtml", 65, 4, true)
+
+
+#ExternalSource("Instrumented.vbhtml",5)
+
+ Dim i As Integer = 1
+ Dim foo =
+
+#End ExternalSource
+Function (item) New Template(Sub (__razor_template_writer)
+
+BeginContext(__razor_template_writer, "~/Instrumented.vbhtml", 118, 12, true)
+
+WriteLiteralTo(__razor_template_writer, "<p>Foo</p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+EndContext(__razor_template_writer, "~/Instrumented.vbhtml", 118, 12, true)
+
+BeginContext("~/Instrumented.vbhtml", 130, 4, true)
+
+WriteLiteral(" ")
+
+EndContext("~/Instrumented.vbhtml", 130, 4, true)
+
+BeginContext("~/Instrumented.vbhtml", 136, 15, true)
+
+WriteLiteral("Hello, World!"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+EndContext("~/Instrumented.vbhtml", 136, 15, true)
+
+BeginContext("~/Instrumented.vbhtml", 151, 4, true)
+
+WriteLiteral(" ")
+
+EndContext("~/Instrumented.vbhtml", 151, 4, true)
+
+BeginContext("~/Instrumented.vbhtml", 156, 22, true)
+
+WriteLiteral("<p>Hello, World!</p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+EndContext("~/Instrumented.vbhtml", 156, 22, true)
+
+End Sub)
+
+#ExternalSource("Instrumented.vbhtml",10)
+
+
+#End ExternalSource
+BeginContext("~/Instrumented.vbhtml", 186, 4, true)
+
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+EndContext("~/Instrumented.vbhtml", 186, 4, true)
+
+
+#ExternalSource("Instrumented.vbhtml",12)
+ While i <= 10
+
+
+#End ExternalSource
+BeginContext("~/Instrumented.vbhtml", 206, 4, true)
+
+WriteLiteral(" ")
+
+EndContext("~/Instrumented.vbhtml", 206, 4, true)
+
+BeginContext("~/Instrumented.vbhtml", 211, 23, true)
+
+WriteLiteral("<p>Hello from VB.Net, #")
+
+EndContext("~/Instrumented.vbhtml", 211, 23, true)
+
+BeginContext("~/Instrumented.vbhtml", 236, 1, false)
+
+
+#ExternalSource("Instrumented.vbhtml",13)
+ Write(i)
+
+
+#End ExternalSource
+EndContext("~/Instrumented.vbhtml", 236, 1, false)
+
+BeginContext("~/Instrumented.vbhtml", 238, 6, true)
+
+WriteLiteral("</p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+EndContext("~/Instrumented.vbhtml", 238, 6, true)
+
+
+#ExternalSource("Instrumented.vbhtml",14)
+ i += 1
+End While
+
+
+#End ExternalSource
+BeginContext("~/Instrumented.vbhtml", 267, 2, true)
+
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+EndContext("~/Instrumented.vbhtml", 267, 2, true)
+
+
+#ExternalSource("Instrumented.vbhtml",17)
+ If i = 11 Then
+
+
+#End ExternalSource
+BeginContext("~/Instrumented.vbhtml", 286, 4, true)
+
+WriteLiteral(" ")
+
+EndContext("~/Instrumented.vbhtml", 286, 4, true)
+
+BeginContext("~/Instrumented.vbhtml", 291, 27, true)
+
+WriteLiteral("<p>We wrote 10 lines!</p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+EndContext("~/Instrumented.vbhtml", 291, 27, true)
+
+
+#ExternalSource("Instrumented.vbhtml",19)
+End If
+
+
+#End ExternalSource
+BeginContext("~/Instrumented.vbhtml", 326, 2, true)
+
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+EndContext("~/Instrumented.vbhtml", 326, 2, true)
+
+
+#ExternalSource("Instrumented.vbhtml",21)
+ Do
+
+
+#End ExternalSource
+BeginContext("~/Instrumented.vbhtml", 333, 4, true)
+
+WriteLiteral(" ")
+
+EndContext("~/Instrumented.vbhtml", 333, 4, true)
+
+BeginContext("~/Instrumented.vbhtml", 338, 16, true)
+
+WriteLiteral("<p>Hello again: ")
+
+EndContext("~/Instrumented.vbhtml", 338, 16, true)
+
+BeginContext("~/Instrumented.vbhtml", 355, 1, false)
+
+
+#ExternalSource("Instrumented.vbhtml",22)
+ Write(i)
+
+
+#End ExternalSource
+EndContext("~/Instrumented.vbhtml", 355, 1, false)
+
+BeginContext("~/Instrumented.vbhtml", 356, 6, true)
+
+WriteLiteral("</p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+EndContext("~/Instrumented.vbhtml", 356, 6, true)
+
+
+#ExternalSource("Instrumented.vbhtml",23)
+ i -= 1
+Loop While i > 0
+
+
+#End ExternalSource
+BeginContext("~/Instrumented.vbhtml", 392, 2, true)
+
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+EndContext("~/Instrumented.vbhtml", 392, 2, true)
+
+
+#ExternalSource("Instrumented.vbhtml",26)
+ Select Case i
+ Case 11
+
+
+#End ExternalSource
+BeginContext("~/Instrumented.vbhtml", 423, 8, true)
+
+WriteLiteral(" ")
+
+EndContext("~/Instrumented.vbhtml", 423, 8, true)
+
+BeginContext("~/Instrumented.vbhtml", 432, 38, true)
+
+WriteLiteral("<p>No really, we wrote 10 lines!</p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+EndContext("~/Instrumented.vbhtml", 432, 38, true)
+
+
+#ExternalSource("Instrumented.vbhtml",29)
+ Case Else
+
+
+#End ExternalSource
+BeginContext("~/Instrumented.vbhtml", 485, 8, true)
+
+WriteLiteral(" ")
+
+EndContext("~/Instrumented.vbhtml", 485, 8, true)
+
+BeginContext("~/Instrumented.vbhtml", 494, 41, true)
+
+WriteLiteral("<p>We wrote a bunch more lines too!</p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+EndContext("~/Instrumented.vbhtml", 494, 41, true)
+
+
+#ExternalSource("Instrumented.vbhtml",31)
+End Select
+
+
+#End ExternalSource
+BeginContext("~/Instrumented.vbhtml", 547, 2, true)
+
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+EndContext("~/Instrumented.vbhtml", 547, 2, true)
+
+
+#ExternalSource("Instrumented.vbhtml",33)
+ For j as Integer = 1 to 10 Step 2
+
+
+#End ExternalSource
+BeginContext("~/Instrumented.vbhtml", 585, 4, true)
+
+WriteLiteral(" ")
+
+EndContext("~/Instrumented.vbhtml", 585, 4, true)
+
+BeginContext("~/Instrumented.vbhtml", 590, 29, true)
+
+WriteLiteral("<p>Hello again from VB.Net, #")
+
+EndContext("~/Instrumented.vbhtml", 590, 29, true)
+
+BeginContext("~/Instrumented.vbhtml", 621, 1, false)
+
+
+#ExternalSource("Instrumented.vbhtml",34)
+ Write(j)
+
+
+#End ExternalSource
+EndContext("~/Instrumented.vbhtml", 621, 1, false)
+
+BeginContext("~/Instrumented.vbhtml", 623, 6, true)
+
+WriteLiteral("</p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+EndContext("~/Instrumented.vbhtml", 623, 6, true)
+
+
+#ExternalSource("Instrumented.vbhtml",35)
+Next j
+
+
+#End ExternalSource
+BeginContext("~/Instrumented.vbhtml", 637, 2, true)
+
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+EndContext("~/Instrumented.vbhtml", 637, 2, true)
+
+
+#ExternalSource("Instrumented.vbhtml",37)
+ Try
+
+
+#End ExternalSource
+BeginContext("~/Instrumented.vbhtml", 645, 4, true)
+
+WriteLiteral(" ")
+
+EndContext("~/Instrumented.vbhtml", 645, 4, true)
+
+BeginContext("~/Instrumented.vbhtml", 650, 37, true)
+
+WriteLiteral("<p>That time, we wrote 5 lines!</p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+EndContext("~/Instrumented.vbhtml", 650, 37, true)
+
+
+#ExternalSource("Instrumented.vbhtml",39)
+Catch ex as Exception
+
+
+#End ExternalSource
+BeginContext("~/Instrumented.vbhtml", 710, 4, true)
+
+WriteLiteral(" ")
+
+EndContext("~/Instrumented.vbhtml", 710, 4, true)
+
+BeginContext("~/Instrumented.vbhtml", 715, 29, true)
+
+WriteLiteral("<p>Oh no! An error occurred: ")
+
+EndContext("~/Instrumented.vbhtml", 715, 29, true)
+
+BeginContext("~/Instrumented.vbhtml", 746, 10, false)
+
+
+#ExternalSource("Instrumented.vbhtml",40)
+ Write(ex.Message)
+
+
+#End ExternalSource
+EndContext("~/Instrumented.vbhtml", 746, 10, false)
+
+BeginContext("~/Instrumented.vbhtml", 757, 6, true)
+
+WriteLiteral("</p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+EndContext("~/Instrumented.vbhtml", 757, 6, true)
+
+
+#ExternalSource("Instrumented.vbhtml",41)
+End Try
+
+
+#End ExternalSource
+BeginContext("~/Instrumented.vbhtml", 772, 2, true)
+
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+EndContext("~/Instrumented.vbhtml", 772, 2, true)
+
+
+#ExternalSource("Instrumented.vbhtml",43)
+ With i
+
+
+#End ExternalSource
+BeginContext("~/Instrumented.vbhtml", 783, 4, true)
+
+WriteLiteral(" ")
+
+EndContext("~/Instrumented.vbhtml", 783, 4, true)
+
+BeginContext("~/Instrumented.vbhtml", 788, 12, true)
+
+WriteLiteral("<p>i is now ")
+
+EndContext("~/Instrumented.vbhtml", 788, 12, true)
+
+BeginContext("~/Instrumented.vbhtml", 802, 11, false)
+
+
+#ExternalSource("Instrumented.vbhtml",44)
+ Write(.ToString())
+
+
+#End ExternalSource
+EndContext("~/Instrumented.vbhtml", 802, 11, false)
+
+BeginContext("~/Instrumented.vbhtml", 814, 6, true)
+
+WriteLiteral("</p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+EndContext("~/Instrumented.vbhtml", 814, 6, true)
+
+
+#ExternalSource("Instrumented.vbhtml",45)
+End With
+
+
+#End ExternalSource
+BeginContext("~/Instrumented.vbhtml", 830, 2, true)
+
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+EndContext("~/Instrumented.vbhtml", 830, 2, true)
+
+
+#ExternalSource("Instrumented.vbhtml",47)
+ SyncLock New Object()
+
+
+#End ExternalSource
+BeginContext("~/Instrumented.vbhtml", 856, 4, true)
+
+WriteLiteral(" ")
+
+EndContext("~/Instrumented.vbhtml", 856, 4, true)
+
+BeginContext("~/Instrumented.vbhtml", 861, 49, true)
+
+WriteLiteral("<p>This block is locked, for your security!</p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+EndContext("~/Instrumented.vbhtml", 861, 49, true)
+
+
+#ExternalSource("Instrumented.vbhtml",49)
+End SyncLock
+
+
+#End ExternalSource
+BeginContext("~/Instrumented.vbhtml", 924, 2, true)
+
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+EndContext("~/Instrumented.vbhtml", 924, 2, true)
+
+
+#ExternalSource("Instrumented.vbhtml",51)
+ Using New System.IO.MemoryStream()
+
+
+#End ExternalSource
+BeginContext("~/Instrumented.vbhtml", 963, 4, true)
+
+WriteLiteral(" ")
+
+EndContext("~/Instrumented.vbhtml", 963, 4, true)
+
+BeginContext("~/Instrumented.vbhtml", 968, 78, true)
+
+WriteLiteral("<p>Some random memory stream will be disposed after rendering this block</p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+EndContext("~/Instrumented.vbhtml", 968, 78, true)
+
+
+#ExternalSource("Instrumented.vbhtml",53)
+End Using
+
+#End ExternalSource
+End Sub
+End Class
+End Namespace
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/LayoutDirective.vb b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/LayoutDirective.vb
new file mode 100644
index 00000000..fb7bdbdd
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/LayoutDirective.vb
@@ -0,0 +1,25 @@
+'------------------------------------------------------------------------------
+' <auto-generated>
+' This code was generated by a tool.
+' Runtime Version:N.N.NNNNN.N
+'
+' Changes to this file may cause incorrect behavior and will be lost if
+' the code is regenerated.
+' </auto-generated>
+'------------------------------------------------------------------------------
+
+Option Strict Off
+Option Explicit On
+
+Imports System
+
+Namespace TestOutput
+Public Class LayoutDirective
+Public Sub New()
+MyBase.New
+End Sub
+Public Overrides Sub Execute()
+Layout = "~/Foo/Bar/Baz"
+End Sub
+End Class
+End Namespace
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/MarkupInCodeBlock.vb b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/MarkupInCodeBlock.vb
new file mode 100644
index 00000000..c70fd0bf
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/MarkupInCodeBlock.vb
@@ -0,0 +1,51 @@
+'------------------------------------------------------------------------------
+' <auto-generated>
+' This code was generated by a tool.
+' Runtime Version:N.N.NNNNN.N
+'
+' Changes to this file may cause incorrect behavior and will be lost if
+' the code is regenerated.
+' </auto-generated>
+'------------------------------------------------------------------------------
+
+Option Strict Off
+Option Explicit On
+
+Imports System
+
+Namespace TestOutput
+Public Class MarkupInCodeBlock
+Public Sub New()
+MyBase.New
+End Sub
+Public Overrides Sub Execute()
+
+#ExternalSource("MarkupInCodeBlock.vbhtml",1)
+
+ For i = 1 To 10
+
+
+#End ExternalSource
+WriteLiteral(" ")
+
+WriteLiteral("<p>Hello from VB.Net, #")
+
+
+#ExternalSource("MarkupInCodeBlock.vbhtml",3)
+ Write(i.ToString())
+
+
+#End ExternalSource
+WriteLiteral("</p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+
+#ExternalSource("MarkupInCodeBlock.vbhtml",4)
+ Next i
+
+
+#End ExternalSource
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+End Sub
+End Class
+End Namespace
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/NestedCodeBlocks.vb b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/NestedCodeBlocks.vb
new file mode 100644
index 00000000..1ea3d48b
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/NestedCodeBlocks.vb
@@ -0,0 +1,42 @@
+'------------------------------------------------------------------------------
+' <auto-generated>
+' This code was generated by a tool.
+' Runtime Version:N.N.NNNNN.N
+'
+' Changes to this file may cause incorrect behavior and will be lost if
+' the code is regenerated.
+' </auto-generated>
+'------------------------------------------------------------------------------
+
+Option Strict Off
+Option Explicit On
+
+Imports System
+
+Namespace TestOutput
+Public Class NestedCodeBlocks
+Public Sub New()
+MyBase.New
+End Sub
+Public Overrides Sub Execute()
+
+#ExternalSource("NestedCodeBlocks.vbhtml",1)
+ If True Then
+
+
+#End ExternalSource
+
+#ExternalSource("NestedCodeBlocks.vbhtml",2)
+ If True Then
+ End If
+
+
+#End ExternalSource
+
+#ExternalSource("NestedCodeBlocks.vbhtml",4)
+End If
+
+#End ExternalSource
+End Sub
+End Class
+End Namespace
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/NestedHelpers.vb b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/NestedHelpers.vb
new file mode 100644
index 00000000..f6db0092
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/NestedHelpers.vb
@@ -0,0 +1,90 @@
+'------------------------------------------------------------------------------
+' <auto-generated>
+' This code was generated by a tool.
+' Runtime Version:N.N.NNNNN.N
+'
+' Changes to this file may cause incorrect behavior and will be lost if
+' the code is regenerated.
+' </auto-generated>
+'------------------------------------------------------------------------------
+
+Option Strict Off
+Option Explicit On
+
+Imports System
+
+Namespace TestOutput
+Public Class NestedHelpers
+
+#ExternalSource("NestedHelpers.vbhtml", 3)
+Public Shared Function Bold(s As String) As Template
+
+#End ExternalSource
+Return New Template(Sub (__razor_helper_writer)
+
+#ExternalSource("NestedHelpers.vbhtml", 3)
+
+ s = s.ToUpper()
+
+#End ExternalSource
+WriteLiteralTo(__razor_helper_writer, " ")
+WriteLiteralTo(__razor_helper_writer, "<strong>")
+
+#ExternalSource("NestedHelpers.vbhtml", 5)
+WriteTo(__razor_helper_writer, s)
+
+#End ExternalSource
+WriteLiteralTo(__razor_helper_writer, "</strong>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+#ExternalSource("NestedHelpers.vbhtml", 6)
+
+#End ExternalSource
+End Sub)
+End Function
+
+#ExternalSource("NestedHelpers.vbhtml", 1)
+Public Shared Function Italic(s As String) As Template
+
+#End ExternalSource
+Return New Template(Sub (__razor_helper_writer)
+
+#ExternalSource("NestedHelpers.vbhtml", 1)
+
+ s = s.ToUpper()
+
+#End ExternalSource
+
+#ExternalSource("NestedHelpers.vbhtml", 6)
+
+
+#End ExternalSource
+WriteLiteralTo(__razor_helper_writer, " ")
+WriteLiteralTo(__razor_helper_writer, "<em>")
+
+#ExternalSource("NestedHelpers.vbhtml", 7)
+WriteTo(__razor_helper_writer, Bold(s))
+
+#End ExternalSource
+WriteLiteralTo(__razor_helper_writer, "</em>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+#ExternalSource("NestedHelpers.vbhtml", 8)
+
+#End ExternalSource
+End Sub)
+End Function
+
+Public Sub New()
+MyBase.New
+End Sub
+Public Overrides Sub Execute()
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+
+#ExternalSource("NestedHelpers.vbhtml",10)
+Write(Italic("Hello"))
+
+
+#End ExternalSource
+End Sub
+End Class
+End Namespace
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/NoLinePragmas.vb b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/NoLinePragmas.vb
new file mode 100644
index 00000000..fea68676
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/NoLinePragmas.vb
@@ -0,0 +1,143 @@
+'------------------------------------------------------------------------------
+' <auto-generated>
+' This code was generated by a tool.
+' Runtime Version:N.N.NNNNN.N
+'
+' Changes to this file may cause incorrect behavior and will be lost if
+' the code is regenerated.
+' </auto-generated>
+'------------------------------------------------------------------------------
+
+Option Strict Off
+Option Explicit On
+
+Imports System
+
+Namespace TestOutput
+Public Class NoLinePragmas
+Public Sub New()
+MyBase.New
+End Sub
+Public Overrides Sub Execute()
+
+ Dim i as Integer = 1
+
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+ While i <= 10
+
+WriteLiteral(" ")
+
+WriteLiteral("<p>Hello from VB.Net, #")
+
+ Write(i)
+
+WriteLiteral("</p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+ i += 1
+ 'End While
+End While
+
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+ If i = 11 Then
+
+WriteLiteral(" ")
+
+WriteLiteral("<p>We wrote 10 lines!</p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+ Dim s = "End If"
+End If
+
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+ Select Case i
+ Case 11
+
+WriteLiteral(" ")
+
+WriteLiteral("<p>No really, we wrote 10 lines!</p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+ Case Else
+
+WriteLiteral(" ")
+
+WriteLiteral("<p>Actually, we didn't...</p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+End Select
+
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+ For j as Integer = 1 to 10 Step 2
+
+WriteLiteral(" ")
+
+WriteLiteral("<p>Hello again from VB.Net, #")
+
+ Write(j)
+
+WriteLiteral("</p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+Next
+
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+ Try
+
+WriteLiteral(" ")
+
+WriteLiteral("<p>That time, we wrote 5 lines!</p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+Catch ex as Exception
+
+WriteLiteral(" ")
+
+WriteLiteral("<p>Oh no! An error occurred: ")
+
+ Write(ex.Message)
+
+WriteLiteral("</p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+End Try
+
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+ With i
+
+WriteLiteral(" ")
+
+WriteLiteral("<p>i is now ")
+
+ Write(.ToString())
+
+WriteLiteral("</p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+End With
+
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+ SyncLock New Object()
+
+WriteLiteral(" ")
+
+WriteLiteral("<p>This block is locked, for your security!</p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+End SyncLock
+
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+ Using New System.IO.MemoryStream()
+
+WriteLiteral(" ")
+
+WriteLiteral("<p>Some random memory stream will be disposed after rendering this block</p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+End Using
+
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+Write(SyntaxSampleHelpers.CodeForLink(Me))
+
+End Sub
+End Class
+End Namespace
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Options.vb b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Options.vb
new file mode 100644
index 00000000..7f472d20
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Options.vb
@@ -0,0 +1,28 @@
+'------------------------------------------------------------------------------
+' <auto-generated>
+' This code was generated by a tool.
+' Runtime Version:N.N.NNNNN.N
+'
+' Changes to this file may cause incorrect behavior and will be lost if
+' the code is regenerated.
+' </auto-generated>
+'------------------------------------------------------------------------------
+
+Option Strict On
+Option Explicit Off
+
+Imports System
+
+Namespace TestOutput
+Public Class Options
+Public Sub New()
+MyBase.New
+End Sub
+Public Overrides Sub Execute()
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&"Hello, World!")
+
+End Sub
+End Class
+End Namespace
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/ParserError.vb b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/ParserError.vb
new file mode 100644
index 00000000..dc1c177b
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/ParserError.vb
@@ -0,0 +1,31 @@
+'------------------------------------------------------------------------------
+' <auto-generated>
+' This code was generated by a tool.
+' Runtime Version:N.N.NNNNN.N
+'
+' Changes to this file may cause incorrect behavior and will be lost if
+' the code is regenerated.
+' </auto-generated>
+'------------------------------------------------------------------------------
+
+Option Strict Off
+Option Explicit On
+
+Imports System
+
+Namespace TestOutput
+Public Class ParserError
+Public Sub New()
+MyBase.New
+End Sub
+Public Overrides Sub Execute()
+
+#ExternalSource("ParserError.vbhtml",1)
+
+Foo
+'End Code
+
+#End ExternalSource
+End Sub
+End Class
+End Namespace
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/RazorComments.DesignTime.vb b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/RazorComments.DesignTime.vb
new file mode 100644
index 00000000..925fc5bb
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/RazorComments.DesignTime.vb
@@ -0,0 +1,59 @@
+'------------------------------------------------------------------------------
+' <auto-generated>
+' This code was generated by a tool.
+' Runtime Version:N.N.NNNNN.N
+'
+' Changes to this file may cause incorrect behavior and will be lost if
+' the code is regenerated.
+' </auto-generated>
+'------------------------------------------------------------------------------
+
+Option Strict Off
+Option Explicit On
+
+Imports System
+
+Namespace TestOutput
+Public Class RazorComments
+Private Shared __o As Object
+Public Sub New()
+MyBase.New
+End Sub
+Public Overrides Sub Execute()
+
+#ExternalSource("RazorComments.vbhtml",1)
+
+
+
+#End ExternalSource
+
+#ExternalSource("RazorComments.vbhtml",2)
+
+
+
+#End ExternalSource
+
+#ExternalSource("RazorComments.vbhtml",3)
+ Dim bar As String = "@* bar *@"
+
+#End ExternalSource
+
+#ExternalSource("RazorComments.vbhtml",4)
+ __o = bar
+
+
+#End ExternalSource
+
+#ExternalSource("RazorComments.vbhtml",5)
+__o = a _
+
+#End ExternalSource
+
+#ExternalSource("RazorComments.vbhtml",6)
+ b
+
+
+#End ExternalSource
+End Sub
+End Class
+End Namespace
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/RazorComments.vb b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/RazorComments.vb
new file mode 100644
index 00000000..909c96fe
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/RazorComments.vb
@@ -0,0 +1,72 @@
+'------------------------------------------------------------------------------
+' <auto-generated>
+' This code was generated by a tool.
+' Runtime Version:N.N.NNNNN.N
+'
+' Changes to this file may cause incorrect behavior and will be lost if
+' the code is regenerated.
+' </auto-generated>
+'------------------------------------------------------------------------------
+
+Option Strict Off
+Option Explicit On
+
+Imports System
+
+Namespace TestOutput
+Public Class RazorComments
+Public Sub New()
+MyBase.New
+End Sub
+Public Overrides Sub Execute()
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&"<p>This should ")
+
+WriteLiteral(" be shown</p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+
+#ExternalSource("RazorComments.vbhtml",4)
+
+
+
+#End ExternalSource
+
+#ExternalSource("RazorComments.vbhtml",5)
+
+
+
+#End ExternalSource
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+
+#ExternalSource("RazorComments.vbhtml",8)
+ Dim bar As String = "@* bar *@"
+
+#End ExternalSource
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&"<p>But this should show the comment syntax: ")
+
+
+#ExternalSource("RazorComments.vbhtml",9)
+ Write(bar)
+
+
+#End ExternalSource
+WriteLiteral("</p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&"<p>So should this: ")
+
+WriteLiteral("@* bar *")
+
+WriteLiteral("@</p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+
+#ExternalSource("RazorComments.vbhtml",12)
+Write(a _
+
+#End ExternalSource
+
+#ExternalSource("RazorComments.vbhtml",12)
+ b)
+
+
+#End ExternalSource
+End Sub
+End Class
+End Namespace
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/ResolveUrl.vb b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/ResolveUrl.vb
new file mode 100644
index 00000000..6e876b1d
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/ResolveUrl.vb
@@ -0,0 +1,183 @@
+'------------------------------------------------------------------------------
+' <auto-generated>
+' This code was generated by a tool.
+' Runtime Version:N.N.NNNNN.N
+'
+' Changes to this file may cause incorrect behavior and will be lost if
+' the code is regenerated.
+' </auto-generated>
+'------------------------------------------------------------------------------
+
+Option Strict Off
+Option Explicit On
+
+Imports System
+
+Namespace TestOutput
+Public Class ResolveUrl
+Public Sub New()
+MyBase.New
+End Sub
+Public Overrides Sub Execute()
+WriteLiteral("<a")
+
+WriteAttribute("href", Tuple.Create(" href=""", 2), Tuple.Create("""", 14) _
+, Tuple.Create(Tuple.Create("", 9), Tuple.Create(Of System.Object, System.Int32)(Href("~/Foo") _
+, 9), False) _
+)
+
+WriteLiteral(">Foo</a>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&"<a")
+
+WriteAttribute("href", Tuple.Create(" href=""", 27), Tuple.Create("""", 56) _
+, Tuple.Create(Tuple.Create("", 34), Tuple.Create(Of System.Object, System.Int32)(Href("~/Products/") _
+, 34), False) _
+, Tuple.Create(Tuple.Create("", 45), Tuple.Create(Of System.Object, System.Int32)(product.id _
+, 45), False) _
+)
+
+WriteLiteral(">")
+
+
+#ExternalSource("ResolveUrl.vbhtml",2)
+ Write(product.Name)
+
+
+#End ExternalSource
+WriteLiteral("</a>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&"<a")
+
+WriteAttribute("href", Tuple.Create(" href=""", 79), Tuple.Create("""", 115) _
+, Tuple.Create(Tuple.Create("", 86), Tuple.Create(Of System.Object, System.Int32)(Href("~/Products/") _
+, 86), False) _
+, Tuple.Create(Tuple.Create("", 97), Tuple.Create(Of System.Object, System.Int32)(product.id _
+, 97), False) _
+, Tuple.Create(Tuple.Create("", 108), Tuple.Create("/Detail", 108), True) _
+)
+
+WriteLiteral(">Details</a>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&"<a")
+
+WriteAttribute("href", Tuple.Create(" href=""", 132), Tuple.Create("""", 187) _
+, Tuple.Create(Tuple.Create("", 139), Tuple.Create(Of System.Object, System.Int32)(Href("~/A+Really(Crazy),Url.Is:This/") _
+, 139), False) _
+, Tuple.Create(Tuple.Create("", 169), Tuple.Create(Of System.Object, System.Int32)(product.id _
+, 169), False) _
+, Tuple.Create(Tuple.Create("", 180), Tuple.Create("/Detail", 180), True) _
+)
+
+WriteLiteral(">Crazy Url!</a>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+
+#ExternalSource("ResolveUrl.vbhtml",6)
+
+
+
+#End ExternalSource
+WriteLiteral(" ")
+
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&" <a")
+
+WriteAttribute("href", Tuple.Create(" href=""", 237), Tuple.Create("""", 249) _
+, Tuple.Create(Tuple.Create("", 244), Tuple.Create(Of System.Object, System.Int32)(Href("~/Foo") _
+, 244), False) _
+)
+
+WriteLiteral(">Foo</a>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&" <a")
+
+WriteAttribute("href", Tuple.Create(" href=""", 270), Tuple.Create("""", 299) _
+, Tuple.Create(Tuple.Create("", 277), Tuple.Create(Of System.Object, System.Int32)(Href("~/Products/") _
+, 277), False) _
+, Tuple.Create(Tuple.Create("", 288), Tuple.Create(Of System.Object, System.Int32)(product.id _
+, 288), False) _
+)
+
+WriteLiteral(">")
+
+
+#ExternalSource("ResolveUrl.vbhtml",9)
+ Write(product.Name)
+
+
+#End ExternalSource
+WriteLiteral("</a>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&" <a")
+
+WriteAttribute("href", Tuple.Create(" href=""", 330), Tuple.Create("""", 366) _
+, Tuple.Create(Tuple.Create("", 337), Tuple.Create(Of System.Object, System.Int32)(Href("~/Products/") _
+, 337), False) _
+, Tuple.Create(Tuple.Create("", 348), Tuple.Create(Of System.Object, System.Int32)(product.id _
+, 348), False) _
+, Tuple.Create(Tuple.Create("", 359), Tuple.Create("/Detail", 359), True) _
+)
+
+WriteLiteral(">Details</a>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&" <a")
+
+WriteAttribute("href", Tuple.Create(" href=""", 391), Tuple.Create("""", 446) _
+, Tuple.Create(Tuple.Create("", 398), Tuple.Create(Of System.Object, System.Int32)(Href("~/A+Really(Crazy),Url.Is:This/") _
+, 398), False) _
+, Tuple.Create(Tuple.Create("", 428), Tuple.Create(Of System.Object, System.Int32)(product.id _
+, 428), False) _
+, Tuple.Create(Tuple.Create("", 439), Tuple.Create("/Detail", 439), True) _
+)
+
+WriteLiteral(">Crazy Url!</a>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&" ")
+
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+
+#ExternalSource("ResolveUrl.vbhtml",13)
+
+
+#End ExternalSource
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+DefineSection("Foo", Sub ()
+
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&" <a")
+
+WriteAttribute("href", Tuple.Create(" href=""", 509), Tuple.Create("""", 521) _
+, Tuple.Create(Tuple.Create("", 516), Tuple.Create(Of System.Object, System.Int32)(Href("~/Foo") _
+, 516), False) _
+)
+
+WriteLiteral(">Foo</a>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&" <a")
+
+WriteAttribute("href", Tuple.Create(" href=""", 538), Tuple.Create("""", 567) _
+, Tuple.Create(Tuple.Create("", 545), Tuple.Create(Of System.Object, System.Int32)(Href("~/Products/") _
+, 545), False) _
+, Tuple.Create(Tuple.Create("", 556), Tuple.Create(Of System.Object, System.Int32)(product.id _
+, 556), False) _
+)
+
+WriteLiteral(">")
+
+
+#ExternalSource("ResolveUrl.vbhtml",17)
+ Write(product.Name)
+
+
+#End ExternalSource
+WriteLiteral("</a>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&" <a")
+
+WriteAttribute("href", Tuple.Create(" href=""", 594), Tuple.Create("""", 630) _
+, Tuple.Create(Tuple.Create("", 601), Tuple.Create(Of System.Object, System.Int32)(Href("~/Products/") _
+, 601), False) _
+, Tuple.Create(Tuple.Create("", 612), Tuple.Create(Of System.Object, System.Int32)(product.id _
+, 612), False) _
+, Tuple.Create(Tuple.Create("", 623), Tuple.Create("/Detail", 623), True) _
+)
+
+WriteLiteral(">Details</a>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&" <a")
+
+WriteAttribute("href", Tuple.Create(" href=""", 651), Tuple.Create("""", 706) _
+, Tuple.Create(Tuple.Create("", 658), Tuple.Create(Of System.Object, System.Int32)(Href("~/A+Really(Crazy),Url.Is:This/") _
+, 658), False) _
+, Tuple.Create(Tuple.Create("", 688), Tuple.Create(Of System.Object, System.Int32)(product.id _
+, 688), False) _
+, Tuple.Create(Tuple.Create("", 699), Tuple.Create("/Detail", 699), True) _
+)
+
+WriteLiteral(">Crazy Url!</a>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+End Sub)
+
+End Sub
+End Class
+End Namespace
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Sections.vb b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Sections.vb
new file mode 100644
index 00000000..8b9b34d0
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Sections.vb
@@ -0,0 +1,47 @@
+'------------------------------------------------------------------------------
+' <auto-generated>
+' This code was generated by a tool.
+' Runtime Version:N.N.NNNNN.N
+'
+' Changes to this file may cause incorrect behavior and will be lost if
+' the code is regenerated.
+' </auto-generated>
+'------------------------------------------------------------------------------
+
+Option Strict Off
+Option Explicit On
+
+Imports System
+
+Namespace TestOutput
+Public Class Sections
+Public Sub New()
+MyBase.New
+End Sub
+Public Overrides Sub Execute()
+
+#ExternalSource("Sections.vbhtml",1)
+
+ Layout = "_SectionTestLayout.vbhtml"
+
+
+#End ExternalSource
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&"<div>This is in the Body>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+DefineSection("Section2", Sub ()
+
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&" <div>This is in Section 2</div>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+End Sub)
+
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+DefineSection("Section1", Sub ()
+
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&" <div>This is in Section 1</div>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+End Sub)
+
+End Sub
+End Class
+End Namespace
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Templates.vb b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Templates.vb
new file mode 100644
index 00000000..8d4bcdd3
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/Templates.vb
@@ -0,0 +1,169 @@
+'------------------------------------------------------------------------------
+' <auto-generated>
+' This code was generated by a tool.
+' Runtime Version:N.N.NNNNN.N
+'
+' Changes to this file may cause incorrect behavior and will be lost if
+' the code is regenerated.
+' </auto-generated>
+'------------------------------------------------------------------------------
+
+Option Strict Off
+Option Explicit On
+
+Imports System
+Imports System.Web.Helpers
+
+
+Namespace TestOutput
+Public Class Templates
+
+#ExternalSource("Templates.vbhtml",3)
+
+ Public Function Repeat(times As Integer, template As Func(Of Integer, object)) As HelperResult
+ Return New HelperResult(Sub(writer)
+ For i = 0 to times
+ DirectCast(template(i), HelperResult).WriteTo(writer)
+ Next i
+ End Sub)
+ End Function
+
+#End ExternalSource
+
+Public Sub New()
+MyBase.New
+End Sub
+Public Overrides Sub Execute()
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+
+#ExternalSource("Templates.vbhtml",13)
+
+ Dim foo As Func(Of Object, Object) =
+
+#End ExternalSource
+WriteLiteral(" ")
+
+WriteLiteral("This works ")
+
+
+#ExternalSource("Templates.vbhtml",14)
+ Write(item)
+
+
+#End ExternalSource
+WriteLiteral("!")
+
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+
+#ExternalSource("Templates.vbhtml",15)
+
+
+#End ExternalSource
+
+#ExternalSource("Templates.vbhtml",15)
+Write(foo("too"))
+
+
+#End ExternalSource
+
+#ExternalSource("Templates.vbhtml",15)
+
+
+
+#End ExternalSource
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&"<ul>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+
+#ExternalSource("Templates.vbhtml",19)
+Write(Repeat(10, Function (item) New Template(Sub (__razor_template_writer)
+
+
+#End ExternalSource
+WriteLiteralTo(__razor_template_writer, "<li>Item #")
+
+
+#ExternalSource("Templates.vbhtml",19)
+WriteTo(__razor_template_writer, item)
+
+
+#End ExternalSource
+WriteLiteralTo(__razor_template_writer, "</li>")
+
+
+#ExternalSource("Templates.vbhtml",19)
+ End Sub)))
+
+
+#End ExternalSource
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&"</ul>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&"<p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+
+#ExternalSource("Templates.vbhtml",23)
+Write(Repeat(10,
+ Function (item) New Template(Sub (__razor_template_writer)
+
+
+#End ExternalSource
+WriteLiteralTo(__razor_template_writer, " This is line#")
+
+
+#ExternalSource("Templates.vbhtml",24)
+WriteTo(__razor_template_writer, item)
+
+
+#End ExternalSource
+WriteLiteralTo(__razor_template_writer, " of markup<br/>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+
+#ExternalSource("Templates.vbhtml",25)
+End Sub)))
+
+
+#End ExternalSource
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&"</p>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&"<ul>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+WriteLiteral(" ")
+
+
+#ExternalSource("Templates.vbhtml",29)
+Write(Repeat(10, Function (item) New Template(Sub (__razor_template_writer)
+
+
+#End ExternalSource
+WriteLiteralTo(__razor_template_writer, "<li>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&" Item #")
+
+
+#ExternalSource("Templates.vbhtml",30)
+WriteTo(__razor_template_writer, item)
+
+
+#End ExternalSource
+WriteLiteralTo(__razor_template_writer, ""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10))
+
+
+#ExternalSource("Templates.vbhtml",31)
+
+
+#End ExternalSource
+
+#ExternalSource("Templates.vbhtml",31)
+ Dim parent = item
+
+#End ExternalSource
+WriteLiteralTo(__razor_template_writer, ""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&" <ul>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&" <li>Child Items... ?</li>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&" </ul>"&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&" </li>")
+
+
+#ExternalSource("Templates.vbhtml",35)
+ End Sub)))
+
+
+#End ExternalSource
+WriteLiteral(""&Global.Microsoft.VisualBasic.ChrW(13)&Global.Microsoft.VisualBasic.ChrW(10)&"</ul>")
+
+End Sub
+End Class
+End Namespace
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/UnfinishedExpressionInCode.vb b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/UnfinishedExpressionInCode.vb
new file mode 100644
index 00000000..a2cccc0f
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Output/UnfinishedExpressionInCode.vb
@@ -0,0 +1,43 @@
+'------------------------------------------------------------------------------
+' <auto-generated>
+' This code was generated by a tool.
+' Runtime Version:N.N.NNNNN.N
+'
+' Changes to this file may cause incorrect behavior and will be lost if
+' the code is regenerated.
+' </auto-generated>
+'------------------------------------------------------------------------------
+
+Option Strict Off
+Option Explicit On
+
+Imports System
+
+Namespace TestOutput
+Public Class UnfinishedExpressionInCode
+Private Shared __o As Object
+Public Sub New()
+MyBase.New
+End Sub
+Public Overrides Sub Execute()
+
+#ExternalSource("UnfinishedExpressionInCode.vbhtml",1)
+
+
+
+#End ExternalSource
+
+#ExternalSource("UnfinishedExpressionInCode.vbhtml",2)
+__o = DateTime.
+
+
+#End ExternalSource
+
+#ExternalSource("UnfinishedExpressionInCode.vbhtml",3)
+
+
+
+#End ExternalSource
+End Sub
+End Class
+End Namespace
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/Blocks.vbhtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/Blocks.vbhtml
new file mode 100644
index 00000000..e2e02a69
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/Blocks.vbhtml
@@ -0,0 +1,46 @@
+@Code
+ Dim i as Integer = 1
+End Code
+
+@While i <= 10
+ @<p>Hello from VB.Net, #@(i)</p>
+ i += 1
+End While
+
+@If i = 11 Then
+ @<p>We wrote 10 lines!</p>
+End If
+
+@Do
+ @<p>Hello again: @i</p>
+ i -= 1
+Loop While i > 0
+
+@Select Case i
+ Case 11
+ @<p>No really, we wrote 10 lines!</p>
+ Case Else
+ @<p>We wrote a bunch more lines too!</p>
+End Select
+
+@For j as Integer = 1 to 10 Step 2
+ @<p>Hello again from VB.Net, #@(j)</p>
+Next j
+
+@Try
+ @<p>That time, we wrote 5 lines!</p>
+Catch ex as Exception
+ @<p>Oh no! An error occurred: @(ex.Message)</p>
+End Try
+
+@With i
+ @<p>i is now @(.ToString())</p>
+End With
+
+@SyncLock New Object()
+ @<p>This block is locked, for your security!</p>
+End SyncLock
+
+@Using New System.IO.MemoryStream()
+ @<p>Some random memory stream will be disposed after rendering this block</p>
+End Using \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/CodeBlock.vbhtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/CodeBlock.vbhtml
new file mode 100644
index 00000000..c710eaf1
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/CodeBlock.vbhtml
@@ -0,0 +1,3 @@
+@Code
+ Test()
+End Code \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/CodeBlockAtEOF.vbhtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/CodeBlockAtEOF.vbhtml
new file mode 100644
index 00000000..011d09a1
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/CodeBlockAtEOF.vbhtml
@@ -0,0 +1,3 @@
+This is markup
+
+@Code \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/ConditionalAttributes.vbhtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/ConditionalAttributes.vbhtml
new file mode 100644
index 00000000..2e49d75f
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/ConditionalAttributes.vbhtml
@@ -0,0 +1,12 @@
+@Code
+ Dim ch = True
+ Dim cls = "bar"
+ @<a href="Foo" />
+ @<p class="@cls" />
+ @<p class="foo @cls" />
+ @<p class="@cls foo" />
+ @<input type="checkbox" checked="@ch" />
+ @<input type="checkbox" checked="foo @ch" />
+ @<p class="@If cls IsNot Nothing Then @cls End If" />
+ @<a href="~/Foo" />
+End Code \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/DesignTime.vbhtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/DesignTime.vbhtml
new file mode 100644
index 00000000..cfd33e71
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/DesignTime.vbhtml
@@ -0,0 +1,21 @@
+<div>
+ @For i = 1 to 10
+@<p>This is item #@i</p>
+ Next
+</div>
+
+<p>
+@(Foo(Bar.Baz))
+@Foo(@@<p>Bar @baz Biz</p>)
+</p>
+
+@Section Footer
+ <p>Foo</p>
+ @bar
+End Section
+
+@Helper Foo()
+ If True Then
+ @<p>Foo</p>
+ End If
+End Helper \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/EmptyExplicitExpression.vbhtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/EmptyExplicitExpression.vbhtml
new file mode 100644
index 00000000..6790c7eb
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/EmptyExplicitExpression.vbhtml
@@ -0,0 +1,3 @@
+This is markup
+
+@() \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/EmptyImplicitExpression.vbhtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/EmptyImplicitExpression.vbhtml
new file mode 100644
index 00000000..021306da
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/EmptyImplicitExpression.vbhtml
@@ -0,0 +1,3 @@
+This is markup
+
+@! \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/EmptyImplicitExpressionInCode.vbhtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/EmptyImplicitExpressionInCode.vbhtml
new file mode 100644
index 00000000..deb98d53
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/EmptyImplicitExpressionInCode.vbhtml
@@ -0,0 +1,3 @@
+@Code
+ @
+End Code \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/ExplicitExpression.vbhtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/ExplicitExpression.vbhtml
new file mode 100644
index 00000000..b65eee56
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/ExplicitExpression.vbhtml
@@ -0,0 +1 @@
+@(Foo(Bar.Baz)) \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/ExplicitExpressionAtEOF.vbhtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/ExplicitExpressionAtEOF.vbhtml
new file mode 100644
index 00000000..a0fdfc9a
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/ExplicitExpressionAtEOF.vbhtml
@@ -0,0 +1,3 @@
+This is markup
+
+@( \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/ExpressionsInCode.vbhtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/ExpressionsInCode.vbhtml
new file mode 100644
index 00000000..3416ee9a
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/ExpressionsInCode.vbhtml
@@ -0,0 +1,16 @@
+@Code
+ Dim foo As Object = Nothing
+ Dim bar as String = "Foo"
+End Code
+
+@If foo IsNot Nothing Then
+ @foo
+Else
+ @<p>Foo is Null!</p>
+End If
+
+<p>
+@If Not String.IsNullOrEmpty(bar) Then
+ @(bar.Replace("F", "B"))
+End If
+</p> \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/FunctionsBlock.vbhtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/FunctionsBlock.vbhtml
new file mode 100644
index 00000000..bcb78211
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/FunctionsBlock.vbhtml
@@ -0,0 +1,12 @@
+@Functions
+
+End Functions
+
+@Functions
+ Private _rand as New Random()
+ Private Function RandomInt() as Integer
+ Return _rand.Next()
+ End Function
+End Functions
+
+Here's a random number: @RandomInt() \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/Helpers.vbhtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/Helpers.vbhtml
new file mode 100644
index 00000000..6fead0cc
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/Helpers.vbhtml
@@ -0,0 +1,11 @@
+@Helper Bold(s as String)
+ s = s.ToUpper()
+ @<strong>@s</strong>
+End Helper
+
+@Helper Italic(s as String)
+ s = s.ToUpper()
+ @<em>@s</em>
+End Helper
+
+@Bold("Hello") \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/HelpersMissingCloseParen.vbhtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/HelpersMissingCloseParen.vbhtml
new file mode 100644
index 00000000..d81d24b2
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/HelpersMissingCloseParen.vbhtml
@@ -0,0 +1,8 @@
+@Helper Bold(s as String)
+ s = s.ToUpper()
+ @<strong>@s</strong>
+End Helper
+
+@Helper Italic(s as String
+
+@Bold("Hello") \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/HelpersMissingName.vbhtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/HelpersMissingName.vbhtml
new file mode 100644
index 00000000..36ec5a7e
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/HelpersMissingName.vbhtml
@@ -0,0 +1 @@
+@Helper \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/HelpersMissingOpenParen.vbhtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/HelpersMissingOpenParen.vbhtml
new file mode 100644
index 00000000..145bda68
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/HelpersMissingOpenParen.vbhtml
@@ -0,0 +1,8 @@
+@Helper Bold(s as String)
+ s = s.ToUpper()
+ @<strong>@s</strong>
+End Helper
+
+@Helper Italic
+
+@Bold("Hello") \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/ImplicitExpression.vbhtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/ImplicitExpression.vbhtml
new file mode 100644
index 00000000..fb6f5281
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/ImplicitExpression.vbhtml
@@ -0,0 +1,5 @@
+@For i = 1 To 10
+ @<p>This is item #@i</p>
+Next
+
+@SyntaxSampleHelpers.CodeForLink(Me) \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/ImplicitExpressionAtEOF.vbhtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/ImplicitExpressionAtEOF.vbhtml
new file mode 100644
index 00000000..365d20e0
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/ImplicitExpressionAtEOF.vbhtml
@@ -0,0 +1,3 @@
+This is markup
+
+@ \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/Imports.vbhtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/Imports.vbhtml
new file mode 100644
index 00000000..da3f0ec1
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/Imports.vbhtml
@@ -0,0 +1,6 @@
+@Imports System.IO
+@Imports Foo = System.Text.Encoding
+@Imports System
+
+<p>Path's full type name is @GetType(Path).FullName</p>
+<p>Foo's actual full type name is @GetType(Foo).FullName</p> \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/Inherits.vbhtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/Inherits.vbhtml
new file mode 100644
index 00000000..cc0a2d2a
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/Inherits.vbhtml
@@ -0,0 +1 @@
+@Inherits System.Web.WebPages.WebPage
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/Instrumented.vbhtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/Instrumented.vbhtml
new file mode 100644
index 00000000..510aae4b
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/Instrumented.vbhtml
@@ -0,0 +1,53 @@
+@Helper Strong(s As String)
+ @<strong>@s</strong>
+End Helper
+
+@Code
+ Dim i As Integer = 1
+ Dim foo = @@<p>Foo</p>
+ @:Hello, World!
+ @<p>Hello, World!</p>
+End Code
+
+@While i <= 10
+ @<p>Hello from VB.Net, #@(i)</p>
+ i += 1
+End While
+
+@If i = 11 Then
+ @<p>We wrote 10 lines!</p>
+End If
+
+@Do
+ @<p>Hello again: @i</p>
+ i -= 1
+Loop While i > 0
+
+@Select Case i
+ Case 11
+ @<p>No really, we wrote 10 lines!</p>
+ Case Else
+ @<p>We wrote a bunch more lines too!</p>
+End Select
+
+@For j as Integer = 1 to 10 Step 2
+ @<p>Hello again from VB.Net, #@(j)</p>
+Next j
+
+@Try
+ @<p>That time, we wrote 5 lines!</p>
+Catch ex as Exception
+ @<p>Oh no! An error occurred: @(ex.Message)</p>
+End Try
+
+@With i
+ @<p>i is now @(.ToString())</p>
+End With
+
+@SyncLock New Object()
+ @<p>This block is locked, for your security!</p>
+End SyncLock
+
+@Using New System.IO.MemoryStream()
+ @<p>Some random memory stream will be disposed after rendering this block</p>
+End Using \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/LayoutDirective.vbhtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/LayoutDirective.vbhtml
new file mode 100644
index 00000000..58904f11
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/LayoutDirective.vbhtml
@@ -0,0 +1 @@
+@Layout ~/Foo/Bar/Baz \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/MarkupInCodeBlock.vbhtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/MarkupInCodeBlock.vbhtml
new file mode 100644
index 00000000..83289134
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/MarkupInCodeBlock.vbhtml
@@ -0,0 +1,5 @@
+@Code
+ For i = 1 To 10
+ @<p>Hello from VB.Net, #@(i.ToString())</p>
+ Next i
+End Code
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/NestedCodeBlocks.vbhtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/NestedCodeBlocks.vbhtml
new file mode 100644
index 00000000..83f518ac
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/NestedCodeBlocks.vbhtml
@@ -0,0 +1,4 @@
+@If True Then
+ @If True Then
+ End If
+End If \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/NestedHelpers.vbhtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/NestedHelpers.vbhtml
new file mode 100644
index 00000000..4c52502b
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/NestedHelpers.vbhtml
@@ -0,0 +1,10 @@
+@Helper Italic(s As String)
+ s = s.ToUpper()
+ @Helper Bold(s As String)
+ s = s.ToUpper()
+ @<strong>@s</strong>
+ End Helper
+ @<em>@Bold(s)</em>
+End Helper
+
+@Italic("Hello") \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/NoLinePragmas.vbhtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/NoLinePragmas.vbhtml
new file mode 100644
index 00000000..4e9df788
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/NoLinePragmas.vbhtml
@@ -0,0 +1,45 @@
+@Code
+ Dim i as Integer = 1
+End Code
+
+@While i <= 10
+ @<p>Hello from VB.Net, #@(i)</p>
+ i += 1
+ 'End While
+End While
+
+@If i = 11 Then
+ @<p>We wrote 10 lines!</p>
+ Dim s = "End If"
+End If
+
+@Select Case i
+ Case 11
+ @<p>No really, we wrote 10 lines!</p>
+ Case Else
+ @<p>Actually, we didn't...</p>
+End Select
+
+@For j as Integer = 1 to 10 Step 2
+ @<p>Hello again from VB.Net, #@(j)</p>
+Next
+
+@Try
+ @<p>That time, we wrote 5 lines!</p>
+Catch ex as Exception
+ @<p>Oh no! An error occurred: @(ex.Message)</p>
+End Try
+
+@With i
+ @<p>i is now @(.ToString())</p>
+End With
+
+@SyncLock New Object()
+ @<p>This block is locked, for your security!</p>
+End SyncLock
+
+@Using New System.IO.MemoryStream()
+ @<p>Some random memory stream will be disposed after rendering this block</p>
+End Using
+
+@SyntaxSampleHelpers.CodeForLink(Me) \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/Options.vbhtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/Options.vbhtml
new file mode 100644
index 00000000..0cd49aa3
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/Options.vbhtml
@@ -0,0 +1,4 @@
+@Option Strict On
+@Option Explicit Off
+
+Hello, World! \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/ParserError.vbhtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/ParserError.vbhtml
new file mode 100644
index 00000000..2d2e4227
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/ParserError.vbhtml
@@ -0,0 +1,3 @@
+@Code
+Foo
+'End Code \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/RazorComments.vbhtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/RazorComments.vbhtml
new file mode 100644
index 00000000..7c638d63
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/RazorComments.vbhtml
@@ -0,0 +1,12 @@
+@*This is not going to be rendered*@
+<p>This should @* not *@ be shown</p>
+
+@Code
+ @* Throw new Exception("Oh no!") *@
+End Code
+
+@Code Dim bar As String = "@* bar *@" End Code
+<p>But this should show the comment syntax: @bar</p>
+<p>So should this: @@* bar *@@</p>
+
+@(a@**@b) \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/ResolveUrl.vbhtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/ResolveUrl.vbhtml
new file mode 100644
index 00000000..dc20f644
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/ResolveUrl.vbhtml
@@ -0,0 +1,20 @@
+<a href="~/Foo">Foo</a>
+<a href="~/Products/@product.id">@product.Name</a>
+<a href="~/Products/@product.id/Detail">Details</a>
+<a href="~/A+Really(Crazy),Url.Is:This/@product.id/Detail">Crazy Url!</a>
+
+@Code
+ @<text>
+ <a href="~/Foo">Foo</a>
+ <a href="~/Products/@product.id">@product.Name</a>
+ <a href="~/Products/@product.id/Detail">Details</a>
+ <a href="~/A+Really(Crazy),Url.Is:This/@product.id/Detail">Crazy Url!</a>
+ </text>
+End Code
+
+@Section Foo
+ <a href="~/Foo">Foo</a>
+ <a href="~/Products/@product.id">@product.Name</a>
+ <a href="~/Products/@product.id/Detail">Details</a>
+ <a href="~/A+Really(Crazy),Url.Is:This/@product.id/Detail">Crazy Url!</a>
+End Section \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/Sections.vbhtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/Sections.vbhtml
new file mode 100644
index 00000000..ad05f5cd
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/Sections.vbhtml
@@ -0,0 +1,13 @@
+@Code
+ Layout = "_SectionTestLayout.vbhtml"
+End Code
+
+<div>This is in the Body>
+
+@Section Section2
+ <div>This is in Section 2</div>
+End Section
+
+@Section Section1
+ <div>This is in Section 1</div>
+End Section \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/Templates.vbhtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/Templates.vbhtml
new file mode 100644
index 00000000..80e17703
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/Templates.vbhtml
@@ -0,0 +1,36 @@
+@Imports System.Web.Helpers
+
+@Functions
+ Public Function Repeat(times As Integer, template As Func(Of Integer, object)) As HelperResult
+ Return New HelperResult(Sub(writer)
+ For i = 0 to times
+ DirectCast(template(i), HelperResult).WriteTo(writer)
+ Next i
+ End Sub)
+ End Function
+End Functions
+
+@Code
+ Dim foo As Func(Of Object, Object) = @<text>This works @item!</text>
+ @foo("too")
+End Code
+
+<ul>
+@(Repeat(10, @@<li>Item #@item</li>))
+</ul>
+
+<p>
+@Repeat(10,
+ @@: This is line#@item of markup<br/>
+)
+</p>
+
+<ul>
+ @Repeat(10, @@<li>
+ Item #@item
+ @Code Dim parent = item End Code
+ <ul>
+ <li>Child Items... ?</li>
+ </ul>
+ </li>)
+</ul> \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/UnfinishedExpressionInCode.vbhtml b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/UnfinishedExpressionInCode.vbhtml
new file mode 100644
index 00000000..4b4acb3b
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/CodeGenerator/VB/Source/UnfinishedExpressionInCode.vbhtml
@@ -0,0 +1,3 @@
+@Code
+@DateTime.
+End Code \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/DesignTime/Simple.cshtml b/test/System.Web.Razor.Test/TestFiles/DesignTime/Simple.cshtml
new file mode 100644
index 00000000..b50db22f
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/DesignTime/Simple.cshtml
@@ -0,0 +1,16 @@
+@{
+ string hello = "Hello, World";
+}
+
+<html>
+ <head>
+ <title>Simple Page</title>
+ </head>
+ <body>
+ <h1>Simple Page</h1>
+ <p>@hello</p>
+ <p>
+ @foreach(char c in hello) {@c}
+ </p>
+ </body>
+</html> \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/TestFiles/DesignTime/Simple.txt b/test/System.Web.Razor.Test/TestFiles/DesignTime/Simple.txt
new file mode 100644
index 00000000..4eaa2321
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/DesignTime/Simple.txt
@@ -0,0 +1,60 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:N.N.NNNNN.N
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Razor {
+
+
+ public class @__CompiledTemplate {
+
+ private static object @__o;
+
+#line hidden
+
+ public @__CompiledTemplate() {
+ }
+
+ public override void Execute() {
+
+ #line 1 "C:\This\Path\Is\Just\For\Line\Pragmas.cshtml"
+
+ string hello = "Hello, World";
+
+
+ #line default
+ #line hidden
+
+ #line 2 "C:\This\Path\Is\Just\For\Line\Pragmas.cshtml"
+ __o = hello;
+
+
+ #line default
+ #line hidden
+
+ #line 3 "C:\This\Path\Is\Just\For\Line\Pragmas.cshtml"
+ foreach(char c in hello) {
+
+ #line default
+ #line hidden
+
+ #line 4 "C:\This\Path\Is\Just\For\Line\Pragmas.cshtml"
+ __o = c;
+
+
+ #line default
+ #line hidden
+
+ #line 5 "C:\This\Path\Is\Just\For\Line\Pragmas.cshtml"
+ }
+
+ #line default
+ #line hidden
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/TestFiles/nested-1000.html b/test/System.Web.Razor.Test/TestFiles/nested-1000.html
new file mode 100644
index 00000000..3c35bdbc
--- /dev/null
+++ b/test/System.Web.Razor.Test/TestFiles/nested-1000.html
@@ -0,0 +1,2002 @@
+<outer>
+<elem-1>
+<elem-2>
+<elem-3>
+<elem-4>
+<elem-5>
+<elem-6>
+<elem-7>
+<elem-8>
+<elem-9>
+<elem-10>
+<elem-11>
+<elem-12>
+<elem-13>
+<elem-14>
+<elem-15>
+<elem-16>
+<elem-17>
+<elem-18>
+<elem-19>
+<elem-20>
+<elem-21>
+<elem-22>
+<elem-23>
+<elem-24>
+<elem-25>
+<elem-26>
+<elem-27>
+<elem-28>
+<elem-29>
+<elem-30>
+<elem-31>
+<elem-32>
+<elem-33>
+<elem-34>
+<elem-35>
+<elem-36>
+<elem-37>
+<elem-38>
+<elem-39>
+<elem-40>
+<elem-41>
+<elem-42>
+<elem-43>
+<elem-44>
+<elem-45>
+<elem-46>
+<elem-47>
+<elem-48>
+<elem-49>
+<elem-50>
+<elem-51>
+<elem-52>
+<elem-53>
+<elem-54>
+<elem-55>
+<elem-56>
+<elem-57>
+<elem-58>
+<elem-59>
+<elem-60>
+<elem-61>
+<elem-62>
+<elem-63>
+<elem-64>
+<elem-65>
+<elem-66>
+<elem-67>
+<elem-68>
+<elem-69>
+<elem-70>
+<elem-71>
+<elem-72>
+<elem-73>
+<elem-74>
+<elem-75>
+<elem-76>
+<elem-77>
+<elem-78>
+<elem-79>
+<elem-80>
+<elem-81>
+<elem-82>
+<elem-83>
+<elem-84>
+<elem-85>
+<elem-86>
+<elem-87>
+<elem-88>
+<elem-89>
+<elem-90>
+<elem-91>
+<elem-92>
+<elem-93>
+<elem-94>
+<elem-95>
+<elem-96>
+<elem-97>
+<elem-98>
+<elem-99>
+<elem-100>
+<elem-101>
+<elem-102>
+<elem-103>
+<elem-104>
+<elem-105>
+<elem-106>
+<elem-107>
+<elem-108>
+<elem-109>
+<elem-110>
+<elem-111>
+<elem-112>
+<elem-113>
+<elem-114>
+<elem-115>
+<elem-116>
+<elem-117>
+<elem-118>
+<elem-119>
+<elem-120>
+<elem-121>
+<elem-122>
+<elem-123>
+<elem-124>
+<elem-125>
+<elem-126>
+<elem-127>
+<elem-128>
+<elem-129>
+<elem-130>
+<elem-131>
+<elem-132>
+<elem-133>
+<elem-134>
+<elem-135>
+<elem-136>
+<elem-137>
+<elem-138>
+<elem-139>
+<elem-140>
+<elem-141>
+<elem-142>
+<elem-143>
+<elem-144>
+<elem-145>
+<elem-146>
+<elem-147>
+<elem-148>
+<elem-149>
+<elem-150>
+<elem-151>
+<elem-152>
+<elem-153>
+<elem-154>
+<elem-155>
+<elem-156>
+<elem-157>
+<elem-158>
+<elem-159>
+<elem-160>
+<elem-161>
+<elem-162>
+<elem-163>
+<elem-164>
+<elem-165>
+<elem-166>
+<elem-167>
+<elem-168>
+<elem-169>
+<elem-170>
+<elem-171>
+<elem-172>
+<elem-173>
+<elem-174>
+<elem-175>
+<elem-176>
+<elem-177>
+<elem-178>
+<elem-179>
+<elem-180>
+<elem-181>
+<elem-182>
+<elem-183>
+<elem-184>
+<elem-185>
+<elem-186>
+<elem-187>
+<elem-188>
+<elem-189>
+<elem-190>
+<elem-191>
+<elem-192>
+<elem-193>
+<elem-194>
+<elem-195>
+<elem-196>
+<elem-197>
+<elem-198>
+<elem-199>
+<elem-200>
+<elem-201>
+<elem-202>
+<elem-203>
+<elem-204>
+<elem-205>
+<elem-206>
+<elem-207>
+<elem-208>
+<elem-209>
+<elem-210>
+<elem-211>
+<elem-212>
+<elem-213>
+<elem-214>
+<elem-215>
+<elem-216>
+<elem-217>
+<elem-218>
+<elem-219>
+<elem-220>
+<elem-221>
+<elem-222>
+<elem-223>
+<elem-224>
+<elem-225>
+<elem-226>
+<elem-227>
+<elem-228>
+<elem-229>
+<elem-230>
+<elem-231>
+<elem-232>
+<elem-233>
+<elem-234>
+<elem-235>
+<elem-236>
+<elem-237>
+<elem-238>
+<elem-239>
+<elem-240>
+<elem-241>
+<elem-242>
+<elem-243>
+<elem-244>
+<elem-245>
+<elem-246>
+<elem-247>
+<elem-248>
+<elem-249>
+<elem-250>
+<elem-251>
+<elem-252>
+<elem-253>
+<elem-254>
+<elem-255>
+<elem-256>
+<elem-257>
+<elem-258>
+<elem-259>
+<elem-260>
+<elem-261>
+<elem-262>
+<elem-263>
+<elem-264>
+<elem-265>
+<elem-266>
+<elem-267>
+<elem-268>
+<elem-269>
+<elem-270>
+<elem-271>
+<elem-272>
+<elem-273>
+<elem-274>
+<elem-275>
+<elem-276>
+<elem-277>
+<elem-278>
+<elem-279>
+<elem-280>
+<elem-281>
+<elem-282>
+<elem-283>
+<elem-284>
+<elem-285>
+<elem-286>
+<elem-287>
+<elem-288>
+<elem-289>
+<elem-290>
+<elem-291>
+<elem-292>
+<elem-293>
+<elem-294>
+<elem-295>
+<elem-296>
+<elem-297>
+<elem-298>
+<elem-299>
+<elem-300>
+<elem-301>
+<elem-302>
+<elem-303>
+<elem-304>
+<elem-305>
+<elem-306>
+<elem-307>
+<elem-308>
+<elem-309>
+<elem-310>
+<elem-311>
+<elem-312>
+<elem-313>
+<elem-314>
+<elem-315>
+<elem-316>
+<elem-317>
+<elem-318>
+<elem-319>
+<elem-320>
+<elem-321>
+<elem-322>
+<elem-323>
+<elem-324>
+<elem-325>
+<elem-326>
+<elem-327>
+<elem-328>
+<elem-329>
+<elem-330>
+<elem-331>
+<elem-332>
+<elem-333>
+<elem-334>
+<elem-335>
+<elem-336>
+<elem-337>
+<elem-338>
+<elem-339>
+<elem-340>
+<elem-341>
+<elem-342>
+<elem-343>
+<elem-344>
+<elem-345>
+<elem-346>
+<elem-347>
+<elem-348>
+<elem-349>
+<elem-350>
+<elem-351>
+<elem-352>
+<elem-353>
+<elem-354>
+<elem-355>
+<elem-356>
+<elem-357>
+<elem-358>
+<elem-359>
+<elem-360>
+<elem-361>
+<elem-362>
+<elem-363>
+<elem-364>
+<elem-365>
+<elem-366>
+<elem-367>
+<elem-368>
+<elem-369>
+<elem-370>
+<elem-371>
+<elem-372>
+<elem-373>
+<elem-374>
+<elem-375>
+<elem-376>
+<elem-377>
+<elem-378>
+<elem-379>
+<elem-380>
+<elem-381>
+<elem-382>
+<elem-383>
+<elem-384>
+<elem-385>
+<elem-386>
+<elem-387>
+<elem-388>
+<elem-389>
+<elem-390>
+<elem-391>
+<elem-392>
+<elem-393>
+<elem-394>
+<elem-395>
+<elem-396>
+<elem-397>
+<elem-398>
+<elem-399>
+<elem-400>
+<elem-401>
+<elem-402>
+<elem-403>
+<elem-404>
+<elem-405>
+<elem-406>
+<elem-407>
+<elem-408>
+<elem-409>
+<elem-410>
+<elem-411>
+<elem-412>
+<elem-413>
+<elem-414>
+<elem-415>
+<elem-416>
+<elem-417>
+<elem-418>
+<elem-419>
+<elem-420>
+<elem-421>
+<elem-422>
+<elem-423>
+<elem-424>
+<elem-425>
+<elem-426>
+<elem-427>
+<elem-428>
+<elem-429>
+<elem-430>
+<elem-431>
+<elem-432>
+<elem-433>
+<elem-434>
+<elem-435>
+<elem-436>
+<elem-437>
+<elem-438>
+<elem-439>
+<elem-440>
+<elem-441>
+<elem-442>
+<elem-443>
+<elem-444>
+<elem-445>
+<elem-446>
+<elem-447>
+<elem-448>
+<elem-449>
+<elem-450>
+<elem-451>
+<elem-452>
+<elem-453>
+<elem-454>
+<elem-455>
+<elem-456>
+<elem-457>
+<elem-458>
+<elem-459>
+<elem-460>
+<elem-461>
+<elem-462>
+<elem-463>
+<elem-464>
+<elem-465>
+<elem-466>
+<elem-467>
+<elem-468>
+<elem-469>
+<elem-470>
+<elem-471>
+<elem-472>
+<elem-473>
+<elem-474>
+<elem-475>
+<elem-476>
+<elem-477>
+<elem-478>
+<elem-479>
+<elem-480>
+<elem-481>
+<elem-482>
+<elem-483>
+<elem-484>
+<elem-485>
+<elem-486>
+<elem-487>
+<elem-488>
+<elem-489>
+<elem-490>
+<elem-491>
+<elem-492>
+<elem-493>
+<elem-494>
+<elem-495>
+<elem-496>
+<elem-497>
+<elem-498>
+<elem-499>
+<elem-500>
+<elem-501>
+<elem-502>
+<elem-503>
+<elem-504>
+<elem-505>
+<elem-506>
+<elem-507>
+<elem-508>
+<elem-509>
+<elem-510>
+<elem-511>
+<elem-512>
+<elem-513>
+<elem-514>
+<elem-515>
+<elem-516>
+<elem-517>
+<elem-518>
+<elem-519>
+<elem-520>
+<elem-521>
+<elem-522>
+<elem-523>
+<elem-524>
+<elem-525>
+<elem-526>
+<elem-527>
+<elem-528>
+<elem-529>
+<elem-530>
+<elem-531>
+<elem-532>
+<elem-533>
+<elem-534>
+<elem-535>
+<elem-536>
+<elem-537>
+<elem-538>
+<elem-539>
+<elem-540>
+<elem-541>
+<elem-542>
+<elem-543>
+<elem-544>
+<elem-545>
+<elem-546>
+<elem-547>
+<elem-548>
+<elem-549>
+<elem-550>
+<elem-551>
+<elem-552>
+<elem-553>
+<elem-554>
+<elem-555>
+<elem-556>
+<elem-557>
+<elem-558>
+<elem-559>
+<elem-560>
+<elem-561>
+<elem-562>
+<elem-563>
+<elem-564>
+<elem-565>
+<elem-566>
+<elem-567>
+<elem-568>
+<elem-569>
+<elem-570>
+<elem-571>
+<elem-572>
+<elem-573>
+<elem-574>
+<elem-575>
+<elem-576>
+<elem-577>
+<elem-578>
+<elem-579>
+<elem-580>
+<elem-581>
+<elem-582>
+<elem-583>
+<elem-584>
+<elem-585>
+<elem-586>
+<elem-587>
+<elem-588>
+<elem-589>
+<elem-590>
+<elem-591>
+<elem-592>
+<elem-593>
+<elem-594>
+<elem-595>
+<elem-596>
+<elem-597>
+<elem-598>
+<elem-599>
+<elem-600>
+<elem-601>
+<elem-602>
+<elem-603>
+<elem-604>
+<elem-605>
+<elem-606>
+<elem-607>
+<elem-608>
+<elem-609>
+<elem-610>
+<elem-611>
+<elem-612>
+<elem-613>
+<elem-614>
+<elem-615>
+<elem-616>
+<elem-617>
+<elem-618>
+<elem-619>
+<elem-620>
+<elem-621>
+<elem-622>
+<elem-623>
+<elem-624>
+<elem-625>
+<elem-626>
+<elem-627>
+<elem-628>
+<elem-629>
+<elem-630>
+<elem-631>
+<elem-632>
+<elem-633>
+<elem-634>
+<elem-635>
+<elem-636>
+<elem-637>
+<elem-638>
+<elem-639>
+<elem-640>
+<elem-641>
+<elem-642>
+<elem-643>
+<elem-644>
+<elem-645>
+<elem-646>
+<elem-647>
+<elem-648>
+<elem-649>
+<elem-650>
+<elem-651>
+<elem-652>
+<elem-653>
+<elem-654>
+<elem-655>
+<elem-656>
+<elem-657>
+<elem-658>
+<elem-659>
+<elem-660>
+<elem-661>
+<elem-662>
+<elem-663>
+<elem-664>
+<elem-665>
+<elem-666>
+<elem-667>
+<elem-668>
+<elem-669>
+<elem-670>
+<elem-671>
+<elem-672>
+<elem-673>
+<elem-674>
+<elem-675>
+<elem-676>
+<elem-677>
+<elem-678>
+<elem-679>
+<elem-680>
+<elem-681>
+<elem-682>
+<elem-683>
+<elem-684>
+<elem-685>
+<elem-686>
+<elem-687>
+<elem-688>
+<elem-689>
+<elem-690>
+<elem-691>
+<elem-692>
+<elem-693>
+<elem-694>
+<elem-695>
+<elem-696>
+<elem-697>
+<elem-698>
+<elem-699>
+<elem-700>
+<elem-701>
+<elem-702>
+<elem-703>
+<elem-704>
+<elem-705>
+<elem-706>
+<elem-707>
+<elem-708>
+<elem-709>
+<elem-710>
+<elem-711>
+<elem-712>
+<elem-713>
+<elem-714>
+<elem-715>
+<elem-716>
+<elem-717>
+<elem-718>
+<elem-719>
+<elem-720>
+<elem-721>
+<elem-722>
+<elem-723>
+<elem-724>
+<elem-725>
+<elem-726>
+<elem-727>
+<elem-728>
+<elem-729>
+<elem-730>
+<elem-731>
+<elem-732>
+<elem-733>
+<elem-734>
+<elem-735>
+<elem-736>
+<elem-737>
+<elem-738>
+<elem-739>
+<elem-740>
+<elem-741>
+<elem-742>
+<elem-743>
+<elem-744>
+<elem-745>
+<elem-746>
+<elem-747>
+<elem-748>
+<elem-749>
+<elem-750>
+<elem-751>
+<elem-752>
+<elem-753>
+<elem-754>
+<elem-755>
+<elem-756>
+<elem-757>
+<elem-758>
+<elem-759>
+<elem-760>
+<elem-761>
+<elem-762>
+<elem-763>
+<elem-764>
+<elem-765>
+<elem-766>
+<elem-767>
+<elem-768>
+<elem-769>
+<elem-770>
+<elem-771>
+<elem-772>
+<elem-773>
+<elem-774>
+<elem-775>
+<elem-776>
+<elem-777>
+<elem-778>
+<elem-779>
+<elem-780>
+<elem-781>
+<elem-782>
+<elem-783>
+<elem-784>
+<elem-785>
+<elem-786>
+<elem-787>
+<elem-788>
+<elem-789>
+<elem-790>
+<elem-791>
+<elem-792>
+<elem-793>
+<elem-794>
+<elem-795>
+<elem-796>
+<elem-797>
+<elem-798>
+<elem-799>
+<elem-800>
+<elem-801>
+<elem-802>
+<elem-803>
+<elem-804>
+<elem-805>
+<elem-806>
+<elem-807>
+<elem-808>
+<elem-809>
+<elem-810>
+<elem-811>
+<elem-812>
+<elem-813>
+<elem-814>
+<elem-815>
+<elem-816>
+<elem-817>
+<elem-818>
+<elem-819>
+<elem-820>
+<elem-821>
+<elem-822>
+<elem-823>
+<elem-824>
+<elem-825>
+<elem-826>
+<elem-827>
+<elem-828>
+<elem-829>
+<elem-830>
+<elem-831>
+<elem-832>
+<elem-833>
+<elem-834>
+<elem-835>
+<elem-836>
+<elem-837>
+<elem-838>
+<elem-839>
+<elem-840>
+<elem-841>
+<elem-842>
+<elem-843>
+<elem-844>
+<elem-845>
+<elem-846>
+<elem-847>
+<elem-848>
+<elem-849>
+<elem-850>
+<elem-851>
+<elem-852>
+<elem-853>
+<elem-854>
+<elem-855>
+<elem-856>
+<elem-857>
+<elem-858>
+<elem-859>
+<elem-860>
+<elem-861>
+<elem-862>
+<elem-863>
+<elem-864>
+<elem-865>
+<elem-866>
+<elem-867>
+<elem-868>
+<elem-869>
+<elem-870>
+<elem-871>
+<elem-872>
+<elem-873>
+<elem-874>
+<elem-875>
+<elem-876>
+<elem-877>
+<elem-878>
+<elem-879>
+<elem-880>
+<elem-881>
+<elem-882>
+<elem-883>
+<elem-884>
+<elem-885>
+<elem-886>
+<elem-887>
+<elem-888>
+<elem-889>
+<elem-890>
+<elem-891>
+<elem-892>
+<elem-893>
+<elem-894>
+<elem-895>
+<elem-896>
+<elem-897>
+<elem-898>
+<elem-899>
+<elem-900>
+<elem-901>
+<elem-902>
+<elem-903>
+<elem-904>
+<elem-905>
+<elem-906>
+<elem-907>
+<elem-908>
+<elem-909>
+<elem-910>
+<elem-911>
+<elem-912>
+<elem-913>
+<elem-914>
+<elem-915>
+<elem-916>
+<elem-917>
+<elem-918>
+<elem-919>
+<elem-920>
+<elem-921>
+<elem-922>
+<elem-923>
+<elem-924>
+<elem-925>
+<elem-926>
+<elem-927>
+<elem-928>
+<elem-929>
+<elem-930>
+<elem-931>
+<elem-932>
+<elem-933>
+<elem-934>
+<elem-935>
+<elem-936>
+<elem-937>
+<elem-938>
+<elem-939>
+<elem-940>
+<elem-941>
+<elem-942>
+<elem-943>
+<elem-944>
+<elem-945>
+<elem-946>
+<elem-947>
+<elem-948>
+<elem-949>
+<elem-950>
+<elem-951>
+<elem-952>
+<elem-953>
+<elem-954>
+<elem-955>
+<elem-956>
+<elem-957>
+<elem-958>
+<elem-959>
+<elem-960>
+<elem-961>
+<elem-962>
+<elem-963>
+<elem-964>
+<elem-965>
+<elem-966>
+<elem-967>
+<elem-968>
+<elem-969>
+<elem-970>
+<elem-971>
+<elem-972>
+<elem-973>
+<elem-974>
+<elem-975>
+<elem-976>
+<elem-977>
+<elem-978>
+<elem-979>
+<elem-980>
+<elem-981>
+<elem-982>
+<elem-983>
+<elem-984>
+<elem-985>
+<elem-986>
+<elem-987>
+<elem-988>
+<elem-989>
+<elem-990>
+<elem-991>
+<elem-992>
+<elem-993>
+<elem-994>
+<elem-995>
+<elem-996>
+<elem-997>
+<elem-998>
+<elem-999>
+<elem-1000>
+</elem-1000>
+</elem-999>
+</elem-998>
+</elem-997>
+</elem-996>
+</elem-995>
+</elem-994>
+</elem-993>
+</elem-992>
+</elem-991>
+</elem-990>
+</elem-989>
+</elem-988>
+</elem-987>
+</elem-986>
+</elem-985>
+</elem-984>
+</elem-983>
+</elem-982>
+</elem-981>
+</elem-980>
+</elem-979>
+</elem-978>
+</elem-977>
+</elem-976>
+</elem-975>
+</elem-974>
+</elem-973>
+</elem-972>
+</elem-971>
+</elem-970>
+</elem-969>
+</elem-968>
+</elem-967>
+</elem-966>
+</elem-965>
+</elem-964>
+</elem-963>
+</elem-962>
+</elem-961>
+</elem-960>
+</elem-959>
+</elem-958>
+</elem-957>
+</elem-956>
+</elem-955>
+</elem-954>
+</elem-953>
+</elem-952>
+</elem-951>
+</elem-950>
+</elem-949>
+</elem-948>
+</elem-947>
+</elem-946>
+</elem-945>
+</elem-944>
+</elem-943>
+</elem-942>
+</elem-941>
+</elem-940>
+</elem-939>
+</elem-938>
+</elem-937>
+</elem-936>
+</elem-935>
+</elem-934>
+</elem-933>
+</elem-932>
+</elem-931>
+</elem-930>
+</elem-929>
+</elem-928>
+</elem-927>
+</elem-926>
+</elem-925>
+</elem-924>
+</elem-923>
+</elem-922>
+</elem-921>
+</elem-920>
+</elem-919>
+</elem-918>
+</elem-917>
+</elem-916>
+</elem-915>
+</elem-914>
+</elem-913>
+</elem-912>
+</elem-911>
+</elem-910>
+</elem-909>
+</elem-908>
+</elem-907>
+</elem-906>
+</elem-905>
+</elem-904>
+</elem-903>
+</elem-902>
+</elem-901>
+</elem-900>
+</elem-899>
+</elem-898>
+</elem-897>
+</elem-896>
+</elem-895>
+</elem-894>
+</elem-893>
+</elem-892>
+</elem-891>
+</elem-890>
+</elem-889>
+</elem-888>
+</elem-887>
+</elem-886>
+</elem-885>
+</elem-884>
+</elem-883>
+</elem-882>
+</elem-881>
+</elem-880>
+</elem-879>
+</elem-878>
+</elem-877>
+</elem-876>
+</elem-875>
+</elem-874>
+</elem-873>
+</elem-872>
+</elem-871>
+</elem-870>
+</elem-869>
+</elem-868>
+</elem-867>
+</elem-866>
+</elem-865>
+</elem-864>
+</elem-863>
+</elem-862>
+</elem-861>
+</elem-860>
+</elem-859>
+</elem-858>
+</elem-857>
+</elem-856>
+</elem-855>
+</elem-854>
+</elem-853>
+</elem-852>
+</elem-851>
+</elem-850>
+</elem-849>
+</elem-848>
+</elem-847>
+</elem-846>
+</elem-845>
+</elem-844>
+</elem-843>
+</elem-842>
+</elem-841>
+</elem-840>
+</elem-839>
+</elem-838>
+</elem-837>
+</elem-836>
+</elem-835>
+</elem-834>
+</elem-833>
+</elem-832>
+</elem-831>
+</elem-830>
+</elem-829>
+</elem-828>
+</elem-827>
+</elem-826>
+</elem-825>
+</elem-824>
+</elem-823>
+</elem-822>
+</elem-821>
+</elem-820>
+</elem-819>
+</elem-818>
+</elem-817>
+</elem-816>
+</elem-815>
+</elem-814>
+</elem-813>
+</elem-812>
+</elem-811>
+</elem-810>
+</elem-809>
+</elem-808>
+</elem-807>
+</elem-806>
+</elem-805>
+</elem-804>
+</elem-803>
+</elem-802>
+</elem-801>
+</elem-800>
+</elem-799>
+</elem-798>
+</elem-797>
+</elem-796>
+</elem-795>
+</elem-794>
+</elem-793>
+</elem-792>
+</elem-791>
+</elem-790>
+</elem-789>
+</elem-788>
+</elem-787>
+</elem-786>
+</elem-785>
+</elem-784>
+</elem-783>
+</elem-782>
+</elem-781>
+</elem-780>
+</elem-779>
+</elem-778>
+</elem-777>
+</elem-776>
+</elem-775>
+</elem-774>
+</elem-773>
+</elem-772>
+</elem-771>
+</elem-770>
+</elem-769>
+</elem-768>
+</elem-767>
+</elem-766>
+</elem-765>
+</elem-764>
+</elem-763>
+</elem-762>
+</elem-761>
+</elem-760>
+</elem-759>
+</elem-758>
+</elem-757>
+</elem-756>
+</elem-755>
+</elem-754>
+</elem-753>
+</elem-752>
+</elem-751>
+</elem-750>
+</elem-749>
+</elem-748>
+</elem-747>
+</elem-746>
+</elem-745>
+</elem-744>
+</elem-743>
+</elem-742>
+</elem-741>
+</elem-740>
+</elem-739>
+</elem-738>
+</elem-737>
+</elem-736>
+</elem-735>
+</elem-734>
+</elem-733>
+</elem-732>
+</elem-731>
+</elem-730>
+</elem-729>
+</elem-728>
+</elem-727>
+</elem-726>
+</elem-725>
+</elem-724>
+</elem-723>
+</elem-722>
+</elem-721>
+</elem-720>
+</elem-719>
+</elem-718>
+</elem-717>
+</elem-716>
+</elem-715>
+</elem-714>
+</elem-713>
+</elem-712>
+</elem-711>
+</elem-710>
+</elem-709>
+</elem-708>
+</elem-707>
+</elem-706>
+</elem-705>
+</elem-704>
+</elem-703>
+</elem-702>
+</elem-701>
+</elem-700>
+</elem-699>
+</elem-698>
+</elem-697>
+</elem-696>
+</elem-695>
+</elem-694>
+</elem-693>
+</elem-692>
+</elem-691>
+</elem-690>
+</elem-689>
+</elem-688>
+</elem-687>
+</elem-686>
+</elem-685>
+</elem-684>
+</elem-683>
+</elem-682>
+</elem-681>
+</elem-680>
+</elem-679>
+</elem-678>
+</elem-677>
+</elem-676>
+</elem-675>
+</elem-674>
+</elem-673>
+</elem-672>
+</elem-671>
+</elem-670>
+</elem-669>
+</elem-668>
+</elem-667>
+</elem-666>
+</elem-665>
+</elem-664>
+</elem-663>
+</elem-662>
+</elem-661>
+</elem-660>
+</elem-659>
+</elem-658>
+</elem-657>
+</elem-656>
+</elem-655>
+</elem-654>
+</elem-653>
+</elem-652>
+</elem-651>
+</elem-650>
+</elem-649>
+</elem-648>
+</elem-647>
+</elem-646>
+</elem-645>
+</elem-644>
+</elem-643>
+</elem-642>
+</elem-641>
+</elem-640>
+</elem-639>
+</elem-638>
+</elem-637>
+</elem-636>
+</elem-635>
+</elem-634>
+</elem-633>
+</elem-632>
+</elem-631>
+</elem-630>
+</elem-629>
+</elem-628>
+</elem-627>
+</elem-626>
+</elem-625>
+</elem-624>
+</elem-623>
+</elem-622>
+</elem-621>
+</elem-620>
+</elem-619>
+</elem-618>
+</elem-617>
+</elem-616>
+</elem-615>
+</elem-614>
+</elem-613>
+</elem-612>
+</elem-611>
+</elem-610>
+</elem-609>
+</elem-608>
+</elem-607>
+</elem-606>
+</elem-605>
+</elem-604>
+</elem-603>
+</elem-602>
+</elem-601>
+</elem-600>
+</elem-599>
+</elem-598>
+</elem-597>
+</elem-596>
+</elem-595>
+</elem-594>
+</elem-593>
+</elem-592>
+</elem-591>
+</elem-590>
+</elem-589>
+</elem-588>
+</elem-587>
+</elem-586>
+</elem-585>
+</elem-584>
+</elem-583>
+</elem-582>
+</elem-581>
+</elem-580>
+</elem-579>
+</elem-578>
+</elem-577>
+</elem-576>
+</elem-575>
+</elem-574>
+</elem-573>
+</elem-572>
+</elem-571>
+</elem-570>
+</elem-569>
+</elem-568>
+</elem-567>
+</elem-566>
+</elem-565>
+</elem-564>
+</elem-563>
+</elem-562>
+</elem-561>
+</elem-560>
+</elem-559>
+</elem-558>
+</elem-557>
+</elem-556>
+</elem-555>
+</elem-554>
+</elem-553>
+</elem-552>
+</elem-551>
+</elem-550>
+</elem-549>
+</elem-548>
+</elem-547>
+</elem-546>
+</elem-545>
+</elem-544>
+</elem-543>
+</elem-542>
+</elem-541>
+</elem-540>
+</elem-539>
+</elem-538>
+</elem-537>
+</elem-536>
+</elem-535>
+</elem-534>
+</elem-533>
+</elem-532>
+</elem-531>
+</elem-530>
+</elem-529>
+</elem-528>
+</elem-527>
+</elem-526>
+</elem-525>
+</elem-524>
+</elem-523>
+</elem-522>
+</elem-521>
+</elem-520>
+</elem-519>
+</elem-518>
+</elem-517>
+</elem-516>
+</elem-515>
+</elem-514>
+</elem-513>
+</elem-512>
+</elem-511>
+</elem-510>
+</elem-509>
+</elem-508>
+</elem-507>
+</elem-506>
+</elem-505>
+</elem-504>
+</elem-503>
+</elem-502>
+</elem-501>
+</elem-500>
+</elem-499>
+</elem-498>
+</elem-497>
+</elem-496>
+</elem-495>
+</elem-494>
+</elem-493>
+</elem-492>
+</elem-491>
+</elem-490>
+</elem-489>
+</elem-488>
+</elem-487>
+</elem-486>
+</elem-485>
+</elem-484>
+</elem-483>
+</elem-482>
+</elem-481>
+</elem-480>
+</elem-479>
+</elem-478>
+</elem-477>
+</elem-476>
+</elem-475>
+</elem-474>
+</elem-473>
+</elem-472>
+</elem-471>
+</elem-470>
+</elem-469>
+</elem-468>
+</elem-467>
+</elem-466>
+</elem-465>
+</elem-464>
+</elem-463>
+</elem-462>
+</elem-461>
+</elem-460>
+</elem-459>
+</elem-458>
+</elem-457>
+</elem-456>
+</elem-455>
+</elem-454>
+</elem-453>
+</elem-452>
+</elem-451>
+</elem-450>
+</elem-449>
+</elem-448>
+</elem-447>
+</elem-446>
+</elem-445>
+</elem-444>
+</elem-443>
+</elem-442>
+</elem-441>
+</elem-440>
+</elem-439>
+</elem-438>
+</elem-437>
+</elem-436>
+</elem-435>
+</elem-434>
+</elem-433>
+</elem-432>
+</elem-431>
+</elem-430>
+</elem-429>
+</elem-428>
+</elem-427>
+</elem-426>
+</elem-425>
+</elem-424>
+</elem-423>
+</elem-422>
+</elem-421>
+</elem-420>
+</elem-419>
+</elem-418>
+</elem-417>
+</elem-416>
+</elem-415>
+</elem-414>
+</elem-413>
+</elem-412>
+</elem-411>
+</elem-410>
+</elem-409>
+</elem-408>
+</elem-407>
+</elem-406>
+</elem-405>
+</elem-404>
+</elem-403>
+</elem-402>
+</elem-401>
+</elem-400>
+</elem-399>
+</elem-398>
+</elem-397>
+</elem-396>
+</elem-395>
+</elem-394>
+</elem-393>
+</elem-392>
+</elem-391>
+</elem-390>
+</elem-389>
+</elem-388>
+</elem-387>
+</elem-386>
+</elem-385>
+</elem-384>
+</elem-383>
+</elem-382>
+</elem-381>
+</elem-380>
+</elem-379>
+</elem-378>
+</elem-377>
+</elem-376>
+</elem-375>
+</elem-374>
+</elem-373>
+</elem-372>
+</elem-371>
+</elem-370>
+</elem-369>
+</elem-368>
+</elem-367>
+</elem-366>
+</elem-365>
+</elem-364>
+</elem-363>
+</elem-362>
+</elem-361>
+</elem-360>
+</elem-359>
+</elem-358>
+</elem-357>
+</elem-356>
+</elem-355>
+</elem-354>
+</elem-353>
+</elem-352>
+</elem-351>
+</elem-350>
+</elem-349>
+</elem-348>
+</elem-347>
+</elem-346>
+</elem-345>
+</elem-344>
+</elem-343>
+</elem-342>
+</elem-341>
+</elem-340>
+</elem-339>
+</elem-338>
+</elem-337>
+</elem-336>
+</elem-335>
+</elem-334>
+</elem-333>
+</elem-332>
+</elem-331>
+</elem-330>
+</elem-329>
+</elem-328>
+</elem-327>
+</elem-326>
+</elem-325>
+</elem-324>
+</elem-323>
+</elem-322>
+</elem-321>
+</elem-320>
+</elem-319>
+</elem-318>
+</elem-317>
+</elem-316>
+</elem-315>
+</elem-314>
+</elem-313>
+</elem-312>
+</elem-311>
+</elem-310>
+</elem-309>
+</elem-308>
+</elem-307>
+</elem-306>
+</elem-305>
+</elem-304>
+</elem-303>
+</elem-302>
+</elem-301>
+</elem-300>
+</elem-299>
+</elem-298>
+</elem-297>
+</elem-296>
+</elem-295>
+</elem-294>
+</elem-293>
+</elem-292>
+</elem-291>
+</elem-290>
+</elem-289>
+</elem-288>
+</elem-287>
+</elem-286>
+</elem-285>
+</elem-284>
+</elem-283>
+</elem-282>
+</elem-281>
+</elem-280>
+</elem-279>
+</elem-278>
+</elem-277>
+</elem-276>
+</elem-275>
+</elem-274>
+</elem-273>
+</elem-272>
+</elem-271>
+</elem-270>
+</elem-269>
+</elem-268>
+</elem-267>
+</elem-266>
+</elem-265>
+</elem-264>
+</elem-263>
+</elem-262>
+</elem-261>
+</elem-260>
+</elem-259>
+</elem-258>
+</elem-257>
+</elem-256>
+</elem-255>
+</elem-254>
+</elem-253>
+</elem-252>
+</elem-251>
+</elem-250>
+</elem-249>
+</elem-248>
+</elem-247>
+</elem-246>
+</elem-245>
+</elem-244>
+</elem-243>
+</elem-242>
+</elem-241>
+</elem-240>
+</elem-239>
+</elem-238>
+</elem-237>
+</elem-236>
+</elem-235>
+</elem-234>
+</elem-233>
+</elem-232>
+</elem-231>
+</elem-230>
+</elem-229>
+</elem-228>
+</elem-227>
+</elem-226>
+</elem-225>
+</elem-224>
+</elem-223>
+</elem-222>
+</elem-221>
+</elem-220>
+</elem-219>
+</elem-218>
+</elem-217>
+</elem-216>
+</elem-215>
+</elem-214>
+</elem-213>
+</elem-212>
+</elem-211>
+</elem-210>
+</elem-209>
+</elem-208>
+</elem-207>
+</elem-206>
+</elem-205>
+</elem-204>
+</elem-203>
+</elem-202>
+</elem-201>
+</elem-200>
+</elem-199>
+</elem-198>
+</elem-197>
+</elem-196>
+</elem-195>
+</elem-194>
+</elem-193>
+</elem-192>
+</elem-191>
+</elem-190>
+</elem-189>
+</elem-188>
+</elem-187>
+</elem-186>
+</elem-185>
+</elem-184>
+</elem-183>
+</elem-182>
+</elem-181>
+</elem-180>
+</elem-179>
+</elem-178>
+</elem-177>
+</elem-176>
+</elem-175>
+</elem-174>
+</elem-173>
+</elem-172>
+</elem-171>
+</elem-170>
+</elem-169>
+</elem-168>
+</elem-167>
+</elem-166>
+</elem-165>
+</elem-164>
+</elem-163>
+</elem-162>
+</elem-161>
+</elem-160>
+</elem-159>
+</elem-158>
+</elem-157>
+</elem-156>
+</elem-155>
+</elem-154>
+</elem-153>
+</elem-152>
+</elem-151>
+</elem-150>
+</elem-149>
+</elem-148>
+</elem-147>
+</elem-146>
+</elem-145>
+</elem-144>
+</elem-143>
+</elem-142>
+</elem-141>
+</elem-140>
+</elem-139>
+</elem-138>
+</elem-137>
+</elem-136>
+</elem-135>
+</elem-134>
+</elem-133>
+</elem-132>
+</elem-131>
+</elem-130>
+</elem-129>
+</elem-128>
+</elem-127>
+</elem-126>
+</elem-125>
+</elem-124>
+</elem-123>
+</elem-122>
+</elem-121>
+</elem-120>
+</elem-119>
+</elem-118>
+</elem-117>
+</elem-116>
+</elem-115>
+</elem-114>
+</elem-113>
+</elem-112>
+</elem-111>
+</elem-110>
+</elem-109>
+</elem-108>
+</elem-107>
+</elem-106>
+</elem-105>
+</elem-104>
+</elem-103>
+</elem-102>
+</elem-101>
+</elem-100>
+</elem-99>
+</elem-98>
+</elem-97>
+</elem-96>
+</elem-95>
+</elem-94>
+</elem-93>
+</elem-92>
+</elem-91>
+</elem-90>
+</elem-89>
+</elem-88>
+</elem-87>
+</elem-86>
+</elem-85>
+</elem-84>
+</elem-83>
+</elem-82>
+</elem-81>
+</elem-80>
+</elem-79>
+</elem-78>
+</elem-77>
+</elem-76>
+</elem-75>
+</elem-74>
+</elem-73>
+</elem-72>
+</elem-71>
+</elem-70>
+</elem-69>
+</elem-68>
+</elem-67>
+</elem-66>
+</elem-65>
+</elem-64>
+</elem-63>
+</elem-62>
+</elem-61>
+</elem-60>
+</elem-59>
+</elem-58>
+</elem-57>
+</elem-56>
+</elem-55>
+</elem-54>
+</elem-53>
+</elem-52>
+</elem-51>
+</elem-50>
+</elem-49>
+</elem-48>
+</elem-47>
+</elem-46>
+</elem-45>
+</elem-44>
+</elem-43>
+</elem-42>
+</elem-41>
+</elem-40>
+</elem-39>
+</elem-38>
+</elem-37>
+</elem-36>
+</elem-35>
+</elem-34>
+</elem-33>
+</elem-32>
+</elem-31>
+</elem-30>
+</elem-29>
+</elem-28>
+</elem-27>
+</elem-26>
+</elem-25>
+</elem-24>
+</elem-23>
+</elem-22>
+</elem-21>
+</elem-20>
+</elem-19>
+</elem-18>
+</elem-17>
+</elem-16>
+</elem-15>
+</elem-14>
+</elem-13>
+</elem-12>
+</elem-11>
+</elem-10>
+</elem-9>
+</elem-8>
+</elem-7>
+</elem-6>
+</elem-5>
+</elem-4>
+</elem-3>
+</elem-2>
+</elem-1>
+</outer> \ No newline at end of file
diff --git a/test/System.Web.Razor.Test/Text/BufferingTextReaderTest.cs b/test/System.Web.Razor.Test/Text/BufferingTextReaderTest.cs
new file mode 100644
index 00000000..b4b8c019
--- /dev/null
+++ b/test/System.Web.Razor.Test/Text/BufferingTextReaderTest.cs
@@ -0,0 +1,265 @@
+using System.IO;
+using System.Web.Razor.Text;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Razor.Test.Text
+{
+ public class BufferingTextReaderTest : LookaheadTextReaderTestBase
+ {
+ private const string TestString = "abcdefg";
+
+ private class DisposeTestMockTextReader : TextReader
+ {
+ public bool Disposed { get; set; }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ Disposed = true;
+ }
+ }
+
+ protected override LookaheadTextReader CreateReader(string testString)
+ {
+ return new BufferingTextReader(new StringReader(testString));
+ }
+
+ [Fact]
+ public void ConstructorRequiresNonNullSourceReader()
+ {
+ Assert.ThrowsArgumentNull(() => new BufferingTextReader(null), "source");
+ }
+
+ [Fact]
+ public void PeekReturnsCurrentCharacterWithoutAdvancingPosition()
+ {
+ RunPeekTest("abc", peekAt: 2);
+ }
+
+ [Fact]
+ public void PeekReturnsNegativeOneAtEndOfSourceReader()
+ {
+ RunPeekTest("abc", peekAt: 3);
+ }
+
+ [Fact]
+ public void ReadReturnsCurrentCharacterAndAdvancesToNextCharacter()
+ {
+ RunReadTest("abc", readAt: 2);
+ }
+
+ [Fact]
+ public void EndingLookaheadReturnsReaderToPreviousLocation()
+ {
+ RunLookaheadTest("abcdefg", "abcb",
+ Read,
+ Lookahead(
+ Read,
+ Read),
+ Read);
+ }
+
+ [Fact]
+ public void MultipleLookaheadsCanBePerformed()
+ {
+ RunLookaheadTest("abcdefg", "abcbcdc",
+ Read,
+ Lookahead(
+ Read,
+ Read),
+ Read,
+ Lookahead(
+ Read,
+ Read),
+ Read);
+ }
+
+ [Fact]
+ public void LookaheadsCanBeNested()
+ {
+ RunLookaheadTest("abcdefg", "abcdefebc",
+ Read, // Appended: "a" Reader: "bcdefg"
+ Lookahead( // Reader: "bcdefg"
+ Read, // Appended: "b" Reader: "cdefg";
+ Read, // Appended: "c" Reader: "defg";
+ Read, // Appended: "d" Reader: "efg";
+ Lookahead( // Reader: "efg"
+ Read, // Appended: "e" Reader: "fg";
+ Read // Appended: "f" Reader: "g";
+ ), // Reader: "efg"
+ Read // Appended: "e" Reader: "fg";
+ ), // Reader: "bcdefg"
+ Read, // Appended: "b" Reader: "cdefg";
+ Read); // Appended: "c" Reader: "defg";
+ }
+
+ [Fact]
+ public void SourceLocationIsZeroWhenInitialized()
+ {
+ RunSourceLocationTest("abcdefg", SourceLocation.Zero, checkAt: 0);
+ }
+
+ [Fact]
+ public void CharacterAndAbsoluteIndicesIncreaseAsCharactersAreRead()
+ {
+ RunSourceLocationTest("abcdefg", new SourceLocation(4, 0, 4), checkAt: 4);
+ }
+
+ [Fact]
+ public void CharacterAndAbsoluteIndicesIncreaseAsSlashRInTwoCharacterNewlineIsRead()
+ {
+ RunSourceLocationTest("f\r\nb", new SourceLocation(2, 0, 2), checkAt: 2);
+ }
+
+ [Fact]
+ public void CharacterIndexResetsToZeroAndLineIndexIncrementsWhenSlashNInTwoCharacterNewlineIsRead()
+ {
+ RunSourceLocationTest("f\r\nb", new SourceLocation(3, 1, 0), checkAt: 3);
+ }
+
+ [Fact]
+ public void CharacterIndexResetsToZeroAndLineIndexIncrementsWhenSlashRInSingleCharacterNewlineIsRead()
+ {
+ RunSourceLocationTest("f\rb", new SourceLocation(2, 1, 0), checkAt: 2);
+ }
+
+ [Fact]
+ public void CharacterIndexResetsToZeroAndLineIndexIncrementsWhenSlashNInSingleCharacterNewlineIsRead()
+ {
+ RunSourceLocationTest("f\nb", new SourceLocation(2, 1, 0), checkAt: 2);
+ }
+
+ [Fact]
+ public void EndingLookaheadResetsRawCharacterAndLineIndexToValuesWhenLookaheadBegan()
+ {
+ RunEndLookaheadUpdatesSourceLocationTest();
+ }
+
+ [Fact]
+ public void OnceBufferingBeginsReadsCanContinuePastEndOfBuffer()
+ {
+ RunLookaheadTest("abcdefg", "abcbcdefg",
+ Read,
+ Lookahead(Read(2)),
+ Read(2),
+ ReadToEnd);
+ }
+
+ [Fact]
+ public void DisposeDisposesSourceReader()
+ {
+ RunDisposeTest(r => r.Dispose());
+ }
+
+ [Fact]
+ public void CloseDisposesSourceReader()
+ {
+ RunDisposeTest(r => r.Close());
+ }
+
+ [Fact]
+ public void ReadWithBufferSupportsLookahead()
+ {
+ RunBufferReadTest((reader, buffer, index, count) => reader.Read(buffer, index, count));
+ }
+
+ [Fact]
+ public void ReadBlockSupportsLookahead()
+ {
+ RunBufferReadTest((reader, buffer, index, count) => reader.ReadBlock(buffer, index, count));
+ }
+
+ [Fact]
+ public void ReadLineSupportsLookahead()
+ {
+ RunReadUntilTest(r => r.ReadLine(), expectedRaw: 8, expectedChar: 0, expectedLine: 2);
+ }
+
+ [Fact]
+ public void ReadToEndSupportsLookahead()
+ {
+ RunReadUntilTest(r => r.ReadToEnd(), expectedRaw: 11, expectedChar: 3, expectedLine: 2);
+ }
+
+ [Fact]
+ public void ReadLineMaintainsCorrectCharacterPosition()
+ {
+ RunSourceLocationTest("abc\r\ndef", new SourceLocation(5, 1, 0), r => r.ReadLine());
+ }
+
+ [Fact]
+ public void ReadToEndWorksAsInNormalTextReader()
+ {
+ RunReadToEndTest();
+ }
+
+ [Fact]
+ public void CancelBacktrackStopsNextEndLookaheadFromBacktracking()
+ {
+ RunLookaheadTest("abcdefg", "abcdefg",
+ Lookahead(
+ Read(2),
+ CancelBacktrack
+ ),
+ ReadToEnd);
+ }
+
+ [Fact]
+ public void CancelBacktrackThrowsInvalidOperationExceptionIfCalledOutsideOfLookahead()
+ {
+ RunCancelBacktrackOutsideLookaheadTest();
+ }
+
+ [Fact]
+ public void CancelBacktrackOnlyCancelsBacktrackingForInnermostNestedLookahead()
+ {
+ RunLookaheadTest("abcdefg", "abcdabcdefg",
+ Lookahead(
+ Read(2),
+ Lookahead(
+ Read,
+ CancelBacktrack
+ ),
+ Read
+ ),
+ ReadToEnd);
+ }
+
+ [Fact]
+ public void BacktrackBufferIsClearedWhenEndReachedAndNoCurrentLookaheads()
+ {
+ // Arrange
+ StringReader source = new StringReader(TestString);
+ BufferingTextReader reader = new BufferingTextReader(source);
+
+ reader.Read(); // Reader: "bcdefg"
+ using (reader.BeginLookahead())
+ {
+ reader.Read(); // Reader: "cdefg"
+ } // Reader: "bcdefg"
+ reader.Read(); // Reader: "cdefg"
+ Assert.NotNull(reader.Buffer); // Verify our assumption that the buffer still exists
+
+ // Act
+ reader.Read();
+
+ // Assert
+ Assert.False(reader.Buffering, "The buffer was not reset when the end was reached");
+ Assert.Equal(0, reader.Buffer.Length);
+ }
+
+ private static void RunDisposeTest(Action<LookaheadTextReader> triggerAction)
+ {
+ // Arrange
+ DisposeTestMockTextReader source = new DisposeTestMockTextReader();
+ LookaheadTextReader reader = new BufferingTextReader(source);
+
+ // Act
+ triggerAction(reader);
+
+ // Assert
+ Assert.True(source.Disposed);
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Text/LineTrackingStringBufferTest.cs b/test/System.Web.Razor.Test/Text/LineTrackingStringBufferTest.cs
new file mode 100644
index 00000000..52cffb7f
--- /dev/null
+++ b/test/System.Web.Razor.Test/Text/LineTrackingStringBufferTest.cs
@@ -0,0 +1,25 @@
+using System.Web.Razor.Text;
+using Xunit;
+
+namespace System.Web.Razor.Test.Text
+{
+ public class LineTrackingStringBufferTest
+ {
+ [Fact]
+ public void CtorInitializesProperties()
+ {
+ LineTrackingStringBuffer buffer = new LineTrackingStringBuffer();
+ Assert.Equal(0, buffer.Length);
+ }
+
+ [Fact]
+ public void CharAtCorrectlyReturnsLocation()
+ {
+ LineTrackingStringBuffer buffer = new LineTrackingStringBuffer();
+ buffer.Append("foo\rbar\nbaz\r\nbiz");
+ LineTrackingStringBuffer.CharacterReference chr = buffer.CharAt(14);
+ Assert.Equal('i', chr.Character);
+ Assert.Equal(new SourceLocation(14, 3, 1), chr.Location);
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Text/LookaheadTextReaderTestBase.cs b/test/System.Web.Razor.Test/Text/LookaheadTextReaderTestBase.cs
new file mode 100644
index 00000000..fbcc8e29
--- /dev/null
+++ b/test/System.Web.Razor.Test/Text/LookaheadTextReaderTestBase.cs
@@ -0,0 +1,252 @@
+using System.Text;
+using System.Web.Razor.Resources;
+using System.Web.Razor.Text;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Razor.Test.Text
+{
+ public abstract class LookaheadTextReaderTestBase
+ {
+ protected abstract LookaheadTextReader CreateReader(string testString);
+
+ protected void RunPeekTest(string input, int peekAt = 0)
+ {
+ RunPeekOrReadTest(input, peekAt, false);
+ }
+
+ protected void RunReadTest(string input, int readAt = 0)
+ {
+ RunPeekOrReadTest(input, readAt, true);
+ }
+
+ protected void RunSourceLocationTest(string input, SourceLocation expected, int checkAt = 0)
+ {
+ RunSourceLocationTest(input, expected, r => AdvanceReader(checkAt, r));
+ }
+
+ protected void RunSourceLocationTest(string input, SourceLocation expected, Action<LookaheadTextReader> readerAction)
+ {
+ // Arrange
+ LookaheadTextReader reader = CreateReader(input);
+ readerAction(reader);
+
+ // Act
+ SourceLocation actual = reader.CurrentLocation;
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+
+ protected void RunEndLookaheadUpdatesSourceLocationTest()
+ {
+ SourceLocation? expectedLocation = null;
+ SourceLocation? actualLocation = null;
+
+ RunLookaheadTest("abc\r\ndef\r\nghi", null,
+ Read(6),
+ CaptureSourceLocation(s => expectedLocation = s),
+ Lookahead(Read(6)),
+ CaptureSourceLocation(s => actualLocation = s));
+ // Assert
+ Assert.Equal(expectedLocation.Value.AbsoluteIndex, actualLocation.Value.AbsoluteIndex);
+ Assert.Equal(expectedLocation.Value.CharacterIndex, actualLocation.Value.CharacterIndex);
+ Assert.Equal(expectedLocation.Value.LineIndex, actualLocation.Value.LineIndex);
+ }
+
+ protected void RunReadToEndTest()
+ {
+ // Arrange
+ LookaheadTextReader reader = CreateReader("abcdefg");
+
+ // Act
+ string str = reader.ReadToEnd();
+
+ // Assert
+ Assert.Equal("abcdefg", str);
+ }
+
+ protected void RunCancelBacktrackOutsideLookaheadTest()
+ {
+ // Arrange
+ LookaheadTextReader reader = CreateReader("abcdefg");
+
+ // Act and Assert
+ Assert.Throws<InvalidOperationException>(() => reader.CancelBacktrack(), RazorResources.CancelBacktrack_Must_Be_Called_Within_Lookahead);
+ }
+
+ protected Action<StringBuilder, LookaheadTextReader> CaptureSourceLocation(Action<SourceLocation> capture)
+ {
+ return (_, reader) => { capture(reader.CurrentLocation); };
+ }
+
+ protected Action<StringBuilder, LookaheadTextReader> Read(int count)
+ {
+ return (builder, reader) =>
+ {
+ for (int i = 0; i < count; i++)
+ {
+ Read(builder, reader);
+ }
+ };
+ }
+
+ protected void Read(StringBuilder builder, LookaheadTextReader reader)
+ {
+ builder.Append((char)reader.Read());
+ }
+
+ protected void ReadToEnd(StringBuilder builder, LookaheadTextReader reader)
+ {
+ builder.Append(reader.ReadToEnd());
+ }
+
+ protected void CancelBacktrack(StringBuilder builder, LookaheadTextReader reader)
+ {
+ reader.CancelBacktrack();
+ }
+
+ protected Action<StringBuilder, LookaheadTextReader> Lookahead(params Action<StringBuilder, LookaheadTextReader>[] readerCommands)
+ {
+ return (builder, reader) =>
+ {
+ using (reader.BeginLookahead())
+ {
+ RunAll(readerCommands, builder, reader);
+ }
+ };
+ }
+
+ protected void RunLookaheadTest(string input, string expected, params Action<StringBuilder, LookaheadTextReader>[] readerCommands)
+ {
+ // Arrange
+ StringBuilder builder = new StringBuilder();
+ using (LookaheadTextReader reader = CreateReader(input))
+ {
+ RunAll(readerCommands, builder, reader);
+ }
+
+ if (expected != null)
+ {
+ Assert.Equal(expected, builder.ToString());
+ }
+ }
+
+ protected void RunReadUntilTest(Func<LookaheadTextReader, string> readMethod, int expectedRaw, int expectedChar, int expectedLine)
+ {
+ // Arrange
+ LookaheadTextReader reader = CreateReader("a\r\nbcd\r\nefg");
+
+ reader.Read(); // Reader: "\r\nbcd\r\nefg"
+ reader.Read(); // Reader: "\nbcd\r\nefg"
+ reader.Read(); // Reader: "bcd\r\nefg"
+
+ // Act
+ string read = null;
+ SourceLocation actualLocation;
+ using (reader.BeginLookahead())
+ {
+ read = readMethod(reader);
+ actualLocation = reader.CurrentLocation;
+ }
+
+ // Assert
+ Assert.Equal(3, reader.CurrentLocation.AbsoluteIndex);
+ Assert.Equal(0, reader.CurrentLocation.CharacterIndex);
+ Assert.Equal(1, reader.CurrentLocation.LineIndex);
+ Assert.Equal(expectedRaw, actualLocation.AbsoluteIndex);
+ Assert.Equal(expectedChar, actualLocation.CharacterIndex);
+ Assert.Equal(expectedLine, actualLocation.LineIndex);
+ Assert.Equal('b', reader.Peek());
+ Assert.Equal(read, readMethod(reader));
+ }
+
+ protected void RunBufferReadTest(Func<LookaheadTextReader, char[], int, int, int> readMethod)
+ {
+ // Arrange
+ LookaheadTextReader reader = CreateReader("abcdefg");
+
+ reader.Read(); // Reader: "bcdefg"
+
+ // Act
+ char[] buffer = new char[4];
+ int read = -1;
+ SourceLocation actualLocation;
+ using (reader.BeginLookahead())
+ {
+ read = readMethod(reader, buffer, 0, 4);
+ actualLocation = reader.CurrentLocation;
+ }
+
+ // Assert
+ Assert.Equal("bcde", new String(buffer));
+ Assert.Equal(4, read);
+ Assert.Equal(5, actualLocation.AbsoluteIndex);
+ Assert.Equal(5, actualLocation.CharacterIndex);
+ Assert.Equal(0, actualLocation.LineIndex);
+ Assert.Equal(1, reader.CurrentLocation.CharacterIndex);
+ Assert.Equal(0, reader.CurrentLocation.LineIndex);
+ Assert.Equal('b', reader.Peek());
+ }
+
+ private static void RunAll(Action<StringBuilder, LookaheadTextReader>[] readerCommands, StringBuilder builder, LookaheadTextReader reader)
+ {
+ foreach (Action<StringBuilder, LookaheadTextReader> readerCommand in readerCommands)
+ {
+ readerCommand(builder, reader);
+ }
+ }
+
+ private void RunPeekOrReadTest(string input, int offset, bool isRead)
+ {
+ using (LookaheadTextReader reader = CreateReader(input))
+ {
+ AdvanceReader(offset, reader);
+
+ // Act
+ int? actual = null;
+ if (isRead)
+ {
+ actual = reader.Read();
+ }
+ else
+ {
+ actual = reader.Peek();
+ }
+
+ Assert.NotNull(actual);
+
+ // Asserts
+ AssertReaderValueCorrect(actual.Value, input, offset, "Peek");
+
+ if (isRead)
+ {
+ AssertReaderValueCorrect(reader.Peek(), input, offset + 1, "Read");
+ }
+ else
+ {
+ Assert.Equal(actual, reader.Peek());
+ }
+ }
+ }
+
+ private static void AdvanceReader(int offset, LookaheadTextReader reader)
+ {
+ for (int i = 0; i < offset; i++)
+ {
+ reader.Read();
+ }
+ }
+
+ private void AssertReaderValueCorrect(int actual, string input, int expectedOffset, string methodName)
+ {
+ if (expectedOffset < input.Length)
+ {
+ Assert.Equal(input[expectedOffset], actual);
+ }
+ else
+ {
+ Assert.Equal(-1, actual);
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Text/SourceLocationTest.cs b/test/System.Web.Razor.Test/Text/SourceLocationTest.cs
new file mode 100644
index 00000000..54e907b8
--- /dev/null
+++ b/test/System.Web.Razor.Test/Text/SourceLocationTest.cs
@@ -0,0 +1,20 @@
+using System.Web.Razor.Text;
+using Xunit;
+
+namespace System.Web.Razor.Test.Text
+{
+ public class SourceLocationTest
+ {
+ [Fact]
+ public void ConstructorWithLineAndCharacterIndexSetsAssociatedProperties()
+ {
+ // Act
+ SourceLocation loc = new SourceLocation(0, 42, 24);
+
+ // Assert
+ Assert.Equal(0, loc.AbsoluteIndex);
+ Assert.Equal(42, loc.LineIndex);
+ Assert.Equal(24, loc.CharacterIndex);
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Text/SourceLocationTrackerTest.cs b/test/System.Web.Razor.Test/Text/SourceLocationTrackerTest.cs
new file mode 100644
index 00000000..35b4d281
--- /dev/null
+++ b/test/System.Web.Razor.Test/Text/SourceLocationTrackerTest.cs
@@ -0,0 +1,179 @@
+using System.Web.Razor.Text;
+using Xunit;
+
+namespace System.Web.Razor.Test.Text
+{
+ public class SourceLocationTrackerTest
+ {
+ private static readonly SourceLocation TestStartLocation = new SourceLocation(10, 42, 45);
+
+ [Fact]
+ public void ConstructorSetsCurrentLocationToZero()
+ {
+ Assert.Equal(SourceLocation.Zero, new SourceLocationTracker().CurrentLocation);
+ }
+
+ [Fact]
+ public void ConstructorWithSourceLocationSetsCurrentLocationToSpecifiedValue()
+ {
+ SourceLocation loc = new SourceLocation(10, 42, 4);
+ Assert.Equal(loc, new SourceLocationTracker(loc).CurrentLocation);
+ }
+
+ [Fact]
+ public void UpdateLocationAdvancesAbsoluteIndexOnNonNewlineCharacter()
+ {
+ // Arrange
+ SourceLocationTracker tracker = new SourceLocationTracker(TestStartLocation);
+
+ // Act
+ tracker.UpdateLocation('f', 'o');
+
+ // Assert
+ Assert.Equal(11, tracker.CurrentLocation.AbsoluteIndex);
+ }
+
+ [Fact]
+ public void UpdateLocationAdvancesCharacterIndexOnNonNewlineCharacter()
+ {
+ // Arrange
+ SourceLocationTracker tracker = new SourceLocationTracker(TestStartLocation);
+
+ // Act
+ tracker.UpdateLocation('f', 'o');
+
+ // Assert
+ Assert.Equal(46, tracker.CurrentLocation.CharacterIndex);
+ }
+
+ [Fact]
+ public void UpdateLocationDoesNotAdvanceLineIndexOnNonNewlineCharacter()
+ {
+ // Arrange
+ SourceLocationTracker tracker = new SourceLocationTracker(TestStartLocation);
+
+ // Act
+ tracker.UpdateLocation('f', 'o');
+
+ // Assert
+ Assert.Equal(42, tracker.CurrentLocation.LineIndex);
+ }
+
+ [Fact]
+ public void UpdateLocationAdvancesLineIndexOnSlashN()
+ {
+ // Arrange
+ SourceLocationTracker tracker = new SourceLocationTracker(TestStartLocation);
+
+ // Act
+ tracker.UpdateLocation('\n', 'o');
+
+ // Assert
+ Assert.Equal(43, tracker.CurrentLocation.LineIndex);
+ }
+
+ [Fact]
+ public void UpdateLocationAdvancesAbsoluteIndexOnSlashN()
+ {
+ // Arrange
+ SourceLocationTracker tracker = new SourceLocationTracker(TestStartLocation);
+
+ // Act
+ tracker.UpdateLocation('\n', 'o');
+
+ // Assert
+ Assert.Equal(11, tracker.CurrentLocation.AbsoluteIndex);
+ }
+
+ [Fact]
+ public void UpdateLocationResetsCharacterIndexOnSlashN()
+ {
+ // Arrange
+ SourceLocationTracker tracker = new SourceLocationTracker(TestStartLocation);
+
+ // Act
+ tracker.UpdateLocation('\n', 'o');
+
+ // Assert
+ Assert.Equal(0, tracker.CurrentLocation.CharacterIndex);
+ }
+
+ [Fact]
+ public void UpdateLocationAdvancesLineIndexOnSlashRFollowedByNonNewlineCharacter()
+ {
+ // Arrange
+ SourceLocationTracker tracker = new SourceLocationTracker(TestStartLocation);
+
+ // Act
+ tracker.UpdateLocation('\r', 'o');
+
+ // Assert
+ Assert.Equal(43, tracker.CurrentLocation.LineIndex);
+ }
+
+ [Fact]
+ public void UpdateLocationAdvancesAbsoluteIndexOnSlashRFollowedByNonNewlineCharacter()
+ {
+ // Arrange
+ SourceLocationTracker tracker = new SourceLocationTracker(TestStartLocation);
+
+ // Act
+ tracker.UpdateLocation('\r', 'o');
+
+ // Assert
+ Assert.Equal(11, tracker.CurrentLocation.AbsoluteIndex);
+ }
+
+ [Fact]
+ public void UpdateLocationResetsCharacterIndexOnSlashRFollowedByNonNewlineCharacter()
+ {
+ // Arrange
+ SourceLocationTracker tracker = new SourceLocationTracker(TestStartLocation);
+
+ // Act
+ tracker.UpdateLocation('\r', 'o');
+
+ // Assert
+ Assert.Equal(0, tracker.CurrentLocation.CharacterIndex);
+ }
+
+ [Fact]
+ public void UpdateLocationDoesNotAdvanceLineIndexOnSlashRFollowedBySlashN()
+ {
+ // Arrange
+ SourceLocationTracker tracker = new SourceLocationTracker(TestStartLocation);
+
+ // Act
+ tracker.UpdateLocation('\r', '\n');
+
+ // Assert
+ Assert.Equal(42, tracker.CurrentLocation.LineIndex);
+ }
+
+ [Fact]
+ public void UpdateLocationAdvancesAbsoluteIndexOnSlashRFollowedBySlashN()
+ {
+ // Arrange
+ SourceLocationTracker tracker = new SourceLocationTracker(TestStartLocation);
+
+ // Act
+ tracker.UpdateLocation('\r', '\n');
+
+ // Assert
+ Assert.Equal(11, tracker.CurrentLocation.AbsoluteIndex);
+ }
+
+ [Fact]
+ public void UpdateLocationAdvancesCharacterIndexOnSlashRFollowedBySlashN()
+ {
+ // Arrange
+ SourceLocationTracker tracker = new SourceLocationTracker(TestStartLocation);
+
+ // Act
+ tracker.UpdateLocation('\r', '\n');
+
+ // Assert
+ Assert.Equal(46, tracker.CurrentLocation.CharacterIndex);
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Text/TextBufferReaderTest.cs b/test/System.Web.Razor.Test/Text/TextBufferReaderTest.cs
new file mode 100644
index 00000000..bb244411
--- /dev/null
+++ b/test/System.Web.Razor.Test/Text/TextBufferReaderTest.cs
@@ -0,0 +1,229 @@
+using System.Web.Razor.Text;
+using System.Web.WebPages.TestUtils;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Razor.Test.Text
+{
+ public class TextBufferReaderTest : LookaheadTextReaderTestBase
+ {
+ protected override LookaheadTextReader CreateReader(string testString)
+ {
+ return new TextBufferReader(new StringTextBuffer(testString));
+ }
+
+ [Fact]
+ public void ConstructorRequiresNonNullTextBuffer()
+ {
+ Assert.ThrowsArgumentNull(() => new TextBufferReader(null), "buffer");
+ }
+
+ [Fact]
+ public void PeekReturnsCurrentCharacterWithoutAdvancingPosition()
+ {
+ RunPeekTest("abc", peekAt: 2);
+ }
+
+ [Fact]
+ public void PeekReturnsNegativeOneAtEndOfSourceReader()
+ {
+ RunPeekTest("abc", peekAt: 3);
+ }
+
+ [Fact]
+ public void ReadReturnsCurrentCharacterAndAdvancesToNextCharacter()
+ {
+ RunReadTest("abc", readAt: 2);
+ }
+
+ [Fact]
+ public void EndingLookaheadReturnsReaderToPreviousLocation()
+ {
+ RunLookaheadTest("abcdefg", "abcb",
+ Read,
+ Lookahead(
+ Read,
+ Read),
+ Read);
+ }
+
+ [Fact]
+ public void MultipleLookaheadsCanBePerformed()
+ {
+ RunLookaheadTest("abcdefg", "abcbcdc",
+ Read,
+ Lookahead(
+ Read,
+ Read),
+ Read,
+ Lookahead(
+ Read,
+ Read),
+ Read);
+ }
+
+ [Fact]
+ public void LookaheadsCanBeNested()
+ {
+ RunLookaheadTest("abcdefg", "abcdefebc",
+ Read, // Appended: "a" Reader: "bcdefg"
+ Lookahead( // Reader: "bcdefg"
+ Read, // Appended: "b" Reader: "cdefg";
+ Read, // Appended: "c" Reader: "defg";
+ Read, // Appended: "d" Reader: "efg";
+ Lookahead( // Reader: "efg"
+ Read, // Appended: "e" Reader: "fg";
+ Read // Appended: "f" Reader: "g";
+ ), // Reader: "efg"
+ Read // Appended: "e" Reader: "fg";
+ ), // Reader: "bcdefg"
+ Read, // Appended: "b" Reader: "cdefg";
+ Read); // Appended: "c" Reader: "defg";
+ }
+
+ [Fact]
+ public void SourceLocationIsZeroWhenInitialized()
+ {
+ RunSourceLocationTest("abcdefg", SourceLocation.Zero, checkAt: 0);
+ }
+
+ [Fact]
+ public void CharacterAndAbsoluteIndicesIncreaseAsCharactersAreRead()
+ {
+ RunSourceLocationTest("abcdefg", new SourceLocation(4, 0, 4), checkAt: 4);
+ }
+
+ [Fact]
+ public void CharacterAndAbsoluteIndicesIncreaseAsSlashRInTwoCharacterNewlineIsRead()
+ {
+ RunSourceLocationTest("f\r\nb", new SourceLocation(2, 0, 2), checkAt: 2);
+ }
+
+ [Fact]
+ public void CharacterIndexResetsToZeroAndLineIndexIncrementsWhenSlashNInTwoCharacterNewlineIsRead()
+ {
+ RunSourceLocationTest("f\r\nb", new SourceLocation(3, 1, 0), checkAt: 3);
+ }
+
+ [Fact]
+ public void CharacterIndexResetsToZeroAndLineIndexIncrementsWhenSlashRInSingleCharacterNewlineIsRead()
+ {
+ RunSourceLocationTest("f\rb", new SourceLocation(2, 1, 0), checkAt: 2);
+ }
+
+ [Fact]
+ public void CharacterIndexResetsToZeroAndLineIndexIncrementsWhenSlashNInSingleCharacterNewlineIsRead()
+ {
+ RunSourceLocationTest("f\nb", new SourceLocation(2, 1, 0), checkAt: 2);
+ }
+
+ [Fact]
+ public void EndingLookaheadResetsRawCharacterAndLineIndexToValuesWhenLookaheadBegan()
+ {
+ RunEndLookaheadUpdatesSourceLocationTest();
+ }
+
+ [Fact]
+ public void OnceBufferingBeginsReadsCanContinuePastEndOfBuffer()
+ {
+ RunLookaheadTest("abcdefg", "abcbcdefg",
+ Read,
+ Lookahead(Read(2)),
+ Read(2),
+ ReadToEnd);
+ }
+
+ [Fact]
+ public void DisposeDisposesSourceReader()
+ {
+ RunDisposeTest(r => r.Dispose());
+ }
+
+ [Fact]
+ public void CloseDisposesSourceReader()
+ {
+ RunDisposeTest(r => r.Close());
+ }
+
+ [Fact]
+ public void ReadWithBufferSupportsLookahead()
+ {
+ RunBufferReadTest((reader, buffer, index, count) => reader.Read(buffer, index, count));
+ }
+
+ [Fact]
+ public void ReadBlockSupportsLookahead()
+ {
+ RunBufferReadTest((reader, buffer, index, count) => reader.ReadBlock(buffer, index, count));
+ }
+
+ [Fact]
+ public void ReadLineSupportsLookahead()
+ {
+ RunReadUntilTest(r => r.ReadLine(), expectedRaw: 8, expectedChar: 0, expectedLine: 2);
+ }
+
+ [Fact]
+ public void ReadToEndSupportsLookahead()
+ {
+ RunReadUntilTest(r => r.ReadToEnd(), expectedRaw: 11, expectedChar: 3, expectedLine: 2);
+ }
+
+ [Fact]
+ public void ReadLineMaintainsCorrectCharacterPosition()
+ {
+ RunSourceLocationTest("abc\r\ndef", new SourceLocation(5, 1, 0), r => r.ReadLine());
+ }
+
+ [Fact]
+ public void ReadToEndWorksAsInNormalTextReader()
+ {
+ RunReadToEndTest();
+ }
+
+ [Fact]
+ public void CancelBacktrackStopsNextEndLookaheadFromBacktracking()
+ {
+ RunLookaheadTest("abcdefg", "abcdefg",
+ Lookahead(
+ Read(2),
+ CancelBacktrack
+ ),
+ ReadToEnd);
+ }
+
+ [Fact]
+ public void CancelBacktrackThrowsInvalidOperationExceptionIfCalledOutsideOfLookahead()
+ {
+ RunCancelBacktrackOutsideLookaheadTest();
+ }
+
+ [Fact]
+ public void CancelBacktrackOnlyCancelsBacktrackingForInnermostNestedLookahead()
+ {
+ RunLookaheadTest("abcdefg", "abcdabcdefg",
+ Lookahead(
+ Read(2),
+ Lookahead(
+ Read,
+ CancelBacktrack
+ ),
+ Read
+ ),
+ ReadToEnd);
+ }
+
+ private static void RunDisposeTest(Action<LookaheadTextReader> triggerAction)
+ {
+ // Arrange
+ StringTextBuffer source = new StringTextBuffer("abcdefg");
+ LookaheadTextReader reader = new TextBufferReader(source);
+
+ // Act
+ triggerAction(reader);
+
+ // Assert
+ Assert.True(source.Disposed);
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Text/TextChangeTest.cs b/test/System.Web.Razor.Test/Text/TextChangeTest.cs
new file mode 100644
index 00000000..6e9e64d5
--- /dev/null
+++ b/test/System.Web.Razor.Test/Text/TextChangeTest.cs
@@ -0,0 +1,249 @@
+using System.Web.Razor.Text;
+using System.Web.WebPages.TestUtils;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Razor.Test.Text
+{
+ public class TextChangeTest
+ {
+ [Fact]
+ public void ConstructorRequiresNonNegativeOldPosition()
+ {
+ Assert.ThrowsArgumentOutOfRange(() => new TextChange(-1, 0, new Mock<ITextBuffer>().Object, 0, 0, new Mock<ITextBuffer>().Object), "oldPosition", "Value must be greater than or equal to 0.");
+ }
+
+ [Fact]
+ public void ConstructorRequiresNonNegativeNewPosition()
+ {
+ Assert.ThrowsArgumentOutOfRange(() => new TextChange(0, 0, new Mock<ITextBuffer>().Object, -1, 0, new Mock<ITextBuffer>().Object), "newPosition", "Value must be greater than or equal to 0.");
+ }
+
+ [Fact]
+ public void ConstructorRequiresNonNegativeOldLength()
+ {
+ Assert.ThrowsArgumentOutOfRange(() => new TextChange(0, -1, new Mock<ITextBuffer>().Object, 0, 0, new Mock<ITextBuffer>().Object), "oldLength", "Value must be greater than or equal to 0.");
+ }
+
+ [Fact]
+ public void ConstructorRequiresNonNegativeNewLength()
+ {
+ Assert.ThrowsArgumentOutOfRange(() => new TextChange(0, 0, new Mock<ITextBuffer>().Object, 0, -1, new Mock<ITextBuffer>().Object), "newLength", "Value must be greater than or equal to 0.");
+ }
+
+ [Fact]
+ public void ConstructorRequiresNonNullOldBuffer()
+ {
+ Assert.ThrowsArgumentNull(() => new TextChange(0, 0, null, 0, 0, new Mock<ITextBuffer>().Object), "oldBuffer");
+ }
+
+ [Fact]
+ public void ConstructorRequiresNonNullNewBuffer()
+ {
+ Assert.ThrowsArgumentNull(() => new TextChange(0, 0, new Mock<ITextBuffer>().Object, 0, 0, null), "newBuffer");
+ }
+
+ [Fact]
+ public void ConstructorInitializesProperties()
+ {
+ // Act
+ ITextBuffer oldBuffer = new Mock<ITextBuffer>().Object;
+ ITextBuffer newBuffer = new Mock<ITextBuffer>().Object;
+ TextChange change = new TextChange(42, 24, oldBuffer, 1337, newBuffer);
+
+ // Assert
+ Assert.Equal(42, change.OldPosition);
+ Assert.Equal(24, change.OldLength);
+ Assert.Equal(1337, change.NewLength);
+ Assert.Same(newBuffer, change.NewBuffer);
+ Assert.Same(oldBuffer, change.OldBuffer);
+ }
+
+ [Fact]
+ public void TestIsDelete()
+ {
+ // Arrange
+ ITextBuffer oldBuffer = new Mock<ITextBuffer>().Object;
+ ITextBuffer newBuffer = new Mock<ITextBuffer>().Object;
+ TextChange change = new TextChange(0, 1, oldBuffer, 0, newBuffer);
+
+ // Assert
+ Assert.True(change.IsDelete);
+ }
+
+ [Fact]
+ public void TestIsInsert()
+ {
+ // Arrange
+ ITextBuffer oldBuffer = new Mock<ITextBuffer>().Object;
+ ITextBuffer newBuffer = new Mock<ITextBuffer>().Object;
+ TextChange change = new TextChange(0, 0, oldBuffer, 35, newBuffer);
+
+ // Assert
+ Assert.True(change.IsInsert);
+ }
+
+ [Fact]
+ public void TestIsReplace()
+ {
+ // Arrange
+ ITextBuffer oldBuffer = new Mock<ITextBuffer>().Object;
+ ITextBuffer newBuffer = new Mock<ITextBuffer>().Object;
+ TextChange change = new TextChange(0, 5, oldBuffer, 10, newBuffer);
+
+ // Assert
+ Assert.True(change.IsReplace);
+ }
+
+ [Fact]
+ public void OldTextReturnsOldSpanFromOldBuffer()
+ {
+ // Arrange
+ var newBuffer = new StringTextBuffer("test");
+ var oldBuffer = new StringTextBuffer("text");
+ var textChange = new TextChange(2, 1, oldBuffer, 1, newBuffer);
+
+ // Act
+ string text = textChange.OldText;
+
+ // Assert
+ Assert.Equal("x", text);
+ }
+
+ [Fact]
+ public void NewTextWithInsertReturnsChangedTextFromBuffer()
+ {
+ // Arrange
+ var newBuffer = new StringTextBuffer("test");
+ var oldBuffer = new StringTextBuffer("");
+ var textChange = new TextChange(0, 0, oldBuffer, 3, newBuffer);
+
+ // Act
+ string text = textChange.NewText;
+
+ // Assert
+ Assert.Equal("tes", text);
+ }
+
+ [Fact]
+ public void NewTextWithDeleteReturnsEmptyString()
+ {
+ // Arrange
+ var newBuffer = new StringTextBuffer("test");
+ var oldBuffer = new StringTextBuffer("");
+ var textChange = new TextChange(1, 1, oldBuffer, 0, newBuffer);
+
+ // Act
+ string text = textChange.NewText;
+
+ // Assert
+ Assert.Equal(String.Empty, text);
+ }
+
+ [Fact]
+ public void NewTextWithReplaceReturnsChangedTextFromBuffer()
+ {
+ // Arrange
+ var newBuffer = new StringTextBuffer("test");
+ var oldBuffer = new StringTextBuffer("");
+ var textChange = new TextChange(2, 2, oldBuffer, 1, newBuffer);
+
+ // Act
+ string text = textChange.NewText;
+
+ // Assert
+ Assert.Equal("s", text);
+ }
+
+ [Fact]
+ public void ApplyChangeWithInsertedTextReturnsNewContentWithChangeApplied()
+ {
+ // Arrange
+ var newBuffer = new StringTextBuffer("test");
+ var oldBuffer = new StringTextBuffer("");
+ var textChange = new TextChange(0, 0, oldBuffer, 3, newBuffer);
+
+ // Act
+ string text = textChange.ApplyChange("abcd", 0);
+
+ // Assert
+ Assert.Equal("tesabcd", text);
+ }
+
+ [Fact]
+ public void ApplyChangeWithRemovedTextReturnsNewContentWithChangeApplied()
+ {
+ // Arrange
+ var newBuffer = new StringTextBuffer("abcdefg");
+ var oldBuffer = new StringTextBuffer("");
+ var textChange = new TextChange(1, 1, oldBuffer, 0, newBuffer);
+
+ // Act
+ string text = textChange.ApplyChange("abcdefg", 1);
+
+ // Assert
+ Assert.Equal("bcdefg", text);
+ }
+
+ [Fact]
+ public void ApplyChangeWithReplacedTextReturnsNewContentWithChangeApplied()
+ {
+ // Arrange
+ var newBuffer = new StringTextBuffer("abcdefg");
+ var oldBuffer = new StringTextBuffer("");
+ var textChange = new TextChange(1, 1, oldBuffer, 2, newBuffer);
+
+ // Act
+ string text = textChange.ApplyChange("abcdefg", 1);
+
+ // Assert
+ Assert.Equal("bcbcdefg", text);
+ }
+
+ [Fact]
+ public void NormalizeFixesUpIntelliSenseStyleReplacements()
+ {
+ // Arrange
+ var newBuffer = new StringTextBuffer("Date.");
+ var oldBuffer = new StringTextBuffer("Date");
+ var original = new TextChange(0, 4, oldBuffer, 5, newBuffer);
+
+ // Act
+ TextChange normalized = original.Normalize();
+
+ // Assert
+ Assert.Equal(new TextChange(4, 0, oldBuffer, 1, newBuffer), normalized);
+ }
+
+ [Fact]
+ public void NormalizeDoesntAffectChangesWithoutCommonPrefixes()
+ {
+ // Arrange
+ var newBuffer = new StringTextBuffer("DateTime.");
+ var oldBuffer = new StringTextBuffer("Date.");
+ var original = new TextChange(0, 5, oldBuffer, 9, newBuffer);
+
+ // Act
+ TextChange normalized = original.Normalize();
+
+ // Assert
+ Assert.Equal(original, normalized);
+ }
+
+ [Fact]
+ public void NormalizeDoesntAffectShrinkingReplacements()
+ {
+ // Arrange
+ var newBuffer = new StringTextBuffer("D");
+ var oldBuffer = new StringTextBuffer("DateTime");
+ var original = new TextChange(0, 8, oldBuffer, 1, newBuffer);
+
+ // Act
+ TextChange normalized = original.Normalize();
+
+ // Assert
+ Assert.Equal(original, normalized);
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Text/TextReaderExtensionsTest.cs b/test/System.Web.Razor.Test/Text/TextReaderExtensionsTest.cs
new file mode 100644
index 00000000..8d544a8b
--- /dev/null
+++ b/test/System.Web.Razor.Test/Text/TextReaderExtensionsTest.cs
@@ -0,0 +1,184 @@
+using System.IO;
+using System.Web.Razor.Parser;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Razor.Test.Text
+{
+ public class TextReaderExtensionsTest
+ {
+ [Fact]
+ public void ReadUntilWithCharThrowsArgNullIfReaderNull()
+ {
+ Assert.ThrowsArgumentNull(() => TextReaderExtensions.ReadUntil(null, '@'), "reader");
+ }
+
+ [Fact]
+ public void ReadUntilInclusiveWithCharThrowsArgNullIfReaderNull()
+ {
+ Assert.ThrowsArgumentNull(() => TextReaderExtensions.ReadUntil(null, '@', inclusive: true), "reader");
+ }
+
+ [Fact]
+ public void ReadUntilWithMultipleTerminatorsThrowsArgNullIfReaderNull()
+ {
+ Assert.ThrowsArgumentNull(() => TextReaderExtensions.ReadUntil(null, '/', '>'), "reader");
+ }
+
+ [Fact]
+ public void ReadUntilInclusiveWithMultipleTerminatorsThrowsArgNullIfReaderNull()
+ {
+ // NOTE: Using named parameters would be difficult here, hence the inline comment
+ Assert.ThrowsArgumentNull(() => TextReaderExtensions.ReadUntil(null, /* inclusive */ true, '/', '>'), "reader");
+ }
+
+ [Fact]
+ public void ReadUntilWithPredicateThrowsArgNullIfReaderNull()
+ {
+ Assert.ThrowsArgumentNull(() => TextReaderExtensions.ReadUntil(null, c => true), "reader");
+ }
+
+ [Fact]
+ public void ReadUntilInclusiveWithPredicateThrowsArgNullIfReaderNull()
+ {
+ Assert.ThrowsArgumentNull(() => TextReaderExtensions.ReadUntil(null, c => true, inclusive: true), "reader");
+ }
+
+ [Fact]
+ public void ReadUntilWithPredicateThrowsArgExceptionIfPredicateNull()
+ {
+ Assert.ThrowsArgumentNull(() => TextReaderExtensions.ReadUntil(new StringReader("Foo"), (Predicate<char>)null), "condition");
+ }
+
+ [Fact]
+ public void ReadUntilInclusiveWithPredicateThrowsArgExceptionIfPredicateNull()
+ {
+ Assert.ThrowsArgumentNull(() => TextReaderExtensions.ReadUntil(new StringReader("Foo"), (Predicate<char>)null, inclusive: true), "condition");
+ }
+
+ [Fact]
+ public void ReadWhileWithPredicateThrowsArgNullIfReaderNull()
+ {
+ Assert.ThrowsArgumentNull(() => TextReaderExtensions.ReadWhile(null, c => true), "reader");
+ }
+
+ [Fact]
+ public void ReadWhileInclusiveWithPredicateThrowsArgNullIfReaderNull()
+ {
+ Assert.ThrowsArgumentNull(() => TextReaderExtensions.ReadWhile(null, c => true, inclusive: true), "reader");
+ }
+
+ [Fact]
+ public void ReadWhileWithPredicateThrowsArgNullIfPredicateNull()
+ {
+ Assert.ThrowsArgumentNull(() => TextReaderExtensions.ReadWhile(new StringReader("Foo"), (Predicate<char>)null), "condition");
+ }
+
+ [Fact]
+ public void ReadWhileInclusiveWithPredicateThrowsArgNullIfPredicateNull()
+ {
+ Assert.ThrowsArgumentNull(() => TextReaderExtensions.ReadWhile(new StringReader("Foo"), (Predicate<char>)null, inclusive: true), "condition");
+ }
+
+ [Fact]
+ public void ReadUntilWithCharReadsAllTextUpToSpecifiedCharacterButNotPast()
+ {
+ RunReaderTest("foo bar baz @biz", "foo bar baz ", '@', r => r.ReadUntil('@'));
+ }
+
+ [Fact]
+ public void ReadUntilWithCharWithInclusiveFlagReadsAllTextUpToSpecifiedCharacterButNotPastIfInclusiveFalse()
+ {
+ RunReaderTest("foo bar baz @biz", "foo bar baz ", '@', r => r.ReadUntil('@', inclusive: false));
+ }
+
+ [Fact]
+ public void ReadUntilWithCharWithInclusiveFlagReadsAllTextUpToAndIncludingSpecifiedCharacterIfInclusiveTrue()
+ {
+ RunReaderTest("foo bar baz @biz", "foo bar baz @", 'b', r => r.ReadUntil('@', inclusive: true));
+ }
+
+ [Fact]
+ public void ReadUntilWithCharReadsToEndIfSpecifiedCharacterNotFound()
+ {
+ RunReaderTest("foo bar baz", "foo bar baz", -1, r => r.ReadUntil('@'));
+ }
+
+ [Fact]
+ public void ReadUntilWithMultipleTerminatorsReadsUntilAnyTerminatorIsFound()
+ {
+ RunReaderTest("<bar/>", "<bar", '/', r => r.ReadUntil('/', '>'));
+ }
+
+ [Fact]
+ public void ReadUntilWithMultipleTerminatorsHonorsInclusiveFlagWhenFalse()
+ {
+ // NOTE: Using named parameters would be difficult here, hence the inline comment
+ RunReaderTest("<bar/>", "<bar", '/', r => r.ReadUntil( /* inclusive */ false, '/', '>'));
+ }
+
+ [Fact]
+ public void ReadUntilWithMultipleTerminatorsHonorsInclusiveFlagWhenTrue()
+ {
+ // NOTE: Using named parameters would be difficult here, hence the inline comment
+ RunReaderTest("<bar/>", "<bar/", '>', r => r.ReadUntil( /* inclusive */ true, '/', '>'));
+ }
+
+ [Fact]
+ public void ReadUntilWithPredicateStopsWhenPredicateIsTrue()
+ {
+ RunReaderTest("foo bar baz 0 zoop zork zoink", "foo bar baz ", '0', r => r.ReadUntil(c => Char.IsDigit(c)));
+ }
+
+ [Fact]
+ public void ReadUntilWithPredicateHonorsInclusiveFlagWhenFalse()
+ {
+ RunReaderTest("foo bar baz 0 zoop zork zoink", "foo bar baz ", '0', r => r.ReadUntil(c => Char.IsDigit(c), inclusive: false));
+ }
+
+ [Fact]
+ public void ReadUntilWithPredicateHonorsInclusiveFlagWhenTrue()
+ {
+ RunReaderTest("foo bar baz 0 zoop zork zoink", "foo bar baz 0", ' ', r => r.ReadUntil(c => Char.IsDigit(c), inclusive: true));
+ }
+
+ [Fact]
+ public void ReadWhileWithPredicateStopsWhenPredicateIsFalse()
+ {
+ RunReaderTest("012345a67890", "012345", 'a', r => r.ReadWhile(c => Char.IsDigit(c)));
+ }
+
+ [Fact]
+ public void ReadWhileWithPredicateHonorsInclusiveFlagWhenFalse()
+ {
+ RunReaderTest("012345a67890", "012345", 'a', r => r.ReadWhile(c => Char.IsDigit(c), inclusive: false));
+ }
+
+ [Fact]
+ public void ReadWhileWithPredicateHonorsInclusiveFlagWhenTrue()
+ {
+ RunReaderTest("012345a67890", "012345a", '6', r => r.ReadWhile(c => Char.IsDigit(c), inclusive: true));
+ }
+
+ private static void RunReaderTest(string testString, string expectedOutput, int expectedPeek, Func<TextReader, string> action)
+ {
+ // Arrange
+ StringReader reader = new StringReader(testString);
+
+ // Act
+ string read = action(reader);
+
+ // Assert
+ Assert.Equal(expectedOutput, read);
+
+ if (expectedPeek == -1)
+ {
+ Assert.True(reader.Peek() == -1, "Expected that the reader would be positioned at the end of the input stream");
+ }
+ else
+ {
+ Assert.Equal((char)expectedPeek, (char)reader.Peek());
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Tokenizer/CSharpTokenizerCommentTest.cs b/test/System.Web.Razor.Test/Tokenizer/CSharpTokenizerCommentTest.cs
new file mode 100644
index 00000000..559b0117
--- /dev/null
+++ b/test/System.Web.Razor.Test/Tokenizer/CSharpTokenizerCommentTest.cs
@@ -0,0 +1,87 @@
+using System.Web.Razor.Tokenizer.Symbols;
+using Xunit;
+
+namespace System.Web.Razor.Test.Tokenizer
+{
+ public class CSharpTokenizerCommentTest : CSharpTokenizerTestBase
+ {
+ [Fact]
+ public void Next_Ignores_Star_At_EOF_In_RazorComment()
+ {
+ TestTokenizer("@* Foo * Bar * Baz *",
+ new CSharpSymbol(0, 0, 0, "@", CSharpSymbolType.RazorCommentTransition),
+ new CSharpSymbol(1, 0, 1, "*", CSharpSymbolType.RazorCommentStar),
+ new CSharpSymbol(2, 0, 2, " Foo * Bar * Baz *", CSharpSymbolType.RazorComment));
+ }
+
+ [Fact]
+ public void Next_Ignores_Star_Without_Trailing_At()
+ {
+ TestTokenizer("@* Foo * Bar * Baz *@",
+ new CSharpSymbol(0, 0, 0, "@", CSharpSymbolType.RazorCommentTransition),
+ new CSharpSymbol(1, 0, 1, "*", CSharpSymbolType.RazorCommentStar),
+ new CSharpSymbol(2, 0, 2, " Foo * Bar * Baz ", CSharpSymbolType.RazorComment),
+ new CSharpSymbol(19, 0, 19, "*", CSharpSymbolType.RazorCommentStar),
+ new CSharpSymbol(20, 0, 20, "@", CSharpSymbolType.RazorCommentTransition));
+ }
+
+ [Fact]
+ public void Next_Returns_RazorComment_Token_For_Entire_Razor_Comment()
+ {
+ TestTokenizer("@* Foo Bar Baz *@",
+ new CSharpSymbol(0, 0, 0, "@", CSharpSymbolType.RazorCommentTransition),
+ new CSharpSymbol(1, 0, 1, "*", CSharpSymbolType.RazorCommentStar),
+ new CSharpSymbol(2, 0, 2, " Foo Bar Baz ", CSharpSymbolType.RazorComment),
+ new CSharpSymbol(15, 0, 15, "*", CSharpSymbolType.RazorCommentStar),
+ new CSharpSymbol(16, 0, 16, "@", CSharpSymbolType.RazorCommentTransition));
+ }
+
+ [Fact]
+ public void Next_Returns_Comment_Token_For_Entire_Single_Line_Comment()
+ {
+ TestTokenizer("// Foo Bar Baz", new CSharpSymbol(0, 0, 0, "// Foo Bar Baz", CSharpSymbolType.Comment));
+ }
+
+ [Fact]
+ public void Single_Line_Comment_Is_Terminated_By_Newline()
+ {
+ TestTokenizer("// Foo Bar Baz\na", new CSharpSymbol(0, 0, 0, "// Foo Bar Baz", CSharpSymbolType.Comment), IgnoreRemaining);
+ }
+
+ [Fact]
+ public void Multi_Line_Comment_In_Single_Line_Comment_Has_No_Effect()
+ {
+ TestTokenizer("// Foo/*Bar*/ Baz\na", new CSharpSymbol(0, 0, 0, "// Foo/*Bar*/ Baz", CSharpSymbolType.Comment), IgnoreRemaining);
+ }
+
+ [Fact]
+ public void Next_Returns_Comment_Token_For_Entire_Multi_Line_Comment()
+ {
+ TestTokenizer("/* Foo\nBar\nBaz */", new CSharpSymbol(0, 0, 0, "/* Foo\nBar\nBaz */", CSharpSymbolType.Comment));
+ }
+
+ [Fact]
+ public void Multi_Line_Comment_Is_Terminated_By_End_Sequence()
+ {
+ TestTokenizer("/* Foo\nBar\nBaz */a", new CSharpSymbol(0, 0, 0, "/* Foo\nBar\nBaz */", CSharpSymbolType.Comment), IgnoreRemaining);
+ }
+
+ [Fact]
+ public void Unterminated_Multi_Line_Comment_Captures_To_EOF()
+ {
+ TestTokenizer("/* Foo\nBar\nBaz", new CSharpSymbol(0, 0, 0, "/* Foo\nBar\nBaz", CSharpSymbolType.Comment), IgnoreRemaining);
+ }
+
+ [Fact]
+ public void Nested_Multi_Line_Comments_Terminated_At_First_End_Sequence()
+ {
+ TestTokenizer("/* Foo/*\nBar\nBaz*/ */", new CSharpSymbol(0, 0, 0, "/* Foo/*\nBar\nBaz*/", CSharpSymbolType.Comment), IgnoreRemaining);
+ }
+
+ [Fact]
+ public void Nested_Multi_Line_Comments_Terminated_At_Full_End_Sequence()
+ {
+ TestTokenizer("/* Foo\nBar\nBaz* */", new CSharpSymbol(0, 0, 0, "/* Foo\nBar\nBaz* */", CSharpSymbolType.Comment), IgnoreRemaining);
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Tokenizer/CSharpTokenizerIdentifierTest.cs b/test/System.Web.Razor.Test/Tokenizer/CSharpTokenizerIdentifierTest.cs
new file mode 100644
index 00000000..a35f483c
--- /dev/null
+++ b/test/System.Web.Razor.Test/Tokenizer/CSharpTokenizerIdentifierTest.cs
@@ -0,0 +1,167 @@
+using System.Web.Razor.Tokenizer.Symbols;
+using Xunit;
+
+namespace System.Web.Razor.Test.Tokenizer
+{
+ public class CSharpTokenizerIdentifierTest : CSharpTokenizerTestBase
+ {
+ [Fact]
+ public void Simple_Identifier_Is_Recognized()
+ {
+ TestTokenizer("foo", new CSharpSymbol(0, 0, 0, "foo", CSharpSymbolType.Identifier));
+ }
+
+ [Fact]
+ public void Identifier_Starting_With_Underscore_Is_Recognized()
+ {
+ TestTokenizer("_foo", new CSharpSymbol(0, 0, 0, "_foo", CSharpSymbolType.Identifier));
+ }
+
+ [Fact]
+ public void Identifier_Can_Contain_Digits()
+ {
+ TestTokenizer("foo4", new CSharpSymbol(0, 0, 0, "foo4", CSharpSymbolType.Identifier));
+ }
+
+ [Fact]
+ public void Identifier_Can_Start_With_Titlecase_Letter()
+ {
+ TestTokenizer("ῼfoo", new CSharpSymbol(0, 0, 0, "ῼfoo", CSharpSymbolType.Identifier));
+ }
+
+ [Fact]
+ public void Identifier_Can_Start_With_Letter_Modifier()
+ {
+ TestTokenizer("ᵊfoo", new CSharpSymbol(0, 0, 0, "ᵊfoo", CSharpSymbolType.Identifier));
+ }
+
+ [Fact]
+ public void Identifier_Can_Start_With_Other_Letter()
+ {
+ TestTokenizer("ƻfoo", new CSharpSymbol(0, 0, 0, "ƻfoo", CSharpSymbolType.Identifier));
+ }
+
+ [Fact]
+ public void Identifier_Can_Start_With_Number_Letter()
+ {
+ TestTokenizer("Ⅽool", new CSharpSymbol(0, 0, 0, "Ⅽool", CSharpSymbolType.Identifier));
+ }
+
+ [Fact]
+ public void Identifier_Can_Contain_Non_Spacing_Mark()
+ {
+ TestTokenizer("foo\u0300", new CSharpSymbol(0, 0, 0, "foo\u0300", CSharpSymbolType.Identifier));
+ }
+
+ [Fact]
+ public void Identifier_Can_Contain_Spacing_Combining_Mark()
+ {
+ TestTokenizer("fooः", new CSharpSymbol(0, 0, 0, "fooः", CSharpSymbolType.Identifier));
+ }
+
+ [Fact]
+ public void Identifier_Can_Contain_Non_English_Digit()
+ {
+ TestTokenizer("foo١", new CSharpSymbol(0, 0, 0, "foo١", CSharpSymbolType.Identifier));
+ }
+
+ [Fact]
+ public void Identifier_Can_Contain_Connector_Punctuation()
+ {
+ TestTokenizer("foo‿bar", new CSharpSymbol(0, 0, 0, "foo‿bar", CSharpSymbolType.Identifier));
+ }
+
+ [Fact]
+ public void Identifier_Can_Contain_Format_Character()
+ {
+ TestTokenizer("foo؃bar", new CSharpSymbol(0, 0, 0, "foo؃bar", CSharpSymbolType.Identifier));
+ }
+
+ [Fact]
+ public void Keywords_Are_Recognized_As_Keyword_Tokens()
+ {
+ TestKeyword("abstract", CSharpKeyword.Abstract);
+ TestKeyword("byte", CSharpKeyword.Byte);
+ TestKeyword("class", CSharpKeyword.Class);
+ TestKeyword("delegate", CSharpKeyword.Delegate);
+ TestKeyword("event", CSharpKeyword.Event);
+ TestKeyword("fixed", CSharpKeyword.Fixed);
+ TestKeyword("if", CSharpKeyword.If);
+ TestKeyword("internal", CSharpKeyword.Internal);
+ TestKeyword("new", CSharpKeyword.New);
+ TestKeyword("override", CSharpKeyword.Override);
+ TestKeyword("readonly", CSharpKeyword.Readonly);
+ TestKeyword("short", CSharpKeyword.Short);
+ TestKeyword("struct", CSharpKeyword.Struct);
+ TestKeyword("try", CSharpKeyword.Try);
+ TestKeyword("unsafe", CSharpKeyword.Unsafe);
+ TestKeyword("volatile", CSharpKeyword.Volatile);
+ TestKeyword("as", CSharpKeyword.As);
+ TestKeyword("do", CSharpKeyword.Do);
+ TestKeyword("is", CSharpKeyword.Is);
+ TestKeyword("params", CSharpKeyword.Params);
+ TestKeyword("ref", CSharpKeyword.Ref);
+ TestKeyword("switch", CSharpKeyword.Switch);
+ TestKeyword("ushort", CSharpKeyword.Ushort);
+ TestKeyword("while", CSharpKeyword.While);
+ TestKeyword("case", CSharpKeyword.Case);
+ TestKeyword("const", CSharpKeyword.Const);
+ TestKeyword("explicit", CSharpKeyword.Explicit);
+ TestKeyword("float", CSharpKeyword.Float);
+ TestKeyword("null", CSharpKeyword.Null);
+ TestKeyword("sizeof", CSharpKeyword.Sizeof);
+ TestKeyword("typeof", CSharpKeyword.Typeof);
+ TestKeyword("implicit", CSharpKeyword.Implicit);
+ TestKeyword("private", CSharpKeyword.Private);
+ TestKeyword("this", CSharpKeyword.This);
+ TestKeyword("using", CSharpKeyword.Using);
+ TestKeyword("extern", CSharpKeyword.Extern);
+ TestKeyword("return", CSharpKeyword.Return);
+ TestKeyword("stackalloc", CSharpKeyword.Stackalloc);
+ TestKeyword("uint", CSharpKeyword.Uint);
+ TestKeyword("base", CSharpKeyword.Base);
+ TestKeyword("catch", CSharpKeyword.Catch);
+ TestKeyword("continue", CSharpKeyword.Continue);
+ TestKeyword("double", CSharpKeyword.Double);
+ TestKeyword("for", CSharpKeyword.For);
+ TestKeyword("in", CSharpKeyword.In);
+ TestKeyword("lock", CSharpKeyword.Lock);
+ TestKeyword("object", CSharpKeyword.Object);
+ TestKeyword("protected", CSharpKeyword.Protected);
+ TestKeyword("static", CSharpKeyword.Static);
+ TestKeyword("false", CSharpKeyword.False);
+ TestKeyword("public", CSharpKeyword.Public);
+ TestKeyword("sbyte", CSharpKeyword.Sbyte);
+ TestKeyword("throw", CSharpKeyword.Throw);
+ TestKeyword("virtual", CSharpKeyword.Virtual);
+ TestKeyword("decimal", CSharpKeyword.Decimal);
+ TestKeyword("else", CSharpKeyword.Else);
+ TestKeyword("operator", CSharpKeyword.Operator);
+ TestKeyword("string", CSharpKeyword.String);
+ TestKeyword("ulong", CSharpKeyword.Ulong);
+ TestKeyword("bool", CSharpKeyword.Bool);
+ TestKeyword("char", CSharpKeyword.Char);
+ TestKeyword("default", CSharpKeyword.Default);
+ TestKeyword("foreach", CSharpKeyword.Foreach);
+ TestKeyword("long", CSharpKeyword.Long);
+ TestKeyword("void", CSharpKeyword.Void);
+ TestKeyword("enum", CSharpKeyword.Enum);
+ TestKeyword("finally", CSharpKeyword.Finally);
+ TestKeyword("int", CSharpKeyword.Int);
+ TestKeyword("out", CSharpKeyword.Out);
+ TestKeyword("sealed", CSharpKeyword.Sealed);
+ TestKeyword("true", CSharpKeyword.True);
+ TestKeyword("goto", CSharpKeyword.Goto);
+ TestKeyword("unchecked", CSharpKeyword.Unchecked);
+ TestKeyword("interface", CSharpKeyword.Interface);
+ TestKeyword("break", CSharpKeyword.Break);
+ TestKeyword("checked", CSharpKeyword.Checked);
+ TestKeyword("namespace", CSharpKeyword.Namespace);
+ }
+
+ private void TestKeyword(string keyword, CSharpKeyword keywordType)
+ {
+ TestTokenizer(keyword, new CSharpSymbol(0, 0, 0, keyword, CSharpSymbolType.Keyword) { Keyword = keywordType });
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Tokenizer/CSharpTokenizerLiteralTest.cs b/test/System.Web.Razor.Test/Tokenizer/CSharpTokenizerLiteralTest.cs
new file mode 100644
index 00000000..b6a0df0f
--- /dev/null
+++ b/test/System.Web.Razor.Test/Tokenizer/CSharpTokenizerLiteralTest.cs
@@ -0,0 +1,222 @@
+using System.Web.Razor.Tokenizer.Symbols;
+using Xunit;
+
+namespace System.Web.Razor.Test.Tokenizer
+{
+ public class CSharpTokenizerLiteralTest : CSharpTokenizerTestBase
+ {
+ [Fact]
+ public void Simple_Integer_Literal_Is_Recognized()
+ {
+ TestSingleToken("01189998819991197253", CSharpSymbolType.IntegerLiteral);
+ }
+
+ [Fact]
+ public void Integer_Type_Suffix_Is_Recognized()
+ {
+ TestSingleToken("42U", CSharpSymbolType.IntegerLiteral);
+ TestSingleToken("42u", CSharpSymbolType.IntegerLiteral);
+
+ TestSingleToken("42L", CSharpSymbolType.IntegerLiteral);
+ TestSingleToken("42l", CSharpSymbolType.IntegerLiteral);
+
+ TestSingleToken("42UL", CSharpSymbolType.IntegerLiteral);
+ TestSingleToken("42Ul", CSharpSymbolType.IntegerLiteral);
+
+ TestSingleToken("42uL", CSharpSymbolType.IntegerLiteral);
+ TestSingleToken("42ul", CSharpSymbolType.IntegerLiteral);
+
+ TestSingleToken("42LU", CSharpSymbolType.IntegerLiteral);
+ TestSingleToken("42Lu", CSharpSymbolType.IntegerLiteral);
+
+ TestSingleToken("42lU", CSharpSymbolType.IntegerLiteral);
+ TestSingleToken("42lu", CSharpSymbolType.IntegerLiteral);
+ }
+
+ [Fact]
+ public void Trailing_Letter_Is_Not_Part_Of_Integer_Literal_If_Not_Type_Sufix()
+ {
+ TestTokenizer("42a", new CSharpSymbol(0, 0, 0, "42", CSharpSymbolType.IntegerLiteral), IgnoreRemaining);
+ }
+
+ [Fact]
+ public void Simple_Hex_Literal_Is_Recognized()
+ {
+ TestSingleToken("0x0123456789ABCDEF", CSharpSymbolType.IntegerLiteral);
+ }
+
+ [Fact]
+ public void Integer_Type_Suffix_Is_Recognized_In_Hex_Literal()
+ {
+ TestSingleToken("0xDEADBEEFU", CSharpSymbolType.IntegerLiteral);
+ TestSingleToken("0xDEADBEEFu", CSharpSymbolType.IntegerLiteral);
+
+ TestSingleToken("0xDEADBEEFL", CSharpSymbolType.IntegerLiteral);
+ TestSingleToken("0xDEADBEEFl", CSharpSymbolType.IntegerLiteral);
+
+ TestSingleToken("0xDEADBEEFUL", CSharpSymbolType.IntegerLiteral);
+ TestSingleToken("0xDEADBEEFUl", CSharpSymbolType.IntegerLiteral);
+
+ TestSingleToken("0xDEADBEEFuL", CSharpSymbolType.IntegerLiteral);
+ TestSingleToken("0xDEADBEEFul", CSharpSymbolType.IntegerLiteral);
+
+ TestSingleToken("0xDEADBEEFLU", CSharpSymbolType.IntegerLiteral);
+ TestSingleToken("0xDEADBEEFLu", CSharpSymbolType.IntegerLiteral);
+
+ TestSingleToken("0xDEADBEEFlU", CSharpSymbolType.IntegerLiteral);
+ TestSingleToken("0xDEADBEEFlu", CSharpSymbolType.IntegerLiteral);
+ }
+
+ [Fact]
+ public void Trailing_Letter_Is_Not_Part_Of_Hex_Literal_If_Not_Type_Sufix()
+ {
+ TestTokenizer("0xDEADBEEFz", new CSharpSymbol(0, 0, 0, "0xDEADBEEF", CSharpSymbolType.IntegerLiteral), IgnoreRemaining);
+ }
+
+ [Fact]
+ public void Dot_Followed_By_Non_Digit_Is_Not_Part_Of_Real_Literal()
+ {
+ TestTokenizer("3.a", new CSharpSymbol(0, 0, 0, "3", CSharpSymbolType.IntegerLiteral), IgnoreRemaining);
+ }
+
+ [Fact]
+ public void Simple_Real_Literal_Is_Recognized()
+ {
+ TestTokenizer("3.14159", new CSharpSymbol(0, 0, 0, "3.14159", CSharpSymbolType.RealLiteral));
+ }
+
+ [Fact]
+ public void Real_Literal_Between_Zero_And_One_Is_Recognized()
+ {
+ TestTokenizer(".14159", new CSharpSymbol(0, 0, 0, ".14159", CSharpSymbolType.RealLiteral));
+ }
+
+ [Fact]
+ public void Integer_With_Real_Type_Suffix_Is_Recognized()
+ {
+ TestSingleToken("42F", CSharpSymbolType.RealLiteral);
+ TestSingleToken("42f", CSharpSymbolType.RealLiteral);
+ TestSingleToken("42D", CSharpSymbolType.RealLiteral);
+ TestSingleToken("42d", CSharpSymbolType.RealLiteral);
+ TestSingleToken("42M", CSharpSymbolType.RealLiteral);
+ TestSingleToken("42m", CSharpSymbolType.RealLiteral);
+ }
+
+ [Fact]
+ public void Integer_With_Exponent_Is_Recognized()
+ {
+ TestSingleToken("1e10", CSharpSymbolType.RealLiteral);
+ TestSingleToken("1E10", CSharpSymbolType.RealLiteral);
+ TestSingleToken("1e+10", CSharpSymbolType.RealLiteral);
+ TestSingleToken("1E+10", CSharpSymbolType.RealLiteral);
+ TestSingleToken("1e-10", CSharpSymbolType.RealLiteral);
+ TestSingleToken("1E-10", CSharpSymbolType.RealLiteral);
+ }
+
+ [Fact]
+ public void Real_Number_With_Type_Suffix_Is_Recognized()
+ {
+ TestSingleToken("3.14F", CSharpSymbolType.RealLiteral);
+ TestSingleToken("3.14f", CSharpSymbolType.RealLiteral);
+ TestSingleToken("3.14D", CSharpSymbolType.RealLiteral);
+ TestSingleToken("3.14d", CSharpSymbolType.RealLiteral);
+ TestSingleToken("3.14M", CSharpSymbolType.RealLiteral);
+ TestSingleToken("3.14m", CSharpSymbolType.RealLiteral);
+ }
+
+ [Fact]
+ public void Real_Number_With_Exponent_Is_Recognized()
+ {
+ TestSingleToken("3.14E10", CSharpSymbolType.RealLiteral);
+ TestSingleToken("3.14e10", CSharpSymbolType.RealLiteral);
+ TestSingleToken("3.14E+10", CSharpSymbolType.RealLiteral);
+ TestSingleToken("3.14e+10", CSharpSymbolType.RealLiteral);
+ TestSingleToken("3.14E-10", CSharpSymbolType.RealLiteral);
+ TestSingleToken("3.14e-10", CSharpSymbolType.RealLiteral);
+ }
+
+ [Fact]
+ public void Real_Number_With_Exponent_And_Type_Suffix_Is_Recognized()
+ {
+ TestSingleToken("3.14E+10F", CSharpSymbolType.RealLiteral);
+ }
+
+ [Fact]
+ public void Single_Character_Literal_Is_Recognized()
+ {
+ TestSingleToken("'f'", CSharpSymbolType.CharacterLiteral);
+ }
+
+ [Fact]
+ public void Multi_Character_Literal_Is_Recognized()
+ {
+ TestSingleToken("'foo'", CSharpSymbolType.CharacterLiteral);
+ }
+
+ [Fact]
+ public void Character_Literal_Is_Terminated_By_EOF_If_Unterminated()
+ {
+ TestSingleToken("'foo bar", CSharpSymbolType.CharacterLiteral);
+ }
+
+ [Fact]
+ public void Character_Literal_Not_Terminated_By_Escaped_Quote()
+ {
+ TestSingleToken("'foo\\'bar'", CSharpSymbolType.CharacterLiteral);
+ }
+
+ [Fact]
+ public void Character_Literal_Is_Terminated_By_EOL_If_Unterminated()
+ {
+ TestTokenizer("'foo\n", new CSharpSymbol(0, 0, 0, "'foo", CSharpSymbolType.CharacterLiteral), IgnoreRemaining);
+ }
+
+ [Fact]
+ public void String_Literal_Is_Recognized()
+ {
+ TestSingleToken("\"foo\"", CSharpSymbolType.StringLiteral);
+ }
+
+ [Fact]
+ public void String_Literal_Is_Terminated_By_EOF_If_Unterminated()
+ {
+ TestSingleToken("\"foo bar", CSharpSymbolType.StringLiteral);
+ }
+
+ [Fact]
+ public void String_Literal_Not_Terminated_By_Escaped_Quote()
+ {
+ TestSingleToken("\"foo\\\"bar\"", CSharpSymbolType.StringLiteral);
+ }
+
+ [Fact]
+ public void String_Literal_Is_Terminated_By_EOL_If_Unterminated()
+ {
+ TestTokenizer("\"foo\n", new CSharpSymbol(0, 0, 0, "\"foo", CSharpSymbolType.StringLiteral), IgnoreRemaining);
+ }
+
+ [Fact]
+ public void Verbatim_String_Literal_Can_Contain_Newlines()
+ {
+ TestSingleToken("@\"foo\nbar\nbaz\"", CSharpSymbolType.StringLiteral);
+ }
+
+ [Fact]
+ public void Verbatim_String_Literal_Not_Terminated_By_Escaped_Double_Quote()
+ {
+ TestSingleToken("@\"foo\"\"bar\"", CSharpSymbolType.StringLiteral);
+ }
+
+ [Fact]
+ public void Verbatim_String_Literal_Is_Terminated_By_Slash_Double_Quote()
+ {
+ TestTokenizer("@\"foo\\\"bar\"", new CSharpSymbol(0, 0, 0, "@\"foo\\\"", CSharpSymbolType.StringLiteral), IgnoreRemaining);
+ }
+
+ [Fact]
+ public void Verbatim_String_Literal_Is_Terminated_By_EOF()
+ {
+ TestSingleToken("@\"foo", CSharpSymbolType.StringLiteral);
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Tokenizer/CSharpTokenizerOperatorsTest.cs b/test/System.Web.Razor.Test/Tokenizer/CSharpTokenizerOperatorsTest.cs
new file mode 100644
index 00000000..5507102a
--- /dev/null
+++ b/test/System.Web.Razor.Test/Tokenizer/CSharpTokenizerOperatorsTest.cs
@@ -0,0 +1,294 @@
+using System.Web.Razor.Tokenizer.Symbols;
+using Xunit;
+
+namespace System.Web.Razor.Test.Tokenizer
+{
+ public class CSharpTokenizerOperatorsTest : CSharpTokenizerTestBase
+ {
+ [Fact]
+ public void LeftBrace_Is_Recognized()
+ {
+ TestSingleToken("{", CSharpSymbolType.LeftBrace);
+ }
+
+ [Fact]
+ public void Plus_Is_Recognized()
+ {
+ TestSingleToken("+", CSharpSymbolType.Plus);
+ }
+
+ [Fact]
+ public void Assign_Is_Recognized()
+ {
+ TestSingleToken("=", CSharpSymbolType.Assign);
+ }
+
+ [Fact]
+ public void Arrow_Is_Recognized()
+ {
+ TestSingleToken("->", CSharpSymbolType.Arrow);
+ }
+
+ [Fact]
+ public void AndAssign_Is_Recognized()
+ {
+ TestSingleToken("&=", CSharpSymbolType.AndAssign);
+ }
+
+ [Fact]
+ public void RightBrace_Is_Recognized()
+ {
+ TestSingleToken("}", CSharpSymbolType.RightBrace);
+ }
+
+ [Fact]
+ public void Minus_Is_Recognized()
+ {
+ TestSingleToken("-", CSharpSymbolType.Minus);
+ }
+
+ [Fact]
+ public void LessThan_Is_Recognized()
+ {
+ TestSingleToken("<", CSharpSymbolType.LessThan);
+ }
+
+ [Fact]
+ public void Equals_Is_Recognized()
+ {
+ TestSingleToken("==", CSharpSymbolType.Equals);
+ }
+
+ [Fact]
+ public void OrAssign_Is_Recognized()
+ {
+ TestSingleToken("|=", CSharpSymbolType.OrAssign);
+ }
+
+ [Fact]
+ public void LeftBracket_Is_Recognized()
+ {
+ TestSingleToken("[", CSharpSymbolType.LeftBracket);
+ }
+
+ [Fact]
+ public void Star_Is_Recognized()
+ {
+ TestSingleToken("*", CSharpSymbolType.Star);
+ }
+
+ [Fact]
+ public void GreaterThan_Is_Recognized()
+ {
+ TestSingleToken(">", CSharpSymbolType.GreaterThan);
+ }
+
+ [Fact]
+ public void NotEqual_Is_Recognized()
+ {
+ TestSingleToken("!=", CSharpSymbolType.NotEqual);
+ }
+
+ [Fact]
+ public void XorAssign_Is_Recognized()
+ {
+ TestSingleToken("^=", CSharpSymbolType.XorAssign);
+ }
+
+ [Fact]
+ public void RightBracket_Is_Recognized()
+ {
+ TestSingleToken("]", CSharpSymbolType.RightBracket);
+ }
+
+ [Fact]
+ public void Slash_Is_Recognized()
+ {
+ TestSingleToken("/", CSharpSymbolType.Slash);
+ }
+
+ [Fact]
+ public void QuestionMark_Is_Recognized()
+ {
+ TestSingleToken("?", CSharpSymbolType.QuestionMark);
+ }
+
+ [Fact]
+ public void LessThanEqual_Is_Recognized()
+ {
+ TestSingleToken("<=", CSharpSymbolType.LessThanEqual);
+ }
+
+ [Fact]
+ public void LeftShift_Is_Not_Specially_Recognized()
+ {
+ TestTokenizer("<<",
+ new CSharpSymbol(0, 0, 0, "<", CSharpSymbolType.LessThan),
+ new CSharpSymbol(1, 0, 1, "<", CSharpSymbolType.LessThan));
+ }
+
+ [Fact]
+ public void LeftParen_Is_Recognized()
+ {
+ TestSingleToken("(", CSharpSymbolType.LeftParenthesis);
+ }
+
+ [Fact]
+ public void Modulo_Is_Recognized()
+ {
+ TestSingleToken("%", CSharpSymbolType.Modulo);
+ }
+
+ [Fact]
+ public void NullCoalesce_Is_Recognized()
+ {
+ TestSingleToken("??", CSharpSymbolType.NullCoalesce);
+ }
+
+ [Fact]
+ public void GreaterThanEqual_Is_Recognized()
+ {
+ TestSingleToken(">=", CSharpSymbolType.GreaterThanEqual);
+ }
+
+ [Fact]
+ public void EqualGreaterThan_Is_Recognized()
+ {
+ TestSingleToken("=>", CSharpSymbolType.GreaterThanEqual);
+ }
+
+ [Fact]
+ public void RightParen_Is_Recognized()
+ {
+ TestSingleToken(")", CSharpSymbolType.RightParenthesis);
+ }
+
+ [Fact]
+ public void And_Is_Recognized()
+ {
+ TestSingleToken("&", CSharpSymbolType.And);
+ }
+
+ [Fact]
+ public void DoubleColon_Is_Recognized()
+ {
+ TestSingleToken("::", CSharpSymbolType.DoubleColon);
+ }
+
+ [Fact]
+ public void PlusAssign_Is_Recognized()
+ {
+ TestSingleToken("+=", CSharpSymbolType.PlusAssign);
+ }
+
+ [Fact]
+ public void Semicolon_Is_Recognized()
+ {
+ TestSingleToken(";", CSharpSymbolType.Semicolon);
+ }
+
+ [Fact]
+ public void Tilde_Is_Recognized()
+ {
+ TestSingleToken("~", CSharpSymbolType.Tilde);
+ }
+
+ [Fact]
+ public void DoubleOr_Is_Recognized()
+ {
+ TestSingleToken("||", CSharpSymbolType.DoubleOr);
+ }
+
+ [Fact]
+ public void ModuloAssign_Is_Recognized()
+ {
+ TestSingleToken("%=", CSharpSymbolType.ModuloAssign);
+ }
+
+ [Fact]
+ public void Colon_Is_Recognized()
+ {
+ TestSingleToken(":", CSharpSymbolType.Colon);
+ }
+
+ [Fact]
+ public void Not_Is_Recognized()
+ {
+ TestSingleToken("!", CSharpSymbolType.Not);
+ }
+
+ [Fact]
+ public void DoubleAnd_Is_Recognized()
+ {
+ TestSingleToken("&&", CSharpSymbolType.DoubleAnd);
+ }
+
+ [Fact]
+ public void DivideAssign_Is_Recognized()
+ {
+ TestSingleToken("/=", CSharpSymbolType.DivideAssign);
+ }
+
+ [Fact]
+ public void Comma_Is_Recognized()
+ {
+ TestSingleToken(",", CSharpSymbolType.Comma);
+ }
+
+ [Fact]
+ public void Xor_Is_Recognized()
+ {
+ TestSingleToken("^", CSharpSymbolType.Xor);
+ }
+
+ [Fact]
+ public void Decrement_Is_Recognized()
+ {
+ TestSingleToken("--", CSharpSymbolType.Decrement);
+ }
+
+ [Fact]
+ public void MultiplyAssign_Is_Recognized()
+ {
+ TestSingleToken("*=", CSharpSymbolType.MultiplyAssign);
+ }
+
+ [Fact]
+ public void Dot_Is_Recognized()
+ {
+ TestSingleToken(".", CSharpSymbolType.Dot);
+ }
+
+ [Fact]
+ public void Or_Is_Recognized()
+ {
+ TestSingleToken("|", CSharpSymbolType.Or);
+ }
+
+ [Fact]
+ public void Increment_Is_Recognized()
+ {
+ TestSingleToken("++", CSharpSymbolType.Increment);
+ }
+
+ [Fact]
+ public void MinusAssign_Is_Recognized()
+ {
+ TestSingleToken("-=", CSharpSymbolType.MinusAssign);
+ }
+
+ [Fact]
+ public void RightShift_Is_Not_Specially_Recognized()
+ {
+ TestTokenizer(">>",
+ new CSharpSymbol(0, 0, 0, ">", CSharpSymbolType.GreaterThan),
+ new CSharpSymbol(1, 0, 1, ">", CSharpSymbolType.GreaterThan));
+ }
+
+ [Fact]
+ public void Hash_Is_Recognized()
+ {
+ TestSingleToken("#", CSharpSymbolType.Hash);
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Tokenizer/CSharpTokenizerTest.cs b/test/System.Web.Razor.Test/Tokenizer/CSharpTokenizerTest.cs
new file mode 100644
index 00000000..9b9b64cf
--- /dev/null
+++ b/test/System.Web.Razor.Test/Tokenizer/CSharpTokenizerTest.cs
@@ -0,0 +1,102 @@
+using System.Web.Razor.Tokenizer;
+using System.Web.Razor.Tokenizer.Symbols;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Razor.Test.Tokenizer
+{
+ public class CSharpTokenizerTest : CSharpTokenizerTestBase
+ {
+ [Fact]
+ public void Constructor_Throws_ArgNull_If_Null_Source_Provided()
+ {
+ Assert.ThrowsArgumentNull(() => new CSharpTokenizer(null), "source");
+ }
+
+ [Fact]
+ public void Next_Returns_Null_When_EOF_Reached()
+ {
+ TestTokenizer("");
+ }
+
+ [Fact]
+ public void Next_Returns_Newline_Token_For_Single_CR()
+ {
+ TestTokenizer("\r\ra",
+ new CSharpSymbol(0, 0, 0, "\r", CSharpSymbolType.NewLine),
+ new CSharpSymbol(1, 1, 0, "\r", CSharpSymbolType.NewLine),
+ IgnoreRemaining);
+ }
+
+ [Fact]
+ public void Next_Returns_Newline_Token_For_Single_LF()
+ {
+ TestTokenizer("\n\na",
+ new CSharpSymbol(0, 0, 0, "\n", CSharpSymbolType.NewLine),
+ new CSharpSymbol(1, 1, 0, "\n", CSharpSymbolType.NewLine),
+ IgnoreRemaining);
+ }
+
+ [Fact]
+ public void Next_Returns_Newline_Token_For_Single_NEL()
+ {
+ // NEL: Unicode "Next Line" U+0085
+ TestTokenizer("\u0085\u0085a",
+ new CSharpSymbol(0, 0, 0, "\u0085", CSharpSymbolType.NewLine),
+ new CSharpSymbol(1, 1, 0, "\u0085", CSharpSymbolType.NewLine),
+ IgnoreRemaining);
+ }
+
+ [Fact]
+ public void Next_Returns_Newline_Token_For_Single_Line_Separator()
+ {
+ // Unicode "Line Separator" U+2028
+ TestTokenizer("\u2028\u2028a",
+ new CSharpSymbol(0, 0, 0, "\u2028", CSharpSymbolType.NewLine),
+ new CSharpSymbol(1, 1, 0, "\u2028", CSharpSymbolType.NewLine),
+ IgnoreRemaining);
+ }
+
+ [Fact]
+ public void Next_Returns_Newline_Token_For_Single_Paragraph_Separator()
+ {
+ // Unicode "Paragraph Separator" U+2029
+ TestTokenizer("\u2029\u2029a",
+ new CSharpSymbol(0, 0, 0, "\u2029", CSharpSymbolType.NewLine),
+ new CSharpSymbol(1, 1, 0, "\u2029", CSharpSymbolType.NewLine),
+ IgnoreRemaining);
+ }
+
+ [Fact]
+ public void Next_Returns_Single_Newline_Token_For_CRLF()
+ {
+ TestTokenizer("\r\n\r\na",
+ new CSharpSymbol(0, 0, 0, "\r\n", CSharpSymbolType.NewLine),
+ new CSharpSymbol(2, 1, 0, "\r\n", CSharpSymbolType.NewLine),
+ IgnoreRemaining);
+ }
+
+ [Fact]
+ public void Next_Returns_Token_For_Whitespace_Characters()
+ {
+ TestTokenizer(" \f\t\u000B \n ",
+ new CSharpSymbol(0, 0, 0, " \f\t\u000B ", CSharpSymbolType.WhiteSpace),
+ new CSharpSymbol(5, 0, 5, "\n", CSharpSymbolType.NewLine),
+ new CSharpSymbol(6, 1, 0, " ", CSharpSymbolType.WhiteSpace));
+ }
+
+ [Fact]
+ public void Transition_Is_Recognized()
+ {
+ TestSingleToken("@", CSharpSymbolType.Transition);
+ }
+
+ [Fact]
+ public void Transition_Is_Recognized_As_SingleCharacter()
+ {
+ TestTokenizer("@(",
+ new CSharpSymbol(0, 0, 0, "@", CSharpSymbolType.Transition),
+ new CSharpSymbol(1, 0, 1, "(", CSharpSymbolType.LeftParenthesis));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Tokenizer/CSharpTokenizerTestBase.cs b/test/System.Web.Razor.Test/Tokenizer/CSharpTokenizerTestBase.cs
new file mode 100644
index 00000000..9d7b4d97
--- /dev/null
+++ b/test/System.Web.Razor.Test/Tokenizer/CSharpTokenizerTestBase.cs
@@ -0,0 +1,26 @@
+using System.Web.Razor.Text;
+using System.Web.Razor.Tokenizer;
+using System.Web.Razor.Tokenizer.Symbols;
+
+namespace System.Web.Razor.Test.Tokenizer
+{
+ public abstract class CSharpTokenizerTestBase : TokenizerTestBase<CSharpSymbol, CSharpSymbolType>
+ {
+ private static CSharpSymbol _ignoreRemaining = new CSharpSymbol(0, 0, 0, String.Empty, CSharpSymbolType.Unknown);
+
+ protected override CSharpSymbol IgnoreRemaining
+ {
+ get { return _ignoreRemaining; }
+ }
+
+ protected override Tokenizer<CSharpSymbol, CSharpSymbolType> CreateTokenizer(ITextDocument source)
+ {
+ return new CSharpTokenizer(source);
+ }
+
+ protected void TestSingleToken(string text, CSharpSymbolType expectedSymbolType)
+ {
+ TestTokenizer(text, new CSharpSymbol(0, 0, 0, text, expectedSymbolType));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Tokenizer/HtmlTokenizerTest.cs b/test/System.Web.Razor.Test/Tokenizer/HtmlTokenizerTest.cs
new file mode 100644
index 00000000..6a2387d8
--- /dev/null
+++ b/test/System.Web.Razor.Test/Tokenizer/HtmlTokenizerTest.cs
@@ -0,0 +1,166 @@
+using System.Web.Razor.Tokenizer;
+using System.Web.Razor.Tokenizer.Symbols;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Razor.Test.Tokenizer
+{
+ public class HtmlTokenizerTest : HtmlTokenizerTestBase
+ {
+ [Fact]
+ public void Constructor_Throws_ArgNull_If_Null_Source_Provided()
+ {
+ Assert.ThrowsArgumentNull(() => new HtmlTokenizer(null), "source");
+ }
+
+ [Fact]
+ public void Next_Returns_Null_When_EOF_Reached()
+ {
+ TestTokenizer("");
+ }
+
+ [Fact]
+ public void Text_Is_Recognized()
+ {
+ TestTokenizer("foo-9309&smlkmb;::-3029022,.sdkq92384",
+ new HtmlSymbol(0, 0, 0, "foo-9309&smlkmb;::-3029022,.sdkq92384", HtmlSymbolType.Text));
+ }
+
+ [Fact]
+ public void Whitespace_Is_Recognized()
+ {
+ TestTokenizer(" \t\f ",
+ new HtmlSymbol(0, 0, 0, " \t\f ", HtmlSymbolType.WhiteSpace));
+ }
+
+ [Fact]
+ public void Newline_Is_Recognized()
+ {
+ TestTokenizer("\n\r\r\n",
+ new HtmlSymbol(0, 0, 0, "\n", HtmlSymbolType.NewLine),
+ new HtmlSymbol(1, 1, 0, "\r", HtmlSymbolType.NewLine),
+ new HtmlSymbol(2, 2, 0, "\r\n", HtmlSymbolType.NewLine));
+ }
+
+ [Fact]
+ public void Transition_Is_Not_Recognized_Mid_Text_If_Surrounded_By_Alphanumeric_Characters()
+ {
+ TestSingleToken("foo@bar", HtmlSymbolType.Text);
+ }
+
+ [Fact]
+ public void OpenAngle_Is_Recognized()
+ {
+ TestSingleToken("<", HtmlSymbolType.OpenAngle);
+ }
+
+ [Fact]
+ public void Bang_Is_Recognized()
+ {
+ TestSingleToken("!", HtmlSymbolType.Bang);
+ }
+
+ [Fact]
+ public void Solidus_Is_Recognized()
+ {
+ TestSingleToken("/", HtmlSymbolType.Solidus);
+ }
+
+ [Fact]
+ public void QuestionMark_Is_Recognized()
+ {
+ TestSingleToken("?", HtmlSymbolType.QuestionMark);
+ }
+
+ [Fact]
+ public void LeftBracket_Is_Recognized()
+ {
+ TestSingleToken("[", HtmlSymbolType.LeftBracket);
+ }
+
+ [Fact]
+ public void CloseAngle_Is_Recognized()
+ {
+ TestSingleToken(">", HtmlSymbolType.CloseAngle);
+ }
+
+ [Fact]
+ public void RightBracket_Is_Recognized()
+ {
+ TestSingleToken("]", HtmlSymbolType.RightBracket);
+ }
+
+ [Fact]
+ public void Equals_Is_Recognized()
+ {
+ TestSingleToken("=", HtmlSymbolType.Equals);
+ }
+
+ [Fact]
+ public void DoubleQuote_Is_Recognized()
+ {
+ TestSingleToken("\"", HtmlSymbolType.DoubleQuote);
+ }
+
+ [Fact]
+ public void SingleQuote_Is_Recognized()
+ {
+ TestSingleToken("'", HtmlSymbolType.SingleQuote);
+ }
+
+ [Fact]
+ public void Transition_Is_Recognized()
+ {
+ TestSingleToken("@", HtmlSymbolType.Transition);
+ }
+
+ [Fact]
+ public void DoubleHyphen_Is_Recognized()
+ {
+ TestSingleToken("--", HtmlSymbolType.DoubleHyphen);
+ }
+
+ [Fact]
+ public void SingleHyphen_Is_Not_Recognized()
+ {
+ TestSingleToken("-", HtmlSymbolType.Text);
+ }
+
+ [Fact]
+ public void SingleHyphen_Mid_Text_Is_Not_Recognized_As_Separate_Token()
+ {
+ TestSingleToken("foo-bar", HtmlSymbolType.Text);
+ }
+
+ [Fact]
+ public void Next_Ignores_Star_At_EOF_In_RazorComment()
+ {
+ TestTokenizer("@* Foo * Bar * Baz *",
+ new HtmlSymbol(0, 0, 0, "@", HtmlSymbolType.RazorCommentTransition),
+ new HtmlSymbol(1, 0, 1, "*", HtmlSymbolType.RazorCommentStar),
+ new HtmlSymbol(2, 0, 2, " Foo * Bar * Baz *", HtmlSymbolType.RazorComment));
+ }
+
+ [Fact]
+ public void Next_Ignores_Star_Without_Trailing_At()
+ {
+ TestTokenizer("@* Foo * Bar * Baz *@",
+ new HtmlSymbol(0, 0, 0, "@", HtmlSymbolType.RazorCommentTransition),
+ new HtmlSymbol(1, 0, 1, "*", HtmlSymbolType.RazorCommentStar),
+ new HtmlSymbol(2, 0, 2, " Foo * Bar * Baz ", HtmlSymbolType.RazorComment),
+ new HtmlSymbol(19, 0, 19, "*", HtmlSymbolType.RazorCommentStar),
+ new HtmlSymbol(20, 0, 20, "@", HtmlSymbolType.RazorCommentTransition));
+ }
+
+ [Fact]
+ public void Next_Returns_RazorComment_Token_For_Entire_Razor_Comment()
+ {
+ TestTokenizer("@* Foo Bar Baz *@",
+ new HtmlSymbol(0, 0, 0, "@", HtmlSymbolType.RazorCommentTransition),
+ new HtmlSymbol(1, 0, 1, "*", HtmlSymbolType.RazorCommentStar),
+ new HtmlSymbol(2, 0, 2, " Foo Bar Baz ", HtmlSymbolType.RazorComment),
+ new HtmlSymbol(15, 0, 15, "*", HtmlSymbolType.RazorCommentStar),
+ new HtmlSymbol(16, 0, 16, "@", HtmlSymbolType.RazorCommentTransition));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Tokenizer/HtmlTokenizerTestBase.cs b/test/System.Web.Razor.Test/Tokenizer/HtmlTokenizerTestBase.cs
new file mode 100644
index 00000000..06210d37
--- /dev/null
+++ b/test/System.Web.Razor.Test/Tokenizer/HtmlTokenizerTestBase.cs
@@ -0,0 +1,26 @@
+using System.Web.Razor.Text;
+using System.Web.Razor.Tokenizer;
+using System.Web.Razor.Tokenizer.Symbols;
+
+namespace System.Web.Razor.Test.Tokenizer
+{
+ public abstract class HtmlTokenizerTestBase : TokenizerTestBase<HtmlSymbol, HtmlSymbolType>
+ {
+ private static HtmlSymbol _ignoreRemaining = new HtmlSymbol(0, 0, 0, String.Empty, HtmlSymbolType.Unknown);
+
+ protected override HtmlSymbol IgnoreRemaining
+ {
+ get { return _ignoreRemaining; }
+ }
+
+ protected override Tokenizer<HtmlSymbol, HtmlSymbolType> CreateTokenizer(ITextDocument source)
+ {
+ return new HtmlTokenizer(source);
+ }
+
+ protected void TestSingleToken(string text, HtmlSymbolType expectedSymbolType)
+ {
+ TestTokenizer(text, new HtmlSymbol(0, 0, 0, text, expectedSymbolType));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Tokenizer/TokenizerLookaheadTest.cs b/test/System.Web.Razor.Test/Tokenizer/TokenizerLookaheadTest.cs
new file mode 100644
index 00000000..bc1d2db9
--- /dev/null
+++ b/test/System.Web.Razor.Test/Tokenizer/TokenizerLookaheadTest.cs
@@ -0,0 +1,39 @@
+using System.IO;
+using System.Web.Razor.Text;
+using System.Web.Razor.Tokenizer;
+using System.Web.Razor.Tokenizer.Symbols;
+using Xunit;
+
+namespace System.Web.Razor.Test.Tokenizer
+{
+ public class TokenizerLookaheadTest : HtmlTokenizerTestBase
+ {
+ [Fact]
+ public void After_Cancelling_Lookahead_Tokenizer_Returns_Same_Tokens_As_It_Did_Before_Lookahead()
+ {
+ HtmlTokenizer tokenizer = new HtmlTokenizer(new SeekableTextReader(new StringReader("<foo>")));
+ using (tokenizer.Source.BeginLookahead())
+ {
+ Assert.Equal(new HtmlSymbol(0, 0, 0, "<", HtmlSymbolType.OpenAngle), tokenizer.NextSymbol());
+ Assert.Equal(new HtmlSymbol(1, 0, 1, "foo", HtmlSymbolType.Text), tokenizer.NextSymbol());
+ Assert.Equal(new HtmlSymbol(4, 0, 4, ">", HtmlSymbolType.CloseAngle), tokenizer.NextSymbol());
+ }
+ Assert.Equal(new HtmlSymbol(0, 0, 0, "<", HtmlSymbolType.OpenAngle), tokenizer.NextSymbol());
+ Assert.Equal(new HtmlSymbol(1, 0, 1, "foo", HtmlSymbolType.Text), tokenizer.NextSymbol());
+ Assert.Equal(new HtmlSymbol(4, 0, 4, ">", HtmlSymbolType.CloseAngle), tokenizer.NextSymbol());
+ }
+
+ [Fact]
+ public void After_Accepting_Lookahead_Tokenizer_Returns_Next_Token()
+ {
+ HtmlTokenizer tokenizer = new HtmlTokenizer(new SeekableTextReader(new StringReader("<foo>")));
+ using (LookaheadToken lookahead = tokenizer.Source.BeginLookahead())
+ {
+ Assert.Equal(new HtmlSymbol(0, 0, 0, "<", HtmlSymbolType.OpenAngle), tokenizer.NextSymbol());
+ Assert.Equal(new HtmlSymbol(1, 0, 1, "foo", HtmlSymbolType.Text), tokenizer.NextSymbol());
+ lookahead.Accept();
+ }
+ Assert.Equal(new HtmlSymbol(4, 0, 4, ">", HtmlSymbolType.CloseAngle), tokenizer.NextSymbol());
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Tokenizer/TokenizerTestBase.cs b/test/System.Web.Razor.Test/Tokenizer/TokenizerTestBase.cs
new file mode 100644
index 00000000..5b5e6612
--- /dev/null
+++ b/test/System.Web.Razor.Test/Tokenizer/TokenizerTestBase.cs
@@ -0,0 +1,74 @@
+using System.Diagnostics;
+using System.IO;
+using System.Text;
+using System.Web.Razor.Text;
+using System.Web.Razor.Tokenizer;
+using System.Web.Razor.Tokenizer.Symbols;
+using Xunit;
+
+namespace System.Web.Razor.Test.Tokenizer
+{
+ public abstract class TokenizerTestBase<TSymbol, TSymbolType>
+ where TSymbol : SymbolBase<TSymbolType>
+ {
+ protected abstract TSymbol IgnoreRemaining { get; }
+ protected abstract Tokenizer<TSymbol, TSymbolType> CreateTokenizer(ITextDocument source);
+
+ protected void TestTokenizer(string input, params TSymbol[] expectedSymbols)
+ {
+ // Arrange
+ bool success = true;
+ StringBuilder output = new StringBuilder();
+ using (StringReader reader = new StringReader(input))
+ {
+ using (SeekableTextReader source = new SeekableTextReader(reader))
+ {
+ Tokenizer<TSymbol, TSymbolType> tokenizer = CreateTokenizer(source);
+ int counter = 0;
+ TSymbol current = null;
+ while ((current = tokenizer.NextSymbol()) != null)
+ {
+ if (counter >= expectedSymbols.Length)
+ {
+ output.AppendLine(String.Format("F: Expected: << Nothing >>; Actual: {0}", current));
+ success = false;
+ }
+ else if (ReferenceEquals(expectedSymbols[counter], IgnoreRemaining))
+ {
+ output.AppendLine(String.Format("P: Ignored {0}", current));
+ }
+ else
+ {
+ if (!Equals(expectedSymbols[counter], current))
+ {
+ output.AppendLine(String.Format("F: Expected: {0}; Actual: {1}", expectedSymbols[counter], current));
+ success = false;
+ }
+ else
+ {
+ output.AppendLine(String.Format("P: Expected: {0}", expectedSymbols[counter]));
+ }
+ counter++;
+ }
+ }
+ if (counter < expectedSymbols.Length && !ReferenceEquals(expectedSymbols[counter], IgnoreRemaining))
+ {
+ success = false;
+ for (; counter < expectedSymbols.Length; counter++)
+ {
+ output.AppendLine(String.Format("F: Expected: {0}; Actual: << None >>", expectedSymbols[counter]));
+ }
+ }
+ }
+ }
+ Assert.True(success, "\r\n" + output.ToString());
+ WriteTraceLine(output.Replace("{", "{{").Replace("}", "}}").ToString());
+ }
+
+ [Conditional("PARSER_TRACE")]
+ private static void WriteTraceLine(string format, params object[] args)
+ {
+ Trace.WriteLine(String.Format(format, args));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Tokenizer/VBTokenizerCommentTest.cs b/test/System.Web.Razor.Test/Tokenizer/VBTokenizerCommentTest.cs
new file mode 100644
index 00000000..cc32eb33
--- /dev/null
+++ b/test/System.Web.Razor.Test/Tokenizer/VBTokenizerCommentTest.cs
@@ -0,0 +1,91 @@
+using System.Web.Razor.Tokenizer.Symbols;
+using Xunit;
+
+namespace System.Web.Razor.Test.Tokenizer
+{
+ public class VBTokenizerCommentTest : VBTokenizerTestBase
+ {
+ [Fact]
+ public void Next_Ignores_Star_At_EOF_In_RazorComment()
+ {
+ TestTokenizer("@* Foo * Bar * Baz *",
+ new VBSymbol(0, 0, 0, "@", VBSymbolType.RazorCommentTransition),
+ new VBSymbol(1, 0, 1, "*", VBSymbolType.RazorCommentStar),
+ new VBSymbol(2, 0, 2, " Foo * Bar * Baz *", VBSymbolType.RazorComment));
+ }
+
+ [Fact]
+ public void Next_Ignores_Star_Without_Trailing_At()
+ {
+ TestTokenizer("@* Foo * Bar * Baz *@",
+ new VBSymbol(0, 0, 0, "@", VBSymbolType.RazorCommentTransition),
+ new VBSymbol(1, 0, 1, "*", VBSymbolType.RazorCommentStar),
+ new VBSymbol(2, 0, 2, " Foo * Bar * Baz ", VBSymbolType.RazorComment),
+ new VBSymbol(19, 0, 19, "*", VBSymbolType.RazorCommentStar),
+ new VBSymbol(20, 0, 20, "@", VBSymbolType.RazorCommentTransition));
+ }
+
+ [Fact]
+ public void Next_Returns_RazorComment_Token_For_Entire_Razor_Comment()
+ {
+ TestTokenizer("@* Foo Bar Baz *@",
+ new VBSymbol(0, 0, 0, "@", VBSymbolType.RazorCommentTransition),
+ new VBSymbol(1, 0, 1, "*", VBSymbolType.RazorCommentStar),
+ new VBSymbol(2, 0, 2, " Foo Bar Baz ", VBSymbolType.RazorComment),
+ new VBSymbol(15, 0, 15, "*", VBSymbolType.RazorCommentStar),
+ new VBSymbol(16, 0, 16, "@", VBSymbolType.RazorCommentTransition));
+ }
+
+ [Fact]
+ public void Tick_Comment_Is_Recognized()
+ {
+ TestTokenizer("' Foo Bar Baz", new VBSymbol(0, 0, 0, "' Foo Bar Baz", VBSymbolType.Comment));
+ }
+
+ [Fact]
+ public void Tick_Comment_Is_Terminated_By_Newline()
+ {
+ TestTokenizer("' Foo Bar Baz\na", new VBSymbol(0, 0, 0, "' Foo Bar Baz", VBSymbolType.Comment), IgnoreRemaining);
+ }
+
+ [Fact]
+ public void LeftQuote_Comment_Is_Recognized()
+ {
+ // U+2018 - Left Quote: ‘
+ TestTokenizer("‘ Foo Bar Baz", new VBSymbol(0, 0, 0, "‘ Foo Bar Baz", VBSymbolType.Comment));
+ }
+
+ [Fact]
+ public void LeftQuote_Comment_Is_Terminated_By_Newline()
+ {
+ // U+2018 - Left Quote: ‘
+ TestTokenizer("‘ Foo Bar Baz\na", new VBSymbol(0, 0, 0, "‘ Foo Bar Baz", VBSymbolType.Comment), IgnoreRemaining);
+ }
+
+ [Fact]
+ public void RightQuote_Comment_Is_Recognized()
+ {
+ // U+2019 - Right Quote: ’
+ TestTokenizer("’ Foo Bar Baz", new VBSymbol(0, 0, 0, "’ Foo Bar Baz", VBSymbolType.Comment));
+ }
+
+ [Fact]
+ public void RightQuote_Comment_Is_Terminated_By_Newline()
+ {
+ // U+2019 - Right Quote: ’
+ TestTokenizer("’ Foo Bar Baz\na", new VBSymbol(0, 0, 0, "’ Foo Bar Baz", VBSymbolType.Comment), IgnoreRemaining);
+ }
+
+ [Fact]
+ public void Rem_Comment_Is_Recognized()
+ {
+ TestTokenizer("REM Foo Bar Baz", new VBSymbol(0, 0, 0, "REM Foo Bar Baz", VBSymbolType.Comment));
+ }
+
+ [Fact]
+ public void Rem_Comment_Is_Terminated_By_Newline()
+ {
+ TestTokenizer("REM Foo Bar Baz\na", new VBSymbol(0, 0, 0, "REM Foo Bar Baz", VBSymbolType.Comment), IgnoreRemaining);
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Tokenizer/VBTokenizerIdentifierTest.cs b/test/System.Web.Razor.Test/Tokenizer/VBTokenizerIdentifierTest.cs
new file mode 100644
index 00000000..a4c5a3e7
--- /dev/null
+++ b/test/System.Web.Razor.Test/Tokenizer/VBTokenizerIdentifierTest.cs
@@ -0,0 +1,265 @@
+using System.Web.Razor.Tokenizer.Symbols;
+using Xunit;
+
+namespace System.Web.Razor.Test.Tokenizer
+{
+ public class VBTokenizerIdentifierTest : VBTokenizerTestBase
+ {
+ [Fact]
+ public void Simple_Identifier_Is_Recognized()
+ {
+ TestTokenizer("foo", new VBSymbol(0, 0, 0, "foo", VBSymbolType.Identifier));
+ }
+
+ [Fact]
+ public void Escaped_Identifier_Terminates_At_EOF()
+ {
+ TestTokenizer("[foo", new VBSymbol(0, 0, 0, "[foo", VBSymbolType.Identifier));
+ }
+
+ [Fact]
+ public void Escaped_Identifier_Terminates_At_Whitespace()
+ {
+ TestTokenizer("[foo ", new VBSymbol(0, 0, 0, "[foo", VBSymbolType.Identifier), IgnoreRemaining);
+ }
+
+ [Fact]
+ public void Escaped_Identifier_Terminates_At_RightBracket_And_Does_Not_Read_TypeCharacter()
+ {
+ TestTokenizer("[foo]&", new VBSymbol(0, 0, 0, "[foo]", VBSymbolType.Identifier), IgnoreRemaining);
+ }
+
+ [Fact]
+ public void Identifier_Starting_With_Underscore_Is_Recognized()
+ {
+ TestTokenizer("_foo", new VBSymbol(0, 0, 0, "_foo", VBSymbolType.Identifier));
+ }
+
+ [Fact]
+ public void Identifier_Can_Contain_Digits()
+ {
+ TestTokenizer("foo4", new VBSymbol(0, 0, 0, "foo4", VBSymbolType.Identifier));
+ }
+
+ [Fact]
+ public void Identifier_Can_Start_With_Titlecase_Letter()
+ {
+ TestTokenizer("ῼfoo", new VBSymbol(0, 0, 0, "ῼfoo", VBSymbolType.Identifier));
+ }
+
+ [Fact]
+ public void Identifier_Can_Start_With_Letter_Modifier()
+ {
+ TestTokenizer("ᵊfoo", new VBSymbol(0, 0, 0, "ᵊfoo", VBSymbolType.Identifier));
+ }
+
+ [Fact]
+ public void Identifier_Can_Start_With_Other_Letter()
+ {
+ TestTokenizer("ƻfoo", new VBSymbol(0, 0, 0, "ƻfoo", VBSymbolType.Identifier));
+ }
+
+ [Fact]
+ public void Identifier_Can_Start_With_Number_Letter()
+ {
+ TestTokenizer("Ⅽool", new VBSymbol(0, 0, 0, "Ⅽool", VBSymbolType.Identifier));
+ }
+
+ [Fact]
+ public void Identifier_Can_Contain_Non_Spacing_Mark()
+ {
+ TestTokenizer("foo\u0300", new VBSymbol(0, 0, 0, "foo\u0300", VBSymbolType.Identifier));
+ }
+
+ [Fact]
+ public void Identifier_Can_Contain_Spacing_Combining_Mark()
+ {
+ TestTokenizer("fooः", new VBSymbol(0, 0, 0, "fooः", VBSymbolType.Identifier));
+ }
+
+ [Fact]
+ public void Identifier_Can_Contain_Non_English_Digit()
+ {
+ TestTokenizer("foo١", new VBSymbol(0, 0, 0, "foo١", VBSymbolType.Identifier));
+ }
+
+ [Fact]
+ public void Identifier_Can_Contain_Connector_Punctuation()
+ {
+ TestTokenizer("foo‿bar", new VBSymbol(0, 0, 0, "foo‿bar", VBSymbolType.Identifier));
+ }
+
+ [Fact]
+ public void Identifier_Can_Contain_Format_Character()
+ {
+ TestTokenizer("foo؃bar", new VBSymbol(0, 0, 0, "foo؃bar", VBSymbolType.Identifier));
+ }
+
+ [Fact]
+ public void Escaped_Keyword_Is_Recognized_As_Identifier()
+ {
+ TestSingleToken("[AddHandler]", VBSymbolType.Identifier);
+ }
+
+ [Fact]
+ public void Keywords_Are_Recognized_As_Keyword_Tokens()
+ {
+ TestKeyword("AddHandler", VBKeyword.AddHandler);
+ TestKeyword("AndAlso", VBKeyword.AndAlso);
+ TestKeyword("Byte", VBKeyword.Byte);
+ TestKeyword("Catch", VBKeyword.Catch);
+ TestKeyword("CDate", VBKeyword.CDate);
+ TestKeyword("CInt", VBKeyword.CInt);
+ TestKeyword("Const", VBKeyword.Const);
+ TestKeyword("CSng", VBKeyword.CSng);
+ TestKeyword("CULng", VBKeyword.CULng);
+ TestKeyword("Declare", VBKeyword.Declare);
+ TestKeyword("DirectCast", VBKeyword.DirectCast);
+ TestKeyword("Else", VBKeyword.Else);
+ TestKeyword("Enum", VBKeyword.Enum);
+ TestKeyword("Exit", VBKeyword.Exit);
+ TestKeyword("Friend", VBKeyword.Friend);
+ TestKeyword("GetXmlNamespace", VBKeyword.GetXmlNamespace);
+ TestKeyword("Handles", VBKeyword.Handles);
+ TestKeyword("In", VBKeyword.In);
+ TestKeyword("Is", VBKeyword.Is);
+ TestKeyword("Like", VBKeyword.Like);
+ TestKeyword("Mod", VBKeyword.Mod);
+ TestKeyword("MyBase", VBKeyword.MyBase);
+ TestKeyword("New", VBKeyword.New);
+ TestKeyword("AddressOf", VBKeyword.AddressOf);
+ TestKeyword("As", VBKeyword.As);
+ TestKeyword("ByVal", VBKeyword.ByVal);
+ TestKeyword("CBool", VBKeyword.CBool);
+ TestKeyword("CDbl", VBKeyword.CDbl);
+ TestKeyword("Class", VBKeyword.Class);
+ TestKeyword("Continue", VBKeyword.Continue);
+ TestKeyword("CStr", VBKeyword.CStr);
+ TestKeyword("CUShort", VBKeyword.CUShort);
+ TestKeyword("Default", VBKeyword.Default);
+ TestKeyword("Do", VBKeyword.Do);
+ TestKeyword("ElseIf", VBKeyword.ElseIf);
+ TestKeyword("Erase", VBKeyword.Erase);
+ TestKeyword("False", VBKeyword.False);
+ TestKeyword("Function", VBKeyword.Function);
+ TestKeyword("Global", VBKeyword.Global);
+ TestKeyword("If", VBKeyword.If);
+ TestKeyword("Inherits", VBKeyword.Inherits);
+ TestKeyword("IsNot", VBKeyword.IsNot);
+ TestKeyword("Long", VBKeyword.Long);
+ TestKeyword("Module", VBKeyword.Module);
+ TestKeyword("MyClass", VBKeyword.MyClass);
+ TestKeyword("Next", VBKeyword.Next);
+ TestKeyword("Alias", VBKeyword.Alias);
+ TestKeyword("Boolean", VBKeyword.Boolean);
+ TestKeyword("Call", VBKeyword.Call);
+ TestKeyword("CByte", VBKeyword.CByte);
+ TestKeyword("CDec", VBKeyword.CDec);
+ TestKeyword("CLng", VBKeyword.CLng);
+ TestKeyword("CSByte", VBKeyword.CSByte);
+ TestKeyword("CType", VBKeyword.CType);
+ TestKeyword("Date", VBKeyword.Date);
+ TestKeyword("Delegate", VBKeyword.Delegate);
+ TestKeyword("Double", VBKeyword.Double);
+ TestKeyword("End", VBKeyword.End);
+ TestKeyword("Error", VBKeyword.Error);
+ TestKeyword("Finally", VBKeyword.Finally);
+ TestKeyword("Get", VBKeyword.Get);
+ TestKeyword("GoSub", VBKeyword.GoSub);
+ TestKeyword("Implements", VBKeyword.Implements);
+ TestKeyword("Integer", VBKeyword.Integer);
+ TestKeyword("Let", VBKeyword.Let);
+ TestKeyword("Loop", VBKeyword.Loop);
+ TestKeyword("MustInherit", VBKeyword.MustInherit);
+ TestKeyword("Namespace", VBKeyword.Namespace);
+ TestKeyword("Not", VBKeyword.Not);
+ TestKeyword("And", VBKeyword.And);
+ TestKeyword("ByRef", VBKeyword.ByRef);
+ TestKeyword("Case", VBKeyword.Case);
+ TestKeyword("CChar", VBKeyword.CChar);
+ TestKeyword("Char", VBKeyword.Char);
+ TestKeyword("CObj", VBKeyword.CObj);
+ TestKeyword("CShort", VBKeyword.CShort);
+ TestKeyword("CUInt", VBKeyword.CUInt);
+ TestKeyword("Decimal", VBKeyword.Decimal);
+ TestKeyword("Dim", VBKeyword.Dim);
+ TestKeyword("Each", VBKeyword.Each);
+ TestKeyword("EndIf", VBKeyword.EndIf);
+ TestKeyword("Event", VBKeyword.Event);
+ TestKeyword("For", VBKeyword.For);
+ TestKeyword("GetType", VBKeyword.GetType);
+ TestKeyword("GoTo", VBKeyword.GoTo);
+ TestKeyword("Imports", VBKeyword.Imports);
+ TestKeyword("Interface", VBKeyword.Interface);
+ TestKeyword("Lib", VBKeyword.Lib);
+ TestKeyword("Me", VBKeyword.Me);
+ TestKeyword("MustOverride", VBKeyword.MustOverride);
+ TestKeyword("Narrowing", VBKeyword.Narrowing);
+ TestKeyword("Nothing", VBKeyword.Nothing);
+ TestKeyword("NotInheritable", VBKeyword.NotInheritable);
+ TestKeyword("On", VBKeyword.On);
+ TestKeyword("Or", VBKeyword.Or);
+ TestKeyword("Overrides", VBKeyword.Overrides);
+ TestKeyword("Property", VBKeyword.Property);
+ TestKeyword("ReadOnly", VBKeyword.ReadOnly);
+ TestKeyword("Resume", VBKeyword.Resume);
+ TestKeyword("Set", VBKeyword.Set);
+ TestKeyword("Single", VBKeyword.Single);
+ TestKeyword("String", VBKeyword.String);
+ TestKeyword("Then", VBKeyword.Then);
+ TestKeyword("Try", VBKeyword.Try);
+ TestKeyword("ULong", VBKeyword.ULong);
+ TestKeyword("Wend", VBKeyword.Wend);
+ TestKeyword("With", VBKeyword.With);
+ TestKeyword("NotOverridable", VBKeyword.NotOverridable);
+ TestKeyword("Operator", VBKeyword.Operator);
+ TestKeyword("OrElse", VBKeyword.OrElse);
+ TestKeyword("ParamArray", VBKeyword.ParamArray);
+ TestKeyword("Protected", VBKeyword.Protected);
+ TestKeyword("ReDim", VBKeyword.ReDim);
+ TestKeyword("Return", VBKeyword.Return);
+ TestKeyword("Shadows", VBKeyword.Shadows);
+ TestKeyword("Static", VBKeyword.Static);
+ TestKeyword("Structure", VBKeyword.Structure);
+ TestKeyword("Throw", VBKeyword.Throw);
+ TestKeyword("TryCast", VBKeyword.TryCast);
+ TestKeyword("UShort", VBKeyword.UShort);
+ TestKeyword("When", VBKeyword.When);
+ TestKeyword("WithEvents", VBKeyword.WithEvents);
+ TestKeyword("Object", VBKeyword.Object);
+ TestKeyword("Option", VBKeyword.Option);
+ TestKeyword("Overloads", VBKeyword.Overloads);
+ TestKeyword("Partial", VBKeyword.Partial);
+ TestKeyword("Public", VBKeyword.Public);
+ TestKeyword("SByte", VBKeyword.SByte);
+ TestKeyword("Shared", VBKeyword.Shared);
+ TestKeyword("Step", VBKeyword.Step);
+ TestKeyword("Sub", VBKeyword.Sub);
+ TestKeyword("To", VBKeyword.To);
+ TestKeyword("TypeOf", VBKeyword.TypeOf);
+ TestKeyword("Using", VBKeyword.Using);
+ TestKeyword("While", VBKeyword.While);
+ TestKeyword("WriteOnly", VBKeyword.WriteOnly);
+ TestKeyword("Of", VBKeyword.Of);
+ TestKeyword("Optional", VBKeyword.Optional);
+ TestKeyword("Overridable", VBKeyword.Overridable);
+ TestKeyword("Private", VBKeyword.Private);
+ TestKeyword("RaiseEvent", VBKeyword.RaiseEvent);
+ TestKeyword("RemoveHandler", VBKeyword.RemoveHandler);
+ TestKeyword("Select", VBKeyword.Select);
+ TestKeyword("Short", VBKeyword.Short);
+ TestKeyword("Stop", VBKeyword.Stop);
+ TestKeyword("SyncLock", VBKeyword.SyncLock);
+ TestKeyword("True", VBKeyword.True);
+ TestKeyword("UInteger", VBKeyword.UInteger);
+ TestKeyword("Variant", VBKeyword.Variant);
+ TestKeyword("Widening", VBKeyword.Widening);
+ TestKeyword("Xor", VBKeyword.Xor);
+ }
+
+ private void TestKeyword(string keyword, VBKeyword keywordType)
+ {
+ TestTokenizer(keyword, new VBSymbol(0, 0, 0, keyword, VBSymbolType.Keyword) { Keyword = keywordType });
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Tokenizer/VBTokenizerLiteralTest.cs b/test/System.Web.Razor.Test/Tokenizer/VBTokenizerLiteralTest.cs
new file mode 100644
index 00000000..e6844e42
--- /dev/null
+++ b/test/System.Web.Razor.Test/Tokenizer/VBTokenizerLiteralTest.cs
@@ -0,0 +1,180 @@
+using System.Web.Razor.Tokenizer.Symbols;
+using Xunit;
+
+namespace System.Web.Razor.Test.Tokenizer
+{
+ public class VBTokenizerLiteralTest : VBTokenizerTestBase
+ {
+ [Fact]
+ public void Decimal_Integer_Literal_Is_Recognized()
+ {
+ TestSingleToken("01189998819991197253", VBSymbolType.IntegerLiteral);
+ }
+
+ [Fact]
+ public void Integer_Type_Suffixes_Are_Recognized_In_Decimal_Literal()
+ {
+ TestSingleToken("42S", VBSymbolType.IntegerLiteral);
+ TestSingleToken("42I", VBSymbolType.IntegerLiteral);
+ TestSingleToken("42L", VBSymbolType.IntegerLiteral);
+ TestSingleToken("42US", VBSymbolType.IntegerLiteral);
+ TestSingleToken("42UI", VBSymbolType.IntegerLiteral);
+ TestSingleToken("42UL", VBSymbolType.IntegerLiteral);
+ }
+
+ [Fact]
+ public void Hex_Integer_Literal_Is_Recognized()
+ {
+ TestSingleToken("&HDeadBeef", VBSymbolType.IntegerLiteral);
+ }
+
+ [Fact]
+ public void Integer_Type_Suffixes_Are_Recognized_In_Hex_Literal()
+ {
+ TestSingleToken("&HDeadBeefS", VBSymbolType.IntegerLiteral);
+ TestSingleToken("&HDeadBeefI", VBSymbolType.IntegerLiteral);
+ TestSingleToken("&HDeadBeefL", VBSymbolType.IntegerLiteral);
+ TestSingleToken("&HDeadBeefUS", VBSymbolType.IntegerLiteral);
+ TestSingleToken("&HDeadBeefUI", VBSymbolType.IntegerLiteral);
+ TestSingleToken("&HDeadBeefUL", VBSymbolType.IntegerLiteral);
+ }
+
+ [Fact]
+ public void Octal_Integer_Literal_Is_Recognized()
+ {
+ TestSingleToken("&O77", VBSymbolType.IntegerLiteral);
+ }
+
+ [Fact]
+ public void Integer_Type_Suffixes_Are_Recognized_In_Octal_Literal()
+ {
+ TestSingleToken("&O77S", VBSymbolType.IntegerLiteral);
+ TestSingleToken("&O77I", VBSymbolType.IntegerLiteral);
+ TestSingleToken("&O77L", VBSymbolType.IntegerLiteral);
+ TestSingleToken("&O77US", VBSymbolType.IntegerLiteral);
+ TestSingleToken("&O77UI", VBSymbolType.IntegerLiteral);
+ TestSingleToken("&O77UL", VBSymbolType.IntegerLiteral);
+ }
+
+ [Fact]
+ public void Incomplete_Type_Suffix_Is_Recognized()
+ {
+ TestSingleToken("42U", VBSymbolType.IntegerLiteral);
+ TestSingleToken("&H42U", VBSymbolType.IntegerLiteral);
+ TestSingleToken("&O77U", VBSymbolType.IntegerLiteral);
+ }
+
+ [Fact]
+ public void Integer_With_FloatingPoint_Type_Suffix_Is_Recognized_As_FloatingPointLiteral()
+ {
+ TestSingleToken("42F", VBSymbolType.FloatingPointLiteral);
+ TestSingleToken("42R", VBSymbolType.FloatingPointLiteral);
+ TestSingleToken("42D", VBSymbolType.FloatingPointLiteral);
+ }
+
+ [Fact]
+ public void Simple_FloatingPoint_Is_Recognized()
+ {
+ TestSingleToken("3.14159", VBSymbolType.FloatingPointLiteral);
+ }
+
+ [Fact]
+ public void Integer_With_Exponent_Is_Recognized()
+ {
+ TestSingleToken("1E10", VBSymbolType.FloatingPointLiteral);
+ TestSingleToken("1e10", VBSymbolType.FloatingPointLiteral);
+ TestSingleToken("1E+10", VBSymbolType.FloatingPointLiteral);
+ TestSingleToken("1e+10", VBSymbolType.FloatingPointLiteral);
+ TestSingleToken("1E-10", VBSymbolType.FloatingPointLiteral);
+ TestSingleToken("1e-10", VBSymbolType.FloatingPointLiteral);
+ }
+
+ [Fact]
+ public void Simple_FloatingPoint_With_Exponent_Is_Recognized()
+ {
+ TestSingleToken("3.14159e10", VBSymbolType.FloatingPointLiteral);
+ }
+
+ [Fact]
+ public void FloatingPoint_Between_Zero_And_One_Is_Recognized()
+ {
+ TestSingleToken(".314159e1", VBSymbolType.FloatingPointLiteral);
+ }
+
+ [Fact]
+ public void Simple_String_Literal_Is_Recognized()
+ {
+ TestSingleToken("\"Foo Bar Baz\"", VBSymbolType.StringLiteral);
+ }
+
+ [Fact]
+ public void Two_Double_Quotes_Are_Recognized_As_Escape_Sequence()
+ {
+ TestSingleToken("\"Foo \"\"Bar\"\" Baz\"", VBSymbolType.StringLiteral);
+ }
+
+ [Fact]
+ public void String_Literal_Is_Terminated_At_EOF()
+ {
+ TestSingleToken("\"Foo", VBSymbolType.StringLiteral);
+ }
+
+ [Fact]
+ public void String_Literal_Is_Terminated_At_EOL()
+ {
+ TestTokenizer("\"Foo\nBar", new VBSymbol(0, 0, 0, "\"Foo", VBSymbolType.StringLiteral), IgnoreRemaining);
+ }
+
+ [Fact]
+ public void Character_Literal_Is_Recognized_By_Trailing_C_After_String_Literal()
+ {
+ TestSingleToken("\"abc\"c", VBSymbolType.CharacterLiteral);
+ }
+
+ [Fact]
+ public void LeftDoubleQuote_Is_Valid_DoubleQuote()
+ {
+ // Repeat all the above tests with Unicode Left Double Quote Character U+201C: “
+ TestSingleToken("“Foo Bar Baz“", VBSymbolType.StringLiteral);
+ TestSingleToken("“Foo ““Bar““ Baz“", VBSymbolType.StringLiteral);
+ TestSingleToken("“Foo", VBSymbolType.StringLiteral);
+ TestSingleToken("“abc“c", VBSymbolType.CharacterLiteral);
+ TestTokenizer("“Foo\nBar", new VBSymbol(0, 0, 0, "“Foo", VBSymbolType.StringLiteral), IgnoreRemaining);
+ }
+
+ [Fact]
+ public void RightDoubleQuote_Is_Valid_DoubleQuote()
+ {
+ // Repeat all the above tests with Unicode Right Double Quote Character U+201D: ”
+ TestSingleToken("”Foo Bar Baz”", VBSymbolType.StringLiteral);
+ TestSingleToken("”Foo ””Bar”” Baz”", VBSymbolType.StringLiteral);
+ TestSingleToken("”Foo", VBSymbolType.StringLiteral);
+ TestSingleToken("”abc”c", VBSymbolType.CharacterLiteral);
+ TestTokenizer("”Foo\nBar", new VBSymbol(0, 0, 0, "”Foo", VBSymbolType.StringLiteral), IgnoreRemaining);
+ }
+
+ [Fact]
+ public void DateLiteral_Is_Recognized()
+ {
+ TestSingleToken("# 8/23/1970 3:45:39AM #", VBSymbolType.DateLiteral);
+ }
+
+ [Fact]
+ public void DateLiteral_Is_Terminated_At_EndHash()
+ {
+ TestTokenizer("# 8/23/1970 # 3:45:39AM", new VBSymbol(0, 0, 0, "# 8/23/1970 #", VBSymbolType.DateLiteral), IgnoreRemaining);
+ }
+
+ [Fact]
+ public void DateLiteral_Is_Terminated_At_EOF()
+ {
+ TestSingleToken("# 8/23/1970 3:45:39AM", VBSymbolType.DateLiteral);
+ }
+
+ [Fact]
+ public void DateLiteral_Is_Terminated_At_EOL()
+ {
+ TestTokenizer("# 8/23/1970\n3:45:39AM", new VBSymbol(0, 0, 0, "# 8/23/1970", VBSymbolType.DateLiteral), IgnoreRemaining);
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Tokenizer/VBTokenizerOperatorsTest.cs b/test/System.Web.Razor.Test/Tokenizer/VBTokenizerOperatorsTest.cs
new file mode 100644
index 00000000..47eab61c
--- /dev/null
+++ b/test/System.Web.Razor.Test/Tokenizer/VBTokenizerOperatorsTest.cs
@@ -0,0 +1,152 @@
+using System.Web.Razor.Tokenizer.Symbols;
+using Xunit;
+
+namespace System.Web.Razor.Test.Tokenizer
+{
+ public class VBTokenizerOperatorsTest : VBTokenizerTestBase
+ {
+ [Fact]
+ public void Line_Continuation_Character_Is_Recognized()
+ {
+ TestSingleToken("_", VBSymbolType.LineContinuation);
+ }
+
+ [Fact]
+ public void LeftParen_Is_Recognized()
+ {
+ TestSingleToken("(", VBSymbolType.LeftParenthesis);
+ }
+
+ [Fact]
+ public void RightParen_Is_Recognized()
+ {
+ TestSingleToken(")", VBSymbolType.RightParenthesis);
+ }
+
+ [Fact]
+ public void LeftBracket_Is_Recognized()
+ {
+ TestSingleToken("[", VBSymbolType.LeftBracket);
+ }
+
+ [Fact]
+ public void RightBracket_Is_Recognized()
+ {
+ TestSingleToken("]", VBSymbolType.RightBracket);
+ }
+
+ [Fact]
+ public void LeftBrace_Is_Recognized()
+ {
+ TestSingleToken("{", VBSymbolType.LeftBrace);
+ }
+
+ [Fact]
+ public void RightBrace_Is_Recognized()
+ {
+ TestSingleToken("}", VBSymbolType.RightBrace);
+ }
+
+ [Fact]
+ public void Bang_Is_Recognized()
+ {
+ TestSingleToken("!", VBSymbolType.Bang);
+ }
+
+ [Fact]
+ public void Hash_Is_Recognized()
+ {
+ TestSingleToken("#", VBSymbolType.Hash);
+ }
+
+ [Fact]
+ public void Comma_Is_Recognized()
+ {
+ TestSingleToken(",", VBSymbolType.Comma);
+ }
+
+ [Fact]
+ public void Dot_Is_Recognized()
+ {
+ TestSingleToken(".", VBSymbolType.Dot);
+ }
+
+ [Fact]
+ public void Colon_Is_Recognized()
+ {
+ TestSingleToken(":", VBSymbolType.Colon);
+ }
+
+ [Fact]
+ public void QuestionMark_Is_Recognized()
+ {
+ TestSingleToken("?", VBSymbolType.QuestionMark);
+ }
+
+ [Fact]
+ public void Concatenation_Is_Recognized()
+ {
+ TestSingleToken("&", VBSymbolType.Concatenation);
+ }
+
+ [Fact]
+ public void Multiply_Is_Recognized()
+ {
+ TestSingleToken("*", VBSymbolType.Multiply);
+ }
+
+ [Fact]
+ public void Add_Is_Recognized()
+ {
+ TestSingleToken("+", VBSymbolType.Add);
+ }
+
+ [Fact]
+ public void Subtract_Is_Recognized()
+ {
+ TestSingleToken("-", VBSymbolType.Subtract);
+ }
+
+ [Fact]
+ public void Divide_Is_Recognized()
+ {
+ TestSingleToken("/", VBSymbolType.Divide);
+ }
+
+ [Fact]
+ public void IntegerDivide_Is_Recognized()
+ {
+ TestSingleToken("\\", VBSymbolType.IntegerDivide);
+ }
+
+ [Fact]
+ public void Exponentiation_Is_Recognized()
+ {
+ TestSingleToken("^", VBSymbolType.Exponentiation);
+ }
+
+ [Fact]
+ public void Equal_Is_Recognized()
+ {
+ TestSingleToken("=", VBSymbolType.Equal);
+ }
+
+ [Fact]
+ public void LessThan_Is_Recognized()
+ {
+ TestSingleToken("<", VBSymbolType.LessThan);
+ }
+
+ [Fact]
+ public void GreaterThan_Is_Recognized()
+ {
+ TestSingleToken(">", VBSymbolType.GreaterThan);
+ }
+
+ [Fact]
+ public void Dollar_Is_Recognized()
+ {
+ TestSingleToken("$", VBSymbolType.Dollar);
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Tokenizer/VBTokenizerTest.cs b/test/System.Web.Razor.Test/Tokenizer/VBTokenizerTest.cs
new file mode 100644
index 00000000..01d8f5ca
--- /dev/null
+++ b/test/System.Web.Razor.Test/Tokenizer/VBTokenizerTest.cs
@@ -0,0 +1,94 @@
+using System.Web.Razor.Tokenizer;
+using System.Web.Razor.Tokenizer.Symbols;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Razor.Test.Tokenizer
+{
+ public class VBTokenizerTest : VBTokenizerTestBase
+ {
+ [Fact]
+ public void Constructor_Throws_ArgNull_If_Null_Source_Provided()
+ {
+ Assert.ThrowsArgumentNull(() => new CSharpTokenizer(null), "source");
+ }
+
+ [Fact]
+ public void Next_Returns_Null_When_EOF_Reached()
+ {
+ TestTokenizer("");
+ }
+
+ [Fact]
+ public void Next_Returns_Newline_Token_For_Single_CR()
+ {
+ TestTokenizer("\r\ra",
+ new VBSymbol(0, 0, 0, "\r", VBSymbolType.NewLine),
+ new VBSymbol(1, 1, 0, "\r", VBSymbolType.NewLine),
+ IgnoreRemaining);
+ }
+
+ [Fact]
+ public void Next_Returns_Newline_Token_For_Single_LF()
+ {
+ TestTokenizer("\n\na",
+ new VBSymbol(0, 0, 0, "\n", VBSymbolType.NewLine),
+ new VBSymbol(1, 1, 0, "\n", VBSymbolType.NewLine),
+ IgnoreRemaining);
+ }
+
+ [Fact]
+ public void Next_Returns_Newline_Token_For_Single_NEL()
+ {
+ // NEL: Unicode "Next Line" U+0085
+ TestTokenizer("\u0085\u0085a",
+ new VBSymbol(0, 0, 0, "\u0085", VBSymbolType.NewLine),
+ new VBSymbol(1, 1, 0, "\u0085", VBSymbolType.NewLine),
+ IgnoreRemaining);
+ }
+
+ [Fact]
+ public void Next_Returns_Newline_Token_For_Single_Line_Separator()
+ {
+ // Unicode "Line Separator" U+2028
+ TestTokenizer("\u2028\u2028a",
+ new VBSymbol(0, 0, 0, "\u2028", VBSymbolType.NewLine),
+ new VBSymbol(1, 1, 0, "\u2028", VBSymbolType.NewLine),
+ IgnoreRemaining);
+ }
+
+ [Fact]
+ public void Next_Returns_Newline_Token_For_Single_Paragraph_Separator()
+ {
+ // Unicode "Paragraph Separator" U+2029
+ TestTokenizer("\u2029\u2029a",
+ new VBSymbol(0, 0, 0, "\u2029", VBSymbolType.NewLine),
+ new VBSymbol(1, 1, 0, "\u2029", VBSymbolType.NewLine),
+ IgnoreRemaining);
+ }
+
+ [Fact]
+ public void Next_Returns_Single_Newline_Token_For_CRLF()
+ {
+ TestTokenizer("\r\n\r\na",
+ new VBSymbol(0, 0, 0, "\r\n", VBSymbolType.NewLine),
+ new VBSymbol(2, 1, 0, "\r\n", VBSymbolType.NewLine),
+ IgnoreRemaining);
+ }
+
+ [Fact]
+ public void Next_Returns_Token_For_Whitespace_Characters()
+ {
+ TestTokenizer(" \f\t\u000B \n ",
+ new VBSymbol(0, 0, 0, " \f\t\u000B ", VBSymbolType.WhiteSpace),
+ new VBSymbol(5, 0, 5, "\n", VBSymbolType.NewLine),
+ new VBSymbol(6, 1, 0, " ", VBSymbolType.WhiteSpace));
+ }
+
+ [Fact]
+ public void Transition_Is_Recognized()
+ {
+ TestSingleToken("@", VBSymbolType.Transition);
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Tokenizer/VBTokenizerTestBase.cs b/test/System.Web.Razor.Test/Tokenizer/VBTokenizerTestBase.cs
new file mode 100644
index 00000000..c6252c04
--- /dev/null
+++ b/test/System.Web.Razor.Test/Tokenizer/VBTokenizerTestBase.cs
@@ -0,0 +1,26 @@
+using System.Web.Razor.Text;
+using System.Web.Razor.Tokenizer;
+using System.Web.Razor.Tokenizer.Symbols;
+
+namespace System.Web.Razor.Test.Tokenizer
+{
+ public abstract class VBTokenizerTestBase : TokenizerTestBase<VBSymbol, VBSymbolType>
+ {
+ private static VBSymbol _ignoreRemaining = new VBSymbol(0, 0, 0, String.Empty, VBSymbolType.Unknown);
+
+ protected override VBSymbol IgnoreRemaining
+ {
+ get { return _ignoreRemaining; }
+ }
+
+ protected override Tokenizer<VBSymbol, VBSymbolType> CreateTokenizer(ITextDocument source)
+ {
+ return new VBTokenizer(source);
+ }
+
+ protected void TestSingleToken(string text, VBSymbolType expectedSymbolType)
+ {
+ TestTokenizer(text, new VBSymbol(0, 0, 0, text, expectedSymbolType));
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Utils/DisposableActionTest.cs b/test/System.Web.Razor.Test/Utils/DisposableActionTest.cs
new file mode 100644
index 00000000..9ddd06ad
--- /dev/null
+++ b/test/System.Web.Razor.Test/Utils/DisposableActionTest.cs
@@ -0,0 +1,29 @@
+using System.Web.Razor.Utils;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Razor.Test.Utils
+{
+ public class DisposableActionTest
+ {
+ [Fact]
+ public void ConstructorRequiresNonNullAction()
+ {
+ Assert.ThrowsArgumentNull(() => new DisposableAction(null), "action");
+ }
+
+ [Fact]
+ public void ActionIsExecutedOnDispose()
+ {
+ // Arrange
+ bool called = false;
+ DisposableAction action = new DisposableAction(() => { called = true; });
+
+ // Act
+ action.Dispose();
+
+ // Assert
+ Assert.True(called, "The action was not run when the DisposableAction was disposed");
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Utils/EnumerableUtils.cs b/test/System.Web.Razor.Test/Utils/EnumerableUtils.cs
new file mode 100644
index 00000000..1fbab3d7
--- /dev/null
+++ b/test/System.Web.Razor.Test/Utils/EnumerableUtils.cs
@@ -0,0 +1,17 @@
+using System.Collections.Generic;
+
+namespace System.Web.Razor.Test.Utils
+{
+ public static class EnumerableUtils
+ {
+ public static void RunPairwise<T, V>(IEnumerable<T> left, IEnumerable<V> right, Action<T, V> action)
+ {
+ IEnumerator<T> leftEnum = left.GetEnumerator();
+ IEnumerator<V> rightEnum = right.GetEnumerator();
+ while (leftEnum.MoveNext() && rightEnum.MoveNext())
+ {
+ action(leftEnum.Current, rightEnum.Current);
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Utils/MiscAssert.cs b/test/System.Web.Razor.Test/Utils/MiscAssert.cs
new file mode 100644
index 00000000..833800b0
--- /dev/null
+++ b/test/System.Web.Razor.Test/Utils/MiscAssert.cs
@@ -0,0 +1,31 @@
+using System.Linq.Expressions;
+using Xunit;
+
+namespace System.Web.Razor.Test.Utils
+{
+ public static class MiscAssert
+ {
+ public static void AssertBothNullOrPropertyEqual<T>(T expected, T actual, Expression<Func<T, object>> propertyExpr, string objectName)
+ {
+ // Unpack convert expressions
+ Expression expr = propertyExpr.Body;
+ while (expr.NodeType == ExpressionType.Convert)
+ {
+ expr = ((UnaryExpression)expr).Operand;
+ }
+
+ string propertyName = ((MemberExpression)expr).Member.Name;
+ Func<T, object> property = propertyExpr.Compile();
+
+ if (expected == null)
+ {
+ Assert.Null(actual);
+ }
+ else
+ {
+ Assert.NotNull(actual);
+ Assert.Equal(property(expected), property(actual));
+ }
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Utils/MiscUtils.cs b/test/System.Web.Razor.Test/Utils/MiscUtils.cs
new file mode 100644
index 00000000..c1e5db39
--- /dev/null
+++ b/test/System.Web.Razor.Test/Utils/MiscUtils.cs
@@ -0,0 +1,33 @@
+using System.Diagnostics;
+using System.Text.RegularExpressions;
+using System.Threading;
+using Xunit;
+
+namespace System.Web.Razor.Test.Utils
+{
+ class MiscUtils
+ {
+ public const int TimeoutInSeconds = 1;
+
+ public static string StripRuntimeVersion(string s)
+ {
+ return Regex.Replace(s, @"Runtime Version:[\d.]*", "Runtime Version:N.N.NNNNN.N");
+ }
+
+ public static void DoWithTimeoutIfNotDebugging(Func<int, bool> withTimeout)
+ {
+#if DEBUG
+ if (Debugger.IsAttached)
+ {
+ withTimeout(Timeout.Infinite);
+ }
+ else
+ {
+#endif
+ Assert.True(withTimeout((int)TimeSpan.FromSeconds(TimeoutInSeconds).TotalMilliseconds), "Timeout expired!");
+#if DEBUG
+ }
+#endif
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/Utils/SpanAssert.cs b/test/System.Web.Razor.Test/Utils/SpanAssert.cs
new file mode 100644
index 00000000..b6a9b39d
--- /dev/null
+++ b/test/System.Web.Razor.Test/Utils/SpanAssert.cs
@@ -0,0 +1,56 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.Text;
+using System.Web.Razor.Parser.SyntaxTree;
+using System.Web.Razor.Text;
+using Xunit;
+
+namespace System.Web.Razor.Test.Utils
+{
+ public static class EventAssert
+ {
+ public static void NoMoreSpans(IEnumerator<Span> enumerator)
+ {
+ IList<Span> tokens = new List<Span>();
+ while (enumerator.MoveNext())
+ {
+ tokens.Add(enumerator.Current);
+ }
+
+ Assert.False(tokens.Count > 0, String.Format(CultureInfo.InvariantCulture, @"There are more tokens available from the source: {0}", FormatList(tokens)));
+ }
+
+ private static string FormatList<T>(IList<T> items)
+ {
+ StringBuilder tokenString = new StringBuilder();
+ foreach (T item in items)
+ {
+ tokenString.AppendLine(item.ToString());
+ }
+ return tokenString.ToString();
+ }
+
+ public static void NextSpanIs(IEnumerator<Span> enumerator, SpanKind type, string content, SourceLocation location)
+ {
+ Assert.True(enumerator.MoveNext(), "There is no next token!");
+ IsSpan(enumerator.Current, type, content, location);
+ }
+
+ public static void NextSpanIs(IEnumerator<Span> enumerator, SpanKind type, string content, int actualIndex, int lineIndex, int charIndex)
+ {
+ NextSpanIs(enumerator, type, content, new SourceLocation(actualIndex, lineIndex, charIndex));
+ }
+
+ public static void IsSpan(Span tok, SpanKind type, string content, int actualIndex, int lineIndex, int charIndex)
+ {
+ IsSpan(tok, type, content, new SourceLocation(actualIndex, lineIndex, charIndex));
+ }
+
+ public static void IsSpan(Span tok, SpanKind type, string content, SourceLocation location)
+ {
+ Assert.Equal(content, tok.Content);
+ Assert.Equal(type, tok.Kind);
+ Assert.Equal(location, tok.Start);
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/VBRazorCodeLanguageTest.cs b/test/System.Web.Razor.Test/VBRazorCodeLanguageTest.cs
new file mode 100644
index 00000000..78e26a83
--- /dev/null
+++ b/test/System.Web.Razor.Test/VBRazorCodeLanguageTest.cs
@@ -0,0 +1,53 @@
+using System.Web.Razor.Generator;
+using System.Web.Razor.Parser;
+using Microsoft.VisualBasic;
+using Xunit;
+
+namespace System.Web.Razor.Test
+{
+ public class VBRazorCodeLanguageTest
+ {
+ [Fact]
+ public void CreateCodeParserReturnsNewVBCodeParser()
+ {
+ // Arrange
+ RazorCodeLanguage service = new VBRazorCodeLanguage();
+
+ // Act
+ ParserBase parser = service.CreateCodeParser();
+
+ // Assert
+ Assert.NotNull(parser);
+ Assert.IsType<VBCodeParser>(parser);
+ }
+
+ [Fact]
+ public void CreateCodeGeneratorParserListenerReturnsNewCSharpCodeGeneratorParserListener()
+ {
+ // Arrange
+ RazorCodeLanguage service = new VBRazorCodeLanguage();
+
+ // Act
+ RazorEngineHost host = new RazorEngineHost(new VBRazorCodeLanguage());
+ RazorCodeGenerator generator = service.CreateCodeGenerator("Foo", "Bar", "Baz", host);
+
+ // Assert
+ Assert.NotNull(generator);
+ Assert.IsType<VBRazorCodeGenerator>(generator);
+ Assert.Equal("Foo", generator.ClassName);
+ Assert.Equal("Bar", generator.RootNamespaceName);
+ Assert.Equal("Baz", generator.SourceFileName);
+ Assert.Same(host, generator.Host);
+ }
+
+ [Fact]
+ public void CodeDomProviderTypeReturnsVBCodeProvider()
+ {
+ // Arrange
+ RazorCodeLanguage service = new VBRazorCodeLanguage();
+
+ // Assert
+ Assert.Equal(typeof(VBCodeProvider), service.CodeDomProviderType);
+ }
+ }
+}
diff --git a/test/System.Web.Razor.Test/packages.config b/test/System.Web.Razor.Test/packages.config
new file mode 100644
index 00000000..d5aa6401
--- /dev/null
+++ b/test/System.Web.Razor.Test/packages.config
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="Moq" version="4.0.10827" />
+ <package id="xunit" version="1.9.0.1566" />
+ <package id="xunit.extensions" version="1.9.0.1566" />
+</packages> \ No newline at end of file
diff --git a/test/System.Web.WebPages.Administration.Test/AdminPackageTest.cs b/test/System.Web.WebPages.Administration.Test/AdminPackageTest.cs
new file mode 100644
index 00000000..aee0dbeb
--- /dev/null
+++ b/test/System.Web.WebPages.Administration.Test/AdminPackageTest.cs
@@ -0,0 +1,397 @@
+using System.Collections.Specialized;
+using System.IO;
+using System.Text;
+using System.Web.Helpers;
+using System.Web.Hosting;
+using System.Web.Security;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.WebPages.Administration.Test
+{
+ public class AdminPackageTest
+ {
+ [Fact]
+ public void GetAdminVirtualPathThrowsIfPathIsNull()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentNull(() => SiteAdmin.GetVirtualPath(null), "virtualPath");
+ }
+
+ [Fact]
+ public void GetAdminVirtualPathDoesNotAppendAdminVirtualPathIfPathStartsWithAdminVirtualPath()
+ {
+ // Act
+ string vpath = SiteAdmin.GetVirtualPath("~/_Admin/Foo");
+
+ // Assert
+ Assert.Equal("~/_Admin/Foo", vpath);
+ }
+
+ [Fact]
+ public void GetAdminVirtualPathAppendsAdminVirtualPath()
+ {
+ // Act
+ string vpath = SiteAdmin.GetVirtualPath("~/Foo");
+
+ // Assert
+ Assert.Equal("~/_Admin/Foo", vpath);
+ }
+
+ [Fact]
+ public void SetAuthCookieAddsAuthCookieToResponseCollection()
+ {
+ // Arrange
+ var mockResponse = new Mock<HttpResponseBase>();
+ var cookies = new HttpCookieCollection();
+ mockResponse.Setup(m => m.Cookies).Returns(cookies);
+
+ // Act
+ AdminSecurity.SetAuthCookie(mockResponse.Object);
+
+ // Assert
+ Assert.NotNull(cookies[".ASPXADMINAUTH"]);
+ }
+
+ [Fact]
+ public void GetAuthAdminCookieCreatesAnAuthTicketWithUserDataSetToAdmin()
+ {
+ // Arrange
+ var cookie = AdminSecurity.GetAuthCookie();
+
+ // Act
+ FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(cookie.Value);
+
+ // Assert
+ Assert.Equal(".ASPXADMINAUTH", cookie.Name);
+ Assert.True(cookie.HttpOnly);
+ Assert.Equal(2, ticket.Version);
+ Assert.Equal("ADMIN", ticket.UserData);
+ }
+
+ [Fact]
+ public void IsAuthenticatedReturnsFalseIfAuthCookieNotInCollection()
+ {
+ // Arrange
+ var mockRequest = new Mock<HttpRequestBase>();
+ var cookies = new HttpCookieCollection();
+ mockRequest.Setup(m => m.Cookies).Returns(cookies);
+
+ // Act
+ bool authorized = AdminSecurity.IsAuthenticated(mockRequest.Object);
+
+ // Assert
+ Assert.False(authorized);
+ }
+
+ [Fact]
+ public void IsAuthenticatedReturnsFalseIfAuthCookieInCollectionAndIsNotAValidAdminAuthCookie()
+ {
+ // Arrange
+ var mockRequest = new Mock<HttpRequestBase>();
+ var cookies = new HttpCookieCollection();
+ mockRequest.Setup(m => m.Cookies).Returns(cookies);
+ cookies.Add(new HttpCookie(".ASPXADMINAUTH", "test"));
+
+ // Act
+ bool authorized = AdminSecurity.IsAuthenticated(mockRequest.Object);
+
+ // Assert
+ Assert.False(authorized);
+ }
+
+ [Fact]
+ public void IsAuthenticatedReturnsTrueIfAuthCookieIsValid()
+ {
+ // Arrange
+ var mockRequest = new Mock<HttpRequestBase>();
+ var cookies = new HttpCookieCollection();
+ mockRequest.Setup(m => m.Cookies).Returns(cookies);
+ cookies.Add(AdminSecurity.GetAuthCookie());
+
+ // Act
+ bool authorized = AdminSecurity.IsAuthenticated(mockRequest.Object);
+
+ // Assert
+ Assert.True(authorized);
+ }
+
+ [Fact]
+ public void GetRedirectUrlAppendsAppRelativePathAsReturnUrl()
+ {
+ // Arrange
+ var mockRequest = new Mock<HttpRequestBase>();
+ mockRequest.Setup(m => m.RawUrl).Returns("~/_Admin/foo/bar/baz");
+ mockRequest.Setup(m => m.QueryString).Returns(new NameValueCollection());
+
+ // Act
+ string redirectUrl = SiteAdmin.GetRedirectUrl(mockRequest.Object, "register", MakeAppRelative);
+
+ // Assert
+ Assert.Equal("~/_Admin/register?ReturnUrl=%7e%2f_Admin%2ffoo%2fbar%2fbaz", redirectUrl);
+ }
+
+ [Fact]
+ public void GetRedirectUrlDoesNotAppendsAppRelativePathAsReturnUrlIfAlreadyExists()
+ {
+ // Arrange
+ var mockRequest = new Mock<HttpRequestBase>();
+ mockRequest.Setup(m => m.RawUrl).Returns("~/_Admin/foo/bar/baz?ReturnUrl=~/foo");
+ var queryString = new NameValueCollection();
+ queryString["ReturnUrl"] = "~/foo";
+ mockRequest.Setup(m => m.QueryString).Returns(queryString);
+
+ // Act
+ string redirectUrl = SiteAdmin.GetRedirectUrl(mockRequest.Object, "register", MakeAppRelative);
+
+ // Assert
+ Assert.Equal("~/_Admin/register?ReturnUrl=%7e%2ffoo", redirectUrl);
+ }
+
+ [Fact]
+ public void GetReturnUrlReturnsNullIfNotSet()
+ {
+ // Arrange
+ var mockRequest = new Mock<HttpRequestBase>();
+ mockRequest.Setup(m => m.QueryString).Returns(new NameValueCollection());
+
+ // Act
+ string returlUrl = SiteAdmin.GetReturnUrl(mockRequest.Object);
+
+ // Assert
+ Assert.Null(returlUrl);
+ }
+
+ [Fact]
+ public void GetReturnUrlThrowsIfReturnUrlIsNotAppRelative()
+ {
+ // Arrange
+ var mockRequest = new Mock<HttpRequestBase>();
+ var queryString = new NameValueCollection();
+ queryString["ReturnUrl"] = "http://www.bing.com";
+ mockRequest.Setup(m => m.QueryString).Returns(queryString);
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(() => SiteAdmin.GetReturnUrl(mockRequest.Object), "The return URL specified for request redirection is invalid.");
+ }
+
+ [Fact]
+ public void GetReturnUrlReturnsReturlUrlQueryStringParameterIfItIsAppRelative()
+ {
+ // Arrange
+ var mockRequest = new Mock<HttpRequestBase>();
+ var queryString = new NameValueCollection();
+ queryString["ReturnUrl"] = "~/_Admin/bar?foo=1";
+ mockRequest.Setup(m => m.QueryString).Returns(queryString);
+
+ // Act
+ string returnUrl = SiteAdmin.GetReturnUrl(mockRequest.Object);
+
+ // Assert
+ Assert.Equal("~/_Admin/bar?foo=1", returnUrl);
+ }
+
+ [Fact]
+ public void SaveAdminPasswordUsesCryptoToWritePasswordAndSalt()
+ {
+ // Arrange
+ var password = "some-random-password";
+ MemoryStream ms = new MemoryStream();
+
+ // Act
+ bool passwordSaved = AdminSecurity.SaveTemporaryPassword(password, () => ms);
+
+ // Assert
+ Assert.True(passwordSaved);
+ string savedPassword = Encoding.Default.GetString(ms.ToArray());
+ // Trim everything after the new line. Cannot use the properties from the stream since it is already closed by the writer.
+ savedPassword = savedPassword.Substring(0, savedPassword.IndexOf(Environment.NewLine));
+
+ Assert.True(Crypto.VerifyHashedPassword(savedPassword, password));
+ }
+
+ [Fact]
+ public void SaveAdminPasswordReturnsFalseIfGettingStreamThrowsUnauthorizedAccessException()
+ {
+ // Act
+ bool passwordSaved = AdminSecurity.SaveTemporaryPassword("password", () => { throw new UnauthorizedAccessException(); });
+
+ // Assert
+ Assert.False(passwordSaved);
+ }
+
+ [Fact]
+ public void CheckPasswordReturnsTrueIfPasswordIsValid()
+ {
+ // Arrange
+ var ms = new MemoryStream();
+ var writer = new StreamWriter(ms);
+ writer.WriteLine(Crypto.HashPassword("password"));
+ writer.Flush();
+ ms.Seek(0, SeekOrigin.Begin);
+
+ // Act
+ bool passwordIsValid = AdminSecurity.CheckPassword("password", () => ms);
+
+ // Assert
+ Assert.True(passwordIsValid);
+
+ writer.Close();
+ }
+
+ [Fact]
+ public void HasAdminPasswordReturnsTrueIfAdminPasswordFileExists()
+ {
+ // Arrange
+ Mock<VirtualPathProvider> mockVpp = new Mock<VirtualPathProvider>();
+ mockVpp.Setup(m => m.FileExists("~/App_Data/Admin/Password.config")).Returns(true);
+
+ // Act
+ bool hasPassword = AdminSecurity.HasAdminPassword(mockVpp.Object);
+
+ // Assert
+ Assert.True(hasPassword);
+ }
+
+ [Fact]
+ public void HasAdminPasswordReturnsFalseIfAdminPasswordFileDoesNotExists()
+ {
+ // Arrange
+ Mock<VirtualPathProvider> mockVpp = new Mock<VirtualPathProvider>();
+ mockVpp.Setup(m => m.FileExists("~/App_Data/Admin/Password.config")).Returns(false);
+
+ // Act
+ bool hasPassword = AdminSecurity.HasAdminPassword(mockVpp.Object);
+
+ // Assert
+ Assert.False(hasPassword);
+ }
+
+ [Fact]
+ public void HasTemporaryPasswordReturnsTrueIfAdminPasswordFileExists()
+ {
+ // Arrange
+ Mock<VirtualPathProvider> mockVpp = new Mock<VirtualPathProvider>();
+ mockVpp.Setup(m => m.FileExists("~/App_Data/Admin/_Password.config")).Returns(true);
+
+ // Act
+ bool hasPassword = AdminSecurity.HasTemporaryPassword(mockVpp.Object);
+
+ // Assert
+ Assert.True(hasPassword);
+ }
+
+ [Fact]
+ public void HasTemporaryPasswordReturnsFalseIfAdminPasswordFileDoesNotExists()
+ {
+ // Arrange
+ Mock<VirtualPathProvider> mockVpp = new Mock<VirtualPathProvider>();
+ mockVpp.Setup(m => m.FileExists("~/App_Data/Admin/_Password.config")).Returns(false);
+
+ // Act
+ bool hasPassword = AdminSecurity.HasTemporaryPassword(mockVpp.Object);
+
+ // Assert
+ Assert.False(hasPassword);
+ }
+
+ [Fact]
+ public void NoPasswordOrTemporaryPasswordRedirectsToRegisterPage()
+ {
+ AssertSecure(requestUrl: "~/",
+ passwordExists: false,
+ temporaryPasswordExists: false,
+ expectedUrl: "~/_Admin/Register.cshtml?ReturnUrl=%7e%2f");
+ }
+
+ [Fact]
+ public void IfPasswordExistsRedirectsToLoginPage()
+ {
+ AssertSecure(requestUrl: "~/",
+ passwordExists: true,
+ temporaryPasswordExists: false,
+ expectedUrl: "~/_Admin/Login.cshtml?ReturnUrl=%7e%2f");
+ }
+
+ [Fact]
+ public void IfPasswordExistsRedirectsToLoginPageEvenIfTemporaryPasswordFileExists()
+ {
+ AssertSecure(requestUrl: "~/",
+ passwordExists: true,
+ temporaryPasswordExists: true,
+ expectedUrl: "~/_Admin/Login.cshtml?ReturnUrl=%7e%2f");
+ }
+
+ [Fact]
+ public void IfTemporaryPasswordExistsRedirectsToInstructionsPage()
+ {
+ AssertSecure(requestUrl: "~/",
+ passwordExists: false,
+ temporaryPasswordExists: true,
+ expectedUrl: "~/_Admin/EnableInstructions.cshtml?ReturnUrl=%7e%2f");
+ }
+
+ [Fact]
+ public void NoRedirectIfAlreadyGoingToRedirectPage()
+ {
+ AssertSecure(requestUrl: "~/_Admin/Register.cshtml",
+ passwordExists: false,
+ temporaryPasswordExists: false,
+ expectedUrl: null);
+
+ AssertSecure(requestUrl: "~/_Admin/Login.cshtml",
+ passwordExists: true,
+ temporaryPasswordExists: false,
+ expectedUrl: null);
+
+ AssertSecure(requestUrl: "~/_Admin/EnableInstructions.cshtml",
+ passwordExists: false,
+ temporaryPasswordExists: true,
+ expectedUrl: null);
+ }
+
+ private static void AssertSecure(string requestUrl, bool passwordExists, bool temporaryPasswordExists, string expectedUrl)
+ {
+ // Arrange
+ var vpp = new Mock<VirtualPathProvider>();
+ if (temporaryPasswordExists)
+ {
+ vpp.Setup(m => m.FileExists("~/App_Data/Admin/_Password.config")).Returns(true);
+ }
+
+ if (passwordExists)
+ {
+ vpp.Setup(m => m.FileExists("~/App_Data/Admin/Password.config")).Returns(true);
+ }
+
+ string redirectUrl = null;
+ var response = new Mock<HttpResponseBase>();
+ response.Setup(m => m.Redirect(It.IsAny<string>())).Callback<string>(url => redirectUrl = url);
+ var request = new Mock<HttpRequestBase>();
+ request.Setup(m => m.QueryString).Returns(new NameValueCollection());
+ request.Setup(m => m.RawUrl).Returns(requestUrl);
+ var cookies = new HttpCookieCollection();
+ request.Setup(m => m.Cookies).Returns(cookies);
+ var context = new Mock<HttpContextBase>();
+ context.Setup(m => m.Request).Returns(request.Object);
+ context.Setup(m => m.Response).Returns(response.Object);
+ var startPage = new Mock<StartPage>() { CallBase = true };
+ var page = new Mock<WebPageRenderingBase>();
+ page.Setup(m => m.VirtualPath).Returns(requestUrl);
+ startPage.Object.ChildPage = page.Object;
+ page.Setup(m => m.Context).Returns(context.Object);
+
+ // Act
+ AdminSecurity.Authorize(startPage.Object, vpp.Object, MakeAppRelative);
+
+ // Assert
+ Assert.Equal(expectedUrl, redirectUrl);
+ }
+
+ private static string MakeAppRelative(string path)
+ {
+ return path;
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Administration.Test/PackageManagerModuleTest.cs b/test/System.Web.WebPages.Administration.Test/PackageManagerModuleTest.cs
new file mode 100644
index 00000000..2f57bf85
--- /dev/null
+++ b/test/System.Web.WebPages.Administration.Test/PackageManagerModuleTest.cs
@@ -0,0 +1,150 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Web.WebPages.Administration.PackageManager;
+using Moq;
+using Xunit;
+
+namespace System.Web.WebPages.Administration.Test
+{
+ public class PackageManagerModuleTest
+ {
+ [Fact]
+ public void InitSourceFileDoesNotAffectSourcesFileWhenFeedIsNotNull()
+ {
+ // Arrange
+ bool sourceFileCalled = false;
+ var sourceFile = GetPackagesSourceFile();
+ sourceFile.Setup(s => s.Exists()).Returns(false);
+ sourceFile.Setup(s => s.WriteSources(It.IsAny<IEnumerable<WebPackageSource>>())).Callback(() => sourceFileCalled = true);
+ sourceFile.Setup(c => c.ReadSources()).Callback(() => sourceFileCalled = true);
+ ISet<WebPackageSource> set = new HashSet<WebPackageSource>();
+
+ // Act
+ PackageManagerModule.InitPackageSourceFile(sourceFile.Object, ref set);
+
+ // Assert
+ Assert.False(sourceFileCalled);
+ }
+
+ [Fact]
+ public void InitSourceFileWritesToDiskIfSourcesFileDoesNotExist()
+ {
+ // Arrange
+ ISet<WebPackageSource> set = null;
+ var sourceFile = GetPackagesSourceFile();
+ sourceFile.Setup(s => s.Exists()).Returns(false);
+ sourceFile.Setup(s => s.WriteSources(It.IsAny<IEnumerable<WebPackageSource>>()));
+
+ // Act
+ PackageManagerModule.InitPackageSourceFile(sourceFile.Object, ref set);
+
+ Assert.NotNull(set);
+ Assert.Equal(set.Count(), 2);
+ Assert.Equal(set.First().Source, "http://go.microsoft.com/fwlink/?LinkID=226946");
+ Assert.Equal(set.Last().Source, "http://go.microsoft.com/fwlink/?LinkID=226948");
+ }
+
+ [Fact]
+ public void InitSourceFileReadsFromDiskWhenFileAlreadyExists()
+ {
+ // Arrange
+ var sourceFile = GetPackagesSourceFile();
+ sourceFile.Setup(s => s.Exists()).Returns(true);
+ ISet<WebPackageSource> set = null;
+
+ // Act
+ PackageManagerModule.InitPackageSourceFile(sourceFile.Object, ref set);
+
+ // Assert
+ Assert.NotNull(set);
+ Assert.Equal(set.Count(), 2);
+ }
+
+ [Fact]
+ public void AddFeedWritesSourceIfItDoesNotExist()
+ {
+ // Arrange
+ bool writeCalled = false;
+ var sourceFile = GetPackagesSourceFile();
+ sourceFile.Setup(c => c.WriteSources(It.IsAny<IEnumerable<WebPackageSource>>())).Callback(() => writeCalled = true);
+ ISet<WebPackageSource> set = new HashSet<WebPackageSource>(GetSources());
+
+ // Act
+ bool returnValue = PackageManagerModule.AddPackageSource(sourceFile.Object, set, new WebPackageSource(source: "http://www.microsoft.com/feed3", name: "Feed3"));
+
+ // Assert
+ Assert.Equal(set.Count(), 3);
+ Assert.True(writeCalled);
+ Assert.True(returnValue);
+ }
+
+ [Fact]
+ public void AddFeedDoesNotWritesSourceIfExists()
+ {
+ // Arrange
+ bool writeCalled = false;
+ var sourceFile = GetPackagesSourceFile();
+ sourceFile.Setup(c => c.WriteSources(It.IsAny<IEnumerable<WebPackageSource>>())).Callback(() => writeCalled = true);
+ ISet<WebPackageSource> set = new HashSet<WebPackageSource>(GetSources());
+
+ // Act
+ bool returnValue = PackageManagerModule.AddPackageSource(sourceFile.Object, set, new WebPackageSource(source: "http://www.microsoft.com/feed1", name: "Feed1"));
+
+ // Assert
+ Assert.Equal(set.Count(), 2);
+ Assert.False(writeCalled);
+ Assert.False(returnValue);
+ }
+
+ [Fact]
+ public void RemoveFeedRemovesSourceFromSet()
+ {
+ // Arrange
+ bool writeCalled = false;
+ var sourceFile = GetPackagesSourceFile();
+ sourceFile.Setup(c => c.WriteSources(It.IsAny<IEnumerable<WebPackageSource>>())).Callback(() => writeCalled = true);
+ ISet<WebPackageSource> set = new HashSet<WebPackageSource>(GetSources());
+
+ // Act
+ PackageManagerModule.RemovePackageSource(sourceFile.Object, set, "feed1");
+
+ // Assert
+ Assert.Equal(set.Count(), 1);
+ Assert.False(set.Any(s => s.Name == "Feed1"));
+ Assert.True(writeCalled);
+ }
+
+ [Fact]
+ public void RemoveFeedDoesNotAffectSourceFileIsFeedDoesNotExist()
+ {
+ // Arrange
+ bool writeCalled = false;
+ var sourceFile = GetPackagesSourceFile();
+ sourceFile.Setup(c => c.WriteSources(It.IsAny<IEnumerable<WebPackageSource>>())).Callback(() => writeCalled = true);
+ ISet<WebPackageSource> set = new HashSet<WebPackageSource>(GetSources());
+
+ // Act
+ PackageManagerModule.RemovePackageSource(sourceFile.Object, set, "feed3");
+
+ // Assert
+ Assert.Equal(set.Count(), 2);
+ Assert.False(writeCalled);
+ }
+
+ private static Mock<IPackagesSourceFile> GetPackagesSourceFile()
+ {
+ var sourceFile = new Mock<IPackagesSourceFile>();
+ sourceFile.Setup(c => c.ReadSources()).Returns(GetSources());
+ return sourceFile;
+ }
+
+ private static IEnumerable<WebPackageSource> GetSources()
+ {
+ return new[]
+ {
+ new WebPackageSource(name: "Feed1", source: "http://www.microsoft.com/feed1"),
+ new WebPackageSource(name: "Feed2", source: "http://www.microsoft.com/feed2")
+ };
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Administration.Test/PackagesSourceFileTest.cs b/test/System.Web.WebPages.Administration.Test/PackagesSourceFileTest.cs
new file mode 100644
index 00000000..25156769
--- /dev/null
+++ b/test/System.Web.WebPages.Administration.Test/PackagesSourceFileTest.cs
@@ -0,0 +1,146 @@
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Web.WebPages.Administration.PackageManager;
+using System.Web.WebPages.TestUtils;
+using System.Xml.Linq;
+using Xunit;
+
+namespace System.Web.WebPages.Administration.Test
+{
+ public class PackagesSourceFileTest
+ {
+ [Fact]
+ public void PackagesSourceFileThrowsIfTheXmlElementDoesNotContainNameAndUrl()
+ {
+ // Arrange
+ var element = new XElement("source");
+
+ // Act and Assert
+ Assert.Throws<FormatException>(() => PackageSourceFile.ParsePackageSource(element));
+ }
+
+ [Fact]
+ public void PackagesSourceFileThrowsIfTheXmlElementDoesNotContainUrl()
+ {
+ // Arrange
+ var element = new XElement("source", new XAttribute("displayname", "foo"), new XAttribute("filterpreferred", false));
+
+ // Act and Assert
+ Assert.Throws<FormatException>(() => PackageSourceFile.ParsePackageSource(element));
+ }
+
+ [Fact]
+ public void PackagesSourceFileThrowsIfTheXmlElementDoesNotContainName()
+ {
+ // Arrange
+ var element = new XElement("source", new XAttribute("url", "http://microsoft.com"), new XAttribute("filterpreferred", false));
+
+ // Act and Assert
+ Assert.Throws<FormatException>(() => PackageSourceFile.ParsePackageSource(element));
+ }
+
+ [Fact]
+ public void PackagesSourceFileDoesNotThrowIfXmlElementDoesNotContainPreferred()
+ {
+ // Arrange
+ var element = new XElement("source", new XAttribute("displayname", "foo"), new XAttribute("url", "http://microsoft.com"));
+
+ // Act
+ var item = PackageSourceFile.ParsePackageSource(element);
+
+ // Assert
+ Assert.NotNull(item);
+ }
+
+ [Fact]
+ public void PackagesSourceFileThrowsIfTheFeedUrlIsMalformed()
+ {
+ // Arrange
+ var element = new XElement("source",
+ new XAttribute("displayname", "foo"),
+ new XAttribute("url", "bad-url.com"),
+ new XAttribute("filterpreferred", false)
+ );
+
+ // Act and Assert
+ Assert.Throws<FormatException>(() => PackageSourceFile.ParsePackageSource(element));
+ }
+
+ [Fact]
+ public void PackagesSourceFileParsesXElement()
+ {
+ // Arrange
+ var element = new XElement("source",
+ new XAttribute("displayname", "foo"),
+ new XAttribute("url", "http://www.microsoft.com"),
+ new XAttribute("filterpreferred", true)
+ );
+
+ // Act
+ var WebPackageSource = PackageSourceFile.ParsePackageSource(element);
+
+ // Assert
+ Assert.Equal("foo", WebPackageSource.Name);
+ Assert.Equal("http://www.microsoft.com", WebPackageSource.Source);
+ Assert.True(WebPackageSource.FilterPreferredPackages);
+ }
+
+ [Fact]
+ public void PackagesSourceFileReadsAllFeedsFromStream()
+ {
+ // Arrange
+ var document = new XDocument(
+ new XElement("sources",
+ new XElement("source", new XAttribute("displayname", "Feed1"), new XAttribute("url", "http://www.microsoft.com/feed1"), new XAttribute("filterpreferred", true)),
+ new XElement("source", new XAttribute("displayname", "Feed2"), new XAttribute("url", "http://www.microsoft.com/feed2"), new XAttribute("filterpreferred", true))
+ ));
+ var stream = new MemoryStream();
+ document.Save(stream);
+ stream = new MemoryStream(stream.ToArray());
+ string xml = new StreamReader(stream).ReadToEnd().TrimEnd('\0');
+
+ // Act
+ var result = PackageSourceFile.ReadFeeds(() => new MemoryStream(Encoding.Default.GetBytes(xml)));
+
+ // Assert
+ Assert.Equal(2, result.Count());
+ Assert.Equal("Feed1", result.First().Name);
+ Assert.Equal("Feed2", result.Last().Name);
+ }
+
+ [Fact]
+ public void PackagesSourceFileWritesAllFeedsToStream()
+ {
+ // Arrange
+ var packagesSources = new[]
+ {
+ new WebPackageSource(name: "Feed1", source: "http://www.microsoft.com/Feed1"),
+ new WebPackageSource(name: "Feed2", source: "http://www.microsoft.com/Feed2") { FilterPreferredPackages = true }
+ };
+ var stream = new MemoryStream();
+
+ // Act
+ PackageSourceFile.WriteFeeds(packagesSources, () => stream);
+ stream = new MemoryStream(stream.ToArray());
+ string result = new StreamReader(stream).ReadToEnd().TrimEnd('\0');
+
+ // Assert
+ var document = XDocument.Parse(result);
+ Assert.Equal(document.Root.Name, "sources");
+ Assert.Equal(document.Root.Elements().Count(), 2);
+
+ var firstFeed = document.Root.Elements().First();
+ Assert.Equal(firstFeed.Name, "source");
+ Assert.Equal(firstFeed.Attribute("displayname").Value, "Feed1");
+ Assert.Equal(firstFeed.Attribute("url").Value, "http://www.microsoft.com/Feed1");
+ Assert.Equal(firstFeed.Attribute("filterpreferred").Value, "false");
+
+ var secondFeed = document.Root.Elements().Last();
+ Assert.Equal(secondFeed.Name, "source");
+ Assert.Equal(secondFeed.Attribute("displayname").Value, "Feed2");
+ Assert.Equal(secondFeed.Attribute("url").Value, "http://www.microsoft.com/Feed2");
+ Assert.Equal(secondFeed.Attribute("filterpreferred").Value, "true");
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Administration.Test/PageUtilsTest.cs b/test/System.Web.WebPages.Administration.Test/PageUtilsTest.cs
new file mode 100644
index 00000000..72370d17
--- /dev/null
+++ b/test/System.Web.WebPages.Administration.Test/PageUtilsTest.cs
@@ -0,0 +1,153 @@
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Web.WebPages.Administration.PackageManager;
+using Moq;
+using Xunit;
+
+namespace System.Web.WebPages.Administration.Test
+{
+ public class PageUtilsTest
+ {
+ [Fact]
+ public void GetFilterValueReturnsNullIfValueWasNotFound()
+ {
+ // Arrange
+ var request = new Mock<HttpRequestBase>();
+ request.Setup(c => c.QueryString).Returns(new NameValueCollection());
+ request.Setup(c => c.Cookies).Returns(new HttpCookieCollection());
+
+ // Act
+ var value = PageUtils.GetFilterValue(request.Object, "foo", "my-key");
+
+ // Assert
+ Assert.Null(value);
+ }
+
+ [Fact]
+ public void GetFilterValueReturnsValueFromCookieIfQueryStringDoesNotContainKey()
+ {
+ // Arrange
+ const string key = "my-key";
+ const string value = "my-cookie-value";
+ var request = new Mock<HttpRequestBase>();
+ request.Setup(c => c.QueryString).Returns(new NameValueCollection());
+ var cookies = new HttpCookieCollection();
+ var cookie = new HttpCookie("foo");
+ cookie[key] = value;
+ cookies.Add(cookie);
+ request.Setup(c => c.Cookies).Returns(cookies);
+
+ // Act
+ var returnedValue = PageUtils.GetFilterValue(request.Object, "foo", key);
+
+ // Assert
+ Assert.Equal(value, returnedValue);
+ }
+
+ [Fact]
+ public void GetFilterValueReturnsValueFromQueryString()
+ {
+ // Arrange
+ const string key = "my-key";
+ const string requestValue = "my-request-value";
+ const string cookieValue = "my-cookie-value";
+ var request = new Mock<HttpRequestBase>();
+ var queryString = new NameValueCollection();
+ queryString[key] = requestValue;
+ request.Setup(c => c.QueryString).Returns(queryString);
+ var cookies = new HttpCookieCollection();
+ var cookie = new HttpCookie("foo");
+ cookie[key] = cookieValue;
+ request.Setup(c => c.Cookies).Returns(cookies);
+
+ // Act
+ var returnedValue = PageUtils.GetFilterValue(request.Object, "foo", key);
+
+ // Assert
+ Assert.Equal(requestValue, returnedValue);
+ }
+
+ [Fact]
+ public void PersistFilterCreatesCookieIfItDoesNotExist()
+ {
+ // Arrange
+ var cookies = new HttpCookieCollection();
+ var response = new Mock<HttpResponseBase>();
+ response.Setup(c => c.Cookies).Returns(cookies);
+
+ // Act
+ PageUtils.PersistFilter(response.Object, "my-cookie", new Dictionary<string, string>());
+
+ // Assert
+ Assert.NotNull(cookies["my-cookie"]);
+ }
+
+ [Fact]
+ public void PersistFilterUsesExistingCookie()
+ {
+ // Arrange
+ var cookieName = "my-cookie";
+ var cookies = new HttpCookieCollection();
+ cookies.Add(new HttpCookie(cookieName));
+ var response = new Mock<HttpResponseBase>();
+ response.Setup(c => c.Cookies).Returns(cookies);
+
+ // Act
+ PageUtils.PersistFilter(response.Object, "my-cookie", new Dictionary<string, string>());
+
+ // Assert
+ Assert.Equal(1, cookies.Count);
+ }
+
+ [Fact]
+ public void PersistFilterAddsDictionaryEntriesToCookie()
+ {
+ // Arrange
+ var cookies = new HttpCookieCollection();
+ var response = new Mock<HttpResponseBase>();
+ response.Setup(c => c.Cookies).Returns(cookies);
+
+ // Act
+ PageUtils.PersistFilter(response.Object, "my-cookie", new Dictionary<string, string>() { { "a", "b" }, { "x", "y" } });
+
+ // Assert
+ var cookie = cookies["my-cookie"];
+ Assert.Equal(cookie["a"], "b");
+ Assert.Equal(cookie["x"], "y");
+ }
+
+ [Fact]
+ public void IsValidLicenseUrlReturnsTrueForHttpUris()
+ {
+ // Arrange
+ var uri = new Uri("http://www.microsoft.com");
+
+ // Act and Assert
+ Assert.True(PageUtils.IsValidLicenseUrl(uri));
+ }
+
+ [Fact]
+ public void IsValidLicenseUrlReturnsTrueForHttpsUris()
+ {
+ // Arrange
+ var uri = new Uri("HTTPs://www.asp.net");
+
+ // Act and Assert
+ Assert.True(PageUtils.IsValidLicenseUrl(uri));
+ }
+
+ [Fact]
+ public void IsValidLicenseUrlReturnsFalseForNonHttpUris()
+ {
+ // Arrange
+ var jsUri = new Uri("javascript:alert('Hello world');");
+ var fileShareUri = new Uri(@"c:\windows\system32\notepad.exe");
+ var mailToUti = new Uri("mailto:invalid-email@microsoft.com");
+
+ // Act and Assert
+ Assert.False(PageUtils.IsValidLicenseUrl(jsUri));
+ Assert.False(PageUtils.IsValidLicenseUrl(fileShareUri));
+ Assert.False(PageUtils.IsValidLicenseUrl(mailToUti));
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Administration.Test/PreApplicationStartCodeTest.cs b/test/System.Web.WebPages.Administration.Test/PreApplicationStartCodeTest.cs
new file mode 100644
index 00000000..de9e5e74
--- /dev/null
+++ b/test/System.Web.WebPages.Administration.Test/PreApplicationStartCodeTest.cs
@@ -0,0 +1,33 @@
+using System.Linq;
+using System.Web.WebPages.TestUtils;
+using Xunit;
+
+namespace System.Web.WebPages.Administration.Test
+{
+ public class PreApplicationStartCodeTest
+ {
+ [Fact]
+ public void StartTest()
+ {
+ AppDomainUtils.RunInSeparateAppDomain(() =>
+ {
+ var adminPackageAssembly = typeof(PreApplicationStartCode).Assembly;
+ AppDomainUtils.SetPreAppStartStage();
+ PreApplicationStartCode.Start();
+ // Call a second time to ensure multiple calls do not cause issues
+ PreApplicationStartCode.Start();
+
+ // TODO: Need a way to see if the module was actually registered
+ var registeredAssemblies = ApplicationPart.GetRegisteredParts().ToList();
+ Assert.Equal(1, registeredAssemblies.Count);
+ registeredAssemblies.First().Assembly.Equals(adminPackageAssembly);
+ });
+ }
+
+ [Fact]
+ public void TestPreAppStartClass()
+ {
+ PreAppStartTestHelper.TestPreAppStartClass(typeof(PreApplicationStartCode));
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Administration.Test/RemoteAssemblyTest.cs b/test/System.Web.WebPages.Administration.Test/RemoteAssemblyTest.cs
new file mode 100644
index 00000000..fea9ddd1
--- /dev/null
+++ b/test/System.Web.WebPages.Administration.Test/RemoteAssemblyTest.cs
@@ -0,0 +1,147 @@
+using System.Linq;
+using System.Web.WebPages.Administration.PackageManager;
+using Moq;
+using NuGet.Runtime;
+using Xunit;
+
+namespace System.Web.WebPages.Administration.Test
+{
+ public class RemoteAssemblyTest
+ {
+ [Fact]
+ public void GetAssembliesForBindingRedirectReturnsEmptySequenceIfNoBinAssembliesAreFound()
+ {
+ // Act
+ var assemblies = RemoteAssembly.GetAssembliesForBindingRedirect(AppDomain.CurrentDomain, @"x:\site\bin", (_, __) => Enumerable.Empty<IAssembly>());
+
+ // Assert
+ Assert.Empty(assemblies);
+ }
+
+ [Fact]
+ public void RemoteAssemblyComparesById()
+ {
+ // Arrange
+ var assemblyA = new RemoteAssembly("A", null, null, null);
+ var assemblyB = new Mock<IAssembly>(MockBehavior.Strict);
+ assemblyB.SetupGet(b => b.Name).Returns("Z").Verifiable();
+
+ // Act
+ var result = RemoteAssembly.Compare(assemblyA, assemblyB.Object);
+
+ // Assert
+ Assert.Equal(-25, result);
+ assemblyB.Verify();
+ }
+
+ [Fact]
+ public void RemoteAssemblyComparesByVersionIfIdsAreIdentical()
+ {
+ // Arrange
+ var assemblyA = new RemoteAssembly("A", new Version("2.0.0.0"), null, null);
+ var assemblyB = new Mock<IAssembly>(MockBehavior.Strict);
+ assemblyB.SetupGet(b => b.Name).Returns("A").Verifiable();
+ assemblyB.SetupGet(b => b.Version).Returns(new Version("1.0.0.0")).Verifiable();
+
+ // Act
+ var result = RemoteAssembly.Compare(assemblyA, assemblyB.Object);
+
+ // Assert
+ Assert.Equal(1, result);
+ assemblyB.Verify();
+ }
+
+ [Fact]
+ public void RemoteAssemblyComparesByPublicKeyIfIdsAndVersionAreIdentical()
+ {
+ // Arrange
+ var assemblyA = new RemoteAssembly("A", new Version("1.0.0.0"), "C", null);
+ var assemblyB = new Mock<IAssembly>(MockBehavior.Strict);
+ assemblyB.SetupGet(b => b.Name).Returns("A").Verifiable();
+ assemblyB.SetupGet(b => b.Version).Returns(new Version("1.0.0.0")).Verifiable();
+ assemblyB.SetupGet(b => b.PublicKeyToken).Returns("E").Verifiable();
+
+ // Act
+ var result = RemoteAssembly.Compare(assemblyA, assemblyB.Object);
+
+ // Assert
+ Assert.Equal(-2, result);
+ assemblyB.Verify();
+ }
+
+ [Fact]
+ public void RemoteAssemblyComparesByCultureIfIdVersionAndPublicKeyAreIdentical()
+ {
+ // Arrange
+ var assemblyA = new RemoteAssembly("A", new Version("1.0.0.0"), "public-key", "en-us");
+ var assemblyB = new Mock<IAssembly>(MockBehavior.Strict);
+ assemblyB.SetupGet(b => b.Name).Returns("A").Verifiable();
+ assemblyB.SetupGet(b => b.Version).Returns(new Version("1.0.0.0")).Verifiable();
+ assemblyB.SetupGet(b => b.PublicKeyToken).Returns("public-key").Verifiable();
+ assemblyB.SetupGet(b => b.Culture).Returns("en-uk").Verifiable();
+
+ // Act
+ var result = RemoteAssembly.Compare(assemblyA, assemblyB.Object);
+
+ // Assert
+ Assert.Equal(8, result);
+ assemblyB.Verify();
+ }
+
+ [Fact]
+ public void RemoteAssemblyReturns0IfAllValuesAreIdentical()
+ {
+ // Arrange
+ var assemblyA = new RemoteAssembly("A", new Version("1.0.0.0"), "public-key", "en-us");
+ var assemblyB = new RemoteAssembly("A", new Version("1.0.0.0"), "public-key", "en-us");
+
+ // Act
+ var result = RemoteAssembly.Compare(assemblyA, assemblyB);
+
+ // Assert
+ Assert.Equal(0, result);
+ }
+
+ [Fact]
+ public void RemoteAssemblyReturns1IfValueToBeComparedToIsNull()
+ {
+ // Arrange
+ RemoteAssembly assemblyA = new RemoteAssembly("A", new Version("1.0.0.0"), "public-key", "en-us");
+ RemoteAssembly assemblyB = null;
+
+ // Act
+ var result = assemblyA.CompareTo(assemblyB);
+
+ // Assert
+ Assert.Equal(1, result);
+ }
+
+ [Fact]
+ public void EqualReturnsTrueIfValuesAreIdentical()
+ {
+ // Arrange
+ var assemblyA = new RemoteAssembly("A", new Version("1.0.0.0"), "public-key", "en-us");
+ var assemblyB = new RemoteAssembly("A", new Version("1.0.0.0"), "public-key", "en-us");
+
+ // Act
+ var result = assemblyA.Equals(assemblyB);
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void EqualReturnsFalseIfValuesAreNotIdentical()
+ {
+ // Arrange
+ var assemblyA = new RemoteAssembly("A", new Version("1.0.0.0"), "public-key", "en-us");
+ var assemblyB = new RemoteAssembly("A", new Version("1.0.0.1"), "public-key", "en-us");
+
+ // Act
+ var result = assemblyA.Equals(assemblyB);
+
+ // Assert
+ Assert.False(result);
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Administration.Test/System.Web.WebPages.Administration.Test.csproj b/test/System.Web.WebPages.Administration.Test/System.Web.WebPages.Administration.Test.csproj
new file mode 100644
index 00000000..5c4ba83e
--- /dev/null
+++ b/test/System.Web.WebPages.Administration.Test/System.Web.WebPages.Administration.Test.csproj
@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProductVersion>9.0.30729</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{21C729D6-ECF8-47EF-A236-7C6A4272EAF0}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>System.Web.WebPages.Administration.Test</RootNamespace>
+ <AssemblyName>System.Web.WebPages.Administration.Test</AssemblyName>
+ <FileAlignment>512</FileAlignment>
+ <ProjectTypeGuids>{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Debug\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Release\Test\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'CodeCoverage' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\CodeCoverage\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="Microsoft.CSharp" />
+ <Reference Include="Moq, Version=4.0.10827.0, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL">
+ <HintPath>..\..\packages\Moq.4.0.10827\lib\NET40\Moq.dll</HintPath>
+ </Reference>
+ <Reference Include="NuGet.Core">
+ <HintPath>..\..\packages\Nuget.Core.1.6.2\lib\net40\NuGet.Core.dll</HintPath>
+ </Reference>
+ <Reference Include="System" />
+ <Reference Include="System.Core">
+ <RequiredTargetFramework>3.5</RequiredTargetFramework>
+ </Reference>
+ <Reference Include="System.Web" />
+ <Reference Include="System.XML" />
+ <Reference Include="System.Xml.Linq" />
+ <Reference Include="xunit">
+ <HintPath>..\..\packages\xunit.1.9.0.1566\lib\xunit.dll</HintPath>
+ </Reference>
+ <Reference Include="xunit.extensions">
+ <HintPath>..\..\packages\xunit.extensions.1.9.0.1566\lib\xunit.extensions.dll</HintPath>
+ </Reference>
+ </ItemGroup>
+ <ItemGroup>
+ <CodeAnalysisDependentAssemblyPaths Condition=" '$(VS100COMNTOOLS)' != '' " Include="$(VS100COMNTOOLS)..\IDE\PrivateAssemblies">
+ <Visible>False</Visible>
+ </CodeAnalysisDependentAssemblyPaths>
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\System.Web.WebPages.Administration\System.Web.WebPages.Administration.csproj">
+ <Project>{C23F02FC-4538-43F5-ABBA-38BA069AEA8F}</Project>
+ <Name>System.Web.WebPages.Administration</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\src\System.Web.Helpers\System.Web.Helpers.csproj">
+ <Project>{9B7E3740-6161-4548-833C-4BBCA43B970E}</Project>
+ <Name>System.Web.Helpers</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\src\System.Web.WebPages\System.Web.WebPages.csproj">
+ <Project>{76EFA9C5-8D7E-4FDF-B710-E20F8B6B00D2}</Project>
+ <Name>System.Web.WebPages</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\Microsoft.TestCommon\Microsoft.TestCommon.csproj">
+ <Project>{FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0}</Project>
+ <Name>Microsoft.TestCommon</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="AdminPackageTest.cs" />
+ <Compile Include="PackageManagerModuleTest.cs" />
+ <Compile Include="PackagesSourceFileTest.cs" />
+ <Compile Include="PageUtilsTest.cs" />
+ <Compile Include="PreApplicationStartCodeTest.cs" />
+ <Compile Include="RemoteAssemblyTest.cs" />
+ <Compile Include="WebProjectManagerTest.cs" />
+ <Compile Include="WebProjectSystemTest.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="packages.config" />
+ </ItemGroup>
+ <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+</Project> \ No newline at end of file
diff --git a/test/System.Web.WebPages.Administration.Test/WebProjectManagerTest.cs b/test/System.Web.WebPages.Administration.Test/WebProjectManagerTest.cs
new file mode 100644
index 00000000..015da442
--- /dev/null
+++ b/test/System.Web.WebPages.Administration.Test/WebProjectManagerTest.cs
@@ -0,0 +1,160 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Web.WebPages.Administration.PackageManager;
+using Moq;
+using NuGet;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.WebPages.Administration.Test
+{
+ public class WebPackageManagerTest
+ {
+ [Fact]
+ public void ConstructorThrowsIfRemoteSourceIsNullOrEmpty()
+ {
+ // Act and Assert
+ Assert.ThrowsArgumentNullOrEmptyString(() => new WebProjectManager((string)null, "foo"), "remoteSource");
+ Assert.ThrowsArgumentNullOrEmptyString(() => new WebProjectManager("", @"D:\baz"), "remoteSource");
+ }
+
+ [Fact]
+ public void ConstructorThrowsIfSiteRootIsNullOrEmpty()
+ {
+ // Act and Assert
+ Assert.ThrowsArgumentNullOrEmptyString(() => new WebProjectManager("foo", null), "siteRoot");
+ Assert.ThrowsArgumentNullOrEmptyString(() => new WebProjectManager("foo", ""), "siteRoot");
+ }
+
+ [Fact]
+ public void AllowInstallingPackageWithToolsFolderDoNotThrow()
+ {
+ // Arrange
+ var projectManager = new Mock<IProjectManager>();
+ projectManager.Setup(p => p.AddPackageReference("A", new SemanticVersion("1.0"), false, false)).Verifiable();
+
+ var webProjectManager = new WebProjectManager(projectManager.Object, @"x:\")
+ {
+ DoNotAddBindingRedirects = true
+ };
+
+ var packageFile1 = new Mock<IPackageFile>();
+ packageFile1.Setup(p => p.Path).Returns("tools\\install.ps1");
+
+ var packageFile2 = new Mock<IPackageFile>();
+ packageFile2.Setup(p => p.Path).Returns("content\\A.txt");
+
+ var package = new Mock<IPackage>();
+ package.Setup(p => p.Id).Returns("A");
+ package.Setup(p => p.Version).Returns(new SemanticVersion("1.0"));
+ package.Setup(p => p.GetFiles()).Returns(new[] { packageFile1.Object, packageFile2.Object });
+
+ // Act
+ webProjectManager.InstallPackage(package.Object, appDomain: null);
+
+ // Assert
+ projectManager.Verify();
+ }
+
+ [Fact]
+ public void GetLocalRepositoryReturnsPackagesFolderUnderAppData()
+ {
+ // Arrange
+ var siteRoot = "my-site";
+
+ // Act
+ var repositoryFolder = WebProjectManager.GetWebRepositoryDirectory(siteRoot);
+
+ Assert.Equal(repositoryFolder, @"my-site\App_Data\packages");
+ }
+
+ [Fact]
+ public void GetPackagesReturnsAllItemsWhenNoSearchTermIsIncluded()
+ {
+ // Arrange
+ var repository = GetRepository();
+
+ // Act
+ var result = WebProjectManager.GetPackages(repository, String.Empty);
+
+ // Assert
+ Assert.Equal(3, result.Count());
+ }
+
+ [Fact]
+ public void GetPackagesReturnsItemsContainingSomeSearchToken()
+ {
+ // Arrange
+ var repository = GetRepository();
+
+ // Act
+ var result = WebProjectManager.GetPackages(repository, "testing .NET");
+ var package = result.SingleOrDefault();
+
+ // Assert
+ Assert.NotNull(package);
+ Assert.Equal(package.Id, "A");
+ }
+
+ [Fact]
+ public void GetPackagesWithLicenseReturnsAllDependenciesWithRequiresAcceptance()
+ {
+ // Arrange
+ var remoteRepository = GetRepository();
+ var localRepository = new Mock<IPackageRepository>().Object;
+
+ // Act
+ var package = remoteRepository.GetPackages().Find("C").SingleOrDefault();
+ var result = WebProjectManager.GetPackagesRequiringLicenseAcceptance(package, localRepository, remoteRepository);
+
+ // Assert
+ Assert.Equal(2, result.Count());
+ Assert.True(result.Any(c => c.Id == "C"));
+ Assert.True(result.Any(c => c.Id == "B"));
+ }
+
+ [Fact]
+ public void GetPackagesWithLicenseReturnsEmptyResultForPackageThatDoesNotRequireLicenses()
+ {
+ // Arrange
+ var remoteRepository = GetRepository();
+ var localRepository = new Mock<IPackageRepository>().Object;
+
+ // Act
+ var package = remoteRepository.GetPackages().Find("A").SingleOrDefault();
+ var result = WebProjectManager.GetPackagesRequiringLicenseAcceptance(package, localRepository, remoteRepository);
+
+ // Assert
+ Assert.False(result.Any());
+ }
+
+ private static IPackageRepository GetRepository()
+ {
+ Mock<IPackageRepository> repository = new Mock<IPackageRepository>();
+ var packages = new[]
+ {
+ GetPackage("A", desc: "testing"),
+ GetPackage("B", version: "1.1", requiresLicense: true),
+ GetPackage("C", requiresLicense: true, dependencies: new[]
+ {
+ new PackageDependency("B", new VersionSpec { MinVersion = new SemanticVersion("1.0") })
+ })
+ };
+ repository.Setup(c => c.GetPackages()).Returns(packages.AsQueryable());
+
+ return repository.Object;
+ }
+
+ private static IPackage GetPackage(string id, string version = "1.0", string desc = null, bool requiresLicense = false, IEnumerable<PackageDependency> dependencies = null)
+ {
+ Mock<IPackage> package = new Mock<IPackage>();
+ package.SetupGet(c => c.Id).Returns(id);
+ package.SetupGet(c => c.Version).Returns(SemanticVersion.Parse(version));
+ package.SetupGet(c => c.Description).Returns(desc ?? id);
+ package.SetupGet(c => c.RequireLicenseAcceptance).Returns(requiresLicense);
+ package.SetupGet(c => c.LicenseUrl).Returns(new Uri("http://www." + id + ".com"));
+ package.SetupGet(c => c.Dependencies).Returns(dependencies ?? Enumerable.Empty<PackageDependency>());
+ return package.Object;
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Administration.Test/WebProjectSystemTest.cs b/test/System.Web.WebPages.Administration.Test/WebProjectSystemTest.cs
new file mode 100644
index 00000000..8cf94a77
--- /dev/null
+++ b/test/System.Web.WebPages.Administration.Test/WebProjectSystemTest.cs
@@ -0,0 +1,253 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Web.WebPages.Administration.PackageManager;
+using System.Xml.Linq;
+using Moq;
+using NuGet;
+using Xunit;
+
+namespace System.Web.WebPages.Administration.Test
+{
+ public class WebProjectSystemTest
+ {
+ [Fact]
+ public void ResolvePathReturnsAppCodePathIfPathIsSourceFile()
+ {
+ // Arrange
+ var path = "Foo.cs";
+ var webProjectSystem = new WebProjectSystem(@"x:\");
+
+ // Act
+ var resolvedPath = webProjectSystem.ResolvePath(path);
+
+ // Assert
+ Assert.Equal(@"App_Code\Foo.cs", resolvedPath);
+ }
+
+ [Fact]
+ public void ResolvePathReturnsOriginalPathIfSourceFilePathIsAlreadyUnderAppCode()
+ {
+ // Arrange
+ var path = @"App_Code\Foo.cs";
+ var webProjectSystem = new WebProjectSystem(@"x:\");
+
+ // Act
+ var resolvedPath = webProjectSystem.ResolvePath(path);
+
+ // Assert
+ Assert.Equal(path, resolvedPath);
+ }
+
+ [Fact]
+ public void ResolvePathReturnsOriginalPathIfFileIsNotSource()
+ {
+ // Arrange
+ var path = @"Foo.js";
+ var webProjectSystem = new WebProjectSystem(@"x:\");
+
+ // Act
+ var resolvedPath = webProjectSystem.ResolvePath(path);
+
+ // Assert
+ Assert.Equal(path, resolvedPath);
+ }
+
+ [Fact]
+ public void AddPackageWithFrameworkReferenceCreatesWebConfigIfItDoesNotExist()
+ {
+ // Arrange
+ string webConfigPath = @"x:\my-website\web.config";
+ MemoryStream memoryStream = new MemoryStream();
+
+ var fileSystem = new Mock<IFileSystem>();
+ fileSystem.SetupGet(f => f.Root).Returns("x:\\my-website");
+ fileSystem.Setup(f => f.FileExists(It.Is<string>(p => p.Equals(webConfigPath)))).Returns(false).Verifiable();
+ fileSystem.Setup(f => f.AddFile(It.Is<string>(p => p.Equals(webConfigPath)), It.IsAny<Stream>()))
+ .Callback<string, Stream>((_, s) => { s.CopyTo(memoryStream); });
+
+ var references = "System";
+
+ // Act
+ WebProjectSystem.AddReferencesToConfig(fileSystem.Object, references);
+
+ // Assert
+ memoryStream.Seek(0, SeekOrigin.Begin);
+ XDocument document = XDocument.Load(memoryStream);
+
+ var element = document.Root;
+ Assert.Equal(element.Name, "configuration");
+
+ // Use SingleOrDefault to ensure there's exactly one element with that name
+ var assemblies = document.Root
+ .Elements().SingleOrDefault(e => e.Name.ToString().Equals("system.web"))
+ .Elements().SingleOrDefault(e => e.Name.ToString().Equals("compilation"))
+ .Elements().SingleOrDefault(e => e.Name.ToString().Equals("assemblies"));
+
+ Assert.Equal(references, assemblies.Elements().First().Attribute("assembly").Value);
+ }
+
+ [Fact]
+ public void AddPackageWithFrameworkReferenceCreatesWebConfigIfItExistsWithoutAssembliesNode()
+ {
+ // Arrange
+ var webConfigPath = @"x:\my-website\web.config";
+ var webConfigContent = @"<?xml version=""1.0""?>
+ <configuration>
+ <connectionStrings>
+ <add name=""test"" />
+ </connectionStrings>
+ <system.web>
+ <profiles><add name=""awesomeprofile"" /></profiles>
+ </system.web>
+ </configuration>
+
+".AsStream();
+ MemoryStream memoryStream = new MemoryStream();
+
+ var fileSystem = new Mock<IFileSystem>();
+ fileSystem.SetupGet(f => f.Root).Returns("x:\\my-website");
+ fileSystem.Setup(f => f.FileExists(It.Is<string>(p => p.Equals(webConfigPath)))).Returns(true).Verifiable();
+ fileSystem.Setup(f => f.OpenFile(It.Is<string>(p => p.Equals(webConfigPath)))).Returns(webConfigContent);
+ fileSystem.Setup(f => f.AddFile(It.Is<string>(p => p.Equals(webConfigPath)), It.IsAny<Stream>()))
+ .Callback<string, Stream>((_, s) => { s.CopyTo(memoryStream); });
+
+ var references = "System.Data";
+
+ // Act
+ WebProjectSystem.AddReferencesToConfig(fileSystem.Object, references);
+
+ // Assert
+ memoryStream.Seek(0, SeekOrigin.Begin);
+ XDocument document = XDocument.Load(memoryStream);
+
+ var element = document.Root;
+ Assert.Equal(element.Name, "configuration");
+
+ // Use SingleOrDefault to ensure there's exactly one element with that name
+ var assemblies = document.Root
+ .Elements().SingleOrDefault(e => e.Name.ToString().Equals("system.web"))
+ .Elements().SingleOrDefault(e => e.Name.ToString().Equals("compilation"))
+ .Elements().SingleOrDefault(e => e.Name.ToString().Equals("assemblies"));
+
+ Assert.Equal(references, assemblies.Elements().First().Attribute("assembly").Value);
+
+ // Make sure the original web.config content is unaffected
+ Assert.Equal("test", document.Root
+ .Elements().SingleOrDefault(e => e.Name.ToString().Equals("connectionStrings"))
+ .Elements().SingleOrDefault(e => e.Name.ToString().Equals("add"))
+ .Attributes().SingleOrDefault(e => e.Name.ToString().Equals("name")).Value);
+
+ Assert.Equal("awesomeprofile", document.Root.Element("system.web").Element("profiles").Element("add").Attribute("name").Value);
+ }
+
+ [Fact]
+ public void AddPackageWithFrameworkReferenceDoesNotAffectWebConfigIfReferencesAlreadyExist()
+ {
+ // Arrange
+ var webConfigPath = @"x:\my-website\web.config";
+ var memoryStream = new NeverCloseMemoryStream(@"<?xml version=""1.0""?>
+ <configuration>
+ <connectionStrings>
+ <add name=""test"" />
+ </connectionStrings>
+ <system.web>
+ <compilation>
+ <assemblies>
+ <add assembly=""System.Data, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"" />
+ </assemblies>
+ </compilation>
+ <profiles><add name=""awesomeprofile"" /></profiles>
+ </system.web>
+ </configuration>
+
+");
+
+ var fileSystem = new Mock<IFileSystem>();
+ fileSystem.SetupGet(f => f.Root).Returns("x:\\my-website");
+ fileSystem.Setup(f => f.FileExists(It.Is<string>(p => p.Equals(webConfigPath)))).Returns(true);
+ fileSystem.Setup(f => f.OpenFile(It.Is<string>(p => p.Equals(webConfigPath)))).Returns(() =>
+ {
+ memoryStream.Seek(0, SeekOrigin.Begin);
+ return memoryStream;
+ });
+ fileSystem.Setup(f => f.AddFile(It.Is<string>(p => p.Equals(webConfigPath)), It.IsAny<Stream>()))
+ .Callback<string, Stream>((_, stream) => { memoryStream = new NeverCloseMemoryStream(stream.ReadToEnd()); });
+
+ // Act
+ WebProjectSystem.AddReferencesToConfig(fileSystem.Object, "System.Data");
+ WebProjectSystem.AddReferencesToConfig(fileSystem.Object, "Microsoft.Abstractions");
+
+ // Assert
+ memoryStream.Seek(0, SeekOrigin.Begin);
+ XDocument document = XDocument.Load(memoryStream);
+
+ var element = document.Root;
+ Assert.Equal(element.Name, "configuration");
+
+ // Use SingleOrDefault to ensure there's exactly one element with that name
+ var assemblies = document.Root
+ .Elements().SingleOrDefault(e => e.Name.ToString().Equals("system.web"))
+ .Elements().SingleOrDefault(e => e.Name.ToString().Equals("compilation"))
+ .Elements().SingleOrDefault(e => e.Name.ToString().Equals("assemblies"));
+
+ Assert.Equal(2, assemblies.Elements("add").Count());
+ Assert.Equal("System.Data, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35",
+ assemblies.Elements().First().Attribute("assembly").Value);
+ Assert.Equal("Microsoft.Abstractions",
+ assemblies.Elements().Last().Attribute("assembly").Value);
+
+ // Make sure the original web.config content is unaffected
+ Assert.Equal("test", document.Root
+ .Elements().SingleOrDefault(e => e.Name.ToString().Equals("connectionStrings"))
+ .Elements().SingleOrDefault(e => e.Name.ToString().Equals("add"))
+ .Attributes().SingleOrDefault(e => e.Name.ToString().Equals("name")).Value);
+
+ Assert.Equal("awesomeprofile", document.Root.Element("system.web").Element("profiles").Element("add").Attribute("name").Value);
+ }
+
+ [Fact]
+ public void ResolveAssemblyPartialNameForCommonAssemblies()
+ {
+ // Arrange
+ var commonAssemblies = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
+ {
+ { "System.Data", "System.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" },
+ { "System.Data.Linq", "System.Data.Linq, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" },
+ { "System.Net", "System.Net, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" },
+ { "System.Runtime.Caching", "System.Runtime.Caching, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" },
+ { "System.Xml", "System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" },
+ { "System.Web.DynamicData", "System.Web.DynamicData, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" },
+ };
+
+ // Act and Assert
+ foreach (var item in commonAssemblies)
+ {
+ var resolvedName = WebProjectSystem.ResolvePartialAssemblyName(item.Key);
+
+ Assert.Equal(item.Value, resolvedName);
+ }
+ }
+
+ private static IFileSystem GetFileSystem()
+ {
+ var fileSystem = new Mock<IFileSystem>();
+ fileSystem.Setup(c => c.Root).Returns(@"X:\packages\");
+ return fileSystem.Object;
+ }
+
+ private class NeverCloseMemoryStream : MemoryStream
+ {
+ public NeverCloseMemoryStream(string content)
+ : base(Encoding.UTF8.GetBytes(content))
+ {
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ // Do nothing
+ }
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Administration.Test/packages.config b/test/System.Web.WebPages.Administration.Test/packages.config
new file mode 100644
index 00000000..bec0dc35
--- /dev/null
+++ b/test/System.Web.WebPages.Administration.Test/packages.config
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="Moq" version="4.0.10827" />
+ <package id="NuGet.Core" version="1.6.2" />
+ <package id="xunit" version="1.9.0.1566" />
+ <package id="xunit.extensions" version="1.9.0.1566" />
+</packages> \ No newline at end of file
diff --git a/test/System.Web.WebPages.Deployment.Test/App.Config b/test/System.Web.WebPages.Deployment.Test/App.Config
new file mode 100644
index 00000000..49cc43e1
--- /dev/null
+++ b/test/System.Web.WebPages.Deployment.Test/App.Config
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<configuration>
+</configuration> \ No newline at end of file
diff --git a/test/System.Web.WebPages.Deployment.Test/AssemblyUtilsTest.cs b/test/System.Web.WebPages.Deployment.Test/AssemblyUtilsTest.cs
new file mode 100644
index 00000000..7cf719f6
--- /dev/null
+++ b/test/System.Web.WebPages.Deployment.Test/AssemblyUtilsTest.cs
@@ -0,0 +1,266 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using Xunit;
+
+namespace System.Web.WebPages.Deployment.Test
+{
+ public class AssemblyUtilsTest
+ {
+ [Fact]
+ public void GetMaxAssemblyVersionReturnsMaximumAvailableVersion()
+ {
+ // Arrange
+ var assemblies = new[]
+ {
+ new AssemblyName("System.Web.WebPages.Deployment, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"),
+ new AssemblyName("System.Web.WebPages.Deployment, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"),
+ new AssemblyName("System.Web.WebPages.Deployment, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35")
+ };
+
+ // Act
+ var maxVersion = AssemblyUtils.GetMaxWebPagesVersion(assemblies);
+
+ // Assert
+ Assert.Equal(new Version("2.1.0.0"), maxVersion);
+ }
+
+ [Fact]
+ public void GetMaxAssemblyVersionMatchesExactName()
+ {
+ // Arrange
+ var assemblies = new[]
+ {
+ new AssemblyName("System.Web.WebPages.Deployment, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"),
+ new AssemblyName("System.Web.WebPages.Development, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"),
+ new AssemblyName("System.Web.WebPages.Deployment, Version=2.1.0.0, Culture=neutral, PublicKeyToken=7777777777777777"),
+ new AssemblyName("System.Web.WebPages.Deployment, Version=2.3.0.0, Culture=en-US, PublicKeyToken=31bf3856ad364e35"),
+ new AssemblyName("System.Web.WebPages.Deployment, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35")
+ };
+
+ // Act
+ var maxVersion = AssemblyUtils.GetMaxWebPagesVersion(assemblies);
+
+ // Assert
+ Assert.Equal(new Version("2.0.0.0"), maxVersion);
+ }
+
+ [Fact]
+ public void GetVersionFromBinReturnsNullIfNoFileWithDeploymentAssemblyNameIsFoundInBin()
+ {
+ // Arrange
+ var binDirectory = @"X:\test\project";
+ TestFileSystem fileSystem = new TestFileSystem();
+
+ // Act
+ var binVersion = AssemblyUtils.GetVersionFromBin(binDirectory, fileSystem, getAssemblyNameThunk: null);
+
+ // Assert
+ Assert.Null(binVersion);
+ }
+
+ [Fact]
+ public void GetVersionFromBinReturnsVersionFromBinIfLower()
+ {
+ // Arrange
+ var binDirectory = @"X:\test\project";
+ TestFileSystem fileSystem = new TestFileSystem();
+ fileSystem.AddFile(Path.Combine(binDirectory, "System.Web.WebPages.Deployment.dll"));
+ Func<string, AssemblyName> getAssembyName = _ => new AssemblyName("System.Web.WebPages.Deployment, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");
+
+ // Act
+ var binVersion = AssemblyUtils.GetVersionFromBin(binDirectory, fileSystem, getAssembyName);
+
+ // Assert
+ Assert.Equal(new Version("1.0.0.0"), binVersion);
+ }
+
+ [Fact]
+ public void GetVersionFromBinReturnsVersionFromBinIfSameVersion()
+ {
+ // Arrange
+ var binDirectory = @"X:\test\project";
+ TestFileSystem fileSystem = new TestFileSystem();
+ fileSystem.AddFile(Path.Combine(binDirectory, "System.Web.WebPages.Deployment.dll"));
+ Func<string, AssemblyName> getAssembyName = _ => new AssemblyName("System.Web.WebPages.Deployment, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");
+
+ // Act
+ var binVersion = AssemblyUtils.GetVersionFromBin(binDirectory, fileSystem, getAssembyName);
+
+ // Assert
+ Assert.Equal(new Version("2.0.0.0"), binVersion);
+ }
+
+ [Fact]
+ public void GetVersionFromBinReturnsVersionFromBinIfHigherVersion()
+ {
+ // Arrange
+ var binDirectory = @"X:\test\project";
+ TestFileSystem fileSystem = new TestFileSystem();
+ fileSystem.AddFile(Path.Combine(binDirectory, "System.Web.WebPages.Deployment.dll"));
+ Func<string, AssemblyName> getAssembyName = _ => new AssemblyName("System.Web.WebPages.Deployment, Version=8.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");
+
+ // Act
+ var binVersion = AssemblyUtils.GetVersionFromBin(binDirectory, fileSystem, getAssembyName);
+
+ // Assert
+ Assert.Equal(new Version("8.0.0.0"), binVersion);
+ }
+
+ [Fact]
+ public void GetVersionFromBinReturnsNullIfFileInBinIsNotAValidBinary()
+ {
+ // Arrange
+ var binDirectory = @"X:\test\project";
+ TestFileSystem fileSystem = new TestFileSystem();
+ fileSystem.AddFile(Path.Combine(binDirectory, "System.Web.WebPages.Deployment.dll"));
+ Func<string, AssemblyName> getAssembyName = _ => { throw new FileLoadException(); };
+
+ // Act
+ var binVersion = AssemblyUtils.GetVersionFromBin(binDirectory, fileSystem, getAssembyName);
+
+ // Assert
+ Assert.Null(binVersion);
+ }
+
+ [Fact]
+ public void GetAssembliesForVersionReturnsCorrectSetForV1()
+ {
+ // Arrange
+ var expectedAssemblies = new[]
+ {
+ "Microsoft.Web.Infrastructure, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35",
+ "System.Web.Razor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35",
+ "System.Web.Helpers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35",
+ "System.Web.WebPages, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35",
+ "System.Web.WebPages.Administration, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35",
+ "System.Web.WebPages.Razor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35",
+ "WebMatrix.Data, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35",
+ "WebMatrix.WebData, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
+ };
+
+ // Act
+ var assemblies = AssemblyUtils.GetAssembliesForVersion(new Version("1.0.0.0"))
+ .Select(c => c.ToString())
+ .ToArray();
+
+ // Assert
+ Assert.Equal(expectedAssemblies, assemblies);
+ }
+
+ [Fact]
+ public void GetAssembliesForVersionReturnsCorrectSetForVCurrent()
+ {
+ // Arrange
+ var expectedAssemblies = new[]
+ {
+ "Microsoft.Web.Infrastructure, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35",
+ "System.Web.Razor, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35",
+ "System.Web.Helpers, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35",
+ "System.Web.WebPages, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35",
+ "System.Web.WebPages.Administration, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35",
+ "System.Web.WebPages.Razor, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35",
+ "WebMatrix.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35",
+ "WebMatrix.WebData, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35",
+ };
+
+ // Act
+ var assemblies = AssemblyUtils.GetAssembliesForVersion(new Version("2.0.0.0"))
+ .Select(c => c.ToString())
+ .ToArray();
+
+ // Assert
+ Assert.Equal(expectedAssemblies, assemblies);
+ }
+
+ [Fact]
+ public void GetMatchingAssembliesReturnsEmptyDictionaryIfNoReferencesMatchWebPagesAssemblies()
+ {
+ // Arrange
+ var assemblyReferences = new Dictionary<string, IEnumerable<string>>
+ {
+ { @"x:\site\bin\A.dll", new List<string> { "mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null" }},
+ { @"x:\site\bin\B.dll", new List<string> { "System.Web.Mvc, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" }},
+ };
+
+ var a = "1";
+ var b = "2";
+
+ var c = new { a, b };
+ Console.WriteLine(c.a);
+
+ // Act
+ var referencedAssemblies = AssemblyUtils.GetAssembliesMatchingOtherVersions(assemblyReferences);
+
+ // Assert
+ Assert.Empty(referencedAssemblies);
+ }
+
+ [Fact]
+ public void GetMatchingAssembliesReturnsReferencingAssemblyAndWebPagesVersionForMatchingReferences()
+ {
+ // Arrange
+ var assemblyReferences = new Dictionary<string, IEnumerable<string>>
+ {
+ { @"x:\site\bin\A.dll", new[] { "mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null" }},
+ { @"x:\site\bin\B.dll", new[]
+ {
+ "mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null",
+ "System.Web.WebPages, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35",
+ "System.Web.Helpers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35",
+ }
+ },
+ };
+
+ // Act
+ var referencedAssemblies = AssemblyUtils.GetAssembliesMatchingOtherVersions(assemblyReferences);
+
+ // Assert
+ Assert.Equal(1, referencedAssemblies.Count);
+ Assert.Equal(@"x:\site\bin\B.dll", referencedAssemblies.Single().Key);
+ Assert.Equal(new Version("1.0.0.0"), referencedAssemblies.Single().Value);
+ }
+
+ [Fact]
+ public void GetMatchingAssembliesFiltersWebPagesVersionsThatMatch()
+ {
+ // Arrange
+ var assemblyReferences = new Dictionary<string, IEnumerable<string>>
+ {
+ { @"x:\site\bin\A.dll", new[] { "mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null" }},
+ { @"x:\site\bin\B.dll", new[]
+ {
+ "mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null",
+ String.Format(CultureInfo.InvariantCulture, "System.Web.WebPages, Version={0}, Culture=neutral, PublicKeyToken=31bf3856ad364e35", AssemblyUtils.ThisAssemblyName.Version),
+ String.Format(CultureInfo.InvariantCulture, "System.Web.Helpers, Version={0}, Culture=neutral, PublicKeyToken=31bf3856ad364e35", AssemblyUtils.ThisAssemblyName.Version)
+ }
+ },
+ { @"x:\site\bin\C.dll", new[]
+ {
+ "mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null",
+ "System.Web.WebPages.Razor, Version=1.2.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35",
+ "System.Web.WebPages.Razor, Version=1.3.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35",
+ }
+ },
+ };
+
+ // Act
+ var referencedAssemblies = AssemblyUtils.GetAssembliesMatchingOtherVersions(assemblyReferences);
+
+ // Assert
+ Assert.Equal(1, referencedAssemblies.Count);
+ Assert.Equal(@"x:\site\bin\C.dll", referencedAssemblies.Single().Key);
+ Assert.Equal(new Version("1.2.0.0"), referencedAssemblies.Single().Value);
+ }
+
+ private static void EnsureDirectory(string directory)
+ {
+ if (!Directory.Exists(directory))
+ {
+ Directory.CreateDirectory(directory);
+ }
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Deployment.Test/DeploymentUtil.cs b/test/System.Web.WebPages.Deployment.Test/DeploymentUtil.cs
new file mode 100644
index 00000000..514b5116
--- /dev/null
+++ b/test/System.Web.WebPages.Deployment.Test/DeploymentUtil.cs
@@ -0,0 +1,13 @@
+using System.IO;
+
+namespace System.Web.WebPages.Deployment.Test
+{
+ internal static class DeploymentUtil
+ {
+ public static string GetBinDirectory()
+ {
+ var tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+ return Path.Combine(tempDirectory, "bin");
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Deployment.Test/PreApplicationStartCodeTest.cs b/test/System.Web.WebPages.Deployment.Test/PreApplicationStartCodeTest.cs
new file mode 100644
index 00000000..464d977a
--- /dev/null
+++ b/test/System.Web.WebPages.Deployment.Test/PreApplicationStartCodeTest.cs
@@ -0,0 +1,510 @@
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Web.WebPages.TestUtils;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.WebPages.Deployment.Test
+{
+ public class PreApplicationStartCodeTest
+ {
+ private const string DeploymentVersionFile = "System.Web.WebPages.Deployment";
+ private static readonly Version MaxVersion = new Version(2, 0, 0, 0);
+
+ [Fact]
+ public void PreApplicationStartCodeDoesNothingIfWebPagesIsExplicitlyDisabled()
+ {
+ // Arrange
+ Version loadedVersion = null;
+ bool registeredForChangeNotification = false;
+ IEnumerable<AssemblyName> loadedAssemblies = GetAssemblies("1", "2");
+
+ var fileSystem = new TestFileSystem();
+ var buildManager = new TestBuildManager();
+ var nameValueCollection = GetAppSettings(enabled: false, webPagesVersion: null);
+ Action<Version> loadWebPages = (version) => { loadedVersion = version; };
+ Action registerForChange = () => { registeredForChangeNotification = true; };
+
+ // Act
+ bool loaded = PreApplicationStartCode.StartCore(fileSystem, "", "bin", nameValueCollection, loadedAssemblies, buildManager, loadWebPages, registerForChange, null);
+
+ // Assert
+ Assert.False(loaded);
+ Assert.Null(loadedVersion);
+ Assert.False(registeredForChangeNotification);
+ Assert.Equal(0, buildManager.Stream.Length);
+ }
+
+ [Fact]
+ public void PreApplicationStartCodeUsesVersionSpecifiedInConfigIfWebPagesIsImplicitlyEnabled()
+ {
+ // Arrange
+ Version loadedVersion = null;
+ bool registeredForChangeNotification = false;
+ IEnumerable<AssemblyName> loadedAssemblies = GetAssemblies("1.12.123.1234", "2.0.0.0");
+ Version webPagesVersion = new Version("1.12.123.1234");
+
+ var fileSystem = new TestFileSystem();
+ fileSystem.AddFile("Default.cshtml");
+ var buildManager = new TestBuildManager();
+ var nameValueCollection = GetAppSettings(enabled: null, webPagesVersion: webPagesVersion);
+ Action<Version> loadWebPages = (version) => { loadedVersion = version; };
+ Action registerForChange = () => { registeredForChangeNotification = true; };
+
+ // Act
+ bool loaded = PreApplicationStartCode.StartCore(fileSystem, "", "bin", nameValueCollection, loadedAssemblies, buildManager, loadWebPages, registerForChange, getAssemblyNameThunk: null);
+
+ // Assert
+ Assert.True(loaded);
+ Assert.Equal(webPagesVersion, loadedVersion);
+ Assert.False(registeredForChangeNotification);
+ VerifyVersionFile(buildManager, webPagesVersion);
+ }
+
+ [Fact]
+ public void PreApplicationStartCodeDoesNotLoadCurrentWebPagesIfOnlyVersionIsListedInConfigAndNoFilesAreFoundInSiteRoot()
+ {
+ // Arrange
+ Version loadedVersion = null;
+ bool registeredForChangeNotification = false;
+ Version webPagesVersion = AssemblyUtils.ThisAssemblyName.Version;
+ IEnumerable<AssemblyName> loadedAssemblies = GetAssemblies("2.0.0.0");
+
+ var fileSystem = new TestFileSystem();
+ var buildManager = new TestBuildManager();
+ var nameValueCollection = GetAppSettings(enabled: null, webPagesVersion: webPagesVersion);
+ Action<Version> loadWebPages = (version) => { loadedVersion = version; };
+ Action registerForChange = () => { registeredForChangeNotification = true; };
+
+ // Arrange
+ bool loaded = PreApplicationStartCode.StartCore(fileSystem, "", "bin", nameValueCollection, loadedAssemblies, buildManager, loadWebPages, registerForChange, null);
+
+ // Assert
+ Assert.False(loaded);
+ Assert.Null(loadedVersion);
+ Assert.True(registeredForChangeNotification);
+ Assert.Equal(0, buildManager.Stream.Length);
+ }
+
+ [Fact]
+ public void PreApplicationStartCodeRegistersForChangeNotificationIfNotExplicitlyDisabledAndNoFilesFoundInSiteRoot()
+ {
+ // Arrange
+ Version loadedVersion = null;
+ bool registeredForChangeNotification = false;
+ IEnumerable<AssemblyName> loadedAssemblies = GetAssemblies("2.0.0.0");
+
+ var fileSystem = new TestFileSystem();
+ var buildManager = new TestBuildManager();
+ var nameValueCollection = GetAppSettings(enabled: null, webPagesVersion: null);
+ Action<Version> loadWebPages = (version) => { loadedVersion = version; };
+ Action registerForChange = () => { registeredForChangeNotification = true; };
+
+ // Act
+ bool loaded = PreApplicationStartCode.StartCore(fileSystem, "", "bin", nameValueCollection, loadedAssemblies, buildManager, loadWebPages, registerForChange, null);
+
+ // Assert
+ Assert.False(loaded);
+ Assert.Null(loadedVersion);
+ Assert.True(registeredForChangeNotification);
+ Assert.Equal(0, buildManager.Stream.Length);
+ }
+
+ [Fact]
+ public void PreApplicationStartCodeDoesNothingIfV1IsAvailableInBinAndSiteIsExplicitlyEnabled()
+ {
+ // Arrange
+ Version loadedVersion = null;
+ bool registeredForChangeNotification = false;
+ var v1Version = new Version("1.0.0.0");
+ IEnumerable<AssemblyName> loadedAssemblies = GetAssemblies("1.0.0.0", "2.0.0.0");
+
+ var binDirectory = DeploymentUtil.GetBinDirectory();
+
+ var fileSystem = new TestFileSystem();
+ fileSystem.AddFile(Path.Combine(binDirectory, "System.Web.WebPages.Deployment.dll"));
+ var buildManager = new TestBuildManager();
+ var nameValueCollection = GetAppSettings(enabled: true, webPagesVersion: null);
+ Action<Version> loadWebPages = (version) => { loadedVersion = version; };
+ Action registerForChange = () => { registeredForChangeNotification = true; };
+ Func<string, AssemblyName> getAssembyName = _ => new AssemblyName("System.Web.WebPages.Deployment, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");
+
+ // Act
+ bool loaded = PreApplicationStartCode.StartCore(fileSystem, "", binDirectory, nameValueCollection, loadedAssemblies, buildManager, loadWebPages, registerForChange, getAssembyName);
+
+ // Assert
+ Assert.False(loaded);
+ Assert.Null(loadedVersion);
+ Assert.False(registeredForChangeNotification);
+ Assert.Equal(0, buildManager.Stream.Length);
+ }
+
+ [Fact]
+ public void PreApplicationStartCodeDoesNothingIfV1IsAvailableInBinAndFileExistsInRootOfWebSite()
+ {
+ // Arrange
+ Version loadedVersion = null;
+ bool registeredForChangeNotification = false;
+ var v1Version = new Version("1.0.0.0");
+ IEnumerable<AssemblyName> loadedAssemblies = GetAssemblies("1.0.0.0", "2.0.0.0");
+
+ var binDirectory = DeploymentUtil.GetBinDirectory();
+
+ var fileSystem = new TestFileSystem();
+ var buildManager = new TestBuildManager();
+ fileSystem.AddFile("Default.cshtml");
+ fileSystem.AddFile(Path.Combine(binDirectory, "System.Web.WebPages.Deployment.dll"));
+ var nameValueCollection = GetAppSettings(enabled: null, webPagesVersion: null);
+ Action<Version> loadWebPages = (version) => { loadedVersion = version; };
+ Action registerForChange = () => { registeredForChangeNotification = true; };
+ Func<string, AssemblyName> getAssembyName = _ => new AssemblyName("System.Web.WebPages.Deployment, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");
+
+ // Act
+ bool loaded = PreApplicationStartCode.StartCore(fileSystem, "", binDirectory, nameValueCollection, loadedAssemblies, buildManager, loadWebPages, registerForChange, getAssembyName);
+
+ // Assert
+ Assert.False(loaded);
+ Assert.Null(loadedVersion);
+ Assert.False(registeredForChangeNotification);
+ Assert.Equal(0, buildManager.Stream.Length);
+ }
+
+ [Fact]
+ public void PreApplicationStartCodeDoesNothingIfItIsAvailableInBinAndFileExistsInRootOfWebSite()
+ {
+ // Arrange
+ Version loadedVersion = null;
+ bool registeredForChangeNotification = false;
+ var webPagesVersion = AssemblyUtils.ThisAssemblyName.Version;
+ IEnumerable<AssemblyName> loadedAssemblies = GetAssemblies(AssemblyUtils.ThisAssemblyName.Version.ToString());
+
+ var fileSystem = new TestFileSystem();
+ var binDirectory = DeploymentUtil.GetBinDirectory();
+
+ var buildManager = new TestBuildManager();
+ fileSystem.AddFile("Default.vbhtml");
+ fileSystem.AddFile(Path.Combine(binDirectory, "System.Web.WebPages.Deployment.dll"));
+ var nameValueCollection = GetAppSettings(enabled: null, webPagesVersion: null);
+ Action<Version> loadWebPages = (version) => { loadedVersion = version; };
+ Action registerForChange = () => { registeredForChangeNotification = true; };
+ Func<string, AssemblyName> getAssembyName = _ => new AssemblyName("System.Web.WebPages.Deployment, Version=" + AssemblyUtils.ThisAssemblyName.Version.ToString() + ", Culture=neutral, PublicKeyToken=31bf3856ad364e35");
+
+ // Act
+ bool loaded = PreApplicationStartCode.StartCore(fileSystem, "", binDirectory, nameValueCollection, loadedAssemblies, buildManager, loadWebPages, registerForChange, getAssembyName);
+
+ // Assert
+ Assert.False(loaded);
+ Assert.Null(loadedVersion);
+ Assert.False(registeredForChangeNotification);
+ Assert.Equal(0, buildManager.Stream.Length);
+ }
+
+ [Fact]
+ public void PreApplicationStartCodeLoadsMaxVersionIfNoVersionIsSpecifiedAndCurrentAssemblyIsTheMaximumVersionAvailable()
+ {
+ // Arrange
+ Version loadedVersion = null;
+ bool registeredForChangeNotification = false;
+ var webPagesVersion = AssemblyUtils.ThisAssemblyName.Version;
+ var v1Version = new Version("1.0.0.0");
+ IEnumerable<AssemblyName> loadedAssemblies = GetAssemblies("1.0.0.0", "2.0.0.0");
+
+ // Note: For this test to work with future versions we would need to create corresponding embedded resources with that version in it.
+ var fileSystem = new TestFileSystem();
+ var buildManager = new TestBuildManager();
+ fileSystem.AddFile("Index.cshtml");
+ var nameValueCollection = GetAppSettings(enabled: null, webPagesVersion: null);
+ Action<Version> loadWebPages = (version) => { loadedVersion = version; };
+ Action registerForChange = () => { registeredForChangeNotification = true; };
+
+ // Act
+ bool loaded = PreApplicationStartCode.StartCore(fileSystem, "", "bin", nameValueCollection, loadedAssemblies, buildManager, loadWebPages, registerForChange, null);
+
+ // Assert
+ Assert.True(loaded);
+ Assert.Equal(MaxVersion, loadedVersion);
+ Assert.False(registeredForChangeNotification);
+ VerifyVersionFile(buildManager, MaxVersion);
+ }
+
+ [Fact]
+ public void PreApplicationStartCodeDoesNotLoadIfAHigherVersionIsAvailableInBin()
+ {
+ // Arrange
+ Version loadedVersion = null;
+ bool registeredForChangeNotification = false;
+ IEnumerable<AssemblyName> loadedAssemblies = GetAssemblies("2.0.0.0", "8.0.0.0");
+
+ var binDirectory = DeploymentUtil.GetBinDirectory();
+
+ var fileSystem = new TestFileSystem();
+ fileSystem.AddFile("Index.cshtml");
+ fileSystem.AddFile(Path.Combine(binDirectory, "System.Web.WebPages.Deployment.dll"));
+ var buildManager = new TestBuildManager();
+ var nameValueCollection = GetAppSettings(enabled: null, webPagesVersion: null);
+ Action<Version> loadWebPages = (version) => { loadedVersion = version; };
+ Action registerForChange = () => { registeredForChangeNotification = true; };
+ Func<string, AssemblyName> getAssembyName = _ => new AssemblyName("System.Web.WebPages.Deployment, Version=8.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");
+
+ // Act
+ bool loaded = PreApplicationStartCode.StartCore(fileSystem, "", binDirectory, nameValueCollection, loadedAssemblies, buildManager, loadWebPages, registerForChange, getAssembyName);
+
+ // Assert
+ Assert.False(loaded);
+ Assert.Null(loadedVersion);
+ Assert.False(registeredForChangeNotification);
+ Assert.Equal(0, buildManager.Stream.Length);
+ }
+
+ [Fact]
+ public void PreApplicationStartCodeDoesNotLoadIfAHigherVersionIsAvailableInGac()
+ {
+ // Arrange
+ Version loadedVersion = null;
+ bool registeredForChangeNotification = false;
+ // Hopefully we'd have figured out a better way to load Plan9 by v8.
+ var webPagesVersion = new Version("8.0.0.0");
+ IEnumerable<AssemblyName> loadedAssemblies = GetAssemblies("1.0.0.0", "2.0.0.0", "8.0.0.0");
+
+ var fileSystem = new TestFileSystem();
+ fileSystem.AddFile("Index.cshtml");
+ var buildManager = new TestBuildManager();
+ var nameValueCollection = GetAppSettings(enabled: null, webPagesVersion: null);
+ Action<Version> loadWebPages = (version) => { loadedVersion = version; };
+ Action registerForChange = () => { registeredForChangeNotification = true; };
+
+ // Act
+ bool loaded = PreApplicationStartCode.StartCore(fileSystem, "", "bin", nameValueCollection, loadedAssemblies, buildManager, loadWebPages, registerForChange, null);
+
+ // Assert
+ Assert.False(loaded);
+ Assert.Null(loadedVersion);
+ Assert.False(registeredForChangeNotification);
+ Assert.Equal(0, buildManager.Stream.Length);
+ }
+
+ [Fact]
+ public void PreApplicationStartCodeForcesRecompileIfPreviousVersionIsNotTheSameAsCurrentVersion()
+ {
+ // Arrange
+ Version loadedVersion = null;
+ bool registeredForChangeNotification = false;
+ IEnumerable<AssemblyName> loadedAssemblies = GetAssemblies("2.0.0.0");
+
+ var fileSystem = new TestFileSystem();
+ fileSystem.AddFile("Index.cshtml");
+ var buildManager = new TestBuildManager();
+ var content = "1.0.0.0" + Environment.NewLine;
+ buildManager.Stream = new MemoryStream(Encoding.Default.GetBytes(content));
+
+ var nameValueCollection = GetAppSettings(enabled: null, webPagesVersion: new Version("2.0.0.0"));
+ Action<Version> loadWebPages = (version) => { loadedVersion = version; };
+ Action registerForChange = () => { registeredForChangeNotification = true; };
+
+ // Act
+ var ex = Assert.Throws<HttpCompileException>(() =>
+ PreApplicationStartCode.StartCore(fileSystem, "", @"site\bin", nameValueCollection, loadedAssemblies, buildManager, loadWebPages, registerForChange, null)
+ );
+
+ // Assert
+ Assert.Equal("Changes were detected in the Web Pages runtime version that require your application to be recompiled. Refresh your browser window to continue.", ex.Message);
+ Assert.Equal(ex.Data["WebPages.VersionChange"], true);
+ Assert.False(registeredForChangeNotification);
+ VerifyVersionFile(buildManager, new Version("2.0.0.0"));
+ Assert.True(fileSystem.FileExists(@"site\bin\WebPagesRecompilation.deleteme"));
+ }
+
+ [Fact]
+ public void PreApplicationStartCodeDoesNotForceRecompileIfNewVersionIsV1AndCurrentAssemblyIsNotMaxVersion()
+ {
+ // Arrange
+ Version loadedVersion = null;
+ bool registeredForChangeNotification = false;
+ IEnumerable<AssemblyName> loadedAssemblies = GetAssemblies("2.0.0.0", "5.0.0.0");
+
+ var fileSystem = new TestFileSystem();
+ fileSystem.AddFile("Index.cshtml");
+ var buildManager = new TestBuildManager();
+ var content = AssemblyUtils.ThisAssemblyName.Version + Environment.NewLine;
+ buildManager.Stream = new MemoryStream(Encoding.Default.GetBytes(content));
+
+ var nameValueCollection = GetAppSettings(enabled: null, webPagesVersion: new Version("1.0.0"));
+ Action<Version> loadWebPages = (version) => { loadedVersion = version; };
+ Action registerForChange = () => { registeredForChangeNotification = true; };
+
+ // Act
+ bool loaded = PreApplicationStartCode.StartCore(fileSystem, "", @"site\bin", nameValueCollection, loadedAssemblies, buildManager, loadWebPages, registerForChange, null);
+
+ // Assert
+ Assert.False(loaded);
+ Assert.False(registeredForChangeNotification);
+ VerifyVersionFile(buildManager, AssemblyUtils.ThisAssemblyName.Version);
+ Assert.False(fileSystem.FileExists(@"site\bin\WebPagesRecompilation.deleteme"));
+ }
+
+ [Fact]
+ public void PreApplicationStartCodeThrowsIfWebPagesIsInBinAndDifferentVersionIsSpecifiedInConfig()
+ {
+ // Arrange
+ Version loadedVersion = null;
+ bool registeredForChangeNotification = false;
+ IEnumerable<AssemblyName> loadedAssemblies = GetAssemblies("1.0.0.0", "2.0.0.0");
+
+ var binDirectory = DeploymentUtil.GetBinDirectory();
+
+ var fileSystem = new TestFileSystem();
+ fileSystem.AddFile("Index.cshtml");
+ fileSystem.AddFile(Path.Combine(binDirectory, "System.Web.WebPages.Deployment.dll"));
+ var buildManager = new TestBuildManager();
+ var content = AssemblyUtils.ThisAssemblyName.Version + Environment.NewLine;
+ buildManager.Stream = new MemoryStream(Encoding.Default.GetBytes(content));
+
+ var nameValueCollection = GetAppSettings(enabled: null, webPagesVersion: new Version("2.0.0"));
+ Action<Version> loadWebPages = (version) => { loadedVersion = version; };
+ Action registerForChange = () => { registeredForChangeNotification = true; };
+ Func<string, AssemblyName> getAssembyName = _ => new AssemblyName("System.Web.WebPages.Deployment, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");
+
+ // Act and Assert
+ Assert.Throws<InvalidOperationException>(() =>
+ PreApplicationStartCode.StartCore(fileSystem, "", binDirectory, nameValueCollection, loadedAssemblies, buildManager, loadWebPages, registerForChange, getAssembyName),
+ @"Conflicting versions of ASP.NET Web Pages detected: specified version is ""2.0.0.0"", but the version in bin is ""1.0.0.0"". To continue, remove files from the application's bin directory or remove the version specification in web.config."
+ );
+
+ Assert.False(registeredForChangeNotification);
+ Assert.Null(loadedVersion);
+ }
+
+ [Fact]
+ public void PreApplicationStartCodeThrowsIfVersionIsSpecifiedInConfigAndDifferentVersionExistsInBin()
+ {
+ // Arrange
+ Version loadedVersion = null;
+
+ var binDirectory = DeploymentUtil.GetBinDirectory();
+ IEnumerable<AssemblyName> loadedAssemblies = GetAssemblies("1.0.0.0", AssemblyUtils.ThisAssemblyName.Version.ToString());
+
+ var fileSystem = new TestFileSystem();
+ fileSystem.AddFile("Index.cshtml");
+ fileSystem.AddFile(Path.Combine(binDirectory, "System.Web.WebPages.Deployment.dll"));
+ var buildManager = new TestBuildManager();
+ var content = AssemblyUtils.ThisAssemblyName.Version + Environment.NewLine;
+ buildManager.Stream = new MemoryStream(Encoding.Default.GetBytes(content));
+
+ var nameValueCollection = GetAppSettings(enabled: null, webPagesVersion: new Version("1.0.0"));
+ Action<Version> loadWebPages = (version) => { loadedVersion = version; };
+ Action registerForChange = () => { };
+ Func<string, AssemblyName> getAssembyName = _ => new AssemblyName("System.Web.WebPages.Deployment, Version=" + AssemblyUtils.ThisAssemblyName.Version + ", Culture=neutral, PublicKeyToken=31bf3856ad364e35");
+
+ // Act and Assert
+ Assert.Throws<InvalidOperationException>(() =>
+ PreApplicationStartCode.StartCore(fileSystem, "", binDirectory, nameValueCollection, loadedAssemblies, buildManager: buildManager, loadWebPages: loadWebPages, registerForChangeNotification: registerForChange, getAssemblyNameThunk: getAssembyName),
+ String.Format(@"Conflicting versions of ASP.NET Web Pages detected: specified version is ""1.0.0.0"", but the version in bin is ""{0}"". To continue, remove files from the application's bin directory or remove the version specification in web.config.",
+ AssemblyUtils.ThisAssemblyName.Version));
+ }
+
+ [Fact]
+ public void PreApplicationStartCodeThrowsIfVersionSpecifiedInConfigIsNotAvailable()
+ {
+ // Arrange
+ Version loadedVersion = null;
+
+ var binDirectory = DeploymentUtil.GetBinDirectory();
+ IEnumerable<AssemblyName> loadedAssemblies = GetAssemblies("1.0.0.0", AssemblyUtils.ThisAssemblyName.Version.ToString());
+
+ var fileSystem = new TestFileSystem();
+ fileSystem.AddFile("Index.cshtml");
+ var buildManager = new TestBuildManager();
+ var content = AssemblyUtils.ThisAssemblyName.Version + Environment.NewLine;
+ buildManager.Stream = new MemoryStream(Encoding.Default.GetBytes(content));
+
+ var nameValueCollection = GetAppSettings(enabled: null, webPagesVersion: new Version("1.5"));
+ Action<Version> loadWebPages = (version) => { loadedVersion = version; };
+ Action registerForChange = () => { };
+
+ // Act and Assert
+ Assert.Throws<InvalidOperationException>(() =>
+ PreApplicationStartCode.StartCore(fileSystem, "", binDirectory, nameValueCollection, loadedAssemblies, buildManager: buildManager, loadWebPages: loadWebPages, registerForChangeNotification: registerForChange, getAssemblyNameThunk: null),
+ String.Format("Specified Web Pages version \"1.5.0.0\" could not be found. Update your web.config to specify a different version. Current version: \"{0}\".",
+ AssemblyUtils.ThisAssemblyName.Version));
+ }
+
+ [Fact]
+ public void TestPreAppStartClass()
+ {
+ PreAppStartTestHelper.TestPreAppStartClass(typeof(PreApplicationStartCode));
+ }
+
+ private static NameValueCollection GetAppSettings(bool? enabled, Version webPagesVersion)
+ {
+ var nameValueCollection = new NameValueCollection();
+ if (enabled.HasValue)
+ {
+ nameValueCollection["webpages:enabled"] = enabled.Value ? "true" : "false";
+ }
+ if (webPagesVersion != null)
+ {
+ nameValueCollection["webpages:version"] = webPagesVersion.ToString();
+ }
+
+ return nameValueCollection;
+ }
+
+ private static void VerifyVersionFile(TestBuildManager buildManager, Version webPagesVersion)
+ {
+ var content = Encoding.UTF8.GetString(buildManager.Stream.ToArray());
+ Version version = Version.Parse(content);
+ Assert.Equal(webPagesVersion, version);
+ }
+
+ private class TestBuildManager : IBuildManager
+ {
+ private MemoryStream _memoryStream = new MemoryStream();
+
+ public MemoryStream Stream
+ {
+ get { return _memoryStream; }
+ set { _memoryStream = value; }
+ }
+
+ public Stream CreateCachedFile(string fileName)
+ {
+ Assert.Equal(DeploymentVersionFile, fileName);
+ CopyMemoryStream();
+ return _memoryStream;
+ }
+
+ public Stream ReadCachedFile(string fileName)
+ {
+ Assert.Equal(DeploymentVersionFile, fileName);
+ CopyMemoryStream();
+ return _memoryStream;
+ }
+
+ /// <summary>
+ /// Need to do this because the MemoryStream is read and written to in consecutive calls which causes it to be closed / non-expandable.
+ /// </summary>
+ private void CopyMemoryStream()
+ {
+ var content = _memoryStream.ToArray();
+ if (content.Length > 0)
+ {
+ _memoryStream = new MemoryStream(_memoryStream.ToArray());
+ }
+ else
+ {
+ _memoryStream = new MemoryStream();
+ }
+ }
+ }
+
+ private static IEnumerable<AssemblyName> GetAssemblies(params string[] versions)
+ {
+ return from version in versions
+ select new AssemblyName("System.Web.WebPages.Deployment, Version=" + version + ", Culture=neutral, PublicKeyToken=31bf3856ad364e35");
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Deployment.Test/Properties/AssemblyInfo.cs b/test/System.Web.WebPages.Deployment.Test/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..16a6ad5e
--- /dev/null
+++ b/test/System.Web.WebPages.Deployment.Test/Properties/AssemblyInfo.cs
@@ -0,0 +1,34 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+
+[assembly: AssemblyTitle("System.Web.WebPages.Deployment.Test")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("Microsoft")]
+[assembly: AssemblyProduct("System.Web.WebPages.Deployment.Test")]
+[assembly: AssemblyCopyright("Copyright © Microsoft 2009")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM componenets. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+
+[assembly: ComVisible(false)]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Revision and Build Numbers
+// by using the '*' as shown below:
+
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/test/System.Web.WebPages.Deployment.Test/System.Web.WebPages.Deployment.Test.csproj b/test/System.Web.WebPages.Deployment.Test/System.Web.WebPages.Deployment.Test.csproj
new file mode 100644
index 00000000..cce1a22d
--- /dev/null
+++ b/test/System.Web.WebPages.Deployment.Test/System.Web.WebPages.Deployment.Test.csproj
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProductVersion>9.0.30729</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{268DEE9D-F323-4A00-8ED8-3784388C3E3A}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>System.Web.WebPages.Deployment.Test</RootNamespace>
+ <AssemblyName>System.Web.WebPages.Deployment.Test</AssemblyName>
+ <FileAlignment>512</FileAlignment>
+ <ProjectTypeGuids>{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Debug\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Release\Test\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'CodeCoverage' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\CodeCoverage\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="Microsoft.CSharp" />
+ <Reference Include="System" />
+ <Reference Include="System.Web" />
+ <Reference Include="xunit">
+ <HintPath>..\..\packages\xunit.1.9.0.1566\lib\xunit.dll</HintPath>
+ </Reference>
+ <Reference Include="xunit.extensions">
+ <HintPath>..\..\packages\xunit.extensions.1.9.0.1566\lib\xunit.extensions.dll</HintPath>
+ </Reference>
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="AssemblyUtilsTest.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="DeploymentUtil.cs" />
+ <Compile Include="PreApplicationStartCodeTest.cs" />
+ <Compile Include="TestFileSystem.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="WebPagesDeploymentTest.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\System.Web.WebPages.Deployment\System.Web.WebPages.Deployment.csproj">
+ <Project>{22BABB60-8F02-4027-AFFC-ACF069954536}</Project>
+ <Name>System.Web.WebPages.Deployment</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\Microsoft.TestCommon\Microsoft.TestCommon.csproj">
+ <Project>{FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0}</Project>
+ <Name>Microsoft.TestCommon</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="App.Config" />
+ <None Include="packages.config" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="TestFiles\ConfigTestAssemblies\V2_Signed\System.Web.WebPages.Deployment.dll" />
+ <EmbeddedResource Include="TestFiles\ConfigTestAssemblies\V2_Unsigned\System.Web.WebPages.Deployment.dll" />
+ <EmbeddedResource Include="TestFiles\ConfigTestSites\CshtmlFileConfigV1\Default.cshtml" />
+ <EmbeddedResource Include="TestFiles\ConfigTestSites\CshtmlFileConfigV1\web.config" />
+ <EmbeddedResource Include="TestFiles\ConfigTestSites\CshtmlFileNoVersion\Default.cshtml" />
+ <EmbeddedResource Include="TestFiles\ConfigTestSites\NoCshtml\Default.htm" />
+ <EmbeddedResource Include="TestFiles\ConfigTestSites\NoCshtmlConfigV1\Default.htm" />
+ <EmbeddedResource Include="TestFiles\ConfigTestSites\NoCshtmlConfigV1\web.config" />
+ <EmbeddedResource Include="TestFiles\ConfigTestSites\NoCshtmlNoConfigSetting\Default.htm" />
+ <EmbeddedResource Include="TestFiles\ConfigTestSites\NoCshtmlNoConfigSetting\web.config" />
+ <EmbeddedResource Include="TestFiles\ConfigTestSites\NoCshtmlWithEnabledSetting\Default.htm" />
+ <EmbeddedResource Include="TestFiles\ConfigTestSites\NoCshtmlWithEnabledSetting\web.config" />
+ <EmbeddedResource Include="TestFiles\ConfigTestSites\NoCshtmlWithEnabledSettingFalse\Default.htm" />
+ <EmbeddedResource Include="TestFiles\ConfigTestSites\NoCshtmlWithEnabledSettingFalse\web.config" />
+ </ItemGroup>
+ <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+</Project> \ No newline at end of file
diff --git a/test/System.Web.WebPages.Deployment.Test/TestFileSystem.cs b/test/System.Web.WebPages.Deployment.Test/TestFileSystem.cs
new file mode 100644
index 00000000..09263d14
--- /dev/null
+++ b/test/System.Web.WebPages.Deployment.Test/TestFileSystem.cs
@@ -0,0 +1,50 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Microsoft.Internal.Web.Utils;
+
+namespace System.Web.WebPages.Deployment.Test
+{
+ public class TestFileSystem : IFileSystem
+ {
+ private readonly Dictionary<string, MemoryStream> _files = new Dictionary<string, MemoryStream>(StringComparer.OrdinalIgnoreCase);
+
+ public void AddFile(string file, MemoryStream content = null)
+ {
+ content = content ?? new MemoryStream();
+ _files[file] = content;
+ }
+
+ public bool FileExists(string path)
+ {
+ return _files.ContainsKey(path);
+ }
+
+ public Stream ReadFile(string path)
+ {
+ return _files[path];
+ }
+
+ public Stream OpenFile(string path)
+ {
+ MemoryStream memoryStream;
+ if (_files.TryGetValue(path, out memoryStream))
+ {
+ var copiedStream = new MemoryStream(memoryStream.ToArray());
+ _files[path] = copiedStream;
+ }
+ else
+ {
+ AddFile(path);
+ }
+ return _files[path];
+ }
+
+ public IEnumerable<string> EnumerateFiles(string path)
+ {
+ return from file in _files.Keys
+ where Path.GetDirectoryName(file).Equals(path, StringComparison.OrdinalIgnoreCase)
+ select file;
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestAssemblies/V2_Signed/System.Web.WebPages.Deployment.dll b/test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestAssemblies/V2_Signed/System.Web.WebPages.Deployment.dll
new file mode 100644
index 00000000..d219ec85
--- /dev/null
+++ b/test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestAssemblies/V2_Signed/System.Web.WebPages.Deployment.dll
Binary files differ
diff --git a/test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestAssemblies/V2_Unsigned/System.Web.WebPages.Deployment.dll b/test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestAssemblies/V2_Unsigned/System.Web.WebPages.Deployment.dll
new file mode 100644
index 00000000..ced14804
--- /dev/null
+++ b/test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestAssemblies/V2_Unsigned/System.Web.WebPages.Deployment.dll
Binary files differ
diff --git a/test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/CshtmlFileConfigV1/Default.cshtml b/test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/CshtmlFileConfigV1/Default.cshtml
new file mode 100644
index 00000000..8557d604
--- /dev/null
+++ b/test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/CshtmlFileConfigV1/Default.cshtml
@@ -0,0 +1,9 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<html>
+ <head>
+ <title></title>
+ </head>
+ <body>
+ Not a plan9 app!
+ </body>
+</html> \ No newline at end of file
diff --git a/test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/CshtmlFileConfigV1/web.config b/test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/CshtmlFileConfigV1/web.config
new file mode 100644
index 00000000..2c83098e
--- /dev/null
+++ b/test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/CshtmlFileConfigV1/web.config
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<configuration>
+ <appSettings>
+ <add key="webpages:Version" value="1.0" />
+ </appSettings>
+</configuration> \ No newline at end of file
diff --git a/test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/CshtmlFileNoVersion/Default.cshtml b/test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/CshtmlFileNoVersion/Default.cshtml
new file mode 100644
index 00000000..8557d604
--- /dev/null
+++ b/test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/CshtmlFileNoVersion/Default.cshtml
@@ -0,0 +1,9 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<html>
+ <head>
+ <title></title>
+ </head>
+ <body>
+ Not a plan9 app!
+ </body>
+</html> \ No newline at end of file
diff --git a/test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/NoCshtml/Default.htm b/test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/NoCshtml/Default.htm
new file mode 100644
index 00000000..8557d604
--- /dev/null
+++ b/test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/NoCshtml/Default.htm
@@ -0,0 +1,9 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<html>
+ <head>
+ <title></title>
+ </head>
+ <body>
+ Not a plan9 app!
+ </body>
+</html> \ No newline at end of file
diff --git a/test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/NoCshtmlConfigv1/Default.htm b/test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/NoCshtmlConfigv1/Default.htm
new file mode 100644
index 00000000..8557d604
--- /dev/null
+++ b/test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/NoCshtmlConfigv1/Default.htm
@@ -0,0 +1,9 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<html>
+ <head>
+ <title></title>
+ </head>
+ <body>
+ Not a plan9 app!
+ </body>
+</html> \ No newline at end of file
diff --git a/test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/NoCshtmlConfigv1/web.config b/test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/NoCshtmlConfigv1/web.config
new file mode 100644
index 00000000..13c15c85
--- /dev/null
+++ b/test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/NoCshtmlConfigv1/web.config
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<configuration>
+ <appSettings>
+ <add key="webPages:version" value="1.0" />
+ </appSettings>
+</configuration> \ No newline at end of file
diff --git a/test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/NoCshtmlNoConfigSetting/Default.htm b/test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/NoCshtmlNoConfigSetting/Default.htm
new file mode 100644
index 00000000..8557d604
--- /dev/null
+++ b/test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/NoCshtmlNoConfigSetting/Default.htm
@@ -0,0 +1,9 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<html>
+ <head>
+ <title></title>
+ </head>
+ <body>
+ Not a plan9 app!
+ </body>
+</html> \ No newline at end of file
diff --git a/test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/NoCshtmlNoConfigSetting/web.config b/test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/NoCshtmlNoConfigSetting/web.config
new file mode 100644
index 00000000..49cc43e1
--- /dev/null
+++ b/test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/NoCshtmlNoConfigSetting/web.config
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<configuration>
+</configuration> \ No newline at end of file
diff --git a/test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/NoCshtmlWithEnabledSetting/Default.htm b/test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/NoCshtmlWithEnabledSetting/Default.htm
new file mode 100644
index 00000000..8557d604
--- /dev/null
+++ b/test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/NoCshtmlWithEnabledSetting/Default.htm
@@ -0,0 +1,9 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<html>
+ <head>
+ <title></title>
+ </head>
+ <body>
+ Not a plan9 app!
+ </body>
+</html> \ No newline at end of file
diff --git a/test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/NoCshtmlWithEnabledSetting/web.config b/test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/NoCshtmlWithEnabledSetting/web.config
new file mode 100644
index 00000000..56151471
--- /dev/null
+++ b/test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/NoCshtmlWithEnabledSetting/web.config
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<configuration>
+ <appSettings>
+ <add key="webpages:Enabled" value="true" />
+ </appSettings>
+</configuration> \ No newline at end of file
diff --git a/test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/NoCshtmlWithEnabledSettingFalse/Default.htm b/test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/NoCshtmlWithEnabledSettingFalse/Default.htm
new file mode 100644
index 00000000..8557d604
--- /dev/null
+++ b/test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/NoCshtmlWithEnabledSettingFalse/Default.htm
@@ -0,0 +1,9 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<html>
+ <head>
+ <title></title>
+ </head>
+ <body>
+ Not a plan9 app!
+ </body>
+</html> \ No newline at end of file
diff --git a/test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/NoCshtmlWithEnabledSettingFalse/web.config b/test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/NoCshtmlWithEnabledSettingFalse/web.config
new file mode 100644
index 00000000..e8364c20
--- /dev/null
+++ b/test/System.Web.WebPages.Deployment.Test/TestFiles/ConfigTestSites/NoCshtmlWithEnabledSettingFalse/web.config
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<configuration>
+ <appSettings>
+ <add key="webpages:Enabled" value="false" />
+ </appSettings>
+</configuration> \ No newline at end of file
diff --git a/test/System.Web.WebPages.Deployment.Test/WebPagesDeploymentTest.cs b/test/System.Web.WebPages.Deployment.Test/WebPagesDeploymentTest.cs
new file mode 100644
index 00000000..7deabc85
--- /dev/null
+++ b/test/System.Web.WebPages.Deployment.Test/WebPagesDeploymentTest.cs
@@ -0,0 +1,303 @@
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.IO;
+using System.Reflection;
+using System.Web.WebPages.TestUtils;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.WebPages.Deployment.Test
+{
+ // We need to mark this type Serializable for TDD.Net to work with some of the AppDomain tests
+ [Serializable]
+ public class WebPagesDeploymentTest : IDisposable
+ {
+ private const string TestNamespacePrefix = "System.Web.WebPages.Deployment.Test.TestFiles.";
+ private static readonly Version MaxVersion = new Version(2, 0, 0, 0);
+
+ private static readonly IDictionary<string, string> _deploymentPaths = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
+ {
+ { @"ConfigTestSites.CshtmlFileNoVersion.Default.cshtml", @"ConfigTestSites\CshtmlFileNoVersion\Default.cshtml" },
+ { @"ConfigTestSites.NoCshtmlWithEnabledSetting.Default.htm", @"ConfigTestSites\NoCshtmlWithEnabledSetting\Default.htm" },
+ { @"ConfigTestSites.NoCshtmlNoConfigSetting.web.config", @"ConfigTestSites\NoCshtmlNoConfigSetting\web.config" },
+ { @"ConfigTestSites.NoCshtmlWithEnabledSetting.web.config", @"ConfigTestSites\NoCshtmlWithEnabledSetting\web.config" },
+ { @"ConfigTestSites.NoCshtmlWithEnabledSettingFalse.Default.htm", @"ConfigTestSites\NoCshtmlWithEnabledSettingFalse\Default.htm" },
+ { @"ConfigTestAssemblies.V2_Unsigned.System.Web.WebPages.Deployment.dll", @"ConfigTestAssemblies\V2_Unsigned\System.Web.WebPages.Deployment.dll" },
+ { @"ConfigTestAssemblies.V2_Signed.System.Web.WebPages.Deployment.dll", @"ConfigTestAssemblies\V2_Signed\System.Web.WebPages.Deployment.dll" },
+ { @"ConfigTestSites.NoCshtmlWithEnabledSettingFalse.web.config", @"ConfigTestSites\NoCshtmlWithEnabledSettingFalse\web.config" },
+ { @"ConfigTestSites.CshtmlFileConfigV1.Default.cshtml", @"ConfigTestSites\CshtmlFileConfigV1\Default.cshtml" },
+ { @"ConfigTestSites.NoCshtml.Default.htm", @"ConfigTestSites\NoCshtml\Default.htm" },
+ { @"ConfigTestSites.NoCshtmlNoConfigSetting.Default.htm", @"ConfigTestSites\NoCshtmlNoConfigSetting\Default.htm" },
+ { @"ConfigTestSites.CshtmlFileConfigV1.web.config", @"ConfigTestSites\CshtmlFileConfigV1\web.config" },
+ { @"ConfigTestSites.NoCshtmlConfigV1.Default.htm", @"ConfigTestSites\NoCshtmlConfigV1\Default.htm" },
+ { @"ConfigTestSites.NoCshtmlConfigV1.web.config", @"ConfigTestSites\NoCshtmlConfigV1\web.config" },
+ };
+
+ private readonly string _tempPath = GetTempPath();
+
+ public WebPagesDeploymentTest()
+ {
+ var assembly = typeof(WebPagesDeploymentTest).Assembly;
+ foreach (var item in _deploymentPaths)
+ {
+ new TestFile(TestNamespacePrefix + item.Key, assembly).Save(Path.Combine(_tempPath, item.Value));
+ }
+ }
+
+ public void Dispose()
+ {
+ try
+ {
+ Directory.Delete(_tempPath, recursive: true);
+ }
+ catch
+ {
+
+ }
+ }
+
+ [Fact]
+ public void IsEnabledReturnsFalseIfNoCshtmlOrConfigFile()
+ {
+ Assert.False(WebPagesDeployment.IsEnabled(Path.Combine(_tempPath, @"ConfigTestSites\NoCshtml")));
+ }
+
+ [Fact]
+ public void IsEnabledReturnsFalseIfNoCshtmlAndNoConfigSetting()
+ {
+ Assert.False(WebPagesDeployment.IsEnabled(Path.Combine(_tempPath, @"ConfigTestSites\NoCshtmlNoConfigSetting")));
+ }
+
+ [Fact]
+ public void IsEnabledReturnsTrueIfNoCshtmlAndEnabledConfigSetting()
+ {
+ Assert.True(WebPagesDeployment.IsEnabled(Path.Combine(_tempPath, @"ConfigTestSites\NoCshtmlWithEnabledSetting")));
+ }
+
+ [Fact]
+ public void IsEnabledReturnsTrueIfCshtmlFilePresent()
+ {
+ Assert.True(WebPagesDeployment.IsEnabled(Path.Combine(_tempPath, @"ConfigTestSites\CshtmlFileNoVersion")));
+ }
+
+ [Fact]
+ public void IsExplicitlyDisabledReturnsTrueIfNoCshtmlAndEnabledConfigSettingSetToFalse()
+ {
+ Assert.True(WebPagesDeployment.IsExplicitlyDisabled(Path.Combine(_tempPath, @"ConfigTestSites\NoCshtmlWithEnabledSettingFalse")));
+ }
+
+ [Fact]
+ public void IsExplicitlyDisabledReturnsFalseIfNoCshtmlAndEnabledConfigSettingSetToTrue()
+ {
+ Assert.False(WebPagesDeployment.IsExplicitlyDisabled(Path.Combine(_tempPath, @"ConfigTestSites\NoCshtmlWithEnabledSetting")));
+ }
+
+ [Fact]
+ public void IsExplicitlyDisabledReturnsFalseIfNoCshtmlAndNoConfigSetting()
+ {
+ Assert.False(WebPagesDeployment.IsExplicitlyDisabled(Path.Combine(_tempPath, @"ConfigTestSites\NoCshtmlNoConfigSetting")));
+ }
+
+ [Fact]
+ public void IsExplicitlyDisabledReturnsFalseIfNoCshtmlOrConfigFile()
+ {
+ Assert.False(WebPagesDeployment.IsExplicitlyDisabled(Path.Combine(_tempPath, @"ConfigTestSites\NoCshtml")));
+ }
+
+ [Fact]
+ public void GetVersionReturnsValueFromAppSettingsIfNotExplicitlyDisabled()
+ {
+ // Arrange
+ var version = "1.2.3.4";
+ var appSettings = new NameValueCollection { { "webPages:Version", version } };
+ var maxVersion = new Version("2.0");
+
+ // Act
+ var actualVersion = WebPagesDeployment.GetVersionInternal(appSettings, binVersion: null, defaultVersion: null);
+
+ // Assert
+ Assert.Equal(new Version(version), actualVersion);
+ }
+
+ [Fact]
+ public void GetVersionReturnsValueEvenIfExplicitlyDisabled()
+ {
+ // Arrange
+ var version = "1.2.3.4";
+ var appSettings = new NameValueCollection { { "webPages:Version", version }, { "webPages:Enabled", "False" } };
+ var maxVersion = new Version("2.0");
+
+ // Act
+ var actualVersion = WebPagesDeployment.GetVersionInternal(appSettings, binVersion: null, defaultVersion: null);
+
+ // Assert
+ Assert.Equal(new Version(version), actualVersion);
+ }
+
+ [Fact]
+ public void GetVersionReturnsLowerVersionIfSpecifiedInConfig()
+ {
+ AppDomainUtils.RunInSeparateAppDomain(() =>
+ {
+ // Arrange - Load v2 Config
+ Assembly asm = Assembly.LoadFrom(Path.Combine(_tempPath, @"ConfigTestAssemblies\V2_Signed\System.Web.WebPages.Deployment.dll"));
+ Assert.Equal(new Version(2, 0, 0, 0), asm.GetName().Version);
+ Assert.Equal("System.Web.WebPages.Deployment", asm.GetName().Name);
+
+ using (WebUtils.CreateHttpRuntime(@"~\foo", "."))
+ {
+ // Act
+ Version ver = WebPagesDeployment.GetVersionWithoutEnabledCheck(Path.Combine(_tempPath, @"ConfigTestSites\CshtmlFileConfigV1"));
+
+ // Assert
+ Assert.Equal(new Version(1, 0, 0, 0), ver);
+ }
+ });
+ }
+
+ [Fact]
+ public void GetVersionReturnsLowerVersionIfSpecifiedInConfigAndNotExplicitlyDisabled()
+ {
+ AppDomainUtils.RunInSeparateAppDomain(() =>
+ {
+ // Arrange - Load v2 Config
+ Assembly asm = Assembly.LoadFrom(Path.Combine(_tempPath, @"ConfigTestAssemblies\V2_Signed\System.Web.WebPages.Deployment.dll"));
+ Assert.Equal(new Version(2, 0, 0, 0), asm.GetName().Version);
+ Assert.Equal("System.Web.WebPages.Deployment", asm.GetName().Name);
+
+ using (WebUtils.CreateHttpRuntime(@"~\foo", "."))
+ {
+ // Act
+ Version ver = WebPagesDeployment.GetVersionWithoutEnabledCheck(Path.Combine(_tempPath, @"ConfigTestSites\NoCshtmlConfigV1"));
+
+ // Assert
+ Assert.Equal(new Version(1, 0, 0, 0), ver);
+ }
+ });
+ }
+
+ [Fact]
+ public void GetVersionIgnoresUnsignedConfigDll()
+ {
+ AppDomainUtils.RunInSeparateAppDomain(() =>
+ {
+ // Arrange - Load v2 Config
+ Assembly asm = Assembly.LoadFrom(Path.Combine(_tempPath, @"ConfigTestAssemblies\V2_Unsigned\System.Web.WebPages.Deployment.dll"));
+ Assert.Equal(new Version(2, 0, 0, 0), asm.GetName().Version);
+ Assert.Equal("System.Web.WebPages.Deployment", asm.GetName().Name);
+
+ using (WebUtils.CreateHttpRuntime(@"~\foo", "."))
+ {
+ // Act
+ Version ver = WebPagesDeployment.GetVersionWithoutEnabledCheck(Path.Combine(_tempPath, @"ConfigTestSites\CshtmlFileNoVersion"));
+
+ // Assert
+ Assert.Equal(MaxVersion, ver);
+ }
+ });
+ }
+
+ [Fact]
+ public void GetVersionReturnsVMaxAssemblyVersionIfCshtmlFilePresent()
+ {
+ AppDomainUtils.RunInSeparateAppDomain(() =>
+ {
+ // Arrange - Load v2 Config
+ Assembly asm = Assembly.LoadFrom(Path.Combine(_tempPath, @"ConfigTestAssemblies\V2_Signed\System.Web.WebPages.Deployment.dll"));
+ Assert.Equal(new Version(2, 0, 0, 0), asm.GetName().Version);
+ Assert.Equal("System.Web.WebPages.Deployment", asm.GetName().Name);
+
+ using (WebUtils.CreateHttpRuntime(@"~\foo", "."))
+ {
+ // Act
+ Version ver = WebPagesDeployment.GetVersionWithoutEnabledCheck(Path.Combine(_tempPath, @"ConfigTestSites\CshtmlFileNoVersion"));
+
+ // Assert
+ Assert.Equal(MaxVersion, ver);
+ }
+ });
+ }
+
+ [Fact]
+ public void GetVersionThrowsIfPathNullOrEmpty()
+ {
+ Assert.ThrowsArgumentNullOrEmptyString(() => WebPagesDeployment.GetVersionWithoutEnabledCheck(null), "path");
+ Assert.ThrowsArgumentNullOrEmptyString(() => WebPagesDeployment.GetVersionWithoutEnabledCheck(String.Empty), "path");
+ }
+
+ [Fact]
+ public void IsEnabledThrowsIfPathNullOrEmpty()
+ {
+ Assert.ThrowsArgumentNullOrEmptyString(() => WebPagesDeployment.IsEnabled(null), "path");
+ Assert.ThrowsArgumentNullOrEmptyString(() => WebPagesDeployment.IsEnabled(String.Empty), "path");
+ }
+
+ [Theory]
+ [InlineData(new object[] { null })]
+ [InlineData(new object[] { "" })]
+ public void ObsoleteGetVersionThrowsIfPathIsNullOrEmpty(string path)
+ {
+ // Arrange
+ var fileSystem = new TestFileSystem();
+ var configuration = new NameValueCollection();
+
+ // Act and Assert
+ Assert.ThrowsArgumentNullOrEmptyString(() => WebPagesDeployment.GetObsoleteVersionInternal(path, configuration, fileSystem, () => new Version("2.0.0.0")), "path");
+ }
+
+ [Fact]
+ public void ObsoleteGetVersionReturnsNullIfNoFilesInTheSite()
+ {
+ // Arrange
+ var path = "blah";
+ var fileSystem = new TestFileSystem();
+ var configuration = new NameValueCollection();
+
+ // Act
+ var version = WebPagesDeployment.GetObsoleteVersionInternal(path, configuration, fileSystem, () => new Version("2.0.0.0"));
+
+ // Assert
+ Assert.Null(version);
+ }
+
+ [Fact]
+ public void ObsoleteGetVersionReturnsMaxVersionIfNoValueInConfigNoFilesInBinSiteContainsCshtmlFiles()
+ {
+ // Arrange
+ var path = "blah";
+ var fileSystem = new TestFileSystem();
+ fileSystem.AddFile(@"blah\Foo.cshtml");
+ var configuration = new NameValueCollection();
+
+ // Act
+ var version = WebPagesDeployment.GetObsoleteVersionInternal(path, configuration, fileSystem, () => new Version("2.0.0.0"));
+
+ // Assert
+ Assert.Equal(new Version("2.0.0.0"), version);
+ }
+
+ [Fact]
+ public void ObsoleteGetVersionReturnsVersionFromConfigIfDisabled()
+ {
+ // Arrange
+ var maxVersion = new Version("2.1.3.4");
+ var fileSystem = new TestFileSystem();
+ var configuration = new NameValueCollection();
+ configuration["webPages:Enabled"] = "False";
+ configuration["webPages:Version"] = "2.0";
+ var path = "blah";
+
+ // Act
+ var version = WebPagesDeployment.GetObsoleteVersionInternal(path, configuration, fileSystem, () => maxVersion);
+
+ // Assert
+ Assert.Equal(new Version("2.0.0.0"), version);
+ }
+
+ private static string GetTempPath()
+ {
+ return Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Deployment.Test/packages.config b/test/System.Web.WebPages.Deployment.Test/packages.config
new file mode 100644
index 00000000..d82739c0
--- /dev/null
+++ b/test/System.Web.WebPages.Deployment.Test/packages.config
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="xunit" version="1.9.0.1566" />
+ <package id="xunit.extensions" version="1.9.0.1566" />
+</packages> \ No newline at end of file
diff --git a/test/System.Web.WebPages.Razor.Test/PreApplicationStartCodeTest.cs b/test/System.Web.WebPages.Razor.Test/PreApplicationStartCodeTest.cs
new file mode 100644
index 00000000..228b0bc5
--- /dev/null
+++ b/test/System.Web.WebPages.Razor.Test/PreApplicationStartCodeTest.cs
@@ -0,0 +1,28 @@
+using System.Reflection;
+using System.Web.Compilation;
+using System.Web.WebPages.TestUtils;
+using Xunit;
+
+namespace System.Web.WebPages.Razor.Test
+{
+ public class PreApplicationStartCodeTest
+ {
+ [Fact]
+ public void StartTest()
+ {
+ AppDomainUtils.RunInSeparateAppDomain(() =>
+ {
+ AppDomainUtils.SetPreAppStartStage();
+ PreApplicationStartCode.Start();
+ var buildProviders = typeof(BuildProvider).GetField("s_dynamicallyRegisteredProviders", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null);
+ Assert.Equal(2, buildProviders.GetType().GetProperty("Count", BindingFlags.Public | BindingFlags.Instance).GetValue(buildProviders, new object[] { }));
+ });
+ }
+
+ [Fact]
+ public void TestPreAppStartClass()
+ {
+ PreAppStartTestHelper.TestPreAppStartClass(typeof(PreApplicationStartCode));
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Razor.Test/Properties/AssemblyInfo.cs b/test/System.Web.WebPages.Razor.Test/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..74ed3ab1
--- /dev/null
+++ b/test/System.Web.WebPages.Razor.Test/Properties/AssemblyInfo.cs
@@ -0,0 +1,34 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+
+[assembly: AssemblyTitle("System.Web.WebPages.Razor.Test")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("Microsoft")]
+[assembly: AssemblyProduct("System.Web.WebPages.Razor.Test")]
+[assembly: AssemblyCopyright("Copyright © Microsoft 2009")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM componenets. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+
+[assembly: ComVisible(false)]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Revision and Build Numbers
+// by using the '*' as shown below:
+
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/test/System.Web.WebPages.Razor.Test/RazorBuildProviderTest.cs b/test/System.Web.WebPages.Razor.Test/RazorBuildProviderTest.cs
new file mode 100644
index 00000000..cea1ff23
--- /dev/null
+++ b/test/System.Web.WebPages.Razor.Test/RazorBuildProviderTest.cs
@@ -0,0 +1,248 @@
+using System.CodeDom;
+using System.CodeDom.Compiler;
+using System.Collections;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Web.Compilation;
+using System.Web.WebPages.TestUtils;
+using ASP;
+using Microsoft.CSharp;
+using Moq;
+using Xunit;
+
+namespace ASP
+{
+ public class _Page_Foo_Test_cshtml
+ {
+ }
+}
+
+namespace System.Web.WebPages.Razor.Test
+{
+ public class RazorBuildProviderTest
+ {
+ private class MockAssemblyBuilder : IAssemblyBuilder
+ {
+ public BuildProvider BuildProvider { get; private set; }
+ public CodeCompileUnit CompileUnit { get; private set; }
+ public string LastTypeFactoryGenerated { get; private set; }
+
+ public void AddCodeCompileUnit(BuildProvider buildProvider, CodeCompileUnit compileUnit)
+ {
+ BuildProvider = buildProvider;
+ CompileUnit = compileUnit;
+ }
+
+ public void GenerateTypeFactory(string typeName)
+ {
+ LastTypeFactoryGenerated = typeName;
+ }
+ }
+
+ [Fact]
+ public void CodeCompilerTypeReturnsTypeFromCodeLanguage()
+ {
+ // Arrange
+ WebPageRazorHost host = new WebPageRazorHost("~/Foo/Baz.cshtml", @"C:\Foo\Baz.cshtml");
+ RazorBuildProvider provider = CreateBuildProvider("foo @bar baz");
+ provider.Host = host;
+
+ // Act
+ CompilerType type = provider.CodeCompilerType;
+
+ // Assert
+ Assert.Equal(typeof(CSharpCodeProvider), type.CodeDomProviderType);
+ }
+
+ [Fact]
+ public void CodeCompilerTypeSetsDebugFlagInFullTrust()
+ {
+ // Arrange
+ WebPageRazorHost host = new WebPageRazorHost("~/Foo/Baz.cshtml", @"C:\Foo\Baz.cshtml");
+ RazorBuildProvider provider = CreateBuildProvider("foo @bar baz");
+ provider.Host = host;
+
+ // Act
+ CompilerType type = provider.CodeCompilerType;
+
+ // Assert
+ Assert.True(type.CompilerParameters.IncludeDebugInformation);
+ }
+
+ [Fact]
+ public void GetGeneratedTypeUsesNameAndNamespaceFromHostToExtractType()
+ {
+ // Arrange
+ WebPageRazorHost host = new WebPageRazorHost("~/Foo/Test.cshtml", @"C:\Foo\Test.cshtml");
+ RazorBuildProvider provider = new RazorBuildProvider() { Host = host };
+ CompilerResults results = new CompilerResults(new TempFileCollection());
+ results.CompiledAssembly = typeof(_Page_Foo_Test_cshtml).Assembly;
+
+ // Act
+ Type typ = provider.GetGeneratedType(results);
+
+ // Assert
+ Assert.Equal(typeof(_Page_Foo_Test_cshtml), typ);
+ }
+
+ [Fact]
+ public void GenerateCodeCoreAddsGeneratedCodeToAssemblyBuilder()
+ {
+ // Arrange
+ WebPageRazorHost host = new WebPageRazorHost("~/Foo/Baz.cshtml", @"C:\Foo\Baz.cshtml");
+ RazorBuildProvider provider = new RazorBuildProvider();
+ CodeCompileUnit ccu = new CodeCompileUnit();
+ MockAssemblyBuilder asmBuilder = new MockAssemblyBuilder();
+ provider.Host = host;
+ provider.GeneratedCode = ccu;
+
+ // Act
+ provider.GenerateCodeCore(asmBuilder);
+
+ // Assert
+ Assert.Same(provider, asmBuilder.BuildProvider);
+ Assert.Same(ccu, asmBuilder.CompileUnit);
+ Assert.Equal("ASP._Page_Foo_Baz_cshtml", asmBuilder.LastTypeFactoryGenerated);
+ }
+
+ [Fact]
+ public void CodeGenerationStartedTest()
+ {
+ // Arrange
+ WebPageRazorHost host = new WebPageRazorHost("~/Foo/Baz.cshtml", @"C:\Foo\Baz.cshtml");
+ RazorBuildProvider provider = CreateBuildProvider("foo @bar baz");
+ provider.Host = host;
+
+ // Expected original base dependencies
+ var baseDependencies = new ArrayList();
+ baseDependencies.Add("/Samples/Foo/Baz.cshtml");
+
+ // Expected list of dependencies after GenerateCode is called
+ var dependencies = new ArrayList();
+ dependencies.Add(baseDependencies[0]);
+ dependencies.Add("/Samples/Foo/Foo.cshtml");
+
+ // Set up the event handler
+ provider.CodeGenerationStartedInternal += (sender, e) =>
+ {
+ var bp = sender as RazorBuildProvider;
+ bp.AddVirtualPathDependency("/Samples/Foo/Foo.cshtml");
+ };
+
+ // Set up the base dependency
+ MockAssemblyBuilder builder = new MockAssemblyBuilder();
+ typeof(BuildProvider).GetField("_virtualPath", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(provider, CreateVirtualPath("/Samples/Foo/Baz.cshtml"));
+
+ // Test that VirtualPathDependencies returns the original dependency before GenerateCode is called
+ Assert.True(baseDependencies.OfType<string>().SequenceEqual(provider.VirtualPathDependencies.OfType<string>()));
+
+ // Act
+ provider.GenerateCodeCore(builder);
+
+ // Assert
+ Assert.NotNull(provider.AssemblyBuilderInternal);
+ Assert.Equal(builder, provider.AssemblyBuilderInternal);
+ Assert.True(dependencies.OfType<string>().SequenceEqual(provider.VirtualPathDependencies.OfType<string>()));
+ Assert.Equal("/Samples/Foo/Baz.cshtml", provider.VirtualPath);
+ }
+
+ [Fact]
+ public void AfterGeneratedCodeEventGetsExecutedAtCorrectTime()
+ {
+ // Arrange
+ WebPageRazorHost host = new WebPageRazorHost("~/Foo/Baz.cshtml", @"C:\Foo\Baz.cshtml");
+ RazorBuildProvider provider = CreateBuildProvider("foo @bar baz");
+ provider.Host = host;
+
+ provider.CodeGenerationCompletedInternal += (sender, e) =>
+ {
+ Assert.Equal("~/Foo/Baz.cshtml", e.VirtualPath);
+ e.GeneratedCode.Namespaces.Add(new CodeNamespace("DummyNamespace"));
+ };
+
+ // Act
+ CodeCompileUnit generated = provider.GeneratedCode;
+
+ // Assert
+ Assert.NotNull(generated.Namespaces
+ .OfType<CodeNamespace>()
+ .SingleOrDefault(ns => String.Equals(ns.Name, "DummyNamespace")));
+ }
+
+ [Fact]
+ public void GeneratedCodeThrowsHttpParseExceptionForLastParserError()
+ {
+ // Arrange
+ WebPageRazorHost host = new WebPageRazorHost("~/Foo/Baz.cshtml", @"C:\Foo\Baz.cshtml");
+ RazorBuildProvider provider = CreateBuildProvider("foo @{ if( baz");
+ provider.Host = host;
+
+ // Act
+ Assert.Throws<HttpParseException>(() => { CodeCompileUnit ccu = provider.GeneratedCode; });
+ }
+
+ [Fact]
+ public void BuildProviderFiresEventToAlterHostBeforeBuildingPath()
+ {
+ // Arrange
+ WebPageRazorHost expected = new TestHost("~/Foo/Boz.cshtml", @"C:\Foo\Boz.cshtml");
+ WebPageRazorHost expectedBefore = new WebPageRazorHost("~/Foo/Baz.cshtml", @"C:\Foo\Baz.cshtml");
+ RazorBuildProvider provider = CreateBuildProvider("foo");
+ typeof(BuildProvider).GetField("_virtualPath", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(provider, CreateVirtualPath("/Samples/Foo/Baz.cshtml"));
+ Mock.Get(provider).Setup(p => p.GetHostFromConfig()).Returns(expectedBefore);
+ bool called = false;
+ EventHandler<CompilingPathEventArgs> handler = (sender, args) =>
+ {
+ Assert.Equal("/Samples/Foo/Baz.cshtml", args.VirtualPath);
+ Assert.Same(expectedBefore, args.Host);
+ args.Host = expected;
+ called = true;
+ };
+ RazorBuildProvider.CompilingPath += handler;
+
+ try
+ {
+ // Act
+ CodeCompileUnit ccu = provider.GeneratedCode;
+
+ // Assert
+ Assert.Equal("Test", ccu.Namespaces[0].Name);
+ Assert.Same(expected, provider.Host);
+ Assert.True(called);
+ }
+ finally
+ {
+ RazorBuildProvider.CompilingPath -= handler;
+ }
+ }
+
+ private static object CreateVirtualPath(string path)
+ {
+ var vPath = typeof(BuildProvider).Assembly.GetType("System.Web.VirtualPath");
+ var method = vPath.GetMethod("CreateNonRelative", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
+ return method.Invoke(null, new object[] { path });
+ }
+
+ private static RazorBuildProvider CreateBuildProvider(string razorContent)
+ {
+ Mock<RazorBuildProvider> mockProvider = new Mock<RazorBuildProvider>()
+ {
+ CallBase = true
+ };
+ mockProvider.Setup(p => p.InternalOpenReader())
+ .Returns(() => new StringReader(razorContent));
+ return mockProvider.Object;
+ }
+
+ private class TestHost : WebPageRazorHost
+ {
+ public TestHost(string virtualPath, string physicalPath) : base(virtualPath, physicalPath) { }
+
+ public override void PostProcessGeneratedCode(Web.Razor.Generator.CodeGeneratorContext context)
+ {
+ context.CompileUnit.Namespaces.Insert(0, new CodeNamespace("Test"));
+ }
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Razor.Test/System.Web.WebPages.Razor.Test.csproj b/test/System.Web.WebPages.Razor.Test/System.Web.WebPages.Razor.Test.csproj
new file mode 100644
index 00000000..9c798da9
--- /dev/null
+++ b/test/System.Web.WebPages.Razor.Test/System.Web.WebPages.Razor.Test.csproj
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <!-- Temporarily disable Obsolete Warnings as Errors -->
+ <WarningsNotAsErrors>618</WarningsNotAsErrors>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProductVersion>9.0.30729</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{66A74F3C-A106-4C1E-BAA0-001908FEA2CA}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>System.Web.WebPages.Razor.Test</RootNamespace>
+ <AssemblyName>System.Web.WebPages.Razor.Test</AssemblyName>
+ <FileAlignment>512</FileAlignment>
+ <ProjectTypeGuids>{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Debug\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Release\Test\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'CodeCoverage' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\CodeCoverage\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <ItemGroup>
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="PreApplicationStartCodeTest.cs" />
+ <Compile Include="RazorBuildProviderTest.cs" />
+ <Compile Include="WebCodeRazorEngineHostTest.cs" />
+ <Compile Include="WebPageRazorEngineHostTest.cs" />
+ <Compile Include="WebRazorHostFactoryTest.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <Reference Include="Microsoft.CSharp" />
+ <Reference Include="Moq, Version=4.0.10827.0, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL">
+ <HintPath>..\..\packages\Moq.4.0.10827\lib\NET40\Moq.dll</HintPath>
+ </Reference>
+ <Reference Include="System" />
+ <Reference Include="System.Configuration" />
+ <Reference Include="System.Web" />
+ <Reference Include="xunit">
+ <HintPath>..\..\packages\xunit.1.9.0.1566\lib\xunit.dll</HintPath>
+ </Reference>
+ <Reference Include="xunit.extensions">
+ <HintPath>..\..\packages\xunit.extensions.1.9.0.1566\lib\xunit.extensions.dll</HintPath>
+ </Reference>
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\System.Web.WebPages\System.Web.WebPages.csproj">
+ <Project>{76EFA9C5-8D7E-4FDF-B710-E20F8B6B00D2}</Project>
+ <Name>System.Web.WebPages</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\src\System.Web.Razor\System.Web.Razor.csproj">
+ <Project>{8F18041B-9410-4C36-A9C5-067813DF5F31}</Project>
+ <Name>System.Web.Razor</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\src\System.Web.WebPages.Razor\System.Web.WebPages.Razor.csproj">
+ <Project>{0939B11A-FE4E-4BA1-8AD6-D97741EE314F}</Project>
+ <Name>System.Web.WebPages.Razor</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\Microsoft.TestCommon\Microsoft.TestCommon.csproj">
+ <Project>{FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0}</Project>
+ <Name>Microsoft.TestCommon</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup />
+ <ItemGroup>
+ <None Include="app.config" />
+ <None Include="packages.config" />
+ </ItemGroup>
+ <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+</Project> \ No newline at end of file
diff --git a/test/System.Web.WebPages.Razor.Test/WebCodeRazorEngineHostTest.cs b/test/System.Web.WebPages.Razor.Test/WebCodeRazorEngineHostTest.cs
new file mode 100644
index 00000000..5824c8fd
--- /dev/null
+++ b/test/System.Web.WebPages.Razor.Test/WebCodeRazorEngineHostTest.cs
@@ -0,0 +1,110 @@
+using System.CodeDom;
+using System.Linq;
+using System.Web.Razor.Generator;
+using Xunit;
+
+namespace System.Web.WebPages.Razor.Test
+{
+ public class WebCodeRazorEngineHostTest
+ {
+ [Fact]
+ public void ConstructorWithMalformedVirtualPathSetsDefaultProperties()
+ {
+ // Act
+ WebCodeRazorHost host = new WebCodeRazorHost(@"~/Foo/App_Code\Bar\Baz\Qux.cshtml");
+
+ // Assert
+ Assert.Equal("System.Web.WebPages.HelperPage", host.DefaultBaseClass);
+ Assert.Equal("ASP.Bar.Baz", host.DefaultNamespace);
+ Assert.Equal("Qux", host.DefaultClassName);
+ Assert.False(host.DefaultDebugCompilation);
+ Assert.True(host.StaticHelpers);
+ }
+
+ [Fact]
+ public void ConstructorWithFileOnlyVirtualPathSetsDefaultProperties()
+ {
+ // Act
+ WebCodeRazorHost host = new WebCodeRazorHost(@"Foo.cshtml");
+
+ // Assert
+ Assert.Equal("System.Web.WebPages.HelperPage", host.DefaultBaseClass);
+ Assert.Equal("ASP", host.DefaultNamespace);
+ Assert.Equal("Foo", host.DefaultClassName);
+ Assert.False(host.DefaultDebugCompilation);
+ }
+
+ [Fact]
+ public void ConstructorWithVirtualPathSetsDefaultProperties()
+ {
+ // Act
+ WebCodeRazorHost host = new WebCodeRazorHost("~/Foo/App_Code/Bar/Baz/Qux.cshtml");
+
+ // Assert
+ Assert.Equal("System.Web.WebPages.HelperPage", host.DefaultBaseClass);
+ Assert.Equal("ASP.Bar.Baz", host.DefaultNamespace);
+ Assert.Equal("Qux", host.DefaultClassName);
+ Assert.False(host.DefaultDebugCompilation);
+ }
+
+ [Fact]
+ public void ConstructorWithVirtualAndPhysicalPathSetsDefaultProperties()
+ {
+ // Act
+ WebCodeRazorHost host = new WebCodeRazorHost("~/Foo/App_Code/Bar/Baz/Qux.cshtml", @"C:\Qux.doodad");
+
+ // Assert
+ Assert.Equal("System.Web.WebPages.HelperPage", host.DefaultBaseClass);
+ Assert.Equal("ASP.Bar.Baz", host.DefaultNamespace);
+ Assert.Equal("Qux", host.DefaultClassName);
+ Assert.False(host.DefaultDebugCompilation);
+ }
+
+ [Fact]
+ public void PostProcessGeneratedCodeRemovesExecuteMethod()
+ {
+ // Arrange
+ WebCodeRazorHost host = new WebCodeRazorHost("Foo.cshtml");
+ CodeGeneratorContext context = CodeGeneratorContext.Create(
+ host,
+ () => new CSharpCodeWriter(),
+ "TestClass",
+ "TestNamespace",
+ "TestFile.cshtml",
+ shouldGenerateLinePragmas: true);
+
+ // Act
+ host.PostProcessGeneratedCode(context);
+
+ // Assert
+ Assert.Equal(0, context.GeneratedClass.Members.OfType<CodeMemberMethod>().Count());
+ }
+
+ [Fact]
+ public void PostProcessGeneratedCodeAddsStaticApplicationInstanceProperty()
+ {
+ // Arrange
+ WebCodeRazorHost host = new WebCodeRazorHost("Foo.cshtml");
+ CodeGeneratorContext context =
+ CodeGeneratorContext.Create(
+ host,
+ () => new CSharpCodeWriter(),
+ "TestClass",
+ "TestNamespace",
+ "Foo.cshtml",
+ shouldGenerateLinePragmas: true);
+
+ // Act
+ host.PostProcessGeneratedCode(context);
+
+ // Assert
+ CodeMemberProperty appInstance = context.GeneratedClass
+ .Members
+ .OfType<CodeMemberProperty>()
+ .Where(p => p.Name.Equals("ApplicationInstance"))
+ .SingleOrDefault();
+ Assert.NotNull(appInstance);
+ Assert.True(appInstance.Attributes.HasFlag(MemberAttributes.Static));
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Razor.Test/WebPageRazorEngineHostTest.cs b/test/System.Web.WebPages.Razor.Test/WebPageRazorEngineHostTest.cs
new file mode 100644
index 00000000..24f0b39c
--- /dev/null
+++ b/test/System.Web.WebPages.Razor.Test/WebPageRazorEngineHostTest.cs
@@ -0,0 +1,100 @@
+using System.CodeDom;
+using System.CodeDom.Compiler;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Web.Razor;
+using System.Web.Razor.Generator;
+using Microsoft.CSharp;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.WebPages.Razor.Test
+{
+ public class WebPageRazorEngineHostTest
+ {
+ [Fact]
+ public void ConstructorRequiresNonNullOrEmptyVirtualPath()
+ {
+ Assert.ThrowsArgumentNullOrEmptyString(() => new WebPageRazorHost(null), "virtualPath");
+ Assert.ThrowsArgumentNullOrEmptyString(() => new WebPageRazorHost(String.Empty), "virtualPath");
+ Assert.ThrowsArgumentNullOrEmptyString(() => new WebPageRazorHost(null, "foo"), "virtualPath");
+ Assert.ThrowsArgumentNullOrEmptyString(() => new WebPageRazorHost(String.Empty, "foo"), "virtualPath");
+ }
+
+ [Fact]
+ public void ConstructorWithVirtualPathUsesItToDetermineBaseClassClassNameAndLanguage()
+ {
+ // Act
+ WebPageRazorHost host = new WebPageRazorHost("~/Foo/Bar.cshtml");
+
+ // Assert
+ Assert.Equal("_Page_Foo_Bar_cshtml", host.DefaultClassName);
+ Assert.Equal("System.Web.WebPages.WebPage", host.DefaultBaseClass);
+ Assert.IsType<CSharpRazorCodeLanguage>(host.CodeLanguage);
+ Assert.False(host.StaticHelpers);
+ }
+
+ [Fact]
+ public void PostProcessGeneratedCodeAddsGlobalImports()
+ {
+ // Arrange
+ WebPageRazorHost.AddGlobalImport("Foo.Bar");
+ WebPageRazorHost host = new WebPageRazorHost("Foo.cshtml");
+ CodeGeneratorContext context = CodeGeneratorContext.Create(
+ host,
+ () => new CSharpCodeWriter(),
+ "TestClass",
+ "TestNs",
+ "TestFile.cshtml",
+ shouldGenerateLinePragmas: true);
+
+ // Act
+ host.PostProcessGeneratedCode(context);
+
+ // Assert
+ Assert.True(context.Namespace.Imports.OfType<CodeNamespaceImport>().Any(import => String.Equals("Foo.Bar", import.Namespace)));
+ }
+
+ [Fact]
+ public void PostProcessGeneratedCodeAddsApplicationInstanceProperty()
+ {
+ const string expectedPropertyCode = @"
+protected Foo.Bar ApplicationInstance {
+ get {
+ return ((Foo.Bar)(Context.ApplicationInstance));
+ }
+}
+";
+
+ // Arrange
+ WebPageRazorHost host = new WebPageRazorHost("Foo.cshtml")
+ {
+ GlobalAsaxTypeName = "Foo.Bar"
+ };
+ CodeGeneratorContext context = CodeGeneratorContext.Create(
+ host,
+ () => new CSharpCodeWriter(),
+ "TestClass",
+ "TestNs",
+ "TestFile.cshtml",
+ shouldGenerateLinePragmas: true);
+
+ // Act
+ host.PostProcessGeneratedCode(context);
+
+ // Assert
+ CodeMemberProperty property = context.GeneratedClass.Members[0] as CodeMemberProperty;
+ Assert.NotNull(property);
+
+ CSharpCodeProvider provider = new CSharpCodeProvider();
+ StringBuilder builder = new StringBuilder();
+ using (StringWriter writer = new StringWriter(builder))
+ {
+ provider.GenerateCodeFromMember(property, writer, new CodeGeneratorOptions());
+ }
+
+ Assert.Equal(expectedPropertyCode, builder.ToString());
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Razor.Test/WebRazorHostFactoryTest.cs b/test/System.Web.WebPages.Razor.Test/WebRazorHostFactoryTest.cs
new file mode 100644
index 00000000..cfae372c
--- /dev/null
+++ b/test/System.Web.WebPages.Razor.Test/WebRazorHostFactoryTest.cs
@@ -0,0 +1,387 @@
+using System.Configuration;
+using System.Reflection;
+using System.Web.Configuration;
+using System.Web.WebPages.Razor.Configuration;
+using System.Web.WebPages.Razor.Resources;
+using System.Web.WebPages.TestUtils;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.WebPages.Razor.Test
+{
+ public class WebRazorHostFactoryTest
+ {
+ public class TestFactory : WebRazorHostFactory
+ {
+ public override WebPageRazorHost CreateHost(string virtualPath, string physicalPath = null)
+ {
+ return new TestHost();
+ }
+ }
+
+ public class TestHost : WebPageRazorHost
+ {
+ public TestHost()
+ : base("Foo.cshtml")
+ {
+ }
+
+ public new void RegisterSpecialFile(string fileName, Type baseType)
+ {
+ base.RegisterSpecialFile(fileName, baseType);
+ }
+
+ public new void RegisterSpecialFile(string fileName, string baseType)
+ {
+ base.RegisterSpecialFile(fileName, baseType);
+ }
+ }
+
+ [Fact]
+ public void CreateHostReturnsWebPageHostWithWebPageAsBaseClassIfVirtualPathIsNormalPage()
+ {
+ // Act
+ WebPageRazorHost host = new WebRazorHostFactory().CreateHost("~/Foo/Bar/Baz.cshtml", null);
+
+ // Assert
+ Assert.IsType<WebPageRazorHost>(host);
+ Assert.Equal(WebPageRazorHost.PageBaseClass, host.DefaultBaseClass);
+ }
+
+ [Fact]
+ public void CreateHostReturnsWebPageHostWithInitPageAsBaseClassIfVirtualPathIsPageStart()
+ {
+ // Act
+ WebPageRazorHost host = new WebRazorHostFactory().CreateHost("~/Foo/Bar/_pagestart.cshtml", null);
+
+ // Assert
+ Assert.IsType<WebPageRazorHost>(host);
+ Assert.Equal(typeof(StartPage).FullName, host.DefaultBaseClass);
+ }
+
+ [Fact]
+ public void CreateHostReturnsWebPageHostWithStartPageAsBaseClassIfVirtualPathIsAppStart()
+ {
+ // Act
+ WebPageRazorHost host = new WebRazorHostFactory().CreateHost("~/Foo/Bar/_appstart.cshtml", null);
+
+ // Assert
+ Assert.IsType<WebPageRazorHost>(host);
+ Assert.Equal(typeof(ApplicationStartPage).FullName, host.DefaultBaseClass);
+ }
+
+ [Fact]
+ public void CreateHostPassesPhysicalPathOnToWebCodeRazorHost()
+ {
+ // Act
+ WebPageRazorHost host = new WebRazorHostFactory().CreateHost("~/Foo/Bar/Baz/App_Code/Bar", @"C:\Foo.cshtml");
+
+ // Assert
+ Assert.Equal(@"C:\Foo.cshtml", host.PhysicalPath);
+ }
+
+ [Fact]
+ public void CreateHostPassesPhysicalPathOnToWebPageRazorHost()
+ {
+ // Act
+ WebPageRazorHost host = new WebRazorHostFactory().CreateHost("~/Foo/Bar/Baz/Bar", @"C:\Foo.cshtml");
+
+ // Assert
+ Assert.Equal(@"C:\Foo.cshtml", host.PhysicalPath);
+ }
+
+ [Fact]
+ public void CreateHostFromConfigRequiresNonNullVirtualPath()
+ {
+ Assert.ThrowsArgumentNullOrEmptyString(() => WebRazorHostFactory.CreateHostFromConfig(virtualPath: null,
+ physicalPath: "foo"), "virtualPath");
+ Assert.ThrowsArgumentNullOrEmptyString(() => WebRazorHostFactory.CreateHostFromConfig(config: new RazorWebSectionGroup(),
+ virtualPath: null,
+ physicalPath: "foo"), "virtualPath");
+ }
+
+ [Fact]
+ public void CreateHostFromConfigRequiresNonEmptyVirtualPath()
+ {
+ Assert.ThrowsArgumentNullOrEmptyString(() => WebRazorHostFactory.CreateHostFromConfig(virtualPath: String.Empty,
+ physicalPath: "foo"), "virtualPath");
+ Assert.ThrowsArgumentNullOrEmptyString(() => WebRazorHostFactory.CreateHostFromConfig(config: new RazorWebSectionGroup(),
+ virtualPath: String.Empty,
+ physicalPath: "foo"), "virtualPath");
+ }
+
+ [Fact]
+ public void CreateHostFromConfigRequiresNonNullSectionGroup()
+ {
+ Assert.ThrowsArgumentNull(() => WebRazorHostFactory.CreateHostFromConfig(config: (RazorWebSectionGroup)null,
+ virtualPath: String.Empty,
+ physicalPath: "foo"), "config");
+ }
+
+ [Fact]
+ public void CreateHostFromConfigReturnsWebCodeHostIfVirtualPathStartsWithAppCode()
+ {
+ // Act
+ WebPageRazorHost host = WebRazorHostFactory.CreateHostFromConfigCore(null, "~/App_Code/Bar.cshtml", null);
+
+ // Assert
+ Assert.IsType<WebCodeRazorHost>(host);
+ }
+
+ [Fact]
+ public void CreateHostFromConfigUsesDefaultFactoryIfNoRazorWebSectionGroupFound()
+ {
+ // Act
+ WebPageRazorHost host = WebRazorHostFactory.CreateHostFromConfigCore(null, "/Foo/Bar.cshtml", null);
+
+ // Assert
+ Assert.IsType<WebPageRazorHost>(host);
+ }
+
+ [Fact]
+ public void CreateHostFromConfigUsesDefaultFactoryIfNoHostSectionFound()
+ {
+ // Arrange
+ RazorWebSectionGroup config = new RazorWebSectionGroup()
+ {
+ Host = null,
+ Pages = null
+ };
+
+ // Act
+ WebPageRazorHost host = WebRazorHostFactory.CreateHostFromConfig(config, "/Foo/Bar.cshtml", null);
+
+ // Assert
+ Assert.IsType<WebPageRazorHost>(host);
+ }
+
+ [Fact]
+ public void CreateHostFromConfigUsesDefaultFactoryIfNullFactoryType()
+ {
+ // Arrange
+ RazorWebSectionGroup config = new RazorWebSectionGroup()
+ {
+ Host = new HostSection()
+ {
+ FactoryType = null
+ },
+ Pages = null
+ };
+
+ // Act
+ WebPageRazorHost host = WebRazorHostFactory.CreateHostFromConfig(config, "/Foo/Bar.cshtml", null);
+
+ // Assert
+ Assert.IsType<WebPageRazorHost>(host);
+ }
+
+ [Fact]
+ public void CreateHostFromConfigUsesFactorySpecifiedInConfig()
+ {
+ // Arrange
+ RazorWebSectionGroup config = new RazorWebSectionGroup()
+ {
+ Host = new HostSection()
+ {
+ FactoryType = typeof(TestFactory).FullName
+ },
+ Pages = null
+ };
+ WebRazorHostFactory.TypeFactory = name => Assembly.GetExecutingAssembly().GetType(name, throwOnError: false);
+
+ // Act
+ WebPageRazorHost host = WebRazorHostFactory.CreateHostFromConfig(config, "/Foo/Bar.cshtml", null);
+
+ // Assert
+ Assert.IsType<TestHost>(host);
+ }
+
+ [Fact]
+ public void CreateHostFromConfigThrowsInvalidOperationExceptionIfFactoryTypeNotFound()
+ {
+ // Arrange
+ RazorWebSectionGroup config = new RazorWebSectionGroup()
+ {
+ Host = new HostSection()
+ {
+ FactoryType = "Foo"
+ },
+ Pages = null
+ };
+ WebRazorHostFactory.TypeFactory = name => Assembly.GetExecutingAssembly().GetType(name, throwOnError: false);
+
+ // Act
+ Assert.Throws<InvalidOperationException>(
+ () => WebRazorHostFactory.CreateHostFromConfig(config, "/Foo/Bar.cshtml", null),
+ String.Format(RazorWebResources.Could_Not_Locate_FactoryType, "Foo"));
+ }
+
+ [Fact]
+ public void CreateHostFromConfigAppliesBaseTypeFromConfigToHost()
+ {
+ // Arrange
+ RazorWebSectionGroup config = new RazorWebSectionGroup()
+ {
+ Host = null,
+ Pages = new RazorPagesSection()
+ {
+ PageBaseType = "System.Foo.Bar"
+ }
+ };
+ WebRazorHostFactory.TypeFactory = name => Assembly.GetExecutingAssembly().GetType(name, throwOnError: false);
+
+ // Act
+ WebPageRazorHost host = WebRazorHostFactory.CreateHostFromConfig(config, "/Foo/Bar.cshtml", null);
+
+ // Assert
+ Assert.Equal("System.Foo.Bar", host.DefaultBaseClass);
+ }
+
+ [Fact]
+ public void CreateHostFromConfigIgnoresBaseTypeFromConfigIfPageIsPageStart()
+ {
+ // Arrange
+ RazorWebSectionGroup config = new RazorWebSectionGroup()
+ {
+ Host = null,
+ Pages = new RazorPagesSection()
+ {
+ PageBaseType = "System.Foo.Bar"
+ }
+ };
+ WebRazorHostFactory.TypeFactory = name => Assembly.GetExecutingAssembly().GetType(name, throwOnError: false);
+
+ // Act
+ WebPageRazorHost host = WebRazorHostFactory.CreateHostFromConfig(config, "/Foo/_pagestart.cshtml", null);
+
+ // Assert
+ Assert.Equal(typeof(StartPage).FullName, host.DefaultBaseClass);
+ }
+
+ [Fact]
+ public void CreateHostFromConfigIgnoresBaseTypeFromConfigIfPageIsAppStart()
+ {
+ // Arrange
+ RazorWebSectionGroup config = new RazorWebSectionGroup()
+ {
+ Host = null,
+ Pages = new RazorPagesSection()
+ {
+ PageBaseType = "System.Foo.Bar"
+ }
+ };
+ WebRazorHostFactory.TypeFactory = name => Assembly.GetExecutingAssembly().GetType(name, throwOnError: false);
+
+ // Act
+ WebPageRazorHost host = WebRazorHostFactory.CreateHostFromConfig(config, "/Foo/_appstart.cshtml", null);
+
+ // Assert
+ Assert.Equal(typeof(ApplicationStartPage).FullName, host.DefaultBaseClass);
+ }
+
+ [Fact]
+ public void CreateHostFromConfigMergesNamespacesFromConfigToHost()
+ {
+ // Arrange
+ RazorWebSectionGroup config = new RazorWebSectionGroup()
+ {
+ Host = null,
+ Pages = new RazorPagesSection()
+ {
+ Namespaces = new NamespaceCollection()
+ {
+ new NamespaceInfo("System"),
+ new NamespaceInfo("Foo")
+ }
+ }
+ };
+ WebRazorHostFactory.TypeFactory = name => Assembly.GetExecutingAssembly().GetType(name, throwOnError: false);
+
+ // Act
+ WebPageRazorHost host = WebRazorHostFactory.CreateHostFromConfig(config, "/Foo/Bar.cshtml", null);
+
+ // Assert
+ Assert.True(host.NamespaceImports.Contains("System"));
+ Assert.True(host.NamespaceImports.Contains("Foo"));
+ }
+
+ [Fact]
+ public void HostFactoryTypeIsCorrectlyLoadedFromConfig()
+ {
+ // Act
+ RazorWebSectionGroup group = GetRazorGroup();
+ HostSection host = (HostSection)group.Host;
+
+ // Assert
+ Assert.NotNull(host);
+ Assert.Equal("System.Web.WebPages.Razor.Test.TestRazorHostFactory, System.Web.WebPages.Razor.Test", host.FactoryType);
+ }
+
+ [Fact]
+ public void PageBaseTypeIsCorrectlyLoadedFromConfig()
+ {
+ // Act
+ RazorWebSectionGroup group = GetRazorGroup();
+ RazorPagesSection pages = (RazorPagesSection)group.Pages;
+
+ // Assert
+ Assert.NotNull(pages);
+ Assert.Equal("System.Web.WebPages.Razor.Test.TestPageBase, System.Web.WebPages.Razor.Test", pages.PageBaseType);
+ }
+
+ [Fact]
+ public void NamespacesAreCorrectlyLoadedFromConfig()
+ {
+ // Act
+ RazorWebSectionGroup group = GetRazorGroup();
+ RazorPagesSection pages = (RazorPagesSection)group.Pages;
+
+ // Assert
+ Assert.NotNull(pages);
+ Assert.Equal(1, pages.Namespaces.Count);
+ Assert.Equal("System.Text.RegularExpressions", pages.Namespaces[0].Namespace);
+ }
+
+ [Fact]
+ public void RegisterSpecialFile_ThrowsOnNullFileName()
+ {
+ TestHost host = new TestHost();
+ Assert.ThrowsArgumentNullOrEmptyString(() => host.RegisterSpecialFile(null, typeof(string)), "fileName");
+ Assert.ThrowsArgumentNullOrEmptyString(() => host.RegisterSpecialFile(null, "string"), "fileName");
+ }
+
+ [Fact]
+ public void RegisterSpecialFile_ThrowsOnEmptyFileName()
+ {
+ TestHost host = new TestHost();
+ Assert.ThrowsArgumentNullOrEmptyString(() => host.RegisterSpecialFile(String.Empty, typeof(string)), "fileName");
+ Assert.ThrowsArgumentNullOrEmptyString(() => host.RegisterSpecialFile(String.Empty, "string"), "fileName");
+ }
+
+ [Fact]
+ public void RegisterSpecialFile_ThrowsOnNullBaseType()
+ {
+ TestHost host = new TestHost();
+ Assert.ThrowsArgumentNull(() => host.RegisterSpecialFile("file", (Type)null), "baseType");
+ }
+
+ [Fact]
+ public void RegisterSpecialFile_ThrowsOnNullBaseTypeName()
+ {
+ TestHost host = new TestHost();
+ Assert.ThrowsArgumentNullOrEmptyString(() => host.RegisterSpecialFile("file", (string)null), "baseTypeName");
+ }
+
+ [Fact]
+ public void RegisterSpecialFile_ThrowsOnEmptyBaseTypeName()
+ {
+ TestHost host = new TestHost();
+ Assert.ThrowsArgumentNullOrEmptyString(() => host.RegisterSpecialFile("file", String.Empty), "baseTypeName");
+ }
+
+ private static RazorWebSectionGroup GetRazorGroup()
+ {
+ return (RazorWebSectionGroup)ConfigurationManager.OpenExeConfiguration(null).GetSectionGroup(RazorWebSectionGroup.GroupName);
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Razor.Test/app.config b/test/System.Web.WebPages.Razor.Test/app.config
new file mode 100644
index 00000000..a78b27b5
--- /dev/null
+++ b/test/System.Web.WebPages.Razor.Test/app.config
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<configuration>
+ <configSections>
+ <sectionGroup name="system.web.webPages.razor" type="System.Web.WebPages.Razor.Configuration.RazorWebSectionGroup, System.Web.WebPages.Razor, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
+ <section name="host" type="System.Web.WebPages.Razor.Configuration.HostSection, System.Web.WebPages.Razor, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
+ <section name="pages" type="System.Web.WebPages.Razor.Configuration.RazorPagesSection, System.Web.WebPages.Razor, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
+ </sectionGroup>
+ </configSections>
+ <system.web.webPages.razor>
+ <host factoryType="System.Web.WebPages.Razor.Test.TestRazorHostFactory, System.Web.WebPages.Razor.Test" />
+ <pages pageBaseType="System.Web.WebPages.Razor.Test.TestPageBase, System.Web.WebPages.Razor.Test">
+ <namespaces>
+ <add namespace="System.IO.Packaging" />
+ <clear />
+ <add namespace="System.Security" />
+ <add namespace="System.Text.RegularExpressions" />
+ <remove namespace="System.Security" />
+ </namespaces>
+ </pages>
+ </system.web.webPages.razor>
+</configuration> \ No newline at end of file
diff --git a/test/System.Web.WebPages.Razor.Test/packages.config b/test/System.Web.WebPages.Razor.Test/packages.config
new file mode 100644
index 00000000..d5aa6401
--- /dev/null
+++ b/test/System.Web.WebPages.Razor.Test/packages.config
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="Moq" version="4.0.10827" />
+ <package id="xunit" version="1.9.0.1566" />
+ <package id="xunit.extensions" version="1.9.0.1566" />
+</packages> \ No newline at end of file
diff --git a/test/System.Web.WebPages.Test/App.config b/test/System.Web.WebPages.Test/App.config
new file mode 100644
index 00000000..a740ab52
--- /dev/null
+++ b/test/System.Web.WebPages.Test/App.config
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<configuration>
+ <system.data>
+ <DbProviderFactories>
+ <remove invariant="System.Data.SqlServerCe.4.0"></remove>
+ <add name="Microsoft SQL Server Compact Data Provider" invariant="System.Data.SqlServerCe.4.0" description=".NET Framework Data Provider for Microsoft SQL Server Compact" type="System.Data.SqlServerCe.SqlCeProviderFactory, System.Data.SqlServerCe, Version=4.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91"/>
+ </DbProviderFactories>
+ </system.data>
+ <runtime>
+ <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
+ <dependentAssembly>
+ <!-- Need this because the BinarySerializer uses the TypeForwardedFrom attribute and deserializes to the original assembly (MVC 2.0) for the HttpAntiForgeryException test -->
+ <assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" />
+ <bindingRedirect oldVersion="1.0.0.0-4.0.0.0" newVersion="4.0.0.0" />
+ </dependentAssembly>
+ </assemblyBinding>
+ </runtime>
+</configuration> \ No newline at end of file
diff --git a/test/System.Web.WebPages.Test/ApplicationParts/ApplicationPartRegistryTest.cs b/test/System.Web.WebPages.Test/ApplicationParts/ApplicationPartRegistryTest.cs
new file mode 100644
index 00000000..14acf0fc
--- /dev/null
+++ b/test/System.Web.WebPages.Test/ApplicationParts/ApplicationPartRegistryTest.cs
@@ -0,0 +1,150 @@
+using System.Web.WebPages.ApplicationParts;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.WebPages.Test.ApplicationModule
+{
+ public class ApplicationPartRegistryTest
+ {
+ [Fact]
+ public void ApplicationModuleGeneratesRootRelativePaths()
+ {
+ // Arrange
+ var path1 = "foo/bar";
+ var path2 = "~/xyz/pqr";
+ var root1 = "~/myappmodule";
+ var root2 = "~/myappmodule2/";
+
+ // Act
+ var actualPath11 = ApplicationPartRegistry.GetRootRelativeVirtualPath(root1, path1);
+ var actualPath12 = ApplicationPartRegistry.GetRootRelativeVirtualPath(root1, path2);
+ var actualPath21 = ApplicationPartRegistry.GetRootRelativeVirtualPath(root2, path1);
+ var actualPath22 = ApplicationPartRegistry.GetRootRelativeVirtualPath(root2, path2);
+
+ // Assert
+ Assert.Equal(actualPath11, root1 + "/" + path1);
+ Assert.Equal(actualPath12, root1 + path2.TrimStart('~'));
+ Assert.Equal(actualPath21, root2 + path1);
+ Assert.Equal(actualPath22, root2 + path2.TrimStart('~', '/'));
+ }
+
+ [Fact]
+ public void ApplicationPartRegistryLooksUpPartsByName()
+ {
+ // Arrange
+ var part = new ApplicationPart(BuildAssembly(), "~/mymodule");
+ var dictionary = new DictionaryBasedVirtualPathFactory();
+ var registry = new ApplicationPartRegistry(dictionary);
+ Func<object> myFunc = () => "foo";
+
+ // Act
+ registry.Register(part, myFunc);
+
+ // Assert
+ Assert.Equal(registry["my-assembly"], part);
+ Assert.Equal(registry["MY-aSSembly"], part);
+ }
+
+ [Fact]
+ public void ApplicationPartRegistryLooksUpPartsByAssembly()
+ {
+ // Arrange
+ var assembly = BuildAssembly();
+ var part = new ApplicationPart(assembly, "~/mymodule");
+ var dictionary = new DictionaryBasedVirtualPathFactory();
+ var registry = new ApplicationPartRegistry(dictionary);
+ Func<object> myFunc = () => "foo";
+
+ // Act
+ registry.Register(part, myFunc);
+
+ // Assert
+ Assert.Equal(registry[assembly], part);
+ }
+
+ [Fact]
+ public void RegisterThrowsIfAssemblyAlreadyRegistered()
+ {
+ // Arrange
+ var part = new ApplicationPart(BuildAssembly(), "~/mymodule");
+ var dictionary = new DictionaryBasedVirtualPathFactory();
+ var registry = new ApplicationPartRegistry(dictionary);
+ Func<object> myFunc = () => "foo";
+
+ // Act
+ registry.Register(part, myFunc);
+
+ // Assert
+ Assert.Throws<InvalidOperationException>(() => registry.Register(part, myFunc),
+ String.Format("The assembly \"{0}\" is already registered.", part.Assembly.ToString()));
+ }
+
+ [Fact]
+ public void RegisterThrowsIfPathAlreadyRegistered()
+ {
+ // Arrange
+ var part = new ApplicationPart(BuildAssembly(), "~/mymodule");
+ var dictionary = new DictionaryBasedVirtualPathFactory();
+ var registry = new ApplicationPartRegistry(dictionary);
+ Func<object> myFunc = () => "foo";
+
+ // Act
+ registry.Register(part, myFunc);
+
+ // Assert
+ var newPart = new ApplicationPart(BuildAssembly("different-assembly"), "~/mymodule");
+ Assert.Throws<InvalidOperationException>(() => registry.Register(newPart, myFunc),
+ "An application module is already registered for virtual path \"~/mymodule/\".");
+ }
+
+ [Fact]
+ public void RegisterCreatesRoutesForValidPages()
+ {
+ // Arrange
+ var part = new ApplicationPart(BuildAssembly(), "~/mymodule");
+ var dictionary = new DictionaryBasedVirtualPathFactory();
+ var registry = new ApplicationPartRegistry(dictionary);
+ Func<object> myFunc = () => "foo";
+
+ // Act
+ registry.Register(part, myFunc);
+
+ // Assert
+ Assert.True(dictionary.Exists("~/mymodule/Page1"));
+ Assert.Equal(dictionary.CreateInstance("~/mymodule/Page1"), "foo");
+ Assert.False(dictionary.Exists("~/mymodule/Page2"));
+ Assert.False(dictionary.Exists("~/mymodule/Page3"));
+ }
+
+ private static IResourceAssembly BuildAssembly(string name = "my-assembly")
+ {
+ Mock<TestResourceAssembly> assembly = new Mock<TestResourceAssembly>();
+ assembly.SetupGet(c => c.Name).Returns(name);
+ assembly.Setup(c => c.GetHashCode()).Returns(name.GetHashCode());
+ assembly.Setup(c => c.Equals(It.IsAny<TestResourceAssembly>())).Returns((TestResourceAssembly c) => c.Name == name);
+
+ assembly.Setup(c => c.GetTypes()).Returns(new[]
+ {
+ BuildPageType(inherits: true, virtualPath: "~/Page1"),
+ BuildPageType(inherits: true, virtualPath: null),
+ BuildPageType(inherits: false, virtualPath: "~/Page3"),
+ });
+
+ return assembly.Object;
+ }
+
+ private static Type BuildPageType(bool inherits, string virtualPath)
+ {
+ Mock<Type> type = new Mock<Type>();
+ type.Setup(c => c.IsSubclassOf(typeof(WebPageRenderingBase))).Returns(inherits);
+
+ if (virtualPath != null)
+ {
+ type.Setup(c => c.GetCustomAttributes(typeof(PageVirtualPathAttribute), false))
+ .Returns(new[] { new PageVirtualPathAttribute(virtualPath) });
+ }
+ return type.Object;
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/ApplicationParts/ApplicationPartTest.cs b/test/System.Web.WebPages.Test/ApplicationParts/ApplicationPartTest.cs
new file mode 100644
index 00000000..488ee62e
--- /dev/null
+++ b/test/System.Web.WebPages.Test/ApplicationParts/ApplicationPartTest.cs
@@ -0,0 +1,198 @@
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.WebPages.Test
+{
+ public class ApplicationPartTest
+ {
+ [Fact]
+ public void ApplicationPartThrowsIfRootVirtualPathIsNullOrEmpty()
+ {
+ // Arrange
+ var assembly = new Mock<TestResourceAssembly>().Object;
+
+ Assert.ThrowsArgumentNullOrEmptyString(() => new ApplicationPart(assembly, rootVirtualPath: null), "rootVirtualPath");
+ Assert.ThrowsArgumentNullOrEmptyString(() => new ApplicationPart(assembly, rootVirtualPath: String.Empty), "rootVirtualPath");
+ }
+
+ [Fact]
+ public void ResolveVirtualPathResolvesRegularPathsUsingBaseVirtualPath()
+ {
+ // Arrange
+ var basePath = "~/base/";
+ var path = "somefile";
+ var appPartRoot = "~/app/";
+
+ // Act
+ var virtualPath = ApplicationPart.ResolveVirtualPath(appPartRoot, basePath, path);
+
+ // Assert
+ Assert.Equal(virtualPath, "~/base/somefile");
+ }
+
+ [Fact]
+ public void ResolveVirtualPathResolvesAppRelativePathsUsingAppVirtualPath()
+ {
+ // Arrange
+ var basePath = "~/base";
+ var path = "@/somefile";
+ var appPartRoot = "~/app/";
+
+ // Act
+ var virtualPath = ApplicationPart.ResolveVirtualPath(appPartRoot, basePath, path);
+
+ // Assert
+ Assert.Equal(virtualPath, "~/app/somefile");
+ }
+
+ [Fact]
+ public void ResolveVirtualPathDoesNotAffectRootRelativePaths()
+ {
+ // Arrange
+ var basePath = "~/base";
+ var path = "~/somefile";
+ var appPartRoot = "~/app/";
+
+ // Act
+ var virtualPath = ApplicationPart.ResolveVirtualPath(appPartRoot, basePath, path);
+
+ // Assert
+ Assert.Equal(virtualPath, "~/somefile");
+ }
+
+ [Fact]
+ public void GetResourceNameFromVirtualPathForTopLevelPath()
+ {
+ // Arrange
+ var moduleName = "my-module";
+ var path = "foo.baz";
+
+ // Act
+ var name = ApplicationPart.GetResourceNameFromVirtualPath(moduleName, path);
+
+ // Assert
+ Assert.Equal(name, moduleName + "." + path);
+ }
+
+ [Fact]
+ public void GetResourceNameFromVirtualPathForItemInSubDir()
+ {
+ // Arrange
+ var moduleName = "my-module";
+ var path = "/bar/foo";
+
+ // Act
+ var name = ApplicationPart.GetResourceNameFromVirtualPath(moduleName, path);
+
+ // Assert
+ Assert.Equal(name, "my-module.bar.foo");
+ }
+
+ [Fact]
+ public void GetResourceNameFromVirtualPathForItemWithSpaces()
+ {
+ // Arrange
+ var moduleName = "my-module";
+ var path = "/program files/data files/my file .foo";
+
+ // Act
+ var name = ApplicationPart.GetResourceNameFromVirtualPath(moduleName, path);
+
+ // Assert
+ Assert.Equal(name, "my-module.program_files.data_files.my file .foo");
+ }
+
+ [Fact]
+ public void GetResourceVirtualPathForTopLevelItem()
+ {
+ // Arrange
+ var moduleName = "my-module";
+ var moduleRoot = "~/root-path";
+ var path = moduleRoot + "/foo.txt";
+
+ // Act
+ var virtualPath = ApplicationPart.GetResourceVirtualPath(moduleName, moduleRoot, path);
+
+ // Assert
+ Assert.Equal(virtualPath, "~/r.ashx/" + moduleName + "/" + "foo.txt");
+ }
+
+ [Fact]
+ public void GetResourceVirtualPathForTopLevelItemAndModuleRootWithTrailingSlash()
+ {
+ // Arrange
+ var moduleName = "my-module";
+ var moduleRoot = "~/root-path/";
+ var path = moduleRoot + "/foo.txt";
+
+ // Act
+ var virtualPath = ApplicationPart.GetResourceVirtualPath(moduleName, moduleRoot, path);
+
+ // Assert
+ Assert.Equal(virtualPath, "~/r.ashx/" + moduleName + "/" + "foo.txt");
+ }
+
+ [Fact]
+ public void GetResourceVirtualPathForTopLevelItemAndNestedModuleRootPath()
+ {
+ // Arrange
+ var moduleName = "my-module";
+ var moduleRoot = "~/root-path/sub-path";
+ var path = moduleRoot + "/foo.txt";
+
+ // Act
+ var virtualPath = ApplicationPart.GetResourceVirtualPath(moduleName, moduleRoot, path);
+
+ // Assert
+ Assert.Equal(virtualPath, "~/r.ashx/" + moduleName + "/" + "foo.txt");
+ }
+
+ [Fact]
+ public void GetResourceVirtualPathEncodesModuleName()
+ {
+ // Arrange
+ var moduleName = "Debugger Package v?&%";
+ var moduleRoot = "~/root-path/sub-path";
+ var path = moduleRoot + "/foo.txt";
+
+ // Act
+ var virtualPath = ApplicationPart.GetResourceVirtualPath(moduleName, moduleRoot, path);
+
+ // Assert
+ Assert.Equal(virtualPath, "~/r.ashx/" + "Debugger%20Package%20v?&%" + "/" + "foo.txt");
+ }
+
+ [Fact]
+ public void GetResourceVirtualPathForNestedItemPath()
+ {
+ // Arrange
+ var moduleName = "DebuggerPackage";
+ var moduleRoot = "~/root-path/sub-path";
+ var itemPath = "some-path/some-more-please/foo.txt";
+ var path = moduleRoot + "/" + itemPath;
+
+ // Act
+ var virtualPath = ApplicationPart.GetResourceVirtualPath(moduleName, moduleRoot, path);
+
+ // Assert
+ Assert.Equal(virtualPath, "~/r.ashx/" + moduleName + "/" + itemPath);
+ }
+
+ [Fact]
+ public void GetResourceVirtualPathForItemPathWithParameters()
+ {
+ // Arrange
+ var moduleName = "DebuggerPackage";
+ var moduleRoot = "~/root-path/sub-path";
+ var itemPath = "some-path/some-more-please/foo.jpg?size=45&height=20";
+ var path = moduleRoot + "/" + itemPath;
+
+ // Act
+ var virtualPath = ApplicationPart.GetResourceVirtualPath(moduleName, moduleRoot, path);
+
+ // Assert
+ Assert.Equal(virtualPath, "~/r.ashx/" + moduleName + "/" + itemPath);
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/ApplicationParts/MimeMappingTest.cs b/test/System.Web.WebPages.Test/ApplicationParts/MimeMappingTest.cs
new file mode 100644
index 00000000..ef3ab63a
--- /dev/null
+++ b/test/System.Web.WebPages.Test/ApplicationParts/MimeMappingTest.cs
@@ -0,0 +1,61 @@
+using Microsoft.Internal.Web.Utils;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.WebPages.Test
+{
+ public class MimeMappingTest
+ {
+ [Fact]
+ public void MimeMappingThrowsForNullFileName()
+ {
+ // Arrange
+ string fileName = null;
+
+ // Act and Assert
+ Assert.ThrowsArgumentNull(() => MimeMapping.GetMimeMapping(fileName), "fileName");
+ }
+
+ [Fact]
+ public void MimeMappingReturnsGenericTypeForUnknownExtensions()
+ {
+ // Arrange
+ string fileName = "file.does-not-exist";
+
+ // Act
+ string mimeType = MimeMapping.GetMimeMapping(fileName);
+
+ // Assert
+ Assert.Equal("application/octet-stream", mimeType);
+ }
+
+ [Fact]
+ public void MimeMappingReturnsGenericTypeForNoExtensions()
+ {
+ // Arrange
+ string fileName = "file";
+
+ // Act
+ string mimeType = MimeMapping.GetMimeMapping(fileName);
+
+ // Assert
+ Assert.Equal("application/octet-stream", mimeType);
+ }
+
+ [Fact]
+ public void MimeMappingPerformsCaseInsensitiveSearches()
+ {
+ // Arrange
+ string fileName1 = "file.doc";
+ string fileName2 = "file.dOC";
+
+ // Act
+ string mimeType1 = MimeMapping.GetMimeMapping(fileName1);
+ string mimeType2 = MimeMapping.GetMimeMapping(fileName2);
+
+ // Assert
+ Assert.Equal("application/msword", mimeType1);
+ Assert.Equal("application/msword", mimeType2);
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/ApplicationParts/ResourceHandlerTest.cs b/test/System.Web.WebPages.Test/ApplicationParts/ResourceHandlerTest.cs
new file mode 100644
index 00000000..06e6225b
--- /dev/null
+++ b/test/System.Web.WebPages.Test/ApplicationParts/ResourceHandlerTest.cs
@@ -0,0 +1,62 @@
+using System.IO;
+using System.Text;
+using System.Web.WebPages.ApplicationParts;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.WebPages.Test
+{
+ public class ResourceHandlerTest
+ {
+ private const string _fileContent = "contents of jpeg file";
+
+ [Fact]
+ public void ResourceHandlerWritesContentsOfFileToStream()
+ {
+ // Arrange
+ var applicationPart = new ApplicationPart(BuildAssembly(), "~/my-app-assembly");
+ MemoryStream stream = new MemoryStream();
+ var response = new Mock<HttpResponseBase>();
+ response.SetupGet(c => c.OutputStream).Returns(stream);
+ response.SetupSet(c => c.ContentType = "image/jpeg").Verifiable();
+ var resourceHandler = new ResourceHandler(applicationPart, "bar.foo.jpg");
+
+ // Act
+ resourceHandler.ProcessRequest(response.Object);
+
+ // Assert
+ response.Verify();
+ Assert.Equal(Encoding.Default.GetString(stream.ToArray()), _fileContent);
+ }
+
+ [Fact]
+ public void ResourceHandlerThrows404IfResourceNotFound()
+ {
+ // Arrange
+ var applicationPart = new ApplicationPart(BuildAssembly(), "~/my-app-assembly");
+ MemoryStream stream = new MemoryStream();
+ var response = new Mock<HttpResponseBase>();
+ response.SetupGet(c => c.OutputStream).Returns(stream);
+ response.SetupSet(c => c.ContentType = "image/jpeg").Verifiable();
+ var resourceHandler = new ResourceHandler(applicationPart, "does-not-exist");
+
+ // Act and Assert
+ Assert.Throws<HttpException>(() => resourceHandler.ProcessRequest(response.Object),
+ "The resource file \"does-not-exist\" could not be found.");
+ }
+
+ private static IResourceAssembly BuildAssembly(string name = "my-assembly")
+ {
+ Mock<TestResourceAssembly> assembly = new Mock<TestResourceAssembly>();
+ assembly.SetupGet(c => c.Name).Returns("my-assembly");
+
+ byte[] content = Encoding.Default.GetBytes(_fileContent);
+ assembly.Setup(c => c.GetManifestResourceStream("my-assembly.bar.foo.jpg")).Returns(new MemoryStream(content));
+
+ assembly.Setup(c => c.GetManifestResourceNames()).Returns(new[] { "my-assembly.bar.foo.jpg" });
+
+ return assembly.Object;
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/ApplicationParts/TestResourceAssembly.cs b/test/System.Web.WebPages.Test/ApplicationParts/TestResourceAssembly.cs
new file mode 100644
index 00000000..6e475f89
--- /dev/null
+++ b/test/System.Web.WebPages.Test/ApplicationParts/TestResourceAssembly.cs
@@ -0,0 +1,17 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Web.WebPages.ApplicationParts;
+
+namespace System.Web.WebPages.Test
+{
+ public abstract class TestResourceAssembly : IResourceAssembly
+ {
+ public abstract string Name { get; }
+
+ public abstract Stream GetManifestResourceStream(string name);
+
+ public abstract IEnumerable<string> GetManifestResourceNames();
+
+ public abstract IEnumerable<Type> GetTypes();
+ }
+}
diff --git a/test/System.Web.WebPages.Test/Extensions/HttpContextExtensionsTest.cs b/test/System.Web.WebPages.Test/Extensions/HttpContextExtensionsTest.cs
new file mode 100644
index 00000000..d70be315
--- /dev/null
+++ b/test/System.Web.WebPages.Test/Extensions/HttpContextExtensionsTest.cs
@@ -0,0 +1,83 @@
+using System;
+using System.Web;
+using System.Web.WebPages;
+using Moq;
+using Xunit;
+
+namespace Microsoft.WebPages.Test.Helpers
+{
+ public class HttpContextExtensionsTest
+ {
+ class RedirectData
+ {
+ public string RequestUrl { get; set; }
+ public string RedirectUrl { get; set; }
+ }
+
+ private static HttpContextBase GetContextForRedirectLocal(RedirectData data)
+ {
+ Mock<HttpContextBase> contextMock = new Mock<HttpContextBase>();
+ contextMock.Setup(context => context.Request.Url).Returns(new Uri(data.RequestUrl));
+ contextMock.Setup(context => context.Response.Redirect(It.IsAny<string>())).Callback((string url) => data.RedirectUrl = url);
+ return contextMock.Object;
+ }
+
+ [Fact]
+ public void RedirectLocalWithNullGoesToRootTest()
+ {
+ RedirectData data = new RedirectData() { RequestUrl = "http://foo" };
+ var context = GetContextForRedirectLocal(data);
+ context.RedirectLocal("");
+ Assert.Equal("~/", data.RedirectUrl);
+ }
+
+ [Fact]
+ public void RedirectLocalWithEmptyStringGoesToRootTest()
+ {
+ RedirectData data = new RedirectData() { RequestUrl = "http://foo" };
+ var context = GetContextForRedirectLocal(data);
+ context.RedirectLocal("");
+ Assert.Equal("~/", data.RedirectUrl);
+ }
+
+ [Fact]
+ public void RedirectLocalWithNonLocalGoesToRootTest()
+ {
+ RedirectData data = new RedirectData() { RequestUrl = "http://foo" };
+ var context = GetContextForRedirectLocal(data);
+ context.RedirectLocal("");
+ Assert.Equal("~/", data.RedirectUrl);
+ }
+
+ [Fact]
+ public void RedirectLocalWithDifferentHostGoesToRootTest()
+ {
+ RedirectData data = new RedirectData() { RequestUrl = "http://foo" };
+ var context = GetContextForRedirectLocal(data);
+ context.RedirectLocal("http://bar");
+ Assert.Equal("~/", data.RedirectUrl);
+ }
+
+ [Fact]
+ public void RedirectLocalOnSameHostTest()
+ {
+ RedirectData data = new RedirectData() { RequestUrl = "http://foo" };
+ var context = GetContextForRedirectLocal(data);
+ context.RedirectLocal("http://foo/bar/baz");
+ Assert.Equal("~/", data.RedirectUrl);
+ context.RedirectLocal("http://foo/bar/baz/woot.htm");
+ Assert.Equal("~/", data.RedirectUrl);
+ }
+
+ [Fact]
+ public void RedirectLocalRelativeTest()
+ {
+ RedirectData data = new RedirectData() { RequestUrl = "http://foo" };
+ var context = GetContextForRedirectLocal(data);
+ context.RedirectLocal("/bar");
+ Assert.Equal("/bar", data.RedirectUrl);
+ context.RedirectLocal("/bar/hey.you");
+ Assert.Equal("/bar/hey.you", data.RedirectUrl);
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/Extensions/HttpRequestExtensionsTest.cs b/test/System.Web.WebPages.Test/Extensions/HttpRequestExtensionsTest.cs
new file mode 100644
index 00000000..e7975126
--- /dev/null
+++ b/test/System.Web.WebPages.Test/Extensions/HttpRequestExtensionsTest.cs
@@ -0,0 +1,130 @@
+using System;
+using System.Web;
+using System.Web.WebPages;
+using Moq;
+using Xunit;
+
+namespace Microsoft.WebPages.Test.Helpers
+{
+ public class HttpRequestExtensionsTest
+ {
+ private static HttpRequestBase GetRequestForIsUrlLocalToHost(string url)
+ {
+ Mock<HttpContextBase> contextMock = new Mock<HttpContextBase>();
+ contextMock.Setup(context => context.Request.Url).Returns(new Uri(url));
+ return contextMock.Object.Request;
+ }
+
+ [Fact]
+ public void IsUrlLocalToHost_ReturnsFalseOnEmpty()
+ {
+ var request = GetRequestForIsUrlLocalToHost("http://www.mysite.com/");
+ Assert.False(request.IsUrlLocalToHost(null));
+ Assert.False(request.IsUrlLocalToHost(String.Empty));
+ }
+
+ [Fact]
+ public void IsUrlLocalToHost_AcceptsRootedUrls()
+ {
+ var helper = GetRequestForIsUrlLocalToHost("http://www.mysite.com/");
+ Assert.True(helper.IsUrlLocalToHost("/foo.html"));
+ Assert.True(helper.IsUrlLocalToHost("/www.hackerz.com"));
+ Assert.True(helper.IsUrlLocalToHost("/"));
+ }
+
+ [Fact]
+ public void IsUrlLocalToHost_AcceptsApplicationRelativeUrls()
+ {
+ var helper = GetRequestForIsUrlLocalToHost("http://www.mysite.com/");
+ Assert.True(helper.IsUrlLocalToHost("~/"));
+ Assert.True(helper.IsUrlLocalToHost("~/foobar.html"));
+ }
+
+ [Fact]
+ public void IsUrlLocalToHost_RejectsRelativeUrls()
+ {
+ var helper = GetRequestForIsUrlLocalToHost("http://www.mysite.com/");
+ Assert.False(helper.IsUrlLocalToHost("foobar.html"));
+ Assert.False(helper.IsUrlLocalToHost("../foobar.html"));
+ Assert.False(helper.IsUrlLocalToHost("fold/foobar.html"));
+ }
+
+ [Fact]
+ public void IsUrlLocalToHost_RejectValidButUnsafeRelativeUrls()
+ {
+ var helper = GetRequestForIsUrlLocalToHost("http://www.mysite.com/");
+ Assert.False(helper.IsUrlLocalToHost("http:/foobar.html"));
+ Assert.False(helper.IsUrlLocalToHost("hTtP:foobar.html"));
+ Assert.False(helper.IsUrlLocalToHost("http:/www.hackerz.com"));
+ Assert.False(helper.IsUrlLocalToHost("HtTpS:/www.hackerz.com"));
+ }
+
+ [Fact]
+ public void IsUrlLocalToHost_RejectsUrlsOnTheSameHost()
+ {
+ var helper = GetRequestForIsUrlLocalToHost("http://www.mysite.com/");
+ Assert.False(helper.IsUrlLocalToHost("http://www.mysite.com/appDir/foobar.html"));
+ Assert.False(helper.IsUrlLocalToHost("http://WWW.MYSITE.COM"));
+ }
+
+ [Fact]
+ public void IsUrlLocalToHost_RejectsUrlsOnLocalHost()
+ {
+ var helper = GetRequestForIsUrlLocalToHost("http://www.mysite.com/");
+ Assert.False(helper.IsUrlLocalToHost("http://localhost/foobar.html"));
+ Assert.False(helper.IsUrlLocalToHost("http://127.0.0.1/foobar.html"));
+ }
+
+ [Fact]
+ public void IsUrlLocalToHost_RejectsUrlsOnTheSameHostButDifferentScheme()
+ {
+ var helper = GetRequestForIsUrlLocalToHost("http://www.mysite.com/");
+ Assert.False(helper.IsUrlLocalToHost("https://www.mysite.com/"));
+ }
+
+ [Fact]
+ public void IsUrlLocalToHost_RejectsUrlsOnDifferentHost()
+ {
+ var helper = GetRequestForIsUrlLocalToHost("http://www.mysite.com/");
+ Assert.False(helper.IsUrlLocalToHost("http://www.hackerz.com"));
+ Assert.False(helper.IsUrlLocalToHost("https://www.hackerz.com"));
+ Assert.False(helper.IsUrlLocalToHost("hTtP://www.hackerz.com"));
+ Assert.False(helper.IsUrlLocalToHost("HtTpS://www.hackerz.com"));
+ }
+
+ [Fact]
+ public void IsUrlLocalToHost_RejectsUrlsWithTooManySchemeSeparatorCharacters()
+ {
+ var helper = GetRequestForIsUrlLocalToHost("http://www.mysite.com/");
+ Assert.False(helper.IsUrlLocalToHost("http://///www.hackerz.com/foobar.html"));
+ Assert.False(helper.IsUrlLocalToHost("https://///www.hackerz.com/foobar.html"));
+ Assert.False(helper.IsUrlLocalToHost("HtTpS://///www.hackerz.com/foobar.html"));
+
+ Assert.False(helper.IsUrlLocalToHost("http:///www.hackerz.com/foobar.html"));
+ Assert.False(helper.IsUrlLocalToHost("http:////www.hackerz.com/foobar.html"));
+ Assert.False(helper.IsUrlLocalToHost("http://///www.hackerz.com/foobar.html"));
+ }
+
+ [Fact]
+ public void IsUrlLocalToHost_RejectsUrlsWithMissingSchemeName()
+ {
+ var helper = GetRequestForIsUrlLocalToHost("http://www.mysite.com/");
+ Assert.False(helper.IsUrlLocalToHost("//www.hackerz.com"));
+ Assert.False(helper.IsUrlLocalToHost("//www.hackerz.com?"));
+ Assert.False(helper.IsUrlLocalToHost("//www.hackerz.com:80"));
+ Assert.False(helper.IsUrlLocalToHost("//www.hackerz.com/foobar.html"));
+ Assert.False(helper.IsUrlLocalToHost("///www.hackerz.com"));
+ Assert.False(helper.IsUrlLocalToHost("//////www.hackerz.com"));
+ }
+
+ [Fact]
+ public void IsUrlLocalToHost_RejectsInvalidUrls()
+ {
+ var helper = GetRequestForIsUrlLocalToHost("http://www.mysite.com/");
+ Assert.False(helper.IsUrlLocalToHost(@"http:\\www.hackerz.com"));
+ Assert.False(helper.IsUrlLocalToHost(@"http:\\www.hackerz.com\"));
+ Assert.False(helper.IsUrlLocalToHost(@"/\"));
+ Assert.False(helper.IsUrlLocalToHost(@"/\foo"));
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/Extensions/HttpResponseExtensionsTest.cs b/test/System.Web.WebPages.Test/Extensions/HttpResponseExtensionsTest.cs
new file mode 100644
index 00000000..28820664
--- /dev/null
+++ b/test/System.Web.WebPages.Test/Extensions/HttpResponseExtensionsTest.cs
@@ -0,0 +1,128 @@
+using System;
+using System.IO;
+using System.Net;
+using System.Text;
+using System.Web;
+using System.Web.WebPages;
+using Moq;
+using Xunit;
+
+namespace Microsoft.WebPages.Test.Helpers
+{
+ public class HttpResponseExtensionsTest
+ {
+ HttpResponseBase _response;
+ string _redirectUrl;
+ StringBuilder _output;
+ Stream _outputStream;
+
+ public HttpResponseExtensionsTest()
+ {
+ _output = new StringBuilder();
+ _outputStream = new MemoryStream();
+ Mock<HttpResponseBase> mockResponse = new Mock<HttpResponseBase>();
+ mockResponse.SetupProperty(response => response.StatusCode);
+ mockResponse.SetupProperty(response => response.ContentType);
+ mockResponse.Setup(response => response.Redirect(It.IsAny<string>())).Callback((string url) => _redirectUrl = url);
+ mockResponse.Setup(response => response.Write(It.IsAny<string>())).Callback((string str) => _output.Append(str));
+ mockResponse.Setup(response => response.OutputStream).Returns(_outputStream);
+ mockResponse.Setup(response => response.OutputStream).Returns(_outputStream);
+ mockResponse.Setup(response => response.Output).Returns(new StringWriter(_output));
+ _response = mockResponse.Object;
+ }
+
+ [Fact]
+ public void SetStatusWithIntTest()
+ {
+ int status = 200;
+ _response.SetStatus(status);
+ Assert.Equal(status, _response.StatusCode);
+ }
+
+ [Fact]
+ public void SetStatusWithHttpStatusCodeTest()
+ {
+ HttpStatusCode status = HttpStatusCode.Forbidden;
+ _response.SetStatus(status);
+ Assert.Equal((int)status, _response.StatusCode);
+ }
+
+ [Fact]
+ public void WriteBinaryTest()
+ {
+ string foo = "I am a string, please don't mangle me!";
+ _response.WriteBinary(ASCIIEncoding.ASCII.GetBytes(foo));
+ _outputStream.Flush();
+ _outputStream.Position = 0;
+ StreamReader reader = new StreamReader(_outputStream);
+ Assert.Equal(foo, reader.ReadToEnd());
+ }
+
+ [Fact]
+ public void WriteBinaryWithMimeTypeTest()
+ {
+ string foo = "I am a string, please don't mangle me!";
+ string mimeType = "mime/foo";
+ _response.WriteBinary(ASCIIEncoding.ASCII.GetBytes(foo), mimeType);
+ _outputStream.Flush();
+ _outputStream.Position = 0;
+ StreamReader reader = new StreamReader(_outputStream);
+ Assert.Equal(foo, reader.ReadToEnd());
+ Assert.Equal(mimeType, _response.ContentType);
+ }
+
+ [Fact]
+ public void OutputCacheSetsExpirationTimeBasedOnCurrentContext()
+ {
+ // Arrange
+ var timestamp = new DateTime(2011, 1, 1, 0, 0, 0);
+ var context = new Mock<HttpContextBase>();
+ context.SetupGet(c => c.Timestamp).Returns(timestamp);
+ var response = new Mock<HttpResponseBase>().Object;
+
+ var cache = new Mock<HttpCachePolicyBase>();
+ cache.Setup(c => c.SetCacheability(It.Is<HttpCacheability>(p => p == HttpCacheability.Public))).Verifiable();
+ cache.Setup(c => c.SetExpires(It.Is<DateTime>(p => p == timestamp.AddSeconds(20)))).Verifiable();
+ cache.Setup(c => c.SetMaxAge(It.Is<TimeSpan>(p => p == TimeSpan.FromSeconds(20)))).Verifiable();
+ cache.Setup(c => c.SetValidUntilExpires(It.Is<bool>(p => p == true))).Verifiable();
+ cache.Setup(c => c.SetLastModified(It.Is<DateTime>(p => p == timestamp))).Verifiable();
+ cache.Setup(c => c.SetSlidingExpiration(It.Is<bool>(p => p == false))).Verifiable();
+
+ // Act
+ ResponseExtensions.OutputCache(context.Object, cache.Object, 20, false, null, null, null, HttpCacheability.Public);
+
+ // Assert
+ cache.VerifyAll();
+ }
+
+ [Fact]
+ public void OutputCacheSetsVaryByValues()
+ {
+ // Arrange
+ var timestamp = new DateTime(2011, 1, 1, 0, 0, 0);
+ var context = new Mock<HttpContextBase>();
+ context.SetupGet(c => c.Timestamp).Returns(timestamp);
+ var response = new Mock<HttpResponseBase>().Object;
+
+ var varyByParams = new HttpCacheVaryByParams();
+ var varyByHeader = new HttpCacheVaryByHeaders();
+ var varyByContentEncoding = new HttpCacheVaryByContentEncodings();
+
+ var cache = new Mock<HttpCachePolicyBase>();
+ cache.SetupGet(c => c.VaryByParams).Returns(varyByParams);
+ cache.SetupGet(c => c.VaryByHeaders).Returns(varyByHeader);
+ cache.SetupGet(c => c.VaryByContentEncodings).Returns(varyByContentEncoding);
+
+ // Act
+ ResponseExtensions.OutputCache(context.Object, cache.Object, 20, false, new[] { "foo" }, new[] { "bar", "bar2" },
+ new[] { "baz", "baz2" }, HttpCacheability.Public);
+
+ // Assert
+ Assert.Equal(varyByParams["foo"], true);
+ Assert.Equal(varyByHeader["bar"], true);
+ Assert.Equal(varyByHeader["bar2"], true);
+ Assert.Equal(varyByContentEncoding["baz"], true);
+ Assert.Equal(varyByContentEncoding["baz2"], true);
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/Extensions/StringExtensionsTest.cs b/test/System.Web.WebPages.Test/Extensions/StringExtensionsTest.cs
new file mode 100644
index 00000000..8fe2edf8
--- /dev/null
+++ b/test/System.Web.WebPages.Test/Extensions/StringExtensionsTest.cs
@@ -0,0 +1,251 @@
+using System.Globalization;
+using System.Web.TestUtil;
+using Xunit;
+using Xunit.Extensions;
+
+namespace System.Web.WebPages.Test
+{
+ public class StringExtensionsTest
+ {
+ [Fact]
+ public void IsIntTests()
+ {
+ Assert.False("1.3".IsInt());
+ Assert.False(".13".IsInt());
+ Assert.False("0.0".IsInt());
+ Assert.False("12345678900123456".IsInt());
+ Assert.False("gooblygook".IsInt());
+ Assert.True("0".IsInt());
+ Assert.True("123456".IsInt());
+ Assert.True(Int32.MaxValue.ToString().IsInt());
+ Assert.True(Int32.MinValue.ToString().IsInt());
+ Assert.False(((string)null).IsInt());
+ }
+
+ [Fact]
+ public void AsIntBasicTests()
+ {
+ Assert.Equal(-123, "-123".AsInt());
+ Assert.Equal(12345, "12345".AsInt());
+ Assert.Equal(0, "0".AsInt());
+ }
+
+ [Fact]
+ public void AsIntDefaultTests()
+ {
+ // Illegal values default to 0
+ Assert.Equal(0, "-100000000000000000000000".AsInt());
+
+ // Illegal values default to 0
+ Assert.Equal(0, "adlfkj".AsInt());
+
+ Assert.Equal(-1, "adlfkj".AsInt(-1));
+ Assert.Equal(-1, "-100000000000000000000000".AsInt(-1));
+ }
+
+ [Fact]
+ public void IsDecimalTests()
+ {
+ Assert.True("1.3".IsDecimal());
+ Assert.True(".13".IsDecimal());
+ Assert.True("0.0".IsDecimal());
+ Assert.True("12345678900123456".IsDecimal());
+ Assert.True("0".IsDecimal());
+ Assert.True("123456".IsDecimal());
+ Assert.True(decimal.MaxValue.ToString().IsDecimal());
+ Assert.True(decimal.MinValue.ToString().IsDecimal());
+ Assert.False("gooblygook".IsDecimal());
+ Assert.False("..0".IsDecimal());
+ Assert.False(((string)null).IsDecimal());
+ }
+
+ [Fact]
+ public void AsDecimalBasicTests()
+ {
+ Assert.Equal(-123m, "-123".AsDecimal());
+ Assert.Equal(9.99m, "9.99".AsDecimal());
+ Assert.Equal(0m, "0".AsDecimal());
+ Assert.Equal(-1.1111m, "-1.1111".AsDecimal());
+ }
+
+ [Fact]
+ public void AsDecimalDefaultTests()
+ {
+ // Illegal values default to 0
+ Assert.Equal(0m, "abc".AsDecimal());
+
+ Assert.Equal(-1.11m, "adlfkj".AsDecimal(-1.11m));
+ }
+
+ [Fact]
+ public void AsDecimalUsesCurrentCulture()
+ {
+ decimal value = 12345.00M;
+ using (new CultureReplacer("ar-DZ"))
+ {
+ Assert.Equal(value.ToString(CultureInfo.CurrentCulture), "12345.00");
+ Assert.Equal(value.ToString(), "12345.00");
+ }
+
+ using (new CultureReplacer("bg-BG"))
+ {
+ Assert.Equal(value.ToString(CultureInfo.CurrentCulture), "12345,00");
+ Assert.Equal(value.ToString(), "12345,00");
+ }
+ }
+
+ [Fact]
+ public void IsAndAsDecimalsUsesCurrentCulture()
+ {
+ // Pretty identical to the earlier test case. This was a post on the forums, making sure it works.
+ using (new CultureReplacer(culture: "lt-LT"))
+ {
+ Assert.False("1.2".IsDecimal());
+ Assert.True("1,2".IsDecimal());
+
+ Assert.Equal(1.2M, "1,2".AsDecimal());
+ Assert.Equal(0, "1.2".AsDecimal());
+ }
+ }
+
+ [Fact]
+ public void IsFloatTests()
+ {
+ Assert.True("1.3".IsFloat());
+ Assert.True(".13".IsFloat());
+ Assert.True("0.0".IsFloat());
+ Assert.True("12345678900123456".IsFloat());
+ Assert.True("0".IsFloat());
+ Assert.True("123456".IsFloat());
+ Assert.True(float.MaxValue.ToString().IsFloat());
+ Assert.True(float.MinValue.ToString().IsFloat());
+ Assert.True(float.NegativeInfinity.ToString().IsFloat());
+ Assert.True(float.PositiveInfinity.ToString().IsFloat());
+ Assert.False("gooblygook".IsFloat());
+ Assert.False(((string)null).IsFloat());
+ }
+
+ [Fact]
+ public void AsFloatBasicTests()
+ {
+ Assert.Equal(-123f, "-123".AsFloat());
+ Assert.Equal(9.99f, "9.99".AsFloat());
+ Assert.Equal(0f, "0".AsFloat());
+ Assert.Equal(-1.1111f, "-1.1111".AsFloat());
+ }
+
+ [Fact]
+ public void AsFloatDefaultTests()
+ {
+ // Illegal values default to 0
+ Assert.Equal(0f, "abc".AsFloat());
+
+ Assert.Equal(-1.11f, "adlfkj".AsFloat(-1.11f));
+ }
+
+ [Fact]
+ public void IsDateTimeTests()
+ {
+ using (new CultureReplacer())
+ {
+ Assert.True("Sat, 01 Nov 2008 19:35:00 GMT".IsDateTime());
+ Assert.True("1/5/1979".IsDateTime());
+ Assert.False("0".IsDateTime());
+ Assert.True(DateTime.MaxValue.ToString().IsDateTime());
+ Assert.True(DateTime.MinValue.ToString().IsDateTime());
+ Assert.True(new DateTime(2010, 12, 21).ToUniversalTime().ToString().IsDateTime());
+ Assert.False("gooblygook".IsDateTime());
+ Assert.False(((string)null).IsDateTime());
+ }
+ }
+
+ /// <remarks>Tests for bug 153439</remarks>
+ [Fact]
+ public void IsDateTimeUsesLocalCulture()
+ {
+ using (new CultureReplacer(culture: "en-gb"))
+ {
+ Assert.True(new DateTime(2010, 12, 21).ToString().IsDateTime());
+ Assert.True(new DateTime(2010, 12, 11).ToString().IsDateTime());
+ Assert.True("2010/01/01".IsDateTime());
+ Assert.True("12/01/2010".IsDateTime());
+ Assert.True("12/12/2010".IsDateTime());
+ Assert.True("13/12/2010".IsDateTime());
+ Assert.True("2010-12-01".IsDateTime());
+ Assert.True("2010-12-13".IsDateTime());
+
+ Assert.False("12/13/2010".IsDateTime());
+ Assert.False("13/13/2010".IsDateTime());
+ Assert.False("2010-13-12".IsDateTime());
+ }
+ }
+
+ [Fact]
+ public void AsDateTimeBasicTests()
+ {
+ using (new CultureReplacer())
+ {
+ Assert.Equal(DateTime.Parse("1/14/1979"), "1/14/1979".AsDateTime());
+ Assert.Equal(DateTime.Parse("Sat, 01 Nov 2008 19:35:00 GMT"), "Sat, 01 Nov 2008 19:35:00 GMT".AsDateTime());
+ }
+ }
+
+ [Theory]
+ [InlineData(new object[] { "en-us" })]
+ [InlineData(new object[] { "en-gb" })]
+ [InlineData(new object[] { "ug" })]
+ [InlineData(new object[] { "lt-LT" })]
+ public void AsDateTimeDefaultTests(string culture)
+ {
+ using (new CultureReplacer(culture))
+ {
+ // Illegal values default to MinTime
+ Assert.Equal(DateTime.MinValue, "1".AsDateTime());
+
+ DateTime defaultV = new DateTime(1979, 01, 05);
+ Assert.Equal(defaultV, "adlfkj".AsDateTime(defaultV));
+ Assert.Equal(defaultV, "Jn 69".AsDateTime(defaultV));
+ }
+ }
+
+ [Theory]
+ [InlineData(new object[] { "en-us" })]
+ [InlineData(new object[] { "en-gb" })]
+ [InlineData(new object[] { "lt-LT" })]
+ public void IsDateTimeDefaultTests(string culture)
+ {
+ using (new CultureReplacer(culture))
+ {
+ var dateTime = new DateTime(2011, 10, 25, 10, 10, 00);
+ Assert.True(dateTime.ToShortDateString().IsDateTime());
+ Assert.True(dateTime.ToString().IsDateTime());
+ Assert.True(dateTime.ToLongDateString().IsDateTime());
+ }
+ }
+
+ [Fact]
+ public void IsBoolTests()
+ {
+ Assert.True("TRUE".IsBool());
+ Assert.True("TRUE ".IsBool());
+ Assert.True("false".IsBool());
+ Assert.False("falsey".IsBool());
+ Assert.False("gooblygook".IsBool());
+ Assert.False("".IsBool());
+ Assert.False(((string)null).IsBool());
+ }
+
+ [Fact]
+ public void AsBoolTests()
+ {
+ Assert.True("TRuE".AsBool());
+ Assert.False("False".AsBool());
+ Assert.False("Die".AsBool(false));
+ Assert.True("true!".AsBool(true));
+ Assert.False("".AsBool());
+ Assert.False(((string)null).AsBool());
+ Assert.True("".AsBool(true));
+ Assert.True(((string)null).AsBool(true));
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/Helpers/AntiForgeryDataSerializerTest.cs b/test/System.Web.WebPages.Test/Helpers/AntiForgeryDataSerializerTest.cs
new file mode 100644
index 00000000..201a7aae
--- /dev/null
+++ b/test/System.Web.WebPages.Test/Helpers/AntiForgeryDataSerializerTest.cs
@@ -0,0 +1,103 @@
+using System.Web.Mvc;
+using System.Web.WebPages.TestUtils;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Helpers.Test
+{
+ public class AntiForgeryDataSerializerTest
+ {
+ [Fact]
+ public void GuardClauses()
+ {
+ // Arrange
+ AntiForgeryDataSerializer serializer = new AntiForgeryDataSerializer();
+
+ // Act & assert
+ Assert.ThrowsArgumentNull(
+ () => serializer.Serialize(null),
+ "token"
+ );
+ Assert.ThrowsArgumentNullOrEmptyString(
+ () => serializer.Deserialize(null),
+ "serializedToken"
+ );
+ Assert.ThrowsArgumentNullOrEmptyString(
+ () => serializer.Deserialize(String.Empty),
+ "serializedToken"
+ );
+ Assert.Throws<HttpAntiForgeryException>(
+ () => serializer.Deserialize("Corrupted Base-64 Value"),
+ "A required anti-forgery token was not supplied or was invalid."
+ );
+ }
+
+ [Fact]
+ public void DeserializationExceptionDoesNotContainInnerException()
+ {
+ // Arrange
+ AntiForgeryDataSerializer serializer = new AntiForgeryDataSerializer();
+
+ // Act & assert
+ HttpAntiForgeryException exception = null;
+ try
+ {
+ serializer.Deserialize("Can't deserialize this.");
+ }
+ catch (HttpAntiForgeryException ex)
+ {
+ exception = ex;
+ }
+
+ Assert.NotNull(exception);
+ Assert.Null(exception.InnerException);
+ }
+
+ [Fact]
+ public void CanRoundTripData()
+ {
+ // Arrange
+ AntiForgeryDataSerializer serializer = new AntiForgeryDataSerializer
+ {
+ Decoder = value => Convert.FromBase64String(value),
+ Encoder = bytes => Convert.ToBase64String(bytes),
+ };
+ AntiForgeryData input = new AntiForgeryData
+ {
+ Salt = "The Salt",
+ Username = "The Username",
+ Value = "The Value",
+ CreationDate = DateTime.Now,
+ };
+
+ // Act
+ AntiForgeryData output = serializer.Deserialize(serializer.Serialize(input));
+
+ // Assert
+ Assert.NotNull(output);
+ Assert.Equal(input.Salt, output.Salt);
+ Assert.Equal(input.Username, output.Username);
+ Assert.Equal(input.Value, output.Value);
+ Assert.Equal(input.CreationDate, output.CreationDate);
+ }
+
+ [Fact]
+ public void HexDigitConvertsIntegersToHexCharsCorrectly()
+ {
+ for (int i = 0; i < 0x10; i++)
+ {
+ Assert.Equal(i.ToString("X")[0], AntiForgeryDataSerializer.HexDigit(i));
+ }
+ }
+
+ [Fact]
+ public void HexValueConvertsCharValuesToIntegersCorrectly()
+ {
+ for (int i = 0; i < 0x10; i++)
+ {
+ var hexChar = i.ToString("X")[0];
+ Assert.Equal(i, AntiForgeryDataSerializer.HexValue(hexChar));
+ }
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/Helpers/AntiForgeryDataTest.cs b/test/System.Web.WebPages.Test/Helpers/AntiForgeryDataTest.cs
new file mode 100644
index 00000000..442b0c9f
--- /dev/null
+++ b/test/System.Web.WebPages.Test/Helpers/AntiForgeryDataTest.cs
@@ -0,0 +1,168 @@
+using System.Security.Principal;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Helpers.Test
+{
+ public class AntiForgeryDataTest
+ {
+ [Fact]
+ public void CopyConstructor()
+ {
+ // Arrange
+ AntiForgeryData originalToken = new AntiForgeryData()
+ {
+ CreationDate = DateTime.Now,
+ Salt = "some salt",
+ Value = "some value"
+ };
+
+ // Act
+ AntiForgeryData newToken = new AntiForgeryData(originalToken);
+
+ // Assert
+ Assert.Equal(originalToken.CreationDate, newToken.CreationDate);
+ Assert.Equal(originalToken.Salt, newToken.Salt);
+ Assert.Equal(originalToken.Value, newToken.Value);
+ }
+
+ [Fact]
+ public void CopyConstructorThrowsIfTokenIsNull()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ delegate { new AntiForgeryData(null); }, "token");
+ }
+
+ [Fact]
+ public void CreationDateProperty()
+ {
+ // Arrange
+ AntiForgeryData token = new AntiForgeryData();
+
+ // Act & Assert
+ var now = DateTime.UtcNow;
+ token.CreationDate = now;
+ Assert.Equal(now, token.CreationDate);
+ }
+
+ [Fact]
+ public void GetAntiForgeryTokenNameReturnsEncodedCookieNameIfAppPathIsNotEmpty()
+ {
+ // Arrange
+ // the string below (as UTF-8 bytes) base64-encodes to "Pz4/Pj8+Pz4/Pj8+Pz4/Pg=="
+ string original = "?>?>?>?>?>?>?>?>";
+
+ // Act
+ string tokenName = AntiForgeryData.GetAntiForgeryTokenName(original);
+
+ // Assert
+ Assert.Equal("__RequestVerificationToken_Pz4-Pj8.Pz4-Pj8.Pz4-Pg__", tokenName);
+ }
+
+ [Fact]
+ public void GetAntiForgeryTokenNameReturnsFieldNameIfAppPathIsNull()
+ {
+ // Act
+ string tokenName = AntiForgeryData.GetAntiForgeryTokenName(null);
+
+ // Assert
+ Assert.Equal("__RequestVerificationToken", tokenName);
+ }
+
+ [Fact]
+ public void GetUsername_ReturnsEmptyStringIfIdentityIsNull()
+ {
+ // Arrange
+ Mock<IPrincipal> mockPrincipal = new Mock<IPrincipal>();
+ mockPrincipal.Setup(o => o.Identity).Returns((IIdentity)null);
+
+ // Act
+ string username = AntiForgeryData.GetUsername(mockPrincipal.Object);
+
+ // Assert
+ Assert.Equal("", username);
+ }
+
+ [Fact]
+ public void GetUsername_ReturnsEmptyStringIfPrincipalIsNull()
+ {
+ // Act
+ string username = AntiForgeryData.GetUsername(null);
+
+ // Assert
+ Assert.Equal("", username);
+ }
+
+ [Fact]
+ public void GetUsername_ReturnsEmptyStringIfUserNotAuthenticated()
+ {
+ // Arrange
+ Mock<IPrincipal> mockPrincipal = new Mock<IPrincipal>();
+ mockPrincipal.Setup(o => o.Identity.IsAuthenticated).Returns(false);
+ mockPrincipal.Setup(o => o.Identity.Name).Returns("SampleName");
+
+ // Act
+ string username = AntiForgeryData.GetUsername(mockPrincipal.Object);
+
+ // Assert
+ Assert.Equal("", username);
+ }
+
+ [Fact]
+ public void GetUsername_ReturnsUsernameIfUserIsAuthenticated()
+ {
+ // Arrange
+ Mock<IPrincipal> mockPrincipal = new Mock<IPrincipal>();
+ mockPrincipal.Setup(o => o.Identity.IsAuthenticated).Returns(true);
+ mockPrincipal.Setup(o => o.Identity.Name).Returns("SampleName");
+
+ // Act
+ string username = AntiForgeryData.GetUsername(mockPrincipal.Object);
+
+ // Assert
+ Assert.Equal("SampleName", username);
+ }
+
+ [Fact]
+ public void NewToken()
+ {
+ // Act
+ AntiForgeryData token = AntiForgeryData.NewToken();
+
+ // Assert
+ int valueLength = Convert.FromBase64String(token.Value).Length;
+ Assert.Equal(16, valueLength);
+ Assert.NotEqual(default(DateTime), token.CreationDate);
+ }
+
+ [Fact]
+ public void SaltProperty()
+ {
+ // Arrange
+ AntiForgeryData token = new AntiForgeryData();
+
+ // Act & Assert
+ Assert.Equal(String.Empty, token.Salt);
+ token.Salt = null;
+ Assert.Equal(String.Empty, token.Salt);
+ token.Salt = String.Empty;
+ Assert.Equal(String.Empty, token.Salt);
+ }
+
+ [Fact]
+ public void ValueProperty()
+ {
+ // Arrange
+ AntiForgeryData token = new AntiForgeryData();
+
+ // Act & Assert
+ Assert.Equal(String.Empty, token.Value);
+ token.Value = null;
+ Assert.Equal(String.Empty, token.Value);
+ token.Value = String.Empty;
+ Assert.Equal(String.Empty, token.Value);
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/Helpers/AntiForgeryTest.cs b/test/System.Web.WebPages.Test/Helpers/AntiForgeryTest.cs
new file mode 100644
index 00000000..db26de50
--- /dev/null
+++ b/test/System.Web.WebPages.Test/Helpers/AntiForgeryTest.cs
@@ -0,0 +1,36 @@
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Helpers.Test
+{
+ public class AntiForgeryTest
+ {
+ private static string _antiForgeryTokenCookieName = AntiForgeryData.GetAntiForgeryTokenName("/SomeAppPath");
+
+ [Fact]
+ public void GetHtml_ThrowsWhenNotCalledInWebContext()
+ {
+ Assert.Throws<ArgumentException>(() => AntiForgery.GetHtml(),
+ "An HttpContext is required to perform this operation. Check that this operation is being performed during a web request.");
+ }
+
+ [Fact]
+ public void GetHtml_ThrowsOnNullContext()
+ {
+ Assert.ThrowsArgumentNull(() => AntiForgery.GetHtml(null, null, null, null), "httpContext");
+ }
+
+ [Fact]
+ public void Validate_ThrowsWhenNotCalledInWebContext()
+ {
+ Assert.Throws<ArgumentException>(() => AntiForgery.Validate(),
+ "An HttpContext is required to perform this operation. Check that this operation is being performed during a web request.");
+ }
+
+ [Fact]
+ public void Validate_ThrowsOnNullContext()
+ {
+ Assert.ThrowsArgumentNull(() => AntiForgery.Validate(httpContext: null, salt: null), "httpContext");
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/Helpers/AntiForgeryWorkerTest.cs b/test/System.Web.WebPages.Test/Helpers/AntiForgeryWorkerTest.cs
new file mode 100644
index 00000000..81cd287d
--- /dev/null
+++ b/test/System.Web.WebPages.Test/Helpers/AntiForgeryWorkerTest.cs
@@ -0,0 +1,237 @@
+using System.Collections.Specialized;
+using System.Globalization;
+using System.Text.RegularExpressions;
+using System.Web.Mvc;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+using Match = System.Text.RegularExpressions.Match;
+
+namespace System.Web.Helpers.Test
+{
+ public class AntiForgeryWorkerTest
+ {
+ private static string _antiForgeryTokenCookieName = AntiForgeryData.GetAntiForgeryTokenName("/SomeAppPath");
+ private const string _serializedValuePrefix = @"<input name=""__RequestVerificationToken"" type=""hidden"" value=""Creation: ";
+ private const string _someValueSuffix = @", Value: some value, Salt: some other salt, Username: username"" />";
+ private readonly Regex _randomFormValueSuffixRegex = new Regex(@", Value: (?<value>[A-Za-z0-9/\+=]{24}), Salt: some other salt, Username: username"" />$");
+ private readonly Regex _randomCookieValueSuffixRegex = new Regex(@", Value: (?<value>[A-Za-z0-9/\+=]{24}), Salt: ");
+
+ [Fact]
+ public void Serializer_DefaultValueIsAntiForgeryDataSerializer()
+ {
+ Assert.Same(typeof(AntiForgeryDataSerializer), new AntiForgeryWorker().Serializer.GetType());
+ }
+
+ [Fact]
+ public void GetHtml_ReturnsFormFieldAndSetsCookieValueIfDoesNotExist()
+ {
+ // Arrange
+ AntiForgeryWorker worker = new AntiForgeryWorker()
+ {
+ Serializer = new DummyAntiForgeryTokenSerializer()
+ };
+ var context = CreateContext();
+
+ // Act
+ string formValue = worker.GetHtml(context, "some other salt", null, null).ToHtmlString();
+
+ // Assert
+ Assert.True(formValue.StartsWith(_serializedValuePrefix), "Form value prefix did not match.");
+
+ Match formMatch = _randomFormValueSuffixRegex.Match(formValue);
+ string formTokenValue = formMatch.Groups["value"].Value;
+
+ HttpCookie cookie = context.Response.Cookies[_antiForgeryTokenCookieName];
+ Assert.NotNull(cookie);
+ Assert.True(cookie.HttpOnly, "Cookie should have HTTP-only flag set.");
+ Assert.True(String.IsNullOrEmpty(cookie.Domain), "Domain should not have been set.");
+ Assert.Equal("/", cookie.Path);
+
+ Match cookieMatch = _randomCookieValueSuffixRegex.Match(cookie.Value);
+ string cookieTokenValue = cookieMatch.Groups["value"].Value;
+
+ Assert.Equal(formTokenValue, cookieTokenValue);
+ }
+
+ [Fact]
+ public void GetHtml_SetsCookieDomainAndPathIfSpecified()
+ {
+ // Arrange
+ AntiForgeryWorker worker = new AntiForgeryWorker()
+ {
+ Serializer = new DummyAntiForgeryTokenSerializer()
+ };
+ var context = CreateContext();
+
+ // Act
+ string formValue = worker.GetHtml(context, "some other salt", "theDomain", "thePath").ToHtmlString();
+
+ // Assert
+ Assert.True(formValue.StartsWith(_serializedValuePrefix), "Form value prefix did not match.");
+
+ Match formMatch = _randomFormValueSuffixRegex.Match(formValue);
+ string formTokenValue = formMatch.Groups["value"].Value;
+
+ HttpCookie cookie = context.Response.Cookies[_antiForgeryTokenCookieName];
+ Assert.NotNull(cookie);
+ Assert.True(cookie.HttpOnly, "Cookie should have HTTP-only flag set.");
+ Assert.Equal("theDomain", cookie.Domain);
+ Assert.Equal("thePath", cookie.Path);
+
+ Match cookieMatch = _randomCookieValueSuffixRegex.Match(cookie.Value);
+ string cookieTokenValue = cookieMatch.Groups["value"].Value;
+
+ Assert.Equal(formTokenValue, cookieTokenValue);
+ }
+
+ [Fact]
+ public void GetHtml_ReusesCookieValueIfExistsAndIsValid()
+ {
+ // Arrange
+ AntiForgeryWorker worker = new AntiForgeryWorker()
+ {
+ Serializer = new DummyAntiForgeryTokenSerializer()
+ };
+ var context = CreateContext("2001-01-01:some value:some salt:username");
+
+ // Act
+ string formValue = worker.GetHtml(context, "some other salt", null, null).ToHtmlString();
+
+ // Assert
+ Assert.True(formValue.StartsWith(_serializedValuePrefix), "Form value prefix did not match.");
+ Assert.True(formValue.EndsWith(_someValueSuffix), "Form value suffix did not match.");
+ Assert.Equal(0, context.Response.Cookies.Count);
+ }
+
+ [Fact]
+ public void GetHtml_CreatesNewCookieValueIfCookieExistsButIsNotValid()
+ {
+ // Arrange
+ AntiForgeryWorker worker = new AntiForgeryWorker()
+ {
+ Serializer = new DummyAntiForgeryTokenSerializer()
+ };
+ var context = CreateContext("invalid");
+
+ // Act
+ string formValue = worker.GetHtml(context, "some other salt", null, null).ToHtmlString();
+
+ // Assert
+ Assert.True(formValue.StartsWith(_serializedValuePrefix), "Form value prefix did not match.");
+
+ Match formMatch = _randomFormValueSuffixRegex.Match(formValue);
+ string formTokenValue = formMatch.Groups["value"].Value;
+
+ HttpCookie cookie = context.Response.Cookies[_antiForgeryTokenCookieName];
+ Assert.NotNull(cookie);
+ Assert.True(cookie.HttpOnly, "Cookie should have HTTP-only flag set.");
+ Assert.True(String.IsNullOrEmpty(cookie.Domain), "Domain should not have been set.");
+ Assert.Equal("/", cookie.Path);
+
+ Match cookieMatch = _randomCookieValueSuffixRegex.Match(cookie.Value);
+ string cookieTokenValue = cookieMatch.Groups["value"].Value;
+
+ Assert.Equal(formTokenValue, cookieTokenValue);
+ }
+
+ [Fact]
+ public void Validate_ThrowsIfCookieMissing()
+ {
+ Validate_Helper(null, "2001-01-01:some other value:the real salt:username");
+ }
+
+ [Fact]
+ public void Validate_ThrowsIfCookieValueDoesNotMatchFormValue()
+ {
+ Validate_Helper("2001-01-01:some value:the real salt:username", "2001-01-01:some other value:the real salt:username");
+ }
+
+ [Fact]
+ public void Validate_ThrowsIfFormSaltDoesNotMatchAttributeSalt()
+ {
+ Validate_Helper("2001-01-01:some value:some salt:username", "2001-01-01:some value:some other salt:username");
+ }
+
+ [Fact]
+ public void Validate_ThrowsIfFormValueMissing()
+ {
+ Validate_Helper("2001-01-01:some value:the real salt:username", null);
+ }
+
+ [Fact]
+ public void Validate_ThrowsIfUsernameInFormIsIncorrect()
+ {
+ Validate_Helper("2001-01-01:value:salt:username", "2001-01-01:value:salt:different username");
+ }
+
+ private static void Validate_Helper(string cookieValue, string formValue, string username = "username")
+ {
+ // Arrange
+ //ValidateAntiForgeryTokenAttribute attribute = GetAttribute();
+ var context = CreateContext(cookieValue, formValue, username);
+
+ AntiForgeryWorker worker = new AntiForgeryWorker()
+ {
+ Serializer = new DummyAntiForgeryTokenSerializer()
+ };
+
+ // Act & Assert
+ Assert.Throws<HttpAntiForgeryException>(
+ delegate
+ {
+ //attribute.OnAuthorization(authContext);
+ worker.Validate(context, "the real salt");
+ }, "A required anti-forgery token was not supplied or was invalid.");
+ }
+
+ private static HttpContextBase CreateContext(string cookieValue = null, string formValue = null, string username = "username")
+ {
+ HttpCookieCollection requestCookies = new HttpCookieCollection();
+ if (!String.IsNullOrEmpty(cookieValue))
+ {
+ requestCookies.Set(new HttpCookie(_antiForgeryTokenCookieName, cookieValue));
+ }
+ NameValueCollection formCollection = new NameValueCollection();
+ if (!String.IsNullOrEmpty(formValue))
+ {
+ formCollection.Set(AntiForgeryData.GetAntiForgeryTokenName(null), formValue);
+ }
+
+ Mock<HttpContextBase> mockContext = new Mock<HttpContextBase>();
+ mockContext.Setup(c => c.Request.ApplicationPath).Returns("/SomeAppPath");
+ mockContext.Setup(c => c.Request.Cookies).Returns(requestCookies);
+ mockContext.Setup(c => c.Request.Form).Returns(formCollection);
+ mockContext.Setup(c => c.Response.Cookies).Returns(new HttpCookieCollection());
+ mockContext.Setup(c => c.User.Identity.IsAuthenticated).Returns(true);
+ mockContext.Setup(c => c.User.Identity.Name).Returns(username);
+
+ return mockContext.Object;
+ }
+
+ internal class DummyAntiForgeryTokenSerializer : AntiForgeryDataSerializer
+ {
+ public override string Serialize(AntiForgeryData token)
+ {
+ return String.Format(CultureInfo.InvariantCulture, "Creation: {0}, Value: {1}, Salt: {2}, Username: {3}",
+ token.CreationDate, token.Value, token.Salt, token.Username);
+ }
+
+ public override AntiForgeryData Deserialize(string serializedToken)
+ {
+ if (serializedToken == "invalid")
+ {
+ throw new HttpAntiForgeryException();
+ }
+ string[] parts = serializedToken.Split(':');
+ return new AntiForgeryData()
+ {
+ CreationDate = DateTime.Parse(parts[0], CultureInfo.InvariantCulture),
+ Value = parts[1],
+ Salt = parts[2],
+ Username = parts[3]
+ };
+ }
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/Helpers/UnvalidatedRequestValuesTest.cs b/test/System.Web.WebPages.Test/Helpers/UnvalidatedRequestValuesTest.cs
new file mode 100644
index 00000000..2be42784
--- /dev/null
+++ b/test/System.Web.WebPages.Test/Helpers/UnvalidatedRequestValuesTest.cs
@@ -0,0 +1,78 @@
+using System.Collections.Specialized;
+using System.Web;
+using System.Web.Helpers;
+using Moq;
+using Xunit;
+
+namespace Microsoft.WebPages.Test.Helpers
+{
+ public class UnvalidatedRequestValuesTest
+ {
+ [Fact]
+ public void Constructor_SetsPropertiesCorrectly()
+ {
+ // Arrange
+ NameValueCollection expectedForm = new NameValueCollection();
+ NameValueCollection expectedQueryString = new NameValueCollection();
+
+ // Act
+ UnvalidatedRequestValues unvalidatedValues = new UnvalidatedRequestValues(null, () => expectedForm, () => expectedQueryString);
+
+ // Assert
+ Assert.Same(expectedForm, unvalidatedValues.Form);
+ Assert.Same(expectedQueryString, unvalidatedValues.QueryString);
+ }
+
+ [Fact]
+ public void Indexer_LooksUpValuesInCorrectOrder()
+ {
+ // Order should be QueryString, Form, Cookies, ServerVariables
+
+ // Arrange
+ NameValueCollection queryString = new NameValueCollection()
+ {
+ { "foo", "fooQueryString" }
+ };
+
+ NameValueCollection form = new NameValueCollection()
+ {
+ { "foo", "fooForm" },
+ { "bar", "barForm" },
+ };
+
+ HttpCookieCollection cookies = new HttpCookieCollection()
+ {
+ new HttpCookie("foo", "fooCookie"),
+ new HttpCookie("bar", "barCookie"),
+ new HttpCookie("baz", "bazCookie")
+ };
+
+ NameValueCollection serverVars = new NameValueCollection()
+ {
+ { "foo", "fooServerVars" },
+ { "bar", "barServerVars" },
+ { "baz", "bazServerVars" },
+ { "quux", "quuxServerVars" },
+ };
+ Mock<HttpRequestBase> mockRequest = new Mock<HttpRequestBase>();
+ mockRequest.Setup(o => o.Cookies).Returns(cookies);
+ mockRequest.Setup(o => o.ServerVariables).Returns(serverVars);
+
+ UnvalidatedRequestValues unvalidatedValues = new UnvalidatedRequestValues(mockRequest.Object, () => form, () => queryString);
+
+ // Act
+ string fooValue = unvalidatedValues["foo"];
+ string barValue = unvalidatedValues["bar"];
+ string bazValue = unvalidatedValues["baz"];
+ string quuxValue = unvalidatedValues["quux"];
+ string notFoundValue = unvalidatedValues["not-found"];
+
+ // Assert
+ Assert.Equal("fooQueryString", fooValue);
+ Assert.Equal("barForm", barValue);
+ Assert.Equal("bazCookie", bazValue);
+ Assert.Equal("quuxServerVars", quuxValue);
+ Assert.Null(notFoundValue);
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/Html/CheckBoxTest.cs b/test/System.Web.WebPages.Test/Html/CheckBoxTest.cs
new file mode 100644
index 00000000..bf31d2b4
--- /dev/null
+++ b/test/System.Web.WebPages.Test/Html/CheckBoxTest.cs
@@ -0,0 +1,272 @@
+using System.Collections.Generic;
+using System.Web.WebPages.Html;
+using System.Web.WebPages.TestUtils;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.WebPages.Test
+{
+ public class CheckBoxTest
+ {
+ [Fact]
+ public void CheckboxWithEmptyNameThrows()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act and assert
+ Assert.ThrowsArgumentNullOrEmptyString(() => helper.CheckBox(null), "name");
+ Assert.ThrowsArgumentNullOrEmptyString(() => helper.CheckBox(String.Empty), "name");
+ }
+
+ [Fact]
+ public void CheckboxWithDefaultArguments()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.CheckBox("foo");
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""checkbox"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void CheckboxWithObjectAttributes()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.CheckBox("foo", new { attr = "attr-value" });
+
+ // Assert
+ Assert.Equal(@"<input attr=""attr-value"" id=""foo"" name=""foo"" type=""checkbox"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void CheckboxWithDictionaryAttributes()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.CheckBox("foo", new Dictionary<string, object> { { "attr", "attr-value" } });
+
+ // Assert
+ Assert.Equal(@"<input attr=""attr-value"" id=""foo"" name=""foo"" type=""checkbox"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void CheckboxWithExplicitChecked()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.CheckBox("foo", true);
+
+ // Assert
+ Assert.Equal(@"<input checked=""checked"" id=""foo"" name=""foo"" type=""checkbox"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void CheckboxWithModelValue()
+ {
+ // Arrange
+ var modelState = new ModelStateDictionary();
+ modelState.SetModelValue("foo", true);
+ HtmlHelper helper = HtmlHelperFactory.Create(modelState);
+
+ // Act
+ var html = helper.CheckBox("foo");
+
+ // Assert
+ Assert.Equal(@"<input checked=""checked"" id=""foo"" name=""foo"" type=""checkbox"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void CheckboxWithNonBooleanModelValue()
+ {
+ // Arrange
+ var modelState = new ModelStateDictionary();
+ modelState.SetModelValue("foo", Boolean.TrueString);
+ HtmlHelper helper = HtmlHelperFactory.Create(modelState);
+
+ // Act
+ var html = helper.CheckBox("foo");
+
+ // Assert
+ Assert.Equal(@"<input checked=""checked"" id=""foo"" name=""foo"" type=""checkbox"" />",
+ html.ToHtmlString());
+
+ modelState.SetModelValue("foo", new object());
+ helper = HtmlHelperFactory.Create(modelState);
+
+ // Act and Assert
+ Assert.Throws<InvalidOperationException>(() => helper.CheckBox("foo"),
+ "The parameter conversion from type \"System.Object\" to type \"System.Boolean\" failed because no " +
+ "type converter can convert between these types.");
+ }
+
+ [Fact]
+ public void CheckboxWithModelAndExplictValue()
+ {
+ // Arrange
+ var modelState = new ModelStateDictionary();
+ modelState.SetModelValue("foo", false);
+ HtmlHelper helper = HtmlHelperFactory.Create(modelState);
+
+ // Act
+ var html = helper.CheckBox("foo", true);
+
+ // Assert
+ Assert.Equal(@"<input checked=""checked"" id=""foo"" name=""foo"" type=""checkbox"" />",
+ html.ToHtmlString());
+
+ modelState.SetModelValue("foo", true);
+
+ // Act
+ html = helper.CheckBox("foo", false);
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""checkbox"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void CheckBoxWithCheckedHtmlAttribute()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.CheckBox("foo", new { @checked = "checked" });
+
+ // Assert
+ Assert.Equal(@"<input checked=""checked"" id=""foo"" name=""foo"" type=""checkbox"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void CheckBoxWithExplicitCheckedOverwritesHtmlAttribute()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.CheckBox("foo", false, new { @checked = "checked" });
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""checkbox"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void CheckBoxWithModelStateCheckedOverwritesHtmlAttribute()
+ {
+ // Arrange
+ var modelState = new ModelStateDictionary();
+ modelState.SetModelValue("foo", false);
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.CheckBox("foo", false, new { @checked = "checked" });
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""checkbox"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void CheckBoxWithError()
+ {
+ // Arrange
+ var modelState = new ModelStateDictionary();
+ modelState.SetModelValue("foo", false);
+ HtmlHelper helper = HtmlHelperFactory.Create(modelState);
+
+ // Act
+ var html = helper.CheckBox("foo", true);
+
+ // Assert
+ Assert.Equal(@"<input checked=""checked"" id=""foo"" name=""foo"" type=""checkbox"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void CheckBoxWithErrorAndCustomCss()
+ {
+ // Arrange
+ var modelState = new ModelStateDictionary();
+ modelState.AddError("foo", "error");
+ HtmlHelper helper = HtmlHelperFactory.Create(modelState);
+
+ // Act
+ var html = helper.CheckBox("foo", true, new { @class = "my-class" });
+
+ // Assert
+ Assert.Equal(@"<input checked=""checked"" class=""input-validation-error my-class"" id=""foo"" name=""foo"" type=""checkbox"" />",
+ html.ToHtmlString());
+ }
+
+ //[Fact]
+ // Can't test as it sets a static property
+ // Review: Need to redo test once we fix set once property
+ public void CheckBoxUsesCustomErrorClass()
+ {
+ // Arrange
+ var modelState = new ModelStateDictionary();
+ modelState.AddError("foo", "error");
+ HtmlHelper.ValidationInputCssClassName = "my-error-class";
+ HtmlHelper helper = HtmlHelperFactory.Create(modelState);
+
+ // Act
+ var html = helper.CheckBox("foo", true, new { @class = "my-class" });
+
+ // Assert
+ Assert.Equal(@"<input checked=""checked"" class=""my-error-class my-class"" id=""foo"" name=""foo"" type=""checkbox"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void CheckBoxOverwritesImplicitAttributes()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.CheckBox("foo", true, new { type = "fooType", name = "bar" });
+
+ // Assert
+ Assert.Equal(@"<input checked=""checked"" id=""foo"" name=""foo"" type=""fooType"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void CheckboxAddsUnobtrusiveValidationAttributes()
+ {
+ // Arrange
+ const string fieldName = "name";
+ var modelStateDictionary = new ModelStateDictionary();
+ var validationHelper = new ValidationHelper(new Mock<HttpContextBase>().Object, modelStateDictionary);
+ HtmlHelper helper = HtmlHelperFactory.Create(modelStateDictionary, validationHelper);
+
+ // Act
+ validationHelper.RequireField(fieldName, "Please specify a valid Name.");
+ validationHelper.Add(fieldName, Validator.StringLength(30, errorMessage: "Name cannot exceed {0} characters"));
+ var html = helper.CheckBox(fieldName, new Dictionary<string, object> { { "data-some-val", "5" } });
+
+ // Assert
+ Assert.Equal(@"<input data-some-val=""5"" data-val=""true"" data-val-length=""Name cannot exceed 30 characters"" data-val-length-max=""30"" data-val-required=""Please specify a valid Name."" id=""name"" name=""name"" type=""checkbox"" />",
+ html.ToString());
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/Html/HtmlHelperFactory.cs b/test/System.Web.WebPages.Test/Html/HtmlHelperFactory.cs
new file mode 100644
index 00000000..2afef6f2
--- /dev/null
+++ b/test/System.Web.WebPages.Test/Html/HtmlHelperFactory.cs
@@ -0,0 +1,16 @@
+using System.Web.WebPages.Html;
+using Moq;
+
+namespace System.Web.WebPages.Test
+{
+ public static class HtmlHelperFactory
+ {
+ internal static HtmlHelper Create(ModelStateDictionary modelStateDictionary = null, ValidationHelper validationHelper = null)
+ {
+ modelStateDictionary = modelStateDictionary ?? new ModelStateDictionary();
+ var httpContext = new Mock<HttpContextBase>();
+ validationHelper = validationHelper ?? new ValidationHelper(httpContext.Object, modelStateDictionary);
+ return new HtmlHelper(modelStateDictionary, validationHelper);
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/Html/HtmlHelperTest.cs b/test/System.Web.WebPages.Test/Html/HtmlHelperTest.cs
new file mode 100644
index 00000000..a466ad93
--- /dev/null
+++ b/test/System.Web.WebPages.Test/Html/HtmlHelperTest.cs
@@ -0,0 +1,159 @@
+using System.Web.WebPages.Html;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.WebPages.Test
+{
+ public class HtmlHelperTest
+ {
+ [Fact]
+ public void ValidationInputCssClassNameThrowsWhenAssignedNull()
+ {
+ // Act and Assert
+ Assert.ThrowsArgumentNull(() => HtmlHelper.ValidationInputCssClassName = null, "value");
+ }
+
+ [Fact]
+ public void ValidationSummaryClassNameThrowsWhenAssignedNull()
+ {
+ // Act and Assert
+ Assert.ThrowsArgumentNull(() => HtmlHelper.ValidationSummaryClass = null, "value");
+ }
+
+ [Fact]
+ public void EncodeObject()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = HtmlHelperFactory.Create();
+ object text = "<br />" as object;
+
+ // Act
+ string encodedHtml = htmlHelper.Encode(text);
+
+ // Assert
+ Assert.Equal(encodedHtml, "&lt;br /&gt;");
+ }
+
+ [Fact]
+ public void EncodeObjectNull()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = HtmlHelperFactory.Create();
+ object text = null;
+
+ // Act
+ string encodedHtml = htmlHelper.Encode(text);
+
+ // Assert
+ Assert.Equal(String.Empty, encodedHtml);
+ }
+
+ [Fact]
+ public void EncodeString()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = HtmlHelperFactory.Create();
+ var text = "<br />";
+
+ // Act
+ string encodedHtml = htmlHelper.Encode(text);
+
+ // Assert
+ Assert.Equal(encodedHtml, "&lt;br /&gt;");
+ }
+
+ [Fact]
+ public void EncodeStringNull()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = HtmlHelperFactory.Create();
+ string text = null;
+
+ // Act
+ string encodedHtml = htmlHelper.Encode(text);
+
+ // Assert
+ Assert.Equal("", encodedHtml);
+ }
+
+ [Fact]
+ public void RawAllowsNullValue()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = HtmlHelperFactory.Create();
+
+ // Act
+ IHtmlString markupHtml = htmlHelper.Raw(null);
+
+ // Assert
+ Assert.Equal(null, markupHtml.ToString());
+ Assert.Equal(null, markupHtml.ToHtmlString());
+ }
+
+ [Fact]
+ public void RawAllowsNullObjectValue()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = HtmlHelperFactory.Create();
+
+ // Act
+ IHtmlString markupHtml = htmlHelper.Raw((object)null);
+
+ // Assert
+ Assert.Equal(null, markupHtml.ToString());
+ Assert.Equal(null, markupHtml.ToHtmlString());
+ }
+
+ [Fact]
+ public void RawAllowsEmptyValue()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = HtmlHelperFactory.Create();
+
+ // Act
+ IHtmlString markupHtml = htmlHelper.Raw("");
+
+ // Assert
+ Assert.Equal("", markupHtml.ToString());
+ Assert.Equal("", markupHtml.ToHtmlString());
+ }
+
+ [Fact]
+ public void RawReturnsWrapperMarkup()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = HtmlHelperFactory.Create();
+ string markup = "<b>bold</b>";
+
+ // Act
+ IHtmlString markupHtml = htmlHelper.Raw(markup);
+
+ // Assert
+ Assert.Equal("<b>bold</b>", markupHtml.ToString());
+ Assert.Equal("<b>bold</b>", markupHtml.ToHtmlString());
+ }
+
+ [Fact]
+ public void RawReturnsWrapperMarkupOfObject()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = HtmlHelperFactory.Create();
+ ObjectWithWrapperMarkup obj = new ObjectWithWrapperMarkup();
+
+ // Act
+ IHtmlString markupHtml = htmlHelper.Raw(obj);
+
+ // Assert
+ Assert.Equal("<b>boldFromObject</b>", markupHtml.ToString());
+ Assert.Equal("<b>boldFromObject</b>", markupHtml.ToHtmlString());
+ }
+
+ private class ObjectWithWrapperMarkup
+ {
+ public override string ToString()
+ {
+ return "<b>boldFromObject</b>";
+ }
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/Html/InputHelperTest.cs b/test/System.Web.WebPages.Test/Html/InputHelperTest.cs
new file mode 100644
index 00000000..15c62472
--- /dev/null
+++ b/test/System.Web.WebPages.Test/Html/InputHelperTest.cs
@@ -0,0 +1,584 @@
+using System.Collections.Generic;
+using System.Data.Linq;
+using System.Web.WebPages.Html;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.WebPages.Test
+{
+ public class InputHelperTest
+ {
+ private static readonly IDictionary<string, object> _attributesDictionary = new Dictionary<string, object> { { "baz", "BazValue" } };
+ private static readonly object _attributesObject = new { baz = "BazValue" };
+
+ [Fact]
+ public void HiddenWithBinaryArrayValueRendersBase64EncodedValue()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var result = helper.Hidden("ProductName", new Binary(new byte[] { 23, 43, 53 }));
+
+ // Assert
+ Assert.Equal("<input id=\"ProductName\" name=\"ProductName\" type=\"hidden\" value=\"Fys1\" />", result.ToHtmlString());
+ }
+
+ [Fact]
+ public void HiddenWithEmptyNameThrows()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmptyString(() => helper.Hidden(String.Empty), "name");
+ Assert.ThrowsArgumentNullOrEmptyString(() => helper.Hidden(null), "name");
+ }
+
+ [Fact]
+ public void HiddenWithExplicitValue()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.Hidden("foo", "DefaultFoo");
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""hidden"" value=""DefaultFoo"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void HiddenWithExplicitValueAndAttributesDictionary()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.Hidden("foo", "DefaultFoo", new Dictionary<string, object> { { "attr", "attr-val" } });
+
+ // Assert
+ Assert.Equal(@"<input attr=""attr-val"" id=""foo"" name=""foo"" type=""hidden"" value=""DefaultFoo"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void HiddenWithExplicitValueAndObjectDictionary()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.Hidden("foo", "DefaultFoo", new { attr = "attr-val" });
+
+ // Assert
+ Assert.Equal(@"<input attr=""attr-val"" id=""foo"" name=""foo"" type=""hidden"" value=""DefaultFoo"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void HiddenWithExplicitValueNull()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.Hidden("foo", value: null);
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""hidden"" value="""" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void HiddenWithModelValue()
+ {
+ // Arrange
+ var model = new ModelStateDictionary();
+ model.SetModelValue("foo", "bar");
+ HtmlHelper helper = HtmlHelperFactory.Create(model);
+
+ // Act
+ var html = helper.Hidden("foo");
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""hidden"" value=""bar"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void HiddenWithModelValueAndAttributesDictionary()
+ {
+ // Arrange
+ var model = new ModelStateDictionary();
+ model.SetModelValue("foo", "bar");
+ HtmlHelper helper = HtmlHelperFactory.Create(model);
+
+ // Act
+ var html = helper.Hidden("foo", null, new Dictionary<string, object> { { "attr", "attr-val" } });
+
+ // Assert
+ Assert.Equal(@"<input attr=""attr-val"" id=""foo"" name=""foo"" type=""hidden"" value=""bar"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void HiddenWithImplicitValueAndAttributesObject()
+ {
+ // Arrange
+ var model = new ModelStateDictionary();
+ model.SetModelValue("foo", "bar");
+ HtmlHelper helper = HtmlHelperFactory.Create(model);
+
+ // Act
+ var html = helper.Hidden("foo", null, new { attr = "attr-val" });
+
+ // Assert
+ Assert.Equal(@"<input attr=""attr-val"" id=""foo"" name=""foo"" type=""hidden"" value=""bar"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void HiddenWithNameAndValue()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.Hidden("foo", "fooValue");
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""hidden"" value=""fooValue"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void HiddenWithExplicitOverwritesAttributeValue()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.Hidden("foo", "fooValue", new { value = "barValue" });
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""hidden"" value=""fooValue"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void HiddenWithModelValueOverwritesAttributeValue()
+ {
+ // Arrange
+ var model = new ModelStateDictionary();
+ model.SetModelValue("foo", "fooValue");
+ HtmlHelper helper = HtmlHelperFactory.Create(model);
+
+ // Act
+ var html = helper.Hidden("foo", null, new { value = "barValue" });
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""hidden"" value=""fooValue"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void HiddenAddsUnobtrusiveValidationAttributes()
+ {
+ // Arrange
+ const string fieldName = "name";
+ var modelStateDictionary = new ModelStateDictionary();
+ var validationHelper = new ValidationHelper(new Mock<HttpContextBase>().Object, modelStateDictionary);
+ HtmlHelper helper = HtmlHelperFactory.Create(modelStateDictionary, validationHelper);
+
+ // Act
+ validationHelper.RequireField(fieldName, "Please specify a valid Name.");
+ validationHelper.Add(fieldName, Validator.StringLength(30, errorMessage: "Name cannot exceed {0} characters"));
+ var html = helper.Hidden(fieldName, value: null, htmlAttributes: new Dictionary<string, object> { { "data-some-val", "5" } });
+
+ // Assert
+ Assert.Equal(@"<input data-some-val=""5"" data-val=""true"" data-val-length=""Name cannot exceed 30 characters"" data-val-length-max=""30"" data-val-required=""Please specify a valid Name."" id=""name"" name=""name"" type=""hidden"" value="""" />",
+ html.ToString());
+ }
+
+ // Password
+
+ [Fact]
+ public void PasswordWithEmptyNameThrows()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmptyString(() => helper.Password(String.Empty), "name");
+ Assert.ThrowsArgumentNullOrEmptyString(() => helper.Password(null), "name");
+ }
+
+ [Fact]
+ public void PasswordDictionaryOverridesImplicitParameters()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.Password("foo", "Some Value", new { type = "fooType" });
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""fooType"" value=""Some Value"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void PasswordExplicitParametersOverrideDictionary()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.Password("foo", "Some Value", new { value = "Another Value", name = "bar" });
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""password"" value=""Some Value"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void PasswordWithExplicitValue()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.Password("foo", "DefaultFoo", (object)null);
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""password"" value=""DefaultFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void PasswordWithExplicitValueAndAttributesDictionary()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.Password("foo", "DefaultFoo", new { baz = "BazValue" });
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazValue"" id=""foo"" name=""foo"" type=""password"" value=""DefaultFoo"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void PasswordWithExplicitValueAndAttributesObject()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.Password("foo", "DefaultFoo", new Dictionary<string, object> { { "baz", "BazValue" } });
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazValue"" id=""foo"" name=""foo"" type=""password"" value=""DefaultFoo"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void PasswordWithExplicitValueNull()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.Password("foo", value: (string)null);
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""password"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void PasswordWithImplicitValue()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.Password("foo");
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""password"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void PasswordWithImplicitValueAndAttributesDictionary()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.Password("foo", null, _attributesDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazValue"" id=""foo"" name=""foo"" type=""password"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void PasswordWithImplicitValueAndAttributesDictionaryReturnsEmptyValueIfNotFound()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.Password("keyNotFound", null, _attributesDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazValue"" id=""keyNotFound"" name=""keyNotFound"" type=""password"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void PasswordWithImplicitValueAndAttributesObject()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.Password("foo", null, _attributesObject);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazValue"" id=""foo"" name=""foo"" type=""password"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void PasswordWithNameAndValue()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.Password("foo", "fooValue");
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""password"" value=""fooValue"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void PasswordWithNullNameThrows()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmptyString(() => helper.Password(null), "name");
+ Assert.ThrowsArgumentNullOrEmptyString(() => helper.Password(String.Empty), "name");
+ }
+
+ [Fact]
+ public void PasswordAddsUnobtrusiveValidationAttributes()
+ {
+ // Arrange
+ const string fieldName = "name";
+ var modelStateDictionary = new ModelStateDictionary();
+ var validationHelper = new ValidationHelper(new Mock<HttpContextBase>().Object, modelStateDictionary);
+ HtmlHelper helper = HtmlHelperFactory.Create(modelStateDictionary, validationHelper);
+
+ // Act
+ validationHelper.RequireField(fieldName, "Please specify a valid Name.");
+ validationHelper.Add(fieldName, Validator.StringLength(30, errorMessage: "Name cannot exceed {0} characters"));
+ var html = helper.Password(fieldName, value: null, htmlAttributes: new Dictionary<string, object> { { "data-some-val", "5" } });
+
+ // Assert
+ Assert.Equal(@"<input data-some-val=""5"" data-val=""true"" data-val-length=""Name cannot exceed 30 characters"" data-val-length-max=""30"" data-val-required=""Please specify a valid Name."" id=""name"" name=""name"" type=""password"" />",
+ html.ToString());
+ }
+
+ //Input
+ [Fact]
+ public void TextBoxDictionaryOverridesImplicitValues()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.TextBox("foo", "DefaultFoo", new { type = "fooType" });
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""fooType"" value=""DefaultFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextBoxExplicitParametersOverrideDictionaryValues()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.TextBox("foo", "DefaultFoo", new { value = "Some other value" });
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""text"" value=""DefaultFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextBoxWithDotReplacementForId()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.TextBox("foo.bar.baz", null);
+
+ // Assert
+ Assert.Equal(@"<input id=""foo_bar_baz"" name=""foo.bar.baz"" type=""text"" value="""" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextBoxWithEmptyNameThrows()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmptyString(() => helper.TextBox(null), "name");
+ Assert.ThrowsArgumentNullOrEmptyString(() => helper.TextBox(String.Empty), "name");
+ }
+
+ [Fact]
+ public void TextBoxWithExplicitValue()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.TextBox("foo", "DefaultFoo", (object)null);
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""text"" value=""DefaultFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextBoxWithExplicitValueAndAttributesDictionary()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.TextBox("foo", "DefaultFoo", _attributesDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazValue"" id=""foo"" name=""foo"" type=""text"" value=""DefaultFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextBoxWithExplicitValueAndAttributesObject()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.TextBox("foo", "DefaultFoo", _attributesObject);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazValue"" id=""foo"" name=""foo"" type=""text"" value=""DefaultFoo"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextBoxWithExplicitValueNull()
+ {
+ // Arrange
+ var modelState = new ModelStateDictionary();
+ modelState.SetModelValue("foo", "fooModelValue");
+ HtmlHelper helper = HtmlHelperFactory.Create(modelState);
+
+ // Act
+ var html = helper.TextBox("foo", (string)null /* value */, (object)null);
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""text"" value=""fooModelValue"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextBoxWithImplicitValue()
+ {
+ // Arrange
+ var modelState = new ModelStateDictionary();
+ modelState.SetModelValue("foo", "fooModelValue");
+ HtmlHelper helper = HtmlHelperFactory.Create(modelState);
+
+ // Act
+ var html = helper.TextBox("foo");
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""text"" value=""fooModelValue"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextBoxWithImplicitValueAndAttributesDictionary()
+ {
+ // Arrange
+ var modelState = new ModelStateDictionary();
+ modelState.SetModelValue("foo", "fooModelValue");
+ HtmlHelper helper = HtmlHelperFactory.Create(modelState);
+
+ // Act
+ var html = helper.TextBox("foo", null, _attributesDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazValue"" id=""foo"" name=""foo"" type=""text"" value=""fooModelValue"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextBoxWithImplicitValueAndAttributesDictionaryReturnsEmptyValueIfNotFound()
+ {
+ // Arrange
+ var modelState = new ModelStateDictionary();
+ modelState.SetModelValue("foo", "fooModelValue");
+ HtmlHelper helper = HtmlHelperFactory.Create(modelState);
+
+ // Act
+ var html = helper.TextBox("keyNotFound", null, _attributesDictionary);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazValue"" id=""keyNotFound"" name=""keyNotFound"" type=""text"" value="""" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextBoxWithImplicitValueAndAttributesObject()
+ {
+ // Arrange
+ var modelState = new ModelStateDictionary();
+ modelState.SetModelValue("foo", "fooModelValue");
+ HtmlHelper helper = HtmlHelperFactory.Create(modelState);
+
+ // Act
+ var html = helper.TextBox("foo", null, _attributesObject);
+
+ // Assert
+ Assert.Equal(@"<input baz=""BazValue"" id=""foo"" name=""foo"" type=""text"" value=""fooModelValue"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextBoxWithNameAndValue()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.TextBox("foo", "fooValue");
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""text"" value=""fooValue"" />", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextBoxAddsUnobtrusiveValidationAttributes()
+ {
+ // Arrange
+ const string fieldName = "name";
+ var modelStateDictionary = new ModelStateDictionary();
+ var validationHelper = new ValidationHelper(new Mock<HttpContextBase>().Object, modelStateDictionary);
+ HtmlHelper helper = HtmlHelperFactory.Create(modelStateDictionary, validationHelper);
+
+ // Act
+ validationHelper.RequireField(fieldName, "Please specify a valid Name.");
+ validationHelper.Add(fieldName, Validator.StringLength(30, errorMessage: "Name cannot exceed {0} characters"));
+ var html = helper.TextBox(fieldName, value: null, htmlAttributes: new Dictionary<string, object> { { "data-some-val", "5" } });
+
+ // Assert
+ Assert.Equal(@"<input data-some-val=""5"" data-val=""true"" data-val-length=""Name cannot exceed 30 characters"" data-val-length-max=""30"" data-val-required=""Please specify a valid Name."" id=""name"" name=""name"" type=""text"" value="""" />",
+ html.ToString());
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/Html/RadioButtonTest.cs b/test/System.Web.WebPages.Test/Html/RadioButtonTest.cs
new file mode 100644
index 00000000..afab382e
--- /dev/null
+++ b/test/System.Web.WebPages.Test/Html/RadioButtonTest.cs
@@ -0,0 +1,197 @@
+using System.Collections.Generic;
+using System.Web.WebPages.Html;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.WebPages.Test
+{
+ public class RadioButtonTest
+ {
+ [Fact]
+ public void RadioButtonWithEmptyNameThrows()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act and assert
+ Assert.ThrowsArgumentNullOrEmptyString(() => helper.RadioButton(null, null), "name");
+ Assert.ThrowsArgumentNullOrEmptyString(() => helper.RadioButton(String.Empty, null), "name");
+ }
+
+ [Fact]
+ public void RadioButtonWithDefaultArguments()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.RadioButton("foo", "bar", true);
+
+ // Assert
+ Assert.Equal(@"<input checked=""checked"" id=""foo"" name=""foo"" type=""radio"" value=""bar"" />",
+ html.ToHtmlString());
+
+ html = helper.RadioButton("foo", "bar", false);
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""radio"" value=""bar"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonWithObjectAttributes()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.RadioButton("foo", "bar", new { attr = "attr-value" });
+
+ // Assert
+ Assert.Equal(@"<input attr=""attr-value"" id=""foo"" name=""foo"" type=""radio"" value=""bar"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonWithDictionaryAttributes()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.RadioButton("foo", "bar", new Dictionary<string, object> { { "attr", "attr-value" } });
+
+ // Assert
+ Assert.Equal(@"<input attr=""attr-value"" id=""foo"" name=""foo"" type=""radio"" value=""bar"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonUsesModelStateToAssignChecked()
+ {
+ // Arrange
+ var modelState = new ModelStateDictionary();
+ modelState.SetModelValue("foo", "bar");
+ HtmlHelper helper = HtmlHelperFactory.Create(modelState);
+
+ // Act
+ var html = helper.RadioButton("foo", "bar");
+
+ // Assert
+ Assert.Equal(@"<input checked=""checked"" id=""foo"" name=""foo"" type=""radio"" value=""bar"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonUsesModelStateToRemoveChecked()
+ {
+ // Arrange
+ var modelState = new ModelStateDictionary();
+ modelState.SetModelValue("foo", "not-a-bar");
+ HtmlHelper helper = HtmlHelperFactory.Create(modelState);
+
+ // Act
+ var html = helper.RadioButton("foo", "bar", new { @checked = "checked" });
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""radio"" value=""bar"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonWithoutModelStateDoesNotAffectChecked()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.RadioButton("foo", "bar", new { @checked = "checked" });
+
+ // Assert
+ Assert.Equal(@"<input checked=""checked"" id=""foo"" name=""foo"" type=""radio"" value=""bar"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonWithNonStringModelValue()
+ {
+ // Arrange
+ var modelState = new ModelStateDictionary();
+ modelState.SetModelValue("foo", new List<double>());
+ HtmlHelper helper = HtmlHelperFactory.Create(modelState);
+
+ // Act
+ var html = helper.RadioButton("foo", "bar");
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""radio"" value=""bar"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonWithNonStringValue()
+ {
+ // Arrange
+ var modelState = new ModelStateDictionary();
+ modelState.SetModelValue("foo", "bar");
+ HtmlHelper helper = HtmlHelperFactory.Create(modelState);
+
+ // Act
+ var html = helper.RadioButton("foo", 2.53);
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""radio"" value=""2.53"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonWithExplicitChecked()
+ {
+ // Arrange
+ var modelState = new ModelStateDictionary();
+ modelState.SetModelValue("foo", "bar");
+ HtmlHelper helper = HtmlHelperFactory.Create(modelState);
+
+ // Act
+ var html = helper.RadioButton("foo", "not-bar", true);
+
+ // Assert
+ Assert.Equal(@"<input checked=""checked"" id=""foo"" name=""foo"" type=""radio"" value=""not-bar"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonOverwritesImplicitAttributes()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.RadioButton("foo", "foo-value", new { value = "bazValue", type = "fooType", name = "bar" });
+
+ // Assert
+ Assert.Equal(@"<input id=""foo"" name=""foo"" type=""fooType"" value=""foo-value"" />",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void RadioButtonAddsUnobtrusiveValidationAttributes()
+ {
+ // Arrange
+ const string fieldName = "name";
+ var modelStateDictionary = new ModelStateDictionary();
+ var validationHelper = new ValidationHelper(new Mock<HttpContextBase>().Object, modelStateDictionary);
+ HtmlHelper helper = HtmlHelperFactory.Create(modelStateDictionary, validationHelper);
+
+ // Act
+ validationHelper.RequireField(fieldName, "Please specify a valid Name.");
+ validationHelper.Add(fieldName, Validator.StringLength(30, errorMessage: "Name cannot exceed {0} characters"));
+ var html = helper.RadioButton(fieldName, value: 8, htmlAttributes: new Dictionary<string, object> { { "data-some-val", "5" } });
+
+ // Assert
+ Assert.Equal(@"<input data-some-val=""5"" data-val=""true"" data-val-length=""Name cannot exceed 30 characters"" data-val-length-max=""30"" data-val-required=""Please specify a valid Name."" id=""name"" name=""name"" type=""radio"" value=""8"" />",
+ html.ToString());
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/Html/SelectHelperTest.cs b/test/System.Web.WebPages.Test/Html/SelectHelperTest.cs
new file mode 100644
index 00000000..fd8fc670
--- /dev/null
+++ b/test/System.Web.WebPages.Test/Html/SelectHelperTest.cs
@@ -0,0 +1,776 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Web.WebPages.Html;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.WebPages.Test
+{
+ public class SelectExtensionsTest
+ {
+ [Fact]
+ public void DropDownListThrowsWithNoName()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act and assert
+ Assert.ThrowsArgumentNullOrEmptyString(() => helper.DropDownList(name: null, selectList: null), "name");
+ }
+
+ [Fact]
+ public void DropDownListWithNoSelectedItem()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.DropDownList("foo", GetSelectList());
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""foo"" name=""foo"">
+<option value=""A"">Alpha</option>
+<option value=""B"">Bravo</option>
+<option value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListWithDefaultOption()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.DropDownList("foo", "select-one", GetSelectList());
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""foo"" name=""foo"">
+<option value="""">select-one</option>
+<option value=""A"">Alpha</option>
+<option value=""B"">Bravo</option>
+<option value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListWithAttributes()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.DropDownList("foo", GetSelectList(), new { attr = "attr-val", attr2 = "attr-val2" });
+
+ // Assert
+ Assert.Equal(
+ @"<select attr=""attr-val"" attr2=""attr-val2"" id=""foo"" name=""foo"">
+<option value=""A"">Alpha</option>
+<option value=""B"">Bravo</option>
+<option value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListWithExplicitValue()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.DropDownList("foo", null, GetSelectList(), "B", new Dictionary<string, object> { { "attr", "attr-val" } });
+
+ // Assert
+ Assert.Equal(
+ @"<select attr=""attr-val"" id=""foo"" name=""foo"">
+<option value=""A"">Alpha</option>
+<option selected=""selected"" value=""B"">Bravo</option>
+<option value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownWithModelValue()
+ {
+ // Arrange
+ var modelState = new ModelStateDictionary();
+ modelState.SetModelValue("foo", "C");
+ HtmlHelper helper = HtmlHelperFactory.Create(modelState);
+
+ // Act
+ var html = helper.DropDownList("foo", GetSelectList(), new { attr = "attr-val" });
+
+ // Assert
+ Assert.Equal(
+ @"<select attr=""attr-val"" id=""foo"" name=""foo"">
+<option value=""A"">Alpha</option>
+<option value=""B"">Bravo</option>
+<option selected=""selected"" value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownWithExplictAndModelValue()
+ {
+ // Arrange
+ var modelState = new ModelStateDictionary();
+ modelState.SetModelValue("foo", "C");
+ HtmlHelper helper = HtmlHelperFactory.Create(modelState);
+
+ // Act
+ var html = helper.DropDownList("foo", null, GetSelectList(), "B", new { attr = "attr-val" });
+
+ // Assert
+ Assert.Equal(
+ @"<select attr=""attr-val"" id=""foo"" name=""foo"">
+<option value=""A"">Alpha</option>
+<option selected=""selected"" value=""B"">Bravo</option>
+<option value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownWithNonStringModelValue()
+ {
+ // Arrange
+ var modelState = new ModelStateDictionary();
+ modelState.SetModelValue("foo", 23);
+ HtmlHelper helper = HtmlHelperFactory.Create(modelState);
+
+ // Act
+ var html = helper.DropDownList("foo", null, GetSelectList(), new { attr = "attr-val" });
+
+ // Assert
+ Assert.Equal(
+ @"<select attr=""attr-val"" id=""foo"" name=""foo"">
+<option value=""A"">Alpha</option>
+<option value=""B"">Bravo</option>
+<option value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownWithNonStringExplicitValue()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.DropDownList("foo", null, GetSelectList(), new List<int>(), new { attr = "attr-val" });
+
+ // Assert
+ Assert.Equal(
+ @"<select attr=""attr-val"" id=""foo"" name=""foo"">
+<option value=""A"">Alpha</option>
+<option value=""B"">Bravo</option>
+<option value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownWithErrors()
+ {
+ // Arrange
+ var modelState = new ModelStateDictionary();
+ modelState.AddError("foo", "some error");
+ HtmlHelper helper = HtmlHelperFactory.Create(modelState);
+
+ // Act
+ var html = helper.DropDownList("foo", GetSelectList());
+
+ // Assert
+ Assert.Equal(
+ @"<select class=""input-validation-error"" id=""foo"" name=""foo"">
+<option value=""A"">Alpha</option>
+<option value=""B"">Bravo</option>
+<option value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListWithErrorsAndCustomClass()
+ {
+ // Arrange
+ var modelState = new ModelStateDictionary();
+ modelState.AddError("foo", "some error");
+ HtmlHelper helper = HtmlHelperFactory.Create(modelState);
+
+ // Act
+ var html = helper.DropDownList("foo", GetSelectList(), new { @class = "my-class" });
+
+ // Assert
+ Assert.Equal(
+ @"<select class=""input-validation-error my-class"" id=""foo"" name=""foo"">
+<option value=""A"">Alpha</option>
+<option value=""B"">Bravo</option>
+<option value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListWithEmptyOptionLabel()
+ {
+ // Arrange
+ var modelState = new ModelStateDictionary();
+ modelState.AddError("foo", "some error");
+ HtmlHelper helper = HtmlHelperFactory.Create(modelState);
+
+ // Act
+ var html = helper.DropDownList("foo", GetSelectList(), new { @class = "my-class" });
+
+ // Assert
+ Assert.Equal(
+ @"<select class=""input-validation-error my-class"" id=""foo"" name=""foo"">
+<option value=""A"">Alpha</option>
+<option value=""B"">Bravo</option>
+<option value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListWithObjectDictionaryAndTitle()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.DropDownList("foo", "Select One", GetSelectList(), new { @class = "my-class" });
+
+ // Assert
+ Assert.Equal(
+ @"<select class=""my-class"" id=""foo"" name=""foo"">
+<option value="""">Select One</option>
+<option value=""A"">Alpha</option>
+<option value=""B"">Bravo</option>
+<option value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownListWithDotReplacementForId()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.DropDownList("foo.bar", "Select One", GetSelectList());
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""foo_bar"" name=""foo.bar"">
+<option value="""">Select One</option>
+<option value=""A"">Alpha</option>
+<option value=""B"">Bravo</option>
+<option value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void DropDownAddsUnobtrusiveValidationAttributes()
+ {
+ // Arrange
+ const string fieldName = "name";
+ var modelStateDictionary = new ModelStateDictionary();
+ var validationHelper = new ValidationHelper(new Mock<HttpContextBase>().Object, modelStateDictionary);
+ HtmlHelper helper = HtmlHelperFactory.Create(modelStateDictionary, validationHelper);
+
+ // Act
+ validationHelper.RequireField(fieldName, "Please specify a valid Name.");
+ validationHelper.Add(fieldName, Validator.StringLength(30, errorMessage: "Name cannot exceed {0} characters"));
+ var html = helper.DropDownList(fieldName, GetSelectList(), htmlAttributes: new Dictionary<string, object> { { "data-some-val", "5" } });
+
+ // Assert
+ Assert.Equal(@"<select data-some-val=""5"" data-val=""true"" data-val-length=""Name cannot exceed 30 characters"" data-val-length-max=""30"" data-val-required=""Please specify a valid Name."" id=""name"" name=""name"">
+<option value=""A"">Alpha</option>
+<option value=""B"">Bravo</option>
+<option value=""C"">Charlie</option>
+</select>", html.ToString());
+ }
+
+ // ListBox
+
+ [Fact]
+ public void ListBoxThrowsWithNoName()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act and assert
+ Assert.ThrowsArgumentNullOrEmptyString(() => helper.ListBox(name: null, selectList: null), "name");
+ }
+
+ [Fact]
+ public void ListBoxWithNoSelectedItem()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.ListBox("foo", GetSelectList());
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""foo"" name=""foo"">
+<option value=""A"">Alpha</option>
+<option value=""B"">Bravo</option>
+<option value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxWithDefaultOption()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.ListBox("foo", "select-one", GetSelectList());
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""foo"" name=""foo"">
+<option value="""">select-one</option>
+<option value=""A"">Alpha</option>
+<option value=""B"">Bravo</option>
+<option value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxWithAttributes()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.ListBox("foo", GetSelectList(), new { attr = "attr-val", attr2 = "attr-val2" });
+
+ // Assert
+ Assert.Equal(
+ @"<select attr=""attr-val"" attr2=""attr-val2"" id=""foo"" name=""foo"">
+<option value=""A"">Alpha</option>
+<option value=""B"">Bravo</option>
+<option value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxWithExplicitValue()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.ListBox("foo", null, GetSelectList(), "B", new Dictionary<string, object> { { "attr", "attr-val" } });
+
+ // Assert
+ Assert.Equal(
+ @"<select attr=""attr-val"" id=""foo"" name=""foo"">
+<option value=""A"">Alpha</option>
+<option selected=""selected"" value=""B"">Bravo</option>
+<option value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxWithModelValue()
+ {
+ // Arrange
+ var modelState = new ModelStateDictionary();
+ modelState.SetModelValue("foo", "C");
+ HtmlHelper helper = HtmlHelperFactory.Create(modelState);
+
+ // Act
+ var html = helper.ListBox("foo", GetSelectList(), new { attr = "attr-val" });
+
+ // Assert
+ Assert.Equal(
+ @"<select attr=""attr-val"" id=""foo"" name=""foo"">
+<option value=""A"">Alpha</option>
+<option value=""B"">Bravo</option>
+<option selected=""selected"" value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxWithExplicitMultipleValuesAndNoMultiple()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.ListBox("foo", null, GetSelectList(), new[] { "B", "C" }, new Dictionary<string, object> { { "attr", "attr-val" } });
+
+ // Assert
+ Assert.Equal(
+ @"<select attr=""attr-val"" id=""foo"" name=""foo"">
+<option value=""A"">Alpha</option>
+<option selected=""selected"" value=""B"">Bravo</option>
+<option value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxWithExplicitMultipleValuesAndMultiple()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.ListBox("foo", null, GetSelectList(), new[] { "B", "C" }, 4, true);
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""foo"" multiple=""multiple"" name=""foo"" size=""4"">
+<option value=""A"">Alpha</option>
+<option selected=""selected"" value=""B"">Bravo</option>
+<option selected=""selected"" value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxWithMultipleModelValue()
+ {
+ // Arrange
+ var modelState = new ModelStateDictionary();
+ modelState.SetModelValue("foo", new[] { "A", "C" });
+ HtmlHelper helper = HtmlHelperFactory.Create(modelState);
+
+ // Act
+ var html = helper.ListBox("foo", GetSelectList(), new { attr = "attr-val" });
+
+ // Assert
+ Assert.Equal(
+ @"<select attr=""attr-val"" id=""foo"" name=""foo"">
+<option selected=""selected"" value=""A"">Alpha</option>
+<option value=""B"">Bravo</option>
+<option value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxWithModelValueAndExplicitSelectItem()
+ {
+ // Arrange
+ var modelState = new ModelStateDictionary();
+ modelState.SetModelValue("foo", new[] { "C", "D" });
+ HtmlHelper helper = HtmlHelperFactory.Create(modelState);
+ var selectList = GetSelectList().ToList();
+ selectList[1].Selected = true;
+
+ // Act
+ var html = helper.ListBox("foo", selectList, new { attr = "attr-val" });
+
+ // Assert
+ Assert.Equal(
+ @"<select attr=""attr-val"" id=""foo"" name=""foo"">
+<option value=""A"">Alpha</option>
+<option selected=""selected"" value=""B"">Bravo</option>
+<option value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxWithMultiSelectAndMultipleModelValue()
+ {
+ // Arrange
+ var modelState = new ModelStateDictionary();
+ modelState.SetModelValue("foo", new[] { "A", "C" });
+ HtmlHelper helper = HtmlHelperFactory.Create(modelState);
+
+ // Act
+ var html = helper.ListBox("foo", GetSelectList(), null, 4, true);
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""foo"" multiple=""multiple"" name=""foo"" size=""4"">
+<option selected=""selected"" value=""A"">Alpha</option>
+<option value=""B"">Bravo</option>
+<option selected=""selected"" value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxWithMultiSelectAndMultipleExplicitValues()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.ListBox("foo", GetSelectList(), new[] { "A", "C" }, 4, true);
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""foo"" multiple=""multiple"" name=""foo"" size=""4"">
+<option selected=""selected"" value=""A"">Alpha</option>
+<option value=""B"">Bravo</option>
+<option selected=""selected"" value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxWithMultiSelectAndExplitSelectValue()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+ var selectList = GetSelectList().ToList();
+ selectList.First().Selected = selectList.Last().Selected = true;
+
+ // Act
+ var html = helper.ListBox("foo", selectList, new[] { "B" }, 4, true);
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""foo"" multiple=""multiple"" name=""foo"" size=""4"">
+<option selected=""selected"" value=""A"">Alpha</option>
+<option selected=""selected"" value=""B"">Bravo</option>
+<option selected=""selected"" value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxWithExplictAndModelValue()
+ {
+ // Arrange
+ var modelState = new ModelStateDictionary();
+ modelState.SetModelValue("foo", "C");
+ HtmlHelper helper = HtmlHelperFactory.Create(modelState);
+
+ // Act
+ var html = helper.ListBox("foo", defaultOption: null, selectList: GetSelectList(),
+ selectedValues: "B", htmlAttributes: new { attr = "attr-val" });
+
+ // Assert
+ Assert.Equal(
+ @"<select attr=""attr-val"" id=""foo"" name=""foo"">
+<option value=""A"">Alpha</option>
+<option selected=""selected"" value=""B"">Bravo</option>
+<option value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxWithErrorAndExplictAndModelState()
+ {
+ // Arrange
+ var modelState = new ModelStateDictionary();
+ modelState.SetModelValue("foo", "C");
+ modelState.AddError("foo", "test");
+ HtmlHelper helper = HtmlHelperFactory.Create(modelState);
+
+ // Act
+ var html = helper.ListBox("foo.bar", "Select One", GetSelectList());
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""foo_bar"" name=""foo.bar"">
+<option value="""">Select One</option>
+<option value=""A"">Alpha</option>
+<option value=""B"">Bravo</option>
+<option value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxWithNonStringModelValue()
+ {
+ // Arrange
+ var modelState = new ModelStateDictionary();
+ modelState.SetModelValue("foo", 23);
+ HtmlHelper helper = HtmlHelperFactory.Create(modelState);
+
+ // Act
+ var html = helper.ListBox("foo", null, GetSelectList(), new { attr = "attr-val" });
+
+ // Assert
+ Assert.Equal(
+ @"<select attr=""attr-val"" id=""foo"" name=""foo"">
+<option value=""A"">Alpha</option>
+<option value=""B"">Bravo</option>
+<option value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxWithNonStringExplicitValue()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.ListBox("foo", null, GetSelectList(), new List<int>(), new { attr = "attr-val" });
+
+ // Assert
+ Assert.Equal(
+ @"<select attr=""attr-val"" id=""foo"" name=""foo"">
+<option value=""A"">Alpha</option>
+<option value=""B"">Bravo</option>
+<option value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxWithErrors()
+ {
+ // Arrange
+ var modelState = new ModelStateDictionary();
+ modelState.AddError("foo", "some error");
+ HtmlHelper helper = HtmlHelperFactory.Create(modelState);
+
+ // Act
+ var html = helper.ListBox("foo", GetSelectList());
+
+ // Assert
+ Assert.Equal(
+ @"<select class=""input-validation-error"" id=""foo"" name=""foo"">
+<option value=""A"">Alpha</option>
+<option value=""B"">Bravo</option>
+<option value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxWithErrorsAndCustomClass()
+ {
+ // Arrange
+ var modelState = new ModelStateDictionary();
+ modelState.AddError("foo", "some error");
+ HtmlHelper helper = HtmlHelperFactory.Create(modelState);
+
+ // Act
+ var html = helper.ListBox("foo", GetSelectList(), new { @class = "my-class" });
+
+ // Assert
+ Assert.Equal(
+ @"<select class=""input-validation-error my-class"" id=""foo"" name=""foo"">
+<option value=""A"">Alpha</option>
+<option value=""B"">Bravo</option>
+<option value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxWithEmptyOptionLabel()
+ {
+ // Arrange
+ var modelState = new ModelStateDictionary();
+ modelState.AddError("foo", "some error");
+ HtmlHelper helper = HtmlHelperFactory.Create(modelState);
+
+ // Act
+ var html = helper.ListBox("foo", GetSelectList(), new { @class = "my-class" });
+
+ // Assert
+ Assert.Equal(
+ @"<select class=""input-validation-error my-class"" id=""foo"" name=""foo"">
+<option value=""A"">Alpha</option>
+<option value=""B"">Bravo</option>
+<option value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxWithObjectDictionaryAndTitle()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.ListBox("foo", "Select One", GetSelectList(), new { @class = "my-class" });
+
+ // Assert
+ Assert.Equal(
+ @"<select class=""my-class"" id=""foo"" name=""foo"">
+<option value="""">Select One</option>
+<option value=""A"">Alpha</option>
+<option value=""B"">Bravo</option>
+<option value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxWithDotReplacementForId()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.ListBox("foo.bar", "Select One", GetSelectList());
+
+ // Assert
+ Assert.Equal(
+ @"<select id=""foo_bar"" name=""foo.bar"">
+<option value="""">Select One</option>
+<option value=""A"">Alpha</option>
+<option value=""B"">Bravo</option>
+<option value=""C"">Charlie</option>
+</select>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ListBoxAddsUnobtrusiveValidationAttributes()
+ {
+ // Arrange
+ const string fieldName = "name";
+ var modelStateDictionary = new ModelStateDictionary();
+ var validationHelper = new ValidationHelper(new Mock<HttpContextBase>().Object, modelStateDictionary);
+ HtmlHelper helper = HtmlHelperFactory.Create(modelStateDictionary, validationHelper);
+
+ // Act
+ validationHelper.RequireField(fieldName, "Please specify a valid Name.");
+ validationHelper.Add(fieldName, Validator.StringLength(30, errorMessage: "Name cannot exceed {0} characters"));
+ var html = helper.ListBox(fieldName, GetSelectList(), htmlAttributes: new Dictionary<string, object> { { "data-some-val", "5" } });
+
+ // Assert
+ Assert.Equal(@"<select data-some-val=""5"" data-val=""true"" data-val-length=""Name cannot exceed 30 characters"" data-val-length-max=""30"" data-val-required=""Please specify a valid Name."" id=""name"" name=""name"">
+<option value=""A"">Alpha</option>
+<option value=""B"">Bravo</option>
+<option value=""C"">Charlie</option>
+</select>", html.ToString());
+ }
+
+ private static IEnumerable<SelectListItem> GetSelectList()
+ {
+ yield return new SelectListItem() { Text = "Alpha", Value = "A" };
+ yield return new SelectListItem() { Text = "Bravo", Value = "B" };
+ yield return new SelectListItem() { Text = "Charlie", Value = "C" };
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/Html/TextAreaHelperTest.cs b/test/System.Web.WebPages.Test/Html/TextAreaHelperTest.cs
new file mode 100644
index 00000000..27f16845
--- /dev/null
+++ b/test/System.Web.WebPages.Test/Html/TextAreaHelperTest.cs
@@ -0,0 +1,209 @@
+using System.Collections.Generic;
+using System.Web.WebPages.Html;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.WebPages.Test
+{
+ public class TextAreaExtensionsTest
+ {
+ [Fact]
+ public void TextAreaWithEmptyNameThrows()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act and assert
+ Assert.ThrowsArgument(() => helper.TextArea(null), "name", "Value cannot be null or an empty string.");
+
+ // Act and assert
+ Assert.ThrowsArgument(() => helper.TextArea(String.Empty), "name", "Value cannot be null or an empty string.");
+ }
+
+ [Fact]
+ public void TextAreaWithDefaultRowsAndCols()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.TextArea("foo");
+
+ // Assert
+ Assert.Equal(@"<textarea cols=""20"" id=""foo"" name=""foo"" rows=""2""></textarea>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextAreaWithZeroRowsAndColumns()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.TextArea("foo", null, 0, 0, null);
+
+ // Assert
+ Assert.Equal(@"<textarea id=""foo"" name=""foo""></textarea>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextAreaWithNonZeroRowsAndColumns()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = helper.TextArea("foo", null, 4, 10, null);
+
+ // Assert
+ Assert.Equal(@"<textarea cols=""10"" id=""foo"" name=""foo"" rows=""4""></textarea>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextAreaWithObjectAttributes()
+ {
+ // Arrange
+ ModelStateDictionary modelState = new ModelStateDictionary();
+ modelState.SetModelValue("foo", "foo-value");
+ HtmlHelper helper = HtmlHelperFactory.Create(modelState);
+
+ // Act
+ var html = helper.TextArea("foo", new { attr = "value", cols = 6 });
+
+ // Assert
+ Assert.Equal(@"<textarea attr=""value"" cols=""6"" id=""foo"" name=""foo"" rows=""2"">foo-value</textarea>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextAreaWithExplicitValue()
+ {
+ // Arrange
+ ModelStateDictionary modelState = new ModelStateDictionary();
+ modelState.SetModelValue("foo", "explicit-foo-value");
+ HtmlHelper helper = HtmlHelperFactory.Create(modelState);
+
+ // Act
+ var html = helper.TextArea("foo", "explicit-foo-value", new { attr = "attr-value", cols = 6 });
+
+ // Assert
+ Assert.Equal(@"<textarea attr=""attr-value"" cols=""6"" id=""foo"" name=""foo"" rows=""2"">explicit-foo-value</textarea>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextAreaWithDictionaryAttributes()
+ {
+ // Arrange
+ ModelStateDictionary modelState = new ModelStateDictionary();
+ modelState.SetModelValue("foo", "explicit-foo-value");
+ HtmlHelper helper = HtmlHelperFactory.Create(modelState);
+ var attributes = new Dictionary<string, object>() { { "attr", "attr-val" }, { "rows", 15 }, { "cols", 12 } };
+ // Act
+ var html = helper.TextArea("foo", attributes);
+
+ // Assert
+ Assert.Equal(@"<textarea attr=""attr-val"" cols=""12"" id=""foo"" name=""foo"" rows=""15"">explicit-foo-value</textarea>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextAreaWithNoValueAndObjectAttributes()
+ {
+ // Arrange
+ HtmlHelper helper = HtmlHelperFactory.Create();
+ var attributes = new Dictionary<string, object>() { { "attr", "attr-val" }, { "rows", 15 }, { "cols", 12 } };
+ // Act
+ var html = helper.TextArea("foo", attributes);
+
+ // Assert
+ Assert.Equal(@"<textarea attr=""attr-val"" cols=""12"" id=""foo"" name=""foo"" rows=""15""></textarea>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextAreaWithNullValue()
+ {
+ // Arrange
+ ModelStateDictionary modelState = new ModelStateDictionary();
+ modelState.SetModelValue("foo", "explicit-foo-value");
+ HtmlHelper helper = HtmlHelperFactory.Create(modelState);
+ var attributes = new Dictionary<string, object>() { { "attr", "attr-val" }, { "rows", 15 }, { "cols", 12 } };
+ // Act
+ var html = helper.TextArea("foo", null, attributes);
+
+ // Assert
+ Assert.Equal(@"<textarea attr=""attr-val"" cols=""12"" id=""foo"" name=""foo"" rows=""15"">explicit-foo-value</textarea>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextAreaWithError()
+ {
+ // Arrange
+ ModelStateDictionary modelState = new ModelStateDictionary();
+ modelState.AddError("foo", "some error");
+ HtmlHelper helper = HtmlHelperFactory.Create(modelState);
+
+ // Act
+ var html = helper.TextArea("foo", String.Empty);
+
+ // Assert
+ Assert.Equal(@"<textarea class=""input-validation-error"" cols=""20"" id=""foo"" name=""foo"" rows=""2""></textarea>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextAreaWithErrorAndCustomCssClass()
+ {
+ // Arrange
+ ModelStateDictionary modelState = new ModelStateDictionary();
+ modelState.AddError("foo", "some error");
+ HtmlHelper helper = HtmlHelperFactory.Create(modelState);
+
+ // Act
+ var html = helper.TextArea("foo", String.Empty, new { @class = "my-css" });
+
+ // Assert
+ Assert.Equal(@"<textarea class=""input-validation-error my-css"" cols=""20"" id=""foo"" name=""foo"" rows=""2""></textarea>",
+ html.ToHtmlString());
+ }
+
+ // [Fact]
+ // Cant test this in multi-threaded
+ public void TextAreaWithCustomErrorClass()
+ {
+ // Arrange
+ ModelStateDictionary modelState = new ModelStateDictionary();
+ modelState.AddError("foo", "some error");
+ HtmlHelper.ValidationInputCssClassName = "custom-input-validation-error";
+ HtmlHelper helper = HtmlHelperFactory.Create(modelState);
+
+ // Act
+ var html = helper.TextArea("foo", String.Empty, new { @class = "my-css" });
+
+ // Assert
+ Assert.Equal(@"<textarea class=""custom-input-validation-error my-css"" cols=""20"" id=""foo"" name=""foo"" rows=""2""></textarea>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void TextAreaAddsUnobtrusiveValidationAttributes()
+ {
+ // Arrange
+ const string fieldName = "name";
+ var modelStateDictionary = new ModelStateDictionary();
+ var validationHelper = new ValidationHelper(new Mock<HttpContextBase>().Object, modelStateDictionary);
+ HtmlHelper helper = HtmlHelperFactory.Create(modelStateDictionary, validationHelper);
+
+ // Act
+ validationHelper.RequireField(fieldName, "Please specify a valid Name.");
+ validationHelper.Add(fieldName, Validator.StringLength(30, errorMessage: "Name cannot exceed {0} characters"));
+ var html = helper.TextArea(fieldName, htmlAttributes: new Dictionary<string, object> { { "data-some-val", "5" } });
+
+ // Assert
+ Assert.Equal(@"<textarea cols=""20"" data-some-val=""5"" data-val=""true"" data-val-length=""Name cannot exceed 30 characters"" data-val-length-max=""30"" data-val-required=""Please specify a valid Name."" id=""name"" name=""name"" rows=""2""></textarea>",
+ html.ToString());
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/Html/ValidationHelperTest.cs b/test/System.Web.WebPages.Test/Html/ValidationHelperTest.cs
new file mode 100644
index 00000000..fbd09d2c
--- /dev/null
+++ b/test/System.Web.WebPages.Test/Html/ValidationHelperTest.cs
@@ -0,0 +1,342 @@
+using System.Collections.Generic;
+using System.Web.WebPages.Html;
+using Xunit;
+
+namespace System.Web.WebPages.Test
+{
+ public class ValidationHelperTest
+ {
+ [Fact]
+ public void ValidationMessageAllowsEmptyModelName()
+ {
+ // Arrange
+ ModelStateDictionary dictionary = new ModelStateDictionary();
+ dictionary.AddError("test", "some error text");
+ HtmlHelper htmlHelper = HtmlHelperFactory.Create(dictionary);
+
+ // Act
+ var html = htmlHelper.ValidationMessage("test");
+
+ // Assert
+ Assert.Equal(@"<span class=""field-validation-error"" data-valmsg-for=""test"" data-valmsg-replace=""true"">some error text</span>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ValidationMessageReturnsFirstError()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = HtmlHelperFactory.Create(GetModelStateWithErrors());
+
+ // Act
+ var html = htmlHelper.ValidationMessage("foo");
+
+ // Assert
+ Assert.Equal(@"<span class=""field-validation-error"" data-valmsg-for=""foo"" data-valmsg-replace=""true"">foo error &lt;1&gt;</span>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ValidationMessageUsesValidCssClassIfFieldDoesNotHaveErrors()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = HtmlHelperFactory.Create(GetModelStateWithErrors());
+
+ // Act
+ var html = htmlHelper.ValidationMessage("baz");
+
+ // Assert
+ Assert.Equal(@"<span class=""field-validation-valid"" data-valmsg-for=""baz"" data-valmsg-replace=""true""></span>", html.ToString());
+ }
+
+ [Fact]
+ public void ValidationMessageReturnsWithObjectAttributes()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = HtmlHelperFactory.Create(GetModelStateWithErrors());
+
+ // Act
+ var html = htmlHelper.ValidationMessage("foo", new { attr = "attr-value" });
+
+ // Assert
+ Assert.Equal(@"<span attr=""attr-value"" class=""field-validation-error"" data-valmsg-for=""foo"" data-valmsg-replace=""true"">foo error &lt;1&gt;</span>",
+ html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ValidationMessageReturnsWithCustomMessage()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = HtmlHelperFactory.Create(GetModelStateWithErrors());
+
+ // Atc
+ var html = htmlHelper.ValidationMessage("foo", "bar error");
+
+ // Assert
+ Assert.Equal(@"<span class=""field-validation-error"" data-valmsg-for=""foo"" data-valmsg-replace=""false"">bar error</span>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ValidationMessageReturnsWithCustomMessageAndObjectAttributes()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = HtmlHelperFactory.Create(GetModelStateWithErrors());
+
+ // Act
+ var html = htmlHelper.ValidationMessage("foo", "bar error", new { baz = "baz" });
+
+ // Assert
+ Assert.Equal(@"<span baz=""baz"" class=""field-validation-error"" data-valmsg-for=""foo"" data-valmsg-replace=""false"">bar error</span>", html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ValidationMessageWithModelStateAndNoErrors()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = HtmlHelperFactory.Create(GetModelStateWithErrors());
+
+ // Act
+ var html = htmlHelper.ValidationMessage("baz");
+
+ // Assert
+ Assert.Equal(@"<span class=""field-validation-valid"" data-valmsg-for=""baz"" data-valmsg-replace=""true""></span>", html.ToString());
+ }
+
+ [Fact]
+ public void ValidationSummary()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = HtmlHelperFactory.Create(GetModelStateWithErrors());
+
+ // Act
+ var html = htmlHelper.ValidationSummary();
+
+ // Assert
+ Assert.Equal(@"<div class=""validation-summary-errors"" data-valmsg-summary=""true""><ul>
+<li>foo error &lt;1&gt;</li>
+<li>foo error &lt;2&gt;</li>
+<li>bar error &lt;1&gt;</li>
+<li>bar error &lt;2&gt;</li>
+</ul></div>"
+ , html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ValidationSummaryWithMessage()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = HtmlHelperFactory.Create(GetModelStateWithErrors());
+
+ // Act
+ var html = htmlHelper.ValidationSummary("test message");
+
+ // Assert
+ Assert.Equal(@"<div class=""validation-summary-errors"" data-valmsg-summary=""true""><span>test message</span>
+<ul>
+<li>foo error &lt;1&gt;</li>
+<li>foo error &lt;2&gt;</li>
+<li>bar error &lt;1&gt;</li>
+<li>bar error &lt;2&gt;</li>
+</ul></div>"
+ , html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ValidationSummaryWithFormErrors()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = HtmlHelperFactory.Create(GetModelStateWithFormErrors());
+
+ // Act
+ var html = htmlHelper.ValidationSummary();
+
+ // Assert
+ Assert.Equal(@"<div class=""validation-summary-errors"" data-valmsg-summary=""true""><ul>
+<li>foo error &lt;1&gt;</li>
+<li>foo error &lt;2&gt;</li>
+<li>bar error &lt;1&gt;</li>
+<li>bar error &lt;2&gt;</li>
+<li>some form error &lt;1&gt;</li>
+<li>some form error &lt;2&gt;</li>
+</ul></div>"
+ , html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ValidationSummaryWithFormErrorsAndExcludeFieldErrors()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = HtmlHelperFactory.Create(GetModelStateWithFormErrors());
+
+ // Act
+ var html = htmlHelper.ValidationSummary(excludeFieldErrors: true);
+
+ // Assert
+ Assert.Equal(@"<div class=""validation-summary-errors""><ul>
+<li>some form error &lt;1&gt;</li>
+<li>some form error &lt;2&gt;</li>
+</ul></div>"
+ , html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ValidationSummaryWithObjectProperties()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = HtmlHelperFactory.Create(GetModelStateWithErrors());
+
+ // Act
+ var html = htmlHelper.ValidationSummary(new { attr = "attr-value", @class = "my-class" });
+
+ // Assert
+ Assert.Equal(@"<div attr=""attr-value"" class=""validation-summary-errors my-class"" data-valmsg-summary=""true""><ul>
+<li>foo error &lt;1&gt;</li>
+<li>foo error &lt;2&gt;</li>
+<li>bar error &lt;1&gt;</li>
+<li>bar error &lt;2&gt;</li>
+</ul></div>"
+ , html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ValidationSummaryWithDictionary()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = HtmlHelperFactory.Create(GetModelStateWithErrors());
+
+ // Act
+ var html = htmlHelper.ValidationSummary(new Dictionary<string, object> { { "attr", "attr-value" }, { "class", "my-class" } });
+
+ // Assert
+ Assert.Equal(@"<div attr=""attr-value"" class=""validation-summary-errors my-class"" data-valmsg-summary=""true""><ul>
+<li>foo error &lt;1&gt;</li>
+<li>foo error &lt;2&gt;</li>
+<li>bar error &lt;1&gt;</li>
+<li>bar error &lt;2&gt;</li>
+</ul></div>"
+ , html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ValidationSummaryWithDictionaryAndMessage()
+ {
+ HtmlHelper htmlHelper = HtmlHelperFactory.Create(GetModelStateWithErrors());
+
+ // Act
+ var html = htmlHelper.ValidationSummary("This is a message.", new Dictionary<string, object> { { "attr", "attr-value" }, { "class", "my-class" } });
+
+ // Assert
+ Assert.Equal(@"<div attr=""attr-value"" class=""validation-summary-errors my-class"" data-valmsg-summary=""true""><span>This is a message.</span>
+<ul>
+<li>foo error &lt;1&gt;</li>
+<li>foo error &lt;2&gt;</li>
+<li>bar error &lt;1&gt;</li>
+<li>bar error &lt;2&gt;</li>
+</ul></div>"
+ , html.ToHtmlString());
+ }
+
+ //[Fact]
+ // Cant test this, as it sets a static property
+ public void ValidationSummaryWithCustomValidationSummaryClass()
+ {
+ // Arrange
+ HtmlHelper.ValidationSummaryClass = "my-val-class";
+ HtmlHelper htmlHelper = HtmlHelperFactory.Create(GetModelStateWithErrors());
+
+ // Act
+ var html = htmlHelper.ValidationSummary("This is a message.", new Dictionary<string, object> { { "attr", "attr-value" }, { "class", "my-class" } });
+
+ // Assert
+ Assert.Equal(@"<div attr=""attr-value"" class=""my-val-class my-class""><span>This is a message.</span>
+<ul>
+<li>foo error &lt;1&gt;</li>
+<li>foo error &lt;2&gt;</li>
+<li>bar error &lt;1&gt;</li>
+<li>bar error &lt;2&gt;</li>
+</ul></div>"
+ , html.ToHtmlString());
+ }
+
+ [Fact]
+ public void ValidationSummaryWithNoErrorReturnsNullIfExcludeFieldErrorsIsSetToFalse()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = htmlHelper.ValidationSummary(excludeFieldErrors: false);
+
+ // Assert
+ Assert.Equal(@"<div class=""validation-summary-valid"" data-valmsg-summary=""true""><ul>
+</ul></div>", html.ToString());
+ }
+
+ [Fact]
+ public void ValidationSummaryWithNoErrorReturnsNull()
+ {
+ // Arrange
+ HtmlHelper htmlHelper = HtmlHelperFactory.Create();
+
+ // Act
+ var html = htmlHelper.ValidationSummary(excludeFieldErrors: true);
+
+ // Assert
+ Assert.Null(html);
+ }
+
+ [Fact]
+ public void ValidationSummaryWithNoFormErrorsAndExcludedFieldErrorsReturnsNull()
+ {
+ // Arrange
+ ModelStateDictionary modelState = new ModelStateDictionary();
+ modelState.AddError("foo", "error");
+ modelState.AddError("bar", "error");
+
+ HtmlHelper htmlHelper = HtmlHelperFactory.Create(modelState);
+
+ // Act
+ var html = htmlHelper.ValidationSummary(excludeFieldErrors: true);
+
+ // Assert
+ Assert.Null(html);
+ }
+
+ [Fact]
+ public void ValidationSummaryWithMultipleFormErrorsAndExcludedFieldErrors()
+ {
+ // Arrange
+ ModelStateDictionary modelState = new ModelStateDictionary();
+ modelState.AddFormError("error <1>");
+ modelState.AddFormError("error <2>");
+
+ HtmlHelper htmlHelper = HtmlHelperFactory.Create(modelState);
+
+ // Act
+ var html = htmlHelper.ValidationSummary(excludeFieldErrors: true);
+
+ // Assert
+ Assert.Equal(@"<div class=""validation-summary-errors""><ul>
+<li>error &lt;1&gt;</li>
+<li>error &lt;2&gt;</li>
+</ul></div>"
+ , html.ToHtmlString());
+ }
+
+ private static ModelStateDictionary GetModelStateWithErrors()
+ {
+ ModelStateDictionary modelState = new ModelStateDictionary();
+ modelState.AddError("foo", "foo error <1>");
+ modelState.AddError("foo", "foo error <2>");
+ modelState.AddError("bar", "bar error <1>");
+ modelState.AddError("bar", "bar error <2>");
+ return modelState;
+ }
+
+ private static ModelStateDictionary GetModelStateWithFormErrors()
+ {
+ ModelStateDictionary modelState = GetModelStateWithErrors();
+ modelState.AddFormError("some form error <1>");
+ modelState.AddFormError("some form error <2>");
+ return modelState;
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/Instrumentation/InstrumentationServiceTest.cs b/test/System.Web.WebPages.Test/Instrumentation/InstrumentationServiceTest.cs
new file mode 100644
index 00000000..c9226fef
--- /dev/null
+++ b/test/System.Web.WebPages.Test/Instrumentation/InstrumentationServiceTest.cs
@@ -0,0 +1,97 @@
+using System.Collections.Generic;
+using System.Dynamic;
+using System.IO;
+using System.Web.WebPages.Instrumentation;
+using Xunit;
+
+namespace System.Web.WebPages.Test.Instrumentation
+{
+ public class InstrumentationServiceTest
+ {
+ [Fact]
+ public void BeginContextDelegatesToRegisteredListeners()
+ {
+ // Arrange
+ dynamic listener1 = CreateListener();
+ dynamic listener2 = CreateListener();
+ InstrumentationService inst = CreateInstrumentationService(listener1, listener2);
+ TextWriter mockWriter = new StringWriter();
+
+ // Act
+ inst.BeginContext(null, "Foo.cshtml", mockWriter, 42, 24, isLiteral: false);
+
+ // Assert
+ Assert.Equal(1, listener1.BeginContextCalls.Count);
+ Assert.Equal(0, listener1.EndContextCalls.Count);
+ Assert.Equal(1, listener2.BeginContextCalls.Count);
+ Assert.Equal(0, listener2.EndContextCalls.Count);
+
+ AssertContext("Foo.cshtml", mockWriter, 42, 24, false, listener1.BeginContextCalls[0]);
+ AssertContext("Foo.cshtml", mockWriter, 42, 24, false, listener2.BeginContextCalls[0]);
+ }
+
+ [Fact]
+ public void EndContextDelegatesToRegisteredListeners()
+ {
+ // Arrange
+ dynamic listener1 = CreateListener();
+ dynamic listener2 = CreateListener();
+ InstrumentationService inst = CreateInstrumentationService(listener1, listener2);
+ TextWriter mockWriter = new StringWriter();
+
+ // Act
+ inst.EndContext(null, "Foo.cshtml", mockWriter, 42, 24, isLiteral: false);
+
+ // Assert
+ Assert.Equal(1, listener1.EndContextCalls.Count);
+ Assert.Equal(0, listener1.BeginContextCalls.Count);
+ Assert.Equal(1, listener2.EndContextCalls.Count);
+ Assert.Equal(0, listener2.BeginContextCalls.Count);
+
+ AssertContext("Foo.cshtml", mockWriter, 42, 24, false, listener1.EndContextCalls[0]);
+ AssertContext("Foo.cshtml", mockWriter, 42, 24, false, listener2.EndContextCalls[0]);
+ }
+
+ private void AssertContext(string virtualPath, TextWriter writer, int startPosition, int length, bool isLiteral, dynamic context)
+ {
+ PageExecutionContextAdapter ctx = new PageExecutionContextAdapter(context);
+ Assert.Equal(virtualPath, ctx.VirtualPath);
+ Assert.Same(writer, ctx.TextWriter);
+ Assert.Equal(startPosition, ctx.StartPosition);
+ Assert.Equal(length, ctx.Length);
+ Assert.Equal(isLiteral, ctx.IsLiteral);
+ }
+
+ private InstrumentationService CreateInstrumentationService(params dynamic[] listeners)
+ {
+ dynamic service = new ExpandoObject();
+ service.ExecutionListeners = new List<dynamic>(listeners);
+ InstrumentationService inst = new InstrumentationService();
+ inst.IsAvailable = true;
+ inst.ExtractInstrumentationService = _ => new PageInstrumentationServiceAdapter(service);
+ inst.CreateContext = CreateExpandoContext;
+ return inst;
+ }
+
+ private dynamic CreateListener()
+ {
+ dynamic listener = new ExpandoObject();
+ listener.BeginContextCalls = new List<dynamic>();
+ listener.EndContextCalls = new List<dynamic>();
+ listener.BeginContext = (Action<dynamic>)(d => { listener.BeginContextCalls.Add(d); });
+ listener.EndContext = (Action<dynamic>)(d => { listener.EndContextCalls.Add(d); });
+ return listener;
+ }
+
+ private PageExecutionContextAdapter CreateExpandoContext(string virtualPath, TextWriter writer, int startPosition, int length, bool isLiteral)
+ {
+ dynamic ctx = new ExpandoObject();
+ ctx.VirtualPath = virtualPath;
+ ctx.TextWriter = writer;
+ ctx.StartPosition = startPosition;
+ ctx.Length = length;
+ ctx.IsLiteral = isLiteral;
+ return new PageExecutionContextAdapter(ctx);
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/Mvc/HttpAntiForgeryExceptionTest.cs b/test/System.Web.WebPages.Test/Mvc/HttpAntiForgeryExceptionTest.cs
new file mode 100644
index 00000000..9d28d216
--- /dev/null
+++ b/test/System.Web.WebPages.Test/Mvc/HttpAntiForgeryExceptionTest.cs
@@ -0,0 +1,64 @@
+using System.IO;
+using System.Runtime.Serialization.Formatters.Binary;
+using Xunit;
+
+namespace System.Web.Mvc.Test
+{
+ public class HttpAntiForgeryExceptionTest
+ {
+ [Fact]
+ public void ConstructorWithMessageAndInnerExceptionParameter()
+ {
+ // Arrange
+ Exception innerException = new Exception();
+
+ // Act
+ HttpAntiForgeryException ex = new HttpAntiForgeryException("the message", innerException);
+
+ // Assert
+ Assert.Equal("the message", ex.Message);
+ Assert.Equal(innerException, ex.InnerException);
+ }
+
+ [Fact]
+ public void ConstructorWithMessageParameter()
+ {
+ // Act
+ HttpAntiForgeryException ex = new HttpAntiForgeryException("the message");
+
+ // Assert
+ Assert.Equal("the message", ex.Message);
+ }
+
+ [Fact]
+ public void ConstructorWithoutParameters()
+ {
+ // Act & assert
+ Assert.Throws<HttpAntiForgeryException>(
+ delegate { throw new HttpAntiForgeryException(); });
+ }
+
+ [Fact]
+ public void TypeIsSerializable()
+ {
+ // If this ever fails with SerializationException : Unable to find assembly 'System.Web.Mvc, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35'
+ // (usually when the assembly version is incremented) you need to modify the App.config file in this test project to reference the new version.
+
+ // Arrange
+ MemoryStream ms = new MemoryStream();
+ BinaryFormatter formatter = new BinaryFormatter();
+ HttpAntiForgeryException ex = new HttpAntiForgeryException("the message", new Exception("inner exception"));
+
+ // Act
+ formatter.Serialize(ms, ex);
+ ms.Position = 0;
+ HttpAntiForgeryException deserialized = formatter.Deserialize(ms) as HttpAntiForgeryException;
+
+ // Assert
+ Assert.NotNull(deserialized);
+ Assert.Equal("the message", deserialized.Message);
+ Assert.NotNull(deserialized.InnerException);
+ Assert.Equal("inner exception", deserialized.InnerException.Message);
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/Mvc/TagBuilderTest.cs b/test/System.Web.WebPages.Test/Mvc/TagBuilderTest.cs
new file mode 100644
index 00000000..8685bcf3
--- /dev/null
+++ b/test/System.Web.WebPages.Test/Mvc/TagBuilderTest.cs
@@ -0,0 +1,416 @@
+using System.Collections.Generic;
+using System.Web.WebPages.Html;
+using System.Web.WebPages.TestUtils;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.Mvc.Test
+{
+ public class TagBuilderTest
+ {
+ [Fact]
+ public void AddCssClassPrepends()
+ {
+ // Arrange
+ TagBuilder builder = new TagBuilder("SomeTag");
+ builder.MergeAttribute("class", "oldA");
+
+ // Act
+ builder.AddCssClass("newA");
+
+ // Assert
+ Assert.Equal("newA oldA", builder.Attributes["class"]);
+ }
+
+ [Fact]
+ public void AttributesProperty()
+ {
+ // Arrange
+ TagBuilder builder = new TagBuilder("SomeTag");
+
+ // Act
+ SortedDictionary<string, string> attributes = builder.Attributes as SortedDictionary<string, string>;
+
+ // Assert
+ Assert.NotNull(attributes);
+ Assert.Equal(StringComparer.Ordinal, attributes.Comparer);
+ }
+
+ [Fact]
+ public void ConstructorSetsTagNameProperty()
+ {
+ // Arrange
+ TagBuilder builder = new TagBuilder("SomeTag");
+
+ // Act
+ string tagName = builder.TagName;
+
+ // Assert
+ Assert.Equal("SomeTag", tagName);
+ }
+
+ [Fact]
+ public void ConstructorWithEmptyTagNameThrows()
+ {
+ Assert.ThrowsArgumentNullOrEmptyString(
+ delegate { new TagBuilder(String.Empty); }, "tagName");
+ }
+
+ [Fact]
+ public void ConstructorWithNullTagNameThrows()
+ {
+ Assert.ThrowsArgumentNullOrEmptyString(
+ delegate { new TagBuilder(null /* tagName */); }, "tagName");
+ }
+
+ [Fact]
+ public void CreateSanitizedIdThrowsIfInvalidCharReplacementIsNull()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentNull(
+ () => TagBuilder.CreateSanitizedId("tagId", null),
+ "invalidCharReplacement");
+ }
+
+ [Fact]
+ public void CreateSanitizedIdDefaultsToHtmlHelperIdAttributeDotReplacement()
+ {
+ // Arrange
+ String defaultReplacementChar = HtmlHelper.IdAttributeDotReplacement;
+
+ // Act
+ string sanitizedId = TagBuilder.CreateSanitizedId("Hello world");
+
+ // Assert
+ Assert.Equal("Hello" + defaultReplacementChar + "world", sanitizedId);
+ }
+
+ [Fact]
+ public void CreateSanitizedId_ReturnsNullIfOriginalIdBeginsWithNonLetter()
+ {
+ // Act
+ string retVal = TagBuilder.CreateSanitizedId("_DoesNotBeginWithALetter", "!REPL!");
+
+ // Assert
+ Assert.Null(retVal);
+ }
+
+ [Fact]
+ public void CreateSanitizedId_ReturnsNullIfOriginalIdIsEmpty()
+ {
+ // Act
+ string retVal = TagBuilder.CreateSanitizedId("", "!REPL!");
+
+ // Assert
+ Assert.Null(retVal);
+ }
+
+ [Fact]
+ public void CreateSanitizedId_ReturnsNullIfOriginalIdIsNull()
+ {
+ // Act
+ string retVal = TagBuilder.CreateSanitizedId(null, "!REPL!");
+
+ // Assert
+ Assert.Null(retVal);
+ }
+
+ [Fact]
+ public void CreateSanitizedId_ReturnsSanitizedId()
+ {
+ // Arrange
+ string expected = "ABCXYZabcxyz012789!REPL!!REPL!!REPL!!REPL!!REPL!!REPL!!REPL!!REPL!!REPL!!REPL!-!REPL!_!REPL!!REPL!:";
+
+ // Act
+ string retVal = TagBuilder.CreateSanitizedId("ABCXYZabcxyz012789!@#$%^&*()-=_+.:", "!REPL!");
+
+ // Assert
+ Assert.Equal(expected, retVal);
+ }
+
+ [Fact]
+ public void GenerateId_AddsSanitizedId()
+ {
+ // Arrange
+ TagBuilder builder = new TagBuilder("div");
+ builder.IdAttributeDotReplacement = "x";
+
+ // Act
+ builder.GenerateId("Hello, world.");
+
+ // Assert
+ Assert.Equal("Helloxxworldx", builder.Attributes["id"]);
+ }
+
+ [Fact]
+ public void GenerateId_DoesNotAddIdIfIdAlreadyExists()
+ {
+ // Arrange
+ TagBuilder builder = new TagBuilder("div");
+ builder.GenerateId("old");
+
+ // Act
+ builder.GenerateId("new");
+
+ // Assert
+ Assert.Equal("old", builder.Attributes["id"]);
+ }
+
+ [Fact]
+ public void GenerateId_DoesNotAddIdIfSanitizationReturnsNull()
+ {
+ // Arrange
+ TagBuilder builder = new TagBuilder("div");
+
+ // Act
+ builder.GenerateId("");
+
+ // Assert
+ Assert.False(builder.Attributes.ContainsKey("id"));
+ }
+
+ [Fact]
+ public void InnerHtmlProperty()
+ {
+ // Arrange
+ TagBuilder builder = new TagBuilder("SomeTag");
+
+ // Act & Assert
+ Assert.Equal(String.Empty, builder.InnerHtml);
+ builder.InnerHtml = "foo";
+ Assert.Equal("foo", builder.InnerHtml);
+ builder.InnerHtml = null;
+ Assert.Equal(String.Empty, builder.InnerHtml);
+ }
+
+ [Fact]
+ public void MergeAttributeDoesNotOverwriteExistingValuesByDefault()
+ {
+ // Arrange
+ TagBuilder builder = new TagBuilder("SomeTag");
+ builder.MergeAttribute("a", "oldA");
+
+ // Act
+ builder.MergeAttribute("a", "newA");
+
+ // Assert
+ Assert.Equal("oldA", builder.Attributes["a"]);
+ }
+
+ [Fact]
+ public void MergeAttributeOverwritesExistingValueIfAsked()
+ {
+ // Arrange
+ TagBuilder builder = new TagBuilder("SomeTag");
+ builder.MergeAttribute("a", "oldA");
+
+ // Act
+ builder.MergeAttribute("a", "newA", true);
+
+ // Assert
+ Assert.Equal("newA", builder.Attributes["a"]);
+ }
+
+ [Fact]
+ public void MergeAttributeWithEmptyKeyThrows()
+ {
+ // Arrange
+ TagBuilder builder = new TagBuilder("SomeTag");
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmptyString(
+ delegate { builder.MergeAttribute(String.Empty, "value"); }, "key");
+ }
+
+ [Fact]
+ public void MergeAttributeWithNullKeyThrows()
+ {
+ // Arrange
+ TagBuilder builder = new TagBuilder("SomeTag");
+
+ // Act & Assert
+ Assert.ThrowsArgumentNullOrEmptyString(
+ delegate { builder.MergeAttribute(null, "value"); }, "key");
+ }
+
+ [Fact]
+ public void MergeAttributesDoesNotOverwriteExistingValuesByDefault()
+ {
+ // Arrange
+ TagBuilder builder = new TagBuilder("SomeTag");
+ builder.Attributes["a"] = "oldA";
+
+ Dictionary<string, string> newAttrs = new Dictionary<string, string>
+ {
+ { "a", "newA" },
+ { "b", "newB" }
+ };
+
+ // Act
+ builder.MergeAttributes(newAttrs);
+
+ // Assert
+ Assert.Equal(2, builder.Attributes.Count);
+ Assert.Equal("oldA", builder.Attributes["a"]);
+ Assert.Equal("newB", builder.Attributes["b"]);
+ }
+
+ [Fact]
+ public void MergeAttributesOverwritesExistingValueIfAsked()
+ {
+ // Arrange
+ TagBuilder builder = new TagBuilder("SomeTag");
+ builder.Attributes["a"] = "oldA";
+
+ Dictionary<string, string> newAttrs = new Dictionary<string, string>
+ {
+ { "a", "newA" },
+ { "b", "newB" }
+ };
+
+ // Act
+ builder.MergeAttributes(newAttrs, true);
+
+ // Assert
+ Assert.Equal(2, builder.Attributes.Count);
+ Assert.Equal("newA", builder.Attributes["a"]);
+ Assert.Equal("newB", builder.Attributes["b"]);
+ }
+
+ [Fact]
+ public void MergeAttributesWithNullAttributesDoesNothing()
+ {
+ // Arrange
+ TagBuilder builder = new TagBuilder("SomeTag");
+
+ // Act
+ builder.MergeAttributes<string, string>(null);
+
+ // Assert
+ Assert.Equal(0, builder.Attributes.Count);
+ }
+
+ [Fact]
+ public void SetInnerTextEncodes()
+ {
+ // Arrange
+ TagBuilder builder = new TagBuilder("SomeTag");
+
+ // Act
+ builder.SetInnerText("<>");
+
+ // Assert
+ Assert.Equal("&lt;&gt;", builder.InnerHtml);
+ }
+
+ [Fact]
+ public void ToStringDefaultsToNormal()
+ {
+ // Arrange
+ TagBuilder builder = new TagBuilder("SomeTag")
+ {
+ InnerHtml = "<x&y>"
+ };
+ builder.MergeAttributes(GetAttributesDictionary());
+
+ // Act
+ string output = builder.ToString();
+
+ // Assert
+ Assert.Equal(@"<SomeTag a=""Foo"" b=""Bar&amp;Baz"" c=""&lt;&quot;Quux&quot;>""><x&y></SomeTag>", output);
+ }
+
+ [Fact]
+ public void ToStringDoesNotOutputEmptyIdTags()
+ {
+ // Arrange
+ TagBuilder builder = new TagBuilder("SomeTag");
+ builder.Attributes["foo"] = "fooValue";
+ builder.Attributes["bar"] = "barValue";
+ builder.Attributes["id"] = "";
+
+ // Act
+ string output = builder.ToString(TagRenderMode.SelfClosing);
+
+ Assert.Equal(@"<SomeTag bar=""barValue"" foo=""fooValue"" />", output);
+ }
+
+ [Fact]
+ public void ToStringEndTag()
+ {
+ // Arrange
+ TagBuilder builder = new TagBuilder("SomeTag")
+ {
+ InnerHtml = "<x&y>"
+ };
+ builder.MergeAttributes(GetAttributesDictionary());
+
+ // Act
+ string output = builder.ToString(TagRenderMode.EndTag);
+
+ // Assert
+ Assert.Equal(@"</SomeTag>", output);
+ }
+
+ [Fact]
+ public void ToStringNormal()
+ {
+ // Arrange
+ TagBuilder builder = new TagBuilder("SomeTag")
+ {
+ InnerHtml = "<x&y>"
+ };
+ builder.MergeAttributes(GetAttributesDictionary());
+
+ // Act
+ string output = builder.ToString(TagRenderMode.Normal);
+
+ // Assert
+ Assert.Equal(@"<SomeTag a=""Foo"" b=""Bar&amp;Baz"" c=""&lt;&quot;Quux&quot;>""><x&y></SomeTag>", output);
+ }
+
+ [Fact]
+ public void ToStringSelfClosing()
+ {
+ // Arrange
+ TagBuilder builder = new TagBuilder("SomeTag")
+ {
+ InnerHtml = "<x&y>"
+ };
+ builder.MergeAttributes(GetAttributesDictionary());
+
+ // Act
+ string output = builder.ToString(TagRenderMode.SelfClosing);
+
+ // Assert
+ Assert.Equal(@"<SomeTag a=""Foo"" b=""Bar&amp;Baz"" c=""&lt;&quot;Quux&quot;>"" />", output);
+ }
+
+ [Fact]
+ public void ToStringStartTag()
+ {
+ // Arrange
+ TagBuilder builder = new TagBuilder("SomeTag")
+ {
+ InnerHtml = "<x&y>"
+ };
+ builder.MergeAttributes(GetAttributesDictionary());
+
+ // Act
+ string output = builder.ToString(TagRenderMode.StartTag);
+
+ // Assert
+ Assert.Equal(@"<SomeTag a=""Foo"" b=""Bar&amp;Baz"" c=""&lt;&quot;Quux&quot;>"">", output);
+ }
+
+ private static IDictionary<string, string> GetAttributesDictionary()
+ {
+ return new SortedDictionary<string, string>
+ {
+ { "a", "Foo" },
+ { "b", "Bar&Baz" },
+ { "c", @"<""Quux"">" }
+ };
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/PreApplicationStartCodeTest.cs b/test/System.Web.WebPages.Test/PreApplicationStartCodeTest.cs
new file mode 100644
index 00000000..bca495a0
--- /dev/null
+++ b/test/System.Web.WebPages.Test/PreApplicationStartCodeTest.cs
@@ -0,0 +1,38 @@
+using System.Reflection;
+using System.Web.Routing;
+using System.Web.Security;
+using System.Web.UI;
+using System.Web.WebPages.TestUtils;
+using Xunit;
+
+namespace System.Web.WebPages.Test
+{
+ public class PreApplicationStartCodeTest
+ {
+ [Fact]
+ public void StartTest()
+ {
+ AppDomainUtils.RunInSeparateAppDomain(() =>
+ {
+ AppDomainUtils.SetPreAppStartStage();
+ PreApplicationStartCode.Start();
+ // Call a second time to ensure multiple calls do not cause issues
+ PreApplicationStartCode.Start();
+
+ Assert.False(RouteTable.Routes.RouteExistingFiles, "We should not be setting RouteExistingFiles");
+ Assert.Empty(RouteTable.Routes);
+
+ Assert.False(PageParser.EnableLongStringsAsResources);
+
+ string formsAuthLoginUrl = (string)typeof(FormsAuthentication).GetField("_LoginUrl", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null);
+ Assert.Null(formsAuthLoginUrl);
+ });
+ }
+
+ [Fact]
+ public void TestPreAppStartClass()
+ {
+ PreAppStartTestHelper.TestPreAppStartClass(typeof(PreApplicationStartCode));
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/Properties/AssemblyInfo.cs b/test/System.Web.WebPages.Test/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..72db8000
--- /dev/null
+++ b/test/System.Web.WebPages.Test/Properties/AssemblyInfo.cs
@@ -0,0 +1,34 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+
+[assembly: AssemblyTitle("Microsoft.WebPages.Test")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("MSIT")]
+[assembly: AssemblyProduct("Microsoft.WebPages.Test")]
+[assembly: AssemblyCopyright("Copyright © MSIT 2010")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+
+[assembly: ComVisible(false)]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/test/System.Web.WebPages.Test/ScopeStorage/AspNetRequestScopeStorageProviderTest.cs b/test/System.Web.WebPages.Test/ScopeStorage/AspNetRequestScopeStorageProviderTest.cs
new file mode 100644
index 00000000..75ca789e
--- /dev/null
+++ b/test/System.Web.WebPages.Test/ScopeStorage/AspNetRequestScopeStorageProviderTest.cs
@@ -0,0 +1,93 @@
+using System.Collections.Generic;
+using System.Web.WebPages.Scope;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.WebPages.Test
+{
+ public class AspNetRequestStorageProvider
+ {
+ [Fact]
+ public void AspNetStorageProviderReturnsApplicationStateBeforeAppStart()
+ {
+ // Arrange
+ var provider = GetProvider(() => false);
+
+ // Act and Assert
+ Assert.NotNull(provider.ApplicationScope);
+ Assert.NotNull(provider.GlobalScope);
+ Assert.Equal(provider.ApplicationScope, provider.GlobalScope);
+ }
+
+ [Fact]
+ public void AspNetStorageProviderThrowsWhenAccessingRequestScopeBeforeAppStart()
+ {
+ // Arrange
+ var provider = GetProvider(() => false);
+
+ // Act and Assert
+ Assert.Throws<InvalidOperationException>(
+ () => { var x = provider.RequestScope; },
+ "RequestScope cannot be created when _AppStart is executing.");
+ }
+
+ [Fact]
+ public void AspNetStorageProviderThrowsWhenAssigningScopeBeforeAppStart()
+ {
+ // Arrange
+ var provider = GetProvider(() => false);
+
+ // Act and Assert
+ Assert.Throws<InvalidOperationException>(
+ () => { provider.CurrentScope = new ScopeStorageDictionary(); },
+ "Storage scopes cannot be created when _AppStart is executing.");
+ }
+
+ [Fact]
+ public void AspNetStorageProviderReturnsRequestScopeAfterAppStart()
+ {
+ // Arrange
+ var provider = GetProvider();
+
+ // Act and Assert
+ Assert.NotNull(provider.RequestScope);
+ Assert.Equal(provider.RequestScope, provider.CurrentScope);
+ }
+
+ [Fact]
+ public void AspNetStorageRetrievesRequestScopeAfterSettingAnonymousScopes()
+ {
+ // Arrange
+ var provider = GetProvider();
+
+ // Act
+ var requestScope = provider.RequestScope;
+
+ var Scope = new ScopeStorageDictionary();
+ provider.CurrentScope = Scope;
+
+ Assert.Equal(provider.CurrentScope, Scope);
+ Assert.Equal(provider.RequestScope, requestScope);
+ }
+
+ [Fact]
+ public void AspNetStorageUsesApplicationScopeAsGlobalScope()
+ {
+ // Arrange
+ var provider = GetProvider();
+
+ // Act and Assert
+ Assert.Equal(provider.GlobalScope, provider.ApplicationScope);
+ }
+
+ private AspNetRequestScopeStorageProvider GetProvider(Func<bool> appStartExecuted = null)
+ {
+ Mock<HttpContextBase> context = new Mock<HttpContextBase>();
+ context.Setup(c => c.Items).Returns(new Dictionary<object, object>());
+ appStartExecuted = appStartExecuted ?? (() => true);
+
+ return new AspNetRequestScopeStorageProvider(context.Object, appStartExecuted);
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/ScopeStorage/ScopeStorageDictionaryTest.cs b/test/System.Web.WebPages.Test/ScopeStorage/ScopeStorageDictionaryTest.cs
new file mode 100644
index 00000000..5f832d07
--- /dev/null
+++ b/test/System.Web.WebPages.Test/ScopeStorage/ScopeStorageDictionaryTest.cs
@@ -0,0 +1,170 @@
+using System.Collections.Generic;
+using System.Web.WebPages.Scope;
+using Xunit;
+
+namespace System.Web.WebPages.Test
+{
+ public class ScopeStorageDictionaryTest
+ {
+ [Fact]
+ public void ScopeStorageDictionaryLooksUpLocalValuesFirst()
+ {
+ // Arrange
+ var stateStorage = GetChainedStorageStateDictionary();
+
+ // Act and Assert
+ Assert.Equal(stateStorage["f"], "f2");
+ }
+
+ [Fact]
+ public void ScopeStorageDictionaryOverridesParentValuesWithLocalValues()
+ {
+ // Arrange
+ var stateStorage = GetChainedStorageStateDictionary();
+
+ // Act and Assert
+ Assert.Equal(stateStorage["a"], "a2");
+ Assert.Equal(stateStorage["d"], "d2");
+ }
+
+ [Fact]
+ public void ScopeStorageDictionaryLooksUpParentValuesWhenNotFoundLocally()
+ {
+ // Arrange
+ var stateStorage = GetChainedStorageStateDictionary();
+
+ // Act and Assert
+ Assert.Equal(stateStorage["c"], "c0");
+ Assert.Equal(stateStorage["b"], "b1");
+ }
+
+ [Fact]
+ public void ScopeStorageDictionaryTreatsNullAsOrdinaryValues()
+ {
+ // Arrange
+ var stateStorage = GetChainedStorageStateDictionary();
+ stateStorage["b"] = null;
+
+ // Act and Assert
+ Assert.Null(stateStorage["b"]);
+ }
+
+ [Fact]
+ public void ContainsKeyReturnsTrueIfItContainsKey()
+ {
+ // Arrange
+ var scopeStorage = GetChainedStorageStateDictionary();
+
+ // Act and Assert
+ Assert.True(scopeStorage.ContainsKey("f"));
+ }
+
+ [Fact]
+ public void ContainsKeyReturnsTrueIfBaseContainsKey()
+ {
+ // Arrange
+ var scopeStorage = GetChainedStorageStateDictionary();
+
+ // Act and Assert
+ Assert.True(scopeStorage.ContainsKey("e"));
+ }
+
+ [Fact]
+ public void ContainsKeyReturnsFalseIfItDoesNotContainKeyAndBaseIsNull()
+ {
+ // Arrange
+ var scopeStorage = new ScopeStorageDictionary() { { "foo", "bar" } };
+
+ // Act and Assert
+ Assert.False(scopeStorage.ContainsKey("baz"));
+ }
+
+ [Fact]
+ public void CountReturnsCountFromCurrentAndBaseScope()
+ {
+ // Arrange
+ var scopeStorage = GetChainedStorageStateDictionary();
+
+ // Act and Assert
+ Assert.Equal(6, scopeStorage.Count);
+ }
+
+ [Fact]
+ public void ScopeStorageDictionaryGetsValuesFromCurrentAndBaseScope()
+ {
+ // Arrange
+ var scopeStorage = GetChainedStorageStateDictionary();
+
+ // Act and Assert
+ Assert.Equal(scopeStorage["a"], "a2");
+ Assert.Equal(scopeStorage["b"], "b1");
+ Assert.Equal(scopeStorage["c"], "c0");
+ Assert.Equal(scopeStorage["d"], "d2");
+ Assert.Equal(scopeStorage["e"], "e1");
+ Assert.Equal(scopeStorage["f"], "f2");
+ }
+
+ [Fact]
+ public void ClearRemovesAllItemsFromCurrentScope()
+ {
+ // Arrange
+ var dictionary = new ScopeStorageDictionary { { "foo", "bar" }, { "foo2", "bar2" } };
+
+ // Act
+ dictionary.Clear();
+
+ // Assert
+ Assert.Equal(0, dictionary.Count);
+ }
+
+ [Fact]
+ public void ScopeStorageDictionaryIsNotReadOnly()
+ {
+ // Arrange
+ var dictionary = new ScopeStorageDictionary();
+
+ // Act and Assert
+ Assert.False(dictionary.IsReadOnly);
+ }
+
+ [Fact]
+ public void CopyToCopiesItemsToArrayAtSpecifiedIndex()
+ {
+ // Arrange
+ var dictionary = GetChainedStorageStateDictionary();
+ var array = new KeyValuePair<object, object>[8];
+
+ // Act
+ dictionary.CopyTo(array, 2);
+
+ // Assert
+ Assert.Equal(array[2].Key, "a");
+ Assert.Equal(array[2].Value, "a2");
+ Assert.Equal(array[4].Key, "f");
+ Assert.Equal(array[4].Value, "f2");
+ Assert.Equal(array[7].Key, "c");
+ Assert.Equal(array[7].Value, "c0");
+ }
+
+ private ScopeStorageDictionary GetChainedStorageStateDictionary()
+ {
+ var root = new ScopeStorageDictionary();
+ root["a"] = "a0";
+ root["b"] = "b0";
+ root["c"] = "c0";
+
+ var firstGen = new ScopeStorageDictionary(baseScope: root);
+ firstGen["a"] = "a1";
+ firstGen["b"] = "b1";
+ firstGen["d"] = "d1";
+ firstGen["e"] = "e1";
+
+ var secondGen = new ScopeStorageDictionary(baseScope: firstGen);
+ secondGen["a"] = "a2";
+ secondGen["d"] = "d2";
+ secondGen["f"] = "f2";
+
+ return secondGen;
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/ScopeStorage/ScopeStorageKeyComparerTest.cs b/test/System.Web.WebPages.Test/ScopeStorage/ScopeStorageKeyComparerTest.cs
new file mode 100644
index 00000000..a30ae2be
--- /dev/null
+++ b/test/System.Web.WebPages.Test/ScopeStorage/ScopeStorageKeyComparerTest.cs
@@ -0,0 +1,48 @@
+using System.Collections.Generic;
+using System.Web.WebPages.Scope;
+using Xunit;
+
+namespace System.Web.WebPages.Test
+{
+ public class ScopeStorageKeyComparerTest
+ {
+ [Fact]
+ public void ScopeStorageComparerPerformsCaseInsensitiveOrdinalComparisonForStrings()
+ {
+ // Arrange
+ var dictionary = new Dictionary<object, object>(ScopeStorageComparer.Instance) { { "foo", "bar" } };
+
+ // Act and Assert
+ Assert.Equal(dictionary["foo"], "bar");
+ Assert.Equal(dictionary["foo"], dictionary["FOo"]);
+ }
+
+ [Fact]
+ public void ScopeStorageComparerPerformsRegularComparisonForOtherTypes()
+ {
+ // Arrange
+ var stateStorage = new Dictionary<object, object> { { 4, "4-value" }, { new Person { ID = 10 }, "person-value" } };
+
+ // Act and Assert
+ Assert.Equal(stateStorage[4], "4-value");
+ Assert.Equal(stateStorage[(int)8 / 2], stateStorage[4]);
+ Assert.Equal(stateStorage[new Person { ID = 10 }], "person-value");
+ }
+
+ private class Person
+ {
+ public int ID { get; set; }
+
+ public override bool Equals(object o)
+ {
+ var other = o as Person;
+ return (other != null) && (other.ID == ID);
+ }
+
+ public override int GetHashCode()
+ {
+ return ID;
+ }
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/ScopeStorage/WebConfigScopeStorageTest.cs b/test/System.Web.WebPages.Test/ScopeStorage/WebConfigScopeStorageTest.cs
new file mode 100644
index 00000000..e99002fa
--- /dev/null
+++ b/test/System.Web.WebPages.Test/ScopeStorage/WebConfigScopeStorageTest.cs
@@ -0,0 +1,78 @@
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Linq;
+using System.Web.WebPages.Scope;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.WebPages.Test
+{
+ public class WebConfigScopeStorageTest
+ {
+ [Fact]
+ public void WebConfigScopeStorageReturnsConfigValue()
+ {
+ // Arrange
+ var stateStorage = GetWebConfigScopeStorage();
+
+ // Assert
+ Assert.Equal(stateStorage["foo1"], "bar1");
+ Assert.Equal(stateStorage["foo2"], "bar2");
+ }
+
+ [Fact]
+ public void WebConfigScopeStoragePerformsCaseInsensitiveKeyCompares()
+ {
+ // Arrange
+ var stateStorage = GetWebConfigScopeStorage();
+
+ // Assert
+ Assert.Equal(stateStorage["FOO1"], "bar1");
+ Assert.Equal(stateStorage["FoO2"], "bar2");
+ }
+
+ [Fact]
+ public void WebConfigScopeStorageThrowsWhenWriting()
+ {
+ // Arrange
+ var stateStorage = GetWebConfigScopeStorage();
+
+ // Act and Assert
+ Assert.Throws<NotSupportedException>(() => stateStorage["foo"] = "some value", "Storage scope is read only.");
+ Assert.Throws<NotSupportedException>(() => stateStorage.Add("foo", "value"), "Storage scope is read only.");
+ Assert.Throws<NotSupportedException>(() => stateStorage.Remove("foo"), "Storage scope is read only.");
+ Assert.Throws<NotSupportedException>(() => stateStorage.Clear(), "Storage scope is read only.");
+ Assert.Throws<NotSupportedException>(() => stateStorage.Remove(new KeyValuePair<object, object>("foo", "bar")), "Storage scope is read only.");
+ }
+
+ [Fact]
+ public void WebConfigStateAllowsEnumeratingOverConfigItems()
+ {
+ // Arrange
+ var dictionary = new Dictionary<string, string> { { "a", "b" }, { "c", "d" }, { "x12", "y34" } };
+ var stateStorage = GetWebConfigScopeStorage(dictionary);
+
+ // Act and Assert
+ Assert.True(dictionary.All(item => item.Value == stateStorage[item.Key] as string));
+ }
+
+ private WebConfigScopeDictionary GetWebConfigScopeStorage(IDictionary<string, string> values = null)
+ {
+ NameValueCollection collection = new NameValueCollection();
+ if (values == null)
+ {
+ collection.Add("foo1", "bar1");
+ collection.Add("foo2", "bar2");
+ }
+ else
+ {
+ foreach (var item in values)
+ {
+ collection.Add(item.Key, item.Value);
+ }
+ }
+
+ return new WebConfigScopeDictionary(collection);
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/System.Web.WebPages.Test.csproj b/test/System.Web.WebPages.Test/System.Web.WebPages.Test.csproj
new file mode 100644
index 00000000..6ad4e51d
--- /dev/null
+++ b/test/System.Web.WebPages.Test/System.Web.WebPages.Test.csproj
@@ -0,0 +1,162 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProductVersion>9.0.30729</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{0F4870DB-A799-4DBA-99DF-0D74BB52FEC2}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>System.Web.WebPages.Test</RootNamespace>
+ <AssemblyName>System.Web.WebPages.Test</AssemblyName>
+ <FileAlignment>512</FileAlignment>
+ <ProjectTypeGuids>{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Debug\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Release\Test\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'CodeCoverage' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\CodeCoverage\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="Microsoft.CSharp" />
+ <Reference Include="Moq, Version=4.0.10827.0, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL">
+ <HintPath>..\..\packages\Moq.4.0.10827\lib\NET40\Moq.dll</HintPath>
+ </Reference>
+ <Reference Include="System" />
+ <Reference Include="System.ComponentModel.DataAnnotations" />
+ <Reference Include="System.Core">
+ <RequiredTargetFramework>3.5</RequiredTargetFramework>
+ </Reference>
+ <Reference Include="System.Data.Linq" />
+ <Reference Include="System.Web" />
+ <Reference Include="xunit">
+ <HintPath>..\..\packages\xunit.1.9.0.1566\lib\xunit.dll</HintPath>
+ </Reference>
+ <Reference Include="xunit.extensions">
+ <HintPath>..\..\packages\xunit.extensions.1.9.0.1566\lib\xunit.extensions.dll</HintPath>
+ </Reference>
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="ApplicationParts\ApplicationPartTest.cs" />
+ <Compile Include="ApplicationParts\ApplicationPartRegistryTest.cs" />
+ <Compile Include="ApplicationParts\MimeMappingTest.cs" />
+ <Compile Include="ApplicationParts\ResourceHandlerTest.cs" />
+ <Compile Include="ApplicationParts\TestResourceAssembly.cs" />
+ <Compile Include="Utils\SessionStateUtilTest.cs" />
+ <Compile Include="WebPage\BrowserHelpersTest.cs" />
+ <Compile Include="WebPage\BrowserOverrideStoresTest.cs" />
+ <Compile Include="WebPage\CookieBrowserOverrideStoreTest.cs" />
+ <Compile Include="WebPage\DefaultDisplayModeTest.cs" />
+ <Compile Include="WebPage\DisplayInfoTest.cs" />
+ <Compile Include="WebPage\DisplayModeProviderTest.cs" />
+ <Compile Include="Extensions\HttpContextExtensionsTest.cs" />
+ <Compile Include="Extensions\HttpRequestExtensionsTest.cs" />
+ <Compile Include="Extensions\StringExtensionsTest.cs" />
+ <Compile Include="Extensions\HttpResponseExtensionsTest.cs" />
+ <Compile Include="Helpers\AntiForgeryDataSerializerTest.cs" />
+ <Compile Include="Helpers\AntiForgeryDataTest.cs" />
+ <Compile Include="Helpers\AntiForgeryTest.cs" />
+ <Compile Include="Helpers\AntiForgeryWorkerTest.cs" />
+ <Compile Include="Helpers\UnvalidatedRequestValuesTest.cs" />
+ <Compile Include="Html\CheckBoxTest.cs" />
+ <Compile Include="Html\HtmlHelperFactory.cs" />
+ <Compile Include="Html\HtmlHelperTest.cs" />
+ <Compile Include="Html\InputHelperTest.cs" />
+ <Compile Include="Html\RadioButtonTest.cs" />
+ <Compile Include="Html\SelectHelperTest.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="Html\TextAreaHelperTest.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="Html\ValidationHelperTest.cs" />
+ <Compile Include="Instrumentation\InstrumentationServiceTest.cs" />
+ <Compile Include="Mvc\HttpAntiForgeryExceptionTest.cs" />
+ <Compile Include="Mvc\TagBuilderTest.cs" />
+ <Compile Include="PreApplicationStartCodeTest.cs" />
+ <Compile Include="ScopeStorage\AspNetRequestScopeStorageProviderTest.cs" />
+ <Compile Include="ScopeStorage\ScopeStorageDictionaryTest.cs" />
+ <Compile Include="ScopeStorage\ScopeStorageKeyComparerTest.cs" />
+ <Compile Include="ScopeStorage\WebConfigScopeStorageTest.cs" />
+ <Compile Include="Utils\CultureUtilTest.cs" />
+ <Compile Include="Utils\PathUtilTest.cs" />
+ <Compile Include="Utils\TestObjectFactory.cs" />
+ <Compile Include="Utils\TypeHelperTest.cs" />
+ <Compile Include="Utils\UrlUtilTest.cs" />
+ <Compile Include="Validation\ValidationHelperTest.cs" />
+ <Compile Include="Validation\ValidatorTest.cs" />
+ <Compile Include="WebPage\ApplicationStartPageTest.cs" />
+ <Compile Include="WebPage\DynamicHttpApplicationStateTest.cs" />
+ <Compile Include="WebPage\DynamicPageDataDictionaryTest.cs" />
+ <Compile Include="WebPage\FileExistenceCacheTest.cs" />
+ <Compile Include="WebPage\RequestBrowserOverrideStoreTest.cs" />
+ <Compile Include="WebPage\RequestResourceTrackerTest.cs" />
+ <Compile Include="WebPage\TemplateStackTest.cs" />
+ <Compile Include="WebPage\BuildManagerWrapperTest.cs" />
+ <Compile Include="WebPage\VirtualPathFactoryExtensionsTest.cs" />
+ <Compile Include="WebPage\VirtualPathFactoryManagerTest.cs" />
+ <Compile Include="WebPage\WebPageContextTest.cs" />
+ <Compile Include="WebPage\WebPageExecutingBaseTest.cs" />
+ <Compile Include="WebPage\WebPageHttpModuleTest.cs" />
+ <Compile Include="WebPage\WebPageHttpHandlerTest.cs" />
+ <Compile Include="WebPage\UrlDataTest.cs" />
+ <Compile Include="WebPage\StartPageTest.cs" />
+ <Compile Include="WebPage\Utils.cs" />
+ <Compile Include="WebPage\PageDataDictionaryTest.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="WebPage\WebPageRenderingBaseTest.cs" />
+ <Compile Include="WebPage\WebPageRouteTest.cs" />
+ <Compile Include="WebPage\RenderPageTest.cs" />
+ <Compile Include="WebPage\BuildManagerExceptionUtilTest.cs" />
+ <Compile Include="WebPage\LayoutTest.cs" />
+ <Compile Include="WebPage\WebPageTest.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\System.Web.Razor\System.Web.Razor.csproj">
+ <Project>{8F18041B-9410-4C36-A9C5-067813DF5F31}</Project>
+ <Name>System.Web.Razor</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\src\System.Web.WebPages\System.Web.WebPages.csproj">
+ <Project>{76EFA9C5-8D7E-4FDF-B710-E20F8B6B00D2}</Project>
+ <Name>System.Web.WebPages</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\Microsoft.TestCommon\Microsoft.TestCommon.csproj">
+ <Project>{FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0}</Project>
+ <Name>Microsoft.TestCommon</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="App.config" />
+ <None Include="packages.config" />
+ <None Include="TestFiles\Deployed\Bar" />
+ <None Include="TestFiles\Deployed\Bar.foohtml" />
+ </ItemGroup>
+ <ItemGroup>
+ <Content Include="TestFiles\Deployed\Bar.cshtml" />
+ </ItemGroup>
+ <ItemGroup />
+ <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+</Project> \ No newline at end of file
diff --git a/test/System.Web.WebPages.Test/TestFiles/Deployed/Bar b/test/System.Web.WebPages.Test/TestFiles/Deployed/Bar
new file mode 100644
index 00000000..5f282702
--- /dev/null
+++ b/test/System.Web.WebPages.Test/TestFiles/Deployed/Bar
@@ -0,0 +1 @@
+ \ No newline at end of file
diff --git a/test/System.Web.WebPages.Test/TestFiles/Deployed/Bar.cshtml b/test/System.Web.WebPages.Test/TestFiles/Deployed/Bar.cshtml
new file mode 100644
index 00000000..5f282702
--- /dev/null
+++ b/test/System.Web.WebPages.Test/TestFiles/Deployed/Bar.cshtml
@@ -0,0 +1 @@
+ \ No newline at end of file
diff --git a/test/System.Web.WebPages.Test/TestFiles/Deployed/Bar.foohtml b/test/System.Web.WebPages.Test/TestFiles/Deployed/Bar.foohtml
new file mode 100644
index 00000000..5f282702
--- /dev/null
+++ b/test/System.Web.WebPages.Test/TestFiles/Deployed/Bar.foohtml
@@ -0,0 +1 @@
+ \ No newline at end of file
diff --git a/test/System.Web.WebPages.Test/Utils/CultureUtilTest.cs b/test/System.Web.WebPages.Test/Utils/CultureUtilTest.cs
new file mode 100644
index 00000000..628c0b21
--- /dev/null
+++ b/test/System.Web.WebPages.Test/Utils/CultureUtilTest.cs
@@ -0,0 +1,256 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Threading;
+using Moq;
+using Xunit;
+
+namespace System.Web.WebPages.Test
+{
+ public class CultureUtilTest
+ {
+ [Fact]
+ public void SetAutoCultureWithNoUserLanguagesDoesNothing()
+ {
+ // Arrange
+ var context = GetContextForSetCulture(null);
+ Thread thread = GetThread();
+ CultureInfo culture = thread.CurrentCulture;
+
+ // Act
+ CultureUtil.SetCulture(thread, context, "auto");
+
+ // Assert
+ Assert.Equal(culture, thread.CurrentCulture);
+ }
+
+ [Fact]
+ public void SetAutoUICultureWithNoUserLanguagesDoesNothing()
+ {
+ // Arrange
+ var context = GetContextForSetCulture(null);
+ Thread thread = GetThread();
+ CultureInfo culture = thread.CurrentUICulture;
+
+ // Act
+ CultureUtil.SetUICulture(thread, context, "auto");
+
+ // Assert
+ Assert.Equal(culture, thread.CurrentUICulture);
+ }
+
+ [Fact]
+ public void SetAutoCultureWithEmptyUserLanguagesDoesNothing()
+ {
+ // Arrange
+ var context = GetContextForSetCulture(Enumerable.Empty<string>());
+ Thread thread = GetThread();
+ CultureInfo culture = thread.CurrentCulture;
+
+ // Act
+ CultureUtil.SetCulture(thread, context, "auto");
+
+ // Assert
+ Assert.Equal(culture, thread.CurrentCulture);
+ }
+
+ [Fact]
+ public void SetAutoUICultureWithEmptyUserLanguagesDoesNothing()
+ {
+ // Arrange
+ var context = GetContextForSetCulture(Enumerable.Empty<string>());
+ Thread thread = GetThread();
+ CultureInfo culture = thread.CurrentUICulture;
+
+ // Act
+ CultureUtil.SetUICulture(thread, context, "auto");
+
+ // Assert
+ Assert.Equal(culture, thread.CurrentUICulture);
+ }
+
+ [Fact]
+ public void SetAutoCultureWithBlankUserLanguagesDoesNothing()
+ {
+ // Arrange
+ var context = GetContextForSetCulture(new[] { " " });
+ Thread thread = GetThread();
+ CultureInfo culture = thread.CurrentCulture;
+
+ // Act
+ CultureUtil.SetCulture(thread, context, "auto");
+
+ // Assert
+ Assert.Equal(culture, thread.CurrentCulture);
+ }
+
+ [Fact]
+ public void SetAutoUICultureWithBlankUserLanguagesDoesNothing()
+ {
+ // Arrange
+ var context = GetContextForSetCulture(new[] { " " });
+ Thread thread = GetThread();
+ CultureInfo culture = thread.CurrentUICulture;
+
+ // Act
+ CultureUtil.SetUICulture(thread, context, "auto");
+
+ // Assert
+ Assert.Equal(culture, thread.CurrentUICulture);
+ }
+
+ [Fact]
+ public void SetAutoCultureWithInvalidLanguageDoesNothing()
+ {
+ // Arrange
+ var context = GetContextForSetCulture(new[] { "aa-AA", "bb-BB", "cc-CC" });
+ Thread thread = GetThread();
+ CultureInfo culture = thread.CurrentCulture;
+
+ // Act
+ CultureUtil.SetCulture(thread, context, "auto");
+
+ // Assert
+ Assert.Equal(culture, thread.CurrentCulture);
+ }
+
+ [Fact]
+ public void SetAutoUICultureWithInvalidLanguageDoesNothing()
+ {
+ // Arrange
+ var context = GetContextForSetCulture(new[] { "aa-AA", "bb-BB", "cc-CC" });
+ Thread thread = GetThread();
+ CultureInfo culture = thread.CurrentUICulture;
+
+ // Act
+ CultureUtil.SetUICulture(thread, context, "auto");
+
+ // Assert
+ Assert.Equal(culture, thread.CurrentUICulture);
+ }
+
+ [Fact]
+ public void SetAutoCultureDetectsUserLanguageCulture()
+ {
+ // Arrange
+ var context = GetContextForSetCulture(new[] { "en-GB", "en-US", "ar-eg" });
+ Thread thread = GetThread();
+
+ // Act
+ CultureUtil.SetCulture(thread, context, "auto");
+
+ // Assert
+ Assert.Equal(CultureInfo.GetCultureInfo("en-GB"), thread.CurrentCulture);
+ Assert.Equal("05/01/1979", new DateTime(1979, 1, 5).ToString("d", thread.CurrentCulture));
+ }
+
+ [Fact]
+ public void SetAutoUICultureDetectsUserLanguageCulture()
+ {
+ // Arrange
+ var context = GetContextForSetCulture(new[] { "en-GB", "en-US", "ar-eg" });
+ Thread thread = GetThread();
+
+ // Act
+ CultureUtil.SetUICulture(thread, context, "auto");
+
+ // Assert
+ Assert.Equal(CultureInfo.GetCultureInfo("en-GB"), thread.CurrentUICulture);
+ Assert.Equal("05/01/1979", new DateTime(1979, 1, 5).ToString("d", thread.CurrentUICulture));
+ }
+
+ [Fact]
+ public void SetAutoCultureUserLanguageWithQParameterCulture()
+ {
+ // Arrange
+ var context = GetContextForSetCulture(new[] { "en-GB;q=0.3", "en-US", "ar-eg;q=0.5" });
+ Thread thread = GetThread();
+
+ // Act
+ CultureUtil.SetCulture(thread, context, "auto");
+
+ // Assert
+ Assert.Equal(CultureInfo.GetCultureInfo("en-GB"), thread.CurrentCulture);
+ Assert.Equal("05/01/1979", new DateTime(1979, 1, 5).ToString("d", thread.CurrentCulture));
+ }
+
+ [Fact]
+ public void SetAutoUICultureDetectsUserLanguageWithQParameterCulture()
+ {
+ // Arrange
+ var context = GetContextForSetCulture(new[] { "en-GB;q=0.3", "en-US", "ar-eg;q=0.5" });
+ Thread thread = GetThread();
+
+ // Act
+ CultureUtil.SetUICulture(thread, context, "auto");
+
+ // Assert
+ Assert.Equal(CultureInfo.GetCultureInfo("en-GB"), thread.CurrentUICulture);
+ Assert.Equal("05/01/1979", new DateTime(1979, 1, 5).ToString("d", thread.CurrentUICulture));
+ }
+
+ [Fact]
+ public void SetCultureWithInvalidCultureThrows()
+ {
+ // Arrange
+ var context = GetContextForSetCulture();
+ Thread thread = GetThread();
+
+ // Act and Assert
+ Assert.Throws<CultureNotFoundException>(() => CultureUtil.SetCulture(thread, context, "sans-culture"));
+ }
+
+ [Fact]
+ public void SetUICultureWithInvalidCultureThrows()
+ {
+ // Arrange
+ var context = GetContextForSetCulture();
+ Thread thread = GetThread();
+
+ // Act and Assert
+ Assert.Throws<CultureNotFoundException>(() => CultureUtil.SetUICulture(thread, context, "sans-culture"));
+ }
+
+ [Fact]
+ public void SetCultureWithValidCulture()
+ {
+ // Arrange
+ var context = GetContextForSetCulture();
+ Thread thread = GetThread();
+
+ // Act
+ CultureUtil.SetCulture(thread, context, "en-GB");
+
+ // Assert
+ Assert.Equal(CultureInfo.GetCultureInfo("en-GB"), thread.CurrentCulture);
+ Assert.Equal("05/01/1979", new DateTime(1979, 1, 5).ToString("d", thread.CurrentCulture));
+ }
+
+ [Fact]
+ public void SetUICultureWithValidCulture()
+ {
+ // Arrange
+ var context = GetContextForSetCulture();
+ Thread thread = GetThread();
+
+ // Act
+ CultureUtil.SetUICulture(thread, context, "en-GB");
+
+ // Assert
+ Assert.Equal(CultureInfo.GetCultureInfo("en-GB"), thread.CurrentUICulture);
+ Assert.Equal("05/01/1979", new DateTime(1979, 1, 5).ToString("d", thread.CurrentUICulture));
+ }
+
+ private static Thread GetThread()
+ {
+ return new Thread(() => { });
+ }
+
+ private static HttpContextBase GetContextForSetCulture(IEnumerable<string> userLanguages = null)
+ {
+ Mock<HttpContextBase> contextMock = new Mock<HttpContextBase>();
+ contextMock.Setup(context => context.Request.UserLanguages).Returns(userLanguages == null ? null : userLanguages.ToArray());
+ return contextMock.Object;
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/Utils/PathUtilTest.cs b/test/System.Web.WebPages.Test/Utils/PathUtilTest.cs
new file mode 100644
index 00000000..2911b748
--- /dev/null
+++ b/test/System.Web.WebPages.Test/Utils/PathUtilTest.cs
@@ -0,0 +1,177 @@
+using System.Linq;
+using System.Web.WebPages.TestUtils;
+using Microsoft.Internal.Web.Utils;
+using Xunit;
+
+namespace System.Web.WebPages.Test
+{
+ public class PathUtilTest
+ {
+ [Fact]
+ public void IsSimpleNameTest()
+ {
+ Assert.True(PathUtil.IsSimpleName("Test.cshtml"));
+ Assert.True(PathUtil.IsSimpleName("Test.Hello.cshtml"));
+ Assert.False(PathUtil.IsSimpleName("~/myapp/Test/Hello.cshtml"));
+ Assert.False(PathUtil.IsSimpleName("../Test/Hello.cshtml"));
+ Assert.False(PathUtil.IsSimpleName("../../Test/Hello.cshtml"));
+ Assert.False(PathUtil.IsSimpleName("/Test/Hello.cshtml"));
+ }
+
+ [Fact]
+ public void GetExtensionForNullPathsReturnsNull()
+ {
+ // Arrange
+ string path = null;
+
+ // Act
+ string extension = PathUtil.GetExtension(path);
+
+ // Assert
+ Assert.Null(extension);
+ }
+
+ [Fact]
+ public void GetExtensionForEmptyPathsReturnsEmptyString()
+ {
+ // Arrange
+ string path = String.Empty;
+
+ // Act
+ string extension = PathUtil.GetExtension(path);
+
+ // Assert
+ Assert.Equal(0, extension.Length);
+ }
+
+ [Fact]
+ public void GetExtensionReturnsEmptyStringForPathsThatDoNotContainExtension()
+ {
+ // Arrange
+ string[] paths = new[] { "SomePath", "SomePath/", "SomePath/MorePath", "SomePath/MorePath/" };
+
+ // Act
+ var extensions = paths.Select(PathUtil.GetExtension);
+
+ // Assert
+ Assert.True(extensions.All(ext => ext.Length == 0));
+ }
+
+ [Fact]
+ public void GetExtensionReturnsEmptyStringForPathsContainingPathInfo()
+ {
+ // Arrange
+ string[] paths = new[] { "SomePath.cshtml/", "SomePath.html/path/info" };
+
+ // Act
+ var extensions = paths.Select(PathUtil.GetExtension);
+
+ // Assert
+ Assert.True(extensions.All(ext => ext.Length == 0));
+ }
+
+ [Fact]
+ public void GetExtensionReturnsEmptyStringForPathsTerminatingWithADot()
+ {
+ // Arrange
+ string[] paths = new[] { "SomePath.", "SomeDirectory/SomePath/SomePath.", "SomeDirectory/SomePath.foo." };
+
+ // Act
+ var extensions = paths.Select(PathUtil.GetExtension);
+
+ // Assert
+ Assert.True(extensions.All(ext => ext.Length == 0));
+ }
+
+ [Fact]
+ public void GetExtensionReturnsExtensionsForPathsTerminatingInExtension()
+ {
+ // Arrange
+ string path1 = "SomePath.cshtml";
+ string path2 = "SomeDir/SomePath.txt";
+
+ // Act
+ string ext1 = PathUtil.GetExtension(path1);
+ string ext2 = PathUtil.GetExtension(path2);
+
+ // Assert
+ Assert.Equal(ext1, ".cshtml");
+ Assert.Equal(ext2, ".txt");
+ }
+
+ [Fact]
+ public void GetExtensionDoesNotThrowForPathsWithInvalidCharacters()
+ {
+ // Arrange
+ // Repro from test case in Bug 93828
+ string path = "Insights/110786998958803%7C2.d24wA6Y3MiT2w8p3OT4yTw__.3600.1289415600-708897727%7CRLN-t1w9bXtKWZ_11osz15Rk_jY";
+
+ // Act
+ string extension = PathUtil.GetExtension(path);
+
+ // Assert
+ Assert.Equal(".1289415600-708897727%7CRLN-t1w9bXtKWZ_11osz15Rk_jY", extension);
+ }
+
+ [Fact]
+ public void IsWithinAppRootNestedTest()
+ {
+ AppDomainUtils.RunInSeparateAppDomain(() =>
+ {
+ var root = "/subfolder1/website1";
+ using (Utils.CreateHttpRuntime(root))
+ {
+ Assert.True(PathUtil.IsWithinAppRoot(root, "~/"));
+ Assert.True(PathUtil.IsWithinAppRoot(root, "~/default.cshtml"));
+ Assert.True(PathUtil.IsWithinAppRoot(root, "~/test/default.cshtml"));
+ Assert.True(PathUtil.IsWithinAppRoot(root, "/subfolder1/website1"));
+ Assert.True(PathUtil.IsWithinAppRoot(root, "/subfolder1/website1/"));
+ Assert.True(PathUtil.IsWithinAppRoot(root, "/subfolder1/website1/default.cshtml"));
+ Assert.True(PathUtil.IsWithinAppRoot(root, "/subfolder1/website1/test/default.cshtml"));
+
+ Assert.False(PathUtil.IsWithinAppRoot(root, "/"));
+ Assert.False(PathUtil.IsWithinAppRoot(root, "/subfolder1"));
+ Assert.False(PathUtil.IsWithinAppRoot(root, "/subfolder1/"));
+ Assert.False(PathUtil.IsWithinAppRoot(root, "/subfolder1/website2"));
+ Assert.False(PathUtil.IsWithinAppRoot(root, "/subfolder2"));
+ }
+ });
+ }
+
+ [Fact]
+ public void IsWithinAppRootTest()
+ {
+ AppDomainUtils.RunInSeparateAppDomain(() =>
+ {
+ var root = "/website1";
+ using (Utils.CreateHttpRuntime(root))
+ {
+ Assert.True(PathUtil.IsWithinAppRoot(root, "~/"));
+ Assert.True(PathUtil.IsWithinAppRoot(root, "~/default.cshtml"));
+ Assert.True(PathUtil.IsWithinAppRoot(root, "~/test/default.cshtml"));
+ Assert.True(PathUtil.IsWithinAppRoot(root, "/website1"));
+ Assert.True(PathUtil.IsWithinAppRoot(root, "/website1/"));
+ Assert.True(PathUtil.IsWithinAppRoot(root, "/website1/default.cshtml"));
+ Assert.True(PathUtil.IsWithinAppRoot(root, "/website1/test/default.cshtml"));
+
+ Assert.False(PathUtil.IsWithinAppRoot(root, "/"));
+ Assert.False(PathUtil.IsWithinAppRoot(root, "/website2"));
+ Assert.False(PathUtil.IsWithinAppRoot(root, "/subfolder1/"));
+ }
+ });
+ }
+
+ private class TestVirtualPathUtility : IVirtualPathUtility
+ {
+ public string Combine(string basePath, string relativePath)
+ {
+ return basePath + "/" + relativePath;
+ }
+
+ public string ToAbsolute(string virtualPath)
+ {
+ return virtualPath;
+ }
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/Utils/SessionStateUtilTest.cs b/test/System.Web.WebPages.Test/Utils/SessionStateUtilTest.cs
new file mode 100644
index 00000000..97e4835d
--- /dev/null
+++ b/test/System.Web.WebPages.Test/Utils/SessionStateUtilTest.cs
@@ -0,0 +1,183 @@
+using System.Collections.Concurrent;
+using System.Web.Razor;
+using System.Web.SessionState;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.WebPages.Test
+{
+ public class SessionStateUtilTest
+ {
+ [Fact]
+ public void SetUpSessionStateDoesNotInvokeSessionStateBehaviorIfNoPageHasDirective()
+ {
+ // Arrange
+ var page = new Mock<WebPage>(MockBehavior.Strict);
+ var startPage = new Mock<StartPage>(MockBehavior.Strict);
+ var webPageHttpHandler = new WebPageHttpHandler(page.Object, startPage: new Lazy<WebPageRenderingBase>(() => startPage.Object));
+ var context = new Mock<HttpContextBase>(MockBehavior.Strict);
+
+ // Act
+ SessionStateUtil.SetUpSessionState(context.Object, webPageHttpHandler, new ConcurrentDictionary<Type, SessionStateBehavior?>());
+
+ // Assert
+ context.Verify(c => c.SetSessionStateBehavior(It.IsAny<SessionStateBehavior>()), Times.Never());
+ }
+
+ [Fact]
+ public void SetUpSessionStateUsesSessionStateValueFromRequestingPageIfAvailable()
+ {
+ // Arrange
+ var page = new DisabledSessionWebPage();
+ var webPageHttpHandler = new WebPageHttpHandler(page, startPage: null);
+ var context = new Mock<HttpContextBase>(MockBehavior.Strict);
+ context.Setup(c => c.SetSessionStateBehavior(SessionStateBehavior.Disabled)).Verifiable();
+
+ // Act
+ SessionStateUtil.SetUpSessionState(context.Object, webPageHttpHandler, new ConcurrentDictionary<Type, SessionStateBehavior?>());
+
+ // Assert
+ context.Verify();
+ }
+
+ [Fact]
+ public void SetUpSessionStateUsesSessionStateValueFromStartPageHierarchy()
+ {
+ // Arrange
+ var page = new Mock<WebPage>(MockBehavior.Strict);
+ var startPage = new DefaultSessionWebPage
+ {
+ ChildPage = new ReadOnlySessionWebPage()
+ };
+ var webPageHttpHandler = new WebPageHttpHandler(page.Object, startPage: new Lazy<WebPageRenderingBase>(() => startPage));
+ var context = new Mock<HttpContextBase>(MockBehavior.Strict);
+ context.Setup(c => c.SetSessionStateBehavior(SessionStateBehavior.ReadOnly)).Verifiable();
+
+ // Act
+ SessionStateUtil.SetUpSessionState(context.Object, webPageHttpHandler, new ConcurrentDictionary<Type, SessionStateBehavior?>());
+
+ // Assert
+ context.Verify();
+ }
+
+ [Fact]
+ public void SetUpSessionStateThrowsIfSessionStateValueIsInvalid()
+ {
+ // Arrange
+ var page = new Mock<WebPage>(MockBehavior.Strict);
+ var startPage = new InvalidSessionState();
+ var webPageHttpHandler = new WebPageHttpHandler(page.Object, startPage: new Lazy<WebPageRenderingBase>(() => startPage));
+ var context = new Mock<HttpContextBase>(MockBehavior.Strict);
+
+ // Act
+ Assert.Throws<ArgumentException>(() => SessionStateUtil.SetUpSessionState(context.Object, webPageHttpHandler, new ConcurrentDictionary<Type, SessionStateBehavior?>()),
+ "Value \"jabberwocky\" specified in \"~/_Invalid.cshtml\" is an invalid value for the SessionState directive. Possible values are: \"Default, Required, ReadOnly, Disabled\".");
+ }
+
+ [Fact]
+ public void SetUpSessionStateThrowsIfMultipleSessionStateValueIsInvalid()
+ {
+ // Arrange
+ var page = new PageWithMultipleSesionStateAttributes();
+ var webPageHttpHandler = new WebPageHttpHandler(page, startPage: null);
+ var context = new Mock<HttpContextBase>(MockBehavior.Strict);
+
+ // Act
+ Assert.Throws<InvalidOperationException>(() => SessionStateUtil.SetUpSessionState(context.Object, webPageHttpHandler, new ConcurrentDictionary<Type, SessionStateBehavior?>()),
+ "At most one SessionState value can be declared per page.");
+ }
+
+ [Fact]
+ public void SetUpSessionStateUsesCache()
+ {
+ // Arrange
+ var page = new PageWithBadAttribute();
+ var webPageHttpHandler = new WebPageHttpHandler(page, startPage: null);
+ var context = new Mock<HttpContextBase>(MockBehavior.Strict);
+ var dictionary = new ConcurrentDictionary<Type, SessionStateBehavior?>();
+ dictionary.TryAdd(webPageHttpHandler.GetType(), SessionStateBehavior.Default);
+ context.Setup(c => c.SetSessionStateBehavior(SessionStateBehavior.Default)).Verifiable();
+
+ // Act
+ SessionStateUtil.SetUpSessionState(context.Object, webPageHttpHandler, dictionary);
+
+ // Assert
+ context.Verify();
+ Assert.Throws<Exception>(() => page.GetType().GetCustomAttributes(inherit: false), "Can't call me!");
+ }
+
+ [RazorDirective("sessionstate", "disabled")]
+ private sealed class DisabledSessionWebPage : WebPage
+ {
+ public override void Execute()
+ {
+ throw new NotSupportedException();
+ }
+ }
+
+ [RazorDirective("sessionstate", "ReadOnly")]
+ private sealed class ReadOnlySessionWebPage : StartPage
+ {
+ public override void Execute()
+ {
+ throw new NotSupportedException();
+ }
+ }
+
+ [RazorDirective("SessionState", "Default")]
+ private sealed class DefaultSessionWebPage : StartPage
+ {
+ public override void Execute()
+ {
+ throw new NotSupportedException();
+ }
+ }
+
+ [RazorDirective("SessionState", "jabberwocky")]
+ private sealed class InvalidSessionState : StartPage
+ {
+ public override string VirtualPath
+ {
+ get
+ {
+ return "~/_Invalid.cshtml";
+ }
+ set
+ {
+ VirtualPath = value;
+ }
+ }
+ public override void Execute()
+ {
+ throw new NotSupportedException();
+ }
+ }
+
+ private sealed class BadAttribute : Attribute
+ {
+ public BadAttribute()
+ {
+ throw new Exception("Can't call me!");
+ }
+ }
+
+ [RazorDirective("SessionState", "Default"), Bad]
+ private sealed class PageWithBadAttribute : WebPage
+ {
+ public override void Execute()
+ {
+ throw new NotSupportedException();
+ }
+ }
+
+ [RazorDirective("SessionState", "Disabled"), RazorDirective("SessionState", "ReadOnly")]
+ private sealed class PageWithMultipleSesionStateAttributes : WebPage
+ {
+ public override void Execute()
+ {
+ throw new NotSupportedException();
+ }
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/Utils/TestObjectFactory.cs b/test/System.Web.WebPages.Test/Utils/TestObjectFactory.cs
new file mode 100644
index 00000000..64ee9914
--- /dev/null
+++ b/test/System.Web.WebPages.Test/Utils/TestObjectFactory.cs
@@ -0,0 +1,30 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace System.Web.WebPages.Test
+{
+ public class HashVirtualPathFactory : IVirtualPathFactory
+ {
+ private IDictionary<string, object> _pages;
+
+ public HashVirtualPathFactory(params WebPageExecutingBase[] pages)
+ {
+ _pages = pages.ToDictionary(p => p.VirtualPath, p => (object)p, StringComparer.OrdinalIgnoreCase);
+ }
+
+ public bool Exists(string virtualPath)
+ {
+ return _pages.ContainsKey(virtualPath);
+ }
+
+ public object CreateInstance(string virtualPath)
+ {
+ object value;
+ if (_pages.TryGetValue(virtualPath, out value))
+ {
+ return value;
+ }
+ return null;
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/Utils/TypeHelperTest.cs b/test/System.Web.WebPages.Test/Utils/TypeHelperTest.cs
new file mode 100644
index 00000000..7116344d
--- /dev/null
+++ b/test/System.Web.WebPages.Test/Utils/TypeHelperTest.cs
@@ -0,0 +1,111 @@
+using System.Collections.Generic;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.WebPages.Test
+{
+ public class TypeHelperTest
+ {
+ [Fact]
+ public void ObjectToDictionaryWithNullObjectReturnsEmptyDictionary()
+ {
+ // Arrange
+ object dict = null;
+
+ IDictionary<string, object> dictValues = TypeHelper.ObjectToDictionary(dict);
+
+ Assert.NotNull(dictValues);
+ Assert.Equal(0, dictValues.Count);
+ }
+
+ [Fact]
+ public void ObjectToDictionaryWithPlainObjectTypeReturnsEmptyDictionary()
+ {
+ // Arrange
+ object dict = new object();
+
+ // Act
+ IDictionary<string, object> dictValues = TypeHelper.ObjectToDictionary(dict);
+
+ // Assert
+ Assert.NotNull(dictValues);
+ Assert.Equal(0, dictValues.Count);
+ }
+
+ [Fact]
+ public void ObjectToDictionaryWithPrimitiveTypeLooksUpPublicProperties()
+ {
+ // Arrange
+ object dict = "test";
+
+ // Act
+ IDictionary<string, object> dictValues = TypeHelper.ObjectToDictionary(dict);
+
+ // Assert
+ Assert.NotNull(dictValues);
+ Assert.Equal(1, dictValues.Count);
+ Assert.Equal(4, dictValues["Length"]);
+ }
+
+ [Fact]
+ public void ObjectToDictionaryWithAnonymousTypeLooksUpProperties()
+ {
+ // Arrange
+ object dict = new { test = "value", other = 1 };
+
+ // Act
+ IDictionary<string, object> dictValues = TypeHelper.ObjectToDictionary(dict);
+
+ // Assert
+ Assert.NotNull(dictValues);
+ Assert.Equal(2, dictValues.Count);
+ Assert.Equal("value", dictValues["test"]);
+ Assert.Equal(1, dictValues["other"]);
+ }
+
+ [Fact]
+ public void ObjectToDictionaryReturnsCaseInsensitiveDictionary()
+ {
+ // Arrange
+ object dict = new { TEST = "value", oThEr = 1 };
+
+ // Act
+ IDictionary<string, object> dictValues = TypeHelper.ObjectToDictionary(dict);
+
+ // Assert
+ Assert.NotNull(dictValues);
+ Assert.Equal(2, dictValues.Count);
+ Assert.Equal("value", dictValues["test"]);
+ Assert.Equal(1, dictValues["other"]);
+ }
+
+ [Fact]
+ public void AddAnonymousTypeObjectToDictionaryTest()
+ {
+ IDictionary<string, object> d = new Dictionary<string, object>();
+ d.Add("X", "Xvalue");
+ TypeHelper.AddAnonymousObjectToDictionary(d, new { A = "a", B = "b" });
+ Assert.Equal("Xvalue", d["X"]);
+ Assert.Equal("a", d["A"]);
+ Assert.Equal("b", d["B"]);
+ }
+
+ [Fact]
+ public void IsAnonymousTypeTest()
+ {
+ Assert.False(TypeHelper.IsAnonymousType(typeof(object)));
+ Assert.False(TypeHelper.IsAnonymousType(typeof(string)));
+ Assert.False(TypeHelper.IsAnonymousType(typeof(IDictionary<object, object>)));
+ Assert.True(TypeHelper.IsAnonymousType((new { A = "a", B = "b" }.GetType())));
+ var x = "x";
+ var y = "y";
+ Assert.True(TypeHelper.IsAnonymousType((new { x, y }.GetType())));
+ }
+
+ [Fact]
+ public void IsAnonymousTypeNullTest()
+ {
+ Assert.ThrowsArgumentNull(() => TypeHelper.IsAnonymousType(null), "type");
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/Utils/UrlUtilTest.cs b/test/System.Web.WebPages.Test/Utils/UrlUtilTest.cs
new file mode 100644
index 00000000..042c6396
--- /dev/null
+++ b/test/System.Web.WebPages.Test/Utils/UrlUtilTest.cs
@@ -0,0 +1,207 @@
+using System.Web.WebPages.TestUtils;
+using Xunit;
+
+namespace System.Web.WebPages.Test
+{
+ public class UrlUtilTest
+ {
+ [Fact]
+ public void UrlTest()
+ {
+ AppDomainUtils.RunInSeparateAppDomain(() =>
+ {
+ using (IDisposable _ = Utils.CreateHttpContext("default.aspx", "http://localhost/WebSite1/subfolder1/default.aspx"),
+ __ = Utils.CreateHttpRuntime("/WebSite1/"))
+ {
+ var vpath = "~/subfolder1/default.aspx";
+ var href = "~/world/test.aspx";
+ var expected = "/WebSite1/world/test.aspx";
+ Assert.Equal(expected, UrlUtil.Url(vpath, href));
+ Assert.Equal(expected, new MockPage() { VirtualPath = vpath }.Href(href));
+ }
+ });
+ }
+
+ [Fact]
+ public void UrlTest2()
+ {
+ AppDomainUtils.RunInSeparateAppDomain(() =>
+ {
+ using (IDisposable _ = Utils.CreateHttpContext("default.aspx", "http://localhost/WebSite1/default.aspx"),
+ __ = Utils.CreateHttpRuntime("/WebSite1/"))
+ {
+ var vpath = "~/default.aspx";
+ var href = "~/world/test.aspx";
+ var expected = "/WebSite1/world/test.aspx";
+ Assert.Equal(expected, UrlUtil.Url(vpath, href));
+ Assert.Equal(expected, new MockPage() { VirtualPath = vpath }.Href(href));
+ }
+ });
+ }
+
+ [Fact]
+ public void UrlTest3()
+ {
+ AppDomainUtils.RunInSeparateAppDomain(() =>
+ {
+ using (IDisposable _ = Utils.CreateHttpContext("default.aspx", "http://localhost/WebSite1/subfolder1/default.aspx"),
+ __ = Utils.CreateHttpRuntime("/WebSite1/"))
+ {
+ var vpath = "~/subfolder1/default.aspx";
+ var href = "world/test.aspx";
+ var expected = "/WebSite1/subfolder1/world/test.aspx";
+ Assert.Equal(expected, UrlUtil.Url(vpath, href));
+ Assert.Equal(expected, new MockPage() { VirtualPath = vpath }.Href(href));
+ }
+ });
+ }
+
+ [Fact]
+ public void UrlTest4()
+ {
+ AppDomainUtils.RunInSeparateAppDomain(() =>
+ {
+ using (IDisposable _ = Utils.CreateHttpContext("default.aspx", "http://localhost/WebSite1/subfolder1/default.aspx"),
+ __ = Utils.CreateHttpRuntime("/WebSite1/"))
+ {
+ var vpath = "~/subfolder2/default.aspx";
+ var href = "world/test.aspx";
+ var expected = "/WebSite1/subfolder2/world/test.aspx";
+ Assert.Equal(expected, UrlUtil.Url(vpath, href));
+ Assert.Equal(expected, new MockPage() { VirtualPath = vpath }.Href(href));
+ }
+ });
+ }
+
+ [Fact]
+ public void BuildUrlEncodesPagePart()
+ {
+ // Arrange
+ var page = "This is a really bad name for a page";
+ var expected = "This%20is%20a%20really%20bad%20name%20for%20a%20page";
+
+ // Act
+ var actual = UrlUtil.BuildUrl(page);
+
+ // Assert
+ Assert.Equal(actual, expected);
+ }
+
+ [Fact]
+ public void BuildUrlAppendsNonAnonymousTypesToPathPortion()
+ {
+ // Arrange
+ object[] pathParts = new object[] { "part", Decimal.One, 1.25f };
+ var page = "home";
+
+ // Act
+ var actual = UrlUtil.BuildUrl(page, pathParts);
+
+ // Assert
+ Assert.Equal(actual, page + "/part/1/1.25");
+ }
+
+ [Fact]
+ public void BuildUrlEncodesAppendedPathPortion()
+ {
+ // Arrange
+ object[] pathParts = new object[] { "path portion", "ζ" };
+ var page = "home";
+
+ // Act
+ var actual = UrlUtil.BuildUrl(page, pathParts);
+
+ // Assert
+ Assert.Equal(actual, page + "/path%20portion/%ce%b6");
+ }
+
+ [Fact]
+ public void BuildUrlAppendsAnonymousObjectsToQueryString()
+ {
+ // Arrange
+ var page = "home";
+ var queryString = new { sort = "FName", dir = "desc" };
+
+ // Act
+ var actual = UrlUtil.BuildUrl(page, queryString);
+
+ // Assert
+ Assert.Equal(actual, page + "?sort=FName&dir=desc");
+ }
+
+ [Fact]
+ public void BuildUrlAppendsMultipleAnonymousObjectsToQueryString()
+ {
+ // Arrange
+ var page = "home";
+ var queryString1 = new { sort = "FName", dir = "desc" };
+ var queryString2 = new { view = "Activities", page = 7 };
+
+ // Act
+ var actual = UrlUtil.BuildUrl(page, queryString1, queryString2);
+
+ // Assert
+ Assert.Equal(actual, page + "?sort=FName&dir=desc&view=Activities&page=7");
+ }
+
+ [Fact]
+ public void BuildUrlEncodesQueryStringKeysAndValues()
+ {
+ // Arrange
+ var page = "home";
+ var queryString = new { ζ = "my=value&", mykey = "<π" };
+
+ // Act
+ var actual = UrlUtil.BuildUrl(page, queryString);
+
+ // Assert
+ Assert.Equal(actual, page + "?%ce%b6=my%3dvalue%26&mykey=%3c%cf%80");
+ }
+
+ [Fact]
+ public void BuildUrlGeneratesPathPartsAndQueryString()
+ {
+ // Arrange
+ var page = "home";
+
+ // Act
+ var actual = UrlUtil.BuildUrl(page, "products", new { cat = 37 }, "furniture", new { sort = "name", dir = "desc" });
+
+ // Assert
+ Assert.Equal(actual, page + "/products/furniture?cat=37&sort=name&dir=desc");
+ }
+
+ [Fact]
+ public void UrlAppRootTest()
+ {
+ AppDomainUtils.RunInSeparateAppDomain(() =>
+ {
+ using (IDisposable _ = Utils.CreateHttpContext("default.aspx", "http://localhost/"),
+ __ = Utils.CreateHttpRuntime("/"))
+ {
+ var vpath = "~/";
+ var href = "~/world/test.aspx";
+ var expected = "/world/test.aspx";
+ Assert.Equal(expected, UrlUtil.Url(vpath, href));
+ Assert.Equal(expected, new MockPage() { VirtualPath = vpath }.Href(href));
+ }
+ });
+ }
+
+ [Fact]
+ public void UrlAnonymousObjectTest()
+ {
+ AppDomainUtils.RunInSeparateAppDomain(() =>
+ {
+ using (IDisposable _ = Utils.CreateHttpContext("default.aspx", "http://localhost/"),
+ __ = Utils.CreateHttpRuntime("/"))
+ {
+ Assert.Equal("/world/test.cshtml?Prop1=value1",
+ UrlUtil.Url("~/world/page.cshtml", "test.cshtml", new { Prop1 = "value1" }));
+ Assert.Equal("/world/test.cshtml?Prop1=value1&Prop2=value2",
+ UrlUtil.Url("~/world/page.cshtml", "test.cshtml", new { Prop1 = "value1", Prop2 = "value2" }));
+ }
+ });
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/Validation/ValidationHelperTest.cs b/test/System.Web.WebPages.Test/Validation/ValidationHelperTest.cs
new file mode 100644
index 00000000..7f36d538
--- /dev/null
+++ b/test/System.Web.WebPages.Test/Validation/ValidationHelperTest.cs
@@ -0,0 +1,835 @@
+using System.Collections.Specialized;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Web.Mvc;
+using System.Web.WebPages.Html;
+using System.Web.WebPages.Scope;
+using System.Web.WebPages.TestUtils;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.WebPages.Validation.Test
+{
+ public class ValidationHelperTest
+ {
+ [Fact]
+ public void FormFieldKeyIsCommonToModelStateAndValidationHelper()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ string key = "_FORM";
+ ValidationHelper validationHelper = GetValidationHelper(GetContext());
+
+ // Act and Assert
+ Assert.Equal(key, ModelStateDictionary.FormFieldKey);
+ Assert.Equal(key, validationHelper.FormField);
+ }
+
+ [Fact]
+ public void AddThrowsIfFieldIsEmpty()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ ValidationHelper validationHelper = GetValidationHelper(GetContext());
+
+ // Act and Assert
+ Assert.ThrowsArgumentNullOrEmptyString(() => validationHelper.Add(field: null), "field");
+ Assert.ThrowsArgumentNullOrEmptyString(() => validationHelper.Add(field: String.Empty), "field");
+ }
+
+ [Fact]
+ public void AddThrowsIfValidatorsParamsArrayIsNull()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ ValidationHelper validationHelper = GetValidationHelper(GetContext());
+
+ // Act and Assert
+ Assert.ThrowsArgumentNull(() => validationHelper.Add("foo", null), "validators");
+ }
+
+ [Fact]
+ public void AddThrowsIfValidatorsAreNull()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ ValidationHelper validationHelper = GetValidationHelper(GetContext());
+
+ // Act and Assert
+ Assert.ThrowsArgumentNull(() => validationHelper.Add("foo", Validator.Required(), null, Validator.Range(0, 10)), "validators");
+ }
+
+ [Fact]
+ public void RequiredReturnsErrorMessageIfFieldIsNotPresentInForm()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ string message = "Foo is required.";
+ ValidationHelper validationHelper = GetValidationHelper(GetContext());
+
+ // Act
+ validationHelper.RequireField("foo", message);
+ var results = validationHelper.Validate();
+
+ // Assert
+ Assert.Equal(1, results.Count());
+ Assert.Equal(message, results.First().ErrorMessage);
+ }
+
+ [Fact]
+ public void RequiredReturnsErrorMessageIfFieldIsEmpty()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ string message = "Foo is required.";
+ ValidationHelper validationHelper = GetValidationHelper(GetContext(new { foo = "" }));
+
+ // Act
+ validationHelper.RequireField("foo", message);
+ var results = validationHelper.Validate();
+
+ // Assert
+ Assert.Equal(1, results.Count());
+ Assert.Equal(message, results.First().ErrorMessage);
+ }
+
+ [Fact]
+ public void RequiredReturnsNoValidationResultsIfFieldIsPresent()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ string message = "Foo is required.";
+ ValidationHelper validationHelper = GetValidationHelper(GetContext(new { foo = "some value" }));
+
+ // Act
+ validationHelper.RequireField("foo", message);
+ var results = validationHelper.Validate();
+
+ // Assert
+ Assert.Equal(0, results.Count());
+ }
+
+ [Fact]
+ public void RequiredUsesDefaultErrorMessageIfNoValueIsProvided()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ ValidationHelper validationHelper = GetValidationHelper(GetContext());
+
+ // Act
+ validationHelper.RequireField("foo");
+ var results = validationHelper.Validate();
+
+ // Assert
+ Assert.Equal(1, results.Count());
+ Assert.Equal("This field is required.", results.First().ErrorMessage);
+ }
+
+ [Fact]
+ public void RequiredReturnsValidationResultForEachFieldThatFailed()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ string message = "This field is required.";
+ ValidationHelper validationHelper = GetValidationHelper(GetContext());
+
+ // Act
+ validationHelper.RequireFields("foo", "bar");
+ var results = validationHelper.Validate();
+
+ // Assert
+ Assert.Equal(2, results.Count());
+ Assert.Equal(message, results.First().ErrorMessage);
+ Assert.Equal("foo", results.First().MemberNames.Single());
+
+ Assert.Equal(message, results.Last().ErrorMessage);
+ Assert.Equal("bar", results.Last().MemberNames.Single());
+ }
+
+ [Fact]
+ public void RequiredReturnsValidationResultForEachFieldThatFailedWhenFieldsIsEmpty()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ string message = "This field is required.";
+ ValidationHelper validationHelper = GetValidationHelper(GetContext());
+
+ // Act
+ validationHelper.RequireFields("foo", "bar");
+ var results = validationHelper.Validate(fields: null);
+
+ // Assert
+ Assert.Equal(2, results.Count());
+ Assert.Equal(message, results.First().ErrorMessage);
+ Assert.Equal("foo", results.First().MemberNames.Single());
+
+ Assert.Equal(message, results.Last().ErrorMessage);
+ Assert.Equal("bar", results.Last().MemberNames.Single());
+ }
+
+ [Fact]
+ public void GetValidationHtmlThrowsIfArgumentIsNullOrEmpty()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ ValidationHelper validationHelper = GetValidationHelper(GetContext());
+
+ // Act and Assert
+ Assert.ThrowsArgumentNullOrEmptyString(() => validationHelper.For(field: null), "field");
+ Assert.ThrowsArgumentNullOrEmptyString(() => validationHelper.For(field: String.Empty), "field");
+ }
+
+ [Fact]
+ public void RequireFieldThrowsIfValueIsNullOrEmpty()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ ValidationHelper validationHelper = GetValidationHelper(GetContext());
+
+ // Act and Assert
+ Assert.ThrowsArgumentNullOrEmptyString(() => validationHelper.RequireField(field: null), "field");
+ Assert.ThrowsArgumentNullOrEmptyString(() => validationHelper.RequireField(field: String.Empty), "field");
+ Assert.ThrowsArgumentNullOrEmptyString(() => validationHelper.RequireField(field: null, errorMessage: "baz"), "field");
+ Assert.ThrowsArgumentNullOrEmptyString(() => validationHelper.RequireField(field: String.Empty, errorMessage: null), "field");
+ }
+
+ [Fact]
+ public void RequireFieldsThrowsIfFieldsAreNullOrHasEmptyValues()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ ValidationHelper validationHelper = GetValidationHelper(GetContext());
+
+ // Act and Assert
+ Assert.ThrowsArgumentNull(() => validationHelper.RequireFields(fields: null), "fields");
+ Assert.ThrowsArgumentNullOrEmptyString(() => validationHelper.RequireFields(fields: new[] { "foo", null }), "field");
+ Assert.ThrowsArgumentNullOrEmptyString(() => validationHelper.RequireFields(fields: new[] { "foo", "" }), "field");
+ }
+
+ [Fact]
+ public void AddThrowsIfFieldIsNullOrEmpty()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ ValidationHelper validationHelper = GetValidationHelper(GetContext());
+
+ // Act and Assert
+ Assert.ThrowsArgumentNullOrEmptyString(() => validationHelper.Add(field: null), "field");
+ Assert.ThrowsArgumentNullOrEmptyString(() => validationHelper.Add(field: String.Empty), "field");
+ }
+
+ [Fact]
+ public void AddThrowsIfValidatorsIsNullOrAnyValidatorIsNull()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ ValidationHelper validationHelper = GetValidationHelper(GetContext());
+
+ // Act and Assert
+ Assert.ThrowsArgumentNull(() => validationHelper.Add(field: "foo", validators: null), "validators");
+ Assert.ThrowsArgumentNull(() => validationHelper.Add(field: "foo", validators: new[] { Validator.DateTime(), null }), "validators");
+ }
+
+ [Fact]
+ public void AddFormErrorCallsMethodInUnderlyingModelStateDictionary()
+ {
+ // Arrange
+ var message = "This is a form error.";
+ var dictionary = new ModelStateDictionary();
+ ValidationHelper validationHelper = GetValidationHelper(GetContext(), dictionary);
+
+ // Act
+ validationHelper.AddFormError(message);
+
+ // Assert
+ Assert.Equal(message, dictionary["_FORM"].Errors.Single());
+ }
+
+ [Fact]
+ public void GetValidationHtmlForRequired()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ string message = "Foo is required.";
+ ValidationHelper validationHelper = GetValidationHelper(GetContext());
+
+ // Act
+ validationHelper.RequireField("foo", message);
+ var validationHtml = validationHelper.For("foo");
+
+ // Assert
+ Assert.Equal(@"data-val-required=""Foo is required."" data-val=""true""", validationHtml.ToString());
+ }
+
+ [Fact]
+ public void ValidateReturnsAnEmptySequenceIfNoValidationsAreRegistered()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ ValidationHelper validationHelper = GetValidationHelper(GetContext());
+
+ // Act
+ var results = validationHelper.Validate();
+
+ // Assert
+ Assert.False(results.Any());
+ }
+
+ [Fact]
+ public void ValidatePopulatesModelStateDictionary()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var modelStateDictionary = new ModelStateDictionary();
+ ValidationHelper validationHelper = GetValidationHelper(GetContext(), modelStateDictionary);
+
+ // Act
+ validationHelper.RequireFields(new[] { "foo", "bar" });
+ validationHelper.Validate();
+
+ // Assert
+ Assert.False(modelStateDictionary.IsValid);
+ Assert.False(modelStateDictionary.IsValidField("foo"));
+ Assert.False(modelStateDictionary.IsValidField("bar"));
+ Assert.Equal("This field is required.", modelStateDictionary["foo"].Errors.Single());
+ Assert.Equal("This field is required.", modelStateDictionary["bar"].Errors.Single());
+ }
+
+ [Fact]
+ public void IsValidPopulatesModelStateDictionary()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var modelStateDictionary = new ModelStateDictionary();
+ ValidationHelper validationHelper = GetValidationHelper(GetContext(), modelStateDictionary);
+
+ // Act
+ validationHelper.RequireFields("foo", "bar");
+ validationHelper.IsValid();
+
+ // Assert
+ Assert.False(modelStateDictionary.IsValid);
+ Assert.False(modelStateDictionary.IsValidField("foo"));
+ Assert.False(modelStateDictionary.IsValidField("bar"));
+ Assert.Equal("This field is required.", modelStateDictionary["foo"].Errors.Single());
+ Assert.Equal("This field is required.", modelStateDictionary["bar"].Errors.Single());
+ }
+
+ [Fact]
+ public void GetErrorsPopulatesModelStateDictionary()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var modelStateDictionary = new ModelStateDictionary();
+ ValidationHelper validationHelper = GetValidationHelper(GetContext(), modelStateDictionary);
+
+ // Act
+ validationHelper.RequireFields("foo", "bar");
+ validationHelper.GetErrors();
+
+ // Assert
+ Assert.False(modelStateDictionary.IsValid);
+ Assert.False(modelStateDictionary.IsValidField("foo"));
+ Assert.False(modelStateDictionary.IsValidField("bar"));
+ Assert.Equal("This field is required.", modelStateDictionary["foo"].Errors.Single());
+ Assert.Equal("This field is required.", modelStateDictionary["bar"].Errors.Single());
+ }
+
+ [Fact]
+ public void GetErrorsReturnsAnEmptySequenceIfNoValidationsAreRegistered()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ ValidationHelper validationHelper = GetValidationHelper(GetContext());
+
+ // Act
+ var results = validationHelper.GetErrors();
+
+ // Assert
+ Assert.False(results.Any());
+ }
+
+ [Fact]
+ public void GetErrorsReturnsErrorsAddedViaAddError()
+ {
+ // Arrange
+ var modelStateDictionary = new ModelStateDictionary();
+ ValidationHelper validationHelper = GetValidationHelper(GetContext(), modelStateDictionary);
+
+ // Act
+ modelStateDictionary.AddError("foo", "Foo error");
+ var errors = validationHelper.GetErrors("foo");
+
+ // Assert
+ Assert.Equal(new[] { "Foo error" }, errors);
+ }
+
+ [Fact]
+ public void GetErrorsReturnsFormErrors()
+ {
+ // Arrange
+ string error = "Unable to connect to remote servers.";
+ var modelStateDictionary = new ModelStateDictionary();
+ ValidationHelper validationHelper = GetValidationHelper(GetContext(), modelStateDictionary);
+
+ // Act
+ validationHelper.AddFormError(error);
+ var errors = validationHelper.GetErrors();
+
+ // Assert
+ Assert.Equal(error, errors.Single());
+ }
+
+ [Fact]
+ public void InvokingValidateMultipleTimesDoesNotCauseErrorMessagesToBeDuplicated()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var modelStateDictionary = new ModelStateDictionary();
+ ValidationHelper validationHelper = GetValidationHelper(GetContext(), modelStateDictionary);
+
+ // Act
+ validationHelper.RequireField("foo", "Foo is required.");
+ validationHelper.RequireField("bar", "Bar is required.");
+ validationHelper.Validate();
+ Assert.False(validationHelper.IsValid());
+ validationHelper.Validate();
+ validationHelper.Validate();
+
+ // Assert
+ Assert.False(modelStateDictionary.IsValid);
+ Assert.False(modelStateDictionary.IsValidField("foo"));
+ Assert.False(modelStateDictionary.IsValidField("bar"));
+ Assert.Equal("Foo is required.", modelStateDictionary["foo"].Errors.Single());
+ Assert.Equal("Bar is required.", modelStateDictionary["bar"].Errors.Single());
+ }
+
+ [Fact]
+ public void AddWorksForCustomValidator()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ string message = "Foo is not an odd number.";
+ var oddValidator = new Mock<IValidator>();
+ oddValidator.Setup(c => c.Validate(It.IsAny<ValidationContext>())).Returns<ValidationContext>(v =>
+ {
+ Assert.IsAssignableFrom<HttpContextBase>(v.ObjectInstance);
+ var context = (HttpContextBase)v.ObjectInstance;
+ var value = Int32.Parse(context.Request.Form["foo"]);
+
+ if (value % 2 != 0)
+ {
+ return ValidationResult.Success;
+ }
+ return new ValidationResult(message);
+ }).Verifiable();
+ ValidationHelper validationHelper = GetValidationHelper(GetContext(new { foo = "6" }));
+
+ // Act
+ validationHelper.Add("foo", oddValidator.Object);
+ var result = validationHelper.Validate();
+
+ // Assert
+ Assert.Equal(1, result.Count());
+ Assert.Equal(message, result.First().ErrorMessage);
+ oddValidator.Verify();
+ }
+
+ [Fact]
+ public void ValidateRunsForSpecifiedFields()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ string message = "Foo is not an odd number.";
+ var oddValidator = new Mock<IValidator>();
+ oddValidator.Setup(c => c.Validate(It.IsAny<ValidationContext>())).Returns<ValidationContext>(v =>
+ {
+ Assert.IsAssignableFrom<HttpContextBase>(v.ObjectInstance);
+ var context = (HttpContextBase)v.ObjectInstance;
+ if (context.Request.Form["foo"].IsEmpty())
+ {
+ return ValidationResult.Success;
+ }
+ int value = context.Request.Form["foo"].AsInt();
+ if (value % 2 != 0)
+ {
+ return ValidationResult.Success;
+ }
+ return new ValidationResult(message);
+ }).Verifiable();
+ ValidationHelper validationHelper = GetValidationHelper(GetContext(new { foo = "", bar = "" }));
+
+ // Act
+ validationHelper.Add(new[] { "foo", "bar" }, oddValidator.Object);
+ validationHelper.RequireField("foo");
+ var result = validationHelper.Validate("foo");
+
+ // Assert
+ Assert.Equal(1, result.Count());
+ Assert.Equal("This field is required.", result.First().ErrorMessage);
+ }
+
+ [Fact]
+ public void GetErrorsReturnsAllErrorsIfNoParametersAreSpecified()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ string message = "Foo is not an odd number.";
+ var oddValidator = new Mock<IValidator>();
+ oddValidator.Setup(c => c.Validate(It.IsAny<ValidationContext>())).Returns<ValidationContext>(v =>
+ {
+ Assert.IsAssignableFrom<HttpContextBase>(v.ObjectInstance);
+ var context = (HttpContextBase)v.ObjectInstance;
+ if (context.Request.Form["foo"].IsEmpty())
+ {
+ return ValidationResult.Success;
+ }
+ int value = context.Request.Form["foo"].AsInt();
+ if (value % 2 != 0)
+ {
+ return ValidationResult.Success;
+ }
+ return new ValidationResult(message);
+ }).Verifiable();
+ ValidationHelper validationHelper = GetValidationHelper(GetContext(new { foo = "4", bar = "" }));
+
+ // Act
+ validationHelper.Add("foo", oddValidator.Object);
+ validationHelper.RequireFields(new[] { "bar", "foo" });
+ var result = validationHelper.GetErrors();
+
+ // Assert
+ Assert.Equal(2, result.Count());
+ Assert.Equal("Foo is not an odd number.", result.First());
+ Assert.Equal("This field is required.", result.Last());
+ }
+
+ [Fact]
+ public void IsValidReturnsTrueIfAllValuesPassValidation()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ string message = "Foo is not an odd number.";
+ var oddValidator = new Mock<IValidator>();
+ oddValidator.Setup(c => c.Validate(It.IsAny<ValidationContext>())).Returns<ValidationContext>(v =>
+ {
+ Assert.IsAssignableFrom<HttpContextBase>(v.ObjectInstance);
+ var context = (HttpContextBase)v.ObjectInstance;
+ if (context.Request.Form["foo"].IsEmpty())
+ {
+ return ValidationResult.Success;
+ }
+ int value = context.Request.Form["foo"].AsInt();
+ if (value % 2 != 0)
+ {
+ return ValidationResult.Success;
+ }
+ return new ValidationResult(message);
+ }).Verifiable();
+ ValidationHelper validationHelper = GetValidationHelper(GetContext(new { foo = "5", bar = "2" }));
+
+ // Act
+ validationHelper.Add(new[] { "foo", "bar" }, oddValidator.Object);
+ validationHelper.RequireField("foo");
+ var result = validationHelper.IsValid();
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void IsValidValidatesSpecifiedFields()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ string message = "Foo is not an odd number.";
+ var oddValidator = new Mock<IValidator>();
+ oddValidator.Setup(c => c.Validate(It.IsAny<ValidationContext>())).Returns<ValidationContext>(v =>
+ {
+ Assert.IsAssignableFrom<HttpContextBase>(v.ObjectInstance);
+ var context = (HttpContextBase)v.ObjectInstance;
+ int value;
+ if (!Int32.TryParse(context.Request.Form["foo"], out value))
+ {
+ return ValidationResult.Success;
+ }
+ if (value % 2 != 0)
+ {
+ return ValidationResult.Success;
+ }
+ return new ValidationResult(message);
+ }).Verifiable();
+ ValidationHelper validationHelper = GetValidationHelper(GetContext(new { foo = "3", bar = "" }));
+
+ // Act
+ validationHelper.Add(new[] { "foo", "bar" }, oddValidator.Object);
+ validationHelper.RequireFields(new[] { "foo", "bar" });
+ var result = validationHelper.IsValid("foo");
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void GetValidationHtmlReturnsNullIfNoRulesAreRegistered()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ ValidationHelper validationHelper = GetValidationHelper(GetContext());
+
+ // Assert
+ var validationAttributes = validationHelper.For("bar");
+
+ // Assert
+ Assert.Null(validationAttributes);
+ }
+
+ [Fact]
+ public void GetValidationHtmlReturnsAttributesForRegisteredValidators()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var validator = new Mock<IValidator>();
+ var clientRules = new ModelClientValidationRule { ValidationType = "foo", ErrorMessage = "Foo error." };
+ clientRules.ValidationParameters["qux"] = "some data";
+ validator.Setup(c => c.ClientValidationRule).Returns(clientRules).Verifiable();
+ var expected = @"data-val-required=""This field is required."" data-val-foo=""Foo error."" data-val-foo-qux=""some data"" data-val=""true""";
+
+ ValidationHelper validationHelper = GetValidationHelper(GetContext());
+
+ // Act
+ validationHelper.RequireField("foo");
+ validationHelper.Add("foo", validator.Object);
+ var validationAttributes = validationHelper.For("foo");
+
+ // Assert
+ Assert.Equal(expected, validationAttributes.ToString());
+ }
+
+ [Fact]
+ public void GetValidationHtmlHtmlEncodesAttributeValues()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var validator = new Mock<IValidator>();
+ var clientRules = new ModelClientValidationRule { ValidationType = "biz", ErrorMessage = "<Biz error.>" };
+ clientRules.ValidationParameters["qux"] = "<some ' data>";
+ validator.Setup(c => c.ClientValidationRule).Returns(clientRules).Verifiable();
+ var expected = @"data-val-required=""This field is required."" data-val-biz=""&lt;Biz error.&gt;"" data-val-biz-qux=""&lt;some &#39; data&gt;"" data-val=""true""";
+
+ // Act
+ ValidationHelper validationHelper = GetValidationHelper(GetContext());
+
+ // Assert
+ validationHelper.RequireField("foo");
+ validationHelper.Add("foo", validator.Object);
+ var validationAttributes = validationHelper.For("foo");
+
+ // Assert
+ Assert.Equal(expected, validationAttributes.ToString());
+ }
+
+ [Fact]
+ public void GetValidationFromClientValidationRulesThrowsIfValidationTypeIsNullOrEmpty()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var clientRule = new ModelClientValidationRule { ValidationType = null };
+
+ // Act and Assert
+ Assert.Throws<InvalidOperationException>(() => ValidationHelper.GenerateHtmlFromClientValidationRules(new[] { clientRule }),
+ "Validation type names in unobtrusive client validation rules cannot be empty. Client rule type: System.Web.Mvc.ModelClientValidationRule");
+
+ clientRule.ValidationType = String.Empty;
+ Assert.Throws<InvalidOperationException>(() => ValidationHelper.GenerateHtmlFromClientValidationRules(new[] { clientRule }),
+ "Validation type names in unobtrusive client validation rules cannot be empty. Client rule type: System.Web.Mvc.ModelClientValidationRule");
+ }
+
+ [Fact]
+ public void GetValidationFromClientValidationRulesThrowsIfSameValidationTypeIsSpecifiedMultipleTimes()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var clientRule1 = new ModelClientValidationRule { ValidationType = "foo" };
+ var clientRule2 = new ModelClientValidationRule { ValidationType = "foo" };
+
+ // Act and Assert
+ Assert.Throws<InvalidOperationException>(() => ValidationHelper.GenerateHtmlFromClientValidationRules(new[] { clientRule1, clientRule2 }),
+ "Validation type names in unobtrusive client validation rules must be unique. The following validation type was seen more than once: foo");
+ }
+
+ [Fact]
+ public void GetValidationFromClientValidationRulesThrowsIfValidationTypeDoesNotContainAllLowerCaseCharacters()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var clientRule = new ModelClientValidationRule { ValidationType = "Foo" };
+
+ // Act and Assert
+ Assert.Throws<InvalidOperationException>(() => ValidationHelper.GenerateHtmlFromClientValidationRules(new[] { clientRule }),
+ "Validation type names in unobtrusive client validation rules must consist of only lowercase letters. Invalid name: \"Foo\", client rule type: System.Web.Mvc.ModelClientValidationRule");
+
+ clientRule.ValidationType = "bAr";
+ Assert.Throws<InvalidOperationException>(() => ValidationHelper.GenerateHtmlFromClientValidationRules(new[] { clientRule }),
+ "Validation type names in unobtrusive client validation rules must consist of only lowercase letters. Invalid name: \"bAr\", client rule type: System.Web.Mvc.ModelClientValidationRule");
+
+ clientRule.ValidationType = "bar123";
+ Assert.Throws<InvalidOperationException>(() => ValidationHelper.GenerateHtmlFromClientValidationRules(new[] { clientRule }),
+ "Validation type names in unobtrusive client validation rules must consist of only lowercase letters. Invalid name: \"bar123\", client rule type: System.Web.Mvc.ModelClientValidationRule");
+ }
+
+ [Fact]
+ public void GetValidationFromClientValidationRulesThrowsIfValidationParamaterContainsNonAlphaNumericCharacters()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var clientRule = new ModelClientValidationRule { ValidationType = "required" };
+ clientRule.ValidationParameters["min^"] = "some-val";
+
+ // Act and Assert
+ Assert.Throws<InvalidOperationException>(() => ValidationHelper.GenerateHtmlFromClientValidationRules(new[] { clientRule }),
+ "Validation parameter names in unobtrusive client validation rules must start with a lowercase letter and consist of only lowercase letters or digits. Validation parameter name: min^, client rule type: System.Web.Mvc.ModelClientValidationRule");
+
+ clientRule.ValidationParameters.Clear();
+ clientRule.ValidationParameters["Min"] = "some-val";
+
+ Assert.Throws<InvalidOperationException>(() => ValidationHelper.GenerateHtmlFromClientValidationRules(new[] { clientRule }),
+ "Validation parameter names in unobtrusive client validation rules must start with a lowercase letter and consist of only lowercase letters or digits. Validation parameter name: Min, client rule type: System.Web.Mvc.ModelClientValidationRule");
+ }
+
+ [Fact]
+ public void DefaultValidCssClassIsNull()
+ {
+ Assert.Null(ValidationHelper.ValidCssClass);
+ }
+
+ [Fact]
+ public void DefaultInvalidCssClassIsSameAsHtmlHelper()
+ {
+ Assert.Equal(HtmlHelper.DefaultValidationInputErrorCssClass, ValidationHelper.InvalidCssClass);
+ }
+
+ [Fact]
+ public void InvalidCssClassIsNullIfExplicitlySetToNull()
+ {
+ using (ValidationHelper.OverrideScope())
+ {
+ ValidationHelper.InvalidCssClass = null;
+ Assert.Null(ValidationHelper.InvalidCssClass);
+ }
+ }
+
+ [Fact]
+ public void ValidCssClassIsScopeBacked()
+ {
+ // Set a value
+ string old = ValidationHelper.ValidCssClass;
+ ValidationHelper.ValidCssClass = "outer";
+ using (ScopeStorage.CreateTransientScope())
+ {
+ ValidationHelper.ValidCssClass = "inner";
+ Assert.Equal("inner", ValidationHelper.ValidCssClass);
+ }
+ Assert.Equal("outer", ValidationHelper.ValidCssClass);
+ ValidationHelper.ValidCssClass = old;
+ }
+
+ [Fact]
+ public void InvalidCssClassIsScopeBacked()
+ {
+ // Set a value
+ string old = ValidationHelper.InvalidCssClass;
+ ValidationHelper.InvalidCssClass = "outer";
+ using (ScopeStorage.CreateTransientScope())
+ {
+ ValidationHelper.InvalidCssClass = "inner";
+ Assert.Equal("inner", ValidationHelper.InvalidCssClass);
+ }
+ Assert.Equal("outer", ValidationHelper.InvalidCssClass);
+ ValidationHelper.InvalidCssClass = old;
+ }
+
+ [Fact]
+ public void ClassForReturnsNullIfNotPost()
+ {
+ // Arrange
+ ValidationHelper helper = GetValidationHelper();
+
+ // Act/Assert
+ Assert.Null(helper.ClassFor("foo"));
+ }
+
+ [Fact]
+ public void ClassForReturnsValidClassNameIfNoErrorsAddedForField()
+ {
+ // Arrange
+ ValidationHelper helper = GetPostValidationHelper();
+
+ // Act/Assert
+ HtmlString html = helper.ClassFor("foo");
+ string str = html == null ? null : html.ToHtmlString();
+ Assert.Equal(ValidationHelper.ValidCssClass, str);
+ }
+
+ [Fact]
+ public void ClassForReturnsInvalidClassNameIfFieldHasErrors()
+ {
+ // Arrange
+ ValidationHelper helper = GetPostValidationHelper();
+ helper.Add("foo", new AutoFailValidator());
+
+ // Act/Assert
+ Assert.Equal(ValidationHelper.InvalidCssClass, helper.ClassFor("foo").ToHtmlString());
+ }
+
+ private static ValidationHelper GetPostValidationHelper()
+ {
+ HttpContextBase context = GetContext();
+ Mock.Get(context.Request).SetupGet(c => c.HttpMethod).Returns("POST");
+ ValidationHelper helper = GetValidationHelper(httpContext: context);
+ return helper;
+ }
+
+ private static HttpContextBase GetContext(object formValues = null)
+ {
+ var context = new Mock<HttpContextBase>();
+ var request = new Mock<HttpRequestBase>();
+
+ var nameValueCollection = new NameValueCollection();
+ if (formValues != null)
+ {
+ foreach (var prop in formValues.GetType().GetProperties())
+ {
+ nameValueCollection.Add(prop.Name, prop.GetValue(formValues, null).ToString());
+ }
+ }
+ request.SetupGet(c => c.Form).Returns(nameValueCollection);
+ context.SetupGet(c => c.Request).Returns(request.Object);
+
+ return context.Object;
+ }
+
+ private static ValidationHelper GetValidationHelper(HttpContextBase httpContext = null, ModelStateDictionary modelStateDictionary = null)
+ {
+ httpContext = httpContext ?? GetContext();
+ modelStateDictionary = modelStateDictionary ?? new ModelStateDictionary();
+
+ return new ValidationHelper(httpContext, modelStateDictionary);
+ }
+
+ private class AutoFailValidator : IValidator
+ {
+
+ public ValidationResult Validate(ValidationContext validationContext)
+ {
+ return new ValidationResult("Failed!");
+ }
+
+ public ModelClientValidationRule ClientValidationRule
+ {
+ get { throw new NotImplementedException(); }
+ }
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/Validation/ValidatorTest.cs b/test/System.Web.WebPages.Test/Validation/ValidatorTest.cs
new file mode 100644
index 00000000..b934a110
--- /dev/null
+++ b/test/System.Web.WebPages.Test/Validation/ValidatorTest.cs
@@ -0,0 +1,882 @@
+using System.Collections.Specialized;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.WebPages.Validation.Test
+{
+ public class ValidatorTest
+ {
+ [Fact]
+ public void RequiredValidatorValidatesIfStringIsNull()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var requiredValidator = Validator.Required();
+ var validationContext = GetValidationContext(GetContext(), "foo");
+
+ // Act
+ var result = requiredValidator.Validate(validationContext);
+
+ // Assert
+ Assert.NotEqual(ValidationResult.Success, result);
+ Assert.Equal("This field is required.", result.ErrorMessage);
+ Assert.Equal("foo", result.MemberNames.Single());
+ }
+
+ [Fact]
+ public void RequiredValidatorValidatesIfStringIsEmpty()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var requiredValidator = Validator.Required();
+ var validationContext = GetValidationContext(GetContext(new { foo = "" }), "foo");
+
+ // Act
+ var result = requiredValidator.Validate(validationContext);
+
+ // Assert
+ Assert.NotEqual(ValidationResult.Success, result);
+ Assert.Equal("This field is required.", result.ErrorMessage);
+ Assert.Equal("foo", result.MemberNames.Single());
+ }
+
+ [Fact]
+ public void RequiredValidatorReturnsCustomErrorMessagesIfSpecified()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var requiredValidator = Validator.Required("There is no string");
+ var httpContext = GetContext(new { foo = "" });
+ var validationContext = GetValidationContext(httpContext, "foo");
+
+ // Act
+ var result = requiredValidator.Validate(validationContext);
+
+ // Assert
+ Assert.NotEqual(ValidationResult.Success, result);
+ Assert.Equal("There is no string", result.ErrorMessage);
+ Assert.Equal("foo", result.MemberNames.Single());
+ }
+
+ [Fact]
+ public void RequiredValidatorReturnsSuccessIfNoFieldIsNotNullOrEmpty()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var requiredValidator = Validator.Required("foo");
+ var validationContext = GetValidationContext(GetContext(new { foo = "some value" }), "foo");
+
+ // Act
+ var result = requiredValidator.Validate(validationContext);
+
+ // Assert
+ Assert.Equal(ValidationResult.Success, result);
+ }
+
+ [Fact]
+ public void GetClientValidationRulesForRequiredValidatorWithDefaultErrorMessage()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var requiredValidator = Validator.Required();
+
+ // Act
+ var result = requiredValidator.ClientValidationRule;
+
+ // Assert
+ Assert.Equal("required", result.ValidationType);
+ Assert.Equal("This field is required.", result.ErrorMessage);
+ Assert.False(result.ValidationParameters.Any());
+ }
+
+ [Fact]
+ public void GetClientValidationRulesForRequiredValidatorWithCustomErrorMessage()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var requiredValidator = Validator.Required("custom message");
+
+ // Act
+ var result = requiredValidator.ClientValidationRule;
+
+ // Assert
+ Assert.Equal("required", result.ValidationType);
+ Assert.Equal("custom message", result.ErrorMessage);
+ Assert.False(result.ValidationParameters.Any());
+ }
+
+ [Fact]
+ public void RangeValidatorReturnsSuccessIfValueIsInRange()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var rangeValidator = Validator.Range(10, 12);
+ var validationContext = GetValidationContext(GetContext(new { foo = 11 }), "foo");
+
+ // Act
+ var result = rangeValidator.Validate(validationContext);
+
+ // Assert
+ Assert.Equal(ValidationResult.Success, result);
+ }
+
+ [Fact]
+ public void RangeValidatorReturnsDefaultErrorMessageIfValueIsNotInRange()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var rangeValidator = Validator.Range(10, 12);
+ var validationContext = GetValidationContext(GetContext(new { foo = 7 }), "foo");
+
+ // Act
+ var result = rangeValidator.Validate(validationContext);
+
+ // Assert
+ Assert.NotEqual(ValidationResult.Success, result);
+ Assert.Equal("Value must be an integer between 10 and 12.", result.ErrorMessage);
+ Assert.Equal("foo", result.MemberNames.Single());
+ }
+
+ [Fact]
+ public void RangeValidatorReturnsDefaultErrorMessageIfValueIsNotInRangeForFloatingPointValues()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var rangeValidator = Validator.Range(10.8, 12.2);
+ var validationContext = GetValidationContext(GetContext(new { foo = 7 }), "foo");
+
+ // Act
+ var result = rangeValidator.Validate(validationContext);
+
+ // Assert
+ Assert.NotEqual(ValidationResult.Success, result);
+ Assert.Equal("Value must be a decimal between 10.8 and 12.2.", result.ErrorMessage);
+ Assert.Equal("foo", result.MemberNames.Single());
+ }
+
+ [Fact]
+ public void RangeValidatorReturnsCustomErrorMessageIfSpecified()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var rangeValidator = Validator.Range(10, 12, "Custom error message");
+ var validationContext = GetValidationContext(GetContext(new { foo = 13 }), "foo");
+
+ // Act
+ var result = rangeValidator.Validate(validationContext);
+
+ // Assert
+ Assert.NotEqual(ValidationResult.Success, result);
+ Assert.Equal("Custom error message", result.ErrorMessage);
+ Assert.Equal("foo", result.MemberNames.Single());
+ }
+
+ [Fact]
+ public void RangeValidatorFormatsCustomErrorMessage()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var rangeValidator = Validator.Range(10, 12, "Valid range: {0}-{1}");
+ var validationContext = GetValidationContext(GetContext(new { foo = 13 }), "foo");
+
+ // Act
+ var result = rangeValidator.Validate(validationContext);
+
+ // Assert
+ Assert.NotEqual(ValidationResult.Success, result);
+ Assert.Equal("Valid range: 10-12", result.ErrorMessage);
+ Assert.Equal("foo", result.MemberNames.Single());
+ }
+
+ [Fact]
+ public void GetClientValidationRulesForRangeValidatorWithDefaultErrorMessage()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var rangeValidator = Validator.Range(10, 12);
+
+ // Act
+ var result = rangeValidator.ClientValidationRule;
+
+ // Assert
+ Assert.Equal("range", result.ValidationType);
+ Assert.Equal("Value must be an integer between 10 and 12.", result.ErrorMessage);
+ Assert.Equal(10, result.ValidationParameters["min"]);
+ Assert.Equal(12, result.ValidationParameters["max"]);
+ }
+
+ [Fact]
+ public void GetClientValidationRulesForRangeValidatorWithCustomErrorMessage()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var rangeValidator = Validator.Range(10, 11, "Range: {0}-{1}");
+
+ // Act
+ var result = rangeValidator.ClientValidationRule;
+
+ // Assert
+ Assert.Equal("range", result.ValidationType);
+ Assert.Equal("Range: 10-11", result.ErrorMessage);
+ Assert.Equal(10, result.ValidationParameters["min"]);
+ Assert.Equal(11, result.ValidationParameters["max"]);
+ }
+
+ [Fact]
+ public void StringLengthValidatorReturnsSuccessIfStringLengthIsSmallerThanMaxValue()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var validator = Validator.StringLength(10);
+ var validationContext = GetValidationContext(GetContext(new { baz = "hello" }), "baz");
+
+ // Act
+ var result = validator.Validate(validationContext);
+
+ // Assert
+ Assert.Equal(ValidationResult.Success, result);
+ }
+
+ [Fact]
+ public void StringLengthValidatorReturnsSuccessIfStringLengthIsRangeOfMinAndMaxValue()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var validator = Validator.StringLength(10, minLength: 6);
+ var validationContext = GetValidationContext(GetContext(new { bar = "woof woof" }), "bar");
+
+ // Act
+ var result = validator.Validate(validationContext);
+
+ // Assert
+ Assert.Equal(ValidationResult.Success, result);
+ }
+
+ [Fact]
+ public void StringLengthValidatorReturnsFailureIfStringLengthIsLongerThanMaxValue()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var validator = Validator.StringLength(4);
+ var validationContext = GetValidationContext(GetContext(new { baz = "woof woof" }), "baz");
+
+ // Act
+ var result = validator.Validate(validationContext);
+
+ // Assert
+ Assert.NotEqual(ValidationResult.Success, result);
+ Assert.Equal("Max length: 4.", result.ErrorMessage);
+ Assert.Equal("baz", result.MemberNames.Single());
+ }
+
+ [Fact]
+ public void StringLengthValidatorReturnsCustomErrorMessageIfStringLengthIsLongerThanMaxValue()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var validator = Validator.StringLength(4, errorMessage: "String must be at least {0} characters long.");
+ var validationContext = GetValidationContext(GetContext(new { baz = "woof woof" }), "baz");
+
+ // Act
+ var result = validator.Validate(validationContext);
+
+ // Assert
+ Assert.NotEqual(ValidationResult.Success, result);
+ Assert.Equal("String must be at least 4 characters long.", result.ErrorMessage);
+ Assert.Equal("baz", result.MemberNames.Single());
+ }
+
+ [Fact]
+ public void StringLengthValidatorReturnsFailureIfStringLengthIsNotInRange()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var validator = Validator.StringLength(6, 4);
+ var validationContext = GetValidationContext(GetContext(new { baz = "woof woof" }), "baz");
+
+ // Act
+ var result = validator.Validate(validationContext);
+
+ // Assert
+ Assert.NotEqual(ValidationResult.Success, result);
+ Assert.Equal("String must be between 4 and 6 characters.", result.ErrorMessage);
+ Assert.Equal("baz", result.MemberNames.Single());
+ }
+
+ [Fact]
+ public void StringLengthValidatorReturnsCustomErrorMessageIfStringLengthIsNotInRange()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var validator = Validator.StringLength(6, 4, "Range {0} - {1}");
+ var validationContext = GetValidationContext(GetContext(new { baz = "woof woof" }), "baz");
+
+ // Act
+ var result = validator.Validate(validationContext);
+
+ // Assert
+ Assert.NotEqual(ValidationResult.Success, result);
+ Assert.Equal("Range 4 - 6", result.ErrorMessage);
+ Assert.Equal("baz", result.MemberNames.Single());
+ }
+
+ [Fact]
+ public void GetClientValidationRulesForStringLengthValidatorWithDefaultErrorMessage()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var validator = Validator.StringLength(6, 4);
+
+ // Act
+ var result = validator.ClientValidationRule;
+
+ // Assert
+ Assert.Equal(result.ValidationType, "length");
+ Assert.Equal(result.ErrorMessage, "String must be between 4 and 6 characters.");
+ Assert.Equal(result.ValidationParameters["min"], 4);
+ Assert.Equal(result.ValidationParameters["max"], 6);
+ }
+
+ [Fact]
+ public void GetClientValidationRulesForStringLengthValidatorWithCustomErrorMessage()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var validator = Validator.StringLength(6, errorMessage: "Must be at least 6 letters.");
+
+ // Act
+ var result = validator.ClientValidationRule;
+
+ // Assert
+ Assert.Equal(result.ValidationType, "length");
+ Assert.Equal(result.ErrorMessage, "Must be at least 6 letters.");
+ Assert.Equal(result.ValidationParameters["max"], 6);
+ }
+
+ [Fact]
+ public void RegexThrowsIfPatternIsNullOrEmpty()
+ {
+ // Act and Assert
+ Assert.ThrowsArgumentNullOrEmptyString(() => Validator.Regex(null), "pattern");
+ Assert.ThrowsArgumentNullOrEmptyString(() => Validator.Regex(String.Empty), "pattern");
+ }
+
+ [Fact]
+ public void RegexReturnsSuccessIfValueMatches()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var validator = Validator.Regex("^a+b+c$");
+ var context = GetValidationContext(GetContext(new { foo = "aaabbc" }), "foo");
+
+ // Act
+ var result = validator.Validate(context);
+
+ // Assert
+ Assert.Equal(ValidationResult.Success, result);
+ }
+
+ [Fact]
+ public void RegexReturnsDefaultErrorMessageIfValidationFails()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var validator = Validator.Regex("^a+b+c$");
+ var context = GetValidationContext(GetContext(new { foo = "aaXabbc" }), "foo");
+
+ // Act
+ var result = validator.Validate(context);
+
+ // Assert
+ Assert.NotEqual(ValidationResult.Success, result);
+ Assert.Equal("Value is invalid.", result.ErrorMessage);
+ Assert.Equal("foo", result.MemberNames.Single());
+ }
+
+ [Fact]
+ public void RegexReturnsCustomErrorMessageIfValidationFails()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var validator = Validator.Regex("^a+b+c$");
+ var context = GetValidationContext(GetContext(new { foo = "aaXabbc" }), "foo");
+
+ // Act
+ var result = validator.Validate(context);
+
+ // Assert
+ Assert.NotEqual(ValidationResult.Success, result);
+ Assert.Equal("Value is invalid.", result.ErrorMessage);
+ Assert.Equal("foo", result.MemberNames.Single());
+ }
+
+ [Fact]
+ public void IntegerReturnsSuccessIfValueIsNull()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var validator = Validator.Integer();
+ var context = GetValidationContext(GetContext(new { Name = "Not-Age" }), "age");
+
+ // Act
+ var result = validator.Validate(context);
+
+ // Assert
+ Assert.Equal(ValidationResult.Success, result);
+ }
+
+ [Fact]
+ public void IntegerReturnsSuccessIfValueIsEmpty()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var validator = Validator.Integer();
+ var context = GetValidationContext(GetContext(new { Age = "" }), "age");
+
+ // Act
+ var result = validator.Validate(context);
+
+ // Assert
+ Assert.Equal(ValidationResult.Success, result);
+ }
+
+ [Fact]
+ public void IntegerReturnsSuccessIfValueIsValidInteger()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var validator = Validator.Integer();
+ var context = GetValidationContext(GetContext(new { Age = "10" }), "age");
+
+ // Act
+ var result = validator.Validate(context);
+
+ // Assert
+ Assert.Equal(ValidationResult.Success, result);
+ }
+
+ [Fact]
+ public void IntegerReturnsSuccessIfValueIsNegativeInteger()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var validator = Validator.Integer();
+ var context = GetValidationContext(GetContext(new { Age = "-42" }), "age");
+
+ // Act
+ var result = validator.Validate(context);
+
+ // Assert
+ Assert.Equal(ValidationResult.Success, result);
+ }
+
+ [Fact]
+ public void IntegerReturnsSuccessIfValueIsZero()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var validator = Validator.Integer();
+ var context = GetValidationContext(GetContext(new { Age = 0 }), "age");
+
+ // Act
+ var result = validator.Validate(context);
+
+ // Assert
+ Assert.Equal(ValidationResult.Success, result);
+ }
+
+ [Fact]
+ public void IntegerReturnsErrorMessageIfValueIsFloatingPointValue()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var validator = Validator.Integer();
+ var context = GetValidationContext(GetContext(new { Age = 1.3 }), "age");
+
+ // Act
+ var result = validator.Validate(context);
+
+ // Assert
+ Assert.NotEqual(ValidationResult.Success, result);
+ Assert.Equal("Input format is invalid.", result.ErrorMessage);
+ Assert.Equal("age", result.MemberNames.Single());
+ }
+
+ [Fact]
+ public void IntegerReturnsErrorMessageIfValueIsNotAnInteger()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var validator = Validator.Integer();
+ var context = GetValidationContext(GetContext(new { Age = "2008-04-01" }), "age");
+
+ // Act
+ var result = validator.Validate(context);
+
+ // Assert
+ Assert.NotEqual(ValidationResult.Success, result);
+ Assert.Equal("Input format is invalid.", result.ErrorMessage);
+ Assert.Equal("age", result.MemberNames.Single());
+ }
+
+ [Fact]
+ public void FloatReturnsSuccessIfValueIsNull()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var validator = Validator.Float();
+ var context = GetValidationContext(GetContext(new { }), "Price");
+
+ // Act
+ var result = validator.Validate(context);
+
+ // Assert
+ Assert.Equal(ValidationResult.Success, result);
+ }
+
+ [Fact]
+ public void FloatReturnsSuccessIfValueIsEmptyString()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var validator = Validator.Float();
+ var context = GetValidationContext(GetContext(new { Price = "" }), "Price");
+
+ // Act
+ var result = validator.Validate(context);
+
+ // Assert
+ Assert.Equal(ValidationResult.Success, result);
+ }
+
+ [Fact]
+ public void FloatReturnsSuccessIfValueIsValidFloatString()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var validator = Validator.Float();
+ var context = GetValidationContext(GetContext(new { Price = Single.MaxValue.ToString() }), "Price");
+
+ // Act
+ var result = validator.Validate(context);
+
+ // Assert
+ Assert.Equal(ValidationResult.Success, result);
+ }
+
+ [Fact]
+ public void FloatReturnsSuccessIfValueIsValidInteger()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var validator = Validator.Float();
+ var context = GetValidationContext(GetContext(new { Price = "1" }), "Price");
+
+ // Act
+ var result = validator.Validate(context);
+
+ // Assert
+ Assert.Equal(ValidationResult.Success, result);
+ }
+
+ [Fact]
+ public void FloatReturnsErrorIfValueIsNotAValidFloat()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var validator = Validator.Float();
+ var context = GetValidationContext(GetContext(new { Price = "Free!!!" }), "Price");
+
+ // Act
+ var result = validator.Validate(context);
+
+ // Assert
+ Assert.NotEqual(ValidationResult.Success, result);
+ Assert.Equal("Input format is invalid.", result.ErrorMessage);
+ Assert.Equal("Price", result.MemberNames.Single());
+ }
+
+ [Fact]
+ public void DateTimeReturnsSuccessIfValueIsNull()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var validator = Validator.DateTime();
+ var context = GetValidationContext(GetContext(new { }), "dateOfBirth");
+
+ // Act
+ var result = validator.Validate(context);
+
+ // Assert
+ Assert.Equal(ValidationResult.Success, result);
+ }
+
+ [Fact]
+ public void DateTimeReturnsSuccessIfValueIsEmpty()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var validator = Validator.DateTime();
+ var context = GetValidationContext(GetContext(new { dateOfBirth = "" }), "dateOfBirth");
+
+ // Act
+ var result = validator.Validate(context);
+
+ // Assert
+ Assert.Equal(ValidationResult.Success, result);
+ }
+
+ [Fact]
+ public void DateTimeReturnsSuccessIfValueIsValidDateTime()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var validator = Validator.DateTime();
+ var context = GetValidationContext(GetContext(new { dateOfBirth = DateTime.Now.ToString() }), "dateOfBirth");
+
+ // Act
+ var result = validator.Validate(context);
+
+ // Assert
+ Assert.Equal(ValidationResult.Success, result);
+ }
+
+ [Fact]
+ public void DateTimeReturnsErrorIfValueIsInvalidDateTime()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var validator = Validator.DateTime();
+ var context = GetValidationContext(GetContext(new { dateOfBirth = "23.28" }), "dateOfBirth");
+
+ // Act
+ var result = validator.Validate(context);
+
+ // Assert
+ Assert.NotEqual(ValidationResult.Success, result);
+ Assert.Equal("Input format is invalid.", result.ErrorMessage);
+ Assert.Equal("dateOfBirth", result.MemberNames.Single());
+ }
+
+ [Fact]
+ public void UrlReturnsSuccessIfInputIsNull()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var validator = Validator.Url();
+ var context = GetValidationContext(GetContext(new { }), "blogUrl");
+
+ // Act
+ var result = validator.Validate(context);
+
+ // Assert
+ Assert.Equal(ValidationResult.Success, result);
+ }
+
+ [Fact]
+ public void UrlReturnsSuccessIfInputIsEmptyString()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var validator = Validator.Url();
+ var context = GetValidationContext(GetContext(new { blogUrl = "" }), "blogUrl");
+
+ // Act
+ var result = validator.Validate(context);
+
+ // Assert
+ Assert.Equal(ValidationResult.Success, result);
+ }
+
+ [Fact]
+ public void UrlReturnsSuccessIfInputIsValidUrl()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var validator = Validator.Url();
+ var context = GetValidationContext(GetContext(new { blogUrl = "http://www.microsoft.com?query-param=query-param-value&some-val=&quot;true&quot;" }), "blogUrl");
+
+ // Act
+ var result = validator.Validate(context);
+
+ // Assert
+ Assert.Equal(ValidationResult.Success, result);
+ }
+
+ [Fact]
+ public void UrlReturnsErrorMessageIfInputIsPhysicalPath()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var validator = Validator.Url();
+ var context = GetValidationContext(GetContext(new { blogUrl = @"x:\some-path\foo.txt" }), "blogUrl");
+
+ // Act
+ var result = validator.Validate(context);
+
+ // Assert
+ Assert.NotEqual(ValidationResult.Success, result);
+ Assert.Equal("Input format is invalid.", result.ErrorMessage);
+ Assert.Equal("blogUrl", result.MemberNames.Single());
+ }
+
+ [Fact]
+ public void UrlReturnsErrorMessageIfInputIsNetworkPath()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var validator = Validator.Url();
+ var context = GetValidationContext(GetContext(new { blogUrl = @"\\network-share\some-path\" }), "blogUrl");
+
+ // Act
+ var result = validator.Validate(context);
+
+ // Assert
+ Assert.NotEqual(ValidationResult.Success, result);
+ Assert.Equal("Input format is invalid.", result.ErrorMessage);
+ Assert.Equal("blogUrl", result.MemberNames.Single());
+ }
+
+ [Fact]
+ public void UrlReturnsErrorMessageIfInputIsNotAnUrl()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var validator = Validator.Url();
+ var context = GetValidationContext(GetContext(new { blogUrl = 65 }), "blogUrl");
+
+ // Act
+ var result = validator.Validate(context);
+
+ // Assert
+ Assert.NotEqual(ValidationResult.Success, result);
+ Assert.Equal("Input format is invalid.", result.ErrorMessage);
+ Assert.Equal("blogUrl", result.MemberNames.Single());
+ }
+
+ [Fact]
+ public void EqualsToValidatorThrowsIfFieldIsNullOrEmpty()
+ {
+ // Act and Assert
+ Assert.ThrowsArgumentNullOrEmptyString(() => Validator.EqualsTo(null), "otherFieldName");
+ }
+
+ [Fact]
+ public void EqualsToValidatorReturnsFalseIfEitherFieldIsEmpty()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var validator = Validator.EqualsTo("password");
+ var context = GetValidationContext(GetContext(new { password = "", confirmPassword = "abcd" }), "confirmPassword");
+
+ // Act
+ var result = validator.Validate(context);
+
+ // Assert
+ Assert.NotEqual(ValidationResult.Success, result);
+ Assert.Equal("Values do not match.", result.ErrorMessage);
+ Assert.Equal("confirmPassword", result.MemberNames.Single());
+
+ context = GetValidationContext(GetContext(new { password = "abcd", confirmPassword = "" }), "confirmPassword");
+
+ // Act
+ result = validator.Validate(context);
+
+ // Assert
+ Assert.NotEqual(ValidationResult.Success, result);
+ Assert.Equal("Values do not match.", result.ErrorMessage);
+ Assert.Equal("confirmPassword", result.MemberNames.Single());
+ }
+
+ [Fact]
+ public void EqualsToValidatorReturnsFalseIfValuesDoNotMatch()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var validator = Validator.EqualsTo("password");
+ var context = GetValidationContext(GetContext(new { password = "password2", confirmPassword = "abcd" }), "confirmPassword");
+
+ // Act
+ var result = validator.Validate(context);
+
+ // Assert
+ Assert.NotEqual(ValidationResult.Success, result);
+ Assert.Equal("Values do not match.", result.ErrorMessage);
+ Assert.Equal("confirmPassword", result.MemberNames.Single());
+ }
+
+ [Fact]
+ public void EqualsToValidatorReturnsTrueIfValuesMatch()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var validator = Validator.EqualsTo("password");
+ var context = GetValidationContext(GetContext(new { password = "abcd", confirmPassword = "abcd" }), "confirmPassword");
+
+ // Act
+ var result = validator.Validate(context);
+
+ // Assert
+ Assert.Equal(ValidationResult.Success, result);
+ }
+
+ [Fact]
+ public void GetClientValidationRulesForRegex()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var validator = Validator.Regex("^a+b+c$");
+
+ // Act
+ var result = validator.ClientValidationRule;
+
+ // Assert
+ Assert.Equal("regex", result.ValidationType);
+ Assert.Equal("Value is invalid.", result.ErrorMessage);
+ Assert.Equal("^a+b+c$", result.ValidationParameters["pattern"]);
+ }
+
+ [Fact]
+ public void GetClientValidationRulesForRegexWithCustomErrorMessage()
+ {
+ // Arrange
+ RequestFieldValidatorBase.IgnoreUseUnvalidatedValues = true;
+ var validator = Validator.Regex("^a+b+c$", "Example aaabbc");
+
+ // Act
+ var result = validator.ClientValidationRule;
+
+ // Assert
+ Assert.Equal("regex", result.ValidationType);
+ Assert.Equal("Example aaabbc", result.ErrorMessage);
+ Assert.Equal("^a+b+c$", result.ValidationParameters["pattern"]);
+ }
+
+ private static HttpContextBase GetContext(object formValues = null)
+ {
+ var context = new Mock<HttpContextBase>();
+ var request = new Mock<HttpRequestBase>();
+
+ var nameValueCollection = new NameValueCollection();
+ if (formValues != null)
+ {
+ foreach (var prop in formValues.GetType().GetProperties())
+ {
+ nameValueCollection.Add(prop.Name, prop.GetValue(formValues, null).ToString());
+ }
+ }
+ request.SetupGet(c => c.Form).Returns(nameValueCollection);
+ context.SetupGet(c => c.Request).Returns(request.Object);
+
+ return context.Object;
+ }
+
+ private static ValidationContext GetValidationContext(HttpContextBase httpContext, string memberName)
+ {
+ return new ValidationContext(httpContext, null, null) { MemberName = memberName };
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/WebPage/ApplicationStartPageTest.cs b/test/System.Web.WebPages.Test/WebPage/ApplicationStartPageTest.cs
new file mode 100644
index 00000000..89729996
--- /dev/null
+++ b/test/System.Web.WebPages.Test/WebPage/ApplicationStartPageTest.cs
@@ -0,0 +1,166 @@
+using System.IO;
+using System.Reflection;
+using System.Text;
+using System.Web.WebPages.TestUtils;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.WebPages.Test
+{
+ public class ApplicationStartPageTest
+ {
+ [Fact]
+ public void StartPageBasicTest()
+ {
+ AppDomainUtils.RunInSeparateAppDomain(() =>
+ {
+ var page = new ApplicationStartPageTest().CreateStartPage(p =>
+ {
+ p.AppState["x"] = "y";
+ p.WriteLiteral("test");
+ });
+ page.ExecuteInternal();
+ Assert.Equal("y", page.ApplicationState["x"]);
+ Assert.Equal("test", ApplicationStartPage.Markup.ToHtmlString());
+ });
+ }
+
+ [Fact]
+ public void StartPageDynamicAppStateBasicTest()
+ {
+ AppDomainUtils.RunInSeparateAppDomain(() =>
+ {
+ var page = new ApplicationStartPageTest().CreateStartPage(p =>
+ {
+ p.App.x = "y";
+ p.WriteLiteral("test");
+ });
+ page.ExecuteInternal();
+ Assert.Equal("y", page.ApplicationState["x"]);
+ Assert.Equal("y", page.App["x"]);
+ Assert.Equal("y", page.App.x);
+ Assert.Equal("test", ApplicationStartPage.Markup.ToHtmlString());
+ });
+ }
+
+ [Fact]
+ public void ExceptionTest()
+ {
+ AppDomainUtils.RunInSeparateAppDomain(() =>
+ {
+ var msg = "This is an error message";
+ var e = new InvalidOperationException(msg);
+ var page = new ApplicationStartPageTest().CreateStartPage(p => { throw e; });
+ var ex = Assert.Throws<HttpException>(() => page.ExecuteStartPage());
+ Assert.Equal(msg, ex.InnerException.Message);
+ Assert.Equal(e, ApplicationStartPage.Exception);
+ });
+ }
+
+ [Fact]
+ public void HtmlEncodeTest()
+ {
+ AppDomainUtils.RunInSeparateAppDomain(() =>
+ {
+ // Set HideRequestResponse to true to simulate the condition in IIS 7/7.5
+ var context = new HttpContext(new HttpRequest("default.cshtml", "http://localhost/default.cshtml", null), new HttpResponse(new StringWriter(new StringBuilder())));
+ var hideRequestResponse = typeof(HttpContext).GetField("HideRequestResponse", BindingFlags.NonPublic | BindingFlags.Instance);
+ hideRequestResponse.SetValue(context, true);
+
+ HttpContext.Current = context;
+ var page = new ApplicationStartPageTest().CreateStartPage(p => { p.Write("test"); });
+ page.ExecuteStartPage();
+ });
+ }
+
+ [Fact]
+ public void GetVirtualPathTest()
+ {
+ var page = new MockStartPage();
+ Assert.Equal(ApplicationStartPage.StartPageVirtualPath, page.VirtualPath);
+ }
+
+ [Fact]
+ public void SetVirtualPathTest()
+ {
+ var page = new MockStartPage();
+ Assert.Throws<NotSupportedException>(() => { page.VirtualPath = "~/hello.cshtml"; });
+ }
+
+ [Fact]
+ public void ExecuteStartPageTest()
+ {
+ AppDomainUtils.RunInSeparateAppDomain(() =>
+ {
+ var startPage = new MockStartPage() { ExecuteAction = p => p.AppState["x"] = "y" };
+ var objectFactory = GetMockVirtualPathFactory(startPage);
+ ApplicationStartPage.ExecuteStartPage(new WebPageHttpApplication(),
+ p => { },
+ objectFactory,
+ new string[] { "cshtml", "vbhtml" });
+ Assert.Equal("y", startPage.ApplicationState["x"]);
+ });
+ }
+
+ [Fact]
+ public void ExecuteStartPageDynamicAppStateTest()
+ {
+ AppDomainUtils.RunInSeparateAppDomain(() =>
+ {
+ var startPage = new MockStartPage() { ExecuteAction = p => p.App.x = "y" };
+ var objectFactory = GetMockVirtualPathFactory(startPage);
+ ApplicationStartPage.ExecuteStartPage(new WebPageHttpApplication(),
+ p => { },
+ objectFactory,
+ new string[] { "cshtml", "vbhtml" });
+ Assert.Equal("y", startPage.ApplicationState["x"]);
+ Assert.Equal("y", startPage.App.x);
+ Assert.Equal("y", startPage.App["x"]);
+ });
+ }
+
+ public class MockStartPage : ApplicationStartPage
+ {
+ public Action<ApplicationStartPage> ExecuteAction { get; set; }
+ public HttpApplicationStateBase ApplicationState = new HttpApplicationStateWrapper(Activator.CreateInstance(typeof(HttpApplicationState), true) as HttpApplicationState);
+
+ public override void Execute()
+ {
+ ExecuteAction(this);
+ }
+
+ public override HttpApplicationStateBase AppState
+ {
+ get { return ApplicationState; }
+ }
+
+ public void ExecuteStartPage()
+ {
+ ExecuteStartPage(new WebPageHttpApplication(),
+ p => { },
+ GetMockVirtualPathFactory(this),
+ new string[] { "cshtml", "vbhtml" });
+ }
+ }
+
+ public MockStartPage CreateStartPage(Action<ApplicationStartPage> action)
+ {
+ var startPage = new MockStartPage() { ExecuteAction = action };
+ return startPage;
+ }
+
+ public sealed class WebPageHttpApplication : HttpApplication
+ {
+ }
+
+ private static IVirtualPathFactory GetMockVirtualPathFactory(ApplicationStartPage page)
+ {
+ var mockFactory = new Mock<IVirtualPathFactory>();
+ mockFactory.Setup(c => c.Exists(It.IsAny<string>())).Returns<string>(_ => true);
+ mockFactory.Setup(c => c.CreateInstance(It.IsAny<string>())).Returns<string>(_ => page);
+
+ return mockFactory.Object;
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/WebPage/BrowserHelpersTest.cs b/test/System.Web.WebPages.Test/WebPage/BrowserHelpersTest.cs
new file mode 100644
index 00000000..9ee6e998
--- /dev/null
+++ b/test/System.Web.WebPages.Test/WebPage/BrowserHelpersTest.cs
@@ -0,0 +1,285 @@
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Web.Configuration;
+using Moq;
+using Xunit;
+
+namespace System.Web.WebPages.Test
+{
+ public class BrowserHelpersTest
+ {
+ [Fact]
+ public void GetOverriddenUserAgentGetsUserAgentFromHttpContext()
+ {
+ // Arrange
+ Mock<HttpContextBase> context = CookieBrowserOverrideStoreTest.CreateCookieContext();
+ string testUserAgent = "testUserAgent";
+
+ // Act
+ context.Object.SetOverriddenBrowser(testUserAgent);
+ Assert.Equal(testUserAgent, context.Object.GetOverriddenUserAgent());
+ context.Object.Response.Cookies.Clear();
+ context.Object.Request.Cookies.Clear();
+
+ // Assert
+ Assert.Equal(testUserAgent, context.Object.GetOverriddenUserAgent());
+ }
+
+ [Fact]
+ public void GetOverriddenUserAgentFallsBackToStoreUserAgent()
+ {
+ // Arrange
+ string testUserAgent = "testUserAgent";
+ HttpCookie existingOverrideCookie = new HttpCookie(CookieBrowserOverrideStore.BrowserOverrideCookieName, testUserAgent);
+ HttpContextBase context = CookieBrowserOverrideStoreTest.CreateCookieContext(requestCookie: existingOverrideCookie).Object;
+
+ // Act & Assert
+ Assert.Equal(testUserAgent, context.GetOverriddenUserAgent());
+ }
+
+ [Fact]
+ public void GetOverriddenUserAgentDefaultsToRequestUserAgent()
+ {
+ // Arrange
+ Mock<HttpContextBase> context = CookieBrowserOverrideStoreTest.CreateCookieContext();
+ context.Setup(c => c.Request.UserAgent).Returns("requestUserAgent");
+
+ // Act & Assert
+ Assert.Equal("requestUserAgent", context.Object.GetOverriddenUserAgent());
+ }
+
+ [Fact]
+ public void SetOverriddenBrowserWithBrowserOverrideSetBrowserMobile()
+ {
+ // Arrange
+ Mock<HttpContextBase> context = CookieBrowserOverrideStoreTest.CreateCookieContext();
+ context.Setup(c => c.Request.Browser.IsMobileDevice).Returns(false);
+
+ // Act
+ context.Object.SetOverriddenBrowser(BrowserOverride.Mobile);
+
+ // Assert
+ Assert.True(context.Object.GetOverriddenBrowser(CreateBrowserThroughFactory).IsMobileDevice);
+ }
+
+ [Fact]
+ public void SetOverriddenBrowserWithUnsupportedBrowserOverrideClearsBrowser()
+ {
+ // Arrange
+ Mock<HttpContextBase> context = CookieBrowserOverrideStoreTest.CreateCookieContext();
+ Mock<HttpBrowserCapabilitiesBase> requestBrowser = new Mock<HttpBrowserCapabilitiesBase>();
+ context.Setup(c => c.Request.Browser).Returns(requestBrowser.Object);
+
+ // Act & Assert
+ context.Object.SetOverriddenBrowser(BrowserOverride.Mobile);
+ Assert.NotSame(requestBrowser.Object, context.Object.GetOverriddenBrowser(CreateBrowserThroughFactory));
+
+ context.Object.SetOverriddenBrowser((BrowserOverride)(-1));
+ Assert.Same(requestBrowser.Object, context.Object.GetOverriddenBrowser(CreateBrowserThroughFactory));
+ }
+
+ [Fact]
+ public void SetOverriddenBrowserWithBrowserOverrideSetBrowserDesktop()
+ {
+ // Arrange
+ Mock<HttpContextBase> context = CookieBrowserOverrideStoreTest.CreateCookieContext();
+ context.Setup(c => c.Request.Browser.IsMobileDevice).Returns(true);
+
+ // Act
+ context.Object.SetOverriddenBrowser(BrowserOverride.Desktop);
+
+ // Assert
+ Assert.False(context.Object.GetOverriddenBrowser(CreateBrowserThroughFactory).IsMobileDevice);
+ }
+
+ [Fact]
+ public void SetOverriddenBrowserWithStringOverrideSetBrowser()
+ {
+ // Arrange
+ Mock<HttpContextBase> context = CookieBrowserOverrideStoreTest.CreateCookieContext();
+ context.Setup(c => c.Request.Browser.IsMobileDevice).Returns(false);
+
+ // Act
+ context.Object.SetOverriddenBrowser("Mozilla/5.0 (iPhone; U; CPU iPhone OS 3_0 like Mac OS X; en-us) AppleWebKit/528.18 (KHTML, like Gecko) Version/4.0 Mobile/7A341 Safari/528.16");
+
+ // Assert
+ Assert.True(context.Object.GetOverriddenBrowser(CreateBrowserThroughFactory).IsMobileDevice);
+ }
+
+ [Fact]
+ public void SetOverriddenBrowserClearsCachedBrowser()
+ {
+ // Arrange
+ Mock<HttpContextBase> context = CookieBrowserOverrideStoreTest.CreateCookieContext();
+ context.Setup(c => c.Request.UserAgent).Returns("testUserAgent");
+
+ // Act
+ context.Object.SetOverriddenBrowser(BrowserOverride.Mobile);
+ context.Object.GetOverriddenBrowser(CreateBrowserThroughFactory);
+
+ // If the browser is generated this will throw an exception because we are going through the provider.
+ // We must be getting the cached overridden browser.
+ context.Object.GetOverriddenBrowser();
+ context.Object.SetOverriddenBrowser("testUserAgent");
+
+ // Assert
+
+ // The browser has been cleared from HttpContext and the user agent has been set to the original user agent.
+ // Otherwise we will either get the cached browser or an exception when trying to generate the browser from the
+ // mobile user agent.
+ Assert.Null(context.Object.GetOverriddenBrowser());
+ }
+
+ [Fact]
+ public void SetOverridenBrowserInSameOverrideClassClearsOverridenBrowser()
+ {
+ // Arrange
+ Mock<HttpContextBase> context = CookieBrowserOverrideStoreTest.CreateCookieContext();
+ context.Setup(c => c.Request.Browser.IsMobileDevice).Returns(true);
+ context.Setup(c => c.Request.UserAgent).Returns("sampleUserAgent");
+
+ // Act
+ context.Object.SetOverriddenBrowser(BrowserOverride.Desktop);
+ context.Object.SetOverriddenBrowser(BrowserOverride.Mobile);
+
+ // Assert
+ Assert.Equal("sampleUserAgent", context.Object.GetOverriddenUserAgent());
+ }
+
+ [Fact]
+ public void GetOverriddenBrowserGetsBrowserFromHttpContext()
+ {
+ // Arrange
+ Mock<HttpContextBase> context = CookieBrowserOverrideStoreTest.CreateCookieContext();
+
+ // Act
+ context.Object.SetOverriddenBrowser(BrowserOverride.Mobile);
+ context.Object.GetOverriddenBrowser(CreateBrowserThroughFactory);
+
+ // Assert
+
+ // If the browser is generated this will throw an exception because we are going through the provider.
+ // We must be getting the cached overridden browser.
+ Assert.True(context.Object.GetOverriddenBrowser().IsMobileDevice);
+ }
+
+ [Fact]
+ public void GetOverriddenBrowserWithStoredBrowserAndNoBrowserInContext()
+ {
+ // Arrange
+ string mobileUserAgent = "Mozilla/5.0 (iPhone; U; CPU iPhone OS 3_0 like Mac OS X; en-us) AppleWebKit/528.18 (KHTML, like Gecko) Version/4.0 Mobile/7A341 Safari/528.16";
+ HttpCookie existingOverrideCookie = new HttpCookie(CookieBrowserOverrideStore.BrowserOverrideCookieName, mobileUserAgent);
+ HttpContextBase context = CookieBrowserOverrideStoreTest.CreateCookieContext(requestCookie: existingOverrideCookie).Object;
+
+ // Act & Assert
+ Assert.True(context.GetOverriddenBrowser(CreateBrowserThroughFactory).IsMobileDevice);
+ }
+
+ [Fact]
+ public void GetOverriddenBrowserDefaultsToRequestBrowser()
+ {
+ // Arrange
+ Mock<HttpContextBase> context = CookieBrowserOverrideStoreTest.CreateCookieContext();
+ Mock<HttpBrowserCapabilitiesBase> currentBrowser = new Mock<HttpBrowserCapabilitiesBase>();
+ context.Setup(c => c.Request.Browser).Returns(currentBrowser.Object);
+
+ // Act & Assert
+ Assert.Same(currentBrowser.Object, context.Object.GetOverriddenBrowser());
+ }
+
+ [Fact]
+ public void ClearOverriddenBrowserClearsSetBrowser()
+ {
+ // Arrange
+ Mock<HttpContextBase> context = CookieBrowserOverrideStoreTest.CreateCookieContext();
+ Mock<HttpBrowserCapabilitiesBase> requestBrowser = new Mock<HttpBrowserCapabilitiesBase>();
+ context.Setup(c => c.Request.Browser).Returns(requestBrowser.Object);
+
+ // Act & Assert
+ context.Object.SetOverriddenBrowser(BrowserOverride.Mobile);
+ Assert.NotSame(requestBrowser.Object, context.Object.GetOverriddenBrowser(CreateBrowserThroughFactory));
+
+ context.Object.ClearOverriddenBrowser();
+ Assert.Same(requestBrowser.Object, context.Object.GetOverriddenBrowser(CreateBrowserThroughFactory));
+ }
+
+ [Fact]
+ public void ClearOverriddenBrowserWithNoSetBrowser()
+ {
+ // Arrange
+ Mock<HttpContextBase> context = CookieBrowserOverrideStoreTest.CreateCookieContext();
+ Mock<HttpBrowserCapabilitiesBase> requestBrowser = new Mock<HttpBrowserCapabilitiesBase>();
+ context.Setup(c => c.Request.Browser).Returns(requestBrowser.Object);
+
+ // Act & Assert
+ context.Object.ClearOverriddenBrowser();
+ Assert.Same(requestBrowser.Object, context.Object.GetOverriddenBrowser(CreateBrowserThroughFactory));
+ }
+
+ [Fact]
+ public void GetVaryByCustomStringVariesBySetOverriddenBrowserMobile()
+ {
+ // Arrange
+ Mock<HttpContextBase> context = CookieBrowserOverrideStoreTest.CreateCookieContext();
+ Mock<HttpBrowserCapabilitiesBase> currentBrowser = new Mock<HttpBrowserCapabilitiesBase>();
+ currentBrowser.Setup(c => c.IsMobileDevice).Returns(true);
+ context.Setup(c => c.Request.Browser).Returns(currentBrowser.Object);
+
+ // Act
+ string originalBrowserType = context.Object.GetVaryByCustomStringForOverriddenBrowser(CreateBrowserThroughFactory);
+
+ context.Object.SetOverriddenBrowser(BrowserOverride.Desktop);
+ string deskTopBrowserType = context.Object.GetVaryByCustomStringForOverriddenBrowser(CreateBrowserThroughFactory);
+
+ context.Object.SetOverriddenBrowser(BrowserOverride.Mobile);
+ string mobileBrowserType = context.Object.GetVaryByCustomStringForOverriddenBrowser(CreateBrowserThroughFactory);
+
+ // Assert
+ Assert.Equal(originalBrowserType, mobileBrowserType);
+ Assert.NotEqual(originalBrowserType, deskTopBrowserType);
+ Assert.NotEqual(mobileBrowserType, deskTopBrowserType);
+ }
+
+ [Fact]
+ public void GetVaryByCustomStringVariesBySetOverriddenBrowserDesktop()
+ {
+ // Arrange
+ Mock<HttpContextBase> context = CookieBrowserOverrideStoreTest.CreateCookieContext();
+ Mock<HttpBrowserCapabilitiesBase> currentBrowser = new Mock<HttpBrowserCapabilitiesBase>();
+ currentBrowser.Setup(c => c.IsMobileDevice).Returns(false);
+ context.Setup(c => c.Request.Browser).Returns(currentBrowser.Object);
+
+ // Act
+ string originalBrowserType = context.Object.GetVaryByCustomStringForOverriddenBrowser(CreateBrowserThroughFactory);
+
+ context.Object.SetOverriddenBrowser(BrowserOverride.Mobile);
+ string mobileBrowserType = context.Object.GetVaryByCustomStringForOverriddenBrowser(CreateBrowserThroughFactory);
+
+ context.Object.SetOverriddenBrowser(BrowserOverride.Desktop);
+ string deskTopBrowserType = context.Object.GetVaryByCustomStringForOverriddenBrowser(CreateBrowserThroughFactory);
+
+ // Assert
+ Assert.NotEqual(originalBrowserType, mobileBrowserType);
+ Assert.Equal(originalBrowserType, deskTopBrowserType);
+ Assert.NotEqual(mobileBrowserType, deskTopBrowserType);
+ }
+
+ // We need to call the .ctor of SimpleWorkerRequest that depends on HttpRuntime so for unit testing
+ // simply create the browser capabilities by going directly through the factory.
+ private static HttpBrowserCapabilitiesBase CreateBrowserThroughFactory(string userAgent)
+ {
+ HttpBrowserCapabilities browser = new HttpBrowserCapabilities
+ {
+ Capabilities = new Dictionary<string, string>
+ {
+ { String.Empty, userAgent }
+ }
+ };
+
+ BrowserCapabilitiesFactory factory = new BrowserCapabilitiesFactory();
+ factory.ConfigureBrowserCapabilities(new NameValueCollection(), browser);
+
+ return new HttpBrowserCapabilitiesWrapper(browser);
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/WebPage/BrowserOverrideStoresTest.cs b/test/System.Web.WebPages.Test/WebPage/BrowserOverrideStoresTest.cs
new file mode 100644
index 00000000..ebe29318
--- /dev/null
+++ b/test/System.Web.WebPages.Test/WebPage/BrowserOverrideStoresTest.cs
@@ -0,0 +1,42 @@
+using Moq;
+using Xunit;
+
+namespace System.Web.WebPages.Test
+{
+ public class BrowserOverrideStoresTest
+ {
+ [Fact]
+ public void DefaultBrowserOverrideStoreIsCookie()
+ {
+ // Act & Assert
+ Assert.Equal(typeof(CookieBrowserOverrideStore), BrowserOverrideStores.Current.GetType());
+ }
+
+ [Fact]
+ public void SetBrowserOverrideStoreReturnsSetBrowserOverrideStore()
+ {
+ // Arrange
+ BrowserOverrideStores stores = new BrowserOverrideStores();
+ Mock<BrowserOverrideStore> store = new Mock<BrowserOverrideStore>();
+
+ // Act
+ stores.CurrentInternal = store.Object;
+
+ // Assert
+ Assert.Same(store.Object, stores.CurrentInternal);
+ }
+
+ [Fact]
+ public void SetBrowserOverrideStoreNullReturnsRequestBrowserOverrideStore()
+ {
+ //Arrange
+ BrowserOverrideStores stores = new BrowserOverrideStores();
+
+ // Act
+ stores.CurrentInternal = null;
+
+ // Assert
+ Assert.Equal(typeof(RequestBrowserOverrideStore), stores.CurrentInternal.GetType());
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/WebPage/BuildManagerExceptionUtilTest.cs b/test/System.Web.WebPages.Test/WebPage/BuildManagerExceptionUtilTest.cs
new file mode 100644
index 00000000..ca1c915c
--- /dev/null
+++ b/test/System.Web.WebPages.Test/WebPage/BuildManagerExceptionUtilTest.cs
@@ -0,0 +1,90 @@
+using System.Globalization;
+using System.Runtime.CompilerServices;
+using System.Web.WebPages.Resources;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.WebPages.Test
+{
+ public class UtilTest
+ {
+ [Fact]
+ public void IsUnsupportedExtensionError()
+ {
+ Assert.False(BuildManagerExceptionUtil.IsUnsupportedExtensionError(new HttpException("The following file could not be rendered because its extension \".txt\" might not be supported: \"myfile.txt\".")));
+
+ var e = CompilationUtil.GetBuildProviderException(".txt");
+ Assert.NotNull(e);
+ Assert.True(BuildManagerExceptionUtil.IsUnsupportedExtensionError(e));
+ }
+
+ [Fact]
+ public void IsUnsupportedExtensionThrowsTest()
+ {
+ var extension = ".txt";
+ var virtualPath = "Layout.txt";
+ var e = CompilationUtil.GetBuildProviderException(extension);
+
+ Assert.Throws<HttpException>(
+ () => { BuildManagerExceptionUtil.ThrowIfUnsupportedExtension(virtualPath, e); }, String.Format(CultureInfo.CurrentCulture, WebPageResources.WebPage_FileNotSupported, extension, virtualPath));
+ }
+
+ [Fact]
+ public void CodeDomDefinedExtensionThrowsTest()
+ {
+ var extension = ".js";
+ var virtualPath = "Layout.js";
+
+ Assert.Throws<HttpException>(
+ () => { BuildManagerExceptionUtil.ThrowIfCodeDomDefinedExtension(virtualPath, new HttpCompileException()); }, String.Format(CultureInfo.CurrentCulture, WebPageResources.WebPage_FileNotSupported, extension, virtualPath));
+ }
+
+ [Fact]
+ public void CodeDomDefinedExtensionDoesNotThrowTest()
+ {
+ var virtualPath = "Layout.txt";
+ // Should not throw an exception
+ BuildManagerExceptionUtil.ThrowIfCodeDomDefinedExtension(virtualPath, new HttpCompileException());
+ }
+ }
+
+ // Dummy class to simulate exception from CompilationUtil.GetBuildProviderTypeFromExtension
+ internal class CompilationUtil : IVirtualPathFactory
+ {
+ /// <remarks>
+ /// The method that consumes this exception walks the stack trace and uses the class name and method name to uniquely identify an exception.
+ /// In release build, the method is inlined causing the call site to appear as the method GetBuildProviderException which causes the test to fail.
+ /// These attributes prevent the compiler from inlining this method.
+ /// </remarks>
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
+ public static void GetBuildProviderTypeFromExtension(string extension)
+ {
+ throw new HttpException(extension);
+ }
+
+ public static HttpException GetBuildProviderException(string extension)
+ {
+ try
+ {
+ GetBuildProviderTypeFromExtension(extension);
+ }
+ catch (HttpException e)
+ {
+ return e;
+ }
+ return null;
+ }
+
+ public bool Exists(string virtualPath)
+ {
+ string extension = PathUtil.GetExtension(virtualPath);
+ GetBuildProviderTypeFromExtension(extension);
+ return false;
+ }
+
+ public object CreateInstance(string virtualPath)
+ {
+ throw new NotSupportedException();
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/WebPage/BuildManagerWrapperTest.cs b/test/System.Web.WebPages.Test/WebPage/BuildManagerWrapperTest.cs
new file mode 100644
index 00000000..ff0e9241
--- /dev/null
+++ b/test/System.Web.WebPages.Test/WebPage/BuildManagerWrapperTest.cs
@@ -0,0 +1,277 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Web.Hosting;
+using Microsoft.Internal.Web.Utils;
+using Moq;
+using Xunit;
+
+namespace System.Web.WebPages.Test
+{
+ public class BuildManagerWrapperTest
+ {
+ private const string _precompileConfigFileName = "~/PrecompiledApp.config";
+
+ [Fact]
+ public void CanCreateObjectFactoryReturnsFalseIfExtensionIsNotRegistered()
+ {
+ // Arrange
+ var buildManagerWrapper = CreateWrapperInstance();
+
+ // Act
+ bool supported = buildManagerWrapper.IsPathExtensionSupported("~/styles/index.css");
+
+ // Assert
+ Assert.False(supported);
+ }
+
+ [Fact]
+ public void CanCreateObjectFactoryReturnsFalseIfVirtualPathIsExtensionless()
+ {
+ // Arrange
+ var buildManagerWrapper = CreateWrapperInstance();
+
+ // Act
+ bool supported = buildManagerWrapper.IsPathExtensionSupported("~/default");
+
+ // Assert
+ Assert.False(supported);
+ }
+
+ [Fact]
+ public void CanCreateObjectFactoryReturnsFalseIfVirtualPathExtensionIsEmpty()
+ {
+ // Arrange
+ var buildManagerWrapper = CreateWrapperInstance();
+
+ // Act
+ bool supported = buildManagerWrapper.IsPathExtensionSupported("~/default.");
+
+ // Assert
+ Assert.False(supported);
+ }
+
+ [Fact]
+ public void CanCreateObjectFactoryReturnsTrueIfVirtualPathExtensionIsRegistered()
+ {
+ // Arrange
+ var buildManagerWrapper = CreateWrapperInstance();
+
+ // Act
+ bool supported = buildManagerWrapper.IsPathExtensionSupported("~/default.cshtml");
+
+ // Assert
+ Assert.True(supported);
+ }
+
+ [Fact]
+ public void CanCreateObjectFactoryPerformsCaseInsenitiveComparison()
+ {
+ // Arrange
+ var buildManagerWrapper = CreateWrapperInstance();
+
+ // Act
+ bool supported = buildManagerWrapper.IsPathExtensionSupported("~/default.CShTml");
+
+ // Assert
+ Assert.True(supported);
+ }
+
+ [Fact]
+ public void CanCreateObjectFactoryWorksForAllRegisteredExtensions()
+ {
+ // Arrange
+ var buildManagerWrapper = CreateWrapperInstance();
+
+ // Act
+ bool supported = buildManagerWrapper.IsPathExtensionSupported("~/default.vbHtml");
+
+ // Assert
+ Assert.True(supported);
+ }
+
+ [Fact]
+ public void IsNonPrecompiledAppReturnsFalseIfPrecompiledConfigFileDoesNotExist()
+ {
+ // Arrange
+ var vpp = new Mock<VirtualPathProvider>();
+ vpp.Setup(c => c.FileExists(It.IsAny<string>())).Returns(false);
+ var buildManagerWrapper = new BuildManagerWrapper(vpp.Object, GetVirtualPathUtility());
+
+ // Act
+ var isPrecompiled = buildManagerWrapper.IsNonUpdatablePrecompiledApp();
+
+ // Assert
+ Assert.False(isPrecompiled);
+ vpp.Verify();
+ }
+
+ [Fact]
+ public void IsNonPrecompiledAppReturnsFalseIfPrecompiledConfigFileIsNotValidXml()
+ {
+ // Arrange
+ var vpp = new Mock<VirtualPathProvider>();
+ vpp.Setup(c => c.FileExists(It.Is<string>(p => p.Equals(_precompileConfigFileName)))).Returns(true).Verifiable();
+ var file = new Mock<VirtualFile>();
+ vpp.Setup(c => c.GetFile(It.Is<string>(p => p.Equals(_precompileConfigFileName)))).Returns(GetFile("some random text that is clearly not xml!"));
+ var buildManagerWrapper = new BuildManagerWrapper(vpp.Object, GetVirtualPathUtility());
+
+ // Act
+ var isPrecompiled = buildManagerWrapper.IsNonUpdatablePrecompiledApp();
+
+ // Assert
+ Assert.False(isPrecompiled);
+ vpp.Verify();
+ }
+
+ [Fact]
+ public void IsNonPrecompiledAppReturnsFalseIfConfigFileDoesNotContainExpectedElements()
+ {
+ // Arrange
+ var fileContent = @"<?xml version=""1.0""?><configuration><appSettings></appSettings></configuration>";
+ var vpp = new Mock<VirtualPathProvider>();
+ vpp.Setup(c => c.FileExists(It.Is<string>(p => p.Equals(_precompileConfigFileName)))).Returns(true).Verifiable();
+ vpp.Setup(c => c.GetFile(It.Is<string>(p => p.Equals(_precompileConfigFileName)))).Returns(GetFile(fileContent)).Verifiable();
+ var buildManagerWrapper = new BuildManagerWrapper(vpp.Object, GetVirtualPathUtility());
+
+ // Act
+ var isPrecompiled = buildManagerWrapper.IsNonUpdatablePrecompiledApp();
+
+ // Assert
+ Assert.False(isPrecompiled);
+ vpp.Verify();
+ }
+
+ [Fact]
+ public void IsNonPrecompiledAppReturnsFalseIfAppIsUpdatable()
+ {
+ // Arrange
+ var fileContent = @"<precompiledApp version=""2"" updatable=""true""/>";
+ var vpp = new Mock<VirtualPathProvider>();
+ vpp.Setup(c => c.FileExists(It.Is<string>(p => p.Equals(_precompileConfigFileName)))).Returns(true).Verifiable();
+ vpp.Setup(c => c.GetFile(It.Is<string>(p => p.Equals(_precompileConfigFileName)))).Returns(GetFile(fileContent)).Verifiable();
+ var buildManagerWrapper = new BuildManagerWrapper(vpp.Object, GetVirtualPathUtility());
+
+ // Act
+ var isPrecompiled = buildManagerWrapper.IsNonUpdatablePrecompiledApp();
+
+ // Assert
+ Assert.False(isPrecompiled);
+ vpp.Verify();
+ }
+
+ [Fact]
+ public void IsNonPrecompiledAppReturnsTrueIfConfigFileIsValidAndIsAppIsNotUpdatable()
+ {
+ // Arrange
+ var fileContent = @"<precompiledApp version=""2"" updatable=""false""/>";
+ var vpp = new Mock<VirtualPathProvider>();
+ vpp.Setup(c => c.FileExists(It.Is<string>(p => p.Equals(_precompileConfigFileName)))).Returns(true).Verifiable();
+ vpp.Setup(c => c.GetFile(It.Is<string>(p => p.Equals(_precompileConfigFileName)))).Returns(GetFile(fileContent)).Verifiable();
+ var buildManagerWrapper = new BuildManagerWrapper(vpp.Object, GetVirtualPathUtility());
+
+ // Act
+ var isPrecompiled = buildManagerWrapper.IsNonUpdatablePrecompiledApp();
+
+ // Assert
+ Assert.True(isPrecompiled);
+ vpp.Verify();
+ }
+
+ [Fact]
+ public void ExistsUsesVppIfSiteIfNotPrecompiled()
+ {
+ // Arrange
+ var virtualPath = "~/default.cshtml";
+ var vpp = new Mock<VirtualPathProvider>();
+ vpp.Setup(c => c.FileExists(It.Is<string>(p => p.Equals(_precompileConfigFileName)))).Returns(false).Verifiable();
+ vpp.Setup(c => c.FileExists(It.Is<string>(p => p.Equals(virtualPath)))).Returns(true).Verifiable();
+ var buildManagerWrapper = new BuildManagerWrapper(vpp.Object, GetVirtualPathUtility());
+
+ // Act
+ var exists = buildManagerWrapper.Exists(virtualPath);
+
+ // Assert
+ vpp.Verify();
+ Assert.True(exists);
+ }
+
+ [Fact]
+ public void ExistsUsesVppIfSiteIfSiteIsPrecompiledButUpdateable()
+ {
+ // Arrange
+ var virtualPath = "~/default.cshtml";
+ var fileContent = @"<precompiledApp version=""2"" updatable=""true""/>";
+ var vpp = new Mock<VirtualPathProvider>();
+ vpp.Setup(c => c.FileExists(It.Is<string>(p => p.Equals(_precompileConfigFileName)))).Returns(true).Verifiable();
+ vpp.Setup(c => c.GetFile(It.Is<string>(p => p.Equals(_precompileConfigFileName)))).Returns(GetFile(fileContent)).Verifiable();
+ vpp.Setup(c => c.FileExists(It.Is<string>(p => p.Equals(virtualPath)))).Returns(true).Verifiable();
+ var buildManagerWrapper = new BuildManagerWrapper(vpp.Object, GetVirtualPathUtility());
+
+ // Act
+ var exists = buildManagerWrapper.Exists(virtualPath);
+
+ // Assert
+ vpp.Verify();
+ Assert.True(exists);
+ }
+
+ /// <remarks>
+ /// This method adds items to HttpRuntime.Cache.
+ /// </summary>
+ [Fact]
+ public void ExistsInPrecompiledReturnsFalseIfExtensionIsUnsupported()
+ {
+ // Arrange
+ var virtualPath = "~/ExistsInPrecompiledReturnsFalseIfExtensionIsUnsupported.jpeg";
+ var fileContent = @"<precompiledApp version=""2"" updatable=""false""/>";
+ var vpp = new Mock<VirtualPathProvider>();
+ vpp.Setup(c => c.FileExists(It.Is<string>(p => p.Equals(_precompileConfigFileName)))).Returns(true).Verifiable();
+ vpp.Setup(c => c.GetFile(It.Is<string>(p => p.Equals(_precompileConfigFileName)))).Returns(GetFile(fileContent)).Verifiable();
+ var buildManagerWrapper = new BuildManagerWrapper(vpp.Object, GetVirtualPathUtility());
+
+ // Act
+ var exists = buildManagerWrapper.Exists(virtualPath);
+
+ // Assert
+ vpp.Verify();
+ Assert.False(exists);
+ object cachedValue = HttpRuntime.Cache.Get(BuildManagerWrapper.KeyGuid + "_" + virtualPath);
+ Assert.NotNull(cachedValue);
+ Assert.False((bool)cachedValue.GetType().GetProperty("Exists").GetValue(cachedValue, null));
+ }
+
+ private static BuildManagerWrapper CreateWrapperInstance(IEnumerable<string> supportedExtensions = null)
+ {
+ return new BuildManagerWrapper(new Mock<VirtualPathProvider>().Object, GetVirtualPathUtility()) { SupportedExtensions = supportedExtensions ?? new[] { "cshtml", "vbhtml" } };
+ }
+
+ private static VirtualFile GetFile(string content)
+ {
+ var file = new Mock<VirtualFile>("test file");
+ file.Setup(f => f.Open()).Returns(() => new MemoryStream(Encoding.UTF8.GetBytes(content)));
+ return file.Object;
+ }
+
+ private static IVirtualPathUtility GetVirtualPathUtility()
+ {
+ var utility = new Mock<VirtualPathUtilityBase>();
+ utility.Setup(c => c.ToAbsolute(It.IsAny<string>())).Returns<string>(c => c);
+
+ return utility.Object;
+ }
+ }
+
+ public abstract class VirtualPathUtilityBase : IVirtualPathUtility
+ {
+ public virtual string Combine(string basePath, string relativePath)
+ {
+ throw new NotImplementedException();
+ }
+
+ public virtual string ToAbsolute(string virtualPath)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/WebPage/CookieBrowserOverrideStoreTest.cs b/test/System.Web.WebPages.Test/WebPage/CookieBrowserOverrideStoreTest.cs
new file mode 100644
index 00000000..6d5410f3
--- /dev/null
+++ b/test/System.Web.WebPages.Test/WebPage/CookieBrowserOverrideStoreTest.cs
@@ -0,0 +1,153 @@
+using System.Collections;
+using Moq;
+using Xunit;
+
+namespace System.Web.WebPages.Test
+{
+ public class CookieBrowserOverrideStoreTest
+ {
+ [Fact]
+ public void GetOverriddenUserAgentReturnsNullIfNoResponseOrRequestCookieIsSet()
+ {
+ // Arrange
+ CookieBrowserOverrideStore store = new CookieBrowserOverrideStore();
+
+ // Act & Assert
+ Assert.Null(store.GetOverriddenUserAgent(CreateCookieContext().Object));
+ }
+
+ [Fact]
+ public void GetOverriddenUserAgentReturnsUserAgentFromRequestCookieIfNoResponseCookie()
+ {
+ // Arrange
+ CookieBrowserOverrideStore store = new CookieBrowserOverrideStore();
+ HttpCookie existingOverrideCookie = new HttpCookie(CookieBrowserOverrideStore.BrowserOverrideCookieName, "existingRequestAgent");
+ HttpContextBase context = CreateCookieContext(requestCookie: existingOverrideCookie).Object;
+
+ // Act & Assert
+ Assert.Equal("existingRequestAgent", store.GetOverriddenUserAgent(context));
+ }
+
+ [Fact]
+ public void SetOverriddenUserAgentWithNoExistingCookie()
+ {
+ // Arrange
+ CookieBrowserOverrideStore store = new CookieBrowserOverrideStore();
+ HttpContextBase context = CreateCookieContext().Object;
+
+ // Act
+ store.SetOverriddenUserAgent(context, "setUserAgent");
+
+ // Assert
+ Assert.Equal("setUserAgent", store.GetOverriddenUserAgent(context));
+ }
+
+ [Fact]
+ public void SetOverriddenUserWithExistingRequestCookie()
+ {
+ // Arrange
+ CookieBrowserOverrideStore store = new CookieBrowserOverrideStore();
+ HttpCookie existingOverrideCookie = new HttpCookie(CookieBrowserOverrideStore.BrowserOverrideCookieName, "existingRequestAgent");
+ HttpContextBase context = CreateCookieContext(requestCookie: existingOverrideCookie).Object;
+
+ // Act
+ store.SetOverriddenUserAgent(context, "setUserAgent");
+
+ // Assert
+ Assert.Equal("setUserAgent", store.GetOverriddenUserAgent(context));
+ }
+
+ [Fact]
+ public void SetOverriddenUserWithExistingResponseCookie()
+ {
+ // Arrange
+ CookieBrowserOverrideStore store = new CookieBrowserOverrideStore();
+ HttpContextBase context = CreateCookieContext().Object;
+
+ // Act & Assert
+ store.SetOverriddenUserAgent(context, "testUserAgent");
+ Assert.Equal("testUserAgent", store.GetOverriddenUserAgent(context));
+
+ store.SetOverriddenUserAgent(context, "subsequentTestUserAgent");
+ Assert.Equal("subsequentTestUserAgent", store.GetOverriddenUserAgent(context));
+ }
+
+ [Fact]
+ public void SetOverriddenUserAgentNullWithRequestCookie()
+ {
+ // Arrange
+ CookieBrowserOverrideStore store = new CookieBrowserOverrideStore();
+ HttpCookie existingOverrideCookie = new HttpCookie(CookieBrowserOverrideStore.BrowserOverrideCookieName, "setUserAgent");
+ HttpContextBase context = CreateCookieContext(requestCookie: existingOverrideCookie).Object;
+
+ // Act
+ store.SetOverriddenUserAgent(context, null);
+
+ // Assert
+ Assert.Null(store.GetOverriddenUserAgent(context));
+ }
+
+ [Fact]
+ public void SetOverriddenUserAgentNullWithNoExistingCookie()
+ {
+ // Arrange
+ CookieBrowserOverrideStore store = new CookieBrowserOverrideStore();
+ HttpContextBase context = CreateCookieContext().Object;
+
+ // Act
+ store.SetOverriddenUserAgent(context, null);
+
+ // Assert
+ Assert.Null(store.GetOverriddenUserAgent(context));
+ }
+
+ [Fact]
+ public void SetOverriddenUserAgentSetsExpiration()
+ {
+ // Arrange
+ CookieBrowserOverrideStore store = new CookieBrowserOverrideStore();
+ CookieBrowserOverrideStore sessionStore = new CookieBrowserOverrideStore(daysToExpire: 0);
+ CookieBrowserOverrideStore longTermStore = new CookieBrowserOverrideStore(daysToExpire: 100);
+ CookieBrowserOverrideStore negativeTermStore = new CookieBrowserOverrideStore(daysToExpire: -1);
+
+ HttpContextBase context = CreateCookieContext().Object;
+
+ // Act & Assert
+ store.SetOverriddenUserAgent(context, "testUserAgent");
+ Assert.True(DateTime.Now.AddDays(6.5) < context.Response.Cookies[CookieBrowserOverrideStore.BrowserOverrideCookieName].Expires &&
+ context.Response.Cookies[CookieBrowserOverrideStore.BrowserOverrideCookieName].Expires < DateTime.Now.AddDays(7.5));
+
+ sessionStore.SetOverriddenUserAgent(context, "testUserAgent");
+ Assert.True(context.Response.Cookies[CookieBrowserOverrideStore.BrowserOverrideCookieName].Expires < DateTime.Now);
+
+ longTermStore.SetOverriddenUserAgent(context, "testUserAgent");
+ Assert.True(context.Response.Cookies[CookieBrowserOverrideStore.BrowserOverrideCookieName].Expires > DateTime.Now.AddDays(99));
+
+ negativeTermStore.SetOverriddenUserAgent(context, "testUserAgent");
+ Assert.True(context.Response.Cookies[CookieBrowserOverrideStore.BrowserOverrideCookieName].Expires < DateTime.Now);
+ }
+
+ internal static Mock<HttpContextBase> CreateCookieContext(HttpCookie requestCookie = null, HttpCookie responseCookie = null)
+ {
+ Mock<HttpContextBase> context = new Mock<HttpContextBase>();
+
+ HttpCookieCollection requestCookies = new HttpCookieCollection();
+ if (requestCookie != null)
+ {
+ requestCookies.Add(requestCookie);
+ }
+
+ HttpCookieCollection responseCookies = new HttpCookieCollection();
+ if (responseCookie != null)
+ {
+ responseCookies.Add(responseCookie);
+ }
+
+ context.Setup(c => c.Request.Cookies).Returns(requestCookies);
+ context.Setup(c => c.Response.Cookies).Returns(responseCookies);
+ context.Setup(c => c.Items).Returns(new Hashtable());
+
+ return context;
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/WebPage/DefaultDisplayModeTest.cs b/test/System.Web.WebPages.Test/WebPage/DefaultDisplayModeTest.cs
new file mode 100644
index 00000000..cc857fbf
--- /dev/null
+++ b/test/System.Web.WebPages.Test/WebPage/DefaultDisplayModeTest.cs
@@ -0,0 +1,139 @@
+using Moq;
+using Xunit;
+
+namespace System.Web.WebPages.Test
+{
+ public class DefaultDisplayModeTest
+ {
+ [Fact]
+ public void DefaultDisplayModeWithEmptySuffix()
+ {
+ // Arrange
+ DefaultDisplayMode displayMode = new DefaultDisplayMode();
+
+ // Act
+ DisplayInfo info = displayMode.GetDisplayInfo(new Mock<HttpContextBase>(MockBehavior.Strict).Object, "/bar/baz.aspx", virtualPath => true);
+
+ // Assert
+ Assert.Equal(String.Empty, displayMode.DisplayModeId);
+ Assert.Equal("/bar/baz.aspx", info.FilePath);
+ }
+
+ [Fact]
+ public void DefaultDisplayModeWithNullSuffix()
+ {
+ // Arrange
+ DefaultDisplayMode displayMode = new DefaultDisplayMode(null);
+
+ // Act
+ DisplayInfo info = displayMode.GetDisplayInfo(new Mock<HttpContextBase>(MockBehavior.Strict).Object, "/bar/baz.aspx", virtualPath => true);
+
+ // Assert
+ Assert.Equal(String.Empty, displayMode.DisplayModeId);
+ Assert.Equal("/bar/baz.aspx", info.FilePath);
+ }
+
+ [Fact]
+ public void DefaultDisplayModeSetSuffixAsId()
+ {
+ // Arrange
+ DefaultDisplayMode displayMode = new DefaultDisplayMode("foo");
+
+ // Act & Assert
+ Assert.Equal("foo", displayMode.DisplayModeId);
+ }
+
+ [Fact]
+ public void GetDisplayInfoWithNullOrEmptySuffixReturnsPathThatExists()
+ {
+ // Arrange
+ DefaultDisplayMode displayMode = new DefaultDisplayMode("foo");
+
+ // Act
+ DisplayInfo info = displayMode.GetDisplayInfo(new Mock<HttpContextBase>(MockBehavior.Strict).Object, "/bar/baz.aspx", virtualPath => true);
+
+ // Assert
+ Assert.IsType<DefaultDisplayMode>(info.DisplayMode);
+ Assert.Equal("/bar/baz.foo.aspx", info.FilePath);
+ }
+
+ [Fact]
+ public void GetDisplayInfoInsertsSuffixIntoVirtualPathThatExists()
+ {
+ // Arrange
+ DefaultDisplayMode displayMode = new DefaultDisplayMode("foo");
+
+ // Act
+ DisplayInfo info = displayMode.GetDisplayInfo(new Mock<HttpContextBase>(MockBehavior.Strict).Object, "/bar/baz.aspx", virtualPath => true);
+
+ // Assert
+ Assert.IsType<DefaultDisplayMode>(info.DisplayMode);
+ Assert.Equal("/bar/baz.foo.aspx", info.FilePath);
+ }
+
+ [Fact]
+ public void GetDisplayInfoInsertsSuffixBeforeLastSectionOfExtension()
+ {
+ // Arrange
+ DefaultDisplayMode displayMode = new DefaultDisplayMode("foo");
+
+ // Act
+ DisplayInfo info = displayMode.GetDisplayInfo(new Mock<HttpContextBase>(MockBehavior.Strict).Object, "/bar/baz.txt.aspx", virtualPath => true);
+
+ // Assert
+ Assert.Equal("/bar/baz.txt.foo.aspx", info.FilePath);
+ }
+
+ [Fact]
+ public void GetDisplayInfoSuffixesPathWithNoExtension()
+ {
+ // Arrange
+ DefaultDisplayMode displayMode = new DefaultDisplayMode("foo");
+
+ // Act
+ DisplayInfo info = displayMode.GetDisplayInfo(new Mock<HttpContextBase>(MockBehavior.Strict).Object, "/bar/baz", virtualPath => true);
+
+ // Assert
+ Assert.Equal("/bar/baz.foo", info.FilePath);
+ }
+
+ [Fact]
+ public void GetDisplayInfoWithNullVirtualPath()
+ {
+ // Arrange
+ DefaultDisplayMode displayMode = new DefaultDisplayMode("foo");
+
+ // Act
+ DisplayInfo info = displayMode.GetDisplayInfo(new Mock<HttpContextBase>(MockBehavior.Strict).Object, virtualPath: null, virtualPathExists: virtualPath => true);
+
+ // Assert
+ Assert.Null(info);
+ }
+
+ [Fact]
+ public void GetDisplayInfoSuffixesPathWithEmptyVirtualPath()
+ {
+ // Arrange
+ DefaultDisplayMode displayMode = new DefaultDisplayMode("foo");
+
+ // Act
+ DisplayInfo info = displayMode.GetDisplayInfo(new Mock<HttpContextBase>(MockBehavior.Strict).Object, String.Empty, virtualPath => true);
+
+ // Assert
+ Assert.Equal(String.Empty, info.FilePath);
+ }
+
+ [Fact]
+ public void GetDisplayInfoReturnsNullIfPathDoesNotExist()
+ {
+ // Arrange
+ DefaultDisplayMode displayMode = new DefaultDisplayMode("foo");
+
+ // Act
+ DisplayInfo info = displayMode.GetDisplayInfo(new Mock<HttpContextBase>(MockBehavior.Strict).Object, "/bar/baz", virtualPath => false);
+
+ // Assert
+ Assert.Null(info);
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/WebPage/DisplayInfoTest.cs b/test/System.Web.WebPages.Test/WebPage/DisplayInfoTest.cs
new file mode 100644
index 00000000..2772f501
--- /dev/null
+++ b/test/System.Web.WebPages.Test/WebPage/DisplayInfoTest.cs
@@ -0,0 +1,37 @@
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.WebPages.Test
+{
+ public class DisplayInfoTest
+ {
+ [Fact]
+ public void GuardClauses()
+ {
+ // Act & Assert
+ Assert.ThrowsArgumentNull(() => new DisplayInfo(filePath: null, displayMode: new Mock<IDisplayMode>().Object), "filePath");
+ Assert.ThrowsArgumentNull(() => new DisplayInfo("testPath", displayMode: null), "displayMode");
+ }
+
+ public void ConstructorSetsDisplayInfoProperties()
+ {
+ // Arrange
+ string path = "testPath";
+ IDisplayMode displayMode = new Mock<IDisplayMode>().Object;
+
+ // Act
+ DisplayInfo info = new DisplayInfo(path, displayMode);
+
+ // Assert
+ Assert.Equal(path, info.FilePath);
+ Assert.Equal(displayMode, info.DisplayMode);
+ }
+
+ public void ConstructorSetsEmptyFilePath()
+ {
+ // Act & Assert
+ Assert.Equal(String.Empty, new DisplayInfo(String.Empty, new Mock<IDisplayMode>().Object).FilePath);
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/WebPage/DisplayModeProviderTest.cs b/test/System.Web.WebPages.Test/WebPage/DisplayModeProviderTest.cs
new file mode 100644
index 00000000..6143910d
--- /dev/null
+++ b/test/System.Web.WebPages.Test/WebPage/DisplayModeProviderTest.cs
@@ -0,0 +1,229 @@
+using System.Collections.Generic;
+using System.Linq;
+using Moq;
+using Xunit;
+
+namespace System.Web.WebPages.Test
+{
+ public class DisplayModesTest
+ {
+ [Fact]
+ public void DefaultInstanceHasDefaultModes()
+ {
+ // Act
+ IList<IDisplayMode> displayModes = DisplayModeProvider.Instance.Modes;
+
+ // Assert
+ Assert.Equal(2, displayModes.Count);
+
+ Assert.IsType<DefaultDisplayMode>(displayModes[0]);
+ Assert.Equal(displayModes[0].DisplayModeId, DisplayModeProvider.MobileDisplayModeId);
+
+ Assert.IsType<DefaultDisplayMode>(displayModes[1]);
+ Assert.Equal(displayModes[1].DisplayModeId, DisplayModeProvider.DefaultDisplayModeId);
+ }
+
+ [Fact]
+ public void GetDisplayInfoForVirtualPathReturnsDisplayInfoFromFirstDisplayModeToHandleRequest()
+ {
+ // Arrange
+ var displayModeProvider = new DisplayModeProvider();
+ displayModeProvider.Modes.Clear();
+ var displayMode1 = new Mock<IDisplayMode>(MockBehavior.Strict);
+ displayMode1.Setup(d => d.CanHandleContext(It.IsAny<HttpContextBase>())).Returns(false);
+ displayModeProvider.Modes.Add(displayMode1.Object);
+
+ var displayMode2 = new Mock<IDisplayMode>(MockBehavior.Strict);
+ displayMode2.Setup(d => d.CanHandleContext(It.IsAny<HttpContextBase>())).Returns(false);
+ displayModeProvider.Modes.Add(displayMode2.Object);
+
+ var displayMode3 = new Mock<IDisplayMode>(MockBehavior.Strict);
+ displayMode3.Setup(d => d.CanHandleContext(It.IsAny<HttpContextBase>())).Returns(true);
+ displayModeProvider.Modes.Add(displayMode3.Object);
+
+ Mock<HttpContextBase> httpContext = new Mock<HttpContextBase>(MockBehavior.Strict);
+
+ var expected = new DisplayInfo("Foo", displayMode3.Object);
+ Func<string, bool> fileExists = path => true;
+ displayMode3.Setup(d => d.GetDisplayInfo(httpContext.Object, "path", fileExists)).Returns(expected);
+
+ // Act
+ DisplayInfo result = displayModeProvider.GetDisplayInfoForVirtualPath("path", httpContext.Object, fileExists, currentDisplayMode: null);
+
+ // Assert
+ Assert.Same(expected, result);
+ }
+
+ [Fact]
+ public void GetDisplayInfoForVirtualPathWithConsistentDisplayModeBeginsSearchAtCurrentDisplayMode()
+ {
+ // Arrange
+ Mock<HttpContextBase> httpContext = new Mock<HttpContextBase>(MockBehavior.Strict);
+
+ var displayModeProvider = new DisplayModeProvider();
+ displayModeProvider.Modes.Clear();
+ var displayMode1 = new Mock<IDisplayMode>(MockBehavior.Strict);
+ displayMode1.Setup(d => d.CanHandleContext(It.IsAny<HttpContextBase>())).Returns(true);
+ displayModeProvider.Modes.Add(displayMode1.Object);
+
+ var displayMode2 = new Mock<IDisplayMode>(MockBehavior.Strict);
+ displayMode2.Setup(d => d.CanHandleContext(It.IsAny<HttpContextBase>())).Returns(false);
+ displayModeProvider.Modes.Add(displayMode2.Object);
+
+ var displayMode3 = new Mock<IDisplayMode>(MockBehavior.Strict);
+ displayMode3.Setup(d => d.CanHandleContext(It.IsAny<HttpContextBase>())).Returns(true);
+ displayModeProvider.Modes.Add(displayMode3.Object);
+
+ var displayInfo = new DisplayInfo("Foo", displayMode3.Object);
+ Func<string, bool> fileExists = path => true;
+ displayMode3.Setup(d => d.GetDisplayInfo(httpContext.Object, "path", fileExists)).Returns(displayInfo);
+
+ // Act
+ DisplayInfo result = displayModeProvider.GetDisplayInfoForVirtualPath("path", httpContext.Object, fileExists, currentDisplayMode: displayMode2.Object,
+ requireConsistentDisplayMode: true);
+
+ // Assert
+ Assert.Same(displayInfo, result);
+ }
+
+ [Fact]
+ public void GetDisplayInfoForVirtualPathWithoutConsistentDisplayModeIgnoresCurrentDisplayMode()
+ {
+ // Arrange
+ Mock<HttpContextBase> httpContext = new Mock<HttpContextBase>(MockBehavior.Strict);
+ var displayModeProvider = new DisplayModeProvider();
+ displayModeProvider.Modes.Clear();
+ var displayMode1 = new Mock<IDisplayMode>(MockBehavior.Strict);
+ displayMode1.Setup(d => d.CanHandleContext(It.IsAny<HttpContextBase>())).Returns(true);
+ displayModeProvider.Modes.Add(displayMode1.Object);
+
+ var displayMode2 = new Mock<IDisplayMode>(MockBehavior.Strict);
+ displayMode2.Setup(d => d.CanHandleContext(It.IsAny<HttpContextBase>())).Returns(false);
+ displayModeProvider.Modes.Add(displayMode2.Object);
+
+ var displayMode3 = new Mock<IDisplayMode>(MockBehavior.Strict);
+ displayMode3.Setup(d => d.CanHandleContext(It.IsAny<HttpContextBase>())).Returns(true);
+ displayModeProvider.Modes.Add(displayMode3.Object);
+
+ var displayInfo = new DisplayInfo("Foo", displayMode3.Object);
+ Func<string, bool> fileExists = path => true;
+ displayMode1.Setup(d => d.GetDisplayInfo(httpContext.Object, "path", fileExists)).Returns(displayInfo);
+
+ // Act
+ DisplayInfo result = displayModeProvider.GetDisplayInfoForVirtualPath("path", httpContext.Object, fileExists, currentDisplayMode: displayMode1.Object,
+ requireConsistentDisplayMode: false);
+
+ // Assert
+ Assert.Same(displayInfo, result);
+ }
+
+ [Fact]
+ public void GetDisplayModesForRequestReturnsNullIfNoDisplayModesHandleRequest()
+ {
+ // Arrange
+ Mock<HttpContextBase> httpContext = new Mock<HttpContextBase>(MockBehavior.Strict);
+ var displayModeProvider = new DisplayModeProvider();
+ displayModeProvider.Modes.Clear();
+ var displayMode1 = new Mock<IDisplayMode>(MockBehavior.Strict);
+ displayMode1.Setup(d => d.CanHandleContext(It.IsAny<HttpContextBase>())).Returns(false);
+ displayModeProvider.Modes.Add(displayMode1.Object);
+
+ var displayMode2 = new Mock<IDisplayMode>(MockBehavior.Strict);
+ displayMode2.Setup(d => d.CanHandleContext(It.IsAny<HttpContextBase>())).Returns(false);
+ displayModeProvider.Modes.Add(displayMode2.Object);
+
+ var displayMode3 = new Mock<IDisplayMode>(MockBehavior.Strict);
+ displayMode3.Setup(d => d.CanHandleContext(It.IsAny<HttpContextBase>())).Returns(false);
+ displayModeProvider.Modes.Add(displayMode3.Object);
+
+ // Act
+ DisplayInfo displayModeForRequest = displayModeProvider.GetDisplayInfoForVirtualPath("path", httpContext.Object, path => false, currentDisplayMode: null);
+
+ // Assert
+ Assert.Null(displayModeForRequest);
+ }
+
+ [Fact]
+ public void GetAvailableDisplayModesForContextWithRestrictingPageElements()
+ {
+ // Arrange
+ Mock<HttpContextBase> httpContext = new Mock<HttpContextBase>(MockBehavior.Strict);
+ var displayModeProvider = new DisplayModeProvider();
+ displayModeProvider.Modes.Clear();
+ var displayMode1 = new Mock<IDisplayMode>(MockBehavior.Strict);
+ displayMode1.Setup(d => d.CanHandleContext(It.IsAny<HttpContextBase>())).Returns(true);
+ displayModeProvider.Modes.Add(displayMode1.Object);
+
+ var displayMode2 = new Mock<IDisplayMode>(MockBehavior.Strict);
+ displayMode2.Setup(d => d.CanHandleContext(It.IsAny<HttpContextBase>())).Returns(false);
+ displayModeProvider.Modes.Add(displayMode2.Object);
+
+ var displayMode3 = new Mock<IDisplayMode>(MockBehavior.Strict);
+ displayMode3.Setup(d => d.CanHandleContext(It.IsAny<HttpContextBase>())).Returns(true);
+ displayModeProvider.Modes.Add(displayMode3.Object);
+
+ // Act
+ var availableDisplayModes = displayModeProvider.GetAvailableDisplayModesForContext(httpContext.Object, displayMode2.Object, requireConsistentDisplayMode: true).ToList();
+
+ // Assert
+ Assert.Equal(1, availableDisplayModes.Count);
+ Assert.Equal(displayMode3.Object, availableDisplayModes[0]);
+ }
+
+ [Fact]
+ public void GetAvailableDisplayModesForContextWithoutRestrictingPageElements()
+ {
+ // Arrange
+ Mock<HttpContextBase> httpContext = new Mock<HttpContextBase>(MockBehavior.Strict);
+ var displayModeProvider = new DisplayModeProvider();
+ displayModeProvider.Modes.Clear();
+
+ var displayMode1 = new Mock<IDisplayMode>(MockBehavior.Strict);
+ displayMode1.Setup(d => d.CanHandleContext(It.IsAny<HttpContextBase>())).Returns(true);
+ displayModeProvider.Modes.Add(displayMode1.Object);
+
+ var displayMode2 = new Mock<IDisplayMode>(MockBehavior.Strict);
+ displayMode2.Setup(d => d.CanHandleContext(It.IsAny<HttpContextBase>())).Returns(false);
+ displayModeProvider.Modes.Add(displayMode2.Object);
+
+ var displayMode3 = new Mock<IDisplayMode>(MockBehavior.Strict);
+ displayMode3.Setup(d => d.CanHandleContext(It.IsAny<HttpContextBase>())).Returns(true);
+ displayModeProvider.Modes.Add(displayMode3.Object);
+
+ // Act
+ var availableDisplayModes = displayModeProvider.GetAvailableDisplayModesForContext(httpContext.Object, displayMode2.Object, requireConsistentDisplayMode: false).ToList();
+
+ // Assert
+ Assert.Equal(2, availableDisplayModes.Count);
+ Assert.Same(displayMode1.Object, availableDisplayModes[0]);
+ Assert.Same(displayMode3.Object, availableDisplayModes[1]);
+ }
+
+ [Fact]
+ public void GetAvailableDisplayModesReturnsOnlyModesThatCanHandleContext()
+ {
+ // Arrange
+ Mock<HttpContextBase> httpContext = new Mock<HttpContextBase>(MockBehavior.Strict);
+ var displayModeProvider = new DisplayModeProvider();
+ displayModeProvider.Modes.Clear();
+ var displayMode1 = new Mock<IDisplayMode>(MockBehavior.Strict);
+ displayMode1.Setup(d => d.CanHandleContext(It.IsAny<HttpContextBase>())).Returns(false);
+ displayModeProvider.Modes.Add(displayMode1.Object);
+
+ var displayMode2 = new Mock<IDisplayMode>(MockBehavior.Strict);
+ displayMode2.Setup(d => d.CanHandleContext(It.IsAny<HttpContextBase>())).Returns(true);
+ displayModeProvider.Modes.Add(displayMode2.Object);
+
+ var displayMode3 = new Mock<IDisplayMode>(MockBehavior.Strict);
+ displayMode3.Setup(d => d.CanHandleContext(It.IsAny<HttpContextBase>())).Returns(false);
+ displayModeProvider.Modes.Add(displayMode3.Object);
+
+ // Act
+ var availableDisplayModes = displayModeProvider.GetAvailableDisplayModesForContext(httpContext.Object, displayMode1.Object, requireConsistentDisplayMode: false).ToList();
+
+ // Assert
+ Assert.Equal(1, availableDisplayModes.Count);
+ Assert.Equal(displayMode2.Object, availableDisplayModes[0]);
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/WebPage/DynamicHttpApplicationStateTest.cs b/test/System.Web.WebPages.Test/WebPage/DynamicHttpApplicationStateTest.cs
new file mode 100644
index 00000000..6241e88f
--- /dev/null
+++ b/test/System.Web.WebPages.Test/WebPage/DynamicHttpApplicationStateTest.cs
@@ -0,0 +1,75 @@
+using System.Web.WebPages.Resources;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.WebPages.Test
+{
+ public class DynamicHttpApplicationStateTest
+ {
+ private static HttpApplicationStateBase CreateAppStateInstance()
+ {
+ return new HttpApplicationStateWrapper((HttpApplicationState)Activator.CreateInstance(typeof(HttpApplicationState), true));
+ }
+
+ [Fact]
+ public void DynamicTest()
+ {
+ HttpApplicationStateBase appState = CreateAppStateInstance();
+ dynamic d = new DynamicHttpApplicationState(appState);
+ d["x"] = "y";
+ Assert.Equal("y", d.x);
+ Assert.Equal("y", d[0]);
+ d.a = "b";
+ Assert.Equal("b", d["a"]);
+ d.Foo = "bar";
+ Assert.Equal("bar", d.Foo);
+ Assert.Null(d.XYZ);
+ Assert.Null(d["xyz"]);
+ Assert.Throws<ArgumentOutOfRangeException>(() => { var x = d[5]; });
+ var a = d.Baz = 42;
+ Assert.Equal(42, a);
+ var b = d["test"] = 666;
+ Assert.Equal(666, b);
+ }
+
+ [Fact]
+ public void InvalidNumberOfIndexes()
+ {
+ Assert.Throws<ArgumentException>(() =>
+ {
+ HttpApplicationStateBase appState = CreateAppStateInstance();
+ dynamic d = new DynamicHttpApplicationState(appState);
+ d[1, 2] = 3;
+ }, WebPageResources.DynamicDictionary_InvalidNumberOfIndexes);
+
+ Assert.Throws<ArgumentException>(() =>
+ {
+ HttpApplicationStateBase appState = CreateAppStateInstance();
+ dynamic d = new DynamicHttpApplicationState(appState);
+ var x = d[1, 2];
+ }, WebPageResources.DynamicDictionary_InvalidNumberOfIndexes);
+ }
+
+ [Fact]
+ public void InvalidTypeWhenSetting()
+ {
+ Assert.Throws<ArgumentException>(() =>
+ {
+ HttpApplicationStateBase appState = CreateAppStateInstance();
+ dynamic d = new DynamicHttpApplicationState(appState);
+ d[new object()] = 3;
+ }, WebPageResources.DynamicHttpApplicationState_UseOnlyStringToSet);
+ }
+
+ [Fact]
+ public void InvalidTypeWhenGetting()
+ {
+ Assert.Throws<ArgumentException>(() =>
+ {
+ HttpApplicationStateBase appState = CreateAppStateInstance();
+ dynamic d = new DynamicHttpApplicationState(appState);
+ var x = d[new object()];
+ }, WebPageResources.DynamicHttpApplicationState_UseOnlyStringOrIntToGet);
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/WebPage/DynamicPageDataDictionaryTest.cs b/test/System.Web.WebPages.Test/WebPage/DynamicPageDataDictionaryTest.cs
new file mode 100644
index 00000000..09c29149
--- /dev/null
+++ b/test/System.Web.WebPages.Test/WebPage/DynamicPageDataDictionaryTest.cs
@@ -0,0 +1,243 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Web.WebPages.Resources;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.WebPages.Test
+{
+ public class DynamicPageDataDictionaryTest
+ {
+ [Fact]
+ public void DynamicTest()
+ {
+ dynamic d = new DynamicPageDataDictionary<dynamic>(new PageDataDictionary<dynamic>());
+ d["x"] = "y";
+ Assert.Equal("y", d.x);
+ d.a = "b";
+ Assert.Equal("b", d["a"]);
+ d[0] = "zero";
+ Assert.Equal("zero", d[0]);
+ d.Foo = "bar";
+ Assert.Equal("bar", d.Foo);
+ var a = d.Baz = 42;
+ Assert.Equal(42, a);
+ var b = d[new object()] = 666;
+ Assert.Equal(666, b);
+ }
+
+ [Fact]
+ public void CastToDictionaryTest()
+ {
+ dynamic d = new DynamicPageDataDictionary<dynamic>(new PageDataDictionary<dynamic>());
+ var typeCheckCast = d as IDictionary<object, dynamic>;
+ var directCast = (IDictionary<object, dynamic>)d;
+
+ Assert.NotNull(typeCheckCast);
+ Assert.NotNull(directCast);
+ }
+
+ [Fact]
+ public void AddTest()
+ {
+ dynamic d = new DynamicPageDataDictionary<dynamic>(new PageDataDictionary<dynamic>());
+ var item = new KeyValuePair<object, object>("x", 2);
+ d.Add(item);
+ Assert.True(d.Contains(item));
+ Assert.Equal(2, d.x);
+ Assert.Equal(2, d["x"]);
+ }
+
+ [Fact]
+ public void AddTest1()
+ {
+ dynamic d = new DynamicPageDataDictionary<dynamic>(new PageDataDictionary<dynamic>());
+ object key = "x";
+ object value = 1;
+ d.Add(key, value);
+ Assert.True(d.ContainsKey(key));
+ Assert.Equal(1, d[key]);
+ Assert.Equal(1, d.x);
+ }
+
+ [Fact]
+ public void ClearTest()
+ {
+ dynamic d = new DynamicPageDataDictionary<dynamic>(new PageDataDictionary<dynamic>());
+ d.x = 2;
+ d.Clear();
+ Assert.Equal(0, d.Count);
+ }
+
+ [Fact]
+ public void ContainsTest()
+ {
+ dynamic d = new DynamicPageDataDictionary<dynamic>(new PageDataDictionary<dynamic>());
+ var item = new KeyValuePair<object, object>("x", 1);
+ d.Add(item);
+ Assert.True(d.Contains(item));
+ var item2 = new KeyValuePair<object, object>("y", 2);
+ Assert.False(d.Contains(item2));
+ }
+
+ [Fact]
+ public void ContainsKeyTest()
+ {
+ dynamic d = new DynamicPageDataDictionary<dynamic>(new PageDataDictionary<dynamic>());
+ object key = "x";
+ Assert.False(d.ContainsKey(key));
+ d.Add(key, 1);
+ Assert.True(d.ContainsKey(key));
+ Assert.True(d.ContainsKey("x"));
+ }
+
+ [Fact]
+ public void CopyToTest()
+ {
+ dynamic d = new DynamicPageDataDictionary<dynamic>(new PageDataDictionary<dynamic>());
+ KeyValuePair<object, object>[] array = new KeyValuePair<object, object>[1];
+ d.Add("x", 1);
+ d.CopyTo(array, 0);
+ Assert.Equal(new KeyValuePair<object, object>("x", 1), array[0]);
+ }
+
+ [Fact]
+ public void GetEnumeratorTest()
+ {
+ dynamic d = new DynamicPageDataDictionary<dynamic>(new PageDataDictionary<dynamic>());
+ d.Add("x", 1);
+ var e = d.GetEnumerator();
+ e.MoveNext();
+ Assert.Equal(new KeyValuePair<object, object>("x", 1), e.Current);
+ }
+
+ [Fact]
+ public void RemoveTest()
+ {
+ dynamic d = new DynamicPageDataDictionary<dynamic>(new PageDataDictionary<dynamic>());
+ var key = "x";
+ d.Add(key, 1);
+ d.Remove(key);
+ Assert.False(d.ContainsKey(key));
+ }
+
+ [Fact]
+ public void RemoveTest1()
+ {
+ dynamic d = new DynamicPageDataDictionary<dynamic>(new PageDataDictionary<dynamic>());
+ var item = new KeyValuePair<object, object>("x", 2);
+ d.Add(item);
+ Assert.True(d.Contains(item));
+ d.Remove(item);
+ Assert.False(d.Contains(item));
+ }
+
+ [Fact]
+ public void GetEnumeratorTest1()
+ {
+ dynamic d = new DynamicPageDataDictionary<dynamic>(new PageDataDictionary<dynamic>());
+ d.Add("x", 1);
+ var e = ((IEnumerable)d).GetEnumerator();
+ e.MoveNext();
+ Assert.Equal(new KeyValuePair<object, object>("x", 1), e.Current);
+ }
+
+ [Fact]
+ public void TryGetValueTest()
+ {
+ dynamic d = new DynamicPageDataDictionary<dynamic>(new PageDataDictionary<dynamic>());
+ object key = "x";
+ d.Add(key, 1);
+ object value = null;
+ Assert.True(d.TryGetValue(key, out value));
+ Assert.Equal(1, value);
+ }
+
+ [Fact]
+ public void CountTest()
+ {
+ var d = new DynamicPageDataDictionary<dynamic>(new PageDataDictionary<dynamic>());
+ dynamic dyn = d;
+ Assert.IsType<int>(dyn.Count);
+ d.Add("x", 1);
+ Assert.Equal(1, d.Count);
+ d.Add("y", 2);
+ Assert.Equal(2, d.Count);
+ dyn.Count = "foo";
+ Assert.Equal("foo", d["Count"]);
+ Assert.Equal(3, d.Count);
+ }
+
+ [Fact]
+ public void IsReadOnlyTest()
+ {
+ PageDataDictionary<dynamic> dict = new PageDataDictionary<dynamic>();
+ var d = new DynamicPageDataDictionary<dynamic>(dict);
+ dynamic dyn = d;
+ Assert.IsType<bool>(dyn.IsReadOnly);
+ Assert.Equal(dict.IsReadOnly, d.IsReadOnly);
+ dyn.IsReadOnly = "foo";
+ Assert.Equal("foo", d["IsReadOnly"]);
+ Assert.Equal(dict.IsReadOnly, d.IsReadOnly);
+ Assert.Equal(dict.IsReadOnly, dyn.IsReadOnly);
+ }
+
+ [Fact]
+ public void ItemTest()
+ {
+ dynamic d = new DynamicPageDataDictionary<dynamic>(new PageDataDictionary<dynamic>());
+ d.Add("x", 1);
+ d.Add("y", 2);
+ Assert.Equal(1, d["x"]);
+ Assert.Equal(2, d["y"]);
+ }
+
+ [Fact]
+ public void KeysTest()
+ {
+ var d = new DynamicPageDataDictionary<dynamic>(new PageDataDictionary<dynamic>());
+ dynamic dyn = d;
+ Assert.IsAssignableFrom<ICollection<object>>(dyn.Keys);
+ d.Add("x", 1);
+ d.Add("y", 2);
+ Assert.True(d.Keys.Contains("x"));
+ Assert.True(d.Keys.Contains("y"));
+ Assert.Equal(2, d.Keys.Count);
+ dyn.Keys = "foo";
+ Assert.Equal("foo", dyn["Keys"]);
+ Assert.Equal(3, d.Count);
+ }
+
+ [Fact]
+ public void ValuesTest()
+ {
+ var d = new DynamicPageDataDictionary<dynamic>(new PageDataDictionary<dynamic>());
+ dynamic dyn = d;
+ Assert.IsAssignableFrom<ICollection<dynamic>>(dyn.Values);
+ d.Add("x", 1);
+ d.Add("y", 2);
+ Assert.True(d.Values.Contains(1));
+ Assert.True(d.Values.Contains(2));
+ Assert.Equal(2, d.Values.Count);
+ dyn.Values = "foo";
+ Assert.Equal("foo", dyn["Values"]);
+ Assert.Equal(3, d.Count);
+ }
+
+ [Fact]
+ public void InvalidNumberOfIndexes()
+ {
+ Assert.Throws<ArgumentException>(() =>
+ {
+ dynamic d = new DynamicPageDataDictionary<dynamic>(new PageDataDictionary<dynamic>());
+ d[1, 2] = 3;
+ }, WebPageResources.DynamicDictionary_InvalidNumberOfIndexes);
+
+ Assert.Throws<ArgumentException>(() =>
+ {
+ dynamic d = new DynamicPageDataDictionary<dynamic>(new PageDataDictionary<dynamic>());
+ var x = d[1, 2];
+ }, WebPageResources.DynamicDictionary_InvalidNumberOfIndexes);
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/WebPage/FileExistenceCacheTest.cs b/test/System.Web.WebPages.Test/WebPage/FileExistenceCacheTest.cs
new file mode 100644
index 00000000..b8d26cad
--- /dev/null
+++ b/test/System.Web.WebPages.Test/WebPage/FileExistenceCacheTest.cs
@@ -0,0 +1,95 @@
+using System.Linq;
+using System.Threading;
+using System.Web.Hosting;
+using Moq;
+using Xunit;
+
+namespace System.Web.WebPages.Test
+{
+ public class FileExistenceCacheTest
+ {
+ [Fact]
+ public void ConstructorTest()
+ {
+ var ms = 1000;
+ var cache = new FileExistenceCache(null);
+ Assert.Null(cache.VirtualPathProvider);
+
+ var vpp = new Mock<VirtualPathProvider>().Object;
+ cache = new FileExistenceCache(vpp);
+ Assert.Equal(vpp, cache.VirtualPathProvider);
+ Assert.Equal(ms, cache.MilliSecondsBeforeReset);
+
+ ms = 9999;
+ cache = new FileExistenceCache(vpp, ms);
+ Assert.Equal(vpp, cache.VirtualPathProvider);
+ Assert.Equal(ms, cache.MilliSecondsBeforeReset);
+ }
+
+ [Fact]
+ public void TimeExceededFalseTest()
+ {
+ var ms = 100000;
+ var cache = new FileExistenceCache(GetVpp(), ms);
+ Assert.False(cache.TimeExceeded);
+ }
+
+ [Fact]
+ public void TimeExceededTrueTest()
+ {
+ var ms = 5;
+ var cache = new FileExistenceCache(GetVpp(), ms);
+ Thread.Sleep(300);
+ Assert.True(cache.TimeExceeded);
+ }
+
+ [Fact]
+ public void ResetTest()
+ {
+ var cache = new FileExistenceCache(GetVpp());
+ var cacheInternal = cache.CacheInternal;
+ cache.Reset();
+ Assert.NotSame(cacheInternal, cache.CacheInternal);
+ }
+
+ [Fact]
+ public void FileExistsTest()
+ {
+ var path = "~/index.cshtml";
+ var cache = new FileExistenceCache(GetVpp(path));
+ Assert.True(cache.FileExists(path));
+ Assert.False(cache.FileExists("~/test.cshtml"));
+ }
+
+ [Fact]
+ public void FileExistsVppLaterTest()
+ {
+ var path = "~/index.cshtml";
+ var cache = new FileExistenceCache(GetVpp(path));
+ Assert.True(cache.FileExists(path));
+ Assert.False(cache.FileExists("~/test.cshtml"));
+ }
+
+ [Fact]
+ public void FileExistsTimeExceededTest()
+ {
+ var path = "~/index.cshtml";
+ Utils.SetupVirtualPathInAppDomain(path, "");
+
+ var cache = new FileExistenceCache(GetVpp(path));
+ var cacheInternal = cache.CacheInternal;
+ cache.MilliSecondsBeforeReset = 5;
+ Thread.Sleep(300);
+ Assert.True(cache.FileExists(path));
+ Assert.False(cache.FileExists("~/test.cshtml"));
+ Assert.NotEqual(cacheInternal, cache.CacheInternal);
+ }
+
+ private static VirtualPathProvider GetVpp(params string[] files)
+ {
+ var vpp = new Mock<VirtualPathProvider>();
+ vpp.Setup(c => c.FileExists(It.IsAny<string>())).Returns<string>(p => files.Contains(p, StringComparer.OrdinalIgnoreCase));
+ return vpp.Object;
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/WebPage/LayoutTest.cs b/test/System.Web.WebPages.Test/WebPage/LayoutTest.cs
new file mode 100644
index 00000000..0d78321b
--- /dev/null
+++ b/test/System.Web.WebPages.Test/WebPage/LayoutTest.cs
@@ -0,0 +1,714 @@
+using System.Globalization;
+using System.Web.WebPages.Resources;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.WebPages.Test
+{
+ public class LayoutTest
+ {
+ [Fact]
+ public void LayoutBasicTest()
+ {
+ var layoutPath = "~/Layout.cshtml";
+ LayoutBasicTestInternal(layoutPath);
+ }
+
+ [Fact]
+ public void RelativeLayoutPageTest()
+ {
+ var pagePath = "~/MyApp/index.cshtml";
+ var layoutPath = "~/MyFiles/Layout.cshtml";
+ var layoutPage = "../MyFiles/Layout.cshtml";
+ LayoutBasicTestInternal(layoutPath, pagePath, layoutPage);
+ }
+
+ [Fact]
+ public void AppRelativeLayoutPageTest()
+ {
+ var pagePath = "~/MyApp/index.cshtml";
+ var layoutPath = "~/MyFiles/Layout.cshtml";
+ var layoutPage = "~/MyFiles/Layout.cshtml";
+ LayoutBasicTestInternal(layoutPath, pagePath, layoutPage);
+ }
+
+ [Fact]
+ public void SourceFileWithLayoutPageTest()
+ {
+ // Arrange
+ var pagePath = "~/MyApp/index.cshtml";
+ var layoutPath = "~/MyFiles/Layout.cshtml";
+ var layoutPage = "~/MyFiles/Layout.cshtml";
+ var content = "hello world";
+ var title = "MyPage";
+ var page = CreatePageWithLayout(
+ p =>
+ {
+ p.PageData["Title"] = title;
+ p.WriteLiteral(content);
+ },
+ p =>
+ {
+ p.WriteLiteral(p.PageData["Title"]);
+ p.Write(p.RenderBody());
+ }, pagePath, layoutPath, layoutPage);
+ var request = new Mock<HttpRequestBase>();
+ request.SetupGet(c => c.Path).Returns("/myapp/index.cshtml");
+ request.SetupGet(c => c.RawUrl).Returns("http://localhost:8080/index.cshtml");
+ request.SetupGet(c => c.IsLocal).Returns(true);
+ request.Setup(c => c.MapPath(It.IsAny<string>())).Returns<string>(c => c);
+ request.Setup(c => c.Browser.IsMobileDevice).Returns(false);
+ request.Setup(c => c.Cookies).Returns(new HttpCookieCollection());
+
+ var result = Utils.RenderWebPage(page, request: request.Object);
+ Assert.Equal(2, page.PageContext.SourceFiles.Count);
+ Assert.True(page.PageContext.SourceFiles.Contains("~/MyApp/index.cshtml"));
+ Assert.True(page.PageContext.SourceFiles.Contains("~/MyFiles/Layout.cshtml"));
+ }
+
+ private static void LayoutBasicTestInternal(string layoutPath, string pagePath = "~/index.cshtml", string layoutPage = "Layout.cshtml")
+ {
+ // The page ~/index.cshtml does the following:
+ // PageData["Title"] = "MyPage";
+ // Layout = "Layout.cshtml";
+ // WriteLiteral("hello world");
+ //
+ // The layout page ~/Layout.cshtml does the following:
+ // WriteLiteral(Title);
+ // RenderBody();
+ //
+ // Expected rendered result is "MyPagehello world"
+
+ var content = "hello world";
+ var title = "MyPage";
+ var result = RenderPageWithLayout(
+ p =>
+ {
+ p.PageData["Title"] = title;
+ p.WriteLiteral(content);
+ },
+ p =>
+ {
+ p.WriteLiteral(p.PageData["Title"]);
+ p.Write(p.RenderBody());
+ },
+ pagePath, layoutPath, layoutPage);
+
+ Assert.Equal(title + content, result);
+ }
+
+ [Fact]
+ public void LayoutNestedTest()
+ {
+ // Testing nested layout pages
+ //
+ // The page ~/index.cshtml does the following:
+ // PageData["Title"] = "MyPage";
+ // Layout = "Layout1.cshtml";
+ // WriteLiteral("hello world");
+ //
+ // The first layout page ~/Layout1.cshtml does the following:
+ // Layout = "Layout2.cshtml";
+ // WriteLiteral("<layout1>");
+ // RenderBody();
+ // WriteLiteral("</layout1>");
+ //
+ // The second layout page ~/Layout2.cshtml does the following:
+ // WriteLiteral(Title);
+ // WriteLiteral("<layout2>");
+ // RenderBody();
+ // WriteLiteral("</layout2>");
+ //
+ // Expected rendered result is "MyPage<layout2><layout1>hello world</layout1></layout2>"
+
+ var layout2Path = "~/Layout2.cshtml";
+ var layout2 = Utils.CreatePage(
+ p =>
+ {
+ p.WriteLiteral(p.PageData["Title"]);
+ p.WriteLiteral("<layout2>");
+ p.Write(p.RenderBody());
+ p.WriteLiteral("</layout2>");
+ },
+ layout2Path);
+
+ var layout1Path = "~/Layout1.cshtml";
+ var layout1 = Utils.CreatePage(
+ p =>
+ {
+ p.Layout = "Layout2.cshtml";
+ p.WriteLiteral("<layout1>");
+ p.Write(p.RenderBody());
+ p.WriteLiteral("</layout1>");
+ },
+ layout1Path);
+
+ var page = Utils.CreatePage(
+ p =>
+ {
+ p.PageData["Title"] = "MyPage";
+ p.Layout = "Layout1.cshtml";
+ p.WriteLiteral("hello world");
+ });
+
+ Utils.AssignObjectFactoriesAndDisplayModeProvider(page, layout1, layout2);
+
+ var result = Utils.RenderWebPage(page);
+ Assert.Equal("MyPage<layout2><layout1>hello world</layout1></layout2>", result);
+ }
+
+ [Fact]
+ public void LayoutSectionsTest()
+ {
+ // Testing nested layout pages with sections
+ //
+ // The page ~/index.cshtml does the following:
+ // PageData["Title"] = "MyPage";
+ // Layout = "Layout1.cshtml";
+ // DefineSection("header1", () => {
+ // WriteLiteral("index header");
+ // });
+ // WriteLiteral("hello world");
+ // DefineSection("footer1", () => {
+ // WriteLiteral("index footer");
+ // });
+ //
+ // The first layout page ~/Layout1.cshtml does the following:
+ // Layout = "Layout2.cshtml";
+ // DefineSection("header2", () => {
+ // WriteLiteral("<layout1 header>");
+ // RenderSection("header1");
+ // WriteLiteral("</layout1 header>");
+ // });
+ // WriteLiteral("<layout1>");
+ // RenderBody();
+ // WriteLiteral("</layout1>");
+ // DefineSection("footer2", () => {
+ // WriteLiteral("<layout1 footer>");
+ // RenderSection("header2");
+ // WriteLiteral("</layout1 footer>");
+ // });
+ //
+ // The second layout page ~/Layout2.cshtml does the following:
+ // WriteLiteral(Title);
+ // WriteLiteral("\n<layout2 header>");
+ // RenderSection("header2");
+ // WriteLiteral("</layout2 header>\n");
+ // WriteLiteral("<layout2>");
+ // RenderBody();
+ // WriteLiteral("</layout2>\n");
+ // WriteLiteral("<layout2 footer>");
+ // RenderSection("footer");
+ // WriteLiteral("</layout2 footer>");
+ //
+ // Expected rendered result is:
+ // MyPage
+ // <layout2 header><layout1 header>index header</layout1 header></layout2 header>
+ // <layout2><layout1>hello world</layout1></layout2>"
+ // <layout2 footer><layout1 footer>index footer</layout1 footer></layout2 footer>
+
+ var layout2Path = "~/Layout2.cshtml";
+ var layout2 = Utils.CreatePage(
+ p =>
+ {
+ p.WriteLiteral(p.PageData["Title"]);
+ p.WriteLiteral("\r\n");
+ p.WriteLiteral("<layout2 header>");
+ p.Write(p.RenderSection("header2"));
+ p.WriteLiteral("</layout2 header>");
+ p.WriteLiteral("\r\n");
+
+ p.WriteLiteral("<layout2>");
+ p.Write(p.RenderBody());
+ p.WriteLiteral("</layout2>");
+ p.WriteLiteral("\r\n");
+
+ p.WriteLiteral("<layout2 footer>");
+ p.Write(p.RenderSection("footer2"));
+ p.WriteLiteral("</layout2 footer>");
+ },
+ layout2Path);
+
+ var layout1Path = "~/Layout1.cshtml";
+ var layout1 = Utils.CreatePage(
+ p =>
+ {
+ p.Layout = "Layout2.cshtml";
+ p.DefineSection("header2", () =>
+ {
+ p.WriteLiteral("<layout1 header>");
+ p.Write(p.RenderSection("header1"));
+ p.WriteLiteral("</layout1 header>");
+ });
+
+ p.WriteLiteral("<layout1>");
+ p.Write(p.RenderBody());
+ p.WriteLiteral("</layout1>");
+
+ p.DefineSection("footer2", () =>
+ {
+ p.WriteLiteral("<layout1 footer>");
+ p.Write(p.RenderSection("footer1"));
+ p.WriteLiteral("</layout1 footer>");
+ });
+ },
+ layout1Path);
+
+ var page = Utils.CreatePage(
+ p =>
+ {
+ p.PageData["Title"] = "MyPage";
+ p.Layout = "Layout1.cshtml";
+ p.DefineSection("header1", () => { p.WriteLiteral("index header"); });
+ p.WriteLiteral("hello world");
+ p.DefineSection("footer1", () => { p.WriteLiteral("index footer"); });
+ });
+
+ Utils.AssignObjectFactoriesAndDisplayModeProvider(page, layout1, layout2);
+
+ var result = Utils.RenderWebPage(page);
+ var expected = @"MyPage
+<layout2 header><layout1 header>index header</layout1 header></layout2 header>
+<layout2><layout1>hello world</layout1></layout2>
+<layout2 footer><layout1 footer>index footer</layout1 footer></layout2 footer>";
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void LayoutSectionsNestedNamesTest()
+ {
+ // Tests nested layout using the same section names at different levels.
+ //
+ // The page ~/index.cshtml does the following:
+ // Layout = "Layout1.cshtml";
+ // @section body {
+ // body in index
+ // }
+ //
+ // The page ~/layout1.cshtml does the following:
+ // Layout = "Layout2.cshtml";
+ // @section body {
+ // body in layout1
+ // @RenderSection("body")
+ // }
+ //
+ // The page ~/layout2.cshtml does the following:
+ // body in layout2
+ // @RenderSection("body")
+ //
+ // Expected rendered result is:
+ // body in layout2 body in layout1 body in index
+ var layout2Path = "~/Layout2.cshtml";
+ var layout2 = Utils.CreatePage(
+ p =>
+ {
+ p.WriteLiteral("body in layout2 ");
+ p.Write(p.RenderSection("body"));
+ },
+ layout2Path);
+ var layout1Path = "~/Layout1.cshtml";
+ var layout1 = Utils.CreatePage(
+ p =>
+ {
+ p.Layout = "Layout2.cshtml";
+ p.DefineSection("body", () =>
+ {
+ p.WriteLiteral("body in layout1 ");
+ p.Write(p.RenderSection("body"));
+ });
+ },
+ layout1Path);
+
+ var page = Utils.CreatePage(
+ p =>
+ {
+ p.Layout = "Layout1.cshtml";
+ p.DefineSection("body", () => { p.WriteLiteral("body in index"); });
+ });
+
+ Utils.AssignObjectFactoriesAndDisplayModeProvider(page, layout1, layout2);
+
+ var result = Utils.RenderWebPage(page);
+ var expected = "body in layout2 body in layout1 body in index";
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void CaseInsensitiveSectionNamesTest()
+ {
+ var page = CreatePageWithLayout(
+ p =>
+ {
+ p.Write("123");
+ p.DefineSection("abc", () => { p.Write("abc"); });
+ p.DefineSection("XYZ", () => { p.Write("xyz"); });
+ p.Write("456");
+ },
+ p =>
+ {
+ p.Write(p.RenderSection("AbC"));
+ p.Write(p.RenderSection("xyZ"));
+ p.Write(p.RenderBody());
+ });
+ var result = Utils.RenderWebPage(page);
+ var expected = "abcxyz123456";
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void MissingLayoutPageTest()
+ {
+ var layoutPage = "Layout.cshtml";
+ var page = Utils.CreatePage(
+ p =>
+ {
+ p.PageData["Title"] = "MyPage";
+ p.Layout = layoutPage;
+ });
+ var layoutPath1 = "~/Layout.cshtml";
+
+ Assert.Throws<HttpException>(() => Utils.RenderWebPage(page),
+ String.Format(CultureInfo.CurrentCulture, WebPageResources.WebPage_LayoutPageNotFound, layoutPage, layoutPath1));
+ }
+
+ [Fact]
+ public void RenderBodyAlreadyCalledTest()
+ {
+ // Layout page calls RenderBody more than once.
+ var page = CreatePageWithLayout(
+ p => { },
+ p =>
+ {
+ p.Write(p.RenderBody());
+ p.Write(p.RenderBody());
+ });
+
+ Assert.Throws<HttpException>(() => Utils.RenderWebPage(page), WebPageResources.WebPage_RenderBodyAlreadyCalled);
+ }
+
+ [Fact]
+ public void RenderBodyNotCalledTest()
+ {
+ // Page does not define any sections, but layout page does not call RenderBody
+ var layoutPath = "~/Layout.cshtml";
+ var page = CreatePageWithLayout(
+ p => { },
+ p => { },
+ layoutPath: layoutPath);
+
+ Assert.Throws<HttpException>(() => Utils.RenderWebPage(page),
+ String.Format(CultureInfo.CurrentCulture, WebPageResources.WebPage_RenderBodyNotCalled, layoutPath));
+ }
+
+ [Fact]
+ public void RenderBodyCalledDirectlyTest()
+ {
+ // A Page that is not a layout page calls the RenderBody method
+ var page = Utils.CreatePage(p => { p.RenderBody(); });
+ Assert.Throws<HttpException>(() => Utils.RenderWebPage(page),
+ String.Format(CultureInfo.CurrentCulture, WebPageResources.WebPage_CannotRequestDirectly, "~/index.cshtml", "RenderBody"));
+ }
+
+ [Fact]
+ public void RenderSectionCalledDirectlyTest()
+ {
+ // A Page that is not a layout page calls the RenderBody method
+ var page = Utils.CreatePage(p => { p.RenderSection(""); });
+ Assert.Throws<HttpException>(() => Utils.RenderWebPage(page),
+ String.Format(CultureInfo.CurrentCulture, WebPageResources.WebPage_CannotRequestDirectly, "~/index.cshtml", "RenderSection"));
+ }
+
+ [Fact]
+ public void SectionAlreadyDefinedTest()
+ {
+ // The page calls DefineSection more than once on the same name
+ var sectionName = "header";
+ var page = Utils.CreatePage(p =>
+ {
+ p.Layout = "Layout.cshtml";
+ p.DefineSection(sectionName, () => { });
+ p.DefineSection(sectionName, () => { });
+ });
+
+ Assert.Throws<HttpException>(() => Utils.RenderWebPage(page),
+ String.Format(CultureInfo.InvariantCulture, WebPageResources.WebPage_SectionAleadyDefined, sectionName));
+ }
+
+ [Fact]
+ public void SectionAlreadyDefinedCaseInsensitiveTest()
+ {
+ // The page calls DefineSection more than once on the same name but with different casing
+ var name1 = "section1";
+ var name2 = "SecTion1";
+
+ var page = Utils.CreatePage(p =>
+ {
+ p.Layout = "Layout.cshtml";
+ p.DefineSection(name1, () => { });
+ p.DefineSection(name2, () => { });
+ });
+
+ Assert.Throws<HttpException>(() => Utils.RenderWebPage(page),
+ String.Format(CultureInfo.InvariantCulture, WebPageResources.WebPage_SectionAleadyDefined, name2));
+ }
+
+ [Fact]
+ public void SectionNotDefinedTest()
+ {
+ // Layout page calls RenderSection on a name that has not been defined.
+ var sectionName = "NoSuchSection";
+ var page = CreatePageWithLayout(
+ p => { },
+ p => { p.Write(p.RenderSection(sectionName)); });
+
+ Assert.Throws<HttpException>(() => Utils.RenderWebPage(page),
+ String.Format(CultureInfo.InvariantCulture, WebPageResources.WebPage_SectionNotDefined, sectionName));
+ }
+
+ [Fact]
+ public void SectionAlreadyRenderedTest()
+ {
+ // Layout page calls RenderSection on the same name more than once.
+ var sectionName = "header";
+ var page = CreatePageWithLayout(
+ p =>
+ {
+ p.Layout = "Layout.cshtml";
+ p.DefineSection(sectionName, () => { });
+ },
+ p =>
+ {
+ p.Write(p.RenderSection(sectionName));
+ p.Write(p.RenderSection(sectionName));
+ });
+
+ Assert.Throws<HttpException>(() => Utils.RenderWebPage(page),
+ String.Format(CultureInfo.InvariantCulture, WebPageResources.WebPage_SectionAleadyRendered, sectionName));
+ }
+
+ [Fact]
+ public void SectionsNotRenderedTest()
+ {
+ // Layout page does not render all the defined sections.
+
+ var layoutPath = "~/Layout.cshtml";
+ var sectionName1 = "section1";
+ var sectionName2 = "section2";
+ var sectionName3 = "section3";
+ var sectionName4 = "section4";
+ var sectionName5 = "section5";
+ // A dummy section action that does nothing
+ SectionWriter sectionAction = () => { };
+
+ // The page defines 5 sections.
+ var page = CreatePageWithLayout(
+ p =>
+ {
+ p.DefineSection(sectionName1, sectionAction);
+ p.DefineSection(sectionName2, sectionAction);
+ p.DefineSection(sectionName3, sectionAction);
+ p.DefineSection(sectionName4, sectionAction);
+ p.DefineSection(sectionName5, sectionAction);
+ },
+ // The layout page renders only two of the sections
+ p =>
+ {
+ p.Write(p.RenderSection(sectionName2));
+ p.Write(p.RenderSection(sectionName4));
+ },
+ layoutPath: layoutPath);
+
+ var sectionsNotRendered = "section1; section3; section5";
+ Assert.Throws<HttpException>(() => Utils.RenderWebPage(page),
+ String.Format(CultureInfo.CurrentCulture, WebPageResources.WebPage_SectionsNotRendered, layoutPath, sectionsNotRendered));
+ }
+
+ [Fact]
+ public void SectionsNotRenderedRenderBodyTest()
+ {
+ // Layout page does not render all the defined sections, but it calls RenderBody.
+ var layoutPath = "~/Layout.cshtml";
+ var sectionName1 = "section1";
+ var sectionName2 = "section2";
+ // A dummy section action that does nothing
+ SectionWriter sectionAction = () => { };
+
+ var page = CreatePageWithLayout(
+ p =>
+ {
+ p.DefineSection(sectionName1, sectionAction);
+ p.DefineSection(sectionName2, sectionAction);
+ },
+ // The layout page only calls RenderBody
+ p => { p.Write(p.RenderBody()); },
+ layoutPath: layoutPath);
+
+ var sectionsNotRendered = "section1; section2";
+ Assert.Throws<HttpException>(() => Utils.RenderWebPage(page),
+ String.Format(CultureInfo.CurrentCulture, WebPageResources.WebPage_SectionsNotRendered, layoutPath, sectionsNotRendered));
+ }
+
+ [Fact]
+ public void InvalidPageTypeTest()
+ {
+ var layoutPath = "~/Layout.js";
+ var contents = "hello world";
+ var page = Utils.CreatePage(p =>
+ {
+ p.Layout = layoutPath;
+ p.Write(contents);
+ });
+ var layoutPage = new object();
+
+ var objectFactory = new Mock<IVirtualPathFactory>();
+ objectFactory.Setup(c => c.Exists(It.IsAny<string>())).Returns<string>(p => layoutPath.Equals(p, StringComparison.OrdinalIgnoreCase));
+ objectFactory.Setup(c => c.CreateInstance(It.IsAny<string>())).Returns<string>(_ => layoutPage as WebPageBase);
+ page.VirtualPathFactory = objectFactory.Object;
+
+ Assert.Throws<HttpException>(() => Utils.RenderWebPage(page),
+ String.Format(CultureInfo.CurrentCulture, WebPageResources.WebPage_InvalidPageType, layoutPath));
+
+ Assert.Throws<HttpException>(() => Utils.RenderWebPage(page),
+ String.Format(CultureInfo.CurrentCulture, WebPageResources.WebPage_InvalidPageType, layoutPath));
+ }
+
+ [Fact]
+ public void ValidPageTypeTest()
+ {
+ var layoutPath = "~/Layout.js";
+ var contents = "hello world";
+ var page = Utils.CreatePage(p =>
+ {
+ p.Layout = layoutPath;
+ p.Write(contents);
+ });
+ var layoutPage = Utils.CreatePage(p => p.WriteLiteral(p.RenderBody()), layoutPath);
+
+ Utils.AssignObjectFactoriesAndDisplayModeProvider(page, layoutPage);
+
+ Assert.Equal(contents, Utils.RenderWebPage(page));
+ }
+
+ [Fact]
+ public void IsSectionDefinedTest()
+ {
+ // Tests for the IsSectionDefined method
+
+ // Only sections named section1 and section3 are defined.
+ var page = CreatePageWithLayout(
+ p =>
+ {
+ p.DefineSection("section1", () => { });
+ p.DefineSection("section3", () => { });
+ },
+ p =>
+ {
+ p.Write(p.RenderSection("section1"));
+ p.Write(p.RenderSection("section3"));
+ p.Write("section1: " + p.IsSectionDefined("section1") + "; ");
+ p.Write("section2: " + p.IsSectionDefined("section2") + "; ");
+ p.Write("section3: " + p.IsSectionDefined("section3") + "; ");
+ p.Write("section4: " + p.IsSectionDefined("section4") + "; ");
+ });
+ var result = Utils.RenderWebPage(page);
+ var expected = "section1: True; section2: False; section3: True; section4: False; ";
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void OptionalSectionsTest()
+ {
+ // Only sections named section1 and section3 are defined.
+ var page = CreatePageWithLayout(
+ p =>
+ {
+ p.DefineSection("section1", () => { p.Write("section1 "); });
+ p.DefineSection("section3", () => { p.Write("section3"); });
+ },
+ p =>
+ {
+ p.Write(p.RenderSection("section1", required: false));
+ p.Write(p.RenderSection("section2", required: false));
+ p.Write(p.RenderSection("section3", required: false));
+ p.Write(p.RenderSection("section4", required: false));
+ });
+ var result = Utils.RenderWebPage(page);
+ var expected = "section1 section3";
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void PageDataTest()
+ {
+ // Layout page uses items in PageData set by content page
+ var contents = "my contents";
+ var page = CreatePageWithLayout(
+ p =>
+ {
+ p.PageData["contents"] = contents;
+ p.Write(" body");
+ },
+ p =>
+ {
+ p.Write(p.PageData["contents"]);
+ p.Write(p.RenderBody());
+ });
+ var result = Utils.RenderWebPage(page);
+ var expected = contents + " body";
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void RenderPageAndLayoutPage()
+ {
+ //Dev10 bug 928341 - a page that has a layout page, and the page calls RenderPage should not cause an error
+ var layoutPagePath = "~/layout.cshtml";
+ var page = Utils.CreatePage(p =>
+ {
+ p.DefineSection("foo", () => { p.Write("This is foo"); });
+ p.Write(p.RenderPage("bar.cshtml"));
+ p.Layout = layoutPagePath;
+ });
+ var layoutPage = Utils.CreatePage(p =>
+ {
+ p.Write(p.RenderBody());
+ p.Write(" ");
+ p.Write(p.RenderSection("foo"));
+ }, layoutPagePath);
+
+ var subPage = Utils.CreatePage(p => p.Write("This is bar"), "~/bar.cshtml");
+ Utils.AssignObjectFactoriesAndDisplayModeProvider(page, layoutPage, subPage);
+
+ var result = Utils.RenderWebPage(page);
+ var expected = "This is bar This is foo";
+ Assert.Equal(expected, result);
+ }
+
+ public static string RenderPageWithLayout(Action<WebPage> pageExecuteAction, Action<WebPage> layoutExecuteAction,
+ string pagePath = "~/index.cshtml", string layoutPath = "~/Layout.cshtml", string layoutPage = "Layout.cshtml")
+ {
+ var page = CreatePageWithLayout(pageExecuteAction, layoutExecuteAction, pagePath, layoutPath, layoutPage);
+ return Utils.RenderWebPage(page);
+ }
+
+ public static MockPage CreatePageWithLayout(Action<WebPage> pageExecuteAction, Action<WebPage> layoutExecuteAction,
+ string pagePath = "~/index.cshtml", string layoutPath = "~/Layout.cshtml", string layoutPageName = "Layout.cshtml")
+ {
+ var page = Utils.CreatePage(
+ p =>
+ {
+ p.Layout = layoutPageName;
+ pageExecuteAction(p);
+ },
+ pagePath);
+ var layoutPage = Utils.CreatePage(
+ p => { layoutExecuteAction(p); },
+ layoutPath);
+
+ Utils.AssignObjectFactoriesAndDisplayModeProvider(layoutPage, page);
+
+ return page;
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/WebPage/PageDataDictionaryTest.cs b/test/System.Web.WebPages.Test/WebPage/PageDataDictionaryTest.cs
new file mode 100644
index 00000000..8c370b91
--- /dev/null
+++ b/test/System.Web.WebPages.Test/WebPage/PageDataDictionaryTest.cs
@@ -0,0 +1,236 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using Xunit;
+
+namespace System.Web.WebPages.Test
+{
+ public class PageDataDictionaryTest
+ {
+ [Fact]
+ public void PageDataDictionaryConstructorTest()
+ {
+ var d = new PageDataDictionary<dynamic>();
+ Assert.NotNull(d.Data);
+ }
+
+ [Fact]
+ public void AddTest()
+ {
+ var d = new PageDataDictionary<dynamic>();
+ var item = new KeyValuePair<object, object>("x", 2);
+ d.Add(item);
+ Assert.True(d.Data.Contains(item));
+ }
+
+ [Fact]
+ public void AddTest1()
+ {
+ var d = new PageDataDictionary<dynamic>();
+ object key = "x";
+ object value = 1;
+ d.Add(key, value);
+ Assert.True(d.Data.ContainsKey(key));
+ Assert.Equal(1, d.Data[key]);
+ // Use uppercase string key
+ Assert.Equal(1, d.Data["X"]);
+ }
+
+ [Fact]
+ public void ClearTest()
+ {
+ var d = new PageDataDictionary<dynamic>();
+ d.Add("x", 2);
+ d.Clear();
+ Assert.Equal(0, d.Data.Count);
+ }
+
+ [Fact]
+ public void ContainsTest()
+ {
+ var d = new PageDataDictionary<dynamic>();
+ var item = new KeyValuePair<object, object>("x", 1);
+ d.Add(item);
+ Assert.True(d.Contains(item));
+ var item2 = new KeyValuePair<object, object>("y", 2);
+ Assert.False(d.Contains(item2));
+ }
+
+ [Fact]
+ public void ContainsKeyTest()
+ {
+ var d = new PageDataDictionary<dynamic>();
+ object key = "x";
+ Assert.False(d.ContainsKey(key));
+ d.Add(key, 1);
+ Assert.True(d.ContainsKey(key));
+ Assert.True(d.ContainsKey("X"));
+ }
+
+ [Fact]
+ public void CopyToTest()
+ {
+ var d = new PageDataDictionary<dynamic>();
+ KeyValuePair<object, object>[] array = new KeyValuePair<object, object>[1];
+ d.Add("x", 1);
+ d.CopyTo(array, 0);
+ Assert.Equal(new KeyValuePair<object, object>("x", 1), array[0]);
+ }
+
+ [Fact]
+ public void GetEnumeratorTest()
+ {
+ var d = new PageDataDictionary<dynamic>();
+ d.Add("x", 1);
+ var e = d.GetEnumerator();
+ e.MoveNext();
+ Assert.Equal<object>(new KeyValuePair<object, object>("x", 1), e.Current);
+ }
+
+ [Fact]
+ public void RemoveTest()
+ {
+ var d = new PageDataDictionary<dynamic>();
+ var key = "x";
+ d.Add(key, 1);
+ d.Remove(key);
+ Assert.False(d.Data.ContainsKey(key));
+ }
+
+ [Fact]
+ public void RemoveTest1()
+ {
+ var d = new PageDataDictionary<dynamic>();
+ var item = new KeyValuePair<object, object>("x", 2);
+ d.Add(item);
+ Assert.True(d.Contains(item));
+ d.Remove(item);
+ Assert.False(d.Contains(item));
+ }
+
+ [Fact]
+ public void GetEnumeratorTest1()
+ {
+ var d = new PageDataDictionary<dynamic>();
+ d.Add("x", 1);
+ var e = ((IEnumerable)d).GetEnumerator();
+ e.MoveNext();
+ Assert.Equal(new KeyValuePair<object, object>("x", 1), e.Current);
+ }
+
+ [Fact]
+ public void TryGetValueTest()
+ {
+ var d = new PageDataDictionary<dynamic>();
+ object key = "x";
+ d.Add(key, 1);
+ object value = null;
+ Assert.True(d.TryGetValue(key, out value));
+ Assert.Equal(1, value);
+ }
+
+ [Fact]
+ public void CountTest()
+ {
+ var d = new PageDataDictionary<dynamic>();
+ d.Add("x", 1);
+ Assert.Equal(1, d.Count);
+ d.Add("y", 2);
+ Assert.Equal(2, d.Count);
+ }
+
+ [Fact]
+ public void IsReadOnlyTest()
+ {
+ var d = new PageDataDictionary<dynamic>();
+ Assert.Equal(d.Data.IsReadOnly, d.IsReadOnly);
+ }
+
+ [Fact]
+ public void ItemTest()
+ {
+ var d = new PageDataDictionary<dynamic>();
+ d.Add("x", 1);
+ d.Add("y", 2);
+ Assert.Equal(1, d["x"]);
+ Assert.Equal(2, d["y"]);
+ }
+
+ [Fact]
+ public void KeysTest()
+ {
+ var d = new PageDataDictionary<dynamic>();
+ d.Add("x", 1);
+ d.Add("y", 2);
+ Assert.True(d.Keys.Contains("x"));
+ Assert.True(d.Keys.Contains("y"));
+ Assert.Equal(2, d.Keys.Count);
+ }
+
+ [Fact]
+ public void ValuesTest()
+ {
+ var d = new PageDataDictionary<dynamic>();
+ d.Add("x", 1);
+ d.Add("y", 2);
+ Assert.True(d.Values.Contains(1));
+ Assert.True(d.Values.Contains(2));
+ Assert.Equal(2, d.Values.Count);
+ }
+
+ [Fact]
+ public void KeysReturnsNumericKeysIfPresent()
+ {
+ // Arrange
+ var d = new PageDataDictionary<string>();
+
+ // Act
+ d[100] = "foo";
+ d[200] = "bar";
+
+ // Assert
+ Assert.Equal(2, d.Keys.Count);
+ Assert.Equal(100, d.Keys.ElementAt(0));
+ Assert.Equal(200, d.Keys.ElementAt(1));
+ }
+
+ [Fact]
+ public void KeysReturnsUniqueSetOfValues()
+ {
+ // Act
+ var innerDict = new Dictionary<string, object>()
+ {
+ { "my-key", "value" },
+ { "test", "test-val" }
+ };
+ var dict = PageDataDictionary<dynamic>.CreatePageDataFromParameters(new PageDataDictionary<dynamic>(), innerDict);
+
+ // Act
+ dict.Add("my-key", "added-value");
+ dict["foo"] = "bar";
+
+ // Assert
+ Assert.Equal(4, dict.Keys.Count);
+ Assert.Equal("my-key", dict.Keys.ElementAt(0));
+ Assert.Equal("test", dict.Keys.ElementAt(1));
+ Assert.Equal(0, dict.Keys.ElementAt(2));
+ Assert.Equal("foo", dict.Keys.ElementAt(3));
+ Assert.Equal(dict[0], innerDict);
+ }
+
+ [Fact]
+ public void AddValueOverwritesIndexDictionaryIfKeyExists()
+ {
+ // Act
+ var dict = PageDataDictionary<dynamic>.CreatePageDataFromParameters(new PageDataDictionary<dynamic>(), new[] { "index-0-orig", "index-1" });
+
+ // Act
+ dict[0] = "index-0-new";
+
+ // Assert
+ Assert.Equal(2, dict.Keys.Count);
+ Assert.Equal("index-0-new", dict[0]);
+ Assert.Equal("index-1", dict[1]);
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/WebPage/RenderPageTest.cs b/test/System.Web.WebPages.Test/WebPage/RenderPageTest.cs
new file mode 100644
index 00000000..f9c84af2
--- /dev/null
+++ b/test/System.Web.WebPages.Test/WebPage/RenderPageTest.cs
@@ -0,0 +1,867 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Web.WebPages.Resources;
+using System.Web.WebPages.TestUtils;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.WebPages.Test
+{
+ public class RenderPageTest
+ {
+ [Fact]
+ public void RenderBasicTest()
+ {
+ // A simple page that does the following:
+ // @{ PageData["Title"] = "MyPage"; }
+ // @PageData["Title"]
+ // hello world
+ //
+ // Expected rendered result is "MyPagehello world"
+
+ var content = "hello world";
+ var title = "MyPage";
+ var result = Utils.RenderWebPage(
+ p =>
+ {
+ p.PageData["Title"] = title;
+ p.Write(p.PageData["Title"]);
+ p.Write(content);
+ });
+
+ Assert.Equal(title + content, result);
+ }
+
+ [Fact]
+ public void RenderDynamicDictionaryBasicTest()
+ {
+ // A simple page that does the following:
+ // @{ Page.Title = "MyPage"; }
+ // @Page.Title
+ // hello world
+ //
+ // Expected rendered result is "MyPagehello world"
+
+ var content = "hello world";
+ var title = "MyPage";
+ var result = Utils.RenderWebPage(
+ p =>
+ {
+ p.Page.Title = title;
+ p.Write(p.Page.Title);
+ p.Write(content);
+ });
+
+ Assert.Equal(title + content, result);
+ }
+
+ [Fact]
+ public void RenderPageBasicTest()
+ {
+ // ~/index.cshtml does the following:
+ // hello
+ // @RenderPage("subpage.cshtml")
+ //
+ // ~/subpage.cshtml does the following:
+ // world
+ //
+ // Expected output is "helloworld"
+
+ var result = Utils.RenderWebPageWithSubPage(
+ p =>
+ {
+ p.Write("hello");
+ p.Write(p.RenderPage("subpage.cshtml"));
+ },
+ p => { p.Write("world"); });
+ Assert.Equal("helloworld", result);
+ }
+
+ [Fact]
+ public void RenderPageAnonymousTypeTest()
+ {
+ // Test for passing an anonymous type object as an argument to RenderPage
+ //
+ // ~/index.cshtml does the following:
+ // @RenderPage("subpage.cshtml", new { HelloKey = "hellovalue", MyKey = "myvalue" })
+ //
+ // ~/subpage.cshtml does the following:
+ // @PageData["HelloKey"] @PageData["MyKey"] @Model.HelloKey @Model.MyKey
+ //
+ // Expected result: hellovalue myvalue hellovalue myvalue
+ var result = Utils.RenderWebPageWithSubPage(
+ p => { p.Write(p.RenderPage("subpage.cshtml", new { HelloKey = "hellovalue", MyKey = "myvalue" })); },
+ p =>
+ {
+ p.Write(p.PageData["HelloKey"]);
+ p.Write(" ");
+ p.Write(p.PageData["MyKey"]);
+ p.Write(" ");
+ p.Write(p.Model.HelloKey);
+ p.Write(" ");
+ p.Write(p.Model.MyKey);
+ });
+ Assert.Equal("hellovalue myvalue hellovalue myvalue", result);
+ }
+
+ [Fact]
+ public void RenderPageDynamicDictionaryAnonymousTypeTest()
+ {
+ // Test for passing an anonymous type object as an argument to RenderPage
+ //
+ // ~/index.cshtml does the following:
+ // @RenderPage("subpage.cshtml", new { HelloKey = "hellovalue", MyKey = "myvalue" })
+ //
+ // ~/subpage.cshtml does the following:
+ // @Page.HelloKey @Page.MyKey @Model.HelloKey @Model.MyKey
+ //
+ // Expected result: hellovalue myvalue hellovalue myvalue
+ var result = Utils.RenderWebPageWithSubPage(
+ p => { p.Write(p.RenderPage("subpage.cshtml", new { HelloKey = "hellovalue", MyKey = "myvalue" })); },
+ p =>
+ {
+ p.Write(p.Page.HelloKey);
+ p.Write(" ");
+ p.Write(p.Page.MyKey);
+ p.Write(" ");
+ p.Write(p.Model.HelloKey);
+ p.Write(" ");
+ p.Write(p.Model.MyKey);
+ });
+ Assert.Equal("hellovalue myvalue hellovalue myvalue", result);
+ }
+
+ [Fact]
+ public void RenderPageDictionaryTest()
+ {
+ // Test for passing a dictionary instance as an argument to RenderPage
+ //
+ // ~/index.cshtml does the following:
+ // @RenderPage("subpage.cshtml", new Dictionary<string, object>(){ { "foo", 1 }, { "bar", "hello"} })
+ //
+ // ~/subpage.cshtml does the following:
+ // @PageData["foo"] @PageData["bar"] @PageData[0]
+ //
+ // Expected result: 1 hello System.Collections.Generic.Dictionary`2[System.String,System.Object]
+
+ var result = Utils.RenderWebPageWithSubPage(
+ p => { p.Write(p.RenderPage("subpage.cshtml", new Dictionary<string, object>() { { "foo", 1 }, { "bar", "hello" } })); },
+ p =>
+ {
+ p.Write(p.PageData["foo"]);
+ p.Write(" ");
+ p.Write(p.PageData["bar"]);
+ p.Write(" ");
+ p.Write(p.PageData[0]);
+ });
+ Assert.Equal("1 hello System.Collections.Generic.Dictionary`2[System.String,System.Object]", result);
+ }
+
+ [Fact]
+ public void RenderPageDynamicDictionaryTest()
+ {
+ // Test for passing a dictionary instance as an argument to RenderPage
+ //
+ // ~/index.cshtml does the following:
+ // @RenderPage("subpage.cshtml", new Dictionary<string, object>(){ { "foo", 1 }, { "bar", "hello"} })
+ //
+ // ~/subpage.cshtml does the following:
+ // @Page.foo @Page.bar @Page[0]
+ //
+ // Expected result: 1 hello System.Collections.Generic.Dictionary`2[System.String,System.Object]
+
+ var result = Utils.RenderWebPageWithSubPage(
+ p => { p.Write(p.RenderPage("subpage.cshtml", new Dictionary<string, object>() { { "foo", 1 }, { "bar", "hello" } })); },
+ p =>
+ {
+ p.Write(p.Page.foo);
+ p.Write(" ");
+ p.Write(p.Page.bar);
+ p.Write(" ");
+ p.Write(p.Page[0]);
+ });
+ Assert.Equal("1 hello System.Collections.Generic.Dictionary`2[System.String,System.Object]", result);
+ }
+
+ [Fact]
+ public void RenderPageListTest()
+ {
+ // Test for passing a list of arguments to RenderPage
+ //
+ // ~/index.cshtml does the following:
+ // @RenderPage("subpage.cshtml", "hello", "world", 1, 2, 3)
+ //
+ // ~/subpage.cshtml does the following:
+ // @PageData[0] @PageData[1] @PageData[2] @PageData[3] @PageData[4]
+ //
+ // Expected result: hello world 1 2 3
+
+ var result = Utils.RenderWebPageWithSubPage(
+ p => { p.Write(p.RenderPage("subpage.cshtml", "hello", "world", 1, 2, 3)); },
+ p =>
+ {
+ p.Write(p.PageData[0]);
+ p.Write(" ");
+ p.Write(p.PageData[1]);
+ p.Write(" ");
+ p.Write(p.PageData[2]);
+ p.Write(" ");
+ p.Write(p.PageData[3]);
+ p.Write(" ");
+ p.Write(p.PageData[4]);
+ });
+ Assert.Equal("hello world 1 2 3", result);
+ }
+
+ [Fact]
+ public void RenderPageDynamicDictionaryListTest()
+ {
+ // Test for passing a list of arguments to RenderPage
+ //
+ // ~/index.cshtml does the following:
+ // @RenderPage("subpage.cshtml", "hello", "world", 1, 2, 3)
+ //
+ // ~/subpage.cshtml does the following:
+ // @Page[0] @Page[1] @Page[2] @Page[3] @Page[4]
+ //
+ // Expected result: hello world 1 2 3
+
+ var result = Utils.RenderWebPageWithSubPage(
+ p => { p.Write(p.RenderPage("subpage.cshtml", "hello", "world", 1, 2, 3)); },
+ p =>
+ {
+ p.Write(p.Page[0]);
+ p.Write(" ");
+ p.Write(p.Page[1]);
+ p.Write(" ");
+ p.Write(p.Page[2]);
+ p.Write(" ");
+ p.Write(p.Page[3]);
+ p.Write(" ");
+ p.Write(p.Page[4]);
+ });
+ Assert.Equal("hello world 1 2 3", result);
+ }
+
+ private class Person
+ {
+ public string FirstName { get; set; }
+ }
+
+ [Fact]
+ public void RenderPageDynamicValueTest()
+ {
+ // Test that PageData[key] returns a dynamic value.
+ // ~/index.cshtml does the following:
+ // @RenderPage("subpage.cshtml", new Person(){ FirstName="MyFirstName" })
+ //
+ // ~/subpage.cshtml does the following:
+ // @PageData[0].FirstName
+ //
+ // Expected result: MyFirstName
+ var result = Utils.RenderWebPageWithSubPage(
+ p => { p.Write(p.RenderPage("subpage.cshtml", new Person() { FirstName = "MyFirstName" })); },
+ p => { p.Write(p.PageData[0].FirstName); });
+ Assert.Equal("MyFirstName", result);
+ }
+
+ [Fact]
+ public void RenderPageDynamicDictionaryDynamicValueTest()
+ {
+ // Test that PageData[key] returns a dynamic value.
+ // ~/index.cshtml does the following:
+ // @RenderPage("subpage.cshtml", new Person(){ FirstName="MyFirstName" })
+ //
+ // ~/subpage.cshtml does the following:
+ // @Page[0].FirstName
+ //
+ // Expected result: MyFirstName
+ var result = Utils.RenderWebPageWithSubPage(
+ p => { p.Write(p.RenderPage("subpage.cshtml", new Person() { FirstName = "MyFirstName" })); },
+ p => { p.Write(p.Page[0].FirstName); });
+ Assert.Equal("MyFirstName", result);
+ }
+
+ [Fact]
+ public void PageDataSetByParentTest()
+ {
+ // Items set in the PageData should be accessible by the subpage
+ var result = Utils.RenderWebPageWithSubPage(
+ p =>
+ {
+ p.PageData["test"] = "hello";
+ p.Write(p.RenderPage("subpage.cshtml"));
+ },
+ p => { p.Write(p.PageData["test"]); });
+ Assert.Equal("hello", result);
+ }
+
+ [Fact]
+ public void DynamicDictionarySetByParentTest()
+ {
+ // Items set in the PageData should be accessible by the subpage
+ var result = Utils.RenderWebPageWithSubPage(
+ p =>
+ {
+ p.Page.test = "hello";
+ p.Write(p.RenderPage("subpage.cshtml"));
+ },
+ p => { p.Write(p.Page.test); });
+ Assert.Equal("hello", result);
+ }
+
+ [Fact]
+ public void OverridePageDataSetByParentTest()
+ {
+ // Items set in the PageData should be accessible by the subpage unless
+ // overriden by parameters passed into RenderPage, in which case the
+ // specified value should be used.
+ var result = Utils.RenderWebPageWithSubPage(
+ p =>
+ {
+ p.PageData["test"] = "hello";
+ p.Write(p.RenderPage("subpage.cshtml", new { Test = "world" }));
+ },
+ p =>
+ {
+ p.Write(p.PageData["test"]);
+ p.Write(p.PageData[0].Test);
+ });
+ Assert.Equal("worldworld", result);
+ }
+
+ [Fact]
+ public void OverrideDynamicDictionarySetByParentTest()
+ {
+ // Items set in the PageData should be accessible by the subpage unless
+ // overriden by parameters passed into RenderPage, in which case the
+ // specified value should be used.
+ var result = Utils.RenderWebPageWithSubPage(
+ p =>
+ {
+ p.PageData["test"] = "hello";
+ p.Write(p.RenderPage("subpage.cshtml", new { Test = "world" }));
+ },
+ p =>
+ {
+ p.Write(p.Page.test);
+ p.Write(p.Page[0].Test);
+ });
+ Assert.Equal("worldworld", result);
+ }
+
+ [Fact]
+ public void RenderPageMissingKeyTest()
+ {
+ // Test that using PageData with a missing key returns null
+ //
+ // ~/index.cshtml does the following:
+ // @RenderPage("subpage.cshtml", new Dictionary<string, object>(){ { "foo", 1 }, { "bar", "hello"} })
+ // @RenderPage("subpage.cshtml", "x", "y", "z")
+ //
+ // ~/subpage.cshtml does the following:
+ // @(PageData[1] ?? "null")
+ // @(PageData["bar"] ?? "null")
+ //
+ // Expected result: null hello y null
+
+ var result = Utils.RenderWebPageWithSubPage(
+ p =>
+ {
+ p.Write(p.RenderPage("subpage.cshtml", new Dictionary<string, object>() { { "foo", 1 }, { "bar", "hello" } }));
+ p.Write(p.RenderPage("subpage.cshtml", "x", "y", "z"));
+ },
+ p =>
+ {
+ p.Write(p.PageData[1] ?? "null1");
+ p.Write(" ");
+ p.Write(p.PageData["bar"] ?? "null2");
+ p.Write(" ");
+ });
+ Assert.Equal("null1 hello y null2 ", result);
+ }
+
+ [Fact]
+ public void RenderPageDynamicDictionaryMissingKeyTest()
+ {
+ // Test that using PageData with a missing key returns null
+ //
+ // ~/index.cshtml does the following:
+ // @RenderPage("subpage.cshtml", new Dictionary<string, object>(){ { "foo", 1 }, { "bar", "hello"} })
+ // @RenderPage("subpage.cshtml", "x", "y", "z")
+ //
+ // ~/subpage.cshtml does the following:
+ // @(Page[1] ?? "null")
+ // @(Page.bar ?? "null")
+ //
+ // Expected result: null hello y null
+
+ Action<WebPage> subPage = p =>
+ {
+ p.Write(p.Page[1] ?? "null1");
+ p.Write(" ");
+ p.Write(p.Page.bar ?? "null2");
+ p.Write(" ");
+ };
+ var result = Utils.RenderWebPageWithSubPage(
+ p => { p.Write(p.RenderPage("subpage.cshtml", new Dictionary<string, object>() { { "foo", 1 }, { "bar", "hello" } })); }, subPage);
+ Assert.Equal("null1 hello ", result);
+ result = Utils.RenderWebPageWithSubPage(
+ p => { p.Write(p.RenderPage("subpage.cshtml", "x", "y", "z")); }, subPage);
+ Assert.Equal("y null2 ", result);
+ }
+
+ [Fact]
+ public void RenderPageNoArgumentsTest()
+ {
+ // Test that using PageData within the calling page, and also
+ // within the subppage when the calling page doesn't provide any arguments
+ //
+ // ~/index.cshtml does the following:
+ // @(PageData["foo"] ?? "null1")
+ // @RenderPage("subpage.cshtml")
+ //
+ // ~/subpage.cshtml does the following:
+ // @(PageData[1] ?? "null2")
+ // @(PageData["bar"] ?? "null3")
+ //
+ // Expected result: null1 null2 null3
+
+ var result = Utils.RenderWebPageWithSubPage(
+ p =>
+ {
+ p.Write(p.PageData["foo"] ?? "null1 ");
+ p.Write(p.RenderPage("subpage.cshtml"));
+ },
+ p =>
+ {
+ p.Write(p.PageData[1] ?? "null2");
+ p.Write(" ");
+ p.Write(p.PageData["bar"] ?? "null3");
+ });
+ Assert.Equal("null1 null2 null3", result);
+ }
+
+ [Fact]
+ public void RenderPageDynamicDictionaryNoArgumentsTest()
+ {
+ // Test that using PageData within the calling page, and also
+ // within the subppage when the calling page doesn't provide any arguments
+ //
+ // ~/index.cshtml does the following:
+ // @(Page.foo ?? "null1")
+ // @RenderPage("subpage.cshtml")
+ //
+ // ~/subpage.cshtml does the following:
+ // @(Page[1] ?? "null2")
+ // @(Page.bar ?? "null3")
+ //
+ // Expected result: null1 null2 null3
+
+ var result = Utils.RenderWebPageWithSubPage(
+ p =>
+ {
+ p.Write(p.Page.foo ?? "null1 ");
+ p.Write(p.RenderPage("subpage.cshtml"));
+ },
+ p =>
+ {
+ p.Write(p.Page[1] ?? "null2");
+ p.Write(" ");
+ p.Write(p.Page.bar ?? "null3");
+ });
+ Assert.Equal("null1 null2 null3", result);
+ }
+
+ [Fact]
+ public void RenderPageNestedSubPageListTest()
+ {
+ // Test that PageData for each level of nesting returns the values as specified in the
+ // previous calling page.
+ //
+ // ~/index.cshtml does the following:
+ // @(PageData["foo"] ?? "null")
+ // @RenderPage("subpage1.cshtml", "a", "b", "c")
+ //
+ // ~/subpage1.cshtml does the following:
+ // @(PageData[0] ?? "sub1null0")
+ // @(PageData[1] ?? "sub1null1")
+ // @(PageData[2] ?? "sub1null2")
+ // @(PageData[3] ?? "sub1null3")
+ // @RenderPage("subpage2.cshtml", "x", "y", "z")
+ //
+ // ~/subpage2.cshtml does the following:
+ // @(PageData[0] ?? "sub2null0")
+ // @(PageData[1] ?? "sub2null1")
+ // @(PageData[2] ?? "sub2null2")
+ // @(PageData[3] ?? "sub2null3")
+ //
+ // Expected result: null a b c sub1null3 x y z sub2null3
+ var page = Utils.CreatePage(
+ p =>
+ {
+ p.Write(p.PageData["foo"] ?? "null ");
+ p.Write(p.RenderPage("subpage1.cshtml", "a", "b", "c"));
+ });
+ var subpage1Path = "~/subpage1.cshtml";
+ var subpage1 = Utils.CreatePage(
+ p =>
+ {
+ p.Write(p.PageData[0] ?? "sub1null0");
+ p.Write(" ");
+ p.Write(p.PageData[1] ?? "sub1null1");
+ p.Write(" ");
+ p.Write(p.PageData[2] ?? "sub1null2");
+ p.Write(" ");
+ p.Write(p.PageData[3] ?? "sub1null3");
+ p.Write(" ");
+ p.Write(p.RenderPage("subpage2.cshtml", "x", "y", "z"));
+ }, subpage1Path);
+ var subpage2Path = "~/subpage2.cshtml";
+ var subpage2 = Utils.CreatePage(
+ p =>
+ {
+ p.Write(p.PageData[0] ?? "sub2null0");
+ p.Write(" ");
+ p.Write(p.PageData[1] ?? "sub2null1");
+ p.Write(" ");
+ p.Write(p.PageData[2] ?? "sub2null2");
+ p.Write(" ");
+ p.Write(p.PageData[3] ?? "sub2null3");
+ }, subpage2Path);
+
+ Utils.AssignObjectFactoriesAndDisplayModeProvider(page, subpage1, subpage2);
+
+ var result = Utils.RenderWebPage(page);
+ Assert.Equal("null a b c sub1null3 x y z sub2null3", result);
+ }
+
+ [Fact]
+ public void RenderPageNestedSubPageAnonymousTypeTest()
+ {
+ // Test that PageData for each level of nesting returns the values as specified in the
+ // previous calling page.
+ //
+ // ~/index.cshtml does the following:
+ // @(PageData["foo"] ?? "null")
+ // @RenderPage("subpage.cshtml", new { foo = 1 , bar = "hello" })
+ //
+ // ~/subpage1.cshtml does the following:
+ // @(PageData["foo"] ?? "sub1nullfoo")
+ // @(PageData["bar"] ?? "sub1nullbar")
+ // @(PageData["x"] ?? "sub1nullx")
+ // @(PageData["y"] ?? "sub1nully")
+ // @RenderPage("subpage2.cshtml", new { bar = "world", x = "good", y = "bye"})
+ //
+ // ~/subpage2.cshtml does the following:
+ // @(PageData["foo"] ?? "sub2nullfoo")
+ // @(PageData["bar"] ?? "sub2nullbar")
+ // @(PageData["x"] ?? "sub2nullx")
+ // @(PageData["y"] ?? "sub2nully")
+ //
+ // Expected result: null 1 hello sub1nullx sub1nully sub2nullfoo world good bye
+ var page = Utils.CreatePage(
+ p =>
+ {
+ p.Write(p.PageData["foo"] ?? "null ");
+ p.Write(p.RenderPage("subpage1.cshtml", new { foo = 1, bar = "hello" }));
+ });
+ var subpage1Path = "~/subpage1.cshtml";
+ var subpage1 = Utils.CreatePage(
+ p =>
+ {
+ p.Write(p.PageData["foo"] ?? "sub1nullfoo");
+ p.Write(" ");
+ p.Write(p.PageData["bar"] ?? "sub1nullbar");
+ p.Write(" ");
+ p.Write(p.PageData["x"] ?? "sub1nullx");
+ p.Write(" ");
+ p.Write(p.PageData["y"] ?? "sub1nully");
+ p.Write(" ");
+ p.Write(p.RenderPage("subpage2.cshtml", new { bar = "world", x = "good", y = "bye" }));
+ }, subpage1Path);
+ var subpage2Path = "~/subpage2.cshtml";
+ var subpage2 = Utils.CreatePage(
+ p =>
+ {
+ p.Write(p.PageData["foo"] ?? "sub2nullfoo");
+ p.Write(" ");
+ p.Write(p.PageData["bar"] ?? "sub2nullbar");
+ p.Write(" ");
+ p.Write(p.PageData["x"] ?? "sub2nullx");
+ p.Write(" ");
+ p.Write(p.PageData["y"] ?? "sub2nully");
+ }, subpage2Path);
+
+ Utils.AssignObjectFactoriesAndDisplayModeProvider(subpage1, subpage2, page);
+
+ var result = Utils.RenderWebPage(page);
+ Assert.Equal("null 1 hello sub1nullx sub1nully sub2nullfoo world good bye", result);
+ }
+
+ [Fact]
+ public void RenderPageNestedSubPageDictionaryTest()
+ {
+ // Test that PageData for each level of nesting returns the values as specified in the
+ // previous calling page.
+ //
+ // ~/index.cshtml does the following:
+ // @(PageData["foo"] ?? "null")
+ // @RenderPage("subpage.cshtml", new Dictionary<string, object>(){ { "foo", 1 }, { "bar", "hello"} })
+ //
+ // ~/subpage1.cshtml does the following:
+ // @(PageData["foo"] ?? "sub1nullfoo")
+ // @(PageData["bar"] ?? "sub1nullbar")
+ // @(PageData["x"] ?? "sub1nullx")
+ // @(PageData["y"] ?? "sub1nully")
+ // @RenderPage("subpage2.cshtml", new Dictionary<string, object>(){ { { "bar", "world"}, {"x", "good"}, {"y", "bye"} })
+ //
+ // ~/subpage2.cshtml does the following:
+ // @(PageData["foo"] ?? "sub2nullfoo")
+ // @(PageData["bar"] ?? "sub2nullbar")
+ // @(PageData["x"] ?? "sub2nullx")
+ // @(PageData["y"] ?? "sub2nully")
+ //
+ // Expected result: null 1 hello sub1nullx sub1nully sub2nullfoo world good bye
+ var page = Utils.CreatePage(
+ p =>
+ {
+ p.Write(p.PageData["foo"] ?? "null ");
+ p.Write(p.RenderPage("subpage1.cshtml", new Dictionary<string, object>() { { "foo", 1 }, { "bar", "hello" } }));
+ });
+ var subpage1Path = "~/subpage1.cshtml";
+ var subpage1 = Utils.CreatePage(
+ p =>
+ {
+ p.Write(p.PageData["foo"] ?? "sub1nullfoo");
+ p.Write(" ");
+ p.Write(p.PageData["bar"] ?? "sub1nullbar");
+ p.Write(" ");
+ p.Write(p.PageData["x"] ?? "sub1nullx");
+ p.Write(" ");
+ p.Write(p.PageData["y"] ?? "sub1nully");
+ p.Write(" ");
+ p.Write(p.RenderPage("subpage2.cshtml", new Dictionary<string, object>() { { "bar", "world" }, { "x", "good" }, { "y", "bye" } }));
+ }, subpage1Path);
+ var subpage2Path = "~/subpage2.cshtml";
+ var subpage2 = Utils.CreatePage(
+ p =>
+ {
+ p.Write(p.PageData["foo"] ?? "sub2nullfoo");
+ p.Write(" ");
+ p.Write(p.PageData["bar"] ?? "sub2nullbar");
+ p.Write(" ");
+ p.Write(p.PageData["x"] ?? "sub2nullx");
+ p.Write(" ");
+ p.Write(p.PageData["y"] ?? "sub2nully");
+ }, subpage2Path);
+
+ Utils.AssignObjectFactoriesAndDisplayModeProvider(page, subpage1, subpage2);
+
+ var result = Utils.RenderWebPage(page);
+ Assert.Equal("null 1 hello sub1nullx sub1nully sub2nullfoo world good bye", result);
+ }
+
+ [Fact]
+ public void RenderPageNestedParentPageDataTest()
+ {
+ // PageData should return values set by parent pages.
+ var page = Utils.CreatePage(
+ p =>
+ {
+ p.PageData["key1"] = "value1";
+ p.Write(p.RenderPage("subpage1.cshtml"));
+ });
+ var subpage1Path = "~/subpage1.cshtml";
+ var subpage1 = Utils.CreatePage(
+ p =>
+ {
+ p.WriteLiteral("<subpage1>");
+ p.Write(p.PageData["key1"]);
+ p.Write(p.RenderPage("subpage2.cshtml"));
+ p.Write(p.PageData["key1"]);
+ p.PageData["key1"] = "value2";
+ p.Write(p.RenderPage("subpage2.cshtml"));
+ p.WriteLiteral("</subpage1>");
+ }, subpage1Path);
+ var subpage2Path = "~/subpage2.cshtml";
+ var subpage2 = Utils.CreatePage(
+ p =>
+ {
+ p.WriteLiteral("<subpage2>");
+ p.Write(p.PageData["key1"]);
+ // Setting the value in the child page should
+ // not affect the parent page
+ p.PageData["key1"] = "value3";
+ p.Write(p.RenderPage("subpage3.cshtml", new { Key1 = "value4" }));
+ p.WriteLiteral("</subpage2>");
+ }, subpage2Path);
+ var subpage3Path = "~/subpage3.cshtml";
+ var subpage3 = Utils.CreatePage(
+ p =>
+ {
+ p.WriteLiteral("<subpage3>");
+ p.Write(p.PageData["key1"]);
+ p.WriteLiteral("</subpage3>");
+ }, subpage3Path);
+
+ Utils.AssignObjectFactoriesAndDisplayModeProvider(subpage1, subpage2, subpage3, page);
+
+ var result = Utils.RenderWebPage(page);
+ Assert.Equal("<subpage1>value1<subpage2>value1<subpage3>value4</subpage3></subpage2>value1<subpage2>value2<subpage3>value4</subpage3></subpage2></subpage1>", result);
+ }
+
+ [Fact]
+ public void WriteNullTest()
+ {
+ // Test for @null
+ var result = Utils.RenderWebPage(
+ p =>
+ {
+ p.Write(null);
+ p.Write((object)null);
+ p.Write((HelperResult)null);
+ });
+
+ Assert.Equal("", result);
+ }
+
+ [Fact]
+ public void WriteTest()
+ {
+ // Test for calling WebPage.Write on text and HtmlHelper
+ var text = "Hello";
+ var wrote = false;
+ Action<TextWriter> action = tw =>
+ {
+ tw.Write(text);
+ wrote = true;
+ };
+ var helper = new HelperResult(action);
+ var result = Utils.RenderWebPage(
+ p => { p.Write(helper); });
+ Assert.Equal(text, result);
+ Assert.True(wrote);
+ }
+
+ [Fact]
+ public void WriteLiteralTest()
+ {
+ // Test for calling WebPage.WriteLiteral on text and HtmlHelper
+ var text = "Hello";
+ var wrote = false;
+ Action<TextWriter> action = tw =>
+ {
+ tw.Write(text);
+ wrote = true;
+ };
+ var helper = new HelperResult(action);
+ var result = Utils.RenderWebPage(
+ p => { p.WriteLiteral(helper); });
+ Assert.Equal(text, result);
+ Assert.True(wrote);
+ }
+
+ [Fact]
+ public void ExtensionNotSupportedTest()
+ {
+ // Tests that calling RenderPage on an unsupported extension returns a new simpler error message
+ // instead of the full error about build providers in system.web.dll.
+ var vpath = "~/hello/world.txt";
+ var ext = ".txt";
+ var compilationUtilThrowingBuildManager = new CompilationUtil();
+ var otherExceptionBuildManager = new Mock<IVirtualPathFactory>();
+ var msg = "The file \"~/hello/world.txt\" could not be rendered, because it does not exist or is not a valid page.";
+ otherExceptionBuildManager.Setup(c => c.CreateInstance(It.IsAny<string>())).Throws(new HttpException(msg));
+
+ Assert.Throws<HttpException>(() =>
+ WebPage.CreateInstanceFromVirtualPath(vpath, new VirtualPathFactoryManager(compilationUtilThrowingBuildManager)),
+ String.Format(CultureInfo.CurrentCulture, WebPageResources.WebPage_FileNotSupported, ext, vpath));
+
+ // Test that other error messages are thrown unmodified.
+ Assert.Throws<HttpException>(() => WebPage.CreateInstanceFromVirtualPath(vpath, otherExceptionBuildManager.Object), msg);
+ }
+
+ [Fact]
+ public void RenderBodyCalledInChildPageTest()
+ {
+ // A Page that is called by RenderPage should not be able to call RenderBody().
+
+ Assert.Throws<HttpException>(() =>
+ Utils.RenderWebPageWithSubPage(
+ p =>
+ {
+ p.Write("hello");
+ p.Write(p.RenderPage("subpage.cshtml"));
+ },
+ p =>
+ {
+ p.Write("world");
+ p.RenderBody();
+ }),
+ String.Format(CultureInfo.CurrentCulture, WebPageResources.WebPage_CannotRequestDirectly, "~/subpage.cshtml", "RenderBody"));
+ }
+
+ [Fact]
+ public void RenderPageInvalidPageType()
+ {
+ var pagePath = "~/foo.js";
+ var page = Utils.CreatePage(p => { p.Write(p.RenderPage(pagePath)); });
+
+ var objectFactory = new Mock<IVirtualPathFactory>();
+ objectFactory.Setup(c => c.Exists(It.IsAny<string>())).Returns<string>(p => pagePath.Equals(p, StringComparison.OrdinalIgnoreCase));
+ objectFactory.Setup(c => c.CreateInstance(It.IsAny<string>())).Returns<string>(_ => null);
+ page.VirtualPathFactory = objectFactory.Object;
+
+ Assert.Throws<HttpException>(() =>
+ {
+ page.VirtualPathFactory = objectFactory.Object;
+ page.DisplayModeProvider = new DisplayModeProvider();
+ Utils.RenderWebPage(page);
+ },
+ String.Format(CultureInfo.CurrentCulture, WebPageResources.WebPage_InvalidPageType, pagePath));
+ }
+
+ [Fact]
+ public void RenderPageValidPageType()
+ {
+ var pagePath = "~/foo.js";
+ var page = Utils.CreatePage(p => { p.Write(p.RenderPage(pagePath)); });
+
+ var contents = "hello world";
+ var subPage = Utils.CreatePage(p => p.Write(contents), pagePath);
+
+ Utils.AssignObjectFactoriesAndDisplayModeProvider(page, subPage);
+
+ Assert.Equal(contents, Utils.RenderWebPage(page));
+ }
+
+ [Fact]
+ public void RenderPageNull()
+ {
+ Assert.ThrowsArgumentNullOrEmptyString(() => Utils.RenderWebPage(p => p.RenderPage(null)), "path");
+ }
+
+ [Fact]
+ public void RenderPageEmptyString()
+ {
+ Assert.ThrowsArgumentNullOrEmptyString(() => Utils.RenderWebPage(p => p.RenderPage("")), "path");
+ }
+
+ [Fact]
+ public void SamePageCaseInsensitiveTest()
+ {
+ var result = Utils.RenderWebPage(
+ p =>
+ {
+ p.PageData["xyz"] = "value";
+ p.PageData["XYZ"] = "VALUE";
+ p.Write(p.PageData["xYz"]);
+ });
+
+ Assert.Equal("VALUE", result);
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/WebPage/RequestBrowserOverrideStoreTest.cs b/test/System.Web.WebPages.Test/WebPage/RequestBrowserOverrideStoreTest.cs
new file mode 100644
index 00000000..091e03cb
--- /dev/null
+++ b/test/System.Web.WebPages.Test/WebPage/RequestBrowserOverrideStoreTest.cs
@@ -0,0 +1,35 @@
+using Moq;
+using Xunit;
+
+namespace System.Web.WebPages.Test
+{
+ public class RequestBrowserOverrideStoreTest
+ {
+ [Fact]
+ public void GetOverriddenUserAgentReturnsRequestUserAgent()
+ {
+ // Arrange
+ RequestBrowserOverrideStore requestStore = new RequestBrowserOverrideStore();
+ Mock<HttpContextBase> context = new Mock<HttpContextBase>();
+ context.Setup(c => c.Request.UserAgent).Returns("testUserAgent");
+
+ // Act & Assert
+ Assert.Equal("testUserAgent", requestStore.GetOverriddenUserAgent(context.Object));
+ }
+
+ [Fact]
+ public void SetOverriddenUserAgentDoesNotOverrideUserAgent()
+ {
+ // Arrange
+ RequestBrowserOverrideStore requestStore = new RequestBrowserOverrideStore();
+ Mock<HttpContextBase> context = new Mock<HttpContextBase>();
+ context.Setup(c => c.Request.UserAgent).Returns("testUserAgent");
+
+ // Act
+ requestStore.SetOverriddenUserAgent(context.Object, "setUserAgent");
+
+ // Assert
+ Assert.Equal("testUserAgent", requestStore.GetOverriddenUserAgent(context.Object));
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/WebPage/RequestResourceTrackerTest.cs b/test/System.Web.WebPages.Test/WebPage/RequestResourceTrackerTest.cs
new file mode 100644
index 00000000..6ee204f4
--- /dev/null
+++ b/test/System.Web.WebPages.Test/WebPage/RequestResourceTrackerTest.cs
@@ -0,0 +1,38 @@
+using System.Collections;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.WebPages.Test
+{
+ public class RequestResourceTrackerTest
+ {
+ [Fact]
+ public void RegisteringForDisposeDisposesObjects()
+ {
+ // Arrange
+ var context = new Mock<HttpContextBase>();
+ IDictionary items = new Hashtable();
+ context.Setup(m => m.Items).Returns(items);
+ var disposable = new Mock<IDisposable>();
+ disposable.Setup(m => m.Dispose()).Verifiable();
+
+ // Act
+ RequestResourceTracker.RegisterForDispose(context.Object, disposable.Object);
+ RequestResourceTracker.DisposeResources(context.Object);
+
+ // Assert
+ disposable.VerifyAll();
+ }
+
+ [Fact]
+ public void RegisteringForDisposeExtensionMethodNullContextThrows()
+ {
+ // Arrange
+ var disposable = new Mock<IDisposable>();
+
+ // Act
+ Assert.ThrowsArgumentNull(() => HttpContextExtensions.RegisterForDispose(null, disposable.Object), "context");
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/WebPage/SectionControlBuilderTest.cs b/test/System.Web.WebPages.Test/WebPage/SectionControlBuilderTest.cs
new file mode 100644
index 00000000..136bfba0
--- /dev/null
+++ b/test/System.Web.WebPages.Test/WebPage/SectionControlBuilderTest.cs
@@ -0,0 +1,249 @@
+using System.CodeDom;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.WebPages.Test {
+ /// <summary>
+ ///This is a test class for SectionControlBuilderTestand is intended
+ ///to contain all SectionControlBuilder Unit Tests
+ ///</summary>
+ [TestClass()]
+ public class SectionControlBuilderTest {
+
+ private const int defaultIndex = 2 ;
+ private const int defaultIndexAfterOffset = 4;
+
+ private TestContext testContextInstance;
+
+ /// <summary>
+ ///Gets or sets the test context which provides
+ ///information about and functionality for the current test run.
+ ///</summary>
+ public TestContext TestContext {
+ get {
+ return testContextInstance;
+ }
+ set {
+ testContextInstance = value;
+ }
+ }
+
+ #region Additional test attributes
+ //
+ //You can use the following additional attributes as you write your tests:
+ //
+ //Use ClassInitialize to run code before running the first test in the class
+ //[ClassInitialize()]
+ //public static void MyClassInitialize(TestContext testContext)
+ //{
+ //}
+ //
+ //Use ClassCleanup to run code after all tests in a class have run
+ //[ClassCleanup()]
+ //public static void MyClassCleanup()
+ //{
+ //}
+ //
+ //Use TestInitialize to run code before running each test
+ //[TestInitialize()]
+ //public void MyTestInitialize()
+ //{
+ //}
+ //
+ //Use TestCleanup to run code after each test has run
+ //[TestCleanup()]
+ //public void MyTestCleanup()
+ //{
+ //}
+ //
+ #endregion
+
+ [TestMethod]
+ public void GetSectionRenderMethodTest() {
+ var members = GetRenderMembers();
+ var result = SectionControlBuilder.GetSectionRenderMethod(members, defaultIndex);
+ Assert.AreEqual(members[0], result);
+ }
+
+ [TestMethod]
+ public void GetSectionNameTest() {
+ var sectionName = "MySectionName";
+ var result = SectionControlBuilder.GetSectionName(GetBuildMembers(), defaultIndex);
+ Assert.AreEqual(sectionName, result);
+ }
+
+ [TestMethod]
+ public void GetSectionNameNullTest() {
+ var index = defaultIndexAfterOffset;
+ var methodName = "__BuildControl__control";
+ Assert.IsNull(SectionControlBuilder.GetSectionName(new CodeTypeMember[] { }, defaultIndex));
+
+ var method = new CodeMemberMethod();
+ method = new CodeMemberMethod() { Name = methodName + index.ToString() };
+ method.Statements.Add(new CodeSnippetStatement("test"));
+ Assert.IsNull(SectionControlBuilder.GetSectionName(new CodeTypeMember[] { method }, defaultIndex));
+
+ var statement = new CodeAssignStatement(null, null);
+ method.Statements.Clear();
+ method.Statements.Add(statement);
+ Assert.IsNull(SectionControlBuilder.GetSectionName(new CodeTypeMember[] { method }, defaultIndex));
+
+ var left = new CodePropertyReferenceExpression(null, "test");
+ statement = new CodeAssignStatement(left, null);
+ method.Statements.Clear();
+ method.Statements.Add(statement);
+ Assert.IsNull(SectionControlBuilder.GetSectionName(new CodeTypeMember[] { method }, defaultIndex));
+
+ left = new CodePropertyReferenceExpression(null, "Name");
+ statement = new CodeAssignStatement(left, null);
+ method.Statements.Clear();
+ method.Statements.Add(statement);
+ Assert.IsNull(SectionControlBuilder.GetSectionName(new CodeTypeMember[] { method }, defaultIndex));
+
+ left = new CodePropertyReferenceExpression(new CodeVariableReferenceExpression("test"), "Name");
+ statement = new CodeAssignStatement(left, null);
+ method.Statements.Clear();
+ method.Statements.Add(statement);
+ Assert.IsNull(SectionControlBuilder.GetSectionName(new CodeTypeMember[] { method }, defaultIndex));
+
+ }
+
+ [TestMethod]
+ public void GetDefineSectionStatementsTest() {
+ var renderMembers = GetRenderMembers();
+ var buildMembers = GetBuildMembers();
+ var members = new List<CodeTypeMember>(renderMembers);
+ foreach (var m in buildMembers) {
+ members.Add(m);
+ }
+ var result = SectionControlBuilder.GetDefineSectionStatements(members, defaultIndex, Language.CSharp);
+ VerifyDefineSection(((CodeMemberMethod)renderMembers[0]).Statements[0], result);
+ }
+
+ private static void VerifyDefineSection(CodeStatement renderStatement, IList<CodeStatement> result) {
+ Assert.AreEqual(3, result.Count);
+ Assert.IsInstanceOfType(result[0], typeof(CodeSnippetStatement));
+ var snippet = result[0] as CodeSnippetStatement;
+ Assert.AreEqual("DefineSection(\"MySectionName\", delegate () {", snippet.Value);
+ Assert.IsInstanceOfType(result[1], typeof(CodeExpressionStatement));
+ Assert.AreEqual(renderStatement, result[1]);
+ Assert.IsInstanceOfType(result[2], typeof(CodeSnippetStatement));
+ snippet = result[2] as CodeSnippetStatement;
+ Assert.AreEqual("});", snippet.Value);
+ }
+
+ public IList<CodeTypeMember> GetRenderMembers() {
+ // Create a method of the following form:
+ // void _Render__control4() {
+ // this.Write("Hello");
+ // }
+ var sectionName = "MySectionName";
+ var methodName = "__Render__control";
+ var expr = new CodeMethodInvokeExpression(new CodeThisReferenceExpression(), "Write", new CodeExpression[] { new CodePrimitiveExpression("Hello") });
+ var statement = new CodeExpressionStatement(expr);
+ return GetBuildMembers(methodName, sectionName, defaultIndexAfterOffset, statement);
+ }
+
+ public IList<CodeTypeMember> GetBuildMembers() {
+ // Create a method of the following form:
+ // void __BuildControl__control4() {
+ // __ctrl.Name = "my_section_name";
+ // }
+ var sectionName = "MySectionName";
+ var methodName = "__BuildControl__control";
+ var left = new CodePropertyReferenceExpression(new CodeVariableReferenceExpression("__ctrl"), "Name");
+ var right = new CodePrimitiveExpression(sectionName);
+ var statement = new CodeAssignStatement(left, right);
+ return GetBuildMembers(methodName, sectionName, defaultIndexAfterOffset, statement);
+ }
+
+ public IList<CodeTypeMember> GetBuildMembers(string methodName, string sectionName, int index, CodeStatement statement = null) {
+
+ var method = new CodeMemberMethod() { Name = methodName + index.ToString() };
+ if (statement != null) {
+ method.Statements.Add(statement);
+ }
+ return new CodeTypeMember[] { method };
+ }
+
+ [TestMethod]
+ public void ProcessRenderControlMethodTest() {
+ // Create a statement of the following form:
+ // parameterContainer.Controls[0].RenderControl(@__w);
+ // where parameterContainer is a parameter.
+ var container = new CodeArgumentReferenceExpression("parameterContainer");
+ var controls = new CodePropertyReferenceExpression(container, "Controls");
+ var indexer = new CodeIndexerExpression(controls, new CodeExpression[] { new CodePrimitiveExpression(defaultIndex) });
+ var invoke = new CodeMethodInvokeExpression(indexer, "RenderControl", new CodeExpression[] { new CodeArgumentReferenceExpression("__w") });
+ var stmt = new CodeExpressionStatement(invoke);
+ var renderMethod = new CodeMemberMethod();
+ var renderMembers = GetRenderMembers();
+ var buildMembers = GetBuildMembers();
+ var members = new List<CodeTypeMember>(renderMembers);
+ foreach (var m in buildMembers) {
+ members.Add(m);
+ }
+ SectionControlBuilder.ProcessRenderControlMethod(members, renderMethod, stmt, Language.CSharp);
+ VerifyDefineSection(((CodeMemberMethod)renderMembers[0]).Statements[0], renderMethod.Statements.OfType<CodeStatement>().ToList());
+ }
+
+ [TestMethod]
+ public void ProcessRenderControlMethodFalseTest() {
+ Assert.IsFalse(SectionControlBuilder.ProcessRenderControlMethod(null, null, null, Language.CSharp));
+ Assert.IsFalse(SectionControlBuilder.ProcessRenderControlMethod(null, null, new CodeExpressionStatement(new CodeSnippetExpression("test")), Language.CSharp));
+
+ var invoke = new CodeMethodInvokeExpression(null, "RenderControlX", new CodeExpression[] { new CodeArgumentReferenceExpression("__w") });
+ var stmt = new CodeExpressionStatement(invoke);
+ Assert.IsFalse(SectionControlBuilder.ProcessRenderControlMethod(null, null, stmt, Language.CSharp));
+
+ invoke = new CodeMethodInvokeExpression(new CodeSnippetExpression(""), "RenderControl", new CodeExpression[] { new CodeArgumentReferenceExpression("__w") });
+ stmt = new CodeExpressionStatement(invoke);
+ Assert.IsFalse(SectionControlBuilder.ProcessRenderControlMethod(null, null, stmt, Language.CSharp));
+
+ var indexer = new CodeIndexerExpression(new CodeSnippetExpression(""), new CodeExpression[] { new CodePrimitiveExpression(defaultIndex) });
+ invoke = new CodeMethodInvokeExpression(indexer, "RenderControl", new CodeExpression[] { new CodeArgumentReferenceExpression("__w") });
+ stmt = new CodeExpressionStatement(invoke);
+ Assert.IsFalse(SectionControlBuilder.ProcessRenderControlMethod(null, null, stmt, Language.CSharp));
+
+ var container = new CodeArgumentReferenceExpression("parameterContainer");
+ var controls = new CodePropertyReferenceExpression(container, "Controls");
+ indexer = new CodeIndexerExpression(controls, new CodeExpression[] { new CodeSnippetExpression("") });
+ invoke = new CodeMethodInvokeExpression(indexer, "RenderControl", new CodeExpression[] { new CodeArgumentReferenceExpression("__w") });
+ stmt = new CodeExpressionStatement(invoke);
+ Assert.IsFalse(SectionControlBuilder.ProcessRenderControlMethod(null, null, stmt, Language.CSharp));
+
+ controls = new CodePropertyReferenceExpression(container, "ControlsX");
+ indexer = new CodeIndexerExpression(controls, new CodeExpression[] { new CodePrimitiveExpression(defaultIndex) });
+ invoke = new CodeMethodInvokeExpression(indexer, "RenderControl", new CodeExpression[] { new CodeArgumentReferenceExpression("__w") });
+ stmt = new CodeExpressionStatement(invoke);
+ Assert.IsFalse(SectionControlBuilder.ProcessRenderControlMethod(null, null, stmt, Language.CSharp));
+
+ controls = new CodePropertyReferenceExpression(new CodeSnippetExpression("test"), "Controls");
+ indexer = new CodeIndexerExpression(controls, new CodeExpression[] { new CodePrimitiveExpression(defaultIndex) });
+ invoke = new CodeMethodInvokeExpression(indexer, "RenderControl", new CodeExpression[] { new CodeArgumentReferenceExpression("__w") });
+ stmt = new CodeExpressionStatement(invoke);
+ Assert.IsFalse(SectionControlBuilder.ProcessRenderControlMethod(null, null, stmt, Language.CSharp));
+
+ container = new CodeArgumentReferenceExpression("parameterContainerX");
+ controls = new CodePropertyReferenceExpression(container, "Controls");
+ indexer = new CodeIndexerExpression(controls, new CodeExpression[] { new CodePrimitiveExpression(defaultIndex) });
+ invoke = new CodeMethodInvokeExpression(indexer, "RenderControl", new CodeExpression[] { new CodeArgumentReferenceExpression("__w") });
+ stmt = new CodeExpressionStatement(invoke);
+ Assert.IsFalse(SectionControlBuilder.ProcessRenderControlMethod(null, null, stmt, Language.CSharp));
+ }
+
+ [TestMethod]
+ public void GetDefineSectionStartSnippetTest() {
+ var snippetStmt = SectionControlBuilder.GetDefineSectionStartSnippet("HelloWorld", Language.CSharp) as CodeSnippetStatement;
+ Assert.AreEqual("DefineSection(\"HelloWorld\", delegate () {", snippetStmt.Value);
+ snippetStmt = SectionControlBuilder.GetDefineSectionStartSnippet("HelloWorld", Language.VisualBasic) as CodeSnippetStatement;
+ Assert.AreEqual("DefineSection(\"HelloWorld\", Sub()", snippetStmt.Value);
+ }
+
+ [TestMethod]
+ public void HasAspCodeTest() {
+ Assert.IsTrue(new SectionControlBuilder().HasAspCode);
+ }
+ }
+} \ No newline at end of file
diff --git a/test/System.Web.WebPages.Test/WebPage/StartPageTest.cs b/test/System.Web.WebPages.Test/WebPage/StartPageTest.cs
new file mode 100644
index 00000000..36430f20
--- /dev/null
+++ b/test/System.Web.WebPages.Test/WebPage/StartPageTest.cs
@@ -0,0 +1,568 @@
+using System.Reflection;
+using System.Web.Caching;
+using System.Web.Compilation;
+using System.Web.Hosting;
+using System.Web.Profile;
+using System.Web.WebPages.TestUtils;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.WebPages.Test
+{
+ public class StartPageTest
+ {
+ // The page ~/_pagestart.cshtml does the following:
+ // this is the init page
+ //
+ // The page ~/index.cshtml does the following:
+ // hello world
+ // Expected result:
+ // this is the init page hello world
+ [Fact]
+ public void InitPageBasicTest()
+ {
+ var init = Utils.CreateStartPage(p =>
+ p.Write("this is the init page "));
+ var page = Utils.CreatePage(p =>
+ p.Write("hello world"));
+
+ init.ChildPage = page;
+
+ var result = Utils.RenderWebPage(page, init);
+ Assert.Equal("this is the init page hello world", result);
+ }
+
+ // The page ~/_pagestart.cshtml does the following:
+ // this is the init page
+ //
+ // The page ~/folder1/index.cshtml does the following:
+ // hello world
+ // Expected result:
+ // this is the init page hello world
+ [Fact]
+ public void InitSubfolderTest()
+ {
+ var init = Utils.CreateStartPage(p =>
+ p.Write("this is the init page "));
+ var page = Utils.CreatePage(p =>
+ p.Write("hello world"), "~/folder1/index.cshtml");
+
+ init.ChildPage = page;
+
+ var result = Utils.RenderWebPage(page, init);
+ Assert.Equal("this is the init page hello world", result);
+ }
+
+ // The page ~/_pagestart.cshtml does the following:
+ // PageData["Title"] = "InitPage";
+ // Layout = "Layout.cshtml";
+ // this is the init page
+ //
+ // The page ~/index.cshtml does the following:
+ // PageData["Title"] = "IndexCshtmlPage"
+ // hello world
+ //
+ // The layout page ~/Layout.cshtml does the following:
+ // layout start
+ // @PageData["Title"]
+ // @RenderBody()
+ // layout end
+ //
+ // Expected result:
+ // layout start IndexCshtmlPage this is the init page hello world layout end
+ [Fact]
+ public void InitPageLayoutTest()
+ {
+ var init = Utils.CreateStartPage(p =>
+ {
+ p.Layout = "Layout.cshtml";
+ p.Write(" this is the init page ");
+ Assert.Equal("~/Layout.cshtml", p.Layout);
+ });
+ var page = Utils.CreatePage(p =>
+ {
+ p.PageData["Title"] = "IndexCshtmlPage";
+ p.Write("hello world");
+ });
+ var layoutPage = Utils.CreatePage(p =>
+ {
+ p.Write("layout start ");
+ p.Write(p.PageData["Title"]);
+ p.WriteLiteral(p.RenderBody());
+ p.Write(" layout end");
+ }, "~/Layout.cshtml");
+
+ init.ChildPage = page;
+ Utils.AssignObjectFactoriesAndDisplayModeProvider(init, page, layoutPage);
+
+ var result = Utils.RenderWebPage(page, init);
+ Assert.Equal("layout start IndexCshtmlPage this is the init page hello world layout end", result);
+ }
+
+ // _pagestart.cshtml sets the LayoutPage to be null
+ [Fact]
+ public void InitPageNullLayoutPageTest()
+ {
+ var init1 = Utils.CreateStartPage(
+ p =>
+ {
+ p.Layout = "~/Layout.cshtml";
+ p.WriteLiteral("<init1>");
+ p.RunPage();
+ p.WriteLiteral("</init1>");
+ });
+ var init2path = "~/folder1/_pagestart.cshtml";
+ var init2 = Utils.CreateStartPage(
+ p =>
+ {
+ p.Layout = null;
+ p.WriteLiteral("<init2>");
+ p.RunPage();
+ p.WriteLiteral("</init2>");
+ }, init2path);
+ var page = Utils.CreatePage(p =>
+ p.Write("hello world"), "~/folder1/index.cshtml");
+ var layoutPage = Utils.CreatePage(p =>
+ p.Write("layout page"), "~/Layout.cshtml");
+
+ Utils.AssignObjectFactoriesAndDisplayModeProvider(page, layoutPage, init1, init2);
+
+ init1.ChildPage = init2;
+ init2.ChildPage = page;
+
+ var result = Utils.RenderWebPage(page, init1);
+ Assert.Equal("<init1><init2>hello world</init2></init1>", result);
+ }
+
+ // _pagestart.cshtml sets the LayoutPage, but page sets it to null
+ [Fact]
+ public void PageSetsNullLayoutPageTest()
+ {
+ var init1 = Utils.CreateStartPage(
+ p =>
+ {
+ p.Layout = "~/Layout.cshtml";
+ p.WriteLiteral("<init1>");
+ p.RunPage();
+ p.WriteLiteral("</init1>");
+ });
+ var layoutPage = Utils.CreatePage(p =>
+ p.Write("layout page"), "~/Layout.cshtml");
+ var page = Utils.CreatePage(p =>
+ {
+ p.Layout = null;
+ p.Write("hello world");
+ });
+ Utils.AssignObjectFactoriesAndDisplayModeProvider(init1, layoutPage, page);
+ init1.ChildPage = page;
+ var result = Utils.RenderWebPage(page, init1);
+ Assert.Equal("<init1>hello world</init1>", result);
+ }
+
+ [Fact]
+ public void PageSetsEmptyLayoutPageTest()
+ {
+ var init1 = Utils.CreateStartPage(
+ p =>
+ {
+ p.Layout = "~/Layout.cshtml";
+ p.WriteLiteral("<init1>");
+ p.RunPage();
+ p.WriteLiteral("</init1>");
+ });
+ var layoutPage = Utils.CreatePage(p =>
+ p.Write("layout page"), "~/Layout.cshtml");
+ var page = Utils.CreatePage(p =>
+ {
+ p.Layout = "";
+ p.Write("hello world");
+ });
+ Utils.AssignObjectFactoriesAndDisplayModeProvider(init1, layoutPage, page);
+ init1.ChildPage = page;
+ var result = Utils.RenderWebPage(page, init1);
+ Assert.Equal("<init1>hello world</init1>", result);
+ }
+
+ // The page ~/_pagestart.cshtml does the following:
+ // init page start
+ // @RunPage()
+ // init page end
+ //
+ // The page ~/index.cshtml does the following:
+ // hello world
+ //
+ // Expected result:
+ // init page start hello world init page end
+ [Fact]
+ public void RunPageTest()
+ {
+ var init = Utils.CreateStartPage(
+ p =>
+ {
+ p.Write("init page start ");
+ p.RunPage();
+ p.Write(" init page end");
+ });
+ var page = Utils.CreatePage(p =>
+ p.Write("hello world"));
+
+ init.ChildPage = page;
+
+ var result = Utils.RenderWebPage(page, init);
+ Assert.Equal("init page start hello world init page end", result);
+ }
+
+ // The page ~/_pagestart.cshtml does the following:
+ // <init1>
+ // @RunPage()
+ // </init1>
+ //
+ // The page ~/folder1/_pagestart.cshtml does the following:
+ // <init2>
+ // @RunPage()
+ // </init2>
+ //
+ // The page ~/folder1/index.cshtml does the following:
+ // hello world
+ //
+ // Expected result:
+ // <init1><init2>hello world</init2></init1>
+ [Fact]
+ public void NestedRunPageTest()
+ {
+ var init1 = Utils.CreateStartPage(
+ p =>
+ {
+ p.WriteLiteral("<init1>");
+ p.RunPage();
+ p.WriteLiteral("</init1>");
+ });
+ var init2path = "~/folder1/_pagestart.cshtml";
+ var init2 = Utils.CreateStartPage(
+ p =>
+ {
+ p.WriteLiteral("<init2>");
+ p.RunPage();
+ p.WriteLiteral("</init2>");
+ }, init2path);
+ var page = Utils.CreatePage(p =>
+ p.Write("hello world"), "~/folder1/index.cshtml");
+
+ init1.ChildPage = init2;
+ init2.ChildPage = page;
+
+ var result = Utils.RenderWebPage(page, init1);
+ Assert.Equal("<init1><init2>hello world</init2></init1>", result);
+ }
+
+ // The page ~/_pagestart.cshtml does the following:
+ // PageData["key1"] = "value1";
+ //
+ // The page ~/folder1/_pagestart.cshtml does the following:
+ // PageData["key2"] = "value2";
+ //
+ // The page ~/folder1/index.cshtml does the following:
+ // @PageData["key1"] @PageData["key2"] @PageData["key3"]
+ //
+ // Expected result:
+ // value1 value2
+ [Fact]
+ public void PageDataTest()
+ {
+ var init1 = Utils.CreateStartPage(p => p.PageData["key1"] = "value1");
+ var init2path = "~/folder1/_pagestart.cshtml";
+ var init2 = Utils.CreateStartPage(p => p.PageData["key2"] = "value2", init2path);
+ var page = Utils.CreatePage(
+ p =>
+ {
+ p.Write(p.PageData["key1"]);
+ p.Write(" ");
+ p.Write(p.PageData["key2"]);
+ },
+ "~/folder1/index.cshtml");
+
+ init1.ChildPage = init2;
+ init2.ChildPage = page;
+
+ var result = Utils.RenderWebPage(page, init1);
+ Assert.Equal("value1 value2", result);
+ }
+
+ // The page ~/_pagestart.cshtml does the following:
+ // init page
+ // @RenderPage("subpage.cshtml", "init_data");
+ //
+ // The page ~/subpage.cshtml does the following:
+ // subpage
+ // @PageData[0]
+ //
+ // The page ~/index.cshtml does the following:
+ // hello world
+ //
+ // Expected result:
+ // init page subpage init_data hello world
+ [Fact]
+ public void RenderPageTest()
+ {
+ var init = Utils.CreateStartPage(
+ p =>
+ {
+ p.Write("init page ");
+ p.Write(p.RenderPage("subpage.cshtml", "init_data"));
+ });
+ var subpagePath = "~/subpage.cshtml";
+ var subpage = Utils.CreatePage(
+ p =>
+ {
+ p.Write("subpage ");
+ p.Write(p.PageData[0]);
+ }, subpagePath);
+ var page = Utils.CreatePage(p =>
+ p.Write(" hello world"));
+
+ init.ChildPage = page;
+ Utils.AssignObjectFactoriesAndDisplayModeProvider(init, page, subpage);
+
+ var result = Utils.RenderWebPage(page, init);
+ Assert.Equal("init page subpage init_data hello world", result);
+ }
+
+ [Fact]
+ // The page ~/_pagestart.cshtml does the following:
+ // <init>
+ // @{
+ // try {
+ // RunPage();
+ // } catch (Exception e) {
+ // Write("Exception: " + e.Message);
+ // }
+ // }
+ // </init>
+ //
+ // The page ~/index.cshtml does the following:
+ // hello world
+ // @{throw new InvalidOperation("exception from index.cshtml");}
+ //
+ // Expected result:
+ // <init>hello world Exception: exception from index.cshtml</init>
+ public void InitCatchExceptionTest()
+ {
+ var init = Utils.CreateStartPage(
+ p =>
+ {
+ p.WriteLiteral("<init>");
+ try
+ {
+ p.RunPage();
+ }
+ catch (Exception e)
+ {
+ p.Write("Exception: " + e.Message);
+ }
+ p.WriteLiteral("</init>");
+ });
+ var page = Utils.CreatePage(
+ p =>
+ {
+ p.WriteLiteral("hello world ");
+ throw new InvalidOperationException("exception from index.cshtml");
+ });
+
+ init.ChildPage = page;
+
+ var result = Utils.RenderWebPage(page, init);
+ Assert.Equal("<init>hello world Exception: exception from index.cshtml</init>", result);
+ }
+
+ public class MockInitPage : MockStartPage
+ {
+ internal object GetBuildManager()
+ {
+ return typeof(BuildManager).GetField("_theBuildManager", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null);
+ }
+ }
+
+ // Simulate a site that is nested, eg /subfolder1/website1
+ [Fact]
+ public void ExecuteWithinInitTest()
+ {
+ AppDomainUtils.RunInSeparateAppDomain(() =>
+ {
+ Utils.CreateHttpRuntime("/subfolder1/website1");
+ new HostingEnvironment();
+ var stringSet = Activator.CreateInstance(typeof(BuildManager).Assembly.GetType("System.Web.Util.StringSet"), true);
+ typeof(BuildManager).GetField("_forbiddenTopLevelDirectories", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(new MockInitPage().GetBuildManager(), stringSet);
+ ;
+
+ var init = new MockInitPage()
+ {
+ VirtualPath = "~/_pagestart.cshtml",
+ ExecuteAction = p => { },
+ };
+ var page = Utils.CreatePage(p => { });
+
+ Utils.AssignObjectFactoriesAndDisplayModeProvider(page, init);
+
+ var result = Utils.RenderWebPage(page);
+ });
+ }
+
+ [Fact]
+ public void SetGetPropertiesTest()
+ {
+ var init = new MockInitPage();
+ var page = new MockPage();
+ init.ChildPage = page;
+
+ // Context
+ var context = new Mock<HttpContextBase>().Object;
+ init.Context = context;
+ Assert.Equal(context, init.Context);
+ Assert.Equal(context, page.Context);
+
+ // Profile/Request/Response/Server/Cache/Session/Application
+ var profile = new Mock<ProfileBase>().Object;
+ var request = new Mock<HttpRequestBase>().Object;
+ var response = new Mock<HttpResponseBase>().Object;
+ var server = new Mock<HttpServerUtilityBase>().Object;
+ var cache = new Cache();
+ var app = new Mock<HttpApplicationStateBase>().Object;
+ var session = new Mock<HttpSessionStateBase>().Object;
+
+ var contextMock = new Mock<HttpContextBase>();
+ contextMock.Setup(c => c.Profile).Returns(profile);
+ contextMock.Setup(c => c.Request).Returns(request);
+ contextMock.Setup(c => c.Response).Returns(response);
+ contextMock.Setup(c => c.Cache).Returns(cache);
+ contextMock.Setup(c => c.Server).Returns(server);
+ contextMock.Setup(c => c.Application).Returns(app);
+ contextMock.Setup(c => c.Session).Returns(session);
+
+ context = contextMock.Object;
+ page.Context = context;
+ Assert.Same(profile, init.Profile);
+ Assert.Same(request, init.Request);
+ Assert.Same(response, init.Response);
+ Assert.Same(cache, init.Cache);
+ Assert.Same(server, init.Server);
+ Assert.Same(session, init.Session);
+ Assert.Same(app, init.AppState);
+ }
+
+ [Fact]
+ public void GetDirectoryTest()
+ {
+ var initPage = new Mock<StartPage>().Object;
+ Assert.Equal("/website1/", initPage.GetDirectory("/website1/default.cshtml"));
+ Assert.Equal("~/", initPage.GetDirectory("~/default.cshtml"));
+ Assert.Equal("/", initPage.GetDirectory("/website1/"));
+ Assert.Equal(null, initPage.GetDirectory("/"));
+ }
+
+ [Fact]
+ public void GetStartPageReturnsStartPageFromCurrentDirectoryIfExists()
+ {
+ // Arrange
+ var initPage = Utils.CreateStartPage(p => p.Write("<init>"), "~/subdir/_pagestart.vbhtml");
+ var page = Utils.CreatePage(p => p.Write("test"), "~/subdir/_index.cshtml");
+ var objectFactory = Utils.AssignObjectFactoriesAndDisplayModeProvider(page, initPage);
+
+ // Act
+ var result = StartPage.GetStartPage(page, objectFactory, null, WebPageHttpHandler.StartPageFileName, new string[] { "cshtml", "vbhtml" });
+
+ // Assert
+ Assert.Equal(initPage, result);
+ }
+
+ [Fact]
+ public void GetStartPageReturnsStartPageFromParentDirectoryIfStartPageDoesNotExistInCurrentDirectory()
+ {
+ // Arrange
+ var initPage = Utils.CreateStartPage(null, "~/subdir/_pagestart.vbhtml");
+ var page = Utils.CreatePage(null, "~/subdir/subsubdir/test.cshtml");
+ var objectFactory = Utils.AssignObjectFactoriesAndDisplayModeProvider(page, initPage);
+
+ // Act
+ var result = StartPage.GetStartPage(page, objectFactory, null, WebPageHttpHandler.StartPageFileName, new string[] { "cshtml", "vbhtml" });
+
+ // Assert
+ Assert.Equal(initPage, result);
+ }
+
+ [Fact]
+ public void GetStartPageCreatesChainOfStartPages()
+ {
+ // Arrange
+ var subInitPage = Utils.CreateStartPage(null, "~/subdir/_pagestart.vbhtml");
+ var initPage = Utils.CreateStartPage(null, "~/_pagestart.vbhtml");
+ var page = Utils.CreatePage(null, "~/subdir/subsubdir/subsubsubdir/test.cshtml");
+ var objectFactory = Utils.AssignObjectFactoriesAndDisplayModeProvider(page, initPage, subInitPage);
+
+ // Act
+ var result = StartPage.GetStartPage(page, objectFactory, null, WebPageHttpHandler.StartPageFileName, new string[] { "cshtml", "vbhtml" });
+
+ // Assert
+ Assert.Equal(initPage, result);
+ Assert.Equal(subInitPage, (result as StartPage).ChildPage);
+ }
+
+ [Fact]
+ public void GetStartPageReturnsStartPageFromRoot()
+ {
+ // Arrange
+ var initPage = Utils.CreateStartPage(null, "~/_pagestart.vbhtml");
+ var page = Utils.CreatePage(null, "~/subdir/subsubdir/subsubsubdir/subsubsubsubdir/why-does-this-remind-me-of-a-movie-title.cshtml");
+ var objectFactory = Utils.AssignObjectFactoriesAndDisplayModeProvider(page, initPage);
+
+ // Act
+ var result = StartPage.GetStartPage(page, objectFactory, null, WebPageHttpHandler.StartPageFileName, new string[] { "cshtml", "vbhtml" });
+
+ // Assert
+ Assert.Equal(initPage, result);
+ }
+
+ [Fact]
+ public void GetStartPageUsesBothFileNamesAndExtensionsWhenDeterminingStartPage()
+ {
+ // Arrange
+ var subInitPage = Utils.CreateStartPage(null, "~/subdir/_pagestart.jshtml");
+ var initPage = Utils.CreateStartPage(null, "~/_pagestart.vbhtml");
+ var page = Utils.CreatePage(null, "~/subdir/test.cshtml");
+ var objectFactory = Utils.AssignObjectFactoriesAndDisplayModeProvider(page, initPage, subInitPage);
+
+ // Act
+ var result = StartPage.GetStartPage(page, objectFactory, null, WebPageHttpHandler.StartPageFileName, new string[] { "cshtml", "vbhtml" });
+
+ // Assert
+ Assert.Equal(initPage, result);
+ }
+
+ [Fact]
+ public void GetStartPage_ThrowsOnNullPage()
+ {
+ Assert.ThrowsArgumentNull(() => StartPage.GetStartPage(null, "name", new[] { "cshtml" }), "page");
+ }
+
+ [Fact]
+ public void GetStartPage_ThrowsOnNullFileName()
+ {
+ var page = Utils.CreatePage(p => p.Write("test"));
+ Assert.ThrowsArgumentNullOrEmptyString(() => StartPage.GetStartPage(page, null, new[] { "cshtml" }), "fileName");
+ }
+
+ [Fact]
+ public void GetStartPage_ThrowsOnEmptyFileName()
+ {
+ var page = Utils.CreatePage(p => p.Write("test"));
+ Assert.ThrowsArgumentNullOrEmptyString(() => StartPage.GetStartPage(page, String.Empty, new[] { "cshtml" }), "fileName");
+ }
+
+ [Fact]
+ public void GetStartPage_ThrowsOnNullSupportedExtensions()
+ {
+ var page = Utils.CreatePage(p => p.Write("test"));
+ Assert.ThrowsArgumentNull(() => StartPage.GetStartPage(page, "name", null), "supportedExtensions");
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/WebPage/TemplateStackTest.cs b/test/System.Web.WebPages.Test/WebPage/TemplateStackTest.cs
new file mode 100644
index 00000000..2e468121
--- /dev/null
+++ b/test/System.Web.WebPages.Test/WebPage/TemplateStackTest.cs
@@ -0,0 +1,86 @@
+using System.Collections.Generic;
+using Moq;
+using Xunit;
+
+namespace System.Web.WebPages.Test
+{
+ public class WebPageContextStackTest
+ {
+ [Fact]
+ public void GetCurrentContextReturnsNullWhenStackIsEmpty()
+ {
+ // Arrange
+ var httpContext = GetHttpContext();
+
+ // Act
+ var template = TemplateStack.GetCurrentTemplate(httpContext);
+
+ // Assert
+ Assert.Equal(1, httpContext.Items.Count);
+ Assert.Null(template);
+ }
+
+ [Fact]
+ public void GetCurrentContextReturnsCurrentContext()
+ {
+ // Arrange
+ var template = GetTemplateFile();
+ var httpContext = GetHttpContext();
+
+ // Act
+ TemplateStack.Push(httpContext, template);
+
+ // Assert
+ var currentTemplate = TemplateStack.GetCurrentTemplate(httpContext);
+ Assert.Equal(template, currentTemplate);
+ }
+
+ [Fact]
+ public void GetCurrentContextReturnsLastPushedContext()
+ {
+ // Arrange
+ var httpContext = GetHttpContext();
+ var template1 = GetTemplateFile("page1");
+ var template2 = GetTemplateFile("page2");
+
+ // Act
+ TemplateStack.Push(httpContext, template1);
+ TemplateStack.Push(httpContext, template2);
+
+ // Assert
+ var currentTemplate = TemplateStack.GetCurrentTemplate(httpContext);
+ Assert.Equal(template2, currentTemplate);
+ }
+
+ [Fact]
+ public void GetCurrentContextReturnsNullAfterPop()
+ {
+ // Arrange
+ var httpContext = GetHttpContext();
+ var template = GetTemplateFile();
+
+ // Act
+ TemplateStack.Push(httpContext, template);
+ TemplateStack.Pop(httpContext);
+
+ // Assert
+ Assert.Null(TemplateStack.GetCurrentTemplate(httpContext));
+ }
+
+ private static HttpContextBase GetHttpContext()
+ {
+ Mock<HttpContextBase> context = new Mock<HttpContextBase>();
+ context.Setup(c => c.Items).Returns(new Dictionary<object, object>());
+
+ return context.Object;
+ }
+
+ private static ITemplateFile GetTemplateFile(string path = null)
+ {
+ Mock<ITemplateFile> templateFile = new Mock<ITemplateFile>();
+ templateFile.Setup(f => f.TemplateInfo).Returns(new TemplateFileInfo(path));
+
+ return templateFile.Object;
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/WebPage/UrlDataTest.cs b/test/System.Web.WebPages.Test/WebPage/UrlDataTest.cs
new file mode 100644
index 00000000..5f2de652
--- /dev/null
+++ b/test/System.Web.WebPages.Test/WebPage/UrlDataTest.cs
@@ -0,0 +1,111 @@
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.WebPages.Test
+{
+ public class UrlDataTest
+ {
+ [Fact]
+ public void UrlDataListConstructorTests()
+ {
+ Assert.NotNull(new UrlDataList(null));
+ Assert.NotNull(new UrlDataList(String.Empty));
+ Assert.NotNull(new UrlDataList("abc/foo"));
+ }
+
+ [Fact]
+ public void AddTest()
+ {
+ var d = new UrlDataList(null);
+ var item = "!!@#$#$";
+ Assert.Throws<NotSupportedException>(() => { d.Add(item); }, "The UrlData collection is read-only.");
+ }
+
+ [Fact]
+ public void ClearTest()
+ {
+ var d = new UrlDataList(null);
+ Assert.Throws<NotSupportedException>(() => { d.Clear(); }, "The UrlData collection is read-only.");
+ }
+
+ [Fact]
+ public void IndexOfTest()
+ {
+ var item = "!!@#$#$";
+ var item2 = "13l53125";
+ var d = new UrlDataList(item + "/" + item2);
+ Assert.True(d.IndexOf(item) == 0);
+ Assert.True(d.IndexOf(item2) == 1);
+ }
+
+ [Fact]
+ public void InsertAtTest()
+ {
+ var d = new UrlDataList("x/y/z");
+ Assert.Throws<NotSupportedException>(() => { d.Insert(1, "a"); }, "The UrlData collection is read-only.");
+ }
+
+ [Fact]
+ public void ContainsTest()
+ {
+ var item = "!!@#$#$";
+ var d = new UrlDataList(item);
+ Assert.True(d.Contains(item));
+ }
+
+ [Fact]
+ public void CopyToTest()
+ {
+ var d = new UrlDataList("x/y");
+ string[] array = new string[2];
+ d.CopyTo(array, 0);
+ Assert.Equal(array[0], d[0]);
+ Assert.Equal(array[1], d[1]);
+ }
+
+ [Fact]
+ public void GetEnumeratorTest()
+ {
+ var d = new UrlDataList("x");
+ var e = d.GetEnumerator();
+ e.MoveNext();
+ Assert.Equal("x", e.Current);
+ }
+
+ [Fact]
+ public void RemoveTest()
+ {
+ var d = new UrlDataList("x");
+ Assert.Throws<NotSupportedException>(() => { d.Remove("x"); }, "The UrlData collection is read-only.");
+ }
+
+ [Fact]
+ public void RemoveAtTest()
+ {
+ var d = new UrlDataList("x/y");
+ Assert.Throws<NotSupportedException>(() => { d.RemoveAt(0); }, "The UrlData collection is read-only.");
+ }
+
+ [Fact]
+ public void CountTest()
+ {
+ var d = new UrlDataList("x");
+ Assert.Equal(1, d.Count);
+ }
+
+ [Fact]
+ public void IsReadOnlyTest()
+ {
+ var d = new UrlDataList(null);
+ Assert.Equal(true, d.IsReadOnly);
+ }
+
+ [Fact]
+ public void ItemTest()
+ {
+ var d = new UrlDataList("x/y");
+ Assert.Equal("x", d[0]);
+ Assert.Equal("y", d[1]);
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/WebPage/Utils.cs b/test/System.Web.WebPages.Test/WebPage/Utils.cs
new file mode 100644
index 00000000..2651e950
--- /dev/null
+++ b/test/System.Web.WebPages.Test/WebPage/Utils.cs
@@ -0,0 +1,261 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.IO;
+using System.Reflection;
+using System.Text;
+using System.Web.Hosting;
+using System.Web.WebPages.TestUtils;
+using Moq;
+
+namespace System.Web.WebPages.Test
+{
+ public static class Utils
+ {
+ public static string RenderWebPage(WebPage page, StartPage startPage = null, HttpRequestBase request = null)
+ {
+ var writer = new StringWriter();
+
+ // Create an actual dummy HttpContext that has a request object
+ var filename = "default.aspx";
+ var url = "http://localhost/default.aspx";
+
+ request = request ?? CreateTestRequest(filename, url).Object;
+ var httpContext = CreateTestContext(request);
+
+ var pageContext = new WebPageContext { HttpContext = httpContext.Object };
+ page.ExecutePageHierarchy(pageContext, writer, startPage);
+ return writer.ToString();
+ }
+
+ public static Mock<HttpContextBase> CreateTestContext(HttpRequestBase request = null, HttpResponseBase response = null, IDictionary items = null)
+ {
+ items = items ?? new Hashtable();
+ request = request ?? CreateTestRequest("default.cshtml", "http://localhost/default.cshtml").Object;
+
+ if (response == null)
+ {
+ var mockResponse = new Mock<HttpResponseBase>();
+ mockResponse.Setup(r => r.Cookies).Returns(new HttpCookieCollection());
+ response = mockResponse.Object;
+ }
+
+ var httpContext = new Mock<HttpContextBase>();
+ httpContext.SetupGet(c => c.Items).Returns(items);
+ httpContext.SetupGet(c => c.Request).Returns(request);
+ httpContext.SetupGet(c => c.Response).Returns(response);
+ return httpContext;
+ }
+
+ public static Mock<HttpRequestBase> CreateTestRequest(string filename, string url)
+ {
+ var mockRequest = new Mock<HttpRequestBase> { CallBase = true };
+ mockRequest.SetupGet(r => r.Path).Returns(filename);
+ mockRequest.SetupGet(r => r.RawUrl).Returns(url);
+ mockRequest.SetupGet(r => r.IsLocal).Returns(false);
+ mockRequest.SetupGet(r => r.QueryString).Returns(new NameValueCollection());
+ mockRequest.SetupGet(r => r.Browser.IsMobileDevice).Returns(false);
+ mockRequest.SetupGet(r => r.Cookies).Returns(new HttpCookieCollection());
+ mockRequest.SetupGet(r => r.UserAgent).Returns(String.Empty);
+
+ return mockRequest;
+ }
+
+ public static string RenderWebPage(Action<WebPage> pageExecuteAction, string pagePath = "~/index.cshtml")
+ {
+ var page = CreatePage(pageExecuteAction, pagePath);
+ return RenderWebPage(page);
+ }
+
+ public static MockPage CreatePage(Action<WebPage> pageExecuteAction, string pagePath = "~/index.cshtml")
+ {
+ var page = new MockPage()
+ {
+ VirtualPath = pagePath,
+ ExecuteAction = p => { pageExecuteAction(p); }
+ };
+ page.VirtualPathFactory = new HashVirtualPathFactory(page);
+ page.DisplayModeProvider = new DisplayModeProvider();
+ return page;
+ }
+
+ public static MockStartPage CreateStartPage(Action<StartPage> pageExecuteAction, string pagePath = "~/_pagestart.cshtml")
+ {
+ var page = new MockStartPage()
+ {
+ VirtualPath = pagePath,
+ ExecuteAction = p => { pageExecuteAction(p); }
+ };
+ page.VirtualPathFactory = new HashVirtualPathFactory(page);
+ page.DisplayModeProvider = new DisplayModeProvider();
+ return page;
+ }
+
+ public static string RenderWebPageWithSubPage(Action<WebPage> pageExecuteAction, Action<WebPage> subpageExecuteAction,
+ string pagePath = "~/index.cshtml", string subpagePath = "~/subpage.cshtml")
+ {
+ var page = CreatePage(pageExecuteAction);
+ var subPage = CreatePage(subpageExecuteAction, subpagePath);
+ var virtualPathFactory = new HashVirtualPathFactory(page, subPage);
+ subPage.VirtualPathFactory = virtualPathFactory;
+ page.VirtualPathFactory = virtualPathFactory;
+ return RenderWebPage(page);
+ }
+
+ // E.g. "default.aspx", "http://localhost/WebSite1/subfolder1/default.aspx"
+ /// <summary>
+ /// Creates an instance of HttpContext and assigns it to HttpContext.Current. Ensure that the returned value is disposed at the end of the test.
+ /// </summary>
+ /// <returns>Returns an IDisposable that restores the original HttpContext.</returns>
+ internal static IDisposable CreateHttpContext(string filename, string url)
+ {
+ var request = new HttpRequest(filename, url, null);
+ var httpContext = new HttpContext(request, new HttpResponse(new StringWriter(new StringBuilder())));
+ HttpContext.Current = httpContext;
+
+ return new DisposableAction(RestoreHttpContext);
+ }
+
+ internal static void RestoreHttpContext()
+ {
+ HttpContext.Current = null;
+ }
+
+ internal static IDisposable CreateHttpRuntime(string appVPath)
+ {
+ return WebUtils.CreateHttpRuntime(appVPath);
+ }
+
+ public static void SetupVirtualPathInAppDomain(string vpath, string contents)
+ {
+ var file = new Mock<VirtualFile>(vpath);
+ file.Setup(f => f.Open()).Returns(new MemoryStream(ASCIIEncoding.Default.GetBytes(contents)));
+ var vpp = new Mock<VirtualPathProvider>();
+ vpp.Setup(p => p.FileExists(vpath)).Returns(true);
+ vpp.Setup(p => p.GetFile(vpath)).Returns(file.Object);
+ AppDomainUtils.SetAppData();
+ var env = new HostingEnvironment();
+
+ var register = typeof(HostingEnvironment).GetMethod("RegisterVirtualPathProviderInternal", BindingFlags.Static | BindingFlags.NonPublic);
+ register.Invoke(null, new object[] { vpp.Object });
+ }
+
+ /// <summary>
+ /// Assigns a common object factory to the pages.
+ /// </summary>
+ internal static IVirtualPathFactory AssignObjectFactoriesAndDisplayModeProvider(params WebPageExecutingBase[] pages)
+ {
+ var objectFactory = new HashVirtualPathFactory(pages);
+ var displayModeProvider = new DisplayModeProvider();
+ foreach (var item in pages)
+ {
+ item.VirtualPathFactory = objectFactory;
+ var webPageRenderingBase = item as WebPageRenderingBase;
+ if (webPageRenderingBase != null)
+ {
+ webPageRenderingBase.DisplayModeProvider = displayModeProvider;
+ }
+ }
+
+ return objectFactory;
+ }
+
+ internal static DisplayModeProvider AssignDisplayModeProvider(params WebPageRenderingBase[] pages)
+ {
+ var displayModeProvider = new DisplayModeProvider();
+ foreach (var item in pages)
+ {
+ item.DisplayModeProvider = displayModeProvider;
+ }
+ return displayModeProvider;
+ }
+ }
+
+ public class MockPageHelper
+ {
+ internal static string GetDirectory(string virtualPath)
+ {
+ var dir = Path.GetDirectoryName(virtualPath);
+ if (dir == "~")
+ {
+ return null;
+ }
+ return dir;
+ }
+ }
+
+ // This is a mock implementation of WebPage mainly to make the Render method work and
+ // generate a string.
+ // The Execute method simulates what is typically generated based on markup by the parsers.
+ public class MockPage : WebPage
+ {
+ public Action<WebPage> ExecuteAction { get; set; }
+
+ internal override string GetDirectory(string virtualPath)
+ {
+ return MockPageHelper.GetDirectory(virtualPath);
+ }
+
+ public override void Execute()
+ {
+ ExecuteAction(this);
+ }
+ }
+
+ public class MockStartPage : StartPage
+ {
+ public Action<StartPage> ExecuteAction { get; set; }
+
+ internal override string GetDirectory(string virtualPath)
+ {
+ return MockPageHelper.GetDirectory(virtualPath);
+ }
+
+ public override void Execute()
+ {
+ ExecuteAction(this);
+ }
+
+ public Dictionary<string, object> CombinedPageInstances
+ {
+ get
+ {
+ var combinedInstances = new Dictionary<string, object>();
+ var instances = new Dictionary<string, object>();
+ WebPageRenderingBase childPage = this;
+ while (childPage != null)
+ {
+ if (childPage is MockStartPage)
+ {
+ var initPage = childPage as MockStartPage;
+ childPage = initPage.ChildPage;
+ }
+ else if (childPage is MockPage)
+ {
+ childPage = null;
+ }
+ foreach (var kvp in instances)
+ {
+ combinedInstances.Add(kvp.Key, kvp.Value);
+ }
+ }
+ return combinedInstances;
+ }
+ }
+ }
+
+ public class MockHttpRuntime
+ {
+ public static Version RequestValidationMode { get; set; }
+ }
+
+ public class MockHttpApplication
+ {
+ public static Type ModuleType { get; set; }
+
+ public static void RegisterModule(Type moduleType)
+ {
+ ModuleType = moduleType;
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/WebPage/VirtualPathFactoryExtensionsTest.cs b/test/System.Web.WebPages.Test/WebPage/VirtualPathFactoryExtensionsTest.cs
new file mode 100644
index 00000000..50bc393e
--- /dev/null
+++ b/test/System.Web.WebPages.Test/WebPage/VirtualPathFactoryExtensionsTest.cs
@@ -0,0 +1,61 @@
+using Moq;
+using Xunit;
+
+namespace System.Web.WebPages.Test
+{
+ public class VirtualPathFactoryExtensionsTest
+ {
+ [Fact]
+ public void VirtualPathFactoryExtensionsSpecialCasesVirtualPathFactoryManager()
+ {
+ // Arrange
+ var virtualPath = "~/index.cshtml";
+ var mockPage = Utils.CreatePage(_ => { }, virtualPath);
+ var factory = new Mock<IVirtualPathFactory>();
+ factory.Setup(c => c.Exists(virtualPath)).Returns(true).Verifiable();
+ factory.Setup(c => c.CreateInstance(virtualPath)).Returns(mockPage);
+
+ // Act
+ var factoryManager = new VirtualPathFactoryManager(factory.Object);
+ var page = factoryManager.CreateInstance<WebPageBase>(virtualPath);
+
+ // Assert
+ Assert.Equal(mockPage, page);
+ factory.Verify();
+ }
+
+ [Fact]
+ public void GenericCreateInstanceLoopsOverAllRegisteredFactories()
+ {
+ // Arrange
+ var virtualPath = "~/index.cshtml";
+ var mockPage = Utils.CreatePage(_ => { }, virtualPath);
+ var factory1 = new HashVirtualPathFactory(mockPage);
+ var factory2 = new HashVirtualPathFactory(Utils.CreatePage(null, "~/_admin/index.cshtml"));
+
+ // Act
+ var factoryManager = new VirtualPathFactoryManager(factory2);
+ factoryManager.RegisterVirtualPathFactoryInternal(factory1);
+ var page = factoryManager.CreateInstance<WebPageBase>(virtualPath);
+
+ // Assert
+ Assert.Equal(mockPage, page);
+ }
+
+ [Fact]
+ public void GenericCreateInstanceReturnsNullIfNoFactoryCanCreateVirtualPath()
+ {
+ // Arrange
+ var factory1 = new HashVirtualPathFactory(Utils.CreatePage(_ => { }, "~/index.cshtml"));
+ var factory2 = new HashVirtualPathFactory(Utils.CreatePage(null, "~/_admin/index.cshtml"));
+
+ // Act
+ var factoryManager = new VirtualPathFactoryManager(factory2);
+ factoryManager.RegisterVirtualPathFactoryInternal(factory1);
+ var page = factoryManager.CreateInstance<WebPageBase>("~/does-not-exist.cshtml");
+
+ // Assert
+ Assert.Null(page);
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/WebPage/VirtualPathFactoryManagerTest.cs b/test/System.Web.WebPages.Test/WebPage/VirtualPathFactoryManagerTest.cs
new file mode 100644
index 00000000..d68ab21d
--- /dev/null
+++ b/test/System.Web.WebPages.Test/WebPage/VirtualPathFactoryManagerTest.cs
@@ -0,0 +1,103 @@
+using System.Linq;
+using Moq;
+using Xunit;
+
+namespace System.Web.WebPages.Test
+{
+ public class VirtualPathFactoryManagerTest
+ {
+ [Fact]
+ public void DefaultFactoryIsListedInRegisteredFactories()
+ {
+ // Arrange
+ var factory = new HashVirtualPathFactory();
+
+ // Act
+ var factoryManager = new VirtualPathFactoryManager(factory);
+
+ // Assert
+ Assert.Equal(factory, factoryManager.RegisteredFactories.Single());
+ }
+
+ [Fact]
+ public void RegisterFactoryEnsuresDefaultFactoryRemainsTheLast()
+ {
+ // Arrange
+ var defaultFactory = new HashVirtualPathFactory();
+ var factory1 = new HashVirtualPathFactory();
+ var factory2 = new HashVirtualPathFactory();
+ var factory3 = new HashVirtualPathFactory();
+
+ // Act
+ var factoryManager = new VirtualPathFactoryManager(defaultFactory);
+ factoryManager.RegisterVirtualPathFactoryInternal(factory1);
+ factoryManager.RegisterVirtualPathFactoryInternal(factory2);
+ factoryManager.RegisterVirtualPathFactoryInternal(factory3);
+
+ // Assert
+ Assert.Equal(factory1, factoryManager.RegisteredFactories.ElementAt(0));
+ Assert.Equal(factory2, factoryManager.RegisteredFactories.ElementAt(1));
+ Assert.Equal(factory3, factoryManager.RegisteredFactories.ElementAt(2));
+ Assert.Equal(defaultFactory, factoryManager.RegisteredFactories.Last());
+ }
+
+ [Fact]
+ public void CreateInstanceUsesRegisteredFactoriesForExistence()
+ {
+ // Arrange
+ var path = "~/index.cshtml";
+ var factory1 = new Mock<IVirtualPathFactory>();
+ factory1.Setup(c => c.Exists(path)).Returns(false).Verifiable();
+ var factory2 = new Mock<IVirtualPathFactory>();
+ factory2.Setup(c => c.Exists(path)).Returns(true).Verifiable();
+ var factory3 = new Mock<IVirtualPathFactory>();
+ factory3.Setup(c => c.Exists(path)).Throws(new Exception("This factory should not be called since the page has already been found in 2"));
+ var defaultFactory = new Mock<IVirtualPathFactory>();
+ defaultFactory.Setup(c => c.Exists(path)).Throws(new Exception("This factory should not be called since it always called last"));
+
+ var vpfm = new VirtualPathFactoryManager(defaultFactory.Object);
+ vpfm.RegisterVirtualPathFactoryInternal(factory1.Object);
+ vpfm.RegisterVirtualPathFactoryInternal(factory2.Object);
+ vpfm.RegisterVirtualPathFactoryInternal(factory3.Object);
+
+ // Act
+ var result = vpfm.Exists(path);
+
+ // Assert
+ Assert.True(result);
+
+ factory1.Verify();
+ factory2.Verify();
+ }
+
+ [Fact]
+ public void CreateInstanceLooksThroughAllRegisteredFactoriesForExistence()
+ {
+ // Arrange
+ var page = Utils.CreatePage(null);
+ var factory1 = new Mock<IVirtualPathFactory>();
+ factory1.Setup(c => c.Exists(page.VirtualPath)).Returns(false).Verifiable();
+ var factory2 = new Mock<IVirtualPathFactory>();
+ factory2.Setup(c => c.Exists(page.VirtualPath)).Returns(true).Verifiable();
+ factory2.Setup(c => c.CreateInstance(page.VirtualPath)).Returns(page).Verifiable();
+ var factory3 = new Mock<IVirtualPathFactory>();
+ factory3.Setup(c => c.Exists(page.VirtualPath)).Throws(new Exception("This factory should not be called since the page has already been found in 2"));
+ var defaultFactory = new Mock<IVirtualPathFactory>();
+ defaultFactory.Setup(c => c.Exists(page.VirtualPath)).Throws(new Exception("This factory should not be called since it always called last"));
+
+ var vpfm = new VirtualPathFactoryManager(defaultFactory.Object);
+ vpfm.RegisterVirtualPathFactoryInternal(factory1.Object);
+ vpfm.RegisterVirtualPathFactoryInternal(factory2.Object);
+ vpfm.RegisterVirtualPathFactoryInternal(factory3.Object);
+
+ // Act
+ var result = vpfm.CreateInstance(page.VirtualPath);
+
+ // Assert
+ Assert.Equal(page, result);
+
+ factory1.Verify();
+ factory2.Verify();
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/WebPage/WebPageContextTest.cs b/test/System.Web.WebPages.Test/WebPage/WebPageContextTest.cs
new file mode 100644
index 00000000..4e466571
--- /dev/null
+++ b/test/System.Web.WebPages.Test/WebPage/WebPageContextTest.cs
@@ -0,0 +1,57 @@
+using System.Collections.Generic;
+using System.IO;
+using Moq;
+using Xunit;
+
+namespace System.Web.WebPages.Test
+{
+ public class WebPageContextTest
+ {
+ [Fact]
+ public void CreateNestedPageContextCopiesPropertiesFromParentPageContext()
+ {
+ // Arrange
+ var httpContext = new Mock<HttpContextBase>();
+ var pageDataDictionary = new Dictionary<object, dynamic>();
+ var model = new { Hello = "World" };
+ Action<TextWriter> bodyAction = writer => { };
+ var sectionWritersStack = new Stack<Dictionary<string, SectionWriter>>();
+ var basePageContext = new WebPageContext(httpContext.Object, null, null) { BodyAction = bodyAction, SectionWritersStack = sectionWritersStack };
+
+ // Act
+ var subPageContext = WebPageContext.CreateNestedPageContext(basePageContext, pageDataDictionary, model, isLayoutPage: false);
+
+ // Assert
+ Assert.Equal(basePageContext.HttpContext, subPageContext.HttpContext);
+ Assert.Equal(basePageContext.OutputStack, subPageContext.OutputStack);
+ Assert.Equal(basePageContext.Validation, subPageContext.Validation);
+ Assert.Equal(pageDataDictionary, subPageContext.PageData);
+ Assert.Equal(model, subPageContext.Model);
+ Assert.Null(subPageContext.BodyAction);
+ }
+
+ [Fact]
+ public void CreateNestedPageCopiesBodyActionAndSectionWritersWithOtherPropertiesFromParentPageContext()
+ {
+ // Arrange
+ var httpContext = new Mock<HttpContextBase>();
+ var pageDataDictionary = new Dictionary<object, dynamic>();
+ var model = new { Hello = "World" };
+ Action<TextWriter> bodyAction = writer => { };
+ var sectionWritersStack = new Stack<Dictionary<string, SectionWriter>>();
+ var basePageContext = new WebPageContext(httpContext.Object, null, null) { BodyAction = bodyAction, SectionWritersStack = sectionWritersStack };
+
+ // Act
+ var subPageContext = WebPageContext.CreateNestedPageContext(basePageContext, pageDataDictionary, model, isLayoutPage: true);
+
+ // Assert
+ Assert.Equal(basePageContext.HttpContext, subPageContext.HttpContext);
+ Assert.Equal(basePageContext.OutputStack, subPageContext.OutputStack);
+ Assert.Equal(basePageContext.Validation, subPageContext.Validation);
+ Assert.Equal(pageDataDictionary, subPageContext.PageData);
+ Assert.Equal(model, subPageContext.Model);
+ Assert.Equal(sectionWritersStack, subPageContext.SectionWritersStack);
+ Assert.Equal(bodyAction, subPageContext.BodyAction);
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/WebPage/WebPageExecutingBaseTest.cs b/test/System.Web.WebPages.Test/WebPage/WebPageExecutingBaseTest.cs
new file mode 100644
index 00000000..211e4e8b
--- /dev/null
+++ b/test/System.Web.WebPages.Test/WebPage/WebPageExecutingBaseTest.cs
@@ -0,0 +1,189 @@
+using System.IO;
+using System.Text;
+using System.Web.WebPages.Instrumentation;
+using Moq;
+using Xunit;
+
+namespace System.Web.WebPages.Test
+{
+ public class WebPageExecutingBaseTest
+ {
+ [Fact]
+ public void NormalizeLayoutPageUsesVirtualPathFactoryManagerToDetermineIfLayoutFileExists()
+ {
+ // Arrange
+ var layoutPagePath = "~/sitelayout.cshtml";
+ var layoutPage = Utils.CreatePage(null, layoutPagePath);
+ var page = Utils.CreatePage(null);
+ var objectFactory = new Mock<IVirtualPathFactory>();
+ objectFactory.Setup(c => c.Exists(It.Is<string>(p => p.Equals(layoutPagePath)))).Returns(true).Verifiable();
+ page.VirtualPathFactory = objectFactory.Object;
+
+ // Act
+ var path = page.NormalizeLayoutPagePath(layoutPage.VirtualPath);
+
+ // Assert
+ objectFactory.Verify();
+ Assert.Equal(path, layoutPage.VirtualPath);
+ }
+
+ [Fact]
+ public void NormalizeLayoutPageAcceptsRelativePathsToLayoutPage()
+ {
+ // Arrange
+ var page = Utils.CreatePage(null, "~/dir/default.cshtml");
+ var layoutPage = Utils.CreatePage(null, "~/layouts/sitelayout.cshtml");
+ var objectFactory = new HashVirtualPathFactory(page, layoutPage);
+ page.VirtualPathFactory = objectFactory;
+
+ // Act
+ var path = page.NormalizeLayoutPagePath(@"../layouts/sitelayout.cshtml");
+
+ // Assert
+ Assert.Equal(path, layoutPage.VirtualPath);
+ }
+
+ [Fact]
+ public void BeginContextSilentlyFailsIfInstrumentationIsNotAvailable()
+ {
+ // Arrange
+ bool called = false;
+
+ var pageMock = new Mock<WebPageExecutingBase>() { CallBase = true };
+ pageMock.Setup(p => p.Context).Returns(new Mock<HttpContextBase>().Object);
+ pageMock.Object.InstrumentationService.IsAvailable = false;
+ pageMock.Object.InstrumentationService.ExtractInstrumentationService = c =>
+ {
+ called = true;
+ return null;
+ };
+
+ // Act
+ pageMock.Object.BeginContext("~/dir/default.cshtml", 0, 1, true);
+
+ // Assert
+ Assert.False(called);
+ }
+
+ [Fact]
+ public void EndContextSilentlyFailsIfInstrumentationIsNotAvailable()
+ {
+ // Arrange
+ bool called = false;
+
+ var pageMock = new Mock<WebPageExecutingBase>() { CallBase = true };
+ pageMock.Setup(p => p.Context).Returns(new Mock<HttpContextBase>().Object);
+ pageMock.Object.InstrumentationService.IsAvailable = false;
+ pageMock.Object.InstrumentationService.ExtractInstrumentationService = c =>
+ {
+ called = true;
+ return null;
+ };
+
+ // Act
+ pageMock.Object.EndContext("~/dir/default.cshtml", 0, 1, true);
+
+ // Assert
+ Assert.False(called);
+ }
+
+ [Fact]
+ public void WriteAttributeToWritesAttributeNormallyIfNoValuesSpecified()
+ {
+ WriteAttributeTest(
+ name: "alt",
+ prefix: new PositionTagged<string>(" alt=\"", 42),
+ suffix: new PositionTagged<string>("\"", 24),
+ expected: " alt=\"\"");
+ }
+
+ [Fact]
+ public void WriteAttributeToWritesNothingIfSingleNullValueProvided()
+ {
+ WriteAttributeTest(
+ name: "alt",
+ prefix: new PositionTagged<string>(" alt=\"", 42),
+ suffix: new PositionTagged<string>("\"", 24),
+ values: new[] {
+ new AttributeValue(new PositionTagged<string>(String.Empty, 142), new PositionTagged<object>(null, 124), literal: true)
+ },
+ expected: "");
+ }
+
+ [Fact]
+ public void WriteAttributeToWritesNothingIfSingleFalseValueProvided()
+ {
+ WriteAttributeTest(
+ name: "alt",
+ prefix: new PositionTagged<string>(" alt=\"", 42),
+ suffix: new PositionTagged<string>("\"", 24),
+ values: new[] {
+ new AttributeValue(new PositionTagged<string>(String.Empty, 142), new PositionTagged<object>(false, 124), literal: true)
+ },
+ expected: "");
+ }
+
+ [Fact]
+ public void WriteAttributeToWritesGlobalPrefixIfSingleValueProvided()
+ {
+ WriteAttributeTest(
+ name: "alt",
+ prefix: new PositionTagged<string>(" alt=\"", 42),
+ suffix: new PositionTagged<string>("\"", 24),
+ values: new[] {
+ new AttributeValue(new PositionTagged<string>(" ", 142), new PositionTagged<object>("foo", 124), literal: true)
+ },
+ expected: " alt=\"foo\"");
+ }
+
+ [Fact]
+ public void WriteAttributeToWritesLocalPrefixForSecondValueProvided()
+ {
+ WriteAttributeTest(
+ name: "alt",
+ prefix: new PositionTagged<string>(" alt=\"", 42),
+ suffix: new PositionTagged<string>("\"", 24),
+ values: new[] {
+ new AttributeValue(new PositionTagged<string>(" ", 142), new PositionTagged<object>("foo", 124), literal: true),
+ new AttributeValue(new PositionTagged<string>("glorb", 142), new PositionTagged<object>("bar", 124), literal: true)
+ },
+ expected: " alt=\"fooglorbbar\"");
+ }
+
+ [Fact]
+ public void WriteAttributeToWritesGlobalPrefixOnlyIfSecondValueIsFirstNonNullOrFalse()
+ {
+ WriteAttributeTest(
+ name: "alt",
+ prefix: new PositionTagged<string>(" alt=\"", 42),
+ suffix: new PositionTagged<string>("\"", 24),
+ values: new[] {
+ new AttributeValue(new PositionTagged<string>(" ", 142), new PositionTagged<object>(null, 124), literal: true),
+ new AttributeValue(new PositionTagged<string>("glorb", 142), new PositionTagged<object>("bar", 124), literal: true)
+ },
+ expected: " alt=\"bar\"");
+ }
+
+ private void WriteAttributeTest(string name, PositionTagged<string> prefix, PositionTagged<string> suffix, string expected)
+ {
+ WriteAttributeTest(name, prefix, suffix, new AttributeValue[0], expected);
+ }
+
+ private void WriteAttributeTest(string name, PositionTagged<string> prefix, PositionTagged<string> suffix, AttributeValue[] values, string expected)
+ {
+ // Arrange
+ var pageMock = new Mock<WebPageExecutingBase>() { CallBase = true };
+ pageMock.Setup(p => p.Context).Returns(new Mock<HttpContextBase>().Object);
+ pageMock.Object.InstrumentationService.IsAvailable = false;
+
+ StringBuilder written = new StringBuilder();
+ StringWriter writer = new StringWriter(written);
+
+ // Act
+ pageMock.Object.WriteAttributeTo(writer, name, prefix, suffix, values);
+
+ // Assert
+ Assert.Equal(expected, written.ToString());
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/WebPage/WebPageHttpHandlerTest.cs b/test/System.Web.WebPages.Test/WebPage/WebPageHttpHandlerTest.cs
new file mode 100644
index 00000000..902ac3d3
--- /dev/null
+++ b/test/System.Web.WebPages.Test/WebPage/WebPageHttpHandlerTest.cs
@@ -0,0 +1,249 @@
+using System.Collections;
+using System.Collections.Specialized;
+using System.IO;
+using System.Security;
+using System.Text;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.WebPages.Test
+{
+ public class WebPageHttpHandlerTest
+ {
+ [Fact]
+ public void ConstructorThrowsWithNullPage()
+ {
+ Assert.ThrowsArgumentNull(() => new WebPageHttpHandler(null), "webPage");
+ }
+
+ [Fact]
+ public void IsReusableTest()
+ {
+ WebPage dummyPage = new DummyPage();
+ Assert.False(new WebPageHttpHandler(dummyPage).IsReusable);
+ }
+
+ [Fact]
+ public void ProcessRequestTest()
+ {
+ var contents = "test";
+ var tw = new StringWriter(new StringBuilder());
+ var httpContext = CreateTestContext(tw);
+ var page = Utils.CreatePage(p => p.Write(contents));
+ new WebPageHttpHandler(page).ProcessRequestInternal(httpContext);
+ Assert.Equal(contents, tw.ToString());
+ }
+
+ [Fact]
+ public void SourceFileHeaderTest()
+ {
+ // Arrange
+ var contents = "test";
+ var writer = new StringWriter();
+
+ Mock<HttpResponseBase> httpResponse = new Mock<HttpResponseBase>();
+ httpResponse.SetupGet(r => r.Output).Returns(writer);
+ Mock<HttpRequestBase> httpRequest = Utils.CreateTestRequest("~/index.cshtml", "~/index.cshtml");
+ httpRequest.SetupGet(r => r.IsLocal).Returns(true);
+ httpRequest.Setup(r => r.MapPath(It.IsAny<string>())).Returns<string>(p => p);
+ Mock<HttpContextBase> context = Utils.CreateTestContext(httpRequest.Object, httpResponse.Object);
+ var page = Utils.CreatePage(p => p.Write(contents));
+
+ // Act
+ var webPageHttpHandler = new WebPageHttpHandler(page);
+ webPageHttpHandler.ProcessRequestInternal(context.Object);
+
+ // Assert
+ Assert.Equal(contents, writer.ToString());
+ Assert.Equal(1, page.PageContext.SourceFiles.Count);
+ Assert.True(page.PageContext.SourceFiles.Contains("~/index.cshtml"));
+ }
+
+ [Fact]
+ public void GenerateSourceFilesHeaderGenerates2047EncodedValue()
+ {
+ // Arrange
+ string headerKey = null, headerValue = null;
+ var context = new Mock<HttpContextBase>();
+ var response = new Mock<HttpResponseBase>();
+ response.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback(
+ (string key, string value) =>
+ {
+ headerKey = key;
+ headerValue = value;
+ });
+ context.Setup(c => c.Response).Returns(response.Object);
+ context.Setup(c => c.Items).Returns(new Hashtable());
+
+ var webPageContext = new WebPageContext(context.Object, page: null, model: null);
+ webPageContext.SourceFiles.Add("foo");
+ webPageContext.SourceFiles.Add("bar");
+ webPageContext.SourceFiles.Add("λ");
+
+ // Act
+ WebPageHttpHandler.GenerateSourceFilesHeader(webPageContext);
+
+ // Assert
+ Assert.Equal(headerKey, "X-SourceFiles");
+ Assert.Equal(headerValue, "=?UTF-8?B?Zm9vfGJhcnzOuw==?=");
+ }
+
+ [Fact]
+ public void HttpHandlerGeneratesSourceFilesHeadersIfRequestIsLocal()
+ {
+ // Arrange
+ string pagePath = "~/index.cshtml", layoutPath = "~/Layout.cshtml", layoutPageName = "Layout.cshtml";
+ var page = Utils.CreatePage(p => { p.Layout = layoutPageName; }, pagePath);
+ var layoutPage = Utils.CreatePage(p => { p.RenderBody(); }, layoutPath);
+ Utils.AssignObjectFactoriesAndDisplayModeProvider(layoutPage, page);
+
+
+ var headers = new NameValueCollection();
+ var request = Utils.CreateTestRequest(pagePath, pagePath);
+ request.Setup(c => c.IsLocal).Returns(true);
+ request.Setup(c => c.MapPath(It.IsAny<string>())).Returns<string>(path => path);
+ request.Setup(c => c.Cookies).Returns(new HttpCookieCollection());
+
+ var response = new Mock<HttpResponseBase>() { CallBase = true };
+ response.SetupGet(r => r.Headers).Returns(headers);
+ response.SetupGet(r => r.Output).Returns(TextWriter.Null);
+ response.Setup(r => r.AppendHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((name, value) => headers.Add(name, value));
+ response.Setup(r => r.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((name, value) => headers.Add(name, value));
+ response.Setup(r => r.Cookies).Returns(new HttpCookieCollection());
+
+ var context = Utils.CreateTestContext(request.Object, response.Object);
+
+ // Act
+ var webPageHttpHandler = new WebPageHttpHandler(page);
+ webPageHttpHandler.ProcessRequestInternal(context.Object);
+
+ // Assert
+ Assert.Equal("2.0", headers[WebPageHttpHandler.WebPagesVersionHeaderName]);
+ Assert.Equal("=?UTF-8?B?fi9pbmRleC5jc2h0bWx8fi9MYXlvdXQuY3NodG1s?=", headers["X-SourceFiles"]);
+ }
+
+ [Fact]
+ public void ExceptionTest()
+ {
+ var contents = "test";
+ var httpContext = Utils.CreateTestContext().Object;
+ var page = Utils.CreatePage(p => { throw new InvalidOperationException(contents); });
+ var e = Assert.Throws<HttpUnhandledException>(
+ () => new WebPageHttpHandler(page).ProcessRequestInternal(httpContext)
+ );
+ Assert.IsType<InvalidOperationException>(e.InnerException);
+ Assert.Equal(contents, e.InnerException.Message, StringComparer.Ordinal);
+ }
+
+ [Fact]
+ public void SecurityExceptionTest()
+ {
+ var contents = "test";
+ var httpContext = Utils.CreateTestContext().Object;
+ var page = Utils.CreatePage(p => { throw new SecurityException(contents); });
+ Assert.Throws<SecurityException>(
+ () => new WebPageHttpHandler(page).ProcessRequestInternal(httpContext),
+ contents);
+ }
+
+ [Fact]
+ public void CreateFromVirtualPathTest()
+ {
+ var contents = "test";
+ var textWriter = new StringWriter();
+
+ var httpResponse = new Mock<HttpResponseBase>();
+ httpResponse.SetupGet(r => r.Output).Returns(textWriter);
+ var httpContext = Utils.CreateTestContext(response: httpResponse.Object);
+ var mockBuildManager = new Mock<IVirtualPathFactory>();
+ var virtualPath = "~/hello/test.cshtml";
+ var page = Utils.CreatePage(p => p.Write(contents));
+ mockBuildManager.Setup(c => c.Exists(It.Is<string>(p => p.Equals(virtualPath)))).Returns<string>(_ => true).Verifiable();
+ mockBuildManager.Setup(c => c.CreateInstance(It.Is<string>(p => p.Equals(virtualPath)))).Returns(page).Verifiable();
+
+ // Act
+ IHttpHandler handler = WebPageHttpHandler.CreateFromVirtualPath(virtualPath, new VirtualPathFactoryManager(mockBuildManager.Object));
+ Assert.IsType<WebPageHttpHandler>(handler);
+ WebPageHttpHandler webPageHttpHandler = (WebPageHttpHandler)handler;
+ webPageHttpHandler.ProcessRequestInternal(httpContext.Object);
+
+ // Assert
+ Assert.Equal(contents, textWriter.ToString());
+ mockBuildManager.Verify();
+ }
+
+ [Fact]
+ public void VersionHeaderTest()
+ {
+ bool headerSet = false;
+ Mock<HttpResponseBase> mockResponse = new Mock<HttpResponseBase>();
+ mockResponse.Setup(response => response.AppendHeader("X-AspNetWebPages-Version", "2.0")).Callback(() => headerSet = true);
+
+ Mock<HttpContextBase> mockContext = new Mock<HttpContextBase>();
+ mockContext.SetupGet(context => context.Response).Returns(mockResponse.Object);
+
+ WebPageHttpHandler.AddVersionHeader(mockContext.Object);
+ Assert.True(headerSet);
+ }
+
+ [Fact]
+ public void CreateFromVirtualPathNonWebPageTest()
+ {
+ // Arrange
+ var virtualPath = "~/hello/test.cshtml";
+ var handler = new WebPageHttpHandler(new DummyPage());
+ var mockBuildManager = new Mock<IVirtualPathFactory>();
+ mockBuildManager.Setup(c => c.CreateInstance(It.IsAny<string>())).Returns(handler);
+ mockBuildManager.Setup(c => c.Exists(It.Is<string>(p => p.Equals(virtualPath)))).Returns<string>(_ => true).Verifiable();
+
+ // Act
+ var result = WebPageHttpHandler.CreateFromVirtualPath(virtualPath, new VirtualPathFactoryManager(mockBuildManager.Object));
+
+ // Assert
+ Assert.Equal(handler, result);
+ mockBuildManager.Verify();
+ }
+
+ [Fact]
+ public void CreateFromVirtualPathReturnsIHttpHandlerIfItCannotCreateAWebPageType()
+ {
+ // Arrange
+ var pageVirtualPath = "~/hello/test.cshtml";
+ var handlerVirtualPath = "~/handler.asmx";
+ var page = new DummyPage();
+ var handler = new Mock<IHttpHandler>().Object;
+ var mockFactory = new Mock<IVirtualPathFactory>();
+ mockFactory.Setup(c => c.Exists(It.IsAny<string>())).Returns(true);
+ mockFactory.Setup(c => c.CreateInstance(pageVirtualPath)).Returns(page);
+ mockFactory.Setup(c => c.CreateInstance(handlerVirtualPath)).Returns(handler);
+
+ // Act
+ var handlerResult = WebPageHttpHandler.CreateFromVirtualPath(handlerVirtualPath, mockFactory.Object);
+ var pageResult = WebPageHttpHandler.CreateFromVirtualPath(pageVirtualPath, mockFactory.Object);
+
+ // Assert
+ Assert.Equal(handler, handlerResult);
+ Assert.NotNull(pageResult as WebPageHttpHandler);
+ }
+
+ private static HttpContextBase CreateTestContext(TextWriter textWriter)
+ {
+ var filename = "default.aspx";
+ var url = "http://localhost/WebSite1/subfolder1/default.aspx";
+ var request = Utils.CreateTestRequest(filename, url);
+
+ var response = new Mock<HttpResponseBase>();
+ response.SetupGet(r => r.Output).Returns(textWriter);
+
+ return Utils.CreateTestContext(request: request.Object, response: response.Object).Object;
+ }
+
+ private sealed class DummyPage : WebPage
+ {
+ public override void Execute()
+ {
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/test/System.Web.WebPages.Test/WebPage/WebPageHttpModuleTest.cs b/test/System.Web.WebPages.Test/WebPage/WebPageHttpModuleTest.cs
new file mode 100644
index 00000000..95866b35
--- /dev/null
+++ b/test/System.Web.WebPages.Test/WebPage/WebPageHttpModuleTest.cs
@@ -0,0 +1,83 @@
+using System.Web.WebPages.TestUtils;
+using Xunit;
+
+namespace System.Web.WebPages.Test
+{
+ public class WebPageHttpModuleTest
+ {
+ [Fact]
+ public void InitializeApplicationTest()
+ {
+ AppDomainUtils.RunInSeparateAppDomain(() =>
+ {
+ var moduleEvents = new ModuleEvents();
+ var app = new MyHttpApplication();
+ WebPageHttpModule.InitializeApplication(app,
+ moduleEvents.OnApplicationPostResolveRequestCache,
+ moduleEvents.Initialize);
+ Assert.True(moduleEvents.CalledInitialize);
+ });
+ }
+
+ [Fact]
+ public void StartApplicationTest()
+ {
+ AppDomainUtils.RunInSeparateAppDomain(() =>
+ {
+ var moduleEvents = new ModuleEvents();
+ var app = new MyHttpApplication();
+ WebPageHttpModule.StartApplication(app, moduleEvents.ExecuteStartPage, moduleEvents.ApplicationStart);
+ Assert.Equal(1, moduleEvents.CalledExecuteStartPage);
+ Assert.Equal(1, moduleEvents.CalledApplicationStart);
+
+ // Call a second time to make sure the methods are only called once
+ WebPageHttpModule.StartApplication(app, moduleEvents.ExecuteStartPage, moduleEvents.ApplicationStart);
+ Assert.Equal(1, moduleEvents.CalledExecuteStartPage);
+ Assert.Equal(1, moduleEvents.CalledApplicationStart);
+ });
+ }
+
+ public class MyHttpApplication : HttpApplication
+ {
+ public MyHttpApplication()
+ {
+ }
+ }
+
+ public class ModuleEvents
+ {
+ public void OnApplicationPostResolveRequestCache(object sender, EventArgs e)
+ {
+ }
+
+ public void OnBeginRequest(object sender, EventArgs e)
+ {
+ }
+
+ public void OnEndRequest(object sender, EventArgs e)
+ {
+ }
+
+ public bool CalledInitialize;
+
+ public void Initialize(object sender, EventArgs e)
+ {
+ CalledInitialize = true;
+ }
+
+ public int CalledExecuteStartPage;
+
+ public void ExecuteStartPage(HttpApplication application)
+ {
+ CalledExecuteStartPage++;
+ }
+
+ public int CalledApplicationStart;
+
+ public void ApplicationStart(object sender, EventArgs e)
+ {
+ CalledApplicationStart++;
+ }
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/WebPage/WebPageRenderingBaseTest.cs b/test/System.Web.WebPages.Test/WebPage/WebPageRenderingBaseTest.cs
new file mode 100644
index 00000000..66546446
--- /dev/null
+++ b/test/System.Web.WebPages.Test/WebPage/WebPageRenderingBaseTest.cs
@@ -0,0 +1,65 @@
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.WebPages.Test
+{
+ public class WebPageRenderingBaseTest
+ {
+ [Fact]
+ public void SetCultureThrowsIfValueIsNull()
+ {
+ // Arrange
+ string value = null;
+ var webPageRenderingBase = new Mock<WebPageRenderingBase>() { CallBase = true }.Object;
+
+ // Act and Assert
+ Assert.ThrowsArgumentNullOrEmptyString(() => webPageRenderingBase.Culture = value, "value");
+ }
+
+ [Fact]
+ public void SetCultureThrowsIfValueIsEmpty()
+ {
+ // Arrange
+ string value = String.Empty;
+ var webPageRenderingBase = new Mock<WebPageRenderingBase>() { CallBase = true }.Object;
+
+ // Act and Assert
+ Assert.ThrowsArgumentNullOrEmptyString(() => webPageRenderingBase.Culture = value, "value");
+ }
+
+ [Fact]
+ public void SetUICultureThrowsIfValueIsNull()
+ {
+ // Arrange
+ string value = null;
+ var webPageRenderingBase = new Mock<WebPageRenderingBase>() { CallBase = true }.Object;
+
+ // Act and Assert
+ Assert.ThrowsArgumentNullOrEmptyString(() => webPageRenderingBase.UICulture = value, "value");
+ }
+
+ [Fact]
+ public void SetUICultureThrowsIfValueIsEmpty()
+ {
+ // Arrange
+ string value = String.Empty;
+ var webPageRenderingBase = new Mock<WebPageRenderingBase>() { CallBase = true }.Object;
+
+ // Act and Assert
+ Assert.ThrowsArgumentNullOrEmptyString(() => webPageRenderingBase.UICulture = value, "value");
+ }
+
+ [Fact]
+ public void DisplayModePropertyWithNullContext()
+ {
+ // Arrange
+ var context = new Mock<HttpContextBase>();
+ var displayMode = new DefaultDisplayMode("test");
+ var webPageRenderingBase = new Mock<WebPageRenderingBase>() { CallBase = true };
+
+ // Act & Assert
+ Assert.Null(webPageRenderingBase.Object.DisplayMode);
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/WebPage/WebPageRouteTest.cs b/test/System.Web.WebPages.Test/WebPage/WebPageRouteTest.cs
new file mode 100644
index 00000000..2cd2b48e
--- /dev/null
+++ b/test/System.Web.WebPages.Test/WebPage/WebPageRouteTest.cs
@@ -0,0 +1,307 @@
+using System.Collections;
+using System.Collections.Generic;
+using Moq;
+using Xunit;
+using Xunit.Extensions;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.WebPages.Test
+{
+ public class WebPageRouteTest
+ {
+ private class HashyBuildManager : IVirtualPathFactory
+ {
+ private readonly HashSet<string> _existingFiles;
+
+ public HashyBuildManager(IEnumerable<string> validFilePaths)
+ {
+ _existingFiles = new HashSet<string>(validFilePaths, StringComparer.InvariantCultureIgnoreCase);
+ }
+
+ public bool Exists(string virtualPath)
+ {
+ return _existingFiles.Contains(virtualPath);
+ }
+
+ public object CreateInstance(string virtualPath)
+ {
+ throw new NotSupportedException();
+ }
+ }
+
+ // Helper to test smarty route match, null match string is used for no expected match
+ private static void ConstraintTest(IEnumerable<string> validFiles, IEnumerable<string> supportedExt, string url, string match, string pathInfo, bool mobileDevice = false)
+ {
+ var objectFactory = new HashyBuildManager(validFiles);
+ var mockContext = new Mock<HttpContextBase>();
+ mockContext.Setup(context => context.Items).Returns(new Hashtable());
+ mockContext.Setup(c => c.Request.Browser.IsMobileDevice).Returns(mobileDevice);
+ mockContext.Setup(c => c.Request.Cookies).Returns(new HttpCookieCollection());
+ mockContext.Setup(c => c.Response.Cookies).Returns(new HttpCookieCollection());
+ var displayModeProvider = new DisplayModeProvider();
+
+ WebPageMatch smartyMatch = WebPageRoute.MatchRequest(url, supportedExt, objectFactory, mockContext.Object, displayModeProvider);
+ if (match != null)
+ {
+ Assert.NotNull(smartyMatch);
+ Assert.Equal(match, smartyMatch.MatchedPath);
+ Assert.Equal(pathInfo, smartyMatch.PathInfo);
+ }
+ else
+ {
+ Assert.Null(smartyMatch);
+ }
+ }
+
+ [Theory,
+ InlineData("1.1/2/3", "1.1/2/3.3", ""),
+ InlineData("1/2/3/4", "1.one", "2/3/4"),
+ InlineData("2/3/4", "2.two", "3/4"),
+ InlineData("one/two/3/4/5/6", "one/two/3/4.4", "5/6"),
+ InlineData("one/two/3/4/5/6/foo", "one/two/3/4.4", "5/6/foo"),
+ InlineData("one/two/3/4/5/6/foo.htm", null, null)]
+ public void MultipleExtensionsTest(string url, string match, string pathInfo)
+ {
+ string[] files = new[] { "~/1.one", "~/2.two", "~/1.1/2/3.3", "~/one/two/3/4.4", "~/one/two/3/4/5/6/foo.htm" };
+ string[] extensions = new[] { "aspx", "hao", "one", "two", "3", "4" };
+
+ ConstraintTest(files, extensions, url, match, pathInfo);
+ }
+
+ [Theory,
+ InlineData("1.1/2/3", "1.1/2/3.Mobile.3", ""),
+ InlineData("1/2/3/4", "1.Mobile.one", "2/3/4"),
+ InlineData("2/3/4", "2.Mobile.two", "3/4"),
+ InlineData("one/two/3/4/5/6", "one/two/3/4.Mobile.4", "5/6"),
+ InlineData("one/two/3/4/5/6/foo", "one/two/3/4.Mobile.4", "5/6/foo"),
+ InlineData("one/two/3/4/5/6/foo.Mobile.htm", null, null)]
+ public void MultipleExtensionsMobileTest(string url, string match, string pathInfo)
+ {
+ string[] files = new[]
+ {
+ "~/1.one", "~/2.two", "~/1.1/2/3.3", "~/one/two/3/4.4", "~/one/two/3/4/5/6/foo.htm",
+ "~/1.Mobile.one", "~/2.Mobile.two", "~/1.1/2/3.Mobile.3", "~/one/two/3/4.Mobile.4", "~/one/two/3/4/5/6/foo.Mobile.htm"
+ };
+ string[] extensions = new[] { "aspx", "hao", "one", "two", "3", "4" };
+
+ ConstraintTest(files, extensions, url, match, pathInfo, mobileDevice: true);
+ }
+
+ [Fact]
+ public void FilesWithLeadingUnderscoresAreNeverServed()
+ {
+ string[] files = new[] { "~/hi.evil", "~/_hi.evil", "~/_nest/good.evil", "~/_nest/_hide.evil", "~/_ok.good" };
+ string[] extensions = new[] { "evil" };
+
+ ConstraintTest(files, extensions, "hi", "hi.evil", "");
+ ConstraintTest(files, extensions, "_nest/good/some/extra/path/info", "_nest/good.evil", "some/extra/path/info");
+ Assert.Throws<HttpException>(() => { ConstraintTest(files, extensions, "_hi", null, null); }, "Files with leading underscores (\"_\") cannot be served.");
+ Assert.Throws<HttpException>(() => { ConstraintTest(files, extensions, "_nest/_hide", null, null); }, "Files with leading underscores (\"_\") cannot be served.");
+ }
+
+ [Theory]
+ [InlineData(new object[] { "_foo", "_foo/default.cshtml" })]
+ [InlineData(new object[] { "_bar/_baz", "_bar/_baz/index.cshtml" })]
+ public void DirectoriesWithLeadingUnderscoresAreServed(string requestPath, string expectedPath)
+ {
+ // Arramge
+ var files = new[] { "~/_foo/default.cshtml", "~/_bar/_baz/index.cshtml" };
+ var extensions = new[] { "cshtml" };
+
+ // Act
+ ConstraintTest(files, extensions, requestPath, expectedPath, "");
+ }
+
+ [Fact]
+ public void TransformedUnderscoreAreNotServed()
+ {
+ string[] files = new[] { "~/_ok.Mobile.ext", "~/ok.ext" };
+ string[] extensions = new[] { "ext" };
+
+ ConstraintTest(files, extensions, "ok.ext", "ok.ext", "", mobileDevice: true);
+ ConstraintTest(files, extensions, "ok/some/extra/path/info", "ok.ext", "some/extra/path/info", mobileDevice: true);
+
+ Assert.Throws<HttpException>(() => { ConstraintTest(files, extensions, "_ok.Mobile.ext", null, null, mobileDevice: true); }, "Files with leading underscores (\"_\") cannot be served.");
+ }
+
+ [Fact]
+ public void MobileFilesAreReturnedInthePresenceOfUnderscoreFiles()
+ {
+ string[] files = new[] { "~/_ok.Mobile.ext", "~/ok.ext", "~/ok.mobile.ext" };
+ string[] extensions = new[] { "ext" };
+
+ ConstraintTest(files, extensions, "ok.ext", "ok.Mobile.ext", "", mobileDevice: true);
+ ConstraintTest(files, extensions, "ok/some/extra/path/info", "ok.Mobile.ext", "some/extra/path/info", mobileDevice: true);
+ ConstraintTest(files, extensions, "ok.mobile", "ok.mobile.ext", "", mobileDevice: false);
+ }
+
+ [Fact]
+ public void UnsupportedExtensionExistingFileTest()
+ {
+ ConstraintTest(new[] { "~/hao.aspx", "~/hao/hao.txt" }, new[] { "aspx" }, "hao/hao.txt", null, null);
+ }
+
+ [Fact]
+ public void NullPathValueDoesNotMatchTest()
+ {
+ ConstraintTest(new[] { "~/hao.aspx", "~/hao/hao.txt" }, new[] { "aspx" }, null, null, null);
+ }
+
+ [Fact]
+ public void RightToLeftPrecedenceTest()
+ {
+ ConstraintTest(new[] { "~/one/two/three.aspx", "~/one/two.aspx", "~/one.aspx" }, new[] { "aspx" }, "one/two/three", "one/two/three.aspx", "");
+ }
+
+ [Fact]
+ public void DefaultPrecedenceTests()
+ {
+ string[] files = new[] { "~/one/two/default.aspx", "~/one/default.aspx", "~/default.aspx" };
+ string[] extensions = new[] { "aspx" };
+
+ // Default only tries to look at the full path level
+ ConstraintTest(files, extensions, "one/two/three", null, null);
+ ConstraintTest(files, extensions, "one/two", "one/two/default.aspx", "");
+ ConstraintTest(files, extensions, "one", "one/default.aspx", "");
+ ConstraintTest(files, extensions, "", "default.aspx", "");
+ ConstraintTest(files, extensions, "one/two/three/four/five/six/7/8", null, null);
+ }
+
+ [Fact]
+ public void IndexTests()
+ {
+ string[] files = new[] { "~/one/two/index.aspx", "~/one/index.aspx", "~/index.aspq" };
+ string[] extensions = new[] { "aspx", "aspq" };
+
+ // index only tries to look at the full path level
+ ConstraintTest(files, extensions, "one/two/three", null, null);
+ ConstraintTest(files, extensions, "one/two", "one/two/index.aspx", "");
+ ConstraintTest(files, extensions, "one", "one/index.aspx", "");
+ ConstraintTest(files, extensions, "", "index.aspq", "");
+ ConstraintTest(files, extensions, "one/two/three/four/five/six/7/8", null, null);
+ }
+
+ [Fact]
+ public void DefaultVsIndexNestedTest()
+ {
+ string[] files = new[] { "~/one/two/index.aspx", "~/one/index.aspx", "~/one/default.aspx", "~/index.aspq", "~/default.aspx" };
+ string[] extensions = new[] { "aspx", "aspq" };
+
+ ConstraintTest(files, extensions, "one/two", "one/two/index.aspx", "");
+ ConstraintTest(files, extensions, "one", "one/default.aspx", "");
+ ConstraintTest(files, extensions, "", "default.aspx", "");
+ }
+
+ [Fact]
+ public void DefaultVsIndexSameExtensionTest()
+ {
+ string[] files = new[] { "~/one/two/index.aspx", "~/one/index.aspx", "~/one/default.aspx", "~/index.aspq", "~/default.aspx" };
+ string[] extensions = new[] { "aspx" };
+
+ ConstraintTest(files, extensions, "one", "one/default.aspx", "");
+ }
+
+ [Fact]
+ public void DefaultVsIndexDifferentExtensionTest()
+ {
+ string[] files = new[] { "~/index.aspq", "~/default.aspx" };
+ string[] extensions = new[] { "aspx", "aspq" };
+
+ ConstraintTest(files, extensions, "", "default.aspx", "");
+ }
+
+ [Fact]
+ public void DefaultVsIndexOnlyOneExtensionTest()
+ {
+ string[] files = new[] { "~/index.aspq", "~/default.aspx" };
+ string[] extensions = new[] { "aspq" };
+
+ ConstraintTest(files, extensions, "", "index.aspq", "");
+ }
+
+ [Fact]
+ public void FullMatchNoPathInfoTest()
+ {
+ ConstraintTest(new[] { "~/hao.aspx" }, new[] { "aspx" }, "hao", "hao.aspx", "");
+ }
+
+ [Fact]
+ public void MatchFileWithExtensionTest()
+ {
+ string[] files = new[] { "~/page.aspq" };
+ string[] extensions = new[] { "aspq" };
+
+ ConstraintTest(files, extensions, "page.aspq", "page.aspq", "");
+ }
+
+ [Fact]
+ public void NoMatchFileWithWrongExtensionTest()
+ {
+ string[] files = new[] { "~/page.aspx" };
+ string[] extensions = new[] { "aspq" };
+
+ ConstraintTest(files, extensions, "page.aspx", null, null);
+ }
+
+ [Fact]
+ public void WebPageRouteDoesNotPerformMappingIfRootLevelIsExplicitlyDisabled()
+ {
+ // Arrange
+ var webPageRoute = new WebPageRoute { IsExplicitlyDisabled = true };
+ var context = new Mock<HttpContextBase>();
+ context.Setup(c => c.RemapHandler(It.IsAny<IHttpHandler>())).Throws(new Exception("Smarty route should be disabled."));
+ context.SetupGet(c => c.Request).Throws(new Exception("We do not need to use the request to identify if the app is disabled."));
+
+ // Act
+ webPageRoute.DoPostResolveRequestCache(context.Object);
+
+ // Assert.
+ // If we've come this far, neither of the setups threw.
+ Assert.True(true);
+ }
+
+ [Fact]
+ public void MatchRequestSetsDisplayModeOfFirstMatchPerContext()
+ {
+ // Arrange
+ var objectFactory = new HashyBuildManager(new string[] { "~/page.Mobile.aspx", "~/nonMobile.aspx" });
+ var mockContext = new Mock<HttpContextBase>();
+ mockContext.Setup(context => context.Items).Returns(new Hashtable());
+ mockContext.Setup(c => c.Request.Browser.IsMobileDevice).Returns(true);
+ mockContext.Setup(c => c.Request.Cookies).Returns(new HttpCookieCollection());
+ mockContext.Setup(c => c.Response.Cookies).Returns(new HttpCookieCollection());
+
+ var displayModeProvider = new DisplayModeProvider();
+
+ // Act
+ WebPageMatch mobileMatch = WebPageRoute.MatchRequest("page.aspx", new string[] { "aspx" }, objectFactory, mockContext.Object, displayModeProvider);
+
+ // Assert
+ Assert.NotNull(mobileMatch.MatchedPath);
+ Assert.Equal(DisplayModeProvider.MobileDisplayModeId, DisplayModeProvider.GetDisplayMode(mockContext.Object).DisplayModeId);
+ }
+
+ [Fact]
+ public void MatchRequestDoesNotSetDisplayModeIfNoMatch()
+ {
+ // Arrange
+ var objectFactory = new HashyBuildManager(new string[] { "~/page.Mobile.aspx" });
+ var mockContext = new Mock<HttpContextBase>();
+ mockContext.Setup(context => context.Items).Returns(new Hashtable());
+ mockContext.Setup(c => c.Request.Browser.IsMobileDevice).Returns(true);
+ mockContext.Setup(c => c.Request.Cookies).Returns(new HttpCookieCollection());
+ mockContext.Setup(c => c.Response.Cookies).Returns(new HttpCookieCollection());
+
+ var displayModeProvider = new DisplayModeProvider();
+ var displayMode = new Mock<IDisplayMode>(MockBehavior.Strict);
+ displayMode.Setup(d => d.CanHandleContext(mockContext.Object)).Returns(false);
+ displayModeProvider.Modes.Add(displayMode.Object);
+
+ // Act
+ WebPageMatch smartyMatch = WebPageRoute.MatchRequest("notThere.aspx", new string[] { "aspx" }, objectFactory, mockContext.Object, displayModeProvider);
+
+ // Assert
+ Assert.Null(DisplayModeProvider.GetDisplayMode(mockContext.Object));
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/WebPage/WebPageSurrogateControlBuilderTest.cs b/test/System.Web.WebPages.Test/WebPage/WebPageSurrogateControlBuilderTest.cs
new file mode 100644
index 00000000..8c7e5f5c
--- /dev/null
+++ b/test/System.Web.WebPages.Test/WebPage/WebPageSurrogateControlBuilderTest.cs
@@ -0,0 +1,550 @@
+using System;
+using System.CodeDom;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Web;
+using System.Web.UI;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Microsoft.WebPages.Resources;
+using Microsoft.WebPages.TestUtils;
+
+namespace Microsoft.WebPages.Test {
+ /// <summary>
+ ///This is a test class for WebPageSurrogateControlBuilderTest and is intended
+ ///to contain all WebPageSurrogateControlBuilderTest Unit Tests
+ ///</summary>
+ [TestClass()]
+ public class WebPageSurrogateControlBuilderTest {
+
+
+ private TestContext testContextInstance;
+
+ /// <summary>
+ ///Gets or sets the test context which provides
+ ///information about and functionality for the current test run.
+ ///</summary>
+ public TestContext TestContext {
+ get {
+ return testContextInstance;
+ }
+ set {
+ testContextInstance = value;
+ }
+ }
+
+ #region Additional test attributes
+ //
+ //You can use the following additional attributes as you write your tests:
+ //
+ //Use ClassInitialize to run code before running the first test in the class
+ //[ClassInitialize()]
+ //public static void MyClassInitialize(TestContext testContext)
+ //{
+ //}
+ //
+ //Use ClassCleanup to run code after all tests in a class have run
+ //[ClassCleanup()]
+ //public static void MyClassCleanup()
+ //{
+ //}
+ //
+ //Use TestInitialize to run code before running each test
+ //[TestInitialize()]
+ //public void MyTestInitialize()
+ //{
+ //}
+ //
+ //Use TestCleanup to run code after each test has run
+ //[TestCleanup()]
+ //public void MyTestCleanup()
+ //{
+ //}
+ //
+ #endregion
+
+ /// <summary>
+ ///A test for IsAspx
+ ///</summary>
+ [TestMethod()]
+ public void IsAspxTest() {
+ Dictionary<string, bool> testCases = new Dictionary<string, bool>() {
+ { "test.abc", false },
+ { "test", false },
+ { "test.aspx", true },
+ { "TEST.AspX", true },
+ { "TEST.xyzabc.AspX", true},
+ };
+ foreach (var kvp in testCases) {
+ string virtualPath = kvp.Key;
+ bool expected = kvp.Value;
+ bool actual;
+ actual = WebPageSurrogateControlBuilder.IsAspx(virtualPath);
+ Assert.AreEqual(expected, actual);
+ }
+ }
+
+ /// <summary>
+ ///A test for IsAspq
+ ///</summary>
+ [TestMethod()]
+ public void IsAspqTest() {
+ Dictionary<string, bool> testCases = new Dictionary<string, bool>() {
+ { "test.abc", false },
+ { "test", false },
+ { "test.aspq", true },
+ { "TEST.AspQ", true },
+ { "TEST.xyzabc.AsPq", true},
+ };
+ foreach (var kvp in testCases) {
+ string virtualPath = kvp.Key;
+ bool expected = kvp.Value;
+ bool actual;
+ actual = WebPageSurrogateControlBuilder.IsAspq(virtualPath);
+ Assert.AreEqual(expected, actual);
+ }
+ }
+
+
+ /// <summary>
+ ///A test for IsAscq
+ ///</summary>
+ [TestMethod()]
+ public void IsAscqTest() {
+ Dictionary<string, bool> testCases = new Dictionary<string, bool>() {
+ { "test.abc", false },
+ { "test", false },
+ { "test.ascq", true },
+ { "TEST.AscQ", true },
+ { "TEST.xyzabc.AsCQ", true},
+ };
+ foreach (var kvp in testCases) {
+ string virtualPath = kvp.Key;
+ bool expected = kvp.Value;
+ bool actual;
+ actual = WebPageSurrogateControlBuilder.IsAscq(virtualPath);
+ Assert.AreEqual(expected, actual);
+ }
+ }
+
+ /// <summary>
+ ///A test for AddImports
+ ///</summary>
+ [TestMethod]
+ public void AddImportsTest() {
+ CodeCompileUnit ccu = new CodeCompileUnit();
+ ccu.Namespaces.Add(new CodeNamespace());
+ WebPageSurrogateControlBuilder.AddImports(ccu);
+ VerifyDefaultNameSpaces(ccu);
+
+ // Temporarily set the static field to something else
+ var originalNamespaces = new List<string>(WebPageSurrogateControlBuilder.Namespaces);
+ WebPageSurrogateControlBuilder.Namespaces.Clear();
+ WebPageSurrogateControlBuilder.Namespaces.Add("System.ABC");
+ ccu = new CodeCompileUnit();
+ ccu.Namespaces.Add(new CodeNamespace());
+ WebPageSurrogateControlBuilder.AddImports(ccu);
+ Assert.AreEqual(1, ccu.Namespaces[0].Imports.Count);
+ Assert.AreEqual("System.ABC", ccu.Namespaces[0].Imports[0].Namespace);
+
+ // Restore the static field
+ WebPageSurrogateControlBuilder.Namespaces.Clear();
+ foreach (var ns in originalNamespaces) {
+ WebPageSurrogateControlBuilder.Namespaces.Add(ns);
+ }
+ }
+
+ [TestMethod]
+ public void ProcessGeneratedCodeTest() {
+ VerifyProcessGeneratedCode(new WebPageSurrogateControlBuilder());
+ VerifyProcessGeneratedCode(new WebUserControlSurrogateControlBuilder());
+ }
+
+ public void VerifyProcessGeneratedCode(ControlBuilder builder) {
+ CodeCompileUnit ccu = new CodeCompileUnit();
+ ccu.Namespaces.Add(new CodeNamespace());
+ builder.ProcessGeneratedCode(ccu, null, null, null, null);
+ VerifyDefaultNameSpaces(ccu);
+ }
+
+ public void VerifyDefaultNameSpaces(CodeCompileUnit ccu) {
+ Assert.AreEqual(13, ccu.Namespaces[0].Imports.Count);
+ Assert.AreEqual(WebPageSurrogateControlBuilder.Namespaces.Count, ccu.Namespaces[0].Imports.Count);
+ }
+
+ /// <summary>
+ ///A test for GetLanguageAttributeFromText
+ ///</summary>
+ [TestMethod()]
+ public void GetLanguageAttributeFromTextTest() {
+ Assert.AreEqual("abcd", WebPageSurrogateControlBuilder.GetLanguageAttributeFromText("<%@ Page Language=\" abcd\" %> csharp c# cs "));
+ Assert.AreEqual("xxx", WebPageSurrogateControlBuilder.GetLanguageAttributeFromText("<%@ Page lanGUAGE='xxx' %> csharp c# cs "));
+ Assert.AreEqual("", WebPageSurrogateControlBuilder.GetLanguageAttributeFromText("<%@ Page languagE='' %> csharp c# cs "));
+ Assert.AreEqual(null, WebPageSurrogateControlBuilder.GetLanguageAttributeFromText("<%@ Page hello world %> csharp c# cs "));
+ }
+
+ /// <summary>
+ ///A test for GetLanguageFromText
+ ///</summary>
+ [TestMethod()]
+ public void GetLanguageFromTextPositiveTest() {
+ Assert.AreEqual(Language.VisualBasic, WebPageSurrogateControlBuilder.GetLanguageFromText("<%@ Page %> csharp c# cs "));
+ Assert.AreEqual(Language.VisualBasic, WebPageSurrogateControlBuilder.GetLanguageFromText("<%@ Page Language=' vB ' %> csharp c# cs "));
+ Assert.AreEqual(Language.VisualBasic, WebPageSurrogateControlBuilder.GetLanguageFromText("<%@ Page Language=\" vb \" %> csharp c# cs "));
+ Assert.AreEqual(Language.VisualBasic, WebPageSurrogateControlBuilder.GetLanguageFromText("<%@ Page Language=' Vb ' %> csharp c# cs "));
+ Assert.AreEqual(Language.VisualBasic, WebPageSurrogateControlBuilder.GetLanguageFromText("<%@ Page Language=' vB ' %> csharp c# cs "));
+ Assert.AreEqual(Language.VisualBasic, WebPageSurrogateControlBuilder.GetLanguageFromText("<%@ Page Language=' vBs ' %> csharp c# cs "));
+ Assert.AreEqual(Language.VisualBasic, WebPageSurrogateControlBuilder.GetLanguageFromText("<%@ Page Language=' vBscript ' %> csharp c# cs "));
+ Assert.AreEqual(Language.VisualBasic, WebPageSurrogateControlBuilder.GetLanguageFromText("<%@ Page Language=' vBscript ' %> csharp c# cs "));
+ Assert.AreEqual(Language.VisualBasic, WebPageSurrogateControlBuilder.GetLanguageFromText("<%@ Page Language=' vIsuaLBasic ' %> csharp c# cs "));
+
+ Assert.AreEqual(Language.CSharp, WebPageSurrogateControlBuilder.GetLanguageFromText("<%@ Page Language=' c# ' %> vb vbs visualbasic vbscript "));
+ Assert.AreEqual(Language.CSharp, WebPageSurrogateControlBuilder.GetLanguageFromText("<%@ Page Language=' cS ' %> vb vbs visualbasic vbscript "));
+ Assert.AreEqual(Language.CSharp, WebPageSurrogateControlBuilder.GetLanguageFromText("<%@ Page Language=' cShaRP ' %> vb vbs visualbasic vbscript "));
+ }
+
+ /// <summary>
+ ///A test for GetLanguageFromText
+ ///</summary>
+ [TestMethod()]
+ public void GetLanguageFromTextNegativeTest() {
+ ExceptionAssert.Throws<HttpException>(() =>
+ WebPageSurrogateControlBuilder.GetLanguageFromText("<%@ Page Language=' zxc' %> vb vbs visualbasic vbscript "),
+ String.Format(WebPageResources.WebPage_InvalidLanguage, "zxc"));
+ }
+
+ /// <summary>
+ ///A test for FixUpWriteSnippetStatement
+ ///</summary>
+ [TestMethod()]
+ public void FixUpWriteSnippetStatementTest() {
+ var code = " abc zyx";
+ var stmt = new CodeSnippetStatement(code);
+ WebPageSurrogateControlBuilder.FixUpWriteSnippetStatement(stmt);
+ Assert.AreEqual(code, stmt.Value);
+
+ code = " this.Write(\"hello\"); ";
+ stmt = new CodeSnippetStatement(code);
+ WebPageSurrogateControlBuilder.FixUpWriteSnippetStatement(stmt);
+ Assert.AreEqual(code, stmt.Value);
+
+ // @__w.Write case
+ code = " @__w.Write(\"hello\"); ";
+ stmt = new CodeSnippetStatement(code);
+ WebPageSurrogateControlBuilder.FixUpWriteSnippetStatement(stmt);
+ Assert.AreEqual(" WriteLiteral(\"hello\"); ", stmt.Value);
+
+ // __w.Write case
+ code = " __w.Write(\"hello\"); ";
+ stmt = new CodeSnippetStatement(code);
+ WebPageSurrogateControlBuilder.FixUpWriteSnippetStatement(stmt);
+ Assert.AreEqual(" WriteLiteral(\"hello\"); ", stmt.Value);
+ }
+
+ /// <summary>
+ ///A test for FixUpWriteCodeExpressionStatement
+ ///</summary>
+ [TestMethod()]
+ public void FixUpWriteCodeExpressionStatementTest() {
+ // Null test
+ WebPageSurrogateControlBuilder.FixUpWriteCodeExpressionStatement(null);
+
+ // Should fix up the statement
+ var invoke = new CodeMethodInvokeExpression(new CodeArgumentReferenceExpression("__w"), "Write");
+ CodeExpressionStatement exprStmt = new CodeExpressionStatement(invoke);
+ WebPageSurrogateControlBuilder.FixUpWriteCodeExpressionStatement(exprStmt);
+ Assert.AreEqual(invoke.Method.MethodName, "WriteLiteral");
+ Assert.AreEqual(invoke.Method.TargetObject.GetType(), typeof(CodeThisReferenceExpression));
+
+ // Should NOT fix up the statement
+ invoke = new CodeMethodInvokeExpression(new CodeArgumentReferenceExpression("xyz"), "Write");
+ exprStmt = new CodeExpressionStatement(invoke);
+ WebPageSurrogateControlBuilder.FixUpWriteCodeExpressionStatement(exprStmt);
+ Assert.AreEqual(invoke.Method.MethodName, "Write");
+ Assert.IsInstanceOfType(invoke.Method.TargetObject, typeof(CodeArgumentReferenceExpression));
+ }
+
+ /// <summary>
+ ///A test for FixUpWriteStatement
+ ///</summary>
+ [TestMethod()]
+ public void FixUpWriteStatementTest() {
+
+ var invoke = new CodeMethodInvokeExpression(new CodeArgumentReferenceExpression("__w"), "Write");
+ CodeExpressionStatement exprStmt = new CodeExpressionStatement(invoke);
+ WebPageSurrogateControlBuilder.FixUpWriteStatement(exprStmt);
+ Assert.AreEqual(invoke.Method.MethodName, "WriteLiteral");
+ Assert.IsInstanceOfType(invoke.Method.TargetObject, typeof(CodeThisReferenceExpression));
+
+ // @__w.Write case
+ var code = " @__w.Write(\"hello\"); ";
+ var stmt = new CodeSnippetStatement(code);
+ WebPageSurrogateControlBuilder.FixUpWriteStatement(stmt);
+ Assert.AreEqual(" WriteLiteral(\"hello\"); ", stmt.Value);
+
+ // __w.Write case
+ code = " __w.Write(\"hello\"); ";
+ stmt = new CodeSnippetStatement(code);
+ WebPageSurrogateControlBuilder.FixUpWriteStatement(stmt);
+ Assert.AreEqual(" WriteLiteral(\"hello\"); ", stmt.Value);
+ }
+
+ [TestMethod]
+ public void FixUpClassNull() {
+ WebPageSurrogateControlBuilder.FixUpClass(null, null);
+ }
+
+ [TestMethod]
+ public void OnCodeGenerationCompleteTest() {
+ Utils.RunInSeparateAppDomain(() => {
+ var vpath = "/WebSite1/index.aspq";
+ var contents = "<%@ Page Language=C# %>";
+ Utils.SetupVirtualPathInAppDomain(vpath, contents);
+ new WebPageSurrogateControlBuilderTest().FixUpClassTest(type => {
+ var ccu = new CodeCompileUnit();
+ ccu.Namespaces.Add(new CodeNamespace());
+
+ var builder = new MockControlBuilder() { VPath = vpath };
+ builder.ProcessGeneratedCode(ccu, null, type, null, null);
+ builder.CallOnCodeGenerationComplete();
+ });
+ });
+ }
+
+ [TestMethod]
+ public void OnCodeGenerationCompleteControlBuilderTest() {
+ Utils.RunInSeparateAppDomain(() => {
+ var vpath = "/WebSite1/index.ascq";
+ var contents = "<%@ Page Language=C# %>";
+ Utils.SetupVirtualPathInAppDomain(vpath, contents);
+ new WebPageSurrogateControlBuilderTest().FixUpClassTest(type => {
+ var ccu = new CodeCompileUnit();
+ ccu.Namespaces.Add(new CodeNamespace());
+
+ var builder = new MockUserControlBuilder() { VPath = vpath };
+ builder.ProcessGeneratedCode(ccu, null, type, null, null);
+ builder.CallOnCodeGenerationComplete();
+ });
+ });
+ }
+
+ public class MockControlBuilder : WebPageSurrogateControlBuilder {
+ public string VPath { get; set; }
+ public void CallOnCodeGenerationComplete() {
+ base.OnCodeGenerationComplete();
+ }
+
+ internal override string GetPageVirtualPath() {
+ return VPath;
+ }
+ }
+
+ public class MockUserControlBuilder : WebUserControlSurrogateControlBuilder {
+ public string VPath { get; set; }
+ public void CallOnCodeGenerationComplete() {
+ base.OnCodeGenerationComplete();
+ }
+
+ internal override string GetPageVirtualPath() {
+ return VPath;
+ }
+ }
+
+ [TestMethod]
+ public void FixUpClassVirtualPathTest() {
+ Utils.RunInSeparateAppDomain(() => {
+ var vpath = "/WebSite1/index.aspq";
+ var contents = "<%@ Page Language=C# %>";
+ Utils.SetupVirtualPathInAppDomain(vpath, contents);
+
+ new WebPageSurrogateControlBuilderTest().FixUpClassTest(type =>
+ WebPageSurrogateControlBuilder.FixUpClass(type, vpath));
+ });
+ }
+
+ /// <summary>
+ ///A test for FixUpClass
+ ///</summary>
+ [TestMethod()]
+ public void FixUpClassLanguageTest() {
+ FixUpClassTest(type =>
+ WebPageSurrogateControlBuilder.FixUpClass(type, Language.CSharp));
+ }
+
+ [TestMethod]
+ public void FixUpClassDefaultApplicationBaseTypeTest() {
+ Utils.RunInSeparateAppDomain(() => {
+ var baseTypeField = typeof(PageParser).GetField("s_defaultApplicationBaseType", BindingFlags.Static | BindingFlags.NonPublic);
+ baseTypeField.SetValue(null, typeof(WebPageHttpApplication));
+ var pageType = new WebPageSurrogateControlBuilderTest().FixUpClassTest(type =>
+ WebPageSurrogateControlBuilder.FixUpClass(type, Language.CSharp));
+ var properties = pageType.Members.OfType<CodeMemberProperty>().ToList();
+ var prop = properties[0];
+ Assert.AreEqual(typeof(WebPageHttpApplication).FullName, prop.Type.BaseType);
+ });
+ }
+
+ public CodeTypeDeclaration FixUpClassTest(Action<CodeTypeDeclaration> fixUpClassMethod) {
+ var type = new CodeTypeDeclaration();
+ // Add some dummy base types which should get removed
+ type.BaseTypes.Add("basetype1");
+ type.BaseTypes.Add("basetype2");
+ type.BaseTypes.Add("basetype3");
+
+ // Add a property which should get retained
+ var appInstance = new CodeMemberProperty() { Name = "ApplicationInstance" };
+ var returnStatement = new CodeMethodReturnStatement(new CodeCastExpression(typeof(HttpApplication), null));
+ appInstance.GetStatements.Add(returnStatement);
+ type.Members.Add(appInstance);
+
+ // Add a render method which should get retained but modified
+ var renderMethod = new CodeMemberMethod();
+ renderMethod.Name = "__Render__control1";
+
+ // Add a code snippet statement that should not be modified
+ var stmt1 = new CodeSnippetStatement("MyCode.DoSomething");
+ renderMethod.Statements.Add(stmt1);
+
+ // Add a code snippet statement that should be modified
+ var code2 = " @__w.Write(\"hello\"); ";
+ var stmt2 = new CodeSnippetStatement(code2);
+ renderMethod.Statements.Add(stmt2);
+
+ // Add a method invoke statement that should be modified
+ var invoke3 = new CodeMethodInvokeExpression(new CodeArgumentReferenceExpression("__w"), "Write");
+ CodeExpressionStatement stmt3 = new CodeExpressionStatement(invoke3);
+ WebPageSurrogateControlBuilder.FixUpWriteStatement(stmt3);
+ renderMethod.Statements.Add(stmt3);
+
+ type.Members.Add(renderMethod);
+
+ // Snippets should get retained
+ var snippet1 = "public void Test1() { }";
+ var snippet2 = "public void Test2() { }";
+ type.Members.Add(new CodeSnippetTypeMember(snippet1));
+ type.Members.Add(new CodeSnippetTypeMember(snippet2));
+
+ // Add dummy members which should get removed
+ type.Members.Add(new CodeMemberProperty() { Name = "DummyProperty1" });
+ type.Members.Add(new CodeMemberProperty() { Name = "DummyProperty2" });
+ type.Members.Add(new CodeMemberMethod() { Name = "DummyMethod1" });
+ type.Members.Add(new CodeMemberMethod() { Name = "DummyMethod2" });
+
+ // Run the method we are testing
+ fixUpClassMethod(type);
+
+ // Basic verification
+ Assert.AreEqual(1, type.BaseTypes.Count);
+ Assert.AreEqual(4, type.Members.Count);
+
+ // Verify properties
+ var properties = type.Members.OfType<CodeMemberProperty>().ToList();
+ Assert.AreEqual(1, properties.Count);
+ Assert.AreEqual("ApplicationInstance", properties[0].Name);
+
+ // Verify snippets
+ var snippets = type.Members.OfType<CodeSnippetTypeMember>().ToList();
+ Assert.AreEqual(2, snippets.Count);
+ Assert.IsNotNull(snippets.Find(s => s.Text == snippet1));
+ Assert.IsNotNull(snippets.Find(s => s.Text == snippet2));
+
+ // Verify methods
+ var methods = type.Members.OfType<CodeMemberMethod>().ToList();
+ Assert.AreEqual(1, methods.Count);
+ Assert.AreEqual("Execute", methods[0].Name);
+
+ // Verify statements in the method
+ var statements = methods[0].Statements;
+ Assert.AreEqual(4, statements.Count); // The fourth statement is a snippet generated for use as helper.
+
+ // First statement should be unchanged
+ Assert.AreEqual(stmt1, statements[0]);
+
+ // Second statement should be fixed to use WriteLiteral
+ Assert.IsInstanceOfType(statements[1], typeof(CodeSnippetStatement));
+ Assert.AreEqual(" WriteLiteral(\"hello\"); ", ((CodeSnippetStatement)statements[1]).Value);
+
+ // Third statement should be fixed to use WriteLiteral
+ Assert.IsInstanceOfType(statements[2], typeof(CodeExpressionStatement));
+ var invokeExpr = ((CodeExpressionStatement)statements[2]).Expression;
+ Assert.IsInstanceOfType(invokeExpr, typeof(CodeMethodInvokeExpression));
+ var invoke = invokeExpr as CodeMethodInvokeExpression;
+ Assert.AreEqual(invoke.Method.MethodName, "WriteLiteral");
+ Assert.IsInstanceOfType(invoke.Method.TargetObject, typeof(CodeThisReferenceExpression));
+
+ // Fourth statement should be a generated code snippet
+ Assert.IsInstanceOfType(statements[3], typeof(CodeSnippetStatement));
+ return type;
+ }
+
+ [TestMethod]
+ public void FixUpAspxClassUserControlTest() {
+ CodeTypeDeclaration type;
+ CodeCastExpression cast;
+ GetAspxClass(out type, out cast, typeof(WebUserControlSurrogate));
+ WebPageSurrogateControlBuilder.FixUpAspxClass(type, "/test/test.ascx");
+ Assert.AreEqual(typeof(UserControl).FullName, cast.TargetType.BaseType);
+ }
+
+ [TestMethod]
+ public void FixUpClassUserControlTest() {
+ CodeTypeDeclaration type;
+ CodeCastExpression cast;
+ GetAspxClass(out type, out cast, typeof(WebUserControlSurrogate));
+ WebPageSurrogateControlBuilder.FixUpClass(type, "/test/test.ascx");
+ Assert.AreEqual(typeof(UserControl).FullName, cast.TargetType.BaseType);
+ }
+
+ private static void GetAspxClass(out CodeTypeDeclaration type, out CodeCastExpression cast, Type surrogateType) {
+ type = new CodeTypeDeclaration();
+ type.BaseTypes.Add(new CodeTypeReference(surrogateType));
+ var ctor = new CodeConstructor();
+ cast = new CodeCastExpression();
+ cast.TargetType = new CodeTypeReference(surrogateType);
+ var prop = new CodePropertyReferenceExpression();
+ prop.TargetObject = cast;
+ var assign = new CodeAssignStatement();
+ assign.Left = prop;
+ ctor.Statements.Add(assign);
+ type.Members.Add(ctor);
+ }
+
+ [TestMethod]
+ public void ProcessScriptBlocksCSTest() {
+ var pageType = new CodeTypeDeclaration("MyControl");
+ var snippet = new CodeSnippetTypeMember("public int foo;");
+ pageType.Members.Add(snippet);
+ var renderMethod = new CodeMemberMethod();
+ WebPageSurrogateControlBuilder.ProcessScriptBlocks(pageType, renderMethod, Language.CSharp);
+ var snippets = renderMethod.Statements.OfType<CodeSnippetStatement>().ToList();
+ Assert.AreEqual(1, snippets.Count);
+ var snip = snippets[0];
+ Assert.IsTrue(snip.Value.Contains("public static class MyControlExtensions"));
+ Assert.IsTrue(snip.Value.Contains("public static HelperResult MyControl(this System.Web.Mvc.HtmlHelper htmlHelper, int foo = default(int))"));
+ Assert.IsTrue(snip.Value.Contains("uc.foo = foo;"));
+ }
+
+ [TestMethod]
+ public void ProcessScriptBlocksVBTest() {
+ var pageType = new CodeTypeDeclaration("MyControl");
+ var snippet = new CodeSnippetTypeMember("public foo as int");
+ pageType.Members.Add(snippet);
+ var renderMethod = new CodeMemberMethod();
+ WebPageSurrogateControlBuilder.ProcessScriptBlocks(pageType, renderMethod, Language.VisualBasic);
+ var snippets = renderMethod.Statements.OfType<CodeSnippetStatement>().ToList();
+ Assert.AreEqual(1, snippets.Count);
+ var snip = snippets[0];
+ Assert.IsTrue(snip.Value.Contains("Public Module MyControlExtensions"));
+ Assert.IsTrue(snip.Value.Contains("Public Function MyControl(htmlHelper As System.Web.Mvc.HtmlHelper, optional foo as int = Nothing) As HelperResult"));
+ Assert.IsTrue(snip.Value.Contains("uc.foo = foo"));
+ }
+
+ [TestMethod]
+ public void HasAspCodeTest() {
+ Assert.IsTrue(new WebPageSurrogateControlBuilder().HasAspCode);
+ Assert.IsTrue(new WebUserControlSurrogateControlBuilder().HasAspCode);
+ }
+ }
+
+}
+
diff --git a/test/System.Web.WebPages.Test/WebPage/WebPageTest.cs b/test/System.Web.WebPages.Test/WebPage/WebPageTest.cs
new file mode 100644
index 00000000..1ae87304
--- /dev/null
+++ b/test/System.Web.WebPages.Test/WebPage/WebPageTest.cs
@@ -0,0 +1,309 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Linq;
+using System.Web.Caching;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace System.Web.WebPages.Test
+{
+ public class WebPageTest
+ {
+ private const string XmlHttpRequestKey = "X-Requested-With";
+ private const string XmlHttpRequestValue = "XMLHttpRequest";
+
+ [Fact]
+ public void CreatePageFromVirtualPathAssignsVirtualPathFactory()
+ {
+ // Arrange
+ var path = "~/index.cshtml";
+ var page = Utils.CreatePage(null, path);
+ var factory = new HashVirtualPathFactory(page);
+
+ // Act
+ var result = WebPage.CreateInstanceFromVirtualPath(path, factory);
+
+ // Assert
+ Assert.Equal(page, result);
+ Assert.Equal(page.VirtualPathFactory, factory);
+ Assert.Equal(page.VirtualPath, path);
+ }
+
+ [Fact]
+ public void NormalizeLayoutPagePathTest()
+ {
+ var layoutPage = "Layout.cshtml";
+ var layoutPath1 = "~/MyApp/Layout.cshtml";
+ var page = new Mock<WebPage>() { CallBase = true }.Object;
+ page.VirtualPath = "~/MyApp/index.cshtml";
+
+ var mockBuildManager = new Mock<IVirtualPathFactory>();
+ mockBuildManager.Setup(c => c.Exists(It.IsAny<string>())).Returns<string>(p => p.Equals(layoutPath1, StringComparison.OrdinalIgnoreCase));
+ page.VirtualPathFactory = mockBuildManager.Object;
+
+ Assert.Equal(layoutPath1, page.NormalizeLayoutPagePath(layoutPage));
+
+ mockBuildManager.Setup(c => c.Exists(It.IsAny<string>())).Returns<string>(_ => false);
+
+ Assert.Throws<HttpException>(() => page.NormalizeLayoutPagePath(layoutPage),
+ @"The layout page ""Layout.cshtml"" could not be found at the following path: ""~/MyApp/Layout.cshtml"".");
+ }
+
+ [Fact]
+ public void UrlDataBasicTests()
+ {
+ Mock<HttpContextBase> mockContext = new Mock<HttpContextBase>();
+ mockContext.Setup(context => context.Items).Returns(new Hashtable());
+ mockContext.Object.Items[typeof(WebPageMatch)] = new WebPageMatch("~/a.cshtml", "one/2/3.0/4.0005");
+ WebPage page = new Mock<WebPage>() { CallBase = true }.Object;
+ page.Context = mockContext.Object;
+
+ Assert.Equal("one", page.UrlData[0]);
+ Assert.Equal(2, page.UrlData[1].AsInt());
+ Assert.Equal(3.0f, page.UrlData[2].AsFloat());
+ Assert.Equal(4.0005m, page.UrlData[3].AsDecimal());
+ }
+
+ [Fact]
+ public void UrlDataEmptyTests()
+ {
+ Mock<HttpContextBase> mockContext = new Mock<HttpContextBase>();
+ mockContext.Setup(context => context.Items).Returns(new Hashtable());
+ mockContext.Object.Items[typeof(WebPageMatch)] = new WebPageMatch("~/a.cshtml", "one///two/");
+ WebPage page = new Mock<WebPage>() { CallBase = true }.Object;
+ page.Context = mockContext.Object;
+
+ Assert.Equal("one", page.UrlData[0]);
+ Assert.True(page.UrlData[1].IsEmpty());
+ Assert.True(page.UrlData[2].IsEmpty());
+ Assert.Equal("two", page.UrlData[3]);
+ Assert.True(page.UrlData[4].IsEmpty());
+ }
+
+ [Fact]
+ public void UrlDataReadOnlyTest()
+ {
+ Mock<HttpContextBase> mockContext = new Mock<HttpContextBase>();
+ mockContext.Setup(context => context.Items).Returns(new Hashtable());
+ mockContext.Object.Items[typeof(WebPageMatch)] = new WebPageMatch("~/a.cshtml", "one/2/3.0/4.0005");
+ WebPage page = new Mock<WebPage>() { CallBase = true }.Object;
+ page.Context = mockContext.Object;
+
+ Assert.Throws<NotSupportedException>(() => { page.UrlData.Add("bogus"); }, "The UrlData collection is read-only.");
+ Assert.Throws<NotSupportedException>(() => { page.UrlData.Insert(0, "bogus"); }, "The UrlData collection is read-only.");
+ Assert.Throws<NotSupportedException>(() => { page.UrlData.Remove("one"); }, "The UrlData collection is read-only.");
+ }
+
+ [Fact]
+ public void UrlDataOutOfBoundsTest()
+ {
+ Mock<HttpContextBase> mockContext = new Mock<HttpContextBase>();
+ mockContext.Setup(context => context.Items).Returns(new Hashtable());
+ mockContext.Object.Items[typeof(WebPageMatch)] = new WebPageMatch("~/a.cshtml", "");
+ WebPage page = new Mock<WebPage>() { CallBase = true }.Object;
+ page.Context = mockContext.Object;
+
+ Assert.Equal(String.Empty, page.UrlData[0]);
+ Assert.Equal(String.Empty, page.UrlData[1]);
+ }
+
+ [Fact]
+ public void NullModelTest()
+ {
+ var page = CreateMockPageWithPostContext().Object;
+ page.PageContext.Model = null;
+ Assert.Null(page.Model);
+ }
+
+ internal class ModelTestClass
+ {
+ public string Prop1 { get; set; }
+
+ public string GetProp1()
+ {
+ return Prop1;
+ }
+
+ public override string ToString()
+ {
+ return Prop1;
+ }
+ }
+
+ [Fact]
+ public void ModelTest()
+ {
+ var v = "value1";
+ var page = CreateMockPageWithPostContext().Object;
+ var model = new ModelTestClass() { Prop1 = v };
+ page.PageContext.Model = model;
+ Assert.NotNull(page.Model);
+ Assert.Equal(v, page.Model.Prop1);
+ Assert.Equal(v, page.Model.GetProp1());
+ Assert.Equal(v, page.Model.ToString());
+ Assert.Equal(model, (ModelTestClass)page.Model);
+ // No such property
+ Assert.Null(page.Model.Prop2);
+ // No such method
+ Assert.Throws<MissingMethodException>(() => page.Model.DoSomething());
+ }
+
+ [Fact]
+ public void AnonymousObjectModelTest()
+ {
+ var v = "value1";
+ var page = CreateMockPageWithPostContext().Object;
+ var model = new { Prop1 = v };
+ page.PageContext.Model = model;
+ Assert.NotNull(page.Model);
+ Assert.Equal(v, page.Model.Prop1);
+ // No such property
+ Assert.Null(page.Model.Prop2);
+ // No such method
+ Assert.Throws<MissingMethodException>(() => page.Model.DoSomething());
+ }
+
+ [Fact]
+ public void SessionPropertyTest()
+ {
+ var page = CreateMockPageWithPostContext().Object;
+ Assert.Equal(0, page.Session.Count);
+ }
+
+ [Fact]
+ public void AppStatePropertyTest()
+ {
+ var page = CreateMockPageWithPostContext().Object;
+ Assert.Equal(0, page.AppState.Count);
+ }
+
+ [Fact]
+ public void ExecutePageHierarchyTest()
+ {
+ var page = new Mock<WebPage>();
+ page.Object.TopLevelPage = true;
+
+ var executors = new List<IWebPageRequestExecutor>();
+
+ // First executor returns false
+ var executor1 = new Mock<IWebPageRequestExecutor>();
+ executor1.Setup(exec => exec.Execute(It.IsAny<WebPage>())).Returns(false);
+ executors.Add(executor1.Object);
+
+ // Second executor returns true
+ var executor2 = new Mock<IWebPageRequestExecutor>();
+ executor2.Setup(exec => exec.Execute(It.IsAny<WebPage>())).Returns(true);
+ executors.Add(executor2.Object);
+
+ // Third executor should never get called, since we stop after the first true
+ var executor3 = new Mock<IWebPageRequestExecutor>();
+ executor3.Setup(exec => exec.Execute(It.IsAny<WebPage>())).Returns(false);
+ executors.Add(executor3.Object);
+
+ page.Object.ExecutePageHierarchy(executors);
+
+ // Make sure the first two got called but not the third
+ executor1.Verify(exec => exec.Execute(It.IsAny<WebPage>()));
+ executor2.Verify(exec => exec.Execute(It.IsAny<WebPage>()));
+ executor3.Verify(exec => exec.Execute(It.IsAny<WebPage>()), Times.Never());
+ }
+
+ [Fact]
+ public void IsPostReturnsTrueWhenMethodIsPost()
+ {
+ // Arrange
+ var page = CreateMockPageWithPostContext();
+
+ // Act and Assert
+ Assert.True(page.Object.IsPost);
+ }
+
+ [Fact]
+ public void IsPostReturnsFalseWhenMethodIsNotPost()
+ {
+ // Arrange
+ var methods = new[] { "GET", "DELETE", "PUT", "RANDOM" };
+
+ // Act and Assert
+ Assert.True(methods.All(method => !CreateMockPageWithContext(method).Object.IsPost));
+ }
+
+ [Fact]
+ public void IsAjaxReturnsTrueWhenRequestContainsAjaxHeader()
+ {
+ // Arrange
+ var headers = new NameValueCollection();
+ headers.Add("X-Requested-With", "XMLHttpRequest");
+ var context = CreateContext("GET", new NameValueCollection(), headers);
+ var page = CreatePage(context);
+
+ // Act and Assert
+ Assert.True(page.Object.IsAjax);
+ }
+
+ [Fact]
+ public void IsAjaxReturnsTrueWhenRequestBodyContainsAjaxHeader()
+ {
+ // Arrange
+ var headers = new NameValueCollection();
+ headers.Add("X-Requested-With", "XMLHttpRequest");
+ var context = CreateContext("POST", headers, headers);
+ var page = CreatePage(context);
+
+ // Act and Assert
+ Assert.True(page.Object.IsAjax);
+ }
+
+ [Fact]
+ public void IsAjaxReturnsFalseWhenRequestDoesNotContainAjaxHeaders()
+ {
+ // Arrange
+ var page = CreateMockPageWithPostContext();
+
+ // Act and Assert
+ Assert.True(!page.Object.IsAjax);
+ }
+
+ private static Mock<WebPage> CreatePage(Mock<HttpContextBase> context)
+ {
+ var page = new Mock<WebPage>() { CallBase = true };
+ var pageContext = new WebPageContext();
+ page.Object.Context = context.Object;
+ page.Object.PageContext = pageContext;
+ return page;
+ }
+
+ private static Mock<WebPage> CreateMockPageWithPostContext()
+ {
+ return CreateMockPageWithContext("POST");
+ }
+
+ private static Mock<WebPage> CreateMockPageWithContext(string httpMethod)
+ {
+ var context = CreateContext(httpMethod, new NameValueCollection());
+ var page = CreatePage(context);
+ return page;
+ }
+
+ private static Mock<HttpContextBase> CreateContext(string httpMethod, NameValueCollection queryString, NameValueCollection httpHeaders = null)
+ {
+ var request = new Mock<HttpRequestBase>();
+ request.Setup(r => r.HttpMethod).Returns(httpMethod);
+ request.Setup(r => r.QueryString).Returns(queryString);
+ request.Setup(r => r.Form).Returns(new NameValueCollection());
+ request.Setup(r => r.Files).Returns(new Mock<HttpFileCollectionBase>().Object);
+ request.Setup(c => c.Headers).Returns(httpHeaders);
+ var context = new Mock<HttpContextBase>();
+ context.Setup(c => c.Response).Returns(new Mock<HttpResponseBase>().Object);
+ context.Setup(c => c.Request).Returns(request.Object);
+ context.Setup(c => c.Items).Returns(new Hashtable());
+ context.Setup(c => c.Session).Returns(new Mock<HttpSessionStateBase>().Object);
+ context.Setup(c => c.Application).Returns(new Mock<HttpApplicationStateBase>().Object);
+ context.Setup(c => c.Cache).Returns(new Cache());
+ context.Setup(c => c.Server).Returns(new Mock<HttpServerUtilityBase>().Object);
+ return context;
+ }
+ }
+}
diff --git a/test/System.Web.WebPages.Test/packages.config b/test/System.Web.WebPages.Test/packages.config
new file mode 100644
index 00000000..d5aa6401
--- /dev/null
+++ b/test/System.Web.WebPages.Test/packages.config
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="Moq" version="4.0.10827" />
+ <package id="xunit" version="1.9.0.1566" />
+ <package id="xunit.extensions" version="1.9.0.1566" />
+</packages> \ No newline at end of file
diff --git a/test/WebMatrix.Data.Test/App.config b/test/WebMatrix.Data.Test/App.config
new file mode 100644
index 00000000..31c34cb6
--- /dev/null
+++ b/test/WebMatrix.Data.Test/App.config
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<configuration>
+ <system.data>
+ <DbProviderFactories>
+ <remove invariant="System.Data.SqlServerCe.4.0"></remove>
+ <add name="Microsoft SQL Server Compact Data Provider" invariant="System.Data.SqlServerCe.4.0" description=".NET Framework Data Provider for Microsoft SQL Server Compact" type="System.Data.SqlServerCe.SqlCeProviderFactory, System.Data.SqlServerCe, Version=4.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91"/>
+ </DbProviderFactories>
+ </system.data>
+</configuration> \ No newline at end of file
diff --git a/test/WebMatrix.Data.Test/ConfigurationManagerWrapperTest.cs b/test/WebMatrix.Data.Test/ConfigurationManagerWrapperTest.cs
new file mode 100644
index 00000000..dbf05a61
--- /dev/null
+++ b/test/WebMatrix.Data.Test/ConfigurationManagerWrapperTest.cs
@@ -0,0 +1,90 @@
+using System;
+using System.Collections.Generic;
+using Moq;
+using WebMatrix.Data.Test.Mocks;
+using Xunit;
+
+namespace WebMatrix.Data.Test
+{
+ public class ConfigurationManagerWrapperTest
+ {
+ [Fact]
+ public void GetConnectionGetsConnectionFromConfig()
+ {
+ // Arrange
+ var configManager = new ConfigurationManagerWrapper(new Dictionary<string, IDbFileHandler>(), "DataDirectory");
+ Func<string, bool> fileExists = path => false;
+ Func<string, IConnectionConfiguration> getFromConfig = name => new MockConnectionConfiguration("connection string");
+
+ // Act
+ IConnectionConfiguration configuration = configManager.GetConnection("foo", getFromConfig, fileExists);
+
+ // Assert
+ Assert.NotNull(configuration);
+ Assert.Equal("connection string", configuration.ConnectionString);
+ }
+
+ [Fact]
+ public void GetConnectionGetsConnectionFromDataDirectoryIfFileWithSupportedExtensionExists()
+ {
+ // Arrange
+ var mockHandler = new Mock<MockDbFileHandler>();
+ mockHandler.Setup(m => m.GetConnectionConfiguration(@"DataDirectory\Bar.foo")).Returns(new MockConnectionConfiguration("some file based connection"));
+ var handlers = new Dictionary<string, IDbFileHandler>
+ {
+ { ".foo", mockHandler.Object }
+ };
+ var configManager = new ConfigurationManagerWrapper(handlers, "DataDirectory");
+ Func<string, bool> fileExists = path => path.Equals(@"DataDirectory\Bar.foo");
+ Func<string, IConnectionConfiguration> getFromConfig = name => null;
+
+ // Act
+ IConnectionConfiguration configuration = configManager.GetConnection("Bar", getFromConfig, fileExists);
+
+ // Assert
+ Assert.NotNull(configuration);
+ Assert.Equal("some file based connection", configuration.ConnectionString);
+ }
+
+ [Fact]
+ public void GetConnectionSdfAndMdfFile_MdfFileWins()
+ {
+ // Arrange
+ var mockSdfHandler = new Mock<MockDbFileHandler>();
+ mockSdfHandler.Setup(m => m.GetConnectionConfiguration(@"DataDirectory\Bar.sdf")).Returns(new MockConnectionConfiguration("sdf connection"));
+ var mockMdfHandler = new Mock<MockDbFileHandler>();
+ mockMdfHandler.Setup(m => m.GetConnectionConfiguration(@"DataDirectory\Bar.mdf")).Returns(new MockConnectionConfiguration("mdf connection"));
+ var handlers = new Dictionary<string, IDbFileHandler>
+ {
+ { ".sdf", mockSdfHandler.Object },
+ { ".mdf", mockMdfHandler.Object },
+ };
+ var configManager = new ConfigurationManagerWrapper(handlers, "DataDirectory");
+ Func<string, bool> fileExists = path => path.Equals(@"DataDirectory\Bar.mdf") ||
+ path.Equals(@"DataDirectory\Bar.sdf");
+ Func<string, IConnectionConfiguration> getFromConfig = name => null;
+
+ // Act
+ IConnectionConfiguration configuration = configManager.GetConnection("Bar", getFromConfig, fileExists);
+
+ // Assert
+ Assert.NotNull(configuration);
+ Assert.Equal("mdf connection", configuration.ConnectionString);
+ }
+
+ [Fact]
+ public void GetConnectionReturnsNullIfNoConnectionFound()
+ {
+ // Act
+ var configManager = new ConfigurationManagerWrapper(new Dictionary<string, IDbFileHandler>(), "DataDirectory");
+ Func<string, bool> fileExists = path => false;
+ Func<string, IConnectionConfiguration> getFromConfig = name => null;
+
+ // Act
+ IConnectionConfiguration configuration = configManager.GetConnection("test", getFromConfig, fileExists);
+
+ // Assert
+ Assert.Null(configuration);
+ }
+ }
+}
diff --git a/test/WebMatrix.Data.Test/DatabaseTest.cs b/test/WebMatrix.Data.Test/DatabaseTest.cs
new file mode 100644
index 00000000..d0f7fbfc
--- /dev/null
+++ b/test/WebMatrix.Data.Test/DatabaseTest.cs
@@ -0,0 +1,89 @@
+using System;
+using System.Collections.Generic;
+using System.Data.Common;
+using System.Web.WebPages.TestUtils;
+using Moq;
+using WebMatrix.Data.Test.Mocks;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace WebMatrix.Data.Test
+{
+ public class DatabaseTest
+ {
+ [Fact]
+ public void OpenWithNullConnectionStringNameThrowsException()
+ {
+ Assert.ThrowsArgumentNullOrEmptyString(() => Database.Open(null), "name");
+ }
+
+ [Fact]
+ public void OpenConnectionStringWithNullConnectionStringThrowsException()
+ {
+ Assert.ThrowsArgumentNullOrEmptyString(() => Database.OpenConnectionString(null), "connectionString");
+ }
+
+ [Fact]
+ public void OpenConnectionStringWithEmptyConnectionStringThrowsException()
+ {
+ Assert.ThrowsArgumentNullOrEmptyString(() => Database.OpenConnectionString(String.Empty), "connectionString");
+ }
+
+ [Fact]
+ public void OpenNamedConnectionUsesConnectionStringFromConfigurationIfExists()
+ {
+ // Arrange
+ MockConfigurationManager mockConfigurationManager = new MockConfigurationManager();
+ Mock<DbConnection> mockConnection = new Mock<DbConnection>();
+ mockConnection.Setup(m => m.ConnectionString).Returns("connection string");
+ Mock<MockDbProviderFactory> mockProviderFactory = new Mock<MockDbProviderFactory>();
+ mockProviderFactory.Setup(m => m.CreateConnection("connection string")).Returns(mockConnection.Object);
+ mockConfigurationManager.AddConnection("foo", new ConnectionConfiguration(mockProviderFactory.Object, "connection string"));
+
+ // Act
+ Database db = Database.OpenNamedConnection("foo", mockConfigurationManager);
+
+ // Assert
+ Assert.Equal("connection string", db.Connection.ConnectionString);
+ }
+
+ [Fact]
+ public void OpenNamedConnectionThrowsIfNoConnectionFound()
+ {
+ // Arrange
+ IConfigurationManager mockConfigurationManager = new MockConfigurationManager();
+
+ // Act & Assert
+ Assert.Throws<InvalidOperationException>(() => Database.OpenNamedConnection("foo", mockConfigurationManager), "Connection string \"foo\" was not found.");
+ }
+
+ [Fact]
+ public void GetConnectionConfigurationGetConnectionForFileHandlersIfRegistered()
+ {
+ // Arrange
+ var mockHandler = new Mock<MockDbFileHandler>();
+ mockHandler.Setup(m => m.GetConnectionConfiguration("filename.foo")).Returns(new MockConnectionConfiguration("some file based connection"));
+ var handlers = new Dictionary<string, IDbFileHandler>
+ {
+ { ".foo", mockHandler.Object }
+ };
+
+ // Act
+ IConnectionConfiguration configuration = Database.GetConnectionConfiguration("filename.foo", handlers);
+
+ // Assert
+ Assert.NotNull(configuration);
+ Assert.Equal("some file based connection", configuration.ConnectionString);
+ }
+
+ [Fact]
+ public void GetConnectionThrowsIfNoHandlersRegisteredForExtension()
+ {
+ // Arrange
+ var handlers = new Dictionary<string, IDbFileHandler>();
+
+ // Act
+ Assert.Throws<InvalidOperationException>(() => Database.GetConnectionConfiguration("filename.foo", handlers), "Unable to determine the provider for the database file \"filename.foo\".");
+ }
+ }
+}
diff --git a/test/WebMatrix.Data.Test/DynamicRecordTest.cs b/test/WebMatrix.Data.Test/DynamicRecordTest.cs
new file mode 100644
index 00000000..2e30e5fb
--- /dev/null
+++ b/test/WebMatrix.Data.Test/DynamicRecordTest.cs
@@ -0,0 +1,150 @@
+using System;
+using System.ComponentModel;
+using System.Data;
+using System.Linq;
+using Moq;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace WebMatrix.Data.Test
+{
+ public class DynamicRecordTest
+ {
+ [Fact]
+ public void GetFieldValueByNameAccessesUnderlyingRecordForValue()
+ {
+ // Arrange
+ var mockRecord = new Mock<IDataRecord>();
+ mockRecord.SetupGet(m => m["A"]).Returns(1);
+ mockRecord.SetupGet(m => m["B"]).Returns(2);
+
+ dynamic record = new DynamicRecord(new[] { "A", "B" }, mockRecord.Object);
+
+ // Assert
+ Assert.Equal(1, record.A);
+ Assert.Equal(2, record.B);
+ }
+
+ [Fact]
+ public void GetFieldValueByIndexAccessesUnderlyingRecordForValue()
+ {
+ // Arrange
+ var mockRecord = new Mock<IDataRecord>();
+ mockRecord.SetupGet(m => m[0]).Returns(1);
+ mockRecord.SetupGet(m => m[1]).Returns(2);
+
+ dynamic record = new DynamicRecord(new[] { "A", "B" }, mockRecord.Object);
+
+ // Assert
+ Assert.Equal(1, record[0]);
+ Assert.Equal(2, record[1]);
+ }
+
+ [Fact]
+ public void GetFieldValueByNameReturnsNullIfValueIsDbNull()
+ {
+ // Arrange
+ var mockRecord = new Mock<IDataRecord>();
+ mockRecord.SetupGet(m => m["A"]).Returns(DBNull.Value);
+
+ dynamic record = new DynamicRecord(new[] { "A" }, mockRecord.Object);
+
+ // Assert
+ Assert.Null(record.A);
+ }
+
+ [Fact]
+ public void GetFieldValueByIndexReturnsNullIfValueIsDbNull()
+ {
+ // Arrange
+ var mockRecord = new Mock<IDataRecord>();
+ mockRecord.SetupGet(m => m[0]).Returns(DBNull.Value);
+
+ dynamic record = new DynamicRecord(new[] { "A" }, mockRecord.Object);
+
+ // Assert
+ Assert.Null(record[0]);
+ }
+
+ [Fact]
+ public void GetInvalidFieldValueThrows()
+ {
+ // Arrange
+ var mockRecord = new Mock<IDataRecord>();
+ dynamic record = new DynamicRecord(Enumerable.Empty<string>(), mockRecord.Object);
+
+ // Assert
+ Assert.Throws<InvalidOperationException>(() => { var value = record.C; }, "Invalid column name \"C\".");
+ }
+
+ [Fact]
+ public void VerfiyCustomTypeDescriptorMethods()
+ {
+ // Arrange
+ var mockRecord = new Mock<IDataRecord>();
+ mockRecord.SetupGet(m => m["A"]).Returns(1);
+ mockRecord.SetupGet(m => m["B"]).Returns(2);
+
+ // Act
+ ICustomTypeDescriptor record = new DynamicRecord(new[] { "A", "B" }, mockRecord.Object);
+
+ // Assert
+ Assert.Equal(AttributeCollection.Empty, record.GetAttributes());
+ Assert.Null(record.GetClassName());
+ Assert.Null(record.GetConverter());
+ Assert.Null(record.GetDefaultEvent());
+ Assert.Null(record.GetComponentName());
+ Assert.Null(record.GetDefaultProperty());
+ Assert.Null(record.GetEditor(null));
+ Assert.Equal(EventDescriptorCollection.Empty, record.GetEvents());
+ Assert.Equal(EventDescriptorCollection.Empty, record.GetEvents(null));
+ Assert.Same(record, record.GetPropertyOwner(null));
+ Assert.Equal(2, record.GetProperties().Count);
+ Assert.Equal(2, record.GetProperties(null).Count);
+ Assert.NotNull(record.GetProperties()["A"]);
+ Assert.NotNull(record.GetProperties()["B"]);
+ }
+
+ [Fact]
+ public void VerifyPropertyDescriptorProperties()
+ {
+ // Arrange
+ var mockRecord = new Mock<IDataRecord>();
+ mockRecord.SetupGet(m => m["A"]).Returns(1);
+ mockRecord.Setup(m => m.GetOrdinal("A")).Returns(0);
+ mockRecord.Setup(m => m.GetFieldType(0)).Returns(typeof(string));
+
+ // Act
+ ICustomTypeDescriptor record = new DynamicRecord(new[] { "A" }, mockRecord.Object);
+
+ // Assert
+ var aDescriptor = record.GetProperties().Find("A", ignoreCase: false);
+
+ Assert.NotNull(aDescriptor);
+ Assert.Null(aDescriptor.GetValue(null));
+ Assert.Equal(1, aDescriptor.GetValue(record));
+ Assert.True(aDescriptor.IsReadOnly);
+ Assert.Equal(typeof(string), aDescriptor.PropertyType);
+ Assert.Equal(typeof(DynamicRecord), aDescriptor.ComponentType);
+ Assert.False(aDescriptor.ShouldSerializeValue(record));
+ Assert.False(aDescriptor.CanResetValue(record));
+ }
+
+ [Fact]
+ public void SetAndResetValueOnPropertyDescriptorThrows()
+ {
+ // Arrange
+ var mockRecord = new Mock<IDataRecord>();
+ mockRecord.SetupGet(m => m["A"]).Returns(1);
+
+ // Act
+ ICustomTypeDescriptor record = new DynamicRecord(new[] { "A" }, mockRecord.Object);
+
+ // Assert
+ var aDescriptor = record.GetProperties().Find("A", ignoreCase: false);
+ Assert.NotNull(aDescriptor);
+ Assert.Throws<InvalidOperationException>(() => aDescriptor.SetValue(record, 1), "Unable to modify the value of column \"A\" because the record is read only.");
+ Assert.Throws<InvalidOperationException>(() => aDescriptor.ResetValue(record), "Unable to modify the value of column \"A\" because the record is read only.");
+ }
+ }
+}
diff --git a/test/WebMatrix.Data.Test/FileHandlerTest.cs b/test/WebMatrix.Data.Test/FileHandlerTest.cs
new file mode 100644
index 00000000..51d833ba
--- /dev/null
+++ b/test/WebMatrix.Data.Test/FileHandlerTest.cs
@@ -0,0 +1,52 @@
+using Xunit;
+
+namespace WebMatrix.Data.Test
+{
+ public class FileHandlerTest
+ {
+ [Fact]
+ public void SqlCeFileHandlerReturnsDataDirectoryRelativeConnectionStringIfPathIsNotRooted()
+ {
+ // Act
+ string connectionString = SqlCeDbFileHandler.GetConnectionString("foo.sdf");
+
+ // Assert
+ Assert.NotNull(connectionString);
+ Assert.Equal(@"Data Source=|DataDirectory|\foo.sdf;File Access Retry Timeout=10", connectionString);
+ }
+
+ [Fact]
+ public void SqlCeFileHandlerReturnsFullPathConnectionStringIfPathIsNotRooted()
+ {
+ // Act
+ string connectionString = SqlCeDbFileHandler.GetConnectionString(@"c:\foo.sdf");
+
+ // Assert
+ Assert.NotNull(connectionString);
+ Assert.Equal(@"Data Source=c:\foo.sdf;File Access Retry Timeout=10", connectionString);
+ }
+
+ [Fact]
+ public void SqlServerFileHandlerReturnsDataDirectoryRelativeConnectionStringIfPathIsNotRooted()
+ {
+ // Act
+ string connectionString = SqlServerDbFileHandler.GetConnectionString("foo.mdf", "datadir");
+
+ // Assert
+ Assert.NotNull(connectionString);
+ Assert.Equal(@"Data Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\foo.mdf;Initial Catalog=datadir\foo.mdf;Integrated Security=True;User Instance=True;MultipleActiveResultSets=True",
+ connectionString);
+ }
+
+ [Fact]
+ public void SqlServerFileHandlerReturnsFullPathConnectionStringIfPathIsNotRooted()
+ {
+ // Act
+ string connectionString = SqlServerDbFileHandler.GetConnectionString(@"c:\foo.mdf", "datadir");
+
+ // Assert
+ Assert.NotNull(connectionString);
+ Assert.Equal(@"Data Source=.\SQLEXPRESS;AttachDbFilename=c:\foo.mdf;Initial Catalog=c:\foo.mdf;Integrated Security=True;User Instance=True;MultipleActiveResultSets=True", connectionString);
+ }
+ }
+}
diff --git a/test/WebMatrix.Data.Test/Mocks/MockConfigurationManager.cs b/test/WebMatrix.Data.Test/Mocks/MockConfigurationManager.cs
new file mode 100644
index 00000000..49275fa8
--- /dev/null
+++ b/test/WebMatrix.Data.Test/Mocks/MockConfigurationManager.cs
@@ -0,0 +1,28 @@
+using System.Collections.Generic;
+
+namespace WebMatrix.Data.Test.Mocks
+{
+ internal class MockConfigurationManager : IConfigurationManager
+ {
+ private Dictionary<string, IConnectionConfiguration> _connectionStrings = new Dictionary<string, IConnectionConfiguration>();
+
+ public MockConfigurationManager()
+ {
+ AppSettings = new Dictionary<string, string>();
+ }
+
+ public IDictionary<string, string> AppSettings { get; private set; }
+
+ public void AddConnection(string name, IConnectionConfiguration configuration)
+ {
+ _connectionStrings.Add(name, configuration);
+ }
+
+ public IConnectionConfiguration GetConnection(string name)
+ {
+ IConnectionConfiguration configuration;
+ _connectionStrings.TryGetValue(name, out configuration);
+ return configuration;
+ }
+ }
+}
diff --git a/test/WebMatrix.Data.Test/Mocks/MockConnectionConfiguration.cs b/test/WebMatrix.Data.Test/Mocks/MockConnectionConfiguration.cs
new file mode 100644
index 00000000..395a9556
--- /dev/null
+++ b/test/WebMatrix.Data.Test/Mocks/MockConnectionConfiguration.cs
@@ -0,0 +1,22 @@
+namespace WebMatrix.Data.Test.Mocks
+{
+ public class MockConnectionConfiguration : IConnectionConfiguration
+ {
+ public MockConnectionConfiguration(string connectionString)
+ {
+ ConnectionString = connectionString;
+ }
+
+ public string ConnectionString { get; private set; }
+
+ string IConnectionConfiguration.ConnectionString
+ {
+ get { return ConnectionString; }
+ }
+
+ IDbProviderFactory IConnectionConfiguration.ProviderFactory
+ {
+ get { return null; }
+ }
+ }
+}
diff --git a/test/WebMatrix.Data.Test/Mocks/MockDbFileHandler.cs b/test/WebMatrix.Data.Test/Mocks/MockDbFileHandler.cs
new file mode 100644
index 00000000..fe5129e2
--- /dev/null
+++ b/test/WebMatrix.Data.Test/Mocks/MockDbFileHandler.cs
@@ -0,0 +1,12 @@
+namespace WebMatrix.Data.Test.Mocks
+{
+ public abstract class MockDbFileHandler : IDbFileHandler
+ {
+ IConnectionConfiguration IDbFileHandler.GetConnectionConfiguration(string fileName)
+ {
+ return GetConnectionConfiguration(fileName);
+ }
+
+ public abstract MockConnectionConfiguration GetConnectionConfiguration(string fileName);
+ }
+}
diff --git a/test/WebMatrix.Data.Test/Mocks/MockDbProviderFactory.cs b/test/WebMatrix.Data.Test/Mocks/MockDbProviderFactory.cs
new file mode 100644
index 00000000..34a09c93
--- /dev/null
+++ b/test/WebMatrix.Data.Test/Mocks/MockDbProviderFactory.cs
@@ -0,0 +1,10 @@
+using System.Data.Common;
+
+namespace WebMatrix.Data.Test.Mocks
+{
+ // Needs to be public for Moq to work
+ public abstract class MockDbProviderFactory : IDbProviderFactory
+ {
+ public abstract DbConnection CreateConnection(string connectionString);
+ }
+}
diff --git a/test/WebMatrix.Data.Test/Properties/AssemblyInfo.cs b/test/WebMatrix.Data.Test/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..60ce4512
--- /dev/null
+++ b/test/WebMatrix.Data.Test/Properties/AssemblyInfo.cs
@@ -0,0 +1,8 @@
+using System.Reflection;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+
+[assembly: AssemblyTitle("WebMatrix.Data.Test")]
+[assembly: AssemblyDescription("")]
diff --git a/test/WebMatrix.Data.Test/WebMatrix.Data.Test.csproj b/test/WebMatrix.Data.Test/WebMatrix.Data.Test.csproj
new file mode 100644
index 00000000..5a0e6691
--- /dev/null
+++ b/test/WebMatrix.Data.Test/WebMatrix.Data.Test.csproj
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProductVersion>9.0.30729</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{E2D008A9-4D1D-4F6B-8325-4ED717D6EA0A}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>WebMatrix.Data.Test</RootNamespace>
+ <AssemblyName>WebMatrix.Data.Test</AssemblyName>
+ <FileAlignment>512</FileAlignment>
+ <ProjectTypeGuids>{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Debug\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Release\Test\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'CodeCoverage' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\CodeCoverage\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="Microsoft.CSharp" />
+ <Reference Include="Moq, Version=4.0.10827.0, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL">
+ <HintPath>..\..\packages\Moq.4.0.10827\lib\NET40\Moq.dll</HintPath>
+ </Reference>
+ <Reference Include="System" />
+ <Reference Include="System.Core">
+ <RequiredTargetFramework>3.5</RequiredTargetFramework>
+ </Reference>
+ <Reference Include="System.Data" />
+ <Reference Include="xunit">
+ <HintPath>..\..\packages\xunit.1.9.0.1566\lib\xunit.dll</HintPath>
+ </Reference>
+ <Reference Include="xunit.extensions">
+ <HintPath>..\..\packages\xunit.extensions.1.9.0.1566\lib\xunit.extensions.dll</HintPath>
+ </Reference>
+ </ItemGroup>
+ <ItemGroup>
+ <CodeAnalysisDependentAssemblyPaths Condition=" '$(VS100COMNTOOLS)' != '' " Include="$(VS100COMNTOOLS)..\IDE\PrivateAssemblies">
+ <Visible>False</Visible>
+ </CodeAnalysisDependentAssemblyPaths>
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\WebMatrix.Data\WebMatrix.Data.csproj">
+ <Project>{4D39BAAF-8A96-473E-AB79-C8A341885137}</Project>
+ <Name>WebMatrix.Data</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\Microsoft.TestCommon\Microsoft.TestCommon.csproj">
+ <Project>{FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0}</Project>
+ <Name>Microsoft.TestCommon</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="ConfigurationManagerWrapperTest.cs" />
+ <Compile Include="DynamicRecordTest.cs" />
+ <Compile Include="FileHandlerTest.cs" />
+ <Compile Include="Mocks\MockConfigurationManager.cs" />
+ <Compile Include="Mocks\MockConnectionConfiguration.cs" />
+ <Compile Include="Mocks\MockDbFileHandler.cs" />
+ <Compile Include="Mocks\MockDbProviderFactory.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="DatabaseTest.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="packages.config" />
+ </ItemGroup>
+ <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+</Project> \ No newline at end of file
diff --git a/test/WebMatrix.Data.Test/packages.config b/test/WebMatrix.Data.Test/packages.config
new file mode 100644
index 00000000..d5aa6401
--- /dev/null
+++ b/test/WebMatrix.Data.Test/packages.config
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="Moq" version="4.0.10827" />
+ <package id="xunit" version="1.9.0.1566" />
+ <package id="xunit.extensions" version="1.9.0.1566" />
+</packages> \ No newline at end of file
diff --git a/test/WebMatrix.WebData.Test/MockDatabase.cs b/test/WebMatrix.WebData.Test/MockDatabase.cs
new file mode 100644
index 00000000..d77edb0b
--- /dev/null
+++ b/test/WebMatrix.WebData.Test/MockDatabase.cs
@@ -0,0 +1,20 @@
+using System.Collections.Generic;
+
+namespace WebMatrix.WebData.Test
+{
+ public abstract class MockDatabase : IDatabase
+ {
+ public abstract dynamic QuerySingle(string commandText, params object[] args);
+
+ public abstract IEnumerable<dynamic> Query(string commandText, params object[] parameters);
+
+ public abstract dynamic QueryValue(string commandText, params object[] parameters);
+
+ public abstract int Execute(string commandText, params object[] args);
+
+ public void Dispose()
+ {
+ // Do nothing.
+ }
+ }
+}
diff --git a/test/WebMatrix.WebData.Test/PreApplicationStartCodeTest.cs b/test/WebMatrix.WebData.Test/PreApplicationStartCodeTest.cs
new file mode 100644
index 00000000..9ff9efc2
--- /dev/null
+++ b/test/WebMatrix.WebData.Test/PreApplicationStartCodeTest.cs
@@ -0,0 +1,102 @@
+using System.Configuration;
+using System.Linq;
+using System.Reflection;
+using System.Web.Security;
+using System.Web.WebPages.Razor;
+using System.Web.WebPages.TestUtils;
+using Xunit;
+
+namespace WebMatrix.WebData.Test
+{
+ public class PreApplicationStartCodeTest
+ {
+ [Fact]
+ public void StartRegistersRazorNamespaces()
+ {
+ AppDomainUtils.RunInSeparateAppDomain(() =>
+ {
+ AppDomainUtils.SetPreAppStartStage();
+ PreApplicationStartCode.Start();
+ // Call a second time to ensure multiple calls do not cause issues
+ PreApplicationStartCode.Start();
+
+ // Verify namespaces
+ var imports = WebPageRazorHost.GetGlobalImports();
+ Assert.True(imports.Any(ns => ns.Equals("WebMatrix.Data")));
+ Assert.True(imports.Any(ns => ns.Equals("WebMatrix.WebData")));
+ });
+ }
+
+ [Fact]
+ public void StartInitializesFormsAuthByDefault()
+ {
+ AppDomainUtils.RunInSeparateAppDomain(() =>
+ {
+ AppDomainUtils.SetPreAppStartStage();
+ PreApplicationStartCode.Start();
+
+ string formsAuthLoginUrl = (string)typeof(FormsAuthentication).GetField("_LoginUrl", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null);
+ Assert.Equal(FormsAuthenticationSettings.DefaultLoginUrl, formsAuthLoginUrl);
+ });
+ }
+
+ [Fact]
+ public void StartDoesNotInitializeFormsAuthWhenDisabled()
+ {
+ AppDomainUtils.RunInSeparateAppDomain(() =>
+ {
+ AppDomainUtils.SetPreAppStartStage();
+ ConfigurationManager.AppSettings[WebSecurity.EnableSimpleMembershipKey] = "False";
+ PreApplicationStartCode.Start();
+
+ string formsAuthLoginUrl = (string)typeof(FormsAuthentication).GetField("_LoginUrl", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null);
+ Assert.Null(formsAuthLoginUrl);
+ });
+ }
+
+ [Fact]
+ public void StartInitializesSimpleMembershipByDefault()
+ {
+ AppDomainUtils.RunInSeparateAppDomain(() =>
+ {
+ AppDomainUtils.SetPreAppStartStage();
+ PreApplicationStartCode.Start();
+
+ // Verify simple membership
+ var providers = Membership.Providers;
+ Assert.Equal(1, providers.Count);
+ foreach (var provider in providers)
+ {
+ Assert.IsAssignableFrom<SimpleMembershipProvider>(provider);
+ }
+ Assert.True(Roles.Enabled);
+ });
+ }
+
+ [Fact]
+ public void StartDoesNotInitializeSimpleMembershipWhenDisabled()
+ {
+ AppDomainUtils.RunInSeparateAppDomain(() =>
+ {
+ AppDomainUtils.SetPreAppStartStage();
+ ConfigurationManager.AppSettings[WebSecurity.EnableSimpleMembershipKey] = "False";
+ PreApplicationStartCode.Start();
+
+ // Verify simple membership
+ var providers = Membership.Providers;
+ Assert.Equal(1, providers.Count);
+ foreach (var provider in providers)
+ {
+ Assert.IsAssignableFrom<SqlMembershipProvider>(provider);
+ }
+ Assert.False(Roles.Enabled);
+ });
+ }
+
+ [Fact]
+ public void TestPreAppStartClass()
+ {
+ PreAppStartTestHelper.TestPreAppStartClass(typeof(PreApplicationStartCode));
+ }
+ }
+}
diff --git a/test/WebMatrix.WebData.Test/Properties/AssemblyInfo.cs b/test/WebMatrix.WebData.Test/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..8e70bc6e
--- /dev/null
+++ b/test/WebMatrix.WebData.Test/Properties/AssemblyInfo.cs
@@ -0,0 +1,8 @@
+using System.Reflection;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+
+[assembly: AssemblyTitle("WebMatrix.WebData.Test")]
+[assembly: AssemblyDescription("")]
diff --git a/test/WebMatrix.WebData.Test/SimpleMembershipProviderTest.cs b/test/WebMatrix.WebData.Test/SimpleMembershipProviderTest.cs
new file mode 100644
index 00000000..fde44182
--- /dev/null
+++ b/test/WebMatrix.WebData.Test/SimpleMembershipProviderTest.cs
@@ -0,0 +1,199 @@
+using System;
+using System.Data;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using Moq;
+using WebMatrix.Data;
+using Xunit;
+
+namespace WebMatrix.WebData.Test
+{
+ public class SimpleMembershipProviderTest
+ {
+ [Fact]
+ public void ConfirmAccountReturnsFalseIfNoRecordExistsForToken()
+ {
+ // Arrange
+ var database = new Mock<MockDatabase>(MockBehavior.Strict);
+ database.Setup(d => d.Query("SELECT [UserId], [ConfirmationToken] FROM webpages_Membership WHERE [ConfirmationToken] = @0", "foo"))
+ .Returns(Enumerable.Empty<DynamicRecord>());
+ var simpleMembershipProvider = new TestSimpleMembershipProvider(database.Object);
+
+ // Act
+ bool result = simpleMembershipProvider.ConfirmAccount("foo");
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void ConfirmAccountReturnsFalseIfConfirmationTokenDoesNotMatchInCase()
+ {
+ // Arrange
+ var database = new Mock<MockDatabase>(MockBehavior.Strict);
+ DynamicRecord record = GetRecord(98, "Foo");
+ database.Setup(d => d.Query("SELECT [UserId], [ConfirmationToken] FROM webpages_Membership WHERE [ConfirmationToken] = @0", "foo"))
+ .Returns(new[] { record });
+ var simpleMembershipProvider = new TestSimpleMembershipProvider(database.Object);
+
+ // Act
+ bool result = simpleMembershipProvider.ConfirmAccount("foo");
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void ConfirmAccountReturnsFalseIfNoConfirmationTokenFromMultipleListMatchesInCase()
+ {
+ // Arrange
+ var database = new Mock<MockDatabase>(MockBehavior.Strict);
+ DynamicRecord recordA = GetRecord(98, "Foo");
+ DynamicRecord recordB = GetRecord(99, "fOo");
+ database.Setup(d => d.Query("SELECT [UserId], [ConfirmationToken] FROM webpages_Membership WHERE [ConfirmationToken] = @0", "foo"))
+ .Returns(new[] { recordA, recordB });
+ var simpleMembershipProvider = new TestSimpleMembershipProvider(database.Object);
+
+ // Act
+ bool result = simpleMembershipProvider.ConfirmAccount("foo");
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void ConfirmAccountUpdatesIsConfirmedFieldIfConfirmationTokenMatches()
+ {
+ // Arrange
+ var database = new Mock<MockDatabase>(MockBehavior.Strict);
+ DynamicRecord record = GetRecord(100, "foo");
+ database.Setup(d => d.Query("SELECT [UserId], [ConfirmationToken] FROM webpages_Membership WHERE [ConfirmationToken] = @0", "foo"))
+ .Returns(new[] { record }).Verifiable();
+ database.Setup(d => d.Execute("UPDATE webpages_Membership SET [IsConfirmed] = 1 WHERE [UserId] = @0", 100)).Returns(1).Verifiable();
+ var simpleMembershipProvider = new TestSimpleMembershipProvider(database.Object);
+
+ // Act
+ bool result = simpleMembershipProvider.ConfirmAccount("foo");
+
+ // Assert
+ Assert.True(result);
+ database.Verify();
+ }
+
+ [Fact]
+ public void ConfirmAccountUpdatesIsConfirmedFieldIfAnyOneOfReturnRecordConfirmationTokenMatches()
+ {
+ // Arrange
+ var database = new Mock<MockDatabase>(MockBehavior.Strict);
+ DynamicRecord recordA = GetRecord(100, "Foo");
+ DynamicRecord recordB = GetRecord(101, "foo");
+ DynamicRecord recordC = GetRecord(102, "fOo");
+ database.Setup(d => d.Query("SELECT [UserId], [ConfirmationToken] FROM webpages_Membership WHERE [ConfirmationToken] = @0", "foo"))
+ .Returns(new[] { recordA, recordB, recordC }).Verifiable();
+ database.Setup(d => d.Execute("UPDATE webpages_Membership SET [IsConfirmed] = 1 WHERE [UserId] = @0", 101)).Returns(1).Verifiable();
+ var simpleMembershipProvider = new TestSimpleMembershipProvider(database.Object);
+
+ // Act
+ bool result = simpleMembershipProvider.ConfirmAccount("foo");
+
+ // Assert
+ Assert.True(result);
+ database.Verify();
+ }
+
+ [Fact]
+ public void ConfirmAccountWithUserNameReturnsFalseIfNoRecordExistsForToken()
+ {
+ // Arrange
+ var database = new Mock<MockDatabase>(MockBehavior.Strict);
+ database.Setup(d => d.QuerySingle("SELECT m.[UserId], m.[ConfirmationToken] FROM webpages_Membership m JOIN [Users] u ON m.[UserId] = u.[UserId] WHERE m.[ConfirmationToken] = @0 AND u.[UserName] = @1", "foo", "user12")).Returns(null);
+ var simpleMembershipProvider = new TestSimpleMembershipProvider(database.Object) { UserIdColumn = "UserId", UserNameColumn = "UserName", UserTableName = "Users" };
+
+ // Act
+ bool result = simpleMembershipProvider.ConfirmAccount("user12", "foo");
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void ConfirmAccountWithUserNameReturnsFalseIfConfirmationTokenDoesNotMatchInCase()
+ {
+ // Arrange
+ var database = new Mock<MockDatabase>(MockBehavior.Strict);
+ DynamicRecord record = GetRecord(98, "Foo");
+ database.Setup(d => d.QuerySingle("SELECT m.[UserId], m.[ConfirmationToken] FROM webpages_Membership m JOIN [Users_bkp2_1] u ON m.[UserId] = u.[wishlist_site_real_user_id] WHERE m.[ConfirmationToken] = @0 AND u.[wishlist_site_real_user_name] = @1", "foo", "user13")).Returns(record);
+ var simpleMembershipProvider = new TestSimpleMembershipProvider(database.Object) { UserIdColumn = "wishlist_site_real_user_id", UserNameColumn = "wishlist_site_real_user_name", UserTableName = "Users_bkp2_1" };
+
+ // Act
+ bool result = simpleMembershipProvider.ConfirmAccount("user13", "foo");
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void ConfirmAccountWithUserNameUpdatesIsConfirmedFieldIfConfirmationTokenMatches()
+ {
+ // Arrange
+ var database = new Mock<MockDatabase>(MockBehavior.Strict);
+ DynamicRecord record = GetRecord(100, "foo");
+ database.Setup(d => d.QuerySingle("SELECT m.[UserId], m.[ConfirmationToken] FROM webpages_Membership m JOIN [Users] u ON m.[UserId] = u.[Id] WHERE m.[ConfirmationToken] = @0 AND u.[UserName] = @1", "foo", "user14"))
+ .Returns(record).Verifiable();
+ database.Setup(d => d.Execute("UPDATE webpages_Membership SET [IsConfirmed] = 1 WHERE [UserId] = @0", 100)).Returns(1).Verifiable();
+ var simpleMembershipProvider = new TestSimpleMembershipProvider(database.Object) { UserTableName = "Users", UserIdColumn = "Id", UserNameColumn = "UserName" };
+
+ // Act
+ bool result = simpleMembershipProvider.ConfirmAccount("user14", "foo");
+
+ // Assert
+ Assert.True(result);
+ database.Verify();
+ }
+
+ [Fact]
+ public void GenerateTokenHtmlEncodesValues()
+ {
+ // Arrange
+ var generator = new Mock<RandomNumberGenerator>(MockBehavior.Strict);
+ var generatedBytes = Encoding.Default.GetBytes("|aÿx§#½oÿ↨îA8Eµ");
+ generator.Setup(g => g.GetBytes(It.IsAny<byte[]>())).Callback((byte[] array) => Array.Copy(generatedBytes, array, generatedBytes.Length));
+
+ // Act
+ var result = SimpleMembershipProvider.GenerateToken(generator.Object);
+
+ // Assert
+ Assert.Equal("fGH/eKcjvW//P+5BOEW1", Convert.ToBase64String(generatedBytes));
+ Assert.Equal("fGH_eKcjvW__P-5BOEW1AA2", result);
+ }
+
+ private static DynamicRecord GetRecord(int userId, string confirmationToken)
+ {
+ var data = new Mock<IDataRecord>(MockBehavior.Strict);
+ data.Setup(c => c[0]).Returns(userId);
+ data.Setup(c => c[1]).Returns(confirmationToken);
+ return new DynamicRecord(new[] { "UserId", "ConfirmationToken" }, data.Object);
+ }
+
+ private class TestSimpleMembershipProvider : SimpleMembershipProvider
+ {
+ private readonly IDatabase _database;
+
+ public TestSimpleMembershipProvider(IDatabase database)
+ {
+ _database = database;
+ }
+
+ internal override IDatabase ConnectToDatabase()
+ {
+ return _database;
+ }
+
+ internal override void VerifyInitialized()
+ {
+ // Do nothing.
+ }
+ }
+ }
+}
diff --git a/test/WebMatrix.WebData.Test/WebMatrix.WebData.Test.csproj b/test/WebMatrix.WebData.Test/WebMatrix.WebData.Test.csproj
new file mode 100644
index 00000000..7587f03c
--- /dev/null
+++ b/test/WebMatrix.WebData.Test/WebMatrix.WebData.Test.csproj
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProductVersion>9.0.30729</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{CD48EB41-92A5-4628-A0F7-6A43DF58827E}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>WebMatrix.WebData.Test</RootNamespace>
+ <AssemblyName>WebMatrix.WebData.Test</AssemblyName>
+ <FileAlignment>512</FileAlignment>
+ <ProjectTypeGuids>{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Debug\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\Release\Test\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'CodeCoverage' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>$(WebStackRootPath)\bin\CodeCoverage\Test\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="Moq, Version=4.0.10827.0, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL">
+ <HintPath>..\..\packages\Moq.4.0.10827\lib\NET40\Moq.dll</HintPath>
+ </Reference>
+ <Reference Include="System" />
+ <Reference Include="System.configuration" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.Web" />
+ <Reference Include="System.Web.ApplicationServices" />
+ <Reference Include="Microsoft.CSharp" />
+ <Reference Include="System.Data" />
+ <Reference Include="xunit">
+ <HintPath>..\..\packages\xunit.1.9.0.1566\lib\xunit.dll</HintPath>
+ </Reference>
+ <Reference Include="xunit.extensions">
+ <HintPath>..\..\packages\xunit.extensions.1.9.0.1566\lib\xunit.extensions.dll</HintPath>
+ </Reference>
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="MockDatabase.cs" />
+ <Compile Include="PreApplicationStartCodeTest.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="SimpleMembershipProviderTest.cs" />
+ <Compile Include="WebSecurityTest.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\System.Web.Razor\System.Web.Razor.csproj">
+ <Project>{8F18041B-9410-4C36-A9C5-067813DF5F31}</Project>
+ <Name>System.Web.Razor</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\src\System.Web.WebPages.Razor\System.Web.WebPages.Razor.csproj">
+ <Project>{0939B11A-FE4E-4BA1-8AD6-D97741EE314F}</Project>
+ <Name>System.Web.WebPages.Razor</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\src\WebMatrix.Data\WebMatrix.Data.csproj">
+ <Project>{4D39BAAF-8A96-473E-AB79-C8A341885137}</Project>
+ <Name>WebMatrix.Data</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\src\WebMatrix.WebData\WebMatrix.WebData.csproj">
+ <Project>{55A15F40-1435-4248-A7F2-2A146BB83586}</Project>
+ <Name>WebMatrix.WebData</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\Microsoft.TestCommon\Microsoft.TestCommon.csproj">
+ <Project>{FCCC4CB7-BAF7-4A57-9F89-E5766FE536C0}</Project>
+ <Name>Microsoft.TestCommon</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="packages.config" />
+ </ItemGroup>
+ <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+</Project> \ No newline at end of file
diff --git a/test/WebMatrix.WebData.Test/WebSecurityTest.cs b/test/WebMatrix.WebData.Test/WebSecurityTest.cs
new file mode 100644
index 00000000..aa58c978
--- /dev/null
+++ b/test/WebMatrix.WebData.Test/WebSecurityTest.cs
@@ -0,0 +1,26 @@
+using System;
+using Xunit;
+using Assert = Microsoft.TestCommon.AssertEx;
+
+namespace WebMatrix.WebData.Test
+{
+ public class WebSecurityTest
+ {
+ [Fact]
+ public void VerifyExtendedMembershipProviderMethodsThrowWithInvalidProvider()
+ {
+ const string errorString = "To call this method, the \"Membership.Provider\" property must be an instance of \"ExtendedMembershipProvider\".";
+ Assert.Throws<InvalidOperationException>(() => WebSecurity.ConfirmAccount(""), errorString);
+ Assert.Throws<InvalidOperationException>(() => WebSecurity.GeneratePasswordResetToken(""), errorString);
+ Assert.Throws<InvalidOperationException>(() => WebSecurity.GetUserIdFromPasswordResetToken(""), errorString);
+ Assert.Throws<InvalidOperationException>(() => WebSecurity.ResetPassword("", "whatever"), errorString);
+ Assert.Throws<InvalidOperationException>(() => WebSecurity.CreateUserAndAccount("", "whatever"), errorString);
+ Assert.Throws<InvalidOperationException>(() => WebSecurity.CreateAccount("", "whatever"), errorString);
+ Assert.Throws<InvalidOperationException>(() => WebSecurity.IsConfirmed("whatever"), errorString);
+ Assert.Throws<InvalidOperationException>(() => WebSecurity.GetPasswordFailuresSinceLastSuccess("whatever"), errorString);
+ Assert.Throws<InvalidOperationException>(() => WebSecurity.GetCreateDate("whatever"), errorString);
+ Assert.Throws<InvalidOperationException>(() => WebSecurity.GetLastPasswordFailureDate("whatever"), errorString);
+ Assert.Throws<InvalidOperationException>(() => WebSecurity.GetPasswordChangedDate("whatever"), errorString);
+ }
+ }
+}
diff --git a/test/WebMatrix.WebData.Test/packages.config b/test/WebMatrix.WebData.Test/packages.config
new file mode 100644
index 00000000..d5aa6401
--- /dev/null
+++ b/test/WebMatrix.WebData.Test/packages.config
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="Moq" version="4.0.10827" />
+ <package id="xunit" version="1.9.0.1566" />
+ <package id="xunit.extensions" version="1.9.0.1566" />
+</packages> \ No newline at end of file
diff --git a/tools/35MSSharedLib1024.snk b/tools/35MSSharedLib1024.snk
new file mode 100644
index 00000000..695f1b38
--- /dev/null
+++ b/tools/35MSSharedLib1024.snk
Binary files differ
diff --git a/tools/WebStack.NuGet.targets b/tools/WebStack.NuGet.targets
new file mode 100644
index 00000000..7864187d
--- /dev/null
+++ b/tools/WebStack.NuGet.targets
@@ -0,0 +1,123 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <PropertyGroup>
+ <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">$(MSBuildProjectDirectory)\..\</SolutionDir>
+ <NuGetToolsPath>$([System.IO.Path]::Combine($(SolutionDir), "Tools"))</NuGetToolsPath>
+ <NuGetExePath>$(NuGetToolsPath)\NuGet.exe</NuGetExePath>
+ <PackagesConfig>$([System.IO.Path]::Combine($(ProjectDir), "packages.config"))</PackagesConfig>
+ <PackagesDir>$([System.IO.Path]::Combine($(SolutionDir), "packages"))</PackagesDir>
+ <PackageOutputDir Condition="$(PackageOutputDir) == ''">$(TargetDir.Trim('\\'))</PackageOutputDir>
+
+ <!-- Package sources used to restore packages. By default will used the registered sources under %APPDATA%\NuGet\NuGet.Config -->
+ <PackageSources>"\\webstack-git\packages;https://go.microsoft.com/fwlink/?LinkID=230477"</PackageSources>
+
+ <!-- Enable the restore command to run before builds -->
+ <RestorePackages Condition="$(RestorePackages) == ''">false</RestorePackages>
+
+ <!-- Property that enables building a package from a project -->
+ <BuildPackage Condition="$(BuildPackage) == ''">false</BuildPackage>
+
+ <!-- Commands -->
+ <RestoreCommand>"$(NuGetExePath)" install "$(PackagesConfig)" -source $(PackageSources) -o "$(PackagesDir)" > NUL</RestoreCommand>
+ <BuildCommand>"$(NuGetExePath)" pack "$(ProjectPath)" -p Configuration=$(Configuration) -o "$(PackageOutputDir)" -symbols</BuildCommand>
+
+ <!-- Make the build depend on restore packages -->
+ <BuildDependsOn Condition="$(RestorePackages) == 'true'">
+ RestorePackages;
+ $(BuildDependsOn);
+ </BuildDependsOn>
+
+ <!-- Make the build depend on restore packages -->
+ <BuildDependsOn Condition="$(BuildPackage) == 'true'">
+ $(BuildDependsOn);
+ BuildPackage;
+ </BuildDependsOn>
+ </PropertyGroup>
+
+ <Target Name="CheckPrerequisites">
+ <DownloadNuGet OutputFilename="$(NuGetExePath)" Condition="!Exists('$(NuGetExePath)')" />
+ </Target>
+
+ <Target Name="RestorePackages" DependsOnTargets="CheckPrerequisites">
+ <Exec Command="$(RestoreCommand)"
+ LogStandardErrorAsError="true"
+ Condition="Exists('$(PackagesConfig)')" />
+ </Target>
+
+ <Target Name="BuildPackage" DependsOnTargets="CheckPrerequisites">
+ <Exec Command="$(BuildCommand)"
+ LogStandardErrorAsError="true" />
+ </Target>
+
+ <UsingTask TaskName="DownloadNuGet" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
+ <ParameterGroup>
+ <OutputFilename ParameterType="System.String" Required="true" />
+ </ParameterGroup>
+ <Task>
+ <Reference Include="System.Core" />
+ <Reference Include="System.Xml" />
+ <Reference Include="WindowsBase" />
+ <Using Namespace="System" />
+ <Using Namespace="System.IO" />
+ <Using Namespace="System.IO.Packaging" />
+ <Using Namespace="System.Linq" />
+ <Using Namespace="System.Net" />
+ <Using Namespace="System.Xml" />
+ <Using Namespace="Microsoft.Build.Framework" />
+ <Using Namespace="Microsoft.Build.Utilities" />
+ <Code Type="Fragment" Language="cs">
+ <![CDATA[
+ string zipTempPath = null;
+
+ try {
+ OutputFilename = Path.GetFullPath(OutputFilename);
+
+ if (File.Exists(OutputFilename)) {
+ return true;
+ }
+
+ Log.LogMessage("Determining latest version of NuGet.CommandLine...");
+ WebClient webClient = new WebClient();
+ XmlDocument xml = new XmlDocument();
+ xml.LoadXml(webClient.DownloadString("http://nuget.org/v1/FeedService.svc/Packages()?$filter=tolower(Id)%20eq%20'nuget.commandline'&$top=1&$orderby=Version%20desc"));
+ XmlNamespaceManager xns = new XmlNamespaceManager(xml.NameTable);
+ xns.AddNamespace("atom", "http://www.w3.org/2005/Atom");
+ xns.AddNamespace("d", "http://schemas.microsoft.com/ado/2007/08/dataservices");
+ xns.AddNamespace("m", "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata");
+ string version = xml.SelectSingleNode("//atom:entry/m:properties/d:Version", xns).InnerText;
+ string zipUrl = xml.SelectSingleNode("//atom:entry/atom:content", xns).Attributes["src"].Value;
+
+ Log.LogMessage("Downloading NuGet.CommandLine v{0}...", version);
+ zipTempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+ webClient.DownloadFile(zipUrl, zipTempPath);
+
+ Log.LogMessage("Copying to {0}...", OutputFilename);
+ using (Package package = Package.Open(zipTempPath)) {
+ PackagePart exePart = package.GetParts().Where(p => p.Uri.ToString().ToLowerInvariant() == "/tools/nuget.exe").Single();
+ using (Stream inputStream = exePart.GetStream(FileMode.Open, FileAccess.Read))
+ using (Stream outputStream = File.Create(OutputFilename)) {
+ byte[] buffer = new byte[16384];
+ while (true) {
+ int read = inputStream.Read(buffer, 0, buffer.Length);
+ if (read == 0) {
+ break;
+ }
+ outputStream.Write(buffer, 0, read);
+ }
+ }
+ }
+
+ return true;
+ }
+ catch (Exception ex) {
+ Log.LogErrorFromException(ex);
+ return false;
+ }
+ finally {
+ if (zipTempPath != null) File.Delete(zipTempPath);
+ }
+ ]]>
+ </Code>
+ </Task>
+ </UsingTask>
+</Project> \ No newline at end of file
diff --git a/tools/WebStack.StyleCop.targets b/tools/WebStack.StyleCop.targets
new file mode 100644
index 00000000..39ac6efa
--- /dev/null
+++ b/tools/WebStack.StyleCop.targets
@@ -0,0 +1,126 @@
+<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <ItemGroup>
+ <StyleCopMsBuildRunner Include="$(WebStackRootPath)packages\**\StyleCop.dll"/>
+ </ItemGroup>
+ <PropertyGroup>
+ <StyleCopEnabled Condition=" '$(StyleCopMsBuildRunner)' == '' ">false</StyleCopEnabled>
+ </PropertyGroup>
+ <UsingTask AssemblyFile="@(StyleCopMsBuildRunner)" TaskName="StyleCopTask" Condition=" '$(StyleCopEnabled)' != 'false' " />
+
+ <PropertyGroup>
+ <BuildDependsOn>$(BuildDependsOn);StyleCop</BuildDependsOn>
+ <RebuildDependsOn>StyleCopForceFullAnalysis;$(RebuildDependsOn)</RebuildDependsOn>
+ </PropertyGroup>
+
+ <!-- Define StyleCopForceFullAnalysis property. -->
+ <PropertyGroup Condition="('$(SourceAnalysisForceFullAnalysis)' != '') and ('$(StyleCopForceFullAnalysis)' == '')">
+ <StyleCopForceFullAnalysis>$(SourceAnalysisForceFullAnalysis)</StyleCopForceFullAnalysis>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(StyleCopForceFullAnalysis)' == ''">
+ <StyleCopForceFullAnalysis>false</StyleCopForceFullAnalysis>
+ </PropertyGroup>
+
+ <!-- Define StyleCopCacheResults property. -->
+ <PropertyGroup Condition="('$(SourceAnalysisCacheResults)' != '') and ('$(StyleCopCacheResults)' == '')">
+ <StyleCopCacheResults>$(SourceAnalysisCacheResults)</StyleCopCacheResults>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(StyleCopCacheResults)' == ''">
+ <StyleCopCacheResults>true</StyleCopCacheResults>
+ </PropertyGroup>
+
+ <!-- Define StyleCopTreatErrorsAsWarnings property. -->
+ <PropertyGroup Condition="('$(SourceAnalysisTreatErrorsAsWarnings)' != '') and ('$(StyleCopTreatErrorsAsWarnings)' == '')">
+ <StyleCopTreatErrorsAsWarnings>$(SourceAnalysisTreatErrorsAsWarnings)</StyleCopTreatErrorsAsWarnings>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(StyleCopTreatErrorsAsWarnings)' == ''">
+ <StyleCopTreatErrorsAsWarnings>true</StyleCopTreatErrorsAsWarnings>
+ </PropertyGroup>
+
+ <!-- Define StyleCopEnabled property. -->
+ <PropertyGroup Condition="('$(SourceAnalysisEnabled)' != '') and ('$(StyleCopEnabled)' == '')">
+ <StyleCopEnabled>$(SourceAnalysisEnabled)</StyleCopEnabled>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(StyleCopEnabled)' == ''">
+ <StyleCopEnabled>true</StyleCopEnabled>
+ </PropertyGroup>
+
+ <!-- Define StyleCopOverrideSettingsFile property. -->
+ <PropertyGroup Condition="('$(SourceAnalysisOverrideSettingsFile)' != '') and ('$(StyleCopOverrideSettingsFile)' == '')">
+ <StyleCopOverrideSettingsFile>$(SourceAnalysisOverrideSettingsFile)</StyleCopOverrideSettingsFile>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(StyleCopOverrideSettingsFile)' == ''">
+ <StyleCopOverrideSettingsFile> </StyleCopOverrideSettingsFile>
+ </PropertyGroup>
+
+ <!-- Define StyleCopOutputFile property. -->
+ <PropertyGroup Condition="('$(SourceAnalysisOutputFile)' != '') and ('$(StyleCopOutputFile)' == '')">
+ <StyleCopOutputFile>$(SourceAnalysisOutputFile)</StyleCopOutputFile>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(StyleCopOutputFile)' == ''">
+ <StyleCopOutputFile>$(IntermediateOutputPath)StyleCopViolations.xml</StyleCopOutputFile>
+ </PropertyGroup>
+
+ <!-- Define all new properties which do not need to have both StyleCop and SourceAnalysis variations. -->
+ <PropertyGroup>
+ <!-- Specifying 0 will cause StyleCop to use the default violation count limit.
+ Specifying any positive number will cause StyleCop to use that number as the violation count limit.
+ Specifying any negative number will cause StyleCop to allow any number of violations without limit. -->
+ <StyleCopMaxViolationCount Condition="'$(StyleCopMaxViolationCount)' == ''">0</StyleCopMaxViolationCount>
+ </PropertyGroup>
+
+ <!-- Define target: StyleCopForceFullAnalysis -->
+ <Target Name="StyleCopForceFullAnalysis">
+ <CreateProperty Value="true">
+ <Output TaskParameter="Value" PropertyName="StyleCopForceFullAnalysis" />
+ </CreateProperty>
+ </Target>
+
+ <!-- Define target: StyleCop -->
+ <Target Name="StyleCop" Condition="'$(StyleCopEnabled)' != 'false'">
+ <Message Text="Forcing full StyleCop reanalysis." Condition="'$(StyleCopForceFullAnalysis)' == 'true'" Importance="Low" />
+
+ <!-- Determine what files should be checked. Take all Compile items, but exclude those that have
+ set ExcludeFromStyleCop=true or ExcludeFromSourceAnalysis=true. -->
+ <CreateItem Include="@(Compile)" Condition="('%(Compile.ExcludeFromStyleCop)' != 'true') and ('%(Compile.ExcludeFromSourceAnalysis)' != 'true')">
+ <Output TaskParameter="Include" ItemName="StyleCopFiles"/>
+ </CreateItem>
+
+ <Message Text="Analyzing @(StyleCopFiles)" Importance="Low" />
+
+ <!-- Show list of what files should be excluded. checked. Take all Compile items, but exclude those that have
+ set ExcludeFromStyleCop=true or ExcludeFromSourceAnalysis=true. -->
+ <CreateItem Include="@(Compile)" Condition="('%(Compile.ExcludeFromStyleCop)' == 'true') or ('%(Compile.ExcludeFromSourceAnalysis)' == 'true')">
+ <Output TaskParameter="Include" ItemName="StyleCopExcludedFiles"/>
+ </CreateItem>
+
+ <ItemGroup>
+ <StyleCopFiles Remove="@(ExcludeFromStyleCop)" />
+ </ItemGroup>
+
+ <Message Text="Excluding @(StyleCopExcludedFiles)" Importance="Normal" Condition=" '@(StyleCopExcludedFiles)' != '' "/>
+
+ <!-- Run the StyleCop MSBuild task. -->
+ <StyleCopTask
+ ProjectFullPath="$(MSBuildProjectDirectory)"
+ SourceFiles="@(StyleCopFiles)"
+ AdditionalAddinPaths="@(StyleCopAdditionalAddinPaths)"
+ ForceFullAnalysis="$(StyleCopForceFullAnalysis)"
+ DefineConstants="$(DefineConstants)"
+ TreatErrorsAsWarnings="$(StyleCopTreatErrorsAsWarnings)"
+ CacheResults="$(StyleCopCacheResults)"
+ OverrideSettingsFile="$(StyleCopOverrideSettingsFile)"
+ OutputFile="$(StyleCopOutputFile)"
+ MaxViolationCount="$(StyleCopMaxViolationCount)"
+ />
+
+ <!-- Make output files cleanable -->
+ <CreateItem Include="$(StyleCopOutputFile)">
+ <Output TaskParameter="Include" ItemName="FileWrites"/>
+ </CreateItem>
+
+ <!-- Add the StyleCop.cache file to the list of files we've written - so they can be cleaned up on a Build Clean. -->
+ <CreateItem Include="StyleCop.Cache" Condition="'$(StyleCopCacheResults)' == 'true'">
+ <Output TaskParameter="Include" ItemName="FileWrites"/>
+ </CreateItem>
+ </Target>
+</Project>
diff --git a/tools/WebStack.settings.targets b/tools/WebStack.settings.targets
new file mode 100644
index 00000000..12f7a1fe
--- /dev/null
+++ b/tools/WebStack.settings.targets
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <PropertyGroup>
+ <!-- Define some basic reference paths -->
+ <WebStackRootPath>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\'))</WebStackRootPath>
+ <WebStackToolsPath>$(MSBuildThisFileDirectory)</WebStackToolsPath>
+
+ <!-- Define default configuration (so building from command line is consistent) -->
+ <Configuration Condition=" '$(Configuration)' == '' ">Release</Configuration>
+
+ <!-- Define basic output paths -->
+ <OutputPath Condition=" '$(OutputPath)' == '' ">$(WebStackRootPath)bin\$(Configuration)\</OutputPath>
+ <WebStackIntermediateOutputPath>$(WebStackRootPath)obj\$(Configuration)\$(MSBuildProjectName)\</WebStackIntermediateOutputPath>
+
+ <!-- Variables for output redirection (localization, signing, etc)-->
+ <Language Condition=" '$(Language)' == '' ">ENU</Language>
+ <LocalizedPath Condition=" '$(LocalizedPath)' == '' "></LocalizedPath>
+
+ <!-- StyleCop support -->
+ <StyleCopTreatErrorsAsWarnings Condition=" '$(StyleCopTreatErrorsAsWarnings)' == '' ">false</StyleCopTreatErrorsAsWarnings>
+ <StyleCopEnabled Condition=" '$(StyleCopEnabled)' == '' ">false</StyleCopEnabled>
+
+ <!-- VisualStudioVersion does not appear in 4.0 so its absence defaults to Dev10 -->
+ <VisualStudioVersion Condition = "'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
+
+ <!-- Target 4.0 for both VS2010 and VS 11 by default -->
+ <TargetFrameworkVersion Condition = "'$(TargetFrameworkVersion)' == ''">v4.0</TargetFrameworkVersion>
+
+ <!-- Enable NuGet package restore for all projects -->
+ <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">$(WebStackRootPath)</SolutionDir>
+ <RestorePackages>true</RestorePackages>
+
+ <!-- Use CustomAfterMicrosoftCommonTargets property (defined in Microsoft.Common.targets) to
+ inject post-Common targets files without requiring the inclusion -->
+ <CustomAfterMicrosoftCommonTargets>$(WebStackToolsPath)WebStack.targets</CustomAfterMicrosoftCommonTargets>
+ </PropertyGroup>
+
+ <!-- Everything is delay signed by default -->
+ <PropertyGroup>
+ <SignAssembly>true</SignAssembly>
+ <DelaySign>true</DelaySign>
+ <AssemblyOriginatorKeyFile>$(WebStackRootPath)\tools\35MSSharedLib1024.snk</AssemblyOriginatorKeyFile>
+ </PropertyGroup>
+</Project> \ No newline at end of file
diff --git a/tools/WebStack.targets b/tools/WebStack.targets
new file mode 100644
index 00000000..7f243de0
--- /dev/null
+++ b/tools/WebStack.targets
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$(WebStackToolsPath)WebStack.StyleCop.targets"/>
+ <Import Project="$(WebStackToolsPath)WebStack.NuGet.targets"/>
+</Project> \ No newline at end of file
diff --git a/tools/WebStack.tasks.targets b/tools/WebStack.tasks.targets
new file mode 100644
index 00000000..0b5fc11d
--- /dev/null
+++ b/tools/WebStack.tasks.targets
@@ -0,0 +1,47 @@
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <UsingTask TaskName="RegexReplace" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
+ <ParameterGroup>
+ <Files ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="true" />
+ <Find ParameterType="System.String" Required="true" />
+ <Replace ParameterType="System.String" Required="true" />
+ <WarnOnNoMatch ParameterType="System.Boolean" Required="false" />
+ </ParameterGroup>
+ <Task>
+ <Using Namespace="System" />
+ <Using Namespace="System.IO" />
+ <Using Namespace="System.Text" />
+ <Using Namespace="System.Text.RegularExpressions" />
+ <Using Namespace="Microsoft.Build.Framework" />
+ <Using Namespace="Microsoft.Build.Utilities" />
+ <Code Type="Fragment" Language="cs">
+ <![CDATA[
+ try {
+ Regex regex = new Regex(Find, RegexOptions.Multiline | RegexOptions.Compiled);
+
+ foreach (ITaskItem file in Files) {
+ string fullPath = Path.GetFullPath(file.ItemSpec);
+ string originalText = File.ReadAllText(fullPath);
+ bool matched = regex.IsMatch(originalText);
+
+ if (!matched) {
+ if (WarnOnNoMatch) {
+ Log.LogWarning("No matches for '{0}' in '{1}'.", Find, fullPath);
+ }
+ }
+ else {
+ File.SetAttributes(fullPath, File.GetAttributes(fullPath) & ~FileAttributes.ReadOnly);
+ File.WriteAllText(fullPath, regex.Replace(originalText, Replace), Encoding.UTF8);
+ }
+ }
+
+ return true;
+ }
+ catch (Exception ex) {
+ Log.LogErrorFromException(ex);
+ return false;
+ }
+ ]]>
+ </Code>
+ </Task>
+ </UsingTask>
+</Project> \ No newline at end of file
diff --git a/tools/WebStack.xunit.targets b/tools/WebStack.xunit.targets
new file mode 100644
index 00000000..d56dd888
--- /dev/null
+++ b/tools/WebStack.xunit.targets
@@ -0,0 +1,20 @@
+<Project ToolsVersion="4.0" DefaultTargets="Xunit" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$(MSBuildThisFileDirectory)WebStack.tasks.targets"/>
+
+ <!-- This is a separate MSBuild file so that we can survive upgrades of the xunit NuGet package
+ and also still work with NuGet Package Restore. -->
+ <ItemGroup>
+ <XunitMsBuildRunner Include="..\packages\**\xunit.runner.msbuild.dll"/>
+ </ItemGroup>
+ <UsingTask TaskName="Xunit.Runner.MSBuild.xunit" AssemblyFile="@(XunitMsBuildRunner)"/>
+
+ <Target Name="Xunit">
+ <xunit Assembly="$(TestAssembly)" Xml="$(XmlPath)"/>
+ <!-- Replace potentially illegal escaped characters -->
+ <RegexReplace
+ Files="$(XmlPath)"
+ Find="&amp;#x(?&lt;char&gt;[0-9A-Fa-f]+);"
+ Replace="\0x${char}"
+ WarnOnNoMatch="false"/>
+ </Target>
+</Project> \ No newline at end of file